clawvault 3.3.0 → 3.4.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/README.md CHANGED
@@ -267,23 +267,24 @@ clawvault setup --theme neural --canvas --bases
267
267
 
268
268
  ## OpenClaw Integration
269
269
 
270
- For hook-based lifecycle integration with OpenClaw:
270
+ ClawVault integrates with OpenClaw as a plugin package (not the deprecated `openclaw hooks install/enable` flow):
271
271
 
272
272
  ```bash
273
- # Install and enable hook pack
274
- openclaw hooks install clawvault
275
- openclaw hooks enable clawvault
273
+ # Install ClawVault
274
+ npm install -g clawvault
275
+
276
+ # Add ClawVault package path to plugins.load.paths in openclaw.json
277
+ # (You can use `npm root -g` to locate node_modules)
276
278
 
277
- # Verify
278
- openclaw hooks list --verbose
279
- openclaw hooks check
279
+ # Enable plugin + memory slot in openclaw.json:
280
+ # plugins.slots.memory = "clawvault"
281
+ # plugins.entries.clawvault.enabled = true
282
+
283
+ # Verify runtime assumptions
280
284
  clawvault compat
281
285
  ```
282
286
 
283
- The hook automatically:
284
- - Detects context death and injects recovery alerts
285
- - Auto-checkpoints before session resets
286
- - Provides `--profile auto` for context queries
287
+ The plugin provides lifecycle hooks, memory tools, and protocol-safe messaging behavior for OpenClaw sessions.
287
288
 
288
289
  ### MEMORY.md vs Vault
289
290
 
@@ -351,27 +352,26 @@ clawvault compat
351
352
 
352
353
  ## OpenClaw Setup (Canonical)
353
354
 
354
- If you want hook-based lifecycle integration, use this sequence:
355
+ Use this sequence for plugin-based OpenClaw integration:
355
356
 
356
357
  ```bash
357
358
  # Install CLI
358
359
  npm install -g clawvault
359
360
 
360
- # Install and enable hook pack
361
- openclaw hooks install clawvault
362
- openclaw hooks enable clawvault
361
+ # Locate global node_modules
362
+ npm root -g
363
363
 
364
- # Verify
365
- openclaw hooks list --verbose
366
- openclaw hooks info clawvault
367
- openclaw hooks check
368
- clawvault compat
364
+ # In openclaw.json:
365
+ # - add <npm-root>/clawvault to plugins.load.paths
366
+ # - set plugins.slots.memory = "clawvault"
367
+ # - set plugins.entries.clawvault.enabled = true
368
+ # - set plugins.entries.clawvault.config.vaultPath as needed
369
369
  ```
370
370
 
371
371
  Important:
372
372
 
373
- - `clawhub install clawvault` installs skill guidance, but does not replace hook-pack installation.
374
- - After enabling hooks, restart the OpenClaw gateway process so hook registration reloads.
373
+ - `clawhub install clawvault` installs skill guidance, but does not configure OpenClaw plugin loading.
374
+ - After changing plugin config, restart the OpenClaw gateway process.
375
375
 
376
376
  ## Minimal AGENTS.md Additions
377
377
 
@@ -488,10 +488,10 @@ vault/
488
488
  - `qmd` fallback errors:
489
489
  - `qmd` is optional; in-process BM25 search is available without it
490
490
  - if you want fallback compatibility, ensure `qmd --version` works in the same shell
491
- - Hook/plugin not active in OpenClaw:
492
- - run `openclaw hooks install clawvault`
493
- - run `openclaw hooks enable clawvault`
494
- - verify with `openclaw hooks list --verbose`
491
+ - Plugin not active in OpenClaw:
492
+ - verify `plugins.load.paths` includes the ClawVault package path
493
+ - verify `plugins.slots.memory` is `clawvault`
494
+ - verify `plugins.entries.clawvault.enabled` is `true`
495
495
  - OpenClaw integration drift:
496
496
  - run `clawvault compat`
497
497
  - Session transcript corruption:
@@ -49,7 +49,7 @@ describe('CLI command registration modules', () => {
49
49
  });
50
50
 
51
51
  const names = listCommandNames(program);
52
- expect(names).toEqual(expect.arrayContaining(['search', 'vsearch', 'context', 'inject', 'observe', 'reflect', 'session-recap']));
52
+ expect(names).toEqual(expect.arrayContaining(['search', 'vsearch', 'context', 'recall', 'inject', 'observe', 'reflect', 'session-recap']));
53
53
 
54
54
  const contextCommand = program.commands.find((command) => command.name() === 'context');
55
55
  const profileOption = contextCommand?.options.find((option) => option.flags.includes('--profile <profile>'));
@@ -127,6 +127,7 @@ describe('CLI command registration modules', () => {
127
127
  'compat',
128
128
  'graph',
129
129
  'entities',
130
+ 'entity',
130
131
  'link',
131
132
  'rebuild',
132
133
  'archive',
@@ -6,6 +6,7 @@ describe('CLI help contract', () => {
6
6
  const help = registerAllCommandModules().helpInformation();
7
7
  expect(help).toContain('init');
8
8
  expect(help).toContain('context');
9
+ expect(help).toContain('recall');
9
10
  expect(help).toContain('inject');
10
11
  expect(help).toContain('doctor');
11
12
  expect(help).toContain('benchmark');
@@ -22,6 +23,7 @@ describe('CLI help contract', () => {
22
23
  expect(help).toContain('template');
23
24
  expect(help).toContain('config');
24
25
  expect(help).toContain('route');
26
+ expect(help).toContain('entity');
25
27
  });
26
28
 
27
29
  it('documents context/compat/inject/project help details', () => {
@@ -174,11 +174,37 @@ export function registerMaintenanceCommands(program, { chalk }) {
174
174
  .command('entities')
175
175
  .description('List all linkable entities in the vault')
176
176
  .option('-v, --vault <path>', 'Vault path')
177
+ .option('--refresh', 'Regenerate entity profiles before listing')
177
178
  .option('--json', 'Output as JSON')
178
179
  .action(async (options) => {
179
180
  try {
180
181
  const { entitiesCommand } = await import('../dist/commands/entities.js');
181
- await entitiesCommand({ json: options.json, vaultPath: options.vault });
182
+ await entitiesCommand({
183
+ json: options.json,
184
+ vaultPath: options.vault,
185
+ refresh: options.refresh
186
+ });
187
+ } catch (err) {
188
+ console.error(chalk.red(`Error: ${err.message}`));
189
+ process.exit(1);
190
+ }
191
+ });
192
+
193
+ // === ENTITY ===
194
+ program
195
+ .command('entity <name>')
196
+ .description('Show synthesized profile for one entity')
197
+ .option('-v, --vault <path>', 'Vault path')
198
+ .option('--refresh', 'Regenerate entity profiles before lookup')
199
+ .option('--json', 'Output as JSON')
200
+ .action(async (name, options) => {
201
+ try {
202
+ const { entityCommand } = await import('../dist/commands/entities.js');
203
+ await entityCommand(name, {
204
+ json: options.json,
205
+ vaultPath: options.vault,
206
+ refresh: options.refresh
207
+ });
182
208
  } catch (err) {
183
209
  console.error(chalk.red(`Error: ${err.message}`));
184
210
  process.exit(1);
@@ -202,6 +202,41 @@ export function registerQueryCommands(
202
202
  }
203
203
  });
204
204
 
205
+ // === INJECT ===
206
+ program
207
+ .command('recall <query>')
208
+ .description('Recall memory context with strategy classification (quick|entity|temporal|verification|relationship)')
209
+ .option('-n, --limit <n>', 'Max results (default: 6)', '6')
210
+ .option('--strategy <strategy>', 'Override strategy (quick|entity|temporal|verification|relationship)')
211
+ .option('--json', 'Output as JSON')
212
+ .option('--no-sources', 'Hide source paths in recall context')
213
+ .option('-v, --vault <path>', 'Vault path')
214
+ .action(async (query, options) => {
215
+ try {
216
+ const parsedLimit = Number.parseInt(options.limit, 10);
217
+ if (!Number.isFinite(parsedLimit) || parsedLimit <= 0) {
218
+ throw new Error(`Invalid --limit value: ${options.limit}`);
219
+ }
220
+
221
+ const allowedStrategies = new Set(['quick', 'entity', 'temporal', 'verification', 'relationship']);
222
+ if (options.strategy && !allowedStrategies.has(options.strategy)) {
223
+ throw new Error(`Invalid --strategy value: ${options.strategy}`);
224
+ }
225
+
226
+ const { recallCommand } = await import('../dist/commands/recall.js');
227
+ await recallCommand(query, {
228
+ vaultPath: resolveVaultPath(options.vault),
229
+ limit: parsedLimit,
230
+ strategy: options.strategy,
231
+ json: options.json,
232
+ includeSources: options.sources
233
+ });
234
+ } catch (err) {
235
+ console.error(chalk.red(`Error: ${err.message}`));
236
+ process.exit(1);
237
+ }
238
+ });
239
+
205
240
  // === INJECT ===
206
241
  program
207
242
  .command('inject <message>')
@@ -20,6 +20,21 @@ function buildProgram() {
20
20
  }
21
21
 
22
22
  describe('register-query-commands', () => {
23
+ it('registers recall command with strategy options', () => {
24
+ const program = buildProgram();
25
+ const recallCommand = program.commands.find((command) => command.name() === 'recall');
26
+ expect(recallCommand).toBeDefined();
27
+
28
+ const flags = recallCommand?.options.map((option) => option.flags) ?? [];
29
+ expect(flags).toEqual(expect.arrayContaining([
30
+ '-n, --limit <n>',
31
+ '--strategy <strategy>',
32
+ '--json',
33
+ '--no-sources',
34
+ '-v, --vault <path>'
35
+ ]));
36
+ });
37
+
23
38
  it('documents inject command options and config-backed defaults', () => {
24
39
  const program = buildProgram();
25
40
  const injectCommand = program.commands.find((command) => command.name() === 'inject');
@@ -0,0 +1,158 @@
1
+ // src/lib/maintenance/heuristics.ts
2
+ var CATEGORY_PATTERNS = [
3
+ {
4
+ category: "decisions",
5
+ patterns: [
6
+ /\b(decid(?:e|ed|ing|ion)|chose|selected|opted|trade[- ]?off)\b/i,
7
+ /\b(approved|agreed|consensus)\b/i
8
+ ]
9
+ },
10
+ {
11
+ category: "lessons",
12
+ patterns: [
13
+ /\b(learn(?:ed|ing|t)|lesson|insight|realized|retrospective)\b/i,
14
+ /\b(next time|note to self|mistake)\b/i
15
+ ]
16
+ },
17
+ {
18
+ category: "commitments",
19
+ patterns: [
20
+ /\b(todo|task|action item|follow[- ]?up|deadline|due)\b/i,
21
+ /\b(i will|we will|must|need to)\b/i
22
+ ]
23
+ },
24
+ {
25
+ category: "people",
26
+ patterns: [
27
+ /\b(met with|talked to|spoke with|emailed|called|messaged)\b/i,
28
+ /\b(client|customer|partner|colleague|contact)\b/i
29
+ ]
30
+ },
31
+ {
32
+ category: "projects",
33
+ patterns: [
34
+ /\b(project|feature|release|deployment|deploy|service|api|repo)\b/i,
35
+ /\b(shipped|launched|merged|rolled out)\b/i
36
+ ]
37
+ },
38
+ {
39
+ category: "preferences",
40
+ patterns: [
41
+ /\b(prefer(?:s|red|ence)?|like(?:s|d)?|dislike|style|convention)\b/i,
42
+ /\b(always use|never use|default to)\b/i
43
+ ]
44
+ },
45
+ {
46
+ category: "facts",
47
+ patterns: [
48
+ /\b(is|are|was|were|has|have|contains|includes)\b/i
49
+ ]
50
+ }
51
+ ];
52
+ function splitSentences(content) {
53
+ return content.split(/\r?\n|(?<=[.?!])\s+/).map((sentence) => sentence.trim()).filter(Boolean);
54
+ }
55
+ function compactWhitespace(value) {
56
+ return value.toLowerCase().replace(/[^\w\s]/g, " ").replace(/\s+/g, " ").trim();
57
+ }
58
+ var STOP_WORDS = /* @__PURE__ */ new Set([
59
+ "the",
60
+ "a",
61
+ "an",
62
+ "and",
63
+ "or",
64
+ "to",
65
+ "of",
66
+ "for",
67
+ "in",
68
+ "on",
69
+ "is",
70
+ "are",
71
+ "was",
72
+ "were",
73
+ "be",
74
+ "this",
75
+ "that",
76
+ "it",
77
+ "with",
78
+ "as",
79
+ "at",
80
+ "by",
81
+ "from",
82
+ "we",
83
+ "i",
84
+ "you",
85
+ "they",
86
+ "he",
87
+ "she"
88
+ ]);
89
+ function classifyInboxItemHeuristic(item) {
90
+ const sample = `${item.title}
91
+ ${item.content.slice(0, 2400)}`;
92
+ for (const rule of CATEGORY_PATTERNS) {
93
+ if (rule.patterns.some((pattern) => pattern.test(sample))) {
94
+ return rule.category;
95
+ }
96
+ }
97
+ return "inbox";
98
+ }
99
+ function normalizeForDedup(content) {
100
+ return compactWhitespace(content.replace(/\[\[[^\]]+\]\]/g, ""));
101
+ }
102
+ function wordSet(content) {
103
+ const words = compactWhitespace(content).split(" ").filter(Boolean);
104
+ const filtered = words.filter((word) => !STOP_WORDS.has(word) && word.length > 2);
105
+ return new Set(filtered);
106
+ }
107
+ function similarityScore(left, right) {
108
+ const leftWords = wordSet(left);
109
+ const rightWords = wordSet(right);
110
+ if (leftWords.size === 0 || rightWords.size === 0) {
111
+ return 0;
112
+ }
113
+ let intersection = 0;
114
+ for (const word of leftWords) {
115
+ if (rightWords.has(word)) {
116
+ intersection += 1;
117
+ }
118
+ }
119
+ const union = leftWords.size + rightWords.size - intersection;
120
+ return union === 0 ? 0 : intersection / union;
121
+ }
122
+ function extractHeuristicInsights(content) {
123
+ const sentences = splitSentences(content);
124
+ const facts = sentences.filter((line) => /\b(is|are|was|were|has|have|contains|includes)\b/i.test(line)).slice(0, 8);
125
+ const decisions = sentences.filter((line) => /\b(decid(?:e|ed|ing|ion)|chose|selected|opted|agreed)\b/i.test(line)).slice(0, 8);
126
+ const lessons = sentences.filter((line) => /\b(learn(?:ed|ing|t)|lesson|insight|realized|next time|mistake)\b/i.test(line)).slice(0, 8);
127
+ if (facts.length === 0 && sentences.length > 0) {
128
+ facts.push(sentences[0]);
129
+ }
130
+ return { facts, decisions, lessons };
131
+ }
132
+ function buildHeuristicSurveyRecommendations(params) {
133
+ const recommendations = [];
134
+ if (params.inboxCount > 20) {
135
+ recommendations.push(`Inbox backlog is high (${params.inboxCount}); run \`clawvault maintain --worker curator\` more frequently.`);
136
+ }
137
+ if (params.linkedRatio < 0.25) {
138
+ recommendations.push("Graph connectivity is low; add wiki-links between related notes to improve context traversal.");
139
+ }
140
+ if ((params.categoryCounts.lessons ?? 0) < 5) {
141
+ recommendations.push("Lessons are sparse; run distillation on long-form captures to keep reusable learnings explicit.");
142
+ }
143
+ if ((params.categoryCounts.decisions ?? 0) < 5) {
144
+ recommendations.push("Decision coverage is light; capture major choices and rationale in decisions/.");
145
+ }
146
+ if (recommendations.length === 0) {
147
+ recommendations.push("Vault health looks balanced. Keep regular maintenance cadence and continue linking related notes.");
148
+ }
149
+ return recommendations;
150
+ }
151
+
152
+ export {
153
+ classifyInboxItemHeuristic,
154
+ normalizeForDedup,
155
+ similarityScore,
156
+ extractHeuristicInsights,
157
+ buildHeuristicSurveyRecommendations
158
+ };
@@ -5,6 +5,13 @@ import {
5
5
  import {
6
6
  readInboxItems
7
7
  } from "./chunk-2PKBIKDH.js";
8
+ import {
9
+ buildHeuristicSurveyRecommendations,
10
+ classifyInboxItemHeuristic,
11
+ extractHeuristicInsights,
12
+ normalizeForDedup,
13
+ similarityScore
14
+ } from "./chunk-35JCYSRR.js";
8
15
  import {
9
16
  resolveVaultPath
10
17
  } from "./chunk-GJO3CFUN.js";
@@ -173,157 +180,6 @@ function writeMaintenanceState(vaultPath, state) {
173
180
  import * as path5 from "path";
174
181
  import matter from "gray-matter";
175
182
 
176
- // src/lib/maintenance/heuristics.ts
177
- var CATEGORY_PATTERNS = [
178
- {
179
- category: "decisions",
180
- patterns: [
181
- /\b(decid(?:e|ed|ing|ion)|chose|selected|opted|trade[- ]?off)\b/i,
182
- /\b(approved|agreed|consensus)\b/i
183
- ]
184
- },
185
- {
186
- category: "lessons",
187
- patterns: [
188
- /\b(learn(?:ed|ing|t)|lesson|insight|realized|retrospective)\b/i,
189
- /\b(next time|note to self|mistake)\b/i
190
- ]
191
- },
192
- {
193
- category: "commitments",
194
- patterns: [
195
- /\b(todo|task|action item|follow[- ]?up|deadline|due)\b/i,
196
- /\b(i will|we will|must|need to)\b/i
197
- ]
198
- },
199
- {
200
- category: "people",
201
- patterns: [
202
- /\b(met with|talked to|spoke with|emailed|called|messaged)\b/i,
203
- /\b(client|customer|partner|colleague|contact)\b/i
204
- ]
205
- },
206
- {
207
- category: "projects",
208
- patterns: [
209
- /\b(project|feature|release|deployment|deploy|service|api|repo)\b/i,
210
- /\b(shipped|launched|merged|rolled out)\b/i
211
- ]
212
- },
213
- {
214
- category: "preferences",
215
- patterns: [
216
- /\b(prefer(?:s|red|ence)?|like(?:s|d)?|dislike|style|convention)\b/i,
217
- /\b(always use|never use|default to)\b/i
218
- ]
219
- },
220
- {
221
- category: "facts",
222
- patterns: [
223
- /\b(is|are|was|were|has|have|contains|includes)\b/i
224
- ]
225
- }
226
- ];
227
- function splitSentences(content) {
228
- return content.split(/\r?\n|(?<=[.?!])\s+/).map((sentence) => sentence.trim()).filter(Boolean);
229
- }
230
- function compactWhitespace(value) {
231
- return value.toLowerCase().replace(/[^\w\s]/g, " ").replace(/\s+/g, " ").trim();
232
- }
233
- var STOP_WORDS = /* @__PURE__ */ new Set([
234
- "the",
235
- "a",
236
- "an",
237
- "and",
238
- "or",
239
- "to",
240
- "of",
241
- "for",
242
- "in",
243
- "on",
244
- "is",
245
- "are",
246
- "was",
247
- "were",
248
- "be",
249
- "this",
250
- "that",
251
- "it",
252
- "with",
253
- "as",
254
- "at",
255
- "by",
256
- "from",
257
- "we",
258
- "i",
259
- "you",
260
- "they",
261
- "he",
262
- "she"
263
- ]);
264
- function classifyInboxItemHeuristic(item) {
265
- const sample = `${item.title}
266
- ${item.content.slice(0, 2400)}`;
267
- for (const rule of CATEGORY_PATTERNS) {
268
- if (rule.patterns.some((pattern) => pattern.test(sample))) {
269
- return rule.category;
270
- }
271
- }
272
- return "inbox";
273
- }
274
- function normalizeForDedup(content) {
275
- return compactWhitespace(content.replace(/\[\[[^\]]+\]\]/g, ""));
276
- }
277
- function wordSet(content) {
278
- const words = compactWhitespace(content).split(" ").filter(Boolean);
279
- const filtered = words.filter((word) => !STOP_WORDS.has(word) && word.length > 2);
280
- return new Set(filtered);
281
- }
282
- function similarityScore(left, right) {
283
- const leftWords = wordSet(left);
284
- const rightWords = wordSet(right);
285
- if (leftWords.size === 0 || rightWords.size === 0) {
286
- return 0;
287
- }
288
- let intersection = 0;
289
- for (const word of leftWords) {
290
- if (rightWords.has(word)) {
291
- intersection += 1;
292
- }
293
- }
294
- const union = leftWords.size + rightWords.size - intersection;
295
- return union === 0 ? 0 : intersection / union;
296
- }
297
- function extractHeuristicInsights(content) {
298
- const sentences = splitSentences(content);
299
- const facts = sentences.filter((line) => /\b(is|are|was|were|has|have|contains|includes)\b/i.test(line)).slice(0, 8);
300
- const decisions = sentences.filter((line) => /\b(decid(?:e|ed|ing|ion)|chose|selected|opted|agreed)\b/i.test(line)).slice(0, 8);
301
- const lessons = sentences.filter((line) => /\b(learn(?:ed|ing|t)|lesson|insight|realized|next time|mistake)\b/i.test(line)).slice(0, 8);
302
- if (facts.length === 0 && sentences.length > 0) {
303
- facts.push(sentences[0]);
304
- }
305
- return { facts, decisions, lessons };
306
- }
307
- function buildHeuristicSurveyRecommendations(params) {
308
- const recommendations = [];
309
- if (params.inboxCount > 20) {
310
- recommendations.push(`Inbox backlog is high (${params.inboxCount}); run \`clawvault maintain --worker curator\` more frequently.`);
311
- }
312
- if (params.linkedRatio < 0.25) {
313
- recommendations.push("Graph connectivity is low; add wiki-links between related notes to improve context traversal.");
314
- }
315
- if ((params.categoryCounts.lessons ?? 0) < 5) {
316
- recommendations.push("Lessons are sparse; run distillation on long-form captures to keep reusable learnings explicit.");
317
- }
318
- if ((params.categoryCounts.decisions ?? 0) < 5) {
319
- recommendations.push("Decision coverage is light; capture major choices and rationale in decisions/.");
320
- }
321
- if (recommendations.length === 0) {
322
- recommendations.push("Vault health looks balanced. Keep regular maintenance cadence and continue linking related notes.");
323
- }
324
- return recommendations;
325
- }
326
-
327
183
  // src/lib/maintenance/worker-utils.ts
328
184
  import * as fs4 from "fs";
329
185
  import * as path4 from "path";
@@ -9,22 +9,22 @@ import {
9
9
  } from "./chunk-OFOCU2V4.js";
10
10
  import {
11
11
  registerMaintainCommand
12
- } from "./chunk-TDWFBDAQ.js";
12
+ } from "./chunk-D5U3Q4N5.js";
13
13
  import {
14
14
  registerObserveCommand
15
15
  } from "./chunk-BLQXXX7Q.js";
16
16
  import {
17
17
  registerContextCommand
18
18
  } from "./chunk-GFCHWMGD.js";
19
- import {
20
- ClawVault
21
- } from "./chunk-ECGJYWNA.js";
22
19
  import {
23
20
  registerEmbedCommand
24
21
  } from "./chunk-T7E764W3.js";
25
22
  import {
26
23
  registerInboxCommand
27
24
  } from "./chunk-HEHO7SMV.js";
25
+ import {
26
+ ClawVault
27
+ } from "./chunk-ECGJYWNA.js";
28
28
  import {
29
29
  resolveVaultPath
30
30
  } from "./chunk-GJO3CFUN.js";