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