claude-code-watch 0.0.4 → 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 CHANGED
@@ -18,7 +18,7 @@ Claude Code writes detailed JSONL logs under `~/.claude/projects/` as it works
18
18
  ## Quick Start
19
19
 
20
20
  ```bash
21
- npx claude-watch
21
+ npx claude-code-watch
22
22
  ```
23
23
 
24
24
  This starts the dashboard at `http://localhost:23000` and opens it in your browser.
@@ -28,19 +28,21 @@ It will auto-discover active Claude Code sessions from `~/.claude/projects/` and
28
28
  ## Installation
29
29
 
30
30
  ```bash
31
- npm install -g claude-watch
31
+ npm install -g claude-code-watch
32
32
  ```
33
33
 
34
34
  Then run:
35
35
 
36
36
  ```bash
37
- claude-watch
37
+ claude-code-watch
38
38
  ```
39
39
 
40
40
  ## Usage
41
41
 
42
42
  ```
43
- claude-watch [OPTIONS]
43
+ claude-code-watch [OPTIONS]
44
+
45
+ Shorter alias: `cc-watch` (equivalent to `claude-code-watch`).
44
46
 
45
47
  OPTIONS:
46
48
  -p, --port <port> HTTP port (default: 23000)
@@ -62,25 +64,25 @@ OPTIONS:
62
64
 
63
65
  ```bash
64
66
  # List recent sessions
65
- claude-watch -l
67
+ claude-code-watch -l
66
68
 
67
69
  # List active sessions from last 10 minutes
68
- claude-watch -a -w 10m
70
+ claude-code-watch -a -w 10m
69
71
 
70
72
  # Watch a specific session
71
- claude-watch -s abc123-def456
73
+ claude-code-watch -s abc123-def456
72
74
 
73
75
  # Live-only mode (don't replay history)
74
- claude-watch -n
76
+ claude-code-watch -n
75
77
 
76
78
  # Custom port and host
77
- claude-watch -p 8080 -h 0.0.0.0
79
+ claude-code-watch -p 8080 -h 0.0.0.0
78
80
 
79
81
  # Limit tree to 5 most recent sessions, auto-collapse after 2m of inactivity
80
- claude-watch -m 5 -c 2m
82
+ claude-code-watch -m 5 -c 2m
81
83
 
82
84
  # Debug mode: see every unknown JSONL line type
83
- claude-watch -D
85
+ claude-code-watch -D
84
86
  ```
85
87
 
86
88
  ## How It Works
package/README.zh-CN.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # claude-watch
2
2
 
3
- claude-watch — 一个 Claude Code 的实时 Web 监控仪表盘。
3
+ claude-code-watch — 一个 Claude Code 的实时 Web 监控仪表盘。短命令 `cc-watch`。
4
4
 
5
5
  ## 核心作用
6
6
 
@@ -2,10 +2,13 @@
2
2
 
3
3
  'use strict';
4
4
 
5
+ const https = require('https');
6
+ const cp = require('child_process');
7
+
5
8
  const { startServer } = require('../src/server/server');
6
9
  const { listSessions, listActiveSessions } = require('../src/watcher/watcher');
7
10
 
8
- const VERSION = '0.0.1';
11
+ const { version: VERSION } = require('../package.json');
9
12
 
10
13
  function printHelp() {
11
14
  console.log(`claude-watch v${VERSION}
@@ -15,6 +18,7 @@ to a web browser.
15
18
 
16
19
  USAGE:
17
20
  claude-watch [OPTIONS]
21
+ claude-watch update Check for updates and install latest
18
22
 
19
23
  OPTIONS:
20
24
  -p, --port <port> HTTP port (default: 23000)
@@ -28,6 +32,7 @@ OPTIONS:
28
32
  -c <dur> Auto-collapse sessions inactive for this duration (e.g. 2m)
29
33
  -D Debug: show raw type:subtype for every JSONL line we'd drop
30
34
  --poll <ms> Polling interval in milliseconds (default: 500)
35
+ --no-open Do not auto-open browser on start
31
36
  -v Show version
32
37
  --help Show this help
33
38
 
@@ -36,6 +41,93 @@ ENVIRONMENT:
36
41
  `);
37
42
  }
38
43
 
44
+ function compareVersions(a, b) {
45
+ const pa = a.split('.').map(Number);
46
+ const pb = b.split('.').map(Number);
47
+ for (let i = 0; i < 3; i++) {
48
+ if (pa[i] > pb[i]) return 1;
49
+ if (pa[i] < pb[i]) return -1;
50
+ }
51
+ return 0;
52
+ }
53
+
54
+ function fetchLatestVersion() {
55
+ return new Promise((resolve, reject) => {
56
+ const opts = {
57
+ hostname: 'registry.npmjs.org',
58
+ path: '/claude-code-watch/latest',
59
+ timeout: 5000,
60
+ };
61
+
62
+ const req = https.get(opts, (res) => {
63
+ if (res.statusCode !== 200) { reject(new Error(`HTTP ${res.statusCode}`)); return; }
64
+ let data = '';
65
+ res.on('data', (chunk) => { data += chunk; });
66
+ res.on('end', () => {
67
+ try {
68
+ const json = JSON.parse(data);
69
+ resolve(json.version);
70
+ } catch (err) {
71
+ reject(err);
72
+ }
73
+ });
74
+ });
75
+
76
+ req.on('error', reject);
77
+ req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
78
+ req.end();
79
+ });
80
+ }
81
+
82
+ function checkForUpdate() {
83
+ fetchLatestVersion().then((latest) => {
84
+ if (compareVersions(latest, VERSION) > 0) {
85
+ console.log(`\n New version available: v${latest} (current: v${VERSION})`);
86
+ console.log(' Updating in background...\n');
87
+ const child = cp.spawn('npm', ['install', '-g', 'claude-code-watch@latest'], {
88
+ stdio: 'ignore',
89
+ detached: true,
90
+ });
91
+ child.unref();
92
+ child.on('exit', (code) => {
93
+ if (code === 0) {
94
+ console.log(` Updated to v${latest}. Changes take effect on next start.\n`);
95
+ }
96
+ });
97
+ }
98
+ }).catch(() => { /* network unavailable, skip */ });
99
+ }
100
+
101
+ async function runUpdate() {
102
+ console.log(` Current version: v${VERSION}`);
103
+ console.log(' Checking for latest version...\n');
104
+
105
+ let latest;
106
+ try {
107
+ latest = await fetchLatestVersion();
108
+ } catch (err) {
109
+ console.error(` Failed to check for updates: ${err.message}`);
110
+ process.exit(1);
111
+ }
112
+
113
+ if (compareVersions(latest, VERSION) <= 0) {
114
+ console.log(` Already up to date (v${VERSION}).`);
115
+ return;
116
+ }
117
+
118
+ console.log(` Latest version: v${latest}`);
119
+ console.log(' Running npm install -g claude-code-watch@latest...\n');
120
+
121
+ const { execSync } = require('child_process');
122
+ try {
123
+ execSync('npm install -g claude-code-watch@latest', { stdio: 'inherit' });
124
+ console.log(`\n Updated to v${latest}. Restart to use the new version.`);
125
+ } catch {
126
+ console.error('\n Update failed. Try manually: npm install -g claude-code-watch@latest');
127
+ process.exit(1);
128
+ }
129
+ }
130
+
39
131
  function parseDuration(s) {
40
132
  const match = s.match(/^(\d+)(ms|s|m|h)$/);
41
133
  if (!match) throw new Error(`Invalid duration: ${s}`);
@@ -62,6 +154,7 @@ async function main() {
62
154
  maxSessions: 0,
63
155
  collapseAfter: 0,
64
156
  debugAll: false,
157
+ openBrowser: true,
65
158
  };
66
159
 
67
160
  // First pass: collect all option values
@@ -74,7 +167,7 @@ async function main() {
74
167
  options.skipHistory = true;
75
168
  break;
76
169
  case '-p':
77
- case '--port':
170
+ case '--port': {
78
171
  if (i + 1 >= args.length || args[i + 1].startsWith('-')) {
79
172
  console.error(`Error: ${args[i]} requires a port number`);
80
173
  process.exit(1);
@@ -86,6 +179,7 @@ async function main() {
86
179
  }
87
180
  options.port = pv;
88
181
  break;
182
+ }
89
183
  case '-h':
90
184
  case '--host':
91
185
  if (i + 1 >= args.length || args[i + 1].startsWith('-')) {
@@ -117,6 +211,9 @@ async function main() {
117
211
  case '--poll':
118
212
  options.pollMs = parseInt(args[++i], 10) || 500;
119
213
  break;
214
+ case '--no-open':
215
+ options.openBrowser = false;
216
+ break;
120
217
  default:
121
218
  break;
122
219
  }
@@ -169,6 +266,14 @@ async function main() {
169
266
  case '--help':
170
267
  printHelp();
171
268
  return;
269
+ case 'update':
270
+ await runUpdate();
271
+ return;
272
+ // Skip option flags already handled in first pass
273
+ case '-s': case '-n': case '-p': case '--port':
274
+ case '-h': case '--host': case '-w': case '-c':
275
+ case '-m': case '-D': case '--poll': case '--no-open':
276
+ break;
172
277
  default:
173
278
  if (args[i].startsWith('-')) {
174
279
  console.error(`Unknown option: ${args[i]}`);
@@ -178,6 +283,7 @@ async function main() {
178
283
  }
179
284
  }
180
285
 
286
+ checkForUpdate();
181
287
  startServer(options);
182
288
  }
183
289
 
package/package.json CHANGED
@@ -1,14 +1,15 @@
1
1
  {
2
2
  "name": "claude-code-watch",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "Web-based real-time monitor for Claude Code.",
5
5
  "main": "./src/server/server.js",
6
6
  "bin": {
7
- "claude-code-watch": "./bin/claude-watch.js"
7
+ "claude-code-watch": "bin/claude-watch.js",
8
+ "cc-watch": "bin/claude-watch.js"
8
9
  },
9
10
  "scripts": {
10
11
  "start": "node bin/claude-watch.js",
11
- "dev": "node --watch bin/claude-watch.js",
12
+ "dev": "node --watch bin/claude-watch.js --no-open",
12
13
  "test": "node --test tests/all.test.js tests/watcher.test.js tests/server.test.js"
13
14
  },
14
15
  "files": [
@@ -26,7 +27,7 @@
26
27
  "license": "MIT",
27
28
  "repository": {
28
29
  "type": "git",
29
- "url": "https://github.com/shuxuecode/claude-watch"
30
+ "url": "git+https://github.com/shuxuecode/claude-watch.git"
30
31
  },
31
32
  "engines": {
32
33
  "node": ">=18.0.0"
package/public/index.html CHANGED
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>claude-watch</title>
7
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
7
+ <link rel="stylesheet" href="vendor/github-dark.min.css">
8
8
  <style>
9
9
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
10
10
 
@@ -269,8 +269,9 @@ body {
269
269
  <span class="sep">│</span>
270
270
  </div>
271
271
 
272
- <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
273
- <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/4.3.0/marked.min.js"></script>
272
+ <script src="vendor/highlight.min.js"></script>
273
+ <script src="vendor/marked.min.js"></script>
274
+ <script src="vendor/purify.min.js"></script>
274
275
  <script>
275
276
  // ══════════════════════════════════════════════════════════════════════════════
276
277
  // DOM refs
@@ -290,6 +291,8 @@ let ws = null;
290
291
  let reconnectTimer = null;
291
292
  let reconnectDelay = 1000;
292
293
  const MaxReconnectDelay = 30000;
294
+ const MaxReconnectAttempts = 20;
295
+ let reconnectAttempts = 0;
293
296
  let showTree = true;
294
297
  let autoScroll = true;
295
298
  let lastMsgTime = 0;
@@ -299,8 +302,40 @@ let sessions = [];
299
302
  let treeNodes = [];
300
303
  let treeCursor = 0;
301
304
  let streamItems = [];
302
- let seenToolIDs = new Set();
305
+ const seenToolIDsKeys = [];
306
+ const seenToolIDsSet = new Set();
307
+ const seenToolIDsMax = 5000;
308
+
309
+ function seenToolIDsHas(key) {
310
+ return seenToolIDsSet.has(key);
311
+ }
312
+ function seenToolIDsAdd(key) {
313
+ seenToolIDsSet.add(key);
314
+ seenToolIDsKeys.push(key);
315
+ if (seenToolIDsKeys.length > seenToolIDsMax) {
316
+ const evictCount = seenToolIDsKeys.length >> 1;
317
+ for (let i = 0; i < evictCount; i++) {
318
+ seenToolIDsSet.delete(seenToolIDsKeys[i]);
319
+ }
320
+ seenToolIDsKeys.splice(0, evictCount);
321
+ }
322
+ }
323
+ const toolNameMapMax = 2000;
303
324
  let toolNameMap = new Map(); // toolID -> toolName
325
+ let toolNameMapKeys = [];
326
+
327
+ function toolNameMapSet(toolID, toolName) {
328
+ if (toolNameMap.has(toolID)) return;
329
+ toolNameMap.set(toolID, toolName);
330
+ toolNameMapKeys.push(toolID);
331
+ if (toolNameMapKeys.length > toolNameMapMax) {
332
+ const evictCount = toolNameMapKeys.length >> 1;
333
+ for (let i = 0; i < evictCount; i++) {
334
+ toolNameMap.delete(toolNameMapKeys[i]);
335
+ }
336
+ toolNameMapKeys.splice(0, evictCount);
337
+ }
338
+ }
304
339
  let filters = new Map();
305
340
 
306
341
  let showThinking = true;
@@ -314,6 +349,16 @@ let renderPending = false;
314
349
  let totalInput = 0, totalOutput = 0, totalCacheCreate = 0, totalCacheRead = 0;
315
350
  let contextData = {};
316
351
 
352
+ function computeTokensFromContext() {
353
+ totalInput = 0; totalOutput = 0; totalCacheCreate = 0; totalCacheRead = 0;
354
+ for (const ctx of Object.values(contextData)) {
355
+ totalInput += ctx.inputTokens || 0;
356
+ totalOutput += ctx.outputTokens || 0;
357
+ totalCacheCreate += ctx.cacheCreationTokens || 0;
358
+ totalCacheRead += ctx.cacheReadTokens || 0;
359
+ }
360
+ }
361
+
317
362
  let collapseAfter = 0;
318
363
  let collapseTimer = null;
319
364
  let activeRefreshTimer = null;
@@ -349,7 +394,7 @@ marked.setOptions({ renderer: mdRenderer, breaks: true, gfm: true });
349
394
 
350
395
  function mdRender(text) {
351
396
  try {
352
- return marked.parse(text);
397
+ return DOMPurify.sanitize(marked.parse(text));
353
398
  } catch {
354
399
  return esc(text);
355
400
  }
@@ -367,10 +412,16 @@ function connect() {
367
412
  sessionInfo.textContent = 'Connected';
368
413
  lastMsgTime = Date.now();
369
414
  reconnectDelay = 1000;
415
+ reconnectAttempts = 0;
370
416
  startStaleCheck();
371
417
  startActiveRefresh();
372
418
  };
373
419
  ws.onclose = () => {
420
+ reconnectAttempts++;
421
+ if (reconnectAttempts >= MaxReconnectAttempts) {
422
+ sessionInfo.textContent = 'Disconnected. Please refresh to reconnect.';
423
+ return;
424
+ }
374
425
  sessionInfo.textContent = 'Disconnected, reconnecting...';
375
426
  stopStaleCheck();
376
427
  if (activeRefreshTimer) { clearInterval(activeRefreshTimer); activeRefreshTimer = null; }
@@ -544,10 +595,8 @@ function handleItemBatch(items) {
544
595
  }
545
596
 
546
597
  function pushItem(item) {
547
- if (item.inputTokens > 0) totalInput += item.inputTokens;
548
- if (item.outputTokens > 0) totalOutput += item.outputTokens;
549
- if (item.cacheCreationTokens > 0) totalCacheCreate += item.cacheCreationTokens;
550
- if (item.cacheReadTokens > 0) totalCacheRead += item.cacheReadTokens;
598
+ // Token counts are sourced exclusively from server context messages
599
+ // to avoid divergence between frontend accumulation and server tracking
551
600
 
552
601
  if (item.model) {
553
602
  const s = sessions.find(s => s.id === item.sessionID);
@@ -555,14 +604,13 @@ function pushItem(item) {
555
604
  }
556
605
 
557
606
  if (item.type === 'tool_input' && item.toolID && item.toolName) {
558
- toolNameMap.set(item.toolID, item.toolName);
607
+ toolNameMapSet(item.toolID, item.toolName);
559
608
  }
560
609
 
561
610
  if (item.toolID) {
562
611
  const key = `${item.toolID}:${item.type}`;
563
- if (seenToolIDs.has(key)) return;
564
- seenToolIDs.add(key);
565
- if (seenToolIDs.size > 5000) seenToolIDs.clear();
612
+ if (seenToolIDsHas(key)) return;
613
+ seenToolIDsAdd(key);
566
614
  }
567
615
 
568
616
  streamItems.push(item);
@@ -639,7 +687,6 @@ function getNodeHTML(node, idx) {
639
687
  if (node.type === 'session') {
640
688
  const displayName = node.title || folderName(node.projectPath) || node.id.slice(0, 14);
641
689
  const parts = [];
642
- if (node.folder) parts.push(`📂 ${esc(node.folder)}`);
643
690
  if (node.model) parts.push(`🧠 ${esc(node.model)}`);
644
691
  const activeDot = isSessionActive(node) ? '<span class="active-dot on">🟢</span>' : '<span class="active-dot off">⚪</span>';
645
692
  const subInfo = parts.length > 0 ? ` <span style="color:#6b7280;font-size:10px">${parts.join(' · ')}</span>` : '';
@@ -881,6 +928,7 @@ function refreshButtons() {
881
928
  sessionInfo.textContent = info;
882
929
 
883
930
  // Token info
931
+ computeTokensFromContext();
884
932
  let tokStr = '';
885
933
  if (totalInput > 0 || totalOutput > 0) {
886
934
  tokStr = `${fmtTok(totalInput)} in / ${fmtTok(totalOutput)} out`;
@@ -1155,7 +1203,7 @@ function folderName(projectPath) {
1155
1203
  }
1156
1204
 
1157
1205
  function esc(s) {
1158
- return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
1206
+ return (s ?? '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#x27;');
1159
1207
  }
1160
1208
 
1161
1209
  function fmtDur(ms) {
@@ -0,0 +1,10 @@
1
+ pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
2
+ Theme: GitHub Dark
3
+ Description: Dark theme as seen on github.com
4
+ Author: github.com
5
+ Maintainer: @Hirse
6
+ Updated: 2021-05-15
7
+
8
+ Outdated base version: https://github.com/primer/github-syntax-dark
9
+ Current colors taken from GitHub's CSS
10
+ */.hljs{color:#c9d1d9;background:#0d1117}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#ff7b72}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#d2a8ff}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#79c0ff}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#a5d6ff}.hljs-built_in,.hljs-symbol{color:#ffa657}.hljs-code,.hljs-comment,.hljs-formula{color:#8b949e}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#7ee787}.hljs-subst{color:#c9d1d9}.hljs-section{color:#1f6feb;font-weight:700}.hljs-bullet{color:#f2cc60}.hljs-emphasis{color:#c9d1d9;font-style:italic}.hljs-strong{color:#c9d1d9;font-weight:700}.hljs-addition{color:#aff5b4;background-color:#033a16}.hljs-deletion{color:#ffdcd7;background-color:#67060c}