@bartolli/kmd 0.6.0 → 0.7.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.
package/dist/kmd.mjs CHANGED
@@ -116,7 +116,7 @@ CREATE TABLE IF NOT EXISTS meta (
116
116
  }
117
117
  });
118
118
 
119
- // ../cli/src/config.ts
119
+ // ../db/src/vault-config.ts
120
120
  import { readFile } from "node:fs/promises";
121
121
  import { join as join2 } from "node:path";
122
122
  import { parse } from "yaml";
@@ -131,6 +131,9 @@ function isValidRegex(pattern) {
131
131
  function kindName(entry) {
132
132
  return typeof entry === "string" ? entry : entry.name;
133
133
  }
134
+ function configJsonSchema() {
135
+ return z.toJSONSchema(VaultConfigSchema, { target: "draft-7" });
136
+ }
134
137
  async function loadVaultConfig(vaultRoot2) {
135
138
  const path = join2(vaultRoot2, "vault.yaml");
136
139
  let raw;
@@ -148,42 +151,46 @@ ${issues}`);
148
151
  return parsed.data;
149
152
  }
150
153
  var ScopeSchema, KindEntrySchema, WhenSchema, TriggerSchema, TriggersSchema, VaultConfigSchema, BUILT_IN_KINDS;
151
- var init_config = __esm({
152
- "../cli/src/config.ts"() {
154
+ var init_vault_config = __esm({
155
+ "../db/src/vault-config.ts"() {
153
156
  "use strict";
154
- ScopeSchema = z.object({
155
- repo: z.string().optional(),
156
- methodology: z.string().optional(),
157
- status: z.string()
157
+ ScopeSchema = z.strictObject({
158
+ repo: z.string().optional().describe(
159
+ "Consumer repo path (~ expands). Load-bearing for kmd hook: the active scope resolves by matching the session cwd against it."
160
+ ),
161
+ methodology: z.string().optional().describe("Must appear in the methodologies list."),
162
+ status: z.string().describe("Free string; keep within statuses by convention.")
158
163
  });
159
164
  KindEntrySchema = z.union([
160
165
  z.string(),
161
- z.object({
166
+ z.strictObject({
162
167
  name: z.string(),
163
- signal: z.string(),
164
- where: z.string()
168
+ signal: z.string().describe("When to pick this kind."),
169
+ where: z.string().describe("Path pattern pages of this kind follow.")
165
170
  })
166
171
  ]);
167
172
  WhenSchema = z.union([
168
173
  z.string(),
169
- z.object({
174
+ z.strictObject({
170
175
  name: z.enum(["newer-than"]),
171
176
  fresh: z.array(z.string().min(1)).min(1),
172
177
  than: z.array(z.string().min(1)).min(1)
173
178
  })
174
179
  ]);
175
- TriggerSchema = z.object({
176
- id: z.string().min(1),
180
+ TriggerSchema = z.strictObject({
181
+ id: z.string().min(1).describe("Unique per scope list; duplicates keep the first occurrence."),
177
182
  on: z.enum(["prompt", "pretool"]),
178
- enforce: z.enum(["inject", "warn", "block"]),
179
- keywords: z.array(z.string().min(1)).optional(),
180
- intent: z.array(z.string()).optional(),
181
- tool: z.string().optional(),
182
- args_match: z.string().optional(),
183
- files: z.array(z.string().min(1)).optional(),
184
- when: WhenSchema.optional(),
185
- text: z.string().optional(),
186
- reason: z.string().optional()
183
+ enforce: z.enum(["inject", "warn", "block"]).describe("inject: context line \xB7 warn: stderr \xB7 block: deny with reason."),
184
+ keywords: z.array(z.string().min(1)).optional().describe("Word-boundary, porter-stemmed match. Prompt triggers need keywords or intent."),
185
+ intent: z.array(z.string()).optional().describe("Case-insensitive regexes over the raw prompt \u2014 the stemming escape hatch."),
186
+ tool: z.string().optional().describe("Exact tool name; pretool matchers AND-compose."),
187
+ args_match: z.string().optional().describe("Regex over the serialized tool input."),
188
+ files: z.array(z.string().min(1)).optional().describe("Globs against the paths the tool touches; pretool triggers only."),
189
+ when: WhenSchema.optional().describe(
190
+ "Precondition \u2014 the gate fires only when it is UNMET. newer-than: the newest page matching fresh must carry frontmatter updated at or after the newest matching than."
191
+ ),
192
+ text: z.string().optional().describe("Required for inject and warn \u2014 the line emitted."),
193
+ reason: z.string().optional().describe("Required for block \u2014 the denial the agent reads.")
187
194
  }).superRefine((trigger, ctx) => {
188
195
  if (trigger.on === "prompt" && !trigger.keywords?.length && !trigger.intent?.length) {
189
196
  ctx.addIssue({
@@ -221,22 +228,28 @@ var init_config = __esm({
221
228
  }
222
229
  });
223
230
  TriggersSchema = z.record(z.string(), z.array(TriggerSchema));
224
- VaultConfigSchema = z.object({
225
- scopes: z.record(z.string(), ScopeSchema),
226
- kinds: z.array(KindEntrySchema),
227
- statuses: z.array(z.string()),
228
- methodologies: z.array(z.string()),
229
- tags: z.object({
230
- canonical: z.array(z.string()),
231
- aliases: z.record(z.string(), z.string())
231
+ VaultConfigSchema = z.strictObject({
232
+ scopes: z.record(z.string(), ScopeSchema).describe("Scope name \u2192 entry; key = directory name under projects/."),
233
+ kinds: z.array(KindEntrySchema).describe(
234
+ "Page kind vocabulary; validate-enforced. Object form adds a kind-selector row to wiki://authoring."
235
+ ),
236
+ statuses: z.array(z.string()).describe("Page status vocabulary; validate-enforced."),
237
+ methodologies: z.array(z.string()).describe("Methodology vocabulary for pages and scope entries."),
238
+ tags: z.strictObject({
239
+ canonical: z.array(z.string()).describe("Approved tags."),
240
+ aliases: z.record(z.string(), z.string()).describe("Alias \u2192 canonical; validate warns on alias use.")
232
241
  }),
233
- authoring_rules: z.string().optional(),
234
- authoring_rules_extra: z.string().optional(),
235
- sync_protocol: z.string().optional(),
236
- sync_protocol_extra: z.string().optional(),
237
- triggers: TriggersSchema.optional(),
238
- triggers_extra: TriggersSchema.optional()
239
- }).superRefine((config, ctx) => {
242
+ authoring_rules: z.string().optional().describe("Replaces the served \xA7 Authoring rules entirely \u2014 escape hatch."),
243
+ authoring_rules_extra: z.string().optional().describe("Appended after the served \xA7 Authoring rules."),
244
+ sync_protocol: z.string().optional().describe("Replaces the served \xA7 Resync protocol entirely \u2014 escape hatch."),
245
+ sync_protocol_extra: z.string().optional().describe("Appended after the served \xA7 Resync protocol."),
246
+ triggers: TriggersSchema.optional().describe(
247
+ 'Full-replace of the trigger base per scope \u2014 escape hatch. "_all" is reserved for triggers_extra.'
248
+ ),
249
+ triggers_extra: TriggersSchema.optional().describe(
250
+ 'Appended per scope after the engine defaults; the reserved "_all" key fires in every session.'
251
+ )
252
+ }).describe("kmd vault.yaml \u2014 controlled vocabulary and gate triggers.").superRefine((config, ctx) => {
240
253
  for (const [name, scope] of Object.entries(config.scopes)) {
241
254
  if (scope.methodology !== void 0 && !config.methodologies.includes(scope.methodology)) {
242
255
  ctx.addIssue({
@@ -310,11 +323,287 @@ var init_frontmatter = __esm({
310
323
  }
311
324
  });
312
325
 
326
+ // ../cli/src/init-templates.ts
327
+ var VAULT_TEMPLATES;
328
+ var init_init_templates = __esm({
329
+ "../cli/src/init-templates.ts"() {
330
+ "use strict";
331
+ VAULT_TEMPLATES = {
332
+ "note.md": '---\ntitle: {{title}}\ntags: []\ncreated: "{{date}}"\nupdated: {{date}}\n---\n\n# {{title}}\n',
333
+ "project-adr.md": '---\ntitle: {{title}}\nkind: adr\nscope:\nstatus: active\nsummary:\ntags: []\nsupersedes:\nsuperseded_by:\nsources: []\ncreated: "{{date}}"\nupdated: {{date}}\n---\n\n# {{title}}\n\n## Status\n\n> [!info] Active\n\n## Context\n\nThe forces in tension \u2014 what made this a decision rather than an\nobvious step. Name the constraints; skip the history recap.\n\n## Decision\n\nWhat was decided, precisely. Predicates only \u2014 falsifiable, not\naspirational.\n\n## Rationale\n\nWhy this side won: which alternatives were live, and what killed each.\nAn ADR with no rejected alternative is a spec in disguise.\n\n## Consequences\n\nWhat this commits the project to going forward \u2014 including the costs\naccepted, not just the benefits claimed.\n\n## Links\n\n-\n',
334
+ "project-index.md": `---
335
+ title: {{title}}
336
+ kind: project
337
+ methodology: sdd
338
+ phase: 1
339
+ repo:
340
+ scope:
341
+ status: active
342
+ tags: []
343
+ summary:
344
+ created: "{{date}}"
345
+ updated: {{date}}
346
+ ---
347
+
348
+ # {{title}}
349
+
350
+ ## Summary
351
+
352
+ One paragraph. What this project is and what it delivers.
353
+
354
+ ## Current Phase
355
+
356
+ What's in flight right now. 1\u20133 sentences.
357
+
358
+ ## Links
359
+
360
+ - [[projects/{{title}}/primer]]
361
+
362
+ ## Sources
363
+
364
+ -
365
+ `,
366
+ "project-ops.md": `---
367
+ title: {{title}}
368
+ kind: ops
369
+ scope:
370
+ status: active
371
+ summary:
372
+ tags: []
373
+ sources: []
374
+ created: "{{date}}"
375
+ updated: {{date}}
376
+ ---
377
+
378
+ # {{title}}
379
+
380
+ ## Summary
381
+
382
+ What this runbook operates and when it runs.
383
+
384
+ ## Context
385
+
386
+ What triggers this flow; what breaks when it's skipped.
387
+
388
+ ## Details
389
+
390
+ Numbered steps executable without asking: exact commands, expected
391
+ output, failure modes and their recovery. Predicates only \u2014 if a step
392
+ needs judgment, say whose.
393
+
394
+ ## Links
395
+
396
+ -
397
+
398
+ ## Sources
399
+
400
+ -
401
+ `,
402
+ "project-plan.md": `---
403
+ title: {{title}}
404
+ kind: plan
405
+ scope:
406
+ status: active
407
+ summary:
408
+ tags: []
409
+ created: "{{date}}"
410
+ updated: {{date}}
411
+ ---
412
+
413
+ # {{title}}
414
+
415
+ ## Goal
416
+
417
+ One sentence: what this phase delivers. If it needs two, it's two
418
+ plans.
419
+
420
+ ## Scope
421
+
422
+ What's in and \u2014 more important \u2014 what's out. Out-of-scope lines stop
423
+ scope creep better than in-scope lines define it.
424
+
425
+ ## Milestones
426
+
427
+ Checkable outcomes, not activities. Tick here; don't cascade ticks to
428
+ index.md unless phase or status changed.
429
+
430
+ 1.
431
+ 2.
432
+
433
+ ## Dependencies
434
+
435
+ -
436
+
437
+ ## Status Log
438
+
439
+ Dated one-liners, newest first. This is the only history surface \u2014
440
+ primer and index stay clean.
441
+
442
+ - {{date}}: Phase started.
443
+ `,
444
+ "project-primer.md": "---\ncreated: \"{{date}}\"\nupdated: {{date}}\n---\n\n# Primer\n\n## Current Focus\n\nWhat we're working on right now. 1\u20133 sentences. Not a history log \u2014\nprogress lives in plan checkboxes; history lives in git.\n\n## Open Questions\n\nCurrently-open questions only. Strip on resolution; reflect the\nresolution in the relevant ADR or spec, not here.\n\n-\n\n## Blocked On\n\nNothing currently. List only items genuinely blocking forward\nprogress.\n\n## Load-bearing invariants (optional)\n\nFacts not derivable from code or commits \u2014 pipeline orderings, sync\nconstraints, file-location rationales. The kind of fact a fresh-context\nagent would otherwise re-derive incorrectly. Durable across sessions.\n\n## Read Order (optional)\n\n\u22645 entries. Wiki/doc pointers \u2014 what to read to orient. Each entry\none line: `[[wikilink]] \u2014 why`.\n\n1.\n\n## Working set (optional)\n\n\u226410 entries. File pointers (code, fixtures, configs) for the\nimmediate Next Steps, with one-line context. Lets a fresh-context\nagent skip the discovery loop. Task-scoped \u2014 refresh when focus\nshifts. Most valuable when transitioning phases or starting a new\nfeature; can be skipped mid-slice when the immediate context already\ncarries it.\n\n- `path/to/file` \u2014 why it's relevant for the next steps.\n",
445
+ "project-spec.md": `---
446
+ title: {{title}}
447
+ kind: spec
448
+ scope:
449
+ status: active
450
+ summary:
451
+ tags: []
452
+ sources: []
453
+ created: "{{date}}"
454
+ updated: {{date}}
455
+ ---
456
+
457
+ # {{title}}
458
+
459
+ ## Summary
460
+
461
+ One paragraph, in predicates \u2014 the system's current shape, not its
462
+ history. If you're recording a *choice* between alternatives, stop:
463
+ that's an ADR.
464
+
465
+ ## Context
466
+
467
+ The question this spec answers and who asks it. One paragraph, no
468
+ narrative arc.
469
+
470
+ ## Details
471
+
472
+ The actual contract: inputs, outputs, invariants, failure modes. Use
473
+ subsections per concern. Present tense \u2014 the spec describes what IS
474
+ and must match current code at every commit.
475
+
476
+ ## Links
477
+
478
+ -
479
+
480
+ ## Sources
481
+
482
+ -
483
+ `,
484
+ "project-story.md": '---\ntitle: {{title}}\nkind: story\nscope:\nparent:\nstatus: active\ntriage_state: needs-triage\ncategory: enhancement\nblocked_by: []\ntags: []\nsources: []\ncreated: "{{date}}"\nupdated: {{date}}\n---\n\n# {{title}}\n\n## User Story\n\nAs a {{actor}}, I want {{capability}}, so that {{benefit}}.\n\n## Scenarios\n\nScenarios are the test specification \u2014 each becomes a failing test\nbefore implementation. Write observable outcomes, not implementation\nsteps.\n\n**Scenario: {{scenario name}}**\n- Given {{precondition}}\n- When {{action}}\n- Then {{expected outcome}}\n\n**Scenario: {{edge case name}}**\n- Given {{precondition}}\n- When {{action}}\n- Then {{expected outcome}}\n\n## Slices\n\nVertical tracer bullets: each slice ships signature + implementation +\ntests + wiring, independently committable. `AFK` = agent works alone;\n`HITL` = human in the loop.\n\n- [ ] **Slice 1** \u2014 {{description}} \xB7 `AFK` \xB7 [[spec-{{topic}}]]\n- [ ] **Slice 2** \u2014 {{description}} \xB7 `HITL` \xB7 [[adr-{{decision}}]]\n\n## References\n\n- [[spec-{{related-spec}}]]\n- [[adr-{{related-adr}}]]\n',
485
+ "research-article.md": '---\ntitle: {{title}}\nkind: article\ntopic:\nstatus: active\nsummary:\ntags: []\nsources: []\ncreated: "{{date}}"\nupdated: {{date}}\n---\n\n# {{title}}\n\n## Summary\n\nOne paragraph. What this page covers and why it matters.\n\n## Context\n\nWhy this page exists. The problem or question it addresses.\n\n## Details\n\nThe actual content. Use subsections as needed.\n\n## Links\n\n-\n\n## Sources\n\n-\n',
486
+ "research-index.md": '---\ntitle: {{title}}\nkind: topic\nstatus: active\nsummary:\nconfidence: medium\nsource_count: 0\ntags: []\ncreated: "{{date}}"\nupdated: {{date}}\n---\n\n# {{title}}\n\n## Summary\n\nOne paragraph. What this research topic covers.\n\n## Confidence\n\nWhy the confidence level is what it is. What would raise or lower it.\n\n## Pages\n\n-\n\n## Sources\n\n-\n',
487
+ "research-src.md": '---\ntitle: {{title}}\nkind: src\ntopic:\nstatus: active\nsummary:\ntags: []\nsource_url:\ncreated: "{{date}}"\nupdated: {{date}}\n---\n\n# {{title}}\n\n## Summary\n\nWhat this source is. Author, origin, year.\n\n## Key Points\n\n-\n\n## Quotes\n\n>\n\n## Relevance\n\nWhy this source matters to the topic.\n\n## Citation\n\n`{author, year, title, url}`\n'
488
+ };
489
+ }
490
+ });
491
+
492
+ // ../cli/src/init.ts
493
+ import { mkdir, readdir, readFile as readFile2, writeFile } from "node:fs/promises";
494
+ import { join as join3, resolve as resolve2 } from "node:path";
495
+ import { stringify } from "yaml";
496
+ async function refreshSchemaFile(root) {
497
+ const path = join3(root, SCHEMA_FILE);
498
+ const next = `${JSON.stringify(configJsonSchema(), null, 2)}
499
+ `;
500
+ try {
501
+ if (await readFile2(path, "utf8") === next) return false;
502
+ } catch {
503
+ }
504
+ await writeFile(path, next);
505
+ return true;
506
+ }
507
+ async function scaffoldVault(dir) {
508
+ const root = resolve2(dir);
509
+ let entries = [];
510
+ try {
511
+ entries = await readdir(root);
512
+ } catch (err) {
513
+ if (err.code !== "ENOENT") throw err;
514
+ }
515
+ if (entries.includes("vault.yaml")) {
516
+ throw new Error(`already a vault: ${root} (vault.yaml exists)`);
517
+ }
518
+ if (entries.length > 0) {
519
+ throw new Error(
520
+ `target is not empty: ${root}
521
+ found: ${entries.join(", ")}
522
+ delete it or pick another directory`
523
+ );
524
+ }
525
+ for (const domain of DOMAIN_DIRS) {
526
+ await mkdir(join3(root, domain), { recursive: true });
527
+ }
528
+ await mkdir(join3(root, "templates"), { recursive: true });
529
+ for (const [file, content] of Object.entries(VAULT_TEMPLATES)) {
530
+ await writeFile(join3(root, "templates", file), content);
531
+ }
532
+ await refreshSchemaFile(root);
533
+ await writeFile(join3(root, "vault.yaml"), SCHEMA_MODELINE + stringify(STARTER_CONFIG));
534
+ return root;
535
+ }
536
+ async function promptYesNo(question, input = process.stdin, output = process.stderr) {
537
+ const { createInterface } = await import("node:readline/promises");
538
+ const rl = createInterface({ input, output });
539
+ try {
540
+ const answer = await rl.question(question);
541
+ return /^y(es)?$/i.test(answer.trim());
542
+ } finally {
543
+ rl.close();
544
+ }
545
+ }
546
+ async function runInit(dir, yes = false) {
547
+ let target = dir;
548
+ if (!target) {
549
+ if (yes) {
550
+ target = ".";
551
+ } else if (process.stdin.isTTY) {
552
+ const ok = await promptYesNo(`initialize a vault in ${resolve2(".")}? [y/N] `);
553
+ if (!ok) {
554
+ console.error("init: aborted");
555
+ process.exit(1);
556
+ }
557
+ target = ".";
558
+ } else {
559
+ console.error("usage: kmd init <dir> (or --yes to scaffold the current directory)");
560
+ process.exit(2);
561
+ }
562
+ }
563
+ let root;
564
+ try {
565
+ root = await scaffoldVault(target);
566
+ } catch (err) {
567
+ console.error(`init: ${err instanceof Error ? err.message : err}`);
568
+ process.exit(1);
569
+ }
570
+ const templateCount = Object.keys(VAULT_TEMPLATES).length;
571
+ console.log(`initialized empty vault at ${root}
572
+
573
+ vault.yaml starter vocabulary \u2014 add your first scope under scopes:
574
+ vault.schema.json IDE validation via the yaml-language-server modeline
575
+ templates/ ${templateCount} built-in templates (served at wiki://template/...)
576
+ projects/ research/ notes/
577
+
578
+ next steps:
579
+ export WIKI_VAULT=${root}
580
+ kmd mcp ${root} # stdio MCP server (prime, search)`);
581
+ }
582
+ var SCHEMA_FILE, SCHEMA_MODELINE, STARTER_CONFIG, DOMAIN_DIRS;
583
+ var init_init = __esm({
584
+ "../cli/src/init.ts"() {
585
+ "use strict";
586
+ init_vault_config();
587
+ init_init_templates();
588
+ SCHEMA_FILE = "vault.schema.json";
589
+ SCHEMA_MODELINE = `# yaml-language-server: $schema=./${SCHEMA_FILE}
590
+ `;
591
+ STARTER_CONFIG = {
592
+ scopes: {},
593
+ kinds: ["project", "spec", "adr", "plan", "story", "ops", "topic", "article", "src", "note"],
594
+ statuses: ["draft", "active", "superseded", "archived"],
595
+ methodologies: ["sdd", "tdd", "hybrid"],
596
+ tags: { canonical: [], aliases: {} }
597
+ };
598
+ DOMAIN_DIRS = ["projects", "research", "notes"];
599
+ }
600
+ });
601
+
313
602
  // ../cli/src/sync.ts
314
603
  import { createHash as createHash2 } from "node:crypto";
315
604
  import { mkdirSync } from "node:fs";
316
- import { readdir, readFile as readFile2 } from "node:fs/promises";
317
- import { dirname, join as join3, relative, sep } from "node:path";
605
+ import { readdir as readdir2, readFile as readFile3 } from "node:fs/promises";
606
+ import { dirname, join as join4, relative, sep } from "node:path";
318
607
  import { z as z2 } from "zod";
319
608
  function loadEnv() {
320
609
  const parsed = EnvSchema.safeParse({
@@ -330,18 +619,18 @@ function loadEnv() {
330
619
  async function walkMarkdown(root, domain) {
331
620
  const out = [];
332
621
  async function recurse(dir) {
333
- const entries = await readdir(dir, { withFileTypes: true }).catch(() => null);
622
+ const entries = await readdir2(dir, { withFileTypes: true }).catch(() => null);
334
623
  if (!entries) return;
335
624
  for (const entry of entries) {
336
625
  if (entry.name.startsWith(".")) continue;
337
626
  if (entry.isDirectory()) {
338
- await recurse(join3(dir, entry.name));
627
+ await recurse(join4(dir, entry.name));
339
628
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
340
- out.push(join3(dir, entry.name));
629
+ out.push(join4(dir, entry.name));
341
630
  }
342
631
  }
343
632
  }
344
- await recurse(join3(root, domain));
633
+ await recurse(join4(root, domain));
345
634
  return out;
346
635
  }
347
636
  function toRelativePath(root, absolute) {
@@ -480,7 +769,7 @@ async function syncVault(vaultRoot2) {
480
769
  let skipped = 0;
481
770
  for (const file of files) {
482
771
  const path = toRelativePath(vaultRoot2, file);
483
- const raw = await readFile2(file, "utf8");
772
+ const raw = await readFile3(file, "utf8");
484
773
  const parsed = parseFrontmatter(raw);
485
774
  const fields = buildPageFields(path, raw, parsed, scopes);
486
775
  if (!fields) {
@@ -507,6 +796,9 @@ async function syncVault(vaultRoot2) {
507
796
  db.exec("INSERT INTO pages_fts(pages_fts) VALUES('rebuild')");
508
797
  setMeta(db, "vault_root", canonicalVaultRoot(vaultRoot2));
509
798
  setMeta(db, "last_synced", (/* @__PURE__ */ new Date()).toISOString());
799
+ if (await refreshSchemaFile(vaultRoot2)) {
800
+ console.error("sync: vault.schema.json refreshed to the running engine");
801
+ }
510
802
  return {
511
803
  changed,
512
804
  unchanged,
@@ -535,8 +827,9 @@ var init_sync = __esm({
535
827
  "../cli/src/sync.ts"() {
536
828
  "use strict";
537
829
  init_database();
538
- init_config();
830
+ init_vault_config();
539
831
  init_frontmatter();
832
+ init_init();
540
833
  EnvSchema = z2.object({
541
834
  WIKI_VAULT: z2.string().min(1)
542
835
  });
@@ -556,8 +849,8 @@ var init_sync = __esm({
556
849
  });
557
850
 
558
851
  // ../cli/src/validate.ts
559
- import { readFile as readFile3, stat } from "node:fs/promises";
560
- import { join as join4 } from "node:path";
852
+ import { readFile as readFile4, stat } from "node:fs/promises";
853
+ import { join as join5 } from "node:path";
561
854
  function hasIndexableTitle(data) {
562
855
  return typeof data.title === "string" && data.title.trim() !== "";
563
856
  }
@@ -915,7 +1208,7 @@ async function validateVault(root) {
915
1208
  const pages = [];
916
1209
  const linkPages = [];
917
1210
  for (const { relPath, abs } of files) {
918
- const raw = await readFile3(abs, "utf8");
1211
+ const raw = await readFile4(abs, "utf8");
919
1212
  findings.push(...validatePage(relPath, raw, cfg, refIndex));
920
1213
  try {
921
1214
  const parsed = parseFrontmatter(raw);
@@ -929,7 +1222,7 @@ async function validateVault(root) {
929
1222
  for (const name of customKindNames(cfg)) {
930
1223
  const file = `templates/${name}.md`;
931
1224
  try {
932
- await stat(join4(root, file));
1225
+ await stat(join5(root, file));
933
1226
  } catch {
934
1227
  findings.push({
935
1228
  path: file,
@@ -945,7 +1238,7 @@ var REQUIRED_FIELDS, TAG_OPTIONAL_KINDS, FOLDER_PATTERNS, UNIVERSAL_FLOOR;
945
1238
  var init_validate = __esm({
946
1239
  "../cli/src/validate.ts"() {
947
1240
  "use strict";
948
- init_config();
1241
+ init_vault_config();
949
1242
  init_frontmatter();
950
1243
  init_sync();
951
1244
  REQUIRED_FIELDS = {
@@ -1003,12 +1296,13 @@ __export(cli_exports, {
1003
1296
  resolveCli: () => resolveCli,
1004
1297
  runConfig: () => runConfig,
1005
1298
  runDbReset: () => runDbReset,
1299
+ runInit: () => runInit,
1006
1300
  runSyncCommand: () => runSyncCommand,
1007
1301
  runValidate: () => runValidate,
1008
1302
  vaultRoot: () => vaultRoot
1009
1303
  });
1010
1304
  import { existsSync, readdirSync, rmSync } from "node:fs";
1011
- import { dirname as dirname2, join as join5 } from "node:path";
1305
+ import { dirname as dirname2, join as join6 } from "node:path";
1012
1306
  import { parseArgs } from "node:util";
1013
1307
  function resolveCli(argv) {
1014
1308
  const { positionals: positionals2 } = parseArgs({ args: argv, allowPositionals: true, strict: false });
@@ -1078,7 +1372,7 @@ function knownVaults() {
1078
1372
  const known = [];
1079
1373
  for (const entry of readdirSync(root, { withFileTypes: true })) {
1080
1374
  if (!entry.isDirectory()) continue;
1081
- const index = join5(root, entry.name, "index.db");
1375
+ const index = join6(root, entry.name, "index.db");
1082
1376
  if (!existsSync(index)) continue;
1083
1377
  const db = openDatabase(index);
1084
1378
  try {
@@ -1141,6 +1435,7 @@ var init_cli = __esm({
1141
1435
  init_database();
1142
1436
  init_sync();
1143
1437
  init_validate();
1438
+ init_init();
1144
1439
  }
1145
1440
  });
1146
1441
 
@@ -1161,7 +1456,7 @@ ${issues}`);
1161
1456
  };
1162
1457
  }
1163
1458
  var EnvSchema2;
1164
- var init_config2 = __esm({
1459
+ var init_config = __esm({
1165
1460
  "../mcp/src/config.ts"() {
1166
1461
  "use strict";
1167
1462
  EnvSchema2 = z3.object({
@@ -1193,7 +1488,7 @@ var init_db = __esm({
1193
1488
  // ../mcp/src/lib/diag.ts
1194
1489
  import { appendFileSync, mkdirSync as mkdirSync3 } from "node:fs";
1195
1490
  import { homedir as homedir2 } from "node:os";
1196
- import { join as join6 } from "node:path";
1491
+ import { join as join7 } from "node:path";
1197
1492
  function diag(msg, data) {
1198
1493
  try {
1199
1494
  const line = data ? `${(/* @__PURE__ */ new Date()).toISOString()} pid=${process.pid} ${msg} ${JSON.stringify(data)}
@@ -1207,8 +1502,8 @@ var DIAG_DIR, DIAG_LOG_PATH;
1207
1502
  var init_diag = __esm({
1208
1503
  "../mcp/src/lib/diag.ts"() {
1209
1504
  "use strict";
1210
- DIAG_DIR = join6(homedir2(), ".local", "state", "wiki-mcp");
1211
- DIAG_LOG_PATH = join6(DIAG_DIR, "server.log");
1505
+ DIAG_DIR = join7(homedir2(), ".local", "state", "wiki-mcp");
1506
+ DIAG_LOG_PATH = join7(DIAG_DIR, "server.log");
1212
1507
  try {
1213
1508
  mkdirSync3(DIAG_DIR, { recursive: true });
1214
1509
  } catch {
@@ -1235,176 +1530,6 @@ var init_logger = __esm({
1235
1530
  }
1236
1531
  });
1237
1532
 
1238
- // ../mcp/src/vault-config.ts
1239
- import { readFile as readFile4 } from "node:fs/promises";
1240
- import { join as join7 } from "node:path";
1241
- import { parse as parse2 } from "yaml";
1242
- import { z as z4 } from "zod";
1243
- function isValidRegex2(pattern) {
1244
- try {
1245
- return Boolean(new RegExp(pattern));
1246
- } catch {
1247
- return false;
1248
- }
1249
- }
1250
- function kindName2(entry) {
1251
- return typeof entry === "string" ? entry : entry.name;
1252
- }
1253
- async function loadVaultConfig2(vaultRoot2) {
1254
- const path = join7(vaultRoot2, "vault.yaml");
1255
- let raw;
1256
- try {
1257
- raw = await readFile4(path, "utf8");
1258
- } catch (err) {
1259
- throw new Error(`vault.yaml not found at ${path}`, { cause: err });
1260
- }
1261
- const parsed = VaultConfigSchema2.safeParse(parse2(raw));
1262
- if (!parsed.success) {
1263
- const issues = parsed.error.issues.map((i) => ` - ${i.path.join(".") || "(root)"}: ${i.message}`).join("\n");
1264
- throw new Error(`Invalid vault.yaml at ${path}:
1265
- ${issues}`);
1266
- }
1267
- return parsed.data;
1268
- }
1269
- var ScopeSchema2, KindEntrySchema2, WhenSchema2, TriggerSchema2, TriggersSchema2, VaultConfigSchema2, BUILT_IN_KINDS2;
1270
- var init_vault_config = __esm({
1271
- "../mcp/src/vault-config.ts"() {
1272
- "use strict";
1273
- ScopeSchema2 = z4.object({
1274
- repo: z4.string().optional(),
1275
- methodology: z4.string().optional(),
1276
- status: z4.string()
1277
- });
1278
- KindEntrySchema2 = z4.union([
1279
- z4.string(),
1280
- z4.object({
1281
- name: z4.string(),
1282
- signal: z4.string(),
1283
- where: z4.string()
1284
- })
1285
- ]);
1286
- WhenSchema2 = z4.union([
1287
- z4.string(),
1288
- z4.object({
1289
- name: z4.enum(["newer-than"]),
1290
- fresh: z4.array(z4.string().min(1)).min(1),
1291
- than: z4.array(z4.string().min(1)).min(1)
1292
- })
1293
- ]);
1294
- TriggerSchema2 = z4.object({
1295
- id: z4.string().min(1),
1296
- on: z4.enum(["prompt", "pretool"]),
1297
- enforce: z4.enum(["inject", "warn", "block"]),
1298
- keywords: z4.array(z4.string().min(1)).optional(),
1299
- intent: z4.array(z4.string()).optional(),
1300
- tool: z4.string().optional(),
1301
- args_match: z4.string().optional(),
1302
- files: z4.array(z4.string().min(1)).optional(),
1303
- when: WhenSchema2.optional(),
1304
- text: z4.string().optional(),
1305
- reason: z4.string().optional()
1306
- }).superRefine((trigger, ctx) => {
1307
- if (trigger.on === "prompt" && !trigger.keywords?.length && !trigger.intent?.length) {
1308
- ctx.addIssue({
1309
- code: "custom",
1310
- message: `prompt trigger "${trigger.id}" needs keywords or intent`
1311
- });
1312
- }
1313
- if (trigger.on === "pretool" && trigger.tool === void 0 && trigger.args_match === void 0 && !trigger.files?.length) {
1314
- ctx.addIssue({
1315
- code: "custom",
1316
- message: `pretool trigger "${trigger.id}" needs a tool, args_match, or files matcher`
1317
- });
1318
- }
1319
- if (trigger.on === "prompt" && trigger.files !== void 0) {
1320
- ctx.addIssue({
1321
- code: "custom",
1322
- message: `trigger "${trigger.id}": files applies to pretool triggers only`
1323
- });
1324
- }
1325
- if (trigger.enforce === "block" ? trigger.reason === void 0 : trigger.text === void 0) {
1326
- ctx.addIssue({
1327
- code: "custom",
1328
- message: trigger.enforce === "block" ? `block trigger "${trigger.id}" needs a reason` : `${trigger.enforce} trigger "${trigger.id}" needs a text`
1329
- });
1330
- }
1331
- const patterns = [...trigger.intent ?? []];
1332
- if (trigger.args_match !== void 0) patterns.push(trigger.args_match);
1333
- for (const pattern of patterns) {
1334
- if (!isValidRegex2(pattern)) {
1335
- ctx.addIssue({
1336
- code: "custom",
1337
- message: `trigger "${trigger.id}" has an invalid regex: ${pattern}`
1338
- });
1339
- }
1340
- }
1341
- });
1342
- TriggersSchema2 = z4.record(z4.string(), z4.array(TriggerSchema2));
1343
- VaultConfigSchema2 = z4.object({
1344
- scopes: z4.record(z4.string(), ScopeSchema2),
1345
- kinds: z4.array(KindEntrySchema2),
1346
- statuses: z4.array(z4.string()),
1347
- methodologies: z4.array(z4.string()),
1348
- tags: z4.object({
1349
- canonical: z4.array(z4.string()),
1350
- aliases: z4.record(z4.string(), z4.string())
1351
- }),
1352
- authoring_rules: z4.string().optional(),
1353
- authoring_rules_extra: z4.string().optional(),
1354
- sync_protocol: z4.string().optional(),
1355
- sync_protocol_extra: z4.string().optional(),
1356
- triggers: TriggersSchema2.optional(),
1357
- triggers_extra: TriggersSchema2.optional()
1358
- }).superRefine((config, ctx) => {
1359
- for (const [name, scope] of Object.entries(config.scopes)) {
1360
- if (scope.methodology !== void 0 && !config.methodologies.includes(scope.methodology)) {
1361
- ctx.addIssue({
1362
- code: "custom",
1363
- path: ["scopes", name, "methodology"],
1364
- message: `"${scope.methodology}" is not in the methodologies list`
1365
- });
1366
- }
1367
- }
1368
- if (config.triggers?._all !== void 0) {
1369
- ctx.addIssue({
1370
- code: "custom",
1371
- path: ["triggers", "_all"],
1372
- message: '"_all" is reserved for triggers_extra'
1373
- });
1374
- }
1375
- for (const field of ["triggers", "triggers_extra"]) {
1376
- for (const [scope, list] of Object.entries(config[field] ?? {})) {
1377
- const seen = /* @__PURE__ */ new Set();
1378
- for (const trigger of list) {
1379
- if (seen.has(trigger.id)) {
1380
- ctx.addIssue({
1381
- code: "custom",
1382
- path: [field, scope],
1383
- message: `duplicate trigger id "${trigger.id}"`
1384
- });
1385
- }
1386
- seen.add(trigger.id);
1387
- }
1388
- }
1389
- }
1390
- });
1391
- BUILT_IN_KINDS2 = /* @__PURE__ */ new Set([
1392
- "project",
1393
- "spec",
1394
- "adr",
1395
- "plan",
1396
- "story",
1397
- "ops",
1398
- "topic",
1399
- "article",
1400
- "src",
1401
- "note",
1402
- "artifact",
1403
- "prompt"
1404
- ]);
1405
- }
1406
- });
1407
-
1408
1533
  // ../mcp/src/resources/authoring.ts
1409
1534
  function buildAuthoringRules(config) {
1410
1535
  const parts = [(config.authoring_rules ?? DEFAULT_AUTHORING_RULES).trim()];
@@ -1419,13 +1544,13 @@ function buildSyncProtocol(config) {
1419
1544
  function buildKindSelector(kinds) {
1420
1545
  const lines = ["## Kind selector", "", "| Signal | Kind | Where |", "|---|---|---|"];
1421
1546
  for (const entry of kinds) {
1422
- const name = kindName2(entry);
1547
+ const name = kindName(entry);
1423
1548
  const pedagogy = typeof entry === "string" ? KIND_PEDAGOGY.get(entry) : entry;
1424
1549
  const signal = pedagogy?.signal ?? "\u2014";
1425
1550
  const where = pedagogy?.where ?? "\u2014";
1426
1551
  lines.push(`| ${signal} | **${name}** | ${where} |`);
1427
1552
  }
1428
- const names = kinds.map(kindName2);
1553
+ const names = kinds.map(kindName);
1429
1554
  const hasNote = names.includes("note");
1430
1555
  const hasAdrAndSpec = names.includes("adr") && names.includes("spec");
1431
1556
  if (hasNote || hasAdrAndSpec) {
@@ -1448,7 +1573,7 @@ function buildVocabulary(config) {
1448
1573
  const lines = [
1449
1574
  "## Controlled vocabulary",
1450
1575
  "",
1451
- `**Kinds:** ${config.kinds.map(kindName2).join(", ")}`,
1576
+ `**Kinds:** ${config.kinds.map(kindName).join(", ")}`,
1452
1577
  buildStatusLine(config.statuses),
1453
1578
  `**Methodologies:** ${config.methodologies.join(", ")}`,
1454
1579
  `**Canonical tags:** ${config.tags.canonical.join(", ")}`
@@ -1624,7 +1749,7 @@ import { join as join8 } from "node:path";
1624
1749
  function customTemplates(config) {
1625
1750
  const specs = [];
1626
1751
  for (const entry of config.kinds) {
1627
- if (typeof entry === "string" || BUILT_IN_KINDS2.has(entry.name)) continue;
1752
+ if (typeof entry === "string" || BUILT_IN_KINDS.has(entry.name)) continue;
1628
1753
  specs.push({
1629
1754
  uri: `wiki://template/${entry.name}`,
1630
1755
  name: entry.name.charAt(0).toUpperCase() + entry.name.slice(1),
@@ -1825,7 +1950,7 @@ var init_toolResponse = __esm({
1825
1950
  });
1826
1951
 
1827
1952
  // ../mcp/src/tools/search.ts
1828
- import { z as z5 } from "zod";
1953
+ import { z as z4 } from "zod";
1829
1954
  function search(deps, input) {
1830
1955
  const ftsQuery = sanitizeFtsQuery(input.query);
1831
1956
  if (!ftsQuery) return { results: [] };
@@ -1872,15 +1997,15 @@ var init_search = __esm({
1872
1997
  "use strict";
1873
1998
  init_fts();
1874
1999
  init_toolResponse();
1875
- SearchInputSchema = z5.object({
1876
- query: z5.string().min(1).describe(
2000
+ SearchInputSchema = z4.object({
2001
+ query: z4.string().min(1).describe(
1877
2002
  "Natural-language search query. Matched against title, summary, and body via SQLite FTS5."
1878
2003
  ),
1879
- scope: z5.string().optional().describe('Optional: restrict to a project scope (e.g. "ontology", "sotto").'),
1880
- kind: z5.string().optional().describe(
2004
+ scope: z4.string().optional().describe('Optional: restrict to a project scope (e.g. "ontology", "sotto").'),
2005
+ kind: z4.string().optional().describe(
1881
2006
  "Optional: restrict to a kind. Project kinds: spec, adr, plan, ops. Research kinds: article, src. Misc: note."
1882
2007
  ),
1883
- limit: z5.number().int().min(1).max(50).default(5).describe("Maximum number of ranked results to return. Default 5.")
2008
+ limit: z4.number().int().min(1).max(50).default(5).describe("Maximum number of ranked results to return. Default 5.")
1884
2009
  });
1885
2010
  FTS_RANK = "bm25(pages_fts, 10.0, 5.0, 1.0)";
1886
2011
  }
@@ -1889,7 +2014,7 @@ var init_search = __esm({
1889
2014
  // ../mcp/src/tools/prime.ts
1890
2015
  import { readFile as readFile6 } from "node:fs/promises";
1891
2016
  import { basename as basename3, join as join9 } from "node:path";
1892
- import { z as z6 } from "zod";
2017
+ import { z as z5 } from "zod";
1893
2018
  function pathSlug(p) {
1894
2019
  return basename3(p).replace(/\.md$/, "");
1895
2020
  }
@@ -2032,7 +2157,7 @@ function renderMarkdown(d, config, task) {
2032
2157
  lines.push(countEntries.map(([k, n]) => `${k}: ${n}`).join(" | "));
2033
2158
  }
2034
2159
  lines.push("", "## Vocabulary");
2035
- lines.push(`kinds: ${config.kinds.map(kindName2).join(", ")}`);
2160
+ lines.push(`kinds: ${config.kinds.map(kindName).join(", ")}`);
2036
2161
  lines.push(`statuses: ${config.statuses.join(", ")}`);
2037
2162
  lines.push(`tags: ${config.tags.canonical.join(", ")}`);
2038
2163
  if (d.top_tags.length > 0) {
@@ -2093,14 +2218,14 @@ var init_prime = __esm({
2093
2218
  "../mcp/src/tools/prime.ts"() {
2094
2219
  "use strict";
2095
2220
  init_database();
2221
+ init_vault_config();
2096
2222
  init_frontmatter2();
2097
2223
  init_fts();
2098
2224
  init_toolResponse();
2099
- init_vault_config();
2100
2225
  init_search();
2101
- PrimeInputSchema = z6.object({
2102
- scope: z6.string().min(1).describe("Project scope to prime (matches projects/{scope}/ in the vault)."),
2103
- task: z6.string().optional().describe("Optional task description; surfaces top-3 tsvector-ranked relevant pages.")
2226
+ PrimeInputSchema = z5.object({
2227
+ scope: z5.string().min(1).describe("Project scope to prime (matches projects/{scope}/ in the vault)."),
2228
+ task: z5.string().optional().describe("Optional task description; surfaces top-3 tsvector-ranked relevant pages.")
2104
2229
  });
2105
2230
  }
2106
2231
  });
@@ -2152,7 +2277,7 @@ async function startMcpServer() {
2152
2277
  diag("main entered");
2153
2278
  const config = loadConfig();
2154
2279
  diag("config loaded", { vault: config.wikiVault, level: config.logLevel });
2155
- const vaultConfig = await loadVaultConfig2(config.wikiVault);
2280
+ const vaultConfig = await loadVaultConfig(config.wikiVault);
2156
2281
  diag("vault config loaded", {
2157
2282
  scopes: Object.keys(vaultConfig.scopes).length,
2158
2283
  kinds: vaultConfig.kinds.length,
@@ -2195,12 +2320,12 @@ async function startMcpServer() {
2195
2320
  var init_start = __esm({
2196
2321
  "../mcp/src/start.ts"() {
2197
2322
  "use strict";
2198
- init_config2();
2323
+ init_vault_config();
2324
+ init_config();
2199
2325
  init_db();
2200
2326
  init_diag();
2201
2327
  init_logger();
2202
2328
  init_server();
2203
- init_vault_config();
2204
2329
  }
2205
2330
  });
2206
2331
 
@@ -2228,11 +2353,11 @@ __export(hook_exports, {
2228
2353
  });
2229
2354
  import { mkdirSync as mkdirSync4, readdirSync as readdirSync2, readFileSync, rmSync as rmSync2, statSync, writeFileSync } from "node:fs";
2230
2355
  import { homedir as homedir3 } from "node:os";
2231
- import { join as join10, resolve as resolve2, sep as sep2 } from "node:path";
2356
+ import { join as join10, resolve as resolve3, sep as sep2 } from "node:path";
2232
2357
  import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
2233
2358
  import { parseArgs as parseArgs2 } from "node:util";
2234
2359
  import { parse as parseYaml3 } from "yaml";
2235
- import { z as z7 } from "zod";
2360
+ import { z as z6 } from "zod";
2236
2361
  function eventFields(raw) {
2237
2362
  let data;
2238
2363
  try {
@@ -2258,7 +2383,7 @@ function kiroIdePromptEvent(now = Date.now()) {
2258
2383
  }
2259
2384
  function loadTriggerFile(path) {
2260
2385
  try {
2261
- const result = z7.array(TriggerSchema).safeParse(parseYaml3(readFileSync(path, "utf8")));
2386
+ const result = z6.array(TriggerSchema).safeParse(parseYaml3(readFileSync(path, "utf8")));
2262
2387
  return result.success ? result.data : null;
2263
2388
  } catch {
2264
2389
  return null;
@@ -2506,10 +2631,10 @@ function patchPaths(toolInput) {
2506
2631
  return paths;
2507
2632
  }
2508
2633
  function vaultPathTouched(toolInput, vaultRoot2, cwd) {
2509
- const root = resolve2(vaultRoot2);
2634
+ const root = resolve3(vaultRoot2);
2510
2635
  const candidates = [...pathCandidates(toolInput, cwd), ...patchPaths(toolInput)];
2511
2636
  return candidates.some((candidate) => {
2512
- const absolute = resolve2(cwd ?? ".", candidate);
2637
+ const absolute = resolve3(cwd ?? ".", candidate);
2513
2638
  return absolute === root || absolute.startsWith(`${root}/`);
2514
2639
  });
2515
2640
  }
@@ -2739,7 +2864,7 @@ var init_hook = __esm({
2739
2864
  "../cli/src/hook.ts"() {
2740
2865
  "use strict";
2741
2866
  init_database();
2742
- init_config();
2867
+ init_vault_config();
2743
2868
  init_frontmatter();
2744
2869
  init_sync();
2745
2870
  init_validate();
@@ -2762,6 +2887,7 @@ process.on("warning", (warning) => {
2762
2887
  var USAGE = `usage: kmd <command> [options]
2763
2888
 
2764
2889
  commands:
2890
+ init [<dir>] [-y] scaffold a fresh vault (no dir: current directory \u2014 TTY prompt, or -y)
2765
2891
  sync vault \u2192 index sync (runs validate first)
2766
2892
  validate [<path>] deterministic vault checker (default: $WIKI_VAULT)
2767
2893
  mcp [<vault-root>] start the stdio MCP server (default: $WIKI_VAULT)
@@ -2780,7 +2906,8 @@ var { positionals, values } = parseArgs3({
2780
2906
  strict: false,
2781
2907
  options: {
2782
2908
  version: { type: "boolean", short: "v" },
2783
- help: { type: "boolean", short: "h" }
2909
+ help: { type: "boolean", short: "h" },
2910
+ yes: { type: "boolean", short: "y" }
2784
2911
  }
2785
2912
  });
2786
2913
  var command = values.version ? "--version" : values.help ? "--help" : positionals[0];
@@ -2792,6 +2919,11 @@ function applyVaultRoot(positionalIndex) {
2792
2919
  }
2793
2920
  async function run() {
2794
2921
  switch (command) {
2922
+ case "init": {
2923
+ const { runInit: runInit2 } = await Promise.resolve().then(() => (init_cli(), cli_exports));
2924
+ await runInit2(positionals[1], Boolean(values.yes));
2925
+ break;
2926
+ }
2795
2927
  case "sync": {
2796
2928
  const { runSyncCommand: runSyncCommand2 } = await Promise.resolve().then(() => (init_cli(), cli_exports));
2797
2929
  await runSyncCommand2();
@@ -2838,9 +2970,11 @@ async function run() {
2838
2970
  } else if (sub === "posttool") {
2839
2971
  const { runHookPosttool: runHookPosttool2 } = await Promise.resolve().then(() => (init_hook(), hook_exports));
2840
2972
  await runHookPosttool2();
2973
+ } else if (sub) {
2974
+ console.error(`kmd hook: unknown event: ${sub}`);
2841
2975
  } else {
2842
2976
  console.error(
2843
- sub ? `unknown hook event: ${sub}` : "usage: kmd hook <prompt|pretool|posttool> [<vault-root>] [--scope <scope>] [--harness <claude|kiro-ide>]"
2977
+ "usage: kmd hook <prompt|pretool|posttool> [<vault-root>] [--scope <scope>] [--harness <claude|kiro-ide>]"
2844
2978
  );
2845
2979
  process.exit(2);
2846
2980
  }
@@ -2864,6 +2998,11 @@ async function run() {
2864
2998
  break;
2865
2999
  }
2866
3000
  default: {
3001
+ const tail = positionals[1];
3002
+ if (tail === "prompt" || tail === "pretool" || tail === "posttool") {
3003
+ console.error(`kmd: unknown command: ${command}`);
3004
+ break;
3005
+ }
2867
3006
  console.error(`unknown command: ${command}
2868
3007
 
2869
3008
  ${USAGE}`);