clawvault 2.5.1 → 2.5.2

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 (63) hide show
  1. package/README.md +1 -0
  2. package/bin/command-registration.test.js +1 -0
  3. package/bin/command-runtime.js +21 -5
  4. package/bin/command-runtime.test.js +52 -0
  5. package/bin/help-contract.test.js +12 -1
  6. package/bin/register-config-commands.js +2 -2
  7. package/bin/register-core-commands.js +9 -9
  8. package/bin/register-kanban-commands.js +5 -5
  9. package/bin/register-maintenance-commands.js +44 -10
  10. package/bin/register-project-commands.js +14 -14
  11. package/bin/register-project-commands.test.js +5 -0
  12. package/bin/register-query-commands.js +23 -18
  13. package/bin/register-query-commands.test.js +65 -0
  14. package/bin/register-session-lifecycle-commands.js +3 -3
  15. package/bin/register-tailscale-commands.js +1 -1
  16. package/bin/register-task-commands.js +5 -5
  17. package/bin/register-template-commands.js +1 -1
  18. package/bin/register-vault-operations-commands.js +7 -7
  19. package/dist/{chunk-G3OQJ2NQ.js → chunk-2YDBJS7M.js} +1 -1
  20. package/dist/chunk-3FP5BJ42.js +88 -0
  21. package/dist/{chunk-C3PF7WBA.js → chunk-4IV3R2F5.js} +2 -2
  22. package/dist/{chunk-7OHQFMJK.js → chunk-AY4PGUVL.js} +5 -4
  23. package/dist/chunk-BHO7WSAY.js +332 -0
  24. package/dist/{chunk-WIICLBNF.js → chunk-GFJ3LIIB.js} +1 -1
  25. package/dist/chunk-HRTPQQF2.js +541 -0
  26. package/dist/chunk-HWUNREDJ.js +33 -0
  27. package/dist/{chunk-6RQPD7X6.js → chunk-M25QVSJM.js} +4 -3
  28. package/dist/{chunk-6B3JWM7J.js → chunk-O7XHXF7F.js} +34 -7
  29. package/dist/chunk-PLZKZW4I.js +406 -0
  30. package/dist/{chunk-PAYUH64O.js → chunk-QVMXF7FY.js} +11 -1
  31. package/dist/{chunk-TMZMN7OS.js → chunk-S2IG7VNM.js} +24 -12
  32. package/dist/{chunk-LMCC5OC7.js → chunk-TPDH3JPP.js} +1 -1
  33. package/dist/cli/index.d.ts +5 -0
  34. package/dist/cli/index.js +31 -0
  35. package/dist/commands/canvas.js +3 -3
  36. package/dist/commands/compat.js +1 -1
  37. package/dist/commands/context.js +4 -4
  38. package/dist/commands/doctor.js +16 -309
  39. package/dist/commands/embed.d.ts +17 -0
  40. package/dist/commands/embed.js +10 -0
  41. package/dist/commands/migrate-observations.js +2 -2
  42. package/dist/commands/observe.d.ts +1 -0
  43. package/dist/commands/observe.js +7 -6
  44. package/dist/commands/rebuild.js +5 -5
  45. package/dist/commands/reflect.js +3 -3
  46. package/dist/commands/replay.js +7 -7
  47. package/dist/commands/setup.d.ts +1 -0
  48. package/dist/commands/setup.js +2 -2
  49. package/dist/commands/sleep.d.ts +2 -1
  50. package/dist/commands/sleep.js +15 -15
  51. package/dist/commands/status.d.ts +9 -1
  52. package/dist/commands/status.js +33 -8
  53. package/dist/commands/wake.d.ts +1 -1
  54. package/dist/commands/wake.js +6 -6
  55. package/dist/index.d.ts +82 -5
  56. package/dist/index.js +127 -105
  57. package/dist/{types-jjuYN2Xn.d.ts → types-C74wgGL1.d.ts} +2 -0
  58. package/hooks/clawvault/handler.js +11 -7
  59. package/hooks/clawvault/handler.test.js +2 -2
  60. package/package.json +2 -2
  61. package/dist/chunk-2RK2AG32.js +0 -743
  62. package/dist/{chunk-FW465EEA.js → chunk-VXEOHTSL.js} +3 -3
  63. package/dist/{chunk-KCCHROBR.js → chunk-YOSEUUNB.js} +4 -4
package/README.md CHANGED
@@ -92,6 +92,7 @@ Auto-compress conversations into prioritized observations. Critical items route
92
92
  ```bash
93
93
  clawvault observe --compress session.md # One-shot compression
94
94
  clawvault observe --active # Incremental from transcripts
95
+ clawvault observe --cron # Cron-safe one-shot summary + exit code
95
96
  ```
96
97
 
97
98
  ### 🛡️ Context Death Recovery
@@ -121,6 +121,7 @@ describe('CLI command registration modules', () => {
121
121
  const names = listCommandNames(program);
122
122
  expect(names).toEqual(expect.arrayContaining([
123
123
  'doctor',
124
+ 'embed',
124
125
  'compat',
125
126
  'graph',
126
127
  'entities',
@@ -8,6 +8,8 @@ import {
8
8
  resolveVaultPath as resolveConfiguredVaultPath
9
9
  } from '../dist/index.js';
10
10
 
11
+ const QMD_INDEX_ENV_VAR = 'CLAWVAULT_QMD_INDEX';
12
+
11
13
  /**
12
14
  * Validates that a path is within an allowed base directory.
13
15
  * Prevents path traversal attacks.
@@ -29,15 +31,29 @@ export function validatePathWithinBase(inputPath, basePath) {
29
31
 
30
32
  /**
31
33
  * Sanitizes an argument that may contain a path to prevent injection.
32
- * @param {string} arg - The argument to sanitize
34
+ * @param {unknown} arg - The argument to sanitize
33
35
  * @returns {string} The sanitized argument
34
36
  */
35
- function sanitizeQmdArg(arg) {
37
+ export function sanitizeQmdArg(arg) {
38
+ const normalizedArg = String(arg);
36
39
  // Reject arguments with null bytes (injection attempt)
37
- if (arg.includes('\0')) {
40
+ if (normalizedArg.includes('\0')) {
38
41
  throw new Error('Invalid argument: contains null byte');
39
42
  }
40
- return arg;
43
+ return normalizedArg;
44
+ }
45
+
46
+ function withQmdIndex(args) {
47
+ if (args.includes('--index')) {
48
+ return [...args];
49
+ }
50
+
51
+ const indexName = process.env[QMD_INDEX_ENV_VAR]?.trim();
52
+ if (!indexName) {
53
+ return [...args];
54
+ }
55
+
56
+ return ['--index', indexName, ...args];
41
57
  }
42
58
 
43
59
  export function resolveVaultPath(vaultPath) {
@@ -53,7 +69,7 @@ export async function getVault(vaultPath) {
53
69
  export async function runQmd(args) {
54
70
  return new Promise((resolve, reject) => {
55
71
  // Sanitize all arguments before passing to spawn
56
- const sanitizedArgs = args.map(sanitizeQmdArg);
72
+ const sanitizedArgs = withQmdIndex(args).map(sanitizeQmdArg);
57
73
  const proc = spawn('qmd', sanitizedArgs, { stdio: 'inherit' });
58
74
  proc.on('close', (code) => {
59
75
  if (code === 0) resolve();
@@ -1,4 +1,5 @@
1
1
  import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import path from 'path';
2
3
 
3
4
  const {
4
5
  spawnMock,
@@ -68,6 +69,40 @@ describe('command runtime helpers', () => {
68
69
  await expect(runQmd(['update'])).rejects.toBeInstanceOf(QmdUnavailableError);
69
70
  });
70
71
 
72
+ it('injects qmd index from environment when configured', async () => {
73
+ const previous = process.env.CLAWVAULT_QMD_INDEX;
74
+ process.env.CLAWVAULT_QMD_INDEX = 'clawvault-test';
75
+
76
+ try {
77
+ const { runQmd } = await loadRuntimeModule();
78
+ spawnMock.mockImplementation((_command, _args) => {
79
+ const handlers = {};
80
+ const proc = {
81
+ on: (event, handler) => {
82
+ handlers[event] = handler;
83
+ }
84
+ };
85
+ queueMicrotask(() => {
86
+ handlers.close?.(0);
87
+ });
88
+ return proc;
89
+ });
90
+
91
+ await runQmd(['update']);
92
+ expect(spawnMock).toHaveBeenCalledWith(
93
+ 'qmd',
94
+ ['--index', 'clawvault-test', 'update'],
95
+ { stdio: 'inherit' }
96
+ );
97
+ } finally {
98
+ if (previous === undefined) {
99
+ delete process.env.CLAWVAULT_QMD_INDEX;
100
+ } else {
101
+ process.env.CLAWVAULT_QMD_INDEX = previous;
102
+ }
103
+ }
104
+ });
105
+
71
106
  it('surfaces qmd non-zero exit codes as errors', async () => {
72
107
  const { runQmd } = await loadRuntimeModule();
73
108
  spawnMock.mockImplementation(() => {
@@ -86,6 +121,23 @@ describe('command runtime helpers', () => {
86
121
  await expect(runQmd(['update'])).rejects.toThrow('qmd exited with code 2');
87
122
  });
88
123
 
124
+ it('exports and enforces argument/path sanitization helpers', async () => {
125
+ const { sanitizeQmdArg, validatePathWithinBase } = await loadRuntimeModule();
126
+ expect(sanitizeQmdArg('update')).toBe('update');
127
+ expect(sanitizeQmdArg(42)).toBe('42');
128
+ expect(() => sanitizeQmdArg('bad\0arg')).toThrow('contains null byte');
129
+
130
+ const safePath = validatePathWithinBase('notes/today.md', '/tmp/vault');
131
+ expect(safePath).toBe(path.resolve('/tmp/vault', 'notes/today.md'));
132
+ expect(() => validatePathWithinBase('../etc/passwd', '/tmp/vault')).toThrow('Path traversal detected');
133
+ });
134
+
135
+ it('rejects qmd args with null-byte injection attempts', async () => {
136
+ const { runQmd } = await loadRuntimeModule();
137
+ await expect(runQmd(['up\0date'])).rejects.toThrow('contains null byte');
138
+ expect(spawnMock).not.toHaveBeenCalled();
139
+ });
140
+
89
141
  it('prints consistent qmd missing guidance', async () => {
90
142
  const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
91
143
  const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
@@ -7,22 +7,33 @@ describe('CLI help contract', () => {
7
7
  expect(help).toContain('init');
8
8
  expect(help).toContain('context');
9
9
  expect(help).toContain('inject');
10
+ expect(help).toContain('doctor');
11
+ expect(help).toContain('embed');
10
12
  expect(help).toContain('compat');
11
13
  expect(help).toContain('graph');
12
14
  expect(help).toContain('reflect');
13
15
  expect(help).toContain('replay');
14
16
  expect(help).toContain('repair-session');
17
+ expect(help).toContain('project');
15
18
  expect(help).toContain('template');
16
19
  expect(help).toContain('config');
17
20
  expect(help).toContain('route');
18
21
  });
19
22
 
20
- it('documents context auto profile and compat strict options', () => {
23
+ it('documents context/compat/inject/project help details', () => {
21
24
  const program = registerAllCommandModules();
22
25
  const contextHelp = program.commands.find((command) => command.name() === 'context')?.helpInformation() ?? '';
23
26
  const compatHelp = program.commands.find((command) => command.name() === 'compat')?.helpInformation() ?? '';
27
+ const injectHelp = program.commands.find((command) => command.name() === 'inject')?.helpInformation() ?? '';
28
+ const projectCommand = program.commands.find((command) => command.name() === 'project');
29
+ const projectListHelp = projectCommand?.commands.find((command) => command.name() === 'list')?.helpInformation() ?? '';
30
+ const projectBoardHelp = projectCommand?.commands.find((command) => command.name() === 'board')?.helpInformation() ?? '';
24
31
  expect(contextHelp).toContain('--profile <profile>');
25
32
  expect(contextHelp).toContain('auto');
26
33
  expect(compatHelp).toContain('--strict');
34
+ expect(injectHelp).toContain('inject.maxResults');
35
+ expect(injectHelp).toContain('inject.scope');
36
+ expect(projectListHelp).toContain('archived projects are hidden');
37
+ expect(projectBoardHelp).toContain('default: status');
27
38
  });
28
39
  });
@@ -66,7 +66,7 @@ function normalizeConfigKey(key) {
66
66
  export function registerConfigCommands(program, { chalk, resolveVaultPath }) {
67
67
  const config = program
68
68
  .command('config')
69
- .description('Read and modify runtime config');
69
+ .description('Read and modify runtime configuration');
70
70
 
71
71
  config
72
72
  .command('get <key>')
@@ -135,7 +135,7 @@ export function registerConfigCommands(program, { chalk, resolveVaultPath }) {
135
135
  config
136
136
  .command('reset')
137
137
  .description('Reset runtime config values to defaults')
138
- .option('--confirm', 'Confirm reset')
138
+ .option('--confirm', 'Confirm reset (required)')
139
139
  .option('-v, --vault <path>', 'Vault path')
140
140
  .action(async (options) => {
141
141
  try {
@@ -11,8 +11,8 @@ export function registerCoreCommands(
11
11
  // === INIT ===
12
12
  program
13
13
  .command('init [path]')
14
- .description('Initialize a new ClawVault')
15
- .option('-n, --name <name>', 'Vault name')
14
+ .description('Initialize a new ClawVault vault')
15
+ .option('-n, --name <name>', 'Vault name (default: target directory name)')
16
16
  .option('--qmd', 'Set up qmd semantic search collection')
17
17
  .option('--qmd-collection <name>', 'qmd collection name (defaults to vault name)')
18
18
  .option('--no-bases', 'Skip Obsidian Bases file generation')
@@ -20,7 +20,7 @@ export function registerCoreCommands(
20
20
  .option('--no-graph', 'Skip initial graph build')
21
21
  .option('--categories <list>', 'Comma-separated list of custom categories to create')
22
22
  .option('--canvas', 'Generate a vault status canvas dashboard on init')
23
- .option('--theme <style>', 'Graph color theme to apply (neural, minimal, none)', 'none')
23
+ .option('--theme <style>', 'Graph color theme to apply (neural, minimal, none) (default: none)', 'none')
24
24
  .option('--minimal', 'Create minimal vault (memory categories only, no tasks/bases/graph)')
25
25
  .action(async (vaultPath, options) => {
26
26
  const targetPath = vaultPath || '.';
@@ -135,14 +135,14 @@ export function registerCoreCommands(
135
135
  // === SETUP ===
136
136
  program
137
137
  .command('setup')
138
- .description('Auto-discover and configure a ClawVault')
138
+ .description('Auto-discover and configure an existing ClawVault vault')
139
139
  .option('--graph-colors', 'Set up graph color scheme for Obsidian')
140
140
  .option('--no-graph-colors', 'Skip graph color configuration')
141
141
  .option('--bases', 'Generate Obsidian Bases views for task management')
142
142
  .option('--no-bases', 'Skip Bases file generation')
143
143
  .option('--canvas', 'Generate vault status canvas dashboard')
144
144
  .option('--no-canvas', 'Skip canvas generation')
145
- .option('--theme <style>', 'Graph color theme (neural, minimal, none)', 'neural')
145
+ .option('--theme <style>', 'Graph color theme (neural, minimal, none) (default: neural)', 'neural')
146
146
  .option('--force', 'Overwrite existing configuration files')
147
147
  .option('-v, --vault <path>', 'Vault path')
148
148
  .action(async (options) => {
@@ -165,11 +165,11 @@ export function registerCoreCommands(
165
165
  // === STORE ===
166
166
  program
167
167
  .command('store')
168
- .description('Store a new memory')
168
+ .description('Store a new memory document')
169
169
  .requiredOption('-c, --category <category>', 'Category (preferences, decisions, patterns, people, projects, goals, transcripts, inbox)')
170
170
  .requiredOption('-t, --title <title>', 'Document title')
171
171
  .option('--content <content>', 'Content body')
172
- .option('-f, --file <file>', 'Read content from file')
172
+ .option('-f, --file <file>', 'Read content from file (validated against current working directory)')
173
173
  .option('--stdin', 'Read content from stdin')
174
174
  .option('--overwrite', 'Overwrite if exists')
175
175
  .option('--no-index', 'Skip qmd index update (auto-updates by default)')
@@ -215,10 +215,10 @@ export function registerCoreCommands(
215
215
  // === CAPTURE ===
216
216
  program
217
217
  .command('capture <note>')
218
- .description('Quick capture to inbox')
218
+ .description('Quick-capture a note to inbox')
219
219
  .option('-t, --title <title>', 'Note title')
220
220
  .option('-v, --vault <path>', 'Vault path')
221
- .option('--no-index', 'Skip qmd index update')
221
+ .option('--no-index', 'Skip qmd index update (auto-updates by default)')
222
222
  .action(async (note, options) => {
223
223
  try {
224
224
  const vault = await getVault(options.vault);
@@ -8,17 +8,17 @@ export function registerKanbanCommands(
8
8
  ) {
9
9
  const kanbanCmd = program
10
10
  .command('kanban')
11
- .description('Sync Obsidian Kanban boards with task frontmatter');
11
+ .description('Manage Obsidian Kanban sync for task frontmatter');
12
12
 
13
13
  kanbanCmd
14
14
  .command('sync')
15
15
  .description('Generate and sync an Obsidian Kanban board from tasks')
16
- .option('-v, --vault <path>', 'Vault path')
16
+ .option('-v, --vault <path>', 'Vault path (default: find nearest)')
17
17
  .option('--output <path>', 'Board markdown path (default: Board.md)')
18
- .option('--group-by <field>', 'Grouping field (status, priority, project, owner)')
18
+ .option('--group-by <field>', 'Grouping field (status, priority, project, owner) (default: status)')
19
19
  .option('--filter-project <project>', 'Only include tasks from a project')
20
20
  .option('--filter-owner <owner>', 'Only include tasks for an owner')
21
- .option('--include-done', 'Include done tasks')
21
+ .option('--include-done', 'Include done tasks (default: hidden)')
22
22
  .action(async (options) => {
23
23
  try {
24
24
  const vaultPath = resolveVaultPath(options.vault);
@@ -39,7 +39,7 @@ export function registerKanbanCommands(
39
39
  kanbanCmd
40
40
  .command('import')
41
41
  .description('Import lane state from an Obsidian Kanban board into tasks')
42
- .option('-v, --vault <path>', 'Vault path')
42
+ .option('-v, --vault <path>', 'Vault path (default: find nearest)')
43
43
  .option('--output <path>', 'Board markdown path (default: Board.md)')
44
44
  .action(async (options) => {
45
45
  try {
@@ -7,27 +7,42 @@ export function registerMaintenanceCommands(program, { chalk }) {
7
7
  // === DOCTOR (health check) ===
8
8
  program
9
9
  .command('doctor')
10
- .description('Check ClawVault setup health')
10
+ .description('Diagnose vault health and optionally apply fixes')
11
11
  .option('-v, --vault <path>', 'Vault path')
12
+ .option('--fix', 'Apply safe auto-fixes for qmd index, embeddings, and dead collections')
13
+ .option('--json', 'Output machine-readable JSON')
12
14
  .action(async (options) => {
13
15
  try {
14
16
  const { doctor } = await import('../dist/commands/doctor.js');
15
- const report = await doctor(options.vault);
17
+ const report = await doctor({
18
+ vaultPath: options.vault,
19
+ fix: options.fix
20
+ });
16
21
 
17
- console.log(chalk.cyan('\n🩺 ClawVault Health Check\n'));
18
- if (report.vaultPath) {
19
- console.log(chalk.dim(`Vault: ${report.vaultPath}`));
20
- console.log();
22
+ if (options.json) {
23
+ console.log(JSON.stringify(report, null, 2));
24
+ return;
21
25
  }
22
26
 
27
+ console.log(chalk.cyan('\n🩺 ClawVault Health Check\n'));
28
+ console.log(chalk.dim(`Vault: ${report.vaultPath}`));
29
+ console.log(chalk.dim(`qmd collection: ${report.qmdCollection}`));
30
+ console.log(chalk.dim(`qmd root: ${report.qmdRoot}`));
31
+ console.log();
32
+
23
33
  for (const check of report.checks) {
24
34
  const prefix = check.status === 'ok'
25
35
  ? chalk.green('✓')
26
36
  : check.status === 'warn'
27
37
  ? chalk.yellow('⚠')
28
38
  : chalk.red('✗');
29
- const detail = check.detail ? ` — ${check.detail}` : '';
30
- console.log(`${prefix} ${check.label}${detail}`);
39
+ const line = `${check.label}: ${check.detail}`;
40
+ const renderedLine = check.status === 'ok'
41
+ ? chalk.green(line)
42
+ : check.status === 'warn'
43
+ ? chalk.yellow(line)
44
+ : chalk.red(line);
45
+ console.log(`${prefix} ${renderedLine}`);
31
46
  if (check.hint) {
32
47
  console.log(chalk.dim(` ${check.hint}`));
33
48
  }
@@ -38,7 +53,9 @@ export function registerMaintenanceCommands(program, { chalk }) {
38
53
  if (issues === 0) {
39
54
  console.log(chalk.green('✅ ClawVault is healthy!\n'));
40
55
  } else {
41
- console.log(chalk.yellow(`⚠ ${issues} issue(s) found\n`));
56
+ const summary = `${report.errors} error(s), ${report.warnings} warning(s)`;
57
+ const colorized = report.errors > 0 ? chalk.red(summary) : chalk.yellow(summary);
58
+ console.log(`${report.errors > 0 ? '✗' : '⚠'} ${colorized}\n`);
42
59
  }
43
60
  } catch (err) {
44
61
  console.error(chalk.red(`Error: ${err.message}`));
@@ -46,6 +63,23 @@ export function registerMaintenanceCommands(program, { chalk }) {
46
63
  }
47
64
  });
48
65
 
66
+ // === EMBED ===
67
+ program
68
+ .command('embed')
69
+ .description('Run qmd embedding for pending vault documents')
70
+ .option('-v, --vault <path>', 'Vault path')
71
+ .action(async (options) => {
72
+ try {
73
+ const { embedCommand } = await import('../dist/commands/embed.js');
74
+ await embedCommand({
75
+ vaultPath: options.vault
76
+ });
77
+ } catch (err) {
78
+ console.error(chalk.red(`Error: ${err.message}`));
79
+ process.exit(1);
80
+ }
81
+ });
82
+
49
83
  // === COMPAT (OpenClaw compatibility) ===
50
84
  program
51
85
  .command('compat')
@@ -160,7 +194,7 @@ export function registerMaintenanceCommands(program, { chalk }) {
160
194
  program
161
195
  .command('archive')
162
196
  .description('Archive old observations into ledger/archive')
163
- .option('--older-than <days>', 'Archive observations older than this many days', '14')
197
+ .option('--older-than <days>', 'Archive observations older than this many days (default: 14)', '14')
164
198
  .option('--dry-run', 'Show archive candidates without writing')
165
199
  .option('-v, --vault <path>', 'Vault path')
166
200
  .action(async (options) => {
@@ -18,14 +18,14 @@ export function registerProjectCommands(
18
18
  ) {
19
19
  const projectCmd = program
20
20
  .command('project')
21
- .description('Project management');
21
+ .description('Manage projects and project boards');
22
22
 
23
23
  projectCmd
24
24
  .command('add <title>')
25
25
  .description('Add a new project')
26
- .option('-v, --vault <path>', 'Vault path')
26
+ .option('-v, --vault <path>', 'Vault path (default: find nearest)')
27
27
  .option('--owner <owner>', 'Project owner')
28
- .option('--status <status>', 'Project status (active, paused, completed, archived)')
28
+ .option('--status <status>', 'Project status (active, paused, completed, archived) (default: active)')
29
29
  .option('--team <team>', 'Comma-separated team members')
30
30
  .option('--client <client>', 'Client name')
31
31
  .option('--tags <tags>', 'Comma-separated tags')
@@ -60,7 +60,7 @@ export function registerProjectCommands(
60
60
  projectCmd
61
61
  .command('update <slug>')
62
62
  .description('Update a project')
63
- .option('-v, --vault <path>', 'Vault path')
63
+ .option('-v, --vault <path>', 'Vault path (default: find nearest)')
64
64
  .option('--status <status>', 'Project status (active, paused, completed, archived)')
65
65
  .option('--owner <owner>', 'Project owner')
66
66
  .option('--team <team>', 'Comma-separated team members')
@@ -97,7 +97,7 @@ export function registerProjectCommands(
97
97
  projectCmd
98
98
  .command('archive <slug>')
99
99
  .description('Archive a project')
100
- .option('-v, --vault <path>', 'Vault path')
100
+ .option('-v, --vault <path>', 'Vault path (default: find nearest)')
101
101
  .option('--reason <reason>', 'Reason for archiving')
102
102
  .action(async (slug, options) => {
103
103
  try {
@@ -117,9 +117,9 @@ export function registerProjectCommands(
117
117
 
118
118
  projectCmd
119
119
  .command('list')
120
- .description('List projects')
121
- .option('-v, --vault <path>', 'Vault path')
122
- .option('--status <status>', 'Filter by status')
120
+ .description('List projects (archived projects are hidden unless --status archived)')
121
+ .option('-v, --vault <path>', 'Vault path (default: find nearest)')
122
+ .option('--status <status>', 'Filter by status (active, paused, completed, archived)')
123
123
  .option('--owner <owner>', 'Filter by owner')
124
124
  .option('--client <client>', 'Filter by client')
125
125
  .option('--tag <tag>', 'Filter by tag')
@@ -146,7 +146,7 @@ export function registerProjectCommands(
146
146
  projectCmd
147
147
  .command('show <slug>')
148
148
  .description('Show project details')
149
- .option('-v, --vault <path>', 'Vault path')
149
+ .option('-v, --vault <path>', 'Vault path (default: find nearest)')
150
150
  .option('--json', 'Output as JSON')
151
151
  .action(async (slug, options) => {
152
152
  try {
@@ -166,8 +166,8 @@ export function registerProjectCommands(
166
166
 
167
167
  projectCmd
168
168
  .command('tasks <slug>')
169
- .description('List tasks for a project')
170
- .option('-v, --vault <path>', 'Vault path')
169
+ .description('List tasks linked to a project')
170
+ .option('-v, --vault <path>', 'Vault path (default: find nearest)')
171
171
  .option('--json', 'Output as JSON')
172
172
  .action(async (slug, options) => {
173
173
  try {
@@ -187,10 +187,10 @@ export function registerProjectCommands(
187
187
 
188
188
  projectCmd
189
189
  .command('board')
190
- .description('Generate project kanban board')
191
- .option('-v, --vault <path>', 'Vault path')
190
+ .description('Generate and sync the project kanban board')
191
+ .option('-v, --vault <path>', 'Vault path (default: find nearest)')
192
192
  .option('--output <path>', 'Board markdown path (default: Projects-Board.md)')
193
- .option('--group-by <field>', 'Grouping field (status, owner, client)')
193
+ .option('--group-by <field>', 'Grouping field (status, owner, client) (default: status)')
194
194
  .action(async (options) => {
195
195
  try {
196
196
  const vaultPath = resolveVaultPath(options.vault);
@@ -76,6 +76,11 @@ describe('register-project-commands', () => {
76
76
  '--output <path>',
77
77
  '--group-by <field>'
78
78
  ]));
79
+
80
+ expect(listCommand?.description()).toContain('archived projects are hidden');
81
+
82
+ const boardGroupByOption = boardCommand?.options.find((option) => option.flags === '--group-by <field>');
83
+ expect(boardGroupByOption?.description).toContain('default: status');
79
84
  });
80
85
 
81
86
  it('dispatches project subcommands to project command handler', async () => {
@@ -16,7 +16,7 @@ export function registerQueryCommands(
16
16
  program
17
17
  .command('search <query>')
18
18
  .description('Search the vault via qmd (BM25)')
19
- .option('-n, --limit <n>', 'Max results', '10')
19
+ .option('-n, --limit <n>', 'Max results (default: 10)', '10')
20
20
  .option('-c, --category <category>', 'Filter by category')
21
21
  .option('--tags <tags>', 'Filter by tags (comma-separated)')
22
22
  .option('--recent', 'Boost recent documents')
@@ -71,7 +71,7 @@ export function registerQueryCommands(
71
71
  program
72
72
  .command('vsearch <query>')
73
73
  .description('Semantic search via qmd (requires qmd installed)')
74
- .option('-n, --limit <n>', 'Max results', '5')
74
+ .option('-n, --limit <n>', 'Max results (default: 5)', '5')
75
75
  .option('-c, --category <category>', 'Filter by category')
76
76
  .option('--tags <tags>', 'Filter by tags (comma-separated)')
77
77
  .option('--recent', 'Boost recent documents')
@@ -126,13 +126,13 @@ export function registerQueryCommands(
126
126
  program
127
127
  .command('context <task>')
128
128
  .description('Generate task-relevant context for prompt injection')
129
- .option('-n, --limit <n>', 'Max results', '5')
130
- .option('--format <format>', 'Output format (markdown|json)', 'markdown')
129
+ .option('-n, --limit <n>', 'Max results (default: 5)', '5')
130
+ .option('--format <format>', 'Output format (markdown|json) (default: markdown)', 'markdown')
131
131
  .option('--recent', 'Boost recent documents (enabled by default)', true)
132
- .option('--include-observations', 'Include observation memories in output', true)
132
+ .option('--include-observations', 'Include observation memories in output (enabled by default)', true)
133
133
  .option('--budget <number>', 'Optional token budget for assembled context')
134
- .option('--profile <profile>', 'Context profile (default|planning|incident|handoff|auto)', 'default')
135
- .option('--max-hops <n>', 'Maximum graph expansion hops', '2')
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
136
  .option('-v, --vault <path>', 'Vault path')
137
137
  .action(async (task, options) => {
138
138
  try {
@@ -171,12 +171,12 @@ export function registerQueryCommands(
171
171
  // === INJECT ===
172
172
  program
173
173
  .command('inject <message>')
174
- .description('Inject relevant rules, decisions, and preferences for prompt context')
175
- .option('-n, --max-results <n>', 'Maximum injected items')
176
- .option('--scope <scope>', 'Comma-separated scope filter override')
177
- .option('--enable-llm', 'Enable optional LLM fuzzy intent matching')
178
- .option('--disable-llm', 'Disable optional LLM fuzzy intent matching')
179
- .option('--format <format>', 'Output format (markdown|json)', 'markdown')
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
180
  .option('--model <model>', 'Override LLM model when fuzzy matching is enabled')
181
181
  .option('-v, --vault <path>', 'Vault path')
182
182
  .action(async (message, options) => {
@@ -214,13 +214,16 @@ export function registerQueryCommands(
214
214
  .description('Observe session files and build observational memory')
215
215
  .option('--watch <path>', 'Watch session file or directory')
216
216
  .option('--active', 'Observe active OpenClaw sessions incrementally')
217
+ .option('--cron', 'Run one-shot active observation for cron hooks')
217
218
  .option('--agent <id>', 'OpenClaw agent ID (default: OPENCLAW_AGENT_ID or clawdious)')
218
219
  .option('--min-new <bytes>', 'Override minimum new-content threshold in bytes')
219
220
  .option('--sessions-dir <path>', 'Override OpenClaw sessions directory')
220
221
  .option('--dry-run', 'Show active observation candidates without compressing')
221
- .option('--threshold <n>', 'Compression token threshold', '30000')
222
- .option('--reflect-threshold <n>', 'Reflection token threshold', '40000')
222
+ .option('--threshold <n>', 'Compression token threshold (default: 30000)', '30000')
223
+ .option('--reflect-threshold <n>', 'Reflection token threshold (default: 40000)', '40000')
223
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')
224
227
  .option('--compress <file>', 'One-shot compression for a conversation file')
225
228
  .option('--daemon', 'Run in detached background mode')
226
229
  .option('-v, --vault <path>', 'Vault path')
@@ -245,6 +248,7 @@ export function registerQueryCommands(
245
248
  await observeCommand({
246
249
  watch: options.watch,
247
250
  active: options.active,
251
+ cron: options.cron,
248
252
  agent: options.agent,
249
253
  minNew,
250
254
  sessionsDir: options.sessionsDir,
@@ -252,6 +256,7 @@ export function registerQueryCommands(
252
256
  threshold,
253
257
  reflectThreshold,
254
258
  model: options.model,
259
+ extractTasks: options.extractTasks,
255
260
  compress: options.compress,
256
261
  daemon: options.daemon,
257
262
  vaultPath: resolveVaultPath(options.vault)
@@ -266,7 +271,7 @@ export function registerQueryCommands(
266
271
  program
267
272
  .command('reflect')
268
273
  .description('Promote stable observations into weekly reflections')
269
- .option('--days <n>', 'Observation window in days', '14')
274
+ .option('--days <n>', 'Observation window in days (default: 14)', '14')
270
275
  .option('--dry-run', 'Show reflection output candidates without writing')
271
276
  .option('-v, --vault <path>', 'Vault path')
272
277
  .action(async (options) => {
@@ -291,8 +296,8 @@ export function registerQueryCommands(
291
296
  program
292
297
  .command('session-recap <sessionKey>')
293
298
  .description('Generate recap from a specific OpenClaw session transcript')
294
- .option('-n, --limit <n>', 'Number of messages to include', '15')
295
- .option('--format <format>', 'Output format (markdown|json)', 'markdown')
299
+ .option('-n, --limit <n>', 'Number of messages to include (default: 15)', '15')
300
+ .option('--format <format>', 'Output format (markdown|json) (default: markdown)', 'markdown')
296
301
  .option('-a, --agent <id>', 'Agent ID (default: OPENCLAW_AGENT_ID or clawdious)')
297
302
  .action(async (sessionKey, options) => {
298
303
  try {