clay-server 2.27.1 → 2.28.0-beta.2

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.
@@ -7,8 +7,10 @@
7
7
 
8
8
  import { iconHtml } from './icons.js';
9
9
  import { showToast } from './utils.js';
10
+ import { renderModelList, renderModeList, renderEffortBar, renderThinkingBar } from './settings-defaults.js';
11
+ import { store } from './store.js';
10
12
  import { initSchedulerConfig, setupCreateModal, openCreateModal, openCreateModalWithRecord, closeCreateModal, removePreview, getPreviewEl, showPreviewOnCell, showPreviewOnSlot, showPreviewForCreate, applyDraggedTask, parseCronSimple } from './scheduler-config.js';
11
- import { initSchedulerHistory, renderHistory } from './scheduler-history.js';
13
+ import { initSchedulerHistory, renderHistory, _lastFiles as lastLoopFiles } from './scheduler-history.js';
12
14
  export { handleLoopRegistryUpdated, handleLoopRegistryFiles, handleScheduleRunStarted, handleScheduleRunFinished, handleLoopScheduled } from './scheduler-history.js';
13
15
 
14
16
  var ctx = null;
@@ -671,7 +673,7 @@ function renderDetail() {
671
673
  html += '<div class="scheduler-detail-tabs">';
672
674
  html += '<button class="scheduler-detail-tab active" data-tab="prompt">PROMPT.md</button>';
673
675
  html += '<button class="scheduler-detail-tab" data-tab="judge">JUDGE.md</button>';
674
- html += '<button class="scheduler-detail-tab" data-tab="meta">Info</button>';
676
+ html += '<button class="scheduler-detail-tab" data-tab="model">Model</button>';
675
677
  html += '</div>';
676
678
 
677
679
  html += '<div class="scheduler-detail-body" id="scheduler-detail-body">';
@@ -738,41 +740,83 @@ function renderDetailBody(tab, rec) {
738
740
  var bodyEl2 = document.getElementById("scheduler-detail-body");
739
741
  if (!bodyEl2) return;
740
742
 
741
- if (tab === "meta") {
742
- var isScheduled = !!rec.cron;
743
- var lastRun = rec.runs && rec.runs.length > 0 ? rec.runs[rec.runs.length - 1] : null;
744
- var scheduleStr = isScheduled ? cronToHuman(rec.cron) : "One-off";
745
- var statusStr = isScheduled ? (rec.enabled ? "Enabled" : "Paused") : "One-off";
746
- var createdStr = rec.createdAt ? formatDateTime(new Date(rec.createdAt)) : "—";
747
- var lastRunStr = "Never";
748
- if (lastRun) {
749
- var resultStr = lastRun.result || "?";
750
- var iterStr = (lastRun.iterations || 0) + " iter";
751
- lastRunStr = formatDateTime(new Date(lastRun.finishedAt || lastRun.startedAt)) + " — " + resultStr + " (" + iterStr + ")";
752
- }
753
-
754
- var html = '<div class="scheduler-detail-meta">';
755
- html += '<span class="scheduler-detail-meta-label">Schedule</span>';
756
- html += '<span class="scheduler-detail-meta-value">' + esc(scheduleStr) + '</span>';
757
- html += '<span class="scheduler-detail-meta-label">Status</span>';
758
- html += '<span class="scheduler-detail-meta-value">' + esc(statusStr) + '</span>';
759
- html += '<span class="scheduler-detail-meta-label">Max Iterations</span>';
760
- html += '<span class="scheduler-detail-meta-value">' + (rec.maxIterations || "—") + '</span>';
761
- html += '<span class="scheduler-detail-meta-label">Created</span>';
762
- html += '<span class="scheduler-detail-meta-value">' + esc(createdStr) + '</span>';
763
- html += '<span class="scheduler-detail-meta-label">Last Run</span>';
764
- html += '<span class="scheduler-detail-meta-value">' + esc(lastRunStr) + '</span>';
765
- if (isScheduled && rec.nextRunAt) {
766
- html += '<span class="scheduler-detail-meta-label">Next Run</span>';
767
- html += '<span class="scheduler-detail-meta-value">' + esc(formatDateTime(new Date(rec.nextRunAt))) + '</span>';
768
- }
769
- html += '</div>';
770
- bodyEl2.innerHTML = html;
771
- } else {
772
- // prompt or judge — request files from server
773
- bodyEl2.innerHTML = '<div class="scheduler-detail-loading">Loading...</div>';
774
- send({ type: "loop_registry_files", id: selectedTaskId });
743
+ if (tab === "model") {
744
+ renderModelTab(bodyEl2, rec);
745
+ return;
775
746
  }
747
+
748
+ // prompt or judge — request files from server
749
+ bodyEl2.innerHTML = '<div class="scheduler-detail-loading">Loading...</div>';
750
+ send({ type: "loop_registry_files", id: selectedTaskId });
751
+ }
752
+
753
+ function renderModelTab(bodyEl, rec) {
754
+ var settings = lastLoopFiles.settings || {};
755
+ var loopFilesId = rec.linkedTaskId || rec.id;
756
+
757
+ bodyEl.innerHTML =
758
+ '<div class="scheduler-model-settings">' +
759
+ '<div class="settings-card"><div class="settings-field">' +
760
+ '<label class="settings-label">Model</label>' +
761
+ '<div class="settings-hint">Choose the Claude model for this task.</div>' +
762
+ '<div id="ls-model-list" class="settings-model-list"></div>' +
763
+ '</div></div>' +
764
+ '<div class="settings-card"><div class="settings-field">' +
765
+ '<label class="settings-label">Mode</label>' +
766
+ '<div class="settings-hint">Controls how Claude handles tool use and file edits.</div>' +
767
+ '<div id="ls-mode-list" class="settings-model-list"></div>' +
768
+ '</div></div>' +
769
+ '<div class="settings-card"><div class="settings-field">' +
770
+ '<label class="settings-label">Effort</label>' +
771
+ '<div class="settings-hint">Controls how much thinking effort Claude puts into responses.</div>' +
772
+ '<div class="settings-btn-group" id="ls-effort-bar"></div>' +
773
+ '</div></div>' +
774
+ '<div class="settings-card"><div class="settings-field">' +
775
+ '<label class="settings-label">Thinking</label>' +
776
+ '<div class="settings-hint">Controls whether Claude shows its reasoning process.</div>' +
777
+ '<div class="settings-btn-group" id="ls-thinking-bar"></div>' +
778
+ '<div id="ls-thinking-budget-row" class="settings-budget-row" style="display:none">' +
779
+ '<label class="settings-budget-label">Budget tokens</label>' +
780
+ '<input id="ls-thinking-budget" type="number" class="settings-budget-input" min="1024" max="128000" step="1024" value="10000">' +
781
+ '</div>' +
782
+ '</div></div>' +
783
+ '</div>';
784
+
785
+ function saveLoopSetting(key, value) {
786
+ var updated = Object.assign({}, settings);
787
+ updated[key] = value;
788
+ settings = updated;
789
+ send({ type: "loop_registry_save_files", id: loopFilesId, settings: updated });
790
+ }
791
+
792
+ var opts = {
793
+ models: store.getState().currentModels || [],
794
+ currentModel: settings.model || "",
795
+ currentMode: settings.permissionMode || "default",
796
+ currentEffort: settings.effort || "medium",
797
+ currentThinking: settings.thinking || "adaptive",
798
+ currentThinkingBudget: settings.thinkingBudget || 10000,
799
+ sendMsg: function (msgType, data) {
800
+ if (msgType === "set_model" || msgType === "loop_set_model") {
801
+ saveLoopSetting("model", data.model);
802
+ } else if (msgType === "loop_set_mode") {
803
+ saveLoopSetting("permissionMode", data.mode);
804
+ } else if (msgType === "loop_set_effort") {
805
+ saveLoopSetting("effort", data.effort);
806
+ } else if (msgType === "set_thinking") {
807
+ saveLoopSetting("thinking", data.thinking);
808
+ if (data.budgetTokens) saveLoopSetting("thinkingBudget", data.budgetTokens);
809
+ }
810
+ },
811
+ modelMsgType: "loop_set_model",
812
+ modeMsgType: "loop_set_mode",
813
+ effortMsgType: "loop_set_effort",
814
+ };
815
+
816
+ renderModelList("ls", opts);
817
+ renderModeList("ls", opts);
818
+ renderEffortBar("ls", opts);
819
+ renderThinkingBar("ls", opts);
776
820
  }
777
821
 
778
822
  // --- Chat reparenting ---
package/lib/sdk-bridge.js CHANGED
@@ -10,6 +10,31 @@ var { splitShellSegments, attachSkillDiscovery } = require("./sdk-skill-discover
10
10
  var { createMessageQueue } = require("./sdk-message-queue");
11
11
  var { attachMessageProcessor } = require("./sdk-message-processor");
12
12
 
13
+ // Merge in-process MCP servers with remote (extension-bridged) MCP servers.
14
+ // Returns the merged object, or null if no servers exist.
15
+ function mergeMcpServers(localServers, getRemoteFn) {
16
+ var merged = {};
17
+ var hasAny = false;
18
+ if (localServers) {
19
+ var lk = Object.keys(localServers);
20
+ for (var i = 0; i < lk.length; i++) {
21
+ merged[lk[i]] = localServers[lk[i]];
22
+ hasAny = true;
23
+ }
24
+ }
25
+ if (typeof getRemoteFn === "function") {
26
+ var remote = getRemoteFn();
27
+ if (remote) {
28
+ var rk = Object.keys(remote);
29
+ for (var j = 0; j < rk.length; j++) {
30
+ merged[rk[j]] = remote[rk[j]];
31
+ hasAny = true;
32
+ }
33
+ }
34
+ }
35
+ return hasAny ? merged : null;
36
+ }
37
+
13
38
  function createSDKBridge(opts) {
14
39
  var cwd = opts.cwd;
15
40
  var slug = opts.slug || "";
@@ -22,6 +47,7 @@ function createSDKBridge(opts) {
22
47
  var isMate = opts.isMate || (slug.indexOf("mate-") === 0);
23
48
  var dangerouslySkipPermissions = opts.dangerouslySkipPermissions || false;
24
49
  var mcpServers = opts.mcpServers || null;
50
+ var getRemoteMcpServers = opts.getRemoteMcpServers || null;
25
51
  var onProcessingChanged = opts.onProcessingChanged || function () {};
26
52
  var onTurnDone = opts.onTurnDone || null;
27
53
 
@@ -553,20 +579,32 @@ function createSDKBridge(opts) {
553
579
  agentProgressSummaries: true,
554
580
  };
555
581
 
556
- if (mcpServers) queryOptions.mcpServers = mcpServers;
557
- if (sm.currentModel) queryOptions.model = sm.currentModel;
558
- if (sm.currentEffort) queryOptions.effort = sm.currentEffort;
582
+ var _mergedMcp = mergeMcpServers(mcpServers, getRemoteMcpServers);
583
+ if (_mergedMcp) queryOptions.mcpServers = _mergedMcp;
584
+
585
+ // Per-loop settings override global defaults when present
586
+ var ls2 = session.loopSettings || {};
587
+
588
+ if (ls2.model || sm.currentModel) queryOptions.model = ls2.model || sm.currentModel;
589
+ if (ls2.effort || sm.currentEffort) queryOptions.effort = ls2.effort || sm.currentEffort;
559
590
  if (sm.currentBetas && sm.currentBetas.length > 0) queryOptions.betas = sm.currentBetas;
560
- if (sm.currentThinking === "disabled") {
591
+
592
+ var thinkingMode2 = ls2.thinking || sm.currentThinking;
593
+ if (thinkingMode2 === "disabled") {
561
594
  queryOptions.thinking = { type: "disabled" };
562
- } else if (sm.currentThinking === "budget" && sm.currentThinkingBudget) {
563
- queryOptions.thinking = { type: "enabled", budgetTokens: sm.currentThinkingBudget };
595
+ } else if (thinkingMode2 === "budget") {
596
+ var budgetTokens2 = ls2.thinkingBudget || sm.currentThinkingBudget;
597
+ if (budgetTokens2) queryOptions.thinking = { type: "enabled", budgetTokens: budgetTokens2 };
598
+ }
599
+
600
+ if (ls2.disableAllHooks !== undefined) {
601
+ queryOptions.settings = Object.assign({}, queryOptions.settings || {}, { disableAllHooks: ls2.disableAllHooks });
564
602
  }
565
603
 
566
604
  if (dangerouslySkipPermissions) {
567
605
  queryOptions.allowDangerouslySkipPermissions = true;
568
606
  }
569
- var modeToApply = session.acceptEditsAfterStart ? "acceptEdits" : sm.currentPermissionMode;
607
+ var modeToApply = ls2.permissionMode || (session.acceptEditsAfterStart ? "acceptEdits" : sm.currentPermissionMode);
570
608
  if (session.acceptEditsAfterStart) delete session.acceptEditsAfterStart;
571
609
  if (modeToApply && modeToApply !== "default") {
572
610
  queryOptions.permissionMode = modeToApply;
@@ -959,6 +997,19 @@ function createSDKBridge(opts) {
959
997
  return { behavior: "allow", updatedInput: input };
960
998
  }
961
999
 
1000
+ // Auto-approve remote MCP tools that the user explicitly enabled in project settings.
1001
+ // These are user-owned local MCP servers, so no additional permission prompt needed.
1002
+ if (toolName.indexOf("mcp__") === 0 && getRemoteMcpServers) {
1003
+ var _rmcp = getRemoteMcpServers();
1004
+ if (_rmcp) {
1005
+ var _mcpParts = toolName.split("__");
1006
+ var _mcpServerName = _mcpParts.length >= 2 ? _mcpParts[1] : "";
1007
+ if (_rmcp[_mcpServerName]) {
1008
+ return { behavior: "allow", updatedInput: input };
1009
+ }
1010
+ }
1011
+ }
1012
+
962
1013
  // Auto-approve safe Bash commands (read-only, non-destructive)
963
1014
  // Applies to ALL sessions (mates and regular projects alike).
964
1015
  // These are purely read-only commands that cannot modify files, install
@@ -1429,7 +1480,7 @@ function createSDKBridge(opts) {
1429
1480
  abortController: session.abortController,
1430
1481
  promptSuggestions: true,
1431
1482
  agentProgressSummaries: true,
1432
- mcpServers: mcpServers || undefined,
1483
+ mcpServers: mergeMcpServers(mcpServers, getRemoteMcpServers) || undefined,
1433
1484
  canUseTool: function(toolName, input, toolOpts) {
1434
1485
  return handleCanUseTool(session, toolName, input, toolOpts);
1435
1486
  },
@@ -1438,29 +1489,44 @@ function createSDKBridge(opts) {
1438
1489
  },
1439
1490
  };
1440
1491
 
1441
- if (sm.currentModel) {
1442
- queryOptions.model = sm.currentModel;
1492
+ // Per-loop settings override global defaults when present
1493
+ var ls = session.loopSettings || {};
1494
+
1495
+ if (ls.model || sm.currentModel) {
1496
+ queryOptions.model = ls.model || sm.currentModel;
1443
1497
  }
1444
1498
 
1445
- if (sm.currentEffort) {
1446
- queryOptions.effort = sm.currentEffort;
1499
+ if (ls.effort || sm.currentEffort) {
1500
+ queryOptions.effort = ls.effort || sm.currentEffort;
1447
1501
  }
1448
1502
 
1449
1503
  if (sm.currentBetas && sm.currentBetas.length > 0) {
1450
1504
  queryOptions.betas = sm.currentBetas;
1451
1505
  }
1452
1506
 
1453
- if (sm.currentThinking === "disabled") {
1507
+ var thinkingMode = ls.thinking || sm.currentThinking;
1508
+ if (thinkingMode === "disabled") {
1454
1509
  queryOptions.thinking = { type: "disabled" };
1455
- } else if (sm.currentThinking === "budget" && sm.currentThinkingBudget) {
1456
- queryOptions.thinking = { type: "enabled", budgetTokens: sm.currentThinkingBudget };
1510
+ } else if (thinkingMode === "budget") {
1511
+ var budgetTokens = ls.thinkingBudget || sm.currentThinkingBudget;
1512
+ if (budgetTokens) queryOptions.thinking = { type: "enabled", budgetTokens: budgetTokens };
1513
+ }
1514
+
1515
+ if (ls.permissionMode) {
1516
+ // Will be applied below, store for later
1517
+ session._loopPermissionMode = ls.permissionMode;
1518
+ }
1519
+
1520
+ // Pass through any extra SDK settings from LOOP.json
1521
+ if (ls.disableAllHooks !== undefined) {
1522
+ queryOptions.settings = Object.assign({}, queryOptions.settings || {}, { disableAllHooks: ls.disableAllHooks });
1457
1523
  }
1458
1524
 
1459
1525
  if (dangerouslySkipPermissions) {
1460
1526
  queryOptions.allowDangerouslySkipPermissions = true;
1461
1527
  }
1462
1528
  // Pass permissionMode in queryOptions at creation time to avoid race condition
1463
- var modeToApply = session.acceptEditsAfterStart ? "acceptEdits" : sm.currentPermissionMode;
1529
+ var modeToApply = session._loopPermissionMode || (session.acceptEditsAfterStart ? "acceptEdits" : sm.currentPermissionMode);
1464
1530
  if (session.acceptEditsAfterStart) delete session.acceptEditsAfterStart;
1465
1531
  if (modeToApply && modeToApply !== "default") {
1466
1532
  queryOptions.permissionMode = modeToApply;
@@ -1763,7 +1829,10 @@ function createSDKBridge(opts) {
1763
1829
  },
1764
1830
  };
1765
1831
  if (opts.model) mentionQueryOptions.model = opts.model;
1766
- if (opts.includeMcpServers && mcpServers) mentionQueryOptions.mcpServers = mcpServers;
1832
+ if (opts.includeMcpServers) {
1833
+ var _mentionMcp = mergeMcpServers(mcpServers, getRemoteMcpServers);
1834
+ if (_mentionMcp) mentionQueryOptions.mcpServers = _mentionMcp;
1835
+ }
1767
1836
  query = sdk.query({
1768
1837
  prompt: mq,
1769
1838
  options: mentionQueryOptions,
@@ -65,8 +65,13 @@ function attachMates(ctx) {
65
65
  // --- Mate message handlers ---
66
66
 
67
67
  function handleMessage(ws, msg) {
68
- if (!users.isMultiUser() || !ws._clayUser) return false;
69
- var userId = ws._clayUser.id;
68
+ var userId;
69
+ if (users.isMultiUser()) {
70
+ if (!ws._clayUser) return false;
71
+ userId = ws._clayUser.id;
72
+ } else {
73
+ userId = "default";
74
+ }
70
75
 
71
76
  if (msg.type === "mate_create") {
72
77
  if (!msg.seedData) return true;
package/lib/server.js CHANGED
@@ -144,6 +144,8 @@ function createServer(opts) {
144
144
  var onSetServerDefaultMode = opts.onSetServerDefaultMode || null;
145
145
  var onGetProjectDefaultMode = opts.onGetProjectDefaultMode || null;
146
146
  var onSetProjectDefaultMode = opts.onSetProjectDefaultMode || null;
147
+ var onGetProjectMcpServers = opts.onGetProjectMcpServers || null;
148
+ var onSetProjectMcpServers = opts.onSetProjectMcpServers || null;
147
149
  var onGetDaemonConfig = opts.onGetDaemonConfig || null;
148
150
  var onSetPin = opts.onSetPin || null;
149
151
  var onSetKeepAwake = opts.onSetKeepAwake || null;
@@ -766,6 +768,8 @@ function createServer(opts) {
766
768
  return origEmit.apply(ws, arguments);
767
769
  };
768
770
  ws._clayUser = wsUser; // attach user context
771
+ var remoteAddr = req.socket.remoteAddress || "";
772
+ ws._clayLocal = (remoteAddr === "127.0.0.1" || remoteAddr === "::1" || remoteAddr === "::ffff:127.0.0.1");
769
773
  // Clear cross-project unread for this project when client connects
770
774
  var unreadMap = getCrossProjectUnread(ws);
771
775
  if (unreadMap[wsSlug]) {
@@ -984,6 +988,8 @@ function createServer(opts) {
984
988
  onSetServerDefaultMode: onSetServerDefaultMode,
985
989
  onGetProjectDefaultMode: onGetProjectDefaultMode,
986
990
  onSetProjectDefaultMode: onSetProjectDefaultMode,
991
+ onGetProjectMcpServers: onGetProjectMcpServers,
992
+ onSetProjectMcpServers: onSetProjectMcpServers,
987
993
  onGetDaemonConfig: onGetDaemonConfig,
988
994
  onSetPin: onSetPin,
989
995
  onSetKeepAwake: onSetKeepAwake,
package/lib/ws-schema.js CHANGED
@@ -338,6 +338,16 @@ var schema = {
338
338
  "clay_ext_result": { direction: "s2c", handler: "lib/public/modules/app-misc.js", description: "Browser extension command result (broadcast)" },
339
339
  "clay_ext_command": { direction: "c2s", handler: "lib/public/modules/app-misc.js", description: "Send a command to the browser extension" },
340
340
 
341
+ // -----------------------------------------------------------------------
342
+ // MCP Bridge (remote MCP servers via Chrome Extension)
343
+ // -----------------------------------------------------------------------
344
+ "mcp_servers_available": { direction: "c2s", handler: "lib/project-mcp.js", description: "Report available MCP servers from Chrome Extension" },
345
+ "mcp_tool_call": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Forward MCP tool call to extension or HTTP endpoint" },
346
+ "mcp_tool_result": { direction: "c2s", handler: "lib/project-mcp.js", description: "Return MCP tool result from extension" },
347
+ "mcp_tool_error": { direction: "c2s", handler: "lib/project-mcp.js", description: "Return MCP tool error from extension" },
348
+ "mcp_toggle_server": { direction: "c2s", handler: "lib/project-mcp.js", description: "Toggle MCP server enabled state for this project" },
349
+ "mcp_servers_state": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Broadcast MCP server list and enabled state to clients" },
350
+
341
351
  // -----------------------------------------------------------------------
342
352
  // Skills
343
353
  // -----------------------------------------------------------------------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "2.27.1",
3
+ "version": "2.28.0-beta.2",
4
4
  "description": "Self-hosted Claude Code in your browser. Multi-session, multi-user, push notifications.",
5
5
  "bin": {
6
6
  "clay-server": "./bin/cli.js",