claude-code-watch 0.0.11 → 0.0.14

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.
@@ -113,9 +113,8 @@ async function runUpdate() {
113
113
  console.log(` Latest version: v${latest}`);
114
114
  console.log(' Running npm install -g claude-code-watch@latest...\n');
115
115
 
116
- const { execSync } = require('child_process');
117
116
  try {
118
- execSync('npm install -g claude-code-watch@latest', { stdio: 'inherit' });
117
+ cp.execSync('npm install -g claude-code-watch@latest', { stdio: 'inherit' });
119
118
  console.log(`\n Updated to v${latest}. Restart to use the new version.`);
120
119
  } catch {
121
120
  console.error('\n Update failed. Try manually: npm install -g claude-code-watch@latest');
@@ -139,9 +138,16 @@ async function main() {
139
138
  openBrowser: true,
140
139
  };
141
140
 
142
- // First pass: collect all option values
141
+ // Action flags
142
+ let listSessionsLimit = 0; // 0 = no list, >0 = limit
143
+ let listActiveLimit = 0; // 0 = no list, >0 = limit, -1 = all
144
+ let showVersion = false;
145
+ let showHelp = false;
146
+ let doUpdate = false;
147
+
143
148
  for (let i = 0; i < args.length; i++) {
144
- switch (args[i]) {
149
+ const arg = args[i];
150
+ switch (arg) {
145
151
  case '-s':
146
152
  options.sessionID = args[++i] || '';
147
153
  break;
@@ -151,7 +157,7 @@ async function main() {
151
157
  case '-p':
152
158
  case '--port': {
153
159
  if (i + 1 >= args.length || args[i + 1].startsWith('-')) {
154
- console.error(`Error: ${args[i]} requires a port number`);
160
+ console.error(`Error: ${arg} requires a port number`);
155
161
  process.exit(1);
156
162
  }
157
163
  const pv = parseInt(args[++i], 10);
@@ -165,7 +171,7 @@ async function main() {
165
171
  case '-h':
166
172
  case '--host':
167
173
  if (i + 1 >= args.length || args[i + 1].startsWith('-')) {
168
- console.error(`Error: ${args[i]} requires a host address`);
174
+ console.error(`Error: ${arg} requires a host address`);
169
175
  process.exit(1);
170
176
  }
171
177
  options.host = args[++i];
@@ -196,75 +202,85 @@ async function main() {
196
202
  case '--no-open':
197
203
  options.openBrowser = false;
198
204
  break;
199
- default:
200
- break;
201
- }
202
- }
203
-
204
- // Second pass: execute action flags with fully resolved options
205
- for (let i = 0; i < args.length; i++) {
206
- switch (args[i]) {
207
205
  case '-l': {
208
- const v = parseInt(args[i + 1]);
209
- const limit = !isNaN(v) ? v : 10;
206
+ const next = args[i + 1];
207
+ const v = parseInt(next);
208
+ listSessionsLimit = !isNaN(v) ? v : 10;
210
209
  if (!isNaN(v)) i++;
211
- const sessions = await listSessions(limit);
212
- if (sessions.length === 0) {
213
- console.log('No sessions found.');
214
- } else {
215
- const now = Date.now();
216
- for (const s of sessions) {
217
- const age = Math.round((now - new Date(s.modified).getTime()) / 1000);
218
- const ageStr = age < 60 ? `${age}s ago` : age < 3600 ? `${Math.floor(age / 60)}m ago` : `${Math.floor(age / 3600)}h ago`;
219
- const active = s.isActive ? '●' : '○';
220
- const id = s.id.length > 40 ? s.id.slice(0, 37) + '...' : s.id;
221
- console.log(`${active} ${id} ${s.projectPath || '?'} ${ageStr}`);
222
- }
223
- }
224
- return;
210
+ break;
225
211
  }
226
212
  case '-a': {
227
- const v = parseInt(args[i + 1]);
228
- const limit = !isNaN(v) ? v : 0;
229
- if (!isNaN(v)) i++;
230
- const sessions = await listActiveSessions(options.activeWindow);
231
- const result = limit > 0 ? sessions.slice(0, limit) : sessions;
232
- if (result.length === 0) {
233
- console.log('No active sessions found.');
234
- } else {
235
- const now = Date.now();
236
- for (const s of result) {
237
- const age = Math.round((now - new Date(s.modified).getTime()) / 1000);
238
- const ageStr = age < 60 ? `${age}s ago` : age < 3600 ? `${Math.floor(age / 60)}m ago` : `${Math.floor(age / 3600)}h ago`;
239
- const id = s.id.length > 40 ? s.id.slice(0, 37) + '...' : s.id;
240
- console.log(`● ${id} ${s.projectPath || '?'} ${ageStr}`);
241
- }
242
- }
243
- return;
213
+ const next = args[i + 1];
214
+ const v = parseInt(next);
215
+ if (!isNaN(v)) { listActiveLimit = v; i++; }
216
+ else { listActiveLimit = -1; }
217
+ break;
244
218
  }
245
219
  case '-v':
246
- console.log(`claude-watch v${VERSION}`);
247
- return;
220
+ showVersion = true;
221
+ break;
248
222
  case '--help':
249
- printHelp();
250
- return;
223
+ showHelp = true;
224
+ break;
251
225
  case 'update':
252
- await runUpdate();
253
- return;
254
- // Skip option flags already handled in first pass
255
- case '-s': case '-n': case '-p': case '--port':
256
- case '-h': case '--host': case '-w': case '-c':
257
- case '-m': case '-D': case '--poll': case '--no-open':
226
+ doUpdate = true;
258
227
  break;
259
228
  default:
260
- if (args[i].startsWith('-')) {
261
- console.error(`Unknown option: ${args[i]}`);
229
+ if (arg.startsWith('-')) {
230
+ console.error(`Unknown option: ${arg}`);
262
231
  printHelp();
263
232
  process.exit(1);
264
233
  }
265
234
  }
266
235
  }
267
236
 
237
+ // Execute action flags
238
+ if (showVersion) {
239
+ printVersion();
240
+ return;
241
+ }
242
+ if (showHelp) {
243
+ printHelp();
244
+ return;
245
+ }
246
+ if (doUpdate) {
247
+ await runUpdate();
248
+ return;
249
+ }
250
+ if (listSessionsLimit > 0) {
251
+ const sessions = await listSessions(listSessionsLimit);
252
+ if (sessions.length === 0) {
253
+ console.log('No sessions found.');
254
+ } else {
255
+ const now = Date.now();
256
+ for (const s of sessions) {
257
+ const age = Math.round((now - new Date(s.modified).getTime()) / 1000);
258
+ const ageStr = age < 60 ? `${age}s ago` : age < 3600 ? `${Math.floor(age / 60)}m ago` : `${Math.floor(age / 3600)}h ago`;
259
+ const active = s.isActive ? '●' : '○';
260
+ const id = s.id.length > 40 ? s.id.slice(0, 37) + '...' : s.id;
261
+ console.log(`${active} ${id} ${s.projectPath || '?'} ${ageStr}`);
262
+ }
263
+ }
264
+ return;
265
+ }
266
+ if (listActiveLimit !== 0) {
267
+ const limit = listActiveLimit > 0 ? listActiveLimit : 0;
268
+ const sessions = await listActiveSessions(options.activeWindow);
269
+ const result = limit > 0 ? sessions.slice(0, limit) : sessions;
270
+ if (result.length === 0) {
271
+ console.log('No active sessions found.');
272
+ } else {
273
+ const now = Date.now();
274
+ for (const s of result) {
275
+ const age = Math.round((now - new Date(s.modified).getTime()) / 1000);
276
+ const ageStr = age < 60 ? `${age}s ago` : age < 3600 ? `${Math.floor(age / 60)}m ago` : `${Math.floor(age / 3600)}h ago`;
277
+ const id = s.id.length > 40 ? s.id.slice(0, 37) + '...' : s.id;
278
+ console.log(`● ${id} ${s.projectPath || '?'} ${ageStr}`);
279
+ }
280
+ }
281
+ return;
282
+ }
283
+
268
284
  checkForUpdate();
269
285
  startServer(options);
270
286
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-watch",
3
- "version": "0.0.11",
3
+ "version": "0.0.14",
4
4
  "description": "Web-based real-time monitor for Claude Code.",
5
5
  "main": "./src/server/server.js",
6
6
  "bin": {
package/public/index.html CHANGED
@@ -194,9 +194,11 @@ body {
194
194
  .stream-line.diag { color: var(--red); }
195
195
  .stream-line.debug { color: var(--gray); }
196
196
  .stream-line.marker { color: var(--dim); }
197
- .stream-line.agent-tag { font-weight: bold; }
197
+ .stream-line.agent-tag { font-weight: bold; display: flex; justify-content: space-between; align-items: baseline; white-space: nowrap; }
198
198
  .stream-line.agent-main { color: var(--blue); }
199
199
  .stream-line.agent-sub { color: var(--magenta); }
200
+ .stream-line.agent-tag .tag-label { flex-shrink: 0; }
201
+ .stream-line.agent-tag .timestamp { font-weight: normal; font-size: 0.85em; color: var(--dim); white-space: nowrap; }
200
202
  .stream-line.separator { color: var(--dim); }
201
203
 
202
204
  /* ── Footer ── */
@@ -393,16 +395,19 @@ let needsFullRender = true;
393
395
  // ══════════════════════════════════════════════════════════════════════════════
394
396
 
395
397
  const mdRenderer = new marked.Renderer();
396
- mdRenderer.code = function (code, lang) {
398
+ mdRenderer.code = function (codeOrObj, langOrEsc) {
399
+ // marked v4: code(text, lang, escaped) — marked v5+: code({ text, lang })
400
+ const text = typeof codeOrObj === 'object' ? codeOrObj.text : codeOrObj;
401
+ const lang = typeof codeOrObj === 'object' ? codeOrObj.lang : langOrEsc;
397
402
  let highlighted;
398
403
  if (lang && hljs.getLanguage(lang)) {
399
404
  try {
400
- highlighted = hljs.highlight(code, { language: lang }).value;
405
+ highlighted = hljs.highlight(text, { language: lang }).value;
401
406
  } catch {
402
- highlighted = hljs.highlightAuto(code).value;
407
+ highlighted = hljs.highlightAuto(text).value;
403
408
  }
404
409
  } else {
405
- highlighted = hljs.highlightAuto(code).value;
410
+ highlighted = hljs.highlightAuto(text).value;
406
411
  }
407
412
  const langTag = lang ? `<span class="lang-tag">${esc(lang)}</span>` : '';
408
413
  return `<div class="code-block-wrapper">
@@ -891,14 +896,15 @@ function renderItem(item) {
891
896
  }
892
897
 
893
898
  const agentName = item.agentName || 'Main';
899
+ const tsHtml = item.timestamp ? `<span class="timestamp">${fmtTimestamp(item.timestamp)}</span>` : '';
894
900
 
895
901
  switch (item.type) {
896
902
  case 'thinking':
897
- lines.push({ cls: agentTagCls, text: agentName + sep + '🧠 Thinking' });
903
+ lines.push({ cls: agentTagCls, text: `<span class="tag-label">${esc(agentName + sep + '🧠 Thinking')}</span>${tsHtml}`, html: true });
898
904
  for (const l of truncContent(item.content)) lines.push({ cls: 'stream-line thinking', text: l });
899
905
  break;
900
906
  case 'tool_input':
901
- lines.push({ cls: agentTagCls, text: agentName + sep + `🔧 ${item.toolName || ''}` });
907
+ lines.push({ cls: agentTagCls, text: `<span class="tag-label">${esc(agentName + sep + `🔧 ${item.toolName || ''}`)}</span>${tsHtml}`, html: true });
902
908
  for (const l of truncContent(item.content)) lines.push({ cls: 'stream-line tool-input', text: l });
903
909
  break;
904
910
  case 'tool_output': {
@@ -908,33 +914,33 @@ function renderItem(item) {
908
914
  }
909
915
  let label = tn ? `📤 ${tn} result` : '📤 Output';
910
916
  if (item.durationMs > 0) label += ' ' + fmtDur(item.durationMs);
911
- lines.push({ cls: agentTagCls, text: agentName + sep + label });
917
+ lines.push({ cls: agentTagCls, text: `<span class="tag-label">${esc(agentName + sep + label)}</span>${tsHtml}`, html: true });
912
918
  for (const l of truncContent(item.content)) lines.push({ cls: 'stream-line tool-output', text: l });
913
919
  break;
914
920
  }
915
921
  case 'text':
916
- lines.push({ cls: agentTagCls, text: agentName + sep + '💬 Response' });
922
+ lines.push({ cls: agentTagCls, text: `<span class="tag-label">${esc(agentName + sep + '💬 Response')}</span>${tsHtml}`, html: true });
917
923
  lines.push({ cls: 'stream-line text md-content', text: mdRender(item.content), html: true });
918
924
  break;
919
925
  case 'hook_output': {
920
926
  let label = '🪝 Hook';
921
927
  if (item.toolName) label += ' ' + item.toolName;
922
928
  if (item.durationMs > 0) label += ' ' + fmtDur(item.durationMs);
923
- lines.push({ cls: agentTagCls, text: agentName + sep + label });
929
+ lines.push({ cls: agentTagCls, text: `<span class="tag-label">${esc(agentName + sep + label)}</span>${tsHtml}`, html: true });
924
930
  for (const l of truncContent(item.content)) lines.push({ cls: 'stream-line hook', text: l });
925
931
  break;
926
932
  }
927
933
  case 'diagnostics': {
928
934
  let label = '⚠ Diagnostics';
929
935
  if (item.toolName) label += ' ' + item.toolName;
930
- lines.push({ cls: agentTagCls, text: agentName + sep + label });
936
+ lines.push({ cls: agentTagCls, text: `<span class="tag-label">${esc(agentName + sep + label)}</span>${tsHtml}`, html: true });
931
937
  for (const l of truncContent(item.content)) lines.push({ cls: 'stream-line diag', text: l });
932
938
  break;
933
939
  }
934
940
  case 'debug': {
935
941
  let label = '🔍 Debug';
936
942
  if (item.toolName) label += ' ' + item.toolName;
937
- lines.push({ cls: agentTagCls, text: agentName + sep + label });
943
+ lines.push({ cls: agentTagCls, text: `<span class="tag-label">${esc(agentName + sep + label)}</span>${tsHtml}`, html: true });
938
944
  for (const l of truncContent(item.content)) lines.push({ cls: 'stream-line debug', text: l });
939
945
  break;
940
946
  }
@@ -1269,6 +1275,15 @@ function fmtDur(ms) {
1269
1275
  return `(${(ms / 60000).toFixed(1)}m)`;
1270
1276
  }
1271
1277
 
1278
+ function fmtTimestamp(ts) {
1279
+ if (!ts) return '';
1280
+ const d = ts instanceof Date ? ts : new Date(ts);
1281
+ if (isNaN(d.getTime())) return '';
1282
+ const pad = (n, len) => String(n).padStart(len, '0');
1283
+ const ms = pad(d.getMilliseconds(), 3);
1284
+ return `${pad(d.getFullYear(),4)}-${pad(d.getMonth()+1,2)}-${pad(d.getDate(),2)} ${pad(d.getHours(),2)}:${pad(d.getMinutes(),2)}:${pad(d.getSeconds(),2)}.${ms}`;
1285
+ }
1286
+
1272
1287
  function fmtTok(n) {
1273
1288
  if (!n) return '0';
1274
1289
  if (n < 1000) return String(n);
@@ -55,6 +55,8 @@ class DashboardServer {
55
55
  ctx = { inputTokens: 0, outputTokens: 0, cacheCreation: 0, cacheRead: 0, model: '', contextWindow: 200000, lastActivity: Date.now() };
56
56
  this.contextMap.set(key, ctx);
57
57
  }
58
+ // inputTokens: Claude API returns cumulative total per call, not incremental — use Math.max
59
+ // outputTokens/cache tokens: API returns incremental values — use +=
58
60
  if (item.inputTokens) ctx.inputTokens = Math.max(ctx.inputTokens, item.inputTokens);
59
61
  if (item.outputTokens) ctx.outputTokens += item.outputTokens;
60
62
  if (item.cacheCreationTokens) ctx.cacheCreation += item.cacheCreationTokens;
@@ -101,7 +103,9 @@ class DashboardServer {
101
103
  }
102
104
  for (const ws of toRemove) {
103
105
  this.clients.delete(ws);
104
- try { ws.terminate(); } catch {}
106
+ try { ws.terminate(); } catch (err) {
107
+ if (this.debugAll) console.error('[server] terminate error:', err.message);
108
+ }
105
109
  }
106
110
  }
107
111
 
@@ -224,7 +228,9 @@ class DashboardServer {
224
228
  try {
225
229
  const cmd = JSON.parse(data.toString('utf-8'));
226
230
  this.handleCommand(ws, cmd);
227
- } catch {}
231
+ } catch (err) {
232
+ if (this.debugAll) console.error('[server] WS message error:', err.message);
233
+ }
228
234
  });
229
235
 
230
236
  ws.on('close', () => {
@@ -373,7 +379,9 @@ class DashboardServer {
373
379
  } else {
374
380
  process.kill(parsedPid, 'SIGTERM');
375
381
  }
376
- } catch {}
382
+ } catch (err) {
383
+ console.error(`[server] Failed to SIGTERM pid ${parsedPid}: ${err.message}`);
384
+ }
377
385
  }
378
386
  }
379
387
 
@@ -383,7 +391,9 @@ class DashboardServer {
383
391
  for (const pid of pids) {
384
392
  const parsedPid = parseInt(pid, 10);
385
393
  if (Number.isInteger(parsedPid) && parsedPid > 1 && parsedPid !== myPid) {
386
- try { process.kill(parsedPid, 0); process.kill(parsedPid, 'SIGKILL'); } catch {}
394
+ try { process.kill(parsedPid, 0); process.kill(parsedPid, 'SIGKILL'); } catch {
395
+ // Process already gone — nothing to do
396
+ }
387
397
  }
388
398
  }
389
399
  }
@@ -424,15 +424,21 @@ class Watcher extends EventEmitter {
424
424
 
425
425
  if (p.endsWith('.jsonl')) {
426
426
  if (p.includes('/subagents/')) {
427
- this._handleNewSubagentFile(p);
427
+ this._handleNewSubagentFile(p).catch(err => {
428
+ if (this.debug) console.error('[watcher] _handleNewSubagentFile error:', err.message);
429
+ });
428
430
  } else if (this.watchActive) {
429
- this._handleNewSessionFile(p); // fire-and-forget, session will be discovered on next poll
431
+ this._handleNewSessionFile(p).catch(err => {
432
+ if (this.debug) console.error('[watcher] _handleNewSessionFile error:', err.message);
433
+ });
430
434
  }
431
435
  return;
432
436
  }
433
437
 
434
438
  if (p.endsWith('.txt') && p.includes('/tool-results/')) {
435
- this._handleNewToolResultFile(p);
439
+ this._handleNewToolResultFile(p).catch(err => {
440
+ if (this.debug) console.error('[watcher] _handleNewToolResultFile error:', err.message);
441
+ });
436
442
  }
437
443
  }
438
444
 
@@ -448,9 +454,13 @@ class Watcher extends EventEmitter {
448
454
  continue;
449
455
  }
450
456
  if (base === 'subagents' && entry.name.endsWith('.jsonl')) {
451
- this._handleNewSubagentFile(fullPath);
457
+ this._handleNewSubagentFile(fullPath).catch(err => {
458
+ if (this.debug) console.error('[watcher] _handleNewSubagentFile error:', err.message);
459
+ });
452
460
  } else if (base === 'tool-results' && entry.name.endsWith('.txt')) {
453
- this._handleNewToolResultFile(fullPath);
461
+ this._handleNewToolResultFile(fullPath).catch(err => {
462
+ if (this.debug) console.error('[watcher] _handleNewToolResultFile error:', err.message);
463
+ });
454
464
  }
455
465
  }
456
466
  }
@@ -542,7 +552,9 @@ class Watcher extends EventEmitter {
542
552
  this.pendingSubagents.delete(session.id);
543
553
  for (const sp of pending) {
544
554
  const agentID = path.basename(sp).replace(/^agent-/, '').replace(/\.jsonl$/, '');
545
- this._registerSubagent(session, session.id, agentID, sp);
555
+ this._registerSubagent(session, session.id, agentID, sp).catch(err => {
556
+ if (this.debug) console.error('[watcher] _registerSubagent error (pending):', err.message);
557
+ });
546
558
  }
547
559
  }
548
560
  }
@@ -561,7 +573,9 @@ class Watcher extends EventEmitter {
561
573
  return;
562
574
  }
563
575
 
564
- this._registerSubagent(session, sessionID, agentID, p); // fire-and-forget, event handler context
576
+ this._registerSubagent(session, sessionID, agentID, p).catch(err => {
577
+ if (this.debug) console.error('[watcher] _registerSubagent error:', err.message);
578
+ });
565
579
  }
566
580
 
567
581
  async _registerSubagent(session, sessionID, agentID, p) {