@bartolli/kmd 0.5.1 → 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 +1068 -164
- package/dist/kmd.mjs.map +4 -4
- package/package.json +1 -1
package/dist/kmd.mjs
CHANGED
|
@@ -27,9 +27,11 @@ function openDatabase(dbPath) {
|
|
|
27
27
|
db.exec(SCHEMA);
|
|
28
28
|
return db;
|
|
29
29
|
}
|
|
30
|
+
function kmdHome() {
|
|
31
|
+
return process.env.KMD_HOME ?? join(homedir(), ".kmd");
|
|
32
|
+
}
|
|
30
33
|
function indexRootDir() {
|
|
31
|
-
|
|
32
|
-
return join(home, "db");
|
|
34
|
+
return join(kmdHome(), "db");
|
|
33
35
|
}
|
|
34
36
|
function canonicalVaultRoot(vaultRoot2) {
|
|
35
37
|
try {
|
|
@@ -114,14 +116,24 @@ CREATE TABLE IF NOT EXISTS meta (
|
|
|
114
116
|
}
|
|
115
117
|
});
|
|
116
118
|
|
|
117
|
-
// ../
|
|
119
|
+
// ../db/src/vault-config.ts
|
|
118
120
|
import { readFile } from "node:fs/promises";
|
|
119
121
|
import { join as join2 } from "node:path";
|
|
120
122
|
import { parse } from "yaml";
|
|
121
123
|
import { z } from "zod";
|
|
124
|
+
function isValidRegex(pattern) {
|
|
125
|
+
try {
|
|
126
|
+
return Boolean(new RegExp(pattern));
|
|
127
|
+
} catch {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
122
131
|
function kindName(entry) {
|
|
123
132
|
return typeof entry === "string" ? entry : entry.name;
|
|
124
133
|
}
|
|
134
|
+
function configJsonSchema() {
|
|
135
|
+
return z.toJSONSchema(VaultConfigSchema, { target: "draft-7" });
|
|
136
|
+
}
|
|
125
137
|
async function loadVaultConfig(vaultRoot2) {
|
|
126
138
|
const path = join2(vaultRoot2, "vault.yaml");
|
|
127
139
|
let raw;
|
|
@@ -138,37 +150,106 @@ ${issues}`);
|
|
|
138
150
|
}
|
|
139
151
|
return parsed.data;
|
|
140
152
|
}
|
|
141
|
-
var ScopeSchema, KindEntrySchema, VaultConfigSchema, BUILT_IN_KINDS;
|
|
142
|
-
var
|
|
143
|
-
"../
|
|
153
|
+
var ScopeSchema, KindEntrySchema, WhenSchema, TriggerSchema, TriggersSchema, VaultConfigSchema, BUILT_IN_KINDS;
|
|
154
|
+
var init_vault_config = __esm({
|
|
155
|
+
"../db/src/vault-config.ts"() {
|
|
144
156
|
"use strict";
|
|
145
|
-
ScopeSchema = z.
|
|
146
|
-
repo: z.string().optional()
|
|
147
|
-
|
|
148
|
-
|
|
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.")
|
|
149
163
|
});
|
|
150
164
|
KindEntrySchema = z.union([
|
|
151
165
|
z.string(),
|
|
152
|
-
z.
|
|
166
|
+
z.strictObject({
|
|
153
167
|
name: z.string(),
|
|
154
|
-
signal: z.string(),
|
|
155
|
-
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.")
|
|
170
|
+
})
|
|
171
|
+
]);
|
|
172
|
+
WhenSchema = z.union([
|
|
173
|
+
z.string(),
|
|
174
|
+
z.strictObject({
|
|
175
|
+
name: z.enum(["newer-than"]),
|
|
176
|
+
fresh: z.array(z.string().min(1)).min(1),
|
|
177
|
+
than: z.array(z.string().min(1)).min(1)
|
|
156
178
|
})
|
|
157
179
|
]);
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
180
|
+
TriggerSchema = z.strictObject({
|
|
181
|
+
id: z.string().min(1).describe("Unique per scope list; duplicates keep the first occurrence."),
|
|
182
|
+
on: z.enum(["prompt", "pretool"]),
|
|
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.")
|
|
194
|
+
}).superRefine((trigger, ctx) => {
|
|
195
|
+
if (trigger.on === "prompt" && !trigger.keywords?.length && !trigger.intent?.length) {
|
|
196
|
+
ctx.addIssue({
|
|
197
|
+
code: "custom",
|
|
198
|
+
message: `prompt trigger "${trigger.id}" needs keywords or intent`
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
if (trigger.on === "pretool" && trigger.tool === void 0 && trigger.args_match === void 0 && !trigger.files?.length) {
|
|
202
|
+
ctx.addIssue({
|
|
203
|
+
code: "custom",
|
|
204
|
+
message: `pretool trigger "${trigger.id}" needs a tool, args_match, or files matcher`
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
if (trigger.on === "prompt" && trigger.files !== void 0) {
|
|
208
|
+
ctx.addIssue({
|
|
209
|
+
code: "custom",
|
|
210
|
+
message: `trigger "${trigger.id}": files applies to pretool triggers only`
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
if (trigger.enforce === "block" ? trigger.reason === void 0 : trigger.text === void 0) {
|
|
214
|
+
ctx.addIssue({
|
|
215
|
+
code: "custom",
|
|
216
|
+
message: trigger.enforce === "block" ? `block trigger "${trigger.id}" needs a reason` : `${trigger.enforce} trigger "${trigger.id}" needs a text`
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
const patterns = [...trigger.intent ?? []];
|
|
220
|
+
if (trigger.args_match !== void 0) patterns.push(trigger.args_match);
|
|
221
|
+
for (const pattern of patterns) {
|
|
222
|
+
if (!isValidRegex(pattern)) {
|
|
223
|
+
ctx.addIssue({
|
|
224
|
+
code: "custom",
|
|
225
|
+
message: `trigger "${trigger.id}" has an invalid regex: ${pattern}`
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
TriggersSchema = z.record(z.string(), z.array(TriggerSchema));
|
|
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.")
|
|
166
241
|
}),
|
|
167
|
-
authoring_rules: z.string().optional(),
|
|
168
|
-
authoring_rules_extra: z.string().optional(),
|
|
169
|
-
sync_protocol: z.string().optional(),
|
|
170
|
-
sync_protocol_extra: z.string().optional()
|
|
171
|
-
|
|
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) => {
|
|
172
253
|
for (const [name, scope] of Object.entries(config.scopes)) {
|
|
173
254
|
if (scope.methodology !== void 0 && !config.methodologies.includes(scope.methodology)) {
|
|
174
255
|
ctx.addIssue({
|
|
@@ -178,6 +259,28 @@ var init_config = __esm({
|
|
|
178
259
|
});
|
|
179
260
|
}
|
|
180
261
|
}
|
|
262
|
+
if (config.triggers?._all !== void 0) {
|
|
263
|
+
ctx.addIssue({
|
|
264
|
+
code: "custom",
|
|
265
|
+
path: ["triggers", "_all"],
|
|
266
|
+
message: '"_all" is reserved for triggers_extra'
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
for (const field of ["triggers", "triggers_extra"]) {
|
|
270
|
+
for (const [scope, list] of Object.entries(config[field] ?? {})) {
|
|
271
|
+
const seen = /* @__PURE__ */ new Set();
|
|
272
|
+
for (const trigger of list) {
|
|
273
|
+
if (seen.has(trigger.id)) {
|
|
274
|
+
ctx.addIssue({
|
|
275
|
+
code: "custom",
|
|
276
|
+
path: [field, scope],
|
|
277
|
+
message: `duplicate trigger id "${trigger.id}"`
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
seen.add(trigger.id);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
181
284
|
});
|
|
182
285
|
BUILT_IN_KINDS = /* @__PURE__ */ new Set([
|
|
183
286
|
"project",
|
|
@@ -220,11 +323,287 @@ var init_frontmatter = __esm({
|
|
|
220
323
|
}
|
|
221
324
|
});
|
|
222
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
|
+
|
|
223
602
|
// ../cli/src/sync.ts
|
|
224
603
|
import { createHash as createHash2 } from "node:crypto";
|
|
225
604
|
import { mkdirSync } from "node:fs";
|
|
226
|
-
import { readdir, readFile as
|
|
227
|
-
import { dirname, join as
|
|
605
|
+
import { readdir as readdir2, readFile as readFile3 } from "node:fs/promises";
|
|
606
|
+
import { dirname, join as join4, relative, sep } from "node:path";
|
|
228
607
|
import { z as z2 } from "zod";
|
|
229
608
|
function loadEnv() {
|
|
230
609
|
const parsed = EnvSchema.safeParse({
|
|
@@ -240,18 +619,18 @@ function loadEnv() {
|
|
|
240
619
|
async function walkMarkdown(root, domain) {
|
|
241
620
|
const out = [];
|
|
242
621
|
async function recurse(dir) {
|
|
243
|
-
const entries = await
|
|
622
|
+
const entries = await readdir2(dir, { withFileTypes: true }).catch(() => null);
|
|
244
623
|
if (!entries) return;
|
|
245
624
|
for (const entry of entries) {
|
|
246
625
|
if (entry.name.startsWith(".")) continue;
|
|
247
626
|
if (entry.isDirectory()) {
|
|
248
|
-
await recurse(
|
|
627
|
+
await recurse(join4(dir, entry.name));
|
|
249
628
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
250
|
-
out.push(
|
|
629
|
+
out.push(join4(dir, entry.name));
|
|
251
630
|
}
|
|
252
631
|
}
|
|
253
632
|
}
|
|
254
|
-
await recurse(
|
|
633
|
+
await recurse(join4(root, domain));
|
|
255
634
|
return out;
|
|
256
635
|
}
|
|
257
636
|
function toRelativePath(root, absolute) {
|
|
@@ -373,26 +752,24 @@ function syncPage(db, fields) {
|
|
|
373
752
|
}
|
|
374
753
|
return "changed";
|
|
375
754
|
}
|
|
376
|
-
async function
|
|
377
|
-
const
|
|
378
|
-
const
|
|
379
|
-
console.log(`sync: ${env.WIKI_VAULT} \u2192 ${dbPath}`);
|
|
380
|
-
const vaultConfig = await loadVaultConfig(env.WIKI_VAULT);
|
|
755
|
+
async function syncVault(vaultRoot2) {
|
|
756
|
+
const dbPath = resolveIndexPath(vaultRoot2);
|
|
757
|
+
const vaultConfig = await loadVaultConfig(vaultRoot2);
|
|
381
758
|
const scopes = new Set(Object.keys(vaultConfig.scopes));
|
|
382
759
|
mkdirSync(dirname(dbPath), { recursive: true });
|
|
383
760
|
const db = openDatabase(dbPath);
|
|
384
761
|
try {
|
|
385
762
|
const files = [];
|
|
386
763
|
for (const domain of SCAN_DOMAINS) {
|
|
387
|
-
files.push(...await walkMarkdown(
|
|
764
|
+
files.push(...await walkMarkdown(vaultRoot2, domain));
|
|
388
765
|
}
|
|
389
766
|
const indexedPaths = [];
|
|
390
767
|
let changed = 0;
|
|
391
768
|
let unchanged = 0;
|
|
392
769
|
let skipped = 0;
|
|
393
770
|
for (const file of files) {
|
|
394
|
-
const path = toRelativePath(
|
|
395
|
-
const raw = await
|
|
771
|
+
const path = toRelativePath(vaultRoot2, file);
|
|
772
|
+
const raw = await readFile3(file, "utf8");
|
|
396
773
|
const parsed = parseFrontmatter(raw);
|
|
397
774
|
const fields = buildPageFields(path, raw, parsed, scopes);
|
|
398
775
|
if (!fields) {
|
|
@@ -415,26 +792,44 @@ async function runSync() {
|
|
|
415
792
|
pagesDeleted = Number(pageResult.changes);
|
|
416
793
|
const linkResult = db.prepare("DELETE FROM links WHERE source_path NOT IN (SELECT path FROM pages)").run();
|
|
417
794
|
linksDeleted = Number(linkResult.changes);
|
|
418
|
-
} else {
|
|
419
|
-
console.warn("no indexable pages found; skipping orphan deletion (safety)");
|
|
420
795
|
}
|
|
421
796
|
db.exec("INSERT INTO pages_fts(pages_fts) VALUES('rebuild')");
|
|
422
|
-
setMeta(db, "vault_root", canonicalVaultRoot(
|
|
797
|
+
setMeta(db, "vault_root", canonicalVaultRoot(vaultRoot2));
|
|
423
798
|
setMeta(db, "last_synced", (/* @__PURE__ */ new Date()).toISOString());
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
799
|
+
if (await refreshSchemaFile(vaultRoot2)) {
|
|
800
|
+
console.error("sync: vault.schema.json refreshed to the running engine");
|
|
801
|
+
}
|
|
802
|
+
return {
|
|
803
|
+
changed,
|
|
804
|
+
unchanged,
|
|
805
|
+
skipped,
|
|
806
|
+
pagesDeleted,
|
|
807
|
+
linksDeleted,
|
|
808
|
+
noPages: indexedPaths.length === 0
|
|
809
|
+
};
|
|
427
810
|
} finally {
|
|
428
811
|
db.close();
|
|
429
812
|
}
|
|
430
813
|
}
|
|
814
|
+
async function runSync() {
|
|
815
|
+
const env = loadEnv();
|
|
816
|
+
console.log(`sync: ${env.WIKI_VAULT} \u2192 ${resolveIndexPath(env.WIKI_VAULT)}`);
|
|
817
|
+
const stats = await syncVault(env.WIKI_VAULT);
|
|
818
|
+
if (stats.noPages) {
|
|
819
|
+
console.warn("no indexable pages found; skipping orphan deletion (safety)");
|
|
820
|
+
}
|
|
821
|
+
console.log(
|
|
822
|
+
`done: ${stats.changed} changed, ${stats.unchanged} unchanged, ${stats.skipped} skipped, ${stats.pagesDeleted} pages deleted, ${stats.linksDeleted} link orphans cleared`
|
|
823
|
+
);
|
|
824
|
+
}
|
|
431
825
|
var EnvSchema, SCAN_DOMAINS, WIKILINK_RE, INDEXED_FRONTMATTER_KEYS;
|
|
432
826
|
var init_sync = __esm({
|
|
433
827
|
"../cli/src/sync.ts"() {
|
|
434
828
|
"use strict";
|
|
435
829
|
init_database();
|
|
436
|
-
|
|
830
|
+
init_vault_config();
|
|
437
831
|
init_frontmatter();
|
|
832
|
+
init_init();
|
|
438
833
|
EnvSchema = z2.object({
|
|
439
834
|
WIKI_VAULT: z2.string().min(1)
|
|
440
835
|
});
|
|
@@ -454,8 +849,8 @@ var init_sync = __esm({
|
|
|
454
849
|
});
|
|
455
850
|
|
|
456
851
|
// ../cli/src/validate.ts
|
|
457
|
-
import { readFile as
|
|
458
|
-
import { join as
|
|
852
|
+
import { readFile as readFile4, stat } from "node:fs/promises";
|
|
853
|
+
import { join as join5 } from "node:path";
|
|
459
854
|
function hasIndexableTitle(data) {
|
|
460
855
|
return typeof data.title === "string" && data.title.trim() !== "";
|
|
461
856
|
}
|
|
@@ -813,7 +1208,7 @@ async function validateVault(root) {
|
|
|
813
1208
|
const pages = [];
|
|
814
1209
|
const linkPages = [];
|
|
815
1210
|
for (const { relPath, abs } of files) {
|
|
816
|
-
const raw = await
|
|
1211
|
+
const raw = await readFile4(abs, "utf8");
|
|
817
1212
|
findings.push(...validatePage(relPath, raw, cfg, refIndex));
|
|
818
1213
|
try {
|
|
819
1214
|
const parsed = parseFrontmatter(raw);
|
|
@@ -827,7 +1222,7 @@ async function validateVault(root) {
|
|
|
827
1222
|
for (const name of customKindNames(cfg)) {
|
|
828
1223
|
const file = `templates/${name}.md`;
|
|
829
1224
|
try {
|
|
830
|
-
await stat(
|
|
1225
|
+
await stat(join5(root, file));
|
|
831
1226
|
} catch {
|
|
832
1227
|
findings.push({
|
|
833
1228
|
path: file,
|
|
@@ -843,7 +1238,7 @@ var REQUIRED_FIELDS, TAG_OPTIONAL_KINDS, FOLDER_PATTERNS, UNIVERSAL_FLOOR;
|
|
|
843
1238
|
var init_validate = __esm({
|
|
844
1239
|
"../cli/src/validate.ts"() {
|
|
845
1240
|
"use strict";
|
|
846
|
-
|
|
1241
|
+
init_vault_config();
|
|
847
1242
|
init_frontmatter();
|
|
848
1243
|
init_sync();
|
|
849
1244
|
REQUIRED_FIELDS = {
|
|
@@ -901,12 +1296,13 @@ __export(cli_exports, {
|
|
|
901
1296
|
resolveCli: () => resolveCli,
|
|
902
1297
|
runConfig: () => runConfig,
|
|
903
1298
|
runDbReset: () => runDbReset,
|
|
1299
|
+
runInit: () => runInit,
|
|
904
1300
|
runSyncCommand: () => runSyncCommand,
|
|
905
1301
|
runValidate: () => runValidate,
|
|
906
1302
|
vaultRoot: () => vaultRoot
|
|
907
1303
|
});
|
|
908
1304
|
import { existsSync, readdirSync, rmSync } from "node:fs";
|
|
909
|
-
import { dirname as dirname2, join as
|
|
1305
|
+
import { dirname as dirname2, join as join6 } from "node:path";
|
|
910
1306
|
import { parseArgs } from "node:util";
|
|
911
1307
|
function resolveCli(argv) {
|
|
912
1308
|
const { positionals: positionals2 } = parseArgs({ args: argv, allowPositionals: true, strict: false });
|
|
@@ -976,7 +1372,7 @@ function knownVaults() {
|
|
|
976
1372
|
const known = [];
|
|
977
1373
|
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
978
1374
|
if (!entry.isDirectory()) continue;
|
|
979
|
-
const index =
|
|
1375
|
+
const index = join6(root, entry.name, "index.db");
|
|
980
1376
|
if (!existsSync(index)) continue;
|
|
981
1377
|
const db = openDatabase(index);
|
|
982
1378
|
try {
|
|
@@ -1039,6 +1435,7 @@ var init_cli = __esm({
|
|
|
1039
1435
|
init_database();
|
|
1040
1436
|
init_sync();
|
|
1041
1437
|
init_validate();
|
|
1438
|
+
init_init();
|
|
1042
1439
|
}
|
|
1043
1440
|
});
|
|
1044
1441
|
|
|
@@ -1059,7 +1456,7 @@ ${issues}`);
|
|
|
1059
1456
|
};
|
|
1060
1457
|
}
|
|
1061
1458
|
var EnvSchema2;
|
|
1062
|
-
var
|
|
1459
|
+
var init_config = __esm({
|
|
1063
1460
|
"../mcp/src/config.ts"() {
|
|
1064
1461
|
"use strict";
|
|
1065
1462
|
EnvSchema2 = z3.object({
|
|
@@ -1091,7 +1488,7 @@ var init_db = __esm({
|
|
|
1091
1488
|
// ../mcp/src/lib/diag.ts
|
|
1092
1489
|
import { appendFileSync, mkdirSync as mkdirSync3 } from "node:fs";
|
|
1093
1490
|
import { homedir as homedir2 } from "node:os";
|
|
1094
|
-
import { join as
|
|
1491
|
+
import { join as join7 } from "node:path";
|
|
1095
1492
|
function diag(msg, data) {
|
|
1096
1493
|
try {
|
|
1097
1494
|
const line = data ? `${(/* @__PURE__ */ new Date()).toISOString()} pid=${process.pid} ${msg} ${JSON.stringify(data)}
|
|
@@ -1105,8 +1502,8 @@ var DIAG_DIR, DIAG_LOG_PATH;
|
|
|
1105
1502
|
var init_diag = __esm({
|
|
1106
1503
|
"../mcp/src/lib/diag.ts"() {
|
|
1107
1504
|
"use strict";
|
|
1108
|
-
DIAG_DIR =
|
|
1109
|
-
DIAG_LOG_PATH =
|
|
1505
|
+
DIAG_DIR = join7(homedir2(), ".local", "state", "wiki-mcp");
|
|
1506
|
+
DIAG_LOG_PATH = join7(DIAG_DIR, "server.log");
|
|
1110
1507
|
try {
|
|
1111
1508
|
mkdirSync3(DIAG_DIR, { recursive: true });
|
|
1112
1509
|
} catch {
|
|
@@ -1133,88 +1530,6 @@ var init_logger = __esm({
|
|
|
1133
1530
|
}
|
|
1134
1531
|
});
|
|
1135
1532
|
|
|
1136
|
-
// ../mcp/src/vault-config.ts
|
|
1137
|
-
import { readFile as readFile4 } from "node:fs/promises";
|
|
1138
|
-
import { join as join7 } from "node:path";
|
|
1139
|
-
import { parse as parse2 } from "yaml";
|
|
1140
|
-
import { z as z4 } from "zod";
|
|
1141
|
-
function kindName2(entry) {
|
|
1142
|
-
return typeof entry === "string" ? entry : entry.name;
|
|
1143
|
-
}
|
|
1144
|
-
async function loadVaultConfig2(vaultRoot2) {
|
|
1145
|
-
const path = join7(vaultRoot2, "vault.yaml");
|
|
1146
|
-
let raw;
|
|
1147
|
-
try {
|
|
1148
|
-
raw = await readFile4(path, "utf8");
|
|
1149
|
-
} catch (err) {
|
|
1150
|
-
throw new Error(`vault.yaml not found at ${path}`, { cause: err });
|
|
1151
|
-
}
|
|
1152
|
-
const parsed = VaultConfigSchema2.safeParse(parse2(raw));
|
|
1153
|
-
if (!parsed.success) {
|
|
1154
|
-
const issues = parsed.error.issues.map((i) => ` - ${i.path.join(".") || "(root)"}: ${i.message}`).join("\n");
|
|
1155
|
-
throw new Error(`Invalid vault.yaml at ${path}:
|
|
1156
|
-
${issues}`);
|
|
1157
|
-
}
|
|
1158
|
-
return parsed.data;
|
|
1159
|
-
}
|
|
1160
|
-
var ScopeSchema2, KindEntrySchema2, VaultConfigSchema2, BUILT_IN_KINDS2;
|
|
1161
|
-
var init_vault_config = __esm({
|
|
1162
|
-
"../mcp/src/vault-config.ts"() {
|
|
1163
|
-
"use strict";
|
|
1164
|
-
ScopeSchema2 = z4.object({
|
|
1165
|
-
repo: z4.string().optional(),
|
|
1166
|
-
methodology: z4.string().optional(),
|
|
1167
|
-
status: z4.string()
|
|
1168
|
-
});
|
|
1169
|
-
KindEntrySchema2 = z4.union([
|
|
1170
|
-
z4.string(),
|
|
1171
|
-
z4.object({
|
|
1172
|
-
name: z4.string(),
|
|
1173
|
-
signal: z4.string(),
|
|
1174
|
-
where: z4.string()
|
|
1175
|
-
})
|
|
1176
|
-
]);
|
|
1177
|
-
VaultConfigSchema2 = z4.object({
|
|
1178
|
-
scopes: z4.record(z4.string(), ScopeSchema2),
|
|
1179
|
-
kinds: z4.array(KindEntrySchema2),
|
|
1180
|
-
statuses: z4.array(z4.string()),
|
|
1181
|
-
methodologies: z4.array(z4.string()),
|
|
1182
|
-
tags: z4.object({
|
|
1183
|
-
canonical: z4.array(z4.string()),
|
|
1184
|
-
aliases: z4.record(z4.string(), z4.string())
|
|
1185
|
-
}),
|
|
1186
|
-
authoring_rules: z4.string().optional(),
|
|
1187
|
-
authoring_rules_extra: z4.string().optional(),
|
|
1188
|
-
sync_protocol: z4.string().optional(),
|
|
1189
|
-
sync_protocol_extra: z4.string().optional()
|
|
1190
|
-
}).superRefine((config, ctx) => {
|
|
1191
|
-
for (const [name, scope] of Object.entries(config.scopes)) {
|
|
1192
|
-
if (scope.methodology !== void 0 && !config.methodologies.includes(scope.methodology)) {
|
|
1193
|
-
ctx.addIssue({
|
|
1194
|
-
code: "custom",
|
|
1195
|
-
path: ["scopes", name, "methodology"],
|
|
1196
|
-
message: `"${scope.methodology}" is not in the methodologies list`
|
|
1197
|
-
});
|
|
1198
|
-
}
|
|
1199
|
-
}
|
|
1200
|
-
});
|
|
1201
|
-
BUILT_IN_KINDS2 = /* @__PURE__ */ new Set([
|
|
1202
|
-
"project",
|
|
1203
|
-
"spec",
|
|
1204
|
-
"adr",
|
|
1205
|
-
"plan",
|
|
1206
|
-
"story",
|
|
1207
|
-
"ops",
|
|
1208
|
-
"topic",
|
|
1209
|
-
"article",
|
|
1210
|
-
"src",
|
|
1211
|
-
"note",
|
|
1212
|
-
"artifact",
|
|
1213
|
-
"prompt"
|
|
1214
|
-
]);
|
|
1215
|
-
}
|
|
1216
|
-
});
|
|
1217
|
-
|
|
1218
1533
|
// ../mcp/src/resources/authoring.ts
|
|
1219
1534
|
function buildAuthoringRules(config) {
|
|
1220
1535
|
const parts = [(config.authoring_rules ?? DEFAULT_AUTHORING_RULES).trim()];
|
|
@@ -1229,13 +1544,13 @@ function buildSyncProtocol(config) {
|
|
|
1229
1544
|
function buildKindSelector(kinds) {
|
|
1230
1545
|
const lines = ["## Kind selector", "", "| Signal | Kind | Where |", "|---|---|---|"];
|
|
1231
1546
|
for (const entry of kinds) {
|
|
1232
|
-
const name =
|
|
1547
|
+
const name = kindName(entry);
|
|
1233
1548
|
const pedagogy = typeof entry === "string" ? KIND_PEDAGOGY.get(entry) : entry;
|
|
1234
1549
|
const signal = pedagogy?.signal ?? "\u2014";
|
|
1235
1550
|
const where = pedagogy?.where ?? "\u2014";
|
|
1236
1551
|
lines.push(`| ${signal} | **${name}** | ${where} |`);
|
|
1237
1552
|
}
|
|
1238
|
-
const names = kinds.map(
|
|
1553
|
+
const names = kinds.map(kindName);
|
|
1239
1554
|
const hasNote = names.includes("note");
|
|
1240
1555
|
const hasAdrAndSpec = names.includes("adr") && names.includes("spec");
|
|
1241
1556
|
if (hasNote || hasAdrAndSpec) {
|
|
@@ -1258,7 +1573,7 @@ function buildVocabulary(config) {
|
|
|
1258
1573
|
const lines = [
|
|
1259
1574
|
"## Controlled vocabulary",
|
|
1260
1575
|
"",
|
|
1261
|
-
`**Kinds:** ${config.kinds.map(
|
|
1576
|
+
`**Kinds:** ${config.kinds.map(kindName).join(", ")}`,
|
|
1262
1577
|
buildStatusLine(config.statuses),
|
|
1263
1578
|
`**Methodologies:** ${config.methodologies.join(", ")}`,
|
|
1264
1579
|
`**Canonical tags:** ${config.tags.canonical.join(", ")}`
|
|
@@ -1434,7 +1749,7 @@ import { join as join8 } from "node:path";
|
|
|
1434
1749
|
function customTemplates(config) {
|
|
1435
1750
|
const specs = [];
|
|
1436
1751
|
for (const entry of config.kinds) {
|
|
1437
|
-
if (typeof entry === "string" ||
|
|
1752
|
+
if (typeof entry === "string" || BUILT_IN_KINDS.has(entry.name)) continue;
|
|
1438
1753
|
specs.push({
|
|
1439
1754
|
uri: `wiki://template/${entry.name}`,
|
|
1440
1755
|
name: entry.name.charAt(0).toUpperCase() + entry.name.slice(1),
|
|
@@ -1635,7 +1950,7 @@ var init_toolResponse = __esm({
|
|
|
1635
1950
|
});
|
|
1636
1951
|
|
|
1637
1952
|
// ../mcp/src/tools/search.ts
|
|
1638
|
-
import { z as
|
|
1953
|
+
import { z as z4 } from "zod";
|
|
1639
1954
|
function search(deps, input) {
|
|
1640
1955
|
const ftsQuery = sanitizeFtsQuery(input.query);
|
|
1641
1956
|
if (!ftsQuery) return { results: [] };
|
|
@@ -1682,15 +1997,15 @@ var init_search = __esm({
|
|
|
1682
1997
|
"use strict";
|
|
1683
1998
|
init_fts();
|
|
1684
1999
|
init_toolResponse();
|
|
1685
|
-
SearchInputSchema =
|
|
1686
|
-
query:
|
|
2000
|
+
SearchInputSchema = z4.object({
|
|
2001
|
+
query: z4.string().min(1).describe(
|
|
1687
2002
|
"Natural-language search query. Matched against title, summary, and body via SQLite FTS5."
|
|
1688
2003
|
),
|
|
1689
|
-
scope:
|
|
1690
|
-
kind:
|
|
2004
|
+
scope: z4.string().optional().describe('Optional: restrict to a project scope (e.g. "ontology", "sotto").'),
|
|
2005
|
+
kind: z4.string().optional().describe(
|
|
1691
2006
|
"Optional: restrict to a kind. Project kinds: spec, adr, plan, ops. Research kinds: article, src. Misc: note."
|
|
1692
2007
|
),
|
|
1693
|
-
limit:
|
|
2008
|
+
limit: z4.number().int().min(1).max(50).default(5).describe("Maximum number of ranked results to return. Default 5.")
|
|
1694
2009
|
});
|
|
1695
2010
|
FTS_RANK = "bm25(pages_fts, 10.0, 5.0, 1.0)";
|
|
1696
2011
|
}
|
|
@@ -1699,7 +2014,7 @@ var init_search = __esm({
|
|
|
1699
2014
|
// ../mcp/src/tools/prime.ts
|
|
1700
2015
|
import { readFile as readFile6 } from "node:fs/promises";
|
|
1701
2016
|
import { basename as basename3, join as join9 } from "node:path";
|
|
1702
|
-
import { z as
|
|
2017
|
+
import { z as z5 } from "zod";
|
|
1703
2018
|
function pathSlug(p) {
|
|
1704
2019
|
return basename3(p).replace(/\.md$/, "");
|
|
1705
2020
|
}
|
|
@@ -1842,7 +2157,7 @@ function renderMarkdown(d, config, task) {
|
|
|
1842
2157
|
lines.push(countEntries.map(([k, n]) => `${k}: ${n}`).join(" | "));
|
|
1843
2158
|
}
|
|
1844
2159
|
lines.push("", "## Vocabulary");
|
|
1845
|
-
lines.push(`kinds: ${config.kinds.map(
|
|
2160
|
+
lines.push(`kinds: ${config.kinds.map(kindName).join(", ")}`);
|
|
1846
2161
|
lines.push(`statuses: ${config.statuses.join(", ")}`);
|
|
1847
2162
|
lines.push(`tags: ${config.tags.canonical.join(", ")}`);
|
|
1848
2163
|
if (d.top_tags.length > 0) {
|
|
@@ -1903,14 +2218,14 @@ var init_prime = __esm({
|
|
|
1903
2218
|
"../mcp/src/tools/prime.ts"() {
|
|
1904
2219
|
"use strict";
|
|
1905
2220
|
init_database();
|
|
2221
|
+
init_vault_config();
|
|
1906
2222
|
init_frontmatter2();
|
|
1907
2223
|
init_fts();
|
|
1908
2224
|
init_toolResponse();
|
|
1909
|
-
init_vault_config();
|
|
1910
2225
|
init_search();
|
|
1911
|
-
PrimeInputSchema =
|
|
1912
|
-
scope:
|
|
1913
|
-
task:
|
|
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.")
|
|
1914
2229
|
});
|
|
1915
2230
|
}
|
|
1916
2231
|
});
|
|
@@ -1962,7 +2277,7 @@ async function startMcpServer() {
|
|
|
1962
2277
|
diag("main entered");
|
|
1963
2278
|
const config = loadConfig();
|
|
1964
2279
|
diag("config loaded", { vault: config.wikiVault, level: config.logLevel });
|
|
1965
|
-
const vaultConfig = await
|
|
2280
|
+
const vaultConfig = await loadVaultConfig(config.wikiVault);
|
|
1966
2281
|
diag("vault config loaded", {
|
|
1967
2282
|
scopes: Object.keys(vaultConfig.scopes).length,
|
|
1968
2283
|
kinds: vaultConfig.kinds.length,
|
|
@@ -2005,36 +2320,594 @@ async function startMcpServer() {
|
|
|
2005
2320
|
var init_start = __esm({
|
|
2006
2321
|
"../mcp/src/start.ts"() {
|
|
2007
2322
|
"use strict";
|
|
2008
|
-
|
|
2323
|
+
init_vault_config();
|
|
2324
|
+
init_config();
|
|
2009
2325
|
init_db();
|
|
2010
2326
|
init_diag();
|
|
2011
2327
|
init_logger();
|
|
2012
2328
|
init_server();
|
|
2329
|
+
}
|
|
2330
|
+
});
|
|
2331
|
+
|
|
2332
|
+
// ../cli/src/hook.ts
|
|
2333
|
+
var hook_exports = {};
|
|
2334
|
+
__export(hook_exports, {
|
|
2335
|
+
dedupeMatches: () => dedupeMatches,
|
|
2336
|
+
dedupePretoolMatches: () => dedupePretoolMatches,
|
|
2337
|
+
effectiveTriggers: () => effectiveTriggers,
|
|
2338
|
+
evaluateMatches: () => evaluateMatches,
|
|
2339
|
+
hookStateDir: () => hookStateDir,
|
|
2340
|
+
kiroIdePromptEvent: () => kiroIdePromptEvent,
|
|
2341
|
+
loadTriggerFile: () => loadTriggerFile,
|
|
2342
|
+
matchPretoolTriggers: () => matchPretoolTriggers,
|
|
2343
|
+
matchPromptTriggers: () => matchPromptTriggers,
|
|
2344
|
+
parsePretoolEvent: () => parsePretoolEvent,
|
|
2345
|
+
parsePromptEvent: () => parsePromptEvent,
|
|
2346
|
+
renderPosttool: () => renderPosttool,
|
|
2347
|
+
renderPretool: () => renderPretool,
|
|
2348
|
+
resolveScope: () => resolveScope,
|
|
2349
|
+
runHookPosttool: () => runHookPosttool,
|
|
2350
|
+
runHookPretool: () => runHookPretool,
|
|
2351
|
+
runHookPrompt: () => runHookPrompt,
|
|
2352
|
+
vaultPathTouched: () => vaultPathTouched
|
|
2353
|
+
});
|
|
2354
|
+
import { mkdirSync as mkdirSync4, readdirSync as readdirSync2, readFileSync, rmSync as rmSync2, statSync, writeFileSync } from "node:fs";
|
|
2355
|
+
import { homedir as homedir3 } from "node:os";
|
|
2356
|
+
import { join as join10, resolve as resolve3, sep as sep2 } from "node:path";
|
|
2357
|
+
import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
|
|
2358
|
+
import { parseArgs as parseArgs2 } from "node:util";
|
|
2359
|
+
import { parse as parseYaml3 } from "yaml";
|
|
2360
|
+
import { z as z6 } from "zod";
|
|
2361
|
+
function eventFields(raw) {
|
|
2362
|
+
let data;
|
|
2363
|
+
try {
|
|
2364
|
+
data = JSON.parse(raw);
|
|
2365
|
+
} catch {
|
|
2366
|
+
return null;
|
|
2367
|
+
}
|
|
2368
|
+
if (typeof data !== "object" || data === null) return null;
|
|
2369
|
+
return data;
|
|
2370
|
+
}
|
|
2371
|
+
function parsePromptEvent(raw) {
|
|
2372
|
+
const fields = eventFields(raw);
|
|
2373
|
+
if (fields === null) return null;
|
|
2374
|
+
const { session_id, prompt, cwd } = fields;
|
|
2375
|
+
if (typeof session_id !== "string" || typeof prompt !== "string") return null;
|
|
2376
|
+
return { session_id, prompt, ...typeof cwd === "string" && { cwd } };
|
|
2377
|
+
}
|
|
2378
|
+
function kiroIdePromptEvent(now = Date.now()) {
|
|
2379
|
+
const prompt = process.env.USER_PROMPT;
|
|
2380
|
+
if (prompt === void 0 || prompt === "") return null;
|
|
2381
|
+
const cwd = process.cwd();
|
|
2382
|
+
return { session_id: `kiro:${cwd}:${Math.floor(now / KIRO_IDE_BUCKET_MS)}`, prompt, cwd };
|
|
2383
|
+
}
|
|
2384
|
+
function loadTriggerFile(path) {
|
|
2385
|
+
try {
|
|
2386
|
+
const result = z6.array(TriggerSchema).safeParse(parseYaml3(readFileSync(path, "utf8")));
|
|
2387
|
+
return result.success ? result.data : null;
|
|
2388
|
+
} catch {
|
|
2389
|
+
return null;
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
function effectiveTriggers(config, scope, fileTriggers = []) {
|
|
2393
|
+
const replace = scope === void 0 ? void 0 : config.triggers?.[scope];
|
|
2394
|
+
const base = replace ?? [...DEFAULT_TRIGGERS, ...fileTriggers];
|
|
2395
|
+
const allExtras = config.triggers_extra?.[ALL_SCOPES_KEY] ?? [];
|
|
2396
|
+
const scopeExtras = scope === void 0 || scope === ALL_SCOPES_KEY ? [] : config.triggers_extra?.[scope] ?? [];
|
|
2397
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2398
|
+
const triggers = [];
|
|
2399
|
+
const duplicates = [];
|
|
2400
|
+
for (const trigger of [...base, ...allExtras, ...scopeExtras]) {
|
|
2401
|
+
if (seen.has(trigger.id)) {
|
|
2402
|
+
duplicates.push(trigger.id);
|
|
2403
|
+
continue;
|
|
2404
|
+
}
|
|
2405
|
+
seen.add(trigger.id);
|
|
2406
|
+
triggers.push(trigger);
|
|
2407
|
+
}
|
|
2408
|
+
return { triggers, duplicates };
|
|
2409
|
+
}
|
|
2410
|
+
function expandHome(path) {
|
|
2411
|
+
if (path === "~") return homedir3();
|
|
2412
|
+
return path.startsWith("~/") ? join10(homedir3(), path.slice(2)) : path;
|
|
2413
|
+
}
|
|
2414
|
+
function resolveScope(config, cwd) {
|
|
2415
|
+
if (cwd === void 0 || cwd === "") return void 0;
|
|
2416
|
+
let best;
|
|
2417
|
+
let bestLength = -1;
|
|
2418
|
+
for (const [name, scope] of Object.entries(config.scopes)) {
|
|
2419
|
+
if (scope.repo === void 0) continue;
|
|
2420
|
+
const repo = expandHome(scope.repo).replace(/\/+$/, "");
|
|
2421
|
+
if (!repo.startsWith("/")) continue;
|
|
2422
|
+
if (cwd !== repo && !cwd.startsWith(`${repo}/`)) continue;
|
|
2423
|
+
if (repo.length > bestLength) {
|
|
2424
|
+
best = name;
|
|
2425
|
+
bestLength = repo.length;
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
return best;
|
|
2429
|
+
}
|
|
2430
|
+
function keywordQuery(keywords) {
|
|
2431
|
+
return keywords.map((keyword) => `"${keyword.replaceAll('"', '""')}"`).join(" OR ");
|
|
2432
|
+
}
|
|
2433
|
+
function openPromptIndex(prompt) {
|
|
2434
|
+
const db = new DatabaseSync2(":memory:");
|
|
2435
|
+
db.exec(`CREATE VIRTUAL TABLE prompt_doc USING fts5(text, tokenize = 'porter unicode61')`);
|
|
2436
|
+
db.prepare("INSERT INTO prompt_doc (text) VALUES (?)").run(prompt);
|
|
2437
|
+
return db;
|
|
2438
|
+
}
|
|
2439
|
+
function matchPromptTriggers(prompt, triggers) {
|
|
2440
|
+
const candidates = triggers.filter(
|
|
2441
|
+
(trigger) => trigger.on === "prompt" && trigger.enforce === "inject" && trigger.text !== void 0
|
|
2442
|
+
);
|
|
2443
|
+
if (candidates.length === 0) return [];
|
|
2444
|
+
const matches = [];
|
|
2445
|
+
let db = null;
|
|
2446
|
+
try {
|
|
2447
|
+
for (const trigger of candidates) {
|
|
2448
|
+
let hit = false;
|
|
2449
|
+
if (trigger.keywords !== void 0 && trigger.keywords.length > 0) {
|
|
2450
|
+
db ??= openPromptIndex(prompt);
|
|
2451
|
+
const row = db.prepare("SELECT count(*) AS n FROM prompt_doc WHERE prompt_doc MATCH ?").get(keywordQuery(trigger.keywords));
|
|
2452
|
+
hit = row.n > 0;
|
|
2453
|
+
}
|
|
2454
|
+
if (!hit && trigger.intent !== void 0) {
|
|
2455
|
+
hit = trigger.intent.some((pattern) => new RegExp(pattern, "i").test(prompt));
|
|
2456
|
+
}
|
|
2457
|
+
if (hit) {
|
|
2458
|
+
matches.push({ id: trigger.id, text: trigger.text });
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
} finally {
|
|
2462
|
+
db?.close();
|
|
2463
|
+
}
|
|
2464
|
+
return matches;
|
|
2465
|
+
}
|
|
2466
|
+
function parsePretoolEvent(raw) {
|
|
2467
|
+
const fields = eventFields(raw);
|
|
2468
|
+
if (fields === null) return null;
|
|
2469
|
+
const { session_id, tool_name, tool_input, cwd } = fields;
|
|
2470
|
+
if (typeof session_id !== "string" || typeof tool_name !== "string") return null;
|
|
2471
|
+
return { session_id, tool_name, tool_input, ...typeof cwd === "string" && { cwd } };
|
|
2472
|
+
}
|
|
2473
|
+
function globToRegExp(glob) {
|
|
2474
|
+
let source = "^";
|
|
2475
|
+
let i = 0;
|
|
2476
|
+
while (i < glob.length) {
|
|
2477
|
+
const char = glob[i];
|
|
2478
|
+
if (char === "*") {
|
|
2479
|
+
if (glob.startsWith("**/", i)) {
|
|
2480
|
+
source += "(?:.*/)?";
|
|
2481
|
+
i += 3;
|
|
2482
|
+
} else if (glob.startsWith("**", i)) {
|
|
2483
|
+
source += ".*";
|
|
2484
|
+
i += 2;
|
|
2485
|
+
} else {
|
|
2486
|
+
source += "[^/]*";
|
|
2487
|
+
i += 1;
|
|
2488
|
+
}
|
|
2489
|
+
} else if (char === "?") {
|
|
2490
|
+
source += "[^/]";
|
|
2491
|
+
i += 1;
|
|
2492
|
+
} else {
|
|
2493
|
+
source += char.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
2494
|
+
i += 1;
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
return new RegExp(`${source}$`);
|
|
2498
|
+
}
|
|
2499
|
+
function pathCandidates(toolInput, cwd) {
|
|
2500
|
+
if (typeof toolInput !== "object" || toolInput === null) return [];
|
|
2501
|
+
const fields = toolInput;
|
|
2502
|
+
const candidates = [];
|
|
2503
|
+
for (const key of ["file_path", "notebook_path", "path"]) {
|
|
2504
|
+
const value = fields[key];
|
|
2505
|
+
if (typeof value === "string" && value !== "") {
|
|
2506
|
+
candidates.push(value);
|
|
2507
|
+
if (cwd !== void 0 && value.startsWith(`${cwd}/`)) {
|
|
2508
|
+
candidates.push(value.slice(cwd.length + 1));
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
return candidates;
|
|
2513
|
+
}
|
|
2514
|
+
function matchPretoolTriggers(toolName, toolInput, triggers, cwd) {
|
|
2515
|
+
const matches = [];
|
|
2516
|
+
for (const trigger of triggers) {
|
|
2517
|
+
if (trigger.on !== "pretool") continue;
|
|
2518
|
+
if (trigger.tool !== void 0 && trigger.tool !== toolName) continue;
|
|
2519
|
+
if (trigger.args_match !== void 0) {
|
|
2520
|
+
const serialized = JSON.stringify(toolInput ?? {});
|
|
2521
|
+
if (!new RegExp(trigger.args_match).test(serialized)) continue;
|
|
2522
|
+
}
|
|
2523
|
+
if (trigger.files !== void 0 && trigger.files.length > 0) {
|
|
2524
|
+
const candidates = pathCandidates(toolInput, cwd);
|
|
2525
|
+
const hit = trigger.files.some((glob) => {
|
|
2526
|
+
const regex = globToRegExp(glob);
|
|
2527
|
+
return candidates.some((candidate) => regex.test(candidate));
|
|
2528
|
+
});
|
|
2529
|
+
if (!hit) continue;
|
|
2530
|
+
}
|
|
2531
|
+
const text = trigger.enforce === "block" ? trigger.reason : trigger.text;
|
|
2532
|
+
if (text === void 0) continue;
|
|
2533
|
+
matches.push({
|
|
2534
|
+
id: trigger.id,
|
|
2535
|
+
enforce: trigger.enforce,
|
|
2536
|
+
text,
|
|
2537
|
+
...trigger.when !== void 0 && { when: trigger.when }
|
|
2538
|
+
});
|
|
2539
|
+
}
|
|
2540
|
+
return matches;
|
|
2541
|
+
}
|
|
2542
|
+
function evaluateMatches(matches, vaultRoot2) {
|
|
2543
|
+
const fired = [];
|
|
2544
|
+
const skipped = [];
|
|
2545
|
+
for (const match of matches) {
|
|
2546
|
+
if (match.when === void 0) {
|
|
2547
|
+
fired.push(match);
|
|
2548
|
+
continue;
|
|
2549
|
+
}
|
|
2550
|
+
const verdict = evaluateWhen(match.when, vaultRoot2);
|
|
2551
|
+
if (verdict === null) skipped.push(match.id);
|
|
2552
|
+
else if (!verdict) fired.push(match);
|
|
2553
|
+
}
|
|
2554
|
+
return { fired, skipped };
|
|
2555
|
+
}
|
|
2556
|
+
function evaluateWhen(when, vaultRoot2) {
|
|
2557
|
+
if (typeof when === "string") return null;
|
|
2558
|
+
try {
|
|
2559
|
+
const than = newestUpdated(vaultRoot2, when.than);
|
|
2560
|
+
if (than === null) return true;
|
|
2561
|
+
const fresh = newestUpdated(vaultRoot2, when.fresh);
|
|
2562
|
+
if (fresh === null) return false;
|
|
2563
|
+
return fresh >= than;
|
|
2564
|
+
} catch {
|
|
2565
|
+
return null;
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
function newestUpdated(vaultRoot2, globs) {
|
|
2569
|
+
const regexes = globs.map(globToRegExp);
|
|
2570
|
+
let newest = null;
|
|
2571
|
+
for (const entry of readdirSync2(vaultRoot2, { recursive: true })) {
|
|
2572
|
+
const rel = entry.split(sep2).join("/");
|
|
2573
|
+
if (!rel.endsWith(".md")) continue;
|
|
2574
|
+
if (rel.startsWith(".") || rel.includes("/.")) continue;
|
|
2575
|
+
if (!regexes.some((regex) => regex.test(rel))) continue;
|
|
2576
|
+
const updated = readUpdated(join10(vaultRoot2, entry));
|
|
2577
|
+
if (updated !== null && (newest === null || updated > newest)) {
|
|
2578
|
+
newest = updated;
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2581
|
+
return newest;
|
|
2582
|
+
}
|
|
2583
|
+
function readUpdated(path) {
|
|
2584
|
+
try {
|
|
2585
|
+
const { data } = parseFrontmatter(readFileSync(path, "utf8"));
|
|
2586
|
+
const updated = data.updated;
|
|
2587
|
+
if (typeof updated === "string") return updated;
|
|
2588
|
+
if (updated instanceof Date) return updated.toISOString().slice(0, 10);
|
|
2589
|
+
} catch {
|
|
2590
|
+
}
|
|
2591
|
+
return null;
|
|
2592
|
+
}
|
|
2593
|
+
function renderPretool(matches, format) {
|
|
2594
|
+
const block = matches.find((match) => match.enforce === "block");
|
|
2595
|
+
const context = matches.filter((match) => match.enforce === "inject").map((match) => match.text);
|
|
2596
|
+
const warnings = matches.filter((match) => match.enforce === "warn").map((match) => match.text);
|
|
2597
|
+
if (format === "claude") {
|
|
2598
|
+
const hookSpecificOutput = { hookEventName: "PreToolUse" };
|
|
2599
|
+
if (block !== void 0) {
|
|
2600
|
+
hookSpecificOutput.permissionDecision = "deny";
|
|
2601
|
+
hookSpecificOutput.permissionDecisionReason = block.text;
|
|
2602
|
+
}
|
|
2603
|
+
if (context.length > 0) {
|
|
2604
|
+
hookSpecificOutput.additionalContext = context.join("\n");
|
|
2605
|
+
}
|
|
2606
|
+
const decided = block !== void 0 || context.length > 0;
|
|
2607
|
+
return { stdout: decided ? JSON.stringify({ hookSpecificOutput }) : null, stderr: warnings };
|
|
2608
|
+
}
|
|
2609
|
+
if (matches.length === 0) return { stdout: null, stderr: [] };
|
|
2610
|
+
return {
|
|
2611
|
+
stdout: JSON.stringify({
|
|
2612
|
+
decision: block !== void 0 ? "deny" : "none",
|
|
2613
|
+
...block !== void 0 && { reason: block.text },
|
|
2614
|
+
context,
|
|
2615
|
+
warnings
|
|
2616
|
+
}),
|
|
2617
|
+
stderr: []
|
|
2618
|
+
};
|
|
2619
|
+
}
|
|
2620
|
+
function patchPaths(toolInput) {
|
|
2621
|
+
const fields = typeof toolInput === "object" && toolInput !== null ? toolInput : {};
|
|
2622
|
+
const sources = [toolInput, fields.patch, fields.input, fields.command].filter(
|
|
2623
|
+
(value) => typeof value === "string"
|
|
2624
|
+
);
|
|
2625
|
+
const paths = [];
|
|
2626
|
+
for (const source of sources) {
|
|
2627
|
+
for (const match of source.matchAll(PATCH_FILE_RE)) {
|
|
2628
|
+
paths.push(match[1].trim());
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2631
|
+
return paths;
|
|
2632
|
+
}
|
|
2633
|
+
function vaultPathTouched(toolInput, vaultRoot2, cwd) {
|
|
2634
|
+
const root = resolve3(vaultRoot2);
|
|
2635
|
+
const candidates = [...pathCandidates(toolInput, cwd), ...patchPaths(toolInput)];
|
|
2636
|
+
return candidates.some((candidate) => {
|
|
2637
|
+
const absolute = resolve3(cwd ?? ".", candidate);
|
|
2638
|
+
return absolute === root || absolute.startsWith(`${root}/`);
|
|
2639
|
+
});
|
|
2640
|
+
}
|
|
2641
|
+
function renderPosttool(findings, synced, format) {
|
|
2642
|
+
if (findings.length === 0 && synced) return null;
|
|
2643
|
+
const lines = findings.map((f) => `${f.severity}: ${f.path} [${f.rule}] ${f.message}`);
|
|
2644
|
+
if (format === "claude") {
|
|
2645
|
+
if (hasErrors(findings)) {
|
|
2646
|
+
return JSON.stringify({
|
|
2647
|
+
decision: "block",
|
|
2648
|
+
reason: `kmd validate failed \u2014 fix before the index syncs:
|
|
2649
|
+
${lines.join("\n")}`
|
|
2650
|
+
});
|
|
2651
|
+
}
|
|
2652
|
+
const hookSpecificOutput = { hookEventName: "PostToolUse" };
|
|
2653
|
+
const notes = [...lines];
|
|
2654
|
+
if (!synced) notes.push("kmd sync failed \u2014 index not updated; see hook stderr");
|
|
2655
|
+
hookSpecificOutput.additionalContext = notes.join("\n");
|
|
2656
|
+
return JSON.stringify({ hookSpecificOutput });
|
|
2657
|
+
}
|
|
2658
|
+
return JSON.stringify({ findings, synced });
|
|
2659
|
+
}
|
|
2660
|
+
function dedupePretoolMatches(stateDir, sessionId, matches) {
|
|
2661
|
+
const blocks = matches.filter((match) => match.enforce === "block");
|
|
2662
|
+
const rest = matches.filter((match) => match.enforce !== "block");
|
|
2663
|
+
const fresh = dedupeMatches(stateDir, sessionId, rest);
|
|
2664
|
+
return matches.filter((match) => blocks.includes(match) || fresh.includes(match));
|
|
2665
|
+
}
|
|
2666
|
+
function hookStateDir() {
|
|
2667
|
+
return join10(kmdHome(), "state", "hook");
|
|
2668
|
+
}
|
|
2669
|
+
function dedupeMatches(stateDir, sessionId, matches) {
|
|
2670
|
+
if (matches.length === 0) return [];
|
|
2671
|
+
const file = join10(stateDir, `${sessionId.replace(/[^A-Za-z0-9._-]/g, "_")}.json`);
|
|
2672
|
+
const fired = readFired(file);
|
|
2673
|
+
const fresh = matches.filter((match) => !fired.has(match.id));
|
|
2674
|
+
if (fresh.length > 0) {
|
|
2675
|
+
mkdirSync4(stateDir, { recursive: true });
|
|
2676
|
+
for (const match of fresh) {
|
|
2677
|
+
fired.add(match.id);
|
|
2678
|
+
}
|
|
2679
|
+
writeFileSync(file, JSON.stringify([...fired]));
|
|
2680
|
+
pruneStale(stateDir, file);
|
|
2681
|
+
}
|
|
2682
|
+
return fresh;
|
|
2683
|
+
}
|
|
2684
|
+
function readFired(file) {
|
|
2685
|
+
try {
|
|
2686
|
+
const data = JSON.parse(readFileSync(file, "utf8"));
|
|
2687
|
+
if (Array.isArray(data)) {
|
|
2688
|
+
return new Set(data.filter((entry) => typeof entry === "string"));
|
|
2689
|
+
}
|
|
2690
|
+
} catch {
|
|
2691
|
+
}
|
|
2692
|
+
return /* @__PURE__ */ new Set();
|
|
2693
|
+
}
|
|
2694
|
+
function pruneStale(stateDir, keep) {
|
|
2695
|
+
try {
|
|
2696
|
+
const cutoff = Date.now() - SESSION_STATE_MAX_AGE_MS;
|
|
2697
|
+
for (const entry of readdirSync2(stateDir)) {
|
|
2698
|
+
const path = join10(stateDir, entry);
|
|
2699
|
+
if (path !== keep && statSync(path).mtimeMs < cutoff) {
|
|
2700
|
+
rmSync2(path, { force: true });
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2703
|
+
} catch {
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
function hookInvocation() {
|
|
2707
|
+
const { values: values2, positionals: positionals2 } = parseArgs2({
|
|
2708
|
+
args: process.argv.slice(2),
|
|
2709
|
+
allowPositionals: true,
|
|
2710
|
+
strict: false,
|
|
2711
|
+
options: {
|
|
2712
|
+
scope: { type: "string" },
|
|
2713
|
+
harness: { type: "string" },
|
|
2714
|
+
triggers: { type: "string" }
|
|
2715
|
+
}
|
|
2716
|
+
});
|
|
2717
|
+
const vaultRoot2 = positionals2[2] ?? process.env.WIKI_VAULT;
|
|
2718
|
+
if (vaultRoot2 === void 0 || vaultRoot2 === "") {
|
|
2719
|
+
diag2("no vault root (positional or $WIKI_VAULT)");
|
|
2720
|
+
return null;
|
|
2721
|
+
}
|
|
2722
|
+
return {
|
|
2723
|
+
vaultRoot: vaultRoot2,
|
|
2724
|
+
scope: typeof values2.scope === "string" ? values2.scope : process.env.WIKI_SCOPE,
|
|
2725
|
+
harness: values2.harness,
|
|
2726
|
+
triggersFile: values2.triggers
|
|
2727
|
+
};
|
|
2728
|
+
}
|
|
2729
|
+
function resolveFileTriggers(invocation) {
|
|
2730
|
+
if (typeof invocation.triggersFile !== "string") return [];
|
|
2731
|
+
const loaded = loadTriggerFile(invocation.triggersFile);
|
|
2732
|
+
if (loaded === null) {
|
|
2733
|
+
diag2(`triggers file unreadable or invalid: ${invocation.triggersFile}`);
|
|
2734
|
+
return [];
|
|
2735
|
+
}
|
|
2736
|
+
return loaded;
|
|
2737
|
+
}
|
|
2738
|
+
async function runHookPrompt() {
|
|
2739
|
+
try {
|
|
2740
|
+
const invocation = hookInvocation();
|
|
2741
|
+
if (invocation === null) return;
|
|
2742
|
+
const { vaultRoot: vaultRoot2 } = invocation;
|
|
2743
|
+
let event = null;
|
|
2744
|
+
if (invocation.harness === "kiro-ide") {
|
|
2745
|
+
event = kiroIdePromptEvent();
|
|
2746
|
+
} else if (invocation.harness !== void 0) {
|
|
2747
|
+
diag2(`unknown harness "${String(invocation.harness)}" \u2014 reading the neutral stdin event`);
|
|
2748
|
+
}
|
|
2749
|
+
event ??= parsePromptEvent(await readStdin());
|
|
2750
|
+
if (event === null) {
|
|
2751
|
+
diag2("stdin is not a prompt event ({session_id, prompt})");
|
|
2752
|
+
return;
|
|
2753
|
+
}
|
|
2754
|
+
const config = await loadVaultConfig(vaultRoot2);
|
|
2755
|
+
const scope = invocation.scope ?? resolveScope(config, event.cwd);
|
|
2756
|
+
const { triggers, duplicates } = effectiveTriggers(
|
|
2757
|
+
config,
|
|
2758
|
+
scope,
|
|
2759
|
+
resolveFileTriggers(invocation)
|
|
2760
|
+
);
|
|
2761
|
+
for (const id of duplicates) {
|
|
2762
|
+
diag2(`duplicate trigger id "${id}" \u2014 later occurrence ignored`);
|
|
2763
|
+
}
|
|
2764
|
+
const matches = matchPromptTriggers(event.prompt, triggers);
|
|
2765
|
+
for (const match of dedupeMatches(hookStateDir(), event.session_id, matches)) {
|
|
2766
|
+
console.log(match.text);
|
|
2767
|
+
}
|
|
2768
|
+
} catch (err) {
|
|
2769
|
+
diag2(err instanceof Error ? err.message : String(err));
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
async function runHookPretool() {
|
|
2773
|
+
try {
|
|
2774
|
+
const invocation = hookInvocation();
|
|
2775
|
+
if (invocation === null) return;
|
|
2776
|
+
const { vaultRoot: vaultRoot2 } = invocation;
|
|
2777
|
+
let format = "neutral";
|
|
2778
|
+
if (invocation.harness === "claude") {
|
|
2779
|
+
format = "claude";
|
|
2780
|
+
} else if (invocation.harness !== void 0) {
|
|
2781
|
+
diag2(`unknown harness "${String(invocation.harness)}" \u2014 emitting the neutral contract`);
|
|
2782
|
+
}
|
|
2783
|
+
const event = parsePretoolEvent(await readStdin());
|
|
2784
|
+
if (event === null) {
|
|
2785
|
+
diag2("stdin is not a pretool event ({session_id, tool_name})");
|
|
2786
|
+
return;
|
|
2787
|
+
}
|
|
2788
|
+
const config = await loadVaultConfig(vaultRoot2);
|
|
2789
|
+
const scope = invocation.scope ?? resolveScope(config, event.cwd);
|
|
2790
|
+
const { triggers, duplicates } = effectiveTriggers(
|
|
2791
|
+
config,
|
|
2792
|
+
scope,
|
|
2793
|
+
resolveFileTriggers(invocation)
|
|
2794
|
+
);
|
|
2795
|
+
for (const id of duplicates) {
|
|
2796
|
+
diag2(`duplicate trigger id "${id}" \u2014 later occurrence ignored`);
|
|
2797
|
+
}
|
|
2798
|
+
const matches = matchPretoolTriggers(event.tool_name, event.tool_input, triggers, event.cwd);
|
|
2799
|
+
const { fired, skipped } = evaluateMatches(matches, vaultRoot2);
|
|
2800
|
+
for (const id of skipped) {
|
|
2801
|
+
diag2(`trigger "${id}": unknown or unevaluable predicate \u2014 skipped`);
|
|
2802
|
+
}
|
|
2803
|
+
const rendered = renderPretool(
|
|
2804
|
+
dedupePretoolMatches(hookStateDir(), event.session_id, fired),
|
|
2805
|
+
format
|
|
2806
|
+
);
|
|
2807
|
+
for (const line of rendered.stderr) {
|
|
2808
|
+
console.error(line);
|
|
2809
|
+
}
|
|
2810
|
+
if (rendered.stdout !== null) {
|
|
2811
|
+
console.log(rendered.stdout);
|
|
2812
|
+
}
|
|
2813
|
+
} catch (err) {
|
|
2814
|
+
diag2(err instanceof Error ? err.message : String(err));
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2817
|
+
async function runHookPosttool() {
|
|
2818
|
+
try {
|
|
2819
|
+
const invocation = hookInvocation();
|
|
2820
|
+
if (invocation === null) return;
|
|
2821
|
+
let format = "neutral";
|
|
2822
|
+
if (invocation.harness === "claude") {
|
|
2823
|
+
format = "claude";
|
|
2824
|
+
} else if (invocation.harness !== void 0) {
|
|
2825
|
+
diag2(`unknown harness "${String(invocation.harness)}" \u2014 emitting the neutral contract`);
|
|
2826
|
+
}
|
|
2827
|
+
const event = parsePretoolEvent(await readStdin());
|
|
2828
|
+
if (event === null) {
|
|
2829
|
+
diag2("stdin is not a posttool event ({session_id, tool_name})");
|
|
2830
|
+
return;
|
|
2831
|
+
}
|
|
2832
|
+
if (!vaultPathTouched(event.tool_input, invocation.vaultRoot, event.cwd)) return;
|
|
2833
|
+
const findings = await validateVault(invocation.vaultRoot);
|
|
2834
|
+
let synced = false;
|
|
2835
|
+
if (!hasErrors(findings)) {
|
|
2836
|
+
try {
|
|
2837
|
+
await syncVault(invocation.vaultRoot);
|
|
2838
|
+
synced = true;
|
|
2839
|
+
} catch (err) {
|
|
2840
|
+
diag2(`sync failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
const rendered = renderPosttool(findings, synced, format);
|
|
2844
|
+
if (rendered !== null) {
|
|
2845
|
+
console.log(rendered);
|
|
2846
|
+
}
|
|
2847
|
+
} catch (err) {
|
|
2848
|
+
diag2(err instanceof Error ? err.message : String(err));
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2851
|
+
async function readStdin() {
|
|
2852
|
+
process.stdin.setEncoding("utf8");
|
|
2853
|
+
let input = "";
|
|
2854
|
+
for await (const chunk of process.stdin) {
|
|
2855
|
+
input += chunk;
|
|
2856
|
+
}
|
|
2857
|
+
return input;
|
|
2858
|
+
}
|
|
2859
|
+
function diag2(message) {
|
|
2860
|
+
console.error(`kmd hook: ${message}`);
|
|
2861
|
+
}
|
|
2862
|
+
var DEFAULT_TRIGGERS, SESSION_STATE_MAX_AGE_MS, KIRO_IDE_BUCKET_MS, ALL_SCOPES_KEY, PATCH_FILE_RE;
|
|
2863
|
+
var init_hook = __esm({
|
|
2864
|
+
"../cli/src/hook.ts"() {
|
|
2865
|
+
"use strict";
|
|
2866
|
+
init_database();
|
|
2013
2867
|
init_vault_config();
|
|
2868
|
+
init_frontmatter();
|
|
2869
|
+
init_sync();
|
|
2870
|
+
init_validate();
|
|
2871
|
+
DEFAULT_TRIGGERS = [];
|
|
2872
|
+
SESSION_STATE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
2873
|
+
KIRO_IDE_BUCKET_MS = 30 * 60 * 1e3;
|
|
2874
|
+
ALL_SCOPES_KEY = "_all";
|
|
2875
|
+
PATCH_FILE_RE = /^\*\*\* (?:Add|Update|Delete) File: (.+)$/gm;
|
|
2014
2876
|
}
|
|
2015
2877
|
});
|
|
2016
2878
|
|
|
2017
2879
|
// bin/kmd.ts
|
|
2018
|
-
import { parseArgs as
|
|
2880
|
+
import { parseArgs as parseArgs3 } from "node:util";
|
|
2881
|
+
process.removeAllListeners("warning");
|
|
2882
|
+
process.on("warning", (warning) => {
|
|
2883
|
+
if (warning.name !== "ExperimentalWarning") {
|
|
2884
|
+
console.error(warning.stack ?? `${warning.name}: ${warning.message}`);
|
|
2885
|
+
}
|
|
2886
|
+
});
|
|
2019
2887
|
var USAGE = `usage: kmd <command> [options]
|
|
2020
2888
|
|
|
2021
2889
|
commands:
|
|
2890
|
+
init [<dir>] [-y] scaffold a fresh vault (no dir: current directory \u2014 TTY prompt, or -y)
|
|
2022
2891
|
sync vault \u2192 index sync (runs validate first)
|
|
2023
2892
|
validate [<path>] deterministic vault checker (default: $WIKI_VAULT)
|
|
2024
2893
|
mcp [<vault-root>] start the stdio MCP server (default: $WIKI_VAULT)
|
|
2025
2894
|
config [<vault-root>] print vault + index resolution; with no vault, list known vaults
|
|
2026
2895
|
db reset [<vault-root>] delete the vault's index (default: $WIKI_VAULT)
|
|
2896
|
+
hook <prompt|pretool|posttool> [<vault-root>] [--scope <s>] [--harness <claude|kiro-ide>] [--triggers <file>]
|
|
2897
|
+
harness gate engine: JSON event on stdin, decision/context on stdout;
|
|
2898
|
+
posttool auto-runs validate + sync after a vault write
|
|
2027
2899
|
|
|
2028
2900
|
options:
|
|
2029
2901
|
--version print version
|
|
2030
2902
|
--help show this help`;
|
|
2031
|
-
var { positionals, values } =
|
|
2903
|
+
var { positionals, values } = parseArgs3({
|
|
2032
2904
|
args: process.argv.slice(2),
|
|
2033
2905
|
allowPositionals: true,
|
|
2034
2906
|
strict: false,
|
|
2035
2907
|
options: {
|
|
2036
2908
|
version: { type: "boolean", short: "v" },
|
|
2037
|
-
help: { type: "boolean", short: "h" }
|
|
2909
|
+
help: { type: "boolean", short: "h" },
|
|
2910
|
+
yes: { type: "boolean", short: "y" }
|
|
2038
2911
|
}
|
|
2039
2912
|
});
|
|
2040
2913
|
var command = values.version ? "--version" : values.help ? "--help" : positionals[0];
|
|
@@ -2046,6 +2919,11 @@ function applyVaultRoot(positionalIndex) {
|
|
|
2046
2919
|
}
|
|
2047
2920
|
async function run() {
|
|
2048
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
|
+
}
|
|
2049
2927
|
case "sync": {
|
|
2050
2928
|
const { runSyncCommand: runSyncCommand2 } = await Promise.resolve().then(() => (init_cli(), cli_exports));
|
|
2051
2929
|
await runSyncCommand2();
|
|
@@ -2081,13 +2959,34 @@ async function run() {
|
|
|
2081
2959
|
}
|
|
2082
2960
|
break;
|
|
2083
2961
|
}
|
|
2962
|
+
case "hook": {
|
|
2963
|
+
const sub = positionals[1];
|
|
2964
|
+
if (sub === "prompt") {
|
|
2965
|
+
const { runHookPrompt: runHookPrompt2 } = await Promise.resolve().then(() => (init_hook(), hook_exports));
|
|
2966
|
+
await runHookPrompt2();
|
|
2967
|
+
} else if (sub === "pretool") {
|
|
2968
|
+
const { runHookPretool: runHookPretool2 } = await Promise.resolve().then(() => (init_hook(), hook_exports));
|
|
2969
|
+
await runHookPretool2();
|
|
2970
|
+
} else if (sub === "posttool") {
|
|
2971
|
+
const { runHookPosttool: runHookPosttool2 } = await Promise.resolve().then(() => (init_hook(), hook_exports));
|
|
2972
|
+
await runHookPosttool2();
|
|
2973
|
+
} else if (sub) {
|
|
2974
|
+
console.error(`kmd hook: unknown event: ${sub}`);
|
|
2975
|
+
} else {
|
|
2976
|
+
console.error(
|
|
2977
|
+
"usage: kmd hook <prompt|pretool|posttool> [<vault-root>] [--scope <scope>] [--harness <claude|kiro-ide>]"
|
|
2978
|
+
);
|
|
2979
|
+
process.exit(2);
|
|
2980
|
+
}
|
|
2981
|
+
break;
|
|
2982
|
+
}
|
|
2084
2983
|
case "--version":
|
|
2085
2984
|
case "-v": {
|
|
2086
|
-
const { readFileSync } = await import("node:fs");
|
|
2087
|
-
const { join:
|
|
2985
|
+
const { readFileSync: readFileSync2 } = await import("node:fs");
|
|
2986
|
+
const { join: join11, dirname: dirname4 } = await import("node:path");
|
|
2088
2987
|
const { fileURLToPath } = await import("node:url");
|
|
2089
2988
|
const pkgDir = dirname4(dirname4(fileURLToPath(import.meta.url)));
|
|
2090
|
-
const pkg = JSON.parse(
|
|
2989
|
+
const pkg = JSON.parse(readFileSync2(join11(pkgDir, "package.json"), "utf8"));
|
|
2091
2990
|
console.log(pkg.version);
|
|
2092
2991
|
break;
|
|
2093
2992
|
}
|
|
@@ -2099,6 +2998,11 @@ async function run() {
|
|
|
2099
2998
|
break;
|
|
2100
2999
|
}
|
|
2101
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
|
+
}
|
|
2102
3006
|
console.error(`unknown command: ${command}
|
|
2103
3007
|
|
|
2104
3008
|
${USAGE}`);
|