clawvault 3.5.0 → 3.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.
- package/README.md +33 -25
- package/bin/command-registration.test.js +179 -0
- package/bin/command-runtime.test.js +154 -0
- package/bin/help-contract.test.js +55 -0
- package/bin/register-config-route-commands.test.js +121 -0
- package/bin/register-core-commands.test.js +80 -0
- package/bin/register-kanban-commands.test.js +83 -0
- package/bin/register-project-commands.test.js +206 -0
- package/bin/register-query-commands.test.js +80 -0
- package/bin/register-resilience-commands.test.js +81 -0
- package/bin/register-task-commands.test.js +69 -0
- package/bin/register-template-commands.test.js +87 -0
- package/bin/test-helpers/cli-command-fixtures.js +120 -0
- package/dashboard/lib/graph-diff.test.js +75 -0
- package/dashboard/lib/vault-parser.test.js +254 -0
- package/dist/{chunk-DCF4KMFD.js → chunk-DPK6P6BO.js} +3 -3
- package/dist/{chunk-BLQXXX7Q.js → chunk-FNFP7N6A.js} +2 -2
- package/dist/{chunk-JI7VUQV7.js → chunk-IFUHSHN3.js} +146 -118
- package/dist/{chunk-QFWERBDP.js → chunk-J6DW6HBX.js} +1 -1
- package/dist/{chunk-VXAGOLDP.js → chunk-LCBHM3D6.js} +1 -1
- package/dist/{chunk-HGDDW24U.js → chunk-NTQD55S3.js} +3 -3
- package/dist/{chunk-QUFQBAHP.js → chunk-P35SHNAU.js} +93 -147
- package/dist/cli/index.js +8 -8
- package/dist/commands/compat.js +1 -1
- package/dist/commands/inject.js +2 -2
- package/dist/commands/maintain.js +2 -2
- package/dist/commands/observe.js +4 -4
- package/dist/commands/rebuild.js +3 -3
- package/dist/commands/replay.js +4 -4
- package/dist/commands/sleep.js +5 -5
- package/dist/commands/status.js +3 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.js +17 -17
- package/dist/{openclaw-plugin--gqA2BZw.d.ts → openclaw-plugin-9M9qCZgl.d.ts} +2 -2
- package/dist/openclaw-plugin.d.ts +1 -1
- package/dist/openclaw-plugin.js +6 -1
- package/package.json +4 -26
- package/CHANGELOG.md +0 -543
- package/SKILL.md +0 -369
- package/docs/clawhub-security-release-playbook.md +0 -75
- package/docs/getting-started/installation.md +0 -99
- package/docs/openclaw-plugin-usage.md +0 -152
- package/dist/{chunk-7SWP5FKU.js → chunk-FSYISBTU.js} +4 -4
- package/dist/{chunk-D5U3Q4N5.js → chunk-IOKLQR4W.js} +4 -4
- package/dist/{chunk-OFOCU2V4.js → chunk-QL34TMGN.js} +3 -3
package/README.md
CHANGED
|
@@ -267,24 +267,29 @@ clawvault setup --theme neural --canvas --bases
|
|
|
267
267
|
|
|
268
268
|
## OpenClaw Integration
|
|
269
269
|
|
|
270
|
-
ClawVault integrates with OpenClaw as a plugin package
|
|
270
|
+
ClawVault integrates with OpenClaw as a plugin. Install the npm package, then configure it in your OpenClaw config:
|
|
271
271
|
|
|
272
272
|
```bash
|
|
273
|
-
# Install ClawVault
|
|
274
|
-
npm install
|
|
273
|
+
# 1. Install the ClawVault package
|
|
274
|
+
npm install clawvault
|
|
275
275
|
|
|
276
|
-
#
|
|
277
|
-
|
|
276
|
+
# 2. Register the plugin and memory slot in openclaw.json
|
|
277
|
+
openclaw config set plugins.entries.clawvault.package clawvault
|
|
278
|
+
openclaw config set plugins.slots.memory clawvault
|
|
278
279
|
|
|
279
|
-
#
|
|
280
|
-
|
|
281
|
-
# plugins.entries.clawvault.enabled = true
|
|
280
|
+
# 3. Configure your vault path
|
|
281
|
+
openclaw config set plugins.entries.clawvault.config.vaultPath ~/my-vault
|
|
282
282
|
|
|
283
|
-
# Verify
|
|
283
|
+
# 4. Verify
|
|
284
284
|
clawvault compat
|
|
285
285
|
```
|
|
286
286
|
|
|
287
|
-
The plugin
|
|
287
|
+
The plugin automatically:
|
|
288
|
+
- Detects context death and injects recovery alerts
|
|
289
|
+
- Auto-checkpoints before session resets
|
|
290
|
+
- Provides `--profile auto` for context queries
|
|
291
|
+
|
|
292
|
+
> **Legacy note:** The older `openclaw hooks install` / `openclaw hooks enable` flow is no longer the recommended path. Use the plugin model above.
|
|
288
293
|
|
|
289
294
|
### MEMORY.md vs Vault
|
|
290
295
|
|
|
@@ -352,26 +357,29 @@ clawvault compat
|
|
|
352
357
|
|
|
353
358
|
## OpenClaw Setup (Canonical)
|
|
354
359
|
|
|
355
|
-
|
|
360
|
+
ClawVault integrates with OpenClaw as a plugin. Use this sequence:
|
|
356
361
|
|
|
357
362
|
```bash
|
|
358
|
-
# Install
|
|
359
|
-
npm install
|
|
363
|
+
# Install ClawVault
|
|
364
|
+
npm install clawvault
|
|
365
|
+
|
|
366
|
+
# Register plugin and memory slot
|
|
367
|
+
openclaw config set plugins.entries.clawvault.package clawvault
|
|
368
|
+
openclaw config set plugins.slots.memory clawvault
|
|
360
369
|
|
|
361
|
-
#
|
|
362
|
-
|
|
370
|
+
# Configure vault path and desired features
|
|
371
|
+
openclaw config set plugins.entries.clawvault.config.vaultPath ~/my-vault
|
|
372
|
+
openclaw config set plugins.entries.clawvault.config.enableStartupRecovery true
|
|
373
|
+
openclaw config set plugins.entries.clawvault.config.enableSessionContextInjection true
|
|
363
374
|
|
|
364
|
-
#
|
|
365
|
-
|
|
366
|
-
# - set plugins.slots.memory = "clawvault"
|
|
367
|
-
# - set plugins.entries.clawvault.enabled = true
|
|
368
|
-
# - set plugins.entries.clawvault.config.vaultPath as needed
|
|
375
|
+
# Verify
|
|
376
|
+
clawvault compat
|
|
369
377
|
```
|
|
370
378
|
|
|
371
379
|
Important:
|
|
372
380
|
|
|
373
|
-
- `clawhub install clawvault` installs skill guidance, but does not
|
|
374
|
-
- After changing plugin config, restart the OpenClaw gateway process.
|
|
381
|
+
- `clawhub install clawvault` installs skill guidance, but does not replace plugin configuration.
|
|
382
|
+
- After changing plugin config, restart the OpenClaw gateway process so plugin registration reloads.
|
|
375
383
|
|
|
376
384
|
## Minimal AGENTS.md Additions
|
|
377
385
|
|
|
@@ -489,9 +497,9 @@ vault/
|
|
|
489
497
|
- `qmd` is optional; in-process BM25 search is available without it
|
|
490
498
|
- if you want fallback compatibility, ensure `qmd --version` works in the same shell
|
|
491
499
|
- Plugin not active in OpenClaw:
|
|
492
|
-
-
|
|
493
|
-
-
|
|
494
|
-
-
|
|
500
|
+
- run `openclaw config set plugins.entries.clawvault.package clawvault`
|
|
501
|
+
- run `openclaw config set plugins.slots.memory clawvault`
|
|
502
|
+
- restart the OpenClaw gateway process
|
|
495
503
|
- OpenClaw integration drift:
|
|
496
504
|
- run `clawvault compat`
|
|
497
505
|
- Session transcript corruption:
|
|
@@ -0,0 +1,179 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,154 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { registerAllCommandModules } from './test-helpers/cli-command-fixtures.js';
|
|
3
|
+
|
|
4
|
+
describe('CLI help contract', () => {
|
|
5
|
+
it('includes expected high-level command surface', () => {
|
|
6
|
+
const help = registerAllCommandModules().helpInformation();
|
|
7
|
+
expect(help).toContain('init');
|
|
8
|
+
expect(help).toContain('context');
|
|
9
|
+
expect(help).toContain('recall');
|
|
10
|
+
expect(help).toContain('inject');
|
|
11
|
+
expect(help).toContain('doctor');
|
|
12
|
+
expect(help).toContain('benchmark');
|
|
13
|
+
expect(help).toContain('maintain');
|
|
14
|
+
expect(help).toContain('embed');
|
|
15
|
+
expect(help).toContain('inbox');
|
|
16
|
+
expect(help).toContain('patch');
|
|
17
|
+
expect(help).toContain('compat');
|
|
18
|
+
expect(help).toContain('graph');
|
|
19
|
+
expect(help).toContain('reflect');
|
|
20
|
+
expect(help).toContain('replay');
|
|
21
|
+
expect(help).toContain('repair-session');
|
|
22
|
+
expect(help).toContain('project');
|
|
23
|
+
expect(help).toContain('template');
|
|
24
|
+
expect(help).toContain('config');
|
|
25
|
+
expect(help).toContain('route');
|
|
26
|
+
expect(help).toContain('entity');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('documents context/compat/inject/project help details', () => {
|
|
30
|
+
const program = registerAllCommandModules();
|
|
31
|
+
const contextHelp = program.commands.find((command) => command.name() === 'context')?.helpInformation() ?? '';
|
|
32
|
+
const compatHelp = program.commands.find((command) => command.name() === 'compat')?.helpInformation() ?? '';
|
|
33
|
+
const injectHelp = program.commands.find((command) => command.name() === 'inject')?.helpInformation() ?? '';
|
|
34
|
+
const projectCommand = program.commands.find((command) => command.name() === 'project');
|
|
35
|
+
const projectListHelp = projectCommand?.commands.find((command) => command.name() === 'list')?.helpInformation() ?? '';
|
|
36
|
+
const projectBoardHelp = projectCommand?.commands.find((command) => command.name() === 'board')?.helpInformation() ?? '';
|
|
37
|
+
expect(contextHelp).toContain('--profile <profile>');
|
|
38
|
+
expect(contextHelp).toContain('auto');
|
|
39
|
+
expect(compatHelp).toContain('--strict');
|
|
40
|
+
expect(injectHelp).toContain('inject.maxResults');
|
|
41
|
+
expect(injectHelp).toContain('inject.scope');
|
|
42
|
+
expect(projectListHelp).toContain('archived projects are hidden');
|
|
43
|
+
expect(projectBoardHelp).toContain('default: status');
|
|
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
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { registerConfigCommands } from './register-config-commands.js';
|
|
4
|
+
import { registerRouteCommands } from './register-route-commands.js';
|
|
5
|
+
import { chalkStub } from './test-helpers/cli-command-fixtures.js';
|
|
6
|
+
|
|
7
|
+
const {
|
|
8
|
+
getConfigValueMock,
|
|
9
|
+
setConfigValueMock,
|
|
10
|
+
listConfigMock,
|
|
11
|
+
resetConfigMock,
|
|
12
|
+
addRouteRuleMock,
|
|
13
|
+
listRouteRulesMock,
|
|
14
|
+
removeRouteRuleMock,
|
|
15
|
+
testRouteRuleMock
|
|
16
|
+
} = vi.hoisted(() => ({
|
|
17
|
+
getConfigValueMock: vi.fn(),
|
|
18
|
+
setConfigValueMock: vi.fn(),
|
|
19
|
+
listConfigMock: vi.fn(),
|
|
20
|
+
resetConfigMock: vi.fn(),
|
|
21
|
+
addRouteRuleMock: vi.fn(),
|
|
22
|
+
listRouteRulesMock: vi.fn(),
|
|
23
|
+
removeRouteRuleMock: vi.fn(),
|
|
24
|
+
testRouteRuleMock: vi.fn()
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
vi.mock('../dist/index.js', () => ({
|
|
28
|
+
SUPPORTED_CONFIG_KEYS: [
|
|
29
|
+
'name',
|
|
30
|
+
'categories',
|
|
31
|
+
'theme',
|
|
32
|
+
'observe.model',
|
|
33
|
+
'observe.provider',
|
|
34
|
+
'observer.compression.provider',
|
|
35
|
+
'observer.compression.model',
|
|
36
|
+
'observer.compression.baseUrl',
|
|
37
|
+
'observer.compression.apiKey',
|
|
38
|
+
'context.maxResults',
|
|
39
|
+
'context.defaultProfile',
|
|
40
|
+
'graph.maxHops',
|
|
41
|
+
'inject.maxResults',
|
|
42
|
+
'inject.useLlm',
|
|
43
|
+
'inject.scope'
|
|
44
|
+
],
|
|
45
|
+
getConfigValue: getConfigValueMock,
|
|
46
|
+
setConfigValue: setConfigValueMock,
|
|
47
|
+
listConfig: listConfigMock,
|
|
48
|
+
resetConfig: resetConfigMock,
|
|
49
|
+
addRouteRule: addRouteRuleMock,
|
|
50
|
+
listRouteRules: listRouteRulesMock,
|
|
51
|
+
removeRouteRule: removeRouteRuleMock,
|
|
52
|
+
testRouteRule: testRouteRuleMock
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
function buildProgram() {
|
|
56
|
+
const program = new Command();
|
|
57
|
+
const resolveVaultPath = (value) => value ?? '/vault';
|
|
58
|
+
registerConfigCommands(program, { chalk: chalkStub, resolveVaultPath });
|
|
59
|
+
registerRouteCommands(program, { chalk: chalkStub, resolveVaultPath });
|
|
60
|
+
return program;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function runCommand(args) {
|
|
64
|
+
const program = buildProgram();
|
|
65
|
+
await program.parseAsync(args, { from: 'user' });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
beforeEach(() => {
|
|
69
|
+
vi.clearAllMocks();
|
|
70
|
+
getConfigValueMock.mockReturnValue('demo-vault');
|
|
71
|
+
setConfigValueMock.mockReturnValue({ value: 'demo-vault' });
|
|
72
|
+
listConfigMock.mockReturnValue({ name: 'demo-vault', context: { maxResults: 5 } });
|
|
73
|
+
resetConfigMock.mockReturnValue(undefined);
|
|
74
|
+
addRouteRuleMock.mockReturnValue({ pattern: 'Pedro', target: 'people/pedro', priority: 1 });
|
|
75
|
+
listRouteRulesMock.mockReturnValue([{ pattern: 'Pedro', target: 'people/pedro', priority: 1 }]);
|
|
76
|
+
removeRouteRuleMock.mockReturnValue(true);
|
|
77
|
+
testRouteRuleMock.mockReturnValue({ pattern: 'Pedro', target: 'people/pedro', priority: 1 });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('config and route command registrations', () => {
|
|
81
|
+
it('executes config get/set/list subcommands', async () => {
|
|
82
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
83
|
+
try {
|
|
84
|
+
await runCommand(['config', 'get', 'name']);
|
|
85
|
+
expect(getConfigValueMock).toHaveBeenCalledWith('/vault', 'name');
|
|
86
|
+
|
|
87
|
+
await runCommand(['config', 'set', 'categories', 'people,projects']);
|
|
88
|
+
expect(setConfigValueMock).toHaveBeenCalledWith('/vault', 'categories', 'people,projects');
|
|
89
|
+
|
|
90
|
+
await runCommand(['config', 'list']);
|
|
91
|
+
expect(listConfigMock).toHaveBeenCalledWith('/vault');
|
|
92
|
+
|
|
93
|
+
const output = logSpy.mock.calls.map((call) => call.join(' ')).join('\n');
|
|
94
|
+
expect(output).toContain('context.maxResults');
|
|
95
|
+
} finally {
|
|
96
|
+
logSpy.mockRestore();
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('executes route add/remove/test subcommands', async () => {
|
|
101
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
102
|
+
try {
|
|
103
|
+
await runCommand(['route', 'add', 'Pedro', 'people/pedro']);
|
|
104
|
+
expect(addRouteRuleMock).toHaveBeenCalledWith('/vault', 'Pedro', 'people/pedro');
|
|
105
|
+
|
|
106
|
+
await runCommand(['route', 'remove', 'Pedro']);
|
|
107
|
+
expect(removeRouteRuleMock).toHaveBeenCalledWith('/vault', 'Pedro');
|
|
108
|
+
|
|
109
|
+
await runCommand(['route', 'test', 'Talked to Pedro']);
|
|
110
|
+
expect(testRouteRuleMock).toHaveBeenCalledWith('/vault', 'Talked to Pedro');
|
|
111
|
+
|
|
112
|
+
await runCommand(['route', 'list']);
|
|
113
|
+
expect(listRouteRulesMock).toHaveBeenCalledWith('/vault');
|
|
114
|
+
|
|
115
|
+
const output = logSpy.mock.calls.map((call) => call.join(' ')).join('\n');
|
|
116
|
+
expect(output).toContain('Route matched');
|
|
117
|
+
} finally {
|
|
118
|
+
logSpy.mockRestore();
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -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
|
+
});
|