clawvault 3.4.1 → 3.5.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 (36) hide show
  1. package/CHANGELOG.md +543 -0
  2. package/LICENSE +21 -0
  3. package/SKILL.md +369 -0
  4. package/dist/{chunk-X3SPPUFG.js → chunk-JI7VUQV7.js} +118 -132
  5. package/dist/{chunk-PLNK37JD.js → chunk-QUFQBAHP.js} +114 -217
  6. package/dist/cli/index.js +1 -1
  7. package/dist/commands/compat.js +1 -1
  8. package/dist/commands/observe.js +1 -1
  9. package/dist/commands/status.js +4 -4
  10. package/dist/index.js +11 -8
  11. package/dist/openclaw-plugin.js +6 -1
  12. package/docs/clawhub-security-release-playbook.md +75 -0
  13. package/docs/getting-started/installation.md +99 -0
  14. package/docs/openclaw-plugin-usage.md +152 -0
  15. package/openclaw.plugin.json +1 -1
  16. package/package.json +26 -8
  17. package/bin/command-registration.test.js +0 -179
  18. package/bin/command-runtime.test.js +0 -154
  19. package/bin/help-contract.test.js +0 -55
  20. package/bin/register-config-route-commands.test.js +0 -121
  21. package/bin/register-core-commands.test.js +0 -80
  22. package/bin/register-kanban-commands.test.js +0 -83
  23. package/bin/register-project-commands.test.js +0 -206
  24. package/bin/register-query-commands.test.js +0 -80
  25. package/bin/register-resilience-commands.test.js +0 -81
  26. package/bin/register-task-commands.test.js +0 -69
  27. package/bin/register-template-commands.test.js +0 -87
  28. package/bin/test-helpers/cli-command-fixtures.js +0 -120
  29. package/dashboard/lib/graph-diff.test.js +0 -75
  30. package/dashboard/lib/vault-parser.test.js +0 -254
  31. package/hooks/clawvault/HOOK.md +0 -130
  32. package/hooks/clawvault/handler.js +0 -1696
  33. package/hooks/clawvault/handler.test.js +0 -576
  34. package/hooks/clawvault/integrity.js +0 -112
  35. package/hooks/clawvault/integrity.test.js +0 -32
  36. package/hooks/clawvault/openclaw.plugin.json +0 -190
@@ -0,0 +1,99 @@
1
+ # Installation
2
+
3
+ This guide covers the recommended install flow for ClawVault on Linux (Ubuntu), macOS, Windows, and WSL.
4
+
5
+ ## System requirements
6
+
7
+ - Node.js 18+ (Node.js 22+ recommended)
8
+ - npm 9+
9
+ - Supported OS: Linux, macOS, Windows, WSL
10
+
11
+ ## Install the CLI
12
+
13
+ ```bash
14
+ npm install -g clawvault
15
+ ```
16
+
17
+ `qmd` is optional. ClawVault ships with an in-process BM25 search engine by default. Install `qmd` only if you want qmd fallback behavior:
18
+
19
+ ```bash
20
+ bun install -g github:tobi/qmd
21
+ ```
22
+
23
+ ## OpenClaw plugin setup (new architecture)
24
+
25
+ ClawVault is loaded as a plugin package (not the deprecated hook install/enable flow).
26
+
27
+ 1. Add ClawVault's package path to `plugins.load.paths` in `openclaw.json`.
28
+ 2. Enable the plugin entry and memory slot:
29
+ - `plugins.slots.memory: "clawvault"`
30
+ - `plugins.entries.clawvault.enabled: true`
31
+ 3. Configure plugin behavior under `plugins.entries.clawvault.config` (for example `vaultPath`, context/observation toggles, and protocol settings).
32
+
33
+ For complete configuration and hook behavior details, see [OpenClaw Plugin Usage](../openclaw-plugin-usage.md).
34
+
35
+ ## Quick verification
36
+
37
+ After installation, run:
38
+
39
+ ```bash
40
+ clawvault doctor
41
+ ```
42
+
43
+ This checks your Node/npm environment, vault/config health, search setup, OpenClaw integration, and common Linux permission issues.
44
+
45
+ ## Linux (Ubuntu 22.04 / 24.04) setup
46
+
47
+ ### 1) Install Node.js and npm
48
+
49
+ Use your preferred version manager (nvm/fnm/asdf) and install Node.js 22 LTS. Validate:
50
+
51
+ ```bash
52
+ node -v
53
+ npm -v
54
+ ```
55
+
56
+ ### 2) Configure npm global prefix (if you hit EACCES)
57
+
58
+ If `npm install -g clawvault` fails with permissions errors:
59
+
60
+ ```bash
61
+ npm config set prefix ~/.npm-global
62
+ ```
63
+
64
+ ### 3) Add npm global bin directory to PATH
65
+
66
+ Append this to `~/.bashrc` (or `~/.zshrc`):
67
+
68
+ ```bash
69
+ export PATH="$HOME/.npm-global/bin:$PATH"
70
+ ```
71
+
72
+ Reload your shell:
73
+
74
+ ```bash
75
+ source ~/.bashrc
76
+ ```
77
+
78
+ ### 4) Re-run install and verify
79
+
80
+ ```bash
81
+ npm install -g clawvault
82
+ clawvault doctor
83
+ ```
84
+
85
+ ## Troubleshooting quick fixes
86
+
87
+ - `clawvault: command not found`
88
+ - Ensure your npm global bin directory is in PATH.
89
+ - Run `npm config get prefix` and confirm `<prefix>/bin` is exported.
90
+ - Global install fails with `EACCES`
91
+ - Set user-owned npm prefix: `npm config set prefix ~/.npm-global`
92
+ - Re-open terminal and retry install.
93
+ - `qmd` not found
94
+ - This is optional. In-process BM25 search still works.
95
+ - Install qmd only if you need fallback compatibility paths.
96
+ - OpenClaw plugin not registered
97
+ - Verify `plugins.load.paths` includes the ClawVault package path.
98
+ - Verify `plugins.slots.memory` is `clawvault`.
99
+ - Verify `plugins.entries.clawvault.enabled` is `true`.
@@ -0,0 +1,152 @@
1
+ # OpenClaw Plugin Usage Guide
2
+
3
+ ClawVault now runs as a proper OpenClaw plugin via `plugins.load.paths` and `plugins.entries.clawvault`.
4
+ The old `openclaw hooks install/enable` flow is deprecated.
5
+
6
+ ## Installation (New Plugin System)
7
+
8
+ ```bash
9
+ # Install ClawVault globally
10
+ npm install -g clawvault
11
+
12
+ # ClawVault auto-registers as an OpenClaw plugin.
13
+ # Add to plugins.load.paths in openclaw.json:
14
+ # /path/to/node_modules/clawvault
15
+
16
+ # Enable in config:
17
+ # plugins.slots.memory: "clawvault"
18
+ # plugins.entries.clawvault.enabled: true
19
+ ```
20
+
21
+ Tip: on most setups, you can discover the install path with `npm root -g`.
22
+
23
+ ## Plugin Configuration
24
+
25
+ All plugin configuration lives under `plugins.entries.clawvault.config`:
26
+
27
+ ```json
28
+ {
29
+ "plugins": {
30
+ "slots": { "memory": "clawvault" },
31
+ "entries": {
32
+ "clawvault": {
33
+ "enabled": true,
34
+ "config": {
35
+ "vaultPath": "/path/to/memory",
36
+ "allowClawvaultExec": true,
37
+ "allowEnvAccess": true,
38
+ "enableStartupRecovery": true,
39
+ "enableSessionContextInjection": true,
40
+ "enableAutoCheckpoint": true,
41
+ "enableObserveOnNew": true,
42
+ "enableHeartbeatObservation": true,
43
+ "enableCompactionObservation": true,
44
+ "enableBeforePromptRecall": true,
45
+ "enforceCommunicationProtocol": true,
46
+ "enableMessageSendingFilter": true,
47
+ "enableFactExtraction": true,
48
+ "contextProfile": "auto",
49
+ "maxContextResults": 5
50
+ }
51
+ }
52
+ }
53
+ }
54
+ }
55
+ ```
56
+
57
+ ## What the Plugin Does (Hook by Hook)
58
+
59
+ 1. **`before_prompt_build`**
60
+ - Injects ClawVault context and recent session recap.
61
+ - Adds a memory recall mandate so the agent uses `memory_search`/`memory_get` before memory-sensitive answers.
62
+ - Appends communication protocol rules.
63
+ - Runs every turn.
64
+
65
+ 2. **`message_sending`**
66
+ - Filters outbound phrasing and removes banned patterns (for example: "good catch", "great question").
67
+ - Rewrites rabbit-hole offers (for example: "if you'd like I can ...").
68
+ - If the response is a question and memory already has the answer, rewrites toward evidence-grounded statements.
69
+
70
+ 3. **`gateway_start`**
71
+ - Runs startup recovery.
72
+ - Detects context death and stores a recovery notice for next prompt injection.
73
+
74
+ 4. **`session_start`**
75
+ - Fetches session recap entries for context injection.
76
+ - Triggers weekly reflection when due.
77
+
78
+ 5. **`session_end`**
79
+ - Clears session-specific runtime state.
80
+
81
+ 6. **`before_reset`**
82
+ - Auto-checkpoints before `/new` when enabled.
83
+ - Runs observer flow when enabled.
84
+ - Extracts facts when enabled.
85
+
86
+ 7. **`before_compaction`**
87
+ - Runs observation before context compaction when enabled.
88
+ - Optionally extracts facts.
89
+
90
+ 8. **`agent_end`**
91
+ - Runs heartbeat observation after agent turns when enabled.
92
+
93
+ ## Registered Tools
94
+
95
+ The plugin registers two tools:
96
+
97
+ - **`memory_search`**
98
+ - Search the vault using:
99
+ - `query` (required)
100
+ - `maxResults` (optional)
101
+ - `minScore` (optional)
102
+ - `sessionKey` (optional)
103
+
104
+ - **`memory_get`**
105
+ - Read vault files using:
106
+ - `path` or `relPath` (either accepted)
107
+ - `from` (optional starting line)
108
+ - `lines` (optional number of lines)
109
+
110
+ ## Communication Protocol
111
+
112
+ When enabled, ClawVault enforces protocol-safe messaging:
113
+
114
+ - Removes banned phrases:
115
+ - "good catch"
116
+ - "great question"
117
+ - "you're right to call that out"
118
+ - Removes rabbit-hole offers like:
119
+ - "if you'd like I can ..."
120
+ - "let me know if you'd like ..."
121
+ - Avoids asking questions when memory already contains the answer by rewriting to evidence-backed output.
122
+
123
+ ## MEMORY.md vs Vault: Understanding the Relationship
124
+
125
+ You may have both a workspace `MEMORY.md` and a ClawVault vault. They are complementary, not competing.
126
+
127
+ | Layer | Purpose | Access Pattern |
128
+ |-------|---------|----------------|
129
+ | **MEMORY.md** | Boot-time executive summary | Immediate, no tool calls |
130
+ | **Vault** | Full structured memory system | Retrieved via tools/CLI/injection |
131
+
132
+ Recommended pattern:
133
+
134
+ 1. Keep `MEMORY.md` short and curated (identity, key decisions, active focus).
135
+ 2. Treat the vault as source of truth for detailed records and history.
136
+ 3. Reference vault notes from `MEMORY.md` instead of duplicating full reasoning.
137
+ 4. Periodically refresh `MEMORY.md` from vault state to prevent drift.
138
+
139
+ ## Troubleshooting
140
+
141
+ - Plugin not loading:
142
+ - Verify `plugins.load.paths` includes the ClawVault package path.
143
+ - Verify `plugins.entries.clawvault.enabled` is `true`.
144
+ - Verify `plugins.slots.memory` is set to `"clawvault"`.
145
+ - Context/tool behavior not appearing:
146
+ - Confirm feature flags under `plugins.entries.clawvault.config`.
147
+ - Run `clawvault compat` to validate runtime assumptions.
148
+
149
+ ## Related Documentation
150
+
151
+ - [README: OpenClaw Integration](../README.md#openclaw-integration)
152
+ - [Installation Guide](./getting-started/installation.md)
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "clawvault",
3
3
  "name": "ClawVault",
4
- "version": "2.7.0",
4
+ "version": "3.5.0",
5
5
  "kind": "memory",
6
6
  "description": "Structured memory system for AI agents with context death resilience",
7
7
  "configSchema": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawvault",
3
- "version": "3.4.1",
3
+ "version": "3.5.0",
4
4
  "description": "Structured memory system for AI agents — typed storage, knowledge graph, context profiles, canvas dashboards, neural graph themes, and Obsidian-native task views. An elephant never forgets. 🐘",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -18,19 +18,37 @@
18
18
  },
19
19
  "files": [
20
20
  "dist",
21
- "bin",
22
- "dashboard",
21
+ "bin/clawvault.js",
22
+ "bin/command-runtime.js",
23
+ "bin/register-core-commands.js",
24
+ "bin/register-query-commands.js",
25
+ "bin/register-session-lifecycle-commands.js",
26
+ "bin/register-template-commands.js",
27
+ "bin/register-maintenance-commands.js",
28
+ "bin/register-resilience-commands.js",
29
+ "bin/register-vault-operations-commands.js",
30
+ "bin/register-config-commands.js",
31
+ "bin/register-route-commands.js",
32
+ "bin/register-kanban-commands.js",
33
+ "bin/register-project-commands.js",
34
+ "bin/register-task-commands.js",
35
+ "bin/register-tailscale-commands.js",
36
+ "docs",
37
+ "dashboard/server.js",
38
+ "dashboard/public",
39
+ "dashboard/lib/vault-parser.js",
40
+ "dashboard/lib/graph-diff.js",
23
41
  "templates",
24
- "hooks",
25
- "openclaw.plugin.json"
42
+ "openclaw.plugin.json",
43
+ "SKILL.md",
44
+ "CHANGELOG.md",
45
+ "README.md",
46
+ "LICENSE"
26
47
  ],
27
48
  "openclaw": {
28
49
  "plugin": "./openclaw.plugin.json",
29
50
  "extensions": [
30
51
  "./dist/openclaw-plugin.js"
31
- ],
32
- "hooks": [
33
- "./hooks/clawvault"
34
52
  ]
35
53
  },
36
54
  "scripts": {
@@ -1,179 +0,0 @@
1
- import { describe, expect, it } 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 { registerMaintenanceCommands } from './register-maintenance-commands.js';
7
- import { registerQueryCommands } from './register-query-commands.js';
8
- import { registerResilienceCommands } from './register-resilience-commands.js';
9
- import { registerSessionLifecycleCommands } from './register-session-lifecycle-commands.js';
10
- import { registerTemplateCommands } from './register-template-commands.js';
11
- import { registerVaultOperationsCommands } from './register-vault-operations-commands.js';
12
- import { registerConfigCommands } from './register-config-commands.js';
13
- import { registerRouteCommands } from './register-route-commands.js';
14
- import {
15
- chalkStub,
16
- createGetVaultStub,
17
- registerAllCommandModules,
18
- stubResolveVaultPath
19
- } from './test-helpers/cli-command-fixtures.js';
20
-
21
- function listCommandNames(program) {
22
- return program.commands.map((command) => command.name()).sort((a, b) => a.localeCompare(b));
23
- }
24
-
25
- describe('CLI command registration modules', () => {
26
- it('registers core lifecycle commands', () => {
27
- const program = new Command();
28
- registerCoreCommands(program, {
29
- chalk: chalkStub,
30
- path,
31
- fs,
32
- createVault: async () => ({ getCategories: () => [], getQmdRoot: () => '', getQmdCollection: () => '' }),
33
- getVault: createGetVaultStub({ store: async () => ({}), capture: async () => ({}) }),
34
- runQmd: async () => {}
35
- });
36
-
37
- const names = listCommandNames(program);
38
- expect(names).toEqual(expect.arrayContaining(['init', 'setup', 'store', 'patch', 'capture', 'inbox']));
39
- });
40
-
41
- it('registers query commands with profile option', () => {
42
- const program = new Command();
43
- registerQueryCommands(program, {
44
- chalk: chalkStub,
45
- getVault: createGetVaultStub({ find: async () => [], vsearch: async () => [] }),
46
- resolveVaultPath: stubResolveVaultPath,
47
- QmdUnavailableError: class extends Error {},
48
- printQmdMissing: () => {}
49
- });
50
-
51
- const names = listCommandNames(program);
52
- expect(names).toEqual(expect.arrayContaining(['search', 'vsearch', 'context', 'recall', 'inject', 'observe', 'reflect', 'session-recap']));
53
-
54
- const contextCommand = program.commands.find((command) => command.name() === 'context');
55
- const profileOption = contextCommand?.options.find((option) => option.flags.includes('--profile <profile>'));
56
- expect(profileOption?.description).toContain('auto');
57
- });
58
-
59
- it('registers vault operation commands', () => {
60
- const program = new Command();
61
- registerVaultOperationsCommands(program, {
62
- chalk: chalkStub,
63
- fs,
64
- getVault: createGetVaultStub({
65
- list: async () => [],
66
- get: async () => null,
67
- stats: async () => ({ tags: [], categories: {} }),
68
- sync: async () => ({ copied: [], deleted: [], unchanged: [], errors: [] }),
69
- reindex: async () => 0,
70
- remember: async () => ({ id: '' }),
71
- getQmdCollection: () => ''
72
- }),
73
- runQmd: async () => {},
74
- resolveVaultPath: stubResolveVaultPath,
75
- path
76
- });
77
-
78
- const names = listCommandNames(program);
79
- expect(names).toEqual(expect.arrayContaining([
80
- 'list',
81
- 'get',
82
- 'stats',
83
- 'sync',
84
- 'reindex',
85
- 'remember',
86
- 'shell-init',
87
- 'dashboard'
88
- ]));
89
- });
90
-
91
- it('registers maintenance, resilience, session-lifecycle and template commands', () => {
92
- const program = new Command();
93
- registerMaintenanceCommands(program, { chalk: chalkStub });
94
- registerResilienceCommands(program, {
95
- chalk: chalkStub,
96
- resolveVaultPath: stubResolveVaultPath
97
- });
98
- registerSessionLifecycleCommands(program, {
99
- chalk: chalkStub,
100
- resolveVaultPath: stubResolveVaultPath,
101
- QmdUnavailableError: class extends Error {},
102
- printQmdMissing: () => {},
103
- getVault: createGetVaultStub({
104
- createHandoff: async () => ({ id: '', path: '' }),
105
- getQmdCollection: () => '',
106
- generateRecap: async () => ({}),
107
- formatRecap: () => ''
108
- }),
109
- runQmd: async () => {}
110
- });
111
- registerTemplateCommands(program, { chalk: chalkStub });
112
- registerConfigCommands(program, {
113
- chalk: chalkStub,
114
- resolveVaultPath: stubResolveVaultPath
115
- });
116
- registerRouteCommands(program, {
117
- chalk: chalkStub,
118
- resolveVaultPath: stubResolveVaultPath
119
- });
120
-
121
- const names = listCommandNames(program);
122
- expect(names).toEqual(expect.arrayContaining([
123
- 'doctor',
124
- 'benchmark',
125
- 'maintain',
126
- 'embed',
127
- 'compat',
128
- 'graph',
129
- 'entities',
130
- 'entity',
131
- 'link',
132
- 'rebuild',
133
- 'archive',
134
- 'migrate-observations',
135
- 'replay',
136
- 'sync-bd',
137
- 'checkpoint',
138
- 'recover',
139
- 'status',
140
- 'clean-exit',
141
- 'repair-session',
142
- 'wake',
143
- 'sleep',
144
- 'handoff',
145
- 'recap',
146
- 'template',
147
- 'config',
148
- 'route'
149
- ]));
150
-
151
- const templateCommand = program.commands.find((command) => command.name() === 'template');
152
- const templateSubcommands = templateCommand?.commands.map((command) => command.name()) ?? [];
153
- expect(templateSubcommands).toEqual(expect.arrayContaining(['list', 'create', 'add']));
154
-
155
- const compatCommand = program.commands.find((command) => command.name() === 'compat');
156
- const strictOption = compatCommand?.options.find((option) => option.flags.includes('--strict'));
157
- const baseDirOption = compatCommand?.options.find((option) => option.flags.includes('--base-dir <path>'));
158
- expect(strictOption).toBeTruthy();
159
- expect(baseDirOption).toBeTruthy();
160
- });
161
-
162
- it('keeps top-level command names unique when modules are combined', () => {
163
- const program = registerAllCommandModules(new Command());
164
-
165
- const names = program.commands.map((command) => command.name());
166
- const unique = new Set(names);
167
- expect(unique.size).toBe(names.length);
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
- });
179
- });
@@ -1,154 +0,0 @@
1
- import { beforeEach, describe, expect, it, vi } from 'vitest';
2
- import path from 'path';
3
-
4
- const {
5
- spawnMock,
6
- resolveConfiguredVaultPathMock,
7
- clawvaultCtorMock,
8
- loadMock
9
- } = vi.hoisted(() => ({
10
- spawnMock: vi.fn(),
11
- resolveConfiguredVaultPathMock: vi.fn(),
12
- clawvaultCtorMock: vi.fn(),
13
- loadMock: vi.fn()
14
- }));
15
-
16
- vi.mock('child_process', () => ({
17
- spawn: spawnMock
18
- }));
19
-
20
- vi.mock('../dist/index.js', () => ({
21
- ClawVault: clawvaultCtorMock,
22
- resolveVaultPath: resolveConfiguredVaultPathMock,
23
- QmdUnavailableError: class QmdUnavailableError extends Error {},
24
- QMD_INSTALL_COMMAND: 'install-qmd'
25
- }));
26
-
27
- async function loadRuntimeModule() {
28
- vi.resetModules();
29
- return await import('./command-runtime.js');
30
- }
31
-
32
- beforeEach(() => {
33
- vi.clearAllMocks();
34
- clawvaultCtorMock.mockImplementation(() => ({
35
- load: loadMock
36
- }));
37
- });
38
-
39
- describe('command runtime helpers', () => {
40
- it('delegates vault path resolution and loads vaults', async () => {
41
- resolveConfiguredVaultPathMock.mockReturnValue('/resolved/vault');
42
- loadMock.mockResolvedValue(undefined);
43
- const { getVault, resolveVaultPath } = await loadRuntimeModule();
44
-
45
- const resolved = resolveVaultPath('/explicit');
46
- expect(resolveConfiguredVaultPathMock).toHaveBeenCalledWith({ explicitPath: '/explicit' });
47
- expect(resolved).toBe('/resolved/vault');
48
-
49
- await getVault('/explicit');
50
- expect(clawvaultCtorMock).toHaveBeenCalledWith('/resolved/vault');
51
- expect(loadMock).toHaveBeenCalled();
52
- });
53
-
54
- it('maps qmd ENOENT failures to QmdUnavailableError', async () => {
55
- const { runQmd, QmdUnavailableError } = await loadRuntimeModule();
56
- spawnMock.mockImplementation(() => {
57
- const handlers = {};
58
- const proc = {
59
- on: (event, handler) => {
60
- handlers[event] = handler;
61
- }
62
- };
63
- queueMicrotask(() => {
64
- handlers.error?.({ code: 'ENOENT' });
65
- });
66
- return proc;
67
- });
68
-
69
- await expect(runQmd(['update'])).rejects.toBeInstanceOf(QmdUnavailableError);
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
-
106
- it('surfaces qmd non-zero exit codes as errors', async () => {
107
- const { runQmd } = await loadRuntimeModule();
108
- spawnMock.mockImplementation(() => {
109
- const handlers = {};
110
- const proc = {
111
- on: (event, handler) => {
112
- handlers[event] = handler;
113
- }
114
- };
115
- queueMicrotask(() => {
116
- handlers.close?.(2);
117
- });
118
- return proc;
119
- });
120
-
121
- await expect(runQmd(['update'])).rejects.toThrow('qmd exited with code 2');
122
- });
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
-
141
- it('prints consistent qmd missing guidance', async () => {
142
- const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
143
- const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
144
- try {
145
- const { printQmdMissing } = await loadRuntimeModule();
146
- printQmdMissing();
147
- expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('ClawVault requires qmd.'));
148
- expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('install-qmd'));
149
- } finally {
150
- errorSpy.mockRestore();
151
- logSpy.mockRestore();
152
- }
153
- });
154
- });