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
package/dist/cli.js
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli/args.ts
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
function parseCliCommand(argv) {
|
|
6
|
+
const [command, ...rest] = argv;
|
|
7
|
+
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
8
|
+
return { kind: "help" };
|
|
9
|
+
}
|
|
10
|
+
if (command !== "init" && command !== "doctor") {
|
|
11
|
+
throw new Error(`unknown command: ${command}`);
|
|
12
|
+
}
|
|
13
|
+
let managed = false;
|
|
14
|
+
let configDir = null;
|
|
15
|
+
for (let index = 0;index < rest.length; index += 1) {
|
|
16
|
+
const argument = rest[index];
|
|
17
|
+
if (argument === "--managed") {
|
|
18
|
+
managed = true;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (argument === "--config-dir") {
|
|
22
|
+
const value = rest[index + 1];
|
|
23
|
+
if (!value) {
|
|
24
|
+
throw new Error("--config-dir requires a value");
|
|
25
|
+
}
|
|
26
|
+
configDir = resolve(value);
|
|
27
|
+
index += 1;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (argument === "--help" || argument === "-h") {
|
|
31
|
+
return { kind: "help" };
|
|
32
|
+
}
|
|
33
|
+
throw new Error(`unknown argument: ${argument}`);
|
|
34
|
+
}
|
|
35
|
+
if (managed && configDir !== null) {
|
|
36
|
+
throw new Error("--managed cannot be combined with --config-dir");
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
kind: command,
|
|
40
|
+
managed,
|
|
41
|
+
configDir
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function formatCliHelp() {
|
|
45
|
+
return [
|
|
46
|
+
"opencode-gateway",
|
|
47
|
+
"",
|
|
48
|
+
"Commands:",
|
|
49
|
+
" opencode-gateway init [--managed] [--config-dir <path>]",
|
|
50
|
+
" opencode-gateway doctor [--managed] [--config-dir <path>]",
|
|
51
|
+
"",
|
|
52
|
+
"Defaults:",
|
|
53
|
+
" init/doctor use OPENCODE_CONFIG_DIR when set, otherwise ~/.config/opencode",
|
|
54
|
+
" --managed uses ~/.config/opencode-gateway/opencode"
|
|
55
|
+
].join(`
|
|
56
|
+
`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/cli/doctor.ts
|
|
60
|
+
import { readFile } from "node:fs/promises";
|
|
61
|
+
import { join as join2 } from "node:path";
|
|
62
|
+
|
|
63
|
+
// src/config/paths.ts
|
|
64
|
+
import { homedir } from "node:os";
|
|
65
|
+
import { join, resolve as resolve2 } from "node:path";
|
|
66
|
+
var GATEWAY_CONFIG_FILE = "opencode-gateway.toml";
|
|
67
|
+
var OPENCODE_CONFIG_FILE = "opencode.json";
|
|
68
|
+
function resolveOpencodeConfigDir(env) {
|
|
69
|
+
const explicit = env.OPENCODE_CONFIG_DIR;
|
|
70
|
+
if (explicit && explicit.trim().length > 0) {
|
|
71
|
+
return resolve2(explicit);
|
|
72
|
+
}
|
|
73
|
+
return defaultOpencodeConfigDir(env);
|
|
74
|
+
}
|
|
75
|
+
function resolveManagedOpencodeConfigDir(env) {
|
|
76
|
+
return join(resolveConfigHome(env), "opencode-gateway", "opencode");
|
|
77
|
+
}
|
|
78
|
+
function defaultGatewayStateDbPath(env) {
|
|
79
|
+
return join(resolveDataHome(env), "opencode-gateway", "state.db");
|
|
80
|
+
}
|
|
81
|
+
function resolveConfigHome(env) {
|
|
82
|
+
return env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
|
|
83
|
+
}
|
|
84
|
+
function resolveDataHome(env) {
|
|
85
|
+
return env.XDG_DATA_HOME ?? join(homedir(), ".local", "share");
|
|
86
|
+
}
|
|
87
|
+
function defaultOpencodeConfigDir(env) {
|
|
88
|
+
return join(resolveConfigHome(env), "opencode");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// src/cli/opencode-config.ts
|
|
92
|
+
var OPENCODE_SCHEMA_URL = "https://opencode.ai/config.json";
|
|
93
|
+
var PACKAGE_NAME = "opencode-gateway";
|
|
94
|
+
function createDefaultOpencodeConfig(managed) {
|
|
95
|
+
const document = {
|
|
96
|
+
$schema: OPENCODE_SCHEMA_URL,
|
|
97
|
+
plugin: [PACKAGE_NAME]
|
|
98
|
+
};
|
|
99
|
+
if (managed) {
|
|
100
|
+
document.server = {
|
|
101
|
+
hostname: "127.0.0.1",
|
|
102
|
+
port: 4096
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
return document;
|
|
106
|
+
}
|
|
107
|
+
function ensureGatewayPlugin(document) {
|
|
108
|
+
const plugins = document.plugin;
|
|
109
|
+
if (plugins === undefined) {
|
|
110
|
+
return {
|
|
111
|
+
changed: true,
|
|
112
|
+
document: {
|
|
113
|
+
...document,
|
|
114
|
+
plugin: [PACKAGE_NAME]
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
if (!Array.isArray(plugins)) {
|
|
119
|
+
throw new Error("opencode.json field `plugin` must be an array when present");
|
|
120
|
+
}
|
|
121
|
+
if (plugins.some((entry) => entry === PACKAGE_NAME)) {
|
|
122
|
+
return {
|
|
123
|
+
changed: false,
|
|
124
|
+
document
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
changed: true,
|
|
129
|
+
document: {
|
|
130
|
+
...document,
|
|
131
|
+
plugin: [...plugins, PACKAGE_NAME]
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function parseOpencodeConfig(source, path) {
|
|
136
|
+
let parsed;
|
|
137
|
+
try {
|
|
138
|
+
parsed = JSON.parse(source);
|
|
139
|
+
} catch (error) {
|
|
140
|
+
throw new Error(`failed to parse opencode config ${path}: ${formatError(error)}`);
|
|
141
|
+
}
|
|
142
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
143
|
+
throw new Error(`opencode config ${path} must decode to a JSON object`);
|
|
144
|
+
}
|
|
145
|
+
return parsed;
|
|
146
|
+
}
|
|
147
|
+
function stringifyOpencodeConfig(document) {
|
|
148
|
+
return `${JSON.stringify(document, null, 2)}
|
|
149
|
+
`;
|
|
150
|
+
}
|
|
151
|
+
function formatError(error) {
|
|
152
|
+
return error instanceof Error ? error.message : String(error);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// src/cli/paths.ts
|
|
156
|
+
import { access } from "node:fs/promises";
|
|
157
|
+
import { constants } from "node:fs";
|
|
158
|
+
import { resolve as resolve3 } from "node:path";
|
|
159
|
+
function resolveCliConfigDir(options, env) {
|
|
160
|
+
if (options.configDir !== null) {
|
|
161
|
+
return resolve3(options.configDir);
|
|
162
|
+
}
|
|
163
|
+
if (options.managed) {
|
|
164
|
+
return resolveManagedOpencodeConfigDir(env);
|
|
165
|
+
}
|
|
166
|
+
return resolveOpencodeConfigDir(env);
|
|
167
|
+
}
|
|
168
|
+
async function pathExists(path) {
|
|
169
|
+
try {
|
|
170
|
+
await access(path, constants.F_OK);
|
|
171
|
+
return true;
|
|
172
|
+
} catch {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// src/cli/doctor.ts
|
|
178
|
+
async function runDoctor(options, env) {
|
|
179
|
+
const configDir = resolveCliConfigDir(options, env);
|
|
180
|
+
const opencodeConfigPath = join2(configDir, OPENCODE_CONFIG_FILE);
|
|
181
|
+
const gatewayConfigPath = join2(configDir, GATEWAY_CONFIG_FILE);
|
|
182
|
+
const opencodeStatus = await inspectOpencodeConfig(opencodeConfigPath);
|
|
183
|
+
const gatewayOverride = env.OPENCODE_GATEWAY_CONFIG?.trim() || null;
|
|
184
|
+
console.log("doctor report");
|
|
185
|
+
console.log(` config dir: ${configDir}`);
|
|
186
|
+
console.log(` opencode config: ${await describePath(opencodeConfigPath)}`);
|
|
187
|
+
console.log(` gateway config: ${await describePath(gatewayConfigPath)}`);
|
|
188
|
+
console.log(` gateway config override: ${gatewayOverride ?? "not set"}`);
|
|
189
|
+
console.log(` plugin configured: ${opencodeStatus.pluginConfigured}`);
|
|
190
|
+
console.log(` TELEGRAM_BOT_TOKEN: ${env.TELEGRAM_BOT_TOKEN?.trim() ? "set" : "missing"}`);
|
|
191
|
+
if (opencodeStatus.error !== null) {
|
|
192
|
+
console.log(` opencode config error: ${opencodeStatus.error}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
async function describePath(path) {
|
|
196
|
+
return await pathExists(path) ? `present at ${path}` : `missing at ${path}`;
|
|
197
|
+
}
|
|
198
|
+
async function inspectOpencodeConfig(path) {
|
|
199
|
+
if (!await pathExists(path)) {
|
|
200
|
+
return {
|
|
201
|
+
pluginConfigured: "no",
|
|
202
|
+
error: null
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
const parsed = parseOpencodeConfig(await readFile(path, "utf8"), path);
|
|
207
|
+
const plugins = parsed.plugin;
|
|
208
|
+
if (plugins === undefined) {
|
|
209
|
+
return {
|
|
210
|
+
pluginConfigured: "no",
|
|
211
|
+
error: null
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
if (!Array.isArray(plugins)) {
|
|
215
|
+
return {
|
|
216
|
+
pluginConfigured: "invalid",
|
|
217
|
+
error: "`plugin` is not an array"
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
pluginConfigured: plugins.some((entry) => entry === "opencode-gateway") ? "yes" : "no",
|
|
222
|
+
error: null
|
|
223
|
+
};
|
|
224
|
+
} catch (error) {
|
|
225
|
+
return {
|
|
226
|
+
pluginConfigured: "unknown",
|
|
227
|
+
error: error instanceof Error ? error.message : String(error)
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// src/cli/init.ts
|
|
233
|
+
import { mkdir, readFile as readFile2, writeFile } from "node:fs/promises";
|
|
234
|
+
import { dirname, join as join3 } from "node:path";
|
|
235
|
+
|
|
236
|
+
// src/cli/templates.ts
|
|
237
|
+
function buildGatewayConfigTemplate(stateDbPath) {
|
|
238
|
+
return [
|
|
239
|
+
"# Opencode Gateway configuration",
|
|
240
|
+
"# Fill in secrets and provider details before enabling real integrations.",
|
|
241
|
+
"",
|
|
242
|
+
"[gateway]",
|
|
243
|
+
`state_db = "${escapeTomlString(stateDbPath)}"`,
|
|
244
|
+
"",
|
|
245
|
+
"[cron]",
|
|
246
|
+
"enabled = true",
|
|
247
|
+
"tick_seconds = 5",
|
|
248
|
+
"max_concurrent_runs = 1",
|
|
249
|
+
'# timezone = "Asia/Shanghai"',
|
|
250
|
+
"",
|
|
251
|
+
"[channels.telegram]",
|
|
252
|
+
"enabled = false",
|
|
253
|
+
'bot_token_env = "TELEGRAM_BOT_TOKEN"',
|
|
254
|
+
"poll_timeout_seconds = 25",
|
|
255
|
+
"allowed_chats = []",
|
|
256
|
+
"allowed_users = []",
|
|
257
|
+
""
|
|
258
|
+
].join(`
|
|
259
|
+
`);
|
|
260
|
+
}
|
|
261
|
+
function escapeTomlString(value) {
|
|
262
|
+
return value.replaceAll("\\", "\\\\").replaceAll('"', "\\\"");
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// src/cli/init.ts
|
|
266
|
+
async function runInit(options, env) {
|
|
267
|
+
const configDir = resolveCliConfigDir(options, env);
|
|
268
|
+
const opencodeConfigPath = join3(configDir, OPENCODE_CONFIG_FILE);
|
|
269
|
+
const gatewayConfigPath = join3(configDir, GATEWAY_CONFIG_FILE);
|
|
270
|
+
await mkdir(configDir, { recursive: true });
|
|
271
|
+
let opencodeStatus = "already present";
|
|
272
|
+
if (!await pathExists(opencodeConfigPath)) {
|
|
273
|
+
await writeFile(opencodeConfigPath, stringifyOpencodeConfig(createDefaultOpencodeConfig(options.managed)));
|
|
274
|
+
opencodeStatus = "created";
|
|
275
|
+
} else {
|
|
276
|
+
const source = await readFile2(opencodeConfigPath, "utf8");
|
|
277
|
+
const parsed = parseOpencodeConfig(source, opencodeConfigPath);
|
|
278
|
+
const next = ensureGatewayPlugin(parsed);
|
|
279
|
+
if (next.changed) {
|
|
280
|
+
await writeFile(opencodeConfigPath, stringifyOpencodeConfig(next.document));
|
|
281
|
+
opencodeStatus = "updated";
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
let gatewayStatus = "already present";
|
|
285
|
+
if (!await pathExists(gatewayConfigPath)) {
|
|
286
|
+
await mkdir(dirname(gatewayConfigPath), { recursive: true });
|
|
287
|
+
await writeFile(gatewayConfigPath, buildGatewayConfigTemplate(defaultGatewayStateDbPath(env)));
|
|
288
|
+
gatewayStatus = "created";
|
|
289
|
+
}
|
|
290
|
+
console.log(`config dir: ${configDir}`);
|
|
291
|
+
console.log(`opencode config: ${opencodeConfigPath} (${opencodeStatus})`);
|
|
292
|
+
console.log(`gateway config: ${gatewayConfigPath} (${gatewayStatus})`);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// src/cli.ts
|
|
296
|
+
async function main() {
|
|
297
|
+
const command = parseCliCommand(process.argv.slice(2));
|
|
298
|
+
switch (command.kind) {
|
|
299
|
+
case "help":
|
|
300
|
+
console.log(formatCliHelp());
|
|
301
|
+
return;
|
|
302
|
+
case "doctor":
|
|
303
|
+
await runDoctor(command, process.env);
|
|
304
|
+
return;
|
|
305
|
+
case "init":
|
|
306
|
+
await runInit(command, process.env);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
main().catch((error) => {
|
|
311
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
312
|
+
console.error(`error: ${message}`);
|
|
313
|
+
process.exitCode = 1;
|
|
314
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export function parseCronConfig(value) {
|
|
2
|
+
const table = readCronTable(value);
|
|
3
|
+
return {
|
|
4
|
+
enabled: readBoolean(table.enabled, "cron.enabled", true),
|
|
5
|
+
tickSeconds: readPositiveInteger(table.tick_seconds, "cron.tick_seconds", 5),
|
|
6
|
+
maxConcurrentRuns: readPositiveInteger(table.max_concurrent_runs, "cron.max_concurrent_runs", 1),
|
|
7
|
+
timezone: readOptionalString(table.timezone, "cron.timezone"),
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
function readCronTable(value) {
|
|
11
|
+
if (value === undefined) {
|
|
12
|
+
return {};
|
|
13
|
+
}
|
|
14
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
15
|
+
throw new Error("cron must be a table when present");
|
|
16
|
+
}
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
function readBoolean(value, field, fallback) {
|
|
20
|
+
if (value === undefined) {
|
|
21
|
+
return fallback;
|
|
22
|
+
}
|
|
23
|
+
if (typeof value !== "boolean") {
|
|
24
|
+
throw new Error(`${field} must be a boolean when present`);
|
|
25
|
+
}
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
function readPositiveInteger(value, field, fallback) {
|
|
29
|
+
if (value === undefined) {
|
|
30
|
+
return fallback;
|
|
31
|
+
}
|
|
32
|
+
if (typeof value !== "number" || !Number.isInteger(value)) {
|
|
33
|
+
throw new Error(`${field} must be an integer when present`);
|
|
34
|
+
}
|
|
35
|
+
if (value < 1) {
|
|
36
|
+
throw new Error(`${field} must be greater than or equal to 1`);
|
|
37
|
+
}
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
function readOptionalString(value, field) {
|
|
41
|
+
if (value === undefined) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
if (typeof value !== "string") {
|
|
45
|
+
throw new Error(`${field} must be a string when present`);
|
|
46
|
+
}
|
|
47
|
+
const trimmed = value.trim();
|
|
48
|
+
if (trimmed.length === 0) {
|
|
49
|
+
throw new Error(`${field} must not be empty when present`);
|
|
50
|
+
}
|
|
51
|
+
return trimmed;
|
|
52
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type CronConfig } from "./cron";
|
|
2
|
+
import { type TelegramConfig } from "./telegram";
|
|
3
|
+
export type GatewayMailboxRouteConfig = {
|
|
4
|
+
channel: string;
|
|
5
|
+
target: string;
|
|
6
|
+
topic: string | null;
|
|
7
|
+
mailboxKey: string;
|
|
8
|
+
};
|
|
9
|
+
export type GatewayMailboxConfig = {
|
|
10
|
+
batchReplies: boolean;
|
|
11
|
+
batchWindowMs: number;
|
|
12
|
+
routes: GatewayMailboxRouteConfig[];
|
|
13
|
+
};
|
|
14
|
+
export type GatewayConfig = {
|
|
15
|
+
configPath: string;
|
|
16
|
+
stateDbPath: string;
|
|
17
|
+
mediaRootPath: string;
|
|
18
|
+
hasLegacyGatewayTimezone: boolean;
|
|
19
|
+
legacyGatewayTimezone: string | null;
|
|
20
|
+
mailbox: GatewayMailboxConfig;
|
|
21
|
+
cron: CronConfig;
|
|
22
|
+
telegram: TelegramConfig;
|
|
23
|
+
};
|
|
24
|
+
type EnvSource = Record<string, string | undefined>;
|
|
25
|
+
export declare function loadGatewayConfig(env?: EnvSource): Promise<GatewayConfig>;
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
3
|
+
import { parseCronConfig } from "./cron";
|
|
4
|
+
import { defaultGatewayStateDbPath, resolveGatewayConfigPath } from "./paths";
|
|
5
|
+
import { parseTelegramConfig } from "./telegram";
|
|
6
|
+
export async function loadGatewayConfig(env = process.env) {
|
|
7
|
+
const configPath = resolveGatewayConfigPath(env);
|
|
8
|
+
const rawConfig = await readGatewayConfigFile(configPath);
|
|
9
|
+
const stateDbValue = rawConfig?.gateway?.state_db;
|
|
10
|
+
if (stateDbValue !== undefined && typeof stateDbValue !== "string") {
|
|
11
|
+
throw new Error("gateway.state_db must be a string when present");
|
|
12
|
+
}
|
|
13
|
+
const stateDbPath = resolveStateDbPath(stateDbValue, configPath, env);
|
|
14
|
+
return {
|
|
15
|
+
configPath,
|
|
16
|
+
stateDbPath,
|
|
17
|
+
mediaRootPath: resolveMediaRootPath(stateDbPath),
|
|
18
|
+
hasLegacyGatewayTimezone: rawConfig?.gateway?.timezone !== undefined,
|
|
19
|
+
legacyGatewayTimezone: readLegacyGatewayTimezone(rawConfig?.gateway?.timezone),
|
|
20
|
+
mailbox: parseMailboxConfig(rawConfig?.gateway?.mailbox),
|
|
21
|
+
cron: parseCronConfig(rawConfig?.cron),
|
|
22
|
+
telegram: parseTelegramConfig(rawConfig?.channels?.telegram, env),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function parseMailboxConfig(value) {
|
|
26
|
+
const table = readMailboxTable(value);
|
|
27
|
+
return {
|
|
28
|
+
batchReplies: readBoolean(table.batch_replies, "gateway.mailbox.batch_replies", false),
|
|
29
|
+
batchWindowMs: readBatchWindowMs(table.batch_window_ms),
|
|
30
|
+
routes: readMailboxRoutes(table.routes),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
async function readGatewayConfigFile(path) {
|
|
34
|
+
if (!existsSync(path)) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
const source = await Bun.file(path).text();
|
|
38
|
+
const parsed = Bun.TOML.parse(source);
|
|
39
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
40
|
+
throw new Error(`gateway config must decode to a table: ${path}`);
|
|
41
|
+
}
|
|
42
|
+
return parsed;
|
|
43
|
+
}
|
|
44
|
+
function resolveStateDbPath(stateDb, configPath, env) {
|
|
45
|
+
if (!stateDb || stateDb.trim().length === 0) {
|
|
46
|
+
return defaultStateDbPath(env);
|
|
47
|
+
}
|
|
48
|
+
if (isAbsolute(stateDb)) {
|
|
49
|
+
return stateDb;
|
|
50
|
+
}
|
|
51
|
+
return resolve(dirname(configPath), stateDb);
|
|
52
|
+
}
|
|
53
|
+
function readMailboxTable(value) {
|
|
54
|
+
if (value === undefined) {
|
|
55
|
+
return {};
|
|
56
|
+
}
|
|
57
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
58
|
+
throw new Error("gateway.mailbox must be a table when present");
|
|
59
|
+
}
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
function readBoolean(value, field, fallback) {
|
|
63
|
+
if (value === undefined) {
|
|
64
|
+
return fallback;
|
|
65
|
+
}
|
|
66
|
+
if (typeof value !== "boolean") {
|
|
67
|
+
throw new Error(`${field} must be a boolean when present`);
|
|
68
|
+
}
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
function readBatchWindowMs(value) {
|
|
72
|
+
if (value === undefined) {
|
|
73
|
+
return 1_500;
|
|
74
|
+
}
|
|
75
|
+
if (typeof value !== "number" || !Number.isInteger(value)) {
|
|
76
|
+
throw new Error("gateway.mailbox.batch_window_ms must be an integer when present");
|
|
77
|
+
}
|
|
78
|
+
if (value < 0 || value > 60_000) {
|
|
79
|
+
throw new Error("gateway.mailbox.batch_window_ms must be between 0 and 60000");
|
|
80
|
+
}
|
|
81
|
+
return value;
|
|
82
|
+
}
|
|
83
|
+
function readMailboxRoutes(value) {
|
|
84
|
+
if (value === undefined) {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
if (!Array.isArray(value)) {
|
|
88
|
+
throw new Error("gateway.mailbox.routes must be an array when present");
|
|
89
|
+
}
|
|
90
|
+
const routes = value.map((entry, index) => readMailboxRoute(entry, index));
|
|
91
|
+
const seen = new Set();
|
|
92
|
+
for (const route of routes) {
|
|
93
|
+
const key = `${route.channel}:${route.target}:${route.topic ?? ""}`;
|
|
94
|
+
if (seen.has(key)) {
|
|
95
|
+
throw new Error(`gateway.mailbox.routes contains a duplicate match for ${key}`);
|
|
96
|
+
}
|
|
97
|
+
seen.add(key);
|
|
98
|
+
}
|
|
99
|
+
return routes;
|
|
100
|
+
}
|
|
101
|
+
function readMailboxRoute(value, index) {
|
|
102
|
+
const field = `gateway.mailbox.routes[${index}]`;
|
|
103
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
104
|
+
throw new Error(`${field} must be a table`);
|
|
105
|
+
}
|
|
106
|
+
const route = value;
|
|
107
|
+
return {
|
|
108
|
+
channel: readRequiredString(route.channel, `${field}.channel`),
|
|
109
|
+
target: readRequiredString(route.target, `${field}.target`),
|
|
110
|
+
topic: readOptionalString(route.topic, `${field}.topic`),
|
|
111
|
+
mailboxKey: readRequiredString(route.mailbox_key, `${field}.mailbox_key`),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function readRequiredString(value, field) {
|
|
115
|
+
if (typeof value !== "string") {
|
|
116
|
+
throw new Error(`${field} must be a string`);
|
|
117
|
+
}
|
|
118
|
+
const trimmed = value.trim();
|
|
119
|
+
if (trimmed.length === 0) {
|
|
120
|
+
throw new Error(`${field} must not be empty`);
|
|
121
|
+
}
|
|
122
|
+
return trimmed;
|
|
123
|
+
}
|
|
124
|
+
function readOptionalString(value, field) {
|
|
125
|
+
if (value === undefined) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
return readRequiredString(value, field);
|
|
129
|
+
}
|
|
130
|
+
function readLegacyGatewayTimezone(value) {
|
|
131
|
+
if (typeof value !== "string") {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
const trimmed = value.trim();
|
|
135
|
+
return trimmed.length === 0 ? null : trimmed;
|
|
136
|
+
}
|
|
137
|
+
function defaultStateDbPath(env) {
|
|
138
|
+
return defaultGatewayStateDbPath(env);
|
|
139
|
+
}
|
|
140
|
+
function resolveMediaRootPath(stateDbPath) {
|
|
141
|
+
return join(dirname(stateDbPath), "media");
|
|
142
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const GATEWAY_CONFIG_FILE = "opencode-gateway.toml";
|
|
2
|
+
export declare const OPENCODE_CONFIG_FILE = "opencode.json";
|
|
3
|
+
type EnvSource = Record<string, string | undefined>;
|
|
4
|
+
export declare function resolveGatewayConfigPath(env: EnvSource): string;
|
|
5
|
+
export declare function resolveOpencodeConfigDir(env: EnvSource): string;
|
|
6
|
+
export declare function resolveManagedOpencodeConfigDir(env: EnvSource): string;
|
|
7
|
+
export declare function defaultGatewayStateDbPath(env: EnvSource): string;
|
|
8
|
+
export declare function resolveConfigHome(env: EnvSource): string;
|
|
9
|
+
export declare function resolveDataHome(env: EnvSource): string;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
export const GATEWAY_CONFIG_FILE = "opencode-gateway.toml";
|
|
4
|
+
export const OPENCODE_CONFIG_FILE = "opencode.json";
|
|
5
|
+
export function resolveGatewayConfigPath(env) {
|
|
6
|
+
const explicit = env.OPENCODE_GATEWAY_CONFIG;
|
|
7
|
+
if (explicit && explicit.trim().length > 0) {
|
|
8
|
+
return resolve(explicit);
|
|
9
|
+
}
|
|
10
|
+
return join(resolveOpencodeConfigDir(env), GATEWAY_CONFIG_FILE);
|
|
11
|
+
}
|
|
12
|
+
export function resolveOpencodeConfigDir(env) {
|
|
13
|
+
const explicit = env.OPENCODE_CONFIG_DIR;
|
|
14
|
+
if (explicit && explicit.trim().length > 0) {
|
|
15
|
+
return resolve(explicit);
|
|
16
|
+
}
|
|
17
|
+
return defaultOpencodeConfigDir(env);
|
|
18
|
+
}
|
|
19
|
+
export function resolveManagedOpencodeConfigDir(env) {
|
|
20
|
+
return join(resolveConfigHome(env), "opencode-gateway", "opencode");
|
|
21
|
+
}
|
|
22
|
+
export function defaultGatewayStateDbPath(env) {
|
|
23
|
+
return join(resolveDataHome(env), "opencode-gateway", "state.db");
|
|
24
|
+
}
|
|
25
|
+
export function resolveConfigHome(env) {
|
|
26
|
+
return env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
|
|
27
|
+
}
|
|
28
|
+
export function resolveDataHome(env) {
|
|
29
|
+
return env.XDG_DATA_HOME ?? join(homedir(), ".local", "share");
|
|
30
|
+
}
|
|
31
|
+
function defaultOpencodeConfigDir(env) {
|
|
32
|
+
return join(resolveConfigHome(env), "opencode");
|
|
33
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
type EnvSource = Record<string, string | undefined>;
|
|
2
|
+
export type TelegramConfig = {
|
|
3
|
+
enabled: false;
|
|
4
|
+
} | {
|
|
5
|
+
enabled: true;
|
|
6
|
+
botToken: string;
|
|
7
|
+
botTokenEnv: string;
|
|
8
|
+
pollTimeoutSeconds: number;
|
|
9
|
+
allowedChats: string[];
|
|
10
|
+
allowedUsers: string[];
|
|
11
|
+
};
|
|
12
|
+
export declare function parseTelegramConfig(value: unknown, env: EnvSource): TelegramConfig;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
export function parseTelegramConfig(value, env) {
|
|
2
|
+
const table = readTelegramTable(value);
|
|
3
|
+
const enabled = readBoolean(table.enabled, "channels.telegram.enabled", false);
|
|
4
|
+
if (!enabled) {
|
|
5
|
+
return { enabled: false };
|
|
6
|
+
}
|
|
7
|
+
const botTokenEnv = readString(table.bot_token_env, "channels.telegram.bot_token_env", "TELEGRAM_BOT_TOKEN");
|
|
8
|
+
const pollTimeoutSeconds = readPollTimeoutSeconds(table.poll_timeout_seconds);
|
|
9
|
+
const allowedChats = readIdentifierList(table.allowed_chats, "channels.telegram.allowed_chats");
|
|
10
|
+
const allowedUsers = readIdentifierList(table.allowed_users, "channels.telegram.allowed_users");
|
|
11
|
+
const botToken = env[botTokenEnv]?.trim();
|
|
12
|
+
if (!botToken) {
|
|
13
|
+
throw new Error(`Telegram is enabled but ${botTokenEnv} is not set`);
|
|
14
|
+
}
|
|
15
|
+
if (allowedChats.length === 0 && allowedUsers.length === 0) {
|
|
16
|
+
throw new Error("Telegram is enabled but no allowlist entries were configured");
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
enabled: true,
|
|
20
|
+
botToken,
|
|
21
|
+
botTokenEnv,
|
|
22
|
+
pollTimeoutSeconds,
|
|
23
|
+
allowedChats,
|
|
24
|
+
allowedUsers,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function readTelegramTable(value) {
|
|
28
|
+
if (value === undefined) {
|
|
29
|
+
return {};
|
|
30
|
+
}
|
|
31
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
32
|
+
throw new Error("channels.telegram must be a table when present");
|
|
33
|
+
}
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
function readBoolean(value, field, fallback) {
|
|
37
|
+
if (value === undefined) {
|
|
38
|
+
return fallback;
|
|
39
|
+
}
|
|
40
|
+
if (typeof value !== "boolean") {
|
|
41
|
+
throw new Error(`${field} must be a boolean when present`);
|
|
42
|
+
}
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
function readString(value, field, fallback) {
|
|
46
|
+
if (value === undefined) {
|
|
47
|
+
return fallback;
|
|
48
|
+
}
|
|
49
|
+
if (typeof value !== "string") {
|
|
50
|
+
throw new Error(`${field} must be a string when present`);
|
|
51
|
+
}
|
|
52
|
+
const trimmed = value.trim();
|
|
53
|
+
if (trimmed.length === 0) {
|
|
54
|
+
throw new Error(`${field} must not be empty`);
|
|
55
|
+
}
|
|
56
|
+
return trimmed;
|
|
57
|
+
}
|
|
58
|
+
function readPollTimeoutSeconds(value) {
|
|
59
|
+
if (value === undefined) {
|
|
60
|
+
return 25;
|
|
61
|
+
}
|
|
62
|
+
if (typeof value !== "number" || !Number.isInteger(value)) {
|
|
63
|
+
throw new Error("channels.telegram.poll_timeout_seconds must be an integer when present");
|
|
64
|
+
}
|
|
65
|
+
if (value < 1 || value > 50) {
|
|
66
|
+
throw new Error("channels.telegram.poll_timeout_seconds must be between 1 and 50");
|
|
67
|
+
}
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
function readIdentifierList(value, field) {
|
|
71
|
+
if (value === undefined) {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
if (!Array.isArray(value)) {
|
|
75
|
+
throw new Error(`${field} must be an array when present`);
|
|
76
|
+
}
|
|
77
|
+
return value.map((entry) => normalizeIdentifier(entry, field));
|
|
78
|
+
}
|
|
79
|
+
function normalizeIdentifier(value, field) {
|
|
80
|
+
if (typeof value === "string") {
|
|
81
|
+
const trimmed = value.trim();
|
|
82
|
+
if (trimmed.length === 0) {
|
|
83
|
+
throw new Error(`${field} entries must not be empty`);
|
|
84
|
+
}
|
|
85
|
+
return trimmed;
|
|
86
|
+
}
|
|
87
|
+
if (typeof value === "number" && Number.isSafeInteger(value)) {
|
|
88
|
+
return String(value);
|
|
89
|
+
}
|
|
90
|
+
throw new Error(`${field} entries must be strings or safe integers`);
|
|
91
|
+
}
|