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.
- package/LICENSE +21 -0
- package/README.md +233 -0
- package/cli.js +2 -0
- package/config/agent-chains.json +16 -0
- package/config/agent-dags.json +16 -0
- package/config/agents.json +46 -0
- package/config/bot-prompt.json +3 -0
- package/config/folders.json +66 -0
- package/config/prompts.json +92 -0
- package/config/repos.json +86 -0
- package/config/telegram-config.json +17 -0
- package/config/workflows.json +90 -0
- package/db.js +1198 -0
- package/package.json +55 -0
- package/plugins/claude-editor/client.css +171 -0
- package/plugins/claude-editor/client.js +183 -0
- package/plugins/event-stream/client.css +207 -0
- package/plugins/event-stream/client.js +271 -0
- package/plugins/linear/client.css +345 -0
- package/plugins/linear/client.js +380 -0
- package/plugins/linear/config.json +5 -0
- package/plugins/linear/server.js +312 -0
- package/plugins/repos/client.css +549 -0
- package/plugins/repos/client.js +663 -0
- package/plugins/repos/server.js +232 -0
- package/plugins/sudoku/client.css +196 -0
- package/plugins/sudoku/client.js +329 -0
- package/plugins/tasks/client.css +414 -0
- package/plugins/tasks/client.js +394 -0
- package/plugins/tasks/server.js +116 -0
- package/plugins/tic-tac-toe/client.css +167 -0
- package/plugins/tic-tac-toe/client.js +241 -0
- package/public/css/core/components.css +232 -0
- package/public/css/core/layout.css +330 -0
- package/public/css/core/print.css +18 -0
- package/public/css/core/reset.css +36 -0
- package/public/css/core/responsive.css +378 -0
- package/public/css/core/theme.css +116 -0
- package/public/css/core/variables.css +93 -0
- package/public/css/features/agent-monitor.css +297 -0
- package/public/css/features/agent-sidebar.css +525 -0
- package/public/css/features/agents.css +996 -0
- package/public/css/features/analytics.css +181 -0
- package/public/css/features/background-sessions.css +321 -0
- package/public/css/features/cost-dashboard.css +168 -0
- package/public/css/features/home.css +313 -0
- package/public/css/features/retro-terminal.css +88 -0
- package/public/css/features/telegram.css +127 -0
- package/public/css/features/tour.css +148 -0
- package/public/css/features/voice-input.css +60 -0
- package/public/css/features/welcome.css +241 -0
- package/public/css/panels/assistant-bot.css +442 -0
- package/public/css/panels/dev-docs.css +292 -0
- package/public/css/panels/file-explorer.css +322 -0
- package/public/css/panels/git-panel.css +221 -0
- package/public/css/panels/mcp-manager.css +199 -0
- package/public/css/panels/tips-feed.css +353 -0
- package/public/css/ui/commands.css +273 -0
- package/public/css/ui/context-gauge.css +76 -0
- package/public/css/ui/file-picker.css +69 -0
- package/public/css/ui/image-attachments.css +106 -0
- package/public/css/ui/messages.css +884 -0
- package/public/css/ui/modals.css +122 -0
- package/public/css/ui/parallel.css +217 -0
- package/public/css/ui/permissions.css +110 -0
- package/public/css/ui/right-panel.css +481 -0
- package/public/css/ui/sessions.css +689 -0
- package/public/css/ui/status-bar.css +425 -0
- package/public/css/ui/toolbox.css +206 -0
- package/public/data/tips.json +218 -0
- package/public/icons/favicon.png +0 -0
- package/public/icons/icon-192.png +0 -0
- package/public/icons/icon-512.png +0 -0
- package/public/icons/whaly.png +0 -0
- package/public/index.html +1140 -0
- package/public/js/core/api.js +591 -0
- package/public/js/core/constants.js +3 -0
- package/public/js/core/dom.js +270 -0
- package/public/js/core/events.js +10 -0
- package/public/js/core/plugin-loader.js +153 -0
- package/public/js/core/store.js +39 -0
- package/public/js/core/utils.js +25 -0
- package/public/js/core/ws.js +64 -0
- package/public/js/features/agent-monitor.js +222 -0
- package/public/js/features/agents.js +1209 -0
- package/public/js/features/analytics.js +397 -0
- package/public/js/features/attachments.js +251 -0
- package/public/js/features/background-sessions.js +475 -0
- package/public/js/features/chat.js +589 -0
- package/public/js/features/cost-dashboard.js +152 -0
- package/public/js/features/dag-editor.js +399 -0
- package/public/js/features/easter-egg.js +46 -0
- package/public/js/features/home.js +270 -0
- package/public/js/features/projects.js +372 -0
- package/public/js/features/prompts.js +228 -0
- package/public/js/features/sessions.js +332 -0
- package/public/js/features/telegram.js +131 -0
- package/public/js/features/tour.js +210 -0
- package/public/js/features/voice-input.js +185 -0
- package/public/js/features/welcome.js +43 -0
- package/public/js/features/workflows.js +277 -0
- package/public/js/main.js +51 -0
- package/public/js/panels/assistant-bot.js +445 -0
- package/public/js/panels/dev-docs.js +380 -0
- package/public/js/panels/file-explorer.js +486 -0
- package/public/js/panels/git-panel.js +285 -0
- package/public/js/panels/mcp-manager.js +311 -0
- package/public/js/panels/tips-feed.js +303 -0
- package/public/js/ui/commands.js +114 -0
- package/public/js/ui/context-gauge.js +100 -0
- package/public/js/ui/diff.js +124 -0
- package/public/js/ui/disabled-tools.js +36 -0
- package/public/js/ui/export.js +74 -0
- package/public/js/ui/formatting.js +206 -0
- package/public/js/ui/header-dropdowns.js +72 -0
- package/public/js/ui/input-meta.js +71 -0
- package/public/js/ui/max-turns.js +21 -0
- package/public/js/ui/messages.js +387 -0
- package/public/js/ui/model-selector.js +20 -0
- package/public/js/ui/notifications.js +232 -0
- package/public/js/ui/parallel.js +176 -0
- package/public/js/ui/permissions.js +168 -0
- package/public/js/ui/right-panel.js +173 -0
- package/public/js/ui/shortcuts.js +143 -0
- package/public/js/ui/sidebar-toggle.js +29 -0
- package/public/js/ui/status-bar.js +172 -0
- package/public/js/ui/tab-sdk.js +623 -0
- package/public/js/ui/theme.js +38 -0
- package/public/manifest.json +13 -0
- package/public/offline.html +190 -0
- package/public/style.css +42 -0
- package/public/sw.js +91 -0
- package/server/agent-loop.js +385 -0
- package/server/dag-executor.js +265 -0
- package/server/orchestrator.js +514 -0
- package/server/paths.js +61 -0
- package/server/plugin-mount.js +56 -0
- package/server/push-sender.js +31 -0
- package/server/routes/agents.js +294 -0
- package/server/routes/bot.js +45 -0
- package/server/routes/exec.js +35 -0
- package/server/routes/files.js +218 -0
- package/server/routes/mcp.js +82 -0
- package/server/routes/messages.js +36 -0
- package/server/routes/notifications.js +37 -0
- package/server/routes/projects.js +207 -0
- package/server/routes/prompts.js +53 -0
- package/server/routes/sessions.js +103 -0
- package/server/routes/stats.js +143 -0
- package/server/routes/telegram.js +71 -0
- package/server/routes/tips.js +135 -0
- package/server/routes/workflows.js +81 -0
- package/server/summarizer.js +55 -0
- package/server/telegram-poller.js +205 -0
- package/server/telegram-sender.js +304 -0
- package/server/ws-handler.js +926 -0
- 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(/</g, "<").replace(/>/g, ">").replace(/&/g, "&")
|
|
39
|
+
.replace(/"/g, '"').replace(/'/g, "'").replace(/—/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
|
+
}
|