clay-server 2.46.0 → 2.47.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,
package/lib/sdk-bridge.js CHANGED
@@ -5,6 +5,7 @@ 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");
@@ -151,6 +152,7 @@ function createSDKBridge(opts) {
151
152
  function getLoginCommand(vendor) {
152
153
  if (vendor === "codex") return "codex login --device-auth";
153
154
  if (vendor === "claude") return "claude login";
155
+ if (vendor === "kiro") return "kiro-cli login";
154
156
  return (vendor || "claude") + " login";
155
157
  }
156
158
 
@@ -799,7 +801,7 @@ function createSDKBridge(opts) {
799
801
  onProcessingChanged();
800
802
  send({ type: "status", processing: false });
801
803
  sendAndRecord(session, { type: "thinking_stop" });
802
- var interruptMsg = (session.vendor === "codex")
804
+ var interruptMsg = (session.vendor && session.vendor !== "claude")
803
805
  ? "\u25a0 Conversation interrupted - tell the model what to do differently."
804
806
  : "Interrupted \u00b7 What should Claude do instead?";
805
807
  sendAndRecord(session, { type: "info", text: interruptMsg });
@@ -813,7 +815,7 @@ function createSDKBridge(opts) {
813
815
  if (err.name === "AbortError" || (myAbortController && myAbortController.signal.aborted) || session.taskStopRequested) {
814
816
  if (!session.destroying) {
815
817
  sendAndRecord(session, { type: "thinking_stop" });
816
- var interruptMsg2 = (session.vendor === "codex")
818
+ var interruptMsg2 = (session.vendor && session.vendor !== "claude")
817
819
  ? "\u25a0 Conversation interrupted - tell the model what to do differently."
818
820
  : "Interrupted \u00b7 What should Claude do instead?";
819
821
  sendAndRecord(session, { type: "info", text: interruptMsg2 });
@@ -867,7 +869,8 @@ function createSDKBridge(opts) {
867
869
  var canAutoLogin = !usersModule.isMultiUser()
868
870
  || !!authLinuxUser
869
871
  || (authUser && authUser.role === "admin");
870
- var authTitle = (session.vendor === "codex" ? "Codex" : "Claude Code") + " is not logged in.";
872
+ var authVendorLabel = session.vendor === "codex" ? "Codex" : (session.vendor === "kiro" ? "Kiro CLI" : "Claude Code");
873
+ var authTitle = authVendorLabel + " is not logged in.";
871
874
  var authMsg = {
872
875
  type: "auth_required",
873
876
  text: authTitle,
@@ -1067,6 +1070,7 @@ function createSDKBridge(opts) {
1067
1070
  async function startQuery(session, text, images, linuxUser) {
1068
1071
  async function ensureVendorReady(vendor) {
1069
1072
  if (!vendor) return null;
1073
+ if (linuxUser && vendor === "kiro") return null;
1070
1074
  var vendorAdapter = adapters[vendor] || null;
1071
1075
  if (!vendorAdapter) {
1072
1076
  var yoke = require("./yoke");
@@ -1091,7 +1095,7 @@ function createSDKBridge(opts) {
1091
1095
  });
1092
1096
  }
1093
1097
  if (vendorAdapter) {
1094
- sm.availableVendors = Object.keys(adapters);
1098
+ sm.availableVendors = getAvailableVendors(linuxUser);
1095
1099
  sm.modelsByVendor = sm.modelsByVendor || {};
1096
1100
  if (!sm.modelsByVendor[vendor] && typeof vendorAdapter.supportedModels === "function") {
1097
1101
  sm.modelsByVendor[vendor] = await vendorAdapter.supportedModels();
@@ -1100,6 +1104,20 @@ function createSDKBridge(opts) {
1100
1104
  return vendorAdapter;
1101
1105
  }
1102
1106
 
1107
+ // Kiro ACP currently spawns as the daemon user. Running it for an
1108
+ // OS-isolated session would expose the daemon user's HOME and Kiro
1109
+ // credentials to another user, so refuse until the adapter has a
1110
+ // 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");
1113
+ sendAndRecord(session, {
1114
+ type: "error",
1115
+ text: "Kiro is not available for OS-isolated users yet.",
1116
+ });
1117
+ sendAndRecord(session, { type: "done", code: 1 });
1118
+ return;
1119
+ }
1120
+
1103
1121
  // If vendor is set but adapter not ready, try lazy creation (user may have logged in)
1104
1122
  if (session.vendor && !adapters[session.vendor]) {
1105
1123
  var lazyAdapter = await ensureVendorReady(session.vendor);
@@ -1292,6 +1310,7 @@ function createSDKBridge(opts) {
1292
1310
  }
1293
1311
 
1294
1312
  var codexConfig = getCodexConfig(sm);
1313
+ var kiroConfig = getKiroConfig(sm);
1295
1314
  var mergedMcpServers = mergeMcpServers(getMcpServers(), getRemoteMcpServers) || undefined;
1296
1315
 
1297
1316
  // Derive an explicit session title for fresh queries so the SDK records
@@ -1346,6 +1365,14 @@ function createSDKBridge(opts) {
1346
1365
  sandboxMode: codexConfig.sandbox,
1347
1366
  webSearchMode: codexConfig.webSearch,
1348
1367
  },
1368
+ KIRO: {
1369
+ engine: kiroConfig.engine,
1370
+ mode: kiroConfig.mode,
1371
+ // Kiro reads its native ~/.kiro/settings/mcp.json. Clay-managed MCP
1372
+ // servers are intentionally not forwarded until the ACP entry shape
1373
+ // and duplicate-registration behavior are verified against CLI 3.x.
1374
+ mcpServers: [],
1375
+ },
1349
1376
  },
1350
1377
  };
1351
1378
 
@@ -1502,9 +1529,27 @@ function createSDKBridge(opts) {
1502
1529
  } catch (e) {}
1503
1530
  if ((codexBin && fs.existsSync(codexBin)) || tryLookup("codex")) result.push("codex");
1504
1531
 
1532
+ // Kiro has no per-user ACP spawn path yet. Do not advertise the daemon's
1533
+ // binary to an OS-isolated user, even if that user also has Kiro installed.
1534
+ if (!linuxUser) {
1535
+ var kiroBin = null;
1536
+ try {
1537
+ kiroBin = require("./yoke/kiro-acp-server").findKiroPath();
1538
+ } catch (e) {}
1539
+ if ((kiroBin && fs.existsSync(kiroBin)) || tryLookup("kiro-cli")) result.push("kiro");
1540
+ } else {
1541
+ console.log("[sdk-bridge] Kiro hidden for OS-isolated user " + linuxUser + ": per-user ACP spawning is not implemented");
1542
+ }
1543
+
1505
1544
  return result;
1506
1545
  }
1507
1546
 
1547
+ function getAvailableVendors(linuxUser) {
1548
+ return Object.keys(adapters).filter(function(vendor) {
1549
+ return !(linuxUser && vendor === "kiro");
1550
+ });
1551
+ }
1552
+
1508
1553
  // SDK warmup: initialize all available adapters and collect models.
1509
1554
  // The default adapter is initialized first for slash_commands and skills.
1510
1555
  // Passes linuxUser to adapter for worker-based warmup when OS isolation is needed.
@@ -1568,7 +1613,7 @@ function createSDKBridge(opts) {
1568
1613
 
1569
1614
  // Detect installed vendors per-user (binary existence check)
1570
1615
  sm.installedVendors = detectInstalledVendors(linuxUser);
1571
- sm.availableVendors = Object.keys(adapters);
1616
+ sm.availableVendors = getAvailableVendors(linuxUser);
1572
1617
 
1573
1618
  // Send initial state to client
1574
1619
  send({
@@ -50,10 +50,12 @@ function attachMessageProcessor(ctx) {
50
50
  var canAutoLogin = !usersModule.isMultiUser()
51
51
  || !!authLinuxUser
52
52
  || (authUser && authUser.role === "admin");
53
- var authTitle = session.vendor === "codex" ? "Codex is not logged in." : "Claude Code is not logged in.";
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.");
54
56
  var loginCommand = session.vendor === "codex"
55
57
  ? "codex login --device-auth"
56
- : "claude login";
58
+ : (session.vendor === "kiro" ? "kiro-cli login" : "claude login");
57
59
  var _nmLogin = getNotificationsModule();
58
60
  sendAndRecord(session, {
59
61
  type: "auth_required",
@@ -102,11 +102,20 @@ function attachAuth(ctx) {
102
102
  function getMultiUserFromReq(req) {
103
103
  var cookies = parseCookies(req);
104
104
  var token = cookies[MULTI_USER_COOKIE];
105
- if (!token) return null;
106
- var userId = multiUserTokens[token];
107
- if (!userId) return null;
108
- var user = users.findUserById(userId);
109
- return user || null;
105
+ if (token) {
106
+ var userId = multiUserTokens[token];
107
+ if (userId) {
108
+ var user = users.findUserById(userId);
109
+ if (user) return user;
110
+ }
111
+ }
112
+ // Solo auto-login: exactly one user with no PIN => frictionless access, no
113
+ // login wall. Mirrors the old single-user onramp once single-user mode is
114
+ // absorbed into multi-user. Disabled the moment a PIN is set or a second
115
+ // user is added (then the normal token/login flow applies).
116
+ var solo = users.getSoloNoPinUser();
117
+ if (solo) return solo;
118
+ return null;
110
119
  }
111
120
 
112
121
  function isMultiUserAuthed(req) {
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) {
package/lib/users.js CHANGED
@@ -165,6 +165,19 @@ function getAllUsers() {
165
165
  });
166
166
  }
167
167
 
168
+ // Solo frictionless access: exactly one user with no PIN. Returns the full
169
+ // user record (callers read id/role/etc.) or null. Used to skip the login wall
170
+ // so an absorbed single-user deploy stays zero-friction; auto-disables the
171
+ // moment a PIN is set or a second user is added.
172
+ function getSoloNoPinUser() {
173
+ if (!isMultiUser()) return null;
174
+ var data = loadUsers();
175
+ if (!data.users || data.users.length !== 1) return null;
176
+ var u = data.users[0];
177
+ if (u.pinHash) return null;
178
+ return u;
179
+ }
180
+
168
181
  function getOtherUsers(excludeUserId) {
169
182
  return getAllUsers().filter(function (u) {
170
183
  return u.id !== excludeUserId;
@@ -451,6 +464,7 @@ module.exports = {
451
464
  findUserByEmail: findUserByEmail,
452
465
  authenticateUser: authenticateUser,
453
466
  getAllUsers: getAllUsers,
467
+ getSoloNoPinUser: getSoloNoPinUser,
454
468
  removeUser: removeUser,
455
469
  updateUserProfile: updateUserProfile,
456
470
  updateUserPin: updateUserPin,
@@ -620,7 +620,7 @@ function createCodexQueryHandle(appServer, queryOpts) {
620
620
  done: false,
621
621
  aborted: false,
622
622
  loopStarted: false,
623
- model: queryOpts.model || "gpt-5.5",
623
+ model: queryOpts.model || "gpt-5.6-terra",
624
624
  // Track incremental text deltas
625
625
  textBlocks: {}, // itemId -> true (text_start sent)
626
626
  textLengths: {}, // itemId -> last sent length
@@ -865,7 +865,7 @@ function createCodexQueryHandle(appServer, queryOpts) {
865
865
 
866
866
  // Start or resume thread
867
867
  var threadParams = {
868
- model: queryOpts.model || "gpt-5.5",
868
+ model: queryOpts.model || "gpt-5.6-terra",
869
869
  sandbox: queryOpts.sandboxMode || "workspace-write",
870
870
  approvalPolicy: queryOpts.approvalPolicy || "on-failure",
871
871
  cwd: queryOpts.cwd,
@@ -1086,6 +1086,9 @@ function createCodexAdapter(opts) {
1086
1086
  // slow/failed `initialize` leaves the picker empty and the chip shows the
1087
1087
  // previous vendor's model.
1088
1088
  var CODEX_MODELS = [
1089
+ "gpt-5.6-terra",
1090
+ "gpt-5.6-sol",
1091
+ "gpt-5.6-luna",
1089
1092
  "gpt-5.5",
1090
1093
  "gpt-5.4",
1091
1094
  "gpt-5.4-mini",
@@ -1131,7 +1134,7 @@ function createCodexAdapter(opts) {
1131
1134
  function buildReadyResponse(skillNames) {
1132
1135
  return {
1133
1136
  models: _cachedModels,
1134
- defaultModel: "gpt-5.5",
1137
+ defaultModel: "gpt-5.6-terra",
1135
1138
  skills: skillNames || [],
1136
1139
  slashCommands: skillNames || [],
1137
1140
  fastModeState: null,
@@ -1428,7 +1431,7 @@ function createCodexAdapter(opts) {
1428
1431
  throw new Error("[yoke/codex] Adapter not initialized. Call init() first.");
1429
1432
  }
1430
1433
 
1431
- var model = queryOpts.model || "gpt-5.5";
1434
+ var model = queryOpts.model || "gpt-5.6-terra";
1432
1435
  var ac = queryOpts.abortController || new AbortController();
1433
1436
  var activeEntry = {
1434
1437
  abort: function() {