clawvault 1.6.0 → 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.
@@ -1,10 +1,10 @@
1
1
  ---
2
2
  name: clawvault
3
- description: "Context death resilience - auto-checkpoint and recovery detection"
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.messages && Array.isArray(event.messages)) {
155
- event.messages.push(alertMsg);
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.6.0",
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",