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.
- package/CHANGELOG.md +543 -0
- package/LICENSE +21 -0
- package/SKILL.md +369 -0
- package/dist/{chunk-X3SPPUFG.js → chunk-JI7VUQV7.js} +118 -132
- package/dist/{chunk-PLNK37JD.js → chunk-QUFQBAHP.js} +114 -217
- package/dist/cli/index.js +1 -1
- package/dist/commands/compat.js +1 -1
- package/dist/commands/observe.js +1 -1
- package/dist/commands/status.js +4 -4
- package/dist/index.js +11 -8
- package/dist/openclaw-plugin.js +6 -1
- package/docs/clawhub-security-release-playbook.md +75 -0
- package/docs/getting-started/installation.md +99 -0
- package/docs/openclaw-plugin-usage.md +152 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +26 -8
- package/bin/command-registration.test.js +0 -179
- package/bin/command-runtime.test.js +0 -154
- package/bin/help-contract.test.js +0 -55
- package/bin/register-config-route-commands.test.js +0 -121
- package/bin/register-core-commands.test.js +0 -80
- package/bin/register-kanban-commands.test.js +0 -83
- package/bin/register-project-commands.test.js +0 -206
- package/bin/register-query-commands.test.js +0 -80
- package/bin/register-resilience-commands.test.js +0 -81
- package/bin/register-task-commands.test.js +0 -69
- package/bin/register-template-commands.test.js +0 -87
- package/bin/test-helpers/cli-command-fixtures.js +0 -120
- package/dashboard/lib/graph-diff.test.js +0 -75
- package/dashboard/lib/vault-parser.test.js +0 -254
- package/hooks/clawvault/HOOK.md +0 -130
- package/hooks/clawvault/handler.js +0 -1696
- package/hooks/clawvault/handler.test.js +0 -576
- package/hooks/clawvault/integrity.js +0 -112
- package/hooks/clawvault/integrity.test.js +0 -32
- 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)
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clawvault",
|
|
3
|
-
"version": "3.
|
|
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
|
-
"
|
|
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
|
-
"
|
|
25
|
-
"
|
|
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
|
-
});
|