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/bin/cli.js +157 -1
- package/lib/daemon.js +341 -2
- package/lib/dm.js +135 -0
- package/lib/os-users.js +301 -0
- package/lib/pages.js +36 -0
- package/lib/project.js +386 -67
- package/lib/public/app.js +675 -17
- package/lib/public/css/admin.css +99 -10
- package/lib/public/css/filebrowser.css +22 -0
- package/lib/public/css/icon-strip.css +162 -1
- package/lib/public/css/menus.css +23 -0
- package/lib/public/css/messages.css +245 -0
- package/lib/public/css/overlays.css +88 -0
- package/lib/public/css/server-settings.css +30 -2
- package/lib/public/css/sidebar.css +4 -0
- package/lib/public/index.html +140 -66
- package/lib/public/modules/admin.js +179 -12
- package/lib/public/modules/input.js +13 -2
- package/lib/public/modules/notifications.js +3 -1
- package/lib/public/modules/project-settings.js +154 -168
- package/lib/public/modules/server-settings.js +78 -189
- package/lib/public/modules/settings-defaults.js +243 -0
- package/lib/public/modules/sidebar.js +112 -6
- package/lib/public/modules/terminal.js +48 -10
- package/lib/public/modules/tools.js +214 -1
- package/lib/sdk-bridge.js +634 -6
- package/lib/sdk-worker.js +446 -0
- package/lib/server.js +335 -3
- package/lib/sessions.js +26 -0
- package/lib/terminal-manager.js +2 -2
- package/lib/terminal.js +20 -4
- package/lib/updater.js +38 -11
- package/lib/users.js +79 -0
- package/package.json +2 -2
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
|
-
|
|
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
|
|
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
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
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
|
-
|
|
1218
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2170
|
+
var fsListUserInfo = getOsUserInfoForWs(ws);
|
|
1933
2171
|
var entries = [];
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
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
|
|
2212
|
+
var fsReadUserInfo = getOsUserInfoForWs(ws);
|
|
1960
2213
|
var ext = path.extname(fsFile).toLowerCase();
|
|
1961
|
-
if (
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
};
|