opencode-router 0.11.87 → 0.11.88

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/README.md CHANGED
@@ -50,13 +50,18 @@ opencode-router
50
50
 
51
51
  Telegram support is configured via identities. You can either:
52
52
  - Use env vars for a single bot: `TELEGRAM_BOT_TOKEN=...`
53
- - Or add multiple bots to the config file (`opencode-router.json`) using the CLI:
53
+ - Or add multiple bots to the config file (`opencode-router.json`) using the CLI:
54
54
 
55
55
  ```bash
56
56
  opencode-router telegram add <token> --id default
57
57
  opencode-router telegram list
58
58
  ```
59
59
 
60
+ Important for direct sends and bindings:
61
+ - Telegram targets must use numeric `chat_id` values.
62
+ - `@username` values are not valid direct `peerId` targets for router sends.
63
+ - If a user has not started a chat with the bot yet, Telegram may return `chat not found`.
64
+
60
65
  ## Slack (Socket Mode)
61
66
 
62
67
  Slack support uses Socket Mode and replies in threads when @mentioned in channels.
package/dist/bridge.js CHANGED
@@ -8,7 +8,7 @@ import { startHealthServer } from "./health.js";
8
8
  import { buildPermissionRules, createClient } from "./opencode.js";
9
9
  import { chunkText, formatInputSummary, truncateText } from "./text.js";
10
10
  import { createSlackAdapter } from "./slack.js";
11
- import { createTelegramAdapter } from "./telegram.js";
11
+ import { createTelegramAdapter, isTelegramPeerId } from "./telegram.js";
12
12
  async function startAdapterBounded(adapter, options) {
13
13
  const outcome = adapter
14
14
  .start()
@@ -50,6 +50,14 @@ const CHANNEL_LABELS = {
50
50
  const TYPING_INTERVAL_MS = 6000;
51
51
  const OPENCODE_ROUTER_AGENT_FILE_RELATIVE_PATH = ".opencode/agents/opencode-router.md";
52
52
  const OPENCODE_ROUTER_AGENT_MAX_CHARS = 16_000;
53
+ const DEFAULT_MESSAGING_AGENT_INSTRUCTIONS = [
54
+ "Respond for non-technical users first.",
55
+ "Do not tell users to run router commands; use tools on their behalf.",
56
+ "Never expose raw peer IDs or Telegram chat IDs unless the user explicitly asks for debug details.",
57
+ "For Telegram send requests, try delivery immediately using existing bindings or direct tool calls.",
58
+ "If Telegram returns 'chat not found', explain that the recipient must message the bot first (for example with /start), then ask the user to retry.",
59
+ "Keep status updates concise and action-oriented.",
60
+ ].join("\n");
53
61
  // Model presets for quick switching
54
62
  const MODEL_PRESETS = {
55
63
  opus: { providerID: "anthropic", modelID: "claude-opus-4-5-20251101" },
@@ -76,6 +84,11 @@ function setUserModel(channel, identityId, peerId, model) {
76
84
  function adapterKey(channel, identityId) {
77
85
  return `${channel}:${identityId}`;
78
86
  }
87
+ function invalidTelegramPeerIdError() {
88
+ const error = new Error("Telegram requires a numeric chat_id for direct targets. Usernames like @name cannot be used as peerId.");
89
+ error.status = 400;
90
+ return error;
91
+ }
79
92
  function normalizeIdentityId(value) {
80
93
  const trimmed = (value ?? "").trim();
81
94
  if (!trimmed)
@@ -792,6 +805,9 @@ export async function startBridge(config, logger, reporter, deps = {}) {
792
805
  if (!peerKey || !directory) {
793
806
  throw new Error("peerId and directory are required");
794
807
  }
808
+ if (channel === "telegram" && !isTelegramPeerId(peerKey)) {
809
+ throw invalidTelegramPeerIdError();
810
+ }
795
811
  const scoped = resolveScopedDirectory(directory);
796
812
  if (!scoped.ok) {
797
813
  const error = new Error(scoped.error);
@@ -833,6 +849,9 @@ export async function startBridge(config, logger, reporter, deps = {}) {
833
849
  if (!directoryInput && !peerId) {
834
850
  throw new Error("directory or peerId is required");
835
851
  }
852
+ if (channel === "telegram" && peerId && !isTelegramPeerId(peerId)) {
853
+ throw invalidTelegramPeerIdError();
854
+ }
836
855
  const normalizedDir = directoryInput ? (() => {
837
856
  const scoped = resolveScopedDirectory(directoryInput);
838
857
  if (!scoped.ok) {
@@ -935,6 +954,16 @@ export async function startBridge(config, logger, reporter, deps = {}) {
935
954
  let sent = 0;
936
955
  for (const binding of bindings) {
937
956
  attempted += 1;
957
+ if (channel === "telegram" && !isTelegramPeerId(binding.peer_id)) {
958
+ store.deleteBinding(channel, binding.identity_id, binding.peer_id);
959
+ store.deleteSession(channel, binding.identity_id, binding.peer_id);
960
+ failures.push({
961
+ identityId: binding.identity_id,
962
+ peerId: binding.peer_id,
963
+ error: "Invalid Telegram peerId binding removed (expected numeric chat_id)",
964
+ });
965
+ continue;
966
+ }
938
967
  const adapter = adapters.get(adapterKey(channel, binding.identity_id));
939
968
  if (!adapter) {
940
969
  failures.push({
@@ -1189,18 +1218,20 @@ export async function startBridge(config, logger, reporter, deps = {}) {
1189
1218
  try {
1190
1219
  const effectiveModel = getUserModel(inbound.channel, inbound.identityId, peerKey, config.model);
1191
1220
  const messagingAgent = await loadMessagingAgentConfig();
1192
- const promptText = messagingAgent.instructions
1193
- ? [
1194
- "You are handling a Slack/Telegram message via OpenWork.",
1195
- `Workspace agent file: ${messagingAgent.filePath}`,
1196
- ...(messagingAgent.selectedAgent ? [`Selected OpenCode agent: ${messagingAgent.selectedAgent}`] : []),
1197
- "Follow these workspace messaging instructions:",
1198
- messagingAgent.instructions,
1199
- "",
1200
- "Incoming user message:",
1201
- inbound.text,
1202
- ].join("\n")
1203
- : inbound.text;
1221
+ const effectiveInstructions = [DEFAULT_MESSAGING_AGENT_INSTRUCTIONS, messagingAgent.instructions]
1222
+ .map((value) => value.trim())
1223
+ .filter(Boolean)
1224
+ .join("\n\n");
1225
+ const promptText = [
1226
+ "You are handling a Slack/Telegram message via OpenWork.",
1227
+ `Workspace agent file: ${messagingAgent.filePath}`,
1228
+ ...(messagingAgent.selectedAgent ? [`Selected OpenCode agent: ${messagingAgent.selectedAgent}`] : []),
1229
+ "Follow these workspace messaging instructions:",
1230
+ effectiveInstructions,
1231
+ "",
1232
+ "Incoming user message:",
1233
+ inbound.text,
1234
+ ].join("\n");
1204
1235
  logger.debug({
1205
1236
  sessionID,
1206
1237
  length: inbound.text.length,
package/dist/telegram.js CHANGED
@@ -1,5 +1,18 @@
1
1
  import { Bot } from "grammy";
2
2
  const MAX_TEXT_LENGTH = 4096;
3
+ const TELEGRAM_CHAT_ID_PATTERN = /^-?\d+$/;
4
+ export function isTelegramPeerId(peerId) {
5
+ return TELEGRAM_CHAT_ID_PATTERN.test(peerId.trim());
6
+ }
7
+ export function parseTelegramPeerId(peerId) {
8
+ const trimmed = peerId.trim();
9
+ if (!isTelegramPeerId(trimmed))
10
+ return null;
11
+ const parsed = Number(trimmed);
12
+ if (!Number.isFinite(parsed))
13
+ return null;
14
+ return parsed;
15
+ }
3
16
  export function createTelegramAdapter(identity, config, logger, onMessage) {
4
17
  const token = identity.token?.trim() ?? "";
5
18
  if (!token) {
@@ -72,7 +85,13 @@ export function createTelegramAdapter(identity, config, logger, onMessage) {
72
85
  log.info("telegram adapter stopped");
73
86
  },
74
87
  async sendText(peerId, text) {
75
- await bot.api.sendMessage(Number(peerId), text);
88
+ const chatId = parseTelegramPeerId(peerId);
89
+ if (chatId === null) {
90
+ const error = new Error("Telegram peerId must be a numeric chat_id. Usernames like @name are not valid direct targets.");
91
+ error.status = 400;
92
+ throw error;
93
+ }
94
+ await bot.api.sendMessage(chatId, text);
76
95
  },
77
96
  };
78
97
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-router",
3
- "version": "0.11.87",
3
+ "version": "0.11.88",
4
4
  "description": "opencode-router: Slack + Telegram bridge + directory routing for a running opencode server",
5
5
  "private": false,
6
6
  "type": "module",