clawvault 1.11.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +135 -1
  2. package/bin/clawvault.js +51 -1252
  3. package/bin/command-registration.test.js +148 -0
  4. package/bin/command-runtime.js +42 -0
  5. package/bin/command-runtime.test.js +102 -0
  6. package/bin/help-contract.test.js +23 -0
  7. package/bin/register-core-commands.js +139 -0
  8. package/bin/register-maintenance-commands.js +137 -0
  9. package/bin/register-query-commands.js +225 -0
  10. package/bin/register-resilience-commands.js +147 -0
  11. package/bin/register-session-lifecycle-commands.js +204 -0
  12. package/bin/register-template-commands.js +72 -0
  13. package/bin/register-vault-operations-commands.js +295 -0
  14. package/bin/test-helpers/cli-command-fixtures.js +94 -0
  15. package/dashboard/lib/graph-diff.js +3 -1
  16. package/dashboard/lib/graph-diff.test.js +19 -0
  17. package/dashboard/lib/vault-parser.js +330 -26
  18. package/dashboard/lib/vault-parser.test.js +191 -11
  19. package/dashboard/public/app.js +22 -9
  20. package/dist/chunk-MXSSG3QU.js +42 -0
  21. package/dist/chunk-O5V7SD5C.js +398 -0
  22. package/dist/chunk-PAYUH64O.js +284 -0
  23. package/dist/{chunk-3HFB7EMU.js → chunk-QFBKWDYR.js} +12 -0
  24. package/dist/{chunk-UBRYOIII.js → chunk-TBVI4N53.js} +210 -21
  25. package/dist/chunk-TXO34J3O.js +56 -0
  26. package/dist/commands/compat.d.ts +28 -0
  27. package/dist/commands/compat.js +10 -0
  28. package/dist/commands/context.d.ts +2 -33
  29. package/dist/commands/context.js +3 -2
  30. package/dist/commands/doctor.js +61 -3
  31. package/dist/commands/entities.d.ts +1 -0
  32. package/dist/commands/entities.js +4 -4
  33. package/dist/commands/graph.d.ts +21 -0
  34. package/dist/commands/graph.js +10 -0
  35. package/dist/commands/link.d.ts +1 -0
  36. package/dist/commands/link.js +14 -5
  37. package/dist/commands/sleep.js +7 -6
  38. package/dist/commands/status.d.ts +6 -0
  39. package/dist/commands/status.js +63 -3
  40. package/dist/commands/wake.js +5 -4
  41. package/dist/context-COo8oq1k.d.ts +45 -0
  42. package/dist/index.d.ts +63 -2
  43. package/dist/index.js +53 -15
  44. package/dist/lib/config.d.ts +6 -1
  45. package/dist/lib/config.js +7 -3
  46. package/hooks/clawvault/HOOK.md +6 -1
  47. package/hooks/clawvault/handler.js +44 -3
  48. package/hooks/clawvault/handler.test.js +161 -0
  49. package/package.json +34 -2
  50. package/dashboard/public/graph.js +0 -376
  51. package/dashboard/public/style.css +0 -154
  52. package/dist/chunk-4KDZZW4X.js +0 -13
@@ -0,0 +1,148 @@
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 {
13
+ chalkStub,
14
+ createGetVaultStub,
15
+ registerAllCommandModules,
16
+ stubResolveVaultPath
17
+ } from './test-helpers/cli-command-fixtures.js';
18
+
19
+ function listCommandNames(program) {
20
+ return program.commands.map((command) => command.name()).sort((a, b) => a.localeCompare(b));
21
+ }
22
+
23
+ describe('CLI command registration modules', () => {
24
+ it('registers core lifecycle commands', () => {
25
+ const program = new Command();
26
+ registerCoreCommands(program, {
27
+ chalk: chalkStub,
28
+ path,
29
+ fs,
30
+ createVault: async () => ({ getCategories: () => [], getQmdRoot: () => '', getQmdCollection: () => '' }),
31
+ getVault: createGetVaultStub({ store: async () => ({}), capture: async () => ({}) }),
32
+ runQmd: async () => {}
33
+ });
34
+
35
+ const names = listCommandNames(program);
36
+ expect(names).toEqual(expect.arrayContaining(['init', 'setup', 'store', 'capture']));
37
+ });
38
+
39
+ it('registers query commands with profile option', () => {
40
+ const program = new Command();
41
+ registerQueryCommands(program, {
42
+ chalk: chalkStub,
43
+ getVault: createGetVaultStub({ find: async () => [], vsearch: async () => [] }),
44
+ resolveVaultPath: stubResolveVaultPath,
45
+ QmdUnavailableError: class extends Error {},
46
+ printQmdMissing: () => {}
47
+ });
48
+
49
+ const names = listCommandNames(program);
50
+ expect(names).toEqual(expect.arrayContaining(['search', 'vsearch', 'context', 'observe', 'session-recap']));
51
+
52
+ const contextCommand = program.commands.find((command) => command.name() === 'context');
53
+ const profileOption = contextCommand?.options.find((option) => option.flags.includes('--profile <profile>'));
54
+ expect(profileOption?.description).toContain('auto');
55
+ });
56
+
57
+ it('registers vault operation commands', () => {
58
+ const program = new Command();
59
+ registerVaultOperationsCommands(program, {
60
+ chalk: chalkStub,
61
+ fs,
62
+ getVault: createGetVaultStub({
63
+ list: async () => [],
64
+ get: async () => null,
65
+ stats: async () => ({ tags: [], categories: {} }),
66
+ sync: async () => ({ copied: [], deleted: [], unchanged: [], errors: [] }),
67
+ reindex: async () => 0,
68
+ remember: async () => ({ id: '' }),
69
+ getQmdCollection: () => ''
70
+ }),
71
+ runQmd: async () => {},
72
+ resolveVaultPath: stubResolveVaultPath,
73
+ path
74
+ });
75
+
76
+ const names = listCommandNames(program);
77
+ expect(names).toEqual(expect.arrayContaining([
78
+ 'list',
79
+ 'get',
80
+ 'stats',
81
+ 'sync',
82
+ 'reindex',
83
+ 'remember',
84
+ 'shell-init',
85
+ 'dashboard'
86
+ ]));
87
+ });
88
+
89
+ it('registers maintenance, resilience, session-lifecycle and template commands', () => {
90
+ const program = new Command();
91
+ registerMaintenanceCommands(program, { chalk: chalkStub });
92
+ registerResilienceCommands(program, {
93
+ chalk: chalkStub,
94
+ resolveVaultPath: stubResolveVaultPath
95
+ });
96
+ registerSessionLifecycleCommands(program, {
97
+ chalk: chalkStub,
98
+ resolveVaultPath: stubResolveVaultPath,
99
+ QmdUnavailableError: class extends Error {},
100
+ printQmdMissing: () => {},
101
+ getVault: createGetVaultStub({
102
+ createHandoff: async () => ({ id: '', path: '' }),
103
+ getQmdCollection: () => '',
104
+ generateRecap: async () => ({}),
105
+ formatRecap: () => ''
106
+ }),
107
+ runQmd: async () => {}
108
+ });
109
+ registerTemplateCommands(program, { chalk: chalkStub });
110
+
111
+ const names = listCommandNames(program);
112
+ expect(names).toEqual(expect.arrayContaining([
113
+ 'doctor',
114
+ 'compat',
115
+ 'graph',
116
+ 'entities',
117
+ 'link',
118
+ 'checkpoint',
119
+ 'recover',
120
+ 'status',
121
+ 'clean-exit',
122
+ 'repair-session',
123
+ 'wake',
124
+ 'sleep',
125
+ 'handoff',
126
+ 'recap',
127
+ 'template'
128
+ ]));
129
+
130
+ const templateCommand = program.commands.find((command) => command.name() === 'template');
131
+ const templateSubcommands = templateCommand?.commands.map((command) => command.name()) ?? [];
132
+ expect(templateSubcommands).toEqual(expect.arrayContaining(['list', 'create', 'add']));
133
+
134
+ const compatCommand = program.commands.find((command) => command.name() === 'compat');
135
+ const strictOption = compatCommand?.options.find((option) => option.flags.includes('--strict'));
136
+ const baseDirOption = compatCommand?.options.find((option) => option.flags.includes('--base-dir <path>'));
137
+ expect(strictOption).toBeTruthy();
138
+ expect(baseDirOption).toBeTruthy();
139
+ });
140
+
141
+ it('keeps top-level command names unique when modules are combined', () => {
142
+ const program = registerAllCommandModules(new Command());
143
+
144
+ const names = program.commands.map((command) => command.name());
145
+ const unique = new Set(names);
146
+ expect(unique.size).toBe(names.length);
147
+ });
148
+ });
@@ -0,0 +1,42 @@
1
+ import { spawn } from 'child_process';
2
+ import chalk from 'chalk';
3
+ import {
4
+ ClawVault,
5
+ QmdUnavailableError,
6
+ QMD_INSTALL_COMMAND,
7
+ resolveVaultPath as resolveConfiguredVaultPath
8
+ } from '../dist/index.js';
9
+
10
+ export function resolveVaultPath(vaultPath) {
11
+ return resolveConfiguredVaultPath({ explicitPath: vaultPath });
12
+ }
13
+
14
+ export async function getVault(vaultPath) {
15
+ const vault = new ClawVault(resolveVaultPath(vaultPath));
16
+ await vault.load();
17
+ return vault;
18
+ }
19
+
20
+ export async function runQmd(args) {
21
+ return new Promise((resolve, reject) => {
22
+ const proc = spawn('qmd', args, { stdio: 'inherit' });
23
+ proc.on('close', (code) => {
24
+ if (code === 0) resolve();
25
+ else reject(new Error(`qmd exited with code ${code}`));
26
+ });
27
+ proc.on('error', (err) => {
28
+ if (err?.code === 'ENOENT') {
29
+ reject(new QmdUnavailableError());
30
+ } else {
31
+ reject(err);
32
+ }
33
+ });
34
+ });
35
+ }
36
+
37
+ export function printQmdMissing() {
38
+ console.error(chalk.red('Error: ClawVault requires qmd.'));
39
+ console.log(chalk.dim(`Install: ${QMD_INSTALL_COMMAND}`));
40
+ }
41
+
42
+ export { QmdUnavailableError };
@@ -0,0 +1,102 @@
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
+ });
@@ -0,0 +1,23 @@
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('compat');
10
+ expect(help).toContain('graph');
11
+ expect(help).toContain('repair-session');
12
+ expect(help).toContain('template');
13
+ });
14
+
15
+ it('documents context auto profile and compat strict options', () => {
16
+ const program = registerAllCommandModules();
17
+ const contextHelp = program.commands.find((command) => command.name() === 'context')?.helpInformation() ?? '';
18
+ const compatHelp = program.commands.find((command) => command.name() === 'compat')?.helpInformation() ?? '';
19
+ expect(contextHelp).toContain('--profile <profile>');
20
+ expect(contextHelp).toContain('auto');
21
+ expect(compatHelp).toContain('--strict');
22
+ });
23
+ });
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Core vault lifecycle and write command registrations.
3
+ */
4
+
5
+ export function registerCoreCommands(
6
+ program,
7
+ { chalk, path, fs, createVault, getVault, runQmd }
8
+ ) {
9
+ // === INIT ===
10
+ program
11
+ .command('init [path]')
12
+ .description('Initialize a new ClawVault')
13
+ .option('-n, --name <name>', 'Vault name')
14
+ .option('--qmd', 'Set up qmd semantic search collection')
15
+ .option('--qmd-collection <name>', 'qmd collection name (defaults to vault name)')
16
+ .action(async (vaultPath, options) => {
17
+ const targetPath = vaultPath || '.';
18
+ console.log(chalk.cyan(`\n🐘 Initializing ClawVault at ${path.resolve(targetPath)}...\n`));
19
+
20
+ try {
21
+ const vault = await createVault(targetPath, {
22
+ name: options.name || path.basename(path.resolve(targetPath)),
23
+ qmdCollection: options.qmdCollection
24
+ });
25
+
26
+ console.log(chalk.green('✓ Vault created'));
27
+ console.log(chalk.dim(` Categories: ${vault.getCategories().join(', ')}`));
28
+
29
+ console.log(chalk.cyan('\nSetting up qmd collection...'));
30
+ try {
31
+ await runQmd([
32
+ 'collection',
33
+ 'add',
34
+ vault.getQmdRoot(),
35
+ '--name',
36
+ vault.getQmdCollection(),
37
+ '--mask',
38
+ '**/*.md'
39
+ ]);
40
+ console.log(chalk.green('✓ qmd collection created'));
41
+ } catch {
42
+ console.log(chalk.yellow('⚠ qmd collection may already exist'));
43
+ }
44
+
45
+ console.log(chalk.green('\n✅ ClawVault ready!\n'));
46
+ console.log(chalk.dim('Next steps:'));
47
+ console.log(chalk.dim(' clawvault store --category inbox --title "My note" --content "Hello world"'));
48
+ console.log(chalk.dim(' clawvault search "hello"'));
49
+ console.log();
50
+ } catch (err) {
51
+ console.error(chalk.red(`Error: ${err.message}`));
52
+ process.exit(1);
53
+ }
54
+ });
55
+
56
+ // === SETUP ===
57
+ program
58
+ .command('setup')
59
+ .description('Auto-discover and configure a ClawVault')
60
+ .action(async () => {
61
+ try {
62
+ const { setupCommand } = await import('../dist/commands/setup.js');
63
+ await setupCommand();
64
+ } catch (err) {
65
+ console.error(chalk.red(`Error: ${err.message}`));
66
+ process.exit(1);
67
+ }
68
+ });
69
+
70
+ // === STORE ===
71
+ program
72
+ .command('store')
73
+ .description('Store a new memory')
74
+ .requiredOption('-c, --category <category>', 'Category (preferences, decisions, patterns, people, projects, goals, transcripts, inbox)')
75
+ .requiredOption('-t, --title <title>', 'Document title')
76
+ .option('--content <content>', 'Content body')
77
+ .option('-f, --file <file>', 'Read content from file')
78
+ .option('--stdin', 'Read content from stdin')
79
+ .option('--overwrite', 'Overwrite if exists')
80
+ .option('--no-index', 'Skip qmd index update (auto-updates by default)')
81
+ .option('--embed', 'Also update qmd embeddings for vector search')
82
+ .option('-v, --vault <path>', 'Vault path (default: find nearest)')
83
+ .action(async (options) => {
84
+ try {
85
+ const vault = await getVault(options.vault);
86
+ let content = options.content || '';
87
+
88
+ if (options.file) {
89
+ content = fs.readFileSync(options.file, 'utf-8');
90
+ } else if (options.stdin) {
91
+ content = fs.readFileSync(0, 'utf-8');
92
+ }
93
+
94
+ const doc = await vault.store({
95
+ category: options.category,
96
+ title: options.title,
97
+ content,
98
+ overwrite: options.overwrite
99
+ });
100
+
101
+ console.log(chalk.green(`✓ Stored: ${doc.id}`));
102
+ console.log(chalk.dim(` Path: ${doc.path}`));
103
+
104
+ if (options.index !== false) {
105
+ const collection = vault.getQmdCollection();
106
+ await runQmd(collection ? ['update', '-c', collection] : ['update']);
107
+ if (options.embed) {
108
+ await runQmd(collection ? ['embed', '-c', collection] : ['embed']);
109
+ }
110
+ }
111
+ } catch (err) {
112
+ console.error(chalk.red(`Error: ${err.message}`));
113
+ process.exit(1);
114
+ }
115
+ });
116
+
117
+ // === CAPTURE ===
118
+ program
119
+ .command('capture <note>')
120
+ .description('Quick capture to inbox')
121
+ .option('-t, --title <title>', 'Note title')
122
+ .option('-v, --vault <path>', 'Vault path')
123
+ .option('--no-index', 'Skip qmd index update')
124
+ .action(async (note, options) => {
125
+ try {
126
+ const vault = await getVault(options.vault);
127
+ const doc = await vault.capture(note, options.title);
128
+ console.log(chalk.green(`✓ Captured: ${doc.id}`));
129
+
130
+ if (options.index !== false) {
131
+ const collection = vault.getQmdCollection();
132
+ await runQmd(collection ? ['update', '-c', collection] : ['update']);
133
+ }
134
+ } catch (err) {
135
+ console.error(chalk.red(`Error: ${err.message}`));
136
+ process.exit(1);
137
+ }
138
+ });
139
+ }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Maintenance and graph-oriented command registrations.
3
+ * Split from the main CLI entrypoint to keep bin/clawvault.js maintainable.
4
+ */
5
+
6
+ export function registerMaintenanceCommands(program, { chalk }) {
7
+ // === DOCTOR (health check) ===
8
+ program
9
+ .command('doctor')
10
+ .description('Check ClawVault setup health')
11
+ .option('-v, --vault <path>', 'Vault path')
12
+ .action(async (options) => {
13
+ try {
14
+ const { doctor } = await import('../dist/commands/doctor.js');
15
+ const report = await doctor(options.vault);
16
+
17
+ console.log(chalk.cyan('\n🩺 ClawVault Health Check\n'));
18
+ if (report.vaultPath) {
19
+ console.log(chalk.dim(`Vault: ${report.vaultPath}`));
20
+ console.log();
21
+ }
22
+
23
+ for (const check of report.checks) {
24
+ const prefix = check.status === 'ok'
25
+ ? chalk.green('✓')
26
+ : check.status === 'warn'
27
+ ? chalk.yellow('⚠')
28
+ : chalk.red('✗');
29
+ const detail = check.detail ? ` — ${check.detail}` : '';
30
+ console.log(`${prefix} ${check.label}${detail}`);
31
+ if (check.hint) {
32
+ console.log(chalk.dim(` ${check.hint}`));
33
+ }
34
+ }
35
+
36
+ const issues = report.warnings + report.errors;
37
+ console.log();
38
+ if (issues === 0) {
39
+ console.log(chalk.green('✅ ClawVault is healthy!\n'));
40
+ } else {
41
+ console.log(chalk.yellow(`⚠ ${issues} issue(s) found\n`));
42
+ }
43
+ } catch (err) {
44
+ console.error(chalk.red(`Error: ${err.message}`));
45
+ process.exit(1);
46
+ }
47
+ });
48
+
49
+ // === COMPAT (OpenClaw compatibility) ===
50
+ program
51
+ .command('compat')
52
+ .description('Check OpenClaw compatibility status')
53
+ .option('--strict', 'Exit non-zero when warnings are present')
54
+ .option('--base-dir <path>', 'Validate compatibility against alternate project root')
55
+ .option('--json', 'Output as JSON')
56
+ .action(async (options) => {
57
+ try {
58
+ const { compatCommand, compatibilityExitCode } = await import('../dist/commands/compat.js');
59
+ const report = await compatCommand({
60
+ json: options.json,
61
+ strict: options.strict,
62
+ baseDir: options.baseDir
63
+ });
64
+ const exitCode = compatibilityExitCode(report, { strict: options.strict });
65
+ if (exitCode !== 0) {
66
+ process.exitCode = exitCode;
67
+ }
68
+ } catch (err) {
69
+ console.error(chalk.red(`Error: ${err.message}`));
70
+ process.exit(1);
71
+ }
72
+ });
73
+
74
+ // === GRAPH ===
75
+ program
76
+ .command('graph')
77
+ .description('Show typed memory graph summary')
78
+ .option('-v, --vault <path>', 'Vault path')
79
+ .option('--refresh', 'Rebuild graph index before showing summary')
80
+ .option('--json', 'Output as JSON')
81
+ .action(async (options) => {
82
+ try {
83
+ const { graphCommand } = await import('../dist/commands/graph.js');
84
+ await graphCommand({
85
+ vaultPath: options.vault,
86
+ refresh: options.refresh,
87
+ json: options.json
88
+ });
89
+ } catch (err) {
90
+ console.error(chalk.red(`Error: ${err.message}`));
91
+ process.exit(1);
92
+ }
93
+ });
94
+
95
+ // === ENTITIES ===
96
+ program
97
+ .command('entities')
98
+ .description('List all linkable entities in the vault')
99
+ .option('-v, --vault <path>', 'Vault path')
100
+ .option('--json', 'Output as JSON')
101
+ .action(async (options) => {
102
+ try {
103
+ const { entitiesCommand } = await import('../dist/commands/entities.js');
104
+ await entitiesCommand({ json: options.json, vaultPath: options.vault });
105
+ } catch (err) {
106
+ console.error(chalk.red(`Error: ${err.message}`));
107
+ process.exit(1);
108
+ }
109
+ });
110
+
111
+ // === LINK ===
112
+ program
113
+ .command('link [file]')
114
+ .description('Auto-link entity mentions in markdown files')
115
+ .option('--all', 'Link all files in vault')
116
+ .option('--backlinks <file>', 'Show backlinks to a file')
117
+ .option('--dry-run', 'Show what would be linked without changing files')
118
+ .option('--orphans', 'List broken wiki-links')
119
+ .option('--rebuild', 'Rebuild backlinks index')
120
+ .option('-v, --vault <path>', 'Vault path')
121
+ .action(async (file, options) => {
122
+ try {
123
+ const { linkCommand } = await import('../dist/commands/link.js');
124
+ await linkCommand(file, {
125
+ all: options.all,
126
+ dryRun: options.dryRun,
127
+ backlinks: options.backlinks,
128
+ orphans: options.orphans,
129
+ rebuild: options.rebuild,
130
+ vaultPath: options.vault
131
+ });
132
+ } catch (err) {
133
+ console.error(chalk.red(`Error: ${err.message}`));
134
+ process.exit(1);
135
+ }
136
+ });
137
+ }