@bartolli/kmd 0.6.0 → 0.8.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 +482 -264
- package/dist/kmd.mjs.map +4 -4
- package/package.json +1 -1
package/dist/kmd.mjs
CHANGED
|
@@ -116,7 +116,7 @@ CREATE TABLE IF NOT EXISTS meta (
|
|
|
116
116
|
}
|
|
117
117
|
});
|
|
118
118
|
|
|
119
|
-
// ../
|
|
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;
|
|
@@ -147,43 +150,54 @@ ${issues}`);
|
|
|
147
150
|
}
|
|
148
151
|
return parsed.data;
|
|
149
152
|
}
|
|
150
|
-
var ScopeSchema, KindEntrySchema, WhenSchema, TriggerSchema, TriggersSchema, VaultConfigSchema, BUILT_IN_KINDS;
|
|
151
|
-
var
|
|
152
|
-
"../
|
|
153
|
+
var ScopeSchema, KindEntrySchema, WhenSchema, DedupSchema, TriggerSchema, TriggersSchema, VaultConfigSchema, BUILT_IN_KINDS;
|
|
154
|
+
var init_vault_config = __esm({
|
|
155
|
+
"../db/src/vault-config.ts"() {
|
|
153
156
|
"use strict";
|
|
154
|
-
ScopeSchema = z.
|
|
155
|
-
repo: z.string().optional()
|
|
156
|
-
|
|
157
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
176
|
-
|
|
180
|
+
DedupSchema = z.union([
|
|
181
|
+
z.enum(["session", "never"]),
|
|
182
|
+
z.strictObject({ minutes: z.number().int().positive() })
|
|
183
|
+
]);
|
|
184
|
+
TriggerSchema = z.strictObject({
|
|
185
|
+
id: z.string().min(1).describe("Unique per scope list; duplicates keep the first occurrence."),
|
|
177
186
|
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
|
-
|
|
186
|
-
|
|
187
|
+
enforce: z.enum(["inject", "warn", "block"]).describe("inject: context line \xB7 warn: stderr \xB7 block: deny with reason."),
|
|
188
|
+
keywords: z.array(z.string().min(1)).optional().describe("Word-boundary, porter-stemmed match. Prompt triggers need keywords or intent."),
|
|
189
|
+
intent: z.array(z.string()).optional().describe("Case-insensitive regexes over the raw prompt \u2014 the stemming escape hatch."),
|
|
190
|
+
tool: z.string().optional().describe("Exact tool name; pretool matchers AND-compose."),
|
|
191
|
+
args_match: z.string().optional().describe("Regex over the serialized tool input."),
|
|
192
|
+
files: z.array(z.string().min(1)).optional().describe("Globs against the paths the tool touches; pretool triggers only."),
|
|
193
|
+
when: WhenSchema.optional().describe(
|
|
194
|
+
"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."
|
|
195
|
+
),
|
|
196
|
+
text: z.string().optional().describe("Required for inject and warn \u2014 the line emitted."),
|
|
197
|
+
reason: z.string().optional().describe("Required for block \u2014 the denial the agent reads."),
|
|
198
|
+
dedup: DedupSchema.optional().describe(
|
|
199
|
+
"Re-fire policy: session (default, once per session), never, or {minutes: N} for at most once per bucket. Rejected on block triggers \u2014 blocks are dedup-exempt."
|
|
200
|
+
)
|
|
187
201
|
}).superRefine((trigger, ctx) => {
|
|
188
202
|
if (trigger.on === "prompt" && !trigger.keywords?.length && !trigger.intent?.length) {
|
|
189
203
|
ctx.addIssue({
|
|
@@ -203,6 +217,12 @@ var init_config = __esm({
|
|
|
203
217
|
message: `trigger "${trigger.id}": files applies to pretool triggers only`
|
|
204
218
|
});
|
|
205
219
|
}
|
|
220
|
+
if (trigger.enforce === "block" && trigger.dedup !== void 0) {
|
|
221
|
+
ctx.addIssue({
|
|
222
|
+
code: "custom",
|
|
223
|
+
message: `block trigger "${trigger.id}" may not set dedup \u2014 blocks fire on every matching event`
|
|
224
|
+
});
|
|
225
|
+
}
|
|
206
226
|
if (trigger.enforce === "block" ? trigger.reason === void 0 : trigger.text === void 0) {
|
|
207
227
|
ctx.addIssue({
|
|
208
228
|
code: "custom",
|
|
@@ -221,22 +241,28 @@ var init_config = __esm({
|
|
|
221
241
|
}
|
|
222
242
|
});
|
|
223
243
|
TriggersSchema = z.record(z.string(), z.array(TriggerSchema));
|
|
224
|
-
VaultConfigSchema = z.
|
|
225
|
-
scopes: z.record(z.string(), ScopeSchema),
|
|
226
|
-
kinds: z.array(KindEntrySchema)
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
244
|
+
VaultConfigSchema = z.strictObject({
|
|
245
|
+
scopes: z.record(z.string(), ScopeSchema).describe("Scope name \u2192 entry; key = directory name under projects/."),
|
|
246
|
+
kinds: z.array(KindEntrySchema).describe(
|
|
247
|
+
"Page kind vocabulary; validate-enforced. Object form adds a kind-selector row to wiki://authoring."
|
|
248
|
+
),
|
|
249
|
+
statuses: z.array(z.string()).describe("Page status vocabulary; validate-enforced."),
|
|
250
|
+
methodologies: z.array(z.string()).describe("Methodology vocabulary for pages and scope entries."),
|
|
251
|
+
tags: z.strictObject({
|
|
252
|
+
canonical: z.array(z.string()).describe("Approved tags."),
|
|
253
|
+
aliases: z.record(z.string(), z.string()).describe("Alias \u2192 canonical; validate warns on alias use.")
|
|
232
254
|
}),
|
|
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
|
-
|
|
239
|
-
|
|
255
|
+
authoring_rules: z.string().optional().describe("Replaces the served \xA7 Authoring rules entirely \u2014 escape hatch."),
|
|
256
|
+
authoring_rules_extra: z.string().optional().describe("Appended after the served \xA7 Authoring rules."),
|
|
257
|
+
sync_protocol: z.string().optional().describe("Replaces the served \xA7 Resync protocol entirely \u2014 escape hatch."),
|
|
258
|
+
sync_protocol_extra: z.string().optional().describe("Appended after the served \xA7 Resync protocol."),
|
|
259
|
+
triggers: TriggersSchema.optional().describe(
|
|
260
|
+
'Full-replace of the trigger base per scope \u2014 escape hatch. "_all" is reserved for triggers_extra.'
|
|
261
|
+
),
|
|
262
|
+
triggers_extra: TriggersSchema.optional().describe(
|
|
263
|
+
'Appended per scope after the engine defaults; the reserved "_all" key fires in every session.'
|
|
264
|
+
)
|
|
265
|
+
}).describe("kmd vault.yaml \u2014 controlled vocabulary and gate triggers.").superRefine((config, ctx) => {
|
|
240
266
|
for (const [name, scope] of Object.entries(config.scopes)) {
|
|
241
267
|
if (scope.methodology !== void 0 && !config.methodologies.includes(scope.methodology)) {
|
|
242
268
|
ctx.addIssue({
|
|
@@ -310,11 +336,287 @@ var init_frontmatter = __esm({
|
|
|
310
336
|
}
|
|
311
337
|
});
|
|
312
338
|
|
|
339
|
+
// ../cli/src/init-templates.ts
|
|
340
|
+
var VAULT_TEMPLATES;
|
|
341
|
+
var init_init_templates = __esm({
|
|
342
|
+
"../cli/src/init-templates.ts"() {
|
|
343
|
+
"use strict";
|
|
344
|
+
VAULT_TEMPLATES = {
|
|
345
|
+
"note.md": '---\ntitle: {{title}}\ntags: []\ncreated: "{{date}}"\nupdated: {{date}}\n---\n\n# {{title}}\n',
|
|
346
|
+
"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',
|
|
347
|
+
"project-index.md": `---
|
|
348
|
+
title: {{title}}
|
|
349
|
+
kind: project
|
|
350
|
+
methodology: sdd
|
|
351
|
+
phase: 1
|
|
352
|
+
repo:
|
|
353
|
+
scope:
|
|
354
|
+
status: active
|
|
355
|
+
tags: []
|
|
356
|
+
summary:
|
|
357
|
+
created: "{{date}}"
|
|
358
|
+
updated: {{date}}
|
|
359
|
+
---
|
|
360
|
+
|
|
361
|
+
# {{title}}
|
|
362
|
+
|
|
363
|
+
## Summary
|
|
364
|
+
|
|
365
|
+
One paragraph. What this project is and what it delivers.
|
|
366
|
+
|
|
367
|
+
## Current Phase
|
|
368
|
+
|
|
369
|
+
What's in flight right now. 1\u20133 sentences.
|
|
370
|
+
|
|
371
|
+
## Links
|
|
372
|
+
|
|
373
|
+
- [[projects/{{title}}/primer]]
|
|
374
|
+
|
|
375
|
+
## Sources
|
|
376
|
+
|
|
377
|
+
-
|
|
378
|
+
`,
|
|
379
|
+
"project-ops.md": `---
|
|
380
|
+
title: {{title}}
|
|
381
|
+
kind: ops
|
|
382
|
+
scope:
|
|
383
|
+
status: active
|
|
384
|
+
summary:
|
|
385
|
+
tags: []
|
|
386
|
+
sources: []
|
|
387
|
+
created: "{{date}}"
|
|
388
|
+
updated: {{date}}
|
|
389
|
+
---
|
|
390
|
+
|
|
391
|
+
# {{title}}
|
|
392
|
+
|
|
393
|
+
## Summary
|
|
394
|
+
|
|
395
|
+
What this runbook operates and when it runs.
|
|
396
|
+
|
|
397
|
+
## Context
|
|
398
|
+
|
|
399
|
+
What triggers this flow; what breaks when it's skipped.
|
|
400
|
+
|
|
401
|
+
## Details
|
|
402
|
+
|
|
403
|
+
Numbered steps executable without asking: exact commands, expected
|
|
404
|
+
output, failure modes and their recovery. Predicates only \u2014 if a step
|
|
405
|
+
needs judgment, say whose.
|
|
406
|
+
|
|
407
|
+
## Links
|
|
408
|
+
|
|
409
|
+
-
|
|
410
|
+
|
|
411
|
+
## Sources
|
|
412
|
+
|
|
413
|
+
-
|
|
414
|
+
`,
|
|
415
|
+
"project-plan.md": `---
|
|
416
|
+
title: {{title}}
|
|
417
|
+
kind: plan
|
|
418
|
+
scope:
|
|
419
|
+
status: active
|
|
420
|
+
summary:
|
|
421
|
+
tags: []
|
|
422
|
+
created: "{{date}}"
|
|
423
|
+
updated: {{date}}
|
|
424
|
+
---
|
|
425
|
+
|
|
426
|
+
# {{title}}
|
|
427
|
+
|
|
428
|
+
## Goal
|
|
429
|
+
|
|
430
|
+
One sentence: what this phase delivers. If it needs two, it's two
|
|
431
|
+
plans.
|
|
432
|
+
|
|
433
|
+
## Scope
|
|
434
|
+
|
|
435
|
+
What's in and \u2014 more important \u2014 what's out. Out-of-scope lines stop
|
|
436
|
+
scope creep better than in-scope lines define it.
|
|
437
|
+
|
|
438
|
+
## Milestones
|
|
439
|
+
|
|
440
|
+
Checkable outcomes, not activities. Tick here; don't cascade ticks to
|
|
441
|
+
index.md unless phase or status changed.
|
|
442
|
+
|
|
443
|
+
1.
|
|
444
|
+
2.
|
|
445
|
+
|
|
446
|
+
## Dependencies
|
|
447
|
+
|
|
448
|
+
-
|
|
449
|
+
|
|
450
|
+
## Status Log
|
|
451
|
+
|
|
452
|
+
Dated one-liners, newest first. This is the only history surface \u2014
|
|
453
|
+
primer and index stay clean.
|
|
454
|
+
|
|
455
|
+
- {{date}}: Phase started.
|
|
456
|
+
`,
|
|
457
|
+
"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",
|
|
458
|
+
"project-spec.md": `---
|
|
459
|
+
title: {{title}}
|
|
460
|
+
kind: spec
|
|
461
|
+
scope:
|
|
462
|
+
status: active
|
|
463
|
+
summary:
|
|
464
|
+
tags: []
|
|
465
|
+
sources: []
|
|
466
|
+
created: "{{date}}"
|
|
467
|
+
updated: {{date}}
|
|
468
|
+
---
|
|
469
|
+
|
|
470
|
+
# {{title}}
|
|
471
|
+
|
|
472
|
+
## Summary
|
|
473
|
+
|
|
474
|
+
One paragraph, in predicates \u2014 the system's current shape, not its
|
|
475
|
+
history. If you're recording a *choice* between alternatives, stop:
|
|
476
|
+
that's an ADR.
|
|
477
|
+
|
|
478
|
+
## Context
|
|
479
|
+
|
|
480
|
+
The question this spec answers and who asks it. One paragraph, no
|
|
481
|
+
narrative arc.
|
|
482
|
+
|
|
483
|
+
## Details
|
|
484
|
+
|
|
485
|
+
The actual contract: inputs, outputs, invariants, failure modes. Use
|
|
486
|
+
subsections per concern. Present tense \u2014 the spec describes what IS
|
|
487
|
+
and must match current code at every commit.
|
|
488
|
+
|
|
489
|
+
## Links
|
|
490
|
+
|
|
491
|
+
-
|
|
492
|
+
|
|
493
|
+
## Sources
|
|
494
|
+
|
|
495
|
+
-
|
|
496
|
+
`,
|
|
497
|
+
"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',
|
|
498
|
+
"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',
|
|
499
|
+
"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',
|
|
500
|
+
"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'
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
// ../cli/src/init.ts
|
|
506
|
+
import { mkdir, readdir, readFile as readFile2, writeFile } from "node:fs/promises";
|
|
507
|
+
import { join as join3, resolve as resolve2 } from "node:path";
|
|
508
|
+
import { stringify } from "yaml";
|
|
509
|
+
async function refreshSchemaFile(root) {
|
|
510
|
+
const path = join3(root, SCHEMA_FILE);
|
|
511
|
+
const next = `${JSON.stringify(configJsonSchema(), null, 2)}
|
|
512
|
+
`;
|
|
513
|
+
try {
|
|
514
|
+
if (await readFile2(path, "utf8") === next) return false;
|
|
515
|
+
} catch {
|
|
516
|
+
}
|
|
517
|
+
await writeFile(path, next);
|
|
518
|
+
return true;
|
|
519
|
+
}
|
|
520
|
+
async function scaffoldVault(dir) {
|
|
521
|
+
const root = resolve2(dir);
|
|
522
|
+
let entries = [];
|
|
523
|
+
try {
|
|
524
|
+
entries = await readdir(root);
|
|
525
|
+
} catch (err) {
|
|
526
|
+
if (err.code !== "ENOENT") throw err;
|
|
527
|
+
}
|
|
528
|
+
if (entries.includes("vault.yaml")) {
|
|
529
|
+
throw new Error(`already a vault: ${root} (vault.yaml exists)`);
|
|
530
|
+
}
|
|
531
|
+
if (entries.length > 0) {
|
|
532
|
+
throw new Error(
|
|
533
|
+
`target is not empty: ${root}
|
|
534
|
+
found: ${entries.join(", ")}
|
|
535
|
+
delete it or pick another directory`
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
for (const domain of DOMAIN_DIRS) {
|
|
539
|
+
await mkdir(join3(root, domain), { recursive: true });
|
|
540
|
+
}
|
|
541
|
+
await mkdir(join3(root, "templates"), { recursive: true });
|
|
542
|
+
for (const [file, content] of Object.entries(VAULT_TEMPLATES)) {
|
|
543
|
+
await writeFile(join3(root, "templates", file), content);
|
|
544
|
+
}
|
|
545
|
+
await refreshSchemaFile(root);
|
|
546
|
+
await writeFile(join3(root, "vault.yaml"), SCHEMA_MODELINE + stringify(STARTER_CONFIG));
|
|
547
|
+
return root;
|
|
548
|
+
}
|
|
549
|
+
async function promptYesNo(question, input = process.stdin, output = process.stderr) {
|
|
550
|
+
const { createInterface } = await import("node:readline/promises");
|
|
551
|
+
const rl = createInterface({ input, output });
|
|
552
|
+
try {
|
|
553
|
+
const answer = await rl.question(question);
|
|
554
|
+
return /^y(es)?$/i.test(answer.trim());
|
|
555
|
+
} finally {
|
|
556
|
+
rl.close();
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
async function runInit(dir, yes = false) {
|
|
560
|
+
let target = dir;
|
|
561
|
+
if (!target) {
|
|
562
|
+
if (yes) {
|
|
563
|
+
target = ".";
|
|
564
|
+
} else if (process.stdin.isTTY) {
|
|
565
|
+
const ok = await promptYesNo(`initialize a vault in ${resolve2(".")}? [y/N] `);
|
|
566
|
+
if (!ok) {
|
|
567
|
+
console.error("init: aborted");
|
|
568
|
+
process.exit(1);
|
|
569
|
+
}
|
|
570
|
+
target = ".";
|
|
571
|
+
} else {
|
|
572
|
+
console.error("usage: kmd init <dir> (or --yes to scaffold the current directory)");
|
|
573
|
+
process.exit(2);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
let root;
|
|
577
|
+
try {
|
|
578
|
+
root = await scaffoldVault(target);
|
|
579
|
+
} catch (err) {
|
|
580
|
+
console.error(`init: ${err instanceof Error ? err.message : err}`);
|
|
581
|
+
process.exit(1);
|
|
582
|
+
}
|
|
583
|
+
const templateCount = Object.keys(VAULT_TEMPLATES).length;
|
|
584
|
+
console.log(`initialized empty vault at ${root}
|
|
585
|
+
|
|
586
|
+
vault.yaml starter vocabulary \u2014 add your first scope under scopes:
|
|
587
|
+
vault.schema.json IDE validation via the yaml-language-server modeline
|
|
588
|
+
templates/ ${templateCount} built-in templates (served at wiki://template/...)
|
|
589
|
+
projects/ research/ notes/
|
|
590
|
+
|
|
591
|
+
next steps:
|
|
592
|
+
export WIKI_VAULT=${root}
|
|
593
|
+
kmd mcp ${root} # stdio MCP server (prime, search)`);
|
|
594
|
+
}
|
|
595
|
+
var SCHEMA_FILE, SCHEMA_MODELINE, STARTER_CONFIG, DOMAIN_DIRS;
|
|
596
|
+
var init_init = __esm({
|
|
597
|
+
"../cli/src/init.ts"() {
|
|
598
|
+
"use strict";
|
|
599
|
+
init_vault_config();
|
|
600
|
+
init_init_templates();
|
|
601
|
+
SCHEMA_FILE = "vault.schema.json";
|
|
602
|
+
SCHEMA_MODELINE = `# yaml-language-server: $schema=./${SCHEMA_FILE}
|
|
603
|
+
`;
|
|
604
|
+
STARTER_CONFIG = {
|
|
605
|
+
scopes: {},
|
|
606
|
+
kinds: ["project", "spec", "adr", "plan", "story", "ops", "topic", "article", "src", "note"],
|
|
607
|
+
statuses: ["draft", "active", "superseded", "archived"],
|
|
608
|
+
methodologies: ["sdd", "tdd", "hybrid"],
|
|
609
|
+
tags: { canonical: [], aliases: {} }
|
|
610
|
+
};
|
|
611
|
+
DOMAIN_DIRS = ["projects", "research", "notes"];
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
|
|
313
615
|
// ../cli/src/sync.ts
|
|
314
616
|
import { createHash as createHash2 } from "node:crypto";
|
|
315
617
|
import { mkdirSync } from "node:fs";
|
|
316
|
-
import { readdir, readFile as
|
|
317
|
-
import { dirname, join as
|
|
618
|
+
import { readdir as readdir2, readFile as readFile3 } from "node:fs/promises";
|
|
619
|
+
import { dirname, join as join4, relative, sep } from "node:path";
|
|
318
620
|
import { z as z2 } from "zod";
|
|
319
621
|
function loadEnv() {
|
|
320
622
|
const parsed = EnvSchema.safeParse({
|
|
@@ -330,18 +632,18 @@ function loadEnv() {
|
|
|
330
632
|
async function walkMarkdown(root, domain) {
|
|
331
633
|
const out = [];
|
|
332
634
|
async function recurse(dir) {
|
|
333
|
-
const entries = await
|
|
635
|
+
const entries = await readdir2(dir, { withFileTypes: true }).catch(() => null);
|
|
334
636
|
if (!entries) return;
|
|
335
637
|
for (const entry of entries) {
|
|
336
638
|
if (entry.name.startsWith(".")) continue;
|
|
337
639
|
if (entry.isDirectory()) {
|
|
338
|
-
await recurse(
|
|
640
|
+
await recurse(join4(dir, entry.name));
|
|
339
641
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
340
|
-
out.push(
|
|
642
|
+
out.push(join4(dir, entry.name));
|
|
341
643
|
}
|
|
342
644
|
}
|
|
343
645
|
}
|
|
344
|
-
await recurse(
|
|
646
|
+
await recurse(join4(root, domain));
|
|
345
647
|
return out;
|
|
346
648
|
}
|
|
347
649
|
function toRelativePath(root, absolute) {
|
|
@@ -480,7 +782,7 @@ async function syncVault(vaultRoot2) {
|
|
|
480
782
|
let skipped = 0;
|
|
481
783
|
for (const file of files) {
|
|
482
784
|
const path = toRelativePath(vaultRoot2, file);
|
|
483
|
-
const raw = await
|
|
785
|
+
const raw = await readFile3(file, "utf8");
|
|
484
786
|
const parsed = parseFrontmatter(raw);
|
|
485
787
|
const fields = buildPageFields(path, raw, parsed, scopes);
|
|
486
788
|
if (!fields) {
|
|
@@ -507,6 +809,9 @@ async function syncVault(vaultRoot2) {
|
|
|
507
809
|
db.exec("INSERT INTO pages_fts(pages_fts) VALUES('rebuild')");
|
|
508
810
|
setMeta(db, "vault_root", canonicalVaultRoot(vaultRoot2));
|
|
509
811
|
setMeta(db, "last_synced", (/* @__PURE__ */ new Date()).toISOString());
|
|
812
|
+
if (await refreshSchemaFile(vaultRoot2)) {
|
|
813
|
+
console.error("sync: vault.schema.json refreshed to the running engine");
|
|
814
|
+
}
|
|
510
815
|
return {
|
|
511
816
|
changed,
|
|
512
817
|
unchanged,
|
|
@@ -535,8 +840,9 @@ var init_sync = __esm({
|
|
|
535
840
|
"../cli/src/sync.ts"() {
|
|
536
841
|
"use strict";
|
|
537
842
|
init_database();
|
|
538
|
-
|
|
843
|
+
init_vault_config();
|
|
539
844
|
init_frontmatter();
|
|
845
|
+
init_init();
|
|
540
846
|
EnvSchema = z2.object({
|
|
541
847
|
WIKI_VAULT: z2.string().min(1)
|
|
542
848
|
});
|
|
@@ -556,8 +862,8 @@ var init_sync = __esm({
|
|
|
556
862
|
});
|
|
557
863
|
|
|
558
864
|
// ../cli/src/validate.ts
|
|
559
|
-
import { readFile as
|
|
560
|
-
import { join as
|
|
865
|
+
import { readFile as readFile4, stat } from "node:fs/promises";
|
|
866
|
+
import { join as join5 } from "node:path";
|
|
561
867
|
function hasIndexableTitle(data) {
|
|
562
868
|
return typeof data.title === "string" && data.title.trim() !== "";
|
|
563
869
|
}
|
|
@@ -915,7 +1221,7 @@ async function validateVault(root) {
|
|
|
915
1221
|
const pages = [];
|
|
916
1222
|
const linkPages = [];
|
|
917
1223
|
for (const { relPath, abs } of files) {
|
|
918
|
-
const raw = await
|
|
1224
|
+
const raw = await readFile4(abs, "utf8");
|
|
919
1225
|
findings.push(...validatePage(relPath, raw, cfg, refIndex));
|
|
920
1226
|
try {
|
|
921
1227
|
const parsed = parseFrontmatter(raw);
|
|
@@ -929,7 +1235,7 @@ async function validateVault(root) {
|
|
|
929
1235
|
for (const name of customKindNames(cfg)) {
|
|
930
1236
|
const file = `templates/${name}.md`;
|
|
931
1237
|
try {
|
|
932
|
-
await stat(
|
|
1238
|
+
await stat(join5(root, file));
|
|
933
1239
|
} catch {
|
|
934
1240
|
findings.push({
|
|
935
1241
|
path: file,
|
|
@@ -945,7 +1251,7 @@ var REQUIRED_FIELDS, TAG_OPTIONAL_KINDS, FOLDER_PATTERNS, UNIVERSAL_FLOOR;
|
|
|
945
1251
|
var init_validate = __esm({
|
|
946
1252
|
"../cli/src/validate.ts"() {
|
|
947
1253
|
"use strict";
|
|
948
|
-
|
|
1254
|
+
init_vault_config();
|
|
949
1255
|
init_frontmatter();
|
|
950
1256
|
init_sync();
|
|
951
1257
|
REQUIRED_FIELDS = {
|
|
@@ -1003,12 +1309,13 @@ __export(cli_exports, {
|
|
|
1003
1309
|
resolveCli: () => resolveCli,
|
|
1004
1310
|
runConfig: () => runConfig,
|
|
1005
1311
|
runDbReset: () => runDbReset,
|
|
1312
|
+
runInit: () => runInit,
|
|
1006
1313
|
runSyncCommand: () => runSyncCommand,
|
|
1007
1314
|
runValidate: () => runValidate,
|
|
1008
1315
|
vaultRoot: () => vaultRoot
|
|
1009
1316
|
});
|
|
1010
1317
|
import { existsSync, readdirSync, rmSync } from "node:fs";
|
|
1011
|
-
import { dirname as dirname2, join as
|
|
1318
|
+
import { dirname as dirname2, join as join6 } from "node:path";
|
|
1012
1319
|
import { parseArgs } from "node:util";
|
|
1013
1320
|
function resolveCli(argv) {
|
|
1014
1321
|
const { positionals: positionals2 } = parseArgs({ args: argv, allowPositionals: true, strict: false });
|
|
@@ -1078,7 +1385,7 @@ function knownVaults() {
|
|
|
1078
1385
|
const known = [];
|
|
1079
1386
|
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
1080
1387
|
if (!entry.isDirectory()) continue;
|
|
1081
|
-
const index =
|
|
1388
|
+
const index = join6(root, entry.name, "index.db");
|
|
1082
1389
|
if (!existsSync(index)) continue;
|
|
1083
1390
|
const db = openDatabase(index);
|
|
1084
1391
|
try {
|
|
@@ -1141,6 +1448,7 @@ var init_cli = __esm({
|
|
|
1141
1448
|
init_database();
|
|
1142
1449
|
init_sync();
|
|
1143
1450
|
init_validate();
|
|
1451
|
+
init_init();
|
|
1144
1452
|
}
|
|
1145
1453
|
});
|
|
1146
1454
|
|
|
@@ -1161,7 +1469,7 @@ ${issues}`);
|
|
|
1161
1469
|
};
|
|
1162
1470
|
}
|
|
1163
1471
|
var EnvSchema2;
|
|
1164
|
-
var
|
|
1472
|
+
var init_config = __esm({
|
|
1165
1473
|
"../mcp/src/config.ts"() {
|
|
1166
1474
|
"use strict";
|
|
1167
1475
|
EnvSchema2 = z3.object({
|
|
@@ -1193,7 +1501,7 @@ var init_db = __esm({
|
|
|
1193
1501
|
// ../mcp/src/lib/diag.ts
|
|
1194
1502
|
import { appendFileSync, mkdirSync as mkdirSync3 } from "node:fs";
|
|
1195
1503
|
import { homedir as homedir2 } from "node:os";
|
|
1196
|
-
import { join as
|
|
1504
|
+
import { join as join7 } from "node:path";
|
|
1197
1505
|
function diag(msg, data) {
|
|
1198
1506
|
try {
|
|
1199
1507
|
const line = data ? `${(/* @__PURE__ */ new Date()).toISOString()} pid=${process.pid} ${msg} ${JSON.stringify(data)}
|
|
@@ -1207,8 +1515,8 @@ var DIAG_DIR, DIAG_LOG_PATH;
|
|
|
1207
1515
|
var init_diag = __esm({
|
|
1208
1516
|
"../mcp/src/lib/diag.ts"() {
|
|
1209
1517
|
"use strict";
|
|
1210
|
-
DIAG_DIR =
|
|
1211
|
-
DIAG_LOG_PATH =
|
|
1518
|
+
DIAG_DIR = join7(homedir2(), ".local", "state", "wiki-mcp");
|
|
1519
|
+
DIAG_LOG_PATH = join7(DIAG_DIR, "server.log");
|
|
1212
1520
|
try {
|
|
1213
1521
|
mkdirSync3(DIAG_DIR, { recursive: true });
|
|
1214
1522
|
} catch {
|
|
@@ -1235,176 +1543,6 @@ var init_logger = __esm({
|
|
|
1235
1543
|
}
|
|
1236
1544
|
});
|
|
1237
1545
|
|
|
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
1546
|
// ../mcp/src/resources/authoring.ts
|
|
1409
1547
|
function buildAuthoringRules(config) {
|
|
1410
1548
|
const parts = [(config.authoring_rules ?? DEFAULT_AUTHORING_RULES).trim()];
|
|
@@ -1419,13 +1557,13 @@ function buildSyncProtocol(config) {
|
|
|
1419
1557
|
function buildKindSelector(kinds) {
|
|
1420
1558
|
const lines = ["## Kind selector", "", "| Signal | Kind | Where |", "|---|---|---|"];
|
|
1421
1559
|
for (const entry of kinds) {
|
|
1422
|
-
const name =
|
|
1560
|
+
const name = kindName(entry);
|
|
1423
1561
|
const pedagogy = typeof entry === "string" ? KIND_PEDAGOGY.get(entry) : entry;
|
|
1424
1562
|
const signal = pedagogy?.signal ?? "\u2014";
|
|
1425
1563
|
const where = pedagogy?.where ?? "\u2014";
|
|
1426
1564
|
lines.push(`| ${signal} | **${name}** | ${where} |`);
|
|
1427
1565
|
}
|
|
1428
|
-
const names = kinds.map(
|
|
1566
|
+
const names = kinds.map(kindName);
|
|
1429
1567
|
const hasNote = names.includes("note");
|
|
1430
1568
|
const hasAdrAndSpec = names.includes("adr") && names.includes("spec");
|
|
1431
1569
|
if (hasNote || hasAdrAndSpec) {
|
|
@@ -1448,7 +1586,7 @@ function buildVocabulary(config) {
|
|
|
1448
1586
|
const lines = [
|
|
1449
1587
|
"## Controlled vocabulary",
|
|
1450
1588
|
"",
|
|
1451
|
-
`**Kinds:** ${config.kinds.map(
|
|
1589
|
+
`**Kinds:** ${config.kinds.map(kindName).join(", ")}`,
|
|
1452
1590
|
buildStatusLine(config.statuses),
|
|
1453
1591
|
`**Methodologies:** ${config.methodologies.join(", ")}`,
|
|
1454
1592
|
`**Canonical tags:** ${config.tags.canonical.join(", ")}`
|
|
@@ -1612,7 +1750,7 @@ var init_authoring = __esm({
|
|
|
1612
1750
|
].join("\n");
|
|
1613
1751
|
DEFAULT_SYNC_PROTOCOL = [
|
|
1614
1752
|
"Edit the smallest set of files that reflects the change. A milestone tick is plan-only; don't cascade to index.md unless phase or status changed. Controlled-vocabulary edits (`vault.yaml`) need explicit user approval.",
|
|
1615
|
-
"
|
|
1753
|
+
"Harnesses with the posttool hook validate and sync automatically on every vault write. Check `kmd config`: if the `synced` line did not advance past your edits, the hook is not wired \u2014 run `kmd validate`, fix findings, then `kmd sync`."
|
|
1616
1754
|
].join("\n");
|
|
1617
1755
|
CANONICAL_STATUS_FLOW = ["draft", "active", "superseded", "archived"];
|
|
1618
1756
|
}
|
|
@@ -1624,7 +1762,7 @@ import { join as join8 } from "node:path";
|
|
|
1624
1762
|
function customTemplates(config) {
|
|
1625
1763
|
const specs = [];
|
|
1626
1764
|
for (const entry of config.kinds) {
|
|
1627
|
-
if (typeof entry === "string" ||
|
|
1765
|
+
if (typeof entry === "string" || BUILT_IN_KINDS.has(entry.name)) continue;
|
|
1628
1766
|
specs.push({
|
|
1629
1767
|
uri: `wiki://template/${entry.name}`,
|
|
1630
1768
|
name: entry.name.charAt(0).toUpperCase() + entry.name.slice(1),
|
|
@@ -1825,7 +1963,7 @@ var init_toolResponse = __esm({
|
|
|
1825
1963
|
});
|
|
1826
1964
|
|
|
1827
1965
|
// ../mcp/src/tools/search.ts
|
|
1828
|
-
import { z as
|
|
1966
|
+
import { z as z4 } from "zod";
|
|
1829
1967
|
function search(deps, input) {
|
|
1830
1968
|
const ftsQuery = sanitizeFtsQuery(input.query);
|
|
1831
1969
|
if (!ftsQuery) return { results: [] };
|
|
@@ -1872,15 +2010,15 @@ var init_search = __esm({
|
|
|
1872
2010
|
"use strict";
|
|
1873
2011
|
init_fts();
|
|
1874
2012
|
init_toolResponse();
|
|
1875
|
-
SearchInputSchema =
|
|
1876
|
-
query:
|
|
2013
|
+
SearchInputSchema = z4.object({
|
|
2014
|
+
query: z4.string().min(1).describe(
|
|
1877
2015
|
"Natural-language search query. Matched against title, summary, and body via SQLite FTS5."
|
|
1878
2016
|
),
|
|
1879
|
-
scope:
|
|
1880
|
-
kind:
|
|
2017
|
+
scope: z4.string().optional().describe('Optional: restrict to a project scope (e.g. "ontology", "sotto").'),
|
|
2018
|
+
kind: z4.string().optional().describe(
|
|
1881
2019
|
"Optional: restrict to a kind. Project kinds: spec, adr, plan, ops. Research kinds: article, src. Misc: note."
|
|
1882
2020
|
),
|
|
1883
|
-
limit:
|
|
2021
|
+
limit: z4.number().int().min(1).max(50).default(5).describe("Maximum number of ranked results to return. Default 5.")
|
|
1884
2022
|
});
|
|
1885
2023
|
FTS_RANK = "bm25(pages_fts, 10.0, 5.0, 1.0)";
|
|
1886
2024
|
}
|
|
@@ -1889,7 +2027,7 @@ var init_search = __esm({
|
|
|
1889
2027
|
// ../mcp/src/tools/prime.ts
|
|
1890
2028
|
import { readFile as readFile6 } from "node:fs/promises";
|
|
1891
2029
|
import { basename as basename3, join as join9 } from "node:path";
|
|
1892
|
-
import { z as
|
|
2030
|
+
import { z as z5 } from "zod";
|
|
1893
2031
|
function pathSlug(p) {
|
|
1894
2032
|
return basename3(p).replace(/\.md$/, "");
|
|
1895
2033
|
}
|
|
@@ -2032,7 +2170,7 @@ function renderMarkdown(d, config, task) {
|
|
|
2032
2170
|
lines.push(countEntries.map(([k, n]) => `${k}: ${n}`).join(" | "));
|
|
2033
2171
|
}
|
|
2034
2172
|
lines.push("", "## Vocabulary");
|
|
2035
|
-
lines.push(`kinds: ${config.kinds.map(
|
|
2173
|
+
lines.push(`kinds: ${config.kinds.map(kindName).join(", ")}`);
|
|
2036
2174
|
lines.push(`statuses: ${config.statuses.join(", ")}`);
|
|
2037
2175
|
lines.push(`tags: ${config.tags.canonical.join(", ")}`);
|
|
2038
2176
|
if (d.top_tags.length > 0) {
|
|
@@ -2093,14 +2231,14 @@ var init_prime = __esm({
|
|
|
2093
2231
|
"../mcp/src/tools/prime.ts"() {
|
|
2094
2232
|
"use strict";
|
|
2095
2233
|
init_database();
|
|
2234
|
+
init_vault_config();
|
|
2096
2235
|
init_frontmatter2();
|
|
2097
2236
|
init_fts();
|
|
2098
2237
|
init_toolResponse();
|
|
2099
|
-
init_vault_config();
|
|
2100
2238
|
init_search();
|
|
2101
|
-
PrimeInputSchema =
|
|
2102
|
-
scope:
|
|
2103
|
-
task:
|
|
2239
|
+
PrimeInputSchema = z5.object({
|
|
2240
|
+
scope: z5.string().min(1).describe("Project scope to prime (matches projects/{scope}/ in the vault)."),
|
|
2241
|
+
task: z5.string().optional().describe("Optional task description; surfaces top-3 tsvector-ranked relevant pages.")
|
|
2104
2242
|
});
|
|
2105
2243
|
}
|
|
2106
2244
|
});
|
|
@@ -2152,7 +2290,7 @@ async function startMcpServer() {
|
|
|
2152
2290
|
diag("main entered");
|
|
2153
2291
|
const config = loadConfig();
|
|
2154
2292
|
diag("config loaded", { vault: config.wikiVault, level: config.logLevel });
|
|
2155
|
-
const vaultConfig = await
|
|
2293
|
+
const vaultConfig = await loadVaultConfig(config.wikiVault);
|
|
2156
2294
|
diag("vault config loaded", {
|
|
2157
2295
|
scopes: Object.keys(vaultConfig.scopes).length,
|
|
2158
2296
|
kinds: vaultConfig.kinds.length,
|
|
@@ -2195,12 +2333,12 @@ async function startMcpServer() {
|
|
|
2195
2333
|
var init_start = __esm({
|
|
2196
2334
|
"../mcp/src/start.ts"() {
|
|
2197
2335
|
"use strict";
|
|
2198
|
-
|
|
2336
|
+
init_vault_config();
|
|
2337
|
+
init_config();
|
|
2199
2338
|
init_db();
|
|
2200
2339
|
init_diag();
|
|
2201
2340
|
init_logger();
|
|
2202
2341
|
init_server();
|
|
2203
|
-
init_vault_config();
|
|
2204
2342
|
}
|
|
2205
2343
|
});
|
|
2206
2344
|
|
|
@@ -2218,21 +2356,24 @@ __export(hook_exports, {
|
|
|
2218
2356
|
matchPromptTriggers: () => matchPromptTriggers,
|
|
2219
2357
|
parsePretoolEvent: () => parsePretoolEvent,
|
|
2220
2358
|
parsePromptEvent: () => parsePromptEvent,
|
|
2359
|
+
parseStopEvent: () => parseStopEvent,
|
|
2221
2360
|
renderPosttool: () => renderPosttool,
|
|
2222
2361
|
renderPretool: () => renderPretool,
|
|
2362
|
+
renderStop: () => renderStop,
|
|
2223
2363
|
resolveScope: () => resolveScope,
|
|
2224
2364
|
runHookPosttool: () => runHookPosttool,
|
|
2225
2365
|
runHookPretool: () => runHookPretool,
|
|
2226
2366
|
runHookPrompt: () => runHookPrompt,
|
|
2367
|
+
runHookStop: () => runHookStop,
|
|
2227
2368
|
vaultPathTouched: () => vaultPathTouched
|
|
2228
2369
|
});
|
|
2229
2370
|
import { mkdirSync as mkdirSync4, readdirSync as readdirSync2, readFileSync, rmSync as rmSync2, statSync, writeFileSync } from "node:fs";
|
|
2230
2371
|
import { homedir as homedir3 } from "node:os";
|
|
2231
|
-
import { join as join10, resolve as
|
|
2372
|
+
import { join as join10, resolve as resolve3, sep as sep2 } from "node:path";
|
|
2232
2373
|
import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
|
|
2233
2374
|
import { parseArgs as parseArgs2 } from "node:util";
|
|
2234
2375
|
import { parse as parseYaml3 } from "yaml";
|
|
2235
|
-
import { z as
|
|
2376
|
+
import { z as z6 } from "zod";
|
|
2236
2377
|
function eventFields(raw) {
|
|
2237
2378
|
let data;
|
|
2238
2379
|
try {
|
|
@@ -2258,7 +2399,7 @@ function kiroIdePromptEvent(now = Date.now()) {
|
|
|
2258
2399
|
}
|
|
2259
2400
|
function loadTriggerFile(path) {
|
|
2260
2401
|
try {
|
|
2261
|
-
const result =
|
|
2402
|
+
const result = z6.array(TriggerSchema).safeParse(parseYaml3(readFileSync(path, "utf8")));
|
|
2262
2403
|
return result.success ? result.data : null;
|
|
2263
2404
|
} catch {
|
|
2264
2405
|
return null;
|
|
@@ -2330,7 +2471,11 @@ function matchPromptTriggers(prompt, triggers) {
|
|
|
2330
2471
|
hit = trigger.intent.some((pattern) => new RegExp(pattern, "i").test(prompt));
|
|
2331
2472
|
}
|
|
2332
2473
|
if (hit) {
|
|
2333
|
-
matches.push({
|
|
2474
|
+
matches.push({
|
|
2475
|
+
id: trigger.id,
|
|
2476
|
+
text: trigger.text,
|
|
2477
|
+
...trigger.dedup !== void 0 && { dedup: trigger.dedup }
|
|
2478
|
+
});
|
|
2334
2479
|
}
|
|
2335
2480
|
}
|
|
2336
2481
|
} finally {
|
|
@@ -2409,7 +2554,8 @@ function matchPretoolTriggers(toolName, toolInput, triggers, cwd) {
|
|
|
2409
2554
|
id: trigger.id,
|
|
2410
2555
|
enforce: trigger.enforce,
|
|
2411
2556
|
text,
|
|
2412
|
-
...trigger.when !== void 0 && { when: trigger.when }
|
|
2557
|
+
...trigger.when !== void 0 && { when: trigger.when },
|
|
2558
|
+
...trigger.dedup !== void 0 && { dedup: trigger.dedup }
|
|
2413
2559
|
});
|
|
2414
2560
|
}
|
|
2415
2561
|
return matches;
|
|
@@ -2506,10 +2652,10 @@ function patchPaths(toolInput) {
|
|
|
2506
2652
|
return paths;
|
|
2507
2653
|
}
|
|
2508
2654
|
function vaultPathTouched(toolInput, vaultRoot2, cwd) {
|
|
2509
|
-
const root =
|
|
2655
|
+
const root = resolve3(vaultRoot2);
|
|
2510
2656
|
const candidates = [...pathCandidates(toolInput, cwd), ...patchPaths(toolInput)];
|
|
2511
2657
|
return candidates.some((candidate) => {
|
|
2512
|
-
const absolute =
|
|
2658
|
+
const absolute = resolve3(cwd ?? ".", candidate);
|
|
2513
2659
|
return absolute === root || absolute.startsWith(`${root}/`);
|
|
2514
2660
|
});
|
|
2515
2661
|
}
|
|
@@ -2532,6 +2678,27 @@ ${lines.join("\n")}`
|
|
|
2532
2678
|
}
|
|
2533
2679
|
return JSON.stringify({ findings, synced });
|
|
2534
2680
|
}
|
|
2681
|
+
function parseStopEvent(raw) {
|
|
2682
|
+
const fields = eventFields(raw);
|
|
2683
|
+
if (fields === null) return null;
|
|
2684
|
+
const { session_id, cwd, stop_hook_active } = fields;
|
|
2685
|
+
if (typeof session_id !== "string") return null;
|
|
2686
|
+
return {
|
|
2687
|
+
session_id,
|
|
2688
|
+
...typeof cwd === "string" && { cwd },
|
|
2689
|
+
...typeof stop_hook_active === "boolean" && { stop_hook_active }
|
|
2690
|
+
};
|
|
2691
|
+
}
|
|
2692
|
+
function renderStop(findings) {
|
|
2693
|
+
const errors = findings.filter((finding) => finding.severity === "error");
|
|
2694
|
+
if (errors.length === 0) return null;
|
|
2695
|
+
const lines = errors.map((f) => `${f.severity}: ${f.path} [${f.rule}] ${f.message}`);
|
|
2696
|
+
return JSON.stringify({
|
|
2697
|
+
decision: "block",
|
|
2698
|
+
reason: `kmd validate: ${errors.length} error(s) outstanding \u2014 the index sync is held. Fix them, let the posttool hook sync, then finish:
|
|
2699
|
+
${lines.join("\n")}`
|
|
2700
|
+
});
|
|
2701
|
+
}
|
|
2535
2702
|
function dedupePretoolMatches(stateDir, sessionId, matches) {
|
|
2536
2703
|
const blocks = matches.filter((match) => match.enforce === "block");
|
|
2537
2704
|
const rest = matches.filter((match) => match.enforce !== "block");
|
|
@@ -2541,15 +2708,26 @@ function dedupePretoolMatches(stateDir, sessionId, matches) {
|
|
|
2541
2708
|
function hookStateDir() {
|
|
2542
2709
|
return join10(kmdHome(), "state", "hook");
|
|
2543
2710
|
}
|
|
2544
|
-
function dedupeMatches(stateDir, sessionId, matches) {
|
|
2711
|
+
function dedupeMatches(stateDir, sessionId, matches, now = Date.now()) {
|
|
2545
2712
|
if (matches.length === 0) return [];
|
|
2546
2713
|
const file = join10(stateDir, `${sessionId.replace(/[^A-Za-z0-9._-]/g, "_")}.json`);
|
|
2547
2714
|
const fired = readFired(file);
|
|
2548
|
-
const fresh =
|
|
2549
|
-
|
|
2715
|
+
const fresh = [];
|
|
2716
|
+
const record = [];
|
|
2717
|
+
for (const match of matches) {
|
|
2718
|
+
if (match.dedup === "never") {
|
|
2719
|
+
fresh.push(match);
|
|
2720
|
+
continue;
|
|
2721
|
+
}
|
|
2722
|
+
const key = typeof match.dedup === "object" ? `${match.id}@${Math.floor(now / (match.dedup.minutes * 6e4))}` : match.id;
|
|
2723
|
+
if (fired.has(key)) continue;
|
|
2724
|
+
fresh.push(match);
|
|
2725
|
+
record.push(key);
|
|
2726
|
+
}
|
|
2727
|
+
if (record.length > 0) {
|
|
2550
2728
|
mkdirSync4(stateDir, { recursive: true });
|
|
2551
|
-
for (const
|
|
2552
|
-
fired.add(
|
|
2729
|
+
for (const key of record) {
|
|
2730
|
+
fired.add(key);
|
|
2553
2731
|
}
|
|
2554
2732
|
writeFileSync(file, JSON.stringify([...fired]));
|
|
2555
2733
|
pruneStale(stateDir, file);
|
|
@@ -2723,6 +2901,28 @@ async function runHookPosttool() {
|
|
|
2723
2901
|
diag2(err instanceof Error ? err.message : String(err));
|
|
2724
2902
|
}
|
|
2725
2903
|
}
|
|
2904
|
+
async function runHookStop() {
|
|
2905
|
+
try {
|
|
2906
|
+
const invocation = hookInvocation();
|
|
2907
|
+
if (invocation === null) return;
|
|
2908
|
+
const event = parseStopEvent(await readStdin());
|
|
2909
|
+
if (event === null) {
|
|
2910
|
+
diag2("stdin is not a stop event ({session_id})");
|
|
2911
|
+
return;
|
|
2912
|
+
}
|
|
2913
|
+
if (event.stop_hook_active === true) return;
|
|
2914
|
+
const config = await loadVaultConfig(invocation.vaultRoot);
|
|
2915
|
+
const scope = invocation.scope ?? resolveScope(config, event.cwd);
|
|
2916
|
+
if (scope === void 0) return;
|
|
2917
|
+
const rendered = renderStop(await validateVault(invocation.vaultRoot));
|
|
2918
|
+
if (rendered === null) return;
|
|
2919
|
+
const fired = dedupeMatches(hookStateDir(), event.session_id, [{ id: "stop-validate-gate" }]);
|
|
2920
|
+
if (fired.length === 0) return;
|
|
2921
|
+
console.log(rendered);
|
|
2922
|
+
} catch (err) {
|
|
2923
|
+
diag2(err instanceof Error ? err.message : String(err));
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2726
2926
|
async function readStdin() {
|
|
2727
2927
|
process.stdin.setEncoding("utf8");
|
|
2728
2928
|
let input = "";
|
|
@@ -2739,7 +2939,7 @@ var init_hook = __esm({
|
|
|
2739
2939
|
"../cli/src/hook.ts"() {
|
|
2740
2940
|
"use strict";
|
|
2741
2941
|
init_database();
|
|
2742
|
-
|
|
2942
|
+
init_vault_config();
|
|
2743
2943
|
init_frontmatter();
|
|
2744
2944
|
init_sync();
|
|
2745
2945
|
init_validate();
|
|
@@ -2762,14 +2962,16 @@ process.on("warning", (warning) => {
|
|
|
2762
2962
|
var USAGE = `usage: kmd <command> [options]
|
|
2763
2963
|
|
|
2764
2964
|
commands:
|
|
2965
|
+
init [<dir>] [-y] scaffold a fresh vault (no dir: current directory \u2014 TTY prompt, or -y)
|
|
2765
2966
|
sync vault \u2192 index sync (runs validate first)
|
|
2766
2967
|
validate [<path>] deterministic vault checker (default: $WIKI_VAULT)
|
|
2767
2968
|
mcp [<vault-root>] start the stdio MCP server (default: $WIKI_VAULT)
|
|
2768
2969
|
config [<vault-root>] print vault + index resolution; with no vault, list known vaults
|
|
2769
2970
|
db reset [<vault-root>] delete the vault's index (default: $WIKI_VAULT)
|
|
2770
|
-
hook <prompt|pretool|posttool> [<vault-root>] [--scope <s>] [--harness <claude|kiro-ide>] [--triggers <file>]
|
|
2971
|
+
hook <prompt|pretool|posttool|stop> [<vault-root>] [--scope <s>] [--harness <claude|kiro-ide>] [--triggers <file>]
|
|
2771
2972
|
harness gate engine: JSON event on stdin, decision/context on stdout;
|
|
2772
|
-
posttool auto-runs validate + sync after a vault write
|
|
2973
|
+
posttool auto-runs validate + sync after a vault write;
|
|
2974
|
+
stop blocks the handoff once while validate errors hold the sync
|
|
2773
2975
|
|
|
2774
2976
|
options:
|
|
2775
2977
|
--version print version
|
|
@@ -2780,7 +2982,8 @@ var { positionals, values } = parseArgs3({
|
|
|
2780
2982
|
strict: false,
|
|
2781
2983
|
options: {
|
|
2782
2984
|
version: { type: "boolean", short: "v" },
|
|
2783
|
-
help: { type: "boolean", short: "h" }
|
|
2985
|
+
help: { type: "boolean", short: "h" },
|
|
2986
|
+
yes: { type: "boolean", short: "y" }
|
|
2784
2987
|
}
|
|
2785
2988
|
});
|
|
2786
2989
|
var command = values.version ? "--version" : values.help ? "--help" : positionals[0];
|
|
@@ -2792,6 +2995,11 @@ function applyVaultRoot(positionalIndex) {
|
|
|
2792
2995
|
}
|
|
2793
2996
|
async function run() {
|
|
2794
2997
|
switch (command) {
|
|
2998
|
+
case "init": {
|
|
2999
|
+
const { runInit: runInit2 } = await Promise.resolve().then(() => (init_cli(), cli_exports));
|
|
3000
|
+
await runInit2(positionals[1], Boolean(values.yes));
|
|
3001
|
+
break;
|
|
3002
|
+
}
|
|
2795
3003
|
case "sync": {
|
|
2796
3004
|
const { runSyncCommand: runSyncCommand2 } = await Promise.resolve().then(() => (init_cli(), cli_exports));
|
|
2797
3005
|
await runSyncCommand2();
|
|
@@ -2838,9 +3046,14 @@ async function run() {
|
|
|
2838
3046
|
} else if (sub === "posttool") {
|
|
2839
3047
|
const { runHookPosttool: runHookPosttool2 } = await Promise.resolve().then(() => (init_hook(), hook_exports));
|
|
2840
3048
|
await runHookPosttool2();
|
|
3049
|
+
} else if (sub === "stop") {
|
|
3050
|
+
const { runHookStop: runHookStop2 } = await Promise.resolve().then(() => (init_hook(), hook_exports));
|
|
3051
|
+
await runHookStop2();
|
|
3052
|
+
} else if (sub) {
|
|
3053
|
+
console.error(`kmd hook: unknown event: ${sub}`);
|
|
2841
3054
|
} else {
|
|
2842
3055
|
console.error(
|
|
2843
|
-
|
|
3056
|
+
"usage: kmd hook <prompt|pretool|posttool|stop> [<vault-root>] [--scope <scope>] [--harness <claude|kiro-ide>]"
|
|
2844
3057
|
);
|
|
2845
3058
|
process.exit(2);
|
|
2846
3059
|
}
|
|
@@ -2864,6 +3077,11 @@ async function run() {
|
|
|
2864
3077
|
break;
|
|
2865
3078
|
}
|
|
2866
3079
|
default: {
|
|
3080
|
+
const tail = positionals[1];
|
|
3081
|
+
if (tail === "prompt" || tail === "pretool" || tail === "posttool") {
|
|
3082
|
+
console.error(`kmd: unknown command: ${command}`);
|
|
3083
|
+
break;
|
|
3084
|
+
}
|
|
2867
3085
|
console.error(`unknown command: ${command}
|
|
2868
3086
|
|
|
2869
3087
|
${USAGE}`);
|