Back to projects
Active Started May 2026

API Registry

Local registry of authoritative API and library docs so agents verify integrations against current sources.

TypeScript SQLite Claude Code Plugin Context7

Install

claude plugin marketplace add tyroneross/RossLabs-AI-Toolkit
claude plugin install api-registry@rosslabs-ai-toolkit

The Problem

Integration bugs often start before code is written. An agent remembers an old SDK shape, a renamed model, a deprecated environment variable, or a config path from another framework. The result is plausible code that fails late.

API Registry moves source verification to the front of the workflow. The registry stores approved documentation URLs, refresh logs, and lookup commands so build tools can ask “what docs should I trust for this service?” before writing the integration.

What I Built

API Registry gives coding agents a local source-of-truth map for API, library, and tool documentation. Before an agent configures Better Auth, Groq, Neon, Vercel, or another integration, it can look up the authoritative docs instead of relying on stale training data.

Impact

The registry is deliberately local and explicit. It does not try to be a universal search engine. It captures the docs this development environment trusts, keeps them refreshable, and makes source verification cheap enough to use every time an integration appears.

Architecture

API Registry is a deterministic TypeScript CLI plugin, not a research agent — every script in scripts/ and src/ is synchronous logic over SQLite plus a handful of plain HTTP calls (npm, PyPI, GitHub). The only place an LLM enters the loop is documentation curation, and that step is deliberately delegated to the calling Claude session rather than run inside the plugin (see Models below).

Two pipelines matter: registry population (turning a project’s dependencies into registered services) and doc lookup (routing a question to the right source and caching the answer). A third, smaller pipeline — refresh + cooldown — keeps version data current and gates freshly-published packages.

flowchart LR
  subgraph Populate["registry population — scan.ts, deterministic"]
    S1["project dirs: package.json,<br/>requirements.txt, .mcp.json, .env.example"] --> S2[scan.ts walkers]
    S2 --> S3[(scan_candidates table)]
    S3 -->|/api-registry:add, approve| S4[(services table<br/>SQLite WAL — registry.db)]
    S4 --> S5[export-yaml.ts]
    S5 --> S6[registry.yaml mirror]
  end

  subgraph Lookup["doc lookup — one question, traced"]
    Q["/api-registry:docs service query"] --> R[docs.ts route resolver]
    S4 -.getServiceByName.-> R
    R --> D{route}
    D -->|not_registered| STOP["stop — no training-data answer"]
    D -->|"answer_from_cache<br/>(last_checked < 7d)"| CACHE[read cached .md, no network]
    D -->|"verify_then_answer<br/>(last_checked > 7d)"| HC["hashCompare:<br/>GET + SHA-256 vs stored hash"]
    HC -->|unchanged| CACHE
    HC -->|changed| CUR["in-session Claude:<br/>WebFetch + curate section"]
    D -->|"fetch_and_cache<br/>(no cache yet)"| CUR
    D -->|"context7_fallback<br/>(no docs_url registered)"| C7[Context7 MCP query-docs]
    CUR --> WRITE[writeCachedDoc]
    WRITE --> DOCFILE[("~/.api-registry/docs/&lt;service&gt;/&lt;slug&gt;.md")]
    CACHE --> ANS["answer + Source: footer"]
    DOCFILE --> ANS
    C7 --> ANS
  end

  subgraph Refresh["refresh + cooldown — refresh.ts / cooldown.ts"]
    NPM[npm registry] --> RF[refresh.ts]
    PYPI[PyPI JSON API] --> RF
    GH[GitHub Releases API] --> RF
    RF --> S4
    S4 --> CD["cooldown.ts verdict<br/>7d supply-chain dwell"]
  end

  HOOK["SessionStart hook<br/>staleness.ts --marker"] -.detect only, never fetches.-> DOCFILE

Models — which, where, why:

Models: none. The plugin contains no model call anywhere in src/ or scripts/ — routing, hashing, cooldown math, and YAML export are all pure TypeScript. The one point where model reasoning is required — fetching a doc page and extracting the section relevant to a query — is explicitly assigned to the in-session Claude that invoked the command, per the repo’s own invariant: “Run an LLM call or background daemon to curate docs” is listed under Do not in the plugin’s CLAUDE.md. Context7 (mcp__plugin_context7_context7__query-docs) is called only as a fallback, when a service has no registered docs_url — it’s an external MCP tool the calling session reaches out to, not a model the plugin hosts or invokes internally.

Tools & infra — which, why:

  • better-sqlite3 (synchronous SQLite driver, WAL mode, ~/.api-registry/registry.db) — source of truth for services, scan_candidates, refresh_log, and doc_cache; chosen for a zero-daemon local store with a synchronous API, appropriate for a CLI that runs one command and exits.
  • YAML mirror (registry.yaml, written by export-yaml.ts) — human-readable, read-only reflection of the same rows; SQLite stays canonical per the repo’s invariant.
  • Markdown doc cache (~/.api-registry/docs/<service>/<slug>.md, YAML frontmatter with source_url/fetched_at/last_checked/content_hash) — payload store for curated doc content; git-ignored, greppable without a database client.
  • npm registry / PyPI JSON API / GitHub Releases API — plain HTTP metadata lookups (src/http.ts) that refresh.ts and scan.ts use for latest-version and publish-date data; no SDKs, just fetch.
  • Context7 MCP — fallback documentation source, reached only when a service has no docs_url registered.
  • tsx — runs the TypeScript scripts directly; commands are markdown files that shell out to tsx, no build step.
  • Claude Code plugin runtime — 8 slash commands, 3 skills (api-registry, docs-search, build-loop-bridge), and one SessionStart hook (session-start-staleness.sh) that are the plugin’s actual surface area.
  • node --test — the test runner (npm test), no separate test framework dependency.

How it works

  1. /api-registry:scan [--path <dir>] walks project directories (depth ≤ 3, skipping node_modules/.git) looking for package.json, requirements.txt, .mcp.json, and .env.example. Each dependency, MCP server, or matched env-var prefix (ENV_PREFIX_MAPGROQ, ANTHROPIC, STRIPE, NEON, etc.) becomes a row in scan_candidates with a proposed category and a confidence score (50 without network metadata, 80 with it).
  2. Approving a candidate — or running /api-registry:add <name> directly — writes a services row: docs_url, context7_id, package_ids, category (one of 12: auth, db, llm, infra, ui, obs, payments, email, storage, search, protocol, other), author_owned, maintenance_status. export-yaml.ts mirrors the table to registry.yaml.
  3. When an agent needs docs, /api-registry:docs <service> <query> runs scripts/docs.ts. It looks up the service, slugifies the query, and checks doc_cache for a matching (service_id, slug) entry — then emits one of five routes: not_registered, answer_from_cache, verify_then_answer, fetch_and_cache, or context7_fallback.
  4. not_registered stops the agent from answering off training data. answer_from_cache (fresh, last_checked < 7 days) reads the local .md file with no network call at all.
  5. verify_then_answer (cache present but stale) runs hashCompare: a GET on the registered docs_url plus a SHA-256 comparison against the stored content_hash. Unchanged content just bumps last_checked; changed content triggers re-curation.
  6. fetch_and_cache (no cache yet) and a changed-content verify_then_answer both hand off to the in-session Claude, which WebFetches the doc and curates the section relevant to the query — no external LLM call, per the repo’s own rule.
  7. The curated result is written via writeCachedDoc (frontmatter + body to ~/.api-registry/docs/<service>/<slug>.md) and indexed via insertDocCacheEntry in doc_cache. The answer returns with a Source: <url> (cached/verified <date>) footer.
  8. context7_fallback fires only when a service has no docs_url registered; it routes to Context7 MCP’s query-docs instead.
  9. /api-registry:refresh [<name> | --all | --stale] hits npm, PyPI, or GitHub Releases for the latest version and its publish date, logs the transition to refresh_log, and updates latest_version_released_at.
  10. cooldown.ts turns that date into an install verdict: a package released under 7 days ago is install_blocked: true (supply-chain dwell time) unless it’s author_owned (matched against the user’s own npm scopes/projects in ~/.api-registry/owned.json); an unknown release date fails open rather than blocking.
  11. A SessionStart hook runs staleness.ts --marker on every session start — detect-only, it writes ~/.api-registry/staleness.json and, only if any cached doc exceeds the 7-day threshold, emits one line nudging /api-registry:refresh --stale. It never fetches or curates.

Tech stack

TypeScript on Node ≥ 20, run directly via tsx (no build step). better-sqlite3 (WAL mode) is the sole datastore — registry.db for services/candidates/refresh log/doc-cache index, mirrored read-only to registry.yaml. Curated doc bodies live as frontmatter’d markdown on disk, not in SQLite. No web framework, no ORM, no vector store: src/http.ts is a thin fetch wrapper over the npm registry, PyPI JSON API, and GitHub Releases API. Ships as a Claude Code plugin (commands + skills + one SessionStart hook); Context7 MCP is the only external service dependency, used solely as a fallback when a service has no registered docs_url. Tests run on Node’s built-in node --test.

Worked Example

Before wiring up Better Auth in a project, an agent routes the question through the registry instead of answering from training data:

/api-registry:docs better-auth "what should I trust for session config?"

Illustrative shape of the routed response (exact fields vary; the real script also returns stale_threshold_days and, on a cache hit, content_hash):

{
  "route": "fetch_and_cache",
  "service": "better-auth",
  "display_name": "Better Auth",
  "docs_url": "<registered docs_url>",
  "context7_id": null,
  "file_path": "~/.api-registry/docs/better-auth/what-should-i-trust-for-session-config.md"
}

The match comes from a service the registry already knows about — Better Auth, Groq, Neon, and Vercel are among the 26 services seeded in this environment’s registry. Lookup latency and refresh-cycle timing aren’t measured anywhere in this write-up, so both stay flagged rather than estimated.