clawvault 3.2.0 → 3.3.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.
Files changed (112) hide show
  1. package/README.md +54 -14
  2. package/bin/clawvault.js +0 -2
  3. package/bin/command-registration.test.js +13 -1
  4. package/bin/help-contract.test.js +14 -0
  5. package/bin/register-core-commands.js +88 -0
  6. package/bin/register-core-commands.test.js +80 -0
  7. package/bin/register-maintenance-commands.js +57 -6
  8. package/bin/register-query-commands.js +10 -28
  9. package/bin/test-helpers/cli-command-fixtures.js +1 -0
  10. package/dist/chunk-2PKBIKDH.js +130 -0
  11. package/dist/{chunk-2JQ3O2YL.js → chunk-5EFSWZO6.js} +3 -3
  12. package/dist/{chunk-77Q5CSPJ.js → chunk-7SWP5FKU.js} +33 -701
  13. package/dist/{chunk-URXDAUVH.js → chunk-AXSJIFOJ.js} +174 -1
  14. package/dist/{chunk-23YDQ3QU.js → chunk-BLQXXX7Q.js} +6 -6
  15. package/dist/chunk-CSHO3PJB.js +684 -0
  16. package/dist/{chunk-SLXOR3CC.js → chunk-DOIUYIXV.js} +2 -2
  17. package/dist/{chunk-NCKFNBHJ.js → chunk-DVOUSOR3.js} +79 -5
  18. package/dist/{chunk-CLJTREDS.js → chunk-ECGJYWNA.js} +193 -41
  19. package/dist/{chunk-BUEW6IIK.js → chunk-EL6UBSX5.js} +5 -5
  20. package/dist/{chunk-6FH3IULF.js → chunk-FZ5I2NF7.js} +1 -1
  21. package/dist/{chunk-ZN54U2OZ.js → chunk-GFCHWMGD.js} +3 -3
  22. package/dist/{chunk-GNJL4YGR.js → chunk-GJO3CFUN.js} +30 -6
  23. package/dist/chunk-H3JZIB5O.js +322 -0
  24. package/dist/chunk-HEHO7SMV.js +51 -0
  25. package/dist/{chunk-STCQGCEQ.js → chunk-HGDDW24U.js} +3 -3
  26. package/dist/chunk-J3YUXVID.js +907 -0
  27. package/dist/{chunk-Y6VJKXGL.js → chunk-KCYWJDDW.js} +1 -1
  28. package/dist/{chunk-W4SPAEE7.js → chunk-OFOCU2V4.js} +5 -4
  29. package/dist/chunk-PTWPPVC7.js +972 -0
  30. package/dist/{chunk-QSHD36LH.js → chunk-QFWERBDP.js} +2 -2
  31. package/dist/{chunk-QSRRMEYM.js → chunk-S7N7HI5E.js} +1 -1
  32. package/dist/{chunk-PBACDKKP.js → chunk-T7E764W3.js} +3 -3
  33. package/dist/chunk-TDWFBDAQ.js +1016 -0
  34. package/dist/{chunk-ESVS6K2B.js → chunk-TWMI3SNN.js} +6 -5
  35. package/dist/{chunk-2RAZ4ZFE.js → chunk-VBILES4B.js} +1 -1
  36. package/dist/{chunk-ESFLMDRB.js → chunk-VXAGOLDP.js} +3 -3
  37. package/dist/chunk-YCUVAOFC.js +158 -0
  38. package/dist/{chunk-SS4B7P7V.js → chunk-YIDV4VV2.js} +1 -1
  39. package/dist/chunk-ZKWPCBYT.js +600 -0
  40. package/dist/cli/index.js +24 -24
  41. package/dist/commands/archive.js +2 -2
  42. package/dist/commands/benchmark.d.ts +12 -0
  43. package/dist/commands/benchmark.js +12 -0
  44. package/dist/commands/context.js +6 -5
  45. package/dist/commands/doctor.d.ts +8 -3
  46. package/dist/commands/doctor.js +6 -20
  47. package/dist/commands/embed.js +5 -4
  48. package/dist/commands/entities.js +1 -1
  49. package/dist/commands/graph.js +2 -2
  50. package/dist/commands/inbox.d.ts +23 -0
  51. package/dist/commands/inbox.js +11 -0
  52. package/dist/commands/inject.d.ts +1 -1
  53. package/dist/commands/inject.js +3 -3
  54. package/dist/commands/link.js +6 -6
  55. package/dist/commands/maintain.d.ts +32 -0
  56. package/dist/commands/maintain.js +12 -0
  57. package/dist/commands/migrate-observations.js +2 -2
  58. package/dist/commands/observe.js +9 -8
  59. package/dist/commands/rebuild-embeddings.js +47 -16
  60. package/dist/commands/rebuild.js +7 -6
  61. package/dist/commands/reflect.js +5 -5
  62. package/dist/commands/replay.js +8 -7
  63. package/dist/commands/setup.js +3 -2
  64. package/dist/commands/sleep.d.ts +1 -1
  65. package/dist/commands/sleep.js +17 -15
  66. package/dist/commands/status.js +26 -24
  67. package/dist/commands/sync-bd.js +2 -2
  68. package/dist/commands/tailscale.js +2 -2
  69. package/dist/commands/wake.d.ts +1 -1
  70. package/dist/commands/wake.js +8 -7
  71. package/dist/index.d.ts +168 -16
  72. package/dist/index.js +271 -108
  73. package/dist/{inject-DYUrDqQO.d.ts → inject-DEb_jpLi.d.ts} +3 -1
  74. package/dist/lib/config.js +1 -1
  75. package/dist/{types-BbWJoC1c.d.ts → types-DslKvCaj.d.ts} +51 -1
  76. package/hooks/clawvault/HOOK.md +22 -5
  77. package/hooks/clawvault/handler.js +213 -78
  78. package/hooks/clawvault/handler.test.js +109 -43
  79. package/hooks/clawvault/integrity.js +112 -0
  80. package/hooks/clawvault/integrity.test.js +32 -0
  81. package/hooks/clawvault/openclaw.plugin.json +133 -15
  82. package/openclaw.plugin.json +126 -20
  83. package/package.json +2 -2
  84. package/bin/register-workgraph-commands.js +0 -1368
  85. package/dist/chunk-33VSQP4J.js +0 -37
  86. package/dist/chunk-4BQTQMJP.js +0 -93
  87. package/dist/chunk-EK6S23ZB.js +0 -469
  88. package/dist/chunk-GAOWA7GR.js +0 -501
  89. package/dist/chunk-GGA32J2R.js +0 -784
  90. package/dist/chunk-MM6QGW3P.js +0 -207
  91. package/dist/chunk-QVEERJSP.js +0 -152
  92. package/dist/chunk-U4O6C46S.js +0 -154
  93. package/dist/chunk-VSL7KY3M.js +0 -189
  94. package/dist/chunk-WMGIIABP.js +0 -15
  95. package/dist/commands/workgraph.d.ts +0 -124
  96. package/dist/commands/workgraph.js +0 -38
  97. package/dist/ledger-B7g7jhqG.d.ts +0 -44
  98. package/dist/registry-BR4326o0.d.ts +0 -30
  99. package/dist/store-CA-6sKCJ.d.ts +0 -34
  100. package/dist/thread-B9LhXNU0.d.ts +0 -41
  101. package/dist/workgraph/index.d.ts +0 -5
  102. package/dist/workgraph/index.js +0 -23
  103. package/dist/workgraph/ledger.d.ts +0 -2
  104. package/dist/workgraph/ledger.js +0 -25
  105. package/dist/workgraph/registry.d.ts +0 -2
  106. package/dist/workgraph/registry.js +0 -19
  107. package/dist/workgraph/store.d.ts +0 -2
  108. package/dist/workgraph/store.js +0 -25
  109. package/dist/workgraph/thread.d.ts +0 -2
  110. package/dist/workgraph/thread.js +0 -25
  111. package/dist/workgraph/types.d.ts +0 -54
  112. package/dist/workgraph/types.js +0 -7
package/README.md CHANGED
@@ -59,7 +59,7 @@ Unlike vector databases or cloud-based memory solutions, ClawVault is:
59
59
  │ ▼ ▼ │
60
60
  │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
61
61
  │ │ wake │◀──▶│ context │◀──▶│ Graph │◀──▶│ Search │ │
62
- │ │ sleep │ │ profiles │ │ Traversal│ │(qmd/vec) │ │
62
+ │ │ sleep │ │ profiles │ │ Traversal│ │(hybrid) │ │
63
63
  │ │checkpoint│ └──────────┘ └──────────┘ └──────────┘ │
64
64
  │ └──────────┘ │
65
65
  │ │
@@ -97,8 +97,11 @@ These primitives map directly to CLI commands and vault structure, creating a co
97
97
  # Install ClawVault CLI
98
98
  npm install -g clawvault
99
99
 
100
- # Install qmd (required for search/context features)
100
+ # Optional: install qmd for backward-compatible fallback paths
101
101
  npm install -g github:tobi/qmd
102
+
103
+ # Quick verification
104
+ clawvault doctor
102
105
  ```
103
106
 
104
107
  ### Initialize Your Vault
@@ -131,12 +134,18 @@ clawvault sleep "finished auth rollout" --next "implement migration"
131
134
  ### Search and Context
132
135
 
133
136
  ```bash
134
- # Keyword search
137
+ # In-process hybrid search (BM25 + semantic reranking)
135
138
  clawvault search "postgresql"
136
139
 
137
- # Semantic search
140
+ # Semantic/vector search commands (requires hosted embeddings configured)
138
141
  clawvault vsearch "what did we decide about storage"
139
142
 
143
+ # Configure hosted embeddings (OpenAI/Gemini/Ollama)
144
+ clawvault config set search.embeddings.provider openai
145
+ clawvault config set search.embeddings.model text-embedding-3-small
146
+ clawvault config set search.embeddings.apiKey "$OPENAI_API_KEY"
147
+ clawvault rebuild-embeddings
148
+
140
149
  # Get context for a task
141
150
  clawvault context "database migration"
142
151
  clawvault context --profile planning "Q1 roadmap"
@@ -163,6 +172,12 @@ ClawVault v3 adds **write-time fact extraction** and **entity graphs** to the co
163
172
 
164
173
  ## Features
165
174
 
175
+ ### v3.3 Highlights
176
+
177
+ - **In-process hybrid search engine** — BM25 + hosted semantic embeddings + cross-encoder reranking, with `qmd` now optional.
178
+ - **Python SDK (`clawvault-py`)** — PyPI package with a `Vault` class, BM25 search, and checkpoint/wake lifecycle helpers.
179
+ - **Inbox + background workers** — `clawvault inbox add` and `clawvault maintain` with Curator, Janitor, Distiller, and Surveyor workers.
180
+
166
181
  ### Memory Graph
167
182
 
168
183
  ClawVault builds a typed knowledge graph from wiki-links, tags, and frontmatter:
@@ -215,6 +230,20 @@ clawvault inject "How should we handle the deployment?"
215
230
  clawvault inject --enable-llm "What's our pricing strategy?"
216
231
  ```
217
232
 
233
+ ### Python SDK (clawvault-py)
234
+
235
+ ```bash
236
+ pip install clawvault-py
237
+ ```
238
+
239
+ ```python
240
+ from clawvault import Vault
241
+
242
+ vault = Vault("~/memory")
243
+ results = vault.search_bm25("postgresql decision")
244
+ vault.checkpoint("working on auth rollout")
245
+ ```
246
+
218
247
  ---
219
248
 
220
249
  ## Obsidian Integration
@@ -269,10 +298,14 @@ See [docs/openclaw-plugin-usage.md](docs/openclaw-plugin-usage.md) for detailed
269
298
 
270
299
  ---
271
300
 
272
- ## Requirements
301
+ ## System Requirements
302
+
303
+ - Node.js 18+ (22+ recommended)
304
+ - npm 9+
305
+ - Linux, macOS, Windows, or WSL
306
+ - `qmd` is optional (used only for backward-compatible fallback paths; in-process search is built in)
273
307
 
274
- - Node.js 18+
275
- - `qmd` installed and available on `PATH` (for search/context features)
308
+ For Linux-specific install and PATH guidance, see [docs/getting-started/installation.md](docs/getting-started/installation.md).
276
309
 
277
310
  ## LLM Providers
278
311
 
@@ -444,14 +477,21 @@ vault/
444
477
 
445
478
  ## Troubleshooting
446
479
 
447
- - Hook not found after enable:
448
- - run `openclaw hooks install clawvault` first
449
- - then `openclaw hooks enable clawvault`
450
- - restart gateway
480
+ - First-line diagnostic:
481
+ - run `clawvault doctor` after install or environment changes
482
+ - Global install fails with `EACCES` / permission denied:
483
+ - run `npm config set prefix ~/.npm-global`
484
+ - add `export PATH="$HOME/.npm-global/bin:$PATH"` to your shell rc and reload shell
485
+ - `clawvault: command not found` after install:
486
+ - check `npm config get prefix`
487
+ - ensure `<prefix>/bin` is present in your `PATH`
488
+ - `qmd` fallback errors:
489
+ - `qmd` is optional; in-process BM25 search is available without it
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`
451
494
  - verify with `openclaw hooks list --verbose`
452
- - `qmd` errors:
453
- - ensure `qmd --version` works from same shell
454
- - rerun `clawvault setup` after qmd install
455
495
  - OpenClaw integration drift:
456
496
  - run `clawvault compat`
457
497
  - Session transcript corruption:
package/bin/clawvault.js CHANGED
@@ -23,7 +23,6 @@ import { registerProjectCommands } from './register-project-commands.js';
23
23
 
24
24
  import { registerTaskCommands } from './register-task-commands.js';
25
25
 
26
- import { registerWorkgraphCommands } from './register-workgraph-commands.js';
27
26
  import { registerTailscaleCommands } from './register-tailscale-commands.js';
28
27
  import {
29
28
  getVault,
@@ -109,7 +108,6 @@ registerProjectCommands(program, {
109
108
  resolveVaultPath
110
109
  });
111
110
 
112
- registerWorkgraphCommands(program, { chalk, resolveVaultPath });
113
111
  registerTailscaleCommands(program, { chalk });
114
112
  registerConfigCommands(program, { chalk, resolveVaultPath });
115
113
  registerRouteCommands(program, { chalk, resolveVaultPath });
@@ -35,7 +35,7 @@ describe('CLI command registration modules', () => {
35
35
  });
36
36
 
37
37
  const names = listCommandNames(program);
38
- expect(names).toEqual(expect.arrayContaining(['init', 'setup', 'store', 'capture']));
38
+ expect(names).toEqual(expect.arrayContaining(['init', 'setup', 'store', 'patch', 'capture', 'inbox']));
39
39
  });
40
40
 
41
41
  it('registers query commands with profile option', () => {
@@ -121,6 +121,8 @@ describe('CLI command registration modules', () => {
121
121
  const names = listCommandNames(program);
122
122
  expect(names).toEqual(expect.arrayContaining([
123
123
  'doctor',
124
+ 'benchmark',
125
+ 'maintain',
124
126
  'embed',
125
127
  'compat',
126
128
  'graph',
@@ -163,4 +165,14 @@ describe('CLI command registration modules', () => {
163
165
  const unique = new Set(names);
164
166
  expect(unique.size).toBe(names.length);
165
167
  });
168
+
169
+ it('does not register removed workgraph commands', () => {
170
+ const program = registerAllCommandModules(new Command());
171
+ const names = listCommandNames(program);
172
+
173
+ expect(names).not.toContain('wg');
174
+ expect(names).not.toContain('thread');
175
+ expect(names).not.toContain('primitive');
176
+ expect(names).not.toContain('ledger');
177
+ });
166
178
  });
@@ -8,7 +8,11 @@ describe('CLI help contract', () => {
8
8
  expect(help).toContain('context');
9
9
  expect(help).toContain('inject');
10
10
  expect(help).toContain('doctor');
11
+ expect(help).toContain('benchmark');
12
+ expect(help).toContain('maintain');
11
13
  expect(help).toContain('embed');
14
+ expect(help).toContain('inbox');
15
+ expect(help).toContain('patch');
12
16
  expect(help).toContain('compat');
13
17
  expect(help).toContain('graph');
14
18
  expect(help).toContain('reflect');
@@ -36,4 +40,14 @@ describe('CLI help contract', () => {
36
40
  expect(projectListHelp).toContain('archived projects are hidden');
37
41
  expect(projectBoardHelp).toContain('default: status');
38
42
  });
43
+
44
+ it('does not advertise removed workgraph commands', () => {
45
+ const program = registerAllCommandModules();
46
+ const names = program.commands.map((command) => command.name());
47
+
48
+ expect(names).not.toContain('wg');
49
+ expect(names).not.toContain('thread');
50
+ expect(names).not.toContain('primitive');
51
+ expect(names).not.toContain('ledger');
52
+ });
39
53
  });
@@ -212,6 +212,66 @@ export function registerCoreCommands(
212
212
  }
213
213
  });
214
214
 
215
+ // === CAPTURE ===
216
+ program
217
+ .command('patch <idOrPath>')
218
+ .description('Patch an existing memory document')
219
+ .option('--append <text>', 'Append text to the document body (or target section)')
220
+ .option('--replace <text>', 'Text to replace')
221
+ .option('--with <text>', 'Replacement text used with --replace')
222
+ .option('--section <heading>', 'Limit patching to a markdown section heading')
223
+ .option('--content <text>', 'Replace document body (or section body) with text')
224
+ .option('-v, --vault <path>', 'Vault path (default: find nearest)')
225
+ .action(async (idOrPath, options) => {
226
+ try {
227
+ const modeFlags = [
228
+ typeof options.append === 'string',
229
+ typeof options.replace === 'string',
230
+ typeof options.content === 'string'
231
+ ];
232
+ const selectedModes = modeFlags.filter(Boolean).length;
233
+ if (selectedModes !== 1) {
234
+ throw new Error('Select exactly one patch mode: --append, --replace/--with, or --content.');
235
+ }
236
+
237
+ if (typeof options.with === 'string' && typeof options.replace !== 'string') {
238
+ throw new Error('--with can only be used together with --replace.');
239
+ }
240
+
241
+ const vault = await getVault(options.vault);
242
+ const patchOptions = {
243
+ idOrPath,
244
+ mode: 'content'
245
+ };
246
+
247
+ if (typeof options.append === 'string') {
248
+ patchOptions.mode = 'append';
249
+ patchOptions.append = options.append;
250
+ } else if (typeof options.replace === 'string') {
251
+ if (typeof options.with !== 'string') {
252
+ throw new Error('--replace requires --with.');
253
+ }
254
+ patchOptions.mode = 'replace';
255
+ patchOptions.replace = options.replace;
256
+ patchOptions.with = options.with;
257
+ } else if (typeof options.content === 'string') {
258
+ patchOptions.mode = 'content';
259
+ patchOptions.content = options.content;
260
+ }
261
+
262
+ if (typeof options.section === 'string') {
263
+ patchOptions.section = options.section;
264
+ }
265
+
266
+ const doc = await vault.patch(patchOptions);
267
+ console.log(chalk.green(`✓ Patched: ${doc.id}`));
268
+ console.log(chalk.dim(` Path: ${doc.path}`));
269
+ } catch (err) {
270
+ console.error(chalk.red(`Error: ${err.message}`));
271
+ process.exit(1);
272
+ }
273
+ });
274
+
215
275
  // === CAPTURE ===
216
276
  program
217
277
  .command('capture <note>')
@@ -234,4 +294,32 @@ export function registerCoreCommands(
234
294
  process.exit(1);
235
295
  }
236
296
  });
297
+
298
+ // === INBOX ===
299
+ const inbox = program
300
+ .command('inbox')
301
+ .description('Manage raw captures in the inbox');
302
+
303
+ inbox
304
+ .command('add [content]')
305
+ .description('Add content to inbox (or pipe stdin)')
306
+ .option('-t, --title <title>', 'Capture title')
307
+ .option('--source <source>', 'Capture source label')
308
+ .option('--stdin', 'Read content from stdin')
309
+ .option('-v, --vault <path>', 'Vault path')
310
+ .action(async (content, options) => {
311
+ try {
312
+ const { inboxAddCommand } = await import('../dist/commands/inbox.js');
313
+ await inboxAddCommand({
314
+ vaultPath: options.vault,
315
+ content,
316
+ title: options.title,
317
+ source: options.source,
318
+ stdin: options.stdin || !process.stdin.isTTY
319
+ });
320
+ } catch (err) {
321
+ console.error(chalk.red(`Error: ${err.message}`));
322
+ process.exit(1);
323
+ }
324
+ });
237
325
  }
@@ -0,0 +1,80 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { Command } from 'commander';
3
+ import * as fs from 'fs';
4
+ import * as path from 'path';
5
+ import { registerCoreCommands } from './register-core-commands.js';
6
+ import { chalkStub } from './test-helpers/cli-command-fixtures.js';
7
+
8
+ function buildProgram(patchImpl) {
9
+ const program = new Command();
10
+ registerCoreCommands(program, {
11
+ chalk: chalkStub,
12
+ path,
13
+ fs,
14
+ createVault: async () => ({ getCategories: () => [], getQmdRoot: () => '', getQmdCollection: () => '' }),
15
+ getVault: async () => ({ patch: patchImpl }),
16
+ runQmd: async () => {}
17
+ });
18
+ return program;
19
+ }
20
+
21
+ describe('register-core-commands patch command', () => {
22
+ it('exposes patch command mode flags', () => {
23
+ const program = buildProgram(async () => ({ id: 'decisions/example', path: '/vault/decisions/example.md' }));
24
+ const patchCommand = program.commands.find((command) => command.name() === 'patch');
25
+ expect(patchCommand).toBeDefined();
26
+ const optionFlags = patchCommand?.options.map((option) => option.flags) ?? [];
27
+ expect(optionFlags).toEqual(expect.arrayContaining([
28
+ '--append <text>',
29
+ '--replace <text>',
30
+ '--with <text>',
31
+ '--section <heading>',
32
+ '--content <text>',
33
+ '-v, --vault <path>'
34
+ ]));
35
+ });
36
+
37
+ it('forwards append mode payload to vault.patch', async () => {
38
+ const patchMock = vi.fn(async () => ({ id: 'decisions/example', path: '/vault/decisions/example.md' }));
39
+ const program = buildProgram(patchMock);
40
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
41
+ const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
42
+
43
+ try {
44
+ await program.parseAsync(['patch', 'decisions/example', '--append', 'new line'], { from: 'user' });
45
+ expect(patchMock).toHaveBeenCalledWith({
46
+ idOrPath: 'decisions/example',
47
+ mode: 'append',
48
+ append: 'new line'
49
+ });
50
+ expect(errorSpy).not.toHaveBeenCalled();
51
+ } finally {
52
+ logSpy.mockRestore();
53
+ errorSpy.mockRestore();
54
+ }
55
+ });
56
+
57
+ it('forwards section/content mode payload to vault.patch', async () => {
58
+ const patchMock = vi.fn(async () => ({ id: 'decisions/example', path: '/vault/decisions/example.md' }));
59
+ const program = buildProgram(patchMock);
60
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
61
+ const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
62
+
63
+ try {
64
+ await program.parseAsync(
65
+ ['patch', 'decisions/example', '--section', 'Notes', '--content', 'updated notes'],
66
+ { from: 'user' }
67
+ );
68
+ expect(patchMock).toHaveBeenCalledWith({
69
+ idOrPath: 'decisions/example',
70
+ mode: 'content',
71
+ content: 'updated notes',
72
+ section: 'Notes'
73
+ });
74
+ expect(errorSpy).not.toHaveBeenCalled();
75
+ } finally {
76
+ logSpy.mockRestore();
77
+ errorSpy.mockRestore();
78
+ }
79
+ });
80
+ });
@@ -4,10 +4,33 @@
4
4
  */
5
5
 
6
6
  export function registerMaintenanceCommands(program, { chalk }) {
7
+ // === MAINTAIN ===
8
+ program
9
+ .command('maintain')
10
+ .description('Run background inbox maintenance workers')
11
+ .option('--worker <name>', 'Run a single worker (curator|janitor|distiller|surveyor)')
12
+ .option('--limit <n>', 'Limit inbox items processed per worker', (value) => Number.parseInt(value, 10))
13
+ .option('--dry-run', 'Preview actions without writing files')
14
+ .option('-v, --vault <path>', 'Vault path')
15
+ .action(async (options) => {
16
+ try {
17
+ const { maintainCommand } = await import('../dist/commands/maintain.js');
18
+ await maintainCommand({
19
+ vaultPath: options.vault,
20
+ worker: options.worker,
21
+ limit: options.limit,
22
+ dryRun: options.dryRun
23
+ });
24
+ } catch (err) {
25
+ console.error(chalk.red(`Error: ${err.message}`));
26
+ process.exit(1);
27
+ }
28
+ });
29
+
7
30
  // === DOCTOR (health check) ===
8
31
  program
9
32
  .command('doctor')
10
- .description('Diagnose vault health and optionally apply fixes')
33
+ .description('Run installation and environment diagnostics')
11
34
  .option('-v, --vault <path>', 'Vault path')
12
35
  .option('--fix', 'Apply safe auto-fixes for qmd index, embeddings, and dead collections')
13
36
  .option('--json', 'Output machine-readable JSON')
@@ -24,10 +47,11 @@ export function registerMaintenanceCommands(program, { chalk }) {
24
47
  return;
25
48
  }
26
49
 
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}`));
50
+ console.log(chalk.cyan('\n🩺 ClawVault Doctor Report\n'));
51
+ if (report.vaultPath) {
52
+ console.log(chalk.dim(`Vault: ${report.vaultPath}`));
53
+ }
54
+ console.log(chalk.dim(`Generated: ${report.generatedAt}`));
31
55
  console.log();
32
56
 
33
57
  for (const check of report.checks) {
@@ -36,7 +60,7 @@ export function registerMaintenanceCommands(program, { chalk }) {
36
60
  : check.status === 'warn'
37
61
  ? chalk.yellow('⚠')
38
62
  : chalk.red('✗');
39
- const line = `${check.label}: ${check.detail}`;
63
+ const line = check.detail ? `${check.label}: ${check.detail}` : check.label;
40
64
  const renderedLine = check.status === 'ok'
41
65
  ? chalk.green(line)
42
66
  : check.status === 'warn'
@@ -298,4 +322,31 @@ export function registerMaintenanceCommands(program, { chalk }) {
298
322
  process.exit(1);
299
323
  }
300
324
  });
325
+
326
+ // === BENCHMARK ===
327
+ const benchmark = program
328
+ .command('benchmark')
329
+ .description('Run benchmark harnesses');
330
+
331
+ benchmark
332
+ .command('observer')
333
+ .description('Evaluate observer output quality against annotated transcripts')
334
+ .option('--fixtures-dir <path>', 'Fixture root directory (default: testdata/observer-benchmark)')
335
+ .option('--provider <provider>', 'Compression provider (mock|anthropic|openai|gemini|xai|openai-compatible|ollama|minimax|zai)', 'mock')
336
+ .option('--model <model>', 'Model override for live provider runs')
337
+ .option('--report-format <format>', 'Report output format (json|text)', 'text')
338
+ .action(async (options) => {
339
+ try {
340
+ const { benchmarkObserverCommand } = await import('../dist/commands/benchmark.js');
341
+ await benchmarkObserverCommand({
342
+ fixturesDir: options.fixturesDir,
343
+ provider: options.provider,
344
+ model: options.model,
345
+ reportFormat: options.reportFormat
346
+ });
347
+ } catch (err) {
348
+ console.error(chalk.red(`Error: ${err.message}`));
349
+ process.exit(1);
350
+ }
351
+ });
301
352
  }
@@ -17,7 +17,7 @@ export function registerQueryCommands(
17
17
  // === SEARCH ===
18
18
  program
19
19
  .command('search <query>')
20
- .description('Search the vault via qmd (BM25), optionally with semantic hybrid search')
20
+ .description('Search the vault using in-process hybrid retrieval (BM25 + semantic when configured)')
21
21
  .option('-n, --limit <n>', 'Max results (default: 10)', '10')
22
22
  .option('-c, --category <category>', 'Filter by category')
23
23
  .option('--tags <tags>', 'Filter by tags (comma-separated)')
@@ -25,8 +25,8 @@ export function registerQueryCommands(
25
25
  .option('--full', 'Include full content in results')
26
26
  .option('-v, --vault <path>', 'Vault path')
27
27
  .option('--json', 'Output as JSON')
28
- .option('--semantic', 'Enable hybrid search (BM25 + semantic with RRF)')
29
- .option('--rebuild-embeddings', 'Rebuild the embedding cache before searching')
28
+ .option('--semantic', 'Legacy alias. Hybrid retrieval is already default when embeddings are configured.')
29
+ .option('--rebuild-embeddings', 'Rebuild hosted embedding cache before searching')
30
30
  .action(async (query, options) => {
31
31
  try {
32
32
  const vaultPath = resolveVaultPath(options.vault);
@@ -45,34 +45,16 @@ export function registerQueryCommands(
45
45
  console.log();
46
46
  }
47
47
 
48
- // Get BM25 results
49
- const bm25Results = await vault.find(query, {
50
- limit: options.semantic ? 50 : parseInt(options.limit, 10),
48
+ const results = await vault.find(query, {
49
+ limit: parseInt(options.limit, 10),
51
50
  category: options.category,
52
51
  tags: options.tags?.split(',').map((value) => value.trim()),
53
52
  fullContent: options.full,
54
53
  temporalBoost: options.recent
55
54
  });
56
-
57
- let results = bm25Results;
58
- let searchMode = 'BM25';
59
-
60
- // Apply hybrid search if --semantic flag is set
61
- if (options.semantic) {
62
- const { EmbeddingCache, hybridSearch } = await import('../dist/lib/hybrid-search.js');
63
- const cache = new EmbeddingCache(vaultPath);
64
- cache.load();
65
-
66
- if (cache.size === 0) {
67
- console.log(chalk.yellow('Warning: No embeddings found. Run with --rebuild-embeddings to build the cache.'));
68
- } else {
69
- results = await hybridSearch(query, bm25Results, cache, {
70
- topK: parseInt(options.limit, 10),
71
- rrfK: 60
72
- });
73
- searchMode = 'Hybrid (BM25 + Semantic)';
74
- }
75
- }
55
+ const searchMode = options.semantic
56
+ ? 'Hybrid (legacy flag acknowledged)'
57
+ : 'Hybrid (in-process)';
76
58
 
77
59
  if (options.json) {
78
60
  console.log(JSON.stringify({ searchMode, results }, null, 2));
@@ -84,7 +66,7 @@ export function registerQueryCommands(
84
66
  return;
85
67
  }
86
68
 
87
- const icon = options.semantic ? '🔍🧠' : '🔍';
69
+ const icon = '🔍🧠';
88
70
  console.log(chalk.cyan(`\n${icon} Found ${results.length} result(s) for "${query}" [${searchMode}]:\n`));
89
71
 
90
72
  for (const result of results) {
@@ -114,7 +96,7 @@ export function registerQueryCommands(
114
96
  // === VSEARCH ===
115
97
  program
116
98
  .command('vsearch <query>')
117
- .description('Semantic search via qmd (requires qmd installed)')
99
+ .description('Semantic search via hosted embeddings (qmd fallback when available)')
118
100
  .option('-n, --limit <n>', 'Max results (default: 5)', '5')
119
101
  .option('-c, --category <category>', 'Filter by category')
120
102
  .option('--tags <tags>', 'Filter by tags (comma-separated)')
@@ -30,6 +30,7 @@ export function stubResolveVaultPath(value) {
30
30
  export function createVaultStub(overrides = {}) {
31
31
  return {
32
32
  store: async () => ({}),
33
+ patch: async () => ({}),
33
34
  capture: async () => ({}),
34
35
  find: async () => [],
35
36
  vsearch: async () => [],