clay-server 5.1.0-beta.2 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +9 -91
- package/lib/builtin-mates.js +7 -7
- package/lib/daemon-settings.js +11 -0
- package/lib/daemon.js +8 -37
- package/lib/linear-store.js +35 -8
- package/lib/mate-identity-file.js +39 -0
- package/lib/mate-knowledge-sync.js +1 -1
- package/lib/mate-ready-creation.js +2 -1
- package/lib/mates-identity.js +4 -4
- package/lib/mates-prompts.js +4 -3
- package/lib/mates.js +12 -11
- package/lib/os-users.js +5 -16
- package/lib/project-connection.js +0 -7
- package/lib/project-debate.js +3 -2
- package/lib/project-filesystem.js +21 -39
- package/lib/project-mate-interaction.js +7 -6
- package/lib/project-server-settings.js +31 -0
- package/lib/project-sessions.js +57 -184
- package/lib/project.js +3 -3
- package/lib/public/app.js +6 -37
- package/lib/public/css/input.css +3 -1
- package/lib/public/css/menus.css +3 -0
- package/lib/public/css/overlays.css +7 -0
- package/lib/public/css/project-settings-modal.css +56 -0
- package/lib/public/css/server-settings-system.css +125 -0
- package/lib/public/css/session-github.css +25 -16
- package/lib/public/css/settings-ui.css +247 -22
- package/lib/public/css/shared-browser.css +7 -3
- package/lib/public/css/title-bar.css +5 -1
- package/lib/public/index.html +173 -447
- package/lib/public/modules/account-island.js +9 -7
- package/lib/public/modules/admin-settings-email.js +197 -0
- package/lib/public/modules/admin-settings-invitations.js +183 -0
- package/lib/public/modules/admin-settings-projects.js +198 -0
- package/lib/public/modules/admin-settings-ui.js +100 -0
- package/lib/public/modules/admin-settings-users.js +331 -0
- package/lib/public/modules/admin.js +19 -949
- package/lib/public/modules/app-dm.js +0 -2
- package/lib/public/modules/app-messages.js +13 -52
- package/lib/public/modules/app-rate-limit.js +70 -12
- package/lib/public/modules/avatar.js +6 -1
- package/lib/public/modules/context-sources.js +27 -18
- package/lib/public/modules/default-ai.js +14 -7
- package/lib/public/modules/header-tui-font.js +10 -20
- package/lib/public/modules/home-mate-avatar-editor.js +2 -2
- package/lib/public/modules/linear-settings.js +3 -2
- package/lib/public/modules/markdown.js +5 -6
- package/lib/public/modules/notification-settings.js +81 -0
- package/lib/public/modules/notifications.js +44 -28
- package/lib/public/modules/personal-settings-dialog.js +51 -0
- package/lib/public/modules/profile.js +15 -6
- package/lib/public/modules/project-settings-environment.js +106 -0
- package/lib/public/modules/project-settings.js +72 -628
- package/lib/public/modules/server-settings-controls.js +316 -0
- package/lib/public/modules/server-settings-environment.js +194 -0
- package/lib/public/modules/server-settings-navigation.js +39 -0
- package/lib/public/modules/server-settings-state.js +54 -0
- package/lib/public/modules/server-settings.js +116 -788
- package/lib/public/modules/server-storage-settings.js +84 -0
- package/lib/public/modules/session-github.js +2 -1
- package/lib/public/modules/session-presence-ui.js +1 -1
- package/lib/public/modules/settings-env-values.js +34 -0
- package/lib/public/modules/settings-select.js +121 -0
- package/lib/public/modules/shared-browser.js +22 -12
- package/lib/public/modules/stt-picker.js +14 -5
- package/lib/public/modules/terminal-prefs.js +22 -0
- package/lib/public/modules/theme.js +88 -52
- package/lib/public/modules/user-settings-account.js +318 -0
- package/lib/public/modules/user-settings-appearance.js +55 -0
- package/lib/public/modules/user-settings-editors.js +24 -0
- package/lib/public/modules/user-settings-integrations.js +104 -0
- package/lib/public/modules/user-settings-layout.js +80 -0
- package/lib/public/modules/user-settings-preferences.js +48 -0
- package/lib/public/modules/user-settings-profile.js +53 -0
- package/lib/public/modules/user-settings-themes.js +38 -0
- package/lib/public/modules/user-settings-voice.js +75 -0
- package/lib/public/modules/user-settings.js +53 -480
- package/lib/public/style.css +4 -0
- package/lib/sdk-message-processor.js +16 -4
- package/lib/sdk-quota-snapshot.js +37 -0
- package/lib/server-appearance-settings.js +71 -0
- package/lib/server-linear.js +14 -8
- package/lib/server-mate-instructions.js +2 -2
- package/lib/server-mates.js +2 -1
- package/lib/server-notification-settings.js +67 -0
- package/lib/server-settings.js +4 -0
- package/lib/server-storage.js +57 -0
- package/lib/server.js +10 -2
- package/lib/sessions.js +3 -4
- package/lib/startup-projects.js +40 -0
- package/lib/storage-native-walk.js +85 -0
- package/lib/storage-usage.js +144 -0
- package/lib/users-preferences.js +0 -74
- package/lib/users.js +0 -4
- package/lib/ws-schema.js +2 -4
- package/lib/yoke/acp-process-manager.js +2 -2
- package/lib/yoke/adapters/codex.js +12 -52
- package/lib/yoke/codex-app-server.js +2 -2
- package/lib/yoke/codex-model-catalog.js +52 -0
- package/lib/yoke/codex-rate-limits.js +26 -0
- package/package.json +3 -3
|
@@ -6,11 +6,13 @@ var yoke = require("./yoke");
|
|
|
6
6
|
var backgroundTaskTiming = require("./background-task-timing");
|
|
7
7
|
var sessionTitlePolicy = require("./session-title-policy");
|
|
8
8
|
var askUserIdentity = require("./ask-user-request-identity");
|
|
9
|
+
var quotaSnapshots = require("./sdk-quota-snapshot");
|
|
9
10
|
|
|
10
11
|
var OVERLOAD_RETRY_LIMIT = 3;
|
|
11
12
|
|
|
12
13
|
function attachMessageProcessor(ctx) {
|
|
13
14
|
var sm = ctx.sm;
|
|
15
|
+
quotaSnapshots.attachQuotaSnapshots(sm);
|
|
14
16
|
var send = ctx.send;
|
|
15
17
|
var slug = ctx.slug;
|
|
16
18
|
var isMate = ctx.isMate;
|
|
@@ -653,11 +655,13 @@ function attachMessageProcessor(ctx) {
|
|
|
653
655
|
session._queuedTurnCount = 0;
|
|
654
656
|
onProcessingChanged();
|
|
655
657
|
}
|
|
656
|
-
//
|
|
658
|
+
// Recognize only short, zero-cost CLI login responses, not ordinary prose.
|
|
657
659
|
var previewTrimmed = (session.responsePreview || "").trim();
|
|
658
660
|
var isZeroCost = !parsed.cost || parsed.cost === 0;
|
|
659
661
|
var isLoginPrompt = isZeroCost && previewTrimmed.length < 100
|
|
660
|
-
&& /not logged in/i.test(previewTrimmed) && /\/login/i.test(previewTrimmed)
|
|
662
|
+
&& ((/not logged in/i.test(previewTrimmed) && /\/login/i.test(previewTrimmed))
|
|
663
|
+
|| ((session.vendor || "claude") === "claude"
|
|
664
|
+
&& /^Failed to authenticate: OAuth session expired and could not be refreshed\.?$/i.test(previewTrimmed)));
|
|
661
665
|
// Fetch rich context usage breakdown (fire-and-forget, non-blocking)
|
|
662
666
|
if (session.queryInstance && typeof session.queryInstance.getContextUsage === "function") {
|
|
663
667
|
var contextQuery = session.queryInstance;
|
|
@@ -692,7 +696,7 @@ function attachMessageProcessor(ctx) {
|
|
|
692
696
|
if (parsed.fastModeState) {
|
|
693
697
|
sendAndRecord(session, { type: "fast_mode_state", state: parsed.fastModeState });
|
|
694
698
|
}
|
|
695
|
-
//
|
|
699
|
+
// Recover both missing login and expired OAuth sessions from the SDK.
|
|
696
700
|
// This is a short canned response with zero cost, not actual AI output.
|
|
697
701
|
if (isLoginPrompt) {
|
|
698
702
|
emitAuthRequired(session);
|
|
@@ -853,6 +857,10 @@ function attachMessageProcessor(ctx) {
|
|
|
853
857
|
|
|
854
858
|
} else if (parsed.yokeType === "rate_limit") {
|
|
855
859
|
var info = parsed.rateLimitInfo;
|
|
860
|
+
if (info.source === "account_snapshot") {
|
|
861
|
+
quotaSnapshots.recordQuotaSnapshot(session, info, sendAndRecord);
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
856
864
|
console.log("[sdk-bridge] rate_limit_event for session " + session.localId + ": status=" + info.status + " resetsAt=" + info.resetsAt + " isUsingOverage=" + info.isUsingOverage + " isProcessing=" + session.isProcessing);
|
|
857
865
|
|
|
858
866
|
// Persist the scoped reset snapshot so reconnect/history restore can reproject it.
|
|
@@ -877,7 +885,11 @@ function attachMessageProcessor(ctx) {
|
|
|
877
885
|
resetsAt: info.resetsAt ? info.resetsAt * 1000 : null,
|
|
878
886
|
rateLimitType: info.rateLimitType || null,
|
|
879
887
|
utilization: info.utilization || null,
|
|
880
|
-
isUsingOverage: info.isUsingOverage ||
|
|
888
|
+
isUsingOverage: info.isUsingOverage === true || info.overageInUse === true,
|
|
889
|
+
overageEnabled: typeof info.overageEnabled === "boolean" ? info.overageEnabled : null,
|
|
890
|
+
overageStatus: info.overageStatus || null,
|
|
891
|
+
overageResetsAt: info.overageResetsAt ? info.overageResetsAt * 1000 : null,
|
|
892
|
+
overageDisabledReason: info.overageDisabledReason || null,
|
|
881
893
|
});
|
|
882
894
|
// Track rejection for auto-continue / scheduled message support
|
|
883
895
|
if (info.status === "rejected" && info.resetsAt) {
|
|
@@ -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 };
|
|
@@ -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 };
|
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 };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
var resolveMateIdentityFile = require('./mate-identity-file').resolveMateIdentityFile;
|
|
1
2
|
// Owner-scoped identity editing; generated instruction sections stay intact.
|
|
2
3
|
var fs = require('fs');
|
|
3
|
-
var path = require('path');
|
|
4
4
|
var crypto = require('crypto');
|
|
5
5
|
|
|
6
6
|
function revision(content) { return crypto.createHash('sha256').update(content).digest('hex'); }
|
|
@@ -11,7 +11,7 @@ function handleMateInstructions(mates, ws, msg, userId) {
|
|
|
11
11
|
var ctx = mates.buildMateCtx(userId);
|
|
12
12
|
if (typeof msg.mateId !== 'string' || !mates.isMateIdFormat(msg.mateId) || !mates.getMate(ctx, msg.mateId)) throw new Error('Mate not found.');
|
|
13
13
|
var directory = mates.getMateDir(ctx, msg.mateId);
|
|
14
|
-
var file =
|
|
14
|
+
var file = resolveMateIdentityFile(directory);
|
|
15
15
|
if (!fs.lstatSync(file).isFile()) throw new Error('Mate instructions are unavailable.');
|
|
16
16
|
var content = fs.readFileSync(file, 'utf8');
|
|
17
17
|
var identity = mates.extractIdentity(content);
|
package/lib/server-mates.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
var resolveMateIdentityFile = require('./mate-identity-file').resolveMateIdentityFile;
|
|
1
2
|
var fs = require("fs");
|
|
2
3
|
var path = require("path");
|
|
3
4
|
var handleMateInstructions = require('./server-mate-instructions').handleMateInstructions;
|
|
@@ -32,7 +33,7 @@ function attachMates(ctx) {
|
|
|
32
33
|
});
|
|
33
34
|
for (var ri = 0; ri < allMates.length; ri++) {
|
|
34
35
|
var mDir = mates.getMateDir(mateCtx, allMates[ri].id);
|
|
35
|
-
var claudePath =
|
|
36
|
+
var claudePath = resolveMateIdentityFile(mDir);
|
|
36
37
|
try {
|
|
37
38
|
mates.enforceAllSections(claudePath, { ctx: mateCtx, mateId: allMates[ri].id, projects: projList });
|
|
38
39
|
} catch (e) {}
|
|
@@ -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-settings.js
CHANGED
|
@@ -12,9 +12,13 @@ function attachSettings(ctx) {
|
|
|
12
12
|
var CONFIG_DIR = ctx.CONFIG_DIR;
|
|
13
13
|
|
|
14
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);
|
|
15
17
|
var experimentalSettings = attachExperimentalSettings(ctx);
|
|
16
18
|
|
|
17
19
|
function handleRequest(req, res, fullUrl) {
|
|
20
|
+
if (appearanceSettings.handleRequest(req, res, fullUrl)) return true;
|
|
21
|
+
if (notificationSettings.handleRequest(req, res, fullUrl)) return true;
|
|
18
22
|
if (experimentalSettings.handleRequest(req, res, fullUrl)) return true;
|
|
19
23
|
// Render historical avatar styles locally so profile seeds never leave Clay.
|
|
20
24
|
if (req.method === "GET" && fullUrl === "/api/generated-avatar") {
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
var scanStorage = require('./storage-usage').scanStorage;
|
|
2
|
+
|
|
3
|
+
function attachStorage(ctx) {
|
|
4
|
+
var pending = null;
|
|
5
|
+
var cached = null;
|
|
6
|
+
var progress = null;
|
|
7
|
+
var expires = 0;
|
|
8
|
+
var failed = false;
|
|
9
|
+
function reply(res, status, data) {
|
|
10
|
+
res.writeHead(status, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
|
|
11
|
+
res.end(JSON.stringify(data));
|
|
12
|
+
}
|
|
13
|
+
function authorized(req, res) {
|
|
14
|
+
if (!ctx.isRequestAuthed(req)) { reply(res, 401, { error: 'Authentication required.' }); return false; }
|
|
15
|
+
var user = ctx.getMultiUserFromReq(req);
|
|
16
|
+
if (ctx.users.isMultiUser() && (!user || user.role !== 'admin')) {
|
|
17
|
+
reply(res, 403, { error: 'Only server administrators can view disk usage.' }); return false;
|
|
18
|
+
}
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
function handleRequest(req, res, url) {
|
|
22
|
+
if (url.split('?')[0] !== '/api/server/storage') return false;
|
|
23
|
+
var refresh = new URL(url, 'http://localhost').searchParams.get('refresh') === '1';
|
|
24
|
+
if (!authorized(req, res)) return true;
|
|
25
|
+
if (req.method !== 'GET') { reply(res, 405, { error: 'Method not allowed.' }); return true; }
|
|
26
|
+
if (failed && Date.now() < expires) { reply(res, 500, { error: 'Disk usage could not be measured. Try again in a few seconds.' }); return true; }
|
|
27
|
+
if (!pending && !refresh && cached && Date.now() < expires) { reply(res, 200, cached); return true; }
|
|
28
|
+
if (!pending) {
|
|
29
|
+
progress = null; failed = false;
|
|
30
|
+
var users = ctx.users.getAllUsers();
|
|
31
|
+
var projects = ctx.getProjects ? ctx.getProjects() : [];
|
|
32
|
+
if (!ctx.users.isMultiUser() && users.length <= 1) {
|
|
33
|
+
if (!users.length) users = [{ id: 'local', username: 'Local user' }];
|
|
34
|
+
projects = projects.map(function (project) { return Object.assign({}, project, { projectOwnerId: project.projectOwnerId || users[0].id }); });
|
|
35
|
+
}
|
|
36
|
+
pending = (ctx.scanStorage || scanStorage)({ configDir: ctx.configDir, platform: process.platform,
|
|
37
|
+
attribution: ctx.osUsers && ctx.users.isMultiUser() && process.platform === 'linux' ? 'filesystem' : 'project',
|
|
38
|
+
users: users, projects: projects, onProgress: function (data) { progress = data; },
|
|
39
|
+
projectPaths: ctx.getProjectPaths ? ctx.getProjectPaths() : [] }).then(function (result) {
|
|
40
|
+
cached = result; expires = Date.now() + 300000; return result;
|
|
41
|
+
}).catch(function (error) { failed = true; expires = Date.now() + 5000; throw error; }).finally(function () { pending = null; });
|
|
42
|
+
}
|
|
43
|
+
var timer;
|
|
44
|
+
Promise.race([pending, new Promise(function (resolve) {
|
|
45
|
+
timer = setTimeout(function () { resolve(progress || { scanning: true }); }, 750);
|
|
46
|
+
})]).then(function (result) {
|
|
47
|
+
clearTimeout(timer);
|
|
48
|
+
if (!res.destroyed && authorized(req, res)) reply(res, result.scanning ? 202 : 200, result);
|
|
49
|
+
}).catch(function () {
|
|
50
|
+
clearTimeout(timer);
|
|
51
|
+
if (!res.destroyed) reply(res, 500, { error: 'Disk usage could not be measured. Try refreshing.' });
|
|
52
|
+
});
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
return { handleRequest: handleRequest };
|
|
56
|
+
}
|
|
57
|
+
module.exports = { attachStorage: attachStorage };
|
package/lib/server.js
CHANGED
|
@@ -188,7 +188,6 @@ function createServer(opts) {
|
|
|
188
188
|
var onGetDaemonConfig = opts.onGetDaemonConfig || null;
|
|
189
189
|
var onSetPin = opts.onSetPin || null;
|
|
190
190
|
var onSetKeepAwake = opts.onSetKeepAwake || null;
|
|
191
|
-
var onSetInheritGroups = opts.onSetInheritGroups || null;
|
|
192
191
|
var onSetImageRetention = opts.onSetImageRetention || null;
|
|
193
192
|
var onShutdown = opts.onShutdown || null;
|
|
194
193
|
var onRestart = opts.onRestart || null;
|
|
@@ -473,9 +472,14 @@ function createServer(opts) {
|
|
|
473
472
|
users: users, getMultiUserFromReq: getMultiUserFromReq,
|
|
474
473
|
isRequestAuthed: isRequestAuthed, opts: opts, getConfiguredOrigins: getConfiguredOrigins,
|
|
475
474
|
});
|
|
475
|
+
var storageSettings = require('./server-storage').attachStorage({
|
|
476
|
+
users: users, getMultiUserFromReq: getMultiUserFromReq, isRequestAuthed: isRequestAuthed,
|
|
477
|
+
configDir: CONFIG_DIR, osUsers: osUsers, getProjects: getProjects,
|
|
478
|
+
});
|
|
476
479
|
var logRejectedOrigin = createOriginRejectionLogger({ log: opts.onOriginRejectedLog });
|
|
477
480
|
|
|
478
481
|
var settings = serverSettings.attachSettings({
|
|
482
|
+
isRequestAuthed: isRequestAuthed,
|
|
479
483
|
users: users,
|
|
480
484
|
mates: mates,
|
|
481
485
|
getMultiUserFromReq: getMultiUserFromReq,
|
|
@@ -815,6 +819,7 @@ function createServer(opts) {
|
|
|
815
819
|
if (linear.handleRequest(req, res, fullUrl)) return;
|
|
816
820
|
if (speech.handleRequest(req, res, fullUrl)) return;
|
|
817
821
|
if (networkSettings.handleRequest(req, res, fullUrl)) return;
|
|
822
|
+
if (storageSettings.handleRequest(req, res, fullUrl)) return;
|
|
818
823
|
if (settings.handleRequest(req, res, fullUrl)) return;
|
|
819
824
|
|
|
820
825
|
// --- Admin API endpoints (multi-user mode only) ---
|
|
@@ -1516,10 +1521,13 @@ function createServer(opts) {
|
|
|
1516
1521
|
},
|
|
1517
1522
|
onGetProjectMcpServers: onGetProjectMcpServers,
|
|
1518
1523
|
onSetProjectMcpServers: onSetProjectMcpServers,
|
|
1524
|
+
onGetSharedEnv: opts.onGetSharedEnv,
|
|
1525
|
+
onSetSharedEnv: opts.onSetSharedEnv,
|
|
1526
|
+
onGetProjectEnv: opts.onGetProjectEnv,
|
|
1527
|
+
onSetProjectEnv: opts.onSetProjectEnv,
|
|
1519
1528
|
onGetDaemonConfig: onGetDaemonConfig,
|
|
1520
1529
|
onSetPin: onSetPin,
|
|
1521
1530
|
onSetKeepAwake: onSetKeepAwake,
|
|
1522
|
-
onSetInheritGroups: onSetInheritGroups,
|
|
1523
1531
|
onSetImageRetention: onSetImageRetention,
|
|
1524
1532
|
onSetUpdateChannel: onSetUpdateChannel,
|
|
1525
1533
|
updateChannel: onGetDaemonConfig ? (onGetDaemonConfig().updateChannel || "stable") : "stable",
|
package/lib/sessions.js
CHANGED
|
@@ -444,9 +444,8 @@ function createSessionManager(opts) {
|
|
|
444
444
|
// Adopt orphaned CLI sessions from ~/.claude/projects/<encoded-cwd>/ as
|
|
445
445
|
// Clay session records. After this runs the sidebar shows a single
|
|
446
446
|
// unified list of sessions regardless of whether they were born inside
|
|
447
|
-
// Clay or via the `claude` CLI directly.
|
|
448
|
-
//
|
|
449
|
-
// paths already exist for born-TUI sessions.
|
|
447
|
+
// Clay or via the `claude` CLI directly. Idle CLI sessions hydrate into
|
|
448
|
+
// Clay chat; already-running terminal runtimes keep their presentation.
|
|
450
449
|
//
|
|
451
450
|
// Adopted records are saved with mode='tui' because they originated in
|
|
452
451
|
// the CLI, not via the SDK. The cross-mode click logic in
|
|
@@ -1410,7 +1409,7 @@ function createSessionManager(opts) {
|
|
|
1410
1409
|
console.log("[session] Migrated " + migrated + " session title(s) to SDK format");
|
|
1411
1410
|
}
|
|
1412
1411
|
if (failed > 0) {
|
|
1413
|
-
console.log("[session]
|
|
1412
|
+
console.log("[session] Could not sync " + failed + " session title(s) to the provider; local titles and history are unchanged");
|
|
1414
1413
|
}
|
|
1415
1414
|
}).catch(function(e) {
|
|
1416
1415
|
console.error("[session] Migration chain failed:", e.message || e);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Saved project records are authoritative; filesystem availability only controls
|
|
2
|
+
// runtime registration. Recent-project entries must never restore access grants.
|
|
3
|
+
function buildStartupProjects(config, recent, options) {
|
|
4
|
+
var projects = [];
|
|
5
|
+
var paths = new Set();
|
|
6
|
+
var slugs = [];
|
|
7
|
+
var removed = new Set((config && config.removedProjects || []).map(function (p) { return p.path; }));
|
|
8
|
+
(config && config.projects || []).forEach(function (p) {
|
|
9
|
+
if (paths.has(p.path)) return;
|
|
10
|
+
projects.push(Object.assign({}, p));
|
|
11
|
+
paths.add(p.path);
|
|
12
|
+
slugs.push(p.slug);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
function add(project) {
|
|
16
|
+
if (paths.has(project.path) || removed.has(project.path)) return;
|
|
17
|
+
if (!options.existsSync(project.path)) return;
|
|
18
|
+
var slug = options.generateSlug(project.path, slugs);
|
|
19
|
+
projects.push({
|
|
20
|
+
path: project.path,
|
|
21
|
+
slug: slug,
|
|
22
|
+
title: project.title || undefined,
|
|
23
|
+
icon: project.icon || undefined,
|
|
24
|
+
addedAt: project.addedAt || Date.now(),
|
|
25
|
+
});
|
|
26
|
+
paths.add(project.path);
|
|
27
|
+
slugs.push(slug);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (options.addCwd) {
|
|
31
|
+
var cwdRecent = (recent || []).find(function (p) { return p.path === options.cwd; });
|
|
32
|
+
add(cwdRecent || { path: options.cwd });
|
|
33
|
+
}
|
|
34
|
+
(recent || []).forEach(function (p) {
|
|
35
|
+
if (p.path !== options.cwd) add(p);
|
|
36
|
+
});
|
|
37
|
+
return projects;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = { buildStartupProjects: buildStartupProjects };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
var spawn = require('node:child_process').spawn;
|
|
2
|
+
var execFile = require('node:child_process').execFile;
|
|
3
|
+
var availability = null;
|
|
4
|
+
var command = process.platform === 'linux' ? 'find' : 'gfind';
|
|
5
|
+
|
|
6
|
+
function available() {
|
|
7
|
+
if (process.platform !== 'linux' && process.platform !== 'darwin') return Promise.resolve(false);
|
|
8
|
+
if (!availability) availability = new Promise(function (resolve) {
|
|
9
|
+
execFile(command, ['--version'], { timeout: 3000, maxBuffer: 4096 }, function (error, output) {
|
|
10
|
+
resolve(!error && output.indexOf('GNU findutils') !== -1);
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
return availability;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function literalPattern(value) {
|
|
17
|
+
return value.replace(/[\\*?\[\]]/g, '\\$&');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// NUL-delimited records keep tabs/newlines in filenames out of metadata parsing.
|
|
21
|
+
function parseRecord(record) {
|
|
22
|
+
var fields = [];
|
|
23
|
+
var start = 0;
|
|
24
|
+
for (var i = 0; i < 6; i++) {
|
|
25
|
+
var end = record.indexOf('\t', start);
|
|
26
|
+
if (end < 0) throw new Error('Invalid native storage metadata');
|
|
27
|
+
fields.push(record.slice(start, end));
|
|
28
|
+
start = end + 1;
|
|
29
|
+
}
|
|
30
|
+
if (!fields.slice(0, 5).every(function (value) { return /^\d+$/.test(value); }) || !/^[a-z?]$/.test(fields[5])) {
|
|
31
|
+
throw new Error('Invalid native storage metadata');
|
|
32
|
+
}
|
|
33
|
+
return { path: record.slice(start), stat: {
|
|
34
|
+
dev: Number(fields[0]), ino: Number(fields[1]), uid: Number(fields[2]), blocks: Number(fields[3]), nlink: Number(fields[4]),
|
|
35
|
+
isDirectory: function () { return fields[5] === 'd'; },
|
|
36
|
+
isFile: function () { return fields[5] === 'f'; },
|
|
37
|
+
isSymbolicLink: function () { return fields[5] === 'l'; }
|
|
38
|
+
} };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function walk(root, excluded, onEntry, maxMs) {
|
|
42
|
+
return new Promise(function (resolve) {
|
|
43
|
+
var args = ['-P', root, '-xdev'];
|
|
44
|
+
if (excluded.length) {
|
|
45
|
+
args.push('(');
|
|
46
|
+
excluded.forEach(function (entry, index) {
|
|
47
|
+
if (index) args.push('-o');
|
|
48
|
+
args.push('-path', literalPattern(entry));
|
|
49
|
+
});
|
|
50
|
+
args.push(')', '-prune', '-o');
|
|
51
|
+
}
|
|
52
|
+
args.push('-printf', '%D\t%i\t%U\t%b\t%n\t%y\t%p\\0');
|
|
53
|
+
var child = spawn(command, args, { stdio: ['ignore', 'pipe', 'pipe'], env: Object.assign({}, process.env, { LC_ALL: 'C' }) });
|
|
54
|
+
var buffer = '';
|
|
55
|
+
var stopped = false;
|
|
56
|
+
var errors = 0;
|
|
57
|
+
var timer = null;
|
|
58
|
+
function stop() { stopped = true; child.kill('SIGKILL'); }
|
|
59
|
+
if (Number.isFinite(maxMs)) timer = setTimeout(stop, Math.max(1, maxMs));
|
|
60
|
+
child.stdout.setEncoding('utf8');
|
|
61
|
+
child.stdout.on('data', function (chunk) {
|
|
62
|
+
if (stopped) return;
|
|
63
|
+
buffer += chunk;
|
|
64
|
+
var end;
|
|
65
|
+
while (!stopped && (end = buffer.indexOf('\0')) !== -1) {
|
|
66
|
+
var record = buffer.slice(0, end);
|
|
67
|
+
buffer = buffer.slice(end + 1);
|
|
68
|
+
try {
|
|
69
|
+
var entry = parseRecord(record);
|
|
70
|
+
if (onEntry(entry.path, entry.stat) === false) stop();
|
|
71
|
+
} catch (error) { errors++; stop(); }
|
|
72
|
+
}
|
|
73
|
+
if (buffer.length > 1024 * 1024) { errors++; stop(); }
|
|
74
|
+
});
|
|
75
|
+
child.stderr.on('data', function () { errors = Math.max(1, errors); });
|
|
76
|
+
child.on('error', function () { errors++; });
|
|
77
|
+
child.on('close', function (code) {
|
|
78
|
+
clearTimeout(timer);
|
|
79
|
+
if (!stopped && (code !== 0 || buffer.length)) errors++;
|
|
80
|
+
resolve({ errors: errors, stopped: stopped });
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = { available: available, walk: walk, parseRecord: parseRecord };
|