@reposkein/mcp 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/README.md +73 -1
  2. package/binary-digests.json +4 -4
  3. package/dist/SKILL.md +19 -0
  4. package/dist/cli/agentAdapters.d.ts +68 -0
  5. package/dist/cli/agentAdapters.js +223 -0
  6. package/dist/cli/agentAdapters.js.map +1 -0
  7. package/dist/cli/ansi.d.ts +24 -0
  8. package/dist/cli/ansi.js +40 -0
  9. package/dist/cli/ansi.js.map +1 -0
  10. package/dist/cli/doctor.d.ts +34 -2
  11. package/dist/cli/doctor.js +51 -6
  12. package/dist/cli/doctor.js.map +1 -1
  13. package/dist/cli/doctorFreshness.d.ts +43 -0
  14. package/dist/cli/doctorFreshness.js +111 -0
  15. package/dist/cli/doctorFreshness.js.map +1 -0
  16. package/dist/cli/doctorSummaries.d.ts +2 -0
  17. package/dist/cli/doctorSummaries.js +95 -0
  18. package/dist/cli/doctorSummaries.js.map +1 -0
  19. package/dist/cli/federatedDiscovery.d.ts +16 -0
  20. package/dist/cli/federatedDiscovery.js +75 -0
  21. package/dist/cli/federatedDiscovery.js.map +1 -0
  22. package/dist/cli/init.d.ts +69 -6
  23. package/dist/cli/init.js +194 -28
  24. package/dist/cli/init.js.map +1 -1
  25. package/dist/cli/sourceSlices.d.ts +17 -0
  26. package/dist/cli/sourceSlices.js +59 -0
  27. package/dist/cli/sourceSlices.js.map +1 -0
  28. package/dist/cli/stats.d.ts +29 -0
  29. package/dist/cli/stats.js +180 -0
  30. package/dist/cli/stats.js.map +1 -0
  31. package/dist/cli/view.d.ts +71 -3
  32. package/dist/cli/view.js +147 -12
  33. package/dist/cli/view.js.map +1 -1
  34. package/dist/index.d.ts +8 -0
  35. package/dist/index.js +286 -123
  36. package/dist/index.js.map +1 -1
  37. package/dist/indexer/fetchBinary.d.ts +14 -0
  38. package/dist/indexer/fetchBinary.js +81 -40
  39. package/dist/indexer/fetchBinary.js.map +1 -1
  40. package/dist/indexer/runIndexer.d.ts +0 -2
  41. package/dist/indexer/runIndexer.js +8 -4
  42. package/dist/indexer/runIndexer.js.map +1 -1
  43. package/dist/store/JsonlGraphStore.d.ts +13 -3
  44. package/dist/store/JsonlGraphStore.js +55 -17
  45. package/dist/store/JsonlGraphStore.js.map +1 -1
  46. package/dist/store/decisions.d.ts +20 -2
  47. package/dist/store/decisions.js +35 -2
  48. package/dist/store/decisions.js.map +1 -1
  49. package/dist/store/indexedAt.d.ts +18 -0
  50. package/dist/store/indexedAt.js +56 -0
  51. package/dist/store/indexedAt.js.map +1 -0
  52. package/dist/store/instrumentTool.d.ts +40 -0
  53. package/dist/store/instrumentTool.js +82 -0
  54. package/dist/store/instrumentTool.js.map +1 -0
  55. package/dist/store/repoContextCache.d.ts +15 -0
  56. package/dist/store/repoContextCache.js +26 -0
  57. package/dist/store/repoContextCache.js.map +1 -0
  58. package/dist/store/repoSession.d.ts +63 -0
  59. package/dist/store/repoSession.js +123 -0
  60. package/dist/store/repoSession.js.map +1 -0
  61. package/dist/store/resolveRepoPath.d.ts +31 -0
  62. package/dist/store/resolveRepoPath.js +85 -0
  63. package/dist/store/resolveRepoPath.js.map +1 -0
  64. package/dist/store/sessionLog.d.ts +98 -0
  65. package/dist/store/sessionLog.js +216 -0
  66. package/dist/store/sessionLog.js.map +1 -0
  67. package/dist/store/sessionStats.d.ts +55 -0
  68. package/dist/store/sessionStats.js +140 -0
  69. package/dist/store/sessionStats.js.map +1 -0
  70. package/dist/store/sidecar.d.ts +39 -3
  71. package/dist/store/sidecar.js +89 -5
  72. package/dist/store/sidecar.js.map +1 -1
  73. package/dist/store/summaryShards.d.ts +117 -0
  74. package/dist/store/summaryShards.js +291 -0
  75. package/dist/store/summaryShards.js.map +1 -0
  76. package/dist/store/teamConfig.d.ts +13 -0
  77. package/dist/store/teamConfig.js +49 -0
  78. package/dist/store/teamConfig.js.map +1 -0
  79. package/dist/tools/indexerTools.d.ts +14 -2
  80. package/dist/tools/indexerTools.js +17 -5
  81. package/dist/tools/indexerTools.js.map +1 -1
  82. package/dist/tools/recordDecision.js +6 -1
  83. package/dist/tools/recordDecision.js.map +1 -1
  84. package/dist/viz/assets/{graph.worker-CC7BeE-8.js → graph.worker-DkH6-lMU.js} +1 -1
  85. package/dist/viz/assets/index-CxqeGuRS.css +1 -0
  86. package/dist/viz/assets/index-DiNZyrXx.js +9 -0
  87. package/dist/viz/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  88. package/dist/viz/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
  89. package/dist/viz/assets/jetbrains-mono-latin-700-normal-BYuf6tUa.woff2 +0 -0
  90. package/dist/viz/assets/r3f-BZY3QR6_.js +336 -0
  91. package/dist/viz/assets/tanstack-CjXr-L-s.js +62 -0
  92. package/dist/viz/assets/three-Cpu5RUEM.js +4116 -0
  93. package/dist/viz/index.html +5 -4
  94. package/package.json +5 -4
  95. package/dist/viz/assets/index-C_8UkiJ8.js +0 -9
  96. package/dist/viz/assets/r3f-Cku1OhB-.js +0 -277
  97. package/dist/viz/assets/tanstack-DyXcNRWk.js +0 -77
  98. package/dist/viz/assets/three-Z8XaLGmf.js +0 -3839
package/README.md CHANGED
@@ -57,6 +57,8 @@ Then ask your agent *"what calls this function?"* or *"what breaks if I change X
57
57
 
58
58
  | Tool | What it does |
59
59
  | --- | --- |
60
+ | `list_repos` | enumerate the repos discovered by resolution — path, name, cheap node/edge counts. One entry in single-repo mode; workspace mode lists every sibling repo found |
61
+ | `select_repo` | set the session-active repo (by `path` or `name` from `list_repos`) for every repo-scoped tool below — how workspace mode resolves ambiguity, and how you switch repos without restarting the server |
60
62
  | `get_context_profile` | resolve a function/class → its caller/callee neighborhood as ready-to-read prose |
61
63
  | `semantic_find` | find where to start — rank functions/classes by meaning (lexical BM25F; optional pluggable embeddings), seeding `get_context_profile` |
62
64
  | `impact` | transitive callers of a function/class — split into impacted code vs covering tests — with counts and truncated flag |
@@ -71,6 +73,7 @@ Then ask your agent *"what calls this function?"* or *"what breaks if I change X
71
73
  - `reposkein-mcp init` — set up a repo (downloads the indexer, installs git hooks + the skill, builds the graph, prints an MCP config block).
72
74
  - `reposkein-mcp doctor` — health check (binary → index → MCP reachability).
73
75
  - `reposkein-mcp index` — rebuild the committed graph after big changes.
76
+ - `reposkein-mcp stats [--last | --session <id> | --all] [--json]` — session usage report: calls by tool, top queried nodes/files, ADRs/summaries written, session duration, and an *estimated* context-tokens-saved-vs-grep number. See [Session usage stats](#session-usage-stats) below.
74
77
  - `reposkein-mcp view [path]` — open the **constellation viewer**: a local, read-only, zero-infra web app (bound to `127.0.0.1`) that renders the committed `.reposkein` graph as an interactive 3D astronomy-style map. `--export <dir>` instead writes a self-contained static site (works from `file://` or any static host). See the [viewer section in the main README](https://github.com/reposkein/reposkein#visualize-the-graph--the-constellation-viewer), or **[try the live demo](https://reposkein.github.io/reposkein/)** (RepoSkein viewing its own graph).
75
78
 
76
79
  ## How your agent uses it
@@ -92,11 +95,80 @@ npx skills add reposkein/reposkein --all
92
95
 
93
96
  (`reposkein-mcp init` already installs the navigation skill for Claude Code; this adds it to Cursor, Codex, and 70+ other agents.)
94
97
 
98
+ ## Session usage stats
99
+
100
+ Every tool call is logged, server-side, as one JSONL line to
101
+ `.reposkein/local/sessions/<session-id>.jsonl` under the repo the call
102
+ touched — `{ts, tool, argsShape, resultBytes, nodeIds}`, never argument or
103
+ result *values* (only argument key names — no code/user text ever lands on
104
+ disk). Because logging happens in the server, this works for **any**
105
+ MCP-capable agent (Claude Code, Cursor, Codex, …), not just one. It's
106
+ zero-infra like everything else here: local-only, git-ignored (`local/`),
107
+ never blocks or fails a tool call — a logging failure is always swallowed.
108
+ Old sessions are pruned automatically (newest 50, or 30 days, whichever is
109
+ stricter).
110
+
111
+ ```sh
112
+ reposkein-mcp stats # last session, human-readable
113
+ reposkein-mcp stats --all # every retained session, combined
114
+ reposkein-mcp stats --session <id> # one specific session
115
+ reposkein-mcp stats --json # machine-readable
116
+ ```
117
+
118
+ The report includes calls by tool, the top queried nodes/files, ADRs and
119
+ semantic summaries written, session duration, and an **estimated**
120
+ context-tokens-saved-vs-grep figure (using the ~8.4× mean token ratio
121
+ measured in [`mcp/bench`](https://github.com/reposkein/reposkein/tree/main/mcp/bench) — always labeled an estimate, never claimed as measured).
122
+
123
+ **Claude Code Stop-hook recipe** — print a session's stats the moment it ends, by adding to `.claude/settings.json`:
124
+
125
+ ```json
126
+ {
127
+ "hooks": {
128
+ "Stop": [
129
+ {
130
+ "hooks": [
131
+ { "type": "command", "command": "reposkein-mcp stats --last" }
132
+ ]
133
+ }
134
+ ]
135
+ }
136
+ }
137
+ ```
138
+
95
139
  ## Configuration
96
140
 
141
+ ### Repo resolution (zero-config)
142
+
143
+ `REPOSKEIN_REPO_PATH` is optional — the server resolves the target repo from
144
+ the process's working directory. Full precedence:
145
+
146
+ 1. **`select_repo`** — an MCP tool call that sets the session-active repo
147
+ (by `path` or `name` from `list_repos`) for every repo-scoped tool call
148
+ made afterward, for the rest of the connection. The only one of these
149
+ that's mutable at runtime — everything below it is fixed at server start.
150
+ 2. `REPOSKEIN_REPO_PATH`, if set.
151
+ 3. **Walk up:** the nearest ancestor of the cwd containing `.reposkein/`.
152
+ 4. **Walk down** (workspace mode, only tried when no ancestor has one): scans
153
+ the cwd's children and grandchildren (skipping `node_modules`, `.git`,
154
+ `target`, `dist`, `.worktrees`, `.claude`) for `.reposkein/` dirs. One hit
155
+ is used automatically; two or more leave the repo unresolved until a
156
+ `select_repo` call picks one — `list_repos` always enumerates every repo
157
+ the walk finds (plus a `REPOSKEIN_REPO_PATH` pin, if one is set and not
158
+ already among them), regardless of which one is currently active.
159
+
160
+ If nothing resolves — or step 4 finds more than one repo and none has been
161
+ `select_repo`'d — repo-scoped tools don't fail silently at startup: each call
162
+ returns a structured error telling you what to run (`reposkein-mcp init`,
163
+ `list_repos`/`select_repo`, or which `REPOSKEIN_REPO_PATH` to set — naming the
164
+ candidates when ambiguous). `reposkein-mcp doctor` and `--help` never require
165
+ a resolved repo. See `mcp/src/store/resolveRepoPath.ts` and
166
+ `mcp/src/store/repoSession.ts`.
167
+
97
168
  | Env var | Purpose |
98
169
  | --- | --- |
99
- | `REPOSKEIN_REPO_PATH` | the repository the server operates on (required for repo-scoped tools) |
170
+ | `REPOSKEIN_REPO_PATH` | pins the repository the server operates on optional, see repo resolution above |
171
+ | `REPOSKEIN_SESSION_ID` | override the session id used for `reposkein-mcp stats` logging (default: start-timestamp + pid) |
100
172
  | `REPOSKEIN_STORE` | `auto` (default) · `jsonl` (zero-infra) · `neo4j` |
101
173
  | `REPOSKEIN_INDEXER_BIN` | override the `reposkein-indexer` binary path (unsupported platforms) |
102
174
  | `NEO4J_URI` / `NEO4J_USER` / `NEO4J_PASSWORD` | optional Neo4j backend (large graphs / Cypher at scale) |
@@ -1,6 +1,6 @@
1
1
  {
2
- "reposkein-indexer-darwin-arm64": "63426ed09bf4d530c7b76ee36c6740d7bd7a7be5cb97b5be6c629b4dbad2f68a",
3
- "reposkein-indexer-linux-arm64": "5eafecd08c8e2b9b9969eb3d5ab50d818734fe7f121c18c91d7dd2662b93beb5",
4
- "reposkein-indexer-linux-x64": "3d91beb9d43344065eb580c864ba894a361fb4ae3ec603999cb731af54adc6a5",
5
- "reposkein-indexer-win32-x64.exe": "61ee6547ccac78f1fe648750835a88c7b457130d686992fcd9c49bfb2b1bddea"
2
+ "reposkein-indexer-darwin-arm64": "8d00be1abda687bb847b6bbd3340c1d53f20c69668d8b65029f0c52966e0b5e8",
3
+ "reposkein-indexer-linux-arm64": "19853d8d8fb9cf7530c1613ec5dc2e0c0b4036a3691f716fb67e055055dea4f7",
4
+ "reposkein-indexer-linux-x64": "d07e83f81da1c8668f17dfb12252199099ada5de9bc13c078a590f52abf3ca1e",
5
+ "reposkein-indexer-win32-x64.exe": "ccce3f03e6b50cec117ef9d33eb8040b9356e12dec0baf43d509bcc7fb63d0dd"
6
6
  }
package/dist/SKILL.md CHANGED
@@ -14,8 +14,27 @@ calls — that you enrich with natural-language summaries just-in-time. Do NOT
14
14
  guess file dependencies, and do NOT explore the repo by directory listing or
15
15
  grep when the graph can answer structurally.
16
16
 
17
+ The server resolves the target repo automatically from your working
18
+ directory — no setup is required. If a tool call returns an error naming
19
+ `list_repos`/`select_repo` or `REPOSKEIN_REPO_PATH`, do not abandon these
20
+ tools and fall back to grep — the error is actionable:
21
+
22
+ - **No repo found at all** — run `reposkein-mcp init` in the repo (if it has
23
+ no `.reposkein/` yet), or set `REPOSKEIN_REPO_PATH`.
24
+ - **Multiple repos found (workspace mode)** — the error names the
25
+ candidates. Call `list_repos` to see them (path, name, node/edge counts),
26
+ then `select_repo` with one candidate's `path` or `name` — this sets the
27
+ active repo for the rest of the session, so you only do it once. Retry the
28
+ call that failed.
29
+
17
30
  ## Tools
18
31
 
32
+ - **`list_repos`** / **`select_repo`** — only needed in workspace mode
33
+ (multiple sibling repos, no unambiguous default). `list_repos` enumerates
34
+ what was discovered; `select_repo` (a `path` or `name` from that list) sets
35
+ the active repo for every repo-scoped tool below, for the rest of the
36
+ session. Skip these in the common single-repo case — resolution just works.
37
+
19
38
  - **`semantic_find`** — **start here when you don't have a seed symbol.** Rank
20
39
  functions/classes/interfaces/enums by a lexical match (BM25F) over their
21
40
  qualified names, signatures, and committed summaries. Use for cold-start "where
@@ -0,0 +1,68 @@
1
+ /** Per-agent MCP config adapters for `reposkein-mcp init`'s join mode
2
+ * (REP-16 T5). A "join" is: someone cloned a repo that already has RepoSkein
3
+ * set up (committed `.reposkein/meta.json`) — the one thing left to do is
4
+ * wire the local agent(s) up, which today means hand-copying a JSON snippet
5
+ * from docs/INSTALL.md §6. These adapters do that automatically.
6
+ *
7
+ * Schemas match docs/INSTALL.md §6 exactly — that doc is the spec:
8
+ * - Claude Code: `<repo>/.mcp.json`, `mcpServers` key. Prefers `claude mcp
9
+ * add` (scope "project", which itself targets .mcp.json) when the CLI is
10
+ * on PATH, so Claude's own approval/health-check flow sees the entry;
11
+ * falls back to writing the file directly.
12
+ * - OpenCode (+omo): `<repo>/opencode.json`, `mcp` key (NOT `mcpServers`).
13
+ * - Cursor: `<repo>/.cursor/mcp.json`, `mcpServers` key.
14
+ * - Unknown/no agent detected: falls back to `.mcp.json` (§6.6 "generic").
15
+ *
16
+ * Every write is idempotent (re-run = no-op, byte-identical file), dry-run
17
+ * aware (never touches disk when `dryRun: true`), and backs up any file it
18
+ * actually modifies to a timestamped `.bak` sibling first. */
19
+ export type AgentId = "claude" | "opencode" | "cursor";
20
+ export declare const ALL_AGENT_IDS: readonly AgentId[];
21
+ export type AdapterAction = "created" | "updated" | "unchanged" | "dry-run" | "error";
22
+ export interface AdapterResult {
23
+ agent: AgentId;
24
+ /** File this adapter wrote (or would write). For the "claude" adapter via
25
+ * the CLI this is still `.mcp.json` — `claude mcp add -s project` writes
26
+ * there under the hood. */
27
+ path: string;
28
+ action: AdapterAction;
29
+ /** Set only when an existing file was actually modified. */
30
+ backupPath?: string;
31
+ message: string;
32
+ }
33
+ export interface ExecResult {
34
+ status: number;
35
+ stdout: string;
36
+ stderr: string;
37
+ }
38
+ /** Injectable command runner — never throws (unlike execFileSync). Tests
39
+ * inject a fake to avoid shelling out to a real `claude`/`opencode` CLI or
40
+ * mutating a real project's MCP config. */
41
+ export type Exec = (cmd: string, args: string[], opts: {
42
+ cwd: string;
43
+ }) => ExecResult;
44
+ export declare const defaultExec: Exec;
45
+ /** Detects which agents are "present": their CLI is on PATH, or their config
46
+ * file already exists (someone opted in previously — keep writing there).
47
+ * Falls back to `["claude"]` (→ `.mcp.json`, the generic/broadest-compat
48
+ * target) when nothing is detected at all, matching docs/INSTALL.md §6.6. */
49
+ export declare function detectAgents(repoPath: string, exec?: Exec): AgentId[];
50
+ /** Parses a comma/space-separated `--agents` value into AgentId[], ignoring
51
+ * unknown tokens (never throws — an unrecognized agent name just writes
52
+ * nothing rather than crashing `init`). */
53
+ export declare function parseAgentsFlag(value: string): AgentId[];
54
+ export interface WriteAgentConfigsOptions {
55
+ /** Explicit `--agents` override — write exactly these, skipping detection. */
56
+ agents?: AgentId[];
57
+ dryRun?: boolean;
58
+ /** Injectable for tests. */
59
+ exec?: Exec;
60
+ now?: () => Date;
61
+ }
62
+ /** Writes (or plans, under `dryRun`) the local agent MCP config for every
63
+ * detected/requested agent. Pure w.r.t. its inputs beyond `exec`/`now`/fs —
64
+ * same repo state + same options always produces the same plan, which is
65
+ * what makes idempotence and dry-run trustworthy. */
66
+ export declare function writeAgentConfigs(repoPath: string, opts?: WriteAgentConfigsOptions): AdapterResult[];
67
+ /** One line per adapter result, for `init`'s stderr summary. */
68
+ export declare function formatAdapterResult(r: AdapterResult): string;
@@ -0,0 +1,223 @@
1
+ /** Per-agent MCP config adapters for `reposkein-mcp init`'s join mode
2
+ * (REP-16 T5). A "join" is: someone cloned a repo that already has RepoSkein
3
+ * set up (committed `.reposkein/meta.json`) — the one thing left to do is
4
+ * wire the local agent(s) up, which today means hand-copying a JSON snippet
5
+ * from docs/INSTALL.md §6. These adapters do that automatically.
6
+ *
7
+ * Schemas match docs/INSTALL.md §6 exactly — that doc is the spec:
8
+ * - Claude Code: `<repo>/.mcp.json`, `mcpServers` key. Prefers `claude mcp
9
+ * add` (scope "project", which itself targets .mcp.json) when the CLI is
10
+ * on PATH, so Claude's own approval/health-check flow sees the entry;
11
+ * falls back to writing the file directly.
12
+ * - OpenCode (+omo): `<repo>/opencode.json`, `mcp` key (NOT `mcpServers`).
13
+ * - Cursor: `<repo>/.cursor/mcp.json`, `mcpServers` key.
14
+ * - Unknown/no agent detected: falls back to `.mcp.json` (§6.6 "generic").
15
+ *
16
+ * Every write is idempotent (re-run = no-op, byte-identical file), dry-run
17
+ * aware (never touches disk when `dryRun: true`), and backs up any file it
18
+ * actually modifies to a timestamped `.bak` sibling first. */
19
+ import { existsSync, readFileSync, writeFileSync, copyFileSync, mkdirSync } from "node:fs";
20
+ import { execFileSync } from "node:child_process";
21
+ import { dirname, join, resolve } from "node:path";
22
+ export const ALL_AGENT_IDS = ["claude", "opencode", "cursor"];
23
+ export const defaultExec = (cmd, args, opts) => {
24
+ try {
25
+ const stdout = execFileSync(cmd, args, { cwd: opts.cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
26
+ return { status: 0, stdout, stderr: "" };
27
+ }
28
+ catch (err) {
29
+ const e = err;
30
+ return {
31
+ status: typeof e.status === "number" ? e.status : 1,
32
+ stdout: e.stdout ? e.stdout.toString() : "",
33
+ stderr: e.stderr ? e.stderr.toString() : String(e.message ?? err),
34
+ };
35
+ }
36
+ };
37
+ function commandAvailable(exec, cmd, cwd) {
38
+ return exec(cmd, ["--version"], { cwd }).status === 0;
39
+ }
40
+ /** Detects which agents are "present": their CLI is on PATH, or their config
41
+ * file already exists (someone opted in previously — keep writing there).
42
+ * Falls back to `["claude"]` (→ `.mcp.json`, the generic/broadest-compat
43
+ * target) when nothing is detected at all, matching docs/INSTALL.md §6.6. */
44
+ export function detectAgents(repoPath, exec = defaultExec) {
45
+ const present = [];
46
+ if (existsSync(join(repoPath, ".mcp.json")) || commandAvailable(exec, "claude", repoPath))
47
+ present.push("claude");
48
+ if (existsSync(join(repoPath, "opencode.json")) || commandAvailable(exec, "opencode", repoPath))
49
+ present.push("opencode");
50
+ if (existsSync(join(repoPath, ".cursor")))
51
+ present.push("cursor");
52
+ if (present.length === 0)
53
+ present.push("claude");
54
+ return present;
55
+ }
56
+ /** Parses a comma/space-separated `--agents` value into AgentId[], ignoring
57
+ * unknown tokens (never throws — an unrecognized agent name just writes
58
+ * nothing rather than crashing `init`). */
59
+ export function parseAgentsFlag(value) {
60
+ const known = new Set(ALL_AGENT_IDS);
61
+ return value
62
+ .split(/[,\s]+/)
63
+ .map((s) => s.trim().toLowerCase())
64
+ .filter((s) => known.has(s));
65
+ }
66
+ function timestampSuffix(now) {
67
+ return now.toISOString().replace(/[:.]/g, "-");
68
+ }
69
+ /** Copies `path` to `<path>.<timestamp>.bak` before it's modified. */
70
+ function backupFile(path, now) {
71
+ const backupPath = `${path}.${timestampSuffix(now)}.bak`;
72
+ copyFileSync(path, backupPath);
73
+ return backupPath;
74
+ }
75
+ const MCP_JSON_SPEC = {
76
+ relPath: ".mcp.json",
77
+ topKey: "mcpServers",
78
+ buildEntry: (absRepoPath) => ({ command: "reposkein-mcp", env: { REPOSKEIN_REPO_PATH: absRepoPath } }),
79
+ };
80
+ const OPENCODE_JSON_SPEC = {
81
+ relPath: "opencode.json",
82
+ topKey: "mcp",
83
+ buildEntry: (absRepoPath) => ({
84
+ type: "local",
85
+ command: ["reposkein-mcp"],
86
+ environment: { REPOSKEIN_REPO_PATH: absRepoPath },
87
+ enabled: true,
88
+ }),
89
+ };
90
+ const CURSOR_JSON_SPEC = {
91
+ relPath: join(".cursor", "mcp.json"),
92
+ topKey: "mcpServers",
93
+ buildEntry: (absRepoPath) => ({ command: "reposkein-mcp", env: { REPOSKEIN_REPO_PATH: absRepoPath } }),
94
+ };
95
+ /** Idempotently upserts `doc[topKey].reposkein = entry` in a JSON config
96
+ * file, preserving every other key untouched (other MCP servers, `$schema`,
97
+ * etc.). Never guesses at a corrupt/hand-edited existing file — see the
98
+ * early return below: it backs the file up (real run only) and reports an
99
+ * "error" result instead of overwriting it with a reposkein-only doc,
100
+ * which would silently destroy whatever else was in there. */
101
+ function upsertJsonAdapter(repoPath, spec, dryRun, now) {
102
+ const path = join(repoPath, spec.relPath);
103
+ const exists = existsSync(path);
104
+ let doc = {};
105
+ if (exists) {
106
+ let raw;
107
+ try {
108
+ raw = readFileSync(path, "utf8");
109
+ }
110
+ catch (err) {
111
+ return {
112
+ path,
113
+ action: "error",
114
+ message: `could not read ${spec.relPath}: ${err instanceof Error ? err.message : String(err)}`,
115
+ };
116
+ }
117
+ try {
118
+ doc = JSON.parse(raw);
119
+ }
120
+ catch (err) {
121
+ const parseMessage = err instanceof Error ? err.message : String(err);
122
+ if (dryRun) {
123
+ return {
124
+ path,
125
+ action: "error",
126
+ message: `${spec.relPath} is not valid JSON (${parseMessage}) — left untouched. ` +
127
+ "Fix the JSON and re-run, or delete the file.",
128
+ };
129
+ }
130
+ const backupPath = backupFile(path, now);
131
+ return {
132
+ path,
133
+ action: "error",
134
+ backupPath,
135
+ message: `${spec.relPath} is not valid JSON (${parseMessage}) — left untouched, backed up to ${backupPath}. ` +
136
+ "Fix the JSON and re-run, or delete the file.",
137
+ };
138
+ }
139
+ }
140
+ const section = doc[spec.topKey] ?? {};
141
+ const entry = spec.buildEntry(resolve(repoPath));
142
+ if (JSON.stringify(section.reposkein ?? null) === JSON.stringify(entry)) {
143
+ return { path, action: "unchanged", message: `${spec.relPath} already has an up-to-date reposkein entry` };
144
+ }
145
+ if (dryRun) {
146
+ return {
147
+ path,
148
+ action: "dry-run",
149
+ message: `would ${exists ? "update" : "create"} ${spec.relPath} (${spec.topKey}.reposkein)`,
150
+ };
151
+ }
152
+ let backupPath;
153
+ if (exists)
154
+ backupPath = backupFile(path, now);
155
+ doc[spec.topKey] = { ...section, reposkein: entry };
156
+ mkdirSync(dirname(path), { recursive: true });
157
+ writeFileSync(path, JSON.stringify(doc, null, 2) + "\n", "utf8");
158
+ return { path, action: exists ? "updated" : "created", backupPath, message: `${exists ? "updated" : "created"} ${spec.relPath}` };
159
+ }
160
+ function claudeCliEntryExists(exec, repoPath) {
161
+ return exec("claude", ["mcp", "get", "reposkein"], { cwd: repoPath }).status === 0;
162
+ }
163
+ /** Writes the Claude Code entry via `claude mcp add -s project` (which
164
+ * itself targets `.mcp.json`), so Claude's own health-check/approval UI
165
+ * picks it up immediately. Idempotent via `claude mcp get` — a re-run
166
+ * no-ops instead of erroring on a duplicate name. */
167
+ function writeViaClaudeCli(repoPath, dryRun, exec) {
168
+ const path = join(repoPath, ".mcp.json");
169
+ if (claudeCliEntryExists(exec, repoPath)) {
170
+ return { agent: "claude", path, action: "unchanged", message: "`claude mcp get reposkein` already resolves — nothing to add" };
171
+ }
172
+ const args = ["mcp", "add", "reposkein", "-s", "project", "-e", `REPOSKEIN_REPO_PATH=${resolve(repoPath)}`, "--", "reposkein-mcp"];
173
+ if (dryRun) {
174
+ return { agent: "claude", path, action: "dry-run", message: `would run: claude ${args.join(" ")}` };
175
+ }
176
+ const r = exec("claude", args, { cwd: repoPath });
177
+ if (r.status !== 0) {
178
+ return { agent: "claude", path, action: "error", message: `claude mcp add failed: ${(r.stderr || r.stdout).trim()}` };
179
+ }
180
+ return { agent: "claude", path, action: "created", message: "added via `claude mcp add` (project scope → .mcp.json)" };
181
+ }
182
+ /** Writes (or plans, under `dryRun`) the local agent MCP config for every
183
+ * detected/requested agent. Pure w.r.t. its inputs beyond `exec`/`now`/fs —
184
+ * same repo state + same options always produces the same plan, which is
185
+ * what makes idempotence and dry-run trustworthy. */
186
+ export function writeAgentConfigs(repoPath, opts = {}) {
187
+ const exec = opts.exec ?? defaultExec;
188
+ const now = (opts.now ?? (() => new Date()))();
189
+ const dryRun = !!opts.dryRun;
190
+ const agents = opts.agents ?? detectAgents(repoPath, exec);
191
+ const results = [];
192
+ for (const agent of agents) {
193
+ if (agent === "claude") {
194
+ if (commandAvailable(exec, "claude", repoPath)) {
195
+ results.push(writeViaClaudeCli(repoPath, dryRun, exec));
196
+ }
197
+ else {
198
+ const r = upsertJsonAdapter(repoPath, MCP_JSON_SPEC, dryRun, now);
199
+ results.push({ agent: "claude", ...r });
200
+ }
201
+ }
202
+ else if (agent === "opencode") {
203
+ const r = upsertJsonAdapter(repoPath, OPENCODE_JSON_SPEC, dryRun, now);
204
+ results.push({ agent: "opencode", ...r });
205
+ }
206
+ else if (agent === "cursor") {
207
+ const r = upsertJsonAdapter(repoPath, CURSOR_JSON_SPEC, dryRun, now);
208
+ results.push({ agent: "cursor", ...r });
209
+ }
210
+ }
211
+ return results;
212
+ }
213
+ /** One line per adapter result, for `init`'s stderr summary. */
214
+ export function formatAdapterResult(r) {
215
+ const verb = r.action === "created" ? "wrote" :
216
+ r.action === "updated" ? "updated" :
217
+ r.action === "unchanged" ? "unchanged" :
218
+ r.action === "dry-run" ? "[dry-run]" :
219
+ "ERROR";
220
+ const backup = r.backupPath ? ` (backup: ${r.backupPath})` : "";
221
+ return `reposkein: [${r.agent}] ${verb} — ${r.message}${backup}`;
222
+ }
223
+ //# sourceMappingURL=agentAdapters.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agentAdapters.js","sourceRoot":"","sources":["../../src/cli/agentAdapters.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ /** Dependency-free ANSI styling for `reposkein-mcp stats` — raw escape codes,
2
+ * no chalk/picocolors. Auto-disables when stdout isn't a TTY or `NO_COLOR`
3
+ * is set (https://no-color.org), per the zero-infra/deterministic-tooling
4
+ * convention: never assume a terminal, never require a dependency for a
5
+ * cosmetic feature.
6
+ *
7
+ * Colors: teal `#2DD4BF` ≈ ANSI 256 color 44 (`38;5;44`), amber ≈ 256 color
8
+ * 214 (`38;5;214`) — same palette as the project's README banners. Falls
9
+ * back to the portable 16-color codes (36 cyan, 33 yellow) is intentionally
10
+ * NOT done: 256-color support is effectively universal on anything that
11
+ * reports `isTTY`, and matching the brand hex matters more here than
12
+ * supporting truly ancient terminals for a purely cosmetic CLI report. */
13
+ export declare function colorEnabled(stream?: {
14
+ isTTY?: boolean;
15
+ }, env?: NodeJS.ProcessEnv): boolean;
16
+ export interface Styler {
17
+ teal(s: string): string;
18
+ amber(s: string): string;
19
+ bold(s: string): string;
20
+ dim(s: string): string;
21
+ }
22
+ /** Returns the styling functions to use, given whether color is enabled —
23
+ * callers pass `colorEnabled()`'s result so this stays pure/testable. */
24
+ export declare function styler(enabled: boolean): Styler;
@@ -0,0 +1,40 @@
1
+ /** Dependency-free ANSI styling for `reposkein-mcp stats` — raw escape codes,
2
+ * no chalk/picocolors. Auto-disables when stdout isn't a TTY or `NO_COLOR`
3
+ * is set (https://no-color.org), per the zero-infra/deterministic-tooling
4
+ * convention: never assume a terminal, never require a dependency for a
5
+ * cosmetic feature.
6
+ *
7
+ * Colors: teal `#2DD4BF` ≈ ANSI 256 color 44 (`38;5;44`), amber ≈ 256 color
8
+ * 214 (`38;5;214`) — same palette as the project's README banners. Falls
9
+ * back to the portable 16-color codes (36 cyan, 33 yellow) is intentionally
10
+ * NOT done: 256-color support is effectively universal on anything that
11
+ * reports `isTTY`, and matching the brand hex matters more here than
12
+ * supporting truly ancient terminals for a purely cosmetic CLI report. */
13
+ const RESET = "\x1b[0m";
14
+ const BOLD = "\x1b[1m";
15
+ const DIM = "\x1b[2m";
16
+ const TEAL = "\x1b[38;5;44m";
17
+ const AMBER = "\x1b[38;5;214m";
18
+ export function colorEnabled(stream = process.stdout, env = process.env) {
19
+ if (env.NO_COLOR !== undefined)
20
+ return false;
21
+ return !!stream.isTTY;
22
+ }
23
+ const identity = {
24
+ teal: (s) => s,
25
+ amber: (s) => s,
26
+ bold: (s) => s,
27
+ dim: (s) => s,
28
+ };
29
+ const styled = {
30
+ teal: (s) => `${TEAL}${s}${RESET}`,
31
+ amber: (s) => `${AMBER}${s}${RESET}`,
32
+ bold: (s) => `${BOLD}${s}${RESET}`,
33
+ dim: (s) => `${DIM}${s}${RESET}`,
34
+ };
35
+ /** Returns the styling functions to use, given whether color is enabled —
36
+ * callers pass `colorEnabled()`'s result so this stays pure/testable. */
37
+ export function styler(enabled) {
38
+ return enabled ? styled : identity;
39
+ }
40
+ //# sourceMappingURL=ansi.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ansi.js","sourceRoot":"","sources":["../../src/cli/ansi.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;2EAW2E;AAE3E,MAAM,KAAK,GAAG,SAAS,CAAC;AACxB,MAAM,IAAI,GAAG,SAAS,CAAC;AACvB,MAAM,GAAG,GAAG,SAAS,CAAC;AACtB,MAAM,IAAI,GAAG,eAAe,CAAC;AAC7B,MAAM,KAAK,GAAG,gBAAgB,CAAC;AAE/B,MAAM,UAAU,YAAY,CAAC,SAA8B,OAAO,CAAC,MAAM,EAAE,MAAyB,OAAO,CAAC,GAAG;IAC7G,IAAI,GAAG,CAAC,QAAQ,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC;IAC7C,OAAO,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;AACxB,CAAC;AASD,MAAM,QAAQ,GAAW;IACvB,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACd,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACf,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACd,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,GAAW;IACrB,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,IAAI,GAAG,CAAC,GAAG,KAAK,EAAE;IAClC,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,KAAK,GAAG,CAAC,GAAG,KAAK,EAAE;IACpC,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,IAAI,GAAG,CAAC,GAAG,KAAK,EAAE;IAClC,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,GAAG,CAAC,GAAG,KAAK,EAAE;CACjC,CAAC;AAEF;0EAC0E;AAC1E,MAAM,UAAU,MAAM,CAAC,OAAgB;IACrC,OAAO,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC;AACrC,CAAC"}
@@ -1,3 +1,15 @@
1
+ export interface DoctorPathResolution {
2
+ path: string;
3
+ /** Set when walk-down found 2+ candidate repos and neither an explicit
4
+ * path nor REPOSKEIN_REPO_PATH picked one — `path` falls back to `cwd`
5
+ * in this case so the caller can still print a real (failing) report. */
6
+ error?: string;
7
+ }
8
+ /** Resolves the repo path for `reposkein-mcp doctor [path]`: explicit arg >
9
+ * REPOSKEIN_REPO_PATH > walk-up (nearest ancestor with .reposkein/) >
10
+ * walk-down (workspace mode). Verifies doctor works from a subdirectory
11
+ * cwd — the common case of running it somewhere other than the repo root. */
12
+ export declare function resolveDoctorRepoPath(explicitPath: string | undefined, cwd: string, envRepoPath: string | undefined): DoctorPathResolution;
1
13
  export interface Check {
2
14
  id: string;
3
15
  label: string;
@@ -15,5 +27,25 @@ export interface DoctorReport {
15
27
  * Does NOT check MCP host registration — a CLI can't see that; the
16
28
  * reposkein-setup skill verifies reachability via a probe tool call. */
17
29
  export declare function runChecks(repoPath: string): Promise<DoctorReport>;
18
- /** Entry point for `reposkein-mcp doctor [path] [--json]`. Returns process exit code. */
19
- export declare function runDoctor(repoPath?: string, json?: boolean): Promise<number>;
30
+ /** Check ids that `doctor --ci` treats as build-breaking even though they're
31
+ * non-critical for interactive use (degrade gracefully for a human at the
32
+ * keyboard, but should fail a CI job so drift gets caught before it's
33
+ * merged): missing/foreign git hooks, a stale committed graph, and an
34
+ * unsplit legacy `summaries.jsonl` (see docs/INSTALL.md §4.2 — every branch
35
+ * that writes a summary would conflict on it). */
36
+ export declare const CI_FAIL_IDS: Set<string>;
37
+ /** Checks (by id) that `doctor --ci` additionally fails on, beyond the
38
+ * normal critical-check gate. */
39
+ export declare function ciFailingChecks(report: DoctorReport): Check[];
40
+ export declare function renderDoctorReport(report: DoctorReport): string;
41
+ export interface RunDoctorOptions {
42
+ json?: boolean;
43
+ /** `doctor --ci`: additionally fail (non-zero exit) when any CI_FAIL_IDS
44
+ * check is non-ok, even though those checks are non-critical for
45
+ * interactive use. Doesn't change what's printed — only the exit code. */
46
+ ci?: boolean;
47
+ }
48
+ /** Entry point for `reposkein-mcp doctor [path] [--json] [--ci]`. Returns the
49
+ * process exit code. Accepts a bare boolean for `json` for backwards
50
+ * compatibility with the pre-`--ci` call signature. */
51
+ export declare function runDoctor(repoPath?: string, opts?: RunDoctorOptions | boolean): Promise<number>;
@@ -3,7 +3,27 @@ import { join } from "node:path";
3
3
  import { ensureIndexerBinary } from "../indexer/fetchBinary.js";
4
4
  import { spawnIndexer } from "../indexer/runIndexer.js";
5
5
  import { resolveRepoId } from "../store/repoId.js";
6
+ import { resolveRepoPath } from "../store/resolveRepoPath.js";
6
7
  import { decisionChecks } from "./doctorDecisions.js";
8
+ import { summaryChecks } from "./doctorSummaries.js";
9
+ import { hooksCheck, graphStaleCheck } from "./doctorFreshness.js";
10
+ /** Resolves the repo path for `reposkein-mcp doctor [path]`: explicit arg >
11
+ * REPOSKEIN_REPO_PATH > walk-up (nearest ancestor with .reposkein/) >
12
+ * walk-down (workspace mode). Verifies doctor works from a subdirectory
13
+ * cwd — the common case of running it somewhere other than the repo root. */
14
+ export function resolveDoctorRepoPath(explicitPath, cwd, envRepoPath) {
15
+ const resolution = resolveRepoPath({ cwd, envRepoPath, explicit: explicitPath });
16
+ if (resolution.repoPath)
17
+ return { path: resolution.repoPath };
18
+ if (resolution.candidates && resolution.candidates.length > 0) {
19
+ return {
20
+ path: cwd,
21
+ error: `multiple RepoSkein repos found under ${cwd}: ${resolution.candidates.join(", ")}. ` +
22
+ "Pass one explicitly: `reposkein-mcp doctor <path>` (or set REPOSKEIN_REPO_PATH).",
23
+ };
24
+ }
25
+ return { path: cwd };
26
+ }
7
27
  /** Counts non-empty lines (≈ node count) without parsing every row. */
8
28
  function countLines(text) {
9
29
  let n = 0;
@@ -66,12 +86,30 @@ export async function runChecks(repoPath) {
66
86
  detail: repoId ?? "could not resolve a repo id",
67
87
  fix: repoId ? undefined : "set REPOSKEIN_REPO_PATH (or REPOSKEIN_REPO_ID) for the MCP server",
68
88
  });
69
- // 4) Decision log validation (all non-critical: degrade, don't block).
89
+ // 4) Committed summary shards (all non-critical: degrade, don't block).
90
+ checks.push(...summaryChecks(repoPath));
91
+ // 5) Decision log validation (all non-critical: degrade, don't block).
70
92
  checks.push(...decisionChecks(repoPath));
93
+ // 6) Git hooks installed + graph freshness (non-critical here; `doctor
94
+ // --ci` promotes both to a failing exit code — see CI_FAIL_IDS below).
95
+ checks.push(hooksCheck(repoPath));
96
+ checks.push(graphStaleCheck(repoPath));
71
97
  const ok = checks.filter((c) => c.critical).every((c) => c.ok);
72
98
  return { repoPath, ok, checks };
73
99
  }
74
- function render(report) {
100
+ /** Check ids that `doctor --ci` treats as build-breaking even though they're
101
+ * non-critical for interactive use (degrade gracefully for a human at the
102
+ * keyboard, but should fail a CI job so drift gets caught before it's
103
+ * merged): missing/foreign git hooks, a stale committed graph, and an
104
+ * unsplit legacy `summaries.jsonl` (see docs/INSTALL.md §4.2 — every branch
105
+ * that writes a summary would conflict on it). */
106
+ export const CI_FAIL_IDS = new Set(["hooks_installed", "graph_stale", "summaries_unsplit"]);
107
+ /** Checks (by id) that `doctor --ci` additionally fails on, beyond the
108
+ * normal critical-check gate. */
109
+ export function ciFailingChecks(report) {
110
+ return report.checks.filter((c) => CI_FAIL_IDS.has(c.id) && !c.ok);
111
+ }
112
+ export function renderDoctorReport(report) {
75
113
  const lines = [`reposkein doctor — ${report.repoPath}`, ""];
76
114
  for (const c of report.checks) {
77
115
  lines.push(`${c.ok ? "✓" : "✗"} ${c.label}: ${c.detail}`);
@@ -83,13 +121,20 @@ function render(report) {
83
121
  lines.push("(Note: this checks prerequisites only. To confirm the MCP server is wired into your agent, ask it to call get_context_profile — see the reposkein-setup skill.)");
84
122
  return lines.join("\n");
85
123
  }
86
- /** Entry point for `reposkein-mcp doctor [path] [--json]`. Returns process exit code. */
87
- export async function runDoctor(repoPath = ".", json = false) {
124
+ /** Entry point for `reposkein-mcp doctor [path] [--json] [--ci]`. Returns the
125
+ * process exit code. Accepts a bare boolean for `json` for backwards
126
+ * compatibility with the pre-`--ci` call signature. */
127
+ export async function runDoctor(repoPath = ".", opts = {}) {
128
+ const { json = false, ci = false } = typeof opts === "boolean" ? { json: opts } : opts;
88
129
  const report = await runChecks(repoPath);
89
130
  if (json)
90
131
  console.log(JSON.stringify(report, null, 2));
91
132
  else
92
- console.error(render(report));
93
- return report.ok ? 0 : 1;
133
+ console.error(renderDoctorReport(report));
134
+ const ciFailures = ci ? ciFailingChecks(report) : [];
135
+ if (ci && ciFailures.length > 0 && !json) {
136
+ console.error(`\n--ci: failing on ${ciFailures.length} additional check(s): ${ciFailures.map((c) => c.id).join(", ")}`);
137
+ }
138
+ return report.ok && ciFailures.length === 0 ? 0 : 1;
94
139
  }
95
140
  //# sourceMappingURL=doctor.js.map