substrata-cli 0.1.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.
@@ -0,0 +1,1279 @@
1
+ import {
2
+ GITIGNORE_LINES,
3
+ NotFoundError,
4
+ SecretDetectedError,
5
+ appendMemoryEntries,
6
+ buildIndex,
7
+ configPath,
8
+ ensureGitignore,
9
+ existingEntryIds,
10
+ findFootprintById,
11
+ getIndexStatus,
12
+ initProject,
13
+ installSecretHook,
14
+ listFootprints,
15
+ listMemoryDocuments,
16
+ loadConfig,
17
+ memoryDir,
18
+ renderConfig,
19
+ scanForSecrets,
20
+ search,
21
+ substrataDir,
22
+ supersedeFootprint,
23
+ upsertAgentsMd,
24
+ writeFootprint,
25
+ writeShellEnv
26
+ } from "./chunk-5V44C5UO.js";
27
+
28
+ // src/index.ts
29
+ import { Command } from "commander";
30
+ import pc5 from "picocolors";
31
+
32
+ // src/wizard/prompts.ts
33
+ import {
34
+ confirm as clackConfirm,
35
+ isCancel,
36
+ multiselect as clackMultiselect,
37
+ text as clackText
38
+ } from "@clack/prompts";
39
+ var assumeYesFlag = false;
40
+ function setAssumeYes(value) {
41
+ assumeYesFlag = value;
42
+ }
43
+ function isNonInteractive() {
44
+ return assumeYesFlag || !process.stdout.isTTY || !process.stdin.isTTY;
45
+ }
46
+ var PromptCancelledError = class extends Error {
47
+ constructor() {
48
+ super("Prompt cancelled");
49
+ this.name = "PromptCancelledError";
50
+ }
51
+ };
52
+ function guardCancel(value) {
53
+ if (isCancel(value)) throw new PromptCancelledError();
54
+ return value;
55
+ }
56
+ async function promptText(options) {
57
+ if (isNonInteractive()) return options.defaultValue;
58
+ const result = await clackText({
59
+ message: options.message,
60
+ placeholder: options.placeholder ?? options.defaultValue,
61
+ defaultValue: options.defaultValue,
62
+ initialValue: options.defaultValue
63
+ });
64
+ const value = guardCancel(result);
65
+ return value.length > 0 ? value : options.defaultValue;
66
+ }
67
+ async function promptConfirm(options) {
68
+ if (isNonInteractive()) return options.defaultValue;
69
+ const result = await clackConfirm({
70
+ message: options.message,
71
+ initialValue: options.defaultValue
72
+ });
73
+ return guardCancel(result);
74
+ }
75
+ async function promptMultiselect(options) {
76
+ if (isNonInteractive()) return options.defaultValues;
77
+ if (options.choices.length === 0) return [];
78
+ const choices = options.choices.map((c) => ({
79
+ value: c.value,
80
+ label: c.label,
81
+ ...c.hint !== void 0 ? { hint: c.hint } : {}
82
+ }));
83
+ const result = await clackMultiselect({
84
+ message: options.message,
85
+ options: choices,
86
+ initialValues: options.defaultValues,
87
+ required: false
88
+ });
89
+ return guardCancel(result);
90
+ }
91
+
92
+ // src/util.ts
93
+ import { execFile } from "child_process";
94
+ import { promisify } from "util";
95
+ import pc from "picocolors";
96
+ var execFileAsync = promisify(execFile);
97
+ var CliError = class extends Error {
98
+ exitCode;
99
+ constructor(message, exitCode = 1) {
100
+ super(message);
101
+ this.name = "CliError";
102
+ this.exitCode = exitCode;
103
+ }
104
+ };
105
+ function resolveCwd(opts) {
106
+ return opts?.cwd ? opts.cwd : process.cwd();
107
+ }
108
+ var out = {
109
+ ok: (msg) => process.stdout.write(`${pc.green("\u2714")} ${msg}
110
+ `),
111
+ info: (msg) => process.stdout.write(`${pc.blue("\u2139")} ${msg}
112
+ `),
113
+ warn: (msg) => process.stderr.write(`${pc.yellow("!")} ${msg}
114
+ `),
115
+ err: (msg) => process.stderr.write(`${pc.red("\u2716")} ${msg}
116
+ `),
117
+ plain: (msg) => process.stdout.write(`${msg}
118
+ `)
119
+ };
120
+ async function git(cwd, args) {
121
+ try {
122
+ const { stdout } = await execFileAsync("git", args, { cwd });
123
+ return stdout.trim();
124
+ } catch {
125
+ return null;
126
+ }
127
+ }
128
+ async function isGitRepo(cwd) {
129
+ const result = await git(cwd, ["rev-parse", "--is-inside-work-tree"]);
130
+ return result === "true";
131
+ }
132
+ async function collectGitContext(cwd) {
133
+ const branch = await git(cwd, ["rev-parse", "--abbrev-ref", "HEAD"]) ?? void 0;
134
+ const commit = await git(cwd, ["rev-parse", "HEAD"]) ?? void 0;
135
+ const staged = await git(cwd, ["diff", "--cached", "--name-only"]);
136
+ const unstaged = await git(cwd, ["diff", "--name-only"]);
137
+ const files = /* @__PURE__ */ new Set();
138
+ for (const block of [staged, unstaged]) {
139
+ if (!block) continue;
140
+ for (const line of block.split(/\r?\n/)) {
141
+ const t = line.trim();
142
+ if (t.length > 0) files.add(t);
143
+ }
144
+ }
145
+ return {
146
+ branch: branch && branch !== "HEAD" ? branch : void 0,
147
+ files: Array.from(files),
148
+ commit
149
+ };
150
+ }
151
+ async function resolveAttribution(cwd, config, flags) {
152
+ const env = process.env;
153
+ const actor = flags.actor || env.SUBSTRATA_ACTOR || config.agent.default_actor || "unknown-agent";
154
+ const model = flags.model || env.SUBSTRATA_MODEL || config.agent.default_model || void 0;
155
+ let requester = flags.requester || env.SUBSTRATA_REQUESTER || void 0;
156
+ if (!requester) {
157
+ const email = await git(cwd, ["config", "user.email"]);
158
+ if (email) requester = email;
159
+ }
160
+ return { actor, model, requester };
161
+ }
162
+ async function requireConfig(cwd) {
163
+ try {
164
+ return await loadConfig(cwd);
165
+ } catch (err) {
166
+ throw new CliError(
167
+ `${err.message}
168
+ Run \`substrata init\` to set up this repository.`
169
+ );
170
+ }
171
+ }
172
+
173
+ // src/commands/add.ts
174
+ var SECURITY_REMINDER = "Reminder: Substrata files are intended to be committed.\nDo not include secrets, credentials, or sensitive user data.";
175
+ function collect(value, previous = []) {
176
+ return [...previous, value];
177
+ }
178
+ function splitCsv(value) {
179
+ if (!value) return [];
180
+ return value.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
181
+ }
182
+ function parseRejected(values) {
183
+ if (!values) return [];
184
+ return values.map((v) => {
185
+ const idx = v.indexOf(":");
186
+ if (idx === -1) return { option: v.trim(), reason: "" };
187
+ return { option: v.slice(0, idx).trim(), reason: v.slice(idx + 1).trim() };
188
+ });
189
+ }
190
+ var VALID_WORK_TYPES = /* @__PURE__ */ new Set([
191
+ "implementation",
192
+ "implementation_decision",
193
+ "bug_fix",
194
+ "refactor",
195
+ "investigation",
196
+ "architecture_decision",
197
+ "test_update",
198
+ "documentation"
199
+ ]);
200
+ function registerAddCommand(program) {
201
+ program.command("add").description("Create a new footprint").option("--title <title>", "Footprint title").option("--purpose <text>", "Why this work was done").option("--actor <id>", "Agent that performed the work").option("--requester <id>", "Who requested the work").option("--model <id>", "Agent model identifier").option("--files <csv>", "Comma-separated files touched").option("--tag <tag>", "Tag (repeatable)", collect, []).option("--work-type <type>", "Footprint work type").option("--decision <text>", "Decision made (repeatable)", collect, []).option("--rejected <option:reason>", "Rejected option (repeatable)", collect, []).option("--notes <text>", "Implementation notes").option("--memory <text>", "Memory learned (repeatable)", collect, []).option("--guidance <text>", "Future agent guidance").option("--template <type>", "Work type template to seed the footprint").option("--supersedes <id>", "Mark this footprint as superseding an old one").option("--allow-secret", "Write even if a secret is detected (NOT recommended)").option("--from-git", "Populate branch/files/commit from Git").action(async (opts, command) => {
202
+ const cwd = resolveCwd(command.parent?.opts());
203
+ const config = await requireConfig(cwd);
204
+ const attribution = await resolveAttribution(cwd, config, {
205
+ actor: opts.actor,
206
+ model: opts.model,
207
+ requester: opts.requester
208
+ });
209
+ let title = opts.title;
210
+ if (!title) {
211
+ if (isNonInteractive()) {
212
+ throw new CliError("`--title` is required in non-interactive mode.");
213
+ }
214
+ title = await promptText({ message: "Title", defaultValue: "" });
215
+ if (!title.trim()) throw new CliError("A title is required.");
216
+ }
217
+ const purpose = opts.purpose ?? (isNonInteractive() ? void 0 : await promptText({ message: "Purpose", defaultValue: "" }) || void 0);
218
+ const workTypeRaw = opts.workType ?? opts.template;
219
+ if (workTypeRaw && !VALID_WORK_TYPES.has(workTypeRaw)) {
220
+ throw new CliError(
221
+ `Invalid work type "${workTypeRaw}". Valid: ${Array.from(VALID_WORK_TYPES).join(", ")}.`
222
+ );
223
+ }
224
+ const workType = workTypeRaw;
225
+ let filesTouched = splitCsv(opts.files);
226
+ let repoBranch;
227
+ let commits;
228
+ if (opts.fromGit) {
229
+ const ctx = await collectGitContext(cwd);
230
+ repoBranch = ctx.branch;
231
+ if (ctx.files.length > 0) {
232
+ filesTouched = Array.from(/* @__PURE__ */ new Set([...filesTouched, ...ctx.files]));
233
+ }
234
+ if (ctx.commit) commits = [ctx.commit];
235
+ }
236
+ const supersedes = opts.supersedes ? [opts.supersedes] : void 0;
237
+ const input = {
238
+ cwd,
239
+ title,
240
+ purpose,
241
+ actor: attribution.actor,
242
+ requester: attribution.requester,
243
+ agentModel: attribution.model,
244
+ workType,
245
+ decisions: opts.decision && opts.decision.length > 0 ? opts.decision : void 0,
246
+ rejectedOptions: opts.rejected && opts.rejected.length > 0 ? parseRejected(opts.rejected) : void 0,
247
+ implementationNotes: opts.notes,
248
+ memoryLearned: opts.memory && opts.memory.length > 0 ? opts.memory : void 0,
249
+ futureAgentGuidance: opts.guidance,
250
+ filesTouched: filesTouched.length > 0 ? filesTouched : void 0,
251
+ tags: opts.tag && opts.tag.length > 0 ? opts.tag : void 0,
252
+ repo: repoBranch ? { branch: repoBranch } : void 0,
253
+ related: commits ? { commits } : void 0,
254
+ supersedes,
255
+ allowSecret: opts.allowSecret
256
+ };
257
+ let footprint;
258
+ try {
259
+ footprint = await writeFootprint(input);
260
+ } catch (err) {
261
+ if (err instanceof SecretDetectedError) {
262
+ const detail = err.findings.map((f) => ` - ${f.name} at body line ${f.line}`).join("\n");
263
+ throw new CliError(
264
+ `Refusing to write footprint: ${err.findings.length} potential secret(s) detected
265
+ ${detail}
266
+ Redact these or pass --allow-secret to override (NOT recommended \u2014 footprints are committed).`
267
+ );
268
+ }
269
+ throw err;
270
+ }
271
+ if (opts.supersedes) {
272
+ await supersedeFootprint(cwd, opts.supersedes, footprint.frontmatter.id);
273
+ }
274
+ out.ok(`Footprint written: ${footprint.frontmatter.id}`);
275
+ out.plain(` ${footprint.filePath}`);
276
+ out.warn(SECURITY_REMINDER);
277
+ });
278
+ }
279
+
280
+ // src/render/context.ts
281
+ var CHARS_PER_TOKEN = 3.5;
282
+ function estimateTokens(text) {
283
+ return Math.ceil(text.length / CHARS_PER_TOKEN);
284
+ }
285
+ var HEADER = "Relevant Substrata context:";
286
+ var APPROX_NOTE = "(Token counts are approximate \u2014 estimated, not tokenizer-exact.)";
287
+ function firstLine(text) {
288
+ if (!text) return void 0;
289
+ for (const line of text.split(/\r?\n/)) {
290
+ const t = line.trim();
291
+ if (t.length > 0) return t;
292
+ }
293
+ return void 0;
294
+ }
295
+ function footprintPoint(fp) {
296
+ const s = fp.sections;
297
+ if (s.decisions && s.decisions.length > 0) {
298
+ const rejected = s.rejectedOptions?.[0];
299
+ return {
300
+ statement: s.decisions[0],
301
+ reason: rejected ? `${rejected.option} was rejected \u2014 ${rejected.reason}` : void 0
302
+ };
303
+ }
304
+ if (s.rejectedOptions && s.rejectedOptions.length > 0) {
305
+ const r = s.rejectedOptions[0];
306
+ return { statement: `Avoid ${r.option} for ${fp.title}.`, reason: r.reason };
307
+ }
308
+ if (s.futureAgentGuidance) {
309
+ return { statement: firstLine(s.futureAgentGuidance) ?? fp.title };
310
+ }
311
+ if (s.purpose) {
312
+ return { statement: fp.title, reason: firstLine(s.purpose) };
313
+ }
314
+ return { statement: fp.title };
315
+ }
316
+ function memoryPoint(doc) {
317
+ for (const line of doc.body.split(/\r?\n/)) {
318
+ const t = line.trim();
319
+ if (t.startsWith("- ")) return { statement: t.slice(2).trim() };
320
+ }
321
+ return { statement: doc.title };
322
+ }
323
+ function blockFor(result, footprintsById, memoryById, index) {
324
+ const fp = footprintsById.get(result.id);
325
+ const mem = memoryById.get(result.id);
326
+ let statement;
327
+ let reason;
328
+ if (fp) {
329
+ ({ statement, reason } = footprintPoint(fp));
330
+ } else if (mem) {
331
+ ({ statement } = memoryPoint(mem));
332
+ } else {
333
+ statement = result.title || result.id;
334
+ }
335
+ const lines = [`${index}. ${statement}`];
336
+ if (reason) lines.push(` Reason: ${reason}`);
337
+ lines.push(` Source: ${result.filePath}`);
338
+ return {
339
+ source: { id: result.id, title: result.title, filePath: result.filePath },
340
+ block: lines.join("\n")
341
+ };
342
+ }
343
+ function renderContext(results, footprints, memory, maxTokens) {
344
+ const footprintsById = new Map(footprints.map((f) => [f.frontmatter.id, f]));
345
+ const memoryById = new Map(memory.map((m) => [m.frontmatter.id, m]));
346
+ const header = `${HEADER}
347
+
348
+ `;
349
+ const footer = `
350
+
351
+ ${APPROX_NOTE}`;
352
+ let used = estimateTokens(header) + estimateTokens(footer);
353
+ const chosen = [];
354
+ const blocks = [];
355
+ let n = 1;
356
+ for (const result of results) {
357
+ const { source, block } = blockFor(result, footprintsById, memoryById, n);
358
+ const blockTokens = estimateTokens(`${block}
359
+
360
+ `);
361
+ if (used + blockTokens > maxTokens && blocks.length > 0) break;
362
+ blocks.push(block);
363
+ chosen.push(source);
364
+ used += blockTokens;
365
+ n += 1;
366
+ }
367
+ if (blocks.length === 0) {
368
+ return { text: `${HEADER}
369
+
370
+ No relevant context found.`, sources: [] };
371
+ }
372
+ return { text: `${header}${blocks.join("\n\n")}${footer}`, sources: chosen };
373
+ }
374
+
375
+ // src/commands/auto-index.ts
376
+ async function ensureFreshIndex(cwd, autoIndex) {
377
+ const status = await getIndexStatus(cwd);
378
+ if (status.state === "fresh") return;
379
+ if (!autoIndex) {
380
+ out.info(
381
+ status.state === "missing" ? "Index missing \u2014 run `substrata index` (auto-index disabled)." : `Index stale (${status.reason}) \u2014 run \`substrata index\` (auto-index disabled).`
382
+ );
383
+ return;
384
+ }
385
+ await buildIndex(cwd);
386
+ }
387
+
388
+ // src/commands/context.ts
389
+ function collect2(value, previous = []) {
390
+ return [...previous, value];
391
+ }
392
+ function registerContextCommand(program) {
393
+ program.command("context <task>").description("Return concise, source-linked context for an agent").option("--json", "Output JSON ({ context, sources })").option("--max-tokens <n>", "Approximate token budget (chars/3.5)").option("--files <path>", "Bias toward docs touching this file (repeatable)", collect2, []).option("--no-auto-index", "Do not auto-(re)build a stale/missing index").action(async (task, opts, command) => {
394
+ const cwd = resolveCwd(command.parent?.opts());
395
+ const config = await requireConfig(cwd);
396
+ await ensureFreshIndex(cwd, opts.autoIndex !== false);
397
+ const maxTokens = opts.maxTokens ? Number(opts.maxTokens) : config.search.max_context_tokens;
398
+ const budget = Number.isFinite(maxTokens) ? maxTokens : config.search.max_context_tokens;
399
+ const results = await search(task, {
400
+ cwd,
401
+ limit: config.search.default_limit,
402
+ files: opts.files && opts.files.length > 0 ? opts.files : void 0,
403
+ excludeSuperseded: true
404
+ });
405
+ const [footprints, memory] = await Promise.all([
406
+ listFootprints(cwd),
407
+ listMemoryDocuments(cwd)
408
+ ]);
409
+ const rendered = renderContext(results, footprints, memory, budget);
410
+ if (opts.json) {
411
+ out.plain(JSON.stringify({ context: rendered.text, sources: rendered.sources }, null, 2));
412
+ return;
413
+ }
414
+ out.plain(rendered.text);
415
+ });
416
+ }
417
+
418
+ // src/commands/doctor.ts
419
+ import { existsSync, readFileSync } from "fs";
420
+ import path from "path";
421
+ async function runDoctor(cwd) {
422
+ let failures = 0;
423
+ if (existsSync(substrataDir(cwd))) {
424
+ out.ok(".substrata exists");
425
+ } else {
426
+ out.err(".substrata missing \u2014 run `substrata init`");
427
+ return 1;
428
+ }
429
+ try {
430
+ await loadConfig(cwd);
431
+ out.ok("config valid");
432
+ } catch (err) {
433
+ out.err(`config invalid: ${err.message}`);
434
+ failures += 1;
435
+ }
436
+ const status = await getIndexStatus(cwd);
437
+ if (status.state === "fresh") {
438
+ out.ok("index fresh");
439
+ } else if (status.state === "missing") {
440
+ out.info("index missing \u2014 run `substrata index` (or it builds automatically on first search)");
441
+ } else {
442
+ out.info(`index stale (${status.reason}) \u2014 run \`substrata index\``);
443
+ }
444
+ const gitignorePath = path.join(cwd, ".gitignore");
445
+ const gitignore = existsSync(gitignorePath) ? readFileSync(gitignorePath, "utf8") : "";
446
+ const ignoredLines = new Set(gitignore.split(/\r?\n/).map((l) => l.trim()));
447
+ const indexCovered = ignoredLines.has(".substrata/index/") || ignoredLines.has(".substrata/");
448
+ if (indexCovered) {
449
+ out.ok("gitignore covers index/ and cache/");
450
+ } else {
451
+ out.err(`gitignore would commit the generated DB \u2014 add: ${GITIGNORE_LINES.join(", ")}`);
452
+ failures += 1;
453
+ }
454
+ try {
455
+ const footprints = await listFootprints(cwd);
456
+ out.ok(`${footprints.length} footprint file(s) parsed`);
457
+ } catch (err) {
458
+ out.err(`footprint parse error: ${err.message}`);
459
+ failures += 1;
460
+ }
461
+ try {
462
+ const memory = await listMemoryDocuments(cwd);
463
+ out.ok(`${memory.length} memory file(s) parsed`);
464
+ } catch (err) {
465
+ out.err(`memory parse error: ${err.message}`);
466
+ failures += 1;
467
+ }
468
+ return failures;
469
+ }
470
+ function registerDoctorCommand(program) {
471
+ program.command("doctor").description("Check repository setup").action(async (_opts, command) => {
472
+ const cwd = resolveCwd(command.parent?.opts());
473
+ const failures = await runDoctor(cwd);
474
+ if (failures > 0) {
475
+ process.exitCode = 1;
476
+ }
477
+ });
478
+ }
479
+
480
+ // src/commands/hook.ts
481
+ import { readFile } from "fs/promises";
482
+ import path2 from "path";
483
+ async function scanStaged(cwd) {
484
+ const staged = await git(cwd, ["diff", "--cached", "--name-only"]);
485
+ if (!staged) return 0;
486
+ const files = staged.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.startsWith(".substrata/") && l.endsWith(".md"));
487
+ let flagged = 0;
488
+ for (const rel of files) {
489
+ let content;
490
+ try {
491
+ content = await readFile(path2.join(cwd, rel), "utf8");
492
+ } catch {
493
+ continue;
494
+ }
495
+ const findings = scanForSecrets(content);
496
+ if (findings.length > 0) {
497
+ flagged += 1;
498
+ out.err(`${findings.length} potential secret(s) in ${rel}:`);
499
+ for (const f of findings) out.plain(` - ${f.name} at line ${f.line}`);
500
+ }
501
+ }
502
+ return flagged;
503
+ }
504
+ function registerHookCommand(program) {
505
+ const hook = program.command("hook").description("Pre-commit secret hook utilities");
506
+ hook.command("install").description("Install the pre-commit secret scan hook").action(async (_opts, command) => {
507
+ const cwd = resolveCwd(command.parent?.parent?.opts());
508
+ const result = installSecretHook(cwd);
509
+ if (result.action === "skip") {
510
+ out.info("Pre-commit hook already installed.");
511
+ } else {
512
+ out.ok(`Pre-commit hook ${result.action === "create" ? "installed" : "updated"}.`);
513
+ }
514
+ });
515
+ hook.command("run").description("Scan staged .substrata files for secrets").action(async (_opts, command) => {
516
+ const cwd = resolveCwd(command.parent?.parent?.opts());
517
+ const flagged = await scanStaged(cwd);
518
+ if (flagged > 0) process.exitCode = 1;
519
+ });
520
+ program.command("internal-scan-staged", { hidden: true }).action(async (_opts, command) => {
521
+ const cwd = resolveCwd(command.parent?.opts());
522
+ const flagged = await scanStaged(cwd);
523
+ if (flagged > 0) process.exitCode = 1;
524
+ });
525
+ }
526
+
527
+ // src/commands/index.ts
528
+ function registerIndexCommand(program) {
529
+ program.command("index").description("Build or rebuild the local search index").option("--rebuild", "Force a full rebuild of the index").action(async (_opts, command) => {
530
+ const cwd = resolveCwd(command.parent?.opts());
531
+ await requireConfig(cwd);
532
+ await buildIndex(cwd, { rebuild: true });
533
+ out.ok("Index built.");
534
+ });
535
+ }
536
+
537
+ // src/commands/init.ts
538
+ import { readFile as readFile2, writeFile } from "fs/promises";
539
+
540
+ // src/wizard/init-wizard.ts
541
+ import { existsSync as existsSync5, readFileSync as readFileSync3 } from "fs";
542
+ import { homedir as homedir2 } from "os";
543
+ import path7 from "path";
544
+ import pc2 from "picocolors";
545
+
546
+ // src/mcp-clients/claude-code.ts
547
+ import { execFile as execFile2 } from "child_process";
548
+ import { existsSync as existsSync2 } from "fs";
549
+ import path4 from "path";
550
+ import { promisify as promisify2 } from "util";
551
+
552
+ // src/mcp-clients/json-config.ts
553
+ import { lstatSync, readFileSync as readFileSync2, writeFileSync, mkdirSync } from "fs";
554
+ import path3 from "path";
555
+ function isSymlink(filePath) {
556
+ try {
557
+ return lstatSync(filePath).isSymbolicLink();
558
+ } catch {
559
+ return false;
560
+ }
561
+ }
562
+ function readJson(filePath) {
563
+ try {
564
+ const raw = readFileSync2(filePath, "utf8");
565
+ const parsed = JSON.parse(raw);
566
+ return parsed && typeof parsed === "object" ? parsed : {};
567
+ } catch {
568
+ return null;
569
+ }
570
+ }
571
+ function entryMatches(existing, spec) {
572
+ const entry = existing?.mcpServers?.[spec.name];
573
+ if (!entry) return false;
574
+ return entry.command === spec.command && JSON.stringify(entry.args ?? []) === JSON.stringify(spec.args);
575
+ }
576
+ function mergeMcpJson(filePath, spec, dry = false) {
577
+ if (isSymlink(filePath)) {
578
+ return {
579
+ path: filePath,
580
+ action: "skip",
581
+ description: `refused: ${path3.basename(filePath)} is a symlink`
582
+ };
583
+ }
584
+ const existing = readJson(filePath);
585
+ if (entryMatches(existing, spec)) {
586
+ return { path: filePath, action: "skip", description: "MCP entry already current" };
587
+ }
588
+ const base = existing ?? {};
589
+ const servers = { ...base.mcpServers ?? {} };
590
+ delete servers[spec.name];
591
+ servers[spec.name] = { command: spec.command, args: spec.args };
592
+ const next = { ...base, mcpServers: servers };
593
+ const contents = `${JSON.stringify(next, null, 2)}
594
+ `;
595
+ if (!dry) {
596
+ mkdirSync(path3.dirname(filePath), { recursive: true });
597
+ writeFileSync(filePath, contents, "utf8");
598
+ }
599
+ return {
600
+ path: filePath,
601
+ action: existing === null ? "create" : "update",
602
+ description: `register Substrata MCP server (${spec.name})`,
603
+ contents
604
+ };
605
+ }
606
+ function removeMcpJson(filePath, name) {
607
+ const existing = readJson(filePath);
608
+ if (!existing?.mcpServers?.[name]) return;
609
+ const servers = { ...existing.mcpServers };
610
+ delete servers[name];
611
+ const next = { ...existing, mcpServers: servers };
612
+ writeFileSync(filePath, `${JSON.stringify(next, null, 2)}
613
+ `, "utf8");
614
+ }
615
+
616
+ // src/mcp-clients/claude-code.ts
617
+ var execFileAsync2 = promisify2(execFile2);
618
+ function mcpJsonPath(cwd) {
619
+ return path4.join(cwd, ".mcp.json");
620
+ }
621
+ async function hasClaudeBinary() {
622
+ try {
623
+ await execFileAsync2("which", ["claude"]);
624
+ return true;
625
+ } catch {
626
+ return false;
627
+ }
628
+ }
629
+ var claudeCodeClient = {
630
+ name: "claude",
631
+ label: "Claude Code",
632
+ async detect(cwd) {
633
+ if (existsSync2(mcpJsonPath(cwd))) return true;
634
+ return hasClaudeBinary();
635
+ },
636
+ async register(cwd, spec, dry = false) {
637
+ return mergeMcpJson(mcpJsonPath(cwd), spec, dry);
638
+ },
639
+ async unregister(cwd, name) {
640
+ removeMcpJson(mcpJsonPath(cwd), name);
641
+ }
642
+ };
643
+
644
+ // src/mcp-clients/cursor.ts
645
+ import { existsSync as existsSync3 } from "fs";
646
+ import path5 from "path";
647
+ function cursorMcpPath(cwd) {
648
+ return path5.join(cwd, ".cursor", "mcp.json");
649
+ }
650
+ var cursorClient = {
651
+ name: "cursor",
652
+ label: "Cursor",
653
+ async detect(cwd) {
654
+ return existsSync3(path5.join(cwd, ".cursor")) || existsSync3(cursorMcpPath(cwd));
655
+ },
656
+ async register(cwd, spec, dry = false) {
657
+ return mergeMcpJson(cursorMcpPath(cwd), spec, dry);
658
+ },
659
+ async unregister(cwd, name) {
660
+ removeMcpJson(cursorMcpPath(cwd), name);
661
+ }
662
+ };
663
+
664
+ // src/mcp-clients/windsurf.ts
665
+ import { existsSync as existsSync4 } from "fs";
666
+ import { homedir } from "os";
667
+ import path6 from "path";
668
+ function windsurfConfigPath() {
669
+ return path6.join(homedir(), ".codeium", "windsurf", "mcp_config.json");
670
+ }
671
+ function snippet(spec) {
672
+ const block = {
673
+ mcpServers: {
674
+ [spec.name]: { command: spec.command, args: spec.args }
675
+ }
676
+ };
677
+ return `${windsurfConfigPath()}
678
+ ${JSON.stringify(block, null, 2)}`;
679
+ }
680
+ var windsurfClient = {
681
+ name: "windsurf",
682
+ label: "Windsurf",
683
+ async detect() {
684
+ return existsSync4(path6.join(homedir(), ".codeium", "windsurf"));
685
+ },
686
+ async register(_cwd, spec) {
687
+ return {
688
+ path: windsurfConfigPath(),
689
+ action: "skip",
690
+ description: `add manually (global config):
691
+ ${snippet(spec)}`
692
+ };
693
+ },
694
+ async unregister() {
695
+ }
696
+ };
697
+
698
+ // src/mcp-clients/registry.ts
699
+ var SUBSTRATA_MCP_SPEC = {
700
+ name: "substrata",
701
+ command: "npx",
702
+ args: ["-y", "substrata-cli", "mcp"]
703
+ };
704
+ var MCP_CLIENTS = [claudeCodeClient, cursorClient, windsurfClient];
705
+ function getMcpClient(name) {
706
+ return MCP_CLIENTS.find((c) => c.name === name);
707
+ }
708
+ async function detectMcpClients(cwd) {
709
+ const detected = [];
710
+ for (const client of MCP_CLIENTS) {
711
+ if (await client.detect(cwd)) detected.push(client);
712
+ }
713
+ return detected;
714
+ }
715
+
716
+ // src/wizard/init-wizard.ts
717
+ function detectShellRc() {
718
+ const shell = process.env.SHELL ?? "";
719
+ const home = homedir2();
720
+ if (shell.includes("bash")) return path7.join(home, ".bashrc");
721
+ return path7.join(home, ".zshrc");
722
+ }
723
+ function defaultProjectName(cwd) {
724
+ const pkgPath = path7.join(cwd, "package.json");
725
+ if (existsSync5(pkgPath)) {
726
+ try {
727
+ const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
728
+ if (pkg.name && typeof pkg.name === "string") {
729
+ return pkg.name.replace(/^@[^/]+\//, "");
730
+ }
731
+ } catch {
732
+ }
733
+ }
734
+ return path7.basename(path7.resolve(cwd));
735
+ }
736
+ async function preflight(cwd, flags) {
737
+ if (!await isGitRepo(cwd)) {
738
+ const doInit = await promptConfirm({
739
+ message: "This is not a Git repository. Run `git init`?",
740
+ defaultValue: true
741
+ });
742
+ if (doInit) {
743
+ const result = await git(cwd, ["init"]);
744
+ if (result !== null) out.ok("Initialized a Git repository.");
745
+ else out.warn("Could not run `git init`; continuing without Git.");
746
+ }
747
+ }
748
+ void flags;
749
+ }
750
+ async function resolveMcpClients(cwd, flags) {
751
+ if (flags.mcp === false) return [];
752
+ if (flags.mcpClient && flags.mcpClient.length > 0) {
753
+ const chosen = [];
754
+ for (const name of flags.mcpClient) {
755
+ const client = getMcpClient(name);
756
+ if (client) chosen.push(client);
757
+ else out.warn(`Unknown MCP client "${name}" \u2014 skipping.`);
758
+ }
759
+ return chosen;
760
+ }
761
+ const detected = await detectMcpClients(cwd);
762
+ if (detected.length === 0) return [];
763
+ return promptMultiselect({
764
+ message: "Register the Substrata MCP server with which clients?",
765
+ choices: detected.map((c) => ({ value: c, label: c.label })),
766
+ defaultValues: detected
767
+ });
768
+ }
769
+ async function collectAnswers(cwd, flags) {
770
+ const projectName = flags.project ?? await promptText({
771
+ message: "Project name",
772
+ defaultValue: defaultProjectName(cwd)
773
+ });
774
+ const actor = await promptText({
775
+ message: "Default actor (agent id)",
776
+ defaultValue: flags.actor ?? "unknown-agent"
777
+ });
778
+ const model = flags.model ?? await promptText({ message: "Default agent model (optional)", defaultValue: "" });
779
+ const gitEmail = await git(cwd, ["config", "user.email"]) ?? "";
780
+ const requester = flags.requester ?? await promptText({ message: "Default requester (optional)", defaultValue: gitEmail });
781
+ const attribution = {
782
+ actor: actor || void 0,
783
+ model: model || void 0,
784
+ requester: requester || void 0
785
+ };
786
+ const writeEnv = flags.env === false ? false : await promptConfirm({
787
+ message: "Persist attribution env vars to your shell rc?",
788
+ defaultValue: true
789
+ });
790
+ const redact = flags.redact === false ? false : await promptConfirm({
791
+ message: "Enable security redaction + content scan?",
792
+ defaultValue: true
793
+ });
794
+ const withHook = flags.withHook === true ? true : await promptConfirm({
795
+ message: "Install the pre-commit secret hook?",
796
+ defaultValue: false
797
+ });
798
+ const writeAgentsMd = flags.agentsMd === false ? false : await promptConfirm({
799
+ message: "Add the Substrata section to AGENTS.md?",
800
+ defaultValue: true
801
+ });
802
+ const writeGitignore = flags.gitignore !== false;
803
+ const mcpClients = await resolveMcpClients(cwd, flags);
804
+ return {
805
+ projectName,
806
+ attribution,
807
+ writeEnv,
808
+ rcPath: detectShellRc(),
809
+ redact,
810
+ withHook,
811
+ writeAgentsMd,
812
+ writeGitignore,
813
+ mcpClients
814
+ };
815
+ }
816
+ async function buildPlan(cwd, answers) {
817
+ const changes = [];
818
+ changes.push({
819
+ path: path7.join(cwd, ".substrata"),
820
+ action: existsSync5(path7.join(cwd, ".substrata", "config.yml")) ? "skip" : "create",
821
+ description: "Substrata scaffold (config, README, footprints, memory, templates)"
822
+ });
823
+ if (answers.writeGitignore) {
824
+ changes.push(ensureGitignore(cwd, true));
825
+ }
826
+ if (answers.writeEnv && (answers.attribution.actor || answers.attribution.model || answers.attribution.requester)) {
827
+ changes.push(writeShellEnv(answers.rcPath, answers.attribution, true));
828
+ }
829
+ if (answers.writeAgentsMd) {
830
+ changes.push(upsertAgentsMd(cwd, true));
831
+ }
832
+ if (answers.withHook) {
833
+ changes.push(installSecretHook(cwd, true));
834
+ }
835
+ for (const client of answers.mcpClients) {
836
+ changes.push(await client.register(cwd, SUBSTRATA_MCP_SPEC, true));
837
+ }
838
+ return changes;
839
+ }
840
+ async function applyPlan(cwd, answers) {
841
+ const applied = [];
842
+ applied.push(...await initProject(cwd, { projectName: answers.projectName }));
843
+ if (answers.writeGitignore) {
844
+ applied.push(ensureGitignore(cwd, false));
845
+ }
846
+ if (answers.writeEnv && (answers.attribution.actor || answers.attribution.model || answers.attribution.requester)) {
847
+ applied.push(writeShellEnv(answers.rcPath, answers.attribution, false));
848
+ } else if (!answers.writeEnv) {
849
+ printEnvSnippet(answers.attribution);
850
+ }
851
+ if (answers.writeAgentsMd) {
852
+ applied.push(upsertAgentsMd(cwd, false));
853
+ }
854
+ if (answers.withHook) {
855
+ applied.push(installSecretHook(cwd, false));
856
+ }
857
+ for (const client of answers.mcpClients) {
858
+ const result = await client.register(cwd, SUBSTRATA_MCP_SPEC, false);
859
+ applied.push(result);
860
+ if (result.action === "skip" && result.description.includes("\n")) {
861
+ out.info(`${client.label}: ${result.description}`);
862
+ }
863
+ }
864
+ return applied;
865
+ }
866
+ function printEnvSnippet(attribution) {
867
+ const lines = [];
868
+ if (attribution.actor) lines.push(`export SUBSTRATA_ACTOR="${attribution.actor}"`);
869
+ if (attribution.model) lines.push(`export SUBSTRATA_MODEL="${attribution.model}"`);
870
+ if (attribution.requester) lines.push(`export SUBSTRATA_REQUESTER="${attribution.requester}"`);
871
+ if (lines.length === 0) return;
872
+ out.info("Add these to your shell rc to attribute footprints:");
873
+ out.plain(lines.map((l) => ` ${l}`).join("\n"));
874
+ }
875
+ function printResolvedConfig(cwd, flags) {
876
+ const name = flags.project ?? defaultProjectName(cwd);
877
+ out.plain(renderConfig(name));
878
+ }
879
+ async function runInitWizard(cwd, flags) {
880
+ const updateMode = existsSync5(path7.join(cwd, ".substrata", "config.yml"));
881
+ if (updateMode) {
882
+ out.info("Existing .substrata detected \u2014 running in update mode.");
883
+ }
884
+ await preflight(cwd, flags);
885
+ const answers = await collectAnswers(cwd, flags);
886
+ const plan = await buildPlan(cwd, answers);
887
+ out.plain(pc2.bold("\nPlanned changes:"));
888
+ out.plain(
889
+ plan.map((c) => {
890
+ const label = c.action === "create" ? pc2.green("CREATE") : c.action === "update" ? pc2.yellow("UPDATE") : pc2.dim("SKIP ");
891
+ return ` ${label} ${c.path}${c.description ? pc2.dim(` \u2014 ${c.description.split("\n")[0]}`) : ""}`;
892
+ }).join("\n")
893
+ );
894
+ out.plain("");
895
+ const confirmed = await promptConfirm({ message: "Apply these changes?", defaultValue: true });
896
+ if (!confirmed) {
897
+ out.info("Aborted. Nothing was written.");
898
+ return { applied: [], aborted: true };
899
+ }
900
+ const applied = await applyPlan(cwd, answers);
901
+ out.ok("Substrata setup applied.");
902
+ if (answers.writeEnv) {
903
+ out.info(`Run \`source ${answers.rcPath}\` to load attribution env vars.`);
904
+ }
905
+ return { applied, aborted: false };
906
+ }
907
+
908
+ // src/commands/init.ts
909
+ async function disableRedaction(cwd) {
910
+ const file = configPath(cwd);
911
+ let raw;
912
+ try {
913
+ raw = await readFile2(file, "utf8");
914
+ } catch {
915
+ return;
916
+ }
917
+ const next = raw.replace(/^(\s*)redact:\s*true\s*$/m, "$1redact: false").replace(/^(\s*)scan_content:\s*true\s*$/m, "$1scan_content: false").replace(/^(\s*)block_on_secret:\s*true\s*$/m, "$1block_on_secret: false");
918
+ if (next !== raw) await writeFile(file, next, "utf8");
919
+ }
920
+ function registerInitCommand(program) {
921
+ program.command("init").description("Set up Substrata in this repository (interactive wizard)").option("--yes", "Accept all defaults, no prompts").option("--project <name>", "Project name").option("--actor <id>", "Default actor").option("--model <id>", "Default agent model").option("--requester <id>", "Default requester").option("--no-env", "Don't touch shell rc; print snippet instead").option("--no-agents-md", "Skip AGENTS.md").option("--no-mcp", "Skip MCP registration").option("--mcp-client <name>", "Register only this client (repeatable)", collect3, []).option("--no-gitignore", "Don't edit .gitignore").option("--no-redact", "Disable security redaction (NOT recommended)").option("--with-hook", "Install the pre-commit secret hook").option("--no-index", "Skip building the initial index").option("--print-config", "Print resolved config.yml without writing anything").action(async (opts, command) => {
922
+ const cwd = resolveCwd(command.parent?.opts());
923
+ const flags = {
924
+ yes: opts.yes,
925
+ project: opts.project,
926
+ actor: opts.actor,
927
+ model: opts.model,
928
+ requester: opts.requester,
929
+ env: opts.env,
930
+ agentsMd: opts.agentsMd,
931
+ mcp: opts.mcp,
932
+ mcpClient: opts.mcpClient,
933
+ gitignore: opts.gitignore,
934
+ redact: opts.redact,
935
+ withHook: opts.withHook,
936
+ index: opts.index,
937
+ printConfig: opts.printConfig
938
+ };
939
+ if (flags.printConfig) {
940
+ printResolvedConfig(cwd, flags);
941
+ return;
942
+ }
943
+ if (flags.yes) setAssumeYes(true);
944
+ const result = await runInitWizard(cwd, flags);
945
+ if (result.aborted) return;
946
+ if (flags.redact === false) {
947
+ await disableRedaction(cwd);
948
+ out.warn("Security redaction disabled (--no-redact).");
949
+ }
950
+ out.plain("");
951
+ await runDoctor(cwd);
952
+ if (flags.index !== false) {
953
+ await buildIndex(cwd);
954
+ out.ok("Initial index built.");
955
+ }
956
+ });
957
+ }
958
+ function collect3(value, previous = []) {
959
+ return [...previous, value];
960
+ }
961
+
962
+ // src/render/table.ts
963
+ import pc3 from "picocolors";
964
+ function statusLabel(status) {
965
+ switch (status) {
966
+ case "superseded":
967
+ return pc3.yellow("[superseded]");
968
+ case "deprecated":
969
+ return pc3.red("[deprecated]");
970
+ case "draft":
971
+ return pc3.dim("[draft]");
972
+ default:
973
+ return "";
974
+ }
975
+ }
976
+ function renderSearchResults(results) {
977
+ if (results.length === 0) {
978
+ return pc3.dim("No matching footprints or memory found.");
979
+ }
980
+ const blocks = results.map((r, i) => {
981
+ const n = pc3.dim(`${i + 1}.`);
982
+ const status = statusLabel(r.status);
983
+ const header = `${n} ${pc3.bold(r.title || r.id)}${status ? ` ${status}` : ""}`;
984
+ const lines = [header, ` ${pc3.dim("id:")} ${r.id}`, ` ${pc3.dim("path:")} ${r.filePath}`];
985
+ if (r.tags.length > 0) lines.push(` ${pc3.dim("tags:")} ${r.tags.join(", ")}`);
986
+ if (r.snippet.trim().length > 0) {
987
+ lines.push(` ${pc3.dim(r.snippet.replace(/\s+/g, " ").trim())}`);
988
+ }
989
+ return lines.join("\n");
990
+ });
991
+ return blocks.join("\n\n");
992
+ }
993
+ function renderFootprintList(rows) {
994
+ if (rows.length === 0) {
995
+ return pc3.dim("No footprints found.");
996
+ }
997
+ return rows.map((r) => {
998
+ const date = r.createdAt ? r.createdAt.slice(0, 10) : "----------";
999
+ const status = statusLabel(r.status);
1000
+ const tags = r.tags.length > 0 ? pc3.dim(` (${r.tags.join(", ")})`) : "";
1001
+ return `${pc3.dim(date)} ${pc3.bold(r.title || r.id)}${status ? ` ${status}` : ""}
1002
+ ${pc3.dim(r.id)}${tags}`;
1003
+ }).join("\n");
1004
+ }
1005
+
1006
+ // src/commands/list.ts
1007
+ function matches(fp, opts) {
1008
+ if (opts.tag && !(fp.frontmatter.tags ?? []).includes(opts.tag)) return false;
1009
+ if (opts.file && !(fp.frontmatter.files_touched ?? []).includes(opts.file)) return false;
1010
+ if (opts.since && fp.frontmatter.created_at < opts.since) return false;
1011
+ return true;
1012
+ }
1013
+ function registerListCommand(program) {
1014
+ program.command("list").description("List recent footprints").option("--tag <tag>", "Only footprints carrying this tag").option("--file <path>", "Only footprints touching this file").option("--since <date>", "Only footprints created on/after this ISO date").option("--json", "Output JSON").action(async (opts, command) => {
1015
+ const cwd = resolveCwd(command.parent?.opts());
1016
+ await requireConfig(cwd);
1017
+ const all = await listFootprints(cwd);
1018
+ const filtered = all.filter((fp) => matches(fp, opts));
1019
+ if (opts.json) {
1020
+ const rows = filtered.map((fp) => ({
1021
+ id: fp.frontmatter.id,
1022
+ title: fp.title,
1023
+ status: fp.frontmatter.status,
1024
+ createdAt: fp.frontmatter.created_at,
1025
+ tags: fp.frontmatter.tags ?? [],
1026
+ filesTouched: fp.frontmatter.files_touched ?? [],
1027
+ filePath: fp.filePath
1028
+ }));
1029
+ out.plain(JSON.stringify(rows, null, 2));
1030
+ return;
1031
+ }
1032
+ out.plain(
1033
+ renderFootprintList(
1034
+ filtered.map((fp) => ({
1035
+ id: fp.frontmatter.id,
1036
+ title: fp.title,
1037
+ status: fp.frontmatter.status,
1038
+ createdAt: fp.frontmatter.created_at,
1039
+ tags: fp.frontmatter.tags ?? []
1040
+ }))
1041
+ )
1042
+ );
1043
+ });
1044
+ }
1045
+
1046
+ // src/commands/mcp.ts
1047
+ function registerMcpCommand(program) {
1048
+ program.command("mcp").description("Run the Substrata MCP server (stdio)").action(async (_opts, command) => {
1049
+ const cwd = resolveCwd(command.parent?.opts());
1050
+ let mod;
1051
+ try {
1052
+ mod = await import("./dist-OCRLBALV.js");
1053
+ } catch (err) {
1054
+ throw new CliError(`Failed to load @substrata/mcp-server: ${err.message}`);
1055
+ }
1056
+ if (typeof mod.runMcpServer !== "function") {
1057
+ throw new CliError("@substrata/mcp-server does not export runMcpServer().");
1058
+ }
1059
+ await mod.runMcpServer({ cwd });
1060
+ });
1061
+ }
1062
+
1063
+ // src/commands/memory-update.ts
1064
+ import { existsSync as existsSync6 } from "fs";
1065
+ import { readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
1066
+ import path8 from "path";
1067
+ var CONVENTIONS_FRONTMATTER = `---
1068
+ schema_version: 1
1069
+ id: mem_repo_conventions
1070
+ type: repo_conventions
1071
+ tags:
1072
+ - conventions
1073
+ ---
1074
+
1075
+ # Repo conventions
1076
+
1077
+ Curated, durable knowledge agents should read often.
1078
+
1079
+ <!-- substrata:entries:start -->
1080
+ <!-- substrata:entries:end -->
1081
+ `;
1082
+ function registerMemoryUpdateCommand(program) {
1083
+ const memory = program.command("memory").description("Curated memory utilities");
1084
+ memory.command("update").description("Suggest and append memory entries from footprints").option("--since <date>", "Only footprints created on/after this ISO date").option("--yes", "Append without confirmation").action(async (opts, command) => {
1085
+ const cwd = resolveCwd(command.parent?.parent?.opts());
1086
+ await requireConfig(cwd);
1087
+ const footprints = await listFootprints(cwd);
1088
+ const filtered = opts.since ? footprints.filter((fp) => fp.frontmatter.created_at >= opts.since) : footprints;
1089
+ const candidates = [];
1090
+ for (const fp of filtered) {
1091
+ const learned = fp.sections.memoryLearned ?? [];
1092
+ if (learned.length === 0) continue;
1093
+ candidates.push({
1094
+ sourceId: fp.frontmatter.id,
1095
+ lines: learned.map((l) => `- ${l}`)
1096
+ });
1097
+ }
1098
+ if (candidates.length === 0) {
1099
+ out.info("No `Memory learned` sections found in the selected footprints.");
1100
+ return;
1101
+ }
1102
+ const filePath = path8.join(memoryDir(cwd), "conventions.md");
1103
+ const alreadyPresent = existsSync6(filePath) ? existingEntryIds(await readFile3(filePath, "utf8")) : /* @__PURE__ */ new Set();
1104
+ const fresh = candidates.filter((c) => !alreadyPresent.has(c.sourceId));
1105
+ if (fresh.length === 0) {
1106
+ out.info("All suggested entries are already present. Nothing to do.");
1107
+ return;
1108
+ }
1109
+ out.plain("Suggested memory entries:");
1110
+ for (const entry of fresh) {
1111
+ out.plain(` [${entry.sourceId}]`);
1112
+ for (const line of entry.lines) out.plain(` ${line}`);
1113
+ }
1114
+ const confirmed = opts.yes || isNonInteractive() ? true : await promptConfirm({
1115
+ message: `Append ${fresh.length} entr${fresh.length === 1 ? "y" : "ies"} to conventions.md?`,
1116
+ defaultValue: true
1117
+ });
1118
+ if (!confirmed) {
1119
+ out.info("Aborted. Nothing written.");
1120
+ return;
1121
+ }
1122
+ if (!existsSync6(filePath)) {
1123
+ await writeFile2(filePath, CONVENTIONS_FRONTMATTER, "utf8");
1124
+ }
1125
+ const changed = await appendMemoryEntries(filePath, candidates);
1126
+ if (changed) {
1127
+ out.ok(`Appended ${fresh.length} memory entr${fresh.length === 1 ? "y" : "ies"}.`);
1128
+ } else {
1129
+ out.info("No new entries appended (already present).");
1130
+ }
1131
+ });
1132
+ }
1133
+
1134
+ // src/commands/search.ts
1135
+ function collect4(value, previous = []) {
1136
+ return [...previous, value];
1137
+ }
1138
+ function registerSearchCommand(program) {
1139
+ program.command("search <query>").description("Search footprints and memory").option("--json", "Output JSON").option("--files <path>", "Filter to docs touching this file (repeatable)", collect4, []).option("--tag <tag>", "Filter to docs carrying this tag (repeatable)", collect4, []).option("--limit <n>", "Maximum number of results").option("--exclude-superseded", "Drop superseded/deprecated footprints").option("--no-auto-index", "Do not auto-(re)build a stale/missing index").action(async (query, opts, command) => {
1140
+ const cwd = resolveCwd(command.parent?.opts());
1141
+ const config = await requireConfig(cwd);
1142
+ await ensureFreshIndex(cwd, opts.autoIndex !== false);
1143
+ const limit = opts.limit ? Number(opts.limit) : config.search.default_limit;
1144
+ const results = await search(query, {
1145
+ cwd,
1146
+ limit: Number.isFinite(limit) ? limit : config.search.default_limit,
1147
+ files: opts.files && opts.files.length > 0 ? opts.files : void 0,
1148
+ tags: opts.tag && opts.tag.length > 0 ? opts.tag : void 0,
1149
+ excludeSuperseded: opts.excludeSuperseded
1150
+ });
1151
+ if (opts.json) {
1152
+ out.plain(JSON.stringify(results, null, 2));
1153
+ return;
1154
+ }
1155
+ out.plain(renderSearchResults(results));
1156
+ });
1157
+ }
1158
+
1159
+ // src/commands/show.ts
1160
+ import pc4 from "picocolors";
1161
+ function registerShowCommand(program) {
1162
+ program.command("show <id>").description("Show one footprint").option("--json", "Output the parsed footprint as JSON").option("--path", "Print only the footprint file path").action(async (id, opts, command) => {
1163
+ const cwd = resolveCwd(command.parent?.opts());
1164
+ await requireConfig(cwd);
1165
+ const fp = await findFootprintById(cwd, id);
1166
+ if (!fp) throw new CliError(`Footprint not found: ${id}`);
1167
+ if (opts.path) {
1168
+ out.plain(fp.filePath);
1169
+ return;
1170
+ }
1171
+ if (opts.json) {
1172
+ out.plain(
1173
+ JSON.stringify(
1174
+ {
1175
+ id: fp.frontmatter.id,
1176
+ title: fp.title,
1177
+ filePath: fp.filePath,
1178
+ frontmatter: fp.frontmatter,
1179
+ sections: fp.sections
1180
+ },
1181
+ null,
1182
+ 2
1183
+ )
1184
+ );
1185
+ return;
1186
+ }
1187
+ const fm = fp.frontmatter;
1188
+ const lines = [
1189
+ pc4.bold(fp.title || fm.id),
1190
+ `${pc4.dim("id:")} ${fm.id}`,
1191
+ `${pc4.dim("status:")} ${fm.status}`,
1192
+ `${pc4.dim("work_type:")} ${fm.work_type}`,
1193
+ `${pc4.dim("actor:")} ${fm.actor}`
1194
+ ];
1195
+ if (fm.requester) lines.push(`${pc4.dim("requester:")} ${fm.requester}`);
1196
+ if (fm.agent_model) lines.push(`${pc4.dim("model:")} ${fm.agent_model}`);
1197
+ lines.push(`${pc4.dim("created:")} ${fm.created_at}`);
1198
+ if (fm.tags && fm.tags.length > 0)
1199
+ lines.push(`${pc4.dim("tags:")} ${fm.tags.join(", ")}`);
1200
+ if (fm.files_touched && fm.files_touched.length > 0) {
1201
+ lines.push(`${pc4.dim("files:")} ${fm.files_touched.join(", ")}`);
1202
+ }
1203
+ lines.push(`${pc4.dim("path:")} ${fp.filePath}`);
1204
+ lines.push("");
1205
+ lines.push(fp.body.trim());
1206
+ out.plain(lines.join("\n"));
1207
+ });
1208
+ }
1209
+
1210
+ // src/commands/supersede.ts
1211
+ function registerSupersedeCommand(program) {
1212
+ program.command("supersede <old-id>").description("Mark an old footprint as superseded by a new one").requiredOption("--by <new-id>", "Id of the footprint that replaces the old one").action(async (oldId, opts, command) => {
1213
+ const cwd = resolveCwd(command.parent?.opts());
1214
+ await requireConfig(cwd);
1215
+ try {
1216
+ await supersedeFootprint(cwd, oldId, opts.by);
1217
+ } catch (err) {
1218
+ if (err instanceof NotFoundError) throw new CliError(err.message);
1219
+ throw err;
1220
+ }
1221
+ await buildIndex(cwd);
1222
+ out.ok(`${oldId} superseded by ${opts.by}. Index rebuilt.`);
1223
+ });
1224
+ }
1225
+
1226
+ // src/index.ts
1227
+ function buildProgram() {
1228
+ const program = new Command();
1229
+ program.name("substrata").description("Shared project memory for AI engineering agents").version("0.1.0").option("--cwd <dir>", "Run as if invoked from this directory").enablePositionalOptions();
1230
+ program.exitOverride();
1231
+ registerInitCommand(program);
1232
+ registerAddCommand(program);
1233
+ registerSearchCommand(program);
1234
+ registerContextCommand(program);
1235
+ registerIndexCommand(program);
1236
+ registerListCommand(program);
1237
+ registerShowCommand(program);
1238
+ registerDoctorCommand(program);
1239
+ registerSupersedeCommand(program);
1240
+ registerMemoryUpdateCommand(program);
1241
+ registerHookCommand(program);
1242
+ registerMcpCommand(program);
1243
+ return program;
1244
+ }
1245
+ function reportError(err) {
1246
+ if (err instanceof CliError) {
1247
+ out.err(err.message);
1248
+ return err.exitCode;
1249
+ }
1250
+ if (err instanceof PromptCancelledError) {
1251
+ out.info("Cancelled.");
1252
+ return 1;
1253
+ }
1254
+ const e = err;
1255
+ if (e && typeof e.code === "string" && e.code.startsWith("commander.")) {
1256
+ if (e.code === "commander.helpDisplayed" || e.code === "commander.version") return 0;
1257
+ if (e.message) out.err(e.message);
1258
+ return typeof e.exitCode === "number" ? e.exitCode : 1;
1259
+ }
1260
+ out.err(`Unexpected error: ${err?.message ?? String(err)}`);
1261
+ process.stderr.write(`${pc5.dim("This is a bug \u2014 please file an issue.")}
1262
+ `);
1263
+ return 2;
1264
+ }
1265
+ async function runCli(argv) {
1266
+ const program = buildProgram();
1267
+ try {
1268
+ await program.parseAsync(argv);
1269
+ return Number(process.exitCode ?? 0);
1270
+ } catch (err) {
1271
+ return reportError(err);
1272
+ }
1273
+ }
1274
+
1275
+ export {
1276
+ buildProgram,
1277
+ runCli
1278
+ };
1279
+ //# sourceMappingURL=chunk-5LCVXNHI.js.map