Skip to content

Latest commit

 

History

History
178 lines (134 loc) · 11.7 KB

CLAUDE.md

File metadata and controls

178 lines (134 loc) · 11.7 KB

agent-root CLI: Project Conventions

The CLI client for the AgentRoot protocol. Standalone, MIT-licensed. No publish pipeline yet: release strategy (registry, signing, provenance, cadence) is still being decided. Do not add a release workflow, a prepublishOnly hook, or any other publish-adjacent automation without explicit sign-off.

Project layout

src/
  index.ts                  Entry point: parseArgs + command router
  cli/                      Argument parsing, fatal exits, spinners, prompts
    parse-args.ts           POSIX argv parser (kebab/camel, --key=value, --, --no-X)
    fatal.ts                Single error sink with JSON envelope + sysexits codes
    exit-codes.ts           Sysexits-style EXIT constants + exitCodeName()
    colors.ts               picocolors wrapper, honors NO_COLOR + --no-color + TTY
    streams.ts              stderr-routing helpers (note, comment) + --quiet wiring
    env-preamble.ts         Mutates process.env before picocolors initialises
    spinner.ts              nanospinner wrapper that respects quiet + non-TTY
    suggest.ts              Levenshtein "Did you mean?" for unknown commands
    confirm.ts              Inquirer-backed yes/no with --yes / CI auto-confirm
  commands/                 One file per CLI command (thin handlers only)
    resolve.ts              DNS lookup -> manifest fetch -> records
    search.ts               /api/records (primary) + legacy fallbacks
    install.ts              install / install-interactive helpers
    list.ts                 read ~/.agentroot/installed.json
    update.ts               re-fetch + hash-compare an installed record
    uninstall.ts            remove an installed record + tool symlinks
    init.ts                 scaffold .well-known/agentroot.json
    validate.ts             validateManifest against a local JSON file
    config.ts               ~/.agentroot/config.json get/set/unset
    health.ts               /api/health reader (status + db)
    manifests.ts            /api/manifests paginated browser
    collections.ts          /api/collections list + detail
    submit.ts               POST /api/submit + DNS probe + TXT instructions
    version.ts              version subcommand + printShortVersion()
    completion.ts           Static bash/zsh/fish/pwsh completion scripts
    help.ts                 Per-command help pages (15 commands + helpCompletion in completion.ts)
  constants/                Module-level lookup tables (RECORD_TYPES, etc.)
  services/                 Stateful work, grouped by concern
    config/                 ~/.agentroot/config.json read/write
    dns/                    DNS TXT resolution + manifest URL parsing
    http/                   fetch wrapper + User-Agent + postJSON (for submit)
    install/                Skill / MCP / agent / a2a install pipeline
  types/                    Shared TypeScript interfaces
  utils/                    Pure helpers (formatRecord, hashContent)
bin/agentroot.ts            Shebang entry compiled to dist/bin/agentroot.js
tests/                      vitest unit tests (mirror src/ layout)
docs/screenshots/           README screenshots (generated by scripts/)
scripts/                    Build-time tooling (screenshots, etc.)
.planning/                  Local-only working notes (gitignored)
vendor/                     Workaround tarball for stale npm core (gitignored)

Search backend

commands/search.ts calls /api/records first (the same paginated endpoint the web UI uses). Earlier versions called /api/discover and looked for data.results, a field that does not exist in that endpoint's response shape, the bug silently dropped every agent/MCP/A2A hit. The legacy searchWithFallback chain (find-skills -> manifest probe by TLD) is preserved but only fires when page 1 of /api/records returns zero rows. --json now returns the full envelope (results, total, page, pages, limit) instead of a bare array.

Conventions

Files: keep under 300 lines; if a file grows past that, split per responsibility.

Imports, then types, then constants, then exports, then private helpers. Constants and lookup tables go at module top, not buried inside functions. Functions read top-down: callers above callees.

Conditionals: if/else-if chains over 2 branches should be replaced with a Record<key, handler> map or strategy dispatch. See commands/install.ts (RECORD_TYPE_HANDLERS) for the canonical pattern.

const by default, let only when mutation is required. No var. One variable per declaration (no let a = 1, b = 2).

No any. Use unknown + type guards, or a real interface. as <Type> casts should be rare; prefer runtime checks.

Document every empty catch {} with a 2 or 3 line comment explaining what condition is being swallowed and why that's the right behavior.

Behavior parity with the monorepo CLI. The standalone is a 1:1 functional copy of packages/cli/ from d3-inc/agentroot. Output must match byte-for-byte (modulo timestamps).

Commits are authored by the human who wrote them. Do NOT add Co-Authored-By: trailers for Claude, Copilot, Cursor, ChatGPT, or any other AI assistant. Do NOT add "🤖 Generated with..." footers. The developer or contributor sending the PR is the sole author; AI tooling that helped is not co-credited. This applies to every commit and every PR, no exceptions. (DCO sign-off via git commit -s is still required and adds only the contributor's own Signed-off-by: line.)

Exit codes are sysexits. Every fatal() call must pass an EXIT.* constant from src/cli/exit-codes.ts (the single source of truth: OK=0, GENERIC=1, USAGE=2, NOINPUT=66, NOHOST=68, UNAVAILABLE=69, PROTOCOL=76, NOPERM=77, CONFIG=78). The default is GENERIC (1); pick the more specific code if it applies. JSON errors put the symbolic name in error.code via exitCodeName(). Document new exit codes in per-command help pages and in the README's "Exit codes" table.

Streams: stdout is data, stderr is conversation. Spinners, comments, progress notes, prompts all go to stderr. Only pipeable output (results tables, JSON envelopes) goes to stdout. cmd --json | jq must work without 2>/dev/null. Use note() / comment() from src/cli/streams.ts for any non-data line; never raw console.log for chatter.

Tests

vitest + setup file at tests/setup.ts that silences console.log/console.error.

pnpm test              # one-shot run
pnpm test:watch        # re-run on save
pnpm test:coverage     # html coverage report in ./coverage/

Every PR that adds or changes runtime behavior must include at least one test that fails without the change. See CONTRIBUTING.md for carve-outs (docs:, chore(deps):, pure refactors).

Build & verify

pnpm install           # frozen-lockfile by default in CI
pnpm build             # tsc to dist/
pnpm type-check        # tsc --noEmit
pnpm test              # 45/45 must pass

Smoke test recipe lives in TESTING.md.

Audit history

  • .planning/PERF-AUDIT.md (local-only): performance review + applied fixes. Read before adding new I/O code paths.
  • .planning/CODE-QUALITY-AUDIT.md (local-only): readability audit referencing Google TS Style Guide, Clean Code TypeScript, refactoring.guru. Documents 25 findings and which were applied vs. deferred.
  • .planning/audit-trail.md (local-only): per-task log of the original extraction work.

Keeping docs in sync with code

Whenever you change CLI surface, you must update docs in the same commit. Surface = anything a user sees or scripts against:

  • A new command, flag, alias, or environment variable → add it to:
    • README.md (Commands table, Help section verbatim text, Usage subsection if it deserves one)
    • src/index.ts showHelp() (the help text users see)
    • CLAUDE.md Project layout if it's a new file
    • TESTING.md smoke recipe
  • A change to command output (column count, JSON envelope, error messages) → regenerate the relevant screenshot:
    pnpm run screenshots
    The recipe is in scripts/capture-screenshots.mjs. Each PNG is committed to docs/screenshots/.
  • A change to default behavior (e.g. switching the search backend) → call it out in ## [Unreleased] of CHANGELOG.md.
  • A change to required Node version, supported tools, or platforms → update README's Install section and package.json engines.

Rule of thumb: if a user could be surprised by your change without reading the diff, the README must reflect it.

Don't ship references that point at something that doesn't exist yet

A class of bug we've already shipped twice and reverted: changing a primary thing (package name, version, repo URL) and dragging related external references along without checking they resolve. Concrete examples:

  • README badges that fetch from npm — broke when we renamed agent-root → @agent-root/cli before the scoped package was published.
  • README "Version & environment" sample that hardcoded agent-root 0.2.0 — went stale the moment package.json.version bumped.
  • Issue-template config.yml contact link to GitHub Discussions — pretended Discussions was live while it was disabled on the repo.

When you change any of:

  • package.json name / version / repository.url / bugs.url
  • The repo's GitHub URL (org or repo rename)
  • Any feature toggle on GitHub (Discussions, Wiki, Pages)
  • Any deployment URL

…then grep for every reference to the old value in README.md, CHANGELOG.md, .github/, and the rest of the docs, and verify each new reference resolves to a real thing (or document why it doesn't yet). For external URLs (badges, package pages, registry endpoints), either confirm the URL returns 200 or replace with a static value until the dependency is published.

Static badge fallback when an external service isn't ready yet:

[![version](https://img.shields.io/badge/version-X.Y.Z-blue.svg)](https://github.com/owner/repo/releases)

is always better than a dynamic badge that renders "not found".

Single source of truth

  • Version: package.json version. Read at runtime via src/services/http/package-info.ts. Never hardcode a version anywhere else.
  • API base URL: @agent-root/core's API_BASE constant. User override via ~/.agentroot/config.json api-url or AGENTROOT_API_BASE env var. Never hardcode agentroot.io in CLI source.
  • Record types: src/constants/record-types.ts. Use RECORD_TYPES and labelForType everywhere; do not repeat the string literals.
  • Tool names: claude, cursor, codex, gemini, agents. These come from @agent-root/core detectTools(). Do not redefine the set in the CLI.
  • Skill dir paths: @agent-root/core resolveToolDir(). Do not hand-write ~/.claude/skills/ etc.

If you add a new repeated string (x appears in 3+ files), extract it to src/constants/.

Notes for AI assistants

When making changes:

  1. Read the file before editing it; preserve its existing structural pattern (imports, types, constants, exports, then private helpers).
  2. Run pnpm build && pnpm test after every batch of changes. Don't accumulate untested edits.
  3. If you add a new command, mirror an existing one in commands/ (e.g., commands/resolve.ts is a good template).
  4. New helpers go in cli/, utils/, or the appropriate services/<concern>/ folder, never in commands/.
  5. The vendor/agent-root-core-0.2.0.tgz is a local workaround because the published @agent-root/core@0.2.0 is stale. When @agent-root/core next publishes (with the manifest/install-state/records APIs), switch package.json back to a semver range and git rm the vendor tarball.
  6. Don't add npm dependencies without a clear need. Current runtime deps: @agent-root/core, @inquirer/prompts, nanospinner, picocolors. Dev deps: vitest, playwright, ansi-to-html, typescript, @types/node.