clawvault 1.5.1 → 1.6.1
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 +36 -2
- package/dist/{chunk-MXNXWOPL.js → chunk-3HFB7EMU.js} +1 -1
- package/dist/{chunk-VJIFT5T5.js → chunk-MIIXBNO3.js} +27 -8
- package/dist/{chunk-QYJI73KF.js → chunk-PIJGYMQZ.js} +1 -1
- package/dist/chunk-XPGSEJGY.js +91 -0
- package/dist/commands/context.d.ts +27 -0
- package/dist/commands/context.js +12 -0
- package/dist/commands/doctor.js +4 -4
- package/dist/commands/setup.js +2 -2
- package/dist/commands/sleep.d.ts +1 -1
- package/dist/commands/sleep.js +4 -4
- package/dist/commands/status.js +5 -5
- package/dist/commands/wake.d.ts +1 -1
- package/dist/commands/wake.js +2 -2
- package/dist/index.d.ts +5 -2
- package/dist/index.js +13 -5
- package/dist/{types-DO8rJ490.d.ts → types-DMU3SuAV.d.ts} +2 -0
- package/hooks/clawvault/HOOK.md +17 -2
- package/hooks/clawvault/handler.js +173 -4
- package/package.json +2 -2
package/bin/clawvault.js
CHANGED
|
@@ -250,6 +250,7 @@ program
|
|
|
250
250
|
.option('-n, --limit <n>', 'Max results', '10')
|
|
251
251
|
.option('-c, --category <category>', 'Filter by category')
|
|
252
252
|
.option('--tags <tags>', 'Filter by tags (comma-separated)')
|
|
253
|
+
.option('--recent', 'Boost recent documents')
|
|
253
254
|
.option('--full', 'Include full content in results')
|
|
254
255
|
.option('-v, --vault <path>', 'Vault path')
|
|
255
256
|
.option('--json', 'Output as JSON')
|
|
@@ -261,7 +262,8 @@ program
|
|
|
261
262
|
limit: parseInt(options.limit),
|
|
262
263
|
category: options.category,
|
|
263
264
|
tags: options.tags?.split(',').map(t => t.trim()),
|
|
264
|
-
fullContent: options.full
|
|
265
|
+
fullContent: options.full,
|
|
266
|
+
temporalBoost: options.recent
|
|
265
267
|
});
|
|
266
268
|
|
|
267
269
|
if (options.json) {
|
|
@@ -303,6 +305,7 @@ program
|
|
|
303
305
|
.option('-n, --limit <n>', 'Max results', '5')
|
|
304
306
|
.option('-c, --category <category>', 'Filter by category')
|
|
305
307
|
.option('--tags <tags>', 'Filter by tags (comma-separated)')
|
|
308
|
+
.option('--recent', 'Boost recent documents')
|
|
306
309
|
.option('--full', 'Include full content in results')
|
|
307
310
|
.option('-v, --vault <path>', 'Vault path')
|
|
308
311
|
.option('--json', 'Output as JSON')
|
|
@@ -314,7 +317,8 @@ program
|
|
|
314
317
|
limit: parseInt(options.limit),
|
|
315
318
|
category: options.category,
|
|
316
319
|
tags: options.tags?.split(',').map(t => t.trim()),
|
|
317
|
-
fullContent: options.full
|
|
320
|
+
fullContent: options.full,
|
|
321
|
+
temporalBoost: options.recent
|
|
318
322
|
});
|
|
319
323
|
|
|
320
324
|
if (options.json) {
|
|
@@ -350,6 +354,36 @@ program
|
|
|
350
354
|
}
|
|
351
355
|
});
|
|
352
356
|
|
|
357
|
+
// === CONTEXT ===
|
|
358
|
+
program
|
|
359
|
+
.command('context <task>')
|
|
360
|
+
.description('Generate task-relevant context for prompt injection')
|
|
361
|
+
.option('-n, --limit <n>', 'Max results', '5')
|
|
362
|
+
.option('--format <format>', 'Output format (markdown|json)', 'markdown')
|
|
363
|
+
.option('--recent', 'Boost recent documents (enabled by default)', true)
|
|
364
|
+
.option('-v, --vault <path>', 'Vault path')
|
|
365
|
+
.action(async (task, options) => {
|
|
366
|
+
try {
|
|
367
|
+
const vaultPath = resolveVaultPath(options.vault);
|
|
368
|
+
const format = options.format === 'json' ? 'json' : 'markdown';
|
|
369
|
+
|
|
370
|
+
const { contextCommand } = await import('../dist/commands/context.js');
|
|
371
|
+
await contextCommand(task, {
|
|
372
|
+
vaultPath,
|
|
373
|
+
limit: parseInt(options.limit),
|
|
374
|
+
format,
|
|
375
|
+
recent: options.recent
|
|
376
|
+
});
|
|
377
|
+
} catch (err) {
|
|
378
|
+
if (err instanceof QmdUnavailableError) {
|
|
379
|
+
printQmdMissing();
|
|
380
|
+
process.exit(1);
|
|
381
|
+
}
|
|
382
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
383
|
+
process.exit(1);
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
|
|
353
387
|
// === LIST ===
|
|
354
388
|
program
|
|
355
389
|
.command('list [category]')
|
|
@@ -36,6 +36,7 @@ var DEFAULT_CONFIG = {
|
|
|
36
36
|
|
|
37
37
|
// src/lib/search.ts
|
|
38
38
|
import { execFileSync, spawnSync } from "child_process";
|
|
39
|
+
import * as fs from "fs";
|
|
39
40
|
import * as path from "path";
|
|
40
41
|
var QMD_INSTALL_URL = "https://github.com/tobi/qmd";
|
|
41
42
|
var QMD_INSTALL_COMMAND = "bun install -g github:tobi/qmd";
|
|
@@ -199,7 +200,8 @@ var SearchEngine = class {
|
|
|
199
200
|
minScore = 0,
|
|
200
201
|
category,
|
|
201
202
|
tags,
|
|
202
|
-
fullContent = false
|
|
203
|
+
fullContent = false,
|
|
204
|
+
temporalBoost = false
|
|
203
205
|
} = options;
|
|
204
206
|
if (!query.trim()) return [];
|
|
205
207
|
const args = [
|
|
@@ -218,14 +220,15 @@ var SearchEngine = class {
|
|
|
218
220
|
minScore,
|
|
219
221
|
category,
|
|
220
222
|
tags,
|
|
221
|
-
fullContent
|
|
223
|
+
fullContent,
|
|
224
|
+
temporalBoost
|
|
222
225
|
});
|
|
223
226
|
}
|
|
224
227
|
/**
|
|
225
228
|
* Convert qmd results to ClawVault SearchResult format
|
|
226
229
|
*/
|
|
227
230
|
convertResults(qmdResults, options) {
|
|
228
|
-
const { limit = 10, minScore = 0, category, tags, fullContent = false } = options;
|
|
231
|
+
const { limit = 10, minScore = 0, category, tags, fullContent = false, temporalBoost = false } = options;
|
|
229
232
|
const results = [];
|
|
230
233
|
const maxScore = qmdResults[0]?.score || 1;
|
|
231
234
|
for (const qr of qmdResults) {
|
|
@@ -233,6 +236,7 @@ var SearchEngine = class {
|
|
|
233
236
|
const relativePath = this.vaultPath ? path.relative(this.vaultPath, filePath) : filePath;
|
|
234
237
|
const docId = relativePath.replace(/\.md$/, "");
|
|
235
238
|
let doc = this.documents.get(docId);
|
|
239
|
+
const modifiedAt = this.resolveModifiedAt(doc, filePath);
|
|
236
240
|
const parts = relativePath.split(path.sep);
|
|
237
241
|
const docCategory = parts.length > 1 ? parts[0] : "root";
|
|
238
242
|
if (category && docCategory !== category) continue;
|
|
@@ -241,7 +245,8 @@ var SearchEngine = class {
|
|
|
241
245
|
if (!tags.some((t) => docTags.has(t))) continue;
|
|
242
246
|
}
|
|
243
247
|
const normalizedScore = maxScore > 0 ? qr.score / maxScore : 0;
|
|
244
|
-
|
|
248
|
+
const finalScore = temporalBoost ? normalizedScore * this.getRecencyFactor(modifiedAt) : normalizedScore;
|
|
249
|
+
if (finalScore < minScore) continue;
|
|
245
250
|
if (!doc) {
|
|
246
251
|
doc = {
|
|
247
252
|
id: docId,
|
|
@@ -253,19 +258,33 @@ var SearchEngine = class {
|
|
|
253
258
|
frontmatter: {},
|
|
254
259
|
links: [],
|
|
255
260
|
tags: [],
|
|
256
|
-
modified:
|
|
261
|
+
modified: modifiedAt
|
|
257
262
|
};
|
|
258
263
|
}
|
|
259
264
|
results.push({
|
|
260
265
|
document: fullContent ? doc : { ...doc, content: "" },
|
|
261
|
-
score:
|
|
266
|
+
score: finalScore,
|
|
262
267
|
snippet: this.cleanSnippet(qr.snippet),
|
|
263
268
|
matchedTerms: []
|
|
264
269
|
// qmd doesn't provide this
|
|
265
270
|
});
|
|
266
|
-
if (results.length >= limit) break;
|
|
267
271
|
}
|
|
268
|
-
return results;
|
|
272
|
+
return results.sort((a, b) => b.score - a.score).slice(0, limit);
|
|
273
|
+
}
|
|
274
|
+
resolveModifiedAt(doc, filePath) {
|
|
275
|
+
if (doc) return doc.modified;
|
|
276
|
+
try {
|
|
277
|
+
return fs.statSync(filePath).mtime;
|
|
278
|
+
} catch {
|
|
279
|
+
return /* @__PURE__ */ new Date(0);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
getRecencyFactor(modifiedAt) {
|
|
283
|
+
const ageMs = Math.max(0, Date.now() - modifiedAt.getTime());
|
|
284
|
+
const ageDays = ageMs / (24 * 60 * 60 * 1e3);
|
|
285
|
+
if (ageDays < 1) return 1;
|
|
286
|
+
if (ageDays <= 7) return 0.9;
|
|
287
|
+
return 0.7;
|
|
269
288
|
}
|
|
270
289
|
/**
|
|
271
290
|
* Convert qmd:// URI to file path
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ClawVault
|
|
3
|
+
} from "./chunk-3HFB7EMU.js";
|
|
4
|
+
|
|
5
|
+
// src/commands/context.ts
|
|
6
|
+
import * as path from "path";
|
|
7
|
+
var DEFAULT_LIMIT = 5;
|
|
8
|
+
var MAX_SNIPPET_LENGTH = 320;
|
|
9
|
+
function formatRelativeAge(date, now = Date.now()) {
|
|
10
|
+
const ageMs = Math.max(0, now - date.getTime());
|
|
11
|
+
const days = Math.floor(ageMs / (24 * 60 * 60 * 1e3));
|
|
12
|
+
if (days === 0) return "today";
|
|
13
|
+
if (days < 7) return `${days} day${days === 1 ? "" : "s"} ago`;
|
|
14
|
+
const weeks = Math.floor(days / 7);
|
|
15
|
+
if (weeks < 5) return `${weeks} week${weeks === 1 ? "" : "s"} ago`;
|
|
16
|
+
const months = Math.floor(days / 30);
|
|
17
|
+
if (months < 12) return `${months} month${months === 1 ? "" : "s"} ago`;
|
|
18
|
+
const years = Math.floor(days / 365);
|
|
19
|
+
return `${years} year${years === 1 ? "" : "s"} ago`;
|
|
20
|
+
}
|
|
21
|
+
function normalizeSnippet(result) {
|
|
22
|
+
const source = (result.snippet || result.document.content || "").trim();
|
|
23
|
+
if (!source) return "No snippet available.";
|
|
24
|
+
return source.replace(/\s+/g, " ").slice(0, MAX_SNIPPET_LENGTH);
|
|
25
|
+
}
|
|
26
|
+
function formatContextMarkdown(task, entries) {
|
|
27
|
+
let output = `## Relevant Context for: ${task}
|
|
28
|
+
|
|
29
|
+
`;
|
|
30
|
+
if (entries.length === 0) {
|
|
31
|
+
output += "_No relevant context found._\n";
|
|
32
|
+
return output;
|
|
33
|
+
}
|
|
34
|
+
for (const entry of entries) {
|
|
35
|
+
output += `### ${entry.title} (score: ${entry.score.toFixed(2)}, ${entry.age})
|
|
36
|
+
`;
|
|
37
|
+
output += `${entry.snippet}
|
|
38
|
+
|
|
39
|
+
`;
|
|
40
|
+
}
|
|
41
|
+
return output.trimEnd();
|
|
42
|
+
}
|
|
43
|
+
async function buildContext(task, options) {
|
|
44
|
+
const normalizedTask = task.trim();
|
|
45
|
+
if (!normalizedTask) {
|
|
46
|
+
throw new Error("Task description is required.");
|
|
47
|
+
}
|
|
48
|
+
const vault = new ClawVault(path.resolve(options.vaultPath));
|
|
49
|
+
await vault.load();
|
|
50
|
+
const limit = Math.max(1, options.limit ?? DEFAULT_LIMIT);
|
|
51
|
+
const recent = options.recent ?? true;
|
|
52
|
+
const results = await vault.vsearch(normalizedTask, {
|
|
53
|
+
limit,
|
|
54
|
+
temporalBoost: recent
|
|
55
|
+
});
|
|
56
|
+
const context = results.map((result) => ({
|
|
57
|
+
title: result.document.title,
|
|
58
|
+
path: path.relative(vault.getPath(), result.document.path).split(path.sep).join("/"),
|
|
59
|
+
category: result.document.category,
|
|
60
|
+
score: result.score,
|
|
61
|
+
snippet: normalizeSnippet(result),
|
|
62
|
+
modified: result.document.modified.toISOString(),
|
|
63
|
+
age: formatRelativeAge(result.document.modified)
|
|
64
|
+
}));
|
|
65
|
+
return {
|
|
66
|
+
task: normalizedTask,
|
|
67
|
+
generated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
68
|
+
context,
|
|
69
|
+
markdown: formatContextMarkdown(normalizedTask, context)
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
async function contextCommand(task, options) {
|
|
73
|
+
const result = await buildContext(task, options);
|
|
74
|
+
const format = options.format ?? "markdown";
|
|
75
|
+
if (format === "json") {
|
|
76
|
+
console.log(JSON.stringify({
|
|
77
|
+
task: result.task,
|
|
78
|
+
generated: result.generated,
|
|
79
|
+
count: result.context.length,
|
|
80
|
+
context: result.context
|
|
81
|
+
}, null, 2));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
console.log(result.markdown);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export {
|
|
88
|
+
formatContextMarkdown,
|
|
89
|
+
buildContext,
|
|
90
|
+
contextCommand
|
|
91
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
type ContextFormat = 'markdown' | 'json';
|
|
2
|
+
interface ContextOptions {
|
|
3
|
+
vaultPath: string;
|
|
4
|
+
limit?: number;
|
|
5
|
+
format?: ContextFormat;
|
|
6
|
+
recent?: boolean;
|
|
7
|
+
}
|
|
8
|
+
interface ContextEntry {
|
|
9
|
+
title: string;
|
|
10
|
+
path: string;
|
|
11
|
+
category: string;
|
|
12
|
+
score: number;
|
|
13
|
+
snippet: string;
|
|
14
|
+
modified: string;
|
|
15
|
+
age: string;
|
|
16
|
+
}
|
|
17
|
+
interface ContextResult {
|
|
18
|
+
task: string;
|
|
19
|
+
generated: string;
|
|
20
|
+
context: ContextEntry[];
|
|
21
|
+
markdown: string;
|
|
22
|
+
}
|
|
23
|
+
declare function formatContextMarkdown(task: string, entries: ContextEntry[]): string;
|
|
24
|
+
declare function buildContext(task: string, options: ContextOptions): Promise<ContextResult>;
|
|
25
|
+
declare function contextCommand(task: string, options: ContextOptions): Promise<void>;
|
|
26
|
+
|
|
27
|
+
export { type ContextEntry, type ContextFormat, type ContextOptions, type ContextResult, buildContext, contextCommand, formatContextMarkdown };
|
package/dist/commands/doctor.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ClawVault,
|
|
3
3
|
findVault
|
|
4
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-3HFB7EMU.js";
|
|
5
|
+
import {
|
|
6
|
+
hasQmd
|
|
7
|
+
} from "../chunk-MIIXBNO3.js";
|
|
5
8
|
import {
|
|
6
9
|
scanVaultLinks
|
|
7
10
|
} from "../chunk-4VQTUVH7.js";
|
|
@@ -9,9 +12,6 @@ import "../chunk-J7ZWCI2C.js";
|
|
|
9
12
|
import {
|
|
10
13
|
formatAge
|
|
11
14
|
} from "../chunk-7ZRP733D.js";
|
|
12
|
-
import {
|
|
13
|
-
hasQmd
|
|
14
|
-
} from "../chunk-VJIFT5T5.js";
|
|
15
15
|
|
|
16
16
|
// src/commands/doctor.ts
|
|
17
17
|
import * as fs from "fs";
|
package/dist/commands/setup.js
CHANGED
package/dist/commands/sleep.d.ts
CHANGED
package/dist/commands/sleep.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ClawVault
|
|
3
|
-
} from "../chunk-
|
|
3
|
+
} from "../chunk-3HFB7EMU.js";
|
|
4
|
+
import {
|
|
5
|
+
qmdUpdate
|
|
6
|
+
} from "../chunk-MIIXBNO3.js";
|
|
4
7
|
import {
|
|
5
8
|
clearDirtyFlag
|
|
6
9
|
} from "../chunk-MZZJLQNQ.js";
|
|
7
|
-
import {
|
|
8
|
-
qmdUpdate
|
|
9
|
-
} from "../chunk-VJIFT5T5.js";
|
|
10
10
|
|
|
11
11
|
// src/commands/sleep.ts
|
|
12
12
|
import * as fs from "fs";
|
package/dist/commands/status.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ClawVault
|
|
3
|
-
} from "../chunk-
|
|
3
|
+
} from "../chunk-3HFB7EMU.js";
|
|
4
|
+
import {
|
|
5
|
+
QmdUnavailableError,
|
|
6
|
+
hasQmd
|
|
7
|
+
} from "../chunk-MIIXBNO3.js";
|
|
4
8
|
import {
|
|
5
9
|
scanVaultLinks
|
|
6
10
|
} from "../chunk-4VQTUVH7.js";
|
|
@@ -8,10 +12,6 @@ import "../chunk-J7ZWCI2C.js";
|
|
|
8
12
|
import {
|
|
9
13
|
formatAge
|
|
10
14
|
} from "../chunk-7ZRP733D.js";
|
|
11
|
-
import {
|
|
12
|
-
QmdUnavailableError,
|
|
13
|
-
hasQmd
|
|
14
|
-
} from "../chunk-VJIFT5T5.js";
|
|
15
15
|
|
|
16
16
|
// src/commands/status.ts
|
|
17
17
|
import * as fs from "fs";
|
package/dist/commands/wake.d.ts
CHANGED
package/dist/commands/wake.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ClawVault
|
|
3
|
-
} from "../chunk-
|
|
3
|
+
} from "../chunk-3HFB7EMU.js";
|
|
4
|
+
import "../chunk-MIIXBNO3.js";
|
|
4
5
|
import {
|
|
5
6
|
recover
|
|
6
7
|
} from "../chunk-MILVYUPK.js";
|
|
@@ -8,7 +9,6 @@ import {
|
|
|
8
9
|
clearDirtyFlag
|
|
9
10
|
} from "../chunk-MZZJLQNQ.js";
|
|
10
11
|
import "../chunk-7ZRP733D.js";
|
|
11
|
-
import "../chunk-VJIFT5T5.js";
|
|
12
12
|
|
|
13
13
|
// src/commands/wake.ts
|
|
14
14
|
import * as path from "path";
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { V as VaultConfig, S as StoreOptions, D as Document, a as SearchOptions, b as SearchResult, c as SyncOptions, d as SyncResult, C as Category, M as MemoryType, H as HandoffDocument, e as SessionRecap } from './types-
|
|
2
|
-
export { f as DEFAULT_CATEGORIES, g as DEFAULT_CONFIG, h as MEMORY_TYPES, T as TYPE_TO_CATEGORY, i as VaultMeta } from './types-
|
|
1
|
+
import { V as VaultConfig, S as StoreOptions, D as Document, a as SearchOptions, b as SearchResult, c as SyncOptions, d as SyncResult, C as Category, M as MemoryType, H as HandoffDocument, e as SessionRecap } from './types-DMU3SuAV.js';
|
|
2
|
+
export { f as DEFAULT_CATEGORIES, g as DEFAULT_CONFIG, h as MEMORY_TYPES, T as TYPE_TO_CATEGORY, i as VaultMeta } from './types-DMU3SuAV.js';
|
|
3
3
|
export { setupCommand } from './commands/setup.js';
|
|
4
|
+
export { ContextEntry, ContextFormat, ContextOptions, ContextResult, buildContext, contextCommand, formatContextMarkdown } from './commands/context.js';
|
|
4
5
|
export { TemplateVariables, buildTemplateVariables, renderTemplate } from './lib/template-engine.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -215,6 +216,8 @@ declare class SearchEngine {
|
|
|
215
216
|
* Convert qmd results to ClawVault SearchResult format
|
|
216
217
|
*/
|
|
217
218
|
private convertResults;
|
|
219
|
+
private resolveModifiedAt;
|
|
220
|
+
private getRecencyFactor;
|
|
218
221
|
/**
|
|
219
222
|
* Convert qmd:// URI to file path
|
|
220
223
|
*/
|
package/dist/index.js
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
|
+
import {
|
|
2
|
+
setupCommand
|
|
3
|
+
} from "./chunk-PIJGYMQZ.js";
|
|
1
4
|
import {
|
|
2
5
|
buildTemplateVariables,
|
|
3
6
|
renderTemplate
|
|
4
7
|
} from "./chunk-7766SIJP.js";
|
|
8
|
+
import {
|
|
9
|
+
buildContext,
|
|
10
|
+
contextCommand,
|
|
11
|
+
formatContextMarkdown
|
|
12
|
+
} from "./chunk-XPGSEJGY.js";
|
|
5
13
|
import {
|
|
6
14
|
ClawVault,
|
|
7
15
|
createVault,
|
|
8
16
|
findVault
|
|
9
|
-
} from "./chunk-
|
|
10
|
-
import {
|
|
11
|
-
setupCommand
|
|
12
|
-
} from "./chunk-QYJI73KF.js";
|
|
17
|
+
} from "./chunk-3HFB7EMU.js";
|
|
13
18
|
import {
|
|
14
19
|
DEFAULT_CATEGORIES,
|
|
15
20
|
DEFAULT_CONFIG,
|
|
@@ -24,7 +29,7 @@ import {
|
|
|
24
29
|
hasQmd,
|
|
25
30
|
qmdEmbed,
|
|
26
31
|
qmdUpdate
|
|
27
|
-
} from "./chunk-
|
|
32
|
+
} from "./chunk-MIIXBNO3.js";
|
|
28
33
|
|
|
29
34
|
// src/index.ts
|
|
30
35
|
import * as fs from "fs";
|
|
@@ -49,11 +54,14 @@ export {
|
|
|
49
54
|
SearchEngine,
|
|
50
55
|
TYPE_TO_CATEGORY,
|
|
51
56
|
VERSION,
|
|
57
|
+
buildContext,
|
|
52
58
|
buildTemplateVariables,
|
|
59
|
+
contextCommand,
|
|
53
60
|
createVault,
|
|
54
61
|
extractTags,
|
|
55
62
|
extractWikiLinks,
|
|
56
63
|
findVault,
|
|
64
|
+
formatContextMarkdown,
|
|
57
65
|
hasQmd,
|
|
58
66
|
qmdEmbed,
|
|
59
67
|
qmdUpdate,
|
package/hooks/clawvault/HOOK.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: clawvault
|
|
3
|
-
description: "Context
|
|
3
|
+
description: "Context resilience - recovery detection, auto-checkpoint, and session context injection"
|
|
4
4
|
metadata:
|
|
5
5
|
openclaw:
|
|
6
6
|
emoji: "🐘"
|
|
7
|
-
events: ["gateway:startup", "command:new"]
|
|
7
|
+
events: ["gateway:startup", "command:new", "session:start"]
|
|
8
8
|
requires:
|
|
9
9
|
bins: ["clawvault"]
|
|
10
10
|
---
|
|
@@ -15,6 +15,7 @@ Integrates ClawVault's context death resilience into OpenClaw:
|
|
|
15
15
|
|
|
16
16
|
- **On gateway startup**: Checks for context death, alerts agent
|
|
17
17
|
- **On /new command**: Auto-checkpoints before session reset
|
|
18
|
+
- **On session start**: Injects relevant vault context for the initial prompt
|
|
18
19
|
|
|
19
20
|
## Installation
|
|
20
21
|
|
|
@@ -43,6 +44,20 @@ openclaw hooks enable clawvault
|
|
|
43
44
|
2. Captures state even if agent forgot to handoff
|
|
44
45
|
3. Ensures continuity across session resets
|
|
45
46
|
|
|
47
|
+
### Session Start
|
|
48
|
+
|
|
49
|
+
1. Extracts the initial user prompt (`context.initialPrompt` or first user message)
|
|
50
|
+
2. Runs `clawvault context "<prompt>" --format json`
|
|
51
|
+
3. Injects up to 4 relevant context bullets into session messages
|
|
52
|
+
|
|
53
|
+
Injection format:
|
|
54
|
+
|
|
55
|
+
```text
|
|
56
|
+
[ClawVault] Relevant context for this task:
|
|
57
|
+
- <title> (<age>): <snippet>
|
|
58
|
+
- <title> (<age>): <snippet>
|
|
59
|
+
```
|
|
60
|
+
|
|
46
61
|
## No Configuration Needed
|
|
47
62
|
|
|
48
63
|
Just enable the hook. It auto-detects vault path via:
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Provides automatic context death resilience:
|
|
5
5
|
* - gateway:startup → detect context death, inject recovery info
|
|
6
6
|
* - command:new → auto-checkpoint before session reset
|
|
7
|
+
* - session:start → inject relevant context for first user prompt
|
|
7
8
|
*
|
|
8
9
|
* SECURITY: Uses execFileSync (no shell) to prevent command injection
|
|
9
10
|
*/
|
|
@@ -12,6 +13,10 @@ import { execFileSync } from 'child_process';
|
|
|
12
13
|
import * as fs from 'fs';
|
|
13
14
|
import * as path from 'path';
|
|
14
15
|
|
|
16
|
+
const MAX_CONTEXT_RESULTS = 4;
|
|
17
|
+
const MAX_CONTEXT_PROMPT_LENGTH = 500;
|
|
18
|
+
const MAX_CONTEXT_SNIPPET_LENGTH = 220;
|
|
19
|
+
|
|
15
20
|
// Sanitize string for safe display (prevent prompt injection via control chars)
|
|
16
21
|
function sanitizeForDisplay(str) {
|
|
17
22
|
if (typeof str !== 'string') return '';
|
|
@@ -22,6 +27,126 @@ function sanitizeForDisplay(str) {
|
|
|
22
27
|
.slice(0, 200); // Limit length
|
|
23
28
|
}
|
|
24
29
|
|
|
30
|
+
// Sanitize prompt before passing to CLI command
|
|
31
|
+
function sanitizePromptForContext(str) {
|
|
32
|
+
if (typeof str !== 'string') return '';
|
|
33
|
+
return str
|
|
34
|
+
.replace(/[\x00-\x1f\x7f]/g, ' ')
|
|
35
|
+
.replace(/\s+/g, ' ')
|
|
36
|
+
.trim()
|
|
37
|
+
.slice(0, MAX_CONTEXT_PROMPT_LENGTH);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function extractTextFromMessage(message) {
|
|
41
|
+
if (typeof message === 'string') return message;
|
|
42
|
+
if (!message || typeof message !== 'object') return '';
|
|
43
|
+
|
|
44
|
+
const content = message.content ?? message.text ?? message.message;
|
|
45
|
+
if (typeof content === 'string') return content;
|
|
46
|
+
|
|
47
|
+
if (Array.isArray(content)) {
|
|
48
|
+
return content
|
|
49
|
+
.map((part) => {
|
|
50
|
+
if (typeof part === 'string') return part;
|
|
51
|
+
if (!part || typeof part !== 'object') return '';
|
|
52
|
+
if (typeof part.text === 'string') return part.text;
|
|
53
|
+
if (typeof part.content === 'string') return part.content;
|
|
54
|
+
return '';
|
|
55
|
+
})
|
|
56
|
+
.filter(Boolean)
|
|
57
|
+
.join(' ');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return '';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isUserMessage(message) {
|
|
64
|
+
if (typeof message === 'string') return true;
|
|
65
|
+
if (!message || typeof message !== 'object') return false;
|
|
66
|
+
const role = typeof message.role === 'string' ? message.role.toLowerCase() : '';
|
|
67
|
+
const type = typeof message.type === 'string' ? message.type.toLowerCase() : '';
|
|
68
|
+
return role === 'user' || role === 'human' || type === 'user';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function extractInitialPrompt(event) {
|
|
72
|
+
const fromContext = sanitizePromptForContext(event?.context?.initialPrompt);
|
|
73
|
+
if (fromContext) return fromContext;
|
|
74
|
+
|
|
75
|
+
const candidates = [
|
|
76
|
+
event?.context?.messages,
|
|
77
|
+
event?.context?.initialMessages,
|
|
78
|
+
event?.context?.history,
|
|
79
|
+
event?.messages
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
for (const list of candidates) {
|
|
83
|
+
if (!Array.isArray(list)) continue;
|
|
84
|
+
for (const message of list) {
|
|
85
|
+
if (!isUserMessage(message)) continue;
|
|
86
|
+
const text = sanitizePromptForContext(extractTextFromMessage(message));
|
|
87
|
+
if (text) return text;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return '';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function truncateSnippet(snippet) {
|
|
95
|
+
const safe = sanitizeForDisplay(snippet).replace(/\s+/g, ' ').trim();
|
|
96
|
+
if (safe.length <= MAX_CONTEXT_SNIPPET_LENGTH) return safe;
|
|
97
|
+
return `${safe.slice(0, MAX_CONTEXT_SNIPPET_LENGTH - 3).trimEnd()}...`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function parseContextJson(output) {
|
|
101
|
+
try {
|
|
102
|
+
const parsed = JSON.parse(output);
|
|
103
|
+
if (!parsed || !Array.isArray(parsed.context)) return [];
|
|
104
|
+
|
|
105
|
+
return parsed.context
|
|
106
|
+
.slice(0, MAX_CONTEXT_RESULTS)
|
|
107
|
+
.map((entry) => ({
|
|
108
|
+
title: sanitizeForDisplay(entry?.title || 'Untitled'),
|
|
109
|
+
age: sanitizeForDisplay(entry?.age || 'unknown age'),
|
|
110
|
+
snippet: truncateSnippet(entry?.snippet || '')
|
|
111
|
+
}))
|
|
112
|
+
.filter((entry) => entry.snippet);
|
|
113
|
+
} catch {
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function formatContextInjection(entries) {
|
|
119
|
+
const lines = ['[ClawVault] Relevant context for this task:'];
|
|
120
|
+
for (const entry of entries) {
|
|
121
|
+
lines.push(`- ${entry.title} (${entry.age}): ${entry.snippet}`);
|
|
122
|
+
}
|
|
123
|
+
return lines.join('\n');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function injectSystemMessage(event, message) {
|
|
127
|
+
if (!event.messages || !Array.isArray(event.messages)) return false;
|
|
128
|
+
|
|
129
|
+
if (event.messages.length === 0) {
|
|
130
|
+
event.messages.push(message);
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const first = event.messages[0];
|
|
135
|
+
if (first && typeof first === 'object' && !Array.isArray(first)) {
|
|
136
|
+
if ('role' in first || 'content' in first) {
|
|
137
|
+
event.messages.push({ role: 'system', content: message });
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
if ('type' in first || 'text' in first) {
|
|
141
|
+
event.messages.push({ type: 'system', text: message });
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
event.messages.push(message);
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
|
|
25
150
|
// Validate vault path - must be absolute and exist
|
|
26
151
|
function validateVaultPath(vaultPath) {
|
|
27
152
|
if (!vaultPath || typeof vaultPath !== 'string') return null;
|
|
@@ -151,11 +276,9 @@ async function handleStartup(event) {
|
|
|
151
276
|
const alertMsg = alertParts.join(' ');
|
|
152
277
|
|
|
153
278
|
// Inject into event messages if available
|
|
154
|
-
if (event
|
|
155
|
-
|
|
279
|
+
if (injectSystemMessage(event, alertMsg)) {
|
|
280
|
+
console.warn('[clawvault] Context death detected, alert injected');
|
|
156
281
|
}
|
|
157
|
-
|
|
158
|
-
console.warn('[clawvault] Context death detected, alert injected');
|
|
159
282
|
} else {
|
|
160
283
|
console.log('[clawvault] Clean startup - no context death');
|
|
161
284
|
}
|
|
@@ -194,6 +317,47 @@ async function handleNew(event) {
|
|
|
194
317
|
}
|
|
195
318
|
}
|
|
196
319
|
|
|
320
|
+
// Handle session start - inject dynamic context for first prompt
|
|
321
|
+
async function handleSessionStart(event) {
|
|
322
|
+
const vaultPath = findVaultPath();
|
|
323
|
+
if (!vaultPath) {
|
|
324
|
+
console.log('[clawvault] No vault found, skipping context injection');
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const prompt = extractInitialPrompt(event);
|
|
329
|
+
if (!prompt) {
|
|
330
|
+
console.log('[clawvault] No initial prompt, skipping context injection');
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
console.log('[clawvault] Fetching context for session start');
|
|
335
|
+
|
|
336
|
+
const result = runClawvault([
|
|
337
|
+
'context',
|
|
338
|
+
prompt,
|
|
339
|
+
'--format', 'json',
|
|
340
|
+
'-v', vaultPath
|
|
341
|
+
]);
|
|
342
|
+
|
|
343
|
+
if (!result.success) {
|
|
344
|
+
console.warn('[clawvault] Context lookup failed');
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const entries = parseContextJson(result.output);
|
|
349
|
+
if (entries.length === 0) {
|
|
350
|
+
console.log('[clawvault] No relevant context found for prompt');
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (injectSystemMessage(event, formatContextInjection(entries))) {
|
|
355
|
+
console.log(`[clawvault] Injected ${entries.length} context item(s)`);
|
|
356
|
+
} else {
|
|
357
|
+
console.log('[clawvault] No message array available, skipping injection');
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
197
361
|
// Main handler - route events
|
|
198
362
|
const handler = async (event) => {
|
|
199
363
|
try {
|
|
@@ -206,6 +370,11 @@ const handler = async (event) => {
|
|
|
206
370
|
await handleNew(event);
|
|
207
371
|
return;
|
|
208
372
|
}
|
|
373
|
+
|
|
374
|
+
if (event.type === 'session' && event.action === 'start') {
|
|
375
|
+
await handleSessionStart(event);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
209
378
|
} catch (err) {
|
|
210
379
|
console.error('[clawvault] Hook error:', err.message || 'unknown error');
|
|
211
380
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clawvault",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.1",
|
|
4
4
|
"description": "🐘 An elephant never forgets. Structured memory for OpenClaw agents. Context death resilience, Obsidian-compatible markdown, local semantic search.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
]
|
|
29
29
|
},
|
|
30
30
|
"scripts": {
|
|
31
|
-
"build": "tsup src/index.ts src/commands/entities.ts src/commands/link.ts src/commands/checkpoint.ts src/commands/recover.ts src/commands/status.ts src/commands/template.ts src/commands/setup.ts src/commands/wake.ts src/commands/sleep.ts src/commands/doctor.ts src/commands/shell-init.ts src/commands/repair-session.ts src/lib/entity-index.ts src/lib/auto-linker.ts src/lib/config.ts src/lib/template-engine.ts src/lib/session-utils.ts src/lib/session-repair.ts --format esm --dts --clean",
|
|
31
|
+
"build": "tsup src/index.ts src/commands/entities.ts src/commands/link.ts src/commands/checkpoint.ts src/commands/recover.ts src/commands/status.ts src/commands/template.ts src/commands/setup.ts src/commands/context.ts src/commands/wake.ts src/commands/sleep.ts src/commands/doctor.ts src/commands/shell-init.ts src/commands/repair-session.ts src/lib/entity-index.ts src/lib/auto-linker.ts src/lib/config.ts src/lib/template-engine.ts src/lib/session-utils.ts src/lib/session-repair.ts --format esm --dts --clean",
|
|
32
32
|
"dev": "tsup src/index.ts src/commands/*.ts src/lib/*.ts --format esm --dts --watch",
|
|
33
33
|
"lint": "eslint src",
|
|
34
34
|
"typecheck": "tsc --noEmit",
|