opencode-tbot 0.1.8 → 0.1.10

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
@@ -22,7 +22,7 @@ A Telegram plugin for driving [OpenCode](https://opencode.ai) from chat.
22
22
  Run:
23
23
 
24
24
  ```bash
25
- npm install opencode-tbot@latest
25
+ npm exec --package opencode-tbot@latest opencode-tbot -- install
26
26
  ```
27
27
 
28
28
  The installer registers the plugin globally and writes the default runtime config.
@@ -30,16 +30,16 @@ The installer registers the plugin globally and writes the default runtime confi
30
30
  Check the installed CLI version:
31
31
 
32
32
  ```bash
33
- npm exec opencode-tbot -- --version
33
+ npm exec --package opencode-tbot@latest opencode-tbot -- --version
34
34
  ```
35
35
 
36
36
  Update the registered npm plugin spec in OpenCode:
37
37
 
38
38
  ```bash
39
- npm exec opencode-tbot -- update
39
+ npm exec --package opencode-tbot@latest opencode-tbot -- update
40
40
  ```
41
41
 
42
- Remove the npm package from the current environment:
42
+ Remove a legacy local npm install if OpenCode is showing the plugin as a `file:///.../node_modules/...` path:
43
43
 
44
44
  ```bash
45
45
  npm uninstall opencode-tbot
package/README.zh-CN.md CHANGED
@@ -22,7 +22,7 @@
22
22
  执行:
23
23
 
24
24
  ```bash
25
- npm install opencode-tbot@latest
25
+ npm exec --package opencode-tbot@latest opencode-tbot -- install
26
26
  ```
27
27
 
28
28
  安装器会注册全局插件并写入默认运行时配置。
@@ -30,13 +30,13 @@ npm install opencode-tbot@latest
30
30
  查看已安装的 CLI 版本:
31
31
 
32
32
  ```bash
33
- npm exec opencode-tbot -- --version
33
+ npm exec --package opencode-tbot@latest opencode-tbot -- --version
34
34
  ```
35
35
 
36
36
  更新 OpenCode 中注册的 npm 插件 spec:
37
37
 
38
38
  ```bash
39
- npm exec opencode-tbot -- update
39
+ npm exec --package opencode-tbot@latest opencode-tbot -- update
40
40
  ```
41
41
 
42
42
  从当前环境卸载 npm 包:
package/dist/cli.js CHANGED
@@ -1,12 +1,13 @@
1
1
  import { a as writePluginConfigFile, n as getOpenCodeConfigFilePath, o as OPENCODE_TBOT_VERSION, r as mergePluginConfigSources, t as getGlobalPluginConfigFilePath } from "./assets/plugin-config-CGIe9zdA.js";
2
- import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
3
3
  import { homedir } from "node:os";
4
- import { dirname } from "node:path";
4
+ import { dirname, join, resolve } from "node:path";
5
5
  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
9
  var DEFAULT_PLUGIN_SPEC = "opencode-tbot@latest";
10
+ var PROMPT_CANCELLED_ERROR = "Prompt cancelled.";
10
11
  async function main(argv = process.argv.slice(2)) {
11
12
  try {
12
13
  const exitCode = await runCli(argv);
@@ -43,8 +44,8 @@ async function installPlugin(options = {}) {
43
44
  const existingPluginConfig = await readPluginConfigFile(globalPluginConfigFilePath);
44
45
  const prompt = createPromptSession();
45
46
  try {
46
- const botToken = normalizeRequiredString(options.botToken ?? await prompt.ask("Telegram bot token: "), "Telegram bot token is required.");
47
- const openrouterApiKey = options.enableVoice ?? await prompt.confirm("Enable voice transcription? (y/N): ", false) ? normalizeRequiredString(options.openrouterApiKey ?? await prompt.ask("OpenRouter API key: "), "OpenRouter API key is required when voice transcription is enabled.") : null;
47
+ const botToken = normalizeRequiredString(options.botToken ?? await prompt.askSecret("Telegram bot token: "), "Telegram bot token is required.");
48
+ const openrouterApiKey = options.enableVoice ?? await prompt.confirm("Enable voice transcription? (y/N): ", false) ? normalizeRequiredString(options.openrouterApiKey ?? await prompt.askSecret("OpenRouter API key: "), "OpenRouter API key is required when voice transcription is enabled.") : null;
48
49
  const telegramApiRoot = normalizeOptionalString(options.telegramApiRoot) ?? "https://api.telegram.org";
49
50
  const pluginSpec = normalizeOptionalString(options.pluginSpec) ?? DEFAULT_PLUGIN_SPEC;
50
51
  const nextOpenCodeConfig = options.registerPlugin === false ? openCodeConfig : ensurePluginRegistered(openCodeConfig, pluginSpec);
@@ -52,19 +53,18 @@ async function installPlugin(options = {}) {
52
53
  if (options.registerPlugin === false) await ensureParentDirectory(openCodeConfigFilePath);
53
54
  else await writeJsonFile(openCodeConfigFilePath, nextOpenCodeConfig);
54
55
  await writePluginConfigFile(globalPluginConfigFilePath, nextPluginConfig);
55
- if (options.registerPlugin === false) stdout.write(`Skipped plugin registration in ${openCodeConfigFilePath}\n`);
56
- else stdout.write(`Registered ${pluginSpec} in ${openCodeConfigFilePath}\n`);
57
- stdout.write(`Wrote plugin defaults to ${globalPluginConfigFilePath}\n`);
56
+ stdout.write("Success.\n");
57
+ await warnAboutLegacyLocalInstallations(homeDir);
58
58
  } finally {
59
59
  prompt.close();
60
60
  }
61
61
  }
62
62
  async function updatePlugin(options = {}) {
63
- const openCodeConfigFilePath = getOpenCodeConfigFilePath(options.homeDir ?? homedir());
64
- const openCodeConfig = await readJsoncObject(openCodeConfigFilePath);
65
- const pluginSpec = normalizeOptionalString(options.pluginSpec) ?? DEFAULT_PLUGIN_SPEC;
66
- await writeJsonFile(openCodeConfigFilePath, replacePluginRegistration(openCodeConfig, pluginSpec));
67
- stdout.write(`Updated plugin registration to ${pluginSpec} in ${openCodeConfigFilePath}\n`);
63
+ const homeDir = options.homeDir ?? homedir();
64
+ const openCodeConfigFilePath = getOpenCodeConfigFilePath(homeDir);
65
+ await writeJsonFile(openCodeConfigFilePath, replacePluginRegistration(await readJsoncObject(openCodeConfigFilePath), normalizeOptionalString(options.pluginSpec) ?? DEFAULT_PLUGIN_SPEC));
66
+ stdout.write("Success.\n");
67
+ await warnAboutLegacyLocalInstallations(homeDir);
68
68
  }
69
69
  function parseCliOptions(argv) {
70
70
  const args = [...argv];
@@ -186,34 +186,108 @@ async function writeJsonFile(filePath, value) {
186
186
  async function ensureParentDirectory(filePath) {
187
187
  await mkdir(dirname(filePath), { recursive: true });
188
188
  }
189
- function createPromptSession() {
190
- if (!stdin.isTTY || !stdout.isTTY) return {
189
+ function createPromptSession(options = {}) {
190
+ const input = options.input ?? stdin;
191
+ const output = options.output ?? stdout;
192
+ if (!input.isTTY || !output.isTTY) return {
191
193
  ask: async () => "",
194
+ askSecret: async () => "",
192
195
  async confirm(_question, defaultValue) {
193
196
  return defaultValue;
194
197
  },
195
198
  close() {}
196
199
  };
197
- const readline = createInterface({
198
- input: stdin,
199
- output: stdout
200
- });
201
200
  return {
202
201
  ask(question) {
203
- return readline.question(question);
202
+ return askQuestion(input, output, question);
203
+ },
204
+ askSecret(question) {
205
+ return askSecretQuestion(input, output, question);
204
206
  },
205
207
  async confirm(question, defaultValue) {
206
- const answer = normalizeOptionalString(await readline.question(question));
208
+ const answer = normalizeOptionalString(await askQuestion(input, output, question));
207
209
  if (!answer) return defaultValue;
208
210
  if (["y", "yes"].includes(answer.toLowerCase())) return true;
209
211
  if (["n", "no"].includes(answer.toLowerCase())) return false;
210
212
  throw new Error(`Unsupported answer: ${answer}`);
211
213
  },
212
- close() {
213
- readline.close();
214
- }
214
+ close() {}
215
215
  };
216
216
  }
217
+ async function askQuestion(input, output, question) {
218
+ const readline = createInterface({
219
+ input,
220
+ output
221
+ });
222
+ try {
223
+ return await readline.question(question);
224
+ } finally {
225
+ readline.close();
226
+ }
227
+ }
228
+ async function askSecretQuestion(input, output, question) {
229
+ if (typeof input.setRawMode !== "function") return askQuestion(input, output, question);
230
+ output.write(question);
231
+ try {
232
+ return await readMaskedInput(input, output);
233
+ } finally {
234
+ output.write("\n");
235
+ }
236
+ }
237
+ async function readMaskedInput(input, output) {
238
+ return new Promise((resolvePromise, rejectPromise) => {
239
+ const buffer = [];
240
+ let settled = false;
241
+ const cleanup = () => {
242
+ input.off("data", handleData);
243
+ input.off("error", handleError);
244
+ input.pause();
245
+ input.setRawMode?.(false);
246
+ };
247
+ const rejectWith = (error) => {
248
+ if (settled) return;
249
+ settled = true;
250
+ cleanup();
251
+ rejectPromise(error);
252
+ };
253
+ const resolveWith = (value) => {
254
+ if (settled) return;
255
+ settled = true;
256
+ cleanup();
257
+ resolvePromise(value);
258
+ };
259
+ const handleError = (error) => {
260
+ rejectWith(error);
261
+ };
262
+ const handleData = (chunk) => {
263
+ const text = Buffer.isBuffer(chunk) ? chunk.toString("utf8") : chunk;
264
+ for (const character of text) {
265
+ if (character === "\r" || character === "\n") {
266
+ resolveWith(buffer.join(""));
267
+ return;
268
+ }
269
+ if (character === "") {
270
+ rejectWith(new Error(PROMPT_CANCELLED_ERROR));
271
+ return;
272
+ }
273
+ if (character === "\b" || character === "") {
274
+ if (buffer.length > 0) {
275
+ buffer.pop();
276
+ output.write("\b \b");
277
+ }
278
+ continue;
279
+ }
280
+ if (character < " " || character === "") continue;
281
+ buffer.push(character);
282
+ output.write("*");
283
+ }
284
+ };
285
+ input.setRawMode?.(true);
286
+ input.resume();
287
+ input.on("error", handleError);
288
+ input.on("data", handleData);
289
+ });
290
+ }
217
291
  function normalizeOptionalString(value) {
218
292
  const normalized = value?.trim();
219
293
  return normalized ? normalized : null;
@@ -233,11 +307,43 @@ function isOpencodeTbotPluginSpec(value) {
233
307
  const normalized = value.trim();
234
308
  return normalized === "opencode-tbot" || normalized.startsWith("opencode-tbot@");
235
309
  }
310
+ async function warnAboutLegacyLocalInstallations(homeDir) {
311
+ const legacyInstallations = await findLegacyLocalInstallations(homeDir);
312
+ if (legacyInstallations.length === 0) return;
313
+ stdout.write("\nDetected local opencode-tbot npm installation(s) that can make OpenCode show the plugin as file:///.../node_modules/...\n");
314
+ for (const installation of legacyInstallations) {
315
+ stdout.write(`- ${installation.packagePath}\n`);
316
+ stdout.write(` cleanup: cd "${installation.rootDir}" && npm uninstall opencode-tbot\n`);
317
+ }
318
+ stdout.write("Recommended npm flow: npm exec --package opencode-tbot@latest opencode-tbot -- install\n");
319
+ }
320
+ async function findLegacyLocalInstallations(homeDir) {
321
+ const roots = [...new Set([resolve(process.cwd()), resolve(homeDir)])];
322
+ const installations = [];
323
+ for (const rootDir of roots) if (await pathExists(join(rootDir, "node_modules", "opencode-tbot", "package.json"))) installations.push({
324
+ packagePath: join(rootDir, "node_modules", "opencode-tbot"),
325
+ rootDir
326
+ });
327
+ return installations;
328
+ }
329
+ async function pathExists(filePath) {
330
+ try {
331
+ await stat(filePath);
332
+ return true;
333
+ } catch {
334
+ return false;
335
+ }
336
+ }
236
337
  function buildHelpText() {
237
338
  return [
238
339
  "Usage: opencode-tbot [install|update] [options]",
239
340
  " opencode-tbot --version",
240
341
  "",
342
+ "Recommended npm usage:",
343
+ " npm exec --package opencode-tbot@latest opencode-tbot -- install",
344
+ " npm exec --package opencode-tbot@latest opencode-tbot -- update",
345
+ " npm exec --package opencode-tbot@latest opencode-tbot -- --version",
346
+ "",
241
347
  "Commands:",
242
348
  " install",
243
349
  " update",
@@ -259,6 +365,6 @@ function formatCliError(error) {
259
365
  return error instanceof Error && error.message.trim().length > 0 ? error.message.trim() : String(error);
260
366
  }
261
367
  //#endregion
262
- export { installPlugin, main, runCli, updatePlugin };
368
+ export { createPromptSession, installPlugin, main, runCli, updatePlugin };
263
369
 
264
370
  //# sourceMappingURL=cli.js.map
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","names":[],"sources":["../src/cli.ts"],"sourcesContent":["import { mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { dirname } from \"node:path\";\nimport { stderr, stdin, stdout } from \"node:process\";\nimport { createInterface } from \"node:readline/promises\";\nimport { parse } from \"jsonc-parser\";\nimport {\n DEFAULT_TELEGRAM_API_ROOT,\n type PluginConfigSource,\n} from \"./app/config.js\";\nimport { OPENCODE_TBOT_VERSION } from \"./app/package-info.js\";\nimport {\n getGlobalPluginConfigFilePath,\n getOpenCodeConfigFilePath,\n mergePluginConfigSources,\n writePluginConfigFile,\n} from \"./app/plugin-config.js\";\n\nexport interface InstallCommandOptions {\n botToken?: string;\n enableVoice?: boolean;\n homeDir?: string;\n openrouterApiKey?: string;\n pluginSpec?: string;\n registerPlugin?: boolean;\n telegramApiRoot?: string;\n}\n\ninterface CliOptions extends InstallCommandOptions {\n command: \"help\" | \"install\" | \"update\" | \"version\";\n}\n\ninterface OpenCodeGlobalConfig {\n plugin?: string[];\n [key: string]: unknown;\n}\n\nconst DEFAULT_PLUGIN_SPEC = \"opencode-tbot@latest\";\n\nexport async function main(argv: string[] = process.argv.slice(2)): Promise<number> {\n try {\n const exitCode = await runCli(argv);\n process.exitCode = exitCode;\n\n return exitCode;\n } catch (error) {\n stderr.write(`${formatCliError(error)}\\n`);\n process.exitCode = 1;\n\n return 1;\n }\n}\n\nexport async function runCli(argv: string[]): Promise<number> {\n const options = parseCliOptions(argv);\n\n if (options.command === \"help\") {\n stdout.write(`${buildHelpText()}\\n`);\n return 0;\n }\n\n if (options.command === \"version\") {\n stdout.write(`${OPENCODE_TBOT_VERSION}\\n`);\n return 0;\n }\n\n if (options.command === \"update\") {\n await updatePlugin(options);\n return 0;\n }\n\n await installPlugin(options);\n return 0;\n}\n\nexport async function installPlugin(options: InstallCommandOptions = {}): Promise<void> {\n const homeDir = options.homeDir ?? homedir();\n const openCodeConfigFilePath = getOpenCodeConfigFilePath(homeDir);\n const globalPluginConfigFilePath = getGlobalPluginConfigFilePath(homeDir);\n const openCodeConfig = await readJsoncObject<OpenCodeGlobalConfig>(openCodeConfigFilePath);\n const existingPluginConfig = await readPluginConfigFile(globalPluginConfigFilePath);\n const prompt = createPromptSession();\n\n try {\n const botToken = normalizeRequiredString(\n options.botToken ?? await prompt.ask(\"Telegram bot token: \"),\n \"Telegram bot token is required.\",\n );\n const enableVoice = options.enableVoice ?? await prompt.confirm(\n \"Enable voice transcription? (y/N): \",\n false,\n );\n const openrouterApiKey = enableVoice\n ? normalizeRequiredString(\n options.openrouterApiKey ?? await prompt.ask(\"OpenRouter API key: \"),\n \"OpenRouter API key is required when voice transcription is enabled.\",\n )\n : null;\n const telegramApiRoot = normalizeOptionalString(options.telegramApiRoot) ?? DEFAULT_TELEGRAM_API_ROOT;\n const pluginSpec = normalizeOptionalString(options.pluginSpec) ?? DEFAULT_PLUGIN_SPEC;\n const nextOpenCodeConfig = options.registerPlugin === false\n ? openCodeConfig\n : ensurePluginRegistered(openCodeConfig, pluginSpec);\n const nextPluginConfig = buildInstalledPluginConfig(\n existingPluginConfig,\n botToken,\n telegramApiRoot,\n openrouterApiKey,\n );\n\n if (options.registerPlugin === false) {\n await ensureParentDirectory(openCodeConfigFilePath);\n } else {\n await writeJsonFile(openCodeConfigFilePath, nextOpenCodeConfig);\n }\n await writePluginConfigFile(globalPluginConfigFilePath, nextPluginConfig);\n\n if (options.registerPlugin === false) {\n stdout.write(`Skipped plugin registration in ${openCodeConfigFilePath}\\n`);\n } else {\n stdout.write(`Registered ${pluginSpec} in ${openCodeConfigFilePath}\\n`);\n }\n stdout.write(`Wrote plugin defaults to ${globalPluginConfigFilePath}\\n`);\n } finally {\n prompt.close();\n }\n}\n\nexport async function updatePlugin(options: Pick<InstallCommandOptions, \"homeDir\" | \"pluginSpec\"> = {}): Promise<void> {\n const homeDir = options.homeDir ?? homedir();\n const openCodeConfigFilePath = getOpenCodeConfigFilePath(homeDir);\n const openCodeConfig = await readJsoncObject<OpenCodeGlobalConfig>(openCodeConfigFilePath);\n const pluginSpec = normalizeOptionalString(options.pluginSpec) ?? DEFAULT_PLUGIN_SPEC;\n const nextOpenCodeConfig = replacePluginRegistration(openCodeConfig, pluginSpec);\n\n await writeJsonFile(openCodeConfigFilePath, nextOpenCodeConfig);\n stdout.write(`Updated plugin registration to ${pluginSpec} in ${openCodeConfigFilePath}\\n`);\n}\n\nfunction parseCliOptions(argv: string[]): CliOptions {\n const args = [...argv];\n const first = args[0];\n const command = !first || first.startsWith(\"-\")\n ? \"install\"\n : first;\n const values = command === \"install\" || command === \"version\"\n ? args\n : args.slice(1);\n const options: CliOptions = {\n command: command === \"help\" || command === \"--help\" || command === \"-h\"\n ? \"help\"\n : command === \"version\" || command === \"--version\" || command === \"-v\"\n ? \"version\"\n : command === \"update\"\n ? \"update\"\n : \"install\",\n };\n\n for (let index = 0; index < values.length; index += 1) {\n const value = values[index];\n\n if (index === 0 && !value.startsWith(\"-\")) {\n continue;\n }\n\n switch (value) {\n case \"--bot-token\":\n options.botToken = values[++index];\n break;\n case \"--enable-voice\":\n options.enableVoice = true;\n break;\n case \"--disable-voice\":\n options.enableVoice = false;\n break;\n case \"--openrouter-api-key\":\n options.openrouterApiKey = values[++index];\n break;\n case \"--plugin-spec\":\n options.pluginSpec = values[++index];\n break;\n case \"--telegram-api-root\":\n options.telegramApiRoot = values[++index];\n break;\n case \"--skip-register\":\n options.registerPlugin = false;\n break;\n case \"--home-dir\":\n options.homeDir = values[++index];\n break;\n case \"--help\":\n case \"-h\":\n options.command = \"help\";\n break;\n case \"--version\":\n case \"-v\":\n options.command = \"version\";\n break;\n default:\n throw new Error(`Unknown argument: ${value}`);\n }\n }\n\n return options;\n}\n\nfunction buildInstalledPluginConfig(\n current: PluginConfigSource,\n botToken: string,\n telegramApiRoot: string,\n openrouterApiKey: string | null,\n): PluginConfigSource {\n const merged = mergePluginConfigSources(current, {\n telegram: {\n botToken,\n apiRoot: telegramApiRoot,\n },\n });\n const nextOpenRouter = openrouterApiKey\n ? {\n ...(merged.openrouter ?? {}),\n apiKey: openrouterApiKey,\n }\n : removeOpenRouterApiKey(merged.openrouter);\n\n return {\n ...merged,\n ...(nextOpenRouter && Object.keys(nextOpenRouter).length > 0\n ? { openrouter: nextOpenRouter }\n : {}),\n };\n}\n\nfunction removeOpenRouterApiKey(\n config: PluginConfigSource[\"openrouter\"],\n): PluginConfigSource[\"openrouter\"] | undefined {\n if (!config) {\n return undefined;\n }\n\n const { apiKey: _apiKey, ...rest } = config;\n\n return Object.keys(rest).length > 0 ? rest : undefined;\n}\n\nfunction ensurePluginRegistered(\n config: OpenCodeGlobalConfig,\n pluginSpec: string,\n): OpenCodeGlobalConfig {\n const plugins = Array.isArray(config.plugin)\n ? config.plugin.filter((item): item is string => typeof item === \"string\")\n : [];\n\n if (!plugins.includes(pluginSpec)) {\n plugins.push(pluginSpec);\n }\n\n return {\n ...config,\n plugin: plugins,\n };\n}\n\nfunction replacePluginRegistration(\n config: OpenCodeGlobalConfig,\n pluginSpec: string,\n): OpenCodeGlobalConfig {\n const currentPlugins = Array.isArray(config.plugin)\n ? config.plugin.filter((item): item is string => typeof item === \"string\")\n : [];\n const nextPlugins: string[] = [];\n let inserted = false;\n\n for (const currentPlugin of currentPlugins) {\n if (isOpencodeTbotPluginSpec(currentPlugin)) {\n if (!inserted) {\n nextPlugins.push(pluginSpec);\n inserted = true;\n }\n\n continue;\n }\n\n nextPlugins.push(currentPlugin);\n }\n\n if (!inserted) {\n nextPlugins.push(pluginSpec);\n }\n\n return {\n ...config,\n plugin: nextPlugins,\n };\n}\n\nasync function readPluginConfigFile(configFilePath: string): Promise<PluginConfigSource> {\n try {\n const content = await readFile(configFilePath, \"utf8\");\n const parsed = JSON.parse(content) as unknown;\n\n return isPlainObject(parsed)\n ? parsed as PluginConfigSource\n : {};\n } catch (error) {\n if (isMissingFileError(error)) {\n return {};\n }\n\n throw error;\n }\n}\n\nasync function readJsoncObject<TObject extends Record<string, unknown>>(\n filePath: string,\n): Promise<TObject> {\n try {\n const content = await readFile(filePath, \"utf8\");\n const parsed = parse(content) as unknown;\n\n return isPlainObject(parsed)\n ? parsed as TObject\n : {} as TObject;\n } catch (error) {\n if (isMissingFileError(error)) {\n return {} as TObject;\n }\n\n throw error;\n }\n}\n\nasync function writeJsonFile(filePath: string, value: Record<string, unknown>): Promise<void> {\n await ensureParentDirectory(filePath);\n await writeFile(filePath, `${JSON.stringify(value, null, 2)}\\n`, \"utf8\");\n}\n\nasync function ensureParentDirectory(filePath: string): Promise<void> {\n await mkdir(dirname(filePath), { recursive: true });\n}\n\nfunction createPromptSession() {\n if (!stdin.isTTY || !stdout.isTTY) {\n return {\n ask: async () => \"\",\n async confirm(_question: string, defaultValue: boolean) {\n return defaultValue;\n },\n close() { },\n };\n }\n\n const readline = createInterface({\n input: stdin,\n output: stdout,\n });\n\n return {\n ask(question: string) {\n return readline.question(question);\n },\n async confirm(question: string, defaultValue: boolean) {\n const answer = normalizeOptionalString(await readline.question(question));\n\n if (!answer) {\n return defaultValue;\n }\n\n if ([\"y\", \"yes\"].includes(answer.toLowerCase())) {\n return true;\n }\n\n if ([\"n\", \"no\"].includes(answer.toLowerCase())) {\n return false;\n }\n\n throw new Error(`Unsupported answer: ${answer}`);\n },\n close() {\n readline.close();\n },\n };\n}\n\nfunction normalizeOptionalString(value: string | undefined | null): string | null {\n const normalized = value?.trim();\n\n return normalized\n ? normalized\n : null;\n}\n\nfunction normalizeRequiredString(value: string | undefined | null, errorMessage: string): string {\n const normalized = normalizeOptionalString(value);\n\n if (!normalized) {\n throw new Error(errorMessage);\n }\n\n return normalized;\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\nfunction isOpencodeTbotPluginSpec(value: string): boolean {\n const normalized = value.trim();\n\n return normalized === \"opencode-tbot\" || normalized.startsWith(\"opencode-tbot@\");\n}\n\nfunction buildHelpText(): string {\n return [\n \"Usage: opencode-tbot [install|update] [options]\",\n \" opencode-tbot --version\",\n \"\",\n \"Commands:\",\n \" install\",\n \" update\",\n \"\",\n \"Options:\",\n \" --bot-token <token>\",\n \" --enable-voice\",\n \" --disable-voice\",\n \" --openrouter-api-key <key>\",\n \" --telegram-api-root <url>\",\n \" --plugin-spec <spec>\",\n \" --skip-register\",\n \" --home-dir <path>\",\n \" --version\",\n \" --help\",\n ].join(\"\\n\");\n}\n\nfunction formatCliError(error: unknown): string {\n return error instanceof Error && error.message.trim().length > 0\n ? error.message.trim()\n : String(error);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"cli.js","names":[],"sources":["../src/cli.ts"],"sourcesContent":["import { mkdir, readFile, stat, writeFile } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { stderr, stdin, stdout } from \"node:process\";\nimport { createInterface } from \"node:readline/promises\";\nimport { parse } from \"jsonc-parser\";\nimport {\n DEFAULT_TELEGRAM_API_ROOT,\n type PluginConfigSource,\n} from \"./app/config.js\";\nimport { OPENCODE_TBOT_VERSION } from \"./app/package-info.js\";\nimport {\n getGlobalPluginConfigFilePath,\n getOpenCodeConfigFilePath,\n mergePluginConfigSources,\n writePluginConfigFile,\n} from \"./app/plugin-config.js\";\n\nexport interface InstallCommandOptions {\n botToken?: string;\n enableVoice?: boolean;\n homeDir?: string;\n openrouterApiKey?: string;\n pluginSpec?: string;\n registerPlugin?: boolean;\n telegramApiRoot?: string;\n}\n\ninterface CliOptions extends InstallCommandOptions {\n command: \"help\" | \"install\" | \"update\" | \"version\";\n}\n\nexport interface PromptSessionLike {\n ask(question: string): Promise<string>;\n askSecret(question: string): Promise<string>;\n confirm(question: string, defaultValue: boolean): Promise<boolean>;\n close(): void;\n}\n\nexport interface CreatePromptSessionOptions {\n input?: NodeJS.ReadStream;\n output?: NodeJS.WriteStream;\n}\n\ninterface OpenCodeGlobalConfig {\n plugin?: string[];\n [key: string]: unknown;\n}\n\nconst DEFAULT_PLUGIN_SPEC = \"opencode-tbot@latest\";\nconst PROMPT_CANCELLED_ERROR = \"Prompt cancelled.\";\n\nexport async function main(argv: string[] = process.argv.slice(2)): Promise<number> {\n try {\n const exitCode = await runCli(argv);\n process.exitCode = exitCode;\n\n return exitCode;\n } catch (error) {\n stderr.write(`${formatCliError(error)}\\n`);\n process.exitCode = 1;\n\n return 1;\n }\n}\n\nexport async function runCli(argv: string[]): Promise<number> {\n const options = parseCliOptions(argv);\n\n if (options.command === \"help\") {\n stdout.write(`${buildHelpText()}\\n`);\n return 0;\n }\n\n if (options.command === \"version\") {\n stdout.write(`${OPENCODE_TBOT_VERSION}\\n`);\n return 0;\n }\n\n if (options.command === \"update\") {\n await updatePlugin(options);\n return 0;\n }\n\n await installPlugin(options);\n return 0;\n}\n\nexport async function installPlugin(options: InstallCommandOptions = {}): Promise<void> {\n const homeDir = options.homeDir ?? homedir();\n const openCodeConfigFilePath = getOpenCodeConfigFilePath(homeDir);\n const globalPluginConfigFilePath = getGlobalPluginConfigFilePath(homeDir);\n const openCodeConfig = await readJsoncObject<OpenCodeGlobalConfig>(openCodeConfigFilePath);\n const existingPluginConfig = await readPluginConfigFile(globalPluginConfigFilePath);\n const prompt = createPromptSession();\n\n try {\n const botToken = normalizeRequiredString(\n options.botToken ?? await prompt.askSecret(\"Telegram bot token: \"),\n \"Telegram bot token is required.\",\n );\n const enableVoice = options.enableVoice ?? await prompt.confirm(\n \"Enable voice transcription? (y/N): \",\n false,\n );\n const openrouterApiKey = enableVoice\n ? normalizeRequiredString(\n options.openrouterApiKey ?? await prompt.askSecret(\"OpenRouter API key: \"),\n \"OpenRouter API key is required when voice transcription is enabled.\",\n )\n : null;\n const telegramApiRoot = normalizeOptionalString(options.telegramApiRoot) ?? DEFAULT_TELEGRAM_API_ROOT;\n const pluginSpec = normalizeOptionalString(options.pluginSpec) ?? DEFAULT_PLUGIN_SPEC;\n const nextOpenCodeConfig = options.registerPlugin === false\n ? openCodeConfig\n : ensurePluginRegistered(openCodeConfig, pluginSpec);\n const nextPluginConfig = buildInstalledPluginConfig(\n existingPluginConfig,\n botToken,\n telegramApiRoot,\n openrouterApiKey,\n );\n\n if (options.registerPlugin === false) {\n await ensureParentDirectory(openCodeConfigFilePath);\n } else {\n await writeJsonFile(openCodeConfigFilePath, nextOpenCodeConfig);\n }\n await writePluginConfigFile(globalPluginConfigFilePath, nextPluginConfig);\n\n stdout.write(\"Success.\\n\");\n await warnAboutLegacyLocalInstallations(homeDir);\n } finally {\n prompt.close();\n }\n}\n\nexport async function updatePlugin(options: Pick<InstallCommandOptions, \"homeDir\" | \"pluginSpec\"> = {}): Promise<void> {\n const homeDir = options.homeDir ?? homedir();\n const openCodeConfigFilePath = getOpenCodeConfigFilePath(homeDir);\n const openCodeConfig = await readJsoncObject<OpenCodeGlobalConfig>(openCodeConfigFilePath);\n const pluginSpec = normalizeOptionalString(options.pluginSpec) ?? DEFAULT_PLUGIN_SPEC;\n const nextOpenCodeConfig = replacePluginRegistration(openCodeConfig, pluginSpec);\n\n await writeJsonFile(openCodeConfigFilePath, nextOpenCodeConfig);\n stdout.write(\"Success.\\n\");\n await warnAboutLegacyLocalInstallations(homeDir);\n}\n\nfunction parseCliOptions(argv: string[]): CliOptions {\n const args = [...argv];\n const first = args[0];\n const command = !first || first.startsWith(\"-\")\n ? \"install\"\n : first;\n const values = command === \"install\" || command === \"version\"\n ? args\n : args.slice(1);\n const options: CliOptions = {\n command: command === \"help\" || command === \"--help\" || command === \"-h\"\n ? \"help\"\n : command === \"version\" || command === \"--version\" || command === \"-v\"\n ? \"version\"\n : command === \"update\"\n ? \"update\"\n : \"install\",\n };\n\n for (let index = 0; index < values.length; index += 1) {\n const value = values[index];\n\n if (index === 0 && !value.startsWith(\"-\")) {\n continue;\n }\n\n switch (value) {\n case \"--bot-token\":\n options.botToken = values[++index];\n break;\n case \"--enable-voice\":\n options.enableVoice = true;\n break;\n case \"--disable-voice\":\n options.enableVoice = false;\n break;\n case \"--openrouter-api-key\":\n options.openrouterApiKey = values[++index];\n break;\n case \"--plugin-spec\":\n options.pluginSpec = values[++index];\n break;\n case \"--telegram-api-root\":\n options.telegramApiRoot = values[++index];\n break;\n case \"--skip-register\":\n options.registerPlugin = false;\n break;\n case \"--home-dir\":\n options.homeDir = values[++index];\n break;\n case \"--help\":\n case \"-h\":\n options.command = \"help\";\n break;\n case \"--version\":\n case \"-v\":\n options.command = \"version\";\n break;\n default:\n throw new Error(`Unknown argument: ${value}`);\n }\n }\n\n return options;\n}\n\nfunction buildInstalledPluginConfig(\n current: PluginConfigSource,\n botToken: string,\n telegramApiRoot: string,\n openrouterApiKey: string | null,\n): PluginConfigSource {\n const merged = mergePluginConfigSources(current, {\n telegram: {\n botToken,\n apiRoot: telegramApiRoot,\n },\n });\n const nextOpenRouter = openrouterApiKey\n ? {\n ...(merged.openrouter ?? {}),\n apiKey: openrouterApiKey,\n }\n : removeOpenRouterApiKey(merged.openrouter);\n\n return {\n ...merged,\n ...(nextOpenRouter && Object.keys(nextOpenRouter).length > 0\n ? { openrouter: nextOpenRouter }\n : {}),\n };\n}\n\nfunction removeOpenRouterApiKey(\n config: PluginConfigSource[\"openrouter\"],\n): PluginConfigSource[\"openrouter\"] | undefined {\n if (!config) {\n return undefined;\n }\n\n const { apiKey: _apiKey, ...rest } = config;\n\n return Object.keys(rest).length > 0 ? rest : undefined;\n}\n\nfunction ensurePluginRegistered(\n config: OpenCodeGlobalConfig,\n pluginSpec: string,\n): OpenCodeGlobalConfig {\n const plugins = Array.isArray(config.plugin)\n ? config.plugin.filter((item): item is string => typeof item === \"string\")\n : [];\n\n if (!plugins.includes(pluginSpec)) {\n plugins.push(pluginSpec);\n }\n\n return {\n ...config,\n plugin: plugins,\n };\n}\n\nfunction replacePluginRegistration(\n config: OpenCodeGlobalConfig,\n pluginSpec: string,\n): OpenCodeGlobalConfig {\n const currentPlugins = Array.isArray(config.plugin)\n ? config.plugin.filter((item): item is string => typeof item === \"string\")\n : [];\n const nextPlugins: string[] = [];\n let inserted = false;\n\n for (const currentPlugin of currentPlugins) {\n if (isOpencodeTbotPluginSpec(currentPlugin)) {\n if (!inserted) {\n nextPlugins.push(pluginSpec);\n inserted = true;\n }\n\n continue;\n }\n\n nextPlugins.push(currentPlugin);\n }\n\n if (!inserted) {\n nextPlugins.push(pluginSpec);\n }\n\n return {\n ...config,\n plugin: nextPlugins,\n };\n}\n\nasync function readPluginConfigFile(configFilePath: string): Promise<PluginConfigSource> {\n try {\n const content = await readFile(configFilePath, \"utf8\");\n const parsed = JSON.parse(content) as unknown;\n\n return isPlainObject(parsed)\n ? parsed as PluginConfigSource\n : {};\n } catch (error) {\n if (isMissingFileError(error)) {\n return {};\n }\n\n throw error;\n }\n}\n\nasync function readJsoncObject<TObject extends Record<string, unknown>>(\n filePath: string,\n): Promise<TObject> {\n try {\n const content = await readFile(filePath, \"utf8\");\n const parsed = parse(content) as unknown;\n\n return isPlainObject(parsed)\n ? parsed as TObject\n : {} as TObject;\n } catch (error) {\n if (isMissingFileError(error)) {\n return {} as TObject;\n }\n\n throw error;\n }\n}\n\nasync function writeJsonFile(filePath: string, value: Record<string, unknown>): Promise<void> {\n await ensureParentDirectory(filePath);\n await writeFile(filePath, `${JSON.stringify(value, null, 2)}\\n`, \"utf8\");\n}\n\nasync function ensureParentDirectory(filePath: string): Promise<void> {\n await mkdir(dirname(filePath), { recursive: true });\n}\n\nexport function createPromptSession(options: CreatePromptSessionOptions = {}): PromptSessionLike {\n const input = options.input ?? stdin;\n const output = options.output ?? stdout;\n\n if (!input.isTTY || !output.isTTY) {\n return {\n ask: async () => \"\",\n askSecret: async () => \"\",\n async confirm(_question: string, defaultValue: boolean) {\n return defaultValue;\n },\n close() { },\n };\n }\n\n return {\n ask(question: string) {\n return askQuestion(input, output, question);\n },\n askSecret(question: string) {\n return askSecretQuestion(input, output, question);\n },\n async confirm(question: string, defaultValue: boolean) {\n const answer = normalizeOptionalString(await askQuestion(input, output, question));\n\n if (!answer) {\n return defaultValue;\n }\n\n if ([\"y\", \"yes\"].includes(answer.toLowerCase())) {\n return true;\n }\n\n if ([\"n\", \"no\"].includes(answer.toLowerCase())) {\n return false;\n }\n\n throw new Error(`Unsupported answer: ${answer}`);\n },\n close() { },\n };\n}\n\nasync function askQuestion(\n input: NodeJS.ReadStream,\n output: NodeJS.WriteStream,\n question: string,\n): Promise<string> {\n const readline = createInterface({ input, output });\n\n try {\n return await readline.question(question);\n } finally {\n readline.close();\n }\n}\n\nasync function askSecretQuestion(\n input: NodeJS.ReadStream,\n output: NodeJS.WriteStream,\n question: string,\n): Promise<string> {\n if (typeof input.setRawMode !== \"function\") {\n return askQuestion(input, output, question);\n }\n\n output.write(question);\n\n try {\n return await readMaskedInput(input, output);\n } finally {\n output.write(\"\\n\");\n }\n}\n\nasync function readMaskedInput(\n input: NodeJS.ReadStream,\n output: NodeJS.WriteStream,\n): Promise<string> {\n return new Promise((resolvePromise, rejectPromise) => {\n const buffer: string[] = [];\n let settled = false;\n\n const cleanup = () => {\n input.off(\"data\", handleData);\n input.off(\"error\", handleError);\n input.pause();\n input.setRawMode?.(false);\n };\n\n const rejectWith = (error: unknown) => {\n if (settled) {\n return;\n }\n\n settled = true;\n cleanup();\n rejectPromise(error);\n };\n\n const resolveWith = (value: string) => {\n if (settled) {\n return;\n }\n\n settled = true;\n cleanup();\n resolvePromise(value);\n };\n\n const handleError = (error: unknown) => {\n rejectWith(error);\n };\n\n const handleData = (chunk: string | Buffer) => {\n const text = Buffer.isBuffer(chunk) ? chunk.toString(\"utf8\") : chunk;\n\n for (const character of text) {\n if (character === \"\\r\" || character === \"\\n\") {\n resolveWith(buffer.join(\"\"));\n return;\n }\n\n if (character === \"\\u0003\") {\n rejectWith(new Error(PROMPT_CANCELLED_ERROR));\n return;\n }\n\n if (character === \"\\u0008\" || character === \"\\u007f\") {\n if (buffer.length > 0) {\n buffer.pop();\n output.write(\"\\b \\b\");\n }\n continue;\n }\n\n if (character < \" \" || character === \"\\u007f\") {\n continue;\n }\n\n buffer.push(character);\n output.write(\"*\");\n }\n };\n\n input.setRawMode?.(true);\n input.resume();\n input.on(\"error\", handleError);\n input.on(\"data\", handleData);\n });\n}\n\nfunction normalizeOptionalString(value: string | undefined | null): string | null {\n const normalized = value?.trim();\n\n return normalized\n ? normalized\n : null;\n}\n\nfunction normalizeRequiredString(value: string | undefined | null, errorMessage: string): string {\n const normalized = normalizeOptionalString(value);\n\n if (!normalized) {\n throw new Error(errorMessage);\n }\n\n return normalized;\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\nfunction isOpencodeTbotPluginSpec(value: string): boolean {\n const normalized = value.trim();\n\n return normalized === \"opencode-tbot\" || normalized.startsWith(\"opencode-tbot@\");\n}\n\nasync function warnAboutLegacyLocalInstallations(homeDir: string): Promise<void> {\n const legacyInstallations = await findLegacyLocalInstallations(homeDir);\n\n if (legacyInstallations.length === 0) {\n return;\n }\n\n stdout.write(\"\\nDetected local opencode-tbot npm installation(s) that can make OpenCode show the plugin as file:///.../node_modules/...\\n\");\n\n for (const installation of legacyInstallations) {\n stdout.write(`- ${installation.packagePath}\\n`);\n stdout.write(` cleanup: cd \"${installation.rootDir}\" && npm uninstall opencode-tbot\\n`);\n }\n\n stdout.write(\"Recommended npm flow: npm exec --package opencode-tbot@latest opencode-tbot -- install\\n\");\n}\n\nasync function findLegacyLocalInstallations(homeDir: string) {\n const roots = [...new Set([\n resolve(process.cwd()),\n resolve(homeDir),\n ])];\n const installations: Array<{ packagePath: string; rootDir: string }> = [];\n\n for (const rootDir of roots) {\n const packagePath = join(rootDir, \"node_modules\", \"opencode-tbot\", \"package.json\");\n\n if (await pathExists(packagePath)) {\n installations.push({\n packagePath: join(rootDir, \"node_modules\", \"opencode-tbot\"),\n rootDir,\n });\n }\n }\n\n return installations;\n}\n\nasync function pathExists(filePath: string): Promise<boolean> {\n try {\n await stat(filePath);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction buildHelpText(): string {\n return [\n \"Usage: opencode-tbot [install|update] [options]\",\n \" opencode-tbot --version\",\n \"\",\n \"Recommended npm usage:\",\n \" npm exec --package opencode-tbot@latest opencode-tbot -- install\",\n \" npm exec --package opencode-tbot@latest opencode-tbot -- update\",\n \" npm exec --package opencode-tbot@latest opencode-tbot -- --version\",\n \"\",\n \"Commands:\",\n \" install\",\n \" update\",\n \"\",\n \"Options:\",\n \" --bot-token <token>\",\n \" --enable-voice\",\n \" --disable-voice\",\n \" --openrouter-api-key <key>\",\n \" --telegram-api-root <url>\",\n \" --plugin-spec <spec>\",\n \" --skip-register\",\n \" --home-dir <path>\",\n \" --version\",\n \" --help\",\n ].join(\"\\n\");\n}\n\nfunction formatCliError(error: unknown): string {\n return error instanceof Error && error.message.trim().length > 0\n ? error.message.trim()\n : String(error);\n}\n"],"mappings":"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"}