@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.
Files changed (50) hide show
  1. package/.env.example +16 -0
  2. package/README.md +22 -1
  3. package/dist/app/start-bot-app.js +82 -7
  4. package/dist/bot/assistant-run-state.js +60 -0
  5. package/dist/bot/commands/abort.js +2 -0
  6. package/dist/bot/commands/commands.js +10 -0
  7. package/dist/bot/commands/definitions.js +1 -0
  8. package/dist/bot/commands/open.js +314 -0
  9. package/dist/bot/commands/projects.js +5 -38
  10. package/dist/bot/commands/start.js +2 -0
  11. package/dist/bot/handlers/inline-menu.js +9 -1
  12. package/dist/bot/handlers/prompt.js +11 -0
  13. package/dist/bot/index.js +187 -100
  14. package/dist/bot/streaming/response-streamer.js +26 -17
  15. package/dist/bot/utils/assistant-rendering.js +117 -0
  16. package/dist/bot/utils/assistant-run-footer.js +9 -0
  17. package/dist/bot/utils/browser-roots.js +140 -0
  18. package/dist/bot/utils/file-tree.js +92 -0
  19. package/dist/bot/utils/finalize-assistant-response.js +18 -24
  20. package/dist/bot/utils/send-with-markdown-fallback.js +3 -3
  21. package/dist/bot/utils/switch-project.js +48 -0
  22. package/dist/bot/utils/telegram-text.js +95 -1
  23. package/dist/cli/args.js +36 -7
  24. package/dist/cli.js +133 -15
  25. package/dist/config.js +4 -0
  26. package/dist/i18n/de.js +15 -10
  27. package/dist/i18n/en.js +15 -10
  28. package/dist/i18n/es.js +15 -10
  29. package/dist/i18n/fr.js +15 -10
  30. package/dist/i18n/ru.js +15 -10
  31. package/dist/i18n/zh.js +15 -10
  32. package/dist/index.js +2 -0
  33. package/dist/project/manager.js +2 -1
  34. package/dist/scheduled-task/runtime.js +8 -0
  35. package/dist/service/manager.js +244 -0
  36. package/dist/service/runtime.js +19 -0
  37. package/dist/service/types.js +1 -0
  38. package/dist/summary/aggregator.js +17 -1
  39. package/dist/summary/formatter.js +2 -88
  40. package/dist/telegram/render/block-fallback.js +28 -0
  41. package/dist/telegram/render/block-parser.js +295 -0
  42. package/dist/telegram/render/block-renderer.js +457 -0
  43. package/dist/telegram/render/chunker.js +281 -0
  44. package/dist/telegram/render/inline-renderer.js +128 -0
  45. package/dist/telegram/render/markdown-normalizer.js +94 -0
  46. package/dist/telegram/render/pipeline.js +9 -0
  47. package/dist/telegram/render/types.js +1 -0
  48. package/dist/telegram/render/validator.js +160 -0
  49. package/dist/utils/logger.js +200 -73
  50. package/package.json +6 -2
@@ -1,57 +1,65 @@
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
- */
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
- * 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
- */
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 "info";
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
- * 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
- */
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
- * 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
- */
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 configLevel = normalizeLogLevel(config.server.logLevel);
75
- return LOG_LEVELS[level] >= LOG_LEVELS[configLevel];
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
- */
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.15.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",