clay-server 2.13.0-beta.5 → 2.13.0-beta.6

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/daemon.js CHANGED
@@ -936,16 +936,40 @@ for (var i = 0; i < projects.length; i++) {
936
936
  }
937
937
  }
938
938
 
939
+ // Migrate legacy mates storage before registration
940
+ mates.migrateLegacyMates();
941
+
939
942
  // Register existing mates as projects
940
- var allMates = mates.getAllMates();
941
- for (var mi = 0; mi < allMates.length; mi++) {
942
- var m = allMates[mi];
943
- var mateDir = path.join(mates.MATES_DIR, m.id);
944
- var mateSlug = "mate-" + m.id;
945
- var mateName = (m.profile && m.profile.displayName) || m.name || "New Mate";
946
- if (fs.existsSync(mateDir)) {
947
- console.log("[daemon] Adding mate project:", mateSlug);
948
- relay.addProject(mateDir, mateSlug, mateName, null, m.createdBy, null, { isMate: true });
943
+ if (usersModule.isMultiUser()) {
944
+ // Multi-user mode: iterate over all users and load each user's mates
945
+ var allUsers = usersModule.getAllUsers();
946
+ for (var ui = 0; ui < allUsers.length; ui++) {
947
+ var mateCtx = mates.buildMateCtx(allUsers[ui].id);
948
+ var userMates = mates.getAllMates(mateCtx);
949
+ for (var mi = 0; mi < userMates.length; mi++) {
950
+ var m = userMates[mi];
951
+ var mateDir = mates.getMateDir(mateCtx, m.id);
952
+ var mateSlug = "mate-" + m.id;
953
+ var mateName = (m.profile && m.profile.displayName) || m.name || "New Mate";
954
+ if (fs.existsSync(mateDir)) {
955
+ console.log("[daemon] Adding mate project:", mateSlug);
956
+ relay.addProject(mateDir, mateSlug, mateName, null, m.createdBy, null, { isMate: true });
957
+ }
958
+ }
959
+ }
960
+ } else {
961
+ // Single-user mode: use null ctx
962
+ var mateCtx = mates.buildMateCtx(null);
963
+ var allMates = mates.getAllMates(mateCtx);
964
+ for (var mi = 0; mi < allMates.length; mi++) {
965
+ var m = allMates[mi];
966
+ var mateDir = mates.getMateDir(mateCtx, m.id);
967
+ var mateSlug = "mate-" + m.id;
968
+ var mateName = (m.profile && m.profile.displayName) || m.name || "New Mate";
969
+ if (fs.existsSync(mateDir)) {
970
+ console.log("[daemon] Adding mate project:", mateSlug);
971
+ relay.addProject(mateDir, mateSlug, mateName, null, m.createdBy, null, { isMate: true });
972
+ }
949
973
  }
950
974
  }
951
975
 
package/lib/dm.js CHANGED
@@ -118,10 +118,10 @@ function getDmList(userId) {
118
118
  return dms;
119
119
  }
120
120
 
121
- // Check if a user is a mate (AI persona)
121
+ // Check if a user ID looks like a mate (AI persona) by format only
122
122
  function isMate(userId) {
123
123
  var mates = require("./mates");
124
- return mates.isMate(userId);
124
+ return mates.isMateIdFormat(userId);
125
125
  }
126
126
 
127
127
  module.exports = {
package/lib/mates.js CHANGED
@@ -3,8 +3,40 @@ var path = require("path");
3
3
  var crypto = require("crypto");
4
4
  var config = require("./config");
5
5
 
6
- var MATES_FILE = path.join(config.CONFIG_DIR, "mates.json");
7
- var MATES_DIR = path.join(process.cwd(), ".claude", "mates");
6
+ // --- Path resolution ---
7
+
8
+ function resolveMatesRoot(ctx) {
9
+ // OS-users mode: per-linuxUser home directory
10
+ if (ctx && ctx.linuxUser) {
11
+ return path.join("/home", ctx.linuxUser, ".clay", "mates");
12
+ }
13
+ // Multi-user mode: per-userId subdirectory
14
+ if (ctx && ctx.multiUser && ctx.userId) {
15
+ return path.join(config.CONFIG_DIR, "mates", ctx.userId);
16
+ }
17
+ // Single-user mode: flat directory
18
+ return path.join(config.CONFIG_DIR, "mates");
19
+ }
20
+
21
+ function buildMateCtx(userId) {
22
+ if (!userId) return { userId: null, multiUser: false, linuxUser: null };
23
+ // Lazy require to avoid circular dependency
24
+ var users = require("./users");
25
+ var multiUser = users.isMultiUser();
26
+ var linuxUser = null;
27
+ if (multiUser && userId) {
28
+ var user = users.findUserById(userId);
29
+ if (user && user.linuxUser) {
30
+ linuxUser = user.linuxUser;
31
+ }
32
+ }
33
+ return { userId: userId, multiUser: multiUser, linuxUser: linuxUser };
34
+ }
35
+
36
+ function isMateIdFormat(id) {
37
+ if (!id) return false;
38
+ return typeof id === "string" && id.indexOf("mate_") === 0;
39
+ }
8
40
 
9
41
  // --- Default data ---
10
42
 
@@ -14,9 +46,13 @@ function defaultData() {
14
46
 
15
47
  // --- Load / Save ---
16
48
 
17
- function loadMates() {
49
+ function matesFilePath(ctx) {
50
+ return path.join(resolveMatesRoot(ctx), "mates.json");
51
+ }
52
+
53
+ function loadMates(ctx) {
18
54
  try {
19
- var raw = fs.readFileSync(MATES_FILE, "utf8");
55
+ var raw = fs.readFileSync(matesFilePath(ctx), "utf8");
20
56
  var data = JSON.parse(raw);
21
57
  if (!data.mates) data.mates = [];
22
58
  return data;
@@ -25,11 +61,12 @@ function loadMates() {
25
61
  }
26
62
  }
27
63
 
28
- function saveMates(data) {
29
- fs.mkdirSync(path.dirname(MATES_FILE), { recursive: true });
30
- var tmpPath = MATES_FILE + ".tmp";
64
+ function saveMates(ctx, data) {
65
+ var filePath = matesFilePath(ctx);
66
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
67
+ var tmpPath = filePath + ".tmp";
31
68
  fs.writeFileSync(tmpPath, JSON.stringify(data, null, 2));
32
- fs.renameSync(tmpPath, MATES_FILE);
69
+ fs.renameSync(tmpPath, filePath);
33
70
  }
34
71
 
35
72
  // --- CRUD ---
@@ -38,9 +75,10 @@ function generateMateId() {
38
75
  return "mate_" + crypto.randomUUID();
39
76
  }
40
77
 
41
- function createMate(seedData, userId) {
42
- var data = loadMates();
78
+ function createMate(ctx, seedData) {
79
+ var data = loadMates(ctx);
43
80
  var id = generateMateId();
81
+ var userId = ctx ? ctx.userId : null;
44
82
 
45
83
  // Pick a random avatar color from a pleasant palette
46
84
  var colors = ["#6c5ce7", "#00b894", "#e17055", "#0984e3", "#fdcb6e", "#e84393", "#00cec9", "#ff7675"];
@@ -63,10 +101,10 @@ function createMate(seedData, userId) {
63
101
  };
64
102
 
65
103
  data.mates.push(mate);
66
- saveMates(data);
104
+ saveMates(ctx, data);
67
105
 
68
106
  // Create the mate's identity directory
69
- var mateDir = path.join(MATES_DIR, id);
107
+ var mateDir = getMateDir(ctx, id);
70
108
  fs.mkdirSync(mateDir, { recursive: true });
71
109
 
72
110
  // Write initial mate.yaml
@@ -98,40 +136,40 @@ function createMate(seedData, userId) {
98
136
  return mate;
99
137
  }
100
138
 
101
- function getMate(id) {
102
- var data = loadMates();
139
+ function getMate(ctx, id) {
140
+ var data = loadMates(ctx);
103
141
  for (var i = 0; i < data.mates.length; i++) {
104
142
  if (data.mates[i].id === id) return data.mates[i];
105
143
  }
106
144
  return null;
107
145
  }
108
146
 
109
- function updateMate(id, updates) {
110
- var data = loadMates();
147
+ function updateMate(ctx, id, updates) {
148
+ var data = loadMates(ctx);
111
149
  for (var i = 0; i < data.mates.length; i++) {
112
150
  if (data.mates[i].id === id) {
113
151
  var keys = Object.keys(updates);
114
152
  for (var j = 0; j < keys.length; j++) {
115
153
  data.mates[i][keys[j]] = updates[keys[j]];
116
154
  }
117
- saveMates(data);
155
+ saveMates(ctx, data);
118
156
  return data.mates[i];
119
157
  }
120
158
  }
121
159
  return null;
122
160
  }
123
161
 
124
- function deleteMate(id) {
125
- var data = loadMates();
162
+ function deleteMate(ctx, id) {
163
+ var data = loadMates(ctx);
126
164
  var before = data.mates.length;
127
165
  data.mates = data.mates.filter(function (m) {
128
166
  return m.id !== id;
129
167
  });
130
168
  if (data.mates.length === before) return { error: "Mate not found" };
131
- saveMates(data);
169
+ saveMates(ctx, data);
132
170
 
133
171
  // Remove mate directory
134
- var mateDir = path.join(MATES_DIR, id);
172
+ var mateDir = getMateDir(ctx, id);
135
173
  try {
136
174
  fs.rmSync(mateDir, { recursive: true, force: true });
137
175
  } catch (e) {
@@ -141,29 +179,86 @@ function deleteMate(id) {
141
179
  return { ok: true };
142
180
  }
143
181
 
144
- function getAllMates() {
145
- var data = loadMates();
182
+ function getAllMates(ctx) {
183
+ var data = loadMates(ctx);
146
184
  return data.mates;
147
185
  }
148
186
 
149
- function getMatesByUser(userId) {
150
- var data = loadMates();
151
- return data.mates.filter(function (m) {
152
- return m.createdBy === userId;
153
- });
154
- }
155
-
156
- function isMate(id) {
187
+ function isMate(ctx, id) {
157
188
  if (!id) return false;
158
189
  if (typeof id === "string" && id.indexOf("mate_") === 0) {
159
190
  // Double check it exists in registry
160
- return !!getMate(id);
191
+ return !!getMate(ctx, id);
161
192
  }
162
193
  return false;
163
194
  }
164
195
 
165
- function getMateDir(id) {
166
- return path.join(MATES_DIR, id);
196
+ function getMateDir(ctx, id) {
197
+ return path.join(resolveMatesRoot(ctx), id);
198
+ }
199
+
200
+ // --- Migration ---
201
+
202
+ function migrateLegacyMates() {
203
+ var legacyFile = path.join(config.CONFIG_DIR, "mates.json");
204
+ if (!fs.existsSync(legacyFile)) return;
205
+
206
+ // Check if already migrated
207
+ var migratedMarker = legacyFile + ".migrated";
208
+ if (fs.existsSync(migratedMarker)) return;
209
+
210
+ try {
211
+ var raw = fs.readFileSync(legacyFile, "utf8");
212
+ var data = JSON.parse(raw);
213
+ if (!data.mates || data.mates.length === 0) {
214
+ // Nothing to migrate, just mark as done
215
+ fs.renameSync(legacyFile, migratedMarker);
216
+ return;
217
+ }
218
+
219
+ // Group mates by createdBy
220
+ var byUser = {};
221
+ for (var i = 0; i < data.mates.length; i++) {
222
+ var m = data.mates[i];
223
+ var key = m.createdBy || "__null__";
224
+ if (!byUser[key]) byUser[key] = [];
225
+ byUser[key].push(m);
226
+ }
227
+
228
+ // Write each user's mates to their own storage path
229
+ var keys = Object.keys(byUser);
230
+ for (var k = 0; k < keys.length; k++) {
231
+ var userId = keys[k] === "__null__" ? null : keys[k];
232
+ var ctx = buildMateCtx(userId);
233
+ var userData = { mates: byUser[keys[k]] };
234
+ saveMates(ctx, userData);
235
+
236
+ // Move mate identity directories to new location
237
+ var legacyMatesDir = path.join(config.CONFIG_DIR, "mates");
238
+ var newRoot = resolveMatesRoot(ctx);
239
+ for (var mi = 0; mi < byUser[keys[k]].length; mi++) {
240
+ var mateId = byUser[keys[k]][mi].id;
241
+ var oldDir = path.join(legacyMatesDir, mateId);
242
+ var newDir = path.join(newRoot, mateId);
243
+ if (fs.existsSync(oldDir) && oldDir !== newDir) {
244
+ fs.mkdirSync(path.dirname(newDir), { recursive: true });
245
+ try {
246
+ fs.renameSync(oldDir, newDir);
247
+ } catch (e) {
248
+ // Cross-device or other issue, copy instead
249
+ fs.cpSync(oldDir, newDir, { recursive: true });
250
+ fs.rmSync(oldDir, { recursive: true, force: true });
251
+ }
252
+ }
253
+ }
254
+ }
255
+
256
+ // Mark legacy file as migrated
257
+ fs.renameSync(legacyFile, migratedMarker);
258
+ console.log("[mates] Migrated legacy mates.json to per-user storage");
259
+ } catch (e) {
260
+ console.error("[mates] Legacy migration failed:", e.message);
261
+ }
167
262
  }
168
263
 
169
264
  // Format seed data as a human-readable context string
@@ -206,8 +301,9 @@ function formatSeedContext(seedData) {
206
301
  }
207
302
 
208
303
  module.exports = {
209
- MATES_FILE: MATES_FILE,
210
- MATES_DIR: MATES_DIR,
304
+ resolveMatesRoot: resolveMatesRoot,
305
+ buildMateCtx: buildMateCtx,
306
+ isMateIdFormat: isMateIdFormat,
211
307
  loadMates: loadMates,
212
308
  saveMates: saveMates,
213
309
  createMate: createMate,
@@ -215,8 +311,8 @@ module.exports = {
215
311
  updateMate: updateMate,
216
312
  deleteMate: deleteMate,
217
313
  getAllMates: getAllMates,
218
- getMatesByUser: getMatesByUser,
219
314
  isMate: isMate,
220
315
  getMateDir: getMateDir,
316
+ migrateLegacyMates: migrateLegacyMates,
221
317
  formatSeedContext: formatSeedContext,
222
318
  };
package/lib/server.js CHANGED
@@ -2614,7 +2614,8 @@ function createServer(opts) {
2614
2614
  }
2615
2615
  }
2616
2616
  // Include mates in the list
2617
- var mateList = mates.getAllMates();
2617
+ var mateCtx = mates.buildMateCtx(userId);
2618
+ var mateList = mates.getAllMates(mateCtx);
2618
2619
  ws.send(JSON.stringify({ type: "dm_list", dms: dmList, mates: mateList }));
2619
2620
  return;
2620
2621
  }
@@ -2623,16 +2624,25 @@ function createServer(opts) {
2623
2624
  if (!msg.targetUserId) return;
2624
2625
 
2625
2626
  // Check if target is a mate
2626
- if (mates.isMate(msg.targetUserId)) {
2627
- var mate = mates.getMate(msg.targetUserId);
2627
+ var mateCtx2 = mates.buildMateCtx(userId);
2628
+ if (mates.isMate(mateCtx2, msg.targetUserId)) {
2629
+ var mate = mates.getMate(mateCtx2, msg.targetUserId);
2628
2630
  if (!mate) return;
2631
+ // Ensure mate project is registered (survives server restarts)
2632
+ var mateSlug2 = "mate-" + mate.id;
2633
+ if (!projects.has(mateSlug2)) {
2634
+ var mateDir2 = mates.getMateDir(mateCtx2, mate.id);
2635
+ fs.mkdirSync(mateDir2, { recursive: true });
2636
+ var mateName2 = (mate.profile && mate.profile.displayName) || mate.name || "New Mate";
2637
+ addProject(mateDir2, mateSlug2, mateName2, null, mate.createdBy || userId, null, { isMate: true });
2638
+ }
2629
2639
  var mp = mate.profile || {};
2630
2640
  ws.send(JSON.stringify({
2631
2641
  type: "dm_history",
2632
2642
  dmKey: "mate:" + mate.id,
2633
2643
  messages: dm.loadHistory("mate:" + mate.id),
2634
2644
  isMate: true,
2635
- projectSlug: "mate-" + mate.id,
2645
+ projectSlug: mateSlug2,
2636
2646
  targetUser: {
2637
2647
  id: mate.id,
2638
2648
  displayName: mp.displayName || mate.name || "New Mate",
@@ -2690,8 +2700,9 @@ function createServer(opts) {
2690
2700
  var parts = msg.dmKey.split(":");
2691
2701
 
2692
2702
  // Handle mate DM: dmKey is "mate:mate_xxx"
2693
- if (parts[0] === "mate" && mates.isMate(parts[1])) {
2694
- var mate = mates.getMate(parts[1]);
2703
+ var mateCtx3 = mates.buildMateCtx(userId);
2704
+ if (parts[0] === "mate" && mates.isMate(mateCtx3, parts[1])) {
2705
+ var mate = mates.getMate(mateCtx3, parts[1]);
2695
2706
  if (!mate) return;
2696
2707
  // Verify sender is the mate's creator
2697
2708
  if (mate.createdBy !== userId) return;
@@ -2758,9 +2769,10 @@ function createServer(opts) {
2758
2769
  if (msg.type === "mate_create") {
2759
2770
  if (!msg.seedData) return;
2760
2771
  try {
2761
- var mate = mates.createMate(msg.seedData, userId);
2772
+ var mateCtx4 = mates.buildMateCtx(userId);
2773
+ var mate = mates.createMate(mateCtx4, msg.seedData);
2762
2774
  // Register mate as a project
2763
- var mateDir = path.join(mates.MATES_DIR, mate.id);
2775
+ var mateDir = mates.getMateDir(mateCtx4, mate.id);
2764
2776
  var mateSlug = "mate-" + mate.id;
2765
2777
  var mateName = (mate.profile && mate.profile.displayName) || mate.name || "New Mate";
2766
2778
  addProject(mateDir, mateSlug, mateName, null, mate.createdBy, null, { isMate: true });
@@ -2772,14 +2784,27 @@ function createServer(opts) {
2772
2784
  }
2773
2785
 
2774
2786
  if (msg.type === "mate_list") {
2775
- var mateList = mates.getAllMates();
2787
+ var mateCtx5 = mates.buildMateCtx(userId);
2788
+ var mateList = mates.getAllMates(mateCtx5);
2789
+ // Ensure all mate projects are registered (survives server restarts)
2790
+ for (var mi = 0; mi < mateList.length; mi++) {
2791
+ var m = mateList[mi];
2792
+ var mSlug = "mate-" + m.id;
2793
+ if (!projects.has(mSlug)) {
2794
+ var mDir = mates.getMateDir(mateCtx5, m.id);
2795
+ fs.mkdirSync(mDir, { recursive: true });
2796
+ var mName = (m.profile && m.profile.displayName) || m.name || "New Mate";
2797
+ addProject(mDir, mSlug, mName, null, m.createdBy || userId, null, { isMate: true });
2798
+ }
2799
+ }
2776
2800
  ws.send(JSON.stringify({ type: "mate_list", mates: mateList }));
2777
2801
  return;
2778
2802
  }
2779
2803
 
2780
2804
  if (msg.type === "mate_delete") {
2781
2805
  if (!msg.mateId) return;
2782
- var result = mates.deleteMate(msg.mateId);
2806
+ var mateCtx6 = mates.buildMateCtx(userId);
2807
+ var result = mates.deleteMate(mateCtx6, msg.mateId);
2783
2808
  if (result.error) {
2784
2809
  ws.send(JSON.stringify({ type: "mate_error", error: result.error }));
2785
2810
  } else {
@@ -2799,7 +2824,8 @@ function createServer(opts) {
2799
2824
 
2800
2825
  if (msg.type === "mate_update") {
2801
2826
  if (!msg.mateId || !msg.updates) return;
2802
- var updated = mates.updateMate(msg.mateId, msg.updates);
2827
+ var mateCtx7 = mates.buildMateCtx(userId);
2828
+ var updated = mates.updateMate(mateCtx7, msg.mateId, msg.updates);
2803
2829
  if (updated) {
2804
2830
  ws.send(JSON.stringify({ type: "mate_updated", mate: updated }));
2805
2831
  // Broadcast update
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "2.13.0-beta.5",
3
+ "version": "2.13.0-beta.6",
4
4
  "description": "Web UI for Claude Code. Any device. Push notifications.",
5
5
  "bin": {
6
6
  "clay-server": "./bin/cli.js",