claude-code-watch 0.1.4 → 0.2.0
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/package.json +1 -1
- package/public/css/app.css +474 -0
- package/public/index.html +31 -2202
- package/public/js/app.js +490 -0
- package/public/js/shared.js +245 -0
- package/public/js/stream.js +1076 -0
- package/public/js/token.js +480 -0
- package/src/scanner/scanner.js +155 -0
- package/src/server/server.js +169 -11
- package/src/watcher/watcher.js +103 -65
package/src/server/server.js
CHANGED
|
@@ -9,6 +9,7 @@ var readline = require('readline');
|
|
|
9
9
|
var { WebSocketServer } = require('ws');
|
|
10
10
|
var { Watcher, listSessions, listActiveSessions } = require('../watcher/watcher');
|
|
11
11
|
var { setDebugAll, contextWindowFor } = require('../parser/parser');
|
|
12
|
+
var { fullScanTokenUsage } = require('../scanner/scanner');
|
|
12
13
|
|
|
13
14
|
var PACKAGE_VERSION = require('../../package.json').version;
|
|
14
15
|
|
|
@@ -37,6 +38,17 @@ class DashboardServer {
|
|
|
37
38
|
this._contextCleanupTimer = null;
|
|
38
39
|
this._pendingItems = [];
|
|
39
40
|
this._flushTimer = null;
|
|
41
|
+
this._tokenStatsDirty = false;
|
|
42
|
+
|
|
43
|
+
// Incremental last-activity tracking: "sessionID:agentID" → { toolName, content }
|
|
44
|
+
this.lastActivities = new Map();
|
|
45
|
+
|
|
46
|
+
// Time-series token stats: daily aggregation (never cleaned up)
|
|
47
|
+
// Key: "YYYY-MM-DD", value: { messages, input, output, cacheCreation, cacheRead, models: { modelName: { input, output, cacheCreation, cacheRead } } }
|
|
48
|
+
this.dailyStats = new Map();
|
|
49
|
+
|
|
50
|
+
// Hourly distribution: 24-hour array of API call counts (local timezone)
|
|
51
|
+
this.hourlyStats = new Array(24).fill(0);
|
|
40
52
|
|
|
41
53
|
this.server = null;
|
|
42
54
|
this.wss = null;
|
|
@@ -67,6 +79,12 @@ class DashboardServer {
|
|
|
67
79
|
return Date.now();
|
|
68
80
|
}
|
|
69
81
|
|
|
82
|
+
_getDateKey(ts) {
|
|
83
|
+
let d = new Date(ts);
|
|
84
|
+
if (isNaN(d.getTime())) d = new Date();
|
|
85
|
+
return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
|
|
86
|
+
}
|
|
87
|
+
|
|
70
88
|
updateContext(item) {
|
|
71
89
|
const key = this.getCtxKey(item.sessionID, item.agentID);
|
|
72
90
|
let ctx = this.contextMap.get(key);
|
|
@@ -85,6 +103,42 @@ class DashboardServer {
|
|
|
85
103
|
ctx.contextWindow = contextWindowFor(item.model);
|
|
86
104
|
}
|
|
87
105
|
ctx.lastActivity = Math.max(ctx.lastActivity || 0, this.itemTime(item));
|
|
106
|
+
|
|
107
|
+
// ── Time-series aggregation for token stats ──
|
|
108
|
+
// All 4 token fields are summed (incremental for billing/consumption perspective)
|
|
109
|
+
const hasTokens = item.inputTokens || item.outputTokens || item.cacheCreationTokens || item.cacheReadTokens;
|
|
110
|
+
if (hasTokens) {
|
|
111
|
+
const dateKey = this._getDateKey(this.itemTime(item));
|
|
112
|
+
let day = this.dailyStats.get(dateKey);
|
|
113
|
+
if (!day) {
|
|
114
|
+
day = { messages: 0, input: 0, output: 0, cacheCreation: 0, cacheRead: 0, models: {} };
|
|
115
|
+
this.dailyStats.set(dateKey, day);
|
|
116
|
+
}
|
|
117
|
+
day.messages++;
|
|
118
|
+
if (item.inputTokens) day.input += item.inputTokens;
|
|
119
|
+
if (item.outputTokens) day.output += item.outputTokens;
|
|
120
|
+
if (item.cacheCreationTokens) day.cacheCreation += item.cacheCreationTokens;
|
|
121
|
+
if (item.cacheReadTokens) day.cacheRead += item.cacheReadTokens;
|
|
122
|
+
|
|
123
|
+
// Hourly distribution: increment the hour bucket
|
|
124
|
+
const tsDate = new Date(this.itemTime(item));
|
|
125
|
+
if (!isNaN(tsDate.getTime())) {
|
|
126
|
+
this.hourlyStats[tsDate.getHours()]++;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Per-model breakdown within this day
|
|
130
|
+
if (item.model) {
|
|
131
|
+
let m = day.models[item.model];
|
|
132
|
+
if (!m) {
|
|
133
|
+
m = { input: 0, output: 0, cacheCreation: 0, cacheRead: 0 };
|
|
134
|
+
day.models[item.model] = m;
|
|
135
|
+
}
|
|
136
|
+
if (item.inputTokens) m.input += item.inputTokens;
|
|
137
|
+
if (item.outputTokens) m.output += item.outputTokens;
|
|
138
|
+
if (item.cacheCreationTokens) m.cacheCreation += item.cacheCreationTokens;
|
|
139
|
+
if (item.cacheReadTokens) m.cacheRead += item.cacheReadTokens;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
88
142
|
}
|
|
89
143
|
|
|
90
144
|
cleanupContextMap() {
|
|
@@ -112,6 +166,48 @@ class DashboardServer {
|
|
|
112
166
|
return result;
|
|
113
167
|
}
|
|
114
168
|
|
|
169
|
+
getTokenStatsSnapshot() {
|
|
170
|
+
// Convert dailyStats Map to plain object, sorted by date descending
|
|
171
|
+
const daily = {};
|
|
172
|
+
const sortedKeys = [...this.dailyStats.keys()].sort().reverse();
|
|
173
|
+
for (const k of sortedKeys) {
|
|
174
|
+
const d = this.dailyStats.get(k);
|
|
175
|
+
daily[k] = {
|
|
176
|
+
messages: d.messages,
|
|
177
|
+
input: d.input,
|
|
178
|
+
output: d.output,
|
|
179
|
+
cacheCreation: d.cacheCreation,
|
|
180
|
+
cacheRead: d.cacheRead,
|
|
181
|
+
models: d.models,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Compute global totals
|
|
186
|
+
let totalMessages = 0, totalInput = 0, totalOutput = 0, totalCacheCreation = 0, totalCacheRead = 0;
|
|
187
|
+
const modelTotals = {};
|
|
188
|
+
for (const [, d] of this.dailyStats) {
|
|
189
|
+
totalMessages += d.messages;
|
|
190
|
+
totalInput += d.input;
|
|
191
|
+
totalOutput += d.output;
|
|
192
|
+
totalCacheCreation += d.cacheCreation;
|
|
193
|
+
totalCacheRead += d.cacheRead;
|
|
194
|
+
for (const [modelName, m] of Object.entries(d.models)) {
|
|
195
|
+
if (!modelTotals[modelName]) modelTotals[modelName] = { input: 0, output: 0, cacheCreation: 0, cacheRead: 0 };
|
|
196
|
+
modelTotals[modelName].input += m.input;
|
|
197
|
+
modelTotals[modelName].output += m.output;
|
|
198
|
+
modelTotals[modelName].cacheCreation += m.cacheCreation;
|
|
199
|
+
modelTotals[modelName].cacheRead += m.cacheRead;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
totals: { messages: totalMessages, input: totalInput, output: totalOutput, cacheCreation: totalCacheCreation, cacheRead: totalCacheRead, days: this.dailyStats.size },
|
|
205
|
+
modelTotals,
|
|
206
|
+
daily,
|
|
207
|
+
hourly: this.hourlyStats,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
115
211
|
broadcast(type, payload) {
|
|
116
212
|
const msg = JSON.stringify({ type, payload });
|
|
117
213
|
const toRemove = [];
|
|
@@ -137,9 +233,16 @@ class DashboardServer {
|
|
|
137
233
|
const ext = path.extname(filePath).toLowerCase();
|
|
138
234
|
try {
|
|
139
235
|
const data = await fs.promises.readFile(filePath);
|
|
236
|
+
// Vendor files (highlight.js, marked, DOMPurify, CSS) are versioned with the
|
|
237
|
+
// package and rarely change — cache for 1 year. Everything else (index.html,
|
|
238
|
+
// favicon) stays no-cache to ensure users always get the latest.
|
|
239
|
+
const isVendor = filePath.includes('/vendor/');
|
|
240
|
+
const cacheControl = isVendor
|
|
241
|
+
? 'public, max-age=31536000, immutable'
|
|
242
|
+
: 'no-cache, no-store, must-revalidate';
|
|
140
243
|
res.writeHead(200, {
|
|
141
244
|
'Content-Type': MIME[ext] || 'application/octet-stream',
|
|
142
|
-
'Cache-Control':
|
|
245
|
+
'Cache-Control': cacheControl,
|
|
143
246
|
});
|
|
144
247
|
res.end(data);
|
|
145
248
|
} catch {
|
|
@@ -207,6 +310,11 @@ class DashboardServer {
|
|
|
207
310
|
return;
|
|
208
311
|
}
|
|
209
312
|
|
|
313
|
+
if (route === '/token-stats') {
|
|
314
|
+
this.sendJSON(res, this.getTokenStatsSnapshot());
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
210
318
|
if (route === '/task-output') {
|
|
211
319
|
const filePath = params.get('path');
|
|
212
320
|
if (!filePath) { this.sendJSON(res, { error: 'Missing path param' }, 400); return; }
|
|
@@ -262,6 +370,7 @@ class DashboardServer {
|
|
|
262
370
|
this.sendSnapshot(ws);
|
|
263
371
|
this.sendItemBatch(ws);
|
|
264
372
|
this.sendContext(ws);
|
|
373
|
+
this.sendTokenStats(ws);
|
|
265
374
|
this.sendConfig(ws);
|
|
266
375
|
}
|
|
267
376
|
|
|
@@ -294,6 +403,10 @@ class DashboardServer {
|
|
|
294
403
|
try { ws.send(JSON.stringify({ type, payload })); } catch {}
|
|
295
404
|
}
|
|
296
405
|
|
|
406
|
+
sendTokenStats(ws) {
|
|
407
|
+
this.send(ws, 'tokenStats', this.getTokenStatsSnapshot());
|
|
408
|
+
}
|
|
409
|
+
|
|
297
410
|
sendSnapshot(ws) {
|
|
298
411
|
if (!this.watcher) return;
|
|
299
412
|
const sessions = this.watcher.getSessionsSnapshot().map(s => ({
|
|
@@ -301,7 +414,7 @@ class DashboardServer {
|
|
|
301
414
|
projectPath: s.projectPath,
|
|
302
415
|
birthtimeMs: s.birthtimeMs || 0,
|
|
303
416
|
subagents: Object.entries(s.subagentTypes || s.subagents || {}).reduce((acc, [id, type]) => {
|
|
304
|
-
acc[id] = typeof type === 'string' ? type : '';
|
|
417
|
+
acc[id] = { type: typeof type === 'string' ? type : '', birthtimeMs: (s.subagentBirthtimes && s.subagentBirthtimes[id]) || 0 };
|
|
305
418
|
return acc;
|
|
306
419
|
}, {}),
|
|
307
420
|
backgroundTasks: Object.entries(s.backgroundTasks || {}).map(([id, t]) => ({
|
|
@@ -312,15 +425,10 @@ class DashboardServer {
|
|
|
312
425
|
isComplete: t.isComplete,
|
|
313
426
|
})),
|
|
314
427
|
}));
|
|
315
|
-
//
|
|
428
|
+
// Use incrementally maintained lastActivities map (O(1) instead of O(itemBuffer))
|
|
316
429
|
const lastActivities = {};
|
|
317
|
-
for (const
|
|
318
|
-
|
|
319
|
-
if (item.type === 'user_text') {
|
|
320
|
-
lastActivities[actKey] = { toolName: '', content: (item.content || '').slice(0, 200) };
|
|
321
|
-
} else if (item.type === 'tool_input' && item.agentID) {
|
|
322
|
-
lastActivities[actKey] = { toolName: item.toolName || '', content: (item.content || '').slice(0, 200) };
|
|
323
|
-
}
|
|
430
|
+
for (const [key, val] of this.lastActivities) {
|
|
431
|
+
lastActivities[key] = val;
|
|
324
432
|
}
|
|
325
433
|
this.send(ws, 'snapshot', {
|
|
326
434
|
sessions,
|
|
@@ -349,6 +457,9 @@ class DashboardServer {
|
|
|
349
457
|
for (const key of this.contextMap.keys()) {
|
|
350
458
|
if (key.startsWith(sessionID + ':')) this.contextMap.delete(key);
|
|
351
459
|
}
|
|
460
|
+
for (const key of this.lastActivities.keys()) {
|
|
461
|
+
if (key.startsWith(sessionID + ':')) this.lastActivities.delete(key);
|
|
462
|
+
}
|
|
352
463
|
});
|
|
353
464
|
|
|
354
465
|
const FLUSH_BATCH_LIMIT = 50;
|
|
@@ -358,13 +469,32 @@ class DashboardServer {
|
|
|
358
469
|
this.itemBuffer = this.itemBuffer.slice(-MAX_ITEM_BUFFER);
|
|
359
470
|
}
|
|
360
471
|
this.updateContext(item);
|
|
472
|
+
|
|
473
|
+
// Incrementally track last activity per agent
|
|
474
|
+
if (item.type === 'user_text') {
|
|
475
|
+
const actKey = item.sessionID + ':' + (item.agentID || '');
|
|
476
|
+
this.lastActivities.set(actKey, { toolName: '', content: (item.content || '').slice(0, 200) });
|
|
477
|
+
} else if (item.type === 'tool_input' && item.agentID) {
|
|
478
|
+
const actKey = item.sessionID + ':' + item.agentID;
|
|
479
|
+
this.lastActivities.set(actKey, { toolName: item.toolName || '', content: (item.content || '').slice(0, 200) });
|
|
480
|
+
}
|
|
481
|
+
|
|
361
482
|
this._pendingItems.push(item);
|
|
483
|
+
// Track if any item in this batch has token data (for tokenStats broadcast)
|
|
484
|
+
if (item.inputTokens || item.outputTokens || item.cacheCreationTokens || item.cacheReadTokens) {
|
|
485
|
+
this._tokenStatsDirty = true;
|
|
486
|
+
}
|
|
362
487
|
if (this._pendingItems.length >= FLUSH_BATCH_LIMIT) {
|
|
363
488
|
// Batch size hit limit — flush immediately
|
|
364
489
|
if (this._flushTimer) { clearTimeout(this._flushTimer); this._flushTimer = null; }
|
|
365
490
|
const batch = this._pendingItems;
|
|
366
491
|
this._pendingItems = [];
|
|
367
492
|
this.broadcast('itemBatch', batch);
|
|
493
|
+
this.broadcast('context', this.getContextSnapshot());
|
|
494
|
+
if (this._tokenStatsDirty) {
|
|
495
|
+
this._tokenStatsDirty = false;
|
|
496
|
+
this.broadcast('tokenStats', this.getTokenStatsSnapshot());
|
|
497
|
+
}
|
|
368
498
|
} else if (!this._flushTimer) {
|
|
369
499
|
this._flushTimer = setTimeout(() => {
|
|
370
500
|
this._flushTimer = null;
|
|
@@ -375,6 +505,11 @@ class DashboardServer {
|
|
|
375
505
|
} else if (batch.length > 1) {
|
|
376
506
|
this.broadcast('itemBatch', batch);
|
|
377
507
|
}
|
|
508
|
+
this.broadcast('context', this.getContextSnapshot());
|
|
509
|
+
if (this._tokenStatsDirty) {
|
|
510
|
+
this._tokenStatsDirty = false;
|
|
511
|
+
this.broadcast('tokenStats', this.getTokenStatsSnapshot());
|
|
512
|
+
}
|
|
378
513
|
}, 50);
|
|
379
514
|
}
|
|
380
515
|
});
|
|
@@ -497,6 +632,29 @@ class DashboardServer {
|
|
|
497
632
|
}
|
|
498
633
|
});
|
|
499
634
|
|
|
635
|
+
// ── Full-scan historical JSONL files for token stats ──
|
|
636
|
+
// This runs BEFORE watcher starts, scanning ALL files regardless of age
|
|
637
|
+
console.log(' Scanning historical token data...');
|
|
638
|
+
try {
|
|
639
|
+
const scanned = await fullScanTokenUsage((done, total) => {
|
|
640
|
+
if (total > 0 && (done % 100 === 0 || done === total)) {
|
|
641
|
+
console.log(` Scanned ${done}/${total} files...`);
|
|
642
|
+
}
|
|
643
|
+
});
|
|
644
|
+
// Merge scanned data into this.dailyStats and this.hourlyStats
|
|
645
|
+
for (const [dateStr, day] of scanned.dailyStats) {
|
|
646
|
+
this.dailyStats.set(dateStr, day);
|
|
647
|
+
}
|
|
648
|
+
for (let h = 0; h < 24; h++) {
|
|
649
|
+
this.hourlyStats[h] += scanned.hourlyStats[h];
|
|
650
|
+
}
|
|
651
|
+
const totalDays = this.dailyStats.size;
|
|
652
|
+
const totalMsgs = [...this.dailyStats.values()].reduce((s, d) => s + d.messages, 0);
|
|
653
|
+
console.log(` Token scan complete: ${totalDays} days, ${totalMsgs.toLocaleString()} messages`);
|
|
654
|
+
} catch (err) {
|
|
655
|
+
console.error(' Token scan error (non-critical, continuing):', err.message);
|
|
656
|
+
}
|
|
657
|
+
|
|
500
658
|
const w = this.setupWatcher(watcherOpts);
|
|
501
659
|
|
|
502
660
|
try {
|
|
@@ -581,4 +739,4 @@ function askYesNo(prompt) {
|
|
|
581
739
|
});
|
|
582
740
|
}
|
|
583
741
|
|
|
584
|
-
module.exports = { DashboardServer, startServer };
|
|
742
|
+
module.exports = { DashboardServer, startServer };
|
package/src/watcher/watcher.js
CHANGED
|
@@ -134,6 +134,7 @@ class Session {
|
|
|
134
134
|
this.birthtimeMs = birthtimeMs || 0;
|
|
135
135
|
this.subagents = {}; // agentID -> file path
|
|
136
136
|
this.subagentTypes = {}; // agentID -> agentType
|
|
137
|
+
this.subagentBirthtimes = {}; // agentID -> birthtimeMs
|
|
137
138
|
this.backgroundTasks = {}; // toolID -> BackgroundTask
|
|
138
139
|
this.toolIndex = new Map(); // toolID -> { toolName, parentAgentID, hasResult }
|
|
139
140
|
this.toolIndexPopulated = false;
|
|
@@ -275,6 +276,10 @@ class Watcher extends EventEmitter {
|
|
|
275
276
|
if (agentType) {
|
|
276
277
|
session.subagentTypes[agentID] = agentType;
|
|
277
278
|
}
|
|
279
|
+
try {
|
|
280
|
+
const agentStats = await fsp.stat(jsonlPath);
|
|
281
|
+
session.subagentBirthtimes[agentID] = agentStats.birthtimeMs || agentStats.mtimeMs || 0;
|
|
282
|
+
} catch {}
|
|
278
283
|
}
|
|
279
284
|
}
|
|
280
285
|
} catch (err) {
|
|
@@ -312,7 +317,7 @@ class Watcher extends EventEmitter {
|
|
|
312
317
|
// Broadcast so connected clients learn about the new session
|
|
313
318
|
this.emit('broadcast', 'newSession', { sessionID: session.id, projectPath: session.projectPath, birthtimeMs: session.birthtimeMs });
|
|
314
319
|
for (const [agentID, agentType] of Object.entries(session.subagentTypes)) {
|
|
315
|
-
this.emit('broadcast', 'newAgent', { sessionID: session.id, agentID, agentType });
|
|
320
|
+
this.emit('broadcast', 'newAgent', { sessionID: session.id, agentID, agentType, birthtimeMs: session.subagentBirthtimes[agentID] || 0 });
|
|
316
321
|
}
|
|
317
322
|
|
|
318
323
|
const pending = this.pendingSubagents.get(session.id);
|
|
@@ -589,7 +594,7 @@ class Watcher extends EventEmitter {
|
|
|
589
594
|
|
|
590
595
|
// Broadcast pre-existing subagents to frontend
|
|
591
596
|
for (const [agentID, agentType] of Object.entries(session.subagentTypes)) {
|
|
592
|
-
this.emit('broadcast', 'newAgent', { sessionID: session.id, agentID, agentType });
|
|
597
|
+
this.emit('broadcast', 'newAgent', { sessionID: session.id, agentID, agentType, birthtimeMs: session.subagentBirthtimes[agentID] || 0 });
|
|
593
598
|
}
|
|
594
599
|
|
|
595
600
|
// Read initial data from the new session's files
|
|
@@ -636,9 +641,13 @@ class Watcher extends EventEmitter {
|
|
|
636
641
|
|
|
637
642
|
session.subagents[agentID] = p;
|
|
638
643
|
if (agentType) session.subagentTypes[agentID] = agentType;
|
|
644
|
+
try {
|
|
645
|
+
const agentStats = await fsp.stat(p);
|
|
646
|
+
session.subagentBirthtimes[agentID] = agentStats.birthtimeMs || agentStats.mtimeMs || 0;
|
|
647
|
+
} catch {}
|
|
639
648
|
|
|
640
649
|
this._addFileWatch(p, sessionID, agentID);
|
|
641
|
-
this.emit('broadcast', 'newAgent', { sessionID, agentID, agentType });
|
|
650
|
+
this.emit('broadcast', 'newAgent', { sessionID, agentID, agentType, birthtimeMs: session.subagentBirthtimes[agentID] || 0 });
|
|
642
651
|
|
|
643
652
|
// Read initial data from the new subagent file
|
|
644
653
|
if (this.useFsnotify) {
|
|
@@ -726,7 +735,7 @@ class Watcher extends EventEmitter {
|
|
|
726
735
|
this.emit('broadcast', 'newSession', { sessionID: c.session.id, projectPath: c.session.projectPath, birthtimeMs: c.session.birthtimeMs });
|
|
727
736
|
|
|
728
737
|
for (const [agentID, agentType] of Object.entries(c.session.subagentTypes)) {
|
|
729
|
-
this.emit('broadcast', 'newAgent', { sessionID: c.session.id, agentID, agentType });
|
|
738
|
+
this.emit('broadcast', 'newAgent', { sessionID: c.session.id, agentID, agentType, birthtimeMs: c.session.subagentBirthtimes[agentID] || 0 });
|
|
730
739
|
}
|
|
731
740
|
|
|
732
741
|
const pending = this.pendingSubagents.get(c.session.id);
|
|
@@ -754,8 +763,12 @@ class Watcher extends EventEmitter {
|
|
|
754
763
|
const agentType = await readAgentType(agentPath);
|
|
755
764
|
session.subagents[agentID] = agentPath;
|
|
756
765
|
if (agentType) session.subagentTypes[agentID] = agentType;
|
|
766
|
+
try {
|
|
767
|
+
const agentStats = await fsp.stat(agentPath);
|
|
768
|
+
session.subagentBirthtimes[agentID] = agentStats.birthtimeMs || agentStats.mtimeMs || 0;
|
|
769
|
+
} catch {}
|
|
757
770
|
|
|
758
|
-
this.emit('broadcast', 'newAgent', { sessionID: session.id, agentID, agentType });
|
|
771
|
+
this.emit('broadcast', 'newAgent', { sessionID: session.id, agentID, agentType, birthtimeMs: session.subagentBirthtimes[agentID] || 0 });
|
|
759
772
|
}
|
|
760
773
|
}
|
|
761
774
|
|
|
@@ -828,59 +841,66 @@ class Watcher extends EventEmitter {
|
|
|
828
841
|
...Object.entries(session.subagents).map(([id, p]) => ({ path: p, agentID: id })),
|
|
829
842
|
];
|
|
830
843
|
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
for await (const line of rl) {
|
|
838
|
-
if (!line.includes('"tool_')) continue;
|
|
839
|
-
|
|
840
|
-
if (line.includes('"tool_use"')) {
|
|
841
|
-
try {
|
|
842
|
-
var raw = JSON.parse(line);
|
|
843
|
-
var content = raw.message && raw.message.content;
|
|
844
|
-
if (!Array.isArray(content)) continue;
|
|
845
|
-
for (var block of content) {
|
|
846
|
-
if (block.type !== 'tool_use' || !block.id) continue;
|
|
847
|
-
if (session.toolIndex.has(block.id)) continue;
|
|
848
|
-
session.toolIndex.set(block.id, {
|
|
849
|
-
toolName: block.name || '',
|
|
850
|
-
parentAgentID: agentID,
|
|
851
|
-
hasResult: false,
|
|
852
|
-
});
|
|
853
|
-
}
|
|
854
|
-
} catch { continue; }
|
|
855
|
-
}
|
|
844
|
+
// Read all files in parallel — each scans its file independently and writes
|
|
845
|
+
// to session.toolIndex (safe under Node single-threaded event loop).
|
|
846
|
+
await Promise.all(files.map(({ path: filePath, agentID }) =>
|
|
847
|
+
this._scanFileForTools(session, filePath, agentID)
|
|
848
|
+
));
|
|
856
849
|
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
850
|
+
session.toolIndexPopulated = true;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
async _scanFileForTools(session, filePath, agentID) {
|
|
854
|
+
if (!filePath) return;
|
|
855
|
+
try {
|
|
856
|
+
const input = fs.createReadStream(filePath, { encoding: 'utf-8' });
|
|
857
|
+
const rl = readline.createInterface({ input, crlfDelay: Infinity });
|
|
858
|
+
|
|
859
|
+
for await (const line of rl) {
|
|
860
|
+
if (!line.includes('"tool_')) continue;
|
|
861
|
+
|
|
862
|
+
if (line.includes('"tool_use"')) {
|
|
863
|
+
try {
|
|
864
|
+
var raw = JSON.parse(line);
|
|
865
|
+
var content = raw.message && raw.message.content;
|
|
866
|
+
if (!Array.isArray(content)) continue;
|
|
867
|
+
for (var block of content) {
|
|
868
|
+
if (block.type !== 'tool_use' || !block.id) continue;
|
|
869
|
+
if (session.toolIndex.has(block.id)) continue;
|
|
870
|
+
session.toolIndex.set(block.id, {
|
|
871
|
+
toolName: block.name || '',
|
|
872
|
+
parentAgentID: agentID,
|
|
873
|
+
hasResult: false,
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
} catch { continue; }
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
if (line.includes('"tool_result"')) {
|
|
880
|
+
try {
|
|
881
|
+
var raw2 = JSON.parse(line);
|
|
882
|
+
var content2 = raw2.message && raw2.message.content;
|
|
883
|
+
if (!Array.isArray(content2)) continue;
|
|
884
|
+
for (var block2 of content2) {
|
|
885
|
+
if (block2.type !== 'tool_result' || !block2.tool_use_id) continue;
|
|
886
|
+
var tid = block2.tool_use_id;
|
|
887
|
+
var existing = session.toolIndex.get(tid);
|
|
888
|
+
if (existing) {
|
|
889
|
+
existing.hasResult = true;
|
|
890
|
+
} else {
|
|
891
|
+
session.toolIndex.set(tid, {
|
|
892
|
+
toolName: '',
|
|
893
|
+
parentAgentID: '',
|
|
894
|
+
hasResult: true,
|
|
895
|
+
});
|
|
875
896
|
}
|
|
876
|
-
}
|
|
877
|
-
}
|
|
897
|
+
}
|
|
898
|
+
} catch { continue; }
|
|
878
899
|
}
|
|
879
|
-
} catch (err) {
|
|
880
|
-
if (this.debug) console.error('[watcher] _populateToolIndex error reading', filePath + ':', err.message);
|
|
881
900
|
}
|
|
901
|
+
} catch (err) {
|
|
902
|
+
if (this.debug) console.error('[watcher] _populateToolIndex error reading', filePath + ':', err.message);
|
|
882
903
|
}
|
|
883
|
-
session.toolIndexPopulated = true;
|
|
884
904
|
}
|
|
885
905
|
|
|
886
906
|
// =========================================================================
|
|
@@ -1087,7 +1107,9 @@ class Watcher extends EventEmitter {
|
|
|
1087
1107
|
rawLine = rawLine.slice(0, -1);
|
|
1088
1108
|
}
|
|
1089
1109
|
|
|
1090
|
-
|
|
1110
|
+
// Fast path: use rawLine.length for ASCII-only lines (1 byte per char).
|
|
1111
|
+
// Only call Buffer.byteLength when multi-byte characters are detected.
|
|
1112
|
+
chunkBytes += (/[^\x00-\x7F]/.test(rawLine) ? Buffer.byteLength(rawLine, 'utf-8') : rawLine.length) + nlLen;
|
|
1091
1113
|
|
|
1092
1114
|
if (!rawLine.trim()) continue;
|
|
1093
1115
|
|
|
@@ -1251,22 +1273,38 @@ class Watcher extends EventEmitter {
|
|
|
1251
1273
|
|
|
1252
1274
|
function createWalkDir(readdirFn) {
|
|
1253
1275
|
const walk = async (dir, callback) => {
|
|
1276
|
+
let entries;
|
|
1254
1277
|
try {
|
|
1255
|
-
|
|
1256
|
-
for (const entry of entries) {
|
|
1257
|
-
const fullPath = path.join(dir, entry.name);
|
|
1258
|
-
if (entry.isDirectory()) {
|
|
1259
|
-
await walk(fullPath, callback);
|
|
1260
|
-
} else {
|
|
1261
|
-
let stats;
|
|
1262
|
-
try { stats = await fsp.stat(fullPath); } catch { continue; }
|
|
1263
|
-
callback(fullPath, stats);
|
|
1264
|
-
}
|
|
1265
|
-
}
|
|
1278
|
+
entries = await readdirFn(dir, { withFileTypes: true });
|
|
1266
1279
|
} catch (err) {
|
|
1267
1280
|
if (err.code !== 'ENOENT' && err.code !== 'EACCES') {
|
|
1268
1281
|
console.error(`[watcher] _walkDir error on ${dir}: ${err.message}`);
|
|
1269
1282
|
}
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
// Separate directories and files for parallel processing
|
|
1287
|
+
const dirs = [];
|
|
1288
|
+
const files = [];
|
|
1289
|
+
for (const entry of entries) {
|
|
1290
|
+
if (entry.isDirectory()) dirs.push(entry);
|
|
1291
|
+
else files.push(entry);
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
// Stat all files in parallel
|
|
1295
|
+
if (files.length > 0) {
|
|
1296
|
+
const results = await Promise.all(files.map(async (entry) => {
|
|
1297
|
+
const fullPath = path.join(dir, entry.name);
|
|
1298
|
+
try { return { path: fullPath, stats: await fsp.stat(fullPath) }; } catch { return null; }
|
|
1299
|
+
}));
|
|
1300
|
+
for (const r of results) {
|
|
1301
|
+
if (r) callback(r.path, r.stats);
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
// Recurse into subdirectories
|
|
1306
|
+
for (const entry of dirs) {
|
|
1307
|
+
await walk(path.join(dir, entry.name), callback);
|
|
1270
1308
|
}
|
|
1271
1309
|
};
|
|
1272
1310
|
return walk;
|