clay-server 2.9.0 → 2.9.2

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 CHANGED
@@ -28,7 +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
+ var { enableMultiUser, disableMultiUser, hasAdmin, isMultiUser } = require("../lib/users");
32
32
 
33
33
  function openUrl(url) {
34
34
  try {
@@ -2319,7 +2319,7 @@ function showSettingsMenu(config, ip) {
2319
2319
  items.push({ label: "Set PIN", value: "pin" });
2320
2320
  }
2321
2321
  if (muEnabled) {
2322
- items.push({ label: "Multi-user mode (enabled)", value: "multi_user" });
2322
+ items.push({ label: "Disable multi-user mode", value: "disable_multi_user" });
2323
2323
  } else {
2324
2324
  items.push({ label: "Enable multi-user mode", value: "multi_user" });
2325
2325
  }
@@ -2365,39 +2365,51 @@ function showSettingsMenu(config, ip) {
2365
2365
  break;
2366
2366
 
2367
2367
  case "multi_user":
2368
- if (muEnabled && hasAdmin()) {
2368
+ var muResult = enableMultiUser();
2369
+ log(sym.bar);
2370
+ log(sym.bar + " " + a.yellow + sym.warn + " Experimental Feature" + a.reset);
2371
+ log(sym.bar);
2372
+ log(sym.bar + " " + a.dim + "Multi-user mode is experimental and may change in future releases." + a.reset);
2373
+ log(sym.bar + " " + a.dim + "Sharing access to AI-powered tools may be subject to your provider's" + a.reset);
2374
+ log(sym.bar + " " + a.dim + "terms of service. Please review the applicable usage policies before" + a.reset);
2375
+ log(sym.bar + " " + a.dim + "granting access to other users." + a.reset);
2376
+ log(sym.bar);
2377
+ if (muResult.setupCode) {
2378
+ log(sym.bar + " " + a.green + "Multi-user mode enabled." + a.reset);
2369
2379
  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);
2380
+ log(sym.bar + " Setup code: " + a.bold + muResult.setupCode + a.reset);
2372
2381
  log(sym.bar);
2373
- promptSelect("Back?", [{ label: "Back", value: "back" }], function () {
2374
- showSettingsMenu(config, ip);
2375
- });
2382
+ log(sym.bar + " " + a.dim + "Open Clay in your browser and enter this code to create the admin account." + a.reset);
2383
+ log(sym.bar + " " + a.dim + "The code is single-use and will be cleared once the admin is set up." + a.reset);
2376
2384
  } 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);
2385
+ log(sym.bar + " " + a.dim + "Multi-user mode is already enabled." + a.reset);
2386
+ }
2387
+ log(sym.bar);
2388
+ promptSelect("Back?", [{ label: "Back", value: "back" }], function () {
2389
+ showSettingsMenu(config, ip);
2390
+ });
2391
+ break;
2392
+
2393
+ case "disable_multi_user":
2394
+ log(sym.bar);
2395
+ log(sym.bar + " " + a.yellow + sym.warn + " Disable multi-user mode?" + a.reset);
2396
+ log(sym.bar);
2397
+ log(sym.bar + " " + a.dim + "Sessions created by other users will no longer be visible." + a.reset);
2398
+ log(sym.bar + " " + a.dim + "User accounts will be preserved and restored if re-enabled." + a.reset);
2399
+ log(sym.bar);
2400
+ promptSelect("Confirm", [
2401
+ { label: "Disable multi-user mode", value: "confirm" },
2402
+ { label: "Cancel", value: "cancel" },
2403
+ ], function (confirmChoice) {
2404
+ if (confirmChoice === "confirm") {
2405
+ disableMultiUser();
2388
2406
  log(sym.bar);
2389
- log(sym.bar + " Setup code: " + a.bold + muResult.setupCode + a.reset);
2407
+ log(sym.done + " " + a.green + "Multi-user mode disabled." + a.reset);
2408
+ log(sym.bar + " " + a.dim + "Restart the daemon for changes to take full effect." + a.reset);
2390
2409
  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
2410
  }
2396
- log(sym.bar);
2397
- promptSelect("Back?", [{ label: "Back", value: "back" }], function () {
2398
- showSettingsMenu(config, ip);
2399
- });
2400
- }
2411
+ showSettingsMenu(config, ip);
2412
+ });
2401
2413
  break;
2402
2414
 
2403
2415
  case "logs":
@@ -2456,7 +2468,12 @@ var currentVersion = require("../package.json").version;
2456
2468
  clearStaleConfig();
2457
2469
  await new Promise(function (resolve) { setTimeout(resolve, 500); });
2458
2470
  }
2459
- // First rungo through setup (disclaimer, port, PIN, etc.)
2471
+ // No running daemon clear config so setup runs fresh
2472
+ if (!devAlive && devConfig) {
2473
+ if (devConfig.pid) clearStaleConfig();
2474
+ devConfig = null;
2475
+ }
2476
+ // No config — go through setup (disclaimer, port, PIN, etc.)
2460
2477
  if (!devConfig) {
2461
2478
  setup(function (pin, keepAwake) {
2462
2479
  devMode(pin, keepAwake, null);
package/lib/pages.js CHANGED
@@ -22,11 +22,10 @@ function pinPageHtml() {
22
22
  'if(d.ok){location.reload();return}' +
23
23
  'if(d.locked){for(var i=0;i<boxes.length;i++)boxes[i].disabled=true;' +
24
24
  'err.textContent="Too many attempts. Try again in "+Math.ceil(d.retryAfter/60)+" min";' +
25
- 'setTimeout(function(){for(var i=0;i<boxes.length;i++){boxes[i].disabled=false;boxes[i].value="";boxes[i].classList.remove("filled")}' +
26
- 'document.getElementById("pin").value="";err.textContent="";boxes[0].focus()},d.retryAfter*1000);return}' +
25
+ 'setTimeout(function(){for(var i=0;i<boxes.length;i++)boxes[i].disabled=false;' +
26
+ 'err.textContent="";resetPinBoxes()},d.retryAfter*1000);return}' +
27
27
  'var msg="Wrong PIN";if(typeof d.attemptsLeft==="number"&&d.attemptsLeft<=3)msg+=" ("+d.attemptsLeft+" left)";' +
28
- 'err.textContent=msg;for(var i=0;i<boxes.length;i++){boxes[i].value="";boxes[i].classList.remove("filled")}' +
29
- 'document.getElementById("pin").value="";boxes[0].focus()})' +
28
+ 'err.textContent=msg;resetPinBoxes()})' +
30
29
  '.catch(function(){err.textContent="Connection error"})}' +
31
30
  'initPinBoxes("pin-boxes","pin",submitPin);' +
32
31
  '</script></div></body></html>';
@@ -727,9 +726,9 @@ var authPageStyles =
727
726
  // PIN digit boxes
728
727
  '.pin-wrap{display:flex;gap:8px;justify-content:center}' +
729
728
  '.pin-digit{width:44px;height:56px;background:var(--input-bg);border:1.5px solid var(--border);border-radius:8px;' +
730
- 'color:var(--accent);font-family:"Courier New",Courier,"Roboto Mono",monospace;font-size:28px;font-weight:700;' +
729
+ 'color:var(--text);font-family:"Courier New",Courier,"Roboto Mono",monospace;font-size:28px;font-weight:700;' +
731
730
  'text-align:center;line-height:56px;outline:none;caret-color:transparent;' +
732
- '-webkit-text-security:disc;transition:border-color 0.15s,box-shadow 0.15s}' +
731
+ 'transition:border-color 0.15s,box-shadow 0.15s}' +
733
732
  '.pin-digit:focus{border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-20)}' +
734
733
  '.pin-digit.filled{color:var(--text)}' +
735
734
  // Legacy single-input fallback
@@ -754,28 +753,30 @@ var pinBoxScript =
754
753
  'function initPinBoxes(cId,hId,onComplete){' +
755
754
  'var wrap=document.getElementById(cId),hidden=document.getElementById(hId);' +
756
755
  'var boxes=wrap.querySelectorAll(".pin-digit");' +
756
+ 'var digits=["","","","","",""];' +
757
757
  'boxes[0].focus();' +
758
+ 'function setDigit(idx,v){digits[idx]=v;boxes[idx].value=v?"\\u2022":"";boxes[idx].classList.toggle("filled",v.length>0);syncHidden()}' +
758
759
  'for(var i=0;i<boxes.length;i++){(function(idx){' +
759
760
  'boxes[idx].addEventListener("input",function(e){' +
760
- 'var v=this.value.replace(/[^0-9]/g,"");' +
761
- 'if(v.length>1)v=v.charAt(v.length-1);' +
762
- 'this.value=v;' +
763
- 'this.classList.toggle("filled",v.length>0);' +
764
- 'syncHidden();' +
761
+ 'var raw=this.value.replace(/[^0-9]/g,"");' +
762
+ 'if(!raw){setDigit(idx,"");return}' +
763
+ 'var v=raw.charAt(raw.length-1);' +
764
+ 'setDigit(idx,v);' +
765
765
  'if(v&&idx<5)boxes[idx+1].focus();' +
766
766
  'if(hidden.value.length===6&&onComplete)onComplete()});' +
767
767
  'boxes[idx].addEventListener("keydown",function(e){' +
768
- 'if(e.key==="Backspace"&&!this.value&&idx>0){boxes[idx-1].focus();boxes[idx-1].value="";boxes[idx-1].classList.remove("filled");syncHidden()}' +
768
+ 'if(e.key==="Backspace"){if(!digits[idx]&&idx>0){setDigit(idx-1,"");boxes[idx-1].focus()}else{setDigit(idx,"")}syncHidden();return}' +
769
769
  'if(e.key==="ArrowLeft"&&idx>0)boxes[idx-1].focus();' +
770
770
  'if(e.key==="ArrowRight"&&idx<5)boxes[idx+1].focus();' +
771
771
  'if(e.key==="Enter"&&hidden.value.length===6&&onComplete){e.preventDefault();onComplete()}});' +
772
772
  'boxes[idx].addEventListener("paste",function(e){' +
773
773
  'e.preventDefault();var d=(e.clipboardData||window.clipboardData).getData("text").replace(/[^0-9]/g,"").slice(0,6);' +
774
- 'for(var j=0;j<d.length&&j<6;j++){boxes[j].value=d.charAt(j);boxes[j].classList.add("filled")}' +
775
- 'syncHidden();if(d.length>=6){boxes[5].focus();if(onComplete)onComplete()}else if(d.length>0)boxes[d.length].focus()});' +
774
+ 'for(var j=0;j<d.length&&j<6;j++){setDigit(j,d.charAt(j))}' +
775
+ 'if(d.length>=6){boxes[5].focus();if(onComplete)onComplete()}else if(d.length>0)boxes[d.length].focus()});' +
776
776
  'boxes[idx].addEventListener("focus",function(){this.select()});' +
777
777
  '})(i)}' +
778
- 'function syncHidden(){var v="";for(var j=0;j<boxes.length;j++)v+=boxes[j].value;hidden.value=v}' +
778
+ 'function syncHidden(){var v="";for(var j=0;j<digits.length;j++)v+=digits[j];hidden.value=v}' +
779
+ 'window.resetPinBoxes=function(){for(var j=0;j<6;j++){digits[j]="";boxes[j].value="";boxes[j].classList.remove("filled")}hidden.value="";boxes[0].focus()}' +
779
780
  '}';
780
781
 
781
782
  // HTML fragment for 6 PIN digit boxes + hidden input
@@ -938,8 +939,8 @@ function multiUserLoginPageHtml() {
938
939
 
939
940
  'function resetPin(){' +
940
941
  'var boxes=document.querySelectorAll(".pin-digit");' +
941
- 'for(var i=0;i<boxes.length;i++){boxes[i].value="";boxes[i].classList.remove("filled");boxes[i].disabled=false}' +
942
- 'pinEl.value="";btns[1].disabled=true;if(boxes[0])boxes[0].focus()}' +
942
+ 'for(var i=0;i<boxes.length;i++)boxes[i].disabled=false;' +
943
+ 'if(window.resetPinBoxes)resetPinBoxes();btns[1].disabled=true}' +
943
944
 
944
945
  'function goBackToUsername(){' +
945
946
  'steps[1].classList.remove("active");dots[1].classList.remove("current");dots[1].classList.remove("done");' +
package/lib/project.js CHANGED
@@ -156,6 +156,24 @@ function createProjectContext(opts) {
156
156
  }
157
157
  }
158
158
 
159
+ function sendToSession(sessionId, obj) {
160
+ var data = JSON.stringify(obj);
161
+ for (var ws of clients) {
162
+ if (ws.readyState === 1 && ws._clayActiveSession === sessionId) {
163
+ ws.send(data);
164
+ }
165
+ }
166
+ }
167
+
168
+ function sendToSessionOthers(sender, sessionId, obj) {
169
+ var data = JSON.stringify(obj);
170
+ for (var ws of clients) {
171
+ if (ws !== sender && ws.readyState === 1 && ws._clayActiveSession === sessionId) {
172
+ ws.send(data);
173
+ }
174
+ }
175
+ }
176
+
159
177
  // --- File watcher ---
160
178
  var fileWatcher = null;
161
179
  var watchedPath = null;
@@ -703,7 +721,7 @@ function createProjectContext(opts) {
703
721
  session.isProcessing = true;
704
722
  onProcessingChanged();
705
723
  session.sentToolResults = {};
706
- send({ type: "status", status: "processing" });
724
+ sendToSession(session.localId, { type: "status", status: "processing" });
707
725
  session.acceptEditsAfterStart = true;
708
726
  session.singleTurn = true;
709
727
  sdk.startQuery(session, loopState.promptText);
@@ -1016,12 +1034,14 @@ function createProjectContext(opts) {
1016
1034
  });
1017
1035
  }
1018
1036
 
1019
- // Session list (filtered for multi-user)
1037
+ // Session list (filtered for access control)
1020
1038
  var allSessions = [].concat(Array.from(sm.sessions.values())).filter(function (s) { return !s.hidden; });
1021
1039
  if (usersModule.isMultiUser() && wsUser) {
1022
1040
  allSessions = allSessions.filter(function (s) {
1023
1041
  return usersModule.canAccessSession(wsUser.id, s, { visibility: "public" });
1024
1042
  });
1043
+ } else if (!usersModule.isMultiUser()) {
1044
+ allSessions = allSessions.filter(function (s) { return !s.ownerId; });
1025
1045
  }
1026
1046
  sendTo(ws, {
1027
1047
  type: "session_list",
@@ -1048,12 +1068,23 @@ function createProjectContext(opts) {
1048
1068
  }),
1049
1069
  });
1050
1070
 
1051
- // Restore active session for this client (check access in multi-user mode)
1071
+ // Restore active session for this client (check access)
1052
1072
  var active = sm.getActiveSession();
1053
1073
  if (active && usersModule.isMultiUser() && wsUser) {
1054
1074
  if (!usersModule.canAccessSession(wsUser.id, active, { visibility: "public" })) {
1055
1075
  active = null;
1056
1076
  }
1077
+ } else if (active && !usersModule.isMultiUser() && active.ownerId) {
1078
+ active = null;
1079
+ }
1080
+ // Fallback: pick the most recent accessible session
1081
+ if (!active && allSessions.length > 0) {
1082
+ active = allSessions[0];
1083
+ for (var fi = 1; fi < allSessions.length; fi++) {
1084
+ if ((allSessions[fi].lastActivity || 0) > (active.lastActivity || 0)) {
1085
+ active = allSessions[fi];
1086
+ }
1087
+ }
1057
1088
  }
1058
1089
  if (active) {
1059
1090
  ws._clayActiveSession = active.localId;
@@ -1101,6 +1132,10 @@ function createProjectContext(opts) {
1101
1132
  }
1102
1133
 
1103
1134
  // --- WS message handler ---
1135
+ function getSessionForWs(ws) {
1136
+ return sm.sessions.get(ws._clayActiveSession) || null;
1137
+ }
1138
+
1104
1139
  function handleMessage(ws, msg) {
1105
1140
  if (msg.type === "push_subscribe") {
1106
1141
  if (pushModule && msg.subscription) pushModule.addSubscription(msg.subscription, msg.replaceEndpoint);
@@ -1108,7 +1143,7 @@ function createProjectContext(opts) {
1108
1143
  }
1109
1144
 
1110
1145
  if (msg.type === "load_more_history") {
1111
- var session = sm.getActiveSession();
1146
+ var session = getSessionForWs(ws);
1112
1147
  if (!session || typeof msg.before !== "number") return;
1113
1148
  var before = msg.before;
1114
1149
  var from = sm.findTurnBoundary(session.history, Math.max(0, before - sm.HISTORY_PAGE_SIZE));
@@ -1126,9 +1161,9 @@ function createProjectContext(opts) {
1126
1161
  var sessionOpts = {};
1127
1162
  if (ws._clayUser) sessionOpts.ownerId = ws._clayUser.id;
1128
1163
  if (msg.sessionVisibility) sessionOpts.sessionVisibility = msg.sessionVisibility;
1129
- var newSess = sm.createSession(sessionOpts, usersModule.isMultiUser() ? ws : undefined);
1164
+ var newSess = sm.createSession(sessionOpts, ws);
1165
+ ws._clayActiveSession = newSess.localId;
1130
1166
  if (usersModule.isMultiUser()) {
1131
- ws._clayActiveSession = newSess.localId;
1132
1167
  broadcastPresence();
1133
1168
  }
1134
1169
  return;
@@ -1152,11 +1187,11 @@ function createProjectContext(opts) {
1152
1187
  break;
1153
1188
  }
1154
1189
  }
1155
- var _rws = usersModule.isMultiUser() ? ws : undefined;
1156
- sm.resumeSession(msg.cliSessionId, { history: history, title: title }, _rws);
1190
+ var resumed = sm.resumeSession(msg.cliSessionId, { history: history, title: title }, ws);
1191
+ if (resumed) ws._clayActiveSession = resumed.localId;
1157
1192
  }).catch(function () {
1158
- var _rws = usersModule.isMultiUser() ? ws : undefined;
1159
- sm.resumeSession(msg.cliSessionId, undefined, _rws);
1193
+ var resumed = sm.resumeSession(msg.cliSessionId, undefined, ws);
1194
+ if (resumed) ws._clayActiveSession = resumed.localId;
1160
1195
  });
1161
1196
  return;
1162
1197
  }
@@ -1201,7 +1236,8 @@ function createProjectContext(opts) {
1201
1236
  sm.switchSession(msg.id, ws);
1202
1237
  broadcastPresence();
1203
1238
  } else {
1204
- sm.switchSession(msg.id);
1239
+ ws._clayActiveSession = msg.id;
1240
+ sm.switchSession(msg.id, ws);
1205
1241
  }
1206
1242
  }
1207
1243
  return;
@@ -1209,7 +1245,7 @@ function createProjectContext(opts) {
1209
1245
 
1210
1246
  if (msg.type === "delete_session") {
1211
1247
  if (msg.id && sm.sessions.has(msg.id)) {
1212
- sm.deleteSession(msg.id, usersModule.isMultiUser() ? ws : undefined);
1248
+ sm.deleteSession(msg.id, ws);
1213
1249
  }
1214
1250
  return;
1215
1251
  }
@@ -1276,7 +1312,7 @@ function createProjectContext(opts) {
1276
1312
  }
1277
1313
 
1278
1314
  if (msg.type === "stop") {
1279
- var session = sm.getActiveSession();
1315
+ var session = getSessionForWs(ws);
1280
1316
  if (session && session.abortController && session.isProcessing) {
1281
1317
  session.abortController.abort();
1282
1318
  }
@@ -1297,22 +1333,22 @@ function createProjectContext(opts) {
1297
1333
  // Verify target is actually a claude process before killing
1298
1334
  if (!sdk.isClaudeProcess(pid)) {
1299
1335
  console.error("[project] Refused to kill PID " + pid + ": not a claude process");
1300
- send({ type: "error", text: "Process " + pid + " is not a Claude process." });
1336
+ sendTo(ws, { type: "error", text: "Process " + pid + " is not a Claude process." });
1301
1337
  return;
1302
1338
  }
1303
1339
  try {
1304
1340
  process.kill(pid, "SIGTERM");
1305
1341
  console.log("[project] Sent SIGTERM to conflicting Claude process PID " + pid);
1306
- send({ type: "process_killed", pid: pid });
1342
+ sendTo(ws, { type: "process_killed", pid: pid });
1307
1343
  } catch (e) {
1308
1344
  console.error("[project] Failed to kill PID " + pid + ":", e.message);
1309
- send({ type: "error", text: "Failed to kill process " + pid + ": " + (e.message || e) });
1345
+ sendTo(ws, { type: "error", text: "Failed to kill process " + pid + ": " + (e.message || e) });
1310
1346
  }
1311
1347
  return;
1312
1348
  }
1313
1349
 
1314
1350
  if (msg.type === "set_model" && msg.model) {
1315
- var session = sm.getActiveSession();
1351
+ var session = getSessionForWs(ws);
1316
1352
  if (session) {
1317
1353
  sdk.setModel(session, msg.model);
1318
1354
  }
@@ -1323,7 +1359,7 @@ function createProjectContext(opts) {
1323
1359
  if (typeof opts.onSetServerDefaultModel === "function") {
1324
1360
  opts.onSetServerDefaultModel(msg.model);
1325
1361
  }
1326
- var session = sm.getActiveSession();
1362
+ var session = getSessionForWs(ws);
1327
1363
  if (session) {
1328
1364
  sdk.setModel(session, msg.model);
1329
1365
  }
@@ -1334,7 +1370,7 @@ function createProjectContext(opts) {
1334
1370
  if (typeof opts.onSetProjectDefaultModel === "function") {
1335
1371
  opts.onSetProjectDefaultModel(slug, msg.model);
1336
1372
  }
1337
- var session = sm.getActiveSession();
1373
+ var session = getSessionForWs(ws);
1338
1374
  if (session) {
1339
1375
  sdk.setModel(session, msg.model);
1340
1376
  }
@@ -1348,7 +1384,7 @@ function createProjectContext(opts) {
1348
1384
  return;
1349
1385
  }
1350
1386
  sm.currentPermissionMode = msg.mode;
1351
- var session = sm.getActiveSession();
1387
+ var session = getSessionForWs(ws);
1352
1388
  if (session) {
1353
1389
  sdk.setPermissionMode(session, msg.mode);
1354
1390
  }
@@ -1362,7 +1398,7 @@ function createProjectContext(opts) {
1362
1398
  }
1363
1399
  if (!dangerouslySkipPermissions) {
1364
1400
  sm.currentPermissionMode = msg.mode;
1365
- var session = sm.getActiveSession();
1401
+ var session = getSessionForWs(ws);
1366
1402
  if (session) {
1367
1403
  sdk.setPermissionMode(session, msg.mode);
1368
1404
  }
@@ -1377,7 +1413,7 @@ function createProjectContext(opts) {
1377
1413
  }
1378
1414
  if (!dangerouslySkipPermissions) {
1379
1415
  sm.currentPermissionMode = msg.mode;
1380
- var session = sm.getActiveSession();
1416
+ var session = getSessionForWs(ws);
1381
1417
  if (session) {
1382
1418
  sdk.setPermissionMode(session, msg.mode);
1383
1419
  }
@@ -1417,7 +1453,7 @@ function createProjectContext(opts) {
1417
1453
  }
1418
1454
 
1419
1455
  if (msg.type === "rewind_preview") {
1420
- var session = sm.getActiveSession();
1456
+ var session = getSessionForWs(ws);
1421
1457
  if (!session || !session.cliSessionId || !msg.uuid) return;
1422
1458
 
1423
1459
  (async function () {
@@ -1446,7 +1482,7 @@ function createProjectContext(opts) {
1446
1482
  }
1447
1483
 
1448
1484
  if (msg.type === "rewind_execute") {
1449
- var session = sm.getActiveSession();
1485
+ var session = getSessionForWs(ws);
1450
1486
  if (!session || !session.cliSessionId || !msg.uuid) return;
1451
1487
  var mode = msg.mode || "both";
1452
1488
 
@@ -1501,11 +1537,11 @@ function createProjectContext(opts) {
1501
1537
  onProcessingChanged();
1502
1538
 
1503
1539
  sm.saveSessionFile(session);
1504
- sm.switchSession(session.localId, usersModule.isMultiUser() ? ws : undefined);
1540
+ sm.switchSession(session.localId, ws);
1505
1541
  sm.sendAndRecord(session, { type: "rewind_complete", mode: mode });
1506
1542
  sm.broadcastSessionList();
1507
1543
  } catch (err) {
1508
- send({ type: "rewind_error", text: "Rewind failed: " + err.message });
1544
+ sendTo(ws, { type: "rewind_error", text: "Rewind failed: " + err.message });
1509
1545
  } finally {
1510
1546
  if (result && result.isTemp) result.cleanup();
1511
1547
  }
@@ -1514,7 +1550,7 @@ function createProjectContext(opts) {
1514
1550
  }
1515
1551
 
1516
1552
  if (msg.type === "ask_user_response") {
1517
- var session = sm.getActiveSession();
1553
+ var session = getSessionForWs(ws);
1518
1554
  if (!session) return;
1519
1555
  var toolId = msg.toolId;
1520
1556
  var answers = msg.answers || {};
@@ -1530,12 +1566,12 @@ function createProjectContext(opts) {
1530
1566
  }
1531
1567
 
1532
1568
  if (msg.type === "input_sync") {
1533
- sendToOthers(ws, msg);
1569
+ sendToSessionOthers(ws, ws._clayActiveSession, msg);
1534
1570
  return;
1535
1571
  }
1536
1572
 
1537
1573
  if (msg.type === "permission_response") {
1538
- var session = sm.getActiveSession();
1574
+ var session = getSessionForWs(ws);
1539
1575
  if (!session) return;
1540
1576
  var requestId = msg.requestId;
1541
1577
  var decision = msg.decision;
@@ -1603,17 +1639,17 @@ function createProjectContext(opts) {
1603
1639
  newSession.title = "Plan execution (cleared context)";
1604
1640
  sm.saveSessionFile(newSession);
1605
1641
  sm.broadcastSessionList();
1606
- send(userMsg);
1642
+ sendToSession(newSession.localId, userMsg);
1607
1643
 
1608
1644
  newSession.isProcessing = true;
1609
1645
  onProcessingChanged();
1610
1646
  newSession.sentToolResults = {};
1611
- send({ type: "status", status: "processing" });
1647
+ sendToSession(newSession.localId, { type: "status", status: "processing" });
1612
1648
  newSession.acceptEditsAfterStart = true;
1613
1649
  sdk.startQuery(newSession, planPrompt);
1614
1650
  } catch (e) {
1615
1651
  console.error("[project] Error starting plan execution:", e);
1616
- send({ type: "error", text: "Failed to start plan execution: " + (e.message || e) });
1652
+ sendTo(ws, { type: "error", text: "Failed to start plan execution: " + (e.message || e) });
1617
1653
  }
1618
1654
  });
1619
1655
  return;
@@ -1631,13 +1667,13 @@ function createProjectContext(opts) {
1631
1667
  var userMsg = { type: "user_message", text: feedback };
1632
1668
  session.history.push(userMsg);
1633
1669
  sm.appendToSessionFile(session, userMsg);
1634
- send(userMsg);
1670
+ sendToSession(session.localId, userMsg);
1635
1671
 
1636
1672
  if (!session.isProcessing) {
1637
1673
  session.isProcessing = true;
1638
1674
  onProcessingChanged();
1639
1675
  session.sentToolResults = {};
1640
- send({ type: "status", status: "processing" });
1676
+ sendToSession(session.localId, { type: "status", status: "processing" });
1641
1677
  if (!session.queryInstance) {
1642
1678
  sdk.startQuery(session, feedback);
1643
1679
  } else {
@@ -2406,11 +2442,11 @@ function createProjectContext(opts) {
2406
2442
  // Send crafting prompt and start the conversation with Claude.
2407
2443
  craftingSession.history.push({ type: "user_message", text: craftingPrompt });
2408
2444
  sm.appendToSessionFile(craftingSession, { type: "user_message", text: craftingPrompt });
2409
- send({ type: "user_message", text: craftingPrompt });
2445
+ sendToSession(craftingSession.localId, { type: "user_message", text: craftingPrompt });
2410
2446
  craftingSession.isProcessing = true;
2411
2447
  onProcessingChanged();
2412
2448
  craftingSession.sentToolResults = {};
2413
- send({ type: "status", status: "processing" });
2449
+ sendToSession(craftingSession.localId, { type: "status", status: "processing" });
2414
2450
  sdk.startQuery(craftingSession, craftingPrompt);
2415
2451
 
2416
2452
  send({ type: "ralph_crafting_started", sessionId: craftingSession.localId, taskId: newLoopId, source: recordSource });
@@ -2600,7 +2636,7 @@ function createProjectContext(opts) {
2600
2636
  if (msg.type !== "message") return;
2601
2637
  if (!msg.text && (!msg.images || msg.images.length === 0) && (!msg.pastes || msg.pastes.length === 0)) return;
2602
2638
 
2603
- var session = sm.getActiveSession();
2639
+ var session = getSessionForWs(ws);
2604
2640
  if (!session) return;
2605
2641
 
2606
2642
  var userMsg = { type: "user_message", text: msg.text || "" };
@@ -2612,7 +2648,7 @@ function createProjectContext(opts) {
2612
2648
  }
2613
2649
  session.history.push(userMsg);
2614
2650
  sm.appendToSessionFile(session, userMsg);
2615
- sendToOthers(ws, userMsg);
2651
+ sendToSessionOthers(ws, session.localId, userMsg);
2616
2652
 
2617
2653
  if (!session.title) {
2618
2654
  session.title = (msg.text || "Image").substring(0, 50);
@@ -2632,7 +2668,7 @@ function createProjectContext(opts) {
2632
2668
  session.isProcessing = true;
2633
2669
  onProcessingChanged();
2634
2670
  session.sentToolResults = {};
2635
- send({ type: "status", status: "processing" });
2671
+ sendToSession(session.localId, { type: "status", status: "processing" });
2636
2672
  if (!session.queryInstance) {
2637
2673
  sdk.startQuery(session, fullText, msg.images);
2638
2674
  } else {
@@ -2845,7 +2881,8 @@ function createProjectContext(opts) {
2845
2881
  return;
2846
2882
  }
2847
2883
  var spawnCwd = scope === "global" ? os.homedir() : cwd;
2848
- var child = spawn("npx", ["skills", "add", url, "--skill", skill], {
2884
+ var scopeFlag = scope === "global" ? "--global" : "--project";
2885
+ var child = spawn("npx", ["skills", "add", url, "--skill", skill, "--yes", scopeFlag], {
2849
2886
  cwd: spawnCwd,
2850
2887
  stdio: "ignore",
2851
2888
  detached: false,
@@ -3116,29 +3153,6 @@ function createProjectContext(opts) {
3116
3153
  },
3117
3154
  warmup: function () {
3118
3155
  sdk.warmup();
3119
- // Auto-install clay-ralph skill globally if not present
3120
- var clayRalphDir = path.join(os.homedir(), ".claude", "skills", "clay-ralph", "SKILL.md");
3121
- try {
3122
- fs.accessSync(clayRalphDir, fs.constants.R_OK);
3123
- } catch (e) {
3124
- console.log("[project] Auto-installing clay-ralph skill...");
3125
- var child = spawn("npx", ["skills", "add", "https://github.com/chadbyte/clay-ralph", "--skill", "clay-ralph"], {
3126
- cwd: os.homedir(),
3127
- stdio: "ignore",
3128
- detached: false,
3129
- });
3130
- child.on("close", function (code) {
3131
- if (code === 0) {
3132
- console.log("[project] clay-ralph skill installed successfully");
3133
- send({ type: "skill_installed", skill: "clay-ralph", scope: "global", success: true, error: null });
3134
- } else {
3135
- console.log("[project] clay-ralph skill install failed (code " + code + ")");
3136
- }
3137
- });
3138
- child.on("error", function (err) {
3139
- console.log("[project] clay-ralph skill install error: " + err.message);
3140
- });
3141
- }
3142
3156
  },
3143
3157
  destroy: destroy,
3144
3158
  };