opencode-gateway 0.2.2 → 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/dist/telegram/client.d.ts +1 -1
- package/dist/telegram/poller.d.ts +16 -1
- package/dist/telegram/runtime.d.ts +3 -1
- package/dist/telegram/state.d.ts +7 -0
- 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 -179
- package/dist/telegram/media.js +0 -65
- package/dist/telegram/normalize.js +0 -119
- package/dist/telegram/poller.js +0 -97
- package/dist/telegram/runtime.js +0 -133
- package/dist/telegram/state.js +0 -128
- 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/questions/parser.js
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
const CANCEL_WORDS = new Set(["/cancel", "cancel", "/reject", "reject"]);
|
|
2
|
-
export function parseQuestionReply(request, text) {
|
|
3
|
-
if (text === null) {
|
|
4
|
-
return {
|
|
5
|
-
kind: "invalid",
|
|
6
|
-
message: "This question currently accepts text replies only.",
|
|
7
|
-
};
|
|
8
|
-
}
|
|
9
|
-
const trimmed = text.trim();
|
|
10
|
-
if (trimmed.length === 0) {
|
|
11
|
-
return {
|
|
12
|
-
kind: "invalid",
|
|
13
|
-
message: "Reply text must not be empty.",
|
|
14
|
-
};
|
|
15
|
-
}
|
|
16
|
-
if (CANCEL_WORDS.has(trimmed.toLowerCase())) {
|
|
17
|
-
return {
|
|
18
|
-
kind: "reject",
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
if (request.questions.length === 1) {
|
|
22
|
-
const parsedAnswer = parseQuestionLine(request.questions[0], trimmed);
|
|
23
|
-
return parsedAnswer.kind === "invalid"
|
|
24
|
-
? parsedAnswer
|
|
25
|
-
: {
|
|
26
|
-
kind: "reply",
|
|
27
|
-
answers: [parsedAnswer.answer],
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
const lines = trimmed
|
|
31
|
-
.split(/\r?\n/u)
|
|
32
|
-
.map((line) => line.trim())
|
|
33
|
-
.filter((line) => line.length > 0);
|
|
34
|
-
if (lines.length !== request.questions.length) {
|
|
35
|
-
return {
|
|
36
|
-
kind: "invalid",
|
|
37
|
-
message: `Expected ${request.questions.length} non-empty lines, but received ${lines.length}.`,
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
const answers = [];
|
|
41
|
-
for (const [index, question] of request.questions.entries()) {
|
|
42
|
-
const parsedAnswer = parseQuestionLine(question, lines[index]);
|
|
43
|
-
if (parsedAnswer.kind === "invalid") {
|
|
44
|
-
return parsedAnswer;
|
|
45
|
-
}
|
|
46
|
-
answers.push(parsedAnswer.answer);
|
|
47
|
-
}
|
|
48
|
-
return {
|
|
49
|
-
kind: "reply",
|
|
50
|
-
answers,
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
function parseQuestionLine(question, line) {
|
|
54
|
-
const rawSelections = question.multiple
|
|
55
|
-
? line
|
|
56
|
-
.split(/[,\n]/u)
|
|
57
|
-
.map((token) => token.trim())
|
|
58
|
-
.filter((token) => token.length > 0)
|
|
59
|
-
: [line];
|
|
60
|
-
if (rawSelections.length === 0) {
|
|
61
|
-
return {
|
|
62
|
-
kind: "invalid",
|
|
63
|
-
message: "At least one answer is required.",
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
const answers = [];
|
|
67
|
-
for (const rawSelection of rawSelections) {
|
|
68
|
-
const option = resolveOptionSelection(question, rawSelection);
|
|
69
|
-
if (option !== null) {
|
|
70
|
-
answers.push(option);
|
|
71
|
-
continue;
|
|
72
|
-
}
|
|
73
|
-
if (!question.custom) {
|
|
74
|
-
return {
|
|
75
|
-
kind: "invalid",
|
|
76
|
-
message: `Answer "${rawSelection}" does not match any allowed option.`,
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
answers.push(rawSelection);
|
|
80
|
-
}
|
|
81
|
-
return {
|
|
82
|
-
kind: "answer",
|
|
83
|
-
answer: answers,
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
function resolveOptionSelection(question, selection) {
|
|
87
|
-
if (question.options.length === 0) {
|
|
88
|
-
return null;
|
|
89
|
-
}
|
|
90
|
-
const numericIndex = Number.parseInt(selection, 10);
|
|
91
|
-
if (Number.isSafeInteger(numericIndex) && String(numericIndex) === selection && numericIndex >= 1) {
|
|
92
|
-
return question.options[numericIndex - 1]?.label ?? null;
|
|
93
|
-
}
|
|
94
|
-
const normalized = selection.toLowerCase();
|
|
95
|
-
return question.options.find((option) => option.label.toLowerCase() === normalized)?.label ?? null;
|
|
96
|
-
}
|
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
import { recordTelegramSendFailure, recordTelegramSendSuccess } from "../telegram/state";
|
|
2
|
-
import { formatError } from "../utils/error";
|
|
3
|
-
import { formatPlainTextQuestion, formatQuestionReplyError } from "./format";
|
|
4
|
-
import { normalizeQuestionEvent } from "./normalize";
|
|
5
|
-
import { parseQuestionReply } from "./parser";
|
|
6
|
-
export class GatewayQuestionRuntime {
|
|
7
|
-
client;
|
|
8
|
-
directory;
|
|
9
|
-
store;
|
|
10
|
-
sessions;
|
|
11
|
-
transport;
|
|
12
|
-
telegramClient;
|
|
13
|
-
logger;
|
|
14
|
-
constructor(client, directory, store, sessions, transport, telegramClient, logger) {
|
|
15
|
-
this.client = client;
|
|
16
|
-
this.directory = directory;
|
|
17
|
-
this.store = store;
|
|
18
|
-
this.sessions = sessions;
|
|
19
|
-
this.transport = transport;
|
|
20
|
-
this.telegramClient = telegramClient;
|
|
21
|
-
this.logger = logger;
|
|
22
|
-
}
|
|
23
|
-
handleEvent(event) {
|
|
24
|
-
const normalized = normalizeQuestionEvent(event);
|
|
25
|
-
if (normalized === null) {
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
void this.processEvent(normalized).catch((error) => {
|
|
29
|
-
this.logger.log("warn", `question bridge failed: ${formatError(error)}`);
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
async tryHandleInboundMessage(message) {
|
|
33
|
-
const pending = this.store.getPendingQuestionForTarget(message.deliveryTarget);
|
|
34
|
-
if (pending === null) {
|
|
35
|
-
return false;
|
|
36
|
-
}
|
|
37
|
-
const parsed = parseQuestionReply(pending, message.text);
|
|
38
|
-
switch (parsed.kind) {
|
|
39
|
-
case "invalid":
|
|
40
|
-
await this.sendPlainText(pending.deliveryTarget, formatQuestionReplyError(pending, parsed.message));
|
|
41
|
-
return true;
|
|
42
|
-
case "reject":
|
|
43
|
-
await this.rejectQuestion(pending.requestId);
|
|
44
|
-
this.store.deletePendingQuestion(pending.requestId);
|
|
45
|
-
return true;
|
|
46
|
-
case "reply":
|
|
47
|
-
await this.replyQuestion(pending.requestId, parsed.answers);
|
|
48
|
-
this.store.deletePendingQuestion(pending.requestId);
|
|
49
|
-
return true;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
async handleTelegramCallbackQuery(query) {
|
|
53
|
-
if (this.telegramClient === null) {
|
|
54
|
-
return false;
|
|
55
|
-
}
|
|
56
|
-
const pending = this.store.getPendingQuestionForTelegramMessage(query.deliveryTarget, query.messageId);
|
|
57
|
-
if (pending === null) {
|
|
58
|
-
await this.telegramClient.answerCallbackQuery(query.callbackQueryId, "This question is no longer pending.");
|
|
59
|
-
return true;
|
|
60
|
-
}
|
|
61
|
-
const answer = resolveCallbackAnswer(query.data, pending);
|
|
62
|
-
if (answer === null) {
|
|
63
|
-
await this.telegramClient.answerCallbackQuery(query.callbackQueryId, "This button is no longer valid.");
|
|
64
|
-
return true;
|
|
65
|
-
}
|
|
66
|
-
await this.replyQuestion(pending.requestId, [[answer]]);
|
|
67
|
-
this.store.deletePendingQuestion(pending.requestId);
|
|
68
|
-
await this.telegramClient.answerCallbackQuery(query.callbackQueryId, `Sent: ${answer}`);
|
|
69
|
-
return true;
|
|
70
|
-
}
|
|
71
|
-
async processEvent(event) {
|
|
72
|
-
switch (event.kind) {
|
|
73
|
-
case "asked":
|
|
74
|
-
await this.handleQuestionAsked(event.request);
|
|
75
|
-
return;
|
|
76
|
-
case "resolved":
|
|
77
|
-
this.store.deletePendingQuestion(event.requestId);
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
async handleQuestionAsked(request) {
|
|
82
|
-
const targets = this.sessions.listReplyTargets(request.sessionId);
|
|
83
|
-
if (targets.length === 0) {
|
|
84
|
-
this.logger.log("warn", `question ${request.requestId} has no reply target for session ${request.sessionId}`);
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
const deliveredTargets = [];
|
|
88
|
-
for (const target of targets) {
|
|
89
|
-
try {
|
|
90
|
-
deliveredTargets.push(await this.sendQuestion(target, request));
|
|
91
|
-
}
|
|
92
|
-
catch (error) {
|
|
93
|
-
this.logger.log("warn", `question ${request.requestId} delivery failed for ${target.channel}:${target.target}: ${formatError(error)}`);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
if (deliveredTargets.length === 0) {
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
this.store.replacePendingQuestion({
|
|
100
|
-
requestId: request.requestId,
|
|
101
|
-
sessionId: request.sessionId,
|
|
102
|
-
questions: request.questions,
|
|
103
|
-
targets: deliveredTargets,
|
|
104
|
-
recordedAtMs: Date.now(),
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
async sendQuestion(target, request) {
|
|
108
|
-
const nativeKeyboard = buildTelegramInlineKeyboard(request);
|
|
109
|
-
if (target.channel === "telegram" && nativeKeyboard !== null && this.telegramClient !== null) {
|
|
110
|
-
try {
|
|
111
|
-
const sent = await this.telegramClient.sendInteractiveMessage(target.target, formatTelegramNativeQuestion(request), target.topic, nativeKeyboard);
|
|
112
|
-
recordTelegramSendSuccess(this.store, Date.now());
|
|
113
|
-
return {
|
|
114
|
-
deliveryTarget: target,
|
|
115
|
-
telegramMessageId: sent.message_id,
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
catch (error) {
|
|
119
|
-
recordTelegramSendFailure(this.store, formatError(error), Date.now());
|
|
120
|
-
throw error;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
await this.sendPlainText(target, formatPlainTextQuestion(request));
|
|
124
|
-
return {
|
|
125
|
-
deliveryTarget: target,
|
|
126
|
-
telegramMessageId: null,
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
async sendPlainText(target, body) {
|
|
130
|
-
const ack = await this.transport.sendMessage({
|
|
131
|
-
deliveryTarget: target,
|
|
132
|
-
body,
|
|
133
|
-
});
|
|
134
|
-
if (ack.errorMessage !== null) {
|
|
135
|
-
throw new Error(ack.errorMessage);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
async replyQuestion(requestId, answers) {
|
|
139
|
-
await this.client.question.reply({
|
|
140
|
-
requestID: requestId,
|
|
141
|
-
directory: this.directory,
|
|
142
|
-
answers,
|
|
143
|
-
}, {
|
|
144
|
-
responseStyle: "data",
|
|
145
|
-
throwOnError: true,
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
async rejectQuestion(requestId) {
|
|
149
|
-
await this.client.question.reject({
|
|
150
|
-
requestID: requestId,
|
|
151
|
-
directory: this.directory,
|
|
152
|
-
}, {
|
|
153
|
-
responseStyle: "data",
|
|
154
|
-
throwOnError: true,
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
function buildTelegramInlineKeyboard(request) {
|
|
159
|
-
if (request.questions.length !== 1) {
|
|
160
|
-
return null;
|
|
161
|
-
}
|
|
162
|
-
const [question] = request.questions;
|
|
163
|
-
if (question.multiple || question.options.length === 0) {
|
|
164
|
-
return null;
|
|
165
|
-
}
|
|
166
|
-
return {
|
|
167
|
-
inline_keyboard: question.options.map((option, index) => [
|
|
168
|
-
{
|
|
169
|
-
text: option.label,
|
|
170
|
-
callback_data: `q:${index}`,
|
|
171
|
-
},
|
|
172
|
-
]),
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
function formatTelegramNativeQuestion(request) {
|
|
176
|
-
const [question] = request.questions;
|
|
177
|
-
return [
|
|
178
|
-
"OpenCode needs additional input before it can continue.",
|
|
179
|
-
"",
|
|
180
|
-
`${question.header}: ${question.question}`,
|
|
181
|
-
"",
|
|
182
|
-
"Tap a button below or reply with text.",
|
|
183
|
-
].join("\n");
|
|
184
|
-
}
|
|
185
|
-
function resolveCallbackAnswer(data, pending) {
|
|
186
|
-
if (data === null || !data.startsWith("q:") || pending.questions.length !== 1) {
|
|
187
|
-
return null;
|
|
188
|
-
}
|
|
189
|
-
const indexText = data.slice(2);
|
|
190
|
-
const index = Number.parseInt(indexText, 10);
|
|
191
|
-
if (!Number.isSafeInteger(index) || index < 0) {
|
|
192
|
-
return null;
|
|
193
|
-
}
|
|
194
|
-
return pending.questions[0]?.options[index]?.label ?? null;
|
|
195
|
-
}
|
package/dist/questions/types.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { rm } from "node:fs/promises";
|
|
2
|
-
export async function deleteInboundAttachmentFiles(entries, logger) {
|
|
3
|
-
const paths = new Set(entries.flatMap((entry) => entry.attachments.map((attachment) => attachment.localPath)));
|
|
4
|
-
await Promise.all([...paths].map(async (path) => {
|
|
5
|
-
try {
|
|
6
|
-
await rm(path, { force: true });
|
|
7
|
-
}
|
|
8
|
-
catch (error) {
|
|
9
|
-
logger.log("warn", `failed to remove cached inbound attachment ${path}: ${String(error)}`);
|
|
10
|
-
}
|
|
11
|
-
}));
|
|
12
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
export class ConversationCoordinator {
|
|
2
|
-
tails = new Map();
|
|
3
|
-
async runExclusive(conversationKey, operation) {
|
|
4
|
-
const previous = this.tails.get(conversationKey) ?? Promise.resolve();
|
|
5
|
-
let release;
|
|
6
|
-
const current = new Promise((resolve) => {
|
|
7
|
-
release = resolve;
|
|
8
|
-
});
|
|
9
|
-
const tail = previous.then(() => current, () => current);
|
|
10
|
-
this.tails.set(conversationKey, tail);
|
|
11
|
-
await previous;
|
|
12
|
-
try {
|
|
13
|
-
return await operation();
|
|
14
|
-
}
|
|
15
|
-
finally {
|
|
16
|
-
release();
|
|
17
|
-
if (this.tails.get(conversationKey) === tail) {
|
|
18
|
-
this.tails.delete(conversationKey);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
}
|