clay-server 2.27.0-beta.8 → 2.27.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 (72) hide show
  1. package/README.md +10 -0
  2. package/lib/daemon-projects.js +164 -0
  3. package/lib/daemon.js +13 -126
  4. package/lib/mates-identity.js +132 -0
  5. package/lib/mates-knowledge.js +113 -0
  6. package/lib/mates-prompts.js +398 -0
  7. package/lib/mates.js +40 -599
  8. package/lib/project-connection.js +2 -0
  9. package/lib/project-debate.js +19 -12
  10. package/lib/project-http.js +4 -2
  11. package/lib/project-loop.js +110 -48
  12. package/lib/project-mate-interaction.js +4 -0
  13. package/lib/project-notifications.js +210 -0
  14. package/lib/project-sessions.js +5 -2
  15. package/lib/project-user-message.js +2 -1
  16. package/lib/project.js +26 -2
  17. package/lib/public/app.js +1193 -8521
  18. package/lib/public/css/command-palette.css +14 -0
  19. package/lib/public/css/loop.css +301 -0
  20. package/lib/public/css/notifications-center.css +190 -0
  21. package/lib/public/css/rewind.css +6 -0
  22. package/lib/public/index.html +89 -35
  23. package/lib/public/modules/app-connection.js +160 -0
  24. package/lib/public/modules/app-cursors.js +473 -0
  25. package/lib/public/modules/app-debate-ui.js +389 -0
  26. package/lib/public/modules/app-dm.js +627 -0
  27. package/lib/public/modules/app-favicon.js +212 -0
  28. package/lib/public/modules/app-header.js +229 -0
  29. package/lib/public/modules/app-home-hub.js +600 -0
  30. package/lib/public/modules/app-loop-ui.js +589 -0
  31. package/lib/public/modules/app-loop-wizard.js +439 -0
  32. package/lib/public/modules/app-messages.js +1560 -0
  33. package/lib/public/modules/app-misc.js +299 -0
  34. package/lib/public/modules/app-notifications.js +372 -0
  35. package/lib/public/modules/app-panels.js +888 -0
  36. package/lib/public/modules/app-projects.js +798 -0
  37. package/lib/public/modules/app-rate-limit.js +451 -0
  38. package/lib/public/modules/app-rendering.js +597 -0
  39. package/lib/public/modules/app-skills-install.js +234 -0
  40. package/lib/public/modules/command-palette.js +27 -4
  41. package/lib/public/modules/input.js +31 -20
  42. package/lib/public/modules/scheduler-config.js +1532 -0
  43. package/lib/public/modules/scheduler-history.js +79 -0
  44. package/lib/public/modules/scheduler.js +33 -1554
  45. package/lib/public/modules/session-search.js +13 -1
  46. package/lib/public/modules/sidebar-mates.js +812 -0
  47. package/lib/public/modules/sidebar-mobile.js +1269 -0
  48. package/lib/public/modules/sidebar-projects.js +1449 -0
  49. package/lib/public/modules/sidebar-sessions.js +986 -0
  50. package/lib/public/modules/sidebar.js +232 -4591
  51. package/lib/public/modules/store.js +27 -0
  52. package/lib/public/modules/ws-ref.js +7 -0
  53. package/lib/public/style.css +1 -0
  54. package/lib/sdk-bridge.js +96 -717
  55. package/lib/sdk-message-processor.js +587 -0
  56. package/lib/sdk-message-queue.js +42 -0
  57. package/lib/sdk-skill-discovery.js +131 -0
  58. package/lib/server-admin.js +712 -0
  59. package/lib/server-auth.js +737 -0
  60. package/lib/server-dm.js +221 -0
  61. package/lib/server-mates.js +281 -0
  62. package/lib/server-palette.js +110 -0
  63. package/lib/server-settings.js +479 -0
  64. package/lib/server-skills.js +280 -0
  65. package/lib/server.js +246 -2755
  66. package/lib/sessions.js +11 -4
  67. package/lib/users-auth.js +146 -0
  68. package/lib/users-permissions.js +118 -0
  69. package/lib/users-preferences.js +210 -0
  70. package/lib/users.js +48 -398
  71. package/lib/ws-schema.js +498 -0
  72. package/package.json +1 -1
package/lib/sdk-bridge.js CHANGED
@@ -6,123 +6,9 @@ var net = require("net");
6
6
  var { execSync, spawn } = require("child_process");
7
7
  var { resolveOsUserInfo } = require("./os-users");
8
8
  var usersModule = require("./users");
9
-
10
- // Split shell command on operators (&&, ||, ;, |) while respecting quotes
11
- // and parentheses. Returns array of command segments.
12
- function splitShellSegments(cmd) {
13
- var segments = [];
14
- var current = "";
15
- var inSingle = false;
16
- var inDouble = false;
17
- var parenDepth = 0;
18
- var i = 0;
19
- while (i < cmd.length) {
20
- var ch = cmd[i];
21
-
22
- // Handle escape
23
- if (ch === "\\" && i + 1 < cmd.length && !inSingle) {
24
- current += ch + cmd[i + 1];
25
- i += 2;
26
- continue;
27
- }
28
-
29
- // Quote tracking
30
- if (ch === "'" && !inDouble) { inSingle = !inSingle; current += ch; i++; continue; }
31
- if (ch === '"' && !inSingle) { inDouble = !inDouble; current += ch; i++; continue; }
32
-
33
- // Inside quotes: no splitting
34
- if (inSingle || inDouble) { current += ch; i++; continue; }
35
-
36
- // Parentheses/subshell tracking
37
- if (ch === "(" || ch === "$" && i + 1 < cmd.length && cmd[i + 1] === "(") {
38
- parenDepth++;
39
- current += ch;
40
- i++;
41
- continue;
42
- }
43
- if (ch === ")" && parenDepth > 0) {
44
- parenDepth--;
45
- current += ch;
46
- i++;
47
- continue;
48
- }
49
-
50
- // Inside subshell: no splitting
51
- if (parenDepth > 0) { current += ch; i++; continue; }
52
-
53
- // Check for operators: &&, ||, ;, |
54
- if (ch === "&" && i + 1 < cmd.length && cmd[i + 1] === "&") {
55
- segments.push(current);
56
- current = "";
57
- i += 2;
58
- continue;
59
- }
60
- if (ch === "|" && i + 1 < cmd.length && cmd[i + 1] === "|") {
61
- segments.push(current);
62
- current = "";
63
- i += 2;
64
- continue;
65
- }
66
- if (ch === "|") {
67
- segments.push(current);
68
- current = "";
69
- i++;
70
- continue;
71
- }
72
- if (ch === ";") {
73
- segments.push(current);
74
- current = "";
75
- i++;
76
- continue;
77
- }
78
-
79
- current += ch;
80
- i++;
81
- }
82
- if (current) segments.push(current);
83
- return segments;
84
- }
85
-
86
- // Async message queue for streaming input to SDK
87
- function createMessageQueue() {
88
- var queue = [];
89
- var waiting = null;
90
- var ended = false;
91
- return {
92
- push: function(msg) {
93
- if (waiting) {
94
- var resolve = waiting;
95
- waiting = null;
96
- resolve({ value: msg, done: false });
97
- } else {
98
- queue.push(msg);
99
- }
100
- },
101
- end: function() {
102
- ended = true;
103
- if (waiting) {
104
- var resolve = waiting;
105
- waiting = null;
106
- resolve({ value: undefined, done: true });
107
- }
108
- },
109
- [Symbol.asyncIterator]: function() {
110
- return {
111
- next: function() {
112
- if (queue.length > 0) {
113
- return Promise.resolve({ value: queue.shift(), done: false });
114
- }
115
- if (ended) {
116
- return Promise.resolve({ value: undefined, done: true });
117
- }
118
- return new Promise(function(resolve) {
119
- waiting = resolve;
120
- });
121
- },
122
- };
123
- },
124
- };
125
- }
9
+ var { splitShellSegments, attachSkillDiscovery } = require("./sdk-skill-discovery");
10
+ var { createMessageQueue } = require("./sdk-message-queue");
11
+ var { attachMessageProcessor } = require("./sdk-message-processor");
126
12
 
127
13
  function createSDKBridge(opts) {
128
14
  var cwd = opts.cwd;
@@ -130,6 +16,7 @@ function createSDKBridge(opts) {
130
16
  var sm = opts.sessionManager; // session manager instance
131
17
  var send = opts.send; // broadcast to all clients
132
18
  var pushModule = opts.pushModule;
19
+ var getNotificationsModule = opts.getNotificationsModule || function () { return null; };
133
20
  var getSDK = opts.getSDK;
134
21
  var mateDisplayName = opts.mateDisplayName || "";
135
22
  var isMate = opts.isMate || (slug.indexOf("mate-") === 0);
@@ -138,596 +25,80 @@ function createSDKBridge(opts) {
138
25
  var onProcessingChanged = opts.onProcessingChanged || function () {};
139
26
  var onTurnDone = opts.onTurnDone || null;
140
27
 
141
- // --- Skill discovery helpers ---
142
-
143
- function discoverSkillDirs() {
144
- var skills = {};
145
- var dirs = [
146
- path.join(require("./config").REAL_HOME, ".claude", "skills"),
147
- path.join(cwd, ".claude", "skills"),
148
- ];
149
- for (var d = 0; d < dirs.length; d++) {
150
- var base = dirs[d];
151
- var entries;
152
- try {
153
- entries = fs.readdirSync(base, { withFileTypes: true });
154
- } catch (e) {
155
- continue; // directory doesn't exist
156
- }
157
- for (var i = 0; i < entries.length; i++) {
158
- var entry = entries[i];
159
- if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
160
- var skillDir = path.join(base, entry.name);
161
- var skillMd = path.join(skillDir, "SKILL.md");
162
- try {
163
- fs.accessSync(skillMd, fs.constants.R_OK);
164
- // project skills override global skills with same name
165
- skills[entry.name] = skillDir;
166
- } catch (e) {
167
- // no SKILL.md, skip
168
- }
169
- }
170
- }
171
- return skills;
172
- }
173
-
174
- function mergeSkills(sdkSkills, fsSkills) {
175
- var merged = new Set();
176
- if (Array.isArray(sdkSkills)) {
177
- for (var i = 0; i < sdkSkills.length; i++) {
178
- merged.add(sdkSkills[i]);
179
- }
180
- }
181
- var fsNames = Object.keys(fsSkills);
182
- for (var i = 0; i < fsNames.length; i++) {
183
- merged.add(fsNames[i]);
184
- }
185
- return merged;
186
- }
187
-
188
- function sendAndRecord(session, obj) {
189
- sm.sendAndRecord(session, obj);
190
- }
191
-
192
- function sendToSession(session, obj) {
193
- sm.sendToSession(session, obj);
194
- }
195
-
196
- function processSDKMessage(session, parsed) {
197
- // Timing: log key SDK milestones relative to query start
198
- if (session._queryStartTs) {
199
- var _elapsed = Date.now() - session._queryStartTs;
200
- if (parsed.type === "system" && parsed.subtype === "init") {
201
- console.log("[PERF] processSDKMessage: system/init +" + _elapsed + "ms");
202
- }
203
- if (parsed.type === "stream_event" && parsed.event) {
204
- if (parsed.event.type === "message_start") {
205
- console.log("[PERF] processSDKMessage: message_start (API response begun) +" + _elapsed + "ms");
206
- }
207
- if (parsed.event.type === "content_block_delta" && !session._firstTextLogged) {
208
- session._firstTextLogged = true;
209
- console.log("[PERF] processSDKMessage: FIRST content_block_delta (visible text) +" + _elapsed + "ms");
210
- }
211
- }
212
- if (parsed.type === "result") {
213
- console.log("[PERF] processSDKMessage: result +" + _elapsed + "ms");
214
- }
215
- }
216
-
217
- // Extract session_id from any message that carries it
218
- if (parsed.session_id && !session.cliSessionId) {
219
- session.cliSessionId = parsed.session_id;
220
- sm.saveSessionFile(session);
221
- sendAndRecord(session, { type: "session_id", cliSessionId: session.cliSessionId });
222
- } else if (parsed.session_id) {
223
- session.cliSessionId = parsed.session_id;
224
- }
225
-
226
- // Capture message UUIDs for rewind support
227
- if (parsed.uuid) {
228
- if (parsed.type === "user" && !parsed.parent_tool_use_id) {
229
- session.messageUUIDs.push({ uuid: parsed.uuid, type: "user", historyIndex: session.history.length });
230
- sendAndRecord(session, { type: "message_uuid", uuid: parsed.uuid, messageType: "user" });
231
- } else if (parsed.type === "assistant") {
232
- session.messageUUIDs.push({ uuid: parsed.uuid, type: "assistant", historyIndex: session.history.length });
233
- sendAndRecord(session, { type: "message_uuid", uuid: parsed.uuid, messageType: "assistant" });
234
- }
235
- }
236
-
237
- // Cache slash_commands and model from CLI init message
238
- if (parsed.type === "system" && parsed.subtype === "init") {
239
- var fsSkills = discoverSkillDirs();
240
- sm.skillNames = mergeSkills(parsed.skills, fsSkills);
241
- if (parsed.slash_commands) {
242
- // Union: SDK slash_commands + merged skills (deduplicated)
243
- var seen = new Set();
244
- var combined = [];
245
- var all = parsed.slash_commands.concat(Array.from(sm.skillNames));
246
- for (var k = 0; k < all.length; k++) {
247
- if (!seen.has(all[k])) {
248
- seen.add(all[k]);
249
- combined.push(all[k]);
250
- }
251
- }
252
- sm.slashCommands = combined;
253
- send({ type: "slash_commands", commands: sm.slashCommands });
254
- }
255
- if (parsed.model) {
256
- sm.currentModel = sm._savedDefaultModel || parsed.model;
257
- send({ type: "model_info", model: sm.currentModel, models: sm.availableModels || [] });
258
- }
259
- if (parsed.fast_mode_state) {
260
- sendAndRecord(session, { type: "fast_mode_state", state: parsed.fast_mode_state });
261
- }
262
- }
263
-
264
- if (parsed.type === "stream_event" && parsed.event) {
265
- var evt = parsed.event;
266
-
267
- if (evt.type === "message_start" && evt.message && evt.message.usage) {
268
- var u = evt.message.usage;
269
- session.lastStreamInputTokens = (u.input_tokens || 0) + (u.cache_read_input_tokens || 0);
270
- }
271
-
272
- if (evt.type === "content_block_start") {
273
- var block = evt.content_block;
274
- var idx = evt.index;
275
-
276
- if (block.type === "tool_use") {
277
- session.blocks[idx] = { type: "tool_use", id: block.id, name: block.name, inputJson: "" };
278
- sendAndRecord(session, { type: "tool_start", id: block.id, name: block.name });
279
- } else if (block.type === "thinking") {
280
- session.blocks[idx] = { type: "thinking", thinkingText: "", startTime: Date.now() };
281
- sendAndRecord(session, { type: "thinking_start" });
282
- } else if (block.type === "text") {
283
- session.blocks[idx] = { type: "text" };
284
- }
285
- }
286
-
287
- if (evt.type === "content_block_delta" && evt.delta) {
288
- var idx = evt.index;
289
-
290
- if (evt.delta.type === "text_delta" && typeof evt.delta.text === "string") {
291
- session.streamedText = true;
292
- if (session.responsePreview.length < 200) {
293
- session.responsePreview += evt.delta.text;
294
- }
295
- // Accumulate text for mate DM response
296
- if (typeof session._mateDmResponseText === "string") {
297
- session._mateDmResponseText += evt.delta.text;
298
- }
299
- sendAndRecord(session, { type: "delta", text: evt.delta.text });
300
- } else if (evt.delta.type === "input_json_delta" && session.blocks[idx]) {
301
- session.blocks[idx].inputJson += evt.delta.partial_json;
302
- } else if (evt.delta.type === "thinking_delta" && session.blocks[idx]) {
303
- session.blocks[idx].thinkingText += evt.delta.thinking;
304
- sendAndRecord(session, { type: "thinking_delta", text: evt.delta.thinking });
305
- }
306
- }
307
-
308
- if (evt.type === "content_block_stop") {
309
- var idx = evt.index;
310
- var block = session.blocks[idx];
311
-
312
- if (block && block.type === "tool_use") {
313
- var input = {};
314
- try { input = JSON.parse(block.inputJson); } catch {}
315
- sendAndRecord(session, { type: "tool_executing", id: block.id, name: block.name, input: input });
316
-
317
- // Track active Task tools for sub-agent done detection
318
- if (block.name === "Task") {
319
- if (!session.activeTaskToolIds) session.activeTaskToolIds = {};
320
- session.activeTaskToolIds[block.id] = true;
321
- }
322
-
323
- if (pushModule && block.name === "AskUserQuestion" && input.questions) {
324
- var q = input.questions[0];
325
- pushModule.sendPush({
326
- type: "ask_user",
327
- slug: slug,
328
- title: (mateDisplayName || "Claude") + " has a question",
329
- body: q ? q.question : "Waiting for your response",
330
- tag: "claude-ask",
331
- });
332
- }
333
- } else if (block && block.type === "thinking") {
334
- var duration = block.startTime ? (Date.now() - block.startTime) / 1000 : 0;
335
- sendAndRecord(session, { type: "thinking_stop", duration: duration });
336
- }
337
-
338
- delete session.blocks[idx];
339
- }
340
-
341
- } else if ((parsed.type === "assistant" || parsed.type === "user") && parsed.message && parsed.message.content) {
342
- // Sub-agent messages: extract tool_use blocks for activity display
343
- if (parsed.parent_tool_use_id) {
344
- processSubagentMessage(session, parsed);
345
- return;
346
- }
347
-
348
- var content = parsed.message.content;
349
-
350
- // Fallback: if assistant text wasn't streamed via deltas, send it now
351
- if (parsed.type === "assistant" && !session.streamedText && Array.isArray(content)) {
352
- var assistantText = content
353
- .filter(function(c) { return c.type === "text"; })
354
- .map(function(c) { return c.text; })
355
- .join("");
356
- if (assistantText) {
357
- if (session.responsePreview.length < 200) {
358
- session.responsePreview += assistantText;
359
- }
360
- sendAndRecord(session, { type: "delta", text: assistantText });
361
- }
362
- }
363
-
364
- // Check for local slash command output in user messages
365
- if (parsed.type === "user") {
366
- var fullText = "";
367
- if (typeof content === "string") {
368
- fullText = content;
369
- } else if (Array.isArray(content)) {
370
- fullText = content
371
- .filter(function(c) { return c.type === "text"; })
372
- .map(function(c) { return c.text; })
373
- .join("\n");
374
- }
375
- if (fullText.indexOf("local-command-stdout") !== -1) {
376
- var m = fullText.match(/<local-command-stdout>([\s\S]*?)<\/local-command-stdout>/);
377
- if (m) {
378
- sendAndRecord(session, { type: "slash_command_result", text: m[1].trim() });
379
- }
380
- }
381
- }
382
-
383
- if (Array.isArray(content)) {
384
- for (var i = 0; i < content.length; i++) {
385
- var block = content[i];
386
- if (block.type === "tool_result" && !session.sentToolResults[block.tool_use_id]) {
387
- // Clear active Task tool when its result arrives
388
- if (session.activeTaskToolIds && session.activeTaskToolIds[block.tool_use_id]) {
389
- sendAndRecord(session, {
390
- type: "subagent_done",
391
- parentToolId: block.tool_use_id,
392
- });
393
- delete session.activeTaskToolIds[block.tool_use_id];
394
- }
395
- var resultText = "";
396
- var resultImages = [];
397
- if (typeof block.content === "string") {
398
- resultText = block.content;
399
- } else if (Array.isArray(block.content)) {
400
- resultText = block.content
401
- .filter(function(c) { return c.type === "text"; })
402
- .map(function(c) { return c.text; })
403
- .join("\n");
404
- for (var ri = 0; ri < block.content.length; ri++) {
405
- var rc = block.content[ri];
406
- if (rc.type === "image" && rc.source) {
407
- resultImages.push({
408
- mediaType: rc.source.media_type,
409
- data: rc.source.data,
410
- });
411
- }
412
- }
413
- }
414
- session.sentToolResults[block.tool_use_id] = true;
415
- var toolResultMsg = {
416
- type: "tool_result",
417
- id: block.tool_use_id,
418
- content: resultText,
419
- is_error: block.is_error || false,
420
- };
421
- if (resultImages.length > 0) toolResultMsg.images = resultImages;
422
- sendAndRecord(session, toolResultMsg);
423
- }
424
- }
425
- }
426
-
427
- } else if (parsed.type === "result") {
428
- session.blocks = {};
429
- session.sentToolResults = {};
430
- session.pendingPermissions = {};
431
- session.pendingElicitations = {};
432
- // Record ask_user_answered for any leftover pending questions so replay pairs correctly
433
- var leftoverAskIds = Object.keys(session.pendingAskUser);
434
- for (var lai = 0; lai < leftoverAskIds.length; lai++) {
435
- sendAndRecord(session, { type: "ask_user_answered", toolId: leftoverAskIds[lai] });
436
- }
437
- session.pendingAskUser = {};
438
- session.activeTaskToolIds = {};
439
- session.taskIdMap = {};
440
- // Only clear rateLimitResetsAt on genuine success (non-zero cost).
441
- // When rate-limited, the SDK sends result with zero cost right after
442
- // rate_limit_event; clearing here would prevent auto-continue scheduling.
443
- if (parsed.total_cost_usd && parsed.total_cost_usd > 0) {
444
- session.rateLimitResetsAt = null;
445
- }
446
- console.log("[sdk-bridge] result handler: session " + session.localId + " cost=" + parsed.total_cost_usd + " rateLimitResetsAt=" + session.rateLimitResetsAt);
447
-
448
- // Handle SDK execution errors: show the error to the user instead of
449
- // silently swallowing it. These have subtype "error_during_execution".
450
- if (parsed.subtype === "error_during_execution") {
451
- var execErrors = parsed.errors || [];
452
- var execError = execErrors.length > 0
453
- ? execErrors.join("; ")
454
- : "Unknown SDK error";
455
- if (parsed.terminal_reason) execError += " (reason: " + parsed.terminal_reason + ")";
456
- console.error("[sdk-bridge] Execution error for session " + session.localId + ": " + execError);
457
- session.isProcessing = false;
458
- onProcessingChanged();
459
- sendAndRecord(session, { type: "error", text: "Claude error: " + execError });
460
- sendAndRecord(session, { type: "done", code: 1 });
461
- sm.broadcastSessionList();
462
- return;
463
- }
464
-
465
- session.isProcessing = false;
466
- onProcessingChanged();
467
- // Detect "Not logged in" scenario early for the check below
468
- var previewTrimmed = (session.responsePreview || "").trim();
469
- var isZeroCost = !parsed.total_cost_usd || parsed.total_cost_usd === 0;
470
- var isLoginPrompt = isZeroCost && previewTrimmed.length < 100
471
- && /not logged in/i.test(previewTrimmed) && /\/login/i.test(previewTrimmed);
472
- // Fetch rich context usage breakdown (fire-and-forget, non-blocking)
473
- if (session.queryInstance && typeof session.queryInstance.getContextUsage === "function") {
474
- session.queryInstance.getContextUsage().then(function(ctxUsage) {
475
- session.lastContextUsage = ctxUsage;
476
- sendToSession(session, { type: "context_usage", data: ctxUsage });
477
- }).catch(function(e) {
478
- console.error("[sdk-bridge] getContextUsage failed (non-fatal):", e.message || e);
479
- });
480
- }
481
- var lastStreamInput = session.lastStreamInputTokens || null;
482
- session.lastStreamInputTokens = null;
483
- sendAndRecord(session, {
484
- type: "result",
485
- cost: parsed.total_cost_usd,
486
- duration: parsed.duration_ms,
487
- usage: parsed.usage || null,
488
- modelUsage: parsed.modelUsage || null,
489
- sessionId: parsed.session_id,
490
- lastStreamInputTokens: lastStreamInput,
491
- });
492
- if (parsed.fast_mode_state) {
493
- sendAndRecord(session, { type: "fast_mode_state", state: parsed.fast_mode_state });
494
- }
495
- // Detect "Not logged in · Please run /login" from SDK.
496
- // This is a short canned response with zero cost, not actual AI output.
497
- if (isLoginPrompt) {
498
- var authUser = session.ownerId ? usersModule.findUserById(session.ownerId) : null;
499
- var authLinuxUser = authUser && authUser.linuxUser ? authUser.linuxUser : null;
500
- var canAutoLogin = !usersModule.isMultiUser()
501
- || !!authLinuxUser
502
- || (authUser && authUser.role === "admin");
503
- sendAndRecord(session, {
504
- type: "auth_required",
505
- text: "Claude Code is not logged in.",
506
- linuxUser: authLinuxUser,
507
- canAutoLogin: canAutoLogin,
508
- });
509
- // Reset CLI session so next query starts fresh with new auth
510
- session.cliSessionId = null;
511
- }
512
- sendAndRecord(session, { type: "done", code: 0 });
513
- if (pushModule) {
514
- var preview = (session.responsePreview || "").replace(/\s+/g, " ").trim();
515
- if (preview.length > 140) preview = preview.substring(0, 140) + "...";
516
- pushModule.sendPush({
517
- type: "done",
518
- slug: slug,
519
- title: mateDisplayName ? (mateDisplayName + " responded") : (session.title || "Claude"),
520
- body: preview || "Response ready",
521
- tag: "claude-done",
522
- });
523
- }
524
- // Reset for next turn in the same query
525
- var donePreview = session.responsePreview || "";
526
- session.responsePreview = "";
527
- session.streamedText = false;
528
- sm.broadcastSessionList();
529
- if (onTurnDone) {
530
- try { onTurnDone(session, donePreview); } catch (e) {}
531
- }
532
-
533
- } else if (parsed.type === "system" && parsed.subtype === "status") {
534
- if (parsed.status === "compacting") {
535
- sendAndRecord(session, { type: "compacting", active: true });
536
- } else if (session.compacting) {
537
- sendAndRecord(session, { type: "compacting", active: false });
538
- }
539
- session.compacting = parsed.status === "compacting";
540
-
541
- } else if (parsed.type === "system" && parsed.subtype === "task_started") {
542
- var parentId = parsed.tool_use_id;
543
- if (parentId) {
544
- if (!session.taskIdMap) session.taskIdMap = {};
545
- session.taskIdMap[parentId] = parsed.task_id;
546
- sendAndRecord(session, {
547
- type: "task_started",
548
- parentToolId: parentId,
549
- taskId: parsed.task_id,
550
- description: parsed.description || "",
551
- });
552
- }
553
-
554
- } else if (parsed.type === "system" && parsed.subtype === "task_progress") {
555
- var parentId = parsed.tool_use_id;
556
- if (parentId) {
557
- sendAndRecord(session, {
558
- type: "task_progress",
559
- parentToolId: parentId,
560
- taskId: parsed.task_id,
561
- usage: parsed.usage || null,
562
- lastToolName: parsed.last_tool_name || null,
563
- description: parsed.description || "",
564
- summary: parsed.summary || null,
565
- });
566
- }
567
-
568
- } else if (parsed.type === "tool_progress") {
569
- // Sub-agent tool_progress: forward as activity update
570
- var parentId = parsed.parent_tool_use_id;
571
- if (parentId) {
572
- sendAndRecord(session, {
573
- type: "subagent_activity",
574
- parentToolId: parentId,
575
- text: parsed.content || "",
576
- });
577
- }
578
-
579
- } else if (parsed.type === "task_notification") {
580
- var parentId = parsed.parent_tool_use_id;
581
- if (parentId) {
582
- sendAndRecord(session, {
583
- type: "subagent_done",
584
- parentToolId: parentId,
585
- status: parsed.status || "completed",
586
- summary: parsed.summary || "",
587
- usage: parsed.usage || null,
588
- });
589
- }
590
- if (session.taskIdMap) {
591
- for (var k in session.taskIdMap) {
592
- if (session.taskIdMap[k] === parsed.task_id) {
593
- delete session.taskIdMap[k];
594
- break;
28
+ // --- Idle session reaper ---
29
+ // In single-user (in-process) mode, each session's Claude child process stays
30
+ // alive between turns because the messageQueue push-stream is never ended.
31
+ // Without a reaper, processes accumulate indefinitely as users switch between
32
+ // sessions and projects. This reaper ends the messageQueue for sessions that
33
+ // have been idle for IDLE_TIMEOUT_MS, allowing processQueryStream's finally
34
+ // block to clean up the child process. Session state on disk is preserved —
35
+ // the next startQuery() call resumes with a fresh process.
36
+ var IDLE_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
37
+ var IDLE_CHECK_INTERVAL_MS = 60 * 1000; // check every 60 seconds
38
+ var _idleReaperTimer = null;
39
+
40
+ function startIdleReaper() {
41
+ if (_idleReaperTimer) return;
42
+ _idleReaperTimer = setInterval(function () {
43
+ var now = Date.now();
44
+ sm.sessions.forEach(function (session) {
45
+ // Skip sessions that are actively processing, have no query, use workers,
46
+ // or are single-turn (Ralph Loop — managed by onQueryComplete).
47
+ if (session.isProcessing) return;
48
+ if (!session.queryInstance) return;
49
+ if (session.worker) return;
50
+ if (session.singleTurn) return;
51
+ if (session.destroying) return;
52
+
53
+ var lastActivity = session.lastActivityAt || 0;
54
+ if (now - lastActivity > IDLE_TIMEOUT_MS) {
55
+ console.log("[sdk-bridge] Reaping idle session " + session.localId +
56
+ " (idle " + Math.round((now - lastActivity) / 60000) + "min)" +
57
+ (session.title ? " title=" + JSON.stringify(session.title) : ""));
58
+ // End the message queue so the for-await loop in processQueryStream
59
+ // exits naturally, triggering the finally block cleanup.
60
+ if (session.messageQueue && typeof session.messageQueue.end === "function") {
61
+ try { session.messageQueue.end(); } catch (e) {}
595
62
  }
596
63
  }
597
- }
598
-
599
- } else if (parsed.type === "rate_limit_event" && parsed.rate_limit_info) {
600
- var info = parsed.rate_limit_info;
601
- console.log("[sdk-bridge] rate_limit_event for session " + session.localId + ": status=" + info.status + " resetsAt=" + info.resetsAt + " isUsingOverage=" + info.isUsingOverage + " isProcessing=" + session.isProcessing);
602
-
603
- // Broadcast reset time for top-bar usage link
604
- if (info.rateLimitType && info.resetsAt) {
605
- send({
606
- type: "rate_limit_usage",
607
- rateLimitType: info.rateLimitType,
608
- resetsAt: info.resetsAt * 1000,
609
- status: info.status,
610
- });
611
- }
612
-
613
- // Warning/rejection handling (existing behavior)
614
- if (info.status === "allowed_warning" || info.status === "rejected") {
615
- sendAndRecord(session, {
616
- type: "rate_limit",
617
- status: info.status,
618
- resetsAt: info.resetsAt ? info.resetsAt * 1000 : null,
619
- rateLimitType: info.rateLimitType || null,
620
- utilization: info.utilization || null,
621
- isUsingOverage: info.isUsingOverage || false,
622
- });
623
- // Track rejection for auto-continue / scheduled message support
624
- if (info.status === "rejected" && info.resetsAt) {
625
- session.rateLimitResetsAt = info.resetsAt * 1000;
626
-
627
- // Schedule auto-continue immediately on rejection (don't wait for
628
- // query completion which has timing issues with worker/non-worker paths).
629
- if (!session.scheduledMessage && !session.destroying) {
630
- var acEnabled = session.onQueryComplete ||
631
- (typeof opts.getAutoContinueSetting === "function" && opts.getAutoContinueSetting(session));
632
- console.log("[sdk-bridge] rate_limit rejected: acEnabled=" + acEnabled + " overage=" + !!info.isUsingOverage + " session=" + session.localId);
633
- if (acEnabled) {
634
- session.rateLimitAutoContinuePending = true;
635
- if (info.isUsingOverage) {
636
- // Extra usage available: send continue immediately (5s delay for query to finish)
637
- console.log("[sdk-bridge] Overage available, sending immediate continue for session " + session.localId);
638
- session.rateLimitResetsAt = null;
639
- if (typeof opts.scheduleMessage === "function") {
640
- opts.scheduleMessage(session, "continue", Date.now());
641
- }
642
- } else {
643
- // No overage: schedule after rate limit resets
644
- var acResetsAt = session.rateLimitResetsAt;
645
- session.rateLimitResetsAt = null;
646
- console.log("[sdk-bridge] Scheduling auto-continue on rate limit rejection for session " + session.localId);
647
- if (typeof opts.scheduleMessage === "function") {
648
- opts.scheduleMessage(session, "continue", acResetsAt);
649
- }
650
- }
651
- }
652
- }
653
- }
654
- }
655
-
656
- } else if (parsed.type === "prompt_suggestion") {
657
- sendAndRecord(session, {
658
- type: "prompt_suggestion",
659
- suggestion: parsed.suggestion || "",
660
64
  });
661
-
662
- } else if (parsed.type === "system") {
663
- // Catch-all for unhandled system subtypes (e.g. hook-block errors).
664
- // Extract any error text and surface it in the UI.
665
- var sysText = parsed.error || parsed.message || parsed.text || "";
666
- if (!sysText && Array.isArray(parsed.content)) {
667
- sysText = parsed.content
668
- .filter(function(c) { return c.type === "text"; })
669
- .map(function(c) { return c.text; })
670
- .join("\n");
671
- }
672
- if (sysText) {
673
- console.log("[sdk-bridge] Unhandled system message (subtype=" + (parsed.subtype || "none") + "): " + sysText.substring(0, 200));
674
- sendAndRecord(session, { type: "error", text: sysText });
675
- }
676
- } else if (parsed.type && parsed.type !== "user") {
677
- }
65
+ }, IDLE_CHECK_INTERVAL_MS);
66
+ // Don't prevent process exit
67
+ if (_idleReaperTimer.unref) _idleReaperTimer.unref();
678
68
  }
679
69
 
680
- // --- Sub-agent message processing ---
681
-
682
- function toolActivityTextForSubagent(name, input) {
683
- if (name === "Bash" && input && input.description) return input.description;
684
- if (name === "Read" && input && input.file_path) return "Reading " + input.file_path.split("/").pop();
685
- if (name === "Edit" && input && input.file_path) return "Editing " + input.file_path.split("/").pop();
686
- if (name === "Write" && input && input.file_path) return "Writing " + input.file_path.split("/").pop();
687
- if (name === "Grep" && input && input.pattern) return "Searching for " + input.pattern;
688
- if (name === "Glob" && input && input.pattern) return "Finding " + input.pattern;
689
- if (name === "WebSearch" && input && input.query) return "Searching: " + input.query;
690
- if (name === "WebFetch") return "Fetching URL...";
691
- if (name === "Task" && input && input.description) return input.description;
692
- return "Running " + name + "...";
693
- }
694
-
695
- function processSubagentMessage(session, parsed) {
696
- var parentId = parsed.parent_tool_use_id;
697
- var content = parsed.message.content;
698
- if (!Array.isArray(content)) return;
699
-
700
- if (parsed.type === "assistant") {
701
- // Extract tool_use blocks from sub-agent assistant messages
702
- for (var i = 0; i < content.length; i++) {
703
- var block = content[i];
704
- if (block.type === "tool_use") {
705
- var activityText = toolActivityTextForSubagent(block.name, block.input);
706
- sendAndRecord(session, {
707
- type: "subagent_tool",
708
- parentToolId: parentId,
709
- toolName: block.name,
710
- toolId: block.id,
711
- text: activityText,
712
- });
713
- } else if (block.type === "thinking") {
714
- sendAndRecord(session, {
715
- type: "subagent_activity",
716
- parentToolId: parentId,
717
- text: "Thinking...",
718
- });
719
- } else if (block.type === "text" && block.text) {
720
- sendAndRecord(session, {
721
- type: "subagent_activity",
722
- parentToolId: parentId,
723
- text: "Writing response...",
724
- });
725
- }
726
- }
70
+ function stopIdleReaper() {
71
+ if (_idleReaperTimer) {
72
+ clearInterval(_idleReaperTimer);
73
+ _idleReaperTimer = null;
727
74
  }
728
- // user messages with parent_tool_use_id contain tool_results — skip silently
729
75
  }
730
76
 
77
+ // --- Skill discovery (extracted to sdk-skill-discovery.js) ---
78
+ var skills = attachSkillDiscovery({ cwd: cwd });
79
+ var discoverSkillDirs = skills.discoverSkillDirs;
80
+ var mergeSkills = skills.mergeSkills;
81
+
82
+ // --- Message processing (extracted to sdk-message-processor.js) ---
83
+ var msgProcessor = attachMessageProcessor({
84
+ sm: sm,
85
+ send: send,
86
+ slug: slug,
87
+ isMate: isMate,
88
+ mateDisplayName: mateDisplayName,
89
+ pushModule: pushModule,
90
+ getNotificationsModule: getNotificationsModule,
91
+ getSDK: getSDK,
92
+ onProcessingChanged: onProcessingChanged,
93
+ onTurnDone: onTurnDone,
94
+ opts: opts,
95
+ discoverSkillDirs: discoverSkillDirs,
96
+ mergeSkills: mergeSkills,
97
+ });
98
+ var processSDKMessage = msgProcessor.processSDKMessage;
99
+ var sendAndRecord = msgProcessor.sendAndRecord;
100
+ var sendToSession = msgProcessor.sendToSession;
101
+
731
102
  // --- MCP elicitation ---
732
103
 
733
104
  function handleElicitation(session, request, opts) {
@@ -1378,15 +749,6 @@ function createSDKBridge(opts) {
1378
749
  sendAndRecord(session, { type: "error", text: "Claude process error: " + errText });
1379
750
  }
1380
751
  sendAndRecord(session, { type: "done", code: 1 });
1381
- if (pushModule) {
1382
- pushModule.sendPush({
1383
- type: "error",
1384
- slug: slug,
1385
- title: (mateDisplayName || "Claude") + ": Connection Lost",
1386
- body: (mateDisplayName || "Claude") + " process disconnected: " + (msg.error || "unknown error"),
1387
- tag: "claude-error",
1388
- });
1389
- }
1390
752
  }
1391
753
  sm.broadcastSessionList();
1392
754
  }
@@ -1669,6 +1031,12 @@ function createSDKBridge(opts) {
1669
1031
  }
1670
1032
 
1671
1033
  function handleCanUseTool(session, toolName, input, opts) {
1034
+ // Full-auto mode: auto-approve everything except AskUserQuestion
1035
+ // (which still needs to go through the user interaction flow).
1036
+ if (sm.currentPermissionMode === "bypassPermissions" && toolName !== "AskUserQuestion") {
1037
+ return Promise.resolve({ behavior: "allow", updatedInput: input });
1038
+ }
1039
+
1672
1040
  // Ralph Loop execution: auto-approve all tools, deny interactive ones.
1673
1041
  // Crafting sessions are interactive — user and Claude collaborate to build PROMPT.md / JUDGE.md.
1674
1042
  if (session.loop && session.loop.active && session.loop.role !== "crafting") {
@@ -1712,6 +1080,7 @@ function createSDKBridge(opts) {
1712
1080
  // Regular tool permission request: send to client and wait
1713
1081
  return new Promise(function(resolve) {
1714
1082
  var requestId = crypto.randomUUID();
1083
+ sm.permissionRequestIndex[requestId] = session.localId;
1715
1084
  session.pendingPermissions[requestId] = {
1716
1085
  resolve: resolve,
1717
1086
  requestId: requestId,
@@ -1742,9 +1111,24 @@ function createSDKBridge(opts) {
1742
1111
  });
1743
1112
  }
1744
1113
 
1114
+ var _nm = getNotificationsModule();
1115
+ if (_nm) {
1116
+ _nm.notify("permission_request", {
1117
+ title: permissionPushTitle(toolName, input),
1118
+ body: permissionPushBody(toolName, input),
1119
+ slug: slug,
1120
+ sessionId: session.localId,
1121
+ ownerId: session.ownerId || null,
1122
+ requestId: requestId,
1123
+ toolName: toolName,
1124
+ toolInput: input,
1125
+ });
1126
+ }
1127
+
1745
1128
  if (opts.signal) {
1746
1129
  opts.signal.addEventListener("abort", function() {
1747
1130
  delete session.pendingPermissions[requestId];
1131
+ delete sm.permissionRequestIndex[requestId];
1748
1132
  sendAndRecord(session, { type: "permission_cancel", requestId: requestId });
1749
1133
  onProcessingChanged(); // update cross-project permission badge
1750
1134
  resolve({ behavior: "deny", message: "Request cancelled" });
@@ -1894,15 +1278,6 @@ function createSDKBridge(opts) {
1894
1278
  }
1895
1279
  }
1896
1280
  sendAndRecord(session, { type: "done", code: 1 });
1897
- if (pushModule) {
1898
- pushModule.sendPush({
1899
- type: "error",
1900
- slug: slug,
1901
- title: (mateDisplayName || "Claude") + ": Connection Lost",
1902
- body: (mateDisplayName || "Claude") + " process disconnected: " + (err.message || "unknown error"),
1903
- tag: "claude-error",
1904
- });
1905
- }
1906
1281
  }
1907
1282
  sm.broadcastSessionList();
1908
1283
  }
@@ -2129,6 +1504,7 @@ function createSDKBridge(opts) {
2129
1504
  session.messageQueue.end();
2130
1505
  }
2131
1506
 
1507
+ session.lastActivityAt = Date.now();
2132
1508
  session.streamPromise = processQueryStream(session).catch(function(err) {
2133
1509
  });
2134
1510
  }
@@ -2150,6 +1526,7 @@ function createSDKBridge(opts) {
2150
1526
  type: "user",
2151
1527
  message: { role: "user", content: content },
2152
1528
  };
1529
+ session.lastActivityAt = Date.now();
2153
1530
  // Route through worker if active, otherwise direct to message queue
2154
1531
  if (session.worker) {
2155
1532
  session.worker.send({ type: "push_message", content: userMsg });
@@ -2566,6 +1943,8 @@ function createSDKBridge(opts) {
2566
1943
  warmup: warmup,
2567
1944
  stopTask: stopTask,
2568
1945
  createMentionSession: createMentionSession,
1946
+ startIdleReaper: startIdleReaper,
1947
+ stopIdleReaper: stopIdleReaper,
2569
1948
  };
2570
1949
  }
2571
1950