openfox 2.0.13 → 2.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/{chat-handler-6NUNVXIU.js → chat-handler-RYOG3WJP.js} +9 -7
  2. package/dist/{chunk-VFRWE5X4.js → chunk-2GX5EIIU.js} +481 -2104
  3. package/dist/{chunk-FQIT7VIA.js → chunk-3IKRNKPI.js} +6 -6
  4. package/dist/{chunk-A52FXWJX.js → chunk-3SGGOBCL.js} +10 -387
  5. package/dist/chunk-EKTGRBDC.js +2072 -0
  6. package/dist/{chunk-3QB2RMX2.js → chunk-F4PMNP7S.js} +2 -2
  7. package/dist/{chunk-TIKQWNYK.js → chunk-J7KOV4ST.js} +2 -2
  8. package/dist/{chunk-2VDLXCLO.js → chunk-KPKSN362.js} +288 -26
  9. package/dist/chunk-NWO6GRYE.js +39 -0
  10. package/dist/chunk-PQ56PX7L.js +397 -0
  11. package/dist/{chunk-HPCGVAS4.js → chunk-SNQT7LNU.js} +18 -1
  12. package/dist/{chunk-5BDVM6YI.js → chunk-YD6NDTKF.js} +1 -1
  13. package/dist/{chunk-EZJUU54W.js → chunk-Z64KW2HD.js} +23 -17
  14. package/dist/cli/dev.js +1 -1
  15. package/dist/cli/index.js +1 -1
  16. package/dist/{config-BU66P4KX.js → config-Z66BQTNX.js} +2 -2
  17. package/dist/{orchestrator-WXEZHUVQ.js → orchestrator-KRCVDTZ6.js} +8 -6
  18. package/dist/package.json +3 -3
  19. package/dist/{processor-GA5NG3T7.js → processor-JHR3Z42W.js} +9 -7
  20. package/dist/{protocol-CaLuIetw.d.ts → protocol-BC1QSQ-Y.d.ts} +16 -1
  21. package/dist/{protocol-YYWMFR35.js → protocol-BKNLAEPJ.js} +3 -3
  22. package/dist/{provider-QPECLUZ7.js → provider-YBTRC77Y.js} +7 -5
  23. package/dist/{provider-manager-YA2WALTF.js → provider-manager-DETXLDKW.js} +3 -2
  24. package/dist/{serve-TJHQ326L.js → serve-AV74ODQ6.js} +13 -9
  25. package/dist/server/index.d.ts +21 -2
  26. package/dist/server/index.js +11 -7
  27. package/dist/server-P7E2IPVL.js +31 -0
  28. package/dist/shared/index.d.ts +2 -2
  29. package/dist/shared/index.js +1 -1
  30. package/dist/tool-adapter-B7QP6NLA.js +7 -0
  31. package/dist/{tools-THXBQJ7A.js → tools-WEE4XDTL.js} +9 -5
  32. package/dist/web/assets/{index-CQw-9GC9.js → index-BMz5-yAf.js} +76 -75
  33. package/dist/web/assets/{index-Db3yUKBI.css → index-Ho_tVUbw.css} +1 -1
  34. package/dist/web/index.html +2 -2
  35. package/dist/web/sw.js +1 -1
  36. package/package.json +3 -3
@@ -0,0 +1,2072 @@
1
+ import {
2
+ appendCompactionPrompt
3
+ } from "./chunk-FWJ6YXGA.js";
4
+ import {
5
+ injectWorkflowKickoffIfNeeded,
6
+ runAgentTurn,
7
+ runChatTurn
8
+ } from "./chunk-Z64KW2HD.js";
9
+ import {
10
+ applyDynamicContext,
11
+ checkAborted,
12
+ computeDynamicContextHash,
13
+ deleteItemFromDir,
14
+ devServerManager,
15
+ executeSubAgent,
16
+ findAgentById,
17
+ getAllInstructions,
18
+ getEnabledSkillMetadata,
19
+ getToolFingerprint,
20
+ getToolRegistryForAgent,
21
+ jsonSerializer,
22
+ loadAllAgentsDefault,
23
+ saveItemToDir,
24
+ spawnShellProcess
25
+ } from "./chunk-KPKSN362.js";
26
+ import {
27
+ TurnMetrics,
28
+ createMessageStartEvent
29
+ } from "./chunk-L7TDUIQY.js";
30
+ import {
31
+ getPlatformShell,
32
+ onProcessEvent
33
+ } from "./chunk-PBGOZMVY.js";
34
+ import {
35
+ getCurrentContextWindowId,
36
+ getEventStore,
37
+ getRuntimeConfig
38
+ } from "./chunk-SYG2ENUQ.js";
39
+ import {
40
+ createChatErrorMessage,
41
+ createChatMessageMessage,
42
+ createContextStateMessage,
43
+ createErrorMessage,
44
+ createGitStatusMessage,
45
+ createQueueStateMessage,
46
+ createSessionRunningMessage,
47
+ isAskAnswerPayload,
48
+ isSessionLoadPayload,
49
+ parseClientMessage,
50
+ serializeServerMessage,
51
+ storedEventToServerMessage
52
+ } from "./chunk-F4PMNP7S.js";
53
+ import {
54
+ provideAnswer
55
+ } from "./chunk-EU3WWTFH.js";
56
+ import {
57
+ computeSessionStats
58
+ } from "./chunk-VUQCQXXJ.js";
59
+ import {
60
+ createServerMessage
61
+ } from "./chunk-YD6NDTKF.js";
62
+ import {
63
+ getGlobalConfigDir
64
+ } from "./chunk-CQGTEGKL.js";
65
+ import {
66
+ createLLMClient,
67
+ ensureVersionPrefix
68
+ } from "./chunk-PQ56PX7L.js";
69
+ import {
70
+ logger
71
+ } from "./chunk-K44MW7JJ.js";
72
+
73
+ // src/server/ws/server.ts
74
+ import { WebSocketServer, WebSocket as WebSocket2 } from "ws";
75
+ import { spawn as spawn3 } from "child_process";
76
+ import { createHash as createHash2 } from "crypto";
77
+
78
+ // src/server/ws/terminal.ts
79
+ import WebSocket from "ws";
80
+
81
+ // src/server/terminal/manager.ts
82
+ import * as pty from "node-pty";
83
+ import os from "os";
84
+ var TerminalManager = class {
85
+ sessions = /* @__PURE__ */ new Map();
86
+ outputCallbacks = /* @__PURE__ */ new Set();
87
+ exitCallbacks = /* @__PURE__ */ new Set();
88
+ outputHistory = /* @__PURE__ */ new Map();
89
+ generateId() {
90
+ return `term_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
91
+ }
92
+ getShell() {
93
+ return getPlatformShell().command;
94
+ }
95
+ resolveWorkdir(workdir) {
96
+ if (workdir && workdir.length > 0) {
97
+ return workdir;
98
+ }
99
+ return os.homedir();
100
+ }
101
+ create(workdir, projectId) {
102
+ const id = this.generateId();
103
+ const shell = this.getShell();
104
+ const cwd = this.resolveWorkdir(workdir);
105
+ const ptyProcess = pty.spawn(shell, [], {
106
+ name: "xterm-256color",
107
+ cols: 80,
108
+ rows: 24,
109
+ cwd,
110
+ env: process.env
111
+ });
112
+ const session = {
113
+ id,
114
+ pty: ptyProcess,
115
+ workdir: cwd,
116
+ projectId: projectId ?? ""
117
+ };
118
+ ptyProcess.onData((data) => {
119
+ const output = { sessionId: id, data };
120
+ for (const cb of this.outputCallbacks) {
121
+ cb(output);
122
+ }
123
+ const history = this.outputHistory.get(id) || [];
124
+ history.push(data);
125
+ if (history.length > 1e4) history.shift();
126
+ this.outputHistory.set(id, history);
127
+ });
128
+ ptyProcess.onExit(({ exitCode }) => {
129
+ logger.info("Terminal session exited", { id, exitCode });
130
+ this.sessions.delete(id);
131
+ this.outputHistory.delete(id);
132
+ for (const cb of this.exitCallbacks) {
133
+ cb(id, exitCode);
134
+ }
135
+ });
136
+ this.sessions.set(id, session);
137
+ logger.info("Terminal session created", { id, shell, cwd });
138
+ return session;
139
+ }
140
+ write(sessionId, data) {
141
+ const session = this.sessions.get(sessionId);
142
+ if (!session) {
143
+ return false;
144
+ }
145
+ try {
146
+ session.pty.write(data);
147
+ return true;
148
+ } catch {
149
+ return false;
150
+ }
151
+ }
152
+ resize(sessionId, cols, rows) {
153
+ const session = this.sessions.get(sessionId);
154
+ if (!session) {
155
+ return false;
156
+ }
157
+ try {
158
+ session.pty.resize(cols, rows);
159
+ return true;
160
+ } catch {
161
+ return false;
162
+ }
163
+ }
164
+ kill(sessionId) {
165
+ const session = this.sessions.get(sessionId);
166
+ if (!session) {
167
+ return false;
168
+ }
169
+ session.pty.kill();
170
+ this.sessions.delete(sessionId);
171
+ logger.info("Terminal session killed", { id: sessionId });
172
+ return true;
173
+ }
174
+ get(sessionId) {
175
+ return this.sessions.get(sessionId);
176
+ }
177
+ getAll() {
178
+ return Array.from(this.sessions.values());
179
+ }
180
+ getByProject(projectId) {
181
+ if (!projectId) return [];
182
+ return Array.from(this.sessions.values()).filter((s) => s.projectId === projectId);
183
+ }
184
+ getOutputHistory(sessionId) {
185
+ return (this.outputHistory.get(sessionId) || []).join("");
186
+ }
187
+ onOutput(callback) {
188
+ this.outputCallbacks.add(callback);
189
+ return () => {
190
+ this.outputCallbacks.delete(callback);
191
+ };
192
+ }
193
+ onExit(callback) {
194
+ this.exitCallbacks.add(callback);
195
+ return () => {
196
+ this.exitCallbacks.delete(callback);
197
+ };
198
+ }
199
+ killAll() {
200
+ for (const session of this.sessions.values()) {
201
+ session.pty.kill();
202
+ }
203
+ this.sessions.clear();
204
+ logger.info("All terminal sessions killed");
205
+ }
206
+ };
207
+ var terminalManager = new TerminalManager();
208
+
209
+ // src/server/ws/terminal.ts
210
+ var subscriptions = /* @__PURE__ */ new Set();
211
+ var outputHandlerRegistered = false;
212
+ var exitHandlerRegistered = false;
213
+ function registerOutputHandler() {
214
+ if (outputHandlerRegistered) return;
215
+ outputHandlerRegistered = true;
216
+ terminalManager.onOutput((output) => {
217
+ const subs = Array.from(subscriptions).filter(
218
+ (s) => s.sessionId === output.sessionId && s.ws.readyState === WebSocket.OPEN
219
+ );
220
+ for (const sub of subs) {
221
+ sub.ws.send(
222
+ JSON.stringify({
223
+ type: "terminal.output",
224
+ payload: { sessionId: output.sessionId, data: output.data }
225
+ })
226
+ );
227
+ }
228
+ });
229
+ }
230
+ function registerExitHandler() {
231
+ if (exitHandlerRegistered) return;
232
+ exitHandlerRegistered = true;
233
+ terminalManager.onExit((sessionId, exitCode) => {
234
+ const subs = Array.from(subscriptions).filter(
235
+ (s) => s.sessionId === sessionId && s.ws.readyState === WebSocket.OPEN
236
+ );
237
+ for (const sub of subs) {
238
+ sub.ws.send(
239
+ JSON.stringify({
240
+ type: "terminal.exit",
241
+ payload: { sessionId, exitCode }
242
+ })
243
+ );
244
+ }
245
+ for (const sub of subscriptions) {
246
+ if (sub.sessionId === sessionId) {
247
+ subscriptions.delete(sub);
248
+ }
249
+ }
250
+ });
251
+ }
252
+ function handleTerminalMessage(ws, message) {
253
+ switch (message.type) {
254
+ case "terminal.subscribe": {
255
+ if (message.payload.sessionId) {
256
+ const exists = Array.from(subscriptions).some(
257
+ (sub) => sub.ws === ws && sub.sessionId === message.payload.sessionId
258
+ );
259
+ if (!exists) {
260
+ subscriptions.add({ ws, sessionId: message.payload.sessionId });
261
+ }
262
+ registerOutputHandler();
263
+ registerExitHandler();
264
+ const sessionId = message.payload.sessionId;
265
+ const history = terminalManager.getOutputHistory(sessionId);
266
+ if (history) {
267
+ setTimeout(() => {
268
+ ws.send(
269
+ JSON.stringify({
270
+ type: "terminal.output",
271
+ payload: { sessionId, data: history }
272
+ })
273
+ );
274
+ }, 100);
275
+ }
276
+ }
277
+ break;
278
+ }
279
+ case "terminal.write": {
280
+ if (message.payload.sessionId && message.payload.data) {
281
+ terminalManager.write(message.payload.sessionId, message.payload.data);
282
+ }
283
+ break;
284
+ }
285
+ case "terminal.resize": {
286
+ if (message.payload.sessionId && typeof message.payload.cols === "number" && typeof message.payload.rows === "number") {
287
+ terminalManager.resize(message.payload.sessionId, message.payload.cols, message.payload.rows);
288
+ }
289
+ break;
290
+ }
291
+ case "terminal.kill": {
292
+ if (message.payload.sessionId) {
293
+ terminalManager.kill(message.payload.sessionId);
294
+ for (const sub of subscriptions) {
295
+ if (sub.sessionId === message.payload.sessionId) {
296
+ subscriptions.delete(sub);
297
+ }
298
+ }
299
+ ws.send(
300
+ JSON.stringify({
301
+ type: "terminal.killed",
302
+ payload: { sessionId: message.payload.sessionId }
303
+ })
304
+ );
305
+ }
306
+ break;
307
+ }
308
+ }
309
+ }
310
+ function unsubscribeAllFromTerminal(ws) {
311
+ for (const sub of subscriptions) {
312
+ if (sub.ws === ws) {
313
+ subscriptions.delete(sub);
314
+ }
315
+ }
316
+ }
317
+
318
+ // src/server/workflows/registry.ts
319
+ import { readdir, readFile, writeFile, mkdir, access, unlink } from "fs/promises";
320
+ import { join, dirname } from "path";
321
+ import { constants } from "fs";
322
+ import { fileURLToPath } from "url";
323
+ var __bundleDir = dirname(fileURLToPath(import.meta.url));
324
+ var DEFAULTS_DIR = join(__bundleDir, "defaults");
325
+ var DEFAULTS_DIR_ALT = join(__bundleDir, "workflow-defaults");
326
+ var WORKFLOW_EXTENSION = ".workflow.json";
327
+ function getWorkflowsDir(configDir) {
328
+ return join(configDir, "workflows");
329
+ }
330
+ function getProjectWorkflowsDir(projectDir) {
331
+ return join(projectDir, ".openfox", "workflows");
332
+ }
333
+ async function pathExists(path) {
334
+ try {
335
+ await access(path, constants.R_OK);
336
+ return true;
337
+ } catch {
338
+ return false;
339
+ }
340
+ }
341
+ async function loadWorkflowsFromDir(dir) {
342
+ if (!await pathExists(dir)) {
343
+ return [];
344
+ }
345
+ let files;
346
+ try {
347
+ files = (await readdir(dir)).filter((f) => f.endsWith(WORKFLOW_EXTENSION));
348
+ } catch {
349
+ return [];
350
+ }
351
+ const workflows = [];
352
+ for (const file of files) {
353
+ try {
354
+ const raw = await readFile(join(dir, file), "utf-8");
355
+ const parsed = JSON.parse(raw);
356
+ if (parsed.metadata?.id && parsed.steps?.length > 0) {
357
+ workflows.push(parsed);
358
+ } else {
359
+ logger.warn("Skipping invalid workflow file", { file });
360
+ }
361
+ } catch (err) {
362
+ logger.warn("Failed to parse workflow file", { file, error: err instanceof Error ? err.message : String(err) });
363
+ }
364
+ }
365
+ return workflows;
366
+ }
367
+ async function loadDefaultWorkflows() {
368
+ let defaults = await loadWorkflowsFromDir(DEFAULTS_DIR);
369
+ if (!defaults.length) {
370
+ defaults = await loadWorkflowsFromDir(DEFAULTS_DIR_ALT);
371
+ }
372
+ return defaults;
373
+ }
374
+ async function loadUserWorkflows(configDir) {
375
+ return loadWorkflowsFromDir(getWorkflowsDir(configDir));
376
+ }
377
+ async function loadProjectWorkflows(projectDir) {
378
+ return loadWorkflowsFromDir(getProjectWorkflowsDir(projectDir));
379
+ }
380
+ async function loadAllWorkflows(configDir, projectDir) {
381
+ const [defaultWorkflows, userWorkflows] = await Promise.all([loadDefaultWorkflows(), loadUserWorkflows(configDir)]);
382
+ const workflowMap = /* @__PURE__ */ new Map();
383
+ for (const workflow of defaultWorkflows) {
384
+ workflowMap.set(workflow.metadata.id, workflow);
385
+ }
386
+ for (const workflow of userWorkflows) {
387
+ workflowMap.set(workflow.metadata.id, workflow);
388
+ }
389
+ if (projectDir) {
390
+ const projectWorkflows = await loadProjectWorkflows(projectDir);
391
+ for (const workflow of projectWorkflows) {
392
+ workflowMap.set(workflow.metadata.id, workflow);
393
+ }
394
+ }
395
+ return Array.from(workflowMap.values());
396
+ }
397
+ async function saveWorkflowToProject(projectDir, workflow) {
398
+ await saveItemToDir(getProjectWorkflowsDir(projectDir), workflow, WORKFLOW_EXTENSION, jsonSerializer);
399
+ }
400
+ async function deleteProjectWorkflow(projectDir, workflowId) {
401
+ return deleteItemFromDir(getProjectWorkflowsDir(projectDir), workflowId, WORKFLOW_EXTENSION);
402
+ }
403
+ async function getDefaultWorkflowIds() {
404
+ for (const dir of [DEFAULTS_DIR, DEFAULTS_DIR_ALT]) {
405
+ try {
406
+ const files = (await readdir(dir)).filter((f) => f.endsWith(WORKFLOW_EXTENSION));
407
+ return files.map((f) => f.replace(WORKFLOW_EXTENSION, ""));
408
+ } catch {
409
+ }
410
+ }
411
+ return [];
412
+ }
413
+ async function isDefaultWorkflow(workflowId) {
414
+ const defaultIds = await getDefaultWorkflowIds();
415
+ return defaultIds.includes(workflowId);
416
+ }
417
+ function findWorkflowById(workflowId, workflows) {
418
+ return workflows.find((p) => p.metadata.id === workflowId);
419
+ }
420
+ async function workflowExists(configDir, workflowId, projectDir) {
421
+ if (await pathExists(join(getWorkflowsDir(configDir), `${workflowId}${WORKFLOW_EXTENSION}`))) return true;
422
+ if (projectDir && await pathExists(join(getProjectWorkflowsDir(projectDir), `${workflowId}${WORKFLOW_EXTENSION}`)))
423
+ return true;
424
+ return false;
425
+ }
426
+ async function saveWorkflow(configDir, workflow) {
427
+ const workflowsDir = getWorkflowsDir(configDir);
428
+ if (!await pathExists(workflowsDir)) {
429
+ await mkdir(workflowsDir, { recursive: true });
430
+ }
431
+ const filePath = join(workflowsDir, `${workflow.metadata.id}${WORKFLOW_EXTENSION}`);
432
+ await writeFile(filePath, JSON.stringify(workflow, null, 2) + "\n", "utf-8");
433
+ }
434
+ async function deleteWorkflow(configDir, workflowId) {
435
+ const isDefault = await isDefaultWorkflow(workflowId);
436
+ if (isDefault) {
437
+ return { success: false, reason: "Cannot delete built-in defaults" };
438
+ }
439
+ const filePath = join(getWorkflowsDir(configDir), `${workflowId}${WORKFLOW_EXTENSION}`);
440
+ try {
441
+ await unlink(filePath);
442
+ return { success: true };
443
+ } catch {
444
+ return { success: false };
445
+ }
446
+ }
447
+
448
+ // src/server/workflows/types.ts
449
+ var TERMINAL_DONE = "$done";
450
+ var TERMINAL_BLOCKED = "$blocked";
451
+
452
+ // src/server/git/diff.ts
453
+ import { spawn as spawn2 } from "child_process";
454
+ function getGitDiffFiles(cwd) {
455
+ return new Promise((resolve) => {
456
+ const diffProc = spawn2("git", ["diff", "--name-status", "HEAD"], { cwd, stdio: ["ignore", "pipe", "pipe"] });
457
+ const statusProc = spawn2("git", ["status", "--porcelain"], { cwd, stdio: ["ignore", "pipe", "pipe"] });
458
+ let diffStdout = "";
459
+ let statusStdout = "";
460
+ let diffExited = false;
461
+ let statusExited = false;
462
+ let diffCode = null;
463
+ let statusCode = null;
464
+ const collect = () => {
465
+ if (!diffExited || !statusExited) return;
466
+ const files = [];
467
+ if (diffCode === 0) {
468
+ for (const line of diffStdout.split("\n")) {
469
+ if (!line.trim()) continue;
470
+ const [statusChar, ...pathParts] = line.split(" ");
471
+ const path = pathParts.join(" ") || statusChar || "";
472
+ if (!path) continue;
473
+ const status = statusChar === "A" ? "added" : statusChar === "D" ? "deleted" : "modified";
474
+ files.push({ path, status, additions: 0, deletions: 0 });
475
+ }
476
+ }
477
+ if (statusCode === 0) {
478
+ for (const line of statusStdout.split("\n")) {
479
+ if (!line.startsWith("?? ")) continue;
480
+ const path = line.slice(3).trim();
481
+ if (!path) continue;
482
+ files.push({ path, status: "added", additions: 0, deletions: 0 });
483
+ }
484
+ }
485
+ resolve(files);
486
+ };
487
+ diffProc.stdout.on("data", (data) => {
488
+ diffStdout += data.toString();
489
+ });
490
+ statusProc.stdout.on("data", (data) => {
491
+ statusStdout += data.toString();
492
+ });
493
+ diffProc.on("close", (code) => {
494
+ diffExited = true;
495
+ diffCode = code;
496
+ collect();
497
+ });
498
+ statusProc.on("close", (code) => {
499
+ statusExited = true;
500
+ statusCode = code;
501
+ collect();
502
+ });
503
+ diffProc.on("error", () => {
504
+ diffExited = true;
505
+ diffCode = 1;
506
+ collect();
507
+ });
508
+ statusProc.on("error", () => {
509
+ statusExited = true;
510
+ statusCode = 1;
511
+ collect();
512
+ });
513
+ });
514
+ }
515
+ async function formatGitDiffFiles(cwd) {
516
+ const files = await getGitDiffFiles(cwd);
517
+ if (files.length === 0) return "(none)";
518
+ return files.map((f) => `- ${f.path} (${f.status})`).join("\n");
519
+ }
520
+
521
+ // src/server/workflows/shell.ts
522
+ function executeShellCommand(command, cwd, timeout, signal) {
523
+ return new Promise((resolve, reject) => {
524
+ if (checkAborted(signal)) {
525
+ reject(new Error("Aborted"));
526
+ return;
527
+ }
528
+ const proc = spawnShellProcess(command, cwd, signal);
529
+ let stdout = "";
530
+ let stderr = "";
531
+ let killed = false;
532
+ const timer = setTimeout(() => {
533
+ killed = true;
534
+ proc.kill("SIGTERM");
535
+ resolve({ exitCode: 1, stdout, stderr: stderr + "\nCommand timed out", success: false });
536
+ }, timeout);
537
+ const onAbort = () => {
538
+ if (!killed) {
539
+ killed = true;
540
+ proc.kill("SIGTERM");
541
+ clearTimeout(timer);
542
+ reject(new Error("Aborted"));
543
+ }
544
+ };
545
+ signal?.addEventListener("abort", onAbort);
546
+ proc.stdout?.on("data", (data) => {
547
+ stdout += data.toString();
548
+ });
549
+ proc.stderr?.on("data", (data) => {
550
+ stderr += data.toString();
551
+ });
552
+ proc.on("close", (code) => {
553
+ clearTimeout(timer);
554
+ signal?.removeEventListener("abort", onAbort);
555
+ if (killed) return;
556
+ const exitCode = code ?? 1;
557
+ resolve({ exitCode, stdout, stderr, success: exitCode === 0 });
558
+ });
559
+ proc.on("error", (err) => {
560
+ clearTimeout(timer);
561
+ signal?.removeEventListener("abort", onAbort);
562
+ if (killed) return;
563
+ resolve({ exitCode: 1, stdout, stderr: err.message, success: false });
564
+ });
565
+ });
566
+ }
567
+
568
+ // src/server/workflows/executor.ts
569
+ var TEMPLATE_VARIABLES = [
570
+ { name: "workdir", description: "Working directory of the session" },
571
+ { name: "reason", description: 'Human-readable reason (e.g. "2 criteria remaining")' },
572
+ {
573
+ name: "stepOutput",
574
+ description: "Structured output from the previous step (content, stdout, stderr, exitCode, etc.)"
575
+ },
576
+ {
577
+ name: "verifierFindings",
578
+ description: "@deprecated Use stepOutput.content instead. Output from the last sub-agent step"
579
+ },
580
+ {
581
+ name: "previousStepOutput",
582
+ description: "@deprecated Use stepOutput.stdout instead. Output from the last shell step"
583
+ },
584
+ { name: "criteriaCount", description: "Total number of criteria" },
585
+ { name: "pendingCount", description: "Number of pending/failed criteria" },
586
+ { name: "criteriaList", description: "Formatted list of all criteria with status" },
587
+ { name: "modifiedFiles", description: "List of modified files" }
588
+ ];
589
+ function formatCriteriaList(entries) {
590
+ if (entries.length === 0) return "(none)";
591
+ return entries.map((e) => {
592
+ const status = e.status === "passed" ? "[PASSED]" : e.status === "completed" ? "[NEEDS VERIFICATION]" : e.status === "failed" ? "[FAILED]" : "[NOT COMPLETED]";
593
+ return `- **${e.id}** ${status}: ${e.description}`;
594
+ }).join("\n");
595
+ }
596
+ async function formatModifiedFiles(session) {
597
+ return formatGitDiffFiles(session.workdir);
598
+ }
599
+ function resolveTemplate(template, ctx) {
600
+ let result = template;
601
+ for (const { name } of TEMPLATE_VARIABLES) {
602
+ if (name === "stepOutput" || name === "verifierFindings" || name === "previousStepOutput") continue;
603
+ const value = String(ctx[name]);
604
+ result = result.replace(new RegExp(`\\{\\{${name}\\}\\}`, "g"), value);
605
+ }
606
+ result = result.replace(/\{\{stepOutput\.(\w+)\}\}/g, (_, key) => ctx.stepOutput[key] ?? "");
607
+ result = result.replace(/\{\{verifierFindings\}\}/g, ctx.stepOutput["content"] ?? "");
608
+ result = result.replace(/\{\{previousStepOutput\}\}/g, ctx.stepOutput["stdout"] ?? "");
609
+ return result;
610
+ }
611
+ function evaluateCondition(condition, stepOutcome, metadataEntries) {
612
+ switch (condition.type) {
613
+ case "step_result":
614
+ if (!stepOutcome) return false;
615
+ return stepOutcome.result === condition.result;
616
+ case "metadata_all_match": {
617
+ if (!metadataEntries) return false;
618
+ const entries = metadataEntries[condition.key];
619
+ if (!entries || entries.length === 0) return true;
620
+ return entries.every((e) => e[condition.field] === condition.value);
621
+ }
622
+ case "metadata_all_in": {
623
+ if (!metadataEntries) return false;
624
+ const entries = metadataEntries[condition.key];
625
+ if (!entries || entries.length === 0) return true;
626
+ return entries.every((e) => condition.values.includes(e[condition.field]));
627
+ }
628
+ case "always":
629
+ return true;
630
+ }
631
+ }
632
+ function evaluateTransitions(transitions, stepOutcome, metadataEntries) {
633
+ for (const transition of transitions) {
634
+ if (evaluateCondition(transition.when, stepOutcome, metadataEntries)) {
635
+ return transition.goto;
636
+ }
637
+ }
638
+ return TERMINAL_BLOCKED;
639
+ }
640
+ function emitWorkflowMessage(eventStore, sessionId, content, windowOptions, onMessage) {
641
+ const msgId = crypto.randomUUID();
642
+ eventStore.append(
643
+ sessionId,
644
+ createMessageStartEvent(msgId, "user", content, {
645
+ ...windowOptions ?? {},
646
+ isSystemGenerated: true,
647
+ messageKind: "correction",
648
+ metadata: { type: "workflow", name: "Workflow", color: "#f59e0b" }
649
+ })
650
+ );
651
+ eventStore.append(sessionId, { type: "message.done", data: { messageId: msgId } });
652
+ if (onMessage) {
653
+ onMessage(
654
+ createChatMessageMessage({
655
+ id: msgId,
656
+ role: "user",
657
+ content,
658
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
659
+ isSystemGenerated: true,
660
+ messageKind: "correction",
661
+ metadata: { type: "workflow", name: "Workflow", color: "#f59e0b" }
662
+ })
663
+ );
664
+ }
665
+ return msgId;
666
+ }
667
+ function getCurrentWindowMessageOptions(sessionId) {
668
+ const contextWindowId = getCurrentContextWindowId(sessionId);
669
+ return contextWindowId ? { contextWindowId } : void 0;
670
+ }
671
+ function buildReason(metadataEntries) {
672
+ const entries = metadataEntries?.["criteria"] ?? [];
673
+ const remaining = entries.filter((e) => e.status !== "passed");
674
+ return `${remaining.length} criteria remaining`;
675
+ }
676
+ async function executeWorkflow(workflow, options, subGroup) {
677
+ const { sessionManager, sessionId, llmClient, signal, onMessage } = options;
678
+ const eventStore = getEventStore();
679
+ const startTime = performance.now();
680
+ let iterations = 0;
681
+ const activeSteps = subGroup ? workflow.steps.filter((s) => s.subGroup === subGroup) : workflow.steps;
682
+ let currentStepId = subGroup ? activeSteps[0]?.id ?? workflow.entryStep : workflow.entryStep;
683
+ let lastStepOutput = {};
684
+ const firstEntryForStep = /* @__PURE__ */ new Set();
685
+ const messagesBeforeWorkflow = sessionManager.requireSession(sessionId).messages.length;
686
+ const activeStepIds = new Set(activeSteps.map((s) => s.id));
687
+ const stepsById = /* @__PURE__ */ new Map();
688
+ for (const step of activeSteps) {
689
+ stepsById.set(step.id, step);
690
+ }
691
+ logger.debug("Workflow executor starting", { sessionId, workflow: workflow.metadata.id });
692
+ if (workflow.startCondition && workflow.startCondition.type !== "always") {
693
+ const session = sessionManager.requireSession(sessionId);
694
+ const conditionMet = evaluateCondition(
695
+ workflow.startCondition,
696
+ null,
697
+ session.metadataEntries
698
+ );
699
+ if (!conditionMet) {
700
+ logger.debug("Workflow start condition not met", { sessionId, condition: workflow.startCondition.type });
701
+ return {
702
+ finalAction: {
703
+ type: "BLOCKED",
704
+ reason: `Start condition not met: ${workflow.startCondition.type}`,
705
+ blockedCriteria: []
706
+ },
707
+ iterations: 0,
708
+ totalTime: (performance.now() - startTime) / 1e3
709
+ };
710
+ }
711
+ }
712
+ const startMsgId = crypto.randomUUID();
713
+ const startWindowOpts = getCurrentWindowMessageOptions(sessionId);
714
+ eventStore.append(
715
+ sessionId,
716
+ createMessageStartEvent(
717
+ startMsgId,
718
+ "user",
719
+ JSON.stringify({
720
+ workflowName: workflow.metadata.name,
721
+ workflowId: workflow.metadata.id,
722
+ workflowColor: workflow.metadata.color
723
+ }),
724
+ { ...startWindowOpts ?? {}, isSystemGenerated: true, messageKind: "workflow-started" }
725
+ )
726
+ );
727
+ eventStore.append(sessionId, { type: "message.done", data: { messageId: startMsgId } });
728
+ if (options.userMessage) {
729
+ sessionManager.addMessage(sessionId, {
730
+ role: "user",
731
+ content: options.userMessage.content,
732
+ ...options.userMessage.attachments ? { attachments: options.userMessage.attachments } : {}
733
+ });
734
+ }
735
+ while (iterations < workflow.settings.maxIterations) {
736
+ if (signal?.aborted) {
737
+ logger.debug("Workflow executor aborted", { sessionId, iterations });
738
+ return {
739
+ finalAction: { type: "RUN_BUILDER", reason: "Aborted" },
740
+ iterations,
741
+ totalTime: (performance.now() - startTime) / 1e3
742
+ };
743
+ }
744
+ iterations++;
745
+ const step = stepsById.get(currentStepId);
746
+ if (!step) {
747
+ logger.error("Workflow step not found", { sessionId, stepId: currentStepId });
748
+ return {
749
+ finalAction: { type: "BLOCKED", reason: `Step "${currentStepId}" not found in workflow`, blockedCriteria: [] },
750
+ iterations,
751
+ totalTime: (performance.now() - startTime) / 1e3
752
+ };
753
+ }
754
+ const session = sessionManager.requireSession(sessionId);
755
+ const currentWindowMessageOptions = getCurrentWindowMessageOptions(sessionId);
756
+ const criteriaEntries = session.metadataEntries["criteria"] ?? [];
757
+ const templateCtx = {
758
+ workdir: session.workdir,
759
+ reason: buildReason(session.metadataEntries),
760
+ verifierFindings: lastStepOutput["content"] ?? "",
761
+ previousStepOutput: lastStepOutput["stdout"] ?? "",
762
+ criteriaCount: criteriaEntries.length,
763
+ pendingCount: criteriaEntries.filter((e) => e.status !== "passed").length,
764
+ criteriaList: formatCriteriaList(criteriaEntries),
765
+ modifiedFiles: await formatModifiedFiles(session),
766
+ stepOutput: lastStepOutput
767
+ };
768
+ sessionManager.setPhase(sessionId, step.phase);
769
+ logger.debug("Workflow step executing", { sessionId, iteration: iterations, stepId: step.id, stepType: step.type });
770
+ let stepOutcome = null;
771
+ switch (step.type) {
772
+ case "agent": {
773
+ const agentStep = step;
774
+ const STEP_DONE_PROMPT = "\n\nOnce you're done, call step_done()";
775
+ 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()";
776
+ let promptContent;
777
+ let nudgeContent;
778
+ if (!firstEntryForStep.has(step.id) && agentStep.prompt) {
779
+ const resolvedPrompt = resolveTemplate(agentStep.prompt, templateCtx);
780
+ promptContent = resolvedPrompt + STEP_DONE_PROMPT;
781
+ const promptMsgId = crypto.randomUUID();
782
+ const msgMetadata = { type: "workflow", name: "Workflow", color: "#f59e0b" };
783
+ eventStore.append(
784
+ sessionId,
785
+ createMessageStartEvent(promptMsgId, "user", promptContent, {
786
+ ...currentWindowMessageOptions ?? {},
787
+ isSystemGenerated: true,
788
+ messageKind: "auto-prompt",
789
+ metadata: msgMetadata
790
+ })
791
+ );
792
+ eventStore.append(sessionId, { type: "message.done", data: { messageId: promptMsgId } });
793
+ if (onMessage) {
794
+ onMessage(
795
+ createChatMessageMessage({
796
+ id: promptMsgId,
797
+ role: "user",
798
+ content: promptContent,
799
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
800
+ isSystemGenerated: true,
801
+ messageKind: "auto-prompt",
802
+ metadata: { type: "workflow", name: "Workflow", color: "#f59e0b" }
803
+ })
804
+ );
805
+ }
806
+ } else if (firstEntryForStep.has(step.id)) {
807
+ const parts = [];
808
+ if (agentStep.nudgePrompt) {
809
+ const resolvedNudge = resolveTemplate(agentStep.nudgePrompt, templateCtx);
810
+ parts.push(resolvedNudge);
811
+ }
812
+ parts.push(STEP_DONE_NUDGE);
813
+ nudgeContent = parts.join("\n\n");
814
+ emitWorkflowMessage(eventStore, sessionId, nudgeContent, currentWindowMessageOptions, onMessage);
815
+ }
816
+ const turnMetrics = new TurnMetrics();
817
+ const es = getEventStore();
818
+ const append = (event) => es.append(sessionId, event);
819
+ let stepDoneCalled = false;
820
+ const agentResult = await runAgentTurn(
821
+ {
822
+ sessionManager,
823
+ sessionId,
824
+ llmClient,
825
+ ...options.statsIdentity ? { statsIdentity: options.statsIdentity } : {},
826
+ ...signal ? { signal } : {},
827
+ ...onMessage ? { onMessage } : {}
828
+ },
829
+ turnMetrics,
830
+ agentStep.agentId ?? "planner",
831
+ append,
832
+ {
833
+ ...!firstEntryForStep.has(step.id) && !agentStep.prompt && options.injectWorkflowKickoff === true ? { injectKickoff: () => injectWorkflowKickoffIfNeeded(sessionManager, sessionId, es) } : {},
834
+ onToolExecuted: (toolCall, toolResult) => {
835
+ if (toolCall.name === "step_done" && toolResult.success) {
836
+ stepDoneCalled = true;
837
+ }
838
+ if (toolResult.success && ["write_file", "edit_file"].includes(toolCall.name)) {
839
+ const path = toolCall.arguments["path"];
840
+ sessionManager.addModifiedFile(sessionId, path);
841
+ }
842
+ }
843
+ }
844
+ );
845
+ firstEntryForStep.add(step.id);
846
+ const agentReturnValue = agentResult.returnValueResult ?? "completed";
847
+ lastStepOutput = {
848
+ ...agentResult.returnValueContent ? { content: agentResult.returnValueContent } : {},
849
+ ...agentResult.returnValueResult ? { result: agentResult.returnValueResult } : {},
850
+ stepDoneCalled: String(stepDoneCalled)
851
+ };
852
+ stepOutcome = { result: agentReturnValue, output: lastStepOutput };
853
+ if (!stepDoneCalled) {
854
+ logger.debug("step_done not called, looping agent step", {
855
+ sessionId,
856
+ stepId: step.id,
857
+ iteration: iterations
858
+ });
859
+ continue;
860
+ }
861
+ break;
862
+ }
863
+ case "sub_agent": {
864
+ const subStep = step;
865
+ const turnMetrics = new TurnMetrics();
866
+ const promptTemplate = subStep.prompt ?? "Perform your task.";
867
+ const resolvedPrompt = resolveTemplate(promptTemplate, templateCtx);
868
+ const allAgents = await loadAllAgentsDefault();
869
+ const agentDef = findAgentById(subStep.subAgentType, allAgents);
870
+ if (!agentDef) {
871
+ logger.error("Sub-agent definition not found", { subAgentType: subStep.subAgentType });
872
+ stepOutcome = { result: "error", output: {} };
873
+ break;
874
+ }
875
+ const toolRegistry = getToolRegistryForAgent(agentDef);
876
+ const filteredToolRegistry = {
877
+ tools: toolRegistry.tools.filter((t) => t.name !== "step_done"),
878
+ definitions: toolRegistry.definitions.filter((d) => d.type === "function" && d.function.name !== "step_done"),
879
+ execute: toolRegistry.execute
880
+ };
881
+ const result = await executeSubAgent({
882
+ subAgentType: subStep.subAgentType,
883
+ prompt: resolvedPrompt,
884
+ sessionManager,
885
+ sessionId,
886
+ llmClient,
887
+ toolRegistry: filteredToolRegistry,
888
+ turnMetrics,
889
+ statsIdentity: options.statsIdentity ?? {
890
+ providerId: "",
891
+ providerName: "",
892
+ backend: "unknown",
893
+ model: llmClient.getModel()
894
+ },
895
+ ...signal ? { signal } : {},
896
+ ...onMessage ? { onMessage } : {}
897
+ });
898
+ lastStepOutput = { content: result.content ?? "", ...result.result ? { result: result.result } : {} };
899
+ stepOutcome = { result: result.result ?? "success", output: lastStepOutput };
900
+ break;
901
+ }
902
+ case "shell": {
903
+ const shellStep = step;
904
+ const command = resolveTemplate(shellStep.command, templateCtx);
905
+ const timeout = shellStep.timeout ?? 6e4;
906
+ const successCodes = shellStep.successExitCodes ?? [0];
907
+ const shellMsgId = crypto.randomUUID();
908
+ eventStore.append(
909
+ sessionId,
910
+ createMessageStartEvent(shellMsgId, "user", `Running: \`${command}\``, {
911
+ ...currentWindowMessageOptions ?? {},
912
+ isSystemGenerated: true,
913
+ messageKind: "auto-prompt",
914
+ metadata: { type: "workflow", name: "Workflow", color: "#f59e0b" }
915
+ })
916
+ );
917
+ if (onMessage) {
918
+ onMessage(
919
+ createChatMessageMessage({
920
+ id: shellMsgId,
921
+ role: "user",
922
+ content: `Running: \`${command}\``,
923
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
924
+ isSystemGenerated: true,
925
+ messageKind: "auto-prompt",
926
+ metadata: { type: "workflow", name: "Workflow", color: "#f59e0b" }
927
+ })
928
+ );
929
+ }
930
+ const result = await executeShellCommand(command, session.workdir, timeout, signal);
931
+ const output = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
932
+ lastStepOutput = { stdout: result.stdout ?? "", stderr: result.stderr ?? "", exitCode: String(result.exitCode) };
933
+ const outputContent = output ? `Exit code: ${result.exitCode}
934
+ \`\`\`
935
+ ${output.slice(0, 1e4)}
936
+ \`\`\`` : `Exit code: ${result.exitCode}`;
937
+ eventStore.append(sessionId, { type: "message.done", data: { messageId: shellMsgId } });
938
+ const outputMsgId = crypto.randomUUID();
939
+ eventStore.append(
940
+ sessionId,
941
+ createMessageStartEvent(outputMsgId, "user", outputContent, {
942
+ ...currentWindowMessageOptions ?? {},
943
+ isSystemGenerated: true,
944
+ messageKind: "correction"
945
+ })
946
+ );
947
+ eventStore.append(sessionId, { type: "message.done", data: { messageId: outputMsgId } });
948
+ if (onMessage) {
949
+ onMessage(
950
+ createChatMessageMessage({
951
+ id: outputMsgId,
952
+ role: "user",
953
+ content: outputContent,
954
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
955
+ isSystemGenerated: true,
956
+ messageKind: "correction"
957
+ })
958
+ );
959
+ }
960
+ stepOutcome = { result: successCodes.includes(result.exitCode) ? "success" : "failure", output: lastStepOutput };
961
+ break;
962
+ }
963
+ }
964
+ const refreshedSession = sessionManager.requireSession(sessionId);
965
+ const effectiveTransitions = subGroup ? step.transitions.filter((t) => !t.subGroup || t.subGroup === subGroup) : step.transitions;
966
+ const rawNextStepId = evaluateTransitions(effectiveTransitions, stepOutcome, refreshedSession.metadataEntries);
967
+ const nextStepId = subGroup && rawNextStepId !== TERMINAL_DONE && rawNextStepId !== TERMINAL_BLOCKED && !activeStepIds.has(rawNextStepId) ? TERMINAL_DONE : rawNextStepId;
968
+ if (nextStepId === TERMINAL_DONE) {
969
+ sessionManager.setPhase(sessionId, "done");
970
+ const totalTimeSeconds = Math.round((performance.now() - startTime) / 100) / 10;
971
+ const completedSession = sessionManager.requireSession(sessionId);
972
+ const workflowMessages = completedSession.messages.slice(messagesBeforeWorkflow);
973
+ const workflowStats = computeSessionStats(workflowMessages);
974
+ const totalToolCalls = workflowMessages.reduce((sum, m) => sum + (m.toolCalls?.length ?? 0), 0);
975
+ const taskCompletedData = {
976
+ summary: null,
977
+ iterations,
978
+ totalTimeSeconds,
979
+ totalToolCalls,
980
+ totalTokensGenerated: workflowStats?.generationTokens ?? 0,
981
+ avgGenerationSpeed: workflowStats?.avgGenerationSpeed ?? 0,
982
+ responseCount: workflowStats?.responseCount ?? 0,
983
+ llmCallCount: workflowStats?.llmCallCount ?? 0,
984
+ criteria: [],
985
+ workflowName: workflow.metadata.name,
986
+ workflowId: workflow.metadata.id,
987
+ ...workflow.metadata.color ? { workflowColor: workflow.metadata.color } : {}
988
+ };
989
+ eventStore.append(sessionId, { type: "task.completed", data: taskCompletedData });
990
+ const markerMsgId = crypto.randomUUID();
991
+ eventStore.append(
992
+ sessionId,
993
+ createMessageStartEvent(markerMsgId, "user", JSON.stringify(taskCompletedData), {
994
+ ...currentWindowMessageOptions ?? {},
995
+ isSystemGenerated: true,
996
+ messageKind: "task-completed"
997
+ })
998
+ );
999
+ eventStore.append(sessionId, { type: "message.done", data: { messageId: markerMsgId } });
1000
+ logger.debug("Workflow executor complete", { sessionId, iterations });
1001
+ const doneAction = { type: "DONE" };
1002
+ return {
1003
+ finalAction: doneAction,
1004
+ iterations,
1005
+ totalTime: totalTimeSeconds
1006
+ };
1007
+ }
1008
+ if (nextStepId === TERMINAL_BLOCKED) {
1009
+ sessionManager.setPhase(sessionId, "blocked");
1010
+ const reason = "No matching transition";
1011
+ emitWorkflowMessage(eventStore, sessionId, `Runner blocked: ${reason}`, currentWindowMessageOptions, onMessage);
1012
+ logger.warn("Workflow executor blocked", { sessionId, iterations, reason });
1013
+ const blockedAction = { type: "BLOCKED", reason, blockedCriteria: [] };
1014
+ return {
1015
+ finalAction: blockedAction,
1016
+ iterations,
1017
+ totalTime: (performance.now() - startTime) / 1e3
1018
+ };
1019
+ }
1020
+ currentStepId = nextStepId;
1021
+ }
1022
+ logger.warn("Workflow executor max iterations reached", { sessionId, iterations });
1023
+ return {
1024
+ finalAction: {
1025
+ type: "BLOCKED",
1026
+ reason: `Max iterations (${workflow.settings.maxIterations}) reached`,
1027
+ blockedCriteria: []
1028
+ },
1029
+ iterations,
1030
+ totalTime: (performance.now() - startTime) / 1e3
1031
+ };
1032
+ }
1033
+
1034
+ // src/server/runner/orchestrator.ts
1035
+ async function runOrchestrator(options) {
1036
+ const runtimeConfig = getRuntimeConfig();
1037
+ const workflowId = options.workflowId ?? runtimeConfig.activeWorkflowId ?? "default";
1038
+ const configDir = getGlobalConfigDir(runtimeConfig.mode ?? "production");
1039
+ const workflows = await loadAllWorkflows(configDir);
1040
+ const workflow = findWorkflowById(workflowId, workflows);
1041
+ if (!workflow) {
1042
+ throw new Error(`Workflow "${workflowId}" not found`);
1043
+ }
1044
+ logger.debug("Using workflow executor", {
1045
+ sessionId: options.sessionId,
1046
+ workflow: workflow.metadata.id,
1047
+ subGroup: options.subGroup
1048
+ });
1049
+ return executeWorkflow(workflow, options, options.subGroup);
1050
+ }
1051
+
1052
+ // src/server/auth.ts
1053
+ import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
1054
+ import { join as join2, dirname as dirname2 } from "path";
1055
+ import { createHash, privateDecrypt, createPublicKey } from "crypto";
1056
+ function getAuthConfigPath() {
1057
+ const configDir = getRuntimeConfig();
1058
+ const mode = configDir.mode === "development" ? "development" : configDir.mode === "test" ? "test" : "production";
1059
+ if (mode === "test") {
1060
+ const cwd = process.cwd();
1061
+ const base = cwd.endsWith("/e2e") ? cwd : join2(cwd, "e2e");
1062
+ const testAuthPath = join2(base, ".openfox-test", "auth.json");
1063
+ return testAuthPath;
1064
+ }
1065
+ const home = process.env["HOME"] || process.env["USERPROFILE"] || "";
1066
+ const basePath = process.env["XDG_CONFIG_HOME"] || `${home}/.config`;
1067
+ const suffix = mode === "development" ? "-dev" : "";
1068
+ return `${basePath}/openfox${suffix}/auth.json`;
1069
+ }
1070
+ function getKeyPath() {
1071
+ const authPath = getAuthConfigPath();
1072
+ const dir = dirname2(authPath);
1073
+ return join2(dir, "auth.key");
1074
+ }
1075
+ var cachedAuth = null;
1076
+ var cachedPrivateKey = null;
1077
+ async function loadPrivateKey() {
1078
+ if (cachedPrivateKey) {
1079
+ return cachedPrivateKey;
1080
+ }
1081
+ const keyPath = getKeyPath();
1082
+ const keyDir = dirname2(keyPath);
1083
+ try {
1084
+ cachedPrivateKey = await readFile2(keyPath, "utf-8");
1085
+ return cachedPrivateKey;
1086
+ } catch {
1087
+ const { privateKey } = await import("crypto").then(
1088
+ (c) => c.generateKeyPairSync("rsa", {
1089
+ modulusLength: 2048,
1090
+ privateKeyEncoding: { type: "pkcs8", format: "pem" },
1091
+ publicKeyEncoding: { type: "spki", format: "pem" }
1092
+ })
1093
+ );
1094
+ await mkdir2(keyDir, { recursive: true });
1095
+ await writeFile2(keyPath, privateKey, { mode: 384 });
1096
+ cachedPrivateKey = privateKey;
1097
+ return privateKey;
1098
+ }
1099
+ }
1100
+ async function loadServerAuthConfig() {
1101
+ const configDir = getRuntimeConfig();
1102
+ const isTestMode = configDir.mode === "test";
1103
+ if (!isTestMode) {
1104
+ if (cachedAuth) {
1105
+ return cachedAuth;
1106
+ }
1107
+ }
1108
+ try {
1109
+ const authPath = getAuthConfigPath();
1110
+ const data = await readFile2(authPath, "utf-8");
1111
+ const authConfig = JSON.parse(data);
1112
+ cachedAuth = authConfig;
1113
+ return authConfig;
1114
+ } catch {
1115
+ return null;
1116
+ }
1117
+ }
1118
+ function getAuthConfig() {
1119
+ return cachedAuth;
1120
+ }
1121
+ function hashPassword(password) {
1122
+ return createHash("sha256").update(password).digest("hex");
1123
+ }
1124
+ function requiresAuth() {
1125
+ return cachedAuth?.strategy === "network";
1126
+ }
1127
+ function hasPassword() {
1128
+ return cachedAuth?.encryptedPassword != null && cachedAuth.encryptedPassword.length > 0;
1129
+ }
1130
+ async function verifyPassword(password) {
1131
+ const encryptedPassword = cachedAuth?.encryptedPassword;
1132
+ if (!encryptedPassword) return false;
1133
+ const privateKey = await loadPrivateKey();
1134
+ try {
1135
+ const decrypted = privateDecrypt({ key: privateKey, padding: 1 }, Buffer.from(encryptedPassword, "base64"));
1136
+ return decrypted.toString() === password;
1137
+ } catch {
1138
+ return false;
1139
+ }
1140
+ }
1141
+ async function tokenFromPassword(password) {
1142
+ const privateKey = await loadPrivateKey();
1143
+ const passwordHash = hashPassword(password);
1144
+ const sign = await import("crypto").then((c) => {
1145
+ const s = c.createSign("SHA256");
1146
+ s.update(passwordHash);
1147
+ s.end();
1148
+ return s.sign(privateKey, "base64");
1149
+ });
1150
+ return sign;
1151
+ }
1152
+ async function isValidToken(token) {
1153
+ if (!cachedAuth?.encryptedPassword) return false;
1154
+ const privateKey = await loadPrivateKey();
1155
+ try {
1156
+ const decrypted = privateDecrypt(
1157
+ { key: privateKey, padding: 1 },
1158
+ Buffer.from(cachedAuth.encryptedPassword, "base64")
1159
+ );
1160
+ const storedPassword = decrypted.toString();
1161
+ const storedHash = hashPassword(storedPassword);
1162
+ const verify = await import("crypto").then((c) => {
1163
+ const v = c.createVerify("SHA256");
1164
+ v.update(storedHash);
1165
+ v.end();
1166
+ return v;
1167
+ });
1168
+ const publicKey = createPublicKey(privateKey).export({ type: "spki", format: "pem" });
1169
+ return verify.verify(publicKey, token, "base64");
1170
+ } catch {
1171
+ return false;
1172
+ }
1173
+ }
1174
+
1175
+ // src/server/ws/server.ts
1176
+ var resolveMcpReady = null;
1177
+ var mcpReadyPromise = new Promise((resolve) => {
1178
+ resolveMcpReady = resolve;
1179
+ });
1180
+ function signalMcpReady() {
1181
+ resolveMcpReady?.();
1182
+ }
1183
+ function moduleGitBranch(cwd) {
1184
+ return new Promise((resolve) => {
1185
+ const proc = spawn3("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
1186
+ cwd,
1187
+ stdio: ["ignore", "pipe", "ignore"]
1188
+ });
1189
+ let stdout = "";
1190
+ proc.stdout.on("data", (data) => {
1191
+ stdout += data.toString();
1192
+ });
1193
+ proc.on("close", (code) => {
1194
+ if (code === 0 && stdout.trim()) {
1195
+ resolve(stdout.trim());
1196
+ } else {
1197
+ resolve(null);
1198
+ }
1199
+ });
1200
+ proc.on("error", () => resolve(null));
1201
+ });
1202
+ }
1203
+ function hashContent(content) {
1204
+ return createHash2("sha256").update(content).digest("hex");
1205
+ }
1206
+ function moduleGitDiff(cwd) {
1207
+ return new Promise((resolve) => {
1208
+ const diffProc = spawn3("git", ["diff", "--name-status", "HEAD"], {
1209
+ cwd,
1210
+ stdio: ["ignore", "pipe", "pipe"]
1211
+ });
1212
+ const statusProc = spawn3("git", ["status", "--porcelain"], {
1213
+ cwd,
1214
+ stdio: ["ignore", "pipe", "pipe"]
1215
+ });
1216
+ let diffStdout = "";
1217
+ let statusStdout = "";
1218
+ let diffExited = false;
1219
+ let statusExited = false;
1220
+ let diffCode = null;
1221
+ let statusCode = null;
1222
+ const processResults = () => {
1223
+ if (!diffExited || !statusExited) return;
1224
+ const raw = diffStdout + statusStdout;
1225
+ const hash = raw ? hashContent(raw) : "";
1226
+ const files = [];
1227
+ if (diffCode === 0) {
1228
+ for (const line of diffStdout.split("\n")) {
1229
+ if (!line.trim()) continue;
1230
+ const [statusChar, ...pathParts] = line.split(" ");
1231
+ const path = pathParts.join(" ") || statusChar || "";
1232
+ if (!path) continue;
1233
+ const status = statusChar === "A" ? "added" : statusChar === "D" ? "deleted" : "modified";
1234
+ files.push({ path, status, additions: 0, deletions: 0 });
1235
+ }
1236
+ }
1237
+ if (statusCode === 0) {
1238
+ for (const line of statusStdout.split("\n")) {
1239
+ if (!line.startsWith("?? ")) continue;
1240
+ const path = line.slice(3).trim();
1241
+ if (!path) continue;
1242
+ files.push({ path, status: "added", additions: 0, deletions: 0 });
1243
+ }
1244
+ }
1245
+ resolve({ hash, files });
1246
+ };
1247
+ diffProc.stdout.on("data", (data) => {
1248
+ diffStdout += data.toString();
1249
+ });
1250
+ statusProc.stdout.on("data", (data) => {
1251
+ statusStdout += data.toString();
1252
+ });
1253
+ diffProc.on("close", (code) => {
1254
+ diffExited = true;
1255
+ diffCode = code;
1256
+ processResults();
1257
+ });
1258
+ statusProc.on("close", (code) => {
1259
+ statusExited = true;
1260
+ statusCode = code;
1261
+ processResults();
1262
+ });
1263
+ diffProc.on("error", () => {
1264
+ diffExited = true;
1265
+ diffCode = 1;
1266
+ processResults();
1267
+ });
1268
+ statusProc.on("error", () => {
1269
+ statusExited = true;
1270
+ statusCode = 1;
1271
+ processResults();
1272
+ });
1273
+ });
1274
+ }
1275
+ var moduleWorkdirLastHash = /* @__PURE__ */ new Map();
1276
+ var moduleWorkdirInterval = /* @__PURE__ */ new Map();
1277
+ var gitPollInterval = parseInt(process.env["OPENFOX_GIT_POLL_INTERVAL"] ?? "", 10) || 1e4;
1278
+ var moduleClients = null;
1279
+ var moduleEnqueueSend = null;
1280
+ function moduleGitPoll(workdir) {
1281
+ ;
1282
+ (async () => {
1283
+ try {
1284
+ const branch = await moduleGitBranch(workdir);
1285
+ const { hash, files } = await moduleGitDiff(workdir);
1286
+ const lastHash = moduleWorkdirLastHash.get(workdir);
1287
+ if (hash !== lastHash) {
1288
+ moduleWorkdirLastHash.set(workdir, hash);
1289
+ const msg = createGitStatusMessage(branch, files);
1290
+ const activeClients = moduleClients;
1291
+ const sendFn = moduleEnqueueSend;
1292
+ if (!activeClients || !sendFn) return;
1293
+ for (const [ws, client] of activeClients) {
1294
+ if (client.activeWorkdir === workdir && ws.readyState === WebSocket2.OPEN) {
1295
+ const seq = client.lastSentSeq + 1;
1296
+ sendFn(client, serializeServerMessage({ ...msg, sessionId: client.activeSessionId ?? "" }), seq);
1297
+ }
1298
+ }
1299
+ }
1300
+ } catch {
1301
+ }
1302
+ })();
1303
+ }
1304
+ function moduleStartGitPolling(workdir) {
1305
+ if (moduleWorkdirInterval.has(workdir)) return;
1306
+ const interval = setInterval(() => moduleGitPoll(workdir), gitPollInterval);
1307
+ moduleWorkdirInterval.set(workdir, interval);
1308
+ }
1309
+ function moduleStopGitPolling(workdir) {
1310
+ const interval = moduleWorkdirInterval.get(workdir);
1311
+ if (interval !== void 0) {
1312
+ clearInterval(interval);
1313
+ moduleWorkdirInterval.delete(workdir);
1314
+ moduleWorkdirLastHash.delete(workdir);
1315
+ }
1316
+ }
1317
+ function resolveStatsIdentity(llmClient, getActiveProvider) {
1318
+ const provider = getActiveProvider?.();
1319
+ const model = llmClient.getModel();
1320
+ const backend = provider?.backend ?? (llmClient.getBackend() === "unknown" ? "unknown" : llmClient.getBackend());
1321
+ return {
1322
+ providerId: provider?.id ?? `provider:${model}`,
1323
+ providerName: provider?.name ?? "Unknown Provider",
1324
+ backend,
1325
+ model
1326
+ };
1327
+ }
1328
+ function addUserMessageAndBroadcast(sessionManager, sessionId, message, broadcastFn) {
1329
+ const userMessage = sessionManager.addMessage(sessionId, {
1330
+ role: "user",
1331
+ content: message.content,
1332
+ ...message.attachments ? { attachments: message.attachments } : {},
1333
+ ...message.messageKind ? { messageKind: message.messageKind } : {}
1334
+ });
1335
+ broadcastFn(sessionId, createChatMessageMessage(userMessage));
1336
+ return userMessage;
1337
+ }
1338
+ function processQueueAndRestartTurn(sessionManager, sessionId, drainFn, queueMode, broadcastFn, activeAgents2, startTurnFn, queueMessageFn) {
1339
+ const messages = drainFn(sessionId);
1340
+ const next = messages[0];
1341
+ if (!next) return false;
1342
+ for (const remaining of messages.slice(1)) {
1343
+ if (queueMessageFn) {
1344
+ queueMessageFn(
1345
+ sessionId,
1346
+ queueMode,
1347
+ remaining.content,
1348
+ remaining.attachments,
1349
+ remaining.messageKind
1350
+ );
1351
+ } else {
1352
+ sessionManager.queueMessage(sessionId, queueMode, remaining.content, remaining.attachments);
1353
+ }
1354
+ }
1355
+ broadcastFn(sessionId, createQueueStateMessage(sessionManager.getQueueState(sessionId)));
1356
+ addUserMessageAndBroadcast(
1357
+ sessionManager,
1358
+ sessionId,
1359
+ {
1360
+ content: next.content,
1361
+ ...next.attachments ? { attachments: next.attachments } : {},
1362
+ ...next.messageKind ? { messageKind: next.messageKind } : {}
1363
+ },
1364
+ broadcastFn
1365
+ );
1366
+ const newController = new AbortController();
1367
+ activeAgents2.set(sessionId, newController);
1368
+ startTurnFn(sessionId, newController);
1369
+ return true;
1370
+ }
1371
+ var activeAgents = /* @__PURE__ */ new Map();
1372
+ var MAX_SEND_QUEUE_SIZE = 1e3;
1373
+ function createWebSocketServer(httpServer, config, getLLMClient, getActiveProvider, sessionManager, providerManager) {
1374
+ const wss = new WebSocketServer({ server: httpServer });
1375
+ const clients = /* @__PURE__ */ new Map();
1376
+ moduleClients = clients;
1377
+ const sessionLLMClients = /* @__PURE__ */ new Map();
1378
+ function getSessionLLMClient(sessionId) {
1379
+ const session = sessionManager.getSession(sessionId);
1380
+ if (!session?.providerId || !session?.providerModel || !providerManager) {
1381
+ return getLLMClient();
1382
+ }
1383
+ const cacheKey = `${session.providerId}:${session.providerModel}`;
1384
+ const cached = sessionLLMClients.get(sessionId);
1385
+ if (cached && cached.key === cacheKey) {
1386
+ return cached.client;
1387
+ }
1388
+ const provider = providerManager.getProviders().find((p) => p.id === session.providerId);
1389
+ if (!provider) {
1390
+ logger.warn("Session references deleted provider, falling back to global", {
1391
+ sessionId,
1392
+ providerId: session.providerId
1393
+ });
1394
+ sessionManager.setSessionProvider(sessionId, null, null);
1395
+ sessionLLMClients.delete(sessionId);
1396
+ return getLLMClient();
1397
+ }
1398
+ const modelConfig = provider.models.find((m) => m.id === session.providerModel);
1399
+ const reasoningEffort = modelConfig?.thinkingEnabled && modelConfig?.thinkingLevel ? modelConfig.thinkingLevel : void 0;
1400
+ const baseUrl = ensureVersionPrefix(provider.url);
1401
+ const sessionConfig = {
1402
+ ...config,
1403
+ llm: {
1404
+ ...config.llm,
1405
+ baseUrl,
1406
+ model: session.providerModel,
1407
+ ...provider.apiKey && { apiKey: provider.apiKey },
1408
+ ...provider.thinkingField && { thinkingField: provider.thinkingField },
1409
+ ...reasoningEffort && { reasoningEffort }
1410
+ }
1411
+ };
1412
+ const client = createLLMClient(sessionConfig);
1413
+ if (provider.backend !== "unknown") {
1414
+ client.setBackend(provider.backend);
1415
+ }
1416
+ client.setModel(session.providerModel);
1417
+ sessionLLMClients.set(sessionId, { key: cacheKey, client });
1418
+ return client;
1419
+ }
1420
+ function getSessionStatsIdentity(sessionId) {
1421
+ const session = sessionManager.getSession(sessionId);
1422
+ if (!session?.providerId || !providerManager) {
1423
+ return resolveStatsIdentity(getLLMClient(), getActiveProvider);
1424
+ }
1425
+ const provider = providerManager.getProviders().find((p) => p.id === session.providerId);
1426
+ const client = getSessionLLMClient(sessionId);
1427
+ return {
1428
+ providerId: provider?.id ?? session.providerId,
1429
+ providerName: provider?.name ?? "Unknown Provider",
1430
+ backend: provider?.backend ?? client.getBackend(),
1431
+ model: client.getModel()
1432
+ };
1433
+ }
1434
+ const isSubscribedToSession = (client, sessionId) => {
1435
+ return client.activeSessionId === sessionId;
1436
+ };
1437
+ const broadcastForSession = (sessionId, msg) => {
1438
+ for (const [clientWs, client] of clients) {
1439
+ if (isSubscribedToSession(client, sessionId) && clientWs.readyState === WebSocket2.OPEN) {
1440
+ clientWs.send(serializeServerMessage({ ...msg, sessionId }));
1441
+ }
1442
+ }
1443
+ };
1444
+ function enqueueSend(client, data, seq) {
1445
+ if (client.sendQueue.length >= MAX_SEND_QUEUE_SIZE) {
1446
+ logger.warn("WebSocket send queue full, dropping message", {
1447
+ queueSize: client.sendQueue.length,
1448
+ sessionId: client.activeSessionId
1449
+ });
1450
+ return;
1451
+ }
1452
+ client.sendQueue.push({ data, seq });
1453
+ processSendQueue(client);
1454
+ }
1455
+ moduleEnqueueSend = enqueueSend;
1456
+ function processSendQueue(client) {
1457
+ if (client.isSending || client.sendQueue.length === 0) {
1458
+ return;
1459
+ }
1460
+ client.isSending = true;
1461
+ const item = client.sendQueue.shift();
1462
+ if (client.ws.readyState === WebSocket2.OPEN) {
1463
+ client.ws.send(item.data, (err) => {
1464
+ if (err) {
1465
+ logger.debug("WebSocket send error", { error: err });
1466
+ }
1467
+ client.isSending = false;
1468
+ client.lastSentSeq = item.seq;
1469
+ processSendQueue(client);
1470
+ });
1471
+ } else {
1472
+ client.isSending = false;
1473
+ processSendQueue(client);
1474
+ }
1475
+ }
1476
+ const llmForSession = (sessionId) => getSessionLLMClient?.(sessionId) ?? getLLMClient();
1477
+ const statsForSession = (sessionId) => getSessionStatsIdentity?.(sessionId) ?? resolveStatsIdentity(getLLMClient(), getActiveProvider);
1478
+ function cleanupAfterTurn(sessionId, controller, sendFn, setRunningOnEarlyReturn) {
1479
+ if (activeAgents.get(sessionId) !== controller) {
1480
+ return;
1481
+ }
1482
+ activeAgents.delete(sessionId);
1483
+ const processed = processQueueAndRestartTurn(
1484
+ sessionManager,
1485
+ sessionId,
1486
+ (sid) => sessionManager.drainAsapMessages(sid),
1487
+ "asap",
1488
+ sendFn,
1489
+ activeAgents,
1490
+ startTurnWithCompletionChain,
1491
+ (sid, mode, content, attachments, messageKind) => sessionManager.queueMessage(sid, mode, content, attachments, messageKind)
1492
+ );
1493
+ if (processed) {
1494
+ if (setRunningOnEarlyReturn) sessionManager.setRunning(sessionId, false);
1495
+ return;
1496
+ }
1497
+ const processedCompletion = processQueueAndRestartTurn(
1498
+ sessionManager,
1499
+ sessionId,
1500
+ (sid) => sessionManager.drainCompletionMessages(sid),
1501
+ "completion",
1502
+ sendFn,
1503
+ activeAgents,
1504
+ startTurnWithCompletionChain
1505
+ );
1506
+ if (processedCompletion) {
1507
+ if (setRunningOnEarlyReturn) sessionManager.setRunning(sessionId, false);
1508
+ return;
1509
+ }
1510
+ sessionManager.clearMessageQueue(sessionId);
1511
+ const contextState = sessionManager.getContextState(sessionId);
1512
+ sendFn(sessionId, createContextStateMessage(contextState));
1513
+ }
1514
+ function startTurnWithCompletionChain(sessionId, controller) {
1515
+ runChatTurn({
1516
+ sessionManager,
1517
+ sessionId,
1518
+ llmClient: llmForSession(sessionId),
1519
+ statsIdentity: statsForSession(sessionId),
1520
+ signal: controller.signal,
1521
+ onMessage: (msg) => broadcastForSession(sessionId, msg)
1522
+ }).catch((error) => {
1523
+ if (error instanceof Error && error.message === "Aborted") {
1524
+ return;
1525
+ }
1526
+ logger.error("Chat turn error", { error });
1527
+ }).finally(() => {
1528
+ cleanupAfterTurn(sessionId, controller, broadcastForSession, false);
1529
+ });
1530
+ }
1531
+ const broadcastAll = (msg) => {
1532
+ const serialized = serializeServerMessage(msg);
1533
+ for (const [clientWs, client] of clients) {
1534
+ if (clientWs.readyState === WebSocket2.OPEN) {
1535
+ const seq = client.lastSentSeq + 1;
1536
+ enqueueSend(client, serialized, seq);
1537
+ }
1538
+ }
1539
+ };
1540
+ devServerManager.onOutput((workdir, chunk) => {
1541
+ broadcastAll(
1542
+ createServerMessage("devServer.output", {
1543
+ workdir,
1544
+ stream: chunk.stream,
1545
+ content: chunk.content
1546
+ })
1547
+ );
1548
+ });
1549
+ devServerManager.onStateChange((workdir, state, errorMessage) => {
1550
+ broadcastAll(
1551
+ createServerMessage("devServer.state", {
1552
+ workdir,
1553
+ state,
1554
+ errorMessage
1555
+ })
1556
+ );
1557
+ });
1558
+ onProcessEvent((_processId, msg) => {
1559
+ const sessionId = msg.sessionId;
1560
+ if (!sessionId) return;
1561
+ for (const [clientWs, client] of clients) {
1562
+ if (client.activeSessionId === sessionId) {
1563
+ if (clientWs.readyState === WebSocket2.OPEN) {
1564
+ const seq = client.lastSentSeq + 1;
1565
+ enqueueSend(client, serializeServerMessage(msg), seq);
1566
+ }
1567
+ }
1568
+ }
1569
+ });
1570
+ wss.on("connection", async (ws, req) => {
1571
+ const url = new URL(req.url || "/", `http://${req.headers.host}`);
1572
+ const token = url.searchParams.get("token");
1573
+ const authConfig = getAuthConfig();
1574
+ if (authConfig?.strategy === "network" && authConfig.encryptedPassword) {
1575
+ if (!token || !await isValidToken(token)) {
1576
+ setTimeout(() => {
1577
+ ws.close(4e3, "Unauthorized");
1578
+ }, 100);
1579
+ return;
1580
+ }
1581
+ }
1582
+ logger.debug("WebSocket client connected");
1583
+ clients.set(ws, {
1584
+ ws,
1585
+ activeSessionId: null,
1586
+ activeWorkdir: null,
1587
+ globalSubscription: null,
1588
+ sendQueue: [],
1589
+ isSending: false,
1590
+ lastSentSeq: 0
1591
+ });
1592
+ const eventStore = getEventStore();
1593
+ const { iterator: globalIterator, unsubscribe: globalUnsubscribe } = eventStore.subscribeAll();
1594
+ clients.get(ws).globalSubscription = globalUnsubscribe;
1595
+ (async () => {
1596
+ try {
1597
+ for await (const storedEvent of globalIterator) {
1598
+ if (ws.readyState !== WebSocket2.OPEN) break;
1599
+ const serverMsg = storedEventToServerMessage(storedEvent);
1600
+ if (serverMsg) {
1601
+ const client = clients.get(ws);
1602
+ enqueueSend(
1603
+ client,
1604
+ serializeServerMessage({ ...serverMsg, seq: storedEvent.seq, sessionId: storedEvent.sessionId }),
1605
+ storedEvent.seq
1606
+ );
1607
+ }
1608
+ }
1609
+ } catch (error) {
1610
+ logger.debug("Global event subscription ended", { error });
1611
+ }
1612
+ })();
1613
+ ws.on("message", async (data) => {
1614
+ const message = parseClientMessage(data.toString());
1615
+ if (!message) {
1616
+ const client2 = clients.get(ws);
1617
+ const seq = client2.lastSentSeq + 1;
1618
+ enqueueSend(
1619
+ client2,
1620
+ serializeServerMessage(createErrorMessage("INVALID_MESSAGE", "Invalid message format")),
1621
+ seq
1622
+ );
1623
+ return;
1624
+ }
1625
+ if (message.type.startsWith("terminal.")) {
1626
+ handleTerminalMessage(ws, message);
1627
+ return;
1628
+ }
1629
+ const client = clients.get(ws);
1630
+ try {
1631
+ await handleClientMessage(
1632
+ ws,
1633
+ client,
1634
+ message,
1635
+ getLLMClient,
1636
+ getActiveProvider,
1637
+ sessionManager,
1638
+ broadcastForSession,
1639
+ providerManager,
1640
+ getSessionLLMClient,
1641
+ getSessionStatsIdentity,
1642
+ llmForSession,
1643
+ statsForSession,
1644
+ startTurnWithCompletionChain,
1645
+ cleanupAfterTurn,
1646
+ enqueueSend
1647
+ );
1648
+ } catch (error) {
1649
+ logger.error("Error handling client message", { error, type: message.type });
1650
+ const client2 = clients.get(ws);
1651
+ const seq = client2.lastSentSeq + 1;
1652
+ enqueueSend(
1653
+ client2,
1654
+ serializeServerMessage(
1655
+ createErrorMessage("INTERNAL_ERROR", error instanceof Error ? error.message : "Unknown error", message.id)
1656
+ ),
1657
+ seq
1658
+ );
1659
+ }
1660
+ });
1661
+ ws.on("close", () => {
1662
+ logger.debug("WebSocket client disconnected");
1663
+ const client = clients.get(ws);
1664
+ const disconnectedWorkdir = client?.activeWorkdir ?? null;
1665
+ if (client?.globalSubscription) {
1666
+ client.globalSubscription();
1667
+ }
1668
+ unsubscribeAllFromTerminal(ws);
1669
+ clients.delete(ws);
1670
+ if (disconnectedWorkdir) {
1671
+ const hasRemaining = [...clients.values()].some((c) => c.activeWorkdir === disconnectedWorkdir);
1672
+ if (!hasRemaining) {
1673
+ moduleStopGitPolling(disconnectedWorkdir);
1674
+ }
1675
+ }
1676
+ });
1677
+ ws.on("error", (error) => {
1678
+ logger.error("WebSocket error", { error });
1679
+ });
1680
+ });
1681
+ return {
1682
+ wss,
1683
+ abortSession: (sessionId) => {
1684
+ const controller = activeAgents.get(sessionId);
1685
+ if (controller) {
1686
+ activeAgents.delete(sessionId);
1687
+ controller.abort();
1688
+ return true;
1689
+ }
1690
+ return false;
1691
+ },
1692
+ close: (cb) => wss.close(cb),
1693
+ broadcastForSession,
1694
+ broadcastAll
1695
+ };
1696
+ }
1697
+ async function handleClientMessage(ws, client, message, _getLLMClient, _getActiveProvider, sessionManager, _broadcastForSession, _providerManager, _getSessionLLMClient, _getSessionStatsIdentity, llmForSession, statsForSession, _startTurnWithCompletionChain, cleanupAfterTurn, enqueueSendFn) {
1698
+ const send = (msg) => {
1699
+ if (ws.readyState === WebSocket2.OPEN) {
1700
+ const seq = client.lastSentSeq + 1;
1701
+ enqueueSendFn(client, serializeServerMessage(msg), seq);
1702
+ }
1703
+ };
1704
+ const sendForSession = (sessionId, msg) => {
1705
+ send({ ...msg, sessionId });
1706
+ };
1707
+ const ensureEventStoreSubscription = (_sessionId) => {
1708
+ };
1709
+ switch (message.type) {
1710
+ // =========================================================================
1711
+ // DEPRECATED: All CRUD operations moved to REST API
1712
+ // If you see this error, update your code to use REST endpoints instead.
1713
+ // See docs/REST-API.md for details.
1714
+ // =========================================================================
1715
+ case "project.create":
1716
+ case "project.create-with-dir":
1717
+ case "project.list":
1718
+ case "project.load":
1719
+ case "project.update":
1720
+ case "project.delete":
1721
+ case "settings.get":
1722
+ case "settings.set":
1723
+ case "session.create":
1724
+ case "session.list":
1725
+ case "session.delete":
1726
+ case "session.deleteAll":
1727
+ case "session.setProvider":
1728
+ send(
1729
+ createErrorMessage(
1730
+ "DEPRECATED_MESSAGE_TYPE",
1731
+ `${message.type} removed. Use REST API instead. See docs/REST-API.md`,
1732
+ message.id
1733
+ )
1734
+ );
1735
+ return;
1736
+ // =========================================================================
1737
+ // Session Load - Sets active session for event routing
1738
+ // Note: Data loading is done via REST API: GET /api/sessions/:id
1739
+ // This WebSocket message is ONLY to tell the server which session is active
1740
+ // so it can route real-time events correctly.
1741
+ // =========================================================================
1742
+ case "session.load": {
1743
+ if (!isSessionLoadPayload(message.payload)) {
1744
+ send(createErrorMessage("INVALID_PAYLOAD", "Invalid session.load payload", message.id));
1745
+ return;
1746
+ }
1747
+ const session = sessionManager.getSession(message.payload.sessionId);
1748
+ if (!session) {
1749
+ send(createErrorMessage("NOT_FOUND", "Session not found", message.id));
1750
+ return;
1751
+ }
1752
+ client.activeSessionId = session.id;
1753
+ client.activeWorkdir = session.workdir;
1754
+ if (session.workdir) {
1755
+ const branch = await moduleGitBranch(session.workdir);
1756
+ const { files } = await moduleGitDiff(session.workdir);
1757
+ const msg = createGitStatusMessage(branch, files);
1758
+ send(msg);
1759
+ if (branch) moduleStartGitPolling(session.workdir);
1760
+ }
1761
+ ensureEventStoreSubscription(session.id);
1762
+ send({ type: "ack", payload: { sessionId: session.id }, id: message.id });
1763
+ const sendContextState = () => {
1764
+ const contextState = sessionManager.getContextState(session.id);
1765
+ send(createContextStateMessage(contextState));
1766
+ };
1767
+ sendContextState();
1768
+ const cachedHash = sessionManager.getCachedPrompt(session.id)?.hash;
1769
+ if (cachedHash) {
1770
+ ;
1771
+ (async () => {
1772
+ try {
1773
+ await mcpReadyPromise;
1774
+ const { content: instructionContent } = await getAllInstructions(session.workdir, session.projectId);
1775
+ const runtimeConfig = getRuntimeConfig();
1776
+ const configDir = getGlobalConfigDir(runtimeConfig.mode ?? "production");
1777
+ const skills = await getEnabledSkillMetadata(configDir, runtimeConfig.workdir);
1778
+ const { createToolRegistry } = await import("./tools-WEE4XDTL.js");
1779
+ const allTools = createToolRegistry().definitions;
1780
+ const toolFingerprint = getToolFingerprint(allTools);
1781
+ const currentHash = computeDynamicContextHash(instructionContent, skills, toolFingerprint);
1782
+ if (currentHash !== cachedHash) {
1783
+ sessionManager.setDynamicContextChanged(session.id, true);
1784
+ sendContextState();
1785
+ } else if (sessionManager.getDynamicContextChanged(session.id)) {
1786
+ sessionManager.setDynamicContextChanged(session.id, false);
1787
+ sendContextState();
1788
+ }
1789
+ } catch {
1790
+ }
1791
+ })();
1792
+ }
1793
+ break;
1794
+ }
1795
+ // =========================================================================
1796
+ // Context Management
1797
+ // =========================================================================
1798
+ case "context.compact": {
1799
+ if (!client.activeSessionId) {
1800
+ send(createErrorMessage("NO_SESSION", "No active session", message.id));
1801
+ return;
1802
+ }
1803
+ const session = sessionManager.requireSession(client.activeSessionId);
1804
+ const sessionId = client.activeSessionId;
1805
+ if (session.isRunning) {
1806
+ send(createErrorMessage("SESSION_RUNNING", "Cannot compact while session is running", message.id));
1807
+ return;
1808
+ }
1809
+ send({ type: "ack", payload: {}, id: message.id });
1810
+ appendCompactionPrompt(sessionId, (event) => getEventStore().append(sessionId, event));
1811
+ runChatTurn({
1812
+ sessionManager,
1813
+ sessionId,
1814
+ llmClient: llmForSession(sessionId),
1815
+ statsIdentity: statsForSession(sessionId),
1816
+ onMessage: (msg) => _broadcastForSession(sessionId, msg),
1817
+ initialCompacting: true
1818
+ }).then(() => {
1819
+ const newContextState = sessionManager.getContextState(sessionId);
1820
+ sendForSession(sessionId, createContextStateMessage(newContextState));
1821
+ }).catch((error) => {
1822
+ if (error instanceof Error && error.message === "Aborted") return;
1823
+ logger.error("Compaction failed", { error, sessionId });
1824
+ sendForSession(
1825
+ sessionId,
1826
+ createChatErrorMessage(
1827
+ `Compaction failed: ${error instanceof Error ? error.message : "Unknown error"}`,
1828
+ true
1829
+ )
1830
+ );
1831
+ }).finally(() => {
1832
+ sessionManager.setRunning(sessionId, false);
1833
+ sendForSession(sessionId, createSessionRunningMessage(false));
1834
+ });
1835
+ break;
1836
+ }
1837
+ case "context.checkDynamic": {
1838
+ if (!client.activeSessionId) {
1839
+ send(createErrorMessage("NO_SESSION", "No active session", message.id));
1840
+ return;
1841
+ }
1842
+ const sessionId = client.activeSessionId;
1843
+ const session = sessionManager.requireSession(sessionId);
1844
+ (async () => {
1845
+ try {
1846
+ await mcpReadyPromise;
1847
+ const { content: instructionContent } = await getAllInstructions(session.workdir, session.projectId);
1848
+ const runtimeConfig = getRuntimeConfig();
1849
+ const configDir = getGlobalConfigDir(runtimeConfig.mode ?? "production");
1850
+ const skills = await getEnabledSkillMetadata(configDir, runtimeConfig.workdir);
1851
+ const { createToolRegistry } = await import("./tools-WEE4XDTL.js");
1852
+ const allTools = createToolRegistry().definitions;
1853
+ const toolFingerprint = getToolFingerprint(allTools);
1854
+ const currentHash = computeDynamicContextHash(instructionContent, skills, toolFingerprint);
1855
+ const cachedHash = sessionManager.getCachedPrompt(sessionId)?.hash;
1856
+ if (cachedHash) {
1857
+ if (currentHash !== cachedHash) {
1858
+ logger.debug("checkDynamic: hash mismatch", {
1859
+ sessionId,
1860
+ cachedHash,
1861
+ currentHash,
1862
+ toolCount: allTools.length
1863
+ });
1864
+ if (!sessionManager.getDynamicContextChanged(sessionId)) {
1865
+ sessionManager.setDynamicContextChanged(sessionId, true);
1866
+ const newContextState = sessionManager.getContextState(sessionId);
1867
+ sendForSession(sessionId, createContextStateMessage(newContextState));
1868
+ }
1869
+ } else if (sessionManager.getDynamicContextChanged(sessionId)) {
1870
+ sessionManager.setDynamicContextChanged(sessionId, false);
1871
+ const newContextState = sessionManager.getContextState(sessionId);
1872
+ sendForSession(sessionId, createContextStateMessage(newContextState));
1873
+ }
1874
+ }
1875
+ send({ type: "ack", payload: {}, id: message.id });
1876
+ } catch (error) {
1877
+ logger.error("Failed to check dynamic context", { error, sessionId });
1878
+ send({ type: "ack", payload: {}, id: message.id });
1879
+ }
1880
+ })();
1881
+ break;
1882
+ }
1883
+ case "context.applyDynamic": {
1884
+ if (!client.activeSessionId) {
1885
+ send(createErrorMessage("NO_SESSION", "No active session", message.id));
1886
+ return;
1887
+ }
1888
+ const sessionId = client.activeSessionId;
1889
+ const session = sessionManager.requireSession(sessionId);
1890
+ if (session.isRunning) {
1891
+ send(createErrorMessage("SESSION_RUNNING", "Cannot apply dynamic context while session is running", message.id));
1892
+ return;
1893
+ }
1894
+ ;
1895
+ (async () => {
1896
+ try {
1897
+ await applyDynamicContext(sessionManager, sessionId);
1898
+ const newContextState = sessionManager.getContextState(sessionId);
1899
+ sendForSession(sessionId, createContextStateMessage(newContextState));
1900
+ send({ type: "ack", payload: {}, id: message.id });
1901
+ } catch (error) {
1902
+ logger.error("Failed to apply dynamic context", { error, sessionId });
1903
+ sendForSession(
1904
+ sessionId,
1905
+ createChatErrorMessage(
1906
+ `Failed to apply dynamic context: ${error instanceof Error ? error.message : "Unknown error"}`,
1907
+ true
1908
+ )
1909
+ );
1910
+ send({ type: "ack", payload: {}, id: message.id });
1911
+ }
1912
+ })();
1913
+ break;
1914
+ }
1915
+ // =========================================================================
1916
+ // Runner (Auto-Loop)
1917
+ // =========================================================================
1918
+ case "runner.launch": {
1919
+ if (!client.activeSessionId) {
1920
+ send(createErrorMessage("NO_SESSION", "No active session", message.id));
1921
+ return;
1922
+ }
1923
+ const session = sessionManager.requireSession(client.activeSessionId);
1924
+ if (session.isRunning) {
1925
+ const launchPayload2 = message.payload;
1926
+ const content = launchPayload2?.content ?? "";
1927
+ const attachments = launchPayload2?.attachments;
1928
+ const workflowId = launchPayload2?.workflowId;
1929
+ let fullContent = content;
1930
+ if (workflowId) {
1931
+ const workflowInfo = `// Workflow: ${workflowId}`;
1932
+ fullContent = content ? `${workflowInfo}
1933
+
1934
+ ${content}` : workflowInfo;
1935
+ }
1936
+ sessionManager.queueMessage(client.activeSessionId, "asap", fullContent, attachments, "workflow-launch");
1937
+ const queueState = sessionManager.getQueueState(client.activeSessionId);
1938
+ send({
1939
+ type: "queue.state",
1940
+ payload: { success: true, queueState },
1941
+ id: message.id
1942
+ });
1943
+ return;
1944
+ }
1945
+ const launchPayloadEarly = message.payload;
1946
+ const pendingCriteria = session.criteria.filter((c) => c.status.type !== "passed");
1947
+ if (!launchPayloadEarly?.workflowId && pendingCriteria.length === 0) {
1948
+ send(createErrorMessage("NO_WORK", "No pending criteria to work on", message.id));
1949
+ return;
1950
+ }
1951
+ const sessionId = client.activeSessionId;
1952
+ if (session.phase === "blocked") {
1953
+ logger.info("User launched runner - resetting blocked state", { sessionId });
1954
+ sessionManager.setPhase(sessionId, "build");
1955
+ }
1956
+ const launchPayload = message.payload;
1957
+ const launchAttachments = launchPayload?.attachments;
1958
+ const hasUserContent = launchPayload?.content && typeof launchPayload.content === "string" && launchPayload.content.trim();
1959
+ const hasUserAttachments = launchAttachments && launchAttachments.length > 0;
1960
+ const hasUserMessage = hasUserContent || hasUserAttachments;
1961
+ sessionManager.setRunning(sessionId, true);
1962
+ sendForSession(sessionId, createSessionRunningMessage(true));
1963
+ const controller = new AbortController();
1964
+ const existingController = activeAgents.get(sessionId);
1965
+ if (existingController) {
1966
+ logger.warn("Aborting existing agent before starting new one", { sessionId });
1967
+ existingController.abort();
1968
+ }
1969
+ activeAgents.set(sessionId, controller);
1970
+ send({ type: "ack", payload: {}, id: message.id });
1971
+ ensureEventStoreSubscription(sessionId);
1972
+ logger.info("Runner launching", { sessionId, pendingCriteria: pendingCriteria.length });
1973
+ runOrchestrator({
1974
+ sessionManager,
1975
+ sessionId,
1976
+ llmClient: llmForSession(sessionId),
1977
+ statsIdentity: statsForSession(sessionId),
1978
+ injectWorkflowKickoff: !hasUserMessage,
1979
+ ...launchPayload?.workflowId ? { workflowId: launchPayload.workflowId } : {},
1980
+ ...launchPayload?.subGroup ? { subGroup: launchPayload.subGroup } : {},
1981
+ ...hasUserMessage ? {
1982
+ userMessage: {
1983
+ content: hasUserContent ? launchPayload.content : "",
1984
+ ...hasUserAttachments ? { attachments: launchAttachments } : {}
1985
+ }
1986
+ } : {},
1987
+ signal: controller.signal,
1988
+ onMessage: (msg) => sendForSession(sessionId, msg)
1989
+ // For path confirmation dialogs
1990
+ }).catch((error) => {
1991
+ if (error instanceof Error && error.message === "Aborted") {
1992
+ return;
1993
+ }
1994
+ logger.error("Runner error", { error, sessionId });
1995
+ }).finally(() => {
1996
+ sessionManager.setRunning(sessionId, false);
1997
+ sendForSession(sessionId, createSessionRunningMessage(false));
1998
+ cleanupAfterTurn(sessionId, controller, sendForSession, true);
1999
+ });
2000
+ break;
2001
+ }
2002
+ // =========================================================================
2003
+ // Path Confirmation
2004
+ // =========================================================================
2005
+ case "path.confirm": {
2006
+ send(
2007
+ createErrorMessage(
2008
+ "DEPRECATED",
2009
+ "path.confirm removed. Use REST API: POST /api/sessions/:id/confirm-path",
2010
+ message.id
2011
+ )
2012
+ );
2013
+ break;
2014
+ }
2015
+ // =========================================================================
2016
+ // Ask User
2017
+ // =========================================================================
2018
+ case "ask.answer": {
2019
+ if (!client.activeSessionId) {
2020
+ send(createErrorMessage("NO_SESSION", "No active session", message.id));
2021
+ return;
2022
+ }
2023
+ if (!isAskAnswerPayload(message.payload)) {
2024
+ send(createErrorMessage("INVALID_PAYLOAD", "Invalid ask.answer payload", message.id));
2025
+ return;
2026
+ }
2027
+ const { callId, answer, skip } = message.payload;
2028
+ const found = provideAnswer(callId, answer, skip);
2029
+ if (!found) {
2030
+ send(createErrorMessage("NOT_FOUND", "No pending question with that ID", message.id));
2031
+ return;
2032
+ }
2033
+ logger.debug("Ask user answer received", {
2034
+ sessionId: client.activeSessionId,
2035
+ callId,
2036
+ answerLength: answer.length
2037
+ });
2038
+ send({ type: "ack", payload: {}, id: message.id });
2039
+ break;
2040
+ }
2041
+ default: {
2042
+ send(createErrorMessage("UNKNOWN_MESSAGE", `Unknown message type: ${message.type}`, message.id));
2043
+ }
2044
+ }
2045
+ }
2046
+
2047
+ export {
2048
+ terminalManager,
2049
+ loadDefaultWorkflows,
2050
+ loadUserWorkflows,
2051
+ loadProjectWorkflows,
2052
+ loadAllWorkflows,
2053
+ saveWorkflowToProject,
2054
+ deleteProjectWorkflow,
2055
+ getDefaultWorkflowIds,
2056
+ isDefaultWorkflow,
2057
+ findWorkflowById,
2058
+ workflowExists,
2059
+ saveWorkflow,
2060
+ deleteWorkflow,
2061
+ TEMPLATE_VARIABLES,
2062
+ loadServerAuthConfig,
2063
+ getAuthConfig,
2064
+ requiresAuth,
2065
+ hasPassword,
2066
+ verifyPassword,
2067
+ tokenFromPassword,
2068
+ isValidToken,
2069
+ signalMcpReady,
2070
+ createWebSocketServer
2071
+ };
2072
+ //# sourceMappingURL=chunk-EKTGRBDC.js.map