clay-server 2.34.0-beta.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.
Files changed (40) hide show
  1. package/lib/ask-user-mcp-server.js +120 -0
  2. package/lib/daemon.js +97 -38
  3. package/lib/mate-datastore.js +27 -5
  4. package/lib/mates.js +2 -2
  5. package/lib/os-users.js +13 -0
  6. package/lib/project-connection.js +16 -9
  7. package/lib/project-mate-datastore.js +16 -2
  8. package/lib/project-sessions.js +110 -7
  9. package/lib/project-user-message.js +4 -3
  10. package/lib/project.js +97 -10
  11. package/lib/public/css/mates.css +94 -52
  12. package/lib/public/css/mobile-nav.css +0 -14
  13. package/lib/public/css/notifications-center.css +23 -19
  14. package/lib/public/css/sidebar.css +326 -101
  15. package/lib/public/index.html +24 -57
  16. package/lib/public/modules/app-dm.js +0 -2
  17. package/lib/public/modules/app-messages.js +3 -5
  18. package/lib/public/modules/app-rendering.js +0 -2
  19. package/lib/public/modules/diff.js +21 -7
  20. package/lib/public/modules/mate-datastore-ui.js +108 -98
  21. package/lib/public/modules/mate-sidebar.js +0 -9
  22. package/lib/public/modules/mate-wizard.js +15 -15
  23. package/lib/public/modules/sidebar-mobile.js +10 -20
  24. package/lib/public/modules/sidebar-sessions.js +490 -113
  25. package/lib/public/modules/sidebar.js +8 -6
  26. package/lib/public/modules/tools.js +58 -13
  27. package/lib/public/sw.js +1 -1
  28. package/lib/sdk-bridge.js +36 -28
  29. package/lib/sdk-message-processor.js +14 -3
  30. package/lib/server.js +28 -72
  31. package/lib/sessions.js +157 -20
  32. package/lib/ws-schema.js +2 -0
  33. package/lib/yoke/adapters/claude-worker.js +114 -2
  34. package/lib/yoke/adapters/claude.js +56 -5
  35. package/lib/yoke/adapters/codex.js +349 -58
  36. package/lib/yoke/index.js +73 -35
  37. package/lib/yoke/instructions.js +0 -1
  38. package/lib/yoke/mcp-bridge-server.js +14 -6
  39. package/package.json +1 -2
  40. package/lib/yoke/adapters/gemini.js +0 -709
@@ -0,0 +1,120 @@
1
+ // Ask User MCP Server for Clay
2
+ // Provides a mate-only ask_user_questions tool that reuses the existing
3
+ // AskUserQuestion UI and ask_user_response flow.
4
+
5
+ var z;
6
+ try { z = require("zod"); } catch (e) { z = null; }
7
+
8
+ // Returns a Zod "shape" object (property -> zod field) matching what
9
+ // Claude SDK's `sdk.tool()` expects. Do NOT wrap in z.object() here —
10
+ // the SDK does that internally.
11
+ //
12
+ // The schema is deliberately tight: options are required (2-6), each with
13
+ // a label and a short description. This pushes Codex (which treats the
14
+ // schema as a loose hint) toward producing well-structured multiple-choice
15
+ // cards instead of a bare "Other..." text field.
16
+ function buildQuestionShape() {
17
+ if (!z) return {};
18
+
19
+ var optionSchema = z.object({
20
+ label: z.string().min(1).max(60)
21
+ .describe("Short button label, 1-6 words. Shown as the primary option text."),
22
+ description: z.string().min(1).max(160)
23
+ .describe("One-line clarifier shown under the label. Concrete example or scope."),
24
+ markdown: z.string().optional()
25
+ .describe("Optional longer markdown body shown on expand. Use sparingly."),
26
+ }).passthrough();
27
+
28
+ var questionSchema = z.object({
29
+ header: z.string().min(1).max(40)
30
+ .describe("Short ALL-CAPS-ish section header above the question (e.g. 'FIRST THINGS FIRST', 'SCOPE'). Gives the user context for this step."),
31
+ question: z.string().min(1)
32
+ .describe("The actual question in natural spoken tone. Be specific, not generic."),
33
+ multiSelect: z.boolean().optional()
34
+ .describe("Set true only when multiple answers genuinely make sense. Default false."),
35
+ options: z.array(optionSchema).min(2).max(6)
36
+ .describe("Concrete options. Preferred count is 4; acceptable range is 2-6. FOUR is the target. Do NOT default to 3 out of habit. If you can only think of 3, push yourself: add a scope variant, edge case, or 'combined' option as the 4th. Only drop below 4 when the answer space is genuinely narrower (e.g. yes/no/unsure). Never include an 'Other' option yourself (the UI renders one automatically)."),
37
+ }).passthrough();
38
+
39
+ return {
40
+ questions: z.array(questionSchema).min(1).max(3)
41
+ .describe("One to three question objects to show together as a card. Prefer ONE focused question per call."),
42
+ };
43
+ }
44
+
45
+ var TOOL_DESCRIPTION = [
46
+ "Ask the user a structured multiple-choice question. Renders as a card with a header, the question, 2-6 clickable option buttons, and an always-present 'Other...' free-text field.",
47
+ "",
48
+ "WHEN TO USE:",
49
+ "- Interviewing the user at mate setup, or whenever you need to narrow scope before acting.",
50
+ "- Any branching decision where 2-6 concrete choices cover most of the space.",
51
+ "",
52
+ "REQUIRED STRUCTURE (all fields matter, do not skip):",
53
+ "- header: short section label, like 'FIRST THINGS FIRST' or 'SCOPE'. Gives context.",
54
+ "- question: one specific question in natural tone. Avoid generic 'how can I help?'.",
55
+ "- options: ALWAYS provide FOUR options by default. Acceptable range is 4-6; only drop to 3 if the answer space is genuinely binary-plus-one (rare). Each option has a short label (1-6 words) AND a one-line description giving a concrete example or scope. The UI already renders an 'Other...' text field, so you never need to include an 'Other' option yourself.",
56
+ "- multiSelect: omit or false unless multiple answers clearly apply.",
57
+ "",
58
+ "ON OPTION COUNT (IMPORTANT):",
59
+ "- Default to 4 options. Not 3. Models tend to gravitate to 3 because it feels tidy; resist that.",
60
+ "- If you're about to produce 3 options, stop and think: is there a fourth axis you're missing? A scope variant? A 'both' or 'neither'? A more niche case? Add it.",
61
+ "- 3 is only acceptable when the fourth option would be truly degenerate (e.g. yes/no/unsure).",
62
+ "- 5 or 6 is fine when the space is wide; don't cram.",
63
+ "",
64
+ "GOOD EXAMPLE:",
65
+ ' { header: "FIRST THINGS FIRST",',
66
+ ' question: "\'Language\' is broad, what do you actually want help with?",',
67
+ ' options: [',
68
+ ' { label: "A new language from scratch", description: "Pick up a language you don\'t speak yet (e.g. Japanese, Spanish)" },',
69
+ ' { label: "Sharpen English", description: "Level up your English, writing, speaking, nuance, executive communication" },',
70
+ ' { label: "Sharpen Korean", description: "Polish your Korean, writing style, formal register, etc." },',
71
+ ' { label: "Teach me how to teach", description: "Help you teach language to others, like Elyse or team members" }',
72
+ " ] }",
73
+ "",
74
+ "BAD EXAMPLES (do not do this):",
75
+ "- Asking a vague question with no options and relying on the Other field.",
76
+ "- Options with only labels and no descriptions.",
77
+ "- Defaulting to exactly 3 options out of habit. Aim for 4.",
78
+ "- More than one question per call unless they are tightly related.",
79
+ "- Single-option 'questions' (use a plain message instead).",
80
+ ].join("\n");
81
+
82
+ function getToolDefs(onAsk) {
83
+ var tools = [];
84
+
85
+ tools.push({
86
+ name: "ask_user_questions",
87
+ description: TOOL_DESCRIPTION,
88
+ inputSchema: buildQuestionShape(),
89
+ handler: function (args) {
90
+ if (!args || !Array.isArray(args.questions) || args.questions.length === 0) {
91
+ return Promise.resolve({
92
+ content: [{ type: "text", text: "Error: questions must be a non-empty array." }],
93
+ isError: true,
94
+ });
95
+ }
96
+ // Defensive structural check only: zod already enforces min 2.
97
+ // We do NOT reject on "only 3 options" — that's a soft preference
98
+ // expressed via the tool description, not a hard validation rule.
99
+ for (var i = 0; i < args.questions.length; i++) {
100
+ var q = args.questions[i];
101
+ if (!q || !Array.isArray(q.options) || q.options.length < 2) {
102
+ return Promise.resolve({
103
+ content: [{
104
+ type: "text",
105
+ text: "Error: question " + (i + 1) + " must include at least 2 options. "
106
+ + "Provide concrete { label, description } choices (4 preferred). "
107
+ + "The UI already shows an 'Other...' free-text field, so never add an 'Other' option yourself.",
108
+ }],
109
+ isError: true,
110
+ });
111
+ }
112
+ }
113
+ return onAsk(args);
114
+ },
115
+ });
116
+
117
+ return tools;
118
+ }
119
+
120
+ module.exports = { getToolDefs: getToolDefs };
package/lib/daemon.js CHANGED
@@ -904,6 +904,47 @@ var relay = createServer({
904
904
  },
905
905
  });
906
906
 
907
+ var IDLE_MS = Number(process.env.CLAY_CODEX_IDLE_MS) || 5 * 60 * 1000;
908
+ var REAP_MS = Number(process.env.CLAY_CODEX_REAPER_MS) || 60 * 1000;
909
+ var reaperHandle = setInterval(function () {
910
+ if (!relay || typeof relay.forEachProject !== "function") return;
911
+ relay.forEachProject(function (ctx) {
912
+ try {
913
+ if (ctx && ctx.adapters && ctx.adapters.codex && typeof ctx.adapters.codex.shutdownIfIdle === "function") {
914
+ var result = ctx.adapters.codex.shutdownIfIdle(IDLE_MS);
915
+ if (result && typeof result.catch === "function") {
916
+ result.catch(function (e) {
917
+ console.error("[daemon] Codex idle reclaim failed:", e && e.message ? e.message : e);
918
+ });
919
+ }
920
+ }
921
+ } catch (e) {}
922
+ });
923
+ }, REAP_MS);
924
+ if (reaperHandle && typeof reaperHandle.unref === "function") {
925
+ reaperHandle.unref();
926
+ }
927
+
928
+ function stopCodexReaper() {
929
+ if (reaperHandle) {
930
+ clearInterval(reaperHandle);
931
+ reaperHandle = null;
932
+ }
933
+ }
934
+
935
+ function shutdownProjects() {
936
+ stopCodexReaper();
937
+ try {
938
+ var result = relay.destroyAll();
939
+ if (result && typeof result.then === "function") {
940
+ return result;
941
+ }
942
+ return Promise.resolve(true);
943
+ } catch (e) {
944
+ return Promise.reject(e);
945
+ }
946
+ }
947
+
907
948
  // Worktree tracking extracted to daemon-projects.js
908
949
 
909
950
  // --- Register projects ---
@@ -1305,41 +1346,48 @@ function spawnAndRestart() {
1305
1346
  try {
1306
1347
  updateHandoff = true;
1307
1348
  ipc.close();
1308
- relay.destroyAll();
1309
- if (relay.onboardingServer) relay.onboardingServer.close();
1349
+ shutdownProjects().then(function () {
1350
+ if (relay.onboardingServer) relay.onboardingServer.close();
1310
1351
 
1311
- // Close the server first so the port is released before spawning the new daemon
1312
- relay.server.close(function () {
1313
- try {
1314
- var { spawn: spawnRestart } = require("child_process");
1315
- var { logPath: restartLogPath, configPath: restartConfigPath } = require("./config");
1316
- var daemonScript = path.join(__dirname, "daemon.js");
1317
- var logFd = fs.openSync(restartLogPath(), "a");
1318
- var child = spawnRestart(process.execPath, [daemonScript], {
1319
- detached: true,
1320
- windowsHide: true,
1321
- stdio: ["ignore", logFd, logFd],
1322
- env: Object.assign({}, process.env, {
1323
- CLAY_CONFIG: restartConfigPath(),
1324
- }),
1325
- });
1326
- child.unref();
1327
- fs.closeSync(logFd);
1328
- config.pid = child.pid;
1329
- saveConfig(config);
1330
- console.log("[daemon] Spawned new daemon (PID " + child.pid + "), exiting.");
1331
- process.exit(120);
1332
- } catch (e) {
1333
- console.error("[daemon] Restart failed:", e.message);
1352
+ // Close the server first so the port is released before spawning the new daemon
1353
+ relay.server.close(function () {
1354
+ try {
1355
+ var { spawn: spawnRestart } = require("child_process");
1356
+ var { logPath: restartLogPath, configPath: restartConfigPath } = require("./config");
1357
+ var daemonScript = path.join(__dirname, "daemon.js");
1358
+ var logFd = fs.openSync(restartLogPath(), "a");
1359
+ var child = spawnRestart(process.execPath, [daemonScript], {
1360
+ detached: true,
1361
+ windowsHide: true,
1362
+ stdio: ["ignore", logFd, logFd],
1363
+ env: Object.assign({}, process.env, {
1364
+ CLAY_CONFIG: restartConfigPath(),
1365
+ }),
1366
+ });
1367
+ child.unref();
1368
+ fs.closeSync(logFd);
1369
+ config.pid = child.pid;
1370
+ saveConfig(config);
1371
+ console.log("[daemon] Spawned new daemon (PID " + child.pid + "), exiting.");
1372
+ process.exit(120);
1373
+ } catch (e) {
1374
+ console.error("[daemon] Restart failed:", e.message);
1375
+ process.exit(1);
1376
+ }
1377
+ });
1378
+ }).catch(function (e) {
1379
+ console.error("[daemon] Restart shutdown failed:", e && e.message ? e.message : e);
1380
+ if (relay.onboardingServer) relay.onboardingServer.close();
1381
+ relay.server.close(function () {
1334
1382
  process.exit(1);
1335
- }
1383
+ });
1336
1384
  });
1337
1385
 
1338
- // Force exit after 5 seconds if server.close hangs
1386
+ // Force exit after 10 seconds if server.close hangs
1339
1387
  setTimeout(function () {
1340
1388
  console.error("[daemon] Forced exit after timeout during restart");
1341
1389
  process.exit(1);
1342
- }, 5000);
1390
+ }, 10000);
1343
1391
  } catch (e) {
1344
1392
  console.error("[daemon] Restart failed:", e.message);
1345
1393
  relay.broadcastAll({ type: "toast", level: "error", message: "Restart failed: " + e.message });
@@ -1349,9 +1397,12 @@ function spawnAndRestart() {
1349
1397
 
1350
1398
  // --- Graceful shutdown ---
1351
1399
  var updateHandoff = false; // true when shutting down for update (new daemon already spawned)
1400
+ var shutdownStarted = false;
1352
1401
 
1353
1402
  function gracefulShutdown() {
1354
1403
  try { console.log("[daemon] Shutting down..."); } catch (e) {}
1404
+ if (shutdownStarted) return;
1405
+ shutdownStarted = true;
1355
1406
  var exitCode = updateHandoff ? 120 : 0; // 120 = update handoff, don't auto-restart
1356
1407
 
1357
1408
  if (caffeinateProc) {
@@ -1371,22 +1422,30 @@ function gracefulShutdown() {
1371
1422
  } catch (e) {}
1372
1423
  }
1373
1424
 
1374
- relay.destroyAll();
1375
-
1376
- if (relay.onboardingServer) {
1377
- relay.onboardingServer.close();
1378
- }
1425
+ shutdownProjects().then(function () {
1426
+ if (relay.onboardingServer) {
1427
+ relay.onboardingServer.close();
1428
+ }
1379
1429
 
1380
- relay.server.close(function () {
1381
- try { console.log("[daemon] Server closed"); } catch (e) {}
1382
- process.exit(exitCode);
1430
+ relay.server.close(function () {
1431
+ try { console.log("[daemon] Server closed"); } catch (e) {}
1432
+ process.exit(exitCode);
1433
+ });
1434
+ }).catch(function (e) {
1435
+ console.error("[daemon] Shutdown cleanup failed:", e && e.message ? e.message : e);
1436
+ if (relay.onboardingServer) {
1437
+ relay.onboardingServer.close();
1438
+ }
1439
+ relay.server.close(function () {
1440
+ process.exit(exitCode);
1441
+ });
1383
1442
  });
1384
1443
 
1385
- // Force exit after 5 seconds
1444
+ // Force exit after 10 seconds
1386
1445
  setTimeout(function () {
1387
1446
  try { console.error("[daemon] Forced exit after timeout"); } catch (e) {}
1388
1447
  process.exit(1);
1389
- }, 5000);
1448
+ }, 10000);
1390
1449
  }
1391
1450
 
1392
1451
  process.on("SIGTERM", gracefulShutdown);
@@ -3,10 +3,12 @@ var path = require("path");
3
3
  var config = require("./config");
4
4
 
5
5
  var sqlite;
6
+ var _availabilityError = null;
6
7
  try {
7
8
  sqlite = require("node:sqlite");
8
9
  } catch (e) {
9
- throw new Error("Mate datastores require Node 22.13.0 or newer with node:sqlite available.");
10
+ sqlite = null;
11
+ _availabilityError = "Mate datastores require Node 22.13.0 or newer with node:sqlite available.";
10
12
  }
11
13
 
12
14
  function parseNodeVersion(version) {
@@ -24,10 +26,13 @@ function assertNodeVersion() {
24
26
  throw new Error("Mate datastores require Node 22.13.0 or newer.");
25
27
  }
26
28
  }
29
+ try {
30
+ assertNodeVersion();
31
+ } catch (e) {
32
+ if (!_availabilityError) _availabilityError = e.message;
33
+ }
27
34
 
28
- assertNodeVersion();
29
-
30
- var DatabaseSync = sqlite.DatabaseSync;
35
+ var DatabaseSync = sqlite ? sqlite.DatabaseSync : null;
31
36
 
32
37
  var MAX_ROWS = 200;
33
38
  var MAX_RESULT_BYTES = 1024 * 1024;
@@ -37,6 +42,20 @@ var CLAY_META_VERSION = "1";
37
42
 
38
43
  var _dbCache = {};
39
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
+
40
59
  function getMateDbPath(opts) {
41
60
  if (opts && opts.dbPath) return String(opts.dbPath);
42
61
  if (opts && opts.mateDir) return path.join(String(opts.mateDir), "store.db");
@@ -77,6 +96,7 @@ function initClayMeta(db) {
77
96
  }
78
97
 
79
98
  function ensureMateDatastore(opts) {
99
+ assertAvailable();
80
100
  var dbPath = getMateDbPath(opts);
81
101
  if (_dbCache[dbPath] && _dbCache[dbPath].db) return _dbCache[dbPath];
82
102
  ensureParentDir(dbPath);
@@ -257,7 +277,7 @@ function listSchemaObjects(handle) {
257
277
  }
258
278
  try {
259
279
  var rows = wrapper.db.prepare(
260
- "SELECT name, type, sql FROM sqlite_master WHERE type IN ('table', 'view', 'index') AND name NOT LIKE 'sqlite_%' ORDER BY type, name"
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"
261
281
  ).all();
262
282
  return { ok: true, objects: rows };
263
283
  } catch (e) {
@@ -325,6 +345,8 @@ function normalizeSqliteError(err, fallbackCode, fallbackMessage) {
325
345
  }
326
346
 
327
347
  module.exports = {
348
+ isMateDatastoreAvailable: isMateDatastoreAvailable,
349
+ getMateDatastoreAvailabilityError: getMateDatastoreAvailabilityError,
328
350
  openMateDatastore: openMateDatastore,
329
351
  ensureMateDatastore: ensureMateDatastore,
330
352
  listSchemaObjects: listSchemaObjects,
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 += "autonomy: " + (seedData.autonomy || "always_ask") + "\n";
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
@@ -4,6 +4,11 @@
4
4
  var fs = require("fs");
5
5
  var path = require("path");
6
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
+ }
7
12
 
8
13
  function isSafeLinuxUsername(username) {
9
14
  return typeof username === "string" && /^[a-z_][a-z0-9_-]*[$]?$/.test(username);
@@ -191,6 +196,10 @@ function grantProjectAccess(projectPath, linuxUser) {
191
196
  console.error("[os-users] Invalid Linux username for ACL grant: " + linuxUser);
192
197
  return;
193
198
  }
199
+ var cacheKey = aclCacheKey(projectPath, linuxUser);
200
+ if (_aclGrantCache[cacheKey]) {
201
+ return;
202
+ }
194
203
  try {
195
204
  // Recursive ACL for existing files
196
205
  execFileSync("setfacl", ["-R", "-m", "u:" + linuxUser + ":rwX", projectPath], {
@@ -204,8 +213,10 @@ function grantProjectAccess(projectPath, linuxUser) {
204
213
  timeout: 30000,
205
214
  stdio: "pipe",
206
215
  });
216
+ _aclGrantCache[cacheKey] = true;
207
217
  console.log("[os-users] Granted ACL access for " + linuxUser + " on " + projectPath);
208
218
  } catch (e) {
219
+ delete _aclGrantCache[cacheKey];
209
220
  var errMsg = (e.stderr || e.message || "").toString();
210
221
  if (errMsg.indexOf("not found") !== -1 || errMsg.indexOf("ENOENT") !== -1) {
211
222
  var cmd = getAclInstallCommand();
@@ -229,6 +240,7 @@ function revokeProjectAccess(projectPath, linuxUser) {
229
240
  console.error("[os-users] Invalid Linux username for ACL revoke: " + linuxUser);
230
241
  return;
231
242
  }
243
+ var cacheKey = aclCacheKey(projectPath, linuxUser);
232
244
  try {
233
245
  execFileSync("setfacl", ["-R", "-x", "u:" + linuxUser, projectPath], {
234
246
  encoding: "utf8",
@@ -240,6 +252,7 @@ function revokeProjectAccess(projectPath, linuxUser) {
240
252
  timeout: 30000,
241
253
  stdio: "pipe",
242
254
  });
255
+ delete _aclGrantCache[cacheKey];
243
256
  console.log("[os-users] Revoked ACL access for " + linuxUser + " on " + projectPath);
244
257
  } catch (e) {
245
258
  var errMsg = (e.stderr || e.message || "").toString();
@@ -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
  });
@@ -1,5 +1,6 @@
1
1
  var z = require("zod");
2
2
  var datastore = require("./mate-datastore");
3
+ var FEATURE_ENABLED = false;
3
4
 
4
5
  function attachMateDatastore(ctx) {
5
6
  var cwd = ctx.cwd;
@@ -7,7 +8,14 @@ function attachMateDatastore(ctx) {
7
8
  var send = ctx.send;
8
9
  var sendTo = ctx.sendTo;
9
10
 
11
+ function isFeatureAvailable() {
12
+ return FEATURE_ENABLED && datastore.isMateDatastoreAvailable();
13
+ }
14
+
10
15
  function ensureProjectDatastore() {
16
+ if (!FEATURE_ENABLED) {
17
+ return { ok: false, code: "MATE_DATASTORE_NOT_ALLOWED", message: "Mate datastore is not enabled." };
18
+ }
11
19
  if (!isMate) {
12
20
  return { ok: false, code: "MATE_DATASTORE_NOT_ALLOWED", message: "Mate datastore is only available in Mate sessions." };
13
21
  }
@@ -87,6 +95,10 @@ function attachMateDatastore(ctx) {
87
95
  return false;
88
96
  }
89
97
 
98
+ if (!FEATURE_ENABLED) {
99
+ return false;
100
+ }
101
+
90
102
  if (!isMate) {
91
103
  sendTo(ws, {
92
104
  type: "mate_db_error",
@@ -127,7 +139,7 @@ function attachMateDatastore(ctx) {
127
139
  }
128
140
 
129
141
  function getToolDefinitions() {
130
- if (!isMate) return [];
142
+ if (!isMate || !isFeatureAvailable()) return [];
131
143
  return [
132
144
  {
133
145
  name: "clay_db_query",
@@ -174,6 +186,7 @@ function attachMateDatastore(ctx) {
174
186
 
175
187
  function createMcpServer() {
176
188
  var defs = getToolDefinitions();
189
+ if (!defs.length) return null;
177
190
  var registered = {};
178
191
  for (var i = 0; i < defs.length; i++) {
179
192
  registered[defs[i].name] = {
@@ -192,7 +205,7 @@ function attachMateDatastore(ctx) {
192
205
  }
193
206
 
194
207
  function getSessionToolDefinitions() {
195
- if (!isMate) return null;
208
+ if (!isMate || !isFeatureAvailable()) return null;
196
209
  return getToolDefinitions();
197
210
  }
198
211
 
@@ -205,6 +218,7 @@ function attachMateDatastore(ctx) {
205
218
  }
206
219
 
207
220
  return {
221
+ isFeatureAvailable: isFeatureAvailable,
208
222
  handleMateDatastoreMessage: handleMateDatastoreMessage,
209
223
  getToolDefinitions: getToolDefinitions,
210
224
  createMcpServer: createMcpServer,