clay-server 2.45.0-beta.3 → 2.45.0-beta.4

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.
package/lib/project.js CHANGED
@@ -907,13 +907,20 @@ function createProjectContext(opts) {
907
907
  slug: slug,
908
908
  });
909
909
  } else if ((!sm.modelsByVendor || !sm.modelsByVendor[msg.vendor]) && typeof vendorAdapter.init === "function") {
910
- await vendorAdapter.init({
911
- cwd: cwd,
912
- clayPort: serverPort,
913
- clayTls: serverTls,
914
- clayAuthToken: serverAuthToken,
915
- slug: slug,
916
- });
910
+ // Init warms the adapter, but a slow/failed init must not block
911
+ // model listing (e.g. Codex models are a fixed list). Keep going
912
+ // to supportedModels() even if init throws.
913
+ try {
914
+ await vendorAdapter.init({
915
+ cwd: cwd,
916
+ clayPort: serverPort,
917
+ clayTls: serverTls,
918
+ clayAuthToken: serverAuthToken,
919
+ slug: slug,
920
+ });
921
+ } catch (e) {
922
+ console.error("[project] " + msg.vendor + " init failed (continuing to model list):", e.message || e);
923
+ }
917
924
  }
918
925
  if (vendorAdapter) {
919
926
  sm.availableVendors = Object.keys(adapters);
@@ -443,8 +443,18 @@ export function processMessage(msg) {
443
443
  if (_modelVal && typeof _modelVal === "object") _modelVal = _modelVal.value || _modelVal.displayName || "";
444
444
  var _miUpdate = { currentModels: msg.models || [] };
445
445
  if (Object.prototype.hasOwnProperty.call(msg, "model")) {
446
- if (store.get('vendorSelectionLocked') && store.get('currentModel')) {
447
- // Keep the user's existing selection; only update models list
446
+ // Keep the user's existing selection only if it actually belongs to
447
+ // this vendor's model list. Otherwise (e.g. switching to a Codex
448
+ // session while currentModel is still a Claude model) snap to the
449
+ // server-provided default so the chip doesn't show a cross-vendor
450
+ // model.
451
+ var _curModel = store.get('currentModel');
452
+ var _curInList = !!_curModel && (msg.models || []).some(function (m) {
453
+ var v = typeof m === "string" ? m : (m && m.value);
454
+ return v === _curModel;
455
+ });
456
+ if (store.get('vendorSelectionLocked') && _curInList) {
457
+ // Keep the user's existing (valid) selection; only update models list
448
458
  } else {
449
459
  _miUpdate.currentModel = _modelVal || "";
450
460
  }
@@ -36,6 +36,50 @@ function attachMessageProcessor(ctx) {
36
36
  sm.sendToSession(session, obj);
37
37
  }
38
38
 
39
+ // Emit the "not logged in" signal (WS message + notification) so the client
40
+ // auto-opens the login modal. Shared by the Claude path (login-prompt text
41
+ // detection) and the Codex path (app-server unauthorized error). Deduped
42
+ // per session to avoid spamming when multiple auth errors arrive in a burst.
43
+ function emitAuthRequired(session) {
44
+ var now = Date.now();
45
+ if (session._lastAuthEmit && (now - session._lastAuthEmit) < 5000) return;
46
+ session._lastAuthEmit = now;
47
+
48
+ var authUser = session.ownerId ? usersModule.findUserById(session.ownerId) : null;
49
+ var authLinuxUser = authUser && authUser.linuxUser ? authUser.linuxUser : null;
50
+ var canAutoLogin = !usersModule.isMultiUser()
51
+ || !!authLinuxUser
52
+ || (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";
57
+ var _nmLogin = getNotificationsModule();
58
+ sendAndRecord(session, {
59
+ type: "auth_required",
60
+ text: authTitle,
61
+ vendor: session.vendor || "claude",
62
+ loginCommand: loginCommand,
63
+ linuxUser: authLinuxUser,
64
+ canAutoLogin: canAutoLogin,
65
+ });
66
+ if (_nmLogin) {
67
+ _nmLogin.notify("auth_required", {
68
+ title: authTitle,
69
+ body: "Open a terminal, then click the URL and follow the instructions.",
70
+ slug: slug,
71
+ sessionId: session.localId,
72
+ ownerId: session.ownerId || null,
73
+ vendor: session.vendor || "claude",
74
+ loginCommand: loginCommand,
75
+ linuxUser: authLinuxUser,
76
+ canAutoLogin: canAutoLogin,
77
+ });
78
+ }
79
+ // Reset CLI session so the next query starts fresh with new auth.
80
+ session.cliSessionId = null;
81
+ }
82
+
39
83
  function getModelsForVendor(vendor) {
40
84
  if (vendor && sm.modelsByVendor && sm.modelsByVendor[vendor]) return sm.modelsByVendor[vendor];
41
85
  return sm.availableModels || [];
@@ -447,40 +491,7 @@ function attachMessageProcessor(ctx) {
447
491
  // Detect "Not logged in / Please run /login" from SDK.
448
492
  // This is a short canned response with zero cost, not actual AI output.
449
493
  if (isLoginPrompt) {
450
- var authUser = session.ownerId ? usersModule.findUserById(session.ownerId) : null;
451
- var authLinuxUser = authUser && authUser.linuxUser ? authUser.linuxUser : null;
452
- var canAutoLogin = !usersModule.isMultiUser()
453
- || !!authLinuxUser
454
- || (authUser && authUser.role === "admin");
455
- var authTitle = session.vendor === "codex" ? "Codex is not logged in." : "Claude Code is not logged in.";
456
- var loginCommand = session.vendor === "codex"
457
- ? "codex login --device-auth"
458
- : "claude login";
459
- var _nmLogin = getNotificationsModule();
460
- var authMsg = {
461
- type: "auth_required",
462
- text: authTitle,
463
- vendor: session.vendor || "claude",
464
- loginCommand: loginCommand,
465
- linuxUser: authLinuxUser,
466
- canAutoLogin: canAutoLogin,
467
- };
468
- sendAndRecord(session, authMsg);
469
- if (_nmLogin) {
470
- _nmLogin.notify("auth_required", {
471
- title: authTitle,
472
- body: "Open a terminal, then click the URL and follow the instructions.",
473
- slug: slug,
474
- sessionId: session.localId,
475
- ownerId: session.ownerId || null,
476
- vendor: session.vendor || "claude",
477
- loginCommand: loginCommand,
478
- linuxUser: authLinuxUser,
479
- canAutoLogin: canAutoLogin,
480
- });
481
- }
482
- // Reset CLI session so next query starts fresh with new auth
483
- session.cliSessionId = null;
494
+ emitAuthRequired(session);
484
495
  }
485
496
  sendAndRecord(session, { type: "done", code: 0 });
486
497
  var _donePreviewText = (session.responsePreview || "").replace(/\s+/g, " ").trim();
@@ -751,6 +762,14 @@ function attachMessageProcessor(ctx) {
751
762
  message: parsed.message || "",
752
763
  });
753
764
 
765
+ } else if (parsed.yokeType === "auth_required") {
766
+ // Vendor adapter signalled the session isn't authenticated (e.g. Codex
767
+ // app-server returned an unauthorized/token-revoked error). Trigger the
768
+ // same login flow as the Claude login-prompt path.
769
+ session.isProcessing = false;
770
+ onProcessingChanged();
771
+ emitAuthRequired(session);
772
+
754
773
  } else if (parsed.yokeType === "model_refusal") {
755
774
  // Model declined the request. "fallback" => the CLI retried on another
756
775
  // model; "no_fallback" => the turn ended with a refusal.
@@ -27,6 +27,7 @@ var ENTRIES = [
27
27
  title: "TUI becomes the default for Claude sessions",
28
28
  publishedAt: "2026-05-25",
29
29
  image: null,
30
+ popup: false, // superseded by "GUI is back"; keep in the home feed only
30
31
  summary: "Anthropic is splitting Claude billing into Interactive and Programmatic buckets on June 15. To keep working with your existing plan, Clay opens Claude sessions in the embedded claude CLI by default from that date.",
31
32
  body: '' +
32
33
  '<p><strong>2026-06-15 Anthropic billing change.</strong> Claude subscriptions split into two buckets:</p>' +
@@ -55,6 +56,7 @@ var ENTRIES = [
55
56
  title: "Mates leaves the official feature set",
56
57
  publishedAt: "2026-05-25",
57
58
  image: null,
59
+ popup: false, // superseded by "GUI is back"; keep in the home feed only
58
60
  summary: "June 15 will be Mates' last day as a first-class Clay feature. It continues as a separate standalone project so Clay can focus fully on being a developer tool.",
59
61
  body: '' +
60
62
  '<p>On <strong>2026-06-15</strong>, Mates leaves Clay\'s official feature set. Your existing Mates and their knowledge files will remain on disk; we will share migration instructions and a link to the new standalone project before the cutover.</p>' +
@@ -63,6 +65,20 @@ var ENTRIES = [
63
65
  '<p>We are formally positioning Clay as a <strong>developer tool</strong>. From here on, every roadmap decision is judged by a single question: does it make Clay the sleekest, sharpest workspace on the AI-coding frontier? Mates no longer fits that bar cleanly, so it gets its own home where it can evolve on its own terms.</p>' +
64
66
  '<p>Thanks for being here. Clay only gets sharper from here.</p>',
65
67
  },
68
+ {
69
+ id: "2026-07-gui-is-back",
70
+ title: "GUI is back as the default",
71
+ publishedAt: "2026-07-01",
72
+ image: null,
73
+ summary: "Anthropic reversed the June 15 billing split, so SDK usage is no longer billed separately. Clay opens Claude sessions in the GUI chat again by default. Mates is now off by default as it winds down.",
74
+ body: '' +
75
+ '<p><strong>Good news: Anthropic walked back the June 15 billing change.</strong> The planned Interactive vs. Programmatic split (which would have charged Claude Agent SDK usage at full API rates) is off the table, so the reason we moved everyone to the TUI is gone.</p>' +
76
+ '<h3>GUI is the default again</h3>' +
77
+ '<p>New Claude sessions open in the <strong>GUI chat</strong> (SDK) by default, the way they did before. The embedded <code>claude</code> <strong>TUI</strong> is still there whenever you want it &mdash; toggle <strong>"Open Claude as terminal (TUI)"</strong> in Settings.</p>' +
78
+ '<h3>Mates is off by default</h3>' +
79
+ '<p>As announced, <strong>Mates</strong> is no longer a first-class Clay feature. It is now <strong>off by default</strong> so Clay stays focused as a developer tool. Your existing Mates and their knowledge files remain on disk &mdash; if you still use them, re-enable Mates in <strong>Settings</strong>.</p>' +
80
+ '<p>Clay only gets sharper from here.</p>',
81
+ },
66
82
  ];
67
83
 
68
84
  module.exports = { ENTRIES: ENTRIES };
package/lib/whats-new.js CHANGED
@@ -26,6 +26,9 @@ function getStateForUser(userId) {
26
26
  var seen = (typeof users.getWhatsNewSeenIds === "function" && userId) ? users.getWhatsNewSeenIds(userId) : [];
27
27
  var unseenIds = [];
28
28
  for (var i = 0; i < entries.length; i++) {
29
+ // popup:false entries stay in the home feed (entries) as history but never
30
+ // auto-pop in the carousel, so superseded announcements don't resurface.
31
+ if (entries[i] && entries[i].popup === false) continue;
29
32
  if (entries[i] && entries[i].id && seen.indexOf(entries[i].id) === -1) {
30
33
  unseenIds.push(entries[i].id);
31
34
  }
@@ -145,6 +145,16 @@ function normalizePlanStatus(status) {
145
145
  return "pending";
146
146
  }
147
147
 
148
+ // Detect Codex "not logged in" errors. Codex surfaces auth failures several
149
+ // ways depending on transport: a clean error event with
150
+ // codexErrorInfo:"unauthorized", or a turn/failed / item error whose message
151
+ // carries a 401 / token-revoked / missing-bearer / "sign in again" string.
152
+ // Callers map a match to the neutral auth_required yokeType.
153
+ function isCodexAuthError(text, errObj) {
154
+ if (errObj && errObj.codexErrorInfo === "unauthorized") return true;
155
+ return /sign in again|token[_ ]?revoked|invalidated oauth|missing bearer|unauthorized|\b401\b/i.test(String(text || ""));
156
+ }
157
+
148
158
  function extractPromptSuggestion(params) {
149
159
  if (!params) return "";
150
160
  if (typeof params.suggestion === "string") return params.suggestion;
@@ -226,9 +236,14 @@ function flattenEvent(notification, state) {
226
236
  }
227
237
 
228
238
  if (method === "turn/failed") {
239
+ var tfMsg = params.error ? params.error.message : "Turn failed";
240
+ if (isCodexAuthError(tfMsg, params.error)) {
241
+ events.push({ yokeType: "auth_required", vendor: "codex" });
242
+ return events;
243
+ }
229
244
  events.push({
230
245
  yokeType: "error",
231
- text: params.error ? params.error.message : "Turn failed",
246
+ text: tfMsg,
232
247
  });
233
248
  return events;
234
249
  }
@@ -525,9 +540,14 @@ function flattenEvent(notification, state) {
525
540
 
526
541
  // Error item
527
542
  if (item.type === "error") {
543
+ var ieMsg = item.message || "Unknown error";
544
+ if (isCodexAuthError(ieMsg, item)) {
545
+ events.push({ yokeType: "auth_required", vendor: "codex" });
546
+ return events;
547
+ }
528
548
  events.push({
529
549
  yokeType: "error",
530
- text: item.message || "Unknown error",
550
+ text: ieMsg,
531
551
  });
532
552
  return events;
533
553
  }
@@ -551,6 +571,20 @@ function flattenEvent(notification, state) {
551
571
  return events;
552
572
  }
553
573
 
574
+ // Top-level error event. Codex signals "not logged in" via an unauthorized /
575
+ // token-revoked error (not a login-prompt message like Claude), so map it to
576
+ // the neutral auth_required yokeType to drive the login flow.
577
+ if (method === "error" && params && params.error) {
578
+ var cErr = params.error;
579
+ var cErrMsg = cErr.message || "Codex error";
580
+ if (isCodexAuthError(cErrMsg, cErr)) {
581
+ events.push({ yokeType: "auth_required", vendor: "codex" });
582
+ return events;
583
+ }
584
+ events.push({ yokeType: "error", text: cErrMsg });
585
+ return events;
586
+ }
587
+
554
588
  // Unknown event type - pass through
555
589
  console.log("[yoke/codex] UNHANDLED event:", method, JSON.stringify(params).substring(0, 200));
556
590
  events.push({
@@ -922,10 +956,10 @@ function createCodexQueryHandle(appServer, queryOpts) {
922
956
  if (!isCancelled() && e.name !== "AbortError") {
923
957
  console.error("[yoke/codex] runQueryLoop error:", e.message || e);
924
958
  console.error("[yoke/codex] stack:", e.stack || "(no stack)");
925
- pushEvent({
926
- yokeType: "error",
927
- text: e.message || String(e),
928
- });
959
+ var loopErrMsg = e.message || String(e);
960
+ pushEvent(isCodexAuthError(loopErrMsg)
961
+ ? { yokeType: "auth_required", vendor: "codex" }
962
+ : { yokeType: "error", text: loopErrMsg });
929
963
  }
930
964
  }
931
965
 
@@ -1047,7 +1081,19 @@ function createCodexAdapter(opts) {
1047
1081
  var _cwd = (opts && opts.cwd) || process.cwd();
1048
1082
  var _slug = (opts && opts.slug) || "";
1049
1083
  var _defaultInitOpts = Object.assign({}, opts || {});
1050
- var _cachedModels = [];
1084
+ // Codex models are a fixed list (the app-server doesn't enumerate them), so
1085
+ // model listing must not depend on a successful app-server init — otherwise a
1086
+ // slow/failed `initialize` leaves the picker empty and the chip shows the
1087
+ // previous vendor's model.
1088
+ var CODEX_MODELS = [
1089
+ "gpt-5.5",
1090
+ "gpt-5.4",
1091
+ "gpt-5.4-mini",
1092
+ "gpt-5.3-codex",
1093
+ "gpt-5.3-codex-spark",
1094
+ "gpt-5.2",
1095
+ ];
1096
+ var _cachedModels = CODEX_MODELS.slice();
1051
1097
  var _appServer = null;
1052
1098
  var _initPromise = null;
1053
1099
  var _shutdownPromise = null;
@@ -1297,16 +1343,9 @@ function createCodexAdapter(opts) {
1297
1343
  throw createShutdownError();
1298
1344
  }
1299
1345
 
1300
- console.log("[codex] App-server initialized, models: gpt-5.5, gpt-5.4, gpt-5.4-mini, gpt-5.3-codex, gpt-5.3-codex-spark, gpt-5.2");
1346
+ console.log("[codex] App-server initialized, models: " + CODEX_MODELS.join(", "));
1301
1347
 
1302
- _cachedModels = [
1303
- "gpt-5.5",
1304
- "gpt-5.4",
1305
- "gpt-5.4-mini",
1306
- "gpt-5.3-codex",
1307
- "gpt-5.3-codex-spark",
1308
- "gpt-5.2",
1309
- ];
1348
+ _cachedModels = CODEX_MODELS.slice();
1310
1349
 
1311
1350
  // Discover skills: built-in Codex skills + Claude skills
1312
1351
  var skillNames = [];
@@ -1361,7 +1400,8 @@ function createCodexAdapter(opts) {
1361
1400
  },
1362
1401
 
1363
1402
  supportedModels: function() {
1364
- return Promise.resolve(_cachedModels.slice());
1403
+ // Fixed list; return it without requiring a live app-server init.
1404
+ return Promise.resolve(CODEX_MODELS.slice());
1365
1405
  },
1366
1406
 
1367
1407
  createToolServer: function(def) {
@@ -164,6 +164,7 @@ CodexAppServer.prototype.start = function() {
164
164
  while (lines.length > 1) {
165
165
  var line = lines.shift();
166
166
  if (line.trim()) console.log("[codex-app-server stderr]", line);
167
+ self._maybeSignalAuthError(line);
167
168
  }
168
169
  self._stderrBuf = lines[0] || "";
169
170
  });
@@ -222,6 +223,23 @@ CodexAppServer.prototype._handleMessage = function(msg) {
222
223
  }
223
224
  };
224
225
 
226
+ // The definitive "not logged in" signal from Codex is a 401 on its own
227
+ // responses endpoint, which only appears on stderr (not as a JSON-RPC error).
228
+ // When we see it, synthesize an error event so the adapter maps it to the
229
+ // neutral auth_required yokeType. Deduped so a burst of 401 retries collapses.
230
+ CodexAppServer.prototype._maybeSignalAuthError = function(line) {
231
+ if (!this.eventHandler || !line || this._authSignalSent) return;
232
+ var isAuth = /missing bearer|token[_ ]?revoked|invalidated oauth|please sign in again/i.test(line)
233
+ || (/\b401\b/.test(line) && /unauthorized|responses|openai\.com|chatgpt\.com/i.test(line));
234
+ if (!isAuth) return;
235
+ this._authSignalSent = true;
236
+ var self = this;
237
+ setTimeout(function () { self._authSignalSent = false; }, 15000);
238
+ try {
239
+ this.eventHandler({ method: "error", params: { error: { codexErrorInfo: "unauthorized", message: line } } });
240
+ } catch (e) {}
241
+ };
242
+
225
243
  // Send a JSON-RPC request (expects a response)
226
244
  CodexAppServer.prototype.send = function(method, params, timeoutMs) {
227
245
  var self = this;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "2.45.0-beta.3",
3
+ "version": "2.45.0-beta.4",
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",