claude-code-watch 0.0.5 → 0.0.7

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.
@@ -1,15 +1,16 @@
1
1
  'use strict';
2
2
 
3
- const http = require('http');
4
- const fs = require('fs');
5
- const path = require('path');
6
- const os = require('os');
7
- const cp = require('child_process');
8
- const { WebSocketServer } = require('ws');
9
- const { Watcher, listSessions, listActiveSessions } = require('../watcher/watcher');
10
- const { setDebugAll, contextWindowFor, formatTokenCount } = require('../parser/parser');
11
-
12
- const MIME = {
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
- const MAX_ITEM_BUFFER = 2000;
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 resolved = path.resolve(filePath);
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
- try {
249
- ws.send(JSON.stringify({
250
- type: 'snapshot',
251
- payload: {
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
- try {
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
- try {
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
- try {
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
- const excess = this.itemBuffer.length - MAX_ITEM_BUFFER;
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
- try {
317
- if (process.platform === 'win32') {
318
- cp.execSync(`taskkill /PID ${pid} /F`, { encoding: 'utf-8' });
319
- } else {
320
- process.kill(parseInt(pid, 10), 'SIGKILL');
321
- }
322
- } catch {}
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
- w.init().then(() => {
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
- const url = `http://localhost:${this.port}`;
382
- const platform = process.platform;
383
- if (platform === 'darwin') {
384
- cp.spawn('open', [url]);
385
- } else if (platform === 'win32') {
386
- cp.spawn('cmd', ['/c', 'start', '', url]);
387
- } else {
388
- cp.spawn('xdg-open', [url]);
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
- }).catch(err => {
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 };