@grinev/opencode-telegram-bot 0.1.0-rc.7 → 0.1.0

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.
Files changed (39) hide show
  1. package/.env.example +3 -1
  2. package/README.md +183 -50
  3. package/dist/app/start-bot-app.js +15 -1
  4. package/dist/bot/commands/agent.js +2 -1
  5. package/dist/bot/commands/definitions.js +16 -15
  6. package/dist/bot/commands/help.js +2 -5
  7. package/dist/bot/commands/model.js +2 -1
  8. package/dist/bot/commands/models.js +7 -6
  9. package/dist/bot/commands/new.js +4 -3
  10. package/dist/bot/commands/opencode-start.js +18 -20
  11. package/dist/bot/commands/opencode-stop.js +7 -9
  12. package/dist/bot/commands/projects.js +9 -6
  13. package/dist/bot/commands/sessions.js +15 -13
  14. package/dist/bot/commands/start.js +2 -9
  15. package/dist/bot/commands/status.js +21 -17
  16. package/dist/bot/commands/stop.js +10 -9
  17. package/dist/bot/handlers/agent.js +6 -5
  18. package/dist/bot/handlers/context.js +14 -16
  19. package/dist/bot/handlers/model.js +7 -6
  20. package/dist/bot/handlers/permission.js +32 -26
  21. package/dist/bot/handlers/question.js +39 -32
  22. package/dist/bot/handlers/variant.js +10 -9
  23. package/dist/bot/index.js +27 -26
  24. package/dist/bot/utils/keyboard.js +6 -3
  25. package/dist/cli/args.js +7 -6
  26. package/dist/cli.js +7 -17
  27. package/dist/config.js +15 -0
  28. package/dist/i18n/en.js +205 -0
  29. package/dist/i18n/index.js +50 -0
  30. package/dist/i18n/ru.js +205 -0
  31. package/dist/keyboard/manager.js +2 -1
  32. package/dist/opencode/events.js +4 -4
  33. package/dist/pinned/manager.js +16 -11
  34. package/dist/question/manager.js +1 -1
  35. package/dist/runtime/bootstrap.js +15 -11
  36. package/dist/summary/aggregator.js +2 -2
  37. package/dist/summary/formatter.js +9 -6
  38. package/dist/utils/logger.js +68 -0
  39. package/package.json +18 -13
@@ -5,6 +5,7 @@ import { createInterface } from "node:readline/promises";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import dotenv from "dotenv";
7
7
  import { getRuntimePaths } from "./paths.js";
8
+ import { t } from "../i18n/index.js";
8
9
  const DEFAULT_API_URL = "http://localhost:4096";
9
10
  const FALLBACK_MODEL_PROVIDER = "opencode";
10
11
  const FALLBACK_MODEL_ID = "big-pickle";
@@ -175,13 +176,13 @@ async function askHidden(question) {
175
176
  }
176
177
  async function askToken() {
177
178
  for (;;) {
178
- const token = await askHidden("Введите токен Telegram-бота (получить у @BotFather).\n> ");
179
+ const token = await askHidden(t("runtime.wizard.ask_token"));
179
180
  if (!token) {
180
- process.stdout.write("Токен обязателен. Попробуйте еще раз.\n");
181
+ process.stdout.write(t("runtime.wizard.token_required"));
181
182
  continue;
182
183
  }
183
184
  if (!token.includes(":")) {
184
- process.stdout.write("Похоже на невалидный токен (ожидается формат <id>:<secret>). Попробуйте еще раз.\n");
185
+ process.stdout.write(t("runtime.wizard.token_invalid"));
185
186
  continue;
186
187
  }
187
188
  return token;
@@ -189,30 +190,30 @@ async function askToken() {
189
190
  }
190
191
  async function askAllowedUserId() {
191
192
  for (;;) {
192
- const allowedUserId = await askVisible("Введите ваш Telegram User ID (можно узнать у @userinfobot).\n> ");
193
+ const allowedUserId = await askVisible(t("runtime.wizard.ask_user_id"));
193
194
  if (!isPositiveInteger(allowedUserId)) {
194
- process.stdout.write("Введите положительное целое число (> 0).\n");
195
+ process.stdout.write(t("runtime.wizard.user_id_invalid"));
195
196
  continue;
196
197
  }
197
198
  return allowedUserId;
198
199
  }
199
200
  }
200
201
  async function askApiUrl() {
201
- const prompt = `Введите URL OpenCode API (опционально).\nНажмите Enter для значения по умолчанию: ${DEFAULT_API_URL}\n> `;
202
+ const prompt = t("runtime.wizard.ask_api_url", { defaultUrl: DEFAULT_API_URL });
202
203
  for (;;) {
203
204
  const apiUrl = await askVisible(prompt);
204
205
  if (!apiUrl) {
205
206
  return undefined;
206
207
  }
207
208
  if (!isValidHttpUrl(apiUrl)) {
208
- process.stdout.write("Введите корректный URL (http/https) или нажмите Enter для значения по умолчанию.\n");
209
+ process.stdout.write(t("runtime.wizard.api_url_invalid"));
209
210
  continue;
210
211
  }
211
212
  return apiUrl;
212
213
  }
213
214
  }
214
215
  async function collectWizardValues() {
215
- process.stdout.write("Запуск first-run wizard для настройки OpenCode Telegram Bot.\n");
216
+ process.stdout.write(t("runtime.wizard.start"));
216
217
  process.stdout.write("\n");
217
218
  const token = await askToken();
218
219
  const allowedUserId = await askAllowedUserId();
@@ -226,7 +227,7 @@ async function collectWizardValues() {
226
227
  }
227
228
  function ensureInteractiveTty() {
228
229
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
229
- throw new Error("Interactive wizard requires a TTY terminal. Run `opencode-telegram config` in an interactive shell.");
230
+ throw new Error(t("runtime.wizard.tty_required"));
230
231
  }
231
232
  }
232
233
  async function validateExistingEnv(envFilePath) {
@@ -257,7 +258,10 @@ async function runWizardAndPersist(runtimePaths) {
257
258
  const envContent = buildEnvFileContent(existingContent ?? "", envValues);
258
259
  await writeFileAtomically(runtimePaths.envFilePath, envContent);
259
260
  await ensureSettingsFile(runtimePaths.settingsFilePath);
260
- process.stdout.write(`Конфигурация сохранена:\n- ${runtimePaths.envFilePath}\n- ${runtimePaths.settingsFilePath}\n`);
261
+ process.stdout.write(t("runtime.wizard.saved", {
262
+ envPath: runtimePaths.envFilePath,
263
+ settingsPath: runtimePaths.settingsFilePath,
264
+ }));
261
265
  }
262
266
  export async function ensureRuntimeConfigForStart() {
263
267
  const runtimePaths = getRuntimePaths();
@@ -269,7 +273,7 @@ export async function ensureRuntimeConfigForStart() {
269
273
  await ensureSettingsFile(runtimePaths.settingsFilePath);
270
274
  return;
271
275
  }
272
- process.stdout.write("Приложение еще не сконфигурировано. Запускаю wizard...\n");
276
+ process.stdout.write(t("runtime.wizard.not_configured_starting"));
273
277
  await runWizardAndPersist(runtimePaths);
274
278
  }
275
279
  export async function runConfigWizardCommand() {
@@ -244,8 +244,8 @@ class SummaryAggregator {
244
244
  logger.debug(`[Aggregator] Tool event: callID=${part.callID}, tool=${part.tool}, status=${"status" in state ? state.status : "unknown"}`);
245
245
  if (part.tool === "question") {
246
246
  logger.debug(`[Aggregator] Question tool part update:`, JSON.stringify(part, null, 2));
247
- // Если question tool завершился с ошибкой, очищаем активный опрос
248
- // чтобы агент мог пересоздать его с исправленными данными
247
+ // If the question tool fails, clear the active poll
248
+ // so the agent can recreate it with corrected data
249
249
  if ("status" in state && state.status === "error") {
250
250
  logger.info(`[Aggregator] Question tool failed with error, clearing active poll. callID=${part.callID}`);
251
251
  if (this.onQuestionErrorCallback) {
@@ -1,6 +1,7 @@
1
1
  import * as path from "path";
2
2
  import { config } from "../config.js";
3
3
  import { logger } from "../utils/logger.js";
4
+ import { t } from "../i18n/index.js";
4
5
  const TELEGRAM_MESSAGE_LIMIT = 4096;
5
6
  function splitText(text, maxLength) {
6
7
  const parts = [];
@@ -44,7 +45,7 @@ function getToolDetails(tool, input) {
44
45
  if (!input) {
45
46
  return "";
46
47
  }
47
- // Сначала проверяем специфичные для известных тулов поля
48
+ // First, check fields specific to known tools
48
49
  switch (tool) {
49
50
  case "read":
50
51
  case "edit":
@@ -63,15 +64,15 @@ function getToolDetails(tool, input) {
63
64
  return input.pattern;
64
65
  break;
65
66
  }
66
- // Универсальный поиск для MCP и прочих инструментов
67
- // Ищем типичные поля: query, url, name, prompt
67
+ // Generic search for MCP and other tools
68
+ // Look for common fields: query, url, name, prompt
68
69
  const commonFields = ["query", "url", "name", "prompt", "text"];
69
70
  for (const field of commonFields) {
70
71
  if (typeof input[field] === "string") {
71
72
  return input[field];
72
73
  }
73
74
  }
74
- // Если ничего не нашли, но есть строковые поля, берем первое попавшееся (кроме description)
75
+ // If nothing matched but string fields exist, take the first one (except description)
75
76
  for (const [key, value] of Object.entries(input)) {
76
77
  if (key !== "description" && typeof value === "string" && value.length > 0) {
77
78
  return value;
@@ -128,7 +129,7 @@ function formatTodos(todos) {
128
129
  }
129
130
  let result = formattedTodos.join("\n");
130
131
  if (todos.length > MAX_TODOS) {
131
- result += `\n*(ещё ${todos.length - MAX_TODOS} задач)*`;
132
+ result += `\n${t("tool.todo.overflow", { count: todos.length - MAX_TODOS })}`;
132
133
  }
133
134
  return result;
134
135
  }
@@ -217,7 +218,9 @@ export function prepareCodeFile(content, filePath, operation) {
217
218
  logger.debug(`[Formatter] File too large: ${filePath} (${sizeKb.toFixed(2)} KB > ${config.files.maxFileSizeKb} KB)`);
218
219
  return null;
219
220
  }
220
- const header = `${operation === "write" ? "Write" : "Edit"} File/Path: ${filePath}\n${"=".repeat(60)}\n\n`;
221
+ const header = operation === "write"
222
+ ? t("tool.file_header.write", { path: filePath })
223
+ : t("tool.file_header.edit", { path: filePath });
221
224
  const fullContent = header + processedContent;
222
225
  const buffer = Buffer.from(fullContent, "utf8");
223
226
  const basename = path.basename(filePath);
@@ -1,25 +1,57 @@
1
1
  import { config } from "../config.js";
2
+ /**
3
+ * Mapping of log levels to numeric values for comparison
4
+ * Used to determine if a message should be logged based on configured level
5
+ */
2
6
  const LOG_LEVELS = {
3
7
  debug: 0,
4
8
  info: 1,
5
9
  warn: 2,
6
10
  error: 3,
7
11
  };
12
+ /**
13
+ * Normalizes a string value to a valid LogLevel
14
+ * Falls back to 'info' if the value is invalid
15
+ *
16
+ * @param value - The log level string to normalize
17
+ * @returns A valid LogLevel
18
+ */
8
19
  function normalizeLogLevel(value) {
9
20
  if (value in LOG_LEVELS) {
10
21
  return value;
11
22
  }
12
23
  return "info";
13
24
  }
25
+ /**
26
+ * Formats the log message prefix with timestamp and level
27
+ *
28
+ * @param level - The log level for the message
29
+ * @returns Formatted prefix string
30
+ */
14
31
  function formatPrefix(level) {
15
32
  return `[${new Date().toISOString()}] [${level.toUpperCase()}]`;
16
33
  }
34
+ /**
35
+ * Formats individual arguments for logging
36
+ * Special handling for Error objects to extract stack trace
37
+ *
38
+ * @param arg - The argument to format
39
+ * @returns Formatted argument
40
+ */
17
41
  function formatArg(arg) {
18
42
  if (arg instanceof Error) {
19
43
  return arg.stack ?? `${arg.name}: ${arg.message}`;
20
44
  }
21
45
  return arg;
22
46
  }
47
+ /**
48
+ * Prepends formatted prefix to log arguments
49
+ * Handles different argument formats (string vs non-string first argument)
50
+ *
51
+ * @param level - The log level for prefix formatting
52
+ * @param args - The arguments to log
53
+ * @returns Array with prefix prepended
54
+ */
23
55
  function withPrefix(level, args) {
24
56
  const formattedArgs = args.map((arg) => formatArg(arg));
25
57
  const prefix = formatPrefix(level);
@@ -31,26 +63,62 @@ function withPrefix(level, args) {
31
63
  }
32
64
  return [prefix, ...formattedArgs];
33
65
  }
66
+ /**
67
+ * Determines if a message should be logged based on configured log level
68
+ * Messages with level >= configured level will be logged
69
+ *
70
+ * @param level - The level of the message to check
71
+ * @returns True if the message should be logged
72
+ */
34
73
  function shouldLog(level) {
35
74
  const configLevel = normalizeLogLevel(config.server.logLevel);
36
75
  return LOG_LEVELS[level] >= LOG_LEVELS[configLevel];
37
76
  }
77
+ /**
78
+ * Logger interface with methods for different log levels
79
+ * Each method checks if the message should be logged based on configured level
80
+ * and formats the output with timestamp and level prefix
81
+ */
38
82
  export const logger = {
83
+ /**
84
+ * Logs debug-level messages (most verbose)
85
+ * Used for detailed diagnostics and internal operations
86
+ *
87
+ * @param args - Arguments to log
88
+ */
39
89
  debug: (...args) => {
40
90
  if (shouldLog("debug")) {
41
91
  console.log(...withPrefix("debug", args));
42
92
  }
43
93
  },
94
+ /**
95
+ * Logs info-level messages
96
+ * Used for important events and general information
97
+ *
98
+ * @param args - Arguments to log
99
+ */
44
100
  info: (...args) => {
45
101
  if (shouldLog("info")) {
46
102
  console.log(...withPrefix("info", args));
47
103
  }
48
104
  },
105
+ /**
106
+ * Logs warning-level messages
107
+ * Used for recoverable errors and potential issues
108
+ *
109
+ * @param args - Arguments to log
110
+ */
49
111
  warn: (...args) => {
50
112
  if (shouldLog("warn")) {
51
113
  console.warn(...withPrefix("warn", args));
52
114
  }
53
115
  },
116
+ /**
117
+ * Logs error-level messages
118
+ * Used for critical failures and exceptions
119
+ *
120
+ * @param args - Arguments to log
121
+ */
54
122
  error: (...args) => {
55
123
  if (shouldLog("error")) {
56
124
  console.error(...withPrefix("error", args));
package/package.json CHANGED
@@ -1,14 +1,15 @@
1
1
  {
2
2
  "name": "@grinev/opencode-telegram-bot",
3
- "version": "0.1.0-rc.7",
4
- "description": "",
3
+ "version": "0.1.0",
4
+ "description": "Telegram bot client for OpenCode to run and monitor coding tasks from chat.",
5
5
  "type": "module",
6
- "main": "dist/index.js",
6
+ "main": "./dist/index.js",
7
7
  "bin": {
8
8
  "opencode-telegram": "dist/cli.js"
9
9
  },
10
10
  "exports": {
11
- ".": "./dist/index.js"
11
+ ".": "./dist/index.js",
12
+ "./cli": "./dist/cli.js"
12
13
  },
13
14
  "files": [
14
15
  "dist",
@@ -23,7 +24,6 @@
23
24
  "build": "tsc",
24
25
  "start": "node dist/index.js",
25
26
  "dev": "npm run build && npm start",
26
- "list-providers": "tsx scripts/list-providers.ts",
27
27
  "release:prepare": "node scripts/release-prepare.mjs",
28
28
  "release:rc": "node scripts/release-prepare.mjs rc",
29
29
  "lint": "eslint src --ext .ts --max-warnings=0",
@@ -33,21 +33,27 @@
33
33
  },
34
34
  "repository": {
35
35
  "type": "git",
36
- "url": "git+https://github.com/grinev/my-telegram-opencode-bot.git"
36
+ "url": "git+https://github.com/grinev/opencode-telegram-bot.git"
37
37
  },
38
- "keywords": [],
39
- "author": "",
38
+ "keywords": [
39
+ "opencode",
40
+ "telegram",
41
+ "telegram-bot",
42
+ "grammy",
43
+ "developer-tools",
44
+ "cli"
45
+ ],
46
+ "author": "grinev",
40
47
  "license": "MIT",
41
48
  "bugs": {
42
- "url": "https://github.com/grinev/my-telegram-opencode-bot/issues"
49
+ "url": "https://github.com/grinev/opencode-telegram-bot/issues"
43
50
  },
44
- "homepage": "https://github.com/grinev/my-telegram-opencode-bot#readme",
51
+ "homepage": "https://github.com/grinev/opencode-telegram-bot#readme",
45
52
  "dependencies": {
46
53
  "@grammyjs/menu": "^1.3.1",
47
54
  "@opencode-ai/sdk": "^1.1.21",
48
55
  "dotenv": "^17.2.3",
49
- "grammy": "^1.39.2",
50
- "pino": "^10.2.0"
56
+ "grammy": "^1.39.2"
51
57
  },
52
58
  "devDependencies": {
53
59
  "@types/node": "^25.0.8",
@@ -56,7 +62,6 @@
56
62
  "@vitest/coverage-v8": "^3.2.4",
57
63
  "eslint": "^8.57.1",
58
64
  "eslint-config-prettier": "^10.1.8",
59
- "pino-pretty": "^13.1.3",
60
65
  "prettier": "^3.8.0",
61
66
  "tsx": "^4.21.0",
62
67
  "typescript": "^5.9.3",