clay-server 2.27.0-beta.8 → 2.27.0

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.
Files changed (72) hide show
  1. package/README.md +10 -0
  2. package/lib/daemon-projects.js +164 -0
  3. package/lib/daemon.js +13 -126
  4. package/lib/mates-identity.js +132 -0
  5. package/lib/mates-knowledge.js +113 -0
  6. package/lib/mates-prompts.js +398 -0
  7. package/lib/mates.js +40 -599
  8. package/lib/project-connection.js +2 -0
  9. package/lib/project-debate.js +19 -12
  10. package/lib/project-http.js +4 -2
  11. package/lib/project-loop.js +110 -48
  12. package/lib/project-mate-interaction.js +4 -0
  13. package/lib/project-notifications.js +210 -0
  14. package/lib/project-sessions.js +5 -2
  15. package/lib/project-user-message.js +2 -1
  16. package/lib/project.js +26 -2
  17. package/lib/public/app.js +1193 -8521
  18. package/lib/public/css/command-palette.css +14 -0
  19. package/lib/public/css/loop.css +301 -0
  20. package/lib/public/css/notifications-center.css +190 -0
  21. package/lib/public/css/rewind.css +6 -0
  22. package/lib/public/index.html +89 -35
  23. package/lib/public/modules/app-connection.js +160 -0
  24. package/lib/public/modules/app-cursors.js +473 -0
  25. package/lib/public/modules/app-debate-ui.js +389 -0
  26. package/lib/public/modules/app-dm.js +627 -0
  27. package/lib/public/modules/app-favicon.js +212 -0
  28. package/lib/public/modules/app-header.js +229 -0
  29. package/lib/public/modules/app-home-hub.js +600 -0
  30. package/lib/public/modules/app-loop-ui.js +589 -0
  31. package/lib/public/modules/app-loop-wizard.js +439 -0
  32. package/lib/public/modules/app-messages.js +1560 -0
  33. package/lib/public/modules/app-misc.js +299 -0
  34. package/lib/public/modules/app-notifications.js +372 -0
  35. package/lib/public/modules/app-panels.js +888 -0
  36. package/lib/public/modules/app-projects.js +798 -0
  37. package/lib/public/modules/app-rate-limit.js +451 -0
  38. package/lib/public/modules/app-rendering.js +597 -0
  39. package/lib/public/modules/app-skills-install.js +234 -0
  40. package/lib/public/modules/command-palette.js +27 -4
  41. package/lib/public/modules/input.js +31 -20
  42. package/lib/public/modules/scheduler-config.js +1532 -0
  43. package/lib/public/modules/scheduler-history.js +79 -0
  44. package/lib/public/modules/scheduler.js +33 -1554
  45. package/lib/public/modules/session-search.js +13 -1
  46. package/lib/public/modules/sidebar-mates.js +812 -0
  47. package/lib/public/modules/sidebar-mobile.js +1269 -0
  48. package/lib/public/modules/sidebar-projects.js +1449 -0
  49. package/lib/public/modules/sidebar-sessions.js +986 -0
  50. package/lib/public/modules/sidebar.js +232 -4591
  51. package/lib/public/modules/store.js +27 -0
  52. package/lib/public/modules/ws-ref.js +7 -0
  53. package/lib/public/style.css +1 -0
  54. package/lib/sdk-bridge.js +96 -717
  55. package/lib/sdk-message-processor.js +587 -0
  56. package/lib/sdk-message-queue.js +42 -0
  57. package/lib/sdk-skill-discovery.js +131 -0
  58. package/lib/server-admin.js +712 -0
  59. package/lib/server-auth.js +737 -0
  60. package/lib/server-dm.js +221 -0
  61. package/lib/server-mates.js +281 -0
  62. package/lib/server-palette.js +110 -0
  63. package/lib/server-settings.js +479 -0
  64. package/lib/server-skills.js +280 -0
  65. package/lib/server.js +246 -2755
  66. package/lib/sessions.js +11 -4
  67. package/lib/users-auth.js +146 -0
  68. package/lib/users-permissions.js +118 -0
  69. package/lib/users-preferences.js +210 -0
  70. package/lib/users.js +48 -398
  71. package/lib/ws-schema.js +498 -0
  72. package/package.json +1 -1
package/lib/sessions.js CHANGED
@@ -19,6 +19,7 @@ function createSessionManager(opts) {
19
19
  var slashCommands = null; // shared across sessions
20
20
  var skillNames = null; // Claude-only skills to filter from slash menu
21
21
  var singleUserUnread = {}; // sessionLocalId -> unread count (single-user mode)
22
+ var permissionRequestIndex = {}; // requestId -> sessionLocalId (O(1) lookup)
22
23
 
23
24
  // --- Session persistence (centralized in ~/.clay/sessions/{encoded-cwd}/) ---
24
25
  var sessionsBase = path.join(config.CONFIG_DIR, "sessions");
@@ -521,11 +522,15 @@ function createSessionManager(opts) {
521
522
  if (ws.readyState === 1) ws.send(ioData);
522
523
  }
523
524
  // Track unread: increment on "done" for clients not viewing this session
525
+ // Only count if session has no owner (my session) or owner matches this client
524
526
  if (obj.type === "done" && ws._clayActiveSession !== session.localId) {
525
- if (!ws._clayUnread) ws._clayUnread = {};
526
- ws._clayUnread[session.localId] = (ws._clayUnread[session.localId] || 0) + 1;
527
- if (ws.readyState === 1) {
528
- ws.send(JSON.stringify({ type: "session_unread", id: session.localId, count: ws._clayUnread[session.localId] }));
527
+ var _isMySession = !session.ownerId || (ws._clayUser && ws._clayUser.id === session.ownerId);
528
+ if (_isMySession) {
529
+ if (!ws._clayUnread) ws._clayUnread = {};
530
+ ws._clayUnread[session.localId] = (ws._clayUnread[session.localId] || 0) + 1;
531
+ if (ws.readyState === 1) {
532
+ ws.send(JSON.stringify({ type: "session_unread", id: session.localId, count: ws._clayUnread[session.localId] }));
533
+ }
529
534
  }
530
535
  }
531
536
  });
@@ -664,6 +669,7 @@ function createSessionManager(opts) {
664
669
  historyIndex: i,
665
670
  snippet: snippet,
666
671
  role: entry.type === "user_message" ? "user" : "assistant",
672
+ ts: entry._ts || null,
667
673
  });
668
674
  }
669
675
  }
@@ -773,6 +779,7 @@ function createSessionManager(opts) {
773
779
  saveSessionFile(session);
774
780
  return { ok: true };
775
781
  },
782
+ permissionRequestIndex: permissionRequestIndex,
776
783
  };
777
784
  }
778
785
 
@@ -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 };