clawvault 2.4.5 → 2.4.7
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/bin/clawvault.js +10 -0
- package/bin/command-registration.test.js +1 -1
- package/bin/help-contract.test.js +1 -0
- package/bin/register-config-route-commands.test.js +8 -1
- package/bin/register-core-commands.js +3 -3
- package/bin/register-kanban-commands.js +56 -0
- package/bin/register-kanban-commands.test.js +83 -0
- package/bin/register-project-commands.js +209 -0
- package/bin/register-project-commands.test.js +201 -0
- package/bin/register-query-commands.js +40 -0
- package/bin/register-task-commands.js +60 -25
- package/bin/register-task-commands.test.js +49 -4
- package/bin/test-helpers/cli-command-fixtures.js +15 -0
- package/dist/{chunk-3PJIGGWV.js → chunk-2CDEETQN.js} +1 -0
- package/dist/{chunk-B3SMJZIZ.js → chunk-2RK2AG32.js} +5 -5
- package/dist/chunk-5GZFTAL7.js +340 -0
- package/dist/{chunk-YIRWDQKA.js → chunk-6RQPD7X6.js} +3 -4
- package/dist/{chunk-HNMFXFYP.js → chunk-7OHQFMJK.js} +2 -1
- package/dist/{chunk-4JJL47IJ.js → chunk-C3PF7WBA.js} +2 -2
- package/dist/{chunk-JXY6T5R7.js → chunk-FW465EEA.js} +1 -1
- package/dist/{chunk-BI6SGGZP.js → chunk-G3OQJ2NQ.js} +1 -1
- package/dist/chunk-GSD4ALSI.js +724 -0
- package/dist/chunk-IOALNTAN.js +757 -0
- package/dist/chunk-ITPEXLHA.js +528 -0
- package/dist/chunk-J5EMBUPK.js +399 -0
- package/dist/chunk-K3CDT7IH.js +122 -0
- package/dist/{chunk-AHGUJG76.js → chunk-KCCHROBR.js} +13 -69
- package/dist/{chunk-U2ONVV7N.js → chunk-LMCC5OC7.js} +2 -2
- package/dist/{chunk-QALB2V3E.js → chunk-MQUJNOHK.js} +1 -1
- package/dist/{chunk-RXEIQ3KQ.js → chunk-TMZMN7OS.js} +334 -457
- package/dist/{chunk-HVTTYDCJ.js → chunk-VR5NE7PZ.js} +1 -1
- package/dist/{chunk-22WE3J4F.js → chunk-WIICLBNF.js} +35 -4
- package/dist/chunk-YCVDVI5B.js +273 -0
- package/dist/{chunk-NAMFB7ZA.js → chunk-Z2XBWN7A.js} +0 -2
- package/dist/commands/archive.js +3 -3
- package/dist/commands/backlog.js +1 -1
- package/dist/commands/blocked.js +1 -1
- package/dist/commands/canvas.d.ts +1 -14
- package/dist/commands/canvas.js +123 -1543
- package/dist/commands/context.js +5 -6
- package/dist/commands/doctor.js +5 -5
- package/dist/commands/inject.d.ts +2 -0
- package/dist/commands/inject.js +14 -0
- package/dist/commands/kanban.d.ts +63 -0
- package/dist/commands/kanban.js +21 -0
- package/dist/commands/migrate-observations.js +2 -2
- package/dist/commands/observe.js +8 -6
- package/dist/commands/project.d.ts +85 -0
- package/dist/commands/project.js +411 -0
- package/dist/commands/rebuild.js +7 -5
- package/dist/commands/reflect.js +5 -4
- package/dist/commands/replay.js +10 -7
- package/dist/commands/setup.d.ts +1 -1
- package/dist/commands/setup.js +2 -2
- package/dist/commands/sleep.d.ts +1 -1
- package/dist/commands/sleep.js +11 -8
- package/dist/commands/status.js +5 -5
- package/dist/commands/task.d.ts +20 -8
- package/dist/commands/task.js +11 -244
- package/dist/commands/wake.d.ts +1 -1
- package/dist/commands/wake.js +4 -4
- package/dist/index.d.ts +76 -106
- package/dist/index.js +99 -34
- package/dist/inject-x65KXWPk.d.ts +137 -0
- package/dist/lib/project-utils.d.ts +97 -0
- package/dist/lib/project-utils.js +19 -0
- package/dist/lib/task-utils.d.ts +48 -12
- package/dist/lib/task-utils.js +5 -1
- package/dist/{types-DMU3SuAV.d.ts → types-jjuYN2Xn.d.ts} +1 -1
- package/package.json +2 -2
- package/dist/chunk-DEFBIVQ3.js +0 -373
- package/dist/chunk-L3DJ36BZ.js +0 -40
- package/dist/chunk-UMMCYTJV.js +0 -105
|
@@ -0,0 +1,724 @@
|
|
|
1
|
+
import {
|
|
2
|
+
requestLlmCompletion,
|
|
3
|
+
resolveLlmProvider
|
|
4
|
+
} from "./chunk-K3CDT7IH.js";
|
|
5
|
+
import {
|
|
6
|
+
listConfig
|
|
7
|
+
} from "./chunk-ITPEXLHA.js";
|
|
8
|
+
import {
|
|
9
|
+
getMemoryGraph,
|
|
10
|
+
loadMemoryGraphIndex
|
|
11
|
+
} from "./chunk-ZZA73MFY.js";
|
|
12
|
+
|
|
13
|
+
// src/commands/inject.ts
|
|
14
|
+
import * as path2 from "path";
|
|
15
|
+
|
|
16
|
+
// src/lib/inject-utils.ts
|
|
17
|
+
import * as fs from "fs";
|
|
18
|
+
import * as path from "path";
|
|
19
|
+
import matter from "gray-matter";
|
|
20
|
+
var INJECTABLE_CATEGORIES = ["rules", "decisions", "preferences"];
|
|
21
|
+
var DEFAULT_CATEGORY_PRIORITY = {
|
|
22
|
+
rules: 100,
|
|
23
|
+
decisions: 80,
|
|
24
|
+
preferences: 60
|
|
25
|
+
};
|
|
26
|
+
var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
27
|
+
"a",
|
|
28
|
+
"an",
|
|
29
|
+
"and",
|
|
30
|
+
"are",
|
|
31
|
+
"as",
|
|
32
|
+
"at",
|
|
33
|
+
"be",
|
|
34
|
+
"by",
|
|
35
|
+
"for",
|
|
36
|
+
"from",
|
|
37
|
+
"in",
|
|
38
|
+
"is",
|
|
39
|
+
"it",
|
|
40
|
+
"of",
|
|
41
|
+
"on",
|
|
42
|
+
"or",
|
|
43
|
+
"that",
|
|
44
|
+
"the",
|
|
45
|
+
"this",
|
|
46
|
+
"to",
|
|
47
|
+
"with",
|
|
48
|
+
"you",
|
|
49
|
+
"your",
|
|
50
|
+
"we",
|
|
51
|
+
"our",
|
|
52
|
+
"their",
|
|
53
|
+
"they",
|
|
54
|
+
"them",
|
|
55
|
+
"i"
|
|
56
|
+
]);
|
|
57
|
+
function normalizeText(value) {
|
|
58
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, " ").replace(/\s+/g, " ").trim();
|
|
59
|
+
}
|
|
60
|
+
function normalizeScopeValue(value) {
|
|
61
|
+
return value.trim().toLowerCase().replace(/\s+/g, "-");
|
|
62
|
+
}
|
|
63
|
+
function toRelativePath(vaultPath, absolutePath) {
|
|
64
|
+
return path.relative(vaultPath, absolutePath).split(path.sep).join("/");
|
|
65
|
+
}
|
|
66
|
+
function toNoteNodeId(relativePath) {
|
|
67
|
+
const normalized = relativePath.toLowerCase().endsWith(".md") ? relativePath.slice(0, -3) : relativePath;
|
|
68
|
+
return `note:${normalized}`;
|
|
69
|
+
}
|
|
70
|
+
function extractHeadingTitle(content) {
|
|
71
|
+
const match = content.match(/^#\s+(.+)$/m);
|
|
72
|
+
return match?.[1]?.trim() || null;
|
|
73
|
+
}
|
|
74
|
+
function toStringArray(value) {
|
|
75
|
+
if (typeof value === "string") {
|
|
76
|
+
return value.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
77
|
+
}
|
|
78
|
+
if (Array.isArray(value)) {
|
|
79
|
+
return value.flatMap((entry) => typeof entry === "string" ? entry.split(",") : []).map((entry) => entry.trim()).filter(Boolean);
|
|
80
|
+
}
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
function parsePriority(frontmatter, category) {
|
|
84
|
+
const explicit = frontmatter.priority;
|
|
85
|
+
if (typeof explicit === "number" && Number.isFinite(explicit)) {
|
|
86
|
+
return Math.max(1, Math.round(explicit));
|
|
87
|
+
}
|
|
88
|
+
if (typeof explicit === "string") {
|
|
89
|
+
const parsed = Number.parseFloat(explicit);
|
|
90
|
+
if (Number.isFinite(parsed)) {
|
|
91
|
+
return Math.max(1, Math.round(parsed));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const importance = frontmatter.importance;
|
|
95
|
+
if (typeof importance === "number" && Number.isFinite(importance) && importance >= 0 && importance <= 1) {
|
|
96
|
+
return Math.max(1, Math.round(importance * 100));
|
|
97
|
+
}
|
|
98
|
+
return DEFAULT_CATEGORY_PRIORITY[category];
|
|
99
|
+
}
|
|
100
|
+
function deriveTitle(frontmatter, markdownContent, fallbackPath) {
|
|
101
|
+
if (typeof frontmatter.title === "string" && frontmatter.title.trim()) {
|
|
102
|
+
return frontmatter.title.trim();
|
|
103
|
+
}
|
|
104
|
+
const heading = extractHeadingTitle(markdownContent);
|
|
105
|
+
if (heading) {
|
|
106
|
+
return heading;
|
|
107
|
+
}
|
|
108
|
+
return path.basename(fallbackPath, ".md").replace(/[-_]+/g, " ").trim();
|
|
109
|
+
}
|
|
110
|
+
function deriveTriggers(params) {
|
|
111
|
+
const { category, frontmatter, title, relativePath } = params;
|
|
112
|
+
const explicitTriggers = toStringArray(frontmatter.triggers);
|
|
113
|
+
const tags = toStringArray(frontmatter.tags);
|
|
114
|
+
const aliases = toStringArray(frontmatter.aliases);
|
|
115
|
+
const baseName = path.basename(relativePath, ".md").replace(/[-_]+/g, " ").trim();
|
|
116
|
+
const normalizedTitle = title.trim();
|
|
117
|
+
const triggerSet = /* @__PURE__ */ new Set();
|
|
118
|
+
for (const value of [...explicitTriggers, ...tags, ...aliases]) {
|
|
119
|
+
if (value.trim()) {
|
|
120
|
+
triggerSet.add(value.trim());
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (triggerSet.size === 0 || category !== "rules") {
|
|
124
|
+
if (normalizedTitle) {
|
|
125
|
+
triggerSet.add(normalizedTitle);
|
|
126
|
+
}
|
|
127
|
+
if (baseName) {
|
|
128
|
+
triggerSet.add(baseName);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
for (const keyword of tokenizeKeywords(normalizedTitle)) {
|
|
132
|
+
if (keyword.length >= 4) {
|
|
133
|
+
triggerSet.add(keyword);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return [...triggerSet];
|
|
137
|
+
}
|
|
138
|
+
function deriveScope(frontmatter) {
|
|
139
|
+
const raw = [
|
|
140
|
+
...toStringArray(frontmatter.scope),
|
|
141
|
+
...toStringArray(frontmatter.scopes)
|
|
142
|
+
];
|
|
143
|
+
return [...new Set(raw.map(normalizeScopeValue).filter(Boolean))];
|
|
144
|
+
}
|
|
145
|
+
function tokenizeKeywords(value) {
|
|
146
|
+
const normalized = normalizeText(value);
|
|
147
|
+
if (!normalized) {
|
|
148
|
+
return [];
|
|
149
|
+
}
|
|
150
|
+
const tokens = normalized.split(" ").filter(Boolean);
|
|
151
|
+
const unique = [];
|
|
152
|
+
const seen = /* @__PURE__ */ new Set();
|
|
153
|
+
for (const token of tokens) {
|
|
154
|
+
if (token.length < 3 || STOP_WORDS.has(token) || seen.has(token)) {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
seen.add(token);
|
|
158
|
+
unique.push(token);
|
|
159
|
+
}
|
|
160
|
+
return unique;
|
|
161
|
+
}
|
|
162
|
+
function collectMarkdownFiles(rootPath, currentPath, collected) {
|
|
163
|
+
if (!fs.existsSync(currentPath)) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const entries = fs.readdirSync(currentPath, { withFileTypes: true });
|
|
167
|
+
for (const entry of entries) {
|
|
168
|
+
const absolutePath = path.join(currentPath, entry.name);
|
|
169
|
+
if (entry.isDirectory()) {
|
|
170
|
+
collectMarkdownFiles(rootPath, absolutePath, collected);
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
174
|
+
collected.push(toRelativePath(rootPath, absolutePath));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function parseInjectableFile(vaultPath, category, relativePath) {
|
|
179
|
+
const absolutePath = path.join(vaultPath, relativePath);
|
|
180
|
+
if (!fs.existsSync(absolutePath)) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
const raw = fs.readFileSync(absolutePath, "utf-8");
|
|
185
|
+
const parsed = matter(raw);
|
|
186
|
+
const frontmatter = parsed.data ?? {};
|
|
187
|
+
const title = deriveTitle(frontmatter, parsed.content, relativePath);
|
|
188
|
+
const triggers = deriveTriggers({ category, frontmatter, title, relativePath });
|
|
189
|
+
const scope = deriveScope(frontmatter);
|
|
190
|
+
const priority = parsePriority(frontmatter, category);
|
|
191
|
+
const searchKeywordSet = /* @__PURE__ */ new Set();
|
|
192
|
+
for (const trigger of triggers) {
|
|
193
|
+
for (const keyword of tokenizeKeywords(trigger)) {
|
|
194
|
+
searchKeywordSet.add(keyword);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
for (const keyword of tokenizeKeywords(title)) {
|
|
198
|
+
searchKeywordSet.add(keyword);
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
id: relativePath,
|
|
202
|
+
category,
|
|
203
|
+
relativePath,
|
|
204
|
+
title,
|
|
205
|
+
content: parsed.content.trim(),
|
|
206
|
+
triggers,
|
|
207
|
+
scope,
|
|
208
|
+
priority,
|
|
209
|
+
searchKeywords: [...searchKeywordSet],
|
|
210
|
+
noteNodeId: toNoteNodeId(relativePath)
|
|
211
|
+
};
|
|
212
|
+
} catch {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function indexInjectableItems(vaultPathInput) {
|
|
217
|
+
const vaultPath = path.resolve(vaultPathInput);
|
|
218
|
+
const items = [];
|
|
219
|
+
for (const category of INJECTABLE_CATEGORIES) {
|
|
220
|
+
const categoryRoot = path.join(vaultPath, category);
|
|
221
|
+
const markdownFiles = [];
|
|
222
|
+
collectMarkdownFiles(vaultPath, categoryRoot, markdownFiles);
|
|
223
|
+
for (const relativePath of markdownFiles.sort((left, right) => left.localeCompare(right))) {
|
|
224
|
+
const parsed = parseInjectableFile(vaultPath, category, relativePath);
|
|
225
|
+
if (parsed) {
|
|
226
|
+
items.push(parsed);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return items;
|
|
231
|
+
}
|
|
232
|
+
function containsPhrase(normalizedHaystack, phrase) {
|
|
233
|
+
const normalizedPhrase = normalizeText(phrase);
|
|
234
|
+
if (!normalizedPhrase) {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
const haystack = ` ${normalizedHaystack} `;
|
|
238
|
+
const needle = ` ${normalizedPhrase} `;
|
|
239
|
+
return haystack.includes(needle);
|
|
240
|
+
}
|
|
241
|
+
function collectNodeAliases(node) {
|
|
242
|
+
const aliases = /* @__PURE__ */ new Set();
|
|
243
|
+
if (node.title) {
|
|
244
|
+
aliases.add(node.title);
|
|
245
|
+
}
|
|
246
|
+
if (node.path) {
|
|
247
|
+
const basename2 = path.basename(node.path, ".md");
|
|
248
|
+
aliases.add(basename2.replace(/[-_]+/g, " "));
|
|
249
|
+
aliases.add(basename2);
|
|
250
|
+
}
|
|
251
|
+
return [...aliases].map((alias) => normalizeText(alias)).filter((alias) => alias.length >= 3);
|
|
252
|
+
}
|
|
253
|
+
function isEligibleGraphNode(node) {
|
|
254
|
+
if (!node) return false;
|
|
255
|
+
if (node.missing) return false;
|
|
256
|
+
if (node.type === "tag" || node.type === "unresolved") return false;
|
|
257
|
+
return Boolean(node.path);
|
|
258
|
+
}
|
|
259
|
+
function buildDeterministicEntityMatches(message, graph) {
|
|
260
|
+
const normalizedMessage = normalizeText(message);
|
|
261
|
+
const nodeById = new Map(graph.nodes.map((node) => [node.id, node]));
|
|
262
|
+
const directAliasesByNode = /* @__PURE__ */ new Map();
|
|
263
|
+
for (const node of graph.nodes) {
|
|
264
|
+
if (!isEligibleGraphNode(node)) {
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
const aliases = collectNodeAliases(node);
|
|
268
|
+
for (const alias of aliases) {
|
|
269
|
+
if (containsPhrase(normalizedMessage, alias)) {
|
|
270
|
+
const bucket = directAliasesByNode.get(node.id) ?? /* @__PURE__ */ new Set();
|
|
271
|
+
bucket.add(alias);
|
|
272
|
+
directAliasesByNode.set(node.id, bucket);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
const oneHopSourcesByNode = /* @__PURE__ */ new Map();
|
|
277
|
+
if (directAliasesByNode.size === 0) {
|
|
278
|
+
return { directAliasesByNode, oneHopSourcesByNode };
|
|
279
|
+
}
|
|
280
|
+
for (const edge of graph.edges) {
|
|
281
|
+
const leftDirect = directAliasesByNode.has(edge.source);
|
|
282
|
+
const rightDirect = directAliasesByNode.has(edge.target);
|
|
283
|
+
if (!leftDirect && !rightDirect) {
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
if (leftDirect && !rightDirect) {
|
|
287
|
+
const target = nodeById.get(edge.target);
|
|
288
|
+
const source = nodeById.get(edge.source);
|
|
289
|
+
if (!isEligibleGraphNode(target) || !isEligibleGraphNode(source)) {
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
const bucket = oneHopSourcesByNode.get(target.id) ?? /* @__PURE__ */ new Set();
|
|
293
|
+
bucket.add(source.title || source.id);
|
|
294
|
+
oneHopSourcesByNode.set(target.id, bucket);
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
if (rightDirect && !leftDirect) {
|
|
298
|
+
const source = nodeById.get(edge.source);
|
|
299
|
+
const target = nodeById.get(edge.target);
|
|
300
|
+
if (!isEligibleGraphNode(source) || !isEligibleGraphNode(target)) {
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
const bucket = oneHopSourcesByNode.get(source.id) ?? /* @__PURE__ */ new Set();
|
|
304
|
+
bucket.add(target.title || target.id);
|
|
305
|
+
oneHopSourcesByNode.set(source.id, bucket);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return { directAliasesByNode, oneHopSourcesByNode };
|
|
309
|
+
}
|
|
310
|
+
function normalizeScopeInput(scope) {
|
|
311
|
+
if (!scope) {
|
|
312
|
+
return [];
|
|
313
|
+
}
|
|
314
|
+
if (Array.isArray(scope)) {
|
|
315
|
+
return [...new Set(scope.map(normalizeScopeValue).filter(Boolean))];
|
|
316
|
+
}
|
|
317
|
+
return [...new Set(scope.split(",").map(normalizeScopeValue).filter(Boolean))];
|
|
318
|
+
}
|
|
319
|
+
function scopeMatches(itemScope, requestedScope) {
|
|
320
|
+
if (requestedScope.length === 0 || requestedScope.includes("global") || requestedScope.includes("*")) {
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
if (itemScope.length === 0 || itemScope.includes("global") || itemScope.includes("*")) {
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
return itemScope.some((scope) => requestedScope.includes(scope));
|
|
327
|
+
}
|
|
328
|
+
function deterministicInjectMatches(params) {
|
|
329
|
+
const message = params.message.trim();
|
|
330
|
+
if (!message) {
|
|
331
|
+
return [];
|
|
332
|
+
}
|
|
333
|
+
const requestedScope = normalizeScopeInput(params.scope);
|
|
334
|
+
const messageKeywords = new Set(tokenizeKeywords(message));
|
|
335
|
+
const { directAliasesByNode, oneHopSourcesByNode } = buildDeterministicEntityMatches(message, params.graph);
|
|
336
|
+
const normalizedMessage = normalizeText(message);
|
|
337
|
+
const matches = [];
|
|
338
|
+
for (const item of params.items) {
|
|
339
|
+
if (!scopeMatches(item.scope, requestedScope)) {
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
const reasons = [];
|
|
343
|
+
let signalScore = 0;
|
|
344
|
+
const triggerHits = [];
|
|
345
|
+
for (const trigger of item.triggers) {
|
|
346
|
+
if (containsPhrase(normalizedMessage, trigger)) {
|
|
347
|
+
triggerHits.push(trigger);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
if (triggerHits.length > 0) {
|
|
351
|
+
const weight = Math.min(30, 12 + triggerHits.length * 6);
|
|
352
|
+
signalScore += weight;
|
|
353
|
+
reasons.push({
|
|
354
|
+
source: "trigger",
|
|
355
|
+
value: triggerHits.slice(0, 3).join(", "),
|
|
356
|
+
weight
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
const keywordHits = item.searchKeywords.filter((keyword) => messageKeywords.has(keyword));
|
|
360
|
+
if (keywordHits.length > 0) {
|
|
361
|
+
const weight = Math.min(18, keywordHits.length * 4);
|
|
362
|
+
signalScore += weight;
|
|
363
|
+
reasons.push({
|
|
364
|
+
source: "keyword",
|
|
365
|
+
value: keywordHits.slice(0, 4).join(", "),
|
|
366
|
+
weight
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
const directAliases = directAliasesByNode.get(item.noteNodeId);
|
|
370
|
+
if (directAliases && directAliases.size > 0) {
|
|
371
|
+
const weight = 18;
|
|
372
|
+
signalScore += weight;
|
|
373
|
+
reasons.push({
|
|
374
|
+
source: "entity",
|
|
375
|
+
value: [...directAliases].slice(0, 3).join(", "),
|
|
376
|
+
weight
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
const oneHopSources = oneHopSourcesByNode.get(item.noteNodeId);
|
|
380
|
+
if (oneHopSources && oneHopSources.size > 0) {
|
|
381
|
+
const weight = 10;
|
|
382
|
+
signalScore += weight;
|
|
383
|
+
reasons.push({
|
|
384
|
+
source: "graph_1hop",
|
|
385
|
+
value: `via ${[...oneHopSources].slice(0, 2).join(", ")}`,
|
|
386
|
+
weight
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
if (reasons.length === 0) {
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
const deterministicScore = item.priority + signalScore;
|
|
393
|
+
matches.push({
|
|
394
|
+
item,
|
|
395
|
+
score: deterministicScore,
|
|
396
|
+
deterministicScore,
|
|
397
|
+
llmScore: null,
|
|
398
|
+
reasons
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
return matches.sort((left, right) => {
|
|
402
|
+
if (right.score !== left.score) {
|
|
403
|
+
return right.score - left.score;
|
|
404
|
+
}
|
|
405
|
+
if (right.item.priority !== left.item.priority) {
|
|
406
|
+
return right.item.priority - left.item.priority;
|
|
407
|
+
}
|
|
408
|
+
return left.item.relativePath.localeCompare(right.item.relativePath);
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
function parseLlmMatches(rawOutput) {
|
|
412
|
+
const cleaned = rawOutput.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "").trim();
|
|
413
|
+
if (!cleaned) {
|
|
414
|
+
return [];
|
|
415
|
+
}
|
|
416
|
+
let parsed;
|
|
417
|
+
try {
|
|
418
|
+
parsed = JSON.parse(cleaned);
|
|
419
|
+
} catch {
|
|
420
|
+
return [];
|
|
421
|
+
}
|
|
422
|
+
const rows = Array.isArray(parsed) ? parsed : parsed && typeof parsed === "object" && Array.isArray(parsed.matches) ? parsed.matches : [];
|
|
423
|
+
return rows.flatMap((row) => {
|
|
424
|
+
if (!row || typeof row !== "object" || Array.isArray(row)) {
|
|
425
|
+
return [];
|
|
426
|
+
}
|
|
427
|
+
const id = typeof row.id === "string" ? row.id.trim() : "";
|
|
428
|
+
const scoreRaw = row.score;
|
|
429
|
+
const score = typeof scoreRaw === "number" ? scoreRaw : Number.NaN;
|
|
430
|
+
const reason = typeof row.reason === "string" ? row.reason.trim() : "";
|
|
431
|
+
if (!id || !Number.isFinite(score)) {
|
|
432
|
+
return [];
|
|
433
|
+
}
|
|
434
|
+
return [{
|
|
435
|
+
id,
|
|
436
|
+
score: Math.max(0, Math.min(1, score)),
|
|
437
|
+
reason
|
|
438
|
+
}];
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
async function addLlmIntentMatches(params) {
|
|
442
|
+
if (params.items.length === 0) {
|
|
443
|
+
return params.matches;
|
|
444
|
+
}
|
|
445
|
+
const existingById = new Map(params.matches.map((match) => [match.item.id, match]));
|
|
446
|
+
const candidates = [...params.items].sort((left, right) => right.priority - left.priority || left.relativePath.localeCompare(right.relativePath)).slice(0, 40);
|
|
447
|
+
const prompt = [
|
|
448
|
+
"You rank ClawVault injectable memory items for an agent prompt.",
|
|
449
|
+
"Return strict JSON only in this shape:",
|
|
450
|
+
'{"matches":[{"id":"<candidate id>","score":0.0-1.0,"reason":"short why"}]}',
|
|
451
|
+
"Only include candidate IDs from the list below.",
|
|
452
|
+
"Use higher scores for items that are relevant to the user message intent even if wording differs.",
|
|
453
|
+
"",
|
|
454
|
+
`User message: ${params.message}`,
|
|
455
|
+
"",
|
|
456
|
+
"Candidates:",
|
|
457
|
+
...candidates.map((item) => {
|
|
458
|
+
const preview = item.content.replace(/\s+/g, " ").trim().slice(0, 220);
|
|
459
|
+
return [
|
|
460
|
+
`- id: ${item.id}`,
|
|
461
|
+
` category: ${item.category}`,
|
|
462
|
+
` title: ${item.title}`,
|
|
463
|
+
` triggers: ${item.triggers.join(", ") || "(none)"}`,
|
|
464
|
+
` scope: ${item.scope.join(", ") || "(none)"}`,
|
|
465
|
+
` content: ${preview || "(empty)"}`
|
|
466
|
+
].join("\n");
|
|
467
|
+
})
|
|
468
|
+
].join("\n");
|
|
469
|
+
const output = await requestLlmCompletion({
|
|
470
|
+
provider: params.provider,
|
|
471
|
+
prompt,
|
|
472
|
+
model: params.model,
|
|
473
|
+
temperature: 0.1,
|
|
474
|
+
maxTokens: 1200,
|
|
475
|
+
fetchImpl: params.fetchImpl,
|
|
476
|
+
systemPrompt: "You are an intent ranking engine. Respond with valid JSON only."
|
|
477
|
+
});
|
|
478
|
+
const parsed = parseLlmMatches(output);
|
|
479
|
+
if (parsed.length === 0) {
|
|
480
|
+
return params.matches;
|
|
481
|
+
}
|
|
482
|
+
const itemById = new Map(params.items.map((item) => [item.id, item]));
|
|
483
|
+
for (const row of parsed) {
|
|
484
|
+
const candidate = itemById.get(row.id);
|
|
485
|
+
if (!candidate) {
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
const llmWeight = row.score * 24;
|
|
489
|
+
const reason = row.reason || `intent score ${row.score.toFixed(2)}`;
|
|
490
|
+
const existing = existingById.get(row.id);
|
|
491
|
+
if (existing) {
|
|
492
|
+
existing.llmScore = row.score;
|
|
493
|
+
existing.score += llmWeight;
|
|
494
|
+
existing.reasons.push({
|
|
495
|
+
source: "llm_intent",
|
|
496
|
+
value: reason,
|
|
497
|
+
weight: llmWeight
|
|
498
|
+
});
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
if (row.score < 0.45) {
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
const seededDeterministic = candidate.priority * 0.2;
|
|
505
|
+
existingById.set(row.id, {
|
|
506
|
+
item: candidate,
|
|
507
|
+
deterministicScore: seededDeterministic,
|
|
508
|
+
llmScore: row.score,
|
|
509
|
+
score: seededDeterministic + llmWeight,
|
|
510
|
+
reasons: [{
|
|
511
|
+
source: "llm_intent",
|
|
512
|
+
value: reason,
|
|
513
|
+
weight: llmWeight
|
|
514
|
+
}]
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
return [...existingById.values()].sort((left, right) => {
|
|
518
|
+
if (right.score !== left.score) {
|
|
519
|
+
return right.score - left.score;
|
|
520
|
+
}
|
|
521
|
+
if (right.item.priority !== left.item.priority) {
|
|
522
|
+
return right.item.priority - left.item.priority;
|
|
523
|
+
}
|
|
524
|
+
return left.item.relativePath.localeCompare(right.item.relativePath);
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
async function readGraph(vaultPath) {
|
|
528
|
+
const resolvedPath = path.resolve(vaultPath);
|
|
529
|
+
const loaded = loadMemoryGraphIndex(resolvedPath);
|
|
530
|
+
if (loaded?.graph) {
|
|
531
|
+
return loaded.graph;
|
|
532
|
+
}
|
|
533
|
+
try {
|
|
534
|
+
return await getMemoryGraph(resolvedPath);
|
|
535
|
+
} catch {
|
|
536
|
+
return {
|
|
537
|
+
schemaVersion: 1,
|
|
538
|
+
nodes: [],
|
|
539
|
+
edges: [],
|
|
540
|
+
stats: {
|
|
541
|
+
generatedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
542
|
+
nodeCount: 0,
|
|
543
|
+
edgeCount: 0,
|
|
544
|
+
nodeTypeCounts: {},
|
|
545
|
+
edgeTypeCounts: {}
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
async function runPromptInjection(vaultPathInput, message, options = {}) {
|
|
551
|
+
const vaultPath = path.resolve(vaultPathInput);
|
|
552
|
+
const maxResults = Math.max(1, Math.floor(options.maxResults ?? 8));
|
|
553
|
+
const useLlm = options.useLlm ?? false;
|
|
554
|
+
const startDeterministic = Date.now();
|
|
555
|
+
const items = indexInjectableItems(vaultPath);
|
|
556
|
+
const graph = await readGraph(vaultPath);
|
|
557
|
+
let matches = deterministicInjectMatches({
|
|
558
|
+
message,
|
|
559
|
+
items,
|
|
560
|
+
graph,
|
|
561
|
+
scope: options.scope
|
|
562
|
+
});
|
|
563
|
+
const deterministicMs = Date.now() - startDeterministic;
|
|
564
|
+
let llmProvider = null;
|
|
565
|
+
if (useLlm) {
|
|
566
|
+
llmProvider = resolveLlmProvider();
|
|
567
|
+
if (llmProvider) {
|
|
568
|
+
try {
|
|
569
|
+
matches = await addLlmIntentMatches({
|
|
570
|
+
message,
|
|
571
|
+
items,
|
|
572
|
+
matches,
|
|
573
|
+
provider: llmProvider,
|
|
574
|
+
model: options.model,
|
|
575
|
+
fetchImpl: options.fetchImpl
|
|
576
|
+
});
|
|
577
|
+
} catch {
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
return {
|
|
582
|
+
message,
|
|
583
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
584
|
+
deterministicMs,
|
|
585
|
+
llmProvider,
|
|
586
|
+
usedLlm: Boolean(useLlm && llmProvider),
|
|
587
|
+
matches: matches.slice(0, maxResults)
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// src/commands/inject.ts
|
|
592
|
+
function asPositiveInteger(value, fallback) {
|
|
593
|
+
if (typeof value === "number" && Number.isInteger(value) && value > 0) {
|
|
594
|
+
return value;
|
|
595
|
+
}
|
|
596
|
+
if (typeof value === "string") {
|
|
597
|
+
const parsed = Number.parseInt(value, 10);
|
|
598
|
+
if (Number.isInteger(parsed) && parsed > 0) {
|
|
599
|
+
return parsed;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
return fallback;
|
|
603
|
+
}
|
|
604
|
+
function asBoolean(value, fallback) {
|
|
605
|
+
if (typeof value === "boolean") {
|
|
606
|
+
return value;
|
|
607
|
+
}
|
|
608
|
+
if (typeof value === "string") {
|
|
609
|
+
const normalized = value.trim().toLowerCase();
|
|
610
|
+
if (["true", "1", "yes", "on"].includes(normalized)) {
|
|
611
|
+
return true;
|
|
612
|
+
}
|
|
613
|
+
if (["false", "0", "no", "off"].includes(normalized)) {
|
|
614
|
+
return false;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
return fallback;
|
|
618
|
+
}
|
|
619
|
+
function asStringArray(value, fallback) {
|
|
620
|
+
if (Array.isArray(value)) {
|
|
621
|
+
const normalized = value.flatMap((entry) => typeof entry === "string" ? entry.split(",") : []).map((entry) => entry.trim()).filter(Boolean);
|
|
622
|
+
if (normalized.length > 0) {
|
|
623
|
+
return normalized;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
if (typeof value === "string") {
|
|
627
|
+
const normalized = value.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
628
|
+
if (normalized.length > 0) {
|
|
629
|
+
return normalized;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
return fallback;
|
|
633
|
+
}
|
|
634
|
+
function readInjectConfig(vaultPath) {
|
|
635
|
+
const config = listConfig(vaultPath);
|
|
636
|
+
const inject = config.inject ?? {};
|
|
637
|
+
return {
|
|
638
|
+
maxResults: asPositiveInteger(inject.maxResults, 8),
|
|
639
|
+
useLlm: asBoolean(inject.useLlm, true),
|
|
640
|
+
scope: asStringArray(inject.scope, ["global"])
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
function formatReasons(match) {
|
|
644
|
+
return match.reasons.map((reason) => `${reason.source}:${reason.value}`).join(" | ");
|
|
645
|
+
}
|
|
646
|
+
function formatInjectMarkdown(result) {
|
|
647
|
+
const lines = [];
|
|
648
|
+
lines.push(`## Prompt Injection for: ${result.message}`);
|
|
649
|
+
lines.push("");
|
|
650
|
+
lines.push(`- Deterministic matching: ${result.deterministicMs}ms`);
|
|
651
|
+
lines.push(`- LLM fuzzy matching: ${result.usedLlm ? `enabled (${result.llmProvider})` : "disabled"}`);
|
|
652
|
+
lines.push("");
|
|
653
|
+
if (result.matches.length === 0) {
|
|
654
|
+
lines.push("_No injectable rules matched this message._");
|
|
655
|
+
return lines.join("\n");
|
|
656
|
+
}
|
|
657
|
+
for (const [index, match] of result.matches.entries()) {
|
|
658
|
+
lines.push(`### ${index + 1}. ${match.item.title}`);
|
|
659
|
+
lines.push(`- Path: ${match.item.relativePath}`);
|
|
660
|
+
lines.push(`- Category: ${match.item.category}`);
|
|
661
|
+
lines.push(`- Priority: ${match.item.priority}`);
|
|
662
|
+
lines.push(`- Score: ${match.score.toFixed(2)}`);
|
|
663
|
+
lines.push(`- Match sources: ${formatReasons(match)}`);
|
|
664
|
+
lines.push("");
|
|
665
|
+
lines.push(match.item.content || "_No content._");
|
|
666
|
+
lines.push("");
|
|
667
|
+
}
|
|
668
|
+
return lines.join("\n").trimEnd();
|
|
669
|
+
}
|
|
670
|
+
async function buildInjectionResult(message, options) {
|
|
671
|
+
const normalizedMessage = message.trim();
|
|
672
|
+
if (!normalizedMessage) {
|
|
673
|
+
throw new Error("Message is required for inject.");
|
|
674
|
+
}
|
|
675
|
+
const vaultPath = path2.resolve(options.vaultPath);
|
|
676
|
+
const config = readInjectConfig(vaultPath);
|
|
677
|
+
const maxResults = options.maxResults ?? config.maxResults;
|
|
678
|
+
const useLlm = options.useLlm ?? config.useLlm;
|
|
679
|
+
const scope = options.scope ?? config.scope;
|
|
680
|
+
return runPromptInjection(vaultPath, normalizedMessage, {
|
|
681
|
+
maxResults,
|
|
682
|
+
useLlm,
|
|
683
|
+
scope,
|
|
684
|
+
model: options.model
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
async function injectCommand(message, options) {
|
|
688
|
+
const result = await buildInjectionResult(message, options);
|
|
689
|
+
if ((options.format ?? "markdown") === "json") {
|
|
690
|
+
console.log(JSON.stringify(result, null, 2));
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
console.log(formatInjectMarkdown(result));
|
|
694
|
+
}
|
|
695
|
+
function parsePositiveInteger(raw, label) {
|
|
696
|
+
const parsed = Number.parseInt(raw, 10);
|
|
697
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
698
|
+
throw new Error(`Invalid ${label}: ${raw}`);
|
|
699
|
+
}
|
|
700
|
+
return parsed;
|
|
701
|
+
}
|
|
702
|
+
function registerInjectCommand(program) {
|
|
703
|
+
program.command("inject <message>").description("Inject rules, decisions, and preferences into your prompt context").option("-v, --vault <path>", "Vault path").option("-n, --max-results <n>", "Maximum number of injected items").option("--scope <scope>", "Comma-separated scope filter override").option("--enable-llm", "Enable optional LLM fuzzy intent matching").option("--disable-llm", "Disable optional LLM fuzzy intent matching").option("--format <format>", "Output format (markdown|json)", "markdown").option("--model <model>", "Override LLM model when fuzzy matching is enabled").action(async (message, rawOptions) => {
|
|
704
|
+
const format = rawOptions.format === "json" ? "json" : "markdown";
|
|
705
|
+
const useLlm = rawOptions.enableLlm ? true : rawOptions.disableLlm ? false : void 0;
|
|
706
|
+
await injectCommand(message, {
|
|
707
|
+
vaultPath: rawOptions.vault ?? process.env.CLAWVAULT_PATH ?? process.cwd(),
|
|
708
|
+
maxResults: rawOptions.maxResults ? parsePositiveInteger(rawOptions.maxResults, "max-results") : void 0,
|
|
709
|
+
useLlm,
|
|
710
|
+
scope: rawOptions.scope,
|
|
711
|
+
format,
|
|
712
|
+
model: rawOptions.model
|
|
713
|
+
});
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
export {
|
|
718
|
+
indexInjectableItems,
|
|
719
|
+
deterministicInjectMatches,
|
|
720
|
+
runPromptInjection,
|
|
721
|
+
buildInjectionResult,
|
|
722
|
+
injectCommand,
|
|
723
|
+
registerInjectCommand
|
|
724
|
+
};
|