clay-server 2.8.2 → 2.9.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/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 +671 -27
- package/lib/project.js +280 -42
- package/lib/public/app.js +321 -84
- package/lib/public/css/admin.css +576 -0
- package/lib/public/css/base.css +3 -0
- package/lib/public/css/filebrowser.css +8 -1
- package/lib/public/css/home-hub.css +1 -1
- package/lib/public/css/icon-strip.css +1 -0
- package/lib/public/css/input.css +4 -0
- package/lib/public/css/menus.css +16 -51
- package/lib/public/css/messages.css +1 -0
- package/lib/public/css/mobile-nav.css +2 -2
- package/lib/public/css/overlays.css +177 -20
- package/lib/public/css/scheduler.css +1 -0
- package/lib/public/css/server-settings.css +37 -34
- package/lib/public/css/sidebar.css +49 -0
- package/lib/public/css/title-bar.css +45 -8
- package/lib/public/index.html +96 -25
- package/lib/public/manifest.json +2 -2
- package/lib/public/modules/admin.js +631 -0
- package/lib/public/modules/markdown.js +9 -5
- package/lib/public/modules/notifications.js +15 -103
- package/lib/public/modules/profile.js +21 -0
- package/lib/public/modules/project-settings.js +4 -1
- package/lib/public/modules/scheduler.js +55 -48
- package/lib/public/modules/server-settings.js +26 -4
- 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/README.md
CHANGED
|
@@ -276,6 +276,8 @@ If you are using claude-relay, let us know how you are using it in Discussions:
|
|
|
276
276
|
|
|
277
277
|
This is an independent project and is not affiliated with Anthropic. Claude is a trademark of Anthropic.
|
|
278
278
|
|
|
279
|
+
Clay is provided "as is" without warranty of any kind. Users are responsible for ensuring their use of this software complies with all applicable terms of service of the underlying AI providers (e.g., Anthropic, OpenAI) and any other third-party services. Features such as multi-user mode are experimental and may involve sharing access to API-backed services — please review your provider's usage policies regarding account sharing, acceptable use, and any applicable rate limits or restrictions before enabling such features. The authors assume no liability for any misuse or violations arising from the use of this software.
|
|
280
|
+
|
|
279
281
|
## License
|
|
280
282
|
|
|
281
283
|
MIT
|
package/bin/cli.js
CHANGED
|
@@ -28,6 +28,7 @@ if (_isDev) process.env.CLAY_DEV = "1";
|
|
|
28
28
|
var { loadConfig, saveConfig, configPath, socketPath, logPath, ensureConfigDir, isDaemonAlive, isDaemonAliveAsync, generateSlug, clearStaleConfig, loadClayrc, saveClayrc, readCrashInfo } = require("../lib/config");
|
|
29
29
|
var { sendIPCCommand } = require("../lib/ipc");
|
|
30
30
|
var { generateAuthToken } = require("../lib/server");
|
|
31
|
+
var { enableMultiUser, hasAdmin, isMultiUser } = require("../lib/users");
|
|
31
32
|
|
|
32
33
|
function openUrl(url) {
|
|
33
34
|
try {
|
|
@@ -56,6 +57,7 @@ var dangerouslySkipPermissions = false;
|
|
|
56
57
|
var headlessMode = false;
|
|
57
58
|
var watchMode = false;
|
|
58
59
|
var host = null;
|
|
60
|
+
var multiUserMode = false;
|
|
59
61
|
|
|
60
62
|
for (var i = 0; i < args.length; i++) {
|
|
61
63
|
if (args[i] === "-p" || args[i] === "--port") {
|
|
@@ -100,6 +102,8 @@ for (var i = 0; i < args.length; i++) {
|
|
|
100
102
|
autoYes = true;
|
|
101
103
|
} else if (args[i] === "--dangerously-skip-permissions") {
|
|
102
104
|
dangerouslySkipPermissions = true;
|
|
105
|
+
} else if (args[i] === "--multi-user") {
|
|
106
|
+
multiUserMode = true;
|
|
103
107
|
} else if (args[i] === "-h" || args[i] === "--help") {
|
|
104
108
|
console.log("Usage: clay-server [-p|--port <port>] [--host <address>] [--no-https] [--no-update] [--debug] [-y|--yes] [--pin <pin>] [--shutdown] [--restart]");
|
|
105
109
|
console.log(" clay-server --add <path> Add a project to the running daemon");
|
|
@@ -120,6 +124,7 @@ for (var i = 0; i < args.length; i++) {
|
|
|
120
124
|
console.log(" --remove <path> Remove a project directory");
|
|
121
125
|
console.log(" --list List all registered projects");
|
|
122
126
|
console.log(" --headless Start daemon and exit immediately (implies --yes)");
|
|
127
|
+
console.log(" --multi-user Enable multi-user mode (generates setup code)");
|
|
123
128
|
console.log(" --dangerously-skip-permissions");
|
|
124
129
|
console.log(" Bypass all permission prompts");
|
|
125
130
|
process.exit(0);
|
|
@@ -258,6 +263,34 @@ if (listMode) {
|
|
|
258
263
|
return;
|
|
259
264
|
}
|
|
260
265
|
|
|
266
|
+
// --- Handle --multi-user before anything else ---
|
|
267
|
+
if (multiUserMode) {
|
|
268
|
+
var muResult = enableMultiUser();
|
|
269
|
+
if (muResult.alreadyEnabled && muResult.hasAdmin) {
|
|
270
|
+
console.log("");
|
|
271
|
+
console.log("Multi-user mode is already enabled and an admin account exists.");
|
|
272
|
+
console.log("No changes made.");
|
|
273
|
+
console.log("");
|
|
274
|
+
} else if (muResult.setupCode) {
|
|
275
|
+
console.log("");
|
|
276
|
+
console.log("\x1b[33m⚠ Experimental Feature\x1b[0m");
|
|
277
|
+
console.log("");
|
|
278
|
+
console.log(" Multi-user mode is experimental and may change in future releases.");
|
|
279
|
+
console.log(" Sharing access to AI-powered tools may be subject to your provider's");
|
|
280
|
+
console.log(" terms of service. Please review the applicable usage policies before");
|
|
281
|
+
console.log(" granting access to other users.");
|
|
282
|
+
console.log("");
|
|
283
|
+
console.log("\x1b[32mMulti-user mode enabled.\x1b[0m");
|
|
284
|
+
console.log("");
|
|
285
|
+
console.log("Setup code: \x1b[1m" + muResult.setupCode + "\x1b[0m");
|
|
286
|
+
console.log("");
|
|
287
|
+
console.log("Open Clay in your browser and enter this code to create the admin account.");
|
|
288
|
+
console.log("The code is single-use and will be cleared once the admin is set up.");
|
|
289
|
+
console.log("");
|
|
290
|
+
}
|
|
291
|
+
process.exit(0);
|
|
292
|
+
}
|
|
293
|
+
|
|
261
294
|
var cwd = process.cwd();
|
|
262
295
|
|
|
263
296
|
// --- ANSI helpers ---
|
|
@@ -1312,6 +1345,15 @@ async function forkDaemon(pin, keepAwake, extraProjects, addCwd) {
|
|
|
1312
1345
|
var allProjects = [];
|
|
1313
1346
|
var usedSlugs = [];
|
|
1314
1347
|
|
|
1348
|
+
// Load previous config to preserve per-project settings (visibility, allowedUsers)
|
|
1349
|
+
var prevConfig = loadConfig();
|
|
1350
|
+
var prevProjectMap = {};
|
|
1351
|
+
if (prevConfig && prevConfig.projects) {
|
|
1352
|
+
for (var pi = 0; pi < prevConfig.projects.length; pi++) {
|
|
1353
|
+
prevProjectMap[prevConfig.projects[pi].path] = prevConfig.projects[pi];
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1315
1357
|
// Only include cwd if explicitly requested
|
|
1316
1358
|
if (addCwd) {
|
|
1317
1359
|
var slug = generateSlug(cwd, []);
|
|
@@ -1326,6 +1368,11 @@ async function forkDaemon(pin, keepAwake, extraProjects, addCwd) {
|
|
|
1326
1368
|
break;
|
|
1327
1369
|
}
|
|
1328
1370
|
}
|
|
1371
|
+
// Restore access settings from previous config
|
|
1372
|
+
if (prevProjectMap[cwd]) {
|
|
1373
|
+
if (prevProjectMap[cwd].visibility) cwdEntry.visibility = prevProjectMap[cwd].visibility;
|
|
1374
|
+
if (prevProjectMap[cwd].allowedUsers) cwdEntry.allowedUsers = prevProjectMap[cwd].allowedUsers;
|
|
1375
|
+
}
|
|
1329
1376
|
allProjects.push(cwdEntry);
|
|
1330
1377
|
usedSlugs.push(slug);
|
|
1331
1378
|
}
|
|
@@ -1338,7 +1385,13 @@ async function forkDaemon(pin, keepAwake, extraProjects, addCwd) {
|
|
|
1338
1385
|
if (!fs.existsSync(rp.path)) continue; // skip missing directories
|
|
1339
1386
|
var rpSlug = generateSlug(rp.path, usedSlugs);
|
|
1340
1387
|
usedSlugs.push(rpSlug);
|
|
1341
|
-
|
|
1388
|
+
var rpEntry = { path: rp.path, slug: rpSlug, title: rp.title || undefined, icon: rp.icon || undefined, addedAt: rp.addedAt || Date.now() };
|
|
1389
|
+
// Restore access settings from previous config
|
|
1390
|
+
if (prevProjectMap[rp.path]) {
|
|
1391
|
+
if (prevProjectMap[rp.path].visibility) rpEntry.visibility = prevProjectMap[rp.path].visibility;
|
|
1392
|
+
if (prevProjectMap[rp.path].allowedUsers) rpEntry.allowedUsers = prevProjectMap[rp.path].allowedUsers;
|
|
1393
|
+
}
|
|
1394
|
+
allProjects.push(rpEntry);
|
|
1342
1395
|
}
|
|
1343
1396
|
}
|
|
1344
1397
|
|
|
@@ -1429,6 +1482,15 @@ async function devMode(pin, keepAwake, existingPinHash) {
|
|
|
1429
1482
|
var slug = generateSlug(cwd, []);
|
|
1430
1483
|
var cwdDevEntry = { path: cwd, slug: slug, addedAt: Date.now() };
|
|
1431
1484
|
|
|
1485
|
+
// Load previous config to preserve per-project settings (visibility, allowedUsers)
|
|
1486
|
+
var prevDevConfig = loadConfig();
|
|
1487
|
+
var prevDevProjectMap = {};
|
|
1488
|
+
if (prevDevConfig && prevDevConfig.projects) {
|
|
1489
|
+
for (var pdi = 0; pdi < prevDevConfig.projects.length; pdi++) {
|
|
1490
|
+
prevDevProjectMap[prevDevConfig.projects[pdi].path] = prevDevConfig.projects[pdi];
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1432
1494
|
// Restore previous projects
|
|
1433
1495
|
var rc = loadClayrc();
|
|
1434
1496
|
var restorable = (rc.recentProjects || []).filter(function (p) {
|
|
@@ -1443,13 +1505,24 @@ async function devMode(pin, keepAwake, existingPinHash) {
|
|
|
1443
1505
|
break;
|
|
1444
1506
|
}
|
|
1445
1507
|
}
|
|
1508
|
+
// Restore access settings for cwd from previous config
|
|
1509
|
+
if (prevDevProjectMap[cwd]) {
|
|
1510
|
+
if (prevDevProjectMap[cwd].visibility) cwdDevEntry.visibility = prevDevProjectMap[cwd].visibility;
|
|
1511
|
+
if (prevDevProjectMap[cwd].allowedUsers) cwdDevEntry.allowedUsers = prevDevProjectMap[cwd].allowedUsers;
|
|
1512
|
+
}
|
|
1446
1513
|
var allProjects = [cwdDevEntry];
|
|
1447
1514
|
var usedSlugs = [slug];
|
|
1448
1515
|
for (var ri = 0; ri < restorable.length; ri++) {
|
|
1449
1516
|
var rp = restorable[ri];
|
|
1450
1517
|
var rpSlug = generateSlug(rp.path, usedSlugs);
|
|
1451
1518
|
usedSlugs.push(rpSlug);
|
|
1452
|
-
|
|
1519
|
+
var rpDevEntry = { path: rp.path, slug: rpSlug, title: rp.title || undefined, icon: rp.icon || undefined, addedAt: rp.addedAt || Date.now() };
|
|
1520
|
+
// Restore access settings from previous config
|
|
1521
|
+
if (prevDevProjectMap[rp.path]) {
|
|
1522
|
+
if (prevDevProjectMap[rp.path].visibility) rpDevEntry.visibility = prevDevProjectMap[rp.path].visibility;
|
|
1523
|
+
if (prevDevProjectMap[rp.path].allowedUsers) rpDevEntry.allowedUsers = prevDevProjectMap[rp.path].allowedUsers;
|
|
1524
|
+
}
|
|
1525
|
+
allProjects.push(rpDevEntry);
|
|
1453
1526
|
}
|
|
1454
1527
|
|
|
1455
1528
|
var config = {
|
|
@@ -2222,7 +2295,13 @@ function showSettingsMenu(config, ip) {
|
|
|
2222
2295
|
log(sym.bar + " Tailscale " + tsStatus);
|
|
2223
2296
|
log(sym.bar + " mkcert " + mcStatus);
|
|
2224
2297
|
log(sym.bar + " HTTPS " + tlsStatus);
|
|
2298
|
+
var muEnabled = isMultiUser();
|
|
2299
|
+
var muStatus = muEnabled
|
|
2300
|
+
? a.green + "Enabled" + a.reset
|
|
2301
|
+
: a.dim + "Off" + a.reset;
|
|
2302
|
+
|
|
2225
2303
|
log(sym.bar + " PIN " + pinStatus);
|
|
2304
|
+
log(sym.bar + " Multi-user " + muStatus);
|
|
2226
2305
|
if (process.platform === "darwin") {
|
|
2227
2306
|
log(sym.bar + " Keep awake " + awakeStatus);
|
|
2228
2307
|
}
|
|
@@ -2239,6 +2318,11 @@ function showSettingsMenu(config, ip) {
|
|
|
2239
2318
|
} else {
|
|
2240
2319
|
items.push({ label: "Set PIN", value: "pin" });
|
|
2241
2320
|
}
|
|
2321
|
+
if (muEnabled) {
|
|
2322
|
+
items.push({ label: "Multi-user mode (enabled)", value: "multi_user" });
|
|
2323
|
+
} else {
|
|
2324
|
+
items.push({ label: "Enable multi-user mode", value: "multi_user" });
|
|
2325
|
+
}
|
|
2242
2326
|
if (process.platform === "darwin") {
|
|
2243
2327
|
items.push({ label: isAwake ? "Disable keep awake" : "Enable keep awake", value: "awake" });
|
|
2244
2328
|
}
|
|
@@ -2280,6 +2364,42 @@ function showSettingsMenu(config, ip) {
|
|
|
2280
2364
|
});
|
|
2281
2365
|
break;
|
|
2282
2366
|
|
|
2367
|
+
case "multi_user":
|
|
2368
|
+
if (muEnabled && hasAdmin()) {
|
|
2369
|
+
log(sym.bar);
|
|
2370
|
+
log(sym.bar + " " + a.dim + "Multi-user mode is already enabled and an admin account exists." + a.reset);
|
|
2371
|
+
log(sym.bar + " " + a.dim + "No changes made." + a.reset);
|
|
2372
|
+
log(sym.bar);
|
|
2373
|
+
promptSelect("Back?", [{ label: "Back", value: "back" }], function () {
|
|
2374
|
+
showSettingsMenu(config, ip);
|
|
2375
|
+
});
|
|
2376
|
+
} else {
|
|
2377
|
+
var muResult = enableMultiUser();
|
|
2378
|
+
log(sym.bar);
|
|
2379
|
+
log(sym.bar + " " + a.yellow + sym.warn + " Experimental Feature" + a.reset);
|
|
2380
|
+
log(sym.bar);
|
|
2381
|
+
log(sym.bar + " " + a.dim + "Multi-user mode is experimental and may change in future releases." + a.reset);
|
|
2382
|
+
log(sym.bar + " " + a.dim + "Sharing access to AI-powered tools may be subject to your provider's" + a.reset);
|
|
2383
|
+
log(sym.bar + " " + a.dim + "terms of service. Please review the applicable usage policies before" + a.reset);
|
|
2384
|
+
log(sym.bar + " " + a.dim + "granting access to other users." + a.reset);
|
|
2385
|
+
log(sym.bar);
|
|
2386
|
+
if (muResult.setupCode) {
|
|
2387
|
+
log(sym.bar + " " + a.green + "Multi-user mode enabled." + a.reset);
|
|
2388
|
+
log(sym.bar);
|
|
2389
|
+
log(sym.bar + " Setup code: " + a.bold + muResult.setupCode + a.reset);
|
|
2390
|
+
log(sym.bar);
|
|
2391
|
+
log(sym.bar + " " + a.dim + "Open Clay in your browser and enter this code to create the admin account." + a.reset);
|
|
2392
|
+
log(sym.bar + " " + a.dim + "The code is single-use and will be cleared once the admin is set up." + a.reset);
|
|
2393
|
+
} else {
|
|
2394
|
+
log(sym.bar + " " + a.dim + "Multi-user mode is already enabled." + a.reset);
|
|
2395
|
+
}
|
|
2396
|
+
log(sym.bar);
|
|
2397
|
+
promptSelect("Back?", [{ label: "Back", value: "back" }], function () {
|
|
2398
|
+
showSettingsMenu(config, ip);
|
|
2399
|
+
});
|
|
2400
|
+
}
|
|
2401
|
+
break;
|
|
2402
|
+
|
|
2283
2403
|
case "logs":
|
|
2284
2404
|
console.clear();
|
|
2285
2405
|
log(a.bold + "Daemon logs" + a.reset + " " + a.dim + "(" + logPath() + ")" + a.reset);
|
package/lib/config.js
CHANGED
|
@@ -89,8 +89,14 @@ function logPath() {
|
|
|
89
89
|
return path.join(CONFIG_DIR, _devMode ? "daemon-dev.log" : "daemon.log");
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
function chmodSafe(filePath, mode) {
|
|
93
|
+
if (process.platform === "win32") return;
|
|
94
|
+
try { fs.chmodSync(filePath, mode); } catch (e) {}
|
|
95
|
+
}
|
|
96
|
+
|
|
92
97
|
function ensureConfigDir() {
|
|
93
98
|
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
99
|
+
chmodSafe(CONFIG_DIR, 0o700);
|
|
94
100
|
}
|
|
95
101
|
|
|
96
102
|
function loadConfig() {
|
|
@@ -106,7 +112,9 @@ function saveConfig(config) {
|
|
|
106
112
|
ensureConfigDir();
|
|
107
113
|
var tmpPath = configPath() + ".tmp";
|
|
108
114
|
fs.writeFileSync(tmpPath, JSON.stringify(config, null, 2));
|
|
115
|
+
chmodSafe(tmpPath, 0o600);
|
|
109
116
|
fs.renameSync(tmpPath, configPath());
|
|
117
|
+
chmodSafe(configPath(), 0o600);
|
|
110
118
|
}
|
|
111
119
|
|
|
112
120
|
function isPidAlive(pid) {
|
|
@@ -173,7 +181,17 @@ function generateSlug(projectPath, existingSlugs) {
|
|
|
173
181
|
}
|
|
174
182
|
|
|
175
183
|
function clearStaleConfig() {
|
|
176
|
-
|
|
184
|
+
// Clear pid from config instead of deleting the file (preserves project settings)
|
|
185
|
+
try {
|
|
186
|
+
var data = fs.readFileSync(configPath(), "utf8");
|
|
187
|
+
var cfg = JSON.parse(data);
|
|
188
|
+
cfg.pid = null;
|
|
189
|
+
var tmpPath = configPath() + ".tmp";
|
|
190
|
+
fs.writeFileSync(tmpPath, JSON.stringify(cfg, null, 2));
|
|
191
|
+
chmodSafe(tmpPath, 0o600);
|
|
192
|
+
fs.renameSync(tmpPath, configPath());
|
|
193
|
+
chmodSafe(configPath(), 0o600);
|
|
194
|
+
} catch (e) {}
|
|
177
195
|
if (process.platform !== "win32") {
|
|
178
196
|
try { fs.unlinkSync(socketPath()); } catch (e) {}
|
|
179
197
|
}
|
|
@@ -316,4 +334,5 @@ module.exports = {
|
|
|
316
334
|
saveClayrc: saveClayrc,
|
|
317
335
|
syncClayrc: syncClayrc,
|
|
318
336
|
removeFromClayrc: removeFromClayrc,
|
|
337
|
+
chmodSafe: chmodSafe,
|
|
319
338
|
};
|
package/lib/daemon.js
CHANGED
|
@@ -368,6 +368,12 @@ var relay = createServer({
|
|
|
368
368
|
console.log("[daemon] PIN", pin ? "set" : "removed", "(web)");
|
|
369
369
|
return { ok: true, pinEnabled: !!config.pinHash };
|
|
370
370
|
},
|
|
371
|
+
onUpgradePin: function (newHash) {
|
|
372
|
+
config.pinHash = newHash;
|
|
373
|
+
relay.setAuthToken(newHash);
|
|
374
|
+
saveConfig(config);
|
|
375
|
+
console.log("[daemon] PIN hash auto-upgraded to scrypt");
|
|
376
|
+
},
|
|
371
377
|
onSetKeepAwake: function (value) {
|
|
372
378
|
var want = !!value;
|
|
373
379
|
config.keepAwake = want;
|
|
@@ -393,6 +399,40 @@ var relay = createServer({
|
|
|
393
399
|
console.log("[daemon] Restart requested via web UI");
|
|
394
400
|
spawnAndRestart();
|
|
395
401
|
},
|
|
402
|
+
onSetProjectVisibility: function (slug, visibility) {
|
|
403
|
+
for (var i = 0; i < config.projects.length; i++) {
|
|
404
|
+
if (config.projects[i].slug === slug) {
|
|
405
|
+
config.projects[i].visibility = visibility;
|
|
406
|
+
saveConfig(config);
|
|
407
|
+
console.log("[daemon] Set project visibility:", slug, "→", visibility);
|
|
408
|
+
return { ok: true };
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return { error: "Project not found" };
|
|
412
|
+
},
|
|
413
|
+
onSetProjectAllowedUsers: function (slug, allowedUsers) {
|
|
414
|
+
for (var i = 0; i < config.projects.length; i++) {
|
|
415
|
+
if (config.projects[i].slug === slug) {
|
|
416
|
+
config.projects[i].allowedUsers = allowedUsers;
|
|
417
|
+
saveConfig(config);
|
|
418
|
+
console.log("[daemon] Set project allowed users:", slug, "→", allowedUsers.length, "users");
|
|
419
|
+
return { ok: true };
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return { error: "Project not found" };
|
|
423
|
+
},
|
|
424
|
+
onGetProjectAccess: function (slug) {
|
|
425
|
+
for (var i = 0; i < config.projects.length; i++) {
|
|
426
|
+
if (config.projects[i].slug === slug) {
|
|
427
|
+
return {
|
|
428
|
+
slug: slug,
|
|
429
|
+
visibility: config.projects[i].visibility || "public",
|
|
430
|
+
allowedUsers: config.projects[i].allowedUsers || [],
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return { error: "Project not found" };
|
|
435
|
+
},
|
|
396
436
|
});
|
|
397
437
|
|
|
398
438
|
// --- Register projects ---
|