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
|
@@ -0,0 +1,1476 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import fs4 from "node:fs";
|
|
5
|
+
import path4 from "node:path";
|
|
6
|
+
|
|
7
|
+
// node_modules/@shims/common/src/args.ts
|
|
8
|
+
var VALID_SANDBOX_LEVELS = ["none", "standard", "strict"];
|
|
9
|
+
function parseArgs(argv, aliases) {
|
|
10
|
+
const args = {
|
|
11
|
+
model: "",
|
|
12
|
+
verbose: false,
|
|
13
|
+
idleTimeout: 120,
|
|
14
|
+
sandbox: "none",
|
|
15
|
+
selfTest: false,
|
|
16
|
+
version: false,
|
|
17
|
+
help: false
|
|
18
|
+
};
|
|
19
|
+
for (let i = 0;i < argv.length; i++) {
|
|
20
|
+
let arg = argv[i];
|
|
21
|
+
if (arg.includes("=")) {
|
|
22
|
+
const [key, value] = arg.split("=", 2);
|
|
23
|
+
argv.splice(i, 1, key, value);
|
|
24
|
+
arg = key;
|
|
25
|
+
}
|
|
26
|
+
if (aliases && arg in aliases) {
|
|
27
|
+
arg = aliases[arg];
|
|
28
|
+
}
|
|
29
|
+
switch (arg) {
|
|
30
|
+
case "--model":
|
|
31
|
+
args.model = argv[++i];
|
|
32
|
+
break;
|
|
33
|
+
case "--resume":
|
|
34
|
+
args.resume = argv[++i];
|
|
35
|
+
break;
|
|
36
|
+
case "--verbose":
|
|
37
|
+
args.verbose = true;
|
|
38
|
+
break;
|
|
39
|
+
case "--append-system-prompt":
|
|
40
|
+
args.appendSystemPrompt = argv[++i];
|
|
41
|
+
break;
|
|
42
|
+
case "--debug-dir":
|
|
43
|
+
args.debugDir = argv[++i];
|
|
44
|
+
break;
|
|
45
|
+
case "--idle-timeout": {
|
|
46
|
+
const val = Number(argv[++i]);
|
|
47
|
+
if (!Number.isFinite(val) || val <= 0) {
|
|
48
|
+
console.error("Invalid --idle-timeout value: must be a positive number");
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
args.idleTimeout = val;
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
case "--sandbox": {
|
|
55
|
+
const level = argv[++i];
|
|
56
|
+
if (!VALID_SANDBOX_LEVELS.includes(level)) {
|
|
57
|
+
console.error(`Invalid --sandbox value: must be one of ${VALID_SANDBOX_LEVELS.join(", ")}`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
args.sandbox = level;
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
case "--self-test":
|
|
64
|
+
args.selfTest = true;
|
|
65
|
+
break;
|
|
66
|
+
case "--version":
|
|
67
|
+
args.version = true;
|
|
68
|
+
break;
|
|
69
|
+
case "--help":
|
|
70
|
+
args.help = true;
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return args;
|
|
75
|
+
}
|
|
76
|
+
// node_modules/@shims/common/src/sessions.ts
|
|
77
|
+
import { randomUUID } from "node:crypto";
|
|
78
|
+
import fs from "node:fs";
|
|
79
|
+
import path from "node:path";
|
|
80
|
+
|
|
81
|
+
class SessionManager {
|
|
82
|
+
sessionsDir;
|
|
83
|
+
constructor(options = {}) {
|
|
84
|
+
if (options.debugDir) {
|
|
85
|
+
this.sessionsDir = path.join(options.debugDir, "sessions");
|
|
86
|
+
} else {
|
|
87
|
+
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
88
|
+
if (!home) {
|
|
89
|
+
throw new Error("Cannot determine home directory for session storage");
|
|
90
|
+
}
|
|
91
|
+
this.sessionsDir = path.join(home, ".shim", "sessions");
|
|
92
|
+
}
|
|
93
|
+
fs.mkdirSync(this.sessionsDir, { recursive: true });
|
|
94
|
+
}
|
|
95
|
+
generateSessionId() {
|
|
96
|
+
return randomUUID();
|
|
97
|
+
}
|
|
98
|
+
saveSession(data) {
|
|
99
|
+
const sessionPath = path.join(this.sessionsDir, `${data.sessionId}.json`);
|
|
100
|
+
fs.writeFileSync(sessionPath, JSON.stringify(data, null, 2), "utf8");
|
|
101
|
+
}
|
|
102
|
+
loadSession(sessionId) {
|
|
103
|
+
const sessionPath = path.join(this.sessionsDir, `${sessionId}.json`);
|
|
104
|
+
try {
|
|
105
|
+
const content = fs.readFileSync(sessionPath, "utf8");
|
|
106
|
+
return JSON.parse(content);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
if (error.code === "ENOENT") {
|
|
109
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
110
|
+
}
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
sessionExists(sessionId) {
|
|
115
|
+
const sessionPath = path.join(this.sessionsDir, `${sessionId}.json`);
|
|
116
|
+
return fs.existsSync(sessionPath);
|
|
117
|
+
}
|
|
118
|
+
deleteSession(sessionId) {
|
|
119
|
+
const sessionPath = path.join(this.sessionsDir, `${sessionId}.json`);
|
|
120
|
+
try {
|
|
121
|
+
fs.unlinkSync(sessionPath);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
if (error.code !== "ENOENT") {
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
listSessions() {
|
|
129
|
+
try {
|
|
130
|
+
const files = fs.readdirSync(this.sessionsDir);
|
|
131
|
+
return files.filter((f) => f.endsWith(".json")).map((f) => f.replace(/\.json$/, ""));
|
|
132
|
+
} catch (error) {
|
|
133
|
+
if (error.code === "ENOENT") {
|
|
134
|
+
return [];
|
|
135
|
+
}
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
getSessionsDir() {
|
|
140
|
+
return this.sessionsDir;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// node_modules/@shims/common/src/timeout.ts
|
|
144
|
+
class IdleTimeoutError extends Error {
|
|
145
|
+
timeoutMs;
|
|
146
|
+
constructor(timeoutMs) {
|
|
147
|
+
super(`Idle timeout: no events received for ${timeoutMs}ms`);
|
|
148
|
+
this.name = "IdleTimeoutError";
|
|
149
|
+
this.timeoutMs = timeoutMs;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
class BusyStepTimeoutError 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(options.busyTimeoutMs ?? options.idleTimeoutMs, options.idleTimeoutMs);
|
|
165
|
+
const controller = {
|
|
166
|
+
markBusy() {
|
|
167
|
+
state = "busy";
|
|
168
|
+
},
|
|
169
|
+
markIdle() {
|
|
170
|
+
state = "idle";
|
|
171
|
+
},
|
|
172
|
+
get state() {
|
|
173
|
+
return state;
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
try {
|
|
177
|
+
while (true) {
|
|
178
|
+
const timeoutMs = state === "busy" ? busyTimeoutMs : options.idleTimeoutMs;
|
|
179
|
+
let timeoutId;
|
|
180
|
+
try {
|
|
181
|
+
const result = await Promise.race([
|
|
182
|
+
iterator.next(),
|
|
183
|
+
new Promise((_, reject) => {
|
|
184
|
+
timeoutId = setTimeout(() => {
|
|
185
|
+
reject(state === "busy" ? new BusyStepTimeoutError(timeoutMs) : new IdleTimeoutError(timeoutMs));
|
|
186
|
+
}, timeoutMs);
|
|
187
|
+
})
|
|
188
|
+
]);
|
|
189
|
+
if (result.done)
|
|
190
|
+
break;
|
|
191
|
+
options.onEvent?.(result.value, controller);
|
|
192
|
+
yield result.value;
|
|
193
|
+
} finally {
|
|
194
|
+
clearTimeout(timeoutId);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
} finally {
|
|
198
|
+
iterator.return?.();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// node_modules/@shims/common/src/tools.ts
|
|
202
|
+
var STANDARD_TOOLS = ["Read", "Write", "Edit", "Bash", "Glob", "Grep", "LS"];
|
|
203
|
+
// package.json
|
|
204
|
+
var package_default = {
|
|
205
|
+
name: "opencode-shim",
|
|
206
|
+
version: "1.0.0",
|
|
207
|
+
description: "Self-contained Hankweave shim package for the OpenCode CLI",
|
|
208
|
+
type: "module",
|
|
209
|
+
bin: {
|
|
210
|
+
"opencode-shim": "./index.js"
|
|
211
|
+
},
|
|
212
|
+
files: [
|
|
213
|
+
"index.js",
|
|
214
|
+
"dist",
|
|
215
|
+
"src",
|
|
216
|
+
"common",
|
|
217
|
+
"docs",
|
|
218
|
+
"README.md",
|
|
219
|
+
"rebuild.sh",
|
|
220
|
+
"VERSION",
|
|
221
|
+
"THIRDPARTY.md",
|
|
222
|
+
"tsconfig.json"
|
|
223
|
+
],
|
|
224
|
+
scripts: {
|
|
225
|
+
build: `node -e "const fs=require('fs'); fs.rmSync('./dist',{recursive:true,force:true}); fs.mkdirSync('./dist',{recursive:true});" && bun build ./src/index.ts --target node --format esm --outfile ./dist/index.js`,
|
|
226
|
+
rebuild: "./rebuild.sh",
|
|
227
|
+
test: "bun test",
|
|
228
|
+
typecheck: "tsc --noEmit",
|
|
229
|
+
clean: `node -e "const fs=require('fs'); fs.rmSync('./dist',{recursive:true,force:true}); fs.rmSync('./index.js',{force:true});"`
|
|
230
|
+
},
|
|
231
|
+
dependencies: {
|
|
232
|
+
"@shims/common": "file:./common"
|
|
233
|
+
},
|
|
234
|
+
devDependencies: {
|
|
235
|
+
"@types/bun": "latest",
|
|
236
|
+
typescript: "^5.3.0"
|
|
237
|
+
},
|
|
238
|
+
engines: {
|
|
239
|
+
bun: ">=1.1.0"
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
// src/selftest.ts
|
|
244
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
245
|
+
|
|
246
|
+
// src/agent/opencode.ts
|
|
247
|
+
import fs2 from "node:fs";
|
|
248
|
+
import os from "node:os";
|
|
249
|
+
import path2 from "node:path";
|
|
250
|
+
import readline from "node:readline";
|
|
251
|
+
import { spawn } from "node:child_process";
|
|
252
|
+
|
|
253
|
+
// src/utils/prompt.ts
|
|
254
|
+
var INTERNAL_INSTRUCTIONS = [
|
|
255
|
+
"Operate headlessly and do not ask the user for confirmation.",
|
|
256
|
+
"Finish all explicit user-requested steps before ending the turn unless a real error prevents completion.",
|
|
257
|
+
"If the user requested multiple ordered actions, do not stop after only a partial subset.",
|
|
258
|
+
"Only use files and absolute paths inside the current working directory unless the user explicitly asks for an external path."
|
|
259
|
+
];
|
|
260
|
+
function buildInstructionFileContents(params) {
|
|
261
|
+
const sections = [
|
|
262
|
+
"# Instructions injected by opencode-shim",
|
|
263
|
+
"",
|
|
264
|
+
"## Internal runtime requirements",
|
|
265
|
+
"",
|
|
266
|
+
...INTERNAL_INSTRUCTIONS.map((line) => `- ${line}`),
|
|
267
|
+
`- Current working directory: ${params.cwd}`
|
|
268
|
+
];
|
|
269
|
+
if (params.appendSystemPrompt?.trim()) {
|
|
270
|
+
sections.push("", "## Additional caller-provided instruction", "", params.appendSystemPrompt.trim(), "", "Treat the caller-provided instruction above as higher priority than the user message.");
|
|
271
|
+
}
|
|
272
|
+
return sections.join(`
|
|
273
|
+
`);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// src/utils/output.ts
|
|
277
|
+
function emit(message) {
|
|
278
|
+
process.stdout.write(`${JSON.stringify(message)}
|
|
279
|
+
`);
|
|
280
|
+
}
|
|
281
|
+
async function flushStdout() {
|
|
282
|
+
await new Promise((resolve) => {
|
|
283
|
+
process.stdout.write("", () => resolve());
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
function verboseLog(enabled, message, data) {
|
|
287
|
+
if (!enabled)
|
|
288
|
+
return;
|
|
289
|
+
if (data === undefined) {
|
|
290
|
+
process.stderr.write(`[opencode-shim] ${message}
|
|
291
|
+
`);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
const rendered = typeof data === "string" ? data : JSON.stringify(data);
|
|
295
|
+
process.stderr.write(`[opencode-shim] ${message}: ${rendered}
|
|
296
|
+
`);
|
|
297
|
+
}
|
|
298
|
+
function errorMessage(error) {
|
|
299
|
+
return error instanceof Error ? error.message : String(error);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// src/agent/opencode.ts
|
|
303
|
+
async function ensureOpencodeAvailable() {
|
|
304
|
+
const envPath = process.env.OPENCODE_BIN?.trim();
|
|
305
|
+
const candidates = [
|
|
306
|
+
envPath,
|
|
307
|
+
path2.join(os.homedir(), ".opencode", "bin", process.platform === "win32" ? "opencode.cmd" : "opencode"),
|
|
308
|
+
path2.join(os.homedir(), ".opencode", "bin", "opencode"),
|
|
309
|
+
"opencode"
|
|
310
|
+
].filter((value) => Boolean(value));
|
|
311
|
+
for (const candidate of candidates) {
|
|
312
|
+
if (candidate.includes(path2.sep) || candidate.startsWith(".")) {
|
|
313
|
+
if (fs2.existsSync(candidate)) {
|
|
314
|
+
return candidate;
|
|
315
|
+
}
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
const resolved = await findOnPath(candidate);
|
|
319
|
+
if (resolved) {
|
|
320
|
+
return resolved;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
throw new Error("OpenCode CLI not found. Set OPENCODE_BIN or install opencode.");
|
|
324
|
+
}
|
|
325
|
+
async function findOnPath(command) {
|
|
326
|
+
const locator = process.platform === "win32" ? "where" : "which";
|
|
327
|
+
const isWindows = process.platform === "win32";
|
|
328
|
+
return await new Promise((resolve) => {
|
|
329
|
+
const proc = spawn(locator, [command], {
|
|
330
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
331
|
+
shell: isWindows
|
|
332
|
+
});
|
|
333
|
+
let output = "";
|
|
334
|
+
proc.stdout.on("data", (chunk) => {
|
|
335
|
+
output += chunk.toString();
|
|
336
|
+
});
|
|
337
|
+
proc.on("close", (code) => {
|
|
338
|
+
if (code === 0) {
|
|
339
|
+
const first = output.split(/\r?\n/).map((line) => line.trim()).find(Boolean);
|
|
340
|
+
resolve(first);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
resolve(undefined);
|
|
344
|
+
});
|
|
345
|
+
proc.on("error", () => resolve(undefined));
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
async function runOpencodeCommand(binaryPath, args2) {
|
|
349
|
+
const isWindows = process.platform === "win32";
|
|
350
|
+
return await new Promise((resolve, reject) => {
|
|
351
|
+
const child = spawn(binaryPath, args2, {
|
|
352
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
353
|
+
shell: isWindows,
|
|
354
|
+
env: process.env
|
|
355
|
+
});
|
|
356
|
+
let stdout = "";
|
|
357
|
+
let stderr = "";
|
|
358
|
+
child.stdout.on("data", (chunk) => {
|
|
359
|
+
stdout += chunk.toString();
|
|
360
|
+
});
|
|
361
|
+
child.stderr.on("data", (chunk) => {
|
|
362
|
+
stderr += chunk.toString();
|
|
363
|
+
});
|
|
364
|
+
child.once("error", reject);
|
|
365
|
+
child.once("close", (code) => {
|
|
366
|
+
resolve({ code, stdout, stderr });
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
async function findSessionIdByTitle(title) {
|
|
371
|
+
const binaryPath = await ensureOpencodeAvailable();
|
|
372
|
+
const result = await runOpencodeCommand(binaryPath, ["session", "list"]);
|
|
373
|
+
if (result.code !== 0) {
|
|
374
|
+
throw new Error(result.stderr.trim() || "Failed to list OpenCode sessions.");
|
|
375
|
+
}
|
|
376
|
+
const normalizedTitle = title.trim();
|
|
377
|
+
for (const rawLine of result.stdout.split(/\r?\n/)) {
|
|
378
|
+
const line = rawLine.trim();
|
|
379
|
+
if (!line.startsWith("ses_"))
|
|
380
|
+
continue;
|
|
381
|
+
const match = line.match(/^(ses_[A-Za-z0-9]+)\s+(.*)$/);
|
|
382
|
+
if (!match)
|
|
383
|
+
continue;
|
|
384
|
+
const [, nativeSessionId, remainder] = match;
|
|
385
|
+
if (remainder.includes(normalizedTitle)) {
|
|
386
|
+
return nativeSessionId;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
async function deleteSessionById(sessionId) {
|
|
392
|
+
const binaryPath = await ensureOpencodeAvailable();
|
|
393
|
+
const result = await runOpencodeCommand(binaryPath, ["session", "delete", sessionId]);
|
|
394
|
+
if (result.code !== 0) {
|
|
395
|
+
throw new Error(result.stderr.trim() || `Failed to delete OpenCode session ${sessionId}.`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
function createTemporaryInstructionContext(options) {
|
|
399
|
+
const config = {
|
|
400
|
+
permission: "allow"
|
|
401
|
+
};
|
|
402
|
+
const baseDir = options.debugDir ?? fs2.mkdtempSync(path2.join(os.tmpdir(), "opencode-shim-"));
|
|
403
|
+
fs2.mkdirSync(baseDir, { recursive: true });
|
|
404
|
+
const instructionPath = path2.join(baseDir, `system-prompt-${options.shimSessionId}.md`);
|
|
405
|
+
fs2.writeFileSync(instructionPath, `${buildInstructionFileContents({ cwd: options.cwd, appendSystemPrompt: options.appendSystemPrompt })}
|
|
406
|
+
`, "utf8");
|
|
407
|
+
config.instructions = [instructionPath];
|
|
408
|
+
return {
|
|
409
|
+
envConfigContent: JSON.stringify(config),
|
|
410
|
+
cleanup() {
|
|
411
|
+
try {
|
|
412
|
+
fs2.rmSync(instructionPath, { force: true });
|
|
413
|
+
} catch {}
|
|
414
|
+
if (!options.debugDir) {
|
|
415
|
+
try {
|
|
416
|
+
fs2.rmSync(baseDir, { recursive: true, force: true });
|
|
417
|
+
} catch {}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
function createDebugWriters(debugDir, shimSessionId) {
|
|
423
|
+
if (!debugDir) {
|
|
424
|
+
return {};
|
|
425
|
+
}
|
|
426
|
+
fs2.mkdirSync(debugDir, { recursive: true });
|
|
427
|
+
return {
|
|
428
|
+
rawJsonlPath: path2.join(debugDir, `session-${shimSessionId}.raw.jsonl`),
|
|
429
|
+
rawStderrPath: path2.join(debugDir, `session-${shimSessionId}.raw.log`)
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
function appendDebug(pathname, chunk) {
|
|
433
|
+
if (!pathname)
|
|
434
|
+
return;
|
|
435
|
+
fs2.appendFileSync(pathname, chunk, "utf8");
|
|
436
|
+
}
|
|
437
|
+
async function spawnOpencodeRun(options) {
|
|
438
|
+
const binaryPath = await ensureOpencodeAvailable();
|
|
439
|
+
const tempInstructionContext = createTemporaryInstructionContext(options);
|
|
440
|
+
const debugWriters = createDebugWriters(options.debugDir, options.shimSessionId);
|
|
441
|
+
const isWindows = process.platform === "win32";
|
|
442
|
+
if (options.sandbox !== "none") {
|
|
443
|
+
verboseLog(options.verbose, `OpenCode CLI has no documented sandbox flag for run mode; ignoring --sandbox=${options.sandbox}`);
|
|
444
|
+
}
|
|
445
|
+
const args2 = [
|
|
446
|
+
"run",
|
|
447
|
+
"--format",
|
|
448
|
+
"json",
|
|
449
|
+
"--model",
|
|
450
|
+
options.model,
|
|
451
|
+
"--dir",
|
|
452
|
+
options.cwd
|
|
453
|
+
];
|
|
454
|
+
if (options.nativeSessionId) {
|
|
455
|
+
args2.push("--session", options.nativeSessionId);
|
|
456
|
+
} else {
|
|
457
|
+
args2.push("--title", options.shimSessionId);
|
|
458
|
+
}
|
|
459
|
+
const child = spawn(binaryPath, args2, {
|
|
460
|
+
cwd: options.cwd,
|
|
461
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
462
|
+
shell: isWindows,
|
|
463
|
+
env: {
|
|
464
|
+
...process.env,
|
|
465
|
+
OPENCODE_CONFIG_CONTENT: tempInstructionContext.envConfigContent
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
child.stdin.write(options.prompt);
|
|
469
|
+
child.stdin.end();
|
|
470
|
+
let stderrBuffer = "";
|
|
471
|
+
child.stderr.on("data", (chunk) => {
|
|
472
|
+
const text = chunk.toString();
|
|
473
|
+
stderrBuffer += text;
|
|
474
|
+
appendDebug(debugWriters.rawStderrPath, text);
|
|
475
|
+
});
|
|
476
|
+
const events = createEventStream(child, debugWriters.rawJsonlPath, options.verbose, options.idleTimeoutMs);
|
|
477
|
+
const exitPromise = new Promise((resolve, reject) => {
|
|
478
|
+
child.once("error", (error) => reject(error));
|
|
479
|
+
child.once("close", (code, signal) => resolve({ code, signal }));
|
|
480
|
+
}).finally(() => {
|
|
481
|
+
tempInstructionContext.cleanup();
|
|
482
|
+
});
|
|
483
|
+
return {
|
|
484
|
+
binaryPath,
|
|
485
|
+
events,
|
|
486
|
+
async waitForExit() {
|
|
487
|
+
return exitPromise;
|
|
488
|
+
},
|
|
489
|
+
kill(signal = "SIGTERM") {
|
|
490
|
+
if (!child.killed) {
|
|
491
|
+
try {
|
|
492
|
+
child.kill(signal);
|
|
493
|
+
} catch {}
|
|
494
|
+
}
|
|
495
|
+
},
|
|
496
|
+
getStderr() {
|
|
497
|
+
return stderrBuffer;
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
function computeBusyTimeoutMs(idleTimeoutMs) {
|
|
502
|
+
return Math.max(300000, Math.min(900000, idleTimeoutMs * 5));
|
|
503
|
+
}
|
|
504
|
+
function createEventStream(child, rawJsonlPath, verbose, idleTimeoutMs) {
|
|
505
|
+
const lineStream = readline.createInterface({ input: child.stdout, crlfDelay: Infinity });
|
|
506
|
+
const parsedEvents = async function* () {
|
|
507
|
+
for await (const rawLine of lineStream) {
|
|
508
|
+
const line = rawLine.trim();
|
|
509
|
+
if (!line)
|
|
510
|
+
continue;
|
|
511
|
+
appendDebug(rawJsonlPath, `${line}
|
|
512
|
+
`);
|
|
513
|
+
try {
|
|
514
|
+
yield JSON.parse(line);
|
|
515
|
+
} catch (error) {
|
|
516
|
+
verboseLog(verbose, "Skipping non-JSON stdout line", {
|
|
517
|
+
line,
|
|
518
|
+
error: errorMessage(error)
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}();
|
|
523
|
+
return withAdaptiveTimeout(parsedEvents, {
|
|
524
|
+
idleTimeoutMs,
|
|
525
|
+
busyTimeoutMs: computeBusyTimeoutMs(idleTimeoutMs),
|
|
526
|
+
onEvent(event, controller) {
|
|
527
|
+
if (event.type === "step_start") {
|
|
528
|
+
controller.markBusy();
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
if (event.type === "step_finish" || event.type === "error") {
|
|
532
|
+
controller.markIdle();
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
function filterDiagnosticStderr(stderr) {
|
|
538
|
+
return stderr.split(/\r?\n/).filter((line) => {
|
|
539
|
+
const trimmed = line.trim();
|
|
540
|
+
if (!trimmed)
|
|
541
|
+
return false;
|
|
542
|
+
try {
|
|
543
|
+
const parsed = JSON.parse(trimmed);
|
|
544
|
+
return typeof parsed?.type !== "string";
|
|
545
|
+
} catch {
|
|
546
|
+
return true;
|
|
547
|
+
}
|
|
548
|
+
}).join(`
|
|
549
|
+
`).trim();
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// src/selftest.ts
|
|
553
|
+
async function runCommand(command, args2) {
|
|
554
|
+
const isWindows = process.platform === "win32";
|
|
555
|
+
return await new Promise((resolve) => {
|
|
556
|
+
const child = spawn2(command, args2, {
|
|
557
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
558
|
+
shell: isWindows,
|
|
559
|
+
env: process.env
|
|
560
|
+
});
|
|
561
|
+
let stdout = "";
|
|
562
|
+
let stderr = "";
|
|
563
|
+
child.stdout.on("data", (chunk) => {
|
|
564
|
+
stdout += chunk.toString();
|
|
565
|
+
});
|
|
566
|
+
child.stderr.on("data", (chunk) => {
|
|
567
|
+
stderr += chunk.toString();
|
|
568
|
+
});
|
|
569
|
+
child.on("close", (code) => resolve({ code, stdout, stderr }));
|
|
570
|
+
child.on("error", () => resolve({ code: 1, stdout, stderr }));
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
async function runSelfTest(version) {
|
|
574
|
+
const checks = [];
|
|
575
|
+
let binary = "";
|
|
576
|
+
let opencodeVersion = "unknown";
|
|
577
|
+
try {
|
|
578
|
+
binary = await ensureOpencodeAvailable();
|
|
579
|
+
checks.push({
|
|
580
|
+
name: "agent_found",
|
|
581
|
+
passed: true,
|
|
582
|
+
message: `Found OpenCode at ${binary}`
|
|
583
|
+
});
|
|
584
|
+
const versionResult = await runCommand(binary, ["--version"]);
|
|
585
|
+
if (versionResult.code === 0) {
|
|
586
|
+
opencodeVersion = versionResult.stdout.trim() || "unknown";
|
|
587
|
+
checks.push({
|
|
588
|
+
name: "agent_version",
|
|
589
|
+
passed: true,
|
|
590
|
+
message: `OpenCode version ${opencodeVersion}`
|
|
591
|
+
});
|
|
592
|
+
} else {
|
|
593
|
+
checks.push({
|
|
594
|
+
name: "agent_version",
|
|
595
|
+
passed: false,
|
|
596
|
+
message: versionResult.stderr.trim() || "Unable to read OpenCode version"
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
const modelsResult = await runCommand(binary, ["models", "google"]);
|
|
600
|
+
checks.push({
|
|
601
|
+
name: "models_command",
|
|
602
|
+
passed: modelsResult.code === 0 && modelsResult.stdout.trim().length > 0,
|
|
603
|
+
message: modelsResult.code === 0 ? "OpenCode model listing succeeded" : modelsResult.stderr.trim() || "OpenCode model listing failed"
|
|
604
|
+
});
|
|
605
|
+
} catch (error) {
|
|
606
|
+
checks.push({
|
|
607
|
+
name: "agent_found",
|
|
608
|
+
passed: false,
|
|
609
|
+
message: errorMessage(error)
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
const hasEnvKey = Boolean(process.env.GOOGLE_API_KEY || process.env.ANTHROPIC_API_KEY || process.env.OPENAI_API_KEY);
|
|
613
|
+
checks.push({
|
|
614
|
+
name: "api_key_hint",
|
|
615
|
+
passed: true,
|
|
616
|
+
message: hasEnvKey ? "Detected at least one provider API key in environment" : "No provider API key detected in environment; OpenCode may still rely on stored auth"
|
|
617
|
+
});
|
|
618
|
+
const overallPassed = checks.every((check) => check.passed);
|
|
619
|
+
process.stdout.write(`${JSON.stringify({
|
|
620
|
+
shim: { name: "opencode-shim", version },
|
|
621
|
+
agent: { name: "opencode", version: opencodeVersion, found: checks.some((c) => c.name === "agent_found" && c.passed) },
|
|
622
|
+
checks,
|
|
623
|
+
overall: {
|
|
624
|
+
passed: overallPassed,
|
|
625
|
+
message: overallPassed ? "All checks passed" : "One or more checks failed"
|
|
626
|
+
}
|
|
627
|
+
}, null, 2)}
|
|
628
|
+
`);
|
|
629
|
+
return overallPassed ? 0 : 1;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// src/shim.ts
|
|
633
|
+
import fs3 from "node:fs";
|
|
634
|
+
import path3 from "node:path";
|
|
635
|
+
|
|
636
|
+
// src/utils/ids.ts
|
|
637
|
+
import { randomBytes, randomUUID as randomUUID2 } from "node:crypto";
|
|
638
|
+
var NIL_UUID = "00000000-0000-0000-0000-000000000000";
|
|
639
|
+
function generateSessionId() {
|
|
640
|
+
return randomUUID2();
|
|
641
|
+
}
|
|
642
|
+
function generateMessageId() {
|
|
643
|
+
return `msg_${randomBytes(8).toString("hex")}`;
|
|
644
|
+
}
|
|
645
|
+
function generateToolUseId() {
|
|
646
|
+
return `toolu_${randomBytes(10).toString("hex")}`;
|
|
647
|
+
}
|
|
648
|
+
function isUuidLike(value) {
|
|
649
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value) || value === NIL_UUID;
|
|
650
|
+
}
|
|
651
|
+
function normalizeMessageId(value) {
|
|
652
|
+
if (typeof value === "string" && /^(msg_[a-zA-Z0-9]+|[0-9a-f-]{36})$/i.test(value)) {
|
|
653
|
+
return value;
|
|
654
|
+
}
|
|
655
|
+
return generateMessageId();
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// src/utils/models.ts
|
|
659
|
+
var SHORTNAME_MAP = {
|
|
660
|
+
sonnet: "anthropic/claude-sonnet-4-20250514",
|
|
661
|
+
haiku: "anthropic/claude-3-haiku-20240307",
|
|
662
|
+
opus: "anthropic/claude-opus-4-20250514",
|
|
663
|
+
flash: "google/gemini-2.5-flash",
|
|
664
|
+
pro: "google/gemini-2.5-pro",
|
|
665
|
+
gpt5: "openai/gpt-5"
|
|
666
|
+
};
|
|
667
|
+
function resolveModel(input) {
|
|
668
|
+
const raw = input.trim();
|
|
669
|
+
if (!raw) {
|
|
670
|
+
return process.env.MODEL?.trim() || "anthropic/claude-sonnet-4-20250514";
|
|
671
|
+
}
|
|
672
|
+
const lowered = raw.toLowerCase();
|
|
673
|
+
if (SHORTNAME_MAP[lowered]) {
|
|
674
|
+
return SHORTNAME_MAP[lowered];
|
|
675
|
+
}
|
|
676
|
+
if (raw.includes("/")) {
|
|
677
|
+
return raw;
|
|
678
|
+
}
|
|
679
|
+
if (lowered.startsWith("claude") || lowered.includes("sonnet") || lowered.includes("haiku") || lowered.includes("opus")) {
|
|
680
|
+
return `anthropic/${raw}`;
|
|
681
|
+
}
|
|
682
|
+
if (lowered.startsWith("gemini") || lowered.includes("flash") || lowered.includes("gemini-")) {
|
|
683
|
+
return `google/${raw}`;
|
|
684
|
+
}
|
|
685
|
+
if (lowered.startsWith("gpt") || lowered.startsWith("o1") || lowered.startsWith("o3") || lowered.startsWith("o4")) {
|
|
686
|
+
return `openai/${raw}`;
|
|
687
|
+
}
|
|
688
|
+
return raw;
|
|
689
|
+
}
|
|
690
|
+
function providerFromModel(model) {
|
|
691
|
+
const [provider] = model.split("/", 1);
|
|
692
|
+
return provider && model.includes("/") ? provider : undefined;
|
|
693
|
+
}
|
|
694
|
+
function detectApiKeySource(model) {
|
|
695
|
+
const provider = providerFromModel(model);
|
|
696
|
+
const providerEnv = provider === "anthropic" ? "ANTHROPIC_API_KEY" : provider === "google" ? "GOOGLE_API_KEY" : provider === "openai" ? "OPENAI_API_KEY" : provider === "groq" ? "GROQ_API_KEY" : provider === "cerebras" ? "CEREBRAS_API_KEY" : undefined;
|
|
697
|
+
if (providerEnv && process.env[providerEnv]) {
|
|
698
|
+
return providerEnv;
|
|
699
|
+
}
|
|
700
|
+
if (process.env.ANTHROPIC_API_KEY || process.env.GOOGLE_API_KEY || process.env.OPENAI_API_KEY || process.env.GROQ_API_KEY || process.env.CEREBRAS_API_KEY) {
|
|
701
|
+
return "env";
|
|
702
|
+
}
|
|
703
|
+
return "none";
|
|
704
|
+
}
|
|
705
|
+
function supportsGeminiEmptyRetry(model) {
|
|
706
|
+
return model.startsWith("google/gemini");
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// src/utils/tools.ts
|
|
710
|
+
var TOOL_NAME_MAP = {
|
|
711
|
+
read: "Read",
|
|
712
|
+
file_read: "Read",
|
|
713
|
+
readfile: "Read",
|
|
714
|
+
write: "Write",
|
|
715
|
+
file_write: "Write",
|
|
716
|
+
writefile: "Write",
|
|
717
|
+
edit: "Edit",
|
|
718
|
+
str_replace_editor: "Edit",
|
|
719
|
+
bash: "Bash",
|
|
720
|
+
shell: "Bash",
|
|
721
|
+
execute_bash: "Bash",
|
|
722
|
+
glob: "Glob",
|
|
723
|
+
find_files: "Glob",
|
|
724
|
+
grep: "Grep",
|
|
725
|
+
search_files: "Grep",
|
|
726
|
+
ls: "LS",
|
|
727
|
+
list: "LS",
|
|
728
|
+
list_directory: "LS"
|
|
729
|
+
};
|
|
730
|
+
function normalizeToolName(name) {
|
|
731
|
+
if (typeof name !== "string" || !name.trim()) {
|
|
732
|
+
return "Tool";
|
|
733
|
+
}
|
|
734
|
+
const trimmed = name.trim();
|
|
735
|
+
const normalizedKey = trimmed.toLowerCase().replace(/[\s-]+/g, "_");
|
|
736
|
+
return TOOL_NAME_MAP[normalizedKey] ?? trimmed;
|
|
737
|
+
}
|
|
738
|
+
function camelToSnakeKey(value) {
|
|
739
|
+
return value.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
|
740
|
+
}
|
|
741
|
+
function isRecord(value) {
|
|
742
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
743
|
+
}
|
|
744
|
+
function normalizeToolInput(toolName, input) {
|
|
745
|
+
if (!isRecord(input)) {
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
const normalized = {};
|
|
749
|
+
for (const [key, value] of Object.entries(input)) {
|
|
750
|
+
normalized[camelToSnakeKey(key)] = value;
|
|
751
|
+
}
|
|
752
|
+
switch (toolName) {
|
|
753
|
+
case "Bash": {
|
|
754
|
+
if (typeof normalized.command === "string") {
|
|
755
|
+
return { command: normalized.command };
|
|
756
|
+
}
|
|
757
|
+
return normalized;
|
|
758
|
+
}
|
|
759
|
+
case "Read": {
|
|
760
|
+
if (typeof normalized.file_path === "string") {
|
|
761
|
+
return { file_path: normalized.file_path };
|
|
762
|
+
}
|
|
763
|
+
return normalized;
|
|
764
|
+
}
|
|
765
|
+
case "Write": {
|
|
766
|
+
const result = {};
|
|
767
|
+
if (typeof normalized.file_path === "string")
|
|
768
|
+
result.file_path = normalized.file_path;
|
|
769
|
+
if (typeof normalized.content === "string")
|
|
770
|
+
result.content = normalized.content;
|
|
771
|
+
return Object.keys(result).length > 0 ? result : normalized;
|
|
772
|
+
}
|
|
773
|
+
case "Edit": {
|
|
774
|
+
const result = {};
|
|
775
|
+
if (typeof normalized.file_path === "string")
|
|
776
|
+
result.file_path = normalized.file_path;
|
|
777
|
+
if (typeof normalized.old_string === "string")
|
|
778
|
+
result.old_string = normalized.old_string;
|
|
779
|
+
if (typeof normalized.new_string === "string")
|
|
780
|
+
result.new_string = normalized.new_string;
|
|
781
|
+
return Object.keys(result).length > 0 ? result : normalized;
|
|
782
|
+
}
|
|
783
|
+
default:
|
|
784
|
+
return normalized;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
function stringifyToolOutput(value) {
|
|
788
|
+
if (typeof value === "string") {
|
|
789
|
+
return value;
|
|
790
|
+
}
|
|
791
|
+
if (value === undefined || value === null) {
|
|
792
|
+
return "";
|
|
793
|
+
}
|
|
794
|
+
return JSON.stringify(value);
|
|
795
|
+
}
|
|
796
|
+
function extractToolResultContent(toolName, state) {
|
|
797
|
+
const rawOutput = stringifyToolOutput(state.output);
|
|
798
|
+
const metadata = isRecord(state.metadata) ? state.metadata : undefined;
|
|
799
|
+
const input = isRecord(state.input) ? state.input : undefined;
|
|
800
|
+
const exitCode = typeof metadata?.exit === "number" ? metadata.exit : undefined;
|
|
801
|
+
const status = typeof state.status === "string" ? state.status : undefined;
|
|
802
|
+
if (status === "error") {
|
|
803
|
+
const errorText = rawOutput || stringifyToolOutput(metadata?.error) || `${toolName} failed`;
|
|
804
|
+
return { is_error: true, error: errorText };
|
|
805
|
+
}
|
|
806
|
+
if (toolName === "Bash" && exitCode !== undefined && exitCode !== 0) {
|
|
807
|
+
const detail = rawOutput.trim();
|
|
808
|
+
return {
|
|
809
|
+
is_error: true,
|
|
810
|
+
error: detail ? `Exit code ${exitCode}: ${detail}` : `Exit code ${exitCode}`
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
if (rawOutput.length > 0) {
|
|
814
|
+
return rawOutput;
|
|
815
|
+
}
|
|
816
|
+
if (toolName === "Write" && typeof input?.filePath === "string") {
|
|
817
|
+
return `File written: ${String(input.filePath)}`;
|
|
818
|
+
}
|
|
819
|
+
if (toolName === "Write" && typeof input?.file_path === "string") {
|
|
820
|
+
return `File written: ${String(input.file_path)}`;
|
|
821
|
+
}
|
|
822
|
+
if (toolName === "Read") {
|
|
823
|
+
const filePath = typeof input?.filePath === "string" ? input.filePath : input?.file_path;
|
|
824
|
+
if (typeof filePath === "string") {
|
|
825
|
+
return `Read file: ${filePath}`;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
if (toolName === "Bash") {
|
|
829
|
+
return exitCode === 0 || exitCode === undefined ? "Command completed successfully with no output." : `Exit code ${exitCode}`;
|
|
830
|
+
}
|
|
831
|
+
if (typeof state.title === "string" && state.title) {
|
|
832
|
+
return state.title;
|
|
833
|
+
}
|
|
834
|
+
return `${toolName} completed.`;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// src/shim.ts
|
|
838
|
+
var MAX_EMPTY_RETRIES = 5;
|
|
839
|
+
function emptyUsage() {
|
|
840
|
+
return {};
|
|
841
|
+
}
|
|
842
|
+
function debugJsonlPath(debugDir, sessionId) {
|
|
843
|
+
return debugDir ? path3.join(debugDir, `session-${sessionId}.raw.jsonl`) : undefined;
|
|
844
|
+
}
|
|
845
|
+
function debugLogPath(debugDir, sessionId) {
|
|
846
|
+
return debugDir ? path3.join(debugDir, `session-${sessionId}.raw.log`) : undefined;
|
|
847
|
+
}
|
|
848
|
+
function appendDebugJson(debugDir, sessionId, payload) {
|
|
849
|
+
const filePath = debugJsonlPath(debugDir, sessionId);
|
|
850
|
+
if (!filePath)
|
|
851
|
+
return;
|
|
852
|
+
fs3.mkdirSync(path3.dirname(filePath), { recursive: true });
|
|
853
|
+
fs3.appendFileSync(filePath, `${JSON.stringify(payload)}
|
|
854
|
+
`, "utf8");
|
|
855
|
+
}
|
|
856
|
+
function appendDebugLog(debugDir, sessionId, text) {
|
|
857
|
+
const filePath = debugLogPath(debugDir, sessionId);
|
|
858
|
+
if (!filePath)
|
|
859
|
+
return;
|
|
860
|
+
fs3.mkdirSync(path3.dirname(filePath), { recursive: true });
|
|
861
|
+
fs3.appendFileSync(filePath, text.endsWith(`
|
|
862
|
+
`) ? text : `${text}
|
|
863
|
+
`, "utf8");
|
|
864
|
+
}
|
|
865
|
+
function touchDebugLog(debugDir, sessionId) {
|
|
866
|
+
const filePath = debugLogPath(debugDir, sessionId);
|
|
867
|
+
if (!filePath)
|
|
868
|
+
return;
|
|
869
|
+
fs3.mkdirSync(path3.dirname(filePath), { recursive: true });
|
|
870
|
+
if (!fs3.existsSync(filePath)) {
|
|
871
|
+
fs3.writeFileSync(filePath, "", "utf8");
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
function addUsage(target, incoming) {
|
|
875
|
+
if (!incoming)
|
|
876
|
+
return;
|
|
877
|
+
for (const key of [
|
|
878
|
+
"input_tokens",
|
|
879
|
+
"output_tokens",
|
|
880
|
+
"cache_creation_input_tokens",
|
|
881
|
+
"cache_read_input_tokens"
|
|
882
|
+
]) {
|
|
883
|
+
const value = incoming[key];
|
|
884
|
+
if (typeof value === "number") {
|
|
885
|
+
target[key] = (target[key] ?? 0) + value;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
function usageFromStepTokens(tokens) {
|
|
890
|
+
if (!tokens)
|
|
891
|
+
return;
|
|
892
|
+
const cache = typeof tokens.cache === "object" && tokens.cache !== null ? tokens.cache : undefined;
|
|
893
|
+
return {
|
|
894
|
+
input_tokens: typeof tokens.input === "number" ? tokens.input : undefined,
|
|
895
|
+
output_tokens: typeof tokens.output === "number" ? tokens.output : undefined,
|
|
896
|
+
cache_creation_input_tokens: typeof cache?.write === "number" ? cache.write : undefined,
|
|
897
|
+
cache_read_input_tokens: typeof cache?.read === "number" ? cache.read : undefined
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
function summarizeAssistantText(step) {
|
|
901
|
+
return step.contentBlocks.filter((block) => block.type === "text").map((block) => block.text).join(`
|
|
902
|
+
`).trim();
|
|
903
|
+
}
|
|
904
|
+
function summarizeLastToolResult(step) {
|
|
905
|
+
const last = step.toolResults.at(-1);
|
|
906
|
+
if (!last)
|
|
907
|
+
return "";
|
|
908
|
+
return typeof last.content === "string" ? last.content : last.content.error;
|
|
909
|
+
}
|
|
910
|
+
function humanizeToolSummary(summary) {
|
|
911
|
+
const contentMatch = summary.match(/<content>([\s\S]*?)<\/content>/i);
|
|
912
|
+
const body = (contentMatch?.[1] ?? summary).trim();
|
|
913
|
+
return body || summary;
|
|
914
|
+
}
|
|
915
|
+
function createStepAccumulator(messageId) {
|
|
916
|
+
return {
|
|
917
|
+
messageId: normalizeMessageId(messageId),
|
|
918
|
+
contentBlocks: [],
|
|
919
|
+
toolResults: []
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
function asRecord(value) {
|
|
923
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : undefined;
|
|
924
|
+
}
|
|
925
|
+
function extractEventError(event) {
|
|
926
|
+
const err = asRecord(event.error);
|
|
927
|
+
const data = asRecord(err?.data);
|
|
928
|
+
return typeof data?.message === "string" && data.message || typeof err?.message === "string" && err.message || "Unknown OpenCode error";
|
|
929
|
+
}
|
|
930
|
+
function isRetryableEmptyGeminiResponse(summary) {
|
|
931
|
+
return !summary.sawAnyContent && summary.numTurns > 0 && (summary.totalUsage.output_tokens ?? 0) === 0 && summary.totalCostUsd > 0;
|
|
932
|
+
}
|
|
933
|
+
function classifyRuntimeError(message) {
|
|
934
|
+
if (/timeout|rate limit|auth|unauthorized|forbidden|model not found|api/i.test(message)) {
|
|
935
|
+
return `API Error: ${message}`;
|
|
936
|
+
}
|
|
937
|
+
return `Agent Error: ${message}`;
|
|
938
|
+
}
|
|
939
|
+
function detectSuspiciousWorkspacePath(toolName, input, cwd) {
|
|
940
|
+
if (!input)
|
|
941
|
+
return;
|
|
942
|
+
if (!["Write", "Read", "Edit", "Glob", "Grep", "LS"].includes(toolName)) {
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
const candidates = [input.file_path, input.path];
|
|
946
|
+
for (const value of candidates) {
|
|
947
|
+
if (typeof value !== "string")
|
|
948
|
+
continue;
|
|
949
|
+
if (!path3.isAbsolute(value))
|
|
950
|
+
continue;
|
|
951
|
+
const relative = path3.relative(cwd, value);
|
|
952
|
+
if (relative.startsWith("..") || path3.isAbsolute(relative)) {
|
|
953
|
+
return value;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
function syntheticErrorAssistant(message) {
|
|
959
|
+
return {
|
|
960
|
+
type: "assistant",
|
|
961
|
+
message: {
|
|
962
|
+
id: NIL_UUID,
|
|
963
|
+
type: "message",
|
|
964
|
+
role: "assistant",
|
|
965
|
+
model: "<synthetic>",
|
|
966
|
+
content: [{ type: "text", text: message }],
|
|
967
|
+
stop_reason: "end_turn"
|
|
968
|
+
}
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
function resultMessage(params) {
|
|
972
|
+
return {
|
|
973
|
+
type: "result",
|
|
974
|
+
subtype: params.isError ? "error" : "success",
|
|
975
|
+
is_error: params.isError,
|
|
976
|
+
duration_ms: params.durationMs,
|
|
977
|
+
duration_api_ms: params.durationApiMs,
|
|
978
|
+
num_turns: params.numTurns,
|
|
979
|
+
result: params.result,
|
|
980
|
+
session_id: params.sessionId,
|
|
981
|
+
total_cost_usd: params.totalCostUsd,
|
|
982
|
+
usage: params.usage
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
async function processAttempt(params) {
|
|
986
|
+
const handle = await spawnOpencodeRun({
|
|
987
|
+
prompt: params.prompt,
|
|
988
|
+
model: params.model,
|
|
989
|
+
cwd: params.cwd,
|
|
990
|
+
nativeSessionId: params.nativeSessionId,
|
|
991
|
+
appendSystemPrompt: params.args.appendSystemPrompt,
|
|
992
|
+
verbose: params.args.verbose,
|
|
993
|
+
debugDir: params.args.debugDir,
|
|
994
|
+
sandbox: params.args.sandbox,
|
|
995
|
+
shimSessionId: params.shimSessionId,
|
|
996
|
+
idleTimeoutMs: params.args.idleTimeout * 1000
|
|
997
|
+
});
|
|
998
|
+
params.onHandle?.(() => handle.kill("SIGTERM"));
|
|
999
|
+
let currentStep;
|
|
1000
|
+
const toolIdMap = new Map;
|
|
1001
|
+
const bufferedMessages = [];
|
|
1002
|
+
let suspiciousWorkspacePath;
|
|
1003
|
+
const summary = {
|
|
1004
|
+
sawAnyContent: false,
|
|
1005
|
+
sawTerminalStop: false,
|
|
1006
|
+
totalCostUsd: 0,
|
|
1007
|
+
totalUsage: emptyUsage(),
|
|
1008
|
+
numTurns: 0,
|
|
1009
|
+
finalResultText: "",
|
|
1010
|
+
lastToolSummary: ""
|
|
1011
|
+
};
|
|
1012
|
+
const ensureCurrentStep = (part) => {
|
|
1013
|
+
if (!currentStep) {
|
|
1014
|
+
currentStep = createStepAccumulator(part?.messageID);
|
|
1015
|
+
}
|
|
1016
|
+
return currentStep;
|
|
1017
|
+
};
|
|
1018
|
+
const flushStep = (reason, usage) => {
|
|
1019
|
+
if (!currentStep)
|
|
1020
|
+
return;
|
|
1021
|
+
const hasAssistantContent = currentStep.contentBlocks.length > 0;
|
|
1022
|
+
if (hasAssistantContent) {
|
|
1023
|
+
const assistantMsg = {
|
|
1024
|
+
type: "assistant",
|
|
1025
|
+
message: {
|
|
1026
|
+
id: currentStep.messageId || generateMessageId(),
|
|
1027
|
+
type: "message",
|
|
1028
|
+
role: "assistant",
|
|
1029
|
+
model: params.model,
|
|
1030
|
+
content: currentStep.contentBlocks,
|
|
1031
|
+
usage,
|
|
1032
|
+
stop_reason: reason === "tool-calls" ? "tool_use" : "end_turn"
|
|
1033
|
+
}
|
|
1034
|
+
};
|
|
1035
|
+
emit(assistantMsg);
|
|
1036
|
+
bufferedMessages.push(assistantMsg);
|
|
1037
|
+
}
|
|
1038
|
+
if (currentStep.toolResults.length > 0) {
|
|
1039
|
+
const userMessage = {
|
|
1040
|
+
type: "user",
|
|
1041
|
+
message: {
|
|
1042
|
+
role: "user",
|
|
1043
|
+
content: currentStep.toolResults
|
|
1044
|
+
}
|
|
1045
|
+
};
|
|
1046
|
+
emit(userMessage);
|
|
1047
|
+
bufferedMessages.push(userMessage);
|
|
1048
|
+
}
|
|
1049
|
+
const textSummary = summarizeAssistantText(currentStep);
|
|
1050
|
+
if (textSummary) {
|
|
1051
|
+
summary.finalResultText = textSummary;
|
|
1052
|
+
}
|
|
1053
|
+
const toolSummary = summarizeLastToolResult(currentStep);
|
|
1054
|
+
if (toolSummary && toolSummary.length >= summary.lastToolSummary.length) {
|
|
1055
|
+
summary.lastToolSummary = toolSummary;
|
|
1056
|
+
}
|
|
1057
|
+
currentStep = undefined;
|
|
1058
|
+
};
|
|
1059
|
+
try {
|
|
1060
|
+
for await (const event of handle.events) {
|
|
1061
|
+
if (typeof event.timestamp === "number") {
|
|
1062
|
+
summary.firstEventTimestamp ??= event.timestamp;
|
|
1063
|
+
summary.lastEventTimestamp = event.timestamp;
|
|
1064
|
+
}
|
|
1065
|
+
if (typeof event.sessionID === "string") {
|
|
1066
|
+
summary.opencodeSessionId = event.sessionID;
|
|
1067
|
+
}
|
|
1068
|
+
switch (event.type) {
|
|
1069
|
+
case "step_start": {
|
|
1070
|
+
const part = asRecord(event.part);
|
|
1071
|
+
currentStep = createStepAccumulator(part?.messageID);
|
|
1072
|
+
break;
|
|
1073
|
+
}
|
|
1074
|
+
case "text": {
|
|
1075
|
+
const part = asRecord(event.part);
|
|
1076
|
+
const step = ensureCurrentStep(part);
|
|
1077
|
+
if (typeof part?.text === "string" && part.text.length > 0) {
|
|
1078
|
+
step.contentBlocks.push({ type: "text", text: part.text });
|
|
1079
|
+
summary.sawAnyContent = true;
|
|
1080
|
+
}
|
|
1081
|
+
break;
|
|
1082
|
+
}
|
|
1083
|
+
case "reasoning": {
|
|
1084
|
+
const part = asRecord(event.part);
|
|
1085
|
+
const step = ensureCurrentStep(part);
|
|
1086
|
+
const thinking = typeof part?.text === "string" ? part.text : typeof part?.reasoning === "string" ? part.reasoning : undefined;
|
|
1087
|
+
if (thinking) {
|
|
1088
|
+
step.contentBlocks.push({ type: "thinking", thinking });
|
|
1089
|
+
summary.sawAnyContent = true;
|
|
1090
|
+
}
|
|
1091
|
+
break;
|
|
1092
|
+
}
|
|
1093
|
+
case "tool_use": {
|
|
1094
|
+
const part = asRecord(event.part);
|
|
1095
|
+
const step = ensureCurrentStep(part);
|
|
1096
|
+
const toolName = normalizeToolName(part?.tool);
|
|
1097
|
+
const nativeToolId = typeof part?.callID === "string" ? part.callID : generateToolUseId();
|
|
1098
|
+
const publicToolId = toolIdMap.get(nativeToolId) ?? generateToolUseId();
|
|
1099
|
+
toolIdMap.set(nativeToolId, publicToolId);
|
|
1100
|
+
const state = asRecord(part?.state) ?? {};
|
|
1101
|
+
const normalizedInput = normalizeToolInput(toolName, state.input);
|
|
1102
|
+
suspiciousWorkspacePath ??= detectSuspiciousWorkspacePath(toolName, normalizedInput, params.cwd);
|
|
1103
|
+
step.contentBlocks.push({
|
|
1104
|
+
type: "tool_use",
|
|
1105
|
+
id: publicToolId,
|
|
1106
|
+
name: toolName,
|
|
1107
|
+
input: normalizedInput
|
|
1108
|
+
});
|
|
1109
|
+
step.toolResults.push({
|
|
1110
|
+
type: "tool_result",
|
|
1111
|
+
tool_use_id: publicToolId,
|
|
1112
|
+
content: extractToolResultContent(toolName, state)
|
|
1113
|
+
});
|
|
1114
|
+
summary.sawAnyContent = true;
|
|
1115
|
+
break;
|
|
1116
|
+
}
|
|
1117
|
+
case "step_finish": {
|
|
1118
|
+
const part = asRecord(event.part);
|
|
1119
|
+
const usage = usageFromStepTokens(asRecord(part?.tokens));
|
|
1120
|
+
addUsage(summary.totalUsage, usage);
|
|
1121
|
+
if (typeof part?.cost === "number") {
|
|
1122
|
+
summary.totalCostUsd += part.cost;
|
|
1123
|
+
}
|
|
1124
|
+
summary.numTurns += 1;
|
|
1125
|
+
const reason = typeof part?.reason === "string" ? part.reason : undefined;
|
|
1126
|
+
if (reason === "stop") {
|
|
1127
|
+
summary.sawTerminalStop = true;
|
|
1128
|
+
}
|
|
1129
|
+
flushStep(reason, usage);
|
|
1130
|
+
break;
|
|
1131
|
+
}
|
|
1132
|
+
case "error": {
|
|
1133
|
+
throw new Error(extractEventError(event));
|
|
1134
|
+
}
|
|
1135
|
+
default: {
|
|
1136
|
+
break;
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
} catch (error) {
|
|
1141
|
+
handle.kill("SIGTERM");
|
|
1142
|
+
try {
|
|
1143
|
+
await handle.waitForExit();
|
|
1144
|
+
} catch {}
|
|
1145
|
+
throw error;
|
|
1146
|
+
}
|
|
1147
|
+
const exitStatus = await handle.waitForExit();
|
|
1148
|
+
return {
|
|
1149
|
+
summary,
|
|
1150
|
+
messages: bufferedMessages,
|
|
1151
|
+
exitCode: exitStatus.code,
|
|
1152
|
+
signal: exitStatus.signal,
|
|
1153
|
+
stderr: filterDiagnosticStderr(handle.getStderr()),
|
|
1154
|
+
suspiciousWorkspacePath
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
function buildPrompt(prompt, retryNote) {
|
|
1158
|
+
const normalized = prompt.endsWith(`
|
|
1159
|
+
`) ? prompt : `${prompt}
|
|
1160
|
+
`;
|
|
1161
|
+
if (!retryNote) {
|
|
1162
|
+
return normalized;
|
|
1163
|
+
}
|
|
1164
|
+
return `${normalized}
|
|
1165
|
+
[Internal retry note: ${retryNote}. Do not mention or repeat this note.]
|
|
1166
|
+
`;
|
|
1167
|
+
}
|
|
1168
|
+
function sleep(ms) {
|
|
1169
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1170
|
+
}
|
|
1171
|
+
async function runShim({ prompt, args: args2 }) {
|
|
1172
|
+
const cwd = process.cwd();
|
|
1173
|
+
const model = resolveModel(args2.model || process.env.MODEL || "sonnet");
|
|
1174
|
+
const sessionManager = args2.debugDir ? new SessionManager({ debugDir: args2.debugDir }) : undefined;
|
|
1175
|
+
await ensureOpencodeAvailable();
|
|
1176
|
+
let shimSessionId = args2.resume ? args2.resume : generateSessionId();
|
|
1177
|
+
let nativeSessionId;
|
|
1178
|
+
if (args2.resume) {
|
|
1179
|
+
if (!isUuidLike(args2.resume)) {
|
|
1180
|
+
const message = `Invalid session ID: ${args2.resume}`;
|
|
1181
|
+
appendDebugLog(args2.debugDir, "unknown", message);
|
|
1182
|
+
process.stderr.write(`${message}
|
|
1183
|
+
`);
|
|
1184
|
+
return 1;
|
|
1185
|
+
}
|
|
1186
|
+
try {
|
|
1187
|
+
if (sessionManager) {
|
|
1188
|
+
try {
|
|
1189
|
+
const saved = sessionManager.loadSession(args2.resume);
|
|
1190
|
+
nativeSessionId = saved.agentSessionId;
|
|
1191
|
+
shimSessionId = saved.sessionId;
|
|
1192
|
+
} catch {
|
|
1193
|
+
nativeSessionId = await findSessionIdByTitle(args2.resume);
|
|
1194
|
+
if (!nativeSessionId) {
|
|
1195
|
+
throw new Error(`Session not found: ${args2.resume}`);
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
} else {
|
|
1199
|
+
nativeSessionId = await findSessionIdByTitle(args2.resume);
|
|
1200
|
+
if (!nativeSessionId) {
|
|
1201
|
+
throw new Error(`Session not found: ${args2.resume}`);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
} catch (error) {
|
|
1205
|
+
const message = errorMessage(error);
|
|
1206
|
+
appendDebugLog(args2.debugDir, "unknown", message);
|
|
1207
|
+
process.stderr.write(`${message}
|
|
1208
|
+
`);
|
|
1209
|
+
return 1;
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
const system = {
|
|
1213
|
+
type: "system",
|
|
1214
|
+
subtype: "init",
|
|
1215
|
+
cwd: path3.resolve(cwd),
|
|
1216
|
+
session_id: shimSessionId,
|
|
1217
|
+
tools: [...STANDARD_TOOLS],
|
|
1218
|
+
model,
|
|
1219
|
+
permissionMode: "bypassPermissions",
|
|
1220
|
+
apiKeySource: detectApiKeySource(model),
|
|
1221
|
+
mcp_servers: []
|
|
1222
|
+
};
|
|
1223
|
+
touchDebugLog(args2.debugDir, shimSessionId);
|
|
1224
|
+
appendDebugJson(args2.debugDir, shimSessionId, {
|
|
1225
|
+
type: "init",
|
|
1226
|
+
session_id: shimSessionId,
|
|
1227
|
+
cwd: system.cwd,
|
|
1228
|
+
model
|
|
1229
|
+
});
|
|
1230
|
+
emit(system);
|
|
1231
|
+
const startTime = Date.now();
|
|
1232
|
+
const interruptedRef = { value: false };
|
|
1233
|
+
let activeKill;
|
|
1234
|
+
const onSignal = () => {
|
|
1235
|
+
if (interruptedRef.value)
|
|
1236
|
+
return;
|
|
1237
|
+
interruptedRef.value = true;
|
|
1238
|
+
activeKill?.();
|
|
1239
|
+
};
|
|
1240
|
+
process.on("SIGINT", onSignal);
|
|
1241
|
+
process.on("SIGTERM", onSignal);
|
|
1242
|
+
const grandUsage = emptyUsage();
|
|
1243
|
+
let grandCostUsd = 0;
|
|
1244
|
+
let finalSummary;
|
|
1245
|
+
let finalStderr = "";
|
|
1246
|
+
let retryNote;
|
|
1247
|
+
try {
|
|
1248
|
+
for (let attempt = 0;attempt <= MAX_EMPTY_RETRIES; attempt++) {
|
|
1249
|
+
verboseLog(args2.verbose, "Starting attempt", { attempt: attempt + 1, model, resumed: Boolean(args2.resume) });
|
|
1250
|
+
if (args2.verbose) {
|
|
1251
|
+
appendDebugLog(args2.debugDir, shimSessionId, `[opencode-shim] Starting attempt ${attempt + 1} model=${model} resumed=${Boolean(args2.resume)}`);
|
|
1252
|
+
}
|
|
1253
|
+
const attemptPromise = processAttempt({
|
|
1254
|
+
prompt: buildPrompt(prompt, retryNote),
|
|
1255
|
+
args: args2,
|
|
1256
|
+
model,
|
|
1257
|
+
cwd,
|
|
1258
|
+
shimSessionId,
|
|
1259
|
+
nativeSessionId,
|
|
1260
|
+
interruptedRef,
|
|
1261
|
+
onHandle(kill) {
|
|
1262
|
+
activeKill = kill;
|
|
1263
|
+
if (interruptedRef.value) {
|
|
1264
|
+
kill();
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
});
|
|
1268
|
+
const outcome = await attemptPromise;
|
|
1269
|
+
activeKill = undefined;
|
|
1270
|
+
finalStderr = outcome.stderr;
|
|
1271
|
+
addUsage(grandUsage, outcome.summary.totalUsage);
|
|
1272
|
+
grandCostUsd += outcome.summary.totalCostUsd;
|
|
1273
|
+
if (outcome.summary.opencodeSessionId) {
|
|
1274
|
+
nativeSessionId = outcome.summary.opencodeSessionId;
|
|
1275
|
+
}
|
|
1276
|
+
const retryableEmpty = !args2.resume && !interruptedRef.value && supportsGeminiEmptyRetry(model) && isRetryableEmptyGeminiResponse(outcome.summary);
|
|
1277
|
+
const retryableStaleWorkspace = !args2.resume && !interruptedRef.value && supportsGeminiEmptyRetry(model) && Boolean(outcome.suspiciousWorkspacePath);
|
|
1278
|
+
if (retryableEmpty || retryableStaleWorkspace) {
|
|
1279
|
+
retryNote = retryableEmpty ? `Retry token ${Date.now()}-${attempt}. A previous hidden attempt returned an empty response with zero output tokens. Produce a fresh non-empty answer and do not reuse any cached empty response` : `Retry token ${Date.now()}-${attempt}. A previous hidden attempt referenced the stale path ${outcome.suspiciousWorkspacePath}. The current working directory is ${cwd}. Redo the task from scratch and only use files inside ${cwd}`;
|
|
1280
|
+
}
|
|
1281
|
+
if ((retryableEmpty || retryableStaleWorkspace) && attempt < MAX_EMPTY_RETRIES) {
|
|
1282
|
+
const backoffMs = Math.min(1000 * 2 ** attempt, 8000);
|
|
1283
|
+
verboseLog(args2.verbose, retryableEmpty ? "Retrying empty Gemini response" : "Retrying suspicious workspace response", {
|
|
1284
|
+
attempt: attempt + 1,
|
|
1285
|
+
backoffMs,
|
|
1286
|
+
suspiciousWorkspacePath: outcome.suspiciousWorkspacePath
|
|
1287
|
+
});
|
|
1288
|
+
if (args2.verbose) {
|
|
1289
|
+
appendDebugLog(args2.debugDir, shimSessionId, `[opencode-shim] retry reason=${retryableEmpty ? "empty-response" : "suspicious-workspace"} attempt=${attempt + 1} backoff_ms=${backoffMs}`);
|
|
1290
|
+
}
|
|
1291
|
+
if (outcome.summary.opencodeSessionId) {
|
|
1292
|
+
try {
|
|
1293
|
+
await deleteSessionById(outcome.summary.opencodeSessionId);
|
|
1294
|
+
} catch (cleanupError) {
|
|
1295
|
+
verboseLog(args2.verbose, "Failed to delete discarded OpenCode retry session", {
|
|
1296
|
+
sessionId: outcome.summary.opencodeSessionId,
|
|
1297
|
+
error: errorMessage(cleanupError)
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
nativeSessionId = undefined;
|
|
1302
|
+
await sleep(backoffMs);
|
|
1303
|
+
continue;
|
|
1304
|
+
}
|
|
1305
|
+
if (retryableEmpty || retryableStaleWorkspace) {
|
|
1306
|
+
throw new Error(retryableEmpty ? "Provider returned repeated empty responses after retries" : `Provider returned stale workspace tool paths after retries: ${outcome.suspiciousWorkspacePath}`);
|
|
1307
|
+
}
|
|
1308
|
+
finalSummary = outcome.summary;
|
|
1309
|
+
if (interruptedRef.value) {
|
|
1310
|
+
break;
|
|
1311
|
+
}
|
|
1312
|
+
if (outcome.exitCode !== 0 && outcome.exitCode !== null) {
|
|
1313
|
+
throw new Error(outcome.stderr || `OpenCode exited with code ${outcome.exitCode}`);
|
|
1314
|
+
}
|
|
1315
|
+
if (outcome.signal && !interruptedRef.value) {
|
|
1316
|
+
throw new Error(`OpenCode exited due to signal ${outcome.signal}`);
|
|
1317
|
+
}
|
|
1318
|
+
break;
|
|
1319
|
+
}
|
|
1320
|
+
if (!finalSummary) {
|
|
1321
|
+
throw new Error("OpenCode produced no final summary.");
|
|
1322
|
+
}
|
|
1323
|
+
if (!interruptedRef.value && !finalSummary.sawTerminalStop) {
|
|
1324
|
+
throw new Error('OpenCode exited without a terminal step_finish reason "stop".');
|
|
1325
|
+
}
|
|
1326
|
+
if (sessionManager && nativeSessionId && shimSessionId !== NIL_UUID) {
|
|
1327
|
+
sessionManager.saveSession({
|
|
1328
|
+
sessionId: shimSessionId,
|
|
1329
|
+
agentSessionId: nativeSessionId,
|
|
1330
|
+
timestamp: new Date().toISOString(),
|
|
1331
|
+
metadata: { cwd, model }
|
|
1332
|
+
});
|
|
1333
|
+
}
|
|
1334
|
+
const durationMs = Date.now() - startTime;
|
|
1335
|
+
const durationApiMs = durationMs;
|
|
1336
|
+
if (!finalSummary.finalResultText && finalSummary.lastToolSummary) {
|
|
1337
|
+
emit({
|
|
1338
|
+
type: "assistant",
|
|
1339
|
+
message: {
|
|
1340
|
+
id: generateMessageId(),
|
|
1341
|
+
type: "message",
|
|
1342
|
+
role: "assistant",
|
|
1343
|
+
model: "<synthetic>",
|
|
1344
|
+
content: [{ type: "text", text: humanizeToolSummary(finalSummary.lastToolSummary) }],
|
|
1345
|
+
stop_reason: "end_turn"
|
|
1346
|
+
}
|
|
1347
|
+
});
|
|
1348
|
+
}
|
|
1349
|
+
const resultText = interruptedRef.value ? finalSummary.finalResultText || humanizeToolSummary(finalSummary.lastToolSummary) || "Interrupted" : finalSummary.finalResultText || humanizeToolSummary(finalSummary.lastToolSummary) || "Completed successfully.";
|
|
1350
|
+
const finalResultMessage = resultMessage({
|
|
1351
|
+
isError: false,
|
|
1352
|
+
durationMs,
|
|
1353
|
+
durationApiMs,
|
|
1354
|
+
numTurns: finalSummary.numTurns,
|
|
1355
|
+
result: resultText,
|
|
1356
|
+
sessionId: shimSessionId,
|
|
1357
|
+
usage: grandUsage,
|
|
1358
|
+
totalCostUsd: grandCostUsd
|
|
1359
|
+
});
|
|
1360
|
+
emit(finalResultMessage);
|
|
1361
|
+
appendDebugJson(args2.debugDir, shimSessionId, {
|
|
1362
|
+
type: "result",
|
|
1363
|
+
status: "success",
|
|
1364
|
+
exit_code: 0,
|
|
1365
|
+
duration_ms: durationMs
|
|
1366
|
+
});
|
|
1367
|
+
await flushStdout();
|
|
1368
|
+
return 0;
|
|
1369
|
+
} catch (error) {
|
|
1370
|
+
const durationMs = Date.now() - startTime;
|
|
1371
|
+
const durationApiMs = durationMs;
|
|
1372
|
+
const rawMessage = error instanceof IdleTimeoutError || error instanceof BusyStepTimeoutError ? error.message : errorMessage(error) || finalStderr || "Unknown runtime failure";
|
|
1373
|
+
const classified = classifyRuntimeError(rawMessage);
|
|
1374
|
+
emit(syntheticErrorAssistant(classified));
|
|
1375
|
+
const finalResultMessage = resultMessage({
|
|
1376
|
+
isError: true,
|
|
1377
|
+
durationMs,
|
|
1378
|
+
durationApiMs,
|
|
1379
|
+
numTurns: finalSummary?.numTurns ?? 0,
|
|
1380
|
+
result: classified,
|
|
1381
|
+
sessionId: shimSessionId,
|
|
1382
|
+
usage: grandUsage,
|
|
1383
|
+
totalCostUsd: grandCostUsd
|
|
1384
|
+
});
|
|
1385
|
+
emit(finalResultMessage);
|
|
1386
|
+
appendDebugJson(args2.debugDir, shimSessionId, {
|
|
1387
|
+
type: "result",
|
|
1388
|
+
status: "error",
|
|
1389
|
+
exit_code: 1,
|
|
1390
|
+
duration_ms: durationMs,
|
|
1391
|
+
error: classified
|
|
1392
|
+
});
|
|
1393
|
+
appendDebugLog(args2.debugDir, shimSessionId, classified);
|
|
1394
|
+
await flushStdout();
|
|
1395
|
+
return 1;
|
|
1396
|
+
} finally {
|
|
1397
|
+
process.off("SIGINT", onSignal);
|
|
1398
|
+
process.off("SIGTERM", onSignal);
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
// src/index.ts
|
|
1403
|
+
function printHelp() {
|
|
1404
|
+
process.stdout.write(`opencode-shim
|
|
1405
|
+
|
|
1406
|
+
Usage:
|
|
1407
|
+
echo "prompt" | opencode-shim --model google/gemini-2.5-flash
|
|
1408
|
+
|
|
1409
|
+
Options:
|
|
1410
|
+
--model <model> Model name (required in normal mode)
|
|
1411
|
+
--resume <session_id> Resume a prior shim session
|
|
1412
|
+
--verbose Verbose logs to stderr
|
|
1413
|
+
--append-system-prompt <text> Extra instruction to inject via OpenCode instructions
|
|
1414
|
+
--idle-timeout <seconds> Baseline idle timeout (default: 120)
|
|
1415
|
+
--debug-dir <path> Debug/session directory
|
|
1416
|
+
--sandbox <level> none | standard | strict
|
|
1417
|
+
--self-test Run environment checks
|
|
1418
|
+
--version Print version
|
|
1419
|
+
--help Show help
|
|
1420
|
+
`);
|
|
1421
|
+
}
|
|
1422
|
+
async function readStdin() {
|
|
1423
|
+
const chunks = [];
|
|
1424
|
+
for await (const chunk of process.stdin) {
|
|
1425
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
1426
|
+
}
|
|
1427
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
1428
|
+
}
|
|
1429
|
+
function appendUnknownDebugLog(debugDir, message) {
|
|
1430
|
+
if (!debugDir)
|
|
1431
|
+
return;
|
|
1432
|
+
fs4.mkdirSync(debugDir, { recursive: true });
|
|
1433
|
+
fs4.appendFileSync(path4.join(debugDir, "session-unknown.raw.log"), `${message}
|
|
1434
|
+
`, "utf8");
|
|
1435
|
+
}
|
|
1436
|
+
async function main() {
|
|
1437
|
+
const args2 = parseArgs(process.argv.slice(2), {
|
|
1438
|
+
"-m": "--model",
|
|
1439
|
+
"-h": "--help",
|
|
1440
|
+
"-v": "--version"
|
|
1441
|
+
});
|
|
1442
|
+
if (args2.help) {
|
|
1443
|
+
printHelp();
|
|
1444
|
+
return 0;
|
|
1445
|
+
}
|
|
1446
|
+
if (args2.version) {
|
|
1447
|
+
process.stdout.write(`${package_default.version}
|
|
1448
|
+
`);
|
|
1449
|
+
return 0;
|
|
1450
|
+
}
|
|
1451
|
+
if (args2.selfTest) {
|
|
1452
|
+
return await runSelfTest(package_default.version);
|
|
1453
|
+
}
|
|
1454
|
+
const prompt = (await readStdin()).trim();
|
|
1455
|
+
if (!prompt) {
|
|
1456
|
+
return 0;
|
|
1457
|
+
}
|
|
1458
|
+
if (!args2.model && !process.env.MODEL) {
|
|
1459
|
+
const message = "Missing required --model";
|
|
1460
|
+
appendUnknownDebugLog(args2.debugDir, message);
|
|
1461
|
+
process.stderr.write(`${message}
|
|
1462
|
+
`);
|
|
1463
|
+
return 1;
|
|
1464
|
+
}
|
|
1465
|
+
try {
|
|
1466
|
+
return await runShim({ prompt, args: args2 });
|
|
1467
|
+
} catch (error) {
|
|
1468
|
+
const message = errorMessage(error);
|
|
1469
|
+
appendUnknownDebugLog(args2.debugDir, message);
|
|
1470
|
+
process.stderr.write(`${message}
|
|
1471
|
+
`);
|
|
1472
|
+
return 1;
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
var exitCode = await main();
|
|
1476
|
+
process.exitCode = exitCode;
|