opencode-telegram-group-topics-bot 0.11.2

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 (101) hide show
  1. package/.env.example +74 -0
  2. package/LICENSE +21 -0
  3. package/README.md +305 -0
  4. package/dist/agent/manager.js +60 -0
  5. package/dist/agent/types.js +26 -0
  6. package/dist/app/start-bot-app.js +47 -0
  7. package/dist/bot/commands/abort.js +116 -0
  8. package/dist/bot/commands/commands.js +389 -0
  9. package/dist/bot/commands/constants.js +20 -0
  10. package/dist/bot/commands/definitions.js +25 -0
  11. package/dist/bot/commands/help.js +27 -0
  12. package/dist/bot/commands/models.js +38 -0
  13. package/dist/bot/commands/new.js +247 -0
  14. package/dist/bot/commands/opencode-start.js +85 -0
  15. package/dist/bot/commands/opencode-stop.js +44 -0
  16. package/dist/bot/commands/projects.js +304 -0
  17. package/dist/bot/commands/rename.js +173 -0
  18. package/dist/bot/commands/sessions.js +491 -0
  19. package/dist/bot/commands/start.js +67 -0
  20. package/dist/bot/commands/status.js +138 -0
  21. package/dist/bot/constants.js +49 -0
  22. package/dist/bot/handlers/agent.js +127 -0
  23. package/dist/bot/handlers/context.js +125 -0
  24. package/dist/bot/handlers/document.js +65 -0
  25. package/dist/bot/handlers/inline-menu.js +124 -0
  26. package/dist/bot/handlers/model.js +152 -0
  27. package/dist/bot/handlers/permission.js +281 -0
  28. package/dist/bot/handlers/prompt.js +263 -0
  29. package/dist/bot/handlers/question.js +285 -0
  30. package/dist/bot/handlers/variant.js +147 -0
  31. package/dist/bot/handlers/voice.js +173 -0
  32. package/dist/bot/index.js +945 -0
  33. package/dist/bot/message-patterns.js +4 -0
  34. package/dist/bot/middleware/auth.js +30 -0
  35. package/dist/bot/middleware/interaction-guard.js +80 -0
  36. package/dist/bot/middleware/unknown-command.js +22 -0
  37. package/dist/bot/scope.js +222 -0
  38. package/dist/bot/telegram-constants.js +3 -0
  39. package/dist/bot/telegram-rate-limiter.js +263 -0
  40. package/dist/bot/utils/commands.js +21 -0
  41. package/dist/bot/utils/file-download.js +91 -0
  42. package/dist/bot/utils/keyboard.js +85 -0
  43. package/dist/bot/utils/send-with-markdown-fallback.js +57 -0
  44. package/dist/bot/utils/session-error-filter.js +34 -0
  45. package/dist/bot/utils/topic-link.js +29 -0
  46. package/dist/cli/args.js +98 -0
  47. package/dist/cli.js +80 -0
  48. package/dist/config.js +103 -0
  49. package/dist/i18n/de.js +330 -0
  50. package/dist/i18n/en.js +330 -0
  51. package/dist/i18n/es.js +330 -0
  52. package/dist/i18n/index.js +102 -0
  53. package/dist/i18n/ru.js +330 -0
  54. package/dist/i18n/zh.js +330 -0
  55. package/dist/index.js +28 -0
  56. package/dist/interaction/cleanup.js +24 -0
  57. package/dist/interaction/constants.js +25 -0
  58. package/dist/interaction/guard.js +100 -0
  59. package/dist/interaction/manager.js +113 -0
  60. package/dist/interaction/types.js +1 -0
  61. package/dist/keyboard/manager.js +115 -0
  62. package/dist/keyboard/types.js +1 -0
  63. package/dist/model/capabilities.js +62 -0
  64. package/dist/model/manager.js +257 -0
  65. package/dist/model/types.js +24 -0
  66. package/dist/opencode/client.js +13 -0
  67. package/dist/opencode/events.js +159 -0
  68. package/dist/opencode/prompt-submit-error.js +101 -0
  69. package/dist/permission/manager.js +92 -0
  70. package/dist/permission/types.js +1 -0
  71. package/dist/pinned/manager.js +405 -0
  72. package/dist/pinned/types.js +1 -0
  73. package/dist/process/manager.js +273 -0
  74. package/dist/process/types.js +1 -0
  75. package/dist/project/manager.js +88 -0
  76. package/dist/question/manager.js +186 -0
  77. package/dist/question/types.js +1 -0
  78. package/dist/rename/manager.js +64 -0
  79. package/dist/runtime/bootstrap.js +350 -0
  80. package/dist/runtime/mode.js +74 -0
  81. package/dist/runtime/paths.js +37 -0
  82. package/dist/runtime/process-error-handlers.js +24 -0
  83. package/dist/session/cache-manager.js +455 -0
  84. package/dist/session/manager.js +87 -0
  85. package/dist/settings/manager.js +283 -0
  86. package/dist/stt/client.js +64 -0
  87. package/dist/summary/aggregator.js +625 -0
  88. package/dist/summary/formatter.js +417 -0
  89. package/dist/summary/tool-message-batcher.js +277 -0
  90. package/dist/topic/colors.js +8 -0
  91. package/dist/topic/constants.js +10 -0
  92. package/dist/topic/manager.js +161 -0
  93. package/dist/topic/title-constants.js +2 -0
  94. package/dist/topic/title-format.js +10 -0
  95. package/dist/topic/title-sync.js +17 -0
  96. package/dist/utils/error-format.js +29 -0
  97. package/dist/utils/logger.js +175 -0
  98. package/dist/utils/safe-background-task.js +33 -0
  99. package/dist/variant/manager.js +103 -0
  100. package/dist/variant/types.js +1 -0
  101. package/package.json +76 -0
@@ -0,0 +1,91 @@
1
+ import { config } from "../../config.js";
2
+ import { logger } from "../../utils/logger.js";
3
+ const TELEGRAM_FILE_URL_BASE = "https://api.telegram.org/file/bot";
4
+ const MAX_FILE_SIZE_BYTES = 20 * 1024 * 1024; // 20MB Telegram limit
5
+ /**
6
+ * Download a photo from Telegram servers
7
+ * @param api Grammy API instance
8
+ * @param fileId Telegram file_id
9
+ * @returns Downloaded photo buffer and path
10
+ */
11
+ export async function downloadTelegramFile(api, fileId) {
12
+ logger.debug(`[FileDownload] Getting file info for fileId=${fileId}`);
13
+ const file = await api.getFile(fileId);
14
+ if (!file.file_path) {
15
+ throw new Error("File path not available from Telegram");
16
+ }
17
+ if (file.file_size && file.file_size > MAX_FILE_SIZE_BYTES) {
18
+ const sizeMb = (file.file_size / (1024 * 1024)).toFixed(2);
19
+ throw new Error(`File too large: ${sizeMb}MB (max 20MB)`);
20
+ }
21
+ const fileUrl = `${TELEGRAM_FILE_URL_BASE}${config.telegram.token}/${file.file_path}`;
22
+ logger.debug(`[FileDownload] Downloading from ${fileUrl.replace(config.telegram.token, "***")}`);
23
+ const fetchOptions = {};
24
+ // Use proxy if configured
25
+ if (config.telegram.proxyUrl) {
26
+ const { HttpsProxyAgent } = await import("https-proxy-agent");
27
+ fetchOptions.agent = new HttpsProxyAgent(config.telegram.proxyUrl);
28
+ }
29
+ const response = await fetch(fileUrl, fetchOptions);
30
+ if (!response.ok) {
31
+ throw new Error(`Failed to download file: ${response.status} ${response.statusText}`);
32
+ }
33
+ const arrayBuffer = await response.arrayBuffer();
34
+ const buffer = Buffer.from(arrayBuffer);
35
+ logger.debug(`[FileDownload] Downloaded ${buffer.length} bytes`);
36
+ return {
37
+ buffer,
38
+ filePath: file.file_path,
39
+ };
40
+ }
41
+ /**
42
+ * Convert buffer to base64 data URI
43
+ * @param buffer File buffer
44
+ * @param mimeType MIME type (e.g., "image/jpeg")
45
+ * @returns Data URI string
46
+ */
47
+ export function toDataUri(buffer, mimeType) {
48
+ const base64 = buffer.toString("base64");
49
+ return `data:${mimeType};base64,${base64}`;
50
+ }
51
+ /**
52
+ * Check if photo size is within limits
53
+ * @param fileSize Photo size in bytes
54
+ * @param maxSizeKb Maximum size in KB (from config)
55
+ * @returns true if within limit
56
+ */
57
+ export function isFileSizeAllowed(fileSize, maxSizeKb) {
58
+ if (!fileSize) {
59
+ return true; // Unknown size, allow (will be checked on download)
60
+ }
61
+ const maxBytes = maxSizeKb * 1024;
62
+ return fileSize <= maxBytes;
63
+ }
64
+ /**
65
+ * Get human-readable photo size
66
+ */
67
+ export function formatFileSize(bytes) {
68
+ if (bytes < 1024) {
69
+ return `${bytes}B`;
70
+ }
71
+ if (bytes < 1024 * 1024) {
72
+ return `${(bytes / 1024).toFixed(1)}KB`;
73
+ }
74
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
75
+ }
76
+ const APPLICATION_TEXT_MIME_TYPES = new Set([
77
+ "application/json",
78
+ "application/xml",
79
+ "application/javascript",
80
+ "application/x-yaml",
81
+ "application/sql",
82
+ ]);
83
+ export function isTextMimeType(mimeType) {
84
+ if (!mimeType) {
85
+ return false;
86
+ }
87
+ if (mimeType.startsWith("text/")) {
88
+ return true;
89
+ }
90
+ return APPLICATION_TEXT_MIME_TYPES.has(mimeType);
91
+ }
@@ -0,0 +1,85 @@
1
+ import { Keyboard } from "grammy";
2
+ import { getAgentDisplayName } from "../../agent/types.js";
3
+ import { formatModelForButton } from "../../model/types.js";
4
+ import { t } from "../../i18n/index.js";
5
+ /**
6
+ * Format token count for display (e.g., 150000 -> "150K", 1500000 -> "1.5M")
7
+ */
8
+ function formatTokenCount(count) {
9
+ if (count >= 1000000) {
10
+ return `${(count / 1000000).toFixed(1)}M`;
11
+ }
12
+ else if (count >= 1000) {
13
+ return `${Math.round(count / 1000)}K`;
14
+ }
15
+ return count.toString();
16
+ }
17
+ /**
18
+ * Format context information for button
19
+ */
20
+ function formatContextForButton(contextInfo) {
21
+ const used = formatTokenCount(contextInfo.tokensUsed);
22
+ const limit = formatTokenCount(contextInfo.tokensLimit);
23
+ const percent = Math.round((contextInfo.tokensUsed / contextInfo.tokensLimit) * 100);
24
+ return t("keyboard.context", { used, limit, percent });
25
+ }
26
+ /**
27
+ * Create Reply Keyboard with agent, model, variant, and context indicators
28
+ * @param currentAgent Current agent name (e.g., "build", "plan")
29
+ * @param currentModel Current model info
30
+ * @param contextInfo Optional context information (tokens used/limit)
31
+ * @param variantName Optional variant display name (e.g., "💭 Default")
32
+ * @returns Reply Keyboard with agent and context in row 1, model and variant in row 2
33
+ */
34
+ export function createMainKeyboard(currentAgent, currentModel, contextInfo, variantName, options) {
35
+ const keyboard = new Keyboard();
36
+ const agentText = getAgentDisplayName(currentAgent);
37
+ // Format model with compact provider/model text and icon
38
+ const modelText = formatModelForButton(currentModel.providerID, currentModel.modelID);
39
+ // Context text - show "0" if no data available
40
+ const contextText = options?.contextLabel ??
41
+ (contextInfo ? formatContextForButton(contextInfo) : t("keyboard.context_empty"));
42
+ // Variant text - default to "💭 Default" if not provided
43
+ const variantText = variantName || t("keyboard.variant_default");
44
+ // Row 1: context and agent buttons (for control-plane) or agent/context defaults
45
+ const contextFirst = options?.contextFirst ?? true;
46
+ if (contextFirst) {
47
+ keyboard.text(contextText).text(agentText).row();
48
+ }
49
+ else {
50
+ keyboard.text(agentText).text(contextText).row();
51
+ }
52
+ // Row 2: model and variant buttons
53
+ keyboard.text(modelText).text(variantText).row();
54
+ return keyboard.resized().persistent();
55
+ }
56
+ export function createDmKeyboard() {
57
+ return new Keyboard()
58
+ .text(t("keyboard.dm.status"))
59
+ .text(t("keyboard.dm.help"))
60
+ .row()
61
+ .text(t("keyboard.dm.opencode_start"))
62
+ .text(t("keyboard.dm.opencode_stop"))
63
+ .row()
64
+ .resized()
65
+ .persistent();
66
+ }
67
+ /**
68
+ * Create Reply Keyboard with agent mode indicator
69
+ * @param currentAgent Current agent name (e.g., "build", "plan")
70
+ * @returns Reply Keyboard with single button showing current mode
71
+ * @deprecated Use createMainKeyboard instead
72
+ */
73
+ export function createAgentKeyboard(currentAgent) {
74
+ const keyboard = new Keyboard();
75
+ const displayName = getAgentDisplayName(currentAgent);
76
+ // Single button with current agent mode
77
+ keyboard.text(displayName).row();
78
+ return keyboard.resized().persistent();
79
+ }
80
+ /**
81
+ * Remove Reply Keyboard (for cleanup)
82
+ */
83
+ export function removeKeyboard() {
84
+ return { remove_keyboard: true };
85
+ }
@@ -0,0 +1,57 @@
1
+ import { logger } from "../../utils/logger.js";
2
+ const MARKDOWN_PARSE_ERROR_MARKERS = [
3
+ "can't parse entities",
4
+ "can't parse entity",
5
+ "can't find end of the entity",
6
+ "entity beginning",
7
+ "bad request: can't parse",
8
+ ];
9
+ function getErrorText(error) {
10
+ const parts = [];
11
+ if (error instanceof Error) {
12
+ parts.push(error.message);
13
+ }
14
+ if (typeof error === "object" && error !== null) {
15
+ const description = Reflect.get(error, "description");
16
+ if (typeof description === "string") {
17
+ parts.push(description);
18
+ }
19
+ const message = Reflect.get(error, "message");
20
+ if (typeof message === "string") {
21
+ parts.push(message);
22
+ }
23
+ }
24
+ if (typeof error === "string") {
25
+ parts.push(error);
26
+ }
27
+ if (parts.length === 0) {
28
+ return "";
29
+ }
30
+ return parts.join("\n").toLowerCase();
31
+ }
32
+ export function isTelegramMarkdownParseError(error) {
33
+ const errorText = getErrorText(error);
34
+ if (!errorText) {
35
+ return false;
36
+ }
37
+ return MARKDOWN_PARSE_ERROR_MARKERS.some((marker) => errorText.includes(marker));
38
+ }
39
+ export async function sendMessageWithMarkdownFallback({ api, chatId, text, options, parseMode, }) {
40
+ if (!parseMode) {
41
+ return await api.sendMessage(chatId, text, options);
42
+ }
43
+ const markdownOptions = {
44
+ ...(options || {}),
45
+ parse_mode: parseMode,
46
+ };
47
+ try {
48
+ return await api.sendMessage(chatId, text, markdownOptions);
49
+ }
50
+ catch (error) {
51
+ if (!isTelegramMarkdownParseError(error)) {
52
+ throw error;
53
+ }
54
+ logger.warn("[Bot] Markdown parse failed, retrying assistant message in raw mode", error);
55
+ return await api.sendMessage(chatId, text, options);
56
+ }
57
+ }
@@ -0,0 +1,34 @@
1
+ const OPERATION_ABORTED_MARKERS = [
2
+ "the operation was aborted",
3
+ "operation was aborted",
4
+ "aborterror",
5
+ ];
6
+ export function isOperationAbortedSessionError(message) {
7
+ const normalized = message.trim().toLowerCase();
8
+ if (!normalized) {
9
+ return false;
10
+ }
11
+ return OPERATION_ABORTED_MARKERS.some((marker) => normalized.includes(marker));
12
+ }
13
+ export class SessionErrorThrottle {
14
+ windowMs;
15
+ lastBySession = new Map();
16
+ constructor(windowMs = 3000) {
17
+ this.windowMs = windowMs;
18
+ }
19
+ shouldSuppress(sessionId, message, now = Date.now()) {
20
+ const normalized = message.trim().toLowerCase();
21
+ if (!normalized) {
22
+ return false;
23
+ }
24
+ const previous = this.lastBySession.get(sessionId);
25
+ this.lastBySession.set(sessionId, {
26
+ message: normalized,
27
+ timestamp: now,
28
+ });
29
+ if (!previous) {
30
+ return false;
31
+ }
32
+ return previous.message === normalized && now - previous.timestamp < this.windowMs;
33
+ }
34
+ }
@@ -0,0 +1,29 @@
1
+ import { CHAT_TYPE, TELEGRAM_CHAT_ID_PREFIX, TELEGRAM_URL, TELEGRAM_CHAT_FIELD, } from "../constants.js";
2
+ function getInternalSupergroupId(chatId) {
3
+ const absoluteId = Math.abs(chatId).toString();
4
+ if (absoluteId.startsWith(TELEGRAM_CHAT_ID_PREFIX.PRIVATE_SUPERGROUP)) {
5
+ return absoluteId.slice(TELEGRAM_CHAT_ID_PREFIX.PRIVATE_SUPERGROUP.length);
6
+ }
7
+ return absoluteId;
8
+ }
9
+ export function buildTopicMessageLink(chat, messageId) {
10
+ if (!chat) {
11
+ return null;
12
+ }
13
+ const username = Reflect.get(chat, TELEGRAM_CHAT_FIELD.USERNAME);
14
+ if (typeof username === "string" && username.length > 0) {
15
+ return `${TELEGRAM_URL.BASE}/${username}/${messageId}`;
16
+ }
17
+ if (chat.type !== CHAT_TYPE.SUPERGROUP) {
18
+ return null;
19
+ }
20
+ const internalId = getInternalSupergroupId(chat.id);
21
+ return `${TELEGRAM_URL.BASE}${TELEGRAM_URL.PRIVATE_SUPERGROUP_PATH}/${internalId}/${messageId}`;
22
+ }
23
+ export function buildTopicThreadLink(chat, threadId) {
24
+ if (!chat || chat.type !== CHAT_TYPE.SUPERGROUP) {
25
+ return null;
26
+ }
27
+ const internalId = getInternalSupergroupId(chat.id);
28
+ return `${TELEGRAM_URL.BASE}${TELEGRAM_URL.PRIVATE_SUPERGROUP_PATH}/${internalId}/${threadId}`;
29
+ }
@@ -0,0 +1,98 @@
1
+ import { t } from "../i18n/index.js";
2
+ const SUPPORTED_COMMANDS = ["start", "status", "stop", "config"];
3
+ function isCliCommand(value) {
4
+ return SUPPORTED_COMMANDS.includes(value);
5
+ }
6
+ function normalizeMode(value) {
7
+ if (value === "installed") {
8
+ return "installed";
9
+ }
10
+ if (value === "sources") {
11
+ return "sources";
12
+ }
13
+ return null;
14
+ }
15
+ export function parseCliArgs(argv) {
16
+ const args = [...argv];
17
+ let command = "start";
18
+ let mode;
19
+ let showHelp = false;
20
+ let currentIndex = 0;
21
+ const firstArg = args[0];
22
+ if (firstArg && !firstArg.startsWith("-")) {
23
+ if (!isCliCommand(firstArg)) {
24
+ return {
25
+ command,
26
+ showHelp: true,
27
+ error: t("cli.args.unknown_command", { value: firstArg }),
28
+ };
29
+ }
30
+ command = firstArg;
31
+ currentIndex = 1;
32
+ }
33
+ while (currentIndex < args.length) {
34
+ const token = args[currentIndex];
35
+ if (token === "--help" || token === "-h") {
36
+ showHelp = true;
37
+ currentIndex += 1;
38
+ continue;
39
+ }
40
+ if (token === "--mode") {
41
+ const modeValue = args[currentIndex + 1];
42
+ if (!modeValue || modeValue.startsWith("-")) {
43
+ return {
44
+ command,
45
+ mode,
46
+ showHelp: true,
47
+ error: t("cli.args.mode_requires_value"),
48
+ };
49
+ }
50
+ const parsedMode = normalizeMode(modeValue);
51
+ if (!parsedMode) {
52
+ return {
53
+ command,
54
+ mode,
55
+ showHelp: true,
56
+ error: t("cli.args.invalid_mode", { value: modeValue }),
57
+ };
58
+ }
59
+ mode = parsedMode;
60
+ currentIndex += 2;
61
+ continue;
62
+ }
63
+ if (token.startsWith("--mode=")) {
64
+ const modeValue = token.slice("--mode=".length);
65
+ const parsedMode = normalizeMode(modeValue);
66
+ if (!parsedMode) {
67
+ return {
68
+ command,
69
+ mode,
70
+ showHelp: true,
71
+ error: t("cli.args.invalid_mode", { value: modeValue }),
72
+ };
73
+ }
74
+ mode = parsedMode;
75
+ currentIndex += 1;
76
+ continue;
77
+ }
78
+ return {
79
+ command,
80
+ mode,
81
+ showHelp: true,
82
+ error: t("cli.args.unknown_option", { value: token }),
83
+ };
84
+ }
85
+ if (command !== "start" && command !== "config" && mode) {
86
+ return {
87
+ command,
88
+ mode,
89
+ showHelp: true,
90
+ error: t("cli.args.mode_only_start"),
91
+ };
92
+ }
93
+ return {
94
+ command,
95
+ mode,
96
+ showHelp,
97
+ };
98
+ }
package/dist/cli.js ADDED
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env node
2
+ import { parseCliArgs } from "./cli/args.js";
3
+ import { resolveRuntimeMode, setRuntimeMode } from "./runtime/mode.js";
4
+ import { t } from "./i18n/index.js";
5
+ const EXIT_SUCCESS = 0;
6
+ const EXIT_RUNTIME_ERROR = 1;
7
+ const EXIT_INVALID_ARGS = 2;
8
+ function writeStdout(message) {
9
+ process.stdout.write(`${message}\n`);
10
+ }
11
+ function writeStderr(message) {
12
+ process.stderr.write(`${message}\n`);
13
+ }
14
+ function printUsage() {
15
+ writeStdout(t("cli.usage"));
16
+ }
17
+ function getPlaceholderMessage(command) {
18
+ if (command === "status") {
19
+ return t("cli.placeholder.status");
20
+ }
21
+ if (command === "stop") {
22
+ return t("cli.placeholder.stop");
23
+ }
24
+ return t("cli.placeholder.unavailable");
25
+ }
26
+ async function runStartCommand(mode) {
27
+ const modeResult = resolveRuntimeMode({
28
+ defaultMode: "installed",
29
+ explicitMode: mode,
30
+ });
31
+ if (modeResult.error) {
32
+ throw new Error(modeResult.error);
33
+ }
34
+ setRuntimeMode(modeResult.mode);
35
+ const { ensureRuntimeConfigForStart } = await import("./runtime/bootstrap.js");
36
+ await ensureRuntimeConfigForStart();
37
+ const { startBotApp } = await import("./app/start-bot-app.js");
38
+ await startBotApp();
39
+ return EXIT_SUCCESS;
40
+ }
41
+ async function runConfigCommand(mode) {
42
+ setRuntimeMode(mode ?? "installed");
43
+ const { runConfigWizardCommand } = await import("./runtime/bootstrap.js");
44
+ await runConfigWizardCommand();
45
+ return EXIT_SUCCESS;
46
+ }
47
+ async function runPlaceholderCommand(command) {
48
+ writeStdout(getPlaceholderMessage(command));
49
+ return EXIT_SUCCESS;
50
+ }
51
+ async function runCli(argv) {
52
+ const parsedArgs = parseCliArgs(argv);
53
+ if (parsedArgs.error) {
54
+ writeStderr(parsedArgs.error);
55
+ }
56
+ if (parsedArgs.showHelp) {
57
+ printUsage();
58
+ return parsedArgs.error ? EXIT_INVALID_ARGS : EXIT_SUCCESS;
59
+ }
60
+ if (parsedArgs.command === "start") {
61
+ return runStartCommand(parsedArgs.mode);
62
+ }
63
+ if (parsedArgs.command === "config") {
64
+ return runConfigCommand(parsedArgs.mode);
65
+ }
66
+ return runPlaceholderCommand(parsedArgs.command);
67
+ }
68
+ void runCli(process.argv.slice(2))
69
+ .then((exitCode) => {
70
+ process.exitCode = exitCode;
71
+ })
72
+ .catch((error) => {
73
+ if (error instanceof Error) {
74
+ writeStderr(t("cli.error.prefix", { message: error.message }));
75
+ }
76
+ else {
77
+ writeStderr(t("cli.error.prefix", { message: String(error) }));
78
+ }
79
+ process.exitCode = EXIT_RUNTIME_ERROR;
80
+ });
package/dist/config.js ADDED
@@ -0,0 +1,103 @@
1
+ import dotenv from "dotenv";
2
+ import { getRuntimePaths } from "./runtime/paths.js";
3
+ import { normalizeLocale } from "./i18n/index.js";
4
+ const runtimePaths = getRuntimePaths();
5
+ dotenv.config({ path: runtimePaths.envFilePath, quiet: true });
6
+ function getEnvVar(key, required = true) {
7
+ const value = process.env[key];
8
+ if (required && !value) {
9
+ throw new Error(`Missing required environment variable: ${key} (expected in ${runtimePaths.envFilePath})`);
10
+ }
11
+ return value || "";
12
+ }
13
+ function getOptionalPositiveIntEnvVar(key, defaultValue) {
14
+ const value = getEnvVar(key, false);
15
+ if (!value) {
16
+ return defaultValue;
17
+ }
18
+ const parsedValue = Number.parseInt(value, 10);
19
+ if (Number.isNaN(parsedValue) || parsedValue <= 0) {
20
+ return defaultValue;
21
+ }
22
+ return parsedValue;
23
+ }
24
+ function getOptionalNonNegativeIntEnvVarFromKeys(keys, defaultValue) {
25
+ for (const key of keys) {
26
+ const value = getEnvVar(key, false);
27
+ if (!value) {
28
+ continue;
29
+ }
30
+ const parsedValue = Number.parseInt(value, 10);
31
+ if (Number.isNaN(parsedValue) || parsedValue < 0) {
32
+ return defaultValue;
33
+ }
34
+ return parsedValue;
35
+ }
36
+ return defaultValue;
37
+ }
38
+ function getOptionalLocaleEnvVar(key, defaultValue) {
39
+ const value = getEnvVar(key, false);
40
+ return normalizeLocale(value, defaultValue);
41
+ }
42
+ function getOptionalBooleanEnvVar(key, defaultValue) {
43
+ const value = getEnvVar(key, false);
44
+ if (!value) {
45
+ return defaultValue;
46
+ }
47
+ const normalized = value.trim().toLowerCase();
48
+ if (["1", "true", "yes", "on"].includes(normalized)) {
49
+ return true;
50
+ }
51
+ if (["0", "false", "no", "off"].includes(normalized)) {
52
+ return false;
53
+ }
54
+ return defaultValue;
55
+ }
56
+ function getOptionalMessageFormatModeEnvVar(key, defaultValue) {
57
+ const value = getEnvVar(key, false);
58
+ if (!value) {
59
+ return defaultValue;
60
+ }
61
+ const normalized = value.trim().toLowerCase();
62
+ if (normalized === "raw" || normalized === "markdown") {
63
+ return normalized;
64
+ }
65
+ return defaultValue;
66
+ }
67
+ export const config = {
68
+ telegram: {
69
+ token: getEnvVar("TELEGRAM_BOT_TOKEN"),
70
+ allowedUserId: parseInt(getEnvVar("TELEGRAM_ALLOWED_USER_ID"), 10),
71
+ proxyUrl: getEnvVar("TELEGRAM_PROXY_URL", false),
72
+ },
73
+ opencode: {
74
+ apiUrl: getEnvVar("OPENCODE_API_URL", false) || "http://localhost:4096",
75
+ username: getEnvVar("OPENCODE_SERVER_USERNAME", false) || "opencode",
76
+ password: getEnvVar("OPENCODE_SERVER_PASSWORD", false),
77
+ model: {
78
+ provider: getEnvVar("OPENCODE_MODEL_PROVIDER", true), // Required
79
+ modelId: getEnvVar("OPENCODE_MODEL_ID", true), // Required
80
+ },
81
+ },
82
+ server: {
83
+ logLevel: getEnvVar("LOG_LEVEL", false) || "info",
84
+ },
85
+ bot: {
86
+ sessionsListLimit: getOptionalPositiveIntEnvVar("SESSIONS_LIST_LIMIT", 10),
87
+ projectsListLimit: getOptionalPositiveIntEnvVar("PROJECTS_LIST_LIMIT", 10),
88
+ locale: getOptionalLocaleEnvVar("BOT_LOCALE", "en"),
89
+ serviceMessagesIntervalSec: getOptionalNonNegativeIntEnvVarFromKeys(["SERVICE_MESSAGES_INTERVAL_SEC", "TOOL_MESSAGES_INTERVAL_SEC"], 5),
90
+ hideThinkingMessages: getOptionalBooleanEnvVar("HIDE_THINKING_MESSAGES", false),
91
+ hideToolCallMessages: getOptionalBooleanEnvVar("HIDE_TOOL_CALL_MESSAGES", false),
92
+ messageFormatMode: getOptionalMessageFormatModeEnvVar("MESSAGE_FORMAT_MODE", "markdown"),
93
+ },
94
+ files: {
95
+ maxFileSizeKb: parseInt(getEnvVar("CODE_FILE_MAX_SIZE_KB", false) || "100", 10),
96
+ },
97
+ stt: {
98
+ apiUrl: getEnvVar("STT_API_URL", false),
99
+ apiKey: getEnvVar("STT_API_KEY", false),
100
+ model: getEnvVar("STT_MODEL", false) || "whisper-large-v3-turbo",
101
+ language: getEnvVar("STT_LANGUAGE", false),
102
+ },
103
+ };