opencode-tbot 0.1.16 → 0.1.19

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/plugin.js CHANGED
@@ -1,13 +1,10 @@
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";
1
+ import { c as loadAppConfig, i as preparePluginConfiguration, o as OPENCODE_TBOT_VERSION } from "./assets/plugin-config-B8ginwol.js";
3
2
  import { mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
4
- import { basename, dirname, extname, isAbsolute, join } from "node:path";
3
+ import { dirname, isAbsolute, join } from "node:path";
5
4
  import { parse, printParseErrorCode } from "jsonc-parser";
6
5
  import { z } from "zod";
7
- import { OpenRouter } from "@openrouter/sdk";
8
6
  import { createOpencodeClient } from "@opencode-ai/sdk/v2/client";
9
7
  import { randomUUID } from "node:crypto";
10
- import { spawn } from "node:child_process";
11
8
  import { run } from "@grammyjs/runner";
12
9
  import { Bot, InlineKeyboard } from "grammy";
13
10
  //#region src/infra/utils/redact.ts
@@ -1089,14 +1086,6 @@ var TelegramFileDownloadError = class extends Error {
1089
1086
  this.data = { message };
1090
1087
  }
1091
1088
  };
1092
- var VoiceMessageUnsupportedError = class extends Error {
1093
- data;
1094
- constructor(message) {
1095
- super(message);
1096
- this.name = "VoiceMessageUnsupportedError";
1097
- this.data = { message };
1098
- }
1099
- };
1100
1089
  var TelegramFileClient = class {
1101
1090
  baseUrl;
1102
1091
  fetchFn;
@@ -1111,11 +1100,11 @@ var TelegramFileClient = class {
1111
1100
  try {
1112
1101
  response = await this.fetchFn(new URL(filePath, this.baseUrl));
1113
1102
  } catch (error) {
1114
- throw new TelegramFileDownloadError(extractErrorMessage$1(error) ?? "Failed to download the Telegram voice file.");
1103
+ throw new TelegramFileDownloadError(extractErrorMessage(error) ?? "Failed to download the Telegram file.");
1115
1104
  }
1116
1105
  if (!response.ok) throw new TelegramFileDownloadError(await buildDownloadFailureMessage(response));
1117
1106
  const data = new Uint8Array(await response.arrayBuffer());
1118
- if (data.byteLength === 0) throw new TelegramFileDownloadError("Telegram returned an empty voice file.");
1107
+ if (data.byteLength === 0) throw new TelegramFileDownloadError("Telegram returned an empty file.");
1119
1108
  return {
1120
1109
  data,
1121
1110
  mimeType: response.headers.get("content-type")
@@ -1137,7 +1126,7 @@ async function safeReadResponseText(response) {
1137
1126
  return null;
1138
1127
  }
1139
1128
  }
1140
- function extractErrorMessage$1(error) {
1129
+ function extractErrorMessage(error) {
1141
1130
  return error instanceof Error && error.message.trim().length > 0 ? error.message.trim() : null;
1142
1131
  }
1143
1132
  //#endregion
@@ -1180,435 +1169,6 @@ var NOOP_FOREGROUND_SESSION_TRACKER = {
1180
1169
  }
1181
1170
  };
1182
1171
  //#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
1322
- //#region src/services/voice-transcription/openrouter-voice.client.ts
1323
- var VoiceTranscriptionNotConfiguredError = class extends Error {
1324
- data;
1325
- constructor(message) {
1326
- super(message);
1327
- this.name = "VoiceTranscriptionNotConfiguredError";
1328
- this.data = { message };
1329
- }
1330
- };
1331
- var VoiceTranscriptionFailedError = class extends Error {
1332
- data;
1333
- constructor(message) {
1334
- super(message);
1335
- this.name = "VoiceTranscriptionFailedError";
1336
- this.data = { message };
1337
- }
1338
- };
1339
- var VoiceTranscriptEmptyError = class extends Error {
1340
- data;
1341
- constructor(message) {
1342
- super(message);
1343
- this.name = "VoiceTranscriptEmptyError";
1344
- this.data = { message };
1345
- }
1346
- };
1347
- var DisabledVoiceTranscriptionClient = class {
1348
- getStatus() {
1349
- return {
1350
- status: "not_configured",
1351
- model: null
1352
- };
1353
- }
1354
- async transcribe() {
1355
- throw new VoiceTranscriptionNotConfiguredError("Set openrouter.apiKey in the global config (~/.config/opencode/opencode-tbot/config.json) to enable Telegram voice transcription.");
1356
- }
1357
- };
1358
- var OpenRouterVoiceTranscriptionClient = class {
1359
- audioTranscoder;
1360
- model;
1361
- sdk;
1362
- timeoutMs;
1363
- transcriptionPrompt;
1364
- constructor(options, sdk, audioTranscoder = new FfmpegAudioTranscoder({
1365
- ffmpegPath: null,
1366
- timeoutMs: options.timeoutMs
1367
- })) {
1368
- this.audioTranscoder = audioTranscoder;
1369
- this.model = options.model;
1370
- this.sdk = sdk;
1371
- this.timeoutMs = options.timeoutMs;
1372
- this.transcriptionPrompt = options.transcriptionPrompt?.trim() || null;
1373
- }
1374
- getStatus() {
1375
- return {
1376
- status: "configured",
1377
- model: this.model
1378
- };
1379
- }
1380
- async transcribe(input) {
1381
- const preparedAudio = await prepareAudioForOpenRouter(input, resolveAudioFormat(input.filename, input.mimeType), this.audioTranscoder);
1382
- const audioData = toBase64(preparedAudio.data);
1383
- const prompt = buildTranscriptionPrompt(this.transcriptionPrompt);
1384
- let response;
1385
- try {
1386
- response = await this.sdk.chat.send({ chatGenerationParams: {
1387
- messages: [{
1388
- role: "user",
1389
- content: [{
1390
- type: "text",
1391
- text: prompt
1392
- }, {
1393
- type: "input_audio",
1394
- inputAudio: {
1395
- data: audioData,
1396
- format: preparedAudio.format
1397
- }
1398
- }]
1399
- }],
1400
- model: this.model,
1401
- stream: false,
1402
- temperature: 0
1403
- } }, { timeoutMs: this.timeoutMs });
1404
- } catch (error) {
1405
- throw new VoiceTranscriptionFailedError(buildTranscriptionErrorMessage(error, {
1406
- format: preparedAudio.format,
1407
- model: this.model
1408
- }));
1409
- }
1410
- return { text: extractTranscript(response) };
1411
- }
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
- }
1429
- var MIME_TYPE_FORMAT_MAP = {
1430
- "audio/aac": "aac",
1431
- "audio/aiff": "aiff",
1432
- "audio/flac": "flac",
1433
- "audio/m4a": "m4a",
1434
- "audio/mp3": "mp3",
1435
- "audio/mp4": "m4a",
1436
- "audio/mpeg": "mp3",
1437
- "audio/ogg": "ogg",
1438
- "audio/wav": "wav",
1439
- "audio/wave": "wav",
1440
- "audio/x-aac": "aac",
1441
- "audio/x-aiff": "aiff",
1442
- "audio/x-flac": "flac",
1443
- "audio/x-m4a": "m4a",
1444
- "audio/x-wav": "wav",
1445
- "audio/vnd.wave": "wav"
1446
- };
1447
- var FILE_EXTENSION_FORMAT_MAP = {
1448
- ".aac": "aac",
1449
- ".aif": "aiff",
1450
- ".aiff": "aiff",
1451
- ".flac": "flac",
1452
- ".m4a": "m4a",
1453
- ".mp3": "mp3",
1454
- ".oga": "ogg",
1455
- ".ogg": "ogg",
1456
- ".wav": "wav"
1457
- };
1458
- function resolveAudioFormat(filename, mimeType) {
1459
- const normalizedMimeType = mimeType?.trim().toLowerCase() || null;
1460
- if (normalizedMimeType && MIME_TYPE_FORMAT_MAP[normalizedMimeType]) return MIME_TYPE_FORMAT_MAP[normalizedMimeType];
1461
- const extension = extname(basename(filename).trim()).toLowerCase();
1462
- if (extension && FILE_EXTENSION_FORMAT_MAP[extension]) return FILE_EXTENSION_FORMAT_MAP[extension];
1463
- return "ogg";
1464
- }
1465
- function toBase64(data) {
1466
- const bytes = toUint8Array(data);
1467
- return Buffer.from(bytes).toString("base64");
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
- }
1475
- function buildTranscriptionPrompt(transcriptionPrompt) {
1476
- const basePrompt = [
1477
- "Transcribe the provided audio verbatim.",
1478
- "Return only the transcript text.",
1479
- "Do not translate, summarize, explain, or add speaker labels.",
1480
- "If the audio is empty or unintelligible, return an empty string."
1481
- ].join(" ");
1482
- return transcriptionPrompt ? `${basePrompt}\n\nAdditional instructions: ${transcriptionPrompt}` : basePrompt;
1483
- }
1484
- function extractTranscript(response) {
1485
- const content = response.choices?.[0]?.message?.content;
1486
- if (typeof content === "string") return content.trim();
1487
- if (!Array.isArray(content)) return "";
1488
- return content.flatMap((item) => isTextContentItem(item) ? [item.text.trim()] : []).filter((text) => text.length > 0).join("\n").trim();
1489
- }
1490
- function isTextContentItem(value) {
1491
- return !!value && typeof value === "object" && "type" in value && value.type === "text" && "text" in value && typeof value.text === "string";
1492
- }
1493
- function buildTranscriptionErrorMessage(error, context) {
1494
- const parsedBody = parseJsonBody(extractStringField(error, "body"));
1495
- const rawMessages = dedupeNonEmptyStrings([
1496
- extractErrorMessage(error),
1497
- extractErrorMessage(readField(error, "error")),
1498
- extractErrorMessage(readField(error, "data")),
1499
- extractErrorMessage(readField(readField(error, "data"), "error")),
1500
- extractErrorMessage(parsedBody),
1501
- extractErrorMessage(readField(parsedBody, "error")),
1502
- extractMetadataRawMessage(error),
1503
- extractMetadataRawMessage(parsedBody)
1504
- ]);
1505
- const messages = rawMessages.some((message) => !isGenericProviderMessage(message)) ? rawMessages.filter((message) => !isGenericProviderMessage(message)) : rawMessages;
1506
- const providerName = extractProviderName(error) ?? extractProviderName(parsedBody);
1507
- const statusCode = extractNumericField(error, "statusCode") ?? extractNumericField(parsedBody, "statusCode");
1508
- const errorCode = extractErrorCode(error, parsedBody);
1509
- return joinNonEmptyParts$1([
1510
- ...messages,
1511
- `model: ${context.model}`,
1512
- `format: ${context.format}`,
1513
- providerName ? `provider: ${providerName}` : null,
1514
- statusCode !== null ? `status: ${statusCode}` : null,
1515
- errorCode !== null ? `code: ${errorCode}` : null
1516
- ]) ?? "Failed to reach OpenRouter voice transcription.";
1517
- }
1518
- function extractErrorMessage(error) {
1519
- if (error instanceof Error && error.message.trim().length > 0) return error.message.trim();
1520
- return extractStringField(error, "message");
1521
- }
1522
- function extractMetadataRawMessage(value) {
1523
- const raw = extractStringField(readField(value, "metadata") ?? readField(readField(value, "error"), "metadata") ?? readField(readField(readField(value, "data"), "error"), "metadata"), "raw");
1524
- if (!raw) return null;
1525
- return raw.length <= 280 ? raw : `${raw.slice(0, 277)}...`;
1526
- }
1527
- function extractProviderName(value) {
1528
- const candidates = [
1529
- readField(value, "metadata"),
1530
- readField(readField(value, "error"), "metadata"),
1531
- readField(readField(readField(value, "data"), "error"), "metadata")
1532
- ];
1533
- for (const candidate of candidates) {
1534
- const providerName = extractStringField(candidate, "provider_name") ?? extractStringField(candidate, "providerName") ?? extractStringField(candidate, "provider");
1535
- if (providerName) return providerName;
1536
- }
1537
- return null;
1538
- }
1539
- function extractErrorCode(...values) {
1540
- for (const value of values) {
1541
- const candidates = [
1542
- value,
1543
- readField(value, "error"),
1544
- readField(value, "data"),
1545
- readField(readField(value, "data"), "error")
1546
- ];
1547
- for (const candidate of candidates) {
1548
- const code = extractNumericField(candidate, "code");
1549
- if (code !== null) return code;
1550
- }
1551
- }
1552
- return null;
1553
- }
1554
- function extractNumericField(value, fieldName) {
1555
- if (!value || typeof value !== "object" || !(fieldName in value)) return null;
1556
- const fieldValue = value[fieldName];
1557
- return typeof fieldValue === "number" && Number.isFinite(fieldValue) ? fieldValue : null;
1558
- }
1559
- function extractStringField(value, fieldName) {
1560
- if (!value || typeof value !== "object" || !(fieldName in value)) return null;
1561
- const fieldValue = value[fieldName];
1562
- return typeof fieldValue === "string" && fieldValue.trim().length > 0 ? fieldValue.trim() : null;
1563
- }
1564
- function readField(value, fieldName) {
1565
- return value && typeof value === "object" && fieldName in value ? value[fieldName] : null;
1566
- }
1567
- function parseJsonBody(body) {
1568
- if (!body) return null;
1569
- try {
1570
- return JSON.parse(body);
1571
- } catch {
1572
- return null;
1573
- }
1574
- }
1575
- function dedupeNonEmptyStrings(values) {
1576
- const seen = /* @__PURE__ */ new Set();
1577
- const result = [];
1578
- for (const value of values) {
1579
- const normalized = value?.trim();
1580
- if (!normalized) continue;
1581
- const key = normalized.toLowerCase();
1582
- if (seen.has(key)) continue;
1583
- seen.add(key);
1584
- result.push(normalized);
1585
- }
1586
- return result;
1587
- }
1588
- function joinNonEmptyParts$1(parts) {
1589
- const filtered = parts.map((part) => part?.trim()).filter((part) => !!part);
1590
- return filtered.length > 0 ? filtered.join(" | ") : null;
1591
- }
1592
- function isGenericProviderMessage(message) {
1593
- const normalized = message.trim().toLowerCase();
1594
- return normalized === "provider returned error" || normalized === "failed to reach openrouter voice transcription.";
1595
- }
1596
- //#endregion
1597
- //#region src/services/voice-transcription/voice-transcription.service.ts
1598
- var VoiceTranscriptionService = class {
1599
- constructor(client) {
1600
- this.client = client;
1601
- }
1602
- getStatus() {
1603
- return this.client.getStatus();
1604
- }
1605
- async transcribeVoice(input) {
1606
- const text = (await this.client.transcribe(input)).text.trim();
1607
- if (!text) throw new VoiceTranscriptEmptyError("Voice transcription returned empty text.");
1608
- return { text };
1609
- }
1610
- };
1611
- //#endregion
1612
1172
  //#region src/use-cases/abort-prompt.usecase.ts
1613
1173
  var AbortPromptUseCase = class {
1614
1174
  constructor(sessionRepo, opencodeClient) {
@@ -1714,14 +1274,13 @@ var GetPathUseCase = class {
1714
1274
  //#endregion
1715
1275
  //#region src/use-cases/get-status.usecase.ts
1716
1276
  var GetStatusUseCase = class {
1717
- constructor(getHealthUseCase, getPathUseCase, listLspUseCase, listMcpUseCase, listSessionsUseCase, sessionRepo, voiceTranscriptionService) {
1277
+ constructor(getHealthUseCase, getPathUseCase, listLspUseCase, listMcpUseCase, listSessionsUseCase, sessionRepo) {
1718
1278
  this.getHealthUseCase = getHealthUseCase;
1719
1279
  this.getPathUseCase = getPathUseCase;
1720
1280
  this.listLspUseCase = listLspUseCase;
1721
1281
  this.listMcpUseCase = listMcpUseCase;
1722
1282
  this.listSessionsUseCase = listSessionsUseCase;
1723
1283
  this.sessionRepo = sessionRepo;
1724
- this.voiceTranscriptionService = voiceTranscriptionService;
1725
1284
  }
1726
1285
  async execute(input) {
1727
1286
  const [health, path, lsp, mcp] = await Promise.allSettled([
@@ -1736,7 +1295,6 @@ var GetStatusUseCase = class {
1736
1295
  health: mapSettledResult(health),
1737
1296
  path: pathResult,
1738
1297
  plugins,
1739
- voiceRecognition: this.voiceTranscriptionService.getStatus(),
1740
1298
  workspace,
1741
1299
  lsp: mapSettledResult(lsp),
1742
1300
  mcp: mapSettledResult(mcp)
@@ -2342,7 +1900,6 @@ function resolveExtension(mimeType) {
2342
1900
  }
2343
1901
  //#endregion
2344
1902
  //#region src/app/container.ts
2345
- var require = createRequire(import.meta.url);
2346
1903
  function createAppContainer(config, client) {
2347
1904
  const logger = createOpenCodeAppLogger(client, { level: config.logLevel });
2348
1905
  return createContainer(config, createOpenCodeClientFromSdkClient(client), logger);
@@ -2361,7 +1918,6 @@ function createContainer(config, opencodeClient, logger) {
2361
1918
  apiRoot: config.telegramApiRoot
2362
1919
  });
2363
1920
  const uploadFileUseCase = new UploadFileUseCase(telegramFileClient);
2364
- const voiceTranscriptionService = new VoiceTranscriptionService(createVoiceTranscriptionClient(config.openrouter));
2365
1921
  const abortPromptUseCase = new AbortPromptUseCase(sessionRepo, opencodeClient);
2366
1922
  const createSessionUseCase = new CreateSessionUseCase(sessionRepo, opencodeClient, logger);
2367
1923
  const getHealthUseCase = new GetHealthUseCase(opencodeClient);
@@ -2370,7 +1926,7 @@ function createContainer(config, opencodeClient, logger) {
2370
1926
  const listLspUseCase = new ListLspUseCase(sessionRepo, opencodeClient);
2371
1927
  const listMcpUseCase = new ListMcpUseCase(sessionRepo, opencodeClient);
2372
1928
  const listSessionsUseCase = new ListSessionsUseCase(sessionRepo, opencodeClient);
2373
- const getStatusUseCase = new GetStatusUseCase(getHealthUseCase, getPathUseCase, listLspUseCase, listMcpUseCase, listSessionsUseCase, sessionRepo, voiceTranscriptionService);
1929
+ const getStatusUseCase = new GetStatusUseCase(getHealthUseCase, getPathUseCase, listLspUseCase, listMcpUseCase, listSessionsUseCase, sessionRepo);
2374
1930
  const listModelsUseCase = new ListModelsUseCase(sessionRepo, opencodeClient);
2375
1931
  const renameSessionUseCase = new RenameSessionUseCase(sessionRepo, opencodeClient, logger);
2376
1932
  const sendPromptUseCase = new SendPromptUseCase(sessionRepo, opencodeClient, logger, foregroundSessionTracker);
@@ -2394,7 +1950,6 @@ function createContainer(config, opencodeClient, logger) {
2394
1950
  renameSessionUseCase,
2395
1951
  sessionRepo,
2396
1952
  sendPromptUseCase,
2397
- voiceTranscriptionService,
2398
1953
  switchAgentUseCase,
2399
1954
  switchModelUseCase,
2400
1955
  switchSessionUseCase,
@@ -2411,27 +1966,6 @@ function createContainer(config, opencodeClient, logger) {
2411
1966
  }
2412
1967
  };
2413
1968
  }
2414
- function createVoiceTranscriptionClient(config) {
2415
- return config.configured && config.apiKey ? new OpenRouterVoiceTranscriptionClient({
2416
- model: config.model,
2417
- timeoutMs: config.timeoutMs,
2418
- transcriptionPrompt: config.transcriptionPrompt
2419
- }, new OpenRouter({
2420
- apiKey: config.apiKey,
2421
- timeoutMs: config.timeoutMs
2422
- }), new FfmpegAudioTranscoder({
2423
- ffmpegPath: loadBundledFfmpegPath(),
2424
- timeoutMs: config.timeoutMs
2425
- })) : new DisabledVoiceTranscriptionClient();
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
- }
2435
1969
  //#endregion
2436
1970
  //#region src/app/bootstrap.ts
2437
1971
  function bootstrapPluginApp(client, configSource = {}, options = {}) {
@@ -2623,7 +2157,7 @@ var EN_BOT_COPY = {
2623
2157
  "1. Run `/status` to confirm the server is ready.",
2624
2158
  "2. Run `/new [title]` to create a fresh session.",
2625
2159
  "",
2626
- "Send a text, image, or voice message directly."
2160
+ "Send a text or image message directly."
2627
2161
  ] },
2628
2162
  systemStatus: { title: "System Status" },
2629
2163
  common: {
@@ -2662,12 +2196,7 @@ var EN_BOT_COPY = {
2662
2196
  requestAborted: "Request was aborted.",
2663
2197
  promptTimeout: "OpenCode request timed out.",
2664
2198
  structuredOutput: "Structured output validation failed.",
2665
- voiceNotConfigured: "Voice transcription is not configured.",
2666
- voiceDownload: "Failed to download the Telegram voice file.",
2667
- voiceTranscoding: "Voice audio preprocessing failed.",
2668
- voiceTranscription: "Voice transcription failed.",
2669
- voiceEmpty: "Voice transcription returned empty text.",
2670
- voiceUnsupported: "Voice message file is too large or unsupported.",
2199
+ voiceUnsupported: "Voice messages are not supported. Send text or an image instead.",
2671
2200
  imageDownload: "Failed to download the Telegram image file.",
2672
2201
  imageUnsupported: "Image file is too large or unsupported.",
2673
2202
  outputLength: "Reply hit the model output limit.",
@@ -2833,7 +2362,7 @@ var ZH_CN_BOT_COPY = {
2833
2362
  "1. 先运行 `/status` 确认服务状态正常。",
2834
2363
  "2. 运行 `/new [title]` 创建一个新会话。",
2835
2364
  "",
2836
- "直接发送文本、图片或语音消息即可。"
2365
+ "直接发送文本或图片消息即可。"
2837
2366
  ] },
2838
2367
  systemStatus: { title: "系统状态" },
2839
2368
  common: {
@@ -2872,12 +2401,7 @@ var ZH_CN_BOT_COPY = {
2872
2401
  requestAborted: "请求已中止。",
2873
2402
  promptTimeout: "OpenCode 响应超时。",
2874
2403
  structuredOutput: "结构化输出校验失败。",
2875
- voiceNotConfigured: "未配置语音转写服务。",
2876
- voiceDownload: "下载 Telegram 语音文件失败。",
2877
- voiceTranscoding: "语音转码失败。",
2878
- voiceTranscription: "语音转写失败。",
2879
- voiceEmpty: "语音转写结果为空。",
2880
- voiceUnsupported: "语音文件过大或不受支持。",
2404
+ voiceUnsupported: "暂不支持语音消息,请改发文本或图片。",
2881
2405
  imageDownload: "下载 Telegram 图片文件失败。",
2882
2406
  imageUnsupported: "图片文件过大或不受支持。",
2883
2407
  outputLength: "回复触发了模型输出长度上限。",
@@ -3223,30 +2747,6 @@ function normalizeError(error, copy) {
3223
2747
  message: copy.errors.structuredOutput,
3224
2748
  cause: joinNonEmptyParts([extractMessage(error.data), extractRetries(error.data)])
3225
2749
  };
3226
- if (isNamedError(error, "VoiceTranscriptionNotConfiguredError")) return {
3227
- message: copy.errors.voiceNotConfigured,
3228
- cause: extractMessage(error.data) ?? null
3229
- };
3230
- if (isNamedError(error, "TelegramFileDownloadError")) return {
3231
- message: copy.errors.voiceDownload,
3232
- cause: extractMessage(error.data) ?? null
3233
- };
3234
- if (isNamedError(error, "VoiceTranscodingFailedError")) return {
3235
- message: copy.errors.voiceTranscoding,
3236
- cause: extractMessage(error.data) ?? null
3237
- };
3238
- if (isNamedError(error, "VoiceTranscriptionFailedError")) return {
3239
- message: copy.errors.voiceTranscription,
3240
- cause: extractMessage(error.data) ?? null
3241
- };
3242
- if (isNamedError(error, "VoiceTranscriptEmptyError")) return {
3243
- message: copy.errors.voiceEmpty,
3244
- cause: extractMessage(error.data) ?? null
3245
- };
3246
- if (isNamedError(error, "VoiceMessageUnsupportedError")) return {
3247
- message: copy.errors.voiceUnsupported,
3248
- cause: extractMessage(error.data) ?? null
3249
- };
3250
2750
  if (isNamedError(error, "ImageFileDownloadError")) return {
3251
2751
  message: copy.errors.imageDownload,
3252
2752
  cause: extractMessage(error.data) ?? null
@@ -3379,7 +2879,7 @@ function presentStatusMarkdownSection(title, lines) {
3379
2879
  return [`## ${title}`, ...lines].join("\n");
3380
2880
  }
3381
2881
  function presentStatusPlainOverviewLines(input, copy, layout) {
3382
- const lines = [presentPlainStatusBullet(layout.connectivityLabel, input.health.status === "error" ? layout.errorStatus : formatHealthBadge(input.health.data.healthy, layout)), presentPlainStatusBullet(layout.voiceRecognitionLabel, formatVoiceRecognitionBadge(input.voiceRecognition, layout))];
2882
+ const lines = [presentPlainStatusBullet(layout.connectivityLabel, input.health.status === "error" ? layout.errorStatus : formatHealthBadge(input.health.data.healthy, layout))];
3383
2883
  if (input.health.status === "error") return [
3384
2884
  ...lines,
3385
2885
  ...presentStatusPlainErrorDetailLines(input.health.error, copy, layout),
@@ -3392,7 +2892,7 @@ function presentStatusPlainOverviewLines(input, copy, layout) {
3392
2892
  ];
3393
2893
  }
3394
2894
  function presentStatusMarkdownOverviewLines(input, copy, layout) {
3395
- const lines = [presentMarkdownStatusBullet(layout.connectivityLabel, input.health.status === "error" ? layout.errorStatus : formatHealthBadge(input.health.data.healthy, layout)), presentMarkdownStatusBullet(layout.voiceRecognitionLabel, formatVoiceRecognitionBadge(input.voiceRecognition, layout))];
2895
+ const lines = [presentMarkdownStatusBullet(layout.connectivityLabel, input.health.status === "error" ? layout.errorStatus : formatHealthBadge(input.health.data.healthy, layout))];
3396
2896
  if (input.health.status === "error") return [
3397
2897
  ...lines,
3398
2898
  ...presentStatusMarkdownErrorDetailLines(input.health.error, copy, layout),
@@ -3492,10 +2992,6 @@ function splitStatusLines(text) {
3492
2992
  function formatHealthBadge(healthy, layout) {
3493
2993
  return healthy ? "🟢" : layout.errorStatus;
3494
2994
  }
3495
- function formatVoiceRecognitionBadge(status, _layout) {
3496
- if (status.status === "configured") return status.model ? `\uD83D\uDFE2 (${status.model})` : "🟡";
3497
- return "⚪";
3498
- }
3499
2995
  function formatLspStatusBadge(status) {
3500
2996
  switch (status.status) {
3501
2997
  case "connected": return "🟢";
@@ -3564,7 +3060,6 @@ function getStatusLayoutCopy(copy) {
3564
3060
  rootLabel: "Root",
3565
3061
  statusLabel: "Status",
3566
3062
  tbotVersionLabel: "opencode-tbot Version",
3567
- voiceRecognitionLabel: "Voice Recognition",
3568
3063
  workspaceTitle: "📁 Workspace"
3569
3064
  };
3570
3065
  return {
@@ -3587,7 +3082,6 @@ function getStatusLayoutCopy(copy) {
3587
3082
  rootLabel: "根目录",
3588
3083
  statusLabel: "状态",
3589
3084
  tbotVersionLabel: "opencode-tbot版本",
3590
- voiceRecognitionLabel: "语音识别",
3591
3085
  workspaceTitle: "📁 工作区"
3592
3086
  };
3593
3087
  }
@@ -4737,13 +4231,13 @@ function isRecoverableStructuredOutputError(promptReply) {
4737
4231
  }
4738
4232
  //#endregion
4739
4233
  //#region src/bot/handlers/file.handler.ts
4740
- var TELEGRAM_MAX_DOWNLOAD_BYTES$1 = 20 * 1024 * 1024;
4234
+ var TELEGRAM_MAX_DOWNLOAD_BYTES = 20 * 1024 * 1024;
4741
4235
  async function handleImageMessage(ctx, dependencies) {
4742
4236
  const image = resolveTelegramImage(ctx.message);
4743
4237
  if (!image) return;
4744
4238
  if (await replyIfSessionRenamePending(ctx, dependencies)) return;
4745
4239
  await executePromptRequest(ctx, dependencies, async () => {
4746
- if (typeof image.fileSize === "number" && image.fileSize > TELEGRAM_MAX_DOWNLOAD_BYTES$1) throw new ImageMessageUnsupportedError(`Image file size ${image.fileSize} exceeds the Telegram download limit of ${TELEGRAM_MAX_DOWNLOAD_BYTES$1} bytes.`);
4240
+ if (typeof image.fileSize === "number" && image.fileSize > TELEGRAM_MAX_DOWNLOAD_BYTES) throw new ImageMessageUnsupportedError(`Image file size ${image.fileSize} exceeds the Telegram download limit of ${TELEGRAM_MAX_DOWNLOAD_BYTES} bytes.`);
4747
4241
  const filePath = (await ctx.getFile()).file_path?.trim();
4748
4242
  if (!filePath) throw new ImageMessageUnsupportedError("Telegram did not provide a downloadable image file path.");
4749
4243
  return {
@@ -4809,39 +4303,17 @@ function registerMessageHandler(bot, dependencies) {
4809
4303
  }
4810
4304
  //#endregion
4811
4305
  //#region src/bot/handlers/voice.handler.ts
4812
- var DEFAULT_VOICE_FILE_NAME = "telegram-voice.ogg";
4813
- var DEFAULT_VOICE_MIME_TYPE = "audio/ogg";
4814
- var TELEGRAM_MAX_DOWNLOAD_BYTES = 20 * 1024 * 1024;
4815
4306
  async function handleVoiceMessage(ctx, dependencies) {
4816
4307
  if (!ctx.message.voice) return;
4817
4308
  if (await replyIfSessionRenamePending(ctx, dependencies)) return;
4818
- await executePromptRequest(ctx, dependencies, async () => {
4819
- const voice = ctx.message.voice;
4820
- if (!voice) throw new VoiceMessageUnsupportedError("Telegram voice payload is missing.");
4821
- if (typeof voice.file_size === "number" && voice.file_size > TELEGRAM_MAX_DOWNLOAD_BYTES) throw new VoiceMessageUnsupportedError(`Voice file size ${voice.file_size} exceeds the Telegram download limit of ${TELEGRAM_MAX_DOWNLOAD_BYTES} bytes.`);
4822
- const filePath = (await ctx.getFile()).file_path?.trim();
4823
- if (!filePath) throw new VoiceMessageUnsupportedError("Telegram did not provide a downloadable voice file path.");
4824
- const download = await dependencies.telegramFileClient.downloadFile({ filePath });
4825
- const mimeType = voice.mime_type?.trim() || download.mimeType || DEFAULT_VOICE_MIME_TYPE;
4826
- const filename = resolveVoiceFilename(filePath);
4827
- return { text: (await dependencies.voiceTranscriptionService.transcribeVoice({
4828
- data: download.data,
4829
- filename,
4830
- mimeType
4831
- })).text };
4832
- });
4309
+ const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
4310
+ await ctx.reply(copy.errors.voiceUnsupported);
4833
4311
  }
4834
4312
  function registerVoiceHandler(bot, dependencies) {
4835
4313
  bot.on("message:voice", async (ctx) => {
4836
4314
  await handleVoiceMessage(ctx, dependencies);
4837
4315
  });
4838
4316
  }
4839
- function resolveVoiceFilename(filePath) {
4840
- const normalizedPath = filePath.trim();
4841
- if (!normalizedPath) return DEFAULT_VOICE_FILE_NAME;
4842
- const filename = normalizedPath.split("/").at(-1)?.trim();
4843
- return filename && filename.length > 0 ? filename : DEFAULT_VOICE_FILE_NAME;
4844
- }
4845
4317
  //#endregion
4846
4318
  //#region src/bot/middlewares/auth.ts
4847
4319
  function createAuthMiddleware(allowedChatIds) {