@jskit-ai/assistant-core 0.2.7 → 0.2.8
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/package.json +5 -5
- package/package.json +18 -7
- package/src/client/browser.js +21 -0
- package/src/client/conversation/AssistantAttachmentQueue.vue +25 -108
- package/src/client/conversation/AssistantComposerActions.vue +1 -1
- package/src/client/conversation/AssistantConversationElement.vue +14 -52
- package/src/client/conversation/AssistantGoalControl.vue +13 -10
- package/src/client/conversation/AssistantPromptInput.vue +6 -91
- package/src/client/conversation/AssistantTranscript.css +505 -0
- package/src/client/conversation/AssistantTranscript.vue +24 -1197
- package/src/client/conversation/attachmentFiles.js +158 -0
- package/src/client/conversation/attachmentQueueItems.js +121 -0
- package/src/client/conversation/attachmentUploadRows.js +265 -0
- package/src/client/conversation/conversationDisplay.js +54 -0
- package/src/client/conversation/messageDelivery.js +13 -9
- package/src/client/conversation/retainedConversation.js +14 -3
- package/src/client/conversation/submitText.js +13 -2
- package/src/client/conversation/transcriptEntries.js +225 -0
- package/src/client/conversation/useAssistantAttachments.js +86 -388
- package/src/client/conversation/useAssistantQuestions.js +16 -3
- package/src/client/conversation/useAssistantSuggestions.js +16 -2
- package/src/client/conversation/useScrollToBottom.js +17 -19
- package/src/client/conversation/useTextareaAutoGrow.js +115 -0
- package/src/client/conversation/useTranscriptScrollFollow.js +525 -0
- package/src/client/lib/assistantApi.js +192 -161
- package/src/client/types.js +110 -0
- package/src/server/conversation/attachments.js +21 -6
- package/src/server/conversation/boundedToolLoop.js +1 -0
- package/src/server/conversation/claudeClient.js +71 -20
- package/src/server/conversation/claudeCommandRun.js +191 -0
- package/src/server/conversation/claudeHistory.js +7 -0
- package/src/server/conversation/claudeNativeTurn.js +708 -0
- package/src/server/conversation/claudeProcess.js +186 -112
- package/src/server/conversation/claudeProcessStart.js +214 -0
- package/src/server/conversation/claudePromptDelivery.js +183 -0
- package/src/server/conversation/claudeSessionCleanup.js +248 -0
- package/src/server/conversation/claudeTools.js +15 -3
- package/src/server/conversation/claudeTurn.js +212 -1388
- package/src/server/conversation/codexAccountReader.js +168 -0
- package/src/server/conversation/codexAuthPreflight.js +5 -4
- package/src/server/conversation/codexClient.js +18 -6
- package/src/server/conversation/codexConfiguration.js +8 -844
- package/src/server/conversation/codexConversationStops.js +377 -0
- package/src/server/conversation/codexConversationStore.js +119 -85
- package/src/server/conversation/codexConversationTurns.js +378 -0
- package/src/server/conversation/codexConversations.js +134 -866
- package/src/server/conversation/codexDetachedConversation.js +203 -0
- package/src/server/conversation/codexDetachedTurn.js +145 -104
- package/src/server/conversation/codexEvents.js +5 -200
- package/src/server/conversation/codexGoalCommands.js +14 -4
- package/src/server/conversation/codexHelperExecution.js +57 -57
- package/src/server/conversation/codexHelperThreadLedger.js +30 -288
- package/src/server/conversation/codexHelperThreadRecord.js +304 -0
- package/src/server/conversation/codexHelperThreadRecovery.js +54 -76
- package/src/server/conversation/codexHelperThreads.js +94 -93
- package/src/server/conversation/codexHistoryAdapter.js +13 -8
- package/src/server/conversation/codexIsolation.js +672 -0
- package/src/server/conversation/codexLiveItems.js +379 -0
- package/src/server/conversation/codexMessageCommands.js +68 -286
- package/src/server/conversation/codexMessageDelivery.js +46 -37
- package/src/server/conversation/codexModelCatalog.js +86 -7
- package/src/server/conversation/codexNotificationObserver.js +81 -88
- package/src/server/conversation/codexNotificationQueue.js +21 -10
- package/src/server/conversation/codexObservationControl.js +105 -89
- package/src/server/conversation/codexOutput.js +84 -419
- package/src/server/conversation/codexProcess.js +29 -1053
- package/src/server/conversation/codexProcessLeader.js +1 -0
- package/src/server/conversation/codexPromptDispatch.js +241 -0
- package/src/server/conversation/codexProvider.js +147 -1807
- package/src/server/conversation/codexProviderAuth.js +348 -0
- package/src/server/conversation/codexProviderInvalidation.js +371 -0
- package/src/server/conversation/codexProviderInventories.js +437 -0
- package/src/server/conversation/codexProviderOwner.js +149 -957
- package/src/server/conversation/codexProviderRelease.js +465 -0
- package/src/server/conversation/codexProviderSelection.js +26 -15
- package/src/server/conversation/codexProviderThreads.js +698 -0
- package/src/server/conversation/codexProviderTurns.js +453 -0
- package/src/server/conversation/codexReasoning.js +32 -23
- package/src/server/conversation/codexRenewalCommands.js +18 -6
- package/src/server/conversation/codexRenewalThreads.js +7 -22
- package/src/server/conversation/codexResultSettlement.js +70 -88
- package/src/server/conversation/codexRunJournal.js +91 -97
- package/src/server/conversation/codexRuntime.js +426 -0
- package/src/server/conversation/codexRuntimeInventory.js +113 -0
- package/src/server/conversation/codexRuntimeLifecycle.js +39 -13
- package/src/server/conversation/codexRuntimeStart.js +431 -0
- package/src/server/conversation/codexRuntimeStop.js +324 -0
- package/src/server/conversation/codexSessionCleanup.js +25 -10
- package/src/server/conversation/codexSessionReconciliation.js +21 -7
- package/src/server/conversation/codexThreadReadiness.js +49 -52
- package/src/server/conversation/codexThreadSnapshot.js +210 -0
- package/src/server/conversation/codexThreadStatus.js +48 -62
- package/src/server/conversation/codexTurn.js +171 -55
- package/src/server/conversation/codexTurnActivity.js +210 -0
- package/src/server/conversation/codexTurnControl.js +44 -48
- package/src/server/conversation/codexTurnEndings.js +610 -0
- package/src/server/conversation/codexTurnRecovery.js +97 -848
- package/src/server/conversation/codexTurnState.js +138 -111
- package/src/server/conversation/completedResponses.js +227 -0
- package/src/server/conversation/configuration.js +2 -2
- package/src/server/conversation/continuity.js +43 -6
- package/src/server/conversation/conversationDelivery.js +394 -0
- package/src/server/conversation/conversationEntries.js +365 -0
- package/src/server/conversation/conversationEvents.js +192 -0
- package/src/server/conversation/conversationGoals.js +187 -0
- package/src/server/conversation/conversationHandles.js +237 -0
- package/src/server/conversation/conversationReads.js +206 -0
- package/src/server/conversation/conversationRefusals.js +58 -0
- package/src/server/conversation/conversationReplacement.js +254 -0
- package/src/server/conversation/conversationSending.js +273 -0
- package/src/server/conversation/conversationStopping.js +121 -0
- package/src/server/conversation/conversationTurnDelivery.js +413 -0
- package/src/server/conversation/conversationTurns.js +511 -0
- package/src/server/conversation/deliveryTiming.js +25 -3
- package/src/server/conversation/fileStorage.js +11 -6
- package/src/server/conversation/hookBridge.js +14 -3
- package/src/server/conversation/index.js +1 -1
- package/src/server/conversation/localExecution.js +82 -625
- package/src/server/conversation/nativeCommandCallbacks.js +10 -0
- package/src/server/conversation/nativeConversationLifecycle.js +379 -0
- package/src/server/conversation/nativeHistoryExport.js +27 -18
- package/src/server/conversation/normalize.js +9 -0
- package/src/server/conversation/openCodeClient.js +295 -366
- package/src/server/conversation/openCodeNativeHistory.js +141 -0
- package/src/server/conversation/openCodeNativeSession.js +79 -0
- package/src/server/conversation/openCodeProcess.js +109 -53
- package/src/server/conversation/openCodeProcessOwner.js +565 -0
- package/src/server/conversation/openCodeRuntime.js +111 -1814
- package/src/server/conversation/openCodeSessionTurns.js +547 -0
- package/src/server/conversation/openCodeTemporaryConversations.js +394 -0
- package/src/server/conversation/openCodeTools.js +28 -14
- package/src/server/conversation/openCodeTurn.js +80 -74
- package/src/server/conversation/openCodeTurnMonitor.js +388 -0
- package/src/server/conversation/processIdentity.js +582 -0
- package/src/server/conversation/provider.js +21 -6
- package/src/server/conversation/providers/api.js +268 -107
- package/src/server/conversation/providers/claude.js +29 -6
- package/src/server/conversation/providers/claudeDriver.js +325 -250
- package/src/server/conversation/providers/codex.js +69 -33
- package/src/server/conversation/providers/codexDriver.js +503 -1081
- package/src/server/conversation/providers/codexDriverConnection.js +331 -0
- package/src/server/conversation/providers/codexDriverTurns.js +464 -0
- package/src/server/conversation/providers/opencode.js +30 -4
- package/src/server/conversation/providers/opencodeDriver.js +321 -659
- package/src/server/conversation/providers/opencodeStandalone.js +475 -0
- package/src/server/conversation/providers/scoped.js +64 -19
- package/src/server/conversation/runtime.js +171 -2863
- package/src/server/conversation/storage.js +91 -50
- package/src/server/conversation/streams.js +15 -2
- package/src/server/conversation/structuredOutput.js +7 -11
- package/src/server/conversation/systemPrompt.js +13 -1
- package/src/server/conversation/tools.js +47 -15
- package/src/server/conversation/transcript.js +49 -20
- package/src/server/hosts/claudeTurn.js +2 -4
- package/src/server/hosts/codexConfiguration.js +6 -8
- package/src/server/hosts/codexEvents.js +4 -2
- package/src/server/hosts/codexProcess.js +1 -1
- package/src/server/hosts/codexProvider.js +11 -9
- package/src/server/hosts/codexTurn.js +3 -3
- package/src/server/hosts/openCodeClient.js +4 -2
- package/src/server/index.js +0 -2
- package/src/server/lib/aiConnectionClient.js +35 -25
- package/src/server/lib/ndjson.js +1 -0
- package/src/server/lib/providers/anthropicClient.js +15 -6
- package/src/server/lib/providers/common.js +10 -6
- package/src/server/lib/providers/openAiCompatibleClient.js +40 -23
- package/src/server/lib/serviceToolCatalog.js +29 -283
- package/src/server/lib/serviceToolEntries.js +270 -0
- package/src/server/types.js +78 -0
- package/src/shared/assistantResource.js +0 -23
- package/src/shared/conversation/answerChoiceSugar.js +2 -14
- package/src/shared/conversation/messageInputSugar.js +18 -0
- package/src/shared/conversation/numberedQuestionSugar.js +6 -16
- package/src/shared/conversation/pagination.js +1 -1
- package/src/shared/conversation/questions.js +1 -0
- package/src/shared/conversation/streaming.js +1 -0
- package/src/shared/index.js +0 -1
- package/src/testing/conversationStorageContract.js +20 -0
- package/test/aiConnectionClient.test.js +42 -1
- package/test/apiConversationDriver.test.js +217 -38
- package/test/assistantBoundedToolLoop.test.js +1 -1
- package/test/claudeConversationSession.test.js +15 -11
- package/test/claudeInstructionResume.test.js +1 -1
- package/test/claudeProcess.test.js +9 -9
- package/test/claudeTurn.test.js +21 -8
- package/test/codexAccountChange.test.js +5 -3
- package/test/codexConversationSession.test.js +22 -21
- package/test/codexEvents.test.js +3 -1
- package/test/codexHelperDeletion.test.js +3 -7
- package/test/codexIsolation.test.js +6 -6
- package/test/codexModelCatalog.test.js +6 -7
- package/test/codexProcess.test.js +3 -3
- package/test/codexThreadSelection.test.js +4 -1
- package/test/codexTurn.test.js +29 -20
- package/test/conversationApiFacilities.test.js +11 -9
- package/test/conversationAttachments.test.js +3 -3
- package/test/conversationConfiguration.test.js +4 -4
- package/test/conversationNativeStorage.test.js +11 -10
- package/test/conversationReplacement.test.js +13 -13
- package/test/conversationRuntime.test.js +9 -5
- package/test/conversationSession.test.js +78 -22
- package/test/conversationTools.test.js +47 -26
- package/test/fileConversationStorage.test.js +10 -3
- package/test/fixtures/codexHelperThreadLedger.js +3 -3
- package/test/frameworkActions.test.js +33 -18
- package/test/localConversationExecution.test.js +4 -4
- package/test/localExecution.test.js +1 -1
- package/test/openCodeConnectionChange.test.js +3 -2
- package/test/openCodeRuntime.test.js +8 -7
- package/test/openCodeTools.test.js +1 -1
- package/test/opencodeConversationSession.test.js +4 -3
- package/test/serviceToolCatalog.test.js +3 -8
- package/src/server/lib/assistantCompletion.js +0 -190
- package/src/server/lib/assistantToolLoop.js +0 -259
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
import { pathToFileURL } from "node:url";
|
|
2
2
|
import { Buffer } from "node:buffer";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { normalizeObject } from "@jskit-ai/kernel/shared/support/normalize";
|
|
5
5
|
import { normalizeNullableText } from "./normalize.js";
|
|
6
|
-
import { createNativeHistoryExport, readStoppedNativeDatabase, retireNativeConversation } from "./nativeHistoryExport.js";
|
|
7
6
|
|
|
8
7
|
const OPENCODE_RESPONSE_LIMIT_BYTES = 2 * 1024 * 1024;
|
|
9
8
|
const OPENCODE_CATALOG_LIMIT_BYTES = 32 * 1024 * 1024;
|
|
10
9
|
const OPENCODE_EVENT_LIMIT_BYTES = 2 * 1024 * 1024;
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
/** An event reader whose caller does not wait for the stream to open. */
|
|
12
|
+
function reportNoReadiness() {}
|
|
14
13
|
|
|
15
14
|
function openCodeServerError(message, {
|
|
16
15
|
body = null,
|
|
@@ -69,7 +68,7 @@ function parsedJson(value = "") {
|
|
|
69
68
|
}
|
|
70
69
|
|
|
71
70
|
function responseErrorMessage(payload = null, fallback = "OpenCode rejected the request.") {
|
|
72
|
-
return
|
|
71
|
+
return normalizeNullableText(
|
|
73
72
|
payload?.message ||
|
|
74
73
|
payload?.error?.message ||
|
|
75
74
|
payload?.error ||
|
|
@@ -89,7 +88,7 @@ function queryString(values = {}) {
|
|
|
89
88
|
}
|
|
90
89
|
|
|
91
90
|
function sessionPath(sessionId = "", suffix = "") {
|
|
92
|
-
const id = encodeURIComponent(
|
|
91
|
+
const id = encodeURIComponent(normalizeNullableText(sessionId));
|
|
93
92
|
if (!id) {
|
|
94
93
|
throw new TypeError("OpenCode session requests require a session id.");
|
|
95
94
|
}
|
|
@@ -97,7 +96,7 @@ function sessionPath(sessionId = "", suffix = "") {
|
|
|
97
96
|
}
|
|
98
97
|
|
|
99
98
|
function stableSessionPath(sessionId = "", suffix = "") {
|
|
100
|
-
const id = encodeURIComponent(
|
|
99
|
+
const id = encodeURIComponent(normalizeNullableText(sessionId));
|
|
101
100
|
if (!id) {
|
|
102
101
|
throw new TypeError("OpenCode session requests require a session id.");
|
|
103
102
|
}
|
|
@@ -114,58 +113,13 @@ function normalizedMessageRows(value = null) {
|
|
|
114
113
|
return {
|
|
115
114
|
...info,
|
|
116
115
|
content: Array.isArray(message.parts) ? message.parts : [],
|
|
117
|
-
type:
|
|
116
|
+
type: normalizeNullableText(info.role)
|
|
118
117
|
};
|
|
119
118
|
});
|
|
120
119
|
}
|
|
121
120
|
|
|
122
|
-
/** Inspect stopped native storage without starting OpenCode or changing its files. */
|
|
123
|
-
async function readOpenCodeConversationDatabase({ databasePath, conversationId, onMessage, signal } = {}) {
|
|
124
|
-
if (!path.isAbsolute(databasePath || "") || !/^ses_[a-zA-Z0-9_]{1,256}$/u.test(conversationId || "") ||
|
|
125
|
-
typeof onMessage !== "function") throw new TypeError("OpenCode inspection requires its exact database, conversation and message reader.");
|
|
126
|
-
const unavailable = () => new Error("OpenCode native storage could not be inspected completely. Keep its writers stopped and inspect its original database before retrying.");
|
|
127
|
-
try {
|
|
128
|
-
return await readStoppedNativeDatabase(databasePath, async database => {
|
|
129
|
-
const session = database.prepare("SELECT * FROM session WHERE id = ?").get(conversationId);
|
|
130
|
-
if (!session || session.id !== conversationId || !path.isAbsolute(session.directory || "")) throw unavailable();
|
|
131
|
-
if (database.prepare("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = 'session_input'").get() &&
|
|
132
|
-
database.prepare("SELECT 1 FROM session_input WHERE session_id = ? AND promoted_seq IS NULL LIMIT 1").get(conversationId)) {
|
|
133
|
-
throw new Error("OpenCode has an unpromoted native request. Inspect it before restoring continuity; no request was repeated.");
|
|
134
|
-
}
|
|
135
|
-
const output = createNativeHistoryExport(async ({ message }) => onMessage(normalizedMessageRows([message])[0]), { signal });
|
|
136
|
-
const parts = database.prepare("SELECT id, message_id, session_id, data FROM part WHERE message_id = ? ORDER BY id");
|
|
137
|
-
for (const row of database.prepare("SELECT id, session_id, data FROM message WHERE session_id = ? ORDER BY time_created, id").iterate(conversationId)) {
|
|
138
|
-
signal?.throwIfAborted();
|
|
139
|
-
if (!/^msg_[a-zA-Z0-9_]{1,256}$/u.test(row.id)) throw unavailable();
|
|
140
|
-
let bytes = Buffer.byteLength(row.data);
|
|
141
|
-
if (bytes > 64 * 1024 * 1024) throw unavailable();
|
|
142
|
-
const data = JSON.parse(row.data);
|
|
143
|
-
if (!data || typeof data !== "object" || Array.isArray(data) || !["user", "assistant"].includes(data.role)) throw unavailable();
|
|
144
|
-
const content = [];
|
|
145
|
-
for (const part of parts.iterate(row.id)) {
|
|
146
|
-
signal?.throwIfAborted();
|
|
147
|
-
bytes += Buffer.byteLength(part.data);
|
|
148
|
-
if (bytes > 64 * 1024 * 1024 || part.session_id !== conversationId || part.message_id !== row.id ||
|
|
149
|
-
!/^prt_[a-zA-Z0-9_]{1,256}$/u.test(part.id)) throw unavailable();
|
|
150
|
-
const value = JSON.parse(part.data);
|
|
151
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) throw unavailable();
|
|
152
|
-
content.push({ ...value, id: part.id, sessionID: part.session_id, messageID: part.message_id });
|
|
153
|
-
}
|
|
154
|
-
// Same original MessageV2 storage projection: native columns own IDs.
|
|
155
|
-
await output.emit({ type: "message", message: { info: { ...data, id: row.id, sessionID: row.session_id }, parts: content }, text: [] });
|
|
156
|
-
}
|
|
157
|
-
return { session, ...output.complete() };
|
|
158
|
-
}, { signal });
|
|
159
|
-
} catch (error) {
|
|
160
|
-
if (signal?.aborted) throw signal.reason;
|
|
161
|
-
if (String(error?.message || "").startsWith("OpenCode ") ||
|
|
162
|
-
String(error?.message || "").startsWith("Native storage ")) throw error;
|
|
163
|
-
throw unavailable();
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
121
|
function providerCapabilityMedia(value = null) {
|
|
168
|
-
const source =
|
|
122
|
+
const source = normalizeObject(value);
|
|
169
123
|
return Object.fromEntries([
|
|
170
124
|
"audio",
|
|
171
125
|
"image",
|
|
@@ -176,8 +130,8 @@ function providerCapabilityMedia(value = null) {
|
|
|
176
130
|
}
|
|
177
131
|
|
|
178
132
|
function sanitizedOpenCodeModel(model = {}, fallbackId = "") {
|
|
179
|
-
const source =
|
|
180
|
-
const id =
|
|
133
|
+
const source = normalizeObject(model);
|
|
134
|
+
const id = normalizeNullableText(fallbackId);
|
|
181
135
|
const context = Number(source.limit?.context);
|
|
182
136
|
const output = Number(source.limit?.output);
|
|
183
137
|
return {
|
|
@@ -188,16 +142,16 @@ function sanitizedOpenCodeModel(model = {}, fallbackId = "") {
|
|
|
188
142
|
reasoning: source.capabilities?.reasoning === true,
|
|
189
143
|
toolcall: source.capabilities?.toolcall === true
|
|
190
144
|
},
|
|
191
|
-
family:
|
|
145
|
+
family: normalizeNullableText(source.family),
|
|
192
146
|
id,
|
|
193
147
|
limit: {
|
|
194
148
|
...(Number.isSafeInteger(context) && context > 0 ? { context } : {}),
|
|
195
149
|
...(Number.isSafeInteger(output) && output > 0 ? { output } : {})
|
|
196
150
|
},
|
|
197
|
-
name:
|
|
198
|
-
release_date:
|
|
199
|
-
status:
|
|
200
|
-
variants: Object.fromEntries(Object.keys(
|
|
151
|
+
name: normalizeNullableText(source.name) || id,
|
|
152
|
+
release_date: normalizeNullableText(source.release_date),
|
|
153
|
+
status: normalizeNullableText(source.status),
|
|
154
|
+
variants: Object.fromEntries(Object.keys(normalizeObject(source.variants))
|
|
201
155
|
.filter(Boolean)
|
|
202
156
|
.map((variantId) => [variantId, {}]))
|
|
203
157
|
};
|
|
@@ -213,17 +167,17 @@ function invalidOpenCodeCatalogError() {
|
|
|
213
167
|
}
|
|
214
168
|
|
|
215
169
|
function sanitizedOpenCodeProviderCatalog(value = null) {
|
|
216
|
-
const source =
|
|
170
|
+
const source = normalizeObject(value);
|
|
217
171
|
if (!Array.isArray(source.all) || source.all.length === 0) {
|
|
218
172
|
throw invalidOpenCodeCatalogError();
|
|
219
173
|
}
|
|
220
174
|
const all = source.all.map((provider) => {
|
|
221
|
-
const candidate =
|
|
222
|
-
const id =
|
|
175
|
+
const candidate = normalizeObject(provider);
|
|
176
|
+
const id = normalizeNullableText(candidate.id);
|
|
223
177
|
const environmentNames = Array.isArray(candidate.env)
|
|
224
|
-
? candidate.env.map(
|
|
178
|
+
? candidate.env.map(normalizeNullableText).filter(Boolean)
|
|
225
179
|
: [];
|
|
226
|
-
const models =
|
|
180
|
+
const models = normalizeObject(candidate.models);
|
|
227
181
|
return {
|
|
228
182
|
apiKeyCompatible: environmentNames.length === 1 || [
|
|
229
183
|
"amazon-bedrock",
|
|
@@ -231,28 +185,28 @@ function sanitizedOpenCodeProviderCatalog(value = null) {
|
|
|
231
185
|
].includes(id),
|
|
232
186
|
id,
|
|
233
187
|
models: Object.fromEntries(Object.entries(models)
|
|
234
|
-
.map(([modelId, model]) => [
|
|
188
|
+
.map(([modelId, model]) => [normalizeNullableText(modelId), sanitizedOpenCodeModel(model, modelId)])
|
|
235
189
|
.filter(([modelId, model]) => modelId && model.id)),
|
|
236
|
-
name:
|
|
190
|
+
name: normalizeNullableText(candidate.name) || id
|
|
237
191
|
};
|
|
238
192
|
}).filter((provider) => provider.id);
|
|
239
193
|
if (all.length === 0) {
|
|
240
194
|
throw invalidOpenCodeCatalogError();
|
|
241
195
|
}
|
|
242
|
-
const defaults =
|
|
196
|
+
const defaults = normalizeObject(source.default);
|
|
243
197
|
return {
|
|
244
198
|
all,
|
|
245
199
|
default: Object.fromEntries(Object.entries(defaults)
|
|
246
|
-
.map(([providerId, modelId]) => [
|
|
200
|
+
.map(([providerId, modelId]) => [normalizeNullableText(providerId), normalizeNullableText(modelId)])
|
|
247
201
|
.filter(([providerId, modelId]) => providerId && modelId))
|
|
248
202
|
};
|
|
249
203
|
}
|
|
250
204
|
|
|
251
205
|
function stablePromptBody(input = {}) {
|
|
252
|
-
const model = input
|
|
253
|
-
const modelID =
|
|
254
|
-
const providerID =
|
|
255
|
-
const prompt = typeof input
|
|
206
|
+
const model = input.model && typeof input.model === "object" ? input.model : {};
|
|
207
|
+
const modelID = normalizeNullableText(model.modelID || model.id);
|
|
208
|
+
const providerID = normalizeNullableText(model.providerID);
|
|
209
|
+
const prompt = typeof input.prompt?.text === "string" ? input.prompt.text : "";
|
|
256
210
|
const content = (input.prompt?.content || []).map(part => {
|
|
257
211
|
if (part.type === "text" && typeof part.text === "string") return { type: "text", text: part.text };
|
|
258
212
|
if (part.type === "image" && part.image instanceof Uint8Array && ["image/png", "image/jpeg", "image/webp", "image/gif"].includes(part.mediaType)) {
|
|
@@ -264,8 +218,8 @@ function stablePromptBody(input = {}) {
|
|
|
264
218
|
throw new TypeError("OpenCode prompt requests require text or attachment content.");
|
|
265
219
|
}
|
|
266
220
|
return {
|
|
267
|
-
...(
|
|
268
|
-
...(
|
|
221
|
+
...(normalizeNullableText(input.agent) ? { agent: normalizeNullableText(input.agent) } : {}),
|
|
222
|
+
...(normalizeNullableText(input.id) ? { messageID: normalizeNullableText(input.id) } : {}),
|
|
269
223
|
...(modelID && providerID ? { model: { modelID, providerID } } : {}),
|
|
270
224
|
parts: [...(prompt ? [{ text: prompt, type: "text" }] : []), ...content, ...(input.attachments || [])
|
|
271
225
|
.filter((attachment) => attachment.contentType?.startsWith("image/"))
|
|
@@ -273,7 +227,7 @@ function stablePromptBody(input = {}) {
|
|
|
273
227
|
type: "file", mime: attachment.contentType, filename: attachment.fileName,
|
|
274
228
|
url: pathToFileURL(attachment.path).href
|
|
275
229
|
}))],
|
|
276
|
-
...(
|
|
230
|
+
...(normalizeNullableText(model.variant) ? { variant: normalizeNullableText(model.variant) } : {})
|
|
277
231
|
};
|
|
278
232
|
}
|
|
279
233
|
|
|
@@ -282,7 +236,7 @@ function eventSessionId(value = null) {
|
|
|
282
236
|
const properties = payload.properties && typeof payload.properties === "object"
|
|
283
237
|
? payload.properties
|
|
284
238
|
: {};
|
|
285
|
-
return
|
|
239
|
+
return normalizeNullableText(
|
|
286
240
|
payload.sessionID ||
|
|
287
241
|
properties.sessionID ||
|
|
288
242
|
properties.info?.sessionID ||
|
|
@@ -298,10 +252,21 @@ function decodeOpenCodeEventData(value = "") {
|
|
|
298
252
|
return parsedJson(first) ?? first;
|
|
299
253
|
}
|
|
300
254
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
255
|
+
/**
|
|
256
|
+
* OpenCode loads one instance per directory and keeps it, with the credentials it
|
|
257
|
+
* read, until that directory is disposed. Every client derived from one server
|
|
258
|
+
* shares the record of the directories it has addressed; `defaultDirectory` is
|
|
259
|
+
* the server's working directory, which requests without a directory reach.
|
|
260
|
+
* @param {object} [options]
|
|
261
|
+
* @param {boolean} [options.allowAttachmentDirectories]
|
|
262
|
+
* @param {string} [options.baseUrl]
|
|
263
|
+
* @param {string} [options.defaultDirectory]
|
|
264
|
+
* @param {string} [options.directory]
|
|
265
|
+
* @param {typeof fetch} [options.fetchImpl]
|
|
266
|
+
* @param {{ addressed: Map<string, number>, sequence: number }} [options.instances]
|
|
267
|
+
* @param {string} [options.password]
|
|
268
|
+
* @param {string} [options.username]
|
|
269
|
+
*/
|
|
305
270
|
function createOpenCodeServerClient({
|
|
306
271
|
allowAttachmentDirectories = false,
|
|
307
272
|
baseUrl = "",
|
|
@@ -316,14 +281,15 @@ function createOpenCodeServerClient({
|
|
|
316
281
|
if (origin.protocol !== "http:" || !["127.0.0.1", "localhost", "[::1]"].includes(origin.hostname)) {
|
|
317
282
|
throw new TypeError("OpenCode bridge clients require a loopback HTTP server.");
|
|
318
283
|
}
|
|
284
|
+
// readable-check: allow method-probe - input validation: the fetch implementation a host hands the OpenCode client
|
|
319
285
|
if (typeof fetchImpl !== "function") {
|
|
320
286
|
throw new TypeError("OpenCode bridge clients require fetch().");
|
|
321
287
|
}
|
|
322
|
-
const authorization = `Basic ${Buffer.from(`${
|
|
323
|
-
const scopedDirectory =
|
|
288
|
+
const authorization = `Basic ${Buffer.from(`${normalizeNullableText(username) || "opencode"}:${String(password)}`).toString("base64")}`;
|
|
289
|
+
const scopedDirectory = normalizeNullableText(directory);
|
|
324
290
|
|
|
325
291
|
function instanceDirectory(requestDirectory = scopedDirectory) {
|
|
326
|
-
const value =
|
|
292
|
+
const value = normalizeNullableText(requestDirectory) || normalizeNullableText(defaultDirectory);
|
|
327
293
|
return value ? path.resolve(value) : "";
|
|
328
294
|
}
|
|
329
295
|
|
|
@@ -397,7 +363,7 @@ function createOpenCodeServerClient({
|
|
|
397
363
|
return `/session/${encodeURIComponent(conversationId)}`;
|
|
398
364
|
}
|
|
399
365
|
|
|
400
|
-
async function *events(sessionId = "", { onReady =
|
|
366
|
+
async function *events(sessionId = "", { onReady = reportNoReadiness, signal } = {}) {
|
|
401
367
|
const requestPath = "/event";
|
|
402
368
|
addressInstance(requestPath, scopedDirectory);
|
|
403
369
|
const response = await fetchImpl(new URL(requestPath, origin), {
|
|
@@ -415,7 +381,7 @@ function createOpenCodeServerClient({
|
|
|
415
381
|
status: response.status
|
|
416
382
|
});
|
|
417
383
|
}
|
|
418
|
-
onReady
|
|
384
|
+
onReady();
|
|
419
385
|
const decoder = new TextDecoder();
|
|
420
386
|
const reader = response.body.getReader();
|
|
421
387
|
let buffered = "";
|
|
@@ -442,7 +408,7 @@ function createOpenCodeServerClient({
|
|
|
442
408
|
if (next.done) {
|
|
443
409
|
const finalEvent = dispatch();
|
|
444
410
|
const finalSessionId = eventSessionId(finalEvent?.data);
|
|
445
|
-
if (finalEvent && (!finalSessionId || finalSessionId ===
|
|
411
|
+
if (finalEvent && (!finalSessionId || finalSessionId === normalizeNullableText(sessionId))) {
|
|
446
412
|
yield finalEvent;
|
|
447
413
|
}
|
|
448
414
|
return;
|
|
@@ -455,7 +421,7 @@ function createOpenCodeServerClient({
|
|
|
455
421
|
if (!line) {
|
|
456
422
|
const value = dispatch();
|
|
457
423
|
const valueSessionId = eventSessionId(value?.data);
|
|
458
|
-
if (value && (!valueSessionId || valueSessionId ===
|
|
424
|
+
if (value && (!valueSessionId || valueSessionId === normalizeNullableText(sessionId))) {
|
|
459
425
|
yield value;
|
|
460
426
|
}
|
|
461
427
|
} else if (!line.startsWith(":")) {
|
|
@@ -490,203 +456,253 @@ function createOpenCodeServerClient({
|
|
|
490
456
|
}
|
|
491
457
|
}
|
|
492
458
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
459
|
+
async function allowConversationAttachments(conversationId, attachments) {
|
|
460
|
+
const route = storageConversationPath(conversationId);
|
|
461
|
+
const { data: session } = await requestStorageResponse(route);
|
|
462
|
+
if (session?.id !== conversationId) throw new Error("OpenCode returned another conversation.");
|
|
463
|
+
const permission = [...(session.permission || [])];
|
|
464
|
+
const previousLength = permission.length;
|
|
465
|
+
for (const attachment of attachments) {
|
|
466
|
+
const pattern = path.join(path.dirname(attachment.path), "*").replaceAll("\\", "/");
|
|
467
|
+
const rule = permission.findLast((item) => item.permission === "external_directory" && item.pattern === pattern);
|
|
468
|
+
if (rule?.action !== "allow") permission.push({ permission: "external_directory", pattern, action: "allow" });
|
|
469
|
+
}
|
|
470
|
+
if (permission.length !== previousLength) {
|
|
471
|
+
await requestStorageResponse(route, { body: { permission } });
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async function listConversationChildren(conversationId, { signal } = {}) {
|
|
476
|
+
const children = await storageInventory(`${storageConversationPath(conversationId)}/children`, { signal });
|
|
477
|
+
if (children.some((child) => child.parentID !== conversationId)) {
|
|
478
|
+
throw new Error("OpenCode returned an incomplete or invalid child inventory.");
|
|
479
|
+
}
|
|
480
|
+
return children;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
async function listConversationsForDirectory(directory, { signal } = {}) {
|
|
484
|
+
if (!path.isAbsolute(directory || "")) throw new TypeError("OpenCode inventory requires an absolute native directory.");
|
|
485
|
+
// Use the global persisted inventory: the project-scoped /session
|
|
486
|
+
// listing loses its Git project identity once archived source is gone.
|
|
487
|
+
// Request one beyond our limit to detect a truncated inventory.
|
|
488
|
+
const rows = await storageInventory(`/experimental/session?${new URLSearchParams({ directory, limit: "1001" })}`, { signal });
|
|
489
|
+
if (rows.some((row) => typeof row.directory !== "string")) throw new Error("OpenCode inventory has no native directory.");
|
|
490
|
+
return rows.filter((row) => row.directory === directory);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
async function readConversationStorage(conversationId, { signal } = {}) {
|
|
494
|
+
// The native storage record does not resolve a live source workspace.
|
|
495
|
+
const { data } = await requestStorageResponse(storageConversationPath(conversationId), { signal });
|
|
496
|
+
if (data?.id !== conversationId || !path.isAbsolute(data.directory || "")) {
|
|
497
|
+
throw new Error("OpenCode returned an invalid native conversation record.");
|
|
498
|
+
}
|
|
499
|
+
return data;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
async function readConversationStoragePage(conversationId, { before = "", signal } = {}) {
|
|
503
|
+
const route = `${storageConversationPath(conversationId)}/message`;
|
|
504
|
+
if (typeof before !== "string" || before.length > 8192) throw new TypeError("Invalid OpenCode storage cursor.");
|
|
505
|
+
const query = new URLSearchParams({ limit: "1", ...(before ? { before } : {}) });
|
|
506
|
+
const { data, headers } = await requestStorageResponse(`${route}?${query}`, {
|
|
507
|
+
signal, maxBytes: 64 * 1024 * 1024
|
|
508
|
+
});
|
|
509
|
+
const nextCursor = headers.get("x-next-cursor");
|
|
510
|
+
if (!nextCursor && /rel="next"/u.test(headers.get("link") || "")) {
|
|
511
|
+
throw new Error("OpenCode omitted its native history continuation cursor.");
|
|
512
|
+
}
|
|
513
|
+
return { data, nextCursor };
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
async function agents({ directory = "", signal } = {}) {
|
|
517
|
+
return request("GET", `/agent${queryString({ directory })}`, {
|
|
518
|
+
limitBytes: OPENCODE_CATALOG_LIMIT_BYTES,
|
|
519
|
+
signal
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
async function authenticateApiKey(modelProviderId = "", apiKey = "", { signal } = {}) {
|
|
524
|
+
const providerId = encodeURIComponent(normalizeNullableText(modelProviderId));
|
|
525
|
+
if (!providerId || !String(apiKey)) {
|
|
526
|
+
throw new TypeError("OpenCode API-key authentication requires a provider id and key.");
|
|
527
|
+
}
|
|
528
|
+
return request("PUT", `/auth/${providerId}`, {
|
|
529
|
+
body: { key: String(apiKey), type: "api" },
|
|
530
|
+
signal
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
async function createSession(input = {}, { signal } = {}) {
|
|
535
|
+
return (await request("POST", "/api/session", { body: input, signal }))?.data || null;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
async function deleteSession(sessionId = "", { signal } = {}) {
|
|
539
|
+
return request("DELETE", `/session/${encodeURIComponent(normalizeNullableText(sessionId))}`, { signal });
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
async function deleteMessage(sessionId, messageId, { signal } = {}) {
|
|
543
|
+
if (!normalizeNullableText(messageId)) throw new TypeError("OpenCode message deletion requires a message id.");
|
|
544
|
+
return request("DELETE", stableSessionPath(sessionId, `/message/${encodeURIComponent(normalizeNullableText(messageId))}`), { signal });
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* The next request to this client's directory loads a new instance, which
|
|
549
|
+
* reads the server's current credentials. A turn in flight there is aborted.
|
|
550
|
+
*/
|
|
551
|
+
async function disposeInstance({ signal } = {}) {
|
|
552
|
+
const instance = instanceDirectory();
|
|
553
|
+
const sequence = instances.sequence;
|
|
554
|
+
await request("POST", "/instance/dispose", { signal });
|
|
555
|
+
// A request that reached the directory during disposal may have loaded it again.
|
|
556
|
+
if (instance && (instances.addressed.get(instance) ?? 0) <= sequence) instances.addressed.delete(instance);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function forDirectory(nextDirectory = "") {
|
|
560
|
+
const normalizedDirectory = normalizeNullableText(nextDirectory);
|
|
561
|
+
if (!normalizedDirectory) {
|
|
562
|
+
throw new TypeError("OpenCode project clients require a working directory.");
|
|
563
|
+
}
|
|
564
|
+
return createOpenCodeServerClient({
|
|
565
|
+
allowAttachmentDirectories,
|
|
566
|
+
baseUrl: origin.toString(),
|
|
567
|
+
defaultDirectory,
|
|
568
|
+
directory: normalizedDirectory,
|
|
569
|
+
fetchImpl,
|
|
570
|
+
instances,
|
|
571
|
+
password,
|
|
572
|
+
username
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/** Absolute directories whose loaded instances may hold older credentials. */
|
|
577
|
+
function instanceDirectories() {
|
|
578
|
+
return [...instances.addressed.keys()];
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
async function health({ signal } = {}) {
|
|
582
|
+
return request("GET", "/global/health", { signal });
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
async function prepareDirectory({ signal } = {}) {
|
|
586
|
+
// Global health and the v2 session routes do not initialize the directory
|
|
587
|
+
// used by prompt/abort. Complete that native startup before exposing a
|
|
588
|
+
// conversation whose first Stop would otherwise wait for plugin installs.
|
|
589
|
+
const deadline = AbortSignal.timeout(30_000);
|
|
590
|
+
const boundedSignal = signal ? AbortSignal.any([signal, deadline]) : deadline;
|
|
591
|
+
boundedSignal.throwIfAborted();
|
|
592
|
+
const result = await request("GET", "/path", { signal: boundedSignal });
|
|
593
|
+
boundedSignal.throwIfAborted();
|
|
594
|
+
if (!normalizeNullableText(result?.directory)) {
|
|
595
|
+
throw openCodeServerError("OpenCode did not confirm its conversation directory is ready.", {
|
|
596
|
+
code: "assistant_opencode_directory_unavailable", method: "GET", path: "/path"
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
async function interrupt(sessionId = "", { signal } = {}) {
|
|
602
|
+
return request("POST", stableSessionPath(sessionId, "/abort"), { signal });
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
async function sessionStatus(sessionId = "", { signal } = {}) {
|
|
606
|
+
const statuses = await request("GET", "/session/status", { signal });
|
|
607
|
+
return statuses?.[normalizeNullableText(sessionId)] || { type: "idle" };
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
async function messages(sessionId = "", input = {}, { signal } = {}) {
|
|
611
|
+
// A limited read is the newest page; `before` continues with older rows.
|
|
612
|
+
const { source, headers } = await requestResponse(
|
|
613
|
+
"GET",
|
|
614
|
+
`${stableSessionPath(sessionId, "/message")}${queryString({ limit: input.limit, before: input.before })}`,
|
|
615
|
+
{ signal }
|
|
616
|
+
);
|
|
617
|
+
const nextCursor = headers.get("x-next-cursor");
|
|
618
|
+
return { data: normalizedMessageRows(parsedJson(source)), ...(nextCursor ? { nextCursor } : {}) };
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
async function prompt(sessionId = "", input = {}, { signal } = {}) {
|
|
622
|
+
const body = stablePromptBody(input);
|
|
623
|
+
if (allowAttachmentDirectories && input.attachments?.length) {
|
|
624
|
+
// Native tools check the parent directory when reopening a supplied file.
|
|
625
|
+
// Keep these exceptions on this conversation, alongside its existing rules.
|
|
626
|
+
const session = await request("GET", stableSessionPath(sessionId), { signal });
|
|
498
627
|
const permission = [...(session.permission || [])];
|
|
499
628
|
const previousLength = permission.length;
|
|
500
|
-
for (const attachment of attachments) {
|
|
629
|
+
for (const attachment of input.attachments) {
|
|
501
630
|
const pattern = path.join(path.dirname(attachment.path), "*").replaceAll("\\", "/");
|
|
502
631
|
const rule = permission.findLast((item) => item.permission === "external_directory" && item.pattern === pattern);
|
|
503
|
-
if (rule?.action !== "allow")
|
|
632
|
+
if (rule?.action !== "allow") {
|
|
633
|
+
permission.push({ permission: "external_directory", pattern, action: "allow" });
|
|
634
|
+
}
|
|
504
635
|
}
|
|
505
636
|
if (permission.length !== previousLength) {
|
|
506
|
-
await
|
|
507
|
-
}
|
|
508
|
-
},
|
|
509
|
-
async listConversationChildren(conversationId, { signal } = {}) {
|
|
510
|
-
const children = await storageInventory(`${storageConversationPath(conversationId)}/children`, { signal });
|
|
511
|
-
if (children.some((child) => child.parentID !== conversationId)) {
|
|
512
|
-
throw new Error("OpenCode returned an incomplete or invalid child inventory.");
|
|
513
|
-
}
|
|
514
|
-
return children;
|
|
515
|
-
},
|
|
516
|
-
async listConversationsForDirectory(directory, { signal } = {}) {
|
|
517
|
-
if (!path.isAbsolute(directory || "")) throw new TypeError("OpenCode inventory requires an absolute native directory.");
|
|
518
|
-
// Use the global persisted inventory: the project-scoped /session
|
|
519
|
-
// listing loses its Git project identity once archived source is gone.
|
|
520
|
-
// Request one beyond our limit to detect a truncated inventory.
|
|
521
|
-
const rows = await storageInventory(`/experimental/session?${new URLSearchParams({ directory, limit: "1001" })}`, { signal });
|
|
522
|
-
if (rows.some((row) => typeof row.directory !== "string")) throw new Error("OpenCode inventory has no native directory.");
|
|
523
|
-
return rows.filter((row) => row.directory === directory);
|
|
524
|
-
},
|
|
525
|
-
async readConversationStorage(conversationId, { signal } = {}) {
|
|
526
|
-
// The native storage record does not resolve a live source workspace.
|
|
527
|
-
const { data } = await requestStorageResponse(storageConversationPath(conversationId), { signal });
|
|
528
|
-
if (data?.id !== conversationId || !path.isAbsolute(data.directory || "")) {
|
|
529
|
-
throw new Error("OpenCode returned an invalid native conversation record.");
|
|
530
|
-
}
|
|
531
|
-
return data;
|
|
532
|
-
},
|
|
533
|
-
async readConversationStoragePage(conversationId, { before = "", signal } = {}) {
|
|
534
|
-
const route = `${storageConversationPath(conversationId)}/message`;
|
|
535
|
-
if (typeof before !== "string" || before.length > 8192) throw new TypeError("Invalid OpenCode storage cursor.");
|
|
536
|
-
const query = new URLSearchParams({ limit: "1", ...(before ? { before } : {}) });
|
|
537
|
-
const { data, headers } = await requestStorageResponse(`${route}?${query}`, {
|
|
538
|
-
signal, maxBytes: 64 * 1024 * 1024
|
|
539
|
-
});
|
|
540
|
-
const nextCursor = headers.get("x-next-cursor");
|
|
541
|
-
if (!nextCursor && /rel="next"/u.test(headers.get("link") || "")) {
|
|
542
|
-
throw new Error("OpenCode omitted its native history continuation cursor.");
|
|
637
|
+
await request("PATCH", stableSessionPath(sessionId), { body: { permission }, signal });
|
|
543
638
|
}
|
|
544
|
-
return { data, nextCursor };
|
|
545
|
-
},
|
|
546
|
-
async agents({ directory = "", signal } = {}) {
|
|
547
|
-
return request("GET", `/agent${queryString({ directory })}`, {
|
|
548
|
-
limitBytes: OPENCODE_CATALOG_LIMIT_BYTES,
|
|
549
|
-
signal
|
|
550
|
-
});
|
|
551
|
-
},
|
|
552
|
-
async authenticateApiKey(modelProviderId = "", apiKey = "", { signal } = {}) {
|
|
553
|
-
const providerId = encodeURIComponent(text(modelProviderId));
|
|
554
|
-
if (!providerId || !String(apiKey)) {
|
|
555
|
-
throw new TypeError("OpenCode API-key authentication requires a provider id and key.");
|
|
556
|
-
}
|
|
557
|
-
return request("PUT", `/auth/${providerId}`, {
|
|
558
|
-
body: { key: String(apiKey), type: "api" },
|
|
559
|
-
signal
|
|
560
|
-
});
|
|
561
|
-
},
|
|
562
|
-
async createSession(input = {}, { signal } = {}) {
|
|
563
|
-
return (await request("POST", "/api/session", { body: input, signal }))?.data || null;
|
|
564
|
-
},
|
|
565
|
-
async deleteSession(sessionId = "", { signal } = {}) {
|
|
566
|
-
return request("DELETE", `/session/${encodeURIComponent(text(sessionId))}`, { signal });
|
|
567
|
-
},
|
|
568
|
-
async deleteMessage(sessionId, messageId, { signal } = {}) {
|
|
569
|
-
if (!text(messageId)) throw new TypeError("OpenCode message deletion requires a message id.");
|
|
570
|
-
return request("DELETE", stableSessionPath(sessionId, `/message/${encodeURIComponent(text(messageId))}`), { signal });
|
|
571
|
-
},
|
|
572
|
-
// The next request to this client's directory loads a new instance, which
|
|
573
|
-
// reads the server's current credentials. A turn in flight there is aborted.
|
|
574
|
-
async disposeInstance({ signal } = {}) {
|
|
575
|
-
const instance = instanceDirectory();
|
|
576
|
-
const sequence = instances.sequence;
|
|
577
|
-
await request("POST", "/instance/dispose", { signal });
|
|
578
|
-
// A request that reached the directory during disposal may have loaded it again.
|
|
579
|
-
if (instance && (instances.addressed.get(instance) ?? 0) <= sequence) instances.addressed.delete(instance);
|
|
580
|
-
},
|
|
581
|
-
events,
|
|
582
|
-
forDirectory(nextDirectory = "") {
|
|
583
|
-
const normalizedDirectory = text(nextDirectory);
|
|
584
|
-
if (!normalizedDirectory) {
|
|
585
|
-
throw new TypeError("OpenCode project clients require a working directory.");
|
|
586
|
-
}
|
|
587
|
-
return createOpenCodeServerClient({
|
|
588
|
-
allowAttachmentDirectories,
|
|
589
|
-
baseUrl: origin.toString(),
|
|
590
|
-
defaultDirectory,
|
|
591
|
-
directory: normalizedDirectory,
|
|
592
|
-
fetchImpl,
|
|
593
|
-
instances,
|
|
594
|
-
password,
|
|
595
|
-
username
|
|
596
|
-
});
|
|
597
|
-
},
|
|
598
|
-
// Absolute directories whose loaded instances may hold older credentials.
|
|
599
|
-
instanceDirectories() {
|
|
600
|
-
return [...instances.addressed.keys()];
|
|
601
|
-
},
|
|
602
|
-
async health({ signal } = {}) {
|
|
603
|
-
return request("GET", "/global/health", { signal });
|
|
604
|
-
},
|
|
605
|
-
async prepareDirectory({ signal } = {}) {
|
|
606
|
-
// Global health and the v2 session routes do not initialize the directory
|
|
607
|
-
// used by prompt/abort. Complete that native startup before exposing a
|
|
608
|
-
// conversation whose first Stop would otherwise wait for plugin installs.
|
|
609
|
-
const deadline = AbortSignal.timeout(30_000);
|
|
610
|
-
const boundedSignal = signal ? AbortSignal.any([signal, deadline]) : deadline;
|
|
611
|
-
boundedSignal.throwIfAborted();
|
|
612
|
-
const result = await request("GET", "/path", { signal: boundedSignal });
|
|
613
|
-
boundedSignal.throwIfAborted();
|
|
614
|
-
if (!text(result?.directory)) {
|
|
615
|
-
throw openCodeServerError("OpenCode did not confirm its conversation directory is ready.", {
|
|
616
|
-
code: "assistant_opencode_directory_unavailable", method: "GET", path: "/path"
|
|
617
|
-
});
|
|
618
|
-
}
|
|
619
|
-
},
|
|
620
|
-
async interrupt(sessionId = "", { signal } = {}) {
|
|
621
|
-
return request("POST", stableSessionPath(sessionId, "/abort"), { signal });
|
|
622
|
-
},
|
|
623
|
-
async sessionStatus(sessionId = "", { signal } = {}) {
|
|
624
|
-
const statuses = await request("GET", "/session/status", { signal });
|
|
625
|
-
return statuses?.[text(sessionId)] || { type: "idle" };
|
|
626
|
-
},
|
|
627
|
-
async messages(sessionId = "", input = {}, { signal } = {}) {
|
|
628
|
-
// A limited read is the newest page; `before` continues with older rows.
|
|
629
|
-
const { source, headers } = await requestResponse(
|
|
630
|
-
"GET",
|
|
631
|
-
`${stableSessionPath(sessionId, "/message")}${queryString({ limit: input.limit, before: input.before })}`,
|
|
632
|
-
{ signal }
|
|
633
|
-
);
|
|
634
|
-
const nextCursor = headers.get("x-next-cursor");
|
|
635
|
-
return { data: normalizedMessageRows(parsedJson(source)), ...(nextCursor ? { nextCursor } : {}) };
|
|
636
|
-
},
|
|
637
|
-
async prompt(sessionId = "", input = {}, { signal } = {}) {
|
|
638
|
-
const body = stablePromptBody(input);
|
|
639
|
-
if (allowAttachmentDirectories && input.attachments?.length) {
|
|
640
|
-
// Native tools check the parent directory when reopening a supplied file.
|
|
641
|
-
// Keep these exceptions on this conversation, alongside its existing rules.
|
|
642
|
-
const session = await request("GET", stableSessionPath(sessionId), { signal });
|
|
643
|
-
const permission = [...(session.permission || [])];
|
|
644
|
-
const previousLength = permission.length;
|
|
645
|
-
for (const attachment of input.attachments) {
|
|
646
|
-
const pattern = path.join(path.dirname(attachment.path), "*").replaceAll("\\", "/");
|
|
647
|
-
const rule = permission.findLast((item) => item.permission === "external_directory" && item.pattern === pattern);
|
|
648
|
-
if (rule?.action !== "allow") {
|
|
649
|
-
permission.push({ permission: "external_directory", pattern, action: "allow" });
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
if (permission.length !== previousLength) {
|
|
653
|
-
await request("PATCH", stableSessionPath(sessionId), { body: { permission }, signal });
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
await request("POST", stableSessionPath(sessionId, "/prompt_async"), {
|
|
657
|
-
body,
|
|
658
|
-
signal
|
|
659
|
-
});
|
|
660
|
-
return {
|
|
661
|
-
delivery: text(input.delivery),
|
|
662
|
-
id: text(input.id),
|
|
663
|
-
sessionID: text(sessionId)
|
|
664
|
-
};
|
|
665
|
-
},
|
|
666
|
-
async providers({ directory = "", signal } = {}) {
|
|
667
|
-
return sanitizedOpenCodeProviderCatalog(await request("GET", `/provider${queryString({ directory })}`, {
|
|
668
|
-
limitBytes: OPENCODE_CATALOG_LIMIT_BYTES,
|
|
669
|
-
signal
|
|
670
|
-
}));
|
|
671
|
-
},
|
|
672
|
-
async readSession(sessionId = "", { signal } = {}) {
|
|
673
|
-
return (await request("GET", sessionPath(sessionId), { signal }))?.data || null;
|
|
674
|
-
},
|
|
675
|
-
async removeAuthentication(modelProviderId = "", { signal } = {}) {
|
|
676
|
-
return request("DELETE", `/auth/${encodeURIComponent(text(modelProviderId))}`, { signal });
|
|
677
|
-
},
|
|
678
|
-
async switchAgent(sessionId = "", agent = "", { signal } = {}) {
|
|
679
|
-
return request("POST", sessionPath(sessionId, "/agent"), {
|
|
680
|
-
body: { agent: text(agent) },
|
|
681
|
-
signal
|
|
682
|
-
});
|
|
683
|
-
},
|
|
684
|
-
async switchModel(sessionId = "", model = {}, { signal } = {}) {
|
|
685
|
-
return request("POST", sessionPath(sessionId, "/model"), {
|
|
686
|
-
body: { model },
|
|
687
|
-
signal
|
|
688
|
-
});
|
|
689
639
|
}
|
|
640
|
+
await request("POST", stableSessionPath(sessionId, "/prompt_async"), {
|
|
641
|
+
body,
|
|
642
|
+
signal
|
|
643
|
+
});
|
|
644
|
+
return {
|
|
645
|
+
delivery: normalizeNullableText(input.delivery),
|
|
646
|
+
id: normalizeNullableText(input.id),
|
|
647
|
+
sessionID: normalizeNullableText(sessionId)
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
async function providers({ directory = "", signal } = {}) {
|
|
652
|
+
return sanitizedOpenCodeProviderCatalog(await request("GET", `/provider${queryString({ directory })}`, {
|
|
653
|
+
limitBytes: OPENCODE_CATALOG_LIMIT_BYTES,
|
|
654
|
+
signal
|
|
655
|
+
}));
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
async function readSession(sessionId = "", { signal } = {}) {
|
|
659
|
+
return (await request("GET", sessionPath(sessionId), { signal }))?.data || null;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
async function removeAuthentication(modelProviderId = "", { signal } = {}) {
|
|
663
|
+
return request("DELETE", `/auth/${encodeURIComponent(normalizeNullableText(modelProviderId))}`, { signal });
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
async function switchAgent(sessionId = "", agent = "", { signal } = {}) {
|
|
667
|
+
return request("POST", sessionPath(sessionId, "/agent"), {
|
|
668
|
+
body: { agent: normalizeNullableText(agent) },
|
|
669
|
+
signal
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
async function switchModel(sessionId = "", model = {}, { signal } = {}) {
|
|
674
|
+
return request("POST", sessionPath(sessionId, "/model"), {
|
|
675
|
+
body: { model },
|
|
676
|
+
signal
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
return Object.freeze({
|
|
681
|
+
allowConversationAttachments,
|
|
682
|
+
listConversationChildren,
|
|
683
|
+
listConversationsForDirectory,
|
|
684
|
+
readConversationStorage,
|
|
685
|
+
readConversationStoragePage,
|
|
686
|
+
agents,
|
|
687
|
+
authenticateApiKey,
|
|
688
|
+
createSession,
|
|
689
|
+
deleteSession,
|
|
690
|
+
deleteMessage,
|
|
691
|
+
disposeInstance,
|
|
692
|
+
events,
|
|
693
|
+
forDirectory,
|
|
694
|
+
instanceDirectories,
|
|
695
|
+
health,
|
|
696
|
+
prepareDirectory,
|
|
697
|
+
interrupt,
|
|
698
|
+
sessionStatus,
|
|
699
|
+
messages,
|
|
700
|
+
prompt,
|
|
701
|
+
providers,
|
|
702
|
+
readSession,
|
|
703
|
+
removeAuthentication,
|
|
704
|
+
switchAgent,
|
|
705
|
+
switchModel
|
|
690
706
|
});
|
|
691
707
|
}
|
|
692
708
|
|
|
@@ -701,97 +717,10 @@ function openCodeAssistantMessageText(message = {}) {
|
|
|
701
717
|
return [...new Set(values)].join("\n\n");
|
|
702
718
|
}
|
|
703
719
|
|
|
704
|
-
// Native preservation uses the storage API independently of the saved worktree.
|
|
705
|
-
// Status still comes from the exact directory-scoped control client.
|
|
706
|
-
async function inspectOpenCodeConversationFamily(storageClient, controlClient, binding, { signal } = {}) {
|
|
707
|
-
const queue = [binding.conversationId];
|
|
708
|
-
const seen = new Set();
|
|
709
|
-
const records = [];
|
|
710
|
-
for (let index = 0; index < queue.length; index += 1) {
|
|
711
|
-
signal?.throwIfAborted();
|
|
712
|
-
const conversationId = queue[index];
|
|
713
|
-
if (seen.has(conversationId) || queue.length > 1000) throw new Error("OpenCode native family is cyclic or exceeds its inventory limit.");
|
|
714
|
-
seen.add(conversationId);
|
|
715
|
-
let native;
|
|
716
|
-
try { native = await storageClient.readConversationStorage(conversationId, { signal }); }
|
|
717
|
-
catch (error) { if (error.statusCode === 404 && index === 0) return []; throw error; }
|
|
718
|
-
if (native?.id !== conversationId || native?.directory !== binding.workdir ||
|
|
719
|
-
(await controlClient.sessionStatus(conversationId, { signal })).type !== "idle") {
|
|
720
|
-
throw new Error("OpenCode retirement requires an idle native family in the exact saved directory.");
|
|
721
|
-
}
|
|
722
|
-
records.push({ conversationId, workdir: native.directory,
|
|
723
|
-
...(Number.isFinite(native.time?.created) ? { createdAt: new Date(native.time.created).toISOString() } : {}),
|
|
724
|
-
...(Number.isFinite(native.time?.updated) ? { updatedAt: new Date(native.time.updated).toISOString() } : {}) });
|
|
725
|
-
const children = await storageClient.listConversationChildren(conversationId, { signal });
|
|
726
|
-
for (const child of children) {
|
|
727
|
-
if (child.directory !== binding.workdir) throw new Error("OpenCode child directory differs from its saved owner.");
|
|
728
|
-
queue.push(child.id);
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
return records.sort((left, right) => left.conversationId.localeCompare(right.conversationId));
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
async function exportOpenCodeNativeHistory(storageClient, controlClient, id, onRecord, { signal: inputSignal } = {}) {
|
|
735
|
-
const deadline = AbortSignal.timeout(300_000);
|
|
736
|
-
const signal = inputSignal ? AbortSignal.any([inputSignal, deadline]) : deadline;
|
|
737
|
-
const output = createNativeHistoryExport(onRecord, { signal });
|
|
738
|
-
const info = await storageClient.readConversationStorage(id, { signal });
|
|
739
|
-
if (info?.id !== id || (await controlClient.sessionStatus(id, { signal })).type !== "idle") {
|
|
740
|
-
throw new Error("OpenCode native export requires the exact idle conversation.");
|
|
741
|
-
}
|
|
742
|
-
await output.emit({ type: "thread", thread: info, text: [] });
|
|
743
|
-
const messageIds = new Set();
|
|
744
|
-
const cursors = new Set();
|
|
745
|
-
let before = "";
|
|
746
|
-
let pages = 0;
|
|
747
|
-
while (true) {
|
|
748
|
-
signal.throwIfAborted();
|
|
749
|
-
if (++pages > 20_000) throw new Error("OpenCode native export exceeded its page limit; history was not retired.");
|
|
750
|
-
const response = await storageClient.readConversationStoragePage(id, { before, signal });
|
|
751
|
-
if (!Array.isArray(response?.data) || response.data.length > 1 ||
|
|
752
|
-
(response.nextCursor !== null && (typeof response.nextCursor !== "string" || !response.nextCursor || response.nextCursor.length > 8192)) ||
|
|
753
|
-
(response.data.length === 0 && response.nextCursor !== null)) {
|
|
754
|
-
throw new Error("OpenCode returned an incomplete or invalid native message page.");
|
|
755
|
-
}
|
|
756
|
-
for (const message of response.data) {
|
|
757
|
-
const info = message?.info;
|
|
758
|
-
if (!/^msg_[a-zA-Z0-9_]{1,256}$/u.test(info?.id) || info.sessionID !== id ||
|
|
759
|
-
!["user", "assistant"].includes(info.role) || !Array.isArray(message.parts) || messageIds.has(info.id)) {
|
|
760
|
-
throw new Error("OpenCode returned an invalid or duplicate native message.");
|
|
761
|
-
}
|
|
762
|
-
messageIds.add(info.id);
|
|
763
|
-
const content = openCodeAssistantMessageText({ content: message.parts });
|
|
764
|
-
await output.emit({ type: "message", message, text: content ? [{ role: info.role, text: content,
|
|
765
|
-
branchId: id, messageId: info.id,
|
|
766
|
-
...(info.parentID ? { parentMessageId: info.parentID } : {}),
|
|
767
|
-
...(Number.isFinite(info.time?.created) ? { createdAt: new Date(info.time.created).toISOString() } : {}),
|
|
768
|
-
...(Number.isFinite(info.time?.completed) ? { completedAt: new Date(info.time.completed).toISOString() } : {}),
|
|
769
|
-
...(info.modelID || info.model?.modelID ? { modelId: info.modelID || info.model.modelID } : {}),
|
|
770
|
-
...(info.providerID || info.model?.providerID ? { modelProviderId: info.providerID || info.model.providerID } : {}),
|
|
771
|
-
...(info.agent ? { agent: info.agent } : {}) }] : [] });
|
|
772
|
-
}
|
|
773
|
-
if (response.nextCursor === null) break;
|
|
774
|
-
if (cursors.has(response.nextCursor)) throw new Error("OpenCode repeated a native history cursor.");
|
|
775
|
-
cursors.add(response.nextCursor);
|
|
776
|
-
before = response.nextCursor;
|
|
777
|
-
}
|
|
778
|
-
return output.complete();
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
/** Retire the exact native family after the host's durable preservation. */
|
|
782
|
-
async function retireOpenCodeConversationHistory(storageClient, controlClient, binding, options = {}) {
|
|
783
|
-
const inspect = () => inspectOpenCodeConversationFamily(storageClient, controlClient, binding, { signal: options.signal });
|
|
784
|
-
return retireNativeConversation({ binding, inspect, beforeDelete: options.beforeDelete,
|
|
785
|
-
readConversation: async (id) => ({ info: await storageClient.readConversationStorage(id), messages: (await controlClient.messages(id)).data }),
|
|
786
|
-
exportConversation: (id, onRecord) => exportOpenCodeNativeHistory(storageClient, controlClient, id, onRecord, { signal: options.signal }),
|
|
787
|
-
remove: () => controlClient.deleteSession(binding.conversationId) });
|
|
788
|
-
}
|
|
789
|
-
|
|
790
720
|
export {
|
|
791
|
-
readOpenCodeConversationDatabase,
|
|
792
|
-
retireOpenCodeConversationHistory,
|
|
793
721
|
openCodeAssistantMessageText,
|
|
794
722
|
OPENCODE_RESPONSE_LIMIT_BYTES,
|
|
795
723
|
createOpenCodeServerClient,
|
|
724
|
+
normalizedMessageRows,
|
|
796
725
|
readBoundedResponse
|
|
797
726
|
};
|