@obversa/engine-opencode-cli 0.1.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/LICENSE +21 -0
- package/README.md +28 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1305 -0
- package/dist/index.js.map +1 -0
- package/dist/opencode-cli.d.ts +58 -0
- package/package.json +53 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1305 @@
|
|
|
1
|
+
import { realpathSync, mkdirSync, mkdtempSync, rmSync, readdirSync, statSync, existsSync } from 'fs';
|
|
2
|
+
import { tmpdir, userInfo } from 'os';
|
|
3
|
+
import { isAbsolute, join, dirname, delimiter, relative, sep } from 'path';
|
|
4
|
+
import { isDeepStrictEqual } from 'util';
|
|
5
|
+
import { modelIdentity, canonicalJson, engineSelection, EngineError, EngineIncompleteResultError, validateIncompleteResultEvidence, cloneFrozenJson, classifyEngineFailure, validateAgentResult, assertReadAccess, reportedUsage } from '@obversa/api';
|
|
6
|
+
import { attemptEnvironment, DEFAULT_OWNED_COMMAND_LIMITS, ownedCommandIdentity, scrubCapture, resolveCommandExecutable, runOwnedCommand, OwnedCommandError, retryAfterHeaderToMs } from '@obversa/core/command';
|
|
7
|
+
|
|
8
|
+
// src/opencode-cli.ts
|
|
9
|
+
var SUPPORTED_VERSION = "1.18.23";
|
|
10
|
+
var VERSION_TIMEOUT_MS = 1e4;
|
|
11
|
+
var VERSION_TEARDOWN_MS = 1e3;
|
|
12
|
+
var VERSION_OUTPUT_BYTES = 4096;
|
|
13
|
+
var OPENCODE_VERSION = /^\d+\.\d+\.\d+$/u;
|
|
14
|
+
var CONTROL_CHARACTER = /[\u0000-\u001f\u007f]/u;
|
|
15
|
+
var STRUCTURED_RESULT_MARKER = "OBVERSA_STRUCTURED_RESULT_V1\n";
|
|
16
|
+
var CONFIG_INTERPOLATION = /\{(?:env|file):/u;
|
|
17
|
+
var STEP_FINISH_REASONS = /* @__PURE__ */ new Set([
|
|
18
|
+
"stop",
|
|
19
|
+
"length",
|
|
20
|
+
"tool-calls",
|
|
21
|
+
"content-filter",
|
|
22
|
+
"error",
|
|
23
|
+
"unknown"
|
|
24
|
+
]);
|
|
25
|
+
var BASE_SYSTEM_PROMPT = "Execute one isolated Obversa node attempt. Follow only this system prompt and the user prompt. Use only the declared tools and permissions.";
|
|
26
|
+
var STRUCTURED_RESULT_INSTRUCTION = [
|
|
27
|
+
"Your final answer must be one completed text part beginning with exactly:",
|
|
28
|
+
"OBVERSA_STRUCTURED_RESULT_V1",
|
|
29
|
+
"After that marker and one newline, write exactly one JSON value and nothing else."
|
|
30
|
+
].join("\n");
|
|
31
|
+
var CAPABILITIES = /* @__PURE__ */ new Set([
|
|
32
|
+
"read",
|
|
33
|
+
"edit",
|
|
34
|
+
"glob",
|
|
35
|
+
"grep",
|
|
36
|
+
"bash",
|
|
37
|
+
"task",
|
|
38
|
+
"todowrite",
|
|
39
|
+
"webfetch",
|
|
40
|
+
"websearch"
|
|
41
|
+
]);
|
|
42
|
+
var FILESYSTEM_CAPABILITIES = /* @__PURE__ */ new Set([
|
|
43
|
+
"read",
|
|
44
|
+
"edit",
|
|
45
|
+
"glob",
|
|
46
|
+
"grep",
|
|
47
|
+
"bash"
|
|
48
|
+
]);
|
|
49
|
+
var WRITE_CAPABILITIES = /* @__PURE__ */ new Set(["edit", "bash"]);
|
|
50
|
+
var PATTERN_PERMISSIONS = /* @__PURE__ */ new Set([
|
|
51
|
+
"read",
|
|
52
|
+
"edit",
|
|
53
|
+
"glob",
|
|
54
|
+
"grep",
|
|
55
|
+
"bash",
|
|
56
|
+
"task"
|
|
57
|
+
]);
|
|
58
|
+
var PERMISSION_NAMES = /* @__PURE__ */ new Map([
|
|
59
|
+
["read", "read"],
|
|
60
|
+
["edit", "edit"],
|
|
61
|
+
["write", "edit"],
|
|
62
|
+
["applypatch", "edit"],
|
|
63
|
+
["glob", "glob"],
|
|
64
|
+
["grep", "grep"],
|
|
65
|
+
["bash", "bash"],
|
|
66
|
+
["task", "task"],
|
|
67
|
+
["todowrite", "todowrite"],
|
|
68
|
+
["webfetch", "webfetch"],
|
|
69
|
+
["websearch", "websearch"]
|
|
70
|
+
]);
|
|
71
|
+
function opencode(modelName, options) {
|
|
72
|
+
const selected = model(modelName);
|
|
73
|
+
const { modelFamily } = modelIdentity(selected.value);
|
|
74
|
+
return {
|
|
75
|
+
engine: new OpenCodeCliEngine({
|
|
76
|
+
executable: options.executable,
|
|
77
|
+
version: SUPPORTED_VERSION,
|
|
78
|
+
identity: { provider: selected.provider, modelFamily }
|
|
79
|
+
}),
|
|
80
|
+
identity: {
|
|
81
|
+
adapter: "opencode-cli",
|
|
82
|
+
provider: selected.provider,
|
|
83
|
+
modelFamily,
|
|
84
|
+
model: selected.value,
|
|
85
|
+
tools: options.tools ?? ["read", "grep"]
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function text(value, field) {
|
|
90
|
+
if (typeof value !== "string" || value.length === 0 || value !== value.trim() || CONTROL_CHARACTER.test(value)) {
|
|
91
|
+
throw new TypeError(
|
|
92
|
+
`${field} must be a non-empty trimmed string without control characters`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
97
|
+
function nullableText(value, field) {
|
|
98
|
+
return value === null ? null : text(value, field);
|
|
99
|
+
}
|
|
100
|
+
function object(value, field) {
|
|
101
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
102
|
+
throw new TypeError(`${field} must be an object`);
|
|
103
|
+
}
|
|
104
|
+
return value;
|
|
105
|
+
}
|
|
106
|
+
function safeInteger(value, field) {
|
|
107
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
108
|
+
throw new TypeError(`${field} must be a non-negative safe integer`);
|
|
109
|
+
}
|
|
110
|
+
return value;
|
|
111
|
+
}
|
|
112
|
+
function safeSum(left, right) {
|
|
113
|
+
const result = left + right;
|
|
114
|
+
return Number.isSafeInteger(result) ? result : void 0;
|
|
115
|
+
}
|
|
116
|
+
function model(value) {
|
|
117
|
+
const checked = text(value, "OpenCode request model");
|
|
118
|
+
const slash = checked.indexOf("/");
|
|
119
|
+
if (slash < 1 || slash === checked.length - 1 || checked.includes(" ") || checked.indexOf("/", slash + 1) !== -1) {
|
|
120
|
+
throw new TypeError("OpenCode request model must use provider/model format");
|
|
121
|
+
}
|
|
122
|
+
if (CONFIG_INTERPOLATION.test(checked)) {
|
|
123
|
+
throw new TypeError(
|
|
124
|
+
"OpenCode request model must not carry config interpolation tokens {env:...} or {file:...}"
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
const derived = modelIdentity(checked);
|
|
128
|
+
if (derived.provider === void 0) {
|
|
129
|
+
throw new TypeError("OpenCode request model must use provider/model format");
|
|
130
|
+
}
|
|
131
|
+
return Object.freeze({ value: checked, provider: derived.provider });
|
|
132
|
+
}
|
|
133
|
+
function providerForModel(selectedModel, identity) {
|
|
134
|
+
const asserted = nullableText(identity.provider, "OpenCode provider");
|
|
135
|
+
if (asserted !== null && asserted !== selectedModel.provider) {
|
|
136
|
+
throw new TypeError(
|
|
137
|
+
`OpenCode provider identity ${asserted} does not match model provider ${selectedModel.provider}`
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
return selectedModel.provider;
|
|
141
|
+
}
|
|
142
|
+
function familyForModel(selectedModel, identity) {
|
|
143
|
+
const asserted = nullableText(identity.modelFamily, "OpenCode model family");
|
|
144
|
+
const derived = modelIdentity(selectedModel.value).modelFamily;
|
|
145
|
+
if (asserted !== null && asserted !== derived) {
|
|
146
|
+
throw new TypeError(
|
|
147
|
+
`OpenCode model family identity ${asserted} does not match model family ${derived}`
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
return derived;
|
|
151
|
+
}
|
|
152
|
+
function selectedEnvironment(raw) {
|
|
153
|
+
const selected = {};
|
|
154
|
+
for (const [name, value] of Object.entries(raw ?? {})) {
|
|
155
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(name)) {
|
|
156
|
+
throw new TypeError(`OpenCode environment name ${name} is invalid`);
|
|
157
|
+
}
|
|
158
|
+
if (typeof value !== "string" || value.includes("\0")) {
|
|
159
|
+
throw new TypeError(
|
|
160
|
+
`OpenCode environment value ${name} must be a string without NUL`
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
if (name === "HOME" || name === "TMPDIR" || name.startsWith("XDG_") || name.startsWith("OPENCODE_")) {
|
|
164
|
+
throw new TypeError(`OpenCode environment cannot replace ${name}`);
|
|
165
|
+
}
|
|
166
|
+
selected[name] = value;
|
|
167
|
+
}
|
|
168
|
+
return Object.freeze(selected);
|
|
169
|
+
}
|
|
170
|
+
function authValue(value) {
|
|
171
|
+
if (value === void 0) return Object.freeze({});
|
|
172
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
173
|
+
throw new TypeError("OpenCode auth must be a provider-keyed object");
|
|
174
|
+
}
|
|
175
|
+
const checked = cloneFrozenJson(value);
|
|
176
|
+
for (const [provider, rawEntry] of Object.entries(checked)) {
|
|
177
|
+
text(provider, "OpenCode auth provider");
|
|
178
|
+
const entry = object(rawEntry, `OpenCode auth entry ${provider}`);
|
|
179
|
+
if (entry.type === "wellknown") {
|
|
180
|
+
throw new TypeError("OpenCode wellknown auth is not isolated");
|
|
181
|
+
}
|
|
182
|
+
if (entry.type === "api") {
|
|
183
|
+
for (const field of Object.keys(entry)) {
|
|
184
|
+
if (!["type", "key", "metadata"].includes(field)) {
|
|
185
|
+
throw new TypeError(`OpenCode api auth field ${field} is not supported`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (typeof entry.key !== "string") {
|
|
189
|
+
throw new TypeError("OpenCode api auth key must be a string");
|
|
190
|
+
}
|
|
191
|
+
if (entry.metadata !== void 0) {
|
|
192
|
+
const metadata = object(
|
|
193
|
+
entry.metadata,
|
|
194
|
+
`OpenCode api auth metadata ${provider}`
|
|
195
|
+
);
|
|
196
|
+
if (Object.values(metadata).some((item) => typeof item !== "string")) {
|
|
197
|
+
throw new TypeError("OpenCode api auth metadata values must be strings");
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (entry.type === "oauth") {
|
|
203
|
+
for (const field of Object.keys(entry)) {
|
|
204
|
+
if (![
|
|
205
|
+
"type",
|
|
206
|
+
"refresh",
|
|
207
|
+
"access",
|
|
208
|
+
"expires",
|
|
209
|
+
"accountId",
|
|
210
|
+
"enterpriseUrl"
|
|
211
|
+
].includes(field)) {
|
|
212
|
+
throw new TypeError(`OpenCode oauth auth field ${field} is not supported`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (typeof entry.refresh !== "string" || typeof entry.access !== "string") {
|
|
216
|
+
throw new TypeError("OpenCode oauth auth tokens must be strings");
|
|
217
|
+
}
|
|
218
|
+
safeInteger(entry.expires, "OpenCode oauth auth expiry");
|
|
219
|
+
for (const field of ["accountId", "enterpriseUrl"]) {
|
|
220
|
+
if (entry[field] !== void 0 && typeof entry[field] !== "string") {
|
|
221
|
+
throw new TypeError(`OpenCode oauth auth ${field} must be a string`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
throw new TypeError(`OpenCode auth type ${String(entry.type)} is not supported`);
|
|
227
|
+
}
|
|
228
|
+
return checked;
|
|
229
|
+
}
|
|
230
|
+
function managedConfigSources(extraDirectories = []) {
|
|
231
|
+
const systemDirectory = process.platform === "darwin" ? "/Library/Application Support/opencode" : process.platform === "win32" ? "C:\\ProgramData\\opencode" : "/etc/opencode";
|
|
232
|
+
const directories = [systemDirectory, ...extraDirectories];
|
|
233
|
+
const sources = directories.flatMap((directory) => [
|
|
234
|
+
join(directory, "opencode.json"),
|
|
235
|
+
join(directory, "opencode.jsonc")
|
|
236
|
+
]);
|
|
237
|
+
if (process.platform === "darwin") {
|
|
238
|
+
let user = "user";
|
|
239
|
+
try {
|
|
240
|
+
user = userInfo().username || user;
|
|
241
|
+
} catch {
|
|
242
|
+
}
|
|
243
|
+
sources.push(
|
|
244
|
+
join(
|
|
245
|
+
"/Library/Managed Preferences",
|
|
246
|
+
user,
|
|
247
|
+
"ai.opencode.managed.plist"
|
|
248
|
+
),
|
|
249
|
+
"/Library/Managed Preferences/ai.opencode.managed.plist"
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
return Object.freeze(sources);
|
|
253
|
+
}
|
|
254
|
+
function findManagedConfig(sources) {
|
|
255
|
+
return sources.find((candidate) => existsSync(candidate));
|
|
256
|
+
}
|
|
257
|
+
function assertNoManagedConfig(sources) {
|
|
258
|
+
const source = findManagedConfig(sources);
|
|
259
|
+
if (source !== void 0) {
|
|
260
|
+
throw new TypeError(`managed OpenCode config is not isolated: ${source}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function assertNoProjectInstructions(workspace, capabilities) {
|
|
264
|
+
if (!capabilities.some((capability) => FILESYSTEM_CAPABILITIES.has(capability))) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
const instructionNames = /* @__PURE__ */ new Set(["agents.md", "claude.md", "context.md"]);
|
|
268
|
+
const pending = [{ directory: workspace, root: true }];
|
|
269
|
+
const visited = /* @__PURE__ */ new Set();
|
|
270
|
+
while (pending.length > 0) {
|
|
271
|
+
const { directory, root } = pending.pop();
|
|
272
|
+
let resolvedDirectory;
|
|
273
|
+
let entries;
|
|
274
|
+
try {
|
|
275
|
+
resolvedDirectory = realpathSync(directory);
|
|
276
|
+
if (visited.has(resolvedDirectory)) continue;
|
|
277
|
+
visited.add(resolvedDirectory);
|
|
278
|
+
entries = readdirSync(resolvedDirectory, { withFileTypes: true });
|
|
279
|
+
} catch (error) {
|
|
280
|
+
throw new TypeError(
|
|
281
|
+
`OpenCode could not inspect project instructions under ${directory}: ${error instanceof Error ? error.message : String(error)}`
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
for (const entry of entries) {
|
|
285
|
+
const path = join(resolvedDirectory, entry.name);
|
|
286
|
+
if (!root && instructionNames.has(entry.name.toLowerCase())) {
|
|
287
|
+
throw new TypeError(
|
|
288
|
+
`OpenCode project instruction ${path} is not isolated`
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
if (entry.isSymbolicLink()) {
|
|
292
|
+
let target;
|
|
293
|
+
let directoryTarget;
|
|
294
|
+
try {
|
|
295
|
+
target = realpathSync(path);
|
|
296
|
+
directoryTarget = statSync(target).isDirectory();
|
|
297
|
+
} catch (error) {
|
|
298
|
+
throw new TypeError(
|
|
299
|
+
`OpenCode could not inspect project symlink ${path}: ${error instanceof Error ? error.message : String(error)}`
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
const pathFromWorkspace = relative(workspace, target);
|
|
303
|
+
if (pathFromWorkspace === ".." || pathFromWorkspace.startsWith(`..${sep}`) || isAbsolute(pathFromWorkspace)) {
|
|
304
|
+
throw new TypeError(
|
|
305
|
+
`OpenCode symlink ${path} resolves outside the workspace`
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
if (directoryTarget) {
|
|
309
|
+
if (target === workspace) {
|
|
310
|
+
throw new TypeError(
|
|
311
|
+
`OpenCode symlink ${path} resolves to the workspace root`
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
pending.push({ directory: target, root: false });
|
|
315
|
+
}
|
|
316
|
+
} else if (entry.isDirectory()) {
|
|
317
|
+
pending.push({ directory: path, root: false });
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
function authRedactions(value) {
|
|
323
|
+
const redactions = {
|
|
324
|
+
OPENCODE_AUTH_CONTENT: canonicalJson(value)
|
|
325
|
+
};
|
|
326
|
+
const pending = [value];
|
|
327
|
+
let index = 0;
|
|
328
|
+
while (pending.length > 0) {
|
|
329
|
+
const current = pending.pop();
|
|
330
|
+
if (typeof current === "string") {
|
|
331
|
+
redactions[`OPENCODE_AUTH_VALUE_${index}`] = current;
|
|
332
|
+
index += 1;
|
|
333
|
+
} else if (Array.isArray(current)) {
|
|
334
|
+
pending.push(...current);
|
|
335
|
+
} else if (typeof current === "object" && current !== null) {
|
|
336
|
+
pending.push(...Object.values(current));
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return Object.freeze(redactions);
|
|
340
|
+
}
|
|
341
|
+
function scrubExactValues(raw, redactions) {
|
|
342
|
+
const values = [...new Set(Object.values(redactions))].filter((value) => value.length > 0).sort((left, right) => right.length - left.length);
|
|
343
|
+
let result = raw;
|
|
344
|
+
for (const value of values) result = result.split(value).join("[redacted]");
|
|
345
|
+
return result;
|
|
346
|
+
}
|
|
347
|
+
function requestedCapabilities(request) {
|
|
348
|
+
assertReadAccess(request);
|
|
349
|
+
const capabilities = engineSelection({
|
|
350
|
+
adapter: "opencode-cli",
|
|
351
|
+
capabilities: request.tools ?? []
|
|
352
|
+
}).capabilities;
|
|
353
|
+
for (const capability of capabilities) {
|
|
354
|
+
if (!CAPABILITIES.has(capability)) {
|
|
355
|
+
throw new TypeError(
|
|
356
|
+
`OpenCode cannot represent capability ${capability}`
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
if (request.leaf !== false && capabilities.includes("task")) {
|
|
361
|
+
throw new TypeError("OpenCode leaf attempts cannot expose task");
|
|
362
|
+
}
|
|
363
|
+
const workspaceMode = request.workspaceMode ?? "none";
|
|
364
|
+
if (workspaceMode !== "write" && capabilities.includes("task")) {
|
|
365
|
+
throw new TypeError(`OpenCode workspace mode ${workspaceMode} cannot expose task capability`);
|
|
366
|
+
}
|
|
367
|
+
if (workspaceMode === "none") {
|
|
368
|
+
const unsafe = capabilities.find(
|
|
369
|
+
(capability) => FILESYSTEM_CAPABILITIES.has(capability)
|
|
370
|
+
);
|
|
371
|
+
if (unsafe !== void 0) {
|
|
372
|
+
throw new TypeError(
|
|
373
|
+
`OpenCode workspace mode none cannot expose capability ${unsafe}`
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
if (workspaceMode === "read") {
|
|
378
|
+
const unsafe = capabilities.find(
|
|
379
|
+
(capability) => WRITE_CAPABILITIES.has(capability)
|
|
380
|
+
);
|
|
381
|
+
if (unsafe !== void 0) {
|
|
382
|
+
throw new TypeError(
|
|
383
|
+
`OpenCode read-only workspace cannot expose capability ${unsafe}`
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
if (!capabilities.some((capability) => ["read", "glob", "grep"].includes(capability))) {
|
|
387
|
+
throw new TypeError("OpenCode read workspace requires a file-reading capability");
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
if (workspaceMode !== "none" && workspaceMode !== "read" && workspaceMode !== "write") {
|
|
391
|
+
throw new TypeError("OpenCode workspace mode must be none, read, or write");
|
|
392
|
+
}
|
|
393
|
+
return capabilities;
|
|
394
|
+
}
|
|
395
|
+
function permissionRule(raw) {
|
|
396
|
+
const checked = text(raw, "OpenCode permission rule");
|
|
397
|
+
const match = /^([A-Za-z][A-Za-z0-9_]*)(?:\(([^\u0000-\u001f\u007f]+)\))?$/u.exec(checked);
|
|
398
|
+
if (!match) {
|
|
399
|
+
throw new TypeError(`OpenCode cannot represent permission rule ${checked}`);
|
|
400
|
+
}
|
|
401
|
+
const permission = PERMISSION_NAMES.get(match[1].toLowerCase());
|
|
402
|
+
if (permission === void 0) {
|
|
403
|
+
throw new TypeError(`OpenCode cannot represent permission rule ${checked}`);
|
|
404
|
+
}
|
|
405
|
+
return Object.freeze({ permission, pattern: match[2] ?? null });
|
|
406
|
+
}
|
|
407
|
+
function permissions(request) {
|
|
408
|
+
const capabilities = requestedCapabilities(request);
|
|
409
|
+
const toolValues = { "*": false };
|
|
410
|
+
for (const capability of capabilities) toolValues[capability] = true;
|
|
411
|
+
const permissionValues = { "*": "deny" };
|
|
412
|
+
const seen = /* @__PURE__ */ new Set();
|
|
413
|
+
for (const raw of request.allowedTools ?? []) {
|
|
414
|
+
if (seen.has(raw)) throw new TypeError("allowedTools must be unique");
|
|
415
|
+
seen.add(raw);
|
|
416
|
+
const rule = permissionRule(raw);
|
|
417
|
+
if (rule.pattern !== null && !PATTERN_PERMISSIONS.has(rule.permission)) {
|
|
418
|
+
throw new TypeError(
|
|
419
|
+
`OpenCode permission ${rule.permission} does not accept patterns`
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
if (!capabilities.includes(rule.permission)) {
|
|
423
|
+
throw new TypeError(
|
|
424
|
+
`OpenCode permission ${raw} has no declared ${rule.permission} capability`
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
if (rule.pattern === null) {
|
|
428
|
+
if (Object.hasOwn(permissionValues, rule.permission)) {
|
|
429
|
+
throw new TypeError(
|
|
430
|
+
`OpenCode permission ${rule.permission} has overlapping rules`
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
permissionValues[rule.permission] = "allow";
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
const current = permissionValues[rule.permission];
|
|
437
|
+
if (typeof current === "string") {
|
|
438
|
+
throw new TypeError(
|
|
439
|
+
`OpenCode permission ${rule.permission} has overlapping rules`
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
const patterns = current === void 0 ? { "*": "deny" } : { ...current };
|
|
443
|
+
if (Object.hasOwn(patterns, rule.pattern)) {
|
|
444
|
+
throw new TypeError(`OpenCode permission rule ${raw} is duplicated`);
|
|
445
|
+
}
|
|
446
|
+
patterns[rule.pattern] = "allow";
|
|
447
|
+
permissionValues[rule.permission] = patterns;
|
|
448
|
+
}
|
|
449
|
+
return Object.freeze({
|
|
450
|
+
capabilities,
|
|
451
|
+
tools: cloneFrozenJson(toolValues),
|
|
452
|
+
permission: cloneFrozenJson(permissionValues)
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
function trustedSystemPrompt(request) {
|
|
456
|
+
let prompt;
|
|
457
|
+
if (request.systemMode === "replace") {
|
|
458
|
+
if (request.system === void 0) {
|
|
459
|
+
throw new TypeError("OpenCode system replacement requires system text");
|
|
460
|
+
}
|
|
461
|
+
prompt = request.system;
|
|
462
|
+
} else {
|
|
463
|
+
prompt = request.system === void 0 ? BASE_SYSTEM_PROMPT : `${BASE_SYSTEM_PROMPT}
|
|
464
|
+
|
|
465
|
+
${request.system}`;
|
|
466
|
+
}
|
|
467
|
+
if (request.jsonSchema !== void 0) {
|
|
468
|
+
prompt = `${prompt}
|
|
469
|
+
|
|
470
|
+
${STRUCTURED_RESULT_INSTRUCTION}`;
|
|
471
|
+
}
|
|
472
|
+
return prompt;
|
|
473
|
+
}
|
|
474
|
+
function configFor(request, selectedModel, built) {
|
|
475
|
+
const agent = {
|
|
476
|
+
mode: "primary",
|
|
477
|
+
model: selectedModel.value,
|
|
478
|
+
prompt: trustedSystemPrompt(request),
|
|
479
|
+
tools: built.tools,
|
|
480
|
+
permission: built.permission
|
|
481
|
+
};
|
|
482
|
+
return cloneFrozenJson({
|
|
483
|
+
share: "disabled",
|
|
484
|
+
autoupdate: false,
|
|
485
|
+
snapshot: false,
|
|
486
|
+
model: selectedModel.value,
|
|
487
|
+
small_model: selectedModel.value,
|
|
488
|
+
enabled_providers: [selectedModel.provider],
|
|
489
|
+
default_agent: "build",
|
|
490
|
+
plugin: [],
|
|
491
|
+
instructions: [],
|
|
492
|
+
mcp: {},
|
|
493
|
+
lsp: false,
|
|
494
|
+
formatter: false,
|
|
495
|
+
tools: built.tools,
|
|
496
|
+
permission: built.permission,
|
|
497
|
+
agent: { build: agent }
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
function serializedConfig(value) {
|
|
501
|
+
const encoded = JSON.stringify(value);
|
|
502
|
+
if (CONFIG_INTERPOLATION.test(encoded)) {
|
|
503
|
+
throw new TypeError(
|
|
504
|
+
"OpenCode config interpolation tokens {env:...} and {file:...} are not allowed"
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
return encoded;
|
|
508
|
+
}
|
|
509
|
+
function buildOpenCodeInvocation(request, options, rawDirectory) {
|
|
510
|
+
if (typeof request.cwd !== "string" || !isAbsolute(request.cwd)) {
|
|
511
|
+
throw new TypeError("OpenCode request cwd must be an absolute path");
|
|
512
|
+
}
|
|
513
|
+
if (request.memory !== void 0) {
|
|
514
|
+
throw new TypeError("OpenCode CLI does not bridge Obversa memory");
|
|
515
|
+
}
|
|
516
|
+
if (request.env !== void 0 && Object.keys(request.env).length > 0) {
|
|
517
|
+
throw new TypeError(
|
|
518
|
+
"OpenCode request environment is not allowed; select values in the constructor environment"
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
if (!isAbsolute(rawDirectory)) {
|
|
522
|
+
throw new TypeError("OpenCode isolation directory must be an absolute path");
|
|
523
|
+
}
|
|
524
|
+
if (typeof options.executable !== "string" || !isAbsolute(options.executable)) {
|
|
525
|
+
throw new TypeError("OpenCode executable must be an absolute path");
|
|
526
|
+
}
|
|
527
|
+
if (options.version !== SUPPORTED_VERSION) {
|
|
528
|
+
throw new TypeError(`OpenCode CLI version must be ${SUPPORTED_VERSION}`);
|
|
529
|
+
}
|
|
530
|
+
const selectedModel = model(request.model);
|
|
531
|
+
providerForModel(selectedModel, options.identity);
|
|
532
|
+
familyForModel(selectedModel, options.identity);
|
|
533
|
+
const built = permissions(request);
|
|
534
|
+
if (built.capabilities.includes("websearch") && selectedModel.provider !== "opencode" && selectedModel.provider !== "opencode-go") {
|
|
535
|
+
throw new TypeError(
|
|
536
|
+
`OpenCode provider ${selectedModel.provider} cannot expose websearch`
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
const directory = realpathSync(rawDirectory);
|
|
540
|
+
const home = join(directory, "home");
|
|
541
|
+
const dataHome = join(directory, "xdg-data");
|
|
542
|
+
const configHome = join(directory, "xdg-config");
|
|
543
|
+
const cacheHome = join(directory, "xdg-cache");
|
|
544
|
+
const stateHome = join(directory, "xdg-state");
|
|
545
|
+
const temporary = join(directory, "tmp");
|
|
546
|
+
for (const path2 of [home, dataHome, configHome, cacheHome, stateHome, temporary]) {
|
|
547
|
+
mkdirSync(path2, { recursive: true, mode: 448 });
|
|
548
|
+
}
|
|
549
|
+
const selected = selectedEnvironment(options.environment);
|
|
550
|
+
const auth = authValue(options.auth);
|
|
551
|
+
const config = configFor(request, selectedModel, built);
|
|
552
|
+
const configContent = serializedConfig(config);
|
|
553
|
+
const attempt = attemptEnvironment({ ...request, env: void 0 }) ?? {};
|
|
554
|
+
const path = selected.PATH ?? [
|
|
555
|
+
dirname(process.execPath),
|
|
556
|
+
dirname(options.executable),
|
|
557
|
+
"/usr/bin",
|
|
558
|
+
"/bin",
|
|
559
|
+
"/usr/sbin",
|
|
560
|
+
"/sbin"
|
|
561
|
+
].filter((value, index, values) => values.indexOf(value) === index).join(delimiter);
|
|
562
|
+
const environment = Object.freeze({
|
|
563
|
+
...selected,
|
|
564
|
+
...attempt,
|
|
565
|
+
PATH: path,
|
|
566
|
+
HOME: home,
|
|
567
|
+
XDG_DATA_HOME: dataHome,
|
|
568
|
+
XDG_CONFIG_HOME: configHome,
|
|
569
|
+
XDG_CACHE_HOME: cacheHome,
|
|
570
|
+
XDG_STATE_HOME: stateHome,
|
|
571
|
+
TMPDIR: temporary,
|
|
572
|
+
OPENCODE_CONFIG_DIR: join(configHome, "opencode"),
|
|
573
|
+
OPENCODE_CONFIG_CONTENT: configContent,
|
|
574
|
+
OPENCODE_AUTH_CONTENT: canonicalJson(auth),
|
|
575
|
+
OPENCODE_DISABLE_PROJECT_CONFIG: "1",
|
|
576
|
+
OPENCODE_PURE: "1",
|
|
577
|
+
OPENCODE_DISABLE_DEFAULT_PLUGINS: "1",
|
|
578
|
+
OPENCODE_DISABLE_EXTERNAL_SKILLS: "1",
|
|
579
|
+
OPENCODE_DISABLE_CLAUDE_CODE: "1",
|
|
580
|
+
OPENCODE_DISABLE_AUTOUPDATE: "1",
|
|
581
|
+
OPENCODE_DISABLE_LSP_DOWNLOAD: "1",
|
|
582
|
+
OPENCODE_DISABLE_SHARE: "1"
|
|
583
|
+
});
|
|
584
|
+
return Object.freeze({
|
|
585
|
+
args: Object.freeze([
|
|
586
|
+
"run",
|
|
587
|
+
"--format",
|
|
588
|
+
"json",
|
|
589
|
+
"--pure",
|
|
590
|
+
"--model",
|
|
591
|
+
selectedModel.value,
|
|
592
|
+
"--dir",
|
|
593
|
+
request.cwd
|
|
594
|
+
]),
|
|
595
|
+
stdin: request.prompt,
|
|
596
|
+
environment,
|
|
597
|
+
configDirectory: configHome
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
function partKey(part) {
|
|
601
|
+
return `${part.sessionID}/${part.messageID}/${part.id}`;
|
|
602
|
+
}
|
|
603
|
+
function consumePart(raw, expectedType, wrapperSessionId, accumulator) {
|
|
604
|
+
const part = object(raw, `OpenCode ${expectedType} part`);
|
|
605
|
+
const id = text(part.id, `OpenCode ${expectedType} part id`);
|
|
606
|
+
const sessionId = text(
|
|
607
|
+
part.sessionID,
|
|
608
|
+
`OpenCode ${expectedType} part sessionID`
|
|
609
|
+
);
|
|
610
|
+
text(part.messageID, `OpenCode ${expectedType} part messageID`);
|
|
611
|
+
if (part.type !== expectedType) {
|
|
612
|
+
throw new TypeError(
|
|
613
|
+
`OpenCode ${expectedType} wrapper contained ${String(part.type)} part`
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
if (sessionId !== wrapperSessionId) {
|
|
617
|
+
throw new TypeError("OpenCode part session did not match its wrapper session");
|
|
618
|
+
}
|
|
619
|
+
const key = partKey({ ...part, id });
|
|
620
|
+
const encoded = canonicalJson(part);
|
|
621
|
+
const prior = accumulator.seenParts.get(key);
|
|
622
|
+
if (prior !== void 0) {
|
|
623
|
+
if (prior !== encoded) {
|
|
624
|
+
throw new TypeError(`OpenCode emitted conflicting duplicate part ${key}`);
|
|
625
|
+
}
|
|
626
|
+
return null;
|
|
627
|
+
}
|
|
628
|
+
accumulator.seenParts.set(key, encoded);
|
|
629
|
+
return part;
|
|
630
|
+
}
|
|
631
|
+
function consumeUsage(part, accumulator) {
|
|
632
|
+
try {
|
|
633
|
+
const tokens = object(part.tokens, "OpenCode step tokens");
|
|
634
|
+
const cache = object(tokens.cache, "OpenCode step cache tokens");
|
|
635
|
+
const input = safeInteger(tokens.input, "OpenCode input tokens");
|
|
636
|
+
const output = safeInteger(tokens.output, "OpenCode output tokens");
|
|
637
|
+
const reasoning = safeInteger(tokens.reasoning, "OpenCode reasoning tokens");
|
|
638
|
+
const cacheRead = safeInteger(cache.read, "OpenCode cache read tokens");
|
|
639
|
+
const cacheWrite = safeInteger(cache.write, "OpenCode cache write tokens");
|
|
640
|
+
const inputWithRead = safeSum(input, cacheRead);
|
|
641
|
+
const inputTotal = inputWithRead === void 0 ? void 0 : safeSum(inputWithRead, cacheWrite);
|
|
642
|
+
const outputTotal = safeSum(output, reasoning);
|
|
643
|
+
const nextInput = inputTotal === void 0 ? void 0 : safeSum(accumulator.inputTokens, inputTotal);
|
|
644
|
+
const nextOutput = outputTotal === void 0 ? void 0 : safeSum(accumulator.outputTokens, outputTotal);
|
|
645
|
+
const nextRead = safeSum(accumulator.cacheReadInputTokens, cacheRead);
|
|
646
|
+
const nextWrite = safeSum(accumulator.cacheCreationInputTokens, cacheWrite);
|
|
647
|
+
if (nextInput === void 0 || nextOutput === void 0 || nextRead === void 0 || nextWrite === void 0) {
|
|
648
|
+
throw new TypeError("OpenCode usage totals overflowed");
|
|
649
|
+
}
|
|
650
|
+
accumulator.inputTokens = nextInput;
|
|
651
|
+
accumulator.outputTokens = nextOutput;
|
|
652
|
+
accumulator.cacheReadInputTokens = nextRead;
|
|
653
|
+
accumulator.cacheCreationInputTokens = nextWrite;
|
|
654
|
+
accumulator.stepCount += 1;
|
|
655
|
+
} catch {
|
|
656
|
+
accumulator.usageValid = false;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
function consumeLine(line, accumulator, onEvent, structured, capabilities) {
|
|
660
|
+
if (line.trim().length === 0 || accumulator.protocolError !== null) return;
|
|
661
|
+
try {
|
|
662
|
+
const frame = object(JSON.parse(line), "OpenCode stream frame");
|
|
663
|
+
const type = text(frame.type, "OpenCode stream frame type");
|
|
664
|
+
safeInteger(frame.timestamp, "OpenCode stream frame timestamp");
|
|
665
|
+
const sessionId = text(frame.sessionID, "OpenCode stream frame sessionID");
|
|
666
|
+
if (accumulator.sessionId === null) accumulator.sessionId = sessionId;
|
|
667
|
+
if (accumulator.sessionId !== sessionId) {
|
|
668
|
+
throw new TypeError("OpenCode stream changed session identity");
|
|
669
|
+
}
|
|
670
|
+
accumulator.frames.push(cloneFrozenJson(frame));
|
|
671
|
+
if (type === "error") {
|
|
672
|
+
if (accumulator.terminalError !== null) {
|
|
673
|
+
throw new TypeError("OpenCode emitted more than one terminal error");
|
|
674
|
+
}
|
|
675
|
+
accumulator.terminalError = object(frame.error, "OpenCode stream error");
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
if (type === "text") {
|
|
679
|
+
const part = consumePart(frame.part, "text", sessionId, accumulator);
|
|
680
|
+
if (part === null) return;
|
|
681
|
+
const value = typeof part.text === "string" ? part.text : (() => {
|
|
682
|
+
throw new TypeError("OpenCode text part text must be a string");
|
|
683
|
+
})();
|
|
684
|
+
const time = object(part.time, "OpenCode text part time");
|
|
685
|
+
safeInteger(time.end, "OpenCode text part end time");
|
|
686
|
+
accumulator.text.push({ text: value });
|
|
687
|
+
if (!structured || !value.includes(STRUCTURED_RESULT_MARKER)) {
|
|
688
|
+
onEvent({ type: "text", delta: value });
|
|
689
|
+
}
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
if (type === "reasoning") {
|
|
693
|
+
const part = consumePart(frame.part, "reasoning", sessionId, accumulator);
|
|
694
|
+
if (part === null) return;
|
|
695
|
+
if (typeof part.text !== "string") {
|
|
696
|
+
throw new TypeError("OpenCode reasoning part text must be a string");
|
|
697
|
+
}
|
|
698
|
+
onEvent({ type: "thinking", delta: part.text });
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
if (type === "tool_use") {
|
|
702
|
+
const part = consumePart(frame.part, "tool", sessionId, accumulator);
|
|
703
|
+
if (part === null) return;
|
|
704
|
+
const name = text(part.tool, "OpenCode tool name");
|
|
705
|
+
const capability = name === "write" || name === "apply_patch" ? "edit" : name;
|
|
706
|
+
if (!capabilities.includes(capability)) {
|
|
707
|
+
throw new TypeError(`OpenCode emitted undeclared tool ${name}`);
|
|
708
|
+
}
|
|
709
|
+
const state = object(part.state, "OpenCode tool state");
|
|
710
|
+
if (state.status !== "completed" && state.status !== "error") {
|
|
711
|
+
throw new TypeError("OpenCode tool terminal state must be completed or error");
|
|
712
|
+
}
|
|
713
|
+
onEvent({ type: "tool", name, phase: "use" });
|
|
714
|
+
onEvent({ type: "tool", name, phase: "result" });
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
if (type === "step_start") {
|
|
718
|
+
consumePart(frame.part, "step-start", sessionId, accumulator);
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
if (type === "step_finish") {
|
|
722
|
+
const part = consumePart(frame.part, "step-finish", sessionId, accumulator);
|
|
723
|
+
if (part !== null) {
|
|
724
|
+
const reason = text(part.reason, "OpenCode step reason");
|
|
725
|
+
if (!STEP_FINISH_REASONS.has(reason)) {
|
|
726
|
+
throw new TypeError(`OpenCode step reason ${reason} is invalid`);
|
|
727
|
+
}
|
|
728
|
+
accumulator.stopReason = reason;
|
|
729
|
+
consumeUsage(part, accumulator);
|
|
730
|
+
}
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
throw new TypeError(`OpenCode emitted unsupported event type ${type}`);
|
|
734
|
+
} catch (error) {
|
|
735
|
+
accumulator.protocolError = error instanceof Error ? error : new Error(String(error));
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
function resultParts(accumulator) {
|
|
739
|
+
return Object.freeze(accumulator.text.map(
|
|
740
|
+
(part, index, values) => Object.freeze({
|
|
741
|
+
kind: "assistant",
|
|
742
|
+
text: part.text,
|
|
743
|
+
final: index === values.length - 1
|
|
744
|
+
})
|
|
745
|
+
));
|
|
746
|
+
}
|
|
747
|
+
function usage(accumulator) {
|
|
748
|
+
if (!accumulator.usageValid || accumulator.stepCount === 0) {
|
|
749
|
+
return Object.freeze({ kind: "unknown" });
|
|
750
|
+
}
|
|
751
|
+
return reportedUsage({
|
|
752
|
+
inputTokens: accumulator.inputTokens,
|
|
753
|
+
outputTokens: accumulator.outputTokens,
|
|
754
|
+
...accumulator.cacheCreationInputTokens === 0 ? {} : {
|
|
755
|
+
cacheCreationInputTokens: accumulator.cacheCreationInputTokens
|
|
756
|
+
},
|
|
757
|
+
...accumulator.cacheReadInputTokens === 0 ? {} : { cacheReadInputTokens: accumulator.cacheReadInputTokens }
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
function errorMessage(raw) {
|
|
761
|
+
const data = typeof raw.data === "object" && raw.data !== null ? object(raw.data, "OpenCode error data") : Object.freeze({});
|
|
762
|
+
const detail = typeof data.message === "string" ? data.message : typeof data.responseBody === "string" ? data.responseBody : typeof raw.name === "string" ? raw.name : "OpenCode failed";
|
|
763
|
+
return detail;
|
|
764
|
+
}
|
|
765
|
+
function nativeFailure(raw) {
|
|
766
|
+
const name = typeof raw.name === "string" ? raw.name : "";
|
|
767
|
+
const data = typeof raw.data === "object" && raw.data !== null ? object(raw.data, "OpenCode error data") : Object.freeze({});
|
|
768
|
+
const status = Number.isSafeInteger(data.statusCode) ? data.statusCode : void 0;
|
|
769
|
+
const detail = errorMessage(raw);
|
|
770
|
+
const responseBody = typeof data.responseBody === "string" ? data.responseBody : "";
|
|
771
|
+
if (name === "ProviderAuthError") return "auth";
|
|
772
|
+
if (name === "APIError" && status === 401) {
|
|
773
|
+
if (/CreditsError|no payment|insufficient balance/iu.test(responseBody)) {
|
|
774
|
+
return "billing";
|
|
775
|
+
}
|
|
776
|
+
if (/MonthlyLimitError/iu.test(responseBody)) return "quota";
|
|
777
|
+
if (/UserLimitError/iu.test(responseBody)) return "rate-limit";
|
|
778
|
+
if (/ModelError/iu.test(responseBody)) return "model-unavailable";
|
|
779
|
+
}
|
|
780
|
+
if (status === 401) return "auth";
|
|
781
|
+
if (name === "MessageAbortedError") return "aborted";
|
|
782
|
+
if (status === 402) return "billing";
|
|
783
|
+
if (status === 404) return "model-unavailable";
|
|
784
|
+
if (status === 408) return "timeout";
|
|
785
|
+
if (status === 429) {
|
|
786
|
+
if (/credit balance|insufficient funds|out of credits|exhausted credit/iu.test(detail)) {
|
|
787
|
+
return "billing";
|
|
788
|
+
}
|
|
789
|
+
const classified = classifyEngineFailure(new Error(detail));
|
|
790
|
+
return classified === "billing" || classified === "quota" ? classified : "rate-limit";
|
|
791
|
+
}
|
|
792
|
+
if (status !== void 0 && status >= 500) return "transient";
|
|
793
|
+
return classifyEngineFailure(new Error(detail));
|
|
794
|
+
}
|
|
795
|
+
function header(raw, name) {
|
|
796
|
+
const data = typeof raw.data === "object" && raw.data !== null ? object(raw.data, "OpenCode error data") : void 0;
|
|
797
|
+
const headers = data !== void 0 && typeof data.responseHeaders === "object" && data.responseHeaders !== null ? object(data.responseHeaders, "OpenCode response headers") : void 0;
|
|
798
|
+
if (headers === void 0) return void 0;
|
|
799
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
800
|
+
if (key.toLowerCase() === name && typeof value === "string") return value;
|
|
801
|
+
}
|
|
802
|
+
return void 0;
|
|
803
|
+
}
|
|
804
|
+
function resetAtHeader(value) {
|
|
805
|
+
if (value === void 0) return void 0;
|
|
806
|
+
const trimmed = value.trim();
|
|
807
|
+
if (/^\d+$/u.test(trimmed)) {
|
|
808
|
+
const numeric = Number(trimmed);
|
|
809
|
+
if (!Number.isSafeInteger(numeric)) return void 0;
|
|
810
|
+
const milliseconds = numeric < 1e10 ? numeric * 1e3 : numeric;
|
|
811
|
+
return Number.isSafeInteger(milliseconds) ? milliseconds : void 0;
|
|
812
|
+
}
|
|
813
|
+
const parsed = Date.parse(trimmed);
|
|
814
|
+
return Number.isNaN(parsed) ? void 0 : parsed;
|
|
815
|
+
}
|
|
816
|
+
function nativeLimitHints(raw) {
|
|
817
|
+
if (raw === null) return Object.freeze({});
|
|
818
|
+
const retryAfterMs = retryAfterHeaderToMs(header(raw, "retry-after"));
|
|
819
|
+
const resetAt = resetAtHeader(
|
|
820
|
+
header(raw, "x-ratelimit-reset") ?? header(raw, "x-rate-limit-reset")
|
|
821
|
+
);
|
|
822
|
+
return Object.freeze({
|
|
823
|
+
...retryAfterMs === void 0 ? {} : { retryAfterMs },
|
|
824
|
+
...resetAt === void 0 ? {} : { resetAt }
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
function loopError(kind, message, hints = {}) {
|
|
828
|
+
return new EngineError({ kind, message, ...hints });
|
|
829
|
+
}
|
|
830
|
+
function typedFailureMessage(kind, detail) {
|
|
831
|
+
const label = kind === "auth" ? "authentication failed" : kind === "billing" ? "payment required" : kind === "model-unavailable" ? "model unavailable" : kind === "rate-limit" ? "rate limit reached" : kind === "quota" ? "quota reached" : kind === "transient" ? "service unavailable" : kind === "timeout" ? "timed out" : kind === "aborted" ? "aborted" : kind === "invalid-config" ? "invalid configuration" : "failed";
|
|
832
|
+
return `OpenCode ${label}: ${detail}`;
|
|
833
|
+
}
|
|
834
|
+
function transportFailure(kind, message, exitCode) {
|
|
835
|
+
return Object.freeze({ kind, message, exitCode });
|
|
836
|
+
}
|
|
837
|
+
var OpenCodeCliEngine = class {
|
|
838
|
+
name = "opencode-cli";
|
|
839
|
+
#executable;
|
|
840
|
+
#version;
|
|
841
|
+
#identity;
|
|
842
|
+
#environment;
|
|
843
|
+
#auth;
|
|
844
|
+
#authRedactions;
|
|
845
|
+
#options;
|
|
846
|
+
#versionObservation;
|
|
847
|
+
constructor(options) {
|
|
848
|
+
if (typeof options.executable !== "string" || !isAbsolute(options.executable)) {
|
|
849
|
+
throw new TypeError("OpenCode executable must be an absolute path");
|
|
850
|
+
}
|
|
851
|
+
this.#executable = text(options.executable, "OpenCode executable");
|
|
852
|
+
if (options.version !== SUPPORTED_VERSION) {
|
|
853
|
+
throw new TypeError(`OpenCode CLI version must be ${SUPPORTED_VERSION}`);
|
|
854
|
+
}
|
|
855
|
+
this.#version = options.version;
|
|
856
|
+
const checked = engineSelection({
|
|
857
|
+
adapter: "opencode-cli",
|
|
858
|
+
adapterVersion: this.#version,
|
|
859
|
+
provider: nullableText(options.identity.provider, "OpenCode provider"),
|
|
860
|
+
modelFamily: nullableText(
|
|
861
|
+
options.identity.modelFamily,
|
|
862
|
+
"OpenCode model family"
|
|
863
|
+
)
|
|
864
|
+
});
|
|
865
|
+
this.#identity = Object.freeze({
|
|
866
|
+
provider: checked.provider,
|
|
867
|
+
modelFamily: checked.modelFamily
|
|
868
|
+
});
|
|
869
|
+
this.#environment = selectedEnvironment(options.environment);
|
|
870
|
+
this.#auth = authValue(options.auth);
|
|
871
|
+
this.#authRedactions = authRedactions(this.#auth);
|
|
872
|
+
const managedConfigDirectories = Object.freeze([
|
|
873
|
+
...options.managedConfigDirectories ?? []
|
|
874
|
+
]);
|
|
875
|
+
this.#options = Object.freeze({
|
|
876
|
+
executable: this.#executable,
|
|
877
|
+
version: this.#version,
|
|
878
|
+
identity: this.#identity,
|
|
879
|
+
...Object.keys(this.#environment).length === 0 ? {} : { environment: this.#environment },
|
|
880
|
+
...Object.keys(this.#auth).length === 0 ? {} : { auth: this.#auth },
|
|
881
|
+
...managedConfigDirectories.length === 0 ? {} : { managedConfigDirectories }
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
async admit(request, signal, expectedSelection) {
|
|
885
|
+
if (signal.aborted) throw loopError("aborted", "OpenCode admission was aborted before start");
|
|
886
|
+
let normalized;
|
|
887
|
+
let selected;
|
|
888
|
+
try {
|
|
889
|
+
assertNoManagedConfig(managedConfigSources(this.#options.managedConfigDirectories));
|
|
890
|
+
const selectedModel = model(request.model);
|
|
891
|
+
const selectedProvider = providerForModel(selectedModel, this.#identity);
|
|
892
|
+
const selectedFamily = familyForModel(selectedModel, this.#identity);
|
|
893
|
+
const capabilities = requestedCapabilities({ ...request, prompt: "" });
|
|
894
|
+
if (typeof request.cwd !== "string" || !isAbsolute(request.cwd)) {
|
|
895
|
+
throw new TypeError("OpenCode request cwd must be an absolute path");
|
|
896
|
+
}
|
|
897
|
+
const cwd = realpathSync(request.cwd);
|
|
898
|
+
assertNoProjectInstructions(cwd, capabilities);
|
|
899
|
+
normalized = { ...request, cwd, prompt: "" };
|
|
900
|
+
const timeout = request.timeoutMs ?? DEFAULT_OWNED_COMMAND_LIMITS.timeoutMs;
|
|
901
|
+
const grace = request.timeoutGraceMs ?? DEFAULT_OWNED_COMMAND_LIMITS.teardownGraceMs;
|
|
902
|
+
const output = request.maxOutputBytes ?? DEFAULT_OWNED_COMMAND_LIMITS.maxOutputBytes;
|
|
903
|
+
const memory = request.maxMemoryBytes ?? DEFAULT_OWNED_COMMAND_LIMITS.maxMemoryBytes;
|
|
904
|
+
if (!Number.isSafeInteger(timeout) || timeout < 1 || timeout > 2147483647 || !Number.isSafeInteger(grace) || grace < 0 || grace > 2147483647 - timeout || !Number.isSafeInteger(output) || output < 0 || !Number.isSafeInteger(memory) || memory < 1) {
|
|
905
|
+
throw new TypeError("OpenCode request command limits are invalid");
|
|
906
|
+
}
|
|
907
|
+
ownedCommandIdentity({
|
|
908
|
+
adapter: "opencode-cli",
|
|
909
|
+
runId: request.attempt?.runId,
|
|
910
|
+
leafId: request.attempt?.leafId,
|
|
911
|
+
attemptId: request.attempt?.attemptId
|
|
912
|
+
});
|
|
913
|
+
selected = engineSelection({
|
|
914
|
+
adapter: "opencode-cli",
|
|
915
|
+
adapterVersion: this.#version,
|
|
916
|
+
provider: selectedProvider,
|
|
917
|
+
modelFamily: selectedFamily,
|
|
918
|
+
model: selectedModel.value,
|
|
919
|
+
executable: this.#executable,
|
|
920
|
+
capabilities
|
|
921
|
+
});
|
|
922
|
+
if (expectedSelection !== void 0 && !isDeepStrictEqual(engineSelection(expectedSelection), selected)) {
|
|
923
|
+
throw new TypeError("OpenCode expected selection does not match its configured path and request");
|
|
924
|
+
}
|
|
925
|
+
let validationDirectory;
|
|
926
|
+
try {
|
|
927
|
+
validationDirectory = mkdtempSync(join(tmpdir(), "lines-opencode-admission-"));
|
|
928
|
+
} catch {
|
|
929
|
+
throw loopError(
|
|
930
|
+
"unknown",
|
|
931
|
+
"OpenCode admission temporary configuration could not be created"
|
|
932
|
+
);
|
|
933
|
+
}
|
|
934
|
+
let validationFailed = false;
|
|
935
|
+
let validationFailure;
|
|
936
|
+
try {
|
|
937
|
+
buildOpenCodeInvocation(normalized, this.#options, validationDirectory);
|
|
938
|
+
} catch (error) {
|
|
939
|
+
validationFailed = true;
|
|
940
|
+
validationFailure = error instanceof TypeError || error instanceof EngineError ? error : loopError(
|
|
941
|
+
"unknown",
|
|
942
|
+
"OpenCode admission temporary configuration could not be prepared"
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
try {
|
|
946
|
+
rmSync(validationDirectory, { recursive: true, force: true });
|
|
947
|
+
} catch {
|
|
948
|
+
if (!validationFailed) {
|
|
949
|
+
throw loopError(
|
|
950
|
+
"unknown",
|
|
951
|
+
"OpenCode admission temporary configuration could not be removed"
|
|
952
|
+
);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
if (validationFailed) throw validationFailure;
|
|
956
|
+
} catch (error) {
|
|
957
|
+
if (error instanceof EngineError) throw error;
|
|
958
|
+
const diagnostic = error instanceof Error ? error.message : "invalid request";
|
|
959
|
+
throw loopError("invalid-config", scrubCapture(
|
|
960
|
+
scrubExactValues(diagnostic, { ...this.#authRedactions, ...this.#environment }),
|
|
961
|
+
{ ...this.#environment },
|
|
962
|
+
700
|
|
963
|
+
));
|
|
964
|
+
}
|
|
965
|
+
try {
|
|
966
|
+
resolveCommandExecutable(this.#executable);
|
|
967
|
+
} catch {
|
|
968
|
+
throw loopError("missing-cli", "OpenCode configured executable is missing or not runnable");
|
|
969
|
+
}
|
|
970
|
+
const version = await (this.#versionObservation ??= this.#observeVersion(normalized, signal).catch((error) => {
|
|
971
|
+
this.#versionObservation = void 0;
|
|
972
|
+
throw error;
|
|
973
|
+
}));
|
|
974
|
+
if (signal.aborted) throw loopError("aborted", "OpenCode admission was aborted");
|
|
975
|
+
return engineSelection({ ...selected, adapterVersion: version });
|
|
976
|
+
}
|
|
977
|
+
async #observeVersion(request, signal) {
|
|
978
|
+
let directory;
|
|
979
|
+
try {
|
|
980
|
+
directory = mkdtempSync(join(tmpdir(), "lines-opencode-version-"));
|
|
981
|
+
} catch {
|
|
982
|
+
throw loopError(
|
|
983
|
+
"unknown",
|
|
984
|
+
"OpenCode version temporary configuration could not be created"
|
|
985
|
+
);
|
|
986
|
+
}
|
|
987
|
+
let version;
|
|
988
|
+
let versionFailed = false;
|
|
989
|
+
let versionFailure;
|
|
990
|
+
try {
|
|
991
|
+
const invocation = buildOpenCodeInvocation(request, this.#options, directory);
|
|
992
|
+
assertNoManagedConfig(managedConfigSources(this.#options.managedConfigDirectories));
|
|
993
|
+
const command = await runOwnedCommand({
|
|
994
|
+
executable: this.#executable,
|
|
995
|
+
args: ["--version"],
|
|
996
|
+
cwd: request.cwd,
|
|
997
|
+
env: invocation.environment,
|
|
998
|
+
inheritParentEnv: false,
|
|
999
|
+
stdin: "",
|
|
1000
|
+
...ownedCommandIdentity({
|
|
1001
|
+
adapter: "opencode-cli",
|
|
1002
|
+
runId: request.attempt?.runId,
|
|
1003
|
+
leafId: request.attempt?.leafId,
|
|
1004
|
+
attemptId: request.attempt?.attemptId
|
|
1005
|
+
}),
|
|
1006
|
+
timeoutMs: Math.min(request.timeoutMs ?? VERSION_TIMEOUT_MS, VERSION_TIMEOUT_MS),
|
|
1007
|
+
teardownGraceMs: Math.min(request.timeoutGraceMs ?? VERSION_TEARDOWN_MS, VERSION_TEARDOWN_MS),
|
|
1008
|
+
maxOutputBytes: Math.min(request.maxOutputBytes ?? VERSION_OUTPUT_BYTES, VERSION_OUTPUT_BYTES),
|
|
1009
|
+
maxMemoryBytes: request.maxMemoryBytes ?? DEFAULT_OWNED_COMMAND_LIMITS.maxMemoryBytes
|
|
1010
|
+
}, signal);
|
|
1011
|
+
if (command.aborted || signal.aborted) throw loopError("aborted", "OpenCode version check was aborted");
|
|
1012
|
+
if (command.timedOut) throw loopError("timeout", "OpenCode version check timed out");
|
|
1013
|
+
if (command.exitCode !== 0) throw loopError("invalid-config", "OpenCode version command did not succeed");
|
|
1014
|
+
const observed = new TextDecoder().decode(command.stdout).trim();
|
|
1015
|
+
if (!OPENCODE_VERSION.test(observed)) {
|
|
1016
|
+
throw loopError("invalid-config", "OpenCode returned an unsupported version format");
|
|
1017
|
+
}
|
|
1018
|
+
if (observed !== SUPPORTED_VERSION) {
|
|
1019
|
+
throw loopError("invalid-config", "OpenCode observed version does not match its supported version");
|
|
1020
|
+
}
|
|
1021
|
+
version = observed;
|
|
1022
|
+
} catch (error) {
|
|
1023
|
+
versionFailed = true;
|
|
1024
|
+
versionFailure = error;
|
|
1025
|
+
}
|
|
1026
|
+
try {
|
|
1027
|
+
rmSync(directory, { recursive: true, force: true });
|
|
1028
|
+
} catch {
|
|
1029
|
+
if (!versionFailed) {
|
|
1030
|
+
throw loopError(
|
|
1031
|
+
"unknown",
|
|
1032
|
+
"OpenCode version temporary configuration could not be removed"
|
|
1033
|
+
);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
if (versionFailed) {
|
|
1037
|
+
const error = versionFailure;
|
|
1038
|
+
if (error instanceof EngineError) throw error;
|
|
1039
|
+
if (signal.aborted) throw loopError("aborted", "OpenCode version check was aborted");
|
|
1040
|
+
if (error instanceof OwnedCommandError) {
|
|
1041
|
+
if (error.code === "INVALID_EXECUTABLE") {
|
|
1042
|
+
throw loopError("missing-cli", "OpenCode configured executable could not start");
|
|
1043
|
+
}
|
|
1044
|
+
if (error.code === "SPAWN_FAILED") {
|
|
1045
|
+
try {
|
|
1046
|
+
resolveCommandExecutable(this.#executable);
|
|
1047
|
+
} catch {
|
|
1048
|
+
throw loopError("missing-cli", "OpenCode configured executable is missing or not runnable");
|
|
1049
|
+
}
|
|
1050
|
+
throw loopError("unknown", "OpenCode version process could not start");
|
|
1051
|
+
}
|
|
1052
|
+
if (error.code === "OUTPUT_LIMIT" || error.code === "INVALID_COMMAND") {
|
|
1053
|
+
throw loopError("invalid-config", "OpenCode version command exceeded or rejected its limits");
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
if (error instanceof TypeError) {
|
|
1057
|
+
throw loopError("invalid-config", "OpenCode version configuration is invalid");
|
|
1058
|
+
}
|
|
1059
|
+
throw loopError("unknown", "OpenCode version check could not complete");
|
|
1060
|
+
}
|
|
1061
|
+
return version;
|
|
1062
|
+
}
|
|
1063
|
+
async run(request, onEvent, signal) {
|
|
1064
|
+
if (signal.aborted) {
|
|
1065
|
+
throw loopError("aborted", "OpenCode attempt was aborted before start");
|
|
1066
|
+
}
|
|
1067
|
+
const { prompt: _prompt, ...staticRequest } = request;
|
|
1068
|
+
const requested = await this.admit(staticRequest, signal);
|
|
1069
|
+
const capabilities = requested.capabilities;
|
|
1070
|
+
if (typeof request.cwd !== "string" || !isAbsolute(request.cwd)) {
|
|
1071
|
+
throw new TypeError("OpenCode request cwd must be an absolute path");
|
|
1072
|
+
}
|
|
1073
|
+
const cwd = realpathSync(request.cwd);
|
|
1074
|
+
assertNoProjectInstructions(cwd, capabilities);
|
|
1075
|
+
const normalized = { ...request, cwd };
|
|
1076
|
+
const directory = mkdtempSync(join(tmpdir(), "lines-opencode-"));
|
|
1077
|
+
const accumulator = {
|
|
1078
|
+
frames: [],
|
|
1079
|
+
seenParts: /* @__PURE__ */ new Map(),
|
|
1080
|
+
text: [],
|
|
1081
|
+
sessionId: null,
|
|
1082
|
+
protocolError: null,
|
|
1083
|
+
terminalError: null,
|
|
1084
|
+
stopReason: null,
|
|
1085
|
+
stepCount: 0,
|
|
1086
|
+
usageValid: true,
|
|
1087
|
+
inputTokens: 0,
|
|
1088
|
+
outputTokens: 0,
|
|
1089
|
+
cacheCreationInputTokens: 0,
|
|
1090
|
+
cacheReadInputTokens: 0
|
|
1091
|
+
};
|
|
1092
|
+
const decoder = new TextDecoder();
|
|
1093
|
+
let buffer = "";
|
|
1094
|
+
const parserAbort = new AbortController();
|
|
1095
|
+
const commandSignal = AbortSignal.any([signal, parserAbort.signal]);
|
|
1096
|
+
const structured = request.jsonSchema !== void 0;
|
|
1097
|
+
const scrub = (value) => scrubCapture(
|
|
1098
|
+
scrubExactValues(value, {
|
|
1099
|
+
...this.#authRedactions,
|
|
1100
|
+
...this.#environment
|
|
1101
|
+
}),
|
|
1102
|
+
{ ...this.#environment },
|
|
1103
|
+
700
|
|
1104
|
+
);
|
|
1105
|
+
const owner = ownedCommandIdentity({
|
|
1106
|
+
adapter: "opencode-cli",
|
|
1107
|
+
runId: request.attempt?.runId,
|
|
1108
|
+
leafId: request.attempt?.leafId,
|
|
1109
|
+
attemptId: request.attempt?.attemptId
|
|
1110
|
+
});
|
|
1111
|
+
const startedAt = Date.now();
|
|
1112
|
+
try {
|
|
1113
|
+
const invocation = buildOpenCodeInvocation(
|
|
1114
|
+
normalized,
|
|
1115
|
+
this.#options,
|
|
1116
|
+
directory
|
|
1117
|
+
);
|
|
1118
|
+
assertNoManagedConfig(managedConfigSources(this.#options.managedConfigDirectories));
|
|
1119
|
+
const command = await runOwnedCommand({
|
|
1120
|
+
executable: this.#executable,
|
|
1121
|
+
args: invocation.args,
|
|
1122
|
+
cwd,
|
|
1123
|
+
env: invocation.environment,
|
|
1124
|
+
inheritParentEnv: false,
|
|
1125
|
+
stdin: invocation.stdin,
|
|
1126
|
+
...owner,
|
|
1127
|
+
...DEFAULT_OWNED_COMMAND_LIMITS,
|
|
1128
|
+
timeoutMs: request.timeoutMs ?? DEFAULT_OWNED_COMMAND_LIMITS.timeoutMs,
|
|
1129
|
+
teardownGraceMs: request.timeoutGraceMs ?? DEFAULT_OWNED_COMMAND_LIMITS.teardownGraceMs,
|
|
1130
|
+
maxOutputBytes: request.maxOutputBytes ?? DEFAULT_OWNED_COMMAND_LIMITS.maxOutputBytes,
|
|
1131
|
+
maxMemoryBytes: request.maxMemoryBytes ?? DEFAULT_OWNED_COMMAND_LIMITS.maxMemoryBytes
|
|
1132
|
+
}, commandSignal, {
|
|
1133
|
+
onStdout(chunk) {
|
|
1134
|
+
buffer += decoder.decode(chunk, { stream: true });
|
|
1135
|
+
let newline;
|
|
1136
|
+
while ((newline = buffer.indexOf("\n")) >= 0) {
|
|
1137
|
+
consumeLine(
|
|
1138
|
+
buffer.slice(0, newline),
|
|
1139
|
+
accumulator,
|
|
1140
|
+
onEvent,
|
|
1141
|
+
structured,
|
|
1142
|
+
capabilities
|
|
1143
|
+
);
|
|
1144
|
+
buffer = buffer.slice(newline + 1);
|
|
1145
|
+
if (accumulator.protocolError !== null) {
|
|
1146
|
+
parserAbort.abort();
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
});
|
|
1152
|
+
buffer += decoder.decode();
|
|
1153
|
+
if (buffer.trim().length > 0) {
|
|
1154
|
+
consumeLine(buffer, accumulator, onEvent, structured, capabilities);
|
|
1155
|
+
}
|
|
1156
|
+
if (accumulator.protocolError !== null) {
|
|
1157
|
+
throw loopError(
|
|
1158
|
+
"invalid-config",
|
|
1159
|
+
`OpenCode returned an invalid JSON protocol: ${scrub(accumulator.protocolError.message)}`
|
|
1160
|
+
);
|
|
1161
|
+
}
|
|
1162
|
+
const aborted = command.aborted || signal.aborted;
|
|
1163
|
+
if (aborted && accumulator.stopReason !== "stop") {
|
|
1164
|
+
throw loopError("aborted", "OpenCode attempt was aborted");
|
|
1165
|
+
}
|
|
1166
|
+
const stderr = scrub(new TextDecoder().decode(command.stderr));
|
|
1167
|
+
const nativeKind = accumulator.terminalError === null ? void 0 : nativeFailure(accumulator.terminalError);
|
|
1168
|
+
const nativeMessage = accumulator.terminalError === null ? "" : scrub(errorMessage(accumulator.terminalError));
|
|
1169
|
+
const failed = aborted || command.timedOut || command.exitCode !== 0 || accumulator.terminalError !== null;
|
|
1170
|
+
const parts = resultParts(accumulator);
|
|
1171
|
+
if (accumulator.stopReason === "error" || accumulator.stopReason === "content-filter") {
|
|
1172
|
+
throw loopError(
|
|
1173
|
+
"unknown",
|
|
1174
|
+
`OpenCode ended with ${accumulator.stopReason}`
|
|
1175
|
+
);
|
|
1176
|
+
}
|
|
1177
|
+
if (accumulator.stopReason === "length") {
|
|
1178
|
+
const measuredUsage2 = usage(accumulator);
|
|
1179
|
+
const effective2 = requested;
|
|
1180
|
+
onEvent({
|
|
1181
|
+
type: "usage",
|
|
1182
|
+
usage: measuredUsage2,
|
|
1183
|
+
model: effective2.model ?? "unknown"
|
|
1184
|
+
});
|
|
1185
|
+
throw new EngineIncompleteResultError(
|
|
1186
|
+
"OpenCode output ended at the token limit",
|
|
1187
|
+
validateIncompleteResultEvidence({
|
|
1188
|
+
parts,
|
|
1189
|
+
usage: measuredUsage2,
|
|
1190
|
+
requested,
|
|
1191
|
+
effective: effective2,
|
|
1192
|
+
stopReason: "length",
|
|
1193
|
+
...failed ? {
|
|
1194
|
+
transportFailure: transportFailure(
|
|
1195
|
+
command.timedOut ? "timeout" : nativeKind ?? classifyEngineFailure(new Error(stderr)),
|
|
1196
|
+
nativeMessage || stderr || "OpenCode transport failed",
|
|
1197
|
+
command.exitCode
|
|
1198
|
+
)
|
|
1199
|
+
} : {},
|
|
1200
|
+
raw: cloneFrozenJson(accumulator.frames)
|
|
1201
|
+
})
|
|
1202
|
+
);
|
|
1203
|
+
}
|
|
1204
|
+
const markedFinal = accumulator.stopReason === "stop";
|
|
1205
|
+
if (!markedFinal) {
|
|
1206
|
+
if (!failed) {
|
|
1207
|
+
throw loopError(
|
|
1208
|
+
"invalid-config",
|
|
1209
|
+
"OpenCode completed without a final step"
|
|
1210
|
+
);
|
|
1211
|
+
}
|
|
1212
|
+
const kind = command.timedOut ? "timeout" : nativeKind ?? classifyEngineFailure(new Error(stderr));
|
|
1213
|
+
throw loopError(
|
|
1214
|
+
kind,
|
|
1215
|
+
typedFailureMessage(
|
|
1216
|
+
kind,
|
|
1217
|
+
nativeMessage || stderr || "incomplete result"
|
|
1218
|
+
),
|
|
1219
|
+
nativeLimitHints(accumulator.terminalError)
|
|
1220
|
+
);
|
|
1221
|
+
}
|
|
1222
|
+
if (parts.length === 0) {
|
|
1223
|
+
if (failed) {
|
|
1224
|
+
const kind = command.timedOut ? "timeout" : nativeKind ?? classifyEngineFailure(new Error(stderr));
|
|
1225
|
+
throw loopError(
|
|
1226
|
+
kind,
|
|
1227
|
+
typedFailureMessage(
|
|
1228
|
+
kind,
|
|
1229
|
+
nativeMessage || stderr || "OpenCode produced no result text"
|
|
1230
|
+
),
|
|
1231
|
+
nativeLimitHints(accumulator.terminalError)
|
|
1232
|
+
);
|
|
1233
|
+
}
|
|
1234
|
+
const measuredUsage2 = usage(accumulator);
|
|
1235
|
+
const effective2 = requested;
|
|
1236
|
+
onEvent({
|
|
1237
|
+
type: "usage",
|
|
1238
|
+
usage: measuredUsage2,
|
|
1239
|
+
model: effective2.model ?? "unknown"
|
|
1240
|
+
});
|
|
1241
|
+
throw new EngineIncompleteResultError(
|
|
1242
|
+
"OpenCode completed without a final text part",
|
|
1243
|
+
validateIncompleteResultEvidence({
|
|
1244
|
+
parts,
|
|
1245
|
+
usage: measuredUsage2,
|
|
1246
|
+
requested,
|
|
1247
|
+
effective: effective2,
|
|
1248
|
+
stopReason: "stop",
|
|
1249
|
+
raw: cloneFrozenJson(accumulator.frames)
|
|
1250
|
+
})
|
|
1251
|
+
);
|
|
1252
|
+
}
|
|
1253
|
+
const measuredUsage = usage(accumulator);
|
|
1254
|
+
const effective = requested;
|
|
1255
|
+
onEvent({
|
|
1256
|
+
type: "usage",
|
|
1257
|
+
usage: measuredUsage,
|
|
1258
|
+
model: effective.model ?? "unknown"
|
|
1259
|
+
});
|
|
1260
|
+
const late = request.timeoutMs !== void 0 && Date.now() - startedAt > request.timeoutMs;
|
|
1261
|
+
const failureKind = aborted ? "aborted" : command.timedOut ? "timeout" : nativeKind ?? classifyEngineFailure(new Error(stderr));
|
|
1262
|
+
const transportMessage = nativeMessage || stderr || "OpenCode transport failed after its final result";
|
|
1263
|
+
return validateAgentResult({
|
|
1264
|
+
parts,
|
|
1265
|
+
usage: measuredUsage,
|
|
1266
|
+
requested,
|
|
1267
|
+
effective,
|
|
1268
|
+
...accumulator.stopReason === null ? {} : { stopReason: accumulator.stopReason },
|
|
1269
|
+
...failed ? {
|
|
1270
|
+
transportFailure: transportFailure(
|
|
1271
|
+
failureKind,
|
|
1272
|
+
transportMessage,
|
|
1273
|
+
command.exitCode
|
|
1274
|
+
)
|
|
1275
|
+
} : late ? {
|
|
1276
|
+
transportFailure: transportFailure(
|
|
1277
|
+
"timeout",
|
|
1278
|
+
"OpenCode result arrived after the soft timeout",
|
|
1279
|
+
command.exitCode
|
|
1280
|
+
)
|
|
1281
|
+
} : {},
|
|
1282
|
+
raw: cloneFrozenJson(accumulator.frames)
|
|
1283
|
+
});
|
|
1284
|
+
} catch (error) {
|
|
1285
|
+
if (error instanceof OwnedCommandError && error.code === "INVALID_EXECUTABLE") {
|
|
1286
|
+
throw loopError("missing-cli", "OpenCode configured executable could not start");
|
|
1287
|
+
}
|
|
1288
|
+
if (error instanceof OwnedCommandError && error.code === "SPAWN_FAILED") {
|
|
1289
|
+
try {
|
|
1290
|
+
resolveCommandExecutable(this.#executable);
|
|
1291
|
+
} catch {
|
|
1292
|
+
throw loopError("missing-cli", "OpenCode configured executable is missing or not runnable");
|
|
1293
|
+
}
|
|
1294
|
+
throw loopError("unknown", "OpenCode model process could not start");
|
|
1295
|
+
}
|
|
1296
|
+
throw error;
|
|
1297
|
+
} finally {
|
|
1298
|
+
rmSync(directory, { recursive: true, force: true });
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
};
|
|
1302
|
+
|
|
1303
|
+
export { OpenCodeCliEngine, buildOpenCodeInvocation, opencode };
|
|
1304
|
+
//# sourceMappingURL=index.js.map
|
|
1305
|
+
//# sourceMappingURL=index.js.map
|