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.
@@ -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 compareVersions(a, b) {
45
- const pa = a.split('.').map(Number);
46
- const pb = b.split('.').map(Number);
47
- for (let i = 0; i < 3; i++) {
48
- if (pa[i] > pb[i]) return 1;
49
- if (pa[i] < pb[i]) return -1;
50
- }
51
- return 0;
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.9",
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
- const session = sessions.splice(idx, 1)[0];
592
- sessions.push(session);
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(`Move session ${sid.slice(0, 12)}... to bottom?`)) return;
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
- const session = sessions.splice(idx, 1)[0];
1119
- sessions.push(session);
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 };
@@ -43,7 +43,8 @@ function setDebugAll(val) {
43
43
 
44
44
  function agentDisplayName(agentID) {
45
45
  if (!agentID) return 'Main';
46
- return `Agent-${agentID.slice(0, Math.min(AgentIDDisplayLength, agentID.length))}`;
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 (!raw.prNumber && !raw.prUrl) return [];
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
  };
@@ -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 { this.clients.delete(ws); ws.terminate(); }
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
- const allowedPrefix = path.resolve(os.homedir(), '.claude', 'projects');
187
- // Resolve symlinks before prefix check to prevent symlink-based path traversal
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 realPath = await fs.promises.realpath(resolved);
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(resolved, 'utf-8');
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
- this.watcher.removeSession(cmd.sessionID);
243
- this.broadcast('sessionRemoved', { sessionID: cmd.sessionID });
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 > 0) {
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 > 0) {
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 => {
@@ -456,8 +456,16 @@ class Watcher extends EventEmitter {
456
456
  }
457
457
 
458
458
  _handleFsWrite(p) {
459
- const ctx = this.fileContexts.get(p);
460
- if (!ctx) return;
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
- const idMatch = line.match(/"id"\s*:\s*"([^"]+)"/);
731
- if (!idMatch) continue;
732
- const tid = idMatch[1];
733
- if (session.toolIndex.has(tid)) continue;
734
- const nameMatch = line.match(/"name"\s*:\s*"([^"]+)"/);
735
- session.toolIndex.set(tid, {
736
- toolName: nameMatch ? nameMatch[1] : '',
737
- parentAgentID: agentID,
738
- hasResult: false,
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
- const useIdMatch = line.match(/"tool_use_id"\s*:\s*"([^"]+)"/);
744
- if (!useIdMatch) continue;
745
- const tid = useIdMatch[1];
746
- const existing = session.toolIndex.get(tid);
747
- if (existing) {
748
- existing.hasResult = true;
749
- } else {
750
- session.toolIndex.set(tid, {
751
- toolName: '',
752
- parentAgentID: '',
753
- hasResult: true,
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 currentPos = this.filePositions.get(filePath) || 0;
922
- if (currentPos >= currentStats.size) break;
977
+ const readFrom = newPos + carryOverBytes;
978
+ if (readFrom >= currentStats.size) break;
923
979
 
924
- const readLen = Math.min(MaxReadChunk, currentStats.size - currentPos);
925
- const { bytesRead } = await handle.read(buf, 0, readLen, currentPos);
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 = bytesRead < readLen ? buf.toString('utf-8', 0, bytesRead) : buf.toString('utf-8');
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 _walkDirStatic(claudeDir, (filePath, stats) => {
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
  };