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.
- package/README.md +135 -1
- package/bin/clawvault.js +51 -1252
- package/bin/command-registration.test.js +148 -0
- package/bin/command-runtime.js +42 -0
- package/bin/command-runtime.test.js +102 -0
- package/bin/help-contract.test.js +23 -0
- package/bin/register-core-commands.js +139 -0
- package/bin/register-maintenance-commands.js +137 -0
- package/bin/register-query-commands.js +225 -0
- package/bin/register-resilience-commands.js +147 -0
- package/bin/register-session-lifecycle-commands.js +204 -0
- package/bin/register-template-commands.js +72 -0
- package/bin/register-vault-operations-commands.js +295 -0
- package/bin/test-helpers/cli-command-fixtures.js +94 -0
- package/dashboard/lib/graph-diff.js +3 -1
- package/dashboard/lib/graph-diff.test.js +19 -0
- package/dashboard/lib/vault-parser.js +330 -26
- package/dashboard/lib/vault-parser.test.js +191 -11
- package/dashboard/public/app.js +22 -9
- package/dist/chunk-MXSSG3QU.js +42 -0
- package/dist/chunk-O5V7SD5C.js +398 -0
- package/dist/chunk-PAYUH64O.js +284 -0
- package/dist/{chunk-3HFB7EMU.js → chunk-QFBKWDYR.js} +12 -0
- package/dist/{chunk-UBRYOIII.js → chunk-TBVI4N53.js} +210 -21
- package/dist/chunk-TXO34J3O.js +56 -0
- package/dist/commands/compat.d.ts +28 -0
- package/dist/commands/compat.js +10 -0
- package/dist/commands/context.d.ts +2 -33
- package/dist/commands/context.js +3 -2
- package/dist/commands/doctor.js +61 -3
- package/dist/commands/entities.d.ts +1 -0
- package/dist/commands/entities.js +4 -4
- package/dist/commands/graph.d.ts +21 -0
- package/dist/commands/graph.js +10 -0
- package/dist/commands/link.d.ts +1 -0
- package/dist/commands/link.js +14 -5
- package/dist/commands/sleep.js +7 -6
- package/dist/commands/status.d.ts +6 -0
- package/dist/commands/status.js +63 -3
- package/dist/commands/wake.js +5 -4
- package/dist/context-COo8oq1k.d.ts +45 -0
- package/dist/index.d.ts +63 -2
- package/dist/index.js +53 -15
- package/dist/lib/config.d.ts +6 -1
- package/dist/lib/config.js +7 -3
- package/hooks/clawvault/HOOK.md +6 -1
- package/hooks/clawvault/handler.js +44 -3
- package/hooks/clawvault/handler.test.js +161 -0
- package/package.json +34 -2
- package/dashboard/public/graph.js +0 -376
- package/dashboard/public/style.css +0 -154
- 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-
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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:
|
|
444
|
-
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,
|