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.
- 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/os-users.js +13 -0
- package/lib/project-connection.js +16 -9
- package/lib/project-mate-datastore.js +16 -2
- package/lib/project-sessions.js +110 -7
- package/lib/project-user-message.js +4 -3
- package/lib/project.js +97 -10
- package/lib/public/css/mates.css +94 -52
- package/lib/public/css/mobile-nav.css +0 -14
- package/lib/public/css/notifications-center.css +23 -19
- package/lib/public/css/sidebar.css +326 -101
- package/lib/public/index.html +24 -57
- 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/diff.js +21 -7
- 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/sidebar-mobile.js +10 -20
- package/lib/public/modules/sidebar-sessions.js +490 -113
- package/lib/public/modules/sidebar.js +8 -6
- package/lib/public/modules/tools.js +58 -13
- package/lib/public/sw.js +1 -1
- package/lib/sdk-bridge.js +36 -28
- package/lib/sdk-message-processor.js +14 -3
- package/lib/server.js +28 -72
- package/lib/sessions.js +157 -20
- package/lib/ws-schema.js +2 -0
- package/lib/yoke/adapters/claude-worker.js +114 -2
- package/lib/yoke/adapters/claude.js +56 -5
- package/lib/yoke/adapters/codex.js +349 -58
- 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
|
@@ -94,12 +94,14 @@ export function initSidebar(_ctx) {
|
|
|
94
94
|
}
|
|
95
95
|
} catch (e) {}
|
|
96
96
|
|
|
97
|
-
ctx.newSessionBtn
|
|
98
|
-
|
|
99
|
-
ctx.ws
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
97
|
+
if (ctx.newSessionBtn) {
|
|
98
|
+
ctx.newSessionBtn.addEventListener("click", function () {
|
|
99
|
+
if (ctx.ws && ctx.connected) {
|
|
100
|
+
ctx.ws.send(JSON.stringify({ type: "new_session" }));
|
|
101
|
+
closeSidebar();
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
103
105
|
|
|
104
106
|
// --- Loop (Ralph wizard) tool-palette tile ---
|
|
105
107
|
// The tile is rendered by tool-palette.js at the stable id
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { escapeHtml, copyToClipboard } from './utils.js';
|
|
2
2
|
import { iconHtml, refreshIcons } from './icons.js';
|
|
3
3
|
import { renderMarkdown, highlightCodeBlocks, renderMermaidBlocks } from './markdown.js';
|
|
4
|
-
import { renderUnifiedDiff, renderSplitDiff, renderPatchDiff } from './diff.js';
|
|
4
|
+
import { renderUnifiedDiff, renderSplitDiff, renderPatchDiff, reconstructPatchSources } from './diff.js';
|
|
5
5
|
import { openFile } from './filebrowser.js';
|
|
6
6
|
import { mateAvatarUrl } from './avatar.js';
|
|
7
7
|
import { getChatLayout } from './theme.js';
|
|
@@ -799,7 +799,7 @@ function resolvePermissionIdentity(mateId, vendor) {
|
|
|
799
799
|
}
|
|
800
800
|
}
|
|
801
801
|
// Project chat: use vendor name and avatar
|
|
802
|
-
var vendorAvatars = { claude: "/claude-code-avatar.png", codex: "/codex-avatar.png"
|
|
802
|
+
var vendorAvatars = { claude: "/claude-code-avatar.png", codex: "/codex-avatar.png" };
|
|
803
803
|
var vendorName = (vendor && VENDOR_NAMES[vendor]) || VENDOR_NAMES.claude;
|
|
804
804
|
return {
|
|
805
805
|
name: vendorName,
|
|
@@ -1587,14 +1587,32 @@ export function stopThinking(duration) {
|
|
|
1587
1587
|
} else {
|
|
1588
1588
|
currentThinking.el.querySelector(".thinking-duration").textContent = " " + secs.toFixed(1) + "s";
|
|
1589
1589
|
}
|
|
1590
|
-
//
|
|
1590
|
+
// If no thinking text was streamed (e.g. Codex reasoning items arrive
|
|
1591
|
+
// with encrypted/hidden content, or Claude without extended-thinking),
|
|
1592
|
+
// the expand affordance is misleading because there's nothing inside.
|
|
1593
|
+
// Strip the chevron and the click handler so the header reads as a
|
|
1594
|
+
// plain label.
|
|
1595
|
+
var hasContent = !!(currentThinking.fullText && currentThinking.fullText.length > 0);
|
|
1596
|
+
if (!hasContent) {
|
|
1597
|
+
currentThinking.el.classList.add("empty");
|
|
1598
|
+
var chev = currentThinking.el.querySelector(".thinking-chevron");
|
|
1599
|
+
if (chev) chev.style.display = "none";
|
|
1600
|
+
var hdr = currentThinking.el.querySelector(".thinking-header");
|
|
1601
|
+
if (hdr) {
|
|
1602
|
+
hdr.style.cursor = "default";
|
|
1603
|
+
// Replace click listener by cloning the node (cheapest way to strip listeners).
|
|
1604
|
+
var clone = hdr.cloneNode(true);
|
|
1605
|
+
hdr.parentNode.replaceChild(clone, hdr);
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
// In mate mode: hide sparkle activity, show compact thinking header.
|
|
1591
1609
|
if (currentThinking.el.classList.contains("mate-thinking")) {
|
|
1592
1610
|
var actRow = currentThinking.el.querySelector(".mate-thinking-activity");
|
|
1593
1611
|
if (actRow) actRow.style.display = "none";
|
|
1594
1612
|
var header = currentThinking.el.querySelector(".thinking-header");
|
|
1595
1613
|
if (header) {
|
|
1596
1614
|
header.style.display = "";
|
|
1597
|
-
header.style.cursor = "pointer";
|
|
1615
|
+
header.style.cursor = hasContent ? "pointer" : "default";
|
|
1598
1616
|
}
|
|
1599
1617
|
}
|
|
1600
1618
|
currentThinking = null;
|
|
@@ -1722,12 +1740,12 @@ export function updateToolExecuting(id, name, input) {
|
|
|
1722
1740
|
ctx.scrollToBottom();
|
|
1723
1741
|
}
|
|
1724
1742
|
|
|
1725
|
-
|
|
1743
|
+
// Shared chrome (filename header + unified/split toggle) for diff renderings.
|
|
1744
|
+
// makeUnified and makeSplit are factories that return a fresh body element.
|
|
1745
|
+
function buildDiffChrome(filePath, linkOldStr, linkNewStr, makeUnified, makeSplit) {
|
|
1726
1746
|
var wrapper = document.createElement("div");
|
|
1727
1747
|
wrapper.className = "edit-diff";
|
|
1728
|
-
var lang = getLanguageFromPath(filePath);
|
|
1729
1748
|
|
|
1730
|
-
// Header with file path and split toggle (desktop only)
|
|
1731
1749
|
var header = document.createElement("div");
|
|
1732
1750
|
header.className = "edit-diff-header";
|
|
1733
1751
|
|
|
@@ -1740,7 +1758,7 @@ function renderEditDiff(oldStr, newStr, filePath) {
|
|
|
1740
1758
|
e.stopPropagation();
|
|
1741
1759
|
openFile(fp, { diff: { oldStr: os || "", newStr: ns || "" } });
|
|
1742
1760
|
});
|
|
1743
|
-
})(filePath,
|
|
1761
|
+
})(filePath, linkOldStr, linkNewStr);
|
|
1744
1762
|
}
|
|
1745
1763
|
header.appendChild(pathSpan);
|
|
1746
1764
|
|
|
@@ -1766,7 +1784,7 @@ function renderEditDiff(oldStr, newStr, filePath) {
|
|
|
1766
1784
|
|
|
1767
1785
|
wrapper.appendChild(header);
|
|
1768
1786
|
|
|
1769
|
-
var currentBody =
|
|
1787
|
+
var currentBody = makeUnified();
|
|
1770
1788
|
wrapper.appendChild(currentBody);
|
|
1771
1789
|
|
|
1772
1790
|
unifiedBtn.addEventListener("click", function (e) {
|
|
@@ -1776,7 +1794,7 @@ function renderEditDiff(oldStr, newStr, filePath) {
|
|
|
1776
1794
|
unifiedBtn.classList.add("active");
|
|
1777
1795
|
splitBtn.classList.remove("active");
|
|
1778
1796
|
wrapper.removeChild(currentBody);
|
|
1779
|
-
currentBody =
|
|
1797
|
+
currentBody = makeUnified();
|
|
1780
1798
|
wrapper.appendChild(currentBody);
|
|
1781
1799
|
refreshIcons();
|
|
1782
1800
|
});
|
|
@@ -1788,7 +1806,7 @@ function renderEditDiff(oldStr, newStr, filePath) {
|
|
|
1788
1806
|
splitBtn.classList.add("active");
|
|
1789
1807
|
unifiedBtn.classList.remove("active");
|
|
1790
1808
|
wrapper.removeChild(currentBody);
|
|
1791
|
-
currentBody =
|
|
1809
|
+
currentBody = makeSplit();
|
|
1792
1810
|
wrapper.appendChild(currentBody);
|
|
1793
1811
|
refreshIcons();
|
|
1794
1812
|
});
|
|
@@ -1796,6 +1814,29 @@ function renderEditDiff(oldStr, newStr, filePath) {
|
|
|
1796
1814
|
return wrapper;
|
|
1797
1815
|
}
|
|
1798
1816
|
|
|
1817
|
+
function renderEditDiff(oldStr, newStr, filePath) {
|
|
1818
|
+
var lang = getLanguageFromPath(filePath);
|
|
1819
|
+
return buildDiffChrome(
|
|
1820
|
+
filePath,
|
|
1821
|
+
oldStr,
|
|
1822
|
+
newStr,
|
|
1823
|
+
function () { return renderUnifiedDiff(oldStr, newStr, lang); },
|
|
1824
|
+
function () { return renderSplitDiff(oldStr, newStr, lang); }
|
|
1825
|
+
);
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
function renderPatchDiffBlock(patchText, filePath) {
|
|
1829
|
+
var lang = getLanguageFromPath(filePath);
|
|
1830
|
+
var sources = reconstructPatchSources(patchText);
|
|
1831
|
+
return buildDiffChrome(
|
|
1832
|
+
filePath,
|
|
1833
|
+
sources.oldStr,
|
|
1834
|
+
sources.newStr,
|
|
1835
|
+
function () { return renderPatchDiff(patchText, lang); },
|
|
1836
|
+
function () { return renderSplitDiff(sources.oldStr, sources.newStr, lang); }
|
|
1837
|
+
);
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1799
1840
|
function isDiffContent(text) {
|
|
1800
1841
|
var lines = text.split("\n");
|
|
1801
1842
|
var hasHunkHeader = false;
|
|
@@ -1903,8 +1944,12 @@ export function updateToolResult(id, content, isError, images) {
|
|
|
1903
1944
|
if (hasEditDiff) {
|
|
1904
1945
|
resultBlock.appendChild(renderEditDiff(tool.input.old_string, tool.input.new_string, tool.input.file_path));
|
|
1905
1946
|
} else if (!isError && isDiffContent(displayContent)) {
|
|
1906
|
-
var
|
|
1907
|
-
|
|
1947
|
+
var patchFilePath = tool.input && tool.input.file_path ? tool.input.file_path : null;
|
|
1948
|
+
if (patchFilePath) {
|
|
1949
|
+
resultBlock.appendChild(renderPatchDiffBlock(displayContent, patchFilePath));
|
|
1950
|
+
} else {
|
|
1951
|
+
resultBlock.appendChild(renderPatchDiff(displayContent, null));
|
|
1952
|
+
}
|
|
1908
1953
|
} else if (!isError && tool.name === "Read" && tool.input && tool.input.file_path && isImagePath(tool.input.file_path)) {
|
|
1909
1954
|
// Image file: show inline preview
|
|
1910
1955
|
var imgWrap = document.createElement("div");
|
package/lib/public/sw.js
CHANGED
package/lib/sdk-bridge.js
CHANGED
|
@@ -1175,17 +1175,21 @@ function createSDKBridge(opts) {
|
|
|
1175
1175
|
|
|
1176
1176
|
if (dangerouslySkipPermissions) {
|
|
1177
1177
|
claudeOpts.allowDangerouslySkipPermissions = true;
|
|
1178
|
+
claudeOpts.permissionMode = "bypassPermissions";
|
|
1179
|
+
} else {
|
|
1180
|
+
var globalMode = sm.currentPermissionMode || "default";
|
|
1181
|
+
var effectiveDefault;
|
|
1182
|
+
if (globalMode === "bypassPermissions") effectiveDefault = "bypassPermissions";
|
|
1183
|
+
else if (session.acceptEditsAfterStart) effectiveDefault = "acceptEdits";
|
|
1184
|
+
else effectiveDefault = globalMode;
|
|
1185
|
+
var modeToApply = session._loopPermissionMode || effectiveDefault;
|
|
1186
|
+
if (modeToApply && modeToApply !== "default") {
|
|
1187
|
+
claudeOpts.permissionMode = modeToApply;
|
|
1188
|
+
}
|
|
1178
1189
|
}
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
if (globalMode === "bypassPermissions") effectiveDefault = "bypassPermissions";
|
|
1182
|
-
else if (session.acceptEditsAfterStart) effectiveDefault = "acceptEdits";
|
|
1183
|
-
else effectiveDefault = globalMode;
|
|
1184
|
-
var modeToApply = session._loopPermissionMode || effectiveDefault;
|
|
1190
|
+
// Clear one-shot acceptEditsAfterStart regardless of which branch ran above,
|
|
1191
|
+
// so the flag does not linger into subsequent turns.
|
|
1185
1192
|
if (session.acceptEditsAfterStart) delete session.acceptEditsAfterStart;
|
|
1186
|
-
if (modeToApply && modeToApply !== "default") {
|
|
1187
|
-
claudeOpts.permissionMode = modeToApply;
|
|
1188
|
-
}
|
|
1189
1193
|
if (session.cliSessionId && session.lastRewindUuid) {
|
|
1190
1194
|
claudeOpts.resumeSessionAt = session.lastRewindUuid;
|
|
1191
1195
|
delete session.lastRewindUuid;
|
|
@@ -1206,21 +1210,30 @@ function createSDKBridge(opts) {
|
|
|
1206
1210
|
}
|
|
1207
1211
|
}
|
|
1208
1212
|
|
|
1209
|
-
//
|
|
1213
|
+
// Pick a model that belongs to the session's vendor. sm.currentModel is
|
|
1214
|
+
// shared project-wide, so a Codex session that last set it to
|
|
1215
|
+
// "gpt-5.4-mini" would otherwise leak into a Claude session in the same
|
|
1216
|
+
// project (or in another session that switches vendor to claude) and
|
|
1217
|
+
// Claude would reject the unknown model. We validate against the
|
|
1218
|
+
// session vendor's model list regardless of which vendor happens to be
|
|
1219
|
+
// the project's default adapter.
|
|
1210
1220
|
var queryModel = ls.model || sm.currentModel || undefined;
|
|
1211
|
-
|
|
1212
|
-
|
|
1221
|
+
var sessionVendor = session.vendor || (adapter && adapter.vendor) || null;
|
|
1222
|
+
if (sessionVendor) {
|
|
1223
|
+
var vendorModels = (sm.modelsByVendor && sm.modelsByVendor[sessionVendor]) || [];
|
|
1213
1224
|
if (vendorModels.length > 0 && queryModel && vendorModels.indexOf(queryModel) === -1) {
|
|
1214
1225
|
queryModel = vendorModels[0];
|
|
1215
1226
|
}
|
|
1216
1227
|
}
|
|
1217
1228
|
|
|
1218
1229
|
var codexConfig = getCodexConfig(sm);
|
|
1230
|
+
var mergedMcpServers = mergeMcpServers(getMcpServers(), getRemoteMcpServers) || undefined;
|
|
1219
1231
|
var queryOpts = {
|
|
1220
1232
|
cwd: cwd,
|
|
1221
1233
|
model: queryModel,
|
|
1222
1234
|
effort: ls.effort || sm.currentEffort || undefined,
|
|
1223
|
-
toolServers:
|
|
1235
|
+
toolServers: mergedMcpServers,
|
|
1236
|
+
toolServerDescriptors: extractMcpDescriptors(mergedMcpServers) || undefined,
|
|
1224
1237
|
resumeSessionId: session.cliSessionId || undefined,
|
|
1225
1238
|
abortController: linuxUser ? undefined : session.abortController,
|
|
1226
1239
|
canUseTool: function(toolName, input, toolOpts) {
|
|
@@ -1229,6 +1242,9 @@ function createSDKBridge(opts) {
|
|
|
1229
1242
|
onElicitation: function(request, elicitOpts) {
|
|
1230
1243
|
return handleElicitation(session, request, elicitOpts);
|
|
1231
1244
|
},
|
|
1245
|
+
callMcpTool: function(serverName, toolName, args) {
|
|
1246
|
+
return callMcpToolHandler(mergedMcpServers, serverName, toolName, args);
|
|
1247
|
+
},
|
|
1232
1248
|
adapterOptions: {
|
|
1233
1249
|
CLAUDE: claudeOpts,
|
|
1234
1250
|
CODEX: {
|
|
@@ -1439,22 +1455,14 @@ function createSDKBridge(opts) {
|
|
|
1439
1455
|
}
|
|
1440
1456
|
}
|
|
1441
1457
|
|
|
1442
|
-
//
|
|
1458
|
+
// Non-default adapters are NOT eagerly initialized here. Doing so used
|
|
1459
|
+
// to spawn a CodexAppServer and an mcp-bridge child per project even
|
|
1460
|
+
// when the user never touched that vendor. Lazy paths cover the gap:
|
|
1461
|
+
// - get_vendor_models (project.js) inits a vendor when the user
|
|
1462
|
+
// opens its model picker.
|
|
1463
|
+
// - ensureVendorReady (this file) inits a vendor when a session
|
|
1464
|
+
// actually issues a query with it.
|
|
1443
1465
|
sm.modelsByVendor = sm.modelsByVendor || {};
|
|
1444
|
-
var otherVendors = Object.keys(adapters).filter(function(v) {
|
|
1445
|
-
return v !== defaultVendor && !sm.modelsByVendor[v];
|
|
1446
|
-
});
|
|
1447
|
-
for (var i = 0; i < otherVendors.length; i++) {
|
|
1448
|
-
(function(v) {
|
|
1449
|
-
adapters[v].init({ cwd: cwd, clayPort: clayPort, clayTls: clayTls, clayAuthToken: clayAuthToken, slug: slug }).then(function(r) {
|
|
1450
|
-
sm.modelsByVendor[v] = r.models || [];
|
|
1451
|
-
sm.capabilitiesByVendor[v] = r.capabilities || {};
|
|
1452
|
-
if (r.slashCommands) sm.setSlashCommandsForVendor(v, r.slashCommands);
|
|
1453
|
-
}).catch(function(e) {
|
|
1454
|
-
console.error("[sdk-bridge] warmup: " + v + " init failed:", e.message || e);
|
|
1455
|
-
});
|
|
1456
|
-
})(otherVendors[i]);
|
|
1457
|
-
}
|
|
1458
1466
|
|
|
1459
1467
|
// Detect installed vendors per-user (binary existence check)
|
|
1460
1468
|
sm.installedVendors = detectInstalledVendors(linuxUser);
|
|
@@ -370,12 +370,23 @@ function attachMessageProcessor(ctx) {
|
|
|
370
370
|
session.sentToolResults = {};
|
|
371
371
|
session.pendingPermissions = {};
|
|
372
372
|
session.pendingElicitations = {};
|
|
373
|
-
// Record ask_user_answered for any leftover pending questions so replay pairs correctly
|
|
373
|
+
// Record ask_user_answered for any leftover pending questions so replay pairs correctly.
|
|
374
|
+
// EXCEPTION: "mcp" mode entries are stateless — the tool returned immediately and the
|
|
375
|
+
// turn is expected to end while the card is still awaiting the user's answer. Those
|
|
376
|
+
// entries must survive across turns so the eventual ask_user_response can inject the
|
|
377
|
+
// answer as the next user message. Only blocking modes (Claude canUseTool) get closed.
|
|
374
378
|
var leftoverAskIds = Object.keys(session.pendingAskUser);
|
|
379
|
+
var keptAskUser = {};
|
|
375
380
|
for (var lai = 0; lai < leftoverAskIds.length; lai++) {
|
|
376
|
-
|
|
381
|
+
var lid = leftoverAskIds[lai];
|
|
382
|
+
var lentry = session.pendingAskUser[lid];
|
|
383
|
+
if (lentry && lentry.mode === "mcp") {
|
|
384
|
+
keptAskUser[lid] = lentry;
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
sendAndRecord(session, { type: "ask_user_answered", toolId: lid });
|
|
377
388
|
}
|
|
378
|
-
session.pendingAskUser =
|
|
389
|
+
session.pendingAskUser = keptAskUser;
|
|
379
390
|
session.activeTaskToolIds = {};
|
|
380
391
|
session.taskIdMap = {};
|
|
381
392
|
// Only clear rateLimitResetsAt on genuine success (non-zero cost).
|
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
|
}
|