#!/usr/bin/env python3 """Reference Discovery CLI — manage source discovery and manifests. Claude performs the actual web search. This script handles: - Receiving search results and creating manifests - Deduplicating URLs against the existing repository - Registering new sources in the database Usage: python discover.py create-manifest --topic "prompt engineering" --output manifest.json < urls.json python discover.py dedup --manifest manifest.json [--output deduped.json] python discover.py register-source --name "Anthropic Docs" --type official_docs --url "https://docs.anthropic.com" --tier tier1_official --vendor anthropic python discover.py list-sources [--vendor anthropic] """ import json import sys from datetime import datetime from pathlib import Path import click from rich.console import Console from rich.table import Table from refcurator.db import db_session from refcurator.manifest import write_manifest, read_manifest, dedup_manifest_urls from refcurator.models import Manifest, ManifestURL from refcurator.utils import normalize_url console = Console() @click.group() def cli(): """Reference Discovery — manage source manifests.""" pass @cli.command("create-manifest") @click.option("--topic", required=True, help="Discovery topic") @click.option("--output", required=True, type=click.Path(), help="Output manifest path") @click.option("--input", "input_file", type=click.Path(exists=True), help="Input JSON file with URL entries (or pipe via stdin)") def create_manifest(topic, output, input_file): """Create a manifest from discovered URLs. Input format (JSON array): [ {"url": "https://...", "title": "...", "credibility_score": 0.85, "source_type": "official_docs"}, ... ] """ if input_file: data = json.loads(Path(input_file).read_text()) elif not sys.stdin.isatty(): data = json.load(sys.stdin) else: console.print("[red]Error:[/red] Provide URLs via --input file or stdin") sys.exit(1) urls = [ManifestURL(**entry) for entry in data] # Normalize and deduplicate seen = {} for u in urls: norm = normalize_url(u.url) if norm not in seen or (u.credibility_score or 0) > (seen[norm].credibility_score or 0): seen[norm] = u unique_urls = list(seen.values()) manifest = Manifest( discovery_date=datetime.now().isoformat(), topic=topic, total_urls=len(unique_urls), urls=unique_urls, ) out_path = Path(output) write_manifest(manifest, out_path) console.print(f"[green]Created manifest:[/green] {len(unique_urls)} URLs → {output}") @cli.command() @click.option("--manifest", required=True, type=click.Path(exists=True), help="Manifest to dedup") @click.option("--output", type=click.Path(), help="Output path (defaults to overwriting input)") def dedup(manifest, output): """Deduplicate manifest URLs against existing repository.""" m = read_manifest(Path(manifest)) original_count = len(m.urls) with db_session() as db: existing = db.fetch_all("SELECT url FROM documents WHERE url IS NOT NULL") existing_urls = {r["url"] for r in existing} deduped = dedup_manifest_urls(m, existing_urls) removed = original_count - len(deduped.urls) out_path = Path(output) if output else Path(manifest) write_manifest(deduped, out_path) console.print( f"[green]Deduped:[/green] {original_count} → {len(deduped.urls)} URLs " f"({removed} duplicates removed)" ) @cli.command("register-source") @click.option("--name", required=True, help="Source name") @click.option("--type", "source_type", required=True, type=click.Choice(["official_docs", "engineering_blog", "research_paper", "github_repo", "community_guide", "pdf_document", "api_reference"])) @click.option("--url", required=True, help="Base URL") @click.option("--tier", default="tier3_community", type=click.Choice(["tier1_official", "tier2_verified", "tier3_community"])) @click.option("--vendor", help="Vendor name (e.g., anthropic, openai)") def register_source(name, source_type, url, tier, vendor): """Register a new source in the repository.""" with db_session() as db: # Check for existing source with same base_url existing = db.fetch_one( "SELECT source_id, source_name FROM sources WHERE base_url = %s", (url,), ) if existing: console.print( f"[yellow]Already registered:[/yellow] source_id={existing['source_id']} " f"({existing['source_name']})" ) return source_id = db.insert_returning_id( """INSERT INTO sources (source_name, source_type, base_url, credibility_tier, vendor) VALUES (%s, %s, %s, %s, %s)""", (name, source_type, url, tier, vendor), ) console.print(f"[green]Registered:[/green] source_id={source_id} — {name}") click.echo(json.dumps({"source_id": source_id, "name": name, "url": url})) @cli.command("list-sources") @click.option("--vendor", help="Filter by vendor") @click.option("--tier", type=click.Choice(["tier1_official", "tier2_verified", "tier3_community"]), help="Filter by credibility tier") def list_sources(vendor, tier): """List registered sources.""" with db_session() as db: if vendor: rows = db.fetch_all( "SELECT * FROM sources WHERE vendor = %s ORDER BY credibility_tier", (vendor,), ) elif tier: rows = db.fetch_all( "SELECT * FROM sources WHERE credibility_tier = %s ORDER BY vendor", (tier,), ) else: rows = db.fetch_all("SELECT * FROM sources ORDER BY credibility_tier, vendor") table = Table(title="Registered Sources") table.add_column("ID", style="cyan") table.add_column("Name") table.add_column("Type") table.add_column("Tier") table.add_column("Vendor") table.add_column("URL") for r in rows: table.add_row( str(r.get("source_id", "")), str(r.get("source_name", "")), str(r.get("source_type", "")), str(r.get("credibility_tier", "")), str(r.get("vendor", "")), str(r.get("base_url", ""))[:40], ) console.print(table) if __name__ == "__main__": cli()