flockbay 0.10.15
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/README.md +56 -0
- package/bin/flockbay-mcp.mjs +56 -0
- package/bin/flockbay.mjs +78 -0
- package/dist/codex/flockbayMcpStdioBridge.cjs +383 -0
- package/dist/codex/flockbayMcpStdioBridge.d.cts +2 -0
- package/dist/codex/flockbayMcpStdioBridge.d.mts +2 -0
- package/dist/codex/flockbayMcpStdioBridge.mjs +381 -0
- package/dist/flockbayScreenshotGate-DJX3Is5d.mjs +136 -0
- package/dist/flockbayScreenshotGate-DkxU24cR.cjs +138 -0
- package/dist/index--o4BPz5o.cjs +10311 -0
- package/dist/index-CUp3juDS.mjs +10268 -0
- package/dist/index.cjs +43 -0
- package/dist/index.d.cts +1 -0
- package/dist/index.d.mts +1 -0
- package/dist/index.mjs +40 -0
- package/dist/lib.cjs +33 -0
- package/dist/lib.d.cts +957 -0
- package/dist/lib.d.mts +957 -0
- package/dist/lib.mjs +23 -0
- package/dist/runCodex-D3eT-TvB.cjs +3449 -0
- package/dist/runCodex-o6PCbHQ7.mjs +3446 -0
- package/dist/runGemini-Bt0oEj_g.mjs +3183 -0
- package/dist/runGemini-CBxZp6I7.cjs +3185 -0
- package/dist/types-C-jnUdn_.cjs +4498 -0
- package/dist/types-DGd6ea2Z.mjs +4450 -0
- package/kits/kit.open_world/kit.json +59 -0
- package/package.json +130 -0
- package/scripts/claude_local_launcher.cjs +73 -0
- package/scripts/claude_remote_launcher.cjs +16 -0
- package/scripts/claude_version_utils.cjs +391 -0
- package/scripts/ripgrep_launcher.cjs +33 -0
- package/scripts/session_hook_forwarder.cjs +49 -0
- package/scripts/test-codex-abort-history.mjs +77 -0
- package/scripts/unpack-tools.cjs +222 -0
- package/tools/licenses/difftastic-LICENSE +21 -0
- package/tools/licenses/ripgrep-LICENSE +3 -0
- package/tools/unreal-mcp/UPSTREAM_VERSION.md +8 -0
- package/tools/unreal-mcp/upstream/Docs/README.md +8 -0
- package/tools/unreal-mcp/upstream/Docs/Tools/README.md +7 -0
- package/tools/unreal-mcp/upstream/Docs/Tools/actor_tools.md +184 -0
- package/tools/unreal-mcp/upstream/Docs/Tools/blueprint_tools.md +268 -0
- package/tools/unreal-mcp/upstream/Docs/Tools/editor_tools.md +104 -0
- package/tools/unreal-mcp/upstream/Docs/Tools/node_tools.md +274 -0
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Config/FilterPlugin.ini +8 -0
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/Commands/UnrealMCPBlueprintCommands.cpp +1160 -0
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/Commands/UnrealMCPBlueprintNodeCommands.cpp +924 -0
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/Commands/UnrealMCPCommonUtils.cpp +709 -0
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/Commands/UnrealMCPEditorCommands.cpp +896 -0
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/Commands/UnrealMCPProjectCommands.cpp +72 -0
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/Commands/UnrealMCPUMGCommands.cpp +544 -0
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/MCPServerRunnable.cpp +321 -0
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/UnrealMCPBridge.cpp +419 -0
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/UnrealMCPModule.cpp +21 -0
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Public/Commands/UnrealMCPBlueprintCommands.h +34 -0
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Public/Commands/UnrealMCPBlueprintNodeCommands.h +27 -0
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Public/Commands/UnrealMCPCommonUtils.h +59 -0
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Public/Commands/UnrealMCPEditorCommands.h +40 -0
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Public/Commands/UnrealMCPProjectCommands.h +20 -0
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Public/Commands/UnrealMCPUMGCommands.h +82 -0
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Public/MCPServerRunnable.h +34 -0
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Public/UnrealMCPBridge.h +64 -0
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Public/UnrealMCPModule.h +22 -0
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/UnrealMCP.Build.cs +78 -0
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/UnrealMCP.uplugin +36 -0
- package/tools/unreal-mcp/upstream/Python/README.md +40 -0
- package/tools/unreal-mcp/upstream/Python/pyproject.toml +22 -0
- package/tools/unreal-mcp/upstream/Python/scripts/actors/test_cube.py +203 -0
- package/tools/unreal-mcp/upstream/Python/scripts/blueprints/test_create_and_spawn_blueprints_with_different_components.py +497 -0
- package/tools/unreal-mcp/upstream/Python/scripts/blueprints/test_create_and_spawn_cube_blueprint.py +194 -0
- package/tools/unreal-mcp/upstream/Python/scripts/node/test_component_reference.py +267 -0
- package/tools/unreal-mcp/upstream/Python/scripts/node/test_create_bird_blueprint_with_input_and_camera.py +618 -0
- package/tools/unreal-mcp/upstream/Python/scripts/node/test_input_mapping.py +366 -0
- package/tools/unreal-mcp/upstream/Python/scripts/node/test_physics_variables.py +390 -0
- package/tools/unreal-mcp/upstream/Python/tools/blueprint_tools.py +420 -0
- package/tools/unreal-mcp/upstream/Python/tools/editor_tools.py +369 -0
- package/tools/unreal-mcp/upstream/Python/tools/node_tools.py +430 -0
- package/tools/unreal-mcp/upstream/Python/tools/project_tools.py +64 -0
- package/tools/unreal-mcp/upstream/Python/tools/umg_tools.py +333 -0
- package/tools/unreal-mcp/upstream/Python/unreal_mcp_server.py +398 -0
- package/tools/unreal-mcp/upstream/Python/uv.lock +521 -0
|
@@ -0,0 +1,3185 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var ink = require('ink');
|
|
4
|
+
var React = require('react');
|
|
5
|
+
var node_crypto = require('node:crypto');
|
|
6
|
+
var os = require('node:os');
|
|
7
|
+
var path = require('node:path');
|
|
8
|
+
var fs$2 = require('node:fs/promises');
|
|
9
|
+
var types = require('./types-C-jnUdn_.cjs');
|
|
10
|
+
var index = require('./index--o4BPz5o.cjs');
|
|
11
|
+
var node_child_process = require('node:child_process');
|
|
12
|
+
var sdk = require('@agentclientprotocol/sdk');
|
|
13
|
+
var fs = require('fs');
|
|
14
|
+
var path$1 = require('path');
|
|
15
|
+
var os$1 = require('os');
|
|
16
|
+
var child_process = require('child_process');
|
|
17
|
+
var fs$1 = require('node:fs');
|
|
18
|
+
var flockbayScreenshotGate = require('./flockbayScreenshotGate-DkxU24cR.cjs');
|
|
19
|
+
require('axios');
|
|
20
|
+
require('chalk');
|
|
21
|
+
require('zod');
|
|
22
|
+
require('tweetnacl');
|
|
23
|
+
require('node:events');
|
|
24
|
+
require('socket.io-client');
|
|
25
|
+
require('fs/promises');
|
|
26
|
+
require('crypto');
|
|
27
|
+
require('url');
|
|
28
|
+
require('node:process');
|
|
29
|
+
require('node:net');
|
|
30
|
+
require('expo-server-sdk');
|
|
31
|
+
require('node:readline');
|
|
32
|
+
require('node:url');
|
|
33
|
+
require('ps-list');
|
|
34
|
+
require('cross-spawn');
|
|
35
|
+
require('tmp');
|
|
36
|
+
require('qrcode-terminal');
|
|
37
|
+
require('open');
|
|
38
|
+
require('fastify');
|
|
39
|
+
require('fastify-type-provider-zod');
|
|
40
|
+
require('@modelcontextprotocol/sdk/server/mcp.js');
|
|
41
|
+
require('node:http');
|
|
42
|
+
require('@modelcontextprotocol/sdk/server/streamableHttp.js');
|
|
43
|
+
require('http');
|
|
44
|
+
require('util');
|
|
45
|
+
|
|
46
|
+
const KNOWN_TOOL_PATTERNS = {
|
|
47
|
+
change_title: ["change_title", "change-title", "mcp__flockbay__change_title"],
|
|
48
|
+
save_memory: ["save_memory", "save-memory"],
|
|
49
|
+
think: ["think"]
|
|
50
|
+
};
|
|
51
|
+
function isInvestigationTool(toolCallId, toolKind) {
|
|
52
|
+
return toolCallId.includes("codebase_investigator") || toolCallId.includes("investigator") || typeof toolKind === "string" && toolKind.includes("investigator");
|
|
53
|
+
}
|
|
54
|
+
function extractToolNameFromId(toolCallId) {
|
|
55
|
+
const lowerId = toolCallId.toLowerCase();
|
|
56
|
+
for (const [toolName, patterns] of Object.entries(KNOWN_TOOL_PATTERNS)) {
|
|
57
|
+
for (const pattern of patterns) {
|
|
58
|
+
if (lowerId.includes(pattern.toLowerCase())) {
|
|
59
|
+
return toolName;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
function determineToolName(toolName, toolCallId, input, params, context) {
|
|
66
|
+
if (toolName !== "other" && toolName !== "Unknown tool") {
|
|
67
|
+
return toolName;
|
|
68
|
+
}
|
|
69
|
+
const idToolName = extractToolNameFromId(toolCallId);
|
|
70
|
+
if (idToolName) {
|
|
71
|
+
return idToolName;
|
|
72
|
+
}
|
|
73
|
+
if (input && typeof input === "object") {
|
|
74
|
+
const inputStr = JSON.stringify(input).toLowerCase();
|
|
75
|
+
for (const [toolName2, patterns] of Object.entries(KNOWN_TOOL_PATTERNS)) {
|
|
76
|
+
for (const pattern of patterns) {
|
|
77
|
+
if (inputStr.includes(pattern.toLowerCase())) {
|
|
78
|
+
return toolName2;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const paramsStr = JSON.stringify(params).toLowerCase();
|
|
84
|
+
for (const [toolName2, patterns] of Object.entries(KNOWN_TOOL_PATTERNS)) {
|
|
85
|
+
for (const pattern of patterns) {
|
|
86
|
+
if (paramsStr.includes(pattern.toLowerCase())) {
|
|
87
|
+
return toolName2;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (context?.recentPromptHadChangeTitle && context.toolCallCountSincePrompt === 0) {
|
|
92
|
+
const isEmptyInput = !input || Array.isArray(input) && input.length === 0 || typeof input === "object" && Object.keys(input).length === 0;
|
|
93
|
+
if (isEmptyInput && toolName === "other") {
|
|
94
|
+
return "change_title";
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return toolName;
|
|
98
|
+
}
|
|
99
|
+
function getRealToolName(toolCallId, toolKind) {
|
|
100
|
+
const extracted = extractToolNameFromId(toolCallId);
|
|
101
|
+
if (extracted) {
|
|
102
|
+
return extracted;
|
|
103
|
+
}
|
|
104
|
+
return typeof toolKind === "string" ? toolKind : "unknown";
|
|
105
|
+
}
|
|
106
|
+
function getToolCallTimeout(toolCallId, toolKind) {
|
|
107
|
+
const isInvestigation = isInvestigationTool(toolCallId, toolKind);
|
|
108
|
+
const isThinkTool = toolKind === "think";
|
|
109
|
+
if (isInvestigation) {
|
|
110
|
+
return 6e5;
|
|
111
|
+
} else if (isThinkTool) {
|
|
112
|
+
return 3e4;
|
|
113
|
+
} else {
|
|
114
|
+
return 12e4;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function hasChangeTitleInstruction(prompt) {
|
|
119
|
+
return prompt.toLowerCase().includes("change_title");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const ACP_INIT_TIMEOUT_MS = 12e4;
|
|
123
|
+
const safeStringify = (value) => {
|
|
124
|
+
if (typeof value === "string") return value;
|
|
125
|
+
if (value instanceof Error) return value.message;
|
|
126
|
+
if (!value || typeof value !== "object") return String(value);
|
|
127
|
+
try {
|
|
128
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
129
|
+
return JSON.stringify(value, (_k, v) => {
|
|
130
|
+
if (!v || typeof v !== "object") return v;
|
|
131
|
+
if (seen.has(v)) return "[Circular]";
|
|
132
|
+
seen.add(v);
|
|
133
|
+
return v;
|
|
134
|
+
});
|
|
135
|
+
} catch {
|
|
136
|
+
return String(value);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
function buildDaemonSafeSpawnEnv(command, extraEnv) {
|
|
140
|
+
const env = {
|
|
141
|
+
...process.env,
|
|
142
|
+
...extraEnv || {}
|
|
143
|
+
};
|
|
144
|
+
const pathKey = process.platform === "win32" ? Object.keys(env).find((k) => k.toLowerCase() === "path") || "Path" : "PATH";
|
|
145
|
+
const pathSep = process.platform === "win32" ? ";" : ":";
|
|
146
|
+
const existingPath = String(env[pathKey] || "");
|
|
147
|
+
const existingParts = existingPath.split(pathSep).filter(Boolean);
|
|
148
|
+
const prepend = [];
|
|
149
|
+
const add = (p) => {
|
|
150
|
+
if (!p) return;
|
|
151
|
+
if (!prepend.includes(p) && !existingParts.includes(p)) prepend.push(p);
|
|
152
|
+
};
|
|
153
|
+
if (command && command.includes("/") && !command.startsWith("\\\\")) {
|
|
154
|
+
add(path.dirname(command));
|
|
155
|
+
}
|
|
156
|
+
add(path.dirname(process.execPath));
|
|
157
|
+
add(path.join(os.homedir(), ".local", "bin"));
|
|
158
|
+
add("/opt/homebrew/bin");
|
|
159
|
+
add("/usr/local/bin");
|
|
160
|
+
add("/usr/bin");
|
|
161
|
+
add("/bin");
|
|
162
|
+
add("/usr/sbin");
|
|
163
|
+
add("/sbin");
|
|
164
|
+
env[pathKey] = [...prepend, ...existingParts].join(pathSep);
|
|
165
|
+
return env;
|
|
166
|
+
}
|
|
167
|
+
function nodeToWebStreams(stdin, stdout) {
|
|
168
|
+
const writable = new WritableStream({
|
|
169
|
+
write(chunk) {
|
|
170
|
+
return new Promise((resolve, reject) => {
|
|
171
|
+
const ok = stdin.write(chunk, (err) => {
|
|
172
|
+
if (err) {
|
|
173
|
+
types.logger.debug(`[AcpSdkBackend] Error writing to stdin:`, err);
|
|
174
|
+
reject(err);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
if (ok) {
|
|
178
|
+
resolve();
|
|
179
|
+
} else {
|
|
180
|
+
stdin.once("drain", resolve);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
},
|
|
184
|
+
close() {
|
|
185
|
+
return new Promise((resolve) => {
|
|
186
|
+
stdin.end(resolve);
|
|
187
|
+
});
|
|
188
|
+
},
|
|
189
|
+
abort(reason) {
|
|
190
|
+
stdin.destroy(reason instanceof Error ? reason : new Error(String(reason)));
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
const readable = new ReadableStream({
|
|
194
|
+
start(controller) {
|
|
195
|
+
stdout.on("data", (chunk) => {
|
|
196
|
+
controller.enqueue(new Uint8Array(chunk));
|
|
197
|
+
});
|
|
198
|
+
stdout.on("end", () => {
|
|
199
|
+
controller.close();
|
|
200
|
+
});
|
|
201
|
+
stdout.on("error", (err) => {
|
|
202
|
+
types.logger.debug(`[AcpSdkBackend] Stdout error:`, err);
|
|
203
|
+
controller.error(err);
|
|
204
|
+
});
|
|
205
|
+
},
|
|
206
|
+
cancel() {
|
|
207
|
+
stdout.destroy();
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
return { writable, readable };
|
|
211
|
+
}
|
|
212
|
+
class AcpSdkBackend {
|
|
213
|
+
constructor(options) {
|
|
214
|
+
this.options = options;
|
|
215
|
+
}
|
|
216
|
+
listeners = [];
|
|
217
|
+
process = null;
|
|
218
|
+
connection = null;
|
|
219
|
+
acpSessionId = null;
|
|
220
|
+
disposed = false;
|
|
221
|
+
/** Track active tool calls to prevent duplicate events */
|
|
222
|
+
activeToolCalls = /* @__PURE__ */ new Set();
|
|
223
|
+
toolCallTimeouts = /* @__PURE__ */ new Map();
|
|
224
|
+
/** Track tool call start times for performance monitoring */
|
|
225
|
+
toolCallStartTimes = /* @__PURE__ */ new Map();
|
|
226
|
+
/** Pending permission requests that need response */
|
|
227
|
+
pendingPermissions = /* @__PURE__ */ new Map();
|
|
228
|
+
/** Map from permission request ID to real tool call ID for tracking */
|
|
229
|
+
permissionToToolCallMap = /* @__PURE__ */ new Map();
|
|
230
|
+
/** Map from real tool call ID to tool name for auto-approval */
|
|
231
|
+
toolCallIdToNameMap = /* @__PURE__ */ new Map();
|
|
232
|
+
/** Track if we just sent a prompt with change_title instruction */
|
|
233
|
+
recentPromptHadChangeTitle = false;
|
|
234
|
+
/** Track tool calls count since last prompt (to identify first tool call) */
|
|
235
|
+
toolCallCountSincePrompt = 0;
|
|
236
|
+
/** Timeout for emitting 'idle' status after last message chunk */
|
|
237
|
+
idleTimeout = null;
|
|
238
|
+
/** Keep a small rolling buffer of stderr lines for diagnostics (e.g. auth failures). */
|
|
239
|
+
stderrRing = [];
|
|
240
|
+
pushStderrLine(line) {
|
|
241
|
+
const trimmed = String(line || "").trimEnd();
|
|
242
|
+
if (!trimmed) return;
|
|
243
|
+
this.stderrRing.push(trimmed);
|
|
244
|
+
const MAX = 80;
|
|
245
|
+
if (this.stderrRing.length > MAX) {
|
|
246
|
+
this.stderrRing.splice(0, this.stderrRing.length - MAX);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
stderrExcerpt(maxLines = 20) {
|
|
250
|
+
if (this.stderrRing.length === 0) return null;
|
|
251
|
+
const lines = this.stderrRing.slice(Math.max(0, this.stderrRing.length - maxLines));
|
|
252
|
+
return lines.join("\n");
|
|
253
|
+
}
|
|
254
|
+
onMessage(handler) {
|
|
255
|
+
this.listeners.push(handler);
|
|
256
|
+
}
|
|
257
|
+
offMessage(handler) {
|
|
258
|
+
const index = this.listeners.indexOf(handler);
|
|
259
|
+
if (index !== -1) {
|
|
260
|
+
this.listeners.splice(index, 1);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
emit(msg) {
|
|
264
|
+
if (this.disposed) return;
|
|
265
|
+
for (const listener of this.listeners) {
|
|
266
|
+
try {
|
|
267
|
+
listener(msg);
|
|
268
|
+
} catch (error) {
|
|
269
|
+
types.logger.warn("[AcpSdkBackend] Error in message handler:", error);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
async startSession(initialPrompt) {
|
|
274
|
+
if (this.disposed) {
|
|
275
|
+
throw new Error("Backend has been disposed");
|
|
276
|
+
}
|
|
277
|
+
const sessionId = node_crypto.randomUUID();
|
|
278
|
+
this.emit({ type: "status", status: "starting" });
|
|
279
|
+
try {
|
|
280
|
+
types.logger.debug(`[AcpSdkBackend] Starting session: ${sessionId}`);
|
|
281
|
+
const args = this.options.args || [];
|
|
282
|
+
if (process.platform === "win32") {
|
|
283
|
+
const fullCommand = [this.options.command, ...args].join(" ");
|
|
284
|
+
this.process = node_child_process.spawn("cmd.exe", ["/c", fullCommand], {
|
|
285
|
+
cwd: this.options.cwd,
|
|
286
|
+
env: buildDaemonSafeSpawnEnv(this.options.command, this.options.env),
|
|
287
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
288
|
+
windowsHide: true
|
|
289
|
+
});
|
|
290
|
+
} else {
|
|
291
|
+
this.process = node_child_process.spawn(this.options.command, args, {
|
|
292
|
+
cwd: this.options.cwd,
|
|
293
|
+
env: buildDaemonSafeSpawnEnv(this.options.command, this.options.env),
|
|
294
|
+
// Use 'pipe' for all stdio to capture output without printing to console
|
|
295
|
+
// stdout and stderr will be handled by our event listeners
|
|
296
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
if (this.process.stderr) {
|
|
300
|
+
}
|
|
301
|
+
if (!this.process.stdin || !this.process.stdout || !this.process.stderr) {
|
|
302
|
+
throw new Error("Failed to create stdio pipes");
|
|
303
|
+
}
|
|
304
|
+
this.process.stderr.on("data", (data) => {
|
|
305
|
+
const text = data.toString();
|
|
306
|
+
for (const line of text.split("\n")) this.pushStderrLine(line);
|
|
307
|
+
if (text.trim()) {
|
|
308
|
+
const hasActiveInvestigation = Array.from(this.activeToolCalls).some(
|
|
309
|
+
(id) => isInvestigationTool(id)
|
|
310
|
+
);
|
|
311
|
+
if (hasActiveInvestigation) {
|
|
312
|
+
types.logger.debug(`[AcpSdkBackend] \u{1F50D} Agent stderr (during investigation): ${text.trim()}`);
|
|
313
|
+
} else {
|
|
314
|
+
types.logger.debug(`[AcpSdkBackend] Agent stderr: ${text.trim()}`);
|
|
315
|
+
}
|
|
316
|
+
if (text.includes("status 429") || text.includes('code":429') || text.includes("rateLimitExceeded") || text.includes("RESOURCE_EXHAUSTED")) {
|
|
317
|
+
types.logger.debug("[AcpSdkBackend] \u26A0\uFE0F Detected rate limit error (429) in stderr - gemini-cli will handle retry");
|
|
318
|
+
} else if (text.includes("status 404") || text.includes('code":404')) {
|
|
319
|
+
types.logger.debug("[AcpSdkBackend] \u26A0\uFE0F Detected 404 error in stderr");
|
|
320
|
+
this.emit({
|
|
321
|
+
type: "status",
|
|
322
|
+
status: "error",
|
|
323
|
+
detail: "Model not found. Available models: gemini-2.5-pro, gemini-2.5-flash, gemini-2.5-flash-lite"
|
|
324
|
+
});
|
|
325
|
+
} else if (/authentication required/i.test(text) || /login required/i.test(text) || /invalid.*(api key|key)/i.test(text) || /permission denied/i.test(text)) {
|
|
326
|
+
const excerpt = this.stderrExcerpt(20);
|
|
327
|
+
const detail = excerpt ? `Authentication required (gemini CLI stderr):
|
|
328
|
+
${excerpt}` : "Authentication required";
|
|
329
|
+
this.emit({ type: "status", status: "error", detail });
|
|
330
|
+
} else if (hasActiveInvestigation && (text.includes("timeout") || text.includes("Timeout") || text.includes("failed") || text.includes("Failed") || text.includes("error") || text.includes("Error"))) {
|
|
331
|
+
types.logger.debug(`[AcpSdkBackend] \u{1F50D} Investigation tool stderr error/timeout: ${text.trim()}`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
this.process.on("error", (err) => {
|
|
336
|
+
console.error("[AcpSdkBackend] Process error:", err);
|
|
337
|
+
types.logger.debug(`[AcpSdkBackend] Process error:`, err);
|
|
338
|
+
const excerpt = this.stderrExcerpt(20);
|
|
339
|
+
const detail = excerpt ? `${err.message}
|
|
340
|
+
|
|
341
|
+
Agent stderr:
|
|
342
|
+
${excerpt}` : err.message;
|
|
343
|
+
this.emit({ type: "status", status: "error", detail });
|
|
344
|
+
});
|
|
345
|
+
this.process.on("exit", (code, signal) => {
|
|
346
|
+
if (!this.disposed && code !== 0 && code !== null) {
|
|
347
|
+
types.logger.debug(`[AcpSdkBackend] Process exited with code ${code}, signal ${signal}`);
|
|
348
|
+
this.emit({ type: "status", status: "stopped", detail: `Exit code: ${code}` });
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
const streams = nodeToWebStreams(
|
|
352
|
+
this.process.stdin,
|
|
353
|
+
this.process.stdout
|
|
354
|
+
);
|
|
355
|
+
const writable = streams.writable;
|
|
356
|
+
const readable = streams.readable;
|
|
357
|
+
const backend = this;
|
|
358
|
+
const filteredReadable = new ReadableStream({
|
|
359
|
+
async start(controller) {
|
|
360
|
+
const reader = readable.getReader();
|
|
361
|
+
const decoder = new TextDecoder();
|
|
362
|
+
const encoder = new TextEncoder();
|
|
363
|
+
let buffer = "";
|
|
364
|
+
let filteredCount = 0;
|
|
365
|
+
let didWarn = false;
|
|
366
|
+
let firstFilteredLine = null;
|
|
367
|
+
let didEmitAuthError = false;
|
|
368
|
+
const isValidJSON = (str) => {
|
|
369
|
+
const trimmed = str.trim();
|
|
370
|
+
if (!trimmed || !trimmed.startsWith("{") && !trimmed.startsWith("[")) {
|
|
371
|
+
return false;
|
|
372
|
+
}
|
|
373
|
+
try {
|
|
374
|
+
JSON.parse(trimmed);
|
|
375
|
+
return true;
|
|
376
|
+
} catch {
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
try {
|
|
381
|
+
while (true) {
|
|
382
|
+
const { done, value } = await reader.read();
|
|
383
|
+
if (done) {
|
|
384
|
+
if (buffer.trim()) {
|
|
385
|
+
if (isValidJSON(buffer)) {
|
|
386
|
+
controller.enqueue(encoder.encode(buffer));
|
|
387
|
+
} else {
|
|
388
|
+
filteredCount++;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (filteredCount > 0) {
|
|
392
|
+
const msg = `[AcpSdkBackend] Gemini CLI emitted ${filteredCount} non-JSON stdout lines; filtering to keep ACP parsing alive. First dropped line: ${firstFilteredLine ?? "(unknown)"}`;
|
|
393
|
+
console.error(msg);
|
|
394
|
+
types.logger.debug(msg);
|
|
395
|
+
}
|
|
396
|
+
controller.close();
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
399
|
+
buffer += decoder.decode(value, { stream: true });
|
|
400
|
+
const lines = buffer.split("\n");
|
|
401
|
+
buffer = lines.pop() || "";
|
|
402
|
+
for (const line of lines) {
|
|
403
|
+
const trimmed = line.trim();
|
|
404
|
+
if (!trimmed) {
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
if (isValidJSON(trimmed)) {
|
|
408
|
+
controller.enqueue(encoder.encode(line + "\n"));
|
|
409
|
+
} else {
|
|
410
|
+
filteredCount++;
|
|
411
|
+
if (!firstFilteredLine) firstFilteredLine = trimmed.slice(0, 300);
|
|
412
|
+
if (!didEmitAuthError && (/authentication required/i.test(trimmed) || /login required/i.test(trimmed) || /invalid.*(api key|key)/i.test(trimmed) || /permission denied/i.test(trimmed))) {
|
|
413
|
+
didEmitAuthError = true;
|
|
414
|
+
const msg = `[AcpSdkBackend] Gemini CLI emitted non-JSON auth error on stdout (filtered for ACP): ${trimmed.slice(0, 500)}`;
|
|
415
|
+
console.error(msg);
|
|
416
|
+
types.logger.debug(msg);
|
|
417
|
+
try {
|
|
418
|
+
backend.emit({
|
|
419
|
+
type: "status",
|
|
420
|
+
status: "error",
|
|
421
|
+
detail: `Authentication required (gemini CLI stdout):
|
|
422
|
+
${trimmed.slice(0, 1500)}`
|
|
423
|
+
});
|
|
424
|
+
} catch (e) {
|
|
425
|
+
types.logger.debug("[AcpSdkBackend] Failed to emit auth error status from stdout filter:", safeStringify(e));
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (!didWarn) {
|
|
429
|
+
didWarn = true;
|
|
430
|
+
const msg = `[AcpSdkBackend] Gemini CLI emitted non-JSON stdout; filtering to keep ACP parsing alive. First dropped line: ${firstFilteredLine}`;
|
|
431
|
+
console.error(msg);
|
|
432
|
+
types.logger.debug(msg);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
} catch (error) {
|
|
438
|
+
console.error("[AcpSdkBackend] Error filtering stdout stream:", error);
|
|
439
|
+
types.logger.debug(`[AcpSdkBackend] Error filtering stdout stream:`, error);
|
|
440
|
+
controller.error(error);
|
|
441
|
+
} finally {
|
|
442
|
+
reader.releaseLock();
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
const stream = sdk.ndJsonStream(writable, filteredReadable);
|
|
447
|
+
const client = {
|
|
448
|
+
sessionUpdate: async (params) => {
|
|
449
|
+
this.handleSessionUpdate(params);
|
|
450
|
+
},
|
|
451
|
+
requestPermission: async (params) => {
|
|
452
|
+
const permissionId = node_crypto.randomUUID();
|
|
453
|
+
const extendedParams = params;
|
|
454
|
+
const toolCall = extendedParams.toolCall;
|
|
455
|
+
let toolName = toolCall?.kind || toolCall?.toolName || extendedParams.kind || "Unknown tool";
|
|
456
|
+
const toolCallId = toolCall?.id || permissionId;
|
|
457
|
+
let input = {};
|
|
458
|
+
if (toolCall) {
|
|
459
|
+
input = toolCall.input || toolCall.arguments || toolCall.content || {};
|
|
460
|
+
} else {
|
|
461
|
+
input = extendedParams.input || extendedParams.arguments || extendedParams.content || {};
|
|
462
|
+
}
|
|
463
|
+
toolName = determineToolName(
|
|
464
|
+
toolName,
|
|
465
|
+
toolCallId,
|
|
466
|
+
input,
|
|
467
|
+
params,
|
|
468
|
+
{
|
|
469
|
+
recentPromptHadChangeTitle: this.recentPromptHadChangeTitle,
|
|
470
|
+
toolCallCountSincePrompt: this.toolCallCountSincePrompt
|
|
471
|
+
}
|
|
472
|
+
);
|
|
473
|
+
if (toolName !== (toolCall?.kind || toolCall?.toolName || extendedParams.kind || "Unknown tool")) {
|
|
474
|
+
types.logger.debug(`[AcpSdkBackend] Detected tool name: ${toolName} from toolCallId: ${toolCallId}`);
|
|
475
|
+
}
|
|
476
|
+
this.toolCallCountSincePrompt++;
|
|
477
|
+
const options = extendedParams.options || [];
|
|
478
|
+
types.logger.debug(`[AcpSdkBackend] Permission request: tool=${toolName}, toolCallId=${toolCallId}, input=`, JSON.stringify(input));
|
|
479
|
+
types.logger.debug(`[AcpSdkBackend] Permission request params structure:`, JSON.stringify({
|
|
480
|
+
hasToolCall: !!toolCall,
|
|
481
|
+
toolCallKind: toolCall?.kind,
|
|
482
|
+
toolCallId: toolCall?.id,
|
|
483
|
+
paramsKind: extendedParams.kind,
|
|
484
|
+
paramsKeys: Object.keys(params)
|
|
485
|
+
}, null, 2));
|
|
486
|
+
this.emit({
|
|
487
|
+
type: "permission-request",
|
|
488
|
+
id: permissionId,
|
|
489
|
+
reason: toolName,
|
|
490
|
+
payload: {
|
|
491
|
+
...params,
|
|
492
|
+
permissionId,
|
|
493
|
+
toolCallId,
|
|
494
|
+
toolName,
|
|
495
|
+
input,
|
|
496
|
+
options: options.map((opt) => ({
|
|
497
|
+
id: opt.optionId,
|
|
498
|
+
name: opt.name,
|
|
499
|
+
kind: opt.kind
|
|
500
|
+
}))
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
if (this.options.permissionHandler) {
|
|
504
|
+
try {
|
|
505
|
+
const result = await this.options.permissionHandler.handleToolCall(
|
|
506
|
+
toolCallId,
|
|
507
|
+
toolName,
|
|
508
|
+
input
|
|
509
|
+
);
|
|
510
|
+
const denyReasonRaw = typeof result?.reason === "string" ? String(result.reason) : "";
|
|
511
|
+
const denyReason = denyReasonRaw.trim().length > 0 ? denyReasonRaw.trim() : null;
|
|
512
|
+
const optionIds2 = options.map((opt) => typeof opt.optionId === "string" ? opt.optionId.trim() : "").filter(Boolean);
|
|
513
|
+
const proceedOnceOptionId2 = options.find((opt) => opt.optionId === "proceed_once")?.optionId ?? null;
|
|
514
|
+
const proceedAlwaysOptionId2 = options.find((opt) => opt.optionId === "proceed_always")?.optionId ?? null;
|
|
515
|
+
const cancelOptionId2 = options.find((opt) => opt.optionId === "cancel")?.optionId ?? null;
|
|
516
|
+
const emergencyFirstOptionId2 = optionIds2[0] ?? null;
|
|
517
|
+
const reportMismatch = (message) => {
|
|
518
|
+
console.error(message);
|
|
519
|
+
types.logger.debug(message);
|
|
520
|
+
this.emit({ type: "status", status: "error", detail: message });
|
|
521
|
+
};
|
|
522
|
+
let optionId2 = cancelOptionId2;
|
|
523
|
+
if (result.decision === "approved" || result.decision === "approved_for_session") {
|
|
524
|
+
if (result.decision === "approved_for_session") {
|
|
525
|
+
if (proceedAlwaysOptionId2) {
|
|
526
|
+
optionId2 = proceedAlwaysOptionId2;
|
|
527
|
+
} else if (proceedOnceOptionId2) {
|
|
528
|
+
reportMismatch(
|
|
529
|
+
`[AcpSdkBackend] Permission request missing 'proceed_always' optionId (tool=${toolName}). Options: ${optionIds2.join(", ") || "(none)"}`
|
|
530
|
+
);
|
|
531
|
+
optionId2 = proceedOnceOptionId2;
|
|
532
|
+
} else {
|
|
533
|
+
reportMismatch(
|
|
534
|
+
`[AcpSdkBackend] Permission request missing proceed options (tool=${toolName}). Expected 'proceed_once'/'proceed_always'. Options: ${optionIds2.join(", ") || "(none)"}`
|
|
535
|
+
);
|
|
536
|
+
optionId2 = cancelOptionId2 ?? emergencyFirstOptionId2;
|
|
537
|
+
}
|
|
538
|
+
} else {
|
|
539
|
+
if (proceedOnceOptionId2) {
|
|
540
|
+
optionId2 = proceedOnceOptionId2;
|
|
541
|
+
} else if (proceedAlwaysOptionId2) {
|
|
542
|
+
reportMismatch(
|
|
543
|
+
`[AcpSdkBackend] Permission request missing 'proceed_once' optionId (tool=${toolName}). Options: ${optionIds2.join(", ") || "(none)"}`
|
|
544
|
+
);
|
|
545
|
+
optionId2 = proceedAlwaysOptionId2;
|
|
546
|
+
} else {
|
|
547
|
+
reportMismatch(
|
|
548
|
+
`[AcpSdkBackend] Permission request missing proceed options (tool=${toolName}). Expected 'proceed_once'/'proceed_always'. Options: ${optionIds2.join(", ") || "(none)"}`
|
|
549
|
+
);
|
|
550
|
+
optionId2 = cancelOptionId2 ?? emergencyFirstOptionId2;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
} else {
|
|
554
|
+
if (!cancelOptionId2) {
|
|
555
|
+
reportMismatch(
|
|
556
|
+
`[AcpSdkBackend] Permission request missing 'cancel' optionId (tool=${toolName}). Options: ${optionIds2.join(", ") || "(none)"}`
|
|
557
|
+
);
|
|
558
|
+
optionId2 = emergencyFirstOptionId2;
|
|
559
|
+
} else {
|
|
560
|
+
optionId2 = cancelOptionId2;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
if (!optionId2) {
|
|
564
|
+
throw new Error(
|
|
565
|
+
`[AcpSdkBackend] Permission request had no usable optionIds (tool=${toolName}). Options: ${optionIds2.join(", ") || "(none)"}`
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
const truncatedReason = denyReason && denyReason.length > 4096 ? `${denyReason.slice(0, 4096)}\u2026` : denyReason;
|
|
569
|
+
const meta = result.decision === "denied" || result.decision === "abort" ? truncatedReason ? { denialReason: truncatedReason, toolName, toolCallId } : { toolName, toolCallId } : null;
|
|
570
|
+
return {
|
|
571
|
+
_meta: meta,
|
|
572
|
+
outcome: {
|
|
573
|
+
outcome: "selected",
|
|
574
|
+
optionId: optionId2,
|
|
575
|
+
_meta: meta
|
|
576
|
+
}
|
|
577
|
+
};
|
|
578
|
+
} catch (error) {
|
|
579
|
+
console.error("[AcpSdkBackend] Error in permission handler:", error);
|
|
580
|
+
types.logger.debug("[AcpSdkBackend] Error in permission handler:", error);
|
|
581
|
+
const optionIds2 = options.map((opt) => typeof opt.optionId === "string" ? opt.optionId.trim() : "").filter(Boolean);
|
|
582
|
+
const cancelOptionId2 = options.find((opt) => opt.optionId === "cancel")?.optionId ?? null;
|
|
583
|
+
const emergencyFirstOptionId2 = optionIds2[0] ?? null;
|
|
584
|
+
const optionId2 = cancelOptionId2 ?? emergencyFirstOptionId2;
|
|
585
|
+
if (!optionId2) throw error;
|
|
586
|
+
this.emit({
|
|
587
|
+
type: "status",
|
|
588
|
+
status: "error",
|
|
589
|
+
detail: error instanceof Error ? `Permission handler error: ${error.message}` : "Permission handler error"
|
|
590
|
+
});
|
|
591
|
+
const meta = {
|
|
592
|
+
denialReason: error instanceof Error ? `Permission handler error: ${error.message}` : "Permission handler error",
|
|
593
|
+
toolName,
|
|
594
|
+
toolCallId
|
|
595
|
+
};
|
|
596
|
+
return { _meta: meta, outcome: { outcome: "selected", optionId: optionId2, _meta: meta } };
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
const optionIds = options.map((opt) => typeof opt.optionId === "string" ? opt.optionId.trim() : "").filter(Boolean);
|
|
600
|
+
const proceedOnceOptionId = options.find((opt) => opt.optionId === "proceed_once")?.optionId ?? null;
|
|
601
|
+
const proceedAlwaysOptionId = options.find((opt) => opt.optionId === "proceed_always")?.optionId ?? null;
|
|
602
|
+
const cancelOptionId = options.find((opt) => opt.optionId === "cancel")?.optionId ?? null;
|
|
603
|
+
const emergencyFirstOptionId = optionIds[0] ?? null;
|
|
604
|
+
if (proceedOnceOptionId) return { outcome: { outcome: "selected", optionId: proceedOnceOptionId } };
|
|
605
|
+
if (proceedAlwaysOptionId) {
|
|
606
|
+
const msg2 = `[AcpSdkBackend] Permission request missing 'proceed_once' optionId (tool=${toolName}); auto-approving with 'proceed_always'. Options: ${optionIds.join(", ") || "(none)"}`;
|
|
607
|
+
console.error(msg2);
|
|
608
|
+
types.logger.debug(msg2);
|
|
609
|
+
return { outcome: { outcome: "selected", optionId: proceedAlwaysOptionId } };
|
|
610
|
+
}
|
|
611
|
+
const msg = `[AcpSdkBackend] Permission request missing proceed options (tool=${toolName}); denying. Expected 'proceed_once'/'proceed_always'. Options: ${optionIds.join(", ") || "(none)"}`;
|
|
612
|
+
console.error(msg);
|
|
613
|
+
types.logger.debug(msg);
|
|
614
|
+
const optionId = cancelOptionId ?? emergencyFirstOptionId;
|
|
615
|
+
if (!optionId) throw new Error(msg);
|
|
616
|
+
return { outcome: { outcome: "selected", optionId } };
|
|
617
|
+
}
|
|
618
|
+
};
|
|
619
|
+
this.connection = new sdk.ClientSideConnection(
|
|
620
|
+
(agent) => client,
|
|
621
|
+
stream
|
|
622
|
+
);
|
|
623
|
+
const initRequest = {
|
|
624
|
+
protocolVersion: 1,
|
|
625
|
+
clientCapabilities: {
|
|
626
|
+
fs: {
|
|
627
|
+
readTextFile: false,
|
|
628
|
+
writeTextFile: false
|
|
629
|
+
}
|
|
630
|
+
},
|
|
631
|
+
clientInfo: {
|
|
632
|
+
name: "flockbay-cli",
|
|
633
|
+
version: types.packageJson.version
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
types.logger.debug(`[AcpSdkBackend] Initializing connection...`);
|
|
637
|
+
let initTimeout = null;
|
|
638
|
+
const initResponse = await Promise.race([
|
|
639
|
+
this.connection.initialize(initRequest).then((result) => {
|
|
640
|
+
if (initTimeout) {
|
|
641
|
+
clearTimeout(initTimeout);
|
|
642
|
+
initTimeout = null;
|
|
643
|
+
}
|
|
644
|
+
return result;
|
|
645
|
+
}),
|
|
646
|
+
new Promise((_, reject) => {
|
|
647
|
+
initTimeout = setTimeout(() => {
|
|
648
|
+
types.logger.debug(`[AcpSdkBackend] Initialize timeout after ${ACP_INIT_TIMEOUT_MS}ms`);
|
|
649
|
+
reject(new Error(`Initialize timeout after ${ACP_INIT_TIMEOUT_MS}ms - Gemini CLI did not respond`));
|
|
650
|
+
}, ACP_INIT_TIMEOUT_MS);
|
|
651
|
+
})
|
|
652
|
+
]);
|
|
653
|
+
types.logger.debug(`[AcpSdkBackend] Initialize completed`);
|
|
654
|
+
const mcpServers = this.options.mcpServers ? Object.entries(this.options.mcpServers).map(([name, config]) => ({
|
|
655
|
+
name,
|
|
656
|
+
command: config.command,
|
|
657
|
+
args: config.args || [],
|
|
658
|
+
env: config.env ? Object.entries(config.env).map(([envName, envValue]) => ({ name: envName, value: envValue })) : []
|
|
659
|
+
})) : [];
|
|
660
|
+
const newSessionRequest = {
|
|
661
|
+
cwd: this.options.cwd,
|
|
662
|
+
mcpServers
|
|
663
|
+
};
|
|
664
|
+
types.logger.debug(`[AcpSdkBackend] Creating new session...`);
|
|
665
|
+
let newSessionTimeout = null;
|
|
666
|
+
const sessionResponse = await Promise.race([
|
|
667
|
+
this.connection.newSession(newSessionRequest).then((result) => {
|
|
668
|
+
if (newSessionTimeout) {
|
|
669
|
+
clearTimeout(newSessionTimeout);
|
|
670
|
+
newSessionTimeout = null;
|
|
671
|
+
}
|
|
672
|
+
return result;
|
|
673
|
+
}),
|
|
674
|
+
new Promise((_, reject) => {
|
|
675
|
+
newSessionTimeout = setTimeout(() => {
|
|
676
|
+
types.logger.debug(`[AcpSdkBackend] NewSession timeout after ${ACP_INIT_TIMEOUT_MS}ms`);
|
|
677
|
+
reject(new Error("New session timeout"));
|
|
678
|
+
}, ACP_INIT_TIMEOUT_MS);
|
|
679
|
+
})
|
|
680
|
+
]);
|
|
681
|
+
this.acpSessionId = sessionResponse.sessionId;
|
|
682
|
+
types.logger.debug(`[AcpSdkBackend] Session created: ${this.acpSessionId}`);
|
|
683
|
+
this.emit({ type: "status", status: "idle" });
|
|
684
|
+
if (initialPrompt) {
|
|
685
|
+
this.sendPrompt(sessionId, initialPrompt).catch((error) => {
|
|
686
|
+
console.error("[AcpSdkBackend] Error sending initial prompt:", error);
|
|
687
|
+
types.logger.debug("[AcpSdkBackend] Error sending initial prompt:", error);
|
|
688
|
+
this.emit({ type: "status", status: "error", detail: String(error) });
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
return { sessionId };
|
|
692
|
+
} catch (error) {
|
|
693
|
+
console.error("[AcpSdkBackend] Error starting session:", error);
|
|
694
|
+
types.logger.debug("[AcpSdkBackend] Error starting session:", error);
|
|
695
|
+
const excerpt = this.stderrExcerpt(20);
|
|
696
|
+
const base = error instanceof Error ? error.message : String(error);
|
|
697
|
+
const detail = excerpt ? `${base}
|
|
698
|
+
|
|
699
|
+
Agent stderr:
|
|
700
|
+
${excerpt}` : base;
|
|
701
|
+
this.emit({
|
|
702
|
+
type: "status",
|
|
703
|
+
status: "error",
|
|
704
|
+
detail
|
|
705
|
+
});
|
|
706
|
+
throw error;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
handleSessionUpdate(params) {
|
|
710
|
+
const notification = params;
|
|
711
|
+
const update = notification.update;
|
|
712
|
+
if (!update) {
|
|
713
|
+
types.logger.debug("[AcpSdkBackend] Received session update without update field:", params);
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
const sessionUpdateType = update.sessionUpdate;
|
|
717
|
+
if (sessionUpdateType !== "agent_message_chunk") {
|
|
718
|
+
types.logger.debug(`[AcpSdkBackend] Received session update: ${sessionUpdateType}`, JSON.stringify({
|
|
719
|
+
sessionUpdate: sessionUpdateType,
|
|
720
|
+
toolCallId: update.toolCallId,
|
|
721
|
+
status: update.status,
|
|
722
|
+
kind: update.kind,
|
|
723
|
+
hasContent: !!update.content,
|
|
724
|
+
hasLocations: !!update.locations
|
|
725
|
+
}, null, 2));
|
|
726
|
+
}
|
|
727
|
+
if (sessionUpdateType === "agent_message_chunk") {
|
|
728
|
+
const content = update.content;
|
|
729
|
+
if (content && typeof content === "object" && "text" in content && typeof content.text === "string") {
|
|
730
|
+
const text = content.text;
|
|
731
|
+
const isThinking = /^\*\*[^*]+\*\*\n/.test(text);
|
|
732
|
+
if (isThinking) {
|
|
733
|
+
this.emit({
|
|
734
|
+
type: "event",
|
|
735
|
+
name: "thinking",
|
|
736
|
+
payload: { text }
|
|
737
|
+
});
|
|
738
|
+
} else {
|
|
739
|
+
types.logger.debug(`[AcpSdkBackend] Received message chunk (length: ${text.length}): ${text.substring(0, 50)}...`);
|
|
740
|
+
this.emit({
|
|
741
|
+
type: "model-output",
|
|
742
|
+
textDelta: text
|
|
743
|
+
});
|
|
744
|
+
if (this.idleTimeout) {
|
|
745
|
+
clearTimeout(this.idleTimeout);
|
|
746
|
+
this.idleTimeout = null;
|
|
747
|
+
}
|
|
748
|
+
this.idleTimeout = setTimeout(() => {
|
|
749
|
+
if (this.activeToolCalls.size === 0) {
|
|
750
|
+
types.logger.debug("[AcpSdkBackend] No more chunks received, emitting idle status");
|
|
751
|
+
this.emit({ type: "status", status: "idle" });
|
|
752
|
+
} else {
|
|
753
|
+
types.logger.debug(`[AcpSdkBackend] Delaying idle status - ${this.activeToolCalls.size} active tool calls`);
|
|
754
|
+
}
|
|
755
|
+
this.idleTimeout = null;
|
|
756
|
+
}, 500);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
if (sessionUpdateType === "tool_call_update") {
|
|
761
|
+
const status = update.status;
|
|
762
|
+
const toolCallId = update.toolCallId;
|
|
763
|
+
if (!toolCallId) {
|
|
764
|
+
types.logger.debug("[AcpSdkBackend] Tool call update without toolCallId:", update);
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
if (status === "in_progress" || status === "pending") {
|
|
768
|
+
if (!this.activeToolCalls.has(toolCallId)) {
|
|
769
|
+
const startTime = Date.now();
|
|
770
|
+
const toolKind = update.kind || "unknown";
|
|
771
|
+
const isInvestigation = isInvestigationTool(toolCallId, toolKind);
|
|
772
|
+
const realToolName = getRealToolName(toolCallId, toolKind);
|
|
773
|
+
this.toolCallIdToNameMap.set(toolCallId, realToolName);
|
|
774
|
+
this.activeToolCalls.add(toolCallId);
|
|
775
|
+
this.toolCallStartTimes.set(toolCallId, startTime);
|
|
776
|
+
types.logger.debug(`[AcpSdkBackend] \u23F1\uFE0F Set startTime for ${toolCallId} at ${new Date(startTime).toISOString()} (from tool_call_update)`);
|
|
777
|
+
this.toolCallCountSincePrompt++;
|
|
778
|
+
types.logger.debug(`[AcpSdkBackend] \u{1F527} Tool call START: ${toolCallId} (${toolKind} -> ${realToolName})${isInvestigation ? " [INVESTIGATION TOOL]" : ""}`);
|
|
779
|
+
if (isInvestigation) {
|
|
780
|
+
types.logger.debug(`[AcpSdkBackend] \u{1F50D} Investigation tool detected (by toolCallId) - extended timeout (10min) will be used`);
|
|
781
|
+
}
|
|
782
|
+
const timeoutMs = getToolCallTimeout(toolCallId, toolKind);
|
|
783
|
+
if (!this.toolCallTimeouts.has(toolCallId)) {
|
|
784
|
+
const timeout = setTimeout(() => {
|
|
785
|
+
const startTime2 = this.toolCallStartTimes.get(toolCallId);
|
|
786
|
+
const duration = startTime2 ? Date.now() - startTime2 : null;
|
|
787
|
+
const durationStr = duration ? `${(duration / 1e3).toFixed(2)}s` : "unknown";
|
|
788
|
+
types.logger.debug(`[AcpSdkBackend] \u23F1\uFE0F Tool call TIMEOUT (from tool_call_update): ${toolCallId} (${toolKind}) after ${(timeoutMs / 1e3).toFixed(0)}s - Duration: ${durationStr}, removing from active set`);
|
|
789
|
+
this.activeToolCalls.delete(toolCallId);
|
|
790
|
+
this.toolCallStartTimes.delete(toolCallId);
|
|
791
|
+
this.toolCallTimeouts.delete(toolCallId);
|
|
792
|
+
if (this.activeToolCalls.size === 0) {
|
|
793
|
+
types.logger.debug("[AcpSdkBackend] No more active tool calls after timeout, emitting idle status");
|
|
794
|
+
this.emit({ type: "status", status: "idle" });
|
|
795
|
+
}
|
|
796
|
+
}, timeoutMs);
|
|
797
|
+
this.toolCallTimeouts.set(toolCallId, timeout);
|
|
798
|
+
types.logger.debug(`[AcpSdkBackend] \u23F1\uFE0F Set timeout for ${toolCallId}: ${(timeoutMs / 1e3).toFixed(0)}s${isInvestigation ? " (investigation tool)" : ""}`);
|
|
799
|
+
} else {
|
|
800
|
+
types.logger.debug(`[AcpSdkBackend] Timeout already set for ${toolCallId}, skipping`);
|
|
801
|
+
}
|
|
802
|
+
if (this.idleTimeout) {
|
|
803
|
+
clearTimeout(this.idleTimeout);
|
|
804
|
+
this.idleTimeout = null;
|
|
805
|
+
}
|
|
806
|
+
this.emit({ type: "status", status: "running" });
|
|
807
|
+
let args = {};
|
|
808
|
+
if (Array.isArray(update.content)) {
|
|
809
|
+
args = { items: update.content };
|
|
810
|
+
} else if (update.content && typeof update.content === "object" && update.content !== null) {
|
|
811
|
+
args = update.content;
|
|
812
|
+
}
|
|
813
|
+
if (isInvestigation && args.objective) {
|
|
814
|
+
types.logger.debug(`[AcpSdkBackend] \u{1F50D} Investigation tool objective: ${String(args.objective).substring(0, 100)}...`);
|
|
815
|
+
}
|
|
816
|
+
this.emit({
|
|
817
|
+
type: "tool-call",
|
|
818
|
+
toolName: typeof toolKind === "string" ? toolKind : "unknown",
|
|
819
|
+
args,
|
|
820
|
+
callId: toolCallId
|
|
821
|
+
});
|
|
822
|
+
} else {
|
|
823
|
+
types.logger.debug(`[AcpSdkBackend] Tool call ${toolCallId} already tracked, status: ${status}`);
|
|
824
|
+
}
|
|
825
|
+
} else if (status === "completed") {
|
|
826
|
+
const startTime = this.toolCallStartTimes.get(toolCallId);
|
|
827
|
+
const duration = startTime ? Date.now() - startTime : null;
|
|
828
|
+
const toolKind = update.kind || "unknown";
|
|
829
|
+
this.activeToolCalls.delete(toolCallId);
|
|
830
|
+
this.toolCallStartTimes.delete(toolCallId);
|
|
831
|
+
const timeout = this.toolCallTimeouts.get(toolCallId);
|
|
832
|
+
if (timeout) {
|
|
833
|
+
clearTimeout(timeout);
|
|
834
|
+
this.toolCallTimeouts.delete(toolCallId);
|
|
835
|
+
}
|
|
836
|
+
const durationStr = duration ? `${(duration / 1e3).toFixed(2)}s` : "unknown";
|
|
837
|
+
types.logger.debug(`[AcpSdkBackend] \u2705 Tool call COMPLETED: ${toolCallId} (${toolKind}) - Duration: ${durationStr}. Active tool calls: ${this.activeToolCalls.size}`);
|
|
838
|
+
const normalizeCompletedResult = (raw) => {
|
|
839
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return raw;
|
|
840
|
+
const obj = raw;
|
|
841
|
+
if (!("error" in obj) || obj.error == null) return raw;
|
|
842
|
+
const err = obj.error;
|
|
843
|
+
const errStr = typeof err === "string" ? err : err && typeof err === "object" && "message" in err && typeof err.message === "string" ? String(err.message) : (() => {
|
|
844
|
+
try {
|
|
845
|
+
return safeStringify(err);
|
|
846
|
+
} catch {
|
|
847
|
+
return String(err);
|
|
848
|
+
}
|
|
849
|
+
})();
|
|
850
|
+
return { ...obj, error: errStr };
|
|
851
|
+
};
|
|
852
|
+
const normalizedResult = normalizeCompletedResult(update.content);
|
|
853
|
+
this.emit({
|
|
854
|
+
type: "tool-result",
|
|
855
|
+
toolName: typeof toolKind === "string" ? toolKind : "unknown",
|
|
856
|
+
result: normalizedResult,
|
|
857
|
+
callId: toolCallId
|
|
858
|
+
});
|
|
859
|
+
if (this.activeToolCalls.size === 0) {
|
|
860
|
+
if (this.idleTimeout) {
|
|
861
|
+
clearTimeout(this.idleTimeout);
|
|
862
|
+
this.idleTimeout = null;
|
|
863
|
+
}
|
|
864
|
+
types.logger.debug("[AcpSdkBackend] All tool calls completed, emitting idle status");
|
|
865
|
+
this.emit({ type: "status", status: "idle" });
|
|
866
|
+
}
|
|
867
|
+
} else if (status === "failed" || status === "cancelled") {
|
|
868
|
+
const startTime = this.toolCallStartTimes.get(toolCallId);
|
|
869
|
+
const duration = startTime ? Date.now() - startTime : null;
|
|
870
|
+
const toolKind = update.kind || "unknown";
|
|
871
|
+
const isInvestigation = isInvestigationTool(toolCallId, toolKind);
|
|
872
|
+
const hadTimeout = this.toolCallTimeouts.has(toolCallId);
|
|
873
|
+
if (isInvestigation) {
|
|
874
|
+
const durationStr2 = duration ? `${(duration / 1e3).toFixed(2)}s` : "unknown";
|
|
875
|
+
const durationMinutes = duration ? (duration / 1e3 / 60).toFixed(2) : "unknown";
|
|
876
|
+
types.logger.debug(`[AcpSdkBackend] \u{1F50D} Investigation tool ${status.toUpperCase()} after ${durationMinutes} minutes (${durationStr2})`);
|
|
877
|
+
if (duration) {
|
|
878
|
+
const threeMinutes = 3 * 60 * 1e3;
|
|
879
|
+
const tolerance = 5e3;
|
|
880
|
+
if (Math.abs(duration - threeMinutes) < tolerance) {
|
|
881
|
+
types.logger.debug(`[AcpSdkBackend] \u{1F50D} \u26A0\uFE0F Investigation tool failed at ~3 minutes - likely Gemini CLI timeout, not our timeout`);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
types.logger.debug(`[AcpSdkBackend] \u{1F50D} Investigation tool FAILED - full update.content:`, JSON.stringify(update.content, null, 2));
|
|
885
|
+
types.logger.debug(`[AcpSdkBackend] \u{1F50D} Investigation tool timeout status BEFORE cleanup: ${hadTimeout ? "timeout was set" : "no timeout was set"}`);
|
|
886
|
+
types.logger.debug(`[AcpSdkBackend] \u{1F50D} Investigation tool startTime status BEFORE cleanup: ${startTime ? `set at ${new Date(startTime).toISOString()}` : "not set"}`);
|
|
887
|
+
}
|
|
888
|
+
this.activeToolCalls.delete(toolCallId);
|
|
889
|
+
this.toolCallStartTimes.delete(toolCallId);
|
|
890
|
+
const timeout = this.toolCallTimeouts.get(toolCallId);
|
|
891
|
+
if (timeout) {
|
|
892
|
+
clearTimeout(timeout);
|
|
893
|
+
this.toolCallTimeouts.delete(toolCallId);
|
|
894
|
+
types.logger.debug(`[AcpSdkBackend] Cleared timeout for ${toolCallId} (tool call ${status})`);
|
|
895
|
+
} else {
|
|
896
|
+
types.logger.debug(`[AcpSdkBackend] No timeout found for ${toolCallId} (tool call ${status}) - timeout may not have been set`);
|
|
897
|
+
}
|
|
898
|
+
const durationStr = duration ? `${(duration / 1e3).toFixed(2)}s` : "unknown";
|
|
899
|
+
types.logger.debug(`[AcpSdkBackend] \u274C Tool call ${status.toUpperCase()}: ${toolCallId} (${toolKind}) - Duration: ${durationStr}. Active tool calls: ${this.activeToolCalls.size}`);
|
|
900
|
+
let errorDetail;
|
|
901
|
+
if (update.content) {
|
|
902
|
+
if (typeof update.content === "string") {
|
|
903
|
+
errorDetail = update.content;
|
|
904
|
+
} else if (typeof update.content === "object" && update.content !== null && !Array.isArray(update.content)) {
|
|
905
|
+
const content = update.content;
|
|
906
|
+
if (content.error) {
|
|
907
|
+
const error = content.error;
|
|
908
|
+
errorDetail = typeof error === "string" ? error : error && typeof error === "object" && "message" in error && typeof error.message === "string" ? error.message : JSON.stringify(error);
|
|
909
|
+
} else if (typeof content.message === "string") {
|
|
910
|
+
errorDetail = content.message;
|
|
911
|
+
} else {
|
|
912
|
+
const status2 = typeof content.status === "string" ? content.status : void 0;
|
|
913
|
+
const reason = typeof content.reason === "string" ? content.reason : void 0;
|
|
914
|
+
errorDetail = status2 || reason || JSON.stringify(content).substring(0, 500);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
if (errorDetail) {
|
|
919
|
+
types.logger.debug(`[AcpSdkBackend] \u274C Tool call error details: ${errorDetail.substring(0, 500)}`);
|
|
920
|
+
} else {
|
|
921
|
+
types.logger.debug(`[AcpSdkBackend] \u274C Tool call ${status} but no error details in update.content`);
|
|
922
|
+
}
|
|
923
|
+
this.emit({
|
|
924
|
+
type: "tool-result",
|
|
925
|
+
toolName: typeof toolKind === "string" ? toolKind : "unknown",
|
|
926
|
+
result: errorDetail ? { error: errorDetail, status } : { error: `Tool call ${status}`, status },
|
|
927
|
+
callId: toolCallId
|
|
928
|
+
});
|
|
929
|
+
if (this.activeToolCalls.size === 0) {
|
|
930
|
+
if (this.idleTimeout) {
|
|
931
|
+
clearTimeout(this.idleTimeout);
|
|
932
|
+
this.idleTimeout = null;
|
|
933
|
+
}
|
|
934
|
+
types.logger.debug("[AcpSdkBackend] All tool calls completed/failed, emitting idle status");
|
|
935
|
+
this.emit({ type: "status", status: "idle" });
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
if (update.messageChunk) {
|
|
940
|
+
const chunk = update.messageChunk;
|
|
941
|
+
if (chunk.textDelta) {
|
|
942
|
+
this.emit({
|
|
943
|
+
type: "model-output",
|
|
944
|
+
textDelta: chunk.textDelta
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
if (update.plan) {
|
|
949
|
+
this.emit({
|
|
950
|
+
type: "event",
|
|
951
|
+
name: "plan",
|
|
952
|
+
payload: update.plan
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
if (sessionUpdateType === "agent_thought_chunk") {
|
|
956
|
+
const content = update.content;
|
|
957
|
+
if (content && typeof content === "object" && "text" in content && typeof content.text === "string") {
|
|
958
|
+
const text = content.text;
|
|
959
|
+
const hasActiveInvestigation = Array.from(this.activeToolCalls).some(() => {
|
|
960
|
+
return true;
|
|
961
|
+
});
|
|
962
|
+
if (hasActiveInvestigation && this.activeToolCalls.size > 0) {
|
|
963
|
+
const activeToolCallsList = Array.from(this.activeToolCalls);
|
|
964
|
+
types.logger.debug(`[AcpSdkBackend] \u{1F4AD} Thinking chunk received (${text.length} chars) during active tool calls: ${activeToolCallsList.join(", ")}`);
|
|
965
|
+
}
|
|
966
|
+
this.emit({
|
|
967
|
+
type: "event",
|
|
968
|
+
name: "thinking",
|
|
969
|
+
payload: { text }
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
if (sessionUpdateType === "tool_call") {
|
|
974
|
+
const toolCallId = update.toolCallId;
|
|
975
|
+
const status = update.status;
|
|
976
|
+
types.logger.debug(`[AcpSdkBackend] Received tool_call: toolCallId=${toolCallId}, status=${status}, kind=${update.kind}`);
|
|
977
|
+
const isInProgress = !status || status === "in_progress" || status === "pending";
|
|
978
|
+
if (toolCallId && isInProgress) {
|
|
979
|
+
if (!this.activeToolCalls.has(toolCallId)) {
|
|
980
|
+
const startTime = Date.now();
|
|
981
|
+
this.activeToolCalls.add(toolCallId);
|
|
982
|
+
this.toolCallStartTimes.set(toolCallId, startTime);
|
|
983
|
+
types.logger.debug(`[AcpSdkBackend] Added tool call ${toolCallId} to active set. Total active: ${this.activeToolCalls.size}`);
|
|
984
|
+
types.logger.debug(`[AcpSdkBackend] \u23F1\uFE0F Set startTime for ${toolCallId} at ${new Date(startTime).toISOString()}`);
|
|
985
|
+
if (this.idleTimeout) {
|
|
986
|
+
clearTimeout(this.idleTimeout);
|
|
987
|
+
this.idleTimeout = null;
|
|
988
|
+
}
|
|
989
|
+
const isInvestigation = isInvestigationTool(toolCallId, update.kind);
|
|
990
|
+
if (isInvestigation) {
|
|
991
|
+
types.logger.debug(`[AcpSdkBackend] \u{1F50D} Investigation tool detected (toolCallId: ${toolCallId}, kind: ${update.kind}) - using extended timeout (10min)`);
|
|
992
|
+
}
|
|
993
|
+
const timeoutMs = getToolCallTimeout(toolCallId, update.kind);
|
|
994
|
+
if (!this.toolCallTimeouts.has(toolCallId)) {
|
|
995
|
+
const timeout = setTimeout(() => {
|
|
996
|
+
const startTime2 = this.toolCallStartTimes.get(toolCallId);
|
|
997
|
+
const duration = startTime2 ? Date.now() - startTime2 : null;
|
|
998
|
+
const durationStr = duration ? `${(duration / 1e3).toFixed(2)}s` : "unknown";
|
|
999
|
+
types.logger.debug(`[AcpSdkBackend] \u23F1\uFE0F Tool call TIMEOUT (from tool_call): ${toolCallId} (${update.kind}) after ${(timeoutMs / 1e3).toFixed(0)}s - Duration: ${durationStr}, removing from active set`);
|
|
1000
|
+
this.activeToolCalls.delete(toolCallId);
|
|
1001
|
+
this.toolCallStartTimes.delete(toolCallId);
|
|
1002
|
+
this.toolCallTimeouts.delete(toolCallId);
|
|
1003
|
+
if (this.activeToolCalls.size === 0) {
|
|
1004
|
+
types.logger.debug("[AcpSdkBackend] No more active tool calls after timeout, emitting idle status");
|
|
1005
|
+
this.emit({ type: "status", status: "idle" });
|
|
1006
|
+
}
|
|
1007
|
+
}, timeoutMs);
|
|
1008
|
+
this.toolCallTimeouts.set(toolCallId, timeout);
|
|
1009
|
+
types.logger.debug(`[AcpSdkBackend] \u23F1\uFE0F Set timeout for ${toolCallId}: ${(timeoutMs / 1e3).toFixed(0)}s${isInvestigation ? " (investigation tool)" : ""}`);
|
|
1010
|
+
} else {
|
|
1011
|
+
types.logger.debug(`[AcpSdkBackend] Timeout already set for ${toolCallId}, skipping`);
|
|
1012
|
+
}
|
|
1013
|
+
this.emit({ type: "status", status: "running" });
|
|
1014
|
+
let args = {};
|
|
1015
|
+
if (Array.isArray(update.content)) {
|
|
1016
|
+
args = { items: update.content };
|
|
1017
|
+
} else if (update.content && typeof update.content === "object") {
|
|
1018
|
+
args = update.content;
|
|
1019
|
+
}
|
|
1020
|
+
if (update.locations && Array.isArray(update.locations)) {
|
|
1021
|
+
args.locations = update.locations;
|
|
1022
|
+
}
|
|
1023
|
+
types.logger.debug(`[AcpSdkBackend] Emitting tool-call event: toolName=${update.kind}, toolCallId=${toolCallId}, args=`, JSON.stringify(args));
|
|
1024
|
+
this.emit({
|
|
1025
|
+
type: "tool-call",
|
|
1026
|
+
toolName: update.kind || "unknown",
|
|
1027
|
+
args,
|
|
1028
|
+
callId: toolCallId
|
|
1029
|
+
});
|
|
1030
|
+
} else {
|
|
1031
|
+
types.logger.debug(`[AcpSdkBackend] Tool call ${toolCallId} already in active set, skipping`);
|
|
1032
|
+
}
|
|
1033
|
+
} else {
|
|
1034
|
+
types.logger.debug(`[AcpSdkBackend] Tool call ${toolCallId} not in progress (status: ${status}), skipping`);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
if (update.thinking) {
|
|
1038
|
+
this.emit({
|
|
1039
|
+
type: "event",
|
|
1040
|
+
name: "thinking",
|
|
1041
|
+
payload: update.thinking
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
if (sessionUpdateType && sessionUpdateType !== "agent_message_chunk" && sessionUpdateType !== "tool_call_update" && sessionUpdateType !== "agent_thought_chunk" && sessionUpdateType !== "tool_call" && !update.messageChunk && !update.plan && !update.thinking) {
|
|
1045
|
+
types.logger.debug(`[AcpSdkBackend] Unhandled session update type: ${sessionUpdateType}`, JSON.stringify(update, null, 2));
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
async sendPrompt(sessionId, prompt) {
|
|
1049
|
+
const promptHasChangeTitle = typeof prompt === "string" ? hasChangeTitleInstruction(prompt) : false;
|
|
1050
|
+
this.toolCallCountSincePrompt = 0;
|
|
1051
|
+
this.recentPromptHadChangeTitle = promptHasChangeTitle;
|
|
1052
|
+
if (promptHasChangeTitle) {
|
|
1053
|
+
types.logger.debug('[AcpSdkBackend] Prompt contains change_title instruction - will auto-approve first "other" tool call if it matches pattern');
|
|
1054
|
+
}
|
|
1055
|
+
if (this.disposed) {
|
|
1056
|
+
throw new Error("Backend has been disposed");
|
|
1057
|
+
}
|
|
1058
|
+
if (!this.connection || !this.acpSessionId) {
|
|
1059
|
+
throw new Error("Session not started");
|
|
1060
|
+
}
|
|
1061
|
+
this.emit({ type: "status", status: "running" });
|
|
1062
|
+
try {
|
|
1063
|
+
if (typeof prompt === "string") {
|
|
1064
|
+
types.logger.debug(`[AcpSdkBackend] Sending prompt (length: ${prompt.length}): ${prompt.substring(0, 100)}...`);
|
|
1065
|
+
types.logger.debug(`[AcpSdkBackend] Full prompt: ${prompt}`);
|
|
1066
|
+
} else {
|
|
1067
|
+
types.logger.debug(`[AcpSdkBackend] Sending prompt blocks (count: ${prompt.length})`);
|
|
1068
|
+
}
|
|
1069
|
+
const promptRequest = {
|
|
1070
|
+
sessionId: this.acpSessionId,
|
|
1071
|
+
prompt: typeof prompt === "string" ? [{ type: "text", text: prompt }] : prompt
|
|
1072
|
+
};
|
|
1073
|
+
types.logger.debug(`[AcpSdkBackend] Prompt request:`, JSON.stringify(promptRequest, null, 2));
|
|
1074
|
+
await this.connection.prompt(promptRequest);
|
|
1075
|
+
types.logger.debug("[AcpSdkBackend] Prompt request sent to ACP connection");
|
|
1076
|
+
} catch (error) {
|
|
1077
|
+
types.logger.debug("[AcpSdkBackend] Error sending prompt:", error);
|
|
1078
|
+
let errorDetail;
|
|
1079
|
+
if (error instanceof Error) {
|
|
1080
|
+
errorDetail = error.message;
|
|
1081
|
+
} else if (typeof error === "object" && error !== null) {
|
|
1082
|
+
const errObj = error;
|
|
1083
|
+
const fallbackMessage = (typeof errObj.message === "string" ? errObj.message : void 0) || String(error);
|
|
1084
|
+
if (errObj.code !== void 0) {
|
|
1085
|
+
errorDetail = JSON.stringify({ code: errObj.code, message: fallbackMessage });
|
|
1086
|
+
} else if (typeof errObj.message === "string") {
|
|
1087
|
+
errorDetail = errObj.message;
|
|
1088
|
+
} else {
|
|
1089
|
+
errorDetail = String(error);
|
|
1090
|
+
}
|
|
1091
|
+
} else {
|
|
1092
|
+
errorDetail = String(error);
|
|
1093
|
+
}
|
|
1094
|
+
this.emit({
|
|
1095
|
+
type: "status",
|
|
1096
|
+
status: "error",
|
|
1097
|
+
detail: errorDetail
|
|
1098
|
+
});
|
|
1099
|
+
throw error;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
async cancel(sessionId) {
|
|
1103
|
+
if (!this.connection || !this.acpSessionId) {
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
try {
|
|
1107
|
+
await this.connection.cancel({ sessionId: this.acpSessionId });
|
|
1108
|
+
this.emit({ type: "status", status: "stopped", detail: "Cancelled by user" });
|
|
1109
|
+
} catch (error) {
|
|
1110
|
+
console.error("[AcpSdkBackend] Error cancelling:", error);
|
|
1111
|
+
types.logger.debug("[AcpSdkBackend] Error cancelling:", error);
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
async respondToPermission(requestId, approved) {
|
|
1115
|
+
types.logger.debug(`[AcpSdkBackend] Permission response: ${requestId} = ${approved}`);
|
|
1116
|
+
this.emit({ type: "permission-response", id: requestId, approved });
|
|
1117
|
+
}
|
|
1118
|
+
async dispose() {
|
|
1119
|
+
if (this.disposed) return;
|
|
1120
|
+
types.logger.debug("[AcpSdkBackend] Disposing backend");
|
|
1121
|
+
this.disposed = true;
|
|
1122
|
+
if (this.connection && this.acpSessionId) {
|
|
1123
|
+
try {
|
|
1124
|
+
await Promise.race([
|
|
1125
|
+
this.connection.cancel({ sessionId: this.acpSessionId }),
|
|
1126
|
+
new Promise((resolve) => setTimeout(resolve, 2e3))
|
|
1127
|
+
// 2s timeout for graceful shutdown
|
|
1128
|
+
]);
|
|
1129
|
+
} catch (error) {
|
|
1130
|
+
types.logger.debug("[AcpSdkBackend] Error during graceful shutdown:", error);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
if (this.process) {
|
|
1134
|
+
this.process.kill("SIGTERM");
|
|
1135
|
+
await new Promise((resolve) => {
|
|
1136
|
+
const timeout = setTimeout(() => {
|
|
1137
|
+
if (this.process) {
|
|
1138
|
+
types.logger.debug("[AcpSdkBackend] Force killing process");
|
|
1139
|
+
this.process.kill("SIGKILL");
|
|
1140
|
+
}
|
|
1141
|
+
resolve();
|
|
1142
|
+
}, 1e3);
|
|
1143
|
+
this.process?.once("exit", () => {
|
|
1144
|
+
clearTimeout(timeout);
|
|
1145
|
+
resolve();
|
|
1146
|
+
});
|
|
1147
|
+
});
|
|
1148
|
+
this.process = null;
|
|
1149
|
+
}
|
|
1150
|
+
if (this.idleTimeout) {
|
|
1151
|
+
clearTimeout(this.idleTimeout);
|
|
1152
|
+
this.idleTimeout = null;
|
|
1153
|
+
}
|
|
1154
|
+
this.listeners = [];
|
|
1155
|
+
this.connection = null;
|
|
1156
|
+
this.acpSessionId = null;
|
|
1157
|
+
this.activeToolCalls.clear();
|
|
1158
|
+
for (const timeout of this.toolCallTimeouts.values()) {
|
|
1159
|
+
clearTimeout(timeout);
|
|
1160
|
+
}
|
|
1161
|
+
this.toolCallTimeouts.clear();
|
|
1162
|
+
this.toolCallStartTimes.clear();
|
|
1163
|
+
this.pendingPermissions.clear();
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
const GEMINI_API_KEY_ENV = "GEMINI_API_KEY";
|
|
1168
|
+
const GOOGLE_API_KEY_ENV = "GOOGLE_API_KEY";
|
|
1169
|
+
const GEMINI_MODEL_ENV = "GEMINI_MODEL";
|
|
1170
|
+
const DEFAULT_GEMINI_MODEL = "gemini-2.5-pro";
|
|
1171
|
+
|
|
1172
|
+
function readGeminiLocalConfig() {
|
|
1173
|
+
let token = null;
|
|
1174
|
+
let model = null;
|
|
1175
|
+
const possiblePaths = [
|
|
1176
|
+
path$1.join(os$1.homedir(), ".gemini", "oauth_creds.json"),
|
|
1177
|
+
// Main OAuth credentials file
|
|
1178
|
+
path$1.join(os$1.homedir(), ".gemini", "config.json"),
|
|
1179
|
+
path$1.join(os$1.homedir(), ".config", "gemini", "config.json"),
|
|
1180
|
+
path$1.join(os$1.homedir(), ".gemini", "auth.json"),
|
|
1181
|
+
path$1.join(os$1.homedir(), ".config", "gemini", "auth.json")
|
|
1182
|
+
];
|
|
1183
|
+
for (const configPath of possiblePaths) {
|
|
1184
|
+
if (fs.existsSync(configPath)) {
|
|
1185
|
+
try {
|
|
1186
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
1187
|
+
if (!token) {
|
|
1188
|
+
const foundToken = config.access_token || config.token || config.apiKey || config.GEMINI_API_KEY;
|
|
1189
|
+
if (foundToken && typeof foundToken === "string") {
|
|
1190
|
+
token = foundToken;
|
|
1191
|
+
types.logger.debug(`[Gemini] Found token in ${configPath}`);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
if (!model) {
|
|
1195
|
+
const foundModel = config.model || config.GEMINI_MODEL;
|
|
1196
|
+
if (foundModel && typeof foundModel === "string") {
|
|
1197
|
+
model = foundModel;
|
|
1198
|
+
types.logger.debug(`[Gemini] Found model in ${configPath}: ${model}`);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
} catch (error) {
|
|
1202
|
+
types.logger.debug(`[Gemini] Failed to read config from ${configPath}:`, error);
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
if (!token) {
|
|
1207
|
+
try {
|
|
1208
|
+
const gcloudToken = child_process.execSync("gcloud auth application-default print-access-token", {
|
|
1209
|
+
encoding: "utf8",
|
|
1210
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1211
|
+
timeout: 5e3
|
|
1212
|
+
}).trim();
|
|
1213
|
+
if (gcloudToken && gcloudToken.length > 0) {
|
|
1214
|
+
token = gcloudToken;
|
|
1215
|
+
types.logger.debug("[Gemini] Found token via gcloud Application Default Credentials");
|
|
1216
|
+
}
|
|
1217
|
+
} catch (error) {
|
|
1218
|
+
types.logger.debug("[Gemini] gcloud Application Default Credentials not available");
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
return { token, model };
|
|
1222
|
+
}
|
|
1223
|
+
function determineGeminiModel(explicitModel, localConfig) {
|
|
1224
|
+
if (explicitModel !== void 0) {
|
|
1225
|
+
if (explicitModel === null) {
|
|
1226
|
+
return process.env[GEMINI_MODEL_ENV] || DEFAULT_GEMINI_MODEL;
|
|
1227
|
+
} else {
|
|
1228
|
+
return explicitModel;
|
|
1229
|
+
}
|
|
1230
|
+
} else {
|
|
1231
|
+
const envModel = process.env[GEMINI_MODEL_ENV];
|
|
1232
|
+
types.logger.debug(`[Gemini] Model selection: env[GEMINI_MODEL_ENV]=${envModel}, localConfig.model=${localConfig.model}, DEFAULT=${DEFAULT_GEMINI_MODEL}`);
|
|
1233
|
+
const model = envModel || localConfig.model || DEFAULT_GEMINI_MODEL;
|
|
1234
|
+
types.logger.debug(`[Gemini] Selected model: ${model}`);
|
|
1235
|
+
return model;
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
function saveGeminiModelToConfig(model) {
|
|
1239
|
+
try {
|
|
1240
|
+
const configDir = path$1.join(os$1.homedir(), ".gemini");
|
|
1241
|
+
const configPath = path$1.join(configDir, "config.json");
|
|
1242
|
+
if (!fs.existsSync(configDir)) {
|
|
1243
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
1244
|
+
}
|
|
1245
|
+
let config = {};
|
|
1246
|
+
if (fs.existsSync(configPath)) {
|
|
1247
|
+
try {
|
|
1248
|
+
config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
1249
|
+
} catch (error) {
|
|
1250
|
+
types.logger.debug(`[Gemini] Failed to read existing config, creating new one`);
|
|
1251
|
+
config = {};
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
config.model = model;
|
|
1255
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
1256
|
+
types.logger.debug(`[Gemini] Saved model "${model}" to ${configPath}`);
|
|
1257
|
+
} catch (error) {
|
|
1258
|
+
types.logger.debug(`[Gemini] Failed to save model to config:`, error);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
function getInitialGeminiModel() {
|
|
1262
|
+
const localConfig = readGeminiLocalConfig();
|
|
1263
|
+
return process.env[GEMINI_MODEL_ENV] || localConfig.model || DEFAULT_GEMINI_MODEL;
|
|
1264
|
+
}
|
|
1265
|
+
function getGeminiModelSource(explicitModel, localConfig) {
|
|
1266
|
+
if (explicitModel !== void 0 && explicitModel !== null) {
|
|
1267
|
+
return "explicit";
|
|
1268
|
+
} else if (process.env[GEMINI_MODEL_ENV]) {
|
|
1269
|
+
return "env-var";
|
|
1270
|
+
} else if (localConfig.model) {
|
|
1271
|
+
return "local-config";
|
|
1272
|
+
} else {
|
|
1273
|
+
return "default";
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
function resolveGeminiBin() {
|
|
1278
|
+
const explicit = String(process.env.FLOCKBAY_GEMINI_BIN || "").trim();
|
|
1279
|
+
if (explicit) return explicit;
|
|
1280
|
+
const existsExecutable = (p) => {
|
|
1281
|
+
try {
|
|
1282
|
+
fs$1.accessSync(p, fs$1.constants.X_OK);
|
|
1283
|
+
return true;
|
|
1284
|
+
} catch {
|
|
1285
|
+
return false;
|
|
1286
|
+
}
|
|
1287
|
+
};
|
|
1288
|
+
const candidates = [
|
|
1289
|
+
path.join(os.homedir(), ".local", "bin", "gemini"),
|
|
1290
|
+
"/opt/homebrew/bin/gemini",
|
|
1291
|
+
"/usr/local/bin/gemini"
|
|
1292
|
+
];
|
|
1293
|
+
for (const c of candidates) {
|
|
1294
|
+
if (c && existsExecutable(c)) return c;
|
|
1295
|
+
}
|
|
1296
|
+
try {
|
|
1297
|
+
const nvmRoot = path.join(os.homedir(), ".nvm", "versions", "node");
|
|
1298
|
+
const versions = fs$1.readdirSync(nvmRoot, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).filter((name) => name.startsWith("v"));
|
|
1299
|
+
const parse = (v) => {
|
|
1300
|
+
const m = v.match(/^v(\d+)\.(\d+)\.(\d+)/);
|
|
1301
|
+
if (!m) return null;
|
|
1302
|
+
return [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
1303
|
+
};
|
|
1304
|
+
versions.sort((a, b) => {
|
|
1305
|
+
const pa = parse(a);
|
|
1306
|
+
const pb = parse(b);
|
|
1307
|
+
if (!pa || !pb) return a.localeCompare(b);
|
|
1308
|
+
if (pa[0] !== pb[0]) return pb[0] - pa[0];
|
|
1309
|
+
if (pa[1] !== pb[1]) return pb[1] - pa[1];
|
|
1310
|
+
return pb[2] - pa[2];
|
|
1311
|
+
});
|
|
1312
|
+
for (const v of versions) {
|
|
1313
|
+
const p = path.join(nvmRoot, v, "bin", "gemini");
|
|
1314
|
+
if (existsExecutable(p)) return p;
|
|
1315
|
+
}
|
|
1316
|
+
} catch {
|
|
1317
|
+
}
|
|
1318
|
+
const tryShell = (shellPath) => {
|
|
1319
|
+
try {
|
|
1320
|
+
const res = node_child_process.spawnSync(shellPath, ["-lc", "command -v gemini"], {
|
|
1321
|
+
encoding: "utf8",
|
|
1322
|
+
env: process.env,
|
|
1323
|
+
timeout: 4e3
|
|
1324
|
+
});
|
|
1325
|
+
const out = String(res.stdout || "").trim();
|
|
1326
|
+
if (res.status === 0 && out) return out.split("\n")[0].trim();
|
|
1327
|
+
} catch {
|
|
1328
|
+
}
|
|
1329
|
+
return null;
|
|
1330
|
+
};
|
|
1331
|
+
return tryShell("/bin/zsh") || tryShell("/bin/bash") || "gemini";
|
|
1332
|
+
}
|
|
1333
|
+
function createGeminiBackend(options) {
|
|
1334
|
+
const localConfig = readGeminiLocalConfig();
|
|
1335
|
+
let apiKey = options.cloudToken || localConfig.token || process.env[GEMINI_API_KEY_ENV] || process.env[GOOGLE_API_KEY_ENV] || options.apiKey;
|
|
1336
|
+
const authDiagnostics = {
|
|
1337
|
+
hasCloudToken: !!options.cloudToken,
|
|
1338
|
+
hasLocalToken: !!localConfig.token,
|
|
1339
|
+
hasEnvGeminiApiKey: !!process.env[GEMINI_API_KEY_ENV],
|
|
1340
|
+
hasEnvGoogleApiKey: !!process.env[GOOGLE_API_KEY_ENV],
|
|
1341
|
+
hasExplicitApiKey: !!options.apiKey
|
|
1342
|
+
};
|
|
1343
|
+
if (!apiKey) {
|
|
1344
|
+
throw new Error(
|
|
1345
|
+
`[Gemini] Authentication required. Run 'flockbay connect gemini' (recommended), or run 'gemini auth', or set ${GEMINI_API_KEY_ENV}. Diagnostics: ${JSON.stringify(authDiagnostics)}`
|
|
1346
|
+
);
|
|
1347
|
+
}
|
|
1348
|
+
const geminiCommand = resolveGeminiBin();
|
|
1349
|
+
const model = determineGeminiModel(options.model, localConfig);
|
|
1350
|
+
const geminiArgs = ["--experimental-acp"];
|
|
1351
|
+
const backendOptions = {
|
|
1352
|
+
agentName: "gemini",
|
|
1353
|
+
cwd: options.cwd,
|
|
1354
|
+
command: geminiCommand,
|
|
1355
|
+
args: geminiArgs,
|
|
1356
|
+
env: {
|
|
1357
|
+
...options.env,
|
|
1358
|
+
...apiKey ? { [GEMINI_API_KEY_ENV]: apiKey, [GOOGLE_API_KEY_ENV]: apiKey } : {},
|
|
1359
|
+
// Pass model via env var - gemini CLI reads GEMINI_MODEL automatically
|
|
1360
|
+
[GEMINI_MODEL_ENV]: model,
|
|
1361
|
+
// Suppress debug output from gemini CLI to avoid stdout pollution
|
|
1362
|
+
NODE_ENV: "production",
|
|
1363
|
+
DEBUG: ""
|
|
1364
|
+
},
|
|
1365
|
+
mcpServers: options.mcpServers,
|
|
1366
|
+
permissionHandler: options.permissionHandler
|
|
1367
|
+
};
|
|
1368
|
+
const modelSource = getGeminiModelSource(options.model, localConfig);
|
|
1369
|
+
types.logger.debug("[Gemini] Creating ACP SDK backend with options:", {
|
|
1370
|
+
cwd: backendOptions.cwd,
|
|
1371
|
+
command: backendOptions.command,
|
|
1372
|
+
args: backendOptions.args,
|
|
1373
|
+
hasApiKey: !!apiKey,
|
|
1374
|
+
model,
|
|
1375
|
+
modelSource,
|
|
1376
|
+
authDiagnostics,
|
|
1377
|
+
mcpServerCount: options.mcpServers ? Object.keys(options.mcpServers).length : 0
|
|
1378
|
+
});
|
|
1379
|
+
return new AcpSdkBackend(backendOptions);
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
const GeminiDisplay = ({ messageBuffer, logPath, currentModel, onExit }) => {
|
|
1383
|
+
const [messages, setMessages] = React.useState([]);
|
|
1384
|
+
const [confirmationMode, setConfirmationMode] = React.useState(false);
|
|
1385
|
+
const [actionInProgress, setActionInProgress] = React.useState(false);
|
|
1386
|
+
const [model, setModel] = React.useState(currentModel);
|
|
1387
|
+
const confirmationTimeoutRef = React.useRef(null);
|
|
1388
|
+
const { stdout } = ink.useStdout();
|
|
1389
|
+
const terminalWidth = stdout.columns || 80;
|
|
1390
|
+
const terminalHeight = stdout.rows || 24;
|
|
1391
|
+
React.useEffect(() => {
|
|
1392
|
+
if (currentModel !== void 0 && currentModel !== model) {
|
|
1393
|
+
setModel(currentModel);
|
|
1394
|
+
}
|
|
1395
|
+
}, [currentModel]);
|
|
1396
|
+
React.useEffect(() => {
|
|
1397
|
+
setMessages(messageBuffer.getMessages());
|
|
1398
|
+
const unsubscribe = messageBuffer.onUpdate((newMessages) => {
|
|
1399
|
+
setMessages(newMessages);
|
|
1400
|
+
const modelMessage = newMessages.find(
|
|
1401
|
+
(msg) => msg.type === "system" && msg.content.startsWith("[MODEL:")
|
|
1402
|
+
);
|
|
1403
|
+
if (modelMessage) {
|
|
1404
|
+
const modelMatch = modelMessage.content.match(/\[MODEL:(.+?)\]/);
|
|
1405
|
+
if (modelMatch && modelMatch[1]) {
|
|
1406
|
+
const extractedModel = modelMatch[1];
|
|
1407
|
+
setModel((prevModel) => {
|
|
1408
|
+
if (extractedModel !== prevModel) {
|
|
1409
|
+
return extractedModel;
|
|
1410
|
+
}
|
|
1411
|
+
return prevModel;
|
|
1412
|
+
});
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
});
|
|
1416
|
+
return () => {
|
|
1417
|
+
unsubscribe();
|
|
1418
|
+
if (confirmationTimeoutRef.current) {
|
|
1419
|
+
clearTimeout(confirmationTimeoutRef.current);
|
|
1420
|
+
}
|
|
1421
|
+
};
|
|
1422
|
+
}, [messageBuffer]);
|
|
1423
|
+
const resetConfirmation = React.useCallback(() => {
|
|
1424
|
+
setConfirmationMode(false);
|
|
1425
|
+
if (confirmationTimeoutRef.current) {
|
|
1426
|
+
clearTimeout(confirmationTimeoutRef.current);
|
|
1427
|
+
confirmationTimeoutRef.current = null;
|
|
1428
|
+
}
|
|
1429
|
+
}, []);
|
|
1430
|
+
const setConfirmationWithTimeout = React.useCallback(() => {
|
|
1431
|
+
setConfirmationMode(true);
|
|
1432
|
+
if (confirmationTimeoutRef.current) {
|
|
1433
|
+
clearTimeout(confirmationTimeoutRef.current);
|
|
1434
|
+
}
|
|
1435
|
+
confirmationTimeoutRef.current = setTimeout(() => {
|
|
1436
|
+
resetConfirmation();
|
|
1437
|
+
}, 15e3);
|
|
1438
|
+
}, [resetConfirmation]);
|
|
1439
|
+
ink.useInput(React.useCallback(async (input, key) => {
|
|
1440
|
+
if (actionInProgress) return;
|
|
1441
|
+
if (key.ctrl && input === "c") {
|
|
1442
|
+
if (confirmationMode) {
|
|
1443
|
+
resetConfirmation();
|
|
1444
|
+
setActionInProgress(true);
|
|
1445
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1446
|
+
onExit?.();
|
|
1447
|
+
} else {
|
|
1448
|
+
setConfirmationWithTimeout();
|
|
1449
|
+
}
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1452
|
+
if (confirmationMode) {
|
|
1453
|
+
resetConfirmation();
|
|
1454
|
+
}
|
|
1455
|
+
}, [confirmationMode, actionInProgress, onExit, setConfirmationWithTimeout, resetConfirmation]));
|
|
1456
|
+
const getMessageColor = (type) => {
|
|
1457
|
+
switch (type) {
|
|
1458
|
+
case "user":
|
|
1459
|
+
return "magenta";
|
|
1460
|
+
case "assistant":
|
|
1461
|
+
return "cyan";
|
|
1462
|
+
case "system":
|
|
1463
|
+
return "blue";
|
|
1464
|
+
case "tool":
|
|
1465
|
+
return "yellow";
|
|
1466
|
+
case "result":
|
|
1467
|
+
return "green";
|
|
1468
|
+
case "status":
|
|
1469
|
+
return "gray";
|
|
1470
|
+
default:
|
|
1471
|
+
return "white";
|
|
1472
|
+
}
|
|
1473
|
+
};
|
|
1474
|
+
const formatMessage = (msg) => {
|
|
1475
|
+
const lines = msg.content.split("\n");
|
|
1476
|
+
const maxLineLength = terminalWidth - 10;
|
|
1477
|
+
return lines.map((line) => {
|
|
1478
|
+
if (line.length <= maxLineLength) return line;
|
|
1479
|
+
const chunks = [];
|
|
1480
|
+
for (let i = 0; i < line.length; i += maxLineLength) {
|
|
1481
|
+
chunks.push(line.slice(i, i + maxLineLength));
|
|
1482
|
+
}
|
|
1483
|
+
return chunks.join("\n");
|
|
1484
|
+
}).join("\n");
|
|
1485
|
+
};
|
|
1486
|
+
return /* @__PURE__ */ React.createElement(ink.Box, { flexDirection: "column", width: terminalWidth, height: terminalHeight }, /* @__PURE__ */ React.createElement(
|
|
1487
|
+
ink.Box,
|
|
1488
|
+
{
|
|
1489
|
+
flexDirection: "column",
|
|
1490
|
+
width: terminalWidth,
|
|
1491
|
+
height: terminalHeight - 4,
|
|
1492
|
+
borderStyle: "round",
|
|
1493
|
+
borderColor: "gray",
|
|
1494
|
+
paddingX: 1,
|
|
1495
|
+
overflow: "hidden"
|
|
1496
|
+
},
|
|
1497
|
+
/* @__PURE__ */ React.createElement(ink.Box, { flexDirection: "column", marginBottom: 1 }, /* @__PURE__ */ React.createElement(ink.Text, { color: "cyan", bold: true }, "\u2728 Gemini Agent Messages"), /* @__PURE__ */ React.createElement(ink.Text, { color: "gray", dimColor: true }, "\u2500".repeat(Math.min(terminalWidth - 4, 60)))),
|
|
1498
|
+
/* @__PURE__ */ React.createElement(ink.Box, { flexDirection: "column", height: terminalHeight - 10, overflow: "hidden" }, messages.length === 0 ? /* @__PURE__ */ React.createElement(ink.Text, { color: "gray", dimColor: true }, "Waiting for messages...") : messages.filter((msg) => {
|
|
1499
|
+
if (msg.type === "system" && !msg.content.trim()) {
|
|
1500
|
+
return false;
|
|
1501
|
+
}
|
|
1502
|
+
if (msg.type === "system" && msg.content.startsWith("[MODEL:")) {
|
|
1503
|
+
return false;
|
|
1504
|
+
}
|
|
1505
|
+
if (msg.type === "system" && msg.content.startsWith("Using model:")) {
|
|
1506
|
+
return false;
|
|
1507
|
+
}
|
|
1508
|
+
return true;
|
|
1509
|
+
}).slice(-Math.max(1, terminalHeight - 10)).map((msg, index, array) => /* @__PURE__ */ React.createElement(ink.Box, { key: msg.id, flexDirection: "column", marginBottom: index < array.length - 1 ? 1 : 0 }, /* @__PURE__ */ React.createElement(ink.Text, { color: getMessageColor(msg.type), dimColor: true }, formatMessage(msg)))))
|
|
1510
|
+
), /* @__PURE__ */ React.createElement(
|
|
1511
|
+
ink.Box,
|
|
1512
|
+
{
|
|
1513
|
+
width: terminalWidth,
|
|
1514
|
+
borderStyle: "round",
|
|
1515
|
+
borderColor: actionInProgress ? "gray" : confirmationMode ? "red" : "cyan",
|
|
1516
|
+
paddingX: 2,
|
|
1517
|
+
justifyContent: "center",
|
|
1518
|
+
alignItems: "center",
|
|
1519
|
+
flexDirection: "column"
|
|
1520
|
+
},
|
|
1521
|
+
/* @__PURE__ */ React.createElement(ink.Box, { flexDirection: "column", alignItems: "center" }, actionInProgress ? /* @__PURE__ */ React.createElement(ink.Text, { color: "gray", bold: true }, "Exiting agent...") : confirmationMode ? /* @__PURE__ */ React.createElement(ink.Text, { color: "red", bold: true }, "\u26A0\uFE0F Press Ctrl-C again to exit the agent") : /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(ink.Text, { color: "cyan", bold: true }, "\u2728 Gemini Agent Running \u2022 Ctrl-C to exit"), model && /* @__PURE__ */ React.createElement(ink.Text, { color: "gray", dimColor: true }, "Model: ", model)), process.env.DEBUG && logPath && /* @__PURE__ */ React.createElement(ink.Text, { color: "gray", dimColor: true }, "Debug logs: ", logPath))
|
|
1522
|
+
));
|
|
1523
|
+
};
|
|
1524
|
+
|
|
1525
|
+
class GeminiPermissionHandler {
|
|
1526
|
+
pendingRequests = /* @__PURE__ */ new Map();
|
|
1527
|
+
session;
|
|
1528
|
+
currentPermissionMode = "default";
|
|
1529
|
+
constructor(session) {
|
|
1530
|
+
this.session = session;
|
|
1531
|
+
this.setupRpcHandler();
|
|
1532
|
+
}
|
|
1533
|
+
/**
|
|
1534
|
+
* Set the current permission mode
|
|
1535
|
+
* This affects how tool calls are automatically approved/denied
|
|
1536
|
+
*/
|
|
1537
|
+
setPermissionMode(mode) {
|
|
1538
|
+
this.currentPermissionMode = mode;
|
|
1539
|
+
types.logger.debug(`[Gemini] Permission mode set to: ${mode}`);
|
|
1540
|
+
}
|
|
1541
|
+
/**
|
|
1542
|
+
* Check if a tool should be auto-approved based on permission mode
|
|
1543
|
+
*/
|
|
1544
|
+
shouldAutoApprove(toolName, toolCallId, input) {
|
|
1545
|
+
const alwaysAutoApproveNames = ["change_title", "mcp__flockbay__change_title", "GeminiReasoning", "CodexReasoning", "think", "save_memory"];
|
|
1546
|
+
const alwaysAutoApproveIds = ["change_title", "save_memory"];
|
|
1547
|
+
if (alwaysAutoApproveNames.some((name) => toolName.toLowerCase().includes(name.toLowerCase()))) {
|
|
1548
|
+
return true;
|
|
1549
|
+
}
|
|
1550
|
+
if (alwaysAutoApproveIds.some((id) => toolCallId.toLowerCase().includes(id.toLowerCase()))) {
|
|
1551
|
+
return true;
|
|
1552
|
+
}
|
|
1553
|
+
switch (this.currentPermissionMode) {
|
|
1554
|
+
case "yolo":
|
|
1555
|
+
return true;
|
|
1556
|
+
case "safe-yolo":
|
|
1557
|
+
return true;
|
|
1558
|
+
case "read-only":
|
|
1559
|
+
const writeTools = ["write", "edit", "create", "delete", "patch", "fs-edit"];
|
|
1560
|
+
const isWriteTool = writeTools.some((wt) => toolName.toLowerCase().includes(wt));
|
|
1561
|
+
return !isWriteTool;
|
|
1562
|
+
case "always-ask":
|
|
1563
|
+
case "default":
|
|
1564
|
+
default:
|
|
1565
|
+
return false;
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
/**
|
|
1569
|
+
* Handle a tool permission request
|
|
1570
|
+
* @param toolCallId - The unique ID of the tool call
|
|
1571
|
+
* @param toolName - The name of the tool being called
|
|
1572
|
+
* @param input - The input parameters for the tool
|
|
1573
|
+
* @returns Promise resolving to permission result
|
|
1574
|
+
*/
|
|
1575
|
+
async handleToolCall(toolCallId, toolName, input) {
|
|
1576
|
+
const enforced = this.enforceCoordinationGate(toolName, input);
|
|
1577
|
+
if (enforced) {
|
|
1578
|
+
this.emitPolicyCard(toolCallId, toolName, input, enforced);
|
|
1579
|
+
this.session.updateAgentState((currentState) => ({
|
|
1580
|
+
...currentState,
|
|
1581
|
+
completedRequests: {
|
|
1582
|
+
...currentState.completedRequests,
|
|
1583
|
+
[toolCallId]: {
|
|
1584
|
+
tool: toolName,
|
|
1585
|
+
arguments: input,
|
|
1586
|
+
createdAt: Date.now(),
|
|
1587
|
+
completedAt: Date.now(),
|
|
1588
|
+
status: "denied",
|
|
1589
|
+
decision: enforced.decision,
|
|
1590
|
+
reason: enforced.reason
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
}));
|
|
1594
|
+
return enforced;
|
|
1595
|
+
}
|
|
1596
|
+
if (this.shouldAutoApprove(toolName, toolCallId, input)) {
|
|
1597
|
+
types.logger.debug(`[Gemini] Auto-approving tool ${toolName} (${toolCallId}) in ${this.currentPermissionMode} mode`);
|
|
1598
|
+
if (index.shouldCountToolCall(toolName)) {
|
|
1599
|
+
try {
|
|
1600
|
+
const quota = await index.consumeToolQuota({ token: this.session.getAuthToken(), toolName });
|
|
1601
|
+
if (quota.allowed !== true) {
|
|
1602
|
+
const reason = index.formatQuotaDeniedReason(quota);
|
|
1603
|
+
this.session.updateAgentState((currentState) => ({
|
|
1604
|
+
...currentState,
|
|
1605
|
+
completedRequests: {
|
|
1606
|
+
...currentState.completedRequests,
|
|
1607
|
+
[toolCallId]: {
|
|
1608
|
+
tool: toolName,
|
|
1609
|
+
arguments: input,
|
|
1610
|
+
createdAt: Date.now(),
|
|
1611
|
+
completedAt: Date.now(),
|
|
1612
|
+
status: "denied",
|
|
1613
|
+
decision: "denied",
|
|
1614
|
+
reason
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
}));
|
|
1618
|
+
const res2 = { decision: "denied", reason };
|
|
1619
|
+
this.emitPolicyCard(toolCallId, toolName, input, res2);
|
|
1620
|
+
return res2;
|
|
1621
|
+
}
|
|
1622
|
+
} catch (err) {
|
|
1623
|
+
const reason = err?.message || "Tool quota check failed";
|
|
1624
|
+
this.session.updateAgentState((currentState) => ({
|
|
1625
|
+
...currentState,
|
|
1626
|
+
completedRequests: {
|
|
1627
|
+
...currentState.completedRequests,
|
|
1628
|
+
[toolCallId]: {
|
|
1629
|
+
tool: toolName,
|
|
1630
|
+
arguments: input,
|
|
1631
|
+
createdAt: Date.now(),
|
|
1632
|
+
completedAt: Date.now(),
|
|
1633
|
+
status: "denied",
|
|
1634
|
+
decision: "denied",
|
|
1635
|
+
reason
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
}));
|
|
1639
|
+
const res2 = { decision: "denied", reason };
|
|
1640
|
+
this.emitPolicyCard(toolCallId, toolName, input, res2);
|
|
1641
|
+
return res2;
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
this.session.updateAgentState((currentState) => ({
|
|
1645
|
+
...currentState,
|
|
1646
|
+
completedRequests: {
|
|
1647
|
+
...currentState.completedRequests,
|
|
1648
|
+
[toolCallId]: {
|
|
1649
|
+
tool: toolName,
|
|
1650
|
+
arguments: input,
|
|
1651
|
+
createdAt: Date.now(),
|
|
1652
|
+
completedAt: Date.now(),
|
|
1653
|
+
status: "approved",
|
|
1654
|
+
decision: this.currentPermissionMode === "yolo" ? "approved_for_session" : "approved"
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
}));
|
|
1658
|
+
const res = {
|
|
1659
|
+
decision: this.currentPermissionMode === "yolo" ? "approved_for_session" : "approved"
|
|
1660
|
+
};
|
|
1661
|
+
this.emitPolicyCard(toolCallId, toolName, input, res);
|
|
1662
|
+
return res;
|
|
1663
|
+
}
|
|
1664
|
+
this.emitPolicyCard(toolCallId, toolName, input, { decision: "abort", reason: "permission_prompt_required" });
|
|
1665
|
+
return new Promise((resolve, reject) => {
|
|
1666
|
+
this.pendingRequests.set(toolCallId, {
|
|
1667
|
+
resolve,
|
|
1668
|
+
reject,
|
|
1669
|
+
toolName,
|
|
1670
|
+
input
|
|
1671
|
+
});
|
|
1672
|
+
this.session.updateAgentState((currentState) => ({
|
|
1673
|
+
...currentState,
|
|
1674
|
+
requests: {
|
|
1675
|
+
...currentState.requests,
|
|
1676
|
+
[toolCallId]: {
|
|
1677
|
+
tool: toolName,
|
|
1678
|
+
arguments: input,
|
|
1679
|
+
createdAt: Date.now()
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
}));
|
|
1683
|
+
types.logger.debug(`[Gemini] Permission request sent for tool: ${toolName} (${toolCallId}) in ${this.currentPermissionMode} mode`);
|
|
1684
|
+
});
|
|
1685
|
+
}
|
|
1686
|
+
normalizeClaimFilePath(filePath) {
|
|
1687
|
+
const raw = typeof filePath === "string" ? filePath.trim() : String(filePath ?? "").trim();
|
|
1688
|
+
if (!raw) return null;
|
|
1689
|
+
let candidate = raw.replace(/\\/g, "/");
|
|
1690
|
+
if (path.isAbsolute(candidate)) {
|
|
1691
|
+
const meta = this.session?.metadata;
|
|
1692
|
+
const base = typeof meta?.projectRootPath === "string" && meta.projectRootPath.trim() ? meta.projectRootPath.trim() : typeof meta?.path === "string" && meta.path.trim() ? meta.path.trim() : process.cwd();
|
|
1693
|
+
const rel = path.relative(base, candidate);
|
|
1694
|
+
if (rel && !rel.startsWith("..") && !path.isAbsolute(rel)) {
|
|
1695
|
+
candidate = rel.replace(/\\/g, "/");
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
candidate = candidate.replace(/^\.\/+/, "").replace(/^\/+/, "");
|
|
1699
|
+
if (!candidate) return null;
|
|
1700
|
+
if (candidate.includes("..")) return null;
|
|
1701
|
+
return candidate;
|
|
1702
|
+
}
|
|
1703
|
+
isFileEditLikeTool(toolName, input) {
|
|
1704
|
+
const name = String(toolName || "").toLowerCase();
|
|
1705
|
+
if (/fs[-_]?edit|patch|apply_patch|edit|write|create|delete/.test(name)) return true;
|
|
1706
|
+
if (!input || typeof input !== "object") return false;
|
|
1707
|
+
const keys = Object.keys(input).map((k) => k.toLowerCase());
|
|
1708
|
+
return keys.some((k) => k === "file_path" || k === "filepath" || k === "file" || k === "path" || k === "changes" || k === "diff");
|
|
1709
|
+
}
|
|
1710
|
+
buildCoordinationGateReason(args) {
|
|
1711
|
+
const file = args.file || null;
|
|
1712
|
+
if (args.reason === "docs_index_read_required") {
|
|
1713
|
+
return {
|
|
1714
|
+
reason: "read the game Documentation index before making edits. Next: call `mcp__flockbay__docs_index_read`, then retry the edit.",
|
|
1715
|
+
nextSteps: ["Call `mcp__flockbay__docs_index_read`.", "Then retry the edit."]
|
|
1716
|
+
};
|
|
1717
|
+
}
|
|
1718
|
+
if (args.reason === "ledger_read_required") {
|
|
1719
|
+
return {
|
|
1720
|
+
reason: "read the ledger before making file edits. Next: call `mcp__flockbay__ledger_read` (or `mcp__flockbay__coordination_ledger_snapshot`), then retry the edit.",
|
|
1721
|
+
nextSteps: [
|
|
1722
|
+
"Call `mcp__flockbay__ledger_read` (or `mcp__flockbay__coordination_ledger_snapshot`).",
|
|
1723
|
+
"Then retry the edit."
|
|
1724
|
+
]
|
|
1725
|
+
};
|
|
1726
|
+
}
|
|
1727
|
+
const display = file ? `claim \`${file}\` before editing it.` : "claim the file before editing it.";
|
|
1728
|
+
return {
|
|
1729
|
+
reason: `${display} Next: claim it via \`mcp__flockbay__ledger_claim\` (or \`mcp__flockbay__coordination_claim_files\`), then retry the edit.`,
|
|
1730
|
+
nextSteps: file ? [
|
|
1731
|
+
`Claim via \`mcp__flockbay__ledger_claim\` (files: ["${file}"]) or \`mcp__flockbay__coordination_claim_files\`.`,
|
|
1732
|
+
"Then retry the edit."
|
|
1733
|
+
] : ["Claim the file via `mcp__flockbay__ledger_claim` (or `mcp__flockbay__coordination_claim_files`).", "Then retry the edit."]
|
|
1734
|
+
};
|
|
1735
|
+
}
|
|
1736
|
+
enforceCoordinationGate(toolName, input) {
|
|
1737
|
+
const hasContext = typeof this.session?.hasCoordinationContext === "function" ? this.session.hasCoordinationContext() : false;
|
|
1738
|
+
if (!hasContext) return null;
|
|
1739
|
+
if (!this.isFileEditLikeTool(toolName, input)) return null;
|
|
1740
|
+
const didReadDocsIndex = typeof this.session?.didReadDocsIndexWithin === "function" ? this.session.didReadDocsIndexWithin(365 * 24 * 60 * 6e4) : false;
|
|
1741
|
+
if (!didReadDocsIndex) {
|
|
1742
|
+
const { reason } = this.buildCoordinationGateReason({ reason: "docs_index_read_required" });
|
|
1743
|
+
return { decision: "denied", reason };
|
|
1744
|
+
}
|
|
1745
|
+
const didReadLedger = typeof this.session?.didReadCoordinationLedgerWithin === "function" ? this.session.didReadCoordinationLedgerWithin(15 * 6e4) : false;
|
|
1746
|
+
if (!didReadLedger) {
|
|
1747
|
+
const { reason } = this.buildCoordinationGateReason({ reason: "ledger_read_required" });
|
|
1748
|
+
return { decision: "denied", reason };
|
|
1749
|
+
}
|
|
1750
|
+
const changes = input?.changes ?? input?.input?.changes;
|
|
1751
|
+
const fileCandidates = [];
|
|
1752
|
+
if (changes && typeof changes === "object") {
|
|
1753
|
+
fileCandidates.push(...Object.keys(changes));
|
|
1754
|
+
}
|
|
1755
|
+
const directPath = input?.file_path ?? input?.path ?? input?.filePath ?? input?.filepath;
|
|
1756
|
+
if (directPath) fileCandidates.push(String(directPath));
|
|
1757
|
+
const requestedFiles = fileCandidates.map((p) => this.normalizeClaimFilePath(p)).filter((v) => Boolean(v));
|
|
1758
|
+
if (requestedFiles.length === 0) return null;
|
|
1759
|
+
const guard = this.session?.coordinationLeaseGuard;
|
|
1760
|
+
const hasClaim = (file) => {
|
|
1761
|
+
try {
|
|
1762
|
+
return Boolean(guard && typeof guard.has === "function" && guard.has(file));
|
|
1763
|
+
} catch (error) {
|
|
1764
|
+
console.error("[GeminiPermissionHandler] coordinationLeaseGuard.has threw:", error);
|
|
1765
|
+
types.logger.debug("[GeminiPermissionHandler] coordinationLeaseGuard.has threw:", error);
|
|
1766
|
+
return false;
|
|
1767
|
+
}
|
|
1768
|
+
};
|
|
1769
|
+
const missing = requestedFiles.filter((p) => !hasClaim(p));
|
|
1770
|
+
if (missing.length > 0) {
|
|
1771
|
+
const { reason } = this.buildCoordinationGateReason({ reason: "file_claim_required", file: missing[0] });
|
|
1772
|
+
return { decision: "denied", reason };
|
|
1773
|
+
}
|
|
1774
|
+
return null;
|
|
1775
|
+
}
|
|
1776
|
+
/**
|
|
1777
|
+
* Setup RPC handler for permission responses
|
|
1778
|
+
*/
|
|
1779
|
+
setupRpcHandler() {
|
|
1780
|
+
this.session.rpcHandlerManager.registerHandler(
|
|
1781
|
+
"permission",
|
|
1782
|
+
async (response) => {
|
|
1783
|
+
const pending = this.pendingRequests.get(response.id);
|
|
1784
|
+
if (!pending) {
|
|
1785
|
+
types.logger.debug("[Gemini] Permission request not found or already resolved");
|
|
1786
|
+
return { ok: true };
|
|
1787
|
+
}
|
|
1788
|
+
this.pendingRequests.delete(response.id);
|
|
1789
|
+
let approved = response.approved;
|
|
1790
|
+
let reason;
|
|
1791
|
+
if (approved && index.shouldCountToolCall(pending.toolName)) {
|
|
1792
|
+
try {
|
|
1793
|
+
const quota = await index.consumeToolQuota({ token: this.session.getAuthToken(), toolName: pending.toolName });
|
|
1794
|
+
if (quota.allowed !== true) {
|
|
1795
|
+
approved = false;
|
|
1796
|
+
reason = index.formatQuotaDeniedReason(quota);
|
|
1797
|
+
}
|
|
1798
|
+
} catch (err) {
|
|
1799
|
+
approved = false;
|
|
1800
|
+
reason = err?.message || "Tool quota check failed";
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
const result = approved ? { decision: response.decision === "approved_for_session" ? "approved_for_session" : "approved" } : { decision: response.decision === "denied" ? "denied" : "abort", reason };
|
|
1804
|
+
this.emitPolicyCard(response.id, pending.toolName, pending.input, result);
|
|
1805
|
+
pending.resolve(result);
|
|
1806
|
+
this.session.updateAgentState((currentState) => {
|
|
1807
|
+
const request = currentState.requests?.[response.id];
|
|
1808
|
+
if (!request) return currentState;
|
|
1809
|
+
const { [response.id]: _, ...remainingRequests } = currentState.requests || {};
|
|
1810
|
+
let res = {
|
|
1811
|
+
...currentState,
|
|
1812
|
+
requests: remainingRequests,
|
|
1813
|
+
completedRequests: {
|
|
1814
|
+
...currentState.completedRequests,
|
|
1815
|
+
[response.id]: {
|
|
1816
|
+
...request,
|
|
1817
|
+
completedAt: Date.now(),
|
|
1818
|
+
status: approved ? "approved" : "denied",
|
|
1819
|
+
decision: result.decision,
|
|
1820
|
+
reason
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
};
|
|
1824
|
+
return res;
|
|
1825
|
+
});
|
|
1826
|
+
types.logger.debug(`[Gemini] Permission ${approved ? "approved" : "denied"} for ${pending.toolName}`);
|
|
1827
|
+
return { ok: true };
|
|
1828
|
+
}
|
|
1829
|
+
);
|
|
1830
|
+
}
|
|
1831
|
+
emitPolicyCard(toolCallId, toolName, input, result) {
|
|
1832
|
+
const decision = result.decision;
|
|
1833
|
+
const reason = result.reason;
|
|
1834
|
+
const kind = decision === "approved" || decision === "approved_for_session" ? "policy_allow" : decision === "abort" && reason === "permission_prompt_required" ? "policy_prompt" : "policy_block";
|
|
1835
|
+
const summary = kind === "policy_allow" ? "Allowed." : kind === "policy_prompt" ? "Waiting for permission to run this tool." : reason ? `Blocked: ${reason}` : "Blocked by policy.";
|
|
1836
|
+
const nextSteps = [];
|
|
1837
|
+
if (kind === "policy_block" && typeof reason === "string") {
|
|
1838
|
+
if (reason.includes("docs_index_read") || reason.includes("Documentation index")) nextSteps.push("Call `mcp__flockbay__docs_index_read`.");
|
|
1839
|
+
if (reason.includes("ledger_read")) nextSteps.push("Call `mcp__flockbay__ledger_read` (or `mcp__flockbay__coordination_ledger_snapshot`).");
|
|
1840
|
+
if (reason.includes("ledger_claim") || reason.includes("claim")) nextSteps.push("Claim the file via `mcp__flockbay__ledger_claim` (or `mcp__flockbay__coordination_claim_files`).");
|
|
1841
|
+
if (nextSteps.length) nextSteps.push("Then retry the edit.");
|
|
1842
|
+
}
|
|
1843
|
+
const callId = `policy:${toolCallId}:${node_crypto.randomUUID().slice(0, 8)}`;
|
|
1844
|
+
const payload = {
|
|
1845
|
+
kind,
|
|
1846
|
+
blockedTool: toolName,
|
|
1847
|
+
summary,
|
|
1848
|
+
nextSteps,
|
|
1849
|
+
toolName,
|
|
1850
|
+
decision,
|
|
1851
|
+
reason,
|
|
1852
|
+
permissionMode: this.currentPermissionMode,
|
|
1853
|
+
inputPreviewKeys: input && typeof input === "object" ? Object.keys(input).slice(0, 20) : null
|
|
1854
|
+
};
|
|
1855
|
+
try {
|
|
1856
|
+
this.session.sendCodexMessage({
|
|
1857
|
+
type: "tool-call",
|
|
1858
|
+
name: "FlockbayPolicy",
|
|
1859
|
+
callId,
|
|
1860
|
+
input: payload,
|
|
1861
|
+
id: node_crypto.randomUUID()
|
|
1862
|
+
});
|
|
1863
|
+
this.session.sendCodexMessage({
|
|
1864
|
+
type: "tool-call-result",
|
|
1865
|
+
callId,
|
|
1866
|
+
output: payload,
|
|
1867
|
+
is_error: kind === "policy_block",
|
|
1868
|
+
id: node_crypto.randomUUID()
|
|
1869
|
+
});
|
|
1870
|
+
} catch (err) {
|
|
1871
|
+
types.logger.debug("[Gemini] Failed to emit FlockbayPolicy card", err);
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
/**
|
|
1875
|
+
* Reset state for new sessions
|
|
1876
|
+
*/
|
|
1877
|
+
reset() {
|
|
1878
|
+
for (const [id, pending] of this.pendingRequests.entries()) {
|
|
1879
|
+
pending.reject(new Error("Session reset"));
|
|
1880
|
+
}
|
|
1881
|
+
this.pendingRequests.clear();
|
|
1882
|
+
this.session.updateAgentState((currentState) => {
|
|
1883
|
+
const pendingRequests = currentState.requests || {};
|
|
1884
|
+
const completedRequests = { ...currentState.completedRequests };
|
|
1885
|
+
for (const [id, request] of Object.entries(pendingRequests)) {
|
|
1886
|
+
completedRequests[id] = {
|
|
1887
|
+
...request,
|
|
1888
|
+
completedAt: Date.now(),
|
|
1889
|
+
status: "canceled",
|
|
1890
|
+
reason: "Session reset"
|
|
1891
|
+
};
|
|
1892
|
+
}
|
|
1893
|
+
return {
|
|
1894
|
+
...currentState,
|
|
1895
|
+
requests: {},
|
|
1896
|
+
completedRequests
|
|
1897
|
+
};
|
|
1898
|
+
});
|
|
1899
|
+
types.logger.debug("[Gemini] Permission handler reset");
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
class GeminiReasoningProcessor {
|
|
1904
|
+
accumulator = "";
|
|
1905
|
+
inTitleCapture = false;
|
|
1906
|
+
titleBuffer = "";
|
|
1907
|
+
contentBuffer = "";
|
|
1908
|
+
hasTitle = false;
|
|
1909
|
+
currentCallId = null;
|
|
1910
|
+
toolCallStarted = false;
|
|
1911
|
+
currentTitle = null;
|
|
1912
|
+
onMessage = null;
|
|
1913
|
+
constructor(onMessage) {
|
|
1914
|
+
this.onMessage = onMessage || null;
|
|
1915
|
+
this.reset();
|
|
1916
|
+
}
|
|
1917
|
+
/**
|
|
1918
|
+
* Set the message callback for sending messages directly
|
|
1919
|
+
*/
|
|
1920
|
+
setMessageCallback(callback) {
|
|
1921
|
+
this.onMessage = callback;
|
|
1922
|
+
}
|
|
1923
|
+
/**
|
|
1924
|
+
* Process a reasoning section break - indicates a new reasoning section is starting
|
|
1925
|
+
*/
|
|
1926
|
+
handleSectionBreak() {
|
|
1927
|
+
this.finishCurrentToolCall("canceled");
|
|
1928
|
+
this.resetState();
|
|
1929
|
+
types.logger.debug("[GeminiReasoningProcessor] Section break - reset state");
|
|
1930
|
+
}
|
|
1931
|
+
/**
|
|
1932
|
+
* Process a reasoning chunk from agent_thought_chunk
|
|
1933
|
+
* Gemini sends reasoning as chunks, we accumulate them similar to Codex
|
|
1934
|
+
*/
|
|
1935
|
+
processChunk(chunk) {
|
|
1936
|
+
this.accumulator += chunk;
|
|
1937
|
+
if (!this.inTitleCapture && !this.hasTitle && !this.contentBuffer) {
|
|
1938
|
+
if (this.accumulator.startsWith("**")) {
|
|
1939
|
+
this.inTitleCapture = true;
|
|
1940
|
+
this.titleBuffer = this.accumulator.substring(2);
|
|
1941
|
+
types.logger.debug("[GeminiReasoningProcessor] Started title capture");
|
|
1942
|
+
} else if (this.accumulator.length > 0) {
|
|
1943
|
+
this.contentBuffer = this.accumulator;
|
|
1944
|
+
}
|
|
1945
|
+
} else if (this.inTitleCapture) {
|
|
1946
|
+
this.titleBuffer = this.accumulator.substring(2);
|
|
1947
|
+
const titleEndIndex = this.titleBuffer.indexOf("**");
|
|
1948
|
+
if (titleEndIndex !== -1) {
|
|
1949
|
+
const title = this.titleBuffer.substring(0, titleEndIndex);
|
|
1950
|
+
const afterTitle = this.titleBuffer.substring(titleEndIndex + 2);
|
|
1951
|
+
this.hasTitle = true;
|
|
1952
|
+
this.inTitleCapture = false;
|
|
1953
|
+
this.currentTitle = title;
|
|
1954
|
+
this.contentBuffer = afterTitle;
|
|
1955
|
+
this.currentCallId = node_crypto.randomUUID();
|
|
1956
|
+
types.logger.debug(`[GeminiReasoningProcessor] Title captured: "${title}"`);
|
|
1957
|
+
this.sendToolCallStart(title);
|
|
1958
|
+
}
|
|
1959
|
+
} else if (this.hasTitle) {
|
|
1960
|
+
const titleStartIndex = this.accumulator.indexOf("**");
|
|
1961
|
+
if (titleStartIndex !== -1) {
|
|
1962
|
+
this.contentBuffer = this.accumulator.substring(
|
|
1963
|
+
titleStartIndex + 2 + this.currentTitle.length + 2
|
|
1964
|
+
);
|
|
1965
|
+
}
|
|
1966
|
+
} else {
|
|
1967
|
+
this.contentBuffer = this.accumulator;
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
/**
|
|
1971
|
+
* Send the tool call start message
|
|
1972
|
+
*/
|
|
1973
|
+
sendToolCallStart(title) {
|
|
1974
|
+
if (!this.currentCallId || this.toolCallStarted) return;
|
|
1975
|
+
types.logger.debug(`[GeminiReasoningProcessor] Reasoning section started (title="${title}")`);
|
|
1976
|
+
this.toolCallStarted = true;
|
|
1977
|
+
}
|
|
1978
|
+
/**
|
|
1979
|
+
* Complete the reasoning section with final text
|
|
1980
|
+
* Called when reasoning is complete (e.g., when status changes to idle)
|
|
1981
|
+
* Returns true if reasoning was actually completed, false if there was nothing to complete
|
|
1982
|
+
*/
|
|
1983
|
+
complete() {
|
|
1984
|
+
const fullText = this.accumulator;
|
|
1985
|
+
if (!fullText.trim() && !this.toolCallStarted) {
|
|
1986
|
+
types.logger.debug("[GeminiReasoningProcessor] Complete called but no content accumulated, skipping");
|
|
1987
|
+
return false;
|
|
1988
|
+
}
|
|
1989
|
+
let title;
|
|
1990
|
+
let content = fullText;
|
|
1991
|
+
if (fullText.startsWith("**")) {
|
|
1992
|
+
const titleEndIndex = fullText.indexOf("**", 2);
|
|
1993
|
+
if (titleEndIndex !== -1) {
|
|
1994
|
+
title = fullText.substring(2, titleEndIndex);
|
|
1995
|
+
content = fullText.substring(titleEndIndex + 2).trim();
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
types.logger.debug(`[GeminiReasoningProcessor] Complete reasoning - Title: "${title}", Has content: ${content.length > 0}`);
|
|
1999
|
+
if (title && !this.toolCallStarted) {
|
|
2000
|
+
this.currentCallId = this.currentCallId || node_crypto.randomUUID();
|
|
2001
|
+
this.sendToolCallStart(title);
|
|
2002
|
+
}
|
|
2003
|
+
if (content.trim()) {
|
|
2004
|
+
types.logger.debug("[GeminiReasoningProcessor] Reasoning completed (content suppressed from UI)");
|
|
2005
|
+
}
|
|
2006
|
+
this.resetState();
|
|
2007
|
+
return true;
|
|
2008
|
+
}
|
|
2009
|
+
/**
|
|
2010
|
+
* Abort the current reasoning section
|
|
2011
|
+
*/
|
|
2012
|
+
abort() {
|
|
2013
|
+
types.logger.debug("[GeminiReasoningProcessor] Abort called");
|
|
2014
|
+
this.finishCurrentToolCall("canceled");
|
|
2015
|
+
this.resetState();
|
|
2016
|
+
}
|
|
2017
|
+
/**
|
|
2018
|
+
* Reset the processor state
|
|
2019
|
+
*/
|
|
2020
|
+
reset() {
|
|
2021
|
+
this.finishCurrentToolCall("canceled");
|
|
2022
|
+
this.resetState();
|
|
2023
|
+
}
|
|
2024
|
+
/**
|
|
2025
|
+
* Finish current tool call if one is in progress
|
|
2026
|
+
*/
|
|
2027
|
+
finishCurrentToolCall(status) {
|
|
2028
|
+
if (this.toolCallStarted && this.currentCallId) {
|
|
2029
|
+
types.logger.debug(`[GeminiReasoningProcessor] Reasoning section finished (status=${status}, suppressed from UI)`);
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
/**
|
|
2033
|
+
* Reset internal state
|
|
2034
|
+
*/
|
|
2035
|
+
resetState() {
|
|
2036
|
+
this.accumulator = "";
|
|
2037
|
+
this.inTitleCapture = false;
|
|
2038
|
+
this.titleBuffer = "";
|
|
2039
|
+
this.contentBuffer = "";
|
|
2040
|
+
this.hasTitle = false;
|
|
2041
|
+
this.currentCallId = null;
|
|
2042
|
+
this.toolCallStarted = false;
|
|
2043
|
+
this.currentTitle = null;
|
|
2044
|
+
}
|
|
2045
|
+
/**
|
|
2046
|
+
* Get the current call ID for tool result matching
|
|
2047
|
+
*/
|
|
2048
|
+
getCurrentCallId() {
|
|
2049
|
+
return this.currentCallId;
|
|
2050
|
+
}
|
|
2051
|
+
/**
|
|
2052
|
+
* Check if a tool call has been started
|
|
2053
|
+
*/
|
|
2054
|
+
hasStartedToolCall() {
|
|
2055
|
+
return this.toolCallStarted;
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
class GeminiDiffProcessor {
|
|
2060
|
+
previousDiffs = /* @__PURE__ */ new Map();
|
|
2061
|
+
// Track diffs per file path
|
|
2062
|
+
onMessage = null;
|
|
2063
|
+
constructor(onMessage) {
|
|
2064
|
+
this.onMessage = onMessage || null;
|
|
2065
|
+
}
|
|
2066
|
+
/**
|
|
2067
|
+
* Process an fs-edit event and check if it contains diff information
|
|
2068
|
+
*/
|
|
2069
|
+
processFsEdit(path, description, diff) {
|
|
2070
|
+
types.logger.debug(`[GeminiDiffProcessor] Processing fs-edit for path: ${path}`);
|
|
2071
|
+
if (diff) {
|
|
2072
|
+
this.processDiff(path, diff, description);
|
|
2073
|
+
} else {
|
|
2074
|
+
const simpleDiff = `File edited: ${path}${description ? ` - ${description}` : ""}`;
|
|
2075
|
+
this.processDiff(path, simpleDiff, description);
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
/**
|
|
2079
|
+
* Process a tool result that may contain diff information
|
|
2080
|
+
*/
|
|
2081
|
+
processToolResult(toolName, result, callId) {
|
|
2082
|
+
if (result && typeof result === "object") {
|
|
2083
|
+
const diff = result.diff || result.unified_diff || result.patch;
|
|
2084
|
+
const path = result.path || result.file;
|
|
2085
|
+
if (diff && path) {
|
|
2086
|
+
types.logger.debug(`[GeminiDiffProcessor] Found diff in tool result: ${toolName} (${callId})`);
|
|
2087
|
+
this.processDiff(path, diff, result.description);
|
|
2088
|
+
} else if (result.changes && typeof result.changes === "object") {
|
|
2089
|
+
for (const [filePath, change] of Object.entries(result.changes)) {
|
|
2090
|
+
const changeDiff = change.diff || change.unified_diff || JSON.stringify(change);
|
|
2091
|
+
this.processDiff(filePath, changeDiff, change.description);
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
/**
|
|
2097
|
+
* Process a unified diff and check if it has changed from the previous value
|
|
2098
|
+
*/
|
|
2099
|
+
processDiff(path, unifiedDiff, description) {
|
|
2100
|
+
const previousDiff = this.previousDiffs.get(path);
|
|
2101
|
+
if (previousDiff !== unifiedDiff) {
|
|
2102
|
+
types.logger.debug(`[GeminiDiffProcessor] Unified diff changed for ${path}, sending GeminiDiff tool call`);
|
|
2103
|
+
const callId = node_crypto.randomUUID();
|
|
2104
|
+
const toolCall = {
|
|
2105
|
+
type: "tool-call",
|
|
2106
|
+
name: "GeminiDiff",
|
|
2107
|
+
callId,
|
|
2108
|
+
input: {
|
|
2109
|
+
unified_diff: unifiedDiff,
|
|
2110
|
+
path,
|
|
2111
|
+
description
|
|
2112
|
+
},
|
|
2113
|
+
id: node_crypto.randomUUID()
|
|
2114
|
+
};
|
|
2115
|
+
this.onMessage?.(toolCall);
|
|
2116
|
+
const toolResult = {
|
|
2117
|
+
type: "tool-call-result",
|
|
2118
|
+
callId,
|
|
2119
|
+
output: {
|
|
2120
|
+
status: "completed"
|
|
2121
|
+
},
|
|
2122
|
+
id: node_crypto.randomUUID()
|
|
2123
|
+
};
|
|
2124
|
+
this.onMessage?.(toolResult);
|
|
2125
|
+
}
|
|
2126
|
+
this.previousDiffs.set(path, unifiedDiff);
|
|
2127
|
+
types.logger.debug(`[GeminiDiffProcessor] Updated stored diff for ${path}`);
|
|
2128
|
+
}
|
|
2129
|
+
/**
|
|
2130
|
+
* Reset the processor state (called on task_complete or turn_aborted)
|
|
2131
|
+
*/
|
|
2132
|
+
reset() {
|
|
2133
|
+
types.logger.debug("[GeminiDiffProcessor] Resetting diff state");
|
|
2134
|
+
this.previousDiffs.clear();
|
|
2135
|
+
}
|
|
2136
|
+
/**
|
|
2137
|
+
* Set the message callback for sending messages directly
|
|
2138
|
+
*/
|
|
2139
|
+
setMessageCallback(callback) {
|
|
2140
|
+
this.onMessage = callback;
|
|
2141
|
+
}
|
|
2142
|
+
/**
|
|
2143
|
+
* Get the current diff value for a specific path
|
|
2144
|
+
*/
|
|
2145
|
+
getCurrentDiff(path) {
|
|
2146
|
+
return this.previousDiffs.get(path) || null;
|
|
2147
|
+
}
|
|
2148
|
+
/**
|
|
2149
|
+
* Get all tracked diffs
|
|
2150
|
+
*/
|
|
2151
|
+
getAllDiffs() {
|
|
2152
|
+
return new Map(this.previousDiffs);
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
function hasIncompleteOptions(text) {
|
|
2157
|
+
const hasOpeningTag = /<options>/i.test(text);
|
|
2158
|
+
const hasClosingTag = /<\/options>/i.test(text);
|
|
2159
|
+
return hasOpeningTag && !hasClosingTag;
|
|
2160
|
+
}
|
|
2161
|
+
function parseOptionsFromText(text) {
|
|
2162
|
+
const optionsRegex = /<options>\s*([\s\S]*?)\s*<\/options>/i;
|
|
2163
|
+
const match = text.match(optionsRegex);
|
|
2164
|
+
if (!match) {
|
|
2165
|
+
return { text: text.trim(), options: [] };
|
|
2166
|
+
}
|
|
2167
|
+
const optionsBlock = match[1];
|
|
2168
|
+
const optionRegex = /<option>(.*?)<\/option>/gi;
|
|
2169
|
+
const options = [];
|
|
2170
|
+
let optionMatch;
|
|
2171
|
+
while ((optionMatch = optionRegex.exec(optionsBlock)) !== null) {
|
|
2172
|
+
const optionText = optionMatch[1].trim();
|
|
2173
|
+
if (optionText) {
|
|
2174
|
+
options.push(optionText);
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
const textWithoutOptions = text.replace(optionsRegex, "").trim();
|
|
2178
|
+
return { text: textWithoutOptions, options };
|
|
2179
|
+
}
|
|
2180
|
+
function formatOptionsXml(options) {
|
|
2181
|
+
if (options.length === 0) {
|
|
2182
|
+
return "";
|
|
2183
|
+
}
|
|
2184
|
+
return "\n<options>\n" + options.map((opt) => ` <option>${opt}</option>`).join("\n") + "\n</options>";
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
function hashGeminiSessionMode(mode) {
|
|
2188
|
+
return index.hashObject({ model: mode.model ?? null });
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
async function runGemini(opts) {
|
|
2192
|
+
const sessionTag = node_crypto.randomUUID();
|
|
2193
|
+
const api = await types.ApiClient.create(opts.credentials);
|
|
2194
|
+
const settings = await types.readSettings();
|
|
2195
|
+
const machineId = settings?.machineId;
|
|
2196
|
+
if (!machineId) {
|
|
2197
|
+
console.error(`[START] No machine ID found in settings, which is unexpected since authAndSetupMachineIfNeeded should have created it. Support: coming soon.`);
|
|
2198
|
+
process.exit(1);
|
|
2199
|
+
}
|
|
2200
|
+
types.logger.debug(`Using machineId: ${machineId}`);
|
|
2201
|
+
try {
|
|
2202
|
+
const existing = await api.getMachine(machineId);
|
|
2203
|
+
await api.getOrCreateMachine({
|
|
2204
|
+
machineId,
|
|
2205
|
+
metadata: { ...existing.metadata || {}, ...index.initialMachineMetadata }
|
|
2206
|
+
});
|
|
2207
|
+
} catch (error) {
|
|
2208
|
+
const status = error?.response?.status;
|
|
2209
|
+
if (status === 404) {
|
|
2210
|
+
await api.getOrCreateMachine({ machineId, metadata: index.initialMachineMetadata });
|
|
2211
|
+
} else {
|
|
2212
|
+
throw error;
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
let cloudToken = void 0;
|
|
2216
|
+
const fingerprintSecret = (value) => {
|
|
2217
|
+
if (!value) return null;
|
|
2218
|
+
const digest = node_crypto.createHash("sha256").update(value).digest("hex");
|
|
2219
|
+
return `${digest.slice(0, 10)}:${value.length}`;
|
|
2220
|
+
};
|
|
2221
|
+
let cloudTokenFingerprint = null;
|
|
2222
|
+
const syncGeminiOauthCredsFile = async (oauth, context) => {
|
|
2223
|
+
const accessToken = typeof oauth?.access_token === "string" ? oauth.access_token : null;
|
|
2224
|
+
if (!accessToken) return { wrote: false };
|
|
2225
|
+
const dir = path.join(os.homedir(), ".gemini");
|
|
2226
|
+
const path$1 = path.join(dir, "oauth_creds.json");
|
|
2227
|
+
try {
|
|
2228
|
+
await fs$2.mkdir(dir, { recursive: true });
|
|
2229
|
+
const payload = {
|
|
2230
|
+
...oauth && typeof oauth === "object" ? oauth : {},
|
|
2231
|
+
access_token: accessToken,
|
|
2232
|
+
token_type: typeof oauth?.token_type === "string" ? oauth.token_type : "Bearer",
|
|
2233
|
+
// Helpful for debugging + token refresh behavior; gemini CLI can ignore if unknown.
|
|
2234
|
+
flockbay_written_at_ms: Date.now(),
|
|
2235
|
+
flockbay_written_context: context
|
|
2236
|
+
};
|
|
2237
|
+
await fs$2.writeFile(path$1, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 384 });
|
|
2238
|
+
types.logger.debug("[Gemini] Synced OAuth creds to local gemini CLI config", {
|
|
2239
|
+
context,
|
|
2240
|
+
path: path$1,
|
|
2241
|
+
accessTokenFingerprint: fingerprintSecret(accessToken)
|
|
2242
|
+
});
|
|
2243
|
+
return { wrote: true };
|
|
2244
|
+
} catch (error) {
|
|
2245
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
2246
|
+
throw new Error(
|
|
2247
|
+
`Failed to write Gemini OAuth credentials to ${path$1}: ${msg}. Try running 'gemini auth' directly, or re-run 'flockbay connect gemini'.`
|
|
2248
|
+
);
|
|
2249
|
+
}
|
|
2250
|
+
};
|
|
2251
|
+
try {
|
|
2252
|
+
const vendorToken = await api.getVendorToken("gemini");
|
|
2253
|
+
const oauth = vendorToken?.oauth;
|
|
2254
|
+
if (oauth?.access_token) {
|
|
2255
|
+
cloudToken = oauth.access_token;
|
|
2256
|
+
cloudTokenFingerprint = fingerprintSecret(cloudToken);
|
|
2257
|
+
types.logger.debug("[Gemini] Using OAuth token from Flockbay cloud");
|
|
2258
|
+
await syncGeminiOauthCredsFile(oauth, "initial-fetch");
|
|
2259
|
+
}
|
|
2260
|
+
} catch (error) {
|
|
2261
|
+
types.logger.debug("[Gemini] Failed to fetch cloud token:", error);
|
|
2262
|
+
}
|
|
2263
|
+
const refreshGeminiCloudToken = async (context) => {
|
|
2264
|
+
const prevFingerprint = cloudTokenFingerprint;
|
|
2265
|
+
let nextToken = void 0;
|
|
2266
|
+
let nextOauth = void 0;
|
|
2267
|
+
try {
|
|
2268
|
+
const vendorToken = await api.getVendorToken("gemini");
|
|
2269
|
+
nextOauth = vendorToken?.oauth;
|
|
2270
|
+
if (nextOauth?.access_token) {
|
|
2271
|
+
nextToken = nextOauth.access_token;
|
|
2272
|
+
}
|
|
2273
|
+
} catch (error) {
|
|
2274
|
+
types.logger.debug("[Gemini] Failed to refresh cloud token:", { context, error });
|
|
2275
|
+
return { changed: false };
|
|
2276
|
+
}
|
|
2277
|
+
const nextFingerprint = fingerprintSecret(nextToken);
|
|
2278
|
+
const changed = nextFingerprint !== prevFingerprint;
|
|
2279
|
+
if (changed) {
|
|
2280
|
+
cloudToken = nextToken;
|
|
2281
|
+
cloudTokenFingerprint = nextFingerprint;
|
|
2282
|
+
types.logger.debug("[Gemini] Cloud token changed; future sessions will use updated credentials", {
|
|
2283
|
+
context,
|
|
2284
|
+
prevFingerprint,
|
|
2285
|
+
nextFingerprint,
|
|
2286
|
+
hasToken: !!cloudToken
|
|
2287
|
+
});
|
|
2288
|
+
if (nextOauth?.access_token) {
|
|
2289
|
+
await syncGeminiOauthCredsFile(nextOauth, `refresh:${context}`);
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
return { changed };
|
|
2293
|
+
};
|
|
2294
|
+
const stringifyErrorish = (value) => {
|
|
2295
|
+
if (typeof value === "string") return value;
|
|
2296
|
+
if (value instanceof Error) return value.message;
|
|
2297
|
+
if (value && typeof value === "object") {
|
|
2298
|
+
const maybeMessage = value?.message;
|
|
2299
|
+
if (typeof maybeMessage === "string" && maybeMessage.trim()) return maybeMessage;
|
|
2300
|
+
try {
|
|
2301
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
2302
|
+
return JSON.stringify(value, (_k, v) => {
|
|
2303
|
+
if (!v || typeof v !== "object") return v;
|
|
2304
|
+
if (seen.has(v)) return "[Circular]";
|
|
2305
|
+
seen.add(v);
|
|
2306
|
+
if (v && typeof v === "object" && ("isAxiosError" in v || "config" in v || "response" in v)) {
|
|
2307
|
+
const anyV = v;
|
|
2308
|
+
return {
|
|
2309
|
+
name: anyV?.name,
|
|
2310
|
+
message: anyV?.message,
|
|
2311
|
+
code: anyV?.code,
|
|
2312
|
+
status: anyV?.response?.status,
|
|
2313
|
+
statusText: anyV?.response?.statusText,
|
|
2314
|
+
data: anyV?.response?.data
|
|
2315
|
+
};
|
|
2316
|
+
}
|
|
2317
|
+
return v;
|
|
2318
|
+
});
|
|
2319
|
+
} catch {
|
|
2320
|
+
return String(value);
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
return String(value);
|
|
2324
|
+
};
|
|
2325
|
+
const state = {
|
|
2326
|
+
controlledByUser: false
|
|
2327
|
+
};
|
|
2328
|
+
const coordinationProjectRootPath = process.env.FLOCKBAY_COORDINATION_PROJECT_ROOT_PATH || void 0;
|
|
2329
|
+
const coordinationWorkspaceProjectId = process.env.FLOCKBAY_COORDINATION_WORKSPACE_PROJECT_ID || void 0;
|
|
2330
|
+
const coordinationFeatureId = process.env.FLOCKBAY_COORDINATION_FEATURE_ID || void 0;
|
|
2331
|
+
const coordinationWorkItemId = process.env.FLOCKBAY_COORDINATION_WORK_ITEM_ID || void 0;
|
|
2332
|
+
const metadata = {
|
|
2333
|
+
path: process.cwd(),
|
|
2334
|
+
projectRootPath: coordinationProjectRootPath,
|
|
2335
|
+
host: os.hostname(),
|
|
2336
|
+
version: types.packageJson.version,
|
|
2337
|
+
os: os.platform(),
|
|
2338
|
+
machineId,
|
|
2339
|
+
workspaceProjectId: coordinationWorkspaceProjectId,
|
|
2340
|
+
featureId: coordinationFeatureId,
|
|
2341
|
+
workItemId: coordinationWorkItemId,
|
|
2342
|
+
homeDir: os.homedir(),
|
|
2343
|
+
flockbayHomeDir: types.configuration.flockbayHomeDir,
|
|
2344
|
+
flockbayLibDir: types.projectPath(),
|
|
2345
|
+
flockbayToolsDir: path.resolve(types.projectPath(), "tools", "unpacked"),
|
|
2346
|
+
startedFromDaemon: opts.startedBy === "daemon",
|
|
2347
|
+
hostPid: process.pid,
|
|
2348
|
+
startedBy: opts.startedBy || "terminal",
|
|
2349
|
+
lifecycleState: "running",
|
|
2350
|
+
lifecycleStateSince: Date.now(),
|
|
2351
|
+
flavor: "gemini"
|
|
2352
|
+
};
|
|
2353
|
+
const response = await api.getOrCreateSession({ tag: sessionTag, metadata, state });
|
|
2354
|
+
const session = api.sessionSyncClient(response);
|
|
2355
|
+
try {
|
|
2356
|
+
types.logger.debug(`[START] Reporting session ${response.id} to daemon`);
|
|
2357
|
+
const result = await index.notifyDaemonSessionStarted(response.id, metadata);
|
|
2358
|
+
if (result.error) {
|
|
2359
|
+
types.logger.debug(`[START] Failed to report to daemon (may not be running):`, result.error);
|
|
2360
|
+
} else {
|
|
2361
|
+
types.logger.debug(`[START] Reported session ${response.id} to daemon`);
|
|
2362
|
+
}
|
|
2363
|
+
} catch (error) {
|
|
2364
|
+
types.logger.debug("[START] Failed to report to daemon (may not be running):", error);
|
|
2365
|
+
}
|
|
2366
|
+
const messageQueue = new index.MessageQueue2((mode) => hashGeminiSessionMode(mode));
|
|
2367
|
+
const projectCapsule = await index.buildProjectCapsule({ startDir: process.cwd() });
|
|
2368
|
+
const screenshotGate = {
|
|
2369
|
+
paths: [],
|
|
2370
|
+
seen: /* @__PURE__ */ new Set(),
|
|
2371
|
+
inAutoReview: false
|
|
2372
|
+
};
|
|
2373
|
+
const resetScreenshotGateForTurn = () => {
|
|
2374
|
+
screenshotGate.paths = [];
|
|
2375
|
+
screenshotGate.seen.clear();
|
|
2376
|
+
screenshotGate.inAutoReview = false;
|
|
2377
|
+
};
|
|
2378
|
+
const collectScreenshotsForGate = (output) => {
|
|
2379
|
+
if (screenshotGate.inAutoReview) return;
|
|
2380
|
+
if (!output) return;
|
|
2381
|
+
const cwdHint = output?.cwd;
|
|
2382
|
+
const cwd = typeof cwdHint === "string" && cwdHint.trim().length > 0 ? cwdHint.trim() : process.cwd();
|
|
2383
|
+
const detected = flockbayScreenshotGate.detectScreenshotsForGate({ output, cwd });
|
|
2384
|
+
if (detected.paths.length === 0) return;
|
|
2385
|
+
for (const p of detected.paths) {
|
|
2386
|
+
const trimmed = String(p || "").trim();
|
|
2387
|
+
if (!trimmed) continue;
|
|
2388
|
+
if (screenshotGate.seen.has(trimmed)) continue;
|
|
2389
|
+
screenshotGate.seen.add(trimmed);
|
|
2390
|
+
screenshotGate.paths.push(trimmed);
|
|
2391
|
+
}
|
|
2392
|
+
};
|
|
2393
|
+
const buildScreenshotReviewBlocks = async (paths) => {
|
|
2394
|
+
const unique = Array.from(new Set(paths.map((p) => String(p || "").trim()).filter(Boolean)));
|
|
2395
|
+
const blocks = [
|
|
2396
|
+
{
|
|
2397
|
+
type: "text",
|
|
2398
|
+
text: [
|
|
2399
|
+
`You just generated ${unique.length} screenshot${unique.length === 1 ? "" : "s"}.`,
|
|
2400
|
+
"",
|
|
2401
|
+
"Visually inspect EVERY screenshot and report:",
|
|
2402
|
+
"- One short bullet per image describing what you see",
|
|
2403
|
+
"- Whether the screenshots match the user's request",
|
|
2404
|
+
"",
|
|
2405
|
+
"Do not run any tools in this step."
|
|
2406
|
+
].join("\n")
|
|
2407
|
+
}
|
|
2408
|
+
];
|
|
2409
|
+
for (let i = 0; i < unique.length; i += 1) {
|
|
2410
|
+
const p = unique[i];
|
|
2411
|
+
const buf = await fs$2.readFile(p);
|
|
2412
|
+
blocks.push({ type: "text", text: `Image ${i + 1}: ${path.basename(p)}` });
|
|
2413
|
+
blocks.push({ type: "image", data: buf.toString("base64"), mimeType: "image/png" });
|
|
2414
|
+
}
|
|
2415
|
+
return blocks;
|
|
2416
|
+
};
|
|
2417
|
+
let currentPermissionMode = void 0;
|
|
2418
|
+
let currentModel = void 0;
|
|
2419
|
+
session.onUserMessage((message) => {
|
|
2420
|
+
const images = Array.isArray(message.meta?.attachments?.images) ? message.meta.attachments.images : [];
|
|
2421
|
+
if (images.length > 0) {
|
|
2422
|
+
index.setLatestUserImages(images);
|
|
2423
|
+
}
|
|
2424
|
+
let messagePermissionMode = currentPermissionMode;
|
|
2425
|
+
if (message.meta?.permissionMode) {
|
|
2426
|
+
const validModes = ["default", "always-ask", "read-only", "safe-yolo", "yolo"];
|
|
2427
|
+
if (validModes.includes(message.meta.permissionMode)) {
|
|
2428
|
+
messagePermissionMode = message.meta.permissionMode;
|
|
2429
|
+
currentPermissionMode = messagePermissionMode;
|
|
2430
|
+
updatePermissionMode(messagePermissionMode);
|
|
2431
|
+
types.logger.debug(`[Gemini] Permission mode updated from user message to: ${currentPermissionMode}`);
|
|
2432
|
+
} else {
|
|
2433
|
+
types.logger.debug(`[Gemini] Invalid permission mode received: ${message.meta.permissionMode}`);
|
|
2434
|
+
}
|
|
2435
|
+
} else {
|
|
2436
|
+
types.logger.debug(`[Gemini] User message received with no permission mode override, using current: ${currentPermissionMode ?? "default (effective)"}`);
|
|
2437
|
+
}
|
|
2438
|
+
if (currentPermissionMode === void 0) {
|
|
2439
|
+
currentPermissionMode = "default";
|
|
2440
|
+
updatePermissionMode("default");
|
|
2441
|
+
}
|
|
2442
|
+
let messageModel = currentModel;
|
|
2443
|
+
if (message.meta?.hasOwnProperty("model")) {
|
|
2444
|
+
if (message.meta.model === null) {
|
|
2445
|
+
messageModel = void 0;
|
|
2446
|
+
currentModel = void 0;
|
|
2447
|
+
} else if (message.meta.model) {
|
|
2448
|
+
messageModel = message.meta.model;
|
|
2449
|
+
currentModel = messageModel;
|
|
2450
|
+
updateDisplayedModel(messageModel, true);
|
|
2451
|
+
messageBuffer.addMessage(`Model changed to: ${messageModel}`, "system");
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
const originalUserMessage = message.content.text;
|
|
2455
|
+
let fullPrompt = originalUserMessage;
|
|
2456
|
+
if (isFirstMessage) {
|
|
2457
|
+
const preambleParts = [];
|
|
2458
|
+
if (message.meta?.hasOwnProperty("appendSystemPrompt")) {
|
|
2459
|
+
const raw = message.meta.appendSystemPrompt;
|
|
2460
|
+
if (typeof raw === "string" && raw.trim().length > 0) {
|
|
2461
|
+
preambleParts.push(raw);
|
|
2462
|
+
} else {
|
|
2463
|
+
preambleParts.push(index.PLATFORM_SYSTEM_PROMPT);
|
|
2464
|
+
}
|
|
2465
|
+
} else {
|
|
2466
|
+
preambleParts.push(index.PLATFORM_SYSTEM_PROMPT);
|
|
2467
|
+
}
|
|
2468
|
+
if (projectCapsule) preambleParts.push(projectCapsule);
|
|
2469
|
+
const preamble = preambleParts.map((p) => String(p || "").trim()).filter(Boolean).join("\n\n");
|
|
2470
|
+
fullPrompt = preamble.length > 0 ? preamble + "\n\n" + originalUserMessage : originalUserMessage;
|
|
2471
|
+
isFirstMessage = false;
|
|
2472
|
+
}
|
|
2473
|
+
const mode = {
|
|
2474
|
+
permissionMode: messagePermissionMode || "default",
|
|
2475
|
+
model: messageModel,
|
|
2476
|
+
originalUserMessage
|
|
2477
|
+
// Store original message separately
|
|
2478
|
+
};
|
|
2479
|
+
const promptText = images.length > 0 ? index.withUserImagesMarker(fullPrompt, images.length) : fullPrompt;
|
|
2480
|
+
messageQueue.push(promptText, mode, images.length > 0 ? { isolate: true } : void 0);
|
|
2481
|
+
});
|
|
2482
|
+
let thinking = false;
|
|
2483
|
+
session.keepAlive(thinking, "remote");
|
|
2484
|
+
const keepAliveInterval = setInterval(() => {
|
|
2485
|
+
session.keepAlive(thinking, "remote");
|
|
2486
|
+
}, 2e3);
|
|
2487
|
+
let isFirstMessage = true;
|
|
2488
|
+
const autoPrompt = String(process.env.FLOCKBAY_AUTO_PROMPT || "").trim();
|
|
2489
|
+
const autoExitOnIdle = String(process.env.FLOCKBAY_AUTO_EXIT_ON_IDLE || "").trim() === "1";
|
|
2490
|
+
if (autoPrompt) {
|
|
2491
|
+
const preambleParts = [index.PLATFORM_SYSTEM_PROMPT];
|
|
2492
|
+
if (projectCapsule) preambleParts.push(projectCapsule);
|
|
2493
|
+
const preamble = preambleParts.map((p) => String(p || "").trim()).filter(Boolean).join("\n\n");
|
|
2494
|
+
const fullPrompt = preamble.length > 0 ? preamble + "\n\n" + autoPrompt : autoPrompt;
|
|
2495
|
+
isFirstMessage = false;
|
|
2496
|
+
messageQueue.push(fullPrompt, {
|
|
2497
|
+
permissionMode: "default",
|
|
2498
|
+
model: void 0,
|
|
2499
|
+
originalUserMessage: autoPrompt
|
|
2500
|
+
});
|
|
2501
|
+
}
|
|
2502
|
+
const sendReady = () => {
|
|
2503
|
+
session.sendSessionEvent({ type: "ready" });
|
|
2504
|
+
try {
|
|
2505
|
+
api.push().sendToAllDevices(
|
|
2506
|
+
"It's ready!",
|
|
2507
|
+
"Gemini is waiting for your command",
|
|
2508
|
+
{ sessionId: session.sessionId }
|
|
2509
|
+
);
|
|
2510
|
+
} catch (pushError) {
|
|
2511
|
+
types.logger.debug("[Gemini] Failed to send ready push", pushError);
|
|
2512
|
+
}
|
|
2513
|
+
};
|
|
2514
|
+
const emitReadyIfIdle = () => {
|
|
2515
|
+
if (shouldExit) {
|
|
2516
|
+
return false;
|
|
2517
|
+
}
|
|
2518
|
+
if (thinking) {
|
|
2519
|
+
return false;
|
|
2520
|
+
}
|
|
2521
|
+
if (isResponseInProgress) {
|
|
2522
|
+
return false;
|
|
2523
|
+
}
|
|
2524
|
+
if (messageQueue.size() > 0) {
|
|
2525
|
+
return false;
|
|
2526
|
+
}
|
|
2527
|
+
sendReady();
|
|
2528
|
+
if (autoExitOnIdle) {
|
|
2529
|
+
shouldExit = true;
|
|
2530
|
+
setTimeout(() => process.exit(0), 50).unref();
|
|
2531
|
+
}
|
|
2532
|
+
return true;
|
|
2533
|
+
};
|
|
2534
|
+
let abortController = new AbortController();
|
|
2535
|
+
let shouldExit = false;
|
|
2536
|
+
let geminiBackend = null;
|
|
2537
|
+
let acpSessionId = null;
|
|
2538
|
+
let wasSessionCreated = false;
|
|
2539
|
+
async function handleAbort() {
|
|
2540
|
+
types.logger.debug("[Gemini] Abort requested - stopping current task");
|
|
2541
|
+
session.sendCodexMessage({
|
|
2542
|
+
type: "turn_aborted",
|
|
2543
|
+
id: node_crypto.randomUUID()
|
|
2544
|
+
});
|
|
2545
|
+
reasoningProcessor.abort();
|
|
2546
|
+
diffProcessor.reset();
|
|
2547
|
+
try {
|
|
2548
|
+
abortController.abort();
|
|
2549
|
+
messageQueue.reset();
|
|
2550
|
+
if (geminiBackend && acpSessionId) {
|
|
2551
|
+
await geminiBackend.cancel(acpSessionId);
|
|
2552
|
+
}
|
|
2553
|
+
if (geminiBackend) {
|
|
2554
|
+
await geminiBackend.dispose();
|
|
2555
|
+
geminiBackend = null;
|
|
2556
|
+
}
|
|
2557
|
+
acpSessionId = null;
|
|
2558
|
+
wasSessionCreated = false;
|
|
2559
|
+
types.logger.debug("[Gemini] Abort completed - session remains active");
|
|
2560
|
+
} catch (error) {
|
|
2561
|
+
types.logger.debug("[Gemini] Error during abort:", error);
|
|
2562
|
+
} finally {
|
|
2563
|
+
abortController = new AbortController();
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
const handleKillSession = async () => {
|
|
2567
|
+
types.logger.debug("[Gemini] Kill session requested - terminating process");
|
|
2568
|
+
await handleAbort();
|
|
2569
|
+
types.logger.debug("[Gemini] Abort completed, proceeding with termination");
|
|
2570
|
+
try {
|
|
2571
|
+
if (session) {
|
|
2572
|
+
session.updateMetadata((currentMetadata) => ({
|
|
2573
|
+
...currentMetadata,
|
|
2574
|
+
lifecycleState: "archived",
|
|
2575
|
+
lifecycleStateSince: Date.now(),
|
|
2576
|
+
archivedBy: "cli",
|
|
2577
|
+
archiveReason: "User terminated"
|
|
2578
|
+
}));
|
|
2579
|
+
session.sendSessionDeath();
|
|
2580
|
+
await session.flush();
|
|
2581
|
+
await session.close();
|
|
2582
|
+
}
|
|
2583
|
+
index.stopCaffeinate();
|
|
2584
|
+
flockbayServer.stop();
|
|
2585
|
+
if (geminiBackend) {
|
|
2586
|
+
await geminiBackend.dispose();
|
|
2587
|
+
}
|
|
2588
|
+
types.logger.debug("[Gemini] Session termination complete, exiting");
|
|
2589
|
+
process.exit(0);
|
|
2590
|
+
} catch (error) {
|
|
2591
|
+
types.logger.debug("[Gemini] Error during session termination:", error);
|
|
2592
|
+
process.exit(1);
|
|
2593
|
+
}
|
|
2594
|
+
};
|
|
2595
|
+
session.rpcHandlerManager.registerHandler("abort", handleAbort);
|
|
2596
|
+
index.registerKillSessionHandler(session.rpcHandlerManager, handleKillSession);
|
|
2597
|
+
const messageBuffer = new index.MessageBuffer();
|
|
2598
|
+
const hasTTY = process.stdout.isTTY && process.stdin.isTTY;
|
|
2599
|
+
let inkInstance = null;
|
|
2600
|
+
let displayedModel = getInitialGeminiModel();
|
|
2601
|
+
const localConfig = readGeminiLocalConfig();
|
|
2602
|
+
types.logger.debug(`[gemini] Initial model setup: env[GEMINI_MODEL_ENV]=${process.env[GEMINI_MODEL_ENV] || "not set"}, localConfig=${localConfig.model || "not set"}, displayedModel=${displayedModel}`);
|
|
2603
|
+
const updateDisplayedModel = (model, saveToConfig = false) => {
|
|
2604
|
+
if (model === void 0) {
|
|
2605
|
+
types.logger.debug(`[gemini] updateDisplayedModel called with undefined, skipping update`);
|
|
2606
|
+
return;
|
|
2607
|
+
}
|
|
2608
|
+
const oldModel = displayedModel;
|
|
2609
|
+
displayedModel = model;
|
|
2610
|
+
types.logger.debug(`[gemini] updateDisplayedModel called: oldModel=${oldModel}, newModel=${model}, saveToConfig=${saveToConfig}`);
|
|
2611
|
+
if (saveToConfig) {
|
|
2612
|
+
saveGeminiModelToConfig(model);
|
|
2613
|
+
}
|
|
2614
|
+
if (hasTTY && oldModel !== model) {
|
|
2615
|
+
types.logger.debug(`[gemini] Adding model update message to buffer: [MODEL:${model}]`);
|
|
2616
|
+
messageBuffer.addMessage(`[MODEL:${model}]`, "system");
|
|
2617
|
+
} else if (hasTTY) {
|
|
2618
|
+
types.logger.debug(`[gemini] Model unchanged, skipping update message`);
|
|
2619
|
+
}
|
|
2620
|
+
};
|
|
2621
|
+
if (hasTTY) {
|
|
2622
|
+
console.clear();
|
|
2623
|
+
const DisplayComponent = () => {
|
|
2624
|
+
const currentModelValue = displayedModel || "gemini-2.5-pro";
|
|
2625
|
+
return React.createElement(GeminiDisplay, {
|
|
2626
|
+
messageBuffer,
|
|
2627
|
+
logPath: process.env.DEBUG ? types.logger.getLogPath() : void 0,
|
|
2628
|
+
currentModel: currentModelValue,
|
|
2629
|
+
onExit: async () => {
|
|
2630
|
+
types.logger.debug("[gemini]: Exiting agent via Ctrl-C");
|
|
2631
|
+
shouldExit = true;
|
|
2632
|
+
await handleAbort();
|
|
2633
|
+
}
|
|
2634
|
+
});
|
|
2635
|
+
};
|
|
2636
|
+
inkInstance = ink.render(React.createElement(DisplayComponent), {
|
|
2637
|
+
exitOnCtrlC: false,
|
|
2638
|
+
patchConsole: false
|
|
2639
|
+
});
|
|
2640
|
+
const initialModelName = displayedModel || "gemini-2.5-pro";
|
|
2641
|
+
types.logger.debug(`[gemini] Sending initial model to UI: ${initialModelName}`);
|
|
2642
|
+
messageBuffer.addMessage(`[MODEL:${initialModelName}]`, "system");
|
|
2643
|
+
}
|
|
2644
|
+
if (hasTTY) {
|
|
2645
|
+
process.stdin.resume();
|
|
2646
|
+
if (process.stdin.isTTY) {
|
|
2647
|
+
process.stdin.setRawMode(true);
|
|
2648
|
+
}
|
|
2649
|
+
process.stdin.setEncoding("utf8");
|
|
2650
|
+
}
|
|
2651
|
+
const elicitationHub = new index.ElicitationHub();
|
|
2652
|
+
const flockbayServer = await index.startFlockbayServer(session, { elicitationHub });
|
|
2653
|
+
const bridgeCommand = path.join(types.projectPath(), "bin", "flockbay-mcp.mjs");
|
|
2654
|
+
const mcpServers = {
|
|
2655
|
+
flockbay: {
|
|
2656
|
+
command: bridgeCommand,
|
|
2657
|
+
args: ["--url", flockbayServer.url]
|
|
2658
|
+
}
|
|
2659
|
+
};
|
|
2660
|
+
const permissionHandler = new GeminiPermissionHandler(session);
|
|
2661
|
+
const reasoningProcessor = new GeminiReasoningProcessor((message) => {
|
|
2662
|
+
session.sendCodexMessage(message);
|
|
2663
|
+
});
|
|
2664
|
+
const diffProcessor = new GeminiDiffProcessor((message) => {
|
|
2665
|
+
session.sendCodexMessage(message);
|
|
2666
|
+
});
|
|
2667
|
+
const updatePermissionMode = (mode) => {
|
|
2668
|
+
permissionHandler.setPermissionMode(mode);
|
|
2669
|
+
};
|
|
2670
|
+
let accumulatedResponse = "";
|
|
2671
|
+
let isResponseInProgress = false;
|
|
2672
|
+
function setupGeminiMessageHandler(backend) {
|
|
2673
|
+
backend.onMessage((msg) => {
|
|
2674
|
+
switch (msg.type) {
|
|
2675
|
+
case "model-output":
|
|
2676
|
+
if (msg.textDelta) {
|
|
2677
|
+
if (!isResponseInProgress) {
|
|
2678
|
+
messageBuffer.removeLastMessage("system");
|
|
2679
|
+
messageBuffer.addMessage(msg.textDelta, "assistant");
|
|
2680
|
+
isResponseInProgress = true;
|
|
2681
|
+
types.logger.debug(`[gemini] Started new response, first chunk length: ${msg.textDelta.length}`);
|
|
2682
|
+
} else {
|
|
2683
|
+
messageBuffer.updateLastMessage(msg.textDelta, "assistant");
|
|
2684
|
+
types.logger.debug(`[gemini] Updated response, chunk length: ${msg.textDelta.length}, total accumulated: ${accumulatedResponse.length + msg.textDelta.length}`);
|
|
2685
|
+
}
|
|
2686
|
+
accumulatedResponse += msg.textDelta;
|
|
2687
|
+
}
|
|
2688
|
+
break;
|
|
2689
|
+
case "status":
|
|
2690
|
+
types.logger.debug(`[gemini] Status changed: ${msg.status}${msg.detail ? ` - ${msg.detail}` : ""}`);
|
|
2691
|
+
if (msg.status === "error") {
|
|
2692
|
+
types.logger.debug(`[gemini] \u26A0\uFE0F Error status received: ${msg.detail || "Unknown error"}`);
|
|
2693
|
+
session.sendCodexMessage({
|
|
2694
|
+
type: "turn_aborted",
|
|
2695
|
+
id: node_crypto.randomUUID()
|
|
2696
|
+
});
|
|
2697
|
+
}
|
|
2698
|
+
if (msg.status === "running") {
|
|
2699
|
+
thinking = true;
|
|
2700
|
+
session.keepAlive(thinking, "remote");
|
|
2701
|
+
session.sendCodexMessage({
|
|
2702
|
+
type: "task_started",
|
|
2703
|
+
id: node_crypto.randomUUID()
|
|
2704
|
+
});
|
|
2705
|
+
messageBuffer.addMessage("Thinking...", "system");
|
|
2706
|
+
} else if (msg.status === "idle" || msg.status === "stopped") {
|
|
2707
|
+
if (thinking) {
|
|
2708
|
+
thinking = false;
|
|
2709
|
+
}
|
|
2710
|
+
thinking = false;
|
|
2711
|
+
session.keepAlive(thinking, "remote");
|
|
2712
|
+
const reasoningCompleted = reasoningProcessor.complete();
|
|
2713
|
+
if (reasoningCompleted || isResponseInProgress) {
|
|
2714
|
+
session.sendCodexMessage({
|
|
2715
|
+
type: "task_complete",
|
|
2716
|
+
id: node_crypto.randomUUID()
|
|
2717
|
+
});
|
|
2718
|
+
}
|
|
2719
|
+
if (isResponseInProgress && accumulatedResponse.trim()) {
|
|
2720
|
+
const { text: messageText, options } = parseOptionsFromText(accumulatedResponse);
|
|
2721
|
+
let finalMessageText = messageText;
|
|
2722
|
+
if (options.length > 0) {
|
|
2723
|
+
const optionsXml = formatOptionsXml(options);
|
|
2724
|
+
finalMessageText = messageText + optionsXml;
|
|
2725
|
+
types.logger.debug(`[gemini] Found ${options.length} options in response:`, options);
|
|
2726
|
+
types.logger.debug(`[gemini] Keeping options in message text for mobile app parsing`);
|
|
2727
|
+
} else if (hasIncompleteOptions(accumulatedResponse)) {
|
|
2728
|
+
types.logger.debug(`[gemini] Warning: Incomplete options block detected but sending message anyway`);
|
|
2729
|
+
}
|
|
2730
|
+
const messageId = node_crypto.randomUUID();
|
|
2731
|
+
const messagePayload = {
|
|
2732
|
+
type: "message",
|
|
2733
|
+
message: finalMessageText,
|
|
2734
|
+
// Include options XML in text for mobile app
|
|
2735
|
+
id: messageId,
|
|
2736
|
+
...options.length > 0 && { options }
|
|
2737
|
+
};
|
|
2738
|
+
types.logger.debug(`[gemini] Sending complete message to mobile (length: ${finalMessageText.length}): ${finalMessageText.substring(0, 100)}...`);
|
|
2739
|
+
types.logger.debug(`[gemini] Full message payload:`, JSON.stringify(messagePayload, null, 2));
|
|
2740
|
+
session.sendCodexMessage(messagePayload);
|
|
2741
|
+
accumulatedResponse = "";
|
|
2742
|
+
isResponseInProgress = false;
|
|
2743
|
+
}
|
|
2744
|
+
} else if (msg.status === "error") {
|
|
2745
|
+
thinking = false;
|
|
2746
|
+
session.keepAlive(thinking, "remote");
|
|
2747
|
+
accumulatedResponse = "";
|
|
2748
|
+
isResponseInProgress = false;
|
|
2749
|
+
const errorMessage = stringifyErrorish(msg.detail || "Unknown error");
|
|
2750
|
+
messageBuffer.addMessage(`Error: ${errorMessage}`, "status");
|
|
2751
|
+
session.sendCodexMessage({
|
|
2752
|
+
type: "message",
|
|
2753
|
+
message: `Error: ${errorMessage}`,
|
|
2754
|
+
id: node_crypto.randomUUID()
|
|
2755
|
+
});
|
|
2756
|
+
}
|
|
2757
|
+
break;
|
|
2758
|
+
case "tool-call":
|
|
2759
|
+
const toolArgs = msg.args ? JSON.stringify(msg.args).substring(0, 100) : "";
|
|
2760
|
+
const isInvestigationTool = msg.toolName === "codebase_investigator" || typeof msg.toolName === "string" && msg.toolName.includes("investigator");
|
|
2761
|
+
types.logger.debug(`[gemini] \u{1F527} Tool call received: ${msg.toolName} (${msg.callId})${isInvestigationTool ? " [INVESTIGATION]" : ""}`);
|
|
2762
|
+
if (isInvestigationTool && msg.args && typeof msg.args === "object" && "objective" in msg.args) {
|
|
2763
|
+
types.logger.debug(`[gemini] \u{1F50D} Investigation objective: ${String(msg.args.objective).substring(0, 150)}...`);
|
|
2764
|
+
}
|
|
2765
|
+
messageBuffer.addMessage(`Executing: ${msg.toolName}${toolArgs ? ` ${toolArgs}${toolArgs.length >= 100 ? "..." : ""}` : ""}`, "tool");
|
|
2766
|
+
session.sendCodexMessage({
|
|
2767
|
+
type: "tool-call",
|
|
2768
|
+
name: msg.toolName,
|
|
2769
|
+
callId: msg.callId,
|
|
2770
|
+
input: msg.args,
|
|
2771
|
+
id: node_crypto.randomUUID()
|
|
2772
|
+
});
|
|
2773
|
+
break;
|
|
2774
|
+
case "tool-result":
|
|
2775
|
+
const isError = msg.result && typeof msg.result === "object" && "error" in msg.result;
|
|
2776
|
+
const resultText = typeof msg.result === "string" ? msg.result.substring(0, 200) : JSON.stringify(msg.result).substring(0, 200);
|
|
2777
|
+
const truncatedResult = resultText + (typeof msg.result === "string" && msg.result.length > 200 ? "..." : "");
|
|
2778
|
+
const resultSize = typeof msg.result === "string" ? msg.result.length : JSON.stringify(msg.result).length;
|
|
2779
|
+
types.logger.debug(`[gemini] ${isError ? "\u274C" : "\u2705"} Tool result received: ${msg.toolName} (${msg.callId}) - Size: ${resultSize} bytes${isError ? " [ERROR]" : ""}`);
|
|
2780
|
+
if (!isError) {
|
|
2781
|
+
diffProcessor.processToolResult(msg.toolName, msg.result, msg.callId);
|
|
2782
|
+
}
|
|
2783
|
+
if (isError) {
|
|
2784
|
+
const rawErr = msg.result.error ?? "Tool call failed";
|
|
2785
|
+
const errorMsg = stringifyErrorish(rawErr);
|
|
2786
|
+
types.logger.debug(`[gemini] \u274C Tool call error: ${errorMsg.substring(0, 300)}`);
|
|
2787
|
+
messageBuffer.addMessage(`Error: ${errorMsg}`, "status");
|
|
2788
|
+
} else {
|
|
2789
|
+
if (resultSize > 1e3) {
|
|
2790
|
+
types.logger.debug(`[gemini] \u2705 Large tool result (${resultSize} bytes) - first 200 chars: ${truncatedResult}`);
|
|
2791
|
+
}
|
|
2792
|
+
messageBuffer.addMessage(`Result: ${truncatedResult}`, "result");
|
|
2793
|
+
}
|
|
2794
|
+
session.sendCodexMessage({
|
|
2795
|
+
type: "tool-call-result",
|
|
2796
|
+
callId: msg.callId,
|
|
2797
|
+
output: msg.result,
|
|
2798
|
+
id: node_crypto.randomUUID()
|
|
2799
|
+
});
|
|
2800
|
+
collectScreenshotsForGate(msg.result);
|
|
2801
|
+
break;
|
|
2802
|
+
case "fs-edit":
|
|
2803
|
+
messageBuffer.addMessage(`File edit: ${msg.description}`, "tool");
|
|
2804
|
+
diffProcessor.processFsEdit(msg.path || "", msg.description, msg.diff);
|
|
2805
|
+
session.sendCodexMessage({
|
|
2806
|
+
type: "file-edit",
|
|
2807
|
+
description: msg.description,
|
|
2808
|
+
diff: msg.diff,
|
|
2809
|
+
path: msg.path,
|
|
2810
|
+
id: node_crypto.randomUUID()
|
|
2811
|
+
});
|
|
2812
|
+
break;
|
|
2813
|
+
default:
|
|
2814
|
+
if (msg.type === "token-count") {
|
|
2815
|
+
session.sendCodexMessage({
|
|
2816
|
+
type: "token_count",
|
|
2817
|
+
...msg,
|
|
2818
|
+
id: node_crypto.randomUUID()
|
|
2819
|
+
});
|
|
2820
|
+
}
|
|
2821
|
+
break;
|
|
2822
|
+
case "terminal-output":
|
|
2823
|
+
messageBuffer.addMessage(msg.data, "result");
|
|
2824
|
+
session.sendCodexMessage({
|
|
2825
|
+
type: "terminal-output",
|
|
2826
|
+
data: msg.data,
|
|
2827
|
+
id: node_crypto.randomUUID()
|
|
2828
|
+
});
|
|
2829
|
+
break;
|
|
2830
|
+
case "permission-request":
|
|
2831
|
+
session.sendCodexMessage({
|
|
2832
|
+
type: "permission-request",
|
|
2833
|
+
permissionId: msg.id,
|
|
2834
|
+
reason: msg.reason,
|
|
2835
|
+
payload: msg.payload,
|
|
2836
|
+
id: node_crypto.randomUUID()
|
|
2837
|
+
});
|
|
2838
|
+
break;
|
|
2839
|
+
case "exec-approval-request":
|
|
2840
|
+
const execApprovalMsg = msg;
|
|
2841
|
+
const callId = execApprovalMsg.call_id || execApprovalMsg.callId || node_crypto.randomUUID();
|
|
2842
|
+
const { call_id, type, ...inputs } = execApprovalMsg;
|
|
2843
|
+
types.logger.debug(`[gemini] Exec approval request received: ${callId}`);
|
|
2844
|
+
messageBuffer.addMessage(`Exec approval requested: ${callId}`, "tool");
|
|
2845
|
+
session.sendCodexMessage({
|
|
2846
|
+
type: "tool-call",
|
|
2847
|
+
name: "GeminiBash",
|
|
2848
|
+
// Similar to Codex's CodexBash
|
|
2849
|
+
callId,
|
|
2850
|
+
input: inputs,
|
|
2851
|
+
id: node_crypto.randomUUID()
|
|
2852
|
+
});
|
|
2853
|
+
break;
|
|
2854
|
+
case "patch-apply-begin":
|
|
2855
|
+
const patchBeginMsg = msg;
|
|
2856
|
+
const patchCallId = patchBeginMsg.call_id || patchBeginMsg.callId || node_crypto.randomUUID();
|
|
2857
|
+
const { call_id: patchCallIdVar, type: patchType, auto_approved, changes } = patchBeginMsg;
|
|
2858
|
+
const changeCount = changes ? Object.keys(changes).length : 0;
|
|
2859
|
+
const filesMsg = changeCount === 1 ? "1 file" : `${changeCount} files`;
|
|
2860
|
+
messageBuffer.addMessage(`Modifying ${filesMsg}...`, "tool");
|
|
2861
|
+
types.logger.debug(`[gemini] Patch apply begin: ${patchCallId}, files: ${changeCount}`);
|
|
2862
|
+
session.sendCodexMessage({
|
|
2863
|
+
type: "tool-call",
|
|
2864
|
+
name: "GeminiPatch",
|
|
2865
|
+
// Similar to Codex's CodexPatch
|
|
2866
|
+
callId: patchCallId,
|
|
2867
|
+
input: {
|
|
2868
|
+
auto_approved,
|
|
2869
|
+
changes
|
|
2870
|
+
},
|
|
2871
|
+
id: node_crypto.randomUUID()
|
|
2872
|
+
});
|
|
2873
|
+
break;
|
|
2874
|
+
case "patch-apply-end":
|
|
2875
|
+
const patchEndMsg = msg;
|
|
2876
|
+
const patchEndCallId = patchEndMsg.call_id || patchEndMsg.callId || node_crypto.randomUUID();
|
|
2877
|
+
const { call_id: patchEndCallIdVar, type: patchEndType, stdout, stderr, success } = patchEndMsg;
|
|
2878
|
+
if (success) {
|
|
2879
|
+
const message = stdout || "Files modified successfully";
|
|
2880
|
+
messageBuffer.addMessage(message.substring(0, 200), "result");
|
|
2881
|
+
} else {
|
|
2882
|
+
const errorMsg = stderr || "Failed to modify files";
|
|
2883
|
+
messageBuffer.addMessage(`Error: ${errorMsg.substring(0, 200)}`, "result");
|
|
2884
|
+
}
|
|
2885
|
+
types.logger.debug(`[gemini] Patch apply end: ${patchEndCallId}, success: ${success}`);
|
|
2886
|
+
session.sendCodexMessage({
|
|
2887
|
+
type: "tool-call-result",
|
|
2888
|
+
callId: patchEndCallId,
|
|
2889
|
+
output: {
|
|
2890
|
+
stdout,
|
|
2891
|
+
stderr,
|
|
2892
|
+
success
|
|
2893
|
+
},
|
|
2894
|
+
id: node_crypto.randomUUID()
|
|
2895
|
+
});
|
|
2896
|
+
break;
|
|
2897
|
+
case "event":
|
|
2898
|
+
if (msg.name === "thinking") {
|
|
2899
|
+
const thinkingPayload = msg.payload;
|
|
2900
|
+
const thinkingText = thinkingPayload && typeof thinkingPayload === "object" && "text" in thinkingPayload ? String(thinkingPayload.text || "") : "";
|
|
2901
|
+
if (thinkingText) {
|
|
2902
|
+
reasoningProcessor.processChunk(thinkingText);
|
|
2903
|
+
types.logger.debug(`[gemini] \u{1F4AD} Thinking chunk received: ${thinkingText.length} chars - Preview: ${thinkingText.substring(0, 100)}...`);
|
|
2904
|
+
if (!thinkingText.startsWith("**")) {
|
|
2905
|
+
const thinkingPreview = thinkingText.substring(0, 100);
|
|
2906
|
+
messageBuffer.updateLastMessage(`[Thinking] ${thinkingPreview}...`, "system");
|
|
2907
|
+
}
|
|
2908
|
+
}
|
|
2909
|
+
session.sendCodexMessage({
|
|
2910
|
+
type: "thinking",
|
|
2911
|
+
text: thinkingText,
|
|
2912
|
+
id: node_crypto.randomUUID()
|
|
2913
|
+
});
|
|
2914
|
+
}
|
|
2915
|
+
break;
|
|
2916
|
+
}
|
|
2917
|
+
});
|
|
2918
|
+
}
|
|
2919
|
+
let first = true;
|
|
2920
|
+
let didAuthRefreshRetry = false;
|
|
2921
|
+
try {
|
|
2922
|
+
let currentModeHash = null;
|
|
2923
|
+
let pending = null;
|
|
2924
|
+
mainLoop: while (!shouldExit) {
|
|
2925
|
+
let message = pending;
|
|
2926
|
+
pending = null;
|
|
2927
|
+
if (!message) {
|
|
2928
|
+
types.logger.debug("[gemini] Main loop: waiting for messages from queue...");
|
|
2929
|
+
const waitSignal = abortController.signal;
|
|
2930
|
+
const batch = await messageQueue.waitForMessagesAndGetAsString(waitSignal);
|
|
2931
|
+
if (!batch) {
|
|
2932
|
+
if (waitSignal.aborted && !shouldExit) {
|
|
2933
|
+
types.logger.debug("[gemini] Main loop: wait aborted, continuing...");
|
|
2934
|
+
continue;
|
|
2935
|
+
}
|
|
2936
|
+
types.logger.debug("[gemini] Main loop: no batch received, breaking...");
|
|
2937
|
+
break;
|
|
2938
|
+
}
|
|
2939
|
+
types.logger.debug(`[gemini] Main loop: received message from queue (length: ${batch.message.length})`);
|
|
2940
|
+
message = batch;
|
|
2941
|
+
}
|
|
2942
|
+
if (!message) {
|
|
2943
|
+
break;
|
|
2944
|
+
}
|
|
2945
|
+
if (wasSessionCreated && currentModeHash && message.hash !== currentModeHash) {
|
|
2946
|
+
types.logger.debug("[Gemini] Mode changed \u2013 restarting Gemini session");
|
|
2947
|
+
messageBuffer.addMessage("\u2550".repeat(40), "status");
|
|
2948
|
+
messageBuffer.addMessage("Starting new Gemini session (mode changed)...", "status");
|
|
2949
|
+
permissionHandler.reset();
|
|
2950
|
+
reasoningProcessor.abort();
|
|
2951
|
+
if (geminiBackend) {
|
|
2952
|
+
await geminiBackend.dispose();
|
|
2953
|
+
geminiBackend = null;
|
|
2954
|
+
}
|
|
2955
|
+
await refreshGeminiCloudToken("mode-change");
|
|
2956
|
+
const modelToUse = message.mode?.model === void 0 ? void 0 : message.mode.model || null;
|
|
2957
|
+
geminiBackend = createGeminiBackend({
|
|
2958
|
+
cwd: process.cwd(),
|
|
2959
|
+
mcpServers,
|
|
2960
|
+
permissionHandler,
|
|
2961
|
+
cloudToken,
|
|
2962
|
+
// Pass model from message - if undefined, will use local config/env/default
|
|
2963
|
+
// If explicitly null, will skip local config and use env/default
|
|
2964
|
+
model: modelToUse
|
|
2965
|
+
});
|
|
2966
|
+
setupGeminiMessageHandler(geminiBackend);
|
|
2967
|
+
const localConfigForModel = readGeminiLocalConfig();
|
|
2968
|
+
const actualModel = determineGeminiModel(modelToUse, localConfigForModel);
|
|
2969
|
+
types.logger.debug(`[gemini] Model change - modelToUse=${modelToUse}, actualModel=${actualModel}`);
|
|
2970
|
+
types.logger.debug("[gemini] Starting new ACP session with model:", actualModel);
|
|
2971
|
+
const { sessionId } = await geminiBackend.startSession();
|
|
2972
|
+
acpSessionId = sessionId;
|
|
2973
|
+
types.logger.debug(`[gemini] New ACP session started: ${acpSessionId}`);
|
|
2974
|
+
types.logger.debug(`[gemini] Calling updateDisplayedModel with: ${actualModel}`);
|
|
2975
|
+
updateDisplayedModel(actualModel, false);
|
|
2976
|
+
updatePermissionMode(message.mode.permissionMode);
|
|
2977
|
+
wasSessionCreated = true;
|
|
2978
|
+
currentModeHash = message.hash;
|
|
2979
|
+
first = false;
|
|
2980
|
+
}
|
|
2981
|
+
currentModeHash = message.hash;
|
|
2982
|
+
const userMessageToShow = message.mode?.originalUserMessage || message.message;
|
|
2983
|
+
messageBuffer.addMessage(userMessageToShow, "user");
|
|
2984
|
+
let retryThisTurn = false;
|
|
2985
|
+
let skipAutoFinalize = false;
|
|
2986
|
+
try {
|
|
2987
|
+
if (first || !wasSessionCreated) {
|
|
2988
|
+
if (!geminiBackend) {
|
|
2989
|
+
await refreshGeminiCloudToken("before-backend-create");
|
|
2990
|
+
const modelToUse = message.mode?.model === void 0 ? void 0 : message.mode.model || null;
|
|
2991
|
+
geminiBackend = createGeminiBackend({
|
|
2992
|
+
cwd: process.cwd(),
|
|
2993
|
+
mcpServers,
|
|
2994
|
+
permissionHandler,
|
|
2995
|
+
cloudToken,
|
|
2996
|
+
// Pass model from message - if undefined, will use local config/env/default
|
|
2997
|
+
// If explicitly null, will skip local config and use env/default
|
|
2998
|
+
model: modelToUse
|
|
2999
|
+
});
|
|
3000
|
+
setupGeminiMessageHandler(geminiBackend);
|
|
3001
|
+
const localConfigForModel = readGeminiLocalConfig();
|
|
3002
|
+
const actualModel = determineGeminiModel(modelToUse, localConfigForModel);
|
|
3003
|
+
const modelSource = modelToUse !== void 0 ? "message" : process.env[GEMINI_MODEL_ENV] ? "env-var" : localConfigForModel.model ? "local-config" : "default";
|
|
3004
|
+
types.logger.debug(`[gemini] Backend created, model will be: ${actualModel} (from ${modelSource})`);
|
|
3005
|
+
types.logger.debug(`[gemini] Calling updateDisplayedModel with: ${actualModel}`);
|
|
3006
|
+
updateDisplayedModel(actualModel, false);
|
|
3007
|
+
}
|
|
3008
|
+
if (!acpSessionId) {
|
|
3009
|
+
types.logger.debug("[gemini] Starting ACP session...");
|
|
3010
|
+
updatePermissionMode(message.mode.permissionMode);
|
|
3011
|
+
const { sessionId } = await geminiBackend.startSession();
|
|
3012
|
+
acpSessionId = sessionId;
|
|
3013
|
+
types.logger.debug(`[gemini] ACP session started: ${acpSessionId}`);
|
|
3014
|
+
wasSessionCreated = true;
|
|
3015
|
+
currentModeHash = message.hash;
|
|
3016
|
+
types.logger.debug(`[gemini] Displaying model in UI: ${displayedModel || "gemini-2.5-pro"}, displayedModel: ${displayedModel}`);
|
|
3017
|
+
}
|
|
3018
|
+
}
|
|
3019
|
+
if (!acpSessionId) {
|
|
3020
|
+
throw new Error("ACP session not started");
|
|
3021
|
+
}
|
|
3022
|
+
accumulatedResponse = "";
|
|
3023
|
+
isResponseInProgress = false;
|
|
3024
|
+
resetScreenshotGateForTurn();
|
|
3025
|
+
if (!geminiBackend || !acpSessionId) {
|
|
3026
|
+
throw new Error("Gemini backend or session not initialized");
|
|
3027
|
+
}
|
|
3028
|
+
const promptToSend = message.message;
|
|
3029
|
+
const parsedPrompt = index.extractUserImagesMarker(promptToSend);
|
|
3030
|
+
const promptText = parsedPrompt.text;
|
|
3031
|
+
const promptImages = parsedPrompt.hasImages ? index.getLatestUserImages().slice(0, parsedPrompt.count) : [];
|
|
3032
|
+
if (promptImages.length > 0) {
|
|
3033
|
+
const blocks = [
|
|
3034
|
+
{ type: "text", text: promptText },
|
|
3035
|
+
...promptImages.map((img) => ({ type: "image", data: img.base64, mimeType: img.mimeType }))
|
|
3036
|
+
];
|
|
3037
|
+
types.logger.debug(`[gemini] Sending multimodal prompt blocks (textLength=${promptText.length}, images=${promptImages.length})`);
|
|
3038
|
+
await geminiBackend.sendPrompt(acpSessionId, blocks);
|
|
3039
|
+
} else {
|
|
3040
|
+
types.logger.debug(`[gemini] Sending prompt to Gemini (length: ${promptText.length}): ${promptText.substring(0, 100)}...`);
|
|
3041
|
+
types.logger.debug(`[gemini] Full prompt: ${promptText}`);
|
|
3042
|
+
await geminiBackend.sendPrompt(acpSessionId, promptText);
|
|
3043
|
+
}
|
|
3044
|
+
types.logger.debug("[gemini] Prompt sent successfully");
|
|
3045
|
+
if (!screenshotGate.inAutoReview && screenshotGate.paths.length > 0) {
|
|
3046
|
+
screenshotGate.inAutoReview = true;
|
|
3047
|
+
messageBuffer.addMessage("Auto-reviewing screenshots\u2026", "status");
|
|
3048
|
+
const blocks = await buildScreenshotReviewBlocks(screenshotGate.paths);
|
|
3049
|
+
await geminiBackend.sendPrompt(acpSessionId, blocks);
|
|
3050
|
+
screenshotGate.inAutoReview = false;
|
|
3051
|
+
}
|
|
3052
|
+
if (first) {
|
|
3053
|
+
first = false;
|
|
3054
|
+
}
|
|
3055
|
+
} catch (error) {
|
|
3056
|
+
types.logger.debug("[gemini] Error in gemini session:", error);
|
|
3057
|
+
const isAbortError = error instanceof Error && error.name === "AbortError";
|
|
3058
|
+
if (isAbortError) {
|
|
3059
|
+
skipAutoFinalize = true;
|
|
3060
|
+
messageBuffer.addMessage("Aborted by user", "status");
|
|
3061
|
+
session.sendSessionEvent({ type: "message", message: "Aborted by user" });
|
|
3062
|
+
} else {
|
|
3063
|
+
const rawErrorString = error instanceof Error ? error.message : String(error);
|
|
3064
|
+
const looksLikeAuthOrQuotaError = /unauthenticated|unauthorized|invalid (api )?key|api key not valid|permission denied|forbidden|expired|quota|usage limit|rate limit|resource exhausted|resource_exhausted|status 401|status 403|status 429|\b401\b|\b403\b|\b429\b/i.test(rawErrorString);
|
|
3065
|
+
if (first && looksLikeAuthOrQuotaError && !didAuthRefreshRetry) {
|
|
3066
|
+
didAuthRefreshRetry = true;
|
|
3067
|
+
await refreshGeminiCloudToken("sendPrompt:auth-or-quota-error");
|
|
3068
|
+
if (geminiBackend) {
|
|
3069
|
+
try {
|
|
3070
|
+
await geminiBackend.dispose();
|
|
3071
|
+
} catch (disposeError) {
|
|
3072
|
+
types.logger.debug("[Gemini] Failed to dispose backend during auth-refresh retry:", disposeError);
|
|
3073
|
+
}
|
|
3074
|
+
geminiBackend = null;
|
|
3075
|
+
}
|
|
3076
|
+
acpSessionId = null;
|
|
3077
|
+
wasSessionCreated = false;
|
|
3078
|
+
messageBuffer.removeLastMessage("user");
|
|
3079
|
+
messageBuffer.addMessage("Restarting Gemini to pick up updated credentials\u2026", "status");
|
|
3080
|
+
retryThisTurn = true;
|
|
3081
|
+
pending = message;
|
|
3082
|
+
continue mainLoop;
|
|
3083
|
+
}
|
|
3084
|
+
let errorMsg = "Process error occurred";
|
|
3085
|
+
if (typeof error === "object" && error !== null) {
|
|
3086
|
+
const errObj = error;
|
|
3087
|
+
const errorDetails = errObj.data?.details || errObj.details || "";
|
|
3088
|
+
const errorCode = errObj.code || errObj.status || errObj.response?.status;
|
|
3089
|
+
const errorMessage = errObj.message || errObj.error?.message || "";
|
|
3090
|
+
const errorString = String(error);
|
|
3091
|
+
if (errorCode === 404 || errorDetails.includes("notFound") || errorDetails.includes("404") || errorMessage.includes("not found") || errorMessage.includes("404")) {
|
|
3092
|
+
const currentModel2 = displayedModel || "gemini-2.5-pro";
|
|
3093
|
+
errorMsg = `Model "${currentModel2}" not found. Available models: gemini-2.5-pro, gemini-2.5-flash, gemini-2.5-flash-lite`;
|
|
3094
|
+
} else if (errorCode === 429 || errorDetails.includes("429") || errorMessage.includes("429") || errorString.includes("429") || errorDetails.includes("rateLimitExceeded") || errorDetails.includes("RESOURCE_EXHAUSTED") || errorMessage.includes("Rate limit exceeded") || errorMessage.includes("Resource exhausted") || errorString.includes("rateLimitExceeded") || errorString.includes("RESOURCE_EXHAUSTED")) {
|
|
3095
|
+
errorMsg = "Gemini API rate limit exceeded. Please wait a moment and try again. The API will retry automatically.";
|
|
3096
|
+
} else if (errorDetails.includes("quota") || errorMessage.includes("quota") || errorString.includes("quota")) {
|
|
3097
|
+
errorMsg = "Gemini API daily quota exceeded. Please wait until quota resets or use a paid API key.";
|
|
3098
|
+
} else if (Object.keys(error).length === 0) {
|
|
3099
|
+
errorMsg = 'Failed to start Gemini. Is "gemini" CLI installed? Run: npm install -g @google/gemini-cli';
|
|
3100
|
+
} else if (errObj.message || errorMessage) {
|
|
3101
|
+
errorMsg = errorDetails || errorMessage || errObj.message;
|
|
3102
|
+
}
|
|
3103
|
+
} else if (error instanceof Error) {
|
|
3104
|
+
errorMsg = error.message;
|
|
3105
|
+
}
|
|
3106
|
+
messageBuffer.addMessage(errorMsg, "status");
|
|
3107
|
+
session.sendCodexMessage({
|
|
3108
|
+
type: "message",
|
|
3109
|
+
message: errorMsg,
|
|
3110
|
+
id: node_crypto.randomUUID()
|
|
3111
|
+
});
|
|
3112
|
+
}
|
|
3113
|
+
} finally {
|
|
3114
|
+
permissionHandler.reset();
|
|
3115
|
+
reasoningProcessor.abort();
|
|
3116
|
+
diffProcessor.reset();
|
|
3117
|
+
thinking = false;
|
|
3118
|
+
session.keepAlive(thinking, "remote");
|
|
3119
|
+
if (!retryThisTurn) {
|
|
3120
|
+
if (!skipAutoFinalize) {
|
|
3121
|
+
try {
|
|
3122
|
+
const finalizeRes = await index.autoFinalizeCoordinationWorkItem({
|
|
3123
|
+
token: session.getAuthToken(),
|
|
3124
|
+
cwd: process.cwd(),
|
|
3125
|
+
summary: userMessageToShow
|
|
3126
|
+
});
|
|
3127
|
+
if (!finalizeRes.ok) {
|
|
3128
|
+
const msg = `Auto-finalize failed: ${finalizeRes.error}`;
|
|
3129
|
+
messageBuffer.addMessage(msg, "status");
|
|
3130
|
+
session.sendCodexMessage({
|
|
3131
|
+
type: "message",
|
|
3132
|
+
message: msg,
|
|
3133
|
+
id: node_crypto.randomUUID()
|
|
3134
|
+
});
|
|
3135
|
+
}
|
|
3136
|
+
} catch (err) {
|
|
3137
|
+
const msg = `Auto-finalize failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
3138
|
+
messageBuffer.addMessage(msg, "status");
|
|
3139
|
+
session.sendCodexMessage({
|
|
3140
|
+
type: "message",
|
|
3141
|
+
message: msg,
|
|
3142
|
+
id: node_crypto.randomUUID()
|
|
3143
|
+
});
|
|
3144
|
+
}
|
|
3145
|
+
}
|
|
3146
|
+
emitReadyIfIdle();
|
|
3147
|
+
}
|
|
3148
|
+
types.logger.debug(`[gemini] Main loop: turn completed, continuing to next iteration (queue size: ${messageQueue.size()})`);
|
|
3149
|
+
}
|
|
3150
|
+
}
|
|
3151
|
+
} finally {
|
|
3152
|
+
types.logger.debug("[gemini]: Final cleanup start");
|
|
3153
|
+
try {
|
|
3154
|
+
session.sendSessionDeath();
|
|
3155
|
+
await session.flush();
|
|
3156
|
+
await session.close();
|
|
3157
|
+
} catch (e) {
|
|
3158
|
+
types.logger.debug("[gemini]: Error while closing session", e);
|
|
3159
|
+
}
|
|
3160
|
+
if (geminiBackend) {
|
|
3161
|
+
await geminiBackend.dispose();
|
|
3162
|
+
}
|
|
3163
|
+
flockbayServer.stop();
|
|
3164
|
+
if (process.stdin.isTTY) {
|
|
3165
|
+
try {
|
|
3166
|
+
process.stdin.setRawMode(false);
|
|
3167
|
+
} catch {
|
|
3168
|
+
}
|
|
3169
|
+
}
|
|
3170
|
+
if (hasTTY) {
|
|
3171
|
+
try {
|
|
3172
|
+
process.stdin.pause();
|
|
3173
|
+
} catch {
|
|
3174
|
+
}
|
|
3175
|
+
}
|
|
3176
|
+
clearInterval(keepAliveInterval);
|
|
3177
|
+
if (inkInstance) {
|
|
3178
|
+
inkInstance.unmount();
|
|
3179
|
+
}
|
|
3180
|
+
messageBuffer.clear();
|
|
3181
|
+
types.logger.debug("[gemini]: Final cleanup completed");
|
|
3182
|
+
}
|
|
3183
|
+
}
|
|
3184
|
+
|
|
3185
|
+
exports.runGemini = runGemini;
|