clay-server 2.47.0-beta.2 → 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.
@@ -1,12 +1,20 @@
1
1
  var CODEX_DEFAULTS = {
2
- approval: "on-failure",
2
+ approval: "on-request",
3
3
  sandbox: "danger-full-access",
4
4
  webSearch: "live",
5
5
  };
6
6
 
7
+ var CODEX_APPROVAL_POLICIES = ["untrusted", "on-request", "granular", "never"];
8
+
9
+ function normalizeCodexApproval(value) {
10
+ if (value === "on-failure") return CODEX_DEFAULTS.approval;
11
+ if (CODEX_APPROVAL_POLICIES.indexOf(value) === -1) return CODEX_DEFAULTS.approval;
12
+ return value;
13
+ }
14
+
7
15
  function getCodexConfig(sm) {
8
16
  return {
9
- approval: (sm && sm.codexApproval) || CODEX_DEFAULTS.approval,
17
+ approval: normalizeCodexApproval(sm && sm.codexApproval),
10
18
  sandbox: (sm && sm.codexSandbox) || CODEX_DEFAULTS.sandbox,
11
19
  webSearch: (sm && sm.codexWebSearch) || CODEX_DEFAULTS.webSearch,
12
20
  };
@@ -14,5 +22,7 @@ function getCodexConfig(sm) {
14
22
 
15
23
  module.exports = {
16
24
  CODEX_DEFAULTS: CODEX_DEFAULTS,
25
+ CODEX_APPROVAL_POLICIES: CODEX_APPROVAL_POLICIES,
26
+ normalizeCodexApproval: normalizeCodexApproval,
17
27
  getCodexConfig: getCodexConfig,
18
28
  };
@@ -4,6 +4,7 @@ var usersModule = require("./users");
4
4
  var userPresence = require("./user-presence");
5
5
  var emailAccounts = require("./email-accounts");
6
6
  var { getCodexConfig } = require("./codex-defaults");
7
+ var yoke = require("./yoke");
7
8
 
8
9
  /**
9
10
  * Attach connection/disconnection handlers to a project context.
@@ -132,7 +133,7 @@ function attachConnection(ctx) {
132
133
  var restoredActive = restoredState.active;
133
134
  var initialVendor = (restoredActive && restoredActive.vendor) || sm.defaultVendor || "claude";
134
135
  var initialModels = (sm.modelsByVendor && sm.modelsByVendor[initialVendor]) || sm.availableModels || [];
135
- sendTo(ws, { type: "info", cwd: cwd, slug: slug, project: title || project, version: currentVersion, debug: !!debug, dangerouslySkipPermissions: dangerouslySkipPermissions, osUsers: osUsers, lanHost: lanHost, projectCount: _filteredProjects.length, projects: _filteredProjects, projectOwnerId: projectOwnerId, ownerLocked: ownerLocked });
136
+ sendTo(ws, { type: "info", cwd: cwd, slug: slug, project: title || project, version: currentVersion, debug: !!debug, dangerouslySkipPermissions: dangerouslySkipPermissions, osUsers: osUsers, lanHost: lanHost, projectCount: _filteredProjects.length, projects: _filteredProjects, projectOwnerId: projectOwnerId, ownerLocked: ownerLocked, vendors: yoke.VENDOR_REGISTRY });
136
137
  // Update notifications are pushed on a scheduled interval (see
137
138
  // scheduleUpdateBroadcast). We no longer push on connect to avoid
138
139
  // re-triggering the banner on every page refresh.
@@ -5,6 +5,7 @@
5
5
  var fs = require("fs");
6
6
  var path = require("path");
7
7
  var config = require("./config");
8
+ var yoke = require("./yoke");
8
9
 
9
10
  var NOTIF_FILE = path.join(config.CONFIG_DIR, "notifications.json");
10
11
  var REMINDER_INTERVAL = 60 * 60 * 1000; // 1 hour
@@ -21,7 +22,8 @@ function generateId() {
21
22
  var formatters = {
22
23
  auth_required: function (data) {
23
24
  var vendor = data.vendor || "claude";
24
- var title = data.title || ((vendor === "codex" ? "Codex" : (vendor === "kiro" ? "Kiro CLI" : "Claude Code")) + " is not logged in");
25
+ var vendorInfo = yoke.getVendorInfo(vendor);
26
+ var title = data.title || (((vendorInfo && vendorInfo.displayName) || "Claude Code") + " is not logged in");
25
27
  return {
26
28
  type: "auth_required",
27
29
  title: title,
@@ -3,6 +3,12 @@ var path = require("path");
3
3
  var crypto = require("crypto");
4
4
  var { execFileSync } = require("child_process");
5
5
  var { CODEX_DEFAULTS, getCodexConfig } = require("./codex-defaults");
6
+ var yoke = require("./yoke");
7
+
8
+ function vendorSupportsTui(vendor) {
9
+ var info = yoke.getVendorInfo(vendor);
10
+ return !!(info && info.sessionModes.indexOf("tui") !== -1);
11
+ }
6
12
 
7
13
  // Format a user's answer to an ask_user_questions card as a plain user
8
14
  // message so the MCP path can feed it back to the agent on the next turn.
@@ -274,6 +280,8 @@ function attachSessions(ctx) {
274
280
  var sid = session.cliSessionId;
275
281
  var localId = session.localId;
276
282
  var resumeSkip = session.dangerouslySkipPermissions ? " --dangerously-skip-permissions" : "";
283
+ // Command construction is Claude-specific. Generalize this before any
284
+ // other vendor declares "tui" in the YOKE registry.
277
285
  var cmd = "claude --resume " + sid + resumeSkip + "; exit\n";
278
286
  var term = tm.create(80, 24, getOsUserInfoForWs(ws), ws, {
279
287
  initialInput: cmd,
@@ -346,7 +354,7 @@ function attachSessions(ctx) {
346
354
  // born-TUI session shows the same read-only + Resume view as a fresh click.
347
355
  function resolveSessionForView(session, ws) {
348
356
  if (!session) return;
349
- if (session.vendor && session.vendor !== "claude") { session.tuiSuspended = false; return; }
357
+ if (session.vendor && !vendorSupportsTui(session.vendor)) { session.tuiSuspended = false; return; }
350
358
  var pref = getClaudeOpenModeForWs(ws);
351
359
  // A LIVE runtime always wins over the viewer's claudeOpenMode pref:
352
360
  // another user (or this user in another tab) may be in the session right
@@ -424,13 +432,13 @@ function attachSessions(ctx) {
424
432
  if (ws._clayUser && usersModule.isMultiUser()) sessionOpts.ownerId = ws._clayUser.id;
425
433
  if (msg.sessionVisibility) sessionOpts.sessionVisibility = msg.sessionVisibility;
426
434
  if (msg.vendor) sessionOpts.vendor = msg.vendor;
427
- // Mode resolution: codex and kiro sessions are always GUI (no TUI adapter).
428
- // Claude sessions honor the explicit msg.mode if provided, otherwise
435
+ // Mode resolution: vendors without a TUI session mode are always GUI.
436
+ // TUI-capable sessions honor the explicit msg.mode if provided, otherwise
429
437
  // fall back to the user's claudeOpenMode preference. This is what
430
438
  // makes the sidebar's "Claude" icon button create the right kind of
431
439
  // session without the client needing to know the preference.
432
440
  var requestedMode;
433
- if (msg.vendor === "codex" || msg.vendor === "kiro") {
441
+ if (msg.vendor && !vendorSupportsTui(msg.vendor)) {
434
442
  requestedMode = "gui";
435
443
  } else if (msg.mode === "tui" || msg.mode === "gui") {
436
444
  requestedMode = msg.mode;
@@ -625,7 +633,7 @@ function attachSessions(ctx) {
625
633
  // for every viewer; only cold sessions follow the clicker's
626
634
  // claudeOpenMode pref. Nothing is spawned here.
627
635
  var xmTarget = sm.sessions.get(msg.id);
628
- if (xmTarget && (xmTarget.vendor === "claude" || !xmTarget.vendor)) {
636
+ if (xmTarget && (!xmTarget.vendor || vendorSupportsTui(xmTarget.vendor))) {
629
637
  // Single source of truth: live runtime wins (tui stays tui, gui stays
630
638
  // gui); only cold sessions follow the viewer's pref. No PTY is spawned
631
639
  // on switch - born-TUI resumes lazily via the Resume bar
@@ -678,7 +686,7 @@ function attachSessions(ctx) {
678
686
  if (msg.type === "resume_tui_session") {
679
687
  if (msg.id && sm.sessions.has(msg.id)) {
680
688
  var rtTarget = sm.sessions.get(msg.id);
681
- var rtOk = rtTarget && (rtTarget.vendor === "claude" || !rtTarget.vendor) &&
689
+ var rtOk = rtTarget && (!rtTarget.vendor || vendorSupportsTui(rtTarget.vendor)) &&
682
690
  rtTarget.cliSessionId && tm;
683
691
  if (rtOk) {
684
692
  if (usersModule.isMultiUser() && ws._clayUser &&
@@ -706,7 +714,7 @@ function attachSessions(ctx) {
706
714
  if (msg.type === "suspend_tui_session") {
707
715
  if (msg.id && sm.sessions.has(msg.id)) {
708
716
  var stTarget = sm.sessions.get(msg.id);
709
- var stOk = stTarget && (stTarget.vendor === "claude" || !stTarget.vendor);
717
+ var stOk = stTarget && (!stTarget.vendor || vendorSupportsTui(stTarget.vendor));
710
718
  if (stOk && (!usersModule.isMultiUser() || !ws._clayUser ||
711
719
  usersModule.canAccessSession(ws._clayUser.id, stTarget, { visibility: "public" }))) {
712
720
  if (tm) {
@@ -737,7 +745,7 @@ function attachSessions(ctx) {
737
745
  var tprId = msg.id;
738
746
  var tprSess = (tprId && sm.sessions.has(tprId)) ? sm.sessions.get(tprId) : null;
739
747
  if (!tprSess || !tprSess.cliSessionId || tprSess.mode !== "tui") return true;
740
- if (tprSess.vendor && tprSess.vendor !== "claude") return true;
748
+ if (tprSess.vendor && !vendorSupportsTui(tprSess.vendor)) return true;
741
749
  if (usersModule.isMultiUser() && ws._clayUser
742
750
  && !usersModule.canAccessSession(ws._clayUser.id, tprSess, { visibility: "public" })) {
743
751
  return true;
@@ -1084,7 +1092,7 @@ function attachSessions(ctx) {
1084
1092
 
1085
1093
  // Codex-specific settings (stored on sessionManager, passed to adapter via adapterOptions)
1086
1094
  if (msg.type === "set_codex_approval") {
1087
- sm.codexApproval = msg.approval || CODEX_DEFAULTS.approval;
1095
+ sm.codexApproval = getCodexConfig({ codexApproval: msg.approval }).approval;
1088
1096
  send(Object.assign({ type: "codex_config" }, getCodexConfig(sm)));
1089
1097
  return true;
1090
1098
  }
package/lib/project.js CHANGED
@@ -914,18 +914,25 @@ function createProjectContext(opts) {
914
914
  clayAuthToken: serverAuthToken,
915
915
  slug: slug,
916
916
  });
917
- } else if ((!sm.modelsByVendor || !sm.modelsByVendor[msg.vendor]) && typeof vendorAdapter.init === "function") {
917
+ }
918
+ var needsReadyMetadata = !sm.capabilitiesByVendor || !sm.capabilitiesByVendor[msg.vendor]
919
+ || !sm.modelsByVendor || !sm.modelsByVendor[msg.vendor];
920
+ if (vendorAdapter && needsReadyMetadata && typeof vendorAdapter.init === "function") {
918
921
  // Init warms the adapter, but a slow/failed init must not block
919
922
  // model listing (e.g. Codex models are a fixed list). Keep going
920
923
  // to supportedModels() even if init throws.
921
924
  try {
922
- await vendorAdapter.init({
925
+ var readyResult = await vendorAdapter.init({
923
926
  cwd: cwd,
924
927
  clayPort: serverPort,
925
928
  clayTls: serverTls,
926
929
  clayAuthToken: serverAuthToken,
927
930
  slug: slug,
928
931
  });
932
+ sm.capabilitiesByVendor = sm.capabilitiesByVendor || {};
933
+ sm.capabilitiesByVendor[msg.vendor] = readyResult.capabilities || {};
934
+ sm.modelsByVendor = sm.modelsByVendor || {};
935
+ if (Array.isArray(readyResult.models)) sm.modelsByVendor[msg.vendor] = readyResult.models;
929
936
  } catch (e) {
930
937
  console.error("[project] " + msg.vendor + " init failed (continuing to model list):", e.message || e);
931
938
  }
@@ -957,7 +964,8 @@ function createProjectContext(opts) {
957
964
  }
958
965
  }
959
966
  }
960
- sendTo(ws, { type: "model_info", model: modelToSend, models: vendorModels, vendor: msg.vendor, availableVendors: sm.availableVendors || [], installedVendors: sm.installedVendors || [] });
967
+ var vendorCapabilities = (sm.capabilitiesByVendor && sm.capabilitiesByVendor[msg.vendor]) || {};
968
+ sendTo(ws, { type: "model_info", model: modelToSend, models: vendorModels, vendor: msg.vendor, capabilities: vendorCapabilities, availableVendors: sm.availableVendors || [], installedVendors: sm.installedVendors || [] });
961
969
  })();
962
970
  return;
963
971
  }
package/lib/public/app.js CHANGED
@@ -312,6 +312,8 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
312
312
  currentModels: [],
313
313
  // Project's last-used vendor; seeds the sidebar's "New session" button.
314
314
  lastVendor: "",
315
+ // Static adapter metadata sent before any vendor is initialized.
316
+ vendorInfo: {},
315
317
  // How Claude sessions open: "gui" (default) or "tui". The server sends
316
318
  // claude_open_mode_changed on connect; this seeds it beforehand so the
317
319
  // new-session menu doesn't flash the TUI-only entry.
@@ -289,7 +289,7 @@ export function processMessage(msg) {
289
289
  // host lives on document.body (it's position: fixed), so it
290
290
  // survives project navigation unless we detach explicitly here.
291
291
  detachTuiView();
292
- store.set({ projectName: msg.project || msg.cwd });
292
+ store.set({ projectName: msg.project || msg.cwd, vendorInfo: msg.vendors || {} });
293
293
  if (msg.cwd) store.set({ cwd: msg.cwd });
294
294
  if (msg.slug) store.set({ currentSlug: msg.slug });
295
295
  try { var _is = store.snap(); localStorage.setItem("clay-project-name-" + (_is.currentSlug || "default"), _is.projectName); } catch (e) {}
@@ -464,6 +464,7 @@ export function processMessage(msg) {
464
464
  if (msg.vendor && !store.get('vendorSelectionLocked')) _miUpdate.currentVendor = msg.vendor;
465
465
  if (msg.availableVendors) _miUpdate.availableVendors = msg.availableVendors;
466
466
  if (msg.installedVendors) _miUpdate.installedVendors = msg.installedVendors;
467
+ if (msg.capabilities) _miUpdate.vendorCapabilities = msg.capabilities;
467
468
  store.set(_miUpdate);
468
469
  updateSettingsModels(_modelVal, msg.models || []);
469
470
  break;
@@ -1359,7 +1360,7 @@ export function processMessage(msg) {
1359
1360
  var _lm = store.get('pendingLoginModal');
1360
1361
  store.set({ pendingLoginModal: null });
1361
1362
  openTuiModal(msg.id, _lm.slug, {
1362
- sessionTitle: (_lm.vendor === "codex" ? "Codex" : "Claude") + " login",
1363
+ sessionTitle: (VENDOR_NAMES[_lm.vendor] || "Claude Code") + " login",
1363
1364
  projectName: _lm.slug,
1364
1365
  compact: true,
1365
1366
  });
@@ -19,6 +19,14 @@ var bannerContainer = null;
19
19
  var bellBtn = null;
20
20
  var badgeEl = null;
21
21
 
22
+ function getVendorLoginCommand(vendor) {
23
+ var vendors = store.get('vendorInfo') || {};
24
+ var info = vendors[vendor];
25
+ if (info && info.loginCommand) return info.loginCommand;
26
+ var fallbacks = { codex: "codex login --device-auth", claude: "claude login" };
27
+ return fallbacks[vendor] || fallbacks.claude;
28
+ }
29
+
22
30
  // --- Pending TUI attention tracking ---
23
31
  // Mirrors the icon-shake / favicon-blink behavior the SDK side already gets
24
32
  // from `pendingPermissions` on project status broadcasts. The notification
@@ -252,7 +260,7 @@ function showBanner(notif, autoDismissMs) {
252
260
  removeBanner(banner);
253
261
  dismissNotif(notif.id);
254
262
  var authMeta = notif.meta || {};
255
- startLoginInModal(authMeta.loginCommand || ((authMeta.vendor || "claude") === "codex" ? "codex login --device-auth" : "claude login"), authMeta.vendor || "claude");
263
+ startLoginInModal(authMeta.loginCommand || getVendorLoginCommand(authMeta.vendor || "claude"), authMeta.vendor || "claude");
256
264
  showLoginReminderBanner();
257
265
  });
258
266
  }
@@ -332,7 +340,7 @@ function startLoginInModal(loginCommand, vendor) {
332
340
  if (authReminderVisible) return;
333
341
  var ws = getWs();
334
342
  if (!ws || ws.readyState !== 1) return;
335
- var cmd = loginCommand || (vendor === "codex" ? "codex login --device-auth" : "claude login");
343
+ var cmd = loginCommand || getVendorLoginCommand(vendor);
336
344
  var slug = currentProjectSlug();
337
345
  if (!slug) { startLoginCommand(cmd); return; }
338
346
  store.set({ pendingLoginModal: { slug: slug, vendor: vendor || "claude" } });
@@ -366,7 +374,7 @@ export function autoStartLoginIfNeeded(msg) {
366
374
  if (authReminderVisible) return false;
367
375
  var vendor = msg.vendor || "claude";
368
376
  var cmd = msg.loginCommand
369
- || (vendor === "codex" ? "codex login --device-auth" : "claude login");
377
+ || getVendorLoginCommand(vendor);
370
378
  startLoginInModal(cmd, vendor);
371
379
  showLoginReminderBanner();
372
380
  return true;
@@ -426,7 +434,7 @@ function showLoginReminderBanner() {
426
434
  export function showAuthRequiredBanner(msg) {
427
435
  if (!bannerContainer) return;
428
436
  var vendor = (msg && (msg.vendor || (msg.meta && msg.meta.vendor))) || "claude";
429
- var loginCommand = (msg && (msg.loginCommand || (msg.meta && msg.meta.loginCommand))) || (vendor === "codex" ? "codex login --device-auth" : "claude login");
437
+ var loginCommand = (msg && (msg.loginCommand || (msg.meta && msg.meta.loginCommand))) || getVendorLoginCommand(vendor);
430
438
  activeAuthRequiredMsg = Object.assign({}, msg || {}, {
431
439
  id: (msg && msg.id) || ("_auth_" + Date.now()),
432
440
  type: "auth_required",
@@ -93,7 +93,7 @@ var EFFORT_LEVELS_BY_VENDOR = {
93
93
  var THINKING_OPTIONS = ["disabled", "adaptive", "budget"];
94
94
  var CODEX_APPROVAL_OPTIONS = [
95
95
  { value: "never", label: "Auto" },
96
- { value: "on-failure", label: "On Fail" },
96
+ { value: "untrusted", label: "Untrusted" },
97
97
  { value: "on-request", label: "Ask" },
98
98
  ];
99
99
  var CODEX_SANDBOX_OPTIONS = [
@@ -641,11 +641,14 @@ export function updateConfigChip() {
641
641
  rebuildModeList();
642
642
  rebuildEffortBar();
643
643
 
644
- // Vendor-specific sections
644
+ // MODE remains Claude-specific until adapter modes become capabilities.
645
645
  var isClaude = vendor === "claude";
646
- // MODE, THINKING, BETA are Claude-only
647
646
  if (configModeList && configModeList.parentElement) configModeList.parentElement.style.display = isClaude ? "" : "none";
648
647
  rebuildThinkingSection();
648
+ // capabilities.thinking means "emits a thinking stream" (true for every
649
+ // vendor), NOT "accepts thinking config". The adaptive/extended toggle and
650
+ // budget only feed Claude queries (sm.currentThinking), so the section
651
+ // stays Claude-only until a dedicated thinkingConfig capability exists.
649
652
  if (configThinkingSection) configThinkingSection.style.display = isClaude ? "" : "none";
650
653
  // BETA section deprecated (1M context is now standard)
651
654
  if (configBetaSection) configBetaSection.style.display = "none";
@@ -23,20 +23,37 @@ var fastModeIndicatorEl = null;
23
23
  // --- Internal helpers ---
24
24
 
25
25
  function getVendorUsageMeta(vendor) {
26
- if (vendor === "codex") {
27
- return {
26
+ var vendors = store.get('vendorInfo') || {};
27
+ var info = vendors[vendor];
28
+ if (info && info.usageDashboard) return info.usageDashboard;
29
+ var fallbacks = {
30
+ codex: {
28
31
  icon: "/codex-avatar.png",
29
32
  alt: "Codex",
30
33
  href: "https://chatgpt.com/admin/usage",
31
34
  title: "Check usage on ChatGPT",
32
- };
33
- }
34
- return {
35
- icon: "/claude-code-avatar.png",
36
- alt: "Claude Code",
37
- href: "https://claude.ai/settings/usage",
38
- title: "Check usage on claude.ai",
35
+ },
36
+ claude: {
37
+ icon: "/claude-code-avatar.png",
38
+ alt: "Claude Code",
39
+ href: "https://claude.ai/settings/usage",
40
+ title: "Check usage on claude.ai",
41
+ },
39
42
  };
43
+ return fallbacks[vendor] || fallbacks.claude;
44
+ }
45
+
46
+ function vendorTracksRateLimits(vendor) {
47
+ var vendors = store.get('vendorInfo') || {};
48
+ var info = vendors[vendor];
49
+ if (info) return info.rateLimitTracking !== false;
50
+ var legacyTrackedVendors = ["claude", "codex"];
51
+ return legacyTrackedVendors.indexOf(vendor) !== -1;
52
+ }
53
+
54
+ function vendorSupportsScheduledMessages(vendor) {
55
+ var scheduledMessageVendors = ["claude"];
56
+ return scheduledMessageVendors.indexOf(vendor) !== -1;
40
57
  }
41
58
 
42
59
  function rateLimitTypeLabel(type) {
@@ -178,8 +195,13 @@ function tickRateLimitUsage() {
178
195
 
179
196
  export function initRateLimit() {
180
197
  store.subscribe(function(state, prev) {
181
- if (state.currentVendor !== prev.currentVendor && state.currentVendor && state.currentVendor !== "claude") {
182
- clearScheduleDelay();
198
+ if (state.currentVendor !== prev.currentVendor && state.currentVendor) {
199
+ if (!vendorSupportsScheduledMessages(state.currentVendor)) clearScheduleDelay();
200
+ }
201
+ if (state.currentVendor !== prev.currentVendor || state.vendorInfo !== prev.vendorInfo) {
202
+ if (rateLimitUsageEl) {
203
+ rateLimitUsageEl.style.display = vendorTracksRateLimits(state.currentVendor || "claude") ? "" : "none";
204
+ }
183
205
  }
184
206
  });
185
207
  }
@@ -203,7 +225,7 @@ export function handleRateLimitEvent(msg) {
203
225
  if (rateLimitResetTimer) clearTimeout(rateLimitResetTimer);
204
226
  // Auto-switch input to schedule mode: any message typed will be queued for after reset
205
227
  var delayUntilReset = msg.resetsAt - Date.now();
206
- if (delayUntilReset > 0 && (store.get('currentVendor') || "claude") === "claude") {
228
+ if (delayUntilReset > 0 && vendorSupportsScheduledMessages(store.get('currentVendor') || "claude")) {
207
229
  setScheduleDelayMs(delayUntilReset + 60000); // +1min buffer after reset
208
230
  }
209
231
  rateLimitResetTimer = setTimeout(function () {
@@ -222,6 +244,11 @@ export function handleRateLimitEvent(msg) {
222
244
  }
223
245
 
224
246
  export function updateRateLimitUsage(msg) {
247
+ var activeVendor = store.get('currentVendor') || "claude";
248
+ if (!vendorTracksRateLimits(activeVendor)) {
249
+ if (rateLimitUsageEl) rateLimitUsageEl.style.display = "none";
250
+ return;
251
+ }
225
252
  if (msg.rateLimitType && msg.resetsAt) {
226
253
  rateLimitResetState[msg.rateLimitType] = { resetsAt: msg.resetsAt, status: msg.status };
227
254
  }
@@ -238,6 +265,7 @@ export function updateRateLimitUsage(msg) {
238
265
  var ref = document.getElementById("skip-perms-pill");
239
266
  topBarActions.insertBefore(rateLimitUsageEl, ref);
240
267
  }
268
+ rateLimitUsageEl.style.display = "";
241
269
 
242
270
  // Build label from available reset times
243
271
  var parts = [];
@@ -251,7 +279,7 @@ export function updateRateLimitUsage(msg) {
251
279
  }
252
280
 
253
281
  var label = parts.length > 0 ? parts.join(" · ") : "Check usage";
254
- var vendor = store.get('currentVendor') || "claude";
282
+ var vendor = activeVendor;
255
283
  var meta = getVendorUsageMeta(vendor);
256
284
  rateLimitUsageEl.href = meta.href;
257
285
  rateLimitUsageEl.title = meta.title;
@@ -14,6 +14,8 @@ import { sendMessage, hasSendableContent } from './input.js';
14
14
  import { getChatLayout } from './theme.js';
15
15
  import { getScheduledMsgEl } from './app-rate-limit.js';
16
16
 
17
+ // Keep these client constants aligned with lib/yoke/vendor-registry.js. The
18
+ // browser cannot import the server's CommonJS registry directly.
17
19
  export var VENDOR_AVATARS = {
18
20
  claude: "/claude-code-avatar.png",
19
21
  codex: "/codex-avatar.png",
@@ -5,6 +5,7 @@ import { checkForMention, showMentionMenu, hideMentionMenu, isMentionMenuVisible
5
5
  import { store } from './store.js';
6
6
  import { mateAvatarUrl } from './avatar.js';
7
7
  import { tuiIsActive, tuiSubmitText } from './session-tui-view.js';
8
+ import { VENDOR_AVATARS, VENDOR_NAMES } from './app-rendering.js';
8
9
 
9
10
  var ctx;
10
11
 
@@ -300,11 +301,9 @@ export function sendMessage() {
300
301
  _vtw2.classList.remove("locked");
301
302
  }
302
303
  if (_avi && _avIcon) {
303
- var _vendorAvatars = { claude: "/claude-code-avatar.png", codex: "/codex-avatar.png", kiro: "/kiro-avatar.svg" };
304
- var _vendorNames = { claude: "Claude", codex: "Codex", kiro: "Kiro CLI" };
305
- _avIcon.src = _vendorAvatars[_committedVendor] || _vendorAvatars.claude;
306
- _avIcon.alt = _vendorNames[_committedVendor] || _vendorNames.claude;
307
- _avi.title = (_vendorNames[_committedVendor] || _vendorNames.claude) + " session";
304
+ _avIcon.src = VENDOR_AVATARS[_committedVendor] || VENDOR_AVATARS.claude;
305
+ _avIcon.alt = VENDOR_NAMES[_committedVendor] || VENDOR_NAMES.claude;
306
+ _avi.title = (VENDOR_NAMES[_committedVendor] || VENDOR_NAMES.claude) + " session";
308
307
  _avi.classList.remove("hidden");
309
308
  }
310
309
  } else if (_vtw2) {
@@ -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
@@ -10,6 +10,7 @@ var { splitShellSegments, attachSkillDiscovery } = require("./sdk-skill-discover
10
10
  var { isSafeBashSegment } = require("./safe-bash-commands");
11
11
  var { createMessageQueue } = require("./sdk-message-queue");
12
12
  var { attachMessageProcessor } = require("./sdk-message-processor");
13
+ var yoke = require("./yoke");
13
14
 
14
15
  // Extract serializable tool descriptors from MCP server instances.
15
16
  // Used for IPC to worker processes (McpSdkServerConfigWithInstance is not serializable).
@@ -127,7 +128,6 @@ function createSDKBridge(opts) {
127
128
  var _cachedFreshAuthAt = 0;
128
129
 
129
130
  function getFreshAuthState(force) {
130
- var yoke = require("./yoke");
131
131
  var now = Date.now();
132
132
  if (!force && _cachedFreshAuthState && now - _cachedFreshAuthAt < 15000) {
133
133
  return _cachedFreshAuthState;
@@ -150,10 +150,13 @@ function createSDKBridge(opts) {
150
150
  }
151
151
 
152
152
  function getLoginCommand(vendor) {
153
- if (vendor === "codex") return "codex login --device-auth";
154
- if (vendor === "claude") return "claude login";
155
- if (vendor === "kiro") return "kiro-cli login";
156
- 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;
157
160
  }
158
161
 
159
162
  function notifyAuthRequired(session, title, body, authLinuxUser, canAutoLogin, loginCommand) {
@@ -220,15 +223,25 @@ function createSDKBridge(opts) {
220
223
  }
221
224
 
222
225
  function sendModelInfoForVendor(vendor, model) {
226
+ var resolvedVendor = vendor || (adapter && adapter.vendor) || "claude";
223
227
  send({
224
228
  type: "model_info",
225
229
  model: model || "",
226
- models: getModelsForVendor(vendor),
227
- vendor: vendor || (adapter && adapter.vendor) || "claude",
230
+ models: getModelsForVendor(resolvedVendor),
231
+ vendor: resolvedVendor,
232
+ capabilities: (sm.capabilitiesByVendor && sm.capabilitiesByVendor[resolvedVendor]) || {},
228
233
  availableVendors: sm.availableVendors || [],
229
234
  installedVendors: sm.installedVendors || [],
230
235
  });
231
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
+ }
232
245
  var onTurnDone = opts.onTurnDone || null;
233
246
 
234
247
  // --- Idle session reaper ---
@@ -869,7 +882,8 @@ function createSDKBridge(opts) {
869
882
  var canAutoLogin = !usersModule.isMultiUser()
870
883
  || !!authLinuxUser
871
884
  || (authUser && authUser.role === "admin");
872
- var authVendorLabel = session.vendor === "codex" ? "Codex" : (session.vendor === "kiro" ? "Kiro CLI" : "Claude Code");
885
+ var authInfo = yoke.getVendorInfo(session.vendor);
886
+ var authVendorLabel = (authInfo && authInfo.displayName) || "Claude Code";
873
887
  var authTitle = authVendorLabel + " is not logged in.";
874
888
  var authMsg = {
875
889
  type: "auth_required",
@@ -1070,10 +1084,9 @@ function createSDKBridge(opts) {
1070
1084
  async function startQuery(session, text, images, linuxUser) {
1071
1085
  async function ensureVendorReady(vendor) {
1072
1086
  if (!vendor) return null;
1073
- if (linuxUser && vendor === "kiro") return null;
1087
+ if (linuxUser && !supportsOsUserIsolation(vendor)) return null;
1074
1088
  var vendorAdapter = adapters[vendor] || null;
1075
1089
  if (!vendorAdapter) {
1076
- var yoke = require("./yoke");
1077
1090
  vendorAdapter = await yoke.lazyCreateAdapter(adapters, vendor, {
1078
1091
  cwd: cwd,
1079
1092
  dangerouslySkipPermissions: dangerouslySkipPermissions,
@@ -1083,8 +1096,11 @@ function createSDKBridge(opts) {
1083
1096
  clayAuthToken: clayAuthToken,
1084
1097
  slug: slug,
1085
1098
  });
1086
- } else if ((!sm.modelsByVendor || !sm.modelsByVendor[vendor]) && typeof vendorAdapter.init === "function") {
1087
- 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({
1088
1104
  cwd: cwd,
1089
1105
  dangerouslySkipPermissions: dangerouslySkipPermissions,
1090
1106
  linuxUser: linuxUser || undefined,
@@ -1093,6 +1109,7 @@ function createSDKBridge(opts) {
1093
1109
  clayAuthToken: clayAuthToken,
1094
1110
  slug: slug,
1095
1111
  });
1112
+ rememberAdapterReady(vendor, readyResult);
1096
1113
  }
1097
1114
  if (vendorAdapter) {
1098
1115
  sm.availableVendors = getAvailableVendors(linuxUser);
@@ -1100,6 +1117,11 @@ function createSDKBridge(opts) {
1100
1117
  if (!sm.modelsByVendor[vendor] && typeof vendorAdapter.supportedModels === "function") {
1101
1118
  sm.modelsByVendor[vendor] = await vendorAdapter.supportedModels();
1102
1119
  }
1120
+ var vendorModels = getModelsForVendor(vendor);
1121
+ var modelForVendor = resolveModelInList(vendorModels, sm.currentModel)
1122
+ || modelEntryValue(vendorModels[0])
1123
+ || "";
1124
+ sendModelInfoForVendor(vendor, modelForVendor);
1103
1125
  }
1104
1126
  return vendorAdapter;
1105
1127
  }
@@ -1108,11 +1130,13 @@ function createSDKBridge(opts) {
1108
1130
  // OS-isolated session would expose the daemon user's HOME and Kiro
1109
1131
  // credentials to another user, so refuse until the adapter has a
1110
1132
  // per-user worker path.
1111
- if (linuxUser && session.vendor === "kiro") {
1112
- console.error("[sdk-bridge] Refusing Kiro for OS-isolated user " + linuxUser + ": per-user ACP spawning is not implemented");
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");
1113
1137
  sendAndRecord(session, {
1114
1138
  type: "error",
1115
- text: "Kiro is not available for OS-isolated users yet.",
1139
+ text: isolatedVendorName + " is not available for OS-isolated users yet.",
1116
1140
  });
1117
1141
  sendAndRecord(session, { type: "done", code: 1 });
1118
1142
  return;
@@ -1139,7 +1163,9 @@ function createSDKBridge(opts) {
1139
1163
  }
1140
1164
  // If still not available after lazy check, send auth_required
1141
1165
  if (session.vendor && !adapters[session.vendor]) {
1142
- 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);
1143
1169
  var authUser = session.ownerId ? usersModule.findUserById(session.ownerId) : null;
1144
1170
  var authLinuxUser = authUser && authUser.linuxUser ? authUser.linuxUser : null;
1145
1171
  var canAutoLogin = !usersModule.isMultiUser()
@@ -1520,25 +1546,26 @@ function createSDKBridge(opts) {
1520
1546
  }
1521
1547
 
1522
1548
  // Claude: check if binary is in PATH
1523
- if (tryLookup("claude")) result.push("claude");
1549
+ if (tryLookup(yoke.getVendorInfo("claude").binaryName)) result.push("claude");
1524
1550
 
1525
1551
  // Codex: check bundled binary or PATH
1526
1552
  var codexBin = null;
1527
1553
  try {
1528
1554
  codexBin = require("./yoke/codex-app-server").findCodexPath();
1529
1555
  } catch (e) {}
1530
- if ((codexBin && fs.existsSync(codexBin)) || tryLookup("codex")) result.push("codex");
1556
+ if ((codexBin && fs.existsSync(codexBin)) || tryLookup(yoke.getVendorInfo("codex").binaryName)) result.push("codex");
1531
1557
 
1532
1558
  // Kiro has no per-user ACP spawn path yet. Do not advertise the daemon's
1533
1559
  // binary to an OS-isolated user, even if that user also has Kiro installed.
1534
- if (!linuxUser) {
1560
+ var kiroInfo = yoke.getVendorInfo("kiro");
1561
+ if (!linuxUser || kiroInfo.osUserIsolation) {
1535
1562
  var kiroBin = null;
1536
1563
  try {
1537
1564
  kiroBin = require("./yoke/kiro-acp-server").findKiroPath();
1538
1565
  } catch (e) {}
1539
- if ((kiroBin && fs.existsSync(kiroBin)) || tryLookup("kiro-cli")) result.push("kiro");
1566
+ if ((kiroBin && fs.existsSync(kiroBin)) || tryLookup(kiroInfo.binaryName)) result.push("kiro");
1540
1567
  } else {
1541
- console.log("[sdk-bridge] Kiro hidden for OS-isolated user " + linuxUser + ": per-user ACP spawning is not implemented");
1568
+ console.log("[sdk-bridge] " + kiroInfo.displayName + " hidden for OS-isolated user " + linuxUser + ": per-user spawning is not implemented");
1542
1569
  }
1543
1570
 
1544
1571
  return result;
@@ -1546,7 +1573,7 @@ function createSDKBridge(opts) {
1546
1573
 
1547
1574
  function getAvailableVendors(linuxUser) {
1548
1575
  return Object.keys(adapters).filter(function(vendor) {
1549
- return !(linuxUser && vendor === "kiro");
1576
+ return !(linuxUser && !supportsOsUserIsolation(vendor));
1550
1577
  });
1551
1578
  }
1552
1579
 
@@ -1591,10 +1618,7 @@ function createSDKBridge(opts) {
1591
1618
  }
1592
1619
  sm.availableModels = result.models || [];
1593
1620
  // Store per-vendor models and capabilities
1594
- sm.modelsByVendor = sm.modelsByVendor || {};
1595
- sm.modelsByVendor[defaultVendor] = result.models || [];
1596
- sm.capabilitiesByVendor = sm.capabilitiesByVendor || {};
1597
- sm.capabilitiesByVendor[defaultVendor] = result.capabilities || {};
1621
+ rememberAdapterReady(defaultVendor, result);
1598
1622
  } catch (e) {
1599
1623
  if (e && e.name !== "AbortError" && !(e.message && e.message.indexOf("aborted") !== -1)) {
1600
1624
  send({ type: "error", text: "Failed to load " + defaultVendor + " SDK: " + (e.message || e) });
@@ -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,12 +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"
54
- ? "Codex is not logged in."
55
- : (session.vendor === "kiro" ? "Kiro CLI is not logged in." : "Claude Code is not logged in.");
56
- var loginCommand = session.vendor === "codex"
57
- ? "codex login --device-auth"
58
- : (session.vendor === "kiro" ? "kiro-cli login" : "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";
59
57
  var _nmLogin = getNotificationsModule();
60
58
  sendAndRecord(session, {
61
59
  type: "auth_required",
@@ -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
  };
@@ -597,6 +597,27 @@ function flattenEvent(notification, state) {
597
597
  return events;
598
598
  }
599
599
 
600
+ function createEventState(model) {
601
+ return {
602
+ blockCounter: 0,
603
+ threadId: null,
604
+ turnStarted: false,
605
+ lastUsage: null,
606
+ lastInputTokens: null,
607
+ done: false,
608
+ aborted: false,
609
+ loopStarted: false,
610
+ model: model || "gpt-5.6-terra",
611
+ textBlocks: {},
612
+ textLengths: {},
613
+ thinkingBlocks: {},
614
+ thinkingLengths: {},
615
+ toolBlocks: {},
616
+ commandInputs: {},
617
+ planTexts: {},
618
+ };
619
+ }
620
+
600
621
  // --- QueryHandle ---
601
622
 
602
623
  function createCodexQueryHandle(appServer, queryOpts) {
@@ -611,25 +632,7 @@ function createCodexQueryHandle(appServer, queryOpts) {
611
632
  return state.aborted || (abortController && abortController.signal && abortController.signal.aborted);
612
633
  }
613
634
 
614
- var state = {
615
- blockCounter: 0,
616
- threadId: null,
617
- turnStarted: false,
618
- lastUsage: null,
619
- lastInputTokens: null, // from thread/tokenUsage/updated
620
- done: false,
621
- aborted: false,
622
- loopStarted: false,
623
- model: queryOpts.model || "gpt-5.6-terra",
624
- // Track incremental text deltas
625
- textBlocks: {}, // itemId -> true (text_start sent)
626
- textLengths: {}, // itemId -> last sent length
627
- thinkingBlocks: {}, // itemId -> blockId
628
- thinkingLengths: {}, // itemId -> last sent length
629
- toolBlocks: {}, // itemId -> blockId (for tool_start dedup)
630
- commandInputs: {}, // itemId -> command captured from approval/start events
631
- planTexts: {}, // itemId -> streamed plan text
632
- };
635
+ var state = createEventState(queryOpts.model);
633
636
 
634
637
  // Internal event buffer for async iterator
635
638
  var eventBuffer = [];
@@ -867,7 +870,7 @@ function createCodexQueryHandle(appServer, queryOpts) {
867
870
  var threadParams = {
868
871
  model: queryOpts.model || "gpt-5.6-terra",
869
872
  sandbox: queryOpts.sandboxMode || "workspace-write",
870
- approvalPolicy: queryOpts.approvalPolicy || "on-failure",
873
+ approvalPolicy: queryOpts.approvalPolicy || "on-request",
871
874
  cwd: queryOpts.cwd,
872
875
  skipGitRepoCheck: true,
873
876
  };
@@ -1139,6 +1142,11 @@ function createCodexAdapter(opts) {
1139
1142
  slashCommands: skillNames || [],
1140
1143
  fastModeState: null,
1141
1144
  capabilities: {
1145
+ effort: true,
1146
+ fork: true,
1147
+ rollback: true,
1148
+ sessionListing: false,
1149
+ sessionRename: false,
1142
1150
  thinking: true,
1143
1151
  betas: false,
1144
1152
  rewind: false,
@@ -1463,7 +1471,7 @@ function createCodexAdapter(opts) {
1463
1471
  if (queryOpts.toolPolicy === "allow-all") {
1464
1472
  handleOpts.approvalPolicy = "never";
1465
1473
  } else {
1466
- handleOpts.approvalPolicy = codexOpts.approvalPolicy || "on-failure";
1474
+ handleOpts.approvalPolicy = codexOpts.approvalPolicy || "on-request";
1467
1475
  }
1468
1476
 
1469
1477
  // Sandbox mode
@@ -1590,4 +1598,9 @@ function createCodexAdapter(opts) {
1590
1598
 
1591
1599
  module.exports = {
1592
1600
  createCodexAdapter: createCodexAdapter,
1601
+ contractTestKit: {
1602
+ createEventState: createEventState,
1603
+ createQueryHandle: createCodexQueryHandle,
1604
+ normalizeEvent: flattenEvent,
1605
+ },
1593
1606
  };
@@ -377,30 +377,19 @@ function finalToolContent(state, callId, update) {
377
377
  return extractRawOutput(update.rawOutput);
378
378
  }
379
379
 
380
- // --- QueryHandle ---
381
-
382
- function createKiroQueryHandle(acp, queryOpts) {
383
- var abortController = queryOpts.abortController;
384
- var systemPrompt = queryOpts.systemPrompt || "";
385
- var canUseTool = queryOpts.canUseTool || null;
386
- var onFinished = queryOpts.onFinished || null;
387
-
388
- function isCancelled() {
389
- return state.aborted || (abortController && abortController.signal && abortController.signal.aborted);
390
- }
391
-
392
- var state = {
380
+ function createEventState(opts) {
381
+ opts = opts || {};
382
+ return {
393
383
  blockCounter: 0,
394
- sessionId: queryOpts.resumeSessionId || null,
395
- model: queryOpts.model || "auto",
396
- engine: queryOpts.engine || KIRO_DEFAULTS.engine,
384
+ sessionId: opts.resumeSessionId || null,
385
+ model: opts.model || "auto",
386
+ engine: opts.engine || KIRO_DEFAULTS.engine,
397
387
  lastInputTokens: null,
398
- contextWindow: queryOpts.contextWindow || null,
388
+ contextWindow: opts.contextWindow || null,
399
389
  done: false,
400
390
  aborted: false,
401
391
  loopStarted: false,
402
392
  loadingSession: false,
403
- // per-turn block tracking (reset each turn)
404
393
  textBlockOpen: false,
405
394
  textBlockId: null,
406
395
  thinkBlockOpen: false,
@@ -409,6 +398,21 @@ function createKiroQueryHandle(acp, queryOpts) {
409
398
  toolMeta: {},
410
399
  toolContent: {},
411
400
  };
401
+ }
402
+
403
+ // --- QueryHandle ---
404
+
405
+ function createKiroQueryHandle(acp, queryOpts) {
406
+ var abortController = queryOpts.abortController;
407
+ var systemPrompt = queryOpts.systemPrompt || "";
408
+ var canUseTool = queryOpts.canUseTool || null;
409
+ var onFinished = queryOpts.onFinished || null;
410
+
411
+ function isCancelled() {
412
+ return state.aborted || (abortController && abortController.signal && abortController.signal.aborted);
413
+ }
414
+
415
+ var state = createEventState(queryOpts);
412
416
 
413
417
  // Async iterator plumbing
414
418
  var eventBuffer = [];
@@ -864,6 +868,11 @@ function createKiroAdapter(opts) {
864
868
  slashCommands: skillNames || [],
865
869
  fastModeState: null,
866
870
  capabilities: {
871
+ effort: false,
872
+ fork: false,
873
+ rollback: false,
874
+ sessionListing: false,
875
+ sessionRename: false,
867
876
  thinking: true,
868
877
  betas: false,
869
878
  rewind: false,
@@ -872,7 +881,7 @@ function createKiroAdapter(opts) {
872
881
  elicitation: false,
873
882
  fileCheckpointing: false,
874
883
  contextCompacting: true,
875
- toolPolicy: ["ask", "allow-all"],
884
+ toolPolicy: ["ask"],
876
885
  },
877
886
  };
878
887
  }
@@ -1153,4 +1162,9 @@ function createKiroAdapter(opts) {
1153
1162
 
1154
1163
  module.exports = {
1155
1164
  createKiroAdapter: createKiroAdapter,
1165
+ contractTestKit: {
1166
+ createEventState: createEventState,
1167
+ createQueryHandle: createKiroQueryHandle,
1168
+ normalizeEvent: flattenUpdate,
1169
+ },
1156
1170
  };
package/lib/yoke/index.js CHANGED
@@ -3,6 +3,7 @@
3
3
 
4
4
  var iface = require("./interface");
5
5
  var instructions = require("./instructions");
6
+ var vendorRegistry = require("./vendor-registry");
6
7
  var createClaudeAdapter = require("./adapters/claude").createClaudeAdapter;
7
8
  var createCodexAdapter = require("./adapters/codex").createCodexAdapter;
8
9
  var createKiroAdapter = require("./adapters/kiro").createKiroAdapter;
@@ -45,6 +46,8 @@ function wrapCreateQuery(adapter, defaultCwd) {
45
46
  * @returns {Adapter}
46
47
  */
47
48
  function createAdapter(opts) {
49
+ // Adding a vendor here also requires a registry entry and an update to the
50
+ // completeness list in test/yoke-vendor-registry.test.js.
48
51
  var vendor = (opts && opts.vendor) || "claude";
49
52
  var adapter;
50
53
  if (vendor === "claude") {
@@ -284,7 +287,12 @@ function createAdapters(opts) {
284
287
  var auth = { claude: false, codex: false, kiro: false };
285
288
  var adapters = {};
286
289
 
287
- if (installed.claude) {
290
+ function supportsConfiguredIsolation(vendor) {
291
+ var info = vendorRegistry.getVendorInfo(vendor);
292
+ return !opts.osUsers || !info || info.osUserIsolation;
293
+ }
294
+
295
+ if (installed.claude && supportsConfiguredIsolation("claude")) {
288
296
  try {
289
297
  if (!_sharedClaudeAdapter) {
290
298
  _sharedClaudeAdapter = createAdapter({ vendor: "claude", cwd: opts.cwd });
@@ -301,7 +309,7 @@ function createAdapters(opts) {
301
309
  }
302
310
  }
303
311
 
304
- if (installed.codex) {
312
+ if (installed.codex && supportsConfiguredIsolation("codex")) {
305
313
  try {
306
314
  adapters.codex = createAdapter({ vendor: "codex", cwd: opts.cwd, slug: opts.slug });
307
315
  auth.codex = true;
@@ -311,7 +319,8 @@ function createAdapters(opts) {
311
319
  }
312
320
  }
313
321
 
314
- if (installed.kiro && !opts.osUsers) {
322
+ var kiroInfo = vendorRegistry.getVendorInfo("kiro");
323
+ if (installed.kiro && supportsConfiguredIsolation("kiro")) {
315
324
  try {
316
325
  adapters.kiro = createAdapter({ vendor: "kiro", cwd: opts.cwd, slug: opts.slug });
317
326
  auth.kiro = true;
@@ -320,7 +329,7 @@ function createAdapters(opts) {
320
329
  console.error("[yoke] Failed to create adapter for kiro:", e.message);
321
330
  }
322
331
  } else if (installed.kiro && opts.osUsers) {
323
- console.log("[yoke] Kiro adapter disabled: OS-user isolation requires per-user ACP spawning");
332
+ console.log("[yoke] " + kiroInfo.displayName + " adapter disabled: OS-user isolation requires per-user spawning");
324
333
  }
325
334
 
326
335
  return { adapters: adapters, auth: auth };
@@ -334,8 +343,9 @@ function createAdapters(opts) {
334
343
  async function lazyCreateAdapter(adapters, vendor, opts) {
335
344
  opts = opts || {};
336
345
 
337
- if (vendor === "kiro" && (opts.osUsers || opts.linuxUser)) {
338
- console.log("[yoke] Refusing lazy Kiro adapter creation for OS-isolated user " + (opts.linuxUser || "unknown"));
346
+ var vendorInfo = vendorRegistry.getVendorInfo(vendor);
347
+ if (vendorInfo && !vendorInfo.osUserIsolation && (opts.osUsers || opts.linuxUser)) {
348
+ console.log("[yoke] Refusing lazy " + vendorInfo.displayName + " adapter creation for OS-isolated user " + (opts.linuxUser || "unknown"));
339
349
  return null;
340
350
  }
341
351
 
@@ -365,6 +375,8 @@ module.exports = {
365
375
  checkAuth: checkAuth,
366
376
  checkInstalled: checkInstalled,
367
377
  invalidateAuthCache: invalidateAuthCache,
378
+ VENDOR_REGISTRY: vendorRegistry.VENDOR_REGISTRY,
379
+ getVendorInfo: vendorRegistry.getVendorInfo,
368
380
  TOOL_POLICIES: iface.TOOL_POLICIES,
369
381
  validateAdapter: iface.validateAdapter,
370
382
  validateQueryHandle: iface.validateQueryHandle,
@@ -0,0 +1,55 @@
1
+ // Static, init-free facts about each vendor YOKE supports. Host code that
2
+ // needs metadata before adapter initialization should read it from here.
3
+ // Do not import adapters into this module.
4
+
5
+ var VENDOR_REGISTRY = {
6
+ claude: {
7
+ displayName: "Claude Code",
8
+ loginCommand: "claude login",
9
+ binaryName: "claude",
10
+ avatar: "/claude-code-avatar.png",
11
+ sessionModes: ["gui", "tui"],
12
+ osUserIsolation: true,
13
+ usageDashboard: {
14
+ icon: "/claude-code-avatar.png",
15
+ alt: "Claude Code",
16
+ href: "https://claude.ai/settings/usage",
17
+ title: "Check usage on claude.ai",
18
+ },
19
+ rateLimitTracking: true,
20
+ },
21
+ codex: {
22
+ displayName: "Codex",
23
+ loginCommand: "codex login --device-auth",
24
+ binaryName: "codex",
25
+ avatar: "/codex-avatar.png",
26
+ sessionModes: ["gui"],
27
+ osUserIsolation: true,
28
+ usageDashboard: {
29
+ icon: "/codex-avatar.png",
30
+ alt: "Codex",
31
+ href: "https://chatgpt.com/admin/usage",
32
+ title: "Check usage on ChatGPT",
33
+ },
34
+ rateLimitTracking: true,
35
+ },
36
+ kiro: {
37
+ displayName: "Kiro CLI",
38
+ loginCommand: "kiro-cli login",
39
+ binaryName: "kiro-cli",
40
+ avatar: "/kiro-avatar.svg",
41
+ sessionModes: ["gui"],
42
+ osUserIsolation: false,
43
+ usageDashboard: null,
44
+ rateLimitTracking: false,
45
+ },
46
+ };
47
+
48
+ function getVendorInfo(vendor) {
49
+ return VENDOR_REGISTRY[vendor] || null;
50
+ }
51
+
52
+ module.exports = {
53
+ VENDOR_REGISTRY: VENDOR_REGISTRY,
54
+ getVendorInfo: getVendorInfo,
55
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "2.47.0-beta.2",
3
+ "version": "2.47.0-beta.3",
4
4
  "description": "Self-hosted team workspace for Claude Code and Codex. Multi-user, browser-based, with persistent AI mates.",
5
5
  "bin": {
6
6
  "clay-server": "./bin/cli.js",
@@ -49,9 +49,9 @@
49
49
  "homepage": "https://github.com/chadbyte/clay#readme",
50
50
  "author": "Chad",
51
51
  "dependencies": {
52
- "@anthropic-ai/claude-agent-sdk": "^0.3.198",
52
+ "@anthropic-ai/claude-agent-sdk": "^0.3.233",
53
53
  "@lydell/node-pty": "^1.2.0-beta.3",
54
- "@openai/codex": "^0.142.4",
54
+ "@openai/codex": "^0.147.0",
55
55
  "imapflow": "^1.3.1",
56
56
  "nodemailer": "^6.10.1",
57
57
  "qrcode-terminal": "^0.12.0",