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.
- package/LICENSES/README.md +4 -0
- package/LICENSES/dicebear-avatars.md +16 -0
- package/bin/cli.js +9 -91
- package/lib/daemon-project-access.js +7 -1
- package/lib/daemon-settings.js +11 -0
- package/lib/daemon.js +10 -37
- package/lib/dicebear-avatar.js +62 -0
- package/lib/knowledge-db-service.js +175 -0
- package/lib/knowledge-db-validation.js +0 -0
- package/lib/knowledge-document-index.js +49 -0
- package/lib/knowledge-draft-store.js +56 -0
- package/lib/knowledge-file-store.js +199 -0
- package/lib/knowledge-markdown.js +128 -0
- package/lib/linear-store.js +35 -8
- package/lib/mate-knowledge-mcp-server.js +29 -6
- package/lib/mate-knowledge-service.js +23 -2
- package/lib/mate-knowledge-sync.js +36 -21
- package/lib/mate-workspace-color.js +4 -0
- package/lib/mates.js +18 -0
- package/lib/os-users.js +5 -16
- package/lib/project-connection.js +36 -9
- package/lib/project-debate-proposal.js +6 -0
- package/lib/project-debate-workspace.js +43 -0
- package/lib/project-debate.js +1 -0
- package/lib/project-filesystem.js +21 -39
- package/lib/project-knowledge.js +315 -136
- package/lib/project-mate-creation-proposal.js +1 -0
- package/lib/project-mate-interaction.js +2 -2
- package/lib/project-server-settings.js +31 -0
- package/lib/project-sessions.js +92 -182
- package/lib/project-user-knowledge.js +60 -0
- package/lib/project.js +59 -18
- package/lib/public/app.js +79 -71
- package/lib/public/clay-studio-wordmark-bold.svg +20 -0
- package/lib/public/css/clay-workspace.css +105 -0
- package/lib/public/css/debates-workbench.css +47 -0
- package/lib/public/css/home-debate-live.css +26 -0
- package/lib/public/css/home-hub.css +3 -1
- package/lib/public/css/home-mate-model-picker.css +15 -131
- package/lib/public/css/home-mate-settings.css +168 -16
- package/lib/public/css/icon-strip.css +249 -32
- package/lib/public/css/input.css +3 -1
- package/lib/public/css/mate-conversation.css +176 -0
- package/lib/public/css/mate-knowledge-workbench.css +118 -0
- package/lib/public/css/mate-workspace-color.css +16 -0
- package/lib/public/css/mates.css +28 -28
- package/lib/public/css/messages.css +1 -114
- package/lib/public/css/mobile-nav.css +16 -16
- package/lib/public/css/overlays.css +8 -0
- package/lib/public/css/project-logs.css +7 -0
- package/lib/public/css/project-mate-creation.css +105 -0
- package/lib/public/css/project-settings-modal.css +56 -0
- package/lib/public/css/right-workbench.css +3 -3
- package/lib/public/css/server-settings-system.css +125 -0
- package/lib/public/css/session-folders.css +80 -21
- 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 +2 -0
- package/lib/public/css/sidebar.css +183 -107
- package/lib/public/css/title-bar.css +156 -1
- package/lib/public/css/worker-delegation.css +47 -0
- package/lib/public/css/worker-proposal.css +72 -150
- package/lib/public/css/you.css +74 -0
- package/lib/public/index.html +203 -547
- package/lib/public/modules/account-island.js +232 -0
- 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-cursors.js +19 -43
- package/lib/public/modules/app-debate-ui.js +4 -3
- package/lib/public/modules/app-dm.js +0 -2
- package/lib/public/modules/app-header.js +106 -0
- package/lib/public/modules/app-message-router.js +12 -3
- package/lib/public/modules/app-messages.js +47 -61
- package/lib/public/modules/app-notifications.js +4 -7
- package/lib/public/modules/app-projects.js +5 -38
- package/lib/public/modules/app-rate-limit.js +70 -12
- package/lib/public/modules/app-rendering.js +12 -13
- package/lib/public/modules/avatar.js +25 -9
- package/lib/public/modules/chat-render-runtime.js +16 -4
- package/lib/public/modules/command-palette.js +2 -15
- package/lib/public/modules/context-sources.js +27 -18
- package/lib/public/modules/cursor-sharing-preference.js +3 -3
- package/lib/public/modules/debate-controls.js +299 -0
- package/lib/public/modules/debate.js +2 -0
- package/lib/public/modules/debates-workbench.js +339 -0
- package/lib/public/modules/default-ai.js +88 -65
- package/lib/public/modules/header-tui-font.js +10 -20
- package/lib/public/modules/home-debate-controls.js +18 -289
- package/lib/public/modules/home-mate-avatar-editor.js +228 -0
- package/lib/public/modules/home-mate-color-picker.js +66 -0
- package/lib/public/modules/home-mate-creation.js +1 -1
- package/lib/public/modules/home-mate-instructions.js +90 -0
- package/lib/public/modules/home-mate-model-picker.js +80 -271
- package/lib/public/modules/home-mate-profile-editor.js +72 -0
- package/lib/public/modules/home-mate-settings.js +118 -125
- package/lib/public/modules/home-shell.js +3 -0
- package/lib/public/modules/home-surface-boot.js +1 -9
- package/lib/public/modules/icon-strip-tooltip.js +95 -0
- package/lib/public/modules/knowledge-db-edit-state.js +35 -0
- package/lib/public/modules/knowledge-db-workbench.js +234 -0
- package/lib/public/modules/knowledge-history-state.js +55 -0
- package/lib/public/modules/knowledge-markdown-editor.js +57 -0
- package/lib/public/modules/knowledge-preview-links.js +70 -0
- package/lib/public/modules/knowledge-workbench-shell.js +21 -0
- package/lib/public/modules/linear-settings.js +3 -2
- package/lib/public/modules/markdown.js +5 -6
- package/lib/public/modules/mate-knowledge-workbench-state.js +174 -0
- package/lib/public/modules/mate-knowledge-workbench.js +500 -0
- package/lib/public/modules/mate-knowledge.js +11 -3
- package/lib/public/modules/mate-management.js +54 -10
- package/lib/public/modules/mate-sidebar.js +11 -1
- package/lib/public/modules/mate-workspace-color.js +33 -0
- package/lib/public/modules/mention.js +5 -13
- 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 +66 -24
- package/lib/public/modules/project-chat-identity.js +79 -0
- package/lib/public/modules/project-debate-planning-workspace.js +250 -0
- package/lib/public/modules/project-debate-workspace.js +185 -0
- package/lib/public/modules/project-header-identity.js +127 -0
- package/lib/public/modules/project-logs-render.js +9 -4
- package/lib/public/modules/project-mate-creation-workspace.js +414 -0
- package/lib/public/modules/project-mate-navigation.js +238 -0
- package/lib/public/modules/project-session-navigation.js +29 -0
- package/lib/public/modules/project-settings-environment.js +106 -0
- package/lib/public/modules/project-settings.js +72 -628
- package/lib/public/modules/project-switcher.js +6 -8
- package/lib/public/modules/projects-hub-view.js +5 -6
- package/lib/public/modules/projects-hub.js +2 -6
- package/lib/public/modules/qrcode.js +27 -8
- package/lib/public/modules/search-clay-chat.js +8 -11
- 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-activity-tooltip.js +12 -0
- package/lib/public/modules/session-create-form.js +86 -14
- package/lib/public/modules/session-create.js +50 -7
- package/lib/public/modules/session-github.js +2 -1
- package/lib/public/modules/session-list-visibility.js +41 -0
- package/lib/public/modules/session-presence-ui.js +3 -0
- package/lib/public/modules/session-title-marquee.js +51 -0
- 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 +10 -7
- package/lib/public/modules/sidebar-mates.js +3 -50
- package/lib/public/modules/sidebar-mobile.js +63 -60
- package/lib/public/modules/sidebar-projects.js +84 -97
- package/lib/public/modules/sidebar-session-folders-ui.js +2 -2
- package/lib/public/modules/sidebar-session-hierarchy.js +149 -70
- package/lib/public/modules/sidebar-sessions.js +49 -50
- package/lib/public/modules/sidebar.js +11 -13
- 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/thinking-lifecycle.js +7 -6
- package/lib/public/modules/tool-palette-order.js +4 -3
- package/lib/public/modules/tool-palette.js +13 -3
- package/lib/public/modules/tools.js +22 -22
- 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/modules/worker-proposal.js +42 -8
- package/lib/public/modules/worker-runtime-picker.js +115 -0
- package/lib/public/modules/you-activity.js +121 -0
- package/lib/public/modules/you.js +251 -0
- package/lib/public/style.css +20 -8
- package/lib/public/vendor/codemirror/LICENSE +21 -0
- package/lib/public/vendor/codemirror/codemirror.css +344 -0
- package/lib/public/vendor/codemirror/codemirror.js +9884 -0
- package/lib/public/vendor/codemirror/continuelist.js +101 -0
- package/lib/public/vendor/codemirror/markdown.js +886 -0
- package/lib/public/vendor/codemirror/show-hint.css +37 -0
- package/lib/public/vendor/codemirror/show-hint.js +523 -0
- package/lib/sdk-bridge.js +15 -3
- package/lib/sdk-message-processor.js +16 -4
- package/lib/sdk-quota-snapshot.js +37 -0
- package/lib/server-admin.js +11 -0
- package/lib/server-appearance-settings.js +71 -0
- package/lib/server-home-chat-events.js +1 -1
- package/lib/server-home-chat.js +25 -3
- package/lib/server-home-debate-planning.js +4 -1
- package/lib/server-home-debates.js +2 -0
- package/lib/server-home-mate-creation.js +4 -1
- package/lib/server-linear.js +14 -8
- package/lib/server-mate-instructions.js +44 -0
- package/lib/server-mates.js +12 -3
- package/lib/server-notification-settings.js +67 -0
- package/lib/server-palette.js +1 -1
- package/lib/server-settings.js +69 -1
- package/lib/server-storage.js +56 -0
- package/lib/server-user-knowledge.js +119 -0
- package/lib/server.js +31 -3
- package/lib/session-visibility.js +7 -2
- package/lib/sessions.js +34 -9
- package/lib/startup-projects.js +40 -0
- package/lib/storage-usage.js +127 -0
- package/lib/user-knowledge-service.js +167 -0
- package/lib/users-permissions.js +7 -0
- package/lib/users-preferences.js +0 -74
- package/lib/users-project-mate-filter-preferences.js +39 -0
- package/lib/users.js +6 -4
- package/lib/workspace-query-access.js +4 -1
- package/lib/ws-schema.js +52 -4
- package/lib/yoke/adapters/codex.js +8 -45
- package/lib/yoke/codex-model-catalog.js +40 -0
- package/lib/yoke/codex-rate-limits.js +26 -0
- 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 };
|
package/lib/server-admin.js
CHANGED
|
@@ -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",
|
|
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
|
}
|
package/lib/server-home-chat.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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.
|
|
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);
|
package/lib/server-linear.js
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
49
|
+
finally { if (pending.get(pendingId) === operation) pending.delete(pendingId); }
|
|
44
50
|
});
|
|
45
|
-
req.on('error', function () { if (pending.get(
|
|
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};
|
package/lib/server-mates.js
CHANGED
|
@@ -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 };
|
package/lib/server-palette.js
CHANGED
|
@@ -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;
|
package/lib/server-settings.js
CHANGED
|
@@ -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": "
|
|
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
|