opencode-router 0.11.77 → 0.11.79
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/bridge.js +1456 -0
- package/dist/cli.js +553 -0
- package/dist/config.js +195 -0
- package/dist/db.js +196 -0
- package/dist/events.js +11 -0
- package/dist/health.js +499 -0
- package/dist/logger.js +14 -0
- package/dist/opencode.js +34 -0
- package/dist/slack.js +169 -0
- package/dist/telegram.js +78 -0
- package/dist/text.js +41 -0
- package/package.json +1 -1
package/dist/cli.js
ADDED
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import { Bot } from "grammy";
|
|
5
|
+
import { WebClient } from "@slack/web-api";
|
|
6
|
+
import { startBridge } from "./bridge.js";
|
|
7
|
+
import { loadConfig, readConfigFile, writeConfigFile, } from "./config.js";
|
|
8
|
+
import { BridgeStore } from "./db.js";
|
|
9
|
+
import { createLogger } from "./logger.js";
|
|
10
|
+
import { createClient } from "./opencode.js";
|
|
11
|
+
import { parseSlackPeerId } from "./slack.js";
|
|
12
|
+
import { truncateText } from "./text.js";
|
|
13
|
+
const VERSION = (() => {
|
|
14
|
+
if (typeof __OPENCODE_ROUTER_VERSION__ === "string" && __OPENCODE_ROUTER_VERSION__.trim()) {
|
|
15
|
+
return __OPENCODE_ROUTER_VERSION__.trim();
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
const pkgPath = new URL("../package.json", import.meta.url);
|
|
19
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
20
|
+
if (typeof pkg.version === "string" && pkg.version.trim()) {
|
|
21
|
+
return pkg.version.trim();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// ignore
|
|
26
|
+
}
|
|
27
|
+
return "0.0.0";
|
|
28
|
+
})();
|
|
29
|
+
function outputJson(data) {
|
|
30
|
+
console.log(JSON.stringify(data, null, 2));
|
|
31
|
+
}
|
|
32
|
+
function outputError(message, exitCode = 1) {
|
|
33
|
+
if (program.opts().json) {
|
|
34
|
+
outputJson({ error: message });
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
console.error(`Error: ${message}`);
|
|
38
|
+
}
|
|
39
|
+
process.exit(exitCode);
|
|
40
|
+
}
|
|
41
|
+
function createAppLogger(config) {
|
|
42
|
+
return createLogger(config.logLevel, { logFile: config.logFile });
|
|
43
|
+
}
|
|
44
|
+
function createConsoleReporter() {
|
|
45
|
+
const formatChannel = (channel, identityId) => {
|
|
46
|
+
const name = channel === "telegram" ? "Telegram" : "Slack";
|
|
47
|
+
return `${name}/${identityId}`;
|
|
48
|
+
};
|
|
49
|
+
const printBlock = (prefix, text) => {
|
|
50
|
+
const lines = text.split(/\r?\n/).map((line) => truncateText(line.trim(), 240));
|
|
51
|
+
const [first, ...rest] = lines.length ? lines : ["(empty)"];
|
|
52
|
+
console.log(`${prefix} ${first}`);
|
|
53
|
+
for (const line of rest) {
|
|
54
|
+
console.log(`${" ".repeat(prefix.length)} ${line}`);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
return {
|
|
58
|
+
onStatus(message) {
|
|
59
|
+
console.log(message);
|
|
60
|
+
},
|
|
61
|
+
onInbound({ channel, identityId, peerId, text, fromMe }) {
|
|
62
|
+
const base = fromMe ? `${peerId} (me)` : peerId;
|
|
63
|
+
const prefix = `[${formatChannel(channel, identityId)}] ${base} >`;
|
|
64
|
+
printBlock(prefix, text);
|
|
65
|
+
},
|
|
66
|
+
onOutbound({ channel, identityId, peerId, text, kind }) {
|
|
67
|
+
const marker = kind === "reply" ? "<" : kind === "tool" ? "*" : "!";
|
|
68
|
+
const prefix = `[${formatChannel(channel, identityId)}] ${peerId} ${marker}`;
|
|
69
|
+
printBlock(prefix, text);
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function updateConfig(configPath, updater) {
|
|
74
|
+
const { config } = readConfigFile(configPath);
|
|
75
|
+
const base = config ?? { version: 1 };
|
|
76
|
+
const next = updater(base);
|
|
77
|
+
next.version = next.version ?? 1;
|
|
78
|
+
writeConfigFile(configPath, next);
|
|
79
|
+
return next;
|
|
80
|
+
}
|
|
81
|
+
function normalizeIdentityId(value) {
|
|
82
|
+
const trimmed = (value ?? "").trim();
|
|
83
|
+
if (!trimmed)
|
|
84
|
+
return "default";
|
|
85
|
+
const safe = trimmed.replace(/[^a-zA-Z0-9_.-]+/g, "-");
|
|
86
|
+
const cleaned = safe.replace(/^-+|-+$/g, "").slice(0, 48);
|
|
87
|
+
return cleaned || "default";
|
|
88
|
+
}
|
|
89
|
+
function upsertTelegramBot(cfg, identity) {
|
|
90
|
+
const next = { ...cfg };
|
|
91
|
+
next.channels = next.channels ?? {};
|
|
92
|
+
const existing = next.channels.telegram ?? {};
|
|
93
|
+
const bots = Array.isArray(existing.bots) ? existing.bots.slice() : [];
|
|
94
|
+
const id = normalizeIdentityId(identity.id);
|
|
95
|
+
const filtered = bots.filter((b) => normalizeIdentityId(b.id) !== id);
|
|
96
|
+
filtered.push({ id, token: identity.token, enabled: identity.enabled !== false });
|
|
97
|
+
next.channels.telegram = { ...existing, enabled: true, bots: filtered };
|
|
98
|
+
return next;
|
|
99
|
+
}
|
|
100
|
+
function deleteTelegramBot(cfg, idRaw) {
|
|
101
|
+
const id = normalizeIdentityId(idRaw);
|
|
102
|
+
const next = { ...cfg };
|
|
103
|
+
next.channels = next.channels ?? {};
|
|
104
|
+
const existing = next.channels.telegram ?? {};
|
|
105
|
+
const bots = Array.isArray(existing.bots) ? existing.bots.slice() : [];
|
|
106
|
+
const filtered = bots.filter((b) => normalizeIdentityId(b.id) !== id);
|
|
107
|
+
const deleted = filtered.length !== bots.length;
|
|
108
|
+
next.channels.telegram = { ...existing, bots: filtered };
|
|
109
|
+
return { next, deleted };
|
|
110
|
+
}
|
|
111
|
+
function upsertSlackApp(cfg, identity) {
|
|
112
|
+
const next = { ...cfg };
|
|
113
|
+
next.channels = next.channels ?? {};
|
|
114
|
+
const existing = next.channels.slack ?? {};
|
|
115
|
+
const apps = Array.isArray(existing.apps) ? existing.apps.slice() : [];
|
|
116
|
+
const id = normalizeIdentityId(identity.id);
|
|
117
|
+
const filtered = apps.filter((a) => normalizeIdentityId(a.id) !== id);
|
|
118
|
+
filtered.push({ id, botToken: identity.botToken, appToken: identity.appToken, enabled: identity.enabled !== false });
|
|
119
|
+
next.channels.slack = { ...existing, enabled: true, apps: filtered };
|
|
120
|
+
return next;
|
|
121
|
+
}
|
|
122
|
+
function deleteSlackApp(cfg, idRaw) {
|
|
123
|
+
const id = normalizeIdentityId(idRaw);
|
|
124
|
+
const next = { ...cfg };
|
|
125
|
+
next.channels = next.channels ?? {};
|
|
126
|
+
const existing = next.channels.slack ?? {};
|
|
127
|
+
const apps = Array.isArray(existing.apps) ? existing.apps.slice() : [];
|
|
128
|
+
const filtered = apps.filter((a) => normalizeIdentityId(a.id) !== id);
|
|
129
|
+
const deleted = filtered.length !== apps.length;
|
|
130
|
+
next.channels.slack = { ...existing, apps: filtered };
|
|
131
|
+
return { next, deleted };
|
|
132
|
+
}
|
|
133
|
+
async function runStart(pathOverride, options) {
|
|
134
|
+
if (pathOverride?.trim()) {
|
|
135
|
+
process.env.OPENCODE_DIRECTORY = pathOverride.trim();
|
|
136
|
+
}
|
|
137
|
+
if (options?.opencodeUrl?.trim()) {
|
|
138
|
+
process.env.OPENCODE_URL = options.opencodeUrl.trim();
|
|
139
|
+
}
|
|
140
|
+
const config = loadConfig();
|
|
141
|
+
const logger = createAppLogger(config);
|
|
142
|
+
const reporter = createConsoleReporter();
|
|
143
|
+
if (!process.env.OPENCODE_DIRECTORY) {
|
|
144
|
+
process.env.OPENCODE_DIRECTORY = config.opencodeDirectory;
|
|
145
|
+
}
|
|
146
|
+
const bridge = await startBridge(config, logger, reporter);
|
|
147
|
+
if (process.stdout.isTTY) {
|
|
148
|
+
reporter.onStatus?.("Commands: opencode-router identities, opencode-router bindings, opencode-router status");
|
|
149
|
+
}
|
|
150
|
+
const shutdown = async () => {
|
|
151
|
+
logger.info("shutting down");
|
|
152
|
+
await bridge.stop();
|
|
153
|
+
process.exit(0);
|
|
154
|
+
};
|
|
155
|
+
process.on("SIGINT", shutdown);
|
|
156
|
+
process.on("SIGTERM", shutdown);
|
|
157
|
+
}
|
|
158
|
+
const program = new Command();
|
|
159
|
+
program
|
|
160
|
+
.name("opencode-router")
|
|
161
|
+
.version(VERSION)
|
|
162
|
+
.description("opencode-router: Slack + Telegram bridge + directory routing")
|
|
163
|
+
.option("--json", "Output in JSON format", false);
|
|
164
|
+
program
|
|
165
|
+
.command("start")
|
|
166
|
+
.description("Start the bridge")
|
|
167
|
+
.argument("[path]", "opencode workspace path")
|
|
168
|
+
.option("--opencode-url <url>", "opencode server URL")
|
|
169
|
+
.action((pathArg, options) => runStart(pathArg, options));
|
|
170
|
+
program
|
|
171
|
+
.command("serve")
|
|
172
|
+
.description("Start the bridge (headless)")
|
|
173
|
+
.argument("[path]", "opencode workspace path")
|
|
174
|
+
.option("--opencode-url <url>", "opencode server URL")
|
|
175
|
+
.action((pathArg, options) => runStart(pathArg, options));
|
|
176
|
+
program
|
|
177
|
+
.command("health")
|
|
178
|
+
.description("Check opencode health (exit 0 if healthy, 1 if not)")
|
|
179
|
+
.action(async () => {
|
|
180
|
+
const useJson = program.opts().json;
|
|
181
|
+
const config = loadConfig(process.env, { requireOpencode: false });
|
|
182
|
+
try {
|
|
183
|
+
const client = createClient(config);
|
|
184
|
+
const health = await client.global.health();
|
|
185
|
+
const healthy = Boolean(health.healthy);
|
|
186
|
+
if (useJson) {
|
|
187
|
+
outputJson({
|
|
188
|
+
healthy,
|
|
189
|
+
opencodeUrl: config.opencodeUrl,
|
|
190
|
+
identities: {
|
|
191
|
+
telegram: config.telegramBots.map((b) => ({ id: b.id, enabled: b.enabled !== false })),
|
|
192
|
+
slack: config.slackApps.map((a) => ({ id: a.id, enabled: a.enabled !== false })),
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
console.log(`Healthy: ${healthy ? "yes" : "no"}`);
|
|
198
|
+
console.log(`opencode URL: ${config.opencodeUrl}`);
|
|
199
|
+
}
|
|
200
|
+
process.exit(healthy ? 0 : 1);
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
if (useJson) {
|
|
204
|
+
outputJson({
|
|
205
|
+
healthy: false,
|
|
206
|
+
error: String(error),
|
|
207
|
+
opencodeUrl: config.opencodeUrl,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
console.log("Healthy: no");
|
|
212
|
+
console.log(`Error: ${String(error)}`);
|
|
213
|
+
}
|
|
214
|
+
process.exit(1);
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
program
|
|
218
|
+
.command("status")
|
|
219
|
+
.description("Show identity and opencode status")
|
|
220
|
+
.action(() => {
|
|
221
|
+
const useJson = program.opts().json;
|
|
222
|
+
const config = loadConfig(process.env, { requireOpencode: false });
|
|
223
|
+
const telegram = config.telegramBots.map((b) => ({ id: b.id, enabled: b.enabled !== false }));
|
|
224
|
+
const slack = config.slackApps.map((a) => ({ id: a.id, enabled: a.enabled !== false }));
|
|
225
|
+
if (useJson) {
|
|
226
|
+
outputJson({
|
|
227
|
+
config: config.configPath,
|
|
228
|
+
healthPort: config.healthPort ?? null,
|
|
229
|
+
telegram,
|
|
230
|
+
slack,
|
|
231
|
+
opencode: { url: config.opencodeUrl, directory: config.opencodeDirectory },
|
|
232
|
+
});
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
console.log(`Config: ${config.configPath}`);
|
|
236
|
+
console.log(`Health port: ${config.healthPort ?? "(not set)"}`);
|
|
237
|
+
console.log(`Telegram bots: ${telegram.length}`);
|
|
238
|
+
console.log(`Slack apps: ${slack.length}`);
|
|
239
|
+
console.log(`opencode URL: ${config.opencodeUrl}`);
|
|
240
|
+
});
|
|
241
|
+
// -----------------------------------------------------------------------------
|
|
242
|
+
// Config helpers
|
|
243
|
+
// -----------------------------------------------------------------------------
|
|
244
|
+
const configCmd = program.command("config").description("Manage configuration");
|
|
245
|
+
configCmd
|
|
246
|
+
.command("get")
|
|
247
|
+
.argument("[key]", "Config key to get (dot notation)")
|
|
248
|
+
.description("Get config value(s)")
|
|
249
|
+
.action((key) => {
|
|
250
|
+
const useJson = program.opts().json;
|
|
251
|
+
const config = loadConfig(process.env, { requireOpencode: false });
|
|
252
|
+
const { config: configFile } = readConfigFile(config.configPath);
|
|
253
|
+
if (!key) {
|
|
254
|
+
if (useJson)
|
|
255
|
+
outputJson(configFile);
|
|
256
|
+
else
|
|
257
|
+
console.log(JSON.stringify(configFile, null, 2));
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const keys = key.split(".");
|
|
261
|
+
let current = configFile;
|
|
262
|
+
for (const k of keys) {
|
|
263
|
+
if (current === null || current === undefined || typeof current !== "object") {
|
|
264
|
+
current = undefined;
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
current = current[k];
|
|
268
|
+
}
|
|
269
|
+
if (useJson)
|
|
270
|
+
outputJson({ [key]: current });
|
|
271
|
+
else
|
|
272
|
+
console.log(`${key}: ${current === undefined ? "(not set)" : typeof current === "object" ? JSON.stringify(current, null, 2) : current}`);
|
|
273
|
+
});
|
|
274
|
+
configCmd
|
|
275
|
+
.command("set")
|
|
276
|
+
.argument("<key>", "Config key to set (dot notation)")
|
|
277
|
+
.argument("<value>", "Value to set (JSON for arrays/objects)")
|
|
278
|
+
.description("Set config value")
|
|
279
|
+
.action((key, value) => {
|
|
280
|
+
const useJson = program.opts().json;
|
|
281
|
+
const config = loadConfig(process.env, { requireOpencode: false });
|
|
282
|
+
const parsed = (() => {
|
|
283
|
+
try {
|
|
284
|
+
return JSON.parse(value);
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
return value;
|
|
288
|
+
}
|
|
289
|
+
})();
|
|
290
|
+
const updated = updateConfig(config.configPath, (cfg) => {
|
|
291
|
+
const next = { ...cfg };
|
|
292
|
+
const keys = key.split(".");
|
|
293
|
+
let cur = next;
|
|
294
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
295
|
+
const k = keys[i];
|
|
296
|
+
if (cur[k] === undefined || cur[k] === null || typeof cur[k] !== "object")
|
|
297
|
+
cur[k] = {};
|
|
298
|
+
cur = cur[k];
|
|
299
|
+
}
|
|
300
|
+
cur[keys[keys.length - 1]] = parsed;
|
|
301
|
+
return next;
|
|
302
|
+
});
|
|
303
|
+
if (useJson)
|
|
304
|
+
outputJson({ success: true, key, value: parsed, config: updated });
|
|
305
|
+
else
|
|
306
|
+
console.log(`Set ${key} = ${JSON.stringify(parsed)}`);
|
|
307
|
+
});
|
|
308
|
+
// -----------------------------------------------------------------------------
|
|
309
|
+
// Identities
|
|
310
|
+
// -----------------------------------------------------------------------------
|
|
311
|
+
const telegram = program.command("telegram").description("Telegram identities");
|
|
312
|
+
telegram
|
|
313
|
+
.command("list")
|
|
314
|
+
.description("List Telegram bot identities")
|
|
315
|
+
.action(() => {
|
|
316
|
+
const useJson = program.opts().json;
|
|
317
|
+
const config = loadConfig(process.env, { requireOpencode: false });
|
|
318
|
+
const items = config.telegramBots.map((b) => ({ id: b.id, enabled: b.enabled !== false }));
|
|
319
|
+
if (useJson)
|
|
320
|
+
outputJson({ items });
|
|
321
|
+
else
|
|
322
|
+
for (const item of items)
|
|
323
|
+
console.log(`${item.id} ${item.enabled ? "enabled" : "disabled"}`);
|
|
324
|
+
});
|
|
325
|
+
telegram
|
|
326
|
+
.command("add")
|
|
327
|
+
.argument("<token>", "Telegram bot token")
|
|
328
|
+
.option("--id <id>", "Identity id (default: default)")
|
|
329
|
+
.option("--disabled", "Add identity but disable it", false)
|
|
330
|
+
.description("Add or update a Telegram bot identity")
|
|
331
|
+
.action((token, opts) => {
|
|
332
|
+
const useJson = program.opts().json;
|
|
333
|
+
const config = loadConfig(process.env, { requireOpencode: false });
|
|
334
|
+
const id = normalizeIdentityId(opts.id);
|
|
335
|
+
const enabled = !opts.disabled;
|
|
336
|
+
updateConfig(config.configPath, (cfg) => upsertTelegramBot(cfg, { id, token: token.trim(), enabled }));
|
|
337
|
+
if (useJson)
|
|
338
|
+
outputJson({ success: true, id, enabled });
|
|
339
|
+
else
|
|
340
|
+
console.log(`Saved Telegram identity: ${id}`);
|
|
341
|
+
});
|
|
342
|
+
telegram
|
|
343
|
+
.command("remove")
|
|
344
|
+
.argument("<id>", "Identity id")
|
|
345
|
+
.description("Remove a Telegram bot identity")
|
|
346
|
+
.action((idRaw) => {
|
|
347
|
+
const useJson = program.opts().json;
|
|
348
|
+
const config = loadConfig(process.env, { requireOpencode: false });
|
|
349
|
+
const { next, deleted } = deleteTelegramBot(readConfigFile(config.configPath).config, idRaw);
|
|
350
|
+
writeConfigFile(config.configPath, next);
|
|
351
|
+
if (useJson)
|
|
352
|
+
outputJson({ success: deleted, id: normalizeIdentityId(idRaw) });
|
|
353
|
+
else
|
|
354
|
+
console.log(deleted ? `Removed Telegram identity: ${normalizeIdentityId(idRaw)}` : "Identity not found.");
|
|
355
|
+
process.exit(deleted ? 0 : 1);
|
|
356
|
+
});
|
|
357
|
+
const slack = program.command("slack").description("Slack identities");
|
|
358
|
+
slack
|
|
359
|
+
.command("list")
|
|
360
|
+
.description("List Slack app identities")
|
|
361
|
+
.action(() => {
|
|
362
|
+
const useJson = program.opts().json;
|
|
363
|
+
const config = loadConfig(process.env, { requireOpencode: false });
|
|
364
|
+
const items = config.slackApps.map((a) => ({ id: a.id, enabled: a.enabled !== false }));
|
|
365
|
+
if (useJson)
|
|
366
|
+
outputJson({ items });
|
|
367
|
+
else
|
|
368
|
+
for (const item of items)
|
|
369
|
+
console.log(`${item.id} ${item.enabled ? "enabled" : "disabled"}`);
|
|
370
|
+
});
|
|
371
|
+
slack
|
|
372
|
+
.command("add")
|
|
373
|
+
.argument("<botToken>", "Slack bot token (xoxb-...)")
|
|
374
|
+
.argument("<appToken>", "Slack app token (xapp-...)")
|
|
375
|
+
.option("--id <id>", "Identity id (default: default)")
|
|
376
|
+
.option("--disabled", "Add identity but disable it", false)
|
|
377
|
+
.description("Add or update a Slack app identity")
|
|
378
|
+
.action((botToken, appToken, opts) => {
|
|
379
|
+
const useJson = program.opts().json;
|
|
380
|
+
const config = loadConfig(process.env, { requireOpencode: false });
|
|
381
|
+
const id = normalizeIdentityId(opts.id);
|
|
382
|
+
const enabled = !opts.disabled;
|
|
383
|
+
updateConfig(config.configPath, (cfg) => upsertSlackApp(cfg, { id, botToken: botToken.trim(), appToken: appToken.trim(), enabled }));
|
|
384
|
+
if (useJson)
|
|
385
|
+
outputJson({ success: true, id, enabled });
|
|
386
|
+
else
|
|
387
|
+
console.log(`Saved Slack identity: ${id}`);
|
|
388
|
+
});
|
|
389
|
+
slack
|
|
390
|
+
.command("remove")
|
|
391
|
+
.argument("<id>", "Identity id")
|
|
392
|
+
.description("Remove a Slack identity")
|
|
393
|
+
.action((idRaw) => {
|
|
394
|
+
const useJson = program.opts().json;
|
|
395
|
+
const config = loadConfig(process.env, { requireOpencode: false });
|
|
396
|
+
const { next, deleted } = deleteSlackApp(readConfigFile(config.configPath).config, idRaw);
|
|
397
|
+
writeConfigFile(config.configPath, next);
|
|
398
|
+
if (useJson)
|
|
399
|
+
outputJson({ success: deleted, id: normalizeIdentityId(idRaw) });
|
|
400
|
+
else
|
|
401
|
+
console.log(deleted ? `Removed Slack identity: ${normalizeIdentityId(idRaw)}` : "Identity not found.");
|
|
402
|
+
process.exit(deleted ? 0 : 1);
|
|
403
|
+
});
|
|
404
|
+
// -----------------------------------------------------------------------------
|
|
405
|
+
// Bindings
|
|
406
|
+
// -----------------------------------------------------------------------------
|
|
407
|
+
const bindings = program.command("bindings").description("Manage identity-scoped bindings");
|
|
408
|
+
bindings
|
|
409
|
+
.command("list")
|
|
410
|
+
.option("--channel <channel>", "telegram|slack")
|
|
411
|
+
.option("--identity <id>", "Identity id")
|
|
412
|
+
.description("List bindings")
|
|
413
|
+
.action((opts) => {
|
|
414
|
+
const useJson = program.opts().json;
|
|
415
|
+
const config = loadConfig(process.env, { requireOpencode: false });
|
|
416
|
+
const store = new BridgeStore(config.dbPath);
|
|
417
|
+
const channelRaw = opts.channel?.trim().toLowerCase();
|
|
418
|
+
const identityId = opts.identity?.trim() ? normalizeIdentityId(opts.identity) : undefined;
|
|
419
|
+
const channel = channelRaw === "telegram" || channelRaw === "slack" ? channelRaw : channelRaw ? (outputError("Invalid channel"), undefined) : undefined;
|
|
420
|
+
const items = store
|
|
421
|
+
.listBindings({ ...(channel ? { channel } : {}), ...(identityId ? { identityId } : {}) })
|
|
422
|
+
.map((b) => ({
|
|
423
|
+
channel: b.channel,
|
|
424
|
+
identityId: b.identity_id,
|
|
425
|
+
peerId: b.peer_id,
|
|
426
|
+
directory: b.directory,
|
|
427
|
+
updatedAt: b.updated_at,
|
|
428
|
+
}));
|
|
429
|
+
store.close();
|
|
430
|
+
if (useJson)
|
|
431
|
+
outputJson({ items });
|
|
432
|
+
else
|
|
433
|
+
for (const item of items)
|
|
434
|
+
console.log(`${item.channel}/${item.identityId} ${item.peerId} -> ${item.directory}`);
|
|
435
|
+
});
|
|
436
|
+
bindings
|
|
437
|
+
.command("set")
|
|
438
|
+
.requiredOption("--channel <channel>", "telegram|slack")
|
|
439
|
+
.requiredOption("--identity <id>", "Identity id")
|
|
440
|
+
.requiredOption("--peer <peerId>", "Peer id")
|
|
441
|
+
.requiredOption("--dir <directory>", "Directory")
|
|
442
|
+
.description("Set a binding")
|
|
443
|
+
.action((opts) => {
|
|
444
|
+
const useJson = program.opts().json;
|
|
445
|
+
const config = loadConfig(process.env, { requireOpencode: false });
|
|
446
|
+
const store = new BridgeStore(config.dbPath);
|
|
447
|
+
const channelRaw = opts.channel.trim().toLowerCase();
|
|
448
|
+
if (channelRaw !== "telegram" && channelRaw !== "slack")
|
|
449
|
+
outputError("Invalid channel");
|
|
450
|
+
const identityId = normalizeIdentityId(opts.identity);
|
|
451
|
+
const peerId = opts.peer.trim();
|
|
452
|
+
const directory = opts.dir.trim();
|
|
453
|
+
if (!peerId || !directory)
|
|
454
|
+
outputError("peer and dir are required");
|
|
455
|
+
store.upsertBinding(channelRaw, identityId, peerId, directory);
|
|
456
|
+
store.deleteSession(channelRaw, identityId, peerId);
|
|
457
|
+
store.close();
|
|
458
|
+
if (useJson)
|
|
459
|
+
outputJson({ success: true });
|
|
460
|
+
else
|
|
461
|
+
console.log("Binding saved.");
|
|
462
|
+
});
|
|
463
|
+
bindings
|
|
464
|
+
.command("clear")
|
|
465
|
+
.requiredOption("--channel <channel>", "telegram|slack")
|
|
466
|
+
.requiredOption("--identity <id>", "Identity id")
|
|
467
|
+
.requiredOption("--peer <peerId>", "Peer id")
|
|
468
|
+
.description("Clear a binding")
|
|
469
|
+
.action((opts) => {
|
|
470
|
+
const useJson = program.opts().json;
|
|
471
|
+
const config = loadConfig(process.env, { requireOpencode: false });
|
|
472
|
+
const store = new BridgeStore(config.dbPath);
|
|
473
|
+
const channelRaw = opts.channel.trim().toLowerCase();
|
|
474
|
+
if (channelRaw !== "telegram" && channelRaw !== "slack")
|
|
475
|
+
outputError("Invalid channel");
|
|
476
|
+
const identityId = normalizeIdentityId(opts.identity);
|
|
477
|
+
const peerId = opts.peer.trim();
|
|
478
|
+
const ok = store.deleteBinding(channelRaw, identityId, peerId);
|
|
479
|
+
store.deleteSession(channelRaw, identityId, peerId);
|
|
480
|
+
store.close();
|
|
481
|
+
if (useJson)
|
|
482
|
+
outputJson({ success: ok });
|
|
483
|
+
else
|
|
484
|
+
console.log(ok ? "Binding removed." : "Binding not found.");
|
|
485
|
+
process.exit(ok ? 0 : 1);
|
|
486
|
+
});
|
|
487
|
+
// -----------------------------------------------------------------------------
|
|
488
|
+
// Send helper
|
|
489
|
+
// -----------------------------------------------------------------------------
|
|
490
|
+
program
|
|
491
|
+
.command("send")
|
|
492
|
+
.description("Send a test message")
|
|
493
|
+
.requiredOption("--channel <channel>", "telegram or slack")
|
|
494
|
+
.requiredOption("--identity <id>", "Identity id")
|
|
495
|
+
.requiredOption("--to <recipient>", "Recipient ID (chat ID or peerId)")
|
|
496
|
+
.requiredOption("--message <text>", "Message text to send")
|
|
497
|
+
.action(async (opts) => {
|
|
498
|
+
const useJson = program.opts().json;
|
|
499
|
+
const channelRaw = opts.channel.trim().toLowerCase();
|
|
500
|
+
if (channelRaw !== "telegram" && channelRaw !== "slack") {
|
|
501
|
+
outputError("Invalid channel. Must be 'telegram' or 'slack'.");
|
|
502
|
+
}
|
|
503
|
+
const config = loadConfig(process.env, { requireOpencode: false });
|
|
504
|
+
const identityId = normalizeIdentityId(opts.identity);
|
|
505
|
+
const to = opts.to.trim();
|
|
506
|
+
const message = opts.message;
|
|
507
|
+
try {
|
|
508
|
+
if (channelRaw === "telegram") {
|
|
509
|
+
const bot = config.telegramBots.find((b) => b.id === identityId);
|
|
510
|
+
if (!bot)
|
|
511
|
+
throw new Error(`Telegram identity not found: ${identityId}`);
|
|
512
|
+
const tg = new Bot(bot.token);
|
|
513
|
+
await tg.api.sendMessage(Number(to), message);
|
|
514
|
+
}
|
|
515
|
+
else {
|
|
516
|
+
const app = config.slackApps.find((a) => a.id === identityId);
|
|
517
|
+
if (!app)
|
|
518
|
+
throw new Error(`Slack identity not found: ${identityId}`);
|
|
519
|
+
const web = new WebClient(app.botToken);
|
|
520
|
+
const peer = parseSlackPeerId(to);
|
|
521
|
+
if (!peer.channelId)
|
|
522
|
+
throw new Error("Invalid recipient for Slack.");
|
|
523
|
+
await web.chat.postMessage({
|
|
524
|
+
channel: peer.channelId,
|
|
525
|
+
text: message,
|
|
526
|
+
...(peer.threadTs ? { thread_ts: peer.threadTs } : {}),
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
if (useJson)
|
|
530
|
+
outputJson({ success: true });
|
|
531
|
+
else
|
|
532
|
+
console.log("Message sent.");
|
|
533
|
+
process.exit(0);
|
|
534
|
+
}
|
|
535
|
+
catch (error) {
|
|
536
|
+
if (useJson)
|
|
537
|
+
outputJson({ success: false, error: String(error) });
|
|
538
|
+
else
|
|
539
|
+
console.error(`Failed to send message: ${String(error)}`);
|
|
540
|
+
process.exit(1);
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
program.action(() => {
|
|
544
|
+
program.outputHelp();
|
|
545
|
+
});
|
|
546
|
+
program.parseAsync(process.argv).catch((error) => {
|
|
547
|
+
const useJson = program.opts().json;
|
|
548
|
+
if (useJson)
|
|
549
|
+
outputJson({ error: String(error) });
|
|
550
|
+
else
|
|
551
|
+
console.error(error);
|
|
552
|
+
process.exitCode = 1;
|
|
553
|
+
});
|