clay-server 5.1.0-beta.1 → 6.0.0-beta.1

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 (222) hide show
  1. package/LICENSES/README.md +4 -0
  2. package/LICENSES/dicebear-avatars.md +16 -0
  3. package/bin/cli.js +9 -91
  4. package/lib/daemon-project-access.js +7 -1
  5. package/lib/daemon-settings.js +11 -0
  6. package/lib/daemon.js +10 -37
  7. package/lib/dicebear-avatar.js +62 -0
  8. package/lib/knowledge-db-service.js +175 -0
  9. package/lib/knowledge-db-validation.js +0 -0
  10. package/lib/knowledge-document-index.js +49 -0
  11. package/lib/knowledge-draft-store.js +56 -0
  12. package/lib/knowledge-file-store.js +199 -0
  13. package/lib/knowledge-markdown.js +128 -0
  14. package/lib/linear-store.js +35 -8
  15. package/lib/mate-knowledge-mcp-server.js +29 -6
  16. package/lib/mate-knowledge-service.js +23 -2
  17. package/lib/mate-knowledge-sync.js +36 -21
  18. package/lib/mate-workspace-color.js +4 -0
  19. package/lib/mates.js +18 -0
  20. package/lib/os-users.js +5 -16
  21. package/lib/project-connection.js +36 -9
  22. package/lib/project-debate-proposal.js +6 -0
  23. package/lib/project-debate-workspace.js +43 -0
  24. package/lib/project-debate.js +1 -0
  25. package/lib/project-filesystem.js +21 -39
  26. package/lib/project-knowledge.js +315 -136
  27. package/lib/project-mate-creation-proposal.js +1 -0
  28. package/lib/project-mate-interaction.js +2 -2
  29. package/lib/project-server-settings.js +31 -0
  30. package/lib/project-sessions.js +92 -182
  31. package/lib/project-user-knowledge.js +60 -0
  32. package/lib/project.js +59 -18
  33. package/lib/public/app.js +79 -71
  34. package/lib/public/clay-studio-wordmark-bold.svg +20 -0
  35. package/lib/public/css/clay-workspace.css +105 -0
  36. package/lib/public/css/debates-workbench.css +47 -0
  37. package/lib/public/css/home-debate-live.css +26 -0
  38. package/lib/public/css/home-hub.css +3 -1
  39. package/lib/public/css/home-mate-model-picker.css +15 -131
  40. package/lib/public/css/home-mate-settings.css +168 -16
  41. package/lib/public/css/icon-strip.css +249 -32
  42. package/lib/public/css/input.css +3 -1
  43. package/lib/public/css/mate-conversation.css +176 -0
  44. package/lib/public/css/mate-knowledge-workbench.css +118 -0
  45. package/lib/public/css/mate-workspace-color.css +16 -0
  46. package/lib/public/css/mates.css +28 -28
  47. package/lib/public/css/messages.css +1 -114
  48. package/lib/public/css/mobile-nav.css +16 -16
  49. package/lib/public/css/overlays.css +8 -0
  50. package/lib/public/css/project-logs.css +7 -0
  51. package/lib/public/css/project-mate-creation.css +105 -0
  52. package/lib/public/css/project-settings-modal.css +56 -0
  53. package/lib/public/css/right-workbench.css +3 -3
  54. package/lib/public/css/server-settings-system.css +125 -0
  55. package/lib/public/css/session-folders.css +80 -21
  56. package/lib/public/css/session-github.css +25 -16
  57. package/lib/public/css/settings-ui.css +247 -22
  58. package/lib/public/css/shared-browser.css +2 -0
  59. package/lib/public/css/sidebar.css +183 -107
  60. package/lib/public/css/title-bar.css +156 -1
  61. package/lib/public/css/worker-delegation.css +47 -0
  62. package/lib/public/css/worker-proposal.css +72 -150
  63. package/lib/public/css/you.css +74 -0
  64. package/lib/public/index.html +203 -547
  65. package/lib/public/modules/account-island.js +232 -0
  66. package/lib/public/modules/admin-settings-email.js +197 -0
  67. package/lib/public/modules/admin-settings-invitations.js +183 -0
  68. package/lib/public/modules/admin-settings-projects.js +198 -0
  69. package/lib/public/modules/admin-settings-ui.js +100 -0
  70. package/lib/public/modules/admin-settings-users.js +331 -0
  71. package/lib/public/modules/admin.js +19 -949
  72. package/lib/public/modules/app-cursors.js +19 -43
  73. package/lib/public/modules/app-debate-ui.js +4 -3
  74. package/lib/public/modules/app-dm.js +0 -2
  75. package/lib/public/modules/app-header.js +106 -0
  76. package/lib/public/modules/app-message-router.js +12 -3
  77. package/lib/public/modules/app-messages.js +47 -61
  78. package/lib/public/modules/app-notifications.js +4 -7
  79. package/lib/public/modules/app-projects.js +5 -38
  80. package/lib/public/modules/app-rate-limit.js +70 -12
  81. package/lib/public/modules/app-rendering.js +12 -13
  82. package/lib/public/modules/avatar.js +25 -9
  83. package/lib/public/modules/chat-render-runtime.js +16 -4
  84. package/lib/public/modules/command-palette.js +2 -15
  85. package/lib/public/modules/context-sources.js +27 -18
  86. package/lib/public/modules/cursor-sharing-preference.js +3 -3
  87. package/lib/public/modules/debate-controls.js +299 -0
  88. package/lib/public/modules/debate.js +2 -0
  89. package/lib/public/modules/debates-workbench.js +339 -0
  90. package/lib/public/modules/default-ai.js +88 -65
  91. package/lib/public/modules/header-tui-font.js +10 -20
  92. package/lib/public/modules/home-debate-controls.js +18 -289
  93. package/lib/public/modules/home-mate-avatar-editor.js +228 -0
  94. package/lib/public/modules/home-mate-color-picker.js +66 -0
  95. package/lib/public/modules/home-mate-creation.js +1 -1
  96. package/lib/public/modules/home-mate-instructions.js +90 -0
  97. package/lib/public/modules/home-mate-model-picker.js +80 -271
  98. package/lib/public/modules/home-mate-profile-editor.js +72 -0
  99. package/lib/public/modules/home-mate-settings.js +118 -125
  100. package/lib/public/modules/home-shell.js +3 -0
  101. package/lib/public/modules/home-surface-boot.js +1 -9
  102. package/lib/public/modules/icon-strip-tooltip.js +95 -0
  103. package/lib/public/modules/knowledge-db-edit-state.js +35 -0
  104. package/lib/public/modules/knowledge-db-workbench.js +234 -0
  105. package/lib/public/modules/knowledge-history-state.js +55 -0
  106. package/lib/public/modules/knowledge-markdown-editor.js +57 -0
  107. package/lib/public/modules/knowledge-preview-links.js +70 -0
  108. package/lib/public/modules/knowledge-workbench-shell.js +21 -0
  109. package/lib/public/modules/linear-settings.js +3 -2
  110. package/lib/public/modules/markdown.js +5 -6
  111. package/lib/public/modules/mate-knowledge-workbench-state.js +174 -0
  112. package/lib/public/modules/mate-knowledge-workbench.js +500 -0
  113. package/lib/public/modules/mate-knowledge.js +11 -3
  114. package/lib/public/modules/mate-management.js +54 -10
  115. package/lib/public/modules/mate-sidebar.js +11 -1
  116. package/lib/public/modules/mate-workspace-color.js +33 -0
  117. package/lib/public/modules/mention.js +5 -13
  118. package/lib/public/modules/notification-settings.js +81 -0
  119. package/lib/public/modules/notifications.js +44 -28
  120. package/lib/public/modules/personal-settings-dialog.js +51 -0
  121. package/lib/public/modules/profile.js +66 -24
  122. package/lib/public/modules/project-chat-identity.js +79 -0
  123. package/lib/public/modules/project-debate-planning-workspace.js +250 -0
  124. package/lib/public/modules/project-debate-workspace.js +185 -0
  125. package/lib/public/modules/project-header-identity.js +127 -0
  126. package/lib/public/modules/project-logs-render.js +9 -4
  127. package/lib/public/modules/project-mate-creation-workspace.js +414 -0
  128. package/lib/public/modules/project-mate-navigation.js +238 -0
  129. package/lib/public/modules/project-session-navigation.js +29 -0
  130. package/lib/public/modules/project-settings-environment.js +106 -0
  131. package/lib/public/modules/project-settings.js +72 -628
  132. package/lib/public/modules/project-switcher.js +6 -8
  133. package/lib/public/modules/projects-hub-view.js +5 -6
  134. package/lib/public/modules/projects-hub.js +2 -6
  135. package/lib/public/modules/qrcode.js +27 -8
  136. package/lib/public/modules/search-clay-chat.js +8 -11
  137. package/lib/public/modules/server-settings-controls.js +316 -0
  138. package/lib/public/modules/server-settings-environment.js +194 -0
  139. package/lib/public/modules/server-settings-navigation.js +39 -0
  140. package/lib/public/modules/server-settings-state.js +54 -0
  141. package/lib/public/modules/server-settings.js +116 -788
  142. package/lib/public/modules/server-storage-settings.js +84 -0
  143. package/lib/public/modules/session-activity-tooltip.js +12 -0
  144. package/lib/public/modules/session-create-form.js +86 -14
  145. package/lib/public/modules/session-create.js +50 -7
  146. package/lib/public/modules/session-github.js +2 -1
  147. package/lib/public/modules/session-list-visibility.js +41 -0
  148. package/lib/public/modules/session-presence-ui.js +3 -0
  149. package/lib/public/modules/session-title-marquee.js +51 -0
  150. package/lib/public/modules/settings-env-values.js +34 -0
  151. package/lib/public/modules/settings-select.js +121 -0
  152. package/lib/public/modules/shared-browser.js +10 -7
  153. package/lib/public/modules/sidebar-mates.js +3 -50
  154. package/lib/public/modules/sidebar-mobile.js +63 -60
  155. package/lib/public/modules/sidebar-projects.js +84 -97
  156. package/lib/public/modules/sidebar-session-folders-ui.js +2 -2
  157. package/lib/public/modules/sidebar-session-hierarchy.js +149 -70
  158. package/lib/public/modules/sidebar-sessions.js +49 -50
  159. package/lib/public/modules/sidebar.js +11 -13
  160. package/lib/public/modules/stt-picker.js +14 -5
  161. package/lib/public/modules/terminal-prefs.js +22 -0
  162. package/lib/public/modules/theme.js +88 -52
  163. package/lib/public/modules/thinking-lifecycle.js +7 -6
  164. package/lib/public/modules/tool-palette-order.js +4 -3
  165. package/lib/public/modules/tool-palette.js +13 -3
  166. package/lib/public/modules/tools.js +22 -22
  167. package/lib/public/modules/user-settings-account.js +318 -0
  168. package/lib/public/modules/user-settings-appearance.js +55 -0
  169. package/lib/public/modules/user-settings-editors.js +24 -0
  170. package/lib/public/modules/user-settings-integrations.js +104 -0
  171. package/lib/public/modules/user-settings-layout.js +80 -0
  172. package/lib/public/modules/user-settings-preferences.js +48 -0
  173. package/lib/public/modules/user-settings-profile.js +53 -0
  174. package/lib/public/modules/user-settings-themes.js +38 -0
  175. package/lib/public/modules/user-settings-voice.js +75 -0
  176. package/lib/public/modules/user-settings.js +53 -480
  177. package/lib/public/modules/worker-proposal.js +42 -8
  178. package/lib/public/modules/worker-runtime-picker.js +115 -0
  179. package/lib/public/modules/you-activity.js +121 -0
  180. package/lib/public/modules/you.js +251 -0
  181. package/lib/public/style.css +20 -8
  182. package/lib/public/vendor/codemirror/LICENSE +21 -0
  183. package/lib/public/vendor/codemirror/codemirror.css +344 -0
  184. package/lib/public/vendor/codemirror/codemirror.js +9884 -0
  185. package/lib/public/vendor/codemirror/continuelist.js +101 -0
  186. package/lib/public/vendor/codemirror/markdown.js +886 -0
  187. package/lib/public/vendor/codemirror/show-hint.css +37 -0
  188. package/lib/public/vendor/codemirror/show-hint.js +523 -0
  189. package/lib/sdk-bridge.js +15 -3
  190. package/lib/sdk-message-processor.js +16 -4
  191. package/lib/sdk-quota-snapshot.js +37 -0
  192. package/lib/server-admin.js +11 -0
  193. package/lib/server-appearance-settings.js +71 -0
  194. package/lib/server-home-chat-events.js +1 -1
  195. package/lib/server-home-chat.js +25 -3
  196. package/lib/server-home-debate-planning.js +4 -1
  197. package/lib/server-home-debates.js +2 -0
  198. package/lib/server-home-mate-creation.js +4 -1
  199. package/lib/server-linear.js +14 -8
  200. package/lib/server-mate-instructions.js +44 -0
  201. package/lib/server-mates.js +12 -3
  202. package/lib/server-notification-settings.js +67 -0
  203. package/lib/server-palette.js +1 -1
  204. package/lib/server-settings.js +69 -1
  205. package/lib/server-storage.js +56 -0
  206. package/lib/server-user-knowledge.js +119 -0
  207. package/lib/server.js +31 -3
  208. package/lib/session-visibility.js +7 -2
  209. package/lib/sessions.js +34 -9
  210. package/lib/startup-projects.js +40 -0
  211. package/lib/storage-usage.js +127 -0
  212. package/lib/user-knowledge-service.js +167 -0
  213. package/lib/users-permissions.js +7 -0
  214. package/lib/users-preferences.js +0 -74
  215. package/lib/users-project-mate-filter-preferences.js +39 -0
  216. package/lib/users.js +6 -4
  217. package/lib/workspace-query-access.js +4 -1
  218. package/lib/ws-schema.js +52 -4
  219. package/lib/yoke/adapters/codex.js +8 -45
  220. package/lib/yoke/codex-model-catalog.js +40 -0
  221. package/lib/yoke/codex-rate-limits.js +26 -0
  222. package/package.json +12 -3
@@ -0,0 +1,37 @@
1
+ function recordQuotaSnapshot(session, info, sendAndRecord) {
2
+ var message = {
3
+ type: "rate_limit_usage",
4
+ sessionId: session.localId,
5
+ vendor: session.vendor,
6
+ source: "account_snapshot",
7
+ limitId: info.limitId,
8
+ quotaWindows: info.windows.map(function (window) {
9
+ return Object.assign({}, window, { resetsAt: window.resetsAt ? window.resetsAt * 1000 : null });
10
+ }),
11
+ };
12
+ var snapshots = session._quotaSnapshots || (session._quotaSnapshots = Object.create(null));
13
+ var key = JSON.stringify([session.ownerId, session.vendor, info.limitId]);
14
+ var serialized = JSON.stringify(message);
15
+ if (snapshots[key] === serialized) return;
16
+ snapshots[key] = serialized;
17
+ sendAndRecord(session, message);
18
+ }
19
+
20
+ function attachQuotaSnapshots(sm) {
21
+ if (typeof sm.addOnSessionViewed !== "function") return;
22
+ sm.addOnSessionViewed(function (session, ws, send) {
23
+ if (session.vendor !== "codex") return;
24
+ var seen = Object.create(null);
25
+ var history = session.history || [];
26
+ // A deduplicated snapshot may precede the initial page of chat history.
27
+ for (var i = history.length - 1; i >= 0; i--) {
28
+ var entry = history[i];
29
+ if (entry.type !== "rate_limit_usage" || entry.source !== "account_snapshot" || entry.vendor !== session.vendor) continue;
30
+ if (seen[entry.limitId]) continue;
31
+ seen[entry.limitId] = true;
32
+ send(Object.assign({}, entry, { sessionId: session.localId }));
33
+ }
34
+ });
35
+ }
36
+
37
+ module.exports = { recordQuotaSnapshot: recordQuotaSnapshot, attachQuotaSnapshots: attachQuotaSnapshots };
@@ -19,6 +19,17 @@ function attachAdmin(ctx) {
19
19
  var onProjectOwnerChanged = ctx.onProjectOwnerChanged;
20
20
 
21
21
  function handleRequest(req, res, fullUrl) {
22
+ var mateAccessRoute = /^\/api\/admin\/projects\/([^/]+)\/(visibility|owner|users|access)$/.exec(fullUrl);
23
+ if (mateAccessRoute) {
24
+ var target = projects.get(mateAccessRoute[1]);
25
+ var status = target && target.getStatus();
26
+ if ((status && status.isMate) || mateAccessRoute[1].indexOf("mate-") === 0) {
27
+ res.writeHead(403, { "Content-Type": "application/json" });
28
+ res.end(JSON.stringify({ error: "Clay and Mate ownership is private and cannot be shared or transferred." }));
29
+ return true;
30
+ }
31
+ }
32
+
22
33
 
23
34
  // --- Admin API endpoints (multi-user mode only) ---
24
35
 
@@ -0,0 +1,71 @@
1
+ var fs = require('fs');
2
+ var path = require('path');
3
+
4
+ function attachAppearanceSettings(ctx) {
5
+ var defaultPath = path.join(ctx.CONFIG_DIR, 'appearance-preferences.json');
6
+ function reply(res, code, data) {
7
+ res.writeHead(code, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
8
+ res.end(JSON.stringify(data));
9
+ }
10
+ function identity(req) {
11
+ if (!ctx.isRequestAuthed(req)) return null;
12
+ if (!ctx.users.isMultiUser()) return { id: 'default' };
13
+ return ctx.getMultiUserFromReq(req);
14
+ }
15
+ function read(user) {
16
+ var saved;
17
+ if (ctx.users.isMultiUser()) saved = user.appearancePreferences;
18
+ else {
19
+ try { saved = JSON.parse(fs.readFileSync(defaultPath, 'utf8')); }
20
+ catch (error) { if (error.code !== 'ENOENT') throw error; }
21
+ }
22
+ return Object.assign({ theme: saved && saved.theme || null, mode: saved && saved.mode || null, configured: !!saved }, saved && saved.lightTheme ? { lightTheme: saved.lightTheme } : {}, saved && saved.darkTheme ? { darkTheme: saved.darkTheme } : {});
23
+ }
24
+ function save(user, values) {
25
+ if (ctx.users.isMultiUser()) {
26
+ var data = ctx.users.loadUsers();
27
+ var current = data.users.find(function (entry) { return entry.id === user.id; });
28
+ if (!current) throw new Error('Account no longer available.');
29
+ current.appearancePreferences = values;
30
+ ctx.users.saveUsers(data);
31
+ } else {
32
+ fs.mkdirSync(ctx.CONFIG_DIR, { recursive: true });
33
+ fs.writeFileSync(defaultPath + '.tmp', JSON.stringify(values), { mode: 0o600 });
34
+ fs.renameSync(defaultPath + '.tmp', defaultPath);
35
+ }
36
+ }
37
+ function handleRequest(req, res, url) {
38
+ if (url !== '/api/user/appearance') return false;
39
+ var user = identity(req);
40
+ if (!user) { reply(res, 401, { error: 'Sign in to manage appearance.' }); return true; }
41
+ if (req.method === 'GET') {
42
+ try { reply(res, 200, read(user)); } catch (error) { reply(res, 500, { error: 'Could not read appearance preferences.' }); }
43
+ return true;
44
+ }
45
+ if (req.method !== 'PUT') { reply(res, 405, { error: 'Method not allowed.' }); return true; }
46
+ if (req.headers['x-clay-settings'] !== '1' || (req.headers['sec-fetch-site'] && ['same-origin', 'none'].indexOf(req.headers['sec-fetch-site']) < 0)) {
47
+ reply(res, 403, { error: 'Save preferences from Clay.' }); return true;
48
+ }
49
+ var body = ''; var tooLarge = false;
50
+ req.setEncoding('utf8');
51
+ req.on('data', function (chunk) { body += chunk; if (body.length > 4096 && !tooLarge) { tooLarge = true; reply(res, 413, { error: 'Request too large.' }); } });
52
+ req.on('end', function () {
53
+ if (tooLarge) return;
54
+ user = identity(req);
55
+ if (!user) { reply(res, 401, { error: 'Sign in again.' }); return; }
56
+ var values;
57
+ try { values = JSON.parse(body); } catch (error) { reply(res, 400, { error: 'Invalid JSON.' }); return; }
58
+ if (!values || (values.theme !== null && (typeof values.theme !== 'string' || !/^[a-zA-Z0-9_-]{1,120}$/.test(values.theme))) || [null, 'light', 'dark'].indexOf(values.mode) < 0) { reply(res, 400, { error: 'Invalid appearance preference.' }); return; }
59
+ if (['lightTheme', 'darkTheme'].some(function (field) { return values[field] !== undefined && (typeof values[field] !== 'string' || !/^[a-zA-Z0-9_-]{1,120}$/.test(values[field])); })) { reply(res, 400, { error: 'Invalid theme pair.' }); return; }
60
+ var previous;
61
+ try { previous = read(user); } catch (error) { reply(res, 500, { error: 'Could not read appearance preferences.' }); return; }
62
+ values = Object.assign({}, previous.lightTheme ? { lightTheme: previous.lightTheme } : {}, previous.darkTheme ? { darkTheme: previous.darkTheme } : {}, values);
63
+ values = Object.assign({ theme: values.theme, mode: values.mode }, values.lightTheme ? { lightTheme: values.lightTheme } : {}, values.darkTheme ? { darkTheme: values.darkTheme } : {});
64
+ try { save(user, values); reply(res, 200, values); }
65
+ catch (error) { reply(res, 500, { error: 'Could not save appearance preferences. Your draft is retained.' }); }
66
+ });
67
+ return true;
68
+ }
69
+ return { handleRequest: handleRequest };
70
+ }
71
+ module.exports = { attachAppearanceSettings: attachAppearanceSettings };
@@ -297,7 +297,7 @@ function transformEvent(event, mateId, session, requestId, stableSessionId) {
297
297
  }
298
298
  if (event.type === "debate_proposal_resolved" && event.proposalId) return Object.assign({ type: "home_debate_proposal_resolved", proposalId: event.proposalId, action: event.action || "cancel", error: event.error || "" }, metadata);
299
299
  if (session && session.mateCreationMode === true && event.type === "mate_creation_proposal" && event.proposal) return Object.assign({ type: "home_mate_creation_proposal", proposal: event.proposal }, metadata);
300
- if (session && session.mateCreationMode === true && event.type === "mate_creation_proposal_resolved" && event.proposalId) return Object.assign({ type: "home_mate_creation_proposal_resolved", proposalId: event.proposalId, action: event.action || "cancel", mateId: event.mateId || null, mateName: event.mateName || null, error: event.error || "" }, metadata);
300
+ if (session && session.mateCreationMode === true && event.type === "mate_creation_proposal_resolved" && event.proposalId) return Object.assign({ type: "home_mate_creation_proposal_resolved", proposalId: event.proposalId, action: event.action || "cancel", createdMateId: event.mateId || null, mateName: event.mateName || null, error: event.error || "" }, metadata);
301
301
  if (session && session.debateSetupMode === true && event.type === "tool_executing" && event.name === "AskUserQuestion" && event.id && event.input) {
302
302
  return Object.assign({ type: "home_debate_question", toolId: event.id, questions: homeDebateQuestions(event.input) }, metadata);
303
303
  }
@@ -11,6 +11,7 @@ var homeChatEvents = require("./server-home-chat-events");
11
11
  var homeCapsuleCreation = require("./server-home-capsule-creation");
12
12
  var sessionTitlePolicy = require("./session-title-policy");
13
13
  var sessionProvenance = require("./session-provenance");
14
+ var userPresence = require("./user-presence");
14
15
  var historyToHomeChat = homeChatEvents.historyToHomeChat;
15
16
  var transformEvent = homeChatEvents.transformEvent;
16
17
  function attachHomeChat(deps) {
@@ -232,6 +233,7 @@ function attachHomeChat(deps) {
232
233
  var payload = {
233
234
  type: "home_mate_history",
234
235
  mateId: found.mate.id,
236
+ localId: session.localId,
235
237
  sessionId: sessionReference(session),
236
238
  requestId: requestId || null,
237
239
  model: session.model || null,
@@ -279,13 +281,18 @@ function attachHomeChat(deps) {
279
281
  sendMessage(ws, { type: "home_mate_sessions_state", mateId: found.mate.id, sessions: listHomeSessions(found, userId) });
280
282
  }
281
283
 
284
+ function persistActiveSession(ws, found, userId, session) {
285
+ if (!ws || ws._clayPane || !found || !session) return;
286
+ userPresence.setPresence(found.slug, userId || "_default", userPresence.sessionIdForPersistence(session), null);
287
+ }
288
+
282
289
  var homeDebate = attachHomeDebatePlanning({
283
290
  findMateProject: findMateProject, resolveHomeSession: resolveHomeSession,
284
291
  sessionReference: sessionReference, setupTap: setupTap, sendHistory: sendHistory,
285
292
  sendSessionList: sendSessionList, sendError: sendError, sendModelError: sendModelError,
286
- homeModels: homeModels, isActorLive: isActorLive,
293
+ homeModels: homeModels, isActorLive: isActorLive, persistActiveSession: persistActiveSession,
287
294
  });
288
- var homeMateCreation = attachHomeMateCreation({ findMateProject: findMateProject, resolveHomeSession: resolveHomeSession, sessionReference: sessionReference, setupTap: setupTap, sendHistory: sendHistory, sendSessionList: sendSessionList, sendError: sendError, sendModelError: sendModelError, homeModels: homeModels, isActorLive: isActorLive });
295
+ var homeMateCreation = attachHomeMateCreation({ findMateProject: findMateProject, resolveHomeSession: resolveHomeSession, sessionReference: sessionReference, setupTap: setupTap, sendHistory: sendHistory, sendSessionList: sendSessionList, sendError: sendError, sendModelError: sendModelError, homeModels: homeModels, isActorLive: isActorLive, persistActiveSession: persistActiveSession });
289
296
  var homeClayEntry = attachHomeClayEntry({ findMateProject: findMateProject, ownsSession: ownsSession, sessionReference: sessionReference, setupSearchTap: setupSearchTap, sendHistory: sendHistory, sendSessionList: sendSessionList, sendError: sendError, sendModelError: sendModelError, homeModels: homeModels, isActorLive: isActorLive });
290
297
 
291
298
  async function openExactSession(ws, found, userId, selected, msg) {
@@ -591,6 +598,21 @@ function attachHomeChat(deps) {
591
598
  teardownTap(ws);
592
599
  teardownTap(ws, "_searchClayTap");
593
600
  }
594
- return { handleMessage: handleMessage, handleDisconnection: handleDisconnection, findMateProject: findMateProject };
601
+ function resolveMateSessionDefaults(ws, userId, mateId) {
602
+ var found = findMateProject(userId, mateId, false);
603
+ if (!found) return Promise.reject(new Error("Mate is no longer available."));
604
+ try {
605
+ found = assertFreshFound(ws, found, userId);
606
+ } catch (error) {
607
+ return Promise.reject(error);
608
+ }
609
+ return homeModels.resolveMateModel(ws, found, userId, { useDefault: true });
610
+ }
611
+ return {
612
+ handleMessage: handleMessage,
613
+ handleDisconnection: handleDisconnection,
614
+ findMateProject: findMateProject,
615
+ resolveMateSessionDefaults: resolveMateSessionDefaults,
616
+ };
595
617
  }
596
618
  module.exports = { attachHomeChat: attachHomeChat };
@@ -91,14 +91,17 @@ function attachHomeDebatePlanning(ctx) {
91
91
  if (!liveFound || liveFound.ctx !== found.ctx || !liveFound.mate || liveFound.mate.builtinKey !== "clay") throw new Error("Clay is no longer available.");
92
92
  var liveManager = liveFound.ctx.getSessionManager();
93
93
  if (!liveManager || liveManager !== manager || !liveFound.ctx.sdk) throw new Error("Clay conversation is no longer available.");
94
- var session = manager.createSession({ ownerId: userId, vendor: resolved.vendor, model: resolved.model, effort: resolved.effort || null }, null);
94
+ var session = manager.createSessionRaw({ ownerId: userId, vendor: resolved.vendor, model: resolved.model, effort: resolved.effort || null });
95
95
  session.title = PLANNING_TITLE;
96
96
  session.titleManuallySet = true;
97
97
  session.debateSetupMode = true;
98
98
  session.homeDebatePlanning = true;
99
99
  session.homeDebatePhase = "planning";
100
100
  session.homeDebateInitialTopic = normalizeInitialTopic(msg.topic);
101
+ if (typeof manager.saveSessionFile === "function" && manager.saveSessionFile(session) === false) throw new Error("The debate setup could not be saved. Try again.");
101
102
  if (requestId) ws._homeDebatePlanningRequests[requestId] = session.localId;
103
+ manager.switchSession(session.localId, ws);
104
+ if (typeof ctx.persistActiveSession === "function") ctx.persistActiveSession(ws, liveFound, userId, session);
102
105
  ctx.setupTap(ws, liveFound, session.localId, requestId);
103
106
  ctx.sendHistory(ws, liveFound, session, requestId);
104
107
  ctx.sendSessionList(ws, liveFound, userId);
@@ -105,6 +105,8 @@ function attachHomeDebates(deps) {
105
105
  var lastActivity = typeof session.lastActivity === "number" ? session.lastActivity : createdAt;
106
106
  debates.push({
107
107
  mateId: found.mate.id,
108
+ projectSlug: found.slug,
109
+ localId: session.localId,
108
110
  sessionId: sessionReference(session),
109
111
  title: details.topic || "Debate planning",
110
112
  topic: details.topic,
@@ -164,14 +164,17 @@ function attachHomeMateCreation(ctx) {
164
164
  if (!liveFound || liveFound.ctx !== found.ctx || !liveFound.mate || liveFound.mate.builtinKey !== "clay") throw new Error("Clay is no longer available.");
165
165
  var liveManager = liveFound.ctx.getSessionManager();
166
166
  if (!liveManager || liveManager !== manager || !liveFound.ctx.sdk) throw new Error("Clay conversation is no longer available.");
167
- var session = manager.createSession({ ownerId: userId, vendor: resolved.vendor, model: resolved.model, effort: resolved.effort || null }, null);
167
+ var session = manager.createSessionRaw({ ownerId: userId, vendor: resolved.vendor, model: resolved.model, effort: resolved.effort || null });
168
168
  session.title = TITLE;
169
169
  session.titleManuallySet = true;
170
170
  session.mateCreationMode = true;
171
171
  session.homeMateCreationPhase = "interview";
172
+ if (typeof manager.saveSessionFile === "function" && manager.saveSessionFile(session) === false) throw new Error("The Mate interview could not be saved. Try again.");
172
173
  if (requestId) ws._homeMateCreationRequests[requestId] = session.localId;
173
174
  manager.sendAndRecord(session, { type: MARKER, internal: true });
174
175
  manager.sendAndRecord(session, { type: "tool_executing", name: "AskUserQuestion", id: firstQuestionId(session), input: { questions: [FIRST_QUESTION] } });
176
+ manager.switchSession(session.localId, ws);
177
+ if (typeof ctx.persistActiveSession === "function") ctx.persistActiveSession(ws, liveFound, userId, session);
175
178
  ctx.setupTap(ws, liveFound, session.localId, requestId);
176
179
  ctx.sendHistory(ws, liveFound, session, requestId);
177
180
  ctx.sendSessionList(ws, liveFound, userId);
@@ -10,39 +10,45 @@ function attachLinear(ctx) {
10
10
  }
11
11
  function json(res, status, body) { res.writeHead(status, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' }); res.end(JSON.stringify(body)); }
12
12
  function handleRequest(req, res, url) {
13
- if (url !== '/api/linear/connection') return false;
13
+ var match = /^\/api\/linear\/connections(?:\/([a-zA-Z0-9_-]{1,64}))?$/.exec(url);
14
+ if (url !== '/api/linear/connection' && !match) return false;
15
+ var connectionId = match && match[1];
16
+ if (match && !connectionId && req.method !== 'GET') { json(res, 405, { error: 'Method not allowed.' }); return true; }
14
17
  var id = owner(req);
15
18
  if (!id) { json(res, 401, { error: 'Sign in to connect Linear.' }); return true; }
16
19
  var origin = req.headers.origin;
17
20
  try { if (req.headers['sec-fetch-site'] === 'cross-site' || (origin && new URL(origin).host !== req.headers.host)) throw new Error(); }
18
21
  catch (error) { json(res, 403, { error: 'Invalid request origin.' }); return true; }
19
22
  if (req.method === 'GET') {
20
- try { json(res, 200, storage.view(id)); } catch (error) { json(res, 500, { error: 'Could not read Linear connection.' }); }
23
+ try { json(res, 200, match && !connectionId ? { connections: storage.list(id) } : storage.view(id, connectionId)); } catch (error) { json(res, 500, { error: 'Could not read Linear connection.' }); }
21
24
  return true;
22
25
  }
23
26
  if (req.method !== 'PUT' && req.method !== 'DELETE') { json(res, 405, { error: 'Method not allowed.' }); return true; }
24
27
  // A disconnect or newer save invalidates an older validation in flight.
25
- var operation = {}; pending.set(id, operation);
28
+ var operation = {}, pendingId = id + ':' + (connectionId || 'default'); pending.set(pendingId, operation);
26
29
  var chunks = [], size = 0;
27
30
  req.on('data', function (chunk) { size += chunk.length; if (size <= 8192) chunks.push(chunk); });
28
31
  req.on('end', async function () {
29
32
  try {
30
33
  if (size > 8192) throw new Error('Request is too large.');
31
- var token = null, workspace = null;
34
+ var token = null, workspace = null, name;
32
35
  if (req.method === 'PUT') {
33
36
  var body = JSON.parse(Buffer.concat(chunks).toString('utf8'));
37
+ name = body.name;
38
+ if (match && (typeof name !== 'string' || !name.trim() || name.trim().length > 80)) throw new Error('Invalid connection name.');
39
+ if (match && !body.key) body.key = storage.key(id, connectionId);
34
40
  if (typeof body.key !== 'string' || !body.key.trim() || body.key.length > 4096 || /[\r\n]/.test(body.key)) throw new Error('Enter a valid Linear API key.');
35
41
  token = body.key.trim();
36
42
  var data = await (ctx.query || api.query)(token, 'query ClayConnection { organization { id name urlKey } }');
37
43
  if (!data.organization || !data.organization.id) throw new Error('Linear workspace is unavailable.');
38
44
  workspace = { id: data.organization.id, name: data.organization.name, slug: data.organization.urlKey };
39
45
  }
40
- if (pending.get(id) !== operation || owner(req) !== id || res.destroyed) throw new Error('Connection changed. Please try again.');
41
- json(res, 200, storage.save(id, token, workspace));
46
+ if (pending.get(pendingId) !== operation || owner(req) !== id || res.destroyed) throw new Error('Connection changed. Please try again.');
47
+ json(res, 200, storage.save(id, token, workspace, connectionId, name));
42
48
  } catch (error) { if (!res.destroyed) json(res, 400, { error: 'Could not update Linear connection. Check your API key and try again.' }); }
43
- finally { if (pending.get(id) === operation) pending.delete(id); }
49
+ finally { if (pending.get(pendingId) === operation) pending.delete(pendingId); }
44
50
  });
45
- req.on('error', function () { if (pending.get(id) === operation) pending.delete(id); });
51
+ req.on('error', function () { if (pending.get(pendingId) === operation) pending.delete(pendingId); });
46
52
  return true;
47
53
  }
48
54
  return { handleRequest: handleRequest, storage: storage };
@@ -0,0 +1,44 @@
1
+ // Owner-scoped identity editing; generated instruction sections stay intact.
2
+ var fs = require('fs');
3
+ var path = require('path');
4
+ var crypto = require('crypto');
5
+
6
+ function revision(content) { return crypto.createHash('sha256').update(content).digest('hex'); }
7
+ function handleMateInstructions(mates, ws, msg, userId) {
8
+ if (msg.type !== 'mate_instructions_get' && msg.type !== 'mate_instructions_set') return false;
9
+ var reply = {type: 'mate_instructions_result', mateId: msg.mateId, requestId: msg.requestId, operation: msg.type === 'mate_instructions_set' ? 'save' : 'read', ok: false};
10
+ try {
11
+ var ctx = mates.buildMateCtx(userId);
12
+ if (typeof msg.mateId !== 'string' || !mates.isMateIdFormat(msg.mateId) || !mates.getMate(ctx, msg.mateId)) throw new Error('Mate not found.');
13
+ var directory = mates.getMateDir(ctx, msg.mateId);
14
+ var file = path.join(directory, 'CLAUDE.md');
15
+ if (!fs.lstatSync(file).isFile()) throw new Error('Mate instructions are unavailable.');
16
+ var content = fs.readFileSync(file, 'utf8');
17
+ var identity = mates.extractIdentity(content);
18
+ if (msg.type === 'mate_instructions_set') {
19
+ if (msg.revision !== revision(identity)) {
20
+ reply.conflict = true;
21
+ throw new Error('The prompt changed elsewhere. Reload the saved prompt before applying your edits.');
22
+ }
23
+ if (typeof msg.content !== 'string' || msg.content.trim().length < 50 || Buffer.byteLength(msg.content, 'utf8') > 131072) throw new Error('Use a prompt between 50 characters and 128 KB.');
24
+ var next = msg.content.trimEnd();
25
+ if (mates.extractIdentity(next) !== next) throw new Error('System-managed instruction sections cannot be included in the editable prompt.');
26
+ var temporary = file + '.settings-' + crypto.randomBytes(8).toString('hex');
27
+ try {
28
+ fs.writeFileSync(temporary, next + content.slice(identity.length), {encoding: 'utf8', mode: fs.statSync(file).mode & 511, flag: 'wx'});
29
+ fs.renameSync(temporary, file);
30
+ } finally { if (fs.existsSync(temporary)) fs.unlinkSync(temporary); }
31
+ // The canonical save succeeds independently of best-effort audit copies.
32
+ try { mates.backupIdentity(directory, next); mates.logIdentityChange(directory, 'user_settings', next, identity); } catch (error) {}
33
+ identity = next;
34
+ }
35
+ reply.ok = true;
36
+ reply.content = identity;
37
+ reply.revision = revision(identity);
38
+ } catch (error) {
39
+ reply.error = error.code === 'ENOENT' ? 'Mate instructions are unavailable.' : error.message;
40
+ }
41
+ ws.send(JSON.stringify(reply));
42
+ return true;
43
+ }
44
+ module.exports = {handleMateInstructions: handleMateInstructions};
@@ -1,5 +1,6 @@
1
1
  var fs = require("fs");
2
2
  var path = require("path");
3
+ var handleMateInstructions = require('./server-mate-instructions').handleMateInstructions;
3
4
  var readyCreation = require("./mate-ready-creation");
4
5
 
5
6
  function attachMates(ctx) {
@@ -106,6 +107,8 @@ function attachMates(ctx) {
106
107
  userId = "default";
107
108
  }
108
109
 
110
+ if (handleMateInstructions(mates, ws, msg, userId)) return true;
111
+
109
112
  if (msg.type === "mate_create") {
110
113
  if (!msg.seedData) return true;
111
114
  try {
@@ -229,6 +232,7 @@ function attachMates(ctx) {
229
232
  projects.forEach(function (pCtx) {
230
233
  pCtx.forEachClient(function (otherWs) {
231
234
  if (otherWs === ws) return;
235
+ if (users.isMultiUser() && (!otherWs._clayUser || otherWs._clayUser.id !== userId)) return;
232
236
  if (otherWs.readyState !== 1) return;
233
237
  otherWs.send(JSON.stringify({ type: "mate_deleted", mateId: msg.mateId, availableBuiltins: availableBuiltins3 }));
234
238
  });
@@ -295,17 +299,22 @@ function attachMates(ctx) {
295
299
  if (msg.type === "mate_update") {
296
300
  if (!msg.mateId || !msg.updates) return true;
297
301
  if (Object.prototype.hasOwnProperty.call(msg.updates, "model")) {
298
- ws.send(JSON.stringify({ type: "mate_error", error: "Use the Mate model selector to change this setting." }));
302
+ ws.send(JSON.stringify({ type: "mate_error", mateId: msg.mateId, error: "Use the Mate model selector to change this setting." }));
299
303
  return true;
300
304
  }
301
305
  var mateCtx7 = mates.buildMateCtx(userId);
302
306
  var updated = mates.updateMate(mateCtx7, msg.mateId, msg.updates);
307
+ if (updated && updated.error) {
308
+ ws.send(JSON.stringify({ type: "mate_error", mateId: msg.mateId, requestId: msg.requestId, error: updated.error }));
309
+ return true;
310
+ }
303
311
  if (updated) {
304
- ws.send(JSON.stringify({ type: "mate_updated", mate: updated }));
312
+ ws.send(JSON.stringify({ type: "mate_updated", mate: updated, requestId: msg.requestId }));
305
313
  // Broadcast update
306
314
  projects.forEach(function (pCtx) {
307
315
  pCtx.forEachClient(function (otherWs) {
308
316
  if (otherWs === ws) return;
317
+ if (users.isMultiUser() && (!otherWs._clayUser || otherWs._clayUser.id !== userId)) return;
309
318
  if (otherWs.readyState !== 1) return;
310
319
  otherWs.send(JSON.stringify({ type: "mate_updated", mate: updated }));
311
320
  });
@@ -313,7 +322,7 @@ function attachMates(ctx) {
313
322
  // Re-enforce team sections across all mate projects so roster stays current
314
323
  refreshTeamSections(mateCtx7);
315
324
  } else {
316
- ws.send(JSON.stringify({ type: "mate_error", error: "Mate not found" }));
325
+ ws.send(JSON.stringify({ type: "mate_error", mateId: msg.mateId, requestId: msg.requestId, error: "Mate not found" }));
317
326
  }
318
327
  return true;
319
328
  }
@@ -0,0 +1,67 @@
1
+ var fs = require('fs');
2
+ var path = require('path');
3
+
4
+ function attachNotificationSettings(ctx) {
5
+ var defaultPath = path.join(ctx.CONFIG_DIR, 'notification-preferences.json');
6
+ function reply(res, code, data) {
7
+ res.writeHead(code, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
8
+ res.end(JSON.stringify(data));
9
+ }
10
+ function identity(req) {
11
+ if (!ctx.isRequestAuthed(req)) return null;
12
+ if (!ctx.users.isMultiUser()) return { id: 'default' };
13
+ return ctx.getMultiUserFromReq(req);
14
+ }
15
+ function read(user) {
16
+ var saved;
17
+ if (ctx.users.isMultiUser()) saved = user.notificationPreferences;
18
+ else {
19
+ try { saved = JSON.parse(fs.readFileSync(defaultPath, 'utf8')); }
20
+ catch (error) { if (error.code !== 'ENOENT') throw error; }
21
+ }
22
+ return { alert: !saved || saved.alert !== false, sound: !saved || saved.sound !== false, configured: !!saved };
23
+ }
24
+ function save(user, values) {
25
+ if (ctx.users.isMultiUser()) {
26
+ var data = ctx.users.loadUsers();
27
+ var current = data.users.find(function (entry) { return entry.id === user.id; });
28
+ if (!current) throw new Error('Account no longer available.');
29
+ current.notificationPreferences = values;
30
+ ctx.users.saveUsers(data);
31
+ } else {
32
+ fs.mkdirSync(ctx.CONFIG_DIR, { recursive: true });
33
+ fs.writeFileSync(defaultPath + '.tmp', JSON.stringify(values), { mode: 0o600 });
34
+ fs.renameSync(defaultPath + '.tmp', defaultPath);
35
+ }
36
+ }
37
+ function handleRequest(req, res, url) {
38
+ if (url !== '/api/user/notifications') return false;
39
+ var user = identity(req);
40
+ if (!user) { reply(res, 401, { error: 'Sign in to manage notifications.' }); return true; }
41
+ if (req.method === 'GET') {
42
+ try { reply(res, 200, read(user)); } catch (error) { reply(res, 500, { error: 'Could not read notification preferences.' }); }
43
+ return true;
44
+ }
45
+ if (req.method !== 'PUT') { reply(res, 405, { error: 'Method not allowed.' }); return true; }
46
+ if (req.headers['x-clay-settings'] !== '1' || (req.headers['sec-fetch-site'] && ['same-origin', 'none'].indexOf(req.headers['sec-fetch-site']) < 0)) {
47
+ reply(res, 403, { error: 'Save preferences from Clay.' }); return true;
48
+ }
49
+ var body = ''; var tooLarge = false;
50
+ req.setEncoding('utf8');
51
+ req.on('data', function (chunk) { body += chunk; if (body.length > 4096 && !tooLarge) { tooLarge = true; reply(res, 413, { error: 'Request too large.' }); } });
52
+ req.on('end', function () {
53
+ if (tooLarge) return;
54
+ user = identity(req);
55
+ if (!user) { reply(res, 401, { error: 'Sign in again.' }); return; }
56
+ var values;
57
+ try { values = JSON.parse(body); } catch (error) { reply(res, 400, { error: 'Invalid JSON.' }); return; }
58
+ if (!values || typeof values.alert !== 'boolean' || typeof values.sound !== 'boolean') { reply(res, 400, { error: 'Alert and sound must be booleans.' }); return; }
59
+ values = { alert: values.alert, sound: values.sound };
60
+ try { save(user, values); reply(res, 200, values); }
61
+ catch (error) { reply(res, 500, { error: 'Could not save notification preferences. Your draft is retained.' }); }
62
+ });
63
+ return true;
64
+ }
65
+ return { handleRequest: handleRequest };
66
+ }
67
+ module.exports = { attachNotificationSettings: attachNotificationSettings };
@@ -11,7 +11,7 @@ function attachPalette(ctx) {
11
11
  if (!user) return { visibility: "public" };
12
12
  if (typeof canUserAccessSlug !== "function" || !canUserAccessSlug(user.id, slug)) return null;
13
13
  if (status && status.isMate) {
14
- return { visibility: "private", ownerId: status.projectOwnerId || null, allowedUsers: [] };
14
+ return { isMate: true, visibility: "private", ownerId: status.projectOwnerId || null, allowedUsers: [] };
15
15
  }
16
16
  var access = onGetProjectAccess ? onGetProjectAccess(slug) : null;
17
17
  return access && !access.error ? access : null;
@@ -1,6 +1,7 @@
1
1
  var fs = require("fs");
2
2
  var path = require("path");
3
3
  var { attachExperimentalSettings } = require("./server-experimental-settings");
4
+ var dicebearAvatar = require("./dicebear-avatar");
4
5
 
5
6
  function attachSettings(ctx) {
6
7
  var users = ctx.users;
@@ -11,10 +12,36 @@ function attachSettings(ctx) {
11
12
  var CONFIG_DIR = ctx.CONFIG_DIR;
12
13
 
13
14
  var profilePath = path.join(CONFIG_DIR, "profile.json");
15
+ var appearanceSettings = require('./server-appearance-settings').attachAppearanceSettings(ctx);
16
+ var notificationSettings = require('./server-notification-settings').attachNotificationSettings(ctx);
14
17
  var experimentalSettings = attachExperimentalSettings(ctx);
15
18
 
16
19
  function handleRequest(req, res, fullUrl) {
20
+ if (appearanceSettings.handleRequest(req, res, fullUrl)) return true;
21
+ if (notificationSettings.handleRequest(req, res, fullUrl)) return true;
17
22
  if (experimentalSettings.handleRequest(req, res, fullUrl)) return true;
23
+ // Render historical avatar styles locally so profile seeds never leave Clay.
24
+ if (req.method === "GET" && fullUrl === "/api/generated-avatar") {
25
+ var avatarParams = new URL(req.url || fullUrl, "http://localhost").searchParams;
26
+ var avatarStyle = avatarParams.get("style") || "";
27
+ if (!dicebearAvatar.isSupportedStyle(avatarStyle)) {
28
+ res.writeHead(404, { "Content-Type": "application/json" });
29
+ res.end('{"error":"Unknown avatar style"}');
30
+ return true;
31
+ }
32
+ dicebearAvatar.renderAvatar(avatarStyle, avatarParams.get("seed") || "", avatarParams.get("size")).then(function (svg) {
33
+ res.writeHead(200, {
34
+ "Content-Type": "image/svg+xml; charset=utf-8",
35
+ "Cache-Control": "private, max-age=31536000, immutable",
36
+ "X-Content-Type-Options": "nosniff",
37
+ });
38
+ res.end(svg);
39
+ }).catch(function () {
40
+ res.writeHead(500, { "Content-Type": "application/json" });
41
+ res.end('{"error":"Avatar generation failed"}');
42
+ });
43
+ return true;
44
+ }
18
45
  // GET /api/profile
19
46
  if (req.method === "GET" && fullUrl === "/api/profile") {
20
47
  if (users.isMultiUser()) {
@@ -31,6 +58,7 @@ function attachSettings(ctx) {
31
58
  profile.pinEnabled = !!mu.pinHash;
32
59
  profile.autoContinueOnRateLimit = !!mu.autoContinueOnRateLimit;
33
60
  profile.chatLayout = mu.chatLayout || "channel";
61
+ profile.projectMateFilterMode = users.getProjectMateFilterMode(mu.id);
34
62
  profile.mateOnboardingShown = !!mu.mateOnboardingShown;
35
63
  profile.matesEnabled = mu.matesEnabled === true;
36
64
  profile.capsulesEnabled = users.getCapsulesEnabled(mu.id);
@@ -55,6 +83,7 @@ function attachSettings(ctx) {
55
83
  var dc = opts.onGetDaemonConfig();
56
84
  profile.autoContinueOnRateLimit = !!dc.autoContinueOnRateLimit;
57
85
  profile.chatLayout = dc.chatLayout || "channel";
86
+ profile.projectMateFilterMode = users.getProjectMateFilterMode(null);
58
87
  profile.mateOnboardingShown = !!dc.mateOnboardingShown;
59
88
  profile.matesEnabled = dc.matesEnabled === true;
60
89
  profile.capsulesEnabled = dc.capsulesEnabled === true;
@@ -266,6 +295,11 @@ function attachSettings(ctx) {
266
295
  res.end('{"error":"Mate not found"}');
267
296
  return;
268
297
  }
298
+ if (mate.profile && mate.profile.avatarLocked) {
299
+ res.writeHead(403, { "Content-Type": "application/json" });
300
+ res.end('{"error":"This Mate avatar is managed by Clay"}');
301
+ return;
302
+ }
269
303
  var ext = ct.split("/")[1] === "jpeg" ? "jpg" : ct.split("/")[1];
270
304
  var avatarDir = path.join(CONFIG_DIR, "mate-avatars");
271
305
  fs.mkdirSync(avatarDir, { recursive: true });
@@ -297,6 +331,10 @@ function attachSettings(ctx) {
297
331
  // Serve custom mate avatar image
298
332
  if (req.method === "GET" && fullUrl.startsWith("/api/mate-avatar/")) {
299
333
  var mateAvatarId = fullUrl.split("/api/mate-avatar/")[1].split("?")[0];
334
+ var avatarUser = users.isMultiUser() ? getMultiUserFromReq(req) : null;
335
+ if ((users.isMultiUser() && !avatarUser) || !mates.getMate(mates.buildMateCtx(avatarUser ? avatarUser.id : "default"), mateAvatarId)) {
336
+ res.writeHead(404); res.end(); return true;
337
+ }
300
338
  var mateAvatarDir = path.join(CONFIG_DIR, "mate-avatars");
301
339
  try {
302
340
  var files = fs.readdirSync(mateAvatarDir);
@@ -312,7 +350,7 @@ function attachSettings(ctx) {
312
350
  var ctMap = { jpg: "image/jpeg", png: "image/png", gif: "image/gif", webp: "image/webp" };
313
351
  res.writeHead(200, {
314
352
  "Content-Type": ctMap[ext] || "application/octet-stream",
315
- "Cache-Control": "public, max-age=31536000, immutable",
353
+ "Cache-Control": "private, no-store",
316
354
  });
317
355
  res.end(fs.readFileSync(path.join(mateAvatarDir, match)));
318
356
  return true;
@@ -528,6 +566,36 @@ function attachSettings(ctx) {
528
566
  return true;
529
567
  }
530
568
 
569
+ // PUT /api/user/project-mate-filter
570
+ if (req.method === "PUT" && fullUrl === "/api/user/project-mate-filter") {
571
+ var isMultiUserFilter = users.isMultiUser();
572
+ var muFilter = getMultiUserFromReq(req);
573
+ if (isMultiUserFilter && !muFilter) {
574
+ res.writeHead(401, { "Content-Type": "application/json" });
575
+ res.end('{"error":"unauthorized"}');
576
+ return true;
577
+ }
578
+ var bodyFilter = "";
579
+ req.on("data", function (chunk) { bodyFilter += chunk; });
580
+ req.on("end", function () {
581
+ try {
582
+ var dataFilter = JSON.parse(bodyFilter);
583
+ var resultFilter = users.setProjectMateFilterMode(muFilter ? muFilter.id : null, dataFilter.mode);
584
+ if (resultFilter.error) {
585
+ res.writeHead(400, { "Content-Type": "application/json" });
586
+ res.end(JSON.stringify({ error: resultFilter.error }));
587
+ return;
588
+ }
589
+ res.writeHead(200, { "Content-Type": "application/json" });
590
+ res.end(JSON.stringify(resultFilter));
591
+ } catch (e) {
592
+ res.writeHead(400, { "Content-Type": "application/json" });
593
+ res.end('{"error":"Invalid request"}');
594
+ }
595
+ });
596
+ return true;
597
+ }
598
+
531
599
  // PUT /api/user/terminal-font
532
600
  // Body: { family?: string, size?: number }
533
601
  // Per-user terminal font preferences shared across every xterm in