@grinev/opencode-telegram-bot 0.15.0 → 0.16.1
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 +16 -0
- package/README.md +22 -1
- package/dist/app/start-bot-app.js +82 -7
- package/dist/bot/assistant-run-state.js +60 -0
- package/dist/bot/commands/abort.js +2 -0
- package/dist/bot/commands/commands.js +10 -0
- package/dist/bot/commands/definitions.js +1 -0
- package/dist/bot/commands/open.js +314 -0
- package/dist/bot/commands/projects.js +5 -38
- package/dist/bot/commands/start.js +2 -0
- package/dist/bot/handlers/inline-menu.js +9 -1
- package/dist/bot/handlers/prompt.js +11 -0
- package/dist/bot/index.js +187 -100
- package/dist/bot/streaming/response-streamer.js +26 -17
- package/dist/bot/utils/assistant-rendering.js +117 -0
- package/dist/bot/utils/assistant-run-footer.js +9 -0
- package/dist/bot/utils/browser-roots.js +140 -0
- package/dist/bot/utils/file-tree.js +92 -0
- package/dist/bot/utils/finalize-assistant-response.js +18 -24
- package/dist/bot/utils/send-with-markdown-fallback.js +3 -3
- package/dist/bot/utils/switch-project.js +48 -0
- package/dist/bot/utils/telegram-text.js +95 -1
- package/dist/cli/args.js +36 -7
- package/dist/cli.js +133 -15
- package/dist/config.js +4 -0
- package/dist/i18n/de.js +15 -10
- package/dist/i18n/en.js +15 -10
- package/dist/i18n/es.js +15 -10
- package/dist/i18n/fr.js +15 -10
- package/dist/i18n/ru.js +15 -10
- package/dist/i18n/zh.js +15 -10
- package/dist/index.js +2 -0
- package/dist/project/manager.js +2 -1
- package/dist/scheduled-task/runtime.js +8 -0
- package/dist/service/manager.js +244 -0
- package/dist/service/runtime.js +19 -0
- package/dist/service/types.js +1 -0
- package/dist/summary/aggregator.js +17 -1
- package/dist/summary/formatter.js +2 -88
- package/dist/telegram/render/block-fallback.js +28 -0
- package/dist/telegram/render/block-parser.js +295 -0
- package/dist/telegram/render/block-renderer.js +457 -0
- package/dist/telegram/render/chunker.js +281 -0
- package/dist/telegram/render/inline-renderer.js +128 -0
- package/dist/telegram/render/markdown-normalizer.js +94 -0
- package/dist/telegram/render/pipeline.js +9 -0
- package/dist/telegram/render/types.js +1 -0
- package/dist/telegram/render/validator.js +160 -0
- package/dist/utils/logger.js +200 -73
- package/package.json +6 -2
package/dist/utils/logger.js
CHANGED
|
@@ -1,57 +1,65 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import fsPromises from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { inspect } from "node:util";
|
|
5
|
+
import { getRuntimeMode } from "../runtime/mode.js";
|
|
6
|
+
import { getRuntimePaths } from "../runtime/paths.js";
|
|
7
|
+
const DEFAULT_LOG_LEVEL = "info";
|
|
8
|
+
const DEFAULT_LOG_RETENTION = 10;
|
|
9
|
+
const LOGGER_ERROR_PREFIX = "[LOGGER]";
|
|
6
10
|
const LOG_LEVELS = {
|
|
7
11
|
debug: 0,
|
|
8
12
|
info: 1,
|
|
9
13
|
warn: 2,
|
|
10
14
|
error: 3,
|
|
11
15
|
};
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
* @param value - The log level string to normalize
|
|
17
|
-
* @returns A valid LogLevel
|
|
18
|
-
*/
|
|
16
|
+
let logStream = null;
|
|
17
|
+
let logFilePath = null;
|
|
18
|
+
let initializePromise = null;
|
|
19
|
+
let streamErrorReported = false;
|
|
19
20
|
function normalizeLogLevel(value) {
|
|
20
21
|
if (value in LOG_LEVELS) {
|
|
21
22
|
return value;
|
|
22
23
|
}
|
|
23
|
-
return
|
|
24
|
+
return DEFAULT_LOG_LEVEL;
|
|
25
|
+
}
|
|
26
|
+
function parsePositiveInteger(value, defaultValue) {
|
|
27
|
+
if (!value) {
|
|
28
|
+
return defaultValue;
|
|
29
|
+
}
|
|
30
|
+
const parsed = Number.parseInt(value, 10);
|
|
31
|
+
if (Number.isNaN(parsed) || parsed <= 0) {
|
|
32
|
+
return defaultValue;
|
|
33
|
+
}
|
|
34
|
+
return parsed;
|
|
35
|
+
}
|
|
36
|
+
function getConfiguredLogLevel() {
|
|
37
|
+
return normalizeLogLevel(process.env.LOG_LEVEL ?? DEFAULT_LOG_LEVEL);
|
|
38
|
+
}
|
|
39
|
+
function getConfiguredLogRetention() {
|
|
40
|
+
return parsePositiveInteger(process.env.LOG_RETENTION, DEFAULT_LOG_RETENTION);
|
|
24
41
|
}
|
|
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
|
-
*/
|
|
31
42
|
function formatPrefix(level) {
|
|
32
43
|
return `[${new Date().toISOString()}] [${level.toUpperCase()}]`;
|
|
33
44
|
}
|
|
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
|
-
*/
|
|
41
45
|
function formatArg(arg) {
|
|
42
46
|
if (arg instanceof Error) {
|
|
43
47
|
return arg.stack ?? `${arg.name}: ${arg.message}`;
|
|
44
48
|
}
|
|
45
49
|
return arg;
|
|
46
50
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
function formatArgForFile(arg) {
|
|
52
|
+
const formatted = formatArg(arg);
|
|
53
|
+
if (typeof formatted === "string") {
|
|
54
|
+
return formatted;
|
|
55
|
+
}
|
|
56
|
+
return inspect(formatted, {
|
|
57
|
+
colors: false,
|
|
58
|
+
compact: true,
|
|
59
|
+
depth: 8,
|
|
60
|
+
breakLength: Infinity,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
55
63
|
function withPrefix(level, args) {
|
|
56
64
|
const formattedArgs = args.map((arg) => formatArg(arg));
|
|
57
65
|
const prefix = formatPrefix(level);
|
|
@@ -63,65 +71,184 @@ function withPrefix(level, args) {
|
|
|
63
71
|
}
|
|
64
72
|
return [prefix, ...formattedArgs];
|
|
65
73
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
74
|
+
function formatLine(level, args) {
|
|
75
|
+
const prefix = formatPrefix(level);
|
|
76
|
+
const formattedArgs = args.map((arg) => formatArgForFile(arg));
|
|
77
|
+
if (formattedArgs.length === 0) {
|
|
78
|
+
return prefix;
|
|
79
|
+
}
|
|
80
|
+
return `${prefix} ${formattedArgs.join(" ")}`;
|
|
81
|
+
}
|
|
73
82
|
function shouldLog(level) {
|
|
74
|
-
const
|
|
75
|
-
return LOG_LEVELS[level] >= LOG_LEVELS[
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
83
|
+
const configuredLevel = getConfiguredLogLevel();
|
|
84
|
+
return LOG_LEVELS[level] >= LOG_LEVELS[configuredLevel];
|
|
85
|
+
}
|
|
86
|
+
function sanitizeTimestampForFile(timestamp) {
|
|
87
|
+
return timestamp.replace(/:/g, "-").replace("T", "_");
|
|
88
|
+
}
|
|
89
|
+
function getSourcesLogFileName() {
|
|
90
|
+
const timestamp = sanitizeTimestampForFile(new Date().toISOString().slice(0, 19));
|
|
91
|
+
return `bot-${timestamp}_${process.pid}.log`;
|
|
92
|
+
}
|
|
93
|
+
function getInstalledLogFileName() {
|
|
94
|
+
return `bot-${new Date().toISOString().slice(0, 10)}.log`;
|
|
95
|
+
}
|
|
96
|
+
function getLogFileName(mode) {
|
|
97
|
+
return mode === "installed" ? getInstalledLogFileName() : getSourcesLogFileName();
|
|
98
|
+
}
|
|
99
|
+
function getLogFilePattern(mode) {
|
|
100
|
+
if (mode === "installed") {
|
|
101
|
+
return /^bot-\d{4}-\d{2}-\d{2}\.log$/;
|
|
102
|
+
}
|
|
103
|
+
return /^bot-\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}_\d+\.log$/;
|
|
104
|
+
}
|
|
105
|
+
function reportLoggerInternalError(message, error) {
|
|
106
|
+
const details = error instanceof Error
|
|
107
|
+
? (error.stack ?? `${error.name}: ${error.message}`)
|
|
108
|
+
: String(error ?? "");
|
|
109
|
+
const suffix = details && details !== "undefined" ? ` ${details}` : "";
|
|
110
|
+
process.stderr.write(`${formatPrefix("error")} ${LOGGER_ERROR_PREFIX} ${message}${suffix}\n`);
|
|
111
|
+
}
|
|
112
|
+
function handleLogStreamError(error) {
|
|
113
|
+
if (!streamErrorReported) {
|
|
114
|
+
streamErrorReported = true;
|
|
115
|
+
reportLoggerInternalError("Failed to write to log file.", error);
|
|
116
|
+
}
|
|
117
|
+
if (logStream) {
|
|
118
|
+
logStream.destroy();
|
|
119
|
+
logStream = null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function closeLogStream() {
|
|
123
|
+
if (!logStream) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
logStream.removeAllListeners("error");
|
|
127
|
+
logStream.end();
|
|
128
|
+
logStream = null;
|
|
129
|
+
}
|
|
130
|
+
function ensureLogStream(filePath) {
|
|
131
|
+
if (logStream && logFilePath === filePath) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
closeLogStream();
|
|
135
|
+
streamErrorReported = false;
|
|
136
|
+
const stream = fs.createWriteStream(filePath, { flags: "a" });
|
|
137
|
+
stream.on("error", handleLogStreamError);
|
|
138
|
+
logStream = stream;
|
|
139
|
+
logFilePath = filePath;
|
|
140
|
+
}
|
|
141
|
+
async function cleanupOldLogs(logsDirPath, mode) {
|
|
142
|
+
const retention = getConfiguredLogRetention();
|
|
143
|
+
const filePattern = getLogFilePattern(mode);
|
|
144
|
+
let fileNames;
|
|
145
|
+
try {
|
|
146
|
+
fileNames = await fsPromises.readdir(logsDirPath);
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
reportLoggerInternalError(`Failed to read log directory ${logsDirPath}.`, error);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const matchingFiles = fileNames.filter((fileName) => filePattern.test(fileName)).sort();
|
|
153
|
+
const filesToDelete = matchingFiles.slice(0, Math.max(0, matchingFiles.length - retention));
|
|
154
|
+
await Promise.all(filesToDelete.map(async (fileName) => {
|
|
155
|
+
try {
|
|
156
|
+
await fsPromises.unlink(path.join(logsDirPath, fileName));
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
reportLoggerInternalError(`Failed to delete old log file ${fileName}.`, error);
|
|
160
|
+
}
|
|
161
|
+
}));
|
|
162
|
+
}
|
|
163
|
+
function writeToFile(line) {
|
|
164
|
+
if (!logStream) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
logStream.write(`${line}\n`);
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
handleLogStreamError(error);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
async function initializeLoggerInternal() {
|
|
175
|
+
if (logStream && logFilePath) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const runtimePaths = getRuntimePaths();
|
|
179
|
+
const mode = getRuntimeMode();
|
|
180
|
+
try {
|
|
181
|
+
await fsPromises.mkdir(runtimePaths.logsDirPath, { recursive: true });
|
|
182
|
+
const nextLogFilePath = path.join(runtimePaths.logsDirPath, getLogFileName(mode));
|
|
183
|
+
await fsPromises.appendFile(nextLogFilePath, "");
|
|
184
|
+
ensureLogStream(nextLogFilePath);
|
|
185
|
+
await cleanupOldLogs(runtimePaths.logsDirPath, mode);
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
reportLoggerInternalError(`Failed to initialize file logging in ${runtimePaths.logsDirPath}.`, error);
|
|
189
|
+
closeLogStream();
|
|
190
|
+
logFilePath = null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
export async function initializeLogger() {
|
|
194
|
+
if (initializePromise) {
|
|
195
|
+
await initializePromise;
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
initializePromise = initializeLoggerInternal();
|
|
199
|
+
try {
|
|
200
|
+
await initializePromise;
|
|
201
|
+
}
|
|
202
|
+
finally {
|
|
203
|
+
initializePromise = null;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
export function getLogFilePath() {
|
|
207
|
+
return logFilePath;
|
|
208
|
+
}
|
|
209
|
+
export async function __flushLoggerForTests() {
|
|
210
|
+
if (!logStream) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
await new Promise((resolve, reject) => {
|
|
214
|
+
logStream?.write("", (error) => {
|
|
215
|
+
if (error) {
|
|
216
|
+
reject(error);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
resolve();
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
export function __resetLoggerForTests() {
|
|
224
|
+
initializePromise = null;
|
|
225
|
+
logFilePath = null;
|
|
226
|
+
streamErrorReported = false;
|
|
227
|
+
closeLogStream();
|
|
228
|
+
}
|
|
82
229
|
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
|
-
*/
|
|
89
230
|
debug: (...args) => {
|
|
90
231
|
if (shouldLog("debug")) {
|
|
91
232
|
console.log(...withPrefix("debug", args));
|
|
233
|
+
writeToFile(formatLine("debug", args));
|
|
92
234
|
}
|
|
93
235
|
},
|
|
94
|
-
/**
|
|
95
|
-
* Logs info-level messages
|
|
96
|
-
* Used for important events and general information
|
|
97
|
-
*
|
|
98
|
-
* @param args - Arguments to log
|
|
99
|
-
*/
|
|
100
236
|
info: (...args) => {
|
|
101
237
|
if (shouldLog("info")) {
|
|
102
238
|
console.log(...withPrefix("info", args));
|
|
239
|
+
writeToFile(formatLine("info", args));
|
|
103
240
|
}
|
|
104
241
|
},
|
|
105
|
-
/**
|
|
106
|
-
* Logs warning-level messages
|
|
107
|
-
* Used for recoverable errors and potential issues
|
|
108
|
-
*
|
|
109
|
-
* @param args - Arguments to log
|
|
110
|
-
*/
|
|
111
242
|
warn: (...args) => {
|
|
112
243
|
if (shouldLog("warn")) {
|
|
113
244
|
console.warn(...withPrefix("warn", args));
|
|
245
|
+
writeToFile(formatLine("warn", args));
|
|
114
246
|
}
|
|
115
247
|
},
|
|
116
|
-
/**
|
|
117
|
-
* Logs error-level messages
|
|
118
|
-
* Used for critical failures and exceptions
|
|
119
|
-
*
|
|
120
|
-
* @param args - Arguments to log
|
|
121
|
-
*/
|
|
122
248
|
error: (...args) => {
|
|
123
249
|
if (shouldLog("error")) {
|
|
124
250
|
console.error(...withPrefix("error", args));
|
|
251
|
+
writeToFile(formatLine("error", args));
|
|
125
252
|
}
|
|
126
253
|
},
|
|
127
254
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@grinev/opencode-telegram-bot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.1",
|
|
4
4
|
"description": "Telegram bot client for OpenCode to run and monitor coding tasks from chat.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -57,8 +57,12 @@
|
|
|
57
57
|
"dotenv": "^17.2.3",
|
|
58
58
|
"grammy": "^1.39.2",
|
|
59
59
|
"https-proxy-agent": "^7.0.6",
|
|
60
|
+
"mdast-util-to-string": "^4.0.0",
|
|
61
|
+
"remark-gfm": "^4.0.1",
|
|
62
|
+
"remark-parse": "^11.0.0",
|
|
60
63
|
"socks-proxy-agent": "^8.0.5",
|
|
61
|
-
"telegram-markdown-v2": "^0.0.4"
|
|
64
|
+
"telegram-markdown-v2": "^0.0.4",
|
|
65
|
+
"unified": "^11.0.5"
|
|
62
66
|
},
|
|
63
67
|
"devDependencies": {
|
|
64
68
|
"@types/better-sqlite3": "^7.6.13",
|