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.
- package/lib/ask-user-mcp-server.js +120 -0
- package/lib/daemon.js +97 -38
- package/lib/mates.js +2 -2
- package/lib/project-connection.js +15 -0
- package/lib/project-sessions.js +73 -4
- package/lib/project.js +75 -8
- package/lib/public/css/mates.css +26 -11
- package/lib/public/index.html +17 -19
- package/lib/public/modules/app-dm.js +0 -2
- package/lib/public/modules/app-messages.js +2 -0
- package/lib/public/modules/mate-sidebar.js +0 -9
- package/lib/public/modules/mate-wizard.js +15 -15
- package/lib/public/modules/tools.js +20 -2
- package/lib/sdk-bridge.js +23 -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/claude-worker.js +108 -0
- package/lib/yoke/adapters/claude.js +17 -3
- package/lib/yoke/adapters/codex.js +318 -54
- package/lib/yoke/index.js +40 -28
- package/lib/yoke/mcp-bridge-server.js +14 -6
- package/package.json +1 -1
package/lib/public/index.html
CHANGED
|
@@ -1855,7 +1855,7 @@
|
|
|
1855
1855
|
<div class="mate-intro-step"><span class="mate-intro-step-num">2</span> Have a short interview where your Mate gets to know you</div>
|
|
1856
1856
|
<div class="mate-intro-step"><span class="mate-intro-step-num">3</span> Start talking</div>
|
|
1857
1857
|
</div>
|
|
1858
|
-
<p class="mate-intro-privacy">Mates run on Claude Code. No separate API keys or external services needed. Your conversations stay on your Clay server and are never stored elsewhere.</p>
|
|
1858
|
+
<p class="mate-intro-privacy">Mates run on Claude Code or ChatGPT Codex, your choice. No separate API keys or external services needed. Your conversations stay on your Clay server and are never stored elsewhere.</p>
|
|
1859
1859
|
</div>
|
|
1860
1860
|
</div>
|
|
1861
1861
|
<!-- Step 1: Relationship -->
|
|
@@ -1996,26 +1996,24 @@
|
|
|
1996
1996
|
</div>
|
|
1997
1997
|
</div>
|
|
1998
1998
|
|
|
1999
|
-
<!-- Step 4:
|
|
1999
|
+
<!-- Step 4: Vendor -->
|
|
2000
2000
|
<div class="mate-step" data-step="4">
|
|
2001
|
-
<h3>
|
|
2002
|
-
<p class="mate-hint">
|
|
2003
|
-
<div class="mate-
|
|
2004
|
-
<button class="mate-
|
|
2005
|
-
<
|
|
2006
|
-
<
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
<span class="mate-autonomy-desc">"Little things, just do it. Anything big, ask me."</span>
|
|
2011
|
-
</button>
|
|
2012
|
-
<button class="mate-autonomy-btn" data-value="mostly_autonomous">
|
|
2013
|
-
<span class="mate-autonomy-title">Mostly free</span>
|
|
2014
|
-
<span class="mate-autonomy-desc">"Do your thing. Only check if it's really important."</span>
|
|
2001
|
+
<h3>Which model should power them?</h3>
|
|
2002
|
+
<p class="mate-hint">You can switch this anytime from the mate's settings.</p>
|
|
2003
|
+
<div class="mate-vendor-option-list">
|
|
2004
|
+
<button class="mate-vendor-option-btn active" data-value="claude">
|
|
2005
|
+
<img src="/claude-code-avatar.png" class="mate-vendor-option-icon" alt="Claude Code">
|
|
2006
|
+
<div class="mate-vendor-option-text">
|
|
2007
|
+
<span class="mate-vendor-option-title">Claude Code</span>
|
|
2008
|
+
<span class="mate-vendor-option-desc">Anthropic's Claude. Thoughtful, conversational, strong at nuanced reasoning and long-form writing. A good default for most mates.</span>
|
|
2009
|
+
</div>
|
|
2015
2010
|
</button>
|
|
2016
|
-
<button class="mate-
|
|
2017
|
-
<
|
|
2018
|
-
<
|
|
2011
|
+
<button class="mate-vendor-option-btn" data-value="codex">
|
|
2012
|
+
<img src="/codex-avatar.png" class="mate-vendor-option-icon" alt="ChatGPT Codex">
|
|
2013
|
+
<div class="mate-vendor-option-text">
|
|
2014
|
+
<span class="mate-vendor-option-title">ChatGPT Codex</span>
|
|
2015
|
+
<span class="mate-vendor-option-desc">OpenAI's GPT coding agent. Fast and direct, strong at code generation and tight execution loops. Good for builder-type mates.</span>
|
|
2016
|
+
</div>
|
|
2019
2017
|
</button>
|
|
2020
2018
|
</div>
|
|
2021
2019
|
</div>
|
|
@@ -474,8 +474,6 @@ export function buildMateInterviewPrompt(mate) {
|
|
|
474
474
|
var styles = sd.communicationStyle.map(function (s) { return styleLabels[s] || s.replace(/_/g, " "); });
|
|
475
475
|
parts.push("Communication: " + styles.join(", "));
|
|
476
476
|
}
|
|
477
|
-
if (sd.autonomy) parts.push("Autonomy: " + sd.autonomy.replace(/_/g, " "));
|
|
478
|
-
|
|
479
477
|
return "Use the /clay-mate-interview skill to start the interview.\n\n" +
|
|
480
478
|
"Mate ID: " + mate.id + "\n" +
|
|
481
479
|
"Mate Directory: .claude/mates/" + mate.id + "\n\n" +
|
|
@@ -732,6 +732,8 @@ export function processMessage(msg) {
|
|
|
732
732
|
getTools()[msg.id] = { el: null, name: msg.name, input: null, done: true, hidden: true };
|
|
733
733
|
} else if (msg.name === "propose_debate" || (msg.name && msg.name.indexOf("propose_debate") !== -1)) {
|
|
734
734
|
getTools()[msg.id] = { el: null, name: msg.name, input: null, done: true, hidden: true };
|
|
735
|
+
} else if (msg.name === "ask_user_questions") {
|
|
736
|
+
getTools()[msg.id] = { el: null, name: msg.name, input: null, done: true, hidden: true };
|
|
735
737
|
} else if (getTodoTools()[msg.name]) {
|
|
736
738
|
getTools()[msg.id] = { el: null, name: msg.name, input: null, done: true, hidden: true };
|
|
737
739
|
} else {
|
|
@@ -225,15 +225,6 @@ export function showMateSidebar(mateId, mateData) {
|
|
|
225
225
|
if (sd.communicationStyle && sd.communicationStyle.length > 0) {
|
|
226
226
|
seedTooltip.appendChild(makeSeedTags("Style", sd.communicationStyle.map(function (s) { return s.replace(/_/g, " "); })));
|
|
227
227
|
}
|
|
228
|
-
if (sd.autonomy) {
|
|
229
|
-
var autonomyLabels = {
|
|
230
|
-
always_ask: "Ask me everything",
|
|
231
|
-
minor_stuff_ok: "Small stuff is fine",
|
|
232
|
-
mostly_autonomous: "Mostly free",
|
|
233
|
-
fully_autonomous: "Full freedom",
|
|
234
|
-
};
|
|
235
|
-
seedTooltip.appendChild(makeSeedRow("Autonomy", autonomyLabels[sd.autonomy] || sd.autonomy.replace(/_/g, " ")));
|
|
236
|
-
}
|
|
237
228
|
}
|
|
238
229
|
|
|
239
230
|
// Clear session list
|
|
@@ -6,7 +6,7 @@ var mateWizardData = {
|
|
|
6
6
|
relationship: null,
|
|
7
7
|
activity: [],
|
|
8
8
|
communicationStyle: [],
|
|
9
|
-
|
|
9
|
+
vendor: "claude",
|
|
10
10
|
};
|
|
11
11
|
|
|
12
12
|
var _sendWs = null;
|
|
@@ -85,19 +85,19 @@ export function initMateWizard(sendWs, onMateCreated) {
|
|
|
85
85
|
})(styleCards[i]);
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
// Step 4:
|
|
89
|
-
var
|
|
90
|
-
for (var i = 0; i <
|
|
88
|
+
// Step 4: Vendor button clicks
|
|
89
|
+
var vendorBtns = document.querySelectorAll("#mate-wizard .mate-vendor-option-btn");
|
|
90
|
+
for (var i = 0; i < vendorBtns.length; i++) {
|
|
91
91
|
(function (btn) {
|
|
92
92
|
btn.addEventListener("click", function () {
|
|
93
|
-
var allBtns = document.querySelectorAll("#mate-wizard .mate-
|
|
93
|
+
var allBtns = document.querySelectorAll("#mate-wizard .mate-vendor-option-btn");
|
|
94
94
|
for (var j = 0; j < allBtns.length; j++) {
|
|
95
95
|
allBtns[j].classList.remove("active");
|
|
96
96
|
}
|
|
97
97
|
btn.classList.add("active");
|
|
98
|
-
mateWizardData.
|
|
98
|
+
mateWizardData.vendor = btn.dataset.value;
|
|
99
99
|
});
|
|
100
|
-
})(
|
|
100
|
+
})(vendorBtns[i]);
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
|
|
@@ -107,7 +107,7 @@ export function openMateWizard() {
|
|
|
107
107
|
relationship: null,
|
|
108
108
|
activity: [],
|
|
109
109
|
communicationStyle: [],
|
|
110
|
-
|
|
110
|
+
vendor: "claude",
|
|
111
111
|
};
|
|
112
112
|
|
|
113
113
|
// Reset UI
|
|
@@ -136,12 +136,12 @@ export function openMateWizard() {
|
|
|
136
136
|
styleCards[i].classList.remove("selected");
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
// Reset
|
|
140
|
-
var
|
|
141
|
-
for (var i = 0; i <
|
|
142
|
-
|
|
143
|
-
if (
|
|
144
|
-
|
|
139
|
+
// Reset vendor
|
|
140
|
+
var vendorBtns = el.querySelectorAll(".mate-vendor-option-btn");
|
|
141
|
+
for (var i = 0; i < vendorBtns.length; i++) {
|
|
142
|
+
vendorBtns[i].classList.remove("active");
|
|
143
|
+
if (vendorBtns[i].dataset.value === "claude") {
|
|
144
|
+
vendorBtns[i].classList.add("active");
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
147
|
|
|
@@ -220,7 +220,7 @@ function collectMateWizardData() {
|
|
|
220
220
|
mateWizardData.communicationStyle.push(selectedStyles[i].dataset.value);
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
-
//
|
|
223
|
+
// Vendor (already set via button clicks)
|
|
224
224
|
}
|
|
225
225
|
|
|
226
226
|
function mateWizardNext() {
|
|
@@ -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;
|
package/lib/sdk-bridge.js
CHANGED
|
@@ -1210,21 +1210,30 @@ function createSDKBridge(opts) {
|
|
|
1210
1210
|
}
|
|
1211
1211
|
}
|
|
1212
1212
|
|
|
1213
|
-
//
|
|
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.
|
|
1214
1220
|
var queryModel = ls.model || sm.currentModel || undefined;
|
|
1215
|
-
|
|
1216
|
-
|
|
1221
|
+
var sessionVendor = session.vendor || (adapter && adapter.vendor) || null;
|
|
1222
|
+
if (sessionVendor) {
|
|
1223
|
+
var vendorModels = (sm.modelsByVendor && sm.modelsByVendor[sessionVendor]) || [];
|
|
1217
1224
|
if (vendorModels.length > 0 && queryModel && vendorModels.indexOf(queryModel) === -1) {
|
|
1218
1225
|
queryModel = vendorModels[0];
|
|
1219
1226
|
}
|
|
1220
1227
|
}
|
|
1221
1228
|
|
|
1222
1229
|
var codexConfig = getCodexConfig(sm);
|
|
1230
|
+
var mergedMcpServers = mergeMcpServers(getMcpServers(), getRemoteMcpServers) || undefined;
|
|
1223
1231
|
var queryOpts = {
|
|
1224
1232
|
cwd: cwd,
|
|
1225
1233
|
model: queryModel,
|
|
1226
1234
|
effort: ls.effort || sm.currentEffort || undefined,
|
|
1227
|
-
toolServers:
|
|
1235
|
+
toolServers: mergedMcpServers,
|
|
1236
|
+
toolServerDescriptors: extractMcpDescriptors(mergedMcpServers) || undefined,
|
|
1228
1237
|
resumeSessionId: session.cliSessionId || undefined,
|
|
1229
1238
|
abortController: linuxUser ? undefined : session.abortController,
|
|
1230
1239
|
canUseTool: function(toolName, input, toolOpts) {
|
|
@@ -1233,6 +1242,9 @@ function createSDKBridge(opts) {
|
|
|
1233
1242
|
onElicitation: function(request, elicitOpts) {
|
|
1234
1243
|
return handleElicitation(session, request, elicitOpts);
|
|
1235
1244
|
},
|
|
1245
|
+
callMcpTool: function(serverName, toolName, args) {
|
|
1246
|
+
return callMcpToolHandler(mergedMcpServers, serverName, toolName, args);
|
|
1247
|
+
},
|
|
1236
1248
|
adapterOptions: {
|
|
1237
1249
|
CLAUDE: claudeOpts,
|
|
1238
1250
|
CODEX: {
|
|
@@ -1443,22 +1455,14 @@ function createSDKBridge(opts) {
|
|
|
1443
1455
|
}
|
|
1444
1456
|
}
|
|
1445
1457
|
|
|
1446
|
-
//
|
|
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.
|
|
1447
1465
|
sm.modelsByVendor = sm.modelsByVendor || {};
|
|
1448
|
-
var otherVendors = Object.keys(adapters).filter(function(v) {
|
|
1449
|
-
return v !== defaultVendor && !sm.modelsByVendor[v];
|
|
1450
|
-
});
|
|
1451
|
-
for (var i = 0; i < otherVendors.length; i++) {
|
|
1452
|
-
(function(v) {
|
|
1453
|
-
adapters[v].init({ cwd: cwd, clayPort: clayPort, clayTls: clayTls, clayAuthToken: clayAuthToken, slug: slug }).then(function(r) {
|
|
1454
|
-
sm.modelsByVendor[v] = r.models || [];
|
|
1455
|
-
sm.capabilitiesByVendor[v] = r.capabilities || {};
|
|
1456
|
-
if (r.slashCommands) sm.setSlashCommandsForVendor(v, r.slashCommands);
|
|
1457
|
-
}).catch(function(e) {
|
|
1458
|
-
console.error("[sdk-bridge] warmup: " + v + " init failed:", e.message || e);
|
|
1459
|
-
});
|
|
1460
|
-
})(otherVendors[i]);
|
|
1461
|
-
}
|
|
1462
1466
|
|
|
1463
1467
|
// Detect installed vendors per-user (binary existence check)
|
|
1464
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
|
}
|
package/lib/sessions.js
CHANGED
|
@@ -680,35 +680,82 @@ function createSessionManager(opts) {
|
|
|
680
680
|
var session = sessions.get(localId);
|
|
681
681
|
if (!session) return { hits: [], total: 0 };
|
|
682
682
|
var q = query.toLowerCase();
|
|
683
|
+
var qLen = query.length;
|
|
683
684
|
var history = session.history;
|
|
684
685
|
var hits = [];
|
|
685
|
-
|
|
686
|
-
|
|
686
|
+
|
|
687
|
+
// Assistant turns can consist of many streaming deltas (especially Codex,
|
|
688
|
+
// where agentMessage/delta fragments arrive in small chunks). We accumulate
|
|
689
|
+
// delta text per turn, scan for ALL occurrences of the query across the
|
|
690
|
+
// accumulated buffer, then map each occurrence back to the historyIndex of
|
|
691
|
+
// the delta that contains its starting offset. This catches multiple
|
|
692
|
+
// matches within a single turn and also matches that straddle delta
|
|
693
|
+
// boundaries.
|
|
694
|
+
var turnBuffer = "";
|
|
695
|
+
var turnSegments = []; // [{ start, end, historyIndex, ts }]
|
|
696
|
+
|
|
697
|
+
function pushScalarHits(text, historyIndex, role, ts) {
|
|
698
|
+
if (!text) return;
|
|
699
|
+
var lower = text.toLowerCase();
|
|
700
|
+
var from = 0;
|
|
701
|
+
while (true) {
|
|
702
|
+
var idx = lower.indexOf(q, from);
|
|
703
|
+
if (idx === -1) break;
|
|
704
|
+
var s = Math.max(0, idx - 15);
|
|
705
|
+
var e = Math.min(text.length, idx + qLen + 15);
|
|
706
|
+
var snippet = (s > 0 ? "\u2026" : "") + text.substring(s, e) + (e < text.length ? "\u2026" : "");
|
|
707
|
+
hits.push({ historyIndex: historyIndex, snippet: snippet, role: role, ts: ts });
|
|
708
|
+
from = idx + qLen;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function flushTurn() {
|
|
713
|
+
if (!turnBuffer || turnSegments.length === 0) {
|
|
714
|
+
turnBuffer = "";
|
|
715
|
+
turnSegments = [];
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
var lowerBuf = turnBuffer.toLowerCase();
|
|
719
|
+
var from = 0;
|
|
720
|
+
var segCursor = 0;
|
|
721
|
+
while (true) {
|
|
722
|
+
var idx = lowerBuf.indexOf(q, from);
|
|
723
|
+
if (idx === -1) break;
|
|
724
|
+
// Advance segCursor to the segment containing idx.
|
|
725
|
+
while (segCursor < turnSegments.length - 1 && turnSegments[segCursor].end <= idx) {
|
|
726
|
+
segCursor++;
|
|
727
|
+
}
|
|
728
|
+
var seg = turnSegments[segCursor];
|
|
729
|
+
var s = Math.max(0, idx - 15);
|
|
730
|
+
var e = Math.min(turnBuffer.length, idx + qLen + 15);
|
|
731
|
+
var snippet = (s > 0 ? "\u2026" : "") + turnBuffer.substring(s, e) + (e < turnBuffer.length ? "\u2026" : "");
|
|
732
|
+
hits.push({ historyIndex: seg.historyIndex, snippet: snippet, role: "assistant", ts: seg.ts });
|
|
733
|
+
from = idx + qLen;
|
|
734
|
+
}
|
|
735
|
+
turnBuffer = "";
|
|
736
|
+
turnSegments = [];
|
|
737
|
+
}
|
|
738
|
+
|
|
687
739
|
for (var i = 0; i < history.length; i++) {
|
|
688
740
|
var entry = history[i];
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
if (
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
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({
|
|
741
|
+
var t = entry.type;
|
|
742
|
+
if (t === "user_message" || t === "mention_user") {
|
|
743
|
+
flushTurn();
|
|
744
|
+
pushScalarHits(entry.text, i, t === "user_message" ? "user" : "assistant", entry._ts || null);
|
|
745
|
+
} else if (t === "delta" && entry.text) {
|
|
746
|
+
turnSegments.push({
|
|
747
|
+
start: turnBuffer.length,
|
|
748
|
+
end: turnBuffer.length + entry.text.length,
|
|
705
749
|
historyIndex: i,
|
|
706
|
-
snippet: snippet,
|
|
707
|
-
role: entry.type === "user_message" ? "user" : "assistant",
|
|
708
750
|
ts: entry._ts || null,
|
|
709
751
|
});
|
|
752
|
+
turnBuffer += entry.text;
|
|
753
|
+
} else if ((t === "mention_response" || t === "debate_turn_done" || t === "debate_comment_injected") && entry.text) {
|
|
754
|
+
flushTurn();
|
|
755
|
+
pushScalarHits(entry.text, i, "assistant", entry._ts || null);
|
|
710
756
|
}
|
|
711
757
|
}
|
|
758
|
+
flushTurn();
|
|
712
759
|
return { hits: hits, total: history.length };
|
|
713
760
|
}
|
|
714
761
|
|