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 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]')
@@ -8,7 +8,7 @@ import {
8
8
  hasQmd,
9
9
  qmdEmbed,
10
10
  qmdUpdate
11
- } from "./chunk-VJIFT5T5.js";
11
+ } from "./chunk-MIIXBNO3.js";
12
12
 
13
13
  // src/lib/vault.ts
14
14
  import * as fs from "fs";
@@ -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
- if (normalizedScore < minScore) continue;
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: /* @__PURE__ */ new Date()
261
+ modified: modifiedAt
257
262
  };
258
263
  }
259
264
  results.push({
260
265
  document: fullContent ? doc : { ...doc, content: "" },
261
- score: normalizedScore,
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
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  DEFAULT_CATEGORIES,
3
3
  hasQmd
4
- } from "./chunk-VJIFT5T5.js";
4
+ } from "./chunk-MIIXBNO3.js";
5
5
 
6
6
  // src/commands/setup.ts
7
7
  import * as fs from "fs";
@@ -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 };
@@ -0,0 +1,12 @@
1
+ import {
2
+ buildContext,
3
+ contextCommand,
4
+ formatContextMarkdown
5
+ } from "../chunk-XPGSEJGY.js";
6
+ import "../chunk-3HFB7EMU.js";
7
+ import "../chunk-MIIXBNO3.js";
8
+ export {
9
+ buildContext,
10
+ contextCommand,
11
+ formatContextMarkdown
12
+ };
@@ -1,7 +1,10 @@
1
1
  import {
2
2
  ClawVault,
3
3
  findVault
4
- } from "../chunk-MXNXWOPL.js";
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";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  setupCommand
3
- } from "../chunk-QYJI73KF.js";
4
- import "../chunk-VJIFT5T5.js";
3
+ } from "../chunk-PIJGYMQZ.js";
4
+ import "../chunk-MIIXBNO3.js";
5
5
  export {
6
6
  setupCommand
7
7
  };
@@ -1,4 +1,4 @@
1
- import { H as HandoffDocument, D as Document } from '../types-DO8rJ490.js';
1
+ import { H as HandoffDocument, D as Document } from '../types-DMU3SuAV.js';
2
2
 
3
3
  type PromptFn = (question: string) => Promise<string>;
4
4
  interface SleepOptions {
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  ClawVault
3
- } from "../chunk-MXNXWOPL.js";
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";
@@ -1,6 +1,10 @@
1
1
  import {
2
2
  ClawVault
3
- } from "../chunk-MXNXWOPL.js";
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";
@@ -1,4 +1,4 @@
1
- import { e as SessionRecap } from '../types-DO8rJ490.js';
1
+ import { e as SessionRecap } from '../types-DMU3SuAV.js';
2
2
  import { RecoveryInfo } from './recover.js';
3
3
  import './checkpoint.js';
4
4
 
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  ClawVault
3
- } from "../chunk-MXNXWOPL.js";
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-DO8rJ490.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-DO8rJ490.js';
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-MXNXWOPL.js";
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-VJIFT5T5.js";
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,
@@ -68,6 +68,8 @@ interface SearchOptions {
68
68
  tags?: string[];
69
69
  /** Include full content in results */
70
70
  fullContent?: boolean;
71
+ /** Boost recent documents in ranking */
72
+ temporalBoost?: boolean;
71
73
  }
72
74
  interface StoreOptions {
73
75
  /** Category to store in */
@@ -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.5.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",