opencode-tbot 0.1.13 → 0.1.16

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.
@@ -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,5 @@
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
+ import { createRequire } from "node:module";
2
3
  import { mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
3
4
  import { basename, dirname, extname, isAbsolute, join } from "node:path";
4
5
  import { parse, printParseErrorCode } from "jsonc-parser";
@@ -6,6 +7,7 @@ import { z } from "zod";
6
7
  import { OpenRouter } from "@openrouter/sdk";
7
8
  import { createOpencodeClient } from "@opencode-ai/sdk/v2/client";
8
9
  import { randomUUID } from "node:crypto";
10
+ import { spawn } from "node:child_process";
9
11
  import { run } from "@grammyjs/runner";
10
12
  import { Bot, InlineKeyboard } from "grammy";
11
13
  //#region src/infra/utils/redact.ts
@@ -1178,6 +1180,145 @@ var NOOP_FOREGROUND_SESSION_TRACKER = {
1178
1180
  }
1179
1181
  };
1180
1182
  //#endregion
1183
+ //#region src/services/voice-transcription/audio-transcoder.ts
1184
+ var OPENROUTER_SUPPORTED_AUDIO_FORMATS = ["mp3", "wav"];
1185
+ var VoiceTranscodingFailedError = class extends Error {
1186
+ data;
1187
+ constructor(message) {
1188
+ super(message);
1189
+ this.name = "VoiceTranscodingFailedError";
1190
+ this.data = { message };
1191
+ }
1192
+ };
1193
+ var DEFAULT_TRANSCODE_TIMEOUT_MS = 15e3;
1194
+ var FfmpegAudioTranscoder = class {
1195
+ ffmpegPath;
1196
+ spawnProcess;
1197
+ timeoutMs;
1198
+ constructor(options) {
1199
+ this.ffmpegPath = options.ffmpegPath?.trim() || null;
1200
+ this.spawnProcess = options.spawnProcess ?? defaultSpawnProcess;
1201
+ this.timeoutMs = options.timeoutMs ?? DEFAULT_TRANSCODE_TIMEOUT_MS;
1202
+ }
1203
+ async transcode(input) {
1204
+ if (!this.ffmpegPath) throw new VoiceTranscodingFailedError(buildTranscodingMessage(input.sourceFormat, input.targetFormat, "Bundled ffmpeg is unavailable."));
1205
+ if (input.targetFormat !== "wav") throw new VoiceTranscodingFailedError(buildTranscodingMessage(input.sourceFormat, input.targetFormat, `Unsupported transcode target: ${input.targetFormat}.`));
1206
+ return {
1207
+ data: await runFfmpegTranscode({
1208
+ data: toUint8Array$1(input.data),
1209
+ ffmpegPath: this.ffmpegPath,
1210
+ filename: input.filename,
1211
+ sourceFormat: input.sourceFormat,
1212
+ spawnProcess: this.spawnProcess,
1213
+ timeoutMs: this.timeoutMs,
1214
+ targetFormat: input.targetFormat
1215
+ }),
1216
+ filename: replaceExtension(input.filename, ".wav"),
1217
+ format: "wav",
1218
+ mimeType: "audio/wav"
1219
+ };
1220
+ }
1221
+ };
1222
+ async function runFfmpegTranscode(input) {
1223
+ return await new Promise((resolve, reject) => {
1224
+ const child = input.spawnProcess(input.ffmpegPath, buildFfmpegArgs(input.targetFormat), {
1225
+ stdio: [
1226
+ "pipe",
1227
+ "pipe",
1228
+ "pipe"
1229
+ ],
1230
+ windowsHide: true
1231
+ });
1232
+ const stdoutChunks = [];
1233
+ const stderrChunks = [];
1234
+ let settled = false;
1235
+ let timedOut = false;
1236
+ const timer = setTimeout(() => {
1237
+ timedOut = true;
1238
+ child.kill();
1239
+ }, input.timeoutMs);
1240
+ const cleanup = () => {
1241
+ clearTimeout(timer);
1242
+ };
1243
+ const rejectOnce = (message) => {
1244
+ if (settled) return;
1245
+ settled = true;
1246
+ cleanup();
1247
+ reject(new VoiceTranscodingFailedError(buildTranscodingMessage(input.sourceFormat, input.targetFormat, message)));
1248
+ };
1249
+ const resolveOnce = (value) => {
1250
+ if (settled) return;
1251
+ settled = true;
1252
+ cleanup();
1253
+ resolve(value);
1254
+ };
1255
+ child.stdout.on("data", (chunk) => {
1256
+ stdoutChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
1257
+ });
1258
+ child.stderr.on("data", (chunk) => {
1259
+ stderrChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
1260
+ });
1261
+ child.once("error", (error) => {
1262
+ rejectOnce(`Failed to start bundled ffmpeg: ${error.message}`);
1263
+ });
1264
+ child.once("close", (code, signal) => {
1265
+ if (timedOut) {
1266
+ rejectOnce(`Bundled ffmpeg timed out after ${input.timeoutMs} ms.`);
1267
+ return;
1268
+ }
1269
+ if (code !== 0) {
1270
+ rejectOnce(Buffer.concat(stderrChunks).toString("utf8").trim() || `Bundled ffmpeg exited with code ${code}${signal ? ` (${signal})` : ""}.`);
1271
+ return;
1272
+ }
1273
+ const output = Buffer.concat(stdoutChunks);
1274
+ if (output.length === 0) {
1275
+ rejectOnce("Bundled ffmpeg returned empty audio output.");
1276
+ return;
1277
+ }
1278
+ resolveOnce(new Uint8Array(output));
1279
+ });
1280
+ child.stdin.on("error", (error) => {
1281
+ rejectOnce(`Failed to write audio data to bundled ffmpeg: ${error.message}`);
1282
+ });
1283
+ child.stdin.write(Buffer.from(input.data));
1284
+ child.stdin.end();
1285
+ });
1286
+ }
1287
+ function buildFfmpegArgs(targetFormat) {
1288
+ if (targetFormat !== "wav") throw new Error(`Unsupported target format: ${targetFormat}`);
1289
+ return [
1290
+ "-hide_banner",
1291
+ "-loglevel",
1292
+ "error",
1293
+ "-i",
1294
+ "pipe:0",
1295
+ "-f",
1296
+ "wav",
1297
+ "-acodec",
1298
+ "pcm_s16le",
1299
+ "-ac",
1300
+ "1",
1301
+ "-ar",
1302
+ "16000",
1303
+ "pipe:1"
1304
+ ];
1305
+ }
1306
+ function buildTranscodingMessage(sourceFormat, targetFormat, reason) {
1307
+ return `Failed to transcode audio from ${sourceFormat} to ${targetFormat}. ${reason}`;
1308
+ }
1309
+ function replaceExtension(filename, nextExtension) {
1310
+ const trimmedFilename = basename(filename).trim();
1311
+ if (!trimmedFilename) return `telegram-voice${nextExtension}`;
1312
+ const currentExtension = extname(trimmedFilename);
1313
+ return currentExtension ? `${trimmedFilename.slice(0, -currentExtension.length)}${nextExtension}` : `${trimmedFilename}${nextExtension}`;
1314
+ }
1315
+ function toUint8Array$1(data) {
1316
+ return data instanceof Uint8Array ? data : new Uint8Array(data);
1317
+ }
1318
+ function defaultSpawnProcess(command, args, options) {
1319
+ return spawn(command, args, options);
1320
+ }
1321
+ //#endregion
1181
1322
  //#region src/services/voice-transcription/openrouter-voice.client.ts
1182
1323
  var VoiceTranscriptionNotConfiguredError = class extends Error {
1183
1324
  data;
@@ -1204,24 +1345,41 @@ var VoiceTranscriptEmptyError = class extends Error {
1204
1345
  }
1205
1346
  };
1206
1347
  var DisabledVoiceTranscriptionClient = class {
1348
+ getStatus() {
1349
+ return {
1350
+ status: "not_configured",
1351
+ model: null
1352
+ };
1353
+ }
1207
1354
  async transcribe() {
1208
- throw new VoiceTranscriptionNotConfiguredError("Set openrouter.apiKey in tbot.config.json to enable Telegram voice transcription.");
1355
+ throw new VoiceTranscriptionNotConfiguredError("Set openrouter.apiKey in the global config (~/.config/opencode/opencode-tbot/config.json) to enable Telegram voice transcription.");
1209
1356
  }
1210
1357
  };
1211
1358
  var OpenRouterVoiceTranscriptionClient = class {
1359
+ audioTranscoder;
1212
1360
  model;
1213
1361
  sdk;
1214
1362
  timeoutMs;
1215
1363
  transcriptionPrompt;
1216
- constructor(options, sdk) {
1364
+ constructor(options, sdk, audioTranscoder = new FfmpegAudioTranscoder({
1365
+ ffmpegPath: null,
1366
+ timeoutMs: options.timeoutMs
1367
+ })) {
1368
+ this.audioTranscoder = audioTranscoder;
1217
1369
  this.model = options.model;
1218
1370
  this.sdk = sdk;
1219
1371
  this.timeoutMs = options.timeoutMs;
1220
1372
  this.transcriptionPrompt = options.transcriptionPrompt?.trim() || null;
1221
1373
  }
1374
+ getStatus() {
1375
+ return {
1376
+ status: "configured",
1377
+ model: this.model
1378
+ };
1379
+ }
1222
1380
  async transcribe(input) {
1223
- const format = resolveAudioFormat(input.filename, input.mimeType);
1224
- const audioData = toBase64(input.data);
1381
+ const preparedAudio = await prepareAudioForOpenRouter(input, resolveAudioFormat(input.filename, input.mimeType), this.audioTranscoder);
1382
+ const audioData = toBase64(preparedAudio.data);
1225
1383
  const prompt = buildTranscriptionPrompt(this.transcriptionPrompt);
1226
1384
  let response;
1227
1385
  try {
@@ -1235,7 +1393,7 @@ var OpenRouterVoiceTranscriptionClient = class {
1235
1393
  type: "input_audio",
1236
1394
  inputAudio: {
1237
1395
  data: audioData,
1238
- format
1396
+ format: preparedAudio.format
1239
1397
  }
1240
1398
  }]
1241
1399
  }],
@@ -1245,13 +1403,29 @@ var OpenRouterVoiceTranscriptionClient = class {
1245
1403
  } }, { timeoutMs: this.timeoutMs });
1246
1404
  } catch (error) {
1247
1405
  throw new VoiceTranscriptionFailedError(buildTranscriptionErrorMessage(error, {
1248
- format,
1406
+ format: preparedAudio.format,
1249
1407
  model: this.model
1250
1408
  }));
1251
1409
  }
1252
1410
  return { text: extractTranscript(response) };
1253
1411
  }
1254
1412
  };
1413
+ async function prepareAudioForOpenRouter(input, sourceFormat, audioTranscoder) {
1414
+ if (isOpenRouterSupportedAudioFormat(sourceFormat)) return {
1415
+ data: toUint8Array(input.data),
1416
+ format: sourceFormat
1417
+ };
1418
+ const transcoded = await audioTranscoder.transcode({
1419
+ data: input.data,
1420
+ filename: input.filename,
1421
+ sourceFormat,
1422
+ targetFormat: "wav"
1423
+ });
1424
+ return {
1425
+ data: transcoded.data,
1426
+ format: transcoded.format
1427
+ };
1428
+ }
1255
1429
  var MIME_TYPE_FORMAT_MAP = {
1256
1430
  "audio/aac": "aac",
1257
1431
  "audio/aiff": "aiff",
@@ -1289,9 +1463,15 @@ function resolveAudioFormat(filename, mimeType) {
1289
1463
  return "ogg";
1290
1464
  }
1291
1465
  function toBase64(data) {
1292
- const bytes = data instanceof Uint8Array ? data : new Uint8Array(data);
1466
+ const bytes = toUint8Array(data);
1293
1467
  return Buffer.from(bytes).toString("base64");
1294
1468
  }
1469
+ function toUint8Array(data) {
1470
+ return data instanceof Uint8Array ? data : new Uint8Array(data);
1471
+ }
1472
+ function isOpenRouterSupportedAudioFormat(format) {
1473
+ return OPENROUTER_SUPPORTED_AUDIO_FORMATS.includes(format);
1474
+ }
1295
1475
  function buildTranscriptionPrompt(transcriptionPrompt) {
1296
1476
  const basePrompt = [
1297
1477
  "Transcribe the provided audio verbatim.",
@@ -1419,6 +1599,9 @@ var VoiceTranscriptionService = class {
1419
1599
  constructor(client) {
1420
1600
  this.client = client;
1421
1601
  }
1602
+ getStatus() {
1603
+ return this.client.getStatus();
1604
+ }
1422
1605
  async transcribeVoice(input) {
1423
1606
  const text = (await this.client.transcribe(input)).text.trim();
1424
1607
  if (!text) throw new VoiceTranscriptEmptyError("Voice transcription returned empty text.");
@@ -1531,14 +1714,14 @@ var GetPathUseCase = class {
1531
1714
  //#endregion
1532
1715
  //#region src/use-cases/get-status.usecase.ts
1533
1716
  var GetStatusUseCase = class {
1534
- constructor(getHealthUseCase, getPathUseCase, listLspUseCase, listMcpUseCase, listSessionsUseCase, sessionRepo, voiceRecognitionStatus) {
1717
+ constructor(getHealthUseCase, getPathUseCase, listLspUseCase, listMcpUseCase, listSessionsUseCase, sessionRepo, voiceTranscriptionService) {
1535
1718
  this.getHealthUseCase = getHealthUseCase;
1536
1719
  this.getPathUseCase = getPathUseCase;
1537
1720
  this.listLspUseCase = listLspUseCase;
1538
1721
  this.listMcpUseCase = listMcpUseCase;
1539
1722
  this.listSessionsUseCase = listSessionsUseCase;
1540
1723
  this.sessionRepo = sessionRepo;
1541
- this.voiceRecognitionStatus = voiceRecognitionStatus;
1724
+ this.voiceTranscriptionService = voiceTranscriptionService;
1542
1725
  }
1543
1726
  async execute(input) {
1544
1727
  const [health, path, lsp, mcp] = await Promise.allSettled([
@@ -1553,7 +1736,7 @@ var GetStatusUseCase = class {
1553
1736
  health: mapSettledResult(health),
1554
1737
  path: pathResult,
1555
1738
  plugins,
1556
- voiceRecognition: this.voiceRecognitionStatus,
1739
+ voiceRecognition: this.voiceTranscriptionService.getStatus(),
1557
1740
  workspace,
1558
1741
  lsp: mapSettledResult(lsp),
1559
1742
  mcp: mapSettledResult(mcp)
@@ -2159,6 +2342,7 @@ function resolveExtension(mimeType) {
2159
2342
  }
2160
2343
  //#endregion
2161
2344
  //#region src/app/container.ts
2345
+ var require = createRequire(import.meta.url);
2162
2346
  function createAppContainer(config, client) {
2163
2347
  const logger = createOpenCodeAppLogger(client, { level: config.logLevel });
2164
2348
  return createContainer(config, createOpenCodeClientFromSdkClient(client), logger);
@@ -2186,10 +2370,7 @@ function createContainer(config, opencodeClient, logger) {
2186
2370
  const listLspUseCase = new ListLspUseCase(sessionRepo, opencodeClient);
2187
2371
  const listMcpUseCase = new ListMcpUseCase(sessionRepo, opencodeClient);
2188
2372
  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
- });
2373
+ const getStatusUseCase = new GetStatusUseCase(getHealthUseCase, getPathUseCase, listLspUseCase, listMcpUseCase, listSessionsUseCase, sessionRepo, voiceTranscriptionService);
2193
2374
  const listModelsUseCase = new ListModelsUseCase(sessionRepo, opencodeClient);
2194
2375
  const renameSessionUseCase = new RenameSessionUseCase(sessionRepo, opencodeClient, logger);
2195
2376
  const sendPromptUseCase = new SendPromptUseCase(sessionRepo, opencodeClient, logger, foregroundSessionTracker);
@@ -2238,8 +2419,19 @@ function createVoiceTranscriptionClient(config) {
2238
2419
  }, new OpenRouter({
2239
2420
  apiKey: config.apiKey,
2240
2421
  timeoutMs: config.timeoutMs
2422
+ }), new FfmpegAudioTranscoder({
2423
+ ffmpegPath: loadBundledFfmpegPath(),
2424
+ timeoutMs: config.timeoutMs
2241
2425
  })) : new DisabledVoiceTranscriptionClient();
2242
2426
  }
2427
+ function loadBundledFfmpegPath() {
2428
+ try {
2429
+ const ffmpegInstaller = require("@ffmpeg-installer/ffmpeg");
2430
+ return typeof ffmpegInstaller.path === "string" && ffmpegInstaller.path.trim().length > 0 ? ffmpegInstaller.path : null;
2431
+ } catch {
2432
+ return null;
2433
+ }
2434
+ }
2243
2435
  //#endregion
2244
2436
  //#region src/app/bootstrap.ts
2245
2437
  function bootstrapPluginApp(client, configSource = {}, options = {}) {
@@ -2472,6 +2664,7 @@ var EN_BOT_COPY = {
2472
2664
  structuredOutput: "Structured output validation failed.",
2473
2665
  voiceNotConfigured: "Voice transcription is not configured.",
2474
2666
  voiceDownload: "Failed to download the Telegram voice file.",
2667
+ voiceTranscoding: "Voice audio preprocessing failed.",
2475
2668
  voiceTranscription: "Voice transcription failed.",
2476
2669
  voiceEmpty: "Voice transcription returned empty text.",
2477
2670
  voiceUnsupported: "Voice message file is too large or unsupported.",
@@ -2681,6 +2874,7 @@ var ZH_CN_BOT_COPY = {
2681
2874
  structuredOutput: "结构化输出校验失败。",
2682
2875
  voiceNotConfigured: "未配置语音转写服务。",
2683
2876
  voiceDownload: "下载 Telegram 语音文件失败。",
2877
+ voiceTranscoding: "语音转码失败。",
2684
2878
  voiceTranscription: "语音转写失败。",
2685
2879
  voiceEmpty: "语音转写结果为空。",
2686
2880
  voiceUnsupported: "语音文件过大或不受支持。",
@@ -3037,6 +3231,10 @@ function normalizeError(error, copy) {
3037
3231
  message: copy.errors.voiceDownload,
3038
3232
  cause: extractMessage(error.data) ?? null
3039
3233
  };
3234
+ if (isNamedError(error, "VoiceTranscodingFailedError")) return {
3235
+ message: copy.errors.voiceTranscoding,
3236
+ cause: extractMessage(error.data) ?? null
3237
+ };
3040
3238
  if (isNamedError(error, "VoiceTranscriptionFailedError")) return {
3041
3239
  message: copy.errors.voiceTranscription,
3042
3240
  cause: extractMessage(error.data) ?? null
@@ -3294,9 +3492,9 @@ function splitStatusLines(text) {
3294
3492
  function formatHealthBadge(healthy, layout) {
3295
3493
  return healthy ? "🟢" : layout.errorStatus;
3296
3494
  }
3297
- function formatVoiceRecognitionBadge(status, layout) {
3298
- if (status.status === "configured") return status.model ? `\uD83D\uDFE1 ${layout.voiceRecognitionConfiguredLabel} (${status.model})` : `\uD83D\uDFE1 ${layout.voiceRecognitionConfiguredLabel}`;
3299
- return `\u26AA ${layout.voiceRecognitionNotConfiguredLabel}`;
3495
+ function formatVoiceRecognitionBadge(status, _layout) {
3496
+ if (status.status === "configured") return status.model ? `\uD83D\uDFE2 (${status.model})` : "🟡";
3497
+ return "⚪";
3300
3498
  }
3301
3499
  function formatLspStatusBadge(status) {
3302
3500
  switch (status.status) {
@@ -3366,9 +3564,7 @@ function getStatusLayoutCopy(copy) {
3366
3564
  rootLabel: "Root",
3367
3565
  statusLabel: "Status",
3368
3566
  tbotVersionLabel: "opencode-tbot Version",
3369
- voiceRecognitionConfiguredLabel: "configured",
3370
3567
  voiceRecognitionLabel: "Voice Recognition",
3371
- voiceRecognitionNotConfiguredLabel: "not configured",
3372
3568
  workspaceTitle: "📁 Workspace"
3373
3569
  };
3374
3570
  return {
@@ -3391,9 +3587,7 @@ function getStatusLayoutCopy(copy) {
3391
3587
  rootLabel: "根目录",
3392
3588
  statusLabel: "状态",
3393
3589
  tbotVersionLabel: "opencode-tbot版本",
3394
- voiceRecognitionConfiguredLabel: "已配置",
3395
3590
  voiceRecognitionLabel: "语音识别",
3396
- voiceRecognitionNotConfiguredLabel: "未配置",
3397
3591
  workspaceTitle: "📁 工作区"
3398
3592
  };
3399
3593
  }