openfox 2.0.13 → 2.0.14

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 (31) hide show
  1. package/dist/{chat-handler-6NUNVXIU.js → chat-handler-ITXCBFJK.js} +4 -4
  2. package/dist/{chunk-A52FXWJX.js → chunk-3SGGOBCL.js} +10 -387
  3. package/dist/{chunk-VFRWE5X4.js → chunk-4U45A7B3.js} +386 -2075
  4. package/dist/chunk-4W2Z4B5U.js +2077 -0
  5. package/dist/{chunk-2VDLXCLO.js → chunk-4ZTQO5JP.js} +31 -25
  6. package/dist/{chunk-EZJUU54W.js → chunk-7GJ6XYAF.js} +10 -8
  7. package/dist/{chunk-FQIT7VIA.js → chunk-LNEBVOH6.js} +6 -6
  8. package/dist/chunk-NWO6GRYE.js +39 -0
  9. package/dist/{chunk-HPCGVAS4.js → chunk-PP6VQXQW.js} +17 -1
  10. package/dist/chunk-PQ56PX7L.js +397 -0
  11. package/dist/cli/dev.js +1 -1
  12. package/dist/cli/index.js +1 -1
  13. package/dist/{config-BU66P4KX.js → config-YFHA5MWV.js} +2 -2
  14. package/dist/{orchestrator-WXEZHUVQ.js → orchestrator-VP74KOXI.js} +4 -4
  15. package/dist/package.json +3 -2
  16. package/dist/{processor-GA5NG3T7.js → processor-CCXZYLX6.js} +4 -4
  17. package/dist/{protocol-CaLuIetw.d.ts → protocol-BE5dRtYw.d.ts} +9 -0
  18. package/dist/{provider-QPECLUZ7.js → provider-QUJQLVOM.js} +7 -5
  19. package/dist/{provider-manager-YA2WALTF.js → provider-manager-DETXLDKW.js} +3 -2
  20. package/dist/{serve-TJHQ326L.js → serve-GA2UIHHB.js} +11 -7
  21. package/dist/server/index.d.ts +3 -2
  22. package/dist/server/index.js +8 -5
  23. package/dist/server-EA6W7YIC.js +29 -0
  24. package/dist/shared/index.d.ts +2 -2
  25. package/dist/tool-adapter-B7QP6NLA.js +7 -0
  26. package/dist/{tools-THXBQJ7A.js → tools-DUC54GEH.js} +5 -3
  27. package/dist/web/assets/{index-CQw-9GC9.js → index-D0qVcBva.js} +76 -75
  28. package/dist/web/assets/{index-Db3yUKBI.css → index-Ho_tVUbw.css} +1 -1
  29. package/dist/web/index.html +2 -2
  30. package/dist/web/sw.js +1 -1
  31. package/package.json +3 -2
@@ -1,15 +1,32 @@
1
1
  import {
2
- appendCompactionPrompt
3
- } from "./chunk-FWJ6YXGA.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-4W2Z4B5U.js";
4
25
  import {
5
- computeDynamicContextHash,
6
- injectWorkflowKickoffIfNeeded,
7
- runAgentTurn,
8
- runChatTurn
9
- } from "./chunk-EZJUU54W.js";
26
+ createMcpTools
27
+ } from "./chunk-NWO6GRYE.js";
10
28
  import {
11
29
  agentExists,
12
- checkAborted,
13
30
  createToolRegistry,
14
31
  deleteAgent,
15
32
  deleteItemFromDir,
@@ -17,21 +34,15 @@ import {
17
34
  deleteProjectSkill,
18
35
  deleteSkill,
19
36
  devServerManager,
20
- executeSubAgent,
21
37
  findAgentById,
22
38
  findSkillById,
23
- getAllInstructions,
24
39
  getDefaultAgentIds,
25
40
  getDefaultIds,
26
41
  getDefaultSkillIds,
27
- getEnabledSkillMetadata,
28
- getSubAgents,
29
- getToolRegistryForAgent,
30
42
  getTopLevelAgents,
31
43
  isDefaultAgent,
32
44
  isDefaultSkill,
33
45
  isSkillEnabled,
34
- jsonSerializer,
35
46
  loadAllAgents,
36
47
  loadAllAgentsDefault,
37
48
  loadAllSkills,
@@ -48,23 +59,16 @@ import {
48
59
  saveItemToDir,
49
60
  saveSkill,
50
61
  saveSkillToProject,
51
- skillExists,
52
- spawnShellProcess
53
- } from "./chunk-2VDLXCLO.js";
62
+ setMcpTools,
63
+ skillExists
64
+ } from "./chunk-4ZTQO5JP.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
- import {
66
- getProject
67
- } from "./chunk-XAMAYRDA.js";
68
72
  import {
69
73
  createSession,
70
74
  deleteSession,
@@ -78,9 +82,7 @@ import {
78
82
  emitRunningChanged,
79
83
  emitSessionInitialized,
80
84
  emitUserMessage,
81
- getCurrentContextWindowId,
82
85
  getEventStore,
83
- getRuntimeConfig,
84
86
  getSession,
85
87
  getSessionState,
86
88
  initEventStore,
@@ -93,45 +95,25 @@ import {
93
95
  updateSessionRunning
94
96
  } from "./chunk-SYG2ENUQ.js";
95
97
  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";
109
- import {
110
- EventEmitter,
111
- provideAnswer
98
+ EventEmitter
112
99
  } from "./chunk-EU3WWTFH.js";
113
100
  import {
114
101
  initDatabase
115
102
  } from "./chunk-FBGWG4N6.js";
116
- import {
117
- computeSessionStats
118
- } from "./chunk-VUQCQXXJ.js";
119
- import {
120
- createServerMessage
121
- } from "./chunk-5BDVM6YI.js";
122
103
  import {
123
104
  getGlobalConfigDir
124
105
  } from "./chunk-CQGTEGKL.js";
106
+ import {
107
+ createProviderManager,
108
+ parseDefaultModelSelection
109
+ } from "./chunk-3SGGOBCL.js";
125
110
  import {
126
111
  SessionNotFoundError,
127
112
  buildModelsUrl,
128
- createLLMClient,
129
- createProviderManager,
130
113
  detectModel,
131
114
  ensureVersionPrefix,
132
- getLlmStatus,
133
- parseDefaultModelSelection
134
- } from "./chunk-A52FXWJX.js";
115
+ getLlmStatus
116
+ } from "./chunk-PQ56PX7L.js";
135
117
  import {
136
118
  getBackendDisplayName,
137
119
  getModelProfile
@@ -145,9 +127,9 @@ import {
145
127
  import express from "express";
146
128
  import cors from "cors";
147
129
  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";
130
+ import { fileURLToPath as fileURLToPath3 } from "url";
131
+ import { dirname as dirname4, resolve as resolve2, join as join5 } from "path";
132
+ import { readFile } from "fs/promises";
151
133
  import { createServer as createViteServer } from "vite";
152
134
 
153
135
  // src/server/llm/mock.ts
@@ -1462,1975 +1444,160 @@ function createMockLLMClient() {
1462
1444
  };
1463
1445
  }
1464
1446
 
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
- });
1447
+ // src/server/mcp/manager.ts
1448
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
1449
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
1450
+ function estimateToolTokens(toolName, description, inputSchema) {
1451
+ const def = {
1452
+ type: "function",
1453
+ function: { name: toolName, description: description ?? "", parameters: inputSchema }
1454
+ };
1455
+ return Math.ceil(JSON.stringify(def).length / 4);
1643
1456
  }
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
- }
1457
+ var McpManager = class {
1458
+ servers = /* @__PURE__ */ new Map();
1459
+ onServersChanged;
1460
+ constructor(options) {
1461
+ this.onServersChanged = options?.onServersChanged;
1700
1462
  }
1701
- }
1702
- function unsubscribeAllFromTerminal(ws) {
1703
- for (const sub of subscriptions) {
1704
- if (sub.ws === ws) {
1705
- subscriptions.delete(sub);
1463
+ async addServer(name, config4) {
1464
+ if (this.servers.has(name)) {
1465
+ throw new Error(`MCP server '${name}' already exists`);
1706
1466
  }
1467
+ const state = { name, config: config4, status: "disconnected", tools: [], estimatedTokens: 0 };
1468
+ this.servers.set(name, { config: config4, client: void 0, transport: null, state });
1469
+ await this.connectServer(name);
1707
1470
  }
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 [];
1471
+ removeServer(name) {
1472
+ const entry = this.servers.get(name);
1473
+ if (entry) {
1474
+ this.disconnectServer(name);
1475
+ this.servers.delete(name);
1476
+ }
1742
1477
  }
1743
- const workflows = [];
1744
- for (const file of files) {
1478
+ async connectServer(name) {
1479
+ const entry = this.servers.get(name);
1480
+ if (!entry) return;
1745
1481
  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);
1482
+ await this.disconnectServer(name);
1483
+ const client = new Client({ name: "openfox-mcp", version: "2.0.0" });
1484
+ let transport = null;
1485
+ if (entry.config.transport === "stdio") {
1486
+ if (!entry.config.command) throw new Error("command is required for stdio transport");
1487
+ transport = new StdioClientTransport({
1488
+ command: entry.config.command,
1489
+ ...entry.config.args ? { args: entry.config.args } : {},
1490
+ ...entry.config.env ? { env: entry.config.env } : {},
1491
+ stderr: "pipe"
1492
+ });
1493
+ await client.connect(transport);
1750
1494
  } else {
1751
- logger.warn("Skipping invalid workflow file", { file });
1495
+ throw new Error("Only stdio transport is supported in this version");
1752
1496
  }
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);
1497
+ const { tools: mcpTools } = await client.listTools();
1498
+ const disabledSet = new Set(entry.config.disabledTools ?? []);
1499
+ const tools = mcpTools.map((t) => {
1500
+ const inputSchema = t.inputSchema;
1501
+ return {
1502
+ name: t.name,
1503
+ description: t.description ?? "",
1504
+ inputSchema,
1505
+ enabled: !disabledSet.has(t.name),
1506
+ estimatedTokens: estimateToolTokens(t.name, t.description, inputSchema)
1507
+ };
1508
+ });
1509
+ const totalTokens = tools.filter((t) => t.enabled).reduce((sum, t) => sum + t.estimatedTokens, 0);
1510
+ entry.client = client;
1511
+ entry.transport = transport;
1512
+ entry.state = { name, config: entry.config, status: "connected", tools, estimatedTokens: totalTokens };
1513
+ logger.info("Connected to MCP server", { name, toolCount: tools.length });
1514
+ this.onServersChanged?.();
1515
+ } catch (error) {
1516
+ const msg = error instanceof Error ? error.message : String(error);
1517
+ logger.error("Failed to connect MCP server", { name, error: msg });
1518
+ entry.state = { name, config: entry.config, status: "error", tools: [], estimatedTokens: 0, error: msg };
1519
+ this.onServersChanged?.();
1785
1520
  }
1786
1521
  }
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]) {
1522
+ async disconnectServer(name) {
1523
+ const entry = this.servers.get(name);
1524
+ if (!entry) return;
1797
1525
  try {
1798
- const files = (await readdir(dir)).filter((f) => f.endsWith(WORKFLOW_EXTENSION));
1799
- return files.map((f) => f.replace(WORKFLOW_EXTENSION, ""));
1526
+ await entry.client?.close();
1800
1527
  } catch {
1801
1528
  }
1529
+ entry.client = void 0;
1530
+ entry.transport = null;
1531
+ entry.state = { name, config: entry.config, status: "disconnected", tools: [], estimatedTokens: 0 };
1802
1532
  }
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;
1533
+ async disconnectAll() {
1534
+ for (const name of this.servers.keys()) {
1535
+ await this.disconnectServer(name);
2028
1536
  }
2029
1537
  }
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
- );
1538
+ async reconnectServer(name) {
1539
+ await this.connectServer(name);
2056
1540
  }
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
- }
1541
+ getServer(name) {
1542
+ return this.servers.get(name)?.state;
2103
1543
  }
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
- });
1544
+ getAllServers() {
1545
+ return Array.from(this.servers.values()).map((e) => e.state);
2126
1546
  }
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
- }
1547
+ getToolDefinitions() {
1548
+ const defs = [];
1549
+ for (const [, entry] of this.servers) {
1550
+ for (const tool of entry.state.tools) {
1551
+ if (!tool.enabled) continue;
1552
+ defs.push({
1553
+ type: "function",
1554
+ function: {
1555
+ name: `${entry.state.name}_${tool.name}`,
1556
+ description: tool.description ?? "",
1557
+ parameters: tool.inputSchema
2235
1558
  }
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
1559
  });
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
1560
  }
2355
1561
  }
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;
1562
+ return defs;
2508
1563
  }
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
- }
2621
- }
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 });
2628
- }
2629
- }
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 () => {
1564
+ async callTool(serverName, toolName, args) {
1565
+ const entry = this.servers.get(serverName);
1566
+ if (!entry) return { success: false, error: `MCP server '${serverName}' not found` };
1567
+ if (!entry.client) return { success: false, error: `MCP server '${serverName}' is not connected` };
2668
1568
  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
- }
2683
- }
2684
- }
2685
- } catch {
2686
- }
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
- };
2712
- }
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;
2722
- }
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
- }
2739
- }
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);
2800
- }
2801
- client.setModel(session.providerModel);
2802
- sessionLLMClients.set(sessionId, { key: cacheKey, client });
2803
- return client;
2804
- }
2805
- function getSessionStatsIdentity(sessionId) {
2806
- const session = sessionManager.getSession(sessionId);
2807
- if (!session?.providerId || !providerManager) {
2808
- return resolveStatsIdentity(getLLMClient(), getActiveProvider);
2809
- }
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
- }
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 }));
2826
- }
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
2834
- });
2835
- return;
2836
- }
2837
- client.sendQueue.push({ data, seq });
2838
- processSendQueue(client);
2839
- }
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);
2859
- }
2860
- }
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;
2894
- }
2895
- sessionManager.clearMessageQueue(sessionId);
2896
- const contextState = sessionManager.getContextState(sessionId);
2897
- sendFn(sessionId, createContextStateMessage(contextState));
2898
- }
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
- });
2915
- }
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
- }
3245
- }
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
- });
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
- }
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;
1569
+ const result = await entry.client.callTool({ name: toolName, arguments: args });
1570
+ const content = result.content;
1571
+ const textParts = content.filter((c) => c.type === "text").map((c) => c.text);
1572
+ return { success: !result.isError, output: textParts.join("\n") };
1573
+ } catch (error) {
1574
+ return { success: false, error: error instanceof Error ? error.message : String(error) };
1575
+ }
1576
+ }
1577
+ async setToolEnabled(serverName, toolName, enabled) {
1578
+ const entry = this.servers.get(serverName);
1579
+ if (!entry) throw new Error(`MCP server '${serverName}' not found`);
1580
+ const tool = entry.state.tools.find((t) => t.name === toolName);
1581
+ if (!tool) throw new Error(`Tool '${toolName}' not found on server '${serverName}'`);
1582
+ tool.enabled = enabled;
1583
+ entry.state.estimatedTokens = entry.state.tools.filter((t) => t.enabled).reduce((sum, t) => sum + t.estimatedTokens, 0);
1584
+ this.onServersChanged?.();
1585
+ }
1586
+ getToolFingerprint() {
1587
+ const parts = [];
1588
+ for (const [, entry] of this.servers) {
1589
+ for (const tool of entry.state.tools) {
1590
+ if (tool.enabled) {
1591
+ parts.push(`${entry.state.name}:${tool.name}`);
3378
1592
  }
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
- }
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
1593
  }
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
1594
  }
1595
+ return parts.sort().join(",");
3429
1596
  }
3430
- }
1597
+ };
3431
1598
 
3432
1599
  // src/server/lsp/server.ts
3433
- import { spawn as spawn4 } from "child_process";
1600
+ import { spawn } from "child_process";
3434
1601
  import { extname } from "path";
3435
1602
  import { createMessageConnection, StreamMessageReader, StreamMessageWriter } from "vscode-jsonrpc/node.js";
3436
1603
  var LSP = {
@@ -3492,7 +1659,7 @@ var LspServer = class {
3492
1659
  }
3493
1660
  this.state = "starting";
3494
1661
  try {
3495
- this.process = spawn4(this.commandPath, this.config.serverArgs, {
1662
+ this.process = spawn(this.commandPath, this.config.serverArgs, {
3496
1663
  cwd: this.workdir,
3497
1664
  stdio: ["pipe", "pipe", "pipe"],
3498
1665
  env: {
@@ -3758,20 +1925,20 @@ var LspServer = class {
3758
1925
  };
3759
1926
 
3760
1927
  // 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));
1928
+ import { access, constants } from "fs/promises";
1929
+ import { join, dirname } from "path";
1930
+ import { fileURLToPath } from "url";
1931
+ var __dirname = dirname(fileURLToPath(import.meta.url));
3765
1932
  function getBundledBinPaths() {
3766
1933
  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"));
1934
+ paths.push(join(process.cwd(), "node_modules", ".bin"));
1935
+ paths.push(join(__dirname, "..", "..", "..", "node_modules", ".bin"));
1936
+ paths.push(join(__dirname, "..", "..", "..", "..", "node_modules", ".bin"));
3770
1937
  return paths;
3771
1938
  }
3772
1939
  async function existsExecutable(path) {
3773
1940
  try {
3774
- await access2(path, constants2.X_OK);
1941
+ await access(path, constants.X_OK);
3775
1942
  return true;
3776
1943
  } catch {
3777
1944
  return false;
@@ -3785,13 +1952,13 @@ async function which(command, workdir) {
3785
1952
  return null;
3786
1953
  }
3787
1954
  for (const binDir of getBundledBinPaths()) {
3788
- const fullPath = join3(binDir, command);
1955
+ const fullPath = join(binDir, command);
3789
1956
  if (await existsExecutable(fullPath)) {
3790
1957
  return fullPath;
3791
1958
  }
3792
1959
  }
3793
1960
  if (workdir) {
3794
- const projectBin = join3(workdir, "node_modules", ".bin", command);
1961
+ const projectBin = join(workdir, "node_modules", ".bin", command);
3795
1962
  if (await existsExecutable(projectBin)) {
3796
1963
  return projectBin;
3797
1964
  }
@@ -3800,7 +1967,7 @@ async function which(command, workdir) {
3800
1967
  const paths = pathEnv.split(getPathSeparator());
3801
1968
  for (const dir of paths) {
3802
1969
  if (!dir) continue;
3803
- const fullPath = join3(dir, command);
1970
+ const fullPath = join(dir, command);
3804
1971
  if (await existsExecutable(fullPath)) {
3805
1972
  return fullPath;
3806
1973
  }
@@ -5021,8 +3188,8 @@ var SessionManager = class {
5021
3188
  updateExecutionState(sessionId, updates) {
5022
3189
  logger.debug("updateExecutionState called", { sessionId, updates });
5023
3190
  }
5024
- setCachedPrompt(sessionId, systemPrompt, hash) {
5025
- this.cachedPromptStore.set(sessionId, { systemPrompt, hash });
3191
+ setCachedPrompt(sessionId, systemPrompt, tools, hash) {
3192
+ this.cachedPromptStore.set(sessionId, { systemPrompt, tools, hash });
5026
3193
  }
5027
3194
  getCachedPrompt(sessionId) {
5028
3195
  return this.cachedPromptStore.get(sessionId);
@@ -5167,6 +3334,7 @@ var SessionManager = class {
5167
3334
  if (!hasCachedPrompt && eventState.cachedSystemPrompt && eventState.dynamicContextHash) {
5168
3335
  this.cachedPromptStore.set(dbSession.id, {
5169
3336
  systemPrompt: eventState.cachedSystemPrompt,
3337
+ tools: [],
5170
3338
  hash: eventState.dynamicContextHash
5171
3339
  });
5172
3340
  }
@@ -5193,7 +3361,7 @@ var SessionManager = class {
5193
3361
 
5194
3362
  // src/server/routes/crud-helpers.ts
5195
3363
  import { Router } from "express";
5196
- import { join as join4 } from "path";
3364
+ import { join as join2 } from "path";
5197
3365
  function computeOverrideIds(defaults, userItems) {
5198
3366
  return userItems.filter((u) => defaults.some((d) => d.metadata.id === u.metadata.id)).map((u) => u.metadata.id);
5199
3367
  }
@@ -5205,7 +3373,7 @@ function validateNameIdPrompt(body) {
5205
3373
  return null;
5206
3374
  }
5207
3375
  function getProjectItemPath(projectDir, dirName, id, ext) {
5208
- return join4(projectDir, ".openfox", dirName, `${id}${ext}`);
3376
+ return join2(projectDir, ".openfox", dirName, `${id}${ext}`);
5209
3377
  }
5210
3378
  async function isProjectItem(projectDir, dirName, id, ext) {
5211
3379
  if (!projectDir) return false;
@@ -5369,27 +3537,27 @@ function createSkillRoutes(configDir, projectDir) {
5369
3537
  }
5370
3538
 
5371
3539
  // 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";
3540
+ import { writeFile, mkdir, unlink } from "fs/promises";
3541
+ import { join as join3, dirname as dirname2 } from "path";
3542
+ import { fileURLToPath as fileURLToPath2 } from "url";
5375
3543
  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");
3544
+ var __bundleDir = dirname2(fileURLToPath2(import.meta.url));
3545
+ var DEFAULTS_DIR = join3(__bundleDir, "defaults");
3546
+ var DEFAULTS_DIR_ALT = join3(__bundleDir, "command-defaults");
5379
3547
  var COMMAND_EXTENSION = ".command.md";
5380
3548
  function getCommandsDir(configDir) {
5381
- return join5(configDir, "commands");
3549
+ return join3(configDir, "commands");
5382
3550
  }
5383
3551
  function getProjectCommandsDir(projectDir) {
5384
- return join5(projectDir, ".openfox", "commands");
3552
+ return join3(projectDir, ".openfox", "commands");
5385
3553
  }
5386
3554
  async function loadDefaultCommands() {
5387
- let defaults = await loadItemsFromDir(DEFAULTS_DIR2, {
3555
+ let defaults = await loadItemsFromDir(DEFAULTS_DIR, {
5388
3556
  extension: COMMAND_EXTENSION,
5389
3557
  logName: "command"
5390
3558
  });
5391
3559
  if (!defaults.length) {
5392
- defaults = await loadItemsFromDir(DEFAULTS_DIR_ALT2, {
3560
+ defaults = await loadItemsFromDir(DEFAULTS_DIR_ALT, {
5393
3561
  extension: COMMAND_EXTENSION,
5394
3562
  logName: "command"
5395
3563
  });
@@ -5426,9 +3594,9 @@ async function loadAllCommands(configDir, projectDir) {
5426
3594
  return Array.from(commandMap.values());
5427
3595
  }
5428
3596
  async function getDefaultCommandIds() {
5429
- const ids = await getDefaultIds(DEFAULTS_DIR2, COMMAND_EXTENSION);
3597
+ const ids = await getDefaultIds(DEFAULTS_DIR, COMMAND_EXTENSION);
5430
3598
  if (ids.length) return ids;
5431
- return getDefaultIds(DEFAULTS_DIR_ALT2, COMMAND_EXTENSION);
3599
+ return getDefaultIds(DEFAULTS_DIR_ALT, COMMAND_EXTENSION);
5432
3600
  }
5433
3601
  async function isDefaultCommand(commandId) {
5434
3602
  const defaultIds = await getDefaultCommandIds();
@@ -5438,19 +3606,19 @@ function findCommandById(commandId, commands) {
5438
3606
  return commands.find((c) => c.metadata.id === commandId);
5439
3607
  }
5440
3608
  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}`)))
3609
+ if (await pathExists(join3(getCommandsDir(configDir), `${commandId}${COMMAND_EXTENSION}`))) return true;
3610
+ if (projectDir && await pathExists(join3(getProjectCommandsDir(projectDir), `${commandId}${COMMAND_EXTENSION}`)))
5443
3611
  return true;
5444
3612
  return false;
5445
3613
  }
5446
3614
  async function saveCommand(configDir, command) {
5447
3615
  const commandsDir = getCommandsDir(configDir);
5448
3616
  if (!await pathExists(commandsDir)) {
5449
- await mkdir3(commandsDir, { recursive: true });
3617
+ await mkdir(commandsDir, { recursive: true });
5450
3618
  }
5451
- const filePath = join5(commandsDir, `${command.metadata.id}${COMMAND_EXTENSION}`);
3619
+ const filePath = join3(commandsDir, `${command.metadata.id}${COMMAND_EXTENSION}`);
5452
3620
  const content = matter.stringify(command.prompt, command.metadata);
5453
- await writeFile3(filePath, content, "utf-8");
3621
+ await writeFile(filePath, content, "utf-8");
5454
3622
  }
5455
3623
  async function saveCommandToProject(projectDir, command) {
5456
3624
  await saveItemToDir(
@@ -5468,9 +3636,9 @@ async function deleteCommand(configDir, commandId) {
5468
3636
  if (isDefault) {
5469
3637
  return { success: false, reason: "Cannot delete built-in defaults" };
5470
3638
  }
5471
- const filePath = join5(getCommandsDir(configDir), `${commandId}${COMMAND_EXTENSION}`);
3639
+ const filePath = join3(getCommandsDir(configDir), `${commandId}${COMMAND_EXTENSION}`);
5472
3640
  try {
5473
- await unlink2(filePath);
3641
+ await unlink(filePath);
5474
3642
  return { success: true };
5475
3643
  } catch {
5476
3644
  return { success: false };
@@ -5691,8 +3859,8 @@ function createTerminalRoutes() {
5691
3859
 
5692
3860
  // src/server/routes/directories.ts
5693
3861
  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";
3862
+ import { readdir } from "fs/promises";
3863
+ import { resolve, join as join4, dirname as dirname3, basename as basename2 } from "path";
5696
3864
  function createDirectoryRoutes() {
5697
3865
  const router = Router5();
5698
3866
  const DEFAULT_BASE_PATH = process.cwd();
@@ -5700,12 +3868,12 @@ function createDirectoryRoutes() {
5700
3868
  const path = req.query["path"] || DEFAULT_BASE_PATH;
5701
3869
  try {
5702
3870
  const resolvedPath = resolve(path);
5703
- const entries = await readdir2(resolvedPath, { withFileTypes: true });
3871
+ const entries = await readdir(resolvedPath, { withFileTypes: true });
5704
3872
  const directories = entries.filter((entry) => entry.isDirectory()).map((entry) => ({
5705
3873
  name: entry.name,
5706
- path: join6(resolvedPath, entry.name)
3874
+ path: join4(resolvedPath, entry.name)
5707
3875
  })).sort((a, b) => a.name.localeCompare(b.name));
5708
- const parent = dirname5(resolvedPath);
3876
+ const parent = dirname3(resolvedPath);
5709
3877
  const hasParent = parent !== resolvedPath;
5710
3878
  res.json({
5711
3879
  current: resolvedPath,
@@ -5728,10 +3896,10 @@ function createDirectoryRoutes() {
5728
3896
 
5729
3897
  // src/server/routes/auto-update.ts
5730
3898
  import { Router as Router6 } from "express";
5731
- import { spawn as spawn5 } from "child_process";
3899
+ import { spawn as spawn2 } from "child_process";
5732
3900
 
5733
3901
  // src/constants.ts
5734
- var VERSION = "2.0.13";
3902
+ var VERSION = "2.0.14";
5735
3903
 
5736
3904
  // src/server/routes/auto-update.ts
5737
3905
  var updateInProgress = false;
@@ -5755,7 +3923,7 @@ function createAutoUpdateRoutes(options = {}) {
5755
3923
  const current = VERSION;
5756
3924
  try {
5757
3925
  const latest = await new Promise((resolve3, reject) => {
5758
- const child = spawn5("npm", ["view", "openfox", "version"], {
3926
+ const child = spawn2("npm", ["view", "openfox", "version"], {
5759
3927
  stdio: ["ignore", "pipe", "pipe"]
5760
3928
  });
5761
3929
  let stdout = "";
@@ -5793,7 +3961,7 @@ function createAutoUpdateRoutes(options = {}) {
5793
3961
  updateInProgress = true;
5794
3962
  try {
5795
3963
  const isService = isRunningAsService();
5796
- const child = spawn5("bash", ["-c", "openfox update"], {
3964
+ const child = spawn2("bash", ["-c", "openfox update"], {
5797
3965
  stdio: ["ignore", "pipe", "pipe"]
5798
3966
  });
5799
3967
  let stdout = "";
@@ -5832,7 +4000,7 @@ function createAutoUpdateRoutes(options = {}) {
5832
4000
  return;
5833
4001
  }
5834
4002
  try {
5835
- const child = spawn5("bash", ["-c", "openfox service restart"], {
4003
+ const child = spawn2("bash", ["-c", "openfox service restart"], {
5836
4004
  detached: true,
5837
4005
  stdio: "ignore"
5838
4006
  });
@@ -5846,7 +4014,7 @@ function createAutoUpdateRoutes(options = {}) {
5846
4014
  }
5847
4015
 
5848
4016
  // src/server/index.ts
5849
- var __dirname2 = dirname6(fileURLToPath4(import.meta.url));
4017
+ var __dirname2 = dirname4(fileURLToPath3(import.meta.url));
5850
4018
  async function createServerHandle(config4) {
5851
4019
  setRuntimeConfig(config4);
5852
4020
  setLogLevel(config4.logging?.level ?? void 0, config4.mode);
@@ -5895,6 +4063,21 @@ async function createServerHandle(config4) {
5895
4063
  (err) => logger.error("LLM initialization failed", { error: err instanceof Error ? err.message : String(err) })
5896
4064
  );
5897
4065
  const toolRegistry = createToolRegistry();
4066
+ const mcpManager = new McpManager();
4067
+ const mcpServers = config4.mcpServers ?? {};
4068
+ Promise.all(
4069
+ Object.entries(mcpServers).map(
4070
+ ([name, serverConfig]) => mcpManager.addServer(name, serverConfig).catch((err) => {
4071
+ logger.warn("Failed to connect MCP server on startup", { name, error: String(err) });
4072
+ })
4073
+ )
4074
+ ).then(() => {
4075
+ const mcpTools = createMcpTools(mcpManager);
4076
+ if (mcpTools.length > 0) {
4077
+ setMcpTools(mcpTools);
4078
+ logger.info("MCP tools registered", { count: mcpTools.length });
4079
+ }
4080
+ });
5898
4081
  const app = express();
5899
4082
  app.use(cors());
5900
4083
  app.use(express.json({ limit: "10mb" }));
@@ -6083,7 +4266,7 @@ async function createServerHandle(config4) {
6083
4266
  app.get("/api/sessions/:id", async (req, res) => {
6084
4267
  const { getEventStore: getEventStore2 } = await import("./events-4K52FKPR.js");
6085
4268
  const { buildMessagesFromStoredEvents } = await import("./folding-QIBNKYA6.js");
6086
- const { getPendingQuestionsForSession } = await import("./tools-THXBQJ7A.js");
4269
+ const { getPendingQuestionsForSession } = await import("./tools-DUC54GEH.js");
6087
4270
  const session = sessionManager.getSession(req.params.id);
6088
4271
  if (!session) {
6089
4272
  return res.status(404).json({ error: "Session not found" });
@@ -6126,7 +4309,7 @@ async function createServerHandle(config4) {
6126
4309
  return res.status(400).json({ error: "providerId is required" });
6127
4310
  }
6128
4311
  sessionManager.setSessionProvider(sessionId, providerId, model ?? "auto");
6129
- const { loadGlobalConfig, saveGlobalConfig, setDefaultModelSelection } = await import("./config-BU66P4KX.js");
4312
+ const { loadGlobalConfig, saveGlobalConfig, setDefaultModelSelection } = await import("./config-YFHA5MWV.js");
6130
4313
  const globalConfig = await loadGlobalConfig(config4.mode ?? "production");
6131
4314
  const updatedConfig = setDefaultModelSelection(globalConfig, providerId, model ?? "auto");
6132
4315
  await saveGlobalConfig(config4.mode ?? "production", updatedConfig);
@@ -6214,7 +4397,7 @@ async function createServerHandle(config4) {
6214
4397
  if (!callId || approved === void 0) {
6215
4398
  return res.status(400).json({ error: "callId and approved are required" });
6216
4399
  }
6217
- const { providePathConfirmation } = await import("./tools-THXBQJ7A.js");
4400
+ const { providePathConfirmation } = await import("./tools-DUC54GEH.js");
6218
4401
  const result = providePathConfirmation(callId, approved, alwaysAllow);
6219
4402
  if (!result.found) {
6220
4403
  return res.status(404).json({ error: "No pending path confirmation with that ID" });
@@ -6222,7 +4405,7 @@ async function createServerHandle(config4) {
6222
4405
  const { getEventStore: getEventStore2 } = await import("./events-4K52FKPR.js");
6223
4406
  const { buildMessagesFromStoredEvents, foldPendingConfirmations } = await import("./folding-QIBNKYA6.js");
6224
4407
  const { createSessionStateMessage } = await import("./protocol-YYWMFR35.js");
6225
- const { getPendingQuestionsForSession } = await import("./tools-THXBQJ7A.js");
4408
+ const { getPendingQuestionsForSession } = await import("./tools-DUC54GEH.js");
6226
4409
  const eventStore = getEventStore2();
6227
4410
  const events = eventStore.getEvents(sessionId);
6228
4411
  const messages = buildMessagesFromStoredEvents(events);
@@ -6243,8 +4426,8 @@ async function createServerHandle(config4) {
6243
4426
  if (!skip && typeof answer !== "string") {
6244
4427
  return res.status(400).json({ error: "answer is required when not skipping" });
6245
4428
  }
6246
- const { provideAnswer: provideAnswer2 } = await import("./tools-THXBQJ7A.js");
6247
- const found = provideAnswer2(callId, answer ?? "", skip ?? false);
4429
+ const { provideAnswer } = await import("./tools-DUC54GEH.js");
4430
+ const found = provideAnswer(callId, answer ?? "", skip ?? false);
6248
4431
  if (!found) {
6249
4432
  return res.status(404).json({ error: "No pending question with that ID" });
6250
4433
  }
@@ -6279,8 +4462,8 @@ async function createServerHandle(config4) {
6279
4462
  if (!session) {
6280
4463
  return res.status(404).json({ error: "Session not found" });
6281
4464
  }
6282
- const { stopSessionExecution } = await import("./chat-handler-6NUNVXIU.js");
6283
- const { cancelQuestionsForSession, cancelPathConfirmationsForSession } = await import("./tools-THXBQJ7A.js");
4465
+ const { stopSessionExecution } = await import("./chat-handler-ITXCBFJK.js");
4466
+ const { cancelQuestionsForSession, cancelPathConfirmationsForSession } = await import("./tools-DUC54GEH.js");
6284
4467
  const queuedMessages = sessionManager.getQueueState(sessionId);
6285
4468
  sessionManager.clearMessageQueue(sessionId);
6286
4469
  stopSessionExecution(sessionId, sessionManager);
@@ -6391,7 +4574,7 @@ async function createServerHandle(config4) {
6391
4574
  let visionFallback;
6392
4575
  let globalWorkdir;
6393
4576
  try {
6394
- const { loadGlobalConfig, getVisionFallback } = await import("./config-BU66P4KX.js");
4577
+ const { loadGlobalConfig, getVisionFallback } = await import("./config-YFHA5MWV.js");
6395
4578
  const globalConfig = await loadGlobalConfig(config4.mode ?? "production");
6396
4579
  const fallback = getVisionFallback(globalConfig);
6397
4580
  if (fallback) {
@@ -6487,7 +4670,7 @@ async function createServerHandle(config4) {
6487
4670
  const apiKey = req.query["apiKey"];
6488
4671
  if (!url) return res.status(400).json({ error: "url is required" });
6489
4672
  try {
6490
- const { fetchModelsWithContext } = await import("./provider-manager-YA2WALTF.js");
4673
+ const { fetchModelsWithContext } = await import("./provider-manager-DETXLDKW.js");
6491
4674
  const models = await fetchModelsWithContext(url, apiKey);
6492
4675
  if (models.length === 0) {
6493
4676
  return res.status(404).json({ error: `No models found at ${buildModelsUrl(url)}`, url });
@@ -6557,7 +4740,7 @@ async function createServerHandle(config4) {
6557
4740
  return res.status(400).json({ error: "name, url, and backend are required" });
6558
4741
  }
6559
4742
  try {
6560
- const { loadGlobalConfig, saveGlobalConfig, addProvider, setDefaultModelSelection } = await import("./config-BU66P4KX.js");
4743
+ const { loadGlobalConfig, saveGlobalConfig, addProvider, setDefaultModelSelection } = await import("./config-YFHA5MWV.js");
6561
4744
  const globalConfig = await loadGlobalConfig(config4.mode ?? "production");
6562
4745
  const providerBackend = backend;
6563
4746
  const configWithProvider = addProvider(globalConfig, {
@@ -6592,7 +4775,7 @@ async function createServerHandle(config4) {
6592
4775
  app.post("/api/init/config", async (req, res) => {
6593
4776
  const { workdir, visionFallback } = req.body;
6594
4777
  try {
6595
- const { loadGlobalConfig, saveGlobalConfig } = await import("./config-BU66P4KX.js");
4778
+ const { loadGlobalConfig, saveGlobalConfig } = await import("./config-YFHA5MWV.js");
6596
4779
  const globalConfig = await loadGlobalConfig(config4.mode ?? "production");
6597
4780
  const updatedConfig = {
6598
4781
  ...globalConfig,
@@ -6620,7 +4803,7 @@ async function createServerHandle(config4) {
6620
4803
  });
6621
4804
  app.delete("/api/providers/:id", async (req, res) => {
6622
4805
  const { id } = req.params;
6623
- const { loadGlobalConfig, saveGlobalConfig, removeProvider } = await import("./config-BU66P4KX.js");
4806
+ const { loadGlobalConfig, saveGlobalConfig, removeProvider } = await import("./config-YFHA5MWV.js");
6624
4807
  const globalConfig = await loadGlobalConfig(config4.mode ?? "production");
6625
4808
  const updatedConfig = removeProvider(globalConfig, id);
6626
4809
  await saveGlobalConfig(config4.mode ?? "production", updatedConfig);
@@ -6631,7 +4814,7 @@ async function createServerHandle(config4) {
6631
4814
  const { id } = req.params;
6632
4815
  const { isLocal } = req.body;
6633
4816
  try {
6634
- const { loadGlobalConfig, saveGlobalConfig, updateProvider } = await import("./config-BU66P4KX.js");
4817
+ const { loadGlobalConfig, saveGlobalConfig, updateProvider } = await import("./config-YFHA5MWV.js");
6635
4818
  const globalConfig = await loadGlobalConfig(config4.mode ?? "production");
6636
4819
  const provider = globalConfig.providers.find((p) => p.id === id);
6637
4820
  if (!provider) {
@@ -6659,7 +4842,7 @@ async function createServerHandle(config4) {
6659
4842
  models: modelConfigs
6660
4843
  } = req.body;
6661
4844
  try {
6662
- const { loadGlobalConfig, saveGlobalConfig, updateProvider } = await import("./config-BU66P4KX.js");
4845
+ const { loadGlobalConfig, saveGlobalConfig, updateProvider } = await import("./config-YFHA5MWV.js");
6663
4846
  const globalConfig = await loadGlobalConfig(config4.mode ?? "production");
6664
4847
  const provider = globalConfig.providers.find((p) => p.id === id);
6665
4848
  if (!provider) {
@@ -6696,7 +4879,7 @@ async function createServerHandle(config4) {
6696
4879
  return res.status(400).json({ error: result.error });
6697
4880
  }
6698
4881
  const llmClient = getLLMClient();
6699
- const { loadGlobalConfig, saveGlobalConfig, setDefaultModelSelection } = await import("./config-BU66P4KX.js");
4882
+ const { loadGlobalConfig, saveGlobalConfig, setDefaultModelSelection } = await import("./config-YFHA5MWV.js");
6700
4883
  const globalConfig = await loadGlobalConfig(config4.mode ?? "production");
6701
4884
  const updatedConfig = setDefaultModelSelection(globalConfig, id, llmClient.getModel());
6702
4885
  await saveGlobalConfig(config4.mode ?? "production", updatedConfig);
@@ -6721,6 +4904,131 @@ async function createServerHandle(config4) {
6721
4904
  status: updatedProvider?.status ?? "unknown"
6722
4905
  });
6723
4906
  });
4907
+ async function rebuildMcpTools() {
4908
+ const { createMcpTools: createMcpTools2 } = await import("./tool-adapter-B7QP6NLA.js");
4909
+ const { setMcpTools: setMcpTools2 } = await import("./tools-DUC54GEH.js");
4910
+ const mcpTools = createMcpTools2(mcpManager);
4911
+ setMcpTools2(mcpTools);
4912
+ }
4913
+ app.get("/api/mcp/servers", (_req, res) => {
4914
+ const servers = mcpManager.getAllServers();
4915
+ res.json({ servers });
4916
+ });
4917
+ app.post("/api/mcp/servers/test", async (req, res) => {
4918
+ const { name, command, args, env } = req.body;
4919
+ if (!command) {
4920
+ return res.status(400).json({ error: "command is required" });
4921
+ }
4922
+ try {
4923
+ const testManager = new McpManager();
4924
+ const testConfig = {
4925
+ transport: "stdio",
4926
+ command,
4927
+ ...args && args.length > 0 ? { args } : {},
4928
+ ...env && Object.keys(env).length > 0 ? { env } : {}
4929
+ };
4930
+ await testManager.addServer(name ?? "test", testConfig);
4931
+ const server = testManager.getServer(name ?? "test");
4932
+ await testManager.disconnectAll();
4933
+ if (server?.status === "connected") {
4934
+ res.json({ success: true, tools: server.tools.map((t) => t.name) });
4935
+ } else {
4936
+ res.json({ success: false, error: server?.error ?? "Connection failed" });
4937
+ }
4938
+ } catch (error) {
4939
+ res.status(400).json({ success: false, error: error instanceof Error ? error.message : String(error) });
4940
+ }
4941
+ });
4942
+ app.post("/api/mcp/servers", async (req, res) => {
4943
+ const { name, transport, command, args, env, url } = req.body;
4944
+ if (!name) {
4945
+ return res.status(400).json({ error: "name is required" });
4946
+ }
4947
+ try {
4948
+ const serverCfg = {
4949
+ transport: transport ?? "stdio",
4950
+ ...command ? { command } : {},
4951
+ ...args && args.length > 0 ? { args } : {},
4952
+ ...env && Object.keys(env).length > 0 ? { env } : {},
4953
+ ...url ? { url } : {}
4954
+ };
4955
+ await mcpManager.addServer(name, serverCfg);
4956
+ const server = mcpManager.getServer(name);
4957
+ const { loadGlobalConfig, saveGlobalConfig } = await import("./config-YFHA5MWV.js");
4958
+ const globalConfig = await loadGlobalConfig(config4.mode ?? "production");
4959
+ const updatedMcpServers = { ...globalConfig.mcpServers ?? {}, [name]: serverCfg };
4960
+ await saveGlobalConfig(config4.mode ?? "production", {
4961
+ ...globalConfig,
4962
+ mcpServers: updatedMcpServers
4963
+ });
4964
+ await rebuildMcpTools();
4965
+ if (server) {
4966
+ const sessions = sessionManager.listSessions();
4967
+ for (const s of sessions) {
4968
+ sessionManager.setDynamicContextChanged(s.id, true);
4969
+ }
4970
+ }
4971
+ res.status(201).json({ server });
4972
+ } catch (error) {
4973
+ res.status(400).json({ error: error instanceof Error ? error.message : String(error) });
4974
+ }
4975
+ });
4976
+ app.delete("/api/mcp/servers/:name", async (req, res) => {
4977
+ const { name } = req.params;
4978
+ const server = mcpManager.getServer(name);
4979
+ if (!server) {
4980
+ return res.status(404).json({ error: `MCP server '${name}' not found` });
4981
+ }
4982
+ mcpManager.removeServer(name);
4983
+ const { loadGlobalConfig, saveGlobalConfig } = await import("./config-YFHA5MWV.js");
4984
+ const globalConfig = await loadGlobalConfig(config4.mode ?? "production");
4985
+ const updatedMcpServers = { ...globalConfig.mcpServers ?? {} };
4986
+ delete updatedMcpServers[name];
4987
+ await saveGlobalConfig(config4.mode ?? "production", {
4988
+ ...globalConfig,
4989
+ mcpServers: updatedMcpServers
4990
+ });
4991
+ await rebuildMcpTools();
4992
+ const sessions = sessionManager.listSessions();
4993
+ for (const s of sessions) {
4994
+ sessionManager.setDynamicContextChanged(s.id, true);
4995
+ }
4996
+ res.json({ success: true });
4997
+ });
4998
+ app.put("/api/mcp/servers/:name/tools/:toolName", async (req, res) => {
4999
+ const { name, toolName } = req.params;
5000
+ const { enabled } = req.body;
5001
+ if (enabled === void 0) {
5002
+ return res.status(400).json({ error: "enabled is required" });
5003
+ }
5004
+ try {
5005
+ await mcpManager.setToolEnabled(name, toolName, enabled);
5006
+ const server = mcpManager.getServer(name);
5007
+ if (server) {
5008
+ const { loadGlobalConfig, saveGlobalConfig } = await import("./config-YFHA5MWV.js");
5009
+ const globalConfig = await loadGlobalConfig(config4.mode ?? "production");
5010
+ const mcpServers2 = { ...globalConfig.mcpServers ?? {} };
5011
+ const serverCfg = mcpServers2[name];
5012
+ if (serverCfg) {
5013
+ const disabledTools = server.tools.filter((t) => !t.enabled).map((t) => t.name);
5014
+ const cfg = { ...serverCfg, ...disabledTools.length > 0 ? { disabledTools } : { disabledTools: void 0 } };
5015
+ mcpServers2[name] = cfg;
5016
+ await saveGlobalConfig(config4.mode ?? "production", {
5017
+ ...globalConfig,
5018
+ mcpServers: mcpServers2
5019
+ });
5020
+ }
5021
+ }
5022
+ await rebuildMcpTools();
5023
+ const sessions = sessionManager.listSessions();
5024
+ for (const s of sessions) {
5025
+ sessionManager.setDynamicContextChanged(s.id, true);
5026
+ }
5027
+ res.json({ success: true });
5028
+ } catch (error) {
5029
+ res.status(400).json({ error: error instanceof Error ? error.message : String(error) });
5030
+ }
5031
+ });
6724
5032
  const projectDir = config4.workdir;
6725
5033
  app.use("/api/skills", createSkillRoutes(configDir, projectDir));
6726
5034
  app.use("/api/commands", createCommandRoutes(configDir, projectDir));
@@ -6795,7 +5103,7 @@ async function createServerHandle(config4) {
6795
5103
  }
6796
5104
  });
6797
5105
  app.get("/fox.svg", (_req, res) => {
6798
- readFile3(join7(webDir, "fox.svg")).then((content) => {
5106
+ readFile(join5(webDir, "fox.svg")).then((content) => {
6799
5107
  res.set("Content-Type", "image/svg+xml");
6800
5108
  res.send(content);
6801
5109
  }).catch(() => {
@@ -6804,7 +5112,7 @@ async function createServerHandle(config4) {
6804
5112
  });
6805
5113
  app.use(
6806
5114
  "/sounds",
6807
- express.static(join7(webDir, "public", "sounds"), {
5115
+ express.static(join5(webDir, "public", "sounds"), {
6808
5116
  setHeaders: (res) => {
6809
5117
  res.set("Content-Type", "audio/mpeg");
6810
5118
  }
@@ -6813,13 +5121,13 @@ async function createServerHandle(config4) {
6813
5121
  app.use("/__inspect__.js", (_req, res) => {
6814
5122
  res.set("Access-Control-Allow-Origin", "*");
6815
5123
  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"));
5124
+ readFile(join5(webDir, "public", "__inspect__.js")).then((content) => res.send(content)).catch(() => res.status(404).send("Not found"));
6817
5125
  });
6818
5126
  app.get("/*path", (req, res) => {
6819
5127
  if (req.path.startsWith("/api/")) {
6820
5128
  return;
6821
5129
  }
6822
- readFile3(join7(webDir, "index.html"), "utf-8").then((indexHtml) => viteServer.transformIndexHtml(req.originalUrl, indexHtml)).then((transformed) => res.send(transformed)).catch((err) => {
5130
+ readFile(join5(webDir, "index.html"), "utf-8").then((indexHtml) => viteServer.transformIndexHtml(req.originalUrl, indexHtml)).then((transformed) => res.send(transformed)).catch((err) => {
6823
5131
  logger.error("Error serving index.html", { error: err });
6824
5132
  res.status(500).send("Server error");
6825
5133
  });
@@ -6830,7 +5138,7 @@ async function createServerHandle(config4) {
6830
5138
  const distWebDir = resolve2(__dirname2, "web");
6831
5139
  app.use(
6832
5140
  "/assets",
6833
- express.static(join7(distWebDir, "assets"), {
5141
+ express.static(join5(distWebDir, "assets"), {
6834
5142
  setHeaders: (res, filepath) => {
6835
5143
  if (filepath.endsWith(".css")) {
6836
5144
  res.set("Content-Type", "text/css");
@@ -6838,11 +5146,11 @@ async function createServerHandle(config4) {
6838
5146
  }
6839
5147
  })
6840
5148
  );
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")));
5149
+ app.use("/manifest.webmanifest", express.static(join5(distWebDir, "manifest.webmanifest")));
5150
+ app.use("/registerSW.js", express.static(join5(distWebDir, "registerSW.js")));
5151
+ app.use("/sw.js", express.static(join5(distWebDir, "sw.js")));
6844
5152
  app.get("/fox.svg", (_req, res) => {
6845
- readFile3(join7(distWebDir, "fox.svg")).then((content) => {
5153
+ readFile(join5(distWebDir, "fox.svg")).then((content) => {
6846
5154
  res.set("Content-Type", "image/svg+xml");
6847
5155
  res.send(content);
6848
5156
  }).catch(() => {
@@ -6851,7 +5159,7 @@ async function createServerHandle(config4) {
6851
5159
  });
6852
5160
  app.use(
6853
5161
  "/sounds",
6854
- express.static(join7(distWebDir, "sounds"), {
5162
+ express.static(join5(distWebDir, "sounds"), {
6855
5163
  setHeaders: (res) => {
6856
5164
  res.set("Content-Type", "audio/mpeg");
6857
5165
  }
@@ -6860,15 +5168,15 @@ async function createServerHandle(config4) {
6860
5168
  app.use("/__inspect__.js", (_req, res) => {
6861
5169
  res.set("Access-Control-Allow-Origin", "*");
6862
5170
  res.set("Content-Type", "application/javascript");
6863
- readFile3(join7(distWebDir, "__inspect__.js")).then((content) => res.send(content)).catch(() => res.status(404).send("Not found"));
5171
+ readFile(join5(distWebDir, "__inspect__.js")).then((content) => res.send(content)).catch(() => res.status(404).send("Not found"));
6864
5172
  });
6865
5173
  app.use("/__inspect__.js", (_req, res) => {
6866
5174
  res.set("Access-Control-Allow-Origin", "*");
6867
5175
  res.set("Content-Type", "application/javascript");
6868
- readFile3(join7(distWebDir, "__inspect__.js")).then((content) => res.send(content)).catch(() => res.status(404).send("Not found"));
5176
+ readFile(join5(distWebDir, "__inspect__.js")).then((content) => res.send(content)).catch(() => res.status(404).send("Not found"));
6869
5177
  });
6870
5178
  app.get("/", (_req, res) => {
6871
- readFile3(join7(distWebDir, "index.html"), "utf-8").then((content) => res.send(content)).catch(() => {
5179
+ readFile(join5(distWebDir, "index.html"), "utf-8").then((content) => res.send(content)).catch(() => {
6872
5180
  res.status(404).send("Web UI not built. Run `npm run build:web`");
6873
5181
  });
6874
5182
  });
@@ -6876,12 +5184,14 @@ async function createServerHandle(config4) {
6876
5184
  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
5185
  return;
6878
5186
  }
6879
- readFile3(join7(distWebDir, "index.html"), "utf-8").then((content) => res.send(content)).catch(() => {
5187
+ readFile(join5(distWebDir, "index.html"), "utf-8").then((content) => res.send(content)).catch(() => {
6880
5188
  res.status(404).send("Web UI not built");
6881
5189
  });
6882
5190
  });
6883
5191
  }
6884
5192
  const httpServer = createHttpServer(app);
5193
+ const { setMcpManagerForWs } = await import("./server-EA6W7YIC.js");
5194
+ setMcpManagerForWs(mcpManager);
6885
5195
  const wssExports = createWebSocketServer(
6886
5196
  httpServer,
6887
5197
  config4,
@@ -6891,7 +5201,7 @@ async function createServerHandle(config4) {
6891
5201
  providerManager
6892
5202
  );
6893
5203
  const wss = wssExports.wss;
6894
- const { QueueProcessor } = await import("./processor-GA5NG3T7.js");
5204
+ const { QueueProcessor } = await import("./processor-CCXZYLX6.js");
6895
5205
  const queueProcessor = new QueueProcessor({
6896
5206
  sessionManager,
6897
5207
  providerManager,
@@ -6931,6 +5241,7 @@ async function createServerHandle(config4) {
6931
5241
  logger.info("Shutting down...");
6932
5242
  void (async () => {
6933
5243
  await devServerManager.stopAll();
5244
+ await mcpManager.disconnectAll();
6934
5245
  const { stopAllInspectProxies } = await import("./inspect-proxy-42ZXL2R5.js");
6935
5246
  stopAllInspectProxies();
6936
5247
  const { cleanupAllProcesses } = await import("./store-OQ45KONN.js");
@@ -6965,4 +5276,4 @@ export {
6965
5276
  createServerHandle,
6966
5277
  createServer
6967
5278
  };
6968
- //# sourceMappingURL=chunk-VFRWE5X4.js.map
5279
+ //# sourceMappingURL=chunk-4U45A7B3.js.map