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.
Files changed (71) hide show
  1. package/README.md +159 -200
  2. package/bin/clawvault.js +111 -111
  3. package/bin/command-registration.test.js +166 -166
  4. package/bin/command-runtime.js +93 -93
  5. package/bin/command-runtime.test.js +154 -154
  6. package/bin/help-contract.test.js +39 -39
  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 -282
  13. package/bin/register-project-commands.js +209 -209
  14. package/bin/register-project-commands.test.js +206 -206
  15. package/bin/register-query-commands.js +317 -317
  16. package/bin/register-query-commands.test.js +65 -65
  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-3FP5BJ42.js → chunk-4QYGFWRM.js} +1 -1
  36. package/dist/{chunk-M25QVSJM.js → chunk-AXKYDCNN.js} +1 -1
  37. package/dist/{chunk-CLE2HHNT.js → chunk-IVRIKYFE.js} +18 -11
  38. package/dist/{chunk-HRTPQQF2.js → chunk-IZEY5S74.js} +1 -1
  39. package/dist/{chunk-HWUNREDJ.js → chunk-JDLOL2PL.js} +4 -4
  40. package/dist/{chunk-AY4PGUVL.js → chunk-KL4NAOMO.js} +1 -1
  41. package/dist/{chunk-O7XHXF7F.js → chunk-MAKNAHAW.js} +4 -4
  42. package/dist/{chunk-PLZKZW4I.js → chunk-OSMS7QIG.js} +1 -1
  43. package/dist/{chunk-NZ4ZZNSR.js → chunk-THRJVD4L.js} +1 -1
  44. package/dist/{chunk-4GBPTBFJ.js → chunk-TIGW564L.js} +1 -1
  45. package/dist/{chunk-BHO7WSAY.js → chunk-W2HNZC22.js} +3 -3
  46. package/dist/{chunk-GFJ3LIIB.js → chunk-XAVB4GB4.js} +1 -1
  47. package/dist/cli/index.js +10 -10
  48. package/dist/commands/context.js +3 -3
  49. package/dist/commands/doctor.js +4 -4
  50. package/dist/commands/embed.js +2 -2
  51. package/dist/commands/observe.js +2 -2
  52. package/dist/commands/setup.js +2 -2
  53. package/dist/commands/sleep.js +2 -2
  54. package/dist/commands/status.js +3 -3
  55. package/dist/commands/tailscale.js +3 -3
  56. package/dist/commands/wake.js +2 -2
  57. package/dist/index.js +12 -12
  58. package/dist/lib/tailscale.js +2 -2
  59. package/dist/lib/webdav.js +1 -1
  60. package/hooks/clawvault/HOOK.md +83 -74
  61. package/hooks/clawvault/handler.js +816 -816
  62. package/hooks/clawvault/handler.test.js +263 -263
  63. package/package.json +94 -125
  64. package/templates/checkpoint.md +19 -19
  65. package/templates/daily-note.md +19 -19
  66. package/templates/daily.md +19 -19
  67. package/templates/decision.md +17 -17
  68. package/templates/handoff.md +19 -19
  69. package/templates/lesson.md +16 -16
  70. package/templates/person.md +19 -19
  71. package/templates/project.md +23 -23
@@ -1,374 +1,374 @@
1
- import express from 'express';
2
- import * as fs from 'node:fs/promises';
3
- import * as os from 'node:os';
4
- import * as path from 'node:path';
5
- import { fileURLToPath } from 'node:url';
6
- import chokidar from 'chokidar';
7
- import { WebSocketServer } from 'ws';
8
- import { buildVaultGraph } from './lib/vault-parser.js';
9
- import { diffGraphs } from './lib/graph-diff.js';
10
-
11
- const DEFAULT_PORT = 3377;
12
- const HOST = '0.0.0.0';
13
-
14
- export async function startDashboard(options = {}) {
15
- const port = normalizePort(options.port ?? DEFAULT_PORT);
16
- const vaultPath = resolveVaultPath(options.vaultPath);
17
- await assertVaultPath(vaultPath);
18
-
19
- const app = express();
20
- const serverDir = path.dirname(fileURLToPath(import.meta.url));
21
- const projectDir = path.resolve(serverDir, '..');
22
- const publicDir = path.join(serverDir, 'public');
23
- const forceGraphDistDir = path.join(projectDir, 'node_modules', 'force-graph', 'dist');
24
-
25
- const graphStore = createLiveGraphStore(vaultPath);
26
- await graphStore.init();
27
-
28
- app.get('/api/graph', async (req, res) => {
29
- try {
30
- const shouldRefresh = req.query.refresh === '1';
31
- if (shouldRefresh) {
32
- await graphStore.refresh({ reason: 'api:refresh' });
33
- }
34
- res.json(graphStore.getGraph());
35
- } catch (error) {
36
- res.status(500).json({
37
- error: 'Failed to build graph',
38
- detail: error instanceof Error ? error.message : String(error)
39
- });
40
- }
41
- });
42
-
43
- app.get('/api/health', (_req, res) => {
44
- res.json({
45
- ok: true,
46
- vaultPath
47
- });
48
- });
49
-
50
- app.use('/vendor', express.static(forceGraphDistDir));
51
- app.use(express.static(publicDir, { extensions: ['html'] }));
52
-
53
- const server = await new Promise((resolve, reject) => {
54
- const runningServer = app
55
- .listen(port, HOST, () => resolve(runningServer))
56
- .on('error', reject);
57
- });
58
-
59
- const wsServer = new WebSocketServer({
60
- server,
61
- path: '/ws'
62
- });
63
-
64
- const unsubscribeGraphUpdates = graphStore.subscribe((update) => {
65
- broadcast(wsServer, {
66
- type: 'graph:patch',
67
- payload: {
68
- version: update.version,
69
- reason: update.reason,
70
- changedPaths: update.changedPaths,
71
- ...update.patch
72
- }
73
- });
74
- });
75
-
76
- wsServer.on('connection', (socket) => {
77
- socket.send(
78
- JSON.stringify({
79
- type: 'graph:init',
80
- payload: {
81
- version: graphStore.getVersion(),
82
- graph: graphStore.getGraph()
83
- }
84
- })
85
- );
86
- });
87
-
88
- const heartbeatInterval = setInterval(() => {
89
- for (const client of wsServer.clients) {
90
- if (client.readyState === 1) {
91
- client.ping();
92
- }
93
- }
94
- }, 20_000);
95
-
96
- await graphStore.startWatching();
97
-
98
- logStartup({
99
- port,
100
- vaultPath
101
- });
102
-
103
- let isShuttingDown = false;
104
- const shutdown = async () => {
105
- if (isShuttingDown) {
106
- return;
107
- }
108
- isShuttingDown = true;
109
- clearInterval(heartbeatInterval);
110
- unsubscribeGraphUpdates();
111
- await graphStore.close();
112
- await new Promise((resolve) => wsServer.close(() => resolve()));
113
- server.close(() => {
114
- process.exit(0);
115
- });
116
- };
117
-
118
- process.on('SIGINT', () => {
119
- void shutdown();
120
- });
121
- process.on('SIGTERM', () => {
122
- void shutdown();
123
- });
124
-
125
- return server;
126
- }
127
-
128
- function createLiveGraphStore(vaultPath) {
129
- const subscribers = new Set();
130
- const changedPathBuffer = new Set();
131
- const refreshDebounceMs = 240;
132
- let graph = null;
133
- let version = 0;
134
- let refreshTimer = null;
135
- let watcher = null;
136
- let inFlightRefresh = null;
137
- let refreshQueued = false;
138
-
139
- async function init() {
140
- graph = await buildVaultGraph(vaultPath);
141
- version = 1;
142
- }
143
-
144
- function getGraph() {
145
- return graph;
146
- }
147
-
148
- function getVersion() {
149
- return version;
150
- }
151
-
152
- function subscribe(listener) {
153
- subscribers.add(listener);
154
- return () => {
155
- subscribers.delete(listener);
156
- };
157
- }
158
-
159
- function emit(update) {
160
- for (const listener of subscribers) {
161
- listener(update);
162
- }
163
- }
164
-
165
- function queueRefresh({ reason, changedPath }) {
166
- if (changedPath) {
167
- changedPathBuffer.add(changedPath);
168
- }
169
- if (refreshTimer) {
170
- clearTimeout(refreshTimer);
171
- }
172
- refreshTimer = setTimeout(() => {
173
- refreshTimer = null;
174
- void refresh({ reason });
175
- }, refreshDebounceMs);
176
- }
177
-
178
- async function refresh({ reason = 'manual' } = {}) {
179
- if (inFlightRefresh) {
180
- refreshQueued = true;
181
- return inFlightRefresh;
182
- }
183
-
184
- const changedPaths = Array.from(changedPathBuffer).sort((a, b) => a.localeCompare(b));
185
- changedPathBuffer.clear();
186
-
187
- inFlightRefresh = buildVaultGraph(vaultPath)
188
- .then((nextGraph) => {
189
- const patch = diffGraphs(graph, nextGraph);
190
- graph = nextGraph;
191
- if (!patch.hasChanges) {
192
- return;
193
- }
194
- version += 1;
195
- emit({
196
- version,
197
- reason,
198
- changedPaths,
199
- patch
200
- });
201
- })
202
- .finally(async () => {
203
- inFlightRefresh = null;
204
- if (refreshQueued) {
205
- refreshQueued = false;
206
- await refresh({ reason: 'coalesced' });
207
- }
208
- });
209
-
210
- return inFlightRefresh;
211
- }
212
-
213
- async function startWatching() {
214
- watcher = chokidar.watch(path.join(vaultPath, '**', '*.md'), {
215
- persistent: true,
216
- ignoreInitial: true,
217
- awaitWriteFinish: {
218
- stabilityThreshold: 180,
219
- pollInterval: 50
220
- },
221
- ignored: (watchedPath) => isIgnoredPath(vaultPath, watchedPath)
222
- });
223
-
224
- watcher
225
- .on('add', (filePath) => {
226
- queueRefresh({
227
- reason: 'fs:add',
228
- changedPath: toRelativeVaultPath(vaultPath, filePath)
229
- });
230
- })
231
- .on('change', (filePath) => {
232
- queueRefresh({
233
- reason: 'fs:change',
234
- changedPath: toRelativeVaultPath(vaultPath, filePath)
235
- });
236
- })
237
- .on('unlink', (filePath) => {
238
- queueRefresh({
239
- reason: 'fs:unlink',
240
- changedPath: toRelativeVaultPath(vaultPath, filePath)
241
- });
242
- })
243
- .on('error', (error) => {
244
- console.error(`Dashboard file watcher error: ${error instanceof Error ? error.message : String(error)}`);
245
- });
246
- }
247
-
248
- async function close() {
249
- if (refreshTimer) {
250
- clearTimeout(refreshTimer);
251
- refreshTimer = null;
252
- }
253
- if (watcher) {
254
- await watcher.close();
255
- watcher = null;
256
- }
257
- }
258
-
259
- return {
260
- init,
261
- getGraph,
262
- getVersion,
263
- subscribe,
264
- refresh,
265
- startWatching,
266
- close
267
- };
268
- }
269
-
270
- function broadcast(wsServer, data) {
271
- const payload = JSON.stringify(data);
272
- for (const client of wsServer.clients) {
273
- if (client.readyState === 1) {
274
- client.send(payload);
275
- }
276
- }
277
- }
278
-
279
- function isIgnoredPath(vaultPath, watchedPath) {
280
- const relativePath = toRelativeVaultPath(vaultPath, watchedPath);
281
- const segments = relativePath.split('/').filter(Boolean);
282
-
283
- return segments.some((segment) =>
284
- segment === '.git' || segment === '.obsidian' || segment === '.trash' || segment === 'node_modules'
285
- );
286
- }
287
-
288
- function toRelativeVaultPath(vaultPath, absolutePath) {
289
- return path.relative(vaultPath, absolutePath).split(path.sep).join('/');
290
- }
291
-
292
- function parseArgs(argv) {
293
- const options = {
294
- port: DEFAULT_PORT,
295
- vaultPath: undefined
296
- };
297
-
298
- for (let i = 0; i < argv.length; i += 1) {
299
- const arg = argv[i];
300
- if (arg === '--port' || arg === '-p') {
301
- options.port = argv[i + 1];
302
- i += 1;
303
- continue;
304
- }
305
- if (arg === '--vault' || arg === '-v') {
306
- options.vaultPath = argv[i + 1];
307
- i += 1;
308
- }
309
- }
310
-
311
- return options;
312
- }
313
-
314
- function normalizePort(value) {
315
- const parsed = Number.parseInt(String(value), 10);
316
- if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) {
317
- throw new Error(`Invalid port: ${value}`);
318
- }
319
- return parsed;
320
- }
321
-
322
- function resolveVaultPath(input) {
323
- const candidate = input || process.env.CLAWVAULT_PATH || process.cwd();
324
- return path.resolve(candidate);
325
- }
326
-
327
- async function assertVaultPath(vaultPath) {
328
- let stat;
329
- try {
330
- stat = await fs.stat(vaultPath);
331
- } catch (error) {
332
- throw new Error(`Vault path not found: ${vaultPath}`);
333
- }
334
-
335
- if (!stat.isDirectory()) {
336
- throw new Error(`Vault path is not a directory: ${vaultPath}`);
337
- }
338
- }
339
-
340
- function logStartup({ port, vaultPath }) {
341
- const interfaces = os.networkInterfaces();
342
- const networkUrls = [];
343
-
344
- for (const addresses of Object.values(interfaces)) {
345
- for (const address of addresses ?? []) {
346
- if (address.family !== 'IPv4' || address.internal) {
347
- continue;
348
- }
349
- networkUrls.push(`http://${address.address}:${port}`);
350
- }
351
- }
352
-
353
- console.log('\nClawVault Dashboard');
354
- console.log(`Vault: ${vaultPath}`);
355
- console.log(`Local: http://localhost:${port}`);
356
- for (const url of networkUrls) {
357
- console.log(`Network: ${url}`);
358
- }
359
- console.log('\nPress Ctrl+C to stop.\n');
360
- }
361
-
362
- const currentFile = fileURLToPath(import.meta.url);
363
- const executedFile = process.argv[1] ? path.resolve(process.argv[1]) : '';
364
-
365
- if (currentFile === executedFile) {
366
- startDashboard(parseArgs(process.argv.slice(2))).catch((error) => {
367
- if (error && typeof error === 'object' && 'code' in error && error.code === 'EADDRINUSE') {
368
- console.error('Port already in use.');
369
- } else {
370
- console.error(error instanceof Error ? error.message : String(error));
371
- }
372
- process.exit(1);
373
- });
374
- }
1
+ import express from 'express';
2
+ import * as fs from 'node:fs/promises';
3
+ import * as os from 'node:os';
4
+ import * as path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import chokidar from 'chokidar';
7
+ import { WebSocketServer } from 'ws';
8
+ import { buildVaultGraph } from './lib/vault-parser.js';
9
+ import { diffGraphs } from './lib/graph-diff.js';
10
+
11
+ const DEFAULT_PORT = 3377;
12
+ const HOST = '0.0.0.0';
13
+
14
+ export async function startDashboard(options = {}) {
15
+ const port = normalizePort(options.port ?? DEFAULT_PORT);
16
+ const vaultPath = resolveVaultPath(options.vaultPath);
17
+ await assertVaultPath(vaultPath);
18
+
19
+ const app = express();
20
+ const serverDir = path.dirname(fileURLToPath(import.meta.url));
21
+ const projectDir = path.resolve(serverDir, '..');
22
+ const publicDir = path.join(serverDir, 'public');
23
+ const forceGraphDistDir = path.join(projectDir, 'node_modules', 'force-graph', 'dist');
24
+
25
+ const graphStore = createLiveGraphStore(vaultPath);
26
+ await graphStore.init();
27
+
28
+ app.get('/api/graph', async (req, res) => {
29
+ try {
30
+ const shouldRefresh = req.query.refresh === '1';
31
+ if (shouldRefresh) {
32
+ await graphStore.refresh({ reason: 'api:refresh' });
33
+ }
34
+ res.json(graphStore.getGraph());
35
+ } catch (error) {
36
+ res.status(500).json({
37
+ error: 'Failed to build graph',
38
+ detail: error instanceof Error ? error.message : String(error)
39
+ });
40
+ }
41
+ });
42
+
43
+ app.get('/api/health', (_req, res) => {
44
+ res.json({
45
+ ok: true,
46
+ vaultPath
47
+ });
48
+ });
49
+
50
+ app.use('/vendor', express.static(forceGraphDistDir));
51
+ app.use(express.static(publicDir, { extensions: ['html'] }));
52
+
53
+ const server = await new Promise((resolve, reject) => {
54
+ const runningServer = app
55
+ .listen(port, HOST, () => resolve(runningServer))
56
+ .on('error', reject);
57
+ });
58
+
59
+ const wsServer = new WebSocketServer({
60
+ server,
61
+ path: '/ws'
62
+ });
63
+
64
+ const unsubscribeGraphUpdates = graphStore.subscribe((update) => {
65
+ broadcast(wsServer, {
66
+ type: 'graph:patch',
67
+ payload: {
68
+ version: update.version,
69
+ reason: update.reason,
70
+ changedPaths: update.changedPaths,
71
+ ...update.patch
72
+ }
73
+ });
74
+ });
75
+
76
+ wsServer.on('connection', (socket) => {
77
+ socket.send(
78
+ JSON.stringify({
79
+ type: 'graph:init',
80
+ payload: {
81
+ version: graphStore.getVersion(),
82
+ graph: graphStore.getGraph()
83
+ }
84
+ })
85
+ );
86
+ });
87
+
88
+ const heartbeatInterval = setInterval(() => {
89
+ for (const client of wsServer.clients) {
90
+ if (client.readyState === 1) {
91
+ client.ping();
92
+ }
93
+ }
94
+ }, 20_000);
95
+
96
+ await graphStore.startWatching();
97
+
98
+ logStartup({
99
+ port,
100
+ vaultPath
101
+ });
102
+
103
+ let isShuttingDown = false;
104
+ const shutdown = async () => {
105
+ if (isShuttingDown) {
106
+ return;
107
+ }
108
+ isShuttingDown = true;
109
+ clearInterval(heartbeatInterval);
110
+ unsubscribeGraphUpdates();
111
+ await graphStore.close();
112
+ await new Promise((resolve) => wsServer.close(() => resolve()));
113
+ server.close(() => {
114
+ process.exit(0);
115
+ });
116
+ };
117
+
118
+ process.on('SIGINT', () => {
119
+ void shutdown();
120
+ });
121
+ process.on('SIGTERM', () => {
122
+ void shutdown();
123
+ });
124
+
125
+ return server;
126
+ }
127
+
128
+ function createLiveGraphStore(vaultPath) {
129
+ const subscribers = new Set();
130
+ const changedPathBuffer = new Set();
131
+ const refreshDebounceMs = 240;
132
+ let graph = null;
133
+ let version = 0;
134
+ let refreshTimer = null;
135
+ let watcher = null;
136
+ let inFlightRefresh = null;
137
+ let refreshQueued = false;
138
+
139
+ async function init() {
140
+ graph = await buildVaultGraph(vaultPath);
141
+ version = 1;
142
+ }
143
+
144
+ function getGraph() {
145
+ return graph;
146
+ }
147
+
148
+ function getVersion() {
149
+ return version;
150
+ }
151
+
152
+ function subscribe(listener) {
153
+ subscribers.add(listener);
154
+ return () => {
155
+ subscribers.delete(listener);
156
+ };
157
+ }
158
+
159
+ function emit(update) {
160
+ for (const listener of subscribers) {
161
+ listener(update);
162
+ }
163
+ }
164
+
165
+ function queueRefresh({ reason, changedPath }) {
166
+ if (changedPath) {
167
+ changedPathBuffer.add(changedPath);
168
+ }
169
+ if (refreshTimer) {
170
+ clearTimeout(refreshTimer);
171
+ }
172
+ refreshTimer = setTimeout(() => {
173
+ refreshTimer = null;
174
+ void refresh({ reason });
175
+ }, refreshDebounceMs);
176
+ }
177
+
178
+ async function refresh({ reason = 'manual' } = {}) {
179
+ if (inFlightRefresh) {
180
+ refreshQueued = true;
181
+ return inFlightRefresh;
182
+ }
183
+
184
+ const changedPaths = Array.from(changedPathBuffer).sort((a, b) => a.localeCompare(b));
185
+ changedPathBuffer.clear();
186
+
187
+ inFlightRefresh = buildVaultGraph(vaultPath)
188
+ .then((nextGraph) => {
189
+ const patch = diffGraphs(graph, nextGraph);
190
+ graph = nextGraph;
191
+ if (!patch.hasChanges) {
192
+ return;
193
+ }
194
+ version += 1;
195
+ emit({
196
+ version,
197
+ reason,
198
+ changedPaths,
199
+ patch
200
+ });
201
+ })
202
+ .finally(async () => {
203
+ inFlightRefresh = null;
204
+ if (refreshQueued) {
205
+ refreshQueued = false;
206
+ await refresh({ reason: 'coalesced' });
207
+ }
208
+ });
209
+
210
+ return inFlightRefresh;
211
+ }
212
+
213
+ async function startWatching() {
214
+ watcher = chokidar.watch(path.join(vaultPath, '**', '*.md'), {
215
+ persistent: true,
216
+ ignoreInitial: true,
217
+ awaitWriteFinish: {
218
+ stabilityThreshold: 180,
219
+ pollInterval: 50
220
+ },
221
+ ignored: (watchedPath) => isIgnoredPath(vaultPath, watchedPath)
222
+ });
223
+
224
+ watcher
225
+ .on('add', (filePath) => {
226
+ queueRefresh({
227
+ reason: 'fs:add',
228
+ changedPath: toRelativeVaultPath(vaultPath, filePath)
229
+ });
230
+ })
231
+ .on('change', (filePath) => {
232
+ queueRefresh({
233
+ reason: 'fs:change',
234
+ changedPath: toRelativeVaultPath(vaultPath, filePath)
235
+ });
236
+ })
237
+ .on('unlink', (filePath) => {
238
+ queueRefresh({
239
+ reason: 'fs:unlink',
240
+ changedPath: toRelativeVaultPath(vaultPath, filePath)
241
+ });
242
+ })
243
+ .on('error', (error) => {
244
+ console.error(`Dashboard file watcher error: ${error instanceof Error ? error.message : String(error)}`);
245
+ });
246
+ }
247
+
248
+ async function close() {
249
+ if (refreshTimer) {
250
+ clearTimeout(refreshTimer);
251
+ refreshTimer = null;
252
+ }
253
+ if (watcher) {
254
+ await watcher.close();
255
+ watcher = null;
256
+ }
257
+ }
258
+
259
+ return {
260
+ init,
261
+ getGraph,
262
+ getVersion,
263
+ subscribe,
264
+ refresh,
265
+ startWatching,
266
+ close
267
+ };
268
+ }
269
+
270
+ function broadcast(wsServer, data) {
271
+ const payload = JSON.stringify(data);
272
+ for (const client of wsServer.clients) {
273
+ if (client.readyState === 1) {
274
+ client.send(payload);
275
+ }
276
+ }
277
+ }
278
+
279
+ function isIgnoredPath(vaultPath, watchedPath) {
280
+ const relativePath = toRelativeVaultPath(vaultPath, watchedPath);
281
+ const segments = relativePath.split('/').filter(Boolean);
282
+
283
+ return segments.some((segment) =>
284
+ segment === '.git' || segment === '.obsidian' || segment === '.trash' || segment === 'node_modules'
285
+ );
286
+ }
287
+
288
+ function toRelativeVaultPath(vaultPath, absolutePath) {
289
+ return path.relative(vaultPath, absolutePath).split(path.sep).join('/');
290
+ }
291
+
292
+ function parseArgs(argv) {
293
+ const options = {
294
+ port: DEFAULT_PORT,
295
+ vaultPath: undefined
296
+ };
297
+
298
+ for (let i = 0; i < argv.length; i += 1) {
299
+ const arg = argv[i];
300
+ if (arg === '--port' || arg === '-p') {
301
+ options.port = argv[i + 1];
302
+ i += 1;
303
+ continue;
304
+ }
305
+ if (arg === '--vault' || arg === '-v') {
306
+ options.vaultPath = argv[i + 1];
307
+ i += 1;
308
+ }
309
+ }
310
+
311
+ return options;
312
+ }
313
+
314
+ function normalizePort(value) {
315
+ const parsed = Number.parseInt(String(value), 10);
316
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) {
317
+ throw new Error(`Invalid port: ${value}`);
318
+ }
319
+ return parsed;
320
+ }
321
+
322
+ function resolveVaultPath(input) {
323
+ const candidate = input || process.env.CLAWVAULT_PATH || process.cwd();
324
+ return path.resolve(candidate);
325
+ }
326
+
327
+ async function assertVaultPath(vaultPath) {
328
+ let stat;
329
+ try {
330
+ stat = await fs.stat(vaultPath);
331
+ } catch (error) {
332
+ throw new Error(`Vault path not found: ${vaultPath}`);
333
+ }
334
+
335
+ if (!stat.isDirectory()) {
336
+ throw new Error(`Vault path is not a directory: ${vaultPath}`);
337
+ }
338
+ }
339
+
340
+ function logStartup({ port, vaultPath }) {
341
+ const interfaces = os.networkInterfaces();
342
+ const networkUrls = [];
343
+
344
+ for (const addresses of Object.values(interfaces)) {
345
+ for (const address of addresses ?? []) {
346
+ if (address.family !== 'IPv4' || address.internal) {
347
+ continue;
348
+ }
349
+ networkUrls.push(`http://${address.address}:${port}`);
350
+ }
351
+ }
352
+
353
+ console.log('\nClawVault Dashboard');
354
+ console.log(`Vault: ${vaultPath}`);
355
+ console.log(`Local: http://localhost:${port}`);
356
+ for (const url of networkUrls) {
357
+ console.log(`Network: ${url}`);
358
+ }
359
+ console.log('\nPress Ctrl+C to stop.\n');
360
+ }
361
+
362
+ const currentFile = fileURLToPath(import.meta.url);
363
+ const executedFile = process.argv[1] ? path.resolve(process.argv[1]) : '';
364
+
365
+ if (currentFile === executedFile) {
366
+ startDashboard(parseArgs(process.argv.slice(2))).catch((error) => {
367
+ if (error && typeof error === 'object' && 'code' in error && error.code === 'EADDRINUSE') {
368
+ console.error('Port already in use.');
369
+ } else {
370
+ console.error(error instanceof Error ? error.message : String(error));
371
+ }
372
+ process.exit(1);
373
+ });
374
+ }