clay-server 2.13.0-beta.4 → 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/project.js CHANGED
@@ -626,13 +626,12 @@ function createProjectContext(opts) {
626
626
  console.log("[loop-registry] Schedule triggered: " + record.name + " → linked task " + loopId);
627
627
  }
628
628
 
629
- // Verify the loop directory and files exist
629
+ // Verify the loop directory and PROMPT.md exist
630
630
  var recDir = path.join(cwd, ".claude", "loops", loopId);
631
631
  try {
632
632
  fs.accessSync(path.join(recDir, "PROMPT.md"));
633
- fs.accessSync(path.join(recDir, "JUDGE.md"));
634
633
  } catch (e) {
635
- console.error("[loop-registry] Loop files missing for " + loopId);
634
+ console.error("[loop-registry] PROMPT.md missing for " + loopId);
636
635
  return;
637
636
  }
638
637
  // Set the loopId and start
@@ -675,8 +674,7 @@ function createProjectContext(opts) {
675
674
  try {
676
675
  judgeText = fs.readFileSync(judgePath, "utf8");
677
676
  } catch (e) {
678
- send({ type: "loop_error", text: "Missing JUDGE.md in " + dir });
679
- return;
677
+ judgeText = null;
680
678
  }
681
679
 
682
680
  var baseCommit;
@@ -700,7 +698,7 @@ function createProjectContext(opts) {
700
698
  loopState.promptText = promptText;
701
699
  loopState.judgeText = judgeText;
702
700
  loopState.iteration = 0;
703
- loopState.maxIterations = loopConfig.maxIterations || loopOpts.maxIterations || 20;
701
+ loopState.maxIterations = judgeText ? (loopConfig.maxIterations || loopOpts.maxIterations || 20) : 1;
704
702
  loopState.baseCommit = baseCommit;
705
703
  loopState.currentSessionId = null;
706
704
  loopState.judgeSessionId = null;
@@ -778,7 +776,11 @@ function createProjectContext(opts) {
778
776
  setTimeout(function() { runNextIteration(); }, 2000);
779
777
  return;
780
778
  }
781
- runJudge();
779
+ if (loopState.judgeText) {
780
+ runJudge();
781
+ } else {
782
+ finishLoop("pass");
783
+ }
782
784
  };
783
785
 
784
786
  // Watchdog: if onQueryComplete hasn't fired after 10 minutes, force error and retry
@@ -2975,6 +2977,11 @@ function createProjectContext(opts) {
2975
2977
  var tmpJudge = judgePath + ".tmp";
2976
2978
  fs.writeFileSync(tmpJudge, wData.judgeText);
2977
2979
  fs.renameSync(tmpJudge, judgePath);
2980
+ } else {
2981
+ // No judge: force single iteration
2982
+ var singleJson = loopJsonPath + ".tmp2";
2983
+ fs.writeFileSync(singleJson, JSON.stringify({ maxIterations: 1 }, null, 2));
2984
+ fs.renameSync(singleJson, loopJsonPath);
2978
2985
  }
2979
2986
 
2980
2987
  // Go straight to approval (no crafting needed)
@@ -3190,9 +3197,8 @@ function createProjectContext(opts) {
3190
3197
  var rerunDir = path.join(cwd, ".claude", "loops", rerunRec.id);
3191
3198
  try {
3192
3199
  fs.accessSync(path.join(rerunDir, "PROMPT.md"));
3193
- fs.accessSync(path.join(rerunDir, "JUDGE.md"));
3194
3200
  } catch (e) {
3195
- sendTo(ws, { type: "loop_registry_error", text: "Loop files missing for " + rerunRec.id });
3201
+ sendTo(ws, { type: "loop_registry_error", text: "PROMPT.md missing for " + rerunRec.id });
3196
3202
  return;
3197
3203
  }
3198
3204
  loopState.loopId = rerunRec.id;
package/lib/public/app.js CHANGED
@@ -859,6 +859,10 @@ import { initCommandPalette, handlePaletteSessionSwitch, setPaletteVersion } fro
859
859
  mateWs = null;
860
860
  }
861
861
  mateProjectSlug = null;
862
+ // If main WS was disconnected while in mate DM, reconnect now
863
+ if (ws && ws.readyState !== 1) {
864
+ connect();
865
+ }
862
866
  }
863
867
 
864
868
  function appendDmMessage(msg) {
@@ -3122,9 +3126,9 @@ import { initCommandPalette, handlePaletteSessionSwitch, setPaletteVersion } fro
3122
3126
  ws = new WebSocket(protocol + "//" + location.host + wsPath);
3123
3127
 
3124
3128
 
3125
- // If not connected within 3s, force retry
3129
+ // If not connected within 3s, force retry (skip if stashed during mate DM)
3126
3130
  connectTimeoutId = setTimeout(function () {
3127
- if (!connected) {
3131
+ if (!connected && !savedMainWs) {
3128
3132
  ws.onclose = null;
3129
3133
  ws.onerror = null;
3130
3134
  ws.close();
@@ -3183,6 +3187,9 @@ import { initCommandPalette, handlePaletteSessionSwitch, setPaletteVersion } fro
3183
3187
  };
3184
3188
 
3185
3189
  ws.onclose = function (e) {
3190
+ // If this WS is stashed while in mate DM, ignore close events
3191
+ if (savedMainWs === this) return;
3192
+
3186
3193
  if (connectTimeoutId) { clearTimeout(connectTimeoutId); connectTimeoutId = null; }
3187
3194
  setStatus("disconnected");
3188
3195
  processing = false;
@@ -3208,9 +3215,14 @@ import { initCommandPalette, handlePaletteSessionSwitch, setPaletteVersion } fro
3208
3215
  };
3209
3216
 
3210
3217
  ws.onerror = function () {
3218
+ // If this WS is stashed while in mate DM, ignore error events
3219
+ if (savedMainWs === this) return;
3211
3220
  };
3212
3221
 
3213
3222
  ws.onmessage = function (event) {
3223
+ // If this WS is stashed while in mate DM, ignore its messages
3224
+ if (savedMainWs === this) return;
3225
+
3214
3226
  // Backup: if we're receiving messages, we're connected
3215
3227
  if (!connected) {
3216
3228
  setStatus("connected");
@@ -4372,6 +4384,8 @@ import { initCommandPalette, handlePaletteSessionSwitch, setPaletteVersion } fro
4372
4384
 
4373
4385
  function scheduleReconnect() {
4374
4386
  if (reconnectTimer) return;
4387
+ // Don't reconnect main WS while in mate DM
4388
+ if (savedMainWs) return;
4375
4389
  reconnectTimer = setTimeout(function () {
4376
4390
  reconnectTimer = null;
4377
4391
  // Check if auth is still valid before reconnecting
@@ -242,9 +242,6 @@ export function renderAskUserQuestion(toolId, input) {
242
242
  answers[qIdx] = opt.label;
243
243
  var otherInput = qDiv.querySelector(".ask-user-other input");
244
244
  if (otherInput) otherInput.value = "";
245
- if (questions.length === 1) {
246
- submitAskUserAnswer(container, toolId, questions, answers, multiSelections);
247
- }
248
245
  }
249
246
  });
250
247
 
@@ -280,17 +277,14 @@ export function renderAskUserQuestion(toolId, input) {
280
277
  container.appendChild(qDiv);
281
278
  });
282
279
 
283
- // Submit button: show for multi-question or any multi-select question
284
- var hasMultiSelect = questions.some(function (q) { return q.multiSelect; });
285
- if (questions.length > 1 || hasMultiSelect) {
286
- var submitBtn = document.createElement("button");
287
- submitBtn.className = "ask-user-submit";
288
- submitBtn.textContent = "Submit";
289
- submitBtn.addEventListener("click", function () {
290
- submitAskUserAnswer(container, toolId, questions, answers, multiSelections);
291
- });
292
- container.appendChild(submitBtn);
293
- }
280
+ // Submit button: always show
281
+ var submitBtn = document.createElement("button");
282
+ submitBtn.className = "ask-user-submit";
283
+ submitBtn.textContent = "Submit";
284
+ submitBtn.addEventListener("click", function () {
285
+ submitAskUserAnswer(container, toolId, questions, answers, multiSelections);
286
+ });
287
+ container.appendChild(submitBtn);
294
288
 
295
289
  // Skip button
296
290
  var skipBtn = document.createElement("button");
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.4",
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",