clawvault 3.3.0 → 3.4.0

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.
@@ -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";