@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.
- package/.swcrc +27 -0
- package/dist/AgentManager.d.ts +4 -0
- package/dist/AgentManager.d.ts.map +1 -1
- package/dist/AgentManager.js +97 -64
- package/dist/AgentManager.js.map +1 -1
- package/dist/__tests__/AgentManager.test.js +564 -0
- package/dist/__tests__/AgentManager.test.js.map +1 -0
- package/dist/__tests__/adapters/ClaudeCodeAdapter.test.js +2157 -0
- package/dist/__tests__/adapters/ClaudeCodeAdapter.test.js.map +1 -0
- package/dist/__tests__/adapters/CodexAdapter.test.js +1354 -0
- package/dist/__tests__/adapters/CodexAdapter.test.js.map +1 -0
- package/dist/__tests__/adapters/CopilotAdapter.test.js +500 -0
- package/dist/__tests__/adapters/CopilotAdapter.test.js.map +1 -0
- package/dist/__tests__/adapters/GeminiCliAdapter.test.js +1229 -0
- package/dist/__tests__/adapters/GeminiCliAdapter.test.js.map +1 -0
- package/dist/__tests__/adapters/OpenCodeAdapter.test.js +626 -0
- package/dist/__tests__/adapters/OpenCodeAdapter.test.js.map +1 -0
- package/dist/__tests__/terminal/TmuxManager.test.js +159 -0
- package/dist/__tests__/terminal/TmuxManager.test.js.map +1 -0
- package/dist/__tests__/terminal/TtyWriter.test.js +162 -0
- package/dist/__tests__/terminal/TtyWriter.test.js.map +1 -0
- package/dist/__tests__/utils/AgentRegistry.test.js +282 -0
- package/dist/__tests__/utils/AgentRegistry.test.js.map +1 -0
- package/dist/__tests__/utils/ClaudeSessionParser.test.js +312 -0
- package/dist/__tests__/utils/ClaudeSessionParser.test.js.map +1 -0
- package/dist/__tests__/utils/matching.test.js +324 -0
- package/dist/__tests__/utils/matching.test.js.map +1 -0
- package/dist/__tests__/utils/process.test.js +177 -0
- package/dist/__tests__/utils/process.test.js.map +1 -0
- package/dist/__tests__/utils/session.test.js +158 -0
- package/dist/__tests__/utils/session.test.js.map +1 -0
- package/dist/adapters/AgentAdapter.d.ts +1 -1
- package/dist/adapters/AgentAdapter.d.ts.map +1 -1
- package/dist/adapters/AgentAdapter.js +8 -7
- package/dist/adapters/AgentAdapter.js.map +1 -1
- package/dist/adapters/ClaudeCodeAdapter.js +66 -77
- package/dist/adapters/ClaudeCodeAdapter.js.map +1 -1
- package/dist/adapters/CodexAdapter.d.ts +13 -1
- package/dist/adapters/CodexAdapter.d.ts.map +1 -1
- package/dist/adapters/CodexAdapter.js +208 -132
- package/dist/adapters/CodexAdapter.js.map +1 -1
- package/dist/adapters/CopilotAdapter.d.ts +44 -0
- package/dist/adapters/CopilotAdapter.d.ts.map +1 -0
- package/dist/adapters/CopilotAdapter.js +314 -0
- package/dist/adapters/CopilotAdapter.js.map +1 -0
- package/dist/adapters/GeminiCliAdapter.d.ts +6 -1
- package/dist/adapters/GeminiCliAdapter.d.ts.map +1 -1
- package/dist/adapters/GeminiCliAdapter.js +160 -149
- package/dist/adapters/GeminiCliAdapter.js.map +1 -1
- package/dist/adapters/OpenCodeAdapter.js +60 -69
- package/dist/adapters/OpenCodeAdapter.js.map +1 -1
- package/dist/adapters/index.d.ts +1 -0
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js +2 -0
- package/dist/adapters/index.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/terminal/TerminalFocusManager.js +62 -49
- package/dist/terminal/TerminalFocusManager.js.map +1 -1
- package/dist/terminal/TmuxManager.d.ts +28 -0
- package/dist/terminal/TmuxManager.d.ts.map +1 -0
- package/dist/terminal/TmuxManager.js +137 -0
- package/dist/terminal/TmuxManager.js.map +1 -0
- package/dist/terminal/TtyWriter.js +37 -16
- package/dist/terminal/TtyWriter.js.map +1 -1
- package/dist/terminal/index.js +1 -0
- package/dist/terminal/index.js.map +1 -1
- package/dist/utils/AgentRegistry.d.ts +35 -0
- package/dist/utils/AgentRegistry.d.ts.map +1 -0
- package/dist/utils/AgentRegistry.js +123 -0
- package/dist/utils/AgentRegistry.js.map +1 -0
- package/dist/utils/ClaudeSessionParser.d.ts.map +1 -1
- package/dist/utils/ClaudeSessionParser.js +62 -93
- package/dist/utils/ClaudeSessionParser.js.map +1 -1
- package/dist/utils/agents.d.ts +15 -0
- package/dist/utils/agents.d.ts.map +1 -0
- package/dist/utils/agents.js +41 -0
- package/dist/utils/agents.js.map +1 -0
- package/dist/utils/applescript.js +3 -6
- package/dist/utils/applescript.js.map +1 -1
- package/dist/utils/index.js +1 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/matching.js +21 -29
- package/dist/utils/matching.js.map +1 -1
- package/dist/utils/process.js +77 -56
- package/dist/utils/process.js.map +1 -1
- package/dist/utils/session.js +20 -34
- package/dist/utils/session.js.map +1 -1
- package/dist/utils/sortAgents.js +6 -8
- package/dist/utils/sortAgents.js.map +1 -1
- package/package.json +11 -7
- package/src/AgentManager.ts +40 -0
- package/src/__tests__/AgentManager.test.ts +135 -0
- package/src/__tests__/adapters/ClaudeCodeAdapter.test.ts +3 -2
- package/src/__tests__/adapters/CodexAdapter.test.ts +199 -0
- package/src/__tests__/adapters/CopilotAdapter.test.ts +411 -0
- package/src/__tests__/adapters/GeminiCliAdapter.test.ts +185 -1
- package/src/__tests__/terminal/TmuxManager.test.ts +175 -0
- package/src/__tests__/utils/AgentRegistry.test.ts +223 -0
- package/src/__tests__/utils/ClaudeSessionParser.test.ts +71 -0
- package/src/adapters/AgentAdapter.ts +1 -1
- package/src/adapters/CodexAdapter.ts +155 -8
- package/src/adapters/CopilotAdapter.ts +453 -0
- package/src/adapters/GeminiCliAdapter.ts +84 -6
- package/src/adapters/index.ts +1 -0
- package/src/index.ts +7 -0
- package/src/terminal/TmuxManager.ts +118 -0
- package/src/utils/AgentRegistry.ts +139 -0
- package/src/utils/ClaudeSessionParser.ts +21 -10
- 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
|
-
|
|
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)
|
|
48
|
-
if (processes.length === 0)
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
54
|
+
return this.deduplicateSessionAgents([
|
|
55
|
+
...cachedAgents,
|
|
56
|
+
...remaining.map((p)=>this.mapProcessOnlyAgent(p))
|
|
57
|
+
]);
|
|
53
58
|
}
|
|
54
|
-
const matches = matchProcessesToSessions(
|
|
55
|
-
const matchedPids = new Set(matches.map((m)
|
|
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
|
|
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
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
|
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
|
-
|
|
317
|
-
if (
|
|
318
|
-
|
|
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
|
-
|
|
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
|
-
|
|
406
|
-
|
|
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
|
-
|
|
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
|