clawvault 3.2.1 → 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.
Files changed (161) hide show
  1. package/README.md +56 -16
  2. package/bin/clawvault.js +0 -2
  3. package/bin/command-registration.test.js +15 -2
  4. package/bin/help-contract.test.js +16 -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 +84 -7
  8. package/bin/register-query-commands.js +45 -28
  9. package/bin/register-query-commands.test.js +15 -0
  10. package/bin/test-helpers/cli-command-fixtures.js +1 -0
  11. package/dist/chunk-2PKBIKDH.js +130 -0
  12. package/dist/{chunk-U67V476Y.js → chunk-2ZDO52B4.js} +18 -1
  13. package/dist/{chunk-ZZA73MFY.js → chunk-33DOSHTA.js} +176 -36
  14. package/dist/chunk-35JCYSRR.js +158 -0
  15. package/dist/{chunk-AZYOKJYC.js → chunk-4PY655YM.js} +13 -1
  16. package/dist/{chunk-2JQ3O2YL.js → chunk-5EFSWZO6.js} +3 -3
  17. package/dist/{chunk-Y3TIJEBP.js → chunk-7SWP5FKU.js} +34 -613
  18. package/dist/{chunk-4VQTUVH7.js → chunk-7YZWHM36.js} +52 -26
  19. package/dist/{chunk-URXDAUVH.js → chunk-AXSJIFOJ.js} +174 -1
  20. package/dist/{chunk-4ITRXIVT.js → chunk-BLQXXX7Q.js} +6 -6
  21. package/dist/chunk-CSHO3PJB.js +684 -0
  22. package/dist/chunk-D5U3Q4N5.js +872 -0
  23. package/dist/chunk-DCF4KMFD.js +158 -0
  24. package/dist/{chunk-S5OJEGFG.js → chunk-DOIUYIXV.js} +2 -2
  25. package/dist/{chunk-YXQCA6B7.js → chunk-DVOUSOR3.js} +112 -7
  26. package/dist/{chunk-YDWHS4LJ.js → chunk-ECGJYWNA.js} +205 -33
  27. package/dist/{chunk-QMHPQYUV.js → chunk-EL6UBSX5.js} +7 -6
  28. package/dist/chunk-FZ5I2NF7.js +352 -0
  29. package/dist/{chunk-WJVWINEM.js → chunk-GFCHWMGD.js} +55 -6
  30. package/dist/{chunk-GNJL4YGR.js → chunk-GJO3CFUN.js} +30 -6
  31. package/dist/chunk-H3JZIB5O.js +322 -0
  32. package/dist/chunk-HEHO7SMV.js +51 -0
  33. package/dist/{chunk-UCQAOZHW.js → chunk-HGDDW24U.js} +3 -3
  34. package/dist/chunk-J3YUXVID.js +907 -0
  35. package/dist/{chunk-Y6VJKXGL.js → chunk-KCYWJDDW.js} +1 -1
  36. package/dist/{chunk-P5EPF6MB.js → chunk-MW5C6ZQA.js} +110 -13
  37. package/dist/chunk-NSXYM6EZ.js +255 -0
  38. package/dist/{chunk-YNIPYN4F.js → chunk-OFOCU2V4.js} +6 -5
  39. package/dist/{chunk-42MXU7A6.js → chunk-P62WHA27.js} +58 -47
  40. package/dist/chunk-PTWPPVC7.js +972 -0
  41. package/dist/{chunk-FAKNOB7Y.js → chunk-QFWERBDP.js} +2 -2
  42. package/dist/chunk-QYQAGBTM.js +2097 -0
  43. package/dist/chunk-RL2L6I6K.js +223 -0
  44. package/dist/{chunk-IIOU45CK.js → chunk-S7N7HI5E.js} +2 -2
  45. package/dist/{chunk-ECRZL5XR.js → chunk-T7E764W3.js} +23 -7
  46. package/dist/{chunk-MNPUYCHQ.js → chunk-TWMI3SNN.js} +6 -5
  47. package/dist/{chunk-2RAZ4ZFE.js → chunk-VBILES4B.js} +1 -1
  48. package/dist/{chunk-PI4WMLMG.js → chunk-VXAGOLDP.js} +1 -1
  49. package/dist/{chunk-SS4B7P7V.js → chunk-YIDV4VV2.js} +1 -1
  50. package/dist/chunk-YTRZNA64.js +37 -0
  51. package/dist/chunk-ZKWPCBYT.js +600 -0
  52. package/dist/cli/index.js +28 -21
  53. package/dist/commands/archive.js +3 -3
  54. package/dist/commands/backlog.js +1 -1
  55. package/dist/commands/benchmark.d.ts +12 -0
  56. package/dist/commands/benchmark.js +12 -0
  57. package/dist/commands/blocked.js +1 -1
  58. package/dist/commands/canvas.js +2 -2
  59. package/dist/commands/checkpoint.js +1 -1
  60. package/dist/commands/compat.js +1 -1
  61. package/dist/commands/context.js +8 -7
  62. package/dist/commands/doctor.d.ts +8 -3
  63. package/dist/commands/doctor.js +8 -22
  64. package/dist/commands/embed.js +6 -5
  65. package/dist/commands/entities.d.ts +8 -1
  66. package/dist/commands/entities.js +46 -3
  67. package/dist/commands/graph.js +4 -4
  68. package/dist/commands/inbox.d.ts +23 -0
  69. package/dist/commands/inbox.js +11 -0
  70. package/dist/commands/inject.d.ts +1 -1
  71. package/dist/commands/inject.js +5 -5
  72. package/dist/commands/kanban.js +1 -1
  73. package/dist/commands/link.js +5 -5
  74. package/dist/commands/maintain.d.ts +32 -0
  75. package/dist/commands/maintain.js +13 -0
  76. package/dist/commands/migrate-observations.js +3 -3
  77. package/dist/commands/observe.js +11 -10
  78. package/dist/commands/project.js +2 -2
  79. package/dist/commands/rebuild-embeddings.js +48 -17
  80. package/dist/commands/rebuild.js +9 -8
  81. package/dist/commands/recall.d.ts +14 -0
  82. package/dist/commands/recall.js +15 -0
  83. package/dist/commands/recover.js +1 -1
  84. package/dist/commands/reflect.js +6 -6
  85. package/dist/commands/repair-session.js +1 -1
  86. package/dist/commands/replay.js +10 -9
  87. package/dist/commands/session-recap.js +1 -1
  88. package/dist/commands/setup.js +4 -3
  89. package/dist/commands/shell-init.js +1 -1
  90. package/dist/commands/sleep.d.ts +1 -1
  91. package/dist/commands/sleep.js +20 -18
  92. package/dist/commands/status.js +40 -26
  93. package/dist/commands/sync-bd.js +3 -3
  94. package/dist/commands/tailscale.js +3 -3
  95. package/dist/commands/task.js +1 -1
  96. package/dist/commands/template.js +1 -1
  97. package/dist/commands/wake.d.ts +1 -1
  98. package/dist/commands/wake.js +10 -9
  99. package/dist/index.d.ts +233 -16
  100. package/dist/index.js +325 -111
  101. package/dist/{inject-DYUrDqQO.d.ts → inject-DEb_jpLi.d.ts} +3 -1
  102. package/dist/lib/auto-linker.js +2 -2
  103. package/dist/lib/canvas-layout.js +1 -1
  104. package/dist/lib/config.js +2 -2
  105. package/dist/lib/entity-index.js +1 -1
  106. package/dist/lib/project-utils.js +2 -2
  107. package/dist/lib/session-repair.js +1 -1
  108. package/dist/lib/session-utils.js +1 -1
  109. package/dist/lib/tailscale.js +1 -1
  110. package/dist/lib/task-utils.js +1 -1
  111. package/dist/lib/template-engine.js +1 -1
  112. package/dist/lib/webdav.js +1 -1
  113. package/dist/onnxruntime_binding-5QEF3SUC.node +0 -0
  114. package/dist/onnxruntime_binding-BKPKNEGC.node +0 -0
  115. package/dist/onnxruntime_binding-FMOXGIUT.node +0 -0
  116. package/dist/onnxruntime_binding-OI2KMXC5.node +0 -0
  117. package/dist/onnxruntime_binding-UX44MLAZ.node +0 -0
  118. package/dist/onnxruntime_binding-Y2W7N7WY.node +0 -0
  119. package/dist/openclaw-plugin--gqA2BZw.d.ts +267 -0
  120. package/dist/openclaw-plugin.d.ts +4 -0
  121. package/dist/openclaw-plugin.js +20 -0
  122. package/dist/transformers.node-A2ZRORSQ.js +46775 -0
  123. package/dist/types-CbL-wIKi.d.ts +36 -0
  124. package/dist/{types-BbWJoC1c.d.ts → types-DslKvCaj.d.ts} +51 -1
  125. package/hooks/clawvault/HOOK.md +25 -8
  126. package/hooks/clawvault/handler.js +215 -78
  127. package/hooks/clawvault/handler.test.js +109 -43
  128. package/hooks/clawvault/integrity.js +112 -0
  129. package/hooks/clawvault/integrity.test.js +32 -0
  130. package/hooks/clawvault/openclaw.plugin.json +133 -15
  131. package/openclaw.plugin.json +161 -194
  132. package/package.json +8 -5
  133. package/bin/register-workgraph-commands.js +0 -451
  134. package/dist/chunk-5PJ4STIC.js +0 -465
  135. package/dist/chunk-ERNE2FZ5.js +0 -189
  136. package/dist/chunk-HR4KN6S2.js +0 -152
  137. package/dist/chunk-IJBFGPCS.js +0 -33
  138. package/dist/chunk-K7PNYS45.js +0 -93
  139. package/dist/chunk-NTOPJI7W.js +0 -207
  140. package/dist/chunk-PG56HX5T.js +0 -154
  141. package/dist/chunk-QPDDIHXE.js +0 -501
  142. package/dist/chunk-WIOLLGAD.js +0 -190
  143. package/dist/chunk-WMGIIABP.js +0 -15
  144. package/dist/ledger-B7g7jhqG.d.ts +0 -44
  145. package/dist/plugin/index.d.ts +0 -352
  146. package/dist/plugin/index.js +0 -4264
  147. package/dist/registry-BR4326o0.d.ts +0 -30
  148. package/dist/store-CA-6sKCJ.d.ts +0 -34
  149. package/dist/thread-B9LhXNU0.d.ts +0 -41
  150. package/dist/workgraph/index.d.ts +0 -5
  151. package/dist/workgraph/index.js +0 -23
  152. package/dist/workgraph/ledger.d.ts +0 -2
  153. package/dist/workgraph/ledger.js +0 -25
  154. package/dist/workgraph/registry.d.ts +0 -2
  155. package/dist/workgraph/registry.js +0 -19
  156. package/dist/workgraph/store.d.ts +0 -2
  157. package/dist/workgraph/store.js +0 -25
  158. package/dist/workgraph/thread.d.ts +0 -2
  159. package/dist/workgraph/thread.js +0 -25
  160. package/dist/workgraph/types.d.ts +0 -54
  161. package/dist/workgraph/types.js +0 -7
package/README.md CHANGED
@@ -12,7 +12,7 @@
12
12
 
13
13
  *An elephant never forgets. Neither should your AI.*
14
14
 
15
- [Documentation](https://clawvault.dev) · [npm Package](https://www.npmjs.com/package/clawvault) · [Obsidian Plugin](https://clawvault.dev/obsidian) · [Community](https://github.com/Versatly/clawvault/discussions) · [GitHub](https://github.com/Versatly/clawvault)
15
+ [Documentation](https://clawvault.dev) · [npm Package](https://www.npmjs.com/package/clawvault) · [Obsidian Plugin](https://clawvault.dev/obsidian) · [GitHub](https://github.com/Versatly/clawvault)
16
16
 
17
17
  </div>
18
18
 
@@ -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:
@@ -484,7 +524,7 @@ See our [contribution guidelines](https://github.com/Versatly/clawvault/blob/mai
484
524
 
485
525
  ---
486
526
 
487
- **$CLAWVAULT**: [`5Fjr82MTB8mvxkzi9FYtvrUsPiDGE2M29w3dYcZpump`](https://pump.fun/coin/5Fjr82MTB8mvxkzi9FYtvrUsPiDGE2M29w3dYcZpump)
527
+ **$CLAW**: [`5Fjr82MTB8mvxkzi9FYtvrUsPiDGE2M29w3dYcZpump`](https://pump.fun/coin/5Fjr82MTB8mvxkzi9FYtvrUsPiDGE2M29w3dYcZpump)
488
528
 
489
529
  ## License
490
530
 
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', () => {
@@ -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>'));
@@ -121,10 +121,13 @@ 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',
127
129
  'entities',
130
+ 'entity',
128
131
  'link',
129
132
  'rebuild',
130
133
  'archive',
@@ -163,4 +166,14 @@ describe('CLI command registration modules', () => {
163
166
  const unique = new Set(names);
164
167
  expect(unique.size).toBe(names.length);
165
168
  });
169
+
170
+ it('does not register removed workgraph commands', () => {
171
+ const program = registerAllCommandModules(new Command());
172
+ const names = listCommandNames(program);
173
+
174
+ expect(names).not.toContain('wg');
175
+ expect(names).not.toContain('thread');
176
+ expect(names).not.toContain('primitive');
177
+ expect(names).not.toContain('ledger');
178
+ });
166
179
  });
@@ -6,9 +6,14 @@ 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');
12
+ expect(help).toContain('benchmark');
13
+ expect(help).toContain('maintain');
11
14
  expect(help).toContain('embed');
15
+ expect(help).toContain('inbox');
16
+ expect(help).toContain('patch');
12
17
  expect(help).toContain('compat');
13
18
  expect(help).toContain('graph');
14
19
  expect(help).toContain('reflect');
@@ -18,6 +23,7 @@ describe('CLI help contract', () => {
18
23
  expect(help).toContain('template');
19
24
  expect(help).toContain('config');
20
25
  expect(help).toContain('route');
26
+ expect(help).toContain('entity');
21
27
  });
22
28
 
23
29
  it('documents context/compat/inject/project help details', () => {
@@ -36,4 +42,14 @@ describe('CLI help contract', () => {
36
42
  expect(projectListHelp).toContain('archived projects are hidden');
37
43
  expect(projectBoardHelp).toContain('default: status');
38
44
  });
45
+
46
+ it('does not advertise removed workgraph commands', () => {
47
+ const program = registerAllCommandModules();
48
+ const names = program.commands.map((command) => command.name());
49
+
50
+ expect(names).not.toContain('wg');
51
+ expect(names).not.toContain('thread');
52
+ expect(names).not.toContain('primitive');
53
+ expect(names).not.toContain('ledger');
54
+ });
39
55
  });
@@ -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'
@@ -150,11 +174,37 @@ export function registerMaintenanceCommands(program, { chalk }) {
150
174
  .command('entities')
151
175
  .description('List all linkable entities in the vault')
152
176
  .option('-v, --vault <path>', 'Vault path')
177
+ .option('--refresh', 'Regenerate entity profiles before listing')
153
178
  .option('--json', 'Output as JSON')
154
179
  .action(async (options) => {
155
180
  try {
156
181
  const { entitiesCommand } = await import('../dist/commands/entities.js');
157
- 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
+ });
158
208
  } catch (err) {
159
209
  console.error(chalk.red(`Error: ${err.message}`));
160
210
  process.exit(1);
@@ -298,4 +348,31 @@ export function registerMaintenanceCommands(program, { chalk }) {
298
348
  process.exit(1);
299
349
  }
300
350
  });
351
+
352
+ // === BENCHMARK ===
353
+ const benchmark = program
354
+ .command('benchmark')
355
+ .description('Run benchmark harnesses');
356
+
357
+ benchmark
358
+ .command('observer')
359
+ .description('Evaluate observer output quality against annotated transcripts')
360
+ .option('--fixtures-dir <path>', 'Fixture root directory (default: testdata/observer-benchmark)')
361
+ .option('--provider <provider>', 'Compression provider (mock|anthropic|openai|gemini|xai|openai-compatible|ollama|minimax|zai)', 'mock')
362
+ .option('--model <model>', 'Model override for live provider runs')
363
+ .option('--report-format <format>', 'Report output format (json|text)', 'text')
364
+ .action(async (options) => {
365
+ try {
366
+ const { benchmarkObserverCommand } = await import('../dist/commands/benchmark.js');
367
+ await benchmarkObserverCommand({
368
+ fixturesDir: options.fixturesDir,
369
+ provider: options.provider,
370
+ model: options.model,
371
+ reportFormat: options.reportFormat
372
+ });
373
+ } catch (err) {
374
+ console.error(chalk.red(`Error: ${err.message}`));
375
+ process.exit(1);
376
+ }
377
+ });
301
378
  }
@@ -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)')
@@ -220,6 +202,41 @@ export function registerQueryCommands(
220
202
  }
221
203
  });
222
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
+
223
240
  // === INJECT ===
224
241
  program
225
242
  .command('inject <message>')