clawvault 2.5.2 → 2.5.3
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 +159 -200
- package/bin/clawvault.js +111 -111
- package/bin/command-registration.test.js +166 -166
- package/bin/command-runtime.js +93 -93
- package/bin/command-runtime.test.js +154 -154
- package/bin/help-contract.test.js +39 -39
- package/bin/register-config-commands.js +153 -153
- package/bin/register-config-route-commands.test.js +121 -121
- package/bin/register-core-commands.js +237 -237
- package/bin/register-kanban-commands.js +56 -56
- package/bin/register-kanban-commands.test.js +83 -83
- package/bin/register-maintenance-commands.js +282 -282
- package/bin/register-project-commands.js +209 -209
- package/bin/register-project-commands.test.js +206 -206
- package/bin/register-query-commands.js +317 -317
- package/bin/register-query-commands.test.js +65 -65
- package/bin/register-resilience-commands.js +182 -182
- package/bin/register-resilience-commands.test.js +81 -81
- package/bin/register-route-commands.js +114 -114
- package/bin/register-session-lifecycle-commands.js +206 -206
- package/bin/register-tailscale-commands.js +106 -106
- package/bin/register-task-commands.js +348 -348
- package/bin/register-task-commands.test.js +69 -69
- package/bin/register-template-commands.js +72 -72
- package/bin/register-vault-operations-commands.js +300 -300
- package/bin/test-helpers/cli-command-fixtures.js +119 -119
- package/dashboard/lib/graph-diff.js +104 -104
- package/dashboard/lib/graph-diff.test.js +75 -75
- package/dashboard/lib/vault-parser.js +556 -556
- package/dashboard/lib/vault-parser.test.js +254 -254
- package/dashboard/public/app.js +796 -796
- package/dashboard/public/index.html +52 -52
- package/dashboard/public/styles.css +221 -221
- package/dashboard/server.js +374 -374
- package/dist/{chunk-HWUNREDJ.js → chunk-FG6RJMCN.js} +1 -1
- package/dist/{chunk-HRTPQQF2.js → chunk-IZEY5S74.js} +1 -1
- package/dist/{chunk-BHO7WSAY.js → chunk-LMEMZGUV.js} +1 -1
- package/dist/{chunk-PLZKZW4I.js → chunk-OSMS7QIG.js} +1 -1
- package/dist/cli/index.js +3 -3
- package/dist/commands/doctor.js +2 -2
- package/dist/commands/observe.js +2 -2
- package/dist/commands/status.js +1 -1
- package/dist/index.js +4 -4
- package/hooks/clawvault/HOOK.md +83 -74
- package/hooks/clawvault/handler.js +816 -816
- package/hooks/clawvault/handler.test.js +263 -263
- package/package.json +94 -125
- package/templates/checkpoint.md +19 -19
- package/templates/daily-note.md +19 -19
- package/templates/daily.md +19 -19
- package/templates/decision.md +17 -17
- package/templates/handoff.md +19 -19
- package/templates/lesson.md +16 -16
- package/templates/person.md +19 -19
- package/templates/project.md +23 -23
|
@@ -1,65 +1,65 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { Command } from 'commander';
|
|
3
|
-
import { registerQueryCommands } from './register-query-commands.js';
|
|
4
|
-
import {
|
|
5
|
-
chalkStub,
|
|
6
|
-
createGetVaultStub,
|
|
7
|
-
stubResolveVaultPath
|
|
8
|
-
} from './test-helpers/cli-command-fixtures.js';
|
|
9
|
-
|
|
10
|
-
function buildProgram() {
|
|
11
|
-
const program = new Command();
|
|
12
|
-
registerQueryCommands(program, {
|
|
13
|
-
chalk: chalkStub,
|
|
14
|
-
getVault: createGetVaultStub({ find: async () => [], vsearch: async () => [] }),
|
|
15
|
-
resolveVaultPath: stubResolveVaultPath,
|
|
16
|
-
QmdUnavailableError: class extends Error {},
|
|
17
|
-
printQmdMissing: () => {}
|
|
18
|
-
});
|
|
19
|
-
return program;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
describe('register-query-commands', () => {
|
|
23
|
-
it('documents inject command options and config-backed defaults', () => {
|
|
24
|
-
const program = buildProgram();
|
|
25
|
-
const injectCommand = program.commands.find((command) => command.name() === 'inject');
|
|
26
|
-
expect(injectCommand).toBeDefined();
|
|
27
|
-
|
|
28
|
-
const injectFlags = injectCommand?.options.map((option) => option.flags) ?? [];
|
|
29
|
-
expect(injectFlags).toEqual(expect.arrayContaining([
|
|
30
|
-
'-n, --max-results <n>',
|
|
31
|
-
'--scope <scope>',
|
|
32
|
-
'--enable-llm',
|
|
33
|
-
'--disable-llm',
|
|
34
|
-
'--format <format>',
|
|
35
|
-
'--model <model>',
|
|
36
|
-
'-v, --vault <path>'
|
|
37
|
-
]));
|
|
38
|
-
|
|
39
|
-
const maxResultsOption = injectCommand?.options.find((option) => option.flags === '-n, --max-results <n>');
|
|
40
|
-
const scopeOption = injectCommand?.options.find((option) => option.flags === '--scope <scope>');
|
|
41
|
-
const formatOption = injectCommand?.options.find((option) => option.flags === '--format <format>');
|
|
42
|
-
expect(maxResultsOption?.description).toContain('inject.maxResults');
|
|
43
|
-
expect(scopeOption?.description).toContain('inject.scope');
|
|
44
|
-
expect(formatOption?.description).toContain('default: markdown');
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('documents observe extraction toggles and threshold defaults', () => {
|
|
48
|
-
const program = buildProgram();
|
|
49
|
-
const observeCommand = program.commands.find((command) => command.name() === 'observe');
|
|
50
|
-
expect(observeCommand).toBeDefined();
|
|
51
|
-
|
|
52
|
-
const observeFlags = observeCommand?.options.map((option) => option.flags) ?? [];
|
|
53
|
-
expect(observeFlags).toEqual(expect.arrayContaining([
|
|
54
|
-
'--extract-tasks',
|
|
55
|
-
'--no-extract-tasks',
|
|
56
|
-
'--threshold <n>',
|
|
57
|
-
'--reflect-threshold <n>'
|
|
58
|
-
]));
|
|
59
|
-
|
|
60
|
-
const thresholdOption = observeCommand?.options.find((option) => option.flags === '--threshold <n>');
|
|
61
|
-
const reflectThresholdOption = observeCommand?.options.find((option) => option.flags === '--reflect-threshold <n>');
|
|
62
|
-
expect(thresholdOption?.description).toContain('default: 30000');
|
|
63
|
-
expect(reflectThresholdOption?.description).toContain('default: 40000');
|
|
64
|
-
});
|
|
65
|
-
});
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { registerQueryCommands } from './register-query-commands.js';
|
|
4
|
+
import {
|
|
5
|
+
chalkStub,
|
|
6
|
+
createGetVaultStub,
|
|
7
|
+
stubResolveVaultPath
|
|
8
|
+
} from './test-helpers/cli-command-fixtures.js';
|
|
9
|
+
|
|
10
|
+
function buildProgram() {
|
|
11
|
+
const program = new Command();
|
|
12
|
+
registerQueryCommands(program, {
|
|
13
|
+
chalk: chalkStub,
|
|
14
|
+
getVault: createGetVaultStub({ find: async () => [], vsearch: async () => [] }),
|
|
15
|
+
resolveVaultPath: stubResolveVaultPath,
|
|
16
|
+
QmdUnavailableError: class extends Error {},
|
|
17
|
+
printQmdMissing: () => {}
|
|
18
|
+
});
|
|
19
|
+
return program;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe('register-query-commands', () => {
|
|
23
|
+
it('documents inject command options and config-backed defaults', () => {
|
|
24
|
+
const program = buildProgram();
|
|
25
|
+
const injectCommand = program.commands.find((command) => command.name() === 'inject');
|
|
26
|
+
expect(injectCommand).toBeDefined();
|
|
27
|
+
|
|
28
|
+
const injectFlags = injectCommand?.options.map((option) => option.flags) ?? [];
|
|
29
|
+
expect(injectFlags).toEqual(expect.arrayContaining([
|
|
30
|
+
'-n, --max-results <n>',
|
|
31
|
+
'--scope <scope>',
|
|
32
|
+
'--enable-llm',
|
|
33
|
+
'--disable-llm',
|
|
34
|
+
'--format <format>',
|
|
35
|
+
'--model <model>',
|
|
36
|
+
'-v, --vault <path>'
|
|
37
|
+
]));
|
|
38
|
+
|
|
39
|
+
const maxResultsOption = injectCommand?.options.find((option) => option.flags === '-n, --max-results <n>');
|
|
40
|
+
const scopeOption = injectCommand?.options.find((option) => option.flags === '--scope <scope>');
|
|
41
|
+
const formatOption = injectCommand?.options.find((option) => option.flags === '--format <format>');
|
|
42
|
+
expect(maxResultsOption?.description).toContain('inject.maxResults');
|
|
43
|
+
expect(scopeOption?.description).toContain('inject.scope');
|
|
44
|
+
expect(formatOption?.description).toContain('default: markdown');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('documents observe extraction toggles and threshold defaults', () => {
|
|
48
|
+
const program = buildProgram();
|
|
49
|
+
const observeCommand = program.commands.find((command) => command.name() === 'observe');
|
|
50
|
+
expect(observeCommand).toBeDefined();
|
|
51
|
+
|
|
52
|
+
const observeFlags = observeCommand?.options.map((option) => option.flags) ?? [];
|
|
53
|
+
expect(observeFlags).toEqual(expect.arrayContaining([
|
|
54
|
+
'--extract-tasks',
|
|
55
|
+
'--no-extract-tasks',
|
|
56
|
+
'--threshold <n>',
|
|
57
|
+
'--reflect-threshold <n>'
|
|
58
|
+
]));
|
|
59
|
+
|
|
60
|
+
const thresholdOption = observeCommand?.options.find((option) => option.flags === '--threshold <n>');
|
|
61
|
+
const reflectThresholdOption = observeCommand?.options.find((option) => option.flags === '--reflect-threshold <n>');
|
|
62
|
+
expect(thresholdOption?.description).toContain('default: 30000');
|
|
63
|
+
expect(reflectThresholdOption?.description).toContain('default: 40000');
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -1,182 +1,182 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Context-resilience and session-repair command registrations.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
export function registerResilienceCommands(program, { chalk, resolveVaultPath }) {
|
|
6
|
-
// === CHECKPOINT ===
|
|
7
|
-
program
|
|
8
|
-
.command('checkpoint')
|
|
9
|
-
.description('Quick state checkpoint for context death resilience')
|
|
10
|
-
.option('--working-on <text>', 'What you are currently working on')
|
|
11
|
-
.option('--focus <text>', 'Current focus area')
|
|
12
|
-
.option('--blocked <text>', 'What is blocking progress')
|
|
13
|
-
.option('--urgent', 'Trigger OpenClaw wake after checkpoint')
|
|
14
|
-
.option('-v, --vault <path>', 'Vault path')
|
|
15
|
-
.option('--json', 'Output as JSON')
|
|
16
|
-
.action(async (options) => {
|
|
17
|
-
try {
|
|
18
|
-
const { checkpoint } = await import('../dist/commands/checkpoint.js');
|
|
19
|
-
const data = await checkpoint({
|
|
20
|
-
vaultPath: resolveVaultPath(options.vault),
|
|
21
|
-
workingOn: options.workingOn,
|
|
22
|
-
focus: options.focus,
|
|
23
|
-
blocked: options.blocked,
|
|
24
|
-
urgent: options.urgent
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
if (options.json) {
|
|
28
|
-
console.log(JSON.stringify(data, null, 2));
|
|
29
|
-
} else {
|
|
30
|
-
console.log(chalk.green('✓ Checkpoint saved'));
|
|
31
|
-
console.log(chalk.dim(` Timestamp: ${data.timestamp}`));
|
|
32
|
-
if (data.workingOn) console.log(chalk.dim(` Working on: ${data.workingOn}`));
|
|
33
|
-
if (data.focus) console.log(chalk.dim(` Focus: ${data.focus}`));
|
|
34
|
-
if (data.blocked) console.log(chalk.dim(` Blocked: ${data.blocked}`));
|
|
35
|
-
if (data.urgent) console.log(chalk.dim(' Urgent: yes'));
|
|
36
|
-
}
|
|
37
|
-
} catch (err) {
|
|
38
|
-
console.error(chalk.red(`Error: ${err.message}`));
|
|
39
|
-
process.exit(1);
|
|
40
|
-
}
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
// === RECOVER ===
|
|
44
|
-
program
|
|
45
|
-
.command('recover')
|
|
46
|
-
.description('Check for context death and recover state')
|
|
47
|
-
.option('--clear', 'Clear the dirty death flag after recovery')
|
|
48
|
-
.option('--check', 'Check dirty death flag without clearing it')
|
|
49
|
-
.option('--list', 'List saved checkpoints (newest first)')
|
|
50
|
-
.option('--verbose', 'Show full checkpoint and handoff content')
|
|
51
|
-
.option('-v, --vault <path>', 'Vault path')
|
|
52
|
-
.option('--json', 'Output as JSON')
|
|
53
|
-
.action(async (options) => {
|
|
54
|
-
try {
|
|
55
|
-
if (options.check && options.list) {
|
|
56
|
-
throw new Error('--check and --list cannot be used together.');
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const {
|
|
60
|
-
recover,
|
|
61
|
-
formatRecoveryInfo,
|
|
62
|
-
checkRecoveryStatus,
|
|
63
|
-
formatRecoveryCheckStatus,
|
|
64
|
-
listCheckpoints,
|
|
65
|
-
formatCheckpointList
|
|
66
|
-
} = await import('../dist/commands/recover.js');
|
|
67
|
-
const vaultPath = resolveVaultPath(options.vault);
|
|
68
|
-
|
|
69
|
-
if (options.check) {
|
|
70
|
-
const status = await checkRecoveryStatus(vaultPath);
|
|
71
|
-
if (options.json) {
|
|
72
|
-
console.log(JSON.stringify(status, null, 2));
|
|
73
|
-
} else {
|
|
74
|
-
console.log(formatRecoveryCheckStatus(status));
|
|
75
|
-
}
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (options.list) {
|
|
80
|
-
const checkpoints = listCheckpoints(vaultPath);
|
|
81
|
-
if (options.json) {
|
|
82
|
-
console.log(JSON.stringify(checkpoints, null, 2));
|
|
83
|
-
} else {
|
|
84
|
-
console.log(formatCheckpointList(checkpoints));
|
|
85
|
-
}
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const info = await recover(vaultPath, {
|
|
90
|
-
clearFlag: options.clear,
|
|
91
|
-
verbose: options.verbose
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
if (options.json) {
|
|
95
|
-
console.log(JSON.stringify(info, null, 2));
|
|
96
|
-
} else {
|
|
97
|
-
console.log(formatRecoveryInfo(info, { verbose: options.verbose }));
|
|
98
|
-
}
|
|
99
|
-
} catch (err) {
|
|
100
|
-
console.error(chalk.red(`Error: ${err.message}`));
|
|
101
|
-
process.exit(1);
|
|
102
|
-
}
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
// === STATUS ===
|
|
106
|
-
program
|
|
107
|
-
.command('status')
|
|
108
|
-
.description('Show vault health and status')
|
|
109
|
-
.option('-v, --vault <path>', 'Vault path')
|
|
110
|
-
.option('--json', 'Output as JSON')
|
|
111
|
-
.action(async (options) => {
|
|
112
|
-
try {
|
|
113
|
-
const { statusCommand } = await import('../dist/commands/status.js');
|
|
114
|
-
await statusCommand(resolveVaultPath(options.vault), { json: options.json });
|
|
115
|
-
} catch (err) {
|
|
116
|
-
console.error(chalk.red(`Error: ${err.message}`));
|
|
117
|
-
process.exit(1);
|
|
118
|
-
}
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
// === CLEAN-EXIT ===
|
|
122
|
-
program
|
|
123
|
-
.command('clean-exit')
|
|
124
|
-
.description('Mark session as cleanly exited (clears dirty death flag)')
|
|
125
|
-
.option('-v, --vault <path>', 'Vault path')
|
|
126
|
-
.action(async (options) => {
|
|
127
|
-
try {
|
|
128
|
-
const { cleanExit } = await import('../dist/commands/checkpoint.js');
|
|
129
|
-
await cleanExit(resolveVaultPath(options.vault));
|
|
130
|
-
console.log(chalk.green('✓ Clean exit recorded'));
|
|
131
|
-
} catch (err) {
|
|
132
|
-
console.error(chalk.red(`Error: ${err.message}`));
|
|
133
|
-
process.exit(1);
|
|
134
|
-
}
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
// === REPAIR-SESSION ===
|
|
138
|
-
program
|
|
139
|
-
.command('repair-session')
|
|
140
|
-
.description('Repair corrupted OpenClaw session transcripts')
|
|
141
|
-
.option('-s, --session <id>', 'Session ID (defaults to current main session)')
|
|
142
|
-
.option('-a, --agent <id>', 'Agent ID (defaults to configured agent)')
|
|
143
|
-
.option('--backup', 'Create backup before repair (default: true)', true)
|
|
144
|
-
.option('--no-backup', 'Skip backup creation')
|
|
145
|
-
.option('--dry-run', 'Show what would be repaired without writing')
|
|
146
|
-
.option('--list', 'List available sessions')
|
|
147
|
-
.option('--json', 'Output as JSON')
|
|
148
|
-
.action(async (options) => {
|
|
149
|
-
try {
|
|
150
|
-
const {
|
|
151
|
-
repairSessionCommand,
|
|
152
|
-
formatRepairResult,
|
|
153
|
-
listAgentSessions
|
|
154
|
-
} = await import('../dist/commands/repair-session.js');
|
|
155
|
-
|
|
156
|
-
if (options.list) {
|
|
157
|
-
console.log(listAgentSessions(options.agent));
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const result = await repairSessionCommand({
|
|
162
|
-
sessionId: options.session,
|
|
163
|
-
agentId: options.agent,
|
|
164
|
-
backup: options.backup,
|
|
165
|
-
dryRun: options.dryRun
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
if (options.json) {
|
|
169
|
-
console.log(JSON.stringify(result, null, 2));
|
|
170
|
-
} else {
|
|
171
|
-
console.log(formatRepairResult(result, { dryRun: options.dryRun }));
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
if (result.corruptedEntries.length > 0 && !result.repaired) {
|
|
175
|
-
process.exit(1);
|
|
176
|
-
}
|
|
177
|
-
} catch (err) {
|
|
178
|
-
console.error(chalk.red(`Error: ${err.message}`));
|
|
179
|
-
process.exit(1);
|
|
180
|
-
}
|
|
181
|
-
});
|
|
182
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Context-resilience and session-repair command registrations.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export function registerResilienceCommands(program, { chalk, resolveVaultPath }) {
|
|
6
|
+
// === CHECKPOINT ===
|
|
7
|
+
program
|
|
8
|
+
.command('checkpoint')
|
|
9
|
+
.description('Quick state checkpoint for context death resilience')
|
|
10
|
+
.option('--working-on <text>', 'What you are currently working on')
|
|
11
|
+
.option('--focus <text>', 'Current focus area')
|
|
12
|
+
.option('--blocked <text>', 'What is blocking progress')
|
|
13
|
+
.option('--urgent', 'Trigger OpenClaw wake after checkpoint')
|
|
14
|
+
.option('-v, --vault <path>', 'Vault path')
|
|
15
|
+
.option('--json', 'Output as JSON')
|
|
16
|
+
.action(async (options) => {
|
|
17
|
+
try {
|
|
18
|
+
const { checkpoint } = await import('../dist/commands/checkpoint.js');
|
|
19
|
+
const data = await checkpoint({
|
|
20
|
+
vaultPath: resolveVaultPath(options.vault),
|
|
21
|
+
workingOn: options.workingOn,
|
|
22
|
+
focus: options.focus,
|
|
23
|
+
blocked: options.blocked,
|
|
24
|
+
urgent: options.urgent
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
if (options.json) {
|
|
28
|
+
console.log(JSON.stringify(data, null, 2));
|
|
29
|
+
} else {
|
|
30
|
+
console.log(chalk.green('✓ Checkpoint saved'));
|
|
31
|
+
console.log(chalk.dim(` Timestamp: ${data.timestamp}`));
|
|
32
|
+
if (data.workingOn) console.log(chalk.dim(` Working on: ${data.workingOn}`));
|
|
33
|
+
if (data.focus) console.log(chalk.dim(` Focus: ${data.focus}`));
|
|
34
|
+
if (data.blocked) console.log(chalk.dim(` Blocked: ${data.blocked}`));
|
|
35
|
+
if (data.urgent) console.log(chalk.dim(' Urgent: yes'));
|
|
36
|
+
}
|
|
37
|
+
} catch (err) {
|
|
38
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// === RECOVER ===
|
|
44
|
+
program
|
|
45
|
+
.command('recover')
|
|
46
|
+
.description('Check for context death and recover state')
|
|
47
|
+
.option('--clear', 'Clear the dirty death flag after recovery')
|
|
48
|
+
.option('--check', 'Check dirty death flag without clearing it')
|
|
49
|
+
.option('--list', 'List saved checkpoints (newest first)')
|
|
50
|
+
.option('--verbose', 'Show full checkpoint and handoff content')
|
|
51
|
+
.option('-v, --vault <path>', 'Vault path')
|
|
52
|
+
.option('--json', 'Output as JSON')
|
|
53
|
+
.action(async (options) => {
|
|
54
|
+
try {
|
|
55
|
+
if (options.check && options.list) {
|
|
56
|
+
throw new Error('--check and --list cannot be used together.');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const {
|
|
60
|
+
recover,
|
|
61
|
+
formatRecoveryInfo,
|
|
62
|
+
checkRecoveryStatus,
|
|
63
|
+
formatRecoveryCheckStatus,
|
|
64
|
+
listCheckpoints,
|
|
65
|
+
formatCheckpointList
|
|
66
|
+
} = await import('../dist/commands/recover.js');
|
|
67
|
+
const vaultPath = resolveVaultPath(options.vault);
|
|
68
|
+
|
|
69
|
+
if (options.check) {
|
|
70
|
+
const status = await checkRecoveryStatus(vaultPath);
|
|
71
|
+
if (options.json) {
|
|
72
|
+
console.log(JSON.stringify(status, null, 2));
|
|
73
|
+
} else {
|
|
74
|
+
console.log(formatRecoveryCheckStatus(status));
|
|
75
|
+
}
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (options.list) {
|
|
80
|
+
const checkpoints = listCheckpoints(vaultPath);
|
|
81
|
+
if (options.json) {
|
|
82
|
+
console.log(JSON.stringify(checkpoints, null, 2));
|
|
83
|
+
} else {
|
|
84
|
+
console.log(formatCheckpointList(checkpoints));
|
|
85
|
+
}
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const info = await recover(vaultPath, {
|
|
90
|
+
clearFlag: options.clear,
|
|
91
|
+
verbose: options.verbose
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
if (options.json) {
|
|
95
|
+
console.log(JSON.stringify(info, null, 2));
|
|
96
|
+
} else {
|
|
97
|
+
console.log(formatRecoveryInfo(info, { verbose: options.verbose }));
|
|
98
|
+
}
|
|
99
|
+
} catch (err) {
|
|
100
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// === STATUS ===
|
|
106
|
+
program
|
|
107
|
+
.command('status')
|
|
108
|
+
.description('Show vault health and status')
|
|
109
|
+
.option('-v, --vault <path>', 'Vault path')
|
|
110
|
+
.option('--json', 'Output as JSON')
|
|
111
|
+
.action(async (options) => {
|
|
112
|
+
try {
|
|
113
|
+
const { statusCommand } = await import('../dist/commands/status.js');
|
|
114
|
+
await statusCommand(resolveVaultPath(options.vault), { json: options.json });
|
|
115
|
+
} catch (err) {
|
|
116
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// === CLEAN-EXIT ===
|
|
122
|
+
program
|
|
123
|
+
.command('clean-exit')
|
|
124
|
+
.description('Mark session as cleanly exited (clears dirty death flag)')
|
|
125
|
+
.option('-v, --vault <path>', 'Vault path')
|
|
126
|
+
.action(async (options) => {
|
|
127
|
+
try {
|
|
128
|
+
const { cleanExit } = await import('../dist/commands/checkpoint.js');
|
|
129
|
+
await cleanExit(resolveVaultPath(options.vault));
|
|
130
|
+
console.log(chalk.green('✓ Clean exit recorded'));
|
|
131
|
+
} catch (err) {
|
|
132
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// === REPAIR-SESSION ===
|
|
138
|
+
program
|
|
139
|
+
.command('repair-session')
|
|
140
|
+
.description('Repair corrupted OpenClaw session transcripts')
|
|
141
|
+
.option('-s, --session <id>', 'Session ID (defaults to current main session)')
|
|
142
|
+
.option('-a, --agent <id>', 'Agent ID (defaults to configured agent)')
|
|
143
|
+
.option('--backup', 'Create backup before repair (default: true)', true)
|
|
144
|
+
.option('--no-backup', 'Skip backup creation')
|
|
145
|
+
.option('--dry-run', 'Show what would be repaired without writing')
|
|
146
|
+
.option('--list', 'List available sessions')
|
|
147
|
+
.option('--json', 'Output as JSON')
|
|
148
|
+
.action(async (options) => {
|
|
149
|
+
try {
|
|
150
|
+
const {
|
|
151
|
+
repairSessionCommand,
|
|
152
|
+
formatRepairResult,
|
|
153
|
+
listAgentSessions
|
|
154
|
+
} = await import('../dist/commands/repair-session.js');
|
|
155
|
+
|
|
156
|
+
if (options.list) {
|
|
157
|
+
console.log(listAgentSessions(options.agent));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const result = await repairSessionCommand({
|
|
162
|
+
sessionId: options.session,
|
|
163
|
+
agentId: options.agent,
|
|
164
|
+
backup: options.backup,
|
|
165
|
+
dryRun: options.dryRun
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
if (options.json) {
|
|
169
|
+
console.log(JSON.stringify(result, null, 2));
|
|
170
|
+
} else {
|
|
171
|
+
console.log(formatRepairResult(result, { dryRun: options.dryRun }));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (result.corruptedEntries.length > 0 && !result.repaired) {
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
} catch (err) {
|
|
178
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
}
|
|
@@ -1,81 +1,81 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { Command } from 'commander';
|
|
3
|
-
import { registerResilienceCommands } from './register-resilience-commands.js';
|
|
4
|
-
import { chalkStub } from './test-helpers/cli-command-fixtures.js';
|
|
5
|
-
|
|
6
|
-
const {
|
|
7
|
-
recoverMock,
|
|
8
|
-
formatRecoveryInfoMock,
|
|
9
|
-
checkRecoveryStatusMock,
|
|
10
|
-
formatRecoveryCheckStatusMock,
|
|
11
|
-
listCheckpointsMock,
|
|
12
|
-
formatCheckpointListMock
|
|
13
|
-
} = vi.hoisted(() => ({
|
|
14
|
-
recoverMock: vi.fn(),
|
|
15
|
-
formatRecoveryInfoMock: vi.fn(),
|
|
16
|
-
checkRecoveryStatusMock: vi.fn(),
|
|
17
|
-
formatRecoveryCheckStatusMock: vi.fn(),
|
|
18
|
-
listCheckpointsMock: vi.fn(),
|
|
19
|
-
formatCheckpointListMock: vi.fn()
|
|
20
|
-
}));
|
|
21
|
-
|
|
22
|
-
vi.mock('../dist/commands/recover.js', () => ({
|
|
23
|
-
recover: recoverMock,
|
|
24
|
-
formatRecoveryInfo: formatRecoveryInfoMock,
|
|
25
|
-
checkRecoveryStatus: checkRecoveryStatusMock,
|
|
26
|
-
formatRecoveryCheckStatus: formatRecoveryCheckStatusMock,
|
|
27
|
-
listCheckpoints: listCheckpointsMock,
|
|
28
|
-
formatCheckpointList: formatCheckpointListMock
|
|
29
|
-
}));
|
|
30
|
-
|
|
31
|
-
function buildProgram() {
|
|
32
|
-
const program = new Command();
|
|
33
|
-
registerResilienceCommands(program, {
|
|
34
|
-
chalk: chalkStub,
|
|
35
|
-
resolveVaultPath: (value) => value ?? '/vault'
|
|
36
|
-
});
|
|
37
|
-
return program;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
async function runCommand(args) {
|
|
41
|
-
const program = buildProgram();
|
|
42
|
-
await program.parseAsync(args, { from: 'user' });
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
beforeEach(() => {
|
|
46
|
-
vi.clearAllMocks();
|
|
47
|
-
recoverMock.mockResolvedValue({ died: false });
|
|
48
|
-
formatRecoveryInfoMock.mockReturnValue('recover');
|
|
49
|
-
checkRecoveryStatusMock.mockResolvedValue({ died: false, deathTime: null, checkpoint: null });
|
|
50
|
-
formatRecoveryCheckStatusMock.mockReturnValue('check');
|
|
51
|
-
listCheckpointsMock.mockReturnValue([]);
|
|
52
|
-
formatCheckpointListMock.mockReturnValue('list');
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
describe('register-resilience-commands', () => {
|
|
56
|
-
it('routes recover --check through check helpers', async () => {
|
|
57
|
-
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
58
|
-
try {
|
|
59
|
-
await runCommand(['recover', '--check']);
|
|
60
|
-
expect(checkRecoveryStatusMock).toHaveBeenCalledWith('/vault');
|
|
61
|
-
expect(formatRecoveryCheckStatusMock).toHaveBeenCalled();
|
|
62
|
-
expect(recoverMock).not.toHaveBeenCalled();
|
|
63
|
-
expect(logSpy).toHaveBeenCalledWith('check');
|
|
64
|
-
} finally {
|
|
65
|
-
logSpy.mockRestore();
|
|
66
|
-
}
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('routes recover --list through checkpoint listing helpers', async () => {
|
|
70
|
-
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
71
|
-
try {
|
|
72
|
-
await runCommand(['recover', '--list']);
|
|
73
|
-
expect(listCheckpointsMock).toHaveBeenCalledWith('/vault');
|
|
74
|
-
expect(formatCheckpointListMock).toHaveBeenCalled();
|
|
75
|
-
expect(recoverMock).not.toHaveBeenCalled();
|
|
76
|
-
expect(logSpy).toHaveBeenCalledWith('list');
|
|
77
|
-
} finally {
|
|
78
|
-
logSpy.mockRestore();
|
|
79
|
-
}
|
|
80
|
-
});
|
|
81
|
-
});
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { registerResilienceCommands } from './register-resilience-commands.js';
|
|
4
|
+
import { chalkStub } from './test-helpers/cli-command-fixtures.js';
|
|
5
|
+
|
|
6
|
+
const {
|
|
7
|
+
recoverMock,
|
|
8
|
+
formatRecoveryInfoMock,
|
|
9
|
+
checkRecoveryStatusMock,
|
|
10
|
+
formatRecoveryCheckStatusMock,
|
|
11
|
+
listCheckpointsMock,
|
|
12
|
+
formatCheckpointListMock
|
|
13
|
+
} = vi.hoisted(() => ({
|
|
14
|
+
recoverMock: vi.fn(),
|
|
15
|
+
formatRecoveryInfoMock: vi.fn(),
|
|
16
|
+
checkRecoveryStatusMock: vi.fn(),
|
|
17
|
+
formatRecoveryCheckStatusMock: vi.fn(),
|
|
18
|
+
listCheckpointsMock: vi.fn(),
|
|
19
|
+
formatCheckpointListMock: vi.fn()
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
vi.mock('../dist/commands/recover.js', () => ({
|
|
23
|
+
recover: recoverMock,
|
|
24
|
+
formatRecoveryInfo: formatRecoveryInfoMock,
|
|
25
|
+
checkRecoveryStatus: checkRecoveryStatusMock,
|
|
26
|
+
formatRecoveryCheckStatus: formatRecoveryCheckStatusMock,
|
|
27
|
+
listCheckpoints: listCheckpointsMock,
|
|
28
|
+
formatCheckpointList: formatCheckpointListMock
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
function buildProgram() {
|
|
32
|
+
const program = new Command();
|
|
33
|
+
registerResilienceCommands(program, {
|
|
34
|
+
chalk: chalkStub,
|
|
35
|
+
resolveVaultPath: (value) => value ?? '/vault'
|
|
36
|
+
});
|
|
37
|
+
return program;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function runCommand(args) {
|
|
41
|
+
const program = buildProgram();
|
|
42
|
+
await program.parseAsync(args, { from: 'user' });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
vi.clearAllMocks();
|
|
47
|
+
recoverMock.mockResolvedValue({ died: false });
|
|
48
|
+
formatRecoveryInfoMock.mockReturnValue('recover');
|
|
49
|
+
checkRecoveryStatusMock.mockResolvedValue({ died: false, deathTime: null, checkpoint: null });
|
|
50
|
+
formatRecoveryCheckStatusMock.mockReturnValue('check');
|
|
51
|
+
listCheckpointsMock.mockReturnValue([]);
|
|
52
|
+
formatCheckpointListMock.mockReturnValue('list');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('register-resilience-commands', () => {
|
|
56
|
+
it('routes recover --check through check helpers', async () => {
|
|
57
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
58
|
+
try {
|
|
59
|
+
await runCommand(['recover', '--check']);
|
|
60
|
+
expect(checkRecoveryStatusMock).toHaveBeenCalledWith('/vault');
|
|
61
|
+
expect(formatRecoveryCheckStatusMock).toHaveBeenCalled();
|
|
62
|
+
expect(recoverMock).not.toHaveBeenCalled();
|
|
63
|
+
expect(logSpy).toHaveBeenCalledWith('check');
|
|
64
|
+
} finally {
|
|
65
|
+
logSpy.mockRestore();
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('routes recover --list through checkpoint listing helpers', async () => {
|
|
70
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
71
|
+
try {
|
|
72
|
+
await runCommand(['recover', '--list']);
|
|
73
|
+
expect(listCheckpointsMock).toHaveBeenCalledWith('/vault');
|
|
74
|
+
expect(formatCheckpointListMock).toHaveBeenCalled();
|
|
75
|
+
expect(recoverMock).not.toHaveBeenCalled();
|
|
76
|
+
expect(logSpy).toHaveBeenCalledWith('list');
|
|
77
|
+
} finally {
|
|
78
|
+
logSpy.mockRestore();
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
});
|