clay-server 2.33.1 → 2.34.0-beta.1

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/os-users.js CHANGED
@@ -3,14 +3,19 @@
3
3
 
4
4
  var fs = require("fs");
5
5
  var path = require("path");
6
- var { execSync } = require("child_process");
6
+ var execFileSync = require("child_process").execFileSync;
7
+
8
+ function isSafeLinuxUsername(username) {
9
+ return typeof username === "string" && /^[a-z_][a-z0-9_-]*[$]?$/.test(username);
10
+ }
7
11
 
8
12
  /**
9
13
  * Resolve Linux user info from username via getent passwd.
10
14
  * Returns { uid, gid, home, user, shell } or throws on failure.
11
15
  */
12
16
  function resolveOsUserInfo(username) {
13
- var output = execSync("getent passwd " + username, { encoding: "utf8", timeout: 5000 }).trim();
17
+ if (!isSafeLinuxUsername(username)) throw new Error("Invalid Linux username");
18
+ var output = execFileSync("getent", ["passwd", username], { encoding: "utf8", timeout: 5000, stdio: "pipe" }).trim();
14
19
  // getent passwd format: username:x:uid:gid:gecos:home:shell
15
20
  var parts = output.split(":");
16
21
  if (parts.length < 7) throw new Error("Unexpected getent output for user " + username);
@@ -74,7 +79,7 @@ function fsAsUser(op, args, osUserInfo) {
74
79
  "var buf = fs.readFileSync(f);",
75
80
  "process.stdout.write(buf.toString('base64'));",
76
81
  ].join(" ");
77
- var binOutput = execSync(process.execPath + " -e " + JSON.stringify(script), {
82
+ var binOutput = execFileSync(process.execPath, ["-e", script], {
78
83
  encoding: "utf8",
79
84
  timeout: 10000,
80
85
  uid: osUserInfo.uid,
@@ -94,7 +99,7 @@ function fsAsUser(op, args, osUserInfo) {
94
99
  throw new Error("Unknown fsAsUser operation: " + op);
95
100
  }
96
101
 
97
- var output = execSync(process.execPath + " -e " + JSON.stringify(script), {
102
+ var output = execFileSync(process.execPath, ["-e", script], {
98
103
  encoding: "utf8",
99
104
  timeout: 10000,
100
105
  uid: osUserInfo.uid,
@@ -151,7 +156,7 @@ function getAclInstallCommand() {
151
156
  */
152
157
  function checkAclSupport() {
153
158
  try {
154
- execSync("which setfacl", { encoding: "utf8", timeout: 5000, stdio: "pipe" });
159
+ execFileSync("which", ["setfacl"], { encoding: "utf8", timeout: 5000, stdio: "pipe" });
155
160
  return { available: true };
156
161
  } catch (e) {
157
162
  return { available: false, installCmd: getAclInstallCommand() };
@@ -182,16 +187,22 @@ function grantProjectAccess(projectPath, linuxUser) {
182
187
  console.log("[os-users] Skipping ACL for home directory: " + projectPath);
183
188
  return;
184
189
  }
190
+ if (!isSafeLinuxUsername(linuxUser)) {
191
+ console.error("[os-users] Invalid Linux username for ACL grant: " + linuxUser);
192
+ return;
193
+ }
185
194
  try {
186
195
  // Recursive ACL for existing files
187
- execSync("setfacl -R -m u:" + linuxUser + ":rwX " + JSON.stringify(projectPath), {
196
+ execFileSync("setfacl", ["-R", "-m", "u:" + linuxUser + ":rwX", projectPath], {
188
197
  encoding: "utf8",
189
198
  timeout: 30000,
199
+ stdio: "pipe",
190
200
  });
191
201
  // Default ACL so new files also inherit access
192
- execSync("setfacl -R -d -m u:" + linuxUser + ":rwX " + JSON.stringify(projectPath), {
202
+ execFileSync("setfacl", ["-R", "-d", "-m", "u:" + linuxUser + ":rwX", projectPath], {
193
203
  encoding: "utf8",
194
204
  timeout: 30000,
205
+ stdio: "pipe",
195
206
  });
196
207
  console.log("[os-users] Granted ACL access for " + linuxUser + " on " + projectPath);
197
208
  } catch (e) {
@@ -214,14 +225,20 @@ function revokeProjectAccess(projectPath, linuxUser) {
214
225
  console.log("[os-users] Skipping ACL revoke for home directory: " + projectPath);
215
226
  return;
216
227
  }
228
+ if (!isSafeLinuxUsername(linuxUser)) {
229
+ console.error("[os-users] Invalid Linux username for ACL revoke: " + linuxUser);
230
+ return;
231
+ }
217
232
  try {
218
- execSync("setfacl -R -x u:" + linuxUser + " " + JSON.stringify(projectPath), {
233
+ execFileSync("setfacl", ["-R", "-x", "u:" + linuxUser, projectPath], {
219
234
  encoding: "utf8",
220
235
  timeout: 30000,
236
+ stdio: "pipe",
221
237
  });
222
- execSync("setfacl -R -d -x u:" + linuxUser + " " + JSON.stringify(projectPath), {
238
+ execFileSync("setfacl", ["-R", "-d", "-x", "u:" + linuxUser, projectPath], {
223
239
  encoding: "utf8",
224
240
  timeout: 30000,
241
+ stdio: "pipe",
225
242
  });
226
243
  console.log("[os-users] Revoked ACL access for " + linuxUser + " on " + projectPath);
227
244
  } catch (e) {
@@ -260,10 +277,11 @@ function toLinuxUsername(clayUsername) {
260
277
  */
261
278
  function ensureLinger(username) {
262
279
  try {
263
- var uid = execSync("id -u " + username, { encoding: "utf8", timeout: 5000, stdio: "pipe" }).trim();
280
+ if (!isSafeLinuxUsername(username)) throw new Error("Invalid Linux username");
281
+ var uid = execFileSync("id", ["-u", username], { encoding: "utf8", timeout: 5000, stdio: "pipe" }).trim();
264
282
  var lingerFile = "/var/lib/systemd/linger/" + username;
265
283
  if (fs.existsSync(lingerFile)) return;
266
- execSync("loginctl enable-linger " + username, {
284
+ execFileSync("loginctl", ["enable-linger", username], {
267
285
  encoding: "utf8",
268
286
  timeout: 10000,
269
287
  stdio: "pipe",
@@ -279,7 +297,8 @@ function ensureLinger(username) {
279
297
  */
280
298
  function linuxUserExists(username) {
281
299
  try {
282
- execSync("id " + username, { encoding: "utf8", timeout: 5000, stdio: "pipe" });
300
+ if (!isSafeLinuxUsername(username)) return false;
301
+ execFileSync("id", [username], { encoding: "utf8", timeout: 5000, stdio: "pipe" });
283
302
  return true;
284
303
  } catch (e) {
285
304
  return false;
@@ -288,7 +307,8 @@ function linuxUserExists(username) {
288
307
 
289
308
  function getLinuxUserHome(username) {
290
309
  try {
291
- var line = execSync("getent passwd " + username, { encoding: "utf8", timeout: 5000, stdio: "pipe" }).trim();
310
+ if (!isSafeLinuxUsername(username)) return "/home/" + (username || "");
311
+ var line = execFileSync("getent", ["passwd", username], { encoding: "utf8", timeout: 5000, stdio: "pipe" }).trim();
292
312
  var parts = line.split(":");
293
313
  return parts[5] || "/home/" + username;
294
314
  } catch (e) {
@@ -298,7 +318,8 @@ function getLinuxUserHome(username) {
298
318
 
299
319
  function getLinuxUserUid(username) {
300
320
  try {
301
- var uid = execSync("id -u " + username, { encoding: "utf8", timeout: 5000, stdio: "pipe" }).trim();
321
+ if (!isSafeLinuxUsername(username)) return null;
322
+ var uid = execFileSync("id", ["-u", username], { encoding: "utf8", timeout: 5000, stdio: "pipe" }).trim();
302
323
  return parseInt(uid, 10);
303
324
  } catch (e) {
304
325
  return null;
@@ -312,11 +333,13 @@ function getLinuxUserUid(username) {
312
333
  */
313
334
  function installClaudeCli(linuxName) {
314
335
  try {
336
+ if (!isSafeLinuxUsername(linuxName)) throw new Error("Invalid Linux username");
315
337
  // Download and run the Claude CLI install script as the target user
316
- execSync(
317
- "su - " + linuxName + " -c \"curl -fsSL https://claude.ai/install.sh | bash\"",
318
- { encoding: "utf8", timeout: 60000, stdio: "pipe" }
319
- );
338
+ execFileSync("su", ["-", linuxName, "-c", "curl -fsSL https://claude.ai/install.sh | bash"], {
339
+ encoding: "utf8",
340
+ timeout: 60000,
341
+ stdio: "pipe",
342
+ });
320
343
  console.log("[os-users] Claude CLI installed for " + linuxName);
321
344
  } catch (e) {
322
345
  var msg = (e.stderr || e.message || "").trim();
@@ -326,24 +349,19 @@ function installClaudeCli(linuxName) {
326
349
 
327
350
  // Append PATH export to the user's shell config if not already present
328
351
  try {
329
- var home = "/home/" + linuxName;
330
- var rcFile;
331
- if (fs.existsSync(home + "/.zshrc")) {
332
- rcFile = "~/.zshrc";
352
+ var userInfo = resolveOsUserInfo(linuxName);
353
+ var home = userInfo.home || ("/home/" + linuxName);
354
+ var rcPath = fs.existsSync(path.join(home, ".zshrc")) ? path.join(home, ".zshrc") : path.join(home, ".bashrc");
355
+ var exportLine = 'export PATH="$HOME/.local/bin:$PATH"';
356
+ var existing = "";
357
+ try { existing = fs.readFileSync(rcPath, "utf8"); } catch (e2) {}
358
+ if (existing.indexOf(exportLine) === -1) {
359
+ var prefix = existing && !/\n$/.test(existing) ? "\n" : "";
360
+ fs.appendFileSync(rcPath, prefix + exportLine + "\n", "utf8");
361
+ try { fs.chownSync(rcPath, userInfo.uid, userInfo.gid); } catch (e3) {}
362
+ console.log("[os-users] PATH export appended to " + rcPath + " for " + linuxName);
333
363
  } else {
334
- rcFile = "~/.bashrc";
335
- }
336
-
337
- // Check if PATH export is already present before appending
338
- var checkCmd = "su - " + linuxName + " -c \"grep -qF '/.local/bin' " + rcFile + "\"";
339
- try {
340
- execSync(checkCmd, { encoding: "utf8", timeout: 5000, stdio: "pipe" });
341
- console.log("[os-users] PATH already configured in " + rcFile + " for " + linuxName);
342
- } catch (grepErr) {
343
- // grep returned non-zero, meaning the line is not present; append it
344
- var appendCmd = "su - " + linuxName + " -c 'echo \"export PATH=\\\"\\$HOME/.local/bin:\\$PATH\\\"\" >> " + rcFile + "'";
345
- execSync(appendCmd, { encoding: "utf8", timeout: 5000, stdio: "pipe" });
346
- console.log("[os-users] PATH export appended to " + rcFile + " for " + linuxName);
364
+ console.log("[os-users] PATH already configured in " + rcPath + " for " + linuxName);
347
365
  }
348
366
  } catch (e) {
349
367
  var rcMsg = (e.stderr || e.message || "").trim();
@@ -368,7 +386,7 @@ function provisionLinuxUser(clayUsername) {
368
386
  }
369
387
 
370
388
  try {
371
- execSync("useradd -m -s /bin/bash " + linuxName, {
389
+ execFileSync("useradd", ["-m", "-s", "/bin/bash", linuxName], {
372
390
  encoding: "utf8",
373
391
  timeout: 15000,
374
392
  stdio: "pipe",
@@ -453,7 +471,8 @@ function grantAllUsersAccess(projectPath, usersModule) {
453
471
  */
454
472
  function deactivateLinuxUser(linuxUsername) {
455
473
  try {
456
- execSync("usermod -L " + linuxUsername, { encoding: "utf8", timeout: 5000, stdio: "pipe" });
474
+ if (!isSafeLinuxUsername(linuxUsername)) throw new Error("Invalid Linux username");
475
+ execFileSync("usermod", ["-L", linuxUsername], { encoding: "utf8", timeout: 5000, stdio: "pipe" });
457
476
  console.log("[os-users] Deactivated Linux user: " + linuxUsername);
458
477
  return { ok: true };
459
478
  } catch (e) {
@@ -493,4 +512,5 @@ module.exports = {
493
512
  isHomeDirectory: isHomeDirectory,
494
513
  getLinuxUserHome: getLinuxUserHome,
495
514
  getLinuxUserUid: getLinuxUserUid,
515
+ isSafeLinuxUsername: isSafeLinuxUsername,
496
516
  };
@@ -163,8 +163,8 @@ function attachHTTP(ctx) {
163
163
  var _osUM = require("./os-users");
164
164
  var _uid = _osUM.getLinuxUserUid(req._clayUser.linuxUser);
165
165
  if (_uid != null) {
166
- require("child_process").execSync("chown " + _uid + " " + JSON.stringify(destPath));
167
- require("child_process").execSync("chown " + _uid + " " + JSON.stringify(tmpDir));
166
+ execFileSync("chown", [String(_uid), destPath]);
167
+ execFileSync("chown", [String(_uid), tmpDir]);
168
168
  }
169
169
  } catch (e2) {}
170
170
  }
@@ -639,9 +639,8 @@ function attachHTTP(ctx) {
639
639
 
640
640
  // Git dirty check
641
641
  if (req.method === "GET" && urlPath === "/api/git-dirty") {
642
- var execSync = require("child_process").execSync;
643
642
  try {
644
- var out = execSync("git status --porcelain", { cwd: cwd, encoding: "utf8", timeout: 5000 });
643
+ var out = execFileSync("git", ["status", "--porcelain"], { cwd: cwd, encoding: "utf8", timeout: 5000 });
645
644
  var lines = out.trim().split("\n").filter(function (line) {
646
645
  return line.trim().length > 0 && !line.startsWith("??");
647
646
  });
@@ -1,6 +1,7 @@
1
1
  var fs = require("fs");
2
2
  var path = require("path");
3
3
  var crypto = require("crypto");
4
+ var execFileSync = require("child_process").execFileSync;
4
5
 
5
6
  /**
6
7
  * Attach image handling to a project context.
@@ -65,12 +66,12 @@ function attachImage(ctx) {
65
66
  var osUsersMod = require("./os-users");
66
67
  var uid = osUsersMod.getLinuxUserUid(ownerLinuxUser);
67
68
  if (uid != null) {
68
- require("child_process").execSync("chown " + uid + " " + JSON.stringify(filePath));
69
+ execFileSync("chown", [String(uid), filePath]);
69
70
  // Also fix parent dirs if root-owned
70
71
  try {
71
72
  var dirStat = fs.statSync(imagesDir);
72
73
  if (dirStat.uid !== uid) {
73
- require("child_process").execSync("chown " + uid + " " + JSON.stringify(imagesDir));
74
+ execFileSync("chown", [String(uid), imagesDir]);
74
75
  }
75
76
  } catch (e2) {}
76
77
  }
@@ -0,0 +1,218 @@
1
+ var z = require("zod");
2
+ var datastore = require("./mate-datastore");
3
+
4
+ function attachMateDatastore(ctx) {
5
+ var cwd = ctx.cwd;
6
+ var isMate = ctx.isMate;
7
+ var send = ctx.send;
8
+ var sendTo = ctx.sendTo;
9
+
10
+ function ensureProjectDatastore() {
11
+ if (!isMate) {
12
+ return { ok: false, code: "MATE_DATASTORE_NOT_ALLOWED", message: "Mate datastore is only available in Mate sessions." };
13
+ }
14
+ try {
15
+ return datastore.ensureMateDatastore({ mateDir: cwd });
16
+ } catch (e) {
17
+ return { ok: false, code: "MATE_DATASTORE_UNAVAILABLE", message: e.message || "Mate datastore is unavailable." };
18
+ }
19
+ }
20
+
21
+ function getDatastoreWarning(handle) {
22
+ if (!handle || !handle.warning) return null;
23
+ return handle.warning;
24
+ }
25
+
26
+ function normalizeParams(input) {
27
+ if (!input) return [];
28
+ if (Array.isArray(input.params)) return input.params;
29
+ return [];
30
+ }
31
+
32
+ function normalizeToolResult(result, requestId) {
33
+ var payload = result || { ok: false, code: "MATE_DATASTORE_UNAVAILABLE", message: "Mate datastore is unavailable." };
34
+ if (typeof requestId !== "undefined") payload.requestId = requestId;
35
+ return payload;
36
+ }
37
+
38
+ function callToolInternal(toolName, input) {
39
+ var handle = ensureProjectDatastore();
40
+ if (!handle || handle.ok === false) {
41
+ return normalizeToolResult(handle, input && input.requestId);
42
+ }
43
+
44
+ var warning = getDatastoreWarning(handle);
45
+ var params = normalizeParams(input);
46
+
47
+ if (toolName === "clay_db_query") {
48
+ var queryResult = datastore.runQuery(handle, input.sql, params, { maxRows: 200, maxBytes: 1024 * 1024, includeSizeInfo: true });
49
+ if (warning && queryResult.ok && !queryResult.warning) queryResult.warning = warning;
50
+ return normalizeToolResult(queryResult, input && input.requestId);
51
+ }
52
+
53
+ if (toolName === "clay_db_exec") {
54
+ var execResult = datastore.runExec(handle, input.sql, params, { maxBytes: 1024 * 1024, includeSizeInfo: true });
55
+ if (warning && execResult.ok && !execResult.warning) execResult.warning = warning;
56
+ if (execResult.ok) {
57
+ broadcastDbChange({ tool: toolName, sql: input.sql, changes: execResult.changes || 0 });
58
+ }
59
+ return normalizeToolResult(execResult, input && input.requestId);
60
+ }
61
+
62
+ if (toolName === "clay_db_tables") {
63
+ var tablesResult = datastore.listSchemaObjects(handle);
64
+ if (warning && tablesResult.ok && !tablesResult.warning) tablesResult.warning = warning;
65
+ return normalizeToolResult(tablesResult, input && input.requestId);
66
+ }
67
+
68
+ if (toolName === "clay_db_describe") {
69
+ var describeResult = datastore.describeTable(handle, input.table);
70
+ if (warning && describeResult.ok && !describeResult.warning) describeResult.warning = warning;
71
+ return normalizeToolResult(describeResult, input && input.requestId);
72
+ }
73
+
74
+ return normalizeToolResult({ ok: false, code: "MATE_DATASTORE_NOT_ALLOWED", message: "Unknown datastore tool: " + toolName }, input && input.requestId);
75
+ }
76
+
77
+ function broadcastDbChange(details) {
78
+ var payload = {
79
+ type: "mate_db_change",
80
+ details: details || {},
81
+ };
82
+ send(payload);
83
+ }
84
+
85
+ function handleMateDatastoreMessage(ws, msg) {
86
+ if (msg.type !== "mate_db_tables" && msg.type !== "mate_db_describe" && msg.type !== "mate_db_query" && msg.type !== "mate_db_exec") {
87
+ return false;
88
+ }
89
+
90
+ if (!isMate) {
91
+ sendTo(ws, {
92
+ type: "mate_db_error",
93
+ requestId: msg.requestId || null,
94
+ code: "MATE_DATASTORE_NOT_ALLOWED",
95
+ message: "Mate datastore is only available in Mate sessions.",
96
+ });
97
+ return true;
98
+ }
99
+
100
+ if (msg.type === "mate_db_tables") {
101
+ sendResult(ws, "mate_db_tables_result", callToolInternal("clay_db_tables", msg));
102
+ return true;
103
+ }
104
+
105
+ if (msg.type === "mate_db_describe") {
106
+ sendResult(ws, "mate_db_describe_result", callToolInternal("clay_db_describe", msg));
107
+ return true;
108
+ }
109
+
110
+ if (msg.type === "mate_db_query") {
111
+ sendResult(ws, "mate_db_query_result", callToolInternal("clay_db_query", msg));
112
+ return true;
113
+ }
114
+
115
+ if (msg.type === "mate_db_exec") {
116
+ sendResult(ws, "mate_db_exec_result", callToolInternal("clay_db_exec", msg));
117
+ return true;
118
+ }
119
+
120
+ return true;
121
+ }
122
+
123
+ function sendResult(ws, type, result) {
124
+ var payload = result || { ok: false, code: "MATE_DATASTORE_UNAVAILABLE", message: "Mate datastore is unavailable." };
125
+ payload.type = type;
126
+ sendTo(ws, payload);
127
+ }
128
+
129
+ function getToolDefinitions() {
130
+ if (!isMate) return [];
131
+ return [
132
+ {
133
+ name: "clay_db_query",
134
+ description: "Execute read-only SQL against the current Mate datastore.",
135
+ inputSchema: {
136
+ sql: z.string().min(1).describe("SQL query"),
137
+ params: z.array(z.any()).optional().describe("Positional parameters"),
138
+ },
139
+ handler: function (input) {
140
+ return callToolInternal("clay_db_query", input || {});
141
+ },
142
+ },
143
+ {
144
+ name: "clay_db_exec",
145
+ description: "Execute schema or write SQL against the current Mate datastore.",
146
+ inputSchema: {
147
+ sql: z.string().min(1).describe("SQL statement"),
148
+ params: z.array(z.any()).optional().describe("Positional parameters"),
149
+ },
150
+ handler: function (input) {
151
+ return callToolInternal("clay_db_exec", input || {});
152
+ },
153
+ },
154
+ {
155
+ name: "clay_db_tables",
156
+ description: "List schema objects in the current Mate datastore.",
157
+ inputSchema: undefined,
158
+ handler: function (input) {
159
+ return callToolInternal("clay_db_tables", input || {});
160
+ },
161
+ },
162
+ {
163
+ name: "clay_db_describe",
164
+ description: "Describe a table or view in the current Mate datastore.",
165
+ inputSchema: {
166
+ table: z.string().min(1).describe("Table name"),
167
+ },
168
+ handler: function (input) {
169
+ return callToolInternal("clay_db_describe", input || {});
170
+ },
171
+ },
172
+ ];
173
+ }
174
+
175
+ function createMcpServer() {
176
+ var defs = getToolDefinitions();
177
+ var registered = {};
178
+ for (var i = 0; i < defs.length; i++) {
179
+ registered[defs[i].name] = {
180
+ description: defs[i].description,
181
+ inputSchema: defs[i].inputSchema,
182
+ handler: defs[i].handler,
183
+ };
184
+ }
185
+ return {
186
+ name: "clay-datastore",
187
+ version: "1.0.0",
188
+ instance: {
189
+ _registeredTools: registered,
190
+ },
191
+ };
192
+ }
193
+
194
+ function getSessionToolDefinitions() {
195
+ if (!isMate) return null;
196
+ return getToolDefinitions();
197
+ }
198
+
199
+ function callMateTool(session, toolName, input) {
200
+ return callToolInternal(toolName, input || {});
201
+ }
202
+
203
+ function closeAllDatastores() {
204
+ datastore.closeAllMateDatastores();
205
+ }
206
+
207
+ return {
208
+ handleMateDatastoreMessage: handleMateDatastoreMessage,
209
+ getToolDefinitions: getToolDefinitions,
210
+ createMcpServer: createMcpServer,
211
+ getSessionToolDefinitions: getSessionToolDefinitions,
212
+ callMateTool: callMateTool,
213
+ ensureProjectDatastore: ensureProjectDatastore,
214
+ closeAllDatastores: closeAllDatastores,
215
+ };
216
+ }
217
+
218
+ module.exports = { attachMateDatastore: attachMateDatastore };
package/lib/project.js CHANGED
@@ -27,6 +27,7 @@ var { attachSessions } = require("./project-sessions");
27
27
  var { attachUserMessage } = require("./project-user-message");
28
28
  var { attachConnection } = require("./project-connection");
29
29
  var { attachMcp } = require("./project-mcp");
30
+ var { attachMateDatastore } = require("./project-mate-datastore");
30
31
  var { createLocalMcp } = require("./mcp-local");
31
32
  var { attachEmail: attachEmailModule } = require("./project-email");
32
33
  // project-notifications is attached globally in server.js, passed via opts.notificationsModule
@@ -457,6 +458,19 @@ function createProjectContext(opts) {
457
458
  },
458
459
  });
459
460
 
461
+ // --- Mate datastore (Mate projects only) ---
462
+ var _mateDatastore = attachMateDatastore({
463
+ cwd: cwd,
464
+ slug: slug,
465
+ isMate: isMate,
466
+ send: send,
467
+ sendTo: sendTo,
468
+ clients: clients,
469
+ getSessionForWs: getSessionForWs,
470
+ usersModule: usersModule,
471
+ getProjectOwnerId: function () { return projectOwnerId; },
472
+ });
473
+
460
474
  // --- MCP tool servers (created via YOKE adapter) ---
461
475
  var mcpServers = (function () {
462
476
  var servers = {};
@@ -537,6 +551,15 @@ function createProjectContext(opts) {
537
551
  console.error("[project] Failed to create email MCP server:", e.message);
538
552
  }
539
553
 
554
+ if (isMate) {
555
+ try {
556
+ var datastoreMcp = _mateDatastore.createMcpServer();
557
+ if (datastoreMcp) servers[datastoreMcp.name || "clay-datastore"] = datastoreMcp;
558
+ } catch (e) {
559
+ console.error("[project] Failed to create datastore MCP server:", e.message);
560
+ }
561
+ }
562
+
540
563
  return Object.keys(servers).length > 0 ? servers : undefined;
541
564
  })();
542
565
 
@@ -878,6 +901,9 @@ function createProjectContext(opts) {
878
901
  // --- MCP bridge (remote MCP servers via extension) ---
879
902
  if (_mcp.handleMcpMessage(ws, msg)) return;
880
903
 
904
+ // --- Mate datastore ---
905
+ if (_mateDatastore.handleMateDatastoreMessage(ws, msg)) return;
906
+
881
907
  // --- Knowledge file management (delegated to project-knowledge.js) ---
882
908
  if (_knowledge.handleKnowledgeMessage(ws, msg)) return;
883
909
 
@@ -1298,6 +1324,9 @@ function createProjectContext(opts) {
1298
1324
  function destroy() {
1299
1325
  _loop.stopTimer();
1300
1326
  _email.destroy();
1327
+ if (_mateDatastore && typeof _mateDatastore.closeAllDatastores === "function") {
1328
+ try { _mateDatastore.closeAllDatastores(); } catch (e) {}
1329
+ }
1301
1330
  stopFileWatch();
1302
1331
  stopAllDirWatches();
1303
1332
  // Abort all active sessions and clean up mention sessions
package/lib/public/app.js CHANGED
@@ -18,6 +18,7 @@ import {
18
18
  openMobileSheet, setMobileSheetMateData, refreshMobileChatSheet
19
19
  } from './modules/sidebar-mobile.js';
20
20
  import { initMateSidebar, showMateSidebar, hideMateSidebar, renderMateSessionList, updateMateSidebarProfile, handleMateSearchResults } from './modules/mate-sidebar.js';
21
+ import { initMateDatastoreUI } from './modules/mate-datastore-ui.js';
21
22
  import { initMateKnowledge, requestKnowledgeList, renderKnowledgeList, handleKnowledgeContent, hideKnowledge } from './modules/mate-knowledge.js';
22
23
  import { initMateMemory, renderMemoryList, hideMemory } from './modules/mate-memory.js';
23
24
  import { initRewind, setRewindMode, showRewindModal, clearPendingRewindUuid, addRewindButton, onRewindComplete, onRewindError } from './modules/rewind.js';
@@ -405,6 +406,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
405
406
  initSidebar(sidebarCtx);
406
407
  var wsGetter = function () { return _getWsRef(); };
407
408
  initMateSidebar(wsGetter);
409
+ initMateDatastoreUI(wsGetter);
408
410
  initMateKnowledge(wsGetter);
409
411
  initMateMemory(wsGetter, { onShow: function () { hideKnowledge(); hideNotes(); } });
410
412
  initMateWizard(