clay-server 2.34.0-beta.3 → 2.34.0-beta.5

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.
@@ -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);
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;
@@ -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
 
@@ -3,6 +3,33 @@ var path = require("path");
3
3
  var { execFileSync } = require("child_process");
4
4
  var { CODEX_DEFAULTS, getCodexConfig } = require("./codex-defaults");
5
5
 
6
+ // Format a user's answer to an ask_user_questions card as a plain user
7
+ // message so the MCP path can feed it back to the agent on the next turn.
8
+ // The agent sees: its own question text, then the selected answer(s).
9
+ function formatAskUserAnswerAsMessage(input, answers) {
10
+ var questions = (input && Array.isArray(input.questions)) ? input.questions : [];
11
+ if (questions.length === 0) {
12
+ // Shouldn't happen, but be defensive.
13
+ try { return "(answered with: " + JSON.stringify(answers || {}) + ")"; }
14
+ catch (e) { return "(answered)"; }
15
+ }
16
+ var lines = [];
17
+ for (var i = 0; i < questions.length; i++) {
18
+ var q = questions[i];
19
+ var qText = (q && q.question) ? q.question : ("Question " + (i + 1));
20
+ var ans = (answers && answers[i] != null) ? String(answers[i]) : "";
21
+ if (!ans) continue;
22
+ if (questions.length === 1) {
23
+ // Single question: keep it tight.
24
+ lines.push(ans);
25
+ } else {
26
+ lines.push("- " + qText + " → " + ans);
27
+ }
28
+ }
29
+ if (lines.length === 0) return "(no answer provided)";
30
+ return lines.join("\n");
31
+ }
32
+
6
33
  /**
7
34
  * Attach session management, config, project management, and mid-section
8
35
  * message handlers to a project context.
@@ -243,6 +270,17 @@ function attachSessions(ctx) {
243
270
 
244
271
  if (msg.type === "switch_session") {
245
272
  if (msg.id && sm.sessions.has(msg.id)) {
273
+ // If the target session's vendor doesn't own the currently cached
274
+ // model, clear sm.currentModel so the UI and next query don't leak
275
+ // the previous session's vendor-specific model into this one.
276
+ var switchTargetSess = sm.sessions.get(msg.id);
277
+ if (switchTargetSess && sm.currentModel) {
278
+ var targetVendor = switchTargetSess.vendor || sm.defaultVendor || null;
279
+ var tvModels = (targetVendor && sm.modelsByVendor && sm.modelsByVendor[targetVendor]) || [];
280
+ if (tvModels.length > 0 && tvModels.indexOf(sm.currentModel) === -1) {
281
+ sm.currentModel = "";
282
+ }
283
+ }
246
284
  // Check access in multi-user mode
247
285
  if (usersModule.isMultiUser() && ws._clayUser) {
248
286
  var switchTarget = sm.sessions.get(msg.id);
@@ -747,10 +785,41 @@ function attachSessions(ctx) {
747
785
  if (!pending) return true;
748
786
  delete session.pendingAskUser[toolId];
749
787
  sm.sendAndRecord(session, { type: "ask_user_answered", toolId: toolId, answers: answers });
750
- pending.resolve({
751
- behavior: "allow",
752
- updatedInput: Object.assign({}, pending.input, { answers: answers }),
753
- });
788
+
789
+ if (pending.mode === "mcp") {
790
+ // Stateless MCP path: the tool already returned. Inject the user's
791
+ // answer as a new user message so the conversation continues
792
+ // naturally on the next turn. This matches how the mate would see
793
+ // any other user input.
794
+ var answerText = formatAskUserAnswerAsMessage(pending.input, answers);
795
+ var userMsg = { type: "user_message", text: answerText };
796
+ session.history.push(userMsg);
797
+ sm.appendToSessionFile(session, userMsg);
798
+ sendToSession(session.localId, userMsg);
799
+
800
+ if (!session.isProcessing) {
801
+ session.isProcessing = true;
802
+ onProcessingChanged();
803
+ session.sentToolResults = {};
804
+ sendToSession(session.localId, { type: "status", status: "processing" });
805
+ if (!session.queryInstance && !session.worker) {
806
+ sdk.startQuery(session, answerText, undefined, getLinuxUserForSession(session));
807
+ } else {
808
+ sdk.pushMessage(session, answerText);
809
+ }
810
+ } else {
811
+ // Turn is still running; queue for the next turn.
812
+ sdk.pushMessage(session, answerText);
813
+ }
814
+ } else {
815
+ // Claude native AskUserQuestion path (canUseTool). The SDK is
816
+ // synchronously blocked on the permission callback, so we must
817
+ // resolve it with the standard permission shape.
818
+ pending.resolve({
819
+ behavior: "allow",
820
+ updatedInput: Object.assign({}, pending.input, { answers: answers }),
821
+ });
822
+ }
754
823
  return true;
755
824
  }
756
825
 
package/lib/project.js CHANGED
@@ -162,7 +162,7 @@ function createProjectContext(opts) {
162
162
  var latestVersion = null;
163
163
 
164
164
  // --- YOKE adapters (multi-vendor, lazy init) ---
165
- var _yokeState = yoke.createAdapters({ cwd: cwd });
165
+ var _yokeState = yoke.createAdapters({ cwd: cwd, slug: slug });
166
166
  var adapters = _yokeState.adapters;
167
167
  var defaultVendor = adapters.claude ? "claude" : Object.keys(adapters)[0] || "claude";
168
168
  var adapter = adapters[defaultVendor] || null;
@@ -496,6 +496,63 @@ function createProjectContext(opts) {
496
496
  console.error("[project] Failed to create debate MCP server:", e.message);
497
497
  }
498
498
 
499
+ // Ask-user MCP server (mates only)
500
+ if (isMate) {
501
+ try {
502
+ var askUserMcp = require("./ask-user-mcp-server");
503
+ var askUserToolDefs = askUserMcp.getToolDefs(function onAsk(input) {
504
+ // Stateless: the tool's job is to *post* the question card.
505
+ // We do NOT hold a promise open waiting for the user. When the
506
+ // user answers, the answer is injected as a fresh user message
507
+ // on the next turn (see project-sessions.js ask_user_response).
508
+ // This avoids HTTP long-poll timeouts on the MCP bridge and
509
+ // matches the natural multi-turn agent loop.
510
+ var session = sm.getActiveSession();
511
+ if (!session) {
512
+ // No active session means we have no way to show a card or
513
+ // route the answer. Fail closed rather than pretend success.
514
+ return Promise.resolve({
515
+ content: [{ type: "text", text: "Error: no active session in " + slug + "; cannot display question card." }],
516
+ isError: true,
517
+ });
518
+ }
519
+ if (session.loop && session.loop.active && session.loop.role !== "crafting") {
520
+ return Promise.resolve({
521
+ content: [{ type: "text", text: "Error: Autonomous mode. Make your own decision." }],
522
+ isError: true,
523
+ });
524
+ }
525
+
526
+ var toolId = "ask_" + Date.now() + "_" + crypto.randomUUID().slice(0, 8);
527
+ // Track for UI card lifecycle + answer routing. No resolve function.
528
+ session.pendingAskUser[toolId] = {
529
+ input: input,
530
+ mode: "mcp",
531
+ sessionId: session.localId,
532
+ postedAt: Date.now(),
533
+ };
534
+
535
+ sm.sendAndRecord(session, {
536
+ type: "tool_executing",
537
+ id: toolId,
538
+ name: "AskUserQuestion",
539
+ input: input,
540
+ });
541
+
542
+ return Promise.resolve({
543
+ content: [{
544
+ type: "text",
545
+ text: "The question card has been posted to the user. End this turn now without further commentary; the user's answer will arrive as the next user message.",
546
+ }],
547
+ });
548
+ });
549
+ var askUserMcpConfig = adapter.createToolServer({ name: "clay-ask-user", version: "1.0.0", tools: askUserToolDefs });
550
+ if (askUserMcpConfig) servers[askUserMcpConfig.name || "clay-ask-user"] = askUserMcpConfig;
551
+ } catch (e) {
552
+ console.error("[project] Failed to create ask-user MCP server:", e.message);
553
+ }
554
+ }
555
+
499
556
  // Browser MCP server (main project only, not mates)
500
557
  if (!isMate) {
501
558
  try {
@@ -1318,6 +1375,13 @@ function createProjectContext(opts) {
1318
1375
  getLatestVersion: function () { return latestVersion; },
1319
1376
  getTitle: function () { return title; },
1320
1377
  getProject: function () { return project; },
1378
+ // Exposed so the first websocket connection can lazily warm up the
1379
+ // adapters for this project (see project-connection handleConnection).
1380
+ warmup: function () {
1381
+ sdk.warmup();
1382
+ sdk.startIdleReaper();
1383
+ sm.migrateSessionTitles(adapter, cwd);
1384
+ },
1321
1385
  });
1322
1386
 
1323
1387
  // --- Destroy ---
@@ -1376,6 +1440,15 @@ function createProjectContext(opts) {
1376
1440
  var tmpDir = path.join(os.tmpdir(), "clay-" + cwdHash);
1377
1441
  fs.rmSync(tmpDir, { recursive: true, force: true });
1378
1442
  } catch (e) {}
1443
+
1444
+ var codexShutdown = Promise.resolve(true);
1445
+ if (adapters && adapters.codex && typeof adapters.codex.shutdown === "function") {
1446
+ codexShutdown = adapters.codex.shutdown().catch(function(err) {
1447
+ console.error("[project] Codex shutdown failed for " + slug + ":", err && err.message ? err.message : err);
1448
+ return false;
1449
+ });
1450
+ }
1451
+ return codexShutdown;
1379
1452
  }
1380
1453
 
1381
1454
  // --- Status info ---
@@ -1523,15 +1596,9 @@ function createProjectContext(opts) {
1523
1596
  broadcastClientCount();
1524
1597
  broadcastPresence();
1525
1598
  },
1526
- warmup: function () {
1527
- sdk.warmup();
1528
- sdk.startIdleReaper();
1529
- // Migrate existing relay session titles to SDK format (one-time, async)
1530
- sm.migrateSessionTitles(adapter, cwd);
1531
- },
1532
1599
  destroy: function () {
1533
1600
  sdk.stopIdleReaper();
1534
- destroy();
1601
+ return destroy();
1535
1602
  },
1536
1603
  };
1537
1604
  }
@@ -410,18 +410,18 @@
410
410
  line-height: 1.4;
411
411
  }
412
412
 
413
- /* Step 4: Autonomy options */
414
- .mate-autonomy-options {
413
+ /* Step 4: Vendor options */
414
+ .mate-vendor-option-list {
415
415
  display: flex;
416
416
  flex-direction: column;
417
- gap: 8px;
417
+ gap: 10px;
418
418
  }
419
- .mate-autonomy-btn {
419
+ .mate-vendor-option-btn {
420
420
  display: flex;
421
- flex-direction: column;
421
+ flex-direction: row;
422
422
  align-items: flex-start;
423
- gap: 2px;
424
- padding: 12px 16px;
423
+ gap: 14px;
424
+ padding: 14px 16px;
425
425
  background: var(--input-bg, #1a1a2e);
426
426
  border: 2px solid var(--border, #333);
427
427
  border-radius: 10px;
@@ -430,21 +430,36 @@
430
430
  text-align: left;
431
431
  color: var(--text, #fff);
432
432
  font-family: inherit;
433
+ width: 100%;
433
434
  }
434
- .mate-autonomy-btn:hover {
435
+ .mate-vendor-option-btn:hover {
435
436
  border-color: var(--text-dimmer, #6272a4);
436
437
  }
437
- .mate-autonomy-btn.active {
438
+ .mate-vendor-option-btn.active {
438
439
  border-color: var(--accent, #6c5ce7);
439
440
  background: var(--accent-bg, rgba(108,92,231,0.12));
440
441
  }
441
- .mate-autonomy-title {
442
+ .mate-vendor-option-icon {
443
+ width: 36px;
444
+ height: 36px;
445
+ border-radius: 8px;
446
+ flex-shrink: 0;
447
+ object-fit: cover;
448
+ }
449
+ .mate-vendor-option-text {
450
+ display: flex;
451
+ flex-direction: column;
452
+ gap: 4px;
453
+ min-width: 0;
454
+ }
455
+ .mate-vendor-option-title {
442
456
  font-weight: 600;
443
457
  font-size: 14px;
444
458
  }
445
- .mate-autonomy-desc {
459
+ .mate-vendor-option-desc {
446
460
  font-size: 12px;
447
461
  color: var(--text-muted, #9ea8c7);
462
+ line-height: 1.4;
448
463
  }
449
464
 
450
465
  /* Footer */