clawvault 2.5.1 → 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.
Files changed (88) hide show
  1. package/README.md +159 -199
  2. package/bin/clawvault.js +111 -111
  3. package/bin/command-registration.test.js +166 -165
  4. package/bin/command-runtime.js +93 -77
  5. package/bin/command-runtime.test.js +154 -102
  6. package/bin/help-contract.test.js +39 -28
  7. package/bin/register-config-commands.js +153 -153
  8. package/bin/register-config-route-commands.test.js +121 -121
  9. package/bin/register-core-commands.js +237 -237
  10. package/bin/register-kanban-commands.js +56 -56
  11. package/bin/register-kanban-commands.test.js +83 -83
  12. package/bin/register-maintenance-commands.js +282 -248
  13. package/bin/register-project-commands.js +209 -209
  14. package/bin/register-project-commands.test.js +206 -201
  15. package/bin/register-query-commands.js +317 -312
  16. package/bin/register-query-commands.test.js +65 -0
  17. package/bin/register-resilience-commands.js +182 -182
  18. package/bin/register-resilience-commands.test.js +81 -81
  19. package/bin/register-route-commands.js +114 -114
  20. package/bin/register-session-lifecycle-commands.js +206 -206
  21. package/bin/register-tailscale-commands.js +106 -106
  22. package/bin/register-task-commands.js +348 -348
  23. package/bin/register-task-commands.test.js +69 -69
  24. package/bin/register-template-commands.js +72 -72
  25. package/bin/register-vault-operations-commands.js +300 -300
  26. package/bin/test-helpers/cli-command-fixtures.js +119 -119
  27. package/dashboard/lib/graph-diff.js +104 -104
  28. package/dashboard/lib/graph-diff.test.js +75 -75
  29. package/dashboard/lib/vault-parser.js +556 -556
  30. package/dashboard/lib/vault-parser.test.js +254 -254
  31. package/dashboard/public/app.js +796 -796
  32. package/dashboard/public/index.html +52 -52
  33. package/dashboard/public/styles.css +221 -221
  34. package/dashboard/server.js +374 -374
  35. package/dist/{chunk-G3OQJ2NQ.js → chunk-2YDBJS7M.js} +1 -1
  36. package/dist/chunk-3FP5BJ42.js +88 -0
  37. package/dist/{chunk-C3PF7WBA.js → chunk-4IV3R2F5.js} +2 -2
  38. package/dist/{chunk-7OHQFMJK.js → chunk-AY4PGUVL.js} +5 -4
  39. package/dist/chunk-FG6RJMCN.js +33 -0
  40. package/dist/{chunk-WIICLBNF.js → chunk-GFJ3LIIB.js} +1 -1
  41. package/dist/chunk-IZEY5S74.js +541 -0
  42. package/dist/chunk-LMEMZGUV.js +332 -0
  43. package/dist/{chunk-6RQPD7X6.js → chunk-M25QVSJM.js} +4 -3
  44. package/dist/{chunk-6B3JWM7J.js → chunk-O7XHXF7F.js} +34 -7
  45. package/dist/chunk-OSMS7QIG.js +406 -0
  46. package/dist/{chunk-PAYUH64O.js → chunk-QVMXF7FY.js} +11 -1
  47. package/dist/{chunk-TMZMN7OS.js → chunk-S2IG7VNM.js} +24 -12
  48. package/dist/{chunk-LMCC5OC7.js → chunk-TPDH3JPP.js} +1 -1
  49. package/dist/cli/index.d.ts +5 -0
  50. package/dist/cli/index.js +31 -0
  51. package/dist/commands/canvas.js +3 -3
  52. package/dist/commands/compat.js +1 -1
  53. package/dist/commands/context.js +4 -4
  54. package/dist/commands/doctor.js +16 -309
  55. package/dist/commands/embed.d.ts +17 -0
  56. package/dist/commands/embed.js +10 -0
  57. package/dist/commands/migrate-observations.js +2 -2
  58. package/dist/commands/observe.d.ts +1 -0
  59. package/dist/commands/observe.js +7 -6
  60. package/dist/commands/rebuild.js +5 -5
  61. package/dist/commands/reflect.js +3 -3
  62. package/dist/commands/replay.js +7 -7
  63. package/dist/commands/setup.d.ts +1 -0
  64. package/dist/commands/setup.js +2 -2
  65. package/dist/commands/sleep.d.ts +2 -1
  66. package/dist/commands/sleep.js +15 -15
  67. package/dist/commands/status.d.ts +9 -1
  68. package/dist/commands/status.js +33 -8
  69. package/dist/commands/wake.d.ts +1 -1
  70. package/dist/commands/wake.js +6 -6
  71. package/dist/index.d.ts +82 -5
  72. package/dist/index.js +127 -105
  73. package/dist/{types-jjuYN2Xn.d.ts → types-C74wgGL1.d.ts} +2 -0
  74. package/hooks/clawvault/HOOK.md +83 -74
  75. package/hooks/clawvault/handler.js +816 -812
  76. package/hooks/clawvault/handler.test.js +263 -263
  77. package/package.json +94 -125
  78. package/templates/checkpoint.md +19 -19
  79. package/templates/daily-note.md +19 -19
  80. package/templates/daily.md +19 -19
  81. package/templates/decision.md +17 -17
  82. package/templates/handoff.md +19 -19
  83. package/templates/lesson.md +16 -16
  84. package/templates/person.md +19 -19
  85. package/templates/project.md +23 -23
  86. package/dist/chunk-2RK2AG32.js +0 -743
  87. package/dist/{chunk-FW465EEA.js → chunk-VXEOHTSL.js} +3 -3
  88. package/dist/{chunk-KCCHROBR.js → chunk-YOSEUUNB.js} +4 -4
@@ -1,102 +1,154 @@
1
- import { beforeEach, describe, expect, it, vi } from 'vitest';
2
-
3
- const {
4
- spawnMock,
5
- resolveConfiguredVaultPathMock,
6
- clawvaultCtorMock,
7
- loadMock
8
- } = vi.hoisted(() => ({
9
- spawnMock: vi.fn(),
10
- resolveConfiguredVaultPathMock: vi.fn(),
11
- clawvaultCtorMock: vi.fn(),
12
- loadMock: vi.fn()
13
- }));
14
-
15
- vi.mock('child_process', () => ({
16
- spawn: spawnMock
17
- }));
18
-
19
- vi.mock('../dist/index.js', () => ({
20
- ClawVault: clawvaultCtorMock,
21
- resolveVaultPath: resolveConfiguredVaultPathMock,
22
- QmdUnavailableError: class QmdUnavailableError extends Error {},
23
- QMD_INSTALL_COMMAND: 'install-qmd'
24
- }));
25
-
26
- async function loadRuntimeModule() {
27
- vi.resetModules();
28
- return await import('./command-runtime.js');
29
- }
30
-
31
- beforeEach(() => {
32
- vi.clearAllMocks();
33
- clawvaultCtorMock.mockImplementation(() => ({
34
- load: loadMock
35
- }));
36
- });
37
-
38
- describe('command runtime helpers', () => {
39
- it('delegates vault path resolution and loads vaults', async () => {
40
- resolveConfiguredVaultPathMock.mockReturnValue('/resolved/vault');
41
- loadMock.mockResolvedValue(undefined);
42
- const { getVault, resolveVaultPath } = await loadRuntimeModule();
43
-
44
- const resolved = resolveVaultPath('/explicit');
45
- expect(resolveConfiguredVaultPathMock).toHaveBeenCalledWith({ explicitPath: '/explicit' });
46
- expect(resolved).toBe('/resolved/vault');
47
-
48
- await getVault('/explicit');
49
- expect(clawvaultCtorMock).toHaveBeenCalledWith('/resolved/vault');
50
- expect(loadMock).toHaveBeenCalled();
51
- });
52
-
53
- it('maps qmd ENOENT failures to QmdUnavailableError', async () => {
54
- const { runQmd, QmdUnavailableError } = await loadRuntimeModule();
55
- spawnMock.mockImplementation(() => {
56
- const handlers = {};
57
- const proc = {
58
- on: (event, handler) => {
59
- handlers[event] = handler;
60
- }
61
- };
62
- queueMicrotask(() => {
63
- handlers.error?.({ code: 'ENOENT' });
64
- });
65
- return proc;
66
- });
67
-
68
- await expect(runQmd(['update'])).rejects.toBeInstanceOf(QmdUnavailableError);
69
- });
70
-
71
- it('surfaces qmd non-zero exit codes as errors', async () => {
72
- const { runQmd } = await loadRuntimeModule();
73
- spawnMock.mockImplementation(() => {
74
- const handlers = {};
75
- const proc = {
76
- on: (event, handler) => {
77
- handlers[event] = handler;
78
- }
79
- };
80
- queueMicrotask(() => {
81
- handlers.close?.(2);
82
- });
83
- return proc;
84
- });
85
-
86
- await expect(runQmd(['update'])).rejects.toThrow('qmd exited with code 2');
87
- });
88
-
89
- it('prints consistent qmd missing guidance', async () => {
90
- const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
91
- const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
92
- try {
93
- const { printQmdMissing } = await loadRuntimeModule();
94
- printQmdMissing();
95
- expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('ClawVault requires qmd.'));
96
- expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('install-qmd'));
97
- } finally {
98
- errorSpy.mockRestore();
99
- logSpy.mockRestore();
100
- }
101
- });
102
- });
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
+ });
@@ -1,28 +1,39 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { registerAllCommandModules } from './test-helpers/cli-command-fixtures.js';
3
-
4
- describe('CLI help contract', () => {
5
- it('includes expected high-level command surface', () => {
6
- const help = registerAllCommandModules().helpInformation();
7
- expect(help).toContain('init');
8
- expect(help).toContain('context');
9
- expect(help).toContain('inject');
10
- expect(help).toContain('compat');
11
- expect(help).toContain('graph');
12
- expect(help).toContain('reflect');
13
- expect(help).toContain('replay');
14
- expect(help).toContain('repair-session');
15
- expect(help).toContain('template');
16
- expect(help).toContain('config');
17
- expect(help).toContain('route');
18
- });
19
-
20
- it('documents context auto profile and compat strict options', () => {
21
- const program = registerAllCommandModules();
22
- const contextHelp = program.commands.find((command) => command.name() === 'context')?.helpInformation() ?? '';
23
- const compatHelp = program.commands.find((command) => command.name() === 'compat')?.helpInformation() ?? '';
24
- expect(contextHelp).toContain('--profile <profile>');
25
- expect(contextHelp).toContain('auto');
26
- expect(compatHelp).toContain('--strict');
27
- });
28
- });
1
+ import { describe, expect, it } from 'vitest';
2
+ import { registerAllCommandModules } from './test-helpers/cli-command-fixtures.js';
3
+
4
+ describe('CLI help contract', () => {
5
+ it('includes expected high-level command surface', () => {
6
+ const help = registerAllCommandModules().helpInformation();
7
+ expect(help).toContain('init');
8
+ expect(help).toContain('context');
9
+ expect(help).toContain('inject');
10
+ expect(help).toContain('doctor');
11
+ expect(help).toContain('embed');
12
+ expect(help).toContain('compat');
13
+ expect(help).toContain('graph');
14
+ expect(help).toContain('reflect');
15
+ expect(help).toContain('replay');
16
+ expect(help).toContain('repair-session');
17
+ expect(help).toContain('project');
18
+ expect(help).toContain('template');
19
+ expect(help).toContain('config');
20
+ expect(help).toContain('route');
21
+ });
22
+
23
+ it('documents context/compat/inject/project help details', () => {
24
+ const program = registerAllCommandModules();
25
+ const contextHelp = program.commands.find((command) => command.name() === 'context')?.helpInformation() ?? '';
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() ?? '';
31
+ expect(contextHelp).toContain('--profile <profile>');
32
+ expect(contextHelp).toContain('auto');
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');
38
+ });
39
+ });
@@ -1,153 +1,153 @@
1
- /**
2
- * Runtime config command registrations backed by .clawvault.json.
3
- */
4
-
5
- function stringifyValue(value) {
6
- if (Array.isArray(value)) {
7
- if (value.every((item) => typeof item === 'string' || typeof item === 'number' || typeof item === 'boolean')) {
8
- return value.join(', ');
9
- }
10
- return JSON.stringify(value);
11
- }
12
- if (value && typeof value === 'object') {
13
- return JSON.stringify(value);
14
- }
15
- if (value === null || value === undefined) {
16
- return '(unset)';
17
- }
18
- return String(value);
19
- }
20
-
21
- function flattenConfig(value, prefix = '') {
22
- if (!value || typeof value !== 'object' || Array.isArray(value)) {
23
- return [{ key: prefix || '(root)', value: stringifyValue(value) }];
24
- }
25
-
26
- const rows = [];
27
- const keys = Object.keys(value).sort((left, right) => left.localeCompare(right));
28
- for (const key of keys) {
29
- const next = prefix ? `${prefix}.${key}` : key;
30
- const entry = value[key];
31
- if (entry && typeof entry === 'object' && !Array.isArray(entry)) {
32
- rows.push(...flattenConfig(entry, next));
33
- continue;
34
- }
35
- rows.push({ key: next, value: stringifyValue(entry) });
36
- }
37
- return rows;
38
- }
39
-
40
- function printTable(rows) {
41
- if (rows.length === 0) {
42
- console.log('No config values found.');
43
- return;
44
- }
45
- const keyWidth = Math.max(
46
- 'KEY'.length,
47
- ...rows.map((row) => row.key.length)
48
- );
49
- const valueWidth = Math.max(
50
- 'VALUE'.length,
51
- ...rows.map((row) => row.value.length)
52
- );
53
- const header = `${'KEY'.padEnd(keyWidth)} ${'VALUE'.padEnd(valueWidth)}`;
54
- const divider = `${'-'.repeat(keyWidth)} ${'-'.repeat(valueWidth)}`;
55
- console.log(header);
56
- console.log(divider);
57
- for (const row of rows) {
58
- console.log(`${row.key.padEnd(keyWidth)} ${row.value}`);
59
- }
60
- }
61
-
62
- function normalizeConfigKey(key) {
63
- return String(key || '').trim();
64
- }
65
-
66
- export function registerConfigCommands(program, { chalk, resolveVaultPath }) {
67
- const config = program
68
- .command('config')
69
- .description('Read and modify runtime config');
70
-
71
- config
72
- .command('get <key>')
73
- .description('Read a runtime config value (dot-notation supported)')
74
- .option('-v, --vault <path>', 'Vault path')
75
- .action(async (key, options) => {
76
- try {
77
- const { getConfigValue, SUPPORTED_CONFIG_KEYS } = await import('../dist/index.js');
78
- const normalizedKey = normalizeConfigKey(key);
79
- if (!SUPPORTED_CONFIG_KEYS.includes(normalizedKey)) {
80
- throw new Error(`Unsupported config key: ${normalizedKey}`);
81
- }
82
-
83
- const value = getConfigValue(resolveVaultPath(options.vault), normalizedKey);
84
- if (Array.isArray(value)) {
85
- console.log(value.join(','));
86
- return;
87
- }
88
- if (value && typeof value === 'object') {
89
- console.log(JSON.stringify(value, null, 2));
90
- return;
91
- }
92
- console.log(value === undefined || value === null ? '' : String(value));
93
- } catch (err) {
94
- console.error(chalk.red(`Error: ${err.message}`));
95
- process.exit(1);
96
- }
97
- });
98
-
99
- config
100
- .command('set <key> <value>')
101
- .description('Set a runtime config value')
102
- .option('-v, --vault <path>', 'Vault path')
103
- .action(async (key, value, options) => {
104
- try {
105
- const { setConfigValue, SUPPORTED_CONFIG_KEYS } = await import('../dist/index.js');
106
- const normalizedKey = normalizeConfigKey(key);
107
- if (!SUPPORTED_CONFIG_KEYS.includes(normalizedKey)) {
108
- throw new Error(`Unsupported config key: ${normalizedKey}`);
109
- }
110
- const result = setConfigValue(resolveVaultPath(options.vault), normalizedKey, value);
111
- console.log(chalk.green(`✓ Updated ${normalizedKey}`));
112
- console.log(chalk.dim(` Value: ${stringifyValue(result.value)}`));
113
- } catch (err) {
114
- console.error(chalk.red(`Error: ${err.message}`));
115
- process.exit(1);
116
- }
117
- });
118
-
119
- config
120
- .command('list')
121
- .description('List all config values')
122
- .option('-v, --vault <path>', 'Vault path')
123
- .action(async (options) => {
124
- try {
125
- const { listConfig } = await import('../dist/index.js');
126
- const values = listConfig(resolveVaultPath(options.vault));
127
- const rows = flattenConfig(values);
128
- printTable(rows);
129
- } catch (err) {
130
- console.error(chalk.red(`Error: ${err.message}`));
131
- process.exit(1);
132
- }
133
- });
134
-
135
- config
136
- .command('reset')
137
- .description('Reset runtime config values to defaults')
138
- .option('--confirm', 'Confirm reset')
139
- .option('-v, --vault <path>', 'Vault path')
140
- .action(async (options) => {
141
- try {
142
- if (!options.confirm) {
143
- throw new Error('Refusing to reset config without --confirm.');
144
- }
145
- const { resetConfig } = await import('../dist/index.js');
146
- resetConfig(resolveVaultPath(options.vault));
147
- console.log(chalk.green('✓ Config reset to defaults'));
148
- } catch (err) {
149
- console.error(chalk.red(`Error: ${err.message}`));
150
- process.exit(1);
151
- }
152
- });
153
- }
1
+ /**
2
+ * Runtime config command registrations backed by .clawvault.json.
3
+ */
4
+
5
+ function stringifyValue(value) {
6
+ if (Array.isArray(value)) {
7
+ if (value.every((item) => typeof item === 'string' || typeof item === 'number' || typeof item === 'boolean')) {
8
+ return value.join(', ');
9
+ }
10
+ return JSON.stringify(value);
11
+ }
12
+ if (value && typeof value === 'object') {
13
+ return JSON.stringify(value);
14
+ }
15
+ if (value === null || value === undefined) {
16
+ return '(unset)';
17
+ }
18
+ return String(value);
19
+ }
20
+
21
+ function flattenConfig(value, prefix = '') {
22
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
23
+ return [{ key: prefix || '(root)', value: stringifyValue(value) }];
24
+ }
25
+
26
+ const rows = [];
27
+ const keys = Object.keys(value).sort((left, right) => left.localeCompare(right));
28
+ for (const key of keys) {
29
+ const next = prefix ? `${prefix}.${key}` : key;
30
+ const entry = value[key];
31
+ if (entry && typeof entry === 'object' && !Array.isArray(entry)) {
32
+ rows.push(...flattenConfig(entry, next));
33
+ continue;
34
+ }
35
+ rows.push({ key: next, value: stringifyValue(entry) });
36
+ }
37
+ return rows;
38
+ }
39
+
40
+ function printTable(rows) {
41
+ if (rows.length === 0) {
42
+ console.log('No config values found.');
43
+ return;
44
+ }
45
+ const keyWidth = Math.max(
46
+ 'KEY'.length,
47
+ ...rows.map((row) => row.key.length)
48
+ );
49
+ const valueWidth = Math.max(
50
+ 'VALUE'.length,
51
+ ...rows.map((row) => row.value.length)
52
+ );
53
+ const header = `${'KEY'.padEnd(keyWidth)} ${'VALUE'.padEnd(valueWidth)}`;
54
+ const divider = `${'-'.repeat(keyWidth)} ${'-'.repeat(valueWidth)}`;
55
+ console.log(header);
56
+ console.log(divider);
57
+ for (const row of rows) {
58
+ console.log(`${row.key.padEnd(keyWidth)} ${row.value}`);
59
+ }
60
+ }
61
+
62
+ function normalizeConfigKey(key) {
63
+ return String(key || '').trim();
64
+ }
65
+
66
+ export function registerConfigCommands(program, { chalk, resolveVaultPath }) {
67
+ const config = program
68
+ .command('config')
69
+ .description('Read and modify runtime configuration');
70
+
71
+ config
72
+ .command('get <key>')
73
+ .description('Read a runtime config value (dot-notation supported)')
74
+ .option('-v, --vault <path>', 'Vault path')
75
+ .action(async (key, options) => {
76
+ try {
77
+ const { getConfigValue, SUPPORTED_CONFIG_KEYS } = await import('../dist/index.js');
78
+ const normalizedKey = normalizeConfigKey(key);
79
+ if (!SUPPORTED_CONFIG_KEYS.includes(normalizedKey)) {
80
+ throw new Error(`Unsupported config key: ${normalizedKey}`);
81
+ }
82
+
83
+ const value = getConfigValue(resolveVaultPath(options.vault), normalizedKey);
84
+ if (Array.isArray(value)) {
85
+ console.log(value.join(','));
86
+ return;
87
+ }
88
+ if (value && typeof value === 'object') {
89
+ console.log(JSON.stringify(value, null, 2));
90
+ return;
91
+ }
92
+ console.log(value === undefined || value === null ? '' : String(value));
93
+ } catch (err) {
94
+ console.error(chalk.red(`Error: ${err.message}`));
95
+ process.exit(1);
96
+ }
97
+ });
98
+
99
+ config
100
+ .command('set <key> <value>')
101
+ .description('Set a runtime config value')
102
+ .option('-v, --vault <path>', 'Vault path')
103
+ .action(async (key, value, options) => {
104
+ try {
105
+ const { setConfigValue, SUPPORTED_CONFIG_KEYS } = await import('../dist/index.js');
106
+ const normalizedKey = normalizeConfigKey(key);
107
+ if (!SUPPORTED_CONFIG_KEYS.includes(normalizedKey)) {
108
+ throw new Error(`Unsupported config key: ${normalizedKey}`);
109
+ }
110
+ const result = setConfigValue(resolveVaultPath(options.vault), normalizedKey, value);
111
+ console.log(chalk.green(`✓ Updated ${normalizedKey}`));
112
+ console.log(chalk.dim(` Value: ${stringifyValue(result.value)}`));
113
+ } catch (err) {
114
+ console.error(chalk.red(`Error: ${err.message}`));
115
+ process.exit(1);
116
+ }
117
+ });
118
+
119
+ config
120
+ .command('list')
121
+ .description('List all config values')
122
+ .option('-v, --vault <path>', 'Vault path')
123
+ .action(async (options) => {
124
+ try {
125
+ const { listConfig } = await import('../dist/index.js');
126
+ const values = listConfig(resolveVaultPath(options.vault));
127
+ const rows = flattenConfig(values);
128
+ printTable(rows);
129
+ } catch (err) {
130
+ console.error(chalk.red(`Error: ${err.message}`));
131
+ process.exit(1);
132
+ }
133
+ });
134
+
135
+ config
136
+ .command('reset')
137
+ .description('Reset runtime config values to defaults')
138
+ .option('--confirm', 'Confirm reset (required)')
139
+ .option('-v, --vault <path>', 'Vault path')
140
+ .action(async (options) => {
141
+ try {
142
+ if (!options.confirm) {
143
+ throw new Error('Refusing to reset config without --confirm.');
144
+ }
145
+ const { resetConfig } = await import('../dist/index.js');
146
+ resetConfig(resolveVaultPath(options.vault));
147
+ console.log(chalk.green('✓ Config reset to defaults'));
148
+ } catch (err) {
149
+ console.error(chalk.red(`Error: ${err.message}`));
150
+ process.exit(1);
151
+ }
152
+ });
153
+ }