diff --git a/.agents/rules/codemap.md b/.agents/rules/codemap.md index e7c0b304..e155cfb8 100644 --- a/.agents/rules/codemap.md +++ b/.agents/rules/codemap.md @@ -25,6 +25,7 @@ A local database (default **`.codemap.db`**) indexes structure: symbols, imports | Save / diff a baseline | — | `bun src/index.ts query --save-baseline -r visibility-tags` then `… --json --baseline -r visibility-tags` | | List / drop baselines | — | `bun src/index.ts query --baselines` · `bun src/index.ts query --drop-baseline ` | | Per-delta audit | — | `bun src/index.ts audit --json --baseline base` (auto-resolves `base-files` / `base-dependencies` / `base-deprecated`) | +| MCP server (for agent hosts) | — | `bun src/index.ts mcp` — JSON-RPC on stdio; one tool per CLI verb. See **MCP** section below. | **Recipe `actions`:** with **`--json`**, recipes that define an `actions` template append it to every row (kebab-case verb + description — e.g. `fan-out` → `review-coupling`). Under `--baseline`, actions attach to the **`added`** rows only. Inspect via **`--recipes-json`**. Ad-hoc SQL never carries actions. @@ -32,6 +33,16 @@ A local database (default **`.codemap.db`**) indexes structure: symbols, imports **Audit (`bun src/index.ts audit`)**: structural-drift command; emits `{head, deltas: {files, dependencies, deprecated}}` (each delta carries its own `base` metadata). Reuses B.6 baselines as the snapshot source. Two CLI shapes — `--baseline ` auto-resolves `-files` / `-dependencies` / `-deprecated`; `---baseline ` is the explicit per-delta override. v1 ships no `verdict` / threshold config — consumers compose `--json` + `jq` for CI exit codes. Auto-runs an incremental index before the diff (use `--no-index` to skip for frozen-DB CI). +**MCP server (`bun src/index.ts mcp`)**: stdio MCP (Model Context Protocol) server — agents call codemap as JSON-RPC tools instead of shelling out to the CLI on every read. v1 ships one tool per CLI verb plus four lazy-cached resources: + +- **Tools:** `query` / `query_batch` / `query_recipe` / `audit` / `save_baseline` / `list_baselines` / `drop_baseline` / `context` / `validate`. Snake_case keys (Codemap convention matching MCP spec examples + reference servers — spec is convention-agnostic; CLI stays kebab). +- **`query_batch` (MCP-only):** N statements in one round-trip. Items are `string | {sql, summary?, changed_since?, group_by?}` — string form inherits batch-wide flag defaults, object form overrides on a per-key basis. Per-statement errors are isolated. +- **`save_baseline` (polymorphic):** one tool, `{name, sql? | recipe?}` with runtime exclusivity check (mirrors the CLI's single `--save-baseline=` verb). +- **Resources:** `codemap://recipes` (catalog), `codemap://recipes/{id}` (one recipe), `codemap://schema` (live DDL from `sqlite_schema`), `codemap://skill` (bundled SKILL.md text). Lazy-cached on first `read_resource`. +- **Output shape uniformity:** every tool returns the JSON envelope its CLI counterpart's `--json` would print — no re-mapping. Schema additions to the CLI envelope propagate to MCP automatically. + +For developing the MCP server itself: `src/cli/cmd-mcp.ts` (CLI shell) + `src/application/mcp-server.ts` (engine). See [`docs/architecture.md` § MCP wiring](../../docs/architecture.md#cli-usage). + After **`bun run build`**, **`node dist/index.mjs`** matches the published **`codemap`** binary (same flags). **`bun link`** / global **`codemap`** also work when testing the packaged CLI. Index another project: **`--root /path/to/repo`**, or set **`CODEMAP_ROOT`** or **`CODEMAP_TEST_BENCH`** (e.g. in **`.env`** — see [docs/benchmark.md § Indexing another project](../../docs/benchmark.md#indexing-another-project)). Full rebuild: **`--full`**. Targeted re-index: **`--files path/to/a.ts path/to/b.tsx`**. diff --git a/.agents/skills/codemap/SKILL.md b/.agents/skills/codemap/SKILL.md index 191eea0d..16006caa 100644 --- a/.agents/skills/codemap/SKILL.md +++ b/.agents/skills/codemap/SKILL.md @@ -53,6 +53,29 @@ Replace placeholders (`'...'`) with your module path, file glob, or symbol name. Each emitted delta carries its own `base` metadata so mixed-baseline audits are first-class. `--summary` collapses each delta to `{added: N, removed: N}`. `--no-index` skips the auto-incremental-index prelude (default is to re-index first so `head` reflects current source). v1 ships no `verdict` / threshold config — `codemap audit --json | jq -e '.deltas.dependencies.added | length <= 50'` is the CI exit-code idiom until v1.x ships native thresholds. Each delta pins a canonical SQL projection and validates baseline column-set membership before diffing — schema-bump-resilient (extras dropped, missing columns surface a clean re-save command). +**MCP server (`bun src/index.ts mcp`)** — separate top-level command that exposes the entire CLI surface to agent hosts (Claude Code, Cursor, Codex, generic MCP clients) as JSON-RPC tools over stdio. Eliminates the bash round-trip on every agent call. Bootstrap once at server boot; tool handlers reuse the existing engine entry-points (`executeQuery`, `runAudit`, etc.) so output shape is verbatim from each tool's CLI counterpart's `--json` envelope. + +**Tools (snake_case keys — Codemap convention matching MCP spec examples + reference servers; spec is convention-agnostic. CLI stays kebab; translation lives at the MCP-arg layer.):** + +- **`query`** — one SQL statement. Args: `{sql, summary?, changed_since?, group_by?}`. Same envelope as `codemap query --json`. +- **`query_batch`** — MCP-only, no CLI counterpart. Args: `{statements: (string | {sql, summary?, changed_since?, group_by?})[], summary?, changed_since?, group_by?}`. Items are bare SQL strings (inherit batch-wide flag defaults) or objects (override on a per-key basis). Output is N-element array; per-element shape mirrors single-`query`'s output for that statement's effective flag set. Per-statement errors are isolated — failed statements return `{error}` in their slot; siblings still execute. SQL-only (no `recipe` polymorphism in items). +- **`query_recipe`** — `{recipe, summary?, changed_since?, group_by?}`. Resolves the recipe id to SQL + per-row actions, then executes like `query`. Unknown recipe id returns a structured `{error}` pointing at the `codemap://recipes` resource. +- **`audit`** — `{baseline_prefix?, baselines?: {files?, dependencies?, deprecated?}, summary?, no_index?}`. Composes per-delta baselines into the `{head, deltas}` envelope. Auto-runs incremental index unless `no_index: true`. +- **`save_baseline`** — polymorphic `{name, sql? | recipe?}` with runtime exclusivity check (mirrors the CLI's single `--save-baseline=` verb). Pass exactly one of `sql` or `recipe`. +- **`list_baselines`** — no args; returns the array `codemap query --baselines --json` would print. +- **`drop_baseline`** — `{name}`. Returns `{dropped: }` on success or `isError` if the name doesn't exist. +- **`context`** — `{compact?, intent?}`. Returns the project-bootstrap envelope (codemap version, schema version, file count, language breakdown, hubs, sample markers). Designed for agent session-start — one call replaces 4-5 `query` calls. +- **`validate`** — `{paths?: string[]}`. Compares on-disk SHA-256 to indexed `files.content_hash`; empty `paths` validates everything. Returns rows with status (`ok`/`stale`/`missing`/`unindexed`). + +**Resources (lazy-cached on first `read_resource`; constant for server-process lifetime):** + +- **`codemap://recipes`** — full catalog JSON (same as `--recipes-json`). +- **`codemap://recipes/{id}`** — single recipe `{id, description, sql, actions?}`. Replaces `--print-sql `. +- **`codemap://schema`** — DDL of every table in `.codemap.db` (queried live from `sqlite_schema`). +- **`codemap://skill`** — full text of bundled `templates/agents/skills/codemap/SKILL.md`. Agents that don't preload the skill at session start can fetch it here. + +**Implementation:** `src/cli/cmd-mcp.ts` (CLI shell — argv + lifecycle) + `src/application/mcp-server.ts` (engine — tool registry, resource handlers). Mirrors the `cmd-audit.ts ↔ audit-engine.ts` seam. `--changed-since` git lookups are memoised per `(root, ref)` pair across batch items so a `query_batch` of N items sharing the same ref does one git invocation, not N. v1.x backlog: `codemap serve` (HTTP API) reuses the same tool taxonomy + output shape. + **Determinism:** Bundled recipes use stable secondary **`ORDER BY`** tie-breakers (and ordered inner **`LIMIT`** samples where applicable). Prefer **`--recipe`** over pasting SQL when you need the maintained ordering. **Canonical SQL** is **`src/cli/query-recipes.ts`** (`QUERY_RECIPES`). The blocks below match **`fan-out`** and **`fan-out-sample`** in **`QUERY_RECIPES`**; other recipes align with “Conditional aggregation”, “Codebase statistics”, and component sections later in this skill. diff --git a/.changeset/agent-transports-mcp-scaffold.md b/.changeset/agent-transports-mcp-scaffold.md new file mode 100644 index 00000000..eac82419 --- /dev/null +++ b/.changeset/agent-transports-mcp-scaffold.md @@ -0,0 +1,35 @@ +--- +"@stainless-code/codemap": minor +--- + +feat(mcp): `codemap mcp` — Model Context Protocol server (agent-transports v1) + +Adds the `codemap mcp` top-level command — boots an MCP server over +stdio so agent hosts (Claude Code, Cursor, Codex, generic MCP clients) +call codemap as JSON-RPC tools instead of shelling out per query. +Eliminates the bash round-trip on every agent invocation. + +Surface (one tool per CLI verb plus `query_batch`, all snake_case): + +- `query`, `query_batch`, `query_recipe`, `audit`, `save_baseline`, + `list_baselines`, `drop_baseline`, `context`, `validate` +- Resources: `codemap://recipes`, `codemap://recipes/{id}`, + `codemap://schema`, `codemap://skill` (lazy-cached) + +`query_batch` is MCP-only — N statements in one round-trip with +batch-wide-defaults + per-statement-overrides (items are +`string | {sql, summary?, changed_since?, group_by?}`). Per-statement +errors are isolated. `save_baseline` ships as one polymorphic tool +(`{name, sql? | recipe?}` with runtime exclusivity check) mirroring +the CLI's single `--save-baseline=` verb. + +Output shape is verbatim from each tool's CLI counterpart's `--json` +envelope (no re-mapping). Bootstrap once at server boot; tool +handlers reuse existing engine entry-points (`executeQuery`, +`runAudit`, etc.) — no duplicate business logic. + +New dep: `@modelcontextprotocol/sdk`. + +HTTP API (`codemap serve`) stays in roadmap backlog; design points +(tool taxonomy + output shape) are reserved in `docs/architecture.md +§ MCP wiring` so HTTP inherits them when a concrete consumer asks. diff --git a/README.md b/README.md index 76464760..fe43c409 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,13 @@ codemap query --recipes-json codemap query --print-sql fan-out # `components-by-hooks` ranks by hook count without SQLite JSON1 (comma-based count on the stored JSON array). +# MCP server (Model Context Protocol) — for agent hosts (Claude Code, Cursor, Codex, generic MCP clients) +codemap mcp # JSON-RPC on stdio; one tool per CLI verb plus query_batch +# Tools: query, query_batch (MCP-only — N statements in one round-trip), query_recipe, audit, +# save_baseline, list_baselines, drop_baseline, context, validate +# Resources: codemap://recipes, codemap://recipes/{id}, codemap://schema, codemap://skill (lazy-cached) +# Output shape verbatim from `--json` envelopes (no re-mapping). Snake_case throughout. + # Another project codemap --root /path/to/repo --full diff --git a/bun.lock b/bun.lock index e77861cc..062d7094 100644 --- a/bun.lock +++ b/bun.lock @@ -6,6 +6,7 @@ "name": "@stainless-code/codemap", "dependencies": { "@clack/prompts": "^1.2.0", + "@modelcontextprotocol/sdk": "^1.29.0", "better-sqlite3": "^12.9.0", "lightningcss": "^1.32.0", "oxc-parser": "^0.127.0", @@ -90,6 +91,8 @@ "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], + "@hono/node-server": ["@hono/node-server@1.19.14", "", { "peerDependencies": { "hono": "^4" } }, "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw=="], + "@inquirer/external-editor": ["@inquirer/external-editor@1.0.3", "", { "dependencies": { "chardet": "^2.1.1", "iconv-lite": "^0.7.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA=="], "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], @@ -104,6 +107,8 @@ "@manypkg/get-packages": ["@manypkg/get-packages@1.1.3", "", { "dependencies": { "@babel/runtime": "^7.5.5", "@changesets/types": "^4.0.1", "@manypkg/find-root": "^1.1.0", "fs-extra": "^8.1.0", "globby": "^11.0.0", "read-yaml-file": "^1.1.0" } }, "sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A=="], + "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="], "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], @@ -332,6 +337,12 @@ "@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20260427.1", "", { "os": "win32", "cpu": "x64" }, "sha512-Ut4Hncq1IuSeNIfcPs1s719j8H3ZA+ogsJ53W3s/Wy1UF5BIhu5Hkspdc7TzGgJgYqGJKo/+pr4vsRnbBPdWgQ=="], + "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], + + "ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], + + "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], + "ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="], "ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="], @@ -360,14 +371,22 @@ "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], + "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], "bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="], + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + "cac": ["cac@7.0.0", "", {}, "sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ=="], + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + "chardet": ["chardet@2.1.1", "", {}, "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ=="], "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], @@ -380,16 +399,30 @@ "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], + "content-disposition": ["content-disposition@1.1.0", "", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="], + + "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], + + "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + + "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], + + "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="], + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], "dataloader": ["dataloader@1.4.0", "", {}, "sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="], "defu": ["defu@6.1.7", "", {}, "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ=="], + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + "detect-indent": ["detect-indent@6.1.0", "", {}, "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA=="], "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], @@ -400,32 +433,60 @@ "dts-resolver": ["dts-resolver@2.1.3", "", { "peerDependencies": { "oxc-resolver": ">=11.0.0" }, "optionalPeers": ["oxc-resolver"] }, "sha512-bihc7jPC90VrosXNzK0LTE2cuLP6jr0Ro8jk+kMugHReJVLIpHz/xadeq3MhuwyO4TD4OA3L1Q8pBBFRc08Tsw=="], + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], "empathic": ["empathic@2.0.0", "", {}, "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA=="], + "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], "enquirer": ["enquirer@2.4.1", "", { "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" } }, "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ=="], "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], + "eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], + "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], + + "eventsource-parser": ["eventsource-parser@3.0.8", "", {}, "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ=="], + "expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="], + "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], + + "express-rate-limit": ["express-rate-limit@8.4.1", "", { "dependencies": { "ip-address": "10.1.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-NGVYwQSAyEQgzxX1iCM978PP9AdO/hW93gMcF6ZwQCm+rFvLsBH6w4xcXWTcliS8La5EPRN3p9wzItqBwJrfNw=="], + "extendable-error": ["extendable-error@0.1.7", "", {}, "sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], "fast-string-truncated-width": ["fast-string-truncated-width@1.2.1", "", {}, "sha512-Q9acT/+Uu3GwGj+5w/zsGuQjh9O1TyywhIwAxHudtWrgF09nHOPrvTLhQevPbttcxjr/SNN7mJmfOw/B1bXgow=="], "fast-string-width": ["fast-string-width@1.1.0", "", { "dependencies": { "fast-string-truncated-width": "^1.2.0" } }, "sha512-O3fwIVIH5gKB38QNbdg+3760ZmGz0SZMgvwJbA1b2TGXceKE6A2cOlfogh1iw8lr049zPyd7YADHy+B7U4W9bQ=="], + "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], + "fast-wrap-ansi": ["fast-wrap-ansi@0.1.6", "", { "dependencies": { "fast-string-width": "^1.1.0" } }, "sha512-HlUwET7a5gqjURj70D5jl7aC3Zmy4weA1SHUfM0JFI0Ptq987NH2TwbBFLoERhfwk+E+eaq4EK3jXoT+R3yp3w=="], "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], @@ -436,14 +497,26 @@ "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], + "find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], + + "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], + "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], "fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="], + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + "get-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="], + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + "get-tsconfig": ["get-tsconfig@4.13.7", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q=="], "github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="], @@ -452,10 +525,20 @@ "globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" } }, "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="], + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="], + + "hono": ["hono@4.12.16", "", {}, "sha512-jN0ZewiNAWSe5khM3EyCmBb250+b40wWbwNILNfEvq84VREWwOIkuUsFONk/3i3nqkz7Oe1PcpM2mwQEK2L9Kg=="], + "hookable": ["hookable@6.1.1", "", {}, "sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ=="], + "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], + "human-id": ["human-id@4.1.3", "", { "bin": { "human-id": "dist/cli.js" } }, "sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q=="], "husky": ["husky@9.1.7", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="], @@ -472,6 +555,10 @@ "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], + "ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="], + + "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], "is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="], @@ -480,16 +567,24 @@ "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], + "is-subdir": ["is-subdir@1.2.0", "", { "dependencies": { "better-path-resolve": "1.0.0" } }, "sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw=="], "is-windows": ["is-windows@1.0.2", "", {}, "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA=="], "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + "jose": ["jose@6.2.3", "", {}, "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw=="], + "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="], + "jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], @@ -526,10 +621,20 @@ "log-update": ["log-update@6.1.0", "", { "dependencies": { "ansi-escapes": "^7.0.0", "cli-cursor": "^5.0.0", "slice-ansi": "^7.1.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w=="], + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], + + "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + + "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], + "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], @@ -540,14 +645,24 @@ "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="], + "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + "node-abi": ["node-abi@3.89.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA=="], "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], + "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], "onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], @@ -574,10 +689,14 @@ "package-manager-detector": ["package-manager-detector@0.2.11", "", { "dependencies": { "quansync": "^0.2.7" } }, "sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ=="], + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + "path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="], + "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], @@ -588,22 +707,34 @@ "pify": ["pify@4.0.1", "", {}, "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g=="], + "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="], + "prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="], "prettier": ["prettier@2.8.8", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q=="], + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], + "pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="], + "qs": ["qs@6.15.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg=="], + "quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="], "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], + + "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], + "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], "read-yaml-file": ["read-yaml-file@1.1.0", "", { "dependencies": { "graceful-fs": "^4.1.5", "js-yaml": "^3.6.1", "pify": "^4.0.1", "strip-bom": "^3.0.0" } }, "sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA=="], "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], @@ -618,6 +749,8 @@ "rolldown-plugin-dts": ["rolldown-plugin-dts@0.23.2", "", { "dependencies": { "@babel/generator": "8.0.0-rc.3", "@babel/helper-validator-identifier": "8.0.0-rc.3", "@babel/parser": "8.0.0-rc.3", "@babel/types": "8.0.0-rc.3", "ast-kit": "^3.0.0-beta.1", "birpc": "^4.0.0", "dts-resolver": "^2.1.3", "get-tsconfig": "^4.13.7", "obug": "^2.1.1", "picomatch": "^4.0.4" }, "peerDependencies": { "@ts-macro/tsc": "^0.3.6", "@typescript/native-preview": ">=7.0.0-dev.20260325.1", "rolldown": "^1.0.0-rc.12", "typescript": "^5.0.0 || ^6.0.0", "vue-tsc": "~3.2.0" }, "optionalPeers": ["@ts-macro/tsc", "@typescript/native-preview", "typescript", "vue-tsc"] }, "sha512-PbSqLawLgZBGcOGT3yqWBGn4cX+wh2nt5FuBGdcMHyOhoukmjbhYAl8NT9sE4U38Cm9tqLOIQeOrvzeayM0DLQ=="], + "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], @@ -626,10 +759,24 @@ "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], + + "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], + + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], + + "side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="], + + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], "simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="], @@ -646,6 +793,8 @@ "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + "string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="], "string-width": ["string-width@8.2.0", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw=="], @@ -672,6 +821,8 @@ "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], @@ -682,6 +833,8 @@ "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], + "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], + "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], "unconfig-core": ["unconfig-core@7.5.0", "", { "dependencies": { "@quansync/fs": "^1.0.0", "quansync": "^1.0.0" } }, "sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w=="], @@ -690,10 +843,14 @@ "universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + "unrun": ["unrun@0.2.37", "", { "dependencies": { "rolldown": "1.0.0-rc.17" }, "peerDependencies": { "synckit": "^0.11.11" }, "optionalPeers": ["synckit"], "bin": { "unrun": "dist/cli.mjs" } }, "sha512-AA7vDuYsgeSYVzJMm16UKA+aXFKhy7nFqW9z5l7q44K4ppFWZAMqYS58ePRZbugMLPH0fwwMzD5A8nP0avxwZQ=="], "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], @@ -708,6 +865,8 @@ "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + "zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], + "@manypkg/find-root/@types/node": ["@types/node@12.20.55", "", {}, "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ=="], "@manypkg/find-root/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], diff --git a/docs/architecture.md b/docs/architecture.md index 39457e6e..8689b727 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -125,6 +125,8 @@ A local SQLite database (`.codemap.db`) indexes the project tree and stores stru **Context wiring:** **`src/cli/cmd-context.ts`** — **`buildContextEnvelope`** composes the JSON envelope from existing recipes (`fan-in` for `hubs`, `markers` SELECT for `sample_markers`, `QUERY_RECIPES` map for the catalog). **`classifyIntent`** maps `--for ""` to one of `refactor | debug | test | feature | explore | other` via regex against the trimmed input; whitespace-only intents are rejected. `--compact` drops `hubs` + `sample_markers` and emits one-line JSON; otherwise pretty-prints with 2-space indent. +**MCP wiring:** **`src/cli/cmd-mcp.ts`** (argv — `--help` only; bootstrap absorbs `--root`/`--config`) + **`src/application/mcp-server.ts`** (engine — tool registry, resource handlers, response composition). Mirrors the `cmd-audit.ts ↔ audit-engine.ts` seam — CLI parses + lifecycle; engine owns the SDK. **`runMcpServer`** bootstraps codemap once at server boot (config + resolver + DB access become module-level state), instantiates `McpServer` from **`@modelcontextprotocol/sdk`**, attaches a **`StdioServerTransport`**, and resolves when stdin closes (clean shutdown). Tool handlers reuse the existing engine entry-points: **`query`** + **`query_recipe`** call **`executeQuery`** in **`src/application/query-engine.ts`** (a pure transport-agnostic engine extracted from `printQueryResult`'s JSON branch — same `[...rows]` / `{count}` / `{group_by, groups}` envelope `--json` would print); **`query_batch`** loops via **`executeQueryBatch`** with batch-wide-defaults + per-statement-overrides (items are `string | {sql, summary?, changed_since?, group_by?}`); **`audit`** runs `resolveAuditBaselines` + `runAudit` from PR #33 unchanged; **`context`** / **`validate`** call `buildContextEnvelope` / `computeValidateRows` (pure functions in `src/cli/cmd-*.ts` — same layer-reversal allowance as `query-recipes`). **`save_baseline`** is one polymorphic tool (`{name, sql? | recipe?}`) with a runtime exclusivity check — mirrors the CLI's single `--save-baseline=` verb. **Tool naming**: snake_case throughout — Codemap convention matching the patterns in MCP spec examples and reference servers (GitHub MCP, Cursor built-ins); the spec itself doesn't mandate it. CLI stays kebab — translation lives at the MCP-arg layer. **Resources** (`codemap://recipes`, `codemap://recipes/{id}`, `codemap://schema`, `codemap://skill`) use **lazy memoisation** — first `read_resource` populates a per-server-instance cache; constant for the server-process lifetime so eager-vs-lazy produce identical observable behavior. `codemap://schema` queries `sqlite_schema` live; `codemap://skill` reads from `resolveAgentsTemplateDir() + skills/codemap/SKILL.md`. Output shape uniformity (plan § 4): every tool returns the JSON envelope its CLI counterpart's `--json` flag prints, surfaced via `content: [{type: "text", text: JSON.stringify(payload)}]`. `--changed-since` git lookups are memoised per `(root, ref)` pair across batch items so a `query_batch` of N items sharing the same ref does one git invocation, not N. Per-statement errors in `query_batch` are isolated — failed statements return `{error}` in their slot while siblings still execute. + **Performance wiring:** **`--performance`** plumbs through **`RunIndexOptions.performance`** → **`indexFiles({ performance, collectMs })`**. `parse-worker-core.ts` records per-file **`parseMs`** on each `ParsedFile`; main thread times the four phases (`collect`, `parse`, `insert`, `index_create`) and assembles **`IndexPerformanceReport`** under `IndexRunStats.performance`. Note: `total_ms` is `indexFiles` wall-clock, **not** end-to-end run wall — `collect_ms` happens before `indexFiles` and is reported separately. **Agent templates:** `codemap agents init` — full matrix [agents.md](./agents.md). diff --git a/docs/glossary.md b/docs/glossary.md index c2ec1735..df9baff2 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -243,6 +243,14 @@ Rust-based CSS parser (NAPI bindings). Codemap's `src/css-parser.ts` uses its vi ## M +### `codemap mcp` / MCP server + +Stdio MCP (Model Context Protocol) server exposing codemap's structural-query surface to agent hosts (Claude Code, Cursor, Codex, generic MCP clients) as JSON-RPC tools — eliminates the bash round-trip on every agent invocation. v1 ships one tool per CLI verb (`query`, `query_batch`, `query_recipe`, `audit`, `save_baseline`, `list_baselines`, `drop_baseline`, `context`, `validate`) plus four lazy-cached resources (`codemap://recipes`, `codemap://recipes/{id}`, `codemap://schema`, `codemap://skill`). Tool input/output keys are snake_case — Codemap's convention, matching the patterns in MCP spec examples and reference servers (GitHub MCP, Cursor built-ins); the spec itself doesn't mandate it. CLI stays kebab — translation lives at the MCP-arg layer. Output shape is verbatim from the CLI's `--json` envelope (no re-mapping). Bootstrap once at server boot; tool handlers reuse engine entry-points (`executeQuery` / `runAudit` / etc.). Distinct from `codemap serve` (HTTP API — v1.x backlog). Implementation: `src/cli/cmd-mcp.ts` (CLI shell) + `src/application/mcp-server.ts` (engine). See [`architecture.md` § MCP wiring](./architecture.md#cli-usage). + +### `query_batch` (MCP-only tool) + +MCP tool with no CLI counterpart — runs N read-only SQL statements in one round-trip. Items are `string | {sql, summary?, changed_since?, group_by?}`: bare strings inherit batch-wide flag defaults; object form overrides on a per-key basis. Output is an N-element array; per-element shape mirrors single-`query`'s output for that statement's effective flag set. Per-statement errors are isolated (failed statement returns `{error}` in its slot; siblings still execute). Distinct from making `query` accept `;`-delimited batches (rejected — would need a SQL tokenizer and would diverge `query`'s output shape from its CLI counterpart). SQL-only (no `recipe` polymorphism); `query_recipe_batch` is an additive future change if a real consumer asks. + ### markers `TODO` / `FIXME` / `HACK` / `NOTE` comments extracted from any indexed file (TS, CSS, Markdown, JSON, YAML, …). Stored in the `markers` table; surfaced by the `markers-by-kind` recipe. See `MarkerRow`. diff --git a/docs/plans/agent-transports.md b/docs/plans/agent-transports.md deleted file mode 100644 index 29937324..00000000 --- a/docs/plans/agent-transports.md +++ /dev/null @@ -1,244 +0,0 @@ -## Plan — `agent-transports` - -> Expose codemap's structural-query surface to agents over a wire protocol. **v1 ships an MCP server**; HTTP API (`codemap serve`) is the v1.x slice. Both wrap the same logical operations (`query` / `audit` / `recipe` / `context` / `validate` / `baseline` ops); the plan settles the surface once so each transport inherits the same shape. -> -> Adopted from [`docs/roadmap.md` § Backlog](../roadmap.md#backlog) ("MCP server wrapping `query` …" + "HTTP API …"). Builds on every CLI primitive shipped to date — Tier A flags (PR #26), B.6 baselines (PR #30), B.7 visibility (PR #28), B.5 v1 audit (PR #33). - -**Status:** Open — design pass; not yet implemented. -**Cross-refs:** [`docs/roadmap.md` § Non-goals](../roadmap.md#non-goals-v1) (no persistent daemon — HTTP API has to negotiate this), [`docs/architecture.md` § CLI usage](../architecture.md#cli-usage) (each MCP / HTTP tool is a thin wrapper), [`.agents/lessons.md`](../../.agents/lessons.md) (changesets policy: pre-v1 patch unless schema-breaks). - ---- - -## 1. Goal - -Agents (Claude Code, Cursor, Codex, generic MCP / HTTP clients) call codemap's structural-query surface **without a Bash round-trip**. Today every agent invocation looks like: - -```bash -codemap query --json "SELECT name, file_path FROM symbols WHERE name = 'X'" -``` - -After v1: - -```jsonc -// MCP tool call (stdio + JSON-RPC) -{ - "name": "query", - "arguments": { - "sql": "SELECT name, file_path FROM symbols WHERE name = 'X'", - }, -} -``` - -The wins: - -- **Tokens.** No bash framing, no shell quoting, no stdout parsing. -- **Latency.** No process spawn per call (MCP server is already running for the session). -- **Discoverability.** Tools self-describe via JSON Schema; agents don't have to read `--help`. -- **Composition.** Agents call `audit` directly; Codemap's existing CLI surface stays the source of truth. - -## 2. Why MCP first, HTTP API as v1.x - -| Axis | MCP server | HTTP API (`codemap serve`) | -| ----------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | -| **Real demand today** | High — we use it ourselves through Cursor / Claude Code; MCP has consumer momentum. | Speculative — no concrete user has asked. | -| **Protocol surface** | Settled (MCP spec; `@modelcontextprotocol/sdk`); JSON-RPC 2.0 over stdio. | Codemap-shaped (REST routes, status codes, JSON shapes — design from scratch). | -| **Process model** | Stdio-spawned per session by the agent host. Stays one-shot-per-session; no daemon. | Wants a long-lived process — tangles with the [persistent-daemon non-goal](../roadmap.md#non-goals-v1). | -| **Auth / security** | Trusted parent process (the agent host). Trivial. | Loopback / token / CORS — meaningful design. | -| **Implementation cost** | Lower — SDK does the JSON-RPC; just register tools. | Higher — pick framework, design routes, handle binding. | - -**v1 ships MCP only.** HTTP API stays in `roadmap.md § Backlog` until a concrete consumer asks; this plan reserves the design points (tool taxonomy, output shape, audit composition) so HTTP can inherit them when its time comes. - -## 3. Tool taxonomy - -**Decision: one MCP tool per CLI top-level operation.** Names mirror the CLI verb; `inputSchema` mirrors the CLI flag set. - -| MCP tool | Wraps | Notes | -| ----------------------------------------------------------------- | ----------------------------------------------- | -------------------------------------------------------------------------------------------- | -| `query` | `codemap query --json ""` | Pure SQL execution; identical error-shape semantics. | -| `query_recipe` | `codemap query --json --recipe ` | Separate tool (vs `query` + recipe param) so agents see the recipe surface in tool listings. | -| `audit` | `codemap audit --json --baseline ...` | Composes per-delta baseline mapping (see § 6). | -| `save_baseline` / `baseline` / `list_baselines` / `drop_baseline` | `codemap query --save-baseline= ...` etc. | Each is a distinct verb the agent should pick deliberately. | -| `context` | `codemap context --json` | Read-only; same JSON envelope. | -| `validate` | `codemap validate --json []` | Optional `paths` array. | - -**Rejected alternatives:** - -- **One mega-`cli` tool with `command` + `args`.** Loses self-description; agents have to know each subcommand's flag set. -- **Grouped tools (`structural` / `baselines` / `audit`).** Too coarse — agents pick a tool by verb, not by category. - -**Not exposed in v1:** - -- `index` (re-index from agent). Risk: agent triggers a multi-second reindex in a tight loop. Defer until a concrete use case ("agent edited files, wants to re-query") emerges; today the codemap rule's auto-incremental-index discipline (and audit's prelude) handle the freshness story. -- `agents init` (developer-side; not an agent-runtime operation). -- `version` / `--help` (MCP exposes its own discovery; CLI help is for humans). - -## 4. Output shape uniformity - -**Decision: every tool returns the same shape its CLI counterpart already returns under `--json`.** No re-mapping. - -- Success → the CLI's JSON output verbatim (`[...rows]` for `query`, `{base, head, deltas}` for `audit`, `{count}` / `{group_by, groups}` for `--summary`-flavoured ops, etc.). -- Error → MCP error response with the same `{"error": "..."}` body the CLI emits today, surfaced as the JSON-RPC error's `data` field. - -**Why no re-mapping:** - -1. The CLI's `--json` output is already the canonical surface; bundled `templates/agents/skills/codemap/SKILL.md` documents it. Re-shaping in MCP would create two surfaces to maintain. -2. Consumers reading docs / running `codemap query` directly see the same shape they get over MCP. -3. Future schema additions to the CLI envelope (e.g. `audit` v1.x verdict field) propagate to MCP automatically. - -The MCP `server.tool(...)` registration just calls the existing CLI entry-point function (`runQueryCmd` / `runAuditCmd` / etc.) with stdout captured into the response body. - -## 5. Per-tool surface (`inputSchema`) - -JSON Schema for each tool mirrors the CLI flag set. Sketch for the three v1 keystones: - -```jsonc -// query -{ - "name": "query", - "description": "Run read-only SQL against .codemap.db. Returns row array.", - "inputSchema": { - "type": "object", - "properties": { - "sql": {"type": "string", "description": "Read-only SELECT."}, - "summary": {"type": "boolean", "default": false, "description": "Return {count: N} instead of rows."}, - "changed_since": {"type": "string", "description": "Filter rows to files changed since ."}, - "group_by": {"type": "string", "enum": ["owner", "directory", "package"]} - }, - "required": ["sql"] - } -} - -// query_recipe -{ - "name": "query_recipe", - "description": "Run a bundled SQL recipe by id. Recipes carry per-row `actions` hints.", - "inputSchema": { - "type": "object", - "properties": { - "recipe": {"type": "string", "description": "Recipe id (call list_recipes for catalog)."}, - "summary": {"type": "boolean", "default": false}, - "changed_since": {"type": "string"}, - "group_by": {"type": "string", "enum": ["owner", "directory", "package"]} - }, - "required": ["recipe"] - } -} - -// audit -{ - "name": "audit", - "description": "Structural-drift audit. Composes per-delta baselines into {head, deltas}.", - "inputSchema": { - "type": "object", - "properties": { - "baseline_prefix": {"type": "string", "description": "Auto-resolves -{files,dependencies,deprecated}."}, - "baselines": { - "type": "object", - "description": "Explicit per-delta override. Keys: files | dependencies | deprecated.", - "properties": { - "files": {"type": "string"}, - "dependencies": {"type": "string"}, - "deprecated": {"type": "string"} - } - }, - "summary": {"type": "boolean", "default": false}, - "no_index": {"type": "boolean", "default": false} - } - } -} -``` - -CLI flag → JSON property: kebab-case → snake_case (idiomatic JSON Schema; matches MCP spec examples). - -## 6. Audit composition over MCP - -The CLI's `---baseline ` flags become a single structured `baselines: {[deltaKey]: name}` argument — same data shape as the engine's `AuditBaselineMap`. `--baseline ` stays a top-level `baseline_prefix` argument. The `resolveAuditBaselines` helper already exposed for tests in PR #33 is the layer the MCP wrapper calls — no logic duplication. - -## 7. Resources - -MCP resources are addressable read-only data the host can fetch ahead of tool calls. Codemap exposes: - -| URI | Body | Purpose | -| ------------------------ | ------------------------------------------------------------------- | ----------------------------------------------------------------------- | -| `codemap://recipes` | JSON array (same as `--recipes-json`) | Catalog discovery. | -| `codemap://recipes/{id}` | `{id, description, sql, actions?}` | Single-recipe inspection (replaces `--print-sql `). | -| `codemap://schema` | DDL / column descriptions (lifted from `architecture.md § Schema`) | Tells the agent what tables exist. | -| `codemap://skill` | Full text of the bundled `templates/agents/skills/codemap/SKILL.md` | Agents that don't preload the bundled skill can `read_resource` for it. | - -Resources don't take input — they're constant-per-server-instance data. The server caches them at startup. - -## 8. Composition with existing CLI - -| CLI flag | MCP equivalent | -| ---------------------------------------- | ------------------------------------------------------------------------------------------------- | -| `--json` | Always-on (MCP responses are structured). The CLI's terminal-mode renderer is dead code over MCP. | -| `--summary` | `summary: true` in tool args. | -| `--changed-since ` | `changed_since: ""` in tool args. | -| `--group-by ` | `group_by: ""` in tool args. | -| `--baseline ` (audit) | `baseline_prefix: ""`. | -| `---baseline ` (audit) | `baselines: {: ""}`. | -| `--no-index` (audit) | `no_index: true`. | -| `--recipe ` | `query_recipe` is a separate tool; no overload. | -| `--print-sql ` | `codemap://recipes/{id}` resource. | -| `--recipes-json` | `codemap://recipes` resource. | -| `--baselines` / `--drop-baseline ` | `list_baselines` / `drop_baseline` tools. | -| `--save-baseline[=]` | `save_baseline` tool with `name` + (`recipe` \| `sql`) inputs. | - -## 9. CLI surface - -```text -# v1 (ships first): -codemap mcp [--root ] [--config ] # spawned by the agent host over stdio - -# v1.x (deferred until a concrete consumer asks): -codemap serve [--port 0] [--host 127.0.0.1] [--token ] [--root ] [--config ] -``` - -- `codemap mcp` — the only new CLI verb in v1. Reads JSON-RPC on stdin, writes on stdout. Logs to stderr (per MCP convention). -- All existing CLI commands continue to work unchanged. -- Exit codes: `0` on clean shutdown (stdin EOF), `1` on bootstrap / DB / config errors. -- No new flags on existing commands. - -## 10. Implementation deps - -- **`@modelcontextprotocol/sdk`** (TypeScript) — official SDK; handles JSON-RPC framing, schema validation, transport. Single new dependency. -- Reuses every CLI entry-point function from `src/cli/cmd-*.ts` (no new business logic). -- New file: `src/cli/cmd-mcp.ts` (CLI dispatch — argv parse + spawn server) + `src/application/mcp-server.ts` (engine — tool registry, resource handlers, response composition). Mirrors the `cmd-audit.ts` ↔ `audit-engine.ts` seam. - -## 11. Tracer-bullet sequence - -Per [`tracer-bullets`](../../.agents/rules/tracer-bullets.md) and the codemap-audit precedent (~6 commits ship-end-to-end): - -1. **CLI scaffold** — `cmd-mcp.ts` + `mcp-server.ts` skeletons. `codemap mcp --help` works; `runMcpCmd` boots `@modelcontextprotocol/sdk` server with one stub tool (e.g. `version` returning `{version: "..."}`). Smoke-test via `npx @modelcontextprotocol/inspector`. Commit. -2. **First tool — `query`** — wires the server to the existing `runQueryCmd` / `printQueryResult` logic; captures stdout into the MCP response body. Tests via SDK in-process. Commit. -3. **`query_recipe`** — separate tool surfacing the recipe catalog. Composes `--summary` / `--changed-since` / `--group-by` via JSON args. Commit. -4. **`audit`** — wraps `runAuditCmd` / `runAudit`; the `baselines` arg becomes the `AuditBaselineMap` directly via `resolveAuditBaselines`. Auto-incremental-index prelude stays. Commit. -5. **Baseline tools** — `save_baseline` / `list_baselines` / `drop_baseline` round-trip via existing helpers. Commit. -6. **Resources** — `codemap://recipes`, `codemap://recipes/{id}`, `codemap://schema`, `codemap://skill`. Commit. -7. **Docs + agents update** — `architecture.md § MCP wiring` paragraph, glossary entry, README CLI block, rule + skill across `.agents/` and `templates/agents/` (Rule 10), patch changeset. Commit. - -Estimated total: ~1 day across ~7 commits. - -## 12. Open questions (worth a `grill-me` round before code) - -- **`context` and `validate` as MCP tools?** Both are CLI commands today. `context` is agent-shaped (returns the existing JSON envelope). `validate` is more dev-shaped (CI gate for stale indices). Worth surfacing both? Just `context`? -- **Should `query` accept multi-statement SQL?** Today's CLI rejects it (one statement per call). MCP could batch — but that's a real semantic shift. -- **Resource caching strategy.** Recipes are constant per server boot. Schema is constant per `SCHEMA_VERSION`. Skill text is constant per package version. Cache once at startup vs per-`read_resource` call? -- **Tool naming convention.** snake_case (matches MCP spec examples) vs kebab-case (matches CLI flags). Picked snake_case in §5; reconsider. -- **`save_baseline` argument shape.** Two sub-shapes: `{name, sql}` and `{name, recipe}`. One tool with optional fields, or two tools (`save_baseline_sql` / `save_baseline_recipe`)? - -## 13. Non-goals (v1) - -- **HTTP API.** Stays on `roadmap.md § Backlog`. Plan settles the tool taxonomy + output shape so HTTP inherits them. -- **Daemon mode for HTTP.** Even when HTTP ships, codemap stays one-shot per request unless a benchmark proves the spawn cost matters. -- **Tool-level auth on MCP.** The agent host is the trust boundary. If you don't trust your agent host, don't enable codemap. -- **Re-indexing from MCP.** Auto-incremental-index in audit handles the staleness story for now; explicit `index` tool deferred until concrete demand. -- **Streaming responses.** Codemap query results are point-in-time row sets; no streaming use case yet. - -## 14. References - -- Motivation: [`docs/roadmap.md` § Backlog](../roadmap.md#backlog) (MCP + HTTP API entries). -- MCP spec: . -- Wraps every CLI primitive shipped in PRs #26 / #28 / #30 / #33. -- Audit baseline composition: see [`docs/architecture.md` § Audit wiring](../architecture.md#cli-usage) — same `AuditBaselineMap` shape over MCP. -- Doc lifecycle: this file follows the **Plan** type per [`docs/README.md` § Document Lifecycle](../README.md#document-lifecycle) — **delete on ship**, lift the canonical bits into `architecture.md` per Rule 2. diff --git a/docs/roadmap.md b/docs/roadmap.md index 5a8188ad..034e5874 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -38,7 +38,7 @@ Codemap stays a structural-index primitive that other tools can consume. Out of - [ ] **`codemap audit --base `** (v1.x) — worktree+reindex snapshot strategy. v1 shipped `--baseline ` / `---baseline ` (B.6 reuse) — see [`architecture.md` § Audit wiring](./architecture.md#cli-usage). v1.x adds `--base ` for "audit against an arbitrary ref I haven't pre-baselined" (defers worktree spawn + cache decision until a real consumer asks). - [ ] **`codemap audit` verdict + thresholds** (v1.x) — `verdict: "pass" | "warn" | "fail"` driven by `codemap.config.audit.deltas[].{added_max, action}`. Triggers: two consumers ship `jq`-based threshold scripts with similar shapes, OR one consumer asks with a concrete config sketch. Until then, raw deltas + consumer-side `jq` is the CI exit-code idiom. -- [ ] **Agent transports — MCP server (v1) + HTTP API (v1.x)** — wrap codemap's structural-query surface (`query` / `query_recipe` / `audit` / `context` / `validate` / baseline ops) for agent-host integration. Plan: [`plans/agent-transports.md`](./plans/agent-transports.md). v1 ships `codemap mcp` (stdio + JSON-RPC, one tool per CLI verb, output shape verbatim from `--json`); v1.x adds `codemap serve [--port] [--host 127.0.0.1]` (loopback default; same tool taxonomy / output shape). Builds on every CLI primitive to date — supersedes the prior bullet-form MCP / HTTP entries. +- [ ] **`codemap serve` (HTTP API, v1.x)** — same tool taxonomy + output shape as `codemap mcp` (shipped in v1), exposed over `POST /tool/{name}` with loopback default and optional `--token`. Defer until a concrete non-MCP consumer asks; design points are reserved in [`architecture.md` § MCP wiring](./architecture.md#cli-usage) so HTTP inherits them when its turn comes. - [ ] **Recipes-as-content registry** — pair every bundled recipe in `src/cli/query-recipes.ts` with a sibling `.md` (or YAML frontmatter) describing _when to use, follow-up SQL_; surface in `--recipes-json`. Plus **project-local recipes** loaded from `.codemap/recipes/*.{sql,md}` so teams can ship internal SQL without an adapter API - [ ] **Targeted-read CLI** — `codemap show ` / `codemap snippet ` returns `file_path:line_start-line_end` + `signature` for one symbol. Same data as `SELECT … FROM symbols WHERE name = ?`, but a one-step CLI keeps agents from composing SQL for trivial precise reads - [ ] **Watch mode** for dev — `node:fs.watch` recursive + `--files` re-index loop; Linux `recursive` requires Node 19.1+ diff --git a/package.json b/package.json index 159cc180..3b9cec83 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ }, "dependencies": { "@clack/prompts": "^1.2.0", + "@modelcontextprotocol/sdk": "^1.29.0", "better-sqlite3": "^12.9.0", "lightningcss": "^1.32.0", "oxc-parser": "^0.127.0", diff --git a/src/application/mcp-server.test.ts b/src/application/mcp-server.test.ts new file mode 100644 index 00000000..45b08ed0 --- /dev/null +++ b/src/application/mcp-server.test.ts @@ -0,0 +1,627 @@ +import { afterEach, beforeEach, describe, expect, it } from "bun:test"; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; + +import { resolveCodemapConfig } from "../config"; +import { closeDb, createTables, openDb, upsertQueryBaseline } from "../db"; +import { initCodemap } from "../runtime"; +import { createMcpServer } from "./mcp-server"; + +let benchDir: string; + +beforeEach(() => { + benchDir = mkdtempSync(join(tmpdir(), "mcp-server-")); + mkdirSync(join(benchDir, "src"), { recursive: true }); + writeFileSync(join(benchDir, "src", "a.ts"), "export const A = 1;\n"); + initCodemap(resolveCodemapConfig(benchDir, undefined)); + const db = openDb(); + try { + createTables(db); + db.run( + "INSERT INTO files (path, content_hash, size, line_count, language, last_modified, indexed_at) VALUES ('src/a.ts', 'h1', 10, 1, 'typescript', 1, 1), ('src/b.ts', 'h2', 10, 1, 'typescript', 1, 1), ('docs/c.md', 'h3', 5, 1, 'markdown', 1, 1)", + ); + } finally { + closeDb(db); + } +}); + +afterEach(() => { + rmSync(benchDir, { recursive: true, force: true }); +}); + +async function makeClient() { + const server = createMcpServer({ version: "0.0.0-test", root: benchDir }); + const client = new Client({ name: "test-client", version: "0.0.0" }); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + await Promise.all([ + server.connect(serverTransport), + client.connect(clientTransport), + ]); + return { client, server }; +} + +function readJson(result: unknown): any { + const r = result as { content?: Array<{ type?: string; text?: string }> }; + const first = r.content?.[0]; + if (first?.type !== "text" || first.text === undefined) { + throw new Error("expected text content"); + } + return JSON.parse(first.text) as unknown; +} + +describe("MCP server — query tool", () => { + it("lists query and query_batch in tools/list", async () => { + const { client, server } = await makeClient(); + try { + const tools = await client.listTools(); + const names = tools.tools.map((t) => t.name).sort(); + expect(names).toContain("query"); + expect(names).toContain("query_batch"); + } finally { + await server.close(); + } + }); + + it("query returns row array verbatim from CLI envelope shape", async () => { + const { client, server } = await makeClient(); + try { + const r = await client.callTool({ + name: "query", + arguments: { sql: "SELECT path FROM files ORDER BY path" }, + }); + expect(readJson(r)).toEqual([ + { path: "docs/c.md" }, + { path: "src/a.ts" }, + { path: "src/b.ts" }, + ]); + } finally { + await server.close(); + } + }); + + it("query honors summary flag", async () => { + const { client, server } = await makeClient(); + try { + const r = await client.callTool({ + name: "query", + arguments: { sql: "SELECT path FROM files", summary: true }, + }); + expect(readJson(r)).toEqual({ count: 3 }); + } finally { + await server.close(); + } + }); + + it("query returns isError + {error} payload on bad SQL", async () => { + const { client, server } = await makeClient(); + try { + const r = await client.callTool({ + name: "query", + arguments: { sql: "SELECT * FROM nonexistent" }, + }); + expect((r as { isError?: boolean }).isError).toBe(true); + expect(readJson(r)).toMatchObject({ error: expect.any(String) }); + } finally { + await server.close(); + } + }); +}); + +describe("MCP server — query_batch tool", () => { + it("runs N statements with batch-wide flag defaults", async () => { + const { client, server } = await makeClient(); + try { + const r = await client.callTool({ + name: "query_batch", + arguments: { + statements: [ + "SELECT path FROM files WHERE language='typescript' ORDER BY path", + "SELECT path FROM files WHERE language='markdown'", + ], + }, + }); + const json = readJson(r); + expect(json).toHaveLength(2); + expect(json[0]).toEqual([{ path: "src/a.ts" }, { path: "src/b.ts" }]); + expect(json[1]).toEqual([{ path: "docs/c.md" }]); + } finally { + await server.close(); + } + }); + + it("per-statement object overrides batch-wide flag", async () => { + const { client, server } = await makeClient(); + try { + const r = await client.callTool({ + name: "query_batch", + arguments: { + statements: [ + "SELECT path FROM files", + { sql: "SELECT path FROM files", summary: true }, + ], + }, + }); + const json = readJson(r); + expect(Array.isArray(json[0])).toBe(true); + expect(json[1]).toEqual({ count: 3 }); + } finally { + await server.close(); + } + }); + + it("string-form items inherit batch-wide summary default", async () => { + const { client, server } = await makeClient(); + try { + const r = await client.callTool({ + name: "query_batch", + arguments: { + statements: ["SELECT path FROM files", "SELECT path FROM files"], + summary: true, + }, + }); + const json = readJson(r); + expect(json).toEqual([{ count: 3 }, { count: 3 }]); + } finally { + await server.close(); + } + }); + + it("isolates changed_since failures per slot — siblings still succeed", async () => { + const { client, server } = await makeClient(); + try { + const r = await client.callTool({ + name: "query_batch", + arguments: { + statements: [ + // ref that doesn't exist anywhere — git lookup should fail + { + sql: "SELECT path FROM files", + changed_since: "definitely-not-a-real-ref-xyz123", + }, + "SELECT path FROM files WHERE language='markdown'", + ], + }, + }); + const json = readJson(r); + expect(json).toHaveLength(2); + expect(json[0]).toMatchObject({ error: expect.any(String) }); + // Sibling statement still ran despite slot 0's git failure. + expect(json[1]).toEqual([{ path: "docs/c.md" }]); + } finally { + await server.close(); + } + }); + + it("isolates per-statement errors — siblings still succeed", async () => { + const { client, server } = await makeClient(); + try { + const r = await client.callTool({ + name: "query_batch", + arguments: { + statements: [ + "SELECT path FROM files WHERE language='markdown'", + "SELECT * FROM nonexistent", + ], + }, + }); + const json = readJson(r); + expect(json).toHaveLength(2); + expect(json[0]).toEqual([{ path: "docs/c.md" }]); + expect(json[1]).toMatchObject({ error: expect.any(String) }); + } finally { + await server.close(); + } + }); +}); + +describe("MCP server — query_recipe tool", () => { + it("lists query_recipe in tools/list", async () => { + const { client, server } = await makeClient(); + try { + const tools = await client.listTools(); + const names = tools.tools.map((t) => t.name); + expect(names).toContain("query_recipe"); + } finally { + await server.close(); + } + }); + + it("returns isError for unknown recipe id", async () => { + const { client, server } = await makeClient(); + try { + const r = await client.callTool({ + name: "query_recipe", + arguments: { recipe: "this-recipe-does-not-exist" }, + }); + expect((r as { isError?: boolean }).isError).toBe(true); + expect(readJson(r)).toMatchObject({ + error: expect.stringContaining("this-recipe-does-not-exist"), + }); + } finally { + await server.close(); + } + }); + + it("attaches per-row recipe actions to output rows", async () => { + // Seed a deprecated symbol so deprecated-symbols recipe returns it. + const db = openDb(); + try { + db.run( + `INSERT INTO symbols (file_path, name, kind, line_start, line_end, signature, doc_comment) + VALUES ('src/a.ts', 'oldFn', 'function', 1, 5, 'function oldFn()', '/** @deprecated use newFn */')`, + ); + } finally { + closeDb(db); + } + + const { client, server } = await makeClient(); + try { + const r = await client.callTool({ + name: "query_recipe", + arguments: { recipe: "deprecated-symbols" }, + }); + const json = readJson(r); + expect(Array.isArray(json)).toBe(true); + expect(json.length).toBeGreaterThan(0); + // Every row carries the recipe's actions template. + expect(json[0]).toMatchObject({ + name: "oldFn", + actions: [{ type: "flag-caller" }], + }); + } finally { + await server.close(); + } + }); + + it("composes summary flag with recipe", async () => { + const { client, server } = await makeClient(); + try { + // No deprecated symbols seeded for this test instance — should yield {count: 0}. + const r = await client.callTool({ + name: "query_recipe", + arguments: { recipe: "deprecated-symbols", summary: true }, + }); + expect(readJson(r)).toEqual({ count: 0 }); + } finally { + await server.close(); + } + }); +}); + +describe("MCP server — audit / context / validate tools", () => { + it("lists audit, context, validate in tools/list", async () => { + const { client, server } = await makeClient(); + try { + const tools = await client.listTools(); + const names = tools.tools.map((t) => t.name); + expect(names).toContain("audit"); + expect(names).toContain("context"); + expect(names).toContain("validate"); + } finally { + await server.close(); + } + }); + + it("audit returns isError when no baseline slot resolves", async () => { + const { client, server } = await makeClient(); + try { + const r = await client.callTool({ + name: "audit", + arguments: { baseline_prefix: "nonexistent", no_index: true }, + }); + expect((r as { isError?: boolean }).isError).toBe(true); + expect(readJson(r)).toMatchObject({ + error: expect.stringContaining("baseline"), + }); + } finally { + await server.close(); + } + }); + + it("audit returns {head, deltas} envelope for a real baseline (no_index)", async () => { + const db = openDb(); + try { + upsertQueryBaseline(db, { + name: "snap-files", + recipe_id: null, + sql: "SELECT path FROM files ORDER BY path", + rows_json: JSON.stringify([ + { path: "docs/c.md" }, + { path: "src/a.ts" }, + { path: "src/b.ts" }, + ]), + row_count: 3, + git_ref: null, + created_at: 1, + }); + } finally { + closeDb(db); + } + + const { client, server } = await makeClient(); + try { + const r = await client.callTool({ + name: "audit", + arguments: { baseline_prefix: "snap", no_index: true }, + }); + const json = readJson(r) as { + head: unknown; + deltas: Record; + }; + // No source change → no drift on files delta. + const filesDelta = json.deltas.files; + expect(filesDelta).toBeDefined(); + expect(filesDelta.added).toEqual([]); + expect(filesDelta.removed).toEqual([]); + } finally { + await server.close(); + } + }); + + it("context returns the envelope shape (file count etc.)", async () => { + const { client, server } = await makeClient(); + try { + const r = await client.callTool({ + name: "context", + arguments: {}, + }); + const json = readJson(r); + // The context envelope's exact shape lives in cmd-context.ts; smoke-check + // a couple of fields that should always be present. + expect(json).toMatchObject({ + codemap: { schema_version: expect.any(Number) }, + project: { root: expect.any(String), file_count: expect.any(Number) }, + }); + } finally { + await server.close(); + } + }); + + it("validate runs without error on the seeded files", async () => { + const { client, server } = await makeClient(); + try { + const r = await client.callTool({ + name: "validate", + arguments: { paths: [] }, + }); + const json = readJson(r); + expect(Array.isArray(json)).toBe(true); + } finally { + await server.close(); + } + }); +}); + +describe("MCP server — baseline tools", () => { + it("lists save_baseline / list_baselines / drop_baseline in tools/list", async () => { + const { client, server } = await makeClient(); + try { + const tools = await client.listTools(); + const names = tools.tools.map((t) => t.name); + expect(names).toContain("save_baseline"); + expect(names).toContain("list_baselines"); + expect(names).toContain("drop_baseline"); + } finally { + await server.close(); + } + }); + + it("save_baseline rejects passing both sql and recipe", async () => { + const { client, server } = await makeClient(); + try { + const r = await client.callTool({ + name: "save_baseline", + arguments: { name: "x", sql: "SELECT 1", recipe: "fan-out" }, + }); + expect((r as { isError?: boolean }).isError).toBe(true); + expect(readJson(r)).toMatchObject({ + error: expect.stringContaining("exactly one"), + }); + } finally { + await server.close(); + } + }); + + it("save_baseline rejects passing neither sql nor recipe", async () => { + const { client, server } = await makeClient(); + try { + const r = await client.callTool({ + name: "save_baseline", + arguments: { name: "x" }, + }); + expect((r as { isError?: boolean }).isError).toBe(true); + expect(readJson(r)).toMatchObject({ + error: expect.stringContaining("exactly one"), + }); + } finally { + await server.close(); + } + }); + + it("save_baseline saves SQL rows then list_baselines surfaces it", async () => { + const { client, server } = await makeClient(); + try { + const saved = await client.callTool({ + name: "save_baseline", + arguments: { + name: "snap-files", + sql: "SELECT path FROM files ORDER BY path", + }, + }); + expect(readJson(saved)).toMatchObject({ + saved: "snap-files", + recipe_id: null, + row_count: 3, + }); + + const listed = await client.callTool({ + name: "list_baselines", + arguments: {}, + }); + const json = readJson(listed) as Array<{ name: string }>; + expect(json.some((b) => b.name === "snap-files")).toBe(true); + } finally { + await server.close(); + } + }); + + it("save_baseline saves a recipe (recipe_id surfaces in payload)", async () => { + const { client, server } = await makeClient(); + try { + const saved = await client.callTool({ + name: "save_baseline", + arguments: { name: "snap-deprecated", recipe: "deprecated-symbols" }, + }); + expect(readJson(saved)).toMatchObject({ + saved: "snap-deprecated", + recipe_id: "deprecated-symbols", + }); + } finally { + await server.close(); + } + }); + + it("save_baseline returns isError for unknown recipe id", async () => { + const { client, server } = await makeClient(); + try { + const r = await client.callTool({ + name: "save_baseline", + arguments: { name: "x", recipe: "nope" }, + }); + expect((r as { isError?: boolean }).isError).toBe(true); + expect(readJson(r)).toMatchObject({ + error: expect.stringContaining("nope"), + }); + } finally { + await server.close(); + } + }); + + it("drop_baseline removes the saved baseline; second drop returns isError", async () => { + const { client, server } = await makeClient(); + try { + await client.callTool({ + name: "save_baseline", + arguments: { name: "to-drop", sql: "SELECT 1" }, + }); + + const first = await client.callTool({ + name: "drop_baseline", + arguments: { name: "to-drop" }, + }); + expect(readJson(first)).toEqual({ dropped: "to-drop" }); + + const second = await client.callTool({ + name: "drop_baseline", + arguments: { name: "to-drop" }, + }); + expect((second as { isError?: boolean }).isError).toBe(true); + expect(readJson(second)).toMatchObject({ + error: expect.stringContaining("to-drop"), + }); + } finally { + await server.close(); + } + }); +}); + +function readResourceText(r: { contents: unknown[] }): string { + const first = r.contents[0] as { text?: string }; + if (typeof first.text !== "string") { + throw new Error("expected text resource content"); + } + return first.text; +} + +describe("MCP server — resources", () => { + it("lists all four resources via resources/list (one as template)", async () => { + const { client, server } = await makeClient(); + try { + const list = await client.listResources(); + const uris = list.resources.map((r) => r.uri); + // Static resources surface in resources/list directly. + expect(uris).toContain("codemap://recipes"); + expect(uris).toContain("codemap://schema"); + expect(uris).toContain("codemap://skill"); + // The recipe-by-id resource is a template — surfaced via list-template + // callback as one entry per recipe id. + const recipeUris = uris.filter((u) => u.startsWith("codemap://recipes/")); + expect(recipeUris.length).toBeGreaterThan(0); + } finally { + await server.close(); + } + }); + + it("codemap://recipes returns the catalog as JSON", async () => { + const { client, server } = await makeClient(); + try { + const r = await client.readResource({ uri: "codemap://recipes" }); + expect(r.contents).toHaveLength(1); + const first = r.contents[0] as { mimeType?: string }; + expect(first.mimeType).toBe("application/json"); + const parsed = JSON.parse(readResourceText(r)); + expect(Array.isArray(parsed)).toBe(true); + expect(parsed.length).toBeGreaterThan(0); + expect(parsed[0]).toMatchObject({ + id: expect.any(String), + description: expect.any(String), + sql: expect.any(String), + }); + } finally { + await server.close(); + } + }); + + it("codemap://recipes/{id} resolves a single recipe", async () => { + const { client, server } = await makeClient(); + try { + const r = await client.readResource({ + uri: "codemap://recipes/deprecated-symbols", + }); + const parsed = JSON.parse(readResourceText(r)); + expect(parsed).toMatchObject({ + id: "deprecated-symbols", + description: expect.any(String), + sql: expect.stringContaining("@deprecated"), + actions: expect.any(Array), + }); + } finally { + await server.close(); + } + }); + + it("codemap://schema returns DDL for live tables", async () => { + const { client, server } = await makeClient(); + try { + const r = await client.readResource({ uri: "codemap://schema" }); + const parsed = JSON.parse(readResourceText(r)); + expect(Array.isArray(parsed)).toBe(true); + const filesEntry = parsed.find( + (t: { name: string }) => t.name === "files", + ); + expect(filesEntry).toBeDefined(); + expect(filesEntry.ddl).toContain("content_hash"); + } finally { + await server.close(); + } + }); + + it("codemap://skill returns the bundled SKILL.md text", async () => { + const { client, server } = await makeClient(); + try { + const r = await client.readResource({ uri: "codemap://skill" }); + const first = r.contents[0] as { mimeType?: string }; + expect(first.mimeType).toBe("text/markdown"); + const text = readResourceText(r); + // SKILL.md begins with the YAML frontmatter convention. + expect(text.startsWith("---")).toBe(true); + } finally { + await server.close(); + } + }); +}); diff --git a/src/application/mcp-server.ts b/src/application/mcp-server.ts new file mode 100644 index 00000000..418de73a --- /dev/null +++ b/src/application/mcp-server.ts @@ -0,0 +1,778 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + +import { + McpServer, + ResourceTemplate, +} from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { z } from "zod"; + +import { resolveAgentsTemplateDir } from "../agents-init"; +// Layer note: several modules below live under `src/cli/` because their CLI +// verb owns them today (`query-recipes`, `cmd-audit`'s baseline resolver, +// `cmd-context`'s envelope builder, `cmd-validate`'s row computer). We import +// them here as pure data / pure functions (no execution flow crosses +// cli → application). A future refactor may lift them to `src/application/` +// once a second consumer (HTTP API) needs them. +import { resolveAuditBaselines } from "../cli/cmd-audit"; +import { buildContextEnvelope } from "../cli/cmd-context"; +import { computeValidateRows } from "../cli/cmd-validate"; +import { + getQueryRecipeActions, + getQueryRecipeSql, + listQueryRecipeCatalog, + listQueryRecipeIds, + QUERY_RECIPES, +} from "../cli/query-recipes"; +import { loadUserConfig, resolveCodemapConfig } from "../config"; +import { + closeDb, + deleteQueryBaseline, + listQueryBaselines, + openDb, + upsertQueryBaseline, +} from "../db"; +import { getFilesChangedSince } from "../git-changed"; +import { GROUP_BY_MODES } from "../group-by"; +import type { GroupByMode } from "../group-by"; +import { configureResolver } from "../resolver"; +import { getProjectRoot, getTsconfigPath, initCodemap } from "../runtime"; +import { runAudit } from "./audit-engine"; +import { getCurrentCommit } from "./index-engine"; +import { executeQuery } from "./query-engine"; +import { runCodemapIndex } from "./run-index"; + +/** + * MCP server engine — owns the tool / resource registry. CLI shell + * (`src/cli/cmd-mcp.ts`) handles argv + lifecycle only; this module is + * the thin wrapper around `@modelcontextprotocol/sdk` that registers + * one tool per CLI verb (plus MCP-only `query_batch`) and the four + * `codemap://` resources. See [`docs/architecture.md` § MCP wiring]. + */ + +interface ServerOpts { + version: string; + root: string; + configFile?: string | undefined; +} + +const groupByEnum = z.enum( + GROUP_BY_MODES as unknown as readonly [GroupByMode, ...GroupByMode[]], +); + +// Per-statement schema for query_batch — matches the `oneOf` polymorphism +// in plan § 5: items are either a bare SQL string or an object that +// overrides batch-wide flags on a per-key basis. +const batchItemSchema = z.union([ + z.string().min(1, "sql must be a non-empty string"), + z.object({ + sql: z.string().min(1, "sql must be a non-empty string"), + summary: z.boolean().optional(), + changed_since: z.string().optional(), + group_by: groupByEnum.optional(), + }), +]); + +/** + * Wraps a tool handler so any thrown error becomes a structured + * `{"error":""}` payload (matching the CLI's `--json` error + * shape — plan § 4 uniformity). Without this, an unhandled throw + * surfaces as a JSON-RPC error which loses the CLI-shape contract. + */ +function jsonResult(payload: unknown) { + return { + content: [{ type: "text" as const, text: JSON.stringify(payload) }], + }; +} + +function jsonError(message: string) { + return { + isError: true, + content: [ + { type: "text" as const, text: JSON.stringify({ error: message }) }, + ], + }; +} + +// Engine helpers (executeQuery / runAudit) return either a result payload OR +// `{error}` for in-band failures. Narrows that union cheaply for the tool +// handlers; centralised so the type-guard logic stays in one place. +function isEnginePayloadError(payload: unknown): payload is { error: string } { + return ( + payload !== null && + typeof payload === "object" && + !Array.isArray(payload) && + "error" in payload && + typeof (payload as { error: unknown }).error === "string" + ); +} + +/** + * Resolve `changed_since: ` to a Set of project-relative paths. + * Memoised per (root, ref) pair across batch items so a batch with N + * items sharing the same ref does one git invocation instead of N. + */ +function makeChangedFilesResolver( + root: string, +): (ref: string | undefined) => Set | undefined | { error: string } { + const cache = new Map>(); + return (ref) => { + if (ref === undefined) return undefined; + const cached = cache.get(ref); + if (cached) return cached; + const result = getFilesChangedSince(ref, root); + if (!result.ok) return { error: result.error }; + cache.set(ref, result.files); + return result.files; + }; +} + +/** + * Build a fully-configured `McpServer` instance with every codemap tool + * and resource registered. Doesn't connect to a transport — caller owns + * lifecycle (production: `runMcpServer` attaches stdio; tests: + * `InMemoryTransport.createLinkedPair()` for in-process driving). + * + * `opts.root` is the indexed project root (forwarded to tool handlers + * for `--changed-since` git lookups, `group_by package` workspace + * discovery, etc.); `opts.version` populates MCP `Implementation.version` + * for the `tools/list` self-description; `opts.configFile` is unused at + * registration time but threaded through for symmetry with bootstrap. + */ +export function createMcpServer(opts: ServerOpts): McpServer { + const server = new McpServer({ + name: "codemap", + version: opts.version, + }); + + registerQueryTool(server, opts); + registerQueryBatchTool(server, opts); + registerQueryRecipeTool(server, opts); + registerAuditTool(server, opts); + registerContextTool(server, opts); + registerValidateTool(server, opts); + registerSaveBaselineTool(server, opts); + registerListBaselinesTool(server, opts); + registerDropBaselineTool(server, opts); + registerResources(server); + + return server; +} + +function registerQueryTool(server: McpServer, opts: ServerOpts): void { + server.registerTool( + "query", + { + description: + "Run one read-only SQL statement against .codemap.db. Returns the JSON envelope `codemap query --json` would print: row array by default, {count} under `summary`, {group_by, groups} under `group_by`. Use `query_batch` for N statements in one round-trip.", + inputSchema: { + sql: z.string().min(1, "sql must be a non-empty string"), + summary: z.boolean().optional(), + changed_since: z.string().optional(), + group_by: groupByEnum.optional(), + }, + }, + (args) => { + try { + const resolveChanged = makeChangedFilesResolver(opts.root); + const changed = resolveChanged(args.changed_since); + if (changed && typeof changed === "object" && "error" in changed) { + return jsonError(changed.error); + } + const payload = executeQuery({ + sql: args.sql, + summary: args.summary, + changedFiles: changed as Set | undefined, + groupBy: args.group_by, + root: opts.root, + }); + if (isEnginePayloadError(payload)) return jsonError(payload.error); + return jsonResult(payload); + } catch (err) { + return jsonError(err instanceof Error ? err.message : String(err)); + } + }, + ); +} + +function registerQueryRecipeTool(server: McpServer, opts: ServerOpts): void { + server.registerTool( + "query_recipe", + { + description: + "Run a bundled SQL recipe by id. Output rows carry per-row `actions` hints (recipe-only — `query` never adds them). Compose with `summary` / `changed_since` / `group_by` exactly like `query`. List available recipes via the `codemap://recipes` resource.", + inputSchema: { + recipe: z.string().min(1, "recipe must be a non-empty string"), + summary: z.boolean().optional(), + changed_since: z.string().optional(), + group_by: groupByEnum.optional(), + }, + }, + (args) => { + try { + const sql = getQueryRecipeSql(args.recipe); + if (sql === undefined) { + return jsonError( + `codemap: unknown recipe "${args.recipe}". List available recipes via the codemap://recipes resource.`, + ); + } + const recipeActions = getQueryRecipeActions(args.recipe); + const resolveChanged = makeChangedFilesResolver(opts.root); + const changed = resolveChanged(args.changed_since); + if (changed && typeof changed === "object" && "error" in changed) { + return jsonError(changed.error); + } + const payload = executeQuery({ + sql, + summary: args.summary, + changedFiles: changed as Set | undefined, + groupBy: args.group_by, + recipeActions, + root: opts.root, + }); + if (isEnginePayloadError(payload)) return jsonError(payload.error); + return jsonResult(payload); + } catch (err) { + return jsonError(err instanceof Error ? err.message : String(err)); + } + }, + ); +} + +function registerQueryBatchTool(server: McpServer, opts: ServerOpts): void { + server.registerTool( + "query_batch", + { + description: + "Run N read-only SQL statements in one round-trip. Each item is either a bare SQL string (inherits batch-wide flags) or an object {sql, summary?, changed_since?, group_by?} overriding batch-wide flags per-key. Returns an N-element array; per-element shape mirrors single `query`'s output for that statement's effective flag set.", + inputSchema: { + statements: z.array(batchItemSchema).min(1), + summary: z.boolean().optional(), + changed_since: z.string().optional(), + group_by: groupByEnum.optional(), + }, + }, + (args) => { + try { + // `changed_since` resolution can fail per-item (bad ref, git missing + // for that branch, etc.). Run the resolver inline so a failure for + // statement i lands in slot i instead of aborting the whole batch — + // matches the per-statement isolation contract documented in + // `executeQueryBatch` and plan § 5. + const resolveChanged = makeChangedFilesResolver(opts.root); + const results = args.statements.map((item) => { + try { + const merged = mergeBatchItem(item, args); + const changed = resolveChanged(merged.changed_since); + if (changed && typeof changed === "object" && "error" in changed) { + return { error: changed.error }; + } + return executeQuery({ + sql: merged.sql, + summary: merged.summary, + changedFiles: changed as Set | undefined, + groupBy: merged.group_by, + root: opts.root, + }); + } catch (err) { + return { + error: err instanceof Error ? err.message : String(err), + }; + } + }); + return jsonResult(results); + } catch (err) { + return jsonError(err instanceof Error ? err.message : String(err)); + } + }, + ); +} + +interface MergedBatchItem { + sql: string; + summary: boolean | undefined; + changed_since: string | undefined; + group_by: GroupByMode | undefined; +} + +function mergeBatchItem( + item: z.infer, + defaults: { + summary?: boolean | undefined; + changed_since?: string | undefined; + group_by?: GroupByMode | undefined; + }, +): MergedBatchItem { + if (typeof item === "string") { + return { + sql: item, + summary: defaults.summary, + changed_since: defaults.changed_since, + group_by: defaults.group_by, + }; + } + // Per-key override; undefined or missing key inherits batch-wide. + return { + sql: item.sql, + summary: item.summary ?? defaults.summary, + changed_since: item.changed_since ?? defaults.changed_since, + group_by: item.group_by ?? defaults.group_by, + }; +} + +function registerAuditTool(server: McpServer, _opts: ServerOpts): void { + server.registerTool( + "audit", + { + description: + "Structural-drift audit. Composes per-delta baselines (files / dependencies / deprecated) into a {head, deltas} envelope. Pass `baseline_prefix` to auto-resolve -{files,dependencies,deprecated} from query_baselines, OR `baselines: {: }` for explicit per-delta overrides (composes with prefix). `summary: true` collapses each delta to {added: N, removed: N}. `no_index: true` skips the auto-incremental-index prelude (default re-indexes first so head reflects current source).", + inputSchema: { + baseline_prefix: z.string().optional(), + baselines: z + .object({ + files: z.string().optional(), + dependencies: z.string().optional(), + deprecated: z.string().optional(), + }) + .optional(), + summary: z.boolean().optional(), + no_index: z.boolean().optional(), + }, + }, + async (args) => { + try { + const db = openDb(); + try { + if (!args.no_index) { + await runCodemapIndex(db, { mode: "incremental", quiet: true }); + } + const perDelta: Record = {}; + if (args.baselines) { + for (const [k, v] of Object.entries(args.baselines)) { + if (typeof v === "string") perDelta[k] = v; + } + } + const baselines = resolveAuditBaselines({ + db, + baselinePrefix: args.baseline_prefix, + perDelta, + }); + const result = runAudit({ db, baselines }); + if ("error" in result) { + return jsonError(result.error); + } + if (args.summary) { + const counts: Record< + string, + { + base: (typeof result.deltas)[string]["base"]; + added: number; + removed: number; + } + > = {}; + for (const [key, delta] of Object.entries(result.deltas)) { + counts[key] = { + base: delta.base, + added: delta.added.length, + removed: delta.removed.length, + }; + } + return jsonResult({ head: result.head, deltas: counts }); + } + return jsonResult(result); + } finally { + closeDb(db, { readonly: args.no_index === true }); + } + } catch (err) { + return jsonError(err instanceof Error ? err.message : String(err)); + } + }, + ); +} + +function registerContextTool(server: McpServer, _opts: ServerOpts): void { + server.registerTool( + "context", + { + description: + "Project bootstrap snapshot — returns the same envelope `codemap context --json` prints (project root, schema version, file/symbol counts, language breakdown, recipe catalog summary, etc.). Designed for agent session-start: one call replaces 4-5 `query` calls.", + inputSchema: { + compact: z.boolean().optional(), + intent: z.string().optional(), + }, + }, + (args) => { + try { + const db = openDb(); + try { + const envelope = buildContextEnvelope(db, getProjectRoot(), { + compact: args.compact === true, + intent: args.intent ?? null, + }); + return jsonResult(envelope); + } finally { + closeDb(db, { readonly: true }); + } + } catch (err) { + return jsonError(err instanceof Error ? err.message : String(err)); + } + }, + ); +} + +function registerValidateTool(server: McpServer, _opts: ServerOpts): void { + server.registerTool( + "validate", + { + description: + "Compare on-disk SHA-256 of indexed files to the indexed `files.content_hash` column. Returns rows with status ('ok' / 'changed' / 'missing'). Empty `paths` validates every indexed file. Useful for 'codemap doctor' agents that diagnose stale .codemap.db before issuing structural queries.", + inputSchema: { + paths: z.array(z.string()).optional(), + }, + }, + (args) => { + try { + const db = openDb(); + try { + const rows = computeValidateRows( + db, + getProjectRoot(), + args.paths ?? [], + ); + return jsonResult(rows); + } finally { + closeDb(db, { readonly: true }); + } + } catch (err) { + return jsonError(err instanceof Error ? err.message : String(err)); + } + }, + ); +} + +function registerSaveBaselineTool(server: McpServer, _opts: ServerOpts): void { + server.registerTool( + "save_baseline", + { + description: + "Snapshot the rows of a SQL or recipe under `name` in query_baselines. Polymorphic input: pass exactly one of `sql` (ad-hoc SELECT) or `recipe` (bundled recipe id). Mirrors `codemap query --save-baseline=`'s single-verb shape; the runtime check that exactly one is set keeps the agent from accidentally saving an unintended source.", + inputSchema: { + name: z.string().min(1, "name must be a non-empty string"), + sql: z.string().optional(), + recipe: z.string().optional(), + }, + }, + (args) => { + try { + if ((args.sql == null) === (args.recipe == null)) { + return jsonError( + "save_baseline: pass exactly one of `sql` or `recipe`.", + ); + } + let sql: string; + let recipeId: string | null = null; + if (args.recipe != null) { + const recipeSql = getQueryRecipeSql(args.recipe); + if (recipeSql === undefined) { + return jsonError( + `save_baseline: unknown recipe "${args.recipe}". List available recipes via the codemap://recipes resource.`, + ); + } + sql = recipeSql; + recipeId = args.recipe; + } else { + sql = args.sql!; + } + const payload = executeQuery({ sql, root: _opts.root }); + if (isEnginePayloadError(payload)) return jsonError(payload.error); + const rows = payload as unknown[]; + const db = openDb(); + const savedAt = Date.now(); + const gitRef = tryGetGitRefSafe(); + try { + upsertQueryBaseline(db, { + name: args.name, + recipe_id: recipeId, + sql, + rows_json: JSON.stringify(rows), + row_count: rows.length, + git_ref: gitRef, + created_at: savedAt, + }); + } finally { + closeDb(db); + } + return jsonResult({ + saved: args.name, + recipe_id: recipeId, + row_count: rows.length, + git_ref: gitRef, + created_at: savedAt, + }); + } catch (err) { + return jsonError(err instanceof Error ? err.message : String(err)); + } + }, + ); +} + +function registerListBaselinesTool(server: McpServer, _opts: ServerOpts): void { + server.registerTool( + "list_baselines", + { + description: + "List all saved baselines (no rows_json payload — use the audit tool with a baseline_prefix to compare against current). Returns the same array `codemap query --baselines --json` prints.", + inputSchema: {}, + }, + () => { + try { + const db = openDb(); + try { + return jsonResult(listQueryBaselines(db)); + } finally { + closeDb(db, { readonly: true }); + } + } catch (err) { + return jsonError(err instanceof Error ? err.message : String(err)); + } + }, + ); +} + +function registerDropBaselineTool(server: McpServer, _opts: ServerOpts): void { + server.registerTool( + "drop_baseline", + { + description: + "Delete the named baseline. Returns {dropped: } on success or {error} if the name doesn't exist.", + inputSchema: { + name: z.string().min(1, "name must be a non-empty string"), + }, + }, + (args) => { + try { + const db = openDb(); + try { + const dropped = deleteQueryBaseline(db, args.name); + if (!dropped) { + return jsonError( + `drop_baseline: no baseline named "${args.name}". Call list_baselines for the catalog.`, + ); + } + return jsonResult({ dropped: args.name }); + } finally { + closeDb(db); + } + } catch (err) { + return jsonError(err instanceof Error ? err.message : String(err)); + } + }, + ); +} + +/** + * MCP resources are addressable read-only data the host can fetch ahead of + * tool calls. Plan § 7 + grill round Q3 settled on **lazy memoisation**: + * resources are constant for the server-process lifetime, so eager-vs-lazy + * produce identical observable behavior — lazy keeps boot lean for sessions + * that never call read_resource. + */ +function registerResources(server: McpServer): void { + // codemap://recipes — full catalog (same as CLI's --recipes-json) + let recipesCache: string | undefined; + server.registerResource( + "recipes", + "codemap://recipes", + { + description: + "Bundled SQL recipes catalog (id, description, sql, optional per-row actions). Same payload as `codemap query --recipes-json`.", + mimeType: "application/json", + }, + () => { + if (recipesCache === undefined) { + recipesCache = JSON.stringify(listQueryRecipeCatalog()); + } + return { + contents: [ + { + uri: "codemap://recipes", + mimeType: "application/json", + text: recipesCache, + }, + ], + }; + }, + ); + + // codemap://recipes/{id} — one recipe (template form) + const oneRecipeCache = new Map(); + server.registerResource( + "recipe", + new ResourceTemplate("codemap://recipes/{id}", { + list: () => ({ + resources: listQueryRecipeIds().map((id) => ({ + uri: `codemap://recipes/${id}`, + name: id, + description: QUERY_RECIPES[id]!.description, + mimeType: "application/json", + })), + }), + }), + { + description: + "Single recipe by id: {id, description, sql, actions?}. Replaces `codemap query --print-sql ` for agents.", + mimeType: "application/json", + }, + (uri, variables) => { + const id = + typeof variables.id === "string" ? variables.id : String(variables.id); + const cached = oneRecipeCache.get(id); + if (cached !== undefined) { + return { + contents: [ + { uri: uri.toString(), mimeType: "application/json", text: cached }, + ], + }; + } + const meta = QUERY_RECIPES[id]; + if (meta === undefined) { + // Resources can't return structured errors the way tools do; throw so + // the SDK surfaces a JSON-RPC error to the host. + throw new Error( + `codemap: unknown recipe "${id}". Read codemap://recipes for the catalog.`, + ); + } + const payload = JSON.stringify({ + id, + description: meta.description, + sql: meta.sql, + ...(meta.actions !== undefined ? { actions: meta.actions } : {}), + }); + oneRecipeCache.set(id, payload); + return { + contents: [ + { + uri: uri.toString(), + mimeType: "application/json", + text: payload, + }, + ], + }; + }, + ); + + // codemap://schema — DDL of every indexed table (queried live from sqlite_schema) + let schemaCache: string | undefined; + server.registerResource( + "schema", + "codemap://schema", + { + description: + "DDL of every table in .codemap.db (queried live from sqlite_schema). Tells the agent what tables and columns exist.", + mimeType: "application/json", + }, + () => { + if (schemaCache === undefined) { + const db = openDb(); + try { + const rows = db + .query( + "SELECT name, sql FROM sqlite_schema WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name", + ) + .all() as { name: string; sql: string | null }[]; + schemaCache = JSON.stringify( + rows + .filter((r) => r.sql !== null) + .map((r) => ({ + name: r.name, + ddl: r.sql, + })), + ); + } finally { + closeDb(db, { readonly: true }); + } + } + return { + contents: [ + { + uri: "codemap://schema", + mimeType: "application/json", + text: schemaCache, + }, + ], + }; + }, + ); + + // codemap://skill — bundled SKILL.md text + let skillCache: string | undefined; + server.registerResource( + "skill", + "codemap://skill", + { + description: + "Full text of the bundled `templates/agents/skills/codemap/SKILL.md`. Agents that don't preload the skill at session start can fetch it here.", + mimeType: "text/markdown", + }, + () => { + if (skillCache === undefined) { + const skillPath = join( + resolveAgentsTemplateDir(), + "skills", + "codemap", + "SKILL.md", + ); + skillCache = readFileSync(skillPath, "utf8"); + } + return { + contents: [ + { + uri: "codemap://skill", + mimeType: "text/markdown", + text: skillCache, + }, + ], + }; + }, + ); +} + +// `git rev-parse HEAD` may legitimately fail (no git, detached worktree, etc.); +// baselines just record git_ref = NULL in that case. Mirrors the same helper +// in cmd-query.ts (kept local to avoid a cli → application import). +function tryGetGitRefSafe(): string | null { + try { + const sha = getCurrentCommit(); + return sha || null; + } catch { + return null; + } +} + +/** + * Bootstrap codemap once at server boot — config + resolver + DB access + * all become module-level state. Tool handlers then call into the + * pre-initialized stack on every request without re-bootstrapping. + */ +async function bootstrapForMcp(opts: ServerOpts): Promise { + const user = await loadUserConfig(opts.root, opts.configFile); + initCodemap(resolveCodemapConfig(opts.root, user)); + configureResolver(getProjectRoot(), getTsconfigPath()); +} + +/** + * Starts the MCP server over stdio (the only transport in v1; HTTP is + * deferred to v1.x — see plan § 2). Resolves when the transport closes + * (stdin EOF). Logs to stderr per MCP convention so stdout stays + * dedicated to JSON-RPC framing. + */ +export async function runMcpServer(opts: ServerOpts): Promise { + await bootstrapForMcp(opts); + const server = createMcpServer(opts); + const transport = new StdioServerTransport(); + await server.connect(transport); + await new Promise((resolve) => { + transport.onclose = () => resolve(); + }); +} diff --git a/src/application/query-engine.test.ts b/src/application/query-engine.test.ts new file mode 100644 index 00000000..59fb206f --- /dev/null +++ b/src/application/query-engine.test.ts @@ -0,0 +1,153 @@ +import { afterEach, beforeEach, describe, expect, it } from "bun:test"; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import { resolveCodemapConfig } from "../config"; +import { closeDb, createTables, openDb } from "../db"; +import { initCodemap } from "../runtime"; +import { executeQuery, executeQueryBatch } from "./query-engine"; + +let benchDir: string; + +beforeEach(() => { + benchDir = mkdtempSync(join(tmpdir(), "query-engine-")); + mkdirSync(join(benchDir, "src"), { recursive: true }); + writeFileSync(join(benchDir, "src", "a.ts"), "export const A = 1;\n"); + initCodemap(resolveCodemapConfig(benchDir, undefined)); + const db = openDb(); + try { + createTables(db); + db.run( + "INSERT INTO files (path, content_hash, size, line_count, language, last_modified, indexed_at) VALUES ('src/a.ts', 'h1', 10, 1, 'typescript', 1, 1), ('src/b.ts', 'h2', 10, 1, 'typescript', 1, 1), ('docs/c.md', 'h3', 5, 1, 'markdown', 1, 1)", + ); + } finally { + closeDb(db); + } +}); + +afterEach(() => { + rmSync(benchDir, { recursive: true, force: true }); +}); + +describe("executeQuery", () => { + it("returns rows array by default", () => { + const r = executeQuery({ + sql: "SELECT path FROM files ORDER BY path", + root: benchDir, + }); + expect(r).toEqual([ + { path: "docs/c.md" }, + { path: "src/a.ts" }, + { path: "src/b.ts" }, + ]); + }); + + it("returns {count} under summary", () => { + const r = executeQuery({ + sql: "SELECT path FROM files", + summary: true, + root: benchDir, + }); + expect(r).toEqual({ count: 3 }); + }); + + it("returns {group_by, groups} under group_by", () => { + const r = executeQuery({ + sql: "SELECT path FROM files", + groupBy: "directory", + root: benchDir, + }); + expect(r).toMatchObject({ group_by: "directory" }); + }); + + it("returns {error} on invalid SQL instead of throwing", () => { + const r = executeQuery({ + sql: "SELECT * FROM nonexistent_table", + root: benchDir, + }); + expect(r).toMatchObject({ error: expect.any(String) }); + }); + + it("filters rows by changedFiles when set", () => { + const r = executeQuery({ + sql: "SELECT path FROM files", + changedFiles: new Set(["src/a.ts"]), + root: benchDir, + }); + expect(r).toEqual([{ path: "src/a.ts" }]); + }); + + it("rejects DML — read-only enforcement via PRAGMA query_only", () => { + const r = executeQuery({ + sql: "DELETE FROM files WHERE language='markdown'", + root: benchDir, + }); + expect(r).toMatchObject({ error: expect.any(String) }); + // Confirm the row wasn't actually deleted. + const after = executeQuery({ + sql: "SELECT COUNT(*) AS n FROM files WHERE language='markdown'", + root: benchDir, + }); + expect(after).toEqual([{ n: 1 }]); + }); + + it("rejects DDL — DROP TABLE blocked by query_only", () => { + const r = executeQuery({ + sql: "DROP TABLE files", + root: benchDir, + }); + expect(r).toMatchObject({ error: expect.any(String) }); + // Confirm the table still exists. + const after = executeQuery({ + sql: "SELECT COUNT(*) AS n FROM files", + root: benchDir, + }); + expect(after).toEqual([{ n: 3 }]); + }); +}); + +describe("executeQueryBatch", () => { + it("runs N statements and returns N results", () => { + const r = executeQueryBatch({ + statements: [ + { + sql: "SELECT path FROM files WHERE language='typescript' ORDER BY path", + }, + { sql: "SELECT path FROM files WHERE language='markdown'" }, + ], + root: benchDir, + }); + expect(r).toHaveLength(2); + expect(r[0]).toEqual([{ path: "src/a.ts" }, { path: "src/b.ts" }]); + expect(r[1]).toEqual([{ path: "docs/c.md" }]); + }); + + it("respects per-statement summary override", () => { + const r = executeQueryBatch({ + statements: [ + { sql: "SELECT path FROM files" }, + { sql: "SELECT path FROM files", summary: true }, + ], + root: benchDir, + }); + expect(Array.isArray(r[0])).toBe(true); + expect(r[1]).toEqual({ count: 3 }); + }); + + it("isolates errors — failed statement returns {error} but siblings succeed", () => { + const r = executeQueryBatch({ + statements: [ + { sql: "SELECT path FROM files WHERE language='markdown'" }, + { sql: "SELECT * FROM nonexistent" }, + { sql: "SELECT path FROM files WHERE language='typescript'" }, + ], + root: benchDir, + }); + expect(r).toHaveLength(3); + expect(r[0]).toEqual([{ path: "docs/c.md" }]); + expect(r[1]).toMatchObject({ error: expect.any(String) }); + expect(Array.isArray(r[2])).toBe(true); + expect((r[2] as unknown[]).length).toBe(2); + }); +}); diff --git a/src/application/query-engine.ts b/src/application/query-engine.ts new file mode 100644 index 00000000..58b6aecb --- /dev/null +++ b/src/application/query-engine.ts @@ -0,0 +1,180 @@ +import { closeDb, openDb } from "../db"; +import { filterRowsByChangedFiles } from "../git-changed"; +import { + discoverWorkspaceRoots, + firstDirectory, + groupRowsBy, + loadCodeowners, + makePackageBucketizer, +} from "../group-by"; +import type { Bucketizer, GroupByMode } from "../group-by"; + +/** + * Pure, transport-agnostic query execution. Mirrors the layering of + * `audit-engine.ts` / `index-engine.ts` — CLI shells (`cmd-query.ts`) + * and the MCP server (`mcp-server.ts`) both call into this engine + * instead of duplicating the result-shaping logic. + * + * `executeQuery` replaces the JSON branch of `printQueryResult` / + * `runGroupedQuery` from `cmd-query.ts` — the CLI version still owns + * console-table rendering for terminal output. Engine returns the + * exact JSON envelope `--json` would print so MCP responses are + * structurally identical to CLI output (plan § 4 uniformity). + */ + +export interface ExecuteQueryOpts { + sql: string; + summary?: boolean; + /** + * Pre-resolved set of project-relative file paths that changed since + * a git ref. The CLI layer / MCP layer is responsible for translating + * `--changed-since ` into this set via `git-changed.ts` — the + * engine stays git-agnostic. + */ + changedFiles?: Set | undefined; + groupBy?: GroupByMode | undefined; + recipeActions?: ReadonlyArray | undefined; + root: string; +} + +/** + * The JSON envelope `executeQuery` returns on success — same shape + * `codemap query --json` prints. Discriminated by which flags were set: + * raw `unknown[]` for default reads, `{count}` under `summary`, + * `{group_by, groups}` under `groupBy` (groups carry full row arrays + * by default; counts only when `summary` is also true). + */ +export type QueryResultPayload = + | unknown[] + | { count: number } + | { group_by: GroupByMode; groups: unknown[] } + | { + group_by: GroupByMode; + groups: Array<{ key: string; count: number }>; + }; + +/** + * In-band failure shape returned for SQL errors, group_by misconfig, + * and other recoverable failures. Mirrors the `{"error":"…"}` shape the + * CLI's `--json` flag emits — callers that care can narrow with + * `"error" in payload` (or use `isEnginePayloadError` from `mcp-server`). + */ +export interface ExecuteQueryError { + error: string; +} + +/** + * Run one SQL statement and return the JSON envelope that `--json` + * would print. Caller owns DB lifecycle decisions only insofar as the + * shared `openDb()` / `closeDb()` pair is used inside; this matches + * `printQueryResult`'s self-contained connection management. + */ +export function executeQuery( + opts: ExecuteQueryOpts, +): QueryResultPayload | ExecuteQueryError { + const db = openDb(); + try { + // SQLite-level read-only enforcement — rejects DML / DDL (DELETE, DROP, + // UPDATE, ATTACH, …) on this connection regardless of the SQL the caller + // passes. Defence in depth: every consumer of `executeQuery` (MCP `query`, + // `query_recipe`, `query_batch`, `save_baseline`'s row capture) is + // contractually read-only; this guard turns the contract into a parser- + // proof boundary. Doesn't bleed across calls — `closeDb()` discards the + // connection. + db.run("PRAGMA query_only = 1"); + let rows = db.query(opts.sql).all() as unknown[]; + + if (opts.changedFiles !== undefined) { + rows = filterRowsByChangedFiles(rows, opts.changedFiles); + } + + if (opts.groupBy !== undefined) { + const bucketize = resolveBucketizer(opts.groupBy, opts.root); + if ("error" in bucketize) return bucketize; + + const enriched = + opts.recipeActions !== undefined && opts.recipeActions.length > 0 + ? rows.map((row) => attachActions(row, opts.recipeActions!)) + : rows; + const noBucketLabel = + opts.groupBy === "owner" ? "" : ""; + const grouped = groupRowsBy(enriched, bucketize.fn, noBucketLabel); + + if (opts.summary) { + return { + group_by: opts.groupBy, + groups: grouped.map((g) => ({ key: g.key, count: g.count })), + }; + } + return { group_by: opts.groupBy, groups: grouped }; + } + + if (opts.summary) { + return { count: rows.length }; + } + + if (opts.recipeActions !== undefined && opts.recipeActions.length > 0) { + return rows.map((row) => attachActions(row, opts.recipeActions!)); + } + return rows; + } catch (err) { + return { + error: err instanceof Error ? err.message : String(err), + }; + } finally { + closeDb(db, { readonly: true }); + } +} + +/** + * One statement in a batch. `string` form inherits all batch-wide + * defaults; object form overrides on a per-key basis. The MCP wrapper + * resolves these into `ExecuteQueryOpts` (including translating any + * `changed_since` strings into `changedFiles` sets) before calling + * the engine. + */ +export type BatchStatementResolved = Omit; + +/** + * Run N statements; one DB connection per call (cheap with `bun:sqlite`). + * Returns N envelopes — same per-element shape as single `executeQuery` + * for the effective flag set on that statement (plan § 5: "per-element + * shape mirrors single `query`'s output for the effective flag set"). + * + * Errors are per-statement: a failed statement returns `{error}` in its + * slot; sibling statements still execute. Matches the "partial success" + * semantic the agent expects when batching independent reads. + */ +export function executeQueryBatch(opts: { + statements: BatchStatementResolved[]; + root: string; +}): Array { + return opts.statements.map((s) => executeQuery({ ...s, root: opts.root })); +} + +function resolveBucketizer( + groupBy: GroupByMode, + root: string, +): { fn: Bucketizer } | ExecuteQueryError { + if (groupBy === "owner") { + const fn = loadCodeowners(root); + if (fn === null) { + return { + error: + "--group-by owner: no CODEOWNERS file found (looked in .github/CODEOWNERS, CODEOWNERS, docs/CODEOWNERS).", + }; + } + return { fn }; + } + if (groupBy === "package") { + return { fn: makePackageBucketizer(discoverWorkspaceRoots(root)) }; + } + return { fn: (path: string) => firstDirectory(path) }; +} + +function attachActions(row: unknown, actions: ReadonlyArray): unknown { + if (typeof row !== "object" || row === null) return row; + const obj = row as Record; + if ("actions" in obj) return obj; + return { ...obj, actions }; +} diff --git a/src/cli/bootstrap.ts b/src/cli/bootstrap.ts index afff682b..df838e65 100644 --- a/src/cli/bootstrap.ts +++ b/src/cli/bootstrap.ts @@ -25,6 +25,9 @@ Context (project snapshot envelope for any agent): Agents: codemap agents init [--force] [--interactive|-i] +MCP server (Model Context Protocol — for agent hosts): + codemap mcp # stdio JSON-RPC, one tool per CLI verb + Other: codemap version codemap --version, -V @@ -53,6 +56,7 @@ export function validateIndexModeArgs(rest: string[]): void { if (rest[0] === "validate") return; if (rest[0] === "context") return; if (rest[0] === "audit") return; + if (rest[0] === "mcp") return; if (rest[0] === "agents") { if (rest[1] === "init") return; diff --git a/src/cli/cmd-mcp.test.ts b/src/cli/cmd-mcp.test.ts new file mode 100644 index 00000000..354e532a --- /dev/null +++ b/src/cli/cmd-mcp.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from "bun:test"; + +import { parseMcpRest } from "./cmd-mcp"; + +describe("parseMcpRest", () => { + it("returns run with no extra args", () => { + const r = parseMcpRest(["mcp"]); + expect(r.kind).toBe("run"); + }); + + it("returns help on --help", () => { + expect(parseMcpRest(["mcp", "--help"]).kind).toBe("help"); + expect(parseMcpRest(["mcp", "-h"]).kind).toBe("help"); + }); + + it("errors on unknown flag", () => { + const r = parseMcpRest(["mcp", "--port", "3000"]); + expect(r.kind).toBe("error"); + if (r.kind === "error") expect(r.message).toContain("--port"); + }); + + it("throws if rest[0] is not 'mcp'", () => { + expect(() => parseMcpRest(["query"])).toThrow(); + }); +}); diff --git a/src/cli/cmd-mcp.ts b/src/cli/cmd-mcp.ts new file mode 100644 index 00000000..2a6a48d9 --- /dev/null +++ b/src/cli/cmd-mcp.ts @@ -0,0 +1,78 @@ +import { runMcpServer } from "../application/mcp-server"; +import { CODEMAP_VERSION } from "../version"; + +/** + * Parse `argv` after the global bootstrap: `rest[0]` must be `"mcp"`. + * v1 takes no MCP-specific flags — `--root` / `--config` are absorbed + * by the bootstrap layer and forwarded via `runMcpCmd`'s opts. + */ +export function parseMcpRest( + rest: string[], +): { kind: "help" } | { kind: "error"; message: string } | { kind: "run" } { + if (rest[0] !== "mcp") { + throw new Error("parseMcpRest: expected mcp"); + } + + for (let i = 1; i < rest.length; i++) { + const a = rest[i]; + if (a === "--help" || a === "-h") return { kind: "help" }; + return { + kind: "error", + message: `codemap mcp: unknown option "${a}". Run \`codemap mcp --help\` for usage.`, + }; + } + + return { kind: "run" }; +} + +export function printMcpCmdHelp(): void { + console.log(`Usage: codemap mcp + +Spawns an MCP (Model Context Protocol) server on stdio. Designed to be +launched by an agent host (Claude Code, Cursor, Codex, generic MCP +clients) — JSON-RPC on stdin/stdout, logs on stderr. + +Tools (one per CLI verb plus the MCP-only batch helper; snake_case): + query One read-only SQL statement. + query_batch N statements in one round-trip (MCP-only). + query_recipe Bundled SQL recipe by id; per-row \`actions\` hints. + audit Structural-drift audit ({head, deltas} envelope). + save_baseline Snapshot rows under a name (sql or recipe). + list_baselines Catalog of saved baselines. + drop_baseline Delete a baseline. + context Project bootstrap envelope. + validate On-disk hash vs indexed hash. + +Resources (lazy-cached on first read): + codemap://recipes Full recipe catalog. + codemap://recipes/{id} Single recipe (id, description, sql). + codemap://schema Live DDL of every table. + codemap://skill Bundled SKILL.md. + +Output shape is verbatim from each tool's CLI counterpart \`--json\` +envelope (no re-mapping). See docs/architecture.md § MCP wiring for +the engine seam and the agent rule + skill for query examples. + +Global flags (parsed by bootstrap, forwarded to the server): + --root Project root (defaults to cwd; respects CODEMAP_ROOT). + --config Config file path (defaults to codemap.config.{ts,js,json}). + +The server stays running until stdin closes (the agent host disconnects). +`); +} + +/** + * Entry-point for `codemap mcp`. Boots the MCP server over stdio and + * resolves when the transport closes (clean shutdown via stdin EOF). + * Bootstrap / DB / SDK errors propagate as exit code 1 via main. + */ +export async function runMcpCmd(opts: { + root: string; + configFile: string | undefined; +}): Promise { + await runMcpServer({ + version: CODEMAP_VERSION, + root: opts.root, + configFile: opts.configFile, + }); +} diff --git a/src/cli/main.ts b/src/cli/main.ts index edb88eac..82f832b3 100644 --- a/src/cli/main.ts +++ b/src/cli/main.ts @@ -108,6 +108,22 @@ Copies bundled agent templates into .agents/ under the project root. return; } + if (rest[0] === "mcp") { + const { parseMcpRest, printMcpCmdHelp, runMcpCmd } = + await import("./cmd-mcp.js"); + const parsed = parseMcpRest(rest); + if (parsed.kind === "help") { + printMcpCmdHelp(); + return; + } + if (parsed.kind === "error") { + console.error(parsed.message); + process.exit(1); + } + await runMcpCmd({ root, configFile }); + return; + } + if (rest[0] === "audit") { const { parseAuditRest, printAuditCmdHelp, runAuditCmd } = await import("./cmd-audit.js"); diff --git a/templates/agents/rules/codemap.md b/templates/agents/rules/codemap.md index 39dcca97..0d0d4c85 100644 --- a/templates/agents/rules/codemap.md +++ b/templates/agents/rules/codemap.md @@ -32,6 +32,7 @@ Install **[@stainless-code/codemap](https://www.npmjs.com/package/@stainless-cod | Save / diff a baseline | `codemap query --save-baseline -r visibility-tags` then `… --json --baseline -r visibility-tags` | | List / drop baselines | `codemap query --baselines` · `codemap query --drop-baseline ` | | Per-delta audit | `codemap audit --json --baseline base` (auto-resolves `base-files` / `base-dependencies` / `base-deprecated`) | +| MCP server (for agent hosts) | `codemap mcp` — JSON-RPC on stdio; one tool per CLI verb. See **MCP** section below. | **Recipe `actions`:** with **`--json`**, recipes that define an `actions` template append it to every row (kebab-case verb + description — e.g. `fan-out` → `review-coupling`). Under `--baseline`, actions attach to the **`added`** rows only. Inspect via **`--recipes-json`**. Ad-hoc SQL never carries actions. @@ -39,6 +40,16 @@ Install **[@stainless-code/codemap](https://www.npmjs.com/package/@stainless-cod **Audit (`codemap audit`)**: structural-drift command; emits `{head, deltas: {files, dependencies, deprecated}}` (each delta carries its own `base` metadata). Reuses B.6 baselines as the snapshot source. Two CLI shapes — `--baseline ` auto-resolves `-files` / `-dependencies` / `-deprecated`; `---baseline ` is the explicit per-delta override. v1 ships no `verdict` / threshold config — consumers compose `--json` + `jq` for CI exit codes. Auto-runs an incremental index before the diff (use `--no-index` to skip for frozen-DB CI). +**MCP server (`codemap mcp`)**: stdio MCP (Model Context Protocol) server — agents call codemap as JSON-RPC tools instead of shelling out to the CLI on every read. v1 ships one tool per CLI verb plus four lazy-cached resources: + +- **Tools:** `query` / `query_batch` / `query_recipe` / `audit` / `save_baseline` / `list_baselines` / `drop_baseline` / `context` / `validate`. Snake_case keys (Codemap convention matching MCP spec examples + reference servers — spec is convention-agnostic; CLI stays kebab). +- **`query_batch` (MCP-only):** N statements in one round-trip. Items are `string | {sql, summary?, changed_since?, group_by?}` — string form inherits batch-wide flag defaults, object form overrides on a per-key basis. Per-statement errors are isolated. +- **`save_baseline` (polymorphic):** one tool, `{name, sql? | recipe?}` with runtime exclusivity check (mirrors the CLI's single `--save-baseline=` verb). +- **Resources:** `codemap://recipes` (catalog), `codemap://recipes/{id}` (one recipe), `codemap://schema` (live DDL from `sqlite_schema`), `codemap://skill` (bundled SKILL.md text). Lazy-cached on first `read_resource`. +- **Output shape uniformity:** every tool returns the JSON envelope its CLI counterpart's `--json` would print — no re-mapping. + +To use from your agent host: launch `codemap mcp` as the MCP server command. Most hosts (Claude Code, Cursor, Codex) accept a stdio command + working directory; codemap will index the working directory's project root. + **Bundled rules/skills:** **`codemap agents init`** writes **`.agents/`** from the package (see [docs/agents.md](../../../docs/agents.md)). Index another project: **`--root /path/to/repo`**, or set **`CODEMAP_ROOT`** or **`CODEMAP_TEST_BENCH`** (e.g. in **`.env`** — see [docs/benchmark.md § Indexing another project](../../../docs/benchmark.md#indexing-another-project)). Full rebuild: **`--full`**. Targeted re-index: **`--files path/to/a.ts path/to/b.tsx`**. diff --git a/templates/agents/skills/codemap/SKILL.md b/templates/agents/skills/codemap/SKILL.md index 4994ecc0..515e72d6 100644 --- a/templates/agents/skills/codemap/SKILL.md +++ b/templates/agents/skills/codemap/SKILL.md @@ -53,6 +53,29 @@ Replace placeholders (`'...'`) with your module path, file glob, or symbol name. Each emitted delta carries its own `base` metadata so mixed-baseline audits are first-class. `--summary` collapses each delta to `{added: N, removed: N}`. `--no-index` skips the auto-incremental-index prelude (default is to re-index first so `head` reflects current source). v1 ships no `verdict` / threshold config — `codemap audit --json | jq -e '.deltas.dependencies.added | length <= 50'` is the CI exit-code idiom until v1.x ships native thresholds. Each delta pins a canonical SQL projection and validates baseline column-set membership before diffing — schema-bump-resilient (extras dropped, missing columns surface a clean re-save command). +**MCP server (`codemap mcp`)** — separate top-level command that exposes the entire CLI surface to agent hosts (Claude Code, Cursor, Codex, generic MCP clients) as JSON-RPC tools over stdio. Eliminates the bash round-trip on every agent call. Bootstrap once at server boot; tool handlers reuse the existing engine entry-points so output shape is verbatim from each tool's CLI counterpart's `--json` envelope. + +**Tools (snake_case keys — Codemap convention matching MCP spec examples + reference servers; spec is convention-agnostic. CLI stays kebab; translation lives at the MCP-arg layer.):** + +- **`query`** — one SQL statement. Args: `{sql, summary?, changed_since?, group_by?}`. Same envelope as `codemap query --json`. +- **`query_batch`** — MCP-only, no CLI counterpart. Args: `{statements: (string | {sql, summary?, changed_since?, group_by?})[], summary?, changed_since?, group_by?}`. Items are bare SQL strings (inherit batch-wide flag defaults) or objects (override on a per-key basis). Output is N-element array; per-element shape mirrors single-`query`'s output for that statement's effective flag set. Per-statement errors are isolated — failed statements return `{error}` in their slot; siblings still execute. SQL-only (no `recipe` polymorphism in items). +- **`query_recipe`** — `{recipe, summary?, changed_since?, group_by?}`. Resolves the recipe id to SQL + per-row actions, then executes like `query`. Unknown recipe id returns a structured `{error}` pointing at the `codemap://recipes` resource. +- **`audit`** — `{baseline_prefix?, baselines?: {files?, dependencies?, deprecated?}, summary?, no_index?}`. Composes per-delta baselines into the `{head, deltas}` envelope. Auto-runs incremental index unless `no_index: true`. +- **`save_baseline`** — polymorphic `{name, sql? | recipe?}` with runtime exclusivity check (mirrors the CLI's single `--save-baseline=` verb). Pass exactly one of `sql` or `recipe`. +- **`list_baselines`** — no args; returns the array `codemap query --baselines --json` would print. +- **`drop_baseline`** — `{name}`. Returns `{dropped: }` on success or `isError` if the name doesn't exist. +- **`context`** — `{compact?, intent?}`. Returns the project-bootstrap envelope (codemap version, schema version, file count, language breakdown, hubs, sample markers). Designed for agent session-start — one call replaces 4-5 `query` calls. +- **`validate`** — `{paths?: string[]}`. Compares on-disk SHA-256 to indexed `files.content_hash`; empty `paths` validates everything. Returns rows with status (`ok`/`stale`/`missing`/`unindexed`). + +**Resources (lazy-cached on first `read_resource`; constant for server-process lifetime):** + +- **`codemap://recipes`** — full catalog JSON (same as `--recipes-json`). +- **`codemap://recipes/{id}`** — single recipe `{id, description, sql, actions?}`. Replaces `--print-sql `. +- **`codemap://schema`** — DDL of every table in `.codemap.db` (queried live from `sqlite_schema`). +- **`codemap://skill`** — full text of this skill file. Agents that don't preload the skill at session start can fetch it here. + +**Launching:** point your agent host at `codemap mcp` as the stdio command. Most hosts (Claude Code, Cursor, Codex) accept `{command: "codemap", args: ["mcp"], cwd: "/path/to/project"}`. The server inherits `cwd` as the project root unless `--root` overrides it. + **Determinism:** Bundled recipes use stable secondary **`ORDER BY`** tie-breakers (and ordered inner **`LIMIT`** samples where applicable). Prefer **`--recipe`** over pasting SQL when you need the maintained ordering. **Canonical SQL** is whatever **`codemap query --print-sql `** or **`codemap query --recipes-json`** returns (single source in the CLI). The blocks below match **`fan-out`** and **`fan-out-sample`** in the bundled catalog; other recipes align with “Conditional aggregation”, “Codebase statistics”, and component sections later in this skill.