claude-code-watch 0.0.5 → 0.0.7
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 +13 -11
- package/README.zh-CN.md +1 -1
- package/bin/claude-watch.js +108 -2
- package/package.json +5 -4
- package/public/index.html +132 -15
- package/public/vendor/github-dark.min.css +10 -0
- package/public/vendor/github-light.min.css +10 -0
- package/public/vendor/highlight.min.js +1213 -0
- package/public/vendor/marked.min.js +6 -0
- package/public/vendor/purify.min.js +3 -0
- package/src/parser/parser.js +34 -83
- package/src/server/server.js +99 -60
- package/src/watcher/watcher.js +286 -239
package/src/watcher/watcher.js
CHANGED
|
@@ -1,22 +1,24 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
var fs = require('fs');
|
|
4
|
+
var fsp = require('fs/promises');
|
|
5
|
+
var path = require('path');
|
|
6
|
+
var os = require('os');
|
|
7
|
+
var readline = require('readline');
|
|
8
|
+
var chokidar = require('chokidar');
|
|
9
|
+
var { EventEmitter } = require('events');
|
|
10
|
+
var { parseLine, AgentIDDisplayLength } = require('../parser/parser');
|
|
9
11
|
|
|
10
12
|
// ============================================================================
|
|
11
13
|
// Constants
|
|
12
14
|
// ============================================================================
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
var AutoSkipLineThreshold = 100;
|
|
17
|
+
var KeepRecentLines = 10;
|
|
18
|
+
var CleanupInterval = 5 * 60 * 1000;
|
|
19
|
+
var FsnotifyDiscoveryInterval = 60 * 1000;
|
|
20
|
+
var RecentActivityThreshold = 2 * 60 * 1000;
|
|
21
|
+
var DebounceInterval = 50;
|
|
20
22
|
|
|
21
23
|
// ============================================================================
|
|
22
24
|
// Helpers
|
|
@@ -44,7 +46,9 @@ function resolveProjectPath(encoded) {
|
|
|
44
46
|
try {
|
|
45
47
|
fs.accessSync(testPath);
|
|
46
48
|
return `${pathPart}/${dirPart}`;
|
|
47
|
-
} catch {
|
|
49
|
+
} catch {
|
|
50
|
+
// Path doesn't exist, try next combination
|
|
51
|
+
}
|
|
48
52
|
}
|
|
49
53
|
|
|
50
54
|
// Fallback to naive conversion
|
|
@@ -60,10 +64,10 @@ function isMainSessionFile(filePath, stats) {
|
|
|
60
64
|
return true;
|
|
61
65
|
}
|
|
62
66
|
|
|
63
|
-
function readAgentType(jsonlPath) {
|
|
67
|
+
async function readAgentType(jsonlPath) {
|
|
64
68
|
const metaPath = jsonlPath.replace(/\.jsonl$/, '.meta.json');
|
|
65
69
|
try {
|
|
66
|
-
const data =
|
|
70
|
+
const data = await fsp.readFile(metaPath, 'utf-8');
|
|
67
71
|
const meta = JSON.parse(data);
|
|
68
72
|
return meta.agentType || '';
|
|
69
73
|
} catch {
|
|
@@ -102,8 +106,8 @@ class BackgroundTask {
|
|
|
102
106
|
// Watcher class
|
|
103
107
|
// ============================================================================
|
|
104
108
|
|
|
105
|
-
class Watcher extends
|
|
106
|
-
constructor({ sessionID, pollInterval, activeWindow, maxSessions } = {}) {
|
|
109
|
+
class Watcher extends EventEmitter {
|
|
110
|
+
constructor({ sessionID, pollInterval, activeWindow, maxSessions, debugAll } = {}) {
|
|
107
111
|
super();
|
|
108
112
|
this.claudeDir = getClaudeProjectsDir();
|
|
109
113
|
this.pollInterval = pollInterval || 500;
|
|
@@ -120,12 +124,15 @@ class Watcher extends require('events').EventEmitter {
|
|
|
120
124
|
this.fileContexts = new Map();
|
|
121
125
|
this.debounceTimers = new Map();
|
|
122
126
|
this.pendingSubagents = new Map();
|
|
127
|
+
this._readLocks = new Map();
|
|
128
|
+
this._pollRunning = false;
|
|
123
129
|
|
|
124
130
|
// Intervals / timers
|
|
125
131
|
this._cleanupTimer = null;
|
|
126
132
|
this._discoveryTimer = null;
|
|
127
133
|
this._pollTimer = null;
|
|
128
134
|
this._running = false;
|
|
135
|
+
this.debug = debugAll || false;
|
|
129
136
|
|
|
130
137
|
this._sessionID = sessionID || '';
|
|
131
138
|
}
|
|
@@ -201,7 +208,7 @@ class Watcher extends require('events').EventEmitter {
|
|
|
201
208
|
return this.buildSession(mainFile);
|
|
202
209
|
}
|
|
203
210
|
|
|
204
|
-
buildSession(mainFile) {
|
|
211
|
+
async buildSession(mainFile) {
|
|
205
212
|
const base = path.basename(mainFile);
|
|
206
213
|
const id = base.replace(/\.jsonl$/, '');
|
|
207
214
|
const projectDir = path.basename(path.dirname(mainFile));
|
|
@@ -212,19 +219,21 @@ class Watcher extends require('events').EventEmitter {
|
|
|
212
219
|
// Find subagent files
|
|
213
220
|
const subagentDir = path.join(path.dirname(mainFile), id, 'subagents');
|
|
214
221
|
try {
|
|
215
|
-
const entries =
|
|
222
|
+
const entries = await fsp.readdir(subagentDir);
|
|
216
223
|
for (const entry of entries) {
|
|
217
224
|
if (entry.endsWith('.jsonl')) {
|
|
218
225
|
const agentID = entry.replace(/^agent-/, '').replace(/\.jsonl$/, '');
|
|
219
226
|
const jsonlPath = path.join(subagentDir, entry);
|
|
220
227
|
session.subagents[agentID] = jsonlPath;
|
|
221
|
-
const agentType = readAgentType(jsonlPath);
|
|
228
|
+
const agentType = await readAgentType(jsonlPath);
|
|
222
229
|
if (agentType) {
|
|
223
230
|
session.subagentTypes[agentID] = agentType;
|
|
224
231
|
}
|
|
225
232
|
}
|
|
226
233
|
}
|
|
227
|
-
} catch {
|
|
234
|
+
} catch (err) {
|
|
235
|
+
if (this.debug) console.error('[watcher] buildSession subagent scan error:', err.message);
|
|
236
|
+
}
|
|
228
237
|
|
|
229
238
|
return session;
|
|
230
239
|
}
|
|
@@ -237,11 +246,11 @@ class Watcher extends require('events').EventEmitter {
|
|
|
237
246
|
await this._walkDir(this.claudeDir, (filePath, stats) => {
|
|
238
247
|
if (!isMainSessionFile(filePath, stats)) return;
|
|
239
248
|
if (now - stats.mtimeMs > this.activeWindow) return;
|
|
240
|
-
|
|
241
|
-
const session = this.buildSession(filePath);
|
|
242
|
-
discovered.push({ session, modTime: stats.mtimeMs });
|
|
249
|
+
discovered.push({ filePath, modTime: stats.mtimeMs });
|
|
243
250
|
});
|
|
244
|
-
} catch {
|
|
251
|
+
} catch (err) {
|
|
252
|
+
if (this.debug) console.error('[watcher] discoverActiveSessions error:', err.message);
|
|
253
|
+
}
|
|
245
254
|
|
|
246
255
|
// Sort by most recent first
|
|
247
256
|
discovered.sort((a, b) => b.modTime - a.modTime);
|
|
@@ -250,21 +259,22 @@ class Watcher extends require('events').EventEmitter {
|
|
|
250
259
|
}
|
|
251
260
|
|
|
252
261
|
for (const d of discovered) {
|
|
253
|
-
|
|
254
|
-
|
|
262
|
+
const session = await this.buildSession(d.filePath);
|
|
263
|
+
if (!this.sessions.has(session.id)) {
|
|
264
|
+
this.sessions.set(session.id, session);
|
|
255
265
|
|
|
256
266
|
// Broadcast so connected clients learn about the new session
|
|
257
|
-
this.emit('broadcast', 'newSession', { sessionID:
|
|
258
|
-
for (const [agentID, agentType] of Object.entries(
|
|
259
|
-
this.emit('broadcast', 'newAgent', { sessionID:
|
|
267
|
+
this.emit('broadcast', 'newSession', { sessionID: session.id, projectPath: session.projectPath });
|
|
268
|
+
for (const [agentID, agentType] of Object.entries(session.subagentTypes)) {
|
|
269
|
+
this.emit('broadcast', 'newAgent', { sessionID: session.id, agentID, agentType });
|
|
260
270
|
}
|
|
261
271
|
|
|
262
|
-
const pending = this.pendingSubagents.get(
|
|
272
|
+
const pending = this.pendingSubagents.get(session.id);
|
|
263
273
|
if (pending) {
|
|
264
|
-
this.pendingSubagents.delete(
|
|
274
|
+
this.pendingSubagents.delete(session.id);
|
|
265
275
|
for (const sp of pending) {
|
|
266
276
|
const agentID = path.basename(sp).replace(/^agent-/, '').replace(/\.jsonl$/, '');
|
|
267
|
-
this._registerSubagent(
|
|
277
|
+
await this._registerSubagent(session, session.id, agentID, sp);
|
|
268
278
|
}
|
|
269
279
|
}
|
|
270
280
|
}
|
|
@@ -279,14 +289,14 @@ class Watcher extends require('events').EventEmitter {
|
|
|
279
289
|
// Start / Stop
|
|
280
290
|
// =========================================================================
|
|
281
291
|
|
|
282
|
-
start() {
|
|
292
|
+
async start() {
|
|
283
293
|
if (this._running) return;
|
|
284
294
|
this._running = true;
|
|
285
295
|
|
|
286
296
|
if (this.useFsnotify) {
|
|
287
|
-
this._startFsnotify();
|
|
297
|
+
await this._startFsnotify();
|
|
288
298
|
} else {
|
|
289
|
-
this._startPolling();
|
|
299
|
+
await this._startPolling();
|
|
290
300
|
}
|
|
291
301
|
}
|
|
292
302
|
|
|
@@ -309,7 +319,7 @@ class Watcher extends require('events').EventEmitter {
|
|
|
309
319
|
// Chokidar (fsnotify) mode
|
|
310
320
|
// =========================================================================
|
|
311
321
|
|
|
312
|
-
_startFsnotify() {
|
|
322
|
+
async _startFsnotify() {
|
|
313
323
|
// Set up watches
|
|
314
324
|
try {
|
|
315
325
|
if (fs.existsSync(this.claudeDir)) {
|
|
@@ -317,10 +327,12 @@ class Watcher extends require('events').EventEmitter {
|
|
|
317
327
|
} else {
|
|
318
328
|
this._watchAncestor(this.claudeDir);
|
|
319
329
|
}
|
|
320
|
-
} catch {
|
|
330
|
+
} catch (err) {
|
|
331
|
+
if (this.debug) console.error('[watcher] start watch setup error:', err.message);
|
|
332
|
+
}
|
|
321
333
|
|
|
322
334
|
const sessions = this.getSessionsSnapshot();
|
|
323
|
-
this._initializeSessionReading(sessions);
|
|
335
|
+
await this._initializeSessionReading(sessions);
|
|
324
336
|
for (const session of sessions) {
|
|
325
337
|
this._registerSessionWatches(session);
|
|
326
338
|
}
|
|
@@ -360,7 +372,7 @@ class Watcher extends require('events').EventEmitter {
|
|
|
360
372
|
}
|
|
361
373
|
}
|
|
362
374
|
|
|
363
|
-
_addDirectoryWatches(root, maxDepth =
|
|
375
|
+
_addDirectoryWatches(root, maxDepth = 10) {
|
|
364
376
|
const addRecursive = (dir, depth) => {
|
|
365
377
|
if (depth > maxDepth) return;
|
|
366
378
|
try {
|
|
@@ -405,8 +417,12 @@ class Watcher extends require('events').EventEmitter {
|
|
|
405
417
|
try {
|
|
406
418
|
fs.accessSync(this.claudeDir);
|
|
407
419
|
this._addDirectoryWatches(this.claudeDir);
|
|
408
|
-
this.discoverActiveSessions()
|
|
409
|
-
|
|
420
|
+
this.discoverActiveSessions().catch(err => {
|
|
421
|
+
if (this.debug) console.error('[watcher] discoverActiveSessions error:', err.message);
|
|
422
|
+
});
|
|
423
|
+
} catch (err) {
|
|
424
|
+
if (this.debug) console.error('[watcher] _handleFsCreate directory scan error:', err.message);
|
|
425
|
+
}
|
|
410
426
|
}
|
|
411
427
|
return;
|
|
412
428
|
}
|
|
@@ -415,7 +431,7 @@ class Watcher extends require('events').EventEmitter {
|
|
|
415
431
|
if (p.includes('/subagents/')) {
|
|
416
432
|
this._handleNewSubagentFile(p);
|
|
417
433
|
} else if (this.watchActive) {
|
|
418
|
-
this._handleNewSessionFile(p);
|
|
434
|
+
this._handleNewSessionFile(p); // fire-and-forget, session will be discovered on next poll
|
|
419
435
|
}
|
|
420
436
|
return;
|
|
421
437
|
}
|
|
@@ -453,10 +469,14 @@ class Watcher extends require('events').EventEmitter {
|
|
|
453
469
|
if (existing) {
|
|
454
470
|
clearTimeout(existing);
|
|
455
471
|
}
|
|
456
|
-
const timer = setTimeout(() => {
|
|
472
|
+
const timer = setTimeout(async () => {
|
|
457
473
|
this.debounceTimers.delete(p);
|
|
458
474
|
const agentType = this._lookupAgentType(ctx.sessionID, ctx.agentID);
|
|
459
|
-
|
|
475
|
+
try {
|
|
476
|
+
await this._readFile(p, ctx.sessionID, ctx.agentID, agentType);
|
|
477
|
+
} catch (err) {
|
|
478
|
+
this.emit('error', err);
|
|
479
|
+
}
|
|
460
480
|
}, DebounceInterval);
|
|
461
481
|
this.debounceTimers.set(p, timer);
|
|
462
482
|
}
|
|
@@ -465,15 +485,15 @@ class Watcher extends require('events').EventEmitter {
|
|
|
465
485
|
// New session handlers
|
|
466
486
|
// =========================================================================
|
|
467
487
|
|
|
468
|
-
_handleNewSessionFile(p) {
|
|
488
|
+
async _handleNewSessionFile(p) {
|
|
469
489
|
let stats;
|
|
470
|
-
try { stats =
|
|
490
|
+
try { stats = await fsp.stat(p); } catch { return; }
|
|
471
491
|
if (!isMainSessionFile(p, stats)) return;
|
|
472
492
|
|
|
473
493
|
// Only accept sessions within the active window
|
|
474
494
|
if (Date.now() - stats.mtimeMs > this.activeWindow) return;
|
|
475
495
|
|
|
476
|
-
const session = this.buildSession(p);
|
|
496
|
+
const session = await this.buildSession(p);
|
|
477
497
|
if (this.sessions.has(session.id)) return;
|
|
478
498
|
|
|
479
499
|
this.sessions.set(session.id, session);
|
|
@@ -510,11 +530,11 @@ class Watcher extends require('events').EventEmitter {
|
|
|
510
530
|
return;
|
|
511
531
|
}
|
|
512
532
|
|
|
513
|
-
this._registerSubagent(session, sessionID, agentID, p);
|
|
533
|
+
this._registerSubagent(session, sessionID, agentID, p); // fire-and-forget, event handler context
|
|
514
534
|
}
|
|
515
535
|
|
|
516
|
-
_registerSubagent(session, sessionID, agentID, p) {
|
|
517
|
-
const agentType = readAgentType(p);
|
|
536
|
+
async _registerSubagent(session, sessionID, agentID, p) {
|
|
537
|
+
const agentType = await readAgentType(p);
|
|
518
538
|
if (session.subagents[agentID]) return;
|
|
519
539
|
|
|
520
540
|
session.subagents[agentID] = p;
|
|
@@ -524,7 +544,7 @@ class Watcher extends require('events').EventEmitter {
|
|
|
524
544
|
this.emit('broadcast', 'newAgent', { sessionID, agentID, agentType });
|
|
525
545
|
}
|
|
526
546
|
|
|
527
|
-
_handleNewToolResultFile(p) {
|
|
547
|
+
async _handleNewToolResultFile(p) {
|
|
528
548
|
const toolID = path.basename(p).replace(/\.txt$/, '');
|
|
529
549
|
const toolResultsDir = path.dirname(p);
|
|
530
550
|
const sessionDir = path.dirname(toolResultsDir);
|
|
@@ -534,8 +554,8 @@ class Watcher extends require('events').EventEmitter {
|
|
|
534
554
|
if (!session) return;
|
|
535
555
|
if (session.backgroundTasks[toolID]) return;
|
|
536
556
|
|
|
537
|
-
const parentAgentID = this._findBackgroundTaskParent(session, toolID);
|
|
538
|
-
const isComplete = this._isBackgroundTaskComplete(session, toolID);
|
|
557
|
+
const parentAgentID = await this._findBackgroundTaskParent(session, toolID);
|
|
558
|
+
const isComplete = await this._isBackgroundTaskComplete(session, toolID);
|
|
539
559
|
|
|
540
560
|
const task = new BackgroundTask(toolID, parentAgentID, 'Background Task', p, isComplete);
|
|
541
561
|
session.backgroundTasks[toolID] = task;
|
|
@@ -561,22 +581,29 @@ class Watcher extends require('events').EventEmitter {
|
|
|
561
581
|
// Periodic checking (polling fallback + fsnotify discovery)
|
|
562
582
|
// =========================================================================
|
|
563
583
|
|
|
564
|
-
_checkForNewSessions() {
|
|
584
|
+
async _checkForNewSessions() {
|
|
565
585
|
const now = Date.now();
|
|
566
|
-
const
|
|
586
|
+
const fileCandidates = [];
|
|
567
587
|
|
|
568
588
|
try {
|
|
569
|
-
|
|
589
|
+
await this._walkDir(this.claudeDir, (filePath, stats) => {
|
|
570
590
|
if (!isMainSessionFile(filePath, stats)) return;
|
|
571
591
|
if (now - stats.mtimeMs > this.activeWindow) return;
|
|
572
592
|
|
|
573
593
|
const id = path.basename(filePath).replace(/\.jsonl$/, '');
|
|
574
594
|
if (this.sessions.has(id)) return;
|
|
575
595
|
|
|
576
|
-
|
|
577
|
-
candidates.push({ session, modTime: stats.mtimeMs });
|
|
596
|
+
fileCandidates.push({ filePath, modTime: stats.mtimeMs });
|
|
578
597
|
});
|
|
579
|
-
} catch {
|
|
598
|
+
} catch (err) {
|
|
599
|
+
if (this.debug) console.error('[watcher] _checkForNewSessions error:', err.message);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const candidates = [];
|
|
603
|
+
for (const fc of fileCandidates) {
|
|
604
|
+
const session = await this.buildSession(fc.filePath);
|
|
605
|
+
candidates.push({ session, modTime: fc.modTime });
|
|
606
|
+
}
|
|
580
607
|
|
|
581
608
|
candidates.sort((a, b) => b.modTime - a.modTime);
|
|
582
609
|
|
|
@@ -601,16 +628,16 @@ class Watcher extends require('events').EventEmitter {
|
|
|
601
628
|
this.pendingSubagents.delete(c.session.id);
|
|
602
629
|
for (const sp of pending) {
|
|
603
630
|
const agentID = path.basename(sp).replace(/^agent-/, '').replace(/\.jsonl$/, '');
|
|
604
|
-
this._registerSubagent(c.session, c.session.id, agentID, sp);
|
|
631
|
+
await this._registerSubagent(c.session, c.session.id, agentID, sp);
|
|
605
632
|
}
|
|
606
633
|
}
|
|
607
634
|
}
|
|
608
635
|
}
|
|
609
636
|
|
|
610
|
-
_checkForNewSubagents(session) {
|
|
637
|
+
async _checkForNewSubagents(session) {
|
|
611
638
|
const subagentDir = path.join(path.dirname(session.mainFile), session.id, 'subagents');
|
|
612
639
|
let entries;
|
|
613
|
-
try { entries =
|
|
640
|
+
try { entries = await fsp.readdir(subagentDir); } catch { return; }
|
|
614
641
|
|
|
615
642
|
for (const entry of entries) {
|
|
616
643
|
if (!entry.endsWith('.jsonl')) continue;
|
|
@@ -618,18 +645,18 @@ class Watcher extends require('events').EventEmitter {
|
|
|
618
645
|
if (session.subagents[agentID]) continue;
|
|
619
646
|
|
|
620
647
|
const agentPath = path.join(subagentDir, entry);
|
|
621
|
-
const agentType = readAgentType(agentPath);
|
|
648
|
+
const agentType = await readAgentType(agentPath);
|
|
622
649
|
session.subagents[agentID] = agentPath;
|
|
623
650
|
if (agentType) session.subagentTypes[agentID] = agentType;
|
|
624
651
|
|
|
625
|
-
this.emit('newAgent', { sessionID: session.id, agentID, agentType });
|
|
652
|
+
this.emit('broadcast', 'newAgent', { sessionID: session.id, agentID, agentType });
|
|
626
653
|
}
|
|
627
654
|
}
|
|
628
655
|
|
|
629
|
-
_checkForBackgroundTasks(session) {
|
|
656
|
+
async _checkForBackgroundTasks(session) {
|
|
630
657
|
const toolResultsDir = path.join(path.dirname(session.mainFile), session.id, 'tool-results');
|
|
631
658
|
let entries;
|
|
632
|
-
try { entries =
|
|
659
|
+
try { entries = await fsp.readdir(toolResultsDir); } catch { return; }
|
|
633
660
|
|
|
634
661
|
for (const entry of entries) {
|
|
635
662
|
if (!entry.endsWith('.txt')) continue;
|
|
@@ -637,8 +664,8 @@ class Watcher extends require('events').EventEmitter {
|
|
|
637
664
|
if (session.backgroundTasks[toolID]) continue;
|
|
638
665
|
|
|
639
666
|
const outputPath = path.join(toolResultsDir, entry);
|
|
640
|
-
const parentAgentID = this._findBackgroundTaskParent(session, toolID);
|
|
641
|
-
const isComplete = this._isBackgroundTaskComplete(session, toolID);
|
|
667
|
+
const parentAgentID = await this._findBackgroundTaskParent(session, toolID);
|
|
668
|
+
const isComplete = await this._isBackgroundTaskComplete(session, toolID);
|
|
642
669
|
|
|
643
670
|
const task = new BackgroundTask(toolID, parentAgentID, 'Background Task', outputPath, isComplete);
|
|
644
671
|
session.backgroundTasks[toolID] = task;
|
|
@@ -654,27 +681,27 @@ class Watcher extends require('events').EventEmitter {
|
|
|
654
681
|
}
|
|
655
682
|
}
|
|
656
683
|
|
|
657
|
-
_findBackgroundTaskParent(session, toolID) {
|
|
684
|
+
async _findBackgroundTaskParent(session, toolID) {
|
|
658
685
|
const entry = session.toolIndex.get(toolID);
|
|
659
686
|
if (entry) return entry.parentAgentID || '';
|
|
660
687
|
if (!session.toolIndexPopulated) {
|
|
661
|
-
this._populateToolIndex(session);
|
|
688
|
+
await this._populateToolIndex(session);
|
|
662
689
|
}
|
|
663
690
|
const cached = session.toolIndex.get(toolID);
|
|
664
691
|
return cached ? (cached.parentAgentID || '') : '';
|
|
665
692
|
}
|
|
666
693
|
|
|
667
|
-
_isBackgroundTaskComplete(session, toolID) {
|
|
694
|
+
async _isBackgroundTaskComplete(session, toolID) {
|
|
668
695
|
const entry = session.toolIndex.get(toolID);
|
|
669
696
|
if (entry) return entry.hasResult;
|
|
670
697
|
if (!session.toolIndexPopulated) {
|
|
671
|
-
this._populateToolIndex(session);
|
|
698
|
+
await this._populateToolIndex(session);
|
|
672
699
|
}
|
|
673
700
|
const cached = session.toolIndex.get(toolID);
|
|
674
701
|
return cached ? cached.hasResult : false;
|
|
675
702
|
}
|
|
676
703
|
|
|
677
|
-
_populateToolIndex(session) {
|
|
704
|
+
async _populateToolIndex(session) {
|
|
678
705
|
if (session.toolIndexPopulated) return;
|
|
679
706
|
session.toolIndexPopulated = true;
|
|
680
707
|
const files = [
|
|
@@ -685,8 +712,10 @@ class Watcher extends require('events').EventEmitter {
|
|
|
685
712
|
for (const { path: filePath, agentID } of files) {
|
|
686
713
|
if (!filePath) continue;
|
|
687
714
|
try {
|
|
688
|
-
const
|
|
689
|
-
|
|
715
|
+
const input = fs.createReadStream(filePath, { encoding: 'utf-8' });
|
|
716
|
+
const rl = readline.createInterface({ input, crlfDelay: Infinity });
|
|
717
|
+
|
|
718
|
+
for await (const line of rl) {
|
|
690
719
|
if (!line.includes('"tool_')) continue;
|
|
691
720
|
|
|
692
721
|
if (line.includes('"tool_use"')) {
|
|
@@ -718,7 +747,9 @@ class Watcher extends require('events').EventEmitter {
|
|
|
718
747
|
}
|
|
719
748
|
}
|
|
720
749
|
}
|
|
721
|
-
} catch {
|
|
750
|
+
} catch (err) {
|
|
751
|
+
if (this.debug) console.error('[watcher] _populateToolIndex error reading', filePath + ':', err.message);
|
|
752
|
+
}
|
|
722
753
|
}
|
|
723
754
|
}
|
|
724
755
|
|
|
@@ -726,13 +757,16 @@ class Watcher extends require('events').EventEmitter {
|
|
|
726
757
|
// Polling mode
|
|
727
758
|
// =========================================================================
|
|
728
759
|
|
|
729
|
-
_startPolling() {
|
|
760
|
+
async _startPolling() {
|
|
730
761
|
const sessions = this.getSessionsSnapshot();
|
|
731
|
-
this._initializeSessionReading(sessions);
|
|
762
|
+
await this._initializeSessionReading(sessions);
|
|
732
763
|
|
|
733
764
|
this._pollTimer = setInterval(() => {
|
|
734
|
-
if (!this._running) return;
|
|
735
|
-
this.
|
|
765
|
+
if (!this._running || this._pollRunning) return;
|
|
766
|
+
this._pollRunning = true;
|
|
767
|
+
this._handlePollTick()
|
|
768
|
+
.then(() => { this._pollRunning = false; })
|
|
769
|
+
.catch(() => { this._pollRunning = false; });
|
|
736
770
|
}, this.pollInterval);
|
|
737
771
|
|
|
738
772
|
this._cleanupTimer = setInterval(() => {
|
|
@@ -741,29 +775,34 @@ class Watcher extends require('events').EventEmitter {
|
|
|
741
775
|
}, CleanupInterval);
|
|
742
776
|
}
|
|
743
777
|
|
|
744
|
-
_handlePollTick() {
|
|
778
|
+
async _handlePollTick() {
|
|
745
779
|
if (this.watchActive) {
|
|
746
|
-
this._checkForNewSessions();
|
|
747
|
-
}
|
|
748
|
-
for (const session of this.getSessionsSnapshot()) {
|
|
749
|
-
this._checkForNewSubagents(session);
|
|
750
|
-
this._checkForBackgroundTasks(session);
|
|
751
|
-
this._readSessionFiles(session);
|
|
780
|
+
await this._checkForNewSessions();
|
|
752
781
|
}
|
|
782
|
+
const sessions = this.getSessionsSnapshot();
|
|
783
|
+
await Promise.all(sessions.map(s => this._processSessionTick(s)));
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
async _processSessionTick(session) {
|
|
787
|
+
await Promise.all([
|
|
788
|
+
this._checkForNewSubagents(session),
|
|
789
|
+
this._checkForBackgroundTasks(session),
|
|
790
|
+
]);
|
|
791
|
+
await this._readSessionFiles(session);
|
|
753
792
|
}
|
|
754
793
|
|
|
755
794
|
// =========================================================================
|
|
756
795
|
// File reading
|
|
757
796
|
// =========================================================================
|
|
758
797
|
|
|
759
|
-
_initializeSessionReading(sessions) {
|
|
798
|
+
async _initializeSessionReading(sessions) {
|
|
760
799
|
let shouldSkip = this.skipHistory;
|
|
761
800
|
if (!shouldSkip) {
|
|
762
801
|
let totalLines = 0;
|
|
763
802
|
for (const session of sessions) {
|
|
764
|
-
totalLines += this._countFileLines(session.mainFile);
|
|
803
|
+
totalLines += await this._countFileLines(session.mainFile);
|
|
765
804
|
for (const agentPath of Object.values(session.subagents)) {
|
|
766
|
-
totalLines += this._countFileLines(agentPath);
|
|
805
|
+
totalLines += await this._countFileLines(agentPath);
|
|
767
806
|
}
|
|
768
807
|
}
|
|
769
808
|
shouldSkip = totalLines > AutoSkipLineThreshold;
|
|
@@ -771,35 +810,34 @@ class Watcher extends require('events').EventEmitter {
|
|
|
771
810
|
|
|
772
811
|
if (shouldSkip) {
|
|
773
812
|
for (const session of sessions) {
|
|
774
|
-
this._skipToEndOfFiles(session);
|
|
775
|
-
this._readSessionFiles(session);
|
|
813
|
+
await this._skipToEndOfFiles(session);
|
|
814
|
+
await this._readSessionFiles(session);
|
|
776
815
|
}
|
|
777
816
|
} else {
|
|
778
817
|
for (const session of sessions) {
|
|
779
|
-
this._readSessionFiles(session);
|
|
818
|
+
await this._readSessionFiles(session);
|
|
780
819
|
}
|
|
781
820
|
}
|
|
782
821
|
}
|
|
783
822
|
|
|
784
|
-
_skipToEndOfFiles(session) {
|
|
785
|
-
const mainPos = this._findPositionForLastNLines(session.mainFile, KeepRecentLines);
|
|
823
|
+
async _skipToEndOfFiles(session) {
|
|
824
|
+
const mainPos = await this._findPositionForLastNLines(session.mainFile, KeepRecentLines);
|
|
786
825
|
this.filePositions.set(session.mainFile, mainPos);
|
|
787
826
|
|
|
788
827
|
for (const agentPath of Object.values(session.subagents)) {
|
|
789
|
-
const pos = this._findPositionForLastNLines(agentPath, KeepRecentLines);
|
|
828
|
+
const pos = await this._findPositionForLastNLines(agentPath, KeepRecentLines);
|
|
790
829
|
this.filePositions.set(agentPath, pos);
|
|
791
830
|
}
|
|
792
831
|
}
|
|
793
832
|
|
|
794
|
-
_findPositionForLastNLines(filePath, n) {
|
|
833
|
+
async _findPositionForLastNLines(filePath, n) {
|
|
795
834
|
try {
|
|
796
|
-
const stat =
|
|
835
|
+
const stat = await fsp.stat(filePath);
|
|
797
836
|
const fileSize = stat.size;
|
|
798
837
|
if (fileSize === 0) return 0;
|
|
799
838
|
|
|
800
|
-
|
|
839
|
+
const handle = await fsp.open(filePath, 'r');
|
|
801
840
|
try {
|
|
802
|
-
fd = fs.openSync(filePath, 'r');
|
|
803
841
|
const chunkSize = 8192;
|
|
804
842
|
const buf = Buffer.alloc(chunkSize);
|
|
805
843
|
let newlineCount = 0;
|
|
@@ -809,9 +847,9 @@ class Watcher extends require('events').EventEmitter {
|
|
|
809
847
|
while (position > 0 && newlineCount <= n) {
|
|
810
848
|
const readLen = Math.min(chunkSize, position);
|
|
811
849
|
position -= readLen;
|
|
812
|
-
|
|
850
|
+
const { bytesRead } = await handle.read(buf, 0, readLen, position);
|
|
813
851
|
|
|
814
|
-
for (let i =
|
|
852
|
+
for (let i = bytesRead - 1; i >= 0; i--) {
|
|
815
853
|
if (buf[i] === 0x0A) {
|
|
816
854
|
newlineCount++;
|
|
817
855
|
if (newlineCount === n) {
|
|
@@ -825,130 +863,165 @@ class Watcher extends require('events').EventEmitter {
|
|
|
825
863
|
if (newlineCount < n) return 0;
|
|
826
864
|
return lastNewlinePos;
|
|
827
865
|
} finally {
|
|
828
|
-
|
|
866
|
+
await handle.close();
|
|
829
867
|
}
|
|
830
868
|
} catch {
|
|
831
869
|
return 0;
|
|
832
870
|
}
|
|
833
871
|
}
|
|
834
872
|
|
|
835
|
-
_readSessionFiles(session) {
|
|
836
|
-
this._readFile(session.mainFile, session.id, '', '');
|
|
873
|
+
async _readSessionFiles(session) {
|
|
874
|
+
const reads = [this._readFile(session.mainFile, session.id, '', '')];
|
|
837
875
|
for (const [agentID, agentPath] of Object.entries(session.subagents)) {
|
|
838
876
|
const agentType = session.subagentTypes[agentID] || '';
|
|
839
|
-
this._readFile(agentPath, session.id, agentID, agentType);
|
|
877
|
+
reads.push(this._readFile(agentPath, session.id, agentID, agentType));
|
|
840
878
|
}
|
|
879
|
+
await Promise.all(reads);
|
|
841
880
|
}
|
|
842
881
|
|
|
843
|
-
_readFile(filePath, sessionID, agentID, agentType) {
|
|
844
|
-
|
|
882
|
+
async _readFile(filePath, sessionID, agentID, agentType) {
|
|
883
|
+
// Serialize reads per file to prevent concurrent access
|
|
884
|
+
const prev = this._readLocks.get(filePath) || Promise.resolve();
|
|
885
|
+
let resolveLock;
|
|
886
|
+
const lock = new Promise(r => { resolveLock = r; });
|
|
887
|
+
this._readLocks.set(filePath, lock);
|
|
888
|
+
|
|
845
889
|
try {
|
|
846
|
-
|
|
847
|
-
const pos = this.filePositions.get(filePath) || 0;
|
|
848
|
-
const stats = fs.fstatSync(fd);
|
|
849
|
-
if (pos >= stats.size) return;
|
|
850
|
-
|
|
851
|
-
const readLen = stats.size - pos;
|
|
852
|
-
const buf = Buffer.alloc(readLen);
|
|
853
|
-
const bytesRead = fs.readSync(fd, buf, 0, readLen, pos);
|
|
854
|
-
if (bytesRead === 0) return;
|
|
855
|
-
|
|
856
|
-
let newPos = pos;
|
|
857
|
-
const content = bytesRead < readLen ? buf.toString('utf-8', 0, bytesRead) : buf.toString('utf-8');
|
|
858
|
-
const lines = content.split('\n');
|
|
859
|
-
|
|
860
|
-
// Check whether the file has grown since we read it.
|
|
861
|
-
// If yes, the last line may be incomplete (no trailing newline yet).
|
|
862
|
-
let currentSize;
|
|
863
|
-
try { currentSize = fs.fstatSync(fd).size; } catch { currentSize = stats.size; }
|
|
864
|
-
const fileGrew = currentSize > pos + bytesRead;
|
|
865
|
-
|
|
866
|
-
for (let i = 0; i < lines.length; i++) {
|
|
867
|
-
const isLast = i === lines.length - 1;
|
|
868
|
-
const rawLine = lines[i];
|
|
869
|
-
|
|
870
|
-
// Trailing empty string after final newline — already processed
|
|
871
|
-
if (isLast && rawLine === '' && content.endsWith('\n')) {
|
|
872
|
-
continue;
|
|
873
|
-
}
|
|
890
|
+
await prev;
|
|
874
891
|
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
892
|
+
let handle;
|
|
893
|
+
let newPos = this.filePositions.get(filePath) || 0;
|
|
894
|
+
try {
|
|
895
|
+
handle = await fsp.open(filePath, 'r');
|
|
896
|
+
const pos = this.filePositions.get(filePath) || 0;
|
|
897
|
+
const stats = await handle.stat();
|
|
898
|
+
if (pos >= stats.size) { await handle.close(); handle = null; return; }
|
|
899
|
+
|
|
900
|
+
const readLen = stats.size - pos;
|
|
901
|
+
const buf = Buffer.alloc(readLen);
|
|
902
|
+
const { bytesRead } = await handle.read(buf, 0, readLen, pos);
|
|
903
|
+
if (bytesRead === 0) { await handle.close(); handle = null; return; }
|
|
904
|
+
|
|
905
|
+
newPos = pos;
|
|
906
|
+
const content = bytesRead < readLen ? buf.toString('utf-8', 0, bytesRead) : buf.toString('utf-8');
|
|
907
|
+
const rawLines = content.split('\n');
|
|
908
|
+
|
|
909
|
+
// Detect Windows-style CRLF line endings
|
|
910
|
+
const firstNl = content.indexOf('\n');
|
|
911
|
+
const crlf = firstNl > 0 && content[firstNl - 1] === '\r';
|
|
912
|
+
const nlLen = crlf ? 2 : 1;
|
|
913
|
+
|
|
914
|
+
let currentSize;
|
|
915
|
+
try { currentSize = (await handle.stat()).size; } catch { currentSize = stats.size; }
|
|
916
|
+
const fileGrew = currentSize > pos + bytesRead;
|
|
917
|
+
|
|
918
|
+
await handle.close();
|
|
919
|
+
handle = null;
|
|
920
|
+
|
|
921
|
+
for (let i = 0; i < rawLines.length; i++) {
|
|
922
|
+
const isLast = i === rawLines.length - 1;
|
|
923
|
+
let rawLine = rawLines[i];
|
|
924
|
+
|
|
925
|
+
// Trailing empty line after a final newline — skip it, advance position
|
|
926
|
+
if (isLast && rawLine === '' && content.endsWith('\n')) {
|
|
927
|
+
newPos += nlLen;
|
|
928
|
+
continue;
|
|
929
|
+
}
|
|
879
930
|
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
const items = parseLine(rawLine);
|
|
887
|
-
for (const item of items) {
|
|
888
|
-
// Set session ID
|
|
889
|
-
item.sessionID = sessionID;
|
|
890
|
-
|
|
891
|
-
// Set agent ID and name from context
|
|
892
|
-
if (agentID) {
|
|
893
|
-
if (!item.agentID) item.agentID = agentID;
|
|
894
|
-
if (agentType) {
|
|
895
|
-
const idx = agentType.lastIndexOf(':');
|
|
896
|
-
if (idx >= 0 && idx < agentType.length - 1) {
|
|
897
|
-
item.agentName = agentType.slice(idx + 1);
|
|
898
|
-
} else {
|
|
899
|
-
item.agentName = agentType;
|
|
900
|
-
}
|
|
901
|
-
} else if (!item.agentName || item.agentName.startsWith('Agent-')) {
|
|
902
|
-
item.agentName = `Agent-${agentID.slice(0, Math.min(AgentIDDisplayLength, agentID.length))}`;
|
|
903
|
-
}
|
|
931
|
+
// Last line may be incomplete if file grew mid-read or lacks a trailing newline
|
|
932
|
+
if (isLast && !content.endsWith('\n')) {
|
|
933
|
+
// Don't process this line, don't advance position past it.
|
|
934
|
+
// Next read will re-read from the current newPos and get the complete line.
|
|
935
|
+
continue;
|
|
904
936
|
}
|
|
905
937
|
|
|
906
|
-
//
|
|
907
|
-
if (
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
938
|
+
// Strip trailing \r for clean line processing (Windows CRLF)
|
|
939
|
+
if (crlf && rawLine.endsWith('\r')) {
|
|
940
|
+
rawLine = rawLine.slice(0, -1);
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
newPos += Buffer.byteLength(rawLine, 'utf-8');
|
|
944
|
+
newPos += nlLen;
|
|
945
|
+
|
|
946
|
+
if (!rawLine.trim()) continue;
|
|
947
|
+
|
|
948
|
+
const items = parseLine(rawLine);
|
|
949
|
+
for (const item of items) {
|
|
950
|
+
item.sessionID = sessionID;
|
|
951
|
+
|
|
952
|
+
if (agentID) {
|
|
953
|
+
if (!item.agentID) item.agentID = agentID;
|
|
954
|
+
if (agentType) {
|
|
955
|
+
const idx = agentType.lastIndexOf(':');
|
|
956
|
+
if (idx >= 0 && idx < agentType.length - 1) {
|
|
957
|
+
item.agentName = agentType.slice(idx + 1);
|
|
914
958
|
} else {
|
|
915
|
-
|
|
959
|
+
item.agentName = agentType;
|
|
960
|
+
}
|
|
961
|
+
} else if (!item.agentName || item.agentName.startsWith('Agent-')) {
|
|
962
|
+
item.agentName = `Agent-${agentID.slice(0, Math.min(AgentIDDisplayLength, agentID.length))}`;
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
if (item.toolID) {
|
|
967
|
+
const session = this.sessions.get(sessionID);
|
|
968
|
+
if (session) {
|
|
969
|
+
const existing = session.toolIndex.get(item.toolID);
|
|
970
|
+
if (item.type === 'tool_output') {
|
|
971
|
+
if (existing) {
|
|
972
|
+
existing.hasResult = true;
|
|
973
|
+
} else {
|
|
974
|
+
session.toolIndex.set(item.toolID, { toolName: '', parentAgentID: agentID || '', hasResult: true });
|
|
975
|
+
}
|
|
976
|
+
} else if (item.type === 'tool_input' && !existing) {
|
|
977
|
+
session.toolIndex.set(item.toolID, { toolName: item.toolName || '', parentAgentID: agentID || '', hasResult: false });
|
|
916
978
|
}
|
|
917
|
-
} else if (item.type === 'tool_input' && !existing) {
|
|
918
|
-
session.toolIndex.set(item.toolID, { toolName: item.toolName || '', parentAgentID: agentID || '', hasResult: false });
|
|
919
979
|
}
|
|
920
980
|
}
|
|
981
|
+
|
|
982
|
+
this.emit('item', item);
|
|
921
983
|
}
|
|
984
|
+
}
|
|
922
985
|
|
|
923
|
-
|
|
986
|
+
this.filePositions.set(filePath, Math.min(newPos, stats.size));
|
|
987
|
+
} catch (err) {
|
|
988
|
+
if (newPos !== undefined) {
|
|
989
|
+
this.filePositions.set(filePath, newPos);
|
|
990
|
+
}
|
|
991
|
+
this.emit('error', err);
|
|
992
|
+
} finally {
|
|
993
|
+
if (handle) {
|
|
994
|
+
try { await handle.close(); } catch {}
|
|
924
995
|
}
|
|
925
996
|
}
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
try { fs.closeSync(fd); } catch {}
|
|
997
|
+
} finally {
|
|
998
|
+
resolveLock();
|
|
999
|
+
if (this._readLocks.get(filePath) === lock) {
|
|
1000
|
+
this._readLocks.delete(filePath);
|
|
931
1001
|
}
|
|
932
1002
|
}
|
|
933
1003
|
}
|
|
934
1004
|
|
|
935
|
-
_countFileLines(filePath) {
|
|
1005
|
+
async _countFileLines(filePath) {
|
|
936
1006
|
try {
|
|
937
|
-
const stat =
|
|
1007
|
+
const stat = await fsp.stat(filePath);
|
|
938
1008
|
if (stat.size === 0) return 0;
|
|
939
|
-
const
|
|
1009
|
+
const handle = await fsp.open(filePath, 'r');
|
|
940
1010
|
const buf = Buffer.alloc(8192);
|
|
941
1011
|
let count = 0;
|
|
942
1012
|
let pos = 0;
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
1013
|
+
try {
|
|
1014
|
+
while (pos < stat.size) {
|
|
1015
|
+
const readLen = Math.min(8192, stat.size - pos);
|
|
1016
|
+
const { bytesRead } = await handle.read(buf, 0, readLen, pos);
|
|
1017
|
+
for (let i = 0; i < bytesRead; i++) {
|
|
1018
|
+
if (buf[i] === 0x0A) count++;
|
|
1019
|
+
}
|
|
1020
|
+
pos += bytesRead;
|
|
948
1021
|
}
|
|
949
|
-
|
|
1022
|
+
} finally {
|
|
1023
|
+
await handle.close();
|
|
950
1024
|
}
|
|
951
|
-
fs.closeSync(fd);
|
|
952
1025
|
return count;
|
|
953
1026
|
} catch {
|
|
954
1027
|
return 0;
|
|
@@ -1021,48 +1094,34 @@ class Watcher extends require('events').EventEmitter {
|
|
|
1021
1094
|
// Directory walking
|
|
1022
1095
|
// =========================================================================
|
|
1023
1096
|
|
|
1024
|
-
|
|
1025
|
-
const walk = async (dir, callback) => {
|
|
1026
|
-
try {
|
|
1027
|
-
const entries = await readdirFn(dir, { withFileTypes: true });
|
|
1028
|
-
for (const entry of entries) {
|
|
1029
|
-
const fullPath = path.join(dir, entry.name);
|
|
1030
|
-
if (entry.isDirectory()) {
|
|
1031
|
-
await walk(fullPath, callback);
|
|
1032
|
-
} else {
|
|
1033
|
-
let stats;
|
|
1034
|
-
try { stats = fs.statSync(fullPath); } catch { continue; }
|
|
1035
|
-
callback(fullPath, stats);
|
|
1036
|
-
}
|
|
1037
|
-
}
|
|
1038
|
-
} catch {}
|
|
1039
|
-
};
|
|
1040
|
-
return walk;
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
_walkDir = this._createWalkDir(fsp.readdir);
|
|
1097
|
+
_walkDir = createWalkDir(fsp.readdir);
|
|
1044
1098
|
}
|
|
1045
1099
|
|
|
1046
1100
|
// ============================================================================
|
|
1047
|
-
//
|
|
1101
|
+
// Directory walking (shared factory for sync and async)
|
|
1048
1102
|
// ============================================================================
|
|
1049
1103
|
|
|
1050
|
-
function
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
const
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1104
|
+
function createWalkDir(readdirFn) {
|
|
1105
|
+
const walk = async (dir, callback) => {
|
|
1106
|
+
try {
|
|
1107
|
+
const entries = await readdirFn(dir, { withFileTypes: true });
|
|
1108
|
+
for (const entry of entries) {
|
|
1109
|
+
const fullPath = path.join(dir, entry.name);
|
|
1110
|
+
if (entry.isDirectory()) {
|
|
1111
|
+
await walk(fullPath, callback);
|
|
1112
|
+
} else {
|
|
1113
|
+
let stats;
|
|
1114
|
+
try { stats = await fsp.stat(fullPath); } catch { continue; }
|
|
1115
|
+
callback(fullPath, stats);
|
|
1116
|
+
}
|
|
1061
1117
|
}
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1118
|
+
} catch {}
|
|
1119
|
+
};
|
|
1120
|
+
return walk;
|
|
1064
1121
|
}
|
|
1065
1122
|
|
|
1123
|
+
var _walkDirAsync = createWalkDir(fsp.readdir);
|
|
1124
|
+
|
|
1066
1125
|
// ============================================================================
|
|
1067
1126
|
// Static listing methods
|
|
1068
1127
|
// ============================================================================
|
|
@@ -1106,19 +1165,7 @@ async function _listSessionsFiltered(limit, activeWithin) {
|
|
|
1106
1165
|
}
|
|
1107
1166
|
|
|
1108
1167
|
async function _walkDirStatic(dir, callback) {
|
|
1109
|
-
|
|
1110
|
-
const entries = await fsp.readdir(dir, { withFileTypes: true });
|
|
1111
|
-
for (const entry of entries) {
|
|
1112
|
-
const fullPath = path.join(dir, entry.name);
|
|
1113
|
-
if (entry.isDirectory()) {
|
|
1114
|
-
await _walkDirStatic(fullPath, callback);
|
|
1115
|
-
} else {
|
|
1116
|
-
let stats;
|
|
1117
|
-
try { stats = fs.statSync(fullPath); } catch { continue; }
|
|
1118
|
-
callback(fullPath, stats);
|
|
1119
|
-
}
|
|
1120
|
-
}
|
|
1121
|
-
} catch {}
|
|
1168
|
+
return _walkDirAsync(dir, callback);
|
|
1122
1169
|
}
|
|
1123
1170
|
|
|
1124
1171
|
module.exports = {
|