clay-server 2.33.1 → 2.34.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/config.js +9 -13
- package/lib/daemon.js +19 -17
- package/lib/mate-datastore.js +337 -0
- package/lib/os-users.js +57 -37
- package/lib/project-http.js +3 -4
- package/lib/project-image.js +3 -2
- package/lib/project-mate-datastore.js +218 -0
- package/lib/project.js +29 -0
- package/lib/public/app.js +2 -0
- package/lib/public/css/mates.css +175 -0
- package/lib/public/css/messages.css +23 -0
- package/lib/public/css/notifications-center.css +76 -0
- package/lib/public/index.html +28 -0
- package/lib/public/modules/app-messages.js +25 -0
- package/lib/public/modules/mate-datastore-ui.js +270 -0
- package/lib/public/modules/mate-sidebar.js +3 -0
- package/lib/public/modules/tools.js +57 -5
- package/lib/sdk-bridge.js +32 -23
- package/lib/sdk-message-processor.js +7 -1
- package/lib/updater.js +2 -2
- package/lib/users.js +2 -2
- package/lib/ws-schema.js +14 -0
- package/lib/yoke/adapters/claude.js +5 -1
- package/lib/yoke/adapters/codex.js +1 -0
- package/lib/yoke/index.js +20 -13
- package/package.json +1 -1
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
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
233
|
+
execFileSync("setfacl", ["-R", "-x", "u:" + linuxUser, projectPath], {
|
|
219
234
|
encoding: "utf8",
|
|
220
235
|
timeout: 30000,
|
|
236
|
+
stdio: "pipe",
|
|
221
237
|
});
|
|
222
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
|
330
|
-
var
|
|
331
|
-
|
|
332
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
};
|
package/lib/project-http.js
CHANGED
|
@@ -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
|
-
|
|
167
|
-
|
|
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 =
|
|
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
|
});
|
package/lib/project-image.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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(
|