opencode-gateway 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +70 -0
- package/dist/binding/execution.d.ts +24 -0
- package/dist/binding/execution.js +1 -0
- package/dist/binding/gateway.d.ts +71 -0
- package/dist/binding/gateway.js +1 -0
- package/dist/binding/index.d.ts +15 -0
- package/dist/binding/index.js +4 -0
- package/dist/binding/opencode.d.ts +123 -0
- package/dist/binding/opencode.js +1 -0
- package/dist/cli/args.d.ts +9 -0
- package/dist/cli/args.js +53 -0
- package/dist/cli/doctor.d.ts +6 -0
- package/dist/cli/doctor.js +59 -0
- package/dist/cli/init.d.ts +6 -0
- package/dist/cli/init.js +35 -0
- package/dist/cli/opencode-config.d.ts +10 -0
- package/dist/cli/opencode-config.js +62 -0
- package/dist/cli/paths.d.ts +7 -0
- package/dist/cli/paths.js +22 -0
- package/dist/cli/templates.d.ts +1 -0
- package/dist/cli/templates.js +26 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +314 -0
- package/dist/config/cron.d.ts +7 -0
- package/dist/config/cron.js +52 -0
- package/dist/config/gateway.d.ts +26 -0
- package/dist/config/gateway.js +142 -0
- package/dist/config/paths.d.ts +10 -0
- package/dist/config/paths.js +33 -0
- package/dist/config/telegram.d.ts +13 -0
- package/dist/config/telegram.js +91 -0
- package/dist/cron/runtime.d.ts +40 -0
- package/dist/cron/runtime.js +237 -0
- package/dist/delivery/telegram.d.ts +16 -0
- package/dist/delivery/telegram.js +75 -0
- package/dist/delivery/text.d.ts +21 -0
- package/dist/delivery/text.js +175 -0
- package/dist/gateway.d.ts +33 -0
- package/dist/gateway.js +105 -0
- package/dist/host/file-sender.d.ts +16 -0
- package/dist/host/file-sender.js +59 -0
- package/dist/host/noop.d.ts +4 -0
- package/dist/host/noop.js +14 -0
- package/dist/host/transport.d.ts +9 -0
- package/dist/host/transport.js +35 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +52 -0
- package/dist/mailbox/router.d.ts +7 -0
- package/dist/mailbox/router.js +16 -0
- package/dist/media/mime.d.ts +2 -0
- package/dist/media/mime.js +45 -0
- package/dist/opencode/adapter.d.ts +19 -0
- package/dist/opencode/adapter.js +291 -0
- package/dist/opencode/driver-hub.d.ts +15 -0
- package/dist/opencode/driver-hub.js +82 -0
- package/dist/opencode/event-normalize.d.ts +48 -0
- package/dist/opencode/event-normalize.js +48 -0
- package/dist/opencode/event-stream.d.ts +23 -0
- package/dist/opencode/event-stream.js +65 -0
- package/dist/opencode/events.d.ts +2 -0
- package/dist/opencode/events.js +1 -0
- package/dist/questions/client.d.ts +5 -0
- package/dist/questions/client.js +36 -0
- package/dist/questions/format.d.ts +3 -0
- package/dist/questions/format.js +36 -0
- package/dist/questions/normalize.d.ts +10 -0
- package/dist/questions/normalize.js +45 -0
- package/dist/questions/parser.d.ts +11 -0
- package/dist/questions/parser.js +96 -0
- package/dist/questions/runtime.d.ts +53 -0
- package/dist/questions/runtime.js +195 -0
- package/dist/questions/types.d.ts +22 -0
- package/dist/questions/types.js +1 -0
- package/dist/runtime/attachments.d.ts +3 -0
- package/dist/runtime/attachments.js +12 -0
- package/dist/runtime/executor.d.ts +24 -0
- package/dist/runtime/executor.js +188 -0
- package/dist/runtime/mailbox.d.ts +25 -0
- package/dist/runtime/mailbox.js +112 -0
- package/dist/runtime/opencode-runner.d.ts +26 -0
- package/dist/runtime/opencode-runner.js +79 -0
- package/dist/session/context.d.ts +10 -0
- package/dist/session/context.js +44 -0
- package/dist/session/conversation-key.d.ts +3 -0
- package/dist/session/conversation-key.js +3 -0
- package/dist/session/switcher.d.ts +25 -0
- package/dist/session/switcher.js +59 -0
- package/dist/store/migrations.d.ts +2 -0
- package/dist/store/migrations.js +183 -0
- package/dist/store/sqlite.d.ts +127 -0
- package/dist/store/sqlite.js +678 -0
- package/dist/telegram/client.d.ts +35 -0
- package/dist/telegram/client.js +179 -0
- package/dist/telegram/media.d.ts +13 -0
- package/dist/telegram/media.js +65 -0
- package/dist/telegram/normalize.d.ts +47 -0
- package/dist/telegram/normalize.js +119 -0
- package/dist/telegram/poller.d.ts +29 -0
- package/dist/telegram/poller.js +97 -0
- package/dist/telegram/runtime.d.ts +51 -0
- package/dist/telegram/runtime.js +133 -0
- package/dist/telegram/state.d.ts +36 -0
- package/dist/telegram/state.js +128 -0
- package/dist/telegram/types.d.ts +80 -0
- package/dist/telegram/types.js +1 -0
- package/dist/tools/channel-new-session.d.ts +4 -0
- package/dist/tools/channel-new-session.js +27 -0
- package/dist/tools/channel-send-file.d.ts +9 -0
- package/dist/tools/channel-send-file.js +27 -0
- package/dist/tools/channel-target.d.ts +7 -0
- package/dist/tools/channel-target.js +28 -0
- package/dist/tools/cron-list.d.ts +3 -0
- package/dist/tools/cron-list.js +34 -0
- package/dist/tools/cron-remove.d.ts +3 -0
- package/dist/tools/cron-remove.js +12 -0
- package/dist/tools/cron-run.d.ts +3 -0
- package/dist/tools/cron-run.js +20 -0
- package/dist/tools/cron-upsert.d.ts +3 -0
- package/dist/tools/cron-upsert.js +37 -0
- package/dist/tools/gateway-dispatch-cron.d.ts +3 -0
- package/dist/tools/gateway-dispatch-cron.js +33 -0
- package/dist/tools/gateway-status.d.ts +3 -0
- package/dist/tools/gateway-status.js +25 -0
- package/dist/tools/telegram-send-test.d.ts +3 -0
- package/dist/tools/telegram-send-test.js +26 -0
- package/dist/tools/telegram-status.d.ts +3 -0
- package/dist/tools/telegram-status.js +49 -0
- package/dist/tools/time.d.ts +3 -0
- package/dist/tools/time.js +25 -0
- package/dist/utils/error.d.ts +1 -0
- package/dist/utils/error.js +57 -0
- package/generated/wasm/pkg/opencode_gateway_ffi.d.ts +23 -0
- package/generated/wasm/pkg/opencode_gateway_ffi.js +574 -0
- package/generated/wasm/pkg/opencode_gateway_ffi_bg.wasm +0 -0
- package/generated/wasm/pkg/opencode_gateway_ffi_bg.wasm.d.ts +22 -0
- package/package.json +61 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
3
|
+
const SESSION_IDLE_POLL_MS = 250;
|
|
4
|
+
const PROMPT_RESPONSE_TIMEOUT_MS = 90_000;
|
|
5
|
+
export class OpencodeSdkAdapter {
|
|
6
|
+
client;
|
|
7
|
+
directory;
|
|
8
|
+
constructor(client, directory) {
|
|
9
|
+
this.client = client;
|
|
10
|
+
this.directory = directory;
|
|
11
|
+
}
|
|
12
|
+
async createFreshSession(title) {
|
|
13
|
+
const session = await this.client.session.create({
|
|
14
|
+
body: { title },
|
|
15
|
+
query: { directory: this.directory },
|
|
16
|
+
responseStyle: "data",
|
|
17
|
+
throwOnError: true,
|
|
18
|
+
});
|
|
19
|
+
return unwrapData(session).id;
|
|
20
|
+
}
|
|
21
|
+
async execute(command) {
|
|
22
|
+
try {
|
|
23
|
+
switch (command.kind) {
|
|
24
|
+
case "lookupSession":
|
|
25
|
+
return await this.lookupSession(command.sessionId);
|
|
26
|
+
case "createSession":
|
|
27
|
+
return await this.createSession(command.title);
|
|
28
|
+
case "waitUntilIdle":
|
|
29
|
+
return await this.waitUntilIdle(command.sessionId);
|
|
30
|
+
case "appendPrompt":
|
|
31
|
+
return await this.appendPrompt(command);
|
|
32
|
+
case "sendPromptAsync":
|
|
33
|
+
return await this.sendPromptAsync(command);
|
|
34
|
+
case "awaitPromptResponse":
|
|
35
|
+
return await this.awaitPromptResponse(command);
|
|
36
|
+
case "readMessage":
|
|
37
|
+
return await this.readMessage(command);
|
|
38
|
+
case "listMessages":
|
|
39
|
+
return await this.listMessages(command);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
return toErrorResult(command, error);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async lookupSession(sessionId) {
|
|
47
|
+
try {
|
|
48
|
+
await this.client.session.get({
|
|
49
|
+
path: { id: sessionId },
|
|
50
|
+
query: { directory: this.directory },
|
|
51
|
+
responseStyle: "data",
|
|
52
|
+
throwOnError: true,
|
|
53
|
+
});
|
|
54
|
+
return {
|
|
55
|
+
kind: "lookupSession",
|
|
56
|
+
sessionId,
|
|
57
|
+
found: true,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
if (isMissingSessionError(error)) {
|
|
62
|
+
return {
|
|
63
|
+
kind: "lookupSession",
|
|
64
|
+
sessionId,
|
|
65
|
+
found: false,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async createSession(title) {
|
|
72
|
+
return {
|
|
73
|
+
kind: "createSession",
|
|
74
|
+
sessionId: await this.createFreshSession(title),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
async waitUntilIdle(sessionId) {
|
|
78
|
+
for (;;) {
|
|
79
|
+
const statuses = await this.client.session.status({
|
|
80
|
+
query: { directory: this.directory },
|
|
81
|
+
responseStyle: "data",
|
|
82
|
+
throwOnError: true,
|
|
83
|
+
});
|
|
84
|
+
const current = unwrapData(statuses)[sessionId];
|
|
85
|
+
if (!current || current.type === "idle") {
|
|
86
|
+
return {
|
|
87
|
+
kind: "waitUntilIdle",
|
|
88
|
+
sessionId,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
await Bun.sleep(SESSION_IDLE_POLL_MS);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async appendPrompt(command) {
|
|
95
|
+
await this.client.session.prompt({
|
|
96
|
+
path: { id: command.sessionId },
|
|
97
|
+
query: { directory: this.directory },
|
|
98
|
+
body: {
|
|
99
|
+
messageID: command.messageId,
|
|
100
|
+
noReply: true,
|
|
101
|
+
parts: command.parts.map(toSessionPromptPart),
|
|
102
|
+
},
|
|
103
|
+
responseStyle: "data",
|
|
104
|
+
throwOnError: true,
|
|
105
|
+
});
|
|
106
|
+
return {
|
|
107
|
+
kind: "appendPrompt",
|
|
108
|
+
sessionId: command.sessionId,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
async sendPromptAsync(command) {
|
|
112
|
+
await this.client.session.promptAsync({
|
|
113
|
+
path: { id: command.sessionId },
|
|
114
|
+
query: { directory: this.directory },
|
|
115
|
+
body: {
|
|
116
|
+
messageID: command.messageId,
|
|
117
|
+
parts: command.parts.map(toSessionPromptPart),
|
|
118
|
+
},
|
|
119
|
+
throwOnError: true,
|
|
120
|
+
});
|
|
121
|
+
return {
|
|
122
|
+
kind: "sendPromptAsync",
|
|
123
|
+
sessionId: command.sessionId,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
async awaitPromptResponse(command) {
|
|
127
|
+
const deadline = Date.now() + PROMPT_RESPONSE_TIMEOUT_MS;
|
|
128
|
+
for (;;) {
|
|
129
|
+
const messages = await this.client.session.messages({
|
|
130
|
+
path: { id: command.sessionId },
|
|
131
|
+
query: {
|
|
132
|
+
directory: this.directory,
|
|
133
|
+
limit: 64,
|
|
134
|
+
},
|
|
135
|
+
responseStyle: "data",
|
|
136
|
+
throwOnError: true,
|
|
137
|
+
});
|
|
138
|
+
const response = selectAssistantResponse(unwrapData(messages), command.messageId);
|
|
139
|
+
if (response !== null) {
|
|
140
|
+
return {
|
|
141
|
+
kind: "awaitPromptResponse",
|
|
142
|
+
sessionId: command.sessionId,
|
|
143
|
+
messageId: response.info.id,
|
|
144
|
+
parts: response.parts.flatMap(toBindingMessagePart),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
if (Date.now() >= deadline) {
|
|
148
|
+
throw new Error(`assistant message for prompt ${command.messageId} is unavailable after prompt completion`);
|
|
149
|
+
}
|
|
150
|
+
await Bun.sleep(SESSION_IDLE_POLL_MS);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
async readMessage(command) {
|
|
154
|
+
const message = await this.client.session.message({
|
|
155
|
+
path: {
|
|
156
|
+
id: command.sessionId,
|
|
157
|
+
messageID: command.messageId,
|
|
158
|
+
},
|
|
159
|
+
query: { directory: this.directory },
|
|
160
|
+
responseStyle: "data",
|
|
161
|
+
throwOnError: true,
|
|
162
|
+
});
|
|
163
|
+
return {
|
|
164
|
+
kind: "readMessage",
|
|
165
|
+
sessionId: command.sessionId,
|
|
166
|
+
messageId: command.messageId,
|
|
167
|
+
parts: unwrapData(message).parts.flatMap(toBindingMessagePart),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
async listMessages(command) {
|
|
171
|
+
const messages = await this.client.session.messages({
|
|
172
|
+
path: { id: command.sessionId },
|
|
173
|
+
query: {
|
|
174
|
+
directory: this.directory,
|
|
175
|
+
limit: 32,
|
|
176
|
+
},
|
|
177
|
+
responseStyle: "data",
|
|
178
|
+
throwOnError: true,
|
|
179
|
+
});
|
|
180
|
+
return {
|
|
181
|
+
kind: "listMessages",
|
|
182
|
+
sessionId: command.sessionId,
|
|
183
|
+
messages: unwrapData(messages).flatMap(toBindingMessage),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function unwrapData(value) {
|
|
188
|
+
return typeof value === "object" && value !== null && "data" in value ? value.data : value;
|
|
189
|
+
}
|
|
190
|
+
function toSessionPromptPart(part) {
|
|
191
|
+
switch (part.kind) {
|
|
192
|
+
case "text":
|
|
193
|
+
return {
|
|
194
|
+
id: part.partId,
|
|
195
|
+
type: "text",
|
|
196
|
+
text: part.text,
|
|
197
|
+
};
|
|
198
|
+
case "file":
|
|
199
|
+
return {
|
|
200
|
+
id: part.partId,
|
|
201
|
+
type: "file",
|
|
202
|
+
mime: part.mimeType,
|
|
203
|
+
url: pathToFileURL(part.localPath).href,
|
|
204
|
+
filename: part.fileName ?? basename(part.localPath),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function selectAssistantResponse(messages, userMessageId) {
|
|
209
|
+
const assistantChildren = messages.filter(isAssistantChildMessage(userMessageId));
|
|
210
|
+
for (let index = assistantChildren.length - 1; index >= 0; index -= 1) {
|
|
211
|
+
const candidate = assistantChildren[index];
|
|
212
|
+
if (hasVisibleText(candidate)) {
|
|
213
|
+
return candidate;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
for (let index = assistantChildren.length - 1; index >= 0; index -= 1) {
|
|
217
|
+
const candidate = assistantChildren[index];
|
|
218
|
+
if (candidate.info?.finish === "stop" || candidate.info?.error !== undefined) {
|
|
219
|
+
return candidate;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
function isAssistantChildMessage(userMessageId) {
|
|
225
|
+
return (message) => message.info?.role === "assistant" && message.info.parentID === userMessageId;
|
|
226
|
+
}
|
|
227
|
+
function toBindingMessagePart(part) {
|
|
228
|
+
if (typeof part.id !== "string" || typeof part.messageID !== "string" || part.type.length === 0) {
|
|
229
|
+
return [];
|
|
230
|
+
}
|
|
231
|
+
return [
|
|
232
|
+
{
|
|
233
|
+
messageId: part.messageID,
|
|
234
|
+
partId: part.id,
|
|
235
|
+
type: part.type,
|
|
236
|
+
text: typeof part.text === "string" ? part.text : null,
|
|
237
|
+
ignored: part.ignored === true,
|
|
238
|
+
},
|
|
239
|
+
];
|
|
240
|
+
}
|
|
241
|
+
function hasVisibleText(message) {
|
|
242
|
+
return message.parts.some((part) => part.type === "text" && part.ignored !== true && typeof part.text === "string" && part.text.length > 0);
|
|
243
|
+
}
|
|
244
|
+
function toBindingMessage(message) {
|
|
245
|
+
if (typeof message.info?.id !== "string" ||
|
|
246
|
+
typeof message.info.role !== "string" ||
|
|
247
|
+
message.info.role.length === 0) {
|
|
248
|
+
return [];
|
|
249
|
+
}
|
|
250
|
+
return [
|
|
251
|
+
{
|
|
252
|
+
messageId: message.info.id,
|
|
253
|
+
role: message.info.role,
|
|
254
|
+
parentId: typeof message.info.parentID === "string" ? message.info.parentID : null,
|
|
255
|
+
parts: message.parts.flatMap(toBindingMessagePart),
|
|
256
|
+
},
|
|
257
|
+
];
|
|
258
|
+
}
|
|
259
|
+
function toErrorResult(command, error) {
|
|
260
|
+
return {
|
|
261
|
+
kind: "error",
|
|
262
|
+
commandKind: command.kind,
|
|
263
|
+
sessionId: "sessionId" in command ? command.sessionId : null,
|
|
264
|
+
code: isMissingSessionError(error) ? "missingSession" : "unknown",
|
|
265
|
+
message: extractErrorMessage(error),
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
function isMissingSessionError(error) {
|
|
269
|
+
if (typeof error !== "object" || error === null) {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
const name = "name" in error ? error.name : undefined;
|
|
273
|
+
const message = "data" in error ? extractDataMessage(error.data) : null;
|
|
274
|
+
return name === "NotFoundError" && message?.includes("Session not found:") === true;
|
|
275
|
+
}
|
|
276
|
+
function extractDataMessage(value) {
|
|
277
|
+
if (typeof value !== "object" || value === null) {
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
const message = value.message;
|
|
281
|
+
return typeof message === "string" ? message : null;
|
|
282
|
+
}
|
|
283
|
+
function extractErrorMessage(error) {
|
|
284
|
+
if (error instanceof Error && error.message.length > 0) {
|
|
285
|
+
return error.message;
|
|
286
|
+
}
|
|
287
|
+
if (typeof error === "string" && error.length > 0) {
|
|
288
|
+
return error;
|
|
289
|
+
}
|
|
290
|
+
return "OpenCode command failed";
|
|
291
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { OpencodeExecutionDriver } from "../binding";
|
|
2
|
+
import { type OpencodeRuntimeEvent } from "./event-normalize";
|
|
3
|
+
type TextSnapshotHandler = (text: string) => Promise<void> | void;
|
|
4
|
+
export declare class OpencodeEventHub {
|
|
5
|
+
private readonly activeDrivers;
|
|
6
|
+
private nextDriverId;
|
|
7
|
+
registerDriver(sessionId: string, driver: OpencodeExecutionDriver, onPreview: TextSnapshotHandler): {
|
|
8
|
+
dispose(): void;
|
|
9
|
+
updateSession(sessionId: string): void;
|
|
10
|
+
};
|
|
11
|
+
handleEvent(event: OpencodeRuntimeEvent): void;
|
|
12
|
+
private dispatchToSession;
|
|
13
|
+
private publishDirective;
|
|
14
|
+
}
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { normalizeExecutionObservation } from "./event-normalize";
|
|
2
|
+
export class OpencodeEventHub {
|
|
3
|
+
activeDrivers = new Map();
|
|
4
|
+
nextDriverId = 0;
|
|
5
|
+
registerDriver(sessionId, driver, onPreview) {
|
|
6
|
+
const driverId = this.nextDriverId++;
|
|
7
|
+
attachDriver(this.activeDrivers, sessionId, driverId, {
|
|
8
|
+
driver,
|
|
9
|
+
onPreview,
|
|
10
|
+
});
|
|
11
|
+
return {
|
|
12
|
+
dispose: () => {
|
|
13
|
+
detachDriver(this.activeDrivers, sessionId, driverId);
|
|
14
|
+
},
|
|
15
|
+
updateSession: (nextSessionId) => {
|
|
16
|
+
if (nextSessionId === sessionId) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const current = this.activeDrivers.get(sessionId)?.get(driverId);
|
|
20
|
+
if (!current) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
detachDriver(this.activeDrivers, sessionId, driverId);
|
|
24
|
+
attachDriver(this.activeDrivers, nextSessionId, driverId, current);
|
|
25
|
+
sessionId = nextSessionId;
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
handleEvent(event) {
|
|
30
|
+
const observation = normalizeExecutionObservation(event);
|
|
31
|
+
if (observation === null) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if ("sessionId" in observation) {
|
|
35
|
+
this.dispatchToSession(observation.sessionId, observation);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
for (const drivers of this.activeDrivers.values()) {
|
|
39
|
+
for (const driver of drivers.values()) {
|
|
40
|
+
this.publishDirective(driver, driver.driver.observeEvent(observation, monotonicNowMs()));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
dispatchToSession(sessionId, observation) {
|
|
45
|
+
const drivers = this.activeDrivers.get(sessionId);
|
|
46
|
+
if (!drivers) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
for (const driver of drivers.values()) {
|
|
50
|
+
this.publishDirective(driver, driver.driver.observeEvent(observation, monotonicNowMs()));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
publishDirective(driver, directive) {
|
|
54
|
+
if (directive.kind !== "preview" || directive.text === null) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
void Promise.resolve(driver.onPreview(directive.text)).catch(() => {
|
|
58
|
+
// Preview delivery must not break the final response path.
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function monotonicNowMs() {
|
|
63
|
+
return Math.trunc(performance.now());
|
|
64
|
+
}
|
|
65
|
+
function attachDriver(activeDrivers, sessionId, driverId, driver) {
|
|
66
|
+
let drivers = activeDrivers.get(sessionId);
|
|
67
|
+
if (!drivers) {
|
|
68
|
+
drivers = new Map();
|
|
69
|
+
activeDrivers.set(sessionId, drivers);
|
|
70
|
+
}
|
|
71
|
+
drivers.set(driverId, driver);
|
|
72
|
+
}
|
|
73
|
+
function detachDriver(activeDrivers, sessionId, driverId) {
|
|
74
|
+
const drivers = activeDrivers.get(sessionId);
|
|
75
|
+
if (!drivers) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
drivers.delete(driverId);
|
|
79
|
+
if (drivers.size === 0) {
|
|
80
|
+
activeDrivers.delete(sessionId);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { BindingExecutionObservation } from "../binding";
|
|
2
|
+
type MessageInfo = {
|
|
3
|
+
sessionID: string;
|
|
4
|
+
id: string;
|
|
5
|
+
role: string;
|
|
6
|
+
parentID?: string;
|
|
7
|
+
} | {
|
|
8
|
+
sessionID: string;
|
|
9
|
+
id: string;
|
|
10
|
+
role: "assistant";
|
|
11
|
+
parentID: string;
|
|
12
|
+
};
|
|
13
|
+
type MessageUpdatedEvent = {
|
|
14
|
+
type: "message.updated";
|
|
15
|
+
properties: {
|
|
16
|
+
info: MessageInfo;
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
type MessagePart = {
|
|
20
|
+
id: string;
|
|
21
|
+
sessionID: string;
|
|
22
|
+
messageID: string;
|
|
23
|
+
type: string;
|
|
24
|
+
text?: string | null;
|
|
25
|
+
ignored?: boolean;
|
|
26
|
+
};
|
|
27
|
+
type MessagePartUpdatedEvent = {
|
|
28
|
+
type: "message.part.updated";
|
|
29
|
+
properties: {
|
|
30
|
+
part: MessagePart;
|
|
31
|
+
delta?: string;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
type MessagePartDeltaEvent = {
|
|
35
|
+
type: "message.part.delta";
|
|
36
|
+
properties: {
|
|
37
|
+
messageID: string;
|
|
38
|
+
partID: string;
|
|
39
|
+
field: string;
|
|
40
|
+
delta: string;
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
export type OpencodeRuntimeEvent = MessageUpdatedEvent | MessagePartUpdatedEvent | MessagePartDeltaEvent | {
|
|
44
|
+
type: string;
|
|
45
|
+
properties?: unknown;
|
|
46
|
+
};
|
|
47
|
+
export declare function normalizeExecutionObservation(event: OpencodeRuntimeEvent): BindingExecutionObservation | null;
|
|
48
|
+
export {};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export function normalizeExecutionObservation(event) {
|
|
2
|
+
if (isMessageUpdatedEvent(event)) {
|
|
3
|
+
const info = event.properties.info;
|
|
4
|
+
return {
|
|
5
|
+
kind: "messageUpdated",
|
|
6
|
+
sessionId: info.sessionID,
|
|
7
|
+
messageId: info.id,
|
|
8
|
+
role: info.role,
|
|
9
|
+
parentId: typeof info.parentID === "string" ? info.parentID : null,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
if (isMessagePartUpdatedEvent(event)) {
|
|
13
|
+
const part = event.properties.part;
|
|
14
|
+
if (part.type !== "text") {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
kind: "textPartUpdated",
|
|
19
|
+
sessionId: part.sessionID,
|
|
20
|
+
messageId: part.messageID,
|
|
21
|
+
partId: part.id,
|
|
22
|
+
text: typeof part.text === "string" ? part.text : null,
|
|
23
|
+
delta: typeof event.properties.delta === "string" ? event.properties.delta : null,
|
|
24
|
+
ignored: part.ignored === true,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
if (isMessagePartDeltaEvent(event)) {
|
|
28
|
+
if (event.properties.field !== "text" || event.properties.delta.length === 0) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
kind: "textPartDelta",
|
|
33
|
+
messageId: event.properties.messageID,
|
|
34
|
+
partId: event.properties.partID,
|
|
35
|
+
delta: event.properties.delta,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
function isMessageUpdatedEvent(event) {
|
|
41
|
+
return event.type === "message.updated" && typeof event.properties === "object" && event.properties !== null;
|
|
42
|
+
}
|
|
43
|
+
function isMessagePartUpdatedEvent(event) {
|
|
44
|
+
return event.type === "message.part.updated" && typeof event.properties === "object" && event.properties !== null;
|
|
45
|
+
}
|
|
46
|
+
function isMessagePartDeltaEvent(event) {
|
|
47
|
+
return event.type === "message.part.delta" && typeof event.properties === "object" && event.properties !== null;
|
|
48
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { OpencodeClient } from "@opencode-ai/sdk";
|
|
2
|
+
import type { BindingLoggerHost } from "../binding";
|
|
3
|
+
import type { OpencodeEventHub, OpencodeRuntimeEvent } from "./events";
|
|
4
|
+
export declare class OpencodeEventStream {
|
|
5
|
+
private readonly client;
|
|
6
|
+
private readonly directory;
|
|
7
|
+
private readonly hub;
|
|
8
|
+
private readonly consumers;
|
|
9
|
+
private readonly logger;
|
|
10
|
+
private running;
|
|
11
|
+
private connected;
|
|
12
|
+
private lastError;
|
|
13
|
+
constructor(client: OpencodeClient, directory: string, hub: OpencodeEventHub, consumers: OpencodeEventConsumerLike[], logger: BindingLoggerHost);
|
|
14
|
+
isConnected(): boolean;
|
|
15
|
+
lastStreamError(): string | null;
|
|
16
|
+
start(): void;
|
|
17
|
+
stop(): void;
|
|
18
|
+
private runLoop;
|
|
19
|
+
}
|
|
20
|
+
type OpencodeEventConsumerLike = {
|
|
21
|
+
handleEvent(event: OpencodeRuntimeEvent): void;
|
|
22
|
+
};
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { formatError } from "../utils/error";
|
|
2
|
+
const RECONNECT_DELAY_MS = 1_000;
|
|
3
|
+
export class OpencodeEventStream {
|
|
4
|
+
client;
|
|
5
|
+
directory;
|
|
6
|
+
hub;
|
|
7
|
+
consumers;
|
|
8
|
+
logger;
|
|
9
|
+
running = false;
|
|
10
|
+
connected = false;
|
|
11
|
+
lastError = null;
|
|
12
|
+
constructor(client, directory, hub, consumers, logger) {
|
|
13
|
+
this.client = client;
|
|
14
|
+
this.directory = directory;
|
|
15
|
+
this.hub = hub;
|
|
16
|
+
this.consumers = consumers;
|
|
17
|
+
this.logger = logger;
|
|
18
|
+
}
|
|
19
|
+
isConnected() {
|
|
20
|
+
return this.connected;
|
|
21
|
+
}
|
|
22
|
+
lastStreamError() {
|
|
23
|
+
return this.lastError;
|
|
24
|
+
}
|
|
25
|
+
start() {
|
|
26
|
+
if (this.running) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
this.running = true;
|
|
30
|
+
void this.runLoop();
|
|
31
|
+
}
|
|
32
|
+
stop() {
|
|
33
|
+
this.running = false;
|
|
34
|
+
}
|
|
35
|
+
async runLoop() {
|
|
36
|
+
while (this.running) {
|
|
37
|
+
try {
|
|
38
|
+
const events = await this.client.event.subscribe({
|
|
39
|
+
query: { directory: this.directory },
|
|
40
|
+
onSseError: (error) => {
|
|
41
|
+
this.connected = false;
|
|
42
|
+
this.lastError = formatError(error);
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
this.connected = true;
|
|
46
|
+
this.lastError = null;
|
|
47
|
+
for await (const event of events.stream) {
|
|
48
|
+
const runtimeEvent = event;
|
|
49
|
+
this.hub.handleEvent(runtimeEvent);
|
|
50
|
+
for (const consumer of this.consumers) {
|
|
51
|
+
consumer.handleEvent(runtimeEvent);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
this.lastError = formatError(error);
|
|
57
|
+
this.logger.log("warn", `opencode event stream failed: ${this.lastError}`);
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
this.connected = false;
|
|
61
|
+
}
|
|
62
|
+
await Bun.sleep(RECONNECT_DELAY_MS);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { OpencodeEventHub } from "./driver-hub";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { PluginInput } from "@opencode-ai/plugin";
|
|
2
|
+
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client";
|
|
3
|
+
type GatewayPluginClient = PluginInput["client"];
|
|
4
|
+
export declare function createQuestionClient(client: GatewayPluginClient, serverUrl: URL, directory: string): ReturnType<typeof createOpencodeClient>;
|
|
5
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client";
|
|
2
|
+
export function createQuestionClient(client, serverUrl, directory) {
|
|
3
|
+
const snapshot = readClientConfig(client);
|
|
4
|
+
return createOpencodeClient({
|
|
5
|
+
baseUrl: snapshot.baseUrl ?? serverUrl.toString(),
|
|
6
|
+
directory,
|
|
7
|
+
headers: stripManagedHeaders(snapshot.headers),
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
function readClientConfig(client) {
|
|
11
|
+
const configReader = client._client?.getConfig;
|
|
12
|
+
if (typeof configReader !== "function") {
|
|
13
|
+
return {};
|
|
14
|
+
}
|
|
15
|
+
return configReader() ?? {};
|
|
16
|
+
}
|
|
17
|
+
function stripManagedHeaders(headers) {
|
|
18
|
+
if (headers === undefined) {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
const normalized = new Headers(toHeadersInit(headers));
|
|
22
|
+
normalized.delete("x-opencode-directory");
|
|
23
|
+
const result = Object.fromEntries(normalized.entries());
|
|
24
|
+
return Object.keys(result).length === 0 ? undefined : result;
|
|
25
|
+
}
|
|
26
|
+
function toHeadersInit(headers) {
|
|
27
|
+
if (headers instanceof Headers || Array.isArray(headers)) {
|
|
28
|
+
return headers;
|
|
29
|
+
}
|
|
30
|
+
if (typeof headers === "object" && headers !== null) {
|
|
31
|
+
return Object.fromEntries(Object.entries(headers)
|
|
32
|
+
.filter((entry) => typeof entry[1] === "string")
|
|
33
|
+
.map(([key, value]) => [key, value]));
|
|
34
|
+
}
|
|
35
|
+
return {};
|
|
36
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export function formatPlainTextQuestion(request) {
|
|
2
|
+
return [
|
|
3
|
+
"OpenCode needs additional input before it can continue.",
|
|
4
|
+
"",
|
|
5
|
+
...request.questions.flatMap((question, index) => formatQuestionBlock(question, index)),
|
|
6
|
+
formatReplyInstructions(request.questions),
|
|
7
|
+
].join("\n");
|
|
8
|
+
}
|
|
9
|
+
export function formatQuestionReplyError(request, message) {
|
|
10
|
+
return [message, "", formatReplyInstructions(request.questions)].join("\n");
|
|
11
|
+
}
|
|
12
|
+
function formatQuestionBlock(question, index) {
|
|
13
|
+
const label = `Question ${index + 1}: ${question.header}`;
|
|
14
|
+
const options = question.options.length === 0
|
|
15
|
+
? []
|
|
16
|
+
: [
|
|
17
|
+
"Options:",
|
|
18
|
+
...question.options.map((option, optionIndex) => `${optionIndex + 1}. ${option.label} - ${option.description}`),
|
|
19
|
+
];
|
|
20
|
+
return [label, question.question, ...options, ""];
|
|
21
|
+
}
|
|
22
|
+
function formatReplyInstructions(questions) {
|
|
23
|
+
if (questions.length === 1) {
|
|
24
|
+
const question = questions[0];
|
|
25
|
+
const selectionHint = question.multiple
|
|
26
|
+
? "Reply with one line. You may send option numbers or labels separated by commas."
|
|
27
|
+
: "Reply with one line. You may send an option number, an option label, or custom text.";
|
|
28
|
+
return ["How to reply:", `- ${selectionHint}`, "- Reply /cancel to reject this question."].join("\n");
|
|
29
|
+
}
|
|
30
|
+
return [
|
|
31
|
+
"How to reply:",
|
|
32
|
+
"- Reply with one non-empty line per question, in order.",
|
|
33
|
+
"- Each line may use option numbers, option labels, or custom text when allowed.",
|
|
34
|
+
"- Reply /cancel to reject this question.",
|
|
35
|
+
].join("\n");
|
|
36
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { OpencodeRuntimeEvent } from "../opencode/events";
|
|
2
|
+
import type { GatewayQuestionRequest } from "./types";
|
|
3
|
+
export type GatewayQuestionEvent = {
|
|
4
|
+
kind: "asked";
|
|
5
|
+
request: GatewayQuestionRequest;
|
|
6
|
+
} | {
|
|
7
|
+
kind: "resolved";
|
|
8
|
+
requestId: string;
|
|
9
|
+
};
|
|
10
|
+
export declare function normalizeQuestionEvent(event: OpencodeRuntimeEvent): GatewayQuestionEvent | null;
|