claude-code-watch 0.0.9 → 0.0.11
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 +3 -21
- package/package.json +2 -2
- package/public/favicon.svg +17 -0
- package/public/index.html +7 -7
- package/src/cli-helpers.js +26 -0
- package/src/parser/parser.js +9 -4
- package/src/server/server.js +29 -12
- package/src/watcher/watcher.js +92 -35
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() {
|
|
@@ -128,19 +123,6 @@ async function runUpdate() {
|
|
|
128
123
|
}
|
|
129
124
|
}
|
|
130
125
|
|
|
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
126
|
async function main() {
|
|
145
127
|
const args = process.argv.slice(2);
|
|
146
128
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-watch",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.11",
|
|
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,7 +387,6 @@ 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)
|
|
@@ -588,8 +588,8 @@ function handleNewBgTask(payload) {
|
|
|
588
588
|
function handleSessionRemoved(payload) {
|
|
589
589
|
const idx = sessions.findIndex(s => s.id === payload.sessionID);
|
|
590
590
|
if (idx >= 0) {
|
|
591
|
-
|
|
592
|
-
|
|
591
|
+
sessions.splice(idx, 1);
|
|
592
|
+
sessionsMap.delete(payload.sessionID);
|
|
593
593
|
}
|
|
594
594
|
updateFilters();
|
|
595
595
|
rebuildNodes();
|
|
@@ -733,7 +733,7 @@ function getNodeHTML(node, idx) {
|
|
|
733
733
|
return `<div class="tree-row${selClass ? ' selected' : ''}">
|
|
734
734
|
<div class="tree-node" onclick="treeClick(${idx})" data-idx="${idx}">
|
|
735
735
|
<span class="tree-prefix">${treePrefix(node)}</span>${activeDot} ${node.collapsed ? '▸' : '▾'} ${esc(displayName)}
|
|
736
|
-
${node.collapsed && agentCount > 0 ? `(${agentCount})` : ''}
|
|
736
|
+
${node.collapsed && agentCount > 0 ? `(${esc(String(agentCount))})` : ''}
|
|
737
737
|
${subInfo}
|
|
738
738
|
</div>
|
|
739
739
|
<span class="tree-actions">
|
|
@@ -1112,11 +1112,11 @@ function removeSelectedSession() {
|
|
|
1112
1112
|
if (node.type === 'session') sid = node.id;
|
|
1113
1113
|
else sid = node.sessionID;
|
|
1114
1114
|
if (!sid) return;
|
|
1115
|
-
if (!confirm(`
|
|
1115
|
+
if (!confirm(`Remove session ${sid.slice(0, 12)}...?`)) return;
|
|
1116
1116
|
const idx = sessions.findIndex(s => s.id === sid);
|
|
1117
1117
|
if (idx >= 0) {
|
|
1118
|
-
|
|
1119
|
-
|
|
1118
|
+
sessions.splice(idx, 1);
|
|
1119
|
+
sessionsMap.delete(sid);
|
|
1120
1120
|
}
|
|
1121
1121
|
sendCmd('removeSession', { sessionID: sid });
|
|
1122
1122
|
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
|
@@ -93,11 +93,16 @@ class DashboardServer {
|
|
|
93
93
|
|
|
94
94
|
broadcast(type, payload) {
|
|
95
95
|
const msg = JSON.stringify({ type, payload });
|
|
96
|
+
const toRemove = [];
|
|
96
97
|
for (const ws of this.clients) {
|
|
97
98
|
if (ws.readyState === 1) {
|
|
98
|
-
try { ws.send(msg); } catch {
|
|
99
|
+
try { ws.send(msg); } catch { toRemove.push(ws); }
|
|
99
100
|
}
|
|
100
101
|
}
|
|
102
|
+
for (const ws of toRemove) {
|
|
103
|
+
this.clients.delete(ws);
|
|
104
|
+
try { ws.terminate(); } catch {}
|
|
105
|
+
}
|
|
101
106
|
}
|
|
102
107
|
|
|
103
108
|
sendJSON(res, data, status = 200) {
|
|
@@ -121,7 +126,7 @@ class DashboardServer {
|
|
|
121
126
|
}
|
|
122
127
|
|
|
123
128
|
async handleHTTP(req, res) {
|
|
124
|
-
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
129
|
+
const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
|
|
125
130
|
const p = url.pathname;
|
|
126
131
|
|
|
127
132
|
if (p === '/' || p === '/index.html') {
|
|
@@ -183,21 +188,25 @@ class DashboardServer {
|
|
|
183
188
|
const filePath = params.get('path');
|
|
184
189
|
if (!filePath) { this.sendJSON(res, { error: 'Missing path param' }, 400); return; }
|
|
185
190
|
const resolved = path.resolve(filePath);
|
|
186
|
-
|
|
187
|
-
//
|
|
191
|
+
// Resolve both the user-provided path AND the allowed prefix through realpath
|
|
192
|
+
// to ensure consistent comparison even if homedir contains symlinks
|
|
193
|
+
let realPath;
|
|
194
|
+
let allowedPrefix;
|
|
188
195
|
try {
|
|
189
|
-
const
|
|
196
|
+
const homeReal = await fs.promises.realpath(os.homedir());
|
|
197
|
+
allowedPrefix = path.join(homeReal, '.claude', 'projects');
|
|
198
|
+
realPath = await fs.promises.realpath(resolved);
|
|
190
199
|
if (!realPath.startsWith(allowedPrefix)) {
|
|
191
200
|
this.sendJSON(res, { error: 'Access denied' }, 403);
|
|
192
201
|
return;
|
|
193
202
|
}
|
|
194
203
|
} catch {
|
|
195
|
-
// realpath fails for non-existent files — block them
|
|
204
|
+
// realpath fails for non-existent files or if homedir can't be resolved — block them
|
|
196
205
|
this.sendJSON(res, { error: 'Access denied' }, 403);
|
|
197
206
|
return;
|
|
198
207
|
}
|
|
199
208
|
try {
|
|
200
|
-
const content = await fs.promises.readFile(
|
|
209
|
+
const content = await fs.promises.readFile(realPath, 'utf-8');
|
|
201
210
|
this.sendJSON(res, { content });
|
|
202
211
|
} catch (err) {
|
|
203
212
|
this.sendJSON(res, { error: err.message }, 404);
|
|
@@ -239,11 +248,13 @@ class DashboardServer {
|
|
|
239
248
|
this.broadcast('autoDiscoveryChanged', { enabled: this.watcher.isAutoDiscoveryEnabled() });
|
|
240
249
|
break;
|
|
241
250
|
case 'removeSession':
|
|
242
|
-
|
|
243
|
-
|
|
251
|
+
if (typeof cmd.sessionID === 'string' && cmd.sessionID) {
|
|
252
|
+
this.watcher.removeSession(cmd.sessionID);
|
|
253
|
+
this.broadcast('sessionRemoved', { sessionID: cmd.sessionID });
|
|
254
|
+
}
|
|
244
255
|
break;
|
|
245
256
|
case 'setSkipHistory':
|
|
246
|
-
this.watcher.setSkipHistory(cmd.skip);
|
|
257
|
+
this.watcher.setSkipHistory(cmd.skip === true);
|
|
247
258
|
break;
|
|
248
259
|
case 'getContext':
|
|
249
260
|
this.sendContext(ws);
|
|
@@ -348,12 +359,14 @@ class DashboardServer {
|
|
|
348
359
|
const confirmed = await askYesNo(`Port ${port} is occupied by process(es) ${pids.join(', ')}. Kill them? [y/N] `);
|
|
349
360
|
if (!confirmed) {
|
|
350
361
|
console.error(`Port ${port} is in use. Exiting.`);
|
|
362
|
+
this.stop();
|
|
351
363
|
process.exit(1);
|
|
352
364
|
}
|
|
353
365
|
|
|
366
|
+
const myPid = process.pid;
|
|
354
367
|
for (const pid of pids) {
|
|
355
368
|
const parsedPid = parseInt(pid, 10);
|
|
356
|
-
if (Number.isInteger(parsedPid) && parsedPid >
|
|
369
|
+
if (Number.isInteger(parsedPid) && parsedPid > 1 && parsedPid !== myPid) {
|
|
357
370
|
try {
|
|
358
371
|
if (process.platform === 'win32') {
|
|
359
372
|
cp.execSync(`taskkill /PID ${parsedPid} /F`, { encoding: 'utf-8' });
|
|
@@ -369,7 +382,7 @@ class DashboardServer {
|
|
|
369
382
|
await new Promise(r => setTimeout(r, 3000));
|
|
370
383
|
for (const pid of pids) {
|
|
371
384
|
const parsedPid = parseInt(pid, 10);
|
|
372
|
-
if (Number.isInteger(parsedPid) && parsedPid >
|
|
385
|
+
if (Number.isInteger(parsedPid) && parsedPid > 1 && parsedPid !== myPid) {
|
|
373
386
|
try { process.kill(parsedPid, 0); process.kill(parsedPid, 'SIGKILL'); } catch {}
|
|
374
387
|
}
|
|
375
388
|
}
|
|
@@ -423,9 +436,11 @@ class DashboardServer {
|
|
|
423
436
|
this.server.on('error', (err) => {
|
|
424
437
|
if (err.code === 'EADDRINUSE') {
|
|
425
438
|
console.error(`Port ${this.port} is still in use after attempting to free it. Exiting.`);
|
|
439
|
+
this.stop();
|
|
426
440
|
process.exit(1);
|
|
427
441
|
} else {
|
|
428
442
|
console.error(`Server error: ${err.message}`);
|
|
443
|
+
this.stop();
|
|
429
444
|
process.exit(1);
|
|
430
445
|
}
|
|
431
446
|
});
|
|
@@ -438,6 +453,7 @@ class DashboardServer {
|
|
|
438
453
|
await w.start();
|
|
439
454
|
} catch (err) {
|
|
440
455
|
console.error('Watcher init error:', err.message);
|
|
456
|
+
this.stop();
|
|
441
457
|
process.exit(1);
|
|
442
458
|
}
|
|
443
459
|
|
|
@@ -490,6 +506,7 @@ async function startServer(options = {}) {
|
|
|
490
506
|
}
|
|
491
507
|
|
|
492
508
|
function askYesNo(prompt) {
|
|
509
|
+
if (!process.stdin.isTTY) return Promise.resolve(false);
|
|
493
510
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
494
511
|
return new Promise(resolve => {
|
|
495
512
|
rl.question(prompt, answer => {
|
package/src/watcher/watcher.js
CHANGED
|
@@ -456,8 +456,16 @@ class Watcher extends EventEmitter {
|
|
|
456
456
|
}
|
|
457
457
|
|
|
458
458
|
_handleFsWrite(p) {
|
|
459
|
-
|
|
460
|
-
|
|
459
|
+
let ctx = this.fileContexts.get(p);
|
|
460
|
+
|
|
461
|
+
// If fileContexts is missing (race condition during async session registration),
|
|
462
|
+
// try to infer the session context from the path
|
|
463
|
+
if (!ctx) {
|
|
464
|
+
ctx = this._inferFileContext(p);
|
|
465
|
+
if (!ctx) return;
|
|
466
|
+
// Register it so future events are found directly
|
|
467
|
+
this.fileContexts.set(p, ctx);
|
|
468
|
+
}
|
|
461
469
|
|
|
462
470
|
// Debounce
|
|
463
471
|
const existing = this.debounceTimers.get(p);
|
|
@@ -476,6 +484,28 @@ class Watcher extends EventEmitter {
|
|
|
476
484
|
this.debounceTimers.set(p, timer);
|
|
477
485
|
}
|
|
478
486
|
|
|
487
|
+
_inferFileContext(p) {
|
|
488
|
+
if (!p.endsWith('.jsonl')) return null;
|
|
489
|
+
|
|
490
|
+
// Subagent file: infer sessionID and agentID from path structure
|
|
491
|
+
if (p.includes('/subagents/')) {
|
|
492
|
+
const subagentsDir = path.dirname(p);
|
|
493
|
+
const sessionDir = path.dirname(subagentsDir);
|
|
494
|
+
const sessionID = path.basename(sessionDir);
|
|
495
|
+
const agentID = path.basename(p).replace(/^agent-/, '').replace(/\.jsonl$/, '');
|
|
496
|
+
const session = this.sessions.get(sessionID);
|
|
497
|
+
if (!session) return null;
|
|
498
|
+
return { sessionID, agentID };
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Main session file: infer sessionID from filename
|
|
502
|
+
const basename = path.basename(p);
|
|
503
|
+
const sessionID = basename.replace(/\.jsonl$/, '');
|
|
504
|
+
const session = this.sessions.get(sessionID);
|
|
505
|
+
if (!session) return null;
|
|
506
|
+
return { sessionID, agentID: '' };
|
|
507
|
+
}
|
|
508
|
+
|
|
479
509
|
// =========================================================================
|
|
480
510
|
// New session handlers
|
|
481
511
|
// =========================================================================
|
|
@@ -500,6 +530,12 @@ class Watcher extends EventEmitter {
|
|
|
500
530
|
this.emit('broadcast', 'newAgent', { sessionID: session.id, agentID, agentType });
|
|
501
531
|
}
|
|
502
532
|
|
|
533
|
+
// Read initial data from the new session's files
|
|
534
|
+
if (this.useFsnotify) {
|
|
535
|
+
await this._skipToEndOfFiles(session);
|
|
536
|
+
await this._readSessionFiles(session);
|
|
537
|
+
}
|
|
538
|
+
|
|
503
539
|
// Process any subagent files that arrived before the session was discovered
|
|
504
540
|
const pending = this.pendingSubagents.get(session.id);
|
|
505
541
|
if (pending) {
|
|
@@ -537,6 +573,13 @@ class Watcher extends EventEmitter {
|
|
|
537
573
|
|
|
538
574
|
this._addFileWatch(p, sessionID, agentID);
|
|
539
575
|
this.emit('broadcast', 'newAgent', { sessionID, agentID, agentType });
|
|
576
|
+
|
|
577
|
+
// Read initial data from the new subagent file
|
|
578
|
+
if (this.useFsnotify) {
|
|
579
|
+
const pos = await this._findPositionForLastNLines(p, KeepRecentLines);
|
|
580
|
+
this.filePositions.set(p, pos);
|
|
581
|
+
await this._readFile(p, sessionID, agentID, agentType);
|
|
582
|
+
}
|
|
540
583
|
}
|
|
541
584
|
|
|
542
585
|
async _handleNewToolResultFile(p) {
|
|
@@ -610,6 +653,8 @@ class Watcher extends EventEmitter {
|
|
|
610
653
|
|
|
611
654
|
if (this.useFsnotify) {
|
|
612
655
|
this._registerSessionWatches(c.session);
|
|
656
|
+
await this._skipToEndOfFiles(c.session);
|
|
657
|
+
await this._readSessionFiles(c.session);
|
|
613
658
|
}
|
|
614
659
|
|
|
615
660
|
this.emit('broadcast', 'newSession', { sessionID: c.session.id, projectPath: c.session.projectPath });
|
|
@@ -727,32 +772,42 @@ class Watcher extends EventEmitter {
|
|
|
727
772
|
if (!line.includes('"tool_')) continue;
|
|
728
773
|
|
|
729
774
|
if (line.includes('"tool_use"')) {
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
775
|
+
try {
|
|
776
|
+
var raw = JSON.parse(line);
|
|
777
|
+
var content = raw.message && raw.message.content;
|
|
778
|
+
if (!Array.isArray(content)) continue;
|
|
779
|
+
for (var block of content) {
|
|
780
|
+
if (block.type !== 'tool_use' || !block.id) continue;
|
|
781
|
+
if (session.toolIndex.has(block.id)) continue;
|
|
782
|
+
session.toolIndex.set(block.id, {
|
|
783
|
+
toolName: block.name || '',
|
|
784
|
+
parentAgentID: agentID,
|
|
785
|
+
hasResult: false,
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
} catch { continue; }
|
|
740
789
|
}
|
|
741
790
|
|
|
742
791
|
if (line.includes('"tool_result"')) {
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
792
|
+
try {
|
|
793
|
+
var raw2 = JSON.parse(line);
|
|
794
|
+
var content2 = raw2.message && raw2.message.content;
|
|
795
|
+
if (!Array.isArray(content2)) continue;
|
|
796
|
+
for (var block2 of content2) {
|
|
797
|
+
if (block2.type !== 'tool_result' || !block2.tool_use_id) continue;
|
|
798
|
+
var tid = block2.tool_use_id;
|
|
799
|
+
var existing = session.toolIndex.get(tid);
|
|
800
|
+
if (existing) {
|
|
801
|
+
existing.hasResult = true;
|
|
802
|
+
} else {
|
|
803
|
+
session.toolIndex.set(tid, {
|
|
804
|
+
toolName: '',
|
|
805
|
+
parentAgentID: '',
|
|
806
|
+
hasResult: true,
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
} catch { continue; }
|
|
756
811
|
}
|
|
757
812
|
}
|
|
758
813
|
} catch (err) {
|
|
@@ -914,18 +969,19 @@ class Watcher extends EventEmitter {
|
|
|
914
969
|
newPos = pos;
|
|
915
970
|
// Read in chunks to avoid large buffer allocations for big file deltas
|
|
916
971
|
let carryOver = ''; // incomplete trailing line from previous chunk
|
|
972
|
+
let carryOverBytes = 0; // byte length of carryOver (to avoid re-reading it)
|
|
917
973
|
const buf = Buffer.alloc(MaxReadChunk);
|
|
918
974
|
|
|
919
975
|
while (true) {
|
|
920
976
|
const currentStats = await handle.stat();
|
|
921
|
-
const
|
|
922
|
-
if (
|
|
977
|
+
const readFrom = newPos + carryOverBytes;
|
|
978
|
+
if (readFrom >= currentStats.size) break;
|
|
923
979
|
|
|
924
|
-
const readLen = Math.min(MaxReadChunk, currentStats.size -
|
|
925
|
-
const { bytesRead } = await handle.read(buf, 0, readLen,
|
|
980
|
+
const readLen = Math.min(MaxReadChunk, currentStats.size - readFrom);
|
|
981
|
+
const { bytesRead } = await handle.read(buf, 0, readLen, readFrom);
|
|
926
982
|
if (bytesRead === 0) break;
|
|
927
983
|
|
|
928
|
-
const chunk =
|
|
984
|
+
const chunk = buf.toString('utf-8', 0, bytesRead);
|
|
929
985
|
const combined = carryOver + chunk;
|
|
930
986
|
|
|
931
987
|
// Detect CRLF from first newline in the combined text
|
|
@@ -939,9 +995,11 @@ class Watcher extends EventEmitter {
|
|
|
939
995
|
// Save it as carryOver for the next chunk; don't process it yet.
|
|
940
996
|
if (!chunk.endsWith('\n')) {
|
|
941
997
|
carryOver = rawLines.pop();
|
|
998
|
+
carryOverBytes = Buffer.byteLength(carryOver, 'utf-8');
|
|
942
999
|
} else {
|
|
943
1000
|
// chunk ends with \n — split produces a trailing empty string; clear carryOver
|
|
944
1001
|
carryOver = '';
|
|
1002
|
+
carryOverBytes = 0;
|
|
945
1003
|
}
|
|
946
1004
|
|
|
947
1005
|
let chunkBytes = 0;
|
|
@@ -1169,7 +1227,7 @@ async function _listSessionsFiltered(limit, activeWithin) {
|
|
|
1169
1227
|
|
|
1170
1228
|
const candidates = [];
|
|
1171
1229
|
try {
|
|
1172
|
-
await
|
|
1230
|
+
await _walkDirAsync(claudeDir, (filePath, stats) => {
|
|
1173
1231
|
if (!isMainSessionFile(filePath, stats)) return;
|
|
1174
1232
|
if (activeWithin > 0 && (now - stats.mtimeMs) > activeWithin) return;
|
|
1175
1233
|
candidates.push({ filePath, stats });
|
|
@@ -1196,14 +1254,13 @@ async function _listSessionsFiltered(limit, activeWithin) {
|
|
|
1196
1254
|
return sessions;
|
|
1197
1255
|
}
|
|
1198
1256
|
|
|
1199
|
-
async function _walkDirStatic(dir, callback) {
|
|
1200
|
-
return _walkDirAsync(dir, callback);
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
1257
|
module.exports = {
|
|
1204
1258
|
Watcher,
|
|
1205
1259
|
Session,
|
|
1206
1260
|
BackgroundTask,
|
|
1207
1261
|
listSessions,
|
|
1208
1262
|
listActiveSessions,
|
|
1263
|
+
resolveProjectPath,
|
|
1264
|
+
isMainSessionFile,
|
|
1265
|
+
readAgentType,
|
|
1209
1266
|
};
|