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.
Files changed (87) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +281 -0
  3. package/bin/cli.js +2385 -0
  4. package/lib/cli-sessions.js +270 -0
  5. package/lib/config.js +237 -0
  6. package/lib/daemon.js +489 -0
  7. package/lib/ipc.js +112 -0
  8. package/lib/notes.js +120 -0
  9. package/lib/pages.js +664 -0
  10. package/lib/project.js +1433 -0
  11. package/lib/public/app.js +2795 -0
  12. package/lib/public/apple-touch-icon-dark.png +0 -0
  13. package/lib/public/apple-touch-icon.png +0 -0
  14. package/lib/public/css/base.css +264 -0
  15. package/lib/public/css/diff.css +128 -0
  16. package/lib/public/css/filebrowser.css +1114 -0
  17. package/lib/public/css/highlight.css +144 -0
  18. package/lib/public/css/icon-strip.css +296 -0
  19. package/lib/public/css/input.css +573 -0
  20. package/lib/public/css/menus.css +856 -0
  21. package/lib/public/css/messages.css +1445 -0
  22. package/lib/public/css/mobile-nav.css +354 -0
  23. package/lib/public/css/overlays.css +697 -0
  24. package/lib/public/css/rewind.css +505 -0
  25. package/lib/public/css/server-settings.css +761 -0
  26. package/lib/public/css/sidebar.css +936 -0
  27. package/lib/public/css/sticky-notes.css +358 -0
  28. package/lib/public/css/title-bar.css +314 -0
  29. package/lib/public/favicon-dark.svg +1 -0
  30. package/lib/public/favicon.svg +1 -0
  31. package/lib/public/icon-192-dark.png +0 -0
  32. package/lib/public/icon-192.png +0 -0
  33. package/lib/public/icon-512-dark.png +0 -0
  34. package/lib/public/icon-512.png +0 -0
  35. package/lib/public/icon-mono.svg +1 -0
  36. package/lib/public/index.html +762 -0
  37. package/lib/public/manifest.json +27 -0
  38. package/lib/public/modules/diff.js +398 -0
  39. package/lib/public/modules/events.js +21 -0
  40. package/lib/public/modules/filebrowser.js +1411 -0
  41. package/lib/public/modules/fileicons.js +172 -0
  42. package/lib/public/modules/icons.js +54 -0
  43. package/lib/public/modules/input.js +584 -0
  44. package/lib/public/modules/markdown.js +356 -0
  45. package/lib/public/modules/notifications.js +649 -0
  46. package/lib/public/modules/qrcode.js +70 -0
  47. package/lib/public/modules/rewind.js +345 -0
  48. package/lib/public/modules/server-settings.js +510 -0
  49. package/lib/public/modules/sidebar.js +1083 -0
  50. package/lib/public/modules/state.js +3 -0
  51. package/lib/public/modules/sticky-notes.js +688 -0
  52. package/lib/public/modules/terminal.js +697 -0
  53. package/lib/public/modules/theme.js +738 -0
  54. package/lib/public/modules/tools.js +1608 -0
  55. package/lib/public/modules/utils.js +56 -0
  56. package/lib/public/style.css +15 -0
  57. package/lib/public/sw.js +75 -0
  58. package/lib/push.js +124 -0
  59. package/lib/sdk-bridge.js +989 -0
  60. package/lib/server.js +582 -0
  61. package/lib/sessions.js +424 -0
  62. package/lib/terminal-manager.js +187 -0
  63. package/lib/terminal.js +24 -0
  64. package/lib/themes/ayu-light.json +9 -0
  65. package/lib/themes/catppuccin-latte.json +9 -0
  66. package/lib/themes/catppuccin-mocha.json +9 -0
  67. package/lib/themes/clay-light.json +10 -0
  68. package/lib/themes/clay.json +10 -0
  69. package/lib/themes/dracula.json +9 -0
  70. package/lib/themes/everforest-light.json +9 -0
  71. package/lib/themes/everforest.json +9 -0
  72. package/lib/themes/github-light.json +9 -0
  73. package/lib/themes/gruvbox-dark.json +9 -0
  74. package/lib/themes/gruvbox-light.json +9 -0
  75. package/lib/themes/monokai.json +9 -0
  76. package/lib/themes/nord-light.json +9 -0
  77. package/lib/themes/nord.json +9 -0
  78. package/lib/themes/one-dark.json +9 -0
  79. package/lib/themes/one-light.json +9 -0
  80. package/lib/themes/rose-pine-dawn.json +9 -0
  81. package/lib/themes/rose-pine.json +9 -0
  82. package/lib/themes/solarized-dark.json +9 -0
  83. package/lib/themes/solarized-light.json +9 -0
  84. package/lib/themes/tokyo-night-light.json +9 -0
  85. package/lib/themes/tokyo-night.json +9 -0
  86. package/lib/updater.js +97 -0
  87. 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 };