clay-server 2.47.0-beta.1 → 2.47.0-beta.3

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 (37) hide show
  1. package/lib/codex-defaults.js +12 -2
  2. package/lib/daemon.js +38 -8
  3. package/lib/kiro-defaults.js +22 -0
  4. package/lib/project-connection.js +3 -1
  5. package/lib/project-notifications.js +3 -1
  6. package/lib/project-sessions.js +28 -9
  7. package/lib/project.js +42 -10
  8. package/lib/public/app.js +8 -0
  9. package/lib/public/css/mobile-nav.css +53 -9
  10. package/lib/public/css/sidebar.css +24 -1
  11. package/lib/public/css/tui-attention.css +4 -63
  12. package/lib/public/index.html +4 -0
  13. package/lib/public/kiro-avatar.svg +14 -0
  14. package/lib/public/modules/app-messages.js +14 -12
  15. package/lib/public/modules/app-notifications.js +12 -4
  16. package/lib/public/modules/app-panels.js +10 -4
  17. package/lib/public/modules/app-rate-limit.js +41 -13
  18. package/lib/public/modules/app-rendering.js +14 -0
  19. package/lib/public/modules/input.js +4 -3
  20. package/lib/public/modules/mate-sidebar.js +3 -3
  21. package/lib/public/modules/session-tui-view.js +4 -40
  22. package/lib/public/modules/sidebar-mates.js +1 -1
  23. package/lib/public/modules/sidebar-mobile.js +56 -23
  24. package/lib/public/modules/sidebar-sessions.js +126 -63
  25. package/lib/public/modules/tools.js +1 -1
  26. package/lib/public/modules/tui-grab.js +12 -3
  27. package/lib/sdk-bridge.js +90 -21
  28. package/lib/sdk-message-processor.js +4 -4
  29. package/lib/server.js +4 -0
  30. package/lib/users-preferences.js +3 -1
  31. package/lib/yoke/adapters/claude.js +25 -0
  32. package/lib/yoke/adapters/codex.js +34 -21
  33. package/lib/yoke/adapters/kiro.js +1170 -0
  34. package/lib/yoke/index.js +70 -6
  35. package/lib/yoke/kiro-acp-server.js +328 -0
  36. package/lib/yoke/vendor-registry.js +55 -0
  37. package/package.json +4 -3
@@ -12,6 +12,7 @@ import { dismissOverlayPanels, closeSidebar, updatePageTitle, spawnDustParticles
12
12
  import { showConfirm } from './app-misc.js';
13
13
  import { getUpcomingSchedules } from './scheduler.js';
14
14
  import { refreshMobileChatSheet } from './sidebar-mobile.js';
15
+ import { VENDOR_AVATARS, VENDOR_NAMES, VENDOR_ORDER, VENDOR_HOMEPAGES } from './app-rendering.js';
15
16
 
16
17
 
17
18
  // --- Session state ---
@@ -292,85 +293,140 @@ function appendSessionCloseButton(el, session) {
292
293
  el.appendChild(closeBtn);
293
294
  }
294
295
 
296
+ // Resolve the vendor the "New session" button should launch: the project's
297
+ // last-used vendor when its CLI is still installed, else the first installed
298
+ // vendor, else Claude (so the button always has a sane label to render).
299
+ export function resolveDefaultVendor() {
300
+ var installed = store.get('installedVendors') || [];
301
+ var last = store.get('lastVendor') || "";
302
+ if (last && installed.indexOf(last) !== -1) return last;
303
+ for (var i = 0; i < VENDOR_ORDER.length; i++) {
304
+ if (installed.indexOf(VENDOR_ORDER[i]) !== -1) return VENDOR_ORDER[i];
305
+ }
306
+ return "claude";
307
+ }
308
+
309
+ // Single entry point for session creation from the sidebar. Optimistically
310
+ // bind the composer to the requested vendor before the server round-trip.
311
+ // Otherwise vendorSelectionLocked from the previous session can reject the
312
+ // new config_state and leave a Kiro/Codex session rendered as Claude.
313
+ export function startNewSession(vendor, extra) {
314
+ if (!getWs() || !store.get('connected')) return;
315
+ var payload = { type: "new_session", vendor: vendor };
316
+ if (extra) {
317
+ var keys = Object.keys(extra);
318
+ for (var i = 0; i < keys.length; i++) payload[keys[i]] = extra[keys[i]];
319
+ }
320
+ getWs().send(JSON.stringify(payload));
321
+ store.set({
322
+ lastVendor: vendor,
323
+ currentVendor: vendor,
324
+ currentModel: "",
325
+ currentModels: [],
326
+ vendorSelectionLocked: true,
327
+ });
328
+ }
329
+
295
330
  function renderSessionTopActions() {
296
331
  var wrap = document.createElement("div");
297
332
  wrap.className = "session-top-actions";
298
333
 
299
- // Claude: split button. The main button creates a session using the user's
300
- // claudeOpenMode pref (server applies it). The chevron opens a menu with
301
- // alternate launch modes (e.g. bypass-permissions, TUI shell only).
302
- var claudeCell = document.createElement("div");
303
- claudeCell.className = "session-top-action-split";
304
-
305
- var claudeBtn = document.createElement("button");
306
- claudeBtn.className = "session-top-action split-main";
307
- claudeBtn.type = "button";
308
- claudeBtn.title = "New Claude session";
309
- claudeBtn.innerHTML = '<img src="/claude-code-avatar.png" class="session-top-action-icon" alt=""><span>Claude</span>';
310
- claudeBtn.addEventListener("click", function () {
311
- if (getWs() && store.get('connected')) {
312
- getWs().send(JSON.stringify({ type: "new_session", vendor: "claude" }));
313
- }
334
+ // Single split button. The main half creates a session with the project's
335
+ // last-used vendor (for Claude the server applies the claudeOpenMode pref);
336
+ // the chevron opens the vendor picker plus alternate launch modes.
337
+ var defaultVendor = resolveDefaultVendor();
338
+
339
+ var cell = document.createElement("div");
340
+ cell.className = "session-top-action-split";
341
+
342
+ var mainBtn = document.createElement("button");
343
+ mainBtn.className = "session-top-action split-main";
344
+ mainBtn.type = "button";
345
+ mainBtn.title = "New " + (VENDOR_NAMES[defaultVendor] || defaultVendor) + " session";
346
+ mainBtn.innerHTML = '<img src="' + (VENDOR_AVATARS[defaultVendor] || VENDOR_AVATARS.claude) +
347
+ '" class="session-top-action-icon" alt=""><span>New session</span>';
348
+ mainBtn.addEventListener("click", function () {
349
+ startNewSession(defaultVendor);
314
350
  });
315
- claudeCell.appendChild(claudeBtn);
316
-
317
- var claudeChevron = document.createElement("button");
318
- claudeChevron.className = "session-top-action split-chevron";
319
- claudeChevron.type = "button";
320
- claudeChevron.title = "More Claude launch options";
321
- claudeChevron.setAttribute("aria-label", "More Claude launch options");
322
- claudeChevron.innerHTML = iconHtml("chevron-down");
323
- claudeChevron.addEventListener("click", function (e) {
351
+ cell.appendChild(mainBtn);
352
+
353
+ var chevron = document.createElement("button");
354
+ chevron.className = "session-top-action split-chevron";
355
+ chevron.type = "button";
356
+ chevron.title = "Choose a vendor or launch mode";
357
+ chevron.setAttribute("aria-label", "Choose a vendor or launch mode");
358
+ chevron.innerHTML = iconHtml("chevron-down");
359
+ chevron.addEventListener("click", function (e) {
324
360
  e.preventDefault();
325
361
  e.stopPropagation();
326
362
  if (sessionCtxMenu) { closeSessionCtxMenu(); return; }
327
- showClaudeStartMenu(claudeChevron);
363
+ showNewSessionMenu(chevron, defaultVendor);
328
364
  });
329
- claudeCell.appendChild(claudeChevron);
330
-
331
- wrap.appendChild(claudeCell);
332
-
333
- // Codex: always GUI (no TUI adapter for Codex).
334
- var codexBtn = document.createElement("button");
335
- codexBtn.className = "session-top-action";
336
- codexBtn.type = "button";
337
- codexBtn.title = "New Codex session";
338
- codexBtn.innerHTML = '<img src="/codex-avatar.png" class="session-top-action-icon" alt=""><span>Codex</span>';
339
- codexBtn.addEventListener("click", function () {
340
- if (getWs() && store.get('connected')) {
341
- getWs().send(JSON.stringify({ type: "new_session", vendor: "codex" }));
342
- }
343
- });
344
- wrap.appendChild(codexBtn);
365
+ cell.appendChild(chevron);
345
366
 
367
+ wrap.appendChild(cell);
346
368
  return wrap;
347
369
  }
348
370
 
349
- // Dropdown anchored to the Claude split-button chevron. Reuses the
350
- // session-ctx-menu element/var so the global document click handler closes it.
351
- function showClaudeStartMenu(anchorBtn) {
371
+ // Dropdown anchored to the split-button chevron. Reuses the session-ctx-menu
372
+ // element/var so the global document click handler closes it.
373
+ function showNewSessionMenu(anchorBtn, defaultVendor) {
352
374
  closeSessionCtxMenu();
353
375
 
354
376
  var menu = document.createElement("div");
355
- menu.className = "session-ctx-menu";
377
+ menu.className = "session-ctx-menu session-new-menu";
378
+ var installed = store.get('installedVendors') || [];
379
+
380
+ // Every known vendor is listed. Uninstalled ones stay visible but disabled,
381
+ // and clicking one opens the vendor's homepage so the user can install it.
382
+ for (var i = 0; i < VENDOR_ORDER.length; i++) {
383
+ (function (vendor) {
384
+ var isInstalled = installed.indexOf(vendor) !== -1;
385
+ var name = VENDOR_NAMES[vendor] || vendor;
386
+ var item = document.createElement("button");
387
+ item.className = "session-ctx-item session-new-vendor";
388
+ if (!isInstalled) item.classList.add("disabled");
389
+ if (vendor === defaultVendor) item.classList.add("active");
390
+ item.type = "button";
391
+ item.innerHTML = '<img src="' + VENDOR_AVATARS[vendor] + '" class="session-top-action-icon" alt="">' +
392
+ '<span class="session-new-vendor-name">' + name + '</span>' +
393
+ (isInstalled ? "" : '<span class="session-new-vendor-note">Not installed</span>' + iconHtml("external-link"));
394
+ item.title = isInstalled
395
+ ? "New " + name + " session"
396
+ : name + " is not installed - open " + VENDOR_HOMEPAGES[vendor];
397
+ item.addEventListener("click", function (e) {
398
+ e.stopPropagation();
399
+ closeSessionCtxMenu();
400
+ if (!isInstalled) {
401
+ window.open(VENDOR_HOMEPAGES[vendor], "_blank", "noopener");
402
+ return;
403
+ }
404
+ startNewSession(vendor);
405
+ });
406
+ menu.appendChild(item);
407
+ })(VENDOR_ORDER[i]);
408
+ }
356
409
 
357
- var skipItem = document.createElement("button");
358
- skipItem.className = "session-ctx-item";
359
- skipItem.innerHTML = iconHtml("shield-off") + " <span>Skip permissions (TUI)</span>";
360
- skipItem.title = "Start a terminal session with --dangerously-skip-permissions";
361
- skipItem.addEventListener("click", function (e) {
362
- e.stopPropagation();
363
- closeSessionCtxMenu();
364
- if (getWs() && store.get('connected')) {
365
- getWs().send(JSON.stringify({
366
- type: "new_session",
367
- vendor: "claude",
368
- mode: "tui",
369
- dangerouslySkipPermissions: true,
370
- }));
371
- }
372
- });
373
- menu.appendChild(skipItem);
410
+ // Claude-only launch mode: a bare TUI shell with permission prompts off.
411
+ // Hidden in GUI mode (the default) because the item only ever produces a
412
+ // terminal session, which contradicts what the rest of the menu creates.
413
+ if (installed.indexOf("claude") !== -1 && store.get('claudeOpenMode') === "tui") {
414
+ var sep = document.createElement("div");
415
+ sep.className = "session-ctx-sep";
416
+ menu.appendChild(sep);
417
+
418
+ var skipItem = document.createElement("button");
419
+ skipItem.className = "session-ctx-item";
420
+ skipItem.type = "button";
421
+ skipItem.innerHTML = iconHtml("shield-off") + " <span>Skip permissions (TUI)</span>";
422
+ skipItem.title = "Start a terminal session with --dangerously-skip-permissions";
423
+ skipItem.addEventListener("click", function (e) {
424
+ e.stopPropagation();
425
+ closeSessionCtxMenu();
426
+ startNewSession("claude", { mode: "tui", dangerouslySkipPermissions: true });
427
+ });
428
+ menu.appendChild(skipItem);
429
+ }
374
430
 
375
431
  document.body.appendChild(menu);
376
432
  sessionCtxMenu = menu;
@@ -528,6 +584,14 @@ export function initSidebarSessions() {
528
584
  syncHeaderSearchUi();
529
585
  }
530
586
 
587
+ // The "New session" split button renders from lastVendor + installedVendors,
588
+ // so a late-arriving vendor detection or a vendor switch has to repaint it.
589
+ store.subscribe(function (state, prev) {
590
+ if (state.lastVendor !== prev.lastVendor || state.installedVendors !== prev.installedVendors) {
591
+ renderSessionList(null);
592
+ }
593
+ });
594
+
531
595
  // --- Resume session picker ---
532
596
  // --- Schedule countdown timer ---
533
597
  startCountdownTimer();
@@ -1411,4 +1475,3 @@ function relativeTime(isoString) {
1411
1475
  if (days < 30) return days + "d ago";
1412
1476
  return new Date(isoString).toLocaleDateString();
1413
1477
  }
1414
-
@@ -814,7 +814,7 @@ function resolvePermissionIdentity(mateId, vendor) {
814
814
  }
815
815
  }
816
816
  // Project chat: use vendor name and avatar
817
- var vendorAvatars = { claude: "/claude-code-avatar.png", codex: "/codex-avatar.png" };
817
+ var vendorAvatars = { claude: "/claude-code-avatar.png", codex: "/codex-avatar.png", kiro: "/kiro-avatar.svg" };
818
818
  var vendorName = (vendor && VENDOR_NAMES[vendor]) || VENDOR_NAMES.claude;
819
819
  return {
820
820
  name: vendorName,
@@ -24,6 +24,8 @@
24
24
  // Codex sessions never receive a transcript_state (no JSONL), so the
25
25
  // overlay stays disabled — selection copy still works normally.
26
26
 
27
+ import { store } from './store.js';
28
+
27
29
  import { copyToClipboard, showToast } from './utils.js';
28
30
  import { getWs } from './ws-ref.js';
29
31
 
@@ -629,9 +631,16 @@ function requestTranscript(localId) {
629
631
  export function attachTuiGrab(xterm, containerEl, localId, opts) {
630
632
  if (!xterm || !containerEl || !localId) return;
631
633
  clearActive();
632
- // For non-claude vendors (e.g. Codex) we don't even bother — no
633
- // transcript exists on disk, so there's nothing to match against.
634
- if (opts && opts.vendor && opts.vendor !== "claude") return;
634
+ // Vendors without TUI sessions have no transcript to match against.
635
+ if (opts && opts.vendor) {
636
+ var vendors = store.get('vendorInfo') || {};
637
+ var info = vendors[opts.vendor];
638
+ var legacyTuiVendors = ["claude"];
639
+ var supportsTui = info
640
+ ? Array.isArray(info.sessionModes) && info.sessionModes.indexOf("tui") !== -1
641
+ : legacyTuiVendors.indexOf(opts.vendor) !== -1;
642
+ if (!supportsTui) return;
643
+ }
635
644
 
636
645
  var overlayEl = buildOverlay(containerEl);
637
646
  active = {
package/lib/sdk-bridge.js CHANGED
@@ -5,10 +5,12 @@ var execSync = require("child_process").execSync;
5
5
  var execFileSync = require("child_process").execFileSync;
6
6
  var usersModule = require("./users");
7
7
  var { getCodexConfig } = require("./codex-defaults");
8
+ var { getKiroConfig } = require("./kiro-defaults");
8
9
  var { splitShellSegments, attachSkillDiscovery } = require("./sdk-skill-discovery");
9
10
  var { isSafeBashSegment } = require("./safe-bash-commands");
10
11
  var { createMessageQueue } = require("./sdk-message-queue");
11
12
  var { attachMessageProcessor } = require("./sdk-message-processor");
13
+ var yoke = require("./yoke");
12
14
 
13
15
  // Extract serializable tool descriptors from MCP server instances.
14
16
  // Used for IPC to worker processes (McpSdkServerConfigWithInstance is not serializable).
@@ -126,7 +128,6 @@ function createSDKBridge(opts) {
126
128
  var _cachedFreshAuthAt = 0;
127
129
 
128
130
  function getFreshAuthState(force) {
129
- var yoke = require("./yoke");
130
131
  var now = Date.now();
131
132
  if (!force && _cachedFreshAuthState && now - _cachedFreshAuthAt < 15000) {
132
133
  return _cachedFreshAuthState;
@@ -149,9 +150,13 @@ function createSDKBridge(opts) {
149
150
  }
150
151
 
151
152
  function getLoginCommand(vendor) {
152
- if (vendor === "codex") return "codex login --device-auth";
153
- if (vendor === "claude") return "claude login";
154
- return (vendor || "claude") + " login";
153
+ var info = yoke.getVendorInfo(vendor);
154
+ return (info && info.loginCommand) || "claude login";
155
+ }
156
+
157
+ function supportsOsUserIsolation(vendor) {
158
+ var info = yoke.getVendorInfo(vendor);
159
+ return !info || info.osUserIsolation !== false;
155
160
  }
156
161
 
157
162
  function notifyAuthRequired(session, title, body, authLinuxUser, canAutoLogin, loginCommand) {
@@ -218,15 +223,25 @@ function createSDKBridge(opts) {
218
223
  }
219
224
 
220
225
  function sendModelInfoForVendor(vendor, model) {
226
+ var resolvedVendor = vendor || (adapter && adapter.vendor) || "claude";
221
227
  send({
222
228
  type: "model_info",
223
229
  model: model || "",
224
- models: getModelsForVendor(vendor),
225
- vendor: vendor || (adapter && adapter.vendor) || "claude",
230
+ models: getModelsForVendor(resolvedVendor),
231
+ vendor: resolvedVendor,
232
+ capabilities: (sm.capabilitiesByVendor && sm.capabilitiesByVendor[resolvedVendor]) || {},
226
233
  availableVendors: sm.availableVendors || [],
227
234
  installedVendors: sm.installedVendors || [],
228
235
  });
229
236
  }
237
+
238
+ function rememberAdapterReady(vendor, result) {
239
+ if (!vendor || !result) return;
240
+ sm.modelsByVendor = sm.modelsByVendor || {};
241
+ sm.capabilitiesByVendor = sm.capabilitiesByVendor || {};
242
+ if (Array.isArray(result.models)) sm.modelsByVendor[vendor] = result.models;
243
+ sm.capabilitiesByVendor[vendor] = result.capabilities || {};
244
+ }
230
245
  var onTurnDone = opts.onTurnDone || null;
231
246
 
232
247
  // --- Idle session reaper ---
@@ -799,7 +814,7 @@ function createSDKBridge(opts) {
799
814
  onProcessingChanged();
800
815
  send({ type: "status", processing: false });
801
816
  sendAndRecord(session, { type: "thinking_stop" });
802
- var interruptMsg = (session.vendor === "codex")
817
+ var interruptMsg = (session.vendor && session.vendor !== "claude")
803
818
  ? "\u25a0 Conversation interrupted - tell the model what to do differently."
804
819
  : "Interrupted \u00b7 What should Claude do instead?";
805
820
  sendAndRecord(session, { type: "info", text: interruptMsg });
@@ -813,7 +828,7 @@ function createSDKBridge(opts) {
813
828
  if (err.name === "AbortError" || (myAbortController && myAbortController.signal.aborted) || session.taskStopRequested) {
814
829
  if (!session.destroying) {
815
830
  sendAndRecord(session, { type: "thinking_stop" });
816
- var interruptMsg2 = (session.vendor === "codex")
831
+ var interruptMsg2 = (session.vendor && session.vendor !== "claude")
817
832
  ? "\u25a0 Conversation interrupted - tell the model what to do differently."
818
833
  : "Interrupted \u00b7 What should Claude do instead?";
819
834
  sendAndRecord(session, { type: "info", text: interruptMsg2 });
@@ -867,7 +882,9 @@ function createSDKBridge(opts) {
867
882
  var canAutoLogin = !usersModule.isMultiUser()
868
883
  || !!authLinuxUser
869
884
  || (authUser && authUser.role === "admin");
870
- var authTitle = (session.vendor === "codex" ? "Codex" : "Claude Code") + " is not logged in.";
885
+ var authInfo = yoke.getVendorInfo(session.vendor);
886
+ var authVendorLabel = (authInfo && authInfo.displayName) || "Claude Code";
887
+ var authTitle = authVendorLabel + " is not logged in.";
871
888
  var authMsg = {
872
889
  type: "auth_required",
873
890
  text: authTitle,
@@ -1067,9 +1084,9 @@ function createSDKBridge(opts) {
1067
1084
  async function startQuery(session, text, images, linuxUser) {
1068
1085
  async function ensureVendorReady(vendor) {
1069
1086
  if (!vendor) return null;
1087
+ if (linuxUser && !supportsOsUserIsolation(vendor)) return null;
1070
1088
  var vendorAdapter = adapters[vendor] || null;
1071
1089
  if (!vendorAdapter) {
1072
- var yoke = require("./yoke");
1073
1090
  vendorAdapter = await yoke.lazyCreateAdapter(adapters, vendor, {
1074
1091
  cwd: cwd,
1075
1092
  dangerouslySkipPermissions: dangerouslySkipPermissions,
@@ -1079,8 +1096,11 @@ function createSDKBridge(opts) {
1079
1096
  clayAuthToken: clayAuthToken,
1080
1097
  slug: slug,
1081
1098
  });
1082
- } else if ((!sm.modelsByVendor || !sm.modelsByVendor[vendor]) && typeof vendorAdapter.init === "function") {
1083
- await vendorAdapter.init({
1099
+ }
1100
+ var needsReadyMetadata = !sm.capabilitiesByVendor || !sm.capabilitiesByVendor[vendor]
1101
+ || !sm.modelsByVendor || !sm.modelsByVendor[vendor];
1102
+ if (vendorAdapter && needsReadyMetadata && typeof vendorAdapter.init === "function") {
1103
+ var readyResult = await vendorAdapter.init({
1084
1104
  cwd: cwd,
1085
1105
  dangerouslySkipPermissions: dangerouslySkipPermissions,
1086
1106
  linuxUser: linuxUser || undefined,
@@ -1089,17 +1109,39 @@ function createSDKBridge(opts) {
1089
1109
  clayAuthToken: clayAuthToken,
1090
1110
  slug: slug,
1091
1111
  });
1112
+ rememberAdapterReady(vendor, readyResult);
1092
1113
  }
1093
1114
  if (vendorAdapter) {
1094
- sm.availableVendors = Object.keys(adapters);
1115
+ sm.availableVendors = getAvailableVendors(linuxUser);
1095
1116
  sm.modelsByVendor = sm.modelsByVendor || {};
1096
1117
  if (!sm.modelsByVendor[vendor] && typeof vendorAdapter.supportedModels === "function") {
1097
1118
  sm.modelsByVendor[vendor] = await vendorAdapter.supportedModels();
1098
1119
  }
1120
+ var vendorModels = getModelsForVendor(vendor);
1121
+ var modelForVendor = resolveModelInList(vendorModels, sm.currentModel)
1122
+ || modelEntryValue(vendorModels[0])
1123
+ || "";
1124
+ sendModelInfoForVendor(vendor, modelForVendor);
1099
1125
  }
1100
1126
  return vendorAdapter;
1101
1127
  }
1102
1128
 
1129
+ // Kiro ACP currently spawns as the daemon user. Running it for an
1130
+ // OS-isolated session would expose the daemon user's HOME and Kiro
1131
+ // credentials to another user, so refuse until the adapter has a
1132
+ // per-user worker path.
1133
+ if (linuxUser && !supportsOsUserIsolation(session.vendor)) {
1134
+ var isolatedVendorInfo = yoke.getVendorInfo(session.vendor);
1135
+ var isolatedVendorName = (isolatedVendorInfo && isolatedVendorInfo.displayName) || session.vendor || "This vendor";
1136
+ console.error("[sdk-bridge] Refusing " + isolatedVendorName + " for OS-isolated user " + linuxUser + ": per-user spawning is not implemented");
1137
+ sendAndRecord(session, {
1138
+ type: "error",
1139
+ text: isolatedVendorName + " is not available for OS-isolated users yet.",
1140
+ });
1141
+ sendAndRecord(session, { type: "done", code: 1 });
1142
+ return;
1143
+ }
1144
+
1103
1145
  // If vendor is set but adapter not ready, try lazy creation (user may have logged in)
1104
1146
  if (session.vendor && !adapters[session.vendor]) {
1105
1147
  var lazyAdapter = await ensureVendorReady(session.vendor);
@@ -1121,7 +1163,9 @@ function createSDKBridge(opts) {
1121
1163
  }
1122
1164
  // If still not available after lazy check, send auth_required
1123
1165
  if (session.vendor && !adapters[session.vendor]) {
1124
- var vendorName = session.vendor.charAt(0).toUpperCase() + session.vendor.slice(1);
1166
+ var missingVendorInfo = yoke.getVendorInfo(session.vendor);
1167
+ var vendorName = (missingVendorInfo && missingVendorInfo.displayName)
1168
+ || session.vendor.charAt(0).toUpperCase() + session.vendor.slice(1);
1125
1169
  var authUser = session.ownerId ? usersModule.findUserById(session.ownerId) : null;
1126
1170
  var authLinuxUser = authUser && authUser.linuxUser ? authUser.linuxUser : null;
1127
1171
  var canAutoLogin = !usersModule.isMultiUser()
@@ -1292,6 +1336,7 @@ function createSDKBridge(opts) {
1292
1336
  }
1293
1337
 
1294
1338
  var codexConfig = getCodexConfig(sm);
1339
+ var kiroConfig = getKiroConfig(sm);
1295
1340
  var mergedMcpServers = mergeMcpServers(getMcpServers(), getRemoteMcpServers) || undefined;
1296
1341
 
1297
1342
  // Derive an explicit session title for fresh queries so the SDK records
@@ -1346,6 +1391,14 @@ function createSDKBridge(opts) {
1346
1391
  sandboxMode: codexConfig.sandbox,
1347
1392
  webSearchMode: codexConfig.webSearch,
1348
1393
  },
1394
+ KIRO: {
1395
+ engine: kiroConfig.engine,
1396
+ mode: kiroConfig.mode,
1397
+ // Kiro reads its native ~/.kiro/settings/mcp.json. Clay-managed MCP
1398
+ // servers are intentionally not forwarded until the ACP entry shape
1399
+ // and duplicate-registration behavior are verified against CLI 3.x.
1400
+ mcpServers: [],
1401
+ },
1349
1402
  },
1350
1403
  };
1351
1404
 
@@ -1493,18 +1546,37 @@ function createSDKBridge(opts) {
1493
1546
  }
1494
1547
 
1495
1548
  // Claude: check if binary is in PATH
1496
- if (tryLookup("claude")) result.push("claude");
1549
+ if (tryLookup(yoke.getVendorInfo("claude").binaryName)) result.push("claude");
1497
1550
 
1498
1551
  // Codex: check bundled binary or PATH
1499
1552
  var codexBin = null;
1500
1553
  try {
1501
1554
  codexBin = require("./yoke/codex-app-server").findCodexPath();
1502
1555
  } catch (e) {}
1503
- if ((codexBin && fs.existsSync(codexBin)) || tryLookup("codex")) result.push("codex");
1556
+ if ((codexBin && fs.existsSync(codexBin)) || tryLookup(yoke.getVendorInfo("codex").binaryName)) result.push("codex");
1557
+
1558
+ // Kiro has no per-user ACP spawn path yet. Do not advertise the daemon's
1559
+ // binary to an OS-isolated user, even if that user also has Kiro installed.
1560
+ var kiroInfo = yoke.getVendorInfo("kiro");
1561
+ if (!linuxUser || kiroInfo.osUserIsolation) {
1562
+ var kiroBin = null;
1563
+ try {
1564
+ kiroBin = require("./yoke/kiro-acp-server").findKiroPath();
1565
+ } catch (e) {}
1566
+ if ((kiroBin && fs.existsSync(kiroBin)) || tryLookup(kiroInfo.binaryName)) result.push("kiro");
1567
+ } else {
1568
+ console.log("[sdk-bridge] " + kiroInfo.displayName + " hidden for OS-isolated user " + linuxUser + ": per-user spawning is not implemented");
1569
+ }
1504
1570
 
1505
1571
  return result;
1506
1572
  }
1507
1573
 
1574
+ function getAvailableVendors(linuxUser) {
1575
+ return Object.keys(adapters).filter(function(vendor) {
1576
+ return !(linuxUser && !supportsOsUserIsolation(vendor));
1577
+ });
1578
+ }
1579
+
1508
1580
  // SDK warmup: initialize all available adapters and collect models.
1509
1581
  // The default adapter is initialized first for slash_commands and skills.
1510
1582
  // Passes linuxUser to adapter for worker-based warmup when OS isolation is needed.
@@ -1546,10 +1618,7 @@ function createSDKBridge(opts) {
1546
1618
  }
1547
1619
  sm.availableModels = result.models || [];
1548
1620
  // Store per-vendor models and capabilities
1549
- sm.modelsByVendor = sm.modelsByVendor || {};
1550
- sm.modelsByVendor[defaultVendor] = result.models || [];
1551
- sm.capabilitiesByVendor = sm.capabilitiesByVendor || {};
1552
- sm.capabilitiesByVendor[defaultVendor] = result.capabilities || {};
1621
+ rememberAdapterReady(defaultVendor, result);
1553
1622
  } catch (e) {
1554
1623
  if (e && e.name !== "AbortError" && !(e.message && e.message.indexOf("aborted") !== -1)) {
1555
1624
  send({ type: "error", text: "Failed to load " + defaultVendor + " SDK: " + (e.message || e) });
@@ -1568,7 +1637,7 @@ function createSDKBridge(opts) {
1568
1637
 
1569
1638
  // Detect installed vendors per-user (binary existence check)
1570
1639
  sm.installedVendors = detectInstalledVendors(linuxUser);
1571
- sm.availableVendors = Object.keys(adapters);
1640
+ sm.availableVendors = getAvailableVendors(linuxUser);
1572
1641
 
1573
1642
  // Send initial state to client
1574
1643
  send({
@@ -1,4 +1,5 @@
1
1
  var usersModule = require("./users");
2
+ var yoke = require("./yoke");
2
3
 
3
4
  function attachMessageProcessor(ctx) {
4
5
  var sm = ctx.sm;
@@ -50,10 +51,9 @@ function attachMessageProcessor(ctx) {
50
51
  var canAutoLogin = !usersModule.isMultiUser()
51
52
  || !!authLinuxUser
52
53
  || (authUser && authUser.role === "admin");
53
- var authTitle = session.vendor === "codex" ? "Codex is not logged in." : "Claude Code is not logged in.";
54
- var loginCommand = session.vendor === "codex"
55
- ? "codex login --device-auth"
56
- : "claude login";
54
+ var vendorInfo = yoke.getVendorInfo(session.vendor);
55
+ var authTitle = ((vendorInfo && vendorInfo.displayName) || "Claude Code") + " is not logged in.";
56
+ var loginCommand = (vendorInfo && vendorInfo.loginCommand) || "claude login";
57
57
  var _nmLogin = getNotificationsModule();
58
58
  sendAndRecord(session, {
59
59
  type: "auth_required",
package/lib/server.js CHANGED
@@ -148,6 +148,8 @@ function createServer(opts) {
148
148
  var onSetServerDefaultMode = opts.onSetServerDefaultMode || null;
149
149
  var onGetProjectDefaultMode = opts.onGetProjectDefaultMode || null;
150
150
  var onSetProjectDefaultMode = opts.onSetProjectDefaultMode || null;
151
+ var onGetProjectLastVendor = opts.onGetProjectLastVendor || null;
152
+ var onSetProjectLastVendor = opts.onSetProjectLastVendor || null;
151
153
  var onGetProjectMcpServers = opts.onGetProjectMcpServers || null;
152
154
  var onSetProjectMcpServers = opts.onSetProjectMcpServers || null;
153
155
  var onGetDaemonConfig = opts.onGetDaemonConfig || null;
@@ -1253,6 +1255,8 @@ function createServer(opts) {
1253
1255
  onSetServerDefaultMode: onSetServerDefaultMode,
1254
1256
  onGetProjectDefaultMode: onGetProjectDefaultMode,
1255
1257
  onSetProjectDefaultMode: onSetProjectDefaultMode,
1258
+ onGetProjectLastVendor: onGetProjectLastVendor,
1259
+ onSetProjectLastVendor: onSetProjectLastVendor,
1256
1260
  onGetProjectMcpServers: onGetProjectMcpServers,
1257
1261
  onSetProjectMcpServers: onSetProjectMcpServers,
1258
1262
  onGetDaemonConfig: onGetDaemonConfig,
@@ -274,7 +274,9 @@ function attachPreferences(deps) {
274
274
  }
275
275
 
276
276
  function setClaudeOpenMode(userId, mode) {
277
- var normalized = (mode === "gui") ? "gui" : "tui";
277
+ // GUI is the default in every other resolution path, so an unrecognized
278
+ // value must land on GUI too. Only an explicit "tui" opts into terminal.
279
+ var normalized = (mode === "tui") ? "tui" : "gui";
278
280
  var data = loadUsers();
279
281
  for (var i = 0; i < data.users.length; i++) {
280
282
  if (data.users[i].id === userId) {
@@ -1180,6 +1180,11 @@ function createClaudeAdapter(opts) {
1180
1180
  slashCommands: [],
1181
1181
  fastModeState: null,
1182
1182
  capabilities: {
1183
+ effort: true,
1184
+ fork: true,
1185
+ rollback: false,
1186
+ sessionListing: true,
1187
+ sessionRename: true,
1183
1188
  thinking: true,
1184
1189
  betas: true,
1185
1190
  rewind: true,
@@ -1556,6 +1561,11 @@ function createClaudeAdapter(opts) {
1556
1561
  slashCommands: r.slashCommands || [],
1557
1562
  fastModeState: r.fastModeState || null,
1558
1563
  capabilities: {
1564
+ effort: true,
1565
+ fork: true,
1566
+ rollback: false,
1567
+ sessionListing: true,
1568
+ sessionRename: true,
1559
1569
  thinking: true,
1560
1570
  betas: true,
1561
1571
  rewind: true,
@@ -1628,6 +1638,11 @@ function createClaudeAdapter(opts) {
1628
1638
  slashCommands: [],
1629
1639
  fastModeState: null,
1630
1640
  capabilities: {
1641
+ effort: true,
1642
+ fork: true,
1643
+ rollback: false,
1644
+ sessionListing: true,
1645
+ sessionRename: true,
1631
1646
  thinking: true,
1632
1647
  betas: true,
1633
1648
  rewind: true,
@@ -1708,6 +1723,11 @@ function createClaudeAdapter(opts) {
1708
1723
  slashCommands: r.slashCommands || [],
1709
1724
  fastModeState: r.fastModeState || null,
1710
1725
  capabilities: {
1726
+ effort: true,
1727
+ fork: true,
1728
+ rollback: false,
1729
+ sessionListing: true,
1730
+ sessionRename: true,
1711
1731
  thinking: true,
1712
1732
  betas: true,
1713
1733
  rewind: true,
@@ -1737,4 +1757,9 @@ function createClaudeAdapter(opts) {
1737
1757
  module.exports = {
1738
1758
  createClaudeAdapter: createClaudeAdapter,
1739
1759
  createMessageQueue: createMessageQueue,
1760
+ contractTestKit: {
1761
+ createMessageQueue: createMessageQueue,
1762
+ createQueryHandle: createQueryHandle,
1763
+ normalizeEvent: flattenEvent,
1764
+ },
1740
1765
  };