opencode-gateway 0.2.3 → 0.2.4
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/dist/cli.js +0 -0
- package/dist/index.js +20907 -52
- package/package.json +1 -1
- package/dist/binding/execution.js +0 -1
- package/dist/binding/gateway.js +0 -1
- package/dist/binding/index.js +0 -4
- package/dist/binding/opencode.js +0 -1
- package/dist/cli/args.js +0 -53
- package/dist/cli/doctor.js +0 -49
- package/dist/cli/init.js +0 -40
- package/dist/cli/opencode-config-file.js +0 -18
- package/dist/cli/opencode-config.js +0 -194
- package/dist/cli/paths.js +0 -22
- package/dist/cli/templates.js +0 -41
- package/dist/config/cron.js +0 -52
- package/dist/config/gateway.js +0 -148
- package/dist/config/memory.js +0 -105
- package/dist/config/paths.js +0 -39
- package/dist/config/telegram.js +0 -91
- package/dist/cron/runtime.js +0 -402
- package/dist/delivery/telegram.js +0 -75
- package/dist/delivery/text.js +0 -175
- package/dist/gateway.js +0 -117
- package/dist/host/file-sender.js +0 -59
- package/dist/host/logger.js +0 -53
- package/dist/host/transport.js +0 -35
- package/dist/mailbox/router.js +0 -16
- package/dist/media/mime.js +0 -45
- package/dist/memory/prompt.js +0 -122
- package/dist/opencode/adapter.js +0 -340
- package/dist/opencode/driver-hub.js +0 -82
- package/dist/opencode/event-normalize.js +0 -48
- package/dist/opencode/event-stream.js +0 -65
- package/dist/opencode/events.js +0 -1
- package/dist/questions/client.js +0 -36
- package/dist/questions/format.js +0 -36
- package/dist/questions/normalize.js +0 -45
- package/dist/questions/parser.js +0 -96
- package/dist/questions/runtime.js +0 -195
- package/dist/questions/types.js +0 -1
- package/dist/runtime/attachments.js +0 -12
- package/dist/runtime/conversation-coordinator.js +0 -22
- package/dist/runtime/executor.js +0 -407
- package/dist/runtime/mailbox.js +0 -112
- package/dist/runtime/opencode-runner.js +0 -79
- package/dist/runtime/runtime-singleton.js +0 -28
- package/dist/session/context.js +0 -23
- package/dist/session/conversation-key.js +0 -3
- package/dist/session/switcher.js +0 -59
- package/dist/session/system-prompt.js +0 -52
- package/dist/store/migrations.js +0 -197
- package/dist/store/sqlite.js +0 -777
- package/dist/telegram/client.js +0 -180
- package/dist/telegram/media.js +0 -65
- package/dist/telegram/normalize.js +0 -119
- package/dist/telegram/poller.js +0 -166
- package/dist/telegram/runtime.js +0 -157
- package/dist/telegram/state.js +0 -149
- package/dist/telegram/types.js +0 -1
- package/dist/tools/channel-new-session.js +0 -27
- package/dist/tools/channel-send-file.js +0 -27
- package/dist/tools/channel-target.js +0 -34
- package/dist/tools/cron-run.js +0 -20
- package/dist/tools/cron-upsert.js +0 -51
- package/dist/tools/gateway-dispatch-cron.js +0 -33
- package/dist/tools/gateway-status.js +0 -25
- package/dist/tools/schedule-cancel.js +0 -12
- package/dist/tools/schedule-format.js +0 -48
- package/dist/tools/schedule-list.js +0 -17
- package/dist/tools/schedule-once.js +0 -43
- package/dist/tools/schedule-status.js +0 -23
- package/dist/tools/telegram-send-test.js +0 -26
- package/dist/tools/telegram-status.js +0 -49
- package/dist/tools/time.js +0 -25
- package/dist/utils/error.js +0 -57
package/dist/memory/prompt.js
DELETED
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
-
import { extname, relative } from "node:path";
|
|
3
|
-
const MARKDOWN_GLOBS = ["**/*.md", "**/*.markdown"];
|
|
4
|
-
const UTF8_TEXT_DECODER = new TextDecoder("utf-8", { fatal: true });
|
|
5
|
-
export class GatewayMemoryPromptProvider {
|
|
6
|
-
config;
|
|
7
|
-
logger;
|
|
8
|
-
constructor(config, logger) {
|
|
9
|
-
this.config = config;
|
|
10
|
-
this.logger = logger;
|
|
11
|
-
}
|
|
12
|
-
async buildPrompt() {
|
|
13
|
-
if (this.config.entries.length === 0) {
|
|
14
|
-
return null;
|
|
15
|
-
}
|
|
16
|
-
const sections = await Promise.all(this.config.entries.map((entry) => this.buildEntrySection(entry)));
|
|
17
|
-
return ["Gateway memory:", ...sections].join("\n\n");
|
|
18
|
-
}
|
|
19
|
-
async buildEntrySection(entry) {
|
|
20
|
-
const lines = [`Configured path: ${entry.displayPath}`, `Description: ${entry.description}`];
|
|
21
|
-
const injectedFiles = await collectInjectedFiles(entry, this.logger);
|
|
22
|
-
for (const file of injectedFiles) {
|
|
23
|
-
lines.push("");
|
|
24
|
-
lines.push(`File: ${file.displayPath}`);
|
|
25
|
-
lines.push(codeFence(file.infoString, file.text));
|
|
26
|
-
}
|
|
27
|
-
return lines.join("\n");
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
async function collectInjectedFiles(entry, logger) {
|
|
31
|
-
if (entry.kind === "file") {
|
|
32
|
-
if (!entry.injectContent) {
|
|
33
|
-
return [];
|
|
34
|
-
}
|
|
35
|
-
const text = await readTextFile(entry.path, logger);
|
|
36
|
-
if (text === null) {
|
|
37
|
-
return [];
|
|
38
|
-
}
|
|
39
|
-
return [
|
|
40
|
-
{
|
|
41
|
-
displayPath: entry.displayPath,
|
|
42
|
-
infoString: inferFenceInfoString(entry.path),
|
|
43
|
-
text,
|
|
44
|
-
},
|
|
45
|
-
];
|
|
46
|
-
}
|
|
47
|
-
const filePaths = new Set();
|
|
48
|
-
if (entry.injectMarkdownContents) {
|
|
49
|
-
for (const pattern of MARKDOWN_GLOBS) {
|
|
50
|
-
addMatchingFiles(filePaths, entry.path, pattern);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
for (const pattern of entry.globs) {
|
|
54
|
-
addMatchingFiles(filePaths, entry.path, pattern);
|
|
55
|
-
}
|
|
56
|
-
const injectedFiles = [];
|
|
57
|
-
for (const filePath of [...filePaths].sort((left, right) => left.localeCompare(right))) {
|
|
58
|
-
const text = await readTextFile(filePath, logger);
|
|
59
|
-
if (text === null) {
|
|
60
|
-
continue;
|
|
61
|
-
}
|
|
62
|
-
injectedFiles.push({
|
|
63
|
-
displayPath: relativeDisplayPath(entry.path, entry.displayPath, filePath),
|
|
64
|
-
infoString: inferFenceInfoString(filePath),
|
|
65
|
-
text,
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
return injectedFiles;
|
|
69
|
-
}
|
|
70
|
-
function addMatchingFiles(result, cwd, pattern) {
|
|
71
|
-
const glob = new Bun.Glob(pattern);
|
|
72
|
-
for (const match of glob.scanSync({ cwd, absolute: true, onlyFiles: true })) {
|
|
73
|
-
result.add(match);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
async function readTextFile(path, logger) {
|
|
77
|
-
let bytes;
|
|
78
|
-
try {
|
|
79
|
-
bytes = await readFile(path);
|
|
80
|
-
}
|
|
81
|
-
catch (error) {
|
|
82
|
-
logger.log("warn", `memory file could not be read and will be skipped: ${path}: ${formatError(error)}`);
|
|
83
|
-
return null;
|
|
84
|
-
}
|
|
85
|
-
let text;
|
|
86
|
-
try {
|
|
87
|
-
text = UTF8_TEXT_DECODER.decode(bytes);
|
|
88
|
-
}
|
|
89
|
-
catch {
|
|
90
|
-
logger.log("warn", `memory file is not valid UTF-8 and will be skipped: ${path}`);
|
|
91
|
-
return null;
|
|
92
|
-
}
|
|
93
|
-
if (text.includes("\u0000")) {
|
|
94
|
-
logger.log("warn", `memory file looks binary and will be skipped: ${path}`);
|
|
95
|
-
return null;
|
|
96
|
-
}
|
|
97
|
-
return text;
|
|
98
|
-
}
|
|
99
|
-
function relativeDisplayPath(rootPath, rootDisplayPath, filePath) {
|
|
100
|
-
const suffix = relative(rootPath, filePath);
|
|
101
|
-
if (suffix.length === 0) {
|
|
102
|
-
return rootDisplayPath;
|
|
103
|
-
}
|
|
104
|
-
return `${rootDisplayPath}/${suffix.replaceAll("\\", "/")}`;
|
|
105
|
-
}
|
|
106
|
-
function inferFenceInfoString(path) {
|
|
107
|
-
const extension = extname(path).slice(1).toLowerCase();
|
|
108
|
-
if (!/^[a-z0-9_+-]+$/.test(extension)) {
|
|
109
|
-
return "";
|
|
110
|
-
}
|
|
111
|
-
return extension;
|
|
112
|
-
}
|
|
113
|
-
function codeFence(infoString, text) {
|
|
114
|
-
const language = infoString.length === 0 ? "" : infoString;
|
|
115
|
-
return [`\`\`\`${language}`, text, "```"].join("\n");
|
|
116
|
-
}
|
|
117
|
-
function formatError(error) {
|
|
118
|
-
if (error instanceof Error && error.message.trim().length > 0) {
|
|
119
|
-
return error.message;
|
|
120
|
-
}
|
|
121
|
-
return String(error);
|
|
122
|
-
}
|
package/dist/opencode/adapter.js
DELETED
|
@@ -1,340 +0,0 @@
|
|
|
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 isSessionBusy(sessionId) {
|
|
22
|
-
const statuses = await this.client.session.status({
|
|
23
|
-
query: { directory: this.directory },
|
|
24
|
-
responseStyle: "data",
|
|
25
|
-
throwOnError: true,
|
|
26
|
-
});
|
|
27
|
-
const current = unwrapData(statuses)[sessionId];
|
|
28
|
-
return current?.type === "busy";
|
|
29
|
-
}
|
|
30
|
-
async abortSession(sessionId) {
|
|
31
|
-
try {
|
|
32
|
-
await this.client.session.abort({
|
|
33
|
-
path: { id: sessionId },
|
|
34
|
-
query: { directory: this.directory },
|
|
35
|
-
responseStyle: "data",
|
|
36
|
-
throwOnError: true,
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
catch (error) {
|
|
40
|
-
if (isMissingSessionError(error)) {
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
throw error;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
async execute(command) {
|
|
47
|
-
try {
|
|
48
|
-
switch (command.kind) {
|
|
49
|
-
case "lookupSession":
|
|
50
|
-
return await this.lookupSession(command.sessionId);
|
|
51
|
-
case "createSession":
|
|
52
|
-
return await this.createSession(command.title);
|
|
53
|
-
case "waitUntilIdle":
|
|
54
|
-
return await this.waitUntilIdle(command.sessionId);
|
|
55
|
-
case "appendPrompt":
|
|
56
|
-
return await this.appendPrompt(command);
|
|
57
|
-
case "sendPromptAsync":
|
|
58
|
-
return await this.sendPromptAsync(command);
|
|
59
|
-
case "awaitPromptResponse":
|
|
60
|
-
return await this.awaitPromptResponse(command);
|
|
61
|
-
case "readMessage":
|
|
62
|
-
return await this.readMessage(command);
|
|
63
|
-
case "listMessages":
|
|
64
|
-
return await this.listMessages(command);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
catch (error) {
|
|
68
|
-
return toErrorResult(command, error);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
async lookupSession(sessionId) {
|
|
72
|
-
try {
|
|
73
|
-
await this.client.session.get({
|
|
74
|
-
path: { id: sessionId },
|
|
75
|
-
query: { directory: this.directory },
|
|
76
|
-
responseStyle: "data",
|
|
77
|
-
throwOnError: true,
|
|
78
|
-
});
|
|
79
|
-
return {
|
|
80
|
-
kind: "lookupSession",
|
|
81
|
-
sessionId,
|
|
82
|
-
found: true,
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
catch (error) {
|
|
86
|
-
if (isMissingSessionError(error)) {
|
|
87
|
-
return {
|
|
88
|
-
kind: "lookupSession",
|
|
89
|
-
sessionId,
|
|
90
|
-
found: false,
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
throw error;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
async createSession(title) {
|
|
97
|
-
return {
|
|
98
|
-
kind: "createSession",
|
|
99
|
-
sessionId: await this.createFreshSession(title),
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
async waitUntilIdle(sessionId) {
|
|
103
|
-
for (;;) {
|
|
104
|
-
const statuses = await this.client.session.status({
|
|
105
|
-
query: { directory: this.directory },
|
|
106
|
-
responseStyle: "data",
|
|
107
|
-
throwOnError: true,
|
|
108
|
-
});
|
|
109
|
-
const current = unwrapData(statuses)[sessionId];
|
|
110
|
-
if (!current || current.type === "idle") {
|
|
111
|
-
return {
|
|
112
|
-
kind: "waitUntilIdle",
|
|
113
|
-
sessionId,
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
await Bun.sleep(SESSION_IDLE_POLL_MS);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
async appendPrompt(command) {
|
|
120
|
-
await this.client.session.prompt({
|
|
121
|
-
path: { id: command.sessionId },
|
|
122
|
-
query: { directory: this.directory },
|
|
123
|
-
body: {
|
|
124
|
-
messageID: command.messageId,
|
|
125
|
-
noReply: true,
|
|
126
|
-
parts: command.parts.map(toSessionPromptPart),
|
|
127
|
-
},
|
|
128
|
-
responseStyle: "data",
|
|
129
|
-
throwOnError: true,
|
|
130
|
-
});
|
|
131
|
-
return {
|
|
132
|
-
kind: "appendPrompt",
|
|
133
|
-
sessionId: command.sessionId,
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
async sendPromptAsync(command) {
|
|
137
|
-
await this.client.session.promptAsync({
|
|
138
|
-
path: { id: command.sessionId },
|
|
139
|
-
query: { directory: this.directory },
|
|
140
|
-
body: {
|
|
141
|
-
messageID: command.messageId,
|
|
142
|
-
parts: command.parts.map(toSessionPromptPart),
|
|
143
|
-
},
|
|
144
|
-
throwOnError: true,
|
|
145
|
-
});
|
|
146
|
-
return {
|
|
147
|
-
kind: "sendPromptAsync",
|
|
148
|
-
sessionId: command.sessionId,
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
async awaitPromptResponse(command) {
|
|
152
|
-
const deadline = Date.now() + PROMPT_RESPONSE_TIMEOUT_MS;
|
|
153
|
-
let stableCandidateKey = null;
|
|
154
|
-
for (;;) {
|
|
155
|
-
const messages = await this.client.session.messages({
|
|
156
|
-
path: { id: command.sessionId },
|
|
157
|
-
query: {
|
|
158
|
-
directory: this.directory,
|
|
159
|
-
limit: 64,
|
|
160
|
-
},
|
|
161
|
-
responseStyle: "data",
|
|
162
|
-
throwOnError: true,
|
|
163
|
-
});
|
|
164
|
-
const response = selectAssistantResponse(unwrapData(messages), command.messageId);
|
|
165
|
-
if (response !== null) {
|
|
166
|
-
const candidateKey = createAssistantCandidateKey(response);
|
|
167
|
-
if (stableCandidateKey === candidateKey) {
|
|
168
|
-
return {
|
|
169
|
-
kind: "awaitPromptResponse",
|
|
170
|
-
sessionId: command.sessionId,
|
|
171
|
-
messageId: response.info.id,
|
|
172
|
-
parts: response.parts.flatMap(toBindingMessagePart),
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
stableCandidateKey = candidateKey;
|
|
176
|
-
}
|
|
177
|
-
else {
|
|
178
|
-
stableCandidateKey = null;
|
|
179
|
-
}
|
|
180
|
-
if (Date.now() >= deadline) {
|
|
181
|
-
throw new Error(`assistant message for prompt ${command.messageId} is unavailable after prompt completion`);
|
|
182
|
-
}
|
|
183
|
-
await Bun.sleep(SESSION_IDLE_POLL_MS);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
async readMessage(command) {
|
|
187
|
-
const message = await this.client.session.message({
|
|
188
|
-
path: {
|
|
189
|
-
id: command.sessionId,
|
|
190
|
-
messageID: command.messageId,
|
|
191
|
-
},
|
|
192
|
-
query: { directory: this.directory },
|
|
193
|
-
responseStyle: "data",
|
|
194
|
-
throwOnError: true,
|
|
195
|
-
});
|
|
196
|
-
return {
|
|
197
|
-
kind: "readMessage",
|
|
198
|
-
sessionId: command.sessionId,
|
|
199
|
-
messageId: command.messageId,
|
|
200
|
-
parts: unwrapData(message).parts.flatMap(toBindingMessagePart),
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
async listMessages(command) {
|
|
204
|
-
const messages = await this.client.session.messages({
|
|
205
|
-
path: { id: command.sessionId },
|
|
206
|
-
query: {
|
|
207
|
-
directory: this.directory,
|
|
208
|
-
limit: 32,
|
|
209
|
-
},
|
|
210
|
-
responseStyle: "data",
|
|
211
|
-
throwOnError: true,
|
|
212
|
-
});
|
|
213
|
-
return {
|
|
214
|
-
kind: "listMessages",
|
|
215
|
-
sessionId: command.sessionId,
|
|
216
|
-
messages: unwrapData(messages).flatMap(toBindingMessage),
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
function unwrapData(value) {
|
|
221
|
-
return typeof value === "object" && value !== null && "data" in value ? value.data : value;
|
|
222
|
-
}
|
|
223
|
-
function toSessionPromptPart(part) {
|
|
224
|
-
switch (part.kind) {
|
|
225
|
-
case "text":
|
|
226
|
-
return {
|
|
227
|
-
id: part.partId,
|
|
228
|
-
type: "text",
|
|
229
|
-
text: part.text,
|
|
230
|
-
};
|
|
231
|
-
case "file":
|
|
232
|
-
return {
|
|
233
|
-
id: part.partId,
|
|
234
|
-
type: "file",
|
|
235
|
-
mime: part.mimeType,
|
|
236
|
-
url: pathToFileURL(part.localPath).href,
|
|
237
|
-
filename: part.fileName ?? basename(part.localPath),
|
|
238
|
-
};
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
function selectAssistantResponse(messages, userMessageId) {
|
|
242
|
-
const assistantChildren = messages.filter(isAssistantChildMessage(userMessageId));
|
|
243
|
-
for (let index = assistantChildren.length - 1; index >= 0; index -= 1) {
|
|
244
|
-
const candidate = assistantChildren[index];
|
|
245
|
-
if (hasVisibleText(candidate)) {
|
|
246
|
-
return candidate;
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
for (let index = assistantChildren.length - 1; index >= 0; index -= 1) {
|
|
250
|
-
const candidate = assistantChildren[index];
|
|
251
|
-
if (candidate.info?.finish === "stop" || candidate.info?.error !== undefined) {
|
|
252
|
-
return candidate;
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
return null;
|
|
256
|
-
}
|
|
257
|
-
function createAssistantCandidateKey(message) {
|
|
258
|
-
return JSON.stringify({
|
|
259
|
-
messageId: message.info.id,
|
|
260
|
-
finish: message.info.finish ?? null,
|
|
261
|
-
hasError: message.info.error !== undefined,
|
|
262
|
-
parts: message.parts.map((part) => ({
|
|
263
|
-
id: part.id ?? null,
|
|
264
|
-
type: part.type,
|
|
265
|
-
text: typeof part.text === "string" ? part.text : null,
|
|
266
|
-
ignored: part.ignored === true,
|
|
267
|
-
})),
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
function isAssistantChildMessage(userMessageId) {
|
|
271
|
-
return (message) => message.info?.role === "assistant" && message.info.parentID === userMessageId;
|
|
272
|
-
}
|
|
273
|
-
function toBindingMessagePart(part) {
|
|
274
|
-
if (typeof part.id !== "string" || typeof part.messageID !== "string" || part.type.length === 0) {
|
|
275
|
-
return [];
|
|
276
|
-
}
|
|
277
|
-
return [
|
|
278
|
-
{
|
|
279
|
-
messageId: part.messageID,
|
|
280
|
-
partId: part.id,
|
|
281
|
-
type: part.type,
|
|
282
|
-
text: typeof part.text === "string" ? part.text : null,
|
|
283
|
-
ignored: part.ignored === true,
|
|
284
|
-
},
|
|
285
|
-
];
|
|
286
|
-
}
|
|
287
|
-
function hasVisibleText(message) {
|
|
288
|
-
return message.parts.some((part) => part.type === "text" &&
|
|
289
|
-
part.ignored !== true &&
|
|
290
|
-
typeof part.text === "string" &&
|
|
291
|
-
part.text.trim().length > 0);
|
|
292
|
-
}
|
|
293
|
-
function toBindingMessage(message) {
|
|
294
|
-
if (typeof message.info?.id !== "string" ||
|
|
295
|
-
typeof message.info.role !== "string" ||
|
|
296
|
-
message.info.role.length === 0) {
|
|
297
|
-
return [];
|
|
298
|
-
}
|
|
299
|
-
return [
|
|
300
|
-
{
|
|
301
|
-
messageId: message.info.id,
|
|
302
|
-
role: message.info.role,
|
|
303
|
-
parentId: typeof message.info.parentID === "string" ? message.info.parentID : null,
|
|
304
|
-
parts: message.parts.flatMap(toBindingMessagePart),
|
|
305
|
-
},
|
|
306
|
-
];
|
|
307
|
-
}
|
|
308
|
-
function toErrorResult(command, error) {
|
|
309
|
-
return {
|
|
310
|
-
kind: "error",
|
|
311
|
-
commandKind: command.kind,
|
|
312
|
-
sessionId: "sessionId" in command ? command.sessionId : null,
|
|
313
|
-
code: isMissingSessionError(error) ? "missingSession" : "unknown",
|
|
314
|
-
message: extractErrorMessage(error),
|
|
315
|
-
};
|
|
316
|
-
}
|
|
317
|
-
function isMissingSessionError(error) {
|
|
318
|
-
if (typeof error !== "object" || error === null) {
|
|
319
|
-
return false;
|
|
320
|
-
}
|
|
321
|
-
const name = "name" in error ? error.name : undefined;
|
|
322
|
-
const message = "data" in error ? extractDataMessage(error.data) : null;
|
|
323
|
-
return name === "NotFoundError" && message?.includes("Session not found:") === true;
|
|
324
|
-
}
|
|
325
|
-
function extractDataMessage(value) {
|
|
326
|
-
if (typeof value !== "object" || value === null) {
|
|
327
|
-
return null;
|
|
328
|
-
}
|
|
329
|
-
const message = value.message;
|
|
330
|
-
return typeof message === "string" ? message : null;
|
|
331
|
-
}
|
|
332
|
-
function extractErrorMessage(error) {
|
|
333
|
-
if (error instanceof Error && error.message.length > 0) {
|
|
334
|
-
return error.message;
|
|
335
|
-
}
|
|
336
|
-
if (typeof error === "string" && error.length > 0) {
|
|
337
|
-
return error;
|
|
338
|
-
}
|
|
339
|
-
return "OpenCode command failed";
|
|
340
|
-
}
|
|
@@ -1,82 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,48 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,65 +0,0 @@
|
|
|
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
|
-
}
|
package/dist/opencode/events.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { OpencodeEventHub } from "./driver-hub";
|
package/dist/questions/client.js
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
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
|
-
}
|