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,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
|
+
}
|