opencode-tbot 0.1.0

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 ADDED
@@ -0,0 +1,4198 @@
1
+ import { i as preparePluginConfiguration, s as loadAppConfig } from "./assets/plugin-config-Crgl_PZz.js";
2
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
3
+ import { basename, dirname, extname } from "node:path";
4
+ import { z } from "zod";
5
+ import { OpenRouter } from "@openrouter/sdk";
6
+ import { createOpencodeClient } from "@opencode-ai/sdk/v2/client";
7
+ import { randomUUID } from "node:crypto";
8
+ import { run } from "@grammyjs/runner";
9
+ import { Bot, InlineKeyboard } from "grammy";
10
+ //#region src/infra/utils/redact.ts
11
+ var REDACTED = "[REDACTED]";
12
+ var DEFAULT_PREVIEW_LENGTH = 160;
13
+ var TELEGRAM_TOKEN_PATTERN = /\b\d{6,}:[A-Za-z0-9_-]{20,}\b/g;
14
+ var BEARER_TOKEN_PATTERN = /\bBearer\s+[A-Za-z0-9._~+/-]+=*\b/gi;
15
+ var NAMED_SECRET_PATTERN = /\b(api[_\s-]?key|token|secret|password)\b(\s*[:=]\s*)([^\s,;]+)/gi;
16
+ var API_KEY_LIKE_PATTERN = /\b(?:sk|pk)_[A-Za-z0-9_-]{10,}\b/g;
17
+ function redactSensitiveText(input) {
18
+ return input.replace(BEARER_TOKEN_PATTERN, `Bearer ${REDACTED}`).replace(TELEGRAM_TOKEN_PATTERN, REDACTED).replace(NAMED_SECRET_PATTERN, (_, name, separator) => `${name}${separator}${REDACTED}`).replace(API_KEY_LIKE_PATTERN, REDACTED);
19
+ }
20
+ function createRedactedPreview(input, maxLength = DEFAULT_PREVIEW_LENGTH) {
21
+ const redacted = redactSensitiveText(input).replace(/\s+/g, " ").trim();
22
+ if (redacted.length <= maxLength) return redacted;
23
+ return `${redacted.slice(0, Math.max(0, maxLength - 3))}...`;
24
+ }
25
+ //#endregion
26
+ //#region src/infra/logger/index.ts
27
+ var DEFAULT_SERVICE_NAME = "opencode-tbot";
28
+ var LEVEL_PRIORITY = {
29
+ debug: 10,
30
+ info: 20,
31
+ warn: 30,
32
+ error: 40
33
+ };
34
+ function createOpenCodeAppLogger(client, options = {}) {
35
+ const service = normalizeServiceName(options.service);
36
+ const minimumLevel = normalizeLogLevel(options.level);
37
+ let queue = Promise.resolve();
38
+ const enqueue = (level, input, message) => {
39
+ if (LEVEL_PRIORITY[level] < LEVEL_PRIORITY[minimumLevel]) return;
40
+ const { extra, text } = normalizeLogArguments(input, message);
41
+ const payload = {
42
+ service,
43
+ level,
44
+ message: text,
45
+ ...extra ? { extra } : {}
46
+ };
47
+ queue = queue.catch(() => void 0).then(async () => {
48
+ try {
49
+ await client.app.log(payload);
50
+ } catch {}
51
+ });
52
+ };
53
+ return {
54
+ debug(input, message) {
55
+ enqueue("debug", input, message);
56
+ },
57
+ info(input, message) {
58
+ enqueue("info", input, message);
59
+ },
60
+ warn(input, message) {
61
+ enqueue("warn", input, message);
62
+ },
63
+ error(input, message) {
64
+ enqueue("error", input, message);
65
+ },
66
+ async flush() {
67
+ await queue.catch(() => void 0);
68
+ }
69
+ };
70
+ }
71
+ function normalizeServiceName(value) {
72
+ const normalized = value?.trim();
73
+ return normalized && normalized.length > 0 ? normalized : DEFAULT_SERVICE_NAME;
74
+ }
75
+ function normalizeLogLevel(value) {
76
+ switch (value?.trim().toLowerCase()) {
77
+ case "debug": return "debug";
78
+ case "warn": return "warn";
79
+ case "error": return "error";
80
+ default: return "info";
81
+ }
82
+ }
83
+ function normalizeLogArguments(input, message) {
84
+ if (typeof input === "string") return { text: input };
85
+ if (message && message.trim().length > 0) {
86
+ const extra = serializeExtra(input);
87
+ return {
88
+ ...extra ? { extra } : {},
89
+ text: message.trim()
90
+ };
91
+ }
92
+ if (input instanceof Error) return {
93
+ extra: serializeExtra(input) ?? void 0,
94
+ text: input.message.trim() || input.name
95
+ };
96
+ const extra = serializeExtra(input);
97
+ return {
98
+ ...extra ? { extra } : {},
99
+ text: "log"
100
+ };
101
+ }
102
+ function serializeExtra(input) {
103
+ if (input === null || input === void 0) return null;
104
+ if (input instanceof Error) return { error: serializeError(input) };
105
+ if (Array.isArray(input)) return { items: input.map((item) => sanitizeValue(item)) };
106
+ if (typeof input !== "object") return { value: sanitizeValue(input) };
107
+ return sanitizeRecord(input);
108
+ }
109
+ function sanitizeRecord(record) {
110
+ return Object.fromEntries(Object.entries(record).map(([key, value]) => [key, isSensitiveKey(key) ? redactSensitiveFieldValue(value) : sanitizeValue(value)]));
111
+ }
112
+ function sanitizeValue(value) {
113
+ if (value instanceof Error) return serializeError(value);
114
+ if (Array.isArray(value)) return value.map((item) => sanitizeValue(item));
115
+ if (typeof value === "string") return redactSensitiveText(value);
116
+ if (!value || typeof value !== "object") return value;
117
+ return sanitizeRecord(value);
118
+ }
119
+ function serializeError(error) {
120
+ return {
121
+ name: error.name,
122
+ message: redactSensitiveText(error.message),
123
+ ...error.stack ? { stack: redactSensitiveText(error.stack) } : {},
124
+ ..."data" in error && error.data && typeof error.data === "object" ? { data: sanitizeValue(error.data) } : {}
125
+ };
126
+ }
127
+ function isSensitiveKey(key) {
128
+ return /token|secret|api[-_]?key|authorization|password/iu.test(key);
129
+ }
130
+ function redactSensitiveFieldValue(value) {
131
+ if (typeof value === "string" && value.trim().length > 0) return "[REDACTED]";
132
+ if (Array.isArray(value)) return value.map(() => "[REDACTED]");
133
+ if (value && typeof value === "object") return "[REDACTED]";
134
+ return value;
135
+ }
136
+ //#endregion
137
+ //#region src/repositories/pending-action.repo.ts
138
+ var FilePendingActionRepository = class {
139
+ constructor(store) {
140
+ this.store = store;
141
+ }
142
+ async getByChatId(chatId) {
143
+ return (await this.store.read()).pendingActions[String(chatId)] ?? null;
144
+ }
145
+ async set(action) {
146
+ await this.store.update((state) => {
147
+ state.pendingActions[String(action.chatId)] = action;
148
+ });
149
+ }
150
+ async clear(chatId) {
151
+ await this.store.update((state) => {
152
+ delete state.pendingActions[String(chatId)];
153
+ });
154
+ }
155
+ };
156
+ //#endregion
157
+ //#region src/repositories/permission-approval.repo.ts
158
+ function buildApprovalKey(requestId, chatId) {
159
+ return `${requestId}:${chatId}`;
160
+ }
161
+ var FilePermissionApprovalRepository = class {
162
+ constructor(store) {
163
+ this.store = store;
164
+ }
165
+ async listByRequestId(requestId) {
166
+ const state = await this.store.read();
167
+ return Object.values(state.pendingPermissions).filter((approval) => approval.requestId === requestId);
168
+ }
169
+ async set(approval) {
170
+ await this.store.update((state) => {
171
+ state.pendingPermissions[buildApprovalKey(approval.requestId, approval.chatId)] = approval;
172
+ });
173
+ }
174
+ };
175
+ //#endregion
176
+ //#region src/repositories/session.repo.ts
177
+ var FileSessionRepository = class {
178
+ constructor(store) {
179
+ this.store = store;
180
+ }
181
+ async getByChatId(chatId) {
182
+ return (await this.store.read()).sessions[String(chatId)] ?? null;
183
+ }
184
+ async listBySessionId(sessionId) {
185
+ const state = await this.store.read();
186
+ return Object.values(state.sessions).filter((binding) => binding.sessionId === sessionId);
187
+ }
188
+ async setCurrent(binding) {
189
+ await this.store.update((state) => {
190
+ state.sessions[String(binding.chatId)] = binding;
191
+ });
192
+ }
193
+ async touch(chatId) {
194
+ await this.store.update((state) => {
195
+ const current = state.sessions[String(chatId)];
196
+ if (!current) return;
197
+ state.sessions[String(chatId)] = {
198
+ ...current,
199
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
200
+ };
201
+ });
202
+ }
203
+ };
204
+ //#endregion
205
+ //#region src/services/opencode/opencode.client.ts
206
+ function buildOpenCodeSdkConfig(options) {
207
+ const apiKey = options.apiKey?.trim();
208
+ return {
209
+ baseUrl: options.baseUrl,
210
+ ...apiKey ? { auth: apiKey } : {}
211
+ };
212
+ }
213
+ var EMPTY_RESPONSE_TEXT = "OpenCode returned empty response.";
214
+ var STRUCTURED_REPLY_SCHEMA = {
215
+ type: "json_schema",
216
+ retryCount: 2,
217
+ schema: {
218
+ type: "object",
219
+ additionalProperties: false,
220
+ required: ["body_md"],
221
+ properties: { body_md: {
222
+ type: "string",
223
+ description: "Markdown body only. Do not include duration, token usage, or any footer. Do not wrap the whole answer in ```md or ```markdown fences. Use Markdown formatting directly unless the user explicitly asks for raw Markdown source."
224
+ } }
225
+ }
226
+ };
227
+ var SDK_OPTIONS = {
228
+ responseStyle: "data",
229
+ throwOnError: true
230
+ };
231
+ var StructuredReplySchema = z.object({ body_md: z.string() });
232
+ var OpenCodeClient = class {
233
+ client;
234
+ fetchFn;
235
+ modelCache = {
236
+ expiresAt: 0,
237
+ promise: null,
238
+ value: null
239
+ };
240
+ constructor(options, client, fetchFn = fetch) {
241
+ if (!options && !client) throw new Error("OpenCodeClient requires either base URL options or an injected SDK client.");
242
+ this.client = client ?? createOpencodeClient(buildOpenCodeSdkConfig(options));
243
+ this.fetchFn = fetchFn;
244
+ }
245
+ async getHealth() {
246
+ return unwrapSdkData(await this.client.global.health(SDK_OPTIONS));
247
+ }
248
+ async abortSession(sessionId) {
249
+ return unwrapSdkData(await this.client.session.abort({ sessionID: sessionId }, SDK_OPTIONS));
250
+ }
251
+ async getPath() {
252
+ return unwrapSdkData(await this.client.path.get(void 0, SDK_OPTIONS));
253
+ }
254
+ async listLspStatuses(directory) {
255
+ return unwrapSdkData(await this.client.lsp.status(directory ? { directory } : void 0, SDK_OPTIONS));
256
+ }
257
+ async listMcpStatuses(directory) {
258
+ return unwrapSdkData(await this.client.mcp.status(directory ? { directory } : void 0, SDK_OPTIONS));
259
+ }
260
+ async getSessionStatuses() {
261
+ return unwrapSdkData(await this.client.session.status(void 0, SDK_OPTIONS));
262
+ }
263
+ async listProjects() {
264
+ return unwrapSdkData(await this.client.project.list(void 0, SDK_OPTIONS));
265
+ }
266
+ async listSessions() {
267
+ return unwrapSdkData(await this.client.session.list(void 0, SDK_OPTIONS));
268
+ }
269
+ async getCurrentProject() {
270
+ return unwrapSdkData(await this.client.project.current(void 0, SDK_OPTIONS));
271
+ }
272
+ async createSessionForDirectory(directory, title) {
273
+ return unwrapSdkData(await this.client.session.create(title ? {
274
+ directory,
275
+ title
276
+ } : { directory }, SDK_OPTIONS));
277
+ }
278
+ async renameSession(sessionId, title) {
279
+ return unwrapSdkData(await this.client.session.update({
280
+ sessionID: sessionId,
281
+ title
282
+ }, SDK_OPTIONS));
283
+ }
284
+ async listAgents() {
285
+ return unwrapSdkData(await this.client.app.agents(void 0, SDK_OPTIONS));
286
+ }
287
+ async listPendingPermissions(directory) {
288
+ return unwrapSdkData(await this.client.permission.list(directory ? { directory } : void 0, SDK_OPTIONS));
289
+ }
290
+ async replyToPermission(requestId, reply, message) {
291
+ return unwrapSdkData(await this.client.permission.reply({
292
+ requestID: requestId,
293
+ reply,
294
+ ...message?.trim() ? { message: message.trim() } : {}
295
+ }, SDK_OPTIONS));
296
+ }
297
+ async listModels() {
298
+ const now = Date.now();
299
+ if (this.modelCache.value && this.modelCache.expiresAt > now) return this.modelCache.value;
300
+ if (this.modelCache.promise) return this.modelCache.promise;
301
+ const refreshPromise = this.loadModels().finally(() => {
302
+ if (this.modelCache.promise === refreshPromise) this.modelCache.promise = null;
303
+ });
304
+ this.modelCache.promise = refreshPromise;
305
+ return refreshPromise;
306
+ }
307
+ async promptSession(input) {
308
+ const startedAt = Date.now();
309
+ const promptText = input.prompt?.trim() ?? "";
310
+ const parts = [...promptText ? [{
311
+ type: "text",
312
+ text: promptText
313
+ }] : [], ...(input.files ?? []).map((file) => ({
314
+ type: "file",
315
+ filename: file.filename,
316
+ mime: file.mime,
317
+ url: file.url
318
+ }))];
319
+ if (parts.length === 0) throw new Error("Prompt requires text or file attachments.");
320
+ const data = unwrapSdkData(await this.client.session.prompt({
321
+ sessionID: input.sessionId,
322
+ ...input.agent ? { agent: input.agent } : {},
323
+ ...input.structured ? { format: STRUCTURED_REPLY_SCHEMA } : {},
324
+ ...input.model ? { model: input.model } : {},
325
+ ...input.variant ? { variant: input.variant } : {},
326
+ parts
327
+ }, SDK_OPTIONS));
328
+ const finishedAt = Date.now();
329
+ const bodyMd = input.structured ? extractStructuredMarkdown(data.info?.structured) : null;
330
+ const fallbackText = extractTextFromParts(data.parts) || bodyMd || EMPTY_RESPONSE_TEXT;
331
+ return {
332
+ assistantError: data.info?.error ?? null,
333
+ bodyMd,
334
+ fallbackText,
335
+ info: data.info ?? null,
336
+ metrics: extractPromptMetrics(data.info, startedAt, finishedAt),
337
+ parts: data.parts,
338
+ structured: data.info?.structured ?? null
339
+ };
340
+ }
341
+ async loadModels() {
342
+ const [configResponse, providersResponse] = await Promise.all([this.client.config.get(void 0, SDK_OPTIONS), this.client.config.providers(void 0, SDK_OPTIONS)]);
343
+ const config = unwrapSdkData(configResponse);
344
+ const providerCatalog = unwrapSdkData(providersResponse);
345
+ const providerAvailability = await resolveProviderAvailability(config, this.fetchFn);
346
+ const models = buildSelectableModels(config, providerCatalog.providers, providerAvailability);
347
+ this.modelCache = {
348
+ expiresAt: Date.now() + 6e4,
349
+ promise: null,
350
+ value: models
351
+ };
352
+ return models;
353
+ }
354
+ };
355
+ function createOpenCodeClientFromSdkClient(client, fetchFn = fetch) {
356
+ return new OpenCodeClient(void 0, client, fetchFn);
357
+ }
358
+ function buildSelectableModels(config, providers, providerAvailability = /* @__PURE__ */ new Map()) {
359
+ const configuredProviders = config.provider ?? {};
360
+ const providersById = new Map(providers.map((provider) => [provider.id, provider]));
361
+ const models = /* @__PURE__ */ new Map();
362
+ for (const [providerId, providerConfig] of Object.entries(configuredProviders)) {
363
+ const configuredModels = providerConfig.models ?? {};
364
+ for (const [configuredModelKey, modelConfig] of Object.entries(configuredModels)) {
365
+ const model = buildConfiguredModel(providerId, providerConfig, configuredModelKey, modelConfig, providersById.get(providerId), providerAvailability.get(providerId) ?? null);
366
+ if (model) models.set(model.qualifiedId, model);
367
+ }
368
+ }
369
+ for (const provider of providers) for (const model of buildCatalogProviderModels(provider, providerAvailability.get(provider.id) ?? null)) if (!models.has(model.qualifiedId)) models.set(model.qualifiedId, model);
370
+ return [...models.values()];
371
+ }
372
+ function buildConfiguredModel(providerId, providerConfig, configuredModelKey, modelConfig, providerCatalog, availableModelIds) {
373
+ const modelId = modelConfig.id ?? configuredModelKey;
374
+ const catalogModel = providerCatalog?.models[modelId];
375
+ if (!isTextModel(modelConfig, catalogModel)) return null;
376
+ if (!isModelAvailable(modelId, availableModelIds)) return null;
377
+ return {
378
+ id: modelId,
379
+ providerID: providerId,
380
+ providerName: resolveProviderDisplayName(providerId, providerConfig, providerCatalog),
381
+ name: modelConfig.name ?? catalogModel?.name ?? modelId,
382
+ qualifiedId: `${providerId}/${modelId}`,
383
+ reasoning: modelConfig.reasoning ?? catalogModel?.capabilities.reasoning ?? false,
384
+ variants: normalizeVariants(modelConfig.variants ?? catalogModel?.variants)
385
+ };
386
+ }
387
+ function buildCatalogProviderModels(provider, availableModelIds) {
388
+ return Object.values(provider.models).filter((model) => isCatalogTextModel(model) && isModelAvailable(model.id, availableModelIds)).map((model) => ({
389
+ id: model.id,
390
+ providerID: provider.id,
391
+ providerName: provider.name,
392
+ name: model.name,
393
+ qualifiedId: `${provider.id}/${model.id}`,
394
+ reasoning: model.capabilities.reasoning,
395
+ variants: normalizeVariants(model.variants)
396
+ }));
397
+ }
398
+ function isTextModel(modelConfig, catalogModel) {
399
+ if (modelConfig.modalities) return modelConfig.modalities.input.includes("text") && modelConfig.modalities.output.includes("text");
400
+ if (catalogModel) return catalogModel.capabilities.input.text && catalogModel.capabilities.output.text;
401
+ return true;
402
+ }
403
+ function isCatalogTextModel(catalogModel) {
404
+ return catalogModel.capabilities.input.text && catalogModel.capabilities.output.text;
405
+ }
406
+ function isModelAvailable(modelId, availableModelIds) {
407
+ return availableModelIds === null || availableModelIds.has(modelId);
408
+ }
409
+ function resolveProviderDisplayName(providerId, providerConfig, providerCatalog) {
410
+ if (providerConfig.name) return providerConfig.name;
411
+ const configuredBaseUrl = extractConfiguredBaseUrl(providerConfig);
412
+ if (configuredBaseUrl) {
413
+ const host = safeParseUrlHost(configuredBaseUrl);
414
+ const label = providerCatalog?.name ?? providerId;
415
+ return host ? `${label} (${host})` : label;
416
+ }
417
+ return providerCatalog?.name ?? providerId;
418
+ }
419
+ function extractConfiguredBaseUrl(providerConfig) {
420
+ const options = providerConfig.options;
421
+ if (!options || typeof options !== "object") return null;
422
+ const baseUrl = "baseURL" in options ? options.baseURL : null;
423
+ return typeof baseUrl === "string" && baseUrl.trim().length > 0 ? baseUrl.trim() : null;
424
+ }
425
+ function safeParseUrlHost(value) {
426
+ try {
427
+ return new URL(value).host || null;
428
+ } catch {
429
+ return null;
430
+ }
431
+ }
432
+ function normalizeVariants(variants) {
433
+ if (!variants) return {};
434
+ return Object.fromEntries(Object.entries(variants).filter(([, config]) => !config?.disabled).map(([variant, config]) => {
435
+ return [variant, Object.fromEntries(Object.entries(config).filter(([key]) => key !== "disabled"))];
436
+ }));
437
+ }
438
+ function extractTextFromParts(parts) {
439
+ return parts.filter((part) => part.type === "text").map((part) => part.text).join("").trim();
440
+ }
441
+ function extractStructuredMarkdown(structured) {
442
+ const parsed = StructuredReplySchema.safeParse(structured);
443
+ if (!parsed.success) return null;
444
+ const bodyMd = parsed.data.body_md.replace(/\r\n?/g, "\n").trim();
445
+ return bodyMd.length > 0 ? bodyMd : null;
446
+ }
447
+ function extractPromptMetrics(info, startedAt, finishedAt) {
448
+ const createdAt = typeof info?.time?.created === "number" && Number.isFinite(info.time.created) ? info.time.created : null;
449
+ const completedAt = typeof info?.time?.completed === "number" && Number.isFinite(info.time.completed) ? info.time.completed : null;
450
+ const completedDuration = createdAt !== null && completedAt !== null ? completedAt - createdAt : null;
451
+ return {
452
+ durationMs: completedDuration !== null && completedDuration >= 0 ? completedDuration : Math.max(0, finishedAt - startedAt),
453
+ tokens: extractTokenMetrics(info)
454
+ };
455
+ }
456
+ function extractTokenMetrics(info) {
457
+ if (!info?.tokens) return {
458
+ total: null,
459
+ input: null,
460
+ output: null,
461
+ reasoning: null,
462
+ cacheRead: null,
463
+ cacheWrite: null
464
+ };
465
+ const input = coerceFiniteNumber(info.tokens.input);
466
+ const output = coerceFiniteNumber(info.tokens.output);
467
+ const reasoning = coerceFiniteNumber(info.tokens.reasoning);
468
+ const cacheRead = coerceFiniteNumber(info.tokens.cache?.read);
469
+ const cacheWrite = coerceFiniteNumber(info.tokens.cache?.write);
470
+ const total = coerceFiniteNumber(info.tokens.total);
471
+ const fallbackTotal = sumTokenMetrics(input, output, cacheRead, cacheWrite);
472
+ return {
473
+ total: total ?? fallbackTotal,
474
+ input,
475
+ output,
476
+ reasoning,
477
+ cacheRead,
478
+ cacheWrite
479
+ };
480
+ }
481
+ function coerceFiniteNumber(value) {
482
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
483
+ }
484
+ function sumTokenMetrics(...values) {
485
+ if (values.every((value) => value === null)) return null;
486
+ const total = values.reduce((sum, value) => sum + (value ?? 0), 0);
487
+ return Number.isFinite(total) ? total : null;
488
+ }
489
+ function unwrapSdkData(response) {
490
+ if (response && typeof response === "object" && "data" in response) return response.data;
491
+ return response;
492
+ }
493
+ async function resolveProviderAvailability(config, fetchFn) {
494
+ const configuredProviders = Object.entries(config.provider ?? {});
495
+ const availabilityEntries = await Promise.all(configuredProviders.map(async ([providerId, providerConfig]) => [providerId, await fetchProviderAvailableModelIds(providerConfig, fetchFn)]));
496
+ return new Map(availabilityEntries);
497
+ }
498
+ async function fetchProviderAvailableModelIds(providerConfig, fetchFn) {
499
+ const baseUrl = extractConfiguredBaseUrl(providerConfig);
500
+ const apiKey = extractConfiguredApiKey(providerConfig);
501
+ if (!baseUrl || !apiKey) return null;
502
+ try {
503
+ const response = await fetchFn(buildModelsEndpointUrl(baseUrl), {
504
+ headers: {
505
+ Authorization: `Bearer ${apiKey}`,
506
+ Accept: "application/json"
507
+ },
508
+ signal: AbortSignal.timeout(1e4)
509
+ });
510
+ if (!response.ok) return null;
511
+ const modelIds = extractModelIdsFromPayload(await response.json());
512
+ return modelIds ? new Set(modelIds) : null;
513
+ } catch {
514
+ return null;
515
+ }
516
+ }
517
+ function extractConfiguredApiKey(providerConfig) {
518
+ const options = providerConfig.options;
519
+ if (!options || typeof options !== "object") return null;
520
+ const apiKey = "apiKey" in options ? options.apiKey : null;
521
+ return typeof apiKey === "string" && apiKey.trim().length > 0 ? apiKey.trim() : null;
522
+ }
523
+ function buildModelsEndpointUrl(baseUrl) {
524
+ const normalizedBaseUrl = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
525
+ return new URL("models", normalizedBaseUrl).toString();
526
+ }
527
+ function extractModelIdsFromPayload(payload) {
528
+ if (Array.isArray(payload)) return payload.map(extractModelId).filter((id) => !!id);
529
+ if (payload && typeof payload === "object" && "data" in payload && Array.isArray(payload.data)) return payload.data.map(extractModelId).filter((id) => !!id);
530
+ return null;
531
+ }
532
+ function extractModelId(value) {
533
+ if (typeof value === "string" && value.trim().length > 0) return value.trim();
534
+ if (value && typeof value === "object" && "id" in value) {
535
+ const id = value.id;
536
+ return typeof id === "string" && id.trim().length > 0 ? id.trim() : null;
537
+ }
538
+ return null;
539
+ }
540
+ //#endregion
541
+ //#region src/services/storage/bot-state.ts
542
+ function createDefaultOpencodeTbotState() {
543
+ return {
544
+ version: 1,
545
+ sessions: {},
546
+ pendingActions: {},
547
+ pendingPermissions: {}
548
+ };
549
+ }
550
+ //#endregion
551
+ //#region src/services/storage/json-state-store.ts
552
+ var JsonStateStore = class {
553
+ createDefaultState;
554
+ filePath;
555
+ statePromise = null;
556
+ writeQueue = Promise.resolve();
557
+ constructor(options) {
558
+ this.createDefaultState = options.createDefaultState;
559
+ this.filePath = options.filePath;
560
+ }
561
+ async read() {
562
+ return cloneState(await this.loadState());
563
+ }
564
+ async update(mutator) {
565
+ const nextStatePromise = this.writeQueue.then(async () => {
566
+ const draft = cloneState(await this.loadState());
567
+ mutator(draft);
568
+ await writeStateFile(this.filePath, draft);
569
+ this.statePromise = Promise.resolve(draft);
570
+ return cloneState(draft);
571
+ });
572
+ this.writeQueue = nextStatePromise.then(() => void 0, () => void 0);
573
+ return nextStatePromise;
574
+ }
575
+ async loadState() {
576
+ if (!this.statePromise) this.statePromise = readStateFile(this.filePath, this.createDefaultState);
577
+ return this.statePromise;
578
+ }
579
+ };
580
+ async function readStateFile(filePath, createDefaultState) {
581
+ try {
582
+ const content = await readFile(filePath, "utf8");
583
+ return JSON.parse(content);
584
+ } catch (error) {
585
+ if (isMissingFileError(error)) return createDefaultState();
586
+ throw error;
587
+ }
588
+ }
589
+ async function writeStateFile(filePath, state) {
590
+ await mkdir(dirname(filePath), { recursive: true });
591
+ const temporaryFilePath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
592
+ await writeFile(temporaryFilePath, `${JSON.stringify(state, null, 2)}\n`, "utf8");
593
+ await rename(temporaryFilePath, filePath);
594
+ }
595
+ function cloneState(state) {
596
+ return JSON.parse(JSON.stringify(state));
597
+ }
598
+ function isMissingFileError(error) {
599
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
600
+ }
601
+ //#endregion
602
+ //#region src/services/telegram/telegram.client.ts
603
+ var TelegramFileDownloadError = class extends Error {
604
+ data;
605
+ constructor(message) {
606
+ super(message);
607
+ this.name = "TelegramFileDownloadError";
608
+ this.data = { message };
609
+ }
610
+ };
611
+ var VoiceMessageUnsupportedError = class extends Error {
612
+ data;
613
+ constructor(message) {
614
+ super(message);
615
+ this.name = "VoiceMessageUnsupportedError";
616
+ this.data = { message };
617
+ }
618
+ };
619
+ var TelegramFileClient = class {
620
+ baseUrl;
621
+ fetchFn;
622
+ constructor(options, fetchFn = fetch) {
623
+ this.baseUrl = options.baseUrl ?? buildTelegramFileApiRoot(options.apiRoot, options.botToken);
624
+ this.fetchFn = fetchFn;
625
+ }
626
+ async downloadFile(input) {
627
+ const filePath = input.filePath.trim();
628
+ if (!filePath) throw new TelegramFileDownloadError("Telegram did not provide a downloadable file path.");
629
+ let response;
630
+ try {
631
+ response = await this.fetchFn(new URL(filePath, this.baseUrl));
632
+ } catch (error) {
633
+ throw new TelegramFileDownloadError(extractErrorMessage$1(error) ?? "Failed to download the Telegram voice file.");
634
+ }
635
+ if (!response.ok) throw new TelegramFileDownloadError(await buildDownloadFailureMessage(response));
636
+ const data = new Uint8Array(await response.arrayBuffer());
637
+ if (data.byteLength === 0) throw new TelegramFileDownloadError("Telegram returned an empty voice file.");
638
+ return {
639
+ data,
640
+ mimeType: response.headers.get("content-type")
641
+ };
642
+ }
643
+ };
644
+ function buildTelegramFileApiRoot(apiRoot, botToken) {
645
+ return `${(apiRoot ?? "https://api.telegram.org").trim().replace(/\/+$/u, "")}/file/bot${botToken}/`;
646
+ }
647
+ async function buildDownloadFailureMessage(response) {
648
+ const normalizedText = (await safeReadResponseText(response))?.trim();
649
+ return normalizedText ? `Telegram file download failed with status ${response.status}: ${normalizedText}` : `Telegram file download failed with status ${response.status}.`;
650
+ }
651
+ async function safeReadResponseText(response) {
652
+ try {
653
+ const text = await response.text();
654
+ return text.trim().length > 0 ? text : null;
655
+ } catch {
656
+ return null;
657
+ }
658
+ }
659
+ function extractErrorMessage$1(error) {
660
+ return error instanceof Error && error.message.trim().length > 0 ? error.message.trim() : null;
661
+ }
662
+ //#endregion
663
+ //#region src/services/session-activity/foreground-session-tracker.ts
664
+ var ForegroundSessionTracker = class {
665
+ counts = /* @__PURE__ */ new Map();
666
+ begin(sessionId) {
667
+ const currentCount = this.counts.get(sessionId) ?? 0;
668
+ this.counts.set(sessionId, currentCount + 1);
669
+ return () => {
670
+ this.decrement(sessionId);
671
+ };
672
+ }
673
+ clear(sessionId) {
674
+ const wasForeground = this.counts.has(sessionId);
675
+ this.counts.delete(sessionId);
676
+ return wasForeground;
677
+ }
678
+ isForeground(sessionId) {
679
+ return this.counts.has(sessionId);
680
+ }
681
+ decrement(sessionId) {
682
+ const currentCount = this.counts.get(sessionId);
683
+ if (!currentCount || currentCount <= 1) {
684
+ this.counts.delete(sessionId);
685
+ return;
686
+ }
687
+ this.counts.set(sessionId, currentCount - 1);
688
+ }
689
+ };
690
+ var NOOP_FOREGROUND_SESSION_TRACKER = {
691
+ begin() {
692
+ return () => void 0;
693
+ },
694
+ clear() {
695
+ return false;
696
+ },
697
+ isForeground() {
698
+ return false;
699
+ }
700
+ };
701
+ //#endregion
702
+ //#region src/services/voice-transcription/openrouter-voice.client.ts
703
+ var VoiceTranscriptionNotConfiguredError = class extends Error {
704
+ data;
705
+ constructor(message) {
706
+ super(message);
707
+ this.name = "VoiceTranscriptionNotConfiguredError";
708
+ this.data = { message };
709
+ }
710
+ };
711
+ var VoiceTranscriptionFailedError = class extends Error {
712
+ data;
713
+ constructor(message) {
714
+ super(message);
715
+ this.name = "VoiceTranscriptionFailedError";
716
+ this.data = { message };
717
+ }
718
+ };
719
+ var VoiceTranscriptEmptyError = class extends Error {
720
+ data;
721
+ constructor(message) {
722
+ super(message);
723
+ this.name = "VoiceTranscriptEmptyError";
724
+ this.data = { message };
725
+ }
726
+ };
727
+ var DisabledVoiceTranscriptionClient = class {
728
+ async transcribe() {
729
+ throw new VoiceTranscriptionNotConfiguredError("Set openrouter.apiKey in tbot.config.json to enable Telegram voice transcription.");
730
+ }
731
+ };
732
+ var OpenRouterVoiceTranscriptionClient = class {
733
+ model;
734
+ sdk;
735
+ timeoutMs;
736
+ transcriptionPrompt;
737
+ constructor(options, sdk) {
738
+ this.model = options.model;
739
+ this.sdk = sdk;
740
+ this.timeoutMs = options.timeoutMs;
741
+ this.transcriptionPrompt = options.transcriptionPrompt?.trim() || null;
742
+ }
743
+ async transcribe(input) {
744
+ const format = resolveAudioFormat(input.filename, input.mimeType);
745
+ const audioData = toBase64(input.data);
746
+ const prompt = buildTranscriptionPrompt(this.transcriptionPrompt);
747
+ let response;
748
+ try {
749
+ response = await this.sdk.chat.send({ chatGenerationParams: {
750
+ messages: [{
751
+ role: "user",
752
+ content: [{
753
+ type: "text",
754
+ text: prompt
755
+ }, {
756
+ type: "input_audio",
757
+ inputAudio: {
758
+ data: audioData,
759
+ format
760
+ }
761
+ }]
762
+ }],
763
+ model: this.model,
764
+ stream: false,
765
+ temperature: 0
766
+ } }, { timeoutMs: this.timeoutMs });
767
+ } catch (error) {
768
+ throw new VoiceTranscriptionFailedError(extractErrorMessage(error) ?? "Failed to reach OpenRouter voice transcription.");
769
+ }
770
+ return { text: extractTranscript(response) };
771
+ }
772
+ };
773
+ var MIME_TYPE_FORMAT_MAP = {
774
+ "audio/aac": "aac",
775
+ "audio/aiff": "aiff",
776
+ "audio/flac": "flac",
777
+ "audio/m4a": "m4a",
778
+ "audio/mp3": "mp3",
779
+ "audio/mp4": "m4a",
780
+ "audio/mpeg": "mp3",
781
+ "audio/ogg": "ogg",
782
+ "audio/wav": "wav",
783
+ "audio/wave": "wav",
784
+ "audio/x-aac": "aac",
785
+ "audio/x-aiff": "aiff",
786
+ "audio/x-flac": "flac",
787
+ "audio/x-m4a": "m4a",
788
+ "audio/x-wav": "wav",
789
+ "audio/vnd.wave": "wav"
790
+ };
791
+ var FILE_EXTENSION_FORMAT_MAP = {
792
+ ".aac": "aac",
793
+ ".aif": "aiff",
794
+ ".aiff": "aiff",
795
+ ".flac": "flac",
796
+ ".m4a": "m4a",
797
+ ".mp3": "mp3",
798
+ ".oga": "ogg",
799
+ ".ogg": "ogg",
800
+ ".wav": "wav"
801
+ };
802
+ function resolveAudioFormat(filename, mimeType) {
803
+ const normalizedMimeType = mimeType?.trim().toLowerCase() || null;
804
+ if (normalizedMimeType && MIME_TYPE_FORMAT_MAP[normalizedMimeType]) return MIME_TYPE_FORMAT_MAP[normalizedMimeType];
805
+ const extension = extname(basename(filename).trim()).toLowerCase();
806
+ if (extension && FILE_EXTENSION_FORMAT_MAP[extension]) return FILE_EXTENSION_FORMAT_MAP[extension];
807
+ return "ogg";
808
+ }
809
+ function toBase64(data) {
810
+ const bytes = data instanceof Uint8Array ? data : new Uint8Array(data);
811
+ return Buffer.from(bytes).toString("base64");
812
+ }
813
+ function buildTranscriptionPrompt(transcriptionPrompt) {
814
+ const basePrompt = [
815
+ "Transcribe the provided audio verbatim.",
816
+ "Return only the transcript text.",
817
+ "Do not translate, summarize, explain, or add speaker labels.",
818
+ "If the audio is empty or unintelligible, return an empty string."
819
+ ].join(" ");
820
+ return transcriptionPrompt ? `${basePrompt}\n\nAdditional instructions: ${transcriptionPrompt}` : basePrompt;
821
+ }
822
+ function extractTranscript(response) {
823
+ const content = response.choices?.[0]?.message?.content;
824
+ if (typeof content === "string") return content.trim();
825
+ if (!Array.isArray(content)) return "";
826
+ return content.flatMap((item) => isTextContentItem(item) ? [item.text.trim()] : []).filter((text) => text.length > 0).join("\n").trim();
827
+ }
828
+ function isTextContentItem(value) {
829
+ return !!value && typeof value === "object" && "type" in value && value.type === "text" && "text" in value && typeof value.text === "string";
830
+ }
831
+ function extractErrorMessage(error) {
832
+ return error instanceof Error && error.message.trim().length > 0 ? error.message.trim() : null;
833
+ }
834
+ //#endregion
835
+ //#region src/services/voice-transcription/voice-transcription.service.ts
836
+ var VoiceTranscriptionService = class {
837
+ constructor(client) {
838
+ this.client = client;
839
+ }
840
+ async transcribeVoice(input) {
841
+ const text = (await this.client.transcribe(input)).text.trim();
842
+ if (!text) throw new VoiceTranscriptEmptyError("Voice transcription returned empty text.");
843
+ return { text };
844
+ }
845
+ };
846
+ //#endregion
847
+ //#region src/use-cases/abort-prompt.usecase.ts
848
+ var AbortPromptUseCase = class {
849
+ constructor(sessionRepo, opencodeClient) {
850
+ this.sessionRepo = sessionRepo;
851
+ this.opencodeClient = opencodeClient;
852
+ }
853
+ async execute(input) {
854
+ const binding = await this.sessionRepo.getByChatId(input.chatId);
855
+ if (!binding?.sessionId) return {
856
+ sessionId: null,
857
+ status: "no_session",
858
+ sessionStatus: null
859
+ };
860
+ const sessionId = binding.sessionId;
861
+ const sessionStatus = (await this.opencodeClient.getSessionStatuses())[sessionId] ?? null;
862
+ if (!sessionStatus || sessionStatus.type === "idle") return {
863
+ sessionId,
864
+ status: "not_running",
865
+ sessionStatus
866
+ };
867
+ return {
868
+ sessionId,
869
+ status: await this.opencodeClient.abortSession(sessionId) ? "aborted" : "not_running",
870
+ sessionStatus
871
+ };
872
+ }
873
+ };
874
+ //#endregion
875
+ //#region src/use-cases/session-creation.ts
876
+ async function createAndBindCurrentProjectSession(sessionRepo, opencodeClient, input) {
877
+ const binding = input.binding ?? await sessionRepo.getByChatId(input.chatId);
878
+ const project = await opencodeClient.getCurrentProject();
879
+ const title = normalizeOptionalText(input.title);
880
+ const session = await opencodeClient.createSessionForDirectory(project.worktree, title ?? void 0);
881
+ const nextBinding = {
882
+ chatId: input.chatId,
883
+ sessionId: session.id,
884
+ projectId: session.projectID,
885
+ directory: session.directory,
886
+ agentName: binding?.agentName ?? null,
887
+ modelProviderId: binding?.modelProviderId ?? null,
888
+ modelId: binding?.modelId ?? null,
889
+ modelVariant: binding?.modelVariant ?? null,
890
+ language: binding?.language ?? null,
891
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
892
+ };
893
+ await sessionRepo.setCurrent(nextBinding);
894
+ return {
895
+ binding: nextBinding,
896
+ project,
897
+ session
898
+ };
899
+ }
900
+ function normalizeOptionalText(value) {
901
+ const normalized = value?.trim();
902
+ return normalized ? normalized : null;
903
+ }
904
+ //#endregion
905
+ //#region src/use-cases/create-session.usecase.ts
906
+ var CreateSessionUseCase = class {
907
+ constructor(sessionRepo, opencodeClient, logger) {
908
+ this.sessionRepo = sessionRepo;
909
+ this.opencodeClient = opencodeClient;
910
+ this.logger = logger;
911
+ }
912
+ async execute(input) {
913
+ const binding = await this.sessionRepo.getByChatId(input.chatId);
914
+ const result = await createAndBindCurrentProjectSession(this.sessionRepo, this.opencodeClient, {
915
+ chatId: input.chatId,
916
+ title: input.title,
917
+ binding
918
+ });
919
+ this.logger.info({
920
+ chatId: input.chatId,
921
+ sessionId: result.session.id,
922
+ projectId: result.session.projectID,
923
+ directory: result.session.directory,
924
+ title: input.title?.trim() || null
925
+ }, "session created");
926
+ return { session: result.session };
927
+ }
928
+ };
929
+ //#endregion
930
+ //#region src/use-cases/get-health.usecase.ts
931
+ var GetHealthUseCase = class {
932
+ constructor(opencodeClient) {
933
+ this.opencodeClient = opencodeClient;
934
+ }
935
+ async execute() {
936
+ return this.opencodeClient.getHealth();
937
+ }
938
+ };
939
+ //#endregion
940
+ //#region src/use-cases/get-path.usecase.ts
941
+ var GetPathUseCase = class {
942
+ constructor(opencodeClient) {
943
+ this.opencodeClient = opencodeClient;
944
+ }
945
+ async execute() {
946
+ return this.opencodeClient.getPath();
947
+ }
948
+ };
949
+ //#endregion
950
+ //#region src/use-cases/get-status.usecase.ts
951
+ var GetStatusUseCase = class {
952
+ constructor(getHealthUseCase, getPathUseCase, listLspUseCase, listMcpUseCase) {
953
+ this.getHealthUseCase = getHealthUseCase;
954
+ this.getPathUseCase = getPathUseCase;
955
+ this.listLspUseCase = listLspUseCase;
956
+ this.listMcpUseCase = listMcpUseCase;
957
+ }
958
+ async execute(input) {
959
+ const [health, path, lsp, mcp] = await Promise.allSettled([
960
+ this.getHealthUseCase.execute(),
961
+ this.getPathUseCase.execute(),
962
+ this.listLspUseCase.execute({ chatId: input.chatId }),
963
+ this.listMcpUseCase.execute({ chatId: input.chatId })
964
+ ]);
965
+ return {
966
+ health: mapSettledResult(health),
967
+ path: mapSettledResult(path),
968
+ lsp: mapSettledResult(lsp),
969
+ mcp: mapSettledResult(mcp)
970
+ };
971
+ }
972
+ };
973
+ function mapSettledResult(result) {
974
+ if (result.status === "fulfilled") return {
975
+ data: result.value,
976
+ status: "ok"
977
+ };
978
+ return {
979
+ error: result.reason,
980
+ status: "error"
981
+ };
982
+ }
983
+ function resolveSelectedAgent(agents, requestedAgentName) {
984
+ const visibleAgents = agents.filter((agent) => !agent.hidden);
985
+ if (requestedAgentName) {
986
+ const requestedAgent = visibleAgents.find((agent) => agent.name === requestedAgentName);
987
+ if (requestedAgent) return requestedAgent;
988
+ }
989
+ return visibleAgents.find((agent) => agent.name === "build") ?? null;
990
+ }
991
+ //#endregion
992
+ //#region src/use-cases/binding-update.ts
993
+ async function persistBindingUpdate(sessionRepo, binding, updates) {
994
+ const nextBinding = {
995
+ ...binding,
996
+ ...updates,
997
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
998
+ };
999
+ await sessionRepo.setCurrent(nextBinding);
1000
+ return nextBinding;
1001
+ }
1002
+ async function clearStoredAgentSelection(sessionRepo, binding) {
1003
+ return persistBindingUpdate(sessionRepo, binding, { agentName: null });
1004
+ }
1005
+ async function clearStoredModelSelection(sessionRepo, binding) {
1006
+ return persistBindingUpdate(sessionRepo, binding, {
1007
+ modelProviderId: null,
1008
+ modelId: null,
1009
+ modelVariant: null
1010
+ });
1011
+ }
1012
+ async function clearStoredModelVariant(sessionRepo, binding) {
1013
+ return persistBindingUpdate(sessionRepo, binding, { modelVariant: null });
1014
+ }
1015
+ async function clearStoredSessionContext(sessionRepo, binding) {
1016
+ return persistBindingUpdate(sessionRepo, binding, {
1017
+ sessionId: null,
1018
+ projectId: null,
1019
+ directory: null
1020
+ });
1021
+ }
1022
+ //#endregion
1023
+ //#region src/use-cases/list-agents.usecase.ts
1024
+ var ListAgentsUseCase = class {
1025
+ constructor(sessionRepo, opencodeClient) {
1026
+ this.sessionRepo = sessionRepo;
1027
+ this.opencodeClient = opencodeClient;
1028
+ }
1029
+ async execute(input) {
1030
+ let [binding, agents] = await Promise.all([this.sessionRepo.getByChatId(input.chatId), this.opencodeClient.listAgents()]);
1031
+ const visibleAgents = agents.filter((agent) => !agent.hidden);
1032
+ const currentAgent = resolveSelectedAgent(visibleAgents, binding?.agentName);
1033
+ if (binding?.agentName && currentAgent?.name !== binding.agentName) binding = await clearStoredAgentSelection(this.sessionRepo, binding);
1034
+ return {
1035
+ agents: visibleAgents,
1036
+ currentAgentName: currentAgent?.name ?? null
1037
+ };
1038
+ }
1039
+ };
1040
+ //#endregion
1041
+ //#region src/use-cases/list-lsp.usecase.ts
1042
+ var ListLspUseCase = class {
1043
+ constructor(sessionRepo, opencodeClient) {
1044
+ this.sessionRepo = sessionRepo;
1045
+ this.opencodeClient = opencodeClient;
1046
+ }
1047
+ async execute(input) {
1048
+ let binding = await this.sessionRepo.getByChatId(input.chatId);
1049
+ if (binding?.projectId && binding.directory) {
1050
+ if (!hasMatchingProject$2(await this.opencodeClient.listProjects(), binding)) binding = await clearStoredSessionContext(this.sessionRepo, binding);
1051
+ }
1052
+ const projectContext = binding?.projectId && binding.directory ? {
1053
+ projectId: binding.projectId,
1054
+ directory: binding.directory
1055
+ } : await this.getCurrentProjectContext();
1056
+ const statuses = await this.opencodeClient.listLspStatuses(projectContext.directory);
1057
+ return {
1058
+ currentDirectory: projectContext.directory,
1059
+ statuses: [...statuses].sort(compareLspStatuses)
1060
+ };
1061
+ }
1062
+ async getCurrentProjectContext() {
1063
+ const currentProject = await this.opencodeClient.getCurrentProject();
1064
+ return {
1065
+ projectId: currentProject.id,
1066
+ directory: currentProject.worktree
1067
+ };
1068
+ }
1069
+ };
1070
+ function compareLspStatuses(left, right) {
1071
+ return left.name.localeCompare(right.name) || left.root.localeCompare(right.root) || left.id.localeCompare(right.id);
1072
+ }
1073
+ function hasMatchingProject$2(projects, binding) {
1074
+ return projects.some((project) => project.id === binding.projectId && project.worktree === binding.directory);
1075
+ }
1076
+ //#endregion
1077
+ //#region src/use-cases/list-mcp.usecase.ts
1078
+ var ListMcpUseCase = class {
1079
+ constructor(sessionRepo, opencodeClient) {
1080
+ this.sessionRepo = sessionRepo;
1081
+ this.opencodeClient = opencodeClient;
1082
+ }
1083
+ async execute(input) {
1084
+ let binding = await this.sessionRepo.getByChatId(input.chatId);
1085
+ if (binding?.projectId && binding.directory) {
1086
+ if (!hasMatchingProject$1(await this.opencodeClient.listProjects(), binding)) binding = await clearStoredSessionContext(this.sessionRepo, binding);
1087
+ }
1088
+ const projectContext = binding?.projectId && binding.directory ? {
1089
+ projectId: binding.projectId,
1090
+ directory: binding.directory
1091
+ } : await this.getCurrentProjectContext();
1092
+ const statuses = await this.opencodeClient.listMcpStatuses(projectContext.directory);
1093
+ return {
1094
+ currentDirectory: projectContext.directory,
1095
+ statuses: Object.entries(statuses).sort(([left], [right]) => left.localeCompare(right)).map(([name, status]) => ({
1096
+ name,
1097
+ status
1098
+ }))
1099
+ };
1100
+ }
1101
+ async getCurrentProjectContext() {
1102
+ const currentProject = await this.opencodeClient.getCurrentProject();
1103
+ return {
1104
+ projectId: currentProject.id,
1105
+ directory: currentProject.worktree
1106
+ };
1107
+ }
1108
+ };
1109
+ function hasMatchingProject$1(projects, binding) {
1110
+ return projects.some((project) => project.id === binding.projectId && project.worktree === binding.directory);
1111
+ }
1112
+ //#endregion
1113
+ //#region src/use-cases/list-models.usecase.ts
1114
+ var ListModelsUseCase = class {
1115
+ constructor(sessionRepo, opencodeClient) {
1116
+ this.sessionRepo = sessionRepo;
1117
+ this.opencodeClient = opencodeClient;
1118
+ }
1119
+ async execute(input) {
1120
+ let [binding, models] = await Promise.all([this.sessionRepo.getByChatId(input.chatId), this.opencodeClient.listModels()]);
1121
+ if (binding?.modelProviderId && binding?.modelId) {
1122
+ const selectedModel = models.find((model) => model.providerID === binding?.modelProviderId && model.id === binding?.modelId);
1123
+ if (!selectedModel) binding = await clearStoredModelSelection(this.sessionRepo, binding);
1124
+ else if (binding.modelVariant && !(binding.modelVariant in selectedModel.variants)) binding = await clearStoredModelVariant(this.sessionRepo, binding);
1125
+ }
1126
+ return {
1127
+ models,
1128
+ currentModelProviderId: binding?.modelProviderId ?? null,
1129
+ currentModelId: binding?.modelId ?? null,
1130
+ currentModelVariant: binding?.modelVariant ?? null
1131
+ };
1132
+ }
1133
+ };
1134
+ //#endregion
1135
+ //#region src/use-cases/list-sessions.usecase.ts
1136
+ var ListSessionsUseCase = class {
1137
+ constructor(sessionRepo, opencodeClient) {
1138
+ this.sessionRepo = sessionRepo;
1139
+ this.opencodeClient = opencodeClient;
1140
+ }
1141
+ async execute(input) {
1142
+ let binding = await this.sessionRepo.getByChatId(input.chatId);
1143
+ const [sessions, projects] = await Promise.all([this.opencodeClient.listSessions(), binding?.projectId && binding.directory ? this.opencodeClient.listProjects() : Promise.resolve(null)]);
1144
+ if (binding?.projectId && binding.directory && !hasMatchingProject(projects, binding)) binding = await clearStoredSessionContext(this.sessionRepo, binding);
1145
+ if (binding?.sessionId && binding.projectId && !hasMatchingSession(sessions, binding)) binding = await clearStoredSessionContext(this.sessionRepo, binding);
1146
+ const projectContext = binding?.projectId && binding.directory ? {
1147
+ projectId: binding.projectId,
1148
+ directory: binding.directory
1149
+ } : await this.getCurrentProjectContext();
1150
+ return {
1151
+ sessions: sessions.filter((session) => session.projectID === projectContext.projectId),
1152
+ currentProjectId: projectContext.projectId,
1153
+ currentDirectory: projectContext.directory,
1154
+ currentSessionId: binding?.projectId === projectContext.projectId ? binding.sessionId : null
1155
+ };
1156
+ }
1157
+ async getCurrentProjectContext() {
1158
+ const currentProject = await this.opencodeClient.getCurrentProject();
1159
+ return {
1160
+ projectId: currentProject.id,
1161
+ directory: currentProject.worktree
1162
+ };
1163
+ }
1164
+ };
1165
+ function hasMatchingProject(projects, binding) {
1166
+ if (!projects) return true;
1167
+ return projects.some((project) => project.id === binding.projectId && project.worktree === binding.directory);
1168
+ }
1169
+ function hasMatchingSession(sessions, binding) {
1170
+ return sessions.some((session) => session.id === binding.sessionId && session.projectID === binding.projectId);
1171
+ }
1172
+ //#endregion
1173
+ //#region src/use-cases/rename-session.usecase.ts
1174
+ var RenameSessionUseCase = class {
1175
+ constructor(sessionRepo, opencodeClient, logger) {
1176
+ this.sessionRepo = sessionRepo;
1177
+ this.opencodeClient = opencodeClient;
1178
+ this.logger = logger;
1179
+ }
1180
+ async execute(input) {
1181
+ const title = input.title.trim();
1182
+ if (!title) throw new Error("Session title cannot be empty.");
1183
+ const [binding, sessions] = await Promise.all([this.sessionRepo.getByChatId(input.chatId), this.opencodeClient.listSessions()]);
1184
+ const currentProjectId = binding?.projectId ?? (await this.opencodeClient.getCurrentProject()).id;
1185
+ const session = sessions.find((item) => item.id === input.sessionId && item.projectID === currentProjectId);
1186
+ if (!session) return { found: false };
1187
+ const renamedSession = await this.opencodeClient.renameSession(session.id, title);
1188
+ this.logger.info({
1189
+ chatId: input.chatId,
1190
+ sessionId: renamedSession.id,
1191
+ projectId: renamedSession.projectID,
1192
+ title: renamedSession.title
1193
+ }, "session renamed");
1194
+ return {
1195
+ found: true,
1196
+ session: renamedSession
1197
+ };
1198
+ }
1199
+ };
1200
+ //#endregion
1201
+ //#region src/use-cases/send-prompt.usecase.ts
1202
+ var SendPromptUseCase = class {
1203
+ constructor(sessionRepo, opencodeClient, logger, foregroundSessionTracker = NOOP_FOREGROUND_SESSION_TRACKER) {
1204
+ this.sessionRepo = sessionRepo;
1205
+ this.opencodeClient = opencodeClient;
1206
+ this.logger = logger;
1207
+ this.foregroundSessionTracker = foregroundSessionTracker;
1208
+ }
1209
+ async execute(input) {
1210
+ const files = input.files ?? [];
1211
+ const promptText = buildPromptText(input.text, files);
1212
+ if (!promptText && files.length === 0) throw new Error("Prompt requires text or file attachments.");
1213
+ let binding = await this.sessionRepo.getByChatId(input.chatId);
1214
+ if (binding?.projectId && binding.directory) {
1215
+ if (!(await this.opencodeClient.listProjects()).find((project) => project.id === binding?.projectId && project.worktree === binding.directory)) binding = await this.clearInvalidSessionContext(input.chatId, binding, "saved project is no longer available");
1216
+ }
1217
+ if (binding?.sessionId && binding.projectId) {
1218
+ if (!(await this.opencodeClient.listSessions()).find((session) => session.id === binding?.sessionId && session.projectID === binding.projectId)) binding = await this.clearInvalidSessionContext(input.chatId, binding, "saved session is no longer available");
1219
+ }
1220
+ if (!binding || !binding.sessionId || !binding.projectId || !binding.directory) {
1221
+ const createdSession = await createAndBindCurrentProjectSession(this.sessionRepo, this.opencodeClient, {
1222
+ chatId: input.chatId,
1223
+ binding
1224
+ });
1225
+ binding = createdSession.binding;
1226
+ this.logger.info({
1227
+ chatId: input.chatId,
1228
+ sessionId: createdSession.session.id,
1229
+ projectId: createdSession.session.projectID,
1230
+ directory: createdSession.session.directory
1231
+ }, "session created");
1232
+ }
1233
+ if (binding?.modelProviderId && binding?.modelId) {
1234
+ const selectedModel = (await this.opencodeClient.listModels()).find((model) => model.providerID === binding?.modelProviderId && model.id === binding?.modelId);
1235
+ if (!selectedModel) {
1236
+ binding = await clearStoredModelSelection(this.sessionRepo, binding);
1237
+ this.logger.warn?.({ chatId: input.chatId }, "selected model is no longer available, falling back to OpenCode default");
1238
+ } else if (binding.modelVariant && !(binding.modelVariant in selectedModel.variants)) {
1239
+ binding = await clearStoredModelVariant(this.sessionRepo, binding);
1240
+ this.logger.warn?.({
1241
+ chatId: input.chatId,
1242
+ providerId: selectedModel.providerID,
1243
+ modelId: selectedModel.id
1244
+ }, "selected model variant is no longer available, falling back to default variant");
1245
+ }
1246
+ }
1247
+ if (!binding || !binding.sessionId || !binding.projectId) throw new Error("Failed to initialize chat session.");
1248
+ let activeBinding = binding;
1249
+ const model = activeBinding.modelProviderId && activeBinding.modelId ? {
1250
+ providerID: activeBinding.modelProviderId,
1251
+ modelID: activeBinding.modelId
1252
+ } : null;
1253
+ const selectedAgent = resolveSelectedAgent(await this.opencodeClient.listAgents(), activeBinding.agentName);
1254
+ if (activeBinding.agentName && selectedAgent?.name !== activeBinding.agentName) {
1255
+ activeBinding = await clearStoredAgentSelection(this.sessionRepo, activeBinding);
1256
+ this.logger.warn?.({ chatId: input.chatId }, "selected agent is no longer available, falling back to OpenCode default");
1257
+ }
1258
+ const endForegroundSession = this.foregroundSessionTracker.begin(activeBinding.sessionId);
1259
+ let result;
1260
+ try {
1261
+ result = await this.opencodeClient.promptSession({
1262
+ sessionId: activeBinding.sessionId,
1263
+ prompt: promptText,
1264
+ ...files.length > 0 ? { files } : {},
1265
+ ...selectedAgent ? { agent: selectedAgent.name } : {},
1266
+ structured: true,
1267
+ ...model ? { model } : {},
1268
+ ...activeBinding.modelVariant ? { variant: activeBinding.modelVariant } : {}
1269
+ });
1270
+ } finally {
1271
+ endForegroundSession();
1272
+ }
1273
+ await this.sessionRepo.touch(input.chatId);
1274
+ return {
1275
+ assistantReply: result,
1276
+ sessionId: activeBinding.sessionId,
1277
+ projectId: activeBinding.projectId
1278
+ };
1279
+ }
1280
+ async clearInvalidSessionContext(chatId, binding, reason) {
1281
+ const nextBinding = await clearStoredSessionContext(this.sessionRepo, binding);
1282
+ this.logger.warn?.({ chatId }, `${reason}, falling back to the current OpenCode project`);
1283
+ return nextBinding;
1284
+ }
1285
+ };
1286
+ function buildPromptText(text, files) {
1287
+ const trimmedText = text?.trim() ?? "";
1288
+ if (!files.some(isImageFile)) return trimmedText;
1289
+ const promptSections = [[
1290
+ "System note for this turn:",
1291
+ "The user attached one or more images in this message and you can inspect those images directly.",
1292
+ "Ignore any earlier statements in this conversation claiming that you could not view images or attachments, because those may have come from a previous model or an earlier limitation.",
1293
+ "If an image is blurry or unreadable, say that it is blurry or unreadable instead of saying that you cannot view images."
1294
+ ].join(" ")];
1295
+ if (trimmedText) promptSections.push(trimmedText);
1296
+ return promptSections.join("\n\n");
1297
+ }
1298
+ function isImageFile(file) {
1299
+ return file.mime.trim().toLowerCase().startsWith("image/");
1300
+ }
1301
+ //#endregion
1302
+ //#region src/use-cases/switch-agent.usecase.ts
1303
+ var SwitchAgentUseCase = class {
1304
+ constructor(sessionRepo, opencodeClient, logger) {
1305
+ this.sessionRepo = sessionRepo;
1306
+ this.opencodeClient = opencodeClient;
1307
+ this.logger = logger;
1308
+ }
1309
+ async execute(input) {
1310
+ const [binding, agents] = await Promise.all([this.sessionRepo.getByChatId(input.chatId), this.opencodeClient.listAgents()]);
1311
+ const agent = agents.find((item) => item.name === input.agentName && !item.hidden);
1312
+ if (!agent) return { found: false };
1313
+ await this.sessionRepo.setCurrent({
1314
+ chatId: input.chatId,
1315
+ sessionId: binding?.sessionId ?? null,
1316
+ projectId: binding?.projectId ?? null,
1317
+ directory: binding?.directory ?? null,
1318
+ agentName: agent.name,
1319
+ modelProviderId: binding?.modelProviderId ?? null,
1320
+ modelId: binding?.modelId ?? null,
1321
+ modelVariant: binding?.modelVariant ?? null,
1322
+ language: binding?.language ?? null,
1323
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1324
+ });
1325
+ this.logger.info({
1326
+ chatId: input.chatId,
1327
+ agentName: agent.name
1328
+ }, "agent switched");
1329
+ return {
1330
+ found: true,
1331
+ agent
1332
+ };
1333
+ }
1334
+ };
1335
+ //#endregion
1336
+ //#region src/use-cases/switch-model.usecase.ts
1337
+ var SwitchModelUseCase = class {
1338
+ constructor(sessionRepo, opencodeClient, logger) {
1339
+ this.sessionRepo = sessionRepo;
1340
+ this.opencodeClient = opencodeClient;
1341
+ this.logger = logger;
1342
+ }
1343
+ async execute(input) {
1344
+ const [binding, models] = await Promise.all([this.sessionRepo.getByChatId(input.chatId), this.opencodeClient.listModels()]);
1345
+ const model = models.find((item) => item.providerID === input.providerId && item.id === input.modelId);
1346
+ if (!model) return { found: false };
1347
+ const variant = input.variant ?? null;
1348
+ if (variant && !(variant in model.variants)) return { found: false };
1349
+ await this.sessionRepo.setCurrent({
1350
+ chatId: input.chatId,
1351
+ sessionId: binding?.sessionId ?? null,
1352
+ projectId: binding?.projectId ?? null,
1353
+ directory: binding?.directory ?? null,
1354
+ agentName: binding?.agentName ?? null,
1355
+ modelProviderId: model.providerID,
1356
+ modelId: model.id,
1357
+ modelVariant: variant,
1358
+ language: binding?.language ?? null,
1359
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1360
+ });
1361
+ this.logger.info({
1362
+ chatId: input.chatId,
1363
+ providerId: model.providerID,
1364
+ modelId: model.id,
1365
+ variant
1366
+ }, "model switched");
1367
+ return {
1368
+ found: true,
1369
+ model,
1370
+ variant
1371
+ };
1372
+ }
1373
+ };
1374
+ //#endregion
1375
+ //#region src/use-cases/switch-session.usecase.ts
1376
+ var SwitchSessionUseCase = class {
1377
+ constructor(sessionRepo, opencodeClient, logger) {
1378
+ this.sessionRepo = sessionRepo;
1379
+ this.opencodeClient = opencodeClient;
1380
+ this.logger = logger;
1381
+ }
1382
+ async execute(input) {
1383
+ const [binding, sessions] = await Promise.all([this.sessionRepo.getByChatId(input.chatId), this.opencodeClient.listSessions()]);
1384
+ const currentProjectId = binding?.projectId ?? (await this.opencodeClient.getCurrentProject()).id;
1385
+ const session = sessions.find((item) => item.id === input.sessionId && item.projectID === currentProjectId);
1386
+ if (!session) return { found: false };
1387
+ await this.sessionRepo.setCurrent({
1388
+ chatId: input.chatId,
1389
+ sessionId: session.id,
1390
+ projectId: session.projectID,
1391
+ directory: session.directory,
1392
+ agentName: binding?.agentName ?? null,
1393
+ modelProviderId: binding?.modelProviderId ?? null,
1394
+ modelId: binding?.modelId ?? null,
1395
+ modelVariant: binding?.modelVariant ?? null,
1396
+ language: binding?.language ?? null,
1397
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1398
+ });
1399
+ this.logger.info({
1400
+ chatId: input.chatId,
1401
+ sessionId: session.id,
1402
+ projectId: session.projectID,
1403
+ directory: session.directory
1404
+ }, "session switched");
1405
+ return {
1406
+ found: true,
1407
+ session
1408
+ };
1409
+ }
1410
+ };
1411
+ //#endregion
1412
+ //#region src/use-cases/upload-file.usecase.ts
1413
+ var ImageFileDownloadError = class extends Error {
1414
+ data;
1415
+ constructor(message) {
1416
+ super(message);
1417
+ this.name = "ImageFileDownloadError";
1418
+ this.data = { message };
1419
+ }
1420
+ };
1421
+ var ImageMessageUnsupportedError = class extends Error {
1422
+ data;
1423
+ constructor(message) {
1424
+ super(message);
1425
+ this.name = "ImageMessageUnsupportedError";
1426
+ this.data = { message };
1427
+ }
1428
+ };
1429
+ var UploadFileUseCase = class {
1430
+ constructor(telegramFileClient) {
1431
+ this.telegramFileClient = telegramFileClient;
1432
+ }
1433
+ async execute(input) {
1434
+ if (input.expectedType !== "image") throw new ImageMessageUnsupportedError("Only image uploads are supported.");
1435
+ let download;
1436
+ try {
1437
+ download = await this.telegramFileClient.downloadFile({ filePath: input.filePath });
1438
+ } catch (error) {
1439
+ if (error instanceof TelegramFileDownloadError) throw new ImageFileDownloadError(error.message);
1440
+ throw error;
1441
+ }
1442
+ const mimeType = normalizeMimeType(input.mimeType ?? download.mimeType);
1443
+ if (!mimeType.startsWith("image/")) throw new ImageMessageUnsupportedError(`Unsupported image MIME type: ${mimeType}`);
1444
+ return {
1445
+ filename: resolveFilename(input.filename, input.filePath, mimeType),
1446
+ mime: mimeType,
1447
+ url: buildDataUrl(download.data, mimeType)
1448
+ };
1449
+ }
1450
+ };
1451
+ function buildDataUrl(data, mimeType) {
1452
+ return `data:${mimeType};base64,${Buffer.from(data.buffer, data.byteOffset, data.byteLength).toString("base64")}`;
1453
+ }
1454
+ function normalizeMimeType(value) {
1455
+ const normalized = value?.trim().toLowerCase();
1456
+ return normalized && normalized.length > 0 ? normalized : "application/octet-stream";
1457
+ }
1458
+ function resolveFilename(explicitFilename, filePath, mimeType) {
1459
+ const preferredFilename = explicitFilename?.trim();
1460
+ if (preferredFilename) return preferredFilename;
1461
+ const filePathFilename = filePath.split("/").at(-1)?.trim();
1462
+ if (filePathFilename) return filePathFilename;
1463
+ return `telegram-upload${resolveExtension(mimeType)}`;
1464
+ }
1465
+ function resolveExtension(mimeType) {
1466
+ switch (mimeType) {
1467
+ case "image/jpeg": return ".jpg";
1468
+ case "image/png": return ".png";
1469
+ case "image/webp": return ".webp";
1470
+ case "image/gif": return ".gif";
1471
+ default: return "";
1472
+ }
1473
+ }
1474
+ //#endregion
1475
+ //#region src/app/container.ts
1476
+ function createAppContainer(config, client) {
1477
+ const logger = createOpenCodeAppLogger(client, { level: config.logLevel });
1478
+ return createContainer(config, createOpenCodeClientFromSdkClient(client), logger);
1479
+ }
1480
+ function createContainer(config, opencodeClient, logger) {
1481
+ const stateStore = new JsonStateStore({
1482
+ filePath: config.stateFilePath,
1483
+ createDefaultState: createDefaultOpencodeTbotState
1484
+ });
1485
+ const pendingActionRepo = new FilePendingActionRepository(stateStore);
1486
+ const permissionApprovalRepo = new FilePermissionApprovalRepository(stateStore);
1487
+ const sessionRepo = new FileSessionRepository(stateStore);
1488
+ const foregroundSessionTracker = new ForegroundSessionTracker();
1489
+ const telegramFileClient = new TelegramFileClient({
1490
+ botToken: config.telegramBotToken,
1491
+ apiRoot: config.telegramApiRoot
1492
+ });
1493
+ const uploadFileUseCase = new UploadFileUseCase(telegramFileClient);
1494
+ const voiceTranscriptionService = new VoiceTranscriptionService(createVoiceTranscriptionClient(config.openrouter));
1495
+ const abortPromptUseCase = new AbortPromptUseCase(sessionRepo, opencodeClient);
1496
+ const createSessionUseCase = new CreateSessionUseCase(sessionRepo, opencodeClient, logger);
1497
+ const getHealthUseCase = new GetHealthUseCase(opencodeClient);
1498
+ const getPathUseCase = new GetPathUseCase(opencodeClient);
1499
+ const listAgentsUseCase = new ListAgentsUseCase(sessionRepo, opencodeClient);
1500
+ const listLspUseCase = new ListLspUseCase(sessionRepo, opencodeClient);
1501
+ const listMcpUseCase = new ListMcpUseCase(sessionRepo, opencodeClient);
1502
+ const getStatusUseCase = new GetStatusUseCase(getHealthUseCase, getPathUseCase, listLspUseCase, listMcpUseCase);
1503
+ const listModelsUseCase = new ListModelsUseCase(sessionRepo, opencodeClient);
1504
+ const listSessionsUseCase = new ListSessionsUseCase(sessionRepo, opencodeClient);
1505
+ const renameSessionUseCase = new RenameSessionUseCase(sessionRepo, opencodeClient, logger);
1506
+ const sendPromptUseCase = new SendPromptUseCase(sessionRepo, opencodeClient, logger, foregroundSessionTracker);
1507
+ const switchAgentUseCase = new SwitchAgentUseCase(sessionRepo, opencodeClient, logger);
1508
+ const switchModelUseCase = new SwitchModelUseCase(sessionRepo, opencodeClient, logger);
1509
+ const switchSessionUseCase = new SwitchSessionUseCase(sessionRepo, opencodeClient, logger);
1510
+ let disposed = false;
1511
+ return {
1512
+ abortPromptUseCase,
1513
+ createSessionUseCase,
1514
+ getHealthUseCase,
1515
+ getPathUseCase,
1516
+ getStatusUseCase,
1517
+ listAgentsUseCase,
1518
+ listLspUseCase,
1519
+ listMcpUseCase,
1520
+ logger,
1521
+ listModelsUseCase,
1522
+ listSessionsUseCase,
1523
+ pendingActionRepo,
1524
+ renameSessionUseCase,
1525
+ sessionRepo,
1526
+ sendPromptUseCase,
1527
+ voiceTranscriptionService,
1528
+ switchAgentUseCase,
1529
+ switchModelUseCase,
1530
+ switchSessionUseCase,
1531
+ telegramFileClient,
1532
+ uploadFileUseCase,
1533
+ opencodeClient,
1534
+ foregroundSessionTracker,
1535
+ permissionApprovalRepo,
1536
+ async dispose() {
1537
+ if (disposed) return;
1538
+ disposed = true;
1539
+ logger.info({ filePath: config.stateFilePath }, "disposing telegram bot container");
1540
+ await logger.flush();
1541
+ }
1542
+ };
1543
+ }
1544
+ function createVoiceTranscriptionClient(config) {
1545
+ return config.configured && config.apiKey ? new OpenRouterVoiceTranscriptionClient({
1546
+ model: config.model,
1547
+ timeoutMs: config.timeoutMs,
1548
+ transcriptionPrompt: config.transcriptionPrompt
1549
+ }, new OpenRouter({
1550
+ apiKey: config.apiKey,
1551
+ timeoutMs: config.timeoutMs
1552
+ })) : new DisabledVoiceTranscriptionClient();
1553
+ }
1554
+ //#endregion
1555
+ //#region src/app/bootstrap.ts
1556
+ function bootstrapPluginApp(client, configSource = {}, options = {}) {
1557
+ const config = loadAppConfig(configSource, options);
1558
+ return {
1559
+ config,
1560
+ container: createAppContainer(config, client)
1561
+ };
1562
+ }
1563
+ //#endregion
1564
+ //#region src/services/permission/telegram-approval.ts
1565
+ function buildPermissionApprovalMessage(request) {
1566
+ const patterns = request.patterns.length > 0 ? request.patterns.map((pattern) => `- ${pattern}`).join("\n") : "- (none)";
1567
+ return [
1568
+ "*Permission Request*",
1569
+ "",
1570
+ `Permission: \`${escapeMarkdownV2(request.permission)}\``,
1571
+ `Session: \`${escapeMarkdownV2(request.sessionID)}\``,
1572
+ "",
1573
+ "Patterns:",
1574
+ patterns
1575
+ ].join("\n");
1576
+ }
1577
+ function buildPermissionApprovalResolvedMessage(requestId, reply) {
1578
+ return [
1579
+ "Permission request resolved.",
1580
+ "",
1581
+ `Request: ${requestId}`,
1582
+ `Reply: ${reply}`
1583
+ ].join("\n");
1584
+ }
1585
+ function buildPermissionApprovalKeyboard(requestId) {
1586
+ return { inline_keyboard: [[
1587
+ {
1588
+ text: "Allow once",
1589
+ callback_data: buildPermissionApprovalCallbackData("once", requestId)
1590
+ },
1591
+ {
1592
+ text: "Always allow",
1593
+ callback_data: buildPermissionApprovalCallbackData("always", requestId)
1594
+ },
1595
+ {
1596
+ text: "Reject",
1597
+ callback_data: buildPermissionApprovalCallbackData("reject", requestId)
1598
+ }
1599
+ ]] };
1600
+ }
1601
+ function buildPermissionApprovalCallbackData(reply, requestId) {
1602
+ return `permission:${reply}:${requestId}`;
1603
+ }
1604
+ function parsePermissionApprovalCallbackData(data) {
1605
+ if (!data.startsWith("permission:")) return null;
1606
+ const [, reply, requestId] = data.split(":", 3);
1607
+ if (!requestId || !isPermissionApprovalReply(reply)) return null;
1608
+ return {
1609
+ reply,
1610
+ requestId
1611
+ };
1612
+ }
1613
+ function isPermissionApprovalReply(value) {
1614
+ return value === "once" || value === "always" || value === "reject";
1615
+ }
1616
+ function escapeMarkdownV2(value) {
1617
+ return value.replaceAll(/([_*\[\]()~`>#+\-=|{}.!\\])/g, "\\$1");
1618
+ }
1619
+ //#endregion
1620
+ //#region src/app/plugin-events.ts
1621
+ async function handleTelegramBotPluginEvent(runtime, event) {
1622
+ switch (event.type) {
1623
+ case "permission.asked":
1624
+ await handlePermissionAsked(runtime, event.properties);
1625
+ return;
1626
+ case "permission.replied":
1627
+ await handlePermissionReplied(runtime, event.properties.requestID, event.properties.reply);
1628
+ return;
1629
+ case "session.error":
1630
+ await handleSessionError(runtime, event.properties.sessionID, event.properties.error);
1631
+ return;
1632
+ case "session.idle":
1633
+ await handleSessionIdle(runtime, event.properties.sessionID);
1634
+ return;
1635
+ default: return;
1636
+ }
1637
+ }
1638
+ async function handlePermissionAsked(runtime, request) {
1639
+ const bindings = await runtime.container.sessionRepo.listBySessionId(request.sessionID);
1640
+ const approvals = await runtime.container.permissionApprovalRepo.listByRequestId(request.id);
1641
+ const approvedChatIds = new Set(approvals.map((approval) => approval.chatId));
1642
+ for (const binding of bindings) {
1643
+ if (approvedChatIds.has(binding.chatId)) continue;
1644
+ try {
1645
+ const message = await runtime.bot.api.sendMessage(binding.chatId, buildPermissionApprovalMessage(request), {
1646
+ parse_mode: "MarkdownV2",
1647
+ reply_markup: buildPermissionApprovalKeyboard(request.id)
1648
+ });
1649
+ await runtime.container.permissionApprovalRepo.set({
1650
+ requestId: request.id,
1651
+ sessionId: request.sessionID,
1652
+ chatId: binding.chatId,
1653
+ messageId: message.message_id,
1654
+ status: "pending",
1655
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1656
+ });
1657
+ } catch (error) {
1658
+ runtime.container.logger.error({
1659
+ error,
1660
+ chatId: binding.chatId,
1661
+ requestId: request.id
1662
+ }, "failed to deliver permission request to Telegram");
1663
+ }
1664
+ }
1665
+ }
1666
+ async function handlePermissionReplied(runtime, requestId, reply) {
1667
+ const approvals = await runtime.container.permissionApprovalRepo.listByRequestId(requestId);
1668
+ await Promise.all(approvals.map(async (approval) => {
1669
+ try {
1670
+ await runtime.bot.api.editMessageText(approval.chatId, approval.messageId, buildPermissionApprovalResolvedMessage(requestId, reply));
1671
+ } catch (error) {
1672
+ runtime.container.logger.warn({
1673
+ error,
1674
+ chatId: approval.chatId,
1675
+ requestId
1676
+ }, "failed to update Telegram permission message");
1677
+ }
1678
+ await runtime.container.permissionApprovalRepo.set(toResolvedApproval(approval, reply));
1679
+ }));
1680
+ }
1681
+ async function handleSessionError(runtime, sessionId, error) {
1682
+ if (!sessionId) {
1683
+ runtime.container.logger.error({ error }, "session error received without a session id");
1684
+ return;
1685
+ }
1686
+ if (runtime.container.foregroundSessionTracker.isForeground(sessionId)) {
1687
+ runtime.container.logger.warn({
1688
+ error,
1689
+ sessionId
1690
+ }, "session error suppressed for foreground Telegram session");
1691
+ return;
1692
+ }
1693
+ await notifyBoundChats(runtime, sessionId, `Session failed.\n\nSession: ${sessionId}\nError: ${error?.data?.message?.trim() || error?.name?.trim() || "Unknown session error."}`);
1694
+ }
1695
+ async function handleSessionIdle(runtime, sessionId) {
1696
+ if (runtime.container.foregroundSessionTracker.clear(sessionId)) {
1697
+ runtime.container.logger.info({ sessionId }, "session idle notification suppressed for foreground Telegram session");
1698
+ return;
1699
+ }
1700
+ await notifyBoundChats(runtime, sessionId, `Session finished.\n\nSession: ${sessionId}`);
1701
+ }
1702
+ async function notifyBoundChats(runtime, sessionId, text) {
1703
+ const bindings = await runtime.container.sessionRepo.listBySessionId(sessionId);
1704
+ const chatIds = [...new Set(bindings.map((binding) => binding.chatId))];
1705
+ await Promise.all(chatIds.map(async (chatId) => {
1706
+ try {
1707
+ await runtime.bot.api.sendMessage(chatId, text);
1708
+ } catch (error) {
1709
+ runtime.container.logger.warn({
1710
+ error,
1711
+ chatId,
1712
+ sessionId
1713
+ }, "failed to notify Telegram chat about session event");
1714
+ }
1715
+ }));
1716
+ }
1717
+ function toResolvedApproval(approval, reply) {
1718
+ return {
1719
+ ...approval,
1720
+ status: reply,
1721
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1722
+ };
1723
+ }
1724
+ var SUPPORTED_BOT_LANGUAGES = ["en", "zh-CN"];
1725
+ var EN_BOT_COPY = {
1726
+ commands: {
1727
+ start: "Welcome and quick start",
1728
+ help: "Show commands and examples",
1729
+ status: "Show system status",
1730
+ new: "Create a new session",
1731
+ agents: "View and switch agents",
1732
+ sessions: "View and switch sessions",
1733
+ cancel: "Cancel rename or abort running request",
1734
+ model: "View and switch models",
1735
+ language: "View and switch language"
1736
+ },
1737
+ start: { lines: [
1738
+ "# Welcome to opencode-tbot",
1739
+ "",
1740
+ "Talk to your OpenCode server from Telegram.",
1741
+ "",
1742
+ "## What you can send",
1743
+ "- Text prompts",
1744
+ "- Images with an optional caption",
1745
+ "- Voice messages (requires OpenRouter voice transcription)",
1746
+ "",
1747
+ "## Quick start",
1748
+ "1. Run `/status` to confirm the server is ready.",
1749
+ "2. Run `/new [title]` to create a fresh session.",
1750
+ "3. Send a text, image, or voice message.",
1751
+ "",
1752
+ "Use `/help` to see the full command list and examples."
1753
+ ] },
1754
+ help: { lines: [
1755
+ "# Help",
1756
+ "",
1757
+ "Use this bot to chat with OpenCode from Telegram.",
1758
+ "",
1759
+ "## Commands",
1760
+ "- `/status` Check server, workspace, MCP, and LSP status",
1761
+ "- `/new [title]` Create a new session",
1762
+ "- `/sessions` View, switch, or rename sessions",
1763
+ "- `/agents` View and switch agents",
1764
+ "- `/model` View and switch models and reasoning levels",
1765
+ "- `/language` Switch the bot display language",
1766
+ "- `/cancel` Cancel session rename or abort the running request",
1767
+ "",
1768
+ "## Examples",
1769
+ "- `/new bug triage`",
1770
+ "- Send a plain text message directly",
1771
+ "- Send an image with a caption",
1772
+ "- Send a voice message if OpenRouter voice transcription is configured"
1773
+ ] },
1774
+ systemStatus: { title: "System Status" },
1775
+ common: {
1776
+ notSelected: "Not selected",
1777
+ openCodeDefault: "Not selected (using OpenCode default)",
1778
+ previousPage: "Previous",
1779
+ nextPage: "Next",
1780
+ page(currentPage, totalPages) {
1781
+ return `Page ${currentPage}/${totalPages}`;
1782
+ }
1783
+ },
1784
+ status: {
1785
+ processing: "Processing...",
1786
+ alreadyProcessing: "Another request is still running. Wait for it to finish before sending a new prompt."
1787
+ },
1788
+ prompt: { emptyResponse: "OpenCode returned empty response." },
1789
+ replyMetrics: {
1790
+ durationLabel: "Duration",
1791
+ tokensLabel: "Tokens",
1792
+ totalLabel: "total",
1793
+ inputLabel: "input",
1794
+ outputLabel: "output",
1795
+ reasoningLabel: "reasoning",
1796
+ cacheReadLabel: "cache.read",
1797
+ cacheWriteLabel: "cache.write",
1798
+ notAvailable: "n/a"
1799
+ },
1800
+ abort: {
1801
+ noSession: "No active session is bound to this chat yet.",
1802
+ notRunning: "No request is currently running for the current session.",
1803
+ aborted: "Abort signal sent to the current session."
1804
+ },
1805
+ errors: {
1806
+ unexpected: "Unexpected error.",
1807
+ providerAuth: "Provider authentication failed.",
1808
+ requestAborted: "Request was aborted.",
1809
+ structuredOutput: "Structured output validation failed.",
1810
+ voiceNotConfigured: "Voice transcription is not configured.",
1811
+ voiceDownload: "Failed to download the Telegram voice file.",
1812
+ voiceTranscription: "Voice transcription failed.",
1813
+ voiceEmpty: "Voice transcription returned empty text.",
1814
+ voiceUnsupported: "Voice message file is too large or unsupported.",
1815
+ imageDownload: "Failed to download the Telegram image file.",
1816
+ imageUnsupported: "Image file is too large or unsupported.",
1817
+ outputLength: "Reply hit the model output limit.",
1818
+ contextOverflow: "Conversation exceeded the model context window.",
1819
+ providerRequest: "Provider request failed.",
1820
+ notFound: "Requested resource was not found.",
1821
+ badRequest: "Request was rejected by OpenCode.",
1822
+ causeLabel: "Cause",
1823
+ retryableLabel: "retryable",
1824
+ statusCodeLabel: "status"
1825
+ },
1826
+ health: {
1827
+ title: "Server Health",
1828
+ status(healthy) {
1829
+ return `Status: ${healthy ? "healthy" : "unhealthy"}`;
1830
+ },
1831
+ version(version) {
1832
+ return `Version: ${version}`;
1833
+ }
1834
+ },
1835
+ path: {
1836
+ title: "Current Paths",
1837
+ home(path) {
1838
+ return `Home: ${path}`;
1839
+ },
1840
+ state(path) {
1841
+ return `State: ${path}`;
1842
+ },
1843
+ config(path) {
1844
+ return `Config: ${path}`;
1845
+ },
1846
+ worktree(path) {
1847
+ return `Worktree: ${path}`;
1848
+ },
1849
+ directory(path) {
1850
+ return `Current working directory: ${path}`;
1851
+ }
1852
+ },
1853
+ sessions: {
1854
+ none: "No sessions available in the current project.",
1855
+ title: "Session List",
1856
+ actionTitle: "Session Actions",
1857
+ chooseAction: "Choose what to do with this session.",
1858
+ currentProject(worktree) {
1859
+ return `Current project: ${worktree}`;
1860
+ },
1861
+ currentSession(session) {
1862
+ return `Current session: ${session}`;
1863
+ },
1864
+ selectedSession(session) {
1865
+ return `Selected session: ${session}`;
1866
+ },
1867
+ switched: "Session switched.",
1868
+ created: "Session created.",
1869
+ renamed: "Session renamed.",
1870
+ renameCancelled: "Session rename cancelled.",
1871
+ renameEmpty: "Session name cannot be empty. Send a new name or /cancel.",
1872
+ renameExpired: "The session is no longer available. Run /sessions again.",
1873
+ renamePendingInput: "Waiting for the new session name. Send plain text or /cancel.",
1874
+ renamePrompt(session) {
1875
+ return [
1876
+ `Rename session: ${session}`,
1877
+ "Send the new session name as your next text message.",
1878
+ "Send /cancel to cancel."
1879
+ ].join("\n");
1880
+ },
1881
+ switchAction: "Switch",
1882
+ renameAction: "Rename",
1883
+ backToList: "Back",
1884
+ expired: "The session is no longer available. Run /sessions again."
1885
+ },
1886
+ lsp: {
1887
+ none: "No LSP servers detected for the current project.",
1888
+ title: "LSP Servers",
1889
+ currentProject(worktree) {
1890
+ return `Current project: ${worktree}`;
1891
+ },
1892
+ connected: "connected",
1893
+ error: "error"
1894
+ },
1895
+ mcp: {
1896
+ none: "No MCP servers configured for the current project.",
1897
+ title: "MCP Servers",
1898
+ currentProject(worktree) {
1899
+ return `Current project: ${worktree}`;
1900
+ },
1901
+ connected: "connected",
1902
+ disabled: "disabled",
1903
+ needsAuth: "needs auth",
1904
+ failed(error) {
1905
+ return `failed: ${error}`;
1906
+ },
1907
+ needsClientRegistration(error) {
1908
+ return `needs client registration: ${error}`;
1909
+ }
1910
+ },
1911
+ agents: {
1912
+ none: "No agents available.",
1913
+ title: "Agent List",
1914
+ current(agent) {
1915
+ return `Current agent: ${agent}`;
1916
+ },
1917
+ switched: "Agent switched.",
1918
+ expired: "The agent is no longer available. Run /agents again."
1919
+ },
1920
+ models: {
1921
+ none: "No models available.",
1922
+ title: "Model List",
1923
+ configuredOnly: "Only models currently available in OpenCode and connected providers are shown.",
1924
+ current(model) {
1925
+ return `Current model: ${model}`;
1926
+ },
1927
+ switched: "Model switched.",
1928
+ currentReasoningLevel(variant) {
1929
+ return `Current reasoning level: ${variant}`;
1930
+ },
1931
+ reasoningLevel(variant) {
1932
+ return `Reasoning level: ${variant}`;
1933
+ },
1934
+ noReasoningLevels: "This model has no selectable reasoning levels.",
1935
+ reasoningLevelsTitle: "Reasoning Levels",
1936
+ model(model) {
1937
+ return `Model: ${model}`;
1938
+ },
1939
+ modelNumber(modelIndex) {
1940
+ return `Model number: ${modelIndex}`;
1941
+ },
1942
+ expired: "The model is no longer available. Run /model again.",
1943
+ reasoningLevelExpired: "The reasoning level is no longer available. Run /model again.",
1944
+ defaultReasoningLevel: "default"
1945
+ },
1946
+ language: {
1947
+ title: "Language",
1948
+ choose: "Choose the display language for bot menus and replies.",
1949
+ current(label) {
1950
+ return `Current language: ${label}`;
1951
+ },
1952
+ switched: "Language switched.",
1953
+ expired: "The language option is no longer available. Run /language again.",
1954
+ labels: {
1955
+ en: "English",
1956
+ "zh-CN": "Simplified Chinese"
1957
+ }
1958
+ }
1959
+ };
1960
+ var ZH_CN_BOT_COPY = {
1961
+ commands: {
1962
+ start: "查看欢迎与快速开始",
1963
+ help: "查看命令说明与示例",
1964
+ status: "查看系统状态",
1965
+ new: "新建会话",
1966
+ agents: "查看并切换代理",
1967
+ sessions: "查看并切换会话",
1968
+ cancel: "取消重命名或中止运行中的请求",
1969
+ model: "查看并切换模型",
1970
+ language: "查看并切换语言"
1971
+ },
1972
+ start: { lines: [
1973
+ "# 欢迎使用 opencode-tbot",
1974
+ "",
1975
+ "通过 Telegram 直接和 OpenCode 服务对话。",
1976
+ "",
1977
+ "## 支持的输入",
1978
+ "- 文本消息",
1979
+ "- 图片 (可附带 caption)",
1980
+ "- 语音消息 (需先配置 OpenRouter 语音转写)",
1981
+ "",
1982
+ "## 快速开始",
1983
+ "1. 先运行 `/status` 确认服务状态正常。",
1984
+ "2. 运行 `/new [title]` 创建一个新会话。",
1985
+ "3. 直接发送文本、图片或语音消息。",
1986
+ "",
1987
+ "更多命令和示例请查看 `/help`。"
1988
+ ] },
1989
+ help: { lines: [
1990
+ "# 帮助",
1991
+ "",
1992
+ "使用这个机器人可以通过 Telegram 与 OpenCode 对话。",
1993
+ "",
1994
+ "## 命令",
1995
+ "- `/status` 查看服务、工作区、MCP 和 LSP 状态",
1996
+ "- `/new [title]` 创建一个新会话",
1997
+ "- `/sessions` 查看、切换或重命名会话",
1998
+ "- `/agents` 查看并切换代理",
1999
+ "- `/model` 查看并切换模型与推理级别",
2000
+ "- `/language` 切换机器人的显示语言",
2001
+ "- `/cancel` 取消会话重命名或中止当前请求",
2002
+ "",
2003
+ "## 示例",
2004
+ "- `/new bug triage`",
2005
+ "- 直接发送一条文本消息",
2006
+ "- 发送一张带 caption 的图片",
2007
+ "- 如果已配置 OpenRouter 语音转写,直接发送语音消息"
2008
+ ] },
2009
+ systemStatus: { title: "系统状态" },
2010
+ common: {
2011
+ notSelected: "未选择",
2012
+ openCodeDefault: "未选择(使用 OpenCode 默认值)",
2013
+ previousPage: "上一页",
2014
+ nextPage: "下一页",
2015
+ page(currentPage, totalPages) {
2016
+ return `第 ${currentPage}/${totalPages} 页`;
2017
+ }
2018
+ },
2019
+ status: {
2020
+ processing: "处理中...",
2021
+ alreadyProcessing: "另一个请求仍在运行。请等待其完成后再发送新的提示词。"
2022
+ },
2023
+ prompt: { emptyResponse: "OpenCode 返回了空响应。" },
2024
+ replyMetrics: {
2025
+ durationLabel: "耗时",
2026
+ tokensLabel: "令牌数",
2027
+ totalLabel: "总计",
2028
+ inputLabel: "输入",
2029
+ outputLabel: "输出",
2030
+ reasoningLabel: "推理",
2031
+ cacheReadLabel: "缓存读取",
2032
+ cacheWriteLabel: "缓存写入",
2033
+ notAvailable: "不可用"
2034
+ },
2035
+ abort: {
2036
+ noSession: "当前聊天还没有绑定活动会话。",
2037
+ notRunning: "当前会话没有正在运行的请求。",
2038
+ aborted: "已向当前会话发送中止信号。"
2039
+ },
2040
+ errors: {
2041
+ unexpected: "发生未知错误。",
2042
+ providerAuth: "Provider 认证失败。",
2043
+ requestAborted: "请求已中止。",
2044
+ structuredOutput: "结构化输出校验失败。",
2045
+ voiceNotConfigured: "未配置语音转写服务。",
2046
+ voiceDownload: "下载 Telegram 语音文件失败。",
2047
+ voiceTranscription: "语音转写失败。",
2048
+ voiceEmpty: "语音转写结果为空。",
2049
+ voiceUnsupported: "语音文件过大或不受支持。",
2050
+ imageDownload: "下载 Telegram 图片文件失败。",
2051
+ imageUnsupported: "图片文件过大或不受支持。",
2052
+ outputLength: "回复触发了模型输出长度上限。",
2053
+ contextOverflow: "会话已超过模型上下文窗口。",
2054
+ providerRequest: "Provider 请求失败。",
2055
+ notFound: "请求的资源不存在。",
2056
+ badRequest: "OpenCode 拒绝了该请求。",
2057
+ causeLabel: "原因",
2058
+ retryableLabel: "可重试",
2059
+ statusCodeLabel: "状态码"
2060
+ },
2061
+ health: {
2062
+ title: "服务健康状态",
2063
+ status(healthy) {
2064
+ return `状态: ${healthy ? "健康" : "异常"}`;
2065
+ },
2066
+ version(version) {
2067
+ return `版本: ${version}`;
2068
+ }
2069
+ },
2070
+ path: {
2071
+ title: "当前路径",
2072
+ home(path) {
2073
+ return `主目录: ${path}`;
2074
+ },
2075
+ state(path) {
2076
+ return `状态目录: ${path}`;
2077
+ },
2078
+ config(path) {
2079
+ return `配置文件: ${path}`;
2080
+ },
2081
+ worktree(path) {
2082
+ return `工作树: ${path}`;
2083
+ },
2084
+ directory(path) {
2085
+ return `当前工作目录: ${path}`;
2086
+ }
2087
+ },
2088
+ sessions: {
2089
+ none: "当前项目下没有可用会话。",
2090
+ title: "会话列表",
2091
+ actionTitle: "会话操作",
2092
+ chooseAction: "请选择对该会话执行的操作。",
2093
+ currentProject(worktree) {
2094
+ return `当前项目: ${worktree}`;
2095
+ },
2096
+ currentSession(session) {
2097
+ return `当前会话: ${session}`;
2098
+ },
2099
+ selectedSession(session) {
2100
+ return `目标会话: ${session}`;
2101
+ },
2102
+ switched: "会话已切换。",
2103
+ created: "会话已创建。",
2104
+ renamed: "会话已重命名。",
2105
+ renameCancelled: "已取消会话重命名。",
2106
+ renameEmpty: "会话名称不能为空。请重新发送名称或发送 /cancel。",
2107
+ renameExpired: "该会话已不可用。请重新运行 /sessions。",
2108
+ renamePendingInput: "当前正在等待新的会话名称。请发送纯文本或 /cancel。",
2109
+ renamePrompt(session) {
2110
+ return [
2111
+ `重命名会话: ${session}`,
2112
+ "请发送新的会话名称。",
2113
+ "发送 /cancel 取消。"
2114
+ ].join("\n");
2115
+ },
2116
+ switchAction: "切换",
2117
+ renameAction: "重命名",
2118
+ backToList: "返回列表",
2119
+ expired: "该会话已不可用。请重新运行 /sessions。"
2120
+ },
2121
+ lsp: {
2122
+ none: "当前项目未检测到 LSP。",
2123
+ title: "LSP 服务",
2124
+ currentProject(worktree) {
2125
+ return `当前项目: ${worktree}`;
2126
+ },
2127
+ connected: "已连接",
2128
+ error: "异常"
2129
+ },
2130
+ mcp: {
2131
+ none: "当前项目未配置 MCP。",
2132
+ title: "MCP 服务",
2133
+ currentProject(worktree) {
2134
+ return `当前项目: ${worktree}`;
2135
+ },
2136
+ connected: "已连接",
2137
+ disabled: "已禁用",
2138
+ needsAuth: "需要认证",
2139
+ failed(error) {
2140
+ return `失败: ${error}`;
2141
+ },
2142
+ needsClientRegistration(error) {
2143
+ return `需要客户端注册: ${error}`;
2144
+ }
2145
+ },
2146
+ agents: {
2147
+ none: "没有可用代理。",
2148
+ title: "代理列表",
2149
+ current(agent) {
2150
+ return `当前代理: ${agent}`;
2151
+ },
2152
+ switched: "代理已切换。",
2153
+ expired: "该代理已不可用。请重新运行 /agents。"
2154
+ },
2155
+ models: {
2156
+ none: "没有可用模型。",
2157
+ title: "模型列表",
2158
+ configuredOnly: "仅显示当前在 OpenCode 和已连接 provider 中实际可用的模型。",
2159
+ current(model) {
2160
+ return `当前模型: ${model}`;
2161
+ },
2162
+ switched: "模型已切换。",
2163
+ currentReasoningLevel(variant) {
2164
+ return `当前推理级别: ${variant}`;
2165
+ },
2166
+ reasoningLevel(variant) {
2167
+ return `推理级别: ${variant}`;
2168
+ },
2169
+ noReasoningLevels: "该模型没有可选的推理级别。",
2170
+ reasoningLevelsTitle: "推理级别",
2171
+ model(model) {
2172
+ return `模型: ${model}`;
2173
+ },
2174
+ modelNumber(modelIndex) {
2175
+ return `模型编号: ${modelIndex}`;
2176
+ },
2177
+ expired: "该模型已不可用。请重新运行 /model。",
2178
+ reasoningLevelExpired: "该推理级别已不可用。请重新运行 /model。",
2179
+ defaultReasoningLevel: "默认"
2180
+ },
2181
+ language: {
2182
+ title: "语言",
2183
+ choose: "选择机器人菜单和回复信息的显示语言。",
2184
+ current(label) {
2185
+ return `当前语言: ${label}`;
2186
+ },
2187
+ switched: "语言已切换。",
2188
+ expired: "该语言选项已不可用。请重新运行 /language。",
2189
+ labels: {
2190
+ en: "English",
2191
+ "zh-CN": "简体中文"
2192
+ }
2193
+ }
2194
+ };
2195
+ var BOT_COPY = EN_BOT_COPY;
2196
+ function isBotLanguage(value) {
2197
+ return SUPPORTED_BOT_LANGUAGES.includes(value);
2198
+ }
2199
+ function normalizeBotLanguage(value) {
2200
+ if (!value) return "en";
2201
+ const normalized = value.trim().toLowerCase();
2202
+ if (normalized === "zh-cn" || normalized === "zh-hans" || normalized === "zh") return "zh-CN";
2203
+ return "en";
2204
+ }
2205
+ function getBotCopy(language = "en") {
2206
+ return normalizeBotLanguage(language) === "zh-CN" ? ZH_CN_BOT_COPY : EN_BOT_COPY;
2207
+ }
2208
+ function getLanguageLabel(language, copy = BOT_COPY) {
2209
+ return copy.language.labels[language];
2210
+ }
2211
+ //#endregion
2212
+ //#region src/bot/commands/command-list.ts
2213
+ function getTelegramCommands(language = "en") {
2214
+ const copy = getBotCopy(language);
2215
+ return [
2216
+ {
2217
+ command: "start",
2218
+ description: copy.commands.start
2219
+ },
2220
+ {
2221
+ command: "help",
2222
+ description: copy.commands.help
2223
+ },
2224
+ {
2225
+ command: "status",
2226
+ description: copy.commands.status
2227
+ },
2228
+ {
2229
+ command: "new",
2230
+ description: copy.commands.new
2231
+ },
2232
+ {
2233
+ command: "agents",
2234
+ description: copy.commands.agents
2235
+ },
2236
+ {
2237
+ command: "sessions",
2238
+ description: copy.commands.sessions
2239
+ },
2240
+ {
2241
+ command: "cancel",
2242
+ description: copy.commands.cancel
2243
+ },
2244
+ {
2245
+ command: "model",
2246
+ description: copy.commands.model
2247
+ },
2248
+ {
2249
+ command: "language",
2250
+ description: copy.commands.language
2251
+ }
2252
+ ];
2253
+ }
2254
+ var TELEGRAM_COMMANDS = getTelegramCommands();
2255
+ //#endregion
2256
+ //#region src/bot/commands/sync-commands.ts
2257
+ var TELEGRAM_COMMAND_SYNC_SCOPES = [{ type: "default" }, { type: "all_private_chats" }];
2258
+ async function syncTelegramCommands(bot, logger) {
2259
+ await Promise.all(TELEGRAM_COMMAND_SYNC_SCOPES.map((scope) => bot.api.setMyCommands(TELEGRAM_COMMANDS, { scope })));
2260
+ logger.info({
2261
+ commands: TELEGRAM_COMMANDS.map((command) => command.command),
2262
+ scopes: TELEGRAM_COMMAND_SYNC_SCOPES.map((scope) => scope.type)
2263
+ }, "telegram commands synced");
2264
+ }
2265
+ async function syncTelegramCommandsForChat(api, chatId, language) {
2266
+ await api.setMyCommands(getTelegramCommands(language), { scope: {
2267
+ type: "chat",
2268
+ chat_id: chatId
2269
+ } });
2270
+ }
2271
+ //#endregion
2272
+ //#region src/bot/i18n.ts
2273
+ async function getChatLanguage(sessionRepo, chatId) {
2274
+ if (!chatId) return "en";
2275
+ return normalizeBotLanguage((await sessionRepo.getByChatId(chatId))?.language);
2276
+ }
2277
+ async function getChatCopy(sessionRepo, chatId) {
2278
+ return getBotCopy(await getChatLanguage(sessionRepo, chatId));
2279
+ }
2280
+ async function setChatLanguage(sessionRepo, chatId, language) {
2281
+ const binding = await sessionRepo.getByChatId(chatId);
2282
+ await sessionRepo.setCurrent({
2283
+ chatId,
2284
+ sessionId: binding?.sessionId ?? null,
2285
+ projectId: binding?.projectId ?? null,
2286
+ directory: binding?.directory ?? null,
2287
+ agentName: binding?.agentName ?? null,
2288
+ modelProviderId: binding?.modelProviderId ?? null,
2289
+ modelId: binding?.modelId ?? null,
2290
+ modelVariant: binding?.modelVariant ?? null,
2291
+ language,
2292
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2293
+ });
2294
+ }
2295
+ var NUMBERED_BUTTONS_PER_ROW = 5;
2296
+ function buildModelsKeyboard(models, requestedPage, copy = BOT_COPY) {
2297
+ const page = getModelsPage(models, requestedPage);
2298
+ const keyboard = buildNumberedKeyboard(page.items, page.startIndex, (_, index) => `model:pick:${page.startIndex + index + 1}`);
2299
+ appendPaginationButtons(keyboard, page.page, page.totalPages, "model:page", copy);
2300
+ return {
2301
+ keyboard,
2302
+ page
2303
+ };
2304
+ }
2305
+ function buildAgentsKeyboard(agents, requestedPage, copy = BOT_COPY) {
2306
+ const page = getAgentsPage(agents, requestedPage);
2307
+ const keyboard = buildNumberedKeyboard(page.items, page.startIndex, (_, index) => `agents:select:${page.startIndex + index + 1}`);
2308
+ appendPaginationButtons(keyboard, page.page, page.totalPages, "agents:page", copy);
2309
+ return {
2310
+ keyboard,
2311
+ page
2312
+ };
2313
+ }
2314
+ function buildSessionsKeyboard(sessions, requestedPage, copy = BOT_COPY) {
2315
+ const page = getSessionsPage(sessions, requestedPage);
2316
+ const keyboard = buildNumberedKeyboard(page.items, page.startIndex, (session) => `sessions:pick:${page.page}:${session.id}`);
2317
+ appendPaginationButtons(keyboard, page.page, page.totalPages, "sessions:page", copy);
2318
+ return {
2319
+ keyboard,
2320
+ page
2321
+ };
2322
+ }
2323
+ function buildSessionActionKeyboard(sessionId, page, copy = BOT_COPY) {
2324
+ return new InlineKeyboard().text(copy.sessions.switchAction, `sessions:switch:${page}:${sessionId}`).text(copy.sessions.renameAction, `sessions:rename:${page}:${sessionId}`).row().text(copy.sessions.backToList, `sessions:back:${page}`);
2325
+ }
2326
+ function buildModelVariantsKeyboard(variants, modelIndex) {
2327
+ return buildNumberedKeyboard(variants, 0, (_, index) => `model:variant:${modelIndex}:${index + 1}`);
2328
+ }
2329
+ function buildLanguageKeyboard(currentLanguage, copy = BOT_COPY) {
2330
+ const keyboard = new InlineKeyboard();
2331
+ SUPPORTED_BOT_LANGUAGES.forEach((language, index) => {
2332
+ const label = currentLanguage === language ? `[${getLanguageLabel(language, copy)}]` : getLanguageLabel(language, copy);
2333
+ keyboard.text(label, `language:select:${language}`);
2334
+ if (index !== SUPPORTED_BOT_LANGUAGES.length - 1) keyboard.row();
2335
+ });
2336
+ return keyboard;
2337
+ }
2338
+ function getModelsPage(models, requestedPage) {
2339
+ return getPagedItems(models, requestedPage, 10);
2340
+ }
2341
+ function getAgentsPage(agents, requestedPage) {
2342
+ return getPagedItems(agents, requestedPage, 10);
2343
+ }
2344
+ function getSessionsPage(sessions, requestedPage) {
2345
+ return getPagedItems(sessions, requestedPage, 10);
2346
+ }
2347
+ function buildNumberedKeyboard(items, startIndex, buildCallbackData) {
2348
+ const keyboard = new InlineKeyboard();
2349
+ items.forEach((item, index) => {
2350
+ const displayIndex = startIndex + index + 1;
2351
+ keyboard.text(`${displayIndex}`, buildCallbackData(item, index));
2352
+ if (index !== items.length - 1 && (index + 1) % NUMBERED_BUTTONS_PER_ROW === 0) keyboard.row();
2353
+ });
2354
+ return keyboard;
2355
+ }
2356
+ function appendPaginationButtons(keyboard, page, totalPages, prefix, copy) {
2357
+ if (totalPages <= 1) return;
2358
+ if (page > 0) keyboard.text(copy.common.previousPage, `${prefix}:${page - 1}`);
2359
+ if (page < totalPages - 1) keyboard.text(copy.common.nextPage, `${prefix}:${page + 1}`);
2360
+ }
2361
+ function getPagedItems(items, requestedPage, pageSize) {
2362
+ const totalPages = Math.max(1, Math.ceil(items.length / pageSize));
2363
+ const page = clampPage(requestedPage, totalPages);
2364
+ const startIndex = page * pageSize;
2365
+ return {
2366
+ items: items.slice(startIndex, startIndex + pageSize),
2367
+ page,
2368
+ startIndex,
2369
+ totalPages
2370
+ };
2371
+ }
2372
+ function clampPage(page, totalPages) {
2373
+ if (!Number.isInteger(page) || page < 0) return 0;
2374
+ return Math.min(page, totalPages - 1);
2375
+ }
2376
+ //#endregion
2377
+ //#region src/bot/presenters/error.presenter.ts
2378
+ function presentError(error, copy = BOT_COPY) {
2379
+ const presented = normalizeError(error, copy);
2380
+ return presented.cause ? `${presented.message}\n${copy.errors.causeLabel}: ${presented.cause}` : presented.message;
2381
+ }
2382
+ function normalizeError(error, copy) {
2383
+ if (isNamedError(error, "ProviderAuthError")) return {
2384
+ message: copy.errors.providerAuth,
2385
+ cause: extractMessage(error.data) ?? null
2386
+ };
2387
+ if (isNamedError(error, "MessageAbortedError")) return {
2388
+ message: copy.errors.requestAborted,
2389
+ cause: extractMessage(error.data) ?? null
2390
+ };
2391
+ if (isNamedError(error, "StructuredOutputError")) return {
2392
+ message: copy.errors.structuredOutput,
2393
+ cause: joinNonEmptyParts([extractMessage(error.data), extractRetries(error.data)])
2394
+ };
2395
+ if (isNamedError(error, "VoiceTranscriptionNotConfiguredError")) return {
2396
+ message: copy.errors.voiceNotConfigured,
2397
+ cause: extractMessage(error.data) ?? null
2398
+ };
2399
+ if (isNamedError(error, "TelegramFileDownloadError")) return {
2400
+ message: copy.errors.voiceDownload,
2401
+ cause: extractMessage(error.data) ?? null
2402
+ };
2403
+ if (isNamedError(error, "VoiceTranscriptionFailedError")) return {
2404
+ message: copy.errors.voiceTranscription,
2405
+ cause: extractMessage(error.data) ?? null
2406
+ };
2407
+ if (isNamedError(error, "VoiceTranscriptEmptyError")) return {
2408
+ message: copy.errors.voiceEmpty,
2409
+ cause: extractMessage(error.data) ?? null
2410
+ };
2411
+ if (isNamedError(error, "VoiceMessageUnsupportedError")) return {
2412
+ message: copy.errors.voiceUnsupported,
2413
+ cause: extractMessage(error.data) ?? null
2414
+ };
2415
+ if (isNamedError(error, "ImageFileDownloadError")) return {
2416
+ message: copy.errors.imageDownload,
2417
+ cause: extractMessage(error.data) ?? null
2418
+ };
2419
+ if (isNamedError(error, "ImageMessageUnsupportedError")) return {
2420
+ message: copy.errors.imageUnsupported,
2421
+ cause: extractMessage(error.data) ?? null
2422
+ };
2423
+ if (isNamedError(error, "MessageOutputLengthError")) return {
2424
+ message: copy.errors.outputLength,
2425
+ cause: extractMessage(error.data) ?? null
2426
+ };
2427
+ if (isNamedError(error, "ContextOverflowError")) return {
2428
+ message: copy.errors.contextOverflow,
2429
+ cause: extractMessage(error.data) ?? null
2430
+ };
2431
+ if (isNamedError(error, "APIError")) return {
2432
+ message: copy.errors.providerRequest,
2433
+ cause: joinNonEmptyParts([
2434
+ extractMessage(error.data),
2435
+ extractStatusCode(error.data, copy),
2436
+ extractRetryable(error.data, copy)
2437
+ ])
2438
+ };
2439
+ if (isNamedError(error, "NotFoundError")) return {
2440
+ message: copy.errors.notFound,
2441
+ cause: extractMessage(error.data) ?? null
2442
+ };
2443
+ if (isBadRequestError(error)) return {
2444
+ message: copy.errors.badRequest,
2445
+ cause: extractBadRequestCause(error)
2446
+ };
2447
+ if (error instanceof Error) return {
2448
+ message: error.name === "AbortError" ? copy.errors.requestAborted : copy.errors.unexpected,
2449
+ cause: error.message || null
2450
+ };
2451
+ return {
2452
+ message: copy.errors.unexpected,
2453
+ cause: extractMessage(error) ?? stringifyUnknown(error)
2454
+ };
2455
+ }
2456
+ function isBadRequestError(error) {
2457
+ return !!error && typeof error === "object" && "success" in error && error.success === false;
2458
+ }
2459
+ function isNamedError(error, name) {
2460
+ return !!error && typeof error === "object" && "name" in error && error.name === name;
2461
+ }
2462
+ function extractBadRequestCause(error) {
2463
+ const directMessage = extractMessage(error.data);
2464
+ if (directMessage) return directMessage;
2465
+ return Array.isArray(error.errors) && error.errors.length > 0 ? stringifyUnknown(error.errors[0]) : null;
2466
+ }
2467
+ function extractMessage(value) {
2468
+ if (!value) return null;
2469
+ if (typeof value === "string") return value.trim() || null;
2470
+ if (typeof value === "object" && "message" in value) {
2471
+ const message = value.message;
2472
+ return typeof message === "string" && message.trim().length > 0 ? message.trim() : null;
2473
+ }
2474
+ return null;
2475
+ }
2476
+ function extractRetries(value) {
2477
+ if (!value || typeof value !== "object" || !("retries" in value)) return null;
2478
+ const retries = value.retries;
2479
+ return typeof retries === "number" && Number.isFinite(retries) ? `retries: ${Math.round(retries)}` : null;
2480
+ }
2481
+ function extractRetryable(value, copy) {
2482
+ if (!value || typeof value !== "object" || !("isRetryable" in value)) return null;
2483
+ const isRetryable = value.isRetryable;
2484
+ return typeof isRetryable === "boolean" ? `${copy.errors.retryableLabel}: ${isRetryable ? "yes" : "no"}` : null;
2485
+ }
2486
+ function extractStatusCode(value, copy) {
2487
+ if (!value || typeof value !== "object" || !("statusCode" in value)) return null;
2488
+ const statusCode = value.statusCode;
2489
+ return typeof statusCode === "number" && Number.isFinite(statusCode) ? `${copy.errors.statusCodeLabel}: ${Math.round(statusCode)}` : null;
2490
+ }
2491
+ function joinNonEmptyParts(parts) {
2492
+ const filtered = parts.map((part) => part?.trim()).filter((part) => !!part);
2493
+ return filtered.length > 0 ? filtered.join(" | ") : null;
2494
+ }
2495
+ function stringifyUnknown(value) {
2496
+ if (value === null || value === void 0) return null;
2497
+ if (typeof value === "string") return value.trim() || null;
2498
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
2499
+ try {
2500
+ const text = JSON.stringify(value);
2501
+ return text && text !== "{}" ? text : null;
2502
+ } catch {
2503
+ return null;
2504
+ }
2505
+ }
2506
+ //#endregion
2507
+ //#region src/bot/presenters/message.presenter.ts
2508
+ var VARIANT_ORDER = [
2509
+ "minimal",
2510
+ "none",
2511
+ "low",
2512
+ "medium",
2513
+ "high",
2514
+ "xhigh",
2515
+ "max"
2516
+ ];
2517
+ function presentStatusMessage(input, copy = BOT_COPY) {
2518
+ const layout = getStatusLayoutCopy(copy);
2519
+ const sections = [
2520
+ presentStatusPlainSection(layout.serverTitle, presentStatusPlainServerLines(input, copy, layout)),
2521
+ presentStatusPlainSection(layout.workspaceTitle, presentStatusPlainWorkspaceLines(input, copy, layout)),
2522
+ presentStatusPlainSection(layout.mcpTitle, presentStatusPlainMcpLines(input, copy, layout)),
2523
+ presentStatusPlainSection(layout.lspTitle, presentStatusPlainLspLines(input, copy, layout)),
2524
+ layout.divider,
2525
+ `${layout.lastUpdatedLabel}: ${formatStatusDate(/* @__PURE__ */ new Date())}`
2526
+ ];
2527
+ return presentStatusSections(layout.pageTitle, sections);
2528
+ }
2529
+ function presentStatusMarkdownMessage(input, copy = BOT_COPY) {
2530
+ const layout = getStatusLayoutCopy(copy);
2531
+ const sections = [
2532
+ presentStatusMarkdownSection(layout.serverTitle, presentStatusMarkdownServerLines(input, copy, layout)),
2533
+ presentStatusMarkdownSection(layout.workspaceTitle, presentStatusMarkdownWorkspaceLines(input, copy, layout)),
2534
+ presentStatusMarkdownSection(layout.mcpTitle, presentStatusMarkdownMcpLines(input, copy, layout)),
2535
+ presentStatusMarkdownSection(layout.lspTitle, presentStatusMarkdownLspLines(input, copy, layout)),
2536
+ layout.divider,
2537
+ `_${layout.lastUpdatedLabel}: ${formatStatusDate(/* @__PURE__ */ new Date())}_`
2538
+ ];
2539
+ return presentStatusSections(`# ${layout.pageTitle}`, sections);
2540
+ }
2541
+ function presentStatusSections(title, sections) {
2542
+ return [
2543
+ title,
2544
+ "",
2545
+ ...sections.flatMap((section, index) => index === 0 ? [section] : ["", section])
2546
+ ].join("\n");
2547
+ }
2548
+ function presentStatusPlainSection(title, lines) {
2549
+ return [title, ...lines].join("\n");
2550
+ }
2551
+ function presentStatusMarkdownSection(title, lines) {
2552
+ return [`## ${title}`, ...lines].join("\n");
2553
+ }
2554
+ function presentStatusPlainServerLines(input, copy, layout) {
2555
+ if (input.health.status === "error") return presentStatusPlainErrorLines(input.health.error, copy, layout);
2556
+ return [presentPlainStatusBullet(layout.statusLabel, formatHealthBadge(input.health.data.healthy, layout)), presentPlainStatusBullet(layout.versionLabel, input.health.data.version)];
2557
+ }
2558
+ function presentStatusMarkdownServerLines(input, copy, layout) {
2559
+ if (input.health.status === "error") return presentStatusMarkdownErrorLines(input.health.error, copy, layout);
2560
+ return [presentMarkdownStatusBullet(layout.statusLabel, formatHealthBadge(input.health.data.healthy, layout)), presentMarkdownStatusBullet(layout.versionLabel, input.health.data.version)];
2561
+ }
2562
+ function presentStatusPlainWorkspaceLines(input, copy, layout) {
2563
+ if (input.path.status === "error") return presentStatusPlainErrorLines(input.path.error, copy, layout);
2564
+ return [presentPlainStatusBullet(layout.currentDirectoryLabel, input.path.data.directory)];
2565
+ }
2566
+ function presentStatusMarkdownWorkspaceLines(input, copy, layout) {
2567
+ if (input.path.status === "error") return presentStatusMarkdownErrorLines(input.path.error, copy, layout);
2568
+ return [presentMarkdownStatusBullet(layout.currentDirectoryLabel, input.path.data.directory, { codeValue: true })];
2569
+ }
2570
+ function presentStatusPlainLspLines(input, copy, layout) {
2571
+ return buildPlainStatusTable([
2572
+ layout.languageLabel,
2573
+ layout.statusLabel,
2574
+ layout.rootLabel
2575
+ ], buildLspStatusRows(input, copy, layout));
2576
+ }
2577
+ function presentStatusMarkdownLspLines(input, copy, layout) {
2578
+ return buildMarkdownStatusTable([
2579
+ layout.languageLabel,
2580
+ layout.statusLabel,
2581
+ layout.rootLabel
2582
+ ], buildLspStatusRows(input, copy, layout));
2583
+ }
2584
+ function presentStatusPlainMcpLines(input, copy, layout) {
2585
+ return buildPlainStatusTable([
2586
+ layout.serviceLabel,
2587
+ layout.statusLabel,
2588
+ layout.mcpNotesLabel
2589
+ ], buildMcpStatusRows(input, copy, layout));
2590
+ }
2591
+ function presentStatusMarkdownMcpLines(input, copy, layout) {
2592
+ return buildMarkdownStatusTable([
2593
+ layout.serviceLabel,
2594
+ layout.statusLabel,
2595
+ layout.mcpNotesLabel
2596
+ ], buildMcpStatusRows(input, copy, layout));
2597
+ }
2598
+ function buildLspStatusRows(input, copy, layout) {
2599
+ if (input.lsp.status === "error") return [[
2600
+ "-",
2601
+ layout.errorStatus,
2602
+ flattenStatusError(input.lsp.error, copy)
2603
+ ]];
2604
+ if (input.lsp.data.statuses.length === 0) return [[
2605
+ "-",
2606
+ layout.noneStatus,
2607
+ copy.lsp.none
2608
+ ]];
2609
+ return input.lsp.data.statuses.map((status) => [
2610
+ status.name,
2611
+ formatLspStatusBadge(status, copy),
2612
+ status.root || "-"
2613
+ ]);
2614
+ }
2615
+ function buildMcpStatusRows(input, copy, layout) {
2616
+ if (input.mcp.status === "error") return [[
2617
+ "-",
2618
+ layout.errorStatus,
2619
+ flattenStatusError(input.mcp.error, copy)
2620
+ ]];
2621
+ if (input.mcp.data.statuses.length === 0) return [[
2622
+ "-",
2623
+ layout.noneStatus,
2624
+ copy.mcp.none
2625
+ ]];
2626
+ return input.mcp.data.statuses.map(({ name, status }) => [
2627
+ name,
2628
+ formatMcpStatusBadge(status, copy, layout),
2629
+ formatMcpStatusNotes(status, layout)
2630
+ ]);
2631
+ }
2632
+ function buildPlainStatusTable(headers, rows) {
2633
+ return [
2634
+ formatPlainTableRow(headers),
2635
+ formatPlainTableSeparator(headers.length),
2636
+ ...rows.map((row) => formatPlainTableRow(row))
2637
+ ];
2638
+ }
2639
+ function buildMarkdownStatusTable(headers, rows) {
2640
+ return [
2641
+ `| ${headers.map(sanitizeTableCell).join(" | ")} |`,
2642
+ `| ${headers.map(() => "---").join(" | ")} |`,
2643
+ ...rows.map((row) => `| ${row.map(sanitizeTableCell).join(" | ")} |`)
2644
+ ];
2645
+ }
2646
+ function formatPlainTableRow(columns) {
2647
+ return `| ${columns.map(sanitizeTableCell).join(" | ")} |`;
2648
+ }
2649
+ function formatPlainTableSeparator(columnCount) {
2650
+ return `| ${Array.from({ length: columnCount }, () => "---").join(" | ")} |`;
2651
+ }
2652
+ function sanitizeTableCell(value) {
2653
+ return value.replace(/\r\n?/g, " / ").replace(/\|/g, "/").trim();
2654
+ }
2655
+ function presentStatusPlainErrorLines(error, copy, layout) {
2656
+ const detailLines = splitStatusLines(presentError(error, copy));
2657
+ return [presentPlainStatusBullet(layout.statusLabel, layout.errorStatus), ...detailLines.map((line) => presentPlainStatusBullet(layout.detailsLabel, line))];
2658
+ }
2659
+ function presentStatusMarkdownErrorLines(error, copy, layout) {
2660
+ const detailLines = splitStatusLines(presentError(error, copy));
2661
+ return [presentMarkdownStatusBullet(layout.statusLabel, layout.errorStatus), ...detailLines.map((line) => presentMarkdownStatusBullet(layout.detailsLabel, line))];
2662
+ }
2663
+ function flattenStatusError(error, copy) {
2664
+ return splitStatusLines(presentError(error, copy)).join(" / ");
2665
+ }
2666
+ function presentPlainStatusBullet(label, value) {
2667
+ return `- ${label}: ${value}`;
2668
+ }
2669
+ function presentMarkdownStatusBullet(label, value, options = {}) {
2670
+ return options.codeValue ? `- **${label}:** \`${value}\`` : `- **${label}:** ${value}`;
2671
+ }
2672
+ function splitStatusLines(text) {
2673
+ return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
2674
+ }
2675
+ function formatHealthBadge(healthy, layout) {
2676
+ return healthy ? layout.healthyStatus : layout.errorStatus;
2677
+ }
2678
+ function formatLspStatusBadge(status, copy) {
2679
+ switch (status.status) {
2680
+ case "connected": return `🟢 ${copy.lsp.connected}`;
2681
+ case "error": return `🔴 ${copy.lsp.error}`;
2682
+ }
2683
+ return status.status;
2684
+ }
2685
+ function formatMcpStatusBadge(status, copy, layout) {
2686
+ switch (status.status) {
2687
+ case "connected": return `🟢 ${copy.mcp.connected}`;
2688
+ case "disabled": return `⚪ ${copy.mcp.disabled}`;
2689
+ case "needs_auth": return `🟡 ${copy.mcp.needsAuth}`;
2690
+ case "failed": return layout.mcpFailedStatus;
2691
+ case "needs_client_registration": return layout.mcpRegistrationRequiredStatus;
2692
+ }
2693
+ return status;
2694
+ }
2695
+ function formatMcpStatusNotes(status, layout) {
2696
+ switch (status.status) {
2697
+ case "connected": return layout.okLabel;
2698
+ case "disabled": return "-";
2699
+ case "needs_auth": return "-";
2700
+ case "failed": return status.error;
2701
+ case "needs_client_registration": return status.error;
2702
+ }
2703
+ return status;
2704
+ }
2705
+ function formatStatusDate(value) {
2706
+ return value.toISOString().slice(0, 10);
2707
+ }
2708
+ function getStatusLayoutCopy(copy) {
2709
+ if (copy.systemStatus.title === BOT_COPY.systemStatus.title) return {
2710
+ currentDirectoryLabel: "Current Directory",
2711
+ detailsLabel: "Details",
2712
+ divider: "────────────────",
2713
+ errorStatus: "🔴 Unhealthy",
2714
+ healthyStatus: "🟢 Healthy",
2715
+ languageLabel: "Language",
2716
+ lastUpdatedLabel: "Last updated",
2717
+ lspTitle: "🧠 LSP (Language Server)",
2718
+ mcpNotesLabel: "Notes",
2719
+ mcpRegistrationRequiredStatus: "🟡 Registration Required",
2720
+ mcpTitle: "🔌 MCP (Model Context Protocol)",
2721
+ mcpFailedStatus: "🔴 Failed",
2722
+ noneStatus: "⚪ None",
2723
+ okLabel: "OK",
2724
+ pageTitle: "📊 Service Status",
2725
+ rootLabel: "Root",
2726
+ serverTitle: "🖥️ Server",
2727
+ serviceLabel: "Service",
2728
+ statusLabel: "Status",
2729
+ versionLabel: "Version",
2730
+ workspaceTitle: "📁 Workspace"
2731
+ };
2732
+ return {
2733
+ currentDirectoryLabel: "当前目录",
2734
+ detailsLabel: "详情",
2735
+ divider: "────────────────",
2736
+ errorStatus: "🔴 异常",
2737
+ healthyStatus: "🟢 健康",
2738
+ languageLabel: "语言",
2739
+ lastUpdatedLabel: "最后更新",
2740
+ lspTitle: "🧠 LSP (Language Server)",
2741
+ mcpNotesLabel: "说明",
2742
+ mcpRegistrationRequiredStatus: "🟡 需要注册",
2743
+ mcpTitle: "🔌 MCP (Model Context Protocol)",
2744
+ mcpFailedStatus: "🔴 失败",
2745
+ noneStatus: "⚪ 无",
2746
+ okLabel: "正常",
2747
+ pageTitle: "📊 服务状态",
2748
+ rootLabel: "根目录",
2749
+ serverTitle: "🖥️ 服务端",
2750
+ serviceLabel: "服务",
2751
+ statusLabel: "状态",
2752
+ versionLabel: "版本",
2753
+ workspaceTitle: "📁 工作区"
2754
+ };
2755
+ }
2756
+ function presentSessionsMessage(input, copy = BOT_COPY) {
2757
+ if (input.sessions.length === 0) return copy.sessions.none;
2758
+ const page = getSessionsPage(input.sessions, input.page);
2759
+ const currentSession = input.currentSessionId ? input.sessions.find((session) => session.id === input.currentSessionId) ?? null : null;
2760
+ return [
2761
+ copy.sessions.title,
2762
+ copy.sessions.currentProject(input.currentDirectory),
2763
+ copy.sessions.currentSession(currentSession ? formatSessionLabel(currentSession) : copy.common.notSelected),
2764
+ copy.common.page(page.page + 1, page.totalPages),
2765
+ "",
2766
+ ...page.items.map((session, index) => `${page.startIndex + index + 1}. ${formatSessionLabel(session)}`)
2767
+ ].join("\n");
2768
+ }
2769
+ function presentSessionSwitchMessage(session, copy = BOT_COPY) {
2770
+ return [copy.sessions.switched, copy.sessions.currentSession(formatSessionLabel(session))].join("\n");
2771
+ }
2772
+ function presentSessionCreatedMessage(session, copy = BOT_COPY) {
2773
+ return [copy.sessions.created, copy.sessions.currentSession(formatSessionLabel(session))].join("\n");
2774
+ }
2775
+ function presentSessionActionsMessage(input, copy = BOT_COPY) {
2776
+ return [
2777
+ copy.sessions.actionTitle,
2778
+ copy.sessions.currentProject(input.currentDirectory),
2779
+ copy.sessions.selectedSession(formatSessionLabel(input.session)),
2780
+ "",
2781
+ copy.sessions.chooseAction
2782
+ ].join("\n");
2783
+ }
2784
+ function presentSessionRenamePromptMessage(session, copy = BOT_COPY) {
2785
+ return copy.sessions.renamePrompt(formatSessionLabel(session));
2786
+ }
2787
+ function presentSessionRenamedMessage(session, copy = BOT_COPY) {
2788
+ return [copy.sessions.renamed, copy.sessions.currentSession(formatSessionLabel(session))].join("\n");
2789
+ }
2790
+ function presentAgentsMessage(input, copy = BOT_COPY) {
2791
+ if (input.agents.length === 0) return copy.agents.none;
2792
+ const page = getAgentsPage(input.agents, input.page);
2793
+ const currentAgent = input.currentAgentName ? input.agents.find((agent) => agent.name === input.currentAgentName) ?? null : null;
2794
+ return [
2795
+ copy.agents.title,
2796
+ copy.agents.current(currentAgent ? formatAgentLabel(currentAgent) : copy.common.openCodeDefault),
2797
+ copy.common.page(page.page + 1, page.totalPages),
2798
+ "",
2799
+ ...page.items.map((agent, index) => `${page.startIndex + index + 1}. ${formatAgentLabel(agent)}`)
2800
+ ].join("\n");
2801
+ }
2802
+ function presentAgentSwitchMessage(agent, copy = BOT_COPY) {
2803
+ return [copy.agents.switched, copy.agents.current(formatAgentLabel(agent))].join("\n");
2804
+ }
2805
+ function presentModelsMessage(input, copy = BOT_COPY) {
2806
+ if (input.models.length === 0) return copy.models.none;
2807
+ const page = getModelsPage(input.models, input.page);
2808
+ const currentModel = input.currentModelId && input.currentModelProviderId ? input.models.find((model) => model.id === input.currentModelId && model.providerID === input.currentModelProviderId) ?? null : null;
2809
+ const currentModelLine = currentModel ? formatModelSelection(currentModel, input.currentModelVariant, copy) : copy.common.openCodeDefault;
2810
+ return [
2811
+ copy.models.title,
2812
+ copy.models.configuredOnly,
2813
+ copy.models.current(currentModelLine),
2814
+ copy.common.page(page.page + 1, page.totalPages),
2815
+ "",
2816
+ ...page.items.map((model, index) => formatModelListLine(model, page.startIndex + index + 1))
2817
+ ].join("\n");
2818
+ }
2819
+ function presentModelVariantsMessage(model, modelIndex, copy = BOT_COPY) {
2820
+ const variants = getModelVariants(model);
2821
+ if (variants.length === 0) return [copy.models.noReasoningLevels, copy.models.model(formatModelLabel(model))].join("\n");
2822
+ return [
2823
+ copy.models.reasoningLevelsTitle,
2824
+ copy.models.model(formatModelLabel(model)),
2825
+ copy.models.modelNumber(modelIndex),
2826
+ "",
2827
+ ...variants.map((variant, index) => `${index + 1}. ${variant}`)
2828
+ ].join("\n");
2829
+ }
2830
+ function presentModelSwitchMessage(model, variant, copy = BOT_COPY) {
2831
+ return [
2832
+ copy.models.switched,
2833
+ copy.models.current(formatModelLabel(model)),
2834
+ copy.models.currentReasoningLevel(variant ?? copy.models.defaultReasoningLevel)
2835
+ ].join("\n");
2836
+ }
2837
+ function presentLanguageMessage(currentLanguage, copy = BOT_COPY) {
2838
+ return [
2839
+ copy.language.title,
2840
+ copy.language.current(getLanguageLabel(currentLanguage, copy)),
2841
+ "",
2842
+ copy.language.choose
2843
+ ].join("\n");
2844
+ }
2845
+ function presentLanguageSwitchMessage(currentLanguage, copy = BOT_COPY) {
2846
+ return [copy.language.switched, copy.language.current(getLanguageLabel(currentLanguage, copy))].join("\n");
2847
+ }
2848
+ function getModelVariants(model) {
2849
+ return Object.keys(model.variants).sort((left, right) => compareVariantNames(left, right));
2850
+ }
2851
+ function compareVariantNames(left, right) {
2852
+ const leftIndex = VARIANT_ORDER.indexOf(left);
2853
+ const rightIndex = VARIANT_ORDER.indexOf(right);
2854
+ if (leftIndex === -1 && rightIndex === -1) return left.localeCompare(right);
2855
+ if (leftIndex === -1) return 1;
2856
+ if (rightIndex === -1) return -1;
2857
+ return leftIndex - rightIndex;
2858
+ }
2859
+ function formatAgentLabel(agent) {
2860
+ return `${agent.name} [${agent.mode}]`;
2861
+ }
2862
+ function formatModelListLine(model, displayIndex) {
2863
+ return `${displayIndex}. ${formatModelLabel(model)}`;
2864
+ }
2865
+ function formatModelSelection(model, variant, copy) {
2866
+ return `${formatModelLabel(model)} | ${copy.models.reasoningLevel(variant ?? copy.models.defaultReasoningLevel)}`;
2867
+ }
2868
+ function formatModelLabel(model) {
2869
+ return `${model.providerName} / ${model.name}`;
2870
+ }
2871
+ function formatSessionLabel(session) {
2872
+ const title = session.title.trim() || session.slug || session.id;
2873
+ return title === session.slug ? title : `${title} [${session.slug}]`;
2874
+ }
2875
+ //#endregion
2876
+ //#region src/bot/commands/agents.ts
2877
+ async function handleAgentsCommand(ctx, dependencies) {
2878
+ const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
2879
+ try {
2880
+ const result = await dependencies.listAgentsUseCase.execute({ chatId: ctx.chat.id });
2881
+ if (result.agents.length === 0) {
2882
+ await ctx.reply(copy.agents.none);
2883
+ return;
2884
+ }
2885
+ const { keyboard, page } = buildAgentsKeyboard(result.agents, 0, copy);
2886
+ await ctx.reply(presentAgentsMessage({
2887
+ agents: result.agents,
2888
+ currentAgentName: result.currentAgentName,
2889
+ page: page.page
2890
+ }, copy), { reply_markup: keyboard });
2891
+ } catch (error) {
2892
+ dependencies.logger.error({ error }, "failed to list agents");
2893
+ await ctx.reply(presentError(error, copy));
2894
+ }
2895
+ }
2896
+ function registerAgentsCommand(bot, dependencies) {
2897
+ bot.command(["agents", "agent"], async (ctx) => {
2898
+ await handleAgentsCommand(ctx, dependencies);
2899
+ });
2900
+ }
2901
+ //#endregion
2902
+ //#region src/bot/sessions-menu.ts
2903
+ async function buildSessionsListView(chatId, requestedPage, dependencies) {
2904
+ const copy = await getChatCopy(dependencies.sessionRepo, chatId);
2905
+ const result = await dependencies.listSessionsUseCase.execute({ chatId });
2906
+ if (result.sessions.length === 0) return {
2907
+ copy,
2908
+ currentDirectory: result.currentDirectory,
2909
+ currentSessionId: result.currentSessionId,
2910
+ page: 0,
2911
+ sessions: [],
2912
+ text: copy.sessions.none
2913
+ };
2914
+ const { keyboard, page } = buildSessionsKeyboard(result.sessions, requestedPage, copy);
2915
+ return {
2916
+ copy,
2917
+ currentDirectory: result.currentDirectory,
2918
+ currentSessionId: result.currentSessionId,
2919
+ keyboard,
2920
+ page: page.page,
2921
+ sessions: result.sessions,
2922
+ text: presentSessionsMessage({
2923
+ currentDirectory: result.currentDirectory,
2924
+ currentSessionId: result.currentSessionId,
2925
+ page: page.page,
2926
+ sessions: result.sessions
2927
+ }, copy)
2928
+ };
2929
+ }
2930
+ async function buildSessionActionView(chatId, requestedPage, sessionId, dependencies) {
2931
+ const listView = await buildSessionsListView(chatId, requestedPage, dependencies);
2932
+ const session = listView.sessions.find((item) => item.id === sessionId);
2933
+ if (!session) return {
2934
+ copy: listView.copy,
2935
+ found: false
2936
+ };
2937
+ return {
2938
+ copy: listView.copy,
2939
+ found: true,
2940
+ keyboard: buildSessionActionKeyboard(session.id, listView.page, listView.copy),
2941
+ page: listView.page,
2942
+ session,
2943
+ text: presentSessionActionsMessage({
2944
+ currentDirectory: listView.currentDirectory,
2945
+ session
2946
+ }, listView.copy)
2947
+ };
2948
+ }
2949
+ async function restoreSessionsListMessage(api, chatId, messageId, requestedPage, dependencies) {
2950
+ const listView = await buildSessionsListView(chatId, requestedPage, dependencies);
2951
+ if (listView.keyboard) await api.editMessageText(chatId, messageId, listView.text, { reply_markup: listView.keyboard });
2952
+ else await api.editMessageText(chatId, messageId, listView.text);
2953
+ return listView;
2954
+ }
2955
+ //#endregion
2956
+ //#region src/bot/session-rename.ts
2957
+ async function getPendingSessionRenameAction(dependencies, chatId) {
2958
+ const action = await dependencies.pendingActionRepo.getByChatId(chatId);
2959
+ return isSessionRenamePendingAction(action) ? action : null;
2960
+ }
2961
+ async function replyIfSessionRenamePending(ctx, dependencies) {
2962
+ if (!await getPendingSessionRenameAction(dependencies, ctx.chat.id)) return false;
2963
+ const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
2964
+ await ctx.reply(copy.sessions.renamePendingInput);
2965
+ return true;
2966
+ }
2967
+ async function handlePendingSessionRenameText(ctx, dependencies) {
2968
+ const pendingAction = await getPendingSessionRenameAction(dependencies, ctx.chat.id);
2969
+ if (!pendingAction) return false;
2970
+ const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
2971
+ const title = ctx.message.text?.trim() ?? "";
2972
+ if (title.startsWith("/")) {
2973
+ await ctx.reply(copy.sessions.renamePendingInput);
2974
+ return true;
2975
+ }
2976
+ if (!title) {
2977
+ await ctx.reply(copy.sessions.renameEmpty);
2978
+ return true;
2979
+ }
2980
+ try {
2981
+ const result = await dependencies.renameSessionUseCase.execute({
2982
+ chatId: ctx.chat.id,
2983
+ sessionId: pendingAction.sessionId,
2984
+ title
2985
+ });
2986
+ await dependencies.pendingActionRepo.clear(ctx.chat.id);
2987
+ await bestEffortRestoreSessionsList(ctx.api, pendingAction, dependencies);
2988
+ if (!result.found) {
2989
+ await ctx.reply(copy.sessions.renameExpired);
2990
+ return true;
2991
+ }
2992
+ await ctx.reply(presentSessionRenamedMessage(result.session, copy));
2993
+ } catch (error) {
2994
+ dependencies.logger.error({ error }, "failed to rename session");
2995
+ await ctx.reply(presentError(error, copy));
2996
+ }
2997
+ return true;
2998
+ }
2999
+ async function cancelPendingSessionRename(ctx, dependencies) {
3000
+ const pendingAction = await getPendingSessionRenameAction(dependencies, ctx.chat.id);
3001
+ if (!pendingAction) return false;
3002
+ const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
3003
+ await dependencies.pendingActionRepo.clear(ctx.chat.id);
3004
+ await bestEffortRestoreSessionsList(ctx.api, pendingAction, dependencies);
3005
+ await ctx.reply(copy.sessions.renameCancelled);
3006
+ return true;
3007
+ }
3008
+ async function bestEffortRestoreSessionsList(api, pendingAction, dependencies) {
3009
+ try {
3010
+ await restoreSessionsListMessage(api, pendingAction.chatId, pendingAction.menuMessageId, pendingAction.returnPage, dependencies);
3011
+ } catch (error) {
3012
+ dependencies.logger.warn?.({ error }, "failed to restore sessions list message");
3013
+ }
3014
+ }
3015
+ function isSessionRenamePendingAction(action) {
3016
+ return action?.kind === "session_rename";
3017
+ }
3018
+ //#endregion
3019
+ //#region src/bot/commands/cancel.ts
3020
+ async function handleCancelCommand(ctx, dependencies) {
3021
+ const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
3022
+ try {
3023
+ if (await cancelPendingSessionRename(ctx, dependencies)) return;
3024
+ const result = await dependencies.abortPromptUseCase.execute({ chatId: ctx.chat.id });
3025
+ if (result.status === "no_session") {
3026
+ await ctx.reply(copy.abort.noSession);
3027
+ return;
3028
+ }
3029
+ if (result.status === "not_running") {
3030
+ await ctx.reply(copy.abort.notRunning);
3031
+ return;
3032
+ }
3033
+ await ctx.reply(copy.abort.aborted);
3034
+ } catch (error) {
3035
+ dependencies.logger.error({ error }, "failed to cancel current action");
3036
+ await ctx.reply(presentError(error, copy));
3037
+ }
3038
+ }
3039
+ function registerCancelCommand(bot, dependencies) {
3040
+ bot.command("cancel", async (ctx) => {
3041
+ await handleCancelCommand(ctx, dependencies);
3042
+ });
3043
+ }
3044
+ //#endregion
3045
+ //#region src/services/telegram/telegram-format.ts
3046
+ var MAX_TELEGRAM_MESSAGE_LENGTH = 4096;
3047
+ var TRUNCATED_SUFFIX = "...";
3048
+ var MARKDOWN_SPECIAL_CHARACTERS = /([_*\[\]()~`>#+\-=|{}.!\\])/g;
3049
+ function buildTelegramPromptReply(result, copy = BOT_COPY) {
3050
+ const renderedMarkdown = result.bodyMd ? renderMarkdownToTelegramMarkdownV2(result.bodyMd) : null;
3051
+ const footerPlain = formatPlainMetricsFooter(result.metrics, copy);
3052
+ const fallback = { text: joinBodyAndFooter(truncatePlainBody(normalizePlainBody(result, renderedMarkdown !== null, copy), footerPlain), footerPlain) };
3053
+ if (!renderedMarkdown) return {
3054
+ preferred: fallback,
3055
+ fallback
3056
+ };
3057
+ const markdownText = joinBodyAndFooter(renderedMarkdown, formatMarkdownMetricsFooter(result.metrics, copy));
3058
+ if (markdownText.length > MAX_TELEGRAM_MESSAGE_LENGTH) return {
3059
+ preferred: fallback,
3060
+ fallback
3061
+ };
3062
+ return {
3063
+ preferred: {
3064
+ text: markdownText,
3065
+ options: { parse_mode: "MarkdownV2" }
3066
+ },
3067
+ fallback
3068
+ };
3069
+ }
3070
+ function buildTelegramStaticReply(markdown) {
3071
+ const renderedMarkdown = renderMarkdownToTelegramMarkdownV2(markdown);
3072
+ const fallback = { text: truncateStaticText(stripMarkdownToPlainText(markdown) || markdown.trim()) };
3073
+ if (!renderedMarkdown || renderedMarkdown.length > MAX_TELEGRAM_MESSAGE_LENGTH) return {
3074
+ preferred: fallback,
3075
+ fallback
3076
+ };
3077
+ return {
3078
+ preferred: {
3079
+ text: renderedMarkdown,
3080
+ options: { parse_mode: "MarkdownV2" }
3081
+ },
3082
+ fallback
3083
+ };
3084
+ }
3085
+ function renderMarkdownToTelegramMarkdownV2(markdown) {
3086
+ const normalizedMarkdown = preprocessMarkdownForTelegram(markdown).trim();
3087
+ if (!normalizedMarkdown) return null;
3088
+ const lines = normalizedMarkdown.split("\n");
3089
+ const rendered = [];
3090
+ let inCodeBlock = false;
3091
+ let codeBlockLanguage = "";
3092
+ for (let index = 0; index < lines.length; index += 1) {
3093
+ const line = lines[index];
3094
+ const fenceMatch = line.match(/^```([A-Za-z0-9_-]+)?\s*$/);
3095
+ if (fenceMatch) {
3096
+ if (inCodeBlock) {
3097
+ rendered.push("```");
3098
+ inCodeBlock = false;
3099
+ codeBlockLanguage = "";
3100
+ } else {
3101
+ codeBlockLanguage = fenceMatch[1] ?? "";
3102
+ rendered.push(`\`\`\`${codeBlockLanguage}`);
3103
+ inCodeBlock = true;
3104
+ }
3105
+ continue;
3106
+ }
3107
+ if (inCodeBlock) {
3108
+ rendered.push(escapeCodeContent(line));
3109
+ continue;
3110
+ }
3111
+ if (line.includes("![")) return null;
3112
+ const tableBlock = consumeMarkdownTable(lines, index);
3113
+ if (tableBlock) {
3114
+ rendered.push(renderTableAsTelegramCodeBlock(tableBlock.rows));
3115
+ index = tableBlock.nextIndex - 1;
3116
+ continue;
3117
+ }
3118
+ if (/<[/A-Za-z][^>]*>/.test(line)) return null;
3119
+ if (!line.trim()) {
3120
+ rendered.push("");
3121
+ continue;
3122
+ }
3123
+ const headingMatch = line.match(/^\s{0,3}(#{1,6})\s+(.+)$/);
3124
+ if (headingMatch) {
3125
+ rendered.push(`*${escapeMarkdownText(headingMatch[2].trim())}*`);
3126
+ continue;
3127
+ }
3128
+ const blockquoteMatch = line.match(/^\s*>\s?(.*)$/);
3129
+ if (blockquoteMatch) {
3130
+ const quote = renderInlineMarkdown(blockquoteMatch[1]);
3131
+ if (quote === null) return null;
3132
+ rendered.push(`>${quote ? ` ${quote}` : ""}`);
3133
+ continue;
3134
+ }
3135
+ const unorderedListMatch = line.match(/^(\s*)[-+*]\s+(.+)$/);
3136
+ if (unorderedListMatch) {
3137
+ const item = renderInlineMarkdown(unorderedListMatch[2]);
3138
+ if (item === null) return null;
3139
+ rendered.push(`${unorderedListMatch[1]}\\- ${item}`);
3140
+ continue;
3141
+ }
3142
+ const orderedListMatch = line.match(/^(\s*)(\d+)[.)]\s+(.+)$/);
3143
+ if (orderedListMatch) {
3144
+ const item = renderInlineMarkdown(orderedListMatch[3]);
3145
+ if (item === null) return null;
3146
+ rendered.push(`${orderedListMatch[1]}${orderedListMatch[2]}\\. ${item}`);
3147
+ continue;
3148
+ }
3149
+ const paragraph = renderInlineMarkdown(line);
3150
+ if (paragraph === null) return null;
3151
+ rendered.push(paragraph);
3152
+ }
3153
+ if (inCodeBlock) return null;
3154
+ return rendered.join("\n");
3155
+ }
3156
+ function stripMarkdownToPlainText(markdown) {
3157
+ const lines = preprocessMarkdownForTelegram(markdown).split("\n");
3158
+ const rendered = [];
3159
+ for (let index = 0; index < lines.length; index += 1) {
3160
+ const tableBlock = consumeMarkdownTable(lines, index);
3161
+ if (tableBlock) {
3162
+ rendered.push(renderTableAsPlainText(tableBlock.rows));
3163
+ index = tableBlock.nextIndex - 1;
3164
+ continue;
3165
+ }
3166
+ rendered.push(lines[index]);
3167
+ }
3168
+ return rendered.join("\n").replace(/```[A-Za-z0-9_-]*\n?/g, "").replace(/```/g, "").replace(/^#{1,6}\s+/gm, "").replace(/^\s*>\s?/gm, "").replace(/^(\s*)[-+*]\s+/gm, "$1- ").replace(/^(\s*)(\d+)[.)]\s+/gm, "$1$2. ").replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1 ($2)").replace(/(\*\*|__)(.*?)\1/g, "$2").replace(/(\*|_)(.*?)\1/g, "$2").replace(/`([^`]+)`/g, "$1").trim();
3169
+ }
3170
+ function normalizePlainBody(result, preferStructured, copy) {
3171
+ const fromStructured = result.bodyMd ? stripMarkdownToPlainText(result.bodyMd) : "";
3172
+ const fromFallback = result.fallbackText.trim();
3173
+ return (preferStructured ? fromStructured || fromFallback : fromFallback || fromStructured).trim() || result.fallbackText || copy.prompt.emptyResponse;
3174
+ }
3175
+ function truncatePlainBody(body, footer) {
3176
+ const reservedLength = footer.length + 2;
3177
+ const maxBodyLength = Math.max(0, MAX_TELEGRAM_MESSAGE_LENGTH - reservedLength);
3178
+ if (body.length <= maxBodyLength) return body;
3179
+ return `${body.slice(0, Math.max(0, maxBodyLength - 3))}${TRUNCATED_SUFFIX}`;
3180
+ }
3181
+ function truncateStaticText(text) {
3182
+ if (text.length <= MAX_TELEGRAM_MESSAGE_LENGTH) return text;
3183
+ return `${text.slice(0, Math.max(0, MAX_TELEGRAM_MESSAGE_LENGTH - 3))}${TRUNCATED_SUFFIX}`;
3184
+ }
3185
+ function joinBodyAndFooter(body, footer) {
3186
+ return `${body}\n\n${footer}`;
3187
+ }
3188
+ function preprocessMarkdownForTelegram(markdown) {
3189
+ const lines = markdown.replace(/\r\n?/g, "\n").split("\n");
3190
+ const processed = [];
3191
+ let activeFence = null;
3192
+ for (const line of lines) {
3193
+ const fenceMatch = line.match(/^```([A-Za-z0-9_-]+)?\s*$/);
3194
+ if (fenceMatch) {
3195
+ const language = (fenceMatch[1] ?? "").toLowerCase();
3196
+ if (activeFence === "markdown") {
3197
+ activeFence = null;
3198
+ continue;
3199
+ }
3200
+ if (activeFence === "plain") {
3201
+ processed.push(line);
3202
+ activeFence = null;
3203
+ continue;
3204
+ }
3205
+ if (language === "md" || language === "markdown") {
3206
+ activeFence = "markdown";
3207
+ continue;
3208
+ }
3209
+ activeFence = "plain";
3210
+ processed.push(line);
3211
+ continue;
3212
+ }
3213
+ processed.push(line);
3214
+ }
3215
+ return processed.join("\n");
3216
+ }
3217
+ function formatPlainMetricsFooter(metrics, copy) {
3218
+ return `${copy.replyMetrics.durationLabel}: ${formatDuration(metrics.durationMs, copy)} | ${copy.replyMetrics.tokensLabel}: ${formatMetricValue(metrics.tokens.total, copy)}`;
3219
+ }
3220
+ function formatMarkdownMetricsFooter(metrics, copy) {
3221
+ return escapeMarkdownText(formatPlainMetricsFooter(metrics, copy));
3222
+ }
3223
+ function formatDuration(durationMs, copy) {
3224
+ if (durationMs === null || !Number.isFinite(durationMs)) return copy.replyMetrics.notAvailable;
3225
+ if (durationMs < 1e3) return `${Math.round(durationMs)} ms`;
3226
+ return `${(durationMs / 1e3).toFixed(1)} s`;
3227
+ }
3228
+ function formatMetricValue(value, copy) {
3229
+ if (value === null || !Number.isFinite(value)) return copy.replyMetrics.notAvailable;
3230
+ return `${Math.round(value)}`;
3231
+ }
3232
+ function consumeMarkdownTable(lines, startIndex) {
3233
+ if (startIndex + 1 >= lines.length) return null;
3234
+ const headerCells = parseMarkdownTableRow(lines[startIndex]);
3235
+ const separatorCells = parseMarkdownTableSeparator(lines[startIndex + 1]);
3236
+ if (!headerCells || !separatorCells || headerCells.length !== separatorCells.length) return null;
3237
+ const rows = [headerCells];
3238
+ let index = startIndex + 2;
3239
+ while (index < lines.length) {
3240
+ const rowCells = parseMarkdownTableRow(lines[index]);
3241
+ if (!rowCells || rowCells.length !== headerCells.length) break;
3242
+ rows.push(rowCells);
3243
+ index += 1;
3244
+ }
3245
+ return {
3246
+ rows,
3247
+ nextIndex: index
3248
+ };
3249
+ }
3250
+ function parseMarkdownTableRow(line) {
3251
+ const trimmed = line.trim();
3252
+ if (!trimmed.includes("|")) return null;
3253
+ const cells = splitMarkdownTableCells(trimmed).map((cell) => normalizeTableCell(cell));
3254
+ return cells.length >= 2 ? cells : null;
3255
+ }
3256
+ function parseMarkdownTableSeparator(line) {
3257
+ const cells = splitMarkdownTableCells(line.trim());
3258
+ if (cells.length < 2) return null;
3259
+ return cells.every((cell) => /^:?-{3,}:?$/.test(cell.trim())) ? cells : null;
3260
+ }
3261
+ function splitMarkdownTableCells(line) {
3262
+ const content = line.replace(/^\|/, "").replace(/\|$/, "");
3263
+ const cells = [];
3264
+ let current = "";
3265
+ let escaped = false;
3266
+ for (const char of content) {
3267
+ if (escaped) {
3268
+ current += char;
3269
+ escaped = false;
3270
+ continue;
3271
+ }
3272
+ if (char === "\\") {
3273
+ escaped = true;
3274
+ current += char;
3275
+ continue;
3276
+ }
3277
+ if (char === "|") {
3278
+ cells.push(current);
3279
+ current = "";
3280
+ continue;
3281
+ }
3282
+ current += char;
3283
+ }
3284
+ cells.push(current);
3285
+ return cells;
3286
+ }
3287
+ function normalizeTableCell(cell) {
3288
+ return cell.trim().replace(/\\\|/g, "|").replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1 ($2)").replace(/(\*\*|__)(.*?)\1/g, "$2").replace(/(\*|_)(.*?)\1/g, "$2").replace(/`([^`]+)`/g, "$1");
3289
+ }
3290
+ function renderTableAsTelegramCodeBlock(rows) {
3291
+ return [
3292
+ "```",
3293
+ ...buildAlignedTableLines(rows).map((line) => escapeCodeContent(line)),
3294
+ "```"
3295
+ ].join("\n");
3296
+ }
3297
+ function renderTableAsPlainText(rows) {
3298
+ return buildAlignedTableLines(rows).join("\n");
3299
+ }
3300
+ function buildAlignedTableLines(rows) {
3301
+ const columnWidths = calculateTableColumnWidths(rows);
3302
+ return [
3303
+ formatTableRow(rows[0], columnWidths),
3304
+ columnWidths.map((width) => "-".repeat(Math.max(3, width))).join("-+-"),
3305
+ ...rows.slice(1).map((row) => formatTableRow(row, columnWidths))
3306
+ ];
3307
+ }
3308
+ function calculateTableColumnWidths(rows) {
3309
+ return rows[0].map((_, columnIndex) => rows.reduce((maxWidth, row) => Math.max(maxWidth, getDisplayWidth(row[columnIndex] ?? "")), 0));
3310
+ }
3311
+ function formatTableRow(row, columnWidths) {
3312
+ return row.map((cell, index) => padDisplayWidth(cell, columnWidths[index] ?? 0)).join(" | ");
3313
+ }
3314
+ function padDisplayWidth(value, targetWidth) {
3315
+ const padding = Math.max(0, targetWidth - getDisplayWidth(value));
3316
+ return `${value}${" ".repeat(padding)}`;
3317
+ }
3318
+ function getDisplayWidth(value) {
3319
+ let width = 0;
3320
+ for (const char of value) width += isWideCharacter(char.codePointAt(0) ?? 0) ? 2 : 1;
3321
+ return width;
3322
+ }
3323
+ function isWideCharacter(codePoint) {
3324
+ return codePoint >= 4352 && (codePoint <= 4447 || codePoint === 9001 || codePoint === 9002 || codePoint >= 11904 && codePoint <= 42191 && codePoint !== 12351 || codePoint >= 44032 && codePoint <= 55203 || codePoint >= 63744 && codePoint <= 64255 || codePoint >= 65040 && codePoint <= 65049 || codePoint >= 65072 && codePoint <= 65135 || codePoint >= 65280 && codePoint <= 65376 || codePoint >= 65504 && codePoint <= 65510);
3325
+ }
3326
+ function renderInlineMarkdown(input) {
3327
+ let output = "";
3328
+ let cursor = 0;
3329
+ while (cursor < input.length) {
3330
+ if (input.startsWith("![", cursor)) return null;
3331
+ if (input.startsWith("**", cursor) || input.startsWith("__", cursor)) {
3332
+ const delimiter = input.slice(cursor, cursor + 2);
3333
+ const closingIndex = input.indexOf(delimiter, cursor + 2);
3334
+ if (closingIndex > cursor + 2) {
3335
+ output += `*${escapeMarkdownText(input.slice(cursor + 2, closingIndex))}*`;
3336
+ cursor = closingIndex + 2;
3337
+ continue;
3338
+ }
3339
+ }
3340
+ if (input[cursor] === "*" || input[cursor] === "_") {
3341
+ const delimiter = input[cursor];
3342
+ const closingIndex = input.indexOf(delimiter, cursor + 1);
3343
+ if (closingIndex > cursor + 1) {
3344
+ output += `_${escapeMarkdownText(input.slice(cursor + 1, closingIndex))}_`;
3345
+ cursor = closingIndex + 1;
3346
+ continue;
3347
+ }
3348
+ }
3349
+ if (input[cursor] === "`") {
3350
+ const closingIndex = input.indexOf("`", cursor + 1);
3351
+ if (closingIndex === -1) return null;
3352
+ output += `\`${escapeCodeContent(input.slice(cursor + 1, closingIndex))}\``;
3353
+ cursor = closingIndex + 1;
3354
+ continue;
3355
+ }
3356
+ if (input[cursor] === "[") {
3357
+ const linkMatch = input.slice(cursor).match(/^\[([^\]]+)\]\(([^)\s]+)\)/);
3358
+ if (!linkMatch) return null;
3359
+ output += `[${escapeMarkdownText(linkMatch[1])}](${escapeLinkDestination(linkMatch[2])})`;
3360
+ cursor += linkMatch[0].length;
3361
+ continue;
3362
+ }
3363
+ output += escapeMarkdownText(input[cursor]);
3364
+ cursor += 1;
3365
+ }
3366
+ return output;
3367
+ }
3368
+ function escapeMarkdownText(text) {
3369
+ return text.replace(MARKDOWN_SPECIAL_CHARACTERS, "\\$1");
3370
+ }
3371
+ function escapeCodeContent(text) {
3372
+ return text.replace(/\\/g, "\\\\").replace(/`/g, "\\`");
3373
+ }
3374
+ function escapeLinkDestination(url) {
3375
+ return url.replace(/\\/g, "\\\\").replace(/\)/g, "\\)").replace(/\(/g, "\\(");
3376
+ }
3377
+ //#endregion
3378
+ //#region src/bot/presenters/static.presenter.ts
3379
+ function presentStartMarkdownMessage(copy = BOT_COPY) {
3380
+ return copy.start.lines.join("\n");
3381
+ }
3382
+ function presentHelpMarkdownMessage(copy = BOT_COPY) {
3383
+ return copy.help.lines.join("\n");
3384
+ }
3385
+ //#endregion
3386
+ //#region src/bot/commands/help.ts
3387
+ async function handleHelpCommand(ctx, dependencies) {
3388
+ const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat?.id);
3389
+ const reply = buildTelegramStaticReply(presentHelpMarkdownMessage(copy));
3390
+ try {
3391
+ await ctx.reply(reply.preferred.text, reply.preferred.options);
3392
+ } catch (error) {
3393
+ if (reply.preferred.options) {
3394
+ dependencies.logger.error({ error }, "failed to send help markdown reply, falling back to plain text");
3395
+ await ctx.reply(reply.fallback.text);
3396
+ return;
3397
+ }
3398
+ dependencies.logger.error({ error }, "failed to show help message");
3399
+ await ctx.reply(presentError(error, copy));
3400
+ }
3401
+ }
3402
+ function registerHelpCommand(bot, dependencies) {
3403
+ bot.command("help", async (ctx) => {
3404
+ await handleHelpCommand(ctx, dependencies);
3405
+ });
3406
+ }
3407
+ //#endregion
3408
+ //#region src/bot/commands/language.ts
3409
+ async function handleLanguageCommand(ctx, dependencies) {
3410
+ const language = await getChatLanguage(dependencies.sessionRepo, ctx.chat.id);
3411
+ const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
3412
+ try {
3413
+ await syncTelegramCommandsForChat(ctx.api, ctx.chat.id, language);
3414
+ await ctx.reply(presentLanguageMessage(language, copy), { reply_markup: buildLanguageKeyboard(language, copy) });
3415
+ } catch (error) {
3416
+ dependencies.logger.error({ error }, "failed to show language options");
3417
+ await ctx.reply(presentError(error, copy));
3418
+ }
3419
+ }
3420
+ async function switchLanguageForChat(api, chatId, language, dependencies) {
3421
+ const currentCopy = await getChatCopy(dependencies.sessionRepo, chatId);
3422
+ if (!isBotLanguage(language)) return {
3423
+ found: false,
3424
+ copy: currentCopy
3425
+ };
3426
+ await setChatLanguage(dependencies.sessionRepo, chatId, language);
3427
+ await syncTelegramCommandsForChat(api, chatId, language);
3428
+ return {
3429
+ found: true,
3430
+ copy: await getChatCopy(dependencies.sessionRepo, chatId),
3431
+ language
3432
+ };
3433
+ }
3434
+ async function presentLanguageSwitchForChat(chatId, api, language, dependencies) {
3435
+ const result = await switchLanguageForChat(api, chatId, language, dependencies);
3436
+ if (!result.found) return {
3437
+ found: false,
3438
+ copy: result.copy,
3439
+ text: result.copy.language.expired,
3440
+ keyboard: buildLanguageKeyboard(await getChatLanguage(dependencies.sessionRepo, chatId), result.copy)
3441
+ };
3442
+ return {
3443
+ found: true,
3444
+ copy: result.copy,
3445
+ text: presentLanguageSwitchMessage(result.language, result.copy),
3446
+ keyboard: buildLanguageKeyboard(result.language, result.copy)
3447
+ };
3448
+ }
3449
+ function registerLanguageCommand(bot, dependencies) {
3450
+ bot.command("language", async (ctx) => {
3451
+ await handleLanguageCommand(ctx, dependencies);
3452
+ });
3453
+ }
3454
+ //#endregion
3455
+ //#region src/bot/commands/models.ts
3456
+ async function handleModelsCommand(ctx, dependencies) {
3457
+ const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
3458
+ try {
3459
+ const result = await dependencies.listModelsUseCase.execute({ chatId: ctx.chat.id });
3460
+ if (result.models.length === 0) {
3461
+ await ctx.reply(copy.models.none);
3462
+ return;
3463
+ }
3464
+ const { keyboard, page } = buildModelsKeyboard(result.models, 0, copy);
3465
+ await ctx.reply(presentModelsMessage({
3466
+ currentModelId: result.currentModelId,
3467
+ currentModelProviderId: result.currentModelProviderId,
3468
+ currentModelVariant: result.currentModelVariant,
3469
+ models: result.models,
3470
+ page: page.page
3471
+ }, copy), { reply_markup: keyboard });
3472
+ } catch (error) {
3473
+ dependencies.logger.error({ error }, "failed to list models");
3474
+ await ctx.reply(presentError(error, copy));
3475
+ }
3476
+ }
3477
+ function registerModelsCommand(bot, dependencies) {
3478
+ bot.command(["model", "models"], async (ctx) => {
3479
+ await handleModelsCommand(ctx, dependencies);
3480
+ });
3481
+ }
3482
+ //#endregion
3483
+ //#region src/bot/commands/new.ts
3484
+ async function handleNewCommand(ctx, dependencies) {
3485
+ const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
3486
+ try {
3487
+ const title = extractSessionTitle(ctx);
3488
+ const result = await dependencies.createSessionUseCase.execute({
3489
+ chatId: ctx.chat.id,
3490
+ title
3491
+ });
3492
+ await ctx.reply(presentSessionCreatedMessage(result.session, copy));
3493
+ } catch (error) {
3494
+ dependencies.logger.error({ error }, "failed to create new session");
3495
+ await ctx.reply(presentError(error, copy));
3496
+ }
3497
+ }
3498
+ function registerNewCommand(bot, dependencies) {
3499
+ bot.command("new", async (ctx) => {
3500
+ await handleNewCommand(ctx, dependencies);
3501
+ });
3502
+ }
3503
+ function extractSessionTitle(ctx) {
3504
+ if (typeof ctx.match === "string") {
3505
+ const title = ctx.match.trim();
3506
+ return title ? title : null;
3507
+ }
3508
+ const messageText = ctx.message?.text?.trim();
3509
+ if (!messageText) return null;
3510
+ const title = messageText.match(/^\/new(?:@\S+)?(?:\s+([\s\S]*))?$/i)?.[1]?.trim();
3511
+ return title ? title : null;
3512
+ }
3513
+ //#endregion
3514
+ //#region src/bot/commands/status.ts
3515
+ async function handleStatusCommand(ctx, dependencies) {
3516
+ const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat?.id);
3517
+ try {
3518
+ const result = await dependencies.getStatusUseCase.execute({ chatId: ctx.chat?.id ?? 0 });
3519
+ const renderedMarkdown = renderMarkdownToTelegramMarkdownV2(presentStatusMarkdownMessage(result, copy));
3520
+ if (renderedMarkdown) {
3521
+ await ctx.reply(renderedMarkdown, { parse_mode: "MarkdownV2" });
3522
+ return;
3523
+ }
3524
+ await ctx.reply(presentStatusMessage(result, copy));
3525
+ } catch (error) {
3526
+ dependencies.logger.error({ error }, "failed to fetch system status");
3527
+ await ctx.reply(presentError(error, copy));
3528
+ }
3529
+ }
3530
+ function registerStatusCommand(bot, dependencies) {
3531
+ bot.command("status", async (ctx) => {
3532
+ await handleStatusCommand(ctx, dependencies);
3533
+ });
3534
+ }
3535
+ //#endregion
3536
+ //#region src/bot/commands/sessions.ts
3537
+ async function handleSessionsCommand(ctx, dependencies) {
3538
+ const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
3539
+ try {
3540
+ await dependencies.pendingActionRepo.clear(ctx.chat.id);
3541
+ const view = await buildSessionsListView(ctx.chat.id, 0, dependencies);
3542
+ await ctx.reply(view.text, view.keyboard ? { reply_markup: view.keyboard } : void 0);
3543
+ } catch (error) {
3544
+ dependencies.logger.error({ error }, "failed to list sessions");
3545
+ await ctx.reply(presentError(error, copy));
3546
+ }
3547
+ }
3548
+ function registerSessionsCommand(bot, dependencies) {
3549
+ bot.command("sessions", async (ctx) => {
3550
+ await handleSessionsCommand(ctx, dependencies);
3551
+ });
3552
+ }
3553
+ //#endregion
3554
+ //#region src/bot/commands/start.ts
3555
+ async function handleStartCommand(ctx, dependencies) {
3556
+ const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat?.id);
3557
+ const reply = buildTelegramStaticReply(presentStartMarkdownMessage(copy));
3558
+ try {
3559
+ await ctx.reply(reply.preferred.text, reply.preferred.options);
3560
+ } catch (error) {
3561
+ if (reply.preferred.options) {
3562
+ dependencies.logger.error({ error }, "failed to send start markdown reply, falling back to plain text");
3563
+ await ctx.reply(reply.fallback.text);
3564
+ return;
3565
+ }
3566
+ dependencies.logger.error({ error }, "failed to show start message");
3567
+ await ctx.reply(presentError(error, copy));
3568
+ }
3569
+ }
3570
+ function registerStartCommand(bot, dependencies) {
3571
+ bot.command("start", async (ctx) => {
3572
+ await handleStartCommand(ctx, dependencies);
3573
+ });
3574
+ }
3575
+ //#endregion
3576
+ //#region src/bot/handlers/callback.handler.ts
3577
+ var AGENTS_PAGE_PREFIX = "agents:page:";
3578
+ var AGENTS_SELECT_PREFIX = "agents:select:";
3579
+ var SESSIONS_PAGE_PREFIX = "sessions:page:";
3580
+ var SESSIONS_PICK_PREFIX = "sessions:pick:";
3581
+ var SESSIONS_SWITCH_PREFIX = "sessions:switch:";
3582
+ var SESSIONS_RENAME_PREFIX = "sessions:rename:";
3583
+ var SESSIONS_BACK_PREFIX = "sessions:back:";
3584
+ var MODEL_PAGE_PREFIX = "model:page:";
3585
+ var MODEL_PICK_PREFIX = "model:pick:";
3586
+ var MODEL_VARIANT_PREFIX = "model:variant:";
3587
+ var LANGUAGE_SELECT_PREFIX = "language:select:";
3588
+ var PERMISSION_PREFIX = "permission:";
3589
+ async function handleAgentsCallback(ctx, dependencies) {
3590
+ const data = ctx.callbackQuery.data;
3591
+ if (!data.startsWith("agents:")) return;
3592
+ await ctx.answerCallbackQuery();
3593
+ if (!ctx.chat) return;
3594
+ const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
3595
+ try {
3596
+ if (data.startsWith(AGENTS_PAGE_PREFIX)) {
3597
+ const requestedPage = Number(data.slice(12));
3598
+ const result = await dependencies.listAgentsUseCase.execute({ chatId: ctx.chat.id });
3599
+ if (result.agents.length === 0) {
3600
+ await ctx.editMessageText(copy.agents.none);
3601
+ return;
3602
+ }
3603
+ const { keyboard, page } = buildAgentsKeyboard(result.agents, requestedPage, copy);
3604
+ await ctx.editMessageText(presentAgentsMessage({
3605
+ agents: result.agents,
3606
+ currentAgentName: result.currentAgentName,
3607
+ page: page.page
3608
+ }, copy), { reply_markup: keyboard });
3609
+ return;
3610
+ }
3611
+ if (data.startsWith(AGENTS_SELECT_PREFIX)) {
3612
+ const agentIndex = Number(data.slice(14));
3613
+ const agent = (await dependencies.listAgentsUseCase.execute({ chatId: ctx.chat.id })).agents[agentIndex - 1];
3614
+ if (!agent) {
3615
+ await ctx.editMessageText(copy.agents.expired);
3616
+ return;
3617
+ }
3618
+ const switchResult = await dependencies.switchAgentUseCase.execute({
3619
+ chatId: ctx.chat.id,
3620
+ agentName: agent.name
3621
+ });
3622
+ if (!switchResult.found) {
3623
+ await ctx.editMessageText(copy.agents.expired);
3624
+ return;
3625
+ }
3626
+ await ctx.editMessageText(presentAgentSwitchMessage(switchResult.agent, copy));
3627
+ }
3628
+ } catch (error) {
3629
+ dependencies.logger.error({ error }, "failed to handle agent callback");
3630
+ await ctx.editMessageText(presentError(error, copy));
3631
+ }
3632
+ }
3633
+ async function handleModelsCallback(ctx, dependencies) {
3634
+ const data = ctx.callbackQuery.data;
3635
+ if (!data.startsWith("model:")) return;
3636
+ await ctx.answerCallbackQuery();
3637
+ if (!ctx.chat) return;
3638
+ const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
3639
+ try {
3640
+ if (data.startsWith(MODEL_PAGE_PREFIX)) {
3641
+ const requestedPage = Number(data.slice(11));
3642
+ const result = await dependencies.listModelsUseCase.execute({ chatId: ctx.chat.id });
3643
+ if (result.models.length === 0) {
3644
+ await ctx.editMessageText(copy.models.none);
3645
+ return;
3646
+ }
3647
+ const { keyboard, page } = buildModelsKeyboard(result.models, requestedPage, copy);
3648
+ await ctx.editMessageText(presentModelsMessage({
3649
+ currentModelId: result.currentModelId,
3650
+ currentModelProviderId: result.currentModelProviderId,
3651
+ currentModelVariant: result.currentModelVariant,
3652
+ models: result.models,
3653
+ page: page.page
3654
+ }, copy), { reply_markup: keyboard });
3655
+ return;
3656
+ }
3657
+ if (data.startsWith(MODEL_PICK_PREFIX)) {
3658
+ const modelIndex = Number(data.slice(11));
3659
+ const model = (await dependencies.listModelsUseCase.execute({ chatId: ctx.chat.id })).models[modelIndex - 1];
3660
+ if (!model) {
3661
+ await ctx.editMessageText(copy.models.expired);
3662
+ return;
3663
+ }
3664
+ const variants = getModelVariants(model);
3665
+ if (variants.length === 0) {
3666
+ const switchResult = await dependencies.switchModelUseCase.execute({
3667
+ chatId: ctx.chat.id,
3668
+ providerId: model.providerID,
3669
+ modelId: model.id
3670
+ });
3671
+ if (!switchResult.found) {
3672
+ await ctx.editMessageText(copy.models.expired);
3673
+ return;
3674
+ }
3675
+ await ctx.editMessageText(presentModelSwitchMessage(switchResult.model, switchResult.variant, copy));
3676
+ return;
3677
+ }
3678
+ await ctx.editMessageText(presentModelVariantsMessage(model, modelIndex, copy), { reply_markup: buildModelVariantsKeyboard(variants, modelIndex) });
3679
+ return;
3680
+ }
3681
+ if (data.startsWith(MODEL_VARIANT_PREFIX)) {
3682
+ const [modelIndexRaw, variantIndexRaw] = data.slice(14).split(":");
3683
+ const modelIndex = Number(modelIndexRaw);
3684
+ const variantIndex = Number(variantIndexRaw);
3685
+ const model = (await dependencies.listModelsUseCase.execute({ chatId: ctx.chat.id })).models[modelIndex - 1];
3686
+ if (!model) {
3687
+ await ctx.editMessageText(copy.models.expired);
3688
+ return;
3689
+ }
3690
+ const variant = getModelVariants(model)[variantIndex - 1];
3691
+ if (!variant) {
3692
+ await ctx.editMessageText(copy.models.reasoningLevelExpired);
3693
+ return;
3694
+ }
3695
+ const switchResult = await dependencies.switchModelUseCase.execute({
3696
+ chatId: ctx.chat.id,
3697
+ providerId: model.providerID,
3698
+ modelId: model.id,
3699
+ variant
3700
+ });
3701
+ if (!switchResult.found) {
3702
+ await ctx.editMessageText(copy.models.expired);
3703
+ return;
3704
+ }
3705
+ await ctx.editMessageText(presentModelSwitchMessage(switchResult.model, switchResult.variant, copy));
3706
+ }
3707
+ } catch (error) {
3708
+ dependencies.logger.error({ error }, "failed to handle model callback");
3709
+ await ctx.editMessageText(presentError(error, copy));
3710
+ }
3711
+ }
3712
+ async function handleSessionsCallback(ctx, dependencies) {
3713
+ const data = ctx.callbackQuery.data;
3714
+ if (!data.startsWith("sessions:")) return;
3715
+ await ctx.answerCallbackQuery();
3716
+ if (!ctx.chat) return;
3717
+ const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
3718
+ try {
3719
+ if (data.startsWith(SESSIONS_PAGE_PREFIX)) {
3720
+ const requestedPage = Number(data.slice(14));
3721
+ const view = await buildSessionsListView(ctx.chat.id, requestedPage, dependencies);
3722
+ await ctx.editMessageText(view.text, view.keyboard ? { reply_markup: view.keyboard } : void 0);
3723
+ return;
3724
+ }
3725
+ if (data.startsWith(SESSIONS_BACK_PREFIX)) {
3726
+ const requestedPage = Number(data.slice(14));
3727
+ const view = await buildSessionsListView(ctx.chat.id, requestedPage, dependencies);
3728
+ await ctx.editMessageText(view.text, view.keyboard ? { reply_markup: view.keyboard } : void 0);
3729
+ return;
3730
+ }
3731
+ if (data.startsWith(SESSIONS_PICK_PREFIX)) {
3732
+ const target = parseSessionActionTarget(data, SESSIONS_PICK_PREFIX);
3733
+ if (!target) {
3734
+ await ctx.editMessageText(copy.sessions.expired);
3735
+ return;
3736
+ }
3737
+ const view = await buildSessionActionView(ctx.chat.id, target.page, target.sessionId, dependencies);
3738
+ if (!view.found) {
3739
+ await ctx.editMessageText(copy.sessions.expired);
3740
+ return;
3741
+ }
3742
+ await ctx.editMessageText(view.text, { reply_markup: view.keyboard });
3743
+ return;
3744
+ }
3745
+ if (data.startsWith(SESSIONS_SWITCH_PREFIX)) {
3746
+ const target = parseSessionActionTarget(data, SESSIONS_SWITCH_PREFIX);
3747
+ if (!target) {
3748
+ await ctx.editMessageText(copy.sessions.expired);
3749
+ return;
3750
+ }
3751
+ const result = await dependencies.switchSessionUseCase.execute({
3752
+ chatId: ctx.chat.id,
3753
+ sessionId: target.sessionId
3754
+ });
3755
+ if (!result.found) {
3756
+ await ctx.editMessageText(copy.sessions.expired);
3757
+ return;
3758
+ }
3759
+ await ctx.editMessageText(presentSessionSwitchMessage(result.session, copy));
3760
+ return;
3761
+ }
3762
+ if (data.startsWith(SESSIONS_RENAME_PREFIX)) {
3763
+ const target = parseSessionActionTarget(data, SESSIONS_RENAME_PREFIX);
3764
+ if (!target) {
3765
+ await ctx.editMessageText(copy.sessions.renameExpired);
3766
+ return;
3767
+ }
3768
+ const view = await buildSessionActionView(ctx.chat.id, target.page, target.sessionId, dependencies);
3769
+ if (!view.found) {
3770
+ await ctx.editMessageText(copy.sessions.renameExpired);
3771
+ return;
3772
+ }
3773
+ const menuMessageId = ctx.callbackQuery.message?.message_id;
3774
+ if (typeof menuMessageId !== "number" || !Number.isInteger(menuMessageId)) {
3775
+ await ctx.editMessageText(copy.sessions.renameExpired);
3776
+ return;
3777
+ }
3778
+ await dependencies.pendingActionRepo.set({
3779
+ chatId: ctx.chat.id,
3780
+ kind: "session_rename",
3781
+ sessionId: view.session.id,
3782
+ menuMessageId,
3783
+ returnPage: view.page,
3784
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
3785
+ });
3786
+ await ctx.editMessageText(presentSessionRenamePromptMessage(view.session, copy));
3787
+ }
3788
+ } catch (error) {
3789
+ dependencies.logger.error({ error }, "failed to handle session callback");
3790
+ await ctx.editMessageText(presentError(error, copy));
3791
+ }
3792
+ }
3793
+ async function handleLanguageCallback(ctx, dependencies) {
3794
+ const data = ctx.callbackQuery.data;
3795
+ if (!data.startsWith("language:")) return;
3796
+ await ctx.answerCallbackQuery();
3797
+ if (!ctx.chat || !ctx.api) return;
3798
+ const currentCopy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
3799
+ try {
3800
+ if (!data.startsWith(LANGUAGE_SELECT_PREFIX)) {
3801
+ await ctx.editMessageText(presentLanguageMessage(await getChatLanguage(dependencies.sessionRepo, ctx.chat.id), currentCopy), { reply_markup: buildLanguageKeyboard(await getChatLanguage(dependencies.sessionRepo, ctx.chat.id), currentCopy) });
3802
+ return;
3803
+ }
3804
+ const selectedLanguage = data.slice(16);
3805
+ const result = await presentLanguageSwitchForChat(ctx.chat.id, ctx.api, selectedLanguage, dependencies);
3806
+ await ctx.editMessageText(result.text, { reply_markup: result.keyboard });
3807
+ } catch (error) {
3808
+ dependencies.logger.error({ error }, "failed to handle language callback");
3809
+ await ctx.editMessageText(presentError(error, currentCopy));
3810
+ }
3811
+ }
3812
+ async function handlePermissionApprovalCallback(ctx, dependencies) {
3813
+ const data = ctx.callbackQuery.data;
3814
+ if (!data.startsWith(PERMISSION_PREFIX)) return;
3815
+ await ctx.answerCallbackQuery();
3816
+ if (!ctx.chat) return;
3817
+ const parsed = parsePermissionApprovalCallbackData(data);
3818
+ if (!parsed) return;
3819
+ try {
3820
+ await dependencies.opencodeClient.replyToPermission(parsed.requestId, parsed.reply);
3821
+ const approval = (await dependencies.permissionApprovalRepo.listByRequestId(parsed.requestId)).find((item) => item.chatId === ctx.chat?.id);
3822
+ if (approval) await dependencies.permissionApprovalRepo.set({
3823
+ ...approval,
3824
+ status: parsed.reply,
3825
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
3826
+ });
3827
+ await ctx.editMessageText(buildPermissionApprovalResolvedMessage(parsed.requestId, parsed.reply));
3828
+ } catch (error) {
3829
+ dependencies.logger.error({
3830
+ error,
3831
+ requestId: parsed.requestId
3832
+ }, "failed to reply to permission request");
3833
+ await ctx.editMessageText("Failed to reply to the permission request.");
3834
+ }
3835
+ }
3836
+ function registerCallbackHandler(bot, dependencies) {
3837
+ bot.callbackQuery(/^agents:/, async (ctx) => {
3838
+ await handleAgentsCallback(ctx, dependencies);
3839
+ });
3840
+ bot.callbackQuery(/^sessions:/, async (ctx) => {
3841
+ await handleSessionsCallback(ctx, dependencies);
3842
+ });
3843
+ bot.callbackQuery(/^model:/, async (ctx) => {
3844
+ await handleModelsCallback(ctx, dependencies);
3845
+ });
3846
+ bot.callbackQuery(/^language:/, async (ctx) => {
3847
+ await handleLanguageCallback(ctx, dependencies);
3848
+ });
3849
+ bot.callbackQuery(/^permission:/, async (ctx) => {
3850
+ await handlePermissionApprovalCallback(ctx, dependencies);
3851
+ });
3852
+ }
3853
+ function parseSessionActionTarget(data, prefix) {
3854
+ const suffix = data.slice(prefix.length);
3855
+ const separatorIndex = suffix.indexOf(":");
3856
+ if (separatorIndex === -1) return null;
3857
+ const page = Number(suffix.slice(0, separatorIndex));
3858
+ const sessionId = suffix.slice(separatorIndex + 1).trim();
3859
+ if (!Number.isInteger(page) || page < 0 || !sessionId) return null;
3860
+ return {
3861
+ page,
3862
+ sessionId
3863
+ };
3864
+ }
3865
+ //#endregion
3866
+ //#region src/bot/handlers/prompt.handler.ts
3867
+ var activePromptChats = /* @__PURE__ */ new Set();
3868
+ async function executePromptRequest(ctx, dependencies, resolvePrompt) {
3869
+ const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
3870
+ if (activePromptChats.has(ctx.chat.id)) {
3871
+ await ctx.reply(copy.status.alreadyProcessing);
3872
+ return;
3873
+ }
3874
+ let processingMessage = null;
3875
+ let sentTerminalReply = false;
3876
+ try {
3877
+ activePromptChats.add(ctx.chat.id);
3878
+ processingMessage = await ctx.reply(copy.status.processing);
3879
+ const promptInput = await resolvePrompt();
3880
+ const telegramReply = buildTelegramPromptReply(normalizePromptReplyForDisplay((await dependencies.sendPromptUseCase.execute({
3881
+ chatId: ctx.chat.id,
3882
+ text: promptInput.text,
3883
+ files: promptInput.files
3884
+ })).assistantReply, copy, dependencies), copy);
3885
+ try {
3886
+ await ctx.reply(telegramReply.preferred.text, telegramReply.preferred.options);
3887
+ } catch (replyError) {
3888
+ dependencies.logger.warn?.({ error: replyError }, "failed to send preferred telegram reply, falling back to plain text");
3889
+ await ctx.reply(telegramReply.fallback.text, telegramReply.fallback.options);
3890
+ }
3891
+ sentTerminalReply = true;
3892
+ } catch (error) {
3893
+ dependencies.logger.error({ error }, "failed to handle prompt request");
3894
+ await ctx.reply(presentError(error, copy));
3895
+ sentTerminalReply = true;
3896
+ } finally {
3897
+ activePromptChats.delete(ctx.chat.id);
3898
+ if (processingMessage && sentTerminalReply) try {
3899
+ await ctx.api.deleteMessage(ctx.chat.id, processingMessage.message_id);
3900
+ } catch (error) {
3901
+ dependencies.logger.warn?.({ error }, "failed to delete processing message");
3902
+ }
3903
+ }
3904
+ }
3905
+ function normalizePromptReplyForDisplay(promptReply, copy, dependencies) {
3906
+ if (!promptReply.assistantError) return promptReply;
3907
+ if (isRecoverableStructuredOutputError(promptReply)) {
3908
+ dependencies.logger.warn?.({ error: promptReply.assistantError }, "structured output validation failed, falling back to assistant text reply");
3909
+ return {
3910
+ ...promptReply,
3911
+ assistantError: null
3912
+ };
3913
+ }
3914
+ return {
3915
+ ...promptReply,
3916
+ bodyMd: null,
3917
+ fallbackText: presentError(promptReply.assistantError, copy)
3918
+ };
3919
+ }
3920
+ function isRecoverableStructuredOutputError(promptReply) {
3921
+ if (promptReply.assistantError?.name !== "StructuredOutputError") return false;
3922
+ if (promptReply.bodyMd?.trim()) return true;
3923
+ return promptReply.parts.some((part) => part.type === "text" && typeof part.text === "string" && part.text.trim().length > 0);
3924
+ }
3925
+ //#endregion
3926
+ //#region src/bot/handlers/file.handler.ts
3927
+ var TELEGRAM_MAX_DOWNLOAD_BYTES$1 = 20 * 1024 * 1024;
3928
+ async function handleImageMessage(ctx, dependencies) {
3929
+ const image = resolveTelegramImage(ctx.message);
3930
+ if (!image) return;
3931
+ if (await replyIfSessionRenamePending(ctx, dependencies)) return;
3932
+ await executePromptRequest(ctx, dependencies, async () => {
3933
+ 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.`);
3934
+ const filePath = (await ctx.getFile()).file_path?.trim();
3935
+ if (!filePath) throw new ImageMessageUnsupportedError("Telegram did not provide a downloadable image file path.");
3936
+ return {
3937
+ files: [await dependencies.uploadFileUseCase.execute({
3938
+ expectedType: "image",
3939
+ filePath,
3940
+ filename: image.filename,
3941
+ mimeType: image.mimeType
3942
+ })],
3943
+ text: ctx.message.caption?.trim() || null
3944
+ };
3945
+ });
3946
+ }
3947
+ function registerFileHandler(bot, dependencies) {
3948
+ bot.on("message:photo", async (ctx) => {
3949
+ await handleImageMessage(ctx, dependencies);
3950
+ });
3951
+ bot.on("message:document", async (ctx) => {
3952
+ await handleImageMessage(ctx, dependencies);
3953
+ });
3954
+ }
3955
+ function resolveTelegramImage(message) {
3956
+ const document = message.document;
3957
+ if (document && isImageDocument(document)) return {
3958
+ fileId: document.file_id,
3959
+ fileSize: document.file_size,
3960
+ filename: document.file_name ?? null,
3961
+ mimeType: document.mime_type ?? null
3962
+ };
3963
+ const photo = pickLargestPhoto(message.photo);
3964
+ if (!photo) return null;
3965
+ return {
3966
+ fileId: photo.file_id,
3967
+ fileSize: photo.file_size,
3968
+ filename: null,
3969
+ mimeType: "image/jpeg"
3970
+ };
3971
+ }
3972
+ function isImageDocument(document) {
3973
+ const mimeType = document.mime_type?.trim().toLowerCase();
3974
+ const filename = document.file_name?.trim().toLowerCase() ?? "";
3975
+ return !!mimeType?.startsWith("image/") || /\.(png|jpe?g|gif|webp|bmp|svg)$/i.test(filename);
3976
+ }
3977
+ function pickLargestPhoto(photos) {
3978
+ if (!photos || photos.length === 0) return null;
3979
+ return photos.reduce((largest, current) => {
3980
+ return (current.file_size ?? 0) >= (largest.file_size ?? 0) ? current : largest;
3981
+ }, photos[0]);
3982
+ }
3983
+ //#endregion
3984
+ //#region src/bot/handlers/message.handler.ts
3985
+ async function handleTextMessage(ctx, dependencies) {
3986
+ const text = ctx.message.text?.trim();
3987
+ if (await handlePendingSessionRenameText(ctx, dependencies)) return;
3988
+ if (!text) return;
3989
+ if (text.startsWith("/")) return;
3990
+ await executePromptRequest(ctx, dependencies, async () => ({ text }));
3991
+ }
3992
+ function registerMessageHandler(bot, dependencies) {
3993
+ bot.on("message:text", async (ctx) => {
3994
+ await handleTextMessage(ctx, dependencies);
3995
+ });
3996
+ }
3997
+ //#endregion
3998
+ //#region src/bot/handlers/voice.handler.ts
3999
+ var DEFAULT_VOICE_FILE_NAME = "telegram-voice.ogg";
4000
+ var DEFAULT_VOICE_MIME_TYPE = "audio/ogg";
4001
+ var TELEGRAM_MAX_DOWNLOAD_BYTES = 20 * 1024 * 1024;
4002
+ async function handleVoiceMessage(ctx, dependencies) {
4003
+ if (!ctx.message.voice) return;
4004
+ if (await replyIfSessionRenamePending(ctx, dependencies)) return;
4005
+ await executePromptRequest(ctx, dependencies, async () => {
4006
+ const voice = ctx.message.voice;
4007
+ if (!voice) throw new VoiceMessageUnsupportedError("Telegram voice payload is missing.");
4008
+ 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.`);
4009
+ const filePath = (await ctx.getFile()).file_path?.trim();
4010
+ if (!filePath) throw new VoiceMessageUnsupportedError("Telegram did not provide a downloadable voice file path.");
4011
+ const download = await dependencies.telegramFileClient.downloadFile({ filePath });
4012
+ const mimeType = voice.mime_type?.trim() || download.mimeType || DEFAULT_VOICE_MIME_TYPE;
4013
+ const filename = resolveVoiceFilename(filePath);
4014
+ return { text: (await dependencies.voiceTranscriptionService.transcribeVoice({
4015
+ data: download.data,
4016
+ filename,
4017
+ mimeType
4018
+ })).text };
4019
+ });
4020
+ }
4021
+ function registerVoiceHandler(bot, dependencies) {
4022
+ bot.on("message:voice", async (ctx) => {
4023
+ await handleVoiceMessage(ctx, dependencies);
4024
+ });
4025
+ }
4026
+ function resolveVoiceFilename(filePath) {
4027
+ const normalizedPath = filePath.trim();
4028
+ if (!normalizedPath) return DEFAULT_VOICE_FILE_NAME;
4029
+ const filename = normalizedPath.split("/").at(-1)?.trim();
4030
+ return filename && filename.length > 0 ? filename : DEFAULT_VOICE_FILE_NAME;
4031
+ }
4032
+ //#endregion
4033
+ //#region src/bot/middlewares/auth.ts
4034
+ function createAuthMiddleware(allowedChatIds) {
4035
+ return async (ctx, next) => {
4036
+ if (allowedChatIds.length === 0) return next();
4037
+ const chatId = ctx.chat?.id;
4038
+ if (!chatId || !allowedChatIds.includes(chatId)) {
4039
+ await ctx.reply("Unauthorized chat.");
4040
+ return;
4041
+ }
4042
+ return next();
4043
+ };
4044
+ }
4045
+ //#endregion
4046
+ //#region src/bot/middlewares/logging.ts
4047
+ function buildIncomingUpdateLogFields(ctx) {
4048
+ const messageText = ctx.msg && "text" in ctx.msg ? ctx.msg.text : void 0;
4049
+ return {
4050
+ updateId: ctx.update.update_id,
4051
+ chatId: ctx.chat?.id,
4052
+ fromId: ctx.from?.id,
4053
+ hasText: typeof messageText === "string" && messageText.length > 0,
4054
+ textLength: typeof messageText === "string" ? messageText.length : 0,
4055
+ textPreview: typeof messageText === "string" && messageText.length > 0 ? createRedactedPreview(messageText) : void 0
4056
+ };
4057
+ }
4058
+ function createLoggingMiddleware(logger) {
4059
+ return async (ctx, next) => {
4060
+ logger.info(buildIncomingUpdateLogFields(ctx), "incoming update");
4061
+ return next();
4062
+ };
4063
+ }
4064
+ //#endregion
4065
+ //#region src/bot/index.ts
4066
+ function registerBot(bot, container, options) {
4067
+ bot.use(createLoggingMiddleware(container.logger));
4068
+ bot.use(createAuthMiddleware(options.telegramAllowedChatIds));
4069
+ registerStartCommand(bot, container);
4070
+ registerHelpCommand(bot, container);
4071
+ registerStatusCommand(bot, container);
4072
+ registerNewCommand(bot, container);
4073
+ registerAgentsCommand(bot, container);
4074
+ registerSessionsCommand(bot, container);
4075
+ registerCancelCommand(bot, container);
4076
+ registerModelsCommand(bot, container);
4077
+ registerLanguageCommand(bot, container);
4078
+ registerCallbackHandler(bot, container);
4079
+ registerFileHandler(bot, container);
4080
+ registerMessageHandler(bot, container);
4081
+ registerVoiceHandler(bot, container);
4082
+ }
4083
+ //#endregion
4084
+ //#region src/app/runtime.ts
4085
+ async function startTelegramBotRuntime(input) {
4086
+ const bot = new Bot(input.config.telegramBotToken, { client: { apiRoot: input.config.telegramApiRoot } });
4087
+ registerBot(bot, input.container, { telegramAllowedChatIds: input.config.telegramAllowedChatIds });
4088
+ bot.catch((error) => {
4089
+ input.container.logger.error({
4090
+ error: error.error,
4091
+ update: error.ctx.update
4092
+ }, "telegram bot update failed");
4093
+ });
4094
+ input.container.logger.info("bot starting...");
4095
+ if (input.syncCommands ?? true) await syncTelegramCommands(bot, input.container.logger);
4096
+ const runner = run(bot);
4097
+ let stopped = false;
4098
+ let disposed = false;
4099
+ const stop = () => {
4100
+ if (stopped) return;
4101
+ stopped = true;
4102
+ runner.stop();
4103
+ };
4104
+ const dispose = async () => {
4105
+ if (disposed) return;
4106
+ disposed = true;
4107
+ stop();
4108
+ await input.container.dispose();
4109
+ };
4110
+ return {
4111
+ bot,
4112
+ container: input.container,
4113
+ stop,
4114
+ dispose
4115
+ };
4116
+ }
4117
+ //#endregion
4118
+ //#region src/plugin.ts
4119
+ var runtimeState = null;
4120
+ async function ensureTelegramBotPluginRuntime(options) {
4121
+ const cwd = resolvePluginRuntimeCwd(options.context);
4122
+ if (runtimeState && runtimeState.cwd !== cwd) {
4123
+ const activeState = runtimeState;
4124
+ runtimeState = null;
4125
+ await disposeTelegramBotPluginRuntimeState(activeState);
4126
+ }
4127
+ if (!runtimeState) {
4128
+ const runtimePromise = startPluginRuntime(options, cwd).then((runtime) => {
4129
+ if (runtimeState?.runtimePromise === runtimePromise) runtimeState.runtime = runtime;
4130
+ return runtime;
4131
+ }).catch((error) => {
4132
+ if (runtimeState?.runtimePromise === runtimePromise) runtimeState = null;
4133
+ throw error;
4134
+ });
4135
+ runtimeState = {
4136
+ cwd,
4137
+ runtime: null,
4138
+ runtimePromise
4139
+ };
4140
+ }
4141
+ return runtimeState.runtimePromise;
4142
+ }
4143
+ var TelegramBotPlugin = async (context) => {
4144
+ return createHooks(await ensureTelegramBotPluginRuntime({ context }));
4145
+ };
4146
+ async function resetTelegramBotPluginRuntimeForTests() {
4147
+ if (!runtimeState) return;
4148
+ const activeState = runtimeState;
4149
+ runtimeState = null;
4150
+ await disposeTelegramBotPluginRuntimeState(activeState);
4151
+ }
4152
+ async function startPluginRuntime(options, cwd) {
4153
+ const bootstrapApp = options.bootstrapApp ?? bootstrapPluginApp;
4154
+ const prepareConfiguration = options.prepareConfiguration ?? preparePluginConfiguration;
4155
+ const startRuntime = options.startRuntime ?? startTelegramBotRuntime;
4156
+ const preparedConfiguration = await prepareConfiguration({
4157
+ cwd,
4158
+ config: options.config
4159
+ });
4160
+ const { config, container } = bootstrapApp(options.context.client, preparedConfiguration.config, { cwd: preparedConfiguration.cwd });
4161
+ try {
4162
+ const runtime = await startRuntime({
4163
+ config,
4164
+ container
4165
+ });
4166
+ container.logger.info({
4167
+ cwd: preparedConfiguration.cwd,
4168
+ globalConfigFilePath: preparedConfiguration.globalConfigFilePath,
4169
+ projectConfigFilePath: preparedConfiguration.projectConfigFilePath,
4170
+ configFilePath: preparedConfiguration.configFilePath,
4171
+ mode: "plugin"
4172
+ }, "telegram bot plugin runtime started");
4173
+ return runtime;
4174
+ } catch (error) {
4175
+ await container.dispose();
4176
+ throw error;
4177
+ }
4178
+ }
4179
+ function resolvePluginRuntimeCwd(context) {
4180
+ return context.worktree ?? context.directory ?? process.cwd();
4181
+ }
4182
+ async function disposeTelegramBotPluginRuntimeState(state) {
4183
+ await (state.runtime ?? await state.runtimePromise.catch(() => null))?.dispose();
4184
+ }
4185
+ function createHooks(runtime) {
4186
+ return {
4187
+ async event({ event }) {
4188
+ await handleTelegramBotPluginEvent(runtime, event);
4189
+ },
4190
+ async "permission.ask"(input, output) {
4191
+ if ((await runtime.container.sessionRepo.listBySessionId(input.sessionID)).length > 0) output.status = "ask";
4192
+ }
4193
+ };
4194
+ }
4195
+ //#endregion
4196
+ export { TelegramBotPlugin, TelegramBotPlugin as default, ensureTelegramBotPluginRuntime, resetTelegramBotPluginRuntimeForTests };
4197
+
4198
+ //# sourceMappingURL=plugin.js.map