@ai-devkit/agent-manager 0.33.0 → 0.34.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/dist/AgentManager.d.ts +11 -2
- package/dist/AgentManager.d.ts.map +1 -1
- package/dist/AgentManager.js +37 -6
- package/dist/AgentManager.js.map +1 -1
- package/dist/__tests__/AgentManager.test.js +212 -5
- package/dist/__tests__/AgentManager.test.js.map +1 -1
- package/dist/__tests__/capacity/codex.test.js +2 -1
- package/dist/__tests__/capacity/codex.test.js.map +1 -1
- package/dist/__tests__/capacity/index.test.js +6 -3
- package/dist/__tests__/capacity/index.test.js.map +1 -1
- package/dist/__tests__/capacity/zai.test.js +213 -0
- package/dist/__tests__/capacity/zai.test.js.map +1 -0
- package/dist/__tests__/{adapters → providers/copilot}/CopilotAdapter.test.js +19 -14
- package/dist/__tests__/providers/copilot/CopilotAdapter.test.js.map +1 -0
- package/dist/__tests__/{adapters → providers/gemini}/GeminiCliAdapter.test.js +37 -29
- package/dist/__tests__/providers/gemini/GeminiCliAdapter.test.js.map +1 -0
- package/dist/__tests__/providers/opencode/OpenCodeAdapter.test.js +189 -0
- package/dist/__tests__/providers/opencode/OpenCodeAdapter.test.js.map +1 -0
- package/dist/__tests__/providers/opencode/OpenCodeAgentMapper.test.js +89 -0
- package/dist/__tests__/providers/opencode/OpenCodeAgentMapper.test.js.map +1 -0
- package/dist/__tests__/providers/opencode/OpenCodeSessionLocator.test.js +94 -0
- package/dist/__tests__/providers/opencode/OpenCodeSessionLocator.test.js.map +1 -0
- package/dist/__tests__/providers/opencode/OpenCodeSessionParser.test.js +147 -0
- package/dist/__tests__/providers/opencode/OpenCodeSessionParser.test.js.map +1 -0
- package/dist/__tests__/runtime/HerdrAgentDiscovery.test.js +164 -0
- package/dist/__tests__/runtime/HerdrAgentDiscovery.test.js.map +1 -0
- package/dist/__tests__/runtime/HerdrAgentRuntime.test.js +37 -1
- package/dist/__tests__/runtime/HerdrAgentRuntime.test.js.map +1 -1
- package/dist/__tests__/utils/AgentRegistry.test.js +20 -13
- package/dist/__tests__/utils/AgentRegistry.test.js.map +1 -1
- package/dist/adapters/index.d.ts +3 -3
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js +3 -3
- package/dist/adapters/index.js.map +1 -1
- package/dist/capacity/codex.d.ts +1 -0
- package/dist/capacity/codex.d.ts.map +1 -1
- package/dist/capacity/codex.js +9 -4
- package/dist/capacity/codex.js.map +1 -1
- package/dist/capacity/index.d.ts +8 -0
- package/dist/capacity/index.d.ts.map +1 -1
- package/dist/capacity/index.js +10 -9
- package/dist/capacity/index.js.map +1 -1
- package/dist/capacity/types.d.ts +8 -0
- package/dist/capacity/types.d.ts.map +1 -1
- package/dist/capacity/types.js.map +1 -1
- package/dist/capacity/zai.d.ts +14 -0
- package/dist/capacity/zai.d.ts.map +1 -0
- package/dist/capacity/zai.js +203 -0
- package/dist/capacity/zai.js.map +1 -0
- package/dist/index.d.ts +8 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -4
- package/dist/index.js.map +1 -1
- package/dist/{adapters → providers/copilot}/CopilotAdapter.d.ts +11 -25
- package/dist/providers/copilot/CopilotAdapter.d.ts.map +1 -0
- package/dist/providers/copilot/CopilotAdapter.js +108 -0
- package/dist/providers/copilot/CopilotAdapter.js.map +1 -0
- package/dist/providers/copilot/CopilotAgentMapper.d.ts +9 -0
- package/dist/providers/copilot/CopilotAgentMapper.d.ts.map +1 -0
- package/dist/providers/copilot/CopilotAgentMapper.js +37 -0
- package/dist/providers/copilot/CopilotAgentMapper.js.map +1 -0
- package/dist/providers/copilot/CopilotSessionLocator.d.ts +19 -0
- package/dist/providers/copilot/CopilotSessionLocator.d.ts.map +1 -0
- package/dist/providers/copilot/CopilotSessionLocator.js +41 -0
- package/dist/providers/copilot/CopilotSessionLocator.js.map +1 -0
- package/dist/providers/copilot/CopilotSessionParser.d.ts +30 -0
- package/dist/providers/copilot/CopilotSessionParser.d.ts.map +1 -0
- package/dist/providers/copilot/CopilotSessionParser.js +181 -0
- package/dist/providers/copilot/CopilotSessionParser.js.map +1 -0
- package/dist/providers/gemini/GeminiAgentMapper.d.ts +9 -0
- package/dist/providers/gemini/GeminiAgentMapper.d.ts.map +1 -0
- package/dist/providers/gemini/GeminiAgentMapper.js +37 -0
- package/dist/providers/gemini/GeminiAgentMapper.js.map +1 -0
- package/dist/providers/gemini/GeminiCliAdapter.d.ts +52 -0
- package/dist/providers/gemini/GeminiCliAdapter.d.ts.map +1 -0
- package/dist/providers/gemini/GeminiCliAdapter.js +188 -0
- package/dist/providers/gemini/GeminiCliAdapter.js.map +1 -0
- package/dist/providers/gemini/GeminiSessionLocator.d.ts +30 -0
- package/dist/providers/gemini/GeminiSessionLocator.d.ts.map +1 -0
- package/dist/providers/gemini/GeminiSessionLocator.js +142 -0
- package/dist/providers/gemini/GeminiSessionLocator.js.map +1 -0
- package/dist/providers/gemini/GeminiSessionParser.d.ts +94 -0
- package/dist/providers/gemini/GeminiSessionParser.d.ts.map +1 -0
- package/dist/providers/gemini/GeminiSessionParser.js +166 -0
- package/dist/providers/gemini/GeminiSessionParser.js.map +1 -0
- package/dist/{adapters → providers/opencode}/OpenCodeAdapter.d.ts +6 -12
- package/dist/providers/opencode/OpenCodeAdapter.d.ts.map +1 -0
- package/dist/providers/opencode/OpenCodeAdapter.js +86 -0
- package/dist/providers/opencode/OpenCodeAdapter.js.map +1 -0
- package/dist/providers/opencode/OpenCodeAgentMapper.d.ts +11 -0
- package/dist/providers/opencode/OpenCodeAgentMapper.d.ts.map +1 -0
- package/dist/providers/opencode/OpenCodeAgentMapper.js +50 -0
- package/dist/providers/opencode/OpenCodeAgentMapper.js.map +1 -0
- package/dist/providers/opencode/OpenCodeSessionLocator.d.ts +21 -0
- package/dist/providers/opencode/OpenCodeSessionLocator.d.ts.map +1 -0
- package/dist/providers/opencode/OpenCodeSessionLocator.js +99 -0
- package/dist/providers/opencode/OpenCodeSessionLocator.js.map +1 -0
- package/dist/providers/opencode/OpenCodeSessionParser.d.ts +18 -0
- package/dist/providers/opencode/OpenCodeSessionParser.d.ts.map +1 -0
- package/dist/providers/opencode/OpenCodeSessionParser.js +104 -0
- package/dist/providers/opencode/OpenCodeSessionParser.js.map +1 -0
- package/dist/runtime/herdr/HerdrAgentDiscovery.d.ts +18 -0
- package/dist/runtime/herdr/HerdrAgentDiscovery.d.ts.map +1 -0
- package/dist/runtime/herdr/HerdrAgentDiscovery.js +183 -0
- package/dist/runtime/herdr/HerdrAgentDiscovery.js.map +1 -0
- package/dist/runtime/herdr/HerdrAgentRuntime.d.ts +5 -0
- package/dist/runtime/herdr/HerdrAgentRuntime.d.ts.map +1 -1
- package/dist/runtime/herdr/HerdrAgentRuntime.js +26 -8
- package/dist/runtime/herdr/HerdrAgentRuntime.js.map +1 -1
- package/dist/runtime/herdr/HerdrResponseParsers.d.ts +2 -0
- package/dist/runtime/herdr/HerdrResponseParsers.d.ts.map +1 -1
- package/dist/runtime/herdr/HerdrResponseParsers.js +12 -1
- package/dist/runtime/herdr/HerdrResponseParsers.js.map +1 -1
- package/dist/utils/AgentRegistry.d.ts.map +1 -1
- package/dist/utils/AgentRegistry.js +5 -5
- package/dist/utils/AgentRegistry.js.map +1 -1
- package/package.json +1 -1
- package/src/AgentManager.ts +67 -5
- package/src/__tests__/AgentManager.test.ts +231 -5
- package/src/__tests__/capacity/codex.test.ts +94 -26
- package/src/__tests__/capacity/fixtures/zai-auth.json +6 -0
- package/src/__tests__/capacity/fixtures/zai-quota-invalid.json +7 -0
- package/src/__tests__/capacity/fixtures/zai-quota.json +38 -0
- package/src/__tests__/capacity/index.test.ts +10 -3
- package/src/__tests__/capacity/zai.test.ts +186 -0
- package/src/__tests__/{adapters → providers/copilot}/CopilotAdapter.test.ts +13 -16
- package/src/__tests__/{adapters → providers/gemini}/GeminiCliAdapter.test.ts +39 -31
- package/src/__tests__/providers/opencode/OpenCodeAdapter.test.ts +197 -0
- package/src/__tests__/providers/opencode/OpenCodeAgentMapper.test.ts +83 -0
- package/src/__tests__/providers/opencode/OpenCodeSessionLocator.test.ts +84 -0
- package/src/__tests__/providers/opencode/OpenCodeSessionParser.test.ts +148 -0
- package/src/__tests__/runtime/HerdrAgentDiscovery.test.ts +131 -0
- package/src/__tests__/runtime/HerdrAgentRuntime.test.ts +20 -1
- package/src/__tests__/utils/AgentRegistry.test.ts +20 -10
- package/src/adapters/index.ts +3 -3
- package/src/capacity/codex.ts +96 -28
- package/src/capacity/index.ts +29 -11
- package/src/capacity/types.ts +8 -0
- package/src/capacity/zai.ts +237 -0
- package/src/index.ts +18 -6
- package/src/providers/copilot/CopilotAdapter.ts +145 -0
- package/src/providers/copilot/CopilotAgentMapper.ts +36 -0
- package/src/providers/copilot/CopilotSessionLocator.ts +57 -0
- package/src/providers/copilot/CopilotSessionParser.ts +289 -0
- package/src/providers/gemini/GeminiAgentMapper.ts +36 -0
- package/src/providers/gemini/GeminiCliAdapter.ts +266 -0
- package/src/providers/gemini/GeminiSessionLocator.ts +183 -0
- package/src/providers/gemini/GeminiSessionParser.ts +271 -0
- package/src/providers/opencode/OpenCodeAdapter.ts +112 -0
- package/src/providers/opencode/OpenCodeAgentMapper.ts +61 -0
- package/src/providers/opencode/OpenCodeSessionLocator.ts +123 -0
- package/src/providers/opencode/OpenCodeSessionParser.ts +145 -0
- package/src/runtime/herdr/HerdrAgentDiscovery.ts +178 -0
- package/src/runtime/herdr/HerdrAgentRuntime.ts +33 -7
- package/src/runtime/herdr/HerdrResponseParsers.ts +11 -1
- package/src/utils/AgentRegistry.ts +8 -6
- package/dist/__tests__/adapters/CopilotAdapter.test.js.map +0 -1
- package/dist/__tests__/adapters/GeminiCliAdapter.test.js.map +0 -1
- package/dist/__tests__/adapters/OpenCodeAdapter.test.js +0 -635
- package/dist/__tests__/adapters/OpenCodeAdapter.test.js.map +0 -1
- package/dist/adapters/CopilotAdapter.d.ts.map +0 -1
- package/dist/adapters/CopilotAdapter.js +0 -329
- package/dist/adapters/CopilotAdapter.js.map +0 -1
- package/dist/adapters/GeminiCliAdapter.d.ts +0 -98
- package/dist/adapters/GeminiCliAdapter.d.ts.map +0 -1
- package/dist/adapters/GeminiCliAdapter.js +0 -513
- package/dist/adapters/GeminiCliAdapter.js.map +0 -1
- package/dist/adapters/OpenCodeAdapter.d.ts.map +0 -1
- package/dist/adapters/OpenCodeAdapter.js +0 -286
- package/dist/adapters/OpenCodeAdapter.js.map +0 -1
- package/src/__tests__/adapters/OpenCodeAdapter.test.ts +0 -543
- package/src/adapters/CopilotAdapter.ts +0 -483
- package/src/adapters/GeminiCliAdapter.ts +0 -699
- package/src/adapters/OpenCodeAdapter.ts +0 -369
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gemini CLI Adapter
|
|
3
|
+
*
|
|
4
|
+
* Detects running Gemini CLI agents by:
|
|
5
|
+
* 1. Filtering Gemini Node processes from a shared asynchronous process snapshot
|
|
6
|
+
* 2. Using snapshot CWD and start-time enrichment
|
|
7
|
+
* 3. Discovering session files from ~/.gemini/tmp/<shortId>/chats/session-*.json
|
|
8
|
+
* 4. Matching sessions to processes via shared matchProcessesToSessions()
|
|
9
|
+
* using sha256(cwd) === session.projectHash as the resolvedCwd source
|
|
10
|
+
* 5. Extracting summary from the most recent user message in the session JSON
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type {
|
|
14
|
+
AgentAdapter,
|
|
15
|
+
AgentInfo,
|
|
16
|
+
ProcessInfo,
|
|
17
|
+
ConversationMessage,
|
|
18
|
+
SessionSummary,
|
|
19
|
+
ListSessionsOptions,
|
|
20
|
+
AgentDetectionContext,
|
|
21
|
+
} from "../../adapters/AgentAdapter.js";
|
|
22
|
+
import {
|
|
23
|
+
captureProcessSnapshot,
|
|
24
|
+
executableBasename,
|
|
25
|
+
filterByProcessNames,
|
|
26
|
+
findWrapperProcess,
|
|
27
|
+
findWrapperProcessPids,
|
|
28
|
+
} from "../../utils/process.js";
|
|
29
|
+
import { matchProcessesToSessions } from "../../utils/matching.js";
|
|
30
|
+
import { AgentRegistry, type RegistryEntry } from "../../utils/AgentRegistry.js";
|
|
31
|
+
import { GeminiAgentMapper } from "./GeminiAgentMapper.js";
|
|
32
|
+
import { GeminiSessionLocator, type GeminiSessionDiscovery } from "./GeminiSessionLocator.js";
|
|
33
|
+
import { GeminiSessionParser } from "./GeminiSessionParser.js";
|
|
34
|
+
|
|
35
|
+
export interface GeminiCliAdapterOptions {
|
|
36
|
+
geminiTmpDir?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class GeminiCliAdapter implements AgentAdapter {
|
|
40
|
+
readonly type = "gemini_cli" as const;
|
|
41
|
+
readonly processNames = ["node"] as const;
|
|
42
|
+
|
|
43
|
+
private readonly registry: AgentRegistry;
|
|
44
|
+
private readonly parser: GeminiSessionParser;
|
|
45
|
+
private readonly mapper: GeminiAgentMapper;
|
|
46
|
+
private readonly locator: GeminiSessionLocator;
|
|
47
|
+
|
|
48
|
+
constructor(
|
|
49
|
+
registry: AgentRegistry = AgentRegistry.default(),
|
|
50
|
+
options: GeminiCliAdapterOptions = {},
|
|
51
|
+
) {
|
|
52
|
+
this.registry = registry;
|
|
53
|
+
this.parser = new GeminiSessionParser();
|
|
54
|
+
this.mapper = new GeminiAgentMapper(this.parser);
|
|
55
|
+
this.locator = new GeminiSessionLocator(options);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
canHandle(processInfo: ProcessInfo): boolean {
|
|
59
|
+
return this.isGeminiExecutable(processInfo.command);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Detect running Gemini CLI agents.
|
|
64
|
+
*
|
|
65
|
+
* Gemini CLI ships as a Node script (`bundle/gemini.js` with shebang
|
|
66
|
+
* `#!/usr/bin/env node`) — unlike Claude Code (native binary per
|
|
67
|
+
* platform) or Codex CLI (Node wrapper that execs a native Rust
|
|
68
|
+
* binary). The primary running process is therefore the Node runtime
|
|
69
|
+
* itself, and `ps aux` lists it as `node /path/to/gemini ...` with
|
|
70
|
+
* argv[0] = `node`. We scan the Node process pool via the shared
|
|
71
|
+
* snapshot and keep only those whose command line references the gemini
|
|
72
|
+
* executable or script via isGeminiExecutable().
|
|
73
|
+
*/
|
|
74
|
+
async detectAgents(context?: AgentDetectionContext): Promise<AgentInfo[]> {
|
|
75
|
+
const processes = await this.getGeminiProcesses(context);
|
|
76
|
+
if (processes.length === 0) return [];
|
|
77
|
+
|
|
78
|
+
const wrapperPids = findWrapperProcessPids(processes);
|
|
79
|
+
const { cachedAgents, remaining } = this.tryRegistryCache(processes, wrapperPids);
|
|
80
|
+
if (remaining.length === 0) return this.deduplicateSessionAgents(cachedAgents);
|
|
81
|
+
|
|
82
|
+
const candidateProcesses = remaining.filter((proc) => !wrapperPids.has(proc.pid));
|
|
83
|
+
const registryEntriesByPid = new Map(this.registry.list().map((entry) => [entry.pid, entry]));
|
|
84
|
+
const discovery = this.locator.discoverSessions(candidateProcesses);
|
|
85
|
+
|
|
86
|
+
const agents =
|
|
87
|
+
discovery.sessions.length === 0
|
|
88
|
+
? this.mapProcessOnlyAgents(candidateProcesses, processes, registryEntriesByPid)
|
|
89
|
+
: this.mapSessionDiscovery(discovery, candidateProcesses, processes, registryEntriesByPid);
|
|
90
|
+
|
|
91
|
+
return this.deduplicateSessionAgents([...cachedAgents, ...agents]);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
getConversation(sessionFilePath: string, options?: { verbose?: boolean }): ConversationMessage[] {
|
|
95
|
+
return this.parser.getConversation(sessionFilePath, options);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async listSessions(opts?: ListSessionsOptions): Promise<SessionSummary[]> {
|
|
99
|
+
const summaries: SessionSummary[] = [];
|
|
100
|
+
|
|
101
|
+
for (const filePath of this.locator.discoverHistoricalSessionFiles()) {
|
|
102
|
+
const summary = this.parser.fileToSessionSummary(filePath);
|
|
103
|
+
if (!summary) continue;
|
|
104
|
+
if (opts?.cwd !== undefined && summary.cwd !== opts.cwd) continue;
|
|
105
|
+
summaries.push(summary);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return summaries;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private async getGeminiProcesses(context?: AgentDetectionContext): Promise<ProcessInfo[]> {
|
|
112
|
+
const snapshot = context?.processes ?? (await captureProcessSnapshot(this.processNames));
|
|
113
|
+
const relevant = filterByProcessNames(snapshot, this.processNames);
|
|
114
|
+
return relevant.filter((process) => this.canHandle(process));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private mapSessionDiscovery(
|
|
118
|
+
discovery: GeminiSessionDiscovery,
|
|
119
|
+
candidateProcesses: ProcessInfo[],
|
|
120
|
+
allProcesses: ProcessInfo[],
|
|
121
|
+
registryEntriesByPid: Map<number, RegistryEntry>,
|
|
122
|
+
): AgentInfo[] {
|
|
123
|
+
const matches = matchProcessesToSessions(candidateProcesses, discovery.sessions);
|
|
124
|
+
const matchedPids = new Set(matches.map((m) => m.process.pid));
|
|
125
|
+
const matchedProcesses: ProcessInfo[] = [];
|
|
126
|
+
const agents: AgentInfo[] = [];
|
|
127
|
+
|
|
128
|
+
for (const match of matches) {
|
|
129
|
+
const cachedContent = discovery.contentCache.get(match.session.filePath);
|
|
130
|
+
const sessionData = this.parser.parseSession(cachedContent, match.session.filePath);
|
|
131
|
+
if (!sessionData) {
|
|
132
|
+
matchedPids.delete(match.process.pid);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const agent = this.mapper.mapSessionToAgent(
|
|
137
|
+
sessionData,
|
|
138
|
+
match.process,
|
|
139
|
+
match.session.filePath,
|
|
140
|
+
);
|
|
141
|
+
this.applyWrapperRegistryName(agent, match.process, allProcesses, registryEntriesByPid);
|
|
142
|
+
agents.push(agent);
|
|
143
|
+
matchedProcesses.push(match.process);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const matchedWrapperPids = findWrapperProcessPids(candidateProcesses, matchedProcesses);
|
|
147
|
+
const unmatchedProcesses = candidateProcesses.filter(
|
|
148
|
+
(process) => !matchedPids.has(process.pid) && !matchedWrapperPids.has(process.pid),
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
agents.push(
|
|
152
|
+
...this.mapProcessOnlyAgents(unmatchedProcesses, allProcesses, registryEntriesByPid),
|
|
153
|
+
);
|
|
154
|
+
return agents;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private mapProcessOnlyAgents(
|
|
158
|
+
processes: ProcessInfo[],
|
|
159
|
+
allProcesses: ProcessInfo[],
|
|
160
|
+
registryEntriesByPid: Map<number, RegistryEntry>,
|
|
161
|
+
): AgentInfo[] {
|
|
162
|
+
return processes.map((processInfo) => {
|
|
163
|
+
const agent = this.mapper.mapProcessOnlyAgent(processInfo);
|
|
164
|
+
this.applyWrapperRegistryName(agent, processInfo, allProcesses, registryEntriesByPid);
|
|
165
|
+
return agent;
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private applyWrapperRegistryName(
|
|
170
|
+
agent: AgentInfo,
|
|
171
|
+
processInfo: ProcessInfo,
|
|
172
|
+
processes: ProcessInfo[],
|
|
173
|
+
registryEntriesByPid: Map<number, RegistryEntry>,
|
|
174
|
+
): void {
|
|
175
|
+
const wrapper = findWrapperProcess(processes, processInfo);
|
|
176
|
+
const wrapperEntry = wrapper ? registryEntriesByPid.get(wrapper.pid) : undefined;
|
|
177
|
+
if (wrapperEntry?.type === this.type) {
|
|
178
|
+
agent.name = wrapperEntry.name;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private deduplicateSessionAgents(agents: AgentInfo[]): AgentInfo[] {
|
|
183
|
+
const bySession = new Map<string, AgentInfo>();
|
|
184
|
+
const result: AgentInfo[] = [];
|
|
185
|
+
|
|
186
|
+
for (const agent of agents) {
|
|
187
|
+
const sessionKey = this.sessionDeduplicationKey(agent);
|
|
188
|
+
if (!sessionKey) {
|
|
189
|
+
result.push(agent);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const existing = bySession.get(sessionKey);
|
|
194
|
+
if (!existing) {
|
|
195
|
+
bySession.set(sessionKey, agent);
|
|
196
|
+
result.push(agent);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (agent.pid > existing.pid) {
|
|
201
|
+
bySession.set(sessionKey, agent);
|
|
202
|
+
const index = result.indexOf(existing);
|
|
203
|
+
if (index >= 0) result[index] = agent;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return result;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private sessionDeduplicationKey(agent: AgentInfo): string | null {
|
|
211
|
+
if (agent.sessionFilePath) return `file:${agent.sessionFilePath}`;
|
|
212
|
+
if (agent.sessionId && !agent.sessionId.startsWith("pid-")) {
|
|
213
|
+
return `session:${agent.sessionId}`;
|
|
214
|
+
}
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private tryRegistryCache(
|
|
219
|
+
processes: ProcessInfo[],
|
|
220
|
+
wrapperPids: Set<number>,
|
|
221
|
+
): {
|
|
222
|
+
cachedAgents: AgentInfo[];
|
|
223
|
+
remaining: ProcessInfo[];
|
|
224
|
+
} {
|
|
225
|
+
const cachedAgents: AgentInfo[] = [];
|
|
226
|
+
const remaining: ProcessInfo[] = [];
|
|
227
|
+
const byPid = new Map(this.registry.list().map((e) => [e.pid, e]));
|
|
228
|
+
|
|
229
|
+
for (const proc of processes) {
|
|
230
|
+
const entry = byPid.get(proc.pid);
|
|
231
|
+
if (
|
|
232
|
+
wrapperPids.has(proc.pid) ||
|
|
233
|
+
!entry ||
|
|
234
|
+
entry.type !== this.type ||
|
|
235
|
+
!entry.sessionFilePath
|
|
236
|
+
) {
|
|
237
|
+
remaining.push(proc);
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const sessionData = this.parser.parseSession(undefined, entry.sessionFilePath);
|
|
242
|
+
if (!sessionData) {
|
|
243
|
+
remaining.push(proc);
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
cachedAgents.push(this.mapper.mapSessionToAgent(sessionData, proc, entry.sessionFilePath));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return { cachedAgents, remaining };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private isGeminiExecutable(command: string): boolean {
|
|
254
|
+
// Accept any token in the command line whose basename matches a
|
|
255
|
+
// known gemini entrypoint. This is intentionally broader than the
|
|
256
|
+
// other adapters' argv[0]-only check because the Node-script
|
|
257
|
+
// distribution puts the real gemini path in argv[1..], not argv[0].
|
|
258
|
+
for (const token of command.trim().split(/\s+/)) {
|
|
259
|
+
const base = executableBasename(token);
|
|
260
|
+
if (base === "gemini" || base === "gemini.exe" || base === "gemini.js") {
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import * as crypto from "crypto";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import type { ProcessInfo } from "../../adapters/AgentAdapter.js";
|
|
5
|
+
import type { SessionFile } from "../../utils/session.js";
|
|
6
|
+
import { isDirectory, safeReaddir } from "../../utils/session.js";
|
|
7
|
+
|
|
8
|
+
export interface GeminiSessionDiscovery {
|
|
9
|
+
sessions: SessionFile[];
|
|
10
|
+
contentCache: Map<string, string>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface GeminiSessionLocatorOptions {
|
|
14
|
+
geminiTmpDir?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface GeminiSessionMetadata {
|
|
18
|
+
sessionId?: string;
|
|
19
|
+
projectHash?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const SESSION_FILE_PREFIX = "session-";
|
|
23
|
+
const CHATS_DIR_NAME = "chats";
|
|
24
|
+
|
|
25
|
+
export class GeminiSessionLocator {
|
|
26
|
+
private readonly geminiTmpDir: string;
|
|
27
|
+
|
|
28
|
+
constructor(options: GeminiSessionLocatorOptions = {}) {
|
|
29
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || "";
|
|
30
|
+
this.geminiTmpDir = options.geminiTmpDir ?? path.join(homeDir, ".gemini", "tmp");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Discover session files for the given processes.
|
|
35
|
+
*
|
|
36
|
+
* Gemini CLI writes sessions to ~/.gemini/tmp/<shortId>/chats/session-*.json
|
|
37
|
+
* where <shortId> is opaque (managed by a project registry). We scan every
|
|
38
|
+
* shortId directory and filter by matching session.projectHash against
|
|
39
|
+
* sha256(process.cwd) to bind each session to a candidate process CWD.
|
|
40
|
+
*/
|
|
41
|
+
discoverSessions(processes: ProcessInfo[]): GeminiSessionDiscovery {
|
|
42
|
+
const empty = { sessions: [] as SessionFile[], contentCache: new Map<string, string>() };
|
|
43
|
+
if (!fs.existsSync(this.geminiTmpDir)) return empty;
|
|
44
|
+
|
|
45
|
+
const cwdHashMap = this.buildCwdHashMap(processes);
|
|
46
|
+
if (cwdHashMap.size === 0) return empty;
|
|
47
|
+
|
|
48
|
+
const contentCache = new Map<string, string>();
|
|
49
|
+
const sessions: SessionFile[] = [];
|
|
50
|
+
|
|
51
|
+
let shortIdEntries: string[];
|
|
52
|
+
try {
|
|
53
|
+
shortIdEntries = fs.readdirSync(this.geminiTmpDir);
|
|
54
|
+
} catch {
|
|
55
|
+
return empty;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for (const shortId of shortIdEntries) {
|
|
59
|
+
const chatsDir = path.join(this.geminiTmpDir, shortId, CHATS_DIR_NAME);
|
|
60
|
+
if (!isDirectory(chatsDir)) continue;
|
|
61
|
+
|
|
62
|
+
for (const fileName of safeReaddir(chatsDir)) {
|
|
63
|
+
if (!this.isSessionFile(fileName)) continue;
|
|
64
|
+
|
|
65
|
+
const session = this.readCandidateSession(chatsDir, fileName, cwdHashMap);
|
|
66
|
+
if (!session) continue;
|
|
67
|
+
|
|
68
|
+
contentCache.set(session.filePath, session.content);
|
|
69
|
+
sessions.push(session.sessionFile);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return { sessions, contentCache };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
discoverHistoricalSessionFiles(): string[] {
|
|
77
|
+
if (!isDirectory(this.geminiTmpDir)) return [];
|
|
78
|
+
|
|
79
|
+
const files: string[] = [];
|
|
80
|
+
for (const shortId of safeReaddir(this.geminiTmpDir)) {
|
|
81
|
+
const chatsDir = path.join(this.geminiTmpDir, shortId, CHATS_DIR_NAME);
|
|
82
|
+
if (!isDirectory(chatsDir)) continue;
|
|
83
|
+
|
|
84
|
+
for (const fileName of safeReaddir(chatsDir)) {
|
|
85
|
+
if (this.isSessionFile(fileName)) {
|
|
86
|
+
files.push(path.join(chatsDir, fileName));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return files;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private readCandidateSession(
|
|
95
|
+
chatsDir: string,
|
|
96
|
+
fileName: string,
|
|
97
|
+
cwdHashMap: Map<string, string>,
|
|
98
|
+
): { filePath: string; content: string; sessionFile: SessionFile } | null {
|
|
99
|
+
const filePath = path.join(chatsDir, fileName);
|
|
100
|
+
|
|
101
|
+
let content: string;
|
|
102
|
+
try {
|
|
103
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
104
|
+
} catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const parsed = this.parseSessionMetadata(content);
|
|
109
|
+
if (!parsed?.projectHash) return null;
|
|
110
|
+
|
|
111
|
+
const resolvedCwd = cwdHashMap.get(parsed.projectHash);
|
|
112
|
+
if (!resolvedCwd) return null;
|
|
113
|
+
|
|
114
|
+
let birthtimeMs = 0;
|
|
115
|
+
try {
|
|
116
|
+
birthtimeMs = fs.statSync(filePath).birthtimeMs;
|
|
117
|
+
} catch {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
filePath,
|
|
123
|
+
content,
|
|
124
|
+
sessionFile: {
|
|
125
|
+
sessionId: parsed.sessionId || fileName.replace(/\.json$/, ""),
|
|
126
|
+
filePath,
|
|
127
|
+
projectDir: chatsDir,
|
|
128
|
+
birthtimeMs,
|
|
129
|
+
resolvedCwd,
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private buildCwdHashMap(processes: ProcessInfo[]): Map<string, string> {
|
|
135
|
+
const map = new Map<string, string>();
|
|
136
|
+
for (const proc of processes) {
|
|
137
|
+
if (!proc.cwd) continue;
|
|
138
|
+
|
|
139
|
+
// Gemini CLI resolves its project root by walking up from the
|
|
140
|
+
// startup directory looking for a `.git` boundary marker. A
|
|
141
|
+
// session's projectHash therefore tracks that ancestor rather
|
|
142
|
+
// than the process' actual CWD. Enumerate every ancestor as a
|
|
143
|
+
// candidate so subdirectory invocations still line up with the
|
|
144
|
+
// session the Gemini process wrote.
|
|
145
|
+
for (const candidate of this.candidateProjectRoots(proc.cwd)) {
|
|
146
|
+
const hash = this.hashProjectRoot(candidate);
|
|
147
|
+
if (!map.has(hash)) {
|
|
148
|
+
map.set(hash, proc.cwd);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return map;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private candidateProjectRoots(cwd: string): string[] {
|
|
156
|
+
const roots: string[] = [];
|
|
157
|
+
let current = path.resolve(cwd);
|
|
158
|
+
let parent = path.dirname(current);
|
|
159
|
+
while (parent !== current) {
|
|
160
|
+
roots.push(current);
|
|
161
|
+
current = parent;
|
|
162
|
+
parent = path.dirname(current);
|
|
163
|
+
}
|
|
164
|
+
roots.push(current);
|
|
165
|
+
return roots;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private hashProjectRoot(projectRoot: string): string {
|
|
169
|
+
return crypto.createHash("sha256").update(projectRoot).digest("hex");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private isSessionFile(fileName: string): boolean {
|
|
173
|
+
return fileName.startsWith(SESSION_FILE_PREFIX) && fileName.endsWith(".json");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private parseSessionMetadata(content: string): GeminiSessionMetadata | null {
|
|
177
|
+
try {
|
|
178
|
+
return JSON.parse(content) as GeminiSessionMetadata;
|
|
179
|
+
} catch {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|