clawvault 2.5.1 → 2.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 +1 -0
- package/bin/command-registration.test.js +1 -0
- package/bin/command-runtime.js +21 -5
- package/bin/command-runtime.test.js +52 -0
- package/bin/help-contract.test.js +12 -1
- package/bin/register-config-commands.js +2 -2
- package/bin/register-core-commands.js +9 -9
- package/bin/register-kanban-commands.js +5 -5
- package/bin/register-maintenance-commands.js +44 -10
- package/bin/register-project-commands.js +14 -14
- package/bin/register-project-commands.test.js +5 -0
- package/bin/register-query-commands.js +23 -18
- package/bin/register-query-commands.test.js +65 -0
- package/bin/register-session-lifecycle-commands.js +3 -3
- package/bin/register-tailscale-commands.js +1 -1
- package/bin/register-task-commands.js +5 -5
- package/bin/register-template-commands.js +1 -1
- package/bin/register-vault-operations-commands.js +7 -7
- package/dist/{chunk-G3OQJ2NQ.js → chunk-2YDBJS7M.js} +1 -1
- package/dist/chunk-3FP5BJ42.js +88 -0
- package/dist/{chunk-C3PF7WBA.js → chunk-4IV3R2F5.js} +2 -2
- package/dist/{chunk-7OHQFMJK.js → chunk-AY4PGUVL.js} +5 -4
- package/dist/chunk-BHO7WSAY.js +332 -0
- package/dist/{chunk-WIICLBNF.js → chunk-GFJ3LIIB.js} +1 -1
- package/dist/chunk-HRTPQQF2.js +541 -0
- package/dist/chunk-HWUNREDJ.js +33 -0
- package/dist/{chunk-6RQPD7X6.js → chunk-M25QVSJM.js} +4 -3
- package/dist/{chunk-6B3JWM7J.js → chunk-O7XHXF7F.js} +34 -7
- package/dist/chunk-PLZKZW4I.js +406 -0
- package/dist/{chunk-PAYUH64O.js → chunk-QVMXF7FY.js} +11 -1
- package/dist/{chunk-TMZMN7OS.js → chunk-S2IG7VNM.js} +24 -12
- package/dist/{chunk-LMCC5OC7.js → chunk-TPDH3JPP.js} +1 -1
- package/dist/cli/index.d.ts +5 -0
- package/dist/cli/index.js +31 -0
- package/dist/commands/canvas.js +3 -3
- package/dist/commands/compat.js +1 -1
- package/dist/commands/context.js +4 -4
- package/dist/commands/doctor.js +16 -309
- package/dist/commands/embed.d.ts +17 -0
- package/dist/commands/embed.js +10 -0
- package/dist/commands/migrate-observations.js +2 -2
- package/dist/commands/observe.d.ts +1 -0
- package/dist/commands/observe.js +7 -6
- package/dist/commands/rebuild.js +5 -5
- package/dist/commands/reflect.js +3 -3
- package/dist/commands/replay.js +7 -7
- package/dist/commands/setup.d.ts +1 -0
- package/dist/commands/setup.js +2 -2
- package/dist/commands/sleep.d.ts +2 -1
- package/dist/commands/sleep.js +15 -15
- package/dist/commands/status.d.ts +9 -1
- package/dist/commands/status.js +33 -8
- package/dist/commands/wake.d.ts +1 -1
- package/dist/commands/wake.js +6 -6
- package/dist/index.d.ts +82 -5
- package/dist/index.js +127 -105
- package/dist/{types-jjuYN2Xn.d.ts → types-C74wgGL1.d.ts} +2 -0
- package/hooks/clawvault/handler.js +11 -7
- package/hooks/clawvault/handler.test.js +2 -2
- package/package.json +2 -2
- package/dist/chunk-2RK2AG32.js +0 -743
- package/dist/{chunk-FW465EEA.js → chunk-VXEOHTSL.js} +3 -3
- package/dist/{chunk-KCCHROBR.js → chunk-YOSEUUNB.js} +4 -4
package/README.md
CHANGED
|
@@ -92,6 +92,7 @@ Auto-compress conversations into prioritized observations. Critical items route
|
|
|
92
92
|
```bash
|
|
93
93
|
clawvault observe --compress session.md # One-shot compression
|
|
94
94
|
clawvault observe --active # Incremental from transcripts
|
|
95
|
+
clawvault observe --cron # Cron-safe one-shot summary + exit code
|
|
95
96
|
```
|
|
96
97
|
|
|
97
98
|
### 🛡️ Context Death Recovery
|
package/bin/command-runtime.js
CHANGED
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
resolveVaultPath as resolveConfiguredVaultPath
|
|
9
9
|
} from '../dist/index.js';
|
|
10
10
|
|
|
11
|
+
const QMD_INDEX_ENV_VAR = 'CLAWVAULT_QMD_INDEX';
|
|
12
|
+
|
|
11
13
|
/**
|
|
12
14
|
* Validates that a path is within an allowed base directory.
|
|
13
15
|
* Prevents path traversal attacks.
|
|
@@ -29,15 +31,29 @@ export function validatePathWithinBase(inputPath, basePath) {
|
|
|
29
31
|
|
|
30
32
|
/**
|
|
31
33
|
* Sanitizes an argument that may contain a path to prevent injection.
|
|
32
|
-
* @param {
|
|
34
|
+
* @param {unknown} arg - The argument to sanitize
|
|
33
35
|
* @returns {string} The sanitized argument
|
|
34
36
|
*/
|
|
35
|
-
function sanitizeQmdArg(arg) {
|
|
37
|
+
export function sanitizeQmdArg(arg) {
|
|
38
|
+
const normalizedArg = String(arg);
|
|
36
39
|
// Reject arguments with null bytes (injection attempt)
|
|
37
|
-
if (
|
|
40
|
+
if (normalizedArg.includes('\0')) {
|
|
38
41
|
throw new Error('Invalid argument: contains null byte');
|
|
39
42
|
}
|
|
40
|
-
return
|
|
43
|
+
return normalizedArg;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function withQmdIndex(args) {
|
|
47
|
+
if (args.includes('--index')) {
|
|
48
|
+
return [...args];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const indexName = process.env[QMD_INDEX_ENV_VAR]?.trim();
|
|
52
|
+
if (!indexName) {
|
|
53
|
+
return [...args];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return ['--index', indexName, ...args];
|
|
41
57
|
}
|
|
42
58
|
|
|
43
59
|
export function resolveVaultPath(vaultPath) {
|
|
@@ -53,7 +69,7 @@ export async function getVault(vaultPath) {
|
|
|
53
69
|
export async function runQmd(args) {
|
|
54
70
|
return new Promise((resolve, reject) => {
|
|
55
71
|
// Sanitize all arguments before passing to spawn
|
|
56
|
-
const sanitizedArgs = args.map(sanitizeQmdArg);
|
|
72
|
+
const sanitizedArgs = withQmdIndex(args).map(sanitizeQmdArg);
|
|
57
73
|
const proc = spawn('qmd', sanitizedArgs, { stdio: 'inherit' });
|
|
58
74
|
proc.on('close', (code) => {
|
|
59
75
|
if (code === 0) resolve();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import path from 'path';
|
|
2
3
|
|
|
3
4
|
const {
|
|
4
5
|
spawnMock,
|
|
@@ -68,6 +69,40 @@ describe('command runtime helpers', () => {
|
|
|
68
69
|
await expect(runQmd(['update'])).rejects.toBeInstanceOf(QmdUnavailableError);
|
|
69
70
|
});
|
|
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
|
+
|
|
71
106
|
it('surfaces qmd non-zero exit codes as errors', async () => {
|
|
72
107
|
const { runQmd } = await loadRuntimeModule();
|
|
73
108
|
spawnMock.mockImplementation(() => {
|
|
@@ -86,6 +121,23 @@ describe('command runtime helpers', () => {
|
|
|
86
121
|
await expect(runQmd(['update'])).rejects.toThrow('qmd exited with code 2');
|
|
87
122
|
});
|
|
88
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
|
+
|
|
89
141
|
it('prints consistent qmd missing guidance', async () => {
|
|
90
142
|
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
91
143
|
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
@@ -7,22 +7,33 @@ describe('CLI help contract', () => {
|
|
|
7
7
|
expect(help).toContain('init');
|
|
8
8
|
expect(help).toContain('context');
|
|
9
9
|
expect(help).toContain('inject');
|
|
10
|
+
expect(help).toContain('doctor');
|
|
11
|
+
expect(help).toContain('embed');
|
|
10
12
|
expect(help).toContain('compat');
|
|
11
13
|
expect(help).toContain('graph');
|
|
12
14
|
expect(help).toContain('reflect');
|
|
13
15
|
expect(help).toContain('replay');
|
|
14
16
|
expect(help).toContain('repair-session');
|
|
17
|
+
expect(help).toContain('project');
|
|
15
18
|
expect(help).toContain('template');
|
|
16
19
|
expect(help).toContain('config');
|
|
17
20
|
expect(help).toContain('route');
|
|
18
21
|
});
|
|
19
22
|
|
|
20
|
-
it('documents context
|
|
23
|
+
it('documents context/compat/inject/project help details', () => {
|
|
21
24
|
const program = registerAllCommandModules();
|
|
22
25
|
const contextHelp = program.commands.find((command) => command.name() === 'context')?.helpInformation() ?? '';
|
|
23
26
|
const compatHelp = program.commands.find((command) => command.name() === 'compat')?.helpInformation() ?? '';
|
|
27
|
+
const injectHelp = program.commands.find((command) => command.name() === 'inject')?.helpInformation() ?? '';
|
|
28
|
+
const projectCommand = program.commands.find((command) => command.name() === 'project');
|
|
29
|
+
const projectListHelp = projectCommand?.commands.find((command) => command.name() === 'list')?.helpInformation() ?? '';
|
|
30
|
+
const projectBoardHelp = projectCommand?.commands.find((command) => command.name() === 'board')?.helpInformation() ?? '';
|
|
24
31
|
expect(contextHelp).toContain('--profile <profile>');
|
|
25
32
|
expect(contextHelp).toContain('auto');
|
|
26
33
|
expect(compatHelp).toContain('--strict');
|
|
34
|
+
expect(injectHelp).toContain('inject.maxResults');
|
|
35
|
+
expect(injectHelp).toContain('inject.scope');
|
|
36
|
+
expect(projectListHelp).toContain('archived projects are hidden');
|
|
37
|
+
expect(projectBoardHelp).toContain('default: status');
|
|
27
38
|
});
|
|
28
39
|
});
|
|
@@ -66,7 +66,7 @@ function normalizeConfigKey(key) {
|
|
|
66
66
|
export function registerConfigCommands(program, { chalk, resolveVaultPath }) {
|
|
67
67
|
const config = program
|
|
68
68
|
.command('config')
|
|
69
|
-
.description('Read and modify runtime
|
|
69
|
+
.description('Read and modify runtime configuration');
|
|
70
70
|
|
|
71
71
|
config
|
|
72
72
|
.command('get <key>')
|
|
@@ -135,7 +135,7 @@ export function registerConfigCommands(program, { chalk, resolveVaultPath }) {
|
|
|
135
135
|
config
|
|
136
136
|
.command('reset')
|
|
137
137
|
.description('Reset runtime config values to defaults')
|
|
138
|
-
.option('--confirm', 'Confirm reset')
|
|
138
|
+
.option('--confirm', 'Confirm reset (required)')
|
|
139
139
|
.option('-v, --vault <path>', 'Vault path')
|
|
140
140
|
.action(async (options) => {
|
|
141
141
|
try {
|
|
@@ -11,8 +11,8 @@ export function registerCoreCommands(
|
|
|
11
11
|
// === INIT ===
|
|
12
12
|
program
|
|
13
13
|
.command('init [path]')
|
|
14
|
-
.description('Initialize a new ClawVault')
|
|
15
|
-
.option('-n, --name <name>', 'Vault name')
|
|
14
|
+
.description('Initialize a new ClawVault vault')
|
|
15
|
+
.option('-n, --name <name>', 'Vault name (default: target directory name)')
|
|
16
16
|
.option('--qmd', 'Set up qmd semantic search collection')
|
|
17
17
|
.option('--qmd-collection <name>', 'qmd collection name (defaults to vault name)')
|
|
18
18
|
.option('--no-bases', 'Skip Obsidian Bases file generation')
|
|
@@ -20,7 +20,7 @@ export function registerCoreCommands(
|
|
|
20
20
|
.option('--no-graph', 'Skip initial graph build')
|
|
21
21
|
.option('--categories <list>', 'Comma-separated list of custom categories to create')
|
|
22
22
|
.option('--canvas', 'Generate a vault status canvas dashboard on init')
|
|
23
|
-
.option('--theme <style>', 'Graph color theme to apply (neural, minimal, none)', 'none')
|
|
23
|
+
.option('--theme <style>', 'Graph color theme to apply (neural, minimal, none) (default: none)', 'none')
|
|
24
24
|
.option('--minimal', 'Create minimal vault (memory categories only, no tasks/bases/graph)')
|
|
25
25
|
.action(async (vaultPath, options) => {
|
|
26
26
|
const targetPath = vaultPath || '.';
|
|
@@ -135,14 +135,14 @@ export function registerCoreCommands(
|
|
|
135
135
|
// === SETUP ===
|
|
136
136
|
program
|
|
137
137
|
.command('setup')
|
|
138
|
-
.description('Auto-discover and configure
|
|
138
|
+
.description('Auto-discover and configure an existing ClawVault vault')
|
|
139
139
|
.option('--graph-colors', 'Set up graph color scheme for Obsidian')
|
|
140
140
|
.option('--no-graph-colors', 'Skip graph color configuration')
|
|
141
141
|
.option('--bases', 'Generate Obsidian Bases views for task management')
|
|
142
142
|
.option('--no-bases', 'Skip Bases file generation')
|
|
143
143
|
.option('--canvas', 'Generate vault status canvas dashboard')
|
|
144
144
|
.option('--no-canvas', 'Skip canvas generation')
|
|
145
|
-
.option('--theme <style>', 'Graph color theme (neural, minimal, none)', 'neural')
|
|
145
|
+
.option('--theme <style>', 'Graph color theme (neural, minimal, none) (default: neural)', 'neural')
|
|
146
146
|
.option('--force', 'Overwrite existing configuration files')
|
|
147
147
|
.option('-v, --vault <path>', 'Vault path')
|
|
148
148
|
.action(async (options) => {
|
|
@@ -165,11 +165,11 @@ export function registerCoreCommands(
|
|
|
165
165
|
// === STORE ===
|
|
166
166
|
program
|
|
167
167
|
.command('store')
|
|
168
|
-
.description('Store a new memory')
|
|
168
|
+
.description('Store a new memory document')
|
|
169
169
|
.requiredOption('-c, --category <category>', 'Category (preferences, decisions, patterns, people, projects, goals, transcripts, inbox)')
|
|
170
170
|
.requiredOption('-t, --title <title>', 'Document title')
|
|
171
171
|
.option('--content <content>', 'Content body')
|
|
172
|
-
.option('-f, --file <file>', 'Read content from file')
|
|
172
|
+
.option('-f, --file <file>', 'Read content from file (validated against current working directory)')
|
|
173
173
|
.option('--stdin', 'Read content from stdin')
|
|
174
174
|
.option('--overwrite', 'Overwrite if exists')
|
|
175
175
|
.option('--no-index', 'Skip qmd index update (auto-updates by default)')
|
|
@@ -215,10 +215,10 @@ export function registerCoreCommands(
|
|
|
215
215
|
// === CAPTURE ===
|
|
216
216
|
program
|
|
217
217
|
.command('capture <note>')
|
|
218
|
-
.description('Quick
|
|
218
|
+
.description('Quick-capture a note to inbox')
|
|
219
219
|
.option('-t, --title <title>', 'Note title')
|
|
220
220
|
.option('-v, --vault <path>', 'Vault path')
|
|
221
|
-
.option('--no-index', 'Skip qmd index update')
|
|
221
|
+
.option('--no-index', 'Skip qmd index update (auto-updates by default)')
|
|
222
222
|
.action(async (note, options) => {
|
|
223
223
|
try {
|
|
224
224
|
const vault = await getVault(options.vault);
|
|
@@ -8,17 +8,17 @@ export function registerKanbanCommands(
|
|
|
8
8
|
) {
|
|
9
9
|
const kanbanCmd = program
|
|
10
10
|
.command('kanban')
|
|
11
|
-
.description('
|
|
11
|
+
.description('Manage Obsidian Kanban sync for task frontmatter');
|
|
12
12
|
|
|
13
13
|
kanbanCmd
|
|
14
14
|
.command('sync')
|
|
15
15
|
.description('Generate and sync an Obsidian Kanban board from tasks')
|
|
16
|
-
.option('-v, --vault <path>', 'Vault path')
|
|
16
|
+
.option('-v, --vault <path>', 'Vault path (default: find nearest)')
|
|
17
17
|
.option('--output <path>', 'Board markdown path (default: Board.md)')
|
|
18
|
-
.option('--group-by <field>', 'Grouping field (status, priority, project, owner)')
|
|
18
|
+
.option('--group-by <field>', 'Grouping field (status, priority, project, owner) (default: status)')
|
|
19
19
|
.option('--filter-project <project>', 'Only include tasks from a project')
|
|
20
20
|
.option('--filter-owner <owner>', 'Only include tasks for an owner')
|
|
21
|
-
.option('--include-done', 'Include done tasks')
|
|
21
|
+
.option('--include-done', 'Include done tasks (default: hidden)')
|
|
22
22
|
.action(async (options) => {
|
|
23
23
|
try {
|
|
24
24
|
const vaultPath = resolveVaultPath(options.vault);
|
|
@@ -39,7 +39,7 @@ export function registerKanbanCommands(
|
|
|
39
39
|
kanbanCmd
|
|
40
40
|
.command('import')
|
|
41
41
|
.description('Import lane state from an Obsidian Kanban board into tasks')
|
|
42
|
-
.option('-v, --vault <path>', 'Vault path')
|
|
42
|
+
.option('-v, --vault <path>', 'Vault path (default: find nearest)')
|
|
43
43
|
.option('--output <path>', 'Board markdown path (default: Board.md)')
|
|
44
44
|
.action(async (options) => {
|
|
45
45
|
try {
|
|
@@ -7,27 +7,42 @@ export function registerMaintenanceCommands(program, { chalk }) {
|
|
|
7
7
|
// === DOCTOR (health check) ===
|
|
8
8
|
program
|
|
9
9
|
.command('doctor')
|
|
10
|
-
.description('
|
|
10
|
+
.description('Diagnose vault health and optionally apply fixes')
|
|
11
11
|
.option('-v, --vault <path>', 'Vault path')
|
|
12
|
+
.option('--fix', 'Apply safe auto-fixes for qmd index, embeddings, and dead collections')
|
|
13
|
+
.option('--json', 'Output machine-readable JSON')
|
|
12
14
|
.action(async (options) => {
|
|
13
15
|
try {
|
|
14
16
|
const { doctor } = await import('../dist/commands/doctor.js');
|
|
15
|
-
const report = await doctor(
|
|
17
|
+
const report = await doctor({
|
|
18
|
+
vaultPath: options.vault,
|
|
19
|
+
fix: options.fix
|
|
20
|
+
});
|
|
16
21
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
console.log();
|
|
22
|
+
if (options.json) {
|
|
23
|
+
console.log(JSON.stringify(report, null, 2));
|
|
24
|
+
return;
|
|
21
25
|
}
|
|
22
26
|
|
|
27
|
+
console.log(chalk.cyan('\n🩺 ClawVault Health Check\n'));
|
|
28
|
+
console.log(chalk.dim(`Vault: ${report.vaultPath}`));
|
|
29
|
+
console.log(chalk.dim(`qmd collection: ${report.qmdCollection}`));
|
|
30
|
+
console.log(chalk.dim(`qmd root: ${report.qmdRoot}`));
|
|
31
|
+
console.log();
|
|
32
|
+
|
|
23
33
|
for (const check of report.checks) {
|
|
24
34
|
const prefix = check.status === 'ok'
|
|
25
35
|
? chalk.green('✓')
|
|
26
36
|
: check.status === 'warn'
|
|
27
37
|
? chalk.yellow('⚠')
|
|
28
38
|
: chalk.red('✗');
|
|
29
|
-
const
|
|
30
|
-
|
|
39
|
+
const line = `${check.label}: ${check.detail}`;
|
|
40
|
+
const renderedLine = check.status === 'ok'
|
|
41
|
+
? chalk.green(line)
|
|
42
|
+
: check.status === 'warn'
|
|
43
|
+
? chalk.yellow(line)
|
|
44
|
+
: chalk.red(line);
|
|
45
|
+
console.log(`${prefix} ${renderedLine}`);
|
|
31
46
|
if (check.hint) {
|
|
32
47
|
console.log(chalk.dim(` ${check.hint}`));
|
|
33
48
|
}
|
|
@@ -38,7 +53,9 @@ export function registerMaintenanceCommands(program, { chalk }) {
|
|
|
38
53
|
if (issues === 0) {
|
|
39
54
|
console.log(chalk.green('✅ ClawVault is healthy!\n'));
|
|
40
55
|
} else {
|
|
41
|
-
|
|
56
|
+
const summary = `${report.errors} error(s), ${report.warnings} warning(s)`;
|
|
57
|
+
const colorized = report.errors > 0 ? chalk.red(summary) : chalk.yellow(summary);
|
|
58
|
+
console.log(`${report.errors > 0 ? '✗' : '⚠'} ${colorized}\n`);
|
|
42
59
|
}
|
|
43
60
|
} catch (err) {
|
|
44
61
|
console.error(chalk.red(`Error: ${err.message}`));
|
|
@@ -46,6 +63,23 @@ export function registerMaintenanceCommands(program, { chalk }) {
|
|
|
46
63
|
}
|
|
47
64
|
});
|
|
48
65
|
|
|
66
|
+
// === EMBED ===
|
|
67
|
+
program
|
|
68
|
+
.command('embed')
|
|
69
|
+
.description('Run qmd embedding for pending vault documents')
|
|
70
|
+
.option('-v, --vault <path>', 'Vault path')
|
|
71
|
+
.action(async (options) => {
|
|
72
|
+
try {
|
|
73
|
+
const { embedCommand } = await import('../dist/commands/embed.js');
|
|
74
|
+
await embedCommand({
|
|
75
|
+
vaultPath: options.vault
|
|
76
|
+
});
|
|
77
|
+
} catch (err) {
|
|
78
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
49
83
|
// === COMPAT (OpenClaw compatibility) ===
|
|
50
84
|
program
|
|
51
85
|
.command('compat')
|
|
@@ -160,7 +194,7 @@ export function registerMaintenanceCommands(program, { chalk }) {
|
|
|
160
194
|
program
|
|
161
195
|
.command('archive')
|
|
162
196
|
.description('Archive old observations into ledger/archive')
|
|
163
|
-
.option('--older-than <days>', 'Archive observations older than this many days', '14')
|
|
197
|
+
.option('--older-than <days>', 'Archive observations older than this many days (default: 14)', '14')
|
|
164
198
|
.option('--dry-run', 'Show archive candidates without writing')
|
|
165
199
|
.option('-v, --vault <path>', 'Vault path')
|
|
166
200
|
.action(async (options) => {
|
|
@@ -18,14 +18,14 @@ export function registerProjectCommands(
|
|
|
18
18
|
) {
|
|
19
19
|
const projectCmd = program
|
|
20
20
|
.command('project')
|
|
21
|
-
.description('
|
|
21
|
+
.description('Manage projects and project boards');
|
|
22
22
|
|
|
23
23
|
projectCmd
|
|
24
24
|
.command('add <title>')
|
|
25
25
|
.description('Add a new project')
|
|
26
|
-
.option('-v, --vault <path>', 'Vault path')
|
|
26
|
+
.option('-v, --vault <path>', 'Vault path (default: find nearest)')
|
|
27
27
|
.option('--owner <owner>', 'Project owner')
|
|
28
|
-
.option('--status <status>', 'Project status (active, paused, completed, archived)')
|
|
28
|
+
.option('--status <status>', 'Project status (active, paused, completed, archived) (default: active)')
|
|
29
29
|
.option('--team <team>', 'Comma-separated team members')
|
|
30
30
|
.option('--client <client>', 'Client name')
|
|
31
31
|
.option('--tags <tags>', 'Comma-separated tags')
|
|
@@ -60,7 +60,7 @@ export function registerProjectCommands(
|
|
|
60
60
|
projectCmd
|
|
61
61
|
.command('update <slug>')
|
|
62
62
|
.description('Update a project')
|
|
63
|
-
.option('-v, --vault <path>', 'Vault path')
|
|
63
|
+
.option('-v, --vault <path>', 'Vault path (default: find nearest)')
|
|
64
64
|
.option('--status <status>', 'Project status (active, paused, completed, archived)')
|
|
65
65
|
.option('--owner <owner>', 'Project owner')
|
|
66
66
|
.option('--team <team>', 'Comma-separated team members')
|
|
@@ -97,7 +97,7 @@ export function registerProjectCommands(
|
|
|
97
97
|
projectCmd
|
|
98
98
|
.command('archive <slug>')
|
|
99
99
|
.description('Archive a project')
|
|
100
|
-
.option('-v, --vault <path>', 'Vault path')
|
|
100
|
+
.option('-v, --vault <path>', 'Vault path (default: find nearest)')
|
|
101
101
|
.option('--reason <reason>', 'Reason for archiving')
|
|
102
102
|
.action(async (slug, options) => {
|
|
103
103
|
try {
|
|
@@ -117,9 +117,9 @@ export function registerProjectCommands(
|
|
|
117
117
|
|
|
118
118
|
projectCmd
|
|
119
119
|
.command('list')
|
|
120
|
-
.description('List projects')
|
|
121
|
-
.option('-v, --vault <path>', 'Vault path')
|
|
122
|
-
.option('--status <status>', 'Filter by status')
|
|
120
|
+
.description('List projects (archived projects are hidden unless --status archived)')
|
|
121
|
+
.option('-v, --vault <path>', 'Vault path (default: find nearest)')
|
|
122
|
+
.option('--status <status>', 'Filter by status (active, paused, completed, archived)')
|
|
123
123
|
.option('--owner <owner>', 'Filter by owner')
|
|
124
124
|
.option('--client <client>', 'Filter by client')
|
|
125
125
|
.option('--tag <tag>', 'Filter by tag')
|
|
@@ -146,7 +146,7 @@ export function registerProjectCommands(
|
|
|
146
146
|
projectCmd
|
|
147
147
|
.command('show <slug>')
|
|
148
148
|
.description('Show project details')
|
|
149
|
-
.option('-v, --vault <path>', 'Vault path')
|
|
149
|
+
.option('-v, --vault <path>', 'Vault path (default: find nearest)')
|
|
150
150
|
.option('--json', 'Output as JSON')
|
|
151
151
|
.action(async (slug, options) => {
|
|
152
152
|
try {
|
|
@@ -166,8 +166,8 @@ export function registerProjectCommands(
|
|
|
166
166
|
|
|
167
167
|
projectCmd
|
|
168
168
|
.command('tasks <slug>')
|
|
169
|
-
.description('List tasks
|
|
170
|
-
.option('-v, --vault <path>', 'Vault path')
|
|
169
|
+
.description('List tasks linked to a project')
|
|
170
|
+
.option('-v, --vault <path>', 'Vault path (default: find nearest)')
|
|
171
171
|
.option('--json', 'Output as JSON')
|
|
172
172
|
.action(async (slug, options) => {
|
|
173
173
|
try {
|
|
@@ -187,10 +187,10 @@ export function registerProjectCommands(
|
|
|
187
187
|
|
|
188
188
|
projectCmd
|
|
189
189
|
.command('board')
|
|
190
|
-
.description('Generate project kanban board')
|
|
191
|
-
.option('-v, --vault <path>', 'Vault path')
|
|
190
|
+
.description('Generate and sync the project kanban board')
|
|
191
|
+
.option('-v, --vault <path>', 'Vault path (default: find nearest)')
|
|
192
192
|
.option('--output <path>', 'Board markdown path (default: Projects-Board.md)')
|
|
193
|
-
.option('--group-by <field>', 'Grouping field (status, owner, client)')
|
|
193
|
+
.option('--group-by <field>', 'Grouping field (status, owner, client) (default: status)')
|
|
194
194
|
.action(async (options) => {
|
|
195
195
|
try {
|
|
196
196
|
const vaultPath = resolveVaultPath(options.vault);
|
|
@@ -76,6 +76,11 @@ describe('register-project-commands', () => {
|
|
|
76
76
|
'--output <path>',
|
|
77
77
|
'--group-by <field>'
|
|
78
78
|
]));
|
|
79
|
+
|
|
80
|
+
expect(listCommand?.description()).toContain('archived projects are hidden');
|
|
81
|
+
|
|
82
|
+
const boardGroupByOption = boardCommand?.options.find((option) => option.flags === '--group-by <field>');
|
|
83
|
+
expect(boardGroupByOption?.description).toContain('default: status');
|
|
79
84
|
});
|
|
80
85
|
|
|
81
86
|
it('dispatches project subcommands to project command handler', async () => {
|
|
@@ -16,7 +16,7 @@ export function registerQueryCommands(
|
|
|
16
16
|
program
|
|
17
17
|
.command('search <query>')
|
|
18
18
|
.description('Search the vault via qmd (BM25)')
|
|
19
|
-
.option('-n, --limit <n>', 'Max results', '10')
|
|
19
|
+
.option('-n, --limit <n>', 'Max results (default: 10)', '10')
|
|
20
20
|
.option('-c, --category <category>', 'Filter by category')
|
|
21
21
|
.option('--tags <tags>', 'Filter by tags (comma-separated)')
|
|
22
22
|
.option('--recent', 'Boost recent documents')
|
|
@@ -71,7 +71,7 @@ export function registerQueryCommands(
|
|
|
71
71
|
program
|
|
72
72
|
.command('vsearch <query>')
|
|
73
73
|
.description('Semantic search via qmd (requires qmd installed)')
|
|
74
|
-
.option('-n, --limit <n>', 'Max results', '5')
|
|
74
|
+
.option('-n, --limit <n>', 'Max results (default: 5)', '5')
|
|
75
75
|
.option('-c, --category <category>', 'Filter by category')
|
|
76
76
|
.option('--tags <tags>', 'Filter by tags (comma-separated)')
|
|
77
77
|
.option('--recent', 'Boost recent documents')
|
|
@@ -126,13 +126,13 @@ export function registerQueryCommands(
|
|
|
126
126
|
program
|
|
127
127
|
.command('context <task>')
|
|
128
128
|
.description('Generate task-relevant context for prompt injection')
|
|
129
|
-
.option('-n, --limit <n>', 'Max results', '5')
|
|
130
|
-
.option('--format <format>', 'Output format (markdown|json)', 'markdown')
|
|
129
|
+
.option('-n, --limit <n>', 'Max results (default: 5)', '5')
|
|
130
|
+
.option('--format <format>', 'Output format (markdown|json) (default: markdown)', 'markdown')
|
|
131
131
|
.option('--recent', 'Boost recent documents (enabled by default)', true)
|
|
132
|
-
.option('--include-observations', 'Include observation memories in output', true)
|
|
132
|
+
.option('--include-observations', 'Include observation memories in output (enabled by default)', true)
|
|
133
133
|
.option('--budget <number>', 'Optional token budget for assembled context')
|
|
134
|
-
.option('--profile <profile>', 'Context profile (default|planning|incident|handoff|auto)', 'default')
|
|
135
|
-
.option('--max-hops <n>', 'Maximum graph expansion hops', '2')
|
|
134
|
+
.option('--profile <profile>', 'Context profile (default|planning|incident|handoff|auto) (default: default)', 'default')
|
|
135
|
+
.option('--max-hops <n>', 'Maximum graph expansion hops (default: 2)', '2')
|
|
136
136
|
.option('-v, --vault <path>', 'Vault path')
|
|
137
137
|
.action(async (task, options) => {
|
|
138
138
|
try {
|
|
@@ -171,12 +171,12 @@ export function registerQueryCommands(
|
|
|
171
171
|
// === INJECT ===
|
|
172
172
|
program
|
|
173
173
|
.command('inject <message>')
|
|
174
|
-
.description('Inject relevant rules, decisions, and preferences
|
|
175
|
-
.option('-n, --max-results <n>', 'Maximum injected items')
|
|
176
|
-
.option('--scope <scope>', 'Comma-separated scope filter override')
|
|
177
|
-
.option('--enable-llm', 'Enable
|
|
178
|
-
.option('--disable-llm', 'Disable
|
|
179
|
-
.option('--format <format>', 'Output format (markdown|json)', 'markdown')
|
|
174
|
+
.description('Inject relevant rules, decisions, and preferences into prompt context')
|
|
175
|
+
.option('-n, --max-results <n>', 'Maximum injected items (default: config inject.maxResults, fallback 8)')
|
|
176
|
+
.option('--scope <scope>', 'Comma-separated scope filter override (default: config inject.scope, fallback global)')
|
|
177
|
+
.option('--enable-llm', 'Enable LLM fuzzy intent matching (overrides config inject.useLlm)')
|
|
178
|
+
.option('--disable-llm', 'Disable LLM fuzzy intent matching (overrides config inject.useLlm)')
|
|
179
|
+
.option('--format <format>', 'Output format (markdown|json) (default: markdown)', 'markdown')
|
|
180
180
|
.option('--model <model>', 'Override LLM model when fuzzy matching is enabled')
|
|
181
181
|
.option('-v, --vault <path>', 'Vault path')
|
|
182
182
|
.action(async (message, options) => {
|
|
@@ -214,13 +214,16 @@ export function registerQueryCommands(
|
|
|
214
214
|
.description('Observe session files and build observational memory')
|
|
215
215
|
.option('--watch <path>', 'Watch session file or directory')
|
|
216
216
|
.option('--active', 'Observe active OpenClaw sessions incrementally')
|
|
217
|
+
.option('--cron', 'Run one-shot active observation for cron hooks')
|
|
217
218
|
.option('--agent <id>', 'OpenClaw agent ID (default: OPENCLAW_AGENT_ID or clawdious)')
|
|
218
219
|
.option('--min-new <bytes>', 'Override minimum new-content threshold in bytes')
|
|
219
220
|
.option('--sessions-dir <path>', 'Override OpenClaw sessions directory')
|
|
220
221
|
.option('--dry-run', 'Show active observation candidates without compressing')
|
|
221
|
-
.option('--threshold <n>', 'Compression token threshold', '30000')
|
|
222
|
-
.option('--reflect-threshold <n>', 'Reflection token threshold', '40000')
|
|
222
|
+
.option('--threshold <n>', 'Compression token threshold (default: 30000)', '30000')
|
|
223
|
+
.option('--reflect-threshold <n>', 'Reflection token threshold (default: 40000)', '40000')
|
|
223
224
|
.option('--model <model>', 'LLM model override')
|
|
225
|
+
.option('--extract-tasks', 'Extract task-like observations into backlog (enabled by default)', true)
|
|
226
|
+
.option('--no-extract-tasks', 'Disable task extraction from observations')
|
|
224
227
|
.option('--compress <file>', 'One-shot compression for a conversation file')
|
|
225
228
|
.option('--daemon', 'Run in detached background mode')
|
|
226
229
|
.option('-v, --vault <path>', 'Vault path')
|
|
@@ -245,6 +248,7 @@ export function registerQueryCommands(
|
|
|
245
248
|
await observeCommand({
|
|
246
249
|
watch: options.watch,
|
|
247
250
|
active: options.active,
|
|
251
|
+
cron: options.cron,
|
|
248
252
|
agent: options.agent,
|
|
249
253
|
minNew,
|
|
250
254
|
sessionsDir: options.sessionsDir,
|
|
@@ -252,6 +256,7 @@ export function registerQueryCommands(
|
|
|
252
256
|
threshold,
|
|
253
257
|
reflectThreshold,
|
|
254
258
|
model: options.model,
|
|
259
|
+
extractTasks: options.extractTasks,
|
|
255
260
|
compress: options.compress,
|
|
256
261
|
daemon: options.daemon,
|
|
257
262
|
vaultPath: resolveVaultPath(options.vault)
|
|
@@ -266,7 +271,7 @@ export function registerQueryCommands(
|
|
|
266
271
|
program
|
|
267
272
|
.command('reflect')
|
|
268
273
|
.description('Promote stable observations into weekly reflections')
|
|
269
|
-
.option('--days <n>', 'Observation window in days', '14')
|
|
274
|
+
.option('--days <n>', 'Observation window in days (default: 14)', '14')
|
|
270
275
|
.option('--dry-run', 'Show reflection output candidates without writing')
|
|
271
276
|
.option('-v, --vault <path>', 'Vault path')
|
|
272
277
|
.action(async (options) => {
|
|
@@ -291,8 +296,8 @@ export function registerQueryCommands(
|
|
|
291
296
|
program
|
|
292
297
|
.command('session-recap <sessionKey>')
|
|
293
298
|
.description('Generate recap from a specific OpenClaw session transcript')
|
|
294
|
-
.option('-n, --limit <n>', 'Number of messages to include', '15')
|
|
295
|
-
.option('--format <format>', 'Output format (markdown|json)', 'markdown')
|
|
299
|
+
.option('-n, --limit <n>', 'Number of messages to include (default: 15)', '15')
|
|
300
|
+
.option('--format <format>', 'Output format (markdown|json) (default: markdown)', 'markdown')
|
|
296
301
|
.option('-a, --agent <id>', 'Agent ID (default: OPENCLAW_AGENT_ID or clawdious)')
|
|
297
302
|
.action(async (sessionKey, options) => {
|
|
298
303
|
try {
|