clay-server 2.33.1 → 2.34.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.
Files changed (47) hide show
  1. package/lib/ask-user-mcp-server.js +120 -0
  2. package/lib/config.js +9 -13
  3. package/lib/daemon.js +116 -55
  4. package/lib/mate-datastore.js +359 -0
  5. package/lib/mates.js +2 -2
  6. package/lib/os-users.js +70 -37
  7. package/lib/project-connection.js +16 -9
  8. package/lib/project-http.js +3 -4
  9. package/lib/project-image.js +3 -2
  10. package/lib/project-mate-datastore.js +232 -0
  11. package/lib/project-sessions.js +110 -7
  12. package/lib/project-user-message.js +4 -3
  13. package/lib/project.js +126 -10
  14. package/lib/public/app.js +2 -0
  15. package/lib/public/css/mates.css +228 -11
  16. package/lib/public/css/messages.css +23 -0
  17. package/lib/public/css/mobile-nav.css +0 -14
  18. package/lib/public/css/notifications-center.css +80 -0
  19. package/lib/public/css/sidebar.css +326 -101
  20. package/lib/public/index.html +24 -29
  21. package/lib/public/modules/app-dm.js +0 -2
  22. package/lib/public/modules/app-messages.js +23 -0
  23. package/lib/public/modules/app-rendering.js +0 -2
  24. package/lib/public/modules/diff.js +21 -7
  25. package/lib/public/modules/mate-datastore-ui.js +280 -0
  26. package/lib/public/modules/mate-sidebar.js +3 -9
  27. package/lib/public/modules/mate-wizard.js +15 -15
  28. package/lib/public/modules/sidebar-mobile.js +10 -20
  29. package/lib/public/modules/sidebar-sessions.js +490 -113
  30. package/lib/public/modules/sidebar.js +8 -6
  31. package/lib/public/modules/tools.js +115 -18
  32. package/lib/public/sw.js +1 -1
  33. package/lib/sdk-bridge.js +56 -41
  34. package/lib/sdk-message-processor.js +21 -4
  35. package/lib/server.js +28 -72
  36. package/lib/sessions.js +157 -20
  37. package/lib/updater.js +2 -2
  38. package/lib/users.js +2 -2
  39. package/lib/ws-schema.js +16 -0
  40. package/lib/yoke/adapters/claude-worker.js +114 -2
  41. package/lib/yoke/adapters/claude.js +56 -5
  42. package/lib/yoke/adapters/codex.js +350 -58
  43. package/lib/yoke/index.js +93 -48
  44. package/lib/yoke/instructions.js +0 -1
  45. package/lib/yoke/mcp-bridge-server.js +14 -6
  46. package/package.json +1 -2
  47. package/lib/yoke/adapters/gemini.js +0 -709
@@ -94,12 +94,14 @@ export function initSidebar(_ctx) {
94
94
  }
95
95
  } catch (e) {}
96
96
 
97
- ctx.newSessionBtn.addEventListener("click", function () {
98
- if (ctx.ws && ctx.connected) {
99
- ctx.ws.send(JSON.stringify({ type: "new_session" }));
100
- closeSidebar();
101
- }
102
- });
97
+ if (ctx.newSessionBtn) {
98
+ ctx.newSessionBtn.addEventListener("click", function () {
99
+ if (ctx.ws && ctx.connected) {
100
+ ctx.ws.send(JSON.stringify({ type: "new_session" }));
101
+ closeSidebar();
102
+ }
103
+ });
104
+ }
103
105
 
104
106
  // --- Loop (Ralph wizard) tool-palette tile ---
105
107
  // The tile is rendered by tool-palette.js at the stable id
@@ -1,7 +1,7 @@
1
1
  import { escapeHtml, copyToClipboard } from './utils.js';
2
2
  import { iconHtml, refreshIcons } from './icons.js';
3
3
  import { renderMarkdown, highlightCodeBlocks, renderMermaidBlocks } from './markdown.js';
4
- import { renderUnifiedDiff, renderSplitDiff, renderPatchDiff } from './diff.js';
4
+ import { renderUnifiedDiff, renderSplitDiff, renderPatchDiff, reconstructPatchSources } from './diff.js';
5
5
  import { openFile } from './filebrowser.js';
6
6
  import { mateAvatarUrl } from './avatar.js';
7
7
  import { getChatLayout } from './theme.js';
@@ -20,6 +20,14 @@ var todoItems = [];
20
20
  var todoWidgetEl = null;
21
21
  var todoWidgetVisible = true; // whether in-chat widget is in viewport
22
22
  var todoObserver = null;
23
+ var todoMeta = {
24
+ variant: "tasks",
25
+ title: "Tasks",
26
+ icon: "list-checks",
27
+ showProgress: true,
28
+ showCompletedCount: true,
29
+ stickyEnabled: true,
30
+ };
23
31
 
24
32
  // --- Tool tracking ---
25
33
  var tools = {};
@@ -791,7 +799,7 @@ function resolvePermissionIdentity(mateId, vendor) {
791
799
  }
792
800
  }
793
801
  // Project chat: use vendor name and avatar
794
- var vendorAvatars = { claude: "/claude-code-avatar.png", codex: "/codex-avatar.png", gemini: "/gemini-avatar.png" };
802
+ var vendorAvatars = { claude: "/claude-code-avatar.png", codex: "/codex-avatar.png" };
795
803
  var vendorName = (vendor && VENDOR_NAMES[vendor]) || VENDOR_NAMES.claude;
796
804
  return {
797
805
  name: vendorName,
@@ -1262,6 +1270,7 @@ function todoStatusIcon(status) {
1262
1270
 
1263
1271
  export function handleTodoWrite(input) {
1264
1272
  if (!input || !Array.isArray(input.todos)) return;
1273
+ todoMeta = normalizeTodoMeta(input.meta);
1265
1274
  todoItems = input.todos.map(function (t, i) {
1266
1275
  return {
1267
1276
  id: t.id || String(i + 1),
@@ -1275,6 +1284,7 @@ export function handleTodoWrite(input) {
1275
1284
 
1276
1285
  export function handleTaskCreate(input) {
1277
1286
  if (!input) return;
1287
+ todoMeta = normalizeTodoMeta();
1278
1288
  var id = String(todoItems.length + 1);
1279
1289
  todoItems.push({
1280
1290
  id: id,
@@ -1287,6 +1297,7 @@ export function handleTaskCreate(input) {
1287
1297
 
1288
1298
  export function handleTaskUpdate(input) {
1289
1299
  if (!input || !input.taskId) return;
1300
+ todoMeta = normalizeTodoMeta();
1290
1301
  for (var i = 0; i < todoItems.length; i++) {
1291
1302
  if (todoItems[i].id === input.taskId) {
1292
1303
  if (input.status === "deleted") {
@@ -1302,11 +1313,33 @@ export function handleTaskUpdate(input) {
1302
1313
  renderTodoWidget();
1303
1314
  }
1304
1315
 
1316
+ function normalizeTodoMeta(meta) {
1317
+ if (meta && meta.variant === "plan") {
1318
+ return {
1319
+ variant: "plan",
1320
+ title: "Plan",
1321
+ icon: "map",
1322
+ showProgress: false,
1323
+ showCompletedCount: false,
1324
+ stickyEnabled: false,
1325
+ };
1326
+ }
1327
+ return {
1328
+ variant: "tasks",
1329
+ title: "Tasks",
1330
+ icon: "list-checks",
1331
+ showProgress: true,
1332
+ showCompletedCount: true,
1333
+ stickyEnabled: true,
1334
+ };
1335
+ }
1336
+
1305
1337
  function renderTodoWidget() {
1306
1338
  if (todoItems.length === 0) {
1307
1339
  if (todoWidgetEl) { todoWidgetEl.remove(); todoWidgetEl = null; }
1308
1340
  if (todoObserver) { todoObserver.disconnect(); todoObserver = null; }
1309
1341
  todoWidgetVisible = true;
1342
+ todoMeta = normalizeTodoMeta();
1310
1343
  updateTodoSticky();
1311
1344
  return;
1312
1345
  }
@@ -1316,19 +1349,26 @@ function renderTodoWidget() {
1316
1349
  todoWidgetEl = document.createElement("div");
1317
1350
  todoWidgetEl.className = "todo-widget";
1318
1351
  }
1352
+ todoWidgetEl.className = "todo-widget" + (todoMeta.variant === "plan" ? " todo-widget-plan" : "");
1319
1353
 
1320
1354
  var completed = 0;
1321
1355
  for (var i = 0; i < todoItems.length; i++) {
1322
1356
  if (todoItems[i].status === "completed") completed++;
1323
1357
  }
1324
1358
 
1359
+ var countText = todoMeta.showCompletedCount
1360
+ ? (completed + "/" + todoItems.length)
1361
+ : (todoItems.length + " " + (todoItems.length === 1 ? "step" : "steps"));
1362
+
1325
1363
  var html = '<div class="todo-header">' +
1326
- '<span class="todo-header-icon">' + iconHtml("list-checks") + '</span>' +
1327
- '<span class="todo-header-title">Tasks</span>' +
1328
- '<span class="todo-header-count">' + completed + '/' + todoItems.length + '</span>' +
1364
+ '<span class="todo-header-icon">' + iconHtml(todoMeta.icon) + '</span>' +
1365
+ '<span class="todo-header-title">' + todoMeta.title + '</span>' +
1366
+ '<span class="todo-header-count">' + countText + '</span>' +
1329
1367
  '</div>';
1330
- html += '<div class="todo-progress"><div class="todo-progress-bar" style="width:' +
1331
- (todoItems.length > 0 ? Math.round(completed / todoItems.length * 100) : 0) + '%"></div></div>';
1368
+ if (todoMeta.showProgress) {
1369
+ html += '<div class="todo-progress"><div class="todo-progress-bar" style="width:' +
1370
+ (todoItems.length > 0 ? Math.round(completed / todoItems.length * 100) : 0) + '%"></div></div>';
1371
+ }
1332
1372
  html += '<div class="todo-items">';
1333
1373
  for (var i = 0; i < todoItems.length; i++) {
1334
1374
  var t = todoItems[i];
@@ -1369,6 +1409,10 @@ function setupTodoObserver() {
1369
1409
  function updateTodoStickyVisibility() {
1370
1410
  var stickyEl = document.getElementById("todo-sticky");
1371
1411
  if (!stickyEl) return;
1412
+ if (!todoMeta.stickyEnabled) {
1413
+ stickyEl.classList.add("hidden");
1414
+ return;
1415
+ }
1372
1416
 
1373
1417
  if (todoWidgetVisible) {
1374
1418
  stickyEl.classList.add("hidden");
@@ -1387,6 +1431,11 @@ function updateTodoStickyVisibility() {
1387
1431
  function updateTodoSticky() {
1388
1432
  var stickyEl = document.getElementById("todo-sticky");
1389
1433
  if (!stickyEl) return;
1434
+ if (!todoMeta.stickyEnabled) {
1435
+ stickyEl.classList.add("hidden");
1436
+ stickyEl.innerHTML = "";
1437
+ return;
1438
+ }
1390
1439
 
1391
1440
  // Hide if no active tasks (all completed or empty)
1392
1441
  var hasActive = false;
@@ -1538,14 +1587,32 @@ export function stopThinking(duration) {
1538
1587
  } else {
1539
1588
  currentThinking.el.querySelector(".thinking-duration").textContent = " " + secs.toFixed(1) + "s";
1540
1589
  }
1541
- // In mate mode: hide sparkle activity, show compact expandable thinking header
1590
+ // If no thinking text was streamed (e.g. Codex reasoning items arrive
1591
+ // with encrypted/hidden content, or Claude without extended-thinking),
1592
+ // the expand affordance is misleading because there's nothing inside.
1593
+ // Strip the chevron and the click handler so the header reads as a
1594
+ // plain label.
1595
+ var hasContent = !!(currentThinking.fullText && currentThinking.fullText.length > 0);
1596
+ if (!hasContent) {
1597
+ currentThinking.el.classList.add("empty");
1598
+ var chev = currentThinking.el.querySelector(".thinking-chevron");
1599
+ if (chev) chev.style.display = "none";
1600
+ var hdr = currentThinking.el.querySelector(".thinking-header");
1601
+ if (hdr) {
1602
+ hdr.style.cursor = "default";
1603
+ // Replace click listener by cloning the node (cheapest way to strip listeners).
1604
+ var clone = hdr.cloneNode(true);
1605
+ hdr.parentNode.replaceChild(clone, hdr);
1606
+ }
1607
+ }
1608
+ // In mate mode: hide sparkle activity, show compact thinking header.
1542
1609
  if (currentThinking.el.classList.contains("mate-thinking")) {
1543
1610
  var actRow = currentThinking.el.querySelector(".mate-thinking-activity");
1544
1611
  if (actRow) actRow.style.display = "none";
1545
1612
  var header = currentThinking.el.querySelector(".thinking-header");
1546
1613
  if (header) {
1547
1614
  header.style.display = "";
1548
- header.style.cursor = "pointer";
1615
+ header.style.cursor = hasContent ? "pointer" : "default";
1549
1616
  }
1550
1617
  }
1551
1618
  currentThinking = null;
@@ -1673,12 +1740,12 @@ export function updateToolExecuting(id, name, input) {
1673
1740
  ctx.scrollToBottom();
1674
1741
  }
1675
1742
 
1676
- function renderEditDiff(oldStr, newStr, filePath) {
1743
+ // Shared chrome (filename header + unified/split toggle) for diff renderings.
1744
+ // makeUnified and makeSplit are factories that return a fresh body element.
1745
+ function buildDiffChrome(filePath, linkOldStr, linkNewStr, makeUnified, makeSplit) {
1677
1746
  var wrapper = document.createElement("div");
1678
1747
  wrapper.className = "edit-diff";
1679
- var lang = getLanguageFromPath(filePath);
1680
1748
 
1681
- // Header with file path and split toggle (desktop only)
1682
1749
  var header = document.createElement("div");
1683
1750
  header.className = "edit-diff-header";
1684
1751
 
@@ -1691,7 +1758,7 @@ function renderEditDiff(oldStr, newStr, filePath) {
1691
1758
  e.stopPropagation();
1692
1759
  openFile(fp, { diff: { oldStr: os || "", newStr: ns || "" } });
1693
1760
  });
1694
- })(filePath, oldStr, newStr);
1761
+ })(filePath, linkOldStr, linkNewStr);
1695
1762
  }
1696
1763
  header.appendChild(pathSpan);
1697
1764
 
@@ -1717,7 +1784,7 @@ function renderEditDiff(oldStr, newStr, filePath) {
1717
1784
 
1718
1785
  wrapper.appendChild(header);
1719
1786
 
1720
- var currentBody = renderUnifiedDiff(oldStr, newStr, lang);
1787
+ var currentBody = makeUnified();
1721
1788
  wrapper.appendChild(currentBody);
1722
1789
 
1723
1790
  unifiedBtn.addEventListener("click", function (e) {
@@ -1727,7 +1794,7 @@ function renderEditDiff(oldStr, newStr, filePath) {
1727
1794
  unifiedBtn.classList.add("active");
1728
1795
  splitBtn.classList.remove("active");
1729
1796
  wrapper.removeChild(currentBody);
1730
- currentBody = renderUnifiedDiff(oldStr, newStr, lang);
1797
+ currentBody = makeUnified();
1731
1798
  wrapper.appendChild(currentBody);
1732
1799
  refreshIcons();
1733
1800
  });
@@ -1739,7 +1806,7 @@ function renderEditDiff(oldStr, newStr, filePath) {
1739
1806
  splitBtn.classList.add("active");
1740
1807
  unifiedBtn.classList.remove("active");
1741
1808
  wrapper.removeChild(currentBody);
1742
- currentBody = renderSplitDiff(oldStr, newStr, lang);
1809
+ currentBody = makeSplit();
1743
1810
  wrapper.appendChild(currentBody);
1744
1811
  refreshIcons();
1745
1812
  });
@@ -1747,6 +1814,29 @@ function renderEditDiff(oldStr, newStr, filePath) {
1747
1814
  return wrapper;
1748
1815
  }
1749
1816
 
1817
+ function renderEditDiff(oldStr, newStr, filePath) {
1818
+ var lang = getLanguageFromPath(filePath);
1819
+ return buildDiffChrome(
1820
+ filePath,
1821
+ oldStr,
1822
+ newStr,
1823
+ function () { return renderUnifiedDiff(oldStr, newStr, lang); },
1824
+ function () { return renderSplitDiff(oldStr, newStr, lang); }
1825
+ );
1826
+ }
1827
+
1828
+ function renderPatchDiffBlock(patchText, filePath) {
1829
+ var lang = getLanguageFromPath(filePath);
1830
+ var sources = reconstructPatchSources(patchText);
1831
+ return buildDiffChrome(
1832
+ filePath,
1833
+ sources.oldStr,
1834
+ sources.newStr,
1835
+ function () { return renderPatchDiff(patchText, lang); },
1836
+ function () { return renderSplitDiff(sources.oldStr, sources.newStr, lang); }
1837
+ );
1838
+ }
1839
+
1750
1840
  function isDiffContent(text) {
1751
1841
  var lines = text.split("\n");
1752
1842
  var hasHunkHeader = false;
@@ -1854,8 +1944,12 @@ export function updateToolResult(id, content, isError, images) {
1854
1944
  if (hasEditDiff) {
1855
1945
  resultBlock.appendChild(renderEditDiff(tool.input.old_string, tool.input.new_string, tool.input.file_path));
1856
1946
  } else if (!isError && isDiffContent(displayContent)) {
1857
- var patchLang = tool.input && tool.input.file_path ? getLanguageFromPath(tool.input.file_path) : null;
1858
- resultBlock.appendChild(renderPatchDiff(displayContent, patchLang));
1947
+ var patchFilePath = tool.input && tool.input.file_path ? tool.input.file_path : null;
1948
+ if (patchFilePath) {
1949
+ resultBlock.appendChild(renderPatchDiffBlock(displayContent, patchFilePath));
1950
+ } else {
1951
+ resultBlock.appendChild(renderPatchDiff(displayContent, null));
1952
+ }
1859
1953
  } else if (!isError && tool.name === "Read" && tool.input && tool.input.file_path && isImagePath(tool.input.file_path)) {
1860
1954
  // Image file: show inline preview
1861
1955
  var imgWrap = document.createElement("div");
@@ -2195,6 +2289,7 @@ export function saveToolState() {
2195
2289
  tools: tools,
2196
2290
  currentThinking: currentThinking,
2197
2291
  todoWidgetEl: todoWidgetEl,
2292
+ todoMeta: todoMeta,
2198
2293
  inPlanMode: inPlanMode,
2199
2294
  planContent: planContent,
2200
2295
  currentPlanCardEl: currentPlanCardEl,
@@ -2209,6 +2304,7 @@ export function restoreToolState(saved) {
2209
2304
  tools = saved.tools;
2210
2305
  currentThinking = saved.currentThinking;
2211
2306
  todoWidgetEl = saved.todoWidgetEl;
2307
+ todoMeta = saved.todoMeta || normalizeTodoMeta();
2212
2308
  inPlanMode = saved.inPlanMode;
2213
2309
  planContent = saved.planContent;
2214
2310
  currentPlanCardEl = saved.currentPlanCardEl || null;
@@ -2229,6 +2325,7 @@ export function resetToolState() {
2229
2325
  planContent = null;
2230
2326
  currentPlanCardEl = null;
2231
2327
  todoItems = [];
2328
+ todoMeta = normalizeTodoMeta();
2232
2329
  todoWidgetEl = null;
2233
2330
  todoWidgetVisible = true;
2234
2331
  if (todoObserver) { todoObserver.disconnect(); todoObserver = null; }
package/lib/public/sw.js CHANGED
@@ -1,4 +1,4 @@
1
- var CACHE_NAME = "clay-offline-v2";
1
+ var CACHE_NAME = "clay-offline-v3";
2
2
 
3
3
  self.addEventListener("install", function (event) {
4
4
  event.waitUntil(self.skipWaiting());
package/lib/sdk-bridge.js CHANGED
@@ -1,7 +1,8 @@
1
1
  const crypto = require("crypto");
2
2
  var fs = require("fs");
3
3
  var path = require("path");
4
- var { execSync } = require("child_process");
4
+ var execSync = require("child_process").execSync;
5
+ var execFileSync = require("child_process").execFileSync;
5
6
  var usersModule = require("./users");
6
7
  var { getCodexConfig } = require("./codex-defaults");
7
8
  var { splitShellSegments, attachSkillDiscovery } = require("./sdk-skill-discovery");
@@ -373,12 +374,12 @@ function createSDKBridge(opts) {
373
374
  // Create and chown the project directory once
374
375
  if (!fs.existsSync(dstDir)) {
375
376
  fs.mkdirSync(dstDir, { recursive: true });
376
- try { require("child_process").execSync("chown -R " + uid + " " + JSON.stringify(path.join(linuxUserHome, ".claude"))); } catch (e2) {}
377
+ try { execFileSync("chown", ["-R", String(uid), path.join(linuxUserHome, ".claude")]); } catch (e2) {}
377
378
  } else {
378
379
  try {
379
380
  var dirStat = fs.statSync(dstDir);
380
381
  if (dirStat.uid !== uid) {
381
- require("child_process").execSync("chown " + uid + " " + JSON.stringify(dstDir));
382
+ execFileSync("chown", [String(uid), dstDir]);
382
383
  }
383
384
  } catch (e2) {}
384
385
  }
@@ -389,7 +390,7 @@ function createSDKBridge(opts) {
389
390
  var dstFile = path.join(dstDir, sessionFileName);
390
391
  if (fs.existsSync(srcFile) && !fs.existsSync(dstFile)) {
391
392
  fs.copyFileSync(srcFile, dstFile);
392
- try { require("child_process").execSync("chown " + uid + " " + JSON.stringify(dstFile)); } catch (e2) {}
393
+ try { execFileSync("chown", [String(uid), dstFile]); } catch (e2) {}
393
394
  console.log("[sdk-bridge] Pre-copied CLI session " + session.cliSessionId + " to " + linuxUser);
394
395
  }
395
396
  }
@@ -447,6 +448,12 @@ function createSDKBridge(opts) {
447
448
  }
448
449
  }
449
450
 
451
+ // Auto-approve Mate datastore tools. These are scoped to the active Mate
452
+ // project and already enforce SQL policy server-side.
453
+ if (toolName.indexOf("mcp__clay-datastore__") === 0) {
454
+ return { behavior: "allow", updatedInput: input };
455
+ }
456
+
450
457
  // Auto-approve remote MCP tools that the user explicitly enabled in project settings.
451
458
  // These are user-owned local MCP servers, so no additional permission prompt needed.
452
459
  if (toolName.indexOf("mcp__") === 0 && getRemoteMcpServers) {
@@ -646,7 +653,7 @@ function createSDKBridge(opts) {
646
653
  */
647
654
  function findConflictingClaude() {
648
655
  try {
649
- var output = execSync("ps ax -o pid,command 2>/dev/null", { encoding: "utf8", timeout: 5000 });
656
+ var output = execFileSync("ps", ["ax", "-o", "pid,command"], { encoding: "utf8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
650
657
  var lines = output.trim().split("\n");
651
658
  var candidates = [];
652
659
  for (var i = 1; i < lines.length; i++) { // skip header
@@ -691,7 +698,7 @@ function createSDKBridge(opts) {
691
698
  */
692
699
  function isClaudeProcess(pid) {
693
700
  try {
694
- var output = execSync("ps -p " + pid + " -o command= 2>/dev/null", { encoding: "utf8", timeout: 3000 }).trim();
701
+ var output = execFileSync("ps", ["-p", String(pid), "-o", "command="], { encoding: "utf8", timeout: 3000, stdio: ["pipe", "pipe", "pipe"] }).trim();
695
702
  return /\/claude(\s|$)/.test(output) || /^claude(\s|$)/.test(output);
696
703
  } catch (e) {
697
704
  return false;
@@ -1168,17 +1175,21 @@ function createSDKBridge(opts) {
1168
1175
 
1169
1176
  if (dangerouslySkipPermissions) {
1170
1177
  claudeOpts.allowDangerouslySkipPermissions = true;
1178
+ claudeOpts.permissionMode = "bypassPermissions";
1179
+ } else {
1180
+ var globalMode = sm.currentPermissionMode || "default";
1181
+ var effectiveDefault;
1182
+ if (globalMode === "bypassPermissions") effectiveDefault = "bypassPermissions";
1183
+ else if (session.acceptEditsAfterStart) effectiveDefault = "acceptEdits";
1184
+ else effectiveDefault = globalMode;
1185
+ var modeToApply = session._loopPermissionMode || effectiveDefault;
1186
+ if (modeToApply && modeToApply !== "default") {
1187
+ claudeOpts.permissionMode = modeToApply;
1188
+ }
1171
1189
  }
1172
- var globalMode = sm.currentPermissionMode || "default";
1173
- var effectiveDefault;
1174
- if (globalMode === "bypassPermissions") effectiveDefault = "bypassPermissions";
1175
- else if (session.acceptEditsAfterStart) effectiveDefault = "acceptEdits";
1176
- else effectiveDefault = globalMode;
1177
- var modeToApply = session._loopPermissionMode || effectiveDefault;
1190
+ // Clear one-shot acceptEditsAfterStart regardless of which branch ran above,
1191
+ // so the flag does not linger into subsequent turns.
1178
1192
  if (session.acceptEditsAfterStart) delete session.acceptEditsAfterStart;
1179
- if (modeToApply && modeToApply !== "default") {
1180
- claudeOpts.permissionMode = modeToApply;
1181
- }
1182
1193
  if (session.cliSessionId && session.lastRewindUuid) {
1183
1194
  claudeOpts.resumeSessionAt = session.lastRewindUuid;
1184
1195
  delete session.lastRewindUuid;
@@ -1199,21 +1210,30 @@ function createSDKBridge(opts) {
1199
1210
  }
1200
1211
  }
1201
1212
 
1202
- // Use vendor-specific model: if session vendor differs from default, use that vendor's default model
1213
+ // Pick a model that belongs to the session's vendor. sm.currentModel is
1214
+ // shared project-wide, so a Codex session that last set it to
1215
+ // "gpt-5.4-mini" would otherwise leak into a Claude session in the same
1216
+ // project (or in another session that switches vendor to claude) and
1217
+ // Claude would reject the unknown model. We validate against the
1218
+ // session vendor's model list regardless of which vendor happens to be
1219
+ // the project's default adapter.
1203
1220
  var queryModel = ls.model || sm.currentModel || undefined;
1204
- if (session.vendor && session.vendor !== (adapter && adapter.vendor)) {
1205
- var vendorModels = (sm.modelsByVendor && sm.modelsByVendor[session.vendor]) || [];
1221
+ var sessionVendor = session.vendor || (adapter && adapter.vendor) || null;
1222
+ if (sessionVendor) {
1223
+ var vendorModels = (sm.modelsByVendor && sm.modelsByVendor[sessionVendor]) || [];
1206
1224
  if (vendorModels.length > 0 && queryModel && vendorModels.indexOf(queryModel) === -1) {
1207
1225
  queryModel = vendorModels[0];
1208
1226
  }
1209
1227
  }
1210
1228
 
1211
1229
  var codexConfig = getCodexConfig(sm);
1230
+ var mergedMcpServers = mergeMcpServers(getMcpServers(), getRemoteMcpServers) || undefined;
1212
1231
  var queryOpts = {
1213
1232
  cwd: cwd,
1214
1233
  model: queryModel,
1215
1234
  effort: ls.effort || sm.currentEffort || undefined,
1216
- toolServers: mergeMcpServers(getMcpServers(), getRemoteMcpServers) || undefined,
1235
+ toolServers: mergedMcpServers,
1236
+ toolServerDescriptors: extractMcpDescriptors(mergedMcpServers) || undefined,
1217
1237
  resumeSessionId: session.cliSessionId || undefined,
1218
1238
  abortController: linuxUser ? undefined : session.abortController,
1219
1239
  canUseTool: function(toolName, input, toolOpts) {
@@ -1222,6 +1242,9 @@ function createSDKBridge(opts) {
1222
1242
  onElicitation: function(request, elicitOpts) {
1223
1243
  return handleElicitation(session, request, elicitOpts);
1224
1244
  },
1245
+ callMcpTool: function(serverName, toolName, args) {
1246
+ return callMcpToolHandler(mergedMcpServers, serverName, toolName, args);
1247
+ },
1225
1248
  adapterOptions: {
1226
1249
  CLAUDE: claudeOpts,
1227
1250
  CODEX: {
@@ -1349,17 +1372,17 @@ function createSDKBridge(opts) {
1349
1372
  // Detect which vendor binaries are installed for this user.
1350
1373
  // In multi-user mode, runs checks as the specific Linux user.
1351
1374
  function detectInstalledVendors(linuxUser) {
1352
- var execSync = require("child_process").execSync;
1375
+ var execFileSync = require("child_process").execFileSync;
1353
1376
  var fs = require("fs");
1354
- var path = require("path");
1355
1377
  var result = [];
1356
1378
 
1357
- function tryExec(cmd) {
1379
+ function tryLookup(name) {
1358
1380
  try {
1359
1381
  if (linuxUser) {
1360
- execSync("su - " + linuxUser + " -c " + JSON.stringify(cmd), { timeout: 3000, stdio: ["pipe", "pipe", "pipe"] });
1382
+ execFileSync("su", ["-", linuxUser, "-c", "which " + name], { timeout: 3000, stdio: ["pipe", "pipe", "pipe"] });
1361
1383
  } else {
1362
- execSync(cmd, { timeout: 3000, stdio: ["pipe", "pipe", "pipe"] });
1384
+ if (process.platform === "win32") execFileSync("where", [name], { timeout: 3000, stdio: ["pipe", "pipe", "pipe"] });
1385
+ else execFileSync("which", [name], { timeout: 3000, stdio: ["pipe", "pipe", "pipe"] });
1363
1386
  }
1364
1387
  return true;
1365
1388
  } catch (e) {
@@ -1368,14 +1391,14 @@ function createSDKBridge(opts) {
1368
1391
  }
1369
1392
 
1370
1393
  // Claude: check if binary is in PATH
1371
- if (tryExec("which claude")) result.push("claude");
1394
+ if (tryLookup("claude")) result.push("claude");
1372
1395
 
1373
1396
  // Codex: check bundled binary or PATH
1374
1397
  var codexBin = null;
1375
1398
  try {
1376
1399
  codexBin = require("./yoke/codex-app-server").findCodexPath();
1377
1400
  } catch (e) {}
1378
- if ((codexBin && fs.existsSync(codexBin)) || tryExec("which codex")) result.push("codex");
1401
+ if ((codexBin && fs.existsSync(codexBin)) || tryLookup("codex")) result.push("codex");
1379
1402
 
1380
1403
  return result;
1381
1404
  }
@@ -1432,22 +1455,14 @@ function createSDKBridge(opts) {
1432
1455
  }
1433
1456
  }
1434
1457
 
1435
- // Initialize other adapters in parallel (skip if already have models cached)
1458
+ // Non-default adapters are NOT eagerly initialized here. Doing so used
1459
+ // to spawn a CodexAppServer and an mcp-bridge child per project even
1460
+ // when the user never touched that vendor. Lazy paths cover the gap:
1461
+ // - get_vendor_models (project.js) inits a vendor when the user
1462
+ // opens its model picker.
1463
+ // - ensureVendorReady (this file) inits a vendor when a session
1464
+ // actually issues a query with it.
1436
1465
  sm.modelsByVendor = sm.modelsByVendor || {};
1437
- var otherVendors = Object.keys(adapters).filter(function(v) {
1438
- return v !== defaultVendor && !sm.modelsByVendor[v];
1439
- });
1440
- for (var i = 0; i < otherVendors.length; i++) {
1441
- (function(v) {
1442
- adapters[v].init({ cwd: cwd, clayPort: clayPort, clayTls: clayTls, clayAuthToken: clayAuthToken, slug: slug }).then(function(r) {
1443
- sm.modelsByVendor[v] = r.models || [];
1444
- sm.capabilitiesByVendor[v] = r.capabilities || {};
1445
- if (r.slashCommands) sm.setSlashCommandsForVendor(v, r.slashCommands);
1446
- }).catch(function(e) {
1447
- console.error("[sdk-bridge] warmup: " + v + " init failed:", e.message || e);
1448
- });
1449
- })(otherVendors[i]);
1450
- }
1451
1466
 
1452
1467
  // Detect installed vendors per-user (binary existence check)
1453
1468
  sm.installedVendors = detectInstalledVendors(linuxUser);
@@ -234,7 +234,13 @@ function attachMessageProcessor(ctx) {
234
234
  type: "tool_executing",
235
235
  id: parsed.turnId || "codex-plan",
236
236
  name: "TodoWrite",
237
- input: { todos: todos },
237
+ input: {
238
+ todos: todos,
239
+ meta: {
240
+ variant: "plan",
241
+ title: parsed.title || "Plan",
242
+ },
243
+ },
238
244
  });
239
245
 
240
246
  } else if (parsed.yokeType === "plan_content") {
@@ -364,12 +370,23 @@ function attachMessageProcessor(ctx) {
364
370
  session.sentToolResults = {};
365
371
  session.pendingPermissions = {};
366
372
  session.pendingElicitations = {};
367
- // Record ask_user_answered for any leftover pending questions so replay pairs correctly
373
+ // Record ask_user_answered for any leftover pending questions so replay pairs correctly.
374
+ // EXCEPTION: "mcp" mode entries are stateless — the tool returned immediately and the
375
+ // turn is expected to end while the card is still awaiting the user's answer. Those
376
+ // entries must survive across turns so the eventual ask_user_response can inject the
377
+ // answer as the next user message. Only blocking modes (Claude canUseTool) get closed.
368
378
  var leftoverAskIds = Object.keys(session.pendingAskUser);
379
+ var keptAskUser = {};
369
380
  for (var lai = 0; lai < leftoverAskIds.length; lai++) {
370
- sendAndRecord(session, { type: "ask_user_answered", toolId: leftoverAskIds[lai] });
381
+ var lid = leftoverAskIds[lai];
382
+ var lentry = session.pendingAskUser[lid];
383
+ if (lentry && lentry.mode === "mcp") {
384
+ keptAskUser[lid] = lentry;
385
+ continue;
386
+ }
387
+ sendAndRecord(session, { type: "ask_user_answered", toolId: lid });
371
388
  }
372
- session.pendingAskUser = {};
389
+ session.pendingAskUser = keptAskUser;
373
390
  session.activeTaskToolIds = {};
374
391
  session.taskIdMap = {};
375
392
  // Only clear rateLimitResetsAt on genuine success (non-zero cost).