@ai-devkit/agent-manager 0.15.0 → 0.17.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 (112) hide show
  1. package/.swcrc +27 -0
  2. package/dist/AgentManager.d.ts +4 -0
  3. package/dist/AgentManager.d.ts.map +1 -1
  4. package/dist/AgentManager.js +97 -64
  5. package/dist/AgentManager.js.map +1 -1
  6. package/dist/__tests__/AgentManager.test.js +564 -0
  7. package/dist/__tests__/AgentManager.test.js.map +1 -0
  8. package/dist/__tests__/adapters/ClaudeCodeAdapter.test.js +2157 -0
  9. package/dist/__tests__/adapters/ClaudeCodeAdapter.test.js.map +1 -0
  10. package/dist/__tests__/adapters/CodexAdapter.test.js +1354 -0
  11. package/dist/__tests__/adapters/CodexAdapter.test.js.map +1 -0
  12. package/dist/__tests__/adapters/CopilotAdapter.test.js +500 -0
  13. package/dist/__tests__/adapters/CopilotAdapter.test.js.map +1 -0
  14. package/dist/__tests__/adapters/GeminiCliAdapter.test.js +1229 -0
  15. package/dist/__tests__/adapters/GeminiCliAdapter.test.js.map +1 -0
  16. package/dist/__tests__/adapters/OpenCodeAdapter.test.js +626 -0
  17. package/dist/__tests__/adapters/OpenCodeAdapter.test.js.map +1 -0
  18. package/dist/__tests__/terminal/TmuxManager.test.js +159 -0
  19. package/dist/__tests__/terminal/TmuxManager.test.js.map +1 -0
  20. package/dist/__tests__/terminal/TtyWriter.test.js +162 -0
  21. package/dist/__tests__/terminal/TtyWriter.test.js.map +1 -0
  22. package/dist/__tests__/utils/AgentRegistry.test.js +282 -0
  23. package/dist/__tests__/utils/AgentRegistry.test.js.map +1 -0
  24. package/dist/__tests__/utils/ClaudeSessionParser.test.js +312 -0
  25. package/dist/__tests__/utils/ClaudeSessionParser.test.js.map +1 -0
  26. package/dist/__tests__/utils/matching.test.js +324 -0
  27. package/dist/__tests__/utils/matching.test.js.map +1 -0
  28. package/dist/__tests__/utils/process.test.js +177 -0
  29. package/dist/__tests__/utils/process.test.js.map +1 -0
  30. package/dist/__tests__/utils/session.test.js +158 -0
  31. package/dist/__tests__/utils/session.test.js.map +1 -0
  32. package/dist/adapters/AgentAdapter.d.ts +1 -1
  33. package/dist/adapters/AgentAdapter.d.ts.map +1 -1
  34. package/dist/adapters/AgentAdapter.js +8 -7
  35. package/dist/adapters/AgentAdapter.js.map +1 -1
  36. package/dist/adapters/ClaudeCodeAdapter.js +66 -77
  37. package/dist/adapters/ClaudeCodeAdapter.js.map +1 -1
  38. package/dist/adapters/CodexAdapter.d.ts +13 -1
  39. package/dist/adapters/CodexAdapter.d.ts.map +1 -1
  40. package/dist/adapters/CodexAdapter.js +208 -132
  41. package/dist/adapters/CodexAdapter.js.map +1 -1
  42. package/dist/adapters/CopilotAdapter.d.ts +44 -0
  43. package/dist/adapters/CopilotAdapter.d.ts.map +1 -0
  44. package/dist/adapters/CopilotAdapter.js +314 -0
  45. package/dist/adapters/CopilotAdapter.js.map +1 -0
  46. package/dist/adapters/GeminiCliAdapter.d.ts +6 -1
  47. package/dist/adapters/GeminiCliAdapter.d.ts.map +1 -1
  48. package/dist/adapters/GeminiCliAdapter.js +160 -149
  49. package/dist/adapters/GeminiCliAdapter.js.map +1 -1
  50. package/dist/adapters/OpenCodeAdapter.js +60 -69
  51. package/dist/adapters/OpenCodeAdapter.js.map +1 -1
  52. package/dist/adapters/index.d.ts +1 -0
  53. package/dist/adapters/index.d.ts.map +1 -1
  54. package/dist/adapters/index.js +2 -0
  55. package/dist/adapters/index.js.map +1 -1
  56. package/dist/index.d.ts +6 -0
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +5 -0
  59. package/dist/index.js.map +1 -1
  60. package/dist/terminal/TerminalFocusManager.js +62 -49
  61. package/dist/terminal/TerminalFocusManager.js.map +1 -1
  62. package/dist/terminal/TmuxManager.d.ts +28 -0
  63. package/dist/terminal/TmuxManager.d.ts.map +1 -0
  64. package/dist/terminal/TmuxManager.js +137 -0
  65. package/dist/terminal/TmuxManager.js.map +1 -0
  66. package/dist/terminal/TtyWriter.js +37 -16
  67. package/dist/terminal/TtyWriter.js.map +1 -1
  68. package/dist/terminal/index.js +1 -0
  69. package/dist/terminal/index.js.map +1 -1
  70. package/dist/utils/AgentRegistry.d.ts +35 -0
  71. package/dist/utils/AgentRegistry.d.ts.map +1 -0
  72. package/dist/utils/AgentRegistry.js +123 -0
  73. package/dist/utils/AgentRegistry.js.map +1 -0
  74. package/dist/utils/ClaudeSessionParser.d.ts.map +1 -1
  75. package/dist/utils/ClaudeSessionParser.js +62 -93
  76. package/dist/utils/ClaudeSessionParser.js.map +1 -1
  77. package/dist/utils/agents.d.ts +15 -0
  78. package/dist/utils/agents.d.ts.map +1 -0
  79. package/dist/utils/agents.js +41 -0
  80. package/dist/utils/agents.js.map +1 -0
  81. package/dist/utils/applescript.js +3 -6
  82. package/dist/utils/applescript.js.map +1 -1
  83. package/dist/utils/index.js +1 -0
  84. package/dist/utils/index.js.map +1 -1
  85. package/dist/utils/matching.js +21 -29
  86. package/dist/utils/matching.js.map +1 -1
  87. package/dist/utils/process.js +77 -56
  88. package/dist/utils/process.js.map +1 -1
  89. package/dist/utils/session.js +20 -34
  90. package/dist/utils/session.js.map +1 -1
  91. package/dist/utils/sortAgents.js +6 -8
  92. package/dist/utils/sortAgents.js.map +1 -1
  93. package/package.json +11 -7
  94. package/src/AgentManager.ts +40 -0
  95. package/src/__tests__/AgentManager.test.ts +135 -0
  96. package/src/__tests__/adapters/ClaudeCodeAdapter.test.ts +3 -2
  97. package/src/__tests__/adapters/CodexAdapter.test.ts +199 -0
  98. package/src/__tests__/adapters/CopilotAdapter.test.ts +411 -0
  99. package/src/__tests__/adapters/GeminiCliAdapter.test.ts +185 -1
  100. package/src/__tests__/terminal/TmuxManager.test.ts +175 -0
  101. package/src/__tests__/utils/AgentRegistry.test.ts +223 -0
  102. package/src/__tests__/utils/ClaudeSessionParser.test.ts +71 -0
  103. package/src/adapters/AgentAdapter.ts +1 -1
  104. package/src/adapters/CodexAdapter.ts +155 -8
  105. package/src/adapters/CopilotAdapter.ts +453 -0
  106. package/src/adapters/GeminiCliAdapter.ts +84 -6
  107. package/src/adapters/index.ts +1 -0
  108. package/src/index.ts +7 -0
  109. package/src/terminal/TmuxManager.ts +118 -0
  110. package/src/utils/AgentRegistry.ts +139 -0
  111. package/src/utils/ClaudeSessionParser.ts +21 -10
  112. package/src/utils/agents.ts +41 -0
@@ -8,14 +8,14 @@
8
8
  * 4. Matching sessions to processes via shared matchProcessesToSessions()
9
9
  * using sha256(cwd) === session.projectHash as the resolvedCwd source
10
10
  * 5. Extracting summary from the most recent user message in the session JSON
11
- */
12
- import * as crypto from 'crypto';
11
+ */ import * as crypto from 'crypto';
13
12
  import * as fs from 'fs';
14
13
  import * as path from 'path';
15
14
  import { AgentStatus } from './AgentAdapter.js';
16
15
  import { listAgentProcesses, enrichProcesses } from '../utils/process.js';
17
16
  import { isDirectory, safeReadFile, safeReaddir, safeStat } from '../utils/session.js';
18
17
  import { matchProcessesToSessions, generateAgentName } from '../utils/matching.js';
18
+ import { AgentRegistry } from '../utils/AgentRegistry.js';
19
19
  export class GeminiCliAdapter {
20
20
  type = 'gemini_cli';
21
21
  static IDLE_THRESHOLD_MINUTES = 5;
@@ -23,9 +23,11 @@ export class GeminiCliAdapter {
23
23
  static CHATS_DIR_NAME = 'chats';
24
24
  static TMP_DIR_NAME = 'tmp';
25
25
  geminiTmpDir;
26
- constructor() {
26
+ registry;
27
+ constructor(registry = AgentRegistry.default()){
27
28
  const homeDir = process.env.HOME || process.env.USERPROFILE || '';
28
29
  this.geminiTmpDir = path.join(homeDir, '.gemini', GeminiCliAdapter.TMP_DIR_NAME);
30
+ this.registry = registry;
29
31
  }
30
32
  canHandle(processInfo) {
31
33
  return this.isGeminiExecutable(processInfo.command);
@@ -41,35 +43,96 @@ export class GeminiCliAdapter {
41
43
  * argv[0] = `node`. We scan the Node process pool via the shared
42
44
  * helper and keep only those whose command line references the gemini
43
45
  * executable or script via isGeminiExecutable().
44
- */
45
- async detectAgents() {
46
+ */ async detectAgents() {
46
47
  const nodeProcesses = enrichProcesses(listAgentProcesses('node'));
47
- const processes = nodeProcesses.filter((proc) => this.isGeminiExecutable(proc.command));
48
- if (processes.length === 0)
49
- return [];
50
- const { sessions, contentCache } = this.discoverSessions(processes);
48
+ const processes = nodeProcesses.filter((proc)=>this.isGeminiExecutable(proc.command));
49
+ if (processes.length === 0) return [];
50
+ const { cachedAgents, remaining } = this.tryRegistryCache(processes);
51
+ if (remaining.length === 0) return this.deduplicateSessionAgents(cachedAgents);
52
+ const { sessions, contentCache } = this.discoverSessions(remaining);
51
53
  if (sessions.length === 0) {
52
- return processes.map((p) => this.mapProcessOnlyAgent(p));
54
+ return this.deduplicateSessionAgents([
55
+ ...cachedAgents,
56
+ ...remaining.map((p)=>this.mapProcessOnlyAgent(p))
57
+ ]);
53
58
  }
54
- const matches = matchProcessesToSessions(processes, sessions);
55
- const matchedPids = new Set(matches.map((m) => m.process.pid));
59
+ const matches = matchProcessesToSessions(remaining, sessions);
60
+ const matchedPids = new Set(matches.map((m)=>m.process.pid));
56
61
  const agents = [];
57
- for (const match of matches) {
62
+ for (const match of matches){
58
63
  const cachedContent = contentCache.get(match.session.filePath);
59
64
  const sessionData = this.parseSession(cachedContent, match.session.filePath);
60
65
  if (sessionData) {
61
66
  agents.push(this.mapSessionToAgent(sessionData, match.process, match.session.filePath));
62
- }
63
- else {
67
+ } else {
64
68
  matchedPids.delete(match.process.pid);
65
69
  }
66
70
  }
67
- for (const proc of processes) {
71
+ for (const proc of remaining){
68
72
  if (!matchedPids.has(proc.pid)) {
69
73
  agents.push(this.mapProcessOnlyAgent(proc));
70
74
  }
71
75
  }
72
- return agents;
76
+ return this.deduplicateSessionAgents([
77
+ ...cachedAgents,
78
+ ...agents
79
+ ]);
80
+ }
81
+ deduplicateSessionAgents(agents) {
82
+ const bySession = new Map();
83
+ const result = [];
84
+ for (const agent of agents){
85
+ const sessionKey = this.sessionDeduplicationKey(agent);
86
+ if (!sessionKey) {
87
+ result.push(agent);
88
+ continue;
89
+ }
90
+ const existing = bySession.get(sessionKey);
91
+ if (!existing) {
92
+ bySession.set(sessionKey, agent);
93
+ result.push(agent);
94
+ continue;
95
+ }
96
+ if (agent.pid > existing.pid) {
97
+ bySession.set(sessionKey, agent);
98
+ const index = result.indexOf(existing);
99
+ if (index >= 0) result[index] = agent;
100
+ }
101
+ }
102
+ return result;
103
+ }
104
+ sessionDeduplicationKey(agent) {
105
+ if (agent.sessionFilePath) return `file:${agent.sessionFilePath}`;
106
+ if (agent.sessionId && !agent.sessionId.startsWith('pid-')) {
107
+ return `session:${agent.sessionId}`;
108
+ }
109
+ return null;
110
+ }
111
+ tryRegistryCache(processes) {
112
+ const cachedAgents = [];
113
+ const remaining = [];
114
+ const byPid = new Map(this.registry.list().map((e)=>[
115
+ e.pid,
116
+ e
117
+ ]));
118
+ for (const proc of processes){
119
+ const entry = byPid.get(proc.pid);
120
+ if (!entry || entry.type !== this.type || !entry.sessionFilePath || !fs.existsSync(entry.sessionFilePath)) {
121
+ remaining.push(proc);
122
+ continue;
123
+ }
124
+ const content = safeReadFile(entry.sessionFilePath);
125
+ const sessionData = this.parseSession(content, entry.sessionFilePath);
126
+ if (!sessionData) {
127
+ remaining.push(proc);
128
+ continue;
129
+ }
130
+ cachedAgents.push(this.mapSessionToAgent(sessionData, proc, entry.sessionFilePath));
131
+ }
132
+ return {
133
+ cachedAgents,
134
+ remaining
135
+ };
73
136
  }
74
137
  /**
75
138
  * Discover session files for the given processes.
@@ -78,40 +141,36 @@ export class GeminiCliAdapter {
78
141
  * where <shortId> is opaque (managed by a project registry). We scan every
79
142
  * shortId directory and filter by matching session.projectHash against
80
143
  * sha256(process.cwd) to bind each session to a candidate process CWD.
81
- */
82
- discoverSessions(processes) {
83
- const empty = { sessions: [], contentCache: new Map() };
84
- if (!fs.existsSync(this.geminiTmpDir))
85
- return empty;
144
+ */ discoverSessions(processes) {
145
+ const empty = {
146
+ sessions: [],
147
+ contentCache: new Map()
148
+ };
149
+ if (!fs.existsSync(this.geminiTmpDir)) return empty;
86
150
  const cwdHashMap = this.buildCwdHashMap(processes);
87
- if (cwdHashMap.size === 0)
88
- return empty;
151
+ if (cwdHashMap.size === 0) return empty;
89
152
  const contentCache = new Map();
90
153
  const sessions = [];
91
154
  let shortIdEntries;
92
155
  try {
93
156
  shortIdEntries = fs.readdirSync(this.geminiTmpDir);
94
- }
95
- catch {
157
+ } catch {
96
158
  return empty;
97
159
  }
98
- for (const shortId of shortIdEntries) {
160
+ for (const shortId of shortIdEntries){
99
161
  const chatsDir = path.join(this.geminiTmpDir, shortId, GeminiCliAdapter.CHATS_DIR_NAME);
100
162
  try {
101
- if (!fs.statSync(chatsDir).isDirectory())
102
- continue;
103
- }
104
- catch {
163
+ if (!fs.statSync(chatsDir).isDirectory()) continue;
164
+ } catch {
105
165
  continue;
106
166
  }
107
167
  let chatFiles;
108
168
  try {
109
169
  chatFiles = fs.readdirSync(chatsDir);
110
- }
111
- catch {
170
+ } catch {
112
171
  continue;
113
172
  }
114
- for (const fileName of chatFiles) {
173
+ for (const fileName of chatFiles){
115
174
  if (!fileName.startsWith(GeminiCliAdapter.SESSION_FILE_PREFIX) || !fileName.endsWith('.json')) {
116
175
  continue;
117
176
  }
@@ -119,27 +178,22 @@ export class GeminiCliAdapter {
119
178
  let content;
120
179
  try {
121
180
  content = fs.readFileSync(filePath, 'utf-8');
122
- }
123
- catch {
181
+ } catch {
124
182
  continue;
125
183
  }
126
184
  let parsed;
127
185
  try {
128
186
  parsed = JSON.parse(content);
129
- }
130
- catch {
187
+ } catch {
131
188
  continue;
132
189
  }
133
- if (!parsed.projectHash)
134
- continue;
190
+ if (!parsed.projectHash) continue;
135
191
  const resolvedCwd = cwdHashMap.get(parsed.projectHash);
136
- if (!resolvedCwd)
137
- continue;
192
+ if (!resolvedCwd) continue;
138
193
  let birthtimeMs = 0;
139
194
  try {
140
195
  birthtimeMs = fs.statSync(filePath).birthtimeMs;
141
- }
142
- catch {
196
+ } catch {
143
197
  continue;
144
198
  }
145
199
  const sessionId = parsed.sessionId || fileName.replace(/\.json$/, '');
@@ -149,24 +203,26 @@ export class GeminiCliAdapter {
149
203
  filePath,
150
204
  projectDir: chatsDir,
151
205
  birthtimeMs,
152
- resolvedCwd,
206
+ resolvedCwd
153
207
  });
154
208
  }
155
209
  }
156
- return { sessions, contentCache };
210
+ return {
211
+ sessions,
212
+ contentCache
213
+ };
157
214
  }
158
215
  buildCwdHashMap(processes) {
159
216
  const map = new Map();
160
- for (const proc of processes) {
161
- if (!proc.cwd)
162
- continue;
217
+ for (const proc of processes){
218
+ if (!proc.cwd) continue;
163
219
  // Gemini CLI resolves its project root by walking up from the
164
220
  // startup directory looking for a `.git` boundary marker. A
165
221
  // session's projectHash therefore tracks that ancestor rather
166
222
  // than the process' actual CWD. Enumerate every ancestor as a
167
223
  // candidate so subdirectory invocations still line up with the
168
224
  // session the Gemini process wrote.
169
- for (const candidate of this.candidateProjectRoots(proc.cwd)) {
225
+ for (const candidate of this.candidateProjectRoots(proc.cwd)){
170
226
  if (!map.has(this.hashProjectRoot(candidate))) {
171
227
  map.set(this.hashProjectRoot(candidate), proc.cwd);
172
228
  }
@@ -178,7 +234,7 @@ export class GeminiCliAdapter {
178
234
  const roots = [];
179
235
  let current = path.resolve(cwd);
180
236
  let parent = path.dirname(current);
181
- while (parent !== current) {
237
+ while(parent !== current){
182
238
  roots.push(current);
183
239
  current = parent;
184
240
  parent = path.dirname(current);
@@ -192,53 +248,42 @@ export class GeminiCliAdapter {
192
248
  /**
193
249
  * Parse session file content into GeminiSession.
194
250
  * Uses cached content if available, otherwise reads from disk.
195
- */
196
- parseSession(cachedContent, filePath) {
251
+ */ parseSession(cachedContent, filePath) {
197
252
  let content;
198
253
  if (cachedContent !== undefined) {
199
254
  content = cachedContent;
200
- }
201
- else {
255
+ } else {
202
256
  try {
203
257
  content = fs.readFileSync(filePath, 'utf-8');
204
- }
205
- catch {
258
+ } catch {
206
259
  return null;
207
260
  }
208
261
  }
209
262
  let parsed;
210
263
  try {
211
264
  parsed = JSON.parse(content);
212
- }
213
- catch {
265
+ } catch {
214
266
  return null;
215
267
  }
216
- if (!parsed.sessionId)
217
- return null;
268
+ if (!parsed.sessionId) return null;
218
269
  const messages = Array.isArray(parsed.messages) ? parsed.messages : [];
219
270
  const lastEntry = messages.length > 0 ? messages[messages.length - 1] : undefined;
220
271
  let mtime = null;
221
272
  try {
222
273
  mtime = fs.statSync(filePath).mtime;
223
- }
224
- catch {
274
+ } catch {
225
275
  mtime = null;
226
276
  }
227
- const lastActive = this.parseTimestamp(parsed.lastUpdated) ||
228
- this.parseTimestamp(lastEntry?.timestamp) ||
229
- mtime ||
230
- new Date();
277
+ const lastActive = this.parseTimestamp(parsed.lastUpdated) || this.parseTimestamp(lastEntry?.timestamp) || mtime || new Date();
231
278
  const sessionStart = this.parseTimestamp(parsed.startTime) || lastActive;
232
- const projectPath = Array.isArray(parsed.directories) && parsed.directories.length > 0
233
- ? parsed.directories[0]
234
- : '';
279
+ const projectPath = Array.isArray(parsed.directories) && parsed.directories.length > 0 ? parsed.directories[0] : '';
235
280
  return {
236
281
  sessionId: parsed.sessionId,
237
282
  projectPath,
238
283
  summary: this.extractSummary(messages),
239
284
  sessionStart,
240
285
  lastActive,
241
- lastMessageType: lastEntry?.type,
286
+ lastMessageType: lastEntry?.type
242
287
  };
243
288
  }
244
289
  mapSessionToAgent(session, processInfo, filePath) {
@@ -252,7 +297,7 @@ export class GeminiCliAdapter {
252
297
  projectPath,
253
298
  sessionId: session.sessionId,
254
299
  lastActive: session.lastActive,
255
- sessionFilePath: filePath,
300
+ sessionFilePath: filePath
256
301
  };
257
302
  }
258
303
  mapProcessOnlyAgent(processInfo) {
@@ -264,12 +309,11 @@ export class GeminiCliAdapter {
264
309
  pid: processInfo.pid,
265
310
  projectPath: processInfo.cwd || '',
266
311
  sessionId: `pid-${processInfo.pid}`,
267
- lastActive: new Date(),
312
+ lastActive: new Date()
268
313
  };
269
314
  }
270
315
  parseTimestamp(value) {
271
- if (!value)
272
- return null;
316
+ if (!value) return null;
273
317
  const timestamp = new Date(value);
274
318
  return Number.isNaN(timestamp.getTime()) ? null : timestamp;
275
319
  }
@@ -285,13 +329,11 @@ export class GeminiCliAdapter {
285
329
  return AgentStatus.RUNNING;
286
330
  }
287
331
  extractSummary(messages) {
288
- for (let i = messages.length - 1; i >= 0; i--) {
332
+ for(let i = messages.length - 1; i >= 0; i--){
289
333
  const entry = messages[i];
290
- if (entry?.type !== 'user')
291
- continue;
334
+ if (entry?.type !== 'user') continue;
292
335
  const text = this.messageText(entry).trim();
293
- if (text)
294
- return this.truncate(text, 120);
336
+ if (text) return this.truncate(text, 120);
295
337
  }
296
338
  return 'Gemini CLI session active';
297
339
  }
@@ -299,11 +341,9 @@ export class GeminiCliAdapter {
299
341
  * Normalize an entry's content/displayContent into a plain string.
300
342
  * Prefers displayContent when both are present (matches Gemini CLI's
301
343
  * own rendering priority for the /chat UI).
302
- */
303
- messageText(entry) {
344
+ */ messageText(entry) {
304
345
  const displayText = this.resolveContent(entry.displayContent);
305
- if (displayText)
306
- return displayText;
346
+ if (displayText) return displayText;
307
347
  return this.resolveContent(entry.content);
308
348
  }
309
349
  /**
@@ -312,16 +352,12 @@ export class GeminiCliAdapter {
312
352
  * list (user turns carrying `[{text: "..."}]`). Non-text part
313
353
  * variants (data, file) are dropped since this helper is only used
314
354
  * for summary/conversation rendering.
315
- */
316
- resolveContent(content) {
317
- if (!content)
318
- return '';
319
- if (typeof content === 'string')
320
- return content;
321
- if (!Array.isArray(content))
322
- return '';
355
+ */ resolveContent(content) {
356
+ if (!content) return '';
357
+ if (typeof content === 'string') return content;
358
+ if (!Array.isArray(content)) return '';
323
359
  const parts = [];
324
- for (const part of content) {
360
+ for (const part of content){
325
361
  if (part && typeof part.text === 'string' && part.text) {
326
362
  parts.push(part.text);
327
363
  }
@@ -329,8 +365,7 @@ export class GeminiCliAdapter {
329
365
  return parts.join('');
330
366
  }
331
367
  truncate(value, maxLength) {
332
- if (value.length <= maxLength)
333
- return value;
368
+ if (value.length <= maxLength) return value;
334
369
  return `${value.slice(0, maxLength - 3)}...`;
335
370
  }
336
371
  isGeminiExecutable(command) {
@@ -338,7 +373,7 @@ export class GeminiCliAdapter {
338
373
  // known gemini entrypoint. This is intentionally broader than the
339
374
  // other adapters' argv[0]-only check because the Node-script
340
375
  // distribution puts the real gemini path in argv[1..], not argv[0].
341
- for (const token of command.trim().split(/\s+/)) {
376
+ for (const token of command.trim().split(/\s+/)){
342
377
  const base = path.basename(token).toLowerCase();
343
378
  if (base === 'gemini' || base === 'gemini.exe' || base === 'gemini.js') {
344
379
  return true;
@@ -351,69 +386,55 @@ export class GeminiCliAdapter {
351
386
  *
352
387
  * Gemini sessions store messages in an array with `type` field — typically
353
388
  * 'user' or 'gemini' for visible turns, with tool and system entries mixed in.
354
- */
355
- getConversation(sessionFilePath, options) {
389
+ */ getConversation(sessionFilePath, options) {
356
390
  const verbose = options?.verbose ?? false;
357
391
  const content = safeReadFile(sessionFilePath);
358
- if (content === undefined)
359
- return [];
392
+ if (content === undefined) return [];
360
393
  let parsed;
361
394
  try {
362
395
  parsed = JSON.parse(content);
363
- }
364
- catch {
396
+ } catch {
365
397
  return [];
366
398
  }
367
399
  const messages = [];
368
- if (!Array.isArray(parsed.messages))
369
- return messages;
370
- for (const entry of parsed.messages) {
400
+ if (!Array.isArray(parsed.messages)) return messages;
401
+ for (const entry of parsed.messages){
371
402
  const entryType = entry?.type;
372
- if (!entryType)
373
- continue;
403
+ if (!entryType) continue;
374
404
  let role;
375
405
  if (entryType === 'user') {
376
406
  role = 'user';
377
- }
378
- else if (entryType === 'gemini' || entryType === 'assistant') {
407
+ } else if (entryType === 'gemini' || entryType === 'assistant') {
379
408
  role = 'assistant';
380
- }
381
- else if (verbose) {
409
+ } else if (verbose) {
382
410
  role = 'system';
383
- }
384
- else {
411
+ } else {
385
412
  continue;
386
413
  }
387
414
  const text = this.messageText(entry).trim();
388
- if (!text)
389
- continue;
415
+ if (!text) continue;
390
416
  messages.push({
391
417
  role,
392
418
  content: text,
393
- timestamp: entry.timestamp,
419
+ timestamp: entry.timestamp
394
420
  });
395
421
  }
396
422
  return messages;
397
423
  }
398
424
  async listSessions(opts) {
399
- if (!isDirectory(this.geminiTmpDir))
400
- return [];
425
+ if (!isDirectory(this.geminiTmpDir)) return [];
401
426
  const summaries = [];
402
- for (const shortId of safeReaddir(this.geminiTmpDir)) {
427
+ for (const shortId of safeReaddir(this.geminiTmpDir)){
403
428
  const chatsDir = path.join(this.geminiTmpDir, shortId, GeminiCliAdapter.CHATS_DIR_NAME);
404
- if (!isDirectory(chatsDir))
405
- continue;
406
- for (const fileName of safeReaddir(chatsDir)) {
407
- if (!fileName.startsWith(GeminiCliAdapter.SESSION_FILE_PREFIX) ||
408
- !fileName.endsWith('.json')) {
429
+ if (!isDirectory(chatsDir)) continue;
430
+ for (const fileName of safeReaddir(chatsDir)){
431
+ if (!fileName.startsWith(GeminiCliAdapter.SESSION_FILE_PREFIX) || !fileName.endsWith('.json')) {
409
432
  continue;
410
433
  }
411
434
  const filePath = path.join(chatsDir, fileName);
412
435
  const summary = this.fileToSessionSummary(filePath);
413
- if (!summary)
414
- continue;
415
- if (opts?.cwd !== undefined && summary.cwd !== opts.cwd)
416
- continue;
436
+ if (!summary) continue;
437
+ if (opts?.cwd !== undefined && summary.cwd !== opts.cwd) continue;
417
438
  summaries.push(summary);
418
439
  }
419
440
  }
@@ -423,31 +444,22 @@ export class GeminiCliAdapter {
423
444
  * Read a Gemini session JSON file and produce a {@link SessionSummary}.
424
445
  * Returns null when the file is unreadable, the JSON doesn't parse,
425
446
  * or the body lacks a sessionId.
426
- */
427
- fileToSessionSummary(filePath) {
447
+ */ fileToSessionSummary(filePath) {
428
448
  const content = safeReadFile(filePath);
429
- if (content === undefined)
430
- return null;
449
+ if (content === undefined) return null;
431
450
  let parsed;
432
451
  try {
433
452
  parsed = JSON.parse(content);
434
- }
435
- catch {
453
+ } catch {
436
454
  return null;
437
455
  }
438
- if (!parsed.sessionId)
439
- return null;
456
+ if (!parsed.sessionId) return null;
440
457
  const messages = Array.isArray(parsed.messages) ? parsed.messages : [];
441
458
  const firstUserMessage = this.extractFirstUserMessage(messages);
442
- const cwd = Array.isArray(parsed.directories) && parsed.directories.length > 0
443
- ? parsed.directories[0]
444
- : '';
459
+ const cwd = Array.isArray(parsed.directories) && parsed.directories.length > 0 ? parsed.directories[0] : '';
445
460
  const stat = safeStat(filePath);
446
461
  const lastEntryTimestamp = this.parseTimestamp(messages.length > 0 ? messages[messages.length - 1]?.timestamp : undefined);
447
- const lastActive = this.parseTimestamp(parsed.lastUpdated) ||
448
- lastEntryTimestamp ||
449
- stat?.mtime ||
450
- new Date();
462
+ const lastActive = this.parseTimestamp(parsed.lastUpdated) || lastEntryTimestamp || stat?.mtime || new Date();
451
463
  const startedAt = this.parseTimestamp(parsed.startTime) || stat?.birthtime || stat?.mtime || lastActive;
452
464
  return {
453
465
  type: 'gemini_cli',
@@ -456,18 +468,17 @@ export class GeminiCliAdapter {
456
468
  firstUserMessage,
457
469
  lastActive,
458
470
  startedAt,
459
- sessionFilePath: filePath,
471
+ sessionFilePath: filePath
460
472
  };
461
473
  }
462
474
  extractFirstUserMessage(messages) {
463
- for (const entry of messages) {
464
- if (entry?.type !== 'user')
465
- continue;
475
+ for (const entry of messages){
476
+ if (entry?.type !== 'user') continue;
466
477
  const text = this.messageText(entry).trim();
467
- if (text)
468
- return text;
478
+ if (text) return text;
469
479
  }
470
480
  return '';
471
481
  }
472
482
  }
483
+
473
484
  //# sourceMappingURL=GeminiCliAdapter.js.map