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,926 @@
1
+ import { query } from "@anthropic-ai/claude-code";
2
+ import { execPath } from "process";
3
+ import { existsSync } from "fs";
4
+ import { homedir } from "os";
5
+ import {
6
+ createSession,
7
+ updateClaudeSessionId,
8
+ getSession,
9
+ touchSession,
10
+ addCost,
11
+ addMessage,
12
+ getTotalCost,
13
+ setClaudeSession,
14
+ updateSessionTitle,
15
+ } from "../db.js";
16
+ import { getProjectSystemPrompt } from "./routes/projects.js";
17
+
18
+ // Map short model names to current model IDs
19
+ const MODEL_MAP = {
20
+ haiku: "claude-haiku-4-5-20251001",
21
+ sonnet: "claude-sonnet-4-6",
22
+ opus: "claude-opus-4-6",
23
+ };
24
+ function resolveModel(name) {
25
+ if (!name) return undefined;
26
+ return MODEL_MAP[name] || name;
27
+ }
28
+ import { sendPushNotification } from "./push-sender.js";
29
+ import { sendTelegramNotification, sendPermissionRequest, isEnabled as telegramEnabled, getConfig as getTelegramConfig } from "./telegram-sender.js";
30
+ import { trackApprovalMessage, markTelegramMessageResolved } from "./telegram-poller.js";
31
+ import { generateSessionSummary } from "./summarizer.js";
32
+ import { runAgent } from "./agent-loop.js";
33
+ import { runOrchestrator } from "./orchestrator.js";
34
+ import { runDag } from "./dag-executor.js";
35
+
36
+ // Tools that are read-only and safe to auto-approve in "confirmDangerous" mode
37
+ const READ_ONLY_TOOLS = new Set([
38
+ "Read", "Glob", "Grep", "WebSearch", "WebFetch", "Agent",
39
+ "TodoRead", "TaskRead", "NotebookRead", "LS", "View", "ListFiles",
40
+ "TaskList", "TaskGet",
41
+ ]);
42
+
43
+ const DEFAULT_APPROVAL_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes (web)
44
+ function getApprovalTimeoutMs() {
45
+ const cfg = getTelegramConfig();
46
+ if (telegramEnabled()) {
47
+ return (cfg.afkTimeoutMinutes || 15) * 60 * 1000;
48
+ }
49
+ return DEFAULT_APPROVAL_TIMEOUT_MS;
50
+ }
51
+
52
+ // Global tracking of active queries across all connections
53
+ // Key: sessionId, Value: Set<queryKey>
54
+ const globalActiveQueries = new Map();
55
+
56
+ function registerGlobalQuery(sessionId, queryKey) {
57
+ if (!sessionId) return;
58
+ if (!globalActiveQueries.has(sessionId)) {
59
+ globalActiveQueries.set(sessionId, new Set());
60
+ }
61
+ globalActiveQueries.get(sessionId).add(queryKey);
62
+ }
63
+
64
+ function unregisterGlobalQuery(sessionId, queryKey) {
65
+ if (!sessionId) return;
66
+ const set = globalActiveQueries.get(sessionId);
67
+ if (set) {
68
+ set.delete(queryKey);
69
+ if (set.size === 0) globalActiveQueries.delete(sessionId);
70
+ }
71
+ }
72
+
73
+ export function getActiveSessionIds() {
74
+ return [...globalActiveQueries.keys()];
75
+ }
76
+
77
+ /**
78
+ * Creates a canUseTool callback that sends permission requests over WebSocket
79
+ * AND Telegram (for AFK approval). Whichever channel responds first wins.
80
+ */
81
+ export function makeCanUseTool(ws, pendingApprovals, permissionMode, chatId, sessionTitle) {
82
+ return async (toolName, toolInput, options) => {
83
+ // Bypass mode — auto-approve everything
84
+ if (permissionMode === "bypass") {
85
+ return { behavior: "allow", updatedInput: toolInput };
86
+ }
87
+
88
+ // Confirm-dangerous mode — auto-approve read-only tools
89
+ if (permissionMode === "confirmDangerous" && READ_ONLY_TOOLS.has(toolName)) {
90
+ return { behavior: "allow", updatedInput: toolInput };
91
+ }
92
+
93
+ // Send permission request to client and wait for response
94
+ const id = crypto.randomUUID();
95
+ const payload = { type: "permission_request", id, toolName, input: toolInput };
96
+ if (chatId) payload.chatId = chatId;
97
+
98
+ if (ws.readyState !== 1) {
99
+ return { behavior: "deny", message: "WebSocket disconnected" };
100
+ }
101
+
102
+ ws.send(JSON.stringify(payload));
103
+
104
+ // Also send to Telegram for AFK approval
105
+ if (telegramEnabled()) {
106
+ sendPermissionRequest(id, toolName, toolInput, sessionTitle).then((result) => {
107
+ if (result?.result?.message_id) {
108
+ trackApprovalMessage(id, result.result.message_id, toolName);
109
+ }
110
+ }).catch(() => {});
111
+ }
112
+
113
+ const timeoutMs = getApprovalTimeoutMs();
114
+
115
+ return new Promise((resolve) => {
116
+ const timer = setTimeout(() => {
117
+ pendingApprovals.delete(id);
118
+ markTelegramMessageResolved(id, "timeout").catch(() => {});
119
+ resolve({ behavior: "deny", message: `Approval timed out (${Math.round(timeoutMs / 60000)}min)` });
120
+ }, timeoutMs);
121
+
122
+ // Clean up if aborted via signal
123
+ if (options?.signal) {
124
+ options.signal.addEventListener("abort", () => {
125
+ clearTimeout(timer);
126
+ pendingApprovals.delete(id);
127
+ markTelegramMessageResolved(id, "abort").catch(() => {});
128
+ resolve({ behavior: "deny", message: "Aborted by user" });
129
+ }, { once: true });
130
+ }
131
+
132
+ pendingApprovals.set(id, { resolve, timer, toolInput, ws });
133
+ });
134
+ };
135
+ }
136
+
137
+ // Shared SDK stream processor — deduplicates chat and workflow message parsing
138
+ async function processSdkStream(q, { ws, wsSend, sessionIds, clientSid, chatId, cwd, projectName, isWorkflow, stepLabel, workflowId, stepIndex }) {
139
+ let claudeSessionId = null;
140
+ let resolvedSid = clientSid;
141
+ let sessionModel = null;
142
+ let lastMetrics = {}; // Captured from result for Telegram notifications
143
+ const wfMeta = isWorkflow ? { workflowId: workflowId || null, stepIndex: stepIndex ?? null, stepLabel: stepLabel || null } : null;
144
+
145
+ for await (const sdkMsg of q) {
146
+ if (ws.readyState !== 1) break;
147
+
148
+ // Capture session ID from init message
149
+ if (sdkMsg.type === "system" && sdkMsg.subtype === "init") {
150
+ claudeSessionId = sdkMsg.session_id;
151
+ if (sdkMsg.model) sessionModel = sdkMsg.model;
152
+ const ourSid = clientSid || crypto.randomUUID();
153
+ resolvedSid = ourSid;
154
+
155
+ const sKey = chatId ? `${ourSid}::${chatId}` : ourSid;
156
+ sessionIds.set(sKey, claudeSessionId);
157
+
158
+ if (!getSession(ourSid)) {
159
+ createSession(ourSid, claudeSessionId, projectName || "Session", cwd || "");
160
+ if (isWorkflow) {
161
+ updateSessionTitle(ourSid, `Workflow: ${stepLabel}`);
162
+ }
163
+ } else {
164
+ updateClaudeSessionId(ourSid, claudeSessionId);
165
+ }
166
+
167
+ if (chatId) {
168
+ setClaudeSession(ourSid, chatId, claudeSessionId);
169
+ }
170
+
171
+ wsSend({ type: "session", sessionId: ourSid });
172
+
173
+ const msgText = isWorkflow ? `[${stepLabel}]` : null;
174
+ // Save user message now that we have a resolved sid
175
+ if (!isWorkflow) {
176
+ // user message saved by caller for chat; for workflow, save with step label
177
+ }
178
+ if (isWorkflow) {
179
+ addMessage(resolvedSid, "user", JSON.stringify({ text: msgText }), null, wfMeta);
180
+ }
181
+
182
+ if (!isWorkflow) {
183
+ // Auto-set session title from first user message
184
+ const existingSession = getSession(ourSid);
185
+ if (existingSession && !existingSession.title) {
186
+ // Title is set by caller
187
+ }
188
+ }
189
+ continue;
190
+ }
191
+
192
+ // Assistant message — extract text and tool_use blocks
193
+ if (sdkMsg.type === "assistant" && sdkMsg.message?.content) {
194
+ for (const block of sdkMsg.message.content) {
195
+ if (block.type === "text" && block.text) {
196
+ wsSend({ type: "text", text: block.text });
197
+ if (resolvedSid) {
198
+ addMessage(resolvedSid, "assistant", JSON.stringify({ text: block.text }), chatId || null, wfMeta);
199
+ }
200
+ } else if (block.type === "tool_use") {
201
+ wsSend({ type: "tool", id: block.id, name: block.name, input: block.input });
202
+ if (resolvedSid) {
203
+ addMessage(resolvedSid, "tool", JSON.stringify({ id: block.id, name: block.name, input: block.input }), chatId || null, wfMeta);
204
+ }
205
+ }
206
+ }
207
+ continue;
208
+ }
209
+
210
+ // Result message
211
+ if (sdkMsg.type === "result") {
212
+ if (sdkMsg.subtype === "success") {
213
+ const costUsd = sdkMsg.total_cost_usd || 0;
214
+ const durationMs = sdkMsg.duration_ms || 0;
215
+ const numTurns = sdkMsg.num_turns || 0;
216
+ const inputTokens = sdkMsg.usage?.input_tokens || 0;
217
+ const outputTokens = sdkMsg.usage?.output_tokens || 0;
218
+ const cacheReadTokens = sdkMsg.usage?.cache_read_input_tokens || 0;
219
+ const cacheCreationTokens = sdkMsg.usage?.cache_creation_input_tokens || 0;
220
+ const model = Object.keys(sdkMsg.modelUsage || {})[0] || sessionModel;
221
+ const sid = resolvedSid || [...sessionIds.entries()].find(
222
+ ([, v]) => v === claudeSessionId
223
+ )?.[0];
224
+ if (sid) {
225
+ addCost(sid, costUsd, durationMs, numTurns, inputTokens, outputTokens, { model, stopReason: "success", isError: 0, cacheReadTokens, cacheCreationTokens });
226
+ }
227
+
228
+ wsSend({
229
+ type: "result",
230
+ duration_ms: sdkMsg.duration_ms,
231
+ num_turns: sdkMsg.num_turns,
232
+ cost_usd: sdkMsg.total_cost_usd,
233
+ totalCost: getTotalCost(),
234
+ input_tokens: inputTokens,
235
+ output_tokens: outputTokens,
236
+ cache_read_tokens: cacheReadTokens,
237
+ cache_creation_tokens: cacheCreationTokens,
238
+ model,
239
+ stop_reason: "success",
240
+ });
241
+
242
+ lastMetrics = { durationMs, costUsd, inputTokens, outputTokens, model, turns: numTurns, isError: false };
243
+
244
+ if (resolvedSid) {
245
+ addMessage(resolvedSid, "result", JSON.stringify({
246
+ duration_ms: sdkMsg.duration_ms,
247
+ num_turns: sdkMsg.num_turns,
248
+ cost_usd: sdkMsg.total_cost_usd,
249
+ model,
250
+ stop_reason: "success",
251
+ }), chatId || null, wfMeta);
252
+ }
253
+ } else if (sdkMsg.subtype?.startsWith("error")) {
254
+ const errMsg = sdkMsg.errors?.join(", ") || "Unknown error";
255
+ const costUsd = sdkMsg.total_cost_usd || 0;
256
+ const durationMs = sdkMsg.duration_ms || 0;
257
+ const numTurns = sdkMsg.num_turns || 0;
258
+ const inputTokens = sdkMsg.usage?.input_tokens || 0;
259
+ const outputTokens = sdkMsg.usage?.output_tokens || 0;
260
+ const cacheReadTokens = sdkMsg.usage?.cache_read_input_tokens || 0;
261
+ const cacheCreationTokens = sdkMsg.usage?.cache_creation_input_tokens || 0;
262
+ const model = Object.keys(sdkMsg.modelUsage || {})[0] || sessionModel;
263
+ const sid = resolvedSid || [...sessionIds.entries()].find(
264
+ ([, v]) => v === claudeSessionId
265
+ )?.[0];
266
+ if (sid) {
267
+ addCost(sid, costUsd, durationMs, numTurns, inputTokens, outputTokens, { model, stopReason: sdkMsg.subtype, isError: 1, cacheReadTokens, cacheCreationTokens });
268
+ addMessage(sid, "error", JSON.stringify({ error: errMsg, subtype: sdkMsg.subtype, duration_ms: durationMs, cost_usd: costUsd, model }), chatId || null, wfMeta);
269
+ }
270
+ lastMetrics = { durationMs, costUsd, inputTokens, outputTokens, model, turns: numTurns, isError: true, error: errMsg };
271
+ wsSend({ type: "error", error: errMsg });
272
+ }
273
+ continue;
274
+ }
275
+
276
+ // User messages (tool results from Claude executing tools)
277
+ if (sdkMsg.type === "user" && sdkMsg.message?.content) {
278
+ const content = sdkMsg.message.content;
279
+ const blocks = Array.isArray(content) ? content : [];
280
+ for (const block of blocks) {
281
+ if (block.type === "tool_result") {
282
+ const text = Array.isArray(block.content)
283
+ ? block.content.map(c => c.type === "text" ? c.text : "").join("")
284
+ : typeof block.content === "string" ? block.content : "";
285
+ const wirePayload = {
286
+ toolUseId: block.tool_use_id,
287
+ content: text.slice(0, 2000),
288
+ isError: block.is_error || false,
289
+ };
290
+ wsSend({ type: "tool_result", ...wirePayload });
291
+ if (resolvedSid) {
292
+ const dbPayload = {
293
+ toolUseId: block.tool_use_id,
294
+ content: text.slice(0, 10000),
295
+ isError: block.is_error || false,
296
+ };
297
+ addMessage(resolvedSid, "tool_result", JSON.stringify(dbPayload), chatId || null, wfMeta);
298
+ }
299
+ }
300
+ }
301
+ continue;
302
+ }
303
+ }
304
+
305
+ return { claudeSessionId, resolvedSid, lastMetrics };
306
+ }
307
+
308
+ export function setupWebSocket(wss, sessionIds) {
309
+ wss.on("connection", (ws) => {
310
+ const activeQueries = new Map();
311
+ const pendingApprovals = new Map();
312
+
313
+ // Abort active queries and deny all pending approvals on disconnect
314
+ ws.on("close", () => {
315
+ // Abort all active SDK streams first (they may be blocked on approval)
316
+ for (const [, q] of activeQueries) {
317
+ q.abort();
318
+ }
319
+ activeQueries.clear();
320
+
321
+ for (const [id, { resolve, timer }] of pendingApprovals) {
322
+ clearTimeout(timer);
323
+ resolve({ behavior: "deny", message: "Client disconnected" });
324
+ }
325
+ pendingApprovals.clear();
326
+ });
327
+
328
+ ws.on("message", async (raw) => {
329
+ let msg;
330
+ try {
331
+ msg = JSON.parse(raw);
332
+ } catch {
333
+ return;
334
+ }
335
+
336
+ // Abort handler
337
+ if (msg.type === "abort") {
338
+ if (msg.chatId) {
339
+ const q = activeQueries.get(msg.chatId);
340
+ if (q) { q.abort(); activeQueries.delete(msg.chatId); }
341
+ } else {
342
+ for (const q of activeQueries.values()) q.abort();
343
+ activeQueries.clear();
344
+ }
345
+ // Also deny any pending approvals on abort
346
+ for (const [id, { resolve, timer }] of pendingApprovals) {
347
+ clearTimeout(timer);
348
+ resolve({ behavior: "deny", message: "Aborted by user" });
349
+ }
350
+ pendingApprovals.clear();
351
+ return;
352
+ }
353
+
354
+ // Permission response handler (from web UI)
355
+ if (msg.type === "permission_response") {
356
+ const pending = pendingApprovals.get(msg.id);
357
+ if (pending) {
358
+ clearTimeout(pending.timer);
359
+ pendingApprovals.delete(msg.id);
360
+ if (msg.behavior === "allow") {
361
+ pending.resolve({ behavior: "allow", updatedInput: pending.toolInput });
362
+ } else {
363
+ pending.resolve({ behavior: "deny", message: "Denied by user" });
364
+ }
365
+ // Update Telegram message to show it was resolved via web
366
+ markTelegramMessageResolved(msg.id, msg.behavior === "allow" ? "allow" : "deny").catch(() => {});
367
+ }
368
+ return;
369
+ }
370
+
371
+ // Workflow handler
372
+ if (msg.type === "workflow") {
373
+ const { workflow, cwd, sessionId: clientSid, projectName, permissionMode: wfPermMode, model: wfModel } = msg;
374
+ if (!workflow || !workflow.steps) return;
375
+
376
+ function wfSend(payload) {
377
+ if (ws.readyState !== 1) return;
378
+ ws.send(JSON.stringify(payload));
379
+ }
380
+
381
+ wfSend({ type: "workflow_started", workflow: { id: workflow.id, title: workflow.title, steps: workflow.steps.map((s) => s.label) } });
382
+
383
+ // Telegram start notification
384
+ const wfStepNames = workflow.steps.map((s, i) => ` ${i + 1}. ${s.label}`).join("\n");
385
+ sendTelegramNotification("start", "Workflow Started", `${workflow.title}\n\n${workflow.steps.length} steps:\n${wfStepNames}`);
386
+
387
+ let resumeId = clientSid ? sessionIds.get(clientSid) : undefined;
388
+ let resolvedSid = clientSid;
389
+ const wfQueryKey = `wf-${workflow.id}-${Date.now()}`;
390
+ let wfAborted = false;
391
+
392
+ for (let i = 0; i < workflow.steps.length; i++) {
393
+ if (wfAborted || ws.readyState !== 1) break;
394
+
395
+ const step = workflow.steps[i];
396
+ wfSend({ type: "workflow_step", stepIndex: i, status: "running" });
397
+
398
+ const abortController = new AbortController();
399
+ activeQueries.set(wfQueryKey, { abort: () => abortController.abort() });
400
+
401
+ const effectivePermMode = wfPermMode || "bypass";
402
+ const useBypass = effectivePermMode === "bypass";
403
+ const usePlan = effectivePermMode === "plan";
404
+ const wfCwd = (cwd && existsSync(cwd)) ? cwd : homedir();
405
+ const stepOpts = {
406
+ cwd: wfCwd,
407
+ permissionMode: usePlan ? "plan" : (useBypass ? "bypassPermissions" : "default"),
408
+ abortController,
409
+ maxTurns: 30,
410
+ executable: execPath,
411
+ };
412
+
413
+ if (!useBypass && !usePlan) {
414
+ stepOpts.canUseTool = makeCanUseTool(ws, pendingApprovals, effectivePermMode, null, `Workflow: ${workflow.title}`);
415
+ }
416
+ if (wfModel) stepOpts.model = resolveModel(wfModel);
417
+
418
+ const projectPrompt = getProjectSystemPrompt(cwd);
419
+ if (projectPrompt) stepOpts.appendSystemPrompt = projectPrompt;
420
+ if (resumeId) stepOpts.resume = resumeId;
421
+
422
+ try {
423
+ const q = query({ prompt: step.prompt, options: stepOpts });
424
+ const result = await processSdkStream(q, {
425
+ ws, wsSend: wfSend, sessionIds,
426
+ clientSid: resolvedSid, chatId: null,
427
+ cwd, projectName: projectName || "Workflow",
428
+ isWorkflow: true, stepLabel: step.label,
429
+ workflowId: workflow.id, stepIndex: i,
430
+ });
431
+
432
+ if (result.resolvedSid) resolvedSid = result.resolvedSid;
433
+ if (result.claudeSessionId) resumeId = result.claudeSessionId;
434
+
435
+ if (i === 0 && result.resolvedSid && !clientSid) {
436
+ wfSend({ type: "session", sessionId: result.resolvedSid });
437
+ }
438
+ } catch (err) {
439
+ if (err.name === "AbortError" || abortController.signal.aborted) {
440
+ wfAborted = true;
441
+ wfSend({ type: "workflow_step", stepIndex: i, status: "aborted" });
442
+ break;
443
+ }
444
+ wfSend({ type: "error", error: `Workflow step "${step.label}" failed: ${err.message}` });
445
+ sendTelegramNotification("error", "Workflow Step Failed", `${workflow.title}\n\nStep ${i + 1}/${workflow.steps.length}: ${step.label}\nError: ${err.message}`);
446
+ break;
447
+ }
448
+
449
+ wfSend({ type: "workflow_step", stepIndex: i, status: "completed" });
450
+ }
451
+
452
+ activeQueries.delete(wfQueryKey);
453
+
454
+ if (wfAborted) {
455
+ wfSend({ type: "workflow_completed", aborted: true });
456
+ wfSend({ type: "done" });
457
+ sendTelegramNotification("error", "Workflow Aborted", `${workflow.title}\nAborted during execution`);
458
+ } else {
459
+ wfSend({ type: "workflow_completed" });
460
+ wfSend({ type: "done" });
461
+ sendPushNotification("Claudeck", `Workflow "${workflow.title}" completed`, `wf-${resolvedSid}`);
462
+ const stepNames = workflow.steps.map((s, i) => ` ${i + 1}. ${s.label}`).join("\n");
463
+ sendTelegramNotification("workflow", "Workflow Completed", `${workflow.title}\n\nSteps:\n${stepNames}`, {
464
+ steps: workflow.steps.length,
465
+ });
466
+ }
467
+ return;
468
+ }
469
+
470
+ // Agent handler
471
+ if (msg.type === "agent") {
472
+ const { agentDef, cwd, sessionId: clientSid, projectName, permissionMode: agentPermMode, model: agentModel, userContext } = msg;
473
+ if (!agentDef) return;
474
+
475
+ runAgent({
476
+ ws,
477
+ agentDef,
478
+ cwd,
479
+ sessionId: clientSid,
480
+ projectName,
481
+ permissionMode: agentPermMode,
482
+ model: agentModel,
483
+ sessionIds,
484
+ pendingApprovals,
485
+ makeCanUseTool,
486
+ userContext,
487
+ activeQueries,
488
+ runType: 'single',
489
+ }).catch(() => {}); // errors already handled inside runAgent
490
+ return;
491
+ }
492
+
493
+ // Agent chain handler — sequential multi-agent execution with context passing
494
+ if (msg.type === "agent_chain") {
495
+ const { chain, agents: agentDefs, cwd, sessionId: clientSid, projectName, permissionMode: chainPermMode, model: chainModel } = msg;
496
+ if (!chain || !agentDefs?.length) return;
497
+
498
+ const runId = crypto.randomUUID();
499
+
500
+ function chainSend(payload) {
501
+ if (ws.readyState !== 1) return;
502
+ ws.send(JSON.stringify(payload));
503
+ }
504
+
505
+ chainSend({
506
+ type: "agent_chain_started",
507
+ chainId: chain.id,
508
+ runId,
509
+ title: chain.title,
510
+ agents: agentDefs.map(a => ({ id: a.id, title: a.title })),
511
+ totalSteps: agentDefs.length,
512
+ });
513
+
514
+ // Telegram start notification
515
+ const chainAgentNames = agentDefs.map((a, i) => ` ${i + 1}. ${a.title}`).join("\n");
516
+ sendTelegramNotification("start", "Chain Started", `${chain.title}\n\n${agentDefs.length} agents:\n${chainAgentNames}`);
517
+
518
+ let chainResumeId = clientSid ? sessionIds.get(clientSid) : undefined;
519
+ let resolvedSid = clientSid;
520
+
521
+ for (let i = 0; i < agentDefs.length; i++) {
522
+ const agentDef = agentDefs[i];
523
+ if (ws.readyState !== 1) break;
524
+
525
+ chainSend({
526
+ type: "agent_chain_step",
527
+ chainId: chain.id,
528
+ stepIndex: i,
529
+ agentId: agentDef.id,
530
+ agentTitle: agentDef.title,
531
+ status: "running",
532
+ });
533
+
534
+ try {
535
+ const result = await runAgent({
536
+ ws,
537
+ agentDef,
538
+ cwd,
539
+ sessionId: resolvedSid,
540
+ projectName: projectName || `Chain: ${chain.title}`,
541
+ permissionMode: chainPermMode,
542
+ model: chainModel,
543
+ sessionIds,
544
+ pendingApprovals,
545
+ makeCanUseTool,
546
+ activeQueries,
547
+ chainResumeId,
548
+ runId,
549
+ runType: 'chain',
550
+ parentRunId: chain.id,
551
+ });
552
+
553
+ if (result?.resolvedSid) resolvedSid = result.resolvedSid;
554
+ if (result?.claudeSessionId) chainResumeId = result.claudeSessionId;
555
+
556
+ chainSend({
557
+ type: "agent_chain_step",
558
+ chainId: chain.id,
559
+ stepIndex: i,
560
+ agentId: agentDef.id,
561
+ agentTitle: agentDef.title,
562
+ status: "completed",
563
+ });
564
+ } catch (err) {
565
+ chainSend({
566
+ type: "agent_chain_step",
567
+ chainId: chain.id,
568
+ stepIndex: i,
569
+ agentId: agentDef.id,
570
+ agentTitle: agentDef.title,
571
+ status: "error",
572
+ error: err.message,
573
+ });
574
+ sendTelegramNotification("error", "Chain Agent Failed", `${chain.title}\n\nAgent ${i + 1}/${agentDefs.length}: ${agentDef.title}\nError: ${err.message}`);
575
+ break;
576
+ }
577
+ }
578
+
579
+ chainSend({ type: "agent_chain_completed", chainId: chain.id, runId });
580
+ sendPushNotification("Claudeck", `Chain "${chain.title}" completed`, `chain-${resolvedSid}`);
581
+ const agentNames = agentDefs.map((a, i) => ` ${i + 1}. ${a.title}`).join("\n");
582
+ sendTelegramNotification("chain", "Chain Completed", `${chain.title}\n\nAgents:\n${agentNames}`, {
583
+ steps: agentDefs.length,
584
+ });
585
+ return;
586
+ }
587
+
588
+ // DAG handler — runs agents in dependency order with parallelism
589
+ if (msg.type === "agent_dag") {
590
+ const { dag, agents: agentDefs, cwd, sessionId: clientSid, projectName, permissionMode: dagPermMode, model: dagModel } = msg;
591
+ if (!dag || !agentDefs?.length) return;
592
+
593
+ runDag({
594
+ ws,
595
+ dag,
596
+ agents: agentDefs,
597
+ cwd,
598
+ sessionId: clientSid,
599
+ projectName,
600
+ permissionMode: dagPermMode,
601
+ model: dagModel,
602
+ sessionIds,
603
+ pendingApprovals,
604
+ makeCanUseTool,
605
+ activeQueries,
606
+ });
607
+ return;
608
+ }
609
+
610
+ // Orchestrator handler — meta-agent that decomposes tasks and delegates
611
+ if (msg.type === "orchestrate") {
612
+ const { task, cwd, sessionId: clientSid, projectName, permissionMode: orchPermMode, model: orchModel } = msg;
613
+ if (!task) return;
614
+
615
+ const { readFile } = await import("fs/promises");
616
+ const { configPath } = await import("./paths.js");
617
+ let agents;
618
+ try {
619
+ agents = JSON.parse(await readFile(configPath("agents.json"), "utf-8"));
620
+ } catch {
621
+ ws.send(JSON.stringify({ type: "error", error: "Failed to load agents" }));
622
+ return;
623
+ }
624
+
625
+ runOrchestrator({
626
+ ws,
627
+ task,
628
+ agents,
629
+ cwd,
630
+ sessionId: clientSid,
631
+ projectName,
632
+ permissionMode: orchPermMode,
633
+ model: orchModel,
634
+ sessionIds,
635
+ pendingApprovals,
636
+ makeCanUseTool,
637
+ activeQueries,
638
+ });
639
+ return;
640
+ }
641
+
642
+ // Chat handler
643
+ if (msg.type !== "chat") return;
644
+
645
+ const { message, cwd, sessionId: clientSid, projectName, chatId, permissionMode: clientPermMode, model: chatModel, maxTurns: clientMaxTurns, images, systemPrompt, disabledTools } = msg;
646
+ const queryKey = chatId || "__default__";
647
+
648
+ const sessionKey = chatId ? `${clientSid}::${chatId}` : clientSid;
649
+ const resumeId = clientSid ? sessionIds.get(sessionKey) : undefined;
650
+
651
+ if (clientSid && getSession(clientSid)) {
652
+ touchSession(clientSid);
653
+ }
654
+
655
+ const abortController = new AbortController();
656
+ const effectivePermMode = clientPermMode || "bypass";
657
+ const useBypass = effectivePermMode === "bypass";
658
+ const usePlan = effectivePermMode === "plan";
659
+ const resolvedCwd = (cwd && existsSync(cwd)) ? cwd : homedir();
660
+ const stderrChunks = [];
661
+ const effectiveMaxTurns = clientMaxTurns > 0 ? clientMaxTurns : undefined;
662
+ const opts = {
663
+ cwd: resolvedCwd,
664
+ permissionMode: usePlan ? "plan" : (useBypass ? "bypassPermissions" : "default"),
665
+ abortController,
666
+ executable: execPath,
667
+ stderr: (text) => stderrChunks.push(text),
668
+ };
669
+ if (effectiveMaxTurns) opts.maxTurns = effectiveMaxTurns;
670
+
671
+ if (!useBypass && !usePlan) {
672
+ opts.canUseTool = makeCanUseTool(ws, pendingApprovals, effectivePermMode, chatId, projectName || "Chat");
673
+ }
674
+ if (chatModel) opts.model = resolveModel(chatModel);
675
+ if (Array.isArray(disabledTools) && disabledTools.length > 0) {
676
+ opts.disallowedTools = disabledTools;
677
+ }
678
+
679
+ const projectPrompt = getProjectSystemPrompt(cwd);
680
+ if (projectPrompt) opts.appendSystemPrompt = projectPrompt;
681
+ if (systemPrompt) {
682
+ opts.appendSystemPrompt = (opts.appendSystemPrompt || '') +
683
+ (opts.appendSystemPrompt ? '\n\n' : '') + systemPrompt;
684
+ }
685
+ if (resumeId) opts.resume = resumeId;
686
+
687
+ let resolvedSid = clientSid;
688
+
689
+ function wsSend(payload) {
690
+ if (ws.readyState !== 1) return;
691
+ if (chatId) payload.chatId = chatId;
692
+ if (resolvedSid) payload.sessionId = resolvedSid;
693
+ ws.send(JSON.stringify(payload));
694
+ }
695
+
696
+ // Register for global tracking if we already know the session
697
+ if (clientSid) registerGlobalQuery(clientSid, queryKey);
698
+
699
+ function buildPrompt(text, imgs) {
700
+ if (!imgs?.length) return text;
701
+ return (async function*() {
702
+ yield {
703
+ type: "user",
704
+ message: { role: "user", content: [
705
+ { type: "text", text },
706
+ ...imgs.map(img => ({
707
+ type: "image",
708
+ source: { type: "base64", media_type: img.mimeType, data: img.data },
709
+ })),
710
+ ]},
711
+ parent_tool_use_id: null,
712
+ session_id: "",
713
+ };
714
+ })();
715
+ }
716
+
717
+ let lastChatMetrics = {};
718
+ let lastAssistantText = "";
719
+
720
+ async function runQuery(queryOpts) {
721
+ const q = query({ prompt: buildPrompt(message, images), options: queryOpts });
722
+ activeQueries.set(queryKey, { abort: () => abortController.abort() });
723
+
724
+ let claudeSessionId = null;
725
+ let sessionModel = null;
726
+
727
+ for await (const sdkMsg of q) {
728
+ if (ws.readyState !== 1) break;
729
+
730
+ if (sdkMsg.type === "system" && sdkMsg.subtype === "init") {
731
+ claudeSessionId = sdkMsg.session_id;
732
+ if (sdkMsg.model) sessionModel = sdkMsg.model;
733
+ const ourSid = clientSid || crypto.randomUUID();
734
+ resolvedSid = ourSid;
735
+
736
+ const sKey = chatId ? `${ourSid}::${chatId}` : ourSid;
737
+ sessionIds.set(sKey, claudeSessionId);
738
+
739
+ if (!getSession(ourSid)) {
740
+ createSession(ourSid, claudeSessionId, projectName || "Session", cwd || "");
741
+ } else {
742
+ updateClaudeSessionId(ourSid, claudeSessionId);
743
+ }
744
+
745
+ if (chatId) {
746
+ setClaudeSession(ourSid, chatId, claudeSessionId);
747
+ }
748
+
749
+ wsSend({ type: "session", sessionId: ourSid });
750
+ const userMsgData = { text: message };
751
+ if (images?.length) {
752
+ userMsgData.images = images.map(i => ({ name: i.name, data: i.data, mimeType: i.mimeType }));
753
+ }
754
+ addMessage(resolvedSid, "user", JSON.stringify(userMsgData), chatId || null);
755
+
756
+ // Register global query tracking now that we know the session
757
+ if (!clientSid) registerGlobalQuery(resolvedSid, queryKey);
758
+
759
+ const existingSession = getSession(ourSid);
760
+ if (existingSession && !existingSession.title) {
761
+ const title = message.slice(0, 100).split("\n")[0];
762
+ updateSessionTitle(ourSid, title);
763
+ }
764
+ continue;
765
+ }
766
+
767
+ if (sdkMsg.type === "assistant" && sdkMsg.message?.content) {
768
+ for (const block of sdkMsg.message.content) {
769
+ if (block.type === "text" && block.text) {
770
+ lastAssistantText = block.text;
771
+ wsSend({ type: "text", text: block.text });
772
+ if (resolvedSid) addMessage(resolvedSid, "assistant", JSON.stringify({ text: block.text }), chatId || null);
773
+ } else if (block.type === "tool_use") {
774
+ wsSend({ type: "tool", id: block.id, name: block.name, input: block.input });
775
+ if (resolvedSid) addMessage(resolvedSid, "tool", JSON.stringify({ id: block.id, name: block.name, input: block.input }), chatId || null);
776
+ }
777
+ }
778
+ continue;
779
+ }
780
+
781
+ if (sdkMsg.type === "result") {
782
+ if (sdkMsg.subtype === "success") {
783
+ const sid = resolvedSid || [...sessionIds.entries()].find(([, v]) => v === claudeSessionId)?.[0];
784
+ const inputTokens = sdkMsg.usage?.input_tokens || 0;
785
+ const outputTokens = sdkMsg.usage?.output_tokens || 0;
786
+ const cacheReadTokens = sdkMsg.usage?.cache_read_input_tokens || 0;
787
+ const cacheCreationTokens = sdkMsg.usage?.cache_creation_input_tokens || 0;
788
+ const model = Object.keys(sdkMsg.modelUsage || {})[0] || sessionModel;
789
+ if (sid) addCost(sid, sdkMsg.total_cost_usd || 0, sdkMsg.duration_ms || 0, sdkMsg.num_turns || 0, inputTokens, outputTokens, { model, stopReason: "success", isError: 0, cacheReadTokens, cacheCreationTokens });
790
+ wsSend({ type: "result", duration_ms: sdkMsg.duration_ms, num_turns: sdkMsg.num_turns, cost_usd: sdkMsg.total_cost_usd, totalCost: getTotalCost(), input_tokens: inputTokens, output_tokens: outputTokens, cache_read_tokens: cacheReadTokens, cache_creation_tokens: cacheCreationTokens, model, stop_reason: "success" });
791
+ lastChatMetrics = { durationMs: sdkMsg.duration_ms, costUsd: sdkMsg.total_cost_usd, inputTokens, outputTokens, model, turns: sdkMsg.num_turns, isError: false };
792
+ if (resolvedSid) addMessage(resolvedSid, "result", JSON.stringify({ duration_ms: sdkMsg.duration_ms, num_turns: sdkMsg.num_turns, cost_usd: sdkMsg.total_cost_usd, input_tokens: inputTokens, output_tokens: outputTokens, cache_read_tokens: cacheReadTokens, cache_creation_tokens: cacheCreationTokens, model, stop_reason: "success" }), chatId || null);
793
+ } else if (sdkMsg.subtype === "error_max_turns") {
794
+ // Max turns reached — treat as a normal completion with a notice
795
+ const sid = resolvedSid || [...sessionIds.entries()].find(([, v]) => v === claudeSessionId)?.[0];
796
+ const inputTokens = sdkMsg.usage?.input_tokens || 0;
797
+ const outputTokens = sdkMsg.usage?.output_tokens || 0;
798
+ const cacheReadTokens = sdkMsg.usage?.cache_read_input_tokens || 0;
799
+ const cacheCreationTokens = sdkMsg.usage?.cache_creation_input_tokens || 0;
800
+ const model = Object.keys(sdkMsg.modelUsage || {})[0] || sessionModel;
801
+ if (sid) addCost(sid, sdkMsg.total_cost_usd || 0, sdkMsg.duration_ms || 0, sdkMsg.num_turns || 0, inputTokens, outputTokens, { model, stopReason: "error_max_turns", isError: 0, cacheReadTokens, cacheCreationTokens });
802
+ wsSend({ type: "result", duration_ms: sdkMsg.duration_ms, num_turns: sdkMsg.num_turns, cost_usd: sdkMsg.total_cost_usd, totalCost: getTotalCost(), input_tokens: inputTokens, output_tokens: outputTokens, cache_read_tokens: cacheReadTokens, cache_creation_tokens: cacheCreationTokens, model, stop_reason: "error_max_turns" });
803
+ wsSend({ type: "error", error: `Reached max turns limit (${sdkMsg.num_turns}). Send another message to continue.` });
804
+ } else if (sdkMsg.subtype?.startsWith("error")) {
805
+ const errMsg = sdkMsg.errors?.join(", ") || sdkMsg.error || sdkMsg.message || "Unknown error";
806
+ console.error("SDK result error:", JSON.stringify(sdkMsg));
807
+ const costUsd = sdkMsg.total_cost_usd || 0;
808
+ const durationMs = sdkMsg.duration_ms || 0;
809
+ const numTurns = sdkMsg.num_turns || 0;
810
+ const inputTokens = sdkMsg.usage?.input_tokens || 0;
811
+ const outputTokens = sdkMsg.usage?.output_tokens || 0;
812
+ const cacheReadTokens = sdkMsg.usage?.cache_read_input_tokens || 0;
813
+ const cacheCreationTokens = sdkMsg.usage?.cache_creation_input_tokens || 0;
814
+ const model = Object.keys(sdkMsg.modelUsage || {})[0] || sessionModel;
815
+ const sid = resolvedSid || [...sessionIds.entries()].find(([, v]) => v === claudeSessionId)?.[0];
816
+ lastChatMetrics = { durationMs, costUsd, inputTokens, outputTokens, model, turns: numTurns, isError: true, error: errMsg };
817
+ if (sid) {
818
+ addCost(sid, costUsd, durationMs, numTurns, inputTokens, outputTokens, { model, stopReason: sdkMsg.subtype, isError: 1, cacheReadTokens, cacheCreationTokens });
819
+ addMessage(sid, "error", JSON.stringify({ error: errMsg, subtype: sdkMsg.subtype, duration_ms: durationMs, cost_usd: costUsd, model }), chatId || null);
820
+ }
821
+ wsSend({ type: "error", error: errMsg });
822
+ }
823
+ continue;
824
+ }
825
+
826
+ if (sdkMsg.type === "user" && sdkMsg.message?.content) {
827
+ const blocks = Array.isArray(sdkMsg.message.content) ? sdkMsg.message.content : [];
828
+ for (const block of blocks) {
829
+ if (block.type === "tool_result") {
830
+ const text = Array.isArray(block.content) ? block.content.map(c => c.type === "text" ? c.text : "").join("") : typeof block.content === "string" ? block.content : "";
831
+ const wirePayload = { toolUseId: block.tool_use_id, content: text.slice(0, 2000), isError: block.is_error || false };
832
+ wsSend({ type: "tool_result", ...wirePayload });
833
+ if (resolvedSid) {
834
+ const dbPayload = { toolUseId: block.tool_use_id, content: text.slice(0, 10000), isError: block.is_error || false };
835
+ addMessage(resolvedSid, "tool_result", JSON.stringify(dbPayload), chatId || null);
836
+ }
837
+ }
838
+ }
839
+ continue;
840
+ }
841
+ }
842
+ }
843
+
844
+ try {
845
+ await runQuery(opts);
846
+ wsSend({ type: "done" });
847
+ } catch (err) {
848
+ if (err.name === "AbortError") {
849
+ if (resolvedSid) addMessage(resolvedSid, "aborted", JSON.stringify({ timestamp: Date.now() }), chatId || null);
850
+ wsSend({ type: "aborted" });
851
+ } else {
852
+ const stderrOutput = stderrChunks.join("");
853
+ // Retry without resume if the Claude session no longer exists
854
+ if (opts.resume && stderrOutput.includes("No conversation found")) {
855
+ console.warn("Stale session", opts.resume, "— retrying without resume");
856
+ delete opts.resume;
857
+ sessionIds.delete(sessionKey);
858
+ stderrChunks.length = 0;
859
+ try {
860
+ await runQuery(opts);
861
+ wsSend({ type: "done" });
862
+ } catch (retryErr) {
863
+ if (retryErr.name === "AbortError") {
864
+ if (resolvedSid) addMessage(resolvedSid, "aborted", JSON.stringify({ timestamp: Date.now() }), chatId || null);
865
+ wsSend({ type: "aborted" });
866
+ } else {
867
+ console.error("Query retry error:", retryErr.message);
868
+ wsSend({ type: "error", error: retryErr.message });
869
+ }
870
+ }
871
+ } else {
872
+ console.error("Query error:", err.message, stderrOutput ? "\nstderr: " + stderrOutput : "");
873
+ wsSend({ type: "error", error: err.message });
874
+ }
875
+ }
876
+ } finally {
877
+ activeQueries.delete(queryKey);
878
+ unregisterGlobalQuery(resolvedSid, queryKey);
879
+ // Send push notification when query completes
880
+ const session = resolvedSid ? getSession(resolvedSid) : null;
881
+ const pushTitle = session?.title || "Session complete";
882
+ sendPushNotification("Claudeck", pushTitle, `chat-${resolvedSid}`);
883
+
884
+ // Rich Telegram notification — meaningful for AFK developer
885
+ const userQuery = (message || "").slice(0, 150).split("\n")[0];
886
+ const answerSnippet = lastAssistantText
887
+ ? lastAssistantText.slice(0, 300).replace(/\n{2,}/g, "\n")
888
+ : "";
889
+
890
+ if (lastChatMetrics.isError) {
891
+ const errorBody = [
892
+ userQuery ? `Q: ${userQuery}` : "",
893
+ `Error: ${lastChatMetrics.error || "Unknown error"}`,
894
+ ].filter(Boolean).join("\n");
895
+ sendTelegramNotification("error", "Session Failed", errorBody, {
896
+ durationMs: lastChatMetrics.durationMs,
897
+ costUsd: lastChatMetrics.costUsd,
898
+ inputTokens: lastChatMetrics.inputTokens,
899
+ outputTokens: lastChatMetrics.outputTokens,
900
+ model: lastChatMetrics.model,
901
+ });
902
+ } else {
903
+ const body = [
904
+ userQuery ? `Q: ${userQuery}` : pushTitle,
905
+ answerSnippet ? `\nA: ${answerSnippet}` : "",
906
+ ].filter(Boolean).join("\n");
907
+ sendTelegramNotification("session", "Session Complete", body, {
908
+ durationMs: lastChatMetrics.durationMs,
909
+ costUsd: lastChatMetrics.costUsd,
910
+ inputTokens: lastChatMetrics.inputTokens,
911
+ outputTokens: lastChatMetrics.outputTokens,
912
+ model: lastChatMetrics.model,
913
+ turns: lastChatMetrics.turns,
914
+ });
915
+ }
916
+
917
+ // Fire-and-forget summary generation
918
+ if (resolvedSid) {
919
+ generateSessionSummary(resolvedSid).catch(err =>
920
+ console.error("Summary generation error:", err.message)
921
+ );
922
+ }
923
+ }
924
+ });
925
+ });
926
+ }