openfox 2.0.13 → 2.0.15

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 (36) hide show
  1. package/dist/{chat-handler-6NUNVXIU.js → chat-handler-RYOG3WJP.js} +9 -7
  2. package/dist/{chunk-VFRWE5X4.js → chunk-2GX5EIIU.js} +481 -2104
  3. package/dist/{chunk-FQIT7VIA.js → chunk-3IKRNKPI.js} +6 -6
  4. package/dist/{chunk-A52FXWJX.js → chunk-3SGGOBCL.js} +10 -387
  5. package/dist/chunk-EKTGRBDC.js +2072 -0
  6. package/dist/{chunk-3QB2RMX2.js → chunk-F4PMNP7S.js} +2 -2
  7. package/dist/{chunk-TIKQWNYK.js → chunk-J7KOV4ST.js} +2 -2
  8. package/dist/{chunk-2VDLXCLO.js → chunk-KPKSN362.js} +288 -26
  9. package/dist/chunk-NWO6GRYE.js +39 -0
  10. package/dist/chunk-PQ56PX7L.js +397 -0
  11. package/dist/{chunk-HPCGVAS4.js → chunk-SNQT7LNU.js} +18 -1
  12. package/dist/{chunk-5BDVM6YI.js → chunk-YD6NDTKF.js} +1 -1
  13. package/dist/{chunk-EZJUU54W.js → chunk-Z64KW2HD.js} +23 -17
  14. package/dist/cli/dev.js +1 -1
  15. package/dist/cli/index.js +1 -1
  16. package/dist/{config-BU66P4KX.js → config-Z66BQTNX.js} +2 -2
  17. package/dist/{orchestrator-WXEZHUVQ.js → orchestrator-KRCVDTZ6.js} +8 -6
  18. package/dist/package.json +3 -3
  19. package/dist/{processor-GA5NG3T7.js → processor-JHR3Z42W.js} +9 -7
  20. package/dist/{protocol-CaLuIetw.d.ts → protocol-BC1QSQ-Y.d.ts} +16 -1
  21. package/dist/{protocol-YYWMFR35.js → protocol-BKNLAEPJ.js} +3 -3
  22. package/dist/{provider-QPECLUZ7.js → provider-YBTRC77Y.js} +7 -5
  23. package/dist/{provider-manager-YA2WALTF.js → provider-manager-DETXLDKW.js} +3 -2
  24. package/dist/{serve-TJHQ326L.js → serve-AV74ODQ6.js} +13 -9
  25. package/dist/server/index.d.ts +21 -2
  26. package/dist/server/index.js +11 -7
  27. package/dist/server-P7E2IPVL.js +31 -0
  28. package/dist/shared/index.d.ts +2 -2
  29. package/dist/shared/index.js +1 -1
  30. package/dist/tool-adapter-B7QP6NLA.js +7 -0
  31. package/dist/{tools-THXBQJ7A.js → tools-WEE4XDTL.js} +9 -5
  32. package/dist/web/assets/{index-CQw-9GC9.js → index-BMz5-yAf.js} +76 -75
  33. package/dist/web/assets/{index-Db3yUKBI.css → index-Ho_tVUbw.css} +1 -1
  34. package/dist/web/index.html +2 -2
  35. package/dist/web/sw.js +1 -1
  36. package/package.json +3 -3
@@ -1,15 +1,29 @@
1
1
  import {
2
- appendCompactionPrompt
3
- } from "./chunk-FWJ6YXGA.js";
4
- import {
5
- computeDynamicContextHash,
6
- injectWorkflowKickoffIfNeeded,
7
- runAgentTurn,
8
- runChatTurn
9
- } from "./chunk-EZJUU54W.js";
2
+ TEMPLATE_VARIABLES,
3
+ createWebSocketServer,
4
+ deleteProjectWorkflow,
5
+ deleteWorkflow,
6
+ findWorkflowById,
7
+ getAuthConfig,
8
+ getDefaultWorkflowIds,
9
+ hasPassword,
10
+ isDefaultWorkflow,
11
+ isValidToken,
12
+ loadAllWorkflows,
13
+ loadDefaultWorkflows,
14
+ loadProjectWorkflows,
15
+ loadServerAuthConfig,
16
+ loadUserWorkflows,
17
+ requiresAuth,
18
+ saveWorkflow,
19
+ saveWorkflowToProject,
20
+ terminalManager,
21
+ tokenFromPassword,
22
+ verifyPassword,
23
+ workflowExists
24
+ } from "./chunk-EKTGRBDC.js";
10
25
  import {
11
26
  agentExists,
12
- checkAborted,
13
27
  createToolRegistry,
14
28
  deleteAgent,
15
29
  deleteItemFromDir,
@@ -17,21 +31,15 @@ import {
17
31
  deleteProjectSkill,
18
32
  deleteSkill,
19
33
  devServerManager,
20
- executeSubAgent,
21
34
  findAgentById,
22
35
  findSkillById,
23
- getAllInstructions,
24
36
  getDefaultAgentIds,
25
37
  getDefaultIds,
26
38
  getDefaultSkillIds,
27
- getEnabledSkillMetadata,
28
- getSubAgents,
29
- getToolRegistryForAgent,
30
39
  getTopLevelAgents,
31
40
  isDefaultAgent,
32
41
  isDefaultSkill,
33
42
  isSkillEnabled,
34
- jsonSerializer,
35
43
  loadAllAgents,
36
44
  loadAllAgentsDefault,
37
45
  loadAllSkills,
@@ -48,23 +56,22 @@ import {
48
56
  saveItemToDir,
49
57
  saveSkill,
50
58
  saveSkillToProject,
51
- skillExists,
52
- spawnShellProcess
53
- } from "./chunk-2VDLXCLO.js";
59
+ setMcpConfigMode,
60
+ setMcpManagerForTools,
61
+ setMcpTools,
62
+ setNotifyMcpServersChanged,
63
+ skillExists
64
+ } from "./chunk-KPKSN362.js";
54
65
  import {
55
- TurnMetrics,
56
- buildTopLevelSystemPrompt,
57
- createMessageStartEvent
58
- } from "./chunk-L7TDUIQY.js";
66
+ getProject
67
+ } from "./chunk-XAMAYRDA.js";
59
68
  import {
60
69
  getPathSeparator,
61
- getPlatformShell,
62
- isAbsolutePath,
63
- onProcessEvent
70
+ isAbsolutePath
64
71
  } from "./chunk-PBGOZMVY.js";
65
72
  import {
66
- getProject
67
- } from "./chunk-XAMAYRDA.js";
73
+ createMcpTools
74
+ } from "./chunk-NWO6GRYE.js";
68
75
  import {
69
76
  createSession,
70
77
  deleteSession,
@@ -78,9 +85,7 @@ import {
78
85
  emitRunningChanged,
79
86
  emitSessionInitialized,
80
87
  emitUserMessage,
81
- getCurrentContextWindowId,
82
88
  getEventStore,
83
- getRuntimeConfig,
84
89
  getSession,
85
90
  getSessionState,
86
91
  initEventStore,
@@ -93,45 +98,31 @@ import {
93
98
  updateSessionRunning
94
99
  } from "./chunk-SYG2ENUQ.js";
95
100
  import {
96
- createChatErrorMessage,
97
- createChatMessageMessage,
98
- createContextStateMessage,
99
- createErrorMessage,
100
- createGitStatusMessage,
101
- createQueueStateMessage,
102
- createSessionRunningMessage,
103
- isAskAnswerPayload,
104
- isSessionLoadPayload,
105
- parseClientMessage,
106
- serializeServerMessage,
107
- storedEventToServerMessage
108
- } from "./chunk-3QB2RMX2.js";
101
+ createContextStateMessage
102
+ } from "./chunk-F4PMNP7S.js";
109
103
  import {
110
- EventEmitter,
111
- provideAnswer
104
+ EventEmitter
112
105
  } from "./chunk-EU3WWTFH.js";
113
106
  import {
114
107
  initDatabase
115
108
  } from "./chunk-FBGWG4N6.js";
116
- import {
117
- computeSessionStats
118
- } from "./chunk-VUQCQXXJ.js";
119
109
  import {
120
110
  createServerMessage
121
- } from "./chunk-5BDVM6YI.js";
111
+ } from "./chunk-YD6NDTKF.js";
122
112
  import {
123
113
  getGlobalConfigDir
124
114
  } from "./chunk-CQGTEGKL.js";
115
+ import {
116
+ createProviderManager,
117
+ parseDefaultModelSelection
118
+ } from "./chunk-3SGGOBCL.js";
125
119
  import {
126
120
  SessionNotFoundError,
127
121
  buildModelsUrl,
128
- createLLMClient,
129
- createProviderManager,
130
122
  detectModel,
131
123
  ensureVersionPrefix,
132
- getLlmStatus,
133
- parseDefaultModelSelection
134
- } from "./chunk-A52FXWJX.js";
124
+ getLlmStatus
125
+ } from "./chunk-PQ56PX7L.js";
135
126
  import {
136
127
  getBackendDisplayName,
137
128
  getModelProfile
@@ -145,9 +136,9 @@ import {
145
136
  import express from "express";
146
137
  import cors from "cors";
147
138
  import { createServer as createHttpServer } from "http";
148
- import { fileURLToPath as fileURLToPath4 } from "url";
149
- import { dirname as dirname6, resolve as resolve2, join as join7 } from "path";
150
- import { readFile as readFile3 } from "fs/promises";
139
+ import { fileURLToPath as fileURLToPath3 } from "url";
140
+ import { dirname as dirname4, resolve as resolve2, join as join5 } from "path";
141
+ import { readFile } from "fs/promises";
151
142
  import { createServer as createViteServer } from "vite";
152
143
 
153
144
  // src/server/llm/mock.ts
@@ -1435,2002 +1426,196 @@ function createMockLLMClient() {
1435
1426
  tools: response.toolCalls.map((tc) => tc.name)
1436
1427
  });
1437
1428
  }
1438
- for (let i = 0; i < response.toolCalls.length; i++) {
1439
- const tc = response.toolCalls[i];
1440
- yield { type: "tool_call_delta", index: i, id: tc.id, name: tc.name, arguments: JSON.stringify(tc.arguments) };
1441
- }
1442
- const words = response.content.split(" ");
1443
- const slowStreaming = isSlowStreamingPrompt(prompt);
1444
- for (const word of words) {
1445
- yield { type: "text_delta", content: word + " " };
1446
- if (slowStreaming) {
1447
- await sleep(20);
1448
- }
1449
- }
1450
- yield {
1451
- type: "done",
1452
- response: {
1453
- id: `mock-${Date.now()}`,
1454
- content: response.content,
1455
- thinkingContent: "",
1456
- toolCalls: response.toolCalls,
1457
- finishReason: response.finishReason,
1458
- usage: { promptTokens: 100, completionTokens: 50, totalTokens: 150 }
1459
- }
1460
- };
1461
- }
1462
- };
1463
- }
1464
-
1465
- // src/server/ws/server.ts
1466
- import { WebSocketServer, WebSocket as WebSocket2 } from "ws";
1467
- import { spawn as spawn3 } from "child_process";
1468
- import { createHash as createHash2 } from "crypto";
1469
-
1470
- // src/server/ws/terminal.ts
1471
- import WebSocket from "ws";
1472
-
1473
- // src/server/terminal/manager.ts
1474
- import * as pty from "node-pty";
1475
- import os from "os";
1476
- var TerminalManager = class {
1477
- sessions = /* @__PURE__ */ new Map();
1478
- outputCallbacks = /* @__PURE__ */ new Set();
1479
- exitCallbacks = /* @__PURE__ */ new Set();
1480
- outputHistory = /* @__PURE__ */ new Map();
1481
- generateId() {
1482
- return `term_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
1483
- }
1484
- getShell() {
1485
- return getPlatformShell().command;
1486
- }
1487
- resolveWorkdir(workdir) {
1488
- if (workdir && workdir.length > 0) {
1489
- return workdir;
1490
- }
1491
- return os.homedir();
1492
- }
1493
- create(workdir, projectId) {
1494
- const id = this.generateId();
1495
- const shell = this.getShell();
1496
- const cwd = this.resolveWorkdir(workdir);
1497
- const ptyProcess = pty.spawn(shell, [], {
1498
- name: "xterm-256color",
1499
- cols: 80,
1500
- rows: 24,
1501
- cwd,
1502
- env: process.env
1503
- });
1504
- const session = {
1505
- id,
1506
- pty: ptyProcess,
1507
- workdir: cwd,
1508
- projectId: projectId ?? ""
1509
- };
1510
- ptyProcess.onData((data) => {
1511
- const output = { sessionId: id, data };
1512
- for (const cb of this.outputCallbacks) {
1513
- cb(output);
1514
- }
1515
- const history = this.outputHistory.get(id) || [];
1516
- history.push(data);
1517
- if (history.length > 1e4) history.shift();
1518
- this.outputHistory.set(id, history);
1519
- });
1520
- ptyProcess.onExit(({ exitCode }) => {
1521
- logger.info("Terminal session exited", { id, exitCode });
1522
- this.sessions.delete(id);
1523
- this.outputHistory.delete(id);
1524
- for (const cb of this.exitCallbacks) {
1525
- cb(id, exitCode);
1526
- }
1527
- });
1528
- this.sessions.set(id, session);
1529
- logger.info("Terminal session created", { id, shell, cwd });
1530
- return session;
1531
- }
1532
- write(sessionId, data) {
1533
- const session = this.sessions.get(sessionId);
1534
- if (!session) {
1535
- return false;
1536
- }
1537
- try {
1538
- session.pty.write(data);
1539
- return true;
1540
- } catch {
1541
- return false;
1542
- }
1543
- }
1544
- resize(sessionId, cols, rows) {
1545
- const session = this.sessions.get(sessionId);
1546
- if (!session) {
1547
- return false;
1548
- }
1549
- try {
1550
- session.pty.resize(cols, rows);
1551
- return true;
1552
- } catch {
1553
- return false;
1554
- }
1555
- }
1556
- kill(sessionId) {
1557
- const session = this.sessions.get(sessionId);
1558
- if (!session) {
1559
- return false;
1560
- }
1561
- session.pty.kill();
1562
- this.sessions.delete(sessionId);
1563
- logger.info("Terminal session killed", { id: sessionId });
1564
- return true;
1565
- }
1566
- get(sessionId) {
1567
- return this.sessions.get(sessionId);
1568
- }
1569
- getAll() {
1570
- return Array.from(this.sessions.values());
1571
- }
1572
- getByProject(projectId) {
1573
- if (!projectId) return [];
1574
- return Array.from(this.sessions.values()).filter((s) => s.projectId === projectId);
1575
- }
1576
- getOutputHistory(sessionId) {
1577
- return (this.outputHistory.get(sessionId) || []).join("");
1578
- }
1579
- onOutput(callback) {
1580
- this.outputCallbacks.add(callback);
1581
- return () => {
1582
- this.outputCallbacks.delete(callback);
1583
- };
1584
- }
1585
- onExit(callback) {
1586
- this.exitCallbacks.add(callback);
1587
- return () => {
1588
- this.exitCallbacks.delete(callback);
1589
- };
1590
- }
1591
- killAll() {
1592
- for (const session of this.sessions.values()) {
1593
- session.pty.kill();
1594
- }
1595
- this.sessions.clear();
1596
- logger.info("All terminal sessions killed");
1597
- }
1598
- };
1599
- var terminalManager = new TerminalManager();
1600
-
1601
- // src/server/ws/terminal.ts
1602
- var subscriptions = /* @__PURE__ */ new Set();
1603
- var outputHandlerRegistered = false;
1604
- var exitHandlerRegistered = false;
1605
- function registerOutputHandler() {
1606
- if (outputHandlerRegistered) return;
1607
- outputHandlerRegistered = true;
1608
- terminalManager.onOutput((output) => {
1609
- const subs = Array.from(subscriptions).filter(
1610
- (s) => s.sessionId === output.sessionId && s.ws.readyState === WebSocket.OPEN
1611
- );
1612
- for (const sub of subs) {
1613
- sub.ws.send(
1614
- JSON.stringify({
1615
- type: "terminal.output",
1616
- payload: { sessionId: output.sessionId, data: output.data }
1617
- })
1618
- );
1619
- }
1620
- });
1621
- }
1622
- function registerExitHandler() {
1623
- if (exitHandlerRegistered) return;
1624
- exitHandlerRegistered = true;
1625
- terminalManager.onExit((sessionId, exitCode) => {
1626
- const subs = Array.from(subscriptions).filter(
1627
- (s) => s.sessionId === sessionId && s.ws.readyState === WebSocket.OPEN
1628
- );
1629
- for (const sub of subs) {
1630
- sub.ws.send(
1631
- JSON.stringify({
1632
- type: "terminal.exit",
1633
- payload: { sessionId, exitCode }
1634
- })
1635
- );
1636
- }
1637
- for (const sub of subscriptions) {
1638
- if (sub.sessionId === sessionId) {
1639
- subscriptions.delete(sub);
1640
- }
1641
- }
1642
- });
1643
- }
1644
- function handleTerminalMessage(ws, message) {
1645
- switch (message.type) {
1646
- case "terminal.subscribe": {
1647
- if (message.payload.sessionId) {
1648
- const exists = Array.from(subscriptions).some(
1649
- (sub) => sub.ws === ws && sub.sessionId === message.payload.sessionId
1650
- );
1651
- if (!exists) {
1652
- subscriptions.add({ ws, sessionId: message.payload.sessionId });
1653
- }
1654
- registerOutputHandler();
1655
- registerExitHandler();
1656
- const sessionId = message.payload.sessionId;
1657
- const history = terminalManager.getOutputHistory(sessionId);
1658
- if (history) {
1659
- setTimeout(() => {
1660
- ws.send(
1661
- JSON.stringify({
1662
- type: "terminal.output",
1663
- payload: { sessionId, data: history }
1664
- })
1665
- );
1666
- }, 100);
1667
- }
1668
- }
1669
- break;
1670
- }
1671
- case "terminal.write": {
1672
- if (message.payload.sessionId && message.payload.data) {
1673
- terminalManager.write(message.payload.sessionId, message.payload.data);
1674
- }
1675
- break;
1676
- }
1677
- case "terminal.resize": {
1678
- if (message.payload.sessionId && typeof message.payload.cols === "number" && typeof message.payload.rows === "number") {
1679
- terminalManager.resize(message.payload.sessionId, message.payload.cols, message.payload.rows);
1680
- }
1681
- break;
1682
- }
1683
- case "terminal.kill": {
1684
- if (message.payload.sessionId) {
1685
- terminalManager.kill(message.payload.sessionId);
1686
- for (const sub of subscriptions) {
1687
- if (sub.sessionId === message.payload.sessionId) {
1688
- subscriptions.delete(sub);
1689
- }
1690
- }
1691
- ws.send(
1692
- JSON.stringify({
1693
- type: "terminal.killed",
1694
- payload: { sessionId: message.payload.sessionId }
1695
- })
1696
- );
1697
- }
1698
- break;
1699
- }
1700
- }
1701
- }
1702
- function unsubscribeAllFromTerminal(ws) {
1703
- for (const sub of subscriptions) {
1704
- if (sub.ws === ws) {
1705
- subscriptions.delete(sub);
1706
- }
1707
- }
1708
- }
1709
-
1710
- // src/server/workflows/registry.ts
1711
- import { readdir, readFile, writeFile, mkdir, access, unlink } from "fs/promises";
1712
- import { join, dirname } from "path";
1713
- import { constants } from "fs";
1714
- import { fileURLToPath } from "url";
1715
- var __bundleDir = dirname(fileURLToPath(import.meta.url));
1716
- var DEFAULTS_DIR = join(__bundleDir, "defaults");
1717
- var DEFAULTS_DIR_ALT = join(__bundleDir, "workflow-defaults");
1718
- var WORKFLOW_EXTENSION = ".workflow.json";
1719
- function getWorkflowsDir(configDir) {
1720
- return join(configDir, "workflows");
1721
- }
1722
- function getProjectWorkflowsDir(projectDir) {
1723
- return join(projectDir, ".openfox", "workflows");
1724
- }
1725
- async function pathExists2(path) {
1726
- try {
1727
- await access(path, constants.R_OK);
1728
- return true;
1729
- } catch {
1730
- return false;
1731
- }
1732
- }
1733
- async function loadWorkflowsFromDir(dir) {
1734
- if (!await pathExists2(dir)) {
1735
- return [];
1736
- }
1737
- let files;
1738
- try {
1739
- files = (await readdir(dir)).filter((f) => f.endsWith(WORKFLOW_EXTENSION));
1740
- } catch {
1741
- return [];
1742
- }
1743
- const workflows = [];
1744
- for (const file of files) {
1745
- try {
1746
- const raw = await readFile(join(dir, file), "utf-8");
1747
- const parsed = JSON.parse(raw);
1748
- if (parsed.metadata?.id && parsed.steps?.length > 0) {
1749
- workflows.push(parsed);
1750
- } else {
1751
- logger.warn("Skipping invalid workflow file", { file });
1752
- }
1753
- } catch (err) {
1754
- logger.warn("Failed to parse workflow file", { file, error: err instanceof Error ? err.message : String(err) });
1755
- }
1756
- }
1757
- return workflows;
1758
- }
1759
- async function loadDefaultWorkflows() {
1760
- let defaults = await loadWorkflowsFromDir(DEFAULTS_DIR);
1761
- if (!defaults.length) {
1762
- defaults = await loadWorkflowsFromDir(DEFAULTS_DIR_ALT);
1763
- }
1764
- return defaults;
1765
- }
1766
- async function loadUserWorkflows(configDir) {
1767
- return loadWorkflowsFromDir(getWorkflowsDir(configDir));
1768
- }
1769
- async function loadProjectWorkflows(projectDir) {
1770
- return loadWorkflowsFromDir(getProjectWorkflowsDir(projectDir));
1771
- }
1772
- async function loadAllWorkflows(configDir, projectDir) {
1773
- const [defaultWorkflows, userWorkflows] = await Promise.all([loadDefaultWorkflows(), loadUserWorkflows(configDir)]);
1774
- const workflowMap = /* @__PURE__ */ new Map();
1775
- for (const workflow of defaultWorkflows) {
1776
- workflowMap.set(workflow.metadata.id, workflow);
1777
- }
1778
- for (const workflow of userWorkflows) {
1779
- workflowMap.set(workflow.metadata.id, workflow);
1780
- }
1781
- if (projectDir) {
1782
- const projectWorkflows = await loadProjectWorkflows(projectDir);
1783
- for (const workflow of projectWorkflows) {
1784
- workflowMap.set(workflow.metadata.id, workflow);
1785
- }
1786
- }
1787
- return Array.from(workflowMap.values());
1788
- }
1789
- async function saveWorkflowToProject(projectDir, workflow) {
1790
- await saveItemToDir(getProjectWorkflowsDir(projectDir), workflow, WORKFLOW_EXTENSION, jsonSerializer);
1791
- }
1792
- async function deleteProjectWorkflow(projectDir, workflowId) {
1793
- return deleteItemFromDir(getProjectWorkflowsDir(projectDir), workflowId, WORKFLOW_EXTENSION);
1794
- }
1795
- async function getDefaultWorkflowIds() {
1796
- for (const dir of [DEFAULTS_DIR, DEFAULTS_DIR_ALT]) {
1797
- try {
1798
- const files = (await readdir(dir)).filter((f) => f.endsWith(WORKFLOW_EXTENSION));
1799
- return files.map((f) => f.replace(WORKFLOW_EXTENSION, ""));
1800
- } catch {
1801
- }
1802
- }
1803
- return [];
1804
- }
1805
- async function isDefaultWorkflow(workflowId) {
1806
- const defaultIds = await getDefaultWorkflowIds();
1807
- return defaultIds.includes(workflowId);
1808
- }
1809
- function findWorkflowById(workflowId, workflows) {
1810
- return workflows.find((p) => p.metadata.id === workflowId);
1811
- }
1812
- async function workflowExists(configDir, workflowId, projectDir) {
1813
- if (await pathExists2(join(getWorkflowsDir(configDir), `${workflowId}${WORKFLOW_EXTENSION}`))) return true;
1814
- if (projectDir && await pathExists2(join(getProjectWorkflowsDir(projectDir), `${workflowId}${WORKFLOW_EXTENSION}`)))
1815
- return true;
1816
- return false;
1817
- }
1818
- async function saveWorkflow(configDir, workflow) {
1819
- const workflowsDir = getWorkflowsDir(configDir);
1820
- if (!await pathExists2(workflowsDir)) {
1821
- await mkdir(workflowsDir, { recursive: true });
1822
- }
1823
- const filePath = join(workflowsDir, `${workflow.metadata.id}${WORKFLOW_EXTENSION}`);
1824
- await writeFile(filePath, JSON.stringify(workflow, null, 2) + "\n", "utf-8");
1825
- }
1826
- async function deleteWorkflow(configDir, workflowId) {
1827
- const isDefault = await isDefaultWorkflow(workflowId);
1828
- if (isDefault) {
1829
- return { success: false, reason: "Cannot delete built-in defaults" };
1830
- }
1831
- const filePath = join(getWorkflowsDir(configDir), `${workflowId}${WORKFLOW_EXTENSION}`);
1832
- try {
1833
- await unlink(filePath);
1834
- return { success: true };
1835
- } catch {
1836
- return { success: false };
1837
- }
1838
- }
1839
-
1840
- // src/server/workflows/types.ts
1841
- var TERMINAL_DONE = "$done";
1842
- var TERMINAL_BLOCKED = "$blocked";
1843
-
1844
- // src/server/git/diff.ts
1845
- import { spawn as spawn2 } from "child_process";
1846
- function getGitDiffFiles(cwd) {
1847
- return new Promise((resolve3) => {
1848
- const diffProc = spawn2("git", ["diff", "--name-status", "HEAD"], { cwd, stdio: ["ignore", "pipe", "pipe"] });
1849
- const statusProc = spawn2("git", ["status", "--porcelain"], { cwd, stdio: ["ignore", "pipe", "pipe"] });
1850
- let diffStdout = "";
1851
- let statusStdout = "";
1852
- let diffExited = false;
1853
- let statusExited = false;
1854
- let diffCode = null;
1855
- let statusCode = null;
1856
- const collect = () => {
1857
- if (!diffExited || !statusExited) return;
1858
- const files = [];
1859
- if (diffCode === 0) {
1860
- for (const line of diffStdout.split("\n")) {
1861
- if (!line.trim()) continue;
1862
- const [statusChar, ...pathParts] = line.split(" ");
1863
- const path = pathParts.join(" ") || statusChar || "";
1864
- if (!path) continue;
1865
- const status = statusChar === "A" ? "added" : statusChar === "D" ? "deleted" : "modified";
1866
- files.push({ path, status, additions: 0, deletions: 0 });
1867
- }
1868
- }
1869
- if (statusCode === 0) {
1870
- for (const line of statusStdout.split("\n")) {
1871
- if (!line.startsWith("?? ")) continue;
1872
- const path = line.slice(3).trim();
1873
- if (!path) continue;
1874
- files.push({ path, status: "added", additions: 0, deletions: 0 });
1875
- }
1876
- }
1877
- resolve3(files);
1878
- };
1879
- diffProc.stdout.on("data", (data) => {
1880
- diffStdout += data.toString();
1881
- });
1882
- statusProc.stdout.on("data", (data) => {
1883
- statusStdout += data.toString();
1884
- });
1885
- diffProc.on("close", (code) => {
1886
- diffExited = true;
1887
- diffCode = code;
1888
- collect();
1889
- });
1890
- statusProc.on("close", (code) => {
1891
- statusExited = true;
1892
- statusCode = code;
1893
- collect();
1894
- });
1895
- diffProc.on("error", () => {
1896
- diffExited = true;
1897
- diffCode = 1;
1898
- collect();
1899
- });
1900
- statusProc.on("error", () => {
1901
- statusExited = true;
1902
- statusCode = 1;
1903
- collect();
1904
- });
1905
- });
1906
- }
1907
- async function formatGitDiffFiles(cwd) {
1908
- const files = await getGitDiffFiles(cwd);
1909
- if (files.length === 0) return "(none)";
1910
- return files.map((f) => `- ${f.path} (${f.status})`).join("\n");
1911
- }
1912
-
1913
- // src/server/workflows/shell.ts
1914
- function executeShellCommand(command, cwd, timeout, signal) {
1915
- return new Promise((resolve3, reject) => {
1916
- if (checkAborted(signal)) {
1917
- reject(new Error("Aborted"));
1918
- return;
1919
- }
1920
- const proc = spawnShellProcess(command, cwd, signal);
1921
- let stdout = "";
1922
- let stderr = "";
1923
- let killed = false;
1924
- const timer = setTimeout(() => {
1925
- killed = true;
1926
- proc.kill("SIGTERM");
1927
- resolve3({ exitCode: 1, stdout, stderr: stderr + "\nCommand timed out", success: false });
1928
- }, timeout);
1929
- const onAbort = () => {
1930
- if (!killed) {
1931
- killed = true;
1932
- proc.kill("SIGTERM");
1933
- clearTimeout(timer);
1934
- reject(new Error("Aborted"));
1935
- }
1936
- };
1937
- signal?.addEventListener("abort", onAbort);
1938
- proc.stdout?.on("data", (data) => {
1939
- stdout += data.toString();
1940
- });
1941
- proc.stderr?.on("data", (data) => {
1942
- stderr += data.toString();
1943
- });
1944
- proc.on("close", (code) => {
1945
- clearTimeout(timer);
1946
- signal?.removeEventListener("abort", onAbort);
1947
- if (killed) return;
1948
- const exitCode = code ?? 1;
1949
- resolve3({ exitCode, stdout, stderr, success: exitCode === 0 });
1950
- });
1951
- proc.on("error", (err) => {
1952
- clearTimeout(timer);
1953
- signal?.removeEventListener("abort", onAbort);
1954
- if (killed) return;
1955
- resolve3({ exitCode: 1, stdout, stderr: err.message, success: false });
1956
- });
1957
- });
1958
- }
1959
-
1960
- // src/server/workflows/executor.ts
1961
- var TEMPLATE_VARIABLES = [
1962
- { name: "workdir", description: "Working directory of the session" },
1963
- { name: "reason", description: 'Human-readable reason (e.g. "2 criteria remaining")' },
1964
- {
1965
- name: "stepOutput",
1966
- description: "Structured output from the previous step (content, stdout, stderr, exitCode, etc.)"
1967
- },
1968
- {
1969
- name: "verifierFindings",
1970
- description: "@deprecated Use stepOutput.content instead. Output from the last sub-agent step"
1971
- },
1972
- {
1973
- name: "previousStepOutput",
1974
- description: "@deprecated Use stepOutput.stdout instead. Output from the last shell step"
1975
- },
1976
- { name: "criteriaCount", description: "Total number of criteria" },
1977
- { name: "pendingCount", description: "Number of pending/failed criteria" },
1978
- { name: "criteriaList", description: "Formatted list of all criteria with status" },
1979
- { name: "modifiedFiles", description: "List of modified files" }
1980
- ];
1981
- function formatCriteriaList(entries) {
1982
- if (entries.length === 0) return "(none)";
1983
- return entries.map((e) => {
1984
- const status = e.status === "passed" ? "[PASSED]" : e.status === "completed" ? "[NEEDS VERIFICATION]" : e.status === "failed" ? "[FAILED]" : "[NOT COMPLETED]";
1985
- return `- **${e.id}** ${status}: ${e.description}`;
1986
- }).join("\n");
1987
- }
1988
- async function formatModifiedFiles(session) {
1989
- return formatGitDiffFiles(session.workdir);
1990
- }
1991
- function resolveTemplate(template, ctx) {
1992
- let result = template;
1993
- for (const { name } of TEMPLATE_VARIABLES) {
1994
- if (name === "stepOutput" || name === "verifierFindings" || name === "previousStepOutput") continue;
1995
- const value = String(ctx[name]);
1996
- result = result.replace(new RegExp(`\\{\\{${name}\\}\\}`, "g"), value);
1997
- }
1998
- result = result.replace(/\{\{stepOutput\.(\w+)\}\}/g, (_, key) => ctx.stepOutput[key] ?? "");
1999
- result = result.replace(/\{\{verifierFindings\}\}/g, ctx.stepOutput["content"] ?? "");
2000
- result = result.replace(/\{\{previousStepOutput\}\}/g, ctx.stepOutput["stdout"] ?? "");
2001
- return result;
2002
- }
2003
- function evaluateCondition(condition, stepOutcome, metadataEntries) {
2004
- switch (condition.type) {
2005
- case "step_result":
2006
- if (!stepOutcome) return false;
2007
- return stepOutcome.result === condition.result;
2008
- case "metadata_all_match": {
2009
- if (!metadataEntries) return false;
2010
- const entries = metadataEntries[condition.key];
2011
- if (!entries || entries.length === 0) return true;
2012
- return entries.every((e) => e[condition.field] === condition.value);
2013
- }
2014
- case "metadata_all_in": {
2015
- if (!metadataEntries) return false;
2016
- const entries = metadataEntries[condition.key];
2017
- if (!entries || entries.length === 0) return true;
2018
- return entries.every((e) => condition.values.includes(e[condition.field]));
2019
- }
2020
- case "always":
2021
- return true;
2022
- }
2023
- }
2024
- function evaluateTransitions(transitions, stepOutcome, metadataEntries) {
2025
- for (const transition of transitions) {
2026
- if (evaluateCondition(transition.when, stepOutcome, metadataEntries)) {
2027
- return transition.goto;
2028
- }
2029
- }
2030
- return TERMINAL_BLOCKED;
2031
- }
2032
- function emitWorkflowMessage(eventStore, sessionId, content, windowOptions, onMessage) {
2033
- const msgId = crypto.randomUUID();
2034
- eventStore.append(
2035
- sessionId,
2036
- createMessageStartEvent(msgId, "user", content, {
2037
- ...windowOptions ?? {},
2038
- isSystemGenerated: true,
2039
- messageKind: "correction",
2040
- metadata: { type: "workflow", name: "Workflow", color: "#f59e0b" }
2041
- })
2042
- );
2043
- eventStore.append(sessionId, { type: "message.done", data: { messageId: msgId } });
2044
- if (onMessage) {
2045
- onMessage(
2046
- createChatMessageMessage({
2047
- id: msgId,
2048
- role: "user",
2049
- content,
2050
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2051
- isSystemGenerated: true,
2052
- messageKind: "correction",
2053
- metadata: { type: "workflow", name: "Workflow", color: "#f59e0b" }
2054
- })
2055
- );
2056
- }
2057
- return msgId;
2058
- }
2059
- function getCurrentWindowMessageOptions(sessionId) {
2060
- const contextWindowId = getCurrentContextWindowId(sessionId);
2061
- return contextWindowId ? { contextWindowId } : void 0;
2062
- }
2063
- function buildReason(metadataEntries) {
2064
- const entries = metadataEntries?.["criteria"] ?? [];
2065
- const remaining = entries.filter((e) => e.status !== "passed");
2066
- return `${remaining.length} criteria remaining`;
2067
- }
2068
- async function executeWorkflow(workflow, options, subGroup) {
2069
- const { sessionManager, sessionId, llmClient, signal, onMessage } = options;
2070
- const eventStore = getEventStore();
2071
- const startTime = performance.now();
2072
- let iterations = 0;
2073
- const activeSteps = subGroup ? workflow.steps.filter((s) => s.subGroup === subGroup) : workflow.steps;
2074
- let currentStepId = subGroup ? activeSteps[0]?.id ?? workflow.entryStep : workflow.entryStep;
2075
- let lastStepOutput = {};
2076
- const firstEntryForStep = /* @__PURE__ */ new Set();
2077
- const messagesBeforeWorkflow = sessionManager.requireSession(sessionId).messages.length;
2078
- const activeStepIds = new Set(activeSteps.map((s) => s.id));
2079
- const stepsById = /* @__PURE__ */ new Map();
2080
- for (const step of activeSteps) {
2081
- stepsById.set(step.id, step);
2082
- }
2083
- logger.debug("Workflow executor starting", { sessionId, workflow: workflow.metadata.id });
2084
- if (workflow.startCondition && workflow.startCondition.type !== "always") {
2085
- const session = sessionManager.requireSession(sessionId);
2086
- const conditionMet = evaluateCondition(
2087
- workflow.startCondition,
2088
- null,
2089
- session.metadataEntries
2090
- );
2091
- if (!conditionMet) {
2092
- logger.debug("Workflow start condition not met", { sessionId, condition: workflow.startCondition.type });
2093
- return {
2094
- finalAction: {
2095
- type: "BLOCKED",
2096
- reason: `Start condition not met: ${workflow.startCondition.type}`,
2097
- blockedCriteria: []
2098
- },
2099
- iterations: 0,
2100
- totalTime: (performance.now() - startTime) / 1e3
2101
- };
2102
- }
2103
- }
2104
- const startMsgId = crypto.randomUUID();
2105
- const startWindowOpts = getCurrentWindowMessageOptions(sessionId);
2106
- eventStore.append(
2107
- sessionId,
2108
- createMessageStartEvent(
2109
- startMsgId,
2110
- "user",
2111
- JSON.stringify({
2112
- workflowName: workflow.metadata.name,
2113
- workflowId: workflow.metadata.id,
2114
- workflowColor: workflow.metadata.color
2115
- }),
2116
- { ...startWindowOpts ?? {}, isSystemGenerated: true, messageKind: "workflow-started" }
2117
- )
2118
- );
2119
- eventStore.append(sessionId, { type: "message.done", data: { messageId: startMsgId } });
2120
- if (options.userMessage) {
2121
- sessionManager.addMessage(sessionId, {
2122
- role: "user",
2123
- content: options.userMessage.content,
2124
- ...options.userMessage.attachments ? { attachments: options.userMessage.attachments } : {}
2125
- });
2126
- }
2127
- while (iterations < workflow.settings.maxIterations) {
2128
- if (signal?.aborted) {
2129
- logger.debug("Workflow executor aborted", { sessionId, iterations });
2130
- return {
2131
- finalAction: { type: "RUN_BUILDER", reason: "Aborted" },
2132
- iterations,
2133
- totalTime: (performance.now() - startTime) / 1e3
2134
- };
2135
- }
2136
- iterations++;
2137
- const step = stepsById.get(currentStepId);
2138
- if (!step) {
2139
- logger.error("Workflow step not found", { sessionId, stepId: currentStepId });
2140
- return {
2141
- finalAction: { type: "BLOCKED", reason: `Step "${currentStepId}" not found in workflow`, blockedCriteria: [] },
2142
- iterations,
2143
- totalTime: (performance.now() - startTime) / 1e3
2144
- };
2145
- }
2146
- const session = sessionManager.requireSession(sessionId);
2147
- const currentWindowMessageOptions = getCurrentWindowMessageOptions(sessionId);
2148
- const criteriaEntries = session.metadataEntries["criteria"] ?? [];
2149
- const templateCtx = {
2150
- workdir: session.workdir,
2151
- reason: buildReason(session.metadataEntries),
2152
- verifierFindings: lastStepOutput["content"] ?? "",
2153
- previousStepOutput: lastStepOutput["stdout"] ?? "",
2154
- criteriaCount: criteriaEntries.length,
2155
- pendingCount: criteriaEntries.filter((e) => e.status !== "passed").length,
2156
- criteriaList: formatCriteriaList(criteriaEntries),
2157
- modifiedFiles: await formatModifiedFiles(session),
2158
- stepOutput: lastStepOutput
2159
- };
2160
- sessionManager.setPhase(sessionId, step.phase);
2161
- logger.debug("Workflow step executing", { sessionId, iteration: iterations, stepId: step.id, stepType: step.type });
2162
- let stepOutcome = null;
2163
- switch (step.type) {
2164
- case "agent": {
2165
- const agentStep = step;
2166
- const STEP_DONE_PROMPT = "\n\nOnce you're done, call step_done()";
2167
- const STEP_DONE_NUDGE = "You haven't called step_done(). If you haven't finished the task, continue and when you're finished call step_done()";
2168
- let promptContent;
2169
- let nudgeContent;
2170
- if (!firstEntryForStep.has(step.id) && agentStep.prompt) {
2171
- const resolvedPrompt = resolveTemplate(agentStep.prompt, templateCtx);
2172
- promptContent = resolvedPrompt + STEP_DONE_PROMPT;
2173
- const promptMsgId = crypto.randomUUID();
2174
- const msgMetadata = { type: "workflow", name: "Workflow", color: "#f59e0b" };
2175
- eventStore.append(
2176
- sessionId,
2177
- createMessageStartEvent(promptMsgId, "user", promptContent, {
2178
- ...currentWindowMessageOptions ?? {},
2179
- isSystemGenerated: true,
2180
- messageKind: "auto-prompt",
2181
- metadata: msgMetadata
2182
- })
2183
- );
2184
- eventStore.append(sessionId, { type: "message.done", data: { messageId: promptMsgId } });
2185
- if (onMessage) {
2186
- onMessage(
2187
- createChatMessageMessage({
2188
- id: promptMsgId,
2189
- role: "user",
2190
- content: promptContent,
2191
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2192
- isSystemGenerated: true,
2193
- messageKind: "auto-prompt",
2194
- metadata: { type: "workflow", name: "Workflow", color: "#f59e0b" }
2195
- })
2196
- );
2197
- }
2198
- } else if (firstEntryForStep.has(step.id)) {
2199
- const parts = [];
2200
- if (agentStep.nudgePrompt) {
2201
- const resolvedNudge = resolveTemplate(agentStep.nudgePrompt, templateCtx);
2202
- parts.push(resolvedNudge);
2203
- }
2204
- parts.push(STEP_DONE_NUDGE);
2205
- nudgeContent = parts.join("\n\n");
2206
- emitWorkflowMessage(eventStore, sessionId, nudgeContent, currentWindowMessageOptions, onMessage);
2207
- }
2208
- const turnMetrics = new TurnMetrics();
2209
- const es = getEventStore();
2210
- const append = (event) => es.append(sessionId, event);
2211
- let stepDoneCalled = false;
2212
- const agentResult = await runAgentTurn(
2213
- {
2214
- sessionManager,
2215
- sessionId,
2216
- llmClient,
2217
- ...options.statsIdentity ? { statsIdentity: options.statsIdentity } : {},
2218
- ...signal ? { signal } : {},
2219
- ...onMessage ? { onMessage } : {}
2220
- },
2221
- turnMetrics,
2222
- agentStep.agentId ?? "planner",
2223
- append,
2224
- {
2225
- ...!firstEntryForStep.has(step.id) && !agentStep.prompt && options.injectWorkflowKickoff === true ? { injectKickoff: () => injectWorkflowKickoffIfNeeded(sessionManager, sessionId, es) } : {},
2226
- onToolExecuted: (toolCall, toolResult) => {
2227
- if (toolCall.name === "step_done" && toolResult.success) {
2228
- stepDoneCalled = true;
2229
- }
2230
- if (toolResult.success && ["write_file", "edit_file"].includes(toolCall.name)) {
2231
- const path = toolCall.arguments["path"];
2232
- sessionManager.addModifiedFile(sessionId, path);
2233
- }
2234
- }
2235
- }
2236
- );
2237
- firstEntryForStep.add(step.id);
2238
- const agentReturnValue = agentResult.returnValueResult ?? "completed";
2239
- lastStepOutput = {
2240
- ...agentResult.returnValueContent ? { content: agentResult.returnValueContent } : {},
2241
- ...agentResult.returnValueResult ? { result: agentResult.returnValueResult } : {},
2242
- stepDoneCalled: String(stepDoneCalled)
2243
- };
2244
- stepOutcome = { result: agentReturnValue, output: lastStepOutput };
2245
- if (!stepDoneCalled) {
2246
- logger.debug("step_done not called, looping agent step", {
2247
- sessionId,
2248
- stepId: step.id,
2249
- iteration: iterations
2250
- });
2251
- continue;
2252
- }
2253
- break;
2254
- }
2255
- case "sub_agent": {
2256
- const subStep = step;
2257
- const turnMetrics = new TurnMetrics();
2258
- const promptTemplate = subStep.prompt ?? "Perform your task.";
2259
- const resolvedPrompt = resolveTemplate(promptTemplate, templateCtx);
2260
- const allAgents = await loadAllAgentsDefault();
2261
- const agentDef = findAgentById(subStep.subAgentType, allAgents);
2262
- if (!agentDef) {
2263
- logger.error("Sub-agent definition not found", { subAgentType: subStep.subAgentType });
2264
- stepOutcome = { result: "error", output: {} };
2265
- break;
2266
- }
2267
- const toolRegistry = getToolRegistryForAgent(agentDef);
2268
- const filteredToolRegistry = {
2269
- tools: toolRegistry.tools.filter((t) => t.name !== "step_done"),
2270
- definitions: toolRegistry.definitions.filter((d) => d.type === "function" && d.function.name !== "step_done"),
2271
- execute: toolRegistry.execute
2272
- };
2273
- const result = await executeSubAgent({
2274
- subAgentType: subStep.subAgentType,
2275
- prompt: resolvedPrompt,
2276
- sessionManager,
2277
- sessionId,
2278
- llmClient,
2279
- toolRegistry: filteredToolRegistry,
2280
- turnMetrics,
2281
- statsIdentity: options.statsIdentity ?? {
2282
- providerId: "",
2283
- providerName: "",
2284
- backend: "unknown",
2285
- model: llmClient.getModel()
2286
- },
2287
- ...signal ? { signal } : {},
2288
- ...onMessage ? { onMessage } : {}
2289
- });
2290
- lastStepOutput = { content: result.content ?? "", ...result.result ? { result: result.result } : {} };
2291
- stepOutcome = { result: result.result ?? "success", output: lastStepOutput };
2292
- break;
2293
- }
2294
- case "shell": {
2295
- const shellStep = step;
2296
- const command = resolveTemplate(shellStep.command, templateCtx);
2297
- const timeout = shellStep.timeout ?? 6e4;
2298
- const successCodes = shellStep.successExitCodes ?? [0];
2299
- const shellMsgId = crypto.randomUUID();
2300
- eventStore.append(
2301
- sessionId,
2302
- createMessageStartEvent(shellMsgId, "user", `Running: \`${command}\``, {
2303
- ...currentWindowMessageOptions ?? {},
2304
- isSystemGenerated: true,
2305
- messageKind: "auto-prompt",
2306
- metadata: { type: "workflow", name: "Workflow", color: "#f59e0b" }
2307
- })
2308
- );
2309
- if (onMessage) {
2310
- onMessage(
2311
- createChatMessageMessage({
2312
- id: shellMsgId,
2313
- role: "user",
2314
- content: `Running: \`${command}\``,
2315
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2316
- isSystemGenerated: true,
2317
- messageKind: "auto-prompt",
2318
- metadata: { type: "workflow", name: "Workflow", color: "#f59e0b" }
2319
- })
2320
- );
2321
- }
2322
- const result = await executeShellCommand(command, session.workdir, timeout, signal);
2323
- const output = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
2324
- lastStepOutput = { stdout: result.stdout ?? "", stderr: result.stderr ?? "", exitCode: String(result.exitCode) };
2325
- const outputContent = output ? `Exit code: ${result.exitCode}
2326
- \`\`\`
2327
- ${output.slice(0, 1e4)}
2328
- \`\`\`` : `Exit code: ${result.exitCode}`;
2329
- eventStore.append(sessionId, { type: "message.done", data: { messageId: shellMsgId } });
2330
- const outputMsgId = crypto.randomUUID();
2331
- eventStore.append(
2332
- sessionId,
2333
- createMessageStartEvent(outputMsgId, "user", outputContent, {
2334
- ...currentWindowMessageOptions ?? {},
2335
- isSystemGenerated: true,
2336
- messageKind: "correction"
2337
- })
2338
- );
2339
- eventStore.append(sessionId, { type: "message.done", data: { messageId: outputMsgId } });
2340
- if (onMessage) {
2341
- onMessage(
2342
- createChatMessageMessage({
2343
- id: outputMsgId,
2344
- role: "user",
2345
- content: outputContent,
2346
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2347
- isSystemGenerated: true,
2348
- messageKind: "correction"
2349
- })
2350
- );
2351
- }
2352
- stepOutcome = { result: successCodes.includes(result.exitCode) ? "success" : "failure", output: lastStepOutput };
2353
- break;
2354
- }
2355
- }
2356
- const refreshedSession = sessionManager.requireSession(sessionId);
2357
- const effectiveTransitions = subGroup ? step.transitions.filter((t) => !t.subGroup || t.subGroup === subGroup) : step.transitions;
2358
- const rawNextStepId = evaluateTransitions(effectiveTransitions, stepOutcome, refreshedSession.metadataEntries);
2359
- const nextStepId = subGroup && rawNextStepId !== TERMINAL_DONE && rawNextStepId !== TERMINAL_BLOCKED && !activeStepIds.has(rawNextStepId) ? TERMINAL_DONE : rawNextStepId;
2360
- if (nextStepId === TERMINAL_DONE) {
2361
- sessionManager.setPhase(sessionId, "done");
2362
- const totalTimeSeconds = Math.round((performance.now() - startTime) / 100) / 10;
2363
- const completedSession = sessionManager.requireSession(sessionId);
2364
- const workflowMessages = completedSession.messages.slice(messagesBeforeWorkflow);
2365
- const workflowStats = computeSessionStats(workflowMessages);
2366
- const totalToolCalls = workflowMessages.reduce((sum, m) => sum + (m.toolCalls?.length ?? 0), 0);
2367
- const taskCompletedData = {
2368
- summary: null,
2369
- iterations,
2370
- totalTimeSeconds,
2371
- totalToolCalls,
2372
- totalTokensGenerated: workflowStats?.generationTokens ?? 0,
2373
- avgGenerationSpeed: workflowStats?.avgGenerationSpeed ?? 0,
2374
- responseCount: workflowStats?.responseCount ?? 0,
2375
- llmCallCount: workflowStats?.llmCallCount ?? 0,
2376
- criteria: [],
2377
- workflowName: workflow.metadata.name,
2378
- workflowId: workflow.metadata.id,
2379
- ...workflow.metadata.color ? { workflowColor: workflow.metadata.color } : {}
2380
- };
2381
- eventStore.append(sessionId, { type: "task.completed", data: taskCompletedData });
2382
- const markerMsgId = crypto.randomUUID();
2383
- eventStore.append(
2384
- sessionId,
2385
- createMessageStartEvent(markerMsgId, "user", JSON.stringify(taskCompletedData), {
2386
- ...currentWindowMessageOptions ?? {},
2387
- isSystemGenerated: true,
2388
- messageKind: "task-completed"
2389
- })
2390
- );
2391
- eventStore.append(sessionId, { type: "message.done", data: { messageId: markerMsgId } });
2392
- logger.debug("Workflow executor complete", { sessionId, iterations });
2393
- const doneAction = { type: "DONE" };
2394
- return {
2395
- finalAction: doneAction,
2396
- iterations,
2397
- totalTime: totalTimeSeconds
2398
- };
2399
- }
2400
- if (nextStepId === TERMINAL_BLOCKED) {
2401
- sessionManager.setPhase(sessionId, "blocked");
2402
- const reason = "No matching transition";
2403
- emitWorkflowMessage(eventStore, sessionId, `Runner blocked: ${reason}`, currentWindowMessageOptions, onMessage);
2404
- logger.warn("Workflow executor blocked", { sessionId, iterations, reason });
2405
- const blockedAction = { type: "BLOCKED", reason, blockedCriteria: [] };
2406
- return {
2407
- finalAction: blockedAction,
2408
- iterations,
2409
- totalTime: (performance.now() - startTime) / 1e3
2410
- };
2411
- }
2412
- currentStepId = nextStepId;
2413
- }
2414
- logger.warn("Workflow executor max iterations reached", { sessionId, iterations });
2415
- return {
2416
- finalAction: {
2417
- type: "BLOCKED",
2418
- reason: `Max iterations (${workflow.settings.maxIterations}) reached`,
2419
- blockedCriteria: []
2420
- },
2421
- iterations,
2422
- totalTime: (performance.now() - startTime) / 1e3
2423
- };
2424
- }
2425
-
2426
- // src/server/runner/orchestrator.ts
2427
- async function runOrchestrator(options) {
2428
- const runtimeConfig = getRuntimeConfig();
2429
- const workflowId = options.workflowId ?? runtimeConfig.activeWorkflowId ?? "default";
2430
- const configDir = getGlobalConfigDir(runtimeConfig.mode ?? "production");
2431
- const workflows = await loadAllWorkflows(configDir);
2432
- const workflow = findWorkflowById(workflowId, workflows);
2433
- if (!workflow) {
2434
- throw new Error(`Workflow "${workflowId}" not found`);
2435
- }
2436
- logger.debug("Using workflow executor", {
2437
- sessionId: options.sessionId,
2438
- workflow: workflow.metadata.id,
2439
- subGroup: options.subGroup
2440
- });
2441
- return executeWorkflow(workflow, options, options.subGroup);
2442
- }
2443
-
2444
- // src/server/auth.ts
2445
- import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
2446
- import { join as join2, dirname as dirname2 } from "path";
2447
- import { createHash, privateDecrypt, createPublicKey } from "crypto";
2448
- function getAuthConfigPath() {
2449
- const configDir = getRuntimeConfig();
2450
- const mode = configDir.mode === "development" ? "development" : configDir.mode === "test" ? "test" : "production";
2451
- if (mode === "test") {
2452
- const cwd = process.cwd();
2453
- const base = cwd.endsWith("/e2e") ? cwd : join2(cwd, "e2e");
2454
- const testAuthPath = join2(base, ".openfox-test", "auth.json");
2455
- return testAuthPath;
2456
- }
2457
- const home = process.env["HOME"] || process.env["USERPROFILE"] || "";
2458
- const basePath = process.env["XDG_CONFIG_HOME"] || `${home}/.config`;
2459
- const suffix = mode === "development" ? "-dev" : "";
2460
- return `${basePath}/openfox${suffix}/auth.json`;
2461
- }
2462
- function getKeyPath() {
2463
- const authPath = getAuthConfigPath();
2464
- const dir = dirname2(authPath);
2465
- return join2(dir, "auth.key");
2466
- }
2467
- var cachedAuth = null;
2468
- var cachedPrivateKey = null;
2469
- async function loadPrivateKey() {
2470
- if (cachedPrivateKey) {
2471
- return cachedPrivateKey;
2472
- }
2473
- const keyPath = getKeyPath();
2474
- const keyDir = dirname2(keyPath);
2475
- try {
2476
- cachedPrivateKey = await readFile2(keyPath, "utf-8");
2477
- return cachedPrivateKey;
2478
- } catch {
2479
- const { privateKey } = await import("crypto").then(
2480
- (c) => c.generateKeyPairSync("rsa", {
2481
- modulusLength: 2048,
2482
- privateKeyEncoding: { type: "pkcs8", format: "pem" },
2483
- publicKeyEncoding: { type: "spki", format: "pem" }
2484
- })
2485
- );
2486
- await mkdir2(keyDir, { recursive: true });
2487
- await writeFile2(keyPath, privateKey, { mode: 384 });
2488
- cachedPrivateKey = privateKey;
2489
- return privateKey;
2490
- }
2491
- }
2492
- async function loadServerAuthConfig() {
2493
- const configDir = getRuntimeConfig();
2494
- const isTestMode = configDir.mode === "test";
2495
- if (!isTestMode) {
2496
- if (cachedAuth) {
2497
- return cachedAuth;
2498
- }
2499
- }
2500
- try {
2501
- const authPath = getAuthConfigPath();
2502
- const data = await readFile2(authPath, "utf-8");
2503
- const authConfig = JSON.parse(data);
2504
- cachedAuth = authConfig;
2505
- return authConfig;
2506
- } catch {
2507
- return null;
2508
- }
2509
- }
2510
- function getAuthConfig() {
2511
- return cachedAuth;
2512
- }
2513
- function hashPassword(password) {
2514
- return createHash("sha256").update(password).digest("hex");
2515
- }
2516
- function requiresAuth() {
2517
- return cachedAuth?.strategy === "network";
2518
- }
2519
- function hasPassword() {
2520
- return cachedAuth?.encryptedPassword != null && cachedAuth.encryptedPassword.length > 0;
2521
- }
2522
- async function verifyPassword(password) {
2523
- const encryptedPassword = cachedAuth?.encryptedPassword;
2524
- if (!encryptedPassword) return false;
2525
- const privateKey = await loadPrivateKey();
2526
- try {
2527
- const decrypted = privateDecrypt({ key: privateKey, padding: 1 }, Buffer.from(encryptedPassword, "base64"));
2528
- return decrypted.toString() === password;
2529
- } catch {
2530
- return false;
2531
- }
2532
- }
2533
- async function tokenFromPassword(password) {
2534
- const privateKey = await loadPrivateKey();
2535
- const passwordHash = hashPassword(password);
2536
- const sign = await import("crypto").then((c) => {
2537
- const s = c.createSign("SHA256");
2538
- s.update(passwordHash);
2539
- s.end();
2540
- return s.sign(privateKey, "base64");
2541
- });
2542
- return sign;
2543
- }
2544
- async function isValidToken(token) {
2545
- if (!cachedAuth?.encryptedPassword) return false;
2546
- const privateKey = await loadPrivateKey();
2547
- try {
2548
- const decrypted = privateDecrypt(
2549
- { key: privateKey, padding: 1 },
2550
- Buffer.from(cachedAuth.encryptedPassword, "base64")
2551
- );
2552
- const storedPassword = decrypted.toString();
2553
- const storedHash = hashPassword(storedPassword);
2554
- const verify = await import("crypto").then((c) => {
2555
- const v = c.createVerify("SHA256");
2556
- v.update(storedHash);
2557
- v.end();
2558
- return v;
2559
- });
2560
- const publicKey = createPublicKey(privateKey).export({ type: "spki", format: "pem" });
2561
- return verify.verify(publicKey, token, "base64");
2562
- } catch {
2563
- return false;
2564
- }
2565
- }
2566
-
2567
- // src/server/ws/server.ts
2568
- function moduleGitBranch(cwd) {
2569
- return new Promise((resolve3) => {
2570
- const proc = spawn3("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
2571
- cwd,
2572
- stdio: ["ignore", "pipe", "ignore"]
2573
- });
2574
- let stdout = "";
2575
- proc.stdout.on("data", (data) => {
2576
- stdout += data.toString();
2577
- });
2578
- proc.on("close", (code) => {
2579
- if (code === 0 && stdout.trim()) {
2580
- resolve3(stdout.trim());
2581
- } else {
2582
- resolve3(null);
2583
- }
2584
- });
2585
- proc.on("error", () => resolve3(null));
2586
- });
2587
- }
2588
- function hashContent(content) {
2589
- return createHash2("sha256").update(content).digest("hex");
2590
- }
2591
- function moduleGitDiff(cwd) {
2592
- return new Promise((resolve3) => {
2593
- const diffProc = spawn3("git", ["diff", "--name-status", "HEAD"], {
2594
- cwd,
2595
- stdio: ["ignore", "pipe", "pipe"]
2596
- });
2597
- const statusProc = spawn3("git", ["status", "--porcelain"], {
2598
- cwd,
2599
- stdio: ["ignore", "pipe", "pipe"]
2600
- });
2601
- let diffStdout = "";
2602
- let statusStdout = "";
2603
- let diffExited = false;
2604
- let statusExited = false;
2605
- let diffCode = null;
2606
- let statusCode = null;
2607
- const processResults = () => {
2608
- if (!diffExited || !statusExited) return;
2609
- const raw = diffStdout + statusStdout;
2610
- const hash = raw ? hashContent(raw) : "";
2611
- const files = [];
2612
- if (diffCode === 0) {
2613
- for (const line of diffStdout.split("\n")) {
2614
- if (!line.trim()) continue;
2615
- const [statusChar, ...pathParts] = line.split(" ");
2616
- const path = pathParts.join(" ") || statusChar || "";
2617
- if (!path) continue;
2618
- const status = statusChar === "A" ? "added" : statusChar === "D" ? "deleted" : "modified";
2619
- files.push({ path, status, additions: 0, deletions: 0 });
2620
- }
1429
+ for (let i = 0; i < response.toolCalls.length; i++) {
1430
+ const tc = response.toolCalls[i];
1431
+ yield { type: "tool_call_delta", index: i, id: tc.id, name: tc.name, arguments: JSON.stringify(tc.arguments) };
2621
1432
  }
2622
- if (statusCode === 0) {
2623
- for (const line of statusStdout.split("\n")) {
2624
- if (!line.startsWith("?? ")) continue;
2625
- const path = line.slice(3).trim();
2626
- if (!path) continue;
2627
- files.push({ path, status: "added", additions: 0, deletions: 0 });
1433
+ const words = response.content.split(" ");
1434
+ const slowStreaming = isSlowStreamingPrompt(prompt);
1435
+ for (const word of words) {
1436
+ yield { type: "text_delta", content: word + " " };
1437
+ if (slowStreaming) {
1438
+ await sleep(20);
2628
1439
  }
2629
1440
  }
2630
- resolve3({ hash, files });
2631
- };
2632
- diffProc.stdout.on("data", (data) => {
2633
- diffStdout += data.toString();
2634
- });
2635
- statusProc.stdout.on("data", (data) => {
2636
- statusStdout += data.toString();
2637
- });
2638
- diffProc.on("close", (code) => {
2639
- diffExited = true;
2640
- diffCode = code;
2641
- processResults();
2642
- });
2643
- statusProc.on("close", (code) => {
2644
- statusExited = true;
2645
- statusCode = code;
2646
- processResults();
2647
- });
2648
- diffProc.on("error", () => {
2649
- diffExited = true;
2650
- diffCode = 1;
2651
- processResults();
2652
- });
2653
- statusProc.on("error", () => {
2654
- statusExited = true;
2655
- statusCode = 1;
2656
- processResults();
2657
- });
2658
- });
2659
- }
2660
- var moduleWorkdirLastHash = /* @__PURE__ */ new Map();
2661
- var moduleWorkdirInterval = /* @__PURE__ */ new Map();
2662
- var gitPollInterval = parseInt(process.env["OPENFOX_GIT_POLL_INTERVAL"] ?? "", 10) || 1e4;
2663
- var moduleClients = null;
2664
- var moduleEnqueueSend = null;
2665
- function moduleGitPoll(workdir) {
2666
- ;
2667
- (async () => {
2668
- try {
2669
- const branch = await moduleGitBranch(workdir);
2670
- const { hash, files } = await moduleGitDiff(workdir);
2671
- const lastHash = moduleWorkdirLastHash.get(workdir);
2672
- if (hash !== lastHash) {
2673
- moduleWorkdirLastHash.set(workdir, hash);
2674
- const msg = createGitStatusMessage(branch, files);
2675
- const activeClients = moduleClients;
2676
- const sendFn = moduleEnqueueSend;
2677
- if (!activeClients || !sendFn) return;
2678
- for (const [ws, client] of activeClients) {
2679
- if (client.activeWorkdir === workdir && ws.readyState === WebSocket2.OPEN) {
2680
- const seq = client.lastSentSeq + 1;
2681
- sendFn(client, serializeServerMessage({ ...msg, sessionId: client.activeSessionId ?? "" }), seq);
2682
- }
1441
+ yield {
1442
+ type: "done",
1443
+ response: {
1444
+ id: `mock-${Date.now()}`,
1445
+ content: response.content,
1446
+ thinkingContent: "",
1447
+ toolCalls: response.toolCalls,
1448
+ finishReason: response.finishReason,
1449
+ usage: { promptTokens: 100, completionTokens: 50, totalTokens: 150 }
2683
1450
  }
2684
- }
2685
- } catch {
1451
+ };
2686
1452
  }
2687
- })();
2688
- }
2689
- function moduleStartGitPolling(workdir) {
2690
- if (moduleWorkdirInterval.has(workdir)) return;
2691
- const interval = setInterval(() => moduleGitPoll(workdir), gitPollInterval);
2692
- moduleWorkdirInterval.set(workdir, interval);
2693
- }
2694
- function moduleStopGitPolling(workdir) {
2695
- const interval = moduleWorkdirInterval.get(workdir);
2696
- if (interval !== void 0) {
2697
- clearInterval(interval);
2698
- moduleWorkdirInterval.delete(workdir);
2699
- moduleWorkdirLastHash.delete(workdir);
2700
- }
2701
- }
2702
- function resolveStatsIdentity(llmClient, getActiveProvider) {
2703
- const provider = getActiveProvider?.();
2704
- const model = llmClient.getModel();
2705
- const backend = provider?.backend ?? (llmClient.getBackend() === "unknown" ? "unknown" : llmClient.getBackend());
2706
- return {
2707
- providerId: provider?.id ?? `provider:${model}`,
2708
- providerName: provider?.name ?? "Unknown Provider",
2709
- backend,
2710
- model
2711
1453
  };
2712
1454
  }
2713
- function addUserMessageAndBroadcast(sessionManager, sessionId, message, broadcastFn) {
2714
- const userMessage = sessionManager.addMessage(sessionId, {
2715
- role: "user",
2716
- content: message.content,
2717
- ...message.attachments ? { attachments: message.attachments } : {},
2718
- ...message.messageKind ? { messageKind: message.messageKind } : {}
2719
- });
2720
- broadcastFn(sessionId, createChatMessageMessage(userMessage));
2721
- return userMessage;
1455
+
1456
+ // src/server/mcp/manager.ts
1457
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
1458
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
1459
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
1460
+ function estimateToolTokens(toolName, description, inputSchema) {
1461
+ const def = {
1462
+ type: "function",
1463
+ function: { name: toolName, description: description ?? "", parameters: inputSchema }
1464
+ };
1465
+ return Math.ceil(JSON.stringify(def).length / 4);
2722
1466
  }
2723
- function processQueueAndRestartTurn(sessionManager, sessionId, drainFn, queueMode, broadcastFn, activeAgents2, startTurnFn, queueMessageFn) {
2724
- const messages = drainFn(sessionId);
2725
- const next = messages[0];
2726
- if (!next) return false;
2727
- for (const remaining of messages.slice(1)) {
2728
- if (queueMessageFn) {
2729
- queueMessageFn(
2730
- sessionId,
2731
- queueMode,
2732
- remaining.content,
2733
- remaining.attachments,
2734
- remaining.messageKind
2735
- );
2736
- } else {
2737
- sessionManager.queueMessage(sessionId, queueMode, remaining.content, remaining.attachments);
2738
- }
1467
+ var McpManager = class {
1468
+ servers = /* @__PURE__ */ new Map();
1469
+ onServersChanged;
1470
+ constructor(options) {
1471
+ this.onServersChanged = options?.onServersChanged;
2739
1472
  }
2740
- broadcastFn(sessionId, createQueueStateMessage(sessionManager.getQueueState(sessionId)));
2741
- addUserMessageAndBroadcast(
2742
- sessionManager,
2743
- sessionId,
2744
- {
2745
- content: next.content,
2746
- ...next.attachments ? { attachments: next.attachments } : {},
2747
- ...next.messageKind ? { messageKind: next.messageKind } : {}
2748
- },
2749
- broadcastFn
2750
- );
2751
- const newController = new AbortController();
2752
- activeAgents2.set(sessionId, newController);
2753
- startTurnFn(sessionId, newController);
2754
- return true;
2755
- }
2756
- var activeAgents = /* @__PURE__ */ new Map();
2757
- var MAX_SEND_QUEUE_SIZE = 1e3;
2758
- function createWebSocketServer(httpServer, config4, getLLMClient, getActiveProvider, sessionManager, providerManager) {
2759
- const wss = new WebSocketServer({ server: httpServer });
2760
- const clients = /* @__PURE__ */ new Map();
2761
- moduleClients = clients;
2762
- const sessionLLMClients = /* @__PURE__ */ new Map();
2763
- function getSessionLLMClient(sessionId) {
2764
- const session = sessionManager.getSession(sessionId);
2765
- if (!session?.providerId || !session?.providerModel || !providerManager) {
2766
- return getLLMClient();
2767
- }
2768
- const cacheKey = `${session.providerId}:${session.providerModel}`;
2769
- const cached = sessionLLMClients.get(sessionId);
2770
- if (cached && cached.key === cacheKey) {
2771
- return cached.client;
2772
- }
2773
- const provider = providerManager.getProviders().find((p) => p.id === session.providerId);
2774
- if (!provider) {
2775
- logger.warn("Session references deleted provider, falling back to global", {
2776
- sessionId,
2777
- providerId: session.providerId
2778
- });
2779
- sessionManager.setSessionProvider(sessionId, null, null);
2780
- sessionLLMClients.delete(sessionId);
2781
- return getLLMClient();
2782
- }
2783
- const modelConfig = provider.models.find((m) => m.id === session.providerModel);
2784
- const reasoningEffort = modelConfig?.thinkingEnabled && modelConfig?.thinkingLevel ? modelConfig.thinkingLevel : void 0;
2785
- const baseUrl = ensureVersionPrefix(provider.url);
2786
- const sessionConfig = {
2787
- ...config4,
2788
- llm: {
2789
- ...config4.llm,
2790
- baseUrl,
2791
- model: session.providerModel,
2792
- ...provider.apiKey && { apiKey: provider.apiKey },
2793
- ...provider.thinkingField && { thinkingField: provider.thinkingField },
2794
- ...reasoningEffort && { reasoningEffort }
2795
- }
2796
- };
2797
- const client = createLLMClient(sessionConfig);
2798
- if (provider.backend !== "unknown") {
2799
- client.setBackend(provider.backend);
1473
+ async addServer(name, config4) {
1474
+ if (this.servers.has(name)) {
1475
+ throw new Error(`MCP server '${name}' already exists`);
2800
1476
  }
2801
- client.setModel(session.providerModel);
2802
- sessionLLMClients.set(sessionId, { key: cacheKey, client });
2803
- return client;
1477
+ const state = { name, config: config4, status: "disconnected", tools: [], estimatedTokens: 0 };
1478
+ this.servers.set(name, { config: config4, client: void 0, transport: null, state });
1479
+ await this.connectServer(name);
2804
1480
  }
2805
- function getSessionStatsIdentity(sessionId) {
2806
- const session = sessionManager.getSession(sessionId);
2807
- if (!session?.providerId || !providerManager) {
2808
- return resolveStatsIdentity(getLLMClient(), getActiveProvider);
1481
+ removeServer(name) {
1482
+ const entry = this.servers.get(name);
1483
+ if (entry) {
1484
+ this.disconnectServer(name);
1485
+ this.servers.delete(name);
2809
1486
  }
2810
- const provider = providerManager.getProviders().find((p) => p.id === session.providerId);
2811
- const client = getSessionLLMClient(sessionId);
2812
- return {
2813
- providerId: provider?.id ?? session.providerId,
2814
- providerName: provider?.name ?? "Unknown Provider",
2815
- backend: provider?.backend ?? client.getBackend(),
2816
- model: client.getModel()
2817
- };
2818
1487
  }
2819
- const isSubscribedToSession = (client, sessionId) => {
2820
- return client.activeSessionId === sessionId;
2821
- };
2822
- const broadcastForSession = (sessionId, msg) => {
2823
- for (const [clientWs, client] of clients) {
2824
- if (isSubscribedToSession(client, sessionId) && clientWs.readyState === WebSocket2.OPEN) {
2825
- clientWs.send(serializeServerMessage({ ...msg, sessionId }));
1488
+ async connectServer(name) {
1489
+ const entry = this.servers.get(name);
1490
+ if (!entry) return;
1491
+ try {
1492
+ await this.disconnectServer(name);
1493
+ const client = new Client({ name: "openfox-mcp", version: "2.0.0" });
1494
+ let transport = null;
1495
+ if (entry.config.transport === "stdio") {
1496
+ if (!entry.config.command) throw new Error("command is required for stdio transport");
1497
+ transport = new StdioClientTransport({
1498
+ command: entry.config.command,
1499
+ ...entry.config.args ? { args: entry.config.args } : {},
1500
+ ...entry.config.env ? { env: entry.config.env } : {},
1501
+ stderr: "pipe"
1502
+ });
1503
+ } else if (entry.config.transport === "http") {
1504
+ if (!entry.config.url) throw new Error("url is required for http transport");
1505
+ const httpOpts = {};
1506
+ if (entry.config.headers) {
1507
+ httpOpts.requestInit = { headers: entry.config.headers };
1508
+ }
1509
+ transport = new StreamableHTTPClientTransport(new URL(entry.config.url), httpOpts);
1510
+ } else {
1511
+ throw new Error(`Unsupported transport: ${entry.config.transport}`);
2826
1512
  }
2827
- }
2828
- };
2829
- function enqueueSend(client, data, seq) {
2830
- if (client.sendQueue.length >= MAX_SEND_QUEUE_SIZE) {
2831
- logger.warn("WebSocket send queue full, dropping message", {
2832
- queueSize: client.sendQueue.length,
2833
- sessionId: client.activeSessionId
1513
+ if (!transport) throw new Error("Failed to create transport");
1514
+ await client.connect(transport);
1515
+ const { tools: mcpTools } = await client.listTools();
1516
+ const disabledSet = new Set(entry.config.disabledTools ?? []);
1517
+ const tools = mcpTools.map((t) => {
1518
+ const inputSchema = t.inputSchema;
1519
+ return {
1520
+ name: t.name,
1521
+ description: t.description ?? "",
1522
+ inputSchema,
1523
+ enabled: !disabledSet.has(t.name),
1524
+ estimatedTokens: estimateToolTokens(t.name, t.description, inputSchema)
1525
+ };
2834
1526
  });
2835
- return;
1527
+ const totalTokens = tools.filter((t) => t.enabled).reduce((sum, t) => sum + t.estimatedTokens, 0);
1528
+ entry.client = client;
1529
+ entry.transport = transport;
1530
+ entry.state = { name, config: entry.config, status: "connected", tools, estimatedTokens: totalTokens };
1531
+ logger.info("Connected to MCP server", { name, toolCount: tools.length });
1532
+ this.onServersChanged?.();
1533
+ } catch (error) {
1534
+ const msg = error instanceof Error ? error.message : String(error);
1535
+ logger.error("Failed to connect MCP server", { name, error: msg });
1536
+ entry.state = { name, config: entry.config, status: "error", tools: [], estimatedTokens: 0, error: msg };
1537
+ this.onServersChanged?.();
2836
1538
  }
2837
- client.sendQueue.push({ data, seq });
2838
- processSendQueue(client);
2839
1539
  }
2840
- moduleEnqueueSend = enqueueSend;
2841
- function processSendQueue(client) {
2842
- if (client.isSending || client.sendQueue.length === 0) {
2843
- return;
2844
- }
2845
- client.isSending = true;
2846
- const item = client.sendQueue.shift();
2847
- if (client.ws.readyState === WebSocket2.OPEN) {
2848
- client.ws.send(item.data, (err) => {
2849
- if (err) {
2850
- logger.debug("WebSocket send error", { error: err });
2851
- }
2852
- client.isSending = false;
2853
- client.lastSentSeq = item.seq;
2854
- processSendQueue(client);
2855
- });
2856
- } else {
2857
- client.isSending = false;
2858
- processSendQueue(client);
1540
+ async disconnectServer(name) {
1541
+ const entry = this.servers.get(name);
1542
+ if (!entry) return;
1543
+ try {
1544
+ await entry.client?.close();
1545
+ } catch {
2859
1546
  }
1547
+ entry.client = void 0;
1548
+ entry.transport = null;
1549
+ entry.state = { name, config: entry.config, status: "disconnected", tools: [], estimatedTokens: 0 };
2860
1550
  }
2861
- const llmForSession = (sessionId) => getSessionLLMClient?.(sessionId) ?? getLLMClient();
2862
- const statsForSession = (sessionId) => getSessionStatsIdentity?.(sessionId) ?? resolveStatsIdentity(getLLMClient(), getActiveProvider);
2863
- function cleanupAfterTurn(sessionId, controller, sendFn, setRunningOnEarlyReturn) {
2864
- if (activeAgents.get(sessionId) !== controller) {
2865
- return;
2866
- }
2867
- activeAgents.delete(sessionId);
2868
- const processed = processQueueAndRestartTurn(
2869
- sessionManager,
2870
- sessionId,
2871
- (sid) => sessionManager.drainAsapMessages(sid),
2872
- "asap",
2873
- sendFn,
2874
- activeAgents,
2875
- startTurnWithCompletionChain,
2876
- (sid, mode, content, attachments, messageKind) => sessionManager.queueMessage(sid, mode, content, attachments, messageKind)
2877
- );
2878
- if (processed) {
2879
- if (setRunningOnEarlyReturn) sessionManager.setRunning(sessionId, false);
2880
- return;
2881
- }
2882
- const processedCompletion = processQueueAndRestartTurn(
2883
- sessionManager,
2884
- sessionId,
2885
- (sid) => sessionManager.drainCompletionMessages(sid),
2886
- "completion",
2887
- sendFn,
2888
- activeAgents,
2889
- startTurnWithCompletionChain
2890
- );
2891
- if (processedCompletion) {
2892
- if (setRunningOnEarlyReturn) sessionManager.setRunning(sessionId, false);
2893
- return;
1551
+ async disconnectAll() {
1552
+ for (const name of this.servers.keys()) {
1553
+ await this.disconnectServer(name);
2894
1554
  }
2895
- sessionManager.clearMessageQueue(sessionId);
2896
- const contextState = sessionManager.getContextState(sessionId);
2897
- sendFn(sessionId, createContextStateMessage(contextState));
2898
1555
  }
2899
- function startTurnWithCompletionChain(sessionId, controller) {
2900
- runChatTurn({
2901
- sessionManager,
2902
- sessionId,
2903
- llmClient: llmForSession(sessionId),
2904
- statsIdentity: statsForSession(sessionId),
2905
- signal: controller.signal,
2906
- onMessage: (msg) => broadcastForSession(sessionId, msg)
2907
- }).catch((error) => {
2908
- if (error instanceof Error && error.message === "Aborted") {
2909
- return;
2910
- }
2911
- logger.error("Chat turn error", { error });
2912
- }).finally(() => {
2913
- cleanupAfterTurn(sessionId, controller, broadcastForSession, false);
2914
- });
1556
+ async reconnectServer(name) {
1557
+ await this.connectServer(name);
2915
1558
  }
2916
- const broadcastAll = (msg) => {
2917
- const serialized = serializeServerMessage(msg);
2918
- for (const [clientWs, client] of clients) {
2919
- if (clientWs.readyState === WebSocket2.OPEN) {
2920
- const seq = client.lastSentSeq + 1;
2921
- enqueueSend(client, serialized, seq);
2922
- }
2923
- }
2924
- };
2925
- devServerManager.onOutput((workdir, chunk) => {
2926
- broadcastAll(
2927
- createServerMessage("devServer.output", {
2928
- workdir,
2929
- stream: chunk.stream,
2930
- content: chunk.content
2931
- })
2932
- );
2933
- });
2934
- devServerManager.onStateChange((workdir, state, errorMessage) => {
2935
- broadcastAll(
2936
- createServerMessage("devServer.state", {
2937
- workdir,
2938
- state,
2939
- errorMessage
2940
- })
2941
- );
2942
- });
2943
- onProcessEvent((_processId, msg) => {
2944
- const sessionId = msg.sessionId;
2945
- if (!sessionId) return;
2946
- for (const [clientWs, client] of clients) {
2947
- if (client.activeSessionId === sessionId) {
2948
- if (clientWs.readyState === WebSocket2.OPEN) {
2949
- const seq = client.lastSentSeq + 1;
2950
- enqueueSend(client, serializeServerMessage(msg), seq);
2951
- }
2952
- }
2953
- }
2954
- });
2955
- wss.on("connection", async (ws, req) => {
2956
- const url = new URL(req.url || "/", `http://${req.headers.host}`);
2957
- const token = url.searchParams.get("token");
2958
- const authConfig = getAuthConfig();
2959
- if (authConfig?.strategy === "network" && authConfig.encryptedPassword) {
2960
- if (!token || !await isValidToken(token)) {
2961
- setTimeout(() => {
2962
- ws.close(4e3, "Unauthorized");
2963
- }, 100);
2964
- return;
2965
- }
2966
- }
2967
- logger.debug("WebSocket client connected");
2968
- clients.set(ws, {
2969
- ws,
2970
- activeSessionId: null,
2971
- activeWorkdir: null,
2972
- globalSubscription: null,
2973
- sendQueue: [],
2974
- isSending: false,
2975
- lastSentSeq: 0
2976
- });
2977
- const eventStore = getEventStore();
2978
- const { iterator: globalIterator, unsubscribe: globalUnsubscribe } = eventStore.subscribeAll();
2979
- clients.get(ws).globalSubscription = globalUnsubscribe;
2980
- (async () => {
2981
- try {
2982
- for await (const storedEvent of globalIterator) {
2983
- if (ws.readyState !== WebSocket2.OPEN) break;
2984
- const serverMsg = storedEventToServerMessage(storedEvent);
2985
- if (serverMsg) {
2986
- const client = clients.get(ws);
2987
- enqueueSend(
2988
- client,
2989
- serializeServerMessage({ ...serverMsg, seq: storedEvent.seq, sessionId: storedEvent.sessionId }),
2990
- storedEvent.seq
2991
- );
2992
- }
2993
- }
2994
- } catch (error) {
2995
- logger.debug("Global event subscription ended", { error });
2996
- }
2997
- })();
2998
- ws.on("message", async (data) => {
2999
- const message = parseClientMessage(data.toString());
3000
- if (!message) {
3001
- const client2 = clients.get(ws);
3002
- const seq = client2.lastSentSeq + 1;
3003
- enqueueSend(
3004
- client2,
3005
- serializeServerMessage(createErrorMessage("INVALID_MESSAGE", "Invalid message format")),
3006
- seq
3007
- );
3008
- return;
3009
- }
3010
- if (message.type.startsWith("terminal.")) {
3011
- handleTerminalMessage(ws, message);
3012
- return;
3013
- }
3014
- const client = clients.get(ws);
3015
- try {
3016
- await handleClientMessage(
3017
- ws,
3018
- client,
3019
- message,
3020
- getLLMClient,
3021
- getActiveProvider,
3022
- sessionManager,
3023
- broadcastForSession,
3024
- providerManager,
3025
- getSessionLLMClient,
3026
- getSessionStatsIdentity,
3027
- llmForSession,
3028
- statsForSession,
3029
- startTurnWithCompletionChain,
3030
- cleanupAfterTurn,
3031
- enqueueSend
3032
- );
3033
- } catch (error) {
3034
- logger.error("Error handling client message", { error, type: message.type });
3035
- const client2 = clients.get(ws);
3036
- const seq = client2.lastSentSeq + 1;
3037
- enqueueSend(
3038
- client2,
3039
- serializeServerMessage(
3040
- createErrorMessage("INTERNAL_ERROR", error instanceof Error ? error.message : "Unknown error", message.id)
3041
- ),
3042
- seq
3043
- );
3044
- }
3045
- });
3046
- ws.on("close", () => {
3047
- logger.debug("WebSocket client disconnected");
3048
- const client = clients.get(ws);
3049
- const disconnectedWorkdir = client?.activeWorkdir ?? null;
3050
- if (client?.globalSubscription) {
3051
- client.globalSubscription();
3052
- }
3053
- unsubscribeAllFromTerminal(ws);
3054
- clients.delete(ws);
3055
- if (disconnectedWorkdir) {
3056
- const hasRemaining = [...clients.values()].some((c) => c.activeWorkdir === disconnectedWorkdir);
3057
- if (!hasRemaining) {
3058
- moduleStopGitPolling(disconnectedWorkdir);
3059
- }
3060
- }
3061
- });
3062
- ws.on("error", (error) => {
3063
- logger.error("WebSocket error", { error });
3064
- });
3065
- });
3066
- return {
3067
- wss,
3068
- abortSession: (sessionId) => {
3069
- const controller = activeAgents.get(sessionId);
3070
- if (controller) {
3071
- activeAgents.delete(sessionId);
3072
- controller.abort();
3073
- return true;
3074
- }
3075
- return false;
3076
- },
3077
- close: (cb) => wss.close(cb),
3078
- broadcastForSession
3079
- };
3080
- }
3081
- async function handleClientMessage(ws, client, message, _getLLMClient, _getActiveProvider, sessionManager, _broadcastForSession, _providerManager, _getSessionLLMClient, _getSessionStatsIdentity, llmForSession, statsForSession, _startTurnWithCompletionChain, cleanupAfterTurn, enqueueSendFn) {
3082
- const send = (msg) => {
3083
- if (ws.readyState === WebSocket2.OPEN) {
3084
- const seq = client.lastSentSeq + 1;
3085
- enqueueSendFn(client, serializeServerMessage(msg), seq);
3086
- }
3087
- };
3088
- const sendForSession = (sessionId, msg) => {
3089
- send({ ...msg, sessionId });
3090
- };
3091
- const ensureEventStoreSubscription = (_sessionId) => {
3092
- };
3093
- switch (message.type) {
3094
- // =========================================================================
3095
- // DEPRECATED: All CRUD operations moved to REST API
3096
- // If you see this error, update your code to use REST endpoints instead.
3097
- // See docs/REST-API.md for details.
3098
- // =========================================================================
3099
- case "project.create":
3100
- case "project.create-with-dir":
3101
- case "project.list":
3102
- case "project.load":
3103
- case "project.update":
3104
- case "project.delete":
3105
- case "settings.get":
3106
- case "settings.set":
3107
- case "session.create":
3108
- case "session.list":
3109
- case "session.delete":
3110
- case "session.deleteAll":
3111
- case "session.setProvider":
3112
- send(
3113
- createErrorMessage(
3114
- "DEPRECATED_MESSAGE_TYPE",
3115
- `${message.type} removed. Use REST API instead. See docs/REST-API.md`,
3116
- message.id
3117
- )
3118
- );
3119
- return;
3120
- // =========================================================================
3121
- // Session Load - Sets active session for event routing
3122
- // Note: Data loading is done via REST API: GET /api/sessions/:id
3123
- // This WebSocket message is ONLY to tell the server which session is active
3124
- // so it can route real-time events correctly.
3125
- // =========================================================================
3126
- case "session.load": {
3127
- if (!isSessionLoadPayload(message.payload)) {
3128
- send(createErrorMessage("INVALID_PAYLOAD", "Invalid session.load payload", message.id));
3129
- return;
3130
- }
3131
- const session = sessionManager.getSession(message.payload.sessionId);
3132
- if (!session) {
3133
- send(createErrorMessage("NOT_FOUND", "Session not found", message.id));
3134
- return;
3135
- }
3136
- client.activeSessionId = session.id;
3137
- client.activeWorkdir = session.workdir;
3138
- if (session.workdir) {
3139
- const branch = await moduleGitBranch(session.workdir);
3140
- const { files } = await moduleGitDiff(session.workdir);
3141
- const msg = createGitStatusMessage(branch, files);
3142
- send(msg);
3143
- if (branch) moduleStartGitPolling(session.workdir);
3144
- }
3145
- ensureEventStoreSubscription(session.id);
3146
- send({ type: "ack", payload: { sessionId: session.id }, id: message.id });
3147
- const sendContextState = () => {
3148
- const contextState = sessionManager.getContextState(session.id);
3149
- send(createContextStateMessage(contextState));
3150
- };
3151
- sendContextState();
3152
- const cachedHash = sessionManager.getCachedPrompt(session.id)?.hash;
3153
- if (cachedHash) {
3154
- ;
3155
- (async () => {
3156
- try {
3157
- const { content: instructionContent } = await getAllInstructions(session.workdir, session.projectId);
3158
- const runtimeConfig = getRuntimeConfig();
3159
- const configDir = getGlobalConfigDir(runtimeConfig.mode ?? "production");
3160
- const skills = await getEnabledSkillMetadata(configDir, runtimeConfig.workdir);
3161
- const dynamicInputs = JSON.stringify({
3162
- instructions: instructionContent,
3163
- skills: skills.map((s) => s.id).sort()
3164
- });
3165
- const currentHash = createHash2("sha256").update(dynamicInputs).digest("hex");
3166
- if (currentHash !== cachedHash) {
3167
- sessionManager.setDynamicContextChanged(session.id, true);
3168
- sendContextState();
3169
- }
3170
- } catch {
3171
- }
3172
- })();
3173
- }
3174
- break;
3175
- }
3176
- // =========================================================================
3177
- // Context Management
3178
- // =========================================================================
3179
- case "context.compact": {
3180
- if (!client.activeSessionId) {
3181
- send(createErrorMessage("NO_SESSION", "No active session", message.id));
3182
- return;
3183
- }
3184
- const session = sessionManager.requireSession(client.activeSessionId);
3185
- const sessionId = client.activeSessionId;
3186
- if (session.isRunning) {
3187
- send(createErrorMessage("SESSION_RUNNING", "Cannot compact while session is running", message.id));
3188
- return;
3189
- }
3190
- send({ type: "ack", payload: {}, id: message.id });
3191
- appendCompactionPrompt(sessionId, (event) => getEventStore().append(sessionId, event));
3192
- runChatTurn({
3193
- sessionManager,
3194
- sessionId,
3195
- llmClient: llmForSession(sessionId),
3196
- statsIdentity: statsForSession(sessionId),
3197
- onMessage: (msg) => _broadcastForSession(sessionId, msg),
3198
- initialCompacting: true
3199
- }).then(() => {
3200
- const newContextState = sessionManager.getContextState(sessionId);
3201
- sendForSession(sessionId, createContextStateMessage(newContextState));
3202
- }).catch((error) => {
3203
- if (error instanceof Error && error.message === "Aborted") return;
3204
- logger.error("Compaction failed", { error, sessionId });
3205
- sendForSession(
3206
- sessionId,
3207
- createChatErrorMessage(
3208
- `Compaction failed: ${error instanceof Error ? error.message : "Unknown error"}`,
3209
- true
3210
- )
3211
- );
3212
- }).finally(() => {
3213
- sessionManager.setRunning(sessionId, false);
3214
- sendForSession(sessionId, createSessionRunningMessage(false));
3215
- });
3216
- break;
3217
- }
3218
- case "context.checkDynamic": {
3219
- if (!client.activeSessionId) {
3220
- send(createErrorMessage("NO_SESSION", "No active session", message.id));
3221
- return;
3222
- }
3223
- const sessionId = client.activeSessionId;
3224
- const session = sessionManager.requireSession(sessionId);
3225
- (async () => {
3226
- try {
3227
- const { content: instructionContent } = await getAllInstructions(session.workdir, session.projectId);
3228
- const runtimeConfig = getRuntimeConfig();
3229
- const configDir = getGlobalConfigDir(runtimeConfig.mode ?? "production");
3230
- const skills = await getEnabledSkillMetadata(configDir, runtimeConfig.workdir);
3231
- const currentHash = computeDynamicContextHash(instructionContent, skills);
3232
- const cachedHash = sessionManager.getCachedPrompt(sessionId)?.hash;
3233
- if (cachedHash) {
3234
- if (currentHash !== cachedHash) {
3235
- if (!sessionManager.getDynamicContextChanged(sessionId)) {
3236
- sessionManager.setDynamicContextChanged(sessionId, true);
3237
- const newContextState = sessionManager.getContextState(sessionId);
3238
- sendForSession(sessionId, createContextStateMessage(newContextState));
3239
- }
3240
- } else if (sessionManager.getDynamicContextChanged(sessionId)) {
3241
- sessionManager.setDynamicContextChanged(sessionId, false);
3242
- const newContextState = sessionManager.getContextState(sessionId);
3243
- sendForSession(sessionId, createContextStateMessage(newContextState));
3244
- }
1559
+ getServer(name) {
1560
+ return this.servers.get(name)?.state;
1561
+ }
1562
+ getAllServers() {
1563
+ return Array.from(this.servers.values()).map((e) => e.state);
1564
+ }
1565
+ getToolDefinitions() {
1566
+ const defs = [];
1567
+ for (const [, entry] of this.servers) {
1568
+ for (const tool of entry.state.tools) {
1569
+ if (!tool.enabled) continue;
1570
+ defs.push({
1571
+ type: "function",
1572
+ function: {
1573
+ name: `${entry.state.name}_${tool.name}`,
1574
+ description: tool.description ?? "",
1575
+ parameters: tool.inputSchema
3245
1576
  }
3246
- send({ type: "ack", payload: {}, id: message.id });
3247
- } catch (error) {
3248
- logger.error("Failed to check dynamic context", { error, sessionId });
3249
- send({ type: "ack", payload: {}, id: message.id });
3250
- }
3251
- })();
3252
- break;
3253
- }
3254
- case "context.applyDynamic": {
3255
- if (!client.activeSessionId) {
3256
- send(createErrorMessage("NO_SESSION", "No active session", message.id));
3257
- return;
3258
- }
3259
- const sessionId = client.activeSessionId;
3260
- const session = sessionManager.requireSession(sessionId);
3261
- if (session.isRunning) {
3262
- send(createErrorMessage("SESSION_RUNNING", "Cannot apply dynamic context while session is running", message.id));
3263
- return;
3264
- }
3265
- ;
3266
- (async () => {
3267
- try {
3268
- const { content: instructionContent } = await getAllInstructions(session.workdir, session.projectId);
3269
- const runtimeConfig = getRuntimeConfig();
3270
- const configDir = getGlobalConfigDir(runtimeConfig.mode ?? "production");
3271
- const skills = await getEnabledSkillMetadata(configDir, runtimeConfig.workdir);
3272
- const dynamicHash = computeDynamicContextHash(instructionContent, skills);
3273
- const allAgents = await loadAllAgentsDefault();
3274
- const subAgentDefs = getSubAgents(allAgents);
3275
- const systemPrompt = buildTopLevelSystemPrompt(
3276
- session.workdir,
3277
- instructionContent || void 0,
3278
- skills,
3279
- subAgentDefs
3280
- );
3281
- sessionManager.setCachedPrompt(sessionId, systemPrompt, dynamicHash);
3282
- sessionManager.setDynamicContextChanged(sessionId, false);
3283
- const newContextState = sessionManager.getContextState(sessionId);
3284
- sendForSession(sessionId, createContextStateMessage(newContextState));
3285
- send({ type: "ack", payload: {}, id: message.id });
3286
- } catch (error) {
3287
- logger.error("Failed to apply dynamic context", { error, sessionId });
3288
- sendForSession(
3289
- sessionId,
3290
- createChatErrorMessage(
3291
- `Failed to apply dynamic context: ${error instanceof Error ? error.message : "Unknown error"}`,
3292
- true
3293
- )
3294
- );
3295
- send({ type: "ack", payload: {}, id: message.id });
3296
- }
3297
- })();
3298
- break;
3299
- }
3300
- // =========================================================================
3301
- // Runner (Auto-Loop)
3302
- // =========================================================================
3303
- case "runner.launch": {
3304
- if (!client.activeSessionId) {
3305
- send(createErrorMessage("NO_SESSION", "No active session", message.id));
3306
- return;
3307
- }
3308
- const session = sessionManager.requireSession(client.activeSessionId);
3309
- if (session.isRunning) {
3310
- const launchPayload2 = message.payload;
3311
- const content = launchPayload2?.content ?? "";
3312
- const attachments = launchPayload2?.attachments;
3313
- const workflowId = launchPayload2?.workflowId;
3314
- let fullContent = content;
3315
- if (workflowId) {
3316
- const workflowInfo = `// Workflow: ${workflowId}`;
3317
- fullContent = content ? `${workflowInfo}
3318
-
3319
- ${content}` : workflowInfo;
3320
- }
3321
- sessionManager.queueMessage(client.activeSessionId, "asap", fullContent, attachments, "workflow-launch");
3322
- const queueState = sessionManager.getQueueState(client.activeSessionId);
3323
- send({
3324
- type: "queue.state",
3325
- payload: { success: true, queueState },
3326
- id: message.id
3327
1577
  });
3328
- return;
3329
- }
3330
- const launchPayloadEarly = message.payload;
3331
- const pendingCriteria = session.criteria.filter((c) => c.status.type !== "passed");
3332
- if (!launchPayloadEarly?.workflowId && pendingCriteria.length === 0) {
3333
- send(createErrorMessage("NO_WORK", "No pending criteria to work on", message.id));
3334
- return;
3335
- }
3336
- const sessionId = client.activeSessionId;
3337
- if (session.phase === "blocked") {
3338
- logger.info("User launched runner - resetting blocked state", { sessionId });
3339
- sessionManager.setPhase(sessionId, "build");
3340
1578
  }
3341
- const launchPayload = message.payload;
3342
- const launchAttachments = launchPayload?.attachments;
3343
- const hasUserContent = launchPayload?.content && typeof launchPayload.content === "string" && launchPayload.content.trim();
3344
- const hasUserAttachments = launchAttachments && launchAttachments.length > 0;
3345
- const hasUserMessage = hasUserContent || hasUserAttachments;
3346
- sessionManager.setRunning(sessionId, true);
3347
- sendForSession(sessionId, createSessionRunningMessage(true));
3348
- const controller = new AbortController();
3349
- const existingController = activeAgents.get(sessionId);
3350
- if (existingController) {
3351
- logger.warn("Aborting existing agent before starting new one", { sessionId });
3352
- existingController.abort();
3353
- }
3354
- activeAgents.set(sessionId, controller);
3355
- send({ type: "ack", payload: {}, id: message.id });
3356
- ensureEventStoreSubscription(sessionId);
3357
- logger.info("Runner launching", { sessionId, pendingCriteria: pendingCriteria.length });
3358
- runOrchestrator({
3359
- sessionManager,
3360
- sessionId,
3361
- llmClient: llmForSession(sessionId),
3362
- statsIdentity: statsForSession(sessionId),
3363
- injectWorkflowKickoff: !hasUserMessage,
3364
- ...launchPayload?.workflowId ? { workflowId: launchPayload.workflowId } : {},
3365
- ...launchPayload?.subGroup ? { subGroup: launchPayload.subGroup } : {},
3366
- ...hasUserMessage ? {
3367
- userMessage: {
3368
- content: hasUserContent ? launchPayload.content : "",
3369
- ...hasUserAttachments ? { attachments: launchAttachments } : {}
3370
- }
3371
- } : {},
3372
- signal: controller.signal,
3373
- onMessage: (msg) => sendForSession(sessionId, msg)
3374
- // For path confirmation dialogs
3375
- }).catch((error) => {
3376
- if (error instanceof Error && error.message === "Aborted") {
3377
- return;
1579
+ }
1580
+ return defs;
1581
+ }
1582
+ async callTool(serverName, toolName, args) {
1583
+ const entry = this.servers.get(serverName);
1584
+ if (!entry) return { success: false, error: `MCP server '${serverName}' not found` };
1585
+ if (!entry.client) return { success: false, error: `MCP server '${serverName}' is not connected` };
1586
+ try {
1587
+ const result = await entry.client.callTool({ name: toolName, arguments: args });
1588
+ const content = result.content;
1589
+ const textParts = content.filter((c) => c.type === "text").map((c) => c.text);
1590
+ return { success: !result.isError, output: textParts.join("\n") };
1591
+ } catch (error) {
1592
+ return { success: false, error: error instanceof Error ? error.message : String(error) };
1593
+ }
1594
+ }
1595
+ async setToolEnabled(serverName, toolName, enabled) {
1596
+ const entry = this.servers.get(serverName);
1597
+ if (!entry) throw new Error(`MCP server '${serverName}' not found`);
1598
+ const tool = entry.state.tools.find((t) => t.name === toolName);
1599
+ if (!tool) throw new Error(`Tool '${toolName}' not found on server '${serverName}'`);
1600
+ tool.enabled = enabled;
1601
+ entry.state.estimatedTokens = entry.state.tools.filter((t) => t.enabled).reduce((sum, t) => sum + t.estimatedTokens, 0);
1602
+ this.onServersChanged?.();
1603
+ }
1604
+ getToolFingerprint() {
1605
+ const parts = [];
1606
+ for (const [, entry] of this.servers) {
1607
+ for (const tool of entry.state.tools) {
1608
+ if (tool.enabled) {
1609
+ parts.push(`${entry.state.name}:${tool.name}`);
3378
1610
  }
3379
- logger.error("Runner error", { error, sessionId });
3380
- }).finally(() => {
3381
- sessionManager.setRunning(sessionId, false);
3382
- sendForSession(sessionId, createSessionRunningMessage(false));
3383
- cleanupAfterTurn(sessionId, controller, sendForSession, true);
3384
- });
3385
- break;
3386
- }
3387
- // =========================================================================
3388
- // Path Confirmation
3389
- // =========================================================================
3390
- case "path.confirm": {
3391
- send(
3392
- createErrorMessage(
3393
- "DEPRECATED",
3394
- "path.confirm removed. Use REST API: POST /api/sessions/:id/confirm-path",
3395
- message.id
3396
- )
3397
- );
3398
- break;
3399
- }
3400
- // =========================================================================
3401
- // Ask User
3402
- // =========================================================================
3403
- case "ask.answer": {
3404
- if (!client.activeSessionId) {
3405
- send(createErrorMessage("NO_SESSION", "No active session", message.id));
3406
- return;
3407
- }
3408
- if (!isAskAnswerPayload(message.payload)) {
3409
- send(createErrorMessage("INVALID_PAYLOAD", "Invalid ask.answer payload", message.id));
3410
- return;
3411
1611
  }
3412
- const { callId, answer, skip } = message.payload;
3413
- const found = provideAnswer(callId, answer, skip);
3414
- if (!found) {
3415
- send(createErrorMessage("NOT_FOUND", "No pending question with that ID", message.id));
3416
- return;
3417
- }
3418
- logger.debug("Ask user answer received", {
3419
- sessionId: client.activeSessionId,
3420
- callId,
3421
- answerLength: answer.length
3422
- });
3423
- send({ type: "ack", payload: {}, id: message.id });
3424
- break;
3425
- }
3426
- default: {
3427
- send(createErrorMessage("UNKNOWN_MESSAGE", `Unknown message type: ${message.type}`, message.id));
3428
1612
  }
1613
+ return parts.sort().join(",");
3429
1614
  }
3430
- }
1615
+ };
3431
1616
 
3432
1617
  // src/server/lsp/server.ts
3433
- import { spawn as spawn4 } from "child_process";
1618
+ import { spawn } from "child_process";
3434
1619
  import { extname } from "path";
3435
1620
  import { createMessageConnection, StreamMessageReader, StreamMessageWriter } from "vscode-jsonrpc/node.js";
3436
1621
  var LSP = {
@@ -3492,7 +1677,7 @@ var LspServer = class {
3492
1677
  }
3493
1678
  this.state = "starting";
3494
1679
  try {
3495
- this.process = spawn4(this.commandPath, this.config.serverArgs, {
1680
+ this.process = spawn(this.commandPath, this.config.serverArgs, {
3496
1681
  cwd: this.workdir,
3497
1682
  stdio: ["pipe", "pipe", "pipe"],
3498
1683
  env: {
@@ -3758,20 +1943,20 @@ var LspServer = class {
3758
1943
  };
3759
1944
 
3760
1945
  // src/server/utils/which.ts
3761
- import { access as access2, constants as constants2 } from "fs/promises";
3762
- import { join as join3, dirname as dirname3 } from "path";
3763
- import { fileURLToPath as fileURLToPath2 } from "url";
3764
- var __dirname = dirname3(fileURLToPath2(import.meta.url));
1946
+ import { access, constants } from "fs/promises";
1947
+ import { join, dirname } from "path";
1948
+ import { fileURLToPath } from "url";
1949
+ var __dirname = dirname(fileURLToPath(import.meta.url));
3765
1950
  function getBundledBinPaths() {
3766
1951
  const paths = [];
3767
- paths.push(join3(process.cwd(), "node_modules", ".bin"));
3768
- paths.push(join3(__dirname, "..", "..", "..", "node_modules", ".bin"));
3769
- paths.push(join3(__dirname, "..", "..", "..", "..", "node_modules", ".bin"));
1952
+ paths.push(join(process.cwd(), "node_modules", ".bin"));
1953
+ paths.push(join(__dirname, "..", "..", "..", "node_modules", ".bin"));
1954
+ paths.push(join(__dirname, "..", "..", "..", "..", "node_modules", ".bin"));
3770
1955
  return paths;
3771
1956
  }
3772
1957
  async function existsExecutable(path) {
3773
1958
  try {
3774
- await access2(path, constants2.X_OK);
1959
+ await access(path, constants.X_OK);
3775
1960
  return true;
3776
1961
  } catch {
3777
1962
  return false;
@@ -3785,13 +1970,13 @@ async function which(command, workdir) {
3785
1970
  return null;
3786
1971
  }
3787
1972
  for (const binDir of getBundledBinPaths()) {
3788
- const fullPath = join3(binDir, command);
1973
+ const fullPath = join(binDir, command);
3789
1974
  if (await existsExecutable(fullPath)) {
3790
1975
  return fullPath;
3791
1976
  }
3792
1977
  }
3793
1978
  if (workdir) {
3794
- const projectBin = join3(workdir, "node_modules", ".bin", command);
1979
+ const projectBin = join(workdir, "node_modules", ".bin", command);
3795
1980
  if (await existsExecutable(projectBin)) {
3796
1981
  return projectBin;
3797
1982
  }
@@ -3800,7 +1985,7 @@ async function which(command, workdir) {
3800
1985
  const paths = pathEnv.split(getPathSeparator());
3801
1986
  for (const dir of paths) {
3802
1987
  if (!dir) continue;
3803
- const fullPath = join3(dir, command);
1988
+ const fullPath = join(dir, command);
3804
1989
  if (await existsExecutable(fullPath)) {
3805
1990
  return fullPath;
3806
1991
  }
@@ -4061,7 +2246,8 @@ var LANGUAGES = [
4061
2246
  extensions: [".sql"],
4062
2247
  serverCommand: "sql-language-server",
4063
2248
  serverArgs: ["up", "--method", "stdio"],
4064
- rootPatterns: []
2249
+ rootPatterns: [],
2250
+ installHint: "npm install -g sql-language-server"
4065
2251
  },
4066
2252
  // GraphQL
4067
2253
  {
@@ -4297,6 +2483,8 @@ var LspManager = class {
4297
2483
  // language ids we've tried and failed
4298
2484
  serverPromises = /* @__PURE__ */ new Map();
4299
2485
  // pending server starts
2486
+ dismissedHints = /* @__PURE__ */ new Set();
2487
+ // hints already shown this session
4300
2488
  constructor(workdir, sessionId) {
4301
2489
  this.workdir = workdir;
4302
2490
  this.sessionId = sessionId;
@@ -4417,6 +2605,22 @@ var LspManager = class {
4417
2605
  }
4418
2606
  return true;
4419
2607
  }
2608
+ /**
2609
+ * Get installation hint for a file's language server, if it's not installed.
2610
+ * Returns the hint once per session; subsequent calls return null
2611
+ * to avoid spamming the user on every edit.
2612
+ */
2613
+ getInstallHint(path) {
2614
+ const config4 = detectLanguage(path);
2615
+ if (!config4?.installHint) {
2616
+ return null;
2617
+ }
2618
+ if (this.unavailableServers.has(config4.id) && !this.dismissedHints.has(config4.id)) {
2619
+ this.dismissedHints.add(config4.id);
2620
+ return config4.installHint;
2621
+ }
2622
+ return null;
2623
+ }
4420
2624
  /**
4421
2625
  * Shutdown all LSP servers.
4422
2626
  */
@@ -4475,6 +2679,7 @@ var SessionManager = class {
4475
2679
  providerManager;
4476
2680
  cachedPromptStore = /* @__PURE__ */ new Map();
4477
2681
  dynamicContextChangedStore = /* @__PURE__ */ new Map();
2682
+ debugDumpStore = /* @__PURE__ */ new Map();
4478
2683
  constructor(providerManager) {
4479
2684
  this.providerManager = providerManager;
4480
2685
  }
@@ -5021,8 +3226,8 @@ var SessionManager = class {
5021
3226
  updateExecutionState(sessionId, updates) {
5022
3227
  logger.debug("updateExecutionState called", { sessionId, updates });
5023
3228
  }
5024
- setCachedPrompt(sessionId, systemPrompt, hash) {
5025
- this.cachedPromptStore.set(sessionId, { systemPrompt, hash });
3229
+ setCachedPrompt(sessionId, systemPrompt, tools, hash) {
3230
+ this.cachedPromptStore.set(sessionId, { systemPrompt, tools, hash });
5026
3231
  }
5027
3232
  getCachedPrompt(sessionId) {
5028
3233
  return this.cachedPromptStore.get(sessionId);
@@ -5030,6 +3235,12 @@ var SessionManager = class {
5030
3235
  setDynamicContextChanged(sessionId, changed) {
5031
3236
  this.dynamicContextChangedStore.set(sessionId, changed);
5032
3237
  }
3238
+ setDebugDump(sessionId, dump) {
3239
+ this.debugDumpStore.set(sessionId, dump);
3240
+ }
3241
+ clearDebugDump(sessionId) {
3242
+ this.debugDumpStore.delete(sessionId);
3243
+ }
5033
3244
  getDynamicContextChanged(sessionId) {
5034
3245
  return this.dynamicContextChangedStore.get(sessionId) ?? false;
5035
3246
  }
@@ -5071,6 +3282,7 @@ var SessionManager = class {
5071
3282
  }
5072
3283
  const state = getSessionState(sessionId, maxTokens);
5073
3284
  const dynamicContextChanged = this.getDynamicContextChanged(sessionId);
3285
+ const debugDump = this.debugDumpStore.get(sessionId);
5074
3286
  if (!state) {
5075
3287
  return {
5076
3288
  currentTokens: 0,
@@ -5078,10 +3290,11 @@ var SessionManager = class {
5078
3290
  compactionCount: 0,
5079
3291
  dangerZone: false,
5080
3292
  canCompact: false,
5081
- dynamicContextChanged
3293
+ dynamicContextChanged,
3294
+ ...debugDump ? { debugDump } : {}
5082
3295
  };
5083
3296
  }
5084
- return { ...state.contextState, dynamicContextChanged };
3297
+ return { ...state.contextState, dynamicContextChanged, ...debugDump ? { debugDump } : {} };
5085
3298
  }
5086
3299
  // ============================================================================
5087
3300
  // LSP Manager
@@ -5167,11 +3380,11 @@ var SessionManager = class {
5167
3380
  if (!hasCachedPrompt && eventState.cachedSystemPrompt && eventState.dynamicContextHash) {
5168
3381
  this.cachedPromptStore.set(dbSession.id, {
5169
3382
  systemPrompt: eventState.cachedSystemPrompt,
3383
+ tools: [],
5170
3384
  hash: eventState.dynamicContextHash
5171
3385
  });
5172
3386
  }
5173
3387
  if (!this.dynamicContextChangedStore.has(dbSession.id) && eventState.contextState.dynamicContextChanged) {
5174
- this.dynamicContextChangedStore.set(dbSession.id, true);
5175
3388
  }
5176
3389
  return {
5177
3390
  ...dbSession,
@@ -5193,7 +3406,7 @@ var SessionManager = class {
5193
3406
 
5194
3407
  // src/server/routes/crud-helpers.ts
5195
3408
  import { Router } from "express";
5196
- import { join as join4 } from "path";
3409
+ import { join as join2 } from "path";
5197
3410
  function computeOverrideIds(defaults, userItems) {
5198
3411
  return userItems.filter((u) => defaults.some((d) => d.metadata.id === u.metadata.id)).map((u) => u.metadata.id);
5199
3412
  }
@@ -5205,7 +3418,7 @@ function validateNameIdPrompt(body) {
5205
3418
  return null;
5206
3419
  }
5207
3420
  function getProjectItemPath(projectDir, dirName, id, ext) {
5208
- return join4(projectDir, ".openfox", dirName, `${id}${ext}`);
3421
+ return join2(projectDir, ".openfox", dirName, `${id}${ext}`);
5209
3422
  }
5210
3423
  async function isProjectItem(projectDir, dirName, id, ext) {
5211
3424
  if (!projectDir) return false;
@@ -5369,27 +3582,27 @@ function createSkillRoutes(configDir, projectDir) {
5369
3582
  }
5370
3583
 
5371
3584
  // src/server/commands/registry.ts
5372
- import { writeFile as writeFile3, mkdir as mkdir3, unlink as unlink2 } from "fs/promises";
5373
- import { join as join5, dirname as dirname4 } from "path";
5374
- import { fileURLToPath as fileURLToPath3 } from "url";
3585
+ import { writeFile, mkdir, unlink } from "fs/promises";
3586
+ import { join as join3, dirname as dirname2 } from "path";
3587
+ import { fileURLToPath as fileURLToPath2 } from "url";
5375
3588
  import matter from "gray-matter";
5376
- var __bundleDir2 = dirname4(fileURLToPath3(import.meta.url));
5377
- var DEFAULTS_DIR2 = join5(__bundleDir2, "defaults");
5378
- var DEFAULTS_DIR_ALT2 = join5(__bundleDir2, "command-defaults");
3589
+ var __bundleDir = dirname2(fileURLToPath2(import.meta.url));
3590
+ var DEFAULTS_DIR = join3(__bundleDir, "defaults");
3591
+ var DEFAULTS_DIR_ALT = join3(__bundleDir, "command-defaults");
5379
3592
  var COMMAND_EXTENSION = ".command.md";
5380
3593
  function getCommandsDir(configDir) {
5381
- return join5(configDir, "commands");
3594
+ return join3(configDir, "commands");
5382
3595
  }
5383
3596
  function getProjectCommandsDir(projectDir) {
5384
- return join5(projectDir, ".openfox", "commands");
3597
+ return join3(projectDir, ".openfox", "commands");
5385
3598
  }
5386
3599
  async function loadDefaultCommands() {
5387
- let defaults = await loadItemsFromDir(DEFAULTS_DIR2, {
3600
+ let defaults = await loadItemsFromDir(DEFAULTS_DIR, {
5388
3601
  extension: COMMAND_EXTENSION,
5389
3602
  logName: "command"
5390
3603
  });
5391
3604
  if (!defaults.length) {
5392
- defaults = await loadItemsFromDir(DEFAULTS_DIR_ALT2, {
3605
+ defaults = await loadItemsFromDir(DEFAULTS_DIR_ALT, {
5393
3606
  extension: COMMAND_EXTENSION,
5394
3607
  logName: "command"
5395
3608
  });
@@ -5426,9 +3639,9 @@ async function loadAllCommands(configDir, projectDir) {
5426
3639
  return Array.from(commandMap.values());
5427
3640
  }
5428
3641
  async function getDefaultCommandIds() {
5429
- const ids = await getDefaultIds(DEFAULTS_DIR2, COMMAND_EXTENSION);
3642
+ const ids = await getDefaultIds(DEFAULTS_DIR, COMMAND_EXTENSION);
5430
3643
  if (ids.length) return ids;
5431
- return getDefaultIds(DEFAULTS_DIR_ALT2, COMMAND_EXTENSION);
3644
+ return getDefaultIds(DEFAULTS_DIR_ALT, COMMAND_EXTENSION);
5432
3645
  }
5433
3646
  async function isDefaultCommand(commandId) {
5434
3647
  const defaultIds = await getDefaultCommandIds();
@@ -5438,19 +3651,19 @@ function findCommandById(commandId, commands) {
5438
3651
  return commands.find((c) => c.metadata.id === commandId);
5439
3652
  }
5440
3653
  async function commandExists(configDir, commandId, projectDir) {
5441
- if (await pathExists(join5(getCommandsDir(configDir), `${commandId}${COMMAND_EXTENSION}`))) return true;
5442
- if (projectDir && await pathExists(join5(getProjectCommandsDir(projectDir), `${commandId}${COMMAND_EXTENSION}`)))
3654
+ if (await pathExists(join3(getCommandsDir(configDir), `${commandId}${COMMAND_EXTENSION}`))) return true;
3655
+ if (projectDir && await pathExists(join3(getProjectCommandsDir(projectDir), `${commandId}${COMMAND_EXTENSION}`)))
5443
3656
  return true;
5444
3657
  return false;
5445
3658
  }
5446
3659
  async function saveCommand(configDir, command) {
5447
3660
  const commandsDir = getCommandsDir(configDir);
5448
3661
  if (!await pathExists(commandsDir)) {
5449
- await mkdir3(commandsDir, { recursive: true });
3662
+ await mkdir(commandsDir, { recursive: true });
5450
3663
  }
5451
- const filePath = join5(commandsDir, `${command.metadata.id}${COMMAND_EXTENSION}`);
3664
+ const filePath = join3(commandsDir, `${command.metadata.id}${COMMAND_EXTENSION}`);
5452
3665
  const content = matter.stringify(command.prompt, command.metadata);
5453
- await writeFile3(filePath, content, "utf-8");
3666
+ await writeFile(filePath, content, "utf-8");
5454
3667
  }
5455
3668
  async function saveCommandToProject(projectDir, command) {
5456
3669
  await saveItemToDir(
@@ -5468,9 +3681,9 @@ async function deleteCommand(configDir, commandId) {
5468
3681
  if (isDefault) {
5469
3682
  return { success: false, reason: "Cannot delete built-in defaults" };
5470
3683
  }
5471
- const filePath = join5(getCommandsDir(configDir), `${commandId}${COMMAND_EXTENSION}`);
3684
+ const filePath = join3(getCommandsDir(configDir), `${commandId}${COMMAND_EXTENSION}`);
5472
3685
  try {
5473
- await unlink2(filePath);
3686
+ await unlink(filePath);
5474
3687
  return { success: true };
5475
3688
  } catch {
5476
3689
  return { success: false };
@@ -5691,8 +3904,8 @@ function createTerminalRoutes() {
5691
3904
 
5692
3905
  // src/server/routes/directories.ts
5693
3906
  import { Router as Router5 } from "express";
5694
- import { readdir as readdir2 } from "fs/promises";
5695
- import { resolve, join as join6, dirname as dirname5, basename as basename2 } from "path";
3907
+ import { readdir } from "fs/promises";
3908
+ import { resolve, join as join4, dirname as dirname3, basename as basename2 } from "path";
5696
3909
  function createDirectoryRoutes() {
5697
3910
  const router = Router5();
5698
3911
  const DEFAULT_BASE_PATH = process.cwd();
@@ -5700,12 +3913,12 @@ function createDirectoryRoutes() {
5700
3913
  const path = req.query["path"] || DEFAULT_BASE_PATH;
5701
3914
  try {
5702
3915
  const resolvedPath = resolve(path);
5703
- const entries = await readdir2(resolvedPath, { withFileTypes: true });
3916
+ const entries = await readdir(resolvedPath, { withFileTypes: true });
5704
3917
  const directories = entries.filter((entry) => entry.isDirectory()).map((entry) => ({
5705
3918
  name: entry.name,
5706
- path: join6(resolvedPath, entry.name)
3919
+ path: join4(resolvedPath, entry.name)
5707
3920
  })).sort((a, b) => a.name.localeCompare(b.name));
5708
- const parent = dirname5(resolvedPath);
3921
+ const parent = dirname3(resolvedPath);
5709
3922
  const hasParent = parent !== resolvedPath;
5710
3923
  res.json({
5711
3924
  current: resolvedPath,
@@ -5728,10 +3941,10 @@ function createDirectoryRoutes() {
5728
3941
 
5729
3942
  // src/server/routes/auto-update.ts
5730
3943
  import { Router as Router6 } from "express";
5731
- import { spawn as spawn5 } from "child_process";
3944
+ import { spawn as spawn2 } from "child_process";
5732
3945
 
5733
3946
  // src/constants.ts
5734
- var VERSION = "2.0.13";
3947
+ var VERSION = "2.0.15";
5735
3948
 
5736
3949
  // src/server/routes/auto-update.ts
5737
3950
  var updateInProgress = false;
@@ -5755,7 +3968,7 @@ function createAutoUpdateRoutes(options = {}) {
5755
3968
  const current = VERSION;
5756
3969
  try {
5757
3970
  const latest = await new Promise((resolve3, reject) => {
5758
- const child = spawn5("npm", ["view", "openfox", "version"], {
3971
+ const child = spawn2("npm", ["view", "openfox", "version"], {
5759
3972
  stdio: ["ignore", "pipe", "pipe"]
5760
3973
  });
5761
3974
  let stdout = "";
@@ -5793,7 +4006,7 @@ function createAutoUpdateRoutes(options = {}) {
5793
4006
  updateInProgress = true;
5794
4007
  try {
5795
4008
  const isService = isRunningAsService();
5796
- const child = spawn5("bash", ["-c", "openfox update"], {
4009
+ const child = spawn2("bash", ["-c", "openfox update"], {
5797
4010
  stdio: ["ignore", "pipe", "pipe"]
5798
4011
  });
5799
4012
  let stdout = "";
@@ -5832,7 +4045,7 @@ function createAutoUpdateRoutes(options = {}) {
5832
4045
  return;
5833
4046
  }
5834
4047
  try {
5835
- const child = spawn5("bash", ["-c", "openfox service restart"], {
4048
+ const child = spawn2("bash", ["-c", "openfox service restart"], {
5836
4049
  detached: true,
5837
4050
  stdio: "ignore"
5838
4051
  });
@@ -5846,7 +4059,7 @@ function createAutoUpdateRoutes(options = {}) {
5846
4059
  }
5847
4060
 
5848
4061
  // src/server/index.ts
5849
- var __dirname2 = dirname6(fileURLToPath4(import.meta.url));
4062
+ var __dirname2 = dirname4(fileURLToPath3(import.meta.url));
5850
4063
  async function createServerHandle(config4) {
5851
4064
  setRuntimeConfig(config4);
5852
4065
  setLogLevel(config4.logging?.level ?? void 0, config4.mode);
@@ -5895,6 +4108,25 @@ async function createServerHandle(config4) {
5895
4108
  (err) => logger.error("LLM initialization failed", { error: err instanceof Error ? err.message : String(err) })
5896
4109
  );
5897
4110
  const toolRegistry = createToolRegistry();
4111
+ const mcpManager = new McpManager();
4112
+ setMcpManagerForTools(mcpManager);
4113
+ setMcpConfigMode(config4.mode ?? "production");
4114
+ const mcpServers = config4.mcpServers ?? {};
4115
+ Promise.all(
4116
+ Object.entries(mcpServers).map(
4117
+ ([name, serverConfig]) => mcpManager.addServer(name, serverConfig).catch((err) => {
4118
+ logger.warn("Failed to connect MCP server on startup", { name, error: String(err) });
4119
+ })
4120
+ )
4121
+ ).then(async () => {
4122
+ const mcpTools = createMcpTools(mcpManager);
4123
+ if (mcpTools.length > 0) {
4124
+ setMcpTools(mcpTools);
4125
+ logger.info("MCP tools registered", { count: mcpTools.length });
4126
+ }
4127
+ const { signalMcpReady } = await import("./server-P7E2IPVL.js");
4128
+ signalMcpReady();
4129
+ });
5898
4130
  const app = express();
5899
4131
  app.use(cors());
5900
4132
  app.use(express.json({ limit: "10mb" }));
@@ -6083,7 +4315,7 @@ async function createServerHandle(config4) {
6083
4315
  app.get("/api/sessions/:id", async (req, res) => {
6084
4316
  const { getEventStore: getEventStore2 } = await import("./events-4K52FKPR.js");
6085
4317
  const { buildMessagesFromStoredEvents } = await import("./folding-QIBNKYA6.js");
6086
- const { getPendingQuestionsForSession } = await import("./tools-THXBQJ7A.js");
4318
+ const { getPendingQuestionsForSession } = await import("./tools-WEE4XDTL.js");
6087
4319
  const session = sessionManager.getSession(req.params.id);
6088
4320
  if (!session) {
6089
4321
  return res.status(404).json({ error: "Session not found" });
@@ -6126,7 +4358,7 @@ async function createServerHandle(config4) {
6126
4358
  return res.status(400).json({ error: "providerId is required" });
6127
4359
  }
6128
4360
  sessionManager.setSessionProvider(sessionId, providerId, model ?? "auto");
6129
- const { loadGlobalConfig, saveGlobalConfig, setDefaultModelSelection } = await import("./config-BU66P4KX.js");
4361
+ const { loadGlobalConfig, saveGlobalConfig, setDefaultModelSelection } = await import("./config-Z66BQTNX.js");
6130
4362
  const globalConfig = await loadGlobalConfig(config4.mode ?? "production");
6131
4363
  const updatedConfig = setDefaultModelSelection(globalConfig, providerId, model ?? "auto");
6132
4364
  await saveGlobalConfig(config4.mode ?? "production", updatedConfig);
@@ -6214,15 +4446,15 @@ async function createServerHandle(config4) {
6214
4446
  if (!callId || approved === void 0) {
6215
4447
  return res.status(400).json({ error: "callId and approved are required" });
6216
4448
  }
6217
- const { providePathConfirmation } = await import("./tools-THXBQJ7A.js");
4449
+ const { providePathConfirmation } = await import("./tools-WEE4XDTL.js");
6218
4450
  const result = providePathConfirmation(callId, approved, alwaysAllow);
6219
4451
  if (!result.found) {
6220
4452
  return res.status(404).json({ error: "No pending path confirmation with that ID" });
6221
4453
  }
6222
4454
  const { getEventStore: getEventStore2 } = await import("./events-4K52FKPR.js");
6223
4455
  const { buildMessagesFromStoredEvents, foldPendingConfirmations } = await import("./folding-QIBNKYA6.js");
6224
- const { createSessionStateMessage } = await import("./protocol-YYWMFR35.js");
6225
- const { getPendingQuestionsForSession } = await import("./tools-THXBQJ7A.js");
4456
+ const { createSessionStateMessage } = await import("./protocol-BKNLAEPJ.js");
4457
+ const { getPendingQuestionsForSession } = await import("./tools-WEE4XDTL.js");
6226
4458
  const eventStore = getEventStore2();
6227
4459
  const events = eventStore.getEvents(sessionId);
6228
4460
  const messages = buildMessagesFromStoredEvents(events);
@@ -6243,8 +4475,8 @@ async function createServerHandle(config4) {
6243
4475
  if (!skip && typeof answer !== "string") {
6244
4476
  return res.status(400).json({ error: "answer is required when not skipping" });
6245
4477
  }
6246
- const { provideAnswer: provideAnswer2 } = await import("./tools-THXBQJ7A.js");
6247
- const found = provideAnswer2(callId, answer ?? "", skip ?? false);
4478
+ const { provideAnswer } = await import("./tools-WEE4XDTL.js");
4479
+ const found = provideAnswer(callId, answer ?? "", skip ?? false);
6248
4480
  if (!found) {
6249
4481
  return res.status(404).json({ error: "No pending question with that ID" });
6250
4482
  }
@@ -6279,8 +4511,8 @@ async function createServerHandle(config4) {
6279
4511
  if (!session) {
6280
4512
  return res.status(404).json({ error: "Session not found" });
6281
4513
  }
6282
- const { stopSessionExecution } = await import("./chat-handler-6NUNVXIU.js");
6283
- const { cancelQuestionsForSession, cancelPathConfirmationsForSession } = await import("./tools-THXBQJ7A.js");
4514
+ const { stopSessionExecution } = await import("./chat-handler-RYOG3WJP.js");
4515
+ const { cancelQuestionsForSession, cancelPathConfirmationsForSession } = await import("./tools-WEE4XDTL.js");
6284
4516
  const queuedMessages = sessionManager.getQueueState(sessionId);
6285
4517
  sessionManager.clearMessageQueue(sessionId);
6286
4518
  stopSessionExecution(sessionId, sessionManager);
@@ -6391,7 +4623,7 @@ async function createServerHandle(config4) {
6391
4623
  let visionFallback;
6392
4624
  let globalWorkdir;
6393
4625
  try {
6394
- const { loadGlobalConfig, getVisionFallback } = await import("./config-BU66P4KX.js");
4626
+ const { loadGlobalConfig, getVisionFallback } = await import("./config-Z66BQTNX.js");
6395
4627
  const globalConfig = await loadGlobalConfig(config4.mode ?? "production");
6396
4628
  const fallback = getVisionFallback(globalConfig);
6397
4629
  if (fallback) {
@@ -6487,7 +4719,7 @@ async function createServerHandle(config4) {
6487
4719
  const apiKey = req.query["apiKey"];
6488
4720
  if (!url) return res.status(400).json({ error: "url is required" });
6489
4721
  try {
6490
- const { fetchModelsWithContext } = await import("./provider-manager-YA2WALTF.js");
4722
+ const { fetchModelsWithContext } = await import("./provider-manager-DETXLDKW.js");
6491
4723
  const models = await fetchModelsWithContext(url, apiKey);
6492
4724
  if (models.length === 0) {
6493
4725
  return res.status(404).json({ error: `No models found at ${buildModelsUrl(url)}`, url });
@@ -6557,7 +4789,7 @@ async function createServerHandle(config4) {
6557
4789
  return res.status(400).json({ error: "name, url, and backend are required" });
6558
4790
  }
6559
4791
  try {
6560
- const { loadGlobalConfig, saveGlobalConfig, addProvider, setDefaultModelSelection } = await import("./config-BU66P4KX.js");
4792
+ const { loadGlobalConfig, saveGlobalConfig, addProvider, setDefaultModelSelection } = await import("./config-Z66BQTNX.js");
6561
4793
  const globalConfig = await loadGlobalConfig(config4.mode ?? "production");
6562
4794
  const providerBackend = backend;
6563
4795
  const configWithProvider = addProvider(globalConfig, {
@@ -6592,7 +4824,7 @@ async function createServerHandle(config4) {
6592
4824
  app.post("/api/init/config", async (req, res) => {
6593
4825
  const { workdir, visionFallback } = req.body;
6594
4826
  try {
6595
- const { loadGlobalConfig, saveGlobalConfig } = await import("./config-BU66P4KX.js");
4827
+ const { loadGlobalConfig, saveGlobalConfig } = await import("./config-Z66BQTNX.js");
6596
4828
  const globalConfig = await loadGlobalConfig(config4.mode ?? "production");
6597
4829
  const updatedConfig = {
6598
4830
  ...globalConfig,
@@ -6620,7 +4852,7 @@ async function createServerHandle(config4) {
6620
4852
  });
6621
4853
  app.delete("/api/providers/:id", async (req, res) => {
6622
4854
  const { id } = req.params;
6623
- const { loadGlobalConfig, saveGlobalConfig, removeProvider } = await import("./config-BU66P4KX.js");
4855
+ const { loadGlobalConfig, saveGlobalConfig, removeProvider } = await import("./config-Z66BQTNX.js");
6624
4856
  const globalConfig = await loadGlobalConfig(config4.mode ?? "production");
6625
4857
  const updatedConfig = removeProvider(globalConfig, id);
6626
4858
  await saveGlobalConfig(config4.mode ?? "production", updatedConfig);
@@ -6631,7 +4863,7 @@ async function createServerHandle(config4) {
6631
4863
  const { id } = req.params;
6632
4864
  const { isLocal } = req.body;
6633
4865
  try {
6634
- const { loadGlobalConfig, saveGlobalConfig, updateProvider } = await import("./config-BU66P4KX.js");
4866
+ const { loadGlobalConfig, saveGlobalConfig, updateProvider } = await import("./config-Z66BQTNX.js");
6635
4867
  const globalConfig = await loadGlobalConfig(config4.mode ?? "production");
6636
4868
  const provider = globalConfig.providers.find((p) => p.id === id);
6637
4869
  if (!provider) {
@@ -6659,7 +4891,7 @@ async function createServerHandle(config4) {
6659
4891
  models: modelConfigs
6660
4892
  } = req.body;
6661
4893
  try {
6662
- const { loadGlobalConfig, saveGlobalConfig, updateProvider } = await import("./config-BU66P4KX.js");
4894
+ const { loadGlobalConfig, saveGlobalConfig, updateProvider } = await import("./config-Z66BQTNX.js");
6663
4895
  const globalConfig = await loadGlobalConfig(config4.mode ?? "production");
6664
4896
  const provider = globalConfig.providers.find((p) => p.id === id);
6665
4897
  if (!provider) {
@@ -6696,7 +4928,7 @@ async function createServerHandle(config4) {
6696
4928
  return res.status(400).json({ error: result.error });
6697
4929
  }
6698
4930
  const llmClient = getLLMClient();
6699
- const { loadGlobalConfig, saveGlobalConfig, setDefaultModelSelection } = await import("./config-BU66P4KX.js");
4931
+ const { loadGlobalConfig, saveGlobalConfig, setDefaultModelSelection } = await import("./config-Z66BQTNX.js");
6700
4932
  const globalConfig = await loadGlobalConfig(config4.mode ?? "production");
6701
4933
  const updatedConfig = setDefaultModelSelection(globalConfig, id, llmClient.getModel());
6702
4934
  await saveGlobalConfig(config4.mode ?? "production", updatedConfig);
@@ -6721,6 +4953,145 @@ async function createServerHandle(config4) {
6721
4953
  status: updatedProvider?.status ?? "unknown"
6722
4954
  });
6723
4955
  });
4956
+ async function rebuildMcpTools() {
4957
+ const { createMcpTools: createMcpTools2 } = await import("./tool-adapter-B7QP6NLA.js");
4958
+ const { setMcpTools: setMcpTools2 } = await import("./tools-WEE4XDTL.js");
4959
+ const mcpTools = createMcpTools2(mcpManager);
4960
+ setMcpTools2(mcpTools);
4961
+ }
4962
+ app.get("/api/mcp/servers", (_req, res) => {
4963
+ const servers = mcpManager.getAllServers();
4964
+ res.json({ servers });
4965
+ });
4966
+ app.post("/api/mcp/servers/test", async (req, res) => {
4967
+ const { name, transport, command, args, env, url, headers } = req.body;
4968
+ if (transport !== void 0 && transport !== "stdio" && transport !== "http") {
4969
+ return res.status(400).json({ error: `Invalid transport '${transport}'. Must be 'stdio' or 'http'.` });
4970
+ }
4971
+ if (transport !== "http" && !command) {
4972
+ return res.status(400).json({ error: "command is required for stdio transport" });
4973
+ }
4974
+ if (transport === "http" && !url) {
4975
+ return res.status(400).json({ error: "url is required for http transport" });
4976
+ }
4977
+ try {
4978
+ const testManager = new McpManager();
4979
+ const resolvedTransport = transport === "http" ? "http" : "stdio";
4980
+ const testConfig = {
4981
+ transport: resolvedTransport,
4982
+ ...command ? { command } : {},
4983
+ ...args && args.length > 0 ? { args } : {},
4984
+ ...env && Object.keys(env).length > 0 ? { env } : {},
4985
+ ...url ? { url } : {},
4986
+ ...headers && Object.keys(headers).length > 0 ? { headers } : {}
4987
+ };
4988
+ await testManager.addServer(name ?? "test", testConfig);
4989
+ const server = testManager.getServer(name ?? "test");
4990
+ await testManager.disconnectAll();
4991
+ if (server?.status === "connected") {
4992
+ res.json({ success: true, tools: server.tools.map((t) => t.name) });
4993
+ } else {
4994
+ res.json({ success: false, error: server?.error ?? "Connection failed" });
4995
+ }
4996
+ } catch (error) {
4997
+ res.status(400).json({ success: false, error: error instanceof Error ? error.message : String(error) });
4998
+ }
4999
+ });
5000
+ app.post("/api/mcp/servers", async (req, res) => {
5001
+ const { name, transport, command, args, env, url, headers } = req.body;
5002
+ if (!name) {
5003
+ return res.status(400).json({ error: "name is required" });
5004
+ }
5005
+ if (transport !== void 0 && transport !== "stdio" && transport !== "http") {
5006
+ return res.status(400).json({ error: `Invalid transport '${transport}'. Must be 'stdio' or 'http'.` });
5007
+ }
5008
+ try {
5009
+ const resolvedTransport = transport === "http" ? "http" : "stdio";
5010
+ const serverCfg = {
5011
+ transport: resolvedTransport,
5012
+ ...command ? { command } : {},
5013
+ ...args && args.length > 0 ? { args } : {},
5014
+ ...env && Object.keys(env).length > 0 ? { env } : {},
5015
+ ...url ? { url } : {},
5016
+ ...headers && Object.keys(headers).length > 0 ? { headers } : {}
5017
+ };
5018
+ await mcpManager.addServer(name, serverCfg);
5019
+ const server = mcpManager.getServer(name);
5020
+ const { loadGlobalConfig, saveGlobalConfig } = await import("./config-Z66BQTNX.js");
5021
+ const globalConfig = await loadGlobalConfig(config4.mode ?? "production");
5022
+ const updatedMcpServers = { ...globalConfig.mcpServers ?? {}, [name]: serverCfg };
5023
+ await saveGlobalConfig(config4.mode ?? "production", {
5024
+ ...globalConfig,
5025
+ mcpServers: updatedMcpServers
5026
+ });
5027
+ await rebuildMcpTools();
5028
+ if (server) {
5029
+ const sessions = sessionManager.listSessions();
5030
+ for (const s of sessions) {
5031
+ sessionManager.setDynamicContextChanged(s.id, true);
5032
+ }
5033
+ }
5034
+ res.status(201).json({ server });
5035
+ } catch (error) {
5036
+ res.status(400).json({ error: error instanceof Error ? error.message : String(error) });
5037
+ }
5038
+ });
5039
+ app.delete("/api/mcp/servers/:name", async (req, res) => {
5040
+ const { name } = req.params;
5041
+ const server = mcpManager.getServer(name);
5042
+ if (!server) {
5043
+ return res.status(404).json({ error: `MCP server '${name}' not found` });
5044
+ }
5045
+ mcpManager.removeServer(name);
5046
+ const { loadGlobalConfig, saveGlobalConfig } = await import("./config-Z66BQTNX.js");
5047
+ const globalConfig = await loadGlobalConfig(config4.mode ?? "production");
5048
+ const updatedMcpServers = { ...globalConfig.mcpServers ?? {} };
5049
+ delete updatedMcpServers[name];
5050
+ await saveGlobalConfig(config4.mode ?? "production", {
5051
+ ...globalConfig,
5052
+ mcpServers: updatedMcpServers
5053
+ });
5054
+ await rebuildMcpTools();
5055
+ const sessions = sessionManager.listSessions();
5056
+ for (const s of sessions) {
5057
+ sessionManager.setDynamicContextChanged(s.id, true);
5058
+ }
5059
+ res.json({ success: true });
5060
+ });
5061
+ app.put("/api/mcp/servers/:name/tools/:toolName", async (req, res) => {
5062
+ const { name, toolName } = req.params;
5063
+ const { enabled } = req.body;
5064
+ if (enabled === void 0) {
5065
+ return res.status(400).json({ error: "enabled is required" });
5066
+ }
5067
+ try {
5068
+ await mcpManager.setToolEnabled(name, toolName, enabled);
5069
+ const server = mcpManager.getServer(name);
5070
+ if (server) {
5071
+ const { loadGlobalConfig, saveGlobalConfig } = await import("./config-Z66BQTNX.js");
5072
+ const globalConfig = await loadGlobalConfig(config4.mode ?? "production");
5073
+ const mcpServers2 = { ...globalConfig.mcpServers ?? {} };
5074
+ const serverCfg = mcpServers2[name];
5075
+ if (serverCfg) {
5076
+ const disabledTools = server.tools.filter((t) => !t.enabled).map((t) => t.name);
5077
+ const cfg = { ...serverCfg, ...disabledTools.length > 0 ? { disabledTools } : { disabledTools: void 0 } };
5078
+ mcpServers2[name] = cfg;
5079
+ await saveGlobalConfig(config4.mode ?? "production", {
5080
+ ...globalConfig,
5081
+ mcpServers: mcpServers2
5082
+ });
5083
+ }
5084
+ }
5085
+ await rebuildMcpTools();
5086
+ const sessions = sessionManager.listSessions();
5087
+ for (const s of sessions) {
5088
+ sessionManager.setDynamicContextChanged(s.id, true);
5089
+ }
5090
+ res.json({ success: true });
5091
+ } catch (error) {
5092
+ res.status(400).json({ error: error instanceof Error ? error.message : String(error) });
5093
+ }
5094
+ });
6724
5095
  const projectDir = config4.workdir;
6725
5096
  app.use("/api/skills", createSkillRoutes(configDir, projectDir));
6726
5097
  app.use("/api/commands", createCommandRoutes(configDir, projectDir));
@@ -6795,7 +5166,7 @@ async function createServerHandle(config4) {
6795
5166
  }
6796
5167
  });
6797
5168
  app.get("/fox.svg", (_req, res) => {
6798
- readFile3(join7(webDir, "fox.svg")).then((content) => {
5169
+ readFile(join5(webDir, "fox.svg")).then((content) => {
6799
5170
  res.set("Content-Type", "image/svg+xml");
6800
5171
  res.send(content);
6801
5172
  }).catch(() => {
@@ -6804,7 +5175,7 @@ async function createServerHandle(config4) {
6804
5175
  });
6805
5176
  app.use(
6806
5177
  "/sounds",
6807
- express.static(join7(webDir, "public", "sounds"), {
5178
+ express.static(join5(webDir, "public", "sounds"), {
6808
5179
  setHeaders: (res) => {
6809
5180
  res.set("Content-Type", "audio/mpeg");
6810
5181
  }
@@ -6813,13 +5184,13 @@ async function createServerHandle(config4) {
6813
5184
  app.use("/__inspect__.js", (_req, res) => {
6814
5185
  res.set("Access-Control-Allow-Origin", "*");
6815
5186
  res.set("Content-Type", "application/javascript");
6816
- readFile3(join7(webDir, "public", "__inspect__.js")).then((content) => res.send(content)).catch(() => res.status(404).send("Not found"));
5187
+ readFile(join5(webDir, "public", "__inspect__.js")).then((content) => res.send(content)).catch(() => res.status(404).send("Not found"));
6817
5188
  });
6818
5189
  app.get("/*path", (req, res) => {
6819
5190
  if (req.path.startsWith("/api/")) {
6820
5191
  return;
6821
5192
  }
6822
- readFile3(join7(webDir, "index.html"), "utf-8").then((indexHtml) => viteServer.transformIndexHtml(req.originalUrl, indexHtml)).then((transformed) => res.send(transformed)).catch((err) => {
5193
+ readFile(join5(webDir, "index.html"), "utf-8").then((indexHtml) => viteServer.transformIndexHtml(req.originalUrl, indexHtml)).then((transformed) => res.send(transformed)).catch((err) => {
6823
5194
  logger.error("Error serving index.html", { error: err });
6824
5195
  res.status(500).send("Server error");
6825
5196
  });
@@ -6830,7 +5201,7 @@ async function createServerHandle(config4) {
6830
5201
  const distWebDir = resolve2(__dirname2, "web");
6831
5202
  app.use(
6832
5203
  "/assets",
6833
- express.static(join7(distWebDir, "assets"), {
5204
+ express.static(join5(distWebDir, "assets"), {
6834
5205
  setHeaders: (res, filepath) => {
6835
5206
  if (filepath.endsWith(".css")) {
6836
5207
  res.set("Content-Type", "text/css");
@@ -6838,11 +5209,11 @@ async function createServerHandle(config4) {
6838
5209
  }
6839
5210
  })
6840
5211
  );
6841
- app.use("/manifest.webmanifest", express.static(join7(distWebDir, "manifest.webmanifest")));
6842
- app.use("/registerSW.js", express.static(join7(distWebDir, "registerSW.js")));
6843
- app.use("/sw.js", express.static(join7(distWebDir, "sw.js")));
5212
+ app.use("/manifest.webmanifest", express.static(join5(distWebDir, "manifest.webmanifest")));
5213
+ app.use("/registerSW.js", express.static(join5(distWebDir, "registerSW.js")));
5214
+ app.use("/sw.js", express.static(join5(distWebDir, "sw.js")));
6844
5215
  app.get("/fox.svg", (_req, res) => {
6845
- readFile3(join7(distWebDir, "fox.svg")).then((content) => {
5216
+ readFile(join5(distWebDir, "fox.svg")).then((content) => {
6846
5217
  res.set("Content-Type", "image/svg+xml");
6847
5218
  res.send(content);
6848
5219
  }).catch(() => {
@@ -6851,7 +5222,7 @@ async function createServerHandle(config4) {
6851
5222
  });
6852
5223
  app.use(
6853
5224
  "/sounds",
6854
- express.static(join7(distWebDir, "sounds"), {
5225
+ express.static(join5(distWebDir, "sounds"), {
6855
5226
  setHeaders: (res) => {
6856
5227
  res.set("Content-Type", "audio/mpeg");
6857
5228
  }
@@ -6860,15 +5231,15 @@ async function createServerHandle(config4) {
6860
5231
  app.use("/__inspect__.js", (_req, res) => {
6861
5232
  res.set("Access-Control-Allow-Origin", "*");
6862
5233
  res.set("Content-Type", "application/javascript");
6863
- readFile3(join7(distWebDir, "__inspect__.js")).then((content) => res.send(content)).catch(() => res.status(404).send("Not found"));
5234
+ readFile(join5(distWebDir, "__inspect__.js")).then((content) => res.send(content)).catch(() => res.status(404).send("Not found"));
6864
5235
  });
6865
5236
  app.use("/__inspect__.js", (_req, res) => {
6866
5237
  res.set("Access-Control-Allow-Origin", "*");
6867
5238
  res.set("Content-Type", "application/javascript");
6868
- readFile3(join7(distWebDir, "__inspect__.js")).then((content) => res.send(content)).catch(() => res.status(404).send("Not found"));
5239
+ readFile(join5(distWebDir, "__inspect__.js")).then((content) => res.send(content)).catch(() => res.status(404).send("Not found"));
6869
5240
  });
6870
5241
  app.get("/", (_req, res) => {
6871
- readFile3(join7(distWebDir, "index.html"), "utf-8").then((content) => res.send(content)).catch(() => {
5242
+ readFile(join5(distWebDir, "index.html"), "utf-8").then((content) => res.send(content)).catch(() => {
6872
5243
  res.status(404).send("Web UI not built. Run `npm run build:web`");
6873
5244
  });
6874
5245
  });
@@ -6876,7 +5247,7 @@ async function createServerHandle(config4) {
6876
5247
  if (req.path.startsWith("/api/") || req.path.startsWith("/assets/") || req.path.startsWith("/sounds/") || req.path.startsWith("/manifest.webmanifest") || req.path.startsWith("/registerSW.js") || req.path.startsWith("/sw.js") || req.path === "/fox.svg") {
6877
5248
  return;
6878
5249
  }
6879
- readFile3(join7(distWebDir, "index.html"), "utf-8").then((content) => res.send(content)).catch(() => {
5250
+ readFile(join5(distWebDir, "index.html"), "utf-8").then((content) => res.send(content)).catch(() => {
6880
5251
  res.status(404).send("Web UI not built");
6881
5252
  });
6882
5253
  });
@@ -6891,7 +5262,12 @@ async function createServerHandle(config4) {
6891
5262
  providerManager
6892
5263
  );
6893
5264
  const wss = wssExports.wss;
6894
- const { QueueProcessor } = await import("./processor-GA5NG3T7.js");
5265
+ setNotifyMcpServersChanged((sessionId) => {
5266
+ wssExports.broadcastAll(createServerMessage("mcp.servers.changed", {}));
5267
+ const state = sessionManager.getContextState(sessionId);
5268
+ wssExports.broadcastForSession(sessionId, createContextStateMessage(state));
5269
+ });
5270
+ const { QueueProcessor } = await import("./processor-JHR3Z42W.js");
6895
5271
  const queueProcessor = new QueueProcessor({
6896
5272
  sessionManager,
6897
5273
  providerManager,
@@ -6931,6 +5307,7 @@ async function createServerHandle(config4) {
6931
5307
  logger.info("Shutting down...");
6932
5308
  void (async () => {
6933
5309
  await devServerManager.stopAll();
5310
+ await mcpManager.disconnectAll();
6934
5311
  const { stopAllInspectProxies } = await import("./inspect-proxy-42ZXL2R5.js");
6935
5312
  stopAllInspectProxies();
6936
5313
  const { cleanupAllProcesses } = await import("./store-OQ45KONN.js");
@@ -6965,4 +5342,4 @@ export {
6965
5342
  createServerHandle,
6966
5343
  createServer
6967
5344
  };
6968
- //# sourceMappingURL=chunk-VFRWE5X4.js.map
5345
+ //# sourceMappingURL=chunk-2GX5EIIU.js.map