sidekick-shared 0.21.2 → 0.21.3

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.
Files changed (73) hide show
  1. package/README.md +45 -45
  2. package/dist/accountManager.js +9 -7
  3. package/dist/accountRegistry.js +18 -14
  4. package/dist/accountStatus.js +4 -1
  5. package/dist/accounts.js +60 -25
  6. package/dist/aggregation/EventAggregator.d.ts +14 -11
  7. package/dist/aggregation/EventAggregator.js +68 -31
  8. package/dist/aggregation/PatternExtractor.js +5 -4
  9. package/dist/autoSwitch.js +6 -12
  10. package/dist/claudeProfiles.js +3 -3
  11. package/dist/codexProfiles.js +45 -22
  12. package/dist/codexQuota.js +44 -16
  13. package/dist/codexQuotaWatcher.js +10 -3
  14. package/dist/context/composer.js +8 -6
  15. package/dist/context/sessionContext.js +12 -9
  16. package/dist/credentialIO.js +10 -8
  17. package/dist/extractors/sessionAssets.js +9 -1
  18. package/dist/extractors/sources/claudeAssets.js +7 -1
  19. package/dist/extractors/sources/codexAssets.js +14 -3
  20. package/dist/extractors/toolCall.js +1 -4
  21. package/dist/formatters/eventHighlighter.js +148 -28
  22. package/dist/formatters/noiseClassifier.js +3 -11
  23. package/dist/formatters/sessionDump.js +22 -13
  24. package/dist/formatters/toolSummary.js +1 -3
  25. package/dist/index.d.ts +23 -23
  26. package/dist/index.js +7 -3
  27. package/dist/modelContext.js +10 -8
  28. package/dist/modelInfo.js +11 -6
  29. package/dist/multiProviderQuotaService.js +10 -7
  30. package/dist/parsers/codexParser.js +170 -103
  31. package/dist/parsers/debugLogParser.js +5 -4
  32. package/dist/parsers/openCodeParser.js +66 -48
  33. package/dist/parsers/planExtractor.js +3 -3
  34. package/dist/parsers/sessionActivityDetector.js +10 -6
  35. package/dist/parsers/sessionPathResolver.js +29 -34
  36. package/dist/parsers/subagentScanner.js +21 -20
  37. package/dist/parsers/subagentTraceParser.js +7 -4
  38. package/dist/phrases.js +11 -11
  39. package/dist/providerStatus.js +3 -4
  40. package/dist/providers/claudeCode.js +3 -1
  41. package/dist/providers/codex.js +35 -14
  42. package/dist/providers/codexDatabase.js +14 -5
  43. package/dist/providers/detect.js +10 -6
  44. package/dist/providers/openCode.js +91 -63
  45. package/dist/providers/openCodeDatabase.js +44 -17
  46. package/dist/quota.d.ts +19 -0
  47. package/dist/quota.js +50 -8
  48. package/dist/quotaHistory.js +10 -3
  49. package/dist/quotaSnapshots.js +2 -2
  50. package/dist/readers/decisions.js +1 -1
  51. package/dist/readers/notes.js +5 -3
  52. package/dist/readers/plans.js +43 -23
  53. package/dist/readers/tasks.js +1 -1
  54. package/dist/report/htmlHelpers.js +135 -16
  55. package/dist/report/htmlReportGenerator.js +38 -12
  56. package/dist/report/transcriptParser.js +8 -2
  57. package/dist/schemas/index.d.ts +4 -4
  58. package/dist/schemas/quota.js +2 -10
  59. package/dist/schemas/sessionEvent.js +20 -10
  60. package/dist/search/advancedFilter.js +3 -3
  61. package/dist/search/sessionSearch.js +10 -4
  62. package/dist/terminalSync.js +4 -2
  63. package/dist/turns/assistantTurn.js +5 -1
  64. package/dist/watchers/eventBridge.js +56 -26
  65. package/dist/watchers/factory.js +5 -2
  66. package/dist/watchers/jsonlTail.js +3 -1
  67. package/dist/watchers/jsonlWatcher.js +128 -52
  68. package/dist/watchers/providerReaderWatcher.js +3 -1
  69. package/dist/watchers/sqliteWatcher.js +52 -22
  70. package/dist/zaiQuota.js +6 -10
  71. package/dist/zaiQuotaApi.js +13 -5
  72. package/dist/zaiQuotaWatcher.js +1 -1
  73. package/package.json +5 -2
@@ -156,7 +156,10 @@ function convertUserMessage(message, parts) {
156
156
  content.push({ type: 'text', text: part.text });
157
157
  }
158
158
  else if (part.type === 'file') {
159
- content.push({ type: 'text', text: `[File: ${part.filename || 'unknown'} (${part.mime || 'unknown'})]` });
159
+ content.push({
160
+ type: 'text',
161
+ text: `[File: ${part.filename || 'unknown'} (${part.mime || 'unknown'})]`,
162
+ });
160
163
  }
161
164
  else if (part.type === 'subtask') {
162
165
  content.push({ type: 'text', text: `[Subtask: ${part.description || 'unknown'}]` });
@@ -166,7 +169,7 @@ function convertUserMessage(message, parts) {
166
169
  events.push({
167
170
  type: 'user',
168
171
  message: { role: 'user', id: message.id, content },
169
- timestamp: toISOString(message.time.created)
172
+ timestamp: toISOString(message.time.created),
170
173
  });
171
174
  }
172
175
  return events;
@@ -195,7 +198,7 @@ function convertAssistantMessage(message, parts) {
195
198
  type: 'tool_use',
196
199
  id: part.callID,
197
200
  name: normalizeToolName(part.tool),
198
- input: normalizeToolInput(part.state.input || {})
201
+ input: normalizeToolInput(part.state.input || {}),
199
202
  });
200
203
  break;
201
204
  case 'tool':
@@ -203,7 +206,7 @@ function convertAssistantMessage(message, parts) {
203
206
  type: 'tool_use',
204
207
  id: part.callID,
205
208
  name: normalizeToolName(part.tool),
206
- input: normalizeToolInput(part.state.input || {})
209
+ input: normalizeToolInput(part.state.input || {}),
207
210
  });
208
211
  break;
209
212
  case 'patch':
@@ -216,7 +219,7 @@ function convertAssistantMessage(message, parts) {
216
219
  type: 'tool_use',
217
220
  id: `patch-${part.id}-${i}`,
218
221
  name: 'Write',
219
- input: { file_path: part.files[i] }
222
+ input: { file_path: part.files[i] },
220
223
  });
221
224
  }
222
225
  }
@@ -225,7 +228,7 @@ function convertAssistantMessage(message, parts) {
225
228
  type: 'tool_use',
226
229
  id: `patch-${part.id}`,
227
230
  name: 'Patch',
228
- input: { hash: part.hash, files: part.files }
231
+ input: { hash: part.hash, files: part.files },
229
232
  });
230
233
  }
231
234
  break;
@@ -244,19 +247,19 @@ function convertAssistantMessage(message, parts) {
244
247
  model: part.model,
245
248
  prompt: part.prompt,
246
249
  command: part.command,
247
- }
250
+ },
248
251
  });
249
252
  break;
250
253
  case 'file':
251
254
  content.push({
252
255
  type: 'text',
253
- text: `[File: ${part.filename || 'unknown'} (${part.mime || 'unknown'})]`
256
+ text: `[File: ${part.filename || 'unknown'} (${part.mime || 'unknown'})]`,
254
257
  });
255
258
  break;
256
259
  case 'retry':
257
260
  content.push({
258
261
  type: 'text',
259
- text: `[Retry attempt ${part.attempt ?? '?'}: ${part.error?.message || 'unknown error'}]`
262
+ text: `[Retry attempt ${part.attempt ?? '?'}: ${part.error?.message || 'unknown error'}]`,
260
263
  });
261
264
  break;
262
265
  case 'agent':
@@ -276,15 +279,14 @@ function convertAssistantMessage(message, parts) {
276
279
  const errCode = message.error.code ? ` (code: ${message.error.code})` : '';
277
280
  content.push({
278
281
  type: 'text',
279
- text: `[${errType}]${errCode} ${errMsg}`
282
+ text: `[${errType}]${errCode} ${errMsg}`,
280
283
  });
281
284
  }
282
285
  // Detect plan mode mentions in text/thinking blocks and inject synthetic
283
286
  // tool_use blocks (OpenCode doesn't emit plan_enter/plan_exit tools).
284
287
  const hasExistingPlanMode = content.some((b) => {
285
288
  const block = b;
286
- return block.type === 'tool_use' &&
287
- (block.name === 'EnterPlanMode' || block.name === 'ExitPlanMode');
289
+ return (block.type === 'tool_use' && (block.name === 'EnterPlanMode' || block.name === 'ExitPlanMode'));
288
290
  });
289
291
  if (!hasExistingPlanMode) {
290
292
  for (const block of content) {
@@ -304,7 +306,7 @@ function convertAssistantMessage(message, parts) {
304
306
  type: 'tool_use',
305
307
  id: `plan-${direction}-${message.id}`,
306
308
  name: toolName,
307
- input: { source: 'opencode_text_heuristic', _sidekickRawToolName: rawName }
309
+ input: { source: 'opencode_text_heuristic', _sidekickRawToolName: rawName },
308
310
  });
309
311
  break; // Only inject one plan mode block per message
310
312
  }
@@ -320,9 +322,11 @@ function convertAssistantMessage(message, parts) {
320
322
  }
321
323
  }
322
324
  // Build usage data from message tokens
323
- const reportedCost = (message.cost && message.cost > 0)
325
+ const reportedCost = message.cost && message.cost > 0
324
326
  ? message.cost
325
- : (stepFinishCost > 0 ? stepFinishCost : undefined);
327
+ : stepFinishCost > 0
328
+ ? stepFinishCost
329
+ : undefined;
326
330
  const usage = {
327
331
  input_tokens: message.tokens.input || 0,
328
332
  output_tokens: message.tokens.output || 0,
@@ -341,9 +345,9 @@ function convertAssistantMessage(message, parts) {
341
345
  id: message.id,
342
346
  model: message.modelID,
343
347
  usage,
344
- content
348
+ content,
345
349
  },
346
- timestamp
350
+ timestamp,
347
351
  });
348
352
  }
349
353
  // Emit synthetic tool_result events for completed tool invocations
@@ -365,20 +369,24 @@ function convertAssistantMessage(message, parts) {
365
369
  duration = endMs - startMs;
366
370
  }
367
371
  }
368
- const resultContent = [{
372
+ const resultContent = [
373
+ {
369
374
  type: 'tool_result',
370
375
  tool_use_id: part.callID,
371
376
  content: part.state.status === 'error' ? part.state.error : part.state.output,
372
377
  is_error: part.state.status === 'error',
373
378
  ...(duration !== undefined && { duration }),
374
- }];
375
- const resultTimestamp = part.state.time?.end
376
- ? toISOString(part.state.time.end)
377
- : timestamp;
379
+ },
380
+ ];
381
+ const resultTimestamp = part.state.time?.end ? toISOString(part.state.time.end) : timestamp;
378
382
  events.push({
379
383
  type: 'user',
380
- message: { role: 'user', id: `${message.id}:${part.callID}:result`, content: resultContent },
381
- timestamp: resultTimestamp
384
+ message: {
385
+ role: 'user',
386
+ id: `${message.id}:${part.callID}:result`,
387
+ content: resultContent,
388
+ },
389
+ timestamp: resultTimestamp,
382
390
  });
383
391
  }
384
392
  // Subtask parts get a synthetic tool_result so they appear in the timeline
@@ -388,24 +396,26 @@ function convertAssistantMessage(message, parts) {
388
396
  message: {
389
397
  role: 'user',
390
398
  id: `${message.id}:subtask-${part.id}:result`,
391
- content: [{
399
+ content: [
400
+ {
392
401
  type: 'tool_result',
393
402
  tool_use_id: `subtask-${part.id}`,
394
403
  content: part.description || 'Subtask completed',
395
- is_error: false
396
- }]
404
+ is_error: false,
405
+ },
406
+ ],
397
407
  },
398
- timestamp
408
+ timestamp,
399
409
  });
400
410
  }
401
411
  }
402
412
  // Detect compaction via summary flag or compaction part
403
- const hasCompaction = message.summary || parts.some(p => p.type === 'compaction');
413
+ const hasCompaction = message.summary || parts.some((p) => p.type === 'compaction');
404
414
  if (hasCompaction) {
405
415
  events.push({
406
416
  type: 'summary',
407
417
  message: { role: 'assistant', id: `${message.id}:summary`, content: 'Context compacted' },
408
- timestamp
418
+ timestamp,
409
419
  });
410
420
  }
411
421
  return events;
@@ -426,11 +436,13 @@ function parseDbMessageData(row) {
426
436
  const summary = data.summary;
427
437
  // Parse error field if present (AuthError, APIError, OutputLengthError, etc.)
428
438
  const errorData = data.error;
429
- const error = errorData ? {
430
- type: errorData.type || undefined,
431
- message: errorData.message || undefined,
432
- code: errorData.code,
433
- } : undefined;
439
+ const error = errorData
440
+ ? {
441
+ type: errorData.type || undefined,
442
+ message: errorData.message || undefined,
443
+ code: errorData.code,
444
+ }
445
+ : undefined;
434
446
  return {
435
447
  id: row.id,
436
448
  sessionID: row.session_id,
@@ -512,15 +524,19 @@ function parseDbPartData(row) {
512
524
  reason: data.reason,
513
525
  snapshot: data.snapshot,
514
526
  cost: data.cost,
515
- tokens: tokensData ? {
516
- input: tokensData.input,
517
- output: tokensData.output,
518
- reasoning: tokensData.reasoning,
519
- cache: cacheData ? {
520
- read: cacheData.read,
521
- write: cacheData.write,
522
- } : undefined,
523
- } : undefined,
527
+ tokens: tokensData
528
+ ? {
529
+ input: tokensData.input,
530
+ output: tokensData.output,
531
+ reasoning: tokensData.reasoning,
532
+ cache: cacheData
533
+ ? {
534
+ read: cacheData.read,
535
+ write: cacheData.write,
536
+ }
537
+ : undefined,
538
+ }
539
+ : undefined,
524
540
  };
525
541
  }
526
542
  case 'patch':
@@ -561,10 +577,12 @@ function parseDbPartData(row) {
561
577
  ...base,
562
578
  type: 'retry',
563
579
  attempt: data.attempt,
564
- error: errorData ? {
565
- message: errorData.message,
566
- code: errorData.code,
567
- } : undefined,
580
+ error: errorData
581
+ ? {
582
+ message: errorData.message,
583
+ code: errorData.code,
584
+ }
585
+ : undefined,
568
586
  time: data.time,
569
587
  };
570
588
  }
@@ -306,9 +306,9 @@ class PlanExtractor {
306
306
  finalizePlanMode() {
307
307
  this._planModeActive = false;
308
308
  // Prefer plan file content (from Write tool) → accumulated assistant text → disk read fallback
309
- const markdown = this._planFileContent
310
- || (this._planTexts.length > 0 ? this._planTexts.join('\n') : null)
311
- || (this._planFilePath && this._readFile ? this._readFile(this._planFilePath) : null);
309
+ const markdown = this._planFileContent ||
310
+ (this._planTexts.length > 0 ? this._planTexts.join('\n') : null) ||
311
+ (this._planFilePath && this._readFile ? this._readFile(this._planFilePath) : null);
312
312
  this._planFileContent = null;
313
313
  this._planFilePath = null;
314
314
  this._planTexts = [];
@@ -68,9 +68,7 @@ const ENDING_PATTERNS = [
68
68
  '"type":"user"', // User typing means AI is done with its turn
69
69
  ];
70
70
  /** Patterns that definitely mean the session is done */
71
- const TERMINAL_PATTERNS = [
72
- '"type":"result"',
73
- ];
71
+ const TERMINAL_PATTERNS = ['"type":"result"'];
74
72
  // ── Public API ──
75
73
  /**
76
74
  * Determines whether a session is ongoing, ended, or stale.
@@ -103,7 +101,7 @@ function detectSessionActivity(sessionPath) {
103
101
  return { state: 'ended', lastActivityTime: new Date(stat.mtimeMs), reason: 'empty-file' };
104
102
  }
105
103
  // Parse the last few JSONL lines
106
- const lines = tail.split('\n').filter(l => l.trim().length > 0);
104
+ const lines = tail.split('\n').filter((l) => l.trim().length > 0);
107
105
  if (lines.length === 0) {
108
106
  return { state: 'ended', lastActivityTime: new Date(stat.mtimeMs), reason: 'no-events' };
109
107
  }
@@ -141,7 +139,11 @@ function detectSessionActivity(sessionPath) {
141
139
  }
142
140
  // AI activity after the last ending event → ongoing
143
141
  if (lastAiActivityIndex > lastEndingIndex) {
144
- return { state: 'ongoing', lastActivityTime: new Date(stat.mtimeMs), reason: 'ai-activity-after-ending' };
142
+ return {
143
+ state: 'ongoing',
144
+ lastActivityTime: new Date(stat.mtimeMs),
145
+ reason: 'ai-activity-after-ending',
146
+ };
145
147
  }
146
148
  // Ending event with no subsequent AI activity
147
149
  if (lastEndingIndex >= 0) {
@@ -177,7 +179,9 @@ function readTail(filePath, fileSize) {
177
179
  try {
178
180
  fs.closeSync(fd);
179
181
  }
180
- catch { /* ignore */ }
182
+ catch {
183
+ /* ignore */
184
+ }
181
185
  }
182
186
  return null;
183
187
  }
@@ -135,7 +135,7 @@ function findSubdirectorySessionDirs(workspacePath) {
135
135
  // Encode workspace path to get the prefix to match
136
136
  const encodedPrefix = encodeWorkspacePath(workspacePath).toLowerCase();
137
137
  // Get all directories in projects folder
138
- const allDirs = fs.readdirSync(projectsDir).filter(name => {
138
+ const allDirs = fs.readdirSync(projectsDir).filter((name) => {
139
139
  const fullPath = path.join(projectsDir, name);
140
140
  try {
141
141
  return fs.statSync(fullPath).isDirectory();
@@ -184,8 +184,7 @@ function getMostRecentlyActiveSessionDir(sessionDirs) {
184
184
  for (const dir of sessionDirs) {
185
185
  try {
186
186
  // Find most recent .jsonl file in this directory
187
- const files = fs.readdirSync(dir)
188
- .filter(file => file.endsWith('.jsonl'));
187
+ const files = fs.readdirSync(dir).filter((file) => file.endsWith('.jsonl'));
189
188
  for (const file of files) {
190
189
  try {
191
190
  const fullPath = path.join(dir, file);
@@ -239,7 +238,7 @@ function discoverSessionDirectory(workspacePath) {
239
238
  // Strategy 2: Scan ~/.claude/projects/ for matching directories
240
239
  try {
241
240
  if (fs.existsSync(projectsDir)) {
242
- const existingDirs = fs.readdirSync(projectsDir).filter(name => {
241
+ const existingDirs = fs.readdirSync(projectsDir).filter((name) => {
243
242
  const fullPath = path.join(projectsDir, name);
244
243
  try {
245
244
  return fs.statSync(fullPath).isDirectory();
@@ -263,9 +262,7 @@ function discoverSessionDirectory(workspacePath) {
263
262
  }
264
263
  }
265
264
  // Fallback: match by final path component (project name)
266
- const workspaceBasename = path.basename(workspacePath)
267
- .replace(/_/g, '-')
268
- .toLowerCase();
265
+ const workspaceBasename = path.basename(workspacePath).replace(/_/g, '-').toLowerCase();
269
266
  for (const dir of existingDirs) {
270
267
  const dirLower = dir.toLowerCase();
271
268
  // Check if dir ends with the project name
@@ -283,7 +280,7 @@ function discoverSessionDirectory(workspacePath) {
283
280
  try {
284
281
  const claudeTempDir = path.join(os.tmpdir(), 'claude');
285
282
  if (fs.existsSync(claudeTempDir)) {
286
- const tempDirs = fs.readdirSync(claudeTempDir).filter(name => {
283
+ const tempDirs = fs.readdirSync(claudeTempDir).filter((name) => {
287
284
  const fullPath = path.join(claudeTempDir, name);
288
285
  try {
289
286
  return fs.statSync(fullPath).isDirectory();
@@ -293,9 +290,7 @@ function discoverSessionDirectory(workspacePath) {
293
290
  }
294
291
  });
295
292
  // Match by workspace basename
296
- const workspaceBasename = path.basename(workspacePath)
297
- .replace(/_/g, '-')
298
- .toLowerCase();
293
+ const workspaceBasename = path.basename(workspacePath).replace(/_/g, '-').toLowerCase();
299
294
  for (const encodedDir of tempDirs) {
300
295
  const encodedLower = encodedDir.toLowerCase();
301
296
  if (encodedLower.endsWith('-' + workspaceBasename) || encodedLower === workspaceBasename) {
@@ -342,9 +337,10 @@ function findActiveSession(workspacePath) {
342
337
  }
343
338
  const now = Date.now();
344
339
  // Find all .jsonl files with stats
345
- const files = fs.readdirSync(sessionDir)
346
- .filter(file => file.endsWith('.jsonl'))
347
- .map(file => {
340
+ const files = fs
341
+ .readdirSync(sessionDir)
342
+ .filter((file) => file.endsWith('.jsonl'))
343
+ .map((file) => {
348
344
  const fullPath = path.join(sessionDir, file);
349
345
  const stats = fs.statSync(fullPath);
350
346
  const mtime = stats.mtime.getTime();
@@ -352,11 +348,11 @@ function findActiveSession(workspacePath) {
352
348
  path: fullPath,
353
349
  mtime,
354
350
  size: stats.size,
355
- isActive: (now - mtime) < ACTIVE_SESSION_THRESHOLD_MS
351
+ isActive: now - mtime < ACTIVE_SESSION_THRESHOLD_MS,
356
352
  };
357
353
  })
358
354
  // Filter out empty files
359
- .filter(file => file.size > 0);
355
+ .filter((file) => file.size > 0);
360
356
  // Return null if no session files
361
357
  if (files.length === 0) {
362
358
  return null;
@@ -405,18 +401,19 @@ function findAllSessions(workspacePath) {
405
401
  return [];
406
402
  }
407
403
  // Find and sort all .jsonl files
408
- const files = fs.readdirSync(sessionDir)
409
- .filter(file => file.endsWith('.jsonl'))
410
- .map(file => {
404
+ const files = fs
405
+ .readdirSync(sessionDir)
406
+ .filter((file) => file.endsWith('.jsonl'))
407
+ .map((file) => {
411
408
  const fullPath = path.join(sessionDir, file);
412
409
  const stats = fs.statSync(fullPath);
413
410
  return {
414
411
  path: fullPath,
415
- mtime: stats.mtime.getTime()
412
+ mtime: stats.mtime.getTime(),
416
413
  };
417
414
  })
418
415
  .sort((a, b) => b.mtime - a.mtime)
419
- .map(file => file.path);
416
+ .map((file) => file.path);
420
417
  return files;
421
418
  }
422
419
  catch (error) {
@@ -505,8 +502,7 @@ function getAllProjectFolders(workspacePath) {
505
502
  continue;
506
503
  }
507
504
  // Count session files and find most recent modification
508
- const sessionFiles = fs.readdirSync(fullPath)
509
- .filter(f => f.endsWith('.jsonl'));
505
+ const sessionFiles = fs.readdirSync(fullPath).filter((f) => f.endsWith('.jsonl'));
510
506
  let lastModified = stats.mtime;
511
507
  let sessionCount = 0;
512
508
  for (const sessionFile of sessionFiles) {
@@ -529,7 +525,7 @@ function getAllProjectFolders(workspacePath) {
529
525
  encodedName: entry,
530
526
  name: decodeEncodedPath(entry),
531
527
  sessionCount,
532
- lastModified
528
+ lastModified,
533
529
  });
534
530
  }
535
531
  catch {
@@ -588,16 +584,17 @@ function findSessionsInDirectory(sessionDir) {
588
584
  if (!fs.existsSync(sessionDir)) {
589
585
  return [];
590
586
  }
591
- const files = fs.readdirSync(sessionDir)
592
- .filter(file => file.endsWith('.jsonl'))
593
- .map(file => {
587
+ const files = fs
588
+ .readdirSync(sessionDir)
589
+ .filter((file) => file.endsWith('.jsonl'))
590
+ .map((file) => {
594
591
  const fullPath = path.join(sessionDir, file);
595
592
  try {
596
593
  const stats = fs.statSync(fullPath);
597
594
  return {
598
595
  path: fullPath,
599
596
  mtime: stats.mtime.getTime(),
600
- size: stats.size
597
+ size: stats.size,
601
598
  };
602
599
  }
603
600
  catch {
@@ -606,7 +603,7 @@ function findSessionsInDirectory(sessionDir) {
606
603
  })
607
604
  .filter((f) => f !== null && f.size > 0)
608
605
  .sort((a, b) => b.mtime - a.mtime)
609
- .map(f => f.path);
606
+ .map((f) => f.path);
610
607
  return files;
611
608
  }
612
609
  catch {
@@ -641,9 +638,7 @@ function resolveWorktreeMainRepo(workspacePath) {
641
638
  const gitdir = match[1].trim();
642
639
  // gitdir points to something like /main-repo/.git/worktrees/<name>
643
640
  // Resolve to absolute path
644
- const resolvedGitdir = path.isAbsolute(gitdir)
645
- ? gitdir
646
- : path.resolve(workspacePath, gitdir);
641
+ const resolvedGitdir = path.isAbsolute(gitdir) ? gitdir : path.resolve(workspacePath, gitdir);
647
642
  // Navigate up from .git/worktrees/<name> to the main repo
648
643
  const worktreesDir = path.dirname(resolvedGitdir);
649
644
  if (path.basename(worktreesDir) !== 'worktrees')
@@ -739,7 +734,7 @@ function findAllSessionsWithWorktrees(workspacePath) {
739
734
  }
740
735
  // Sort by mtime (most recent first)
741
736
  return Array.from(allPaths)
742
- .map(sessionPath => {
737
+ .map((sessionPath) => {
743
738
  try {
744
739
  const stat = fs.statSync(sessionPath);
745
740
  return { path: sessionPath, mtime: stat.mtime.getTime() };
@@ -749,5 +744,5 @@ function findAllSessionsWithWorktrees(workspacePath) {
749
744
  }
750
745
  })
751
746
  .sort((a, b) => b.mtime - a.mtime)
752
- .map(f => f.path);
747
+ .map((f) => f.path);
753
748
  }
@@ -60,9 +60,7 @@ const TASK_TOOLS = ['TaskCreate', 'TaskUpdate', 'TaskGet', 'TaskList'];
60
60
  * Looks for "Task #N" or JSON taskId patterns in the result string.
61
61
  */
62
62
  function extractTaskIdFromResult(resultContent) {
63
- const resultStr = typeof resultContent === 'string'
64
- ? resultContent
65
- : JSON.stringify(resultContent || '');
63
+ const resultStr = typeof resultContent === 'string' ? resultContent : JSON.stringify(resultContent || '');
66
64
  // Try to match "Task #N" pattern
67
65
  const taskIdMatch = resultStr.match(/Task #(\d+)/i);
68
66
  if (taskIdMatch) {
@@ -146,7 +144,7 @@ function scanSubagentDir(sessionDir, sessionId, logger) {
146
144
  function parseAgentFile(filePath, agentId, log) {
147
145
  try {
148
146
  const content = fs.readFileSync(filePath, 'utf-8');
149
- const lines = content.split('\n').filter(line => line.trim());
147
+ const lines = content.split('\n').filter((line) => line.trim());
150
148
  const toolCalls = [];
151
149
  let agentType;
152
150
  let description;
@@ -157,7 +155,7 @@ function parseAgentFile(filePath, agentId, log) {
157
155
  // Task tracking state
158
156
  const taskState = {
159
157
  tasks: new Map(),
160
- activeTaskId: null
158
+ activeTaskId: null,
161
159
  };
162
160
  const pendingTaskCreates = new Map();
163
161
  for (const line of lines) {
@@ -171,7 +169,10 @@ function parseAgentFile(filePath, agentId, log) {
171
169
  // Extract token usage from assistant messages
172
170
  if (event.type === 'assistant' && event.message?.usage) {
173
171
  const usage = event.message.usage;
174
- inputTokens += (usage.input_tokens || 0) + (usage.cache_creation_input_tokens || 0) + (usage.cache_read_input_tokens || 0);
172
+ inputTokens +=
173
+ (usage.input_tokens || 0) +
174
+ (usage.cache_creation_input_tokens || 0) +
175
+ (usage.cache_read_input_tokens || 0);
175
176
  outputTokens += usage.output_tokens || 0;
176
177
  }
177
178
  // Extract agent type and description from Task tool invocation
@@ -189,24 +190,26 @@ function parseAgentFile(filePath, agentId, log) {
189
190
  }
190
191
  // Look for tool_use events in assistant messages
191
192
  if (event.type === 'assistant' && event.message?.content) {
192
- const contentArray = Array.isArray(event.message.content)
193
- ? event.message.content
194
- : [];
193
+ const contentArray = Array.isArray(event.message.content) ? event.message.content : [];
195
194
  for (const block of contentArray) {
196
195
  if (block && typeof block === 'object' && block.type === 'tool_use') {
197
196
  const toolUse = block;
198
197
  const toolCall = {
199
198
  name: toolUse.name,
200
199
  input: toolUse.input || {},
201
- timestamp: eventTimestamp
200
+ timestamp: eventTimestamp,
202
201
  };
203
202
  // Handle task tools
204
203
  if (toolUse.name === 'TaskCreate') {
205
204
  pendingTaskCreates.set(toolUse.id, {
206
205
  subject: String(toolUse.input.subject || ''),
207
- description: toolUse.input.description ? String(toolUse.input.description) : undefined,
208
- activeForm: toolUse.input.activeForm ? String(toolUse.input.activeForm) : undefined,
209
- timestamp: eventTimestamp
206
+ description: toolUse.input.description
207
+ ? String(toolUse.input.description)
208
+ : undefined,
209
+ activeForm: toolUse.input.activeForm
210
+ ? String(toolUse.input.activeForm)
211
+ : undefined,
212
+ timestamp: eventTimestamp,
210
213
  });
211
214
  }
212
215
  else if (toolUse.name === 'TaskUpdate') {
@@ -234,9 +237,7 @@ function parseAgentFile(filePath, agentId, log) {
234
237
  }
235
238
  // Look for tool_result events in user messages
236
239
  if (event.type === 'user' && event.message?.content) {
237
- const contentArray = Array.isArray(event.message.content)
238
- ? event.message.content
239
- : [];
240
+ const contentArray = Array.isArray(event.message.content) ? event.message.content : [];
240
241
  for (const block of contentArray) {
241
242
  if (block && typeof block === 'object' && block.type === 'tool_result') {
242
243
  const toolResult = block;
@@ -255,7 +256,7 @@ function parseAgentFile(filePath, agentId, log) {
255
256
  activeForm: pendingCreate.activeForm,
256
257
  blockedBy: [],
257
258
  blocks: [],
258
- associatedToolCalls: []
259
+ associatedToolCalls: [],
259
260
  };
260
261
  taskState.tasks.set(taskId, task);
261
262
  }
@@ -282,7 +283,7 @@ function parseAgentFile(filePath, agentId, log) {
282
283
  outputTokens,
283
284
  startTime,
284
285
  endTime,
285
- durationMs
286
+ durationMs,
286
287
  };
287
288
  }
288
289
  return null;
@@ -309,7 +310,7 @@ function handleTaskUpdate(taskState, input, timestamp) {
309
310
  updatedAt: timestamp,
310
311
  blockedBy: [],
311
312
  blocks: [],
312
- associatedToolCalls: []
313
+ associatedToolCalls: [],
313
314
  };
314
315
  taskState.tasks.set(taskId, task);
315
316
  }
@@ -361,6 +362,6 @@ function handleTaskUpdate(taskState, input, timestamp) {
361
362
  function extractTaskInfo(input) {
362
363
  return {
363
364
  agentType: input.subagent_type ? String(input.subagent_type) : undefined,
364
- description: input.description ? String(input.description) : undefined
365
+ description: input.description ? String(input.description) : undefined,
365
366
  };
366
367
  }
@@ -86,7 +86,7 @@ function scanSubagentTraces(sessionDir, sessionId) {
86
86
  // Link parent-child relationships
87
87
  linkTraces(traces);
88
88
  // Return only top-level traces (those without parents)
89
- return traces.filter(t => !t.parentToolUseId);
89
+ return traces.filter((t) => !t.parentToolUseId);
90
90
  }
91
91
  catch {
92
92
  return [];
@@ -96,7 +96,7 @@ function scanSubagentTraces(sessionDir, sessionId) {
96
96
  function parseAgentTrace(filePath, agentId) {
97
97
  try {
98
98
  const content = fs.readFileSync(filePath, 'utf-8');
99
- const lines = content.split('\n').filter(l => l.trim());
99
+ const lines = content.split('\n').filter((l) => l.trim());
100
100
  const events = [];
101
101
  const toolCalls = [];
102
102
  let agentType;
@@ -155,7 +155,10 @@ function parseAgentTrace(filePath, agentId) {
155
155
  // Extract token usage
156
156
  if (raw.type === 'assistant' && raw.message?.usage) {
157
157
  const usage = raw.message.usage;
158
- inputTokens += (usage.input_tokens || 0) + (usage.cache_creation_input_tokens || 0) + (usage.cache_read_input_tokens || 0);
158
+ inputTokens +=
159
+ (usage.input_tokens || 0) +
160
+ (usage.cache_creation_input_tokens || 0) +
161
+ (usage.cache_read_input_tokens || 0);
159
162
  outputTokens += usage.output_tokens || 0;
160
163
  }
161
164
  // Extract tool calls
@@ -258,7 +261,7 @@ function linkTraces(traces) {
258
261
  }
259
262
  // Phase 3: Positional fallback — timestamp proximity
260
263
  // Traces without parents that fall within another trace's time window
261
- const parentless = traces.filter(t => !t.parentToolUseId);
264
+ const parentless = traces.filter((t) => !t.parentToolUseId);
262
265
  for (const child of parentless) {
263
266
  if (!child.stats.startTime)
264
267
  continue;