clay-server 2.12.0 → 2.13.0-beta.2

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
@@ -31,6 +31,7 @@ var { createServer, generateAuthToken } = require("./server");
31
31
  var { grantProjectAccess, revokeProjectAccess, provisionAllUsers, provisionLinuxUser, grantAllUsersAccess, deactivateLinuxUser, ensureProjectsDir } = require("./os-users");
32
32
  var usersModule = require("./users");
33
33
  var { scanWorktrees, createWorktree, removeWorktree, isWorktree } = require("./worktree");
34
+ var mates = require("./mates");
34
35
 
35
36
  var configFile = process.env.CLAY_CONFIG || process.env.CLAUDE_RELAY_CONFIG || require("./config").configPath();
36
37
  var config;
@@ -935,6 +936,19 @@ for (var i = 0; i < projects.length; i++) {
935
936
  }
936
937
  }
937
938
 
939
+ // 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 });
949
+ }
950
+ }
951
+
938
952
  // Sync ~/.clayrc on startup
939
953
  try { syncClayrc(config.projects); } catch (e) {}
940
954
 
package/lib/dm.js CHANGED
@@ -118,10 +118,10 @@ function getDmList(userId) {
118
118
  return dms;
119
119
  }
120
120
 
121
- // Extension point: check if a user is a mate (AI persona)
122
- // Returns false for now - will be implemented when Mates feature is added
121
+ // Check if a user is a mate (AI persona)
123
122
  function isMate(userId) {
124
- return false;
123
+ var mates = require("./mates");
124
+ return mates.isMate(userId);
125
125
  }
126
126
 
127
127
  module.exports = {
package/lib/mates.js ADDED
@@ -0,0 +1,222 @@
1
+ var fs = require("fs");
2
+ var path = require("path");
3
+ var crypto = require("crypto");
4
+ var config = require("./config");
5
+
6
+ var MATES_FILE = path.join(config.CONFIG_DIR, "mates.json");
7
+ var MATES_DIR = path.join(process.cwd(), ".claude", "mates");
8
+
9
+ // --- Default data ---
10
+
11
+ function defaultData() {
12
+ return { mates: [] };
13
+ }
14
+
15
+ // --- Load / Save ---
16
+
17
+ function loadMates() {
18
+ try {
19
+ var raw = fs.readFileSync(MATES_FILE, "utf8");
20
+ var data = JSON.parse(raw);
21
+ if (!data.mates) data.mates = [];
22
+ return data;
23
+ } catch (e) {
24
+ return defaultData();
25
+ }
26
+ }
27
+
28
+ function saveMates(data) {
29
+ fs.mkdirSync(path.dirname(MATES_FILE), { recursive: true });
30
+ var tmpPath = MATES_FILE + ".tmp";
31
+ fs.writeFileSync(tmpPath, JSON.stringify(data, null, 2));
32
+ fs.renameSync(tmpPath, MATES_FILE);
33
+ }
34
+
35
+ // --- CRUD ---
36
+
37
+ function generateMateId() {
38
+ return "mate_" + crypto.randomUUID();
39
+ }
40
+
41
+ function createMate(seedData, userId) {
42
+ var data = loadMates();
43
+ var id = generateMateId();
44
+
45
+ // Pick a random avatar color from a pleasant palette
46
+ var colors = ["#6c5ce7", "#00b894", "#e17055", "#0984e3", "#fdcb6e", "#e84393", "#00cec9", "#ff7675"];
47
+ var colorIdx = crypto.randomBytes(1)[0] % colors.length;
48
+
49
+ var mate = {
50
+ id: id,
51
+ name: null,
52
+ createdBy: userId,
53
+ createdAt: Date.now(),
54
+ seedData: seedData || {},
55
+ profile: {
56
+ displayName: null,
57
+ avatarColor: colors[colorIdx],
58
+ avatarStyle: "bottts",
59
+ avatarSeed: crypto.randomBytes(4).toString("hex"),
60
+ },
61
+ status: "interviewing",
62
+ interviewProjectPath: null,
63
+ };
64
+
65
+ data.mates.push(mate);
66
+ saveMates(data);
67
+
68
+ // Create the mate's identity directory
69
+ var mateDir = path.join(MATES_DIR, id);
70
+ fs.mkdirSync(mateDir, { recursive: true });
71
+
72
+ // Write initial mate.yaml
73
+ var yaml = "# Mate metadata\n";
74
+ yaml += "id: " + id + "\n";
75
+ yaml += "name: null\n";
76
+ yaml += "status: interviewing\n";
77
+ yaml += "createdBy: " + userId + "\n";
78
+ yaml += "createdAt: " + mate.createdAt + "\n";
79
+ yaml += "relationship: " + (seedData.relationship || "assistant") + "\n";
80
+ yaml += "activities: " + JSON.stringify(seedData.activity || []) + "\n";
81
+ yaml += "autonomy: " + (seedData.autonomy || "always_ask") + "\n";
82
+ fs.writeFileSync(path.join(mateDir, "mate.yaml"), yaml);
83
+
84
+ // Write initial CLAUDE.md (will be replaced by interview)
85
+ var claudeMd = "# Mate Identity\n\n";
86
+ claudeMd += "This mate is currently being interviewed. Identity will be generated after the interview.\n\n";
87
+ claudeMd += "## Seed Data\n\n";
88
+ claudeMd += "- Relationship: " + (seedData.relationship || "assistant") + "\n";
89
+ if (seedData.activity && seedData.activity.length > 0) {
90
+ claudeMd += "- Activities: " + seedData.activity.join(", ") + "\n";
91
+ }
92
+ if (seedData.communicationStyle && seedData.communicationStyle.length > 0) {
93
+ claudeMd += "- Communication: " + seedData.communicationStyle.join(", ") + "\n";
94
+ }
95
+ claudeMd += "- Autonomy: " + (seedData.autonomy || "always_ask") + "\n";
96
+ fs.writeFileSync(path.join(mateDir, "CLAUDE.md"), claudeMd);
97
+
98
+ return mate;
99
+ }
100
+
101
+ function getMate(id) {
102
+ var data = loadMates();
103
+ for (var i = 0; i < data.mates.length; i++) {
104
+ if (data.mates[i].id === id) return data.mates[i];
105
+ }
106
+ return null;
107
+ }
108
+
109
+ function updateMate(id, updates) {
110
+ var data = loadMates();
111
+ for (var i = 0; i < data.mates.length; i++) {
112
+ if (data.mates[i].id === id) {
113
+ var keys = Object.keys(updates);
114
+ for (var j = 0; j < keys.length; j++) {
115
+ data.mates[i][keys[j]] = updates[keys[j]];
116
+ }
117
+ saveMates(data);
118
+ return data.mates[i];
119
+ }
120
+ }
121
+ return null;
122
+ }
123
+
124
+ function deleteMate(id) {
125
+ var data = loadMates();
126
+ var before = data.mates.length;
127
+ data.mates = data.mates.filter(function (m) {
128
+ return m.id !== id;
129
+ });
130
+ if (data.mates.length === before) return { error: "Mate not found" };
131
+ saveMates(data);
132
+
133
+ // Remove mate directory
134
+ var mateDir = path.join(MATES_DIR, id);
135
+ try {
136
+ fs.rmSync(mateDir, { recursive: true, force: true });
137
+ } catch (e) {
138
+ // Directory may not exist
139
+ }
140
+
141
+ return { ok: true };
142
+ }
143
+
144
+ function getAllMates() {
145
+ var data = loadMates();
146
+ return data.mates;
147
+ }
148
+
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) {
157
+ if (!id) return false;
158
+ if (typeof id === "string" && id.indexOf("mate_") === 0) {
159
+ // Double check it exists in registry
160
+ return !!getMate(id);
161
+ }
162
+ return false;
163
+ }
164
+
165
+ function getMateDir(id) {
166
+ return path.join(MATES_DIR, id);
167
+ }
168
+
169
+ // Format seed data as a human-readable context string
170
+ function formatSeedContext(seedData) {
171
+ if (!seedData) return "";
172
+ var parts = [];
173
+
174
+ if (seedData.relationship) {
175
+ parts.push("The user wants a " + seedData.relationship + " relationship.");
176
+ }
177
+
178
+ if (seedData.activity && seedData.activity.length > 0) {
179
+ parts.push("Primary activities: " + seedData.activity.join(", ") + ".");
180
+ }
181
+
182
+ if (seedData.communicationStyle && seedData.communicationStyle.length > 0) {
183
+ var styleLabels = {
184
+ direct_concise: "direct and concise",
185
+ soft_detailed: "soft and detailed",
186
+ witty: "witty",
187
+ encouraging: "encouraging",
188
+ formal: "formal",
189
+ no_nonsense: "no-nonsense",
190
+ };
191
+ var styles = seedData.communicationStyle.map(function (s) { return styleLabels[s] || s.replace(/_/g, " "); });
192
+ parts.push("Communication style: " + styles.join(", ") + ".");
193
+ }
194
+
195
+ if (seedData.autonomy) {
196
+ var autonomyLabels = {
197
+ always_ask: "Always ask before acting",
198
+ minor_stuff_ok: "Handle minor stuff without asking",
199
+ mostly_autonomous: "Mostly autonomous, ask for big decisions",
200
+ fully_autonomous: "Fully autonomous",
201
+ };
202
+ parts.push("Autonomy: " + (autonomyLabels[seedData.autonomy] || seedData.autonomy) + ".");
203
+ }
204
+
205
+ return parts.join(" ");
206
+ }
207
+
208
+ module.exports = {
209
+ MATES_FILE: MATES_FILE,
210
+ MATES_DIR: MATES_DIR,
211
+ loadMates: loadMates,
212
+ saveMates: saveMates,
213
+ createMate: createMate,
214
+ getMate: getMate,
215
+ updateMate: updateMate,
216
+ deleteMate: deleteMate,
217
+ getAllMates: getAllMates,
218
+ getMatesByUser: getMatesByUser,
219
+ isMate: isMate,
220
+ getMateDir: getMateDir,
221
+ formatSeedContext: formatSeedContext,
222
+ };
package/lib/project.js CHANGED
@@ -11,7 +11,6 @@ var { execFileSync, spawn } = require("child_process");
11
11
  var { createLoopRegistry } = require("./scheduler");
12
12
  var usersModule = require("./users");
13
13
  var { resolveOsUserInfo, fsAsUser } = require("./os-users");
14
-
15
14
  var MAX_UPLOAD_BYTES = 50 * 1024 * 1024; // 50 MB
16
15
 
17
16
  // Validate environment variable string (KEY=VALUE per line)
@@ -114,6 +113,7 @@ function createProjectContext(opts) {
114
113
  var osUsers = opts.osUsers || false;
115
114
  var projectOwnerId = opts.projectOwnerId || null;
116
115
  var worktreeMeta = opts.worktreeMeta || null; // { parentSlug, branch, accessible }
116
+ var isMate = opts.isMate || false;
117
117
  var onCreateWorktree = opts.onCreateWorktree || null;
118
118
  var latestVersion = null;
119
119
 
@@ -1214,13 +1214,91 @@ function createProjectContext(opts) {
1214
1214
 
1215
1215
  function handleMessage(ws, msg) {
1216
1216
  // --- DM messages (delegated to server-level handler) ---
1217
- if (msg.type === "dm_open" || msg.type === "dm_send" || msg.type === "dm_list" || msg.type === "dm_typing" || msg.type === "dm_add_favorite" || msg.type === "dm_remove_favorite") {
1217
+ if (msg.type === "dm_open" || msg.type === "dm_send" || msg.type === "dm_list" || msg.type === "dm_typing" || msg.type === "dm_add_favorite" || msg.type === "dm_remove_favorite" || msg.type === "mate_create" || msg.type === "mate_list" || msg.type === "mate_delete" || msg.type === "mate_update") {
1218
1218
  if (typeof opts.onDmMessage === "function") {
1219
1219
  opts.onDmMessage(ws, msg);
1220
1220
  }
1221
1221
  return;
1222
1222
  }
1223
1223
 
1224
+ // --- Knowledge file management ---
1225
+ if (msg.type === "knowledge_list") {
1226
+ var knowledgeDir = path.join(cwd, "knowledge");
1227
+ var files = [];
1228
+ try {
1229
+ var entries = fs.readdirSync(knowledgeDir);
1230
+ for (var ki = 0; ki < entries.length; ki++) {
1231
+ if (entries[ki].endsWith(".md")) {
1232
+ var stat = fs.statSync(path.join(knowledgeDir, entries[ki]));
1233
+ files.push({ name: entries[ki], size: stat.size, mtime: stat.mtimeMs });
1234
+ }
1235
+ }
1236
+ } catch (e) { /* dir may not exist */ }
1237
+ files.sort(function (a, b) { return b.mtime - a.mtime; });
1238
+ sendTo(ws, { type: "knowledge_list", files: files });
1239
+ return;
1240
+ }
1241
+
1242
+ if (msg.type === "knowledge_read") {
1243
+ if (!msg.name) return;
1244
+ var safeName = path.basename(msg.name);
1245
+ var filePath = path.join(cwd, "knowledge", safeName);
1246
+ try {
1247
+ var content = fs.readFileSync(filePath, "utf8");
1248
+ sendTo(ws, { type: "knowledge_content", name: safeName, content: content });
1249
+ } catch (e) {
1250
+ sendTo(ws, { type: "knowledge_content", name: safeName, content: "", error: "File not found" });
1251
+ }
1252
+ return;
1253
+ }
1254
+
1255
+ if (msg.type === "knowledge_save") {
1256
+ if (!msg.name || typeof msg.content !== "string") return;
1257
+ var safeName = path.basename(msg.name);
1258
+ if (!safeName.endsWith(".md")) safeName += ".md";
1259
+ var knowledgeDir = path.join(cwd, "knowledge");
1260
+ fs.mkdirSync(knowledgeDir, { recursive: true });
1261
+ fs.writeFileSync(path.join(knowledgeDir, safeName), msg.content);
1262
+ // Return updated list
1263
+ var files = [];
1264
+ try {
1265
+ var entries = fs.readdirSync(knowledgeDir);
1266
+ for (var ki = 0; ki < entries.length; ki++) {
1267
+ if (entries[ki].endsWith(".md")) {
1268
+ var stat = fs.statSync(path.join(knowledgeDir, entries[ki]));
1269
+ files.push({ name: entries[ki], size: stat.size, mtime: stat.mtimeMs });
1270
+ }
1271
+ }
1272
+ } catch (e) {}
1273
+ files.sort(function (a, b) { return b.mtime - a.mtime; });
1274
+ sendTo(ws, { type: "knowledge_saved", name: safeName });
1275
+ sendTo(ws, { type: "knowledge_list", files: files });
1276
+ return;
1277
+ }
1278
+
1279
+ if (msg.type === "knowledge_delete") {
1280
+ if (!msg.name) return;
1281
+ var safeName = path.basename(msg.name);
1282
+ var filePath = path.join(cwd, "knowledge", safeName);
1283
+ try { fs.unlinkSync(filePath); } catch (e) {}
1284
+ // Return updated list
1285
+ var knowledgeDir = path.join(cwd, "knowledge");
1286
+ var files = [];
1287
+ try {
1288
+ var entries = fs.readdirSync(knowledgeDir);
1289
+ for (var ki = 0; ki < entries.length; ki++) {
1290
+ if (entries[ki].endsWith(".md")) {
1291
+ var stat = fs.statSync(path.join(knowledgeDir, entries[ki]));
1292
+ files.push({ name: entries[ki], size: stat.size, mtime: stat.mtimeMs });
1293
+ }
1294
+ }
1295
+ } catch (e) {}
1296
+ files.sort(function (a, b) { return b.mtime - a.mtime; });
1297
+ sendTo(ws, { type: "knowledge_deleted", name: safeName });
1298
+ sendTo(ws, { type: "knowledge_list", files: files });
1299
+ return;
1300
+ }
1301
+
1224
1302
  if (msg.type === "push_subscribe") {
1225
1303
  if (pushModule && msg.subscription) pushModule.addSubscription(msg.subscription, msg.replaceEndpoint);
1226
1304
  return;
@@ -3496,6 +3574,9 @@ function createProjectContext(opts) {
3496
3574
  isProcessing: hasProcessing,
3497
3575
  projectOwnerId: projectOwnerId,
3498
3576
  };
3577
+ if (isMate) {
3578
+ status.isMate = true;
3579
+ }
3499
3580
  if (worktreeMeta) {
3500
3581
  status.isWorktree = true;
3501
3582
  status.parentSlug = worktreeMeta.parentSlug;