clay-server 2.8.2 → 2.9.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/README.md +2 -0
- package/bin/cli.js +122 -2
- package/lib/config.js +20 -1
- package/lib/daemon.js +40 -0
- package/lib/pages.js +670 -27
- package/lib/project.js +267 -16
- package/lib/public/app.js +74 -14
- package/lib/public/css/admin.css +576 -0
- package/lib/public/css/icon-strip.css +1 -0
- package/lib/public/css/menus.css +16 -11
- package/lib/public/css/overlays.css +2 -4
- package/lib/public/css/sidebar.css +49 -0
- package/lib/public/css/title-bar.css +45 -1
- package/lib/public/index.html +38 -8
- package/lib/public/modules/admin.js +631 -0
- package/lib/public/modules/markdown.js +9 -5
- package/lib/public/modules/profile.js +21 -0
- package/lib/public/modules/project-settings.js +4 -1
- package/lib/public/modules/server-settings.js +13 -0
- package/lib/public/modules/sidebar.js +111 -5
- package/lib/public/style.css +1 -0
- package/lib/push.js +6 -0
- package/lib/server.js +1075 -27
- package/lib/sessions.js +127 -41
- package/lib/smtp.js +221 -0
- package/lib/users.js +459 -0
- package/package.json +2 -1
package/lib/project.js
CHANGED
|
@@ -9,9 +9,35 @@ var { createNotesManager } = require("./notes");
|
|
|
9
9
|
var { fetchLatestVersion, isNewer } = require("./updater");
|
|
10
10
|
var { execFileSync, spawn } = require("child_process");
|
|
11
11
|
var { createLoopRegistry } = require("./scheduler");
|
|
12
|
+
var usersModule = require("./users");
|
|
12
13
|
|
|
13
14
|
var MAX_UPLOAD_BYTES = 50 * 1024 * 1024; // 50 MB
|
|
14
15
|
|
|
16
|
+
// Validate environment variable string (KEY=VALUE per line)
|
|
17
|
+
// Returns null if valid, or an error string if invalid
|
|
18
|
+
function validateEnvString(str) {
|
|
19
|
+
if (!str || !str.trim()) return null;
|
|
20
|
+
var lines = str.split("\n");
|
|
21
|
+
for (var i = 0; i < lines.length; i++) {
|
|
22
|
+
var line = lines[i].trim();
|
|
23
|
+
if (!line || line.charAt(0) === "#") continue;
|
|
24
|
+
// Must be KEY=VALUE format
|
|
25
|
+
var eqIdx = line.indexOf("=");
|
|
26
|
+
if (eqIdx < 1) return "Invalid format at line " + (i + 1) + ": expected KEY=VALUE";
|
|
27
|
+
var key = line.substring(0, eqIdx);
|
|
28
|
+
// Key must be valid env var name (no shell metacharacters)
|
|
29
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key)) {
|
|
30
|
+
return "Invalid variable name at line " + (i + 1) + ": " + key;
|
|
31
|
+
}
|
|
32
|
+
// Value must not contain shell injection characters
|
|
33
|
+
var value = line.substring(eqIdx + 1);
|
|
34
|
+
if (/[`$\\;|&><(){}\n]/.test(value) && !/^["'].*["']$/.test(value)) {
|
|
35
|
+
return "Potentially unsafe value at line " + (i + 1) + ": shell metacharacters detected";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
15
41
|
// SDK loaded dynamically (ESM module)
|
|
16
42
|
var sdkModule = null;
|
|
17
43
|
function getSDK() {
|
|
@@ -81,6 +107,7 @@ function createProjectContext(opts) {
|
|
|
81
107
|
var moveAllSchedulesToProject = opts.moveAllSchedulesToProject || function () { return { ok: false, error: "Not supported" }; };
|
|
82
108
|
var getScheduleCount = opts.getScheduleCount || function () { return 0; };
|
|
83
109
|
var onProcessingChanged = opts.onProcessingChanged || function () {};
|
|
110
|
+
var onPresenceChange = opts.onPresenceChange || function () {};
|
|
84
111
|
var latestVersion = null;
|
|
85
112
|
|
|
86
113
|
// --- Per-project clients ---
|
|
@@ -98,7 +125,28 @@ function createProjectContext(opts) {
|
|
|
98
125
|
}
|
|
99
126
|
|
|
100
127
|
function broadcastClientCount() {
|
|
101
|
-
|
|
128
|
+
var msg = { type: "client_count", count: clients.size };
|
|
129
|
+
if (usersModule.isMultiUser()) {
|
|
130
|
+
var seen = {};
|
|
131
|
+
var userList = [];
|
|
132
|
+
for (var c of clients) {
|
|
133
|
+
if (!c._clayUser) continue;
|
|
134
|
+
var u = c._clayUser;
|
|
135
|
+
if (seen[u.id]) continue;
|
|
136
|
+
seen[u.id] = true;
|
|
137
|
+
var p = u.profile || {};
|
|
138
|
+
userList.push({
|
|
139
|
+
id: u.id,
|
|
140
|
+
displayName: p.name || u.displayName || u.username,
|
|
141
|
+
username: u.username,
|
|
142
|
+
avatarStyle: p.avatarStyle || "thumbs",
|
|
143
|
+
avatarSeed: p.avatarSeed || u.username,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
msg.users = userList;
|
|
147
|
+
}
|
|
148
|
+
send(msg);
|
|
149
|
+
onPresenceChange();
|
|
102
150
|
}
|
|
103
151
|
|
|
104
152
|
function sendToOthers(sender, obj) {
|
|
@@ -202,7 +250,25 @@ function createProjectContext(opts) {
|
|
|
202
250
|
}
|
|
203
251
|
|
|
204
252
|
// --- Session manager ---
|
|
205
|
-
var sm = createSessionManager({
|
|
253
|
+
var sm = createSessionManager({
|
|
254
|
+
cwd: cwd,
|
|
255
|
+
send: send,
|
|
256
|
+
sendTo: sendTo,
|
|
257
|
+
sendEach: function (fn) {
|
|
258
|
+
for (var ws of clients) {
|
|
259
|
+
var user = ws._clayUser;
|
|
260
|
+
var filterFn = null;
|
|
261
|
+
if (usersModule.isMultiUser() && user) {
|
|
262
|
+
filterFn = (function (u) {
|
|
263
|
+
return function (s) {
|
|
264
|
+
return usersModule.canAccessSession(u.id, s, { visibility: "public" });
|
|
265
|
+
};
|
|
266
|
+
})(user);
|
|
267
|
+
}
|
|
268
|
+
fn(ws, filterFn);
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
});
|
|
206
272
|
var _projMode = typeof opts.onGetProjectDefaultMode === "function" ? opts.onGetProjectDefaultMode(slug) : null;
|
|
207
273
|
var _srvMode = typeof opts.onGetServerDefaultMode === "function" ? opts.onGetServerDefaultMode() : null;
|
|
208
274
|
sm.currentPermissionMode = (_projMode && _projMode.mode) || (_srvMode && _srvMode.mode) || "default";
|
|
@@ -320,6 +386,31 @@ function createProjectContext(opts) {
|
|
|
320
386
|
} catch (e) {
|
|
321
387
|
// No saved state, use defaults
|
|
322
388
|
}
|
|
389
|
+
// Recover orphaned loops: if idle but completed loop files exist in .claude/loops/
|
|
390
|
+
if (loopState.phase === "idle") {
|
|
391
|
+
var _loopsBase = path.join(cwd, ".claude", "loops");
|
|
392
|
+
try {
|
|
393
|
+
var _loopDirs = fs.readdirSync(_loopsBase).filter(function (d) {
|
|
394
|
+
return d.indexOf("loop_") === 0;
|
|
395
|
+
});
|
|
396
|
+
for (var _li = 0; _li < _loopDirs.length; _li++) {
|
|
397
|
+
var _ld = path.join(_loopsBase, _loopDirs[_li]);
|
|
398
|
+
try {
|
|
399
|
+
fs.accessSync(path.join(_ld, "PROMPT.md"));
|
|
400
|
+
fs.accessSync(path.join(_ld, "JUDGE.md"));
|
|
401
|
+
fs.accessSync(path.join(_ld, "LOOP.json"));
|
|
402
|
+
// Found a completed loop — recover to approval phase
|
|
403
|
+
loopState.loopId = _loopDirs[_li];
|
|
404
|
+
loopState.phase = "approval";
|
|
405
|
+
var _loopCfg = JSON.parse(fs.readFileSync(path.join(_ld, "LOOP.json"), "utf8"));
|
|
406
|
+
loopState.maxIterations = _loopCfg.maxIterations || 20;
|
|
407
|
+
saveLoopState();
|
|
408
|
+
console.log("[ralph-loop] Recovered orphaned loop: " + _loopDirs[_li]);
|
|
409
|
+
break;
|
|
410
|
+
} catch (e) {}
|
|
411
|
+
}
|
|
412
|
+
} catch (e) {}
|
|
413
|
+
}
|
|
323
414
|
}
|
|
324
415
|
|
|
325
416
|
function clearLoopState() {
|
|
@@ -846,7 +937,8 @@ function createProjectContext(opts) {
|
|
|
846
937
|
});
|
|
847
938
|
|
|
848
939
|
// --- WS connection handler ---
|
|
849
|
-
function handleConnection(ws) {
|
|
940
|
+
function handleConnection(ws, wsUser) {
|
|
941
|
+
ws._clayUser = wsUser || null;
|
|
850
942
|
clients.add(ws);
|
|
851
943
|
broadcastClientCount();
|
|
852
944
|
|
|
@@ -857,7 +949,9 @@ function createProjectContext(opts) {
|
|
|
857
949
|
}
|
|
858
950
|
|
|
859
951
|
// Send cached state
|
|
860
|
-
|
|
952
|
+
var _userId = ws._clayUser ? ws._clayUser.id : null;
|
|
953
|
+
var _filteredProjects = getProjectList(_userId);
|
|
954
|
+
sendTo(ws, { type: "info", cwd: cwd, slug: slug, project: title || project, version: currentVersion, debug: !!debug, dangerouslySkipPermissions: dangerouslySkipPermissions, lanHost: lanHost, projectCount: _filteredProjects.length, projects: _filteredProjects });
|
|
861
955
|
if (latestVersion) {
|
|
862
956
|
sendTo(ws, { type: "update_available", version: latestVersion });
|
|
863
957
|
}
|
|
@@ -879,6 +973,17 @@ function createProjectContext(opts) {
|
|
|
879
973
|
fs.accessSync(path.join(cwd, ".claude", "JUDGE.md"));
|
|
880
974
|
hasLoopFiles = true;
|
|
881
975
|
} catch (e) {}
|
|
976
|
+
// Also check loop directory files
|
|
977
|
+
if (!hasLoopFiles && loopState.loopId) {
|
|
978
|
+
var _avDir = loopDir();
|
|
979
|
+
if (_avDir) {
|
|
980
|
+
try {
|
|
981
|
+
fs.accessSync(path.join(_avDir, "PROMPT.md"));
|
|
982
|
+
fs.accessSync(path.join(_avDir, "JUDGE.md"));
|
|
983
|
+
hasLoopFiles = true;
|
|
984
|
+
} catch (e) {}
|
|
985
|
+
}
|
|
986
|
+
}
|
|
882
987
|
sendTo(ws, {
|
|
883
988
|
type: "loop_available",
|
|
884
989
|
available: hasLoopFiles,
|
|
@@ -911,10 +1016,16 @@ function createProjectContext(opts) {
|
|
|
911
1016
|
});
|
|
912
1017
|
}
|
|
913
1018
|
|
|
914
|
-
// Session list
|
|
1019
|
+
// Session list (filtered for multi-user)
|
|
1020
|
+
var allSessions = [].concat(Array.from(sm.sessions.values())).filter(function (s) { return !s.hidden; });
|
|
1021
|
+
if (usersModule.isMultiUser() && wsUser) {
|
|
1022
|
+
allSessions = allSessions.filter(function (s) {
|
|
1023
|
+
return usersModule.canAccessSession(wsUser.id, s, { visibility: "public" });
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
915
1026
|
sendTo(ws, {
|
|
916
1027
|
type: "session_list",
|
|
917
|
-
sessions:
|
|
1028
|
+
sessions: allSessions.map(function (s) {
|
|
918
1029
|
var loop = s.loop ? Object.assign({}, s.loop) : null;
|
|
919
1030
|
if (loop && loop.loopId && loopRegistry) {
|
|
920
1031
|
var rec = loopRegistry.getById(loop.loopId);
|
|
@@ -931,13 +1042,21 @@ function createProjectContext(opts) {
|
|
|
931
1042
|
isProcessing: s.isProcessing,
|
|
932
1043
|
lastActivity: s.lastActivity || s.createdAt || 0,
|
|
933
1044
|
loop: loop,
|
|
1045
|
+
ownerId: s.ownerId || null,
|
|
1046
|
+
sessionVisibility: s.sessionVisibility || "shared",
|
|
934
1047
|
};
|
|
935
1048
|
}),
|
|
936
1049
|
});
|
|
937
1050
|
|
|
938
|
-
// Restore active session for this client
|
|
1051
|
+
// Restore active session for this client (check access in multi-user mode)
|
|
939
1052
|
var active = sm.getActiveSession();
|
|
1053
|
+
if (active && usersModule.isMultiUser() && wsUser) {
|
|
1054
|
+
if (!usersModule.canAccessSession(wsUser.id, active, { visibility: "public" })) {
|
|
1055
|
+
active = null;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
940
1058
|
if (active) {
|
|
1059
|
+
ws._clayActiveSession = active.localId;
|
|
941
1060
|
sendTo(ws, { type: "session_switched", id: active.localId, cliSessionId: active.cliSessionId || null, loop: active.loop || null });
|
|
942
1061
|
|
|
943
1062
|
var total = active.history.length;
|
|
@@ -968,6 +1087,8 @@ function createProjectContext(opts) {
|
|
|
968
1087
|
}
|
|
969
1088
|
}
|
|
970
1089
|
|
|
1090
|
+
broadcastPresence();
|
|
1091
|
+
|
|
971
1092
|
ws.on("message", function (raw) {
|
|
972
1093
|
var msg;
|
|
973
1094
|
try { msg = JSON.parse(raw.toString()); } catch (e) { return; }
|
|
@@ -1002,7 +1123,21 @@ function createProjectContext(opts) {
|
|
|
1002
1123
|
}
|
|
1003
1124
|
|
|
1004
1125
|
if (msg.type === "new_session") {
|
|
1005
|
-
|
|
1126
|
+
var sessionOpts = {};
|
|
1127
|
+
if (ws._clayUser) sessionOpts.ownerId = ws._clayUser.id;
|
|
1128
|
+
if (msg.sessionVisibility) sessionOpts.sessionVisibility = msg.sessionVisibility;
|
|
1129
|
+
var newSess = sm.createSession(sessionOpts, usersModule.isMultiUser() ? ws : undefined);
|
|
1130
|
+
if (usersModule.isMultiUser()) {
|
|
1131
|
+
ws._clayActiveSession = newSess.localId;
|
|
1132
|
+
broadcastPresence();
|
|
1133
|
+
}
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
if (msg.type === "set_session_visibility") {
|
|
1138
|
+
if (typeof msg.sessionId === "number" && (msg.visibility === "shared" || msg.visibility === "private")) {
|
|
1139
|
+
sm.setSessionVisibility(msg.sessionId, msg.visibility);
|
|
1140
|
+
}
|
|
1006
1141
|
return;
|
|
1007
1142
|
}
|
|
1008
1143
|
|
|
@@ -1017,9 +1152,11 @@ function createProjectContext(opts) {
|
|
|
1017
1152
|
break;
|
|
1018
1153
|
}
|
|
1019
1154
|
}
|
|
1020
|
-
|
|
1155
|
+
var _rws = usersModule.isMultiUser() ? ws : undefined;
|
|
1156
|
+
sm.resumeSession(msg.cliSessionId, { history: history, title: title }, _rws);
|
|
1021
1157
|
}).catch(function () {
|
|
1022
|
-
|
|
1158
|
+
var _rws = usersModule.isMultiUser() ? ws : undefined;
|
|
1159
|
+
sm.resumeSession(msg.cliSessionId, undefined, _rws);
|
|
1023
1160
|
});
|
|
1024
1161
|
return;
|
|
1025
1162
|
}
|
|
@@ -1056,14 +1193,23 @@ function createProjectContext(opts) {
|
|
|
1056
1193
|
|
|
1057
1194
|
if (msg.type === "switch_session") {
|
|
1058
1195
|
if (msg.id && sm.sessions.has(msg.id)) {
|
|
1059
|
-
|
|
1196
|
+
// Check access in multi-user mode
|
|
1197
|
+
if (usersModule.isMultiUser() && ws._clayUser) {
|
|
1198
|
+
var switchTarget = sm.sessions.get(msg.id);
|
|
1199
|
+
if (!usersModule.canAccessSession(ws._clayUser.id, switchTarget, { visibility: "public" })) return;
|
|
1200
|
+
ws._clayActiveSession = msg.id;
|
|
1201
|
+
sm.switchSession(msg.id, ws);
|
|
1202
|
+
broadcastPresence();
|
|
1203
|
+
} else {
|
|
1204
|
+
sm.switchSession(msg.id);
|
|
1205
|
+
}
|
|
1060
1206
|
}
|
|
1061
1207
|
return;
|
|
1062
1208
|
}
|
|
1063
1209
|
|
|
1064
1210
|
if (msg.type === "delete_session") {
|
|
1065
1211
|
if (msg.id && sm.sessions.has(msg.id)) {
|
|
1066
|
-
sm.deleteSession(msg.id);
|
|
1212
|
+
sm.deleteSession(msg.id, usersModule.isMultiUser() ? ws : undefined);
|
|
1067
1213
|
}
|
|
1068
1214
|
return;
|
|
1069
1215
|
}
|
|
@@ -1085,6 +1231,7 @@ function createProjectContext(opts) {
|
|
|
1085
1231
|
}
|
|
1086
1232
|
|
|
1087
1233
|
if (msg.type === "check_update") {
|
|
1234
|
+
if (usersModule.isMultiUser() && (!ws._clayUser || ws._clayUser.role !== "admin")) return;
|
|
1088
1235
|
fetchLatestVersion().then(function (v) {
|
|
1089
1236
|
if (v && isNewer(v, currentVersion)) {
|
|
1090
1237
|
latestVersion = v;
|
|
@@ -1095,6 +1242,7 @@ function createProjectContext(opts) {
|
|
|
1095
1242
|
}
|
|
1096
1243
|
|
|
1097
1244
|
if (msg.type === "update_now") {
|
|
1245
|
+
if (usersModule.isMultiUser() && (!ws._clayUser || ws._clayUser.role !== "admin")) return;
|
|
1098
1246
|
send({ type: "update_started", version: latestVersion || "" });
|
|
1099
1247
|
var _ipc = require("./ipc");
|
|
1100
1248
|
var _config = require("./config");
|
|
@@ -1353,7 +1501,7 @@ function createProjectContext(opts) {
|
|
|
1353
1501
|
onProcessingChanged();
|
|
1354
1502
|
|
|
1355
1503
|
sm.saveSessionFile(session);
|
|
1356
|
-
sm.switchSession(session.localId);
|
|
1504
|
+
sm.switchSession(session.localId, usersModule.isMultiUser() ? ws : undefined);
|
|
1357
1505
|
sm.sendAndRecord(session, { type: "rewind_complete", mode: mode });
|
|
1358
1506
|
sm.broadcastSessionList();
|
|
1359
1507
|
} catch (err) {
|
|
@@ -1672,7 +1820,18 @@ function createProjectContext(opts) {
|
|
|
1672
1820
|
return;
|
|
1673
1821
|
}
|
|
1674
1822
|
|
|
1675
|
-
// --- Daemon config
|
|
1823
|
+
// --- Daemon config / server management (admin-only in multi-user mode) ---
|
|
1824
|
+
if (msg.type === "get_daemon_config" || msg.type === "set_pin" || msg.type === "set_keep_awake" ||
|
|
1825
|
+
msg.type === "shutdown_server" || msg.type === "restart_server") {
|
|
1826
|
+
if (usersModule.isMultiUser()) {
|
|
1827
|
+
var _wsUser = ws._clayUser;
|
|
1828
|
+
if (!_wsUser || _wsUser.role !== "admin") {
|
|
1829
|
+
sendTo(ws, { type: "error", message: "Admin access required" });
|
|
1830
|
+
return;
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1676
1835
|
if (msg.type === "get_daemon_config") {
|
|
1677
1836
|
if (typeof opts.onGetDaemonConfig === "function") {
|
|
1678
1837
|
var daemonConfig = opts.onGetDaemonConfig();
|
|
@@ -1815,6 +1974,11 @@ function createProjectContext(opts) {
|
|
|
1815
1974
|
|
|
1816
1975
|
if (msg.type === "set_project_env") {
|
|
1817
1976
|
if (typeof opts.onSetProjectEnv === "function") {
|
|
1977
|
+
var envError = validateEnvString(msg.envrc || "");
|
|
1978
|
+
if (envError) {
|
|
1979
|
+
sendTo(ws, { type: "set_project_env_result", ok: false, slug: msg.slug, error: envError });
|
|
1980
|
+
return;
|
|
1981
|
+
}
|
|
1818
1982
|
var setResult = opts.onSetProjectEnv(msg.slug, msg.envrc || "");
|
|
1819
1983
|
sendTo(ws, { type: "set_project_env_result", ok: setResult.ok, slug: msg.slug, error: setResult.error });
|
|
1820
1984
|
} else {
|
|
@@ -1865,6 +2029,11 @@ function createProjectContext(opts) {
|
|
|
1865
2029
|
|
|
1866
2030
|
if (msg.type === "set_shared_env") {
|
|
1867
2031
|
if (typeof opts.onSetSharedEnv === "function") {
|
|
2032
|
+
var sharedEnvError = validateEnvString(msg.envrc || "");
|
|
2033
|
+
if (sharedEnvError) {
|
|
2034
|
+
sendTo(ws, { type: "set_shared_env_result", ok: false, error: sharedEnvError });
|
|
2035
|
+
return;
|
|
2036
|
+
}
|
|
1868
2037
|
var sharedSetResult = opts.onSetSharedEnv(msg.envrc || "");
|
|
1869
2038
|
sendTo(ws, { type: "set_shared_env_result", ok: sharedSetResult.ok, error: sharedSetResult.error });
|
|
1870
2039
|
} else {
|
|
@@ -2475,6 +2644,33 @@ function createProjectContext(opts) {
|
|
|
2475
2644
|
sm.broadcastSessionList();
|
|
2476
2645
|
}
|
|
2477
2646
|
|
|
2647
|
+
// --- Session presence (who is viewing which session) ---
|
|
2648
|
+
function broadcastPresence() {
|
|
2649
|
+
if (!usersModule.isMultiUser()) return;
|
|
2650
|
+
var presence = {};
|
|
2651
|
+
for (var c of clients) {
|
|
2652
|
+
if (!c._clayUser || !c._clayActiveSession) continue;
|
|
2653
|
+
var sid = c._clayActiveSession;
|
|
2654
|
+
if (!presence[sid]) presence[sid] = [];
|
|
2655
|
+
var u = c._clayUser;
|
|
2656
|
+
var p = u.profile || {};
|
|
2657
|
+
// Deduplicate: skip if this user is already listed for this session
|
|
2658
|
+
var dominated = false;
|
|
2659
|
+
for (var di = 0; di < presence[sid].length; di++) {
|
|
2660
|
+
if (presence[sid][di].id === u.id) { dominated = true; break; }
|
|
2661
|
+
}
|
|
2662
|
+
if (dominated) continue;
|
|
2663
|
+
presence[sid].push({
|
|
2664
|
+
id: u.id,
|
|
2665
|
+
displayName: p.name || u.displayName || u.username,
|
|
2666
|
+
username: u.username,
|
|
2667
|
+
avatarStyle: p.avatarStyle || "thumbs",
|
|
2668
|
+
avatarSeed: p.avatarSeed || u.username,
|
|
2669
|
+
});
|
|
2670
|
+
}
|
|
2671
|
+
send({ type: "session_presence", presence: presence });
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2478
2674
|
// --- WS disconnection handler ---
|
|
2479
2675
|
function handleDisconnection(ws) {
|
|
2480
2676
|
tm.detachAll(ws);
|
|
@@ -2484,6 +2680,7 @@ function createProjectContext(opts) {
|
|
|
2484
2680
|
stopAllDirWatches();
|
|
2485
2681
|
}
|
|
2486
2682
|
broadcastClientCount();
|
|
2683
|
+
broadcastPresence();
|
|
2487
2684
|
}
|
|
2488
2685
|
|
|
2489
2686
|
// --- Handle project-scoped HTTP requests ---
|
|
@@ -2635,6 +2832,18 @@ function createProjectContext(opts) {
|
|
|
2635
2832
|
res.end('{"error":"missing url, skill, or scope"}');
|
|
2636
2833
|
return;
|
|
2637
2834
|
}
|
|
2835
|
+
// Validate skill name: alphanumeric, hyphens, underscores only
|
|
2836
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(skill)) {
|
|
2837
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2838
|
+
res.end('{"error":"invalid skill name"}');
|
|
2839
|
+
return;
|
|
2840
|
+
}
|
|
2841
|
+
// Validate URL: must be https://
|
|
2842
|
+
if (!/^https:\/\//i.test(url)) {
|
|
2843
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2844
|
+
res.end('{"error":"only https:// URLs are allowed"}');
|
|
2845
|
+
return;
|
|
2846
|
+
}
|
|
2638
2847
|
var spawnCwd = scope === "global" ? os.homedir() : cwd;
|
|
2639
2848
|
var child = spawn("npx", ["skills", "add", url, "--skill", skill], {
|
|
2640
2849
|
cwd: spawnCwd,
|
|
@@ -2679,6 +2888,12 @@ function createProjectContext(opts) {
|
|
|
2679
2888
|
res.end('{"error":"missing skill or scope"}');
|
|
2680
2889
|
return;
|
|
2681
2890
|
}
|
|
2891
|
+
// Validate skill name
|
|
2892
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(skill)) {
|
|
2893
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2894
|
+
res.end('{"error":"invalid skill name"}');
|
|
2895
|
+
return;
|
|
2896
|
+
}
|
|
2682
2897
|
var baseDir = scope === "global" ? os.homedir() : cwd;
|
|
2683
2898
|
var skillDir = path.join(baseDir, ".claude", "skills", skill);
|
|
2684
2899
|
// Safety: ensure skillDir is inside the expected .claude/skills directory
|
|
@@ -2823,7 +3038,7 @@ function createProjectContext(opts) {
|
|
|
2823
3038
|
sm.sessions.forEach(function (s) {
|
|
2824
3039
|
if (s.isProcessing) hasProcessing = true;
|
|
2825
3040
|
});
|
|
2826
|
-
|
|
3041
|
+
var status = {
|
|
2827
3042
|
slug: slug,
|
|
2828
3043
|
path: cwd,
|
|
2829
3044
|
project: project,
|
|
@@ -2833,6 +3048,26 @@ function createProjectContext(opts) {
|
|
|
2833
3048
|
sessions: sessionCount,
|
|
2834
3049
|
isProcessing: hasProcessing,
|
|
2835
3050
|
};
|
|
3051
|
+
if (usersModule.isMultiUser()) {
|
|
3052
|
+
var seen = {};
|
|
3053
|
+
var onlineUsers = [];
|
|
3054
|
+
for (var c of clients) {
|
|
3055
|
+
if (!c._clayUser) continue;
|
|
3056
|
+
var u = c._clayUser;
|
|
3057
|
+
if (seen[u.id]) continue;
|
|
3058
|
+
seen[u.id] = true;
|
|
3059
|
+
var p = u.profile || {};
|
|
3060
|
+
onlineUsers.push({
|
|
3061
|
+
id: u.id,
|
|
3062
|
+
displayName: p.name || u.displayName || u.username,
|
|
3063
|
+
username: u.username,
|
|
3064
|
+
avatarStyle: p.avatarStyle || "thumbs",
|
|
3065
|
+
avatarSeed: p.avatarSeed || u.username,
|
|
3066
|
+
});
|
|
3067
|
+
}
|
|
3068
|
+
status.onlineUsers = onlineUsers;
|
|
3069
|
+
}
|
|
3070
|
+
return status;
|
|
2836
3071
|
}
|
|
2837
3072
|
|
|
2838
3073
|
function setTitle(newTitle) {
|
|
@@ -2853,6 +3088,11 @@ function createProjectContext(opts) {
|
|
|
2853
3088
|
sdk: sdk,
|
|
2854
3089
|
send: send,
|
|
2855
3090
|
sendTo: sendTo,
|
|
3091
|
+
forEachClient: function (fn) {
|
|
3092
|
+
for (var ws of clients) {
|
|
3093
|
+
if (ws.readyState === 1) fn(ws);
|
|
3094
|
+
}
|
|
3095
|
+
},
|
|
2856
3096
|
handleConnection: handleConnection,
|
|
2857
3097
|
handleMessage: handleMessage,
|
|
2858
3098
|
handleDisconnection: handleDisconnection,
|
|
@@ -2863,6 +3103,17 @@ function createProjectContext(opts) {
|
|
|
2863
3103
|
removeSchedule: function (id) { return loopRegistry.remove(id); },
|
|
2864
3104
|
setTitle: setTitle,
|
|
2865
3105
|
setIcon: setIcon,
|
|
3106
|
+
refreshUserProfile: function (userId) {
|
|
3107
|
+
var user = usersModule.findUserById(userId);
|
|
3108
|
+
if (!user) return;
|
|
3109
|
+
for (var ws of clients) {
|
|
3110
|
+
if (ws._clayUser && ws._clayUser.id === userId) {
|
|
3111
|
+
ws._clayUser = user;
|
|
3112
|
+
}
|
|
3113
|
+
}
|
|
3114
|
+
broadcastClientCount();
|
|
3115
|
+
broadcastPresence();
|
|
3116
|
+
},
|
|
2866
3117
|
warmup: function () {
|
|
2867
3118
|
sdk.warmup();
|
|
2868
3119
|
// Auto-install clay-ralph skill globally if not present
|
|
@@ -2904,4 +3155,4 @@ function parseJsonBody(req) {
|
|
|
2904
3155
|
});
|
|
2905
3156
|
}
|
|
2906
3157
|
|
|
2907
|
-
module.exports = { createProjectContext: createProjectContext };
|
|
3158
|
+
module.exports = { createProjectContext: createProjectContext, safePath: safePath, validateEnvString: validateEnvString };
|
package/lib/public/app.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { showToast, copyToClipboard, escapeHtml } from './modules/utils.js';
|
|
2
2
|
import { refreshIcons, iconHtml, randomThinkingVerb } from './modules/icons.js';
|
|
3
|
-
import { renderMarkdown, highlightCodeBlocks, renderMermaidBlocks, closeMermaidModal } from './modules/markdown.js';
|
|
4
|
-
import { initSidebar, renderSessionList, handleSearchResults, updatePageTitle, getActiveSearchQuery, buildSearchTimeline, removeSearchTimeline, populateCliSessionList, renderIconStrip, initIconStrip, getEmojiCategories } from './modules/sidebar.js';
|
|
3
|
+
import { renderMarkdown, highlightCodeBlocks, renderMermaidBlocks, closeMermaidModal, parseEmojis } from './modules/markdown.js';
|
|
4
|
+
import { initSidebar, renderSessionList, handleSearchResults, updateSessionPresence, updatePageTitle, getActiveSearchQuery, buildSearchTimeline, removeSearchTimeline, populateCliSessionList, renderIconStrip, renderSidebarPresence, initIconStrip, getEmojiCategories } from './modules/sidebar.js';
|
|
5
5
|
import { initRewind, setRewindMode, showRewindModal, clearPendingRewindUuid, addRewindButton } from './modules/rewind.js';
|
|
6
6
|
import { initNotifications, showDoneNotification, playDoneSound, isNotifAlertEnabled, isNotifSoundEnabled } from './modules/notifications.js';
|
|
7
7
|
import { initInput, clearPendingImages, handleInputSync, autoResize, builtinCommands, sendMessage } from './modules/input.js';
|
|
@@ -19,6 +19,7 @@ import { initAsciiLogo, startLogoAnimation, stopLogoAnimation } from './modules/
|
|
|
19
19
|
import { initPlaybook, openPlaybook, getPlaybooks, getPlaybookForTip, isCompleted as isPlaybookCompleted } from './modules/playbook.js';
|
|
20
20
|
import { initSTT } from './modules/stt.js';
|
|
21
21
|
import { initProfile } from './modules/profile.js';
|
|
22
|
+
import { initAdmin, checkAdminAccess } from './modules/admin.js';
|
|
22
23
|
|
|
23
24
|
// --- Base path for multi-project routing ---
|
|
24
25
|
var slugMatch = location.pathname.match(/^\/p\/([a-z0-9_-]+)/);
|
|
@@ -598,12 +599,37 @@ import { initProfile } from './modules/profile.js';
|
|
|
598
599
|
} else if (projectHint) {
|
|
599
600
|
projectHint.classList.add("hidden");
|
|
600
601
|
}
|
|
602
|
+
// Update topbar with server-wide presence
|
|
603
|
+
if (msg.serverUsers) {
|
|
604
|
+
renderTopbarPresence(msg.serverUsers);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function renderTopbarPresence(serverUsers) {
|
|
609
|
+
var countEl = document.getElementById("client-count");
|
|
610
|
+
if (!countEl) return;
|
|
611
|
+
if (serverUsers.length > 1) {
|
|
612
|
+
countEl.innerHTML = "";
|
|
613
|
+
for (var cui = 0; cui < serverUsers.length; cui++) {
|
|
614
|
+
var cu = serverUsers[cui];
|
|
615
|
+
var cuImg = document.createElement("img");
|
|
616
|
+
cuImg.className = "client-avatar";
|
|
617
|
+
cuImg.src = "https://api.dicebear.com/9.x/" + (cu.avatarStyle || "thumbs") + "/svg?seed=" + encodeURIComponent(cu.avatarSeed || cu.username) + "&size=24";
|
|
618
|
+
cuImg.alt = cu.displayName;
|
|
619
|
+
cuImg.dataset.tip = cu.displayName + " (@" + cu.username + ")";
|
|
620
|
+
if (cui > 0) cuImg.style.marginLeft = "-6px";
|
|
621
|
+
countEl.appendChild(cuImg);
|
|
622
|
+
}
|
|
623
|
+
countEl.classList.remove("hidden");
|
|
624
|
+
} else {
|
|
625
|
+
countEl.classList.add("hidden");
|
|
626
|
+
}
|
|
601
627
|
}
|
|
602
628
|
|
|
603
629
|
function renderProjectList() {
|
|
604
630
|
// Render icon strip projects
|
|
605
631
|
var iconStripProjects = cachedProjects.map(function (p) {
|
|
606
|
-
return { slug: p.slug, name: p.title || p.project, icon: p.icon || null, isProcessing: p.isProcessing };
|
|
632
|
+
return { slug: p.slug, name: p.title || p.project, icon: p.icon || null, isProcessing: p.isProcessing, onlineUsers: p.onlineUsers || [] };
|
|
607
633
|
});
|
|
608
634
|
renderIconStrip(iconStripProjects, currentSlug);
|
|
609
635
|
// Update title bar project name and icon if it changed
|
|
@@ -617,7 +643,7 @@ import { initProfile } from './modules/profile.js';
|
|
|
617
643
|
var pIcon = cachedProjects[pi].icon || null;
|
|
618
644
|
if (pIcon) {
|
|
619
645
|
tbIcon.textContent = pIcon;
|
|
620
|
-
|
|
646
|
+
parseEmojis(tbIcon);
|
|
621
647
|
tbIcon.classList.add("has-icon");
|
|
622
648
|
try { localStorage.setItem("clay-project-icon-" + (currentSlug || "default"), pIcon); } catch (e) {}
|
|
623
649
|
} else {
|
|
@@ -736,7 +762,7 @@ import { initProfile } from './modules/profile.js';
|
|
|
736
762
|
var _tbi = $("title-bar-project-icon");
|
|
737
763
|
if (_tbi) {
|
|
738
764
|
_tbi.textContent = _cachedProjectIcon;
|
|
739
|
-
|
|
765
|
+
parseEmojis(_tbi);
|
|
740
766
|
_tbi.classList.add("has-icon");
|
|
741
767
|
}
|
|
742
768
|
}
|
|
@@ -936,6 +962,8 @@ import { initProfile } from './modules/profile.js';
|
|
|
936
962
|
showHomeHub: function () { showHomeHub(); },
|
|
937
963
|
openRalphWizard: function () { openRalphWizard(); },
|
|
938
964
|
getUpcomingSchedules: getUpcomingSchedules,
|
|
965
|
+
get multiUser() { return isMultiUserMode; },
|
|
966
|
+
get myUserId() { return myUserId; },
|
|
939
967
|
};
|
|
940
968
|
initSidebar(sidebarCtx);
|
|
941
969
|
initIconStrip(sidebarCtx);
|
|
@@ -2615,14 +2643,20 @@ import { initProfile } from './modules/profile.js';
|
|
|
2615
2643
|
break;
|
|
2616
2644
|
|
|
2617
2645
|
case "client_count":
|
|
2618
|
-
|
|
2619
|
-
if (
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2646
|
+
// Sidebar presence: current project's online users
|
|
2647
|
+
if (msg.users) {
|
|
2648
|
+
renderSidebarPresence(msg.users);
|
|
2649
|
+
}
|
|
2650
|
+
// Non-multi-user mode: simple count in topbar
|
|
2651
|
+
if (!msg.users) {
|
|
2652
|
+
var countEl = document.getElementById("client-count");
|
|
2653
|
+
if (countEl) {
|
|
2654
|
+
if (msg.count > 1) {
|
|
2655
|
+
countEl.textContent = msg.count;
|
|
2656
|
+
countEl.classList.remove("hidden");
|
|
2657
|
+
} else {
|
|
2658
|
+
countEl.classList.add("hidden");
|
|
2659
|
+
}
|
|
2626
2660
|
}
|
|
2627
2661
|
}
|
|
2628
2662
|
break;
|
|
@@ -2693,6 +2727,10 @@ import { initProfile } from './modules/profile.js';
|
|
|
2693
2727
|
renderSessionList(msg.sessions || []);
|
|
2694
2728
|
break;
|
|
2695
2729
|
|
|
2730
|
+
case "session_presence":
|
|
2731
|
+
updateSessionPresence(msg.presence || {});
|
|
2732
|
+
break;
|
|
2733
|
+
|
|
2696
2734
|
case "session_io":
|
|
2697
2735
|
blinkSessionDot(msg.id);
|
|
2698
2736
|
break;
|
|
@@ -3453,6 +3491,26 @@ import { initProfile } from './modules/profile.js';
|
|
|
3453
3491
|
basePath: basePath,
|
|
3454
3492
|
});
|
|
3455
3493
|
|
|
3494
|
+
// --- Admin (multi-user mode) ---
|
|
3495
|
+
var isMultiUserMode = false;
|
|
3496
|
+
var myUserId = null;
|
|
3497
|
+
initAdmin({
|
|
3498
|
+
get projectList() { return cachedProjects; },
|
|
3499
|
+
});
|
|
3500
|
+
fetch("/api/me").then(function (r) { return r.json(); }).then(function (d) {
|
|
3501
|
+
if (d.multiUser) isMultiUserMode = true;
|
|
3502
|
+
if (d.user && d.user.id) myUserId = d.user.id;
|
|
3503
|
+
}).catch(function () {});
|
|
3504
|
+
// Hide server settings and update controls for non-admin users in multi-user mode
|
|
3505
|
+
checkAdminAccess().then(function (isAdmin) {
|
|
3506
|
+
if (isMultiUserMode && !isAdmin) {
|
|
3507
|
+
var settingsBtn = document.getElementById("server-settings-btn");
|
|
3508
|
+
if (settingsBtn) settingsBtn.style.display = "none";
|
|
3509
|
+
var updatePill = document.getElementById("update-pill-wrap");
|
|
3510
|
+
if (updatePill) updatePill.style.display = "none";
|
|
3511
|
+
}
|
|
3512
|
+
});
|
|
3513
|
+
|
|
3456
3514
|
// --- Notifications module (viewport, banners, notifications, debug, service worker) ---
|
|
3457
3515
|
initNotifications({
|
|
3458
3516
|
$: $,
|
|
@@ -3725,12 +3783,14 @@ import { initProfile } from './modules/profile.js';
|
|
|
3725
3783
|
|
|
3726
3784
|
function updateRalphBars() {
|
|
3727
3785
|
var onCraftingSession = ralphCraftingSessionId && activeSessionId === ralphCraftingSessionId;
|
|
3786
|
+
// If approval phase but no craftingSessionId (recovered after server restart), show bar anyway
|
|
3787
|
+
var recoveredApproval = ralphPhase === "approval" && !ralphCraftingSessionId;
|
|
3728
3788
|
if (ralphPhase === "crafting" && onCraftingSession) {
|
|
3729
3789
|
showRalphCraftingBar(true);
|
|
3730
3790
|
} else {
|
|
3731
3791
|
showRalphCraftingBar(false);
|
|
3732
3792
|
}
|
|
3733
|
-
if (ralphPhase === "approval" && onCraftingSession) {
|
|
3793
|
+
if (ralphPhase === "approval" && (onCraftingSession || recoveredApproval)) {
|
|
3734
3794
|
showRalphApprovalBar(true);
|
|
3735
3795
|
} else {
|
|
3736
3796
|
showRalphApprovalBar(false);
|