clay-server 2.5.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 +281 -0
- package/bin/cli.js +2385 -0
- package/lib/cli-sessions.js +270 -0
- package/lib/config.js +237 -0
- package/lib/daemon.js +489 -0
- package/lib/ipc.js +112 -0
- package/lib/notes.js +120 -0
- package/lib/pages.js +664 -0
- package/lib/project.js +1433 -0
- package/lib/public/app.js +2795 -0
- package/lib/public/apple-touch-icon-dark.png +0 -0
- package/lib/public/apple-touch-icon.png +0 -0
- package/lib/public/css/base.css +264 -0
- package/lib/public/css/diff.css +128 -0
- package/lib/public/css/filebrowser.css +1114 -0
- package/lib/public/css/highlight.css +144 -0
- package/lib/public/css/icon-strip.css +296 -0
- package/lib/public/css/input.css +573 -0
- package/lib/public/css/menus.css +856 -0
- package/lib/public/css/messages.css +1445 -0
- package/lib/public/css/mobile-nav.css +354 -0
- package/lib/public/css/overlays.css +697 -0
- package/lib/public/css/rewind.css +505 -0
- package/lib/public/css/server-settings.css +761 -0
- package/lib/public/css/sidebar.css +936 -0
- package/lib/public/css/sticky-notes.css +358 -0
- package/lib/public/css/title-bar.css +314 -0
- package/lib/public/favicon-dark.svg +1 -0
- package/lib/public/favicon.svg +1 -0
- package/lib/public/icon-192-dark.png +0 -0
- package/lib/public/icon-192.png +0 -0
- package/lib/public/icon-512-dark.png +0 -0
- package/lib/public/icon-512.png +0 -0
- package/lib/public/icon-mono.svg +1 -0
- package/lib/public/index.html +762 -0
- package/lib/public/manifest.json +27 -0
- package/lib/public/modules/diff.js +398 -0
- package/lib/public/modules/events.js +21 -0
- package/lib/public/modules/filebrowser.js +1411 -0
- package/lib/public/modules/fileicons.js +172 -0
- package/lib/public/modules/icons.js +54 -0
- package/lib/public/modules/input.js +584 -0
- package/lib/public/modules/markdown.js +356 -0
- package/lib/public/modules/notifications.js +649 -0
- package/lib/public/modules/qrcode.js +70 -0
- package/lib/public/modules/rewind.js +345 -0
- package/lib/public/modules/server-settings.js +510 -0
- package/lib/public/modules/sidebar.js +1083 -0
- package/lib/public/modules/state.js +3 -0
- package/lib/public/modules/sticky-notes.js +688 -0
- package/lib/public/modules/terminal.js +697 -0
- package/lib/public/modules/theme.js +738 -0
- package/lib/public/modules/tools.js +1608 -0
- package/lib/public/modules/utils.js +56 -0
- package/lib/public/style.css +15 -0
- package/lib/public/sw.js +75 -0
- package/lib/push.js +124 -0
- package/lib/sdk-bridge.js +989 -0
- package/lib/server.js +582 -0
- package/lib/sessions.js +424 -0
- package/lib/terminal-manager.js +187 -0
- package/lib/terminal.js +24 -0
- package/lib/themes/ayu-light.json +9 -0
- package/lib/themes/catppuccin-latte.json +9 -0
- package/lib/themes/catppuccin-mocha.json +9 -0
- package/lib/themes/clay-light.json +10 -0
- package/lib/themes/clay.json +10 -0
- package/lib/themes/dracula.json +9 -0
- package/lib/themes/everforest-light.json +9 -0
- package/lib/themes/everforest.json +9 -0
- package/lib/themes/github-light.json +9 -0
- package/lib/themes/gruvbox-dark.json +9 -0
- package/lib/themes/gruvbox-light.json +9 -0
- package/lib/themes/monokai.json +9 -0
- package/lib/themes/nord-light.json +9 -0
- package/lib/themes/nord.json +9 -0
- package/lib/themes/one-dark.json +9 -0
- package/lib/themes/one-light.json +9 -0
- package/lib/themes/rose-pine-dawn.json +9 -0
- package/lib/themes/rose-pine.json +9 -0
- package/lib/themes/solarized-dark.json +9 -0
- package/lib/themes/solarized-light.json +9 -0
- package/lib/themes/tokyo-night-light.json +9 -0
- package/lib/themes/tokyo-night.json +9 -0
- package/lib/updater.js +97 -0
- package/package.json +47 -0
|
@@ -0,0 +1,989 @@
|
|
|
1
|
+
const crypto = require("crypto");
|
|
2
|
+
var fs = require("fs");
|
|
3
|
+
var path = require("path");
|
|
4
|
+
var os = require("os");
|
|
5
|
+
var { execSync } = require("child_process");
|
|
6
|
+
|
|
7
|
+
// Async message queue for streaming input to SDK
|
|
8
|
+
function createMessageQueue() {
|
|
9
|
+
var queue = [];
|
|
10
|
+
var waiting = null;
|
|
11
|
+
var ended = false;
|
|
12
|
+
return {
|
|
13
|
+
push: function(msg) {
|
|
14
|
+
if (waiting) {
|
|
15
|
+
var resolve = waiting;
|
|
16
|
+
waiting = null;
|
|
17
|
+
resolve({ value: msg, done: false });
|
|
18
|
+
} else {
|
|
19
|
+
queue.push(msg);
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
end: function() {
|
|
23
|
+
ended = true;
|
|
24
|
+
if (waiting) {
|
|
25
|
+
var resolve = waiting;
|
|
26
|
+
waiting = null;
|
|
27
|
+
resolve({ value: undefined, done: true });
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
[Symbol.asyncIterator]: function() {
|
|
31
|
+
return {
|
|
32
|
+
next: function() {
|
|
33
|
+
if (queue.length > 0) {
|
|
34
|
+
return Promise.resolve({ value: queue.shift(), done: false });
|
|
35
|
+
}
|
|
36
|
+
if (ended) {
|
|
37
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
38
|
+
}
|
|
39
|
+
return new Promise(function(resolve) {
|
|
40
|
+
waiting = resolve;
|
|
41
|
+
});
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function createSDKBridge(opts) {
|
|
49
|
+
var cwd = opts.cwd;
|
|
50
|
+
var slug = opts.slug || "";
|
|
51
|
+
var sm = opts.sessionManager; // session manager instance
|
|
52
|
+
var send = opts.send; // broadcast to all clients
|
|
53
|
+
var pushModule = opts.pushModule;
|
|
54
|
+
var getSDK = opts.getSDK;
|
|
55
|
+
var dangerouslySkipPermissions = opts.dangerouslySkipPermissions || false;
|
|
56
|
+
|
|
57
|
+
// --- Skill discovery helpers ---
|
|
58
|
+
|
|
59
|
+
function discoverSkillDirs() {
|
|
60
|
+
var skills = {};
|
|
61
|
+
var dirs = [
|
|
62
|
+
path.join(os.homedir(), ".claude", "skills"),
|
|
63
|
+
path.join(cwd, ".claude", "skills"),
|
|
64
|
+
];
|
|
65
|
+
for (var d = 0; d < dirs.length; d++) {
|
|
66
|
+
var base = dirs[d];
|
|
67
|
+
var entries;
|
|
68
|
+
try {
|
|
69
|
+
entries = fs.readdirSync(base, { withFileTypes: true });
|
|
70
|
+
} catch (e) {
|
|
71
|
+
continue; // directory doesn't exist
|
|
72
|
+
}
|
|
73
|
+
for (var i = 0; i < entries.length; i++) {
|
|
74
|
+
var entry = entries[i];
|
|
75
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
|
|
76
|
+
var skillDir = path.join(base, entry.name);
|
|
77
|
+
var skillMd = path.join(skillDir, "SKILL.md");
|
|
78
|
+
try {
|
|
79
|
+
fs.accessSync(skillMd, fs.constants.R_OK);
|
|
80
|
+
// project skills override global skills with same name
|
|
81
|
+
skills[entry.name] = skillDir;
|
|
82
|
+
} catch (e) {
|
|
83
|
+
// no SKILL.md, skip
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return skills;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function mergeSkills(sdkSkills, fsSkills) {
|
|
91
|
+
var merged = new Set();
|
|
92
|
+
if (Array.isArray(sdkSkills)) {
|
|
93
|
+
for (var i = 0; i < sdkSkills.length; i++) {
|
|
94
|
+
merged.add(sdkSkills[i]);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
var fsNames = Object.keys(fsSkills);
|
|
98
|
+
for (var i = 0; i < fsNames.length; i++) {
|
|
99
|
+
merged.add(fsNames[i]);
|
|
100
|
+
}
|
|
101
|
+
return merged;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function sendAndRecord(session, obj) {
|
|
105
|
+
sm.sendAndRecord(session, obj);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function processSDKMessage(session, parsed) {
|
|
109
|
+
// Extract session_id from any message that carries it
|
|
110
|
+
if (parsed.session_id && !session.cliSessionId) {
|
|
111
|
+
session.cliSessionId = parsed.session_id;
|
|
112
|
+
sm.saveSessionFile(session);
|
|
113
|
+
if (session.localId === sm.activeSessionId) {
|
|
114
|
+
send({ type: "session_id", cliSessionId: session.cliSessionId });
|
|
115
|
+
}
|
|
116
|
+
} else if (parsed.session_id) {
|
|
117
|
+
session.cliSessionId = parsed.session_id;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Capture message UUIDs for rewind support
|
|
121
|
+
if (parsed.uuid) {
|
|
122
|
+
if (parsed.type === "user" && !parsed.parent_tool_use_id) {
|
|
123
|
+
session.messageUUIDs.push({ uuid: parsed.uuid, type: "user", historyIndex: session.history.length });
|
|
124
|
+
sendAndRecord(session, { type: "message_uuid", uuid: parsed.uuid, messageType: "user" });
|
|
125
|
+
} else if (parsed.type === "assistant") {
|
|
126
|
+
session.messageUUIDs.push({ uuid: parsed.uuid, type: "assistant", historyIndex: session.history.length });
|
|
127
|
+
sendAndRecord(session, { type: "message_uuid", uuid: parsed.uuid, messageType: "assistant" });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Cache slash_commands and model from CLI init message
|
|
132
|
+
if (parsed.type === "system" && parsed.subtype === "init") {
|
|
133
|
+
var fsSkills = discoverSkillDirs();
|
|
134
|
+
sm.skillNames = mergeSkills(parsed.skills, fsSkills);
|
|
135
|
+
if (parsed.slash_commands) {
|
|
136
|
+
// Union: SDK slash_commands + merged skills (deduplicated)
|
|
137
|
+
var seen = new Set();
|
|
138
|
+
var combined = [];
|
|
139
|
+
var all = parsed.slash_commands.concat(Array.from(sm.skillNames));
|
|
140
|
+
for (var k = 0; k < all.length; k++) {
|
|
141
|
+
if (!seen.has(all[k])) {
|
|
142
|
+
seen.add(all[k]);
|
|
143
|
+
combined.push(all[k]);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
sm.slashCommands = combined;
|
|
147
|
+
send({ type: "slash_commands", commands: sm.slashCommands });
|
|
148
|
+
}
|
|
149
|
+
if (parsed.model) {
|
|
150
|
+
sm.currentModel = parsed.model;
|
|
151
|
+
send({ type: "model_info", model: parsed.model, models: sm.availableModels || [] });
|
|
152
|
+
}
|
|
153
|
+
if (parsed.fast_mode_state) {
|
|
154
|
+
sendAndRecord(session, { type: "fast_mode_state", state: parsed.fast_mode_state });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (parsed.type === "stream_event" && parsed.event) {
|
|
159
|
+
var evt = parsed.event;
|
|
160
|
+
|
|
161
|
+
if (evt.type === "content_block_start") {
|
|
162
|
+
var block = evt.content_block;
|
|
163
|
+
var idx = evt.index;
|
|
164
|
+
|
|
165
|
+
if (block.type === "tool_use") {
|
|
166
|
+
session.blocks[idx] = { type: "tool_use", id: block.id, name: block.name, inputJson: "" };
|
|
167
|
+
sendAndRecord(session, { type: "tool_start", id: block.id, name: block.name });
|
|
168
|
+
} else if (block.type === "thinking") {
|
|
169
|
+
session.blocks[idx] = { type: "thinking", thinkingText: "", startTime: Date.now() };
|
|
170
|
+
sendAndRecord(session, { type: "thinking_start" });
|
|
171
|
+
} else if (block.type === "text") {
|
|
172
|
+
session.blocks[idx] = { type: "text" };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (evt.type === "content_block_delta" && evt.delta) {
|
|
177
|
+
var idx = evt.index;
|
|
178
|
+
|
|
179
|
+
if (evt.delta.type === "text_delta" && typeof evt.delta.text === "string") {
|
|
180
|
+
session.streamedText = true;
|
|
181
|
+
if (session.responsePreview.length < 200) {
|
|
182
|
+
session.responsePreview += evt.delta.text;
|
|
183
|
+
}
|
|
184
|
+
sendAndRecord(session, { type: "delta", text: evt.delta.text });
|
|
185
|
+
} else if (evt.delta.type === "input_json_delta" && session.blocks[idx]) {
|
|
186
|
+
session.blocks[idx].inputJson += evt.delta.partial_json;
|
|
187
|
+
} else if (evt.delta.type === "thinking_delta" && session.blocks[idx]) {
|
|
188
|
+
session.blocks[idx].thinkingText += evt.delta.thinking;
|
|
189
|
+
sendAndRecord(session, { type: "thinking_delta", text: evt.delta.thinking });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (evt.type === "content_block_stop") {
|
|
194
|
+
var idx = evt.index;
|
|
195
|
+
var block = session.blocks[idx];
|
|
196
|
+
|
|
197
|
+
if (block && block.type === "tool_use") {
|
|
198
|
+
var input = {};
|
|
199
|
+
try { input = JSON.parse(block.inputJson); } catch {}
|
|
200
|
+
sendAndRecord(session, { type: "tool_executing", id: block.id, name: block.name, input: input });
|
|
201
|
+
|
|
202
|
+
// Track active Task tools for sub-agent done detection
|
|
203
|
+
if (block.name === "Task") {
|
|
204
|
+
if (!session.activeTaskToolIds) session.activeTaskToolIds = {};
|
|
205
|
+
session.activeTaskToolIds[block.id] = true;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (pushModule && block.name === "AskUserQuestion" && input.questions) {
|
|
209
|
+
var q = input.questions[0];
|
|
210
|
+
pushModule.sendPush({
|
|
211
|
+
type: "ask_user",
|
|
212
|
+
slug: slug,
|
|
213
|
+
title: "Claude has a question",
|
|
214
|
+
body: q ? q.question : "Waiting for your response",
|
|
215
|
+
tag: "claude-ask",
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
} else if (block && block.type === "thinking") {
|
|
219
|
+
var duration = block.startTime ? (Date.now() - block.startTime) / 1000 : 0;
|
|
220
|
+
sendAndRecord(session, { type: "thinking_stop", duration: duration });
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
delete session.blocks[idx];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
} else if ((parsed.type === "assistant" || parsed.type === "user") && parsed.message && parsed.message.content) {
|
|
227
|
+
// Sub-agent messages: extract tool_use blocks for activity display
|
|
228
|
+
if (parsed.parent_tool_use_id) {
|
|
229
|
+
processSubagentMessage(session, parsed);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
var content = parsed.message.content;
|
|
234
|
+
|
|
235
|
+
// Fallback: if assistant text wasn't streamed via deltas, send it now
|
|
236
|
+
if (parsed.type === "assistant" && !session.streamedText && Array.isArray(content)) {
|
|
237
|
+
var assistantText = content
|
|
238
|
+
.filter(function(c) { return c.type === "text"; })
|
|
239
|
+
.map(function(c) { return c.text; })
|
|
240
|
+
.join("");
|
|
241
|
+
if (assistantText) {
|
|
242
|
+
sendAndRecord(session, { type: "delta", text: assistantText });
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Check for local slash command output in user messages
|
|
247
|
+
if (parsed.type === "user") {
|
|
248
|
+
var fullText = "";
|
|
249
|
+
if (typeof content === "string") {
|
|
250
|
+
fullText = content;
|
|
251
|
+
} else if (Array.isArray(content)) {
|
|
252
|
+
fullText = content
|
|
253
|
+
.filter(function(c) { return c.type === "text"; })
|
|
254
|
+
.map(function(c) { return c.text; })
|
|
255
|
+
.join("\n");
|
|
256
|
+
}
|
|
257
|
+
if (fullText.indexOf("local-command-stdout") !== -1) {
|
|
258
|
+
var m = fullText.match(/<local-command-stdout>([\s\S]*?)<\/local-command-stdout>/);
|
|
259
|
+
if (m) {
|
|
260
|
+
sendAndRecord(session, { type: "slash_command_result", text: m[1].trim() });
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (Array.isArray(content)) {
|
|
266
|
+
for (var i = 0; i < content.length; i++) {
|
|
267
|
+
var block = content[i];
|
|
268
|
+
if (block.type === "tool_result" && !session.sentToolResults[block.tool_use_id]) {
|
|
269
|
+
// Clear active Task tool when its result arrives
|
|
270
|
+
if (session.activeTaskToolIds && session.activeTaskToolIds[block.tool_use_id]) {
|
|
271
|
+
sendAndRecord(session, {
|
|
272
|
+
type: "subagent_done",
|
|
273
|
+
parentToolId: block.tool_use_id,
|
|
274
|
+
});
|
|
275
|
+
delete session.activeTaskToolIds[block.tool_use_id];
|
|
276
|
+
}
|
|
277
|
+
var resultText = "";
|
|
278
|
+
if (typeof block.content === "string") {
|
|
279
|
+
resultText = block.content;
|
|
280
|
+
} else if (Array.isArray(block.content)) {
|
|
281
|
+
resultText = block.content
|
|
282
|
+
.filter(function(c) { return c.type === "text"; })
|
|
283
|
+
.map(function(c) { return c.text; })
|
|
284
|
+
.join("\n");
|
|
285
|
+
}
|
|
286
|
+
session.sentToolResults[block.tool_use_id] = true;
|
|
287
|
+
sendAndRecord(session, {
|
|
288
|
+
type: "tool_result",
|
|
289
|
+
id: block.tool_use_id,
|
|
290
|
+
content: resultText,
|
|
291
|
+
is_error: block.is_error || false,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
} else if (parsed.type === "result") {
|
|
298
|
+
session.blocks = {};
|
|
299
|
+
session.sentToolResults = {};
|
|
300
|
+
session.pendingPermissions = {};
|
|
301
|
+
// Record ask_user_answered for any leftover pending questions so replay pairs correctly
|
|
302
|
+
var leftoverAskIds = Object.keys(session.pendingAskUser);
|
|
303
|
+
for (var lai = 0; lai < leftoverAskIds.length; lai++) {
|
|
304
|
+
sendAndRecord(session, { type: "ask_user_answered", toolId: leftoverAskIds[lai] });
|
|
305
|
+
}
|
|
306
|
+
session.pendingAskUser = {};
|
|
307
|
+
session.activeTaskToolIds = {};
|
|
308
|
+
session.taskIdMap = {};
|
|
309
|
+
session.isProcessing = false;
|
|
310
|
+
sendAndRecord(session, {
|
|
311
|
+
type: "result",
|
|
312
|
+
cost: parsed.total_cost_usd,
|
|
313
|
+
duration: parsed.duration_ms,
|
|
314
|
+
usage: parsed.usage || null,
|
|
315
|
+
modelUsage: parsed.modelUsage || null,
|
|
316
|
+
sessionId: parsed.session_id,
|
|
317
|
+
});
|
|
318
|
+
if (parsed.fast_mode_state) {
|
|
319
|
+
sendAndRecord(session, { type: "fast_mode_state", state: parsed.fast_mode_state });
|
|
320
|
+
}
|
|
321
|
+
sendAndRecord(session, { type: "done", code: 0 });
|
|
322
|
+
if (pushModule) {
|
|
323
|
+
var preview = (session.responsePreview || "").replace(/\s+/g, " ").trim();
|
|
324
|
+
if (preview.length > 140) preview = preview.substring(0, 140) + "...";
|
|
325
|
+
pushModule.sendPush({
|
|
326
|
+
type: "done",
|
|
327
|
+
slug: slug,
|
|
328
|
+
title: session.title || "Claude",
|
|
329
|
+
body: preview || "Response ready",
|
|
330
|
+
tag: "claude-done",
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
// Reset for next turn in the same query
|
|
334
|
+
session.responsePreview = "";
|
|
335
|
+
session.streamedText = false;
|
|
336
|
+
sm.broadcastSessionList();
|
|
337
|
+
|
|
338
|
+
} else if (parsed.type === "system" && parsed.subtype === "status") {
|
|
339
|
+
if (parsed.status === "compacting") {
|
|
340
|
+
sendAndRecord(session, { type: "compacting", active: true });
|
|
341
|
+
} else if (session.compacting) {
|
|
342
|
+
sendAndRecord(session, { type: "compacting", active: false });
|
|
343
|
+
}
|
|
344
|
+
session.compacting = parsed.status === "compacting";
|
|
345
|
+
|
|
346
|
+
} else if (parsed.type === "system" && parsed.subtype === "task_started") {
|
|
347
|
+
var parentId = parsed.tool_use_id;
|
|
348
|
+
if (parentId) {
|
|
349
|
+
if (!session.taskIdMap) session.taskIdMap = {};
|
|
350
|
+
session.taskIdMap[parentId] = parsed.task_id;
|
|
351
|
+
sendAndRecord(session, {
|
|
352
|
+
type: "task_started",
|
|
353
|
+
parentToolId: parentId,
|
|
354
|
+
taskId: parsed.task_id,
|
|
355
|
+
description: parsed.description || "",
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
} else if (parsed.type === "system" && parsed.subtype === "task_progress") {
|
|
360
|
+
var parentId = parsed.tool_use_id;
|
|
361
|
+
if (parentId) {
|
|
362
|
+
sendAndRecord(session, {
|
|
363
|
+
type: "task_progress",
|
|
364
|
+
parentToolId: parentId,
|
|
365
|
+
taskId: parsed.task_id,
|
|
366
|
+
usage: parsed.usage || null,
|
|
367
|
+
lastToolName: parsed.last_tool_name || null,
|
|
368
|
+
description: parsed.description || "",
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
} else if (parsed.type === "tool_progress") {
|
|
373
|
+
// Sub-agent tool_progress: forward as activity update
|
|
374
|
+
var parentId = parsed.parent_tool_use_id;
|
|
375
|
+
if (parentId) {
|
|
376
|
+
sendAndRecord(session, {
|
|
377
|
+
type: "subagent_activity",
|
|
378
|
+
parentToolId: parentId,
|
|
379
|
+
text: parsed.content || "",
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
} else if (parsed.type === "task_notification") {
|
|
384
|
+
var parentId = parsed.parent_tool_use_id;
|
|
385
|
+
if (parentId) {
|
|
386
|
+
sendAndRecord(session, {
|
|
387
|
+
type: "subagent_done",
|
|
388
|
+
parentToolId: parentId,
|
|
389
|
+
status: parsed.status || "completed",
|
|
390
|
+
summary: parsed.summary || "",
|
|
391
|
+
usage: parsed.usage || null,
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
if (session.taskIdMap) {
|
|
395
|
+
for (var k in session.taskIdMap) {
|
|
396
|
+
if (session.taskIdMap[k] === parsed.task_id) {
|
|
397
|
+
delete session.taskIdMap[k];
|
|
398
|
+
break;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
} else if (parsed.type === "rate_limit_event" && parsed.rate_limit_info) {
|
|
404
|
+
var info = parsed.rate_limit_info;
|
|
405
|
+
if (info.status === "allowed_warning" || info.status === "rejected") {
|
|
406
|
+
sendAndRecord(session, {
|
|
407
|
+
type: "rate_limit",
|
|
408
|
+
status: info.status,
|
|
409
|
+
resetsAt: info.resetsAt ? info.resetsAt * 1000 : null,
|
|
410
|
+
rateLimitType: info.rateLimitType || null,
|
|
411
|
+
utilization: info.utilization || null,
|
|
412
|
+
isUsingOverage: info.isUsingOverage || false,
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
} else if (parsed.type === "prompt_suggestion") {
|
|
417
|
+
sendAndRecord(session, {
|
|
418
|
+
type: "prompt_suggestion",
|
|
419
|
+
suggestion: parsed.suggestion || "",
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
} else if (parsed.type && parsed.type !== "system" && parsed.type !== "user") {
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// --- Sub-agent message processing ---
|
|
427
|
+
|
|
428
|
+
function toolActivityTextForSubagent(name, input) {
|
|
429
|
+
if (name === "Bash" && input && input.description) return input.description;
|
|
430
|
+
if (name === "Read" && input && input.file_path) return "Reading " + input.file_path.split("/").pop();
|
|
431
|
+
if (name === "Edit" && input && input.file_path) return "Editing " + input.file_path.split("/").pop();
|
|
432
|
+
if (name === "Write" && input && input.file_path) return "Writing " + input.file_path.split("/").pop();
|
|
433
|
+
if (name === "Grep" && input && input.pattern) return "Searching for " + input.pattern;
|
|
434
|
+
if (name === "Glob" && input && input.pattern) return "Finding " + input.pattern;
|
|
435
|
+
if (name === "WebSearch" && input && input.query) return "Searching: " + input.query;
|
|
436
|
+
if (name === "WebFetch") return "Fetching URL...";
|
|
437
|
+
if (name === "Task" && input && input.description) return input.description;
|
|
438
|
+
return "Running " + name + "...";
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function processSubagentMessage(session, parsed) {
|
|
442
|
+
var parentId = parsed.parent_tool_use_id;
|
|
443
|
+
var content = parsed.message.content;
|
|
444
|
+
if (!Array.isArray(content)) return;
|
|
445
|
+
|
|
446
|
+
if (parsed.type === "assistant") {
|
|
447
|
+
// Extract tool_use blocks from sub-agent assistant messages
|
|
448
|
+
for (var i = 0; i < content.length; i++) {
|
|
449
|
+
var block = content[i];
|
|
450
|
+
if (block.type === "tool_use") {
|
|
451
|
+
var activityText = toolActivityTextForSubagent(block.name, block.input);
|
|
452
|
+
sendAndRecord(session, {
|
|
453
|
+
type: "subagent_tool",
|
|
454
|
+
parentToolId: parentId,
|
|
455
|
+
toolName: block.name,
|
|
456
|
+
toolId: block.id,
|
|
457
|
+
text: activityText,
|
|
458
|
+
});
|
|
459
|
+
} else if (block.type === "thinking") {
|
|
460
|
+
sendAndRecord(session, {
|
|
461
|
+
type: "subagent_activity",
|
|
462
|
+
parentToolId: parentId,
|
|
463
|
+
text: "Thinking...",
|
|
464
|
+
});
|
|
465
|
+
} else if (block.type === "text" && block.text) {
|
|
466
|
+
sendAndRecord(session, {
|
|
467
|
+
type: "subagent_activity",
|
|
468
|
+
parentToolId: parentId,
|
|
469
|
+
text: "Writing response...",
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
// user messages with parent_tool_use_id contain tool_results — skip silently
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// --- SDK query lifecycle ---
|
|
478
|
+
|
|
479
|
+
function handleCanUseTool(session, toolName, input, opts) {
|
|
480
|
+
// AskUserQuestion: wait for user answers via WebSocket
|
|
481
|
+
if (toolName === "AskUserQuestion") {
|
|
482
|
+
return new Promise(function(resolve) {
|
|
483
|
+
session.pendingAskUser[opts.toolUseID] = {
|
|
484
|
+
resolve: resolve,
|
|
485
|
+
input: input,
|
|
486
|
+
};
|
|
487
|
+
if (opts.signal) {
|
|
488
|
+
opts.signal.addEventListener("abort", function() {
|
|
489
|
+
delete session.pendingAskUser[opts.toolUseID];
|
|
490
|
+
sendAndRecord(session, { type: "ask_user_answered", toolId: opts.toolUseID });
|
|
491
|
+
resolve({ behavior: "deny", message: "Cancelled" });
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Auto-approve if tool was previously allowed for session
|
|
498
|
+
if (session.allowedTools && session.allowedTools[toolName]) {
|
|
499
|
+
return Promise.resolve({ behavior: "allow", updatedInput: input });
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Regular tool permission request: send to client and wait
|
|
503
|
+
return new Promise(function(resolve) {
|
|
504
|
+
var requestId = crypto.randomUUID();
|
|
505
|
+
session.pendingPermissions[requestId] = {
|
|
506
|
+
resolve: resolve,
|
|
507
|
+
requestId: requestId,
|
|
508
|
+
toolName: toolName,
|
|
509
|
+
toolInput: input,
|
|
510
|
+
toolUseId: opts.toolUseID,
|
|
511
|
+
decisionReason: opts.decisionReason || "",
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
var permMsg = {
|
|
515
|
+
type: "permission_request",
|
|
516
|
+
requestId: requestId,
|
|
517
|
+
toolName: toolName,
|
|
518
|
+
toolInput: input,
|
|
519
|
+
toolUseId: opts.toolUseID,
|
|
520
|
+
decisionReason: opts.decisionReason || "",
|
|
521
|
+
};
|
|
522
|
+
sendAndRecord(session, permMsg);
|
|
523
|
+
|
|
524
|
+
if (pushModule) {
|
|
525
|
+
pushModule.sendPush({
|
|
526
|
+
type: "permission_request",
|
|
527
|
+
slug: slug,
|
|
528
|
+
requestId: requestId,
|
|
529
|
+
title: permissionPushTitle(toolName, input),
|
|
530
|
+
body: permissionPushBody(toolName, input),
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (opts.signal) {
|
|
535
|
+
opts.signal.addEventListener("abort", function() {
|
|
536
|
+
delete session.pendingPermissions[requestId];
|
|
537
|
+
sendAndRecord(session, { type: "permission_cancel", requestId: requestId });
|
|
538
|
+
resolve({ behavior: "deny", message: "Request cancelled" });
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Detect running Claude Code CLI processes that may conflict with our SDK queries.
|
|
546
|
+
* Only returns processes whose cwd matches our project directory.
|
|
547
|
+
* Returns an array of { pid, command } for each conflicting process found.
|
|
548
|
+
*/
|
|
549
|
+
function findConflictingClaude() {
|
|
550
|
+
try {
|
|
551
|
+
var output = execSync("ps ax -o pid,command 2>/dev/null", { encoding: "utf8", timeout: 5000 });
|
|
552
|
+
var lines = output.trim().split("\n");
|
|
553
|
+
var candidates = [];
|
|
554
|
+
for (var i = 1; i < lines.length; i++) { // skip header
|
|
555
|
+
var line = lines[i].trim();
|
|
556
|
+
var m = line.match(/^(\d+)\s+(.+)/);
|
|
557
|
+
if (!m) continue;
|
|
558
|
+
var pid = parseInt(m[1], 10);
|
|
559
|
+
var cmd = m[2];
|
|
560
|
+
// Skip our own process
|
|
561
|
+
if (pid === process.pid) continue;
|
|
562
|
+
// Skip node processes (our daemon, dev watchers, etc.)
|
|
563
|
+
if (/\bnode\b/.test(cmd.split(/\s/)[0])) continue;
|
|
564
|
+
// Match actual claude binary (e.g. /Users/x/.claude/local/claude, /usr/local/bin/claude)
|
|
565
|
+
if (/\/claude(\s|$)/.test(cmd) || /^claude(\s|$)/.test(cmd)) {
|
|
566
|
+
candidates.push({ pid: pid, command: cmd.substring(0, 200) });
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Filter to only processes whose cwd matches our project
|
|
571
|
+
var results = [];
|
|
572
|
+
for (var j = 0; j < candidates.length; j++) {
|
|
573
|
+
var c = candidates[j];
|
|
574
|
+
try {
|
|
575
|
+
var lsof = execSync("lsof -a -p " + c.pid + " -d cwd -F n 2>/dev/null", { encoding: "utf8", timeout: 3000 });
|
|
576
|
+
// lsof -F n output: lines starting with 'n' contain the path
|
|
577
|
+
var cwdMatch = lsof.match(/\nn(.+)/);
|
|
578
|
+
if (cwdMatch && cwdMatch[1] === cwd) {
|
|
579
|
+
results.push(c);
|
|
580
|
+
}
|
|
581
|
+
} catch (e) {
|
|
582
|
+
// lsof failed — include as candidate anyway (conservative)
|
|
583
|
+
results.push(c);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
return results;
|
|
587
|
+
} catch (e) {
|
|
588
|
+
return [];
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Verify that a PID is actually a claude binary process (not arbitrary).
|
|
594
|
+
*/
|
|
595
|
+
function isClaudeProcess(pid) {
|
|
596
|
+
try {
|
|
597
|
+
var output = execSync("ps -p " + pid + " -o command= 2>/dev/null", { encoding: "utf8", timeout: 3000 }).trim();
|
|
598
|
+
return /\/claude(\s|$)/.test(output) || /^claude(\s|$)/.test(output);
|
|
599
|
+
} catch (e) {
|
|
600
|
+
return false;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
async function processQueryStream(session) {
|
|
605
|
+
try {
|
|
606
|
+
for await (var msg of session.queryInstance) {
|
|
607
|
+
processSDKMessage(session, msg);
|
|
608
|
+
}
|
|
609
|
+
} catch (err) {
|
|
610
|
+
if (session.isProcessing) {
|
|
611
|
+
session.isProcessing = false;
|
|
612
|
+
if (err.name === "AbortError" || (session.abortController && session.abortController.signal.aborted)) {
|
|
613
|
+
sendAndRecord(session, { type: "info", text: "Interrupted \u00b7 What should Claude do instead?" });
|
|
614
|
+
sendAndRecord(session, { type: "done", code: 0 });
|
|
615
|
+
} else {
|
|
616
|
+
var errDetail = err.message || String(err);
|
|
617
|
+
if (err.stderr) errDetail += "\nstderr: " + err.stderr;
|
|
618
|
+
if (err.exitCode != null) errDetail += " (exitCode: " + err.exitCode + ")";
|
|
619
|
+
console.error("[sdk-bridge] Query stream error for session " + session.localId + ":", errDetail);
|
|
620
|
+
console.error("[sdk-bridge] Stack:", err.stack || "(no stack)");
|
|
621
|
+
|
|
622
|
+
// Check for conflicting Claude processes only on exit code 1
|
|
623
|
+
var isExitCode1 = err.exitCode === 1 || (err.message && err.message.indexOf("exited with code 1") !== -1);
|
|
624
|
+
var conflicts = isExitCode1 ? findConflictingClaude() : [];
|
|
625
|
+
if (conflicts.length > 0) {
|
|
626
|
+
console.error("[sdk-bridge] Found " + conflicts.length + " conflicting Claude process(es):", conflicts.map(function(c) { return "PID " + c.pid; }).join(", "));
|
|
627
|
+
sendAndRecord(session, {
|
|
628
|
+
type: "process_conflict",
|
|
629
|
+
text: "Another Claude Code process is already running in this project.",
|
|
630
|
+
processes: conflicts,
|
|
631
|
+
});
|
|
632
|
+
} else {
|
|
633
|
+
var errLower = errDetail.toLowerCase();
|
|
634
|
+
var isContextOverflow = errLower.indexOf("prompt is too long") !== -1
|
|
635
|
+
|| errLower.indexOf("context_length") !== -1
|
|
636
|
+
|| errLower.indexOf("maximum context length") !== -1;
|
|
637
|
+
if (isContextOverflow) {
|
|
638
|
+
sendAndRecord(session, {
|
|
639
|
+
type: "context_overflow",
|
|
640
|
+
text: "Conversation too long to continue.",
|
|
641
|
+
});
|
|
642
|
+
} else {
|
|
643
|
+
sendAndRecord(session, { type: "error", text: "Claude process error: " + err.message });
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
sendAndRecord(session, { type: "done", code: 1 });
|
|
647
|
+
if (pushModule) {
|
|
648
|
+
pushModule.sendPush({
|
|
649
|
+
type: "error",
|
|
650
|
+
slug: slug,
|
|
651
|
+
title: "Connection Lost",
|
|
652
|
+
body: "Claude process disconnected: " + (err.message || "unknown error"),
|
|
653
|
+
tag: "claude-error",
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
sm.broadcastSessionList();
|
|
658
|
+
}
|
|
659
|
+
} finally {
|
|
660
|
+
session.queryInstance = null;
|
|
661
|
+
session.messageQueue = null;
|
|
662
|
+
session.abortController = null;
|
|
663
|
+
session.pendingPermissions = {};
|
|
664
|
+
session.pendingAskUser = {};
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
async function getOrCreateRewindQuery(session) {
|
|
669
|
+
if (session.queryInstance) return { query: session.queryInstance, isTemp: false, cleanup: function() {} };
|
|
670
|
+
|
|
671
|
+
var sdk;
|
|
672
|
+
try {
|
|
673
|
+
sdk = await getSDK();
|
|
674
|
+
} catch (e) {
|
|
675
|
+
send({ type: "error", text: "Failed to load Claude SDK: " + (e.message || e) });
|
|
676
|
+
throw e;
|
|
677
|
+
}
|
|
678
|
+
var mq = createMessageQueue();
|
|
679
|
+
|
|
680
|
+
var tempQuery = sdk.query({
|
|
681
|
+
prompt: mq,
|
|
682
|
+
options: {
|
|
683
|
+
cwd: cwd,
|
|
684
|
+
settingSources: ["user", "project", "local"],
|
|
685
|
+
enableFileCheckpointing: true,
|
|
686
|
+
resume: session.cliSessionId,
|
|
687
|
+
},
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
// Drain messages in background (stream stays alive until mq.end())
|
|
691
|
+
(async function() {
|
|
692
|
+
try { for await (var msg of tempQuery) {} } catch(e) {}
|
|
693
|
+
})();
|
|
694
|
+
|
|
695
|
+
return {
|
|
696
|
+
query: tempQuery,
|
|
697
|
+
isTemp: true,
|
|
698
|
+
cleanup: function() { try { mq.end(); } catch(e) {} },
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
async function startQuery(session, text, images) {
|
|
703
|
+
var sdk;
|
|
704
|
+
try {
|
|
705
|
+
sdk = await getSDK();
|
|
706
|
+
} catch (e) {
|
|
707
|
+
session.isProcessing = false;
|
|
708
|
+
send({ type: "error", text: "Failed to load Claude SDK: " + (e.message || e) });
|
|
709
|
+
sendAndRecord(session, { type: "done", code: 1 });
|
|
710
|
+
sm.broadcastSessionList();
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
session.messageQueue = createMessageQueue();
|
|
715
|
+
session.blocks = {};
|
|
716
|
+
session.sentToolResults = {};
|
|
717
|
+
session.activeTaskToolIds = {};
|
|
718
|
+
session.streamedText = false;
|
|
719
|
+
session.responsePreview = "";
|
|
720
|
+
|
|
721
|
+
// Build initial user message
|
|
722
|
+
var content = [];
|
|
723
|
+
if (images && images.length > 0) {
|
|
724
|
+
for (var i = 0; i < images.length; i++) {
|
|
725
|
+
content.push({
|
|
726
|
+
type: "image",
|
|
727
|
+
source: { type: "base64", media_type: images[i].mediaType, data: images[i].data },
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
if (text) {
|
|
732
|
+
content.push({ type: "text", text: text });
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
session.messageQueue.push({
|
|
736
|
+
type: "user",
|
|
737
|
+
message: { role: "user", content: content },
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
session.abortController = new AbortController();
|
|
741
|
+
|
|
742
|
+
var queryOptions = {
|
|
743
|
+
cwd: cwd,
|
|
744
|
+
settingSources: ["user", "project", "local"],
|
|
745
|
+
includePartialMessages: true,
|
|
746
|
+
enableFileCheckpointing: true,
|
|
747
|
+
extraArgs: { "replay-user-messages": null },
|
|
748
|
+
abortController: session.abortController,
|
|
749
|
+
promptSuggestions: true,
|
|
750
|
+
canUseTool: function(toolName, input, toolOpts) {
|
|
751
|
+
return handleCanUseTool(session, toolName, input, toolOpts);
|
|
752
|
+
},
|
|
753
|
+
};
|
|
754
|
+
|
|
755
|
+
if (sm.currentModel) {
|
|
756
|
+
queryOptions.model = sm.currentModel;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
if (sm.currentEffort) {
|
|
760
|
+
queryOptions.effort = sm.currentEffort;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
if (sm.currentBetas && sm.currentBetas.length > 0) {
|
|
764
|
+
queryOptions.betas = sm.currentBetas;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
if (dangerouslySkipPermissions) {
|
|
768
|
+
queryOptions.permissionMode = "bypassPermissions";
|
|
769
|
+
queryOptions.allowDangerouslySkipPermissions = true;
|
|
770
|
+
} else {
|
|
771
|
+
// Pass permissionMode in queryOptions at creation time to avoid race condition
|
|
772
|
+
var modeToApply = session.acceptEditsAfterStart ? "acceptEdits" : sm.currentPermissionMode;
|
|
773
|
+
if (session.acceptEditsAfterStart) delete session.acceptEditsAfterStart;
|
|
774
|
+
if (modeToApply && modeToApply !== "default") {
|
|
775
|
+
queryOptions.permissionMode = modeToApply;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
if (session.cliSessionId) {
|
|
780
|
+
queryOptions.resume = session.cliSessionId;
|
|
781
|
+
if (session.lastRewindUuid) {
|
|
782
|
+
queryOptions.resumeSessionAt = session.lastRewindUuid;
|
|
783
|
+
delete session.lastRewindUuid;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
try {
|
|
788
|
+
session.queryInstance = sdk.query({
|
|
789
|
+
prompt: session.messageQueue,
|
|
790
|
+
options: queryOptions,
|
|
791
|
+
});
|
|
792
|
+
} catch (e) {
|
|
793
|
+
console.error("[sdk-bridge] Failed to create query for session " + session.localId + ":", e.message || e);
|
|
794
|
+
console.error("[sdk-bridge] cliSessionId:", session.cliSessionId, "resume:", !!queryOptions.resume);
|
|
795
|
+
console.error("[sdk-bridge] Stack:", e.stack || "(no stack)");
|
|
796
|
+
session.isProcessing = false;
|
|
797
|
+
session.queryInstance = null;
|
|
798
|
+
session.messageQueue = null;
|
|
799
|
+
session.abortController = null;
|
|
800
|
+
send({ type: "error", text: "Failed to start query: " + (e.message || e) });
|
|
801
|
+
sendAndRecord(session, { type: "done", code: 1 });
|
|
802
|
+
sm.broadcastSessionList();
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
processQueryStream(session).catch(function(err) {
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
function pushMessage(session, text, images) {
|
|
811
|
+
var content = [];
|
|
812
|
+
if (images && images.length > 0) {
|
|
813
|
+
for (var i = 0; i < images.length; i++) {
|
|
814
|
+
content.push({
|
|
815
|
+
type: "image",
|
|
816
|
+
source: { type: "base64", media_type: images[i].mediaType, data: images[i].data },
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
if (text) {
|
|
821
|
+
content.push({ type: "text", text: text });
|
|
822
|
+
}
|
|
823
|
+
session.messageQueue.push({
|
|
824
|
+
type: "user",
|
|
825
|
+
message: { role: "user", content: content },
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
function permissionPushTitle(toolName, input) {
|
|
830
|
+
if (!input) return "Claude wants to use " + toolName;
|
|
831
|
+
var file = input.file_path ? input.file_path.split(/[/\\]/).pop() : "";
|
|
832
|
+
switch (toolName) {
|
|
833
|
+
case "Bash": return "Claude wants to run a command";
|
|
834
|
+
case "Edit": return "Claude wants to edit " + (file || "a file");
|
|
835
|
+
case "Write": return "Claude wants to write " + (file || "a file");
|
|
836
|
+
case "Read": return "Claude wants to read " + (file || "a file");
|
|
837
|
+
case "Grep": return "Claude wants to search files";
|
|
838
|
+
case "Glob": return "Claude wants to find files";
|
|
839
|
+
case "WebFetch": return "Claude wants to fetch a URL";
|
|
840
|
+
case "WebSearch": return "Claude wants to search the web";
|
|
841
|
+
case "Task": return "Claude wants to launch an agent";
|
|
842
|
+
default: return "Claude wants to use " + toolName;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
function permissionPushBody(toolName, input) {
|
|
847
|
+
if (!input) return "";
|
|
848
|
+
var text = "";
|
|
849
|
+
if (toolName === "Bash" && input.command) {
|
|
850
|
+
text = input.command;
|
|
851
|
+
} else if (toolName === "Edit" && input.file_path) {
|
|
852
|
+
text = input.file_path.split(/[/\\]/).pop() + ": " + (input.old_string || "").substring(0, 40) + " \u2192 " + (input.new_string || "").substring(0, 40);
|
|
853
|
+
} else if (toolName === "Write" && input.file_path) {
|
|
854
|
+
text = input.file_path;
|
|
855
|
+
} else if (input.file_path) {
|
|
856
|
+
text = input.file_path;
|
|
857
|
+
} else if (input.command) {
|
|
858
|
+
text = input.command;
|
|
859
|
+
} else if (input.url) {
|
|
860
|
+
text = input.url;
|
|
861
|
+
} else if (input.query) {
|
|
862
|
+
text = input.query;
|
|
863
|
+
} else if (input.pattern) {
|
|
864
|
+
text = input.pattern;
|
|
865
|
+
} else if (input.description) {
|
|
866
|
+
text = input.description;
|
|
867
|
+
}
|
|
868
|
+
if (text.length > 120) text = text.substring(0, 120) + "...";
|
|
869
|
+
return text;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// SDK warmup: grab slash_commands, model, and available models from SDK init
|
|
873
|
+
async function warmup() {
|
|
874
|
+
try {
|
|
875
|
+
var sdk = await getSDK();
|
|
876
|
+
var ac = new AbortController();
|
|
877
|
+
var mq = createMessageQueue();
|
|
878
|
+
mq.push({ type: "user", message: { role: "user", content: [{ type: "text", text: "hi" }] } });
|
|
879
|
+
mq.end();
|
|
880
|
+
var warmupOptions = { cwd: cwd, settingSources: ["user", "project", "local"], abortController: ac };
|
|
881
|
+
if (dangerouslySkipPermissions) {
|
|
882
|
+
warmupOptions.permissionMode = "bypassPermissions";
|
|
883
|
+
warmupOptions.allowDangerouslySkipPermissions = true;
|
|
884
|
+
}
|
|
885
|
+
var stream = sdk.query({
|
|
886
|
+
prompt: mq,
|
|
887
|
+
options: warmupOptions,
|
|
888
|
+
});
|
|
889
|
+
for await (var msg of stream) {
|
|
890
|
+
if (msg.type === "system" && msg.subtype === "init") {
|
|
891
|
+
var fsSkills = discoverSkillDirs();
|
|
892
|
+
sm.skillNames = mergeSkills(msg.skills, fsSkills);
|
|
893
|
+
if (msg.slash_commands) {
|
|
894
|
+
// Union: SDK slash_commands + merged skills (deduplicated)
|
|
895
|
+
var seen = new Set();
|
|
896
|
+
var combined = [];
|
|
897
|
+
var all = msg.slash_commands.concat(Array.from(sm.skillNames));
|
|
898
|
+
for (var k = 0; k < all.length; k++) {
|
|
899
|
+
if (!seen.has(all[k])) {
|
|
900
|
+
seen.add(all[k]);
|
|
901
|
+
combined.push(all[k]);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
sm.slashCommands = combined;
|
|
905
|
+
send({ type: "slash_commands", commands: sm.slashCommands });
|
|
906
|
+
}
|
|
907
|
+
if (msg.model) {
|
|
908
|
+
sm.currentModel = msg.model;
|
|
909
|
+
}
|
|
910
|
+
// Fetch available models before aborting
|
|
911
|
+
try {
|
|
912
|
+
var models = await stream.supportedModels();
|
|
913
|
+
sm.availableModels = models || [];
|
|
914
|
+
} catch (e) {}
|
|
915
|
+
send({ type: "model_info", model: sm.currentModel || "", models: sm.availableModels || [] });
|
|
916
|
+
ac.abort();
|
|
917
|
+
break;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
} catch (e) {
|
|
921
|
+
if (e && e.name !== "AbortError" && !(e.message && e.message.indexOf("aborted") !== -1)) {
|
|
922
|
+
send({ type: "error", text: "Failed to load Claude SDK: " + (e.message || e) });
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
async function setModel(session, model) {
|
|
928
|
+
if (!session.queryInstance) {
|
|
929
|
+
// No active query — just store the model for next startQuery
|
|
930
|
+
sm.currentModel = model;
|
|
931
|
+
send({ type: "model_info", model: model, models: sm.availableModels || [] });
|
|
932
|
+
send({ type: "config_state", model: sm.currentModel, mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "high", betas: sm.currentBetas || [] });
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
try {
|
|
936
|
+
await session.queryInstance.setModel(model);
|
|
937
|
+
sm.currentModel = model;
|
|
938
|
+
send({ type: "model_info", model: model, models: sm.availableModels || [] });
|
|
939
|
+
send({ type: "config_state", model: sm.currentModel, mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "high", betas: sm.currentBetas || [] });
|
|
940
|
+
} catch (e) {
|
|
941
|
+
send({ type: "error", text: "Failed to switch model: " + (e.message || e) });
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
async function setPermissionMode(session, mode) {
|
|
946
|
+
if (!session.queryInstance) {
|
|
947
|
+
// No active query — just store the mode for next startQuery
|
|
948
|
+
sm.currentPermissionMode = mode;
|
|
949
|
+
send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "high", betas: sm.currentBetas || [] });
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
try {
|
|
953
|
+
await session.queryInstance.setPermissionMode(mode);
|
|
954
|
+
sm.currentPermissionMode = mode;
|
|
955
|
+
send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "high", betas: sm.currentBetas || [] });
|
|
956
|
+
} catch (e) {
|
|
957
|
+
send({ type: "error", text: "Failed to set permission mode: " + (e.message || e) });
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
async function stopTask(taskId) {
|
|
962
|
+
var session = sm.getActiveSession();
|
|
963
|
+
if (!session || !session.queryInstance) return;
|
|
964
|
+
try {
|
|
965
|
+
await session.queryInstance.stopTask(taskId);
|
|
966
|
+
} catch (e) {
|
|
967
|
+
console.error("[sdk-bridge] stopTask error:", e.message);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
return {
|
|
972
|
+
createMessageQueue: createMessageQueue,
|
|
973
|
+
processSDKMessage: processSDKMessage,
|
|
974
|
+
handleCanUseTool: handleCanUseTool,
|
|
975
|
+
processQueryStream: processQueryStream,
|
|
976
|
+
getOrCreateRewindQuery: getOrCreateRewindQuery,
|
|
977
|
+
startQuery: startQuery,
|
|
978
|
+
pushMessage: pushMessage,
|
|
979
|
+
setModel: setModel,
|
|
980
|
+
setPermissionMode: setPermissionMode,
|
|
981
|
+
isClaudeProcess: isClaudeProcess,
|
|
982
|
+
permissionPushTitle: permissionPushTitle,
|
|
983
|
+
permissionPushBody: permissionPushBody,
|
|
984
|
+
warmup: warmup,
|
|
985
|
+
stopTask: stopTask,
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
module.exports = { createSDKBridge, createMessageQueue };
|