@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.
- package/.env.example +3 -1
- package/README.md +183 -50
- package/dist/app/start-bot-app.js +15 -1
- package/dist/bot/commands/agent.js +2 -1
- package/dist/bot/commands/definitions.js +16 -15
- package/dist/bot/commands/help.js +2 -5
- package/dist/bot/commands/model.js +2 -1
- package/dist/bot/commands/models.js +7 -6
- package/dist/bot/commands/new.js +4 -3
- package/dist/bot/commands/opencode-start.js +18 -20
- package/dist/bot/commands/opencode-stop.js +7 -9
- package/dist/bot/commands/projects.js +9 -6
- package/dist/bot/commands/sessions.js +15 -13
- package/dist/bot/commands/start.js +2 -9
- package/dist/bot/commands/status.js +21 -17
- package/dist/bot/commands/stop.js +10 -9
- package/dist/bot/handlers/agent.js +6 -5
- package/dist/bot/handlers/context.js +14 -16
- package/dist/bot/handlers/model.js +7 -6
- package/dist/bot/handlers/permission.js +32 -26
- package/dist/bot/handlers/question.js +39 -32
- package/dist/bot/handlers/variant.js +10 -9
- package/dist/bot/index.js +27 -26
- package/dist/bot/utils/keyboard.js +6 -3
- package/dist/cli/args.js +7 -6
- package/dist/cli.js +7 -17
- package/dist/config.js +15 -0
- package/dist/i18n/en.js +205 -0
- package/dist/i18n/index.js +50 -0
- package/dist/i18n/ru.js +205 -0
- package/dist/keyboard/manager.js +2 -1
- package/dist/opencode/events.js +4 -4
- package/dist/pinned/manager.js +16 -11
- package/dist/question/manager.js +1 -1
- package/dist/runtime/bootstrap.js +15 -11
- package/dist/summary/aggregator.js +2 -2
- package/dist/summary/formatter.js +9 -6
- package/dist/utils/logger.js +68 -0
- 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(
|
|
179
|
+
const token = await askHidden(t("runtime.wizard.ask_token"));
|
|
179
180
|
if (!token) {
|
|
180
|
-
process.stdout.write("
|
|
181
|
+
process.stdout.write(t("runtime.wizard.token_required"));
|
|
181
182
|
continue;
|
|
182
183
|
}
|
|
183
184
|
if (!token.includes(":")) {
|
|
184
|
-
process.stdout.write("
|
|
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(
|
|
193
|
+
const allowedUserId = await askVisible(t("runtime.wizard.ask_user_id"));
|
|
193
194
|
if (!isPositiveInteger(allowedUserId)) {
|
|
194
|
-
process.stdout.write(
|
|
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 =
|
|
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(
|
|
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("
|
|
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("
|
|
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(
|
|
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("
|
|
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
|
-
//
|
|
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
|
-
//
|
|
67
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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 =
|
|
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);
|
package/dist/utils/logger.js
CHANGED
|
@@ -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
|
|
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/
|
|
36
|
+
"url": "git+https://github.com/grinev/opencode-telegram-bot.git"
|
|
37
37
|
},
|
|
38
|
-
"keywords": [
|
|
39
|
-
|
|
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/
|
|
49
|
+
"url": "https://github.com/grinev/opencode-telegram-bot/issues"
|
|
43
50
|
},
|
|
44
|
-
"homepage": "https://github.com/grinev/
|
|
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",
|