@jskit-ai/assistant-core 0.1.199 → 0.1.201

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.
Files changed (164) hide show
  1. package/examples/conversation/README.md +8 -1
  2. package/examples/conversation/package.json +5 -5
  3. package/fixtures/responsive-assistant/App.vue +4 -1
  4. package/package.json +99 -36
  5. package/src/client/conversation/AssistantConversationElement.vue +27 -4
  6. package/src/client/conversation/AssistantConversationStatus.vue +24 -0
  7. package/src/client/conversation/AssistantGoalControl.vue +8 -5
  8. package/src/client/conversation/AssistantTranscript.vue +10 -3
  9. package/src/client/conversation/index.js +2 -0
  10. package/src/client/conversation/messageDelivery.js +115 -32
  11. package/src/client/conversation/retainedConversation.js +32 -0
  12. package/src/client/conversation/useAssistantQuestions.js +76 -0
  13. package/src/client/lib/assistantApi.js +72 -2
  14. package/src/server/conversation/agentRun.js +222 -0
  15. package/src/server/conversation/attachments.js +93 -0
  16. package/src/server/conversation/claudeClient.js +221 -0
  17. package/src/server/conversation/claudeHistory.js +256 -0
  18. package/src/server/conversation/claudeProcess.js +327 -0
  19. package/src/server/conversation/claudeTools.js +42 -0
  20. package/src/server/conversation/claudeTurn.js +1082 -0
  21. package/src/server/conversation/codexConfiguration.js +1183 -0
  22. package/src/server/conversation/codexConversations.js +1082 -0
  23. package/src/server/conversation/codexDetachedTurn.js +332 -0
  24. package/src/server/conversation/codexEvents.js +194 -1
  25. package/src/server/conversation/codexGoalCommands.js +124 -0
  26. package/src/server/conversation/codexHelperExecution.js +306 -0
  27. package/src/server/conversation/codexHelperThreadLedger.js +804 -0
  28. package/src/server/conversation/codexHelperThreadRecovery.js +395 -0
  29. package/src/server/conversation/codexHelperThreads.js +638 -0
  30. package/src/server/conversation/codexHistoryAdapter.js +369 -0
  31. package/src/server/conversation/codexMessageCommands.js +663 -0
  32. package/src/server/conversation/codexMessageDelivery.js +211 -0
  33. package/src/server/conversation/codexNativeHistoryExport.js +95 -0
  34. package/src/server/conversation/codexNotificationObserver.js +482 -0
  35. package/src/server/conversation/codexNotificationQueue.js +103 -0
  36. package/src/server/conversation/codexObservationControl.js +174 -0
  37. package/src/server/conversation/codexOutput.js +931 -0
  38. package/src/server/conversation/codexProcess.js +1559 -0
  39. package/src/server/conversation/codexProcessLeader.js +14 -0
  40. package/src/server/conversation/codexProvider.js +2710 -0
  41. package/src/server/conversation/codexProviderOwner.js +1058 -0
  42. package/src/server/conversation/codexProviderSelection.js +218 -0
  43. package/src/server/conversation/codexReasoning.js +314 -0
  44. package/src/server/conversation/codexRenewalCommands.js +290 -0
  45. package/src/server/conversation/codexResultSettlement.js +409 -0
  46. package/src/server/conversation/codexRunJournal.js +658 -0
  47. package/src/server/conversation/codexRuntimeLifecycle.js +288 -0
  48. package/src/server/conversation/codexSessionCleanup.js +96 -0
  49. package/src/server/conversation/codexSessionReconciliation.js +284 -0
  50. package/src/server/conversation/codexThreadReadiness.js +397 -0
  51. package/src/server/conversation/codexThreadStatus.js +354 -0
  52. package/src/server/conversation/codexTurn.js +701 -255
  53. package/src/server/conversation/codexTurnControl.js +317 -0
  54. package/src/server/conversation/codexTurnRecovery.js +990 -0
  55. package/src/server/conversation/codexTurnState.js +549 -0
  56. package/src/server/conversation/commandHook.js +18 -0
  57. package/src/server/conversation/commandWrapper.js +37 -0
  58. package/src/server/conversation/configuration.js +43 -0
  59. package/src/server/conversation/continuity.js +289 -0
  60. package/src/server/conversation/fileStorage.js +55 -0
  61. package/src/server/conversation/hookBridge.js +19 -0
  62. package/src/server/conversation/index.js +13 -0
  63. package/src/server/conversation/localExecution.js +665 -0
  64. package/src/server/conversation/memoryStorage.js +7 -72
  65. package/src/server/conversation/nativeHistoryExport.js +90 -0
  66. package/src/server/conversation/nativeStorage.js +23 -0
  67. package/src/server/conversation/openCodeClient.js +209 -19
  68. package/src/server/conversation/openCodePlugin.js +84 -0
  69. package/src/server/conversation/openCodeProcess.js +476 -0
  70. package/src/server/conversation/openCodeRuntime.js +1578 -0
  71. package/src/server/conversation/openCodeTools.js +60 -0
  72. package/src/server/conversation/openCodeTurn.js +557 -0
  73. package/src/server/conversation/provider.js +30 -0
  74. package/src/server/conversation/providers/api.js +136 -0
  75. package/src/server/conversation/providers/claude.js +63 -0
  76. package/src/server/conversation/providers/claudeDriver.js +425 -0
  77. package/src/server/conversation/providers/codex.js +248 -0
  78. package/src/server/conversation/providers/codexDriver.js +1078 -0
  79. package/src/server/conversation/providers/opencode.js +49 -0
  80. package/src/server/conversation/providers/opencodeDriver.js +650 -0
  81. package/src/server/conversation/providers/scoped.js +312 -0
  82. package/src/server/conversation/runtime.js +2220 -0
  83. package/src/server/conversation/runtimeStateUpgrade.js +125 -0
  84. package/src/server/conversation/storage.js +184 -0
  85. package/src/server/conversation/streams.js +7 -2
  86. package/src/server/conversation/structuredOutput.js +240 -0
  87. package/src/server/conversation/systemPrompt.js +54 -0
  88. package/src/server/conversation/tools.js +84 -0
  89. package/src/server/conversation/transcript.js +5 -4
  90. package/src/server/hosts/claudeHistory.js +5 -0
  91. package/src/server/hosts/claudeProcess.js +11 -0
  92. package/src/server/hosts/claudeTurn.js +4 -0
  93. package/src/server/hosts/codexConfiguration.js +20 -0
  94. package/src/server/hosts/codexEvents.js +13 -0
  95. package/src/server/hosts/codexProcess.js +12 -0
  96. package/src/server/hosts/codexProvider.js +23 -0
  97. package/src/server/hosts/codexTurn.js +20 -0
  98. package/src/server/hosts/nativeHistory.js +3 -0
  99. package/src/server/hosts/openCodeClient.js +3 -0
  100. package/src/server/hosts/openCodeProcess.js +23 -0
  101. package/src/server/hosts/openCodeTurn.js +7 -0
  102. package/src/server/index.js +2 -0
  103. package/src/server/lib/aiConnectionClient.js +13 -6
  104. package/src/server/lib/assistantCompletion.js +190 -0
  105. package/src/server/lib/assistantToolLoop.js +314 -0
  106. package/src/server/lib/serviceToolCatalog.js +25 -10
  107. package/src/shared/assistantResponseText.js +29 -0
  108. package/src/shared/conversation/attachments.js +19 -0
  109. package/src/shared/conversation/index.js +3 -0
  110. package/src/shared/conversation/pagination.js +78 -0
  111. package/src/shared/conversation/patches.js +150 -0
  112. package/src/shared/conversation/questions.js +48 -0
  113. package/src/shared/conversation/streaming.js +7 -4
  114. package/src/shared/index.js +2 -0
  115. package/src/shared/nativeProviders.js +45 -0
  116. package/src/testing/conversationStorageContract.js +31 -1
  117. package/src/testing/nativeCodex.js +11 -0
  118. package/src/testing/nativeOpenCode.js +3 -0
  119. package/test/aiConnectionClient.test.js +2 -0
  120. package/test/apiConversationDriver.test.js +73 -0
  121. package/test/assistantApiSurfaceHeader.test.js +85 -0
  122. package/test/assistantBoundedToolLoop.test.js +285 -0
  123. package/test/assistantScroll.browser.test.js +16 -0
  124. package/test/claudeClient.test.js +189 -0
  125. package/test/claudeConversationSession.test.js +1173 -0
  126. package/test/claudeInstructionResume.test.js +82 -0
  127. package/test/claudeProcess.test.js +204 -0
  128. package/test/claudeTurn.test.js +253 -0
  129. package/test/codexConversationSession.test.js +2122 -0
  130. package/test/codexGoal.test.js +37 -0
  131. package/test/codexHelperDeletion.test.js +163 -0
  132. package/test/codexHelperThreadLedger.test.js +494 -0
  133. package/test/codexHistoryAdapter.test.js +541 -0
  134. package/test/codexInteractiveArguments.test.js +30 -0
  135. package/test/codexIsolation.test.js +756 -0
  136. package/test/codexModelCatalog.test.js +362 -0
  137. package/test/codexNativeHistoryExport.test.js +81 -0
  138. package/test/codexThreadSelection.test.js +136 -0
  139. package/test/codexTurn.test.js +2158 -0
  140. package/test/conversation.test.js +110 -0
  141. package/test/conversationAgentRun.test.js +326 -0
  142. package/test/conversationApiFacilities.test.js +90 -0
  143. package/test/conversationAttachments.test.js +238 -0
  144. package/test/conversationCommandWrapper.test.js +48 -0
  145. package/test/conversationConfiguration.test.js +174 -0
  146. package/test/conversationNativeStorage.test.js +25 -0
  147. package/test/conversationReplacement.test.js +172 -0
  148. package/test/conversationRuntime.test.js +241 -0
  149. package/test/conversationRuntimeStateUpgrade.test.js +144 -0
  150. package/test/conversationSession.test.js +1080 -0
  151. package/test/conversationStreaming.test.js +68 -0
  152. package/test/conversationSystemPrompt.test.js +84 -0
  153. package/test/conversationTools.test.js +416 -0
  154. package/test/fileConversationStorage.test.js +73 -0
  155. package/test/fixtures/codexHelperThreadLedger.js +39 -0
  156. package/test/localConversationExecution.test.js +90 -0
  157. package/test/messageDelivery.test.js +234 -1
  158. package/test/nativeConversationRetirement.test.js +215 -0
  159. package/test/openCodeClient.test.js +515 -0
  160. package/test/openCodeProcess.test.js +177 -0
  161. package/test/openCodeRuntime.test.js +322 -0
  162. package/test/openCodeTools.test.js +287 -0
  163. package/test/opencodeConversationSession.test.js +789 -0
  164. package/test/serviceToolCatalog.test.js +24 -0
@@ -0,0 +1,476 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
3
+ import net from "node:net";
4
+ import path from "node:path";
5
+ import { setTimeout as delay } from "node:timers/promises";
6
+ import { createOpenCodeServerClient, readBoundedResponse } from "./openCodeClient.js";
7
+ import { createLocalConversationExecution } from "./localExecution.js";
8
+ import { DISCOVERY_TOOL_DESCRIPTORS } from "../lib/serviceToolCatalog.js";
9
+ export { createOpenCodeSharedRuntime, openCodeServerForDirectory, readOpenCodeEnvironments, openCodeEnvironmentForDirectory,
10
+ openCodeEnvironmentForSession, assertOpenCodeModelProvider, limitOpenCodeModelOutput,
11
+ ensureOpenCodeSession, openCodeModel, sameOpenCodeSelection } from "./openCodeRuntime.js";
12
+ export { default as createOpenCodeConversationPlugin } from "./openCodePlugin.js";
13
+
14
+ export const OPENCODE_EXPECTED_VERSION = "1.18.31";
15
+ export const OPENCODE_HOST = "127.0.0.1";
16
+ export const OPENCODE_READY_TIMEOUT_MS = 30_000;
17
+ export const OPENCODE_STOP_TIMEOUT_MS = 3_000;
18
+
19
+ const OPENCODE_VERIFY_LIMIT_BYTES = 256 * 1024;
20
+ const OPENCODE_VERIFY_OUTPUT_TOKEN_MAX = 16;
21
+ const OPENCODE_VERIFY_TIMEOUT_MS = 30_000;
22
+ const OPENCODE_ZEN_CATALOG_LIMIT_BYTES = 2 * 1024 * 1024;
23
+ const OPENCODE_ZEN_CATALOG_TIMEOUT_MS = 5_000;
24
+ const OPENCODE_ZEN_MODELS_URL = "https://opencode.ai/zen/v1/models";
25
+ const OPENCODE_ZEN_PROVIDER_ID = "opencode";
26
+ const OPENCODE_ZEN_PUBLIC_API_KEY = "public";
27
+
28
+ function text(value = "") {
29
+ return String(value ?? "").trim();
30
+ }
31
+
32
+ export function openCodeProcessEnvironment(baseEnv = {}, {
33
+ cacheRoot = "", dbPath = "", inlineConfig = "", outputTokenMax = 0, password = ""
34
+ } = {}) {
35
+ return {
36
+ // OpenCode installs plugin dependencies through npm, which ignores XDG_CACHE_HOME.
37
+ npm_config_cache: path.join(cacheRoot, "npm"),
38
+ npm_config_prefer_offline: "true",
39
+ NO_PROXY: [text(baseEnv.NO_PROXY), OPENCODE_HOST, "localhost", "::1"]
40
+ .filter(Boolean)
41
+ .join(","),
42
+ OPENCODE_DB: dbPath,
43
+ OPENCODE_CONFIG_CONTENT: inlineConfig,
44
+ OPENCODE_DISABLE_AUTOUPDATE: "1",
45
+ OPENCODE_DISABLE_DEFAULT_PLUGINS: "1",
46
+ OPENCODE_DISABLE_SHARE: "1",
47
+ OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: "1",
48
+ OPENCODE_PRINT_LOGS: "0",
49
+ OPENCODE_SERVER_PASSWORD: password,
50
+ OPENCODE_SERVER_USERNAME: "opencode",
51
+ ...(Number.isSafeInteger(outputTokenMax) && outputTokenMax > 0
52
+ ? { OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX: String(outputTokenMax) }
53
+ : {})
54
+ };
55
+ }
56
+
57
+ async function availableLoopbackPort() {
58
+ const server = net.createServer();
59
+ return new Promise((resolve, reject) => {
60
+ server.once("error", reject);
61
+ server.listen(0, OPENCODE_HOST, () => {
62
+ const address = server.address();
63
+ const port = typeof address === "object" && address ? address.port : 0;
64
+ server.close((error) => {
65
+ if (error) {
66
+ reject(error);
67
+ } else {
68
+ resolve(port);
69
+ }
70
+ });
71
+ });
72
+ });
73
+ }
74
+
75
+ async function waitForOpenCodeReady({
76
+ client,
77
+ expectedVersion = OPENCODE_EXPECTED_VERSION,
78
+ processHandle,
79
+ timeoutMs = OPENCODE_READY_TIMEOUT_MS,
80
+ signal
81
+ } = {}) {
82
+ const deadline = Date.now() + timeoutMs;
83
+ let lastError = null;
84
+ while (Date.now() < deadline) {
85
+ signal?.throwIfAborted();
86
+ if (processHandle.exited) {
87
+ throw new Error("OpenCode exited before its server became ready.");
88
+ }
89
+ try {
90
+ const probe = AbortSignal.timeout(Math.max(100, Math.min(2_000, deadline - Date.now())));
91
+ const health = await client.health({
92
+ signal: signal ? AbortSignal.any([signal, probe]) : probe
93
+ });
94
+ signal?.throwIfAborted();
95
+ if (health?.healthy === true) {
96
+ if (expectedVersion && text(health.version) !== expectedVersion) {
97
+ const error = new Error(
98
+ `OpenCode ${text(health.version) || "(unknown)"} is installed; this host requires ${expectedVersion}.`
99
+ );
100
+ error.code = "assistant_opencode_version_mismatch";
101
+ throw error;
102
+ }
103
+ return health;
104
+ }
105
+ } catch (error) {
106
+ signal?.throwIfAborted();
107
+ if (error?.code === "assistant_opencode_version_mismatch") {
108
+ throw error;
109
+ }
110
+ lastError = error;
111
+ }
112
+ await delay(100, undefined, { signal });
113
+ }
114
+ signal?.throwIfAborted();
115
+ const error = new Error("OpenCode did not become ready before the startup deadline.");
116
+ error.cause = lastError;
117
+ error.code = "assistant_opencode_start_timeout";
118
+ throw error;
119
+ }
120
+
121
+ const actionPermission = Object.fromEntries(DISCOVERY_TOOL_DESCRIPTORS.map(tool => [tool.name, "allow"]));
122
+ const nativePermission = { bash: "allow", read: "allow", write: "allow", edit: "allow",
123
+ glob: "allow", grep: "allow", list: "allow", patch: "allow", apply_patch: "allow", todoread: "allow", todowrite: "allow" };
124
+ export const openCodeApplicationToolSchemas = DISCOVERY_TOOL_DESCRIPTORS.map(({ name, description, parameters }) => ({
125
+ type: "function", function: { name, description, parameters }
126
+ }));
127
+
128
+ export function openCodeConversationAgent({ nativeTools = false, tools = false } = {}) {
129
+ return `jskit-assistant${nativeTools ? "-coding" : ""}${tools ? "-actions" : ""}`;
130
+ }
131
+
132
+ export function openCodeConversationAgents({ sessionEnvironmentRegistry = "" } = {}) {
133
+ const guarded = Boolean(text(sessionEnvironmentRegistry));
134
+ return Object.fromEntries([false, true].flatMap(nativeTools => [false, true].map(tools => [
135
+ openCodeConversationAgent({ nativeTools, tools }), {
136
+ hidden: true, mode: "primary",
137
+ // Keep native definitions behind approval, as in the original host agents.
138
+ // The registered plugin rejects every ungranted native call before execution.
139
+ permission: nativeTools || tools || guarded ? { "*": !nativeTools && guarded ? "ask" : "deny",
140
+ ...(nativeTools ? nativePermission : {}), ...(tools ? actionPermission : {}) } : "deny"
141
+ }
142
+ ])));
143
+ }
144
+
145
+ /** Shared conversation configuration around the extracted server lifecycle. */
146
+ export async function createOpenCodeConversationServer({
147
+ command = "opencode", execution = createLocalConversationExecution(), env = process.env,
148
+ workdir, stateDirectory, databasePath, sessionEnvironmentRegistry, connection, maxOutputTokens,
149
+ onStarted, onFailure, executionLimits, signal
150
+ } = {}) {
151
+ await mkdir(stateDirectory, { recursive: true, mode: 0o700 });
152
+ const directory = await mkdtemp(path.join(stateDirectory, "process-"));
153
+ const model = `${connection.providerId}/${connection.model}`;
154
+ const configuration = {
155
+ autoupdate: false, share: "disabled", snapshot: false, permission: "deny",
156
+ model, small_model: model, default_agent: "jskit-assistant",
157
+ enabled_providers: [connection.providerId],
158
+ agent: openCodeConversationAgents({ sessionEnvironmentRegistry }),
159
+ plugin: [new URL("./openCodePlugin.js", import.meta.url).href],
160
+ provider: { [connection.providerId]: {
161
+ npm: connection.sdkPackage,
162
+ ...(connection.modelLimits ? { models: { [connection.model]: {
163
+ // The authorized application catalogue selects this exact model. Do not
164
+ // inherit a native catalogue alias or its availability classification.
165
+ id: connection.model, name: connection.model, status: "active", limit: connection.modelLimits
166
+ } } } : {}),
167
+ options: { ...(connection.baseURL ? { baseURL: connection.baseURL } : {}) }
168
+ } }
169
+ };
170
+ // No inherited account/config variables: only the selected authorized connection
171
+ // is installed into this process's private credential home.
172
+ const environment = Object.fromEntries(Object.entries(env).filter(([key]) =>
173
+ /^(PATH|LANG|LC_\w+|TZ|HTTPS?_PROXY|https?_proxy|NO_PROXY|no_proxy|SSL_CERT_FILE|SSL_CERT_DIR|NODE_EXTRA_CA_CERTS)$/u.test(key)));
174
+ Object.assign(environment, {
175
+ HOME: path.join(directory, "home"), XDG_CONFIG_HOME: path.join(directory, "config"),
176
+ XDG_DATA_HOME: path.join(directory, "data"), XDG_STATE_HOME: path.join(directory, "state"),
177
+ XDG_CACHE_HOME: path.join(stateDirectory, "cache"),
178
+ ...openCodeProcessEnvironment(env, { cacheRoot: path.join(stateDirectory, "cache"),
179
+ dbPath: databasePath, inlineConfig: JSON.stringify(configuration), outputTokenMax: maxOutputTokens }),
180
+ OPENCODE_DISABLE_PROJECT_CONFIG: "1", OPENCODE_DISABLE_EXTERNAL_SKILLS: "1", OPENCODE_DISABLE_CLAUDE_CODE_SKILLS: "1",
181
+ JSKIT_OPENCODE_ENV_REGISTRY: sessionEnvironmentRegistry,
182
+ JSKIT_OPENCODE_TOOL_SCHEMAS: JSON.stringify(openCodeApplicationToolSchemas)
183
+ });
184
+ return createOpenCodeServerProcess({ command, execution, env: environment, workdir, privateRoot: directory,
185
+ directory: workdir, connections: [connection], onStarted: id => onStarted?.(id, directory),
186
+ onFailure, executionLimits, signal });
187
+ }
188
+
189
+ /** Shared server lifecycle for standalone and managed hosts. */
190
+ export async function createOpenCodeServerProcess({
191
+ command = "opencode", execution = createLocalConversationExecution(), env = process.env,
192
+ workdir, privateRoot, directory = "", connections = [], port = 0, fetchImpl = globalThis.fetch,
193
+ allowAttachmentDirectories = false, expectedVersion = OPENCODE_EXPECTED_VERSION, readinessTimeoutMs = OPENCODE_READY_TIMEOUT_MS,
194
+ onStarted, onFailure, executionLimits, signal
195
+ } = {}) {
196
+ if (!path.isAbsolute(workdir || "") || !path.isAbsolute(privateRoot || "")) {
197
+ throw new TypeError("OpenCode server processes require absolute working and private roots.");
198
+ }
199
+ const selectedPort = Number(port) || await availableLoopbackPort();
200
+ const baseUrl = `http://${OPENCODE_HOST}:${selectedPort}`;
201
+ const password = randomBytes(32).toString("base64url");
202
+ const environment = { ...env, OPENCODE_SERVER_USERNAME: "opencode", OPENCODE_SERVER_PASSWORD: password };
203
+ const client = createOpenCodeServerClient({ baseUrl, password, directory, fetchImpl, allowAttachmentDirectories });
204
+ let native;
205
+ let executionId;
206
+ let closing = false;
207
+ let stopped = false;
208
+ let stopPromise = null;
209
+ const logPath = path.join(privateRoot, "opencode-server.log");
210
+ const processHandle = {
211
+ get exited() { return stopped || native?.running === false; },
212
+ async stop() {
213
+ const proof = await execution.stop(executionId, {
214
+ allowMissingRecordScopeRecovery: true,
215
+ reason: "opencode-server-stop",
216
+ termTimeoutMs: OPENCODE_STOP_TIMEOUT_MS
217
+ });
218
+ stopped = proof?.scopeEmpty === true;
219
+ return { ...(proof && typeof proof === "object" ? proof : {}), exited: stopped };
220
+ }
221
+ };
222
+ function stop() {
223
+ if (stopPromise) {
224
+ return stopPromise;
225
+ }
226
+ closing = true;
227
+ const stopping = Promise.resolve().then(async () => {
228
+ const exitProof = executionId
229
+ ? await processHandle.stop()
230
+ : { code: null, exited: true, signal: "", scopeEmpty: true };
231
+ if (exitProof.exited === true) {
232
+ await rm(privateRoot, { force: true, recursive: true });
233
+ }
234
+ return exitProof;
235
+ });
236
+ stopPromise = stopping;
237
+ void stopping.then((proof) => {
238
+ if (proof?.exited !== true && stopPromise === stopping) stopPromise = null;
239
+ }, () => {
240
+ if (stopPromise === stopping) stopPromise = null;
241
+ });
242
+ return stopPromise;
243
+ }
244
+ try {
245
+ await mkdir(privateRoot, { recursive: true, mode: 0o700 });
246
+ for (const name of ["HOME", "XDG_CONFIG_HOME", "XDG_DATA_HOME", "XDG_STATE_HOME", "XDG_CACHE_HOME"]) {
247
+ if (environment[name]) await mkdir(environment[name], { recursive: true, mode: 0o700 });
248
+ }
249
+ signal?.throwIfAborted();
250
+ await writeFile(logPath, "", { mode: 0o600 });
251
+ native = await execution.start({ command, cwd: workdir, env: environment, limits: executionLimits,
252
+ logPath, args: ["serve", "--hostname", OPENCODE_HOST, "--port", String(selectedPort), "--mdns=false"] });
253
+ executionId = native.id;
254
+ await onStarted?.(native.id);
255
+ const fail = error => { if (!closing) Promise.resolve().then(() => onFailure?.(error)).catch(() => {}); };
256
+ native.exited.then(() => fail(new Error("The owned OpenCode process exited."))).catch(fail);
257
+ const health = await waitForOpenCodeReady({ client, expectedVersion, processHandle, timeoutMs: readinessTimeoutMs, signal });
258
+ const authenticated = new Set();
259
+ for (const connection of connections) {
260
+ const providerId = text(connection?.providerId);
261
+ const key = String(connection?.apiKey || "");
262
+ if (!providerId || !key || authenticated.has(providerId)) {
263
+ continue;
264
+ }
265
+ await client.authenticateApiKey(providerId, key, { signal });
266
+ authenticated.add(providerId);
267
+ }
268
+ return Object.freeze({ client, executionId: native.id, privateRoot, pid: native.pid, port: selectedPort,
269
+ environment, health: Object.freeze({ ...health }), modelProviderIds: Object.freeze([...authenticated]),
270
+ readLogs: () => native.readLogs?.() || { stderr: "", stdout: "" }, stop,
271
+ attachArguments({ conversationId, directory }) {
272
+ if (!conversationId || !path.isAbsolute(directory || "")) throw new TypeError("An OpenCode terminal requires a conversation and absolute directory.");
273
+ return ["attach", baseUrl, "--dir", directory, "--session", conversationId, "--pure"];
274
+ }
275
+ });
276
+ } catch (error) {
277
+ error.code ||= "assistant_opencode_start_failed";
278
+ executionId ||= error.executionId;
279
+ error.executionId = executionId;
280
+ if (!native && executionId) {
281
+ try { await onStarted?.(executionId); }
282
+ catch (bindingError) { error.bindingError = bindingError; }
283
+ }
284
+ try { error.details = native?.readLogs?.() || { stderr: "", stdout: "" }; }
285
+ catch (logError) { error.logError = logError; }
286
+ try { error.stopProof = await stop(); }
287
+ catch (cleanupError) { error.cleanupError = cleanupError; error.stopProof = { scopeEmpty: false }; }
288
+ if (!error.stopProof.scopeEmpty) {
289
+ error.cleanupFailed = true;
290
+ error.retryCleanup = stop;
291
+ }
292
+ throw error;
293
+ }
294
+ }
295
+
296
+ function openCodeZenCatalogError(cause = null) {
297
+ const error = new Error("OpenCode Zen's current model list could not be read. Try again.", {
298
+ cause
299
+ });
300
+ error.code = "assistant_opencode_zen_catalog_unavailable";
301
+ error.retryable = true;
302
+ error.statusCode = 503;
303
+ return error;
304
+ }
305
+
306
+ export async function readOpenCodeZenModelIds({
307
+ fetchImpl = globalThis.fetch,
308
+ timeoutMs = OPENCODE_ZEN_CATALOG_TIMEOUT_MS
309
+ } = {}) {
310
+ if (typeof fetchImpl !== "function") {
311
+ throw new TypeError("OpenCode Zen catalogue reads require fetch().");
312
+ }
313
+ try {
314
+ const response = await fetchImpl(OPENCODE_ZEN_MODELS_URL, {
315
+ headers: { accept: "application/json" },
316
+ method: "GET",
317
+ signal: AbortSignal.timeout(Math.max(100, Math.min(30_000, Number(timeoutMs) || 0)))
318
+ });
319
+ if (!response?.ok) {
320
+ throw new Error(`OpenCode Zen returned HTTP ${Number(response?.status) || 0}.`);
321
+ }
322
+ const payload = JSON.parse(await readBoundedResponse(
323
+ response,
324
+ OPENCODE_ZEN_CATALOG_LIMIT_BYTES
325
+ ));
326
+ if (!Array.isArray(payload?.data) || payload.data.length === 0) {
327
+ throw new Error("OpenCode Zen returned an empty model list.");
328
+ }
329
+ const ids = payload.data.map((model) => text(model?.id));
330
+ const uniqueIds = new Set(ids);
331
+ if (ids.some((id) => !id) || uniqueIds.size !== ids.length) {
332
+ throw new Error("OpenCode Zen returned an invalid model list.");
333
+ }
334
+ return Object.freeze([...uniqueIds].sort((left, right) => left.localeCompare(right)));
335
+ } catch (error) {
336
+ if (error?.code === "assistant_opencode_zen_catalog_unavailable") {
337
+ throw error;
338
+ }
339
+ throw openCodeZenCatalogError(error);
340
+ }
341
+ }
342
+
343
+ export async function readOpenCodeCatalog({
344
+ createServerProcess,
345
+ privateRoot = "",
346
+ workdir = ""
347
+ } = {}) {
348
+ const normalizedPrivateRoot = path.resolve(text(privateRoot));
349
+ const normalizedWorkdir = path.resolve(text(workdir));
350
+ if (!text(privateRoot) || !text(workdir)) {
351
+ throw new TypeError("OpenCode catalogue reads require private and working roots.");
352
+ }
353
+ const authRoot = path.join(normalizedPrivateRoot, "data", "opencode");
354
+ await mkdir(authRoot, { mode: 0o700, recursive: true });
355
+ await writeFile(
356
+ path.join(authRoot, "auth.json"),
357
+ `${JSON.stringify({
358
+ [OPENCODE_ZEN_PROVIDER_ID]: {
359
+ key: OPENCODE_ZEN_PUBLIC_API_KEY,
360
+ type: "api"
361
+ }
362
+ })}\n`,
363
+ { mode: 0o600 }
364
+ );
365
+ const server = await createServerProcess({
366
+ dbPath: path.join(normalizedPrivateRoot, "opencode.db"),
367
+ privateRoot: normalizedPrivateRoot,
368
+ providerConnections: [],
369
+ workdir: normalizedWorkdir
370
+ });
371
+ let catalog;
372
+ let readError;
373
+ try {
374
+ const [providers, agents] = await Promise.all([
375
+ server.client.providers({ directory: server.workdir }),
376
+ server.client.agents({ directory: server.workdir })
377
+ ]);
378
+ catalog = { agents, providers };
379
+ } catch (error) {
380
+ readError = error;
381
+ }
382
+ const stopped = await server.stop();
383
+ if (stopped?.exited !== true) {
384
+ const error = new Error("The temporary OpenCode catalogue process did not stop cleanly.");
385
+ error.code = "assistant_opencode_catalog_stop_failed";
386
+ throw error;
387
+ }
388
+ if (readError) {
389
+ throw readError;
390
+ }
391
+ return catalog;
392
+ }
393
+
394
+ export async function verifyOpenCodeApiKey({
395
+ apiKey = "",
396
+ agentId = openCodeConversationAgent(),
397
+ prepareCommand,
398
+ modelId = "",
399
+ modelProviderId = "",
400
+ privateRoot = "",
401
+ workdir = ""
402
+ } = {}) {
403
+ const key = String(apiKey || "");
404
+ const providerId = text(modelProviderId);
405
+ const selectedModelId = text(modelId);
406
+ const normalizedPrivateRoot = path.resolve(text(privateRoot));
407
+ const normalizedWorkdir = path.resolve(text(workdir));
408
+ if (!key || !providerId || !selectedModelId || !text(privateRoot) || !text(workdir)) {
409
+ throw new TypeError("OpenCode API-key verification requires a key, provider, model, and isolated roots.");
410
+ }
411
+ try {
412
+ await Promise.all([
413
+ mkdir(path.join(normalizedPrivateRoot, "data", "opencode"), {
414
+ mode: 0o700,
415
+ recursive: true
416
+ }),
417
+ mkdir(normalizedWorkdir, { recursive: true })
418
+ ]);
419
+ await writeFile(
420
+ path.join(normalizedPrivateRoot, "data", "opencode", "auth.json"),
421
+ `${JSON.stringify({ [providerId]: { key, type: "api" } })}\n`,
422
+ { mode: 0o600 }
423
+ );
424
+ const commandRunner = prepareCommand({
425
+ providerId,
426
+ modelId: selectedModelId,
427
+ privateRoot: normalizedPrivateRoot,
428
+ workdir: normalizedWorkdir,
429
+ outputTokenMax: OPENCODE_VERIFY_OUTPUT_TOKEN_MAX
430
+ });
431
+ let result;
432
+ try {
433
+ result = await commandRunner({
434
+ args: [
435
+ "run",
436
+ "--pure",
437
+ "--agent",
438
+ agentId,
439
+ "--model",
440
+ `${providerId}/${selectedModelId}`,
441
+ "--format",
442
+ "json",
443
+ "Reply only OK."
444
+ ],
445
+ maxBuffer: OPENCODE_VERIFY_LIMIT_BYTES,
446
+ mode: "capture",
447
+ timeout: OPENCODE_VERIFY_TIMEOUT_MS
448
+ });
449
+ } catch {
450
+ const error = new Error("OpenCode API-key verification could not run.");
451
+ error.code = "assistant_opencode_key_verification_unavailable";
452
+ error.retryable = true;
453
+ error.statusCode = 503;
454
+ throw error;
455
+ }
456
+ if (result?.ok !== true) {
457
+ const exitCode = Number(result?.exitCode);
458
+ const unavailable = result?.timedOut === true ||
459
+ result?.retryable === true ||
460
+ Boolean(text(result?.code)) ||
461
+ (Number.isSafeInteger(exitCode) && exitCode !== 1);
462
+ const error = new Error(unavailable
463
+ ? "OpenCode API-key verification could not run."
464
+ : "OpenCode rejected the API-key verification request.");
465
+ error.code = unavailable
466
+ ? "assistant_opencode_key_verification_unavailable"
467
+ : "assistant_opencode_key_verification_failed";
468
+ error.retryable = unavailable;
469
+ error.statusCode = unavailable ? 503 : 422;
470
+ throw error;
471
+ }
472
+ return Object.freeze({ ok: true });
473
+ } finally {
474
+ await rm(normalizedPrivateRoot, { force: true, recursive: true });
475
+ }
476
+ }