@mono-agent/agent-runtime 0.6.2 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -16
- package/package.json +14 -7
- package/src/agent/approval.js +52 -17
- package/src/agent/sandbox-seam.js +1 -0
- package/src/agent/tools/pi-bridge.js +2 -0
- package/src/agent/tools/shared/ripgrep.js +12 -8
- package/src/ai/index.js +8 -0
- package/src/ai/providers/claude-cli.js +109 -5
- package/src/ai/providers/claude-sandbox.js +71 -0
- package/src/ai/providers/claude-sdk-discovery-worker.js +53 -0
- package/src/ai/providers/claude-sdk-discovery.js +352 -0
- package/src/ai/providers/claude-sdk.js +313 -35
- package/src/ai/providers/codex-app.js +823 -78
- package/src/ai/providers/opencode-app.js +682 -96
- package/src/ai/providers/opencode-server.js +508 -0
- package/src/ai/runtime/capabilities.js +12 -0
- package/src/ai/runtime/context-windows.js +8 -0
- package/src/ai/runtime/registry.js +8 -2
- package/src/ai/runtime/router.js +627 -29
- package/src/ai/types.js +29 -2
- package/src/index.js +6 -0
- package/src/runtime.js +17 -1
- package/types/agent/approval.d.ts +4 -7
- package/types/agent/sandbox-seam.d.ts +5 -0
- package/types/ai/backend.d.ts +16 -0
- package/types/ai/index.d.ts +1 -0
- package/types/ai/providers/claude-cli.d.ts +116 -0
- package/types/ai/providers/claude-sandbox.d.ts +79 -0
- package/types/ai/providers/claude-sdk-discovery-worker.d.ts +1 -0
- package/types/ai/providers/claude-sdk-discovery.d.ts +97 -0
- package/types/ai/providers/claude-sdk.d.ts +81 -5
- package/types/ai/providers/codex-app.d.ts +11 -7
- package/types/ai/providers/opencode-app.d.ts +15 -16
- package/types/ai/providers/opencode-server.d.ts +20 -0
- package/types/ai/runtime/capabilities.d.ts +19 -0
- package/types/ai/runtime/context-windows.d.ts +1 -0
- package/types/ai/runtime/router.d.ts +24 -23
- package/types/ai/types.d.ts +75 -2
- package/types/index.d.ts +1 -0
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
import launch from "cross-spawn";
|
|
2
|
+
import { chmod, lstat, mkdir, mkdtemp, open, rm } from "node:fs/promises";
|
|
3
|
+
import { rmSync } from "node:fs";
|
|
4
|
+
import { randomBytes } from "node:crypto";
|
|
5
|
+
import { homedir, tmpdir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { createOpencodeClient } from "@opencode-ai/sdk/v2";
|
|
8
|
+
|
|
9
|
+
const DEFAULT_START_TIMEOUT_MS = 5_000;
|
|
10
|
+
const DEFAULT_STOP_TIMEOUT_MS = 1_000;
|
|
11
|
+
const VERSION_CHECK_TIMEOUT_MS = 3_000;
|
|
12
|
+
const MAX_START_OUTPUT_CHARS = 16_384;
|
|
13
|
+
const MAX_VERSION_OUTPUT_CHARS = 128;
|
|
14
|
+
const PRIVATE_STATE_PREFIX = "mono-agent-opencode-";
|
|
15
|
+
const MINIMUM_OPENCODE_VERSION = [1, 15, 0];
|
|
16
|
+
|
|
17
|
+
let versionCheckPromise;
|
|
18
|
+
let exitCleanupRegistered = false;
|
|
19
|
+
const privateStateDirectories = new Set();
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Start one OpenCode server with mono-agent-owned process state.
|
|
23
|
+
*
|
|
24
|
+
* OpenCode stores sessions and project-wide permission approvals in SQLite. A
|
|
25
|
+
* unique per-run DB prevents pre-existing user approvals and other conversations
|
|
26
|
+
* from entering this tool environment. The normal OpenCode data home remains
|
|
27
|
+
* mounted so auth refresh rotation persists; global/repo config, external
|
|
28
|
+
* plugins, and external skills are disabled below. State is deleted after close.
|
|
29
|
+
*/
|
|
30
|
+
export async function createIsolatedOpencode(options = {}) {
|
|
31
|
+
if (process.env.OPENCODE_AUTH_CONTENT !== undefined) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
"Direct OpenCode cannot safely inherit OPENCODE_AUTH_CONTENT. Persist credentials with `opencode auth login`, unset OPENCODE_AUTH_CONTENT, and retry.",
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
const hostname = options.hostname ?? "127.0.0.1";
|
|
37
|
+
const port = options.port ?? 0;
|
|
38
|
+
if (hostname !== "127.0.0.1" || port !== 0) {
|
|
39
|
+
throw new Error("The direct OpenCode server must bind to 127.0.0.1 on an ephemeral port.");
|
|
40
|
+
}
|
|
41
|
+
const timeoutMs = positiveTimeout(options.timeout);
|
|
42
|
+
await verifyOpenCodeVersion();
|
|
43
|
+
if (options.signal?.aborted) {
|
|
44
|
+
throw abortReason(options.signal);
|
|
45
|
+
}
|
|
46
|
+
const state = await createPrivateOpenCodeState();
|
|
47
|
+
const args = [
|
|
48
|
+
"serve",
|
|
49
|
+
"--pure",
|
|
50
|
+
`--hostname=${hostname}`,
|
|
51
|
+
`--port=${port}`,
|
|
52
|
+
];
|
|
53
|
+
if (options.config?.logLevel) args.push(`--log-level=${options.config.logLevel}`);
|
|
54
|
+
|
|
55
|
+
const serverUsername = "mono-agent";
|
|
56
|
+
const serverPassword = randomBytes(32).toString("base64url");
|
|
57
|
+
const serverConfig = {
|
|
58
|
+
...(options.config ?? {}),
|
|
59
|
+
share: "disabled",
|
|
60
|
+
autoshare: false,
|
|
61
|
+
};
|
|
62
|
+
// Provider-owned Bash inherits the server environment. Start from a narrow,
|
|
63
|
+
// non-secret process allowlist; provider credentials come only from auth.json.
|
|
64
|
+
const childEnv = safeProcessEnv();
|
|
65
|
+
Object.assign(childEnv, {
|
|
66
|
+
HOME: state.privateHome,
|
|
67
|
+
USERPROFILE: state.privateHome,
|
|
68
|
+
OPENCODE_TEST_HOME: state.privateHome,
|
|
69
|
+
XDG_CONFIG_HOME: state.configHome,
|
|
70
|
+
XDG_CONFIG_DIRS: state.configHome,
|
|
71
|
+
XDG_DATA_HOME: state.userDataHome,
|
|
72
|
+
XDG_STATE_HOME: state.stateHome,
|
|
73
|
+
OPENCODE_DB: state.databasePath,
|
|
74
|
+
OPENCODE_CONFIG_CONTENT: JSON.stringify(serverConfig),
|
|
75
|
+
OPENCODE_DISABLE_PROJECT_CONFIG: "true",
|
|
76
|
+
OPENCODE_DISABLE_AUTOUPDATE: "true",
|
|
77
|
+
OPENCODE_DISABLE_SHARE: "true",
|
|
78
|
+
OPENCODE_AUTO_SHARE: "false",
|
|
79
|
+
OPENCODE_DISABLE_EXTERNAL_SKILLS: "true",
|
|
80
|
+
OPENCODE_DISABLE_CLAUDE_CODE_SKILLS: "true",
|
|
81
|
+
OPENCODE_DISABLE_CLAUDE_CODE_PROMPT: "true",
|
|
82
|
+
OPENCODE_DISABLE_LSP_DOWNLOAD: "true",
|
|
83
|
+
OPENCODE_EXPERIMENTAL: "false",
|
|
84
|
+
OPENCODE_EXPERIMENTAL_WORKSPACES: "false",
|
|
85
|
+
OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS: "false",
|
|
86
|
+
OPENCODE_EXPERIMENTAL_PARALLEL: "false",
|
|
87
|
+
OPENCODE_EXPERIMENTAL_SCOUT: "false",
|
|
88
|
+
OPENCODE_ENABLE_PARALLEL: "false",
|
|
89
|
+
OPENCODE_ENABLE_QUESTION_TOOL: "false",
|
|
90
|
+
OPENCODE_SERVER_USERNAME: serverUsername,
|
|
91
|
+
OPENCODE_SERVER_PASSWORD: serverPassword,
|
|
92
|
+
});
|
|
93
|
+
let child;
|
|
94
|
+
try {
|
|
95
|
+
child = launch("opencode", args, {
|
|
96
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
97
|
+
env: childEnv,
|
|
98
|
+
});
|
|
99
|
+
} catch (error) {
|
|
100
|
+
await disposePrivateOpenCodeState(state);
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let closed = false;
|
|
105
|
+
let ready = false;
|
|
106
|
+
let startOutput = "";
|
|
107
|
+
let stdoutBuffer = "";
|
|
108
|
+
let timer;
|
|
109
|
+
let rejectStartup;
|
|
110
|
+
let closePromise;
|
|
111
|
+
let disposePromise;
|
|
112
|
+
|
|
113
|
+
const stop = () => {
|
|
114
|
+
if (closePromise !== undefined) return closePromise;
|
|
115
|
+
closed = true;
|
|
116
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
117
|
+
options.signal?.removeEventListener?.("abort", onAbort);
|
|
118
|
+
child.stdout?.off?.("data", onStdout);
|
|
119
|
+
child.stderr?.off?.("data", onStderr);
|
|
120
|
+
child.off?.("error", onError);
|
|
121
|
+
child.off?.("exit", onExit);
|
|
122
|
+
closePromise = new Promise((resolve, reject) => {
|
|
123
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
124
|
+
resolve();
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
let settled = false;
|
|
128
|
+
let ultimateTimer;
|
|
129
|
+
const finish = (error) => {
|
|
130
|
+
if (settled) return;
|
|
131
|
+
settled = true;
|
|
132
|
+
clearTimeout(forceTimer);
|
|
133
|
+
if (ultimateTimer !== undefined) clearTimeout(ultimateTimer);
|
|
134
|
+
child.off?.("exit", onStopped);
|
|
135
|
+
child.off?.("error", onStopError);
|
|
136
|
+
if (error === undefined) resolve();
|
|
137
|
+
else reject(error);
|
|
138
|
+
};
|
|
139
|
+
const onStopped = () => finish();
|
|
140
|
+
const onStopError = () => finish(new Error("OpenCode server process failed while stopping."));
|
|
141
|
+
const forceTimer = setTimeout(() => {
|
|
142
|
+
try {
|
|
143
|
+
child.kill("SIGKILL");
|
|
144
|
+
// Wait for the actual exit before deleting the DB. Retain one final
|
|
145
|
+
// bound for pathological/mocked processes that never report exit.
|
|
146
|
+
ultimateTimer = setTimeout(() => {
|
|
147
|
+
finish(new Error("Timed out waiting for the OpenCode server to exit after SIGKILL."));
|
|
148
|
+
}, DEFAULT_STOP_TIMEOUT_MS);
|
|
149
|
+
ultimateTimer.unref?.();
|
|
150
|
+
} catch {
|
|
151
|
+
finish(new Error("Unable to terminate the OpenCode server process."));
|
|
152
|
+
}
|
|
153
|
+
}, DEFAULT_STOP_TIMEOUT_MS);
|
|
154
|
+
forceTimer.unref?.();
|
|
155
|
+
child.once?.("exit", onStopped);
|
|
156
|
+
child.once?.("error", onStopError);
|
|
157
|
+
try {
|
|
158
|
+
child.kill();
|
|
159
|
+
} catch {
|
|
160
|
+
finish(new Error("Unable to terminate the OpenCode server process."));
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
return closePromise;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const close = () => {
|
|
167
|
+
if (disposePromise === undefined) {
|
|
168
|
+
// Only delete run state after process exit is confirmed. If termination
|
|
169
|
+
// cannot be confirmed, retain the directory in the exit-cleanup set.
|
|
170
|
+
disposePromise = stop().then(() => disposePrivateOpenCodeState(state));
|
|
171
|
+
}
|
|
172
|
+
return disposePromise;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const failStartup = (error) => {
|
|
176
|
+
if (ready || closed) return;
|
|
177
|
+
// Captured output is intentionally not attached: the child can load provider
|
|
178
|
+
// config/auth and startup errors must never echo either into host diagnostics.
|
|
179
|
+
void close().then(
|
|
180
|
+
() => rejectStartup(error),
|
|
181
|
+
() => rejectStartup(error),
|
|
182
|
+
);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const onAbort = () => {
|
|
186
|
+
if (ready) {
|
|
187
|
+
void close().catch(() => undefined);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
failStartup(abortReason(options.signal));
|
|
191
|
+
};
|
|
192
|
+
const onError = (error) => failStartup(error instanceof Error ? error : new Error(String(error)));
|
|
193
|
+
const onExit = (code, signal) => {
|
|
194
|
+
if (ready || closed) return;
|
|
195
|
+
failStartup(new Error(`OpenCode server exited before startup (code=${code ?? "none"}, signal=${signal ?? "none"}).`));
|
|
196
|
+
};
|
|
197
|
+
const onStderr = (chunk) => {
|
|
198
|
+
startOutput = appendBounded(startOutput, chunk);
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
let resolveStartup;
|
|
202
|
+
const started = new Promise((resolve, reject) => {
|
|
203
|
+
resolveStartup = resolve;
|
|
204
|
+
rejectStartup = reject;
|
|
205
|
+
});
|
|
206
|
+
const onStdout = (chunk) => {
|
|
207
|
+
const text = String(chunk);
|
|
208
|
+
startOutput = appendBounded(startOutput, text);
|
|
209
|
+
stdoutBuffer = appendBounded(stdoutBuffer, text);
|
|
210
|
+
const lines = stdoutBuffer.split(/\r?\n/u);
|
|
211
|
+
stdoutBuffer = lines.pop() ?? "";
|
|
212
|
+
for (const line of lines) {
|
|
213
|
+
if (!line.startsWith("opencode server listening")) continue;
|
|
214
|
+
const match = line.match(/on\s+(https?:\/\/[^\s]+)/u);
|
|
215
|
+
const safeUrl = match?.[1] === undefined ? undefined : safeLoopbackServerUrl(match[1]);
|
|
216
|
+
if (safeUrl === undefined) {
|
|
217
|
+
failStartup(new Error("OpenCode server reported an invalid listening URL."));
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
ready = true;
|
|
221
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
222
|
+
child.stdout?.off?.("data", onStdout);
|
|
223
|
+
child.stderr?.off?.("data", onStderr);
|
|
224
|
+
child.off?.("error", onError);
|
|
225
|
+
child.off?.("exit", onExit);
|
|
226
|
+
child.stdout?.resume?.();
|
|
227
|
+
child.stderr?.resume?.();
|
|
228
|
+
resolveStartup(safeUrl);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
child.stdout?.on?.("data", onStdout);
|
|
234
|
+
child.stderr?.on?.("data", onStderr);
|
|
235
|
+
child.on?.("error", onError);
|
|
236
|
+
child.on?.("exit", onExit);
|
|
237
|
+
options.signal?.addEventListener?.("abort", onAbort, { once: true });
|
|
238
|
+
timer = setTimeout(() => {
|
|
239
|
+
failStartup(new Error(`Timeout waiting for OpenCode server after ${timeoutMs}ms.`));
|
|
240
|
+
}, timeoutMs);
|
|
241
|
+
timer.unref?.();
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
const url = await started;
|
|
245
|
+
const authorization = Buffer.from(`${serverUsername}:${serverPassword}`, "utf8").toString("base64");
|
|
246
|
+
return {
|
|
247
|
+
client: createOpencodeClient({
|
|
248
|
+
baseUrl: url,
|
|
249
|
+
headers: { Authorization: `Basic ${authorization}` },
|
|
250
|
+
}),
|
|
251
|
+
server: { url, close },
|
|
252
|
+
};
|
|
253
|
+
} catch (error) {
|
|
254
|
+
await close().catch(() => undefined);
|
|
255
|
+
throw error;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function createPrivateOpenCodeState() {
|
|
260
|
+
const userDataHome = await assertUserMigrationMarker();
|
|
261
|
+
const directory = await mkdtemp(join(tmpdir(), PRIVATE_STATE_PREFIX));
|
|
262
|
+
try {
|
|
263
|
+
await chmod(directory, 0o700);
|
|
264
|
+
const privateHome = await privateDirectory(directory, "home");
|
|
265
|
+
const configHome = await privateDirectory(directory, "config");
|
|
266
|
+
const opencodeConfig = await privateDirectory(configHome, "opencode");
|
|
267
|
+
if (process.platform !== "win32") await chmod(opencodeConfig, 0o500);
|
|
268
|
+
const stateHome = await privateDirectory(directory, "state");
|
|
269
|
+
const databasePath = join(directory, "opencode.db");
|
|
270
|
+
await createPrivateFile(databasePath);
|
|
271
|
+
privateStateDirectories.add(directory);
|
|
272
|
+
registerExitCleanup();
|
|
273
|
+
return {
|
|
274
|
+
directory,
|
|
275
|
+
privateHome,
|
|
276
|
+
configHome,
|
|
277
|
+
stateHome,
|
|
278
|
+
userDataHome,
|
|
279
|
+
databasePath,
|
|
280
|
+
};
|
|
281
|
+
} catch (error) {
|
|
282
|
+
await rm(directory, { recursive: true, force: true }).catch(() => undefined);
|
|
283
|
+
throw error;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async function disposePrivateOpenCodeState(state) {
|
|
288
|
+
await rm(state.directory, { recursive: true, force: true });
|
|
289
|
+
privateStateDirectories.delete(state.directory);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function privateDirectory(parent, name) {
|
|
293
|
+
const path = join(parent, name);
|
|
294
|
+
await mkdir(path, { mode: 0o700 });
|
|
295
|
+
await chmod(path, 0o700);
|
|
296
|
+
return path;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function createPrivateFile(path, content) {
|
|
300
|
+
const handle = await open(path, "wx", 0o600);
|
|
301
|
+
try {
|
|
302
|
+
if (content !== undefined) await handle.writeFile(content);
|
|
303
|
+
} finally {
|
|
304
|
+
await handle.close();
|
|
305
|
+
}
|
|
306
|
+
await chmod(path, 0o600);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async function assertUserMigrationMarker() {
|
|
310
|
+
const userDataHome = process.env.XDG_DATA_HOME
|
|
311
|
+
|| join(homedir(), ".local", "share");
|
|
312
|
+
const marker = join(userDataHome, "opencode", "opencode.db");
|
|
313
|
+
let info;
|
|
314
|
+
try {
|
|
315
|
+
info = await lstat(marker);
|
|
316
|
+
} catch (error) {
|
|
317
|
+
if (error?.code !== "ENOENT" && error?.code !== "ENOTDIR") {
|
|
318
|
+
throw new Error("Unable to verify the OpenCode database migration marker.", { cause: error });
|
|
319
|
+
}
|
|
320
|
+
throw migrationRequiredError();
|
|
321
|
+
}
|
|
322
|
+
const ownedByCurrentUser = typeof process.getuid !== "function"
|
|
323
|
+
|| info.uid === process.getuid();
|
|
324
|
+
if (!info.isFile() || !ownedByCurrentUser) {
|
|
325
|
+
throw migrationRequiredError();
|
|
326
|
+
}
|
|
327
|
+
return userDataHome;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function migrationRequiredError() {
|
|
331
|
+
return new Error(
|
|
332
|
+
"Direct OpenCode requires a migrated user database before it can isolate run state. Run `opencode db migrate --pure` once, then retry.",
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function registerExitCleanup() {
|
|
337
|
+
if (exitCleanupRegistered) return;
|
|
338
|
+
exitCleanupRegistered = true;
|
|
339
|
+
process.once("exit", () => {
|
|
340
|
+
for (const directory of privateStateDirectories) {
|
|
341
|
+
try { rmSync(directory, { recursive: true, force: true }); } catch { /* best effort */ }
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** Internal test cleanup for any server state left by an interrupted test/process. */
|
|
347
|
+
export async function disposeIsolatedOpenCodeState() {
|
|
348
|
+
versionCheckPromise = undefined;
|
|
349
|
+
const directories = [...privateStateDirectories];
|
|
350
|
+
privateStateDirectories.clear();
|
|
351
|
+
await Promise.all(directories.map((directory) => rm(directory, { recursive: true, force: true })));
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** Cached security preflight for the CLI features/event contract this bridge requires. */
|
|
355
|
+
export function verifyOpenCodeVersion() {
|
|
356
|
+
if (versionCheckPromise === undefined) {
|
|
357
|
+
const pending = runOpenCodeVersionCheck();
|
|
358
|
+
versionCheckPromise = pending;
|
|
359
|
+
void pending.catch(() => {
|
|
360
|
+
// A user may install/upgrade OpenCode while this host process remains
|
|
361
|
+
// alive. Cache successes, but let a later run retry a failed preflight.
|
|
362
|
+
if (versionCheckPromise === pending) versionCheckPromise = undefined;
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
return versionCheckPromise;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function runOpenCodeVersionCheck() {
|
|
369
|
+
const env = safeProcessEnv();
|
|
370
|
+
const child = launch("opencode", ["--version"], {
|
|
371
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
372
|
+
env,
|
|
373
|
+
});
|
|
374
|
+
let output = "";
|
|
375
|
+
return new Promise((resolve, reject) => {
|
|
376
|
+
let settled = false;
|
|
377
|
+
let timer;
|
|
378
|
+
const finish = (error) => {
|
|
379
|
+
if (settled) return;
|
|
380
|
+
settled = true;
|
|
381
|
+
clearTimeout(timer);
|
|
382
|
+
child.stdout?.off?.("data", onStdout);
|
|
383
|
+
child.off?.("error", onError);
|
|
384
|
+
child.off?.("exit", onExit);
|
|
385
|
+
if (error === undefined) resolve();
|
|
386
|
+
else reject(error);
|
|
387
|
+
};
|
|
388
|
+
const onStdout = (chunk) => {
|
|
389
|
+
output = appendVersionOutput(output, chunk);
|
|
390
|
+
};
|
|
391
|
+
const onError = () => finish(versionUnavailableError());
|
|
392
|
+
const onExit = (code) => {
|
|
393
|
+
if (code !== 0) {
|
|
394
|
+
finish(versionUnavailableError());
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
const version = parseStableVersion(output.trim());
|
|
398
|
+
if (version === undefined) {
|
|
399
|
+
finish(versionUnavailableError());
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
if (compareVersion(version, MINIMUM_OPENCODE_VERSION) < 0) {
|
|
403
|
+
finish(new Error(
|
|
404
|
+
`Direct OpenCode requires stable opencode CLI >=1.15.0; found ${version.join(".")}. Upgrade OpenCode and retry.`,
|
|
405
|
+
));
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
finish();
|
|
409
|
+
};
|
|
410
|
+
child.stdout?.on?.("data", onStdout);
|
|
411
|
+
child.stderr?.resume?.();
|
|
412
|
+
child.on?.("error", onError);
|
|
413
|
+
child.on?.("exit", onExit);
|
|
414
|
+
timer = setTimeout(() => {
|
|
415
|
+
try { child.kill("SIGKILL"); } catch { /* best effort */ }
|
|
416
|
+
finish(versionUnavailableError());
|
|
417
|
+
}, VERSION_CHECK_TIMEOUT_MS);
|
|
418
|
+
timer.unref?.();
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function parseStableVersion(value) {
|
|
423
|
+
const match = /^v?(\d+)\.(\d+)\.(\d+)(?:\+[0-9A-Za-z.-]+)?$/u.exec(value);
|
|
424
|
+
if (match === null) return undefined;
|
|
425
|
+
const version = match.slice(1, 4).map(Number);
|
|
426
|
+
return version.every((part) => Number.isSafeInteger(part)) ? version : undefined;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function compareVersion(left, right) {
|
|
430
|
+
for (let index = 0; index < 3; index += 1) {
|
|
431
|
+
if (left[index] !== right[index]) return left[index] - right[index];
|
|
432
|
+
}
|
|
433
|
+
return 0;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function versionUnavailableError() {
|
|
437
|
+
return new Error(
|
|
438
|
+
"Direct OpenCode requires stable opencode CLI >=1.15.0, but its installed version could not be verified. Install or upgrade OpenCode and retry.",
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function appendVersionOutput(current, chunk) {
|
|
443
|
+
const next = current + String(chunk);
|
|
444
|
+
return next.length <= MAX_VERSION_OUTPUT_CHARS
|
|
445
|
+
? next
|
|
446
|
+
: next.slice(0, MAX_VERSION_OUTPUT_CHARS);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function safeProcessEnv() {
|
|
450
|
+
const env = {};
|
|
451
|
+
for (const key of [
|
|
452
|
+
"PATH",
|
|
453
|
+
"LANG",
|
|
454
|
+
"LC_ALL",
|
|
455
|
+
"LC_CTYPE",
|
|
456
|
+
"SHELL",
|
|
457
|
+
"TMPDIR",
|
|
458
|
+
"TMP",
|
|
459
|
+
"TEMP",
|
|
460
|
+
"SystemRoot",
|
|
461
|
+
"WINDIR",
|
|
462
|
+
"ComSpec",
|
|
463
|
+
"PATHEXT",
|
|
464
|
+
]) {
|
|
465
|
+
const value = process.env[key];
|
|
466
|
+
if (typeof value === "string" && value.length > 0) env[key] = value;
|
|
467
|
+
}
|
|
468
|
+
return env;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function positiveTimeout(value) {
|
|
472
|
+
const parsed = Number(value);
|
|
473
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_START_TIMEOUT_MS;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function abortReason(signal) {
|
|
477
|
+
if (signal?.reason instanceof Error) return signal.reason;
|
|
478
|
+
return Object.assign(new Error("OpenCode server startup aborted."), { name: "AbortError" });
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function appendBounded(current, chunk) {
|
|
482
|
+
const next = current + String(chunk);
|
|
483
|
+
return next.length <= MAX_START_OUTPUT_CHARS
|
|
484
|
+
? next
|
|
485
|
+
: next.slice(next.length - MAX_START_OUTPUT_CHARS);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function safeLoopbackServerUrl(value) {
|
|
489
|
+
try {
|
|
490
|
+
const parsed = new URL(value);
|
|
491
|
+
const port = Number(parsed.port);
|
|
492
|
+
if (parsed.protocol !== "http:"
|
|
493
|
+
|| parsed.hostname !== "127.0.0.1"
|
|
494
|
+
|| !Number.isInteger(port)
|
|
495
|
+
|| port < 1
|
|
496
|
+
|| port > 65_535
|
|
497
|
+
|| parsed.username !== ""
|
|
498
|
+
|| parsed.password !== ""
|
|
499
|
+
|| parsed.pathname !== "/"
|
|
500
|
+
|| parsed.search !== ""
|
|
501
|
+
|| parsed.hash !== "") {
|
|
502
|
+
return undefined;
|
|
503
|
+
}
|
|
504
|
+
return parsed.origin;
|
|
505
|
+
} catch {
|
|
506
|
+
return undefined;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
@@ -8,6 +8,7 @@ export const COMMON_CAPABILITIES = {
|
|
|
8
8
|
supports_builtin_tools: true,
|
|
9
9
|
supports_live_input: true,
|
|
10
10
|
supports_native_subagents: true,
|
|
11
|
+
supports_fast_mode: false,
|
|
11
12
|
};
|
|
12
13
|
|
|
13
14
|
export const RUNTIME_CAPABILITIES = {
|
|
@@ -34,6 +35,17 @@ export const RUNTIME_CAPABILITIES = {
|
|
|
34
35
|
// The codex-app bridge keeps the app-server subprocess + thread alive
|
|
35
36
|
// when options.sessionKeepAlive is set; resumed turns reuse the thread.
|
|
36
37
|
supports_session_resume: true,
|
|
38
|
+
supports_fast_mode: true,
|
|
39
|
+
},
|
|
40
|
+
opencode: {
|
|
41
|
+
runtime: "cli",
|
|
42
|
+
...COMMON_CAPABILITIES,
|
|
43
|
+
structured_output: false,
|
|
44
|
+
supports_session_resume: false,
|
|
45
|
+
supports_mcp: false,
|
|
46
|
+
supports_skills: false,
|
|
47
|
+
supports_live_input: false,
|
|
48
|
+
supports_native_subagents: false,
|
|
37
49
|
},
|
|
38
50
|
};
|
|
39
51
|
|
|
@@ -2,6 +2,7 @@ export const DEFAULT_CONTEXT_WINDOW = "default";
|
|
|
2
2
|
export const ONE_MILLION_CONTEXT_WINDOW = "1m";
|
|
3
3
|
|
|
4
4
|
export const CLAUDE_ONE_MILLION_CONTEXT_MODELS = new Set([
|
|
5
|
+
"claude-opus-4-8",
|
|
5
6
|
"claude-opus-4-7",
|
|
6
7
|
"claude-opus-4-6",
|
|
7
8
|
]);
|
|
@@ -16,6 +17,10 @@ export function stripContextWindowSuffix(model) {
|
|
|
16
17
|
return String(model || "").replace(/\[1m\]$/i, "");
|
|
17
18
|
}
|
|
18
19
|
|
|
20
|
+
export function hasExplicitOneMillionContextWindow(model) {
|
|
21
|
+
return /\[1m\]$/i.test(String(model || ""));
|
|
22
|
+
}
|
|
23
|
+
|
|
19
24
|
export function claudeModelSupportsOneMillionContext(model) {
|
|
20
25
|
return CLAUDE_ONE_MILLION_CONTEXT_MODELS.has(stripContextWindowSuffix(model));
|
|
21
26
|
}
|
|
@@ -27,6 +32,9 @@ export function claudeModelSupportsContextWindow(model, contextWindow) {
|
|
|
27
32
|
}
|
|
28
33
|
|
|
29
34
|
export function modelWithContextWindow(model, contextWindow) {
|
|
35
|
+
if (hasExplicitOneMillionContextWindow(model)) {
|
|
36
|
+
return String(model);
|
|
37
|
+
}
|
|
30
38
|
const base = stripContextWindowSuffix(model);
|
|
31
39
|
if (
|
|
32
40
|
normalizeContextWindow(contextWindow) === ONE_MILLION_CONTEXT_WINDOW
|
|
@@ -37,13 +37,19 @@ const builtinBridgeSpecs = {
|
|
|
37
37
|
supports: (ref, options) => ref?.sdk === "codex" && options?.executionMode === "cli",
|
|
38
38
|
// The codex-app bridge keeps the app-server subprocess + thread alive
|
|
39
39
|
// across turns when options.sessionKeepAlive is set.
|
|
40
|
-
capabilities: () =>
|
|
40
|
+
capabilities: () => {
|
|
41
|
+
const { kind: _sdkKind, ...capabilities } = runtimeCapabilities("codex");
|
|
42
|
+
return { kind: "codex-app", ...capabilities };
|
|
43
|
+
},
|
|
41
44
|
load: async () => (await import("../providers/codex-app.js")).codexAppRuntimeBridge,
|
|
42
45
|
},
|
|
43
46
|
"opencode-app": {
|
|
44
47
|
id: "opencode-app",
|
|
45
48
|
supports: (ref, options) => ref?.sdk === "opencode" && options?.executionMode === "cli",
|
|
46
|
-
capabilities: () =>
|
|
49
|
+
capabilities: () => {
|
|
50
|
+
const { kind: _sdkKind, ...capabilities } = runtimeCapabilities("opencode");
|
|
51
|
+
return { kind: "opencode-app", ...capabilities };
|
|
52
|
+
},
|
|
47
53
|
load: async () => (await import("../providers/opencode-app.js")).opencodeAppRuntimeBridge,
|
|
48
54
|
},
|
|
49
55
|
claude: {
|