clay-server 2.34.0-beta.2 → 2.34.0-beta.4
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/daemon.js +97 -38
- package/lib/mate-datastore.js +27 -5
- package/lib/mates.js +2 -2
- package/lib/project-connection.js +15 -0
- package/lib/project-mate-datastore.js +16 -2
- package/lib/project-sessions.js +73 -4
- package/lib/project.js +75 -8
- package/lib/public/css/mates.css +94 -52
- package/lib/public/index.html +17 -47
- package/lib/public/modules/app-dm.js +0 -2
- package/lib/public/modules/app-messages.js +3 -5
- package/lib/public/modules/app-rendering.js +0 -2
- package/lib/public/modules/mate-datastore-ui.js +108 -98
- package/lib/public/modules/mate-sidebar.js +0 -9
- package/lib/public/modules/mate-wizard.js +15 -15
- package/lib/public/modules/tools.js +21 -3
- package/lib/sdk-bridge.js +20 -19
- package/lib/sdk-message-processor.js +14 -3
- package/lib/server.js +28 -72
- package/lib/sessions.js +67 -20
- package/lib/yoke/adapters/codex.js +318 -54
- package/lib/yoke/index.js +73 -35
- 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,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
|
-
|
|
1309
|
-
|
|
1349
|
+
shutdownProjects().then(function () {
|
|
1350
|
+
if (relay.onboardingServer) relay.onboardingServer.close();
|
|
1310
1351
|
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
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
|
|
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
|
-
},
|
|
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
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
}
|
|
1425
|
+
shutdownProjects().then(function () {
|
|
1426
|
+
if (relay.onboardingServer) {
|
|
1427
|
+
relay.onboardingServer.close();
|
|
1428
|
+
}
|
|
1379
1429
|
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
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
|
|
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
|
-
},
|
|
1448
|
+
}, 10000);
|
|
1390
1449
|
}
|
|
1391
1450
|
|
|
1392
1451
|
process.on("SIGTERM", gracefulShutdown);
|
package/lib/mate-datastore.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 += "
|
|
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
|
|
|
@@ -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,
|
package/lib/project-sessions.js
CHANGED
|
@@ -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
|
-
|
|
751
|
-
|
|
752
|
-
|
|
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
|
|