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.
@@ -7,29 +7,85 @@ var panelEl = null;
7
7
  var tableListEl = null;
8
8
  var tableNameEl = null;
9
9
  var tableSchemaEl = null;
10
- var queryInputEl = null;
11
- var paramsInputEl = null;
12
10
  var resultEl = null;
13
11
  var statusEl = null;
14
12
  var dataBtnEl = null;
15
- var conversationsEl = null;
16
- var memoryEl = null;
17
- var knowledgeEl = null;
13
+ var mainColumnEl = null;
18
14
  var currentTables = [];
19
15
  var currentTable = null;
20
16
  var panelOpen = false;
17
+ var routingToScheduler = false;
21
18
 
22
19
  function sendWs(msg) {
23
20
  var ws = wsGetter ? wsGetter() : getWs();
24
21
  if (ws && ws.readyState === 1) ws.send(JSON.stringify(msg));
25
22
  }
26
23
 
24
+ function quoteIdentifier(name) {
25
+ return '"' + String(name || "").replace(/"/g, '""') + '"';
26
+ }
27
+
28
+ function ensurePanel() {
29
+ if (panelEl) return;
30
+ mainColumnEl = document.getElementById("main-column");
31
+ if (!mainColumnEl) return;
32
+
33
+ panelEl = document.createElement("div");
34
+ panelEl.id = "mate-datastore-panel";
35
+ panelEl.className = "hidden";
36
+
37
+ panelEl.innerHTML =
38
+ '<div class="scheduler-top-bar mate-datastore-top-bar">' +
39
+ '<span class="scheduler-top-title mate-datastore-top-title"><i data-lucide="database"></i>Data</span>' +
40
+ '<div class="mate-datastore-top-actions">' +
41
+ '<button id="mate-db-refresh-btn" class="scheduler-close-btn" type="button" title="Refresh tables"><i data-lucide="refresh-cw"></i></button>' +
42
+ '<button id="mate-db-back-btn" class="scheduler-close-btn" type="button" title="Close"><i data-lucide="x"></i></button>' +
43
+ '</div>' +
44
+ '</div>' +
45
+ '<div class="mate-db-status" id="mate-db-status"></div>' +
46
+ '<div class="mate-db-layout">' +
47
+ '<div class="mate-db-table-column">' +
48
+ '<div class="mate-db-section-title">Objects</div>' +
49
+ '<div id="mate-db-table-list" class="mate-db-table-list"></div>' +
50
+ '</div>' +
51
+ '<div class="mate-db-detail">' +
52
+ '<div class="mate-db-section-title" id="mate-db-table-name">No table selected</div>' +
53
+ '<pre id="mate-db-table-schema" class="mate-db-table-schema"></pre>' +
54
+ '<div class="mate-db-section-title">Rows</div>' +
55
+ '<pre id="mate-db-result" class="mate-db-result"></pre>' +
56
+ '</div>' +
57
+ '</div>';
58
+
59
+ mainColumnEl.appendChild(panelEl);
60
+ tableListEl = document.getElementById("mate-db-table-list");
61
+ tableNameEl = document.getElementById("mate-db-table-name");
62
+ tableSchemaEl = document.getElementById("mate-db-table-schema");
63
+ resultEl = document.getElementById("mate-db-result");
64
+ statusEl = document.getElementById("mate-db-status");
65
+
66
+ var refreshBtn = document.getElementById("mate-db-refresh-btn");
67
+ var backBtn = document.getElementById("mate-db-back-btn");
68
+
69
+ if (refreshBtn) {
70
+ refreshBtn.addEventListener("click", function () {
71
+ requestTables();
72
+ });
73
+ }
74
+
75
+ if (backBtn) {
76
+ backBtn.addEventListener("click", function () {
77
+ setSectionVisibility(false);
78
+ });
79
+ }
80
+
81
+ refreshIcons();
82
+ }
83
+
27
84
  function setSectionVisibility(open) {
85
+ ensurePanel();
28
86
  panelOpen = open;
29
87
  if (panelEl) panelEl.classList.toggle("hidden", !open);
30
- if (conversationsEl) conversationsEl.classList.toggle("hidden", open);
31
- if (memoryEl) memoryEl.classList.toggle("hidden", true);
32
- if (knowledgeEl) knowledgeEl.classList.toggle("hidden", true);
88
+ if (mainColumnEl) mainColumnEl.classList.toggle("mate-datastore-open", open);
33
89
  if (dataBtnEl) dataBtnEl.classList.toggle("active", open);
34
90
  refreshIcons();
35
91
  }
@@ -49,6 +105,21 @@ function renderResult(payload) {
49
105
  resultEl.textContent = JSON.stringify(payload, null, 2);
50
106
  }
51
107
 
108
+ function formatColumns(columns) {
109
+ var list = Array.isArray(columns) ? columns : [];
110
+ if (!list.length) return "No column information available.";
111
+ var lines = [];
112
+ for (var i = 0; i < list.length; i++) {
113
+ var col = list[i] || {};
114
+ var line = (col.name || "?") + " " + (col.type || "");
115
+ if (col.pk) line += " PRIMARY KEY";
116
+ if (col.notnull) line += " NOT NULL";
117
+ if (typeof col.dflt_value !== "undefined" && col.dflt_value !== null) line += " DEFAULT " + col.dflt_value;
118
+ lines.push(line.trim());
119
+ }
120
+ return lines.join("\n");
121
+ }
122
+
52
123
  function renderTableList(objects) {
53
124
  currentTables = objects || [];
54
125
  if (!tableListEl) return;
@@ -101,94 +172,43 @@ function selectTable(tableName) {
101
172
  if (obj.type !== "table" && obj.type !== "view") {
102
173
  renderStatus("Selected " + (obj.type || "object") + " " + tableName + ".", "ok");
103
174
  if (tableNameEl) tableNameEl.textContent = tableName;
104
- if (tableSchemaEl) tableSchemaEl.textContent = obj.sql || "";
175
+ if (tableSchemaEl) tableSchemaEl.textContent = (obj.type || "object") + ": " + tableName;
105
176
  renderResult(obj);
106
177
  return;
107
178
  }
108
179
  renderStatus("Loading " + tableName + "...", "info");
109
180
  sendWs({ type: "mate_db_describe", table: tableName });
110
- if (queryInputEl) queryInputEl.value = 'SELECT * FROM "' + tableName.replace(/"/g, '""') + '" LIMIT 50;';
111
- }
112
-
113
- function parseParams() {
114
- if (!paramsInputEl) return [];
115
- var text = paramsInputEl.value.trim();
116
- if (!text) return [];
117
- try {
118
- var parsed = JSON.parse(text);
119
- if (Array.isArray(parsed)) return parsed;
120
- renderStatus("Parameters must be a JSON array.", "error");
121
- } catch (e) {
122
- renderStatus("Parameters must be valid JSON.", "error");
123
- }
124
- return null;
125
- }
126
-
127
- function sendQueryMessage(type) {
128
- var sql = queryInputEl ? queryInputEl.value : "";
129
- var params = parseParams();
130
- if (params === null) return;
131
- if (!sql || !sql.trim()) {
132
- renderStatus("Enter SQL before running it.", "error");
133
- return;
134
- }
135
- sendWs({ type: type, sql: sql, params: params });
181
+ sendWs({ type: "mate_db_query", sql: "SELECT * FROM " + quoteIdentifier(tableName) + " LIMIT 100", params: [] });
136
182
  }
137
183
 
138
184
  export function initMateDatastoreUI(getWsFn) {
139
185
  wsGetter = getWsFn;
140
- panelEl = document.getElementById("mate-sidebar-datastore");
141
- tableListEl = document.getElementById("mate-db-table-list");
142
- tableNameEl = document.getElementById("mate-db-table-name");
143
- tableSchemaEl = document.getElementById("mate-db-table-schema");
144
- queryInputEl = document.getElementById("mate-db-query");
145
- paramsInputEl = document.getElementById("mate-db-params");
146
- resultEl = document.getElementById("mate-db-result");
147
- statusEl = document.getElementById("mate-db-status");
148
186
  dataBtnEl = document.getElementById("mate-data-btn");
149
- conversationsEl = document.getElementById("mate-sidebar-conversations");
150
- memoryEl = document.getElementById("mate-sidebar-memory");
151
- knowledgeEl = document.getElementById("mate-sidebar-knowledge");
152
-
153
- var refreshBtn = document.getElementById("mate-db-refresh-btn");
154
- var queryBtn = document.getElementById("mate-db-query-btn");
155
- var execBtn = document.getElementById("mate-db-exec-btn");
156
- var backBtn = document.getElementById("mate-db-back-btn");
157
-
158
- if (dataBtnEl) {
159
- dataBtnEl.addEventListener("click", function () {
160
- if (panelOpen) {
161
- setSectionVisibility(false);
162
- } else {
163
- setSectionVisibility(true);
164
- requestTables();
165
- }
166
- });
167
- }
187
+ if (!dataBtnEl) return;
168
188
 
169
- if (refreshBtn) {
170
- refreshBtn.addEventListener("click", function () {
189
+ dataBtnEl.addEventListener("click", function () {
190
+ if (panelOpen) {
191
+ setSectionVisibility(false);
192
+ } else {
193
+ setSectionVisibility(true);
171
194
  requestTables();
172
- });
173
- }
174
-
175
- if (queryBtn) {
176
- queryBtn.addEventListener("click", function () {
177
- sendQueryMessage("mate_db_query");
178
- });
179
- }
180
-
181
- if (execBtn) {
182
- execBtn.addEventListener("click", function () {
183
- sendQueryMessage("mate_db_exec");
184
- });
185
- }
195
+ }
196
+ });
186
197
 
187
- if (backBtn) {
188
- backBtn.addEventListener("click", function () {
189
- setSectionVisibility(false);
190
- });
191
- }
198
+ document.addEventListener("click", function (e) {
199
+ var btn = e.target && e.target.closest ? e.target.closest("#scheduler-btn, #mate-scheduler-btn") : null;
200
+ if (!btn || !panelOpen || routingToScheduler) return;
201
+ e.preventDefault();
202
+ e.stopPropagation();
203
+ if (typeof e.stopImmediatePropagation === "function") e.stopImmediatePropagation();
204
+ routingToScheduler = true;
205
+ setSectionVisibility(false);
206
+ setTimeout(function () {
207
+ var schedulerBtn = document.getElementById("scheduler-btn");
208
+ if (schedulerBtn) schedulerBtn.click();
209
+ routingToScheduler = false;
210
+ }, 0);
211
+ }, true);
192
212
 
193
213
  setSectionVisibility(false);
194
214
  }
@@ -234,8 +254,7 @@ export function handleMateDatastoreDescribeResult(msg) {
234
254
  }
235
255
  renderStatus(msg.warning || ("Described " + (msg.table || "table") + "."), msg.warning ? "warn" : "ok");
236
256
  if (tableNameEl) tableNameEl.textContent = msg.table || "Table";
237
- if (tableSchemaEl) tableSchemaEl.textContent = msg.createSql || "";
238
- renderResult(msg);
257
+ if (tableSchemaEl) tableSchemaEl.textContent = formatColumns(msg.columns);
239
258
  }
240
259
 
241
260
  export function handleMateDatastoreQueryResult(msg) {
@@ -244,19 +263,10 @@ export function handleMateDatastoreQueryResult(msg) {
244
263
  renderResult(msg);
245
264
  return;
246
265
  }
247
- renderStatus(msg.warning || ("Returned " + (msg.rowCount || 0) + " row(s)."), msg.warning ? "warn" : "ok");
248
- renderResult(msg);
249
- }
250
-
251
- export function handleMateDatastoreExecResult(msg) {
252
- if (msg.ok === false) {
253
- renderStatus(msg.message || "Execution failed.", "error");
254
- renderResult(msg);
255
- return;
256
- }
257
- renderStatus(msg.warning || ("Applied " + (msg.changes || 0) + " change(s)."), msg.warning ? "warn" : "ok");
258
- renderResult(msg);
259
- requestTables();
266
+ var status = "Showing " + (msg.rows ? msg.rows.length : 0) + " row(s).";
267
+ if (msg.truncated) status = status.replace(".", " (truncated).");
268
+ renderStatus(msg.warning || status, msg.warning ? "warn" : "ok");
269
+ renderResult(msg.rows || []);
260
270
  }
261
271
 
262
272
  export function handleMateDatastoreError(msg) {
@@ -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
- autonomy: "minor_stuff_ok",
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: Autonomy button clicks
89
- var autonomyBtns = document.querySelectorAll("#mate-wizard .mate-autonomy-btn");
90
- for (var i = 0; i < autonomyBtns.length; 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-autonomy-btn");
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.autonomy = btn.dataset.value;
98
+ mateWizardData.vendor = btn.dataset.value;
99
99
  });
100
- })(autonomyBtns[i]);
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
- autonomy: "minor_stuff_ok",
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 autonomy
140
- var autonomyBtns = el.querySelectorAll(".mate-autonomy-btn");
141
- for (var i = 0; i < autonomyBtns.length; i++) {
142
- autonomyBtns[i].classList.remove("active");
143
- if (autonomyBtns[i].dataset.value === "minor_stuff_ok") {
144
- autonomyBtns[i].classList.add("active");
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
- // Autonomy (already set via button clicks)
223
+ // Vendor (already set via button clicks)
224
224
  }
225
225
 
226
226
  function mateWizardNext() {
@@ -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", gemini: "/gemini-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
- // In mate mode: hide sparkle activity, show compact expandable thinking header
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
@@ -1183,11 +1183,13 @@ function createSDKBridge(opts) {
1183
1183
  else if (session.acceptEditsAfterStart) effectiveDefault = "acceptEdits";
1184
1184
  else effectiveDefault = globalMode;
1185
1185
  var modeToApply = session._loopPermissionMode || effectiveDefault;
1186
- if (session.acceptEditsAfterStart) delete session.acceptEditsAfterStart;
1187
1186
  if (modeToApply && modeToApply !== "default") {
1188
1187
  claudeOpts.permissionMode = modeToApply;
1189
1188
  }
1190
1189
  }
1190
+ // Clear one-shot acceptEditsAfterStart regardless of which branch ran above,
1191
+ // so the flag does not linger into subsequent turns.
1192
+ if (session.acceptEditsAfterStart) delete session.acceptEditsAfterStart;
1191
1193
  if (session.cliSessionId && session.lastRewindUuid) {
1192
1194
  claudeOpts.resumeSessionAt = session.lastRewindUuid;
1193
1195
  delete session.lastRewindUuid;
@@ -1208,10 +1210,17 @@ function createSDKBridge(opts) {
1208
1210
  }
1209
1211
  }
1210
1212
 
1211
- // Use vendor-specific model: if session vendor differs from default, use that vendor's default model
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.
1212
1220
  var queryModel = ls.model || sm.currentModel || undefined;
1213
- if (session.vendor && session.vendor !== (adapter && adapter.vendor)) {
1214
- var vendorModels = (sm.modelsByVendor && sm.modelsByVendor[session.vendor]) || [];
1221
+ var sessionVendor = session.vendor || (adapter && adapter.vendor) || null;
1222
+ if (sessionVendor) {
1223
+ var vendorModels = (sm.modelsByVendor && sm.modelsByVendor[sessionVendor]) || [];
1215
1224
  if (vendorModels.length > 0 && queryModel && vendorModels.indexOf(queryModel) === -1) {
1216
1225
  queryModel = vendorModels[0];
1217
1226
  }
@@ -1441,22 +1450,14 @@ function createSDKBridge(opts) {
1441
1450
  }
1442
1451
  }
1443
1452
 
1444
- // Initialize other adapters in parallel (skip if already have models cached)
1453
+ // Non-default adapters are NOT eagerly initialized here. Doing so used
1454
+ // to spawn a CodexAppServer and an mcp-bridge child per project even
1455
+ // when the user never touched that vendor. Lazy paths cover the gap:
1456
+ // - get_vendor_models (project.js) inits a vendor when the user
1457
+ // opens its model picker.
1458
+ // - ensureVendorReady (this file) inits a vendor when a session
1459
+ // actually issues a query with it.
1445
1460
  sm.modelsByVendor = sm.modelsByVendor || {};
1446
- var otherVendors = Object.keys(adapters).filter(function(v) {
1447
- return v !== defaultVendor && !sm.modelsByVendor[v];
1448
- });
1449
- for (var i = 0; i < otherVendors.length; i++) {
1450
- (function(v) {
1451
- adapters[v].init({ cwd: cwd, clayPort: clayPort, clayTls: clayTls, clayAuthToken: clayAuthToken, slug: slug }).then(function(r) {
1452
- sm.modelsByVendor[v] = r.models || [];
1453
- sm.capabilitiesByVendor[v] = r.capabilities || {};
1454
- if (r.slashCommands) sm.setSlashCommandsForVendor(v, r.slashCommands);
1455
- }).catch(function(e) {
1456
- console.error("[sdk-bridge] warmup: " + v + " init failed:", e.message || e);
1457
- });
1458
- })(otherVendors[i]);
1459
- }
1460
1461
 
1461
1462
  // Detect installed vendors per-user (binary existence check)
1462
1463
  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
- sendAndRecord(session, { type: "ask_user_answered", toolId: leftoverAskIds[lai] });
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
  }