clay-server 2.10.0 → 2.11.0-beta.10

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/lib/project.js CHANGED
@@ -6,10 +6,11 @@ var { createSessionManager } = require("./sessions");
6
6
  var { createSDKBridge } = require("./sdk-bridge");
7
7
  var { createTerminalManager } = require("./terminal-manager");
8
8
  var { createNotesManager } = require("./notes");
9
- var { fetchLatestVersion, isNewer } = require("./updater");
9
+ var { fetchLatestVersion, fetchVersion, isNewer } = require("./updater");
10
10
  var { execFileSync, spawn } = require("child_process");
11
11
  var { createLoopRegistry } = require("./scheduler");
12
12
  var usersModule = require("./users");
13
+ var { resolveOsUserInfo, fsAsUser } = require("./os-users");
13
14
 
14
15
  var MAX_UPLOAD_BYTES = 50 * 1024 * 1024; // 50 MB
15
16
 
@@ -108,8 +109,59 @@ function createProjectContext(opts) {
108
109
  var getScheduleCount = opts.getScheduleCount || function () { return 0; };
109
110
  var onProcessingChanged = opts.onProcessingChanged || function () {};
110
111
  var onPresenceChange = opts.onPresenceChange || function () {};
112
+ var updateChannel = opts.updateChannel || "stable";
113
+ var osUsers = opts.osUsers || false;
114
+ var projectOwnerId = opts.projectOwnerId || null;
111
115
  var latestVersion = null;
112
116
 
117
+ // --- OS-level user isolation helper ---
118
+ // Returns the Linux username for the session owner.
119
+ // Each session uses its own owner's Claude account and credits.
120
+ function getLinuxUserForSession(session) {
121
+ if (!osUsers) return null;
122
+ if (!session.ownerId) return null;
123
+ var user = usersModule.findUserById(session.ownerId);
124
+ if (!user || !user.linuxUser) return null;
125
+ return user.linuxUser;
126
+ }
127
+
128
+ function getLinuxUserForWs(ws) {
129
+ if (!osUsers) return null;
130
+ if (!ws._clayUser || !ws._clayUser.linuxUser) return null;
131
+ return ws._clayUser.linuxUser;
132
+ }
133
+
134
+ // Cache resolved OS user info to avoid repeated getent calls
135
+ var osUserInfoCache = {};
136
+ function getOsUserInfoForWs(ws) {
137
+ var linuxUser = getLinuxUserForWs(ws);
138
+ if (!linuxUser) return null;
139
+ if (osUserInfoCache[linuxUser]) return osUserInfoCache[linuxUser];
140
+ try {
141
+ var info = resolveOsUserInfo(linuxUser);
142
+ osUserInfoCache[linuxUser] = info;
143
+ return info;
144
+ } catch (e) {
145
+ console.error("[project] Failed to resolve OS user info for " + linuxUser + ":", e.message);
146
+ return null;
147
+ }
148
+ }
149
+
150
+ function getOsUserInfoForReq(req) {
151
+ if (!osUsers) return null;
152
+ if (!req._clayUser || !req._clayUser.linuxUser) return null;
153
+ var linuxUser = req._clayUser.linuxUser;
154
+ if (osUserInfoCache[linuxUser]) return osUserInfoCache[linuxUser];
155
+ try {
156
+ var info = resolveOsUserInfo(linuxUser);
157
+ osUserInfoCache[linuxUser] = info;
158
+ return info;
159
+ } catch (e) {
160
+ console.error("[project] Failed to resolve OS user info for " + linuxUser + ":", e.message);
161
+ return null;
162
+ }
163
+ }
164
+
113
165
  // --- Per-project clients ---
114
166
  var clients = new Set();
115
167
 
@@ -724,7 +776,7 @@ function createProjectContext(opts) {
724
776
  sendToSession(session.localId, { type: "status", status: "processing" });
725
777
  session.acceptEditsAfterStart = true;
726
778
  session.singleTurn = true;
727
- sdk.startQuery(session, loopState.promptText);
779
+ sdk.startQuery(session, loopState.promptText, undefined, getLinuxUserForSession(session));
728
780
  }
729
781
 
730
782
  function runJudge() {
@@ -806,7 +858,7 @@ function createProjectContext(opts) {
806
858
  judgeSession.sentToolResults = {};
807
859
  judgeSession.acceptEditsAfterStart = true;
808
860
  judgeSession.singleTurn = true;
809
- sdk.startQuery(judgeSession, judgePrompt);
861
+ sdk.startQuery(judgeSession, judgePrompt, undefined, getLinuxUserForSession(judgeSession));
810
862
  }
811
863
 
812
864
  function parseJudgeVerdict(session) {
@@ -947,7 +999,7 @@ function createProjectContext(opts) {
947
999
  var nm = createNotesManager({ cwd: cwd, send: send, sendTo: sendTo });
948
1000
 
949
1001
  // Check for updates in background
950
- fetchLatestVersion().then(function (v) {
1002
+ fetchVersion(updateChannel).then(function (v) {
951
1003
  if (v && isNewer(v, currentVersion)) {
952
1004
  latestVersion = v;
953
1005
  send({ type: "update_available", version: v });
@@ -969,7 +1021,7 @@ function createProjectContext(opts) {
969
1021
  // Send cached state
970
1022
  var _userId = ws._clayUser ? ws._clayUser.id : null;
971
1023
  var _filteredProjects = getProjectList(_userId);
972
- sendTo(ws, { type: "info", cwd: cwd, slug: slug, project: title || project, version: currentVersion, debug: !!debug, dangerouslySkipPermissions: dangerouslySkipPermissions, lanHost: lanHost, projectCount: _filteredProjects.length, projects: _filteredProjects });
1024
+ sendTo(ws, { type: "info", cwd: cwd, slug: slug, project: title || project, version: currentVersion, debug: !!debug, dangerouslySkipPermissions: dangerouslySkipPermissions, osUsers: osUsers, lanHost: lanHost, projectCount: _filteredProjects.length, projects: _filteredProjects, projectOwnerId: projectOwnerId });
973
1025
  if (latestVersion) {
974
1026
  sendTo(ws, { type: "update_available", version: latestVersion });
975
1027
  }
@@ -979,7 +1031,7 @@ function createProjectContext(opts) {
979
1031
  if (sm.currentModel) {
980
1032
  sendTo(ws, { type: "model_info", model: sm.currentModel, models: sm.availableModels || [] });
981
1033
  }
982
- sendTo(ws, { type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas || [] });
1034
+ sendTo(ws, { type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
983
1035
  sendTo(ws, { type: "term_list", terminals: tm.list() });
984
1036
  sendTo(ws, { type: "notes_list", notes: nm.list() });
985
1037
  sendTo(ws, { type: "loop_registry_updated", records: getHubSchedules() });
@@ -1086,7 +1138,20 @@ function createProjectContext(opts) {
1086
1138
  }
1087
1139
  }
1088
1140
  }
1089
- if (active) {
1141
+ // Auto-create a session if none exist for this client
1142
+ var autoCreated = false;
1143
+ if (!active) {
1144
+ var autoOpts = {};
1145
+ if (wsUser) autoOpts.ownerId = wsUser.id;
1146
+ active = sm.createSession(autoOpts, ws);
1147
+ autoCreated = true;
1148
+ }
1149
+ if (active && !autoCreated) {
1150
+ // Backfill ownerId for legacy sessions restored without one
1151
+ if (!active.ownerId && wsUser) {
1152
+ active.ownerId = wsUser.id;
1153
+ sm.saveSessionFile(active);
1154
+ }
1090
1155
  ws._clayActiveSession = active.localId;
1091
1156
  sendTo(ws, { type: "session_switched", id: active.localId, cliSessionId: active.cliSessionId || null, loop: active.loop || null });
1092
1157
 
@@ -1137,6 +1202,14 @@ function createProjectContext(opts) {
1137
1202
  }
1138
1203
 
1139
1204
  function handleMessage(ws, msg) {
1205
+ // --- DM messages (delegated to server-level handler) ---
1206
+ if (msg.type === "dm_open" || msg.type === "dm_send" || msg.type === "dm_list" || msg.type === "dm_typing") {
1207
+ if (typeof opts.onDmMessage === "function") {
1208
+ opts.onDmMessage(ws, msg);
1209
+ }
1210
+ return;
1211
+ }
1212
+
1140
1213
  if (msg.type === "push_subscribe") {
1141
1214
  if (pushModule && msg.subscription) pushModule.addSubscription(msg.subscription, msg.replaceEndpoint);
1142
1215
  return;
@@ -1176,20 +1249,55 @@ function createProjectContext(opts) {
1176
1249
  return;
1177
1250
  }
1178
1251
 
1252
+ if (msg.type === "transfer_project_owner") {
1253
+ var isAdmin = ws._clayUser && ws._clayUser.role === "admin";
1254
+ var isProjectOwner = ws._clayUser && projectOwnerId && ws._clayUser.id === projectOwnerId;
1255
+ if (!ws._clayUser || (!isAdmin && !isProjectOwner)) {
1256
+ sendTo(ws, { type: "error", text: "Only project owners or admins can transfer ownership." });
1257
+ return;
1258
+ }
1259
+ var targetUser = msg.userId ? usersModule.findUserById(msg.userId) : null;
1260
+ if (!targetUser) {
1261
+ sendTo(ws, { type: "error", text: "User not found." });
1262
+ return;
1263
+ }
1264
+ projectOwnerId = targetUser.id;
1265
+ // Persist via daemon callback
1266
+ if (opts.onProjectOwnerChanged) {
1267
+ opts.onProjectOwnerChanged(slug, projectOwnerId);
1268
+ }
1269
+ send({ type: "project_owner_changed", ownerId: projectOwnerId, ownerName: targetUser.displayName || targetUser.username });
1270
+ return;
1271
+ }
1272
+
1179
1273
  if (msg.type === "resume_session") {
1180
1274
  if (!msg.cliSessionId) return;
1181
1275
  var cliSess = require("./cli-sessions");
1182
- cliSess.readCliSessionHistory(cwd, msg.cliSessionId).then(function (history) {
1183
- var title = "Resumed session";
1184
- for (var i = 0; i < history.length; i++) {
1185
- if (history[i].type === "user_message" && history[i].text) {
1186
- title = history[i].text.substring(0, 50);
1187
- break;
1276
+ // Try SDK for title first, then fall back to manual parsing
1277
+ var titlePromise = getSDK().then(function(sdkMod) {
1278
+ return sdkMod.getSessionInfo(msg.cliSessionId, { dir: cwd });
1279
+ }).then(function(info) {
1280
+ return (info && info.summary) ? info.summary.substring(0, 100) : null;
1281
+ }).catch(function() { return null; });
1282
+
1283
+ Promise.all([
1284
+ cliSess.readCliSessionHistory(cwd, msg.cliSessionId),
1285
+ titlePromise
1286
+ ]).then(function(results) {
1287
+ var history = results[0];
1288
+ var sdkTitle = results[1];
1289
+ var title = sdkTitle || "Resumed session";
1290
+ if (!sdkTitle) {
1291
+ for (var i = 0; i < history.length; i++) {
1292
+ if (history[i].type === "user_message" && history[i].text) {
1293
+ title = history[i].text.substring(0, 50);
1294
+ break;
1295
+ }
1188
1296
  }
1189
1297
  }
1190
1298
  var resumed = sm.resumeSession(msg.cliSessionId, { history: history, title: title }, ws);
1191
1299
  if (resumed) ws._clayActiveSession = resumed.localId;
1192
- }).catch(function () {
1300
+ }).catch(function() {
1193
1301
  var resumed = sm.resumeSession(msg.cliSessionId, undefined, ws);
1194
1302
  if (resumed) ws._clayActiveSession = resumed.localId;
1195
1303
  });
@@ -1197,9 +1305,7 @@ function createProjectContext(opts) {
1197
1305
  }
1198
1306
 
1199
1307
  if (msg.type === "list_cli_sessions") {
1200
- var cliSessions = require("./cli-sessions");
1201
1308
  var _fs = require("fs");
1202
- var _path = require("path");
1203
1309
  // Collect session IDs already in relay (in-memory + persisted on disk)
1204
1310
  var relayIds = {};
1205
1311
  sm.sessions.forEach(function (s) {
@@ -1214,13 +1320,34 @@ function createProjectContext(opts) {
1214
1320
  }
1215
1321
  }
1216
1322
  } catch (e) {}
1217
- cliSessions.listCliSessions(cwd).then(function (sessions) {
1218
- var filtered = sessions.filter(function (s) {
1323
+
1324
+ getSDK().then(function(sdkMod) {
1325
+ return sdkMod.listSessions({ dir: cwd });
1326
+ }).then(function(sdkSessions) {
1327
+ var filtered = sdkSessions.filter(function(s) {
1219
1328
  return !relayIds[s.sessionId];
1329
+ }).map(function(s) {
1330
+ return {
1331
+ sessionId: s.sessionId,
1332
+ firstPrompt: s.summary || s.firstPrompt || "",
1333
+ model: null,
1334
+ gitBranch: s.gitBranch || null,
1335
+ startTime: s.createdAt ? new Date(s.createdAt).toISOString() : null,
1336
+ lastActivity: s.lastModified ? new Date(s.lastModified).toISOString() : null,
1337
+ };
1220
1338
  });
1221
1339
  sendTo(ws, { type: "cli_session_list", sessions: filtered });
1222
- }).catch(function () {
1223
- sendTo(ws, { type: "cli_session_list", sessions: [] });
1340
+ }).catch(function() {
1341
+ // Fallback to manual parsing if SDK fails
1342
+ var cliSessions = require("./cli-sessions");
1343
+ cliSessions.listCliSessions(cwd).then(function(sessions) {
1344
+ var filtered = sessions.filter(function(s) {
1345
+ return !relayIds[s.sessionId];
1346
+ });
1347
+ sendTo(ws, { type: "cli_session_list", sessions: filtered });
1348
+ }).catch(function() {
1349
+ sendTo(ws, { type: "cli_session_list", sessions: [] });
1350
+ });
1224
1351
  });
1225
1352
  return;
1226
1353
  }
@@ -1256,6 +1383,14 @@ function createProjectContext(opts) {
1256
1383
  s.title = String(msg.title).substring(0, 100);
1257
1384
  sm.saveSessionFile(s);
1258
1385
  sm.broadcastSessionList();
1386
+ // Sync title to SDK session
1387
+ if (s.cliSessionId) {
1388
+ getSDK().then(function(sdk) {
1389
+ sdk.renameSession(s.cliSessionId, s.title, { dir: cwd }).catch(function(e) {
1390
+ console.error("[project] SDK renameSession failed:", e.message);
1391
+ });
1392
+ }).catch(function() {});
1393
+ }
1259
1394
  }
1260
1395
  return;
1261
1396
  }
@@ -1266,9 +1401,19 @@ function createProjectContext(opts) {
1266
1401
  return;
1267
1402
  }
1268
1403
 
1404
+ if (msg.type === "set_update_channel") {
1405
+ if (usersModule.isMultiUser() && (!ws._clayUser || ws._clayUser.role !== "admin")) return;
1406
+ var newChannel = msg.channel === "beta" ? "beta" : "stable";
1407
+ updateChannel = newChannel;
1408
+ if (typeof opts.onSetUpdateChannel === "function") {
1409
+ opts.onSetUpdateChannel(newChannel);
1410
+ }
1411
+ return;
1412
+ }
1413
+
1269
1414
  if (msg.type === "check_update") {
1270
1415
  if (usersModule.isMultiUser() && (!ws._clayUser || ws._clayUser.role !== "admin")) return;
1271
- fetchLatestVersion().then(function (v) {
1416
+ fetchVersion(updateChannel).then(function (v) {
1272
1417
  if (v && isNewer(v, currentVersion)) {
1273
1418
  latestVersion = v;
1274
1419
  sendTo(ws, { type: "update_available", version: v });
@@ -1380,7 +1525,7 @@ function createProjectContext(opts) {
1380
1525
  if (msg.type === "set_permission_mode" && msg.mode) {
1381
1526
  // When dangerouslySkipPermissions is active, don't allow UI to change mode
1382
1527
  if (dangerouslySkipPermissions) {
1383
- send({ type: "config_state", model: sm.currentModel || "", mode: "bypassPermissions", effort: sm.currentEffort || "medium", betas: sm.currentBetas || [] });
1528
+ send({ type: "config_state", model: sm.currentModel || "", mode: "bypassPermissions", effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1384
1529
  return;
1385
1530
  }
1386
1531
  sm.currentPermissionMode = msg.mode;
@@ -1388,7 +1533,7 @@ function createProjectContext(opts) {
1388
1533
  if (session) {
1389
1534
  sdk.setPermissionMode(session, msg.mode);
1390
1535
  }
1391
- send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [] });
1536
+ send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1392
1537
  return;
1393
1538
  }
1394
1539
 
@@ -1402,7 +1547,7 @@ function createProjectContext(opts) {
1402
1547
  if (session) {
1403
1548
  sdk.setPermissionMode(session, msg.mode);
1404
1549
  }
1405
- send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [] });
1550
+ send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1406
1551
  }
1407
1552
  return;
1408
1553
  }
@@ -1417,14 +1562,18 @@ function createProjectContext(opts) {
1417
1562
  if (session) {
1418
1563
  sdk.setPermissionMode(session, msg.mode);
1419
1564
  }
1420
- send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [] });
1565
+ send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1421
1566
  }
1422
1567
  return;
1423
1568
  }
1424
1569
 
1425
1570
  if (msg.type === "set_effort" && msg.effort) {
1426
1571
  sm.currentEffort = msg.effort;
1427
- send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort, betas: sm.currentBetas || [] });
1572
+ var session = getSessionForWs(ws);
1573
+ if (session) {
1574
+ sdk.setEffort(session, msg.effort);
1575
+ }
1576
+ send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort, betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1428
1577
  return;
1429
1578
  }
1430
1579
 
@@ -1433,7 +1582,7 @@ function createProjectContext(opts) {
1433
1582
  opts.onSetServerDefaultEffort(msg.effort);
1434
1583
  }
1435
1584
  sm.currentEffort = msg.effort;
1436
- send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort, betas: sm.currentBetas || [] });
1585
+ send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort, betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1437
1586
  return;
1438
1587
  }
1439
1588
 
@@ -1442,13 +1591,20 @@ function createProjectContext(opts) {
1442
1591
  opts.onSetProjectDefaultEffort(slug, msg.effort);
1443
1592
  }
1444
1593
  sm.currentEffort = msg.effort;
1445
- send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort, betas: sm.currentBetas || [] });
1594
+ send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort, betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1446
1595
  return;
1447
1596
  }
1448
1597
 
1449
1598
  if (msg.type === "set_betas") {
1450
1599
  sm.currentBetas = msg.betas || [];
1451
- send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas });
1600
+ send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas, thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1601
+ return;
1602
+ }
1603
+
1604
+ if (msg.type === "set_thinking") {
1605
+ sm.currentThinking = msg.thinking || "adaptive";
1606
+ if (msg.budgetTokens) sm.currentThinkingBudget = msg.budgetTokens;
1607
+ send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1452
1608
  return;
1453
1609
  }
1454
1610
 
@@ -1549,6 +1705,34 @@ function createProjectContext(opts) {
1549
1705
  return;
1550
1706
  }
1551
1707
 
1708
+ if (msg.type === "fork_session" && msg.uuid) {
1709
+ var session = getSessionForWs(ws);
1710
+ if (!session || !session.cliSessionId) {
1711
+ sendTo(ws, { type: "error", text: "Cannot fork: no CLI session" });
1712
+ return;
1713
+ }
1714
+ var forkCliId = session.cliSessionId;
1715
+ var forkTitle = (session.title || "New Session") + " (fork)";
1716
+ getSDK().then(function(sdkMod) {
1717
+ return sdkMod.forkSession(forkCliId, {
1718
+ upToMessageId: msg.uuid,
1719
+ dir: cwd,
1720
+ });
1721
+ }).then(function(result) {
1722
+ var cliSess = require("./cli-sessions");
1723
+ return cliSess.readCliSessionHistory(cwd, result.sessionId).then(function(history) {
1724
+ var forked = sm.resumeSession(result.sessionId, { history: history, title: forkTitle }, ws);
1725
+ if (forked) {
1726
+ ws._clayActiveSession = forked.localId;
1727
+ sendTo(ws, { type: "fork_complete", sessionId: forked.localId });
1728
+ }
1729
+ });
1730
+ }).catch(function(e) {
1731
+ sendTo(ws, { type: "error", text: "Fork failed: " + (e.message || e) });
1732
+ });
1733
+ return;
1734
+ }
1735
+
1552
1736
  if (msg.type === "ask_user_response") {
1553
1737
  var session = getSessionForWs(ws);
1554
1738
  if (!session) return;
@@ -1583,7 +1767,7 @@ function createProjectContext(opts) {
1583
1767
  if (decision === "allow_accept_edits") {
1584
1768
  sdk.setPermissionMode(session, "acceptEdits");
1585
1769
  sm.currentPermissionMode = "acceptEdits";
1586
- send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [] });
1770
+ send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1587
1771
  pending.resolve({ behavior: "allow", updatedInput: pending.toolInput });
1588
1772
  sm.sendAndRecord(session, { type: "permission_resolved", requestId: requestId, decision: decision });
1589
1773
  return;
@@ -1612,7 +1796,7 @@ function createProjectContext(opts) {
1612
1796
 
1613
1797
  // Update permission mode for the new session
1614
1798
  sm.currentPermissionMode = "acceptEdits";
1615
- send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [] });
1799
+ send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1616
1800
 
1617
1801
  // Build prompt from plan content (sent from client) or plan file path
1618
1802
  var clientPlanContent = msg.planContent || "";
@@ -1646,7 +1830,7 @@ function createProjectContext(opts) {
1646
1830
  newSession.sentToolResults = {};
1647
1831
  sendToSession(newSession.localId, { type: "status", status: "processing" });
1648
1832
  newSession.acceptEditsAfterStart = true;
1649
- sdk.startQuery(newSession, planPrompt);
1833
+ sdk.startQuery(newSession, planPrompt, undefined, getLinuxUserForSession(newSession));
1650
1834
  } catch (e) {
1651
1835
  console.error("[project] Error starting plan execution:", e);
1652
1836
  sendTo(ws, { type: "error", text: "Failed to start plan execution: " + (e.message || e) });
@@ -1674,8 +1858,8 @@ function createProjectContext(opts) {
1674
1858
  onProcessingChanged();
1675
1859
  session.sentToolResults = {};
1676
1860
  sendToSession(session.localId, { type: "status", status: "processing" });
1677
- if (!session.queryInstance) {
1678
- sdk.startQuery(session, feedback);
1861
+ if (!session.queryInstance && !session.worker) {
1862
+ sdk.startQuery(session, feedback, undefined, getLinuxUserForSession(session));
1679
1863
  } else {
1680
1864
  sdk.pushMessage(session, feedback);
1681
1865
  }
@@ -1705,6 +1889,26 @@ function createProjectContext(opts) {
1705
1889
  return;
1706
1890
  }
1707
1891
 
1892
+ // --- MCP elicitation response ---
1893
+ if (msg.type === "elicitation_response") {
1894
+ var session = getSessionForWs(ws);
1895
+ if (!session) return;
1896
+ var pending = session.pendingElicitations && session.pendingElicitations[msg.requestId];
1897
+ if (!pending) return;
1898
+ delete session.pendingElicitations[msg.requestId];
1899
+ if (msg.action === "accept") {
1900
+ pending.resolve({ action: "accept", content: msg.content || {} });
1901
+ } else {
1902
+ pending.resolve({ action: "reject" });
1903
+ }
1904
+ sm.sendAndRecord(session, {
1905
+ type: "elicitation_resolved",
1906
+ requestId: msg.requestId,
1907
+ action: msg.action,
1908
+ });
1909
+ return;
1910
+ }
1911
+
1708
1912
  // --- Browse directories (for add-project autocomplete) ---
1709
1913
  if (msg.type === "browse_dir") {
1710
1914
  var rawPath = (msg.path || "").replace(/^~/, process.env.HOME || "/");
@@ -1767,6 +1971,40 @@ function createProjectContext(opts) {
1767
1971
  return;
1768
1972
  }
1769
1973
 
1974
+ // --- Create new empty project ---
1975
+ if (msg.type === "create_project") {
1976
+ var createName = (msg.name || "").trim();
1977
+ if (!createName || !/^[a-zA-Z0-9_-]+$/.test(createName)) {
1978
+ sendTo(ws, { type: "add_project_result", ok: false, error: "Invalid name. Use only letters, numbers, dashes, and underscores." });
1979
+ return;
1980
+ }
1981
+ if (typeof opts.onCreateProject === "function") {
1982
+ var createResult = opts.onCreateProject(createName, ws._clayUser);
1983
+ sendTo(ws, { type: "add_project_result", ok: createResult.ok, slug: createResult.slug, error: createResult.error });
1984
+ } else {
1985
+ sendTo(ws, { type: "add_project_result", ok: false, error: "Not supported" });
1986
+ }
1987
+ return;
1988
+ }
1989
+
1990
+ // --- Clone project from GitHub ---
1991
+ if (msg.type === "clone_project") {
1992
+ var cloneUrl = (msg.url || "").trim();
1993
+ if (!cloneUrl || (!/^https?:\/\//.test(cloneUrl) && !/^git@/.test(cloneUrl))) {
1994
+ sendTo(ws, { type: "add_project_result", ok: false, error: "Invalid URL. Use https:// or git@ format." });
1995
+ return;
1996
+ }
1997
+ sendTo(ws, { type: "clone_project_progress", status: "cloning" });
1998
+ if (typeof opts.onCloneProject === "function") {
1999
+ opts.onCloneProject(cloneUrl, ws._clayUser, function (cloneResult) {
2000
+ sendTo(ws, { type: "add_project_result", ok: cloneResult.ok, slug: cloneResult.slug, error: cloneResult.error });
2001
+ });
2002
+ } else {
2003
+ sendTo(ws, { type: "add_project_result", ok: false, error: "Not supported" });
2004
+ }
2005
+ return;
2006
+ }
2007
+
1770
2008
  // --- Pre-check: does the project have tasks/schedules? ---
1771
2009
  if (msg.type === "remove_project_check") {
1772
2010
  var checkSlug = msg.slug;
@@ -1929,16 +2167,31 @@ function createProjectContext(opts) {
1929
2167
  return;
1930
2168
  }
1931
2169
  try {
1932
- var items = fs.readdirSync(fsDir, { withFileTypes: true });
2170
+ var fsListUserInfo = getOsUserInfoForWs(ws);
1933
2171
  var entries = [];
1934
- for (var fi = 0; fi < items.length; fi++) {
1935
- var item = items[fi];
1936
- if (item.isDirectory() && IGNORED_DIRS.has(item.name)) continue;
1937
- entries.push({
1938
- name: item.name,
1939
- type: item.isDirectory() ? "dir" : "file",
1940
- path: path.relative(cwd, path.join(fsDir, item.name)).split(path.sep).join("/"),
1941
- });
2172
+ if (fsListUserInfo) {
2173
+ // Run as target OS user to respect Linux file permissions
2174
+ var rawEntries = fsAsUser("list", { dir: fsDir }, fsListUserInfo);
2175
+ for (var fi = 0; fi < rawEntries.length; fi++) {
2176
+ var re = rawEntries[fi];
2177
+ if (re.isDir && IGNORED_DIRS.has(re.name)) continue;
2178
+ entries.push({
2179
+ name: re.name,
2180
+ type: re.isDir ? "dir" : "file",
2181
+ path: path.relative(cwd, path.join(fsDir, re.name)).split(path.sep).join("/"),
2182
+ });
2183
+ }
2184
+ } else {
2185
+ var items = fs.readdirSync(fsDir, { withFileTypes: true });
2186
+ for (var fi = 0; fi < items.length; fi++) {
2187
+ var item = items[fi];
2188
+ if (item.isDirectory() && IGNORED_DIRS.has(item.name)) continue;
2189
+ entries.push({
2190
+ name: item.name,
2191
+ type: item.isDirectory() ? "dir" : "file",
2192
+ path: path.relative(cwd, path.join(fsDir, item.name)).split(path.sep).join("/"),
2193
+ });
2194
+ }
1942
2195
  }
1943
2196
  sendTo(ws, { type: "fs_list_result", path: msg.path || ".", entries: entries });
1944
2197
  // Auto-watch the directory for changes
@@ -1956,20 +2209,38 @@ function createProjectContext(opts) {
1956
2209
  return;
1957
2210
  }
1958
2211
  try {
1959
- var stat = fs.statSync(fsFile);
2212
+ var fsReadUserInfo = getOsUserInfoForWs(ws);
1960
2213
  var ext = path.extname(fsFile).toLowerCase();
1961
- if (stat.size > FS_MAX_SIZE) {
1962
- sendTo(ws, { type: "fs_read_result", path: msg.path, binary: true, size: stat.size, error: "File too large (" + (stat.size / 1024 / 1024).toFixed(1) + " MB)" });
1963
- return;
1964
- }
1965
- if (BINARY_EXTS.has(ext)) {
1966
- var result = { type: "fs_read_result", path: msg.path, binary: true, size: stat.size };
1967
- if (IMAGE_EXTS.has(ext)) result.imageUrl = "api/file?path=" + encodeURIComponent(msg.path);
1968
- sendTo(ws, result);
1969
- return;
2214
+ if (fsReadUserInfo) {
2215
+ // Run stat and read as target OS user
2216
+ var statResult = fsAsUser("stat", { file: fsFile }, fsReadUserInfo);
2217
+ if (statResult.size > FS_MAX_SIZE) {
2218
+ sendTo(ws, { type: "fs_read_result", path: msg.path, binary: true, size: statResult.size, error: "File too large (" + (statResult.size / 1024 / 1024).toFixed(1) + " MB)" });
2219
+ return;
2220
+ }
2221
+ if (BINARY_EXTS.has(ext)) {
2222
+ var result = { type: "fs_read_result", path: msg.path, binary: true, size: statResult.size };
2223
+ if (IMAGE_EXTS.has(ext)) result.imageUrl = "api/file?path=" + encodeURIComponent(msg.path);
2224
+ sendTo(ws, result);
2225
+ return;
2226
+ }
2227
+ var readResult = fsAsUser("read", { file: fsFile, readContent: true }, fsReadUserInfo);
2228
+ sendTo(ws, { type: "fs_read_result", path: msg.path, content: readResult.content, size: statResult.size });
2229
+ } else {
2230
+ var stat = fs.statSync(fsFile);
2231
+ if (stat.size > FS_MAX_SIZE) {
2232
+ sendTo(ws, { type: "fs_read_result", path: msg.path, binary: true, size: stat.size, error: "File too large (" + (stat.size / 1024 / 1024).toFixed(1) + " MB)" });
2233
+ return;
2234
+ }
2235
+ if (BINARY_EXTS.has(ext)) {
2236
+ var result = { type: "fs_read_result", path: msg.path, binary: true, size: stat.size };
2237
+ if (IMAGE_EXTS.has(ext)) result.imageUrl = "api/file?path=" + encodeURIComponent(msg.path);
2238
+ sendTo(ws, result);
2239
+ return;
2240
+ }
2241
+ var content = fs.readFileSync(fsFile, "utf8");
2242
+ sendTo(ws, { type: "fs_read_result", path: msg.path, content: content, size: stat.size });
1970
2243
  }
1971
- var content = fs.readFileSync(fsFile, "utf8");
1972
- sendTo(ws, { type: "fs_read_result", path: msg.path, content: content, size: stat.size });
1973
2244
  } catch (e) {
1974
2245
  sendTo(ws, { type: "fs_read_result", path: msg.path, error: e.message });
1975
2246
  }
@@ -1984,7 +2255,12 @@ function createProjectContext(opts) {
1984
2255
  return;
1985
2256
  }
1986
2257
  try {
1987
- fs.writeFileSync(fsWriteFile, msg.content || "", "utf8");
2258
+ var fsWriteUserInfo = getOsUserInfoForWs(ws);
2259
+ if (fsWriteUserInfo) {
2260
+ fsAsUser("write", { file: fsWriteFile, content: msg.content || "" }, fsWriteUserInfo);
2261
+ } else {
2262
+ fs.writeFileSync(fsWriteFile, msg.content || "", "utf8");
2263
+ }
1988
2264
  sendTo(ws, { type: "fs_write_result", path: msg.path, ok: true });
1989
2265
  } catch (e) {
1990
2266
  sendTo(ws, { type: "fs_write_result", path: msg.path, ok: false, error: e.message });
@@ -2309,7 +2585,7 @@ function createProjectContext(opts) {
2309
2585
 
2310
2586
  // --- Web terminal ---
2311
2587
  if (msg.type === "term_create") {
2312
- var t = tm.create(msg.cols || 80, msg.rows || 24);
2588
+ var t = tm.create(msg.cols || 80, msg.rows || 24, getOsUserInfoForWs(ws));
2313
2589
  if (!t) {
2314
2590
  sendTo(ws, { type: "term_error", error: "Cannot create terminal (node-pty not available or limit reached)" });
2315
2591
  return;
@@ -2447,7 +2723,7 @@ function createProjectContext(opts) {
2447
2723
  onProcessingChanged();
2448
2724
  craftingSession.sentToolResults = {};
2449
2725
  sendToSession(craftingSession.localId, { type: "status", status: "processing" });
2450
- sdk.startQuery(craftingSession, craftingPrompt);
2726
+ sdk.startQuery(craftingSession, craftingPrompt, undefined, getLinuxUserForSession(craftingSession));
2451
2727
 
2452
2728
  send({ type: "ralph_crafting_started", sessionId: craftingSession.localId, taskId: newLoopId, source: recordSource });
2453
2729
  send({ type: "ralph_phase", phase: "crafting", wizardData: loopState.wizardData, craftingSessionId: craftingSession.localId });
@@ -2639,6 +2915,12 @@ function createProjectContext(opts) {
2639
2915
  var session = getSessionForWs(ws);
2640
2916
  if (!session) return;
2641
2917
 
2918
+ // Backfill ownerId for legacy sessions restored without one
2919
+ if (!session.ownerId && ws._clayUser) {
2920
+ session.ownerId = ws._clayUser.id;
2921
+ sm.saveSessionFile(session);
2922
+ }
2923
+
2642
2924
  var userMsg = { type: "user_message", text: msg.text || "" };
2643
2925
  if (msg.images && msg.images.length > 0) {
2644
2926
  userMsg.imageCount = msg.images.length;
@@ -2654,6 +2936,14 @@ function createProjectContext(opts) {
2654
2936
  session.title = (msg.text || "Image").substring(0, 50);
2655
2937
  sm.saveSessionFile(session);
2656
2938
  sm.broadcastSessionList();
2939
+ // Sync auto-title to SDK
2940
+ if (session.cliSessionId) {
2941
+ getSDK().then(function(sdk) {
2942
+ sdk.renameSession(session.cliSessionId, session.title, { dir: cwd }).catch(function(e) {
2943
+ console.error("[project] SDK renameSession failed:", e.message);
2944
+ });
2945
+ }).catch(function() {});
2946
+ }
2657
2947
  }
2658
2948
 
2659
2949
  var fullText = msg.text || "";
@@ -2669,8 +2959,8 @@ function createProjectContext(opts) {
2669
2959
  onProcessingChanged();
2670
2960
  session.sentToolResults = {};
2671
2961
  sendToSession(session.localId, { type: "status", status: "processing" });
2672
- if (!session.queryInstance) {
2673
- sdk.startQuery(session, fullText, msg.images);
2962
+ if (!session.queryInstance && !session.worker) {
2963
+ sdk.startQuery(session, fullText, msg.images, getLinuxUserForSession(session));
2674
2964
  } else {
2675
2965
  sdk.pushMessage(session, fullText, msg.images);
2676
2966
  }
@@ -2847,7 +3137,14 @@ function createProjectContext(opts) {
2847
3137
  var fileExt = path.extname(absFile).toLowerCase();
2848
3138
  if (!IMAGE_EXTS.has(fileExt)) { res.writeHead(403); res.end("Only image files"); return true; }
2849
3139
  try {
2850
- var fileContent = fs.readFileSync(absFile);
3140
+ var fileServeUserInfo = getOsUserInfoForReq(req);
3141
+ var fileContent;
3142
+ if (fileServeUserInfo) {
3143
+ var binResult = fsAsUser("read_binary", { file: absFile }, fileServeUserInfo);
3144
+ fileContent = binResult.buffer;
3145
+ } else {
3146
+ fileContent = fs.readFileSync(absFile);
3147
+ }
2851
3148
  var fileMime = MIME_TYPES[fileExt] || "application/octet-stream";
2852
3149
  res.writeHead(200, { "Content-Type": fileMime, "Cache-Control": "no-cache" });
2853
3150
  res.end(fileContent);
@@ -2880,13 +3177,19 @@ function createProjectContext(opts) {
2880
3177
  res.end('{"error":"only https:// URLs are allowed"}');
2881
3178
  return;
2882
3179
  }
2883
- var spawnCwd = scope === "global" ? os.homedir() : cwd;
3180
+ var skillUserInfo = getOsUserInfoForReq(req);
3181
+ var spawnCwd = scope === "global" ? (skillUserInfo ? skillUserInfo.home : os.homedir()) : cwd;
2884
3182
  var scopeFlag = scope === "global" ? "--global" : "--project";
2885
- var child = spawn("npx", ["skills", "add", url, "--skill", skill, "--yes", scopeFlag], {
3183
+ var skillSpawnOpts = {
2886
3184
  cwd: spawnCwd,
2887
3185
  stdio: "ignore",
2888
3186
  detached: false,
2889
- });
3187
+ };
3188
+ if (skillUserInfo) {
3189
+ skillSpawnOpts.uid = skillUserInfo.uid;
3190
+ skillSpawnOpts.gid = skillUserInfo.gid;
3191
+ }
3192
+ var child = spawn("npx", ["skills", "add", url, "--skill", skill, "--yes", scopeFlag], skillSpawnOpts);
2890
3193
  child.on("close", function (code) {
2891
3194
  var success = code === 0;
2892
3195
  send({
@@ -2931,7 +3234,8 @@ function createProjectContext(opts) {
2931
3234
  res.end('{"error":"invalid skill name"}');
2932
3235
  return;
2933
3236
  }
2934
- var baseDir = scope === "global" ? os.homedir() : cwd;
3237
+ var uninstallUserInfo = getOsUserInfoForReq(req);
3238
+ var baseDir = scope === "global" ? (uninstallUserInfo ? uninstallUserInfo.home : os.homedir()) : cwd;
2935
3239
  var skillDir = path.join(baseDir, ".claude", "skills", skill);
2936
3240
  // Safety: ensure skillDir is inside the expected .claude/skills directory
2937
3241
  var expectedParent = path.join(baseDir, ".claude", "skills");
@@ -2942,7 +3246,17 @@ function createProjectContext(opts) {
2942
3246
  return;
2943
3247
  }
2944
3248
  try {
2945
- fs.rmSync(resolved, { recursive: true, force: true });
3249
+ if (uninstallUserInfo) {
3250
+ // Run rm as target user to respect permissions
3251
+ var rmScript = "var fs = require('fs'); fs.rmSync(" + JSON.stringify(resolved) + ", { recursive: true, force: true });";
3252
+ execFileSync(process.execPath, ["-e", rmScript], {
3253
+ uid: uninstallUserInfo.uid,
3254
+ gid: uninstallUserInfo.gid,
3255
+ timeout: 10000,
3256
+ });
3257
+ } else {
3258
+ fs.rmSync(resolved, { recursive: true, force: true });
3259
+ }
2946
3260
  send({
2947
3261
  type: "skill_uninstalled",
2948
3262
  skill: skill,
@@ -3085,6 +3399,7 @@ function createProjectContext(opts) {
3085
3399
  clients: clients.size,
3086
3400
  sessions: sessionCount,
3087
3401
  isProcessing: hasProcessing,
3402
+ projectOwnerId: projectOwnerId,
3088
3403
  };
3089
3404
  if (usersModule.isMultiUser()) {
3090
3405
  var seen = {};
@@ -3110,7 +3425,7 @@ function createProjectContext(opts) {
3110
3425
 
3111
3426
  function setTitle(newTitle) {
3112
3427
  title = newTitle || null;
3113
- send({ type: "info", cwd: cwd, slug: slug, project: title || project, version: currentVersion, debug: !!debug, lanHost: lanHost, projectCount: getProjectCount(), projects: getProjectList() });
3428
+ send({ type: "info", cwd: cwd, slug: slug, project: title || project, version: currentVersion, debug: !!debug, osUsers: osUsers, lanHost: lanHost, projectCount: getProjectCount(), projects: getProjectList(), projectOwnerId: projectOwnerId });
3114
3429
  }
3115
3430
 
3116
3431
  function setIcon(newIcon) {
@@ -3141,6 +3456,8 @@ function createProjectContext(opts) {
3141
3456
  removeSchedule: function (id) { return loopRegistry.remove(id); },
3142
3457
  setTitle: setTitle,
3143
3458
  setIcon: setIcon,
3459
+ setProjectOwner: function (ownerId) { projectOwnerId = ownerId; },
3460
+ getProjectOwner: function () { return projectOwnerId; },
3144
3461
  refreshUserProfile: function (userId) {
3145
3462
  var user = usersModule.findUserById(userId);
3146
3463
  if (!user) return;
@@ -3154,6 +3471,8 @@ function createProjectContext(opts) {
3154
3471
  },
3155
3472
  warmup: function () {
3156
3473
  sdk.warmup();
3474
+ // Migrate existing relay session titles to SDK format (one-time, async)
3475
+ sm.migrateSessionTitles(getSDK, cwd);
3157
3476
  },
3158
3477
  destroy: destroy,
3159
3478
  };