clay-server 5.1.0-beta.2 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/bin/cli.js +9 -91
  2. package/lib/builtin-mates.js +7 -7
  3. package/lib/daemon-settings.js +11 -0
  4. package/lib/daemon.js +8 -37
  5. package/lib/linear-store.js +35 -8
  6. package/lib/mate-identity-file.js +39 -0
  7. package/lib/mate-knowledge-sync.js +1 -1
  8. package/lib/mate-ready-creation.js +2 -1
  9. package/lib/mates-identity.js +4 -4
  10. package/lib/mates-prompts.js +4 -3
  11. package/lib/mates.js +12 -11
  12. package/lib/os-users.js +5 -16
  13. package/lib/project-connection.js +0 -7
  14. package/lib/project-debate.js +3 -2
  15. package/lib/project-filesystem.js +21 -39
  16. package/lib/project-mate-interaction.js +7 -6
  17. package/lib/project-server-settings.js +31 -0
  18. package/lib/project-sessions.js +57 -184
  19. package/lib/project.js +3 -3
  20. package/lib/public/app.js +6 -37
  21. package/lib/public/css/input.css +3 -1
  22. package/lib/public/css/menus.css +3 -0
  23. package/lib/public/css/overlays.css +7 -0
  24. package/lib/public/css/project-settings-modal.css +56 -0
  25. package/lib/public/css/server-settings-system.css +125 -0
  26. package/lib/public/css/session-github.css +25 -16
  27. package/lib/public/css/settings-ui.css +247 -22
  28. package/lib/public/css/shared-browser.css +7 -3
  29. package/lib/public/css/title-bar.css +5 -1
  30. package/lib/public/index.html +173 -447
  31. package/lib/public/modules/account-island.js +9 -7
  32. package/lib/public/modules/admin-settings-email.js +197 -0
  33. package/lib/public/modules/admin-settings-invitations.js +183 -0
  34. package/lib/public/modules/admin-settings-projects.js +198 -0
  35. package/lib/public/modules/admin-settings-ui.js +100 -0
  36. package/lib/public/modules/admin-settings-users.js +331 -0
  37. package/lib/public/modules/admin.js +19 -949
  38. package/lib/public/modules/app-dm.js +0 -2
  39. package/lib/public/modules/app-messages.js +13 -52
  40. package/lib/public/modules/app-rate-limit.js +70 -12
  41. package/lib/public/modules/avatar.js +6 -1
  42. package/lib/public/modules/context-sources.js +27 -18
  43. package/lib/public/modules/default-ai.js +14 -7
  44. package/lib/public/modules/header-tui-font.js +10 -20
  45. package/lib/public/modules/home-mate-avatar-editor.js +2 -2
  46. package/lib/public/modules/linear-settings.js +3 -2
  47. package/lib/public/modules/markdown.js +5 -6
  48. package/lib/public/modules/notification-settings.js +81 -0
  49. package/lib/public/modules/notifications.js +44 -28
  50. package/lib/public/modules/personal-settings-dialog.js +51 -0
  51. package/lib/public/modules/profile.js +15 -6
  52. package/lib/public/modules/project-settings-environment.js +106 -0
  53. package/lib/public/modules/project-settings.js +72 -628
  54. package/lib/public/modules/server-settings-controls.js +316 -0
  55. package/lib/public/modules/server-settings-environment.js +194 -0
  56. package/lib/public/modules/server-settings-navigation.js +39 -0
  57. package/lib/public/modules/server-settings-state.js +54 -0
  58. package/lib/public/modules/server-settings.js +116 -788
  59. package/lib/public/modules/server-storage-settings.js +84 -0
  60. package/lib/public/modules/session-github.js +2 -1
  61. package/lib/public/modules/session-presence-ui.js +1 -1
  62. package/lib/public/modules/settings-env-values.js +34 -0
  63. package/lib/public/modules/settings-select.js +121 -0
  64. package/lib/public/modules/shared-browser.js +22 -12
  65. package/lib/public/modules/stt-picker.js +14 -5
  66. package/lib/public/modules/terminal-prefs.js +22 -0
  67. package/lib/public/modules/theme.js +88 -52
  68. package/lib/public/modules/user-settings-account.js +318 -0
  69. package/lib/public/modules/user-settings-appearance.js +55 -0
  70. package/lib/public/modules/user-settings-editors.js +24 -0
  71. package/lib/public/modules/user-settings-integrations.js +104 -0
  72. package/lib/public/modules/user-settings-layout.js +80 -0
  73. package/lib/public/modules/user-settings-preferences.js +48 -0
  74. package/lib/public/modules/user-settings-profile.js +53 -0
  75. package/lib/public/modules/user-settings-themes.js +38 -0
  76. package/lib/public/modules/user-settings-voice.js +75 -0
  77. package/lib/public/modules/user-settings.js +53 -480
  78. package/lib/public/style.css +4 -0
  79. package/lib/sdk-message-processor.js +16 -4
  80. package/lib/sdk-quota-snapshot.js +37 -0
  81. package/lib/server-appearance-settings.js +71 -0
  82. package/lib/server-linear.js +14 -8
  83. package/lib/server-mate-instructions.js +2 -2
  84. package/lib/server-mates.js +2 -1
  85. package/lib/server-notification-settings.js +67 -0
  86. package/lib/server-settings.js +4 -0
  87. package/lib/server-storage.js +57 -0
  88. package/lib/server.js +10 -2
  89. package/lib/sessions.js +3 -4
  90. package/lib/startup-projects.js +40 -0
  91. package/lib/storage-native-walk.js +85 -0
  92. package/lib/storage-usage.js +144 -0
  93. package/lib/users-preferences.js +0 -74
  94. package/lib/users.js +0 -4
  95. package/lib/ws-schema.js +2 -4
  96. package/lib/yoke/acp-process-manager.js +2 -2
  97. package/lib/yoke/adapters/codex.js +12 -52
  98. package/lib/yoke/codex-app-server.js +2 -2
  99. package/lib/yoke/codex-model-catalog.js +52 -0
  100. package/lib/yoke/codex-rate-limits.js +26 -0
  101. package/package.json +3 -3
@@ -246,13 +246,6 @@ function attachConnection(ctx) {
246
246
  sendTo(ws, { type: "email_accounts_list", accounts: emailAccountsList, providers: emailAccounts.PROVIDER_PRESETS });
247
247
  sendTo(ws, { type: "notes_list", notes: nm.list() });
248
248
  sendTo(ws, { type: "loop_registry_updated", records: getHubSchedules() });
249
- // Initial per-user preference: how to render Claude sessions.
250
- if (usersModule && typeof usersModule.getClaudeOpenMode === "function") {
251
- var _comUid = (wsUser && wsUser.id) || null;
252
- var _comVal = _comUid ? usersModule.getClaudeOpenMode(_comUid) : "gui";
253
- sendTo(ws, { type: "claude_open_mode_changed", claudeOpenMode: _comVal || "gui" });
254
- }
255
-
256
249
  _loop.sendConnectionState(ws);
257
250
  if (_mcp) _mcp.sendConnectionState(ws);
258
251
  if (_notifications) _notifications.sendConnectionState(ws, sendTo);
@@ -1,3 +1,4 @@
1
+ var resolveMateIdentityFile = require('./mate-identity-file').resolveMateIdentityFile;
1
2
  var fs = require("fs");
2
3
  var path = require("path");
3
4
  var crypto = require("crypto");
@@ -15,10 +16,10 @@ var debateToolPolicy = require("./home-debate-tool-policy");
15
16
  */
16
17
  function attachDebate(ctx) {
17
18
 
18
- // For mate projects, enforce latest debate awareness prompt in CLAUDE.md
19
+ // For mate projects, enforce latest debate awareness prompt in AGENTS.md
19
20
  // so mates use the propose_debate MCP tool instead of writing files.
20
21
  if (ctx.isMate) {
21
- var _debateClaudeMdPath = path.join(ctx.cwd, "CLAUDE.md");
22
+ var _debateClaudeMdPath = resolveMateIdentityFile(ctx.cwd);
22
23
  try { matesModule.enforceDebateAwareness(_debateClaudeMdPath); } catch (e) {}
23
24
  }
24
25
 
@@ -204,26 +204,28 @@ function attachFilesystem(ctx) {
204
204
 
205
205
  // --- Project settings permission gate ---
206
206
  if (msg.type === "get_project_env" || msg.type === "set_project_env" ||
207
- msg.type === "read_global_claude_md" || msg.type === "write_global_claude_md" ||
208
207
  msg.type === "get_shared_env" || msg.type === "set_shared_env" ||
209
208
  msg.type === "transfer_project_owner") {
210
- var globalSetting = /^(read_global_claude_md|write_global_claude_md|get_shared_env|set_shared_env)$/.test(msg.type);
209
+ var globalSetting = /^(get_shared_env|set_shared_env)$/.test(msg.type);
211
210
  var targetSlug = msg.slug || slug;
212
211
  if (!access.hasPermission(ws, "projectSettings") ||
213
212
  (globalSetting ? !access.isAdmin(ws) : !access.canAccessProject(ws, targetSlug))) {
214
- sendTo(ws, { type: "error", text: "Project settings access is not permitted" });
213
+ sendTo(ws, { type: globalSetting ? (msg.type === 'get_shared_env' ? 'shared_env_result' : 'set_shared_env_result') : (msg.type === 'get_project_env' ? 'project_env_result' : msg.type === 'set_project_env' ? 'set_project_env_result' : 'error'), slug: targetSlug, requestId: msg.requestId, ok: false, error: 'Project settings access is not permitted', text: "Project settings access is not permitted" });
215
214
  return true;
216
215
  }
217
216
  }
218
217
 
219
218
  // --- Project environment variables ---
220
219
  if (msg.type === "get_project_env") {
220
+ if (typeof opts.onGetProjectEnv !== "function") { sendTo(ws, { type: "project_env_result", slug: targetSlug, requestId: msg.requestId, error: "Project environment is unavailable in this runtime." }); return true; }
221
221
  var envrc = "";
222
222
  if (typeof opts.onGetProjectEnv === "function") {
223
- var envResult = opts.onGetProjectEnv(targetSlug);
223
+ var envResult;
224
+ try { envResult = opts.onGetProjectEnv(targetSlug); }
225
+ catch (error) { sendTo(ws, { type: "project_env_result", slug: targetSlug, requestId: msg.requestId, error: error.message }); return true; }
224
226
  envrc = envResult.envrc || "";
225
227
  }
226
- sendTo(ws, { type: "project_env_result", slug: msg.slug, envrc: envrc });
228
+ sendTo(ws, { type: "project_env_result", slug: targetSlug, requestId: msg.requestId, envrc: envrc });
227
229
  return true;
228
230
  }
229
231
 
@@ -231,41 +233,16 @@ function attachFilesystem(ctx) {
231
233
  if (typeof opts.onSetProjectEnv === "function") {
232
234
  var envError = validateEnvString(msg.envrc || "");
233
235
  if (envError) {
234
- sendTo(ws, { type: "set_project_env_result", ok: false, slug: msg.slug, error: envError });
236
+ sendTo(ws, { type: "set_project_env_result", ok: false, slug: targetSlug, requestId: msg.requestId, error: envError });
235
237
  return true;
236
238
  }
237
- var setResult = opts.onSetProjectEnv(targetSlug, msg.envrc || "");
239
+ var setResult;
240
+ try { setResult = opts.onSetProjectEnv(targetSlug, msg.envrc || ""); }
241
+ catch (error) { sendTo(ws, { type: "set_project_env_result", slug: targetSlug, requestId: msg.requestId, ok: false, error: error.message }); return true; }
238
242
  if (setResult.ok) onEnvironmentChanged();
239
- sendTo(ws, { type: "set_project_env_result", ok: setResult.ok, slug: msg.slug, error: setResult.error, timing: setResult.ok ? "Applies to newly created coding-agent processes. Active processes keep their current environment." : undefined });
243
+ sendTo(ws, { type: "set_project_env_result", ok: setResult.ok, slug: targetSlug, requestId: msg.requestId, error: setResult.error, timing: setResult.ok ? "Applies to newly created coding-agent processes. Active processes keep their current environment." : undefined });
240
244
  } else {
241
- sendTo(ws, { type: "set_project_env_result", ok: false, error: "Not supported" });
242
- }
243
- return true;
244
- }
245
-
246
- // --- Global CLAUDE.md ---
247
- if (msg.type === "read_global_claude_md") {
248
- var globalMdPath = path.join(require("./config").REAL_HOME, ".claude", "CLAUDE.md");
249
- try {
250
- var globalMdContent = fs.readFileSync(globalMdPath, "utf8");
251
- sendTo(ws, { type: "global_claude_md_result", content: globalMdContent });
252
- } catch (e) {
253
- sendTo(ws, { type: "global_claude_md_result", error: e.message });
254
- }
255
- return true;
256
- }
257
-
258
- if (msg.type === "write_global_claude_md") {
259
- var globalMdDir = path.join(require("./config").REAL_HOME, ".claude");
260
- var globalMdWritePath = path.join(globalMdDir, "CLAUDE.md");
261
- try {
262
- if (!fs.existsSync(globalMdDir)) {
263
- fs.mkdirSync(globalMdDir, { recursive: true });
264
- }
265
- fs.writeFileSync(globalMdWritePath, msg.content || "", "utf8");
266
- sendTo(ws, { type: "write_global_claude_md_result", ok: true });
267
- } catch (e) {
268
- sendTo(ws, { type: "write_global_claude_md_result", ok: false, error: e.message });
245
+ sendTo(ws, { type: "set_project_env_result", slug: targetSlug, requestId: msg.requestId, ok: false, error: "Not supported" });
269
246
  }
270
247
  return true;
271
248
  }
@@ -274,9 +251,12 @@ function attachFilesystem(ctx) {
274
251
  if (msg.type === "get_shared_env") {
275
252
  var sharedEnvrc = "";
276
253
  if (typeof opts.onGetSharedEnv === "function") {
277
- var sharedResult = opts.onGetSharedEnv();
278
- sharedEnvrc = sharedResult.envrc || "";
254
+ try {
255
+ var sharedResult = opts.onGetSharedEnv();
256
+ sharedEnvrc = sharedResult.envrc || "";
257
+ } catch (error) { sendTo(ws, { type: "shared_env_result", error: error.message }); return true; }
279
258
  }
259
+ if (typeof opts.onGetSharedEnv !== "function") { sendTo(ws, { type: "shared_env_result", error: "Shared environment is unavailable in this runtime." }); return true; }
280
260
  sendTo(ws, { type: "shared_env_result", envrc: sharedEnvrc });
281
261
  return true;
282
262
  }
@@ -288,7 +268,9 @@ function attachFilesystem(ctx) {
288
268
  sendTo(ws, { type: "set_shared_env_result", ok: false, error: sharedEnvError });
289
269
  return true;
290
270
  }
291
- var sharedSetResult = opts.onSetSharedEnv(msg.envrc || "");
271
+ var sharedSetResult;
272
+ try { sharedSetResult = opts.onSetSharedEnv(msg.envrc || ""); }
273
+ catch (error) { sendTo(ws, { type: "set_shared_env_result", ok: false, error: error.message }); return true; }
292
274
  if (sharedSetResult.ok) onEnvironmentChanged();
293
275
  sendTo(ws, { type: "set_shared_env_result", ok: sharedSetResult.ok, error: sharedSetResult.error, timing: sharedSetResult.ok ? "Applies to newly created coding-agent processes. Active processes keep their current environment." : undefined });
294
276
  } else {
@@ -1,3 +1,4 @@
1
+ var resolveMateIdentityFile = require('./mate-identity-file').resolveMateIdentityFile;
1
2
  var fs = require("fs");
2
3
  var path = require("path");
3
4
  var crypto = require("crypto");
@@ -729,12 +730,12 @@ function attachMateInteraction(ctx) {
729
730
  "Read it, then answer the user's question directly. Be concise and give your honest take.\n" +
730
731
  "You are not driving the session, so do not run tools or take actions unless the user explicitly asks; commenting and advising is the default.";
731
732
  } else {
732
- // Load Mate CLAUDE.md
733
+ // Load Mate AGENTS.md
733
734
  var mateDir = matesModule.getMateDir(mateCtx, msg.mateId);
734
735
  try {
735
- claudeMd = fs.readFileSync(path.join(mateDir, "CLAUDE.md"), "utf8");
736
+ claudeMd = fs.readFileSync(resolveMateIdentityFile(mateDir), "utf8");
736
737
  } catch (e) {
737
- // CLAUDE.md may not exist for new mates
738
+ // AGENTS.md may not exist for new mates
738
739
  }
739
740
 
740
741
  // Load session digests (unified: uses memory-summary.md if available)
@@ -758,8 +759,8 @@ function attachMateInteraction(ctx) {
758
759
  onDone: mentionCallbacks.onDone,
759
760
  onError: mentionCallbacks.onError,
760
761
  canUseTool: function (toolName, input, toolOpts) {
761
- // Full-auto mode: auto-approve everything except AskUserQuestion
762
- if (sm.currentPermissionMode === "bypassPermissions" && toolName !== "AskUserQuestion") {
762
+ // Read the caller's live mode so reused mentions follow session changes.
763
+ if ((session.permissionMode || sm.currentPermissionMode) === "bypassPermissions" && toolName !== "AskUserQuestion") {
763
764
  return Promise.resolve({ behavior: "allow", updatedInput: input });
764
765
  }
765
766
  // Use the shared whitelist from sdk-bridge (read-only tools + safe bash commands)
@@ -826,7 +827,7 @@ function attachMateInteraction(ctx) {
826
827
  function loadMateClaudeMd(mateCtx, mateId) {
827
828
  var mateDir = matesModule.getMateDir(mateCtx, mateId);
828
829
  try {
829
- return fs.readFileSync(path.join(mateDir, "CLAUDE.md"), "utf8");
830
+ return fs.readFileSync(resolveMateIdentityFile(mateDir), "utf8");
830
831
  } catch (e) {
831
832
  return "";
832
833
  }
@@ -0,0 +1,31 @@
1
+ // Server administration messages. Every mutation returns an acknowledgement.
2
+ var SETTINGS = {
3
+ set_pin: { callback: 'onSetPin', field: 'pin', valid: function (value) { return value === null || typeof value === 'string' && /^\d{6}$/.test(value); }, timing: 'Applies to new sign-ins.' },
4
+ set_keep_awake: { callback: 'onSetKeepAwake', field: 'value', timing: 'Applies while the daemon is running.' },
5
+ set_inherit_groups: { callback: 'onSetInheritGroups', field: 'value', timing: 'Applies to newly spawned agent and terminal processes.' },
6
+ set_image_retention: { callback: 'onSetImageRetention', field: 'days', valid: function (value) { return Number.isInteger(value) && value >= 0 && value <= 36500; }, timing: 'The next cleanup applies this period to existing images.' }
7
+ };
8
+ function handleServerSettings(ctx, ws, msg) {
9
+ var entry = SETTINGS[msg.type];
10
+ if (!entry) return false;
11
+ function reply(data) { ctx.sendTo(ws, Object.assign({ type: msg.type + '_result' }, data)); }
12
+ if (ctx.usersModule.isMultiUser()) {
13
+ var id = ws._clayUser && ws._clayUser.id;
14
+ var user = id && ctx.usersModule.findUserById(id);
15
+ if (!user || user.role !== 'admin') { reply({ ok: false, error: 'Administrator access required.' }); return true; }
16
+ }
17
+ var value = msg[entry.field];
18
+ if (entry.valid ? !entry.valid(value) : typeof value !== 'boolean') { reply({ ok: false, error: 'Invalid setting value.' }); return true; }
19
+ var opts = ctx.opts;
20
+ try {
21
+ var config = opts.onGetDaemonConfig ? opts.onGetDaemonConfig() : {};
22
+ if (msg.type === 'set_keep_awake' && config.platform !== 'darwin') throw new Error('Keep awake requires macOS.');
23
+ if (msg.type === 'set_inherit_groups') throw new Error('Group inheritance is automatic in OS-user mode and is no longer configurable.');
24
+ if (typeof opts[entry.callback] !== 'function') throw new Error('This runtime does not support saving this setting.');
25
+ var result = opts[entry.callback](value);
26
+ reply(Object.assign({}, result, { timing: entry.timing }));
27
+ if (result.ok && msg.type === 'set_keep_awake') ctx.send({ type: 'keep_awake_changed', keepAwake: result.keepAwake });
28
+ } catch (error) { reply({ ok: false, error: error.message || 'Could not save this setting.' }); }
29
+ return true;
30
+ }
31
+ module.exports = { handleServerSettings: handleServerSettings };
@@ -1,6 +1,6 @@
1
+ var serverSettingsMessages = require("./project-server-settings");
1
2
  var fs = require("fs");
2
3
  var path = require("path");
3
- var crypto = require("crypto");
4
4
  var { execFileSync } = require("child_process");
5
5
  var { CODEX_DEFAULTS, getCodexConfig } = require("./codex-defaults");
6
6
  var updateSnooze = require("./update-snooze");
@@ -217,16 +217,6 @@ function attachSessions(ctx) {
217
217
  return opts.canAccessProjectSlug(userId, targetSlug) === true;
218
218
  }
219
219
 
220
- // Resolve the active user's Claude open-mode preference ('gui' or 'tui').
221
- // Multi-user mode reads per-user storage; single-user mode falls back to
222
- // the daemon-level default ('gui').
223
- function getClaudeOpenModeForWs(ws) {
224
- if (!usersModule || typeof usersModule.getClaudeOpenMode !== "function") return "gui";
225
- var uid = ws && ws._clayUser ? ws._clayUser.id : null;
226
- if (!uid) return "gui";
227
- try { return usersModule.getClaudeOpenMode(uid) || "gui"; } catch (e) { return "gui"; }
228
- }
229
-
230
220
  // Resolve the home directory where Claude Code writes JSONL for a
231
221
  // given session. In OS-isolation mode each Clay user runs as a real
232
222
  // Linux account so JSONL lives under /home/clay-name; for single-user
@@ -396,8 +386,8 @@ function attachSessions(ctx) {
396
386
  };
397
387
  }
398
388
 
399
- // Spawn a transient PTY for "view this Claude GUI session as TUI" (the
400
- // user's claudeOpenMode is 'tui'). The session itself stays a GUI session
389
+ // Resume a legacy terminal session without rewriting its saved origin.
390
+ // The session itself keeps its original mode
401
391
  // on disk; we only attach a runtime terminal so xterm can render
402
392
  // `claude --resume <cliSessionId>`. When the PTY dies the runtime link
403
393
  // clears and the next click can re-attach without converting the session.
@@ -441,8 +431,7 @@ function attachSessions(ctx) {
441
431
  // current click. Reads the CLI jsonl transcript (cli-sessions.js),
442
432
  // populates session.history, and tears
443
433
  // down the PTY. The born-TUI marker (session.mode === 'tui') is kept on
444
- // disk so that flipping claudeOpenMode back to 'tui' later restores the
445
- // embedded-terminal experience instead of locking the session as GUI.
434
+ // disk to preserve the session's origin and compatibility metadata.
446
435
  //
447
436
  // Idempotent: if the PTY is already gone and history is populated, this
448
437
  // is a no-op (avoids re-reading jsonl and re-writing the session file on
@@ -486,8 +475,7 @@ function attachSessions(ctx) {
486
475
  function resolveSessionForView(session, ws) {
487
476
  if (!session) return;
488
477
  if (session.vendor && !vendorSupportsTui(session.vendor)) { session.tuiSuspended = false; return; }
489
- var pref = getClaudeOpenModeForWs(ws);
490
- // A LIVE runtime always wins over the viewer's claudeOpenMode pref:
478
+ // Preserve a live runtime across this feature's retirement:
491
479
  // another user (or this user in another tab) may be in the session right
492
480
  // now, so we join it in whatever mode it is actually running and never
493
481
  // convert or kill it. "tui stays tui, gui stays gui."
@@ -512,23 +500,14 @@ function attachSessions(ctx) {
512
500
  session.tuiSuspended = false;
513
501
  return;
514
502
  }
515
- // Cold session: apply the viewer's pref.
503
+ // Retired terminal presentation preferences never affect an idle session.
516
504
  if (session.mode === "tui") {
517
- if (pref === "gui") {
518
- prepareTuiSessionForGuiView(session);
519
- session.runtimeMode = "gui";
520
- session.runtimeTerminalId = null;
521
- session.tuiSuspended = false;
522
- } else {
523
- prepareTuiSessionForGuiView(session);
524
- session.runtimeMode = null;
525
- session.runtimeTerminalId = null;
526
- session.tuiSuspended = true;
527
- }
505
+ prepareTuiSessionForGuiView(session);
506
+ session.runtimeMode = "gui";
507
+ session.runtimeTerminalId = null;
508
+ session.tuiSuspended = false;
528
509
  } else {
529
- // Born-GUI: always GUI. We no longer auto-convert a GUI session to a
530
- // `claude --resume` terminal on a pref=tui click - that hijacked shared
531
- // GUI sessions in multi-user. Such sessions render as their SDK chat.
510
+ // Chat sessions always retain their SDK presentation.
532
511
  session.runtimeMode = null;
533
512
  session.tuiSuspended = false;
534
513
  }
@@ -593,6 +572,7 @@ function attachSessions(ctx) {
593
572
  }
594
573
 
595
574
  function handleSessionsMessage(ws, msg) {
575
+ if (serverSettingsMessages.handleServerSettings({ opts: opts, usersModule: usersModule, sendTo: sendTo, send: send }, ws, msg)) return true;
596
576
  if (contextUsage.handleMessage(ws, msg)) return true;
597
577
 
598
578
  if (msg.type === "push_subscribe") {
@@ -700,95 +680,27 @@ function attachSessions(ctx) {
700
680
  sessionOpts.sessionVisibility = visibilityChoice.visibility;
701
681
  if (msg.vendor) sessionOpts.vendor = msg.vendor;
702
682
  if (isMate && msg._mateDefaultsToken === mateDefaultsToken) sessionOpts.model = msg.model;
703
- // Mode resolution: vendors without a TUI session mode are always GUI.
704
- // TUI-capable sessions honor the explicit msg.mode if provided, otherwise
705
- // fall back to the user's claudeOpenMode preference. This is what
706
- // makes the sidebar's "Claude" icon button create the right kind of
707
- // session without the client needing to know the preference.
708
- var requestedMode;
709
- if (msg.vendor && !vendorSupportsTui(msg.vendor)) {
710
- requestedMode = "gui";
711
- } else if (msg.mode === "tui" || msg.mode === "gui") {
712
- requestedMode = msg.mode;
713
- } else {
714
- requestedMode = getClaudeOpenModeForWs(ws);
715
- }
716
683
  var newSess;
717
- if (requestedMode === "tui") {
718
- // TUI sessions own their cliSessionId up-front so we can launch
719
- // `claude --session-id <uuid>` and resume the same conversation
720
- // from external terminals (claude --resume <uuid>) and from the
721
- // jsonl watcher (~/.claude/projects/<cwd>/<uuid>.jsonl).
722
- //
723
- // Construction order matters: createSession() fires session_switched
724
- // synchronously, so we must populate terminalId on the record before
725
- // switching. Use createSessionRaw + switchSession to get the right
726
- // ordering and avoid an extra rebroadcast.
727
- sessionOpts.mode = "tui";
728
- sessionOpts.cliSessionId = crypto.randomUUID();
729
- sessionOpts.vendor = sessionOpts.vendor || "claude";
730
- // Per-session bypass-permissions: TUI shell command only. The flag is
731
- // persisted on the session so lazy-resume re-spawns the same way.
732
- if (msg.dangerouslySkipPermissions) sessionOpts.dangerouslySkipPermissions = true;
733
- newSess = sm.createSessionRaw(sessionOpts);
734
- if (tm) {
735
- var tuiSid = newSess.cliSessionId;
736
- var tuiLocalId = newSess.localId;
737
- var tuiSkip = newSess.dangerouslySkipPermissions ? " --dangerously-skip-permissions" : "";
738
- var tuiCmd = "claude --session-id " + tuiSid + tuiSkip + "; exit\n";
739
- var tuiTerm = tm.create(80, 24, getOsUserInfoForWs(ws), ws, {
740
- initialInput: tuiCmd,
741
- kind: "tui-session",
742
- title: "claude " + tuiSid.slice(0, 8),
743
- onExit: function (termSession) {
744
- var s = sm.sessions.get(tuiLocalId);
745
- if (!s) return;
746
- if (termSession && termSession.reclaimed) {
747
- // Reclaimed (idle sweep or explicit Close), not a real /exit:
748
- // keep the session (its jsonl transcript stays on disk and
749
- // lazy-resume can re-spawn claude). Just drop the dead PTY link.
750
- s.terminalId = null;
751
- try { sm.saveSessionFile(s); } catch (e) {}
752
- try { sm.broadcastSessionList(); } catch (e) {}
753
- } else {
754
- try { sm.deleteSessionQuiet(tuiLocalId); } catch (e) {}
755
- try { sm.broadcastSessionList(); } catch (e) {}
756
- }
757
- },
758
- onData: makeTuiActivityHook(tuiLocalId),
759
- });
760
- if (tuiTerm) {
761
- newSess.terminalId = tuiTerm.id;
762
- }
763
- }
764
- // Persist immediately so the session reappears in the sidebar
765
- // after a daemon restart (the SDK path saves on stream events,
766
- // but TUI sessions never produce any so this is our only chance).
767
- try { sm.saveSessionFile(newSess); } catch (e) {}
768
- startTitleWatcher(newSess);
769
- sm.switchSession(newSess.localId, ws);
684
+ var newSessionVendor = sessionOpts.vendor || sm.defaultVendor || "claude";
685
+ var newSessionEffort = isMate && msg._mateDefaultsToken === mateDefaultsToken
686
+ ? (msg.effort || "")
687
+ : ((sm.currentEffortByVendor && sm.currentEffortByVendor[newSessionVendor]) || sm.currentEffort || "medium");
688
+ sessionOpts.effort = yoke.clampEffort(newSessionVendor, newSessionEffort) || null;
689
+ // Reuse an existing blank GUI session instead of stacking another
690
+ // one. A vendor-less blank is acceptable: nothing has happened in it,
691
+ // so stamping the requested vendor is equivalent to a fresh create.
692
+ var reusable = msg.forceNew === true ? null : sm.findReusableBlankSession({
693
+ vendor: sessionOpts.vendor || null,
694
+ ownerId: sessionOpts.ownerId || null,
695
+ });
696
+ if (reusable) {
697
+ if (sessionOpts.vendor && reusable.vendor !== sessionOpts.vendor) reusable.vendor = sessionOpts.vendor;
698
+ if (!reusable.effort) reusable.effort = sessionOpts.effort;
699
+ // Reusable blanks already exist; do not retroactively change them.
700
+ sm.switchSession(reusable.localId, ws);
701
+ newSess = reusable;
770
702
  } else {
771
- var newSessionVendor = sessionOpts.vendor || sm.defaultVendor || "claude";
772
- var newSessionEffort = isMate && msg._mateDefaultsToken === mateDefaultsToken
773
- ? (msg.effort || "")
774
- : ((sm.currentEffortByVendor && sm.currentEffortByVendor[newSessionVendor]) || sm.currentEffort || "medium");
775
- sessionOpts.effort = yoke.clampEffort(newSessionVendor, newSessionEffort) || null;
776
- // Reuse an existing blank GUI session instead of stacking another
777
- // one. A vendor-less blank is acceptable: nothing has happened in it,
778
- // so stamping the requested vendor is equivalent to a fresh create.
779
- var reusable = msg.forceNew === true ? null : sm.findReusableBlankSession({
780
- vendor: sessionOpts.vendor || null,
781
- ownerId: sessionOpts.ownerId || null,
782
- });
783
- if (reusable) {
784
- if (sessionOpts.vendor && reusable.vendor !== sessionOpts.vendor) reusable.vendor = sessionOpts.vendor;
785
- if (!reusable.effort) reusable.effort = sessionOpts.effort;
786
- // Reusable blanks already exist; do not retroactively change them.
787
- sm.switchSession(reusable.localId, ws);
788
- newSess = reusable;
789
- } else {
790
- newSess = sm.createSession(sessionOpts, ws);
791
- }
703
+ newSess = sm.createSession(sessionOpts, ws);
792
704
  }
793
705
  ws._clayActiveSession = newSess.localId;
794
706
  var folderPlaced = msg.folderId === undefined ? null : sessionFolders.placeNewSession(ws, newSess, msg.folderId, msg.folderSlug);
@@ -881,14 +793,11 @@ function attachSessions(ctx) {
881
793
  // resolveSessionForView sets runtimeMode / runtimeTerminalId /
882
794
  // tuiSuspended (and hydrates the transcript) before sm.switchSession
883
795
  // broadcasts session_switched. A live runtime keeps its actual mode
884
- // for every viewer; only cold sessions follow the clicker's
885
- // claudeOpenMode pref. Nothing is spawned here.
796
+ // for every viewer; idle sessions open in chat. Nothing is spawned here.
886
797
  var xmTarget = sm.sessions.get(msg.id);
887
798
  if (xmTarget && (!xmTarget.vendor || vendorSupportsTui(xmTarget.vendor))) {
888
799
  // Single source of truth: live runtime wins (tui stays tui, gui stays
889
- // gui); only cold sessions follow the viewer's pref. No PTY is spawned
890
- // on switch - born-TUI resumes lazily via the Resume bar
891
- // (resume_tui_session), and born-GUI never auto-converts to a terminal.
800
+ // gui); idle legacy sessions hydrate their transcript into chat.
892
801
  resolveSessionForView(xmTarget, ws);
893
802
  }
894
803
  var switchTargetSess = sm.sessions.get(msg.id);
@@ -1063,12 +972,17 @@ function attachSessions(ctx) {
1063
972
  }
1064
973
 
1065
974
  if (msg.type === "set_update_channel") {
1066
- if (!updateSnooze.isUpdateAdmin(ws, usersModule)) return true;
975
+ if (!updateSnooze.isUpdateAdmin(ws, usersModule)) { sendTo(ws, { type: "set_update_channel_result", ok: false, error: "Administrator access required." }); return true; }
1067
976
  var newChannel = msg.channel === "beta" ? "beta" : "stable";
1068
- setUpdateChannel(newChannel);
1069
- setLatestVersion(null);
1070
- if (typeof opts.onSetUpdateChannel === "function") {
977
+ try {
978
+ if (typeof opts.onSetUpdateChannel !== "function") throw new Error("Release channel cannot be saved in this runtime.");
1071
979
  opts.onSetUpdateChannel(newChannel);
980
+ setUpdateChannel(newChannel);
981
+ setLatestVersion(null);
982
+ sendTo(ws, { type: "set_update_channel_result", ok: true, timing: "Future update checks use this channel." });
983
+ } catch (error) {
984
+ sendTo(ws, { type: "set_update_channel_result", ok: false, error: error.message });
985
+ return true;
1072
986
  }
1073
987
  // Re-fetch with new channel and broadcast to admin clients, honouring
1074
988
  // any snooze each of them has on the version that comes back.
@@ -1095,7 +1009,7 @@ function attachSessions(ctx) {
1095
1009
  } else {
1096
1010
  sendTo(ws, { type: "up_to_date", version: currentVersion });
1097
1011
  }
1098
- }).catch(function () {});
1012
+ }).catch(function () { sendTo(ws, { type: "update_check_failed", error: "Could not check for updates. Retry when the server can reach the release service." }); });
1099
1013
  return true;
1100
1014
  }
1101
1015
 
@@ -2150,9 +2064,9 @@ function attachSessions(ctx) {
2150
2064
  if (msg.type === "get_daemon_config" || msg.type === "set_pin" || msg.type === "set_keep_awake" ||
2151
2065
  msg.type === "set_auto_continue" || msg.type === "set_inherit_groups" || msg.type === "set_image_retention" || msg.type === "shutdown_server" || msg.type === "restart_server") {
2152
2066
  if (usersModule.isMultiUser()) {
2153
- var _wsUser = ws._clayUser;
2067
+ var _wsUser = ws._clayUser && usersModule.findUserById(ws._clayUser.id);
2154
2068
  if (!_wsUser || _wsUser.role !== "admin") {
2155
- sendTo(ws, { type: "error", message: "Admin access required" });
2069
+ sendTo(ws, { type: msg.type + "_result", ok: false, error: "Administrator access required." });
2156
2070
  return true;
2157
2071
  }
2158
2072
  }
@@ -2160,28 +2074,15 @@ function attachSessions(ctx) {
2160
2074
 
2161
2075
  if (msg.type === "get_daemon_config") {
2162
2076
  if (typeof opts.onGetDaemonConfig === "function") {
2163
- var daemonConfig = opts.onGetDaemonConfig();
2164
- sendTo(ws, { type: "daemon_config", config: daemonConfig });
2165
- }
2077
+ try { sendTo(ws, { type: "daemon_config", config: opts.onGetDaemonConfig() }); }
2078
+ catch (error) { sendTo(ws, { type: "daemon_config", config: { error: 'Server configuration is unavailable. Close and reopen Settings to retry.' } }); }
2079
+ } else { sendTo(ws, { type: "daemon_config", config: { error: 'Server configuration is unavailable in this runtime.' } }); }
2166
2080
  return true;
2167
2081
  }
2168
2082
 
2169
- if (msg.type === "set_pin") {
2170
- if (typeof opts.onSetPin === "function") {
2171
- var pinResult = opts.onSetPin(msg.pin || null);
2172
- sendTo(ws, { type: "set_pin_result", ok: pinResult.ok, pinEnabled: pinResult.pinEnabled });
2173
- }
2174
- return true;
2175
- }
2176
2083
 
2177
- if (msg.type === "set_keep_awake") {
2178
- if (typeof opts.onSetKeepAwake === "function") {
2179
- var kaResult = opts.onSetKeepAwake(msg.value);
2180
- sendTo(ws, { type: "set_keep_awake_result", ok: kaResult.ok, keepAwake: kaResult.keepAwake });
2181
- send({ type: "keep_awake_changed", keepAwake: kaResult.keepAwake });
2182
- }
2183
- return true;
2184
- }
2084
+
2085
+
2185
2086
 
2186
2087
  if (msg.type === "set_auto_continue") {
2187
2088
  if (typeof opts.onSetAutoContinue === "function") {
@@ -2192,14 +2093,7 @@ function attachSessions(ctx) {
2192
2093
  return true;
2193
2094
  }
2194
2095
 
2195
- if (msg.type === "set_inherit_groups") {
2196
- if (typeof opts.onSetInheritGroups === "function") {
2197
- var igResult = opts.onSetInheritGroups(msg.value);
2198
- sendTo(ws, { type: "set_inherit_groups_result", ok: igResult.ok, inheritGroups: igResult.inheritGroups });
2199
- send({ type: "inherit_groups_changed", inheritGroups: igResult.inheritGroups });
2200
- }
2201
- return true;
2202
- }
2096
+
2203
2097
 
2204
2098
  if (msg.type === "get_claude_allow_list") {
2205
2099
  var galUid = ws._clayUser ? ws._clayUser.id : null;
@@ -2230,6 +2124,7 @@ function attachSessions(ctx) {
2230
2124
  // Re-install settings.json with the updated list so the new patterns
2231
2125
  // take effect on the next `claude` invocation. Resolve this user's
2232
2126
  // home (OS-mode: getent passwd; single-user: os.homedir()).
2127
+ var salEffective = false;
2233
2128
  try {
2234
2129
  var salInstaller = require("./claude-hook-installer");
2235
2130
  var salHome = null;
@@ -2241,38 +2136,16 @@ function attachSessions(ctx) {
2241
2136
  }
2242
2137
  if (!salHome) salHome = require("os").homedir();
2243
2138
  var salMerged = (salInstaller.CLAY_MANAGED_ALLOW || []).concat(salResult.claudeUserAllowList);
2244
- salInstaller.installAllowList({ homeDirs: [salHome], patterns: salMerged });
2139
+ var salInstalled = salInstaller.installAllowList({ homeDirs: [salHome], patterns: salMerged });
2140
+ salEffective = !salInstalled.errors.length;
2245
2141
  } catch (e) {}
2246
- sendTo(ws, { type: "set_claude_user_allow_list_result", ok: true, patterns: salResult.claudeUserAllowList });
2142
+ sendTo(ws, { type: "set_claude_user_allow_list_result", ok: true, effective: salEffective, patterns: salResult.claudeUserAllowList });
2247
2143
  return true;
2248
2144
  }
2249
2145
 
2146
+ // Old clients receive an explicit refusal; stored legacy values are ignored.
2250
2147
  if (msg.type === "set_claude_open_mode") {
2251
- // Per-user preference: when Clay opens a Claude session, render it as
2252
- // the SDK-driven custom chat ("gui") or as an embedded `claude` TUI
2253
- // ("tui"). Applies to the next session open; currently displayed
2254
- // sessions are not re-rendered retroactively.
2255
- var comUserId = ws._clayUser ? ws._clayUser.id : null;
2256
- if (!comUserId) {
2257
- sendTo(ws, { type: "set_claude_open_mode_result", ok: false, error: "no_user" });
2258
- return true;
2259
- }
2260
- var comResult = usersModule.setClaudeOpenMode(comUserId, msg.value);
2261
- if (comResult && comResult.ok) {
2262
- sendTo(ws, { type: "set_claude_open_mode_result", ok: true, claudeOpenMode: comResult.claudeOpenMode });
2263
- // Echo as a "changed" broadcast for this user's other tabs/devices.
2264
- sendTo(ws, { type: "claude_open_mode_changed", claudeOpenMode: comResult.claudeOpenMode });
2265
- } else {
2266
- sendTo(ws, { type: "set_claude_open_mode_result", ok: false, error: (comResult && comResult.error) || "unknown" });
2267
- }
2268
- return true;
2269
- }
2270
-
2271
- if (msg.type === "set_image_retention") {
2272
- if (typeof opts.onSetImageRetention === "function") {
2273
- var irResult = opts.onSetImageRetention(msg.days);
2274
- sendTo(ws, { type: "set_image_retention_result", ok: irResult.ok, days: irResult.days });
2275
- }
2148
+ sendTo(ws, { type: "set_claude_open_mode_result", ok: false, error: "Terminal presentation has been removed. Claude opens in Clay chat." });
2276
2149
  return true;
2277
2150
  }
2278
2151
 
package/lib/project.js CHANGED
@@ -1695,7 +1695,7 @@ function createProjectContext(opts) {
1695
1695
  },
1696
1696
  });
1697
1697
 
1698
- // Mate CLAUDE.md crisis safety watcher
1698
+ // Mate AGENTS.md crisis safety watcher
1699
1699
  var crisisWatcher = null;
1700
1700
  var crisisDebounce = null;
1701
1701
 
@@ -2845,9 +2845,9 @@ function createProjectContext(opts) {
2845
2845
  icon = newIcon || null;
2846
2846
  }
2847
2847
 
2848
- // Mate projects: watch CLAUDE.md and enforce system-managed sections
2848
+ // Mate projects: watch AGENTS.md and enforce system-managed sections
2849
2849
  if (isMate) {
2850
- var claudeMdPath = path.join(cwd, "CLAUDE.md");
2850
+ var claudeMdPath = require("./mate-identity-file").resolveMateIdentityFile(cwd);
2851
2851
  // Derive mateId from cwd (last path segment) and build ctx for dynamic team section
2852
2852
  var _mateId = path.basename(cwd);
2853
2853
  var _mateCtx = matesModule.buildMateCtx(projectOwnerId);