@jskit-ai/assistant-core 0.1.200 → 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.
- package/examples/conversation/README.md +8 -1
- package/examples/conversation/package.json +5 -5
- package/fixtures/responsive-assistant/App.vue +4 -1
- package/package.json +99 -36
- package/src/client/conversation/AssistantConversationElement.vue +27 -4
- package/src/client/conversation/AssistantConversationStatus.vue +24 -0
- package/src/client/conversation/AssistantGoalControl.vue +8 -5
- package/src/client/conversation/AssistantTranscript.vue +10 -3
- package/src/client/conversation/index.js +2 -0
- package/src/client/conversation/messageDelivery.js +115 -32
- package/src/client/conversation/retainedConversation.js +32 -0
- package/src/client/conversation/useAssistantQuestions.js +76 -0
- package/src/client/lib/assistantApi.js +72 -2
- package/src/server/conversation/agentRun.js +222 -0
- package/src/server/conversation/attachments.js +93 -0
- package/src/server/conversation/claudeClient.js +221 -0
- package/src/server/conversation/claudeHistory.js +256 -0
- package/src/server/conversation/claudeProcess.js +327 -0
- package/src/server/conversation/claudeTools.js +42 -0
- package/src/server/conversation/claudeTurn.js +1082 -0
- package/src/server/conversation/codexConfiguration.js +1183 -0
- package/src/server/conversation/codexConversations.js +1082 -0
- package/src/server/conversation/codexDetachedTurn.js +332 -0
- package/src/server/conversation/codexEvents.js +194 -1
- package/src/server/conversation/codexGoalCommands.js +124 -0
- package/src/server/conversation/codexHelperExecution.js +306 -0
- package/src/server/conversation/codexHelperThreadLedger.js +804 -0
- package/src/server/conversation/codexHelperThreadRecovery.js +395 -0
- package/src/server/conversation/codexHelperThreads.js +638 -0
- package/src/server/conversation/codexHistoryAdapter.js +369 -0
- package/src/server/conversation/codexMessageCommands.js +663 -0
- package/src/server/conversation/codexMessageDelivery.js +211 -0
- package/src/server/conversation/codexNativeHistoryExport.js +95 -0
- package/src/server/conversation/codexNotificationObserver.js +482 -0
- package/src/server/conversation/codexNotificationQueue.js +103 -0
- package/src/server/conversation/codexObservationControl.js +174 -0
- package/src/server/conversation/codexOutput.js +931 -0
- package/src/server/conversation/codexProcess.js +1559 -0
- package/src/server/conversation/codexProcessLeader.js +14 -0
- package/src/server/conversation/codexProvider.js +2710 -0
- package/src/server/conversation/codexProviderOwner.js +1058 -0
- package/src/server/conversation/codexProviderSelection.js +218 -0
- package/src/server/conversation/codexReasoning.js +314 -0
- package/src/server/conversation/codexRenewalCommands.js +290 -0
- package/src/server/conversation/codexResultSettlement.js +409 -0
- package/src/server/conversation/codexRunJournal.js +658 -0
- package/src/server/conversation/codexRuntimeLifecycle.js +288 -0
- package/src/server/conversation/codexSessionCleanup.js +96 -0
- package/src/server/conversation/codexSessionReconciliation.js +284 -0
- package/src/server/conversation/codexThreadReadiness.js +397 -0
- package/src/server/conversation/codexThreadStatus.js +354 -0
- package/src/server/conversation/codexTurn.js +701 -255
- package/src/server/conversation/codexTurnControl.js +317 -0
- package/src/server/conversation/codexTurnRecovery.js +990 -0
- package/src/server/conversation/codexTurnState.js +549 -0
- package/src/server/conversation/commandHook.js +18 -0
- package/src/server/conversation/commandWrapper.js +37 -0
- package/src/server/conversation/configuration.js +43 -0
- package/src/server/conversation/continuity.js +289 -0
- package/src/server/conversation/fileStorage.js +55 -0
- package/src/server/conversation/hookBridge.js +19 -0
- package/src/server/conversation/index.js +13 -0
- package/src/server/conversation/localExecution.js +665 -0
- package/src/server/conversation/memoryStorage.js +7 -72
- package/src/server/conversation/nativeHistoryExport.js +90 -0
- package/src/server/conversation/nativeStorage.js +23 -0
- package/src/server/conversation/openCodeClient.js +209 -19
- package/src/server/conversation/openCodePlugin.js +84 -0
- package/src/server/conversation/openCodeProcess.js +476 -0
- package/src/server/conversation/openCodeRuntime.js +1578 -0
- package/src/server/conversation/openCodeTools.js +60 -0
- package/src/server/conversation/openCodeTurn.js +557 -0
- package/src/server/conversation/provider.js +30 -0
- package/src/server/conversation/providers/api.js +136 -0
- package/src/server/conversation/providers/claude.js +63 -0
- package/src/server/conversation/providers/claudeDriver.js +425 -0
- package/src/server/conversation/providers/codex.js +248 -0
- package/src/server/conversation/providers/codexDriver.js +1078 -0
- package/src/server/conversation/providers/opencode.js +49 -0
- package/src/server/conversation/providers/opencodeDriver.js +650 -0
- package/src/server/conversation/providers/scoped.js +312 -0
- package/src/server/conversation/runtime.js +2220 -0
- package/src/server/conversation/runtimeStateUpgrade.js +125 -0
- package/src/server/conversation/storage.js +184 -0
- package/src/server/conversation/streams.js +7 -2
- package/src/server/conversation/structuredOutput.js +240 -0
- package/src/server/conversation/systemPrompt.js +54 -0
- package/src/server/conversation/tools.js +84 -0
- package/src/server/conversation/transcript.js +5 -4
- package/src/server/hosts/claudeHistory.js +5 -0
- package/src/server/hosts/claudeProcess.js +11 -0
- package/src/server/hosts/claudeTurn.js +4 -0
- package/src/server/hosts/codexConfiguration.js +20 -0
- package/src/server/hosts/codexEvents.js +13 -0
- package/src/server/hosts/codexProcess.js +12 -0
- package/src/server/hosts/codexProvider.js +23 -0
- package/src/server/hosts/codexTurn.js +20 -0
- package/src/server/hosts/nativeHistory.js +3 -0
- package/src/server/hosts/openCodeClient.js +3 -0
- package/src/server/hosts/openCodeProcess.js +23 -0
- package/src/server/hosts/openCodeTurn.js +7 -0
- package/src/server/index.js +2 -0
- package/src/server/lib/aiConnectionClient.js +13 -6
- package/src/server/lib/assistantCompletion.js +190 -0
- package/src/server/lib/assistantToolLoop.js +314 -0
- package/src/server/lib/serviceToolCatalog.js +25 -10
- package/src/shared/assistantResponseText.js +29 -0
- package/src/shared/conversation/attachments.js +19 -0
- package/src/shared/conversation/index.js +3 -0
- package/src/shared/conversation/pagination.js +78 -0
- package/src/shared/conversation/patches.js +150 -0
- package/src/shared/conversation/questions.js +48 -0
- package/src/shared/conversation/streaming.js +7 -4
- package/src/shared/index.js +2 -0
- package/src/shared/nativeProviders.js +45 -0
- package/src/testing/conversationStorageContract.js +31 -1
- package/src/testing/nativeCodex.js +11 -0
- package/src/testing/nativeOpenCode.js +3 -0
- package/test/aiConnectionClient.test.js +2 -0
- package/test/apiConversationDriver.test.js +73 -0
- package/test/assistantApiSurfaceHeader.test.js +85 -0
- package/test/assistantBoundedToolLoop.test.js +285 -0
- package/test/assistantScroll.browser.test.js +16 -0
- package/test/claudeClient.test.js +189 -0
- package/test/claudeConversationSession.test.js +1173 -0
- package/test/claudeInstructionResume.test.js +82 -0
- package/test/claudeProcess.test.js +204 -0
- package/test/claudeTurn.test.js +253 -0
- package/test/codexConversationSession.test.js +2122 -0
- package/test/codexGoal.test.js +37 -0
- package/test/codexHelperDeletion.test.js +163 -0
- package/test/codexHelperThreadLedger.test.js +494 -0
- package/test/codexHistoryAdapter.test.js +541 -0
- package/test/codexInteractiveArguments.test.js +30 -0
- package/test/codexIsolation.test.js +756 -0
- package/test/codexModelCatalog.test.js +362 -0
- package/test/codexNativeHistoryExport.test.js +81 -0
- package/test/codexThreadSelection.test.js +136 -0
- package/test/codexTurn.test.js +2158 -0
- package/test/conversation.test.js +110 -0
- package/test/conversationAgentRun.test.js +326 -0
- package/test/conversationApiFacilities.test.js +90 -0
- package/test/conversationAttachments.test.js +238 -0
- package/test/conversationCommandWrapper.test.js +48 -0
- package/test/conversationConfiguration.test.js +174 -0
- package/test/conversationNativeStorage.test.js +25 -0
- package/test/conversationReplacement.test.js +172 -0
- package/test/conversationRuntime.test.js +241 -0
- package/test/conversationRuntimeStateUpgrade.test.js +144 -0
- package/test/conversationSession.test.js +1080 -0
- package/test/conversationStreaming.test.js +68 -0
- package/test/conversationSystemPrompt.test.js +84 -0
- package/test/conversationTools.test.js +416 -0
- package/test/fileConversationStorage.test.js +73 -0
- package/test/fixtures/codexHelperThreadLedger.js +39 -0
- package/test/localConversationExecution.test.js +90 -0
- package/test/messageDelivery.test.js +234 -1
- package/test/nativeConversationRetirement.test.js +215 -0
- package/test/openCodeClient.test.js +515 -0
- package/test/openCodeProcess.test.js +177 -0
- package/test/openCodeRuntime.test.js +322 -0
- package/test/openCodeTools.test.js +287 -0
- package/test/opencodeConversationSession.test.js +789 -0
- 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
|
+
}
|