clay-server 2.31.0 → 2.32.0-beta.1

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 (74) hide show
  1. package/lib/browser-mcp-server.js +32 -44
  2. package/lib/debate-mcp-server.js +14 -31
  3. package/lib/mcp-local.js +31 -1
  4. package/lib/project-connection.js +4 -2
  5. package/lib/project-filesystem.js +47 -1
  6. package/lib/project-http.js +75 -8
  7. package/lib/project-mcp.js +4 -0
  8. package/lib/project-sessions.js +88 -51
  9. package/lib/project-user-message.js +12 -7
  10. package/lib/project.js +204 -90
  11. package/lib/public/app.js +123 -448
  12. package/lib/public/codex-avatar.png +0 -0
  13. package/lib/public/css/debate.css +3 -2
  14. package/lib/public/css/filebrowser.css +91 -1
  15. package/lib/public/css/icon-strip.css +21 -5
  16. package/lib/public/css/input.css +181 -100
  17. package/lib/public/css/mates.css +43 -0
  18. package/lib/public/css/mention.css +48 -4
  19. package/lib/public/css/menus.css +1 -1
  20. package/lib/public/css/messages.css +2 -0
  21. package/lib/public/css/notifications-center.css +19 -0
  22. package/lib/public/index.html +46 -24
  23. package/lib/public/modules/app-connection.js +138 -37
  24. package/lib/public/modules/app-cursors.js +18 -17
  25. package/lib/public/modules/app-debate-ui.js +9 -9
  26. package/lib/public/modules/app-dm.js +170 -131
  27. package/lib/public/modules/app-favicon.js +28 -26
  28. package/lib/public/modules/app-header.js +79 -68
  29. package/lib/public/modules/app-home-hub.js +55 -47
  30. package/lib/public/modules/app-loop-ui.js +34 -18
  31. package/lib/public/modules/app-loop-wizard.js +6 -6
  32. package/lib/public/modules/app-messages.js +195 -152
  33. package/lib/public/modules/app-misc.js +23 -12
  34. package/lib/public/modules/app-notifications.js +91 -3
  35. package/lib/public/modules/app-panels.js +203 -49
  36. package/lib/public/modules/app-projects.js +159 -150
  37. package/lib/public/modules/app-rate-limit.js +5 -4
  38. package/lib/public/modules/app-rendering.js +149 -101
  39. package/lib/public/modules/app-skills-install.js +4 -4
  40. package/lib/public/modules/context-sources.js +12 -41
  41. package/lib/public/modules/dom-refs.js +21 -0
  42. package/lib/public/modules/filebrowser.js +173 -2
  43. package/lib/public/modules/input.js +86 -0
  44. package/lib/public/modules/mate-sidebar.js +38 -0
  45. package/lib/public/modules/mention.js +24 -6
  46. package/lib/public/modules/scheduler.js +1 -1
  47. package/lib/public/modules/sidebar-mates.js +66 -34
  48. package/lib/public/modules/sidebar-mobile.js +34 -30
  49. package/lib/public/modules/sidebar-projects.js +60 -57
  50. package/lib/public/modules/sidebar-sessions.js +75 -69
  51. package/lib/public/modules/sidebar.js +12 -20
  52. package/lib/public/modules/skills.js +8 -9
  53. package/lib/public/modules/sticky-notes.js +1 -2
  54. package/lib/public/modules/store.js +9 -2
  55. package/lib/public/modules/stt.js +4 -1
  56. package/lib/public/modules/tools.js +14 -9
  57. package/lib/sdk-bridge.js +511 -1113
  58. package/lib/sdk-message-processor.js +123 -134
  59. package/lib/sdk-worker.js +4 -0
  60. package/lib/server-dm.js +1 -0
  61. package/lib/server.js +86 -1
  62. package/lib/sessions.js +47 -36
  63. package/lib/ws-schema.js +2 -0
  64. package/lib/yoke/adapters/claude-worker.js +559 -0
  65. package/lib/yoke/adapters/claude.js +1418 -0
  66. package/lib/yoke/adapters/codex.js +968 -0
  67. package/lib/yoke/adapters/gemini.js +668 -0
  68. package/lib/yoke/codex-app-server.js +307 -0
  69. package/lib/yoke/index.js +199 -0
  70. package/lib/yoke/instructions.js +62 -0
  71. package/lib/yoke/interface.js +92 -0
  72. package/lib/yoke/mcp-bridge-server.js +294 -0
  73. package/lib/yoke/package.json +7 -0
  74. package/package.json +3 -1
@@ -13,7 +13,7 @@ var { execFileSync } = require("child_process");
13
13
  * opts, usersModule, userPresence, matesModule, pushModule,
14
14
  * getSessionForWs, getLinuxUserForSession, getOsUserInfoForWs,
15
15
  * hydrateImageRefs, onProcessingChanged, broadcastPresence,
16
- * getSDK, getProjectList, getProjectCount, getScheduleCount,
16
+ * adapter, getProjectList, getProjectCount, getScheduleCount,
17
17
  * moveScheduleToProject, moveAllSchedulesToProject, getHubSchedules,
18
18
  * fetchVersion, isNewer, onCreateWorktree, IGNORED_DIRS,
19
19
  * scheduleMessage, cancelScheduledMessage,
@@ -46,7 +46,7 @@ function attachSessions(ctx) {
46
46
  var hydrateImageRefs = ctx.hydrateImageRefs;
47
47
  var onProcessingChanged = ctx.onProcessingChanged;
48
48
  var broadcastPresence = ctx.broadcastPresence;
49
- var getSDK = ctx.getSDK;
49
+ var adapter = ctx.adapter;
50
50
  var getProjectList = ctx.getProjectList;
51
51
  var getProjectCount = ctx.getProjectCount;
52
52
  var getScheduleCount = ctx.getScheduleCount;
@@ -96,6 +96,7 @@ function attachSessions(ctx) {
96
96
  var sessionOpts = {};
97
97
  if (ws._clayUser && usersModule.isMultiUser()) sessionOpts.ownerId = ws._clayUser.id;
98
98
  if (msg.sessionVisibility) sessionOpts.sessionVisibility = msg.sessionVisibility;
99
+ if (msg.vendor) sessionOpts.vendor = msg.vendor;
99
100
  var newSess = sm.createSession(sessionOpts, ws);
100
101
  ws._clayActiveSession = newSess.localId;
101
102
  // Apply project-level email defaults to new session
@@ -153,9 +154,7 @@ function attachSessions(ctx) {
153
154
  if (!msg.cliSessionId) return true;
154
155
  var cliSess = require("./cli-sessions");
155
156
  // Try SDK for title first, then fall back to manual parsing
156
- var titlePromise = getSDK().then(function(sdkMod) {
157
- return sdkMod.getSessionInfo(msg.cliSessionId, { dir: cwd });
158
- }).then(function(info) {
157
+ var titlePromise = adapter.getSessionInfo(msg.cliSessionId, { dir: cwd }).then(function(info) {
159
158
  return (info && info.summary) ? info.summary.substring(0, 100) : null;
160
159
  }).catch(function() { return null; });
161
160
 
@@ -200,9 +199,7 @@ function attachSessions(ctx) {
200
199
  }
201
200
  } catch (e) {}
202
201
 
203
- getSDK().then(function(sdkMod) {
204
- return sdkMod.listSessions({ dir: cwd });
205
- }).then(function(sdkSessions) {
202
+ adapter.listSessions({ dir: cwd }).then(function(sdkSessions) {
206
203
  var filtered = sdkSessions.filter(function(s) {
207
204
  return !relayIds[s.sessionId];
208
205
  }).map(function(s) {
@@ -287,11 +284,9 @@ function attachSessions(ctx) {
287
284
  sm.broadcastSessionList();
288
285
  // Sync title to SDK session
289
286
  if (s.cliSessionId) {
290
- getSDK().then(function(sdkInst) {
291
- sdkInst.renameSession(s.cliSessionId, s.title, { dir: cwd }).catch(function(e) {
292
- console.error("[project] SDK renameSession failed:", e.message);
293
- });
294
- }).catch(function() {});
287
+ adapter.renameSession(s.cliSessionId, s.title, { dir: cwd }).catch(function(e) {
288
+ console.error("[project] SDK renameSession failed:", e.message);
289
+ });
295
290
  }
296
291
  }
297
292
  return true;
@@ -381,8 +376,9 @@ function attachSessions(ctx) {
381
376
 
382
377
  if (msg.type === "stop") {
383
378
  var session = getSessionForWs(ws);
384
- if (session && session.abortController && session.isProcessing) {
385
- session.abortController.abort();
379
+ if (session && session.isProcessing) {
380
+ session.taskStopRequested = true;
381
+ if (session.abortController) session.abortController.abort();
386
382
  }
387
383
  return true;
388
384
  }
@@ -521,32 +517,34 @@ function attachSessions(ctx) {
521
517
  return true;
522
518
  }
523
519
 
520
+ // Codex-specific settings (stored on sessionManager, passed to adapter via adapterOptions)
521
+ if (msg.type === "set_codex_approval") {
522
+ sm.codexApproval = msg.approval || "on-failure";
523
+ send({ type: "codex_config", approval: sm.codexApproval, sandbox: sm.codexSandbox || "workspace-write", webSearch: sm.codexWebSearch || "disabled" });
524
+ return true;
525
+ }
526
+ if (msg.type === "set_codex_sandbox") {
527
+ sm.codexSandbox = msg.sandbox || "workspace-write";
528
+ send({ type: "codex_config", approval: sm.codexApproval || "on-failure", sandbox: sm.codexSandbox, webSearch: sm.codexWebSearch || "disabled" });
529
+ return true;
530
+ }
531
+ if (msg.type === "set_codex_websearch") {
532
+ sm.codexWebSearch = msg.webSearch || "disabled";
533
+ send({ type: "codex_config", approval: sm.codexApproval || "on-failure", sandbox: sm.codexSandbox || "workspace-write", webSearch: sm.codexWebSearch });
534
+ return true;
535
+ }
536
+
524
537
  if (msg.type === "rewind_preview") {
525
538
  var session = getSessionForWs(ws);
526
539
  if (!session || !session.cliSessionId || !msg.uuid) return true;
527
- // Reject preview requests while a rewind is executing
528
540
  if (session._rewindInProgress) return true;
529
541
 
530
542
  (async function () {
531
- var result;
532
543
  try {
533
- result = await sdk.getOrCreateRewindQuery(session);
534
- var preview = await result.query.rewindFiles(msg.uuid, { dryRun: true });
535
- var diffs = {};
536
- var changedFiles = preview.filesChanged || [];
537
- for (var f = 0; f < changedFiles.length; f++) {
538
- try {
539
- diffs[changedFiles[f]] = execFileSync(
540
- "git", ["diff", "HEAD", "--", changedFiles[f]],
541
- { cwd: cwd, encoding: "utf8", timeout: 5000 }
542
- ) || "";
543
- } catch (e) { diffs[changedFiles[f]] = ""; }
544
- }
545
- sendTo(ws, { type: "rewind_preview_result", preview: preview, diffs: diffs, uuid: msg.uuid });
544
+ var r = await sdk.rewindPreview(session, msg.uuid);
545
+ sendTo(ws, { type: "rewind_preview_result", preview: r.preview, diffs: r.diffs, uuid: msg.uuid, chatOnly: r.chatOnly || false });
546
546
  } catch (err) {
547
547
  sendTo(ws, { type: "rewind_error", text: "Failed to preview rewind: " + err.message });
548
- } finally {
549
- if (result && result.isTemp) result.cleanup();
550
548
  }
551
549
  })();
552
550
  return true;
@@ -564,12 +562,10 @@ function attachSessions(ctx) {
564
562
  var mode = msg.mode || "both";
565
563
 
566
564
  (async function () {
567
- var result;
568
565
  try {
569
- // File restoration (skip for chat-only mode)
566
+ // File restoration (delegated to adapter via sdk-bridge)
570
567
  if (mode !== "chat") {
571
- result = await sdk.getOrCreateRewindQuery(session);
572
- await result.query.rewindFiles(msg.uuid, { dryRun: false });
568
+ await sdk.rewindExecuteFiles(session, msg.uuid);
573
569
  }
574
570
 
575
571
  // Conversation rollback (skip for files-only mode)
@@ -582,6 +578,14 @@ function attachSessions(ctx) {
582
578
  }
583
579
  }
584
580
 
581
+ // Count turns to roll back BEFORE trimming local history
582
+ var turnsToRollBack = 0;
583
+ if (targetIdx >= 0) {
584
+ for (var ri = targetIdx; ri < session.messageUUIDs.length; ri++) {
585
+ if (session.messageUUIDs[ri].type === "user") turnsToRollBack++;
586
+ }
587
+ }
588
+
585
589
  if (targetIdx >= 0) {
586
590
  var trimTo = session.messageUUIDs[targetIdx].historyIndex;
587
591
  for (var k = trimTo - 1; k >= 0; k--) {
@@ -594,6 +598,15 @@ function attachSessions(ctx) {
594
598
  session.messageUUIDs = session.messageUUIDs.slice(0, targetIdx);
595
599
  }
596
600
 
601
+ // Notify adapter of conversation rollback (e.g. Codex thread/rollback)
602
+ if (turnsToRollBack > 0) {
603
+ try {
604
+ await sdk.rollbackConversation(session, turnsToRollBack);
605
+ } catch (rbErr) {
606
+ console.error("[project-sessions] conversation rollback failed:", rbErr.message || rbErr);
607
+ }
608
+ }
609
+
597
610
  var kept = session.messageUUIDs;
598
611
  session.lastRewindUuid = kept.length > 0 ? kept[kept.length - 1].uuid : null;
599
612
  }
@@ -622,7 +635,6 @@ function attachSessions(ctx) {
622
635
  sendTo(ws, { type: "rewind_error", text: "Rewind failed: " + err.message });
623
636
  } finally {
624
637
  session._rewindInProgress = false;
625
- if (result && result.isTemp) result.cleanup();
626
638
  }
627
639
  })();
628
640
  return true;
@@ -634,22 +646,47 @@ function attachSessions(ctx) {
634
646
  sendTo(ws, { type: "error", text: "Cannot fork: no CLI session" });
635
647
  return true;
636
648
  }
637
- var forkCliId = session.cliSessionId;
638
649
  var forkTitle = (session.title || "New Session") + " (fork)";
639
- getSDK().then(function(sdkMod) {
640
- return sdkMod.forkSession(forkCliId, {
641
- upToMessageId: msg.uuid,
642
- dir: cwd,
643
- });
644
- }).then(function(result) {
645
- var cliSess = require("./cli-sessions");
646
- return cliSess.readCliSessionHistory(cwd, result.sessionId).then(function(history) {
647
- var forked = sm.resumeSession(result.sessionId, { history: history, title: forkTitle }, ws);
648
- if (forked) {
649
- ws._clayActiveSession = forked.localId;
650
- sendTo(ws, { type: "fork_complete", sessionId: forked.localId });
650
+
651
+ sdk.forkSession(session, msg.uuid).then(function(result) {
652
+ if (result.useLocalHistory) {
653
+ // Copy local history up to the target UUID
654
+ var targetIdx = -1;
655
+ for (var fi = 0; fi < session.messageUUIDs.length; fi++) {
656
+ if (session.messageUUIDs[fi].uuid === msg.uuid) { targetIdx = fi; break; }
651
657
  }
652
- });
658
+ var forkHistory = [];
659
+ if (targetIdx >= 0) {
660
+ var trimTo = session.messageUUIDs[targetIdx].historyIndex;
661
+ forkHistory = session.history.slice(0, trimTo);
662
+ } else {
663
+ forkHistory = session.history.slice();
664
+ }
665
+ var forked = sm.createSession({ vendor: session.vendor, ownerId: session.ownerId || null }, ws);
666
+ forked.cliSessionId = result.sessionId;
667
+ forked.title = forkTitle;
668
+ forked.history = forkHistory;
669
+ forked.messageUUIDs = [];
670
+ for (var hi = 0; hi < forkHistory.length; hi++) {
671
+ if (forkHistory[hi].type === "message_uuid") {
672
+ forked.messageUUIDs.push({ uuid: forkHistory[hi].uuid, type: forkHistory[hi].messageType, historyIndex: hi });
673
+ }
674
+ }
675
+ sm.saveSessionFile(forked);
676
+ sm.switchSession(forked.localId, ws, hydrateImageRefs);
677
+ sendTo(ws, { type: "fork_complete", sessionId: forked.localId });
678
+ sm.broadcastSessionList();
679
+ } else {
680
+ // Read history from CLI session files
681
+ var cliSess = require("./cli-sessions");
682
+ return cliSess.readCliSessionHistory(cwd, result.sessionId).then(function(history) {
683
+ var forked = sm.resumeSession(result.sessionId, { history: history, title: forkTitle }, ws);
684
+ if (forked) {
685
+ ws._clayActiveSession = forked.localId;
686
+ sendTo(ws, { type: "fork_complete", sessionId: forked.localId });
687
+ }
688
+ });
689
+ }
653
690
  }).catch(function(e) {
654
691
  sendTo(ws, { type: "error", text: "Fork failed: " + (e.message || e) });
655
692
  });
@@ -23,7 +23,7 @@ var fs = require("fs");
23
23
  * scheduleMessage, cancelScheduledMessage,
24
24
  * loadContextSources, saveContextSources,
25
25
  * digestDmTurn, gateMemory,
26
- * getSDK - lazy ESM loader returning Promise<sdk>
26
+ * adapter - YOKE adapter instance
27
27
  */
28
28
  function attachUserMessage(ctx) {
29
29
  var cwd = ctx.cwd;
@@ -69,7 +69,7 @@ function attachUserMessage(ctx) {
69
69
  var loadContextSources = ctx.loadContextSources;
70
70
  var saveContextSources = ctx.saveContextSources;
71
71
 
72
- var getSDK = ctx.getSDK;
72
+ var adapter = ctx.adapter;
73
73
  var _email = ctx._email;
74
74
 
75
75
  // --------------- Sticky notes ---------------
@@ -292,6 +292,13 @@ function attachUserMessage(ctx) {
292
292
  var session = getSessionForWs(ws);
293
293
  if (!session) return true;
294
294
 
295
+ // Bind vendor to session on first message (if not already set)
296
+ if (!session.vendor && msg.vendor) {
297
+ session.vendor = msg.vendor;
298
+ sm.saveSessionFile(session);
299
+ sm.broadcastSessionList();
300
+ }
301
+
295
302
  // Backfill ownerId for legacy sessions restored without one (multi-user only)
296
303
  if (!session.ownerId && ws._clayUser && usersModule.isMultiUser()) {
297
304
  session.ownerId = ws._clayUser.id;
@@ -337,11 +344,9 @@ function attachUserMessage(ctx) {
337
344
  sm.broadcastSessionList();
338
345
  // Sync auto-title to SDK
339
346
  if (session.cliSessionId) {
340
- getSDK().then(function(sdkMod) {
341
- sdkMod.renameSession(session.cliSessionId, session.title, { dir: cwd }).catch(function(e) {
342
- console.error("[project] SDK renameSession failed:", e.message);
343
- });
344
- }).catch(function() {});
347
+ adapter.renameSession(session.cliSessionId, session.title, { dir: cwd }).catch(function(e) {
348
+ console.error("[project] SDK renameSession failed:", e.message);
349
+ });
345
350
  }
346
351
  }
347
352
 
package/lib/project.js CHANGED
@@ -84,12 +84,8 @@ function validateEnvString(str) {
84
84
  return null;
85
85
  }
86
86
 
87
- // SDK loaded dynamically (ESM module)
88
- var sdkModule = null;
89
- function getSDK() {
90
- if (!sdkModule) sdkModule = import("@anthropic-ai/claude-agent-sdk");
91
- return sdkModule;
92
- }
87
+ // YOKE adapter (replaces direct SDK access)
88
+ var yoke = require("./yoke");
93
89
 
94
90
  // --- Shared constants ---
95
91
  var IGNORED_DIRS = new Set(["node_modules", ".git", ".next", "__pycache__", ".cache", "dist", "build", ".clay", ".claude-relay"]);
@@ -161,8 +157,15 @@ function createProjectContext(opts) {
161
157
  var onCreateWorktree = opts.onCreateWorktree || null;
162
158
  var serverPort = opts.port || 2633;
163
159
  var serverTls = opts.tls || false;
160
+ var serverAuthToken = opts.authToken || null;
164
161
  var latestVersion = null;
165
162
 
163
+ // --- YOKE adapters (multi-vendor, lazy init) ---
164
+ var _yokeState = yoke.createAdapters({ cwd: cwd });
165
+ var adapters = _yokeState.adapters;
166
+ var defaultVendor = adapters.claude ? "claude" : Object.keys(adapters)[0] || "claude";
167
+ var adapter = adapters[defaultVendor] || null;
168
+
166
169
  // Browser MCP server runs in-process via createSdkMcpServer (no child process spawn).
167
170
  // Do NOT write to .claude-local/settings.json -- the SDK reads that too, causing duplicate spawns.
168
171
 
@@ -398,6 +401,9 @@ function createProjectContext(opts) {
398
401
  },
399
402
  onSessionDone: onSessionDone,
400
403
  });
404
+ sm.availableVendors = Object.keys(adapters);
405
+ sm.defaultVendor = defaultVendor;
406
+
401
407
  var _projMode = typeof opts.onGetProjectDefaultMode === "function" ? opts.onGetProjectDefaultMode(slug) : null;
402
408
  var _srvMode = typeof opts.onGetServerDefaultMode === "function" ? opts.onGetServerDefaultMode() : null;
403
409
  sm._savedDefaultMode = (_projMode && _projMode.mode) || (_srvMode && _srvMode.mode) || "default";
@@ -451,6 +457,89 @@ function createProjectContext(opts) {
451
457
  },
452
458
  });
453
459
 
460
+ // --- MCP tool servers (created via YOKE adapter) ---
461
+ var mcpServers = (function () {
462
+ var servers = {};
463
+
464
+ // Debate MCP server (available to both mates and main project)
465
+ try {
466
+ var debateMcp = require("./debate-mcp-server");
467
+ var debateToolDefs = debateMcp.getToolDefs(function onPropose(briefData) {
468
+ return new Promise(function (resolve) {
469
+ var proposalId = "dp_" + Date.now() + "_" + Math.random().toString(36).slice(2, 8);
470
+ briefData.proposalId = proposalId;
471
+ _pendingDebateProposals[proposalId] = {
472
+ resolve: resolve,
473
+ briefData: briefData,
474
+ };
475
+ // The SDK sends tool_executing with briefData as input.
476
+ // Client renders the debate brief card when it sees propose_debate.
477
+ });
478
+ });
479
+ var debateMcpConfig = adapter.createToolServer({ name: "clay-debate", version: "1.0.0", tools: debateToolDefs });
480
+ if (debateMcpConfig) servers[debateMcpConfig.name || "clay-debate"] = debateMcpConfig;
481
+ } catch (e) {
482
+ console.error("[project] Failed to create debate MCP server:", e.message);
483
+ }
484
+
485
+ // Browser MCP server (main project only, not mates)
486
+ if (!isMate) {
487
+ try {
488
+ var browserMcp = require("./browser-mcp-server");
489
+ var browserToolDefs = browserMcp.getToolDefs(sendExtensionCommandAny, function () {
490
+ return Object.values(browserState._browserTabList || {});
491
+ }, {
492
+ watchTab: function (tabId) {
493
+ var key = "tab:" + tabId;
494
+ // Apply to all connected clients' active sessions
495
+ for (var c of clients) {
496
+ if (c.readyState !== 1) continue;
497
+ var sid = c._clayActiveSession || null;
498
+ var active = loadContextSources(slug, sid);
499
+ if (active.indexOf(key) === -1) {
500
+ active.push(key);
501
+ saveContextSources(slug, sid, active);
502
+ c.send(JSON.stringify({ type: "context_sources_state", active: active }));
503
+ }
504
+ }
505
+ return [];
506
+ },
507
+ unwatchTab: function (tabId) {
508
+ var key = "tab:" + tabId;
509
+ for (var c of clients) {
510
+ if (c.readyState !== 1) continue;
511
+ var sid = c._clayActiveSession || null;
512
+ var active = loadContextSources(slug, sid);
513
+ var idx = active.indexOf(key);
514
+ if (idx !== -1) {
515
+ active.splice(idx, 1);
516
+ saveContextSources(slug, sid, active);
517
+ c.send(JSON.stringify({ type: "context_sources_state", active: active }));
518
+ }
519
+ }
520
+ return active;
521
+ },
522
+ });
523
+ var mcpConfig = adapter.createToolServer({ name: "clay-browser", version: "1.0.0", tools: browserToolDefs });
524
+ if (mcpConfig) servers[mcpConfig.name || "clay-browser"] = mcpConfig;
525
+ } catch (e) {
526
+ console.error("[project] Failed to create browser MCP server:", e.message);
527
+ }
528
+ }
529
+
530
+ // Email MCP server (available to both mates and main project)
531
+ // Note: email-mcp-server still uses the legacy create() pattern (not yet converted to getToolDefs).
532
+ try {
533
+ var emailMcp = require("./email-mcp-server");
534
+ var emailMcpConfig = emailMcp.create(_email.createMcpDeps());
535
+ if (emailMcpConfig) servers[emailMcpConfig.name || "clay-email"] = emailMcpConfig;
536
+ } catch (e) {
537
+ console.error("[project] Failed to create email MCP server:", e.message);
538
+ }
539
+
540
+ return Object.keys(servers).length > 0 ? servers : undefined;
541
+ })();
542
+
454
543
  // --- SDK bridge ---
455
544
  var sdk = createSDKBridge({
456
545
  cwd: cwd,
@@ -458,92 +547,17 @@ function createProjectContext(opts) {
458
547
  sessionManager: sm,
459
548
  send: send,
460
549
  pushModule: pushModule,
550
+ adapter: adapter,
551
+ adapters: adapters,
461
552
  getNotificationsModule: function () { return _notifications; },
462
- getSDK: getSDK,
463
553
  mateDisplayName: opts.mateDisplayName || "",
464
554
  isMate: isMate,
465
555
  dangerouslySkipPermissions: dangerouslySkipPermissions,
466
- mcpServers: (function () {
467
- var servers = {};
468
-
469
- // Debate MCP server (available to both mates and main project)
470
- try {
471
- var debateMcp = require("./debate-mcp-server");
472
- var debateMcpConfig = debateMcp.create(function onPropose(briefData) {
473
- return new Promise(function (resolve) {
474
- var proposalId = "dp_" + Date.now() + "_" + Math.random().toString(36).slice(2, 8);
475
- briefData.proposalId = proposalId;
476
- _pendingDebateProposals[proposalId] = {
477
- resolve: resolve,
478
- briefData: briefData,
479
- };
480
- // The SDK sends tool_executing with briefData as input.
481
- // Client renders the debate brief card when it sees propose_debate.
482
- });
483
- });
484
- if (debateMcpConfig) servers[debateMcpConfig.name || "clay-debate"] = debateMcpConfig;
485
- } catch (e) {
486
- console.error("[project] Failed to create debate MCP server:", e.message);
487
- }
488
-
489
- // Browser MCP server (main project only, not mates)
490
- if (!isMate) {
491
- try {
492
- var browserMcp = require("./browser-mcp-server");
493
- var mcpConfig = browserMcp.create(sendExtensionCommandAny, function () {
494
- return Object.values(browserState._browserTabList || {});
495
- }, {
496
- watchTab: function (tabId) {
497
- var key = "tab:" + tabId;
498
- // Apply to all connected clients' active sessions
499
- for (var c of clients) {
500
- if (c.readyState !== 1) continue;
501
- var sid = c._clayActiveSession || null;
502
- var active = loadContextSources(slug, sid);
503
- if (active.indexOf(key) === -1) {
504
- active.push(key);
505
- saveContextSources(slug, sid, active);
506
- c.send(JSON.stringify({ type: "context_sources_state", active: active }));
507
- }
508
- }
509
- return [];
510
- },
511
- unwatchTab: function (tabId) {
512
- var key = "tab:" + tabId;
513
- for (var c of clients) {
514
- if (c.readyState !== 1) continue;
515
- var sid = c._clayActiveSession || null;
516
- var active = loadContextSources(slug, sid);
517
- var idx = active.indexOf(key);
518
- if (idx !== -1) {
519
- active.splice(idx, 1);
520
- saveContextSources(slug, sid, active);
521
- c.send(JSON.stringify({ type: "context_sources_state", active: active }));
522
- }
523
- }
524
- return active;
525
- },
526
- });
527
- if (mcpConfig) servers[mcpConfig.name || "clay-browser"] = mcpConfig;
528
- } catch (e) {
529
- console.error("[project] Failed to create browser MCP server:", e.message);
530
- }
531
- }
532
-
533
- // Email MCP server (available to both mates and main project)
534
- try {
535
- var emailMcp = require("./email-mcp-server");
536
- // Use "default" userId initially; the actual userId is resolved per-tool-call
537
- // via the deps closures which read from the active WS connection.
538
- var emailMcpConfig = emailMcp.create(_email.createMcpDeps());
539
- if (emailMcpConfig) servers[emailMcpConfig.name || "clay-email"] = emailMcpConfig;
540
- } catch (e) {
541
- console.error("[project] Failed to create email MCP server:", e.message);
542
- }
543
-
544
- return Object.keys(servers).length > 0 ? servers : undefined;
545
- })(),
556
+ mcpServers: mcpServers,
546
557
  getRemoteMcpServers: function () { return _mcp.getMcpServers(); },
558
+ clayPort: serverPort,
559
+ clayTls: serverTls,
560
+ clayAuthToken: serverAuthToken,
547
561
  onProcessingChanged: onProcessingChanged,
548
562
  onTurnDone: isMate ? function (session, preview) {
549
563
  digestDmTurn(session, preview);
@@ -718,6 +732,16 @@ function createProjectContext(opts) {
718
732
  return;
719
733
  }
720
734
 
735
+ // --- Vendor model switching ---
736
+ if (msg.type === "get_vendor_models") {
737
+ var vendorModels = (sm.modelsByVendor && sm.modelsByVendor[msg.vendor]) || [];
738
+ var firstModel = vendorModels[0] || "";
739
+ // model value can be string or {value, displayName} object
740
+ var defaultModel = typeof firstModel === "string" ? firstModel : (firstModel.value || "");
741
+ sendTo(ws, { type: "model_info", model: defaultModel, models: vendorModels, vendor: msg.vendor, availableVendors: sm.availableVendors || [], installedVendors: sm.installedVendors || [] });
742
+ return;
743
+ }
744
+
721
745
  // --- Debate ---
722
746
  if (msg.type === "debate_start") {
723
747
  handleDebateStart(ws, msg);
@@ -950,7 +974,7 @@ function createProjectContext(opts) {
950
974
  hydrateImageRefs: hydrateImageRefs,
951
975
  onProcessingChanged: onProcessingChanged,
952
976
  broadcastPresence: broadcastPresence,
953
- getSDK: getSDK,
977
+ adapter: adapter,
954
978
  getProjectList: getProjectList,
955
979
  getProjectCount: getProjectCount,
956
980
  getScheduleCount: getScheduleCount,
@@ -1010,7 +1034,7 @@ function createProjectContext(opts) {
1010
1034
  digestDmTurn: digestDmTurn,
1011
1035
  gateMemory: gateMemory,
1012
1036
  escapeRegex: escapeRegex,
1013
- getSDK: getSDK,
1037
+ adapter: adapter,
1014
1038
  getHubSchedules: getHubSchedules,
1015
1039
  getProjectOwnerId: function () { return projectOwnerId; },
1016
1040
  _email: _email,
@@ -1040,6 +1064,94 @@ function createProjectContext(opts) {
1040
1064
  FS_MAX_SIZE: FS_MAX_SIZE,
1041
1065
  });
1042
1066
 
1067
+ // --- MCP bridge handler for Codex (Track 2) ---
1068
+ // Provides list_tools and call_tool operations over HTTP for mcp-bridge-server.js.
1069
+ // Excludes local MCP servers since Codex manages those natively via Track 1.
1070
+ function getMcpBridgeHandler() {
1071
+ // Build set of local MCP server names to exclude (Codex handles these natively)
1072
+ var localMcpNames = {};
1073
+ try {
1074
+ var mcpLocalModule = require("./mcp-local");
1075
+ var localConfig = mcpLocalModule.readMergedServers();
1076
+ var lcNames = Object.keys(localConfig);
1077
+ for (var li = 0; li < lcNames.length; li++) {
1078
+ localMcpNames[lcNames[li]] = true;
1079
+ }
1080
+ } catch (e) { /* no local MCP config */ }
1081
+
1082
+ return {
1083
+ listTools: function () {
1084
+ var tools = [];
1085
+ var toJSONSchema;
1086
+ try { toJSONSchema = require("zod").toJSONSchema; } catch (e) { /* fallback */ }
1087
+
1088
+ // Helper to extract tools from an SDK MCP server object
1089
+ function extractServerTools(serverName, server) {
1090
+ if (!server || !server.instance || !server.instance._registeredTools) return;
1091
+ var toolNames = Object.keys(server.instance._registeredTools);
1092
+ for (var j = 0; j < toolNames.length; j++) {
1093
+ var toolDef = server.instance._registeredTools[toolNames[j]];
1094
+ var inputSchema = { type: "object", properties: {} };
1095
+ try {
1096
+ if (toJSONSchema && toolDef.inputSchema) inputSchema = toJSONSchema(toolDef.inputSchema);
1097
+ } catch (e) { /* fallback */ }
1098
+ tools.push({
1099
+ server: serverName,
1100
+ name: toolNames[j],
1101
+ description: toolDef.description || toolNames[j],
1102
+ inputSchema: inputSchema,
1103
+ });
1104
+ }
1105
+ }
1106
+
1107
+ // In-app MCP servers (debate, browser, email)
1108
+ if (mcpServers) {
1109
+ var inAppNames = Object.keys(mcpServers);
1110
+ for (var i = 0; i < inAppNames.length; i++) {
1111
+ extractServerTools(inAppNames[i], mcpServers[inAppNames[i]]);
1112
+ }
1113
+ }
1114
+
1115
+ // Remote MCP servers (extension-proxied only, skip local proxy servers)
1116
+ var remoteServers = _mcp.getMcpServers();
1117
+ if (remoteServers) {
1118
+ var remoteNames = Object.keys(remoteServers);
1119
+ for (var ri = 0; ri < remoteNames.length; ri++) {
1120
+ // Skip servers that Codex manages natively via Track 1
1121
+ if (localMcpNames[remoteNames[ri]]) continue;
1122
+ extractServerTools(remoteNames[ri], remoteServers[remoteNames[ri]]);
1123
+ }
1124
+ }
1125
+
1126
+ return Promise.resolve(tools);
1127
+ },
1128
+ callTool: function (serverName, toolName, args) {
1129
+ // Try in-app servers first
1130
+ if (mcpServers && mcpServers[serverName]) {
1131
+ var server = mcpServers[serverName];
1132
+ if (server.instance && server.instance._registeredTools && server.instance._registeredTools[toolName]) {
1133
+ var handler = server.instance._registeredTools[toolName].handler;
1134
+ if (typeof handler === "function") {
1135
+ return Promise.resolve(handler(args));
1136
+ }
1137
+ }
1138
+ }
1139
+ // Try remote/local proxy servers
1140
+ var remoteServers = _mcp.getMcpServers();
1141
+ if (remoteServers && remoteServers[serverName]) {
1142
+ var rServer = remoteServers[serverName];
1143
+ if (rServer.instance && rServer.instance._registeredTools && rServer.instance._registeredTools[toolName]) {
1144
+ var rHandler = rServer.instance._registeredTools[toolName].handler;
1145
+ if (typeof rHandler === "function") {
1146
+ return Promise.resolve(rHandler(args));
1147
+ }
1148
+ }
1149
+ }
1150
+ return Promise.reject(new Error("Tool not found: " + serverName + "/" + toolName));
1151
+ },
1152
+ };
1153
+ }
1154
+
1043
1155
  // --- HTTP handler (delegated to project-http.js) ---
1044
1156
  var _http = attachHTTP({
1045
1157
  cwd: cwd,
@@ -1056,6 +1168,7 @@ function createProjectContext(opts) {
1056
1168
  sendExtensionCommandAny: sendExtensionCommandAny,
1057
1169
  _extToken: _extToken,
1058
1170
  _browserTabList: browserState._browserTabList,
1171
+ getMcpBridgeHandler: getMcpBridgeHandler,
1059
1172
  });
1060
1173
  var handleHTTP = _http.handleHTTP;
1061
1174
 
@@ -1275,6 +1388,7 @@ function createProjectContext(opts) {
1275
1388
  handleMessage: handleMessage,
1276
1389
  handleDisconnection: handleDisconnection,
1277
1390
  handleHTTP: handleHTTP,
1391
+ getMcpBridgeHandler: getMcpBridgeHandler,
1278
1392
  getStatus: getStatus,
1279
1393
  getSessionManager: function () { return sm; },
1280
1394
  getNotificationsModule: function () { return _notifications; },
@@ -1300,7 +1414,7 @@ function createProjectContext(opts) {
1300
1414
  sdk.warmup();
1301
1415
  sdk.startIdleReaper();
1302
1416
  // Migrate existing relay session titles to SDK format (one-time, async)
1303
- sm.migrateSessionTitles(getSDK, cwd);
1417
+ sm.migrateSessionTitles(adapter, cwd);
1304
1418
  },
1305
1419
  destroy: function () {
1306
1420
  sdk.stopIdleReaper();