claude-code-watch 0.0.5 → 0.0.6
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 +13 -11
- package/README.zh-CN.md +1 -1
- package/bin/claude-watch.js +108 -2
- package/package.json +5 -4
- package/public/index.html +63 -15
- package/public/vendor/github-dark.min.css +10 -0
- package/public/vendor/highlight.min.js +1213 -0
- package/public/vendor/marked.min.js +6 -0
- package/public/vendor/purify.min.js +3 -0
- package/src/parser/parser.js +34 -83
- package/src/server/server.js +99 -60
- package/src/watcher/watcher.js +286 -239
package/src/server/server.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
3
|
+
var http = require('http');
|
|
4
|
+
var fs = require('fs');
|
|
5
|
+
var path = require('path');
|
|
6
|
+
var os = require('os');
|
|
7
|
+
var cp = require('child_process');
|
|
8
|
+
var readline = require('readline');
|
|
9
|
+
var { WebSocketServer } = require('ws');
|
|
10
|
+
var { Watcher, listSessions, listActiveSessions } = require('../watcher/watcher');
|
|
11
|
+
var { setDebugAll, contextWindowFor } = require('../parser/parser');
|
|
12
|
+
|
|
13
|
+
var MIME = {
|
|
13
14
|
'.html': 'text/html; charset=utf-8',
|
|
14
15
|
'.css': 'text/css; charset=utf-8',
|
|
15
16
|
'.js': 'application/javascript; charset=utf-8',
|
|
@@ -19,7 +20,8 @@ const MIME = {
|
|
|
19
20
|
'.ico': 'image/x-icon',
|
|
20
21
|
};
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
var MAX_ITEM_BUFFER = 2000;
|
|
24
|
+
var CONTEXT_STALE_MS = 30 * 60 * 1000; // 30 minutes
|
|
23
25
|
|
|
24
26
|
class DashboardServer {
|
|
25
27
|
constructor(options = {}) {
|
|
@@ -30,11 +32,13 @@ class DashboardServer {
|
|
|
30
32
|
this.clients = new Set();
|
|
31
33
|
this.itemBuffer = [];
|
|
32
34
|
this.contextMap = new Map();
|
|
35
|
+
this._contextCleanupTimer = null;
|
|
33
36
|
|
|
34
37
|
this.server = null;
|
|
35
38
|
this.wss = null;
|
|
36
39
|
|
|
37
40
|
setDebugAll(options.debugAll || false);
|
|
41
|
+
this.debugAll = options.debugAll || false;
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
getCtxKey(sessionID, agentID) {
|
|
@@ -59,6 +63,15 @@ class DashboardServer {
|
|
|
59
63
|
ctx.lastActivity = Date.now();
|
|
60
64
|
}
|
|
61
65
|
|
|
66
|
+
cleanupContextMap() {
|
|
67
|
+
const now = Date.now();
|
|
68
|
+
for (const [key, ctx] of this.contextMap) {
|
|
69
|
+
if (now - ctx.lastActivity > CONTEXT_STALE_MS) {
|
|
70
|
+
this.contextMap.delete(key);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
62
75
|
getContextSnapshot() {
|
|
63
76
|
const result = {};
|
|
64
77
|
for (const [key, ctx] of this.contextMap) {
|
|
@@ -79,7 +92,7 @@ class DashboardServer {
|
|
|
79
92
|
const msg = JSON.stringify({ type, payload });
|
|
80
93
|
for (const ws of this.clients) {
|
|
81
94
|
if (ws.readyState === 1) {
|
|
82
|
-
try { ws.send(msg); } catch {}
|
|
95
|
+
try { ws.send(msg); } catch { this.clients.delete(ws); ws.terminate(); }
|
|
83
96
|
}
|
|
84
97
|
}
|
|
85
98
|
}
|
|
@@ -114,7 +127,7 @@ class DashboardServer {
|
|
|
114
127
|
}
|
|
115
128
|
|
|
116
129
|
if (p.startsWith('/api/')) {
|
|
117
|
-
this.handleAPI(req, res, url);
|
|
130
|
+
await this.handleAPI(req, res, url);
|
|
118
131
|
return;
|
|
119
132
|
}
|
|
120
133
|
|
|
@@ -128,7 +141,7 @@ class DashboardServer {
|
|
|
128
141
|
await this.serveStatic(res, resolved);
|
|
129
142
|
}
|
|
130
143
|
|
|
131
|
-
handleAPI(req, res, url) {
|
|
144
|
+
async handleAPI(req, res, url) {
|
|
132
145
|
const route = url.pathname.slice('/api'.length);
|
|
133
146
|
const params = url.searchParams;
|
|
134
147
|
|
|
@@ -166,13 +179,13 @@ class DashboardServer {
|
|
|
166
179
|
if (route === '/task-output') {
|
|
167
180
|
const filePath = params.get('path');
|
|
168
181
|
if (!filePath) { this.sendJSON(res, { error: 'Missing path param' }, 400); return; }
|
|
182
|
+
const resolved = path.resolve(filePath);
|
|
183
|
+
if (!resolved.startsWith(path.resolve(os.homedir(), '.claude', 'projects'))) {
|
|
184
|
+
this.sendJSON(res, { error: 'Access denied' }, 403);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
169
187
|
try {
|
|
170
|
-
const
|
|
171
|
-
if (!resolved.startsWith(path.resolve(os.homedir(), '.claude', 'projects'))) {
|
|
172
|
-
this.sendJSON(res, { error: 'Access denied' }, 403);
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
const content = fs.readFileSync(resolved, 'utf-8');
|
|
188
|
+
const content = await fs.promises.readFile(resolved, 'utf-8');
|
|
176
189
|
this.sendJSON(res, { content });
|
|
177
190
|
} catch (err) {
|
|
178
191
|
this.sendJSON(res, { error: err.message }, 404);
|
|
@@ -228,6 +241,10 @@ class DashboardServer {
|
|
|
228
241
|
}
|
|
229
242
|
}
|
|
230
243
|
|
|
244
|
+
send(ws, type, payload) {
|
|
245
|
+
try { ws.send(JSON.stringify({ type, payload })); } catch {}
|
|
246
|
+
}
|
|
247
|
+
|
|
231
248
|
sendSnapshot(ws) {
|
|
232
249
|
if (!this.watcher) return;
|
|
233
250
|
const sessions = this.watcher.getSessionsSnapshot().map(s => ({
|
|
@@ -245,33 +262,22 @@ class DashboardServer {
|
|
|
245
262
|
isComplete: t.isComplete,
|
|
246
263
|
})),
|
|
247
264
|
}));
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
sessions,
|
|
253
|
-
autoDiscovery: this.watcher.isAutoDiscoveryEnabled(),
|
|
254
|
-
},
|
|
255
|
-
}));
|
|
256
|
-
} catch {}
|
|
265
|
+
this.send(ws, 'snapshot', {
|
|
266
|
+
sessions,
|
|
267
|
+
autoDiscovery: this.watcher.isAutoDiscoveryEnabled(),
|
|
268
|
+
});
|
|
257
269
|
}
|
|
258
270
|
|
|
259
271
|
sendItemBatch(ws) {
|
|
260
|
-
|
|
261
|
-
ws.send(JSON.stringify({ type: 'itemBatch', payload: this.itemBuffer }));
|
|
262
|
-
} catch {}
|
|
272
|
+
this.send(ws, 'itemBatch', this.itemBuffer);
|
|
263
273
|
}
|
|
264
274
|
|
|
265
275
|
sendContext(ws) {
|
|
266
|
-
|
|
267
|
-
ws.send(JSON.stringify({ type: 'context', payload: this.getContextSnapshot() }));
|
|
268
|
-
} catch {}
|
|
276
|
+
this.send(ws, 'context', this.getContextSnapshot());
|
|
269
277
|
}
|
|
270
278
|
|
|
271
279
|
sendConfig(ws) {
|
|
272
|
-
|
|
273
|
-
ws.send(JSON.stringify({ type: 'config', payload: { collapseAfter: this.collapseAfterMs } }));
|
|
274
|
-
} catch {}
|
|
280
|
+
this.send(ws, 'config', { collapseAfter: this.collapseAfterMs });
|
|
275
281
|
}
|
|
276
282
|
|
|
277
283
|
setupWatcher(watcherOpts) {
|
|
@@ -287,9 +293,7 @@ class DashboardServer {
|
|
|
287
293
|
w.on('item', (item) => {
|
|
288
294
|
this.itemBuffer.push(item);
|
|
289
295
|
if (this.itemBuffer.length > MAX_ITEM_BUFFER) {
|
|
290
|
-
|
|
291
|
-
this.itemBuffer.copyWithin(0, excess);
|
|
292
|
-
this.itemBuffer.length = MAX_ITEM_BUFFER;
|
|
296
|
+
this.itemBuffer = this.itemBuffer.slice(-MAX_ITEM_BUFFER);
|
|
293
297
|
}
|
|
294
298
|
this.updateContext(item);
|
|
295
299
|
this.broadcast('item', item);
|
|
@@ -302,6 +306,9 @@ class DashboardServer {
|
|
|
302
306
|
}
|
|
303
307
|
|
|
304
308
|
async killExistingPort(port) {
|
|
309
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
310
|
+
throw new Error(`Invalid port: ${port}`);
|
|
311
|
+
}
|
|
305
312
|
let cmd;
|
|
306
313
|
if (process.platform === 'win32') {
|
|
307
314
|
cmd = `netstat -ano | findstr :${port} | findstr LISTENING`;
|
|
@@ -312,14 +319,25 @@ class DashboardServer {
|
|
|
312
319
|
const result = cp.execSync(cmd, { encoding: 'utf-8' }).trim();
|
|
313
320
|
if (!result) return false;
|
|
314
321
|
const pids = result.split('\n').map(s => s.trim()).filter(Boolean);
|
|
322
|
+
|
|
323
|
+
// Ask user for confirmation before killing
|
|
324
|
+
const confirmed = await askYesNo(`Port ${port} is occupied by process(es) ${pids.join(', ')}. Kill them? [y/N] `);
|
|
325
|
+
if (!confirmed) {
|
|
326
|
+
console.error(`Port ${port} is in use. Exiting.`);
|
|
327
|
+
process.exit(1);
|
|
328
|
+
}
|
|
329
|
+
|
|
315
330
|
for (const pid of pids) {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
331
|
+
const parsedPid = parseInt(pid, 10);
|
|
332
|
+
if (Number.isInteger(parsedPid) && parsedPid > 0) {
|
|
333
|
+
try {
|
|
334
|
+
if (process.platform === 'win32') {
|
|
335
|
+
cp.execSync(`taskkill /PID ${parsedPid} /F`, { encoding: 'utf-8' });
|
|
336
|
+
} else {
|
|
337
|
+
process.kill(parsedPid, 'SIGKILL');
|
|
338
|
+
}
|
|
339
|
+
} catch {}
|
|
340
|
+
}
|
|
323
341
|
}
|
|
324
342
|
// Wait briefly for the port to be released
|
|
325
343
|
await new Promise(r => setTimeout(r, 500));
|
|
@@ -330,16 +348,21 @@ class DashboardServer {
|
|
|
330
348
|
}
|
|
331
349
|
|
|
332
350
|
async start(options = {}) {
|
|
351
|
+
if (!Number.isInteger(this.port) || this.port < 1 || this.port > 65535) {
|
|
352
|
+
throw new Error(`Invalid port: ${this.port}`);
|
|
353
|
+
}
|
|
333
354
|
const skipHistory = options.skipHistory || false;
|
|
334
355
|
const pollMs = options.pollMs || 500;
|
|
335
356
|
const activeWindow = options.activeWindow || 5 * 60 * 1000;
|
|
336
357
|
const maxSessions = options.maxSessions || 0;
|
|
358
|
+
const openBrowser = options.openBrowser !== false;
|
|
337
359
|
|
|
338
360
|
const watcherOpts = {
|
|
339
361
|
sessionID: options.sessionID || '',
|
|
340
362
|
pollInterval: pollMs,
|
|
341
363
|
activeWindow,
|
|
342
364
|
maxSessions,
|
|
365
|
+
debugAll: this.debugAll,
|
|
343
366
|
};
|
|
344
367
|
|
|
345
368
|
// Proactively kill any process occupying the port before starting
|
|
@@ -373,24 +396,29 @@ class DashboardServer {
|
|
|
373
396
|
|
|
374
397
|
const w = this.setupWatcher(watcherOpts);
|
|
375
398
|
|
|
376
|
-
|
|
399
|
+
try {
|
|
400
|
+
await w.init();
|
|
377
401
|
if (skipHistory) w.setSkipHistory(true);
|
|
378
|
-
w.start();
|
|
402
|
+
await w.start();
|
|
379
403
|
|
|
380
404
|
// Open browser AFTER sessions are discovered, so new clients get a full snapshot
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
405
|
+
if (openBrowser) {
|
|
406
|
+
const url = `http://localhost:${this.port}`;
|
|
407
|
+
const platform = process.platform;
|
|
408
|
+
if (platform === 'darwin') {
|
|
409
|
+
cp.spawn('open', [url]);
|
|
410
|
+
} else if (platform === 'win32') {
|
|
411
|
+
cp.spawn('cmd', ['/c', 'start', '', url]);
|
|
412
|
+
} else {
|
|
413
|
+
cp.spawn('xdg-open', [url]);
|
|
414
|
+
}
|
|
389
415
|
}
|
|
390
|
-
}
|
|
416
|
+
} catch (err) {
|
|
391
417
|
console.error('Watcher init error:', err.message);
|
|
392
418
|
process.exit(1);
|
|
393
|
-
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
this._contextCleanupTimer = setInterval(() => this.cleanupContextMap(), CONTEXT_STALE_MS);
|
|
394
422
|
|
|
395
423
|
this.server.listen(this.port, this.host, () => {
|
|
396
424
|
const url = `http://localhost:${this.port}`;
|
|
@@ -405,6 +433,7 @@ class DashboardServer {
|
|
|
405
433
|
}
|
|
406
434
|
|
|
407
435
|
stop() {
|
|
436
|
+
if (this._contextCleanupTimer) clearInterval(this._contextCleanupTimer);
|
|
408
437
|
if (this.wss) this.wss.close();
|
|
409
438
|
if (this.server) this.server.close();
|
|
410
439
|
if (this.watcher) this.watcher.stop();
|
|
@@ -417,4 +446,14 @@ async function startServer(options = {}) {
|
|
|
417
446
|
return ds.start(options);
|
|
418
447
|
}
|
|
419
448
|
|
|
449
|
+
function askYesNo(prompt) {
|
|
450
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
451
|
+
return new Promise(resolve => {
|
|
452
|
+
rl.question(prompt, answer => {
|
|
453
|
+
rl.close();
|
|
454
|
+
resolve(/^y(es)?$/i.test(answer.trim()));
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
|
|
420
459
|
module.exports = { DashboardServer, startServer };
|