clay-server 2.10.0 → 2.11.0-beta.10
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/bin/cli.js +157 -1
- package/lib/daemon.js +341 -2
- package/lib/dm.js +135 -0
- package/lib/os-users.js +301 -0
- package/lib/pages.js +36 -0
- package/lib/project.js +386 -67
- package/lib/public/app.js +675 -17
- package/lib/public/css/admin.css +99 -10
- package/lib/public/css/filebrowser.css +22 -0
- package/lib/public/css/icon-strip.css +162 -1
- package/lib/public/css/menus.css +23 -0
- package/lib/public/css/messages.css +245 -0
- package/lib/public/css/overlays.css +88 -0
- package/lib/public/css/server-settings.css +30 -2
- package/lib/public/css/sidebar.css +4 -0
- package/lib/public/index.html +140 -66
- package/lib/public/modules/admin.js +179 -12
- package/lib/public/modules/input.js +13 -2
- package/lib/public/modules/notifications.js +3 -1
- package/lib/public/modules/project-settings.js +154 -168
- package/lib/public/modules/server-settings.js +78 -189
- package/lib/public/modules/settings-defaults.js +243 -0
- package/lib/public/modules/sidebar.js +112 -6
- package/lib/public/modules/terminal.js +48 -10
- package/lib/public/modules/tools.js +214 -1
- package/lib/sdk-bridge.js +634 -6
- package/lib/sdk-worker.js +446 -0
- package/lib/server.js +335 -3
- package/lib/sessions.js +26 -0
- package/lib/terminal-manager.js +2 -2
- package/lib/terminal.js +20 -4
- package/lib/updater.js +38 -11
- package/lib/users.js +79 -0
- package/package.json +2 -2
package/lib/sdk-bridge.js
CHANGED
|
@@ -2,7 +2,10 @@ const crypto = require("crypto");
|
|
|
2
2
|
var fs = require("fs");
|
|
3
3
|
var path = require("path");
|
|
4
4
|
var os = require("os");
|
|
5
|
-
var
|
|
5
|
+
var net = require("net");
|
|
6
|
+
var { execSync, spawn } = require("child_process");
|
|
7
|
+
var { resolveOsUserInfo } = require("./os-users");
|
|
8
|
+
var usersModule = require("./users");
|
|
6
9
|
|
|
7
10
|
// Async message queue for streaming input to SDK
|
|
8
11
|
function createMessageQueue() {
|
|
@@ -55,6 +58,7 @@ function createSDKBridge(opts) {
|
|
|
55
58
|
var dangerouslySkipPermissions = opts.dangerouslySkipPermissions || false;
|
|
56
59
|
var onProcessingChanged = opts.onProcessingChanged || function () {};
|
|
57
60
|
|
|
61
|
+
|
|
58
62
|
// --- Skill discovery helpers ---
|
|
59
63
|
|
|
60
64
|
function discoverSkillDirs() {
|
|
@@ -243,6 +247,9 @@ function createSDKBridge(opts) {
|
|
|
243
247
|
.map(function(c) { return c.text; })
|
|
244
248
|
.join("");
|
|
245
249
|
if (assistantText) {
|
|
250
|
+
if (session.responsePreview.length < 200) {
|
|
251
|
+
session.responsePreview += assistantText;
|
|
252
|
+
}
|
|
246
253
|
sendAndRecord(session, { type: "delta", text: assistantText });
|
|
247
254
|
}
|
|
248
255
|
}
|
|
@@ -314,6 +321,7 @@ function createSDKBridge(opts) {
|
|
|
314
321
|
session.blocks = {};
|
|
315
322
|
session.sentToolResults = {};
|
|
316
323
|
session.pendingPermissions = {};
|
|
324
|
+
session.pendingElicitations = {};
|
|
317
325
|
// Record ask_user_answered for any leftover pending questions so replay pairs correctly
|
|
318
326
|
var leftoverAskIds = Object.keys(session.pendingAskUser);
|
|
319
327
|
for (var lai = 0; lai < leftoverAskIds.length; lai++) {
|
|
@@ -338,6 +346,27 @@ function createSDKBridge(opts) {
|
|
|
338
346
|
if (parsed.fast_mode_state) {
|
|
339
347
|
sendAndRecord(session, { type: "fast_mode_state", state: parsed.fast_mode_state });
|
|
340
348
|
}
|
|
349
|
+
// Detect "Not logged in · Please run /login" from SDK.
|
|
350
|
+
// This is a short canned response with zero cost, not actual AI output.
|
|
351
|
+
var previewTrimmed = (session.responsePreview || "").trim();
|
|
352
|
+
var isZeroCost = !parsed.total_cost_usd || parsed.total_cost_usd === 0;
|
|
353
|
+
var isLoginPrompt = isZeroCost && previewTrimmed.length < 100
|
|
354
|
+
&& /not logged in/i.test(previewTrimmed) && /\/login/i.test(previewTrimmed);
|
|
355
|
+
if (isLoginPrompt) {
|
|
356
|
+
var authUser = session.ownerId ? usersModule.findUserById(session.ownerId) : null;
|
|
357
|
+
var authLinuxUser = authUser && authUser.linuxUser ? authUser.linuxUser : null;
|
|
358
|
+
var canAutoLogin = !usersModule.isMultiUser()
|
|
359
|
+
|| !!authLinuxUser
|
|
360
|
+
|| (authUser && authUser.role === "admin");
|
|
361
|
+
sendAndRecord(session, {
|
|
362
|
+
type: "auth_required",
|
|
363
|
+
text: "Claude Code is not logged in.",
|
|
364
|
+
linuxUser: authLinuxUser,
|
|
365
|
+
canAutoLogin: canAutoLogin,
|
|
366
|
+
});
|
|
367
|
+
// Reset CLI session so next query starts fresh with new auth
|
|
368
|
+
session.cliSessionId = null;
|
|
369
|
+
}
|
|
341
370
|
sendAndRecord(session, { type: "done", code: 0 });
|
|
342
371
|
if (pushModule) {
|
|
343
372
|
var preview = (session.responsePreview || "").replace(/\s+/g, " ").trim();
|
|
@@ -386,6 +415,7 @@ function createSDKBridge(opts) {
|
|
|
386
415
|
usage: parsed.usage || null,
|
|
387
416
|
lastToolName: parsed.last_tool_name || null,
|
|
388
417
|
description: parsed.description || "",
|
|
418
|
+
summary: parsed.summary || null,
|
|
389
419
|
});
|
|
390
420
|
}
|
|
391
421
|
|
|
@@ -494,6 +524,524 @@ function createSDKBridge(opts) {
|
|
|
494
524
|
// user messages with parent_tool_use_id contain tool_results — skip silently
|
|
495
525
|
}
|
|
496
526
|
|
|
527
|
+
// --- MCP elicitation ---
|
|
528
|
+
|
|
529
|
+
function handleElicitation(session, request, opts) {
|
|
530
|
+
// Ralph Loop: auto-reject elicitation in autonomous mode
|
|
531
|
+
if (session.loop && session.loop.active && session.loop.role !== "crafting") {
|
|
532
|
+
return Promise.resolve({ action: "reject" });
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
return new Promise(function(resolve) {
|
|
536
|
+
var requestId = crypto.randomUUID();
|
|
537
|
+
if (!session.pendingElicitations) session.pendingElicitations = {};
|
|
538
|
+
session.pendingElicitations[requestId] = {
|
|
539
|
+
resolve: resolve,
|
|
540
|
+
request: request,
|
|
541
|
+
};
|
|
542
|
+
sendAndRecord(session, {
|
|
543
|
+
type: "elicitation_request",
|
|
544
|
+
requestId: requestId,
|
|
545
|
+
serverName: request.serverName,
|
|
546
|
+
message: request.message,
|
|
547
|
+
mode: request.mode || "form",
|
|
548
|
+
url: request.url || null,
|
|
549
|
+
elicitationId: request.elicitationId || null,
|
|
550
|
+
requestedSchema: request.requestedSchema || null,
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
if (pushModule) {
|
|
554
|
+
pushModule.sendPush({
|
|
555
|
+
type: "elicitation",
|
|
556
|
+
slug: slug,
|
|
557
|
+
title: (request.serverName || "MCP Server") + " needs input",
|
|
558
|
+
body: request.message || "Waiting for your response",
|
|
559
|
+
tag: "claude-elicitation",
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if (opts.signal) {
|
|
564
|
+
opts.signal.addEventListener("abort", function() {
|
|
565
|
+
delete session.pendingElicitations[requestId];
|
|
566
|
+
resolve({ action: "reject" });
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// --- Worker process management (OS-level multi-user) ---
|
|
573
|
+
|
|
574
|
+
var WORKER_SCRIPT = path.join(__dirname, "sdk-worker.js");
|
|
575
|
+
|
|
576
|
+
// resolveLinuxUser delegates to shared os-users utility
|
|
577
|
+
function resolveLinuxUser(username) {
|
|
578
|
+
return resolveOsUserInfo(username);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Spawn an SDK worker process running as the given Linux user.
|
|
583
|
+
* Returns a worker handle with send/kill/event methods.
|
|
584
|
+
*/
|
|
585
|
+
function spawnWorker(linuxUser) {
|
|
586
|
+
var userInfo = resolveLinuxUser(linuxUser);
|
|
587
|
+
var socketId = crypto.randomUUID();
|
|
588
|
+
var socketPath = path.join(os.tmpdir(), "clay-worker-" + socketId + ".sock");
|
|
589
|
+
|
|
590
|
+
var worker = {
|
|
591
|
+
process: null,
|
|
592
|
+
connection: null,
|
|
593
|
+
socketPath: socketPath,
|
|
594
|
+
server: null,
|
|
595
|
+
messageHandlers: [],
|
|
596
|
+
ready: false,
|
|
597
|
+
readyPromise: null,
|
|
598
|
+
_readyResolve: null,
|
|
599
|
+
buffer: "",
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
worker.readyPromise = new Promise(function(resolve) {
|
|
603
|
+
worker._readyResolve = resolve;
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
// Create Unix socket server
|
|
607
|
+
worker.server = net.createServer(function(connection) {
|
|
608
|
+
worker.connection = connection;
|
|
609
|
+
connection.on("data", function(chunk) {
|
|
610
|
+
worker.buffer += chunk.toString();
|
|
611
|
+
var lines = worker.buffer.split("\n");
|
|
612
|
+
worker.buffer = lines.pop();
|
|
613
|
+
for (var i = 0; i < lines.length; i++) {
|
|
614
|
+
if (!lines[i].trim()) continue;
|
|
615
|
+
try {
|
|
616
|
+
var msg = JSON.parse(lines[i]);
|
|
617
|
+
if (msg.type === "ready") {
|
|
618
|
+
worker.ready = true;
|
|
619
|
+
if (worker._readyResolve) {
|
|
620
|
+
worker._readyResolve();
|
|
621
|
+
worker._readyResolve = null;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
for (var h = 0; h < worker.messageHandlers.length; h++) {
|
|
625
|
+
worker.messageHandlers[h](msg);
|
|
626
|
+
}
|
|
627
|
+
} catch (e) {
|
|
628
|
+
console.error("[sdk-bridge] Failed to parse worker message:", e.message);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
});
|
|
632
|
+
connection.on("error", function(err) {
|
|
633
|
+
console.error("[sdk-bridge] Worker connection error:", err.message);
|
|
634
|
+
});
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
worker.server.listen(socketPath, function() {
|
|
638
|
+
// Set socket permissions so the target user can connect
|
|
639
|
+
try { fs.chmodSync(socketPath, 0o777); } catch (e) {}
|
|
640
|
+
|
|
641
|
+
// Spawn worker process as the target Linux user
|
|
642
|
+
var workerEnv = {
|
|
643
|
+
HOME: userInfo.home,
|
|
644
|
+
USER: linuxUser,
|
|
645
|
+
PATH: process.env.PATH || "/usr/local/bin:/usr/bin:/bin",
|
|
646
|
+
NODE_PATH: process.env.NODE_PATH || "",
|
|
647
|
+
LANG: process.env.LANG || "en_US.UTF-8",
|
|
648
|
+
};
|
|
649
|
+
|
|
650
|
+
worker.process = spawn(process.execPath, [WORKER_SCRIPT, socketPath], {
|
|
651
|
+
uid: userInfo.uid,
|
|
652
|
+
gid: userInfo.gid,
|
|
653
|
+
env: workerEnv,
|
|
654
|
+
cwd: cwd,
|
|
655
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
worker.process.stdout.on("data", function(data) {
|
|
659
|
+
console.log("[sdk-worker:" + linuxUser + "] " + data.toString().trim());
|
|
660
|
+
});
|
|
661
|
+
worker.process.stderr.on("data", function(data) {
|
|
662
|
+
console.error("[sdk-worker:" + linuxUser + "] " + data.toString().trim());
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
worker.process.on("exit", function(code, signal) {
|
|
666
|
+
console.log("[sdk-bridge] Worker for " + linuxUser + " exited (code=" + code + ", signal=" + signal + ")");
|
|
667
|
+
cleanupWorker(worker);
|
|
668
|
+
});
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
worker.send = function(msg) {
|
|
672
|
+
if (!worker.connection || worker.connection.destroyed) return;
|
|
673
|
+
try {
|
|
674
|
+
worker.connection.write(JSON.stringify(msg) + "\n");
|
|
675
|
+
} catch (e) {
|
|
676
|
+
console.error("[sdk-bridge] Failed to send to worker:", e.message);
|
|
677
|
+
}
|
|
678
|
+
};
|
|
679
|
+
|
|
680
|
+
worker.onMessage = function(handler) {
|
|
681
|
+
worker.messageHandlers.push(handler);
|
|
682
|
+
};
|
|
683
|
+
|
|
684
|
+
worker.kill = function() {
|
|
685
|
+
worker.send({ type: "shutdown" });
|
|
686
|
+
// Force kill after 3 seconds if still alive
|
|
687
|
+
setTimeout(function() {
|
|
688
|
+
if (worker.process && !worker.process.killed) {
|
|
689
|
+
try { worker.process.kill("SIGKILL"); } catch (e) {}
|
|
690
|
+
}
|
|
691
|
+
}, 3000);
|
|
692
|
+
cleanupWorker(worker);
|
|
693
|
+
};
|
|
694
|
+
|
|
695
|
+
return worker;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function cleanupWorker(worker) {
|
|
699
|
+
if (worker.connection && !worker.connection.destroyed) {
|
|
700
|
+
try { worker.connection.end(); } catch (e) {}
|
|
701
|
+
}
|
|
702
|
+
if (worker.server) {
|
|
703
|
+
try { worker.server.close(); } catch (e) {}
|
|
704
|
+
}
|
|
705
|
+
// Remove socket file
|
|
706
|
+
try { fs.unlinkSync(worker.socketPath); } catch (e) {}
|
|
707
|
+
worker.ready = false;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* Start a query via a worker process running as the target Linux user.
|
|
712
|
+
* Mirrors the in-process startQuery flow but delegates SDK execution to the worker.
|
|
713
|
+
*/
|
|
714
|
+
async function startQueryViaWorker(session, text, images, linuxUser) {
|
|
715
|
+
var worker;
|
|
716
|
+
try {
|
|
717
|
+
worker = spawnWorker(linuxUser);
|
|
718
|
+
session.worker = worker;
|
|
719
|
+
} catch (e) {
|
|
720
|
+
session.isProcessing = false;
|
|
721
|
+
onProcessingChanged();
|
|
722
|
+
sendAndRecord(session, { type: "error", text: "Failed to spawn worker for " + linuxUser + ": " + (e.message || e) });
|
|
723
|
+
sendAndRecord(session, { type: "done", code: 1 });
|
|
724
|
+
sm.broadcastSessionList();
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
session.messageQueue = "worker"; // sentinel: messages go via worker IPC
|
|
729
|
+
session.blocks = {};
|
|
730
|
+
session.sentToolResults = {};
|
|
731
|
+
session.activeTaskToolIds = {};
|
|
732
|
+
session.pendingElicitations = {};
|
|
733
|
+
session.streamedText = false;
|
|
734
|
+
session.responsePreview = "";
|
|
735
|
+
session.abortController = { abort: function() { worker.send({ type: "abort" }); } };
|
|
736
|
+
|
|
737
|
+
// Build initial user message content
|
|
738
|
+
var content = [];
|
|
739
|
+
if (images && images.length > 0) {
|
|
740
|
+
for (var i = 0; i < images.length; i++) {
|
|
741
|
+
content.push({
|
|
742
|
+
type: "image",
|
|
743
|
+
source: { type: "base64", media_type: images[i].mediaType, data: images[i].data },
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
if (text) {
|
|
748
|
+
content.push({ type: "text", text: text });
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
var initialMessage = {
|
|
752
|
+
type: "user",
|
|
753
|
+
message: { role: "user", content: content },
|
|
754
|
+
};
|
|
755
|
+
|
|
756
|
+
// Build serializable query options (no callbacks, no AbortController)
|
|
757
|
+
var queryOptions = {
|
|
758
|
+
cwd: cwd,
|
|
759
|
+
settingSources: ["user", "project", "local"],
|
|
760
|
+
includePartialMessages: true,
|
|
761
|
+
enableFileCheckpointing: true,
|
|
762
|
+
extraArgs: { "replay-user-messages": null },
|
|
763
|
+
promptSuggestions: true,
|
|
764
|
+
agentProgressSummaries: true,
|
|
765
|
+
};
|
|
766
|
+
|
|
767
|
+
if (sm.currentModel) queryOptions.model = sm.currentModel;
|
|
768
|
+
if (sm.currentEffort) queryOptions.effort = sm.currentEffort;
|
|
769
|
+
if (sm.currentBetas && sm.currentBetas.length > 0) queryOptions.betas = sm.currentBetas;
|
|
770
|
+
if (sm.currentThinking === "disabled") {
|
|
771
|
+
queryOptions.thinking = { type: "disabled" };
|
|
772
|
+
} else if (sm.currentThinking === "budget" && sm.currentThinkingBudget) {
|
|
773
|
+
queryOptions.thinking = { type: "enabled", budgetTokens: sm.currentThinkingBudget };
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
if (dangerouslySkipPermissions) {
|
|
777
|
+
queryOptions.permissionMode = "bypassPermissions";
|
|
778
|
+
queryOptions.allowDangerouslySkipPermissions = true;
|
|
779
|
+
} else {
|
|
780
|
+
var modeToApply = session.acceptEditsAfterStart ? "acceptEdits" : sm.currentPermissionMode;
|
|
781
|
+
if (session.acceptEditsAfterStart) delete session.acceptEditsAfterStart;
|
|
782
|
+
if (modeToApply && modeToApply !== "default") {
|
|
783
|
+
queryOptions.permissionMode = modeToApply;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
if (session.cliSessionId) {
|
|
788
|
+
queryOptions.resume = session.cliSessionId;
|
|
789
|
+
if (session.lastRewindUuid) {
|
|
790
|
+
queryOptions.resumeSessionAt = session.lastRewindUuid;
|
|
791
|
+
delete session.lastRewindUuid;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// Set up message handler for worker events
|
|
796
|
+
worker.onMessage(function(msg) {
|
|
797
|
+
switch (msg.type) {
|
|
798
|
+
case "sdk_event":
|
|
799
|
+
processSDKMessage(session, msg.event);
|
|
800
|
+
break;
|
|
801
|
+
|
|
802
|
+
case "permission_request":
|
|
803
|
+
handleCanUseTool(session, msg.toolName, msg.input, {
|
|
804
|
+
toolUseID: msg.toolUseId,
|
|
805
|
+
decisionReason: msg.decisionReason,
|
|
806
|
+
signal: session.abortController ? { addEventListener: function() {} } : undefined,
|
|
807
|
+
}).then(function(result) {
|
|
808
|
+
worker.send({ type: "permission_response", requestId: msg.requestId, result: result });
|
|
809
|
+
});
|
|
810
|
+
break;
|
|
811
|
+
|
|
812
|
+
case "ask_user_request":
|
|
813
|
+
// Delegate to the daemon's AskUserQuestion handling
|
|
814
|
+
handleCanUseTool(session, "AskUserQuestion", msg.input, {
|
|
815
|
+
toolUseID: msg.toolUseId,
|
|
816
|
+
signal: session.abortController ? { addEventListener: function() {} } : undefined,
|
|
817
|
+
}).then(function(result) {
|
|
818
|
+
worker.send({ type: "ask_user_response", toolUseId: msg.toolUseId, result: result });
|
|
819
|
+
});
|
|
820
|
+
break;
|
|
821
|
+
|
|
822
|
+
case "elicitation_request":
|
|
823
|
+
handleElicitation(session, {
|
|
824
|
+
serverName: msg.serverName,
|
|
825
|
+
message: msg.message,
|
|
826
|
+
mode: msg.mode,
|
|
827
|
+
url: msg.url,
|
|
828
|
+
elicitationId: msg.elicitationId,
|
|
829
|
+
requestedSchema: msg.requestedSchema,
|
|
830
|
+
}, {
|
|
831
|
+
signal: session.abortController ? { addEventListener: function() {} } : undefined,
|
|
832
|
+
}).then(function(result) {
|
|
833
|
+
worker.send({ type: "elicitation_response", requestId: msg.requestId, result: result });
|
|
834
|
+
});
|
|
835
|
+
break;
|
|
836
|
+
|
|
837
|
+
case "query_done":
|
|
838
|
+
// Stream ended normally
|
|
839
|
+
if (session.isProcessing && session.taskStopRequested) {
|
|
840
|
+
session.isProcessing = false;
|
|
841
|
+
onProcessingChanged();
|
|
842
|
+
sendAndRecord(session, { type: "info", text: "Interrupted \u00b7 What should Claude do instead?" });
|
|
843
|
+
sendAndRecord(session, { type: "done", code: 0 });
|
|
844
|
+
sm.broadcastSessionList();
|
|
845
|
+
}
|
|
846
|
+
cleanupSessionWorker(session);
|
|
847
|
+
if (session.onQueryComplete) {
|
|
848
|
+
try { session.onQueryComplete(session); } catch (err) {
|
|
849
|
+
console.error("[sdk-bridge] onQueryComplete error:", err.message || err);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
break;
|
|
853
|
+
|
|
854
|
+
case "query_error":
|
|
855
|
+
if (session.isProcessing) {
|
|
856
|
+
session.isProcessing = false;
|
|
857
|
+
onProcessingChanged();
|
|
858
|
+
var isAbort = (msg.error && (msg.error.indexOf("AbortError") !== -1 || msg.error.indexOf("aborted") !== -1))
|
|
859
|
+
|| session.taskStopRequested;
|
|
860
|
+
if (isAbort) {
|
|
861
|
+
if (!session.destroying) {
|
|
862
|
+
sendAndRecord(session, { type: "info", text: "Interrupted \u00b7 What should Claude do instead?" });
|
|
863
|
+
sendAndRecord(session, { type: "done", code: 0 });
|
|
864
|
+
}
|
|
865
|
+
} else if (session.destroying) {
|
|
866
|
+
console.log("[sdk-bridge] Suppressing worker error during shutdown for session " + session.localId);
|
|
867
|
+
} else {
|
|
868
|
+
var errDetail = msg.error || "Unknown error";
|
|
869
|
+
if (msg.stderr) errDetail += "\nstderr: " + msg.stderr;
|
|
870
|
+
if (msg.exitCode != null) errDetail += " (exitCode: " + msg.exitCode + ")";
|
|
871
|
+
console.error("[sdk-bridge] Worker query error for session " + session.localId + ":", errDetail);
|
|
872
|
+
|
|
873
|
+
var errLower = errDetail.toLowerCase();
|
|
874
|
+
var isContextOverflow = errLower.indexOf("prompt is too long") !== -1
|
|
875
|
+
|| errLower.indexOf("context_length") !== -1
|
|
876
|
+
|| errLower.indexOf("maximum context length") !== -1;
|
|
877
|
+
var isAuthError = errLower.indexOf("not logged in") !== -1
|
|
878
|
+
|| errLower.indexOf("unauthenticated") !== -1
|
|
879
|
+
|| errLower.indexOf("authentication") !== -1
|
|
880
|
+
|| errLower.indexOf("sign in") !== -1
|
|
881
|
+
|| errLower.indexOf("log in") !== -1
|
|
882
|
+
|| errLower.indexOf("please login") !== -1;
|
|
883
|
+
if (isContextOverflow) {
|
|
884
|
+
sendAndRecord(session, { type: "context_overflow", text: "Conversation too long to continue." });
|
|
885
|
+
} else if (isAuthError) {
|
|
886
|
+
var authUser = session.ownerId ? usersModule.findUserById(session.ownerId) : null;
|
|
887
|
+
var authLinuxUser = authUser && authUser.linuxUser ? authUser.linuxUser : null;
|
|
888
|
+
// Determine if auto-login (auto terminal + claude) is safe:
|
|
889
|
+
// - Single-user mode: always ok
|
|
890
|
+
// - Multi-user + OS user isolation (linuxUser set): ok (isolated)
|
|
891
|
+
// - Multi-user + admin role: ok (they own the shared account)
|
|
892
|
+
// - Multi-user + regular user (no linuxUser): not ok (shared account)
|
|
893
|
+
var canAutoLogin = !usersModule.isMultiUser()
|
|
894
|
+
|| !!authLinuxUser
|
|
895
|
+
|| (authUser && authUser.role === "admin");
|
|
896
|
+
sendAndRecord(session, {
|
|
897
|
+
type: "auth_required",
|
|
898
|
+
text: "Claude Code is not logged in.",
|
|
899
|
+
linuxUser: authLinuxUser,
|
|
900
|
+
canAutoLogin: canAutoLogin,
|
|
901
|
+
});
|
|
902
|
+
} else {
|
|
903
|
+
sendAndRecord(session, { type: "error", text: "Claude process error: " + msg.error });
|
|
904
|
+
}
|
|
905
|
+
sendAndRecord(session, { type: "done", code: 1 });
|
|
906
|
+
if (pushModule) {
|
|
907
|
+
pushModule.sendPush({
|
|
908
|
+
type: "error",
|
|
909
|
+
slug: slug,
|
|
910
|
+
title: "Connection Lost",
|
|
911
|
+
body: "Claude process disconnected: " + (msg.error || "unknown error"),
|
|
912
|
+
tag: "claude-error",
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
sm.broadcastSessionList();
|
|
917
|
+
}
|
|
918
|
+
cleanupSessionWorker(session);
|
|
919
|
+
if (session.onQueryComplete) {
|
|
920
|
+
try { session.onQueryComplete(session); } catch (err) {
|
|
921
|
+
console.error("[sdk-bridge] onQueryComplete error:", err.message || err);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
break;
|
|
925
|
+
|
|
926
|
+
case "model_changed":
|
|
927
|
+
sm.currentModel = msg.model;
|
|
928
|
+
send({ type: "model_info", model: msg.model, models: sm.availableModels || [] });
|
|
929
|
+
send({ type: "config_state", model: sm.currentModel, mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas || [] });
|
|
930
|
+
break;
|
|
931
|
+
|
|
932
|
+
case "effort_changed":
|
|
933
|
+
sm.currentEffort = msg.effort;
|
|
934
|
+
send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort, betas: sm.currentBetas || [] });
|
|
935
|
+
break;
|
|
936
|
+
|
|
937
|
+
case "permission_mode_changed":
|
|
938
|
+
sm.currentPermissionMode = msg.mode;
|
|
939
|
+
send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [] });
|
|
940
|
+
break;
|
|
941
|
+
|
|
942
|
+
case "worker_error":
|
|
943
|
+
send({ type: "error", text: msg.error });
|
|
944
|
+
break;
|
|
945
|
+
}
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
// Wait for worker to be ready, then send query
|
|
949
|
+
try {
|
|
950
|
+
await worker.readyPromise;
|
|
951
|
+
} catch (e) {
|
|
952
|
+
session.isProcessing = false;
|
|
953
|
+
onProcessingChanged();
|
|
954
|
+
sendAndRecord(session, { type: "error", text: "Worker failed to connect: " + (e.message || e) });
|
|
955
|
+
sendAndRecord(session, { type: "done", code: 1 });
|
|
956
|
+
sm.broadcastSessionList();
|
|
957
|
+
cleanupSessionWorker(session);
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
worker.send({
|
|
962
|
+
type: "query_start",
|
|
963
|
+
prompt: initialMessage,
|
|
964
|
+
options: queryOptions,
|
|
965
|
+
singleTurn: !!session.singleTurn,
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
function cleanupSessionWorker(session) {
|
|
970
|
+
session.queryInstance = null;
|
|
971
|
+
session.messageQueue = null;
|
|
972
|
+
session.abortController = null;
|
|
973
|
+
session.taskStopRequested = false;
|
|
974
|
+
session.pendingPermissions = {};
|
|
975
|
+
session.pendingAskUser = {};
|
|
976
|
+
session.pendingElicitations = {};
|
|
977
|
+
if (session.worker) {
|
|
978
|
+
session.worker.kill();
|
|
979
|
+
session.worker = null;
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
/**
|
|
984
|
+
* Run warmup via a worker process for a specific Linux user.
|
|
985
|
+
*/
|
|
986
|
+
async function warmupViaWorker(linuxUser) {
|
|
987
|
+
var worker;
|
|
988
|
+
try {
|
|
989
|
+
worker = spawnWorker(linuxUser);
|
|
990
|
+
} catch (e) {
|
|
991
|
+
send({ type: "error", text: "Failed to spawn warmup worker for " + linuxUser + ": " + (e.message || e) });
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
var warmupDone = false;
|
|
996
|
+
|
|
997
|
+
worker.onMessage(function(msg) {
|
|
998
|
+
if (msg.type === "warmup_done" && !warmupDone) {
|
|
999
|
+
warmupDone = true;
|
|
1000
|
+
var result = msg.result || {};
|
|
1001
|
+
var fsSkills = discoverSkillDirs();
|
|
1002
|
+
sm.skillNames = mergeSkills(result.skills, fsSkills);
|
|
1003
|
+
if (result.slashCommands) {
|
|
1004
|
+
var seen = new Set();
|
|
1005
|
+
var combined = [];
|
|
1006
|
+
var all = result.slashCommands.concat(Array.from(sm.skillNames));
|
|
1007
|
+
for (var k = 0; k < all.length; k++) {
|
|
1008
|
+
if (!seen.has(all[k])) {
|
|
1009
|
+
seen.add(all[k]);
|
|
1010
|
+
combined.push(all[k]);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
sm.slashCommands = combined;
|
|
1014
|
+
send({ type: "slash_commands", commands: sm.slashCommands });
|
|
1015
|
+
}
|
|
1016
|
+
if (result.model) {
|
|
1017
|
+
sm.currentModel = sm._savedDefaultModel || result.model;
|
|
1018
|
+
}
|
|
1019
|
+
sm.availableModels = result.models || [];
|
|
1020
|
+
send({ type: "model_info", model: sm.currentModel || "", models: sm.availableModels || [] });
|
|
1021
|
+
worker.kill();
|
|
1022
|
+
} else if (msg.type === "warmup_error" && !warmupDone) {
|
|
1023
|
+
warmupDone = true;
|
|
1024
|
+
send({ type: "error", text: result.error || "Warmup failed" });
|
|
1025
|
+
worker.kill();
|
|
1026
|
+
}
|
|
1027
|
+
});
|
|
1028
|
+
|
|
1029
|
+
try {
|
|
1030
|
+
await worker.readyPromise;
|
|
1031
|
+
} catch (e) {
|
|
1032
|
+
send({ type: "error", text: "Warmup worker failed to connect: " + (e.message || e) });
|
|
1033
|
+
cleanupWorker(worker);
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
var warmupOptions = { cwd: cwd, settingSources: ["user", "project", "local"] };
|
|
1038
|
+
if (dangerouslySkipPermissions) {
|
|
1039
|
+
warmupOptions.permissionMode = "bypassPermissions";
|
|
1040
|
+
warmupOptions.allowDangerouslySkipPermissions = true;
|
|
1041
|
+
}
|
|
1042
|
+
worker.send({ type: "warmup", options: warmupOptions });
|
|
1043
|
+
}
|
|
1044
|
+
|
|
497
1045
|
// --- SDK query lifecycle ---
|
|
498
1046
|
|
|
499
1047
|
function handleCanUseTool(session, toolName, input, opts) {
|
|
@@ -681,11 +1229,29 @@ function createSDKBridge(opts) {
|
|
|
681
1229
|
var isContextOverflow = errLower.indexOf("prompt is too long") !== -1
|
|
682
1230
|
|| errLower.indexOf("context_length") !== -1
|
|
683
1231
|
|| errLower.indexOf("maximum context length") !== -1;
|
|
1232
|
+
var isAuthError = errLower.indexOf("not logged in") !== -1
|
|
1233
|
+
|| errLower.indexOf("unauthenticated") !== -1
|
|
1234
|
+
|| errLower.indexOf("authentication") !== -1
|
|
1235
|
+
|| errLower.indexOf("sign in") !== -1
|
|
1236
|
+
|| errLower.indexOf("log in") !== -1
|
|
1237
|
+
|| errLower.indexOf("please login") !== -1;
|
|
684
1238
|
if (isContextOverflow) {
|
|
685
1239
|
sendAndRecord(session, {
|
|
686
1240
|
type: "context_overflow",
|
|
687
1241
|
text: "Conversation too long to continue.",
|
|
688
1242
|
});
|
|
1243
|
+
} else if (isAuthError) {
|
|
1244
|
+
var authUser = session.ownerId ? usersModule.findUserById(session.ownerId) : null;
|
|
1245
|
+
var authLinuxUser = authUser && authUser.linuxUser ? authUser.linuxUser : null;
|
|
1246
|
+
var canAutoLogin = !usersModule.isMultiUser()
|
|
1247
|
+
|| !!authLinuxUser
|
|
1248
|
+
|| (authUser && authUser.role === "admin");
|
|
1249
|
+
sendAndRecord(session, {
|
|
1250
|
+
type: "auth_required",
|
|
1251
|
+
text: "Claude Code is not logged in.",
|
|
1252
|
+
linuxUser: authLinuxUser,
|
|
1253
|
+
canAutoLogin: canAutoLogin,
|
|
1254
|
+
});
|
|
689
1255
|
} else {
|
|
690
1256
|
sendAndRecord(session, { type: "error", text: "Claude process error: " + err.message });
|
|
691
1257
|
}
|
|
@@ -710,6 +1276,7 @@ function createSDKBridge(opts) {
|
|
|
710
1276
|
session.taskStopRequested = false;
|
|
711
1277
|
session.pendingPermissions = {};
|
|
712
1278
|
session.pendingAskUser = {};
|
|
1279
|
+
session.pendingElicitations = {};
|
|
713
1280
|
}
|
|
714
1281
|
// Ralph Loop: notify completion so loop orchestrator can proceed
|
|
715
1282
|
if (session.onQueryComplete) {
|
|
@@ -756,7 +1323,12 @@ function createSDKBridge(opts) {
|
|
|
756
1323
|
};
|
|
757
1324
|
}
|
|
758
1325
|
|
|
759
|
-
async function startQuery(session, text, images) {
|
|
1326
|
+
async function startQuery(session, text, images, linuxUser) {
|
|
1327
|
+
// OS-level isolation: delegate to worker process if linuxUser is set
|
|
1328
|
+
if (linuxUser) {
|
|
1329
|
+
return startQueryViaWorker(session, text, images, linuxUser);
|
|
1330
|
+
}
|
|
1331
|
+
|
|
760
1332
|
var sdk;
|
|
761
1333
|
try {
|
|
762
1334
|
sdk = await getSDK();
|
|
@@ -773,6 +1345,7 @@ function createSDKBridge(opts) {
|
|
|
773
1345
|
session.blocks = {};
|
|
774
1346
|
session.sentToolResults = {};
|
|
775
1347
|
session.activeTaskToolIds = {};
|
|
1348
|
+
session.pendingElicitations = {};
|
|
776
1349
|
session.streamedText = false;
|
|
777
1350
|
session.responsePreview = "";
|
|
778
1351
|
|
|
@@ -805,9 +1378,13 @@ function createSDKBridge(opts) {
|
|
|
805
1378
|
extraArgs: { "replay-user-messages": null },
|
|
806
1379
|
abortController: session.abortController,
|
|
807
1380
|
promptSuggestions: true,
|
|
1381
|
+
agentProgressSummaries: true,
|
|
808
1382
|
canUseTool: function(toolName, input, toolOpts) {
|
|
809
1383
|
return handleCanUseTool(session, toolName, input, toolOpts);
|
|
810
1384
|
},
|
|
1385
|
+
onElicitation: function(request, elicitOpts) {
|
|
1386
|
+
return handleElicitation(session, request, elicitOpts);
|
|
1387
|
+
},
|
|
811
1388
|
};
|
|
812
1389
|
|
|
813
1390
|
if (sm.currentModel) {
|
|
@@ -822,6 +1399,12 @@ function createSDKBridge(opts) {
|
|
|
822
1399
|
queryOptions.betas = sm.currentBetas;
|
|
823
1400
|
}
|
|
824
1401
|
|
|
1402
|
+
if (sm.currentThinking === "disabled") {
|
|
1403
|
+
queryOptions.thinking = { type: "disabled" };
|
|
1404
|
+
} else if (sm.currentThinking === "budget" && sm.currentThinkingBudget) {
|
|
1405
|
+
queryOptions.thinking = { type: "enabled", budgetTokens: sm.currentThinkingBudget };
|
|
1406
|
+
}
|
|
1407
|
+
|
|
825
1408
|
if (dangerouslySkipPermissions) {
|
|
826
1409
|
queryOptions.permissionMode = "bypassPermissions";
|
|
827
1410
|
queryOptions.allowDangerouslySkipPermissions = true;
|
|
@@ -887,10 +1470,16 @@ function createSDKBridge(opts) {
|
|
|
887
1470
|
if (text) {
|
|
888
1471
|
content.push({ type: "text", text: text });
|
|
889
1472
|
}
|
|
890
|
-
|
|
1473
|
+
var userMsg = {
|
|
891
1474
|
type: "user",
|
|
892
1475
|
message: { role: "user", content: content },
|
|
893
|
-
}
|
|
1476
|
+
};
|
|
1477
|
+
// Route through worker if active, otherwise direct to message queue
|
|
1478
|
+
if (session.worker) {
|
|
1479
|
+
session.worker.send({ type: "push_message", content: userMsg });
|
|
1480
|
+
} else {
|
|
1481
|
+
session.messageQueue.push(userMsg);
|
|
1482
|
+
}
|
|
894
1483
|
}
|
|
895
1484
|
|
|
896
1485
|
function permissionPushTitle(toolName, input) {
|
|
@@ -937,7 +1526,12 @@ function createSDKBridge(opts) {
|
|
|
937
1526
|
}
|
|
938
1527
|
|
|
939
1528
|
// SDK warmup: grab slash_commands, model, and available models from SDK init
|
|
940
|
-
async function warmup() {
|
|
1529
|
+
async function warmup(linuxUser) {
|
|
1530
|
+
// OS-level isolation: delegate warmup to worker process
|
|
1531
|
+
if (linuxUser) {
|
|
1532
|
+
return warmupViaWorker(linuxUser);
|
|
1533
|
+
}
|
|
1534
|
+
|
|
941
1535
|
try {
|
|
942
1536
|
var sdk = await getSDK();
|
|
943
1537
|
var ac = new AbortController();
|
|
@@ -992,6 +1586,10 @@ function createSDKBridge(opts) {
|
|
|
992
1586
|
}
|
|
993
1587
|
|
|
994
1588
|
async function setModel(session, model) {
|
|
1589
|
+
if (session.worker) {
|
|
1590
|
+
session.worker.send({ type: "set_model", model: model });
|
|
1591
|
+
return;
|
|
1592
|
+
}
|
|
995
1593
|
if (!session.queryInstance) {
|
|
996
1594
|
// No active query — just store the model for next startQuery
|
|
997
1595
|
sm.currentModel = model;
|
|
@@ -1009,6 +1607,25 @@ function createSDKBridge(opts) {
|
|
|
1009
1607
|
}
|
|
1010
1608
|
}
|
|
1011
1609
|
|
|
1610
|
+
async function setEffort(session, effort) {
|
|
1611
|
+
if (session.worker) {
|
|
1612
|
+
session.worker.send({ type: "set_effort", effort: effort });
|
|
1613
|
+
return;
|
|
1614
|
+
}
|
|
1615
|
+
if (!session.queryInstance) {
|
|
1616
|
+
sm.currentEffort = effort;
|
|
1617
|
+
send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort, betas: sm.currentBetas || [] });
|
|
1618
|
+
return;
|
|
1619
|
+
}
|
|
1620
|
+
try {
|
|
1621
|
+
await session.queryInstance.setEffort(effort);
|
|
1622
|
+
sm.currentEffort = effort;
|
|
1623
|
+
send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort, betas: sm.currentBetas || [] });
|
|
1624
|
+
} catch (e) {
|
|
1625
|
+
send({ type: "error", text: "Failed to set effort: " + (e.message || e) });
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1012
1629
|
async function setPermissionMode(session, mode) {
|
|
1013
1630
|
// When dangerouslySkipPermissions is active, ignore mode changes from UI
|
|
1014
1631
|
// to prevent accidentally downgrading from bypassPermissions
|
|
@@ -1016,6 +1633,10 @@ function createSDKBridge(opts) {
|
|
|
1016
1633
|
send({ type: "config_state", model: sm.currentModel || "", mode: "bypassPermissions", effort: sm.currentEffort || "medium", betas: sm.currentBetas || [] });
|
|
1017
1634
|
return;
|
|
1018
1635
|
}
|
|
1636
|
+
if (session.worker) {
|
|
1637
|
+
session.worker.send({ type: "set_permission_mode", mode: mode });
|
|
1638
|
+
return;
|
|
1639
|
+
}
|
|
1019
1640
|
if (!session.queryInstance) {
|
|
1020
1641
|
// No active query — just store the mode for next startQuery
|
|
1021
1642
|
sm.currentPermissionMode = mode;
|
|
@@ -1033,8 +1654,13 @@ function createSDKBridge(opts) {
|
|
|
1033
1654
|
|
|
1034
1655
|
async function stopTask(taskId) {
|
|
1035
1656
|
var session = sm.getActiveSession();
|
|
1036
|
-
if (!session
|
|
1657
|
+
if (!session) return;
|
|
1037
1658
|
session.taskStopRequested = true;
|
|
1659
|
+
if (session.worker) {
|
|
1660
|
+
session.worker.send({ type: "stop_task", taskId: taskId });
|
|
1661
|
+
return;
|
|
1662
|
+
}
|
|
1663
|
+
if (!session.queryInstance) return;
|
|
1038
1664
|
try {
|
|
1039
1665
|
await session.queryInstance.stopTask(taskId);
|
|
1040
1666
|
} catch (e) {
|
|
@@ -1051,11 +1677,13 @@ function createSDKBridge(opts) {
|
|
|
1051
1677
|
createMessageQueue: createMessageQueue,
|
|
1052
1678
|
processSDKMessage: processSDKMessage,
|
|
1053
1679
|
handleCanUseTool: handleCanUseTool,
|
|
1680
|
+
handleElicitation: handleElicitation,
|
|
1054
1681
|
processQueryStream: processQueryStream,
|
|
1055
1682
|
getOrCreateRewindQuery: getOrCreateRewindQuery,
|
|
1056
1683
|
startQuery: startQuery,
|
|
1057
1684
|
pushMessage: pushMessage,
|
|
1058
1685
|
setModel: setModel,
|
|
1686
|
+
setEffort: setEffort,
|
|
1059
1687
|
setPermissionMode: setPermissionMode,
|
|
1060
1688
|
isClaudeProcess: isClaudeProcess,
|
|
1061
1689
|
permissionPushTitle: permissionPushTitle,
|