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.
Files changed (47) hide show
  1. package/lib/ask-user-mcp-server.js +120 -0
  2. package/lib/config.js +9 -13
  3. package/lib/daemon.js +116 -55
  4. package/lib/mate-datastore.js +359 -0
  5. package/lib/mates.js +2 -2
  6. package/lib/os-users.js +70 -37
  7. package/lib/project-connection.js +16 -9
  8. package/lib/project-http.js +3 -4
  9. package/lib/project-image.js +3 -2
  10. package/lib/project-mate-datastore.js +232 -0
  11. package/lib/project-sessions.js +110 -7
  12. package/lib/project-user-message.js +4 -3
  13. package/lib/project.js +126 -10
  14. package/lib/public/app.js +2 -0
  15. package/lib/public/css/mates.css +228 -11
  16. package/lib/public/css/messages.css +23 -0
  17. package/lib/public/css/mobile-nav.css +0 -14
  18. package/lib/public/css/notifications-center.css +80 -0
  19. package/lib/public/css/sidebar.css +326 -101
  20. package/lib/public/index.html +24 -29
  21. package/lib/public/modules/app-dm.js +0 -2
  22. package/lib/public/modules/app-messages.js +23 -0
  23. package/lib/public/modules/app-rendering.js +0 -2
  24. package/lib/public/modules/diff.js +21 -7
  25. package/lib/public/modules/mate-datastore-ui.js +280 -0
  26. package/lib/public/modules/mate-sidebar.js +3 -9
  27. package/lib/public/modules/mate-wizard.js +15 -15
  28. package/lib/public/modules/sidebar-mobile.js +10 -20
  29. package/lib/public/modules/sidebar-sessions.js +490 -113
  30. package/lib/public/modules/sidebar.js +8 -6
  31. package/lib/public/modules/tools.js +115 -18
  32. package/lib/public/sw.js +1 -1
  33. package/lib/sdk-bridge.js +56 -41
  34. package/lib/sdk-message-processor.js +21 -4
  35. package/lib/server.js +28 -72
  36. package/lib/sessions.js +157 -20
  37. package/lib/updater.js +2 -2
  38. package/lib/users.js +2 -2
  39. package/lib/ws-schema.js +16 -0
  40. package/lib/yoke/adapters/claude-worker.js +114 -2
  41. package/lib/yoke/adapters/claude.js +56 -5
  42. package/lib/yoke/adapters/codex.js +350 -58
  43. package/lib/yoke/index.js +93 -48
  44. package/lib/yoke/instructions.js +0 -1
  45. package/lib/yoke/mcp-bridge-server.js +14 -6
  46. package/package.json +1 -2
  47. package/lib/yoke/adapters/gemini.js +0 -709
package/lib/server.js CHANGED
@@ -459,75 +459,6 @@ function createServer(opts) {
459
459
  return;
460
460
  }
461
461
 
462
- // --- Global MCP bridge endpoint (localhost only) ---
463
- // Used by Codex mcp-bridge-server.js which can't know the active project slug.
464
- // Aggregates MCP tools from all project contexts.
465
- if (req.method === "POST" && fullUrl === "/api/mcp-bridge") {
466
- var mcpRemoteAddr = req.socket.remoteAddress || "";
467
- var mcpIsLocal = mcpRemoteAddr === "127.0.0.1" || mcpRemoteAddr === "::1" || mcpRemoteAddr === "::ffff:127.0.0.1";
468
- if (!mcpIsLocal) {
469
- res.writeHead(403, { "Content-Type": "application/json" });
470
- res.end('{"error":"Forbidden"}');
471
- return;
472
- }
473
- var parseJsonBody = function(r) {
474
- return new Promise(function(resolve, reject) {
475
- var chunks = [];
476
- r.on("data", function(c) { chunks.push(c); });
477
- r.on("end", function() {
478
- try { resolve(JSON.parse(Buffer.concat(chunks).toString("utf8"))); }
479
- catch(e) { reject(e); }
480
- });
481
- });
482
- };
483
- parseJsonBody(req).then(function(body) {
484
- // Find the first project context that has a MCP bridge handler
485
- var handler = null;
486
- projects.forEach(function(ctx) {
487
- if (handler) return;
488
- if (ctx.getMcpBridgeHandler) {
489
- var h = ctx.getMcpBridgeHandler();
490
- if (h) handler = h;
491
- }
492
- });
493
- if (!handler) {
494
- res.writeHead(500, { "Content-Type": "application/json" });
495
- res.end('{"error":"No MCP bridge handler available"}');
496
- return;
497
- }
498
- if (body.action === "list_tools") {
499
- handler.listTools().then(function(tools) {
500
- var serverCounts = {};
501
- for (var ti = 0; ti < tools.length; ti++) {
502
- serverCounts[tools[ti].server] = (serverCounts[tools[ti].server] || 0) + 1;
503
- }
504
- console.log("[mcp-bridge-http] global list_tools:", tools.length, "tools -", Object.keys(serverCounts).map(function(s) { return s + "(" + serverCounts[s] + ")"; }).join(", ") || "(none)");
505
- res.writeHead(200, { "Content-Type": "application/json" });
506
- res.end(JSON.stringify({ tools: tools }));
507
- }).catch(function(err) {
508
- res.writeHead(500, { "Content-Type": "application/json" });
509
- res.end(JSON.stringify({ error: err.message }));
510
- });
511
- } else if (body.action === "call_tool") {
512
- console.log("[mcp-bridge-http] global call_tool:", body.server + "/" + body.tool);
513
- handler.callTool(body.server, body.tool, body.args || {}).then(function(result) {
514
- res.writeHead(200, { "Content-Type": "application/json" });
515
- res.end(JSON.stringify({ result: result }));
516
- }).catch(function(err) {
517
- res.writeHead(200, { "Content-Type": "application/json" });
518
- res.end(JSON.stringify({ error: err.message }));
519
- });
520
- } else {
521
- res.writeHead(400, { "Content-Type": "application/json" });
522
- res.end('{"error":"Unknown action"}');
523
- }
524
- }).catch(function() {
525
- res.writeHead(400, { "Content-Type": "application/json" });
526
- res.end('{"error":"Invalid JSON"}');
527
- });
528
- return;
529
- }
530
-
531
462
  // --- Skills routes (delegated to server-skills) ---
532
463
  if (skills.handleRequest(req, res, fullUrl)) return;
533
464
 
@@ -1090,7 +1021,11 @@ function createServer(opts) {
1090
1021
  getProject: function (s) { return projects.get(s) || null; },
1091
1022
  });
1092
1023
  projects.set(slug, ctx);
1093
- ctx.warmup();
1024
+ // ctx.warmup() is now deferred to the first websocket connection into
1025
+ // this project (see project-connection.js handleConnection). Warming
1026
+ // every project at startup spawned a CodexAppServer and an mcp-bridge
1027
+ // child for each one, which cost 30+ processes on daemons with many
1028
+ // projects/mates even though the user typically only opens one.
1094
1029
  // Schedule project registry refresh for all mates when a non-mate project is added
1095
1030
  if (!extra.isMate) scheduleRegistryRefresh();
1096
1031
  return true;
@@ -1126,8 +1061,13 @@ function createServer(opts) {
1126
1061
  var ctx = projects.get(slug);
1127
1062
  if (!ctx) return false;
1128
1063
  var wasMate = ctx.getStatus().isMate;
1129
- ctx.destroy();
1064
+ var shutdownResult = ctx.destroy();
1130
1065
  projects.delete(slug);
1066
+ if (shutdownResult && typeof shutdownResult.catch === "function") {
1067
+ shutdownResult.catch(function(err) {
1068
+ console.error("[server] Project destroy failed for " + slug + ":", err && err.message ? err.message : err);
1069
+ });
1070
+ }
1131
1071
  if (!wasMate) scheduleRegistryRefresh();
1132
1072
  return true;
1133
1073
  }
@@ -1289,12 +1229,26 @@ function createServer(opts) {
1289
1229
  });
1290
1230
  }
1291
1231
 
1232
+ function forEachProject(fn) {
1233
+ projects.forEach(function (ctx, slug) {
1234
+ fn(ctx, slug);
1235
+ });
1236
+ }
1237
+
1292
1238
  function destroyAll() {
1239
+ var shutdowns = [];
1293
1240
  projects.forEach(function (ctx, slug) {
1294
1241
  console.log("[server] Destroying project:", slug);
1295
- ctx.destroy();
1242
+ var result = ctx.destroy();
1243
+ if (result && typeof result.then === "function") {
1244
+ shutdowns.push(result.catch(function(err) {
1245
+ console.error("[server] Project destroy failed for " + slug + ":", err && err.message ? err.message : err);
1246
+ return false;
1247
+ }));
1248
+ }
1296
1249
  });
1297
1250
  projects.clear();
1251
+ return Promise.all(shutdowns);
1298
1252
  }
1299
1253
 
1300
1254
  // --- Periodic cleanup of old chat images ---
@@ -1358,6 +1312,8 @@ function createServer(opts) {
1358
1312
  setRecovery: auth.setRecovery,
1359
1313
  clearRecovery: auth.clearRecovery,
1360
1314
  broadcastAll: broadcastAll,
1315
+ forEachProject: forEachProject,
1316
+ destroyProject: removeProject,
1361
1317
  destroyAll: destroyAll,
1362
1318
  };
1363
1319
  }
package/lib/sessions.js CHANGED
@@ -96,6 +96,7 @@ function createSessionManager(opts) {
96
96
  if (session.vendor) metaObj.vendor = session.vendor;
97
97
  if (session.sessionVisibility) metaObj.sessionVisibility = session.sessionVisibility;
98
98
  if (session.bookmarked) metaObj.bookmarked = true;
99
+ if (typeof session.favoriteOrder === "number") metaObj.favoriteOrder = session.favoriteOrder;
99
100
  if (session.lastRewindUuid) metaObj.lastRewindUuid = session.lastRewindUuid;
100
101
  if (session.loop) metaObj.loop = session.loop;
101
102
  if (session.debateState) metaObj.debateState = session.debateState;
@@ -202,6 +203,7 @@ function createSessionManager(opts) {
202
203
  if (m.ownerId) session.ownerId = m.ownerId;
203
204
  session.sessionVisibility = m.sessionVisibility || "shared";
204
205
  session.bookmarked = !!m.bookmarked;
206
+ session.favoriteOrder = typeof m.favoriteOrder === "number" ? m.favoriteOrder : null;
205
207
  sessions.set(localId, session);
206
208
  }
207
209
  }
@@ -241,6 +243,7 @@ function createSessionManager(opts) {
241
243
  ownerId: s.ownerId || null,
242
244
  sessionVisibility: s.sessionVisibility || "shared",
243
245
  bookmarked: !!s.bookmarked,
246
+ favoriteOrder: typeof s.favoriteOrder === "number" ? s.favoriteOrder : null,
244
247
  unread: unreadMap[s.localId] || 0,
245
248
  vendor: s.vendor || null,
246
249
  };
@@ -303,6 +306,7 @@ function createSessionManager(opts) {
303
306
  ownerId: (sessionOpts && sessionOpts.ownerId) || null,
304
307
  sessionVisibility: (sessionOpts && sessionOpts.sessionVisibility) || "shared",
305
308
  bookmarked: false,
309
+ favoriteOrder: null,
306
310
  vendor: (sessionOpts && sessionOpts.vendor) || null,
307
311
  };
308
312
  sessions.set(localId, session);
@@ -334,6 +338,7 @@ function createSessionManager(opts) {
334
338
  ownerId: (sessionOpts && sessionOpts.ownerId) || null,
335
339
  sessionVisibility: (sessionOpts && sessionOpts.sessionVisibility) || "shared",
336
340
  bookmarked: false,
341
+ favoriteOrder: null,
337
342
  vendor: (sessionOpts && sessionOpts.vendor) || null,
338
343
  };
339
344
  sessions.set(localId, session);
@@ -525,6 +530,38 @@ function createSessionManager(opts) {
525
530
  sessions.delete(localId);
526
531
  }
527
532
 
533
+ function deleteSessionsBulk(localIds, targetWs) {
534
+ if (!Array.isArray(localIds) || localIds.length === 0) return;
535
+
536
+ var seen = {};
537
+ var ids = [];
538
+ for (var i = 0; i < localIds.length; i++) {
539
+ var id = localIds[i];
540
+ if (typeof id !== "number" || seen[id] || !sessions.has(id)) continue;
541
+ seen[id] = true;
542
+ ids.push(id);
543
+ }
544
+ if (ids.length === 0) return;
545
+
546
+ var deletedActive = false;
547
+ for (var j = 0; j < ids.length; j++) {
548
+ if (ids[j] === activeSessionId) deletedActive = true;
549
+ deleteSessionQuiet(ids[j]);
550
+ }
551
+
552
+ if (sessions.size === 0) {
553
+ createSession(null, targetWs);
554
+ return;
555
+ }
556
+
557
+ if (deletedActive) {
558
+ var remaining = [...sessions.keys()];
559
+ switchSession(remaining[remaining.length - 1], targetWs);
560
+ } else {
561
+ broadcastSessionList();
562
+ }
563
+ }
564
+
528
565
  function doSendToSession(session, obj) {
529
566
  // Send to active clients without recording to history/disk (ephemeral data)
530
567
  if (sendEach) {
@@ -622,6 +659,7 @@ function createSessionManager(opts) {
622
659
  history: cliHistory,
623
660
  messageUUIDs: [],
624
661
  bookmarked: false,
662
+ favoriteOrder: null,
625
663
  };
626
664
  sessions.set(localId, session);
627
665
  saveSessionFile(session);
@@ -680,35 +718,82 @@ function createSessionManager(opts) {
680
718
  var session = sessions.get(localId);
681
719
  if (!session) return { hits: [], total: 0 };
682
720
  var q = query.toLowerCase();
721
+ var qLen = query.length;
683
722
  var history = session.history;
684
723
  var hits = [];
685
- var lastAssistantHitTurn = -1; // track current assistant turn to deduplicate delta hits
686
- var currentTurnStart = -1;
724
+
725
+ // Assistant turns can consist of many streaming deltas (especially Codex,
726
+ // where agentMessage/delta fragments arrive in small chunks). We accumulate
727
+ // delta text per turn, scan for ALL occurrences of the query across the
728
+ // accumulated buffer, then map each occurrence back to the historyIndex of
729
+ // the delta that contains its starting offset. This catches multiple
730
+ // matches within a single turn and also matches that straddle delta
731
+ // boundaries.
732
+ var turnBuffer = "";
733
+ var turnSegments = []; // [{ start, end, historyIndex, ts }]
734
+
735
+ function pushScalarHits(text, historyIndex, role, ts) {
736
+ if (!text) return;
737
+ var lower = text.toLowerCase();
738
+ var from = 0;
739
+ while (true) {
740
+ var idx = lower.indexOf(q, from);
741
+ if (idx === -1) break;
742
+ var s = Math.max(0, idx - 15);
743
+ var e = Math.min(text.length, idx + qLen + 15);
744
+ var snippet = (s > 0 ? "\u2026" : "") + text.substring(s, e) + (e < text.length ? "\u2026" : "");
745
+ hits.push({ historyIndex: historyIndex, snippet: snippet, role: role, ts: ts });
746
+ from = idx + qLen;
747
+ }
748
+ }
749
+
750
+ function flushTurn() {
751
+ if (!turnBuffer || turnSegments.length === 0) {
752
+ turnBuffer = "";
753
+ turnSegments = [];
754
+ return;
755
+ }
756
+ var lowerBuf = turnBuffer.toLowerCase();
757
+ var from = 0;
758
+ var segCursor = 0;
759
+ while (true) {
760
+ var idx = lowerBuf.indexOf(q, from);
761
+ if (idx === -1) break;
762
+ // Advance segCursor to the segment containing idx.
763
+ while (segCursor < turnSegments.length - 1 && turnSegments[segCursor].end <= idx) {
764
+ segCursor++;
765
+ }
766
+ var seg = turnSegments[segCursor];
767
+ var s = Math.max(0, idx - 15);
768
+ var e = Math.min(turnBuffer.length, idx + qLen + 15);
769
+ var snippet = (s > 0 ? "\u2026" : "") + turnBuffer.substring(s, e) + (e < turnBuffer.length ? "\u2026" : "");
770
+ hits.push({ historyIndex: seg.historyIndex, snippet: snippet, role: "assistant", ts: seg.ts });
771
+ from = idx + qLen;
772
+ }
773
+ turnBuffer = "";
774
+ turnSegments = [];
775
+ }
776
+
687
777
  for (var i = 0; i < history.length; i++) {
688
778
  var entry = history[i];
689
- if (entry.type === "user_message" || entry.type === "mention_user") {
690
- currentTurnStart = i;
691
- lastAssistantHitTurn = -1;
692
- }
693
- if ((entry.type === "delta" || entry.type === "user_message" || entry.type === "mention_user" || entry.type === "mention_response" || entry.type === "debate_turn_done" || entry.type === "debate_comment_injected") && entry.text) {
694
- // Skip duplicate delta hits within the same assistant turn
695
- if (entry.type === "delta" && currentTurnStart === lastAssistantHitTurn) continue;
696
- var text = entry.text;
697
- var lowerText = text.toLowerCase();
698
- var idx = lowerText.indexOf(q);
699
- if (idx === -1) continue;
700
- var start = Math.max(0, idx - 15);
701
- var end = Math.min(text.length, idx + query.length + 15);
702
- var snippet = (start > 0 ? "\u2026" : "") + text.substring(start, end) + (end < text.length ? "\u2026" : "");
703
- if (entry.type === "delta") lastAssistantHitTurn = currentTurnStart;
704
- hits.push({
779
+ var t = entry.type;
780
+ if (t === "user_message" || t === "mention_user") {
781
+ flushTurn();
782
+ pushScalarHits(entry.text, i, t === "user_message" ? "user" : "assistant", entry._ts || null);
783
+ } else if (t === "delta" && entry.text) {
784
+ turnSegments.push({
785
+ start: turnBuffer.length,
786
+ end: turnBuffer.length + entry.text.length,
705
787
  historyIndex: i,
706
- snippet: snippet,
707
- role: entry.type === "user_message" ? "user" : "assistant",
708
788
  ts: entry._ts || null,
709
789
  });
790
+ turnBuffer += entry.text;
791
+ } else if ((t === "mention_response" || t === "debate_turn_done" || t === "debate_comment_injected") && entry.text) {
792
+ flushTurn();
793
+ pushScalarHits(entry.text, i, "assistant", entry._ts || null);
710
794
  }
711
795
  }
796
+ flushTurn();
712
797
  return { hits: hits, total: history.length };
713
798
  }
714
799
 
@@ -796,6 +881,7 @@ function createSessionManager(opts) {
796
881
  switchSession: switchSession,
797
882
  deleteSession: deleteSession,
798
883
  deleteSessionQuiet: deleteSessionQuiet,
884
+ deleteSessionsBulk: deleteSessionsBulk,
799
885
  resumeSession: resumeSession,
800
886
  broadcastSessionList: broadcastSessionList,
801
887
  getTotalUnread: function (ws) {
@@ -829,10 +915,61 @@ function createSessionManager(opts) {
829
915
  var session = sessions.get(localId);
830
916
  if (!session) return { error: "Session not found" };
831
917
  session.bookmarked = !!bookmarked;
918
+ if (session.bookmarked) {
919
+ var maxOrder = -1;
920
+ sessions.forEach(function (s) {
921
+ if (s.bookmarked && typeof s.favoriteOrder === "number" && s.favoriteOrder > maxOrder) {
922
+ maxOrder = s.favoriteOrder;
923
+ }
924
+ });
925
+ session.favoriteOrder = maxOrder + 1;
926
+ } else {
927
+ session.favoriteOrder = null;
928
+ }
832
929
  saveSessionFile(session);
833
930
  broadcastSessionList();
834
931
  return { ok: true };
835
932
  },
933
+ reorderBookmarkedSessions: function (sourceId, targetId, insertBefore) {
934
+ var source = sessions.get(sourceId);
935
+ var target = sessions.get(targetId);
936
+ if (!source || !target) return { error: "Session not found" };
937
+ if (!source.bookmarked || !target.bookmarked) return { error: "Only favorites can be reordered" };
938
+
939
+ var favorites = [];
940
+ sessions.forEach(function (s) {
941
+ if (s.bookmarked) favorites.push(s);
942
+ });
943
+ favorites.sort(function (a, b) {
944
+ var ao = typeof a.favoriteOrder === "number" ? a.favoriteOrder : Number.MAX_SAFE_INTEGER;
945
+ var bo = typeof b.favoriteOrder === "number" ? b.favoriteOrder : Number.MAX_SAFE_INTEGER;
946
+ if (ao !== bo) return ao - bo;
947
+ return (b.lastActivity || 0) - (a.lastActivity || 0);
948
+ });
949
+
950
+ var reordered = [];
951
+ for (var i = 0; i < favorites.length; i++) {
952
+ if (favorites[i].localId !== sourceId) reordered.push(favorites[i]);
953
+ }
954
+
955
+ var targetIdx = -1;
956
+ for (var j = 0; j < reordered.length; j++) {
957
+ if (reordered[j].localId === targetId) {
958
+ targetIdx = j;
959
+ break;
960
+ }
961
+ }
962
+ if (targetIdx === -1) return { error: "Target favorite not found" };
963
+ if (!insertBefore) targetIdx++;
964
+ reordered.splice(targetIdx, 0, source);
965
+
966
+ for (var k = 0; k < reordered.length; k++) {
967
+ reordered[k].favoriteOrder = k;
968
+ saveSessionFile(reordered[k]);
969
+ }
970
+ broadcastSessionList();
971
+ return { ok: true };
972
+ },
836
973
  setSessionOwner: function (localId, ownerId) {
837
974
  var session = sessions.get(localId);
838
975
  if (!session) return { error: "Session not found" };
package/lib/updater.js CHANGED
@@ -1,5 +1,5 @@
1
1
  const https = require("https");
2
- const { execSync, spawn } = require("child_process");
2
+ const { execFileSync, spawn } = require("child_process");
3
3
 
4
4
  // ANSI helpers (mirrors cli.js)
5
5
  var isBasicTerm = process.env.TERM_PROGRAM === "Apple_Terminal";
@@ -84,7 +84,7 @@ function isNewer(latest, current) {
84
84
  function performUpdate(channel) {
85
85
  var tag = channel === "beta" ? "beta" : "latest";
86
86
  try {
87
- execSync("npm install -g clay-server@" + tag, { stdio: "pipe" });
87
+ execFileSync("npm", ["install", "-g", "clay-server@" + tag], { stdio: "pipe" });
88
88
  return true;
89
89
  } catch (e) {
90
90
  return false;
package/lib/users.js CHANGED
@@ -1,7 +1,7 @@
1
1
  var fs = require("fs");
2
2
  var path = require("path");
3
3
  var crypto = require("crypto");
4
- var { execSync } = require("child_process");
4
+ var execFileSync = require("child_process").execFileSync;
5
5
  var { CONFIG_DIR } = require("./config");
6
6
  var { attachAuth } = require("./users-auth");
7
7
  var { DEFAULT_PERMISSIONS, ALL_PERMISSIONS, attachPermissions } = require("./users-permissions");
@@ -243,7 +243,7 @@ function updateLinuxUser(userId, linuxUsername) {
243
243
 
244
244
  // Validate Linux user exists
245
245
  try {
246
- execSync("id " + linuxUsername, { encoding: "utf8", timeout: 5000 });
246
+ execFileSync("id", [linuxUsername], { encoding: "utf8", timeout: 5000, stdio: "pipe" });
247
247
  } catch (e) {
248
248
  return { error: "Linux user '" + linuxUsername + "' does not exist" };
249
249
  }
package/lib/ws-schema.js CHANGED
@@ -21,6 +21,8 @@ var schema = {
21
21
  "delete_session": { direction: "c2s", handler: "lib/project-sessions.js", description: "Delete a session by ID" },
22
22
  "rename_session": { direction: "c2s", handler: "lib/project-sessions.js", description: "Rename a session" },
23
23
  "set_session_bookmark": { direction: "c2s", handler: "lib/project-sessions.js", description: "Bookmark or unbookmark a session in the sidebar" },
24
+ "reorder_session_bookmarks": { direction: "c2s", handler: "lib/project-sessions.js", description: "Reorder favorited sessions within the favorites area" },
25
+ "bulk_delete_sessions": { direction: "c2s", handler: "lib/project-sessions.js", description: "Delete a group of sessions at once" },
24
26
  "resume_session": { direction: "c2s", handler: "lib/project-sessions.js", description: "Resume a CLI session by its CLI session ID" },
25
27
  "set_session_visibility": { direction: "c2s", handler: "lib/project-sessions.js", description: "Show or hide a session in the sidebar" },
26
28
  "search_sessions": { direction: "c2s", handler: "lib/project-sessions.js", description: "Search session titles" },
@@ -454,6 +456,20 @@ var schema = {
454
456
  "memory_delete": { direction: "c2s", handler: "lib/project.js", description: "Delete a memory entry by index" },
455
457
  "memory_deleted": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Memory entry was deleted" },
456
458
 
459
+ // -----------------------------------------------------------------------
460
+ // Mate datastore
461
+ // -----------------------------------------------------------------------
462
+ "mate_db_tables": { direction: "c2s", handler: "lib/project-mate-datastore.js", description: "List schema objects in the current Mate datastore" },
463
+ "mate_db_describe": { direction: "c2s", handler: "lib/project-mate-datastore.js", description: "Describe a table or view in the current Mate datastore" },
464
+ "mate_db_query": { direction: "c2s", handler: "lib/project-mate-datastore.js", description: "Run read-only SQL against the current Mate datastore" },
465
+ "mate_db_exec": { direction: "c2s", handler: "lib/project-mate-datastore.js", description: "Run schema or write SQL against the current Mate datastore" },
466
+ "mate_db_tables_result": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Schema objects returned from a Mate datastore" },
467
+ "mate_db_describe_result": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Table description returned from a Mate datastore" },
468
+ "mate_db_query_result": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Query results from a Mate datastore" },
469
+ "mate_db_exec_result": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Execution summary from a Mate datastore" },
470
+ "mate_db_error": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Mate datastore error" },
471
+ "mate_db_change": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Mate datastore changed" },
472
+
457
473
  // -----------------------------------------------------------------------
458
474
  // Loop (automated task runner)
459
475
  // -----------------------------------------------------------------------
@@ -16,6 +16,7 @@ try { require("fs").writeSync(2, "[sdk-worker] BOOT pid=" + process.pid + " uid=
16
16
  var net = require("net");
17
17
  var crypto = require("crypto");
18
18
  var path = require("path");
19
+ var fs = require("fs");
19
20
 
20
21
  var socketPath = process.argv[2];
21
22
  if (!socketPath) {
@@ -31,6 +32,7 @@ var abortController = null;
31
32
  var pendingPermissions = {}; // requestId -> resolve
32
33
  var pendingAskUser = {}; // toolUseId -> resolve
33
34
  var pendingElicitations = {}; // requestId -> resolve
35
+ var pendingMcpToolCalls = {}; // requestId -> { resolve, reject }
34
36
  var conn = null;
35
37
  var buffer = "";
36
38
 
@@ -81,6 +83,90 @@ function getSDK() {
81
83
  return sdkModule;
82
84
  }
83
85
 
86
+ function buildZodShape(z, inputSchema) {
87
+ if (!inputSchema || !inputSchema.properties) return {};
88
+ var shape = {};
89
+ var props = inputSchema.properties;
90
+ var required = inputSchema.required || [];
91
+ var keys = Object.keys(props);
92
+
93
+ for (var i = 0; i < keys.length; i++) {
94
+ var key = keys[i];
95
+ var prop = props[key];
96
+ var field;
97
+
98
+ if (prop.type === "number" || prop.type === "integer") {
99
+ field = z.number();
100
+ } else if (prop.type === "boolean") {
101
+ field = z.boolean();
102
+ } else if (prop.type === "array") {
103
+ field = z.array(z.any());
104
+ } else if (prop.type === "object") {
105
+ field = z.record(z.any());
106
+ } else if (prop.enum) {
107
+ field = z.enum(prop.enum);
108
+ } else {
109
+ field = z.string();
110
+ }
111
+
112
+ if (prop.description) field = field.describe(prop.description);
113
+ if (required.indexOf(key) === -1) field = field.optional();
114
+ shape[key] = field;
115
+ }
116
+
117
+ return shape;
118
+ }
119
+
120
+ function createWorkerMcpToolHandler(serverName, toolName) {
121
+ return function(args) {
122
+ var requestId = crypto.randomUUID();
123
+ sendToDaemon({
124
+ type: "mcp_tool_call",
125
+ requestId: requestId,
126
+ serverName: serverName,
127
+ toolName: toolName,
128
+ args: args || {},
129
+ });
130
+ return new Promise(function(resolve, reject) {
131
+ pendingMcpToolCalls[requestId] = { resolve: resolve, reject: reject };
132
+ });
133
+ };
134
+ }
135
+
136
+ function buildMcpServersFromDescriptors(descriptors, sdk) {
137
+ if (!descriptors || !descriptors.length) return null;
138
+ var z;
139
+ try { z = require("zod").z; } catch (e) {
140
+ try { z = require("zod"); } catch (e2) { return null; }
141
+ }
142
+
143
+ var servers = {};
144
+ for (var i = 0; i < descriptors.length; i++) {
145
+ var descriptor = descriptors[i];
146
+ if (!descriptor || !descriptor.serverName || !descriptor.tools || !descriptor.tools.length) continue;
147
+ var tools = [];
148
+ for (var j = 0; j < descriptor.tools.length; j++) {
149
+ var toolDescriptor = descriptor.tools[j];
150
+ if (!toolDescriptor || !toolDescriptor.name) continue;
151
+ tools.push(sdk.tool(
152
+ toolDescriptor.name,
153
+ toolDescriptor.description || toolDescriptor.name,
154
+ buildZodShape(z, toolDescriptor.inputSchema),
155
+ createWorkerMcpToolHandler(descriptor.serverName, toolDescriptor.name)
156
+ ));
157
+ }
158
+ if (tools.length > 0) {
159
+ servers[descriptor.serverName] = sdk.createSdkMcpServer({
160
+ name: descriptor.serverName,
161
+ version: "1.0.0",
162
+ tools: tools,
163
+ });
164
+ }
165
+ }
166
+
167
+ return Object.keys(servers).length > 0 ? servers : null;
168
+ }
169
+
84
170
  // --- IPC helpers ---
85
171
  function sendToDaemon(msg) {
86
172
  if (!conn || conn.destroyed) return;
@@ -127,6 +213,9 @@ function handleMessage(msg) {
127
213
  case "elicitation_response":
128
214
  handleElicitationResponse(msg);
129
215
  break;
216
+ case "mcp_tool_result":
217
+ handleMcpToolResult(msg);
218
+ break;
130
219
  case "warmup":
131
220
  handleWarmup(msg);
132
221
  break;
@@ -208,6 +297,17 @@ function handleElicitationResponse(msg) {
208
297
  }
209
298
  }
210
299
 
300
+ function handleMcpToolResult(msg) {
301
+ var pending = pendingMcpToolCalls[msg.requestId];
302
+ if (!pending) return;
303
+ delete pendingMcpToolCalls[msg.requestId];
304
+ if (msg.error) {
305
+ pending.reject(new Error(msg.error));
306
+ return;
307
+ }
308
+ pending.resolve(msg.result);
309
+ }
310
+
211
311
  // --- Query handling ---
212
312
  async function handleQueryStart(msg) {
213
313
  var t0 = msg._perfT0 || Date.now();
@@ -238,6 +338,15 @@ async function handleQueryStart(msg) {
238
338
  options.abortController = abortController;
239
339
  options.debug = true;
240
340
  options.debugFile = "/tmp/clay-cli-debug-" + process.pid + ".log";
341
+ if (options.mcpServerDescriptors && options.mcpServerDescriptors.length) {
342
+ try {
343
+ var mcpServers = buildMcpServersFromDescriptors(options.mcpServerDescriptors, sdk);
344
+ if (mcpServers) options.mcpServers = mcpServers;
345
+ } catch (e) {
346
+ console.error("[sdk-worker] Failed to build MCP servers:", e.message || e);
347
+ }
348
+ delete options.mcpServerDescriptors;
349
+ }
241
350
  // Override CLI subprocess spawn to inject NODE_OPTIONS for IPv4-first DNS.
242
351
  // The SDK constructs its own env for the CLI process, so worker env vars
243
352
  // like NODE_OPTIONS are not inherited. We intercept the spawn to fix this.
@@ -247,8 +356,11 @@ async function handleQueryStart(msg) {
247
356
  // This is needed because the CLI's Axios-based HTTP client ignores
248
357
  // NODE_OPTIONS dns flags and still attempts IPv6 connections via its
249
358
  // custom TLS agent, causing 5-10s timeouts on IPv6-less servers.
250
- var preloadScript = require("path").join(__dirname, "ipv4-only.js");
251
- var extraOpts = " --require " + JSON.stringify(preloadScript);
359
+ var preloadScript = path.join(__dirname, "..", "..", "ipv4-only.js");
360
+ var extraOpts = "";
361
+ if (fs.existsSync(preloadScript)) {
362
+ extraOpts += " --require " + JSON.stringify(preloadScript);
363
+ }
252
364
  extraOpts += " --dns-result-order=ipv4first --no-network-family-autoselection";
253
365
  spawnOpts.env.NODE_OPTIONS = (spawnOpts.env.NODE_OPTIONS || "") + extraOpts;
254
366
  console.log("[sdk-worker] spawnClaudeCodeProcess called, command=" + spawnOpts.command);