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 +4 -9
- package/README.md +6 -9
- package/README.zh-CN.md +4 -9
- package/dist/assets/{plugin-config-B8ginwol.js → plugin-config-CCeFjxSf.js} +6 -4
- package/dist/assets/plugin-config-CCeFjxSf.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- package/dist/plugin.js +173 -48
- package/dist/plugin.js.map +1 -1
- package/package.json +1 -2
- package/dist/assets/plugin-config-B8ginwol.js.map +0 -1
- package/tbot.config.example.json +0 -11
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
|
-
|
|
68
|
-
2. `<worktree>/tbot.config.json` のプロジェクト上書き設定
|
|
69
|
-
|
|
70
|
-
プロジェクト設定はグローバル設定に上書きマージされます。`telegram` と `state` はセクション単位でディープマージされます。
|
|
67
|
+
古い `<worktree>/tbot.config.json` はランタイムでは無視されます。検出された場合は、値をグローバル設定へ移行できるように警告ログを出します。
|
|
71
68
|
|
|
72
69
|
古い `openrouter` 音声転写設定はランタイムでは無視され、インストーラーが設定を書き直す際にも削除されます。
|
|
73
70
|
|
|
74
|
-
|
|
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
|
|
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
|
|
66
|
+
Runtime config is loaded from `~/.config/opencode/opencode-tbot/config.json`.
|
|
66
67
|
|
|
67
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
68
|
-
2. 项目覆盖配置 `<worktree>/tbot.config.json`
|
|
69
|
-
|
|
70
|
-
项目配置会覆盖全局默认值;`telegram` 和 `state` 会按分段进行深合并。
|
|
67
|
+
遗留的 `<worktree>/tbot.config.json` 会在运行时被忽略;如果检测到该文件,插件会记录一条警告,提示你把其中的值迁移到全局配置。
|
|
71
68
|
|
|
72
69
|
遗留的 `openrouter` 语音转写配置在运行时会被忽略;安装器重写配置时也会自动移除这些字段。
|
|
73
70
|
|
|
74
|
-
|
|
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,请在
|
|
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,
|
|
73
|
-
const config = stripLegacyVoiceConfig(mergePluginConfigSources(globalConfig,
|
|
74
|
-
const
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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)
|
|
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
|
-
|
|
426
|
-
if (
|
|
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
|
-
|
|
432
|
-
if (
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1155
|
-
|
|
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.
|
|
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
|
|
1180
|
-
|
|
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
|
|
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:
|
|
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
|
|
2064
|
-
if (approvedChatIds.has(
|
|
2181
|
+
for (const chatId of chatIds) {
|
|
2182
|
+
if (approvedChatIds.has(chatId)) continue;
|
|
2065
2183
|
try {
|
|
2066
|
-
const message = await runtime.bot.api.sendMessage(
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|