@dreb/telegram 1.16.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 (82) hide show
  1. package/README.md +91 -0
  2. package/dist/agent-bridge.d.ts +146 -0
  3. package/dist/agent-bridge.d.ts.map +1 -0
  4. package/dist/agent-bridge.js +466 -0
  5. package/dist/agent-bridge.js.map +1 -0
  6. package/dist/bot.d.ts +11 -0
  7. package/dist/bot.d.ts.map +1 -0
  8. package/dist/bot.js +112 -0
  9. package/dist/bot.js.map +1 -0
  10. package/dist/bridge-lifecycle.d.ts +17 -0
  11. package/dist/bridge-lifecycle.d.ts.map +1 -0
  12. package/dist/bridge-lifecycle.js +71 -0
  13. package/dist/bridge-lifecycle.js.map +1 -0
  14. package/dist/commands/agent.d.ts +11 -0
  15. package/dist/commands/agent.d.ts.map +1 -0
  16. package/dist/commands/agent.js +171 -0
  17. package/dist/commands/agent.js.map +1 -0
  18. package/dist/commands/buddy.d.ts +20 -0
  19. package/dist/commands/buddy.d.ts.map +1 -0
  20. package/dist/commands/buddy.js +84 -0
  21. package/dist/commands/buddy.js.map +1 -0
  22. package/dist/commands/core.d.ts +13 -0
  23. package/dist/commands/core.d.ts.map +1 -0
  24. package/dist/commands/core.js +107 -0
  25. package/dist/commands/core.js.map +1 -0
  26. package/dist/commands/index.d.ts +16 -0
  27. package/dist/commands/index.d.ts.map +1 -0
  28. package/dist/commands/index.js +132 -0
  29. package/dist/commands/index.js.map +1 -0
  30. package/dist/commands/refresh.d.ts +18 -0
  31. package/dist/commands/refresh.d.ts.map +1 -0
  32. package/dist/commands/refresh.js +55 -0
  33. package/dist/commands/refresh.js.map +1 -0
  34. package/dist/commands/sessions.d.ts +10 -0
  35. package/dist/commands/sessions.d.ts.map +1 -0
  36. package/dist/commands/sessions.js +125 -0
  37. package/dist/commands/sessions.js.map +1 -0
  38. package/dist/commands/skills.d.ts +10 -0
  39. package/dist/commands/skills.d.ts.map +1 -0
  40. package/dist/commands/skills.js +48 -0
  41. package/dist/commands/skills.js.map +1 -0
  42. package/dist/config.d.ts +30 -0
  43. package/dist/config.d.ts.map +1 -0
  44. package/dist/config.js +77 -0
  45. package/dist/config.js.map +1 -0
  46. package/dist/handlers/buddy.d.ts +31 -0
  47. package/dist/handlers/buddy.d.ts.map +1 -0
  48. package/dist/handlers/buddy.js +126 -0
  49. package/dist/handlers/buddy.js.map +1 -0
  50. package/dist/handlers/events.d.ts +65 -0
  51. package/dist/handlers/events.d.ts.map +1 -0
  52. package/dist/handlers/events.js +381 -0
  53. package/dist/handlers/events.js.map +1 -0
  54. package/dist/handlers/file.d.ts +11 -0
  55. package/dist/handlers/file.d.ts.map +1 -0
  56. package/dist/handlers/file.js +138 -0
  57. package/dist/handlers/file.js.map +1 -0
  58. package/dist/handlers/message.d.ts +34 -0
  59. package/dist/handlers/message.d.ts.map +1 -0
  60. package/dist/handlers/message.js +262 -0
  61. package/dist/handlers/message.js.map +1 -0
  62. package/dist/index.d.ts +8 -0
  63. package/dist/index.d.ts.map +1 -0
  64. package/dist/index.js +82 -0
  65. package/dist/index.js.map +1 -0
  66. package/dist/state.d.ts +11 -0
  67. package/dist/state.d.ts.map +1 -0
  68. package/dist/state.js +47 -0
  69. package/dist/state.js.map +1 -0
  70. package/dist/types.d.ts +50 -0
  71. package/dist/types.d.ts.map +1 -0
  72. package/dist/types.js +5 -0
  73. package/dist/types.js.map +1 -0
  74. package/dist/util/files.d.ts +27 -0
  75. package/dist/util/files.d.ts.map +1 -0
  76. package/dist/util/files.js +75 -0
  77. package/dist/util/files.js.map +1 -0
  78. package/dist/util/telegram.d.ts +60 -0
  79. package/dist/util/telegram.d.ts.map +1 -0
  80. package/dist/util/telegram.js +192 -0
  81. package/dist/util/telegram.js.map +1 -0
  82. package/package.json +49 -0
@@ -0,0 +1,381 @@
1
+ /**
2
+ * Event display — translates RPC agent events into Telegram messages.
3
+ *
4
+ * Manages an ephemeral status message that shows tool use, task lists,
5
+ * and subagent activity. Text from the agent is sent as permanent messages.
6
+ */
7
+ import { existsSync } from "node:fs";
8
+ import { InputFile } from "grammy";
9
+ import { extractSendFiles } from "../util/files.js";
10
+ import { DebouncedEditor, log, safeDelete } from "../util/telegram.js";
11
+ // Tool emoji mapping (tool names are lowercase in definitions)
12
+ const TOOL_EMOJI = {
13
+ bash: "🔧",
14
+ read: "📖",
15
+ edit: "âœī¸",
16
+ write: "📝",
17
+ grep: "🔎",
18
+ find: "🔍",
19
+ ls: "📂",
20
+ web_search: "🌐",
21
+ web_fetch: "🌐",
22
+ subagent: "🤖",
23
+ tasks_update: "📋",
24
+ skill: "⚡",
25
+ };
26
+ function toolEmoji(name) {
27
+ return TOOL_EMOJI[name] || "🔧";
28
+ }
29
+ /** Format a tool call for display */
30
+ function formatTool(name, args) {
31
+ const emoji = toolEmoji(name);
32
+ switch (name) {
33
+ case "bash": {
34
+ const cmd = args.command || "";
35
+ return `${emoji} *bash*\n\`${cmd.slice(0, 500)}\``;
36
+ }
37
+ case "read":
38
+ return `${emoji} *read*: \`${args.path || "?"}\``;
39
+ case "edit":
40
+ return `${emoji} *edit*: \`${args.path || "?"}\``;
41
+ case "write":
42
+ return `${emoji} *write*: \`${args.path || "?"}\``;
43
+ case "grep":
44
+ return `${emoji} *grep*: \`${args.pattern || "?"}\``;
45
+ case "find":
46
+ return `${emoji} *find*: \`${args.pattern || "?"}\``;
47
+ case "ls":
48
+ return `${emoji} *ls*: \`${args.path || "."}\``;
49
+ case "web_search":
50
+ return `${emoji} *web\\_search*: ${args.query || "?"}`;
51
+ case "web_fetch":
52
+ return `${emoji} *web\\_fetch*: ${(args.url || "?").slice(0, 80)}`;
53
+ case "subagent":
54
+ return `${emoji} *subagent* (${args.agent || "?"}): ${(args.task || args.tasks?.[0]?.task || "?").slice(0, 200)}`;
55
+ case "skill":
56
+ return `${emoji} *skill*: ${args.skill || "?"}`;
57
+ default:
58
+ return `${emoji} *${name}*`;
59
+ }
60
+ }
61
+ /** Format task list as checklist */
62
+ function formatTaskList(tasks) {
63
+ if (!tasks.length)
64
+ return "📋 *Tasks*: (empty)";
65
+ const lines = ["📋 *Tasks*:"];
66
+ for (const task of tasks) {
67
+ if (task.status === "completed")
68
+ lines.push(` ✅ ${task.title}`);
69
+ else if (task.status === "in_progress")
70
+ lines.push(` 🔄 ${task.title}`);
71
+ else
72
+ lines.push(` âŦœ ${task.title}`);
73
+ }
74
+ return lines.join("\n");
75
+ }
76
+ /**
77
+ * Check if an error message looks retryable (overloaded, rate limit, server errors).
78
+ * Mirrors the core's _isRetryableError check as a defensive Layer 2.
79
+ */
80
+ const RETRYABLE_ERROR_PATTERN = /overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|timed? out|timeout|terminated|retry delay/i;
81
+ function isRetryableError(errorMessage) {
82
+ return RETRYABLE_ERROR_PATTERN.test(errorMessage);
83
+ }
84
+ /**
85
+ * Create a fresh event display state for a new agent run.
86
+ */
87
+ export function createEventDisplay(api, chatId, replyToId, statusMessageId) {
88
+ return {
89
+ chatId,
90
+ replyToId,
91
+ statusMessageId,
92
+ toolsSinceText: [],
93
+ toolCount: 0,
94
+ textBlocks: [],
95
+ tasks: [],
96
+ backgroundAgents: new Map(),
97
+ done: false,
98
+ editor: new DebouncedEditor(api),
99
+ retryInProgress: false,
100
+ pendingRetry: false,
101
+ retryAttempt: 0,
102
+ };
103
+ }
104
+ /**
105
+ * Process an agent event and update the display.
106
+ */
107
+ export async function handleAgentEvent(send, api, state, event) {
108
+ switch (event.type) {
109
+ case "tool_execution_start": {
110
+ const name = event.toolName || "?";
111
+ const args = event.args || {};
112
+ state.toolCount++;
113
+ // tasks_update is shown via the separate tasks_update event — skip from tool summary
114
+ if (name !== "tasks_update") {
115
+ const toolMsg = formatTool(name, args);
116
+ state.toolsSinceText.push(toolMsg);
117
+ }
118
+ // Update status with tool count and recent tools
119
+ updateStatus(state);
120
+ break;
121
+ }
122
+ case "tool_execution_end": {
123
+ // Feed event to buddy controller for context capture + error reactions
124
+ state.buddyController?.handleEvent(event);
125
+ break;
126
+ }
127
+ case "message_end": {
128
+ const msg = event.message;
129
+ // Show subagent results — the parent agent references these but the
130
+ // Telegram user can't see them otherwise. Send the full content.
131
+ if (msg?.role === "toolResult" && msg?.toolName === "subagent") {
132
+ const content = msg?.content;
133
+ if (content && Array.isArray(content)) {
134
+ for (const block of content) {
135
+ if (block.type === "text" && block.text?.trim()) {
136
+ send(`🤖 *Subagent result:*\n${block.text.trim()}`, true);
137
+ }
138
+ }
139
+ }
140
+ break;
141
+ }
142
+ // Show background agent completion results — these arrive as user
143
+ // messages injected by agent-session.ts via prompt()/steer() and
144
+ // contain the actual subagent output the model sees.
145
+ if (msg?.role === "user") {
146
+ const content = msg?.content;
147
+ if (content && Array.isArray(content)) {
148
+ for (const block of content) {
149
+ if (block.type === "text" && block.text?.includes("<background-agent-complete>")) {
150
+ // Extract the content between the XML tags
151
+ const match = block.text.match(/<background-agent-complete>\n?([\s\S]*?)\n?<\/background-agent-complete>/);
152
+ if (match?.[1]?.trim()) {
153
+ send(`🤖 *Background agent complete:*\n${match[1].trim()}`, true);
154
+ }
155
+ }
156
+ }
157
+ }
158
+ break;
159
+ }
160
+ // Only display assistant messages — user messages are echoed back by RPC
161
+ if (msg?.role !== "assistant")
162
+ break;
163
+ const content = msg?.content;
164
+ if (!content || !Array.isArray(content))
165
+ break;
166
+ for (const block of content) {
167
+ // Display thinking blocks (collapsed summary)
168
+ if (block.type === "thinking" && block.thinking?.trim() && !block.redacted) {
169
+ const thinking = block.thinking.trim();
170
+ send(`💭 _${thinking}_`, true);
171
+ }
172
+ if (block.type === "text" && block.text?.trim()) {
173
+ const text = block.text.trim();
174
+ // Flush accumulated tools as permanent summary
175
+ if (state.toolsSinceText.length > 0) {
176
+ const summary = `📋 *${state.toolsSinceText.length} tools*:\n${state.toolsSinceText.join("\n")}`;
177
+ send(summary, true);
178
+ state.toolsSinceText = [];
179
+ }
180
+ // Send the text as a permanent message
181
+ state.textBlocks.push(text);
182
+ // Check for file send markers
183
+ const [cleanText, filePaths] = extractSendFiles(text);
184
+ if (cleanText) {
185
+ send(cleanText, true);
186
+ }
187
+ // Send any requested files (silently skip non-existent paths —
188
+ // the pattern may appear in explanatory text)
189
+ for (const filePath of filePaths) {
190
+ try {
191
+ if (existsSync(filePath)) {
192
+ await api.sendDocument(state.chatId, new InputFile(filePath));
193
+ }
194
+ }
195
+ catch (e) {
196
+ log(`[EVENTS] Failed to send file ${filePath}: ${e}`);
197
+ }
198
+ }
199
+ }
200
+ }
201
+ // Feed event to buddy controller for context capture + reactions
202
+ state.buddyController?.handleEvent(event);
203
+ break;
204
+ }
205
+ case "tasks_update": {
206
+ state.tasks = event.tasks || [];
207
+ updateStatus(state);
208
+ break;
209
+ }
210
+ case "background_agent_start": {
211
+ const { agentId, agentType, taskSummary } = event;
212
+ state.backgroundAgents.set(agentId, {
213
+ agentId,
214
+ agentType,
215
+ taskSummary,
216
+ startTime: Date.now(),
217
+ });
218
+ updateStatus(state);
219
+ break;
220
+ }
221
+ case "background_agent_end": {
222
+ const { agentId } = event;
223
+ state.backgroundAgents.delete(agentId);
224
+ // Background agents completing does not end the parent's turn.
225
+ // Only agent_end sets done — same as TUI behavior.
226
+ updateStatus(state);
227
+ break;
228
+ }
229
+ case "auto_compaction_start": {
230
+ updateStatusText(state, "🗜 _Compacting context..._");
231
+ break;
232
+ }
233
+ case "auto_compaction_end": {
234
+ const result = event.result;
235
+ if (result) {
236
+ const before = result.tokensBefore || 0;
237
+ const msg = `🗜 Context compacted (was ${Math.round(before / 1000)}k tokens)`;
238
+ send(msg);
239
+ }
240
+ break;
241
+ }
242
+ // =====================================================================
243
+ // Auto-retry — prevents agent_end from marking done during retries
244
+ // =====================================================================
245
+ case "auto_retry_start": {
246
+ const { attempt, maxAttempts, delayMs, errorMessage } = event;
247
+ state.retryInProgress = true;
248
+ state.pendingRetry = false; // Layer 1 has taken over from Layer 2
249
+ state.retryAttempt = attempt;
250
+ const delaySec = Math.round(delayMs / 1000);
251
+ const shortErr = errorMessage?.length > 80 ? `${errorMessage.slice(0, 80)}â€Ļ` : errorMessage;
252
+ updateStatusText(state, `🔄 _Retrying (${attempt}/${maxAttempts}) in ${delaySec}s — ${shortErr || "error"}_`);
253
+ break;
254
+ }
255
+ case "auto_retry_end": {
256
+ const { success, attempt, finalError } = event;
257
+ state.retryInProgress = false;
258
+ state.retryAttempt = 0;
259
+ if (!success && finalError) {
260
+ // Max retries exhausted — show final error
261
+ send(`❌ _Retry failed (${attempt} attempts):_ ${finalError}`, true);
262
+ }
263
+ // On success, the retry's agent_start/agent_end cycle will handle display normally
264
+ break;
265
+ }
266
+ case "agent_end": {
267
+ // Flush any remaining tools
268
+ if (state.toolsSinceText.length > 0) {
269
+ const summary = `📋 *${state.toolsSinceText.length} tools*:\n${state.toolsSinceText.join("\n")}`;
270
+ send(summary, true);
271
+ state.toolsSinceText = [];
272
+ }
273
+ // Check for error in agent_end messages
274
+ const errorMsg = event.messages?.find((m) => m.stopReason === "error" || m.stopReason === "aborted");
275
+ // Layer 2 (defensive): If this error looks retryable and we're not already
276
+ // tracking a retry via Layer 1, don't mark done — the core will auto-retry
277
+ // and emit a new agent_start/agent_end cycle.
278
+ const errorIsRetryable = errorMsg?.errorMessage && isRetryableError(errorMsg.errorMessage);
279
+ if (errorMsg?.errorMessage) {
280
+ // Suppress the scary error message during retry — user already saw the
281
+ // auto_retry_start status. Only show the error if retry tracking missed it
282
+ // (defensive: shouldn't happen, but better than silence).
283
+ if (!state.retryInProgress && !errorIsRetryable) {
284
+ const provider = errorMsg.provider ? `${errorMsg.provider}/${errorMsg.model}` : "";
285
+ const prefix = provider ? `${provider}: ` : "";
286
+ const errLower = errorMsg.errorMessage.toLowerCase();
287
+ const hint = errLower.includes("connection") || errLower.includes("timeout") || errLower.includes("network")
288
+ ? "\n_Provider may be down — try /model to switch._"
289
+ : "";
290
+ send(`❌ ${prefix}${errorMsg.errorMessage}${hint}`, true);
291
+ }
292
+ }
293
+ else if (state.textBlocks.length === 0 && state.backgroundAgents.size === 0) {
294
+ // Only show "(No response)" when truly done — not between agent cycles
295
+ if (!state.retryInProgress && !errorIsRetryable) {
296
+ send("(No response)");
297
+ }
298
+ }
299
+ // Feed event to buddy controller for context capture + reactions
300
+ state.buddyController?.handleEvent(event);
301
+ // Don't mark done if auto-retry is in progress (Layer 1) or the error
302
+ // looks retryable (Layer 2 — defensive catch in case events were missed).
303
+ // The core will emit a new agent_start/agent_end cycle for the retry.
304
+ if (state.retryInProgress || errorIsRetryable) {
305
+ // Signal that a retry is expected — the completion check in
306
+ // ensureSubscribed needs this because it runs in the eventChain
307
+ // BEFORE auto_retry_start has been processed.
308
+ if (errorIsRetryable)
309
+ state.pendingRetry = true;
310
+ // Reset per-cycle state for the next agent loop
311
+ state.textBlocks = [];
312
+ state.toolCount = 0;
313
+ break;
314
+ }
315
+ // If background agents are still running, keep the subscription alive
316
+ // and reset per-cycle state for the next agent loop
317
+ if (state.backgroundAgents.size > 0) {
318
+ state.textBlocks = [];
319
+ state.toolCount = 0;
320
+ break;
321
+ }
322
+ // Delete ephemeral status before signaling done
323
+ if (state.statusMessageId) {
324
+ await state.editor.flush(state.chatId, state.statusMessageId);
325
+ await safeDelete(api, state.chatId, state.statusMessageId);
326
+ state.statusMessageId = null;
327
+ }
328
+ // Clean up editor
329
+ state.editor.clear();
330
+ // Signal done AFTER cleanup — waitForCompletion checks this flag,
331
+ // so setting it last ensures status message is deleted before DONE is sent
332
+ state.done = true;
333
+ break;
334
+ }
335
+ // Handle error responses that leak through RPC (async prompt errors)
336
+ case "response": {
337
+ const resp = event;
338
+ if (!resp.success && resp.error) {
339
+ send(`❌ ${resp.error}`, true);
340
+ }
341
+ break;
342
+ }
343
+ }
344
+ }
345
+ /**
346
+ * Build and push a status update to the ephemeral message.
347
+ */
348
+ function updateStatus(state) {
349
+ if (!state.statusMessageId)
350
+ return;
351
+ const parts = [];
352
+ // Tool count header
353
+ if (state.toolCount > 0) {
354
+ parts.push(`🔧 *Tool ${state.toolCount}*`);
355
+ }
356
+ // Task list
357
+ if (state.tasks.length > 0) {
358
+ parts.push(formatTaskList(state.tasks));
359
+ }
360
+ // Background agents
361
+ if (state.backgroundAgents.size > 0) {
362
+ for (const agent of state.backgroundAgents.values()) {
363
+ parts.push(`🤖 *${agent.agentType}*: ${agent.taskSummary.slice(0, 200)}`);
364
+ }
365
+ }
366
+ // Recent tools (last 5)
367
+ if (state.toolsSinceText.length > 0) {
368
+ const recent = state.toolsSinceText.slice(-5);
369
+ parts.push(recent.join("\n\n"));
370
+ }
371
+ if (parts.length === 0)
372
+ return;
373
+ const text = parts.join("\n\n").slice(0, 4000);
374
+ state.editor.edit(state.chatId, state.statusMessageId, text);
375
+ }
376
+ function updateStatusText(state, text) {
377
+ if (!state.statusMessageId)
378
+ return;
379
+ state.editor.edit(state.chatId, state.statusMessageId, text);
380
+ }
381
+ //# sourceMappingURL=events.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.js","sourceRoot":"","sources":["../../src/handlers/events.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAErC,OAAO,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AAEnC,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,eAAe,EAAE,GAAG,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAavE,+DAA+D;AAC/D,MAAM,UAAU,GAA2B;IAC1C,IAAI,EAAE,MAAG;IACT,IAAI,EAAE,MAAG;IACT,IAAI,EAAE,QAAI;IACV,KAAK,EAAE,MAAG;IACV,IAAI,EAAE,MAAG;IACT,IAAI,EAAE,MAAG;IACT,EAAE,EAAE,MAAG;IACP,UAAU,EAAE,MAAG;IACf,SAAS,EAAE,MAAG;IACd,QAAQ,EAAE,MAAG;IACb,YAAY,EAAE,MAAG;IACjB,KAAK,EAAE,KAAG;CACV,CAAC;AAEF,SAAS,SAAS,CAAC,IAAY,EAAU;IACxC,OAAO,UAAU,CAAC,IAAI,CAAC,IAAI,MAAG,CAAC;AAAA,CAC/B;AAED,qCAAqC;AACrC,SAAS,UAAU,CAAC,IAAY,EAAE,IAAyB,EAAU;IACpE,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;IAC9B,QAAQ,IAAI,EAAE,CAAC;QACd,KAAK,MAAM,EAAE,CAAC;YACb,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,IAAI,EAAE,CAAC;YAC/B,OAAO,GAAG,KAAK,cAAc,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,IAAI,CAAC;QACpD,CAAC;QACD,KAAK,MAAM;YACV,OAAO,GAAG,KAAK,cAAc,IAAI,CAAC,IAAI,IAAI,GAAG,IAAI,CAAC;QACnD,KAAK,MAAM;YACV,OAAO,GAAG,KAAK,cAAc,IAAI,CAAC,IAAI,IAAI,GAAG,IAAI,CAAC;QACnD,KAAK,OAAO;YACX,OAAO,GAAG,KAAK,eAAe,IAAI,CAAC,IAAI,IAAI,GAAG,IAAI,CAAC;QACpD,KAAK,MAAM;YACV,OAAO,GAAG,KAAK,cAAc,IAAI,CAAC,OAAO,IAAI,GAAG,IAAI,CAAC;QACtD,KAAK,MAAM;YACV,OAAO,GAAG,KAAK,cAAc,IAAI,CAAC,OAAO,IAAI,GAAG,IAAI,CAAC;QACtD,KAAK,IAAI;YACR,OAAO,GAAG,KAAK,YAAY,IAAI,CAAC,IAAI,IAAI,GAAG,IAAI,CAAC;QACjD,KAAK,YAAY;YAChB,OAAO,GAAG,KAAK,oBAAoB,IAAI,CAAC,KAAK,IAAI,GAAG,EAAE,CAAC;QACxD,KAAK,WAAW;YACf,OAAO,GAAG,KAAK,mBAAmB,CAAC,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC;QACpE,KAAK,UAAU;YACd,OAAO,GAAG,KAAK,gBAAgB,IAAI,CAAC,KAAK,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC;QACnH,KAAK,OAAO;YACX,OAAO,GAAG,KAAK,aAAa,IAAI,CAAC,KAAK,IAAI,GAAG,EAAE,CAAC;QACjD;YACC,OAAO,GAAG,KAAK,KAAK,IAAI,GAAG,CAAC;IAC9B,CAAC;AAAA,CACD;AAED,oCAAoC;AACpC,SAAS,cAAc,CAAC,KAA2D,EAAU;IAC5F,IAAI,CAAC,KAAK,CAAC,MAAM;QAAE,OAAO,uBAAoB,CAAC;IAC/C,MAAM,KAAK,GAAG,CAAC,eAAY,CAAC,CAAC;IAC7B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QAC1B,IAAI,IAAI,CAAC,MAAM,KAAK,WAAW;YAAE,KAAK,CAAC,IAAI,CAAC,SAAO,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;aAC5D,IAAI,IAAI,CAAC,MAAM,KAAK,aAAa;YAAE,KAAK,CAAC,IAAI,CAAC,UAAO,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;;YACnE,KAAK,CAAC,IAAI,CAAC,SAAO,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;IACtC,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAAA,CACxB;AAiCD;;;GAGG;AACH,MAAM,uBAAuB,GAC5B,gUAAgU,CAAC;AAElU,SAAS,gBAAgB,CAAC,YAAoB,EAAW;IACxD,OAAO,uBAAuB,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;AAAA,CAClD;AAED;;GAEG;AACH,MAAM,UAAU,kBAAkB,CACjC,GAAQ,EACR,MAAc,EACd,SAAiB,EACjB,eAA8B,EACV;IACpB,OAAO;QACN,MAAM;QACN,SAAS;QACT,eAAe;QACf,cAAc,EAAE,EAAE;QAClB,SAAS,EAAE,CAAC;QACZ,UAAU,EAAE,EAAE;QACd,KAAK,EAAE,EAAE;QACT,gBAAgB,EAAE,IAAI,GAAG,EAAE;QAC3B,IAAI,EAAE,KAAK;QACX,MAAM,EAAE,IAAI,eAAe,CAAC,GAAG,CAAC;QAChC,eAAe,EAAE,KAAK;QACtB,YAAY,EAAE,KAAK;QACnB,YAAY,EAAE,CAAC;KACf,CAAC;AAAA,CACF;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACrC,IAAY,EACZ,GAAQ,EACR,KAAwB,EACxB,KAAe,EACC;IAChB,QAAQ,KAAK,CAAC,IAAI,EAAE,CAAC;QACpB,KAAK,sBAAsB,EAAE,CAAC;YAC7B,MAAM,IAAI,GAAG,KAAK,CAAC,QAAQ,IAAI,GAAG,CAAC;YACnC,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,IAAI,EAAE,CAAC;YAC9B,KAAK,CAAC,SAAS,EAAE,CAAC;YAElB,uFAAqF;YACrF,IAAI,IAAI,KAAK,cAAc,EAAE,CAAC;gBAC7B,MAAM,OAAO,GAAG,UAAU,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;gBACvC,KAAK,CAAC,cAAc,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YACpC,CAAC;YAED,iDAAiD;YACjD,YAAY,CAAC,KAAK,CAAC,CAAC;YACpB,MAAM;QACP,CAAC;QAED,KAAK,oBAAoB,EAAE,CAAC;YAC3B,uEAAuE;YACvE,KAAK,CAAC,eAAe,EAAE,WAAW,CAAC,KAAK,CAAC,CAAC;YAC1C,MAAM;QACP,CAAC;QAED,KAAK,aAAa,EAAE,CAAC;YACpB,MAAM,GAAG,GAAG,KAAK,CAAC,OAAO,CAAC;YAE1B,sEAAoE;YACpE,iEAAiE;YACjE,IAAI,GAAG,EAAE,IAAI,KAAK,YAAY,IAAI,GAAG,EAAE,QAAQ,KAAK,UAAU,EAAE,CAAC;gBAChE,MAAM,OAAO,GAAG,GAAG,EAAE,OAAO,CAAC;gBAC7B,IAAI,OAAO,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;oBACvC,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;wBAC7B,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,IAAI,KAAK,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,CAAC;4BACjD,IAAI,CAAC,4BAAyB,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,EAAE,IAAI,CAAC,CAAC;wBAC1D,CAAC;oBACF,CAAC;gBACF,CAAC;gBACD,MAAM;YACP,CAAC;YAED,oEAAkE;YAClE,iEAAiE;YACjE,qDAAqD;YACrD,IAAI,GAAG,EAAE,IAAI,KAAK,MAAM,EAAE,CAAC;gBAC1B,MAAM,OAAO,GAAG,GAAG,EAAE,OAAO,CAAC;gBAC7B,IAAI,OAAO,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;oBACvC,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;wBAC7B,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,IAAI,KAAK,CAAC,IAAI,EAAE,QAAQ,CAAC,6BAA6B,CAAC,EAAE,CAAC;4BAClF,2CAA2C;4BAC3C,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,KAAK,CAC7B,0EAA0E,CAC1E,CAAC;4BACF,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,CAAC;gCACxB,IAAI,CAAC,sCAAmC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,EAAE,IAAI,CAAC,CAAC;4BAClE,CAAC;wBACF,CAAC;oBACF,CAAC;gBACF,CAAC;gBACD,MAAM;YACP,CAAC;YAED,2EAAyE;YACzE,IAAI,GAAG,EAAE,IAAI,KAAK,WAAW;gBAAE,MAAM;YACrC,MAAM,OAAO,GAAG,GAAG,EAAE,OAAO,CAAC;YAC7B,IAAI,CAAC,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC;gBAAE,MAAM;YAE/C,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;gBAC7B,8CAA8C;gBAC9C,IAAI,KAAK,CAAC,IAAI,KAAK,UAAU,IAAI,KAAK,CAAC,QAAQ,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC;oBAC5E,MAAM,QAAQ,GAAG,KAAK,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;oBACvC,IAAI,CAAC,SAAM,QAAQ,GAAG,EAAE,IAAI,CAAC,CAAC;gBAC/B,CAAC;gBAED,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,IAAI,KAAK,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,CAAC;oBACjD,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;oBAE/B,+CAA+C;oBAC/C,IAAI,KAAK,CAAC,cAAc,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;wBACrC,MAAM,OAAO,GAAG,SAAM,KAAK,CAAC,cAAc,CAAC,MAAM,aAAa,KAAK,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;wBAChG,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;wBACpB,KAAK,CAAC,cAAc,GAAG,EAAE,CAAC;oBAC3B,CAAC;oBAED,uCAAuC;oBACvC,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;oBAE5B,8BAA8B;oBAC9B,MAAM,CAAC,SAAS,EAAE,SAAS,CAAC,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAC;oBACtD,IAAI,SAAS,EAAE,CAAC;wBACf,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;oBACvB,CAAC;oBAED,iEAA+D;oBAC/D,8CAA8C;oBAC9C,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;wBAClC,IAAI,CAAC;4BACJ,IAAI,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;gCAC1B,MAAM,GAAG,CAAC,YAAY,CAAC,KAAK,CAAC,MAAM,EAAE,IAAI,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC;4BAC/D,CAAC;wBACF,CAAC;wBAAC,OAAO,CAAC,EAAE,CAAC;4BACZ,GAAG,CAAC,gCAAgC,QAAQ,KAAK,CAAC,EAAE,CAAC,CAAC;wBACvD,CAAC;oBACF,CAAC;gBACF,CAAC;YACF,CAAC;YACD,iEAAiE;YACjE,KAAK,CAAC,eAAe,EAAE,WAAW,CAAC,KAAK,CAAC,CAAC;YAC1C,MAAM;QACP,CAAC;QAED,KAAK,cAAc,EAAE,CAAC;YACrB,KAAK,CAAC,KAAK,GAAI,KAAa,CAAC,KAAK,IAAI,EAAE,CAAC;YACzC,YAAY,CAAC,KAAK,CAAC,CAAC;YACpB,MAAM;QACP,CAAC;QAED,KAAK,wBAAwB,EAAE,CAAC;YAC/B,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,GAAG,KAAY,CAAC;YACzD,KAAK,CAAC,gBAAgB,CAAC,GAAG,CAAC,OAAO,EAAE;gBACnC,OAAO;gBACP,SAAS;gBACT,WAAW;gBACX,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;aACrB,CAAC,CAAC;YACH,YAAY,CAAC,KAAK,CAAC,CAAC;YACpB,MAAM;QACP,CAAC;QAED,KAAK,sBAAsB,EAAE,CAAC;YAC7B,MAAM,EAAE,OAAO,EAAE,GAAG,KAAY,CAAC;YACjC,KAAK,CAAC,gBAAgB,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;YACvC,+DAA+D;YAC/D,qDAAmD;YACnD,YAAY,CAAC,KAAK,CAAC,CAAC;YACpB,MAAM;QACP,CAAC;QAED,KAAK,uBAAuB,EAAE,CAAC;YAC9B,gBAAgB,CAAC,KAAK,EAAE,8BAA2B,CAAC,CAAC;YACrD,MAAM;QACP,CAAC;QAED,KAAK,qBAAqB,EAAE,CAAC;YAC5B,MAAM,MAAM,GAAI,KAAa,CAAC,MAAM,CAAC;YACrC,IAAI,MAAM,EAAE,CAAC;gBACZ,MAAM,MAAM,GAAG,MAAM,CAAC,YAAY,IAAI,CAAC,CAAC;gBACxC,MAAM,GAAG,GAAG,+BAA4B,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,WAAW,CAAC;gBAC7E,IAAI,CAAC,GAAG,CAAC,CAAC;YACX,CAAC;YACD,MAAM;QACP,CAAC;QAED,wEAAwE;QACxE,qEAAmE;QACnE,wEAAwE;QAExE,KAAK,kBAAkB,EAAE,CAAC;YACzB,MAAM,EAAE,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,YAAY,EAAE,GAAG,KAAY,CAAC;YACrE,KAAK,CAAC,eAAe,GAAG,IAAI,CAAC;YAC7B,KAAK,CAAC,YAAY,GAAG,KAAK,CAAC,CAAC,sCAAsC;YAClE,KAAK,CAAC,YAAY,GAAG,OAAO,CAAC;YAC7B,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC;YAC5C,MAAM,QAAQ,GAAG,YAAY,EAAE,MAAM,GAAG,EAAE,CAAC,CAAC,CAAC,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,KAAG,CAAC,CAAC,CAAC,YAAY,CAAC;YAC5F,gBAAgB,CAAC,KAAK,EAAE,mBAAgB,OAAO,IAAI,WAAW,QAAQ,QAAQ,SAAO,QAAQ,IAAI,OAAO,GAAG,CAAC,CAAC;YAC7G,MAAM;QACP,CAAC;QAED,KAAK,gBAAgB,EAAE,CAAC;YACvB,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,GAAG,KAAY,CAAC;YACtD,KAAK,CAAC,eAAe,GAAG,KAAK,CAAC;YAC9B,KAAK,CAAC,YAAY,GAAG,CAAC,CAAC;YACvB,IAAI,CAAC,OAAO,IAAI,UAAU,EAAE,CAAC;gBAC5B,6CAA2C;gBAC3C,IAAI,CAAC,sBAAoB,OAAO,gBAAgB,UAAU,EAAE,EAAE,IAAI,CAAC,CAAC;YACrE,CAAC;YACD,mFAAmF;YACnF,MAAM;QACP,CAAC;QAED,KAAK,WAAW,EAAE,CAAC;YAClB,4BAA4B;YAC5B,IAAI,KAAK,CAAC,cAAc,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACrC,MAAM,OAAO,GAAG,SAAM,KAAK,CAAC,cAAc,CAAC,MAAM,aAAa,KAAK,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;gBAChG,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;gBACpB,KAAK,CAAC,cAAc,GAAG,EAAE,CAAC;YAC3B,CAAC;YAED,wCAAwC;YACxC,MAAM,QAAQ,GAAI,KAAK,CAAC,QAAkB,EAAE,IAAI,CAC/C,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,KAAK,OAAO,IAAI,CAAC,CAAC,UAAU,KAAK,SAAS,CAClE,CAAC;YAEF,2EAA2E;YAC3E,6EAA2E;YAC3E,8CAA8C;YAC9C,MAAM,gBAAgB,GAAG,QAAQ,EAAE,YAAY,IAAI,gBAAgB,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC;YAE3F,IAAI,QAAQ,EAAE,YAAY,EAAE,CAAC;gBAC5B,yEAAuE;gBACvE,2EAA2E;gBAC3E,0DAA0D;gBAC1D,IAAI,CAAC,KAAK,CAAC,eAAe,IAAI,CAAC,gBAAgB,EAAE,CAAC;oBACjD,MAAM,QAAQ,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,QAAQ,IAAI,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;oBACnF,MAAM,MAAM,GAAG,QAAQ,CAAC,CAAC,CAAC,GAAG,QAAQ,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;oBAC/C,MAAM,QAAQ,GAAG,QAAQ,CAAC,YAAY,CAAC,WAAW,EAAE,CAAC;oBACrD,MAAM,IAAI,GACT,QAAQ,CAAC,QAAQ,CAAC,YAAY,CAAC,IAAI,QAAQ,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,QAAQ,CAAC,QAAQ,CAAC,SAAS,CAAC;wBAC9F,CAAC,CAAC,oDAAkD;wBACpD,CAAC,CAAC,EAAE,CAAC;oBACP,IAAI,CAAC,OAAK,MAAM,GAAG,QAAQ,CAAC,YAAY,GAAG,IAAI,EAAE,EAAE,IAAI,CAAC,CAAC;gBAC1D,CAAC;YACF,CAAC;iBAAM,IAAI,KAAK,CAAC,UAAU,CAAC,MAAM,KAAK,CAAC,IAAI,KAAK,CAAC,gBAAgB,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;gBAC/E,yEAAuE;gBACvE,IAAI,CAAC,KAAK,CAAC,eAAe,IAAI,CAAC,gBAAgB,EAAE,CAAC;oBACjD,IAAI,CAAC,eAAe,CAAC,CAAC;gBACvB,CAAC;YACF,CAAC;YAED,iEAAiE;YACjE,KAAK,CAAC,eAAe,EAAE,WAAW,CAAC,KAAK,CAAC,CAAC;YAE1C,sEAAsE;YACtE,4EAA0E;YAC1E,sEAAsE;YACtE,IAAI,KAAK,CAAC,eAAe,IAAI,gBAAgB,EAAE,CAAC;gBAC/C,8DAA4D;gBAC5D,gEAAgE;gBAChE,8CAA8C;gBAC9C,IAAI,gBAAgB;oBAAE,KAAK,CAAC,YAAY,GAAG,IAAI,CAAC;gBAChD,gDAAgD;gBAChD,KAAK,CAAC,UAAU,GAAG,EAAE,CAAC;gBACtB,KAAK,CAAC,SAAS,GAAG,CAAC,CAAC;gBACpB,MAAM;YACP,CAAC;YAED,sEAAsE;YACtE,oDAAoD;YACpD,IAAI,KAAK,CAAC,gBAAgB,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;gBACrC,KAAK,CAAC,UAAU,GAAG,EAAE,CAAC;gBACtB,KAAK,CAAC,SAAS,GAAG,CAAC,CAAC;gBACpB,MAAM;YACP,CAAC;YAED,gDAAgD;YAChD,IAAI,KAAK,CAAC,eAAe,EAAE,CAAC;gBAC3B,MAAM,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,eAAe,CAAC,CAAC;gBAC9D,MAAM,UAAU,CAAC,GAAG,EAAE,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,eAAe,CAAC,CAAC;gBAC3D,KAAK,CAAC,eAAe,GAAG,IAAI,CAAC;YAC9B,CAAC;YAED,kBAAkB;YAClB,KAAK,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;YAErB,oEAAkE;YAClE,2EAA2E;YAC3E,KAAK,CAAC,IAAI,GAAG,IAAI,CAAC;YAClB,MAAM;QACP,CAAC;QAED,qEAAqE;QACrE,KAAK,UAAU,EAAE,CAAC;YACjB,MAAM,IAAI,GAAG,KAAY,CAAC;YAC1B,IAAI,CAAC,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;gBACjC,IAAI,CAAC,OAAK,IAAI,CAAC,KAAK,EAAE,EAAE,IAAI,CAAC,CAAC;YAC/B,CAAC;YACD,MAAM;QACP,CAAC;IACF,CAAC;AAAA,CACD;AAED;;GAEG;AACH,SAAS,YAAY,CAAC,KAAwB,EAAQ;IACrD,IAAI,CAAC,KAAK,CAAC,eAAe;QAAE,OAAO;IAEnC,MAAM,KAAK,GAAa,EAAE,CAAC;IAE3B,oBAAoB;IACpB,IAAI,KAAK,CAAC,SAAS,GAAG,CAAC,EAAE,CAAC;QACzB,KAAK,CAAC,IAAI,CAAC,cAAW,KAAK,CAAC,SAAS,GAAG,CAAC,CAAC;IAC3C,CAAC;IAED,YAAY;IACZ,IAAI,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC5B,KAAK,CAAC,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC;IACzC,CAAC;IAED,oBAAoB;IACpB,IAAI,KAAK,CAAC,gBAAgB,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;QACrC,KAAK,MAAM,KAAK,IAAI,KAAK,CAAC,gBAAgB,CAAC,MAAM,EAAE,EAAE,CAAC;YACrD,KAAK,CAAC,IAAI,CAAC,SAAM,KAAK,CAAC,SAAS,MAAM,KAAK,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;QAC1E,CAAC;IACF,CAAC;IAED,wBAAwB;IACxB,IAAI,KAAK,CAAC,cAAc,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACrC,MAAM,MAAM,GAAG,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;QAC9C,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;IACjC,CAAC;IAED,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO;IAE/B,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;IAC/C,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,eAAe,EAAE,IAAI,CAAC,CAAC;AAAA,CAC7D;AAED,SAAS,gBAAgB,CAAC,KAAwB,EAAE,IAAY,EAAQ;IACvE,IAAI,CAAC,KAAK,CAAC,eAAe;QAAE,OAAO;IACnC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,eAAe,EAAE,IAAI,CAAC,CAAC;AAAA,CAC7D","sourcesContent":["/**\n * Event display — translates RPC agent events into Telegram messages.\n *\n * Manages an ephemeral status message that shows tool use, task lists,\n * and subagent activity. Text from the agent is sent as permanent messages.\n */\n\nimport { existsSync } from \"node:fs\";\nimport type { Api } from \"grammy\";\nimport { InputFile } from \"grammy\";\nimport type { TrackedAgent } from \"../types.js\";\nimport { extractSendFiles } from \"../util/files.js\";\nimport { DebouncedEditor, log, safeDelete } from \"../util/telegram.js\";\n\n/** Callback to queue a message for delivery — never blocks the event chain */\nexport type SendFn = (text: string, long?: boolean) => void;\n\n/**\n * RPC events include both core AgentEvent and session-specific events\n * (tasks_update, background_agent_*, auto_compaction_*).\n * We type loosely here since the RPC client types onEvent as AgentEvent\n * but actually forwards all AgentSessionEvent types.\n */\ntype RpcEvent = { type: string; [key: string]: any };\n\n// Tool emoji mapping (tool names are lowercase in definitions)\nconst TOOL_EMOJI: Record<string, string> = {\n\tbash: \"🔧\",\n\tread: \"📖\",\n\tedit: \"âœī¸\",\n\twrite: \"📝\",\n\tgrep: \"🔎\",\n\tfind: \"🔍\",\n\tls: \"📂\",\n\tweb_search: \"🌐\",\n\tweb_fetch: \"🌐\",\n\tsubagent: \"🤖\",\n\ttasks_update: \"📋\",\n\tskill: \"⚡\",\n};\n\nfunction toolEmoji(name: string): string {\n\treturn TOOL_EMOJI[name] || \"🔧\";\n}\n\n/** Format a tool call for display */\nfunction formatTool(name: string, args: Record<string, any>): string {\n\tconst emoji = toolEmoji(name);\n\tswitch (name) {\n\t\tcase \"bash\": {\n\t\t\tconst cmd = args.command || \"\";\n\t\t\treturn `${emoji} *bash*\\n\\`${cmd.slice(0, 500)}\\``;\n\t\t}\n\t\tcase \"read\":\n\t\t\treturn `${emoji} *read*: \\`${args.path || \"?\"}\\``;\n\t\tcase \"edit\":\n\t\t\treturn `${emoji} *edit*: \\`${args.path || \"?\"}\\``;\n\t\tcase \"write\":\n\t\t\treturn `${emoji} *write*: \\`${args.path || \"?\"}\\``;\n\t\tcase \"grep\":\n\t\t\treturn `${emoji} *grep*: \\`${args.pattern || \"?\"}\\``;\n\t\tcase \"find\":\n\t\t\treturn `${emoji} *find*: \\`${args.pattern || \"?\"}\\``;\n\t\tcase \"ls\":\n\t\t\treturn `${emoji} *ls*: \\`${args.path || \".\"}\\``;\n\t\tcase \"web_search\":\n\t\t\treturn `${emoji} *web\\\\_search*: ${args.query || \"?\"}`;\n\t\tcase \"web_fetch\":\n\t\t\treturn `${emoji} *web\\\\_fetch*: ${(args.url || \"?\").slice(0, 80)}`;\n\t\tcase \"subagent\":\n\t\t\treturn `${emoji} *subagent* (${args.agent || \"?\"}): ${(args.task || args.tasks?.[0]?.task || \"?\").slice(0, 200)}`;\n\t\tcase \"skill\":\n\t\t\treturn `${emoji} *skill*: ${args.skill || \"?\"}`;\n\t\tdefault:\n\t\t\treturn `${emoji} *${name}*`;\n\t}\n}\n\n/** Format task list as checklist */\nfunction formatTaskList(tasks: Array<{ id: string; title: string; status: string }>): string {\n\tif (!tasks.length) return \"📋 *Tasks*: (empty)\";\n\tconst lines = [\"📋 *Tasks*:\"];\n\tfor (const task of tasks) {\n\t\tif (task.status === \"completed\") lines.push(` ✅ ${task.title}`);\n\t\telse if (task.status === \"in_progress\") lines.push(` 🔄 ${task.title}`);\n\t\telse lines.push(` âŦœ ${task.title}`);\n\t}\n\treturn lines.join(\"\\n\");\n}\n\nexport interface EventDisplayState {\n\t/** Chat ID to send messages to */\n\tchatId: number;\n\t/** Message ID to reply to */\n\treplyToId: number;\n\t/** Ephemeral status message ID (edited in-place) */\n\tstatusMessageId: number | null;\n\t/** Tool messages accumulated since last text */\n\ttoolsSinceText: string[];\n\t/** Total tool count */\n\ttoolCount: number;\n\t/** All text blocks received */\n\ttextBlocks: string[];\n\t/** Current task list */\n\ttasks: Array<{ id: string; title: string; status: string }>;\n\t/** Background agents */\n\tbackgroundAgents: Map<string, TrackedAgent>;\n\t/** Whether agent has finished */\n\tdone: boolean;\n\t/** Debounced editor instance */\n\teditor: DebouncedEditor;\n\t/** Whether auto-retry is in progress (Layer 1: reactive — set by auto_retry_start) */\n\tretryInProgress: boolean;\n\t/** Whether a retry is expected (Layer 2: predictive — set by agent_end when error looks retryable) */\n\tpendingRetry: boolean;\n\t/** Current retry attempt number for display */\n\tretryAttempt: number;\n\t/** Buddy controller — receives agent events for context + reactions */\n\tbuddyController?: any;\n}\n\n/**\n * Check if an error message looks retryable (overloaded, rate limit, server errors).\n * Mirrors the core's _isRetryableError check as a defensive Layer 2.\n */\nconst RETRYABLE_ERROR_PATTERN =\n\t/overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|timed? out|timeout|terminated|retry delay/i;\n\nfunction isRetryableError(errorMessage: string): boolean {\n\treturn RETRYABLE_ERROR_PATTERN.test(errorMessage);\n}\n\n/**\n * Create a fresh event display state for a new agent run.\n */\nexport function createEventDisplay(\n\tapi: Api,\n\tchatId: number,\n\treplyToId: number,\n\tstatusMessageId: number | null,\n): EventDisplayState {\n\treturn {\n\t\tchatId,\n\t\treplyToId,\n\t\tstatusMessageId,\n\t\ttoolsSinceText: [],\n\t\ttoolCount: 0,\n\t\ttextBlocks: [],\n\t\ttasks: [],\n\t\tbackgroundAgents: new Map(),\n\t\tdone: false,\n\t\teditor: new DebouncedEditor(api),\n\t\tretryInProgress: false,\n\t\tpendingRetry: false,\n\t\tretryAttempt: 0,\n\t};\n}\n\n/**\n * Process an agent event and update the display.\n */\nexport async function handleAgentEvent(\n\tsend: SendFn,\n\tapi: Api,\n\tstate: EventDisplayState,\n\tevent: RpcEvent,\n): Promise<void> {\n\tswitch (event.type) {\n\t\tcase \"tool_execution_start\": {\n\t\t\tconst name = event.toolName || \"?\";\n\t\t\tconst args = event.args || {};\n\t\t\tstate.toolCount++;\n\n\t\t\t// tasks_update is shown via the separate tasks_update event — skip from tool summary\n\t\t\tif (name !== \"tasks_update\") {\n\t\t\t\tconst toolMsg = formatTool(name, args);\n\t\t\t\tstate.toolsSinceText.push(toolMsg);\n\t\t\t}\n\n\t\t\t// Update status with tool count and recent tools\n\t\t\tupdateStatus(state);\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"tool_execution_end\": {\n\t\t\t// Feed event to buddy controller for context capture + error reactions\n\t\t\tstate.buddyController?.handleEvent(event);\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"message_end\": {\n\t\t\tconst msg = event.message;\n\n\t\t\t// Show subagent results — the parent agent references these but the\n\t\t\t// Telegram user can't see them otherwise. Send the full content.\n\t\t\tif (msg?.role === \"toolResult\" && msg?.toolName === \"subagent\") {\n\t\t\t\tconst content = msg?.content;\n\t\t\t\tif (content && Array.isArray(content)) {\n\t\t\t\t\tfor (const block of content) {\n\t\t\t\t\t\tif (block.type === \"text\" && block.text?.trim()) {\n\t\t\t\t\t\t\tsend(`🤖 *Subagent result:*\\n${block.text.trim()}`, true);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\t// Show background agent completion results — these arrive as user\n\t\t\t// messages injected by agent-session.ts via prompt()/steer() and\n\t\t\t// contain the actual subagent output the model sees.\n\t\t\tif (msg?.role === \"user\") {\n\t\t\t\tconst content = msg?.content;\n\t\t\t\tif (content && Array.isArray(content)) {\n\t\t\t\t\tfor (const block of content) {\n\t\t\t\t\t\tif (block.type === \"text\" && block.text?.includes(\"<background-agent-complete>\")) {\n\t\t\t\t\t\t\t// Extract the content between the XML tags\n\t\t\t\t\t\t\tconst match = block.text.match(\n\t\t\t\t\t\t\t\t/<background-agent-complete>\\n?([\\s\\S]*?)\\n?<\\/background-agent-complete>/,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tif (match?.[1]?.trim()) {\n\t\t\t\t\t\t\t\tsend(`🤖 *Background agent complete:*\\n${match[1].trim()}`, true);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\t// Only display assistant messages — user messages are echoed back by RPC\n\t\t\tif (msg?.role !== \"assistant\") break;\n\t\t\tconst content = msg?.content;\n\t\t\tif (!content || !Array.isArray(content)) break;\n\n\t\t\tfor (const block of content) {\n\t\t\t\t// Display thinking blocks (collapsed summary)\n\t\t\t\tif (block.type === \"thinking\" && block.thinking?.trim() && !block.redacted) {\n\t\t\t\t\tconst thinking = block.thinking.trim();\n\t\t\t\t\tsend(`💭 _${thinking}_`, true);\n\t\t\t\t}\n\n\t\t\t\tif (block.type === \"text\" && block.text?.trim()) {\n\t\t\t\t\tconst text = block.text.trim();\n\n\t\t\t\t\t// Flush accumulated tools as permanent summary\n\t\t\t\t\tif (state.toolsSinceText.length > 0) {\n\t\t\t\t\t\tconst summary = `📋 *${state.toolsSinceText.length} tools*:\\n${state.toolsSinceText.join(\"\\n\")}`;\n\t\t\t\t\t\tsend(summary, true);\n\t\t\t\t\t\tstate.toolsSinceText = [];\n\t\t\t\t\t}\n\n\t\t\t\t\t// Send the text as a permanent message\n\t\t\t\t\tstate.textBlocks.push(text);\n\n\t\t\t\t\t// Check for file send markers\n\t\t\t\t\tconst [cleanText, filePaths] = extractSendFiles(text);\n\t\t\t\t\tif (cleanText) {\n\t\t\t\t\t\tsend(cleanText, true);\n\t\t\t\t\t}\n\n\t\t\t\t\t// Send any requested files (silently skip non-existent paths —\n\t\t\t\t\t// the pattern may appear in explanatory text)\n\t\t\t\t\tfor (const filePath of filePaths) {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tif (existsSync(filePath)) {\n\t\t\t\t\t\t\t\tawait api.sendDocument(state.chatId, new InputFile(filePath));\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} catch (e) {\n\t\t\t\t\t\t\tlog(`[EVENTS] Failed to send file ${filePath}: ${e}`);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Feed event to buddy controller for context capture + reactions\n\t\t\tstate.buddyController?.handleEvent(event);\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"tasks_update\": {\n\t\t\tstate.tasks = (event as any).tasks || [];\n\t\t\tupdateStatus(state);\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"background_agent_start\": {\n\t\t\tconst { agentId, agentType, taskSummary } = event as any;\n\t\t\tstate.backgroundAgents.set(agentId, {\n\t\t\t\tagentId,\n\t\t\t\tagentType,\n\t\t\t\ttaskSummary,\n\t\t\t\tstartTime: Date.now(),\n\t\t\t});\n\t\t\tupdateStatus(state);\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"background_agent_end\": {\n\t\t\tconst { agentId } = event as any;\n\t\t\tstate.backgroundAgents.delete(agentId);\n\t\t\t// Background agents completing does not end the parent's turn.\n\t\t\t// Only agent_end sets done — same as TUI behavior.\n\t\t\tupdateStatus(state);\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"auto_compaction_start\": {\n\t\t\tupdateStatusText(state, \"🗜 _Compacting context..._\");\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"auto_compaction_end\": {\n\t\t\tconst result = (event as any).result;\n\t\t\tif (result) {\n\t\t\t\tconst before = result.tokensBefore || 0;\n\t\t\t\tconst msg = `🗜 Context compacted (was ${Math.round(before / 1000)}k tokens)`;\n\t\t\t\tsend(msg);\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\n\t\t// =====================================================================\n\t\t// Auto-retry — prevents agent_end from marking done during retries\n\t\t// =====================================================================\n\n\t\tcase \"auto_retry_start\": {\n\t\t\tconst { attempt, maxAttempts, delayMs, errorMessage } = event as any;\n\t\t\tstate.retryInProgress = true;\n\t\t\tstate.pendingRetry = false; // Layer 1 has taken over from Layer 2\n\t\t\tstate.retryAttempt = attempt;\n\t\t\tconst delaySec = Math.round(delayMs / 1000);\n\t\t\tconst shortErr = errorMessage?.length > 80 ? `${errorMessage.slice(0, 80)}â€Ļ` : errorMessage;\n\t\t\tupdateStatusText(state, `🔄 _Retrying (${attempt}/${maxAttempts}) in ${delaySec}s — ${shortErr || \"error\"}_`);\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"auto_retry_end\": {\n\t\t\tconst { success, attempt, finalError } = event as any;\n\t\t\tstate.retryInProgress = false;\n\t\t\tstate.retryAttempt = 0;\n\t\t\tif (!success && finalError) {\n\t\t\t\t// Max retries exhausted — show final error\n\t\t\t\tsend(`❌ _Retry failed (${attempt} attempts):_ ${finalError}`, true);\n\t\t\t}\n\t\t\t// On success, the retry's agent_start/agent_end cycle will handle display normally\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"agent_end\": {\n\t\t\t// Flush any remaining tools\n\t\t\tif (state.toolsSinceText.length > 0) {\n\t\t\t\tconst summary = `📋 *${state.toolsSinceText.length} tools*:\\n${state.toolsSinceText.join(\"\\n\")}`;\n\t\t\t\tsend(summary, true);\n\t\t\t\tstate.toolsSinceText = [];\n\t\t\t}\n\n\t\t\t// Check for error in agent_end messages\n\t\t\tconst errorMsg = (event.messages as any[])?.find(\n\t\t\t\t(m: any) => m.stopReason === \"error\" || m.stopReason === \"aborted\",\n\t\t\t);\n\n\t\t\t// Layer 2 (defensive): If this error looks retryable and we're not already\n\t\t\t// tracking a retry via Layer 1, don't mark done — the core will auto-retry\n\t\t\t// and emit a new agent_start/agent_end cycle.\n\t\t\tconst errorIsRetryable = errorMsg?.errorMessage && isRetryableError(errorMsg.errorMessage);\n\n\t\t\tif (errorMsg?.errorMessage) {\n\t\t\t\t// Suppress the scary error message during retry — user already saw the\n\t\t\t\t// auto_retry_start status. Only show the error if retry tracking missed it\n\t\t\t\t// (defensive: shouldn't happen, but better than silence).\n\t\t\t\tif (!state.retryInProgress && !errorIsRetryable) {\n\t\t\t\t\tconst provider = errorMsg.provider ? `${errorMsg.provider}/${errorMsg.model}` : \"\";\n\t\t\t\t\tconst prefix = provider ? `${provider}: ` : \"\";\n\t\t\t\t\tconst errLower = errorMsg.errorMessage.toLowerCase();\n\t\t\t\t\tconst hint =\n\t\t\t\t\t\terrLower.includes(\"connection\") || errLower.includes(\"timeout\") || errLower.includes(\"network\")\n\t\t\t\t\t\t\t? \"\\n_Provider may be down — try /model to switch._\"\n\t\t\t\t\t\t\t: \"\";\n\t\t\t\t\tsend(`❌ ${prefix}${errorMsg.errorMessage}${hint}`, true);\n\t\t\t\t}\n\t\t\t} else if (state.textBlocks.length === 0 && state.backgroundAgents.size === 0) {\n\t\t\t\t// Only show \"(No response)\" when truly done — not between agent cycles\n\t\t\t\tif (!state.retryInProgress && !errorIsRetryable) {\n\t\t\t\t\tsend(\"(No response)\");\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Feed event to buddy controller for context capture + reactions\n\t\t\tstate.buddyController?.handleEvent(event);\n\n\t\t\t// Don't mark done if auto-retry is in progress (Layer 1) or the error\n\t\t\t// looks retryable (Layer 2 — defensive catch in case events were missed).\n\t\t\t// The core will emit a new agent_start/agent_end cycle for the retry.\n\t\t\tif (state.retryInProgress || errorIsRetryable) {\n\t\t\t\t// Signal that a retry is expected — the completion check in\n\t\t\t\t// ensureSubscribed needs this because it runs in the eventChain\n\t\t\t\t// BEFORE auto_retry_start has been processed.\n\t\t\t\tif (errorIsRetryable) state.pendingRetry = true;\n\t\t\t\t// Reset per-cycle state for the next agent loop\n\t\t\t\tstate.textBlocks = [];\n\t\t\t\tstate.toolCount = 0;\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\t// If background agents are still running, keep the subscription alive\n\t\t\t// and reset per-cycle state for the next agent loop\n\t\t\tif (state.backgroundAgents.size > 0) {\n\t\t\t\tstate.textBlocks = [];\n\t\t\t\tstate.toolCount = 0;\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\t// Delete ephemeral status before signaling done\n\t\t\tif (state.statusMessageId) {\n\t\t\t\tawait state.editor.flush(state.chatId, state.statusMessageId);\n\t\t\t\tawait safeDelete(api, state.chatId, state.statusMessageId);\n\t\t\t\tstate.statusMessageId = null;\n\t\t\t}\n\n\t\t\t// Clean up editor\n\t\t\tstate.editor.clear();\n\n\t\t\t// Signal done AFTER cleanup — waitForCompletion checks this flag,\n\t\t\t// so setting it last ensures status message is deleted before DONE is sent\n\t\t\tstate.done = true;\n\t\t\tbreak;\n\t\t}\n\n\t\t// Handle error responses that leak through RPC (async prompt errors)\n\t\tcase \"response\": {\n\t\t\tconst resp = event as any;\n\t\t\tif (!resp.success && resp.error) {\n\t\t\t\tsend(`❌ ${resp.error}`, true);\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t}\n}\n\n/**\n * Build and push a status update to the ephemeral message.\n */\nfunction updateStatus(state: EventDisplayState): void {\n\tif (!state.statusMessageId) return;\n\n\tconst parts: string[] = [];\n\n\t// Tool count header\n\tif (state.toolCount > 0) {\n\t\tparts.push(`🔧 *Tool ${state.toolCount}*`);\n\t}\n\n\t// Task list\n\tif (state.tasks.length > 0) {\n\t\tparts.push(formatTaskList(state.tasks));\n\t}\n\n\t// Background agents\n\tif (state.backgroundAgents.size > 0) {\n\t\tfor (const agent of state.backgroundAgents.values()) {\n\t\t\tparts.push(`🤖 *${agent.agentType}*: ${agent.taskSummary.slice(0, 200)}`);\n\t\t}\n\t}\n\n\t// Recent tools (last 5)\n\tif (state.toolsSinceText.length > 0) {\n\t\tconst recent = state.toolsSinceText.slice(-5);\n\t\tparts.push(recent.join(\"\\n\\n\"));\n\t}\n\n\tif (parts.length === 0) return;\n\n\tconst text = parts.join(\"\\n\\n\").slice(0, 4000);\n\tstate.editor.edit(state.chatId, state.statusMessageId, text);\n}\n\nfunction updateStatusText(state: EventDisplayState, text: string): void {\n\tif (!state.statusMessageId) return;\n\tstate.editor.edit(state.chatId, state.statusMessageId, text);\n}\n"]}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * File upload handler — downloads files from Telegram, batches rapid uploads,
3
+ * and sends them to the agent as file path references.
4
+ */
5
+ import type { Api, Context } from "grammy";
6
+ import type { UserState } from "../types.js";
7
+ /**
8
+ * Handle an incoming file (document, photo, voice, audio, video).
9
+ */
10
+ export declare function handleFile(ctx: Context, api: Api, getUserState: (userId: number) => UserState): Promise<void>;
11
+ //# sourceMappingURL=file.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file.d.ts","sourceRoot":"","sources":["../../src/handlers/file.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAC3C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAmB7C;;GAEG;AACH,wBAAsB,UAAU,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,YAAY,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAoEnH","sourcesContent":["/**\n * File upload handler — downloads files from Telegram, batches rapid uploads,\n * and sends them to the agent as file path references.\n */\n\nimport type { Api, Context } from \"grammy\";\nimport type { UserState } from \"../types.js\";\nimport { saveUpload, setPendingBatches } from \"../util/files.js\";\nimport { log, safeSend } from \"../util/telegram.js\";\nimport { sendPrompt } from \"./message.js\";\n\n/** Pending file batch */\ninterface FileBatch {\n\tuserId: number;\n\tchatId: number;\n\treplyToId: number;\n\tstatusMessageId: number | null;\n\tfiles: Array<{ path: string; name: string }>;\n\tcaption: string;\n\ttimer: ReturnType<typeof setTimeout>;\n}\n\nconst BATCH_DELAY = 3000; // 3 second debounce\nconst pendingBatches = new Map<string, FileBatch>();\n\n/**\n * Handle an incoming file (document, photo, voice, audio, video).\n */\nexport async function handleFile(ctx: Context, api: Api, getUserState: (userId: number) => UserState): Promise<void> {\n\tconst msg = ctx.message;\n\tif (!msg) return;\n\n\tconst userId = msg.from?.id;\n\tif (!userId) return;\n\n\tconst chatId = msg.chat.id;\n\tconst caption = msg.caption || \"I've uploaded a file. Please analyze it.\";\n\n\t// Extract file info\n\tconst fileInfo = getFileInfo(msg);\n\tif (!fileInfo) {\n\t\tawait safeSend(api, chatId, \"❓ Unsupported file type\");\n\t\treturn;\n\t}\n\n\t// Download the file\n\tlet localPath: string;\n\ttry {\n\t\tconst file = await ctx.getFile();\n\t\tif (!file.file_path) {\n\t\t\tawait safeSend(api, chatId, \"❌ File too large for download (max 20MB via Bot API)\");\n\t\t\treturn;\n\t\t}\n\t\tconst buffer = await downloadFile(api, file.file_path);\n\t\tlocalPath = saveUpload(fileInfo.name, buffer);\n\t\tlog(`[FILE] Downloaded: ${localPath}`);\n\t} catch (e) {\n\t\tlog(`[FILE] Download failed: ${e}`);\n\t\tawait safeSend(api, chatId, `❌ Failed to download file: ${e}`);\n\t\treturn;\n\t}\n\n\t// Buffer key: media_group_id for albums, user ID for sequential uploads\n\tconst bufferKey = msg.media_group_id || `user_${userId}`;\n\n\tif (pendingBatches.has(bufferKey)) {\n\t\t// Add to existing batch\n\t\tconst batch = pendingBatches.get(bufferKey)!;\n\t\tbatch.files.push({ path: localPath, name: fileInfo.name });\n\t\tif (msg.caption) batch.caption = caption;\n\n\t\t// Reset debounce timer\n\t\tclearTimeout(batch.timer);\n\t\tbatch.timer = setTimeout(() => flushBatch(bufferKey, api, getUserState), BATCH_DELAY);\n\t} else {\n\t\t// Start new batch\n\t\tlet statusMessageId: number | null = null;\n\t\ttry {\n\t\t\tconst statusMsg = await api.sendMessage(chatId, \"đŸ“Ĩ Downloading files...\");\n\t\t\tstatusMessageId = statusMsg.message_id;\n\t\t} catch {\n\t\t\t// Non-critical\n\t\t}\n\n\t\tconst batch: FileBatch = {\n\t\t\tuserId,\n\t\t\tchatId,\n\t\t\treplyToId: msg.message_id,\n\t\t\tstatusMessageId,\n\t\t\tfiles: [{ path: localPath, name: fileInfo.name }],\n\t\t\tcaption,\n\t\t\ttimer: setTimeout(() => flushBatch(bufferKey, api, getUserState), BATCH_DELAY),\n\t\t};\n\t\tpendingBatches.set(bufferKey, batch);\n\t\tsetPendingBatches(pendingBatches.size);\n\t}\n}\n\n/**\n * Flush a file batch — combine all files into a single prompt.\n */\nasync function flushBatch(key: string, api: Api, getUserState: (userId: number) => UserState): Promise<void> {\n\tconst batch = pendingBatches.get(key);\n\tif (!batch) return;\n\tpendingBatches.delete(key);\n\tsetPendingBatches(pendingBatches.size);\n\n\tconst userState = getUserState(batch.userId);\n\tconst n = batch.files.length;\n\tconst fileNames = batch.files.map((f) => f.name).join(\", \");\n\tconst filesText = batch.files.map((f) => `File path: ${f.path}`).join(\"\\n\");\n\tconst prompt = `${batch.caption}\\n\\n${filesText}`;\n\n\tlog(`[FILE] Flushing batch: ${n} file(s) for user ${batch.userId}`);\n\n\t// Send BEFORE any async operations — prevents cleanupUploads race\n\tsendPrompt(api, userState, {\n\t\tchatId: batch.chatId,\n\t\treplyToId: batch.replyToId,\n\t\tuserId: batch.userId,\n\t\tprompt,\n\t\tstatusMessageId: batch.statusMessageId,\n\t});\n\n\t// Update status (non-critical, after send)\n\tif (batch.statusMessageId) {\n\t\tconst isBusy = userState.bridge?.isStreaming || userState.promptInFlight;\n\t\tconst indicator = isBusy ? \"â†Šī¸ _Steering..._\" : \"🧠 _Processing..._\";\n\t\tconst status =\n\t\t\tn === 1\n\t\t\t\t? `đŸ“Ĩ Downloaded: \\`${fileNames}\\`\\n${indicator}`\n\t\t\t\t: `đŸ“Ĩ Downloaded ${n} files: ${fileNames}\\n${indicator}`;\n\t\ttry {\n\t\t\tawait api.editMessageText(batch.chatId, batch.statusMessageId, status, { parse_mode: \"Markdown\" });\n\t\t} catch {\n\t\t\t// Non-critical\n\t\t}\n\t}\n}\n\nfunction getFileInfo(msg: any): { name: string } | null {\n\tif (msg.document) return { name: msg.document.file_name || \"document\" };\n\tif (msg.photo) return { name: \"photo.jpg\" };\n\tif (msg.voice) return { name: \"voice.ogg\" };\n\tif (msg.audio) return { name: msg.audio.file_name || \"audio\" };\n\tif (msg.video) return { name: msg.video.file_name || \"video.mp4\" };\n\treturn null;\n}\n\nasync function downloadFile(api: Api, filePath: string): Promise<Buffer> {\n\tconst url = `https://api.telegram.org/file/bot${api.token}/${filePath}`;\n\tconst response = await fetch(url);\n\tif (!response.ok) throw new Error(`HTTP ${response.status}`);\n\treturn Buffer.from(await response.arrayBuffer());\n}\n"]}
@@ -0,0 +1,138 @@
1
+ /**
2
+ * File upload handler — downloads files from Telegram, batches rapid uploads,
3
+ * and sends them to the agent as file path references.
4
+ */
5
+ import { saveUpload, setPendingBatches } from "../util/files.js";
6
+ import { log, safeSend } from "../util/telegram.js";
7
+ import { sendPrompt } from "./message.js";
8
+ const BATCH_DELAY = 3000; // 3 second debounce
9
+ const pendingBatches = new Map();
10
+ /**
11
+ * Handle an incoming file (document, photo, voice, audio, video).
12
+ */
13
+ export async function handleFile(ctx, api, getUserState) {
14
+ const msg = ctx.message;
15
+ if (!msg)
16
+ return;
17
+ const userId = msg.from?.id;
18
+ if (!userId)
19
+ return;
20
+ const chatId = msg.chat.id;
21
+ const caption = msg.caption || "I've uploaded a file. Please analyze it.";
22
+ // Extract file info
23
+ const fileInfo = getFileInfo(msg);
24
+ if (!fileInfo) {
25
+ await safeSend(api, chatId, "❓ Unsupported file type");
26
+ return;
27
+ }
28
+ // Download the file
29
+ let localPath;
30
+ try {
31
+ const file = await ctx.getFile();
32
+ if (!file.file_path) {
33
+ await safeSend(api, chatId, "❌ File too large for download (max 20MB via Bot API)");
34
+ return;
35
+ }
36
+ const buffer = await downloadFile(api, file.file_path);
37
+ localPath = saveUpload(fileInfo.name, buffer);
38
+ log(`[FILE] Downloaded: ${localPath}`);
39
+ }
40
+ catch (e) {
41
+ log(`[FILE] Download failed: ${e}`);
42
+ await safeSend(api, chatId, `❌ Failed to download file: ${e}`);
43
+ return;
44
+ }
45
+ // Buffer key: media_group_id for albums, user ID for sequential uploads
46
+ const bufferKey = msg.media_group_id || `user_${userId}`;
47
+ if (pendingBatches.has(bufferKey)) {
48
+ // Add to existing batch
49
+ const batch = pendingBatches.get(bufferKey);
50
+ batch.files.push({ path: localPath, name: fileInfo.name });
51
+ if (msg.caption)
52
+ batch.caption = caption;
53
+ // Reset debounce timer
54
+ clearTimeout(batch.timer);
55
+ batch.timer = setTimeout(() => flushBatch(bufferKey, api, getUserState), BATCH_DELAY);
56
+ }
57
+ else {
58
+ // Start new batch
59
+ let statusMessageId = null;
60
+ try {
61
+ const statusMsg = await api.sendMessage(chatId, "đŸ“Ĩ Downloading files...");
62
+ statusMessageId = statusMsg.message_id;
63
+ }
64
+ catch {
65
+ // Non-critical
66
+ }
67
+ const batch = {
68
+ userId,
69
+ chatId,
70
+ replyToId: msg.message_id,
71
+ statusMessageId,
72
+ files: [{ path: localPath, name: fileInfo.name }],
73
+ caption,
74
+ timer: setTimeout(() => flushBatch(bufferKey, api, getUserState), BATCH_DELAY),
75
+ };
76
+ pendingBatches.set(bufferKey, batch);
77
+ setPendingBatches(pendingBatches.size);
78
+ }
79
+ }
80
+ /**
81
+ * Flush a file batch — combine all files into a single prompt.
82
+ */
83
+ async function flushBatch(key, api, getUserState) {
84
+ const batch = pendingBatches.get(key);
85
+ if (!batch)
86
+ return;
87
+ pendingBatches.delete(key);
88
+ setPendingBatches(pendingBatches.size);
89
+ const userState = getUserState(batch.userId);
90
+ const n = batch.files.length;
91
+ const fileNames = batch.files.map((f) => f.name).join(", ");
92
+ const filesText = batch.files.map((f) => `File path: ${f.path}`).join("\n");
93
+ const prompt = `${batch.caption}\n\n${filesText}`;
94
+ log(`[FILE] Flushing batch: ${n} file(s) for user ${batch.userId}`);
95
+ // Send BEFORE any async operations — prevents cleanupUploads race
96
+ sendPrompt(api, userState, {
97
+ chatId: batch.chatId,
98
+ replyToId: batch.replyToId,
99
+ userId: batch.userId,
100
+ prompt,
101
+ statusMessageId: batch.statusMessageId,
102
+ });
103
+ // Update status (non-critical, after send)
104
+ if (batch.statusMessageId) {
105
+ const isBusy = userState.bridge?.isStreaming || userState.promptInFlight;
106
+ const indicator = isBusy ? "â†Šī¸ _Steering..._" : "🧠 _Processing..._";
107
+ const status = n === 1
108
+ ? `đŸ“Ĩ Downloaded: \`${fileNames}\`\n${indicator}`
109
+ : `đŸ“Ĩ Downloaded ${n} files: ${fileNames}\n${indicator}`;
110
+ try {
111
+ await api.editMessageText(batch.chatId, batch.statusMessageId, status, { parse_mode: "Markdown" });
112
+ }
113
+ catch {
114
+ // Non-critical
115
+ }
116
+ }
117
+ }
118
+ function getFileInfo(msg) {
119
+ if (msg.document)
120
+ return { name: msg.document.file_name || "document" };
121
+ if (msg.photo)
122
+ return { name: "photo.jpg" };
123
+ if (msg.voice)
124
+ return { name: "voice.ogg" };
125
+ if (msg.audio)
126
+ return { name: msg.audio.file_name || "audio" };
127
+ if (msg.video)
128
+ return { name: msg.video.file_name || "video.mp4" };
129
+ return null;
130
+ }
131
+ async function downloadFile(api, filePath) {
132
+ const url = `https://api.telegram.org/file/bot${api.token}/${filePath}`;
133
+ const response = await fetch(url);
134
+ if (!response.ok)
135
+ throw new Error(`HTTP ${response.status}`);
136
+ return Buffer.from(await response.arrayBuffer());
137
+ }
138
+ //# sourceMappingURL=file.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file.js","sourceRoot":"","sources":["../../src/handlers/file.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * File upload handler — downloads files from Telegram, batches rapid uploads,\n * and sends them to the agent as file path references.\n */\n\nimport type { Api, Context } from \"grammy\";\nimport type { UserState } from \"../types.js\";\nimport { saveUpload, setPendingBatches } from \"../util/files.js\";\nimport { log, safeSend } from \"../util/telegram.js\";\nimport { sendPrompt } from \"./message.js\";\n\n/** Pending file batch */\ninterface FileBatch {\n\tuserId: number;\n\tchatId: number;\n\treplyToId: number;\n\tstatusMessageId: number | null;\n\tfiles: Array<{ path: string; name: string }>;\n\tcaption: string;\n\ttimer: ReturnType<typeof setTimeout>;\n}\n\nconst BATCH_DELAY = 3000; // 3 second debounce\nconst pendingBatches = new Map<string, FileBatch>();\n\n/**\n * Handle an incoming file (document, photo, voice, audio, video).\n */\nexport async function handleFile(ctx: Context, api: Api, getUserState: (userId: number) => UserState): Promise<void> {\n\tconst msg = ctx.message;\n\tif (!msg) return;\n\n\tconst userId = msg.from?.id;\n\tif (!userId) return;\n\n\tconst chatId = msg.chat.id;\n\tconst caption = msg.caption || \"I've uploaded a file. Please analyze it.\";\n\n\t// Extract file info\n\tconst fileInfo = getFileInfo(msg);\n\tif (!fileInfo) {\n\t\tawait safeSend(api, chatId, \"❓ Unsupported file type\");\n\t\treturn;\n\t}\n\n\t// Download the file\n\tlet localPath: string;\n\ttry {\n\t\tconst file = await ctx.getFile();\n\t\tif (!file.file_path) {\n\t\t\tawait safeSend(api, chatId, \"❌ File too large for download (max 20MB via Bot API)\");\n\t\t\treturn;\n\t\t}\n\t\tconst buffer = await downloadFile(api, file.file_path);\n\t\tlocalPath = saveUpload(fileInfo.name, buffer);\n\t\tlog(`[FILE] Downloaded: ${localPath}`);\n\t} catch (e) {\n\t\tlog(`[FILE] Download failed: ${e}`);\n\t\tawait safeSend(api, chatId, `❌ Failed to download file: ${e}`);\n\t\treturn;\n\t}\n\n\t// Buffer key: media_group_id for albums, user ID for sequential uploads\n\tconst bufferKey = msg.media_group_id || `user_${userId}`;\n\n\tif (pendingBatches.has(bufferKey)) {\n\t\t// Add to existing batch\n\t\tconst batch = pendingBatches.get(bufferKey)!;\n\t\tbatch.files.push({ path: localPath, name: fileInfo.name });\n\t\tif (msg.caption) batch.caption = caption;\n\n\t\t// Reset debounce timer\n\t\tclearTimeout(batch.timer);\n\t\tbatch.timer = setTimeout(() => flushBatch(bufferKey, api, getUserState), BATCH_DELAY);\n\t} else {\n\t\t// Start new batch\n\t\tlet statusMessageId: number | null = null;\n\t\ttry {\n\t\t\tconst statusMsg = await api.sendMessage(chatId, \"đŸ“Ĩ Downloading files...\");\n\t\t\tstatusMessageId = statusMsg.message_id;\n\t\t} catch {\n\t\t\t// Non-critical\n\t\t}\n\n\t\tconst batch: FileBatch = {\n\t\t\tuserId,\n\t\t\tchatId,\n\t\t\treplyToId: msg.message_id,\n\t\t\tstatusMessageId,\n\t\t\tfiles: [{ path: localPath, name: fileInfo.name }],\n\t\t\tcaption,\n\t\t\ttimer: setTimeout(() => flushBatch(bufferKey, api, getUserState), BATCH_DELAY),\n\t\t};\n\t\tpendingBatches.set(bufferKey, batch);\n\t\tsetPendingBatches(pendingBatches.size);\n\t}\n}\n\n/**\n * Flush a file batch — combine all files into a single prompt.\n */\nasync function flushBatch(key: string, api: Api, getUserState: (userId: number) => UserState): Promise<void> {\n\tconst batch = pendingBatches.get(key);\n\tif (!batch) return;\n\tpendingBatches.delete(key);\n\tsetPendingBatches(pendingBatches.size);\n\n\tconst userState = getUserState(batch.userId);\n\tconst n = batch.files.length;\n\tconst fileNames = batch.files.map((f) => f.name).join(\", \");\n\tconst filesText = batch.files.map((f) => `File path: ${f.path}`).join(\"\\n\");\n\tconst prompt = `${batch.caption}\\n\\n${filesText}`;\n\n\tlog(`[FILE] Flushing batch: ${n} file(s) for user ${batch.userId}`);\n\n\t// Send BEFORE any async operations — prevents cleanupUploads race\n\tsendPrompt(api, userState, {\n\t\tchatId: batch.chatId,\n\t\treplyToId: batch.replyToId,\n\t\tuserId: batch.userId,\n\t\tprompt,\n\t\tstatusMessageId: batch.statusMessageId,\n\t});\n\n\t// Update status (non-critical, after send)\n\tif (batch.statusMessageId) {\n\t\tconst isBusy = userState.bridge?.isStreaming || userState.promptInFlight;\n\t\tconst indicator = isBusy ? \"â†Šī¸ _Steering..._\" : \"🧠 _Processing..._\";\n\t\tconst status =\n\t\t\tn === 1\n\t\t\t\t? `đŸ“Ĩ Downloaded: \\`${fileNames}\\`\\n${indicator}`\n\t\t\t\t: `đŸ“Ĩ Downloaded ${n} files: ${fileNames}\\n${indicator}`;\n\t\ttry {\n\t\t\tawait api.editMessageText(batch.chatId, batch.statusMessageId, status, { parse_mode: \"Markdown\" });\n\t\t} catch {\n\t\t\t// Non-critical\n\t\t}\n\t}\n}\n\nfunction getFileInfo(msg: any): { name: string } | null {\n\tif (msg.document) return { name: msg.document.file_name || \"document\" };\n\tif (msg.photo) return { name: \"photo.jpg\" };\n\tif (msg.voice) return { name: \"voice.ogg\" };\n\tif (msg.audio) return { name: msg.audio.file_name || \"audio\" };\n\tif (msg.video) return { name: msg.video.file_name || \"video.mp4\" };\n\treturn null;\n}\n\nasync function downloadFile(api: Api, filePath: string): Promise<Buffer> {\n\tconst url = `https://api.telegram.org/file/bot${api.token}/${filePath}`;\n\tconst response = await fetch(url);\n\tif (!response.ok) throw new Error(`HTTP ${response.status}`);\n\treturn Buffer.from(await response.arrayBuffer());\n}\n"]}