opencode-tbot 0.1.33 → 0.1.35
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 +137 -48
- package/README.md +94 -46
- package/README.zh-CN.md +114 -63
- package/dist/assets/{plugin-config-LIr8LS0-.js → plugin-config-Be3vV2kr.js} +10 -2
- package/dist/assets/plugin-config-Be3vV2kr.js.map +1 -0
- package/dist/cli.js +54 -40
- package/dist/cli.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/plugin.js +41 -18
- package/dist/plugin.js.map +1 -1
- package/package.json +1 -1
- package/dist/assets/plugin-config-LIr8LS0-.js.map +0 -1
package/README.zh-CN.md
CHANGED
|
@@ -4,27 +4,28 @@
|
|
|
4
4
|
|
|
5
5
|
[English](./README.md) | [简体中文](./README.zh-CN.md) | [日本語](./README.ja.md)
|
|
6
6
|
|
|
7
|
-
> 本项目并非由 OpenCode
|
|
7
|
+
> 本项目并非由 OpenCode 官方团队开发,也不隶属于 OpenCode 官方。
|
|
8
8
|
|
|
9
9
|
## 概览
|
|
10
10
|
|
|
11
|
-
`opencode-tbot`
|
|
11
|
+
`opencode-tbot` 让你可以在 Telegram 中直接操作 OpenCode,并为每个聊天维护一份绑定状态。
|
|
12
12
|
|
|
13
|
-
-
|
|
14
|
-
- Telegram
|
|
15
|
-
- 图片轮次会在临时 fork
|
|
16
|
-
- Telegram
|
|
17
|
-
- OpenCode 的权限请求可以直接在 Telegram 中审批。
|
|
13
|
+
- 纯文本消息会转发到当前 OpenCode 会话。
|
|
14
|
+
- Telegram 照片和图片文档会作为 OpenCode 文件片段上传。
|
|
15
|
+
- 图片轮次会在临时 fork 会话中执行,避免后续纯文本对话继承图片上下文。
|
|
16
|
+
- OpenCode 发起的权限请求可以直接在 Telegram 内联按钮里审批或拒绝。
|
|
18
17
|
- 会话错误事件可以回推到已绑定的 Telegram chat。
|
|
19
|
-
-
|
|
18
|
+
- 语音消息会被明确拒绝,并返回本地化提示。
|
|
19
|
+
- 聊天绑定和会话选择状态会保存在 JSON 状态文件中。
|
|
20
20
|
|
|
21
21
|
## 环境要求
|
|
22
22
|
|
|
23
23
|
- 一个正在运行、并会加载该插件的 OpenCode Host 进程。
|
|
24
24
|
- 一个 Telegram bot token。
|
|
25
|
-
- Node.js `>=22.12.0
|
|
25
|
+
- Node.js `>=22.12.0`,用于 CLI 和本地开发。
|
|
26
|
+
- `pnpm`,用于仓库开发。
|
|
26
27
|
|
|
27
|
-
##
|
|
28
|
+
## 安装与更新
|
|
28
29
|
|
|
29
30
|
推荐安装方式:
|
|
30
31
|
|
|
@@ -32,48 +33,72 @@
|
|
|
32
33
|
npm exec --package opencode-tbot@latest opencode-tbot -- install
|
|
33
34
|
```
|
|
34
35
|
|
|
35
|
-
|
|
36
|
+
安装器会:
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
- 写入 `~/.config/opencode/plugins/opencode-tbot.js`
|
|
39
|
+
- 在目录不存在时自动创建所需的父级目录
|
|
40
|
+
- 写入或合并全局插件运行时配置
|
|
41
|
+
- 如果发现旧的 `opencode-tbot` npm 注册,会从全局 OpenCode 配置中清理掉,避免重复加载
|
|
42
|
+
- 打印 OpenCode 配置路径、插件 bridge 路径、插件配置路径和默认插件日志目录
|
|
38
43
|
|
|
39
|
-
|
|
44
|
+
如果 `~/.config/opencode/opencode.jsonc` 或 `~/.config/opencode/opencode.json` 中还残留旧的 `opencode-tbot` npm 注册,安装器会自动清理,只保留全局 bridge 加载方式。
|
|
45
|
+
|
|
46
|
+
查看已安装 CLI 版本:
|
|
40
47
|
|
|
41
48
|
```bash
|
|
42
49
|
npm exec --package opencode-tbot@latest opencode-tbot -- --version
|
|
43
50
|
```
|
|
44
51
|
|
|
45
|
-
|
|
52
|
+
刷新全局插件 bridge,而不改 bot token:
|
|
46
53
|
|
|
47
54
|
```bash
|
|
48
55
|
npm exec --package opencode-tbot@latest opencode-tbot -- update
|
|
49
56
|
```
|
|
50
57
|
|
|
58
|
+
`install` 是默认命令。例如,下面这种写法也适合非交互安装:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
npm exec --package opencode-tbot@latest opencode-tbot -- --bot-token <token>
|
|
62
|
+
```
|
|
63
|
+
|
|
51
64
|
### CLI 参数
|
|
52
65
|
|
|
53
66
|
`install` 支持:
|
|
54
67
|
|
|
55
68
|
- `--bot-token <token>` 非交互写入 Telegram bot token
|
|
56
69
|
- `--telegram-api-root <url>` 覆盖 Telegram Bot API 根地址
|
|
57
|
-
- `--plugin-spec <spec>`
|
|
58
|
-
- `--skip-register`
|
|
70
|
+
- `--plugin-spec <spec>` 已弃用;会被忽略
|
|
71
|
+
- `--skip-register` 只重写插件配置,不修改全局插件 bridge
|
|
59
72
|
- `--home-dir <path>` 使用自定义 home 目录
|
|
60
73
|
|
|
61
74
|
`update` 支持:
|
|
62
75
|
|
|
63
|
-
- `--plugin-spec <spec>`
|
|
76
|
+
- `--plugin-spec <spec>` 已弃用;会被忽略
|
|
64
77
|
- `--home-dir <path>`
|
|
65
78
|
|
|
66
79
|
## 配置
|
|
67
80
|
|
|
68
|
-
|
|
81
|
+
运行时配置只会从以下路径加载:
|
|
82
|
+
|
|
83
|
+
- `~/.config/opencode/opencode-tbot/config.json`
|
|
84
|
+
|
|
85
|
+
OpenCode 会从以下路径加载这个插件的全局 bridge:
|
|
86
|
+
|
|
87
|
+
- `~/.config/opencode/plugins/opencode-tbot.js`
|
|
88
|
+
|
|
89
|
+
旧版 npm 注册迁移时,安装器只会在发现残留配置时清理全局 OpenCode 配置中的以下文件:
|
|
69
90
|
|
|
70
|
-
|
|
91
|
+
- `~/.config/opencode/opencode.jsonc`
|
|
92
|
+
- `~/.config/opencode/opencode.json`
|
|
71
93
|
|
|
72
|
-
|
|
94
|
+
本插件不会从 `.env` 读取运行时配置,请使用全局 JSON 配置文件。
|
|
73
95
|
|
|
74
|
-
|
|
96
|
+
遗留行为说明:
|
|
75
97
|
|
|
76
|
-
|
|
98
|
+
- `<worktree>/tbot.config.json` 会在运行时被忽略;如果检测到,插件会记录警告,提示你迁移配置
|
|
99
|
+
- 遗留的 `openrouter` 语音转写配置会在运行时被忽略;安装器重写配置时也会自动移除
|
|
100
|
+
|
|
101
|
+
### `config.json` 示例
|
|
77
102
|
|
|
78
103
|
```json
|
|
79
104
|
{
|
|
@@ -112,69 +137,77 @@ OpenCode 的插件注册信息保存在全局 OpenCode 配置中:优先使用
|
|
|
112
137
|
|
|
113
138
|
| 字段 | 必填 | 默认值 | 说明 |
|
|
114
139
|
| --- | --- | --- | --- |
|
|
115
|
-
| `telegram.botToken` | 是 | - | Telegram bot token
|
|
140
|
+
| `telegram.botToken` | 是 | - | Telegram bot token。通常由安装器写入全局插件配置。 |
|
|
116
141
|
| `telegram.allowedChatIds` | 否 | `[]` | 允许访问的 Telegram chat ID。为空时接受任意 chat。 |
|
|
117
|
-
| `telegram.apiRoot` | 否 | `https://api.telegram.org` | Telegram Bot API
|
|
142
|
+
| `telegram.apiRoot` | 否 | `https://api.telegram.org` | Telegram Bot API 根地址,适合测试或自建网关。 |
|
|
118
143
|
| `state.path` | 否 | `./data/opencode-tbot.state.json` | JSON 状态文件路径,相对当前 OpenCode worktree 解析。 |
|
|
119
|
-
| `prompt.waitTimeoutMs` | 否 | `1800000` | 单次异步 prompt
|
|
120
|
-
| `prompt.pollRequestTimeoutMs` | 否 | `15000` |
|
|
121
|
-
| `prompt.recoveryInactivityTimeoutMs` | 否 | `120000` |
|
|
122
|
-
| `logging.level` | 否 | `info` |
|
|
144
|
+
| `prompt.waitTimeoutMs` | 否 | `1800000` | 单次异步 prompt 生命周期的总等待上限。 |
|
|
145
|
+
| `prompt.pollRequestTimeoutMs` | 否 | `15000` | 每次向 OpenCode 发起恢复轮询请求时的超时时间。 |
|
|
146
|
+
| `prompt.recoveryInactivityTimeoutMs` | 否 | `120000` | 仅在长时间没有新进展时生效的恢复超时。 |
|
|
147
|
+
| `logging.level` | 否 | `info` | Host 和文件日志共用的结构化日志级别,会规范化为 `debug`、`info`、`warn` 或 `error`。 |
|
|
123
148
|
| `logging.sinks.host` | 否 | `true` | 是否通过 `client.app.log({ body: ... })` 写入 OpenCode Host 日志。 |
|
|
124
149
|
| `logging.sinks.file` | 否 | `true` | 是否写入插件自己的 JSONL 文件日志。 |
|
|
125
|
-
| `logging.file.dir` | 否 | `%USERPROFILE%/.local/share/opencode/log/plugins/opencode-tbot` |
|
|
150
|
+
| `logging.file.dir` | 否 | `%USERPROFILE%/.local/share/opencode/log/plugins/opencode-tbot` | 插件 JSONL 日志目录。相对路径会基于当前 OpenCode worktree 解析。 |
|
|
126
151
|
| `logging.file.retention.maxFiles` | 否 | `30` | 最多保留多少个插件日志文件。 |
|
|
127
152
|
| `logging.file.retention.maxTotalBytes` | 否 | `314572800` | 插件日志文件总大小上限。 |
|
|
128
|
-
| `logLevel` | 否 | `info` | `logging.level`
|
|
153
|
+
| `logLevel` | 否 | `info` | `logging.level` 的兼容别名。仍然可用。 |
|
|
129
154
|
|
|
130
155
|
### 说明
|
|
131
156
|
|
|
132
157
|
- `state.path` 会相对当前 OpenCode worktree 解析。
|
|
133
|
-
-
|
|
158
|
+
- `logging.file.dir` 如果是相对路径,也会相对当前 worktree 解析。
|
|
159
|
+
- Telegram prompt 采用 async-first:插件先提交 `session.promptAsync()`,再通过消息与状态恢复最终回复。
|
|
134
160
|
- 如果 `telegram.allowedChatIds` 为空,bot 会接受任意 chat 的消息;生产环境建议显式限制。
|
|
135
161
|
- 权限审批与会话错误通知通过插件 hooks 处理。
|
|
136
|
-
- `/language` 当前支持 English
|
|
137
|
-
- 默认会双写日志:OpenCode Host 日志 + 插件 JSONL 文件日志。
|
|
162
|
+
- `/language` 当前支持 English、简体中文、日语,并会同步当前 chat 的 Telegram 命令描述。
|
|
138
163
|
- 文件日志默认只记录元数据,不记录 prompt 正文、附件内容、原始 URL 或 secrets。
|
|
139
164
|
|
|
140
|
-
## 日志
|
|
141
|
-
|
|
142
|
-
- OpenCode Host 日志目录:`%USERPROFILE%/.local/share/opencode/log`
|
|
143
|
-
- 插件日志目录:`%USERPROFILE%/.local/share/opencode/log/plugins/opencode-tbot`
|
|
144
|
-
- 文件格式:JSONL,每行一条结构化事件
|
|
145
|
-
- 关键关联字段:`runtimeId`、`operationId`、`correlationId`、`updateId`、`chatId`、`sessionId`、`projectId`
|
|
146
|
-
- 常见组件:`runtime`、`telegram`、`opencode`、`prompt`、`plugin-event`、`storage`
|
|
147
|
-
- 默认保留策略:最多 30 个文件,总大小不超过 300 MB
|
|
148
|
-
|
|
149
165
|
## 快速开始
|
|
150
166
|
|
|
151
167
|
1. 运行 `npm exec --package opencode-tbot@latest opencode-tbot -- install`。
|
|
152
|
-
2.
|
|
153
|
-
3. 在目标 worktree 中启动 OpenCode
|
|
168
|
+
2. 如果需要限制可访问 chat,在 `~/.config/opencode/opencode-tbot/config.json` 中设置 `telegram.allowedChatIds`。
|
|
169
|
+
3. 在目标 worktree 中启动 OpenCode,让插件运行时被加载。
|
|
154
170
|
4. 在 Telegram 中执行 `/status` 验证连通性。
|
|
155
|
-
5.
|
|
171
|
+
5. 运行 `/new [title]`,或直接发送文本消息开始使用。
|
|
156
172
|
|
|
157
173
|
## 命令
|
|
158
174
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
175
|
+
| 命令 | 作用 |
|
|
176
|
+
| --- | --- |
|
|
177
|
+
| `/start` | 显示欢迎消息和快速开始说明。 |
|
|
178
|
+
| `/status` | 汇总展示 OpenCode 健康状态、版本、工作区路径、插件列表、LSP 状态和 MCP 状态。 |
|
|
179
|
+
| `/new [title]` | 在当前项目中创建一个新的 OpenCode 会话。 |
|
|
180
|
+
| `/agents` 或 `/agent` | 列出可用 Agent 并切换当前 Agent。 |
|
|
181
|
+
| `/sessions` | 列出可用会话、切换当前会话,并提供重命名操作。 |
|
|
182
|
+
| `/cancel` | 取消待输入的会话重命名,或中止当前会话正在运行的请求。 |
|
|
183
|
+
| `/model` 或 `/models` | 列出可用模型、切换当前模型,并在可用时选择推理级别。 |
|
|
184
|
+
| `/language` | 切换当前 chat 的 bot 显示语言。 |
|
|
167
185
|
|
|
168
|
-
|
|
186
|
+
### 消息行为
|
|
169
187
|
|
|
170
188
|
- 非命令文本会作为 prompt 发送给 OpenCode。
|
|
171
|
-
- Telegram
|
|
172
|
-
-
|
|
173
|
-
- `/cancel` 会同时中止 OpenCode
|
|
174
|
-
- Telegram
|
|
189
|
+
- Telegram 照片和图片文档会作为 OpenCode 文件片段转发。
|
|
190
|
+
- 图片附件会在当前会话的临时 fork 中处理,避免污染后续纯文本上下文。
|
|
191
|
+
- `/cancel` 会同时中止 OpenCode 会话请求和本地 Telegram 等待状态,让下一次 prompt 能立即开始。
|
|
192
|
+
- Telegram 语音消息暂不支持,会收到本地化拒绝提示。
|
|
193
|
+
|
|
194
|
+
## 日志
|
|
195
|
+
|
|
196
|
+
- OpenCode Host 日志目录:`%USERPROFILE%/.local/share/opencode/log`
|
|
197
|
+
- 插件日志目录:`%USERPROFILE%/.local/share/opencode/log/plugins/opencode-tbot`
|
|
198
|
+
- 文件格式:追加写入的 JSONL,每行一个结构化事件
|
|
199
|
+
- 关联字段:`runtimeId`、`operationId`、`correlationId`、`updateId`、`chatId`、`sessionId`、`projectId`
|
|
200
|
+
- 常见组件:`runtime`、`telegram`、`opencode`、`prompt`、`plugin-event`、`storage`
|
|
201
|
+
- 默认保留策略:最多保留最新 30 个文件,且总大小不超过 300 MB
|
|
175
202
|
|
|
176
203
|
## 开发
|
|
177
204
|
|
|
205
|
+
安装依赖:
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
pnpm install
|
|
209
|
+
```
|
|
210
|
+
|
|
178
211
|
构建:
|
|
179
212
|
|
|
180
213
|
```bash
|
|
@@ -187,13 +220,27 @@ pnpm build
|
|
|
187
220
|
pnpm typecheck
|
|
188
221
|
```
|
|
189
222
|
|
|
190
|
-
|
|
223
|
+
运行默认测试集:
|
|
191
224
|
|
|
192
225
|
```bash
|
|
193
226
|
pnpm test
|
|
194
227
|
```
|
|
195
228
|
|
|
196
|
-
|
|
229
|
+
运行最接近 CI 的集成路径:
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
npm install -g opencode-ai
|
|
233
|
+
OPENCODE_HOST_E2E=1 pnpm test
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
PowerShell 写法:
|
|
237
|
+
|
|
238
|
+
```powershell
|
|
239
|
+
npm install -g opencode-ai
|
|
240
|
+
$env:OPENCODE_HOST_E2E="1"; pnpm test
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
正常使用应依赖自动生成的全局 bridge `~/.config/opencode/plugins/opencode-tbot.js` 加上全局插件配置。本仓库不再默认提供项目级 `.opencode/plugins` bridge。
|
|
197
244
|
|
|
198
245
|
如果你在本仓库里做源码调试,需要手动创建 `.opencode/plugins/opencode-tbot.ts`:
|
|
199
246
|
|
|
@@ -201,14 +248,18 @@ pnpm test
|
|
|
201
248
|
export { default } from "../../src/plugin.ts";
|
|
202
249
|
```
|
|
203
250
|
|
|
204
|
-
同一时间只保留一种加载方式即可:要么使用手动创建的本地 bridge
|
|
251
|
+
同一时间只保留一种加载方式即可:要么使用手动创建的本地 bridge,要么使用 `~/.config/opencode/plugins/` 下自动生成的全局 bridge。
|
|
205
252
|
|
|
206
253
|
## FAQ
|
|
207
254
|
|
|
208
255
|
### 需要一个正在运行的 OpenCode 实例吗?
|
|
209
256
|
|
|
210
|
-
|
|
257
|
+
需要。本仓库只提供 Telegram 集成层,依赖加载该插件的 OpenCode Host 进程。
|
|
258
|
+
|
|
259
|
+
### 为什么生成出来的 bridge 可能会指向 `file:///.../node_modules/...`?
|
|
260
|
+
|
|
261
|
+
这通常表示你是在某个已经把 `opencode-tbot` 安装进本地 `node_modules` 的项目里运行了安装器,所以生成的 bridge 指向了那个包路径。CLI 检测到这种布局时会给出警告。请在那个项目里执行 `npm uninstall opencode-tbot` 移除本地包,然后改用推荐的 `npm exec --package ...` 安装流程。
|
|
211
262
|
|
|
212
263
|
### 这是 OpenCode 官方项目吗?
|
|
213
264
|
|
|
214
|
-
不是。它集成 OpenCode
|
|
265
|
+
不是。它集成 OpenCode,但并非 OpenCode 官方团队开发。
|
|
@@ -7,6 +7,8 @@ import { fileURLToPath } from "node:url";
|
|
|
7
7
|
//#region src/app/opencode-paths.ts
|
|
8
8
|
var GLOBAL_PLUGIN_DIRECTORY_NAME = "opencode-tbot";
|
|
9
9
|
var GLOBAL_PLUGIN_CONFIG_FILE_NAME = "config.json";
|
|
10
|
+
var GLOBAL_PLUGINS_DIRECTORY_NAME = "plugins";
|
|
11
|
+
var GLOBAL_PLUGIN_BRIDGE_FILE_NAME = "opencode-tbot.js";
|
|
10
12
|
var OPENCODE_CONFIG_FILE_NAME = "opencode.json";
|
|
11
13
|
var OPENCODE_CONFIG_JSONC_FILE_NAME = "opencode.jsonc";
|
|
12
14
|
function getOpenCodeConfigDirectory(homeDir = homedir()) {
|
|
@@ -28,6 +30,12 @@ async function resolveWritableOpenCodeConfigFilePath(homeDir = homedir()) {
|
|
|
28
30
|
function getGlobalPluginConfigFilePath(homeDir = homedir()) {
|
|
29
31
|
return join(getOpenCodeConfigDirectory(homeDir), GLOBAL_PLUGIN_DIRECTORY_NAME, GLOBAL_PLUGIN_CONFIG_FILE_NAME);
|
|
30
32
|
}
|
|
33
|
+
function getGlobalPluginsDirectory(homeDir = homedir()) {
|
|
34
|
+
return join(getOpenCodeConfigDirectory(homeDir), GLOBAL_PLUGINS_DIRECTORY_NAME);
|
|
35
|
+
}
|
|
36
|
+
function getGlobalPluginBridgeFilePath(homeDir = homedir()) {
|
|
37
|
+
return join(getGlobalPluginsDirectory(homeDir), GLOBAL_PLUGIN_BRIDGE_FILE_NAME);
|
|
38
|
+
}
|
|
31
39
|
function getOpenCodeLogDirectory(homeDir = homedir()) {
|
|
32
40
|
return join(homeDir, ".local", "share", "opencode", "log");
|
|
33
41
|
}
|
|
@@ -285,6 +293,6 @@ function stripLegacyVoiceConfig(config) {
|
|
|
285
293
|
return rest;
|
|
286
294
|
}
|
|
287
295
|
//#endregion
|
|
288
|
-
export { DEFAULT_TELEGRAM_API_ROOT as a,
|
|
296
|
+
export { DEFAULT_TELEGRAM_API_ROOT as a, getGlobalPluginBridgeFilePath as c, OPENCODE_TBOT_VERSION as i, getGlobalPluginConfigFilePath as l, preparePluginConfiguration as n, loadAppConfig as o, writePluginConfigFile as r, getDefaultPluginLogDirectory as s, mergePluginConfigSources as t, resolveWritableOpenCodeConfigFilePath as u };
|
|
289
297
|
|
|
290
|
-
//# sourceMappingURL=plugin-config-
|
|
298
|
+
//# sourceMappingURL=plugin-config-Be3vV2kr.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin-config-Be3vV2kr.js","names":[],"sources":["../../src/app/opencode-paths.ts","../../src/app/config.ts","../../src/app/package-info.ts","../../src/app/plugin-config.ts"],"sourcesContent":["import { access } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\nexport const GLOBAL_PLUGIN_DIRECTORY_NAME = \"opencode-tbot\";\nexport const GLOBAL_PLUGIN_CONFIG_FILE_NAME = \"config.json\";\nexport const GLOBAL_PLUGINS_DIRECTORY_NAME = \"plugins\";\nexport const GLOBAL_PLUGIN_BRIDGE_FILE_NAME = \"opencode-tbot.js\";\nexport const OPENCODE_CONFIG_FILE_NAME = \"opencode.json\";\nexport const OPENCODE_CONFIG_JSONC_FILE_NAME = \"opencode.jsonc\";\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 getOpenCodeJsoncConfigFilePath(homeDir: string = homedir()): string {\n return join(getOpenCodeConfigDirectory(homeDir), OPENCODE_CONFIG_JSONC_FILE_NAME);\n}\n\nexport async function resolveWritableOpenCodeConfigFilePath(homeDir: string = homedir()): Promise<string> {\n const jsoncConfigFilePath = getOpenCodeJsoncConfigFilePath(homeDir);\n\n if (await pathExists(jsoncConfigFilePath)) {\n return jsoncConfigFilePath;\n }\n\n const jsonConfigFilePath = getOpenCodeConfigFilePath(homeDir);\n\n if (await pathExists(jsonConfigFilePath)) {\n return jsonConfigFilePath;\n }\n\n return jsonConfigFilePath;\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 function getGlobalPluginsDirectory(homeDir: string = homedir()): string {\n return join(getOpenCodeConfigDirectory(homeDir), GLOBAL_PLUGINS_DIRECTORY_NAME);\n}\n\nexport function getGlobalPluginBridgeFilePath(homeDir: string = homedir()): string {\n return join(\n getGlobalPluginsDirectory(homeDir),\n GLOBAL_PLUGIN_BRIDGE_FILE_NAME,\n );\n}\n\nexport function getOpenCodeLogDirectory(homeDir: string = homedir()): string {\n return join(homeDir, \".local\", \"share\", \"opencode\", \"log\");\n}\n\nexport function getDefaultPluginLogDirectory(homeDir: string = homedir()): string {\n return join(getOpenCodeLogDirectory(homeDir), \"plugins\", \"opencode-tbot\");\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 isMissingFileError(error: unknown): error is NodeJS.ErrnoException {\n return error instanceof Error && \"code\" in error && error.code === \"ENOENT\";\n}\n","import { homedir } from \"node:os\";\nimport { isAbsolute, resolve } from \"node:path\";\nimport { z } from \"zod\";\nimport { getDefaultPluginLogDirectory } from \"./opencode-paths.js\";\n\nexport const DEFAULT_STATE_FILE_PATH = \"./data/opencode-tbot.state.json\";\nexport const DEFAULT_TELEGRAM_API_ROOT = \"https://api.telegram.org\";\nexport const DEFAULT_PROMPT_WAIT_TIMEOUT_MS = 1_800_000;\nexport const DEFAULT_PROMPT_POLL_REQUEST_TIMEOUT_MS = 15_000;\nexport const DEFAULT_PROMPT_RECOVERY_INACTIVITY_TIMEOUT_MS = 120_000;\nexport const DEFAULT_LOG_LEVEL = \"info\";\nexport const DEFAULT_LOG_RETENTION_MAX_FILES = 30;\nexport const DEFAULT_LOG_RETENTION_MAX_TOTAL_BYTES = 314_572_800;\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 PromptConfigSchema = z.preprocess(\n (value) => value ?? {},\n z.object({\n waitTimeoutMs: z.number().int().positive().default(DEFAULT_PROMPT_WAIT_TIMEOUT_MS),\n pollRequestTimeoutMs: z.number().int().positive().default(DEFAULT_PROMPT_POLL_REQUEST_TIMEOUT_MS),\n recoveryInactivityTimeoutMs: z.number().int().positive().default(DEFAULT_PROMPT_RECOVERY_INACTIVITY_TIMEOUT_MS),\n }),\n);\n\nconst LoggingConfigSchema = z.preprocess(\n (value) => value ?? {},\n z.object({\n level: z.string().trim().min(1).optional(),\n sinks: z.preprocess(\n (value) => value ?? {},\n z.object({\n host: z.boolean().default(true),\n file: z.boolean().default(true),\n }),\n ),\n file: z.preprocess(\n (value) => value ?? {},\n z.object({\n dir: z.string().trim().min(1).optional(),\n retention: z.preprocess(\n (value) => value ?? {},\n z.object({\n maxFiles: z.number().int().positive().default(DEFAULT_LOG_RETENTION_MAX_FILES),\n maxTotalBytes: z.number().int().positive().default(DEFAULT_LOG_RETENTION_MAX_TOTAL_BYTES),\n }),\n ),\n }),\n ),\n }),\n);\n\nconst AppConfigSchema = z.object({\n telegram: TelegramConfigSchema,\n state: StateConfigSchema,\n prompt: PromptConfigSchema,\n logging: LoggingConfigSchema.default({}),\n logLevel: z.string().trim().min(1).optional(),\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 prompt?: {\n waitTimeoutMs?: number;\n pollRequestTimeoutMs?: number;\n recoveryInactivityTimeoutMs?: number;\n [key: string]: unknown;\n };\n logging?: {\n level?: string;\n sinks?: {\n host?: boolean;\n file?: boolean;\n [key: string]: unknown;\n };\n file?: {\n dir?: string;\n retention?: {\n maxFiles?: number;\n maxTotalBytes?: number;\n [key: string]: unknown;\n };\n [key: string]: unknown;\n };\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 loggingLevel: string;\n loggingHostSinkEnabled: boolean;\n loggingFileSinkEnabled: boolean;\n loggingFileDir: string;\n loggingRetentionMaxFiles: number;\n loggingRetentionMaxTotalBytes: number;\n promptWaitTimeoutMs: number;\n promptPollRequestTimeoutMs: number;\n promptRecoveryInactivityTimeoutMs: number;\n stateFilePath: string;\n worktreePath: 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 const cwd = options.cwd ?? process.cwd();\n const loggingLevel = normalizeLogLevelValue(data.logging.level ?? data.logLevel);\n\n return {\n telegramBotToken: data.telegram.botToken,\n telegramAllowedChatIds: data.telegram.allowedChatIds,\n telegramApiRoot: normalizeApiRoot(data.telegram.apiRoot),\n logLevel: loggingLevel,\n loggingLevel,\n loggingHostSinkEnabled: data.logging.sinks.host,\n loggingFileSinkEnabled: data.logging.sinks.file,\n loggingFileDir: resolveLoggingDirectory(\n data.logging.file.dir,\n cwd,\n ),\n loggingRetentionMaxFiles: data.logging.file.retention.maxFiles,\n loggingRetentionMaxTotalBytes: data.logging.file.retention.maxTotalBytes,\n promptWaitTimeoutMs: data.prompt.waitTimeoutMs,\n promptPollRequestTimeoutMs: data.prompt.pollRequestTimeoutMs,\n promptRecoveryInactivityTimeoutMs: data.prompt.recoveryInactivityTimeoutMs,\n stateFilePath: resolveStatePath(data, cwd),\n worktreePath: 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 normalizeLogLevelValue(value: string | undefined): string {\n const normalized = value?.trim().toLowerCase();\n\n switch (normalized) {\n case \"debug\":\n case \"warn\":\n case \"error\":\n case \"info\":\n return normalized;\n default:\n return DEFAULT_LOG_LEVEL;\n }\n}\n\nfunction resolveLoggingDirectory(value: string | undefined, cwd: string): string {\n const normalized = value?.trim();\n\n if (!normalized) {\n return getDefaultPluginLogDirectory(homedir());\n }\n\n return isAbsolute(normalized)\n ? normalized\n : resolve(cwd, 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\";\nimport {\n getDefaultPluginLogDirectory,\n getGlobalPluginBridgeFilePath,\n getGlobalPluginConfigFilePath,\n getGlobalPluginsDirectory,\n getOpenCodeConfigDirectory,\n getOpenCodeConfigFilePath,\n getOpenCodeJsoncConfigFilePath,\n GLOBAL_PLUGIN_BRIDGE_FILE_NAME,\n GLOBAL_PLUGIN_CONFIG_FILE_NAME,\n GLOBAL_PLUGIN_DIRECTORY_NAME,\n GLOBAL_PLUGINS_DIRECTORY_NAME,\n OPENCODE_CONFIG_FILE_NAME,\n OPENCODE_CONFIG_JSONC_FILE_NAME,\n resolveWritableOpenCodeConfigFilePath,\n} from \"./opencode-paths.js\";\n\nexport const PLUGIN_CONFIG_FILE_NAME = \"tbot.config.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 {\n getDefaultPluginLogDirectory,\n getGlobalPluginBridgeFilePath,\n getGlobalPluginConfigFilePath,\n getGlobalPluginsDirectory,\n getOpenCodeConfigDirectory,\n getOpenCodeConfigFilePath,\n getOpenCodeJsoncConfigFilePath,\n GLOBAL_PLUGIN_BRIDGE_FILE_NAME,\n GLOBAL_PLUGIN_CONFIG_FILE_NAME,\n GLOBAL_PLUGIN_DIRECTORY_NAME,\n GLOBAL_PLUGINS_DIRECTORY_NAME,\n OPENCODE_CONFIG_FILE_NAME,\n OPENCODE_CONFIG_JSONC_FILE_NAME,\n resolveWritableOpenCodeConfigFilePath,\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 previousPrompt = merged.prompt;\n const previousLogging = merged.logging;\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.prompt) {\n merged.prompt = {\n ...(previousPrompt ?? {}),\n ...normalized.prompt,\n };\n }\n\n if (normalized.logging) {\n const previousLoggingSinks = previousLogging?.sinks;\n const previousLoggingFile = previousLogging?.file;\n const previousRetention = previousLoggingFile?.retention;\n\n merged.logging = {\n ...(previousLogging ?? {}),\n ...normalized.logging,\n ...(normalized.logging.sinks || previousLoggingSinks\n ? {\n sinks: {\n ...(previousLoggingSinks ?? {}),\n ...(normalized.logging.sinks ?? {}),\n },\n }\n : {}),\n ...(normalized.logging.file || previousLoggingFile\n ? {\n file: {\n ...(previousLoggingFile ?? {}),\n ...(normalized.logging.file ?? {}),\n ...(normalized.logging.file?.retention || previousRetention\n ? {\n retention: {\n ...(previousRetention ?? {}),\n ...(normalized.logging.file?.retention ?? {}),\n },\n }\n : {}),\n },\n }\n : {}),\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 \"prompt\",\n \"logging\",\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.prompt) {\n ordered.prompt = config.prompt;\n }\n\n if (config.logging) {\n ordered.logging = config.logging;\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 { c as
|
|
1
|
+
import { c as getGlobalPluginBridgeFilePath, i as OPENCODE_TBOT_VERSION, l as getGlobalPluginConfigFilePath, r as writePluginConfigFile, s as getDefaultPluginLogDirectory, t as mergePluginConfigSources, u as resolveWritableOpenCodeConfigFilePath } from "./assets/plugin-config-Be3vV2kr.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";
|
|
@@ -6,7 +6,6 @@ import { stderr, stdin, stdout } from "node:process";
|
|
|
6
6
|
import { createInterface } from "node:readline/promises";
|
|
7
7
|
import { parse } from "jsonc-parser";
|
|
8
8
|
//#region src/cli.ts
|
|
9
|
-
var DEFAULT_PLUGIN_SPEC = "opencode-tbot@latest";
|
|
10
9
|
var PROMPT_CANCELLED_ERROR = "Prompt cancelled.";
|
|
11
10
|
async function main(argv = process.argv.slice(2)) {
|
|
12
11
|
try {
|
|
@@ -39,21 +38,20 @@ async function runCli(argv) {
|
|
|
39
38
|
async function installPlugin(options = {}) {
|
|
40
39
|
const homeDir = options.homeDir ?? homedir();
|
|
41
40
|
const openCodeConfigFilePath = await resolveWritableOpenCodeConfigFilePath(homeDir);
|
|
41
|
+
const globalPluginBridgeFilePath = getGlobalPluginBridgeFilePath(homeDir);
|
|
42
42
|
const globalPluginConfigFilePath = getGlobalPluginConfigFilePath(homeDir);
|
|
43
|
-
const openCodeConfig = await readJsoncObject(openCodeConfigFilePath);
|
|
44
43
|
const existingPluginConfig = await readPluginConfigFile(globalPluginConfigFilePath);
|
|
45
44
|
const prompt = createPromptSession();
|
|
46
45
|
try {
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (options.registerPlugin === false) await ensureParentDirectory(openCodeConfigFilePath);
|
|
53
|
-
else await writeJsonFile(openCodeConfigFilePath, nextOpenCodeConfig);
|
|
46
|
+
const nextPluginConfig = buildInstalledPluginConfig(existingPluginConfig, normalizeRequiredString(options.botToken ?? await prompt.askSecret("Telegram bot token: "), "Telegram bot token is required."), normalizeOptionalString(options.telegramApiRoot) ?? "https://api.telegram.org");
|
|
47
|
+
if (options.registerPlugin !== false) await installGlobalPluginBridge({
|
|
48
|
+
openCodeConfigFilePath,
|
|
49
|
+
pluginBridgeFilePath: globalPluginBridgeFilePath
|
|
50
|
+
});
|
|
54
51
|
await writePluginConfigFile(globalPluginConfigFilePath, nextPluginConfig);
|
|
55
52
|
stdout.write("Success.\n");
|
|
56
53
|
stdout.write(`OpenCode config: ${openCodeConfigFilePath}\n`);
|
|
54
|
+
stdout.write(`Plugin bridge: ${globalPluginBridgeFilePath}${options.registerPlugin === false ? " (skipped)" : ""}\n`);
|
|
57
55
|
stdout.write(`Plugin config: ${globalPluginConfigFilePath}\n`);
|
|
58
56
|
stdout.write(`Plugin logs: ${getDefaultPluginLogDirectory(homeDir)}\n`);
|
|
59
57
|
await warnAboutLegacyLocalInstallations(homeDir);
|
|
@@ -64,9 +62,14 @@ async function installPlugin(options = {}) {
|
|
|
64
62
|
async function updatePlugin(options = {}) {
|
|
65
63
|
const homeDir = options.homeDir ?? homedir();
|
|
66
64
|
const openCodeConfigFilePath = await resolveWritableOpenCodeConfigFilePath(homeDir);
|
|
67
|
-
|
|
65
|
+
const globalPluginBridgeFilePath = getGlobalPluginBridgeFilePath(homeDir);
|
|
66
|
+
await installGlobalPluginBridge({
|
|
67
|
+
openCodeConfigFilePath,
|
|
68
|
+
pluginBridgeFilePath: globalPluginBridgeFilePath
|
|
69
|
+
});
|
|
68
70
|
stdout.write("Success.\n");
|
|
69
71
|
stdout.write(`OpenCode config: ${openCodeConfigFilePath}\n`);
|
|
72
|
+
stdout.write(`Plugin bridge: ${globalPluginBridgeFilePath}\n`);
|
|
70
73
|
stdout.write(`Plugin config: ${getGlobalPluginConfigFilePath(homeDir)}\n`);
|
|
71
74
|
stdout.write(`Plugin logs: ${getDefaultPluginLogDirectory(homeDir)}\n`);
|
|
72
75
|
await warnAboutLegacyLocalInstallations(homeDir);
|
|
@@ -116,33 +119,12 @@ function buildInstalledPluginConfig(current, botToken, telegramApiRoot) {
|
|
|
116
119
|
} });
|
|
117
120
|
return nextConfig;
|
|
118
121
|
}
|
|
119
|
-
function
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
function replacePluginRegistration(config, pluginSpec) {
|
|
128
|
-
const currentPlugins = Array.isArray(config.plugin) ? config.plugin.filter((item) => typeof item === "string") : [];
|
|
129
|
-
const nextPlugins = [];
|
|
130
|
-
let inserted = false;
|
|
131
|
-
for (const currentPlugin of currentPlugins) {
|
|
132
|
-
if (isOpencodeTbotPluginSpec(currentPlugin)) {
|
|
133
|
-
if (!inserted) {
|
|
134
|
-
nextPlugins.push(pluginSpec);
|
|
135
|
-
inserted = true;
|
|
136
|
-
}
|
|
137
|
-
continue;
|
|
138
|
-
}
|
|
139
|
-
nextPlugins.push(currentPlugin);
|
|
140
|
-
}
|
|
141
|
-
if (!inserted) nextPlugins.push(pluginSpec);
|
|
142
|
-
return {
|
|
143
|
-
...config,
|
|
144
|
-
plugin: nextPlugins
|
|
145
|
-
};
|
|
122
|
+
function removePluginRegistration(config) {
|
|
123
|
+
const nextPlugins = (Array.isArray(config.plugin) ? config.plugin : []).filter((currentPlugin) => typeof currentPlugin !== "string" || !isOpencodeTbotPluginSpec(currentPlugin));
|
|
124
|
+
const nextConfig = { ...config };
|
|
125
|
+
if (nextPlugins.length > 0) nextConfig.plugin = nextPlugins;
|
|
126
|
+
else delete nextConfig.plugin;
|
|
127
|
+
return nextConfig;
|
|
146
128
|
}
|
|
147
129
|
async function readPluginConfigFile(configFilePath) {
|
|
148
130
|
try {
|
|
@@ -167,9 +149,41 @@ async function writeJsonFile(filePath, value) {
|
|
|
167
149
|
await ensureParentDirectory(filePath);
|
|
168
150
|
await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
169
151
|
}
|
|
152
|
+
async function installGlobalPluginBridge(input) {
|
|
153
|
+
const pluginModuleUrl = await resolveInstalledPluginModuleUrl();
|
|
154
|
+
await writePluginBridgeFile(input.pluginBridgeFilePath, pluginModuleUrl);
|
|
155
|
+
await removeLegacyNpmPluginRegistration(input.openCodeConfigFilePath);
|
|
156
|
+
}
|
|
170
157
|
async function ensureParentDirectory(filePath) {
|
|
171
158
|
await mkdir(dirname(filePath), { recursive: true });
|
|
172
159
|
}
|
|
160
|
+
async function writePluginBridgeFile(pluginBridgeFilePath, pluginModuleUrl) {
|
|
161
|
+
await ensureParentDirectory(pluginBridgeFilePath);
|
|
162
|
+
await writeFile(pluginBridgeFilePath, buildPluginBridgeSource(pluginModuleUrl), "utf8");
|
|
163
|
+
}
|
|
164
|
+
function buildPluginBridgeSource(pluginModuleUrl) {
|
|
165
|
+
return `export { default } from ${JSON.stringify(pluginModuleUrl)};\n`;
|
|
166
|
+
}
|
|
167
|
+
async function resolveInstalledPluginModuleUrl() {
|
|
168
|
+
const candidates = [
|
|
169
|
+
new URL("./plugin.js", import.meta.url),
|
|
170
|
+
new URL("./plugin.ts", import.meta.url),
|
|
171
|
+
new URL("../src/plugin.ts", import.meta.url),
|
|
172
|
+
new URL("../dist/plugin.js", import.meta.url)
|
|
173
|
+
];
|
|
174
|
+
for (const candidate of candidates) if (await pathExists(candidate)) return candidate.href;
|
|
175
|
+
throw new Error("Failed to resolve the opencode-tbot plugin entry for the global OpenCode bridge.");
|
|
176
|
+
}
|
|
177
|
+
async function removeLegacyNpmPluginRegistration(openCodeConfigFilePath) {
|
|
178
|
+
if (!await pathExists(openCodeConfigFilePath)) return;
|
|
179
|
+
const currentOpenCodeConfig = await readJsoncObject(openCodeConfigFilePath);
|
|
180
|
+
const nextOpenCodeConfig = removePluginRegistration(currentOpenCodeConfig);
|
|
181
|
+
if (areJsonObjectsEqual(currentOpenCodeConfig, nextOpenCodeConfig)) return;
|
|
182
|
+
await writeJsonFile(openCodeConfigFilePath, nextOpenCodeConfig);
|
|
183
|
+
}
|
|
184
|
+
function areJsonObjectsEqual(left, right) {
|
|
185
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
186
|
+
}
|
|
173
187
|
function createPromptSession(options = {}) {
|
|
174
188
|
const input = options.input ?? stdin;
|
|
175
189
|
const output = options.output ?? stdout;
|
|
@@ -294,7 +308,7 @@ function isOpencodeTbotPluginSpec(value) {
|
|
|
294
308
|
async function warnAboutLegacyLocalInstallations(homeDir) {
|
|
295
309
|
const legacyInstallations = await findLegacyLocalInstallations(homeDir);
|
|
296
310
|
if (legacyInstallations.length === 0) return;
|
|
297
|
-
stdout.write("\nDetected local opencode-tbot npm installation(s) that can make
|
|
311
|
+
stdout.write("\nDetected local opencode-tbot npm installation(s) that can make the global OpenCode plugin bridge depend on project-local node_modules paths.\n");
|
|
298
312
|
for (const installation of legacyInstallations) {
|
|
299
313
|
stdout.write(`- ${installation.packagePath}\n`);
|
|
300
314
|
stdout.write(` cleanup: cd "${installation.rootDir}" && npm uninstall opencode-tbot\n`);
|
|
@@ -335,7 +349,7 @@ function buildHelpText() {
|
|
|
335
349
|
"Options:",
|
|
336
350
|
" --bot-token <token>",
|
|
337
351
|
" --telegram-api-root <url>",
|
|
338
|
-
" --plugin-spec <spec>",
|
|
352
|
+
" --plugin-spec <spec> (deprecated; ignored)",
|
|
339
353
|
" --skip-register",
|
|
340
354
|
" --home-dir <path>",
|
|
341
355
|
" --version",
|