opencode-tbot 0.1.12 → 0.1.14

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
@@ -115,8 +115,7 @@ Notes:
115
115
 
116
116
  ## Commands
117
117
 
118
- - `/start` show the welcome message and quick-start steps
119
- - `/help` show the full command reference and examples
118
+ - `/start` show a short welcome message and quick-start steps
120
119
  - `/status` show aggregated OpenCode health, path, LSP, and MCP status
121
120
  - `/new [title]` create a new OpenCode session
122
121
  - `/agents` or `/agent` list available agents and switch the active one
package/README.zh-CN.md CHANGED
@@ -115,8 +115,7 @@ npm uninstall opencode-tbot
115
115
 
116
116
  ## 命令
117
117
 
118
- - `/start` 显示欢迎信息和快速开始说明
119
- - `/help` 显示完整命令说明和示例
118
+ - `/start` 显示简短欢迎信息和快速开始说明
120
119
  - `/status` 显示 OpenCode 健康状态、路径、LSP 和 MCP 信息
121
120
  - `/new [title]` 创建新的 OpenCode 会话
122
121
  - `/agents` 或 `/agent` 列出可用 agent 并切换当前 agent
@@ -31,9 +31,9 @@ function loadAppConfig(configSource = {}, options = {}) {
31
31
  return buildAppConfig(parseConfig(AppConfigSchema, configSource), options);
32
32
  }
33
33
  function buildAppConfig(data, options) {
34
- const openRouterApiKey = normalizeOptionalString(data.openrouter.apiKey);
35
- const openRouterModel = normalizeOptionalString(data.openrouter.model) ?? "openai/gpt-audio-mini";
36
- const transcriptionPrompt = normalizeOptionalString(data.openrouter.transcriptionPrompt);
34
+ const openRouterApiKey = normalizeOptionalString$1(data.openrouter.apiKey);
35
+ const openRouterModel = normalizeOptionalString$1(data.openrouter.model) ?? "openai/gpt-audio-mini";
36
+ const transcriptionPrompt = normalizeOptionalString$1(data.openrouter.transcriptionPrompt);
37
37
  return {
38
38
  telegramBotToken: data.telegram.botToken,
39
39
  telegramAllowedChatIds: data.telegram.allowedChatIds,
@@ -49,7 +49,7 @@ function buildAppConfig(data, options) {
49
49
  }
50
50
  };
51
51
  }
52
- function normalizeOptionalString(value) {
52
+ function normalizeOptionalString$1(value) {
53
53
  const normalized = value.trim();
54
54
  return normalized.length > 0 ? normalized : null;
55
55
  }
@@ -93,6 +93,7 @@ async function preparePluginConfiguration(options) {
93
93
  const projectConfigFilePath = await resolveProjectPluginConfigFilePath(options.cwd);
94
94
  const [globalConfig, projectConfig] = await Promise.all([loadPluginConfigFile(globalConfigFilePath), loadPluginConfigFile(projectConfigFilePath)]);
95
95
  const config = mergePluginConfigSources(globalConfig, projectConfig, options.config);
96
+ applyGlobalOpenRouterApiKey(config, globalConfig);
96
97
  const configFilePath = await pathExists(projectConfigFilePath) ? projectConfigFilePath : globalConfigFilePath;
97
98
  return {
98
99
  cwd: options.cwd,
@@ -139,6 +140,20 @@ function mergePluginConfigSources(...sources) {
139
140
  }
140
141
  return merged;
141
142
  }
143
+ function applyGlobalOpenRouterApiKey(config, globalConfig) {
144
+ const globalApiKey = normalizeOptionalString(globalConfig.openrouter?.apiKey);
145
+ if (!config.openrouter) {
146
+ if (!globalApiKey) return;
147
+ config.openrouter = { apiKey: globalApiKey };
148
+ return;
149
+ }
150
+ if (globalApiKey) {
151
+ config.openrouter.apiKey = globalApiKey;
152
+ return;
153
+ }
154
+ delete config.openrouter.apiKey;
155
+ if (Object.keys(config.openrouter).length === 0) delete config.openrouter;
156
+ }
142
157
  function serializePluginConfig(config) {
143
158
  return `${JSON.stringify(orderPluginConfig(config), null, 2)}\n`;
144
159
  }
@@ -177,6 +192,10 @@ function orderPluginConfig(config) {
177
192
  function isPlainObject(value) {
178
193
  return value !== null && typeof value === "object" && !Array.isArray(value);
179
194
  }
195
+ function normalizeOptionalString(value) {
196
+ const normalized = value?.trim();
197
+ return normalized ? normalized : null;
198
+ }
180
199
  function isMissingFileError(error) {
181
200
  return error instanceof Error && "code" in error && error.code === "ENOENT";
182
201
  }
@@ -195,4 +214,4 @@ async function pathExists(filePath) {
195
214
  //#endregion
196
215
  export { writePluginConfigFile as a, loadAppConfig as c, preparePluginConfiguration as i, getOpenCodeConfigFilePath as n, OPENCODE_TBOT_VERSION as o, mergePluginConfigSources as r, DEFAULT_TELEGRAM_API_ROOT as s, getGlobalPluginConfigFilePath as t };
197
216
 
198
- //# sourceMappingURL=plugin-config-CGIe9zdA.js.map
217
+ //# sourceMappingURL=plugin-config-DA71_jD3.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin-config-DA71_jD3.js","names":[],"sources":["../../src/app/config.ts","../../src/app/package-info.ts","../../src/app/plugin-config.ts"],"sourcesContent":["import { resolve } from \"node:path\";\nimport { z } from \"zod\";\n\nexport const DEFAULT_OPENROUTER_MODEL = \"openai/gpt-audio-mini\";\nexport const DEFAULT_STATE_FILE_PATH = \"./data/opencode-tbot.state.json\";\nexport const DEFAULT_TELEGRAM_API_ROOT = \"https://api.telegram.org\";\n\nconst AllowedChatIdSchema = z.union([\n z.number().int(),\n z.string().regex(/^-?\\d+$/u).transform((value) => Number(value)),\n]);\n\nconst TelegramConfigSchema = z.preprocess(\n (value) => value ?? {},\n z.object({\n botToken: z.string().trim().min(1),\n allowedChatIds: z.array(AllowedChatIdSchema).default([]),\n apiRoot: z.string().trim().url().default(DEFAULT_TELEGRAM_API_ROOT),\n }),\n);\n\nconst StateConfigSchema = z.preprocess(\n (value) => value ?? {},\n z.object({\n path: z.string().trim().min(1).default(DEFAULT_STATE_FILE_PATH),\n }),\n);\n\nconst OpenRouterConfigSchema = z.preprocess(\n (value) => value ?? {},\n z.object({\n apiKey: z.string().default(\"\"),\n model: z.string().default(DEFAULT_OPENROUTER_MODEL),\n timeoutMs: z.coerce.number().int().positive().default(30_000),\n transcriptionPrompt: z.string().default(\"\"),\n }),\n);\n\nconst AppConfigSchema = z.object({\n telegram: TelegramConfigSchema,\n state: StateConfigSchema,\n openrouter: OpenRouterConfigSchema,\n logLevel: z.string().default(\"info\"),\n});\n\nexport interface PluginConfigSource {\n telegram?: {\n botToken?: string;\n allowedChatIds?: Array<number | string>;\n apiRoot?: string;\n [key: string]: unknown;\n };\n state?: {\n path?: string;\n [key: string]: unknown;\n };\n openrouter?: {\n apiKey?: string;\n model?: string;\n timeoutMs?: number;\n transcriptionPrompt?: string;\n [key: string]: unknown;\n };\n logLevel?: string;\n [key: string]: unknown;\n}\n\nexport interface AppOpenRouterConfig {\n configured: boolean;\n apiKey: string | null;\n model: string;\n timeoutMs: number;\n transcriptionPrompt: string | null;\n}\n\nexport interface AppConfig {\n telegramBotToken: string;\n telegramAllowedChatIds: number[];\n telegramApiRoot: string;\n logLevel: string;\n stateFilePath: string;\n openrouter: AppOpenRouterConfig;\n}\n\nexport interface LoadAppConfigOptions {\n cwd?: string;\n}\n\nexport function loadAppConfig(\n configSource: PluginConfigSource | undefined = {},\n options: LoadAppConfigOptions = {},\n): AppConfig {\n const parsed = parseConfig(AppConfigSchema, configSource);\n\n return buildAppConfig(parsed, options);\n}\n\nexport const loadPluginConfig = loadAppConfig;\n\nfunction buildAppConfig(\n data: z.infer<typeof AppConfigSchema>,\n options: LoadAppConfigOptions,\n): AppConfig {\n const openRouterApiKey = normalizeOptionalString(data.openrouter.apiKey);\n const openRouterModel = normalizeOptionalString(data.openrouter.model) ?? DEFAULT_OPENROUTER_MODEL;\n const transcriptionPrompt = normalizeOptionalString(data.openrouter.transcriptionPrompt);\n\n return {\n telegramBotToken: data.telegram.botToken,\n telegramAllowedChatIds: data.telegram.allowedChatIds,\n telegramApiRoot: normalizeApiRoot(data.telegram.apiRoot),\n logLevel: data.logLevel,\n stateFilePath: resolveStatePath(data, options.cwd ?? process.cwd()),\n openrouter: {\n configured: !!openRouterApiKey,\n apiKey: openRouterApiKey,\n model: openRouterModel,\n timeoutMs: data.openrouter.timeoutMs,\n transcriptionPrompt,\n },\n };\n}\n\nfunction normalizeOptionalString(value: string): string | null {\n const normalized = value.trim();\n\n return normalized.length > 0 ? normalized : null;\n}\n\nfunction resolveStatePath(\n data: z.infer<typeof AppConfigSchema>,\n cwd: string,\n): string {\n return resolve(cwd, data.state.path || DEFAULT_STATE_FILE_PATH);\n}\n\nfunction normalizeApiRoot(value: string): string {\n const normalized = value.trim();\n\n return normalized.endsWith(\"/\")\n ? normalized.slice(0, -1)\n : normalized;\n}\n\nfunction parseConfig<TSchema extends z.ZodTypeAny>(\n schema: TSchema,\n configSource: PluginConfigSource | undefined,\n): z.infer<TSchema> {\n const parsed = schema.safeParse(configSource ?? {});\n\n if (parsed.success) {\n return parsed.data;\n }\n\n throw new Error(\n `Invalid plugin configuration: ${JSON.stringify(parsed.error.flatten())}`,\n );\n}\n","import { existsSync, readFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nexport const OPENCODE_TBOT_VERSION = resolvePackageVersion();\n\nfunction resolvePackageVersion(): string {\n let directory = dirname(fileURLToPath(import.meta.url));\n\n while (true) {\n const packageFilePath = join(directory, \"package.json\");\n\n if (existsSync(packageFilePath)) {\n try {\n const parsed = JSON.parse(readFileSync(packageFilePath, \"utf8\")) as {\n version?: unknown;\n };\n\n if (typeof parsed.version === \"string\" && parsed.version.trim().length > 0) {\n return parsed.version;\n }\n } catch {\n // Fall through and continue searching parent directories.\n }\n }\n\n const parentDirectory = dirname(directory);\n\n if (parentDirectory === directory) {\n break;\n }\n\n directory = parentDirectory;\n }\n\n return \"unknown\";\n}\n","import { access, mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport type { PluginConfigSource } from \"./config.js\";\n\nexport const PLUGIN_CONFIG_FILE_NAME = \"tbot.config.json\";\nexport const GLOBAL_PLUGIN_DIRECTORY_NAME = \"opencode-tbot\";\nexport const GLOBAL_PLUGIN_CONFIG_FILE_NAME = \"config.json\";\nexport const OPENCODE_CONFIG_FILE_NAME = \"opencode.json\";\n\nexport interface PreparedPluginConfiguration {\n cwd: string;\n config: PluginConfigSource;\n globalConfigFilePath: string;\n projectConfigFilePath: string;\n configFilePath: string;\n}\n\nexport interface PreparePluginConfigurationOptions {\n cwd: string;\n config?: PluginConfigSource;\n homeDir?: string;\n}\n\nexport async function preparePluginConfiguration(\n options: PreparePluginConfigurationOptions,\n): Promise<PreparedPluginConfiguration> {\n const homeDir = options.homeDir ?? homedir();\n const globalConfigFilePath = getGlobalPluginConfigFilePath(homeDir);\n const projectConfigFilePath = await resolveProjectPluginConfigFilePath(options.cwd);\n const [globalConfig, projectConfig] = await Promise.all([\n loadPluginConfigFile(globalConfigFilePath),\n loadPluginConfigFile(projectConfigFilePath),\n ]);\n const config = mergePluginConfigSources(globalConfig, projectConfig, options.config);\n applyGlobalOpenRouterApiKey(config, globalConfig);\n const configFilePath = await pathExists(projectConfigFilePath)\n ? projectConfigFilePath\n : globalConfigFilePath;\n\n return {\n cwd: options.cwd,\n config,\n globalConfigFilePath,\n projectConfigFilePath,\n configFilePath,\n };\n}\n\nexport function getOpenCodeConfigDirectory(homeDir: string = homedir()): string {\n return join(homeDir, \".config\", \"opencode\");\n}\n\nexport function getOpenCodeConfigFilePath(homeDir: string = homedir()): string {\n return join(getOpenCodeConfigDirectory(homeDir), OPENCODE_CONFIG_FILE_NAME);\n}\n\nexport function getGlobalPluginConfigFilePath(homeDir: string = homedir()): string {\n return join(\n getOpenCodeConfigDirectory(homeDir),\n GLOBAL_PLUGIN_DIRECTORY_NAME,\n GLOBAL_PLUGIN_CONFIG_FILE_NAME,\n );\n}\n\nexport async function writePluginConfigFile(\n configFilePath: string,\n config: PluginConfigSource,\n): Promise<void> {\n await mkdir(dirname(configFilePath), { recursive: true });\n await writeFile(configFilePath, serializePluginConfig(config), \"utf8\");\n}\n\nexport function mergePluginConfigSources(\n ...sources: Array<PluginConfigSource | undefined>\n): PluginConfigSource {\n const merged: PluginConfigSource = {};\n\n for (const source of sources) {\n if (!source) {\n continue;\n }\n\n const normalized = source;\n const previousTelegram = merged.telegram;\n const previousState = merged.state;\n const previousOpenRouter = merged.openrouter;\n\n Object.assign(merged, normalized);\n\n if (normalized.telegram) {\n merged.telegram = {\n ...(previousTelegram ?? {}),\n ...normalized.telegram,\n };\n }\n\n if (normalized.state) {\n merged.state = {\n ...(previousState ?? {}),\n ...normalized.state,\n };\n }\n\n if (normalized.openrouter) {\n merged.openrouter = {\n ...(previousOpenRouter ?? {}),\n ...normalized.openrouter,\n };\n }\n }\n\n return merged;\n}\n\nfunction applyGlobalOpenRouterApiKey(\n config: PluginConfigSource,\n globalConfig: PluginConfigSource,\n) {\n const globalApiKey = normalizeOptionalString(globalConfig.openrouter?.apiKey);\n\n if (!config.openrouter) {\n if (!globalApiKey) {\n return;\n }\n\n config.openrouter = {\n apiKey: globalApiKey,\n };\n\n return;\n }\n\n if (globalApiKey) {\n config.openrouter.apiKey = globalApiKey;\n return;\n }\n\n delete config.openrouter.apiKey;\n\n if (Object.keys(config.openrouter).length === 0) {\n delete config.openrouter;\n }\n}\n\nexport function serializePluginConfig(config: PluginConfigSource): string {\n return `${JSON.stringify(orderPluginConfig(config), null, 2)}\\n`;\n}\n\nasync function loadPluginConfigFile(configFilePath: string): Promise<PluginConfigSource> {\n try {\n const content = await readFile(configFilePath, \"utf8\");\n\n return parsePluginConfigText(content, configFilePath);\n } catch (error) {\n if (isMissingFileError(error)) {\n return {};\n }\n\n throw error;\n }\n}\n\nfunction parsePluginConfigText(\n content: string,\n configFilePath: string,\n): PluginConfigSource {\n try {\n const parsed = JSON.parse(content) as unknown;\n\n if (!isPlainObject(parsed)) {\n throw new Error(\"Config root must be a JSON object.\");\n }\n\n return parsed as PluginConfigSource;\n } catch (error) {\n throw new Error(\n [\n `Failed to parse ${configFilePath} as JSON.`,\n error instanceof Error ? error.message : String(error),\n ].join(\" \"),\n );\n }\n}\n\nfunction orderPluginConfig(config: PluginConfigSource): PluginConfigSource {\n const prioritizedKeys = new Set([\n \"telegram\",\n \"state\",\n \"openrouter\",\n \"logLevel\",\n ]);\n const ordered: PluginConfigSource = {};\n\n if (config.telegram) {\n ordered.telegram = config.telegram;\n }\n\n if (config.state) {\n ordered.state = config.state;\n }\n\n if (config.openrouter) {\n ordered.openrouter = config.openrouter;\n }\n\n if (config.logLevel !== undefined) {\n ordered.logLevel = config.logLevel;\n }\n\n for (const [key, value] of Object.entries(config)) {\n if (!prioritizedKeys.has(key)) {\n ordered[key] = value;\n }\n }\n\n return ordered;\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === \"object\" && !Array.isArray(value);\n}\n\nfunction normalizeOptionalString(value: string | undefined): string | null {\n const normalized = value?.trim();\n\n return normalized\n ? normalized\n : null;\n}\n\nfunction isMissingFileError(error: unknown): error is NodeJS.ErrnoException {\n return error instanceof Error && \"code\" in error && error.code === \"ENOENT\";\n}\n\nasync function resolveProjectPluginConfigFilePath(cwd: string): Promise<string> {\n const preferredPath = join(cwd, PLUGIN_CONFIG_FILE_NAME);\n\n return preferredPath;\n}\n\nasync function pathExists(filePath: string): Promise<boolean> {\n try {\n await access(filePath);\n return true;\n } catch (error) {\n if (isMissingFileError(error)) {\n return false;\n }\n\n throw error;\n }\n}\n"],"mappings":"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"}
package/dist/cli.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as writePluginConfigFile, n as getOpenCodeConfigFilePath, o as OPENCODE_TBOT_VERSION, r as mergePluginConfigSources, t as getGlobalPluginConfigFilePath } from "./assets/plugin-config-CGIe9zdA.js";
1
+ import { a as writePluginConfigFile, n as getOpenCodeConfigFilePath, o as OPENCODE_TBOT_VERSION, r as mergePluginConfigSources, t as getGlobalPluginConfigFilePath } from "./assets/plugin-config-DA71_jD3.js";
2
2
  import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
3
3
  import { homedir } from "node:os";
4
4
  import { dirname, join, resolve } from "node:path";
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
- import "./assets/plugin-config-CGIe9zdA.js";
1
+ import "./assets/plugin-config-DA71_jD3.js";
2
2
  import { TelegramBotPlugin, ensureTelegramBotPluginRuntime, resetTelegramBotPluginRuntimeForTests } from "./plugin.js";
3
3
  export { TelegramBotPlugin, TelegramBotPlugin as default, ensureTelegramBotPluginRuntime, resetTelegramBotPluginRuntimeForTests };
package/dist/plugin.js CHANGED
@@ -1,4 +1,4 @@
1
- import { c as loadAppConfig, i as preparePluginConfiguration, o as OPENCODE_TBOT_VERSION } from "./assets/plugin-config-CGIe9zdA.js";
1
+ import { c as loadAppConfig, i as preparePluginConfiguration, o as OPENCODE_TBOT_VERSION } from "./assets/plugin-config-DA71_jD3.js";
2
2
  import { mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
3
3
  import { basename, dirname, extname, isAbsolute, join } from "node:path";
4
4
  import { parse, printParseErrorCode } from "jsonc-parser";
@@ -1204,8 +1204,14 @@ var VoiceTranscriptEmptyError = class extends Error {
1204
1204
  }
1205
1205
  };
1206
1206
  var DisabledVoiceTranscriptionClient = class {
1207
+ getStatus() {
1208
+ return {
1209
+ status: "not_configured",
1210
+ model: null
1211
+ };
1212
+ }
1207
1213
  async transcribe() {
1208
- throw new VoiceTranscriptionNotConfiguredError("Set openrouter.apiKey in tbot.config.json to enable Telegram voice transcription.");
1214
+ throw new VoiceTranscriptionNotConfiguredError("Set openrouter.apiKey in the global config (~/.config/opencode/opencode-tbot/config.json) to enable Telegram voice transcription.");
1209
1215
  }
1210
1216
  };
1211
1217
  var OpenRouterVoiceTranscriptionClient = class {
@@ -1219,6 +1225,12 @@ var OpenRouterVoiceTranscriptionClient = class {
1219
1225
  this.timeoutMs = options.timeoutMs;
1220
1226
  this.transcriptionPrompt = options.transcriptionPrompt?.trim() || null;
1221
1227
  }
1228
+ getStatus() {
1229
+ return {
1230
+ status: "configured",
1231
+ model: this.model
1232
+ };
1233
+ }
1222
1234
  async transcribe(input) {
1223
1235
  const format = resolveAudioFormat(input.filename, input.mimeType);
1224
1236
  const audioData = toBase64(input.data);
@@ -1419,6 +1431,9 @@ var VoiceTranscriptionService = class {
1419
1431
  constructor(client) {
1420
1432
  this.client = client;
1421
1433
  }
1434
+ getStatus() {
1435
+ return this.client.getStatus();
1436
+ }
1422
1437
  async transcribeVoice(input) {
1423
1438
  const text = (await this.client.transcribe(input)).text.trim();
1424
1439
  if (!text) throw new VoiceTranscriptEmptyError("Voice transcription returned empty text.");
@@ -1531,14 +1546,14 @@ var GetPathUseCase = class {
1531
1546
  //#endregion
1532
1547
  //#region src/use-cases/get-status.usecase.ts
1533
1548
  var GetStatusUseCase = class {
1534
- constructor(getHealthUseCase, getPathUseCase, listLspUseCase, listMcpUseCase, listSessionsUseCase, sessionRepo, voiceRecognitionStatus) {
1549
+ constructor(getHealthUseCase, getPathUseCase, listLspUseCase, listMcpUseCase, listSessionsUseCase, sessionRepo, voiceTranscriptionService) {
1535
1550
  this.getHealthUseCase = getHealthUseCase;
1536
1551
  this.getPathUseCase = getPathUseCase;
1537
1552
  this.listLspUseCase = listLspUseCase;
1538
1553
  this.listMcpUseCase = listMcpUseCase;
1539
1554
  this.listSessionsUseCase = listSessionsUseCase;
1540
1555
  this.sessionRepo = sessionRepo;
1541
- this.voiceRecognitionStatus = voiceRecognitionStatus;
1556
+ this.voiceTranscriptionService = voiceTranscriptionService;
1542
1557
  }
1543
1558
  async execute(input) {
1544
1559
  const [health, path, lsp, mcp] = await Promise.allSettled([
@@ -1553,7 +1568,7 @@ var GetStatusUseCase = class {
1553
1568
  health: mapSettledResult(health),
1554
1569
  path: pathResult,
1555
1570
  plugins,
1556
- voiceRecognition: this.voiceRecognitionStatus,
1571
+ voiceRecognition: this.voiceTranscriptionService.getStatus(),
1557
1572
  workspace,
1558
1573
  lsp: mapSettledResult(lsp),
1559
1574
  mcp: mapSettledResult(mcp)
@@ -2186,10 +2201,7 @@ function createContainer(config, opencodeClient, logger) {
2186
2201
  const listLspUseCase = new ListLspUseCase(sessionRepo, opencodeClient);
2187
2202
  const listMcpUseCase = new ListMcpUseCase(sessionRepo, opencodeClient);
2188
2203
  const listSessionsUseCase = new ListSessionsUseCase(sessionRepo, opencodeClient);
2189
- const getStatusUseCase = new GetStatusUseCase(getHealthUseCase, getPathUseCase, listLspUseCase, listMcpUseCase, listSessionsUseCase, sessionRepo, {
2190
- status: config.openrouter.configured ? "configured" : "not_configured",
2191
- model: config.openrouter.configured ? config.openrouter.model : null
2192
- });
2204
+ const getStatusUseCase = new GetStatusUseCase(getHealthUseCase, getPathUseCase, listLspUseCase, listMcpUseCase, listSessionsUseCase, sessionRepo, voiceTranscriptionService);
2193
2205
  const listModelsUseCase = new ListModelsUseCase(sessionRepo, opencodeClient);
2194
2206
  const renameSessionUseCase = new RenameSessionUseCase(sessionRepo, opencodeClient, logger);
2195
2207
  const sendPromptUseCase = new SendPromptUseCase(sessionRepo, opencodeClient, logger, foregroundSessionTracker);
@@ -2414,7 +2426,6 @@ var SUPPORTED_BOT_LANGUAGES = ["en", "zh-CN"];
2414
2426
  var EN_BOT_COPY = {
2415
2427
  commands: {
2416
2428
  start: "Welcome and quick start",
2417
- help: "Show commands and examples",
2418
2429
  status: "Show system status",
2419
2430
  new: "Create a new session",
2420
2431
  agents: "View and switch agents",
@@ -2428,37 +2439,11 @@ var EN_BOT_COPY = {
2428
2439
  "",
2429
2440
  "Talk to your OpenCode server from Telegram.",
2430
2441
  "",
2431
- "## What you can send",
2432
- "- Text prompts",
2433
- "- Images with an optional caption",
2434
- "- Voice messages (requires OpenRouter voice transcription)",
2435
- "",
2436
2442
  "## Quick start",
2437
2443
  "1. Run `/status` to confirm the server is ready.",
2438
2444
  "2. Run `/new [title]` to create a fresh session.",
2439
- "3. Send a text, image, or voice message.",
2440
2445
  "",
2441
- "Use `/help` to see the full command list and examples."
2442
- ] },
2443
- help: { lines: [
2444
- "# Help",
2445
- "",
2446
- "Use this bot to chat with OpenCode from Telegram.",
2447
- "",
2448
- "## Commands",
2449
- "- `/status` Check server, workspace, MCP, and LSP status",
2450
- "- `/new [title]` Create a new session",
2451
- "- `/sessions` View, switch, or rename sessions",
2452
- "- `/agents` View and switch agents",
2453
- "- `/model` View and switch models and reasoning levels",
2454
- "- `/language` Switch the bot display language",
2455
- "- `/cancel` Cancel session rename or abort the running request",
2456
- "",
2457
- "## Examples",
2458
- "- `/new bug triage`",
2459
- "- Send a plain text message directly",
2460
- "- Send an image with a caption",
2461
- "- Send a voice message if OpenRouter voice transcription is configured"
2446
+ "Send a text, image, or voice message directly."
2462
2447
  ] },
2463
2448
  systemStatus: { title: "System Status" },
2464
2449
  common: {
@@ -2650,7 +2635,6 @@ var EN_BOT_COPY = {
2650
2635
  var ZH_CN_BOT_COPY = {
2651
2636
  commands: {
2652
2637
  start: "查看欢迎与快速开始",
2653
- help: "查看命令说明与示例",
2654
2638
  status: "查看系统状态",
2655
2639
  new: "新建会话",
2656
2640
  agents: "查看并切换代理",
@@ -2664,37 +2648,11 @@ var ZH_CN_BOT_COPY = {
2664
2648
  "",
2665
2649
  "通过 Telegram 直接和 OpenCode 服务对话。",
2666
2650
  "",
2667
- "## 支持的输入",
2668
- "- 文本消息",
2669
- "- 图片 (可附带 caption)",
2670
- "- 语音消息 (需先配置 OpenRouter 语音转写)",
2671
- "",
2672
2651
  "## 快速开始",
2673
2652
  "1. 先运行 `/status` 确认服务状态正常。",
2674
2653
  "2. 运行 `/new [title]` 创建一个新会话。",
2675
- "3. 直接发送文本、图片或语音消息。",
2676
2654
  "",
2677
- "更多命令和示例请查看 `/help`。"
2678
- ] },
2679
- help: { lines: [
2680
- "# 帮助",
2681
- "",
2682
- "使用这个机器人可以通过 Telegram 与 OpenCode 对话。",
2683
- "",
2684
- "## 命令",
2685
- "- `/status` 查看服务、工作区、MCP 和 LSP 状态",
2686
- "- `/new [title]` 创建一个新会话",
2687
- "- `/sessions` 查看、切换或重命名会话",
2688
- "- `/agents` 查看并切换代理",
2689
- "- `/model` 查看并切换模型与推理级别",
2690
- "- `/language` 切换机器人的显示语言",
2691
- "- `/cancel` 取消会话重命名或中止当前请求",
2692
- "",
2693
- "## 示例",
2694
- "- `/new bug triage`",
2695
- "- 直接发送一条文本消息",
2696
- "- 发送一张带 caption 的图片",
2697
- "- 如果已配置 OpenRouter 语音转写,直接发送语音消息"
2655
+ "直接发送文本、图片或语音消息即可。"
2698
2656
  ] },
2699
2657
  systemStatus: { title: "系统状态" },
2700
2658
  common: {
@@ -2908,10 +2866,6 @@ function getTelegramCommands(language = "en") {
2908
2866
  command: "start",
2909
2867
  description: copy.commands.start
2910
2868
  },
2911
- {
2912
- command: "help",
2913
- description: copy.commands.help
2914
- },
2915
2869
  {
2916
2870
  command: "status",
2917
2871
  description: copy.commands.status
@@ -3353,7 +3307,7 @@ function formatHealthBadge(healthy, layout) {
3353
3307
  return healthy ? "🟢" : layout.errorStatus;
3354
3308
  }
3355
3309
  function formatVoiceRecognitionBadge(status, layout) {
3356
- if (status.status === "configured") return status.model ? `\uD83D\uDFE1 ${layout.voiceRecognitionConfiguredLabel} (${status.model})` : `\uD83D\uDFE1 ${layout.voiceRecognitionConfiguredLabel}`;
3310
+ if (status.status === "configured") return status.model ? `\uD83D\uDFE2 ${layout.voiceRecognitionConfiguredLabel} (${status.model})` : `\uD83D\uDFE2 ${layout.voiceRecognitionConfiguredLabel}`;
3357
3311
  return `\u26AA ${layout.voiceRecognitionNotConfiguredLabel}`;
3358
3312
  }
3359
3313
  function formatLspStatusBadge(status) {
@@ -3744,6 +3698,112 @@ function registerCancelCommand(bot, dependencies) {
3744
3698
  });
3745
3699
  }
3746
3700
  //#endregion
3701
+ //#region src/bot/commands/language.ts
3702
+ async function handleLanguageCommand(ctx, dependencies) {
3703
+ const language = await getChatLanguage(dependencies.sessionRepo, ctx.chat.id);
3704
+ const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
3705
+ try {
3706
+ await syncTelegramCommandsForChat(ctx.api, ctx.chat.id, language);
3707
+ await ctx.reply(presentLanguageMessage(language, copy), { reply_markup: buildLanguageKeyboard(language, copy) });
3708
+ } catch (error) {
3709
+ dependencies.logger.error({ error }, "failed to show language options");
3710
+ await ctx.reply(presentError(error, copy));
3711
+ }
3712
+ }
3713
+ async function switchLanguageForChat(api, chatId, language, dependencies) {
3714
+ const currentCopy = await getChatCopy(dependencies.sessionRepo, chatId);
3715
+ if (!isBotLanguage(language)) return {
3716
+ found: false,
3717
+ copy: currentCopy
3718
+ };
3719
+ await setChatLanguage(dependencies.sessionRepo, chatId, language);
3720
+ await syncTelegramCommandsForChat(api, chatId, language);
3721
+ return {
3722
+ found: true,
3723
+ copy: await getChatCopy(dependencies.sessionRepo, chatId),
3724
+ language
3725
+ };
3726
+ }
3727
+ async function presentLanguageSwitchForChat(chatId, api, language, dependencies) {
3728
+ const result = await switchLanguageForChat(api, chatId, language, dependencies);
3729
+ if (!result.found) return {
3730
+ found: false,
3731
+ copy: result.copy,
3732
+ text: result.copy.language.expired,
3733
+ keyboard: buildLanguageKeyboard(await getChatLanguage(dependencies.sessionRepo, chatId), result.copy)
3734
+ };
3735
+ return {
3736
+ found: true,
3737
+ copy: result.copy,
3738
+ text: presentLanguageSwitchMessage(result.language, result.copy),
3739
+ keyboard: buildLanguageKeyboard(result.language, result.copy)
3740
+ };
3741
+ }
3742
+ function registerLanguageCommand(bot, dependencies) {
3743
+ bot.command("language", async (ctx) => {
3744
+ await handleLanguageCommand(ctx, dependencies);
3745
+ });
3746
+ }
3747
+ //#endregion
3748
+ //#region src/bot/commands/models.ts
3749
+ async function handleModelsCommand(ctx, dependencies) {
3750
+ const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
3751
+ try {
3752
+ const result = await dependencies.listModelsUseCase.execute({ chatId: ctx.chat.id });
3753
+ if (result.models.length === 0) {
3754
+ await ctx.reply(copy.models.none);
3755
+ return;
3756
+ }
3757
+ const { keyboard, page } = buildModelsKeyboard(result.models, 0, copy);
3758
+ await ctx.reply(presentModelsMessage({
3759
+ currentModelId: result.currentModelId,
3760
+ currentModelProviderId: result.currentModelProviderId,
3761
+ currentModelVariant: result.currentModelVariant,
3762
+ models: result.models,
3763
+ page: page.page
3764
+ }, copy), { reply_markup: keyboard });
3765
+ } catch (error) {
3766
+ dependencies.logger.error({ error }, "failed to list models");
3767
+ await ctx.reply(presentError(error, copy));
3768
+ }
3769
+ }
3770
+ function registerModelsCommand(bot, dependencies) {
3771
+ bot.command(["model", "models"], async (ctx) => {
3772
+ await handleModelsCommand(ctx, dependencies);
3773
+ });
3774
+ }
3775
+ //#endregion
3776
+ //#region src/bot/commands/new.ts
3777
+ async function handleNewCommand(ctx, dependencies) {
3778
+ const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
3779
+ try {
3780
+ const title = extractSessionTitle(ctx);
3781
+ const result = await dependencies.createSessionUseCase.execute({
3782
+ chatId: ctx.chat.id,
3783
+ title
3784
+ });
3785
+ await ctx.reply(presentSessionCreatedMessage(result.session, copy));
3786
+ } catch (error) {
3787
+ dependencies.logger.error({ error }, "failed to create new session");
3788
+ await ctx.reply(presentError(error, copy));
3789
+ }
3790
+ }
3791
+ function registerNewCommand(bot, dependencies) {
3792
+ bot.command("new", async (ctx) => {
3793
+ await handleNewCommand(ctx, dependencies);
3794
+ });
3795
+ }
3796
+ function extractSessionTitle(ctx) {
3797
+ if (typeof ctx.match === "string") {
3798
+ const title = ctx.match.trim();
3799
+ return title ? title : null;
3800
+ }
3801
+ const messageText = ctx.message?.text?.trim();
3802
+ if (!messageText) return null;
3803
+ const title = messageText.match(/^\/new(?:@\S+)?(?:\s+([\s\S]*))?$/i)?.[1]?.trim();
3804
+ return title ? title : null;
3805
+ }
3806
+ //#endregion
3747
3807
  //#region src/services/telegram/telegram-format.ts
3748
3808
  var MAX_TELEGRAM_MESSAGE_LENGTH = 4096;
3749
3809
  var TRUNCATED_SUFFIX = "...";
@@ -4077,142 +4137,6 @@ function escapeLinkDestination(url) {
4077
4137
  return url.replace(/\\/g, "\\\\").replace(/\)/g, "\\)").replace(/\(/g, "\\(");
4078
4138
  }
4079
4139
  //#endregion
4080
- //#region src/bot/presenters/static.presenter.ts
4081
- function presentStartMarkdownMessage(copy = BOT_COPY) {
4082
- return copy.start.lines.join("\n");
4083
- }
4084
- function presentHelpMarkdownMessage(copy = BOT_COPY) {
4085
- return copy.help.lines.join("\n");
4086
- }
4087
- //#endregion
4088
- //#region src/bot/commands/help.ts
4089
- async function handleHelpCommand(ctx, dependencies) {
4090
- const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat?.id);
4091
- const reply = buildTelegramStaticReply(presentHelpMarkdownMessage(copy));
4092
- try {
4093
- await ctx.reply(reply.preferred.text, reply.preferred.options);
4094
- } catch (error) {
4095
- if (reply.preferred.options) {
4096
- dependencies.logger.error({ error }, "failed to send help markdown reply, falling back to plain text");
4097
- await ctx.reply(reply.fallback.text);
4098
- return;
4099
- }
4100
- dependencies.logger.error({ error }, "failed to show help message");
4101
- await ctx.reply(presentError(error, copy));
4102
- }
4103
- }
4104
- function registerHelpCommand(bot, dependencies) {
4105
- bot.command("help", async (ctx) => {
4106
- await handleHelpCommand(ctx, dependencies);
4107
- });
4108
- }
4109
- //#endregion
4110
- //#region src/bot/commands/language.ts
4111
- async function handleLanguageCommand(ctx, dependencies) {
4112
- const language = await getChatLanguage(dependencies.sessionRepo, ctx.chat.id);
4113
- const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
4114
- try {
4115
- await syncTelegramCommandsForChat(ctx.api, ctx.chat.id, language);
4116
- await ctx.reply(presentLanguageMessage(language, copy), { reply_markup: buildLanguageKeyboard(language, copy) });
4117
- } catch (error) {
4118
- dependencies.logger.error({ error }, "failed to show language options");
4119
- await ctx.reply(presentError(error, copy));
4120
- }
4121
- }
4122
- async function switchLanguageForChat(api, chatId, language, dependencies) {
4123
- const currentCopy = await getChatCopy(dependencies.sessionRepo, chatId);
4124
- if (!isBotLanguage(language)) return {
4125
- found: false,
4126
- copy: currentCopy
4127
- };
4128
- await setChatLanguage(dependencies.sessionRepo, chatId, language);
4129
- await syncTelegramCommandsForChat(api, chatId, language);
4130
- return {
4131
- found: true,
4132
- copy: await getChatCopy(dependencies.sessionRepo, chatId),
4133
- language
4134
- };
4135
- }
4136
- async function presentLanguageSwitchForChat(chatId, api, language, dependencies) {
4137
- const result = await switchLanguageForChat(api, chatId, language, dependencies);
4138
- if (!result.found) return {
4139
- found: false,
4140
- copy: result.copy,
4141
- text: result.copy.language.expired,
4142
- keyboard: buildLanguageKeyboard(await getChatLanguage(dependencies.sessionRepo, chatId), result.copy)
4143
- };
4144
- return {
4145
- found: true,
4146
- copy: result.copy,
4147
- text: presentLanguageSwitchMessage(result.language, result.copy),
4148
- keyboard: buildLanguageKeyboard(result.language, result.copy)
4149
- };
4150
- }
4151
- function registerLanguageCommand(bot, dependencies) {
4152
- bot.command("language", async (ctx) => {
4153
- await handleLanguageCommand(ctx, dependencies);
4154
- });
4155
- }
4156
- //#endregion
4157
- //#region src/bot/commands/models.ts
4158
- async function handleModelsCommand(ctx, dependencies) {
4159
- const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
4160
- try {
4161
- const result = await dependencies.listModelsUseCase.execute({ chatId: ctx.chat.id });
4162
- if (result.models.length === 0) {
4163
- await ctx.reply(copy.models.none);
4164
- return;
4165
- }
4166
- const { keyboard, page } = buildModelsKeyboard(result.models, 0, copy);
4167
- await ctx.reply(presentModelsMessage({
4168
- currentModelId: result.currentModelId,
4169
- currentModelProviderId: result.currentModelProviderId,
4170
- currentModelVariant: result.currentModelVariant,
4171
- models: result.models,
4172
- page: page.page
4173
- }, copy), { reply_markup: keyboard });
4174
- } catch (error) {
4175
- dependencies.logger.error({ error }, "failed to list models");
4176
- await ctx.reply(presentError(error, copy));
4177
- }
4178
- }
4179
- function registerModelsCommand(bot, dependencies) {
4180
- bot.command(["model", "models"], async (ctx) => {
4181
- await handleModelsCommand(ctx, dependencies);
4182
- });
4183
- }
4184
- //#endregion
4185
- //#region src/bot/commands/new.ts
4186
- async function handleNewCommand(ctx, dependencies) {
4187
- const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
4188
- try {
4189
- const title = extractSessionTitle(ctx);
4190
- const result = await dependencies.createSessionUseCase.execute({
4191
- chatId: ctx.chat.id,
4192
- title
4193
- });
4194
- await ctx.reply(presentSessionCreatedMessage(result.session, copy));
4195
- } catch (error) {
4196
- dependencies.logger.error({ error }, "failed to create new session");
4197
- await ctx.reply(presentError(error, copy));
4198
- }
4199
- }
4200
- function registerNewCommand(bot, dependencies) {
4201
- bot.command("new", async (ctx) => {
4202
- await handleNewCommand(ctx, dependencies);
4203
- });
4204
- }
4205
- function extractSessionTitle(ctx) {
4206
- if (typeof ctx.match === "string") {
4207
- const title = ctx.match.trim();
4208
- return title ? title : null;
4209
- }
4210
- const messageText = ctx.message?.text?.trim();
4211
- if (!messageText) return null;
4212
- const title = messageText.match(/^\/new(?:@\S+)?(?:\s+([\s\S]*))?$/i)?.[1]?.trim();
4213
- return title ? title : null;
4214
- }
4215
- //#endregion
4216
4140
  //#region src/bot/commands/status.ts
4217
4141
  async function handleStatusCommand(ctx, dependencies) {
4218
4142
  const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat?.id);
@@ -4253,6 +4177,11 @@ function registerSessionsCommand(bot, dependencies) {
4253
4177
  });
4254
4178
  }
4255
4179
  //#endregion
4180
+ //#region src/bot/presenters/static.presenter.ts
4181
+ function presentStartMarkdownMessage(copy = BOT_COPY) {
4182
+ return copy.start.lines.join("\n");
4183
+ }
4184
+ //#endregion
4256
4185
  //#region src/bot/commands/start.ts
4257
4186
  async function handleStartCommand(ctx, dependencies) {
4258
4187
  const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat?.id);
@@ -4769,7 +4698,6 @@ function registerBot(bot, container, options) {
4769
4698
  bot.use(createLoggingMiddleware(container.logger));
4770
4699
  bot.use(createAuthMiddleware(options.telegramAllowedChatIds));
4771
4700
  registerStartCommand(bot, container);
4772
- registerHelpCommand(bot, container);
4773
4701
  registerStatusCommand(bot, container);
4774
4702
  registerNewCommand(bot, container);
4775
4703
  registerAgentsCommand(bot, container);