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.
- package/bin/cli.js +9 -91
- package/lib/builtin-mates.js +7 -7
- package/lib/daemon-settings.js +11 -0
- package/lib/daemon.js +8 -37
- package/lib/linear-store.js +35 -8
- package/lib/mate-identity-file.js +39 -0
- package/lib/mate-knowledge-sync.js +1 -1
- package/lib/mate-ready-creation.js +2 -1
- package/lib/mates-identity.js +4 -4
- package/lib/mates-prompts.js +4 -3
- package/lib/mates.js +12 -11
- package/lib/os-users.js +5 -16
- package/lib/project-connection.js +0 -7
- package/lib/project-debate.js +3 -2
- package/lib/project-filesystem.js +21 -39
- package/lib/project-mate-interaction.js +7 -6
- package/lib/project-server-settings.js +31 -0
- package/lib/project-sessions.js +57 -184
- package/lib/project.js +3 -3
- package/lib/public/app.js +6 -37
- package/lib/public/css/input.css +3 -1
- package/lib/public/css/menus.css +3 -0
- package/lib/public/css/overlays.css +7 -0
- package/lib/public/css/project-settings-modal.css +56 -0
- package/lib/public/css/server-settings-system.css +125 -0
- package/lib/public/css/session-github.css +25 -16
- package/lib/public/css/settings-ui.css +247 -22
- package/lib/public/css/shared-browser.css +7 -3
- package/lib/public/css/title-bar.css +5 -1
- package/lib/public/index.html +173 -447
- package/lib/public/modules/account-island.js +9 -7
- package/lib/public/modules/admin-settings-email.js +197 -0
- package/lib/public/modules/admin-settings-invitations.js +183 -0
- package/lib/public/modules/admin-settings-projects.js +198 -0
- package/lib/public/modules/admin-settings-ui.js +100 -0
- package/lib/public/modules/admin-settings-users.js +331 -0
- package/lib/public/modules/admin.js +19 -949
- package/lib/public/modules/app-dm.js +0 -2
- package/lib/public/modules/app-messages.js +13 -52
- package/lib/public/modules/app-rate-limit.js +70 -12
- package/lib/public/modules/avatar.js +6 -1
- package/lib/public/modules/context-sources.js +27 -18
- package/lib/public/modules/default-ai.js +14 -7
- package/lib/public/modules/header-tui-font.js +10 -20
- package/lib/public/modules/home-mate-avatar-editor.js +2 -2
- package/lib/public/modules/linear-settings.js +3 -2
- package/lib/public/modules/markdown.js +5 -6
- package/lib/public/modules/notification-settings.js +81 -0
- package/lib/public/modules/notifications.js +44 -28
- package/lib/public/modules/personal-settings-dialog.js +51 -0
- package/lib/public/modules/profile.js +15 -6
- package/lib/public/modules/project-settings-environment.js +106 -0
- package/lib/public/modules/project-settings.js +72 -628
- package/lib/public/modules/server-settings-controls.js +316 -0
- package/lib/public/modules/server-settings-environment.js +194 -0
- package/lib/public/modules/server-settings-navigation.js +39 -0
- package/lib/public/modules/server-settings-state.js +54 -0
- package/lib/public/modules/server-settings.js +116 -788
- package/lib/public/modules/server-storage-settings.js +84 -0
- package/lib/public/modules/session-github.js +2 -1
- package/lib/public/modules/session-presence-ui.js +1 -1
- package/lib/public/modules/settings-env-values.js +34 -0
- package/lib/public/modules/settings-select.js +121 -0
- package/lib/public/modules/shared-browser.js +22 -12
- package/lib/public/modules/stt-picker.js +14 -5
- package/lib/public/modules/terminal-prefs.js +22 -0
- package/lib/public/modules/theme.js +88 -52
- package/lib/public/modules/user-settings-account.js +318 -0
- package/lib/public/modules/user-settings-appearance.js +55 -0
- package/lib/public/modules/user-settings-editors.js +24 -0
- package/lib/public/modules/user-settings-integrations.js +104 -0
- package/lib/public/modules/user-settings-layout.js +80 -0
- package/lib/public/modules/user-settings-preferences.js +48 -0
- package/lib/public/modules/user-settings-profile.js +53 -0
- package/lib/public/modules/user-settings-themes.js +38 -0
- package/lib/public/modules/user-settings-voice.js +75 -0
- package/lib/public/modules/user-settings.js +53 -480
- package/lib/public/style.css +4 -0
- package/lib/sdk-message-processor.js +16 -4
- package/lib/sdk-quota-snapshot.js +37 -0
- package/lib/server-appearance-settings.js +71 -0
- package/lib/server-linear.js +14 -8
- package/lib/server-mate-instructions.js +2 -2
- package/lib/server-mates.js +2 -1
- package/lib/server-notification-settings.js +67 -0
- package/lib/server-settings.js +4 -0
- package/lib/server-storage.js +57 -0
- package/lib/server.js +10 -2
- package/lib/sessions.js +3 -4
- package/lib/startup-projects.js +40 -0
- package/lib/storage-native-walk.js +85 -0
- package/lib/storage-usage.js +144 -0
- package/lib/users-preferences.js +0 -74
- package/lib/users.js +0 -4
- package/lib/ws-schema.js +2 -4
- package/lib/yoke/acp-process-manager.js +2 -2
- package/lib/yoke/adapters/codex.js +12 -52
- package/lib/yoke/codex-app-server.js +2 -2
- package/lib/yoke/codex-model-catalog.js +52 -0
- package/lib/yoke/codex-rate-limits.js +26 -0
- 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);
|
package/lib/project-debate.js
CHANGED
|
@@ -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
|
|
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 =
|
|
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 = /^(
|
|
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:
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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
|
-
|
|
278
|
-
|
|
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
|
|
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
|
|
733
|
+
// Load Mate AGENTS.md
|
|
733
734
|
var mateDir = matesModule.getMateDir(mateCtx, msg.mateId);
|
|
734
735
|
try {
|
|
735
|
-
claudeMd = fs.readFileSync(
|
|
736
|
+
claudeMd = fs.readFileSync(resolveMateIdentityFile(mateDir), "utf8");
|
|
736
737
|
} catch (e) {
|
|
737
|
-
//
|
|
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
|
-
//
|
|
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(
|
|
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 };
|
package/lib/project-sessions.js
CHANGED
|
@@ -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
|
-
//
|
|
400
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
503
|
+
// Retired terminal presentation preferences never affect an idle session.
|
|
516
504
|
if (session.mode === "tui") {
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
sessionOpts.
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
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
|
-
|
|
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;
|
|
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);
|
|
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
|
-
|
|
1069
|
-
|
|
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: "
|
|
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
|
-
|
|
2164
|
-
sendTo(ws, { type: "daemon_config", config:
|
|
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
|
-
|
|
2178
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
2848
|
+
// Mate projects: watch AGENTS.md and enforce system-managed sections
|
|
2849
2849
|
if (isMate) {
|
|
2850
|
-
var claudeMdPath =
|
|
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);
|