projectstore-claude 0.29.2 → 0.29.4

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 (76) hide show
  1. package/README.md +1 -1
  2. package/bin/projectstore-claude.mjs +1 -1
  3. package/node_modules/projectstore/.claude-plugin/marketplace.json +1 -1
  4. package/node_modules/projectstore/.claude-plugin/plugin.json +2 -1
  5. package/node_modules/projectstore/README.md +44 -7
  6. package/node_modules/projectstore/commands/bar.md +30 -0
  7. package/node_modules/projectstore/commands/bind.md +16 -20
  8. package/node_modules/projectstore/commands/doctor.md +12 -7
  9. package/node_modules/projectstore/commands/scaffold.md +24 -16
  10. package/node_modules/projectstore/commands/status.md +1 -1
  11. package/node_modules/projectstore/commands/statusline.md +2 -2
  12. package/node_modules/projectstore/commands/story.md +15 -2
  13. package/node_modules/projectstore/docs/alongside.md +167 -0
  14. package/node_modules/projectstore/docs/extending.md +17 -3
  15. package/node_modules/projectstore/docs/getting-started.md +18 -9
  16. package/node_modules/projectstore/docs/harnesses.md +1 -1
  17. package/node_modules/projectstore/docs/how-it-works.md +12 -3
  18. package/node_modules/projectstore/harnesses/claude-code.json +23 -8
  19. package/node_modules/projectstore/harnesses/codex.json +16 -6
  20. package/node_modules/projectstore/hooks/band-state.d.ts +44 -0
  21. package/node_modules/projectstore/hooks/band.tsx +622 -0
  22. package/node_modules/projectstore/hooks/hooks.json +1 -0
  23. package/node_modules/projectstore/hooks/pre-compact.mjs +7 -10
  24. package/node_modules/projectstore/hooks/session-rules.mjs +14 -8
  25. package/node_modules/projectstore/hooks/session-start.mjs +56 -20
  26. package/node_modules/projectstore/hooks/session-stop.mjs +8 -6
  27. package/node_modules/projectstore/package.json +12 -2
  28. package/node_modules/projectstore/scaffold/headings.json +8 -0
  29. package/node_modules/projectstore/scripts/band-shared.mjs +446 -0
  30. package/node_modules/projectstore/scripts/band.mjs +207 -0
  31. package/node_modules/projectstore/scripts/binding.mjs +733 -62
  32. package/node_modules/projectstore/scripts/build-adapters.mjs +98 -35
  33. package/node_modules/projectstore/scripts/cli.mjs +1463 -267
  34. package/node_modules/projectstore/scripts/codemap.mjs +22 -7
  35. package/node_modules/projectstore/scripts/convergence.mjs +638 -0
  36. package/node_modules/projectstore/scripts/diff-refs.mjs +14 -8
  37. package/node_modules/projectstore/scripts/doctor-report.mjs +253 -0
  38. package/node_modules/projectstore/scripts/doctor.mjs +1384 -420
  39. package/node_modules/projectstore/scripts/draft.mjs +5 -2
  40. package/node_modules/projectstore/scripts/fetch-shell.mjs +369 -0
  41. package/node_modules/projectstore/scripts/graph.mjs +19 -13
  42. package/node_modules/projectstore/scripts/harness.mjs +283 -37
  43. package/node_modules/projectstore/scripts/install-harness.mjs +4701 -575
  44. package/node_modules/projectstore/scripts/kanban.mjs +6 -21
  45. package/node_modules/projectstore/scripts/lib.mjs +933 -199
  46. package/node_modules/projectstore/scripts/mcp.mjs +386 -105
  47. package/node_modules/projectstore/scripts/portable-registration.mjs +276 -56
  48. package/node_modules/projectstore/scripts/provenance.mjs +26 -10
  49. package/node_modules/projectstore/scripts/query.mjs +441 -72
  50. package/node_modules/projectstore/scripts/reconcile.mjs +55 -18
  51. package/node_modules/projectstore/scripts/run-host.mjs +139 -0
  52. package/node_modules/projectstore/scripts/scaffold.mjs +244 -0
  53. package/node_modules/projectstore/scripts/setup.mjs +987 -0
  54. package/node_modules/projectstore/scripts/stages.mjs +84 -0
  55. package/node_modules/projectstore/scripts/statusline-launcher.mjs +36 -11
  56. package/node_modules/projectstore/scripts/statusline.mjs +55 -14
  57. package/node_modules/projectstore/scripts/story-section.mjs +39 -17
  58. package/node_modules/projectstore/scripts/surfaces.mjs +485 -99
  59. package/node_modules/projectstore/scripts/term.mjs +1027 -52
  60. package/node_modules/projectstore/scripts/tokens.mjs +106 -28
  61. package/node_modules/projectstore/scripts/touch-session.mjs +109 -70
  62. package/node_modules/projectstore/scripts/version-guard.mjs +79 -27
  63. package/node_modules/projectstore/scripts/worktree.mjs +16 -5
  64. package/node_modules/projectstore/templates/de/strings.json +63 -1
  65. package/node_modules/projectstore/templates/de/vault-readme.md.tmpl +13 -0
  66. package/node_modules/projectstore/templates/en/strings.json +63 -1
  67. package/node_modules/projectstore/templates/en/vault-readme.md.tmpl +13 -0
  68. package/node_modules/projectstore/templates/es/strings.json +63 -1
  69. package/node_modules/projectstore/templates/es/vault-readme.md.tmpl +13 -0
  70. package/node_modules/projectstore/templates/fr/strings.json +63 -1
  71. package/node_modules/projectstore/templates/fr/vault-readme.md.tmpl +13 -0
  72. package/node_modules/projectstore/templates/ru/strings.json +63 -1
  73. package/node_modules/projectstore/templates/ru/vault-readme.md.tmpl +13 -0
  74. package/node_modules/projectstore/templates/zh/strings.json +63 -1
  75. package/node_modules/projectstore/templates/zh/vault-readme.md.tmpl +13 -0
  76. package/package.json +2 -2
@@ -55,14 +55,32 @@ export const isErrorExit = (code) => code !== 0 && code !== 1;
55
55
  // the CLI's envelope, so parity holds.
56
56
  function spawnBin(argv, { env, cwd, timeoutMs = DOCTOR_TIMEOUT_MS, onChild = null }) {
57
57
  return new Promise((done) => {
58
- const child = spawn(process.execPath, [BIN, ...argv], { env, cwd: existsSync(cwd) ? cwd : undefined, stdio: ["ignore", "pipe", "pipe"] });
58
+ const child = spawn(process.execPath, [BIN, ...argv], {
59
+ env,
60
+ cwd: existsSync(cwd) ? cwd : undefined,
61
+ stdio: ["ignore", "pipe", "pipe"],
62
+ });
59
63
  if (onChild) onChild(child);
60
- let out = "", err = "";
61
- child.stdout.on("data", (d) => { out += d; });
62
- child.stderr.on("data", (d) => { err += d; });
63
- const timer = setTimeout(() => { child.kill("SIGKILL"); err += `timed out after ${timeoutMs} ms`; }, timeoutMs);
64
- child.on("error", (e) => { clearTimeout(timer); done({ code: 2, out, err: err + e.message }); });
65
- child.on("close", (code) => { clearTimeout(timer); done({ code: code === null ? 2 : code, out, err }); });
64
+ let out = "",
65
+ err = "";
66
+ child.stdout.on("data", (d) => {
67
+ out += d;
68
+ });
69
+ child.stderr.on("data", (d) => {
70
+ err += d;
71
+ });
72
+ const timer = setTimeout(() => {
73
+ child.kill("SIGKILL");
74
+ err += `timed out after ${timeoutMs} ms`;
75
+ }, timeoutMs);
76
+ child.on("error", (e) => {
77
+ clearTimeout(timer);
78
+ done({ code: 2, out, err: err + e.message });
79
+ });
80
+ child.on("close", (code) => {
81
+ clearTimeout(timer);
82
+ done({ code: code === null ? 2 : code, out, err });
83
+ });
66
84
  });
67
85
  }
68
86
  import { readConfigAt, isInsideVault, commandForm } from "./lib.mjs";
@@ -103,7 +121,12 @@ const many = (name, vs) => (Array.isArray(vs) ? vs.flatMap((v) => [`--${name}`,
103
121
  // [verb, ...opts, --json, --project, <dir>, --, ...positionals], so a value
104
122
  // beginning with "-" (a search for "--kind", a path named "-x") is a
105
123
  // positional, never an option the parser would act on.
106
- const READ_ONLY = Object.freeze({ readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false });
124
+ const READ_ONLY = Object.freeze({
125
+ readOnlyHint: true,
126
+ destructiveHint: false,
127
+ idempotentHint: true,
128
+ openWorldHint: false,
129
+ });
107
130
 
108
131
  export const TOOLS = Object.freeze({
109
132
  status: {
@@ -120,60 +143,94 @@ export const TOOLS = Object.freeze({
120
143
  },
121
144
  search: {
122
145
  verb: "search",
123
- description: "Find a phrase in the vault's artifacts: literal, case-insensitive by default, bounded; matches carry path, line, snippet, type, title, status.",
124
- inputSchema: schema({
125
- query: str("the phrase to find (literal substring)"),
126
- kind: strs("only artifacts of these kinds (adr, spec, epic, story, research, …)"),
127
- status: str("only artifacts in this status"),
128
- limit: int("at most this many matches (default 20, hard cap 100)", 1),
129
- include_derived: bool("search the derived views (kanban, code-map, graph) too"),
130
- case_sensitive: bool("match case"),
131
- }, ["query"]),
132
- argv: (a) => ({ opts: [...many("kind", a.kind), ...opt("status", a.status), ...opt("limit", a.limit), ...flag("include-derived", a.include_derived), ...flag("case-sensitive", a.case_sensitive)], positionals: [a.query] }),
146
+ description:
147
+ "Find a phrase in the vault's artifacts: literal, case-insensitive by default, bounded; matches carry path, line, snippet, type, title, status.",
148
+ inputSchema: schema(
149
+ {
150
+ query: str("the phrase to find (literal substring)"),
151
+ kind: strs("only artifacts of these kinds (adr, spec, epic, story, research, …)"),
152
+ status: str("only artifacts in this status"),
153
+ limit: int("at most this many matches (default 20, hard cap 100)", 1),
154
+ include_derived: bool("search the derived views (kanban, code-map, graph) too"),
155
+ case_sensitive: bool("match case"),
156
+ },
157
+ ["query"],
158
+ ),
159
+ argv: (a) => ({
160
+ opts: [
161
+ ...many("kind", a.kind),
162
+ ...opt("status", a.status),
163
+ ...opt("limit", a.limit),
164
+ ...flag("include-derived", a.include_derived),
165
+ ...flag("case-sensitive", a.case_sensitive),
166
+ ],
167
+ positionals: [a.query],
168
+ }),
133
169
  },
134
170
  get_artifact: {
135
171
  verb: "show",
136
172
  description: "One artifact by vault-relative path: its frontmatter, and its body or one section on request.",
137
- inputSchema: schema({
138
- path: str("vault-relative path of the artifact"),
139
- body: bool("include the body"),
140
- section: str("one section by its registry id (description, acceptance, …)"),
141
- }, ["path"]),
173
+ inputSchema: schema(
174
+ {
175
+ path: str("vault-relative path of the artifact"),
176
+ body: bool("include the body"),
177
+ section: str("one section by its registry id (description, acceptance, …)"),
178
+ },
179
+ ["path"],
180
+ ),
142
181
  argv: (a) => ({ opts: [...flag("body", a.body), ...opt("section", a.section)], positionals: [a.path] }),
143
182
  },
144
183
  neighbors: {
145
184
  verb: "graph",
146
- description: "The typed links of one artifact in both directions, from the live link graph (the same edges graph.md holds).",
147
- inputSchema: schema({
148
- path: str("vault-relative path of the artifact"),
149
- kind: strs("only edges of these kinds (wikilink, dead, supersedes, spec-covers, spec-implements-adr, epic-contains, …)"),
150
- direction: { type: "string", enum: ["in", "out", "both"], description: "which edges (default both)" },
151
- limit: int("cap per direction (default and maximum 100)", 1),
152
- }, ["path"]),
153
- argv: (a) => ({ opts: [...many("kind", a.kind), ...opt("direction", a.direction), ...opt("limit", a.limit)], positionals: ["neighbors", a.path] }),
185
+ description:
186
+ "The typed links of one artifact in both directions, from the live link graph (the same edges graph.md holds).",
187
+ inputSchema: schema(
188
+ {
189
+ path: str("vault-relative path of the artifact"),
190
+ kind: strs(
191
+ "only edges of these kinds (wikilink, dead, supersedes, spec-covers, spec-implements-adr, epic-contains, …)",
192
+ ),
193
+ direction: { type: "string", enum: ["in", "out", "both"], description: "which edges (default both)" },
194
+ limit: int("cap per direction (default and maximum 100)", 1),
195
+ },
196
+ ["path"],
197
+ ),
198
+ argv: (a) => ({
199
+ opts: [...many("kind", a.kind), ...opt("direction", a.direction), ...opt("limit", a.limit)],
200
+ positionals: ["neighbors", a.path],
201
+ }),
154
202
  },
155
203
  lineage: {
156
204
  verb: "graph",
157
- description: "The typed ancestry and descent of one artifact: supersedes, spec-covers, spec-implements-adr, epic-contains, breadth-first to a depth (default 3).",
158
- inputSchema: schema({
159
- path: str("vault-relative path of the artifact"),
160
- kind: strs("only these lineage kinds (a subset of the four)"),
161
- depth: int("how far to walk (default 3, maximum 10)"),
162
- }, ["path"]),
205
+ description:
206
+ "The typed ancestry and descent of one artifact: supersedes, spec-covers, spec-implements-adr, epic-contains, breadth-first to a depth (default 3).",
207
+ inputSchema: schema(
208
+ {
209
+ path: str("vault-relative path of the artifact"),
210
+ kind: strs("only these lineage kinds (a subset of the four)"),
211
+ depth: int("how far to walk (default 3, maximum 10)"),
212
+ },
213
+ ["path"],
214
+ ),
163
215
  argv: (a) => ({ opts: [...many("kind", a.kind), ...opt("depth", a.depth)], positionals: ["lineage", a.path] }),
164
216
  },
165
217
  code_refs: {
166
218
  verb: "codemap",
167
- description: "Which code an epic or artifact maps to (its code_refs), or which artifacts map to a repository path; the result says which reading was taken.",
168
- inputSchema: schema({
169
- selector: str("an epic id, an artifact identity or vault path, or a repository-relative path"),
170
- reverse: bool("read the selector as a path even if it names an artifact"),
171
- }, ["selector"]),
219
+ description:
220
+ "Which code an epic or artifact maps to (its code_refs), or which artifacts map to a repository path; the result says which reading was taken.",
221
+ inputSchema: schema(
222
+ {
223
+ selector: str("an epic id, an artifact identity or vault path, or a repository-relative path"),
224
+ reverse: bool("read the selector as a path even if it names an artifact"),
225
+ },
226
+ ["selector"],
227
+ ),
172
228
  argv: (a) => ({ opts: [...opt("for", a.selector), ...flag("reverse", a.reverse)], positionals: [] }),
173
229
  },
174
230
  doctor: {
175
231
  verb: "doctor",
176
- description: "Check the install wiring and the vault's consistency: findings with severity, id and remedy. The slow tool (it spawns the doctor script).",
232
+ description:
233
+ "Check the install wiring and the vault's consistency: findings with severity, id and remedy. The slow tool (it spawns the doctor script).",
177
234
  inputSchema: schema({
178
235
  install: bool("only the install section"),
179
236
  vault: bool("only the vault section"),
@@ -183,30 +240,68 @@ export const TOOLS = Object.freeze({
183
240
  });
184
241
 
185
242
  export function toolList() {
186
- return Object.entries(TOOLS).map(([name, t]) => ({ name, description: t.description, inputSchema: t.inputSchema, annotations: { ...READ_ONLY } }));
243
+ return Object.entries(TOOLS).map(([name, t]) => ({
244
+ name,
245
+ description: t.description,
246
+ inputSchema: t.inputSchema,
247
+ annotations: { ...READ_ONLY },
248
+ }));
187
249
  }
188
250
 
189
251
  // ─── Resources ─────────────────────────────────────────────────────────
190
252
 
191
253
  export const RESOURCES = Object.freeze([
192
- { uri: `${RESOURCE_SCHEME}orientation`, name: "orientation", description: "The vault's navigation skeleton, as SessionStart renders it.", mimeType: "text/markdown" },
193
- { uri: `${RESOURCE_SCHEME}kanban`, name: "kanban", description: "The derived board (kanban.md).", mimeType: "text/markdown", file: "kanban.md" },
194
- { uri: `${RESOURCE_SCHEME}graph`, name: "graph", description: "The derived link graph (graph.md) — large; grep it by vault-relative path.", mimeType: "text/markdown", file: "graph.md" },
254
+ {
255
+ uri: `${RESOURCE_SCHEME}orientation`,
256
+ name: "orientation",
257
+ description: "The vault's navigation skeleton, as SessionStart renders it.",
258
+ mimeType: "text/markdown",
259
+ },
260
+ {
261
+ uri: `${RESOURCE_SCHEME}kanban`,
262
+ name: "kanban",
263
+ description: "The derived board (kanban.md).",
264
+ mimeType: "text/markdown",
265
+ file: "kanban.md",
266
+ },
267
+ {
268
+ uri: `${RESOURCE_SCHEME}graph`,
269
+ name: "graph",
270
+ description: "The derived link graph (graph.md) — large; grep it by vault-relative path.",
271
+ mimeType: "text/markdown",
272
+ file: "graph.md",
273
+ },
195
274
  ]);
196
275
 
197
276
  export const RESOURCE_TEMPLATES = Object.freeze([
198
- { uriTemplate: `${RESOURCE_SCHEME}artifact/{path}`, name: "artifact", description: "One artifact's markdown by vault-relative path (percent-encoded).", mimeType: "text/markdown" },
277
+ {
278
+ uriTemplate: `${RESOURCE_SCHEME}artifact/{path}`,
279
+ name: "artifact",
280
+ description: "One artifact's markdown by vault-relative path (percent-encoded).",
281
+ mimeType: "text/markdown",
282
+ },
199
283
  ]);
200
284
 
201
285
  // ─── The server ────────────────────────────────────────────────────────
202
286
 
203
287
  class Sink {
204
- constructor() { this.text = ""; }
205
- write(s) { this.text += String(s); return true; }
206
- get isTTY() { return false; }
288
+ constructor() {
289
+ this.text = "";
290
+ }
291
+ write(s) {
292
+ this.text += String(s);
293
+ return true;
294
+ }
295
+ get isTTY() {
296
+ return false;
297
+ }
207
298
  }
208
299
 
209
- const rpcError = (id, code, message, data) => ({ jsonrpc: "2.0", id, error: data === undefined ? { code, message } : { code, message, data } });
300
+ const rpcError = (id, code, message, data) => ({
301
+ jsonrpc: "2.0",
302
+ id,
303
+ error: data === undefined ? { code, message } : { code, message, data },
304
+ });
210
305
  const rpcResult = (id, result) => ({ jsonrpc: "2.0", id, result });
211
306
  const hasId = (m) => m && typeof m === "object" && "id" in m && m.id !== null; // 0 is a valid id — never `if (!msg.id)`
212
307
 
@@ -215,15 +310,26 @@ export const UNEXPANDED = /\$\{[A-Za-z_]/;
215
310
 
216
311
  // Why the server may not start, or null.
217
312
  export function startRefusal(project) {
218
- if (!project) return "projectstore mcp: no project directory — pass --project <dir> or set PROJECTSTORE_PROJECT_DIR (the host's own project variable is honoured when present).";
219
- if (UNEXPANDED.test(project)) return `projectstore mcp: the project directory "${project}" still contains a placeholder the host did not expand — pass --project <dir> or set PROJECTSTORE_PROJECT_DIR.`;
313
+ if (!project)
314
+ return "projectstore mcp: no project directory — pass --project <dir> or set PROJECTSTORE_PROJECT_DIR (the host's own project variable is honoured when present).";
315
+ if (UNEXPANDED.test(project))
316
+ return `projectstore mcp: the project directory "${project}" still contains a placeholder the host did not expand — pass --project <dir> or set PROJECTSTORE_PROJECT_DIR.`;
220
317
  return null;
221
318
  }
222
319
 
223
- export function createServer({ project, env = process.env, cwd = project, version = packageVersion(), runVerb = run } = {}) {
320
+ export function createServer({
321
+ project,
322
+ env = process.env,
323
+ cwd = project,
324
+ version = packageVersion(),
325
+ runVerb = run,
326
+ } = {}) {
224
327
  let initialized = false;
225
328
  const cfgOf = () => readConfigAt(project);
226
- const vaultOf = () => { const c = cfgOf(); return c && c.vault_path ? String(c.vault_path).replace(/\/+$/, "") : null; };
329
+ const vaultOf = () => {
330
+ const c = cfgOf();
331
+ return c && c.vault_path ? String(c.vault_path).replace(/\/+$/, "") : null;
332
+ };
227
333
 
228
334
  // In-flight doctor children by request id, so notifications/cancelled can
229
335
  // kill the one cancellable operation instead of leaking a process per call.
@@ -233,38 +339,97 @@ export function createServer({ project, env = process.env, cwd = project, versio
233
339
  const t = TOOLS[name];
234
340
  if (!t) return { error: rpcError(null, E_INVALID_PARAMS, `unknown tool: ${name}`).error };
235
341
  const a = args && typeof args === "object" ? args : {};
236
- for (const r of t.inputSchema.required) if (a[r] === undefined || a[r] === null || a[r] === "") return { error: rpcError(null, E_INVALID_PARAMS, `${name}: "${r}" is required`).error };
342
+ for (const r of t.inputSchema.required)
343
+ if (a[r] === undefined || a[r] === null || a[r] === "")
344
+ return { error: rpcError(null, E_INVALID_PARAMS, `${name}: "${r}" is required`).error };
237
345
  for (const [k, v] of Object.entries(a)) {
238
- if (!Object.hasOwn(t.inputSchema.properties, k)) return { error: rpcError(null, E_INVALID_PARAMS, `${name}: unknown argument "${k}"`).error };
346
+ if (!Object.hasOwn(t.inputSchema.properties, k))
347
+ return { error: rpcError(null, E_INVALID_PARAMS, `${name}: unknown argument "${k}"`).error };
239
348
  const want = t.inputSchema.properties[k].type;
240
- const okType = want === "array" ? Array.isArray(v) && v.every((x) => typeof x === "string") : want === "integer" ? Number.isInteger(v) : typeof v === want;
241
- if (!okType) return { error: rpcError(null, E_INVALID_PARAMS, `${name}: "${k}" must be ${want === "array" ? "an array of strings" : want === "integer" ? "an integer" : "a " + want}`).error };
349
+ const okType =
350
+ want === "array"
351
+ ? Array.isArray(v) && v.every((x) => typeof x === "string")
352
+ : want === "integer"
353
+ ? Number.isInteger(v)
354
+ : typeof v === want;
355
+ if (!okType)
356
+ return {
357
+ error: rpcError(
358
+ null,
359
+ E_INVALID_PARAMS,
360
+ `${name}: "${k}" must be ${want === "array" ? "an array of strings" : want === "integer" ? "an integer" : "a " + want}`,
361
+ ).error,
362
+ };
242
363
  // An option value that starts with "-" would read as a flag to the CLI's parser; a positional is safe behind "--".
243
- if (typeof v === "string" && v.startsWith("-") && ["section", "status", "direction", "selector"].includes(k)) return { error: rpcError(null, E_INVALID_PARAMS, `${name}: "${k}" may not start with "-"`).error };
364
+ if (typeof v === "string" && v.startsWith("-") && ["section", "status", "direction", "selector"].includes(k))
365
+ return { error: rpcError(null, E_INVALID_PARAMS, `${name}: "${k}" may not start with "-"`).error };
244
366
  }
245
367
  // The verb table decides who runs unbound: status and doctor do (doctor
246
368
  // is the tool one calls to learn why a project is unbound); the rest
247
369
  // answer the bind instruction inside the envelope.
248
370
  const rowRequiresBinding = (VERBS.find((v) => v.verb === t.verb) || {}).requiresBinding !== false;
249
371
  if (rowRequiresBinding && !cfgOf()) {
250
- return { content: [{ type: "text", text: JSON.stringify(envelope(t.verb, project, false, { error: `${project} is not bound to a vault — run ${commandForm("bind", { args: "<vault>", env })} in a session, or projectstore bind <vault> --project ${project}` })) }], isError: true };
372
+ return {
373
+ content: [
374
+ {
375
+ type: "text",
376
+ text: JSON.stringify(
377
+ envelope(t.verb, project, false, {
378
+ error: `${project} is not bound to a vault — run ${commandForm("bind", { args: "<vault>", env })} in a session, or projectstore bind <vault> --project ${project}`,
379
+ }),
380
+ ),
381
+ },
382
+ ],
383
+ isError: true,
384
+ };
251
385
  }
252
386
  const { opts, positionals } = t.argv(a);
253
- const argv = [t.verb, ...opts, "--json", "--project", project, ...(positionals.length ? ["--", ...positionals] : [])];
387
+ const argv = [
388
+ t.verb,
389
+ ...opts,
390
+ "--json",
391
+ "--project",
392
+ project,
393
+ ...(positionals.length ? ["--", ...positionals] : []),
394
+ ];
254
395
  let code, outText, errText;
255
396
  if (t.verb === "doctor") {
256
- ({ code, out: outText, err: errText } = await spawnBin(argv, { env, cwd, onChild: (child) => { if (requestId !== null) inFlight.set(requestId, child); } }));
397
+ ({
398
+ code,
399
+ out: outText,
400
+ err: errText,
401
+ } = await spawnBin(argv, {
402
+ env,
403
+ cwd,
404
+ onChild: (child) => {
405
+ if (requestId !== null) inFlight.set(requestId, child);
406
+ },
407
+ }));
257
408
  if (requestId !== null) inFlight.delete(requestId);
258
409
  } else {
259
- const out = new Sink(), err = new Sink();
410
+ const out = new Sink(),
411
+ err = new Sink();
260
412
  try {
261
- code = await runVerb(argv, { env, cwd: existsSync(cwd) ? cwd : undefined, stdin: null, stdout: out, stderr: err, ask: async () => "no" });
413
+ code = await runVerb(argv, {
414
+ env,
415
+ cwd: existsSync(cwd) ? cwd : undefined,
416
+ stdin: null,
417
+ stdout: out,
418
+ stderr: err,
419
+ ask: async () => "no",
420
+ });
262
421
  } catch (e) {
263
- return { content: [{ type: "text", text: JSON.stringify(envelope(t.verb, project, false, { error: e.message })) }], isError: true };
422
+ return {
423
+ content: [{ type: "text", text: JSON.stringify(envelope(t.verb, project, false, { error: e.message })) }],
424
+ isError: true,
425
+ };
264
426
  }
265
- outText = out.text; errText = err.text;
427
+ outText = out.text;
428
+ errText = err.text;
266
429
  }
267
- const text = outText.trim() || JSON.stringify(envelope(t.verb, project, false, { error: errText.trim() || `exit ${code}`, exit: code }));
430
+ const text =
431
+ outText.trim() ||
432
+ JSON.stringify(envelope(t.verb, project, false, { error: errText.trim() || `exit ${code}`, exit: code }));
268
433
  return { content: [{ type: "text", text }], isError: isErrorExit(code) };
269
434
  }
270
435
 
@@ -272,41 +437,87 @@ export function createServer({ project, env = process.env, cwd = project, versio
272
437
  const vault = vaultOf();
273
438
  return RESOURCES.map((r) => {
274
439
  const row = { uri: r.uri, name: r.name, description: r.description, mimeType: r.mimeType };
275
- if (r.file && vault) { try { row.size = statSync(join(vault, r.file)).size; } catch {} }
440
+ if (r.file && vault) {
441
+ try {
442
+ row.size = statSync(join(vault, r.file)).size;
443
+ } catch {}
444
+ }
276
445
  return row;
277
446
  });
278
447
  }
279
448
 
280
449
  async function readResource(uri) {
281
- if (typeof uri !== "string" || !uri.startsWith(RESOURCE_SCHEME)) return { error: { code: E_INVALID_PARAMS, message: `unknown resource: ${uri}` } };
450
+ if (typeof uri !== "string" || !uri.startsWith(RESOURCE_SCHEME))
451
+ return { error: { code: E_INVALID_PARAMS, message: `unknown resource: ${uri}` } };
282
452
  const vault = vaultOf();
283
453
  const rest = uri.slice(RESOURCE_SCHEME.length);
284
454
  if (rest === "orientation") {
285
- const out = new Sink(), err = new Sink();
286
- const code = await runVerb(["orientation", "--project", project], { env, cwd: existsSync(cwd) ? cwd : undefined, stdin: null, stdout: out, stderr: err, ask: async () => "no" });
287
- if (code !== 0) return { error: { code: code === 3 ? E_INVALID_PARAMS : E_INTERNAL, message: err.text.trim() || `orientation exited ${code}` } };
455
+ const out = new Sink(),
456
+ err = new Sink();
457
+ const code = await runVerb(["orientation", "--project", project], {
458
+ env,
459
+ cwd: existsSync(cwd) ? cwd : undefined,
460
+ stdin: null,
461
+ stdout: out,
462
+ stderr: err,
463
+ ask: async () => "no",
464
+ });
465
+ if (code !== 0)
466
+ return {
467
+ error: {
468
+ code: code === 3 ? E_INVALID_PARAMS : E_INTERNAL,
469
+ message: err.text.trim() || `orientation exited ${code}`,
470
+ },
471
+ };
288
472
  return { contents: [{ uri, mimeType: "text/markdown", text: out.text }] };
289
473
  }
290
474
  const fixed = RESOURCES.find((r) => r.file && r.name === rest);
291
475
  if (fixed) {
292
476
  if (!vault) return { error: { code: E_INVALID_PARAMS, message: `${project} is not bound to a vault` } };
293
477
  const p = join(vault, fixed.file);
294
- if (!existsSync(p)) return { error: { code: E_INVALID_PARAMS, message: `${fixed.file} does not exist yet — run reconcile` } };
478
+ if (!existsSync(p))
479
+ return { error: { code: E_INVALID_PARAMS, message: `${fixed.file} does not exist yet — run reconcile` } };
295
480
  // A read can fail on an existing file (an iCloud-evicted, dataless one): an error, not a crash.
296
- try { return { contents: [{ uri, mimeType: "text/markdown", text: readFileSync(p, "utf8") }] }; } catch (e) { return { error: { code: E_INTERNAL, message: `${fixed.file}: ${e.message}` } }; }
481
+ try {
482
+ return { contents: [{ uri, mimeType: "text/markdown", text: readFileSync(p, "utf8") }] };
483
+ } catch (e) {
484
+ return { error: { code: E_INTERNAL, message: `${fixed.file}: ${e.message}` } };
485
+ }
297
486
  }
298
487
  if (rest.startsWith("artifact/")) {
299
488
  if (!vault) return { error: { code: E_INVALID_PARAMS, message: `${project} is not bound to a vault` } };
300
489
  let rel;
301
- try { rel = decodeURIComponent(rest.slice("artifact/".length)); } catch { return { error: { code: E_INVALID_PARAMS, message: "artifact path is not valid percent-encoding" } }; }
490
+ try {
491
+ rel = decodeURIComponent(rest.slice("artifact/".length));
492
+ } catch {
493
+ return { error: { code: E_INVALID_PARAMS, message: "artifact path is not valid percent-encoding" } };
494
+ }
302
495
  // Containment: resolved (collapses `..`), then the vault prefix test,
303
496
  // then the realpath so a symlink out of the vault is refused too.
304
497
  const abs = resolve(vault, rel);
305
- if (!isInsideVault(abs, vault)) return { error: { code: E_INVALID_PARAMS, message: `${rel} is outside the vault` } };
498
+ if (!isInsideVault(abs, vault))
499
+ return { error: { code: E_INVALID_PARAMS, message: `${rel} is outside the vault` } };
306
500
  let real;
307
- try { real = statSync(abs).isFile() ? realpathSync(abs) : null; } catch { real = null; }
308
- if (!real || !isInsideVault(real, realpathSync(vault))) return { error: { code: E_INVALID_PARAMS, message: `${rel} is not an artifact of the vault` } };
309
- try { return { contents: [{ uri: `${RESOURCE_SCHEME}artifact/${encodeURIComponent(rel).replace(/%2F/g, "/")}`, mimeType: "text/markdown", text: readFileSync(real, "utf8") }] }; } catch (e) { return { error: { code: E_INTERNAL, message: `${rel}: ${e.message}` } }; }
501
+ try {
502
+ real = statSync(abs).isFile() ? realpathSync(abs) : null;
503
+ } catch {
504
+ real = null;
505
+ }
506
+ if (!real || !isInsideVault(real, realpathSync(vault)))
507
+ return { error: { code: E_INVALID_PARAMS, message: `${rel} is not an artifact of the vault` } };
508
+ try {
509
+ return {
510
+ contents: [
511
+ {
512
+ uri: `${RESOURCE_SCHEME}artifact/${encodeURIComponent(rel).replace(/%2F/g, "/")}`,
513
+ mimeType: "text/markdown",
514
+ text: readFileSync(real, "utf8"),
515
+ },
516
+ ],
517
+ };
518
+ } catch (e) {
519
+ return { error: { code: E_INTERNAL, message: `${rel}: ${e.message}` } };
520
+ }
310
521
  }
311
522
  return { error: { code: E_INVALID_PARAMS, message: `unknown resource: ${uri}` } };
312
523
  }
@@ -314,8 +525,10 @@ export function createServer({ project, env = process.env, cwd = project, versio
314
525
  // One message in, one response (or null for a notification) out. Pure of
315
526
  // I/O: the loop owns the streams.
316
527
  async function handle(msg) {
317
- if (Array.isArray(msg)) return rpcError(null, E_INVALID_REQUEST, "batches are not supported (removed in protocol revision 2025-06-18)");
318
- if (!msg || typeof msg !== "object" || msg.jsonrpc !== "2.0" || typeof msg.method !== "string") return rpcError(hasId(msg) ? msg.id : null, E_INVALID_REQUEST, "not a JSON-RPC 2.0 request");
528
+ if (Array.isArray(msg))
529
+ return rpcError(null, E_INVALID_REQUEST, "batches are not supported (removed in protocol revision 2025-06-18)");
530
+ if (!msg || typeof msg !== "object" || msg.jsonrpc !== "2.0" || typeof msg.method !== "string")
531
+ return rpcError(hasId(msg) ? msg.id : null, E_INVALID_REQUEST, "not a JSON-RPC 2.0 request");
319
532
  const id = hasId(msg) ? msg.id : null;
320
533
  const params = msg.params && typeof msg.params === "object" ? msg.params : {};
321
534
  const isNotification = !hasId(msg);
@@ -323,69 +536,137 @@ export function createServer({ project, env = process.env, cwd = project, versio
323
536
  case "initialize": {
324
537
  const asked = String(params.protocolVersion || "");
325
538
  const negotiated = PROTOCOL_VERSIONS.includes(asked) ? asked : PROTOCOL_VERSIONS[0];
326
- return rpcResult(id, { protocolVersion: negotiated, capabilities: { tools: {}, resources: {} }, serverInfo: { name: SERVER_NAME, version }, instructions: `Read-only tools over the projectstore vault bound to ${project}. Every result is the CLI's --json envelope; grep-sized answers, never the vault.` });
539
+ return rpcResult(id, {
540
+ protocolVersion: negotiated,
541
+ capabilities: { tools: {}, resources: {} },
542
+ serverInfo: { name: SERVER_NAME, version },
543
+ instructions: `Read-only tools over the projectstore vault bound to ${project}. Every result is the CLI's --json envelope; grep-sized answers, never the vault.`,
544
+ });
327
545
  }
328
- case "notifications/initialized": initialized = true; return null;
546
+ case "notifications/initialized":
547
+ initialized = true;
548
+ return null;
329
549
  case "notifications/cancelled": {
330
550
  const child = inFlight.get(params.requestId);
331
- if (child) { inFlight.delete(params.requestId); try { child.kill("SIGTERM"); } catch {} }
551
+ if (child) {
552
+ inFlight.delete(params.requestId);
553
+ try {
554
+ child.kill("SIGTERM");
555
+ } catch {}
556
+ }
332
557
  return null;
333
558
  }
334
- case "ping": return isNotification ? null : rpcResult(id, {});
559
+ case "ping":
560
+ return isNotification ? null : rpcResult(id, {});
335
561
  case "server/discover":
336
562
  // The 2026-07-28 era's probe. -32601 here is the documented answer a
337
563
  // legacy server gives so a dual-era client falls back to initialize.
338
- return rpcError(id, E_METHOD_NOT_FOUND, "server/discover is not implemented — this server speaks the initialize era");
564
+ return rpcError(
565
+ id,
566
+ E_METHOD_NOT_FOUND,
567
+ "server/discover is not implemented — this server speaks the initialize era",
568
+ );
339
569
  }
340
570
  if (msg.method.startsWith("notifications/")) return null;
341
571
  // JSON-RPC: a request without an id is a notification and is never
342
572
  // answered, whatever its method.
343
573
  if (isNotification) return null;
344
- if (!initialized) return rpcError(id, E_INVALID_REQUEST, "server not initialized — send initialize, then notifications/initialized");
574
+ if (!initialized)
575
+ return rpcError(
576
+ id,
577
+ E_INVALID_REQUEST,
578
+ "server not initialized — send initialize, then notifications/initialized",
579
+ );
345
580
  switch (msg.method) {
346
- case "tools/list": return rpcResult(id, { tools: toolList() });
581
+ case "tools/list":
582
+ return rpcResult(id, { tools: toolList() });
347
583
  case "tools/call": {
348
584
  const r = await callTool(params.name, params.arguments, id);
349
585
  return r.error ? rpcError(id, r.error.code, r.error.message) : rpcResult(id, r);
350
586
  }
351
- case "resources/list": return rpcResult(id, { resources: resourceList() });
352
- case "resources/templates/list": return rpcResult(id, { resourceTemplates: RESOURCE_TEMPLATES.map((t) => ({ ...t })) });
587
+ case "resources/list":
588
+ return rpcResult(id, { resources: resourceList() });
589
+ case "resources/templates/list":
590
+ return rpcResult(id, { resourceTemplates: RESOURCE_TEMPLATES.map((t) => ({ ...t })) });
353
591
  case "resources/read": {
354
592
  const r = await readResource(params.uri);
355
593
  return r.error ? rpcError(id, r.error.code, r.error.message) : rpcResult(id, r);
356
594
  }
357
- default: return rpcError(id, E_METHOD_NOT_FOUND, `method not found: ${msg.method}`);
595
+ default:
596
+ return rpcError(id, E_METHOD_NOT_FOUND, `method not found: ${msg.method}`);
358
597
  }
359
598
  }
360
599
 
361
- return { handle, callTool, readResource, resourceList, get initialized() { return initialized; } };
600
+ return {
601
+ handle,
602
+ callTool,
603
+ readResource,
604
+ resourceList,
605
+ get initialized() {
606
+ return initialized;
607
+ },
608
+ };
362
609
  }
363
610
 
364
611
  // The loop: newline-delimited JSON frames on stdin, one response per line on
365
612
  // stdout, nothing else ever on stdout. Resolves with the exit code when
366
613
  // stdin closes or a signal arrives; never calls process.exit (the bin sets
367
614
  // process.exitCode and lets the streams drain).
368
- export function serve({ project, env = process.env, stdin = process.stdin, stdout = process.stdout, stderr = process.stderr, signals = process } = {}) {
615
+ export function serve({
616
+ project,
617
+ env = process.env,
618
+ stdin = process.stdin,
619
+ stdout = process.stdout,
620
+ stderr = process.stderr,
621
+ signals = process,
622
+ } = {}) {
369
623
  const refusal = startRefusal(project);
370
- if (refusal) { stderr.write(refusal + "\n"); return Promise.resolve(2); }
624
+ if (refusal) {
625
+ stderr.write(refusal + "\n");
626
+ return Promise.resolve(2);
627
+ }
371
628
  const server = createServer({ project, env });
372
- const send = (obj) => { if (obj) stdout.write(JSON.stringify(obj) + "\n"); };
629
+ const send = (obj) => {
630
+ if (obj) stdout.write(JSON.stringify(obj) + "\n");
631
+ };
373
632
  return new Promise((done) => {
374
- let pending = 0, closing = false;
375
- const finish = () => { if (closing && pending === 0) done(0); };
633
+ let pending = 0,
634
+ closing = false;
635
+ const finish = () => {
636
+ if (closing && pending === 0) done(0);
637
+ };
376
638
  const rl = createInterface({ input: stdin, crlfDelay: Infinity, terminal: false });
377
639
  rl.on("line", async (line) => {
378
640
  if (!line.trim()) return;
379
641
  let msg;
380
- try { msg = JSON.parse(line); } catch { send(rpcError(null, E_PARSE, "parse error")); return; }
642
+ try {
643
+ msg = JSON.parse(line);
644
+ } catch {
645
+ send(rpcError(null, E_PARSE, "parse error"));
646
+ return;
647
+ }
381
648
  pending++;
382
- try { send(await server.handle(msg)); }
383
- catch (e) { send(rpcError(hasId(msg) ? msg.id : null, E_INTERNAL, e && e.message ? e.message : String(e))); }
384
- finally { pending--; finish(); }
649
+ try {
650
+ send(await server.handle(msg));
651
+ } catch (e) {
652
+ send(rpcError(hasId(msg) ? msg.id : null, E_INTERNAL, e && e.message ? e.message : String(e)));
653
+ } finally {
654
+ pending--;
655
+ finish();
656
+ }
385
657
  });
386
- const close = () => { if (closing) return; closing = true; rl.close(); finish(); };
658
+ const close = () => {
659
+ if (closing) return;
660
+ closing = true;
661
+ rl.close();
662
+ finish();
663
+ };
387
664
  rl.on("close", close);
388
665
  stdin.on("end", close);
389
- for (const sig of ["SIGINT", "SIGTERM"]) { try { signals.once(sig, close); } catch {} }
666
+ for (const sig of ["SIGINT", "SIGTERM"]) {
667
+ try {
668
+ signals.once(sig, close);
669
+ } catch {}
670
+ }
390
671
  });
391
672
  }