@swarmify/agents-cli 1.10.3 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +71 -0
  3. package/dist/commands/__tests__/sessions.test.d.ts +2 -0
  4. package/dist/commands/__tests__/sessions.test.d.ts.map +1 -0
  5. package/dist/commands/__tests__/sessions.test.js +436 -0
  6. package/dist/commands/__tests__/sessions.test.js.map +1 -0
  7. package/dist/commands/commands.d.ts.map +1 -1
  8. package/dist/commands/commands.js +79 -67
  9. package/dist/commands/commands.js.map +1 -1
  10. package/dist/commands/daemon.js +1 -1
  11. package/dist/commands/daemon.js.map +1 -1
  12. package/dist/commands/exec.d.ts.map +1 -1
  13. package/dist/commands/exec.js +11 -1
  14. package/dist/commands/exec.js.map +1 -1
  15. package/dist/commands/hooks.d.ts.map +1 -1
  16. package/dist/commands/hooks.js +72 -60
  17. package/dist/commands/hooks.js.map +1 -1
  18. package/dist/commands/mcp.d.ts.map +1 -1
  19. package/dist/commands/mcp.js +134 -57
  20. package/dist/commands/mcp.js.map +1 -1
  21. package/dist/commands/packages.d.ts.map +1 -1
  22. package/dist/commands/packages.js +92 -42
  23. package/dist/commands/packages.js.map +1 -1
  24. package/dist/commands/permissions.d.ts.map +1 -1
  25. package/dist/commands/permissions.js +108 -81
  26. package/dist/commands/permissions.js.map +1 -1
  27. package/dist/commands/plugins.js +8 -8
  28. package/dist/commands/plugins.js.map +1 -1
  29. package/dist/commands/pty.d.ts +20 -0
  30. package/dist/commands/pty.d.ts.map +1 -0
  31. package/dist/commands/pty.js +280 -0
  32. package/dist/commands/pty.js.map +1 -0
  33. package/dist/commands/pull.d.ts.map +1 -1
  34. package/dist/commands/pull.js +27 -27
  35. package/dist/commands/pull.js.map +1 -1
  36. package/dist/commands/push.d.ts.map +1 -1
  37. package/dist/commands/push.js +4 -2
  38. package/dist/commands/push.js.map +1 -1
  39. package/dist/commands/routines.d.ts.map +1 -1
  40. package/dist/commands/routines.js +14 -10
  41. package/dist/commands/routines.js.map +1 -1
  42. package/dist/commands/rules.d.ts.map +1 -1
  43. package/dist/commands/rules.js +70 -61
  44. package/dist/commands/rules.js.map +1 -1
  45. package/dist/commands/sessions.d.ts.map +1 -1
  46. package/dist/commands/sessions.js +369 -44
  47. package/dist/commands/sessions.js.map +1 -1
  48. package/dist/commands/sessions.test.d.ts +2 -0
  49. package/dist/commands/sessions.test.d.ts.map +1 -0
  50. package/dist/commands/sessions.test.js +53 -0
  51. package/dist/commands/sessions.test.js.map +1 -0
  52. package/dist/commands/skills.d.ts.map +1 -1
  53. package/dist/commands/skills.js +75 -57
  54. package/dist/commands/skills.js.map +1 -1
  55. package/dist/commands/subagents.d.ts.map +1 -1
  56. package/dist/commands/subagents.js +10 -4
  57. package/dist/commands/subagents.js.map +1 -1
  58. package/dist/commands/utils.d.ts +16 -0
  59. package/dist/commands/utils.d.ts.map +1 -1
  60. package/dist/commands/utils.js +48 -0
  61. package/dist/commands/utils.js.map +1 -1
  62. package/dist/commands/versions.d.ts.map +1 -1
  63. package/dist/commands/versions.js +148 -43
  64. package/dist/commands/versions.js.map +1 -1
  65. package/dist/commands/view.d.ts.map +1 -1
  66. package/dist/commands/view.js +109 -45
  67. package/dist/commands/view.js.map +1 -1
  68. package/dist/index.js +66 -42
  69. package/dist/index.js.map +1 -1
  70. package/dist/lib/__tests__/exec.test.js +34 -1
  71. package/dist/lib/__tests__/exec.test.js.map +1 -1
  72. package/dist/lib/agents.d.ts +23 -0
  73. package/dist/lib/agents.d.ts.map +1 -1
  74. package/dist/lib/agents.js +212 -16
  75. package/dist/lib/agents.js.map +1 -1
  76. package/dist/lib/daemon.d.ts.map +1 -1
  77. package/dist/lib/daemon.js +15 -7
  78. package/dist/lib/daemon.js.map +1 -1
  79. package/dist/lib/exec.d.ts +3 -0
  80. package/dist/lib/exec.d.ts.map +1 -1
  81. package/dist/lib/exec.js +26 -0
  82. package/dist/lib/exec.js.map +1 -1
  83. package/dist/lib/git.d.ts.map +1 -1
  84. package/dist/lib/git.js +11 -1
  85. package/dist/lib/git.js.map +1 -1
  86. package/dist/lib/pty-client.d.ts +22 -0
  87. package/dist/lib/pty-client.d.ts.map +1 -0
  88. package/dist/lib/pty-client.js +181 -0
  89. package/dist/lib/pty-client.js.map +1 -0
  90. package/dist/lib/pty-server.d.ts +16 -0
  91. package/dist/lib/pty-server.d.ts.map +1 -0
  92. package/dist/lib/pty-server.js +422 -0
  93. package/dist/lib/pty-server.js.map +1 -0
  94. package/dist/lib/registry.d.ts.map +1 -1
  95. package/dist/lib/registry.js +8 -0
  96. package/dist/lib/registry.js.map +1 -1
  97. package/dist/lib/resources.d.ts +2 -0
  98. package/dist/lib/resources.d.ts.map +1 -1
  99. package/dist/lib/resources.js +7 -7
  100. package/dist/lib/resources.js.map +1 -1
  101. package/dist/lib/runner.d.ts.map +1 -1
  102. package/dist/lib/runner.js +13 -9
  103. package/dist/lib/runner.js.map +1 -1
  104. package/dist/lib/sandbox.js +1 -1
  105. package/dist/lib/sandbox.js.map +1 -1
  106. package/dist/lib/session/discover.d.ts +18 -0
  107. package/dist/lib/session/discover.d.ts.map +1 -1
  108. package/dist/lib/session/discover.js +405 -167
  109. package/dist/lib/session/discover.js.map +1 -1
  110. package/dist/lib/session/parse.d.ts.map +1 -1
  111. package/dist/lib/session/parse.js +8 -3
  112. package/dist/lib/session/parse.js.map +1 -1
  113. package/dist/lib/session/prompt.d.ts +3 -0
  114. package/dist/lib/session/prompt.d.ts.map +1 -0
  115. package/dist/lib/session/prompt.js +40 -0
  116. package/dist/lib/session/prompt.js.map +1 -0
  117. package/dist/lib/session/render.d.ts.map +1 -1
  118. package/dist/lib/session/render.js +6 -37
  119. package/dist/lib/session/render.js.map +1 -1
  120. package/dist/lib/session/types.d.ts +1 -0
  121. package/dist/lib/session/types.d.ts.map +1 -1
  122. package/dist/lib/shims.d.ts.map +1 -1
  123. package/dist/lib/shims.js +6 -1
  124. package/dist/lib/shims.js.map +1 -1
  125. package/dist/lib/state.d.ts +0 -1
  126. package/dist/lib/state.d.ts.map +1 -1
  127. package/dist/lib/state.js +3 -9
  128. package/dist/lib/state.js.map +1 -1
  129. package/dist/lib/types.d.ts +3 -5
  130. package/dist/lib/types.d.ts.map +1 -1
  131. package/dist/lib/types.js.map +1 -1
  132. package/dist/lib/usage.d.ts +30 -0
  133. package/dist/lib/usage.d.ts.map +1 -0
  134. package/dist/lib/usage.js +428 -0
  135. package/dist/lib/usage.js.map +1 -0
  136. package/dist/lib/versions.d.ts +28 -0
  137. package/dist/lib/versions.d.ts.map +1 -1
  138. package/dist/lib/versions.js +200 -9
  139. package/dist/lib/versions.js.map +1 -1
  140. package/package.json +3 -1
  141. package/dist/commands/cron.d.ts +0 -3
  142. package/dist/commands/cron.d.ts.map +0 -1
  143. package/dist/commands/cron.js +0 -457
  144. package/dist/commands/cron.js.map +0 -1
  145. package/dist/lib/cron.d.ts +0 -70
  146. package/dist/lib/cron.d.ts.map +0 -1
  147. package/dist/lib/cron.js +0 -325
  148. package/dist/lib/cron.js.map +0 -1
  149. package/dist/lib/drive-server.d.ts +0 -9
  150. package/dist/lib/drive-server.d.ts.map +0 -1
  151. package/dist/lib/drive-server.js +0 -217
  152. package/dist/lib/drive-server.js.map +0 -1
  153. package/dist/lib/drives.d.ts +0 -34
  154. package/dist/lib/drives.d.ts.map +0 -1
  155. package/dist/lib/drives.js +0 -267
  156. package/dist/lib/drives.js.map +0 -1
@@ -5,6 +5,7 @@ import * as crypto from 'crypto';
5
5
  import * as readline from 'readline';
6
6
  import { execSync } from 'child_process';
7
7
  import { SESSION_AGENTS } from './types.js';
8
+ import { extractSessionTopic } from './prompt.js';
8
9
  const HOME = os.homedir();
9
10
  const AGENTS_DIR = path.join(HOME, '.agents');
10
11
  const SESSIONS_DIR = path.join(AGENTS_DIR, 'sessions');
@@ -30,18 +31,31 @@ export async function discoverSessions(options) {
30
31
  // Merge with persistent index (preserves sessions whose files were removed)
31
32
  const index = loadIndex();
32
33
  const liveIds = new Set(sessions.map(s => s.id));
34
+ const agentFilter = new Set(agents);
35
+ // Add matching index entries to display results
33
36
  index.forEach((entry, id) => {
34
- if (!liveIds.has(id)) {
37
+ if (!liveIds.has(id) && agentFilter.has(entry.agent)) {
35
38
  sessions.push(entry);
36
39
  }
37
40
  });
38
- // Persist updated index
39
- saveIndex(sessions);
41
+ // Persist: merge live sessions into full index (don't drop unqueried agents)
42
+ const toSave = new Map(index);
43
+ for (const s of sessions) {
44
+ toSave.set(s.id, s);
45
+ }
46
+ saveIndex([...toSave.values()]);
47
+ const projectQuery = options?.project?.trim();
40
48
  // Filter by project (case-insensitive substring match)
41
- if (options?.project) {
42
- const query = options.project.toLowerCase();
49
+ if (projectQuery) {
50
+ const query = projectQuery.toLowerCase();
43
51
  sessions = sessions.filter(s => s.project?.toLowerCase().includes(query));
44
52
  }
53
+ // An explicit project search should scan across directories instead of
54
+ // intersecting with the default cwd-only scope.
55
+ if (!options?.all && !projectQuery) {
56
+ const currentDir = normalizeCwd(options?.cwd || process.cwd());
57
+ sessions = sessions.filter(s => normalizeCwd(s.cwd) === currentDir);
58
+ }
45
59
  // Sort by timestamp descending
46
60
  sessions.sort((a, b) => {
47
61
  const ta = new Date(a.timestamp).getTime() || 0;
@@ -50,17 +64,23 @@ export async function discoverSessions(options) {
50
64
  });
51
65
  return sessions.slice(0, limit);
52
66
  }
67
+ function normalizeCwd(cwd) {
68
+ if (!cwd)
69
+ return '';
70
+ const resolved = path.resolve(cwd);
71
+ return safeRealpathSync(resolved) || resolved;
72
+ }
53
73
  /**
54
74
  * Resolve a session by full or short ID from the full index.
55
75
  */
56
76
  export function resolveSessionById(sessions, idQuery) {
57
77
  const query = idQuery.toLowerCase();
58
- // Exact match first
59
- const exact = sessions.filter(s => s.id.toLowerCase() === query);
78
+ // Exact match first (full id or shortId)
79
+ const exact = sessions.filter(s => s.id.toLowerCase() === query || s.shortId.toLowerCase() === query);
60
80
  if (exact.length > 0)
61
81
  return exact;
62
- // Prefix match
63
- return sessions.filter(s => s.id.toLowerCase().startsWith(query));
82
+ // Prefix match (against both id and shortId)
83
+ return sessions.filter(s => s.id.toLowerCase().startsWith(query) || s.shortId.toLowerCase().startsWith(query));
64
84
  }
65
85
  // ---------------------------------------------------------------------------
66
86
  // Persistent session index
@@ -79,10 +99,12 @@ function loadIndex() {
79
99
  if (entry.id)
80
100
  map.set(entry.id, entry);
81
101
  }
82
- catch { }
102
+ catch { /* malformed index entry, skip */ }
83
103
  }
84
104
  }
85
- catch { }
105
+ catch (err) {
106
+ console.error(`Warning: Could not load session cache (${err.message}). Rebuilding...`);
107
+ }
86
108
  return map;
87
109
  }
88
110
  function saveIndex(sessions) {
@@ -95,22 +117,13 @@ function saveIndex(sessions) {
95
117
  if (seen.has(s.id))
96
118
  continue;
97
119
  seen.add(s.id);
98
- lines.push(JSON.stringify({
99
- id: s.id,
100
- shortId: s.shortId,
101
- agent: s.agent,
102
- timestamp: s.timestamp,
103
- project: s.project,
104
- cwd: s.cwd,
105
- filePath: s.filePath,
106
- gitBranch: s.gitBranch,
107
- version: s.version,
108
- account: s.account,
109
- }));
120
+ lines.push(JSON.stringify(s));
110
121
  }
111
122
  fs.writeFileSync(INDEX_PATH, lines.join('\n') + '\n', 'utf-8');
112
123
  }
113
- catch { }
124
+ catch (err) {
125
+ console.error(`Warning: Could not save session cache: ${err.message}`);
126
+ }
114
127
  }
115
128
  // ---------------------------------------------------------------------------
116
129
  // Multi-version directory scanning
@@ -124,7 +137,7 @@ function saveIndex(sessions) {
124
137
  * @param subdir - Subdirectory within the agent's config dir where sessions live
125
138
  * (e.g., 'projects' for Claude, 'sessions' for Codex, 'tmp' for Gemini)
126
139
  */
127
- function getAgentSessionDirs(agent, subdir) {
140
+ export function getAgentSessionDirs(agent, subdir) {
128
141
  const resolved = new Set();
129
142
  const dirs = [];
130
143
  function addDir(dir) {
@@ -147,7 +160,7 @@ function getAgentSessionDirs(agent, subdir) {
147
160
  addDir(path.join(versionsBase, version, 'home', `.${agent}`, subdir));
148
161
  }
149
162
  }
150
- catch { }
163
+ catch { /* dir unreadable or missing */ }
151
164
  }
152
165
  // 3. Backups (from before version management was enabled)
153
166
  const backupsBase = path.join(AGENTS_DIR, 'backups', agent);
@@ -157,7 +170,7 @@ function getAgentSessionDirs(agent, subdir) {
157
170
  addDir(path.join(backupsBase, ts, subdir));
158
171
  }
159
172
  }
160
- catch { }
173
+ catch { /* dir unreadable or missing */ }
161
174
  }
162
175
  return dirs;
163
176
  }
@@ -180,7 +193,7 @@ function getClaudeAccount() {
180
193
  candidates.push(path.join(versionsBase, version, 'home', '.claude.json'));
181
194
  }
182
195
  }
183
- catch { }
196
+ catch { /* versions dir unreadable */ }
184
197
  }
185
198
  for (const candidate of candidates) {
186
199
  try {
@@ -193,7 +206,7 @@ function getClaudeAccount() {
193
206
  return name;
194
207
  }
195
208
  }
196
- catch { }
209
+ catch { /* auth file unreadable or malformed */ }
197
210
  }
198
211
  cachedClaudeAccount = '';
199
212
  return undefined;
@@ -205,6 +218,7 @@ async function discoverClaudeSessions() {
205
218
  const sessions = [];
206
219
  const seen = new Set();
207
220
  const account = getClaudeAccount();
221
+ let skipped = 0;
208
222
  for (const projectsDir of getAgentSessionDirs('claude', 'projects')) {
209
223
  let projectDirs;
210
224
  try {
@@ -236,48 +250,36 @@ async function discoverClaudeSessions() {
236
250
  if (meta)
237
251
  sessions.push(meta);
238
252
  }
239
- catch { }
253
+ catch {
254
+ skipped++;
255
+ }
240
256
  }
241
257
  }
242
258
  }
259
+ if (skipped > 0 && process.env.AGENTS_DEBUG) {
260
+ console.error(`[debug] Skipped ${skipped} unreadable Claude session(s)`);
261
+ }
243
262
  return sessions;
244
263
  }
245
264
  async function readClaudeMeta(filePath, sessionId, account) {
246
- const lines = await readFirstLines(filePath, 10);
247
- let topic;
248
- for (const line of lines) {
249
- let parsed;
250
- try {
251
- parsed = JSON.parse(line);
252
- }
253
- catch {
254
- continue;
255
- }
256
- // Extract topic from first user message
257
- if (!topic && parsed.type === 'user' && parsed.message?.content) {
258
- const text = Array.isArray(parsed.message.content)
259
- ? parsed.message.content.find((b) => b.type === 'text')?.text
260
- : typeof parsed.message.content === 'string' ? parsed.message.content : undefined;
261
- if (text)
262
- topic = extractTopic(text);
263
- }
264
- // Look for first user or assistant line with timestamp/cwd
265
- if ((parsed.type === 'user' || parsed.type === 'assistant') && parsed.timestamp) {
266
- const cwd = parsed.cwd || '';
267
- return {
268
- id: sessionId,
269
- shortId: sessionId.slice(0, 8),
270
- agent: 'claude',
271
- timestamp: parsed.timestamp,
272
- project: cwd ? path.basename(cwd) : undefined,
273
- cwd,
274
- filePath,
275
- gitBranch: parsed.gitBranch || undefined,
276
- version: parsed.version || undefined,
277
- account,
278
- topic,
279
- };
280
- }
265
+ const scan = await scanClaudeSession(filePath);
266
+ if (scan.timestamp) {
267
+ const cwd = scan.cwd || '';
268
+ return {
269
+ id: sessionId,
270
+ shortId: sessionId.slice(0, 8),
271
+ agent: 'claude',
272
+ timestamp: scan.timestamp,
273
+ project: cwd ? path.basename(cwd) : undefined,
274
+ cwd,
275
+ filePath,
276
+ gitBranch: scan.gitBranch,
277
+ version: scan.version,
278
+ account,
279
+ topic: scan.topic,
280
+ messageCount: scan.messageCount,
281
+ tokenCount: scan.tokenCount,
282
+ };
281
283
  }
282
284
  // Fallback: use file mtime
283
285
  const stat = safeStatSync(filePath);
@@ -288,6 +290,9 @@ async function readClaudeMeta(filePath, sessionId, account) {
288
290
  timestamp: stat ? stat.mtime.toISOString() : new Date().toISOString(),
289
291
  filePath,
290
292
  account,
293
+ messageCount: scan.messageCount,
294
+ tokenCount: scan.tokenCount,
295
+ topic: scan.topic,
291
296
  };
292
297
  }
293
298
  // ---------------------------------------------------------------------------
@@ -308,7 +313,7 @@ function getCodexAccount() {
308
313
  candidates.push(path.join(versionsBase, version, 'home', '.codex', 'auth.json'));
309
314
  }
310
315
  }
311
- catch { }
316
+ catch { /* versions dir unreadable */ }
312
317
  }
313
318
  for (const candidate of candidates) {
314
319
  try {
@@ -328,7 +333,7 @@ function getCodexAccount() {
328
333
  }
329
334
  }
330
335
  }
331
- catch { }
336
+ catch { /* auth file or JWT malformed */ }
332
337
  }
333
338
  cachedCodexAccount = '';
334
339
  return undefined;
@@ -340,6 +345,7 @@ async function discoverCodexSessions() {
340
345
  const sessions = [];
341
346
  const seen = new Set();
342
347
  const account = getCodexAccount();
348
+ let skipped = 0;
343
349
  for (const sessionsDir of getAgentSessionDirs('codex', 'sessions')) {
344
350
  const jsonlFiles = walkForFiles(sessionsDir, '.jsonl', 200);
345
351
  for (const filePath of jsonlFiles) {
@@ -350,56 +356,35 @@ async function discoverCodexSessions() {
350
356
  sessions.push(meta);
351
357
  }
352
358
  }
353
- catch { }
359
+ catch {
360
+ skipped++;
361
+ }
354
362
  }
355
363
  }
364
+ if (skipped > 0 && process.env.AGENTS_DEBUG) {
365
+ console.error(`[debug] Skipped ${skipped} unreadable Codex session(s)`);
366
+ }
356
367
  return sessions;
357
368
  }
358
369
  async function readCodexMeta(filePath, account) {
359
- const lines = await readFirstLines(filePath, 5);
360
- if (lines.length === 0)
361
- return null;
362
- let parsed;
363
- try {
364
- parsed = JSON.parse(lines[0]);
365
- }
366
- catch {
367
- return null;
368
- }
369
- if (parsed.type !== 'session_meta')
370
- return null;
371
- const payload = parsed.payload || {};
372
- const sessionId = payload.id || '';
370
+ const scan = await scanCodexSession(filePath);
371
+ const sessionId = scan.sessionId || '';
373
372
  if (!sessionId)
374
373
  return null;
375
- // Extract topic from first user message in subsequent lines
376
- let topic;
377
- for (let i = 1; i < lines.length; i++) {
378
- try {
379
- const ev = JSON.parse(lines[i]);
380
- if (ev.type === 'message' && ev.role === 'user' && ev.content) {
381
- const text = typeof ev.content === 'string' ? ev.content
382
- : Array.isArray(ev.content) ? ev.content.find((b) => b.type === 'input_text')?.text : undefined;
383
- if (text) {
384
- topic = extractTopic(text);
385
- break;
386
- }
387
- }
388
- }
389
- catch { }
390
- }
391
- const cwd = payload.cwd || '';
374
+ const cwd = scan.cwd || '';
392
375
  return {
393
376
  id: sessionId,
394
377
  shortId: sessionId.slice(0, 8),
395
378
  agent: 'codex',
396
- timestamp: payload.timestamp || parsed.timestamp || new Date().toISOString(),
379
+ timestamp: scan.timestamp || new Date().toISOString(),
397
380
  project: cwd ? path.basename(cwd) : undefined,
398
381
  cwd,
399
382
  filePath,
400
- gitBranch: payload.git?.branch || undefined,
401
- version: payload.version || undefined,
402
- topic,
383
+ gitBranch: scan.gitBranch,
384
+ version: scan.version,
385
+ topic: scan.topic,
386
+ messageCount: scan.messageCount,
387
+ tokenCount: scan.tokenCount,
403
388
  account,
404
389
  };
405
390
  }
@@ -410,6 +395,7 @@ async function discoverGeminiSessions() {
410
395
  const projectMap = buildGeminiProjectMap();
411
396
  const sessions = [];
412
397
  const seen = new Set();
398
+ let skipped = 0;
413
399
  for (const tmpDir of getAgentSessionDirs('gemini', 'tmp')) {
414
400
  let hashDirs;
415
401
  try {
@@ -438,23 +424,28 @@ async function discoverGeminiSessions() {
438
424
  sessions.push(meta);
439
425
  }
440
426
  }
441
- catch { }
427
+ catch {
428
+ skipped++;
429
+ }
442
430
  }
443
431
  }
444
432
  }
433
+ if (skipped > 0 && process.env.AGENTS_DEBUG) {
434
+ console.error(`[debug] Skipped ${skipped} unreadable Gemini session(s)`);
435
+ }
445
436
  return sessions;
446
437
  }
447
438
  function readGeminiMeta(filePath, hashDir, projectMap) {
448
- // Read the first ~2KB to get top-level fields without parsing entire messages array
449
- const fd = fs.openSync(filePath, 'r');
450
- const buf = Buffer.alloc(2048);
451
- const bytesRead = fs.readSync(fd, buf, 0, 2048, 0);
452
- fs.closeSync(fd);
453
- const header = buf.toString('utf-8', 0, bytesRead);
454
- // Extract fields via regex (avoids parsing potentially huge messages array)
455
- const sessionId = extractJsonField(header, 'sessionId');
456
- const startTime = extractJsonField(header, 'startTime');
457
- const projectHash = extractJsonField(header, 'projectHash');
439
+ let session;
440
+ try {
441
+ session = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
442
+ }
443
+ catch {
444
+ return null;
445
+ }
446
+ const sessionId = typeof session.sessionId === 'string' ? session.sessionId : '';
447
+ const startTime = typeof session.startTime === 'string' ? session.startTime : '';
448
+ const projectHash = typeof session.projectHash === 'string' ? session.projectHash : '';
458
449
  if (!sessionId)
459
450
  return null;
460
451
  // Resolve project name from hash
@@ -462,11 +453,31 @@ function readGeminiMeta(filePath, hashDir, projectMap) {
462
453
  const project = projectInfo?.name || hashDir.slice(0, 12);
463
454
  const cwd = projectInfo?.path;
464
455
  const stat = safeStatSync(filePath);
465
- // Try to extract first user message from the header bytes
456
+ const messages = Array.isArray(session.messages) ? session.messages : [];
466
457
  let topic;
467
- const userMsgMatch = header.match(/"role"\s*:\s*"user"[\s\S]*?"text"\s*:\s*"([^"]{1,200})"/);
468
- if (userMsgMatch)
469
- topic = extractTopic(userMsgMatch[1]);
458
+ let messageCount = 0;
459
+ let tokenCount = 0;
460
+ let sawTokenCount = false;
461
+ for (const message of messages) {
462
+ if (message.type === 'user') {
463
+ const text = extractGeminiMessageText(message.content);
464
+ if (text) {
465
+ messageCount++;
466
+ if (!topic)
467
+ topic = extractSessionTopic(text);
468
+ }
469
+ }
470
+ else if (message.type === 'gemini') {
471
+ if (extractGeminiMessageText(message.content)) {
472
+ messageCount++;
473
+ }
474
+ }
475
+ const total = getGeminiTokenCount(message.tokens);
476
+ if (total !== null) {
477
+ tokenCount += total;
478
+ sawTokenCount = true;
479
+ }
480
+ }
470
481
  return {
471
482
  id: sessionId,
472
483
  shortId: sessionId.slice(0, 8),
@@ -476,6 +487,8 @@ function readGeminiMeta(filePath, hashDir, projectMap) {
476
487
  cwd,
477
488
  filePath,
478
489
  topic,
490
+ messageCount,
491
+ tokenCount: sawTokenCount ? tokenCount : undefined,
479
492
  };
480
493
  }
481
494
  function buildGeminiProjectMap() {
@@ -507,9 +520,7 @@ function buildGeminiProjectMap() {
507
520
  }
508
521
  }
509
522
  }
510
- catch {
511
- // Ignore parse errors
512
- }
523
+ catch { /* projects.json missing or malformed */ }
513
524
  // Also check ~/.gemini/history/*/.project_root for additional mappings
514
525
  const historyDir = path.join(HOME, '.gemini', 'history');
515
526
  if (fs.existsSync(historyDir)) {
@@ -524,15 +535,11 @@ function buildGeminiProjectMap() {
524
535
  map.set(hash, { name, path: projectPath });
525
536
  }
526
537
  }
527
- catch {
528
- // Skip
529
- }
538
+ catch { /* history entry unreadable */ }
530
539
  }
531
540
  }
532
541
  }
533
- catch {
534
- // Skip
535
- }
542
+ catch { /* history entry unreadable */ }
536
543
  }
537
544
  return map;
538
545
  }
@@ -554,7 +561,7 @@ function getOpenCodeAccount() {
554
561
  }
555
562
  }
556
563
  }
557
- catch { }
564
+ catch { /* sqlite3 unavailable or DB locked */ }
558
565
  cachedOpenCodeAccount = '';
559
566
  return undefined;
560
567
  }
@@ -563,8 +570,8 @@ async function discoverOpenCodeSessions() {
563
570
  return [];
564
571
  const account = getOpenCodeAccount();
565
572
  try {
566
- // Query sessions joined with first user message for topic.
567
- // time_created is millisecond epoch. Limit to 200 most recent.
573
+ // Query sessions. time_created is millisecond epoch. Limit to 200 most recent.
574
+ // Use session.title as topic (OpenCode auto-generates good titles).
568
575
  const query = `
569
576
  SELECT
570
577
  s.id,
@@ -572,41 +579,43 @@ async function discoverOpenCodeSessions() {
572
579
  s.directory,
573
580
  s.version,
574
581
  s.time_created,
575
- s.parent_id,
576
- (SELECT substr(p.data, 1, 300)
577
- FROM message m
578
- JOIN part p ON p.message_id = m.id AND p.session_id = m.session_id
579
- WHERE m.session_id = s.id
580
- AND json_extract(m.data, '$.role') = 'user'
581
- AND json_extract(p.data, '$.type') = 'text'
582
- ORDER BY m.time_created ASC
583
- LIMIT 1) AS first_user_text
582
+ COALESCE(stats.message_count, 0),
583
+ stats.token_count,
584
+ COALESCE(stats.has_token_data, 0)
584
585
  FROM session s
586
+ LEFT JOIN (
587
+ SELECT
588
+ session_id,
589
+ COUNT(*) AS message_count,
590
+ SUM(
591
+ COALESCE(json_extract(data, '$.tokens.input'), 0) +
592
+ COALESCE(json_extract(data, '$.tokens.output'), 0) +
593
+ COALESCE(json_extract(data, '$.tokens.reasoning'), 0) +
594
+ COALESCE(json_extract(data, '$.tokens.cache.read'), 0) +
595
+ COALESCE(json_extract(data, '$.tokens.cache.write'), 0)
596
+ ) AS token_count,
597
+ MAX(CASE WHEN json_type(data, '$.tokens') IS NOT NULL THEN 1 ELSE 0 END) AS has_token_data
598
+ FROM message
599
+ GROUP BY session_id
600
+ ) stats ON stats.session_id = s.id
585
601
  WHERE s.parent_id IS NULL
586
- ORDER BY s.time_created DESC
602
+ ORDER BY time_created DESC
587
603
  LIMIT 200;
588
604
  `.replace(/\n/g, ' ');
589
- const out = execSync(`sqlite3 -separator '|||' "${OPENCODE_DB}" "${query}"`, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 5000 });
605
+ const out = execSync(`sqlite3 -separator '|||' "${OPENCODE_DB}"`, { encoding: 'utf-8', input: query, stdio: ['pipe', 'pipe', 'ignore'], timeout: 5000 });
590
606
  const sessions = [];
591
607
  for (const line of out.split('\n')) {
592
608
  if (!line.trim())
593
609
  continue;
594
- const [id, title, directory, version, timeCreatedStr, _parentId, firstUserText] = line.split('|||');
610
+ const [id, title, directory, version, timeCreatedStr, messageCountStr, tokenCountStr, hasTokenDataStr] = line.split('|||');
595
611
  if (!id)
596
612
  continue;
597
613
  const timeCreated = parseInt(timeCreatedStr, 10);
614
+ const messageCount = parseInt(messageCountStr, 10);
615
+ const tokenCount = parseInt(tokenCountStr, 10);
616
+ const hasTokenData = hasTokenDataStr === '1';
598
617
  const timestamp = isNaN(timeCreated) ? new Date().toISOString() : new Date(timeCreated).toISOString();
599
- // Extract topic from the part data JSON or fall back to session title
600
- let topic = title || undefined;
601
- if (firstUserText) {
602
- try {
603
- const partData = JSON.parse(firstUserText);
604
- if (partData.text) {
605
- topic = extractTopic(partData.text);
606
- }
607
- }
608
- catch { }
609
- }
618
+ const topic = title || undefined;
610
619
  sessions.push({
611
620
  id,
612
621
  shortId: id.replace(/^ses_/, '').slice(0, 8),
@@ -618,11 +627,16 @@ async function discoverOpenCodeSessions() {
618
627
  version: version || undefined,
619
628
  account,
620
629
  topic,
630
+ messageCount: Number.isNaN(messageCount) ? undefined : messageCount,
631
+ tokenCount: hasTokenData && !Number.isNaN(tokenCount) ? tokenCount : undefined,
621
632
  });
622
633
  }
623
634
  return sessions;
624
635
  }
625
- catch {
636
+ catch (err) {
637
+ if (process.stderr.isTTY) {
638
+ console.error(`Warning: Could not query OpenCode sessions: ${err.message}`);
639
+ }
626
640
  return [];
627
641
  }
628
642
  }
@@ -711,10 +725,146 @@ async function discoverOpenClawSessions() {
711
725
  }
712
726
  return sessions;
713
727
  }
728
+ async function scanClaudeSession(filePath) {
729
+ const stream = fs.createReadStream(filePath, { encoding: 'utf-8' });
730
+ const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
731
+ let timestamp;
732
+ let cwd;
733
+ let gitBranch;
734
+ let version;
735
+ let topic;
736
+ let messageCount = 0;
737
+ let tokenCount = 0;
738
+ let sawTokenCount = false;
739
+ const seenAssistantIds = new Set();
740
+ try {
741
+ for await (const line of rl) {
742
+ if (!line.trim())
743
+ continue;
744
+ let parsed;
745
+ try {
746
+ parsed = JSON.parse(line);
747
+ }
748
+ catch {
749
+ continue;
750
+ }
751
+ if (!timestamp && (parsed.type === 'user' || parsed.type === 'assistant') && parsed.timestamp) {
752
+ timestamp = parsed.timestamp;
753
+ cwd = parsed.cwd || '';
754
+ gitBranch = parsed.gitBranch || undefined;
755
+ version = parsed.version || undefined;
756
+ }
757
+ if (parsed.type === 'user') {
758
+ const text = extractClaudeUserText(parsed);
759
+ if (text) {
760
+ messageCount++;
761
+ if (!topic)
762
+ topic = extractSessionTopic(text);
763
+ }
764
+ continue;
765
+ }
766
+ if (parsed.type !== 'assistant')
767
+ continue;
768
+ const assistantId = typeof parsed.message?.id === 'string'
769
+ ? parsed.message.id
770
+ : typeof parsed.uuid === 'string'
771
+ ? parsed.uuid
772
+ : undefined;
773
+ const logicalId = assistantId || `${parsed.timestamp || ''}:${seenAssistantIds.size}`;
774
+ if (seenAssistantIds.has(logicalId))
775
+ continue;
776
+ seenAssistantIds.add(logicalId);
777
+ messageCount++;
778
+ const usage = getClaudeUsageTotal(parsed.message?.usage || parsed.usage);
779
+ if (usage !== null) {
780
+ tokenCount += usage;
781
+ sawTokenCount = true;
782
+ }
783
+ }
784
+ }
785
+ finally {
786
+ rl.close();
787
+ stream.destroy();
788
+ }
789
+ return {
790
+ timestamp,
791
+ cwd,
792
+ gitBranch,
793
+ version,
794
+ topic,
795
+ messageCount,
796
+ tokenCount: sawTokenCount ? tokenCount : undefined,
797
+ };
798
+ }
799
+ async function scanCodexSession(filePath) {
800
+ const stream = fs.createReadStream(filePath, { encoding: 'utf-8' });
801
+ const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
802
+ let sessionId;
803
+ let timestamp;
804
+ let cwd;
805
+ let gitBranch;
806
+ let version;
807
+ let topic;
808
+ let messageCount = 0;
809
+ let tokenCount;
810
+ try {
811
+ for await (const line of rl) {
812
+ if (!line.trim())
813
+ continue;
814
+ let parsed;
815
+ try {
816
+ parsed = JSON.parse(line);
817
+ }
818
+ catch {
819
+ continue;
820
+ }
821
+ if (parsed.type === 'session_meta') {
822
+ const payload = parsed.payload || {};
823
+ sessionId = payload.id || sessionId;
824
+ timestamp = payload.timestamp || parsed.timestamp || timestamp;
825
+ cwd = payload.cwd || cwd;
826
+ gitBranch = payload.git?.branch || gitBranch;
827
+ version = payload.version || version;
828
+ continue;
829
+ }
830
+ if (parsed.type === 'response_item' && parsed.payload?.type === 'message') {
831
+ const role = parsed.payload.role === 'user' || parsed.payload.role === 'developer'
832
+ ? 'user'
833
+ : 'assistant';
834
+ const text = extractCodexMessageText(parsed.payload.content, role);
835
+ if (!text)
836
+ continue;
837
+ messageCount++;
838
+ if (role === 'user' && !topic)
839
+ topic = extractSessionTopic(text);
840
+ continue;
841
+ }
842
+ if (parsed.type === 'event_msg' && parsed.payload?.type === 'token_count') {
843
+ const total = getCodexTokenCount(parsed.payload.info?.total_token_usage);
844
+ if (total !== null)
845
+ tokenCount = total;
846
+ }
847
+ }
848
+ }
849
+ finally {
850
+ rl.close();
851
+ stream.destroy();
852
+ }
853
+ return {
854
+ sessionId,
855
+ timestamp,
856
+ cwd,
857
+ gitBranch,
858
+ version,
859
+ topic,
860
+ messageCount,
861
+ tokenCount,
862
+ };
863
+ }
714
864
  // ---------------------------------------------------------------------------
715
865
  // Utilities
716
866
  // ---------------------------------------------------------------------------
717
- function readFirstLines(filePath, maxLines) {
867
+ export function readFirstLines(filePath, maxLines) {
718
868
  return new Promise((resolve) => {
719
869
  const lines = [];
720
870
  const stream = fs.createReadStream(filePath, { encoding: 'utf-8' });
@@ -736,7 +886,7 @@ function readFirstLines(filePath, maxLines) {
736
886
  * Walk a directory recursively for files with a given extension.
737
887
  * Returns at most `limit` files, sorted by mtime descending.
738
888
  */
739
- function walkForFiles(dir, ext, limit) {
889
+ export function walkForFiles(dir, ext, limit) {
740
890
  const results = [];
741
891
  function walk(d, depth) {
742
892
  if (depth > 5)
@@ -766,11 +916,6 @@ function walkForFiles(dir, ext, limit) {
766
916
  results.sort((a, b) => b.mtime - a.mtime);
767
917
  return results.slice(0, limit).map(r => r.path);
768
918
  }
769
- function extractJsonField(text, field) {
770
- const re = new RegExp(`"${field}"\\s*:\\s*"([^"]*)"`, 'i');
771
- const match = text.match(re);
772
- return match ? match[1] : '';
773
- }
774
919
  function sha256(input) {
775
920
  return crypto.createHash('sha256').update(input).digest('hex');
776
921
  }
@@ -790,11 +935,104 @@ function safeRealpathSync(p) {
790
935
  return null;
791
936
  }
792
937
  }
793
- function extractTopic(text) {
794
- // Strip leading whitespace, slash commands, and system tags
795
- let clean = text.replace(/^[\s\n]+/, '').replace(/<[^>]+>/g, '').trim();
796
- // Take first line only
797
- const firstLine = clean.split('\n')[0].trim();
798
- return firstLine;
938
+ function extractClaudeUserText(parsed) {
939
+ if (parsed.isMeta === true)
940
+ return undefined;
941
+ const content = parsed.message?.content;
942
+ if (typeof content === 'string') {
943
+ const text = content.trim();
944
+ return isLocalCommandMessage(text) ? undefined : text || undefined;
945
+ }
946
+ if (!Array.isArray(content))
947
+ return undefined;
948
+ const text = content
949
+ .filter((block) => block.type === 'text')
950
+ .map((block) => String(block.text || '').trim())
951
+ .find((value) => value && !value.startsWith('[Request interrupted'));
952
+ if (!text || isLocalCommandMessage(text))
953
+ return undefined;
954
+ return text;
955
+ }
956
+ function isLocalCommandMessage(text) {
957
+ return /<local-command-caveat>|<bash-(input|stdout|stderr)>/i.test(text);
958
+ }
959
+ function getClaudeUsageTotal(usage) {
960
+ if (!usage || typeof usage !== 'object')
961
+ return null;
962
+ return sumKnownNumbers([
963
+ usage.input_tokens,
964
+ usage.output_tokens,
965
+ usage.cache_creation_input_tokens,
966
+ usage.cache_read_input_tokens,
967
+ ]);
968
+ }
969
+ function extractCodexMessageText(contentBlocks, role) {
970
+ if (!Array.isArray(contentBlocks))
971
+ return undefined;
972
+ const matches = role === 'user'
973
+ ? contentBlocks.filter((block) => block.type === 'input_text')
974
+ : contentBlocks.filter((block) => block.type === 'output_text');
975
+ const text = matches
976
+ .map((block) => String(block.text || '').trim())
977
+ .find((value) => {
978
+ if (!value)
979
+ return false;
980
+ if (role === 'user' && (value.length >= 2000 || value.includes('<permissions instructions>') || value.startsWith('# AGENTS.md instructions'))) {
981
+ return false;
982
+ }
983
+ return true;
984
+ });
985
+ return text || undefined;
986
+ }
987
+ function getCodexTokenCount(totalTokenUsage) {
988
+ if (!totalTokenUsage || typeof totalTokenUsage !== 'object')
989
+ return null;
990
+ return sumKnownNumbers([
991
+ totalTokenUsage.input_tokens,
992
+ totalTokenUsage.cached_input_tokens,
993
+ totalTokenUsage.output_tokens,
994
+ totalTokenUsage.reasoning_output_tokens,
995
+ ]);
996
+ }
997
+ function extractGeminiMessageText(content) {
998
+ if (typeof content === 'string')
999
+ return content.trim();
1000
+ if (Array.isArray(content)) {
1001
+ return content
1002
+ .map((part) => {
1003
+ if (typeof part === 'string')
1004
+ return part;
1005
+ if (typeof part?.text === 'string')
1006
+ return part.text;
1007
+ return '';
1008
+ })
1009
+ .join('\n')
1010
+ .trim();
1011
+ }
1012
+ return '';
1013
+ }
1014
+ function getGeminiTokenCount(tokens) {
1015
+ if (!tokens || typeof tokens !== 'object')
1016
+ return null;
1017
+ if (typeof tokens.total === 'number')
1018
+ return tokens.total;
1019
+ return sumKnownNumbers([
1020
+ tokens.input,
1021
+ tokens.output,
1022
+ tokens.cached,
1023
+ tokens.thoughts,
1024
+ tokens.tool,
1025
+ ]);
1026
+ }
1027
+ function sumKnownNumbers(values) {
1028
+ let total = 0;
1029
+ let found = false;
1030
+ for (const value of values) {
1031
+ if (typeof value !== 'number' || Number.isNaN(value))
1032
+ continue;
1033
+ total += value;
1034
+ found = true;
1035
+ }
1036
+ return found ? total : null;
799
1037
  }
800
1038
  //# sourceMappingURL=discover.js.map