clawvault 2.5.2 → 2.5.4
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-3FP5BJ42.js → chunk-4QYGFWRM.js} +1 -1
- package/dist/{chunk-M25QVSJM.js → chunk-AXKYDCNN.js} +1 -1
- package/dist/{chunk-CLE2HHNT.js → chunk-IVRIKYFE.js} +18 -11
- package/dist/{chunk-HRTPQQF2.js → chunk-IZEY5S74.js} +1 -1
- package/dist/{chunk-HWUNREDJ.js → chunk-JDLOL2PL.js} +4 -4
- package/dist/{chunk-AY4PGUVL.js → chunk-KL4NAOMO.js} +1 -1
- package/dist/{chunk-O7XHXF7F.js → chunk-MAKNAHAW.js} +4 -4
- package/dist/{chunk-PLZKZW4I.js → chunk-OSMS7QIG.js} +1 -1
- package/dist/{chunk-NZ4ZZNSR.js → chunk-THRJVD4L.js} +1 -1
- package/dist/{chunk-4GBPTBFJ.js → chunk-TIGW564L.js} +1 -1
- package/dist/{chunk-BHO7WSAY.js → chunk-W2HNZC22.js} +3 -3
- package/dist/{chunk-GFJ3LIIB.js → chunk-XAVB4GB4.js} +1 -1
- package/dist/cli/index.js +10 -10
- package/dist/commands/context.js +3 -3
- package/dist/commands/doctor.js +4 -4
- package/dist/commands/embed.js +2 -2
- package/dist/commands/observe.js +2 -2
- package/dist/commands/setup.js +2 -2
- package/dist/commands/sleep.js +2 -2
- package/dist/commands/status.js +3 -3
- package/dist/commands/tailscale.js +3 -3
- package/dist/commands/wake.js +2 -2
- package/dist/index.js +12 -12
- package/dist/lib/tailscale.js +2 -2
- package/dist/lib/webdav.js +1 -1
- 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,166 +1,166 @@
|
|
|
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', 'capture']));
|
|
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', '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
|
-
'embed',
|
|
125
|
-
'compat',
|
|
126
|
-
'graph',
|
|
127
|
-
'entities',
|
|
128
|
-
'link',
|
|
129
|
-
'rebuild',
|
|
130
|
-
'archive',
|
|
131
|
-
'migrate-observations',
|
|
132
|
-
'replay',
|
|
133
|
-
'sync-bd',
|
|
134
|
-
'checkpoint',
|
|
135
|
-
'recover',
|
|
136
|
-
'status',
|
|
137
|
-
'clean-exit',
|
|
138
|
-
'repair-session',
|
|
139
|
-
'wake',
|
|
140
|
-
'sleep',
|
|
141
|
-
'handoff',
|
|
142
|
-
'recap',
|
|
143
|
-
'template',
|
|
144
|
-
'config',
|
|
145
|
-
'route'
|
|
146
|
-
]));
|
|
147
|
-
|
|
148
|
-
const templateCommand = program.commands.find((command) => command.name() === 'template');
|
|
149
|
-
const templateSubcommands = templateCommand?.commands.map((command) => command.name()) ?? [];
|
|
150
|
-
expect(templateSubcommands).toEqual(expect.arrayContaining(['list', 'create', 'add']));
|
|
151
|
-
|
|
152
|
-
const compatCommand = program.commands.find((command) => command.name() === 'compat');
|
|
153
|
-
const strictOption = compatCommand?.options.find((option) => option.flags.includes('--strict'));
|
|
154
|
-
const baseDirOption = compatCommand?.options.find((option) => option.flags.includes('--base-dir <path>'));
|
|
155
|
-
expect(strictOption).toBeTruthy();
|
|
156
|
-
expect(baseDirOption).toBeTruthy();
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
it('keeps top-level command names unique when modules are combined', () => {
|
|
160
|
-
const program = registerAllCommandModules(new Command());
|
|
161
|
-
|
|
162
|
-
const names = program.commands.map((command) => command.name());
|
|
163
|
-
const unique = new Set(names);
|
|
164
|
-
expect(unique.size).toBe(names.length);
|
|
165
|
-
});
|
|
166
|
-
});
|
|
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', 'capture']));
|
|
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', '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
|
+
'embed',
|
|
125
|
+
'compat',
|
|
126
|
+
'graph',
|
|
127
|
+
'entities',
|
|
128
|
+
'link',
|
|
129
|
+
'rebuild',
|
|
130
|
+
'archive',
|
|
131
|
+
'migrate-observations',
|
|
132
|
+
'replay',
|
|
133
|
+
'sync-bd',
|
|
134
|
+
'checkpoint',
|
|
135
|
+
'recover',
|
|
136
|
+
'status',
|
|
137
|
+
'clean-exit',
|
|
138
|
+
'repair-session',
|
|
139
|
+
'wake',
|
|
140
|
+
'sleep',
|
|
141
|
+
'handoff',
|
|
142
|
+
'recap',
|
|
143
|
+
'template',
|
|
144
|
+
'config',
|
|
145
|
+
'route'
|
|
146
|
+
]));
|
|
147
|
+
|
|
148
|
+
const templateCommand = program.commands.find((command) => command.name() === 'template');
|
|
149
|
+
const templateSubcommands = templateCommand?.commands.map((command) => command.name()) ?? [];
|
|
150
|
+
expect(templateSubcommands).toEqual(expect.arrayContaining(['list', 'create', 'add']));
|
|
151
|
+
|
|
152
|
+
const compatCommand = program.commands.find((command) => command.name() === 'compat');
|
|
153
|
+
const strictOption = compatCommand?.options.find((option) => option.flags.includes('--strict'));
|
|
154
|
+
const baseDirOption = compatCommand?.options.find((option) => option.flags.includes('--base-dir <path>'));
|
|
155
|
+
expect(strictOption).toBeTruthy();
|
|
156
|
+
expect(baseDirOption).toBeTruthy();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('keeps top-level command names unique when modules are combined', () => {
|
|
160
|
+
const program = registerAllCommandModules(new Command());
|
|
161
|
+
|
|
162
|
+
const names = program.commands.map((command) => command.name());
|
|
163
|
+
const unique = new Set(names);
|
|
164
|
+
expect(unique.size).toBe(names.length);
|
|
165
|
+
});
|
|
166
|
+
});
|
package/bin/command-runtime.js
CHANGED
|
@@ -1,93 +1,93 @@
|
|
|
1
|
-
import { spawn } from 'child_process';
|
|
2
|
-
import chalk from 'chalk';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import {
|
|
5
|
-
ClawVault,
|
|
6
|
-
QmdUnavailableError,
|
|
7
|
-
QMD_INSTALL_COMMAND,
|
|
8
|
-
resolveVaultPath as resolveConfiguredVaultPath
|
|
9
|
-
} from '../dist/index.js';
|
|
10
|
-
|
|
11
|
-
const QMD_INDEX_ENV_VAR = 'CLAWVAULT_QMD_INDEX';
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Validates that a path is within an allowed base directory.
|
|
15
|
-
* Prevents path traversal attacks.
|
|
16
|
-
* @param {string} inputPath - The path to validate
|
|
17
|
-
* @param {string} basePath - The allowed base directory
|
|
18
|
-
* @returns {string} The resolved, validated path
|
|
19
|
-
* @throws {Error} If the path escapes the base directory
|
|
20
|
-
*/
|
|
21
|
-
export function validatePathWithinBase(inputPath, basePath) {
|
|
22
|
-
const resolvedBase = path.resolve(basePath);
|
|
23
|
-
const resolvedPath = path.resolve(basePath, inputPath);
|
|
24
|
-
|
|
25
|
-
if (!resolvedPath.startsWith(resolvedBase + path.sep) && resolvedPath !== resolvedBase) {
|
|
26
|
-
throw new Error(`Path traversal detected: ${inputPath} escapes ${basePath}`);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return resolvedPath;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Sanitizes an argument that may contain a path to prevent injection.
|
|
34
|
-
* @param {unknown} arg - The argument to sanitize
|
|
35
|
-
* @returns {string} The sanitized argument
|
|
36
|
-
*/
|
|
37
|
-
export function sanitizeQmdArg(arg) {
|
|
38
|
-
const normalizedArg = String(arg);
|
|
39
|
-
// Reject arguments with null bytes (injection attempt)
|
|
40
|
-
if (normalizedArg.includes('\0')) {
|
|
41
|
-
throw new Error('Invalid argument: contains null byte');
|
|
42
|
-
}
|
|
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];
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function resolveVaultPath(vaultPath) {
|
|
60
|
-
return resolveConfiguredVaultPath({ explicitPath: vaultPath });
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export async function getVault(vaultPath) {
|
|
64
|
-
const vault = new ClawVault(resolveVaultPath(vaultPath));
|
|
65
|
-
await vault.load();
|
|
66
|
-
return vault;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export async function runQmd(args) {
|
|
70
|
-
return new Promise((resolve, reject) => {
|
|
71
|
-
// Sanitize all arguments before passing to spawn
|
|
72
|
-
const sanitizedArgs = withQmdIndex(args).map(sanitizeQmdArg);
|
|
73
|
-
const proc = spawn('qmd', sanitizedArgs, { stdio: 'inherit' });
|
|
74
|
-
proc.on('close', (code) => {
|
|
75
|
-
if (code === 0) resolve();
|
|
76
|
-
else reject(new Error(`qmd exited with code ${code}`));
|
|
77
|
-
});
|
|
78
|
-
proc.on('error', (err) => {
|
|
79
|
-
if (err?.code === 'ENOENT') {
|
|
80
|
-
reject(new QmdUnavailableError());
|
|
81
|
-
} else {
|
|
82
|
-
reject(err);
|
|
83
|
-
}
|
|
84
|
-
});
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export function printQmdMissing() {
|
|
89
|
-
console.error(chalk.red('Error: ClawVault requires qmd.'));
|
|
90
|
-
console.log(chalk.dim(`Install: ${QMD_INSTALL_COMMAND}`));
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export { QmdUnavailableError };
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import {
|
|
5
|
+
ClawVault,
|
|
6
|
+
QmdUnavailableError,
|
|
7
|
+
QMD_INSTALL_COMMAND,
|
|
8
|
+
resolveVaultPath as resolveConfiguredVaultPath
|
|
9
|
+
} from '../dist/index.js';
|
|
10
|
+
|
|
11
|
+
const QMD_INDEX_ENV_VAR = 'CLAWVAULT_QMD_INDEX';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Validates that a path is within an allowed base directory.
|
|
15
|
+
* Prevents path traversal attacks.
|
|
16
|
+
* @param {string} inputPath - The path to validate
|
|
17
|
+
* @param {string} basePath - The allowed base directory
|
|
18
|
+
* @returns {string} The resolved, validated path
|
|
19
|
+
* @throws {Error} If the path escapes the base directory
|
|
20
|
+
*/
|
|
21
|
+
export function validatePathWithinBase(inputPath, basePath) {
|
|
22
|
+
const resolvedBase = path.resolve(basePath);
|
|
23
|
+
const resolvedPath = path.resolve(basePath, inputPath);
|
|
24
|
+
|
|
25
|
+
if (!resolvedPath.startsWith(resolvedBase + path.sep) && resolvedPath !== resolvedBase) {
|
|
26
|
+
throw new Error(`Path traversal detected: ${inputPath} escapes ${basePath}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return resolvedPath;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Sanitizes an argument that may contain a path to prevent injection.
|
|
34
|
+
* @param {unknown} arg - The argument to sanitize
|
|
35
|
+
* @returns {string} The sanitized argument
|
|
36
|
+
*/
|
|
37
|
+
export function sanitizeQmdArg(arg) {
|
|
38
|
+
const normalizedArg = String(arg);
|
|
39
|
+
// Reject arguments with null bytes (injection attempt)
|
|
40
|
+
if (normalizedArg.includes('\0')) {
|
|
41
|
+
throw new Error('Invalid argument: contains null byte');
|
|
42
|
+
}
|
|
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];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function resolveVaultPath(vaultPath) {
|
|
60
|
+
return resolveConfiguredVaultPath({ explicitPath: vaultPath });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function getVault(vaultPath) {
|
|
64
|
+
const vault = new ClawVault(resolveVaultPath(vaultPath));
|
|
65
|
+
await vault.load();
|
|
66
|
+
return vault;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function runQmd(args) {
|
|
70
|
+
return new Promise((resolve, reject) => {
|
|
71
|
+
// Sanitize all arguments before passing to spawn
|
|
72
|
+
const sanitizedArgs = withQmdIndex(args).map(sanitizeQmdArg);
|
|
73
|
+
const proc = spawn('qmd', sanitizedArgs, { stdio: 'inherit' });
|
|
74
|
+
proc.on('close', (code) => {
|
|
75
|
+
if (code === 0) resolve();
|
|
76
|
+
else reject(new Error(`qmd exited with code ${code}`));
|
|
77
|
+
});
|
|
78
|
+
proc.on('error', (err) => {
|
|
79
|
+
if (err?.code === 'ENOENT') {
|
|
80
|
+
reject(new QmdUnavailableError());
|
|
81
|
+
} else {
|
|
82
|
+
reject(err);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function printQmdMissing() {
|
|
89
|
+
console.error(chalk.red('Error: ClawVault requires qmd.'));
|
|
90
|
+
console.log(chalk.dim(`Install: ${QMD_INSTALL_COMMAND}`));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export { QmdUnavailableError };
|