opencode-tbot 0.1.28 → 0.1.31

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
@@ -32,7 +32,7 @@ Recommended install:
32
32
  npm exec --package opencode-tbot@latest opencode-tbot -- install
33
33
  ```
34
34
 
35
- The installer registers the plugin globally and writes the default runtime config.
35
+ The installer registers the plugin globally, writes the default runtime config, and prints both the config path and the default plugin log directory.
36
36
 
37
37
  Check the installed CLI version:
38
38
 
@@ -86,6 +86,20 @@ Legacy `openrouter` voice-transcription settings are ignored at runtime. When th
86
86
  "pollRequestTimeoutMs": 15000,
87
87
  "recoveryInactivityTimeoutMs": 120000
88
88
  },
89
+ "logging": {
90
+ "level": "info",
91
+ "sinks": {
92
+ "host": true,
93
+ "file": true
94
+ },
95
+ "file": {
96
+ "dir": "C:/Users/your-user/.local/share/opencode/log/plugins/opencode-tbot",
97
+ "retention": {
98
+ "maxFiles": 30,
99
+ "maxTotalBytes": 314572800
100
+ }
101
+ }
102
+ },
89
103
  "logLevel": "info"
90
104
  }
91
105
  ```
@@ -101,7 +115,13 @@ Legacy `openrouter` voice-transcription settings are ignored at runtime. When th
101
115
  | `prompt.waitTimeoutMs` | No | `1800000` | Maximum total wait for one async prompt lifecycle before the plugin stops waiting for OpenCode recovery. |
102
116
  | `prompt.pollRequestTimeoutMs` | No | `15000` | Timeout for each individual recovery poll request to OpenCode. |
103
117
  | `prompt.recoveryInactivityTimeoutMs` | No | `120000` | Recovery timeout that only applies when prompt progress stops advancing. |
104
- | `logLevel` | No | `info` | Plugin log level. Logs are emitted through `client.app.log()`. |
118
+ | `logging.level` | No | `info` | Structured log level for both host and file sinks. |
119
+ | `logging.sinks.host` | No | `true` | Enable OpenCode host logging through `client.app.log({ body: ... })`. |
120
+ | `logging.sinks.file` | No | `true` | Enable plugin-owned JSONL file logs. |
121
+ | `logging.file.dir` | No | `%USERPROFILE%/.local/share/opencode/log/plugins/opencode-tbot` | Plugin JSONL log directory. |
122
+ | `logging.file.retention.maxFiles` | No | `30` | Maximum number of retained plugin log files. |
123
+ | `logging.file.retention.maxTotalBytes` | No | `314572800` | Maximum total size of retained plugin log files. |
124
+ | `logLevel` | No | `info` | Legacy alias for `logging.level`. Still accepted for compatibility. |
105
125
 
106
126
  ### Notes
107
127
 
@@ -111,6 +131,19 @@ Legacy `openrouter` voice-transcription settings are ignored at runtime. When th
111
131
  - If `telegram.allowedChatIds` is left empty, the bot accepts messages from any chat. Restrict it in production.
112
132
  - Permission approvals and session notifications are handled through plugin hooks.
113
133
  - `/language` currently supports English, Simplified Chinese, and Japanese.
134
+ - Plugin logs are dual-written by default: OpenCode host logs through `client.app.log({ body: ... })`, plus plugin JSONL files.
135
+ - OpenCode host logs live under `%USERPROFILE%/.local/share/opencode/log`.
136
+ - The default plugin JSONL log directory is `%USERPROFILE%/.local/share/opencode/log/plugins/opencode-tbot`.
137
+ - File logs are metadata-only by default. Prompt bodies, attachment contents, raw URLs, and secrets are not written.
138
+
139
+ ## Logging
140
+
141
+ - Host log directory: `%USERPROFILE%/.local/share/opencode/log`
142
+ - Plugin log directory: `%USERPROFILE%/.local/share/opencode/log/plugins/opencode-tbot`
143
+ - File format: append-only JSONL, one structured event per line
144
+ - Correlation fields: `runtimeId`, `operationId`, `correlationId`, `updateId`, `chatId`, `sessionId`, `projectId`
145
+ - Common components: `runtime`, `telegram`, `opencode`, `prompt`, `plugin-event`, `storage`
146
+ - Retention: keep the newest 30 files and at most 300 MB by default
114
147
 
115
148
  ## Quick Start
116
149
 
@@ -161,6 +194,8 @@ pnpm test
161
194
 
162
195
  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`.
163
196
 
197
+ OpenCode may load both the local `.opencode/plugins/` bridge and an installed npm plugin such as `opencode-tbot@latest` in the same process. This repository now guards against duplicate Telegram runners, but local development is still clearer if you keep only one loading path enabled at a time.
198
+
164
199
  ## FAQ
165
200
 
166
201
  ### Do I need a running OpenCode instance?
package/README.zh-CN.md CHANGED
@@ -4,24 +4,25 @@
4
4
 
5
5
  [English](./README.md) | [简体中文](./README.zh-CN.md) | [日本語](./README.ja.md)
6
6
 
7
- > 本项目并非由 OpenCode 团队开发,也不隶属于 OpenCode 官方。
7
+ > 本项目并非由 OpenCode 官方团队开发。
8
8
 
9
- ## 项目概览
9
+ ## 概览
10
10
 
11
11
  `opencode-tbot` 允许你直接在 Telegram 中操作 OpenCode。
12
12
 
13
- - 文本消息会转发到当前激活的 OpenCode 会话。
14
- - Telegram 照片和图片文档会作为 OpenCode 文件片段上传。
15
- - Telegram 语音消息会被明确拒绝,并返回本地化提示。
16
- - OpenCode 触发的权限请求可以直接在 Telegram 内联按钮中批准或拒绝。
17
- - 会话完成和错误事件可以回推到绑定的 Telegram chat。
18
- - 聊天绑定状态通过 JSON 状态文件持久化。
13
+ - 文本消息会转发到当前 OpenCode 会话。
14
+ - Telegram 图片会作为 OpenCode 文件片段上传。
15
+ - 图片轮次会在临时 fork 会话中执行,避免后续纯文本上下文被污染。
16
+ - Telegram 语音消息会被明确拒绝。
17
+ - OpenCode 的权限请求可以直接在 Telegram 中审批。
18
+ - 会话完成和错误事件可以回推到已绑定的 Telegram chat。
19
+ - 聊天绑定状态会保存在 JSON 状态文件中。
19
20
 
20
21
  ## 环境要求
21
22
 
22
23
  - 一个正在运行、并会加载该插件的 OpenCode Host 进程。
23
24
  - 一个 Telegram bot token。
24
- - Node.js `>=22.12.0`,用于 CLI 和本地开发流程。
25
+ - Node.js `>=22.12.0`。
25
26
 
26
27
  ## 安装
27
28
 
@@ -31,9 +32,9 @@
31
32
  npm exec --package opencode-tbot@latest opencode-tbot -- install
32
33
  ```
33
34
 
34
- 安装器会注册全局插件并写入默认运行时配置。
35
+ 安装器会全局注册插件、写入默认运行时配置,并打印插件配置路径与默认日志目录。
35
36
 
36
- 查看已安装的 CLI 版本:
37
+ 查看 CLI 版本:
37
38
 
38
39
  ```bash
39
40
  npm exec --package opencode-tbot@latest opencode-tbot -- --version
@@ -49,10 +50,10 @@ npm exec --package opencode-tbot@latest opencode-tbot -- update
49
50
 
50
51
  `install` 支持:
51
52
 
52
- - `--bot-token <token>` 非交互式写入 Telegram bot token
53
+ - `--bot-token <token>` 非交互写入 Telegram bot token
53
54
  - `--telegram-api-root <url>` 覆盖 Telegram Bot API 根地址
54
55
  - `--plugin-spec <spec>` 注册自定义 npm 插件 spec
55
- - `--skip-register` 只重写插件配置,不改动 OpenCode 的插件注册
56
+ - `--skip-register` 只重写插件配置,不改 OpenCode 插件注册
56
57
  - `--home-dir <path>` 使用自定义 home 目录
57
58
 
58
59
  `update` 支持:
@@ -62,11 +63,11 @@ npm exec --package opencode-tbot@latest opencode-tbot -- update
62
63
 
63
64
  ## 配置
64
65
 
65
- 运行时配置只会从 `~/.config/opencode/opencode-tbot/config.json` 加载。
66
+ 运行时配置只从 `~/.config/opencode/opencode-tbot/config.json` 加载。
66
67
 
67
- 遗留的 `<worktree>/tbot.config.json` 会在运行时被忽略;如果检测到该文件,插件会记录一条警告,提示你把其中的值迁移到全局配置。
68
+ 遗留的 `<worktree>/tbot.config.json` 会在运行时被忽略;如果检测到,插件会记录一条警告,提示你把配置迁移到全局文件。
68
69
 
69
- 遗留的 `openrouter` 语音转写配置在运行时会被忽略;安装器重写配置时也会自动移除这些字段。
70
+ 遗留的 `openrouter` 语音转写配置会在运行时被忽略;安装器重写配置时也会自动移除它们。
70
71
 
71
72
  ### 全局 `config.json` 示例
72
73
 
@@ -85,6 +86,20 @@ npm exec --package opencode-tbot@latest opencode-tbot -- update
85
86
  "pollRequestTimeoutMs": 15000,
86
87
  "recoveryInactivityTimeoutMs": 120000
87
88
  },
89
+ "logging": {
90
+ "level": "info",
91
+ "sinks": {
92
+ "host": true,
93
+ "file": true
94
+ },
95
+ "file": {
96
+ "dir": "C:/Users/your-user/.local/share/opencode/log/plugins/opencode-tbot",
97
+ "retention": {
98
+ "maxFiles": 30,
99
+ "maxTotalBytes": 314572800
100
+ }
101
+ }
102
+ },
88
103
  "logLevel": "info"
89
104
  }
90
105
  ```
@@ -93,53 +108,70 @@ npm exec --package opencode-tbot@latest opencode-tbot -- update
93
108
 
94
109
  | 字段 | 必填 | 默认值 | 说明 |
95
110
  | --- | --- | --- | --- |
96
- | `telegram.botToken` | 是 | - | Telegram bot token。通常由安装器写入全局插件配置。 |
97
- | `telegram.allowedChatIds` | 否 | `[]` | 允许访问的 Telegram chat ID 数组。为空时表示接受任意 chat。 |
98
- | `telegram.apiRoot` | 否 | `https://api.telegram.org` | Telegram Bot API 根地址,适合测试或自托管网关。 |
111
+ | `telegram.botToken` | 是 | - | Telegram bot token。通常由安装器写入。 |
112
+ | `telegram.allowedChatIds` | 否 | `[]` | 允许访问的 Telegram chat ID。为空时接受任意 chat。 |
113
+ | `telegram.apiRoot` | 否 | `https://api.telegram.org` | Telegram Bot API 根地址。 |
99
114
  | `state.path` | 否 | `./data/opencode-tbot.state.json` | JSON 状态文件路径,相对当前 OpenCode worktree 解析。 |
100
- | `prompt.waitTimeoutMs` | 否 | `1800000` | 单次异步 prompt 生命周期的最大等待时间;超过后插件会停止等待 OpenCode 恢复结果。 |
101
- | `prompt.pollRequestTimeoutMs` | 否 | `15000` | 每次向 OpenCode 拉取恢复状态时的单次请求超时。 |
102
- | `prompt.recoveryInactivityTimeoutMs` | 否 | `120000` | 仅在 prompt 长时间没有任何新进展时生效的恢复超时。 |
103
- | `logLevel` | 否 | `info` | 插件日志级别。日志统一通过 `client.app.log()` 上报。 |
115
+ | `prompt.waitTimeoutMs` | 否 | `1800000` | 单次异步 prompt 生命周期的最大等待时间。 |
116
+ | `prompt.pollRequestTimeoutMs` | 否 | `15000` | 每次恢复轮询请求的超时时间。 |
117
+ | `prompt.recoveryInactivityTimeoutMs` | 否 | `120000` | 仅在 OpenCode 长时间没有新进展时生效的恢复超时。 |
118
+ | `logging.level` | 否 | `info` | 结构化日志级别。 |
119
+ | `logging.sinks.host` | 否 | `true` | 是否通过 `client.app.log({ body: ... })` 写入 OpenCode Host 日志。 |
120
+ | `logging.sinks.file` | 否 | `true` | 是否写入插件自己的 JSONL 文件日志。 |
121
+ | `logging.file.dir` | 否 | `%USERPROFILE%/.local/share/opencode/log/plugins/opencode-tbot` | 插件日志目录。 |
122
+ | `logging.file.retention.maxFiles` | 否 | `30` | 最多保留多少个插件日志文件。 |
123
+ | `logging.file.retention.maxTotalBytes` | 否 | `314572800` | 插件日志文件总大小上限。 |
124
+ | `logLevel` | 否 | `info` | `logging.level` 的兼容别名。 |
104
125
 
105
126
  ### 说明
106
127
 
107
128
  - `state.path` 会相对当前 OpenCode worktree 解析。
108
- - Telegram prompt 处理现在采用 async-first:插件先调用 `session.promptAsync()`,再通过会话消息和会话状态恢复最终回复。
109
- - `prompt.waitTimeoutMs` 是整体安全上限;`prompt.recoveryInactivityTimeoutMs` 只在 OpenCode 长时间没有新进展时生效。
129
+ - Telegram prompt 采用 async-first:插件先提交异步 prompt,再通过消息与状态恢复最终回复。
110
130
  - 如果 `telegram.allowedChatIds` 为空,bot 会接受任意 chat 的消息;生产环境建议显式限制。
111
- - 权限审批和会话通知通过插件 hook 处理。
112
- - `/language` 当前支持 English、简体中文、日本語。
131
+ - 权限审批与会话通知通过插件 hooks 处理。
132
+ - `/language` 当前支持 English、简体中文、日语。
133
+ - 默认会双写日志:OpenCode Host 日志 + 插件 JSONL 文件日志。
134
+ - 文件日志默认只记录元数据,不记录 prompt 正文、附件内容、原始 URL 或 secrets。
135
+
136
+ ## 日志
137
+
138
+ - OpenCode Host 日志目录:`%USERPROFILE%/.local/share/opencode/log`
139
+ - 插件日志目录:`%USERPROFILE%/.local/share/opencode/log/plugins/opencode-tbot`
140
+ - 文件格式:JSONL,每行一条结构化事件
141
+ - 关键关联字段:`runtimeId`、`operationId`、`correlationId`、`updateId`、`chatId`、`sessionId`、`projectId`
142
+ - 常见组件:`runtime`、`telegram`、`opencode`、`prompt`、`plugin-event`、`storage`
143
+ - 默认保留策略:最多 30 个文件,总大小不超过 300 MB
113
144
 
114
145
  ## 快速开始
115
146
 
116
- 1. 使用 `npm exec --package opencode-tbot@latest opencode-tbot -- install` 安装插件。
117
- 2. 如果你只想允许特定聊天使用 bot,请在 `~/.config/opencode/opencode-tbot/config.json` 中设置 `telegram.allowedChatIds`。
118
- 3. 在目标 worktree 中启动 OpenCode,让插件运行时被加载。
119
- 4. 在 Telegram 中执行 `/status` 验证连接是否正常。
120
- 5. 执行 `/new [title]`,或者直接发送文本消息开始使用。
147
+ 1. 运行 `npm exec --package opencode-tbot@latest opencode-tbot -- install`。
148
+ 2. 如需限制可访问 chat,在 `~/.config/opencode/opencode-tbot/config.json` 中设置 `telegram.allowedChatIds`。
149
+ 3. 在目标 worktree 中启动 OpenCode
150
+ 4. 在 Telegram 中执行 `/status` 验证连通性。
151
+ 5. 执行 `/new [title]`,或者直接发送文本/图片开始使用。
121
152
 
122
153
  ## 命令
123
154
 
124
- - `/start` 显示简短欢迎信息和快速开始说明
125
- - `/status` 显示 OpenCode 健康状态、路径、插件、LSPMCP 信息
126
- - `/new [title]` 创建新的 OpenCode 会话
127
- - `/agents` 或 `/agent` 列出可用 agent 并切换当前 agent
128
- - `/sessions` 列出会话并切换当前会话
129
- - `/cancel` 取消会话重命名,或中止当前会话正在运行的请求,并同步结束本地 Telegram 等待状态
130
- - `/model` 或 `/models` 列出可用模型并切换当前模型
131
- - `/language` 切换 bot 显示语言
155
+ - `/start` 显示欢迎信息和快速开始说明
156
+ - `/status` 显示 OpenCode 健康状态、路径、LSPMCP 等信息
157
+ - `/new [title]` 创建新会话
158
+ - `/agents` 或 `/agent` 列出并切换 agent
159
+ - `/sessions` 列出并切换会话
160
+ - `/cancel` 取消重命名,或中止当前正在运行的请求
161
+ - `/model` 或 `/models` 列出并切换模型
162
+ - `/language` 切换显示语言
132
163
 
133
164
  消息处理规则:
134
165
 
135
- - 任意非命令文本都会被当作 prompt 发送给 OpenCode。
136
- - Telegram 照片和图片文档会作为 OpenCode 文件片段上传。
137
- - `/cancel` 会同时中止 OpenCode 侧会话和本地 Telegram 等待,因此下一条消息可以立即开始处理。
138
- - Telegram 语音消息当前不受支持,bot 会直接返回本地化拒绝提示。
166
+ - 非命令文本会作为 prompt 发送给 OpenCode。
167
+ - Telegram 图片会作为 OpenCode 文件片段上传。
168
+ - 图片消息会在临时 fork 会话中处理。
169
+ - `/cancel` 会同时中止 OpenCode 会话和本地 Telegram 等待状态。
170
+ - Telegram 语音消息不受支持,会收到本地化拒绝提示。
139
171
 
140
172
  ## 开发
141
173
 
142
- 构建插件:
174
+ 构建:
143
175
 
144
176
  ```bash
145
177
  pnpm build
@@ -157,14 +189,16 @@ pnpm typecheck
157
189
  pnpm test
158
190
  ```
159
191
 
160
- 在本仓库中做源码调试时,OpenCode 可以通过 [.opencode/plugins/opencode-tbot.ts](./.opencode/plugins/opencode-tbot.ts) 直接加载 `src/plugin.ts`。
192
+ 本仓库本地开发时,OpenCode 可以通过 [.opencode/plugins/opencode-tbot.ts](./.opencode/plugins/opencode-tbot.ts) 直接加载 `src/plugin.ts`。
193
+
194
+ OpenCode 可能会在同一进程中同时加载本地 `.opencode/plugins/` bridge 和已安装的 `opencode-tbot@latest` npm 插件。当前实现已经防止重复启动 Telegram runner,但本地开发仍建议只保留一种加载方式。
161
195
 
162
- ## 常见问题
196
+ ## FAQ
163
197
 
164
- ### 我需要一个正在运行的 OpenCode 实例吗?
198
+ ### 需要一个正在运行的 OpenCode 实例吗?
165
199
 
166
- 需要。这个仓库提供的是 Telegram 集成层,依赖加载它的 OpenCode Host 进程。
200
+ 需要。这个仓库只提供 Telegram 集成层,依赖加载它的 OpenCode Host 进程。
167
201
 
168
202
  ### 这是 OpenCode 官方项目吗?
169
203
 
170
- 不是。它只是一个 OpenCode 集成,并非 OpenCode 官方项目。
204
+ 不是。它集成 OpenCode,但不是 OpenCode 官方项目。
@@ -1,15 +1,37 @@
1
1
  import { access, mkdir, readFile, writeFile } from "node:fs/promises";
2
2
  import { homedir } from "node:os";
3
- import { dirname, join, resolve } from "node:path";
3
+ import { dirname, isAbsolute, join, resolve } from "node:path";
4
4
  import { z } from "zod";
5
5
  import { existsSync, readFileSync } from "node:fs";
6
6
  import { fileURLToPath } from "node:url";
7
+ //#region src/app/opencode-paths.ts
8
+ var GLOBAL_PLUGIN_DIRECTORY_NAME = "opencode-tbot";
9
+ var GLOBAL_PLUGIN_CONFIG_FILE_NAME = "config.json";
10
+ var OPENCODE_CONFIG_FILE_NAME = "opencode.json";
11
+ function getOpenCodeConfigDirectory(homeDir = homedir()) {
12
+ return join(homeDir, ".config", "opencode");
13
+ }
14
+ function getOpenCodeConfigFilePath(homeDir = homedir()) {
15
+ return join(getOpenCodeConfigDirectory(homeDir), OPENCODE_CONFIG_FILE_NAME);
16
+ }
17
+ function getGlobalPluginConfigFilePath(homeDir = homedir()) {
18
+ return join(getOpenCodeConfigDirectory(homeDir), GLOBAL_PLUGIN_DIRECTORY_NAME, GLOBAL_PLUGIN_CONFIG_FILE_NAME);
19
+ }
20
+ function getOpenCodeLogDirectory(homeDir = homedir()) {
21
+ return join(homeDir, ".local", "share", "opencode", "log");
22
+ }
23
+ function getDefaultPluginLogDirectory(homeDir = homedir()) {
24
+ return join(getOpenCodeLogDirectory(homeDir), "plugins", "opencode-tbot");
25
+ }
26
+ //#endregion
7
27
  //#region src/app/config.ts
8
28
  var DEFAULT_STATE_FILE_PATH = "./data/opencode-tbot.state.json";
9
29
  var DEFAULT_TELEGRAM_API_ROOT = "https://api.telegram.org";
10
30
  var DEFAULT_PROMPT_WAIT_TIMEOUT_MS = 18e5;
11
31
  var DEFAULT_PROMPT_POLL_REQUEST_TIMEOUT_MS = 15e3;
12
32
  var DEFAULT_PROMPT_RECOVERY_INACTIVITY_TIMEOUT_MS = 12e4;
33
+ var DEFAULT_LOG_LEVEL = "info";
34
+ var DEFAULT_LOG_RETENTION_MAX_TOTAL_BYTES = 314572800;
13
35
  var AllowedChatIdSchema = z.union([z.number().int(), z.string().regex(/^-?\d+$/u).transform((value) => Number(value))]);
14
36
  var TelegramConfigSchema = z.preprocess((value) => value ?? {}, z.object({
15
37
  botToken: z.string().trim().min(1),
@@ -22,25 +44,49 @@ var PromptConfigSchema = z.preprocess((value) => value ?? {}, z.object({
22
44
  pollRequestTimeoutMs: z.number().int().positive().default(DEFAULT_PROMPT_POLL_REQUEST_TIMEOUT_MS),
23
45
  recoveryInactivityTimeoutMs: z.number().int().positive().default(DEFAULT_PROMPT_RECOVERY_INACTIVITY_TIMEOUT_MS)
24
46
  }));
47
+ var LoggingConfigSchema = z.preprocess((value) => value ?? {}, z.object({
48
+ level: z.string().trim().min(1).optional(),
49
+ sinks: z.preprocess((value) => value ?? {}, z.object({
50
+ host: z.boolean().default(true),
51
+ file: z.boolean().default(true)
52
+ })),
53
+ file: z.preprocess((value) => value ?? {}, z.object({
54
+ dir: z.string().trim().min(1).optional(),
55
+ retention: z.preprocess((value) => value ?? {}, z.object({
56
+ maxFiles: z.number().int().positive().default(30),
57
+ maxTotalBytes: z.number().int().positive().default(DEFAULT_LOG_RETENTION_MAX_TOTAL_BYTES)
58
+ }))
59
+ }))
60
+ }));
25
61
  var AppConfigSchema = z.object({
26
62
  telegram: TelegramConfigSchema,
27
63
  state: StateConfigSchema,
28
64
  prompt: PromptConfigSchema,
29
- logLevel: z.string().default("info")
65
+ logging: LoggingConfigSchema.default({}),
66
+ logLevel: z.string().trim().min(1).optional()
30
67
  });
31
68
  function loadAppConfig(configSource = {}, options = {}) {
32
69
  return buildAppConfig(parseConfig(AppConfigSchema, configSource), options);
33
70
  }
34
71
  function buildAppConfig(data, options) {
72
+ const cwd = options.cwd ?? process.cwd();
73
+ const loggingLevel = normalizeLogLevelValue(data.logging.level ?? data.logLevel);
35
74
  return {
36
75
  telegramBotToken: data.telegram.botToken,
37
76
  telegramAllowedChatIds: data.telegram.allowedChatIds,
38
77
  telegramApiRoot: normalizeApiRoot(data.telegram.apiRoot),
39
- logLevel: data.logLevel,
78
+ logLevel: loggingLevel,
79
+ loggingLevel,
80
+ loggingHostSinkEnabled: data.logging.sinks.host,
81
+ loggingFileSinkEnabled: data.logging.sinks.file,
82
+ loggingFileDir: resolveLoggingDirectory(data.logging.file.dir, cwd),
83
+ loggingRetentionMaxFiles: data.logging.file.retention.maxFiles,
84
+ loggingRetentionMaxTotalBytes: data.logging.file.retention.maxTotalBytes,
40
85
  promptWaitTimeoutMs: data.prompt.waitTimeoutMs,
41
86
  promptPollRequestTimeoutMs: data.prompt.pollRequestTimeoutMs,
42
87
  promptRecoveryInactivityTimeoutMs: data.prompt.recoveryInactivityTimeoutMs,
43
- stateFilePath: resolveStatePath(data, options.cwd ?? process.cwd())
88
+ stateFilePath: resolveStatePath(data, cwd),
89
+ worktreePath: cwd
44
90
  };
45
91
  }
46
92
  function resolveStatePath(data, cwd) {
@@ -50,6 +96,21 @@ function normalizeApiRoot(value) {
50
96
  const normalized = value.trim();
51
97
  return normalized.endsWith("/") ? normalized.slice(0, -1) : normalized;
52
98
  }
99
+ function normalizeLogLevelValue(value) {
100
+ const normalized = value?.trim().toLowerCase();
101
+ switch (normalized) {
102
+ case "debug":
103
+ case "warn":
104
+ case "error":
105
+ case "info": return normalized;
106
+ default: return DEFAULT_LOG_LEVEL;
107
+ }
108
+ }
109
+ function resolveLoggingDirectory(value, cwd) {
110
+ const normalized = value?.trim();
111
+ if (!normalized) return getDefaultPluginLogDirectory(homedir());
112
+ return isAbsolute(normalized) ? normalized : resolve(cwd, normalized);
113
+ }
53
114
  function parseConfig(schema, configSource) {
54
115
  const parsed = schema.safeParse(configSource ?? {});
55
116
  if (parsed.success) return parsed.data;
@@ -75,9 +136,6 @@ function resolvePackageVersion() {
75
136
  //#endregion
76
137
  //#region src/app/plugin-config.ts
77
138
  var PLUGIN_CONFIG_FILE_NAME = "tbot.config.json";
78
- var GLOBAL_PLUGIN_DIRECTORY_NAME = "opencode-tbot";
79
- var GLOBAL_PLUGIN_CONFIG_FILE_NAME = "config.json";
80
- var OPENCODE_CONFIG_FILE_NAME = "opencode.json";
81
139
  async function preparePluginConfiguration(options) {
82
140
  const globalConfigFilePath = getGlobalPluginConfigFilePath(options.homeDir ?? homedir());
83
141
  const projectConfigFilePath = await resolveProjectPluginConfigFilePath(options.cwd);
@@ -94,15 +152,6 @@ async function preparePluginConfiguration(options) {
94
152
  configFilePath
95
153
  };
96
154
  }
97
- function getOpenCodeConfigDirectory(homeDir = homedir()) {
98
- return join(homeDir, ".config", "opencode");
99
- }
100
- function getOpenCodeConfigFilePath(homeDir = homedir()) {
101
- return join(getOpenCodeConfigDirectory(homeDir), OPENCODE_CONFIG_FILE_NAME);
102
- }
103
- function getGlobalPluginConfigFilePath(homeDir = homedir()) {
104
- return join(getOpenCodeConfigDirectory(homeDir), GLOBAL_PLUGIN_DIRECTORY_NAME, GLOBAL_PLUGIN_CONFIG_FILE_NAME);
105
- }
106
155
  async function writePluginConfigFile(configFilePath, config) {
107
156
  await mkdir(dirname(configFilePath), { recursive: true });
108
157
  await writeFile(configFilePath, serializePluginConfig(config), "utf8");
@@ -115,6 +164,7 @@ function mergePluginConfigSources(...sources) {
115
164
  const previousTelegram = merged.telegram;
116
165
  const previousState = merged.state;
117
166
  const previousPrompt = merged.prompt;
167
+ const previousLogging = merged.logging;
118
168
  Object.assign(merged, normalized);
119
169
  if (normalized.telegram) merged.telegram = {
120
170
  ...previousTelegram ?? {},
@@ -128,6 +178,27 @@ function mergePluginConfigSources(...sources) {
128
178
  ...previousPrompt ?? {},
129
179
  ...normalized.prompt
130
180
  };
181
+ if (normalized.logging) {
182
+ const previousLoggingSinks = previousLogging?.sinks;
183
+ const previousLoggingFile = previousLogging?.file;
184
+ const previousRetention = previousLoggingFile?.retention;
185
+ merged.logging = {
186
+ ...previousLogging ?? {},
187
+ ...normalized.logging,
188
+ ...normalized.logging.sinks || previousLoggingSinks ? { sinks: {
189
+ ...previousLoggingSinks ?? {},
190
+ ...normalized.logging.sinks ?? {}
191
+ } } : {},
192
+ ...normalized.logging.file || previousLoggingFile ? { file: {
193
+ ...previousLoggingFile ?? {},
194
+ ...normalized.logging.file ?? {},
195
+ ...normalized.logging.file?.retention || previousRetention ? { retention: {
196
+ ...previousRetention ?? {},
197
+ ...normalized.logging.file?.retention ?? {}
198
+ } } : {}
199
+ } } : {}
200
+ };
201
+ }
131
202
  }
132
203
  return merged;
133
204
  }
@@ -156,12 +227,14 @@ function orderPluginConfig(config) {
156
227
  "telegram",
157
228
  "state",
158
229
  "prompt",
230
+ "logging",
159
231
  "logLevel"
160
232
  ]);
161
233
  const ordered = {};
162
234
  if (config.telegram) ordered.telegram = config.telegram;
163
235
  if (config.state) ordered.state = config.state;
164
236
  if (config.prompt) ordered.prompt = config.prompt;
237
+ if (config.logging) ordered.logging = config.logging;
165
238
  if (config.logLevel !== void 0) ordered.logLevel = config.logLevel;
166
239
  for (const [key, value] of Object.entries(config)) if (!prioritizedKeys.has(key)) ordered[key] = value;
167
240
  return ordered;
@@ -189,6 +262,6 @@ function stripLegacyVoiceConfig(config) {
189
262
  return rest;
190
263
  }
191
264
  //#endregion
192
- 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 };
265
+ export { DEFAULT_TELEGRAM_API_ROOT as a, getGlobalPluginConfigFilePath as c, OPENCODE_TBOT_VERSION as i, getOpenCodeConfigFilePath as l, preparePluginConfiguration as n, loadAppConfig as o, writePluginConfigFile as r, getDefaultPluginLogDirectory as s, mergePluginConfigSources as t };
193
266
 
194
- //# sourceMappingURL=plugin-config-DNeV2Ckw.js.map
267
+ //# sourceMappingURL=plugin-config-jkAZYbFW.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin-config-jkAZYbFW.js","names":[],"sources":["../../src/app/opencode-paths.ts","../../src/app/config.ts","../../src/app/package-info.ts","../../src/app/plugin-config.ts"],"sourcesContent":["import { 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 OPENCODE_CONFIG_FILE_NAME = \"opencode.json\";\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 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","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 getGlobalPluginConfigFilePath,\n getOpenCodeConfigDirectory,\n getOpenCodeConfigFilePath,\n GLOBAL_PLUGIN_CONFIG_FILE_NAME,\n GLOBAL_PLUGIN_DIRECTORY_NAME,\n OPENCODE_CONFIG_FILE_NAME,\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 getGlobalPluginConfigFilePath,\n getOpenCodeConfigDirectory,\n getOpenCodeConfigFilePath,\n GLOBAL_PLUGIN_CONFIG_FILE_NAME,\n GLOBAL_PLUGIN_DIRECTORY_NAME,\n OPENCODE_CONFIG_FILE_NAME,\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 { a as writePluginConfigFile, n as getOpenCodeConfigFilePath, o as OPENCODE_TBOT_VERSION, r as mergePluginConfigSources, t as getGlobalPluginConfigFilePath } from "./assets/plugin-config-DNeV2Ckw.js";
1
+ import { c as getGlobalPluginConfigFilePath, i as OPENCODE_TBOT_VERSION, l as getOpenCodeConfigFilePath, r as writePluginConfigFile, s as getDefaultPluginLogDirectory, t as mergePluginConfigSources } from "./assets/plugin-config-jkAZYbFW.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";
@@ -53,6 +53,8 @@ async function installPlugin(options = {}) {
53
53
  else await writeJsonFile(openCodeConfigFilePath, nextOpenCodeConfig);
54
54
  await writePluginConfigFile(globalPluginConfigFilePath, nextPluginConfig);
55
55
  stdout.write("Success.\n");
56
+ stdout.write(`Plugin config: ${globalPluginConfigFilePath}\n`);
57
+ stdout.write(`Plugin logs: ${getDefaultPluginLogDirectory(homeDir)}\n`);
56
58
  await warnAboutLegacyLocalInstallations(homeDir);
57
59
  } finally {
58
60
  prompt.close();
@@ -63,6 +65,8 @@ async function updatePlugin(options = {}) {
63
65
  const openCodeConfigFilePath = getOpenCodeConfigFilePath(homeDir);
64
66
  await writeJsonFile(openCodeConfigFilePath, replacePluginRegistration(await readJsoncObject(openCodeConfigFilePath), normalizeOptionalString(options.pluginSpec) ?? DEFAULT_PLUGIN_SPEC));
65
67
  stdout.write("Success.\n");
68
+ stdout.write(`Plugin config: ${getGlobalPluginConfigFilePath(homeDir)}\n`);
69
+ stdout.write(`Plugin logs: ${getDefaultPluginLogDirectory(homeDir)}\n`);
66
70
  await warnAboutLegacyLocalInstallations(homeDir);
67
71
  }
68
72
  function parseCliOptions(argv) {