claudeck 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 (157) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +233 -0
  3. package/cli.js +2 -0
  4. package/config/agent-chains.json +16 -0
  5. package/config/agent-dags.json +16 -0
  6. package/config/agents.json +46 -0
  7. package/config/bot-prompt.json +3 -0
  8. package/config/folders.json +66 -0
  9. package/config/prompts.json +92 -0
  10. package/config/repos.json +86 -0
  11. package/config/telegram-config.json +17 -0
  12. package/config/workflows.json +90 -0
  13. package/db.js +1198 -0
  14. package/package.json +55 -0
  15. package/plugins/claude-editor/client.css +171 -0
  16. package/plugins/claude-editor/client.js +183 -0
  17. package/plugins/event-stream/client.css +207 -0
  18. package/plugins/event-stream/client.js +271 -0
  19. package/plugins/linear/client.css +345 -0
  20. package/plugins/linear/client.js +380 -0
  21. package/plugins/linear/config.json +5 -0
  22. package/plugins/linear/server.js +312 -0
  23. package/plugins/repos/client.css +549 -0
  24. package/plugins/repos/client.js +663 -0
  25. package/plugins/repos/server.js +232 -0
  26. package/plugins/sudoku/client.css +196 -0
  27. package/plugins/sudoku/client.js +329 -0
  28. package/plugins/tasks/client.css +414 -0
  29. package/plugins/tasks/client.js +394 -0
  30. package/plugins/tasks/server.js +116 -0
  31. package/plugins/tic-tac-toe/client.css +167 -0
  32. package/plugins/tic-tac-toe/client.js +241 -0
  33. package/public/css/core/components.css +232 -0
  34. package/public/css/core/layout.css +330 -0
  35. package/public/css/core/print.css +18 -0
  36. package/public/css/core/reset.css +36 -0
  37. package/public/css/core/responsive.css +378 -0
  38. package/public/css/core/theme.css +116 -0
  39. package/public/css/core/variables.css +93 -0
  40. package/public/css/features/agent-monitor.css +297 -0
  41. package/public/css/features/agent-sidebar.css +525 -0
  42. package/public/css/features/agents.css +996 -0
  43. package/public/css/features/analytics.css +181 -0
  44. package/public/css/features/background-sessions.css +321 -0
  45. package/public/css/features/cost-dashboard.css +168 -0
  46. package/public/css/features/home.css +313 -0
  47. package/public/css/features/retro-terminal.css +88 -0
  48. package/public/css/features/telegram.css +127 -0
  49. package/public/css/features/tour.css +148 -0
  50. package/public/css/features/voice-input.css +60 -0
  51. package/public/css/features/welcome.css +241 -0
  52. package/public/css/panels/assistant-bot.css +442 -0
  53. package/public/css/panels/dev-docs.css +292 -0
  54. package/public/css/panels/file-explorer.css +322 -0
  55. package/public/css/panels/git-panel.css +221 -0
  56. package/public/css/panels/mcp-manager.css +199 -0
  57. package/public/css/panels/tips-feed.css +353 -0
  58. package/public/css/ui/commands.css +273 -0
  59. package/public/css/ui/context-gauge.css +76 -0
  60. package/public/css/ui/file-picker.css +69 -0
  61. package/public/css/ui/image-attachments.css +106 -0
  62. package/public/css/ui/messages.css +884 -0
  63. package/public/css/ui/modals.css +122 -0
  64. package/public/css/ui/parallel.css +217 -0
  65. package/public/css/ui/permissions.css +110 -0
  66. package/public/css/ui/right-panel.css +481 -0
  67. package/public/css/ui/sessions.css +689 -0
  68. package/public/css/ui/status-bar.css +425 -0
  69. package/public/css/ui/toolbox.css +206 -0
  70. package/public/data/tips.json +218 -0
  71. package/public/icons/favicon.png +0 -0
  72. package/public/icons/icon-192.png +0 -0
  73. package/public/icons/icon-512.png +0 -0
  74. package/public/icons/whaly.png +0 -0
  75. package/public/index.html +1140 -0
  76. package/public/js/core/api.js +591 -0
  77. package/public/js/core/constants.js +3 -0
  78. package/public/js/core/dom.js +270 -0
  79. package/public/js/core/events.js +10 -0
  80. package/public/js/core/plugin-loader.js +153 -0
  81. package/public/js/core/store.js +39 -0
  82. package/public/js/core/utils.js +25 -0
  83. package/public/js/core/ws.js +64 -0
  84. package/public/js/features/agent-monitor.js +222 -0
  85. package/public/js/features/agents.js +1209 -0
  86. package/public/js/features/analytics.js +397 -0
  87. package/public/js/features/attachments.js +251 -0
  88. package/public/js/features/background-sessions.js +475 -0
  89. package/public/js/features/chat.js +589 -0
  90. package/public/js/features/cost-dashboard.js +152 -0
  91. package/public/js/features/dag-editor.js +399 -0
  92. package/public/js/features/easter-egg.js +46 -0
  93. package/public/js/features/home.js +270 -0
  94. package/public/js/features/projects.js +372 -0
  95. package/public/js/features/prompts.js +228 -0
  96. package/public/js/features/sessions.js +332 -0
  97. package/public/js/features/telegram.js +131 -0
  98. package/public/js/features/tour.js +210 -0
  99. package/public/js/features/voice-input.js +185 -0
  100. package/public/js/features/welcome.js +43 -0
  101. package/public/js/features/workflows.js +277 -0
  102. package/public/js/main.js +51 -0
  103. package/public/js/panels/assistant-bot.js +445 -0
  104. package/public/js/panels/dev-docs.js +380 -0
  105. package/public/js/panels/file-explorer.js +486 -0
  106. package/public/js/panels/git-panel.js +285 -0
  107. package/public/js/panels/mcp-manager.js +311 -0
  108. package/public/js/panels/tips-feed.js +303 -0
  109. package/public/js/ui/commands.js +114 -0
  110. package/public/js/ui/context-gauge.js +100 -0
  111. package/public/js/ui/diff.js +124 -0
  112. package/public/js/ui/disabled-tools.js +36 -0
  113. package/public/js/ui/export.js +74 -0
  114. package/public/js/ui/formatting.js +206 -0
  115. package/public/js/ui/header-dropdowns.js +72 -0
  116. package/public/js/ui/input-meta.js +71 -0
  117. package/public/js/ui/max-turns.js +21 -0
  118. package/public/js/ui/messages.js +387 -0
  119. package/public/js/ui/model-selector.js +20 -0
  120. package/public/js/ui/notifications.js +232 -0
  121. package/public/js/ui/parallel.js +176 -0
  122. package/public/js/ui/permissions.js +168 -0
  123. package/public/js/ui/right-panel.js +173 -0
  124. package/public/js/ui/shortcuts.js +143 -0
  125. package/public/js/ui/sidebar-toggle.js +29 -0
  126. package/public/js/ui/status-bar.js +172 -0
  127. package/public/js/ui/tab-sdk.js +623 -0
  128. package/public/js/ui/theme.js +38 -0
  129. package/public/manifest.json +13 -0
  130. package/public/offline.html +190 -0
  131. package/public/style.css +42 -0
  132. package/public/sw.js +91 -0
  133. package/server/agent-loop.js +385 -0
  134. package/server/dag-executor.js +265 -0
  135. package/server/orchestrator.js +514 -0
  136. package/server/paths.js +61 -0
  137. package/server/plugin-mount.js +56 -0
  138. package/server/push-sender.js +31 -0
  139. package/server/routes/agents.js +294 -0
  140. package/server/routes/bot.js +45 -0
  141. package/server/routes/exec.js +35 -0
  142. package/server/routes/files.js +218 -0
  143. package/server/routes/mcp.js +82 -0
  144. package/server/routes/messages.js +36 -0
  145. package/server/routes/notifications.js +37 -0
  146. package/server/routes/projects.js +207 -0
  147. package/server/routes/prompts.js +53 -0
  148. package/server/routes/sessions.js +103 -0
  149. package/server/routes/stats.js +143 -0
  150. package/server/routes/telegram.js +71 -0
  151. package/server/routes/tips.js +135 -0
  152. package/server/routes/workflows.js +81 -0
  153. package/server/summarizer.js +55 -0
  154. package/server/telegram-poller.js +205 -0
  155. package/server/telegram-sender.js +304 -0
  156. package/server/ws-handler.js +926 -0
  157. package/server.js +179 -0
@@ -0,0 +1,135 @@
1
+ import { Router } from "express";
2
+ import { readFileSync } from "fs";
3
+ import { join, dirname } from "path";
4
+ import { fileURLToPath } from "url";
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = dirname(__filename);
8
+
9
+ const router = Router();
10
+
11
+ // Cache for tips.json
12
+ let tipsData = null;
13
+
14
+ function loadTips() {
15
+ if (!tipsData) {
16
+ const filePath = join(__dirname, "../../public/data/tips.json");
17
+ tipsData = JSON.parse(readFileSync(filePath, "utf-8"));
18
+ }
19
+ return tipsData;
20
+ }
21
+
22
+ // GET /api/tips — serve tips data
23
+ router.get("/", (req, res) => {
24
+ try {
25
+ res.json(loadTips());
26
+ } catch (err) {
27
+ res.status(500).json({ error: "Failed to load tips" });
28
+ }
29
+ });
30
+
31
+ // RSS proxy cache: url -> { data, timestamp }
32
+ const rssCache = new Map();
33
+ const RSS_CACHE_TTL = 15 * 60 * 1000; // 15 minutes
34
+
35
+ function stripHtml(str) {
36
+ return str
37
+ .replace(/<[^>]*>/g, " ")
38
+ .replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&")
39
+ .replace(/&quot;/g, '"').replace(/&#039;/g, "'").replace(/&#8212;/g, "—")
40
+ .replace(/&#\d+;/g, "")
41
+ .replace(/<[^>]*>/g, "") // strip any decoded tags
42
+ .replace(/\s+/g, " ")
43
+ .trim();
44
+ }
45
+
46
+ // Simple regex-based XML parser — handles RSS 2.0 (<item>) and Atom (<entry>)
47
+ function parseRssXml(xml) {
48
+ const items = [];
49
+
50
+ // Helper to extract tag content (CDATA or plain)
51
+ const getTag = (block, tag) => {
52
+ const m = block.match(new RegExp(`<${tag}[^>]*><!\\[CDATA\\[([\\s\\S]*?)\\]\\]><\\/${tag}>`, "i"))
53
+ || block.match(new RegExp(`<${tag}[^>]*>([\\s\\S]*?)<\\/${tag}>`, "i"));
54
+ return m ? m[1].trim() : "";
55
+ };
56
+
57
+ // Helper to extract Atom <link href="..."/>
58
+ const getAtomLink = (block) => {
59
+ const m = block.match(/<link[^>]*href="([^"]*)"[^>]*rel="alternate"/i)
60
+ || block.match(/<link[^>]*rel="alternate"[^>]*href="([^"]*)"/i)
61
+ || block.match(/<link[^>]*href="([^"]*)"/i);
62
+ return m ? m[1].trim() : "";
63
+ };
64
+
65
+ // Try RSS 2.0 first (<item>)
66
+ const itemRegex = /<item>([\s\S]*?)<\/item>/gi;
67
+ let match;
68
+ while ((match = itemRegex.exec(xml)) !== null && items.length < 20) {
69
+ const block = match[1];
70
+ items.push({
71
+ title: stripHtml(getTag(block, "title")),
72
+ link: getTag(block, "link"),
73
+ pubDate: getTag(block, "pubDate"),
74
+ description: stripHtml(getTag(block, "description")).slice(0, 200),
75
+ });
76
+ }
77
+
78
+ // Fall back to Atom (<entry>) if no RSS items found
79
+ if (items.length === 0) {
80
+ const entryRegex = /<entry>([\s\S]*?)<\/entry>/gi;
81
+ while ((match = entryRegex.exec(xml)) !== null && items.length < 20) {
82
+ const block = match[1];
83
+ const desc = getTag(block, "summary") || getTag(block, "content");
84
+ items.push({
85
+ title: stripHtml(getTag(block, "title")),
86
+ link: getAtomLink(block) || getTag(block, "link"),
87
+ pubDate: getTag(block, "published") || getTag(block, "updated"),
88
+ description: stripHtml(desc).slice(0, 200),
89
+ });
90
+ }
91
+ }
92
+
93
+ return items;
94
+ }
95
+
96
+ // GET /api/tips/rss?url=<encoded> — proxy RSS feed
97
+ router.get("/rss", async (req, res) => {
98
+ const { url } = req.query;
99
+ if (!url) return res.status(400).json({ error: "url parameter required" });
100
+
101
+ // Check cache
102
+ const cached = rssCache.get(url);
103
+ if (cached && Date.now() - cached.timestamp < RSS_CACHE_TTL) {
104
+ return res.json({ items: cached.data });
105
+ }
106
+
107
+ try {
108
+ const controller = new AbortController();
109
+ const timeout = setTimeout(() => controller.abort(), 8000);
110
+ const response = await fetch(url, {
111
+ signal: controller.signal,
112
+ headers: { "User-Agent": "claudeck/1.0" },
113
+ });
114
+ clearTimeout(timeout);
115
+
116
+ if (!response.ok) {
117
+ return res.status(502).json({ error: `Feed returned ${response.status}` });
118
+ }
119
+
120
+ const xml = await response.text();
121
+ const items = parseRssXml(xml);
122
+
123
+ // Cache the result
124
+ rssCache.set(url, { data: items, timestamp: Date.now() });
125
+
126
+ res.json({ items });
127
+ } catch (err) {
128
+ if (err.name === "AbortError") {
129
+ return res.status(504).json({ error: "Feed request timed out" });
130
+ }
131
+ res.status(502).json({ error: "Failed to fetch feed" });
132
+ }
133
+ });
134
+
135
+ export default router;
@@ -0,0 +1,81 @@
1
+ import { Router } from "express";
2
+ import { readFile, writeFile } from "fs/promises";
3
+ import { configPath } from "../paths.js";
4
+
5
+ const router = Router();
6
+
7
+ async function readWorkflows() {
8
+ try {
9
+ const data = await readFile(configPath("workflows.json"), "utf-8");
10
+ return JSON.parse(data);
11
+ } catch {
12
+ return [];
13
+ }
14
+ }
15
+
16
+ async function writeWorkflows(workflows) {
17
+ await writeFile(configPath("workflows.json"), JSON.stringify(workflows, null, 2) + "\n");
18
+ }
19
+
20
+ function slugify(text) {
21
+ return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
22
+ }
23
+
24
+ router.get("/", async (req, res) => {
25
+ try {
26
+ res.json(await readWorkflows());
27
+ } catch (err) {
28
+ res.status(500).json({ error: err.message });
29
+ }
30
+ });
31
+
32
+ router.post("/", async (req, res) => {
33
+ try {
34
+ const { title, description, steps } = req.body;
35
+ if (!title || !steps?.length) {
36
+ return res.status(400).json({ error: "title and steps are required" });
37
+ }
38
+ const workflows = await readWorkflows();
39
+ const id = slugify(title);
40
+ if (workflows.find((w) => w.id === id)) {
41
+ return res.status(409).json({ error: `Workflow "${id}" already exists` });
42
+ }
43
+ const wf = { id, title, description: description || "", steps };
44
+ workflows.push(wf);
45
+ await writeWorkflows(workflows);
46
+ res.json(wf);
47
+ } catch (err) {
48
+ res.status(500).json({ error: err.message });
49
+ }
50
+ });
51
+
52
+ router.put("/:id", async (req, res) => {
53
+ try {
54
+ const workflows = await readWorkflows();
55
+ const idx = workflows.findIndex((w) => w.id === req.params.id);
56
+ if (idx === -1) return res.status(404).json({ error: "Workflow not found" });
57
+ const { title, description, steps } = req.body;
58
+ if (title !== undefined) workflows[idx].title = title;
59
+ if (description !== undefined) workflows[idx].description = description;
60
+ if (steps !== undefined) workflows[idx].steps = steps;
61
+ await writeWorkflows(workflows);
62
+ res.json(workflows[idx]);
63
+ } catch (err) {
64
+ res.status(500).json({ error: err.message });
65
+ }
66
+ });
67
+
68
+ router.delete("/:id", async (req, res) => {
69
+ try {
70
+ const workflows = await readWorkflows();
71
+ const idx = workflows.findIndex((w) => w.id === req.params.id);
72
+ if (idx === -1) return res.status(404).json({ error: "Workflow not found" });
73
+ workflows.splice(idx, 1);
74
+ await writeWorkflows(workflows);
75
+ res.json({ ok: true });
76
+ } catch (err) {
77
+ res.status(500).json({ error: err.message });
78
+ }
79
+ });
80
+
81
+ export default router;
@@ -0,0 +1,55 @@
1
+ import { query } from "@anthropic-ai/claude-code";
2
+ import { execPath } from "process";
3
+ import { getMessagesNoChatId, updateSessionSummary, getSession } from "../db.js";
4
+
5
+ export async function generateSessionSummary(sessionId) {
6
+ const session = getSession(sessionId);
7
+ if (!session) return null;
8
+
9
+ const messages = getMessagesNoChatId(sessionId);
10
+ const conversation = [];
11
+ for (const msg of messages) {
12
+ try {
13
+ const data = JSON.parse(msg.content);
14
+ if (msg.role === "user" && data.text) {
15
+ conversation.push(`User: ${data.text.slice(0, 500)}`);
16
+ } else if (msg.role === "assistant" && data.text) {
17
+ conversation.push(`Assistant: ${data.text.slice(0, 500)}`);
18
+ }
19
+ } catch {
20
+ // skip unparseable messages
21
+ }
22
+ }
23
+
24
+ if (conversation.length < 2) return null;
25
+
26
+ const transcript = conversation.join("\n").slice(-4000);
27
+ const prompt = `Summarize this coding session in 1 short sentence (max 120 chars). Focus on what was accomplished or discussed. No quotes, no prefixes like "Summary:". Just the sentence.\n\n${transcript}`;
28
+
29
+ let summary = null;
30
+
31
+ const q = query({
32
+ prompt,
33
+ options: {
34
+ maxTurns: 1,
35
+ model: "claude-haiku-4-5-20251001",
36
+ permissionMode: "bypassPermissions",
37
+ executable: execPath,
38
+ },
39
+ });
40
+
41
+ for await (const msg of q) {
42
+ if (msg.type === "assistant" && msg.message?.content) {
43
+ for (const block of msg.message.content) {
44
+ if (block.type === "text" && block.text) {
45
+ summary = block.text.trim().slice(0, 200);
46
+ }
47
+ }
48
+ }
49
+ }
50
+
51
+ if (summary) {
52
+ updateSessionSummary(sessionId, summary);
53
+ }
54
+ return summary;
55
+ }
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Telegram long-poller — listens for callback_query events (Approve/Deny button presses)
3
+ * and routes them back to the pendingApprovals Map used by the permission system.
4
+ *
5
+ * Also sends a WebSocket message to the frontend so the permission modal auto-dismisses
6
+ * when approval comes from Telegram.
7
+ */
8
+ import {
9
+ getRawBotToken,
10
+ isEnabled,
11
+ answerCallbackQuery,
12
+ editMessageText,
13
+ editMessageReplyMarkup,
14
+ } from "./telegram-sender.js";
15
+
16
+ let polling = false;
17
+ let pollAbort = null;
18
+ let lastUpdateId = 0;
19
+
20
+ // Registry: approvalId → { resolve, timer, toolInput, telegramMessageId, ws }
21
+ // Shared reference set by ws-handler via registerApprovalBridge
22
+ let pendingApprovalsRef = null;
23
+ let broadcastToSessionRef = null;
24
+
25
+ /**
26
+ * Register the bridge so the poller can resolve pending approvals and notify the frontend.
27
+ * @param {Map} pendingApprovals - The Map from ws-handler
28
+ * @param {Function} broadcastToSession - fn(sessionId, payload) to send WS msg to frontend
29
+ */
30
+ export function registerApprovalBridge(pendingApprovals, broadcastToSession) {
31
+ pendingApprovalsRef = pendingApprovals;
32
+ broadcastToSessionRef = broadcastToSession;
33
+ }
34
+
35
+ /**
36
+ * Track a Telegram message ID for a given approval so we can edit it later.
37
+ */
38
+ const approvalMessages = new Map(); // approvalId → { messageId, toolName }
39
+
40
+ export function trackApprovalMessage(approvalId, messageId, toolName) {
41
+ approvalMessages.set(approvalId, { messageId, toolName });
42
+ }
43
+
44
+ export function removeApprovalMessage(approvalId) {
45
+ const entry = approvalMessages.get(approvalId);
46
+ approvalMessages.delete(approvalId);
47
+ return entry;
48
+ }
49
+
50
+ /**
51
+ * Mark a Telegram permission message as resolved (from web UI).
52
+ * Called when the user approves/denies via the web modal.
53
+ */
54
+ export async function markTelegramMessageResolved(approvalId, behavior) {
55
+ const entry = removeApprovalMessage(approvalId);
56
+ if (!entry) return;
57
+
58
+ const icon = behavior === "allow" ? "\u{2705}" : "\u{274C}";
59
+ const label = behavior === "allow" ? "Approved" : "Denied";
60
+
61
+ await editMessageText(
62
+ entry.messageId,
63
+ `${icon} <b>${label} via Web</b>\n<s>${entry.toolName}</s>`
64
+ );
65
+ }
66
+
67
+ // ── Polling loop ──
68
+
69
+ async function pollOnce() {
70
+ const token = getRawBotToken();
71
+ if (!token) return;
72
+
73
+ pollAbort = new AbortController();
74
+
75
+ try {
76
+ const res = await fetch(
77
+ `https://api.telegram.org/bot${token}/getUpdates`,
78
+ {
79
+ method: "POST",
80
+ headers: { "Content-Type": "application/json" },
81
+ body: JSON.stringify({
82
+ offset: lastUpdateId + 1,
83
+ timeout: 30,
84
+ allowed_updates: ["callback_query"],
85
+ }),
86
+ signal: pollAbort.signal,
87
+ }
88
+ );
89
+
90
+ if (!res.ok) {
91
+ console.error("Telegram poll error:", res.status, await res.text());
92
+ return;
93
+ }
94
+
95
+ const data = await res.json();
96
+ if (!data.ok || !data.result?.length) return;
97
+
98
+ for (const update of data.result) {
99
+ lastUpdateId = Math.max(lastUpdateId, update.update_id);
100
+ if (update.callback_query) {
101
+ await handleCallback(update.callback_query);
102
+ }
103
+ }
104
+ } catch (err) {
105
+ if (err.name === "AbortError") return;
106
+ console.error("Telegram poll error:", err.message);
107
+ }
108
+ }
109
+
110
+ async function handleCallback(cb) {
111
+ const data = cb.data || "";
112
+ const [action, approvalId] = data.split(":");
113
+
114
+ if (!approvalId || (action !== "approve" && action !== "deny")) {
115
+ await answerCallbackQuery(cb.id, "Unknown action");
116
+ return;
117
+ }
118
+
119
+ const behavior = action === "approve" ? "allow" : "deny";
120
+ const icon = action === "approve" ? "\u{2705}" : "\u{274C}";
121
+ const label = action === "approve" ? "Approved" : "Denied";
122
+
123
+ // Try to resolve the pending approval
124
+ let resolved = false;
125
+
126
+ if (pendingApprovalsRef) {
127
+ const pending = pendingApprovalsRef.get(approvalId);
128
+ if (pending) {
129
+ clearTimeout(pending.timer);
130
+ pendingApprovalsRef.delete(approvalId);
131
+
132
+ if (behavior === "allow") {
133
+ pending.resolve({ behavior: "allow", updatedInput: pending.toolInput });
134
+ } else {
135
+ pending.resolve({ behavior: "deny", message: "Denied via Telegram" });
136
+ }
137
+
138
+ resolved = true;
139
+
140
+ // Notify the frontend to dismiss the permission modal
141
+ if (pending.ws && pending.ws.readyState === 1) {
142
+ pending.ws.send(JSON.stringify({
143
+ type: "permission_response_external",
144
+ id: approvalId,
145
+ behavior,
146
+ source: "telegram",
147
+ }));
148
+ }
149
+ }
150
+ }
151
+
152
+ // Update the Telegram message
153
+ const entry = removeApprovalMessage(approvalId);
154
+ if (entry) {
155
+ await editMessageText(
156
+ entry.messageId,
157
+ `${icon} <b>${label} via Telegram</b>\n<s>${entry.toolName || "Tool"}</s>`
158
+ );
159
+ } else if (cb.message?.message_id) {
160
+ // Fallback: remove buttons even if we lost track
161
+ await editMessageReplyMarkup(cb.message.message_id);
162
+ }
163
+
164
+ // Answer the callback to dismiss the loading spinner
165
+ await answerCallbackQuery(cb.id, resolved ? `${label}!` : "Already resolved");
166
+ }
167
+
168
+ // ── Lifecycle ──
169
+
170
+ export function startTelegramPoller() {
171
+ if (polling) return;
172
+ if (!isEnabled()) {
173
+ console.log("Telegram poller: disabled (not configured)");
174
+ return;
175
+ }
176
+
177
+ polling = true;
178
+ console.log("Telegram poller: started");
179
+
180
+ (async () => {
181
+ while (polling) {
182
+ await pollOnce();
183
+ // Small delay between polls to avoid hammering on errors
184
+ if (polling) await new Promise((r) => setTimeout(r, 500));
185
+ }
186
+ })();
187
+ }
188
+
189
+ export function stopTelegramPoller() {
190
+ polling = false;
191
+ if (pollAbort) {
192
+ pollAbort.abort();
193
+ pollAbort = null;
194
+ }
195
+ console.log("Telegram poller: stopped");
196
+ }
197
+
198
+ /**
199
+ * Restart the poller (e.g., after config change).
200
+ */
201
+ export function restartTelegramPoller() {
202
+ stopTelegramPoller();
203
+ // Small delay to let the old poll abort cleanly
204
+ setTimeout(() => startTelegramPoller(), 1000);
205
+ }