claude-code-watch 0.0.10 → 0.0.12
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/bin/claude-watch.js +78 -80
- package/package.json +2 -2
- package/public/favicon.svg +17 -0
- package/public/index.html +14 -11
- package/src/cli-helpers.js +26 -0
- package/src/parser/parser.js +9 -4
- package/src/server/server.js +42 -15
- package/src/watcher/watcher.js +59 -36
package/bin/claude-watch.js
CHANGED
|
@@ -7,6 +7,7 @@ const cp = require('child_process');
|
|
|
7
7
|
|
|
8
8
|
const { startServer } = require('../src/server/server');
|
|
9
9
|
const { listSessions, listActiveSessions } = require('../src/watcher/watcher');
|
|
10
|
+
const { compareVersions, parseDuration } = require('../src/cli-helpers');
|
|
10
11
|
|
|
11
12
|
const { version: VERSION } = require('../package.json');
|
|
12
13
|
|
|
@@ -41,14 +42,8 @@ ENVIRONMENT:
|
|
|
41
42
|
`);
|
|
42
43
|
}
|
|
43
44
|
|
|
44
|
-
function
|
|
45
|
-
|
|
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;
|
|
45
|
+
function printVersion() {
|
|
46
|
+
console.log(`claude-watch v${VERSION}`);
|
|
52
47
|
}
|
|
53
48
|
|
|
54
49
|
function fetchLatestVersion() {
|
|
@@ -118,9 +113,8 @@ async function runUpdate() {
|
|
|
118
113
|
console.log(` Latest version: v${latest}`);
|
|
119
114
|
console.log(' Running npm install -g claude-code-watch@latest...\n');
|
|
120
115
|
|
|
121
|
-
const { execSync } = require('child_process');
|
|
122
116
|
try {
|
|
123
|
-
execSync('npm install -g claude-code-watch@latest', { stdio: 'inherit' });
|
|
117
|
+
cp.execSync('npm install -g claude-code-watch@latest', { stdio: 'inherit' });
|
|
124
118
|
console.log(`\n Updated to v${latest}. Restart to use the new version.`);
|
|
125
119
|
} catch {
|
|
126
120
|
console.error('\n Update failed. Try manually: npm install -g claude-code-watch@latest');
|
|
@@ -128,19 +122,6 @@ async function runUpdate() {
|
|
|
128
122
|
}
|
|
129
123
|
}
|
|
130
124
|
|
|
131
|
-
function parseDuration(s) {
|
|
132
|
-
const match = s.match(/^(\d+)(ms|s|m|h)$/);
|
|
133
|
-
if (!match) throw new Error(`Invalid duration: ${s}`);
|
|
134
|
-
const val = parseInt(match[1], 10);
|
|
135
|
-
switch (match[2]) {
|
|
136
|
-
case 'ms': return val;
|
|
137
|
-
case 's': return val * 1000;
|
|
138
|
-
case 'm': return val * 60 * 1000;
|
|
139
|
-
case 'h': return val * 3600 * 1000;
|
|
140
|
-
default: throw new Error(`Invalid duration unit: ${match[2]}`);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
125
|
async function main() {
|
|
145
126
|
const args = process.argv.slice(2);
|
|
146
127
|
|
|
@@ -157,9 +138,16 @@ async function main() {
|
|
|
157
138
|
openBrowser: true,
|
|
158
139
|
};
|
|
159
140
|
|
|
160
|
-
//
|
|
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
|
+
|
|
161
148
|
for (let i = 0; i < args.length; i++) {
|
|
162
|
-
|
|
149
|
+
const arg = args[i];
|
|
150
|
+
switch (arg) {
|
|
163
151
|
case '-s':
|
|
164
152
|
options.sessionID = args[++i] || '';
|
|
165
153
|
break;
|
|
@@ -169,7 +157,7 @@ async function main() {
|
|
|
169
157
|
case '-p':
|
|
170
158
|
case '--port': {
|
|
171
159
|
if (i + 1 >= args.length || args[i + 1].startsWith('-')) {
|
|
172
|
-
console.error(`Error: ${
|
|
160
|
+
console.error(`Error: ${arg} requires a port number`);
|
|
173
161
|
process.exit(1);
|
|
174
162
|
}
|
|
175
163
|
const pv = parseInt(args[++i], 10);
|
|
@@ -183,7 +171,7 @@ async function main() {
|
|
|
183
171
|
case '-h':
|
|
184
172
|
case '--host':
|
|
185
173
|
if (i + 1 >= args.length || args[i + 1].startsWith('-')) {
|
|
186
|
-
console.error(`Error: ${
|
|
174
|
+
console.error(`Error: ${arg} requires a host address`);
|
|
187
175
|
process.exit(1);
|
|
188
176
|
}
|
|
189
177
|
options.host = args[++i];
|
|
@@ -214,75 +202,85 @@ async function main() {
|
|
|
214
202
|
case '--no-open':
|
|
215
203
|
options.openBrowser = false;
|
|
216
204
|
break;
|
|
217
|
-
default:
|
|
218
|
-
break;
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// Second pass: execute action flags with fully resolved options
|
|
223
|
-
for (let i = 0; i < args.length; i++) {
|
|
224
|
-
switch (args[i]) {
|
|
225
205
|
case '-l': {
|
|
226
|
-
const
|
|
227
|
-
const
|
|
206
|
+
const next = args[i + 1];
|
|
207
|
+
const v = parseInt(next);
|
|
208
|
+
listSessionsLimit = !isNaN(v) ? v : 10;
|
|
228
209
|
if (!isNaN(v)) i++;
|
|
229
|
-
|
|
230
|
-
if (sessions.length === 0) {
|
|
231
|
-
console.log('No sessions found.');
|
|
232
|
-
} else {
|
|
233
|
-
const now = Date.now();
|
|
234
|
-
for (const s of sessions) {
|
|
235
|
-
const age = Math.round((now - new Date(s.modified).getTime()) / 1000);
|
|
236
|
-
const ageStr = age < 60 ? `${age}s ago` : age < 3600 ? `${Math.floor(age / 60)}m ago` : `${Math.floor(age / 3600)}h ago`;
|
|
237
|
-
const active = s.isActive ? '●' : '○';
|
|
238
|
-
const id = s.id.length > 40 ? s.id.slice(0, 37) + '...' : s.id;
|
|
239
|
-
console.log(`${active} ${id} ${s.projectPath || '?'} ${ageStr}`);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
return;
|
|
210
|
+
break;
|
|
243
211
|
}
|
|
244
212
|
case '-a': {
|
|
245
|
-
const
|
|
246
|
-
const
|
|
247
|
-
if (!isNaN(v)) i++;
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
if (result.length === 0) {
|
|
251
|
-
console.log('No active sessions found.');
|
|
252
|
-
} else {
|
|
253
|
-
const now = Date.now();
|
|
254
|
-
for (const s of result) {
|
|
255
|
-
const age = Math.round((now - new Date(s.modified).getTime()) / 1000);
|
|
256
|
-
const ageStr = age < 60 ? `${age}s ago` : age < 3600 ? `${Math.floor(age / 60)}m ago` : `${Math.floor(age / 3600)}h ago`;
|
|
257
|
-
const id = s.id.length > 40 ? s.id.slice(0, 37) + '...' : s.id;
|
|
258
|
-
console.log(`● ${id} ${s.projectPath || '?'} ${ageStr}`);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
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;
|
|
262
218
|
}
|
|
263
219
|
case '-v':
|
|
264
|
-
|
|
265
|
-
|
|
220
|
+
showVersion = true;
|
|
221
|
+
break;
|
|
266
222
|
case '--help':
|
|
267
|
-
|
|
268
|
-
|
|
223
|
+
showHelp = true;
|
|
224
|
+
break;
|
|
269
225
|
case 'update':
|
|
270
|
-
|
|
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':
|
|
226
|
+
doUpdate = true;
|
|
276
227
|
break;
|
|
277
228
|
default:
|
|
278
|
-
if (
|
|
279
|
-
console.error(`Unknown option: ${
|
|
229
|
+
if (arg.startsWith('-')) {
|
|
230
|
+
console.error(`Unknown option: ${arg}`);
|
|
280
231
|
printHelp();
|
|
281
232
|
process.exit(1);
|
|
282
233
|
}
|
|
283
234
|
}
|
|
284
235
|
}
|
|
285
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
|
+
|
|
286
284
|
checkForUpdate();
|
|
287
285
|
startServer(options);
|
|
288
286
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-watch",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.12",
|
|
4
4
|
"description": "Web-based real-time monitor for Claude Code.",
|
|
5
5
|
"main": "./src/server/server.js",
|
|
6
6
|
"bin": {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"scripts": {
|
|
11
11
|
"start": "node bin/claude-watch.js",
|
|
12
12
|
"dev": "node --watch bin/claude-watch.js --no-open",
|
|
13
|
-
"test": "node --test tests/all.test.js tests/watcher.test.js tests/server.test.js"
|
|
13
|
+
"test": "node --test tests/all.test.js tests/watcher.test.js tests/server.test.js tests/cli.test.js"
|
|
14
14
|
},
|
|
15
15
|
"files": [
|
|
16
16
|
"bin",
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
|
4
|
+
<stop offset="0%" stop-color="#7c3aed"/>
|
|
5
|
+
<stop offset="100%" stop-color="#4c1d95"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
</defs>
|
|
8
|
+
<rect width="32" height="32" rx="6" fill="url(#bg)"/>
|
|
9
|
+
<!-- Eye outline - wide almond, flatter top/bottom -->
|
|
10
|
+
<path d="M4 16 C6 11 10 9 16 9 C22 9 26 11 28 16 C26 21 22 23 16 23 C10 23 6 21 4 16Z" fill="#1f2937" stroke="#c084fc" stroke-width="2"/>
|
|
11
|
+
<!-- Iris - horizontal ellipse -->
|
|
12
|
+
<ellipse cx="16" cy="16" rx="6" ry="4.5" fill="#a855f7" stroke="#e9d5ff" stroke-width="1.5"/>
|
|
13
|
+
<!-- Pupil -->
|
|
14
|
+
<ellipse cx="16" cy="16" rx="2.5" ry="2" fill="#0f0a1a"/>
|
|
15
|
+
<!-- Shine -->
|
|
16
|
+
<circle cx="14" cy="14.5" r="1.2" fill="#f9fafb"/>
|
|
17
|
+
</svg>
|
package/public/index.html
CHANGED
|
@@ -4,6 +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="icon" type="image/svg+xml" href="favicon.svg">
|
|
7
8
|
<link rel="stylesheet" href="vendor/github-dark.min.css">
|
|
8
9
|
<style>
|
|
9
10
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
@@ -386,23 +387,25 @@ const MAX_ITEMS = 3000;
|
|
|
386
387
|
const MAX_LINES = 50;
|
|
387
388
|
let renderedItemCount = 0;
|
|
388
389
|
let needsFullRender = true;
|
|
389
|
-
visibleDirty = true;
|
|
390
390
|
|
|
391
391
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
392
392
|
// Markdown renderer (marked + highlight.js)
|
|
393
393
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
394
394
|
|
|
395
395
|
const mdRenderer = new marked.Renderer();
|
|
396
|
-
mdRenderer.code = function (
|
|
396
|
+
mdRenderer.code = function (codeOrObj, langOrEsc) {
|
|
397
|
+
// marked v4: code(text, lang, escaped) — marked v5+: code({ text, lang })
|
|
398
|
+
const text = typeof codeOrObj === 'object' ? codeOrObj.text : codeOrObj;
|
|
399
|
+
const lang = typeof codeOrObj === 'object' ? codeOrObj.lang : langOrEsc;
|
|
397
400
|
let highlighted;
|
|
398
401
|
if (lang && hljs.getLanguage(lang)) {
|
|
399
402
|
try {
|
|
400
|
-
highlighted = hljs.highlight(
|
|
403
|
+
highlighted = hljs.highlight(text, { language: lang }).value;
|
|
401
404
|
} catch {
|
|
402
|
-
highlighted = hljs.highlightAuto(
|
|
405
|
+
highlighted = hljs.highlightAuto(text).value;
|
|
403
406
|
}
|
|
404
407
|
} else {
|
|
405
|
-
highlighted = hljs.highlightAuto(
|
|
408
|
+
highlighted = hljs.highlightAuto(text).value;
|
|
406
409
|
}
|
|
407
410
|
const langTag = lang ? `<span class="lang-tag">${esc(lang)}</span>` : '';
|
|
408
411
|
return `<div class="code-block-wrapper">
|
|
@@ -588,8 +591,8 @@ function handleNewBgTask(payload) {
|
|
|
588
591
|
function handleSessionRemoved(payload) {
|
|
589
592
|
const idx = sessions.findIndex(s => s.id === payload.sessionID);
|
|
590
593
|
if (idx >= 0) {
|
|
591
|
-
|
|
592
|
-
|
|
594
|
+
sessions.splice(idx, 1);
|
|
595
|
+
sessionsMap.delete(payload.sessionID);
|
|
593
596
|
}
|
|
594
597
|
updateFilters();
|
|
595
598
|
rebuildNodes();
|
|
@@ -733,7 +736,7 @@ function getNodeHTML(node, idx) {
|
|
|
733
736
|
return `<div class="tree-row${selClass ? ' selected' : ''}">
|
|
734
737
|
<div class="tree-node" onclick="treeClick(${idx})" data-idx="${idx}">
|
|
735
738
|
<span class="tree-prefix">${treePrefix(node)}</span>${activeDot} ${node.collapsed ? '▸' : '▾'} ${esc(displayName)}
|
|
736
|
-
${node.collapsed && agentCount > 0 ? `(${agentCount})` : ''}
|
|
739
|
+
${node.collapsed && agentCount > 0 ? `(${esc(String(agentCount))})` : ''}
|
|
737
740
|
${subInfo}
|
|
738
741
|
</div>
|
|
739
742
|
<span class="tree-actions">
|
|
@@ -1112,11 +1115,11 @@ function removeSelectedSession() {
|
|
|
1112
1115
|
if (node.type === 'session') sid = node.id;
|
|
1113
1116
|
else sid = node.sessionID;
|
|
1114
1117
|
if (!sid) return;
|
|
1115
|
-
if (!confirm(`
|
|
1118
|
+
if (!confirm(`Remove session ${sid.slice(0, 12)}...?`)) return;
|
|
1116
1119
|
const idx = sessions.findIndex(s => s.id === sid);
|
|
1117
1120
|
if (idx >= 0) {
|
|
1118
|
-
|
|
1119
|
-
|
|
1121
|
+
sessions.splice(idx, 1);
|
|
1122
|
+
sessionsMap.delete(sid);
|
|
1120
1123
|
}
|
|
1121
1124
|
sendCmd('removeSession', { sessionID: sid });
|
|
1122
1125
|
updateFilters();
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function compareVersions(a, b) {
|
|
4
|
+
var pa = a.split('.').map(Number);
|
|
5
|
+
var pb = b.split('.').map(Number);
|
|
6
|
+
for (var i = 0; i < 3; i++) {
|
|
7
|
+
if ((pa[i] || 0) > (pb[i] || 0)) return 1;
|
|
8
|
+
if ((pa[i] || 0) < (pb[i] || 0)) return -1;
|
|
9
|
+
}
|
|
10
|
+
return 0;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function parseDuration(s) {
|
|
14
|
+
var match = s.match(/^(\d+)(ms|s|m|h)$/);
|
|
15
|
+
if (!match) throw new Error('Invalid duration: ' + s);
|
|
16
|
+
var val = parseInt(match[1], 10);
|
|
17
|
+
switch (match[2]) {
|
|
18
|
+
case 'ms': return val;
|
|
19
|
+
case 's': return val * 1000;
|
|
20
|
+
case 'm': return val * 60 * 1000;
|
|
21
|
+
case 'h': return val * 3600 * 1000;
|
|
22
|
+
default: throw new Error('Invalid duration unit: ' + match[2]);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = { compareVersions, parseDuration };
|
package/src/parser/parser.js
CHANGED
|
@@ -43,7 +43,8 @@ function setDebugAll(val) {
|
|
|
43
43
|
|
|
44
44
|
function agentDisplayName(agentID) {
|
|
45
45
|
if (!agentID) return 'Main';
|
|
46
|
-
|
|
46
|
+
var id = String(agentID);
|
|
47
|
+
return 'Agent-' + id.slice(0, Math.min(AgentIDDisplayLength, id.length));
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
// ============================================================================
|
|
@@ -281,7 +282,7 @@ function diagnosticsBody(diagnostics) {
|
|
|
281
282
|
// ============================================================================
|
|
282
283
|
|
|
283
284
|
function parsePRLink(raw, timestamp) {
|
|
284
|
-
if (
|
|
285
|
+
if (raw.prNumber == null && !raw.prUrl) return [];
|
|
285
286
|
let content;
|
|
286
287
|
if (raw.prRepository && raw.prUrl) {
|
|
287
288
|
content = `PR #${raw.prNumber} ${raw.prRepository} \u2192 ${raw.prUrl}`;
|
|
@@ -441,8 +442,8 @@ function formatToolInput(toolName, input) {
|
|
|
441
442
|
if (inp.path) return `${inp.pattern} in ${inp.path}`;
|
|
442
443
|
return inp.pattern || '';
|
|
443
444
|
case 'Grep':
|
|
444
|
-
if (inp.path) return `/${inp.pattern}/ in ${inp.path}`;
|
|
445
|
-
return `/${inp.pattern}/`;
|
|
445
|
+
if (inp.path) return `/${inp.pattern || ''}/ in ${inp.path}`;
|
|
446
|
+
return `/${inp.pattern || ''}/`;
|
|
446
447
|
case 'WebFetch':
|
|
447
448
|
return inp.prompt || '';
|
|
448
449
|
case 'WebSearch':
|
|
@@ -493,4 +494,8 @@ module.exports = {
|
|
|
493
494
|
contextWindowFor,
|
|
494
495
|
formatTokenCount,
|
|
495
496
|
AgentIDDisplayLength,
|
|
497
|
+
formatToolInput,
|
|
498
|
+
prettyToolName,
|
|
499
|
+
agentDisplayName,
|
|
500
|
+
MAX_TOOL_INPUT_LENGTH,
|
|
496
501
|
};
|
package/src/server/server.js
CHANGED
|
@@ -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;
|
|
@@ -93,9 +95,16 @@ class DashboardServer {
|
|
|
93
95
|
|
|
94
96
|
broadcast(type, payload) {
|
|
95
97
|
const msg = JSON.stringify({ type, payload });
|
|
98
|
+
const toRemove = [];
|
|
96
99
|
for (const ws of this.clients) {
|
|
97
100
|
if (ws.readyState === 1) {
|
|
98
|
-
try { ws.send(msg); } catch {
|
|
101
|
+
try { ws.send(msg); } catch { toRemove.push(ws); }
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
for (const ws of toRemove) {
|
|
105
|
+
this.clients.delete(ws);
|
|
106
|
+
try { ws.terminate(); } catch (err) {
|
|
107
|
+
if (this.debugAll) console.error('[server] terminate error:', err.message);
|
|
99
108
|
}
|
|
100
109
|
}
|
|
101
110
|
}
|
|
@@ -121,7 +130,7 @@ class DashboardServer {
|
|
|
121
130
|
}
|
|
122
131
|
|
|
123
132
|
async handleHTTP(req, res) {
|
|
124
|
-
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
133
|
+
const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
|
|
125
134
|
const p = url.pathname;
|
|
126
135
|
|
|
127
136
|
if (p === '/' || p === '/index.html') {
|
|
@@ -183,21 +192,25 @@ class DashboardServer {
|
|
|
183
192
|
const filePath = params.get('path');
|
|
184
193
|
if (!filePath) { this.sendJSON(res, { error: 'Missing path param' }, 400); return; }
|
|
185
194
|
const resolved = path.resolve(filePath);
|
|
186
|
-
|
|
187
|
-
//
|
|
195
|
+
// Resolve both the user-provided path AND the allowed prefix through realpath
|
|
196
|
+
// to ensure consistent comparison even if homedir contains symlinks
|
|
197
|
+
let realPath;
|
|
198
|
+
let allowedPrefix;
|
|
188
199
|
try {
|
|
189
|
-
const
|
|
200
|
+
const homeReal = await fs.promises.realpath(os.homedir());
|
|
201
|
+
allowedPrefix = path.join(homeReal, '.claude', 'projects');
|
|
202
|
+
realPath = await fs.promises.realpath(resolved);
|
|
190
203
|
if (!realPath.startsWith(allowedPrefix)) {
|
|
191
204
|
this.sendJSON(res, { error: 'Access denied' }, 403);
|
|
192
205
|
return;
|
|
193
206
|
}
|
|
194
207
|
} catch {
|
|
195
|
-
// realpath fails for non-existent files — block them
|
|
208
|
+
// realpath fails for non-existent files or if homedir can't be resolved — block them
|
|
196
209
|
this.sendJSON(res, { error: 'Access denied' }, 403);
|
|
197
210
|
return;
|
|
198
211
|
}
|
|
199
212
|
try {
|
|
200
|
-
const content = await fs.promises.readFile(
|
|
213
|
+
const content = await fs.promises.readFile(realPath, 'utf-8');
|
|
201
214
|
this.sendJSON(res, { content });
|
|
202
215
|
} catch (err) {
|
|
203
216
|
this.sendJSON(res, { error: err.message }, 404);
|
|
@@ -215,7 +228,9 @@ class DashboardServer {
|
|
|
215
228
|
try {
|
|
216
229
|
const cmd = JSON.parse(data.toString('utf-8'));
|
|
217
230
|
this.handleCommand(ws, cmd);
|
|
218
|
-
} catch {
|
|
231
|
+
} catch (err) {
|
|
232
|
+
if (this.debugAll) console.error('[server] WS message error:', err.message);
|
|
233
|
+
}
|
|
219
234
|
});
|
|
220
235
|
|
|
221
236
|
ws.on('close', () => {
|
|
@@ -239,11 +254,13 @@ class DashboardServer {
|
|
|
239
254
|
this.broadcast('autoDiscoveryChanged', { enabled: this.watcher.isAutoDiscoveryEnabled() });
|
|
240
255
|
break;
|
|
241
256
|
case 'removeSession':
|
|
242
|
-
|
|
243
|
-
|
|
257
|
+
if (typeof cmd.sessionID === 'string' && cmd.sessionID) {
|
|
258
|
+
this.watcher.removeSession(cmd.sessionID);
|
|
259
|
+
this.broadcast('sessionRemoved', { sessionID: cmd.sessionID });
|
|
260
|
+
}
|
|
244
261
|
break;
|
|
245
262
|
case 'setSkipHistory':
|
|
246
|
-
this.watcher.setSkipHistory(cmd.skip);
|
|
263
|
+
this.watcher.setSkipHistory(cmd.skip === true);
|
|
247
264
|
break;
|
|
248
265
|
case 'getContext':
|
|
249
266
|
this.sendContext(ws);
|
|
@@ -348,19 +365,23 @@ class DashboardServer {
|
|
|
348
365
|
const confirmed = await askYesNo(`Port ${port} is occupied by process(es) ${pids.join(', ')}. Kill them? [y/N] `);
|
|
349
366
|
if (!confirmed) {
|
|
350
367
|
console.error(`Port ${port} is in use. Exiting.`);
|
|
368
|
+
this.stop();
|
|
351
369
|
process.exit(1);
|
|
352
370
|
}
|
|
353
371
|
|
|
372
|
+
const myPid = process.pid;
|
|
354
373
|
for (const pid of pids) {
|
|
355
374
|
const parsedPid = parseInt(pid, 10);
|
|
356
|
-
if (Number.isInteger(parsedPid) && parsedPid >
|
|
375
|
+
if (Number.isInteger(parsedPid) && parsedPid > 1 && parsedPid !== myPid) {
|
|
357
376
|
try {
|
|
358
377
|
if (process.platform === 'win32') {
|
|
359
378
|
cp.execSync(`taskkill /PID ${parsedPid} /F`, { encoding: 'utf-8' });
|
|
360
379
|
} else {
|
|
361
380
|
process.kill(parsedPid, 'SIGTERM');
|
|
362
381
|
}
|
|
363
|
-
} catch {
|
|
382
|
+
} catch (err) {
|
|
383
|
+
console.error(`[server] Failed to SIGTERM pid ${parsedPid}: ${err.message}`);
|
|
384
|
+
}
|
|
364
385
|
}
|
|
365
386
|
}
|
|
366
387
|
|
|
@@ -369,8 +390,10 @@ class DashboardServer {
|
|
|
369
390
|
await new Promise(r => setTimeout(r, 3000));
|
|
370
391
|
for (const pid of pids) {
|
|
371
392
|
const parsedPid = parseInt(pid, 10);
|
|
372
|
-
if (Number.isInteger(parsedPid) && parsedPid >
|
|
373
|
-
try { process.kill(parsedPid, 0); process.kill(parsedPid, 'SIGKILL'); } catch {
|
|
393
|
+
if (Number.isInteger(parsedPid) && parsedPid > 1 && parsedPid !== myPid) {
|
|
394
|
+
try { process.kill(parsedPid, 0); process.kill(parsedPid, 'SIGKILL'); } catch {
|
|
395
|
+
// Process already gone — nothing to do
|
|
396
|
+
}
|
|
374
397
|
}
|
|
375
398
|
}
|
|
376
399
|
}
|
|
@@ -423,9 +446,11 @@ class DashboardServer {
|
|
|
423
446
|
this.server.on('error', (err) => {
|
|
424
447
|
if (err.code === 'EADDRINUSE') {
|
|
425
448
|
console.error(`Port ${this.port} is still in use after attempting to free it. Exiting.`);
|
|
449
|
+
this.stop();
|
|
426
450
|
process.exit(1);
|
|
427
451
|
} else {
|
|
428
452
|
console.error(`Server error: ${err.message}`);
|
|
453
|
+
this.stop();
|
|
429
454
|
process.exit(1);
|
|
430
455
|
}
|
|
431
456
|
});
|
|
@@ -438,6 +463,7 @@ class DashboardServer {
|
|
|
438
463
|
await w.start();
|
|
439
464
|
} catch (err) {
|
|
440
465
|
console.error('Watcher init error:', err.message);
|
|
466
|
+
this.stop();
|
|
441
467
|
process.exit(1);
|
|
442
468
|
}
|
|
443
469
|
|
|
@@ -490,6 +516,7 @@ async function startServer(options = {}) {
|
|
|
490
516
|
}
|
|
491
517
|
|
|
492
518
|
function askYesNo(prompt) {
|
|
519
|
+
if (!process.stdin.isTTY) return Promise.resolve(false);
|
|
493
520
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
494
521
|
return new Promise(resolve => {
|
|
495
522
|
rl.question(prompt, answer => {
|
package/src/watcher/watcher.js
CHANGED
|
@@ -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)
|
|
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)
|
|
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) {
|
|
@@ -772,32 +786,42 @@ class Watcher extends EventEmitter {
|
|
|
772
786
|
if (!line.includes('"tool_')) continue;
|
|
773
787
|
|
|
774
788
|
if (line.includes('"tool_use"')) {
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
789
|
+
try {
|
|
790
|
+
var raw = JSON.parse(line);
|
|
791
|
+
var content = raw.message && raw.message.content;
|
|
792
|
+
if (!Array.isArray(content)) continue;
|
|
793
|
+
for (var block of content) {
|
|
794
|
+
if (block.type !== 'tool_use' || !block.id) continue;
|
|
795
|
+
if (session.toolIndex.has(block.id)) continue;
|
|
796
|
+
session.toolIndex.set(block.id, {
|
|
797
|
+
toolName: block.name || '',
|
|
798
|
+
parentAgentID: agentID,
|
|
799
|
+
hasResult: false,
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
} catch { continue; }
|
|
785
803
|
}
|
|
786
804
|
|
|
787
805
|
if (line.includes('"tool_result"')) {
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
806
|
+
try {
|
|
807
|
+
var raw2 = JSON.parse(line);
|
|
808
|
+
var content2 = raw2.message && raw2.message.content;
|
|
809
|
+
if (!Array.isArray(content2)) continue;
|
|
810
|
+
for (var block2 of content2) {
|
|
811
|
+
if (block2.type !== 'tool_result' || !block2.tool_use_id) continue;
|
|
812
|
+
var tid = block2.tool_use_id;
|
|
813
|
+
var existing = session.toolIndex.get(tid);
|
|
814
|
+
if (existing) {
|
|
815
|
+
existing.hasResult = true;
|
|
816
|
+
} else {
|
|
817
|
+
session.toolIndex.set(tid, {
|
|
818
|
+
toolName: '',
|
|
819
|
+
parentAgentID: '',
|
|
820
|
+
hasResult: true,
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
} catch { continue; }
|
|
801
825
|
}
|
|
802
826
|
}
|
|
803
827
|
} catch (err) {
|
|
@@ -971,7 +995,7 @@ class Watcher extends EventEmitter {
|
|
|
971
995
|
const { bytesRead } = await handle.read(buf, 0, readLen, readFrom);
|
|
972
996
|
if (bytesRead === 0) break;
|
|
973
997
|
|
|
974
|
-
const chunk =
|
|
998
|
+
const chunk = buf.toString('utf-8', 0, bytesRead);
|
|
975
999
|
const combined = carryOver + chunk;
|
|
976
1000
|
|
|
977
1001
|
// Detect CRLF from first newline in the combined text
|
|
@@ -1217,7 +1241,7 @@ async function _listSessionsFiltered(limit, activeWithin) {
|
|
|
1217
1241
|
|
|
1218
1242
|
const candidates = [];
|
|
1219
1243
|
try {
|
|
1220
|
-
await
|
|
1244
|
+
await _walkDirAsync(claudeDir, (filePath, stats) => {
|
|
1221
1245
|
if (!isMainSessionFile(filePath, stats)) return;
|
|
1222
1246
|
if (activeWithin > 0 && (now - stats.mtimeMs) > activeWithin) return;
|
|
1223
1247
|
candidates.push({ filePath, stats });
|
|
@@ -1244,14 +1268,13 @@ async function _listSessionsFiltered(limit, activeWithin) {
|
|
|
1244
1268
|
return sessions;
|
|
1245
1269
|
}
|
|
1246
1270
|
|
|
1247
|
-
async function _walkDirStatic(dir, callback) {
|
|
1248
|
-
return _walkDirAsync(dir, callback);
|
|
1249
|
-
}
|
|
1250
|
-
|
|
1251
1271
|
module.exports = {
|
|
1252
1272
|
Watcher,
|
|
1253
1273
|
Session,
|
|
1254
1274
|
BackgroundTask,
|
|
1255
1275
|
listSessions,
|
|
1256
1276
|
listActiveSessions,
|
|
1277
|
+
resolveProjectPath,
|
|
1278
|
+
isMainSessionFile,
|
|
1279
|
+
readAgentType,
|
|
1257
1280
|
};
|