hankweave 0.5.7 → 0.6.2
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 +12 -11
- package/dist/base-process-manager.d.ts +30 -0
- package/dist/budget.d.ts +315 -0
- package/dist/checkpoint-git.d.ts +98 -0
- package/dist/claude-agent-sdk-manager.d.ts +144 -0
- package/dist/claude-log-parser.d.ts +63 -0
- package/dist/claude-runtime-extractor.d.ts +73 -0
- package/dist/codex-runtime-extractor.d.ts +107 -0
- package/dist/codon-runner.d.ts +278 -0
- package/dist/config-validation/model-validator.d.ts +16 -0
- package/dist/config-validation/sentinel.schema.d.ts +6967 -0
- package/dist/config.d.ts +40815 -0
- package/dist/cost-tracker.d.ts +72 -0
- package/dist/execution-planner.d.ts +62 -0
- package/dist/execution-thread.d.ts +71 -0
- package/dist/exports/schemas.d.ts +9 -0
- package/dist/exports/schemas.js +1019 -0
- package/dist/exports/types.d.ts +15 -0
- package/dist/exports/types.js +60 -0
- package/dist/file-resolver.d.ts +33 -0
- package/dist/index.js +380 -293
- package/dist/index.js.map +33 -29
- package/dist/llm/llm-provider-registry.d.ts +207 -0
- package/dist/llm/models-dev-schema.d.ts +679 -0
- package/dist/llm/provider-config.d.ts +30 -0
- package/dist/prompt-builder.d.ts +75 -0
- package/dist/prompt-frontmatter.d.ts +61 -0
- package/dist/replay-process-manager.d.ts +82 -0
- package/dist/runtime-extractor-base.d.ts +120 -0
- package/dist/schemas/event-schemas.d.ts +8389 -0
- package/dist/schemas/websocket-log-schemas.d.ts +4502 -0
- package/dist/shim-process-manager.d.ts +98 -0
- package/dist/shim-runtime-extractor.d.ts +51 -0
- package/dist/shims/codex/README.md +129 -0
- package/dist/shims/codex/THIRDPARTY.md +18 -0
- package/dist/shims/codex/VERSION +1 -0
- package/dist/shims/codex/common/package.json +24 -0
- package/dist/shims/codex/index.js +1154 -970
- package/dist/shims/codex/package.json +46 -0
- package/dist/shims/codex/tsup.config.ts +16 -0
- package/dist/shims/gemini/README.md +59 -0
- package/dist/shims/gemini/THIRDPARTY.md +32 -0
- package/dist/shims/gemini/VERSION +1 -0
- package/dist/shims/gemini/common/package.json +24 -0
- package/dist/shims/gemini/index.js +1359 -30
- package/dist/shims/gemini/package.json +37 -0
- package/dist/shims/opencode/README.md +82 -0
- package/dist/shims/opencode/THIRDPARTY.md +32 -0
- package/dist/shims/opencode/VERSION +1 -0
- package/dist/shims/opencode/common/package.json +24 -0
- package/dist/shims/opencode/index.js +1476 -0
- package/dist/shims/opencode/package.json +38 -0
- package/dist/shims/pi/README.md +87 -0
- package/dist/shims/pi/THIRDPARTY.md +24 -0
- package/dist/shims/pi/VERSION +1 -0
- package/dist/shims/pi/common/package.json +24 -0
- package/dist/shims/pi/index.js +249832 -0
- package/dist/shims/pi/package.json +53 -0
- package/dist/state-manager.d.ts +161 -0
- package/dist/state-transition-guards.d.ts +37 -0
- package/dist/telemetry/telemetry-types.d.ts +206 -0
- package/dist/typed-event-emitter.d.ts +57 -0
- package/dist/types/branded-types.d.ts +15 -0
- package/dist/types/budget-types.d.ts +82 -0
- package/dist/types/claude-session-schema.d.ts +2430 -0
- package/dist/types/error-types.d.ts +44 -0
- package/dist/types/input-ai-types.d.ts +1070 -0
- package/dist/types/llm-call-types.d.ts +3829 -0
- package/dist/types/sentinel-types.d.ts +66 -0
- package/dist/types/state-types.d.ts +1099 -0
- package/dist/types/tool-types.d.ts +86 -0
- package/dist/types/types.d.ts +367 -0
- package/dist/types/websocket-log-types.d.ts +7 -0
- package/dist/utils.d.ts +452 -0
- package/package.json +15 -2
- package/schemas/hank.schema.json +158 -3
- package/schemas/hankweave.schema.json +17 -1
- package/shims/codex/index.js +0 -1583
- package/shims/gemini/index.js +0 -31
|
@@ -1,31 +1,1360 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import process3 from "node:process";
|
|
5
|
+
|
|
6
|
+
// src/args.ts
|
|
7
|
+
function parseArgs(argv) {
|
|
8
|
+
const args = {
|
|
9
|
+
model: process.env.MODEL || "gemini-2.5-flash",
|
|
10
|
+
verbose: false,
|
|
11
|
+
idleTimeout: 120,
|
|
12
|
+
sandbox: "none",
|
|
13
|
+
selfTest: false,
|
|
14
|
+
version: false,
|
|
15
|
+
help: false
|
|
16
|
+
};
|
|
17
|
+
const takeValue = (index, current) => {
|
|
18
|
+
if (current.includes("=")) {
|
|
19
|
+
const [, value] = current.split(/=(.*)/s, 2);
|
|
20
|
+
return [value, index];
|
|
21
|
+
}
|
|
22
|
+
return [argv[index + 1], index + 1];
|
|
23
|
+
};
|
|
24
|
+
for (let i = 0; i < argv.length; i++) {
|
|
25
|
+
const raw = argv[i] ?? "";
|
|
26
|
+
const key = raw.includes("=") ? raw.split("=", 1)[0] : raw;
|
|
27
|
+
switch (key) {
|
|
28
|
+
case "-p":
|
|
29
|
+
break;
|
|
30
|
+
case "--model": {
|
|
31
|
+
const [value, nextIndex] = takeValue(i, raw);
|
|
32
|
+
if (value) {
|
|
33
|
+
args.model = value;
|
|
34
|
+
i = nextIndex;
|
|
35
|
+
}
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
case "--resume": {
|
|
39
|
+
const [value, nextIndex] = takeValue(i, raw);
|
|
40
|
+
if (value) {
|
|
41
|
+
args.resume = value;
|
|
42
|
+
i = nextIndex;
|
|
43
|
+
}
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
case "--verbose":
|
|
47
|
+
args.verbose = true;
|
|
48
|
+
break;
|
|
49
|
+
case "--append-system-prompt": {
|
|
50
|
+
const [value, nextIndex] = takeValue(i, raw);
|
|
51
|
+
if (value !== void 0) {
|
|
52
|
+
args.appendSystemPrompt = value;
|
|
53
|
+
i = nextIndex;
|
|
54
|
+
}
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
case "--debug-dir": {
|
|
58
|
+
const [value, nextIndex] = takeValue(i, raw);
|
|
59
|
+
if (value) {
|
|
60
|
+
args.debugDir = value;
|
|
61
|
+
i = nextIndex;
|
|
62
|
+
}
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
case "--idle-timeout": {
|
|
66
|
+
const [value, nextIndex] = takeValue(i, raw);
|
|
67
|
+
const parsed = Number(value);
|
|
68
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
69
|
+
throw new Error("Invalid --idle-timeout value: must be a positive number");
|
|
70
|
+
}
|
|
71
|
+
args.idleTimeout = parsed;
|
|
72
|
+
i = nextIndex;
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
case "--sandbox": {
|
|
76
|
+
const [value, nextIndex] = takeValue(i, raw);
|
|
77
|
+
if (value === "none" || value === "standard" || value === "strict") {
|
|
78
|
+
args.sandbox = value;
|
|
79
|
+
i = nextIndex;
|
|
80
|
+
} else {
|
|
81
|
+
throw new Error("Invalid --sandbox value: must be one of none, standard, strict");
|
|
82
|
+
}
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
case "--self-test":
|
|
86
|
+
args.selfTest = true;
|
|
87
|
+
break;
|
|
88
|
+
case "--version":
|
|
89
|
+
args.version = true;
|
|
90
|
+
break;
|
|
91
|
+
case "--help":
|
|
92
|
+
args.help = true;
|
|
93
|
+
break;
|
|
94
|
+
default:
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return args;
|
|
99
|
+
}
|
|
100
|
+
function printHelp() {
|
|
101
|
+
process.stdout.write(`Gemini CLI shim
|
|
102
|
+
|
|
103
|
+
Usage:
|
|
104
|
+
gemini-cli-shim --model <model> [options]
|
|
105
|
+
|
|
106
|
+
Options:
|
|
107
|
+
-p Prompt via stdin (accepted, optional)
|
|
108
|
+
--model <model> Gemini model or alias (for example: gemini-2.5-flash, google/gemini-2.5-pro, flash, pro)
|
|
109
|
+
--resume <session_id> Resume an existing Gemini session UUID
|
|
110
|
+
--verbose Verbose stderr logging
|
|
111
|
+
--append-system-prompt <text> Extra system instructions appended to the internal shim prompt
|
|
112
|
+
--idle-timeout <seconds> Baseline idle timeout before work starts (default: 120)
|
|
113
|
+
--debug-dir <path> Write raw debug logs into this directory
|
|
114
|
+
--sandbox <level> none | standard | strict
|
|
115
|
+
--self-test Verify environment and print JSON
|
|
116
|
+
--version Print version
|
|
117
|
+
--help Print help
|
|
118
|
+
`);
|
|
119
|
+
}
|
|
120
|
+
async function readStdinTrimmed() {
|
|
121
|
+
const chunks = [];
|
|
122
|
+
for await (const chunk of process.stdin) {
|
|
123
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
124
|
+
}
|
|
125
|
+
return Buffer.concat(chunks).toString("utf8").trim();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// node_modules/@shims/common/src/tools.ts
|
|
129
|
+
var STANDARD_TOOLS = ["Read", "Write", "Edit", "Bash", "Glob", "Grep", "LS"];
|
|
130
|
+
|
|
131
|
+
// src/constants.ts
|
|
132
|
+
var SHIM_NAME = "gemini-cli-shim";
|
|
133
|
+
var SHIM_VERSION = "0.1.0";
|
|
134
|
+
var KNOWN_TOOLS = [...STANDARD_TOOLS];
|
|
135
|
+
var MAX_SILENT_SUCCESS_RETRIES = 1;
|
|
136
|
+
var MAX_INVALID_JSON_REPAIRS = 2;
|
|
137
|
+
|
|
138
|
+
// src/shim.ts
|
|
139
|
+
import { spawn } from "node:child_process";
|
|
140
|
+
import path4 from "node:path";
|
|
141
|
+
import process2 from "node:process";
|
|
142
|
+
import { createInterface } from "node:readline";
|
|
143
|
+
|
|
144
|
+
// node_modules/@shims/common/src/timeout.ts
|
|
145
|
+
var IdleTimeoutError = class extends Error {
|
|
146
|
+
timeoutMs;
|
|
147
|
+
constructor(timeoutMs) {
|
|
148
|
+
super(`Idle timeout: no events received for ${timeoutMs}ms`);
|
|
149
|
+
this.name = "IdleTimeoutError";
|
|
150
|
+
this.timeoutMs = timeoutMs;
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
var BusyStepTimeoutError = class extends Error {
|
|
154
|
+
timeoutMs;
|
|
155
|
+
constructor(timeoutMs) {
|
|
156
|
+
super(`Busy-step stall timeout: no observed activity for ${timeoutMs}ms`);
|
|
157
|
+
this.name = "BusyStepTimeoutError";
|
|
158
|
+
this.timeoutMs = timeoutMs;
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
async function* withAdaptiveTimeout(events, options) {
|
|
162
|
+
const iterator = events[Symbol.asyncIterator]();
|
|
163
|
+
let state = "idle";
|
|
164
|
+
const busyTimeoutMs = Math.max(
|
|
165
|
+
options.busyTimeoutMs ?? options.idleTimeoutMs,
|
|
166
|
+
options.idleTimeoutMs
|
|
167
|
+
);
|
|
168
|
+
const controller = {
|
|
169
|
+
markBusy() {
|
|
170
|
+
state = "busy";
|
|
171
|
+
},
|
|
172
|
+
markIdle() {
|
|
173
|
+
state = "idle";
|
|
174
|
+
},
|
|
175
|
+
get state() {
|
|
176
|
+
return state;
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
try {
|
|
180
|
+
while (true) {
|
|
181
|
+
const timeoutMs = state === "busy" ? busyTimeoutMs : options.idleTimeoutMs;
|
|
182
|
+
let timeoutId;
|
|
183
|
+
try {
|
|
184
|
+
const result = await Promise.race([
|
|
185
|
+
iterator.next(),
|
|
186
|
+
new Promise((_, reject) => {
|
|
187
|
+
timeoutId = setTimeout(() => {
|
|
188
|
+
reject(
|
|
189
|
+
state === "busy" ? new BusyStepTimeoutError(timeoutMs) : new IdleTimeoutError(timeoutMs)
|
|
190
|
+
);
|
|
191
|
+
}, timeoutMs);
|
|
192
|
+
})
|
|
193
|
+
]);
|
|
194
|
+
if (result.done) break;
|
|
195
|
+
options.onEvent?.(result.value, controller);
|
|
196
|
+
yield result.value;
|
|
197
|
+
} finally {
|
|
198
|
+
clearTimeout(timeoutId);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
} finally {
|
|
202
|
+
void iterator.return?.();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// src/debug.ts
|
|
207
|
+
import { appendFile as appendFile2, writeFile } from "node:fs/promises";
|
|
208
|
+
import path from "node:path";
|
|
209
|
+
|
|
210
|
+
// src/filesystem.ts
|
|
211
|
+
import { appendFile, access, mkdir, readFile } from "node:fs/promises";
|
|
212
|
+
import { constants } from "node:fs";
|
|
213
|
+
async function pathExists(target) {
|
|
214
|
+
try {
|
|
215
|
+
await access(target, constants.F_OK);
|
|
216
|
+
return true;
|
|
217
|
+
} catch {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
async function ensureDir(dir) {
|
|
222
|
+
await mkdir(dir, { recursive: true });
|
|
223
|
+
}
|
|
224
|
+
async function writeJsonDebugLine(filePath, event) {
|
|
225
|
+
await appendFile(filePath, JSON.stringify(event) + "\n", "utf8");
|
|
226
|
+
}
|
|
227
|
+
async function sleep(ms) {
|
|
228
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
229
|
+
}
|
|
230
|
+
async function findInvalidJsonFiles(filePaths) {
|
|
231
|
+
const invalid = [];
|
|
232
|
+
for (const filePath of filePaths) {
|
|
233
|
+
if (!filePath.toLowerCase().endsWith(".json")) {
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
try {
|
|
237
|
+
const content = await readFile(filePath, "utf8");
|
|
238
|
+
JSON.parse(content);
|
|
239
|
+
} catch (error) {
|
|
240
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
241
|
+
invalid.push({ filePath, error: message });
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return invalid;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// src/debug.ts
|
|
248
|
+
async function bindDebugSession(debug, sessionId) {
|
|
249
|
+
if (!debug.debugDir || debug.sessionId) return;
|
|
250
|
+
debug.sessionId = sessionId;
|
|
251
|
+
debug.jsonlPath = path.join(debug.debugDir, `session-${sessionId}.raw.jsonl`);
|
|
252
|
+
debug.logPath = path.join(debug.debugDir, `session-${sessionId}.raw.log`);
|
|
253
|
+
await ensureDir(debug.debugDir);
|
|
254
|
+
for (const event of debug.bufferedEvents) {
|
|
255
|
+
await writeJsonDebugLine(debug.jsonlPath, event);
|
|
256
|
+
}
|
|
257
|
+
debug.bufferedEvents = [];
|
|
258
|
+
if (debug.bufferedStderr.length > 0) {
|
|
259
|
+
await ensureLogFile(debug.logPath, debug.bufferedStderr.join(""));
|
|
260
|
+
debug.bufferedStderr = [];
|
|
261
|
+
} else {
|
|
262
|
+
await ensureLogFile(debug.logPath, "");
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
async function captureStderr(debug, text) {
|
|
266
|
+
if (!debug.debugDir) return;
|
|
267
|
+
if (debug.logPath) {
|
|
268
|
+
await ensureLogFile(debug.logPath, text, true);
|
|
269
|
+
} else {
|
|
270
|
+
debug.bufferedStderr.push(text);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
async function writeUnknownLog(debug, text) {
|
|
274
|
+
if (!debug.debugDir) return;
|
|
275
|
+
const unknownLog = path.join(debug.debugDir, "session-unknown.raw.log");
|
|
276
|
+
await ensureLogFile(unknownLog, text, true);
|
|
277
|
+
}
|
|
278
|
+
async function ensureLogFile(filePath, text, appendOnly = false) {
|
|
279
|
+
if (!await pathExists(filePath)) {
|
|
280
|
+
await ensureDir(path.dirname(filePath));
|
|
281
|
+
await writeFile(filePath, text, "utf8");
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
if (!appendOnly && text === "") {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
await appendFile2(filePath, text, "utf8");
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// src/ids.ts
|
|
291
|
+
var NIL_UUID = "00000000-0000-0000-0000-000000000000";
|
|
292
|
+
var UUID_V4_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
293
|
+
function validateResumeSessionId(value) {
|
|
294
|
+
if (!UUID_V4_REGEX.test(value)) {
|
|
295
|
+
throw new Error(`Invalid session ID: ${value}`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
function generateMessageId() {
|
|
299
|
+
return `msg_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;
|
|
300
|
+
}
|
|
301
|
+
function generateToolUseId() {
|
|
302
|
+
return `toolu_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 12)}`;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// src/models.ts
|
|
306
|
+
function formatGeminiOutputModel(model) {
|
|
307
|
+
const trimmed = model.trim();
|
|
308
|
+
if (!trimmed) {
|
|
309
|
+
return "google/gemini-2.5-flash";
|
|
310
|
+
}
|
|
311
|
+
return trimmed.includes("/") ? trimmed : `google/${trimmed}`;
|
|
312
|
+
}
|
|
313
|
+
function normalizeModelForGeminiCli(model) {
|
|
314
|
+
const trimmed = model.trim();
|
|
315
|
+
if (!trimmed) {
|
|
316
|
+
return {
|
|
317
|
+
requested: "gemini-2.5-flash",
|
|
318
|
+
outputModel: "google/gemini-2.5-flash",
|
|
319
|
+
geminiModel: "gemini-2.5-flash"
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
const short = trimmed.toLowerCase();
|
|
323
|
+
if (short === "flash") {
|
|
324
|
+
return {
|
|
325
|
+
requested: trimmed,
|
|
326
|
+
outputModel: "google/gemini-2.5-flash",
|
|
327
|
+
geminiModel: "gemini-2.5-flash"
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
if (short === "pro") {
|
|
331
|
+
return {
|
|
332
|
+
requested: trimmed,
|
|
333
|
+
outputModel: "google/gemini-2.5-pro",
|
|
334
|
+
geminiModel: "gemini-2.5-pro"
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
if (trimmed.includes("/")) {
|
|
338
|
+
const [, rest] = trimmed.split(/\/(.*)/s, 2);
|
|
339
|
+
const geminiModel = rest || trimmed;
|
|
340
|
+
return {
|
|
341
|
+
requested: trimmed,
|
|
342
|
+
outputModel: formatGeminiOutputModel(trimmed),
|
|
343
|
+
geminiModel
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
return {
|
|
347
|
+
requested: trimmed,
|
|
348
|
+
outputModel: formatGeminiOutputModel(trimmed),
|
|
349
|
+
geminiModel: trimmed
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
function getApiKeySource() {
|
|
353
|
+
if (process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY || process.env.GOOGLE_GENAI_USE_VERTEXAI === "true" || process.env.GOOGLE_GENAI_USE_GCA === "true") {
|
|
354
|
+
return "env";
|
|
355
|
+
}
|
|
356
|
+
return "none";
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// src/output.ts
|
|
360
|
+
function syntheticErrorMessage(prefix, text, model = "<synthetic>") {
|
|
361
|
+
return {
|
|
362
|
+
type: "assistant",
|
|
363
|
+
message: {
|
|
364
|
+
id: NIL_UUID,
|
|
365
|
+
type: "message",
|
|
366
|
+
role: "assistant",
|
|
367
|
+
model,
|
|
368
|
+
content: [{ type: "text", text: `${prefix}: ${text}` }],
|
|
369
|
+
stop_reason: null
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
function emit(message) {
|
|
374
|
+
process.stdout.write(JSON.stringify(message) + "\n");
|
|
375
|
+
}
|
|
376
|
+
async function flushStdout() {
|
|
377
|
+
await new Promise((resolve, reject) => {
|
|
378
|
+
process.stdout.write("", (error) => {
|
|
379
|
+
if (error) reject(error);
|
|
380
|
+
else resolve();
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
function summarizeText(text) {
|
|
385
|
+
const trimmed = text.trim();
|
|
386
|
+
if (!trimmed) {
|
|
387
|
+
return "Completed successfully.";
|
|
388
|
+
}
|
|
389
|
+
return trimmed.length > 500 ? `${trimmed.slice(0, 497)}...` : trimmed;
|
|
390
|
+
}
|
|
391
|
+
function countNumberedSteps(text) {
|
|
392
|
+
return text.match(/(?:^|\n)\s*\d+\.\s+/g)?.length ?? 0;
|
|
393
|
+
}
|
|
394
|
+
function shouldRetrySilentSuccessTurn(input) {
|
|
395
|
+
return !input.isError && !input.sawAssistantText && !input.sawToolUse && !input.sawToolResult;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// src/process-utils.ts
|
|
399
|
+
import { readFile as readFile2 } from "node:fs/promises";
|
|
400
|
+
import os from "node:os";
|
|
401
|
+
import path2 from "node:path";
|
|
402
|
+
function trackChildExit(child) {
|
|
403
|
+
if (child.exitCode !== null) {
|
|
404
|
+
return Promise.resolve(child.exitCode);
|
|
405
|
+
}
|
|
406
|
+
return new Promise((resolve, reject) => {
|
|
407
|
+
const onError = (error) => {
|
|
408
|
+
cleanup();
|
|
409
|
+
reject(error);
|
|
410
|
+
};
|
|
411
|
+
const onClose = (code) => {
|
|
412
|
+
cleanup();
|
|
413
|
+
resolve(code ?? child.exitCode ?? 1);
|
|
414
|
+
};
|
|
415
|
+
const cleanup = () => {
|
|
416
|
+
child.removeListener("error", onError);
|
|
417
|
+
child.removeListener("close", onClose);
|
|
418
|
+
};
|
|
419
|
+
child.once("error", onError);
|
|
420
|
+
child.once("close", onClose);
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
async function which(command) {
|
|
424
|
+
const isWindows = process.platform === "win32";
|
|
425
|
+
const { spawn: spawn2 } = await import("node:child_process");
|
|
426
|
+
return await new Promise((resolve) => {
|
|
427
|
+
const proc = spawn2(isWindows ? "where" : "which", [command], {
|
|
428
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
429
|
+
shell: isWindows
|
|
430
|
+
});
|
|
431
|
+
let output = "";
|
|
432
|
+
proc.stdout.on("data", (chunk) => {
|
|
433
|
+
output += String(chunk);
|
|
434
|
+
});
|
|
435
|
+
proc.on("close", (code) => {
|
|
436
|
+
if (code === 0) {
|
|
437
|
+
resolve(output.split(/\r?\n/).map((line) => line.trim()).find(Boolean) ?? null);
|
|
438
|
+
} else {
|
|
439
|
+
resolve(null);
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
async function loadGeminiSettingsAuthType() {
|
|
445
|
+
try {
|
|
446
|
+
const settingsPath = path2.join(os.homedir(), ".gemini", "settings.json");
|
|
447
|
+
const content = JSON.parse(await readFile2(settingsPath, "utf8"));
|
|
448
|
+
return content?.security?.auth?.selectedType ?? null;
|
|
449
|
+
} catch {
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// src/prompts.ts
|
|
455
|
+
function buildGeminiPrompt(prompt, appendSystemPrompt) {
|
|
456
|
+
const internalInstructions = [
|
|
457
|
+
"You are running behind a machine-oriented shim.",
|
|
458
|
+
"Answer directly from the conversation context and tool results whenever possible.",
|
|
459
|
+
"If the user explicitly asks you to create, update, or maintain a file, do that with tools before ending the turn.",
|
|
460
|
+
"When the user gives a numbered or ordered task list, complete every requested step in sequence before finishing.",
|
|
461
|
+
"Do not stop after an intermediate answer if additional requested steps remain.",
|
|
462
|
+
"If the user asks for ongoing research notes or a progress file, keep that file updated as you work.",
|
|
463
|
+
"If the user asks for research, use the available search/web/file tools rather than answering from memory alone whenever the request calls for current sources.",
|
|
464
|
+
"When creating machine-readable files such as JSON, ensure the final file contents are syntactically valid before ending the turn.",
|
|
465
|
+
"Do not delegate to CLI help, documentation helpers, or other subagents unless the user explicitly asks about Gemini CLI usage or external documentation.",
|
|
466
|
+
"If the user asks what they told you earlier in this same conversation, answer from the conversation history directly."
|
|
467
|
+
].join(" ");
|
|
468
|
+
if (!appendSystemPrompt?.trim()) {
|
|
469
|
+
return [
|
|
470
|
+
"SYSTEM INSTRUCTIONS (highest priority for this run):",
|
|
471
|
+
internalInstructions,
|
|
472
|
+
"",
|
|
473
|
+
"USER PROMPT:",
|
|
474
|
+
prompt
|
|
475
|
+
].join("\n");
|
|
476
|
+
}
|
|
477
|
+
return [
|
|
478
|
+
"SYSTEM INSTRUCTIONS (highest priority for this run):",
|
|
479
|
+
internalInstructions,
|
|
480
|
+
"",
|
|
481
|
+
"ADDITIONAL CALLER SYSTEM INSTRUCTIONS:",
|
|
482
|
+
appendSystemPrompt.trim(),
|
|
483
|
+
"",
|
|
484
|
+
"USER PROMPT:",
|
|
485
|
+
prompt
|
|
486
|
+
].join("\n");
|
|
487
|
+
}
|
|
488
|
+
function buildSilentTurnRecoveryPrompt() {
|
|
489
|
+
return [
|
|
490
|
+
"System: Your previous turn produced no assistant-visible text, tool calls, or tool results.",
|
|
491
|
+
"Continue the pending user request now.",
|
|
492
|
+
"You must either produce assistant text or use tools to complete the requested work before ending the turn.",
|
|
493
|
+
"Do not end with an empty response."
|
|
494
|
+
].join(" ");
|
|
495
|
+
}
|
|
496
|
+
function buildRemainingStepsPrompt() {
|
|
497
|
+
return [
|
|
498
|
+
"System: Re-check the user's original numbered task list.",
|
|
499
|
+
"If any requested numbered steps are still incomplete, complete them now before you finish.",
|
|
500
|
+
"If everything is already complete, briefly confirm that all requested steps are done."
|
|
501
|
+
].join(" ");
|
|
502
|
+
}
|
|
503
|
+
function buildInvalidJsonRepairPrompt(invalidFiles) {
|
|
504
|
+
return [
|
|
505
|
+
"System: Re-check the machine-readable files you created or modified.",
|
|
506
|
+
`These files are currently invalid JSON: ${invalidFiles.join("; ")}.`,
|
|
507
|
+
"Use tools to read the current file contents, repair them, and verify the final on-disk files parse as valid JSON before you end the turn.",
|
|
508
|
+
"If a previous repair introduced extra escaping, remove the extra escaping so the file itself is valid JSON.",
|
|
509
|
+
"After repairing them, briefly confirm which files were fixed."
|
|
510
|
+
].join(" ");
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// src/session-files.ts
|
|
514
|
+
import { readFile as readFile3, readdir } from "node:fs/promises";
|
|
515
|
+
import os2 from "node:os";
|
|
516
|
+
import path3 from "node:path";
|
|
517
|
+
var sessionFileCache = /* @__PURE__ */ new Map();
|
|
518
|
+
async function findGeminiSessionFile(sessionId) {
|
|
519
|
+
const cached = sessionFileCache.get(sessionId);
|
|
520
|
+
if (cached && await pathExists(cached)) {
|
|
521
|
+
return cached;
|
|
522
|
+
}
|
|
523
|
+
const baseDir = path3.join(os2.homedir(), ".gemini", "tmp");
|
|
524
|
+
if (!await pathExists(baseDir)) {
|
|
525
|
+
return void 0;
|
|
526
|
+
}
|
|
527
|
+
const prefix = sessionId.slice(0, 8);
|
|
528
|
+
const firstLevel = await readdir(baseDir, { withFileTypes: true });
|
|
529
|
+
for (const entry of firstLevel) {
|
|
530
|
+
if (!entry.isDirectory()) continue;
|
|
531
|
+
const chatsDir = path3.join(baseDir, entry.name, "chats");
|
|
532
|
+
if (!await pathExists(chatsDir)) continue;
|
|
533
|
+
const files = await readdir(chatsDir);
|
|
534
|
+
for (const file of files) {
|
|
535
|
+
if (!file.startsWith("session-") || !file.endsWith(".json") || !file.includes(prefix)) continue;
|
|
536
|
+
const filePath = path3.join(chatsDir, file);
|
|
537
|
+
try {
|
|
538
|
+
const content = JSON.parse(await readFile3(filePath, "utf8"));
|
|
539
|
+
if (content.sessionId === sessionId) {
|
|
540
|
+
sessionFileCache.set(sessionId, filePath);
|
|
541
|
+
return filePath;
|
|
542
|
+
}
|
|
543
|
+
} catch {
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
return void 0;
|
|
549
|
+
}
|
|
550
|
+
async function readSessionFile(sessionId) {
|
|
551
|
+
const filePath = await findGeminiSessionFile(sessionId);
|
|
552
|
+
if (!filePath) return void 0;
|
|
553
|
+
try {
|
|
554
|
+
return JSON.parse(await readFile3(filePath, "utf8"));
|
|
555
|
+
} catch {
|
|
556
|
+
return void 0;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
async function waitForToolRecord(sessionId, toolId, timeoutMs = 2e3) {
|
|
560
|
+
const started = Date.now();
|
|
561
|
+
while (Date.now() - started <= timeoutMs) {
|
|
562
|
+
const data = await readSessionFile(sessionId);
|
|
563
|
+
if (data?.messages.some((message) => message.toolCalls?.some((call) => call.id === toolId))) {
|
|
564
|
+
return data;
|
|
565
|
+
}
|
|
566
|
+
await sleep(100);
|
|
567
|
+
}
|
|
568
|
+
return readSessionFile(sessionId);
|
|
569
|
+
}
|
|
570
|
+
function extractMeaningfulToolContent(session, toolId, fallbackOutput) {
|
|
571
|
+
if (fallbackOutput && fallbackOutput.trim()) {
|
|
572
|
+
return fallbackOutput;
|
|
573
|
+
}
|
|
574
|
+
const toolCall = session?.messages.flatMap((message) => message.toolCalls ?? []).find((call) => call.id === toolId);
|
|
575
|
+
if (!toolCall) {
|
|
576
|
+
return fallbackOutput && fallbackOutput.trim() ? fallbackOutput : void 0;
|
|
577
|
+
}
|
|
578
|
+
const functionResponseOutput = extractOutputFromResult(toolCall.result);
|
|
579
|
+
if (functionResponseOutput?.trim()) {
|
|
580
|
+
return functionResponseOutput;
|
|
581
|
+
}
|
|
582
|
+
const resultDisplay = toolCall.resultDisplay;
|
|
583
|
+
if (typeof resultDisplay === "string" && resultDisplay.trim()) {
|
|
584
|
+
return resultDisplay;
|
|
585
|
+
}
|
|
586
|
+
if (resultDisplay && typeof resultDisplay === "object") {
|
|
587
|
+
const record = resultDisplay;
|
|
588
|
+
if (typeof record.fileDiff === "string" && record.fileDiff.trim()) {
|
|
589
|
+
return record.fileDiff;
|
|
590
|
+
}
|
|
591
|
+
if (typeof record.filePath === "string") {
|
|
592
|
+
return `File updated: ${record.filePath}`;
|
|
593
|
+
}
|
|
594
|
+
if (typeof record.fileName === "string") {
|
|
595
|
+
return `File updated: ${record.fileName}`;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
return fallbackOutput && fallbackOutput.trim() ? fallbackOutput : void 0;
|
|
599
|
+
}
|
|
600
|
+
function extractOutputFromResult(result) {
|
|
601
|
+
if (!Array.isArray(result)) return void 0;
|
|
602
|
+
for (const item of result) {
|
|
603
|
+
if (!item || typeof item !== "object") continue;
|
|
604
|
+
const functionResponse = item.functionResponse;
|
|
605
|
+
if (!functionResponse || typeof functionResponse !== "object") continue;
|
|
606
|
+
const response = functionResponse.response;
|
|
607
|
+
if (!response || typeof response !== "object") continue;
|
|
608
|
+
const output = response.output;
|
|
609
|
+
if (typeof output === "string") {
|
|
610
|
+
return output;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
return void 0;
|
|
614
|
+
}
|
|
615
|
+
function aggregateInvocationUsage(session, invocationStartIso) {
|
|
616
|
+
if (!session) return void 0;
|
|
617
|
+
const startMs = Date.parse(invocationStartIso);
|
|
618
|
+
const totals = /* @__PURE__ */ new Map();
|
|
619
|
+
for (const message of session.messages) {
|
|
620
|
+
if (message.type !== "gemini" || !message.model || !message.tokens) continue;
|
|
621
|
+
if (Date.parse(message.timestamp) < startMs) continue;
|
|
622
|
+
const current = totals.get(message.model) ?? {
|
|
623
|
+
input_tokens: 0,
|
|
624
|
+
output_tokens: 0,
|
|
625
|
+
cache_read_input_tokens: 0,
|
|
626
|
+
cost_usd: 0
|
|
627
|
+
};
|
|
628
|
+
current.input_tokens += Number(message.tokens.input ?? 0);
|
|
629
|
+
current.output_tokens += Number(message.tokens.output ?? 0);
|
|
630
|
+
current.cache_read_input_tokens = (current.cache_read_input_tokens ?? 0) + Number(message.tokens.cached ?? 0);
|
|
631
|
+
totals.set(message.model, current);
|
|
632
|
+
}
|
|
633
|
+
if (totals.size <= 1) return void 0;
|
|
634
|
+
return Object.fromEntries(
|
|
635
|
+
[...totals.entries()].map(([model, usage]) => [formatGeminiOutputModel(model), usage])
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// src/tools.ts
|
|
640
|
+
function camelToSnake(value) {
|
|
641
|
+
return value.replace(/[A-Z]/g, (char) => `_${char.toLowerCase()}`);
|
|
642
|
+
}
|
|
643
|
+
function topLevelSnakeCase(input) {
|
|
644
|
+
if (!input) return void 0;
|
|
645
|
+
const output = {};
|
|
646
|
+
for (const [key, value] of Object.entries(input)) {
|
|
647
|
+
output[camelToSnake(key)] = value;
|
|
648
|
+
}
|
|
649
|
+
return output;
|
|
650
|
+
}
|
|
651
|
+
var TOOL_NAME_MAP = /* @__PURE__ */ new Map([
|
|
652
|
+
["read_file", "Read"],
|
|
653
|
+
["readfile", "Read"],
|
|
654
|
+
["write_file", "Write"],
|
|
655
|
+
["writefile", "Write"],
|
|
656
|
+
["replace", "Edit"],
|
|
657
|
+
["edit", "Edit"],
|
|
658
|
+
["run_shell_command", "Bash"],
|
|
659
|
+
["shell", "Bash"],
|
|
660
|
+
["bash", "Bash"],
|
|
661
|
+
["glob", "Glob"],
|
|
662
|
+
["search_file_content", "Grep"],
|
|
663
|
+
["grep", "Grep"],
|
|
664
|
+
["list_directory", "LS"],
|
|
665
|
+
["ls", "LS"]
|
|
666
|
+
]);
|
|
667
|
+
function normalizeToolName(name) {
|
|
668
|
+
return TOOL_NAME_MAP.get(name.toLowerCase()) ?? name;
|
|
669
|
+
}
|
|
670
|
+
function normalizeToolInput(name, input) {
|
|
671
|
+
const snake = topLevelSnakeCase(input);
|
|
672
|
+
if (!snake) return void 0;
|
|
673
|
+
switch (name) {
|
|
674
|
+
case "Read": {
|
|
675
|
+
const filePath = snake.file_path ?? snake.path;
|
|
676
|
+
const result = {};
|
|
677
|
+
if (filePath !== void 0) result.file_path = filePath;
|
|
678
|
+
if (snake.offset !== void 0) result.offset = snake.offset;
|
|
679
|
+
if (snake.limit !== void 0) result.limit = snake.limit;
|
|
680
|
+
return result;
|
|
681
|
+
}
|
|
682
|
+
case "Write": {
|
|
683
|
+
const filePath = snake.file_path ?? snake.path;
|
|
684
|
+
const result = {};
|
|
685
|
+
if (filePath !== void 0) result.file_path = filePath;
|
|
686
|
+
if (snake.content !== void 0) result.content = snake.content;
|
|
687
|
+
return result;
|
|
688
|
+
}
|
|
689
|
+
case "Edit":
|
|
690
|
+
return snake;
|
|
691
|
+
case "Bash":
|
|
692
|
+
return {
|
|
693
|
+
command: snake.command,
|
|
694
|
+
...snake.description !== void 0 ? { description: snake.description } : {},
|
|
695
|
+
...snake.directory !== void 0 ? { directory: snake.directory } : {}
|
|
696
|
+
};
|
|
697
|
+
case "Glob": {
|
|
698
|
+
const out = {};
|
|
699
|
+
if (snake.pattern !== void 0) out.pattern = snake.pattern;
|
|
700
|
+
if (snake.path !== void 0) out.path = snake.path;
|
|
701
|
+
if (snake.case_sensitive !== void 0) out.case_sensitive = snake.case_sensitive;
|
|
702
|
+
if (snake.respect_git_ignore !== void 0) out.respect_git_ignore = snake.respect_git_ignore;
|
|
703
|
+
return out;
|
|
704
|
+
}
|
|
705
|
+
case "Grep": {
|
|
706
|
+
const out = {};
|
|
707
|
+
if (snake.pattern !== void 0) out.pattern = snake.pattern;
|
|
708
|
+
if (snake.path !== void 0) out.path = snake.path;
|
|
709
|
+
if (snake.include !== void 0) out.glob = snake.include;
|
|
710
|
+
return out;
|
|
711
|
+
}
|
|
712
|
+
case "LS": {
|
|
713
|
+
const out = {};
|
|
714
|
+
if (snake.path !== void 0) out.path = snake.path;
|
|
715
|
+
if (snake.ignore !== void 0) out.ignore = snake.ignore;
|
|
716
|
+
if (snake.respect_git_ignore !== void 0) out.respect_git_ignore = snake.respect_git_ignore;
|
|
717
|
+
return out;
|
|
718
|
+
}
|
|
719
|
+
default:
|
|
720
|
+
return snake;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
function extractToolFilePath(input) {
|
|
724
|
+
const candidate = input?.file_path ?? input?.path;
|
|
725
|
+
return typeof candidate === "string" ? candidate : void 0;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// src/shim.ts
|
|
729
|
+
async function runShim(args, prompt) {
|
|
730
|
+
const invocationStart = (/* @__PURE__ */ new Date()).toISOString();
|
|
731
|
+
const startTime = Date.now();
|
|
732
|
+
const { outputModel, geminiModel } = normalizeModelForGeminiCli(args.model);
|
|
733
|
+
const cwd = process2.cwd();
|
|
734
|
+
const debug = {
|
|
735
|
+
debugDir: args.debugDir,
|
|
736
|
+
bufferedEvents: [],
|
|
737
|
+
bufferedStderr: []
|
|
738
|
+
};
|
|
739
|
+
if (debug.debugDir) {
|
|
740
|
+
await ensureDir(debug.debugDir);
|
|
741
|
+
}
|
|
742
|
+
if (args.resume) {
|
|
743
|
+
try {
|
|
744
|
+
validateResumeSessionId(args.resume);
|
|
745
|
+
} catch (error) {
|
|
746
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
747
|
+
await writeUnknownLog(debug, `${message}
|
|
748
|
+
`);
|
|
749
|
+
throw error;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
const geminiPath = await which("gemini");
|
|
753
|
+
if (!geminiPath) {
|
|
754
|
+
process2.stderr.write("Gemini CLI not found in PATH.\n");
|
|
755
|
+
await writeUnknownLog(debug, "Gemini CLI not found in PATH.\n");
|
|
756
|
+
return 1;
|
|
757
|
+
}
|
|
758
|
+
let interrupted = false;
|
|
759
|
+
let activeChild;
|
|
760
|
+
let systemEmitted = false;
|
|
761
|
+
let resultEmitted = false;
|
|
762
|
+
let sessionId = args.resume;
|
|
763
|
+
let actualModel = outputModel;
|
|
764
|
+
let durationApiMs = 0;
|
|
765
|
+
let usage;
|
|
766
|
+
let modelUsage;
|
|
767
|
+
let finalIsError = false;
|
|
768
|
+
let numTurns = 0;
|
|
769
|
+
let remainingSilentSuccessRetries = MAX_SILENT_SUCCESS_RETRIES;
|
|
770
|
+
let remainingInvalidJsonRepairs = MAX_INVALID_JSON_REPAIRS;
|
|
771
|
+
let completionCheckIssued = false;
|
|
772
|
+
let totalToolUseCount = 0;
|
|
773
|
+
let resumeSessionId = args.resume;
|
|
774
|
+
let attemptPrompt = buildGeminiPrompt(prompt, args.appendSystemPrompt);
|
|
775
|
+
const toolIdMap = /* @__PURE__ */ new Map();
|
|
776
|
+
const emittedToolUses = /* @__PURE__ */ new Set();
|
|
777
|
+
const emittedToolResults = /* @__PURE__ */ new Set();
|
|
778
|
+
const pendingToolResults = /* @__PURE__ */ new Map();
|
|
779
|
+
const assistantTextChunks = [];
|
|
780
|
+
let pendingAssistantText = "";
|
|
781
|
+
let currentAssistantStreamHasDelta = false;
|
|
782
|
+
let lastSyntheticError;
|
|
783
|
+
const candidateJsonFiles = /* @__PURE__ */ new Set();
|
|
784
|
+
const numberedStepCount = countNumberedSteps(prompt);
|
|
785
|
+
const flushAssistantText = () => {
|
|
786
|
+
if (!pendingAssistantText) return;
|
|
787
|
+
assistantTextChunks.push(pendingAssistantText);
|
|
788
|
+
const message = {
|
|
789
|
+
type: "assistant",
|
|
790
|
+
message: {
|
|
791
|
+
id: generateMessageId(),
|
|
792
|
+
type: "message",
|
|
793
|
+
role: "assistant",
|
|
794
|
+
model: actualModel,
|
|
795
|
+
content: [{ type: "text", text: pendingAssistantText }],
|
|
796
|
+
stop_reason: null
|
|
797
|
+
}
|
|
798
|
+
};
|
|
799
|
+
emit(message);
|
|
800
|
+
pendingAssistantText = "";
|
|
801
|
+
currentAssistantStreamHasDelta = false;
|
|
802
|
+
};
|
|
803
|
+
const emitFinalResult = async (isError, resultText) => {
|
|
804
|
+
const result = {
|
|
805
|
+
type: "result",
|
|
806
|
+
subtype: isError ? "error" : "success",
|
|
807
|
+
is_error: isError,
|
|
808
|
+
duration_ms: Date.now() - startTime,
|
|
809
|
+
duration_api_ms: durationApiMs,
|
|
810
|
+
num_turns: Math.max(1, numTurns),
|
|
811
|
+
result: resultText,
|
|
812
|
+
session_id: sessionId,
|
|
813
|
+
usage,
|
|
814
|
+
...modelUsage ? { model_usage: modelUsage } : {}
|
|
815
|
+
};
|
|
816
|
+
emit(result);
|
|
817
|
+
resultEmitted = true;
|
|
818
|
+
finalIsError = isError;
|
|
819
|
+
await flushStdout();
|
|
820
|
+
return isError ? 1 : 0;
|
|
821
|
+
};
|
|
822
|
+
const maybeRepairInvalidJsonFiles = async () => {
|
|
823
|
+
const invalidJsonFiles = await findInvalidJsonFiles(candidateJsonFiles);
|
|
824
|
+
if (invalidJsonFiles.length === 0) {
|
|
825
|
+
return void 0;
|
|
826
|
+
}
|
|
827
|
+
const invalidFileDescriptions = invalidJsonFiles.map(({ filePath, error }) => {
|
|
828
|
+
const relativePath = path4.relative(cwd, filePath) || filePath;
|
|
829
|
+
return `${relativePath} (${error})`;
|
|
830
|
+
});
|
|
831
|
+
const errorText = `Gemini left invalid JSON files: ${invalidFileDescriptions.join("; ")}`;
|
|
832
|
+
if (remainingInvalidJsonRepairs > 0 && sessionId) {
|
|
833
|
+
remainingInvalidJsonRepairs -= 1;
|
|
834
|
+
resumeSessionId = sessionId;
|
|
835
|
+
attemptPrompt = buildInvalidJsonRepairPrompt(invalidFileDescriptions);
|
|
836
|
+
if (args.verbose) {
|
|
837
|
+
process2.stderr.write(
|
|
838
|
+
`[shim] Gemini left invalid JSON files for session ${sessionId}; requesting repair: ${invalidFileDescriptions.join(", ")}
|
|
839
|
+
`
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
return "continue";
|
|
843
|
+
}
|
|
844
|
+
emit(syntheticErrorMessage("Agent Error", errorText));
|
|
845
|
+
return await emitFinalResult(true, errorText);
|
|
846
|
+
};
|
|
847
|
+
const signalHandler = async () => {
|
|
848
|
+
if (interrupted) return;
|
|
849
|
+
interrupted = true;
|
|
850
|
+
activeChild?.kill("SIGTERM");
|
|
851
|
+
flushAssistantText();
|
|
852
|
+
if (systemEmitted && !resultEmitted) {
|
|
853
|
+
await emitFinalResult(false, summarizeText(assistantTextChunks.join("")));
|
|
854
|
+
}
|
|
855
|
+
process2.exit(0);
|
|
856
|
+
};
|
|
857
|
+
process2.once("SIGINT", signalHandler);
|
|
858
|
+
process2.once("SIGTERM", signalHandler);
|
|
859
|
+
try {
|
|
860
|
+
while (true) {
|
|
861
|
+
numTurns += 1;
|
|
862
|
+
lastSyntheticError = void 0;
|
|
863
|
+
let syntheticErrorEmitted = false;
|
|
864
|
+
const child = spawnGemini({
|
|
865
|
+
args,
|
|
866
|
+
cwd,
|
|
867
|
+
geminiModel,
|
|
868
|
+
geminiPath,
|
|
869
|
+
resumeSessionId
|
|
870
|
+
});
|
|
871
|
+
activeChild = child;
|
|
872
|
+
const childExitPromise = trackChildExit(child);
|
|
873
|
+
child.stdin.write(attemptPrompt);
|
|
874
|
+
child.stdin.end();
|
|
875
|
+
child.stderr.on("data", async (chunk) => {
|
|
876
|
+
const text = String(chunk);
|
|
877
|
+
if (args.verbose) {
|
|
878
|
+
process2.stderr.write(`[gemini-stderr] ${text}`);
|
|
879
|
+
}
|
|
880
|
+
await captureStderr(debug, text);
|
|
881
|
+
});
|
|
882
|
+
const eventIterator = readGeminiEvents(child, debug, args.verbose);
|
|
883
|
+
let attemptSawAssistantText = false;
|
|
884
|
+
let attemptSawToolUse = false;
|
|
885
|
+
let attemptSawToolResult = false;
|
|
886
|
+
let retryDueToSilentSuccess = false;
|
|
887
|
+
let continueWithFollowUpPrompt = false;
|
|
888
|
+
const emitToolResultFromEvent = async (event) => {
|
|
889
|
+
if (emittedToolResults.has(event.tool_id)) {
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
const publicToolId = toolIdMap.get(event.tool_id);
|
|
893
|
+
if (!publicToolId) {
|
|
894
|
+
pendingToolResults.set(event.tool_id, event);
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
const sessionData = sessionId ? await waitForToolRecord(sessionId, event.tool_id) : void 0;
|
|
898
|
+
const contentText = sessionId ? extractMeaningfulToolContent(sessionData, event.tool_id, event.output) : event.output;
|
|
899
|
+
const userMessage = {
|
|
900
|
+
type: "user",
|
|
901
|
+
message: {
|
|
902
|
+
role: "user",
|
|
903
|
+
content: [
|
|
904
|
+
event.status === "error" ? {
|
|
905
|
+
type: "tool_result",
|
|
906
|
+
tool_use_id: publicToolId,
|
|
907
|
+
content: {
|
|
908
|
+
is_error: true,
|
|
909
|
+
error: event.error?.message || contentText || "Tool execution failed"
|
|
910
|
+
}
|
|
911
|
+
} : {
|
|
912
|
+
type: "tool_result",
|
|
913
|
+
tool_use_id: publicToolId,
|
|
914
|
+
content: contentText || `Tool completed: ${event.tool_id}`
|
|
915
|
+
}
|
|
916
|
+
]
|
|
917
|
+
}
|
|
918
|
+
};
|
|
919
|
+
emit(userMessage);
|
|
920
|
+
emittedToolResults.add(event.tool_id);
|
|
921
|
+
};
|
|
922
|
+
try {
|
|
923
|
+
attemptLoop: for await (const event of withAdaptiveTimeout(eventIterator, {
|
|
924
|
+
idleTimeoutMs: args.idleTimeout * 1e3,
|
|
925
|
+
busyTimeoutMs: Math.max(args.idleTimeout * 1e3, 3e5),
|
|
926
|
+
onEvent(event2, controller) {
|
|
927
|
+
if (event2.type === "tool_use" || event2.type === "message" && event2.role === "assistant" && event2.content.length > 0) {
|
|
928
|
+
controller.markBusy();
|
|
929
|
+
}
|
|
930
|
+
if (event2.type === "result") {
|
|
931
|
+
controller.markIdle();
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
})) {
|
|
935
|
+
switch (event.type) {
|
|
936
|
+
case "init": {
|
|
937
|
+
sessionId = event.session_id;
|
|
938
|
+
actualModel = formatGeminiOutputModel(event.model || geminiModel || outputModel);
|
|
939
|
+
await bindDebugSession(debug, sessionId);
|
|
940
|
+
if (!systemEmitted) {
|
|
941
|
+
const system = {
|
|
942
|
+
type: "system",
|
|
943
|
+
subtype: "init",
|
|
944
|
+
cwd,
|
|
945
|
+
session_id: sessionId,
|
|
946
|
+
tools: KNOWN_TOOLS,
|
|
947
|
+
model: actualModel,
|
|
948
|
+
permissionMode: "bypassPermissions",
|
|
949
|
+
apiKeySource: getApiKeySource(),
|
|
950
|
+
mcp_servers: []
|
|
951
|
+
};
|
|
952
|
+
emit(system);
|
|
953
|
+
systemEmitted = true;
|
|
954
|
+
}
|
|
955
|
+
break;
|
|
956
|
+
}
|
|
957
|
+
case "message": {
|
|
958
|
+
if (event.role !== "assistant") {
|
|
959
|
+
break;
|
|
960
|
+
}
|
|
961
|
+
if (!event.content) {
|
|
962
|
+
break;
|
|
963
|
+
}
|
|
964
|
+
if (event.delta) {
|
|
965
|
+
currentAssistantStreamHasDelta = true;
|
|
966
|
+
pendingAssistantText += event.content;
|
|
967
|
+
if (event.content.trim().length > 0) {
|
|
968
|
+
attemptSawAssistantText = true;
|
|
969
|
+
}
|
|
970
|
+
break;
|
|
971
|
+
}
|
|
972
|
+
if (currentAssistantStreamHasDelta) {
|
|
973
|
+
break;
|
|
974
|
+
}
|
|
975
|
+
pendingAssistantText += event.content;
|
|
976
|
+
if (event.content.trim().length > 0) {
|
|
977
|
+
attemptSawAssistantText = true;
|
|
978
|
+
}
|
|
979
|
+
break;
|
|
980
|
+
}
|
|
981
|
+
case "tool_use": {
|
|
982
|
+
attemptSawToolUse = true;
|
|
983
|
+
if (emittedToolUses.has(event.tool_id)) {
|
|
984
|
+
break;
|
|
985
|
+
}
|
|
986
|
+
totalToolUseCount += 1;
|
|
987
|
+
flushAssistantText();
|
|
988
|
+
const publicToolId = toolIdMap.get(event.tool_id) ?? generateToolUseId();
|
|
989
|
+
toolIdMap.set(event.tool_id, publicToolId);
|
|
990
|
+
emittedToolUses.add(event.tool_id);
|
|
991
|
+
const normalizedName = normalizeToolName(event.tool_name);
|
|
992
|
+
const normalizedInput = normalizeToolInput(normalizedName, event.parameters);
|
|
993
|
+
const candidateFilePath = extractToolFilePath(normalizedInput);
|
|
994
|
+
if (candidateFilePath && (normalizedName === "Write" || normalizedName === "Edit") && candidateFilePath.toLowerCase().endsWith(".json")) {
|
|
995
|
+
candidateJsonFiles.add(path4.resolve(cwd, candidateFilePath));
|
|
996
|
+
}
|
|
997
|
+
const toolUse = {
|
|
998
|
+
type: "assistant",
|
|
999
|
+
message: {
|
|
1000
|
+
id: generateMessageId(),
|
|
1001
|
+
type: "message",
|
|
1002
|
+
role: "assistant",
|
|
1003
|
+
model: actualModel,
|
|
1004
|
+
content: [
|
|
1005
|
+
{
|
|
1006
|
+
type: "tool_use",
|
|
1007
|
+
id: publicToolId,
|
|
1008
|
+
name: normalizedName,
|
|
1009
|
+
input: normalizedInput
|
|
1010
|
+
}
|
|
1011
|
+
],
|
|
1012
|
+
stop_reason: "tool_use"
|
|
1013
|
+
}
|
|
1014
|
+
};
|
|
1015
|
+
emit(toolUse);
|
|
1016
|
+
const pendingResult = pendingToolResults.get(event.tool_id);
|
|
1017
|
+
if (pendingResult) {
|
|
1018
|
+
pendingToolResults.delete(event.tool_id);
|
|
1019
|
+
await emitToolResultFromEvent(pendingResult);
|
|
1020
|
+
}
|
|
1021
|
+
break;
|
|
1022
|
+
}
|
|
1023
|
+
case "tool_result": {
|
|
1024
|
+
attemptSawToolResult = true;
|
|
1025
|
+
flushAssistantText();
|
|
1026
|
+
await emitToolResultFromEvent(event);
|
|
1027
|
+
break;
|
|
1028
|
+
}
|
|
1029
|
+
case "error": {
|
|
1030
|
+
flushAssistantText();
|
|
1031
|
+
if (event.severity === "error") {
|
|
1032
|
+
lastSyntheticError = event.message;
|
|
1033
|
+
finalIsError = true;
|
|
1034
|
+
if (!syntheticErrorEmitted) {
|
|
1035
|
+
emit(syntheticErrorMessage("Agent Error", event.message));
|
|
1036
|
+
syntheticErrorEmitted = true;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
break;
|
|
1040
|
+
}
|
|
1041
|
+
case "result": {
|
|
1042
|
+
flushAssistantText();
|
|
1043
|
+
durationApiMs = Number(event.stats?.duration_ms ?? Date.now() - startTime);
|
|
1044
|
+
usage = {
|
|
1045
|
+
input_tokens: event.stats?.input_tokens,
|
|
1046
|
+
output_tokens: event.stats?.output_tokens,
|
|
1047
|
+
cache_read_input_tokens: event.stats?.cached
|
|
1048
|
+
};
|
|
1049
|
+
const sessionData = sessionId ? await waitForSessionFile(sessionId) : void 0;
|
|
1050
|
+
modelUsage = aggregateInvocationUsage(sessionData, invocationStart);
|
|
1051
|
+
const isError = event.status === "error" || Boolean(lastSyntheticError) || Boolean(event.error?.message);
|
|
1052
|
+
if (isError && !syntheticErrorEmitted) {
|
|
1053
|
+
emit(
|
|
1054
|
+
syntheticErrorMessage(
|
|
1055
|
+
"Agent Error",
|
|
1056
|
+
event.error?.message || lastSyntheticError || "Gemini CLI reported an error"
|
|
1057
|
+
)
|
|
1058
|
+
);
|
|
1059
|
+
syntheticErrorEmitted = true;
|
|
1060
|
+
}
|
|
1061
|
+
const silentSuccess = shouldRetrySilentSuccessTurn({
|
|
1062
|
+
isError,
|
|
1063
|
+
sawAssistantText: attemptSawAssistantText,
|
|
1064
|
+
sawToolUse: attemptSawToolUse,
|
|
1065
|
+
sawToolResult: attemptSawToolResult
|
|
1066
|
+
});
|
|
1067
|
+
if (silentSuccess) {
|
|
1068
|
+
if (remainingSilentSuccessRetries > 0 && sessionId) {
|
|
1069
|
+
remainingSilentSuccessRetries -= 1;
|
|
1070
|
+
retryDueToSilentSuccess = true;
|
|
1071
|
+
continueWithFollowUpPrompt = true;
|
|
1072
|
+
resumeSessionId = sessionId;
|
|
1073
|
+
attemptPrompt = buildSilentTurnRecoveryPrompt();
|
|
1074
|
+
if (args.verbose) {
|
|
1075
|
+
process2.stderr.write(
|
|
1076
|
+
`[shim] Gemini returned a silent success turn for session ${sessionId}; retrying with continuation prompt.
|
|
1077
|
+
`
|
|
1078
|
+
);
|
|
1079
|
+
}
|
|
1080
|
+
break attemptLoop;
|
|
1081
|
+
}
|
|
1082
|
+
const message = "Gemini completed with no assistant text or tool activity.";
|
|
1083
|
+
emit(syntheticErrorMessage("Agent Error", message));
|
|
1084
|
+
const emittedCode2 = await emitFinalResult(true, message);
|
|
1085
|
+
await childExitPromise;
|
|
1086
|
+
activeChild = void 0;
|
|
1087
|
+
return emittedCode2;
|
|
1088
|
+
}
|
|
1089
|
+
const needsCompletionCheck = !isError && !completionCheckIssued && sessionId && numberedStepCount >= 2 && totalToolUseCount > 0 && totalToolUseCount < numberedStepCount;
|
|
1090
|
+
if (needsCompletionCheck) {
|
|
1091
|
+
completionCheckIssued = true;
|
|
1092
|
+
continueWithFollowUpPrompt = true;
|
|
1093
|
+
resumeSessionId = sessionId;
|
|
1094
|
+
attemptPrompt = buildRemainingStepsPrompt();
|
|
1095
|
+
if (args.verbose) {
|
|
1096
|
+
process2.stderr.write(
|
|
1097
|
+
`[shim] Gemini may have stopped before finishing a numbered task list for session ${sessionId}; requesting completion check.
|
|
1098
|
+
`
|
|
1099
|
+
);
|
|
1100
|
+
}
|
|
1101
|
+
break attemptLoop;
|
|
1102
|
+
}
|
|
1103
|
+
if (!isError) {
|
|
1104
|
+
const jsonRepairOutcome = await maybeRepairInvalidJsonFiles();
|
|
1105
|
+
if (jsonRepairOutcome === "continue") {
|
|
1106
|
+
continueWithFollowUpPrompt = true;
|
|
1107
|
+
break attemptLoop;
|
|
1108
|
+
}
|
|
1109
|
+
if (typeof jsonRepairOutcome === "number") {
|
|
1110
|
+
await childExitPromise;
|
|
1111
|
+
activeChild = void 0;
|
|
1112
|
+
return jsonRepairOutcome;
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
const resultText = isError ? event.error?.message || lastSyntheticError || "Gemini CLI reported an error" : summarizeText(assistantTextChunks.join(""));
|
|
1116
|
+
const emittedCode = await emitFinalResult(isError, resultText);
|
|
1117
|
+
await childExitPromise;
|
|
1118
|
+
activeChild = void 0;
|
|
1119
|
+
return emittedCode;
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
flushAssistantText();
|
|
1124
|
+
const exitCode = await childExitPromise;
|
|
1125
|
+
activeChild = void 0;
|
|
1126
|
+
if (retryDueToSilentSuccess || continueWithFollowUpPrompt) {
|
|
1127
|
+
continue;
|
|
1128
|
+
}
|
|
1129
|
+
if (!resultEmitted && systemEmitted) {
|
|
1130
|
+
const sessionData = sessionId ? await waitForSessionFile(sessionId) : void 0;
|
|
1131
|
+
modelUsage = aggregateInvocationUsage(sessionData, invocationStart);
|
|
1132
|
+
const success = exitCode === 0 && !lastSyntheticError;
|
|
1133
|
+
const silentSuccess = shouldRetrySilentSuccessTurn({
|
|
1134
|
+
isError: !success,
|
|
1135
|
+
sawAssistantText: attemptSawAssistantText,
|
|
1136
|
+
sawToolUse: attemptSawToolUse,
|
|
1137
|
+
sawToolResult: attemptSawToolResult
|
|
1138
|
+
});
|
|
1139
|
+
if (silentSuccess) {
|
|
1140
|
+
if (remainingSilentSuccessRetries > 0 && sessionId) {
|
|
1141
|
+
remainingSilentSuccessRetries -= 1;
|
|
1142
|
+
resumeSessionId = sessionId;
|
|
1143
|
+
attemptPrompt = buildSilentTurnRecoveryPrompt();
|
|
1144
|
+
if (args.verbose) {
|
|
1145
|
+
process2.stderr.write(
|
|
1146
|
+
`[shim] Gemini exited after a silent success turn for session ${sessionId}; retrying with continuation prompt.
|
|
1147
|
+
`
|
|
1148
|
+
);
|
|
1149
|
+
}
|
|
1150
|
+
continue;
|
|
1151
|
+
}
|
|
1152
|
+
const message = "Gemini completed with no assistant text or tool activity.";
|
|
1153
|
+
emit(syntheticErrorMessage("Agent Error", message));
|
|
1154
|
+
return await emitFinalResult(true, message);
|
|
1155
|
+
}
|
|
1156
|
+
const needsCompletionCheck = success && !completionCheckIssued && sessionId && numberedStepCount >= 2 && totalToolUseCount > 0 && totalToolUseCount < numberedStepCount;
|
|
1157
|
+
if (needsCompletionCheck) {
|
|
1158
|
+
completionCheckIssued = true;
|
|
1159
|
+
resumeSessionId = sessionId;
|
|
1160
|
+
attemptPrompt = buildRemainingStepsPrompt();
|
|
1161
|
+
if (args.verbose) {
|
|
1162
|
+
process2.stderr.write(
|
|
1163
|
+
`[shim] Gemini may have exited before finishing a numbered task list for session ${sessionId}; requesting completion check.
|
|
1164
|
+
`
|
|
1165
|
+
);
|
|
1166
|
+
}
|
|
1167
|
+
continue;
|
|
1168
|
+
}
|
|
1169
|
+
if (success) {
|
|
1170
|
+
const jsonRepairOutcome = await maybeRepairInvalidJsonFiles();
|
|
1171
|
+
if (jsonRepairOutcome === "continue") {
|
|
1172
|
+
continue;
|
|
1173
|
+
}
|
|
1174
|
+
if (typeof jsonRepairOutcome === "number") {
|
|
1175
|
+
return jsonRepairOutcome;
|
|
1176
|
+
}
|
|
1177
|
+
} else if (!syntheticErrorEmitted) {
|
|
1178
|
+
emit(syntheticErrorMessage("Agent Error", lastSyntheticError || `Gemini CLI exited with code ${exitCode}`));
|
|
1179
|
+
syntheticErrorEmitted = true;
|
|
1180
|
+
}
|
|
1181
|
+
const resultText = success ? summarizeText(assistantTextChunks.join("")) : lastSyntheticError || `Gemini CLI exited with code ${exitCode}`;
|
|
1182
|
+
return await emitFinalResult(!success, resultText);
|
|
1183
|
+
}
|
|
1184
|
+
await flushStdout();
|
|
1185
|
+
return finalIsError ? 1 : exitCode === 0 ? 0 : 1;
|
|
1186
|
+
} catch (error) {
|
|
1187
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1188
|
+
const _isTimeout = error instanceof IdleTimeoutError || error instanceof BusyStepTimeoutError;
|
|
1189
|
+
if (args.verbose) {
|
|
1190
|
+
process2.stderr.write(`[shim] ${message}
|
|
1191
|
+
`);
|
|
1192
|
+
}
|
|
1193
|
+
if (debug.debugDir && !sessionId) {
|
|
1194
|
+
await writeUnknownLog(debug, `${message}
|
|
1195
|
+
`);
|
|
1196
|
+
}
|
|
1197
|
+
activeChild?.kill("SIGKILL");
|
|
1198
|
+
activeChild = void 0;
|
|
1199
|
+
flushAssistantText();
|
|
1200
|
+
if (systemEmitted) {
|
|
1201
|
+
emit(syntheticErrorMessage("Agent Error", message));
|
|
1202
|
+
return await emitFinalResult(true, message);
|
|
1203
|
+
}
|
|
1204
|
+
process2.stderr.write(`${message}
|
|
1205
|
+
`);
|
|
1206
|
+
await flushStdout();
|
|
1207
|
+
return 1;
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
} finally {
|
|
1211
|
+
process2.removeListener("SIGINT", signalHandler);
|
|
1212
|
+
process2.removeListener("SIGTERM", signalHandler);
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
function spawnGemini({
|
|
1216
|
+
args,
|
|
1217
|
+
cwd,
|
|
1218
|
+
geminiModel,
|
|
1219
|
+
geminiPath,
|
|
1220
|
+
resumeSessionId
|
|
1221
|
+
}) {
|
|
1222
|
+
const isWindows = process2.platform === "win32";
|
|
1223
|
+
const geminiArgs = ["--model", geminiModel, "--output-format", "stream-json", "--approval-mode", "yolo"];
|
|
1224
|
+
if (resumeSessionId) {
|
|
1225
|
+
geminiArgs.push("--resume", resumeSessionId);
|
|
1226
|
+
}
|
|
1227
|
+
if (args.sandbox === "standard" || args.sandbox === "strict") {
|
|
1228
|
+
geminiArgs.push("--sandbox");
|
|
1229
|
+
}
|
|
1230
|
+
const env = {
|
|
1231
|
+
...process2.env,
|
|
1232
|
+
FORCE_COLOR: "0",
|
|
1233
|
+
GEMINI_SANDBOX: args.sandbox === "none" ? "false" : process2.env.GEMINI_SANDBOX || "true",
|
|
1234
|
+
BASH_ENV: ""
|
|
1235
|
+
};
|
|
1236
|
+
if (args.sandbox === "strict" && process2.platform === "darwin") {
|
|
1237
|
+
env.SEATBELT_PROFILE = "restrictive-open";
|
|
1238
|
+
}
|
|
1239
|
+
return spawn(geminiPath, geminiArgs, {
|
|
1240
|
+
cwd,
|
|
1241
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1242
|
+
env,
|
|
1243
|
+
shell: isWindows
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
async function* readGeminiEvents(child, debug, verbose) {
|
|
1247
|
+
const rl = createInterface({ input: child.stdout, crlfDelay: Infinity });
|
|
1248
|
+
for await (const line of rl) {
|
|
1249
|
+
if (!line.trim()) continue;
|
|
1250
|
+
let parsed;
|
|
1251
|
+
try {
|
|
1252
|
+
parsed = JSON.parse(line);
|
|
1253
|
+
} catch {
|
|
1254
|
+
if (verbose) {
|
|
1255
|
+
process2.stderr.write(`[shim] Skipping non-JSON Gemini stdout line: ${line}
|
|
1256
|
+
`);
|
|
1257
|
+
}
|
|
1258
|
+
continue;
|
|
1259
|
+
}
|
|
1260
|
+
if (debug.sessionId && debug.jsonlPath) {
|
|
1261
|
+
await writeJsonDebugLine(debug.jsonlPath, parsed);
|
|
1262
|
+
} else {
|
|
1263
|
+
debug.bufferedEvents.push(parsed);
|
|
1264
|
+
}
|
|
1265
|
+
yield parsed;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
async function waitForSessionFile(sessionId) {
|
|
1269
|
+
const started = Date.now();
|
|
1270
|
+
while (Date.now() - started <= 2e3) {
|
|
1271
|
+
const filePath = await findGeminiSessionFile(sessionId);
|
|
1272
|
+
if (filePath) {
|
|
1273
|
+
return await readSessionFile(sessionId);
|
|
1274
|
+
}
|
|
1275
|
+
await sleep(100);
|
|
1276
|
+
}
|
|
1277
|
+
return await readSessionFile(sessionId);
|
|
1278
|
+
}
|
|
1279
|
+
async function runSelfTest() {
|
|
1280
|
+
const geminiPath = await which("gemini");
|
|
1281
|
+
const authType = await loadGeminiSettingsAuthType();
|
|
1282
|
+
const checks = [
|
|
1283
|
+
{
|
|
1284
|
+
name: "agent_found",
|
|
1285
|
+
passed: Boolean(geminiPath),
|
|
1286
|
+
message: geminiPath ? `Found Gemini CLI at ${geminiPath}` : "Gemini CLI not found in PATH"
|
|
1287
|
+
},
|
|
1288
|
+
{
|
|
1289
|
+
name: "auth_configured",
|
|
1290
|
+
passed: Boolean(process2.env.GEMINI_API_KEY || process2.env.GOOGLE_API_KEY || authType),
|
|
1291
|
+
message: process2.env.GEMINI_API_KEY || process2.env.GOOGLE_API_KEY || authType ? `Authentication appears configured (${authType || "environment"})` : "No Gemini auth configuration detected"
|
|
1292
|
+
}
|
|
1293
|
+
];
|
|
1294
|
+
const overallPassed = checks.every((check) => check.passed);
|
|
1295
|
+
const payload = {
|
|
1296
|
+
shim: { name: SHIM_NAME, version: SHIM_VERSION },
|
|
1297
|
+
agent: { name: "gemini", version: await detectGeminiVersion(), found: Boolean(geminiPath) },
|
|
1298
|
+
checks,
|
|
1299
|
+
overall: {
|
|
1300
|
+
passed: overallPassed,
|
|
1301
|
+
message: overallPassed ? "All checks passed" : "One or more checks failed"
|
|
1302
|
+
}
|
|
1303
|
+
};
|
|
1304
|
+
process2.stdout.write(JSON.stringify(payload, null, 2) + "\n");
|
|
1305
|
+
return overallPassed ? 0 : 1;
|
|
1306
|
+
}
|
|
1307
|
+
async function detectGeminiVersion() {
|
|
1308
|
+
const geminiPath = await which("gemini");
|
|
1309
|
+
if (!geminiPath) return "unknown";
|
|
1310
|
+
return await new Promise((resolve) => {
|
|
1311
|
+
const isWindows = process2.platform === "win32";
|
|
1312
|
+
const child = spawn(geminiPath, ["--version"], {
|
|
1313
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1314
|
+
shell: isWindows
|
|
1315
|
+
});
|
|
1316
|
+
let output = "";
|
|
1317
|
+
child.stdout.on("data", (chunk) => {
|
|
1318
|
+
output += String(chunk);
|
|
1319
|
+
});
|
|
1320
|
+
child.on("close", () => resolve(output.trim() || "unknown"));
|
|
1321
|
+
child.on("error", () => resolve("unknown"));
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
// src/index.ts
|
|
1326
|
+
async function main() {
|
|
1327
|
+
try {
|
|
1328
|
+
const args = parseArgs(process3.argv.slice(2));
|
|
1329
|
+
if (args.help) {
|
|
1330
|
+
printHelp();
|
|
1331
|
+
return 0;
|
|
1332
|
+
}
|
|
1333
|
+
if (args.version) {
|
|
1334
|
+
process3.stdout.write(`${SHIM_VERSION}
|
|
1335
|
+
`);
|
|
1336
|
+
return 0;
|
|
1337
|
+
}
|
|
1338
|
+
if (args.selfTest) {
|
|
1339
|
+
return await runSelfTest();
|
|
1340
|
+
}
|
|
1341
|
+
const prompt = await readStdinTrimmed();
|
|
1342
|
+
if (!prompt) {
|
|
1343
|
+
return 0;
|
|
1344
|
+
}
|
|
1345
|
+
return await runShim(args, prompt);
|
|
1346
|
+
} catch (error) {
|
|
1347
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1348
|
+
process3.stderr.write(`${message}
|
|
1349
|
+
`);
|
|
1350
|
+
return 1;
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
main().then((code) => {
|
|
1354
|
+
process3.exit(code);
|
|
1355
|
+
}).catch((error) => {
|
|
1356
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1357
|
+
process3.stderr.write(`${message}
|
|
1358
|
+
`);
|
|
1359
|
+
process3.exit(1);
|
|
1360
|
+
});
|