clay-server 2.33.1 → 2.34.0-beta.10
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/ask-user-mcp-server.js +120 -0
- package/lib/config.js +9 -13
- package/lib/daemon.js +116 -55
- package/lib/mate-datastore.js +359 -0
- package/lib/mates.js +2 -2
- package/lib/os-users.js +70 -37
- package/lib/project-connection.js +16 -9
- package/lib/project-http.js +3 -4
- package/lib/project-image.js +3 -2
- package/lib/project-mate-datastore.js +232 -0
- package/lib/project-sessions.js +110 -7
- package/lib/project-user-message.js +4 -3
- package/lib/project.js +126 -10
- package/lib/public/app.js +2 -0
- package/lib/public/css/mates.css +228 -11
- package/lib/public/css/messages.css +23 -0
- package/lib/public/css/mobile-nav.css +0 -14
- package/lib/public/css/notifications-center.css +80 -0
- package/lib/public/css/sidebar.css +326 -101
- package/lib/public/index.html +24 -29
- package/lib/public/modules/app-dm.js +0 -2
- package/lib/public/modules/app-messages.js +23 -0
- package/lib/public/modules/app-rendering.js +0 -2
- package/lib/public/modules/diff.js +21 -7
- package/lib/public/modules/mate-datastore-ui.js +280 -0
- package/lib/public/modules/mate-sidebar.js +3 -9
- package/lib/public/modules/mate-wizard.js +15 -15
- package/lib/public/modules/sidebar-mobile.js +10 -20
- package/lib/public/modules/sidebar-sessions.js +490 -113
- package/lib/public/modules/sidebar.js +8 -6
- package/lib/public/modules/tools.js +115 -18
- package/lib/public/sw.js +1 -1
- package/lib/sdk-bridge.js +56 -41
- package/lib/sdk-message-processor.js +21 -4
- package/lib/server.js +28 -72
- package/lib/sessions.js +157 -20
- package/lib/updater.js +2 -2
- package/lib/users.js +2 -2
- package/lib/ws-schema.js +16 -0
- package/lib/yoke/adapters/claude-worker.js +114 -2
- package/lib/yoke/adapters/claude.js +56 -5
- package/lib/yoke/adapters/codex.js +350 -58
- package/lib/yoke/index.js +93 -48
- package/lib/yoke/instructions.js +0 -1
- package/lib/yoke/mcp-bridge-server.js +14 -6
- package/package.json +1 -2
- package/lib/yoke/adapters/gemini.js +0 -709
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
var fs = require("fs");
|
|
2
|
+
var path = require("path");
|
|
3
|
+
var config = require("./config");
|
|
4
|
+
|
|
5
|
+
var sqlite;
|
|
6
|
+
var _availabilityError = null;
|
|
7
|
+
try {
|
|
8
|
+
sqlite = require("node:sqlite");
|
|
9
|
+
} catch (e) {
|
|
10
|
+
sqlite = null;
|
|
11
|
+
_availabilityError = "Mate datastores require Node 22.13.0 or newer with node:sqlite available.";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function parseNodeVersion(version) {
|
|
15
|
+
var parts = String(version || "").split(".");
|
|
16
|
+
return {
|
|
17
|
+
major: parseInt(parts[0] || "0", 10) || 0,
|
|
18
|
+
minor: parseInt(parts[1] || "0", 10) || 0,
|
|
19
|
+
patch: parseInt(parts[2] || "0", 10) || 0,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function assertNodeVersion() {
|
|
24
|
+
var v = parseNodeVersion(process.versions.node);
|
|
25
|
+
if (v.major < 22 || (v.major === 22 && v.minor < 13)) {
|
|
26
|
+
throw new Error("Mate datastores require Node 22.13.0 or newer.");
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
assertNodeVersion();
|
|
31
|
+
} catch (e) {
|
|
32
|
+
if (!_availabilityError) _availabilityError = e.message;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
var DatabaseSync = sqlite ? sqlite.DatabaseSync : null;
|
|
36
|
+
|
|
37
|
+
var MAX_ROWS = 200;
|
|
38
|
+
var MAX_RESULT_BYTES = 1024 * 1024;
|
|
39
|
+
var DB_SIZE_WARNING_BYTES = 100 * 1024 * 1024;
|
|
40
|
+
var PRAGMA_BUSY_TIMEOUT = 5000;
|
|
41
|
+
var CLAY_META_VERSION = "1";
|
|
42
|
+
|
|
43
|
+
var _dbCache = {};
|
|
44
|
+
|
|
45
|
+
function isMateDatastoreAvailable() {
|
|
46
|
+
return !!DatabaseSync && !_availabilityError;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getMateDatastoreAvailabilityError() {
|
|
50
|
+
return _availabilityError || null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function assertAvailable() {
|
|
54
|
+
if (!isMateDatastoreAvailable()) {
|
|
55
|
+
throw new Error(getMateDatastoreAvailabilityError() || "Mate datastore is unavailable.");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getMateDbPath(opts) {
|
|
60
|
+
if (opts && opts.dbPath) return String(opts.dbPath);
|
|
61
|
+
if (opts && opts.mateDir) return path.join(String(opts.mateDir), "store.db");
|
|
62
|
+
var userId = opts && opts.userId ? String(opts.userId) : "default";
|
|
63
|
+
var mateId = opts && opts.mateId ? String(opts.mateId) : null;
|
|
64
|
+
if (!mateId) throw new Error("Mate datastore requires a mateId.");
|
|
65
|
+
return path.join(config.CONFIG_DIR, "mates", userId, mateId, "store.db");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function ensureParentDir(filePath) {
|
|
69
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function getDbSizeBytes(dbPath) {
|
|
73
|
+
try {
|
|
74
|
+
return fs.statSync(dbPath).size;
|
|
75
|
+
} catch (e) {
|
|
76
|
+
return 0;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function openDatabase(dbPath) {
|
|
81
|
+
var db = new DatabaseSync(dbPath);
|
|
82
|
+
try { db.exec("PRAGMA journal_mode = WAL"); } catch (e) {}
|
|
83
|
+
try { db.exec("PRAGMA foreign_keys = ON"); } catch (e2) {}
|
|
84
|
+
try { db.exec("PRAGMA busy_timeout = " + PRAGMA_BUSY_TIMEOUT); } catch (e3) {}
|
|
85
|
+
return db;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function initClayMeta(db) {
|
|
89
|
+
var now = new Date().toISOString();
|
|
90
|
+
db.exec("CREATE TABLE IF NOT EXISTS clay_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL)");
|
|
91
|
+
var stmt = db.prepare("INSERT OR IGNORE INTO clay_meta (key, value) VALUES (?, ?)");
|
|
92
|
+
stmt.run("clay_meta_version", CLAY_META_VERSION);
|
|
93
|
+
stmt.run("created_at", now);
|
|
94
|
+
stmt.run("last_opened_at", now);
|
|
95
|
+
db.prepare("UPDATE clay_meta SET value = ? WHERE key = ?").run(now, "last_opened_at");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function ensureMateDatastore(opts) {
|
|
99
|
+
assertAvailable();
|
|
100
|
+
var dbPath = getMateDbPath(opts);
|
|
101
|
+
if (_dbCache[dbPath] && _dbCache[dbPath].db) return _dbCache[dbPath];
|
|
102
|
+
ensureParentDir(dbPath);
|
|
103
|
+
var db = openDatabase(dbPath);
|
|
104
|
+
initClayMeta(db);
|
|
105
|
+
var wrapper = {
|
|
106
|
+
db: db,
|
|
107
|
+
dbPath: dbPath,
|
|
108
|
+
sizeBytes: getDbSizeBytes(dbPath),
|
|
109
|
+
warning: getDbSizeBytes(dbPath) > DB_SIZE_WARNING_BYTES ? "Mate datastore exceeds 100 MB soft warning threshold." : null,
|
|
110
|
+
};
|
|
111
|
+
_dbCache[dbPath] = wrapper;
|
|
112
|
+
return wrapper;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function openMateDatastore(opts) {
|
|
116
|
+
return ensureMateDatastore(opts);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function closeMateDatastore(handle) {
|
|
120
|
+
var wrapper = unwrapHandle(handle);
|
|
121
|
+
if (!wrapper || !wrapper.db) return;
|
|
122
|
+
try {
|
|
123
|
+
wrapper.db.close();
|
|
124
|
+
} catch (e) {}
|
|
125
|
+
if (wrapper.dbPath && _dbCache[wrapper.dbPath]) delete _dbCache[wrapper.dbPath];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function closeAllMateDatastores() {
|
|
129
|
+
var keys = Object.keys(_dbCache);
|
|
130
|
+
for (var i = 0; i < keys.length; i++) {
|
|
131
|
+
closeMateDatastore(_dbCache[keys[i]]);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function unwrapHandle(handle) {
|
|
136
|
+
if (!handle) return null;
|
|
137
|
+
if (handle.db && handle.dbPath) return handle;
|
|
138
|
+
return { db: handle, dbPath: null, sizeBytes: 0, warning: null };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function normalizeSql(sql) {
|
|
142
|
+
var text = String(sql || "");
|
|
143
|
+
text = text.replace(/^\s*(?:--[^\n]*\n|\/\*[\s\S]*?\*\/\s*)*/g, "");
|
|
144
|
+
return text.trim();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function getFirstKeyword(sql) {
|
|
148
|
+
var text = normalizeSql(sql);
|
|
149
|
+
var match = text.match(/^([A-Za-z]+)/);
|
|
150
|
+
return match ? match[1].toUpperCase() : "";
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function hasMultipleStatements(sql) {
|
|
154
|
+
var text = normalizeSql(sql);
|
|
155
|
+
if (text.indexOf(";") === -1) return false;
|
|
156
|
+
return !/;\s*$/.test(text) || text.slice(0, -1).indexOf(";") !== -1;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function isForbiddenSql(sql) {
|
|
160
|
+
var text = normalizeSql(sql).toUpperCase();
|
|
161
|
+
var banned = [
|
|
162
|
+
"ATTACH DATABASE",
|
|
163
|
+
"DETACH DATABASE",
|
|
164
|
+
"LOAD_EXTENSION",
|
|
165
|
+
"LOAD EXTENSION",
|
|
166
|
+
];
|
|
167
|
+
for (var i = 0; i < banned.length; i++) {
|
|
168
|
+
if (text.indexOf(banned[i]) !== -1) return banned[i];
|
|
169
|
+
}
|
|
170
|
+
if (/PRAGMA\s+(?!table_info|table_xinfo|index_list|index_info|foreign_key_list)/i.test(text)) {
|
|
171
|
+
return "PRAGMA";
|
|
172
|
+
}
|
|
173
|
+
return "";
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function isReadOnlyQuery(sql) {
|
|
177
|
+
var kw = getFirstKeyword(sql);
|
|
178
|
+
if (kw === "SELECT" || kw === "WITH") return true;
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function isAllowedExec(sql) {
|
|
183
|
+
var kw = getFirstKeyword(sql);
|
|
184
|
+
return kw === "CREATE" || kw === "ALTER" || kw === "DROP" || kw === "INSERT" || kw === "UPDATE" || kw === "DELETE";
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function bindParams(stmt, params) {
|
|
188
|
+
if (!params) return stmt;
|
|
189
|
+
if (!Array.isArray(params)) return stmt;
|
|
190
|
+
return stmt.run.apply(stmt, params);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function runQuery(handle, sql, params, limits) {
|
|
194
|
+
var wrapper = unwrapHandle(handle);
|
|
195
|
+
if (!wrapper || !wrapper.db) {
|
|
196
|
+
return makeError("MATE_DATASTORE_UNAVAILABLE", "Mate datastore is not available.");
|
|
197
|
+
}
|
|
198
|
+
var text = normalizeSql(sql);
|
|
199
|
+
if (!text) return makeError("SQLITE_QUERY_REJECTED", "Query SQL is required.");
|
|
200
|
+
if (hasMultipleStatements(text)) return makeError("SQLITE_QUERY_REJECTED", "Multiple statements are not allowed in query mode.");
|
|
201
|
+
var forbidden = isForbiddenSql(text);
|
|
202
|
+
if (forbidden) return makeError("SQLITE_FORBIDDEN", forbidden + " is not allowed in Mate datastores.");
|
|
203
|
+
if (!isReadOnlyQuery(text)) return makeError("SQLITE_QUERY_REJECTED", "Only SELECT and WITH queries are allowed.");
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
var stmt = wrapper.db.prepare(text);
|
|
207
|
+
var rows = Array.isArray(params) ? stmt.all.apply(stmt, params) : stmt.all();
|
|
208
|
+
rows = sanitizeValue(rows);
|
|
209
|
+
var rowCount = rows.length;
|
|
210
|
+
var maxRows = limits && typeof limits.maxRows === "number" ? limits.maxRows : MAX_ROWS;
|
|
211
|
+
var truncated = false;
|
|
212
|
+
if (rows.length > maxRows) {
|
|
213
|
+
rows = rows.slice(0, maxRows);
|
|
214
|
+
truncated = true;
|
|
215
|
+
}
|
|
216
|
+
var result = {
|
|
217
|
+
ok: true,
|
|
218
|
+
rows: rows,
|
|
219
|
+
rowCount: rowCount,
|
|
220
|
+
truncated: truncated,
|
|
221
|
+
};
|
|
222
|
+
if (wrapper.warning) result.warning = wrapper.warning;
|
|
223
|
+
if (limits && limits.includeSizeInfo) {
|
|
224
|
+
result.sizeBytes = wrapper.sizeBytes;
|
|
225
|
+
}
|
|
226
|
+
if (Buffer.byteLength(JSON.stringify(result), "utf8") > (limits && limits.maxBytes ? limits.maxBytes : MAX_RESULT_BYTES)) {
|
|
227
|
+
return makeError("SQLITE_RESULT_TOO_LARGE", "Query result exceeds the 1 MB response limit.");
|
|
228
|
+
}
|
|
229
|
+
return result;
|
|
230
|
+
} catch (e) {
|
|
231
|
+
return normalizeSqliteError(e, "SQLITE_EXEC_FAILED", "Query failed.");
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function runExec(handle, sql, params, limits) {
|
|
236
|
+
var wrapper = unwrapHandle(handle);
|
|
237
|
+
if (!wrapper || !wrapper.db) {
|
|
238
|
+
return makeError("MATE_DATASTORE_UNAVAILABLE", "Mate datastore is not available.");
|
|
239
|
+
}
|
|
240
|
+
var text = normalizeSql(sql);
|
|
241
|
+
if (!text) return makeError("MATE_DATASTORE_BAD_INPUT", "SQL is required.");
|
|
242
|
+
if (hasMultipleStatements(text)) return makeError("SQLITE_EXEC_FAILED", "Multiple statements are not allowed in exec mode.");
|
|
243
|
+
var forbidden = isForbiddenSql(text);
|
|
244
|
+
if (forbidden) return makeError("SQLITE_FORBIDDEN", forbidden + " is not allowed in Mate datastores.");
|
|
245
|
+
if (!isAllowedExec(text)) return makeError("SQLITE_EXEC_FAILED", "Only CREATE, ALTER, DROP, INSERT, UPDATE, and DELETE are allowed in exec mode.");
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
var stmt = wrapper.db.prepare(text);
|
|
249
|
+
var runResult = Array.isArray(params) ? stmt.run.apply(stmt, params) : stmt.run();
|
|
250
|
+
var result = {
|
|
251
|
+
ok: true,
|
|
252
|
+
changes: typeof runResult.changes === "number" ? runResult.changes : 0,
|
|
253
|
+
lastInsertRowid: typeof runResult.lastInsertRowid !== "undefined" ? sanitizeValue(runResult.lastInsertRowid) : null,
|
|
254
|
+
};
|
|
255
|
+
if (wrapper.warning) result.warning = wrapper.warning;
|
|
256
|
+
if (limits && limits.includeSizeInfo) {
|
|
257
|
+
result.sizeBytes = getDbSizeBytes(wrapper.dbPath);
|
|
258
|
+
}
|
|
259
|
+
if (Buffer.byteLength(JSON.stringify(result), "utf8") > (limits && limits.maxBytes ? limits.maxBytes : MAX_RESULT_BYTES)) {
|
|
260
|
+
return makeError("SQLITE_RESULT_TOO_LARGE", "Execution result exceeds the 1 MB response limit.");
|
|
261
|
+
}
|
|
262
|
+
wrapper.sizeBytes = getDbSizeBytes(wrapper.dbPath);
|
|
263
|
+
if (wrapper.sizeBytes > DB_SIZE_WARNING_BYTES) {
|
|
264
|
+
wrapper.warning = "Mate datastore exceeds 100 MB soft warning threshold.";
|
|
265
|
+
result.warning = wrapper.warning;
|
|
266
|
+
}
|
|
267
|
+
return result;
|
|
268
|
+
} catch (e) {
|
|
269
|
+
return normalizeSqliteError(e, "SQLITE_EXEC_FAILED", "Execution failed.");
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function listSchemaObjects(handle) {
|
|
274
|
+
var wrapper = unwrapHandle(handle);
|
|
275
|
+
if (!wrapper || !wrapper.db) {
|
|
276
|
+
return makeError("MATE_DATASTORE_UNAVAILABLE", "Mate datastore is not available.");
|
|
277
|
+
}
|
|
278
|
+
try {
|
|
279
|
+
var rows = wrapper.db.prepare(
|
|
280
|
+
"SELECT name, type, sql FROM sqlite_master WHERE type IN ('table', 'view', 'index') AND name NOT LIKE 'sqlite_%' AND name NOT LIKE 'clay_%' ORDER BY type, name"
|
|
281
|
+
).all();
|
|
282
|
+
return { ok: true, objects: rows };
|
|
283
|
+
} catch (e) {
|
|
284
|
+
return normalizeSqliteError(e, "SQLITE_EXEC_FAILED", "Failed to list schema objects.");
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function describeTable(handle, tableName) {
|
|
289
|
+
var wrapper = unwrapHandle(handle);
|
|
290
|
+
if (!wrapper || !wrapper.db) {
|
|
291
|
+
return makeError("MATE_DATASTORE_UNAVAILABLE", "Mate datastore is not available.");
|
|
292
|
+
}
|
|
293
|
+
if (!isSafeIdentifier(tableName)) {
|
|
294
|
+
return makeError("MATE_DATASTORE_BAD_INPUT", "Table name is invalid.");
|
|
295
|
+
}
|
|
296
|
+
try {
|
|
297
|
+
var info = wrapper.db.prepare("SELECT name, type, sql FROM sqlite_master WHERE name = ? AND type IN ('table', 'view')").get(tableName);
|
|
298
|
+
if (!info) return makeError("SQLITE_TABLE_NOT_FOUND", "Table not found.");
|
|
299
|
+
var columns = wrapper.db.prepare("PRAGMA table_info(" + quoteIdentifier(tableName) + ")").all();
|
|
300
|
+
var indexes = wrapper.db.prepare("PRAGMA index_list(" + quoteIdentifier(tableName) + ")").all();
|
|
301
|
+
return {
|
|
302
|
+
ok: true,
|
|
303
|
+
table: tableName,
|
|
304
|
+
columns: columns,
|
|
305
|
+
indexes: indexes,
|
|
306
|
+
createSql: info.sql || null,
|
|
307
|
+
};
|
|
308
|
+
} catch (e) {
|
|
309
|
+
return normalizeSqliteError(e, "SQLITE_EXEC_FAILED", "Failed to describe table.");
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function isSafeIdentifier(name) {
|
|
314
|
+
return typeof name === "string" && /^[A-Za-z_][A-Za-z0-9_]*$/.test(name);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function quoteIdentifier(name) {
|
|
318
|
+
return '"' + String(name).replace(/"/g, '""') + '"';
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function makeError(code, message) {
|
|
322
|
+
return { ok: false, code: code, message: message };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function sanitizeValue(value) {
|
|
326
|
+
if (typeof value === "bigint") return value.toString();
|
|
327
|
+
if (Array.isArray(value)) return value.map(sanitizeValue);
|
|
328
|
+
if (value && typeof value === "object") {
|
|
329
|
+
var out = {};
|
|
330
|
+
var keys = Object.keys(value);
|
|
331
|
+
for (var i = 0; i < keys.length; i++) {
|
|
332
|
+
out[keys[i]] = sanitizeValue(value[keys[i]]);
|
|
333
|
+
}
|
|
334
|
+
return out;
|
|
335
|
+
}
|
|
336
|
+
return value;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function normalizeSqliteError(err, fallbackCode, fallbackMessage) {
|
|
340
|
+
var message = err && err.message ? String(err.message) : fallbackMessage;
|
|
341
|
+
var code = fallbackCode;
|
|
342
|
+
if (message.indexOf("no such table") !== -1) code = "SQLITE_TABLE_NOT_FOUND";
|
|
343
|
+
if (message.indexOf("no such column") !== -1) code = "SQLITE_QUERY_REJECTED";
|
|
344
|
+
return { ok: false, code: code, message: message };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
module.exports = {
|
|
348
|
+
isMateDatastoreAvailable: isMateDatastoreAvailable,
|
|
349
|
+
getMateDatastoreAvailabilityError: getMateDatastoreAvailabilityError,
|
|
350
|
+
openMateDatastore: openMateDatastore,
|
|
351
|
+
ensureMateDatastore: ensureMateDatastore,
|
|
352
|
+
listSchemaObjects: listSchemaObjects,
|
|
353
|
+
describeTable: describeTable,
|
|
354
|
+
runQuery: runQuery,
|
|
355
|
+
runExec: runExec,
|
|
356
|
+
closeMateDatastore: closeMateDatastore,
|
|
357
|
+
closeAllMateDatastores: closeAllMateDatastores,
|
|
358
|
+
getMateDbPath: getMateDbPath,
|
|
359
|
+
};
|
package/lib/mates.js
CHANGED
|
@@ -131,6 +131,7 @@ function createMate(ctx, seedData) {
|
|
|
131
131
|
createdBy: userId,
|
|
132
132
|
createdAt: Date.now(),
|
|
133
133
|
seedData: seedData || {},
|
|
134
|
+
vendor: (seedData && seedData.vendor) || "claude",
|
|
134
135
|
profile: {
|
|
135
136
|
displayName: null,
|
|
136
137
|
avatarColor: colors[colorIdx],
|
|
@@ -158,7 +159,7 @@ function createMate(ctx, seedData) {
|
|
|
158
159
|
yaml += "createdAt: " + mate.createdAt + "\n";
|
|
159
160
|
yaml += "relationship: " + (seedData.relationship || "assistant") + "\n";
|
|
160
161
|
yaml += "activities: " + JSON.stringify(seedData.activity || []) + "\n";
|
|
161
|
-
yaml += "
|
|
162
|
+
yaml += "vendor: " + (seedData.vendor || "claude") + "\n";
|
|
162
163
|
fs.writeFileSync(path.join(mateDir, "mate.yaml"), yaml);
|
|
163
164
|
|
|
164
165
|
// Write initial CLAUDE.md (will be replaced by interview)
|
|
@@ -172,7 +173,6 @@ function createMate(ctx, seedData) {
|
|
|
172
173
|
if (seedData.communicationStyle && seedData.communicationStyle.length > 0) {
|
|
173
174
|
claudeMd += "- Communication: " + seedData.communicationStyle.join(", ") + "\n";
|
|
174
175
|
}
|
|
175
|
-
claudeMd += "- Autonomy: " + (seedData.autonomy || "always_ask") + "\n";
|
|
176
176
|
var initialIdentity = claudeMd.trimEnd();
|
|
177
177
|
claudeMd += TEAM_SECTION;
|
|
178
178
|
claudeMd += SESSION_MEMORY_SECTION;
|
package/lib/os-users.js
CHANGED
|
@@ -3,14 +3,24 @@
|
|
|
3
3
|
|
|
4
4
|
var fs = require("fs");
|
|
5
5
|
var path = require("path");
|
|
6
|
-
var
|
|
6
|
+
var execFileSync = require("child_process").execFileSync;
|
|
7
|
+
var _aclGrantCache = Object.create(null);
|
|
8
|
+
|
|
9
|
+
function aclCacheKey(projectPath, linuxUser) {
|
|
10
|
+
return path.resolve(projectPath) + "::" + linuxUser;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function isSafeLinuxUsername(username) {
|
|
14
|
+
return typeof username === "string" && /^[a-z_][a-z0-9_-]*[$]?$/.test(username);
|
|
15
|
+
}
|
|
7
16
|
|
|
8
17
|
/**
|
|
9
18
|
* Resolve Linux user info from username via getent passwd.
|
|
10
19
|
* Returns { uid, gid, home, user, shell } or throws on failure.
|
|
11
20
|
*/
|
|
12
21
|
function resolveOsUserInfo(username) {
|
|
13
|
-
|
|
22
|
+
if (!isSafeLinuxUsername(username)) throw new Error("Invalid Linux username");
|
|
23
|
+
var output = execFileSync("getent", ["passwd", username], { encoding: "utf8", timeout: 5000, stdio: "pipe" }).trim();
|
|
14
24
|
// getent passwd format: username:x:uid:gid:gecos:home:shell
|
|
15
25
|
var parts = output.split(":");
|
|
16
26
|
if (parts.length < 7) throw new Error("Unexpected getent output for user " + username);
|
|
@@ -74,7 +84,7 @@ function fsAsUser(op, args, osUserInfo) {
|
|
|
74
84
|
"var buf = fs.readFileSync(f);",
|
|
75
85
|
"process.stdout.write(buf.toString('base64'));",
|
|
76
86
|
].join(" ");
|
|
77
|
-
var binOutput =
|
|
87
|
+
var binOutput = execFileSync(process.execPath, ["-e", script], {
|
|
78
88
|
encoding: "utf8",
|
|
79
89
|
timeout: 10000,
|
|
80
90
|
uid: osUserInfo.uid,
|
|
@@ -94,7 +104,7 @@ function fsAsUser(op, args, osUserInfo) {
|
|
|
94
104
|
throw new Error("Unknown fsAsUser operation: " + op);
|
|
95
105
|
}
|
|
96
106
|
|
|
97
|
-
var output =
|
|
107
|
+
var output = execFileSync(process.execPath, ["-e", script], {
|
|
98
108
|
encoding: "utf8",
|
|
99
109
|
timeout: 10000,
|
|
100
110
|
uid: osUserInfo.uid,
|
|
@@ -151,7 +161,7 @@ function getAclInstallCommand() {
|
|
|
151
161
|
*/
|
|
152
162
|
function checkAclSupport() {
|
|
153
163
|
try {
|
|
154
|
-
|
|
164
|
+
execFileSync("which", ["setfacl"], { encoding: "utf8", timeout: 5000, stdio: "pipe" });
|
|
155
165
|
return { available: true };
|
|
156
166
|
} catch (e) {
|
|
157
167
|
return { available: false, installCmd: getAclInstallCommand() };
|
|
@@ -182,19 +192,31 @@ function grantProjectAccess(projectPath, linuxUser) {
|
|
|
182
192
|
console.log("[os-users] Skipping ACL for home directory: " + projectPath);
|
|
183
193
|
return;
|
|
184
194
|
}
|
|
195
|
+
if (!isSafeLinuxUsername(linuxUser)) {
|
|
196
|
+
console.error("[os-users] Invalid Linux username for ACL grant: " + linuxUser);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
var cacheKey = aclCacheKey(projectPath, linuxUser);
|
|
200
|
+
if (_aclGrantCache[cacheKey]) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
185
203
|
try {
|
|
186
204
|
// Recursive ACL for existing files
|
|
187
|
-
|
|
205
|
+
execFileSync("setfacl", ["-R", "-m", "u:" + linuxUser + ":rwX", projectPath], {
|
|
188
206
|
encoding: "utf8",
|
|
189
207
|
timeout: 30000,
|
|
208
|
+
stdio: "pipe",
|
|
190
209
|
});
|
|
191
210
|
// Default ACL so new files also inherit access
|
|
192
|
-
|
|
211
|
+
execFileSync("setfacl", ["-R", "-d", "-m", "u:" + linuxUser + ":rwX", projectPath], {
|
|
193
212
|
encoding: "utf8",
|
|
194
213
|
timeout: 30000,
|
|
214
|
+
stdio: "pipe",
|
|
195
215
|
});
|
|
216
|
+
_aclGrantCache[cacheKey] = true;
|
|
196
217
|
console.log("[os-users] Granted ACL access for " + linuxUser + " on " + projectPath);
|
|
197
218
|
} catch (e) {
|
|
219
|
+
delete _aclGrantCache[cacheKey];
|
|
198
220
|
var errMsg = (e.stderr || e.message || "").toString();
|
|
199
221
|
if (errMsg.indexOf("not found") !== -1 || errMsg.indexOf("ENOENT") !== -1) {
|
|
200
222
|
var cmd = getAclInstallCommand();
|
|
@@ -214,15 +236,23 @@ function revokeProjectAccess(projectPath, linuxUser) {
|
|
|
214
236
|
console.log("[os-users] Skipping ACL revoke for home directory: " + projectPath);
|
|
215
237
|
return;
|
|
216
238
|
}
|
|
239
|
+
if (!isSafeLinuxUsername(linuxUser)) {
|
|
240
|
+
console.error("[os-users] Invalid Linux username for ACL revoke: " + linuxUser);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
var cacheKey = aclCacheKey(projectPath, linuxUser);
|
|
217
244
|
try {
|
|
218
|
-
|
|
245
|
+
execFileSync("setfacl", ["-R", "-x", "u:" + linuxUser, projectPath], {
|
|
219
246
|
encoding: "utf8",
|
|
220
247
|
timeout: 30000,
|
|
248
|
+
stdio: "pipe",
|
|
221
249
|
});
|
|
222
|
-
|
|
250
|
+
execFileSync("setfacl", ["-R", "-d", "-x", "u:" + linuxUser, projectPath], {
|
|
223
251
|
encoding: "utf8",
|
|
224
252
|
timeout: 30000,
|
|
253
|
+
stdio: "pipe",
|
|
225
254
|
});
|
|
255
|
+
delete _aclGrantCache[cacheKey];
|
|
226
256
|
console.log("[os-users] Revoked ACL access for " + linuxUser + " on " + projectPath);
|
|
227
257
|
} catch (e) {
|
|
228
258
|
var errMsg = (e.stderr || e.message || "").toString();
|
|
@@ -260,10 +290,11 @@ function toLinuxUsername(clayUsername) {
|
|
|
260
290
|
*/
|
|
261
291
|
function ensureLinger(username) {
|
|
262
292
|
try {
|
|
263
|
-
|
|
293
|
+
if (!isSafeLinuxUsername(username)) throw new Error("Invalid Linux username");
|
|
294
|
+
var uid = execFileSync("id", ["-u", username], { encoding: "utf8", timeout: 5000, stdio: "pipe" }).trim();
|
|
264
295
|
var lingerFile = "/var/lib/systemd/linger/" + username;
|
|
265
296
|
if (fs.existsSync(lingerFile)) return;
|
|
266
|
-
|
|
297
|
+
execFileSync("loginctl", ["enable-linger", username], {
|
|
267
298
|
encoding: "utf8",
|
|
268
299
|
timeout: 10000,
|
|
269
300
|
stdio: "pipe",
|
|
@@ -279,7 +310,8 @@ function ensureLinger(username) {
|
|
|
279
310
|
*/
|
|
280
311
|
function linuxUserExists(username) {
|
|
281
312
|
try {
|
|
282
|
-
|
|
313
|
+
if (!isSafeLinuxUsername(username)) return false;
|
|
314
|
+
execFileSync("id", [username], { encoding: "utf8", timeout: 5000, stdio: "pipe" });
|
|
283
315
|
return true;
|
|
284
316
|
} catch (e) {
|
|
285
317
|
return false;
|
|
@@ -288,7 +320,8 @@ function linuxUserExists(username) {
|
|
|
288
320
|
|
|
289
321
|
function getLinuxUserHome(username) {
|
|
290
322
|
try {
|
|
291
|
-
|
|
323
|
+
if (!isSafeLinuxUsername(username)) return "/home/" + (username || "");
|
|
324
|
+
var line = execFileSync("getent", ["passwd", username], { encoding: "utf8", timeout: 5000, stdio: "pipe" }).trim();
|
|
292
325
|
var parts = line.split(":");
|
|
293
326
|
return parts[5] || "/home/" + username;
|
|
294
327
|
} catch (e) {
|
|
@@ -298,7 +331,8 @@ function getLinuxUserHome(username) {
|
|
|
298
331
|
|
|
299
332
|
function getLinuxUserUid(username) {
|
|
300
333
|
try {
|
|
301
|
-
|
|
334
|
+
if (!isSafeLinuxUsername(username)) return null;
|
|
335
|
+
var uid = execFileSync("id", ["-u", username], { encoding: "utf8", timeout: 5000, stdio: "pipe" }).trim();
|
|
302
336
|
return parseInt(uid, 10);
|
|
303
337
|
} catch (e) {
|
|
304
338
|
return null;
|
|
@@ -312,11 +346,13 @@ function getLinuxUserUid(username) {
|
|
|
312
346
|
*/
|
|
313
347
|
function installClaudeCli(linuxName) {
|
|
314
348
|
try {
|
|
349
|
+
if (!isSafeLinuxUsername(linuxName)) throw new Error("Invalid Linux username");
|
|
315
350
|
// Download and run the Claude CLI install script as the target user
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
351
|
+
execFileSync("su", ["-", linuxName, "-c", "curl -fsSL https://claude.ai/install.sh | bash"], {
|
|
352
|
+
encoding: "utf8",
|
|
353
|
+
timeout: 60000,
|
|
354
|
+
stdio: "pipe",
|
|
355
|
+
});
|
|
320
356
|
console.log("[os-users] Claude CLI installed for " + linuxName);
|
|
321
357
|
} catch (e) {
|
|
322
358
|
var msg = (e.stderr || e.message || "").trim();
|
|
@@ -326,24 +362,19 @@ function installClaudeCli(linuxName) {
|
|
|
326
362
|
|
|
327
363
|
// Append PATH export to the user's shell config if not already present
|
|
328
364
|
try {
|
|
329
|
-
var
|
|
330
|
-
var
|
|
331
|
-
|
|
332
|
-
|
|
365
|
+
var userInfo = resolveOsUserInfo(linuxName);
|
|
366
|
+
var home = userInfo.home || ("/home/" + linuxName);
|
|
367
|
+
var rcPath = fs.existsSync(path.join(home, ".zshrc")) ? path.join(home, ".zshrc") : path.join(home, ".bashrc");
|
|
368
|
+
var exportLine = 'export PATH="$HOME/.local/bin:$PATH"';
|
|
369
|
+
var existing = "";
|
|
370
|
+
try { existing = fs.readFileSync(rcPath, "utf8"); } catch (e2) {}
|
|
371
|
+
if (existing.indexOf(exportLine) === -1) {
|
|
372
|
+
var prefix = existing && !/\n$/.test(existing) ? "\n" : "";
|
|
373
|
+
fs.appendFileSync(rcPath, prefix + exportLine + "\n", "utf8");
|
|
374
|
+
try { fs.chownSync(rcPath, userInfo.uid, userInfo.gid); } catch (e3) {}
|
|
375
|
+
console.log("[os-users] PATH export appended to " + rcPath + " for " + linuxName);
|
|
333
376
|
} 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);
|
|
377
|
+
console.log("[os-users] PATH already configured in " + rcPath + " for " + linuxName);
|
|
347
378
|
}
|
|
348
379
|
} catch (e) {
|
|
349
380
|
var rcMsg = (e.stderr || e.message || "").trim();
|
|
@@ -368,7 +399,7 @@ function provisionLinuxUser(clayUsername) {
|
|
|
368
399
|
}
|
|
369
400
|
|
|
370
401
|
try {
|
|
371
|
-
|
|
402
|
+
execFileSync("useradd", ["-m", "-s", "/bin/bash", linuxName], {
|
|
372
403
|
encoding: "utf8",
|
|
373
404
|
timeout: 15000,
|
|
374
405
|
stdio: "pipe",
|
|
@@ -453,7 +484,8 @@ function grantAllUsersAccess(projectPath, usersModule) {
|
|
|
453
484
|
*/
|
|
454
485
|
function deactivateLinuxUser(linuxUsername) {
|
|
455
486
|
try {
|
|
456
|
-
|
|
487
|
+
if (!isSafeLinuxUsername(linuxUsername)) throw new Error("Invalid Linux username");
|
|
488
|
+
execFileSync("usermod", ["-L", linuxUsername], { encoding: "utf8", timeout: 5000, stdio: "pipe" });
|
|
457
489
|
console.log("[os-users] Deactivated Linux user: " + linuxUsername);
|
|
458
490
|
return { ok: true };
|
|
459
491
|
} catch (e) {
|
|
@@ -493,4 +525,5 @@ module.exports = {
|
|
|
493
525
|
isHomeDirectory: isHomeDirectory,
|
|
494
526
|
getLinuxUserHome: getLinuxUserHome,
|
|
495
527
|
getLinuxUserUid: getLinuxUserUid,
|
|
528
|
+
isSafeLinuxUsername: isSafeLinuxUsername,
|
|
496
529
|
};
|
|
@@ -52,12 +52,27 @@ function attachConnection(ctx) {
|
|
|
52
52
|
var getLatestVersion = ctx.getLatestVersion;
|
|
53
53
|
var getTitle = ctx.getTitle;
|
|
54
54
|
var getProject = ctx.getProject;
|
|
55
|
+
var warmup = ctx.warmup;
|
|
56
|
+
|
|
57
|
+
// Adapters are initialized lazily: the first websocket connection into
|
|
58
|
+
// this project triggers warmup. Without this guard we would either keep
|
|
59
|
+
// the old eager behavior (30+ Codex processes at daemon start) or run
|
|
60
|
+
// warmup once per reconnect.
|
|
61
|
+
var _warmedUp = false;
|
|
55
62
|
|
|
56
63
|
function handleConnection(ws, wsUser, handleMessage, handleDisconnection) {
|
|
57
64
|
ws._clayUser = wsUser || null;
|
|
58
65
|
clients.add(ws);
|
|
59
66
|
broadcastClientCount();
|
|
60
67
|
|
|
68
|
+
if (!_warmedUp) {
|
|
69
|
+
_warmedUp = true;
|
|
70
|
+
if (typeof warmup === "function") {
|
|
71
|
+
try { warmup(); }
|
|
72
|
+
catch (e) { console.error("[project-connection] warmup failed for " + slug + ":", e && e.message ? e.message : e); }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
61
76
|
var loopState = _loop.loopState;
|
|
62
77
|
var loopRegistry = _loop.loopRegistry;
|
|
63
78
|
|
|
@@ -67,16 +82,7 @@ function attachConnection(ctx) {
|
|
|
67
82
|
setTimeout(function() { _loop.resumeLoop(); }, 500);
|
|
68
83
|
}
|
|
69
84
|
|
|
70
|
-
// Auto-assign owner if project has none and a user connects (e.g. IPC-added projects)
|
|
71
85
|
var projectOwnerId = getProjectOwnerId();
|
|
72
|
-
if (!projectOwnerId && ws._clayUser && ws._clayUser.id && !isMate) {
|
|
73
|
-
setProjectOwnerId(ws._clayUser.id);
|
|
74
|
-
projectOwnerId = ws._clayUser.id;
|
|
75
|
-
if (opts.onProjectOwnerChanged) {
|
|
76
|
-
opts.onProjectOwnerChanged(slug, projectOwnerId);
|
|
77
|
-
}
|
|
78
|
-
console.log("[project] Auto-assigned owner for " + slug + ": " + projectOwnerId);
|
|
79
|
-
}
|
|
80
86
|
|
|
81
87
|
// Send cached state
|
|
82
88
|
var _userId = ws._clayUser ? ws._clayUser.id : null;
|
|
@@ -140,6 +146,7 @@ function attachConnection(ctx) {
|
|
|
140
146
|
ownerId: s.ownerId || null,
|
|
141
147
|
sessionVisibility: s.sessionVisibility || "shared",
|
|
142
148
|
bookmarked: !!s.bookmarked,
|
|
149
|
+
favoriteOrder: typeof s.favoriteOrder === "number" ? s.favoriteOrder : null,
|
|
143
150
|
};
|
|
144
151
|
}),
|
|
145
152
|
});
|