clay-server 2.27.0-beta.13 → 2.27.0-beta.14

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.
@@ -0,0 +1,131 @@
1
+ var fs = require("fs");
2
+ var path = require("path");
3
+
4
+ // Split shell command on operators (&&, ||, ;, |) while respecting quotes
5
+ // and parentheses. Returns array of command segments.
6
+ function splitShellSegments(cmd) {
7
+ var segments = [];
8
+ var current = "";
9
+ var inSingle = false;
10
+ var inDouble = false;
11
+ var parenDepth = 0;
12
+ var i = 0;
13
+ while (i < cmd.length) {
14
+ var ch = cmd[i];
15
+
16
+ // Handle escape
17
+ if (ch === "\\" && i + 1 < cmd.length && !inSingle) {
18
+ current += ch + cmd[i + 1];
19
+ i += 2;
20
+ continue;
21
+ }
22
+
23
+ // Quote tracking
24
+ if (ch === "'" && !inDouble) { inSingle = !inSingle; current += ch; i++; continue; }
25
+ if (ch === '"' && !inSingle) { inDouble = !inDouble; current += ch; i++; continue; }
26
+
27
+ // Inside quotes: no splitting
28
+ if (inSingle || inDouble) { current += ch; i++; continue; }
29
+
30
+ // Parentheses/subshell tracking
31
+ if (ch === "(" || ch === "$" && i + 1 < cmd.length && cmd[i + 1] === "(") {
32
+ parenDepth++;
33
+ current += ch;
34
+ i++;
35
+ continue;
36
+ }
37
+ if (ch === ")" && parenDepth > 0) {
38
+ parenDepth--;
39
+ current += ch;
40
+ i++;
41
+ continue;
42
+ }
43
+
44
+ // Inside subshell: no splitting
45
+ if (parenDepth > 0) { current += ch; i++; continue; }
46
+
47
+ // Check for operators: &&, ||, ;, |
48
+ if (ch === "&" && i + 1 < cmd.length && cmd[i + 1] === "&") {
49
+ segments.push(current);
50
+ current = "";
51
+ i += 2;
52
+ continue;
53
+ }
54
+ if (ch === "|" && i + 1 < cmd.length && cmd[i + 1] === "|") {
55
+ segments.push(current);
56
+ current = "";
57
+ i += 2;
58
+ continue;
59
+ }
60
+ if (ch === "|") {
61
+ segments.push(current);
62
+ current = "";
63
+ i++;
64
+ continue;
65
+ }
66
+ if (ch === ";") {
67
+ segments.push(current);
68
+ current = "";
69
+ i++;
70
+ continue;
71
+ }
72
+
73
+ current += ch;
74
+ i++;
75
+ }
76
+ if (current) segments.push(current);
77
+ return segments;
78
+ }
79
+
80
+ function attachSkillDiscovery(ctx) {
81
+ var cwd = ctx.cwd;
82
+
83
+ function discoverSkillDirs() {
84
+ var skills = {};
85
+ var dirs = [
86
+ path.join(require("./config").REAL_HOME, ".claude", "skills"),
87
+ path.join(cwd, ".claude", "skills"),
88
+ ];
89
+ for (var d = 0; d < dirs.length; d++) {
90
+ var base = dirs[d];
91
+ var entries;
92
+ try {
93
+ entries = fs.readdirSync(base, { withFileTypes: true });
94
+ } catch (e) {
95
+ continue; // directory doesn't exist
96
+ }
97
+ for (var i = 0; i < entries.length; i++) {
98
+ var entry = entries[i];
99
+ if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
100
+ var skillDir = path.join(base, entry.name);
101
+ var skillMd = path.join(skillDir, "SKILL.md");
102
+ try {
103
+ fs.accessSync(skillMd, fs.constants.R_OK);
104
+ // project skills override global skills with same name
105
+ skills[entry.name] = skillDir;
106
+ } catch (e) {
107
+ // no SKILL.md, skip
108
+ }
109
+ }
110
+ }
111
+ return skills;
112
+ }
113
+
114
+ function mergeSkills(sdkSkills, fsSkills) {
115
+ var merged = new Set();
116
+ if (Array.isArray(sdkSkills)) {
117
+ for (var i = 0; i < sdkSkills.length; i++) {
118
+ merged.add(sdkSkills[i]);
119
+ }
120
+ }
121
+ var fsNames = Object.keys(fsSkills);
122
+ for (var i = 0; i < fsNames.length; i++) {
123
+ merged.add(fsNames[i]);
124
+ }
125
+ return merged;
126
+ }
127
+
128
+ return { discoverSkillDirs: discoverSkillDirs, mergeSkills: mergeSkills };
129
+ }
130
+
131
+ module.exports = { splitShellSegments: splitShellSegments, attachSkillDiscovery: attachSkillDiscovery };
@@ -0,0 +1,146 @@
1
+ var crypto = require("crypto");
2
+
3
+ function attachAuth(deps) {
4
+ var loadUsers = deps.loadUsers;
5
+ var saveUsers = deps.saveUsers;
6
+ var findAdmin = deps.findAdmin;
7
+
8
+ // --- Multi-user mode ---
9
+
10
+ function isMultiUser() {
11
+ var data = loadUsers();
12
+ return !!data.multiUser;
13
+ }
14
+
15
+ function enableMultiUser() {
16
+ var data = loadUsers();
17
+ if (data.multiUser) {
18
+ // Already enabled -- check if admin exists
19
+ var admin = findAdmin(data);
20
+ if (admin) {
21
+ return { alreadyEnabled: true, hasAdmin: true, setupCode: null };
22
+ }
23
+ // Multi-user enabled but no admin -- regenerate setup code
24
+ var code = generateSetupCode();
25
+ data.setupCode = code;
26
+ saveUsers(data);
27
+ return { alreadyEnabled: true, hasAdmin: false, setupCode: code };
28
+ }
29
+ var code = generateSetupCode();
30
+ data.multiUser = true;
31
+ data.setupCode = code;
32
+ saveUsers(data);
33
+ return { alreadyEnabled: false, hasAdmin: false, setupCode: code };
34
+ }
35
+
36
+ function disableMultiUser() {
37
+ var data = loadUsers();
38
+ data.multiUser = false;
39
+ data.setupCode = null;
40
+ saveUsers(data);
41
+ }
42
+
43
+ // --- Setup code ---
44
+
45
+ function generateSetupCode() {
46
+ var chars = "abcdefghijkmnpqrstuvwxyz23456789"; // no ambiguous chars
47
+ var code = "";
48
+ var bytes = crypto.randomBytes(6);
49
+ for (var i = 0; i < 6; i++) {
50
+ code += chars[bytes[i] % chars.length];
51
+ }
52
+ return code;
53
+ }
54
+
55
+ function getSetupCode() {
56
+ var data = loadUsers();
57
+ if (data.setupCode) return data.setupCode;
58
+ // Defensive: if multi-user is on, no admin, and no code, auto-generate one
59
+ if (data.multiUser && !findAdmin(data)) {
60
+ var code = generateSetupCode();
61
+ data.setupCode = code;
62
+ saveUsers(data);
63
+ return code;
64
+ }
65
+ return null;
66
+ }
67
+
68
+ function clearSetupCode() {
69
+ var data = loadUsers();
70
+ data.setupCode = null;
71
+ saveUsers(data);
72
+ }
73
+
74
+ function validateSetupCode(code) {
75
+ var data = loadUsers();
76
+ if (!data.setupCode) return false;
77
+ return data.setupCode === code;
78
+ }
79
+
80
+ // --- Pin hashing ---
81
+
82
+ function hashPin(pin) {
83
+ return crypto.createHash("sha256").update("clay-user:" + pin).digest("hex");
84
+ }
85
+
86
+ // Generate a random 6-digit PIN
87
+ function generatePin() {
88
+ var digits = "";
89
+ var bytes = crypto.randomBytes(6);
90
+ for (var i = 0; i < 6; i++) {
91
+ digits += (bytes[i] % 10).toString();
92
+ }
93
+ return digits;
94
+ }
95
+
96
+ // --- Authentication ---
97
+
98
+ function authenticateUser(username, pin) {
99
+ var data = loadUsers();
100
+ var user = null;
101
+ for (var i = 0; i < data.users.length; i++) {
102
+ if (data.users[i].username.toLowerCase() === username.toLowerCase()) {
103
+ user = data.users[i];
104
+ break;
105
+ }
106
+ }
107
+ if (!user) return null;
108
+ var pinH = hashPin(pin);
109
+ if (user.pinHash !== pinH) return null;
110
+ return user;
111
+ }
112
+
113
+ // --- Auth tokens ---
114
+
115
+ function generateUserAuthToken(userId) {
116
+ var token = crypto.randomBytes(32).toString("hex");
117
+ return userId + ":" + token;
118
+ }
119
+
120
+ function parseAuthCookie(cookieValue) {
121
+ if (!cookieValue) return null;
122
+ var idx = cookieValue.indexOf(":");
123
+ if (idx < 0) return null;
124
+ return {
125
+ userId: cookieValue.substring(0, idx),
126
+ token: cookieValue.substring(idx + 1),
127
+ };
128
+ }
129
+
130
+ return {
131
+ isMultiUser: isMultiUser,
132
+ enableMultiUser: enableMultiUser,
133
+ disableMultiUser: disableMultiUser,
134
+ generateSetupCode: generateSetupCode,
135
+ getSetupCode: getSetupCode,
136
+ clearSetupCode: clearSetupCode,
137
+ validateSetupCode: validateSetupCode,
138
+ hashPin: hashPin,
139
+ generatePin: generatePin,
140
+ authenticateUser: authenticateUser,
141
+ generateUserAuthToken: generateUserAuthToken,
142
+ parseAuthCookie: parseAuthCookie,
143
+ };
144
+ }
145
+
146
+ module.exports = { attachAuth: attachAuth };
@@ -0,0 +1,118 @@
1
+ // --- Per-user RBAC permissions (default values for regular users) ---
2
+ var DEFAULT_PERMISSIONS = {
3
+ terminal: false,
4
+ fileBrowser: true,
5
+ createProject: true,
6
+ deleteProject: false,
7
+ skills: true,
8
+ sessionDelete: false,
9
+ scheduledTasks: false,
10
+ projectSettings: false,
11
+ };
12
+
13
+ var ALL_PERMISSIONS = {
14
+ terminal: true,
15
+ fileBrowser: true,
16
+ createProject: true,
17
+ deleteProject: true,
18
+ skills: true,
19
+ sessionDelete: true,
20
+ scheduledTasks: true,
21
+ projectSettings: true,
22
+ };
23
+
24
+ function attachPermissions(deps) {
25
+ var loadUsers = deps.loadUsers;
26
+ var saveUsers = deps.saveUsers;
27
+ var findUserById = deps.findUserById;
28
+
29
+ function getEffectivePermissions(user, osUsersMode) {
30
+ // OS-mode users with linuxUser are exempt from RBAC (OS handles isolation)
31
+ if (osUsersMode && user && user.linuxUser) return ALL_PERMISSIONS;
32
+ // Admin always has full permissions
33
+ if (user && user.role === "admin") return ALL_PERMISSIONS;
34
+ // Merge stored permissions with defaults (handles missing keys for forward-compat)
35
+ var stored = (user && user.permissions) || {};
36
+ var result = {};
37
+ var keys = Object.keys(DEFAULT_PERMISSIONS);
38
+ for (var i = 0; i < keys.length; i++) {
39
+ var k = keys[i];
40
+ result[k] = stored[k] !== undefined ? stored[k] : DEFAULT_PERMISSIONS[k];
41
+ }
42
+ return result;
43
+ }
44
+
45
+ function updateUserPermissions(userId, permissions) {
46
+ var data = loadUsers();
47
+ for (var i = 0; i < data.users.length; i++) {
48
+ if (data.users[i].id === userId) {
49
+ // Validate: only allow known permission keys with boolean values
50
+ var clean = {};
51
+ var keys = Object.keys(DEFAULT_PERMISSIONS);
52
+ for (var j = 0; j < keys.length; j++) {
53
+ var k = keys[j];
54
+ clean[k] = permissions[k] === true;
55
+ }
56
+ data.users[i].permissions = clean;
57
+ saveUsers(data);
58
+ return { ok: true, permissions: clean };
59
+ }
60
+ }
61
+ return { error: "User not found" };
62
+ }
63
+
64
+ // --- Project access helpers ---
65
+
66
+ function canAccessProject(userId, project) {
67
+ if (!project) return false;
68
+ // Public projects are accessible to all authenticated users
69
+ if (!project.visibility || project.visibility === "public") return true;
70
+ // Admin always has access
71
+ var user = findUserById(userId);
72
+ if (user && user.role === "admin") return true;
73
+ // Owner always has access to their own project
74
+ if (project.ownerId && project.ownerId === userId) return true;
75
+ // Private project -- check allowedUsers
76
+ var allowed = project.allowedUsers || [];
77
+ return allowed.indexOf(userId) >= 0;
78
+ }
79
+
80
+ function getAccessibleProjects(userId, projects) {
81
+ if (!projects) return [];
82
+ return projects.filter(function (p) {
83
+ return canAccessProject(userId, p);
84
+ });
85
+ }
86
+
87
+ // --- Session visibility helpers ---
88
+
89
+ function canAccessSession(userId, session, project) {
90
+ // Must have project access first
91
+ if (!canAccessProject(userId, project)) return false;
92
+ // Sessions without ownerId are legacy -- only admin can see them
93
+ if (!session.ownerId) {
94
+ var user = findUserById(userId);
95
+ return !!(user && user.role === "admin");
96
+ }
97
+ // Owner can always see their own sessions
98
+ if (session.ownerId === userId) return true;
99
+ // Shared sessions are visible to all project members (default)
100
+ if (!session.sessionVisibility || session.sessionVisibility === "shared") return true;
101
+ // Private sessions are only visible to the owner
102
+ return false;
103
+ }
104
+
105
+ return {
106
+ getEffectivePermissions: getEffectivePermissions,
107
+ updateUserPermissions: updateUserPermissions,
108
+ canAccessProject: canAccessProject,
109
+ getAccessibleProjects: getAccessibleProjects,
110
+ canAccessSession: canAccessSession,
111
+ };
112
+ }
113
+
114
+ module.exports = {
115
+ DEFAULT_PERMISSIONS: DEFAULT_PERMISSIONS,
116
+ ALL_PERMISSIONS: ALL_PERMISSIONS,
117
+ attachPermissions: attachPermissions,
118
+ };
@@ -0,0 +1,210 @@
1
+ function attachPreferences(deps) {
2
+ var loadUsers = deps.loadUsers;
3
+ var saveUsers = deps.saveUsers;
4
+
5
+ // --- DM Favorites ---
6
+
7
+ function getDmFavorites(userId) {
8
+ var data = loadUsers();
9
+ for (var i = 0; i < data.users.length; i++) {
10
+ if (data.users[i].id === userId) {
11
+ return data.users[i].dmFavorites || [];
12
+ }
13
+ }
14
+ return [];
15
+ }
16
+
17
+ function addDmFavorite(userId, targetUserId) {
18
+ var data = loadUsers();
19
+ for (var i = 0; i < data.users.length; i++) {
20
+ if (data.users[i].id === userId) {
21
+ if (!data.users[i].dmFavorites) data.users[i].dmFavorites = [];
22
+ if (data.users[i].dmFavorites.indexOf(targetUserId) === -1) {
23
+ data.users[i].dmFavorites.push(targetUserId);
24
+ saveUsers(data);
25
+ }
26
+ return data.users[i].dmFavorites;
27
+ }
28
+ }
29
+ return [];
30
+ }
31
+
32
+ function removeDmFavorite(userId, targetUserId) {
33
+ var data = loadUsers();
34
+ for (var i = 0; i < data.users.length; i++) {
35
+ if (data.users[i].id === userId) {
36
+ if (!data.users[i].dmFavorites) data.users[i].dmFavorites = [];
37
+ data.users[i].dmFavorites = data.users[i].dmFavorites.filter(function (id) {
38
+ return id !== targetUserId;
39
+ });
40
+ saveUsers(data);
41
+ return data.users[i].dmFavorites;
42
+ }
43
+ }
44
+ return [];
45
+ }
46
+
47
+ // --- DM Hidden (dismissed from strip) ---
48
+
49
+ function getDmHidden(userId) {
50
+ var data = loadUsers();
51
+ for (var i = 0; i < data.users.length; i++) {
52
+ if (data.users[i].id === userId) {
53
+ return data.users[i].dmHidden || [];
54
+ }
55
+ }
56
+ return [];
57
+ }
58
+
59
+ function addDmHidden(userId, targetUserId) {
60
+ var data = loadUsers();
61
+ for (var i = 0; i < data.users.length; i++) {
62
+ if (data.users[i].id === userId) {
63
+ if (!data.users[i].dmHidden) data.users[i].dmHidden = [];
64
+ if (data.users[i].dmHidden.indexOf(targetUserId) === -1) {
65
+ data.users[i].dmHidden.push(targetUserId);
66
+ saveUsers(data);
67
+ }
68
+ return data.users[i].dmHidden;
69
+ }
70
+ }
71
+ return [];
72
+ }
73
+
74
+ function removeDmHidden(userId, targetUserId) {
75
+ var data = loadUsers();
76
+ for (var i = 0; i < data.users.length; i++) {
77
+ if (data.users[i].id === userId) {
78
+ if (!data.users[i].dmHidden) data.users[i].dmHidden = [];
79
+ data.users[i].dmHidden = data.users[i].dmHidden.filter(function (id) {
80
+ return id !== targetUserId;
81
+ });
82
+ saveUsers(data);
83
+ return data.users[i].dmHidden;
84
+ }
85
+ }
86
+ return [];
87
+ }
88
+
89
+ // --- Deleted built-in mate keys tracking ---
90
+
91
+ function getDeletedBuiltinKeys(userId) {
92
+ var data = loadUsers();
93
+ for (var i = 0; i < data.users.length; i++) {
94
+ if (data.users[i].id === userId) {
95
+ return data.users[i].deletedBuiltinKeys || [];
96
+ }
97
+ }
98
+ return [];
99
+ }
100
+
101
+ function addDeletedBuiltinKey(userId, key) {
102
+ var data = loadUsers();
103
+ for (var i = 0; i < data.users.length; i++) {
104
+ if (data.users[i].id === userId) {
105
+ if (!data.users[i].deletedBuiltinKeys) data.users[i].deletedBuiltinKeys = [];
106
+ if (data.users[i].deletedBuiltinKeys.indexOf(key) === -1) {
107
+ data.users[i].deletedBuiltinKeys.push(key);
108
+ saveUsers(data);
109
+ }
110
+ return;
111
+ }
112
+ }
113
+ }
114
+
115
+ function removeDeletedBuiltinKey(userId, key) {
116
+ var data = loadUsers();
117
+ for (var i = 0; i < data.users.length; i++) {
118
+ if (data.users[i].id === userId) {
119
+ if (!data.users[i].deletedBuiltinKeys) return;
120
+ data.users[i].deletedBuiltinKeys = data.users[i].deletedBuiltinKeys.filter(function (k) {
121
+ return k !== key;
122
+ });
123
+ saveUsers(data);
124
+ return;
125
+ }
126
+ }
127
+ }
128
+
129
+ // --- Per-user chat layout setting ---
130
+
131
+ function getChatLayout(userId) {
132
+ var data = loadUsers();
133
+ for (var i = 0; i < data.users.length; i++) {
134
+ if (data.users[i].id === userId) {
135
+ return data.users[i].chatLayout || "channel";
136
+ }
137
+ }
138
+ return "channel";
139
+ }
140
+
141
+ function setChatLayout(userId, layout) {
142
+ var val = (layout === "bubble") ? "bubble" : "channel";
143
+ var data = loadUsers();
144
+ for (var i = 0; i < data.users.length; i++) {
145
+ if (data.users[i].id === userId) {
146
+ data.users[i].chatLayout = val;
147
+ saveUsers(data);
148
+ return { ok: true, chatLayout: val };
149
+ }
150
+ }
151
+ return { error: "User not found" };
152
+ }
153
+
154
+ // --- Per-user auto-continue setting ---
155
+
156
+ function getAutoContinue(userId) {
157
+ var data = loadUsers();
158
+ for (var i = 0; i < data.users.length; i++) {
159
+ if (data.users[i].id === userId) {
160
+ return !!data.users[i].autoContinueOnRateLimit;
161
+ }
162
+ }
163
+ return false;
164
+ }
165
+
166
+ function setAutoContinue(userId, enabled) {
167
+ var data = loadUsers();
168
+ for (var i = 0; i < data.users.length; i++) {
169
+ if (data.users[i].id === userId) {
170
+ data.users[i].autoContinueOnRateLimit = !!enabled;
171
+ saveUsers(data);
172
+ return { ok: true, autoContinueOnRateLimit: !!enabled };
173
+ }
174
+ }
175
+ return { error: "User not found" };
176
+ }
177
+
178
+ // --- Mate onboarding ---
179
+
180
+ function setMateOnboarded(userId) {
181
+ var data = loadUsers();
182
+ for (var i = 0; i < data.users.length; i++) {
183
+ if (data.users[i].id === userId) {
184
+ data.users[i].mateOnboardingShown = true;
185
+ saveUsers(data);
186
+ return { ok: true };
187
+ }
188
+ }
189
+ return { error: "User not found" };
190
+ }
191
+
192
+ return {
193
+ getDmFavorites: getDmFavorites,
194
+ addDmFavorite: addDmFavorite,
195
+ removeDmFavorite: removeDmFavorite,
196
+ getDmHidden: getDmHidden,
197
+ addDmHidden: addDmHidden,
198
+ removeDmHidden: removeDmHidden,
199
+ getDeletedBuiltinKeys: getDeletedBuiltinKeys,
200
+ addDeletedBuiltinKey: addDeletedBuiltinKey,
201
+ removeDeletedBuiltinKey: removeDeletedBuiltinKey,
202
+ getChatLayout: getChatLayout,
203
+ setChatLayout: setChatLayout,
204
+ getAutoContinue: getAutoContinue,
205
+ setAutoContinue: setAutoContinue,
206
+ setMateOnboarded: setMateOnboarded,
207
+ };
208
+ }
209
+
210
+ module.exports = { attachPreferences: attachPreferences };