@swarmify/agents-cli 1.10.4 → 1.11.1

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 (95) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +73 -2
  3. package/dist/commands/__tests__/sessions.test.js +333 -0
  4. package/dist/commands/__tests__/sessions.test.js.map +1 -1
  5. package/dist/commands/commands.d.ts.map +1 -1
  6. package/dist/commands/commands.js +5 -11
  7. package/dist/commands/commands.js.map +1 -1
  8. package/dist/commands/exec.d.ts.map +1 -1
  9. package/dist/commands/exec.js +11 -1
  10. package/dist/commands/exec.js.map +1 -1
  11. package/dist/commands/hooks.d.ts.map +1 -1
  12. package/dist/commands/hooks.js +5 -11
  13. package/dist/commands/hooks.js.map +1 -1
  14. package/dist/commands/mcp.d.ts.map +1 -1
  15. package/dist/commands/mcp.js +109 -43
  16. package/dist/commands/mcp.js.map +1 -1
  17. package/dist/commands/packages.d.ts.map +1 -1
  18. package/dist/commands/packages.js +92 -42
  19. package/dist/commands/packages.js.map +1 -1
  20. package/dist/commands/permissions.d.ts.map +1 -1
  21. package/dist/commands/permissions.js +38 -34
  22. package/dist/commands/permissions.js.map +1 -1
  23. package/dist/commands/pull.d.ts.map +1 -1
  24. package/dist/commands/pull.js +14 -15
  25. package/dist/commands/pull.js.map +1 -1
  26. package/dist/commands/push.d.ts.map +1 -1
  27. package/dist/commands/push.js +2 -0
  28. package/dist/commands/push.js.map +1 -1
  29. package/dist/commands/rules.d.ts.map +1 -1
  30. package/dist/commands/rules.js +5 -11
  31. package/dist/commands/rules.js.map +1 -1
  32. package/dist/commands/sessions.d.ts.map +1 -1
  33. package/dist/commands/sessions.js +217 -34
  34. package/dist/commands/sessions.js.map +1 -1
  35. package/dist/commands/skills.d.ts.map +1 -1
  36. package/dist/commands/skills.js +5 -11
  37. package/dist/commands/skills.js.map +1 -1
  38. package/dist/commands/versions.d.ts.map +1 -1
  39. package/dist/commands/versions.js +31 -9
  40. package/dist/commands/versions.js.map +1 -1
  41. package/dist/commands/view.d.ts.map +1 -1
  42. package/dist/commands/view.js +38 -58
  43. package/dist/commands/view.js.map +1 -1
  44. package/dist/index.js +1 -0
  45. package/dist/index.js.map +1 -1
  46. package/dist/lib/__tests__/exec.test.js +50 -1
  47. package/dist/lib/__tests__/exec.test.js.map +1 -1
  48. package/dist/lib/__tests__/usage.test.d.ts +2 -0
  49. package/dist/lib/__tests__/usage.test.d.ts.map +1 -0
  50. package/dist/lib/__tests__/usage.test.js +145 -0
  51. package/dist/lib/__tests__/usage.test.js.map +1 -0
  52. package/dist/lib/agents.d.ts +19 -0
  53. package/dist/lib/agents.d.ts.map +1 -1
  54. package/dist/lib/agents.js +177 -16
  55. package/dist/lib/agents.js.map +1 -1
  56. package/dist/lib/exec.d.ts +3 -0
  57. package/dist/lib/exec.d.ts.map +1 -1
  58. package/dist/lib/exec.js +37 -0
  59. package/dist/lib/exec.js.map +1 -1
  60. package/dist/lib/registry.d.ts.map +1 -1
  61. package/dist/lib/registry.js +8 -0
  62. package/dist/lib/registry.js.map +1 -1
  63. package/dist/lib/resources.d.ts +2 -0
  64. package/dist/lib/resources.d.ts.map +1 -1
  65. package/dist/lib/resources.js +7 -7
  66. package/dist/lib/resources.js.map +1 -1
  67. package/dist/lib/session/discover.d.ts +10 -0
  68. package/dist/lib/session/discover.d.ts.map +1 -1
  69. package/dist/lib/session/discover.js +563 -119
  70. package/dist/lib/session/discover.js.map +1 -1
  71. package/dist/lib/session/prompt.d.ts +3 -0
  72. package/dist/lib/session/prompt.d.ts.map +1 -0
  73. package/dist/lib/session/prompt.js +41 -0
  74. package/dist/lib/session/prompt.js.map +1 -0
  75. package/dist/lib/session/render.d.ts.map +1 -1
  76. package/dist/lib/session/render.js +6 -37
  77. package/dist/lib/session/render.js.map +1 -1
  78. package/dist/lib/session/types.d.ts +7 -0
  79. package/dist/lib/session/types.d.ts.map +1 -1
  80. package/dist/lib/shims.d.ts +5 -0
  81. package/dist/lib/shims.d.ts.map +1 -1
  82. package/dist/lib/shims.js +20 -1
  83. package/dist/lib/shims.js.map +1 -1
  84. package/dist/lib/types.d.ts +1 -0
  85. package/dist/lib/types.d.ts.map +1 -1
  86. package/dist/lib/types.js.map +1 -1
  87. package/dist/lib/usage.d.ts +26 -0
  88. package/dist/lib/usage.d.ts.map +1 -1
  89. package/dist/lib/usage.js +125 -48
  90. package/dist/lib/usage.js.map +1 -1
  91. package/dist/lib/versions.d.ts +28 -0
  92. package/dist/lib/versions.d.ts.map +1 -1
  93. package/dist/lib/versions.js +181 -1
  94. package/dist/lib/versions.js.map +1 -1
  95. package/package.json +1 -1
@@ -4,11 +4,16 @@ import * as os from 'os';
4
4
  import * as crypto from 'crypto';
5
5
  import * as readline from 'readline';
6
6
  import { execSync } from 'child_process';
7
+ import { getCliVersion } from '../agents.js';
8
+ import { getConfigSymlinkVersion } from '../shims.js';
7
9
  import { SESSION_AGENTS } from './types.js';
10
+ import { extractSessionTopic } from './prompt.js';
8
11
  const HOME = os.homedir();
9
12
  const AGENTS_DIR = path.join(HOME, '.agents');
10
13
  const SESSIONS_DIR = path.join(AGENTS_DIR, 'sessions');
11
14
  const INDEX_PATH = path.join(SESSIONS_DIR, 'index.jsonl');
15
+ const CONTENT_INDEX_PATH = path.join(SESSIONS_DIR, 'content_index.jsonl');
16
+ const cachedAgentVersions = new Map();
12
17
  /**
13
18
  * Discover sessions across all installed agents, versions, and backups.
14
19
  * Merges with a persistent index so sessions survive version removal.
@@ -43,12 +48,27 @@ export async function discoverSessions(options) {
43
48
  toSave.set(s.id, s);
44
49
  }
45
50
  saveIndex([...toSave.values()]);
51
+ // Build content index for all discovered sessions
52
+ const contentIndex = await buildContentIndex(sessions);
53
+ saveContentIndex(contentIndex);
54
+ const projectQuery = options?.project?.trim();
46
55
  // Filter by project (case-insensitive substring match)
47
- if (options?.project) {
48
- const query = options.project.toLowerCase();
56
+ if (projectQuery) {
57
+ const query = projectQuery.toLowerCase();
49
58
  sessions = sessions.filter(s => s.project?.toLowerCase().includes(query));
50
59
  }
51
- if (!options?.all) {
60
+ // Apply time range filters
61
+ if (options?.since || options?.until) {
62
+ const sinceMs = options.since ? parseTimeFilter(options.since) : 0;
63
+ const untilMs = options.until ? new Date(options.until).getTime() : Infinity;
64
+ sessions = sessions.filter(s => {
65
+ const ts = new Date(s.timestamp).getTime();
66
+ return ts >= sinceMs && ts <= untilMs;
67
+ });
68
+ }
69
+ // An explicit project search should scan across directories instead of
70
+ // intersecting with the default cwd-only scope.
71
+ if (!options?.all && !projectQuery) {
52
72
  const currentDir = normalizeCwd(options?.cwd || process.cwd());
53
73
  sessions = sessions.filter(s => normalizeCwd(s.cwd) === currentDir);
54
74
  }
@@ -113,18 +133,7 @@ function saveIndex(sessions) {
113
133
  if (seen.has(s.id))
114
134
  continue;
115
135
  seen.add(s.id);
116
- lines.push(JSON.stringify({
117
- id: s.id,
118
- shortId: s.shortId,
119
- agent: s.agent,
120
- timestamp: s.timestamp,
121
- project: s.project,
122
- cwd: s.cwd,
123
- filePath: s.filePath,
124
- gitBranch: s.gitBranch,
125
- version: s.version,
126
- account: s.account,
127
- }));
136
+ lines.push(JSON.stringify(s));
128
137
  }
129
138
  fs.writeFileSync(INDEX_PATH, lines.join('\n') + '\n', 'utf-8');
130
139
  }
@@ -133,6 +142,81 @@ function saveIndex(sessions) {
133
142
  }
134
143
  }
135
144
  // ---------------------------------------------------------------------------
145
+ // Content index (inverted term -> session terms)
146
+ // ---------------------------------------------------------------------------
147
+ async function buildContentIndex(sessions) {
148
+ const index = new Map();
149
+ for (const session of sessions) {
150
+ const terms = extractSessionTerms(session);
151
+ for (const term of terms) {
152
+ if (!index.has(term))
153
+ index.set(term, new Set());
154
+ index.get(term).add(session.id);
155
+ }
156
+ }
157
+ return index;
158
+ }
159
+ function extractSessionTerms(session) {
160
+ const textParts = [];
161
+ if (session.topic)
162
+ textParts.push(session.topic);
163
+ if (session.project)
164
+ textParts.push(session.project);
165
+ if (session.cwd)
166
+ textParts.push(session.cwd);
167
+ if (session.gitBranch)
168
+ textParts.push(session.gitBranch);
169
+ if (session.account)
170
+ textParts.push(session.account);
171
+ if (session._userTerms)
172
+ textParts.push(session._userTerms.join('\n'));
173
+ return tokenizeText(textParts.join('\n'));
174
+ }
175
+ function tokenizeText(text) {
176
+ const seen = new Set();
177
+ const terms = [];
178
+ const tokens = text.toLowerCase().split(/[^a-z0-9]+/);
179
+ for (const token of tokens) {
180
+ if (token.length < 2 || seen.has(token))
181
+ continue;
182
+ seen.add(token);
183
+ terms.push(token);
184
+ }
185
+ return terms;
186
+ }
187
+ function saveContentIndex(index) {
188
+ try {
189
+ fs.mkdirSync(SESSIONS_DIR, { recursive: true });
190
+ const lines = [];
191
+ for (const [term, sessionIds] of index) {
192
+ lines.push(JSON.stringify({ term, sessions: [...sessionIds] }));
193
+ }
194
+ fs.writeFileSync(CONTENT_INDEX_PATH, lines.join('\n') + '\n', 'utf-8');
195
+ }
196
+ catch { /* index save failure is non-fatal */ }
197
+ }
198
+ function loadContentIndex() {
199
+ const index = new Map();
200
+ if (!fs.existsSync(CONTENT_INDEX_PATH))
201
+ return index;
202
+ try {
203
+ const content = fs.readFileSync(CONTENT_INDEX_PATH, 'utf-8');
204
+ for (const line of content.split('\n')) {
205
+ if (!line.trim())
206
+ continue;
207
+ try {
208
+ const entry = JSON.parse(line);
209
+ if (entry.term && entry.sessions) {
210
+ index.set(entry.term, new Set(entry.sessions));
211
+ }
212
+ }
213
+ catch { /* malformed index entry, skip */ }
214
+ }
215
+ }
216
+ catch { /* index load failure is non-fatal */ }
217
+ return index;
218
+ }
219
+ // ---------------------------------------------------------------------------
136
220
  // Multi-version directory scanning
137
221
  // ---------------------------------------------------------------------------
138
222
  /**
@@ -269,41 +353,25 @@ async function discoverClaudeSessions() {
269
353
  return sessions;
270
354
  }
271
355
  async function readClaudeMeta(filePath, sessionId, account) {
272
- const lines = await readFirstLines(filePath, 10);
273
- let topic;
274
- for (const line of lines) {
275
- let parsed;
276
- try {
277
- parsed = JSON.parse(line);
278
- }
279
- catch {
280
- continue;
281
- }
282
- // Extract topic from first user message
283
- if (!topic && parsed.type === 'user' && parsed.message?.content) {
284
- const text = Array.isArray(parsed.message.content)
285
- ? parsed.message.content.find((b) => b.type === 'text')?.text
286
- : typeof parsed.message.content === 'string' ? parsed.message.content : undefined;
287
- if (text)
288
- topic = extractTopic(text);
289
- }
290
- // Look for first user or assistant line with timestamp/cwd
291
- if ((parsed.type === 'user' || parsed.type === 'assistant') && parsed.timestamp) {
292
- const cwd = parsed.cwd || '';
293
- return {
294
- id: sessionId,
295
- shortId: sessionId.slice(0, 8),
296
- agent: 'claude',
297
- timestamp: parsed.timestamp,
298
- project: cwd ? path.basename(cwd) : undefined,
299
- cwd,
300
- filePath,
301
- gitBranch: parsed.gitBranch || undefined,
302
- version: parsed.version || undefined,
303
- account,
304
- topic,
305
- };
306
- }
356
+ const scan = await scanClaudeSession(filePath);
357
+ if (scan.timestamp) {
358
+ const cwd = scan.cwd || '';
359
+ return {
360
+ id: sessionId,
361
+ shortId: sessionId.slice(0, 8),
362
+ agent: 'claude',
363
+ timestamp: scan.timestamp,
364
+ project: cwd ? path.basename(cwd) : undefined,
365
+ cwd,
366
+ filePath,
367
+ gitBranch: scan.gitBranch,
368
+ version: scan.version,
369
+ account,
370
+ topic: scan.topic,
371
+ messageCount: scan.messageCount,
372
+ tokenCount: scan.tokenCount,
373
+ _userTerms: scan.userTerms?.flatMap(splitLines),
374
+ };
307
375
  }
308
376
  // Fallback: use file mtime
309
377
  const stat = safeStatSync(filePath);
@@ -314,6 +382,10 @@ async function readClaudeMeta(filePath, sessionId, account) {
314
382
  timestamp: stat ? stat.mtime.toISOString() : new Date().toISOString(),
315
383
  filePath,
316
384
  account,
385
+ messageCount: scan.messageCount,
386
+ tokenCount: scan.tokenCount,
387
+ topic: scan.topic,
388
+ _userTerms: scan.userTerms?.flatMap(splitLines),
317
389
  };
318
390
  }
319
391
  // ---------------------------------------------------------------------------
@@ -366,12 +438,13 @@ async function discoverCodexSessions() {
366
438
  const sessions = [];
367
439
  const seen = new Set();
368
440
  const account = getCodexAccount();
441
+ const currentVersion = await getCurrentAgentVersion('codex');
369
442
  let skipped = 0;
370
443
  for (const sessionsDir of getAgentSessionDirs('codex', 'sessions')) {
371
444
  const jsonlFiles = walkForFiles(sessionsDir, '.jsonl', 200);
372
445
  for (const filePath of jsonlFiles) {
373
446
  try {
374
- const meta = await readCodexMeta(filePath, account);
447
+ const meta = await readCodexMeta(filePath, account, currentVersion);
375
448
  if (meta && !seen.has(meta.id)) {
376
449
  seen.add(meta.id);
377
450
  sessions.push(meta);
@@ -387,52 +460,27 @@ async function discoverCodexSessions() {
387
460
  }
388
461
  return sessions;
389
462
  }
390
- async function readCodexMeta(filePath, account) {
391
- const lines = await readFirstLines(filePath, 5);
392
- if (lines.length === 0)
393
- return null;
394
- let parsed;
395
- try {
396
- parsed = JSON.parse(lines[0]);
397
- }
398
- catch {
399
- return null;
400
- }
401
- if (parsed.type !== 'session_meta')
402
- return null;
403
- const payload = parsed.payload || {};
404
- const sessionId = payload.id || '';
463
+ async function readCodexMeta(filePath, account, currentVersion) {
464
+ const scan = await scanCodexSession(filePath);
465
+ const sessionId = scan.sessionId || '';
405
466
  if (!sessionId)
406
467
  return null;
407
- // Extract topic from first user message in subsequent lines
408
- let topic;
409
- for (let i = 1; i < lines.length; i++) {
410
- try {
411
- const ev = JSON.parse(lines[i]);
412
- if (ev.type === 'message' && ev.role === 'user' && ev.content) {
413
- const text = typeof ev.content === 'string' ? ev.content
414
- : Array.isArray(ev.content) ? ev.content.find((b) => b.type === 'input_text')?.text : undefined;
415
- if (text) {
416
- topic = extractTopic(text);
417
- break;
418
- }
419
- }
420
- }
421
- catch { /* malformed event line */ }
422
- }
423
- const cwd = payload.cwd || '';
468
+ const cwd = scan.cwd || '';
424
469
  return {
425
470
  id: sessionId,
426
471
  shortId: sessionId.slice(0, 8),
427
472
  agent: 'codex',
428
- timestamp: payload.timestamp || parsed.timestamp || new Date().toISOString(),
473
+ timestamp: scan.timestamp || new Date().toISOString(),
429
474
  project: cwd ? path.basename(cwd) : undefined,
430
475
  cwd,
431
476
  filePath,
432
- gitBranch: payload.git?.branch || undefined,
433
- version: payload.version || undefined,
434
- topic,
477
+ gitBranch: scan.gitBranch,
478
+ version: resolveSessionVersion('codex', filePath, scan.version, currentVersion),
479
+ topic: scan.topic,
480
+ messageCount: scan.messageCount,
481
+ tokenCount: scan.tokenCount,
435
482
  account,
483
+ _userTerms: scan.userTerms?.flatMap(splitLines),
436
484
  };
437
485
  }
438
486
  // ---------------------------------------------------------------------------
@@ -442,6 +490,7 @@ async function discoverGeminiSessions() {
442
490
  const projectMap = buildGeminiProjectMap();
443
491
  const sessions = [];
444
492
  const seen = new Set();
493
+ const currentVersion = await getCurrentAgentVersion('gemini');
445
494
  let skipped = 0;
446
495
  for (const tmpDir of getAgentSessionDirs('gemini', 'tmp')) {
447
496
  let hashDirs;
@@ -465,7 +514,7 @@ async function discoverGeminiSessions() {
465
514
  for (const file of chatFiles) {
466
515
  const filePath = path.join(chatsDir, file);
467
516
  try {
468
- const meta = readGeminiMeta(filePath, hashDir, projectMap);
517
+ const meta = readGeminiMeta(filePath, hashDir, projectMap, currentVersion);
469
518
  if (meta && !seen.has(meta.id)) {
470
519
  seen.add(meta.id);
471
520
  sessions.push(meta);
@@ -482,17 +531,22 @@ async function discoverGeminiSessions() {
482
531
  }
483
532
  return sessions;
484
533
  }
485
- function readGeminiMeta(filePath, hashDir, projectMap) {
486
- // Read the first ~2KB to get top-level fields without parsing entire messages array
487
- const fd = fs.openSync(filePath, 'r');
488
- const buf = Buffer.alloc(2048);
489
- const bytesRead = fs.readSync(fd, buf, 0, 2048, 0);
490
- fs.closeSync(fd);
491
- const header = buf.toString('utf-8', 0, bytesRead);
492
- // Extract fields via regex (avoids parsing potentially huge messages array)
493
- const sessionId = extractJsonField(header, 'sessionId');
494
- const startTime = extractJsonField(header, 'startTime');
495
- const projectHash = extractJsonField(header, 'projectHash');
534
+ function readGeminiMeta(filePath, hashDir, projectMap, currentVersion) {
535
+ let session;
536
+ try {
537
+ session = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
538
+ }
539
+ catch {
540
+ return null;
541
+ }
542
+ const sessionId = typeof session.sessionId === 'string' ? session.sessionId : '';
543
+ const startTime = typeof session.startTime === 'string' ? session.startTime : '';
544
+ const projectHash = typeof session.projectHash === 'string' ? session.projectHash : '';
545
+ const embeddedVersion = typeof session.version === 'string'
546
+ ? session.version
547
+ : typeof session.cliVersion === 'string'
548
+ ? session.cliVersion
549
+ : undefined;
496
550
  if (!sessionId)
497
551
  return null;
498
552
  // Resolve project name from hash
@@ -500,11 +554,33 @@ function readGeminiMeta(filePath, hashDir, projectMap) {
500
554
  const project = projectInfo?.name || hashDir.slice(0, 12);
501
555
  const cwd = projectInfo?.path;
502
556
  const stat = safeStatSync(filePath);
503
- // Try to extract first user message from the header bytes
557
+ const messages = Array.isArray(session.messages) ? session.messages : [];
504
558
  let topic;
505
- const userMsgMatch = header.match(/"role"\s*:\s*"user"[\s\S]*?"text"\s*:\s*"([^"]{1,200})"/);
506
- if (userMsgMatch)
507
- topic = extractTopic(userMsgMatch[1]);
559
+ let messageCount = 0;
560
+ let tokenCount = 0;
561
+ let sawTokenCount = false;
562
+ const userTerms = [];
563
+ for (const message of messages) {
564
+ if (message.type === 'user') {
565
+ const text = extractGeminiMessageText(message.content);
566
+ if (text) {
567
+ messageCount++;
568
+ userTerms.push(text);
569
+ if (!topic)
570
+ topic = extractSessionTopic(text);
571
+ }
572
+ }
573
+ else if (message.type === 'gemini') {
574
+ if (extractGeminiMessageText(message.content)) {
575
+ messageCount++;
576
+ }
577
+ }
578
+ const total = getGeminiTokenCount(message.tokens);
579
+ if (total !== null) {
580
+ tokenCount += total;
581
+ sawTokenCount = true;
582
+ }
583
+ }
508
584
  return {
509
585
  id: sessionId,
510
586
  shortId: sessionId.slice(0, 8),
@@ -513,7 +589,11 @@ function readGeminiMeta(filePath, hashDir, projectMap) {
513
589
  project,
514
590
  cwd,
515
591
  filePath,
592
+ version: resolveSessionVersion('gemini', filePath, embeddedVersion, currentVersion),
516
593
  topic,
594
+ messageCount,
595
+ tokenCount: sawTokenCount ? tokenCount : undefined,
596
+ _userTerms: userTerms.length > 0 ? userTerms : undefined,
517
597
  };
518
598
  }
519
599
  function buildGeminiProjectMap() {
@@ -594,13 +674,37 @@ async function discoverOpenCodeSessions() {
594
674
  if (!fs.existsSync(OPENCODE_DB))
595
675
  return [];
596
676
  const account = getOpenCodeAccount();
677
+ const currentVersion = await getCurrentAgentVersion('opencode');
597
678
  try {
598
679
  // Query sessions. time_created is millisecond epoch. Limit to 200 most recent.
599
680
  // Use session.title as topic (OpenCode auto-generates good titles).
600
681
  const query = `
601
- SELECT id, title, directory, version, time_created
602
- FROM session
603
- WHERE parent_id IS NULL
682
+ SELECT
683
+ s.id,
684
+ s.title,
685
+ s.directory,
686
+ s.version,
687
+ s.time_created,
688
+ COALESCE(stats.message_count, 0),
689
+ stats.token_count,
690
+ COALESCE(stats.has_token_data, 0)
691
+ FROM session s
692
+ LEFT JOIN (
693
+ SELECT
694
+ session_id,
695
+ COUNT(*) AS message_count,
696
+ SUM(
697
+ COALESCE(json_extract(data, '$.tokens.input'), 0) +
698
+ COALESCE(json_extract(data, '$.tokens.output'), 0) +
699
+ COALESCE(json_extract(data, '$.tokens.reasoning'), 0) +
700
+ COALESCE(json_extract(data, '$.tokens.cache.read'), 0) +
701
+ COALESCE(json_extract(data, '$.tokens.cache.write'), 0)
702
+ ) AS token_count,
703
+ MAX(CASE WHEN json_type(data, '$.tokens') IS NOT NULL THEN 1 ELSE 0 END) AS has_token_data
704
+ FROM message
705
+ GROUP BY session_id
706
+ ) stats ON stats.session_id = s.id
707
+ WHERE s.parent_id IS NULL
604
708
  ORDER BY time_created DESC
605
709
  LIMIT 200;
606
710
  `.replace(/\n/g, ' ');
@@ -609,10 +713,13 @@ async function discoverOpenCodeSessions() {
609
713
  for (const line of out.split('\n')) {
610
714
  if (!line.trim())
611
715
  continue;
612
- const [id, title, directory, version, timeCreatedStr] = line.split('|||');
716
+ const [id, title, directory, version, timeCreatedStr, messageCountStr, tokenCountStr, hasTokenDataStr] = line.split('|||');
613
717
  if (!id)
614
718
  continue;
615
719
  const timeCreated = parseInt(timeCreatedStr, 10);
720
+ const messageCount = parseInt(messageCountStr, 10);
721
+ const tokenCount = parseInt(tokenCountStr, 10);
722
+ const hasTokenData = hasTokenDataStr === '1';
616
723
  const timestamp = isNaN(timeCreated) ? new Date().toISOString() : new Date(timeCreated).toISOString();
617
724
  const topic = title || undefined;
618
725
  sessions.push({
@@ -623,9 +730,11 @@ async function discoverOpenCodeSessions() {
623
730
  project: directory ? path.basename(directory) : undefined,
624
731
  cwd: directory || undefined,
625
732
  filePath: `${OPENCODE_DB}#${id}`,
626
- version: version || undefined,
733
+ version: resolveSessionVersion('opencode', OPENCODE_DB, version || undefined, currentVersion),
627
734
  account,
628
735
  topic,
736
+ messageCount: Number.isNaN(messageCount) ? undefined : messageCount,
737
+ tokenCount: hasTokenData && !Number.isNaN(tokenCount) ? tokenCount : undefined,
629
738
  });
630
739
  }
631
740
  return sessions;
@@ -649,6 +758,7 @@ async function discoverOpenClawSessions() {
649
758
  catch {
650
759
  return sessions;
651
760
  }
761
+ const currentVersion = await getCurrentAgentVersion('openclaw');
652
762
  // Discover active channels
653
763
  // Format: "- Telegram default (Jeff): enabled, configured, running, out:2h ago, mode:polling, token:config"
654
764
  try {
@@ -671,6 +781,7 @@ async function discoverOpenClawSessions() {
671
781
  agent: 'openclaw',
672
782
  timestamp: new Date().toISOString(),
673
783
  project: name,
784
+ version: currentVersion,
674
785
  filePath: '',
675
786
  });
676
787
  }
@@ -713,6 +824,7 @@ async function discoverOpenClawSessions() {
713
824
  timestamp: new Date().toISOString(),
714
825
  project: `${jobName} (${agentId || 'unknown'})`,
715
826
  cwd: status,
827
+ version: currentVersion,
716
828
  filePath: '',
717
829
  });
718
830
  }
@@ -722,6 +834,150 @@ async function discoverOpenClawSessions() {
722
834
  }
723
835
  return sessions;
724
836
  }
837
+ async function scanClaudeSession(filePath) {
838
+ const stream = fs.createReadStream(filePath, { encoding: 'utf-8' });
839
+ const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
840
+ let timestamp;
841
+ let cwd;
842
+ let gitBranch;
843
+ let version;
844
+ let topic;
845
+ let messageCount = 0;
846
+ let tokenCount = 0;
847
+ let sawTokenCount = false;
848
+ const seenAssistantIds = new Set();
849
+ const userTexts = [];
850
+ try {
851
+ for await (const line of rl) {
852
+ if (!line.trim())
853
+ continue;
854
+ let parsed;
855
+ try {
856
+ parsed = JSON.parse(line);
857
+ }
858
+ catch {
859
+ continue;
860
+ }
861
+ if (!timestamp && (parsed.type === 'user' || parsed.type === 'assistant') && parsed.timestamp) {
862
+ timestamp = parsed.timestamp;
863
+ cwd = parsed.cwd || '';
864
+ gitBranch = parsed.gitBranch || undefined;
865
+ version = parsed.version || undefined;
866
+ }
867
+ if (parsed.type === 'user') {
868
+ const text = extractClaudeUserText(parsed);
869
+ if (text) {
870
+ messageCount++;
871
+ userTexts.push(text);
872
+ if (!topic)
873
+ topic = extractSessionTopic(text);
874
+ }
875
+ continue;
876
+ }
877
+ if (parsed.type !== 'assistant')
878
+ continue;
879
+ const assistantId = typeof parsed.message?.id === 'string'
880
+ ? parsed.message.id
881
+ : typeof parsed.uuid === 'string'
882
+ ? parsed.uuid
883
+ : undefined;
884
+ const logicalId = assistantId || `${parsed.timestamp || ''}:${seenAssistantIds.size}`;
885
+ if (seenAssistantIds.has(logicalId))
886
+ continue;
887
+ seenAssistantIds.add(logicalId);
888
+ messageCount++;
889
+ const usage = getClaudeUsageTotal(parsed.message?.usage || parsed.usage);
890
+ if (usage !== null) {
891
+ tokenCount += usage;
892
+ sawTokenCount = true;
893
+ }
894
+ }
895
+ }
896
+ finally {
897
+ rl.close();
898
+ stream.destroy();
899
+ }
900
+ return {
901
+ timestamp,
902
+ cwd,
903
+ gitBranch,
904
+ version,
905
+ topic,
906
+ messageCount,
907
+ tokenCount: sawTokenCount ? tokenCount : undefined,
908
+ userTerms: userTexts.length > 0 ? userTexts : undefined,
909
+ };
910
+ }
911
+ async function scanCodexSession(filePath) {
912
+ const stream = fs.createReadStream(filePath, { encoding: 'utf-8' });
913
+ const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
914
+ let sessionId;
915
+ let timestamp;
916
+ let cwd;
917
+ let gitBranch;
918
+ let version;
919
+ let topic;
920
+ let messageCount = 0;
921
+ let tokenCount;
922
+ const userTexts = [];
923
+ try {
924
+ for await (const line of rl) {
925
+ if (!line.trim())
926
+ continue;
927
+ let parsed;
928
+ try {
929
+ parsed = JSON.parse(line);
930
+ }
931
+ catch {
932
+ continue;
933
+ }
934
+ if (parsed.type === 'session_meta') {
935
+ const payload = parsed.payload || {};
936
+ sessionId = payload.id || sessionId;
937
+ timestamp = payload.timestamp || parsed.timestamp || timestamp;
938
+ cwd = payload.cwd || cwd;
939
+ gitBranch = payload.git?.branch || gitBranch;
940
+ version = payload.cli_version || payload.version || version;
941
+ continue;
942
+ }
943
+ if (parsed.type === 'response_item' && parsed.payload?.type === 'message') {
944
+ const role = parsed.payload.role === 'user' || parsed.payload.role === 'developer'
945
+ ? 'user'
946
+ : 'assistant';
947
+ const text = extractCodexMessageText(parsed.payload.content, role);
948
+ if (!text)
949
+ continue;
950
+ messageCount++;
951
+ if (role === 'user') {
952
+ userTexts.push(text);
953
+ if (!topic)
954
+ topic = extractSessionTopic(text);
955
+ }
956
+ continue;
957
+ }
958
+ if (parsed.type === 'event_msg' && parsed.payload?.type === 'token_count') {
959
+ const total = getCodexTokenCount(parsed.payload.info?.total_token_usage);
960
+ if (total !== null)
961
+ tokenCount = total;
962
+ }
963
+ }
964
+ }
965
+ finally {
966
+ rl.close();
967
+ stream.destroy();
968
+ }
969
+ return {
970
+ sessionId,
971
+ timestamp,
972
+ cwd,
973
+ gitBranch,
974
+ version,
975
+ topic,
976
+ messageCount,
977
+ tokenCount,
978
+ userTerms: userTexts.length > 0 ? userTexts : undefined,
979
+ };
980
+ }
725
981
  // ---------------------------------------------------------------------------
726
982
  // Utilities
727
983
  // ---------------------------------------------------------------------------
@@ -777,11 +1033,6 @@ export function walkForFiles(dir, ext, limit) {
777
1033
  results.sort((a, b) => b.mtime - a.mtime);
778
1034
  return results.slice(0, limit).map(r => r.path);
779
1035
  }
780
- function extractJsonField(text, field) {
781
- const re = new RegExp(`"${field}"\\s*:\\s*"([^"]*)"`, 'i');
782
- const match = text.match(re);
783
- return match ? match[1] : '';
784
- }
785
1036
  function sha256(input) {
786
1037
  return crypto.createHash('sha256').update(input).digest('hex');
787
1038
  }
@@ -801,11 +1052,204 @@ function safeRealpathSync(p) {
801
1052
  return null;
802
1053
  }
803
1054
  }
804
- function extractTopic(text) {
805
- // Strip leading whitespace, slash commands, and system tags
806
- let clean = text.replace(/^[\s\n]+/, '').replace(/<[^>]+>/g, '').trim();
807
- // Take first line only
808
- const firstLine = clean.split('\n')[0].trim();
809
- return firstLine;
1055
+ function extractClaudeUserText(parsed) {
1056
+ if (parsed.isMeta === true)
1057
+ return undefined;
1058
+ const content = parsed.message?.content;
1059
+ if (typeof content === 'string') {
1060
+ const text = content.trim();
1061
+ return isLocalCommandMessage(text) ? undefined : text || undefined;
1062
+ }
1063
+ if (!Array.isArray(content))
1064
+ return undefined;
1065
+ const text = content
1066
+ .filter((block) => block.type === 'text')
1067
+ .map((block) => String(block.text || '').trim())
1068
+ .find((value) => value && !value.startsWith('[Request interrupted'));
1069
+ if (!text || isLocalCommandMessage(text))
1070
+ return undefined;
1071
+ return text;
1072
+ }
1073
+ function isLocalCommandMessage(text) {
1074
+ return /<local-command-caveat>|<bash-(input|stdout|stderr)>/i.test(text);
1075
+ }
1076
+ function splitLines(text) {
1077
+ return text.split('\n').map(l => l.trim()).filter(Boolean);
1078
+ }
1079
+ function getClaudeUsageTotal(usage) {
1080
+ if (!usage || typeof usage !== 'object')
1081
+ return null;
1082
+ return sumKnownNumbers([
1083
+ usage.input_tokens,
1084
+ usage.output_tokens,
1085
+ usage.cache_creation_input_tokens,
1086
+ usage.cache_read_input_tokens,
1087
+ ]);
1088
+ }
1089
+ function extractCodexMessageText(contentBlocks, role) {
1090
+ if (!Array.isArray(contentBlocks))
1091
+ return undefined;
1092
+ const matches = role === 'user'
1093
+ ? contentBlocks.filter((block) => block.type === 'input_text')
1094
+ : contentBlocks.filter((block) => block.type === 'output_text');
1095
+ const text = matches
1096
+ .map((block) => String(block.text || '').trim())
1097
+ .find((value) => {
1098
+ if (!value)
1099
+ return false;
1100
+ if (role === 'user' && (value.length >= 2000 || value.includes('<permissions instructions>') || value.startsWith('# AGENTS.md instructions'))) {
1101
+ return false;
1102
+ }
1103
+ return true;
1104
+ });
1105
+ return text || undefined;
1106
+ }
1107
+ function normalizeVersion(version) {
1108
+ const trimmed = version?.trim();
1109
+ return trimmed ? trimmed : undefined;
1110
+ }
1111
+ function extractVersionFromManagedPath(agent, sourcePath) {
1112
+ if (!sourcePath)
1113
+ return undefined;
1114
+ const candidates = [sourcePath, safeRealpathSync(sourcePath) || ''];
1115
+ const marker = `/.agents/versions/${agent}/`;
1116
+ for (const candidate of candidates) {
1117
+ if (!candidate)
1118
+ continue;
1119
+ const normalized = candidate.split(path.sep).join('/');
1120
+ const start = normalized.indexOf(marker);
1121
+ if (start === -1)
1122
+ continue;
1123
+ const version = normalized.slice(start + marker.length).split('/')[0];
1124
+ if (version)
1125
+ return version;
1126
+ }
1127
+ return undefined;
1128
+ }
1129
+ async function getCurrentAgentVersion(agent) {
1130
+ const cached = cachedAgentVersions.get(agent);
1131
+ if (cached)
1132
+ return cached;
1133
+ const promise = (async () => {
1134
+ const symlinkVersion = normalizeVersion(getConfigSymlinkVersion(agent));
1135
+ if (symlinkVersion)
1136
+ return symlinkVersion;
1137
+ return normalizeVersion(await getCliVersion(agent));
1138
+ })();
1139
+ cachedAgentVersions.set(agent, promise);
1140
+ return promise;
1141
+ }
1142
+ function resolveSessionVersion(agent, sourcePath, embeddedVersion, currentVersion) {
1143
+ return normalizeVersion(embeddedVersion)
1144
+ || extractVersionFromManagedPath(agent, sourcePath)
1145
+ || normalizeVersion(currentVersion);
1146
+ }
1147
+ function getCodexTokenCount(totalTokenUsage) {
1148
+ if (!totalTokenUsage || typeof totalTokenUsage !== 'object')
1149
+ return null;
1150
+ return sumKnownNumbers([
1151
+ totalTokenUsage.input_tokens,
1152
+ totalTokenUsage.cached_input_tokens,
1153
+ totalTokenUsage.output_tokens,
1154
+ totalTokenUsage.reasoning_output_tokens,
1155
+ ]);
1156
+ }
1157
+ function extractGeminiMessageText(content) {
1158
+ if (typeof content === 'string')
1159
+ return content.trim();
1160
+ if (Array.isArray(content)) {
1161
+ return content
1162
+ .map((part) => {
1163
+ if (typeof part === 'string')
1164
+ return part;
1165
+ if (typeof part?.text === 'string')
1166
+ return part.text;
1167
+ return '';
1168
+ })
1169
+ .join('\n')
1170
+ .trim();
1171
+ }
1172
+ return '';
1173
+ }
1174
+ function getGeminiTokenCount(tokens) {
1175
+ if (!tokens || typeof tokens !== 'object')
1176
+ return null;
1177
+ if (typeof tokens.total === 'number')
1178
+ return tokens.total;
1179
+ return sumKnownNumbers([
1180
+ tokens.input,
1181
+ tokens.output,
1182
+ tokens.cached,
1183
+ tokens.thoughts,
1184
+ tokens.tool,
1185
+ ]);
1186
+ }
1187
+ function sumKnownNumbers(values) {
1188
+ let total = 0;
1189
+ let found = false;
1190
+ for (const value of values) {
1191
+ if (typeof value !== 'number' || Number.isNaN(value))
1192
+ continue;
1193
+ total += value;
1194
+ found = true;
1195
+ }
1196
+ return found ? total : null;
1197
+ }
1198
+ // ---------------------------------------------------------------------------
1199
+ // Time range parsing
1200
+ // ---------------------------------------------------------------------------
1201
+ export function parseTimeFilter(input) {
1202
+ const relativeMatch = input.match(/^(\d+)([dw])$/i);
1203
+ if (relativeMatch) {
1204
+ const value = parseInt(relativeMatch[1], 10);
1205
+ const unit = relativeMatch[2].toLowerCase();
1206
+ if (unit === 'd')
1207
+ return Date.now() - value * 86_400_000;
1208
+ if (unit === 'w')
1209
+ return Date.now() - value * 7 * 86_400_000;
1210
+ }
1211
+ const ts = new Date(input).getTime();
1212
+ return Number.isNaN(ts) ? 0 : ts;
1213
+ }
1214
+ // ---------------------------------------------------------------------------
1215
+ // Content index search
1216
+ // ---------------------------------------------------------------------------
1217
+ /**
1218
+ * Score sessions by matching against terms from the content index.
1219
+ * Returns sessions with matched terms attached for highlighting.
1220
+ */
1221
+ export function searchContentIndex(sessions, query) {
1222
+ const index = loadContentIndex();
1223
+ if (index.size === 0)
1224
+ return new Map();
1225
+ const terms = tokenizeText(query);
1226
+ if (terms.length === 0)
1227
+ return new Map();
1228
+ const scored = new Map();
1229
+ for (const term of terms) {
1230
+ const matchingSessions = index.get(term);
1231
+ if (!matchingSessions)
1232
+ continue;
1233
+ for (const sessionId of matchingSessions) {
1234
+ const entry = scored.get(sessionId);
1235
+ if (!entry) {
1236
+ scored.set(sessionId, { score: 1, matchedTerms: [term] });
1237
+ }
1238
+ else {
1239
+ entry.score += 1;
1240
+ if (!entry.matchedTerms.includes(term)) {
1241
+ entry.matchedTerms.push(term);
1242
+ }
1243
+ }
1244
+ }
1245
+ }
1246
+ const result = new Map();
1247
+ for (const [sessionId, info] of scored) {
1248
+ const session = sessions.find(s => s.id === sessionId);
1249
+ if (session) {
1250
+ result.set(sessionId, { ...session, _matchedTerms: info.matchedTerms });
1251
+ }
1252
+ }
1253
+ return result;
810
1254
  }
811
1255
  //# sourceMappingURL=discover.js.map