zardbot-telegram 1.0.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 (111) hide show
  1. package/.env.example +116 -0
  2. package/LICENSE +21 -0
  3. package/README.md +250 -0
  4. package/dist/agent/manager.js +88 -0
  5. package/dist/agent/types.js +26 -0
  6. package/dist/app/start-bot-app.js +49 -0
  7. package/dist/bot/commands/abort.js +121 -0
  8. package/dist/bot/commands/commands.js +480 -0
  9. package/dist/bot/commands/definitions.js +27 -0
  10. package/dist/bot/commands/help.js +10 -0
  11. package/dist/bot/commands/models.js +38 -0
  12. package/dist/bot/commands/new.js +70 -0
  13. package/dist/bot/commands/opencode-start.js +101 -0
  14. package/dist/bot/commands/opencode-stop.js +44 -0
  15. package/dist/bot/commands/projects.js +223 -0
  16. package/dist/bot/commands/rename.js +139 -0
  17. package/dist/bot/commands/sessions.js +351 -0
  18. package/dist/bot/commands/start.js +43 -0
  19. package/dist/bot/commands/status.js +95 -0
  20. package/dist/bot/commands/task.js +399 -0
  21. package/dist/bot/commands/tasklist.js +220 -0
  22. package/dist/bot/commands/voice.js +145 -0
  23. package/dist/bot/handlers/agent.js +118 -0
  24. package/dist/bot/handlers/context.js +100 -0
  25. package/dist/bot/handlers/document.js +65 -0
  26. package/dist/bot/handlers/inline-menu.js +119 -0
  27. package/dist/bot/handlers/model.js +143 -0
  28. package/dist/bot/handlers/permission.js +235 -0
  29. package/dist/bot/handlers/prompt.js +240 -0
  30. package/dist/bot/handlers/question.js +390 -0
  31. package/dist/bot/handlers/tts.js +89 -0
  32. package/dist/bot/handlers/variant.js +138 -0
  33. package/dist/bot/handlers/voice.js +173 -0
  34. package/dist/bot/index.js +977 -0
  35. package/dist/bot/message-patterns.js +4 -0
  36. package/dist/bot/middleware/auth.js +30 -0
  37. package/dist/bot/middleware/interaction-guard.js +95 -0
  38. package/dist/bot/middleware/unknown-command.js +22 -0
  39. package/dist/bot/streaming/response-streamer.js +286 -0
  40. package/dist/bot/streaming/tool-call-streamer.js +285 -0
  41. package/dist/bot/utils/busy-guard.js +15 -0
  42. package/dist/bot/utils/commands.js +21 -0
  43. package/dist/bot/utils/file-download.js +91 -0
  44. package/dist/bot/utils/finalize-assistant-response.js +52 -0
  45. package/dist/bot/utils/keyboard.js +69 -0
  46. package/dist/bot/utils/send-with-markdown-fallback.js +165 -0
  47. package/dist/bot/utils/telegram-text.js +28 -0
  48. package/dist/bot/utils/thinking-message.js +8 -0
  49. package/dist/cli/args.js +98 -0
  50. package/dist/cli.js +80 -0
  51. package/dist/config.js +97 -0
  52. package/dist/i18n/de.js +357 -0
  53. package/dist/i18n/en.js +357 -0
  54. package/dist/i18n/es.js +357 -0
  55. package/dist/i18n/fr.js +357 -0
  56. package/dist/i18n/index.js +109 -0
  57. package/dist/i18n/ru.js +357 -0
  58. package/dist/i18n/zh.js +357 -0
  59. package/dist/index.js +26 -0
  60. package/dist/interaction/busy.js +8 -0
  61. package/dist/interaction/cleanup.js +32 -0
  62. package/dist/interaction/guard.js +140 -0
  63. package/dist/interaction/manager.js +106 -0
  64. package/dist/interaction/types.js +1 -0
  65. package/dist/keyboard/manager.js +172 -0
  66. package/dist/keyboard/types.js +1 -0
  67. package/dist/model/capabilities.js +62 -0
  68. package/dist/model/context-limit.js +57 -0
  69. package/dist/model/manager.js +259 -0
  70. package/dist/model/types.js +24 -0
  71. package/dist/opencode/client.js +13 -0
  72. package/dist/opencode/events.js +140 -0
  73. package/dist/permission/manager.js +100 -0
  74. package/dist/permission/types.js +1 -0
  75. package/dist/pinned/format.js +29 -0
  76. package/dist/pinned/manager.js +682 -0
  77. package/dist/pinned/types.js +1 -0
  78. package/dist/process/manager.js +273 -0
  79. package/dist/process/types.js +1 -0
  80. package/dist/project/manager.js +88 -0
  81. package/dist/question/manager.js +176 -0
  82. package/dist/question/types.js +1 -0
  83. package/dist/rename/manager.js +53 -0
  84. package/dist/runtime/bootstrap.js +350 -0
  85. package/dist/runtime/mode.js +74 -0
  86. package/dist/runtime/paths.js +37 -0
  87. package/dist/scheduled-task/creation-manager.js +113 -0
  88. package/dist/scheduled-task/display.js +239 -0
  89. package/dist/scheduled-task/executor.js +87 -0
  90. package/dist/scheduled-task/foreground-state.js +32 -0
  91. package/dist/scheduled-task/next-run.js +207 -0
  92. package/dist/scheduled-task/runtime.js +368 -0
  93. package/dist/scheduled-task/schedule-parser.js +169 -0
  94. package/dist/scheduled-task/store.js +65 -0
  95. package/dist/scheduled-task/types.js +19 -0
  96. package/dist/session/cache-manager.js +455 -0
  97. package/dist/session/manager.js +10 -0
  98. package/dist/settings/manager.js +158 -0
  99. package/dist/stt/client.js +97 -0
  100. package/dist/summary/aggregator.js +1136 -0
  101. package/dist/summary/formatter.js +491 -0
  102. package/dist/summary/subagent-formatter.js +63 -0
  103. package/dist/summary/tool-message-batcher.js +90 -0
  104. package/dist/tts/client.js +130 -0
  105. package/dist/utils/error-format.js +29 -0
  106. package/dist/utils/logger.js +127 -0
  107. package/dist/utils/safe-background-task.js +33 -0
  108. package/dist/utils/telegram-rate-limit-retry.js +93 -0
  109. package/dist/variant/manager.js +103 -0
  110. package/dist/variant/types.js +1 -0
  111. package/package.json +79 -0
@@ -0,0 +1,13 @@
1
+ import { createOpencodeClient } from "@opencode-ai/sdk/v2";
2
+ import { config } from "../config.js";
3
+ const getAuth = () => {
4
+ if (!config.opencode.password) {
5
+ return undefined;
6
+ }
7
+ const credentials = `${config.opencode.username}:${config.opencode.password}`;
8
+ return `Basic ${Buffer.from(credentials).toString("base64")}`;
9
+ };
10
+ export const opencodeClient = createOpencodeClient({
11
+ baseUrl: config.opencode.apiUrl,
12
+ headers: config.opencode.password ? { Authorization: getAuth() } : undefined,
13
+ });
@@ -0,0 +1,140 @@
1
+ import { opencodeClient } from "./client.js";
2
+ import { logger } from "../utils/logger.js";
3
+ const RECONNECT_BASE_DELAY_MS = 1000;
4
+ const RECONNECT_MAX_DELAY_MS = 15000;
5
+ const FATAL_NO_STREAM_ERROR = "No stream returned from event subscription";
6
+ let eventStream = null;
7
+ let eventCallback = null;
8
+ let isListening = false;
9
+ let activeDirectory = null;
10
+ let streamAbortController = null;
11
+ function getReconnectDelayMs(attempt) {
12
+ const exponentialDelay = RECONNECT_BASE_DELAY_MS * Math.pow(2, Math.max(0, attempt - 1));
13
+ return Math.min(exponentialDelay, RECONNECT_MAX_DELAY_MS);
14
+ }
15
+ function waitWithAbort(ms, signal) {
16
+ return new Promise((resolve) => {
17
+ if (signal.aborted) {
18
+ resolve(false);
19
+ return;
20
+ }
21
+ const onAbort = () => {
22
+ clearTimeout(timeout);
23
+ signal.removeEventListener("abort", onAbort);
24
+ resolve(false);
25
+ };
26
+ const timeout = setTimeout(() => {
27
+ signal.removeEventListener("abort", onAbort);
28
+ resolve(true);
29
+ }, ms);
30
+ signal.addEventListener("abort", onAbort, { once: true });
31
+ });
32
+ }
33
+ export async function subscribeToEvents(directory, callback) {
34
+ if (isListening && activeDirectory === directory) {
35
+ eventCallback = callback;
36
+ logger.debug(`Event listener already running for ${directory}`);
37
+ return;
38
+ }
39
+ if (isListening && activeDirectory !== directory) {
40
+ logger.info(`Stopping event listener for ${activeDirectory}, starting for ${directory}`);
41
+ streamAbortController?.abort();
42
+ streamAbortController = null;
43
+ isListening = false;
44
+ activeDirectory = null;
45
+ }
46
+ const controller = new AbortController();
47
+ activeDirectory = directory;
48
+ eventCallback = callback;
49
+ isListening = true;
50
+ streamAbortController = controller;
51
+ try {
52
+ let reconnectAttempt = 0;
53
+ while (isListening && activeDirectory === directory && !controller.signal.aborted) {
54
+ try {
55
+ const result = await opencodeClient.event.subscribe({ directory }, { signal: controller.signal });
56
+ if (!result.stream) {
57
+ throw new Error(FATAL_NO_STREAM_ERROR);
58
+ }
59
+ reconnectAttempt = 0;
60
+ eventStream = result.stream;
61
+ for await (const event of eventStream) {
62
+ if (!isListening || activeDirectory !== directory || controller.signal.aborted) {
63
+ logger.debug(`Event listener stopped or changed directory, breaking loop`);
64
+ break;
65
+ }
66
+ // CRITICAL: Explicitly yield to the event loop BEFORE processing the event
67
+ // This allows grammY to handle getUpdates between SSE events
68
+ await new Promise((resolve) => setImmediate(resolve));
69
+ if (eventCallback) {
70
+ // Use setImmediate to avoid blocking the event loop
71
+ // and let grammY process incoming Telegram updates
72
+ const callbackSnapshot = eventCallback;
73
+ setImmediate(() => callbackSnapshot(event));
74
+ }
75
+ }
76
+ eventStream = null;
77
+ if (!isListening || activeDirectory !== directory || controller.signal.aborted) {
78
+ break;
79
+ }
80
+ reconnectAttempt++;
81
+ const reconnectDelay = getReconnectDelayMs(reconnectAttempt);
82
+ logger.warn(`Event stream ended for ${directory}, reconnecting in ${reconnectDelay}ms (attempt=${reconnectAttempt})`);
83
+ const shouldContinue = await waitWithAbort(reconnectDelay, controller.signal);
84
+ if (!shouldContinue) {
85
+ break;
86
+ }
87
+ }
88
+ catch (error) {
89
+ eventStream = null;
90
+ if (controller.signal.aborted || !isListening || activeDirectory !== directory) {
91
+ logger.info("Event listener aborted");
92
+ return;
93
+ }
94
+ if (error instanceof Error && error.message === FATAL_NO_STREAM_ERROR) {
95
+ logger.error("Event stream fatal error:", error);
96
+ throw error;
97
+ }
98
+ reconnectAttempt++;
99
+ const reconnectDelay = getReconnectDelayMs(reconnectAttempt);
100
+ logger.error(`Event stream error for ${directory}, reconnecting in ${reconnectDelay}ms (attempt=${reconnectAttempt})`, error);
101
+ const shouldContinue = await waitWithAbort(reconnectDelay, controller.signal);
102
+ if (!shouldContinue) {
103
+ break;
104
+ }
105
+ }
106
+ }
107
+ }
108
+ catch (error) {
109
+ if (controller.signal.aborted) {
110
+ logger.info("Event listener aborted");
111
+ return;
112
+ }
113
+ logger.error("Event stream error:", error);
114
+ isListening = false;
115
+ activeDirectory = null;
116
+ streamAbortController = null;
117
+ throw error;
118
+ }
119
+ finally {
120
+ if (streamAbortController === controller) {
121
+ if (isListening && activeDirectory === directory && !controller.signal.aborted) {
122
+ logger.warn(`Event stream ended for ${directory}, listener marked as disconnected`);
123
+ }
124
+ streamAbortController = null;
125
+ eventStream = null;
126
+ eventCallback = null;
127
+ isListening = false;
128
+ activeDirectory = null;
129
+ }
130
+ }
131
+ }
132
+ export function stopEventListening() {
133
+ streamAbortController?.abort();
134
+ streamAbortController = null;
135
+ isListening = false;
136
+ eventCallback = null;
137
+ eventStream = null;
138
+ activeDirectory = null;
139
+ logger.info("Event listener stopped");
140
+ }
@@ -0,0 +1,100 @@
1
+ import { logger } from "../utils/logger.js";
2
+ class PermissionManager {
3
+ state = {
4
+ requestsByMessageId: new Map(),
5
+ };
6
+ /**
7
+ * Register a new permission request message
8
+ */
9
+ startPermission(request, messageId) {
10
+ logger.debug(`[PermissionManager] startPermission: id=${request.id}, permission=${request.permission}, messageId=${messageId}`);
11
+ if (this.state.requestsByMessageId.has(messageId)) {
12
+ logger.warn(`[PermissionManager] Message ID already tracked, replacing: ${messageId}`);
13
+ }
14
+ this.state.requestsByMessageId.set(messageId, request);
15
+ logger.info(`[PermissionManager] New permission request: type=${request.permission}, patterns=${request.patterns.join(", ")}, pending=${this.state.requestsByMessageId.size}`);
16
+ }
17
+ /**
18
+ * Get permission request by Telegram message ID
19
+ */
20
+ getRequest(messageId) {
21
+ if (messageId === null) {
22
+ return null;
23
+ }
24
+ return this.state.requestsByMessageId.get(messageId) ?? null;
25
+ }
26
+ /**
27
+ * Get request ID for API reply by Telegram message ID
28
+ */
29
+ getRequestID(messageId) {
30
+ return this.getRequest(messageId)?.id ?? null;
31
+ }
32
+ /**
33
+ * Get permission type (bash, edit, etc.) by message ID
34
+ */
35
+ getPermissionType(messageId) {
36
+ return this.getRequest(messageId)?.permission ?? null;
37
+ }
38
+ /**
39
+ * Get patterns (commands/files) by message ID
40
+ */
41
+ getPatterns(messageId) {
42
+ return this.getRequest(messageId)?.patterns ?? [];
43
+ }
44
+ /**
45
+ * Check if callback message ID belongs to active permission request
46
+ */
47
+ isActiveMessage(messageId) {
48
+ return messageId !== null && this.state.requestsByMessageId.has(messageId);
49
+ }
50
+ /**
51
+ * Get latest Telegram message ID
52
+ */
53
+ getMessageId() {
54
+ const messageIds = this.getMessageIds();
55
+ if (messageIds.length === 0) {
56
+ return null;
57
+ }
58
+ return messageIds[messageIds.length - 1];
59
+ }
60
+ /**
61
+ * Get Telegram message IDs for all active requests
62
+ */
63
+ getMessageIds() {
64
+ return Array.from(this.state.requestsByMessageId.keys());
65
+ }
66
+ /**
67
+ * Remove permission request by Telegram message ID
68
+ */
69
+ removeByMessageId(messageId) {
70
+ const request = this.getRequest(messageId);
71
+ if (!request || messageId === null) {
72
+ return null;
73
+ }
74
+ this.state.requestsByMessageId.delete(messageId);
75
+ logger.debug(`[PermissionManager] Removed permission request: id=${request.id}, messageId=${messageId}, pending=${this.state.requestsByMessageId.size}`);
76
+ return request;
77
+ }
78
+ /**
79
+ * Get number of active permission requests
80
+ */
81
+ getPendingCount() {
82
+ return this.state.requestsByMessageId.size;
83
+ }
84
+ /**
85
+ * Check if there are active permission requests
86
+ */
87
+ isActive() {
88
+ return this.state.requestsByMessageId.size > 0;
89
+ }
90
+ /**
91
+ * Clear state after reply
92
+ */
93
+ clear() {
94
+ logger.debug(`[PermissionManager] Clearing permission state: pending=${this.state.requestsByMessageId.size}`);
95
+ this.state = {
96
+ requestsByMessageId: new Map(),
97
+ };
98
+ }
99
+ }
100
+ export const permissionManager = new PermissionManager();
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,29 @@
1
+ import { t } from "../i18n/index.js";
2
+ export const DEFAULT_CONTEXT_LIMIT = 200000;
3
+ export function formatTokenCount(count) {
4
+ if (count >= 1000000) {
5
+ return `${(count / 1000000).toFixed(1)}M`;
6
+ }
7
+ if (count >= 1000) {
8
+ return `${Math.round(count / 1000)}K`;
9
+ }
10
+ return count.toString();
11
+ }
12
+ export function formatModelDisplayName(providerID, modelID) {
13
+ if (providerID && modelID) {
14
+ return `${providerID}/${modelID}`;
15
+ }
16
+ return t("pinned.unknown");
17
+ }
18
+ export function formatContextLine(tokensUsed, tokensLimit) {
19
+ const safeLimit = typeof tokensLimit === "number" && tokensLimit > 0 ? tokensLimit : null;
20
+ const percentage = safeLimit ? Math.round((tokensUsed / safeLimit) * 100) : 0;
21
+ return t("pinned.line.context", {
22
+ used: formatTokenCount(tokensUsed),
23
+ limit: safeLimit ? formatTokenCount(safeLimit) : t("pinned.unknown"),
24
+ percent: percentage,
25
+ });
26
+ }
27
+ export function formatCostLine(cost) {
28
+ return t("pinned.line.cost", { cost: `$${cost.toFixed(2)}` });
29
+ }