clawvault 1.11.2 → 2.0.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.
Files changed (52) hide show
  1. package/README.md +135 -1
  2. package/bin/clawvault.js +51 -1252
  3. package/bin/command-registration.test.js +148 -0
  4. package/bin/command-runtime.js +42 -0
  5. package/bin/command-runtime.test.js +102 -0
  6. package/bin/help-contract.test.js +23 -0
  7. package/bin/register-core-commands.js +139 -0
  8. package/bin/register-maintenance-commands.js +137 -0
  9. package/bin/register-query-commands.js +225 -0
  10. package/bin/register-resilience-commands.js +147 -0
  11. package/bin/register-session-lifecycle-commands.js +204 -0
  12. package/bin/register-template-commands.js +72 -0
  13. package/bin/register-vault-operations-commands.js +295 -0
  14. package/bin/test-helpers/cli-command-fixtures.js +94 -0
  15. package/dashboard/lib/graph-diff.js +3 -1
  16. package/dashboard/lib/graph-diff.test.js +19 -0
  17. package/dashboard/lib/vault-parser.js +330 -26
  18. package/dashboard/lib/vault-parser.test.js +191 -11
  19. package/dashboard/public/app.js +22 -9
  20. package/dist/chunk-MXSSG3QU.js +42 -0
  21. package/dist/chunk-O5V7SD5C.js +398 -0
  22. package/dist/chunk-PAYUH64O.js +284 -0
  23. package/dist/{chunk-3HFB7EMU.js → chunk-QFBKWDYR.js} +12 -0
  24. package/dist/{chunk-UBRYOIII.js → chunk-TBVI4N53.js} +210 -21
  25. package/dist/chunk-TXO34J3O.js +56 -0
  26. package/dist/commands/compat.d.ts +28 -0
  27. package/dist/commands/compat.js +10 -0
  28. package/dist/commands/context.d.ts +2 -33
  29. package/dist/commands/context.js +3 -2
  30. package/dist/commands/doctor.js +61 -3
  31. package/dist/commands/entities.d.ts +1 -0
  32. package/dist/commands/entities.js +4 -4
  33. package/dist/commands/graph.d.ts +21 -0
  34. package/dist/commands/graph.js +10 -0
  35. package/dist/commands/link.d.ts +1 -0
  36. package/dist/commands/link.js +14 -5
  37. package/dist/commands/sleep.js +7 -6
  38. package/dist/commands/status.d.ts +6 -0
  39. package/dist/commands/status.js +63 -3
  40. package/dist/commands/wake.js +5 -4
  41. package/dist/context-COo8oq1k.d.ts +45 -0
  42. package/dist/index.d.ts +63 -2
  43. package/dist/index.js +53 -15
  44. package/dist/lib/config.d.ts +6 -1
  45. package/dist/lib/config.js +7 -3
  46. package/hooks/clawvault/HOOK.md +6 -1
  47. package/hooks/clawvault/handler.js +44 -3
  48. package/hooks/clawvault/handler.test.js +161 -0
  49. package/package.json +34 -2
  50. package/dashboard/public/graph.js +0 -376
  51. package/dashboard/public/style.css +0 -154
  52. package/dist/chunk-4KDZZW4X.js +0 -13
@@ -0,0 +1,284 @@
1
+ // src/commands/compat.ts
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+ import matter from "gray-matter";
5
+ import { spawnSync } from "child_process";
6
+ import { fileURLToPath } from "url";
7
+ var REQUIRED_HOOK_EVENTS = ["gateway:startup", "command:new", "session:start"];
8
+ var REQUIRED_HOOK_BIN = "clawvault";
9
+ function readOptionalFile(filePath) {
10
+ try {
11
+ if (!fs.existsSync(filePath)) return null;
12
+ return fs.readFileSync(filePath, "utf-8");
13
+ } catch {
14
+ return null;
15
+ }
16
+ }
17
+ function resolveProjectFile(relativePath, baseDir) {
18
+ if (baseDir) {
19
+ return path.resolve(baseDir, relativePath);
20
+ }
21
+ const fromCwd = path.resolve(process.cwd(), relativePath);
22
+ if (fs.existsSync(fromCwd)) {
23
+ return fromCwd;
24
+ }
25
+ return fileURLToPath(new URL(`../../${relativePath}`, import.meta.url));
26
+ }
27
+ function checkOpenClawCli() {
28
+ const result = spawnSync("openclaw", ["--version"], { stdio: "ignore" });
29
+ if (result.error) {
30
+ return {
31
+ label: "openclaw CLI available",
32
+ status: "warn",
33
+ detail: "openclaw binary not found",
34
+ hint: "Install OpenClaw CLI to enable hook runtime validation."
35
+ };
36
+ }
37
+ if (typeof result.status === "number" && result.status !== 0) {
38
+ return {
39
+ label: "openclaw CLI available",
40
+ status: "warn",
41
+ detail: `openclaw --version exited with code ${result.status}`,
42
+ hint: "Ensure OpenClaw CLI is installed and runnable in PATH."
43
+ };
44
+ }
45
+ if (typeof result.signal === "string" && result.signal.length > 0) {
46
+ return {
47
+ label: "openclaw CLI available",
48
+ status: "warn",
49
+ detail: `openclaw --version terminated by signal ${result.signal}`,
50
+ hint: "Ensure OpenClaw CLI can execute normally in PATH."
51
+ };
52
+ }
53
+ return { label: "openclaw CLI available", status: "ok" };
54
+ }
55
+ function checkPackageHookRegistration(options) {
56
+ const packageRaw = readOptionalFile(resolveProjectFile("package.json", options.baseDir));
57
+ if (!packageRaw) {
58
+ return {
59
+ label: "package hook registration",
60
+ status: "error",
61
+ detail: "package.json not found"
62
+ };
63
+ }
64
+ try {
65
+ const parsed = JSON.parse(packageRaw);
66
+ const registeredHooks = parsed.openclaw?.hooks ?? [];
67
+ if (registeredHooks.includes("./hooks/clawvault")) {
68
+ return {
69
+ label: "package hook registration",
70
+ status: "ok",
71
+ detail: "./hooks/clawvault"
72
+ };
73
+ }
74
+ return {
75
+ label: "package hook registration",
76
+ status: "error",
77
+ detail: "Missing ./hooks/clawvault in package openclaw.hooks"
78
+ };
79
+ } catch (err) {
80
+ return {
81
+ label: "package hook registration",
82
+ status: "error",
83
+ detail: err?.message || "Unable to parse package.json"
84
+ };
85
+ }
86
+ }
87
+ function checkHookManifest(options) {
88
+ const hookRaw = readOptionalFile(resolveProjectFile("hooks/clawvault/HOOK.md", options.baseDir));
89
+ if (!hookRaw) {
90
+ return {
91
+ label: "hook manifest",
92
+ status: "error",
93
+ detail: "HOOK.md not found"
94
+ };
95
+ }
96
+ try {
97
+ const parsed = matter(hookRaw);
98
+ const openclaw = parsed.data?.metadata?.openclaw;
99
+ const events = Array.isArray(openclaw?.events) ? openclaw?.events ?? [] : [];
100
+ const missingEvents = REQUIRED_HOOK_EVENTS.filter((event) => !events.includes(event));
101
+ if (missingEvents.length === 0) {
102
+ return {
103
+ label: "hook manifest events",
104
+ status: "ok",
105
+ detail: events.join(", ")
106
+ };
107
+ }
108
+ return {
109
+ label: "hook manifest events",
110
+ status: "error",
111
+ detail: `Missing events: ${missingEvents.join(", ")}`
112
+ };
113
+ } catch (err) {
114
+ return {
115
+ label: "hook manifest events",
116
+ status: "error",
117
+ detail: err?.message || "Unable to parse HOOK.md frontmatter"
118
+ };
119
+ }
120
+ }
121
+ function checkHookManifestRequirements(options) {
122
+ const hookRaw = readOptionalFile(resolveProjectFile("hooks/clawvault/HOOK.md", options.baseDir));
123
+ if (!hookRaw) {
124
+ return {
125
+ label: "hook manifest requirements",
126
+ status: "error",
127
+ detail: "HOOK.md not found"
128
+ };
129
+ }
130
+ try {
131
+ const parsed = matter(hookRaw);
132
+ const requiresBins = parsed.data?.metadata?.openclaw?.requires?.bins;
133
+ const bins = Array.isArray(requiresBins) ? requiresBins : [];
134
+ if (bins.includes(REQUIRED_HOOK_BIN)) {
135
+ return {
136
+ label: "hook manifest requirements",
137
+ status: "ok",
138
+ detail: `bins: ${bins.join(", ")}`
139
+ };
140
+ }
141
+ return {
142
+ label: "hook manifest requirements",
143
+ status: "warn",
144
+ detail: `Missing required hook bin "${REQUIRED_HOOK_BIN}"`,
145
+ hint: 'Add metadata.openclaw.requires.bins: ["clawvault"] to hooks/clawvault/HOOK.md.'
146
+ };
147
+ } catch (err) {
148
+ return {
149
+ label: "hook manifest requirements",
150
+ status: "error",
151
+ detail: err?.message || "Unable to parse HOOK.md frontmatter"
152
+ };
153
+ }
154
+ }
155
+ function checkHookHandlerSafety(options) {
156
+ const handlerRaw = readOptionalFile(resolveProjectFile("hooks/clawvault/handler.js", options.baseDir));
157
+ if (!handlerRaw) {
158
+ return {
159
+ label: "hook handler script",
160
+ status: "error",
161
+ detail: "handler.js not found"
162
+ };
163
+ }
164
+ const usesExecFileSync = handlerRaw.includes("execFileSync");
165
+ const usesExecSync = /\bexecSync\b/.test(handlerRaw);
166
+ const enablesShell = /\bshell\s*:\s*true\b/.test(handlerRaw);
167
+ const delegatesAutoProfile = /['"]--profile['"]\s*,\s*['"]auto['"]/.test(handlerRaw);
168
+ const violations = [];
169
+ if (!usesExecFileSync || usesExecSync) {
170
+ violations.push("execFileSync-only execution path");
171
+ }
172
+ if (enablesShell) {
173
+ violations.push("shell:false execution option");
174
+ }
175
+ if (!delegatesAutoProfile) {
176
+ violations.push("shared context profile delegation (--profile auto)");
177
+ }
178
+ if (violations.length > 0) {
179
+ return {
180
+ label: "hook handler safety",
181
+ status: "warn",
182
+ detail: `Missing conventions: ${violations.join(", ")}`,
183
+ hint: "Use execFileSync (no shell), avoid execSync, and delegate profile inference via --profile auto."
184
+ };
185
+ }
186
+ return { label: "hook handler safety", status: "ok" };
187
+ }
188
+ function checkSkillMetadata(options) {
189
+ const skillRaw = readOptionalFile(resolveProjectFile("SKILL.md", options.baseDir));
190
+ if (!skillRaw) {
191
+ return {
192
+ label: "skill metadata",
193
+ status: "warn",
194
+ detail: "SKILL.md not found",
195
+ hint: "Ensure SKILL.md is present for OpenClaw skill distribution."
196
+ };
197
+ }
198
+ let hasOpenClawMetadata = false;
199
+ let parseError;
200
+ try {
201
+ const parsed = matter(skillRaw);
202
+ const frontmatter = parsed.data ?? {};
203
+ const metadata = frontmatter.metadata && typeof frontmatter.metadata === "object" && !Array.isArray(frontmatter.metadata) ? frontmatter.metadata : void 0;
204
+ hasOpenClawMetadata = Boolean(
205
+ metadata && typeof metadata.openclaw === "object" && metadata.openclaw !== null || typeof frontmatter.openclaw === "object" && frontmatter.openclaw !== null
206
+ );
207
+ } catch {
208
+ parseError = "Unable to parse SKILL.md frontmatter";
209
+ hasOpenClawMetadata = false;
210
+ }
211
+ if (!hasOpenClawMetadata) {
212
+ hasOpenClawMetadata = /"openclaw"\s*:/.test(skillRaw);
213
+ }
214
+ if (!hasOpenClawMetadata) {
215
+ const detail = parseError ? `${parseError} (or missing metadata.openclaw)` : "Missing metadata.openclaw in SKILL.md";
216
+ return {
217
+ label: "skill metadata",
218
+ status: "warn",
219
+ detail,
220
+ hint: "Add metadata.openclaw to SKILL.md frontmatter for OpenClaw compatibility."
221
+ };
222
+ }
223
+ return { label: "skill metadata", status: "ok" };
224
+ }
225
+ function checkOpenClawCompatibility(options = {}) {
226
+ const checks = [
227
+ checkOpenClawCli(),
228
+ checkPackageHookRegistration(options),
229
+ checkHookManifest(options),
230
+ checkHookManifestRequirements(options),
231
+ checkHookHandlerSafety(options),
232
+ checkSkillMetadata(options)
233
+ ];
234
+ const warnings = checks.filter((check) => check.status === "warn").length;
235
+ const errors = checks.filter((check) => check.status === "error").length;
236
+ return {
237
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
238
+ checks,
239
+ warnings,
240
+ errors
241
+ };
242
+ }
243
+ function formatCompatibilityReport(report) {
244
+ const lines = [];
245
+ lines.push("OpenClaw Compatibility Report");
246
+ lines.push("-".repeat(34));
247
+ lines.push(`Generated: ${report.generatedAt}`);
248
+ lines.push("");
249
+ for (const check of report.checks) {
250
+ const prefix = check.status === "ok" ? "\u2713" : check.status === "warn" ? "\u26A0" : "\u2717";
251
+ lines.push(`${prefix} ${check.label}${check.detail ? ` \u2014 ${check.detail}` : ""}`);
252
+ if (check.hint) {
253
+ lines.push(` ${check.hint}`);
254
+ }
255
+ }
256
+ lines.push("");
257
+ lines.push(`Warnings: ${report.warnings}`);
258
+ lines.push(`Errors: ${report.errors}`);
259
+ return lines.join("\n");
260
+ }
261
+ function compatibilityExitCode(report, options = {}) {
262
+ if (report.errors > 0) {
263
+ return 1;
264
+ }
265
+ if (options.strict && report.warnings > 0) {
266
+ return 1;
267
+ }
268
+ return 0;
269
+ }
270
+ async function compatCommand(options = {}) {
271
+ const report = checkOpenClawCompatibility({ baseDir: options.baseDir });
272
+ if (options.json) {
273
+ console.log(JSON.stringify(report, null, 2));
274
+ } else {
275
+ console.log(formatCompatibilityReport(report));
276
+ }
277
+ return report;
278
+ }
279
+
280
+ export {
281
+ checkOpenClawCompatibility,
282
+ compatibilityExitCode,
283
+ compatCommand
284
+ };
@@ -9,6 +9,9 @@ import {
9
9
  qmdEmbed,
10
10
  qmdUpdate
11
11
  } from "./chunk-MIIXBNO3.js";
12
+ import {
13
+ buildOrUpdateMemoryGraphIndex
14
+ } from "./chunk-O5V7SD5C.js";
12
15
 
13
16
  // src/lib/vault.ts
14
17
  import * as fs from "fs";
@@ -72,6 +75,7 @@ var ClawVault = class {
72
75
  qmdRoot: this.getQmdRoot()
73
76
  };
74
77
  fs.writeFileSync(configPath, JSON.stringify(meta, null, 2));
78
+ await this.syncMemoryGraphIndex({ forceFull: true });
75
79
  this.initialized = true;
76
80
  }
77
81
  /**
@@ -116,6 +120,7 @@ var ClawVault = class {
116
120
  }
117
121
  }
118
122
  await this.saveIndex();
123
+ await this.syncMemoryGraphIndex();
119
124
  return this.search.size;
120
125
  }
121
126
  /**
@@ -180,6 +185,7 @@ var ClawVault = class {
180
185
  if (doc) {
181
186
  this.search.addDocument(doc);
182
187
  await this.saveIndex();
188
+ await this.syncMemoryGraphIndex();
183
189
  }
184
190
  if (triggerUpdate || triggerEmbed) {
185
191
  qmdUpdate(this.getQmdCollection());
@@ -622,6 +628,12 @@ var ClawVault = class {
622
628
  }
623
629
  }
624
630
  }
631
+ async syncMemoryGraphIndex(options = {}) {
632
+ try {
633
+ await buildOrUpdateMemoryGraphIndex(this.config.path, options);
634
+ } catch {
635
+ }
636
+ }
625
637
  generateReadme() {
626
638
  return `# ${this.config.name} \u{1F418}
627
639
 
@@ -1,6 +1,9 @@
1
1
  import {
2
2
  ClawVault
3
- } from "./chunk-3HFB7EMU.js";
3
+ } from "./chunk-QFBKWDYR.js";
4
+ import {
5
+ getMemoryGraph
6
+ } from "./chunk-O5V7SD5C.js";
4
7
 
5
8
  // src/commands/context.ts
6
9
  import * as path2 from "path";
@@ -86,6 +89,34 @@ function fitWithinBudget(items, budget) {
86
89
  return fitted;
87
90
  }
88
91
 
92
+ // src/lib/context-profile.ts
93
+ var INCIDENT_PROMPT_RE = /\b(outage|incident|sev[1-4]|p[0-3]|broken|failure|urgent|rollback|hotfix|degraded)\b/i;
94
+ var PLANNING_PROMPT_RE = /\b(plan|planning|design|architecture|roadmap|proposal|spec|migrate|migration|approach)\b/i;
95
+ var HANDOFF_PROMPT_RE = /\b(resume|continue|handoff|pick up|where (did|was) i|last session)\b/i;
96
+ function inferContextProfile(task) {
97
+ const normalizedTask = task.trim();
98
+ if (!normalizedTask) {
99
+ return "default";
100
+ }
101
+ if (INCIDENT_PROMPT_RE.test(normalizedTask)) return "incident";
102
+ if (HANDOFF_PROMPT_RE.test(normalizedTask)) return "handoff";
103
+ if (PLANNING_PROMPT_RE.test(normalizedTask)) return "planning";
104
+ return "default";
105
+ }
106
+ function normalizeContextProfileInput(profile) {
107
+ if (profile === "planning" || profile === "incident" || profile === "handoff" || profile === "auto") {
108
+ return profile;
109
+ }
110
+ return "default";
111
+ }
112
+ function resolveContextProfile(profile, task) {
113
+ const normalized = normalizeContextProfileInput(profile);
114
+ if (normalized === "auto") {
115
+ return inferContextProfile(task);
116
+ }
117
+ return normalized;
118
+ }
119
+
89
120
  // src/commands/context.ts
90
121
  var DEFAULT_LIMIT = 5;
91
122
  var MAX_SNIPPET_LENGTH = 320;
@@ -157,6 +188,24 @@ function formatContextMarkdown(task, entries) {
157
188
  }
158
189
  return output.trimEnd();
159
190
  }
191
+ var PROFILE_ORDERING = {
192
+ default: {
193
+ order: ["red", "daily", "search", "graph", "yellow", "green"],
194
+ caps: {}
195
+ },
196
+ planning: {
197
+ order: ["search", "graph", "red", "yellow", "daily", "green"],
198
+ caps: { observation: 12, graph: 12 }
199
+ },
200
+ incident: {
201
+ order: ["red", "search", "yellow", "daily", "graph", "green"],
202
+ caps: { observation: 20, graph: 8 }
203
+ },
204
+ handoff: {
205
+ order: ["daily", "red", "yellow", "search", "graph", "green"],
206
+ caps: { "daily-note": 2, observation: 15 }
207
+ }
208
+ };
160
209
  function extractKeywords(text) {
161
210
  const raw = text.toLowerCase().match(/[a-z0-9]+/g) ?? [];
162
211
  const seen = /* @__PURE__ */ new Set();
@@ -257,8 +306,7 @@ function getTargetDailyDates(now = /* @__PURE__ */ new Date()) {
257
306
  const yesterday = yesterdayDate.toISOString().slice(0, 10);
258
307
  return [today, yesterday];
259
308
  }
260
- async function buildDailyContextItems(vault) {
261
- const allDocuments = await vault.list();
309
+ function buildDailyContextItems(vaultPath, allDocuments) {
262
310
  const targetDates = getTargetDailyDates();
263
311
  const targetDateSet = new Set(targetDates);
264
312
  const byDate = /* @__PURE__ */ new Map();
@@ -278,7 +326,7 @@ async function buildDailyContextItems(vault) {
278
326
  if (!document) {
279
327
  continue;
280
328
  }
281
- const relativePath = path2.relative(vault.getPath(), document.path).split(path2.sep).join("/");
329
+ const relativePath = path2.relative(vaultPath, document.path).split(path2.sep).join("/");
282
330
  const snippet = estimateSnippet(document.content);
283
331
  items.push({
284
332
  priority: 2,
@@ -290,7 +338,9 @@ async function buildDailyContextItems(vault) {
290
338
  snippet,
291
339
  modified: document.modified.toISOString(),
292
340
  age: formatRelativeAge(document.modified),
293
- source: "daily-note"
341
+ source: "daily-note",
342
+ signals: ["daily_recency"],
343
+ rationale: "Pinned daily note context (today/yesterday)."
294
344
  }
295
345
  });
296
346
  }
@@ -300,7 +350,7 @@ function buildObservationContextItems(vaultPath, queryKeywords) {
300
350
  const observationMarkdown = readObservations(vaultPath, OBSERVATION_LOOKBACK_DAYS);
301
351
  const parsed = parseObservationLines(observationMarkdown);
302
352
  const items = [];
303
- for (const observation of parsed) {
353
+ for (const [index, observation] of parsed.entries()) {
304
354
  const priority = observationPriorityToRank(observation.priority);
305
355
  const modifiedDate = asDate(observation.date, /* @__PURE__ */ new Date());
306
356
  const date = observation.date || modifiedDate.toISOString().slice(0, 10);
@@ -308,14 +358,16 @@ function buildObservationContextItems(vaultPath, queryKeywords) {
308
358
  items.push({
309
359
  priority,
310
360
  entry: {
311
- title: `${observation.priority} observation (${date})`,
361
+ title: `${observation.priority} observation (${date}) #${index + 1}`,
312
362
  path: `observations/${date}.md`,
313
363
  category: "observations",
314
364
  score: computeKeywordOverlapScore(queryKeywords, observation.content),
315
365
  snippet,
316
366
  modified: modifiedDate.toISOString(),
317
367
  age: formatRelativeAge(modifiedDate),
318
- source: "observation"
368
+ source: "observation",
369
+ signals: ["observation_priority", "keyword_overlap"],
370
+ rationale: `Observation priority ${observation.priority} matched task keywords.`
319
371
  }
320
372
  });
321
373
  }
@@ -332,7 +384,9 @@ function buildSearchContextItems(vault, results) {
332
384
  snippet: normalizeSnippet(result),
333
385
  modified: result.document.modified.toISOString(),
334
386
  age: formatRelativeAge(result.document.modified),
335
- source: "search"
387
+ source: "search",
388
+ signals: ["semantic_search"],
389
+ rationale: "Selected by semantic retrieval."
336
390
  };
337
391
  return {
338
392
  priority: 3,
@@ -340,6 +394,109 @@ function buildSearchContextItems(vault, results) {
340
394
  };
341
395
  });
342
396
  }
397
+ function toNoteNodeId(vaultPath, absolutePath) {
398
+ const relativePath = path2.relative(vaultPath, absolutePath).split(path2.sep).join("/");
399
+ const noteKey = relativePath.toLowerCase().endsWith(".md") ? relativePath.slice(0, -3) : relativePath;
400
+ return `note:${noteKey}`;
401
+ }
402
+ function buildGraphAdjacency(edges) {
403
+ const adjacency = /* @__PURE__ */ new Map();
404
+ for (const edge of edges) {
405
+ const sourceBucket = adjacency.get(edge.source) ?? [];
406
+ sourceBucket.push(edge);
407
+ adjacency.set(edge.source, sourceBucket);
408
+ const targetBucket = adjacency.get(edge.target) ?? [];
409
+ targetBucket.push(edge);
410
+ adjacency.set(edge.target, targetBucket);
411
+ }
412
+ return adjacency;
413
+ }
414
+ function edgeWeight(edge) {
415
+ if (edge.type === "frontmatter_relation") return 0.95;
416
+ if (edge.type === "wiki_link") return 0.8;
417
+ return 0.6;
418
+ }
419
+ function buildGraphContextItems(params) {
420
+ const { graph, vaultPath, documents, searchItems, limit } = params;
421
+ if (searchItems.length === 0 || graph.nodes.length === 0 || graph.edges.length === 0) {
422
+ return [];
423
+ }
424
+ const graphNodeById = new Map(graph.nodes.map((node) => [node.id, node]));
425
+ const adjacency = buildGraphAdjacency(graph.edges);
426
+ const docByNodeId = /* @__PURE__ */ new Map();
427
+ for (const document of documents) {
428
+ docByNodeId.set(toNoteNodeId(vaultPath, document.path), document);
429
+ }
430
+ const anchors = searchItems.map((item) => ({
431
+ item,
432
+ nodeId: toNoteNodeId(vaultPath, path2.join(vaultPath, item.entry.path))
433
+ })).filter((anchor) => graphNodeById.has(anchor.nodeId));
434
+ const candidates = /* @__PURE__ */ new Map();
435
+ for (const anchor of anchors) {
436
+ const connectedEdges = adjacency.get(anchor.nodeId) ?? [];
437
+ for (const edge of connectedEdges) {
438
+ const neighborId = edge.source === anchor.nodeId ? edge.target : edge.source;
439
+ if (neighborId === anchor.nodeId) continue;
440
+ const neighborNode = graphNodeById.get(neighborId);
441
+ if (!neighborNode || neighborNode.type === "tag") continue;
442
+ const neighborDoc = docByNodeId.get(neighborId);
443
+ const neighborPath = neighborDoc ? path2.relative(vaultPath, neighborDoc.path).split(path2.sep).join("/") : neighborNode.path ?? neighborNode.id;
444
+ const neighborTitle = neighborDoc?.title ?? neighborNode.title;
445
+ const modifiedAt = neighborDoc?.modified ?? (neighborNode.modifiedAt ? new Date(neighborNode.modifiedAt) : /* @__PURE__ */ new Date(0));
446
+ const snippet = neighborDoc?.content ? estimateSnippet(neighborDoc.content) : `Connected via ${edge.type}${edge.label ? ` (${edge.label})` : ""}.`;
447
+ const score = Math.max(0.05, Math.min(1, anchor.item.entry.score * edgeWeight(edge)));
448
+ const key = `${neighborId}|${edge.type}|${edge.label ?? ""}`;
449
+ const existing = candidates.get(key);
450
+ const candidate = {
451
+ priority: 3,
452
+ entry: {
453
+ title: neighborTitle,
454
+ path: neighborPath,
455
+ category: neighborDoc?.category ?? neighborNode.category,
456
+ score,
457
+ snippet,
458
+ modified: modifiedAt.toISOString(),
459
+ age: formatRelativeAge(modifiedAt),
460
+ source: "graph",
461
+ signals: ["graph_neighbor", `edge:${edge.type}`],
462
+ rationale: `Connected to "${anchor.item.entry.title}" via ${edge.type}${edge.label ? ` (${edge.label})` : ""}.`
463
+ }
464
+ };
465
+ if (!existing || existing.entry.score < candidate.entry.score) {
466
+ candidates.set(key, candidate);
467
+ }
468
+ }
469
+ }
470
+ return [...candidates.values()].sort((left, right) => right.entry.score - left.entry.score).slice(0, Math.max(limit, 1));
471
+ }
472
+ function dedupeContextItems(items) {
473
+ const deduped = /* @__PURE__ */ new Map();
474
+ for (const item of items) {
475
+ const key = `${item.entry.path}|${item.entry.source}|${item.entry.title}`;
476
+ const existing = deduped.get(key);
477
+ if (!existing || existing.entry.score < item.entry.score) {
478
+ deduped.set(key, item);
479
+ }
480
+ }
481
+ return [...deduped.values()];
482
+ }
483
+ function applySourceCaps(items, caps) {
484
+ const counts = {};
485
+ const capped = [];
486
+ for (const item of items) {
487
+ const source = item.entry.source;
488
+ const limit = caps[source];
489
+ if (limit !== void 0) {
490
+ const current = counts[source] ?? 0;
491
+ if (current >= limit) {
492
+ continue;
493
+ }
494
+ counts[source] = current + 1;
495
+ }
496
+ capped.push(item);
497
+ }
498
+ return capped;
499
+ }
343
500
  function renderEntryBlock(entry) {
344
501
  return `### ${entry.title} (${entry.source}, score: ${entry.score.toFixed(2)}, ${entry.age})
345
502
  ${entry.snippet}
@@ -404,30 +561,50 @@ async function buildContext(task, options) {
404
561
  const limit = Math.max(1, options.limit ?? DEFAULT_LIMIT);
405
562
  const recent = options.recent ?? true;
406
563
  const includeObservations = options.includeObservations ?? true;
564
+ const profile = resolveContextProfile(options.profile, normalizedTask);
407
565
  const queryKeywords = extractKeywords(normalizedTask);
566
+ const allDocuments = await vault.list();
408
567
  const searchResults = await vault.vsearch(normalizedTask, {
409
568
  limit,
410
569
  temporalBoost: recent
411
570
  });
412
571
  const searchItems = buildSearchContextItems(vault, searchResults);
413
- const dailyItems = await buildDailyContextItems(vault);
572
+ const dailyItems = buildDailyContextItems(vault.getPath(), allDocuments);
414
573
  const observationItems = includeObservations ? buildObservationContextItems(vault.getPath(), queryKeywords) : [];
574
+ const graph = await getMemoryGraph(vault.getPath());
575
+ const graphItems = buildGraphContextItems({
576
+ graph,
577
+ vaultPath: vault.getPath(),
578
+ documents: allDocuments,
579
+ searchItems,
580
+ limit
581
+ });
415
582
  const byScoreDesc = (left, right) => right.entry.score - left.entry.score;
416
583
  const redObservations = observationItems.filter((item) => item.priority === 1).sort(byScoreDesc);
417
584
  const yellowObservations = observationItems.filter((item) => item.priority === 4).sort(byScoreDesc);
418
585
  const greenObservations = observationItems.filter((item) => item.priority === 5).sort(byScoreDesc);
419
586
  const sortedDailyItems = [...dailyItems].sort(byScoreDesc);
420
587
  const sortedSearchItems = [...searchItems].sort(byScoreDesc);
421
- const ordered = [
422
- ...redObservations,
423
- ...sortedDailyItems,
424
- ...sortedSearchItems,
425
- ...yellowObservations,
426
- ...greenObservations
427
- ];
588
+ const sortedGraphItems = [...graphItems].sort(byScoreDesc);
589
+ const grouped = {
590
+ red: redObservations,
591
+ daily: sortedDailyItems,
592
+ search: sortedSearchItems,
593
+ graph: sortedGraphItems,
594
+ yellow: yellowObservations,
595
+ green: greenObservations
596
+ };
597
+ const ordering = PROFILE_ORDERING[profile];
598
+ const ordered = dedupeContextItems(
599
+ applySourceCaps(
600
+ ordering.order.flatMap((group) => grouped[group]),
601
+ ordering.caps
602
+ )
603
+ );
428
604
  const { context, markdown } = applyTokenBudget(ordered, normalizedTask, options.budget);
429
605
  return {
430
606
  task: normalizedTask,
607
+ profile,
431
608
  generated: (/* @__PURE__ */ new Date()).toISOString(),
432
609
  context,
433
610
  markdown
@@ -437,11 +614,19 @@ async function contextCommand(task, options) {
437
614
  const result = await buildContext(task, options);
438
615
  const format = options.format ?? "markdown";
439
616
  if (format === "json") {
617
+ const context = result.context.map((entry) => ({
618
+ ...entry,
619
+ explain: {
620
+ signals: entry.signals ?? [],
621
+ rationale: entry.rationale ?? ""
622
+ }
623
+ }));
440
624
  console.log(JSON.stringify({
441
625
  task: result.task,
626
+ profile: result.profile,
442
627
  generated: result.generated,
443
- count: result.context.length,
444
- context: result.context
628
+ count: context.length,
629
+ context
445
630
  }, null, 2));
446
631
  return;
447
632
  }
@@ -455,7 +640,7 @@ function parsePositiveInteger(raw, label) {
455
640
  return parsed;
456
641
  }
457
642
  function registerContextCommand(program) {
458
- program.command("context <task>").description("Generate task-relevant context for prompt injection").option("-n, --limit <n>", "Max results", "5").option("--format <format>", "Output format (markdown|json)", "markdown").option("--recent", "Boost recent documents (enabled by default)", true).option("--include-observations", "Include observation memories in output", true).option("--budget <number>", "Optional token budget for assembled context").option("-v, --vault <path>", "Vault path").action(async (task, rawOptions) => {
643
+ program.command("context <task>").description("Generate task-relevant context for prompt injection").option("-n, --limit <n>", "Max results", "5").option("--format <format>", "Output format (markdown|json)", "markdown").option("--recent", "Boost recent documents (enabled by default)", true).option("--include-observations", "Include observation memories in output", true).option("--budget <number>", "Optional token budget for assembled context").option("--profile <profile>", "Context profile (default|planning|incident|handoff|auto)", "default").option("-v, --vault <path>", "Vault path").action(async (task, rawOptions) => {
459
644
  const format = rawOptions.format === "json" ? "json" : "markdown";
460
645
  const budget = rawOptions.budget ? parsePositiveInteger(rawOptions.budget, "budget") : void 0;
461
646
  const limit = parsePositiveInteger(rawOptions.limit, "limit");
@@ -466,12 +651,16 @@ function registerContextCommand(program) {
466
651
  format,
467
652
  recent: rawOptions.recent ?? true,
468
653
  includeObservations: rawOptions.includeObservations ?? true,
469
- budget
654
+ budget,
655
+ profile: normalizeContextProfileInput(rawOptions.profile)
470
656
  });
471
657
  });
472
658
  }
473
659
 
474
660
  export {
661
+ inferContextProfile,
662
+ normalizeContextProfileInput,
663
+ resolveContextProfile,
475
664
  formatContextMarkdown,
476
665
  buildContext,
477
666
  contextCommand,