opencode-tbot 0.1.23 → 0.1.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.ja.md CHANGED
@@ -62,18 +62,13 @@ npm exec --package opencode-tbot@latest opencode-tbot -- update
62
62
 
63
63
  ## 設定
64
64
 
65
- ランタイム設定は次の順序で読み込まれます。
65
+ ランタイム設定は `~/.config/opencode/opencode-tbot/config.json` からのみ読み込まれます。
66
66
 
67
- 1. `~/.config/opencode/opencode-tbot/config.json` のグローバルデフォルト
68
- 2. `<worktree>/tbot.config.json` のプロジェクト上書き設定
69
-
70
- プロジェクト設定はグローバル設定に上書きマージされます。`telegram` と `state` はセクション単位でディープマージされます。
67
+ 古い `<worktree>/tbot.config.json` はランタイムでは無視されます。検出された場合は、値をグローバル設定へ移行できるように警告ログを出します。
71
68
 
72
69
  古い `openrouter` 音声転写設定はランタイムでは無視され、インストーラーが設定を書き直す際にも削除されます。
73
70
 
74
- リポジトリには最小構成の参考として [tbot.config.example.json](./tbot.config.example.json) も含まれています。
75
-
76
- ### `tbot.config.json` の例
71
+ ### グローバル `config.json` の例
77
72
 
78
73
  ```json
79
74
  {
@@ -109,7 +104,7 @@ npm exec --package opencode-tbot@latest opencode-tbot -- update
109
104
  ## クイックスタート
110
105
 
111
106
  1. `npm exec --package opencode-tbot@latest opencode-tbot -- install` でプラグインをインストールします。
112
- 2. 特定の chat のみ許可したい場合は、`tbot.config.json` で `telegram.allowedChatIds` を設定します。
107
+ 2. 特定の chat のみ許可したい場合は、`~/.config/opencode/opencode-tbot/config.json` で `telegram.allowedChatIds` を設定します。
113
108
  3. 対象の worktree で OpenCode を起動し、プラグインランタイムを読み込ませます。
114
109
  4. Telegram で `/status` を実行し、接続を確認します。
115
110
  5. `/new [title]` を実行するか、テキストメッセージを直接送信して使い始めます。
package/README.md CHANGED
@@ -12,6 +12,7 @@ A Telegram plugin for driving [OpenCode](https://opencode.ai) from chat.
12
12
 
13
13
  - Text messages are forwarded to the active OpenCode session.
14
14
  - Telegram photos and image documents are uploaded as OpenCode file parts.
15
+ - Image turns run in a temporary forked session, so later text-only prompts do not inherit image context.
15
16
  - Telegram voice messages are explicitly rejected with a localized reply.
16
17
  - Permission requests raised by OpenCode can be approved or rejected from Telegram inline buttons.
17
18
  - Session completion and error events can be reported back to the bound Telegram chat.
@@ -62,18 +63,13 @@ npm exec --package opencode-tbot@latest opencode-tbot -- update
62
63
 
63
64
  ## Configuration
64
65
 
65
- Runtime config is loaded in this order:
66
+ Runtime config is loaded from `~/.config/opencode/opencode-tbot/config.json`.
66
67
 
67
- 1. Global defaults from `~/.config/opencode/opencode-tbot/config.json`
68
- 2. Project overrides from `<worktree>/tbot.config.json`
69
-
70
- Project config is merged on top of the global config. `telegram` and `state` are deep-merged by section.
68
+ Legacy `<worktree>/tbot.config.json` files are ignored at runtime. If one is present, the plugin logs a warning so you can migrate its values into the global config.
71
69
 
72
70
  Legacy `openrouter` voice-transcription settings are ignored at runtime. When the installer rewrites the config, it removes them.
73
71
 
74
- The repository also includes [tbot.config.example.json](./tbot.config.example.json) as a minimal reference.
75
-
76
- ### Example `tbot.config.json`
72
+ ### Example Global `config.json`
77
73
 
78
74
  ```json
79
75
  {
@@ -109,7 +105,7 @@ The repository also includes [tbot.config.example.json](./tbot.config.example.js
109
105
  ## Quick Start
110
106
 
111
107
  1. Install the plugin with `npm exec --package opencode-tbot@latest opencode-tbot -- install`.
112
- 2. Set `telegram.allowedChatIds` in `tbot.config.json` if you want to restrict the bot to specific chats.
108
+ 2. Set `telegram.allowedChatIds` in `~/.config/opencode/opencode-tbot/config.json` if you want to restrict the bot to specific chats.
113
109
  3. Start OpenCode in the target worktree so the plugin runtime can load.
114
110
  4. In Telegram, run `/status` to verify the connection.
115
111
  5. Run `/new [title]` or send a text message directly to start working.
@@ -129,6 +125,7 @@ Message handling:
129
125
 
130
126
  - Non-command text is treated as a prompt and sent to OpenCode.
131
127
  - Telegram photos and image documents are forwarded as OpenCode file parts.
128
+ - Image attachments are processed in a temporary fork of the active session so later text-only prompts stay clean.
132
129
  - Telegram voice messages are not supported and receive a localized rejection reply.
133
130
 
134
131
  ## Development
package/README.zh-CN.md CHANGED
@@ -62,18 +62,13 @@ npm exec --package opencode-tbot@latest opencode-tbot -- update
62
62
 
63
63
  ## 配置
64
64
 
65
- 运行时配置按以下优先级加载:
65
+ 运行时配置只会从 `~/.config/opencode/opencode-tbot/config.json` 加载。
66
66
 
67
- 1. 全局默认配置 `~/.config/opencode/opencode-tbot/config.json`
68
- 2. 项目覆盖配置 `<worktree>/tbot.config.json`
69
-
70
- 项目配置会覆盖全局默认值;`telegram` 和 `state` 会按分段进行深合并。
67
+ 遗留的 `<worktree>/tbot.config.json` 会在运行时被忽略;如果检测到该文件,插件会记录一条警告,提示你把其中的值迁移到全局配置。
71
68
 
72
69
  遗留的 `openrouter` 语音转写配置在运行时会被忽略;安装器重写配置时也会自动移除这些字段。
73
70
 
74
- 仓库内还提供了 [tbot.config.example.json](./tbot.config.example.json) 作为最小配置参考。
75
-
76
- ### `tbot.config.json` 示例
71
+ ### 全局 `config.json` 示例
77
72
 
78
73
  ```json
79
74
  {
@@ -109,7 +104,7 @@ npm exec --package opencode-tbot@latest opencode-tbot -- update
109
104
  ## 快速开始
110
105
 
111
106
  1. 使用 `npm exec --package opencode-tbot@latest opencode-tbot -- install` 安装插件。
112
- 2. 如果你只想允许特定聊天使用 bot,请在 `tbot.config.json` 中设置 `telegram.allowedChatIds`。
107
+ 2. 如果你只想允许特定聊天使用 bot,请在 `~/.config/opencode/opencode-tbot/config.json` 中设置 `telegram.allowedChatIds`。
113
108
  3. 在目标 worktree 中启动 OpenCode,让插件运行时被加载。
114
109
  4. 在 Telegram 中执行 `/status` 验证连接是否正常。
115
110
  5. 执行 `/new [title]`,或者直接发送文本消息开始使用。
@@ -69,14 +69,16 @@ var OPENCODE_CONFIG_FILE_NAME = "opencode.json";
69
69
  async function preparePluginConfiguration(options) {
70
70
  const globalConfigFilePath = getGlobalPluginConfigFilePath(options.homeDir ?? homedir());
71
71
  const projectConfigFilePath = await resolveProjectPluginConfigFilePath(options.cwd);
72
- const [globalConfig, projectConfig] = await Promise.all([loadPluginConfigFile(globalConfigFilePath), loadPluginConfigFile(projectConfigFilePath)]);
73
- const config = stripLegacyVoiceConfig(mergePluginConfigSources(globalConfig, projectConfig, options.config));
74
- const configFilePath = await pathExists(projectConfigFilePath) ? projectConfigFilePath : globalConfigFilePath;
72
+ const [globalConfig, hasIgnoredProjectConfig] = await Promise.all([loadPluginConfigFile(globalConfigFilePath), pathExists(projectConfigFilePath)]);
73
+ const config = stripLegacyVoiceConfig(mergePluginConfigSources(globalConfig, options.config));
74
+ const ignoredProjectConfigFilePath = hasIgnoredProjectConfig ? projectConfigFilePath : void 0;
75
+ const configFilePath = globalConfigFilePath;
75
76
  return {
76
77
  cwd: options.cwd,
77
78
  config,
78
79
  globalConfigFilePath,
79
80
  projectConfigFilePath,
81
+ ...ignoredProjectConfigFilePath ? { ignoredProjectConfigFilePath } : {},
80
82
  configFilePath
81
83
  };
82
84
  }
@@ -170,4 +172,4 @@ function stripLegacyVoiceConfig(config) {
170
172
  //#endregion
171
173
  export { writePluginConfigFile as a, loadAppConfig as c, preparePluginConfiguration as i, getOpenCodeConfigFilePath as n, OPENCODE_TBOT_VERSION as o, mergePluginConfigSources as r, DEFAULT_TELEGRAM_API_ROOT as s, getGlobalPluginConfigFilePath as t };
172
174
 
173
- //# sourceMappingURL=plugin-config-B8ginwol.js.map
175
+ //# sourceMappingURL=plugin-config-CCeFjxSf.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin-config-CCeFjxSf.js","names":[],"sources":["../../src/app/config.ts","../../src/app/package-info.ts","../../src/app/plugin-config.ts"],"sourcesContent":["import { resolve } from \"node:path\";\nimport { z } from \"zod\";\n\nexport const DEFAULT_STATE_FILE_PATH = \"./data/opencode-tbot.state.json\";\nexport const DEFAULT_TELEGRAM_API_ROOT = \"https://api.telegram.org\";\n\nconst AllowedChatIdSchema = z.union([\n z.number().int(),\n z.string().regex(/^-?\\d+$/u).transform((value) => Number(value)),\n]);\n\nconst TelegramConfigSchema = z.preprocess(\n (value) => value ?? {},\n z.object({\n botToken: z.string().trim().min(1),\n allowedChatIds: z.array(AllowedChatIdSchema).default([]),\n apiRoot: z.string().trim().url().default(DEFAULT_TELEGRAM_API_ROOT),\n }),\n);\n\nconst StateConfigSchema = z.preprocess(\n (value) => value ?? {},\n z.object({\n path: z.string().trim().min(1).default(DEFAULT_STATE_FILE_PATH),\n }),\n);\n\nconst AppConfigSchema = z.object({\n telegram: TelegramConfigSchema,\n state: StateConfigSchema,\n logLevel: z.string().default(\"info\"),\n});\n\nexport interface PluginConfigSource {\n telegram?: {\n botToken?: string;\n allowedChatIds?: Array<number | string>;\n apiRoot?: string;\n [key: string]: unknown;\n };\n state?: {\n path?: string;\n [key: string]: unknown;\n };\n logLevel?: string;\n [key: string]: unknown;\n}\n\nexport interface AppConfig {\n telegramBotToken: string;\n telegramAllowedChatIds: number[];\n telegramApiRoot: string;\n logLevel: string;\n stateFilePath: string;\n}\n\nexport interface LoadAppConfigOptions {\n cwd?: string;\n}\n\nexport function loadAppConfig(\n configSource: PluginConfigSource | undefined = {},\n options: LoadAppConfigOptions = {},\n): AppConfig {\n const parsed = parseConfig(AppConfigSchema, configSource);\n\n return buildAppConfig(parsed, options);\n}\n\nexport const loadPluginConfig = loadAppConfig;\n\nfunction buildAppConfig(\n data: z.infer<typeof AppConfigSchema>,\n options: LoadAppConfigOptions,\n): AppConfig {\n return {\n telegramBotToken: data.telegram.botToken,\n telegramAllowedChatIds: data.telegram.allowedChatIds,\n telegramApiRoot: normalizeApiRoot(data.telegram.apiRoot),\n logLevel: data.logLevel,\n stateFilePath: resolveStatePath(data, options.cwd ?? process.cwd()),\n };\n}\n\nfunction resolveStatePath(\n data: z.infer<typeof AppConfigSchema>,\n cwd: string,\n): string {\n return resolve(cwd, data.state.path || DEFAULT_STATE_FILE_PATH);\n}\n\nfunction normalizeApiRoot(value: string): string {\n const normalized = value.trim();\n\n return normalized.endsWith(\"/\")\n ? normalized.slice(0, -1)\n : normalized;\n}\n\nfunction parseConfig<TSchema extends z.ZodTypeAny>(\n schema: TSchema,\n configSource: PluginConfigSource | undefined,\n): z.infer<TSchema> {\n const parsed = schema.safeParse(configSource ?? {});\n\n if (parsed.success) {\n return parsed.data;\n }\n\n throw new Error(\n `Invalid plugin configuration: ${JSON.stringify(parsed.error.flatten())}`,\n );\n}\n","import { existsSync, readFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nexport const OPENCODE_TBOT_VERSION = resolvePackageVersion();\n\nfunction resolvePackageVersion(): string {\n let directory = dirname(fileURLToPath(import.meta.url));\n\n while (true) {\n const packageFilePath = join(directory, \"package.json\");\n\n if (existsSync(packageFilePath)) {\n try {\n const parsed = JSON.parse(readFileSync(packageFilePath, \"utf8\")) as {\n version?: unknown;\n };\n\n if (typeof parsed.version === \"string\" && parsed.version.trim().length > 0) {\n return parsed.version;\n }\n } catch {\n // Fall through and continue searching parent directories.\n }\n }\n\n const parentDirectory = dirname(directory);\n\n if (parentDirectory === directory) {\n break;\n }\n\n directory = parentDirectory;\n }\n\n return \"unknown\";\n}\n","import { access, mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport type { PluginConfigSource } from \"./config.js\";\n\nexport const PLUGIN_CONFIG_FILE_NAME = \"tbot.config.json\";\nexport const GLOBAL_PLUGIN_DIRECTORY_NAME = \"opencode-tbot\";\nexport const GLOBAL_PLUGIN_CONFIG_FILE_NAME = \"config.json\";\nexport const OPENCODE_CONFIG_FILE_NAME = \"opencode.json\";\n\nexport interface PreparedPluginConfiguration {\n cwd: string;\n config: PluginConfigSource;\n globalConfigFilePath: string;\n projectConfigFilePath: string;\n ignoredProjectConfigFilePath?: string;\n configFilePath: string;\n}\n\nexport interface PreparePluginConfigurationOptions {\n cwd: string;\n config?: PluginConfigSource;\n homeDir?: string;\n}\n\nexport async function preparePluginConfiguration(\n options: PreparePluginConfigurationOptions,\n): Promise<PreparedPluginConfiguration> {\n const homeDir = options.homeDir ?? homedir();\n const globalConfigFilePath = getGlobalPluginConfigFilePath(homeDir);\n const projectConfigFilePath = await resolveProjectPluginConfigFilePath(options.cwd);\n const [globalConfig, hasIgnoredProjectConfig] = await Promise.all([\n loadPluginConfigFile(globalConfigFilePath),\n pathExists(projectConfigFilePath),\n ]);\n const config = stripLegacyVoiceConfig(mergePluginConfigSources(globalConfig, options.config));\n const ignoredProjectConfigFilePath = hasIgnoredProjectConfig\n ? projectConfigFilePath\n : undefined;\n const configFilePath = globalConfigFilePath;\n\n return {\n cwd: options.cwd,\n config,\n globalConfigFilePath,\n projectConfigFilePath,\n ...(ignoredProjectConfigFilePath ? { ignoredProjectConfigFilePath } : {}),\n configFilePath,\n };\n}\n\nexport function getOpenCodeConfigDirectory(homeDir: string = homedir()): string {\n return join(homeDir, \".config\", \"opencode\");\n}\n\nexport function getOpenCodeConfigFilePath(homeDir: string = homedir()): string {\n return join(getOpenCodeConfigDirectory(homeDir), OPENCODE_CONFIG_FILE_NAME);\n}\n\nexport function getGlobalPluginConfigFilePath(homeDir: string = homedir()): string {\n return join(\n getOpenCodeConfigDirectory(homeDir),\n GLOBAL_PLUGIN_DIRECTORY_NAME,\n GLOBAL_PLUGIN_CONFIG_FILE_NAME,\n );\n}\n\nexport async function writePluginConfigFile(\n configFilePath: string,\n config: PluginConfigSource,\n): Promise<void> {\n await mkdir(dirname(configFilePath), { recursive: true });\n await writeFile(configFilePath, serializePluginConfig(config), \"utf8\");\n}\n\nexport function mergePluginConfigSources(\n ...sources: Array<PluginConfigSource | undefined>\n): PluginConfigSource {\n const merged: PluginConfigSource = {};\n\n for (const source of sources) {\n if (!source) {\n continue;\n }\n\n const normalized = source;\n const previousTelegram = merged.telegram;\n const previousState = merged.state;\n\n Object.assign(merged, normalized);\n\n if (normalized.telegram) {\n merged.telegram = {\n ...(previousTelegram ?? {}),\n ...normalized.telegram,\n };\n }\n\n if (normalized.state) {\n merged.state = {\n ...(previousState ?? {}),\n ...normalized.state,\n };\n }\n }\n\n return merged;\n}\n\nexport function serializePluginConfig(config: PluginConfigSource): string {\n return `${JSON.stringify(orderPluginConfig(config), null, 2)}\\n`;\n}\n\nasync function loadPluginConfigFile(configFilePath: string): Promise<PluginConfigSource> {\n try {\n const content = await readFile(configFilePath, \"utf8\");\n\n return parsePluginConfigText(content, configFilePath);\n } catch (error) {\n if (isMissingFileError(error)) {\n return {};\n }\n\n throw error;\n }\n}\n\nfunction parsePluginConfigText(\n content: string,\n configFilePath: string,\n): PluginConfigSource {\n try {\n const parsed = JSON.parse(content) as unknown;\n\n if (!isPlainObject(parsed)) {\n throw new Error(\"Config root must be a JSON object.\");\n }\n\n return parsed as PluginConfigSource;\n } catch (error) {\n throw new Error(\n [\n `Failed to parse ${configFilePath} as JSON.`,\n error instanceof Error ? error.message : String(error),\n ].join(\" \"),\n );\n }\n}\n\nfunction orderPluginConfig(config: PluginConfigSource): PluginConfigSource {\n const prioritizedKeys = new Set([\n \"telegram\",\n \"state\",\n \"logLevel\",\n ]);\n const ordered: PluginConfigSource = {};\n\n if (config.telegram) {\n ordered.telegram = config.telegram;\n }\n\n if (config.state) {\n ordered.state = config.state;\n }\n\n if (config.logLevel !== undefined) {\n ordered.logLevel = config.logLevel;\n }\n\n for (const [key, value] of Object.entries(config)) {\n if (!prioritizedKeys.has(key)) {\n ordered[key] = value;\n }\n }\n\n return ordered;\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === \"object\" && !Array.isArray(value);\n}\n\nfunction isMissingFileError(error: unknown): error is NodeJS.ErrnoException {\n return error instanceof Error && \"code\" in error && error.code === \"ENOENT\";\n}\n\nasync function resolveProjectPluginConfigFilePath(cwd: string): Promise<string> {\n const preferredPath = join(cwd, PLUGIN_CONFIG_FILE_NAME);\n\n return preferredPath;\n}\n\nasync function pathExists(filePath: string): Promise<boolean> {\n try {\n await access(filePath);\n return true;\n } catch (error) {\n if (isMissingFileError(error)) {\n return false;\n }\n\n throw error;\n }\n}\n\nfunction stripLegacyVoiceConfig(config: PluginConfigSource): PluginConfigSource {\n const { openrouter: _openrouter, ...rest } = config as PluginConfigSource & {\n openrouter?: unknown;\n };\n\n return rest;\n}\n"],"mappings":"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"}
package/dist/cli.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as writePluginConfigFile, n as getOpenCodeConfigFilePath, o as OPENCODE_TBOT_VERSION, r as mergePluginConfigSources, t as getGlobalPluginConfigFilePath } from "./assets/plugin-config-B8ginwol.js";
1
+ import { a as writePluginConfigFile, n as getOpenCodeConfigFilePath, o as OPENCODE_TBOT_VERSION, r as mergePluginConfigSources, t as getGlobalPluginConfigFilePath } from "./assets/plugin-config-CCeFjxSf.js";
2
2
  import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
3
3
  import { homedir } from "node:os";
4
4
  import { dirname, join, resolve } from "node:path";
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
- import "./assets/plugin-config-B8ginwol.js";
1
+ import "./assets/plugin-config-CCeFjxSf.js";
2
2
  import { TelegramBotPlugin, ensureTelegramBotPluginRuntime, resetTelegramBotPluginRuntimeForTests } from "./plugin.js";
3
3
  export { TelegramBotPlugin, TelegramBotPlugin as default, ensureTelegramBotPluginRuntime, resetTelegramBotPluginRuntimeForTests };
package/dist/plugin.js CHANGED
@@ -1,4 +1,4 @@
1
- import { c as loadAppConfig, i as preparePluginConfiguration, o as OPENCODE_TBOT_VERSION } from "./assets/plugin-config-B8ginwol.js";
1
+ import { c as loadAppConfig, i as preparePluginConfiguration, o as OPENCODE_TBOT_VERSION } from "./assets/plugin-config-CCeFjxSf.js";
2
2
  import { mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
3
3
  import { dirname, isAbsolute, join } from "node:path";
4
4
  import { parse, printParseErrorCode } from "jsonc-parser";
@@ -283,6 +283,24 @@ var OpenCodeClient = class {
283
283
  });
284
284
  return unwrapSdkData(await this.client.session.abort({ sessionID: sessionId }, SDK_OPTIONS));
285
285
  }
286
+ async deleteSession(sessionId) {
287
+ if (hasRawSdkMethod(this.client, "delete")) return this.requestRaw("delete", {
288
+ url: "/session/{sessionID}",
289
+ path: { sessionID: sessionId }
290
+ });
291
+ return unwrapSdkData(await this.client.session.delete({ sessionID: sessionId }, SDK_OPTIONS));
292
+ }
293
+ async forkSession(sessionId, messageId) {
294
+ if (hasRawSdkMethod(this.client, "post")) return this.requestRaw("post", {
295
+ url: "/session/{sessionID}/fork",
296
+ path: { sessionID: sessionId },
297
+ ...messageId?.trim() ? { body: { messageID: messageId.trim() } } : {}
298
+ });
299
+ return unwrapSdkData(await this.client.session.fork({
300
+ sessionID: sessionId,
301
+ ...messageId?.trim() ? { messageID: messageId.trim() } : {}
302
+ }, SDK_OPTIONS));
303
+ }
286
304
  async getPath() {
287
305
  if (hasRawSdkMethod(this.client, "get")) return this.requestRaw("get", { url: "/path" });
288
306
  return unwrapSdkData(await this.client.path.get(void 0, SDK_OPTIONS));
@@ -398,16 +416,16 @@ var OpenCodeClient = class {
398
416
  }
399
417
  async resolvePromptResponse(input, data, knownMessageIds, startedAt) {
400
418
  const structured = input.structured ?? false;
401
- if (!shouldPollPromptMessage(data, structured)) return data;
402
- const messageId = extractMessageId(data.info);
419
+ if (data && !shouldPollPromptMessage(data, structured)) return data;
420
+ const messageId = data ? extractMessageId(data.info) : null;
403
421
  const candidateOptions = {
404
422
  initialMessageId: messageId,
405
- initialParentId: toAssistantMessage(data.info)?.parentID ?? null,
423
+ initialParentId: data ? toAssistantMessage(data.info)?.parentID ?? null : null,
406
424
  knownMessageIds,
407
425
  requestStartedAt: resolvePromptCandidateStartTime(startedAt, data),
408
426
  structured
409
427
  };
410
- let bestCandidate = selectPromptResponseCandidate([data], candidateOptions) ?? data;
428
+ let bestCandidate = selectPromptResponseCandidate(data ? [data] : [], candidateOptions);
411
429
  const deadlineAt = Date.now() + this.promptRequestTimeouts.totalPollMs;
412
430
  let idleStatusSeen = false;
413
431
  let attempt = 0;
@@ -422,14 +440,16 @@ var OpenCodeClient = class {
422
440
  if (messageId) {
423
441
  const next = await this.fetchPromptMessage(input.sessionId, messageId);
424
442
  if (next) {
425
- bestCandidate = selectPromptResponseCandidate([bestCandidate, next], candidateOptions) ?? bestCandidate;
426
- if (!shouldPollPromptMessage(next, structured)) return bestCandidate;
443
+ const nextCandidate = selectPromptResponseCandidate([bestCandidate, next], candidateOptions);
444
+ if (nextCandidate) bestCandidate = nextCandidate;
445
+ if (bestCandidate && !shouldPollPromptMessage(bestCandidate, structured)) return bestCandidate;
427
446
  }
428
447
  }
429
448
  const latest = await this.findLatestPromptResponse(input.sessionId, candidateOptions, "poll-messages");
430
449
  if (latest) {
431
- bestCandidate = selectPromptResponseCandidate([bestCandidate, latest], candidateOptions) ?? bestCandidate;
432
- if (!shouldPollPromptMessage(bestCandidate, structured)) return bestCandidate;
450
+ const nextCandidate = selectPromptResponseCandidate([bestCandidate, latest], candidateOptions);
451
+ if (nextCandidate) bestCandidate = nextCandidate;
452
+ if (bestCandidate && !shouldPollPromptMessage(bestCandidate, structured)) return bestCandidate;
433
453
  }
434
454
  if ((await this.fetchPromptSessionStatus(input.sessionId))?.type === "idle") {
435
455
  if (idleStatusSeen) break;
@@ -438,8 +458,8 @@ var OpenCodeClient = class {
438
458
  if (Date.now() >= deadlineAt) break;
439
459
  }
440
460
  const latest = await this.findLatestPromptResponse(input.sessionId, candidateOptions, "final-scan");
441
- const resolved = selectPromptResponseCandidate([bestCandidate, latest], candidateOptions) ?? bestCandidate;
442
- if (shouldPollPromptMessage(resolved, structured)) {
461
+ const resolved = selectPromptResponseCandidate([bestCandidate, latest], candidateOptions);
462
+ if (!resolved || shouldPollPromptMessage(resolved, structured)) {
443
463
  const error = createOpenCodePromptTimeoutError({
444
464
  sessionId: input.sessionId,
445
465
  stage: "final-scan",
@@ -568,7 +588,31 @@ var OpenCodeClient = class {
568
588
  return unwrapSdkData(await this.client.config.providers(void 0, SDK_OPTIONS));
569
589
  }
570
590
  async sendPromptRequest(input, parts) {
591
+ const requestBody = {
592
+ ...input.agent ? { agent: input.agent } : {},
593
+ ...input.structured ? { format: STRUCTURED_REPLY_SCHEMA } : {},
594
+ ...input.model ? { model: input.model } : {},
595
+ ...input.variant ? { variant: input.variant } : {},
596
+ parts
597
+ };
598
+ const requestParameters = {
599
+ sessionID: input.sessionId,
600
+ ...requestBody
601
+ };
571
602
  try {
603
+ if (typeof this.client.session?.promptAsync === "function") {
604
+ await this.runPromptRequestWithTimeout({
605
+ sessionId: input.sessionId,
606
+ stage: "send-prompt",
607
+ timeoutMs: this.promptRequestTimeouts.sendMs
608
+ }, async (signal) => {
609
+ await this.client.session.promptAsync(requestParameters, {
610
+ ...SDK_OPTIONS,
611
+ signal
612
+ });
613
+ });
614
+ return null;
615
+ }
572
616
  return await this.runPromptRequestWithTimeout({
573
617
  sessionId: input.sessionId,
574
618
  stage: "send-prompt",
@@ -577,23 +621,10 @@ var OpenCodeClient = class {
577
621
  if (hasRawSdkMethod(this.client, "post")) return normalizePromptResponse(await this.requestRaw("post", {
578
622
  url: "/session/{sessionID}/message",
579
623
  path: { sessionID: input.sessionId },
580
- body: {
581
- ...input.agent ? { agent: input.agent } : {},
582
- ...input.structured ? { format: STRUCTURED_REPLY_SCHEMA } : {},
583
- ...input.model ? { model: input.model } : {},
584
- ...input.variant ? { variant: input.variant } : {},
585
- parts
586
- },
624
+ body: requestBody,
587
625
  signal
588
626
  }));
589
- return normalizePromptResponse(unwrapSdkData(await this.client.session.prompt({
590
- sessionID: input.sessionId,
591
- ...input.agent ? { agent: input.agent } : {},
592
- ...input.structured ? { format: STRUCTURED_REPLY_SCHEMA } : {},
593
- ...input.model ? { model: input.model } : {},
594
- ...input.variant ? { variant: input.variant } : {},
595
- parts
596
- }, {
627
+ return normalizePromptResponse(unwrapSdkData(await this.client.session.prompt(requestParameters, {
597
628
  ...SDK_OPTIONS,
598
629
  signal
599
630
  })));
@@ -952,6 +983,7 @@ function getPromptResponseCandidateRank(message, options) {
952
983
  };
953
984
  }
954
985
  function resolvePromptCandidateStartTime(startedAt, initialMessage) {
986
+ if (!initialMessage) return null;
955
987
  const initialCreatedAt = coerceFiniteNumber(toAssistantMessage(initialMessage.info)?.time?.created);
956
988
  if (initialCreatedAt === null) return startedAt;
957
989
  return areComparablePromptTimestamps(startedAt, initialCreatedAt) ? startedAt : initialCreatedAt;
@@ -1132,29 +1164,73 @@ function extractErrorMessage(error) {
1132
1164
  //#endregion
1133
1165
  //#region src/services/session-activity/foreground-session-tracker.ts
1134
1166
  var ForegroundSessionTracker = class {
1167
+ chatStacks = /* @__PURE__ */ new Map();
1135
1168
  counts = /* @__PURE__ */ new Map();
1136
- begin(sessionId) {
1169
+ sessionChats = /* @__PURE__ */ new Map();
1170
+ begin(chatId, sessionId) {
1137
1171
  const currentCount = this.counts.get(sessionId) ?? 0;
1138
1172
  this.counts.set(sessionId, currentCount + 1);
1173
+ this.incrementChat(chatId, sessionId);
1139
1174
  return () => {
1140
- this.decrement(sessionId);
1175
+ this.decrement(chatId, sessionId);
1141
1176
  };
1142
1177
  }
1143
1178
  clear(sessionId) {
1144
1179
  const wasForeground = this.counts.has(sessionId);
1180
+ const chatCounts = this.sessionChats.get(sessionId);
1145
1181
  this.counts.delete(sessionId);
1182
+ this.sessionChats.delete(sessionId);
1183
+ for (const chatId of chatCounts?.keys() ?? []) {
1184
+ const stack = this.chatStacks.get(chatId);
1185
+ if (!stack) continue;
1186
+ const nextStack = stack.filter((trackedSessionId) => trackedSessionId !== sessionId);
1187
+ if (nextStack.length === 0) {
1188
+ this.chatStacks.delete(chatId);
1189
+ continue;
1190
+ }
1191
+ this.chatStacks.set(chatId, nextStack);
1192
+ }
1146
1193
  return wasForeground;
1147
1194
  }
1195
+ getActiveSessionId(chatId) {
1196
+ return this.chatStacks.get(chatId)?.at(-1) ?? null;
1197
+ }
1148
1198
  isForeground(sessionId) {
1149
1199
  return this.counts.has(sessionId);
1150
1200
  }
1151
- decrement(sessionId) {
1201
+ listChatIds(sessionId) {
1202
+ return [...this.sessionChats.get(sessionId)?.keys() ?? []];
1203
+ }
1204
+ decrement(chatId, sessionId) {
1152
1205
  const currentCount = this.counts.get(sessionId);
1153
- if (!currentCount || currentCount <= 1) {
1154
- this.counts.delete(sessionId);
1155
- return;
1206
+ if (!currentCount || currentCount <= 1) this.counts.delete(sessionId);
1207
+ else this.counts.set(sessionId, currentCount - 1);
1208
+ this.decrementChat(chatId, sessionId);
1209
+ }
1210
+ incrementChat(chatId, sessionId) {
1211
+ const stack = this.chatStacks.get(chatId) ?? [];
1212
+ stack.push(sessionId);
1213
+ this.chatStacks.set(chatId, stack);
1214
+ const chatCounts = this.sessionChats.get(sessionId) ?? /* @__PURE__ */ new Map();
1215
+ const currentCount = chatCounts.get(chatId) ?? 0;
1216
+ chatCounts.set(chatId, currentCount + 1);
1217
+ this.sessionChats.set(sessionId, chatCounts);
1218
+ }
1219
+ decrementChat(chatId, sessionId) {
1220
+ const stack = this.chatStacks.get(chatId);
1221
+ if (stack) {
1222
+ const index = stack.lastIndexOf(sessionId);
1223
+ if (index >= 0) stack.splice(index, 1);
1224
+ if (stack.length === 0) this.chatStacks.delete(chatId);
1225
+ else this.chatStacks.set(chatId, stack);
1156
1226
  }
1157
- this.counts.set(sessionId, currentCount - 1);
1227
+ const chatCounts = this.sessionChats.get(sessionId);
1228
+ if (!chatCounts) return;
1229
+ const currentCount = chatCounts.get(chatId) ?? 0;
1230
+ if (currentCount <= 1) chatCounts.delete(chatId);
1231
+ else chatCounts.set(chatId, currentCount - 1);
1232
+ if (chatCounts.size === 0) this.sessionChats.delete(sessionId);
1233
+ else this.sessionChats.set(sessionId, chatCounts);
1158
1234
  }
1159
1235
  };
1160
1236
  var NOOP_FOREGROUND_SESSION_TRACKER = {
@@ -1164,25 +1240,33 @@ var NOOP_FOREGROUND_SESSION_TRACKER = {
1164
1240
  clear() {
1165
1241
  return false;
1166
1242
  },
1243
+ getActiveSessionId() {
1244
+ return null;
1245
+ },
1167
1246
  isForeground() {
1168
1247
  return false;
1248
+ },
1249
+ listChatIds() {
1250
+ return [];
1169
1251
  }
1170
1252
  };
1171
1253
  //#endregion
1172
1254
  //#region src/use-cases/abort-prompt.usecase.ts
1173
1255
  var AbortPromptUseCase = class {
1174
- constructor(sessionRepo, opencodeClient) {
1256
+ constructor(sessionRepo, opencodeClient, foregroundSessionTracker = NOOP_FOREGROUND_SESSION_TRACKER) {
1175
1257
  this.sessionRepo = sessionRepo;
1176
1258
  this.opencodeClient = opencodeClient;
1259
+ this.foregroundSessionTracker = foregroundSessionTracker;
1177
1260
  }
1178
1261
  async execute(input) {
1179
- const binding = await this.sessionRepo.getByChatId(input.chatId);
1180
- if (!binding?.sessionId) return {
1262
+ const activeSessionId = this.foregroundSessionTracker.getActiveSessionId(input.chatId);
1263
+ const binding = activeSessionId ? null : await this.sessionRepo.getByChatId(input.chatId);
1264
+ const sessionId = activeSessionId ?? binding?.sessionId ?? null;
1265
+ if (!sessionId) return {
1181
1266
  sessionId: null,
1182
1267
  status: "no_session",
1183
1268
  sessionStatus: null
1184
1269
  };
1185
- const sessionId = binding.sessionId;
1186
1270
  const sessionStatus = (await this.opencodeClient.getSessionStatuses())[sessionId] ?? null;
1187
1271
  if (!sessionStatus || sessionStatus.type === "idle") return {
1188
1272
  sessionId,
@@ -1679,6 +1763,7 @@ var SendPromptUseCase = class {
1679
1763
  }
1680
1764
  if (!binding || !binding.sessionId || !binding.projectId) throw new Error("Failed to initialize chat session.");
1681
1765
  let activeBinding = binding;
1766
+ const shouldIsolateImageTurn = hasImageFiles(files);
1682
1767
  const model = activeBinding.modelProviderId && activeBinding.modelId ? {
1683
1768
  providerID: activeBinding.modelProviderId,
1684
1769
  modelID: activeBinding.modelId
@@ -1688,11 +1773,13 @@ var SendPromptUseCase = class {
1688
1773
  activeBinding = await clearStoredAgentSelection(this.sessionRepo, activeBinding);
1689
1774
  this.logger.warn?.({ chatId: input.chatId }, "selected agent is no longer available, falling back to OpenCode default");
1690
1775
  }
1691
- const endForegroundSession = this.foregroundSessionTracker.begin(activeBinding.sessionId);
1776
+ const temporarySessionId = shouldIsolateImageTurn ? await this.createTemporaryImageSession(input.chatId, activeBinding.sessionId) : null;
1777
+ const executionSessionId = temporarySessionId ?? activeBinding.sessionId;
1778
+ const endForegroundSession = this.foregroundSessionTracker.begin(input.chatId, executionSessionId);
1692
1779
  let result;
1693
1780
  try {
1694
1781
  result = await this.opencodeClient.promptSession({
1695
- sessionId: activeBinding.sessionId,
1782
+ sessionId: executionSessionId,
1696
1783
  prompt: promptText,
1697
1784
  ...files.length > 0 ? { files } : {},
1698
1785
  ...selectedAgent ? { agent: selectedAgent.name } : {},
@@ -1702,6 +1789,7 @@ var SendPromptUseCase = class {
1702
1789
  });
1703
1790
  } finally {
1704
1791
  endForegroundSession();
1792
+ if (temporarySessionId) await this.cleanupTemporaryImageSession(input.chatId, activeBinding.sessionId, temporarySessionId);
1705
1793
  }
1706
1794
  await this.sessionRepo.touch(input.chatId);
1707
1795
  return {
@@ -1715,6 +1803,32 @@ var SendPromptUseCase = class {
1715
1803
  this.logger.warn?.({ chatId }, `${reason}, falling back to the current OpenCode project`);
1716
1804
  return nextBinding;
1717
1805
  }
1806
+ async createTemporaryImageSession(chatId, sessionId) {
1807
+ const temporarySession = await this.opencodeClient.forkSession(sessionId);
1808
+ if (!temporarySession.id || temporarySession.id === sessionId) throw new Error("OpenCode did not return a distinct temporary session for the image turn.");
1809
+ this.logger.info?.({
1810
+ chatId,
1811
+ parentSessionId: sessionId,
1812
+ sessionId: temporarySession.id
1813
+ }, "created temporary image session");
1814
+ return temporarySession.id;
1815
+ }
1816
+ async cleanupTemporaryImageSession(chatId, parentSessionId, sessionId) {
1817
+ try {
1818
+ if (!await this.opencodeClient.deleteSession(sessionId)) this.logger.warn?.({
1819
+ chatId,
1820
+ parentSessionId,
1821
+ sessionId
1822
+ }, "failed to delete temporary image session");
1823
+ } catch (error) {
1824
+ this.logger.warn?.({
1825
+ error,
1826
+ chatId,
1827
+ parentSessionId,
1828
+ sessionId
1829
+ }, "failed to delete temporary image session");
1830
+ }
1831
+ }
1718
1832
  };
1719
1833
  function buildPromptText(text, files) {
1720
1834
  const trimmedText = text?.trim() ?? "";
@@ -1731,6 +1845,9 @@ function buildPromptText(text, files) {
1731
1845
  function isImageFile(file) {
1732
1846
  return file.mime.trim().toLowerCase().startsWith("image/");
1733
1847
  }
1848
+ function hasImageFiles(files) {
1849
+ return files.some(isImageFile);
1850
+ }
1734
1851
  //#endregion
1735
1852
  //#region src/use-cases/switch-agent.usecase.ts
1736
1853
  var SwitchAgentUseCase = class {
@@ -1924,7 +2041,7 @@ function createContainer(config, opencodeClient, logger) {
1924
2041
  apiRoot: config.telegramApiRoot
1925
2042
  });
1926
2043
  const uploadFileUseCase = new UploadFileUseCase(telegramFileClient);
1927
- const abortPromptUseCase = new AbortPromptUseCase(sessionRepo, opencodeClient);
2044
+ const abortPromptUseCase = new AbortPromptUseCase(sessionRepo, opencodeClient, foregroundSessionTracker);
1928
2045
  const createSessionUseCase = new CreateSessionUseCase(sessionRepo, opencodeClient, logger);
1929
2046
  const getHealthUseCase = new GetHealthUseCase(opencodeClient);
1930
2047
  const getPathUseCase = new GetPathUseCase(opencodeClient);
@@ -2058,19 +2175,20 @@ async function handleTelegramBotPluginEvent(runtime, event) {
2058
2175
  }
2059
2176
  async function handlePermissionAsked(runtime, request) {
2060
2177
  const bindings = await runtime.container.sessionRepo.listBySessionId(request.sessionID);
2178
+ const chatIds = new Set([...bindings.map((binding) => binding.chatId), ...runtime.container.foregroundSessionTracker.listChatIds(request.sessionID)]);
2061
2179
  const approvals = await runtime.container.permissionApprovalRepo.listByRequestId(request.id);
2062
2180
  const approvedChatIds = new Set(approvals.map((approval) => approval.chatId));
2063
- for (const binding of bindings) {
2064
- if (approvedChatIds.has(binding.chatId)) continue;
2181
+ for (const chatId of chatIds) {
2182
+ if (approvedChatIds.has(chatId)) continue;
2065
2183
  try {
2066
- const message = await runtime.bot.api.sendMessage(binding.chatId, buildPermissionApprovalMessage(request), {
2184
+ const message = await runtime.bot.api.sendMessage(chatId, buildPermissionApprovalMessage(request), {
2067
2185
  parse_mode: "MarkdownV2",
2068
2186
  reply_markup: buildPermissionApprovalKeyboard(request.id)
2069
2187
  });
2070
2188
  await runtime.container.permissionApprovalRepo.set({
2071
2189
  requestId: request.id,
2072
2190
  sessionId: request.sessionID,
2073
- chatId: binding.chatId,
2191
+ chatId,
2074
2192
  messageId: message.message_id,
2075
2193
  status: "pending",
2076
2194
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -2078,7 +2196,7 @@ async function handlePermissionAsked(runtime, request) {
2078
2196
  } catch (error) {
2079
2197
  runtime.container.logger.error({
2080
2198
  error,
2081
- chatId: binding.chatId,
2199
+ chatId,
2082
2200
  requestId: request.id
2083
2201
  }, "failed to deliver permission request to Telegram");
2084
2202
  }
@@ -2104,7 +2222,7 @@ async function handleSessionError(runtime, sessionId, error) {
2104
2222
  runtime.container.logger.error({ error }, "session error received without a session id");
2105
2223
  return;
2106
2224
  }
2107
- if (runtime.container.foregroundSessionTracker.isForeground(sessionId)) {
2225
+ if (runtime.container.foregroundSessionTracker.clear(sessionId)) {
2108
2226
  runtime.container.logger.warn({
2109
2227
  error,
2110
2228
  sessionId
@@ -4679,6 +4797,11 @@ async function startPluginRuntime(options, cwd) {
4679
4797
  });
4680
4798
  const { config, container } = bootstrapApp(options.context.client, preparedConfiguration.config, { cwd: preparedConfiguration.cwd });
4681
4799
  try {
4800
+ if (preparedConfiguration.ignoredProjectConfigFilePath) container.logger.warn({
4801
+ cwd: preparedConfiguration.cwd,
4802
+ ignoredProjectConfigFilePath: preparedConfiguration.ignoredProjectConfigFilePath,
4803
+ globalConfigFilePath: preparedConfiguration.globalConfigFilePath
4804
+ }, "legacy worktree plugin config is ignored; migrate settings to the global opencode-tbot config");
4682
4805
  const runtime = await startRuntime({
4683
4806
  config,
4684
4807
  container
@@ -4686,7 +4809,7 @@ async function startPluginRuntime(options, cwd) {
4686
4809
  container.logger.info({
4687
4810
  cwd: preparedConfiguration.cwd,
4688
4811
  globalConfigFilePath: preparedConfiguration.globalConfigFilePath,
4689
- projectConfigFilePath: preparedConfiguration.projectConfigFilePath,
4812
+ ignoredProjectConfigFilePath: preparedConfiguration.ignoredProjectConfigFilePath,
4690
4813
  configFilePath: preparedConfiguration.configFilePath,
4691
4814
  mode: "plugin"
4692
4815
  }, "telegram bot plugin runtime started");
@@ -4708,7 +4831,9 @@ function createHooks(runtime) {
4708
4831
  await handleTelegramBotPluginEvent(runtime, event);
4709
4832
  },
4710
4833
  async "permission.ask"(input, output) {
4711
- if ((await runtime.container.sessionRepo.listBySessionId(input.sessionID)).length > 0) output.status = "ask";
4834
+ const bindings = await runtime.container.sessionRepo.listBySessionId(input.sessionID);
4835
+ const foregroundChatIds = runtime.container.foregroundSessionTracker.listChatIds(input.sessionID);
4836
+ if (bindings.length > 0 || foregroundChatIds.length > 0) output.status = "ask";
4712
4837
  }
4713
4838
  };
4714
4839
  }