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.
- package/README.md +1 -1
- package/bin/projectstore-claude.mjs +1 -1
- package/node_modules/projectstore/.claude-plugin/marketplace.json +1 -1
- package/node_modules/projectstore/.claude-plugin/plugin.json +2 -1
- package/node_modules/projectstore/README.md +44 -7
- package/node_modules/projectstore/commands/bar.md +30 -0
- package/node_modules/projectstore/commands/bind.md +16 -20
- package/node_modules/projectstore/commands/doctor.md +12 -7
- package/node_modules/projectstore/commands/scaffold.md +24 -16
- package/node_modules/projectstore/commands/status.md +1 -1
- package/node_modules/projectstore/commands/statusline.md +2 -2
- package/node_modules/projectstore/commands/story.md +15 -2
- package/node_modules/projectstore/docs/alongside.md +167 -0
- package/node_modules/projectstore/docs/extending.md +17 -3
- package/node_modules/projectstore/docs/getting-started.md +18 -9
- package/node_modules/projectstore/docs/harnesses.md +1 -1
- package/node_modules/projectstore/docs/how-it-works.md +12 -3
- package/node_modules/projectstore/harnesses/claude-code.json +23 -8
- package/node_modules/projectstore/harnesses/codex.json +16 -6
- package/node_modules/projectstore/hooks/band-state.d.ts +44 -0
- package/node_modules/projectstore/hooks/band.tsx +622 -0
- package/node_modules/projectstore/hooks/hooks.json +1 -0
- package/node_modules/projectstore/hooks/pre-compact.mjs +7 -10
- package/node_modules/projectstore/hooks/session-rules.mjs +14 -8
- package/node_modules/projectstore/hooks/session-start.mjs +56 -20
- package/node_modules/projectstore/hooks/session-stop.mjs +8 -6
- package/node_modules/projectstore/package.json +12 -2
- package/node_modules/projectstore/scaffold/headings.json +8 -0
- package/node_modules/projectstore/scripts/band-shared.mjs +446 -0
- package/node_modules/projectstore/scripts/band.mjs +207 -0
- package/node_modules/projectstore/scripts/binding.mjs +733 -62
- package/node_modules/projectstore/scripts/build-adapters.mjs +98 -35
- package/node_modules/projectstore/scripts/cli.mjs +1463 -267
- package/node_modules/projectstore/scripts/codemap.mjs +22 -7
- package/node_modules/projectstore/scripts/convergence.mjs +638 -0
- package/node_modules/projectstore/scripts/diff-refs.mjs +14 -8
- package/node_modules/projectstore/scripts/doctor-report.mjs +253 -0
- package/node_modules/projectstore/scripts/doctor.mjs +1384 -420
- package/node_modules/projectstore/scripts/draft.mjs +5 -2
- package/node_modules/projectstore/scripts/fetch-shell.mjs +369 -0
- package/node_modules/projectstore/scripts/graph.mjs +19 -13
- package/node_modules/projectstore/scripts/harness.mjs +283 -37
- package/node_modules/projectstore/scripts/install-harness.mjs +4701 -575
- package/node_modules/projectstore/scripts/kanban.mjs +6 -21
- package/node_modules/projectstore/scripts/lib.mjs +933 -199
- package/node_modules/projectstore/scripts/mcp.mjs +386 -105
- package/node_modules/projectstore/scripts/portable-registration.mjs +276 -56
- package/node_modules/projectstore/scripts/provenance.mjs +26 -10
- package/node_modules/projectstore/scripts/query.mjs +441 -72
- package/node_modules/projectstore/scripts/reconcile.mjs +55 -18
- package/node_modules/projectstore/scripts/run-host.mjs +139 -0
- package/node_modules/projectstore/scripts/scaffold.mjs +244 -0
- package/node_modules/projectstore/scripts/setup.mjs +987 -0
- package/node_modules/projectstore/scripts/stages.mjs +84 -0
- package/node_modules/projectstore/scripts/statusline-launcher.mjs +36 -11
- package/node_modules/projectstore/scripts/statusline.mjs +55 -14
- package/node_modules/projectstore/scripts/story-section.mjs +39 -17
- package/node_modules/projectstore/scripts/surfaces.mjs +485 -99
- package/node_modules/projectstore/scripts/term.mjs +1027 -52
- package/node_modules/projectstore/scripts/tokens.mjs +106 -28
- package/node_modules/projectstore/scripts/touch-session.mjs +109 -70
- package/node_modules/projectstore/scripts/version-guard.mjs +79 -27
- package/node_modules/projectstore/scripts/worktree.mjs +16 -5
- package/node_modules/projectstore/templates/de/strings.json +63 -1
- package/node_modules/projectstore/templates/de/vault-readme.md.tmpl +13 -0
- package/node_modules/projectstore/templates/en/strings.json +63 -1
- package/node_modules/projectstore/templates/en/vault-readme.md.tmpl +13 -0
- package/node_modules/projectstore/templates/es/strings.json +63 -1
- package/node_modules/projectstore/templates/es/vault-readme.md.tmpl +13 -0
- package/node_modules/projectstore/templates/fr/strings.json +63 -1
- package/node_modules/projectstore/templates/fr/vault-readme.md.tmpl +13 -0
- package/node_modules/projectstore/templates/ru/strings.json +63 -1
- package/node_modules/projectstore/templates/ru/vault-readme.md.tmpl +13 -0
- package/node_modules/projectstore/templates/zh/strings.json +63 -1
- package/node_modules/projectstore/templates/zh/vault-readme.md.tmpl +13 -0
- 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], {
|
|
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 = "",
|
|
61
|
-
|
|
62
|
-
child.
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
child.on("
|
|
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({
|
|
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:
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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:
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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:
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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:
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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:
|
|
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]) => ({
|
|
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
|
-
{
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
{
|
|
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() {
|
|
205
|
-
|
|
206
|
-
|
|
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) => ({
|
|
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)
|
|
219
|
-
|
|
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({
|
|
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 = () => {
|
|
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)
|
|
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))
|
|
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 =
|
|
241
|
-
|
|
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))
|
|
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 {
|
|
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 = [
|
|
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
|
-
({
|
|
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(),
|
|
410
|
+
const out = new Sink(),
|
|
411
|
+
err = new Sink();
|
|
260
412
|
try {
|
|
261
|
-
code = await runVerb(argv, {
|
|
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 {
|
|
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;
|
|
427
|
+
outText = out.text;
|
|
428
|
+
errText = err.text;
|
|
266
429
|
}
|
|
267
|
-
const text =
|
|
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) {
|
|
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))
|
|
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(),
|
|
286
|
-
|
|
287
|
-
|
|
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))
|
|
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 {
|
|
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 {
|
|
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))
|
|
498
|
+
if (!isInsideVault(abs, vault))
|
|
499
|
+
return { error: { code: E_INVALID_PARAMS, message: `${rel} is outside the vault` } };
|
|
306
500
|
let real;
|
|
307
|
-
try {
|
|
308
|
-
|
|
309
|
-
|
|
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))
|
|
318
|
-
|
|
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, {
|
|
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":
|
|
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) {
|
|
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":
|
|
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(
|
|
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)
|
|
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":
|
|
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":
|
|
352
|
-
|
|
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:
|
|
595
|
+
default:
|
|
596
|
+
return rpcError(id, E_METHOD_NOT_FOUND, `method not found: ${msg.method}`);
|
|
358
597
|
}
|
|
359
598
|
}
|
|
360
599
|
|
|
361
|
-
return {
|
|
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({
|
|
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) {
|
|
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) => {
|
|
629
|
+
const send = (obj) => {
|
|
630
|
+
if (obj) stdout.write(JSON.stringify(obj) + "\n");
|
|
631
|
+
};
|
|
373
632
|
return new Promise((done) => {
|
|
374
|
-
let pending = 0,
|
|
375
|
-
|
|
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 {
|
|
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 {
|
|
383
|
-
|
|
384
|
-
|
|
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 = () => {
|
|
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"]) {
|
|
666
|
+
for (const sig of ["SIGINT", "SIGTERM"]) {
|
|
667
|
+
try {
|
|
668
|
+
signals.once(sig, close);
|
|
669
|
+
} catch {}
|
|
670
|
+
}
|
|
390
671
|
});
|
|
391
672
|
}
|