openfox 2.0.12 → 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-WF3MA65W.js → chunk-4U45A7B3.js} +386 -2072
  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-HYYFVYDV.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-5BSOEK54.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,1972 +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
- });
3213
- break;
3214
- }
3215
- case "context.checkDynamic": {
3216
- if (!client.activeSessionId) {
3217
- send(createErrorMessage("NO_SESSION", "No active session", message.id));
3218
- return;
3219
- }
3220
- const sessionId = client.activeSessionId;
3221
- const session = sessionManager.requireSession(sessionId);
3222
- (async () => {
3223
- try {
3224
- const { content: instructionContent } = await getAllInstructions(session.workdir, session.projectId);
3225
- const runtimeConfig = getRuntimeConfig();
3226
- const configDir = getGlobalConfigDir(runtimeConfig.mode ?? "production");
3227
- const skills = await getEnabledSkillMetadata(configDir, runtimeConfig.workdir);
3228
- const currentHash = computeDynamicContextHash(instructionContent, skills);
3229
- const cachedHash = sessionManager.getCachedPrompt(sessionId)?.hash;
3230
- if (cachedHash) {
3231
- if (currentHash !== cachedHash) {
3232
- if (!sessionManager.getDynamicContextChanged(sessionId)) {
3233
- sessionManager.setDynamicContextChanged(sessionId, true);
3234
- const newContextState = sessionManager.getContextState(sessionId);
3235
- sendForSession(sessionId, createContextStateMessage(newContextState));
3236
- }
3237
- } else if (sessionManager.getDynamicContextChanged(sessionId)) {
3238
- sessionManager.setDynamicContextChanged(sessionId, false);
3239
- const newContextState = sessionManager.getContextState(sessionId);
3240
- sendForSession(sessionId, createContextStateMessage(newContextState));
3241
- }
3242
- }
3243
- send({ type: "ack", payload: {}, id: message.id });
3244
- } catch (error) {
3245
- logger.error("Failed to check dynamic context", { error, sessionId });
3246
- send({ type: "ack", payload: {}, id: message.id });
3247
- }
3248
- })();
3249
- break;
3250
- }
3251
- case "context.applyDynamic": {
3252
- if (!client.activeSessionId) {
3253
- send(createErrorMessage("NO_SESSION", "No active session", message.id));
3254
- return;
3255
- }
3256
- const sessionId = client.activeSessionId;
3257
- const session = sessionManager.requireSession(sessionId);
3258
- if (session.isRunning) {
3259
- send(createErrorMessage("SESSION_RUNNING", "Cannot apply dynamic context while session is running", message.id));
3260
- return;
3261
- }
3262
- ;
3263
- (async () => {
3264
- try {
3265
- const { content: instructionContent } = await getAllInstructions(session.workdir, session.projectId);
3266
- const runtimeConfig = getRuntimeConfig();
3267
- const configDir = getGlobalConfigDir(runtimeConfig.mode ?? "production");
3268
- const skills = await getEnabledSkillMetadata(configDir, runtimeConfig.workdir);
3269
- const dynamicHash = computeDynamicContextHash(instructionContent, skills);
3270
- const allAgents = await loadAllAgentsDefault();
3271
- const subAgentDefs = getSubAgents(allAgents);
3272
- const systemPrompt = buildTopLevelSystemPrompt(
3273
- session.workdir,
3274
- instructionContent || void 0,
3275
- skills,
3276
- subAgentDefs
3277
- );
3278
- sessionManager.setCachedPrompt(sessionId, systemPrompt, dynamicHash);
3279
- sessionManager.setDynamicContextChanged(sessionId, false);
3280
- const newContextState = sessionManager.getContextState(sessionId);
3281
- sendForSession(sessionId, createContextStateMessage(newContextState));
3282
- send({ type: "ack", payload: {}, id: message.id });
3283
- } catch (error) {
3284
- logger.error("Failed to apply dynamic context", { error, sessionId });
3285
- sendForSession(
3286
- sessionId,
3287
- createChatErrorMessage(
3288
- `Failed to apply dynamic context: ${error instanceof Error ? error.message : "Unknown error"}`,
3289
- true
3290
- )
3291
- );
3292
- send({ type: "ack", payload: {}, id: message.id });
3293
- }
3294
- })();
3295
- break;
3296
- }
3297
- // =========================================================================
3298
- // Runner (Auto-Loop)
3299
- // =========================================================================
3300
- case "runner.launch": {
3301
- if (!client.activeSessionId) {
3302
- send(createErrorMessage("NO_SESSION", "No active session", message.id));
3303
- return;
3304
- }
3305
- const session = sessionManager.requireSession(client.activeSessionId);
3306
- if (session.isRunning) {
3307
- const launchPayload2 = message.payload;
3308
- const content = launchPayload2?.content ?? "";
3309
- const attachments = launchPayload2?.attachments;
3310
- const workflowId = launchPayload2?.workflowId;
3311
- let fullContent = content;
3312
- if (workflowId) {
3313
- const workflowInfo = `// Workflow: ${workflowId}`;
3314
- fullContent = content ? `${workflowInfo}
3315
-
3316
- ${content}` : workflowInfo;
3317
- }
3318
- sessionManager.queueMessage(client.activeSessionId, "asap", fullContent, attachments, "workflow-launch");
3319
- const queueState = sessionManager.getQueueState(client.activeSessionId);
3320
- send({
3321
- type: "queue.state",
3322
- payload: { success: true, queueState },
3323
- id: message.id
3324
- });
3325
- return;
3326
- }
3327
- const launchPayloadEarly = message.payload;
3328
- const pendingCriteria = session.criteria.filter((c) => c.status.type !== "passed");
3329
- if (!launchPayloadEarly?.workflowId && pendingCriteria.length === 0) {
3330
- send(createErrorMessage("NO_WORK", "No pending criteria to work on", message.id));
3331
- return;
3332
- }
3333
- const sessionId = client.activeSessionId;
3334
- if (session.phase === "blocked") {
3335
- logger.info("User launched runner - resetting blocked state", { sessionId });
3336
- sessionManager.setPhase(sessionId, "build");
3337
- }
3338
- const launchPayload = message.payload;
3339
- const launchAttachments = launchPayload?.attachments;
3340
- const hasUserContent = launchPayload?.content && typeof launchPayload.content === "string" && launchPayload.content.trim();
3341
- const hasUserAttachments = launchAttachments && launchAttachments.length > 0;
3342
- const hasUserMessage = hasUserContent || hasUserAttachments;
3343
- sessionManager.setRunning(sessionId, true);
3344
- sendForSession(sessionId, createSessionRunningMessage(true));
3345
- const controller = new AbortController();
3346
- const existingController = activeAgents.get(sessionId);
3347
- if (existingController) {
3348
- logger.warn("Aborting existing agent before starting new one", { sessionId });
3349
- existingController.abort();
3350
- }
3351
- activeAgents.set(sessionId, controller);
3352
- send({ type: "ack", payload: {}, id: message.id });
3353
- ensureEventStoreSubscription(sessionId);
3354
- logger.info("Runner launching", { sessionId, pendingCriteria: pendingCriteria.length });
3355
- runOrchestrator({
3356
- sessionManager,
3357
- sessionId,
3358
- llmClient: llmForSession(sessionId),
3359
- statsIdentity: statsForSession(sessionId),
3360
- injectWorkflowKickoff: !hasUserMessage,
3361
- ...launchPayload?.workflowId ? { workflowId: launchPayload.workflowId } : {},
3362
- ...launchPayload?.subGroup ? { subGroup: launchPayload.subGroup } : {},
3363
- ...hasUserMessage ? {
3364
- userMessage: {
3365
- content: hasUserContent ? launchPayload.content : "",
3366
- ...hasUserAttachments ? { attachments: launchAttachments } : {}
3367
- }
3368
- } : {},
3369
- signal: controller.signal,
3370
- onMessage: (msg) => sendForSession(sessionId, msg)
3371
- // For path confirmation dialogs
3372
- }).catch((error) => {
3373
- if (error instanceof Error && error.message === "Aborted") {
3374
- 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}`);
3375
1592
  }
3376
- logger.error("Runner error", { error, sessionId });
3377
- }).finally(() => {
3378
- sessionManager.setRunning(sessionId, false);
3379
- sendForSession(sessionId, createSessionRunningMessage(false));
3380
- cleanupAfterTurn(sessionId, controller, sendForSession, true);
3381
- });
3382
- break;
3383
- }
3384
- // =========================================================================
3385
- // Path Confirmation
3386
- // =========================================================================
3387
- case "path.confirm": {
3388
- send(
3389
- createErrorMessage(
3390
- "DEPRECATED",
3391
- "path.confirm removed. Use REST API: POST /api/sessions/:id/confirm-path",
3392
- message.id
3393
- )
3394
- );
3395
- break;
3396
- }
3397
- // =========================================================================
3398
- // Ask User
3399
- // =========================================================================
3400
- case "ask.answer": {
3401
- if (!client.activeSessionId) {
3402
- send(createErrorMessage("NO_SESSION", "No active session", message.id));
3403
- return;
3404
- }
3405
- if (!isAskAnswerPayload(message.payload)) {
3406
- send(createErrorMessage("INVALID_PAYLOAD", "Invalid ask.answer payload", message.id));
3407
- return;
3408
- }
3409
- const { callId, answer, skip } = message.payload;
3410
- const found = provideAnswer(callId, answer, skip);
3411
- if (!found) {
3412
- send(createErrorMessage("NOT_FOUND", "No pending question with that ID", message.id));
3413
- return;
3414
1593
  }
3415
- logger.debug("Ask user answer received", {
3416
- sessionId: client.activeSessionId,
3417
- callId,
3418
- answerLength: answer.length
3419
- });
3420
- send({ type: "ack", payload: {}, id: message.id });
3421
- break;
3422
- }
3423
- default: {
3424
- send(createErrorMessage("UNKNOWN_MESSAGE", `Unknown message type: ${message.type}`, message.id));
3425
1594
  }
1595
+ return parts.sort().join(",");
3426
1596
  }
3427
- }
1597
+ };
3428
1598
 
3429
1599
  // src/server/lsp/server.ts
3430
- import { spawn as spawn4 } from "child_process";
1600
+ import { spawn } from "child_process";
3431
1601
  import { extname } from "path";
3432
1602
  import { createMessageConnection, StreamMessageReader, StreamMessageWriter } from "vscode-jsonrpc/node.js";
3433
1603
  var LSP = {
@@ -3489,7 +1659,7 @@ var LspServer = class {
3489
1659
  }
3490
1660
  this.state = "starting";
3491
1661
  try {
3492
- this.process = spawn4(this.commandPath, this.config.serverArgs, {
1662
+ this.process = spawn(this.commandPath, this.config.serverArgs, {
3493
1663
  cwd: this.workdir,
3494
1664
  stdio: ["pipe", "pipe", "pipe"],
3495
1665
  env: {
@@ -3755,20 +1925,20 @@ var LspServer = class {
3755
1925
  };
3756
1926
 
3757
1927
  // src/server/utils/which.ts
3758
- import { access as access2, constants as constants2 } from "fs/promises";
3759
- import { join as join3, dirname as dirname3 } from "path";
3760
- import { fileURLToPath as fileURLToPath2 } from "url";
3761
- 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));
3762
1932
  function getBundledBinPaths() {
3763
1933
  const paths = [];
3764
- paths.push(join3(process.cwd(), "node_modules", ".bin"));
3765
- paths.push(join3(__dirname, "..", "..", "..", "node_modules", ".bin"));
3766
- 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"));
3767
1937
  return paths;
3768
1938
  }
3769
1939
  async function existsExecutable(path) {
3770
1940
  try {
3771
- await access2(path, constants2.X_OK);
1941
+ await access(path, constants.X_OK);
3772
1942
  return true;
3773
1943
  } catch {
3774
1944
  return false;
@@ -3782,13 +1952,13 @@ async function which(command, workdir) {
3782
1952
  return null;
3783
1953
  }
3784
1954
  for (const binDir of getBundledBinPaths()) {
3785
- const fullPath = join3(binDir, command);
1955
+ const fullPath = join(binDir, command);
3786
1956
  if (await existsExecutable(fullPath)) {
3787
1957
  return fullPath;
3788
1958
  }
3789
1959
  }
3790
1960
  if (workdir) {
3791
- const projectBin = join3(workdir, "node_modules", ".bin", command);
1961
+ const projectBin = join(workdir, "node_modules", ".bin", command);
3792
1962
  if (await existsExecutable(projectBin)) {
3793
1963
  return projectBin;
3794
1964
  }
@@ -3797,7 +1967,7 @@ async function which(command, workdir) {
3797
1967
  const paths = pathEnv.split(getPathSeparator());
3798
1968
  for (const dir of paths) {
3799
1969
  if (!dir) continue;
3800
- const fullPath = join3(dir, command);
1970
+ const fullPath = join(dir, command);
3801
1971
  if (await existsExecutable(fullPath)) {
3802
1972
  return fullPath;
3803
1973
  }
@@ -5018,8 +3188,8 @@ var SessionManager = class {
5018
3188
  updateExecutionState(sessionId, updates) {
5019
3189
  logger.debug("updateExecutionState called", { sessionId, updates });
5020
3190
  }
5021
- setCachedPrompt(sessionId, systemPrompt, hash) {
5022
- this.cachedPromptStore.set(sessionId, { systemPrompt, hash });
3191
+ setCachedPrompt(sessionId, systemPrompt, tools, hash) {
3192
+ this.cachedPromptStore.set(sessionId, { systemPrompt, tools, hash });
5023
3193
  }
5024
3194
  getCachedPrompt(sessionId) {
5025
3195
  return this.cachedPromptStore.get(sessionId);
@@ -5164,6 +3334,7 @@ var SessionManager = class {
5164
3334
  if (!hasCachedPrompt && eventState.cachedSystemPrompt && eventState.dynamicContextHash) {
5165
3335
  this.cachedPromptStore.set(dbSession.id, {
5166
3336
  systemPrompt: eventState.cachedSystemPrompt,
3337
+ tools: [],
5167
3338
  hash: eventState.dynamicContextHash
5168
3339
  });
5169
3340
  }
@@ -5190,7 +3361,7 @@ var SessionManager = class {
5190
3361
 
5191
3362
  // src/server/routes/crud-helpers.ts
5192
3363
  import { Router } from "express";
5193
- import { join as join4 } from "path";
3364
+ import { join as join2 } from "path";
5194
3365
  function computeOverrideIds(defaults, userItems) {
5195
3366
  return userItems.filter((u) => defaults.some((d) => d.metadata.id === u.metadata.id)).map((u) => u.metadata.id);
5196
3367
  }
@@ -5202,7 +3373,7 @@ function validateNameIdPrompt(body) {
5202
3373
  return null;
5203
3374
  }
5204
3375
  function getProjectItemPath(projectDir, dirName, id, ext) {
5205
- return join4(projectDir, ".openfox", dirName, `${id}${ext}`);
3376
+ return join2(projectDir, ".openfox", dirName, `${id}${ext}`);
5206
3377
  }
5207
3378
  async function isProjectItem(projectDir, dirName, id, ext) {
5208
3379
  if (!projectDir) return false;
@@ -5366,27 +3537,27 @@ function createSkillRoutes(configDir, projectDir) {
5366
3537
  }
5367
3538
 
5368
3539
  // src/server/commands/registry.ts
5369
- import { writeFile as writeFile3, mkdir as mkdir3, unlink as unlink2 } from "fs/promises";
5370
- import { join as join5, dirname as dirname4 } from "path";
5371
- 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";
5372
3543
  import matter from "gray-matter";
5373
- var __bundleDir2 = dirname4(fileURLToPath3(import.meta.url));
5374
- var DEFAULTS_DIR2 = join5(__bundleDir2, "defaults");
5375
- 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");
5376
3547
  var COMMAND_EXTENSION = ".command.md";
5377
3548
  function getCommandsDir(configDir) {
5378
- return join5(configDir, "commands");
3549
+ return join3(configDir, "commands");
5379
3550
  }
5380
3551
  function getProjectCommandsDir(projectDir) {
5381
- return join5(projectDir, ".openfox", "commands");
3552
+ return join3(projectDir, ".openfox", "commands");
5382
3553
  }
5383
3554
  async function loadDefaultCommands() {
5384
- let defaults = await loadItemsFromDir(DEFAULTS_DIR2, {
3555
+ let defaults = await loadItemsFromDir(DEFAULTS_DIR, {
5385
3556
  extension: COMMAND_EXTENSION,
5386
3557
  logName: "command"
5387
3558
  });
5388
3559
  if (!defaults.length) {
5389
- defaults = await loadItemsFromDir(DEFAULTS_DIR_ALT2, {
3560
+ defaults = await loadItemsFromDir(DEFAULTS_DIR_ALT, {
5390
3561
  extension: COMMAND_EXTENSION,
5391
3562
  logName: "command"
5392
3563
  });
@@ -5423,9 +3594,9 @@ async function loadAllCommands(configDir, projectDir) {
5423
3594
  return Array.from(commandMap.values());
5424
3595
  }
5425
3596
  async function getDefaultCommandIds() {
5426
- const ids = await getDefaultIds(DEFAULTS_DIR2, COMMAND_EXTENSION);
3597
+ const ids = await getDefaultIds(DEFAULTS_DIR, COMMAND_EXTENSION);
5427
3598
  if (ids.length) return ids;
5428
- return getDefaultIds(DEFAULTS_DIR_ALT2, COMMAND_EXTENSION);
3599
+ return getDefaultIds(DEFAULTS_DIR_ALT, COMMAND_EXTENSION);
5429
3600
  }
5430
3601
  async function isDefaultCommand(commandId) {
5431
3602
  const defaultIds = await getDefaultCommandIds();
@@ -5435,19 +3606,19 @@ function findCommandById(commandId, commands) {
5435
3606
  return commands.find((c) => c.metadata.id === commandId);
5436
3607
  }
5437
3608
  async function commandExists(configDir, commandId, projectDir) {
5438
- if (await pathExists(join5(getCommandsDir(configDir), `${commandId}${COMMAND_EXTENSION}`))) return true;
5439
- 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}`)))
5440
3611
  return true;
5441
3612
  return false;
5442
3613
  }
5443
3614
  async function saveCommand(configDir, command) {
5444
3615
  const commandsDir = getCommandsDir(configDir);
5445
3616
  if (!await pathExists(commandsDir)) {
5446
- await mkdir3(commandsDir, { recursive: true });
3617
+ await mkdir(commandsDir, { recursive: true });
5447
3618
  }
5448
- const filePath = join5(commandsDir, `${command.metadata.id}${COMMAND_EXTENSION}`);
3619
+ const filePath = join3(commandsDir, `${command.metadata.id}${COMMAND_EXTENSION}`);
5449
3620
  const content = matter.stringify(command.prompt, command.metadata);
5450
- await writeFile3(filePath, content, "utf-8");
3621
+ await writeFile(filePath, content, "utf-8");
5451
3622
  }
5452
3623
  async function saveCommandToProject(projectDir, command) {
5453
3624
  await saveItemToDir(
@@ -5465,9 +3636,9 @@ async function deleteCommand(configDir, commandId) {
5465
3636
  if (isDefault) {
5466
3637
  return { success: false, reason: "Cannot delete built-in defaults" };
5467
3638
  }
5468
- const filePath = join5(getCommandsDir(configDir), `${commandId}${COMMAND_EXTENSION}`);
3639
+ const filePath = join3(getCommandsDir(configDir), `${commandId}${COMMAND_EXTENSION}`);
5469
3640
  try {
5470
- await unlink2(filePath);
3641
+ await unlink(filePath);
5471
3642
  return { success: true };
5472
3643
  } catch {
5473
3644
  return { success: false };
@@ -5688,8 +3859,8 @@ function createTerminalRoutes() {
5688
3859
 
5689
3860
  // src/server/routes/directories.ts
5690
3861
  import { Router as Router5 } from "express";
5691
- import { readdir as readdir2 } from "fs/promises";
5692
- 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";
5693
3864
  function createDirectoryRoutes() {
5694
3865
  const router = Router5();
5695
3866
  const DEFAULT_BASE_PATH = process.cwd();
@@ -5697,12 +3868,12 @@ function createDirectoryRoutes() {
5697
3868
  const path = req.query["path"] || DEFAULT_BASE_PATH;
5698
3869
  try {
5699
3870
  const resolvedPath = resolve(path);
5700
- const entries = await readdir2(resolvedPath, { withFileTypes: true });
3871
+ const entries = await readdir(resolvedPath, { withFileTypes: true });
5701
3872
  const directories = entries.filter((entry) => entry.isDirectory()).map((entry) => ({
5702
3873
  name: entry.name,
5703
- path: join6(resolvedPath, entry.name)
3874
+ path: join4(resolvedPath, entry.name)
5704
3875
  })).sort((a, b) => a.name.localeCompare(b.name));
5705
- const parent = dirname5(resolvedPath);
3876
+ const parent = dirname3(resolvedPath);
5706
3877
  const hasParent = parent !== resolvedPath;
5707
3878
  res.json({
5708
3879
  current: resolvedPath,
@@ -5725,10 +3896,10 @@ function createDirectoryRoutes() {
5725
3896
 
5726
3897
  // src/server/routes/auto-update.ts
5727
3898
  import { Router as Router6 } from "express";
5728
- import { spawn as spawn5 } from "child_process";
3899
+ import { spawn as spawn2 } from "child_process";
5729
3900
 
5730
3901
  // src/constants.ts
5731
- var VERSION = "2.0.12";
3902
+ var VERSION = "2.0.14";
5732
3903
 
5733
3904
  // src/server/routes/auto-update.ts
5734
3905
  var updateInProgress = false;
@@ -5752,7 +3923,7 @@ function createAutoUpdateRoutes(options = {}) {
5752
3923
  const current = VERSION;
5753
3924
  try {
5754
3925
  const latest = await new Promise((resolve3, reject) => {
5755
- const child = spawn5("npm", ["view", "openfox", "version"], {
3926
+ const child = spawn2("npm", ["view", "openfox", "version"], {
5756
3927
  stdio: ["ignore", "pipe", "pipe"]
5757
3928
  });
5758
3929
  let stdout = "";
@@ -5790,7 +3961,7 @@ function createAutoUpdateRoutes(options = {}) {
5790
3961
  updateInProgress = true;
5791
3962
  try {
5792
3963
  const isService = isRunningAsService();
5793
- const child = spawn5("bash", ["-c", "openfox update"], {
3964
+ const child = spawn2("bash", ["-c", "openfox update"], {
5794
3965
  stdio: ["ignore", "pipe", "pipe"]
5795
3966
  });
5796
3967
  let stdout = "";
@@ -5829,7 +4000,7 @@ function createAutoUpdateRoutes(options = {}) {
5829
4000
  return;
5830
4001
  }
5831
4002
  try {
5832
- const child = spawn5("bash", ["-c", "openfox service restart"], {
4003
+ const child = spawn2("bash", ["-c", "openfox service restart"], {
5833
4004
  detached: true,
5834
4005
  stdio: "ignore"
5835
4006
  });
@@ -5843,7 +4014,7 @@ function createAutoUpdateRoutes(options = {}) {
5843
4014
  }
5844
4015
 
5845
4016
  // src/server/index.ts
5846
- var __dirname2 = dirname6(fileURLToPath4(import.meta.url));
4017
+ var __dirname2 = dirname4(fileURLToPath3(import.meta.url));
5847
4018
  async function createServerHandle(config4) {
5848
4019
  setRuntimeConfig(config4);
5849
4020
  setLogLevel(config4.logging?.level ?? void 0, config4.mode);
@@ -5892,6 +4063,21 @@ async function createServerHandle(config4) {
5892
4063
  (err) => logger.error("LLM initialization failed", { error: err instanceof Error ? err.message : String(err) })
5893
4064
  );
5894
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
+ });
5895
4081
  const app = express();
5896
4082
  app.use(cors());
5897
4083
  app.use(express.json({ limit: "10mb" }));
@@ -6080,7 +4266,7 @@ async function createServerHandle(config4) {
6080
4266
  app.get("/api/sessions/:id", async (req, res) => {
6081
4267
  const { getEventStore: getEventStore2 } = await import("./events-4K52FKPR.js");
6082
4268
  const { buildMessagesFromStoredEvents } = await import("./folding-QIBNKYA6.js");
6083
- const { getPendingQuestionsForSession } = await import("./tools-THXBQJ7A.js");
4269
+ const { getPendingQuestionsForSession } = await import("./tools-DUC54GEH.js");
6084
4270
  const session = sessionManager.getSession(req.params.id);
6085
4271
  if (!session) {
6086
4272
  return res.status(404).json({ error: "Session not found" });
@@ -6123,7 +4309,7 @@ async function createServerHandle(config4) {
6123
4309
  return res.status(400).json({ error: "providerId is required" });
6124
4310
  }
6125
4311
  sessionManager.setSessionProvider(sessionId, providerId, model ?? "auto");
6126
- const { loadGlobalConfig, saveGlobalConfig, setDefaultModelSelection } = await import("./config-BU66P4KX.js");
4312
+ const { loadGlobalConfig, saveGlobalConfig, setDefaultModelSelection } = await import("./config-YFHA5MWV.js");
6127
4313
  const globalConfig = await loadGlobalConfig(config4.mode ?? "production");
6128
4314
  const updatedConfig = setDefaultModelSelection(globalConfig, providerId, model ?? "auto");
6129
4315
  await saveGlobalConfig(config4.mode ?? "production", updatedConfig);
@@ -6211,7 +4397,7 @@ async function createServerHandle(config4) {
6211
4397
  if (!callId || approved === void 0) {
6212
4398
  return res.status(400).json({ error: "callId and approved are required" });
6213
4399
  }
6214
- const { providePathConfirmation } = await import("./tools-THXBQJ7A.js");
4400
+ const { providePathConfirmation } = await import("./tools-DUC54GEH.js");
6215
4401
  const result = providePathConfirmation(callId, approved, alwaysAllow);
6216
4402
  if (!result.found) {
6217
4403
  return res.status(404).json({ error: "No pending path confirmation with that ID" });
@@ -6219,7 +4405,7 @@ async function createServerHandle(config4) {
6219
4405
  const { getEventStore: getEventStore2 } = await import("./events-4K52FKPR.js");
6220
4406
  const { buildMessagesFromStoredEvents, foldPendingConfirmations } = await import("./folding-QIBNKYA6.js");
6221
4407
  const { createSessionStateMessage } = await import("./protocol-YYWMFR35.js");
6222
- const { getPendingQuestionsForSession } = await import("./tools-THXBQJ7A.js");
4408
+ const { getPendingQuestionsForSession } = await import("./tools-DUC54GEH.js");
6223
4409
  const eventStore = getEventStore2();
6224
4410
  const events = eventStore.getEvents(sessionId);
6225
4411
  const messages = buildMessagesFromStoredEvents(events);
@@ -6240,8 +4426,8 @@ async function createServerHandle(config4) {
6240
4426
  if (!skip && typeof answer !== "string") {
6241
4427
  return res.status(400).json({ error: "answer is required when not skipping" });
6242
4428
  }
6243
- const { provideAnswer: provideAnswer2 } = await import("./tools-THXBQJ7A.js");
6244
- const found = provideAnswer2(callId, answer ?? "", skip ?? false);
4429
+ const { provideAnswer } = await import("./tools-DUC54GEH.js");
4430
+ const found = provideAnswer(callId, answer ?? "", skip ?? false);
6245
4431
  if (!found) {
6246
4432
  return res.status(404).json({ error: "No pending question with that ID" });
6247
4433
  }
@@ -6276,8 +4462,8 @@ async function createServerHandle(config4) {
6276
4462
  if (!session) {
6277
4463
  return res.status(404).json({ error: "Session not found" });
6278
4464
  }
6279
- const { stopSessionExecution } = await import("./chat-handler-6NUNVXIU.js");
6280
- 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");
6281
4467
  const queuedMessages = sessionManager.getQueueState(sessionId);
6282
4468
  sessionManager.clearMessageQueue(sessionId);
6283
4469
  stopSessionExecution(sessionId, sessionManager);
@@ -6388,7 +4574,7 @@ async function createServerHandle(config4) {
6388
4574
  let visionFallback;
6389
4575
  let globalWorkdir;
6390
4576
  try {
6391
- const { loadGlobalConfig, getVisionFallback } = await import("./config-BU66P4KX.js");
4577
+ const { loadGlobalConfig, getVisionFallback } = await import("./config-YFHA5MWV.js");
6392
4578
  const globalConfig = await loadGlobalConfig(config4.mode ?? "production");
6393
4579
  const fallback = getVisionFallback(globalConfig);
6394
4580
  if (fallback) {
@@ -6484,7 +4670,7 @@ async function createServerHandle(config4) {
6484
4670
  const apiKey = req.query["apiKey"];
6485
4671
  if (!url) return res.status(400).json({ error: "url is required" });
6486
4672
  try {
6487
- const { fetchModelsWithContext } = await import("./provider-manager-YA2WALTF.js");
4673
+ const { fetchModelsWithContext } = await import("./provider-manager-DETXLDKW.js");
6488
4674
  const models = await fetchModelsWithContext(url, apiKey);
6489
4675
  if (models.length === 0) {
6490
4676
  return res.status(404).json({ error: `No models found at ${buildModelsUrl(url)}`, url });
@@ -6554,7 +4740,7 @@ async function createServerHandle(config4) {
6554
4740
  return res.status(400).json({ error: "name, url, and backend are required" });
6555
4741
  }
6556
4742
  try {
6557
- const { loadGlobalConfig, saveGlobalConfig, addProvider, setDefaultModelSelection } = await import("./config-BU66P4KX.js");
4743
+ const { loadGlobalConfig, saveGlobalConfig, addProvider, setDefaultModelSelection } = await import("./config-YFHA5MWV.js");
6558
4744
  const globalConfig = await loadGlobalConfig(config4.mode ?? "production");
6559
4745
  const providerBackend = backend;
6560
4746
  const configWithProvider = addProvider(globalConfig, {
@@ -6589,7 +4775,7 @@ async function createServerHandle(config4) {
6589
4775
  app.post("/api/init/config", async (req, res) => {
6590
4776
  const { workdir, visionFallback } = req.body;
6591
4777
  try {
6592
- const { loadGlobalConfig, saveGlobalConfig } = await import("./config-BU66P4KX.js");
4778
+ const { loadGlobalConfig, saveGlobalConfig } = await import("./config-YFHA5MWV.js");
6593
4779
  const globalConfig = await loadGlobalConfig(config4.mode ?? "production");
6594
4780
  const updatedConfig = {
6595
4781
  ...globalConfig,
@@ -6617,7 +4803,7 @@ async function createServerHandle(config4) {
6617
4803
  });
6618
4804
  app.delete("/api/providers/:id", async (req, res) => {
6619
4805
  const { id } = req.params;
6620
- const { loadGlobalConfig, saveGlobalConfig, removeProvider } = await import("./config-BU66P4KX.js");
4806
+ const { loadGlobalConfig, saveGlobalConfig, removeProvider } = await import("./config-YFHA5MWV.js");
6621
4807
  const globalConfig = await loadGlobalConfig(config4.mode ?? "production");
6622
4808
  const updatedConfig = removeProvider(globalConfig, id);
6623
4809
  await saveGlobalConfig(config4.mode ?? "production", updatedConfig);
@@ -6628,7 +4814,7 @@ async function createServerHandle(config4) {
6628
4814
  const { id } = req.params;
6629
4815
  const { isLocal } = req.body;
6630
4816
  try {
6631
- const { loadGlobalConfig, saveGlobalConfig, updateProvider } = await import("./config-BU66P4KX.js");
4817
+ const { loadGlobalConfig, saveGlobalConfig, updateProvider } = await import("./config-YFHA5MWV.js");
6632
4818
  const globalConfig = await loadGlobalConfig(config4.mode ?? "production");
6633
4819
  const provider = globalConfig.providers.find((p) => p.id === id);
6634
4820
  if (!provider) {
@@ -6656,7 +4842,7 @@ async function createServerHandle(config4) {
6656
4842
  models: modelConfigs
6657
4843
  } = req.body;
6658
4844
  try {
6659
- const { loadGlobalConfig, saveGlobalConfig, updateProvider } = await import("./config-BU66P4KX.js");
4845
+ const { loadGlobalConfig, saveGlobalConfig, updateProvider } = await import("./config-YFHA5MWV.js");
6660
4846
  const globalConfig = await loadGlobalConfig(config4.mode ?? "production");
6661
4847
  const provider = globalConfig.providers.find((p) => p.id === id);
6662
4848
  if (!provider) {
@@ -6693,7 +4879,7 @@ async function createServerHandle(config4) {
6693
4879
  return res.status(400).json({ error: result.error });
6694
4880
  }
6695
4881
  const llmClient = getLLMClient();
6696
- const { loadGlobalConfig, saveGlobalConfig, setDefaultModelSelection } = await import("./config-BU66P4KX.js");
4882
+ const { loadGlobalConfig, saveGlobalConfig, setDefaultModelSelection } = await import("./config-YFHA5MWV.js");
6697
4883
  const globalConfig = await loadGlobalConfig(config4.mode ?? "production");
6698
4884
  const updatedConfig = setDefaultModelSelection(globalConfig, id, llmClient.getModel());
6699
4885
  await saveGlobalConfig(config4.mode ?? "production", updatedConfig);
@@ -6718,6 +4904,131 @@ async function createServerHandle(config4) {
6718
4904
  status: updatedProvider?.status ?? "unknown"
6719
4905
  });
6720
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
+ });
6721
5032
  const projectDir = config4.workdir;
6722
5033
  app.use("/api/skills", createSkillRoutes(configDir, projectDir));
6723
5034
  app.use("/api/commands", createCommandRoutes(configDir, projectDir));
@@ -6792,7 +5103,7 @@ async function createServerHandle(config4) {
6792
5103
  }
6793
5104
  });
6794
5105
  app.get("/fox.svg", (_req, res) => {
6795
- readFile3(join7(webDir, "fox.svg")).then((content) => {
5106
+ readFile(join5(webDir, "fox.svg")).then((content) => {
6796
5107
  res.set("Content-Type", "image/svg+xml");
6797
5108
  res.send(content);
6798
5109
  }).catch(() => {
@@ -6801,7 +5112,7 @@ async function createServerHandle(config4) {
6801
5112
  });
6802
5113
  app.use(
6803
5114
  "/sounds",
6804
- express.static(join7(webDir, "public", "sounds"), {
5115
+ express.static(join5(webDir, "public", "sounds"), {
6805
5116
  setHeaders: (res) => {
6806
5117
  res.set("Content-Type", "audio/mpeg");
6807
5118
  }
@@ -6810,13 +5121,13 @@ async function createServerHandle(config4) {
6810
5121
  app.use("/__inspect__.js", (_req, res) => {
6811
5122
  res.set("Access-Control-Allow-Origin", "*");
6812
5123
  res.set("Content-Type", "application/javascript");
6813
- 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"));
6814
5125
  });
6815
5126
  app.get("/*path", (req, res) => {
6816
5127
  if (req.path.startsWith("/api/")) {
6817
5128
  return;
6818
5129
  }
6819
- 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) => {
6820
5131
  logger.error("Error serving index.html", { error: err });
6821
5132
  res.status(500).send("Server error");
6822
5133
  });
@@ -6827,7 +5138,7 @@ async function createServerHandle(config4) {
6827
5138
  const distWebDir = resolve2(__dirname2, "web");
6828
5139
  app.use(
6829
5140
  "/assets",
6830
- express.static(join7(distWebDir, "assets"), {
5141
+ express.static(join5(distWebDir, "assets"), {
6831
5142
  setHeaders: (res, filepath) => {
6832
5143
  if (filepath.endsWith(".css")) {
6833
5144
  res.set("Content-Type", "text/css");
@@ -6835,11 +5146,11 @@ async function createServerHandle(config4) {
6835
5146
  }
6836
5147
  })
6837
5148
  );
6838
- app.use("/manifest.webmanifest", express.static(join7(distWebDir, "manifest.webmanifest")));
6839
- app.use("/registerSW.js", express.static(join7(distWebDir, "registerSW.js")));
6840
- 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")));
6841
5152
  app.get("/fox.svg", (_req, res) => {
6842
- readFile3(join7(distWebDir, "fox.svg")).then((content) => {
5153
+ readFile(join5(distWebDir, "fox.svg")).then((content) => {
6843
5154
  res.set("Content-Type", "image/svg+xml");
6844
5155
  res.send(content);
6845
5156
  }).catch(() => {
@@ -6848,7 +5159,7 @@ async function createServerHandle(config4) {
6848
5159
  });
6849
5160
  app.use(
6850
5161
  "/sounds",
6851
- express.static(join7(distWebDir, "sounds"), {
5162
+ express.static(join5(distWebDir, "sounds"), {
6852
5163
  setHeaders: (res) => {
6853
5164
  res.set("Content-Type", "audio/mpeg");
6854
5165
  }
@@ -6857,15 +5168,15 @@ async function createServerHandle(config4) {
6857
5168
  app.use("/__inspect__.js", (_req, res) => {
6858
5169
  res.set("Access-Control-Allow-Origin", "*");
6859
5170
  res.set("Content-Type", "application/javascript");
6860
- 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"));
6861
5172
  });
6862
5173
  app.use("/__inspect__.js", (_req, res) => {
6863
5174
  res.set("Access-Control-Allow-Origin", "*");
6864
5175
  res.set("Content-Type", "application/javascript");
6865
- 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"));
6866
5177
  });
6867
5178
  app.get("/", (_req, res) => {
6868
- 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(() => {
6869
5180
  res.status(404).send("Web UI not built. Run `npm run build:web`");
6870
5181
  });
6871
5182
  });
@@ -6873,12 +5184,14 @@ async function createServerHandle(config4) {
6873
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") {
6874
5185
  return;
6875
5186
  }
6876
- 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(() => {
6877
5188
  res.status(404).send("Web UI not built");
6878
5189
  });
6879
5190
  });
6880
5191
  }
6881
5192
  const httpServer = createHttpServer(app);
5193
+ const { setMcpManagerForWs } = await import("./server-EA6W7YIC.js");
5194
+ setMcpManagerForWs(mcpManager);
6882
5195
  const wssExports = createWebSocketServer(
6883
5196
  httpServer,
6884
5197
  config4,
@@ -6888,7 +5201,7 @@ async function createServerHandle(config4) {
6888
5201
  providerManager
6889
5202
  );
6890
5203
  const wss = wssExports.wss;
6891
- const { QueueProcessor } = await import("./processor-GA5NG3T7.js");
5204
+ const { QueueProcessor } = await import("./processor-CCXZYLX6.js");
6892
5205
  const queueProcessor = new QueueProcessor({
6893
5206
  sessionManager,
6894
5207
  providerManager,
@@ -6928,6 +5241,7 @@ async function createServerHandle(config4) {
6928
5241
  logger.info("Shutting down...");
6929
5242
  void (async () => {
6930
5243
  await devServerManager.stopAll();
5244
+ await mcpManager.disconnectAll();
6931
5245
  const { stopAllInspectProxies } = await import("./inspect-proxy-42ZXL2R5.js");
6932
5246
  stopAllInspectProxies();
6933
5247
  const { cleanupAllProcesses } = await import("./store-OQ45KONN.js");
@@ -6962,4 +5276,4 @@ export {
6962
5276
  createServerHandle,
6963
5277
  createServer
6964
5278
  };
6965
- //# sourceMappingURL=chunk-WF3MA65W.js.map
5279
+ //# sourceMappingURL=chunk-4U45A7B3.js.map