clawvault 2.5.2 → 2.5.4

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.
Files changed (71) hide show
  1. package/README.md +159 -200
  2. package/bin/clawvault.js +111 -111
  3. package/bin/command-registration.test.js +166 -166
  4. package/bin/command-runtime.js +93 -93
  5. package/bin/command-runtime.test.js +154 -154
  6. package/bin/help-contract.test.js +39 -39
  7. package/bin/register-config-commands.js +153 -153
  8. package/bin/register-config-route-commands.test.js +121 -121
  9. package/bin/register-core-commands.js +237 -237
  10. package/bin/register-kanban-commands.js +56 -56
  11. package/bin/register-kanban-commands.test.js +83 -83
  12. package/bin/register-maintenance-commands.js +282 -282
  13. package/bin/register-project-commands.js +209 -209
  14. package/bin/register-project-commands.test.js +206 -206
  15. package/bin/register-query-commands.js +317 -317
  16. package/bin/register-query-commands.test.js +65 -65
  17. package/bin/register-resilience-commands.js +182 -182
  18. package/bin/register-resilience-commands.test.js +81 -81
  19. package/bin/register-route-commands.js +114 -114
  20. package/bin/register-session-lifecycle-commands.js +206 -206
  21. package/bin/register-tailscale-commands.js +106 -106
  22. package/bin/register-task-commands.js +348 -348
  23. package/bin/register-task-commands.test.js +69 -69
  24. package/bin/register-template-commands.js +72 -72
  25. package/bin/register-vault-operations-commands.js +300 -300
  26. package/bin/test-helpers/cli-command-fixtures.js +119 -119
  27. package/dashboard/lib/graph-diff.js +104 -104
  28. package/dashboard/lib/graph-diff.test.js +75 -75
  29. package/dashboard/lib/vault-parser.js +556 -556
  30. package/dashboard/lib/vault-parser.test.js +254 -254
  31. package/dashboard/public/app.js +796 -796
  32. package/dashboard/public/index.html +52 -52
  33. package/dashboard/public/styles.css +221 -221
  34. package/dashboard/server.js +374 -374
  35. package/dist/{chunk-3FP5BJ42.js → chunk-4QYGFWRM.js} +1 -1
  36. package/dist/{chunk-M25QVSJM.js → chunk-AXKYDCNN.js} +1 -1
  37. package/dist/{chunk-CLE2HHNT.js → chunk-IVRIKYFE.js} +18 -11
  38. package/dist/{chunk-HRTPQQF2.js → chunk-IZEY5S74.js} +1 -1
  39. package/dist/{chunk-HWUNREDJ.js → chunk-JDLOL2PL.js} +4 -4
  40. package/dist/{chunk-AY4PGUVL.js → chunk-KL4NAOMO.js} +1 -1
  41. package/dist/{chunk-O7XHXF7F.js → chunk-MAKNAHAW.js} +4 -4
  42. package/dist/{chunk-PLZKZW4I.js → chunk-OSMS7QIG.js} +1 -1
  43. package/dist/{chunk-NZ4ZZNSR.js → chunk-THRJVD4L.js} +1 -1
  44. package/dist/{chunk-4GBPTBFJ.js → chunk-TIGW564L.js} +1 -1
  45. package/dist/{chunk-BHO7WSAY.js → chunk-W2HNZC22.js} +3 -3
  46. package/dist/{chunk-GFJ3LIIB.js → chunk-XAVB4GB4.js} +1 -1
  47. package/dist/cli/index.js +10 -10
  48. package/dist/commands/context.js +3 -3
  49. package/dist/commands/doctor.js +4 -4
  50. package/dist/commands/embed.js +2 -2
  51. package/dist/commands/observe.js +2 -2
  52. package/dist/commands/setup.js +2 -2
  53. package/dist/commands/sleep.js +2 -2
  54. package/dist/commands/status.js +3 -3
  55. package/dist/commands/tailscale.js +3 -3
  56. package/dist/commands/wake.js +2 -2
  57. package/dist/index.js +12 -12
  58. package/dist/lib/tailscale.js +2 -2
  59. package/dist/lib/webdav.js +1 -1
  60. package/hooks/clawvault/HOOK.md +83 -74
  61. package/hooks/clawvault/handler.js +816 -816
  62. package/hooks/clawvault/handler.test.js +263 -263
  63. package/package.json +94 -125
  64. package/templates/checkpoint.md +19 -19
  65. package/templates/daily-note.md +19 -19
  66. package/templates/daily.md +19 -19
  67. package/templates/decision.md +17 -17
  68. package/templates/handoff.md +19 -19
  69. package/templates/lesson.md +16 -16
  70. package/templates/person.md +19 -19
  71. package/templates/project.md +23 -23
@@ -1,317 +1,317 @@
1
- /**
2
- * Query and context command registrations.
3
- */
4
-
5
- export function registerQueryCommands(
6
- program,
7
- {
8
- chalk,
9
- getVault,
10
- resolveVaultPath,
11
- QmdUnavailableError,
12
- printQmdMissing
13
- }
14
- ) {
15
- // === SEARCH ===
16
- program
17
- .command('search <query>')
18
- .description('Search the vault via qmd (BM25)')
19
- .option('-n, --limit <n>', 'Max results (default: 10)', '10')
20
- .option('-c, --category <category>', 'Filter by category')
21
- .option('--tags <tags>', 'Filter by tags (comma-separated)')
22
- .option('--recent', 'Boost recent documents')
23
- .option('--full', 'Include full content in results')
24
- .option('-v, --vault <path>', 'Vault path')
25
- .option('--json', 'Output as JSON')
26
- .action(async (query, options) => {
27
- try {
28
- const vault = await getVault(options.vault);
29
-
30
- const results = await vault.find(query, {
31
- limit: parseInt(options.limit, 10),
32
- category: options.category,
33
- tags: options.tags?.split(',').map((value) => value.trim()),
34
- fullContent: options.full,
35
- temporalBoost: options.recent
36
- });
37
-
38
- if (options.json) {
39
- console.log(JSON.stringify(results, null, 2));
40
- return;
41
- }
42
-
43
- if (results.length === 0) {
44
- console.log(chalk.yellow('No results found.'));
45
- return;
46
- }
47
-
48
- console.log(chalk.cyan(`\n🔍 Found ${results.length} result(s) for "${query}":\n`));
49
-
50
- for (const result of results) {
51
- const scoreBar = '█'.repeat(Math.round(result.score * 10)).padEnd(10, '░');
52
- console.log(chalk.green(`📄 ${result.document.title}`));
53
- console.log(chalk.dim(` ${result.document.category}/${result.document.id.split('/').pop()}`));
54
- console.log(chalk.dim(` Score: ${scoreBar} ${(result.score * 100).toFixed(0)}%`));
55
- if (result.snippet) {
56
- console.log(chalk.white(` ${result.snippet.split('\n')[0].slice(0, 80)}...`));
57
- }
58
- console.log();
59
- }
60
- } catch (err) {
61
- if (err instanceof QmdUnavailableError) {
62
- printQmdMissing();
63
- process.exit(1);
64
- }
65
- console.error(chalk.red(`Error: ${err.message}`));
66
- process.exit(1);
67
- }
68
- });
69
-
70
- // === VSEARCH ===
71
- program
72
- .command('vsearch <query>')
73
- .description('Semantic search via qmd (requires qmd installed)')
74
- .option('-n, --limit <n>', 'Max results (default: 5)', '5')
75
- .option('-c, --category <category>', 'Filter by category')
76
- .option('--tags <tags>', 'Filter by tags (comma-separated)')
77
- .option('--recent', 'Boost recent documents')
78
- .option('--full', 'Include full content in results')
79
- .option('-v, --vault <path>', 'Vault path')
80
- .option('--json', 'Output as JSON')
81
- .action(async (query, options) => {
82
- try {
83
- const vault = await getVault(options.vault);
84
-
85
- const results = await vault.vsearch(query, {
86
- limit: parseInt(options.limit, 10),
87
- category: options.category,
88
- tags: options.tags?.split(',').map((value) => value.trim()),
89
- fullContent: options.full,
90
- temporalBoost: options.recent
91
- });
92
-
93
- if (options.json) {
94
- console.log(JSON.stringify(results, null, 2));
95
- return;
96
- }
97
-
98
- if (results.length === 0) {
99
- console.log(chalk.yellow('No results found.'));
100
- return;
101
- }
102
-
103
- console.log(chalk.cyan(`\n🧠 Found ${results.length} result(s) for "${query}":\n`));
104
-
105
- for (const result of results) {
106
- const scoreBar = '█'.repeat(Math.round(result.score * 10)).padEnd(10, '░');
107
- console.log(chalk.green(`📄 ${result.document.title}`));
108
- console.log(chalk.dim(` ${result.document.category}/${result.document.id.split('/').pop()}`));
109
- console.log(chalk.dim(` Score: ${scoreBar} ${(result.score * 100).toFixed(0)}%`));
110
- if (result.snippet) {
111
- console.log(chalk.white(` ${result.snippet.split('\n')[0].slice(0, 80)}...`));
112
- }
113
- console.log();
114
- }
115
- } catch (err) {
116
- if (err instanceof QmdUnavailableError) {
117
- printQmdMissing();
118
- process.exit(1);
119
- }
120
- console.error(chalk.red(`Error: ${err.message}`));
121
- process.exit(1);
122
- }
123
- });
124
-
125
- // === CONTEXT ===
126
- program
127
- .command('context <task>')
128
- .description('Generate task-relevant context for prompt injection')
129
- .option('-n, --limit <n>', 'Max results (default: 5)', '5')
130
- .option('--format <format>', 'Output format (markdown|json) (default: markdown)', 'markdown')
131
- .option('--recent', 'Boost recent documents (enabled by default)', true)
132
- .option('--include-observations', 'Include observation memories in output (enabled by default)', true)
133
- .option('--budget <number>', 'Optional token budget for assembled context')
134
- .option('--profile <profile>', 'Context profile (default|planning|incident|handoff|auto) (default: default)', 'default')
135
- .option('--max-hops <n>', 'Maximum graph expansion hops (default: 2)', '2')
136
- .option('-v, --vault <path>', 'Vault path')
137
- .action(async (task, options) => {
138
- try {
139
- const vaultPath = resolveVaultPath(options.vault);
140
- const format = options.format === 'json' ? 'json' : 'markdown';
141
- const parsedBudget = options.budget ? Number.parseInt(options.budget, 10) : undefined;
142
- const parsedMaxHops = Number.parseInt(options.maxHops, 10);
143
- if (options.budget && (!Number.isFinite(parsedBudget) || parsedBudget <= 0)) {
144
- throw new Error(`Invalid --budget value: ${options.budget}`);
145
- }
146
- if (!Number.isFinite(parsedMaxHops) || parsedMaxHops <= 0) {
147
- throw new Error(`Invalid --max-hops value: ${options.maxHops}`);
148
- }
149
-
150
- const { contextCommand } = await import('../dist/commands/context.js');
151
- await contextCommand(task, {
152
- vaultPath,
153
- limit: parseInt(options.limit, 10),
154
- format,
155
- recent: options.recent,
156
- includeObservations: options.includeObservations,
157
- budget: parsedBudget,
158
- profile: options.profile,
159
- maxHops: parsedMaxHops
160
- });
161
- } catch (err) {
162
- if (err instanceof QmdUnavailableError) {
163
- printQmdMissing();
164
- process.exit(1);
165
- }
166
- console.error(chalk.red(`Error: ${err.message}`));
167
- process.exit(1);
168
- }
169
- });
170
-
171
- // === INJECT ===
172
- program
173
- .command('inject <message>')
174
- .description('Inject relevant rules, decisions, and preferences into prompt context')
175
- .option('-n, --max-results <n>', 'Maximum injected items (default: config inject.maxResults, fallback 8)')
176
- .option('--scope <scope>', 'Comma-separated scope filter override (default: config inject.scope, fallback global)')
177
- .option('--enable-llm', 'Enable LLM fuzzy intent matching (overrides config inject.useLlm)')
178
- .option('--disable-llm', 'Disable LLM fuzzy intent matching (overrides config inject.useLlm)')
179
- .option('--format <format>', 'Output format (markdown|json) (default: markdown)', 'markdown')
180
- .option('--model <model>', 'Override LLM model when fuzzy matching is enabled')
181
- .option('-v, --vault <path>', 'Vault path')
182
- .action(async (message, options) => {
183
- try {
184
- const parsedMaxResults = options.maxResults
185
- ? Number.parseInt(options.maxResults, 10)
186
- : undefined;
187
- if (options.maxResults && (!Number.isFinite(parsedMaxResults) || parsedMaxResults <= 0)) {
188
- throw new Error(`Invalid --max-results value: ${options.maxResults}`);
189
- }
190
- const useLlm = options.enableLlm
191
- ? true
192
- : options.disableLlm
193
- ? false
194
- : undefined;
195
-
196
- const { injectCommand } = await import('../dist/commands/inject.js');
197
- await injectCommand(message, {
198
- vaultPath: resolveVaultPath(options.vault),
199
- maxResults: parsedMaxResults,
200
- useLlm,
201
- scope: options.scope,
202
- format: options.format === 'json' ? 'json' : 'markdown',
203
- model: options.model
204
- });
205
- } catch (err) {
206
- console.error(chalk.red(`Error: ${err.message}`));
207
- process.exit(1);
208
- }
209
- });
210
-
211
- // === OBSERVE ===
212
- program
213
- .command('observe')
214
- .description('Observe session files and build observational memory')
215
- .option('--watch <path>', 'Watch session file or directory')
216
- .option('--active', 'Observe active OpenClaw sessions incrementally')
217
- .option('--cron', 'Run one-shot active observation for cron hooks')
218
- .option('--agent <id>', 'OpenClaw agent ID (default: OPENCLAW_AGENT_ID or clawdious)')
219
- .option('--min-new <bytes>', 'Override minimum new-content threshold in bytes')
220
- .option('--sessions-dir <path>', 'Override OpenClaw sessions directory')
221
- .option('--dry-run', 'Show active observation candidates without compressing')
222
- .option('--threshold <n>', 'Compression token threshold (default: 30000)', '30000')
223
- .option('--reflect-threshold <n>', 'Reflection token threshold (default: 40000)', '40000')
224
- .option('--model <model>', 'LLM model override')
225
- .option('--extract-tasks', 'Extract task-like observations into backlog (enabled by default)', true)
226
- .option('--no-extract-tasks', 'Disable task extraction from observations')
227
- .option('--compress <file>', 'One-shot compression for a conversation file')
228
- .option('--daemon', 'Run in detached background mode')
229
- .option('-v, --vault <path>', 'Vault path')
230
- .action(async (options) => {
231
- try {
232
- const { observeCommand } = await import('../dist/commands/observe.js');
233
- const threshold = Number.parseInt(options.threshold, 10);
234
- const reflectThreshold = Number.parseInt(options.reflectThreshold, 10);
235
- const minNew = options.minNew === undefined
236
- ? undefined
237
- : Number.parseInt(options.minNew, 10);
238
- if (Number.isNaN(threshold) || threshold <= 0) {
239
- throw new Error(`Invalid --threshold value: ${options.threshold}`);
240
- }
241
- if (Number.isNaN(reflectThreshold) || reflectThreshold <= 0) {
242
- throw new Error(`Invalid --reflect-threshold value: ${options.reflectThreshold}`);
243
- }
244
- if (options.minNew !== undefined && (Number.isNaN(minNew) || minNew <= 0)) {
245
- throw new Error(`Invalid --min-new value: ${options.minNew}`);
246
- }
247
-
248
- await observeCommand({
249
- watch: options.watch,
250
- active: options.active,
251
- cron: options.cron,
252
- agent: options.agent,
253
- minNew,
254
- sessionsDir: options.sessionsDir,
255
- dryRun: options.dryRun,
256
- threshold,
257
- reflectThreshold,
258
- model: options.model,
259
- extractTasks: options.extractTasks,
260
- compress: options.compress,
261
- daemon: options.daemon,
262
- vaultPath: resolveVaultPath(options.vault)
263
- });
264
- } catch (err) {
265
- console.error(chalk.red(`Error: ${err.message}`));
266
- process.exit(1);
267
- }
268
- });
269
-
270
- // === REFLECT ===
271
- program
272
- .command('reflect')
273
- .description('Promote stable observations into weekly reflections')
274
- .option('--days <n>', 'Observation window in days (default: 14)', '14')
275
- .option('--dry-run', 'Show reflection output candidates without writing')
276
- .option('-v, --vault <path>', 'Vault path')
277
- .action(async (options) => {
278
- try {
279
- const { reflectCommand } = await import('../dist/commands/reflect.js');
280
- const days = Number.parseInt(options.days, 10);
281
- if (!Number.isFinite(days) || days <= 0) {
282
- throw new Error(`Invalid --days value: ${options.days}`);
283
- }
284
- await reflectCommand({
285
- vaultPath: resolveVaultPath(options.vault),
286
- days,
287
- dryRun: options.dryRun
288
- });
289
- } catch (err) {
290
- console.error(chalk.red(`Error: ${err.message}`));
291
- process.exit(1);
292
- }
293
- });
294
-
295
- // === SESSION-RECAP ===
296
- program
297
- .command('session-recap <sessionKey>')
298
- .description('Generate recap from a specific OpenClaw session transcript')
299
- .option('-n, --limit <n>', 'Number of messages to include (default: 15)', '15')
300
- .option('--format <format>', 'Output format (markdown|json) (default: markdown)', 'markdown')
301
- .option('-a, --agent <id>', 'Agent ID (default: OPENCLAW_AGENT_ID or clawdious)')
302
- .action(async (sessionKey, options) => {
303
- try {
304
- const { sessionRecapCommand } = await import('../dist/commands/session-recap.js');
305
- const format = options.format === 'json' ? 'json' : 'markdown';
306
- const parsedLimit = Number.parseInt(options.limit, 10);
307
- await sessionRecapCommand(sessionKey, {
308
- limit: Number.isNaN(parsedLimit) ? 15 : parsedLimit,
309
- format,
310
- agentId: options.agent
311
- });
312
- } catch (err) {
313
- console.error(chalk.red(`Error: ${err.message}`));
314
- process.exit(1);
315
- }
316
- });
317
- }
1
+ /**
2
+ * Query and context command registrations.
3
+ */
4
+
5
+ export function registerQueryCommands(
6
+ program,
7
+ {
8
+ chalk,
9
+ getVault,
10
+ resolveVaultPath,
11
+ QmdUnavailableError,
12
+ printQmdMissing
13
+ }
14
+ ) {
15
+ // === SEARCH ===
16
+ program
17
+ .command('search <query>')
18
+ .description('Search the vault via qmd (BM25)')
19
+ .option('-n, --limit <n>', 'Max results (default: 10)', '10')
20
+ .option('-c, --category <category>', 'Filter by category')
21
+ .option('--tags <tags>', 'Filter by tags (comma-separated)')
22
+ .option('--recent', 'Boost recent documents')
23
+ .option('--full', 'Include full content in results')
24
+ .option('-v, --vault <path>', 'Vault path')
25
+ .option('--json', 'Output as JSON')
26
+ .action(async (query, options) => {
27
+ try {
28
+ const vault = await getVault(options.vault);
29
+
30
+ const results = await vault.find(query, {
31
+ limit: parseInt(options.limit, 10),
32
+ category: options.category,
33
+ tags: options.tags?.split(',').map((value) => value.trim()),
34
+ fullContent: options.full,
35
+ temporalBoost: options.recent
36
+ });
37
+
38
+ if (options.json) {
39
+ console.log(JSON.stringify(results, null, 2));
40
+ return;
41
+ }
42
+
43
+ if (results.length === 0) {
44
+ console.log(chalk.yellow('No results found.'));
45
+ return;
46
+ }
47
+
48
+ console.log(chalk.cyan(`\n🔍 Found ${results.length} result(s) for "${query}":\n`));
49
+
50
+ for (const result of results) {
51
+ const scoreBar = '█'.repeat(Math.round(result.score * 10)).padEnd(10, '░');
52
+ console.log(chalk.green(`📄 ${result.document.title}`));
53
+ console.log(chalk.dim(` ${result.document.category}/${result.document.id.split('/').pop()}`));
54
+ console.log(chalk.dim(` Score: ${scoreBar} ${(result.score * 100).toFixed(0)}%`));
55
+ if (result.snippet) {
56
+ console.log(chalk.white(` ${result.snippet.split('\n')[0].slice(0, 80)}...`));
57
+ }
58
+ console.log();
59
+ }
60
+ } catch (err) {
61
+ if (err instanceof QmdUnavailableError) {
62
+ printQmdMissing();
63
+ process.exit(1);
64
+ }
65
+ console.error(chalk.red(`Error: ${err.message}`));
66
+ process.exit(1);
67
+ }
68
+ });
69
+
70
+ // === VSEARCH ===
71
+ program
72
+ .command('vsearch <query>')
73
+ .description('Semantic search via qmd (requires qmd installed)')
74
+ .option('-n, --limit <n>', 'Max results (default: 5)', '5')
75
+ .option('-c, --category <category>', 'Filter by category')
76
+ .option('--tags <tags>', 'Filter by tags (comma-separated)')
77
+ .option('--recent', 'Boost recent documents')
78
+ .option('--full', 'Include full content in results')
79
+ .option('-v, --vault <path>', 'Vault path')
80
+ .option('--json', 'Output as JSON')
81
+ .action(async (query, options) => {
82
+ try {
83
+ const vault = await getVault(options.vault);
84
+
85
+ const results = await vault.vsearch(query, {
86
+ limit: parseInt(options.limit, 10),
87
+ category: options.category,
88
+ tags: options.tags?.split(',').map((value) => value.trim()),
89
+ fullContent: options.full,
90
+ temporalBoost: options.recent
91
+ });
92
+
93
+ if (options.json) {
94
+ console.log(JSON.stringify(results, null, 2));
95
+ return;
96
+ }
97
+
98
+ if (results.length === 0) {
99
+ console.log(chalk.yellow('No results found.'));
100
+ return;
101
+ }
102
+
103
+ console.log(chalk.cyan(`\n🧠 Found ${results.length} result(s) for "${query}":\n`));
104
+
105
+ for (const result of results) {
106
+ const scoreBar = '█'.repeat(Math.round(result.score * 10)).padEnd(10, '░');
107
+ console.log(chalk.green(`📄 ${result.document.title}`));
108
+ console.log(chalk.dim(` ${result.document.category}/${result.document.id.split('/').pop()}`));
109
+ console.log(chalk.dim(` Score: ${scoreBar} ${(result.score * 100).toFixed(0)}%`));
110
+ if (result.snippet) {
111
+ console.log(chalk.white(` ${result.snippet.split('\n')[0].slice(0, 80)}...`));
112
+ }
113
+ console.log();
114
+ }
115
+ } catch (err) {
116
+ if (err instanceof QmdUnavailableError) {
117
+ printQmdMissing();
118
+ process.exit(1);
119
+ }
120
+ console.error(chalk.red(`Error: ${err.message}`));
121
+ process.exit(1);
122
+ }
123
+ });
124
+
125
+ // === CONTEXT ===
126
+ program
127
+ .command('context <task>')
128
+ .description('Generate task-relevant context for prompt injection')
129
+ .option('-n, --limit <n>', 'Max results (default: 5)', '5')
130
+ .option('--format <format>', 'Output format (markdown|json) (default: markdown)', 'markdown')
131
+ .option('--recent', 'Boost recent documents (enabled by default)', true)
132
+ .option('--include-observations', 'Include observation memories in output (enabled by default)', true)
133
+ .option('--budget <number>', 'Optional token budget for assembled context')
134
+ .option('--profile <profile>', 'Context profile (default|planning|incident|handoff|auto) (default: default)', 'default')
135
+ .option('--max-hops <n>', 'Maximum graph expansion hops (default: 2)', '2')
136
+ .option('-v, --vault <path>', 'Vault path')
137
+ .action(async (task, options) => {
138
+ try {
139
+ const vaultPath = resolveVaultPath(options.vault);
140
+ const format = options.format === 'json' ? 'json' : 'markdown';
141
+ const parsedBudget = options.budget ? Number.parseInt(options.budget, 10) : undefined;
142
+ const parsedMaxHops = Number.parseInt(options.maxHops, 10);
143
+ if (options.budget && (!Number.isFinite(parsedBudget) || parsedBudget <= 0)) {
144
+ throw new Error(`Invalid --budget value: ${options.budget}`);
145
+ }
146
+ if (!Number.isFinite(parsedMaxHops) || parsedMaxHops <= 0) {
147
+ throw new Error(`Invalid --max-hops value: ${options.maxHops}`);
148
+ }
149
+
150
+ const { contextCommand } = await import('../dist/commands/context.js');
151
+ await contextCommand(task, {
152
+ vaultPath,
153
+ limit: parseInt(options.limit, 10),
154
+ format,
155
+ recent: options.recent,
156
+ includeObservations: options.includeObservations,
157
+ budget: parsedBudget,
158
+ profile: options.profile,
159
+ maxHops: parsedMaxHops
160
+ });
161
+ } catch (err) {
162
+ if (err instanceof QmdUnavailableError) {
163
+ printQmdMissing();
164
+ process.exit(1);
165
+ }
166
+ console.error(chalk.red(`Error: ${err.message}`));
167
+ process.exit(1);
168
+ }
169
+ });
170
+
171
+ // === INJECT ===
172
+ program
173
+ .command('inject <message>')
174
+ .description('Inject relevant rules, decisions, and preferences into prompt context')
175
+ .option('-n, --max-results <n>', 'Maximum injected items (default: config inject.maxResults, fallback 8)')
176
+ .option('--scope <scope>', 'Comma-separated scope filter override (default: config inject.scope, fallback global)')
177
+ .option('--enable-llm', 'Enable LLM fuzzy intent matching (overrides config inject.useLlm)')
178
+ .option('--disable-llm', 'Disable LLM fuzzy intent matching (overrides config inject.useLlm)')
179
+ .option('--format <format>', 'Output format (markdown|json) (default: markdown)', 'markdown')
180
+ .option('--model <model>', 'Override LLM model when fuzzy matching is enabled')
181
+ .option('-v, --vault <path>', 'Vault path')
182
+ .action(async (message, options) => {
183
+ try {
184
+ const parsedMaxResults = options.maxResults
185
+ ? Number.parseInt(options.maxResults, 10)
186
+ : undefined;
187
+ if (options.maxResults && (!Number.isFinite(parsedMaxResults) || parsedMaxResults <= 0)) {
188
+ throw new Error(`Invalid --max-results value: ${options.maxResults}`);
189
+ }
190
+ const useLlm = options.enableLlm
191
+ ? true
192
+ : options.disableLlm
193
+ ? false
194
+ : undefined;
195
+
196
+ const { injectCommand } = await import('../dist/commands/inject.js');
197
+ await injectCommand(message, {
198
+ vaultPath: resolveVaultPath(options.vault),
199
+ maxResults: parsedMaxResults,
200
+ useLlm,
201
+ scope: options.scope,
202
+ format: options.format === 'json' ? 'json' : 'markdown',
203
+ model: options.model
204
+ });
205
+ } catch (err) {
206
+ console.error(chalk.red(`Error: ${err.message}`));
207
+ process.exit(1);
208
+ }
209
+ });
210
+
211
+ // === OBSERVE ===
212
+ program
213
+ .command('observe')
214
+ .description('Observe session files and build observational memory')
215
+ .option('--watch <path>', 'Watch session file or directory')
216
+ .option('--active', 'Observe active OpenClaw sessions incrementally')
217
+ .option('--cron', 'Run one-shot active observation for cron hooks')
218
+ .option('--agent <id>', 'OpenClaw agent ID (default: OPENCLAW_AGENT_ID or clawdious)')
219
+ .option('--min-new <bytes>', 'Override minimum new-content threshold in bytes')
220
+ .option('--sessions-dir <path>', 'Override OpenClaw sessions directory')
221
+ .option('--dry-run', 'Show active observation candidates without compressing')
222
+ .option('--threshold <n>', 'Compression token threshold (default: 30000)', '30000')
223
+ .option('--reflect-threshold <n>', 'Reflection token threshold (default: 40000)', '40000')
224
+ .option('--model <model>', 'LLM model override')
225
+ .option('--extract-tasks', 'Extract task-like observations into backlog (enabled by default)', true)
226
+ .option('--no-extract-tasks', 'Disable task extraction from observations')
227
+ .option('--compress <file>', 'One-shot compression for a conversation file')
228
+ .option('--daemon', 'Run in detached background mode')
229
+ .option('-v, --vault <path>', 'Vault path')
230
+ .action(async (options) => {
231
+ try {
232
+ const { observeCommand } = await import('../dist/commands/observe.js');
233
+ const threshold = Number.parseInt(options.threshold, 10);
234
+ const reflectThreshold = Number.parseInt(options.reflectThreshold, 10);
235
+ const minNew = options.minNew === undefined
236
+ ? undefined
237
+ : Number.parseInt(options.minNew, 10);
238
+ if (Number.isNaN(threshold) || threshold <= 0) {
239
+ throw new Error(`Invalid --threshold value: ${options.threshold}`);
240
+ }
241
+ if (Number.isNaN(reflectThreshold) || reflectThreshold <= 0) {
242
+ throw new Error(`Invalid --reflect-threshold value: ${options.reflectThreshold}`);
243
+ }
244
+ if (options.minNew !== undefined && (Number.isNaN(minNew) || minNew <= 0)) {
245
+ throw new Error(`Invalid --min-new value: ${options.minNew}`);
246
+ }
247
+
248
+ await observeCommand({
249
+ watch: options.watch,
250
+ active: options.active,
251
+ cron: options.cron,
252
+ agent: options.agent,
253
+ minNew,
254
+ sessionsDir: options.sessionsDir,
255
+ dryRun: options.dryRun,
256
+ threshold,
257
+ reflectThreshold,
258
+ model: options.model,
259
+ extractTasks: options.extractTasks,
260
+ compress: options.compress,
261
+ daemon: options.daemon,
262
+ vaultPath: resolveVaultPath(options.vault)
263
+ });
264
+ } catch (err) {
265
+ console.error(chalk.red(`Error: ${err.message}`));
266
+ process.exit(1);
267
+ }
268
+ });
269
+
270
+ // === REFLECT ===
271
+ program
272
+ .command('reflect')
273
+ .description('Promote stable observations into weekly reflections')
274
+ .option('--days <n>', 'Observation window in days (default: 14)', '14')
275
+ .option('--dry-run', 'Show reflection output candidates without writing')
276
+ .option('-v, --vault <path>', 'Vault path')
277
+ .action(async (options) => {
278
+ try {
279
+ const { reflectCommand } = await import('../dist/commands/reflect.js');
280
+ const days = Number.parseInt(options.days, 10);
281
+ if (!Number.isFinite(days) || days <= 0) {
282
+ throw new Error(`Invalid --days value: ${options.days}`);
283
+ }
284
+ await reflectCommand({
285
+ vaultPath: resolveVaultPath(options.vault),
286
+ days,
287
+ dryRun: options.dryRun
288
+ });
289
+ } catch (err) {
290
+ console.error(chalk.red(`Error: ${err.message}`));
291
+ process.exit(1);
292
+ }
293
+ });
294
+
295
+ // === SESSION-RECAP ===
296
+ program
297
+ .command('session-recap <sessionKey>')
298
+ .description('Generate recap from a specific OpenClaw session transcript')
299
+ .option('-n, --limit <n>', 'Number of messages to include (default: 15)', '15')
300
+ .option('--format <format>', 'Output format (markdown|json) (default: markdown)', 'markdown')
301
+ .option('-a, --agent <id>', 'Agent ID (default: OPENCLAW_AGENT_ID or clawdious)')
302
+ .action(async (sessionKey, options) => {
303
+ try {
304
+ const { sessionRecapCommand } = await import('../dist/commands/session-recap.js');
305
+ const format = options.format === 'json' ? 'json' : 'markdown';
306
+ const parsedLimit = Number.parseInt(options.limit, 10);
307
+ await sessionRecapCommand(sessionKey, {
308
+ limit: Number.isNaN(parsedLimit) ? 15 : parsedLimit,
309
+ format,
310
+ agentId: options.agent
311
+ });
312
+ } catch (err) {
313
+ console.error(chalk.red(`Error: ${err.message}`));
314
+ process.exit(1);
315
+ }
316
+ });
317
+ }