@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,271 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import type { ConversationMessage, SessionSummary } from "../../adapters/AgentAdapter.js";
|
|
3
|
+
import { AgentStatus } from "../../adapters/AgentAdapter.js";
|
|
4
|
+
import { safeReadFile, safeStat } from "../../utils/session.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A single Gemini CLI message content part. Mirrors the `{text?: string}`
|
|
8
|
+
* shape that Gemini writes for user message parts (derived from the
|
|
9
|
+
* Gemini API `Content.parts[]` schema). Non-text part variants (data,
|
|
10
|
+
* file, etc.) are preserved via the index signature but ignored by
|
|
11
|
+
* `resolveContent` since the adapter only surfaces human-readable text.
|
|
12
|
+
*/
|
|
13
|
+
interface GeminiContentPart {
|
|
14
|
+
text?: string;
|
|
15
|
+
[key: string]: unknown;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type GeminiMessageContent = string | GeminiContentPart[];
|
|
19
|
+
|
|
20
|
+
export interface GeminiMessageEntry {
|
|
21
|
+
id?: string;
|
|
22
|
+
timestamp?: string;
|
|
23
|
+
type?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Gemini CLI stores two different content shapes depending on the
|
|
26
|
+
* message origin:
|
|
27
|
+
* - `type: "user"` messages carry the raw Part[] from userContent.parts
|
|
28
|
+
* (e.g. `[{ text: "hello" }]`).
|
|
29
|
+
* - `type: "gemini"` (assistant) messages carry a pre-joined string
|
|
30
|
+
* built from `consolidatedParts.filter(p => p.text).join('').trim()`.
|
|
31
|
+
* Both forms must be normalized via resolveContent before any string
|
|
32
|
+
* operation is applied.
|
|
33
|
+
*/
|
|
34
|
+
content?: GeminiMessageContent;
|
|
35
|
+
displayContent?: GeminiMessageContent;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface GeminiSessionFile {
|
|
39
|
+
sessionId?: string;
|
|
40
|
+
projectHash?: string;
|
|
41
|
+
startTime?: string;
|
|
42
|
+
lastUpdated?: string;
|
|
43
|
+
messages?: GeminiMessageEntry[];
|
|
44
|
+
directories?: string[];
|
|
45
|
+
kind?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface GeminiSession {
|
|
49
|
+
sessionId: string;
|
|
50
|
+
projectPath: string;
|
|
51
|
+
summary: string;
|
|
52
|
+
sessionStart: Date;
|
|
53
|
+
lastActive: Date;
|
|
54
|
+
lastMessageType?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const IDLE_THRESHOLD_MINUTES = 5;
|
|
58
|
+
|
|
59
|
+
export class GeminiSessionParser {
|
|
60
|
+
/**
|
|
61
|
+
* Parse session file content into GeminiSession.
|
|
62
|
+
* Uses cached content if available, otherwise reads from disk.
|
|
63
|
+
*/
|
|
64
|
+
parseSession(cachedContent: string | undefined, filePath: string): GeminiSession | null {
|
|
65
|
+
const content = cachedContent ?? this.readSessionFile(filePath);
|
|
66
|
+
if (content === null) return null;
|
|
67
|
+
|
|
68
|
+
const parsed = this.parseSessionJson(content);
|
|
69
|
+
if (!parsed?.sessionId) return null;
|
|
70
|
+
|
|
71
|
+
const messages = Array.isArray(parsed.messages) ? parsed.messages : [];
|
|
72
|
+
const lastEntry = messages.length > 0 ? messages[messages.length - 1] : undefined;
|
|
73
|
+
const fileStat = safeStat(filePath);
|
|
74
|
+
|
|
75
|
+
const lastActive =
|
|
76
|
+
this.parseTimestamp(parsed.lastUpdated) ||
|
|
77
|
+
this.parseTimestamp(lastEntry?.timestamp) ||
|
|
78
|
+
fileStat?.mtime ||
|
|
79
|
+
new Date();
|
|
80
|
+
|
|
81
|
+
const sessionStart = this.parseTimestamp(parsed.startTime) || lastActive;
|
|
82
|
+
const projectPath =
|
|
83
|
+
Array.isArray(parsed.directories) && parsed.directories.length > 0
|
|
84
|
+
? parsed.directories[0]
|
|
85
|
+
: "";
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
sessionId: parsed.sessionId,
|
|
89
|
+
projectPath,
|
|
90
|
+
summary: this.extractSummary(messages),
|
|
91
|
+
sessionStart,
|
|
92
|
+
lastActive,
|
|
93
|
+
lastMessageType: lastEntry?.type,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
determineStatus(session: GeminiSession): AgentStatus {
|
|
98
|
+
const diffMs = Date.now() - session.lastActive.getTime();
|
|
99
|
+
const diffMinutes = diffMs / 60000;
|
|
100
|
+
|
|
101
|
+
if (diffMinutes > IDLE_THRESHOLD_MINUTES) {
|
|
102
|
+
return AgentStatus.IDLE;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (session.lastMessageType === "gemini" || session.lastMessageType === "assistant") {
|
|
106
|
+
return AgentStatus.WAITING;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return AgentStatus.RUNNING;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Read the full conversation from a Gemini CLI session JSON file.
|
|
114
|
+
*
|
|
115
|
+
* Gemini sessions store messages in an array with `type` field — typically
|
|
116
|
+
* 'user' or 'gemini' for visible turns, with tool and system entries mixed in.
|
|
117
|
+
*/
|
|
118
|
+
getConversation(sessionFilePath: string, options?: { verbose?: boolean }): ConversationMessage[] {
|
|
119
|
+
const verbose = options?.verbose ?? false;
|
|
120
|
+
const content = safeReadFile(sessionFilePath);
|
|
121
|
+
if (content === undefined) return [];
|
|
122
|
+
|
|
123
|
+
const parsed = this.parseSessionJson(content);
|
|
124
|
+
if (!Array.isArray(parsed?.messages)) return [];
|
|
125
|
+
|
|
126
|
+
const messages: ConversationMessage[] = [];
|
|
127
|
+
for (const entry of parsed.messages) {
|
|
128
|
+
const role = this.roleForEntry(entry?.type, verbose);
|
|
129
|
+
if (!role) continue;
|
|
130
|
+
|
|
131
|
+
const text = this.messageText(entry).trim();
|
|
132
|
+
if (!text) continue;
|
|
133
|
+
|
|
134
|
+
messages.push({
|
|
135
|
+
role,
|
|
136
|
+
content: text,
|
|
137
|
+
timestamp: entry.timestamp,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return messages;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Read a Gemini session JSON file and produce a {@link SessionSummary}.
|
|
146
|
+
* Returns null when the file is unreadable, the JSON doesn't parse,
|
|
147
|
+
* or the body lacks a sessionId.
|
|
148
|
+
*/
|
|
149
|
+
fileToSessionSummary(filePath: string): SessionSummary | null {
|
|
150
|
+
const content = safeReadFile(filePath);
|
|
151
|
+
if (content === undefined) return null;
|
|
152
|
+
|
|
153
|
+
const parsed = this.parseSessionJson(content);
|
|
154
|
+
if (!parsed?.sessionId) return null;
|
|
155
|
+
|
|
156
|
+
const messages = Array.isArray(parsed.messages) ? parsed.messages : [];
|
|
157
|
+
const firstUserMessage = this.extractFirstUserMessage(messages);
|
|
158
|
+
const cwd =
|
|
159
|
+
Array.isArray(parsed.directories) && parsed.directories.length > 0
|
|
160
|
+
? parsed.directories[0]
|
|
161
|
+
: "";
|
|
162
|
+
|
|
163
|
+
const stat = safeStat(filePath);
|
|
164
|
+
const lastEntryTimestamp = this.parseTimestamp(
|
|
165
|
+
messages.length > 0 ? messages[messages.length - 1]?.timestamp : undefined,
|
|
166
|
+
);
|
|
167
|
+
const lastActive =
|
|
168
|
+
this.parseTimestamp(parsed.lastUpdated) || lastEntryTimestamp || stat?.mtime || new Date();
|
|
169
|
+
const startedAt =
|
|
170
|
+
this.parseTimestamp(parsed.startTime) || stat?.birthtime || stat?.mtime || lastActive;
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
type: "gemini_cli",
|
|
174
|
+
sessionId: parsed.sessionId,
|
|
175
|
+
cwd,
|
|
176
|
+
firstUserMessage,
|
|
177
|
+
lastActive,
|
|
178
|
+
startedAt,
|
|
179
|
+
sessionFilePath: filePath,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private parseSessionJson(content: string): GeminiSessionFile | null {
|
|
184
|
+
try {
|
|
185
|
+
return JSON.parse(content) as GeminiSessionFile;
|
|
186
|
+
} catch {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private readSessionFile(filePath: string): string | null {
|
|
192
|
+
try {
|
|
193
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
194
|
+
} catch {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private roleForEntry(
|
|
200
|
+
entryType: string | undefined,
|
|
201
|
+
verbose: boolean,
|
|
202
|
+
): ConversationMessage["role"] | null {
|
|
203
|
+
if (entryType === "user") return "user";
|
|
204
|
+
if (entryType === "gemini" || entryType === "assistant") return "assistant";
|
|
205
|
+
if (verbose && entryType) return "system";
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private extractSummary(messages: GeminiMessageEntry[]): string {
|
|
210
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
211
|
+
const entry = messages[i];
|
|
212
|
+
if (entry?.type !== "user") continue;
|
|
213
|
+
const text = this.messageText(entry).trim();
|
|
214
|
+
if (text) return this.truncate(text, 120);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return "Gemini CLI session active";
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Normalize an entry's content/displayContent into a plain string.
|
|
222
|
+
* Prefers displayContent when both are present (matches Gemini CLI's
|
|
223
|
+
* own rendering priority for the /chat UI).
|
|
224
|
+
*/
|
|
225
|
+
private messageText(entry: GeminiMessageEntry): string {
|
|
226
|
+
const displayText = this.resolveContent(entry.displayContent);
|
|
227
|
+
if (displayText) return displayText;
|
|
228
|
+
return this.resolveContent(entry.content);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Collapse a Gemini message content field into plain text.
|
|
233
|
+
* Accepts either a pre-joined string (assistant turns) or a Part[]
|
|
234
|
+
* list (user turns carrying `[{text: "..."}]`). Non-text part
|
|
235
|
+
* variants (data, file) are dropped since this helper is only used
|
|
236
|
+
* for summary/conversation rendering.
|
|
237
|
+
*/
|
|
238
|
+
private resolveContent(content: GeminiMessageContent | undefined): string {
|
|
239
|
+
if (!content) return "";
|
|
240
|
+
if (typeof content === "string") return content;
|
|
241
|
+
if (!Array.isArray(content)) return "";
|
|
242
|
+
|
|
243
|
+
const parts: string[] = [];
|
|
244
|
+
for (const part of content) {
|
|
245
|
+
if (part && typeof part.text === "string" && part.text) {
|
|
246
|
+
parts.push(part.text);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return parts.join("");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private extractFirstUserMessage(messages: GeminiMessageEntry[]): string {
|
|
253
|
+
for (const entry of messages) {
|
|
254
|
+
if (entry?.type !== "user") continue;
|
|
255
|
+
const text = this.messageText(entry).trim();
|
|
256
|
+
if (text) return text;
|
|
257
|
+
}
|
|
258
|
+
return "";
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private parseTimestamp(value?: string): Date | null {
|
|
262
|
+
if (!value) return null;
|
|
263
|
+
const timestamp = new Date(value);
|
|
264
|
+
return Number.isNaN(timestamp.getTime()) ? null : timestamp;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
private truncate(value: string, maxLength: number): string {
|
|
268
|
+
if (value.length <= maxLength) return value;
|
|
269
|
+
return `${value.slice(0, maxLength - 3)}...`;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode Adapter
|
|
3
|
+
*
|
|
4
|
+
* Detects running OpenCode agents by:
|
|
5
|
+
* 1. Filtering OpenCode processes from a shared asynchronous process snapshot
|
|
6
|
+
* 2. Using snapshot CWD and start-time enrichment
|
|
7
|
+
* 3. Querying OpenCode's SQLite DB (~/.local/share/opencode/opencode.db) to
|
|
8
|
+
* find the session matching each process's CWD and read status from message.time.completed
|
|
9
|
+
*
|
|
10
|
+
* sessionFilePath encodes "<dbPath>::<sessionId>" so getConversation() can open the right
|
|
11
|
+
* DB row without extending the AgentAdapter interface.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type {
|
|
15
|
+
AgentAdapter,
|
|
16
|
+
AgentInfo,
|
|
17
|
+
ProcessInfo,
|
|
18
|
+
ConversationMessage,
|
|
19
|
+
SessionSummary,
|
|
20
|
+
ListSessionsOptions,
|
|
21
|
+
AgentDetectionContext,
|
|
22
|
+
} from "../../adapters/AgentAdapter.js";
|
|
23
|
+
import {
|
|
24
|
+
captureProcessSnapshot,
|
|
25
|
+
executableBasename,
|
|
26
|
+
filterByProcessNames,
|
|
27
|
+
} from "../../utils/process.js";
|
|
28
|
+
import { OpenCodeAgentMapper } from "./OpenCodeAgentMapper.js";
|
|
29
|
+
import { OpenCodeSessionLocator } from "./OpenCodeSessionLocator.js";
|
|
30
|
+
import { OpenCodeSessionParser } from "./OpenCodeSessionParser.js";
|
|
31
|
+
|
|
32
|
+
const SESSION_REF_SEP = "::";
|
|
33
|
+
|
|
34
|
+
function decodeSessionRef(ref: string): { sessionId: string } | null {
|
|
35
|
+
const idx = ref.lastIndexOf(SESSION_REF_SEP);
|
|
36
|
+
if (idx === -1) return null;
|
|
37
|
+
return { sessionId: ref.slice(idx + SESSION_REF_SEP.length) };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class OpenCodeAdapter implements AgentAdapter {
|
|
41
|
+
readonly type = "opencode" as const;
|
|
42
|
+
readonly processNames = ["opencode"] as const;
|
|
43
|
+
|
|
44
|
+
private readonly parser: OpenCodeSessionParser;
|
|
45
|
+
private readonly locator: OpenCodeSessionLocator;
|
|
46
|
+
private readonly mapper: OpenCodeAgentMapper;
|
|
47
|
+
private readonly cleanup = (): void => this.close();
|
|
48
|
+
|
|
49
|
+
constructor(dbPath?: string) {
|
|
50
|
+
this.parser = new OpenCodeSessionParser();
|
|
51
|
+
this.locator = new OpenCodeSessionLocator(dbPath, this.parser);
|
|
52
|
+
this.mapper = new OpenCodeAgentMapper(this.locator.dbFilePath);
|
|
53
|
+
process.once("exit", this.cleanup);
|
|
54
|
+
process.once("SIGINT", this.cleanup);
|
|
55
|
+
process.once("SIGTERM", this.cleanup);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
close(): void {
|
|
59
|
+
process.off("exit", this.cleanup);
|
|
60
|
+
process.off("SIGINT", this.cleanup);
|
|
61
|
+
process.off("SIGTERM", this.cleanup);
|
|
62
|
+
this.locator.close();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
canHandle(processInfo: ProcessInfo): boolean {
|
|
66
|
+
const base = executableBasename(processInfo.command);
|
|
67
|
+
return base === "opencode" || base === "opencode.exe";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async detectAgents(context?: AgentDetectionContext): Promise<AgentInfo[]> {
|
|
71
|
+
const snapshot = context?.processes ?? (await captureProcessSnapshot(this.processNames));
|
|
72
|
+
const relevant = filterByProcessNames(snapshot, this.processNames);
|
|
73
|
+
const processes = relevant.filter((process) => this.canHandle(process));
|
|
74
|
+
if (processes.length === 0) return [];
|
|
75
|
+
|
|
76
|
+
const db = this.locator.openDb();
|
|
77
|
+
if (!db) return processes.map((process) => this.mapper.mapProcessOnlyAgent(process));
|
|
78
|
+
|
|
79
|
+
const agents: AgentInfo[] = [];
|
|
80
|
+
for (const proc of processes) {
|
|
81
|
+
if (!proc.cwd) {
|
|
82
|
+
agents.push(this.mapper.mapProcessOnlyAgent(proc));
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const session = this.locator.findSessionForDirectory(db, proc.cwd);
|
|
87
|
+
if (!session) {
|
|
88
|
+
agents.push(this.mapper.mapProcessOnlyAgent(proc));
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const stats = this.parser.getSessionStats(db, session.sessionId);
|
|
93
|
+
agents.push(this.mapper.mapSessionToAgent(session, stats, proc));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return agents;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
getConversation(sessionFilePath: string, options?: { verbose?: boolean }): ConversationMessage[] {
|
|
100
|
+
const ref = decodeSessionRef(sessionFilePath);
|
|
101
|
+
if (!ref) return [];
|
|
102
|
+
|
|
103
|
+
const db = this.locator.openDb();
|
|
104
|
+
if (!db) return [];
|
|
105
|
+
|
|
106
|
+
return this.parser.getConversation(db, ref.sessionId, options);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async listSessions(opts?: ListSessionsOptions): Promise<SessionSummary[]> {
|
|
110
|
+
return this.locator.listSessions(opts);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { AgentInfo, ProcessInfo } from "../../adapters/AgentAdapter.js";
|
|
2
|
+
import { AgentStatus } from "../../adapters/AgentAdapter.js";
|
|
3
|
+
import { generateAgentName } from "../../utils/matching.js";
|
|
4
|
+
import type { OpenCodeSession } from "./OpenCodeSessionLocator.js";
|
|
5
|
+
import type { OpenCodeSessionStats } from "./OpenCodeSessionParser.js";
|
|
6
|
+
|
|
7
|
+
const SESSION_REF_SEP = "::";
|
|
8
|
+
const IDLE_THRESHOLD_MINUTES = 5;
|
|
9
|
+
|
|
10
|
+
function encodeSessionRef(dbPath: string, sessionId: string): string {
|
|
11
|
+
return `${dbPath}${SESSION_REF_SEP}${sessionId}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class OpenCodeAgentMapper {
|
|
15
|
+
constructor(private readonly dbPath: string) {}
|
|
16
|
+
|
|
17
|
+
mapSessionToAgent(
|
|
18
|
+
session: OpenCodeSession,
|
|
19
|
+
stats: OpenCodeSessionStats,
|
|
20
|
+
proc: ProcessInfo,
|
|
21
|
+
): AgentInfo {
|
|
22
|
+
const lastActive =
|
|
23
|
+
stats.lastTimeUpdated > 0 ? new Date(stats.lastTimeUpdated) : new Date(session.timeCreated);
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
name: generateAgentName(session.directory || proc.cwd || "", proc.pid),
|
|
27
|
+
type: "opencode",
|
|
28
|
+
status: this.determineStatus(stats, lastActive),
|
|
29
|
+
summary: stats.summary || "OpenCode session active",
|
|
30
|
+
pid: proc.pid,
|
|
31
|
+
projectPath: session.directory || proc.cwd || "",
|
|
32
|
+
sessionId: session.sessionId,
|
|
33
|
+
lastActive,
|
|
34
|
+
sessionFilePath: encodeSessionRef(this.dbPath, session.sessionId),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
mapProcessOnlyAgent(proc: ProcessInfo): AgentInfo {
|
|
39
|
+
return {
|
|
40
|
+
name: generateAgentName(proc.cwd || "", proc.pid),
|
|
41
|
+
type: "opencode",
|
|
42
|
+
status: AgentStatus.RUNNING,
|
|
43
|
+
summary: "OpenCode process running",
|
|
44
|
+
pid: proc.pid,
|
|
45
|
+
projectPath: proc.cwd || "",
|
|
46
|
+
sessionId: `pid-${proc.pid}`,
|
|
47
|
+
lastActive: new Date(),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private determineStatus(stats: OpenCodeSessionStats, lastActive: Date): AgentStatus {
|
|
52
|
+
const ageMin = (Date.now() - lastActive.getTime()) / 60000;
|
|
53
|
+
if (ageMin > IDLE_THRESHOLD_MINUTES) return AgentStatus.IDLE;
|
|
54
|
+
|
|
55
|
+
if (stats.lastRole === "assistant" && !stats.lastAssistantCompleted) {
|
|
56
|
+
return AgentStatus.RUNNING;
|
|
57
|
+
}
|
|
58
|
+
if (stats.lastRole === "assistant") return AgentStatus.WAITING;
|
|
59
|
+
return AgentStatus.RUNNING;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import Database from "better-sqlite3";
|
|
4
|
+
import type { ListSessionsOptions, SessionSummary } from "../../adapters/AgentAdapter.js";
|
|
5
|
+
import { OpenCodeSessionParser } from "./OpenCodeSessionParser.js";
|
|
6
|
+
|
|
7
|
+
const SESSION_REF_SEP = "::";
|
|
8
|
+
|
|
9
|
+
function encodeOpenCodeSessionRef(dbPath: string, sessionId: string): string {
|
|
10
|
+
return `${dbPath}${SESSION_REF_SEP}${sessionId}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface OpenCodeSession {
|
|
14
|
+
sessionId: string;
|
|
15
|
+
directory: string;
|
|
16
|
+
timeCreated: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class OpenCodeSessionLocator {
|
|
20
|
+
private readonly dbPath: string;
|
|
21
|
+
private db: Database.Database | null = null;
|
|
22
|
+
|
|
23
|
+
constructor(
|
|
24
|
+
dbPath = OpenCodeSessionLocator.resolveDbPath(),
|
|
25
|
+
private readonly parser: OpenCodeSessionParser = new OpenCodeSessionParser(),
|
|
26
|
+
) {
|
|
27
|
+
this.dbPath = dbPath;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static resolveDbPath(): string {
|
|
31
|
+
const xdg = process.env.XDG_DATA_HOME;
|
|
32
|
+
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
33
|
+
const base = xdg || path.join(home, ".local", "share");
|
|
34
|
+
return path.join(base, "opencode", "opencode.db");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
openDb(): Database.Database | null {
|
|
38
|
+
if (this.db) return this.db;
|
|
39
|
+
if (!fs.existsSync(this.dbPath)) return null;
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
this.db = new Database(this.dbPath, { readonly: true });
|
|
43
|
+
return this.db;
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
close(): void {
|
|
50
|
+
if (this.db) {
|
|
51
|
+
try {
|
|
52
|
+
this.db.close();
|
|
53
|
+
} catch {
|
|
54
|
+
/* ignore */
|
|
55
|
+
}
|
|
56
|
+
this.db = null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
findSessionForDirectory(db: Database.Database, directory: string): OpenCodeSession | null {
|
|
61
|
+
try {
|
|
62
|
+
const row = db
|
|
63
|
+
.prepare<[string], { id: string; directory: string; time_created: number }>(`
|
|
64
|
+
SELECT id, directory, time_created
|
|
65
|
+
FROM session
|
|
66
|
+
WHERE directory = ?
|
|
67
|
+
ORDER BY time_created DESC
|
|
68
|
+
LIMIT 1
|
|
69
|
+
`)
|
|
70
|
+
.get(directory);
|
|
71
|
+
|
|
72
|
+
if (!row) return null;
|
|
73
|
+
return { sessionId: row.id, directory: row.directory, timeCreated: row.time_created };
|
|
74
|
+
} catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
listSessions(opts?: ListSessionsOptions): SessionSummary[] {
|
|
80
|
+
const db = this.openDb();
|
|
81
|
+
if (!db) return [];
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const rows = db
|
|
85
|
+
.prepare<[], { id: string; directory: string; timeCreated: number }>(`
|
|
86
|
+
SELECT id, directory, time_created AS timeCreated
|
|
87
|
+
FROM session
|
|
88
|
+
ORDER BY time_created DESC
|
|
89
|
+
`)
|
|
90
|
+
.all();
|
|
91
|
+
|
|
92
|
+
const summaries: SessionSummary[] = [];
|
|
93
|
+
|
|
94
|
+
for (const row of rows) {
|
|
95
|
+
if (opts?.cwd !== undefined && row.directory !== opts.cwd) continue;
|
|
96
|
+
|
|
97
|
+
const stats = this.parser.getSessionStats(db, row.id);
|
|
98
|
+
const lastActive =
|
|
99
|
+
stats.lastTimeUpdated > 0 ? new Date(stats.lastTimeUpdated) : new Date(row.timeCreated);
|
|
100
|
+
const startedAt = new Date(row.timeCreated);
|
|
101
|
+
|
|
102
|
+
summaries.push({
|
|
103
|
+
type: "opencode",
|
|
104
|
+
sessionId: row.id,
|
|
105
|
+
cwd: row.directory,
|
|
106
|
+
firstUserMessage: stats.summary,
|
|
107
|
+
lastActive,
|
|
108
|
+
startedAt,
|
|
109
|
+
sessionFilePath: encodeOpenCodeSessionRef(this.dbPath, row.id),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return summaries;
|
|
114
|
+
} catch {
|
|
115
|
+
this.close();
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
get dbFilePath(): string {
|
|
121
|
+
return this.dbPath;
|
|
122
|
+
}
|
|
123
|
+
}
|