clawvault 3.5.0 → 3.5.1
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 +26 -26
- 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-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/{chunk-JI7VUQV7.js → chunk-X3SPPUFG.js} +132 -118
- 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/LICENSE +0 -21
- 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,23 @@ clawvault setup --theme neural --canvas --bases
|
|
|
267
267
|
|
|
268
268
|
## OpenClaw Integration
|
|
269
269
|
|
|
270
|
-
|
|
270
|
+
For hook-based lifecycle integration with OpenClaw:
|
|
271
271
|
|
|
272
272
|
```bash
|
|
273
|
-
# Install
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
# Add ClawVault package path to plugins.load.paths in openclaw.json
|
|
277
|
-
# (You can use `npm root -g` to locate node_modules)
|
|
273
|
+
# Install and enable hook pack
|
|
274
|
+
openclaw hooks install clawvault
|
|
275
|
+
openclaw hooks enable clawvault
|
|
278
276
|
|
|
279
|
-
#
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
# Verify runtime assumptions
|
|
277
|
+
# Verify
|
|
278
|
+
openclaw hooks list --verbose
|
|
279
|
+
openclaw hooks check
|
|
284
280
|
clawvault compat
|
|
285
281
|
```
|
|
286
282
|
|
|
287
|
-
The
|
|
283
|
+
The hook automatically:
|
|
284
|
+
- Detects context death and injects recovery alerts
|
|
285
|
+
- Auto-checkpoints before session resets
|
|
286
|
+
- Provides `--profile auto` for context queries
|
|
288
287
|
|
|
289
288
|
### MEMORY.md vs Vault
|
|
290
289
|
|
|
@@ -352,26 +351,27 @@ clawvault compat
|
|
|
352
351
|
|
|
353
352
|
## OpenClaw Setup (Canonical)
|
|
354
353
|
|
|
355
|
-
|
|
354
|
+
If you want hook-based lifecycle integration, use this sequence:
|
|
356
355
|
|
|
357
356
|
```bash
|
|
358
357
|
# Install CLI
|
|
359
358
|
npm install -g clawvault
|
|
360
359
|
|
|
361
|
-
#
|
|
362
|
-
|
|
360
|
+
# Install and enable hook pack
|
|
361
|
+
openclaw hooks install clawvault
|
|
362
|
+
openclaw hooks enable clawvault
|
|
363
363
|
|
|
364
|
-
#
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
364
|
+
# Verify
|
|
365
|
+
openclaw hooks list --verbose
|
|
366
|
+
openclaw hooks info clawvault
|
|
367
|
+
openclaw hooks check
|
|
368
|
+
clawvault compat
|
|
369
369
|
```
|
|
370
370
|
|
|
371
371
|
Important:
|
|
372
372
|
|
|
373
|
-
- `clawhub install clawvault` installs skill guidance, but does not
|
|
374
|
-
- After
|
|
373
|
+
- `clawhub install clawvault` installs skill guidance, but does not replace hook-pack installation.
|
|
374
|
+
- After enabling hooks, restart the OpenClaw gateway process so hook registration reloads.
|
|
375
375
|
|
|
376
376
|
## Minimal AGENTS.md Additions
|
|
377
377
|
|
|
@@ -488,10 +488,10 @@ vault/
|
|
|
488
488
|
- `qmd` fallback errors:
|
|
489
489
|
- `qmd` is optional; in-process BM25 search is available without it
|
|
490
490
|
- if you want fallback compatibility, ensure `qmd --version` works in the same shell
|
|
491
|
-
-
|
|
492
|
-
-
|
|
493
|
-
-
|
|
494
|
-
- verify `
|
|
491
|
+
- Hook/plugin not active in OpenClaw:
|
|
492
|
+
- run `openclaw hooks install clawvault`
|
|
493
|
+
- run `openclaw hooks enable clawvault`
|
|
494
|
+
- verify with `openclaw hooks list --verbose`
|
|
495
495
|
- OpenClaw integration drift:
|
|
496
496
|
- run `clawvault compat`
|
|
497
497
|
- 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
|
+
});
|