opencode-tbot 0.1.0 → 0.1.2

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.md CHANGED
@@ -17,17 +17,7 @@ A Telegram plugin for driving [OpenCode](https://opencode.ai) from chat.
17
17
  - Session completion and error events can be reported back to the bound Telegram chat.
18
18
  - Chat state is stored in a JSON state file that works in both Node and Bun runtimes.
19
19
 
20
- This repository runs as an OpenCode plugin. There is no standalone bot service.
21
-
22
- ## Installation
23
-
24
- ### Requirements
25
-
26
- - OpenCode must be installed and able to load plugins.
27
- - Node.js 22.12.0 or newer is required for local builds and development.
28
- - You need a Telegram bot token from BotFather.
29
-
30
- ### Recommended Install Flow
20
+ ## Install
31
21
 
32
22
  Run:
33
23
 
@@ -35,38 +25,7 @@ Run:
35
25
  npx opencode-tbot@latest install
36
26
  ```
37
27
 
38
- The installer will:
39
-
40
- 1. register `opencode-tbot@latest` in `~/.config/opencode/opencode.json`
41
- 2. ask for your Telegram bot token
42
- 3. ask whether voice transcription should be enabled
43
- 4. ask for an OpenRouter API key if voice transcription is enabled
44
- 5. write plugin defaults to `~/.config/opencode/opencode-tbot/config.json`
45
-
46
- Restart OpenCode after installation, then message the bot with `/start` or `/status`.
47
-
48
- ### Non-Interactive Install
49
-
50
- For CI or scripted setup:
51
-
52
- ```bash
53
- npx opencode-tbot@latest install \
54
- --bot-token <telegram-token> \
55
- --disable-voice
56
- ```
57
-
58
- If voice transcription is enabled, `--openrouter-api-key` is required.
59
-
60
- Useful flags:
61
-
62
- - `--enable-voice`
63
- - `--disable-voice`
64
- - `--openrouter-api-key <key>`
65
- - `--telegram-api-root <url>`
66
- - `--skip-register`
67
- - `--home-dir <path>`
68
-
69
- `--skip-register` is mainly for tests or local packaging flows where plugin registration is handled separately.
28
+ The installer registers the plugin globally and writes the default runtime config.
70
29
 
71
30
  ## Configuration
72
31
 
@@ -74,7 +33,6 @@ The runtime config is loaded in this order:
74
33
 
75
34
  1. global defaults from `~/.config/opencode/opencode-tbot/config.json`
76
35
  2. project overrides from `<worktree>/tbot.config.json`
77
- 3. legacy project filename `<worktree>/opencode-tbot.config.json` if `tbot.config.json` is absent
78
36
 
79
37
  Project config is merged on top of the global config. `telegram`, `state`, and `openrouter` are deep-merged by section.
80
38
 
@@ -108,25 +66,34 @@ Project config is merged on top of the global config. `telegram`, `state`, and `
108
66
  | `telegram.allowedChatIds` | No | `[]` | Allowed Telegram chat IDs. If empty, the bot accepts messages from any chat. |
109
67
  | `telegram.apiRoot` | No | `https://api.telegram.org` | Telegram Bot API base URL. Useful for tests or self-hosted gateways. |
110
68
  | `state.path` | No | `./data/opencode-tbot.state.json` | JSON state file path, resolved relative to the current OpenCode worktree. |
111
- | `database.path` | Legacy | - | Backward-compatible alias for `state.path`. No sqlite migration is performed. |
112
69
  | `openrouter.apiKey` | No | `""` | OpenRouter API key. Required only when voice transcription is enabled. |
113
70
  | `openrouter.model` | No | `openai/gpt-audio-mini` | OpenRouter model for voice transcription. |
114
71
  | `openrouter.timeoutMs` | No | `30000` | Voice transcription timeout in milliseconds. |
115
72
  | `openrouter.transcriptionPrompt` | No | `""` | Optional extra instruction appended to the transcription prompt. |
116
73
  | `logLevel` | No | `info` | Plugin log level. Logs are emitted through `client.app.log()`. |
117
74
 
118
- ## Features
75
+ ## Runtime Expectations
76
+
77
+ Required:
78
+
79
+ - `telegram.botToken`
80
+
81
+ Optional:
82
+
83
+ - `telegram.allowedChatIds`
84
+ - `telegram.apiRoot`
85
+ - `state.path`
86
+ - `openrouter.apiKey`
87
+ - `openrouter.model`
88
+ - `openrouter.timeoutMs`
89
+ - `openrouter.transcriptionPrompt`
90
+ - `logLevel`
91
+
92
+ Notes:
119
93
 
120
- - Send text prompts to OpenCode from Telegram
121
- - Send Telegram images to OpenCode with optional caption text
122
- - Transcribe Telegram voice messages through OpenRouter
123
- - Create and switch OpenCode sessions in chat
124
- - View and switch agents
125
- - View and switch models and reasoning levels
126
- - Approve or reject OpenCode permission requests from Telegram
127
- - Receive session error and idle notifications in Telegram
128
- - Restrict access with an optional Telegram allowlist
129
- - Persist chat bindings and pending actions in a JSON state file
94
+ - `state.path` defaults to `./data/opencode-tbot.state.json` and is resolved relative to the current OpenCode worktree.
95
+ - Logs are emitted through `client.app.log()`.
96
+ - Permission approvals and session notifications are handled through plugin hooks.
130
97
 
131
98
  ## Commands
132
99
 
@@ -142,7 +109,7 @@ Project config is merged on top of the global config. `telegram`, `state`, and `
142
109
 
143
110
  Any non-command text message is treated as a prompt and sent to OpenCode. Telegram `voice` messages follow the same prompt flow after transcription when OpenRouter is configured. Telegram images are forwarded as OpenCode file parts.
144
111
 
145
- ## Local Development
112
+ ## Development
146
113
 
147
114
  Build the plugin bundle:
148
115
 
@@ -164,17 +131,6 @@ pnpm test
164
131
 
165
132
  For local source-based loading in this repository, OpenCode can use [.opencode/plugins/opencode-tbot.ts](./.opencode/plugins/opencode-tbot.ts), which re-exports `src/plugin.ts`.
166
133
 
167
- ## Deployment
168
-
169
- Supported deployment modes:
170
-
171
- - Recommended npm installation through `npx opencode-tbot@latest install`
172
- - Non-interactive install for CI or scripted environments
173
- - Local development bridge through `.opencode/plugins/opencode-tbot.ts`
174
- - Unpublished external-project bridge that re-exports `dist/plugin.js`
175
-
176
- Deployment details are documented in [docs/deployment.md](./docs/deployment.md).
177
-
178
134
  ## FAQ
179
135
 
180
136
  ### Do I need a running OpenCode instance?
@@ -184,7 +140,3 @@ Yes. This repository provides a Telegram integration layer and depends on the Op
184
140
  ### Is this an official OpenCode project?
185
141
 
186
142
  No. It integrates with OpenCode, but it is not built by the OpenCode team.
187
-
188
- ### Why is Node.js 22 recommended?
189
-
190
- The project uses modern Node APIs for the CLI, tests, and local development. Runtime state storage is file-based and works in both Node and Bun-compatible plugin hosts.
package/README.zh-CN.md CHANGED
@@ -15,58 +15,17 @@
15
15
  - Telegram 语音消息可先通过 OpenRouter 转写,再进入正常 prompt 流程。
16
16
  - OpenCode 触发的权限请求可以直接在 Telegram 中通过内联按钮批准或拒绝。
17
17
  - 会话完成和错误事件可以主动回推到绑定的 Telegram chat。
18
- - 聊天状态通过 JSON 状态文件持久化,可同时兼容 Node 和 Bun 运行环境。
19
-
20
- 当前仓库以 OpenCode 插件形式运行,不再提供独立 bot 进程。
18
+ - 聊天状态通过 JSON 状态文件持久化,可兼容 Node 和 Bun 运行环境。
21
19
 
22
20
  ## 安装
23
21
 
24
- ### 环境要求
25
-
26
- - 需要已安装并可加载插件的 OpenCode。
27
- - 本地构建和开发需要 Node.js 22.12.0 或更高版本。
28
- - 需要一个由 BotFather 创建的 Telegram bot token。
29
-
30
- ### 推荐安装方式
31
-
32
22
  执行:
33
23
 
34
24
  ```bash
35
25
  npx opencode-tbot@latest install
36
26
  ```
37
27
 
38
- 安装器会自动完成以下步骤:
39
-
40
- 1. 在 `~/.config/opencode/opencode.json` 中注册 `opencode-tbot@latest`
41
- 2. 提示输入 Telegram bot token
42
- 3. 询问是否启用语音转写
43
- 4. 如果启用语音转写,则继续提示输入 OpenRouter API key
44
- 5. 将插件全局默认配置写入 `~/.config/opencode/opencode-tbot/config.json`
45
-
46
- 安装完成后重启 OpenCode,再向 bot 发送 `/start` 或 `/status` 验证即可。
47
-
48
- ### 非交互安装
49
-
50
- 适合 CI 或脚本化环境:
51
-
52
- ```bash
53
- npx opencode-tbot@latest install \
54
- --bot-token <telegram-token> \
55
- --disable-voice
56
- ```
57
-
58
- 如果启用了语音转写,则必须同时提供 `--openrouter-api-key`。
59
-
60
- 常用参数:
61
-
62
- - `--enable-voice`
63
- - `--disable-voice`
64
- - `--openrouter-api-key <key>`
65
- - `--telegram-api-root <url>`
66
- - `--skip-register`
67
- - `--home-dir <path>`
68
-
69
- `--skip-register` 主要用于测试或本地打包流程,此时插件注册由外部步骤负责。
28
+ 安装器会注册全局插件并写入默认运行时配置。
70
29
 
71
30
  ## 配置
72
31
 
@@ -74,7 +33,6 @@ npx opencode-tbot@latest install \
74
33
 
75
34
  1. 全局默认配置 `~/.config/opencode/opencode-tbot/config.json`
76
35
  2. 项目覆盖配置 `<worktree>/tbot.config.json`
77
- 3. 如果不存在 `tbot.config.json`,则兼容旧文件名 `<worktree>/opencode-tbot.config.json`
78
36
 
79
37
  项目配置会覆盖全局默认值;`telegram`、`state`、`openrouter` 这些分段配置会进行深合并。
80
38
 
@@ -108,25 +66,34 @@ npx opencode-tbot@latest install \
108
66
  | `telegram.allowedChatIds` | 否 | `[]` | 允许访问的 Telegram chat ID 数组。为空时表示接受任意 chat。 |
109
67
  | `telegram.apiRoot` | 否 | `https://api.telegram.org` | Telegram Bot API 根地址,适合测试或自托管网关。 |
110
68
  | `state.path` | 否 | `./data/opencode-tbot.state.json` | JSON 状态文件路径,相对当前 OpenCode worktree 解析。 |
111
- | `database.path` | 兼容字段 | - | `state.path` 的旧别名,仅做兼容映射,不迁移 sqlite 数据。 |
112
69
  | `openrouter.apiKey` | 否 | `""` | OpenRouter API key。仅在启用语音转写时需要。 |
113
70
  | `openrouter.model` | 否 | `openai/gpt-audio-mini` | 语音转写使用的 OpenRouter 模型。 |
114
71
  | `openrouter.timeoutMs` | 否 | `30000` | 语音转写超时时间,单位毫秒。 |
115
72
  | `openrouter.transcriptionPrompt` | 否 | `""` | 追加到内置转写提示词后的可选说明。 |
116
73
  | `logLevel` | 否 | `info` | 插件日志级别。日志统一通过 `client.app.log()` 上报。 |
117
74
 
118
- ## 功能
75
+ ## 运行时约定
76
+
77
+ 必填:
78
+
79
+ - `telegram.botToken`
80
+
81
+ 可选:
82
+
83
+ - `telegram.allowedChatIds`
84
+ - `telegram.apiRoot`
85
+ - `state.path`
86
+ - `openrouter.apiKey`
87
+ - `openrouter.model`
88
+ - `openrouter.timeoutMs`
89
+ - `openrouter.transcriptionPrompt`
90
+ - `logLevel`
91
+
92
+ 说明:
119
93
 
120
- - Telegram 中向 OpenCode 发送文本 prompt
121
- - Telegram 图片连同可选 caption 一起转发给 OpenCode
122
- - 通过 OpenRouter 转写 Telegram 语音消息
123
- - 在聊天中创建和切换 OpenCode 会话
124
- - 查看和切换 agent
125
- - 查看和切换模型与推理等级
126
- - 在 Telegram 中审批或拒绝 OpenCode 权限请求
127
- - 接收会话错误和空闲通知
128
- - 通过 Telegram allowlist 限制访问
129
- - 使用 JSON 状态文件持久化聊天绑定和待处理动作
94
+ - `state.path` 默认是 `./data/opencode-tbot.state.json`,并相对当前 OpenCode worktree 解析
95
+ - 日志通过 `client.app.log()` 统一输出
96
+ - 权限审批和会话通知由插件 hook 处理
130
97
 
131
98
  ## 命令
132
99
 
@@ -140,9 +107,9 @@ npx opencode-tbot@latest install \
140
107
  - `/model` 或 `/models` 列出可用模型并切换当前模型
141
108
  - `/language` 切换 bot 显示语言
142
109
 
143
- 任意非命令文本都会被当作 prompt 发送给 OpenCode。配置了 OpenRouter 后,Telegram `voice` 消息会先转写再走同一条 prompt 流程。图片会作为 OpenCode 文件片段上传。
110
+ 任意非命令文本都会被当作 prompt 发送给 OpenCode。配置了 OpenRouter 后,Telegram `voice` 消息会先转写再进入同一条 prompt 流程。图片会作为 OpenCode 文件片段上传。
144
111
 
145
- ## 本地开发
112
+ ## 开发
146
113
 
147
114
  构建插件:
148
115
 
@@ -164,17 +131,6 @@ pnpm test
164
131
 
165
132
  在本仓库中做源码调试时,OpenCode 可以通过 [.opencode/plugins/opencode-tbot.ts](./.opencode/plugins/opencode-tbot.ts) 直接加载 `src/plugin.ts`。
166
133
 
167
- ## 部署
168
-
169
- 支持的部署模式:
170
-
171
- - 通过 `npx opencode-tbot@latest install` 的推荐安装流
172
- - 面向 CI 或脚本环境的非交互安装
173
- - 通过 `.opencode/plugins/opencode-tbot.ts` 的本地开发桥接
174
- - 在外部项目中通过 re-export `dist/plugin.js` 的构建产物桥接
175
-
176
- 更完整的部署说明见 [docs/deployment.md](./docs/deployment.md)。
177
-
178
134
  ## 常见问题
179
135
 
180
136
  ### 我需要一个正在运行的 OpenCode 实例吗?
@@ -184,7 +140,3 @@ pnpm test
184
140
  ### 这是 OpenCode 官方项目吗?
185
141
 
186
142
  不是。它只是与 OpenCode 集成,并非 OpenCode 官方项目。
187
-
188
- ### 为什么仍然推荐 Node.js 22?
189
-
190
- CLI、测试和本地开发仍然使用现代 Node API。插件运行时的状态存储已经改为文件方案,可兼容 Node 和 Bun 风格的宿主环境。
@@ -13,20 +13,18 @@ var TelegramConfigSchema = z.preprocess((value) => value ?? {}, z.object({
13
13
  apiRoot: z.string().trim().url().default(DEFAULT_TELEGRAM_API_ROOT)
14
14
  }));
15
15
  var StateConfigSchema = z.preprocess((value) => value ?? {}, z.object({ path: z.string().trim().min(1).default(DEFAULT_STATE_FILE_PATH) }));
16
- var LegacyDatabaseConfigSchema = z.preprocess((value) => value ?? {}, z.object({ path: z.string().trim().min(1).optional() }));
17
16
  var OpenRouterConfigSchema = z.preprocess((value) => value ?? {}, z.object({
18
17
  apiKey: z.string().default(""),
19
18
  model: z.string().default(DEFAULT_OPENROUTER_MODEL),
20
19
  timeoutMs: z.coerce.number().int().positive().default(3e4),
21
20
  transcriptionPrompt: z.string().default("")
22
21
  }));
23
- var AppConfigSchema = z.preprocess(normalizeLegacyConfigSource, z.object({
22
+ var AppConfigSchema = z.object({
24
23
  telegram: TelegramConfigSchema,
25
24
  state: StateConfigSchema,
26
- database: LegacyDatabaseConfigSchema.optional(),
27
25
  openrouter: OpenRouterConfigSchema,
28
26
  logLevel: z.string().default("info")
29
- }));
27
+ });
30
28
  function loadAppConfig(configSource = {}, options = {}) {
31
29
  return buildAppConfig(parseConfig(AppConfigSchema, configSource), options);
32
30
  }
@@ -54,36 +52,20 @@ function normalizeOptionalString(value) {
54
52
  return normalized.length > 0 ? normalized : null;
55
53
  }
56
54
  function resolveStatePath(data, cwd) {
57
- return resolve(cwd, data.state.path || data.database?.path || "./data/opencode-tbot.state.json");
55
+ return resolve(cwd, data.state.path || "./data/opencode-tbot.state.json");
58
56
  }
59
57
  function normalizeApiRoot(value) {
60
58
  const normalized = value.trim();
61
59
  return normalized.endsWith("/") ? normalized.slice(0, -1) : normalized;
62
60
  }
63
- function normalizeLegacyConfigSource(value) {
64
- if (!isPlainObject$1(value)) return value ?? {};
65
- const source = value;
66
- if (source.state?.path || !source.database?.path) return source;
67
- return {
68
- ...source,
69
- state: {
70
- ...source.state ?? {},
71
- path: source.database.path
72
- }
73
- };
74
- }
75
61
  function parseConfig(schema, configSource) {
76
62
  const parsed = schema.safeParse(configSource ?? {});
77
63
  if (parsed.success) return parsed.data;
78
64
  throw new Error(`Invalid plugin configuration: ${JSON.stringify(parsed.error.flatten())}`);
79
65
  }
80
- function isPlainObject$1(value) {
81
- return value !== null && typeof value === "object" && !Array.isArray(value);
82
- }
83
66
  //#endregion
84
67
  //#region src/app/plugin-config.ts
85
68
  var PLUGIN_CONFIG_FILE_NAME = "tbot.config.json";
86
- var LEGACY_PLUGIN_CONFIG_FILE_NAME = "opencode-tbot.config.json";
87
69
  var GLOBAL_PLUGIN_DIRECTORY_NAME = "opencode-tbot";
88
70
  var GLOBAL_PLUGIN_CONFIG_FILE_NAME = "config.json";
89
71
  var OPENCODE_CONFIG_FILE_NAME = "opencode.json";
@@ -118,10 +100,9 @@ function mergePluginConfigSources(...sources) {
118
100
  const merged = {};
119
101
  for (const source of sources) {
120
102
  if (!source) continue;
121
- const normalized = normalizePluginConfigSource(source);
103
+ const normalized = source;
122
104
  const previousTelegram = merged.telegram;
123
105
  const previousState = merged.state;
124
- const previousDatabase = merged.database;
125
106
  const previousOpenRouter = merged.openrouter;
126
107
  Object.assign(merged, normalized);
127
108
  if (normalized.telegram) merged.telegram = {
@@ -132,10 +113,6 @@ function mergePluginConfigSources(...sources) {
132
113
  ...previousState ?? {},
133
114
  ...normalized.state
134
115
  };
135
- if (normalized.database) merged.database = {
136
- ...previousDatabase ?? {},
137
- ...normalized.database
138
- };
139
116
  if (normalized.openrouter) merged.openrouter = {
140
117
  ...previousOpenRouter ?? {},
141
118
  ...normalized.openrouter
@@ -167,14 +144,12 @@ function orderPluginConfig(config) {
167
144
  const prioritizedKeys = new Set([
168
145
  "telegram",
169
146
  "state",
170
- "database",
171
147
  "openrouter",
172
148
  "logLevel"
173
149
  ]);
174
150
  const ordered = {};
175
151
  if (config.telegram) ordered.telegram = config.telegram;
176
152
  if (config.state) ordered.state = config.state;
177
- if (config.database) ordered.database = config.database;
178
153
  if (config.openrouter) ordered.openrouter = config.openrouter;
179
154
  if (config.logLevel !== void 0) ordered.logLevel = config.logLevel;
180
155
  for (const [key, value] of Object.entries(config)) if (!prioritizedKeys.has(key)) ordered[key] = value;
@@ -187,11 +162,7 @@ function isMissingFileError(error) {
187
162
  return error instanceof Error && "code" in error && error.code === "ENOENT";
188
163
  }
189
164
  async function resolveProjectPluginConfigFilePath(cwd) {
190
- const preferredPath = join(cwd, PLUGIN_CONFIG_FILE_NAME);
191
- if (await pathExists(preferredPath)) return preferredPath;
192
- const legacyPath = join(cwd, LEGACY_PLUGIN_CONFIG_FILE_NAME);
193
- if (await pathExists(legacyPath)) return legacyPath;
194
- return preferredPath;
165
+ return join(cwd, PLUGIN_CONFIG_FILE_NAME);
195
166
  }
196
167
  async function pathExists(filePath) {
197
168
  try {
@@ -202,17 +173,7 @@ async function pathExists(filePath) {
202
173
  throw error;
203
174
  }
204
175
  }
205
- function normalizePluginConfigSource(source) {
206
- if (source.state?.path || !source.database?.path) return source;
207
- return {
208
- ...source,
209
- state: {
210
- ...source.state ?? {},
211
- path: source.database.path
212
- }
213
- };
214
- }
215
176
  //#endregion
216
177
  export { writePluginConfigFile as a, preparePluginConfiguration as i, getOpenCodeConfigFilePath as n, DEFAULT_TELEGRAM_API_ROOT as o, mergePluginConfigSources as r, loadAppConfig as s, getGlobalPluginConfigFilePath as t };
217
178
 
218
- //# sourceMappingURL=plugin-config-Crgl_PZz.js.map
179
+ //# sourceMappingURL=plugin-config-BYsYAzvx.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin-config-BYsYAzvx.js","names":[],"sources":["../../src/app/config.ts","../../src/app/plugin-config.ts"],"sourcesContent":["import { resolve } from \"node:path\";\nimport { z } from \"zod\";\n\nexport const DEFAULT_OPENROUTER_MODEL = \"openai/gpt-audio-mini\";\nexport const DEFAULT_STATE_FILE_PATH = \"./data/opencode-tbot.state.json\";\nexport const DEFAULT_TELEGRAM_API_ROOT = \"https://api.telegram.org\";\n\nconst AllowedChatIdSchema = z.union([\n z.number().int(),\n z.string().regex(/^-?\\d+$/u).transform((value) => Number(value)),\n]);\n\nconst TelegramConfigSchema = z.preprocess(\n (value) => value ?? {},\n z.object({\n botToken: z.string().trim().min(1),\n allowedChatIds: z.array(AllowedChatIdSchema).default([]),\n apiRoot: z.string().trim().url().default(DEFAULT_TELEGRAM_API_ROOT),\n }),\n);\n\nconst StateConfigSchema = z.preprocess(\n (value) => value ?? {},\n z.object({\n path: z.string().trim().min(1).default(DEFAULT_STATE_FILE_PATH),\n }),\n);\n\nconst OpenRouterConfigSchema = z.preprocess(\n (value) => value ?? {},\n z.object({\n apiKey: z.string().default(\"\"),\n model: z.string().default(DEFAULT_OPENROUTER_MODEL),\n timeoutMs: z.coerce.number().int().positive().default(30_000),\n transcriptionPrompt: z.string().default(\"\"),\n }),\n);\n\nconst AppConfigSchema = z.object({\n telegram: TelegramConfigSchema,\n state: StateConfigSchema,\n openrouter: OpenRouterConfigSchema,\n logLevel: z.string().default(\"info\"),\n});\n\nexport interface PluginConfigSource {\n telegram?: {\n botToken?: string;\n allowedChatIds?: Array<number | string>;\n apiRoot?: string;\n [key: string]: unknown;\n };\n state?: {\n path?: string;\n [key: string]: unknown;\n };\n openrouter?: {\n apiKey?: string;\n model?: string;\n timeoutMs?: number;\n transcriptionPrompt?: string;\n [key: string]: unknown;\n };\n logLevel?: string;\n [key: string]: unknown;\n}\n\nexport interface AppOpenRouterConfig {\n configured: boolean;\n apiKey: string | null;\n model: string;\n timeoutMs: number;\n transcriptionPrompt: string | null;\n}\n\nexport interface AppConfig {\n telegramBotToken: string;\n telegramAllowedChatIds: number[];\n telegramApiRoot: string;\n logLevel: string;\n stateFilePath: string;\n openrouter: AppOpenRouterConfig;\n}\n\nexport interface LoadAppConfigOptions {\n cwd?: string;\n}\n\nexport function loadAppConfig(\n configSource: PluginConfigSource | undefined = {},\n options: LoadAppConfigOptions = {},\n): AppConfig {\n const parsed = parseConfig(AppConfigSchema, configSource);\n\n return buildAppConfig(parsed, options);\n}\n\nexport const loadPluginConfig = loadAppConfig;\n\nfunction buildAppConfig(\n data: z.infer<typeof AppConfigSchema>,\n options: LoadAppConfigOptions,\n): AppConfig {\n const openRouterApiKey = normalizeOptionalString(data.openrouter.apiKey);\n const openRouterModel = normalizeOptionalString(data.openrouter.model) ?? DEFAULT_OPENROUTER_MODEL;\n const transcriptionPrompt = normalizeOptionalString(data.openrouter.transcriptionPrompt);\n\n return {\n telegramBotToken: data.telegram.botToken,\n telegramAllowedChatIds: data.telegram.allowedChatIds,\n telegramApiRoot: normalizeApiRoot(data.telegram.apiRoot),\n logLevel: data.logLevel,\n stateFilePath: resolveStatePath(data, options.cwd ?? process.cwd()),\n openrouter: {\n configured: !!openRouterApiKey,\n apiKey: openRouterApiKey,\n model: openRouterModel,\n timeoutMs: data.openrouter.timeoutMs,\n transcriptionPrompt,\n },\n };\n}\n\nfunction normalizeOptionalString(value: string): string | null {\n const normalized = value.trim();\n\n return normalized.length > 0 ? normalized : null;\n}\n\nfunction resolveStatePath(\n data: z.infer<typeof AppConfigSchema>,\n cwd: string,\n): string {\n return resolve(cwd, data.state.path || DEFAULT_STATE_FILE_PATH);\n}\n\nfunction normalizeApiRoot(value: string): string {\n const normalized = value.trim();\n\n return normalized.endsWith(\"/\")\n ? normalized.slice(0, -1)\n : normalized;\n}\n\nfunction parseConfig<TSchema extends z.ZodTypeAny>(\n schema: TSchema,\n configSource: PluginConfigSource | undefined,\n): z.infer<TSchema> {\n const parsed = schema.safeParse(configSource ?? {});\n\n if (parsed.success) {\n return parsed.data;\n }\n\n throw new Error(\n `Invalid plugin configuration: ${JSON.stringify(parsed.error.flatten())}`,\n );\n}\n","import { access, mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport type { PluginConfigSource } from \"./config.js\";\n\nexport const PLUGIN_CONFIG_FILE_NAME = \"tbot.config.json\";\nexport const GLOBAL_PLUGIN_DIRECTORY_NAME = \"opencode-tbot\";\nexport const GLOBAL_PLUGIN_CONFIG_FILE_NAME = \"config.json\";\nexport const OPENCODE_CONFIG_FILE_NAME = \"opencode.json\";\n\nexport interface PreparedPluginConfiguration {\n cwd: string;\n config: PluginConfigSource;\n globalConfigFilePath: string;\n projectConfigFilePath: string;\n configFilePath: string;\n}\n\nexport interface PreparePluginConfigurationOptions {\n cwd: string;\n config?: PluginConfigSource;\n homeDir?: string;\n}\n\nexport async function preparePluginConfiguration(\n options: PreparePluginConfigurationOptions,\n): Promise<PreparedPluginConfiguration> {\n const homeDir = options.homeDir ?? homedir();\n const globalConfigFilePath = getGlobalPluginConfigFilePath(homeDir);\n const projectConfigFilePath = await resolveProjectPluginConfigFilePath(options.cwd);\n const [globalConfig, projectConfig] = await Promise.all([\n loadPluginConfigFile(globalConfigFilePath),\n loadPluginConfigFile(projectConfigFilePath),\n ]);\n const config = mergePluginConfigSources(globalConfig, projectConfig, options.config);\n const configFilePath = await pathExists(projectConfigFilePath)\n ? projectConfigFilePath\n : globalConfigFilePath;\n\n return {\n cwd: options.cwd,\n config,\n globalConfigFilePath,\n projectConfigFilePath,\n configFilePath,\n };\n}\n\nexport function getOpenCodeConfigDirectory(homeDir: string = homedir()): string {\n return join(homeDir, \".config\", \"opencode\");\n}\n\nexport function getOpenCodeConfigFilePath(homeDir: string = homedir()): string {\n return join(getOpenCodeConfigDirectory(homeDir), OPENCODE_CONFIG_FILE_NAME);\n}\n\nexport function getGlobalPluginConfigFilePath(homeDir: string = homedir()): string {\n return join(\n getOpenCodeConfigDirectory(homeDir),\n GLOBAL_PLUGIN_DIRECTORY_NAME,\n GLOBAL_PLUGIN_CONFIG_FILE_NAME,\n );\n}\n\nexport async function writePluginConfigFile(\n configFilePath: string,\n config: PluginConfigSource,\n): Promise<void> {\n await mkdir(dirname(configFilePath), { recursive: true });\n await writeFile(configFilePath, serializePluginConfig(config), \"utf8\");\n}\n\nexport function mergePluginConfigSources(\n ...sources: Array<PluginConfigSource | undefined>\n): PluginConfigSource {\n const merged: PluginConfigSource = {};\n\n for (const source of sources) {\n if (!source) {\n continue;\n }\n\n const normalized = source;\n const previousTelegram = merged.telegram;\n const previousState = merged.state;\n const previousOpenRouter = merged.openrouter;\n\n Object.assign(merged, normalized);\n\n if (normalized.telegram) {\n merged.telegram = {\n ...(previousTelegram ?? {}),\n ...normalized.telegram,\n };\n }\n\n if (normalized.state) {\n merged.state = {\n ...(previousState ?? {}),\n ...normalized.state,\n };\n }\n\n if (normalized.openrouter) {\n merged.openrouter = {\n ...(previousOpenRouter ?? {}),\n ...normalized.openrouter,\n };\n }\n }\n\n return merged;\n}\n\nexport function serializePluginConfig(config: PluginConfigSource): string {\n return `${JSON.stringify(orderPluginConfig(config), null, 2)}\\n`;\n}\n\nasync function loadPluginConfigFile(configFilePath: string): Promise<PluginConfigSource> {\n try {\n const content = await readFile(configFilePath, \"utf8\");\n\n return parsePluginConfigText(content, configFilePath);\n } catch (error) {\n if (isMissingFileError(error)) {\n return {};\n }\n\n throw error;\n }\n}\n\nfunction parsePluginConfigText(\n content: string,\n configFilePath: string,\n): PluginConfigSource {\n try {\n const parsed = JSON.parse(content) as unknown;\n\n if (!isPlainObject(parsed)) {\n throw new Error(\"Config root must be a JSON object.\");\n }\n\n return parsed as PluginConfigSource;\n } catch (error) {\n throw new Error(\n [\n `Failed to parse ${configFilePath} as JSON.`,\n error instanceof Error ? error.message : String(error),\n ].join(\" \"),\n );\n }\n}\n\nfunction orderPluginConfig(config: PluginConfigSource): PluginConfigSource {\n const prioritizedKeys = new Set([\n \"telegram\",\n \"state\",\n \"openrouter\",\n \"logLevel\",\n ]);\n const ordered: PluginConfigSource = {};\n\n if (config.telegram) {\n ordered.telegram = config.telegram;\n }\n\n if (config.state) {\n ordered.state = config.state;\n }\n\n if (config.openrouter) {\n ordered.openrouter = config.openrouter;\n }\n\n if (config.logLevel !== undefined) {\n ordered.logLevel = config.logLevel;\n }\n\n for (const [key, value] of Object.entries(config)) {\n if (!prioritizedKeys.has(key)) {\n ordered[key] = value;\n }\n }\n\n return ordered;\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === \"object\" && !Array.isArray(value);\n}\n\nfunction isMissingFileError(error: unknown): error is NodeJS.ErrnoException {\n return error instanceof Error && \"code\" in error && error.code === \"ENOENT\";\n}\n\nasync function resolveProjectPluginConfigFilePath(cwd: string): Promise<string> {\n const preferredPath = join(cwd, PLUGIN_CONFIG_FILE_NAME);\n\n return preferredPath;\n}\n\nasync function pathExists(filePath: string): Promise<boolean> {\n try {\n await access(filePath);\n return true;\n } catch (error) {\n if (isMissingFileError(error)) {\n return false;\n }\n\n throw error;\n }\n}\n"],"mappings":"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"}
package/dist/cli.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as writePluginConfigFile, n as getOpenCodeConfigFilePath, r as mergePluginConfigSources, t as getGlobalPluginConfigFilePath } from "./assets/plugin-config-Crgl_PZz.js";
1
+ import { a as writePluginConfigFile, n as getOpenCodeConfigFilePath, r as mergePluginConfigSources, t as getGlobalPluginConfigFilePath } from "./assets/plugin-config-BYsYAzvx.js";
2
2
  import { mkdir, readFile, writeFile } from "node:fs/promises";
3
3
  import { homedir } from "node:os";
4
4
  import { dirname } from "node:path";
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
- import "./assets/plugin-config-Crgl_PZz.js";
1
+ import "./assets/plugin-config-BYsYAzvx.js";
2
2
  import { TelegramBotPlugin, ensureTelegramBotPluginRuntime, resetTelegramBotPluginRuntimeForTests } from "./plugin.js";
3
3
  export { TelegramBotPlugin, TelegramBotPlugin as default, ensureTelegramBotPluginRuntime, resetTelegramBotPluginRuntimeForTests };