openfox 0.1.0 → 0.2.1

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 (62) hide show
  1. package/dist/agent-defaults/builder.agent.md +38 -0
  2. package/dist/agent-defaults/code-reviewer.agent.md +21 -0
  3. package/dist/agent-defaults/debugger.agent.md +21 -0
  4. package/dist/agent-defaults/planner.agent.md +35 -0
  5. package/dist/agent-defaults/test-generator.agent.md +22 -0
  6. package/dist/agent-defaults/verifier.agent.md +41 -0
  7. package/dist/{chunk-FF6Z2MC3.js → chunk-5ZUU23XF.js} +18 -1
  8. package/dist/{chunk-IVHCPQO2.js → chunk-ARFK3RZW.js} +1 -1
  9. package/dist/{chunk-IKAWIOXM.js → chunk-FUUYYDVG.js} +387 -73
  10. package/dist/{chunk-YQ36AK5A.js → chunk-JFQEATRC.js} +623 -327
  11. package/dist/{chunk-43SWNGYW.js → chunk-L475ND2Z.js} +9 -9
  12. package/dist/{chunk-6MOGZMGX.js → chunk-L4XDA3V7.js} +2 -2
  13. package/dist/{chunk-EI46G5WO.js → chunk-MKAHOQBI.js} +10 -10
  14. package/dist/cli/dev.js +1 -1
  15. package/dist/cli/index.js +1 -1
  16. package/dist/{config-6GJ4SVFH.js → config-BMXSDOZB.js} +3 -3
  17. package/dist/{init-UVZDOHMX.js → init-PLRTRSDC.js} +6 -6
  18. package/dist/{protocol-BJsm88OW.d.ts → protocol-BFFz3WM0.d.ts} +14 -3
  19. package/dist/{provider-LCE6WZA6.js → provider-ETVY64A2.js} +5 -5
  20. package/dist/{serve-27DZK34K.js → serve-EOKIXTWM.js} +8 -8
  21. package/dist/server/index.d.ts +1 -1
  22. package/dist/server/index.js +5 -5
  23. package/dist/shared/index.d.ts +2 -2
  24. package/dist/shared/index.js +1 -1
  25. package/dist/{tools-R4YQAYUF.js → tools-QMIIDJTV.js} +7 -5
  26. package/dist/web/assets/index-CAD7Iv_d.js +122 -0
  27. package/dist/web/assets/{index-BSlSWZrM.css → index-DHmZmhnR.css} +1 -1
  28. package/dist/web/index.html +2 -2
  29. package/dist/workflow-defaults/default.workflow.json +45 -0
  30. package/package.json +34 -16
  31. package/dist/branch.api-WDHKZJS6.js.map +0 -1
  32. package/dist/chunk-26NRL3HX.js.map +0 -1
  33. package/dist/chunk-43SWNGYW.js.map +0 -1
  34. package/dist/chunk-4Z2OZ6NX.js.map +0 -1
  35. package/dist/chunk-6MOGZMGX.js.map +0 -1
  36. package/dist/chunk-AF7XS4OI.js.map +0 -1
  37. package/dist/chunk-EI46G5WO.js.map +0 -1
  38. package/dist/chunk-FF6Z2MC3.js.map +0 -1
  39. package/dist/chunk-IKAWIOXM.js.map +0 -1
  40. package/dist/chunk-IVHCPQO2.js.map +0 -1
  41. package/dist/chunk-MTX5Y3V3.js.map +0 -1
  42. package/dist/chunk-N2XYX4ZA.js.map +0 -1
  43. package/dist/chunk-QY7BMXWT.js.map +0 -1
  44. package/dist/chunk-U62ZECSQ.js.map +0 -1
  45. package/dist/chunk-YQ36AK5A.js.map +0 -1
  46. package/dist/cli/dev.js.map +0 -1
  47. package/dist/cli/index.js.map +0 -1
  48. package/dist/config-6GJ4SVFH.js.map +0 -1
  49. package/dist/init-UVZDOHMX.js.map +0 -1
  50. package/dist/paths-4BPXWB2W.js.map +0 -1
  51. package/dist/project-creator-CWPKRIKI.js.map +0 -1
  52. package/dist/provider-LCE6WZA6.js.map +0 -1
  53. package/dist/serve-27DZK34K.js.map +0 -1
  54. package/dist/server/index.js.map +0 -1
  55. package/dist/settings-D5O3TMLD.js.map +0 -1
  56. package/dist/shared/index.js.map +0 -1
  57. package/dist/tools-R4YQAYUF.js.map +0 -1
  58. package/dist/web/assets/index-BMNDCsbJ.js +0 -120
  59. /package/dist/command-defaults/{defaults/commit-push.command.md → commit-push.command.md} +0 -0
  60. /package/dist/command-defaults/{defaults/init.command.md → init.command.md} +0 -0
  61. /package/dist/command-defaults/{defaults/test-ui.command.md → test-ui.command.md} +0 -0
  62. /package/dist/skill-defaults/{defaults/playwright-cli.skill.md → playwright-cli.skill.md} +0 -0
@@ -31,6 +31,7 @@ import {
31
31
  createToolRegistry,
32
32
  deleteAgent,
33
33
  deleteSkill,
34
+ devServerManager,
34
35
  emitAssistantMessageStart,
35
36
  emitContextCompacted,
36
37
  emitContextState,
@@ -95,7 +96,7 @@ import {
95
96
  setSkillEnabled,
96
97
  skillExists,
97
98
  storedEventToServerMessage
98
- } from "./chunk-IKAWIOXM.js";
99
+ } from "./chunk-FUUYYDVG.js";
99
100
  import {
100
101
  getSetting,
101
102
  setSetting
@@ -114,9 +115,12 @@ import {
114
115
  import {
115
116
  computeSessionStats
116
117
  } from "./chunk-QY7BMXWT.js";
118
+ import {
119
+ createServerMessage
120
+ } from "./chunk-ARFK3RZW.js";
117
121
  import {
118
122
  createProviderManager
119
- } from "./chunk-6MOGZMGX.js";
123
+ } from "./chunk-L4XDA3V7.js";
120
124
  import {
121
125
  SessionNotFoundError,
122
126
  createLLMClient,
@@ -125,7 +129,7 @@ import {
125
129
  getBackendDisplayName,
126
130
  getLlmStatus,
127
131
  getModelProfile
128
- } from "./chunk-FF6Z2MC3.js";
132
+ } from "./chunk-5ZUU23XF.js";
129
133
  import {
130
134
  logger,
131
135
  setLogLevel
@@ -749,7 +753,8 @@ function getPromptAwareToolResponse(prompt) {
749
753
  tools: [
750
754
  { name: "get_criteria", arguments: {} },
751
755
  { name: "write_file", arguments: { path: "src/test.ts", content: "export const created = true" } },
752
- { name: "complete_criterion", arguments: { id: "test-file", reason: "Created the requested file" } }
756
+ { name: "complete_criterion", arguments: { id: "test-file", reason: "Created the requested file" } },
757
+ { name: "step_done", arguments: {} }
753
758
  ],
754
759
  response: "Reviewed the criteria, created the file, and completed the criterion."
755
760
  };
@@ -758,7 +763,8 @@ function getPromptAwareToolResponse(prompt) {
758
763
  return {
759
764
  tools: [
760
765
  { name: "write_file", arguments: { path: "src/utils.ts", content: "export const created = true" } },
761
- { name: "complete_criterion", arguments: { id: "file-created", reason: "Created the requested file" } }
766
+ { name: "complete_criterion", arguments: { id: "file-created", reason: "Created the requested file" } },
767
+ { name: "step_done", arguments: {} }
762
768
  ],
763
769
  response: "Created the file and completed the criterion."
764
770
  };
@@ -865,12 +871,48 @@ function getConversationAwareToolResponse(request) {
865
871
  );
866
872
  completedCriteria.push("file-created");
867
873
  }
868
- if (tools.length > 0) {
869
- return {
870
- tools,
871
- response: `Completed builder work for: ${completedCriteria.join(", ")}.`
872
- };
874
+ if (tools.length === 0) {
875
+ tools.push({ name: "complete_criterion", arguments: { id: "mock-crit", reason: "Completed for testing" } });
876
+ completedCriteria.push("mock-crit");
873
877
  }
878
+ tools.push({ name: "step_done", arguments: {} });
879
+ return {
880
+ tools,
881
+ response: `Completed builder work for: ${completedCriteria.join(", ")}.`
882
+ };
883
+ }
884
+ if (/fulfil the \d+ criteria/i.test(prompt)) {
885
+ return {
886
+ tools: [
887
+ { name: "complete_criterion", arguments: { id: "mock-crit", reason: "Completed for testing" } },
888
+ { name: "step_done", arguments: {} }
889
+ ],
890
+ response: "Completed criterion and finished step."
891
+ };
892
+ }
893
+ if (/Continue working on the acceptance criteria/i.test(prompt)) {
894
+ const tools = [];
895
+ if (conversationText.includes("file-created")) {
896
+ tools.push(
897
+ { name: "write_file", arguments: { path: "src/utils.ts", content: "export const created = true" } },
898
+ { name: "complete_criterion", arguments: { id: "file-created", reason: "Created the requested file" } }
899
+ );
900
+ } else if (conversationText.includes("trivial-pass")) {
901
+ tools.push({ name: "complete_criterion", arguments: { id: "trivial-pass", reason: "Trivial criterion passes immediately" } });
902
+ } else if (conversationText.includes("verify-fail")) {
903
+ tools.push({ name: "complete_criterion", arguments: { id: "verify-fail", reason: "Prepared criterion for verification" } });
904
+ } else {
905
+ if (/1 criteria remaining/i.test(prompt)) {
906
+ tools.push({ name: "complete_criterion", arguments: { id: "trivial-pass", reason: "Trivial criterion passes immediately" } });
907
+ } else {
908
+ tools.push({ name: "complete_criterion", arguments: { id: "mock-crit", reason: "Completed for testing" } });
909
+ }
910
+ }
911
+ tools.push({ name: "step_done", arguments: {} });
912
+ return {
913
+ tools,
914
+ response: "Completed builder work."
915
+ };
874
916
  }
875
917
  if (/Verify each criterion marked \[NEEDS VERIFICATION\]\./i.test(prompt)) {
876
918
  const alreadyVerified = request.messages.some(
@@ -1199,43 +1241,60 @@ async function runBuilderTurn(options, turnMetrics) {
1199
1241
  const allAgents = await loadAllAgentsDefault();
1200
1242
  const builderDef = findAgentById("builder", allAgents);
1201
1243
  const subAgentDefs = getSubAgents(allAgents);
1202
- await runTopLevelAgentLoop({
1203
- mode: "builder",
1204
- sessionManager: sessionManager2,
1205
- sessionId,
1206
- llmClient: options.llmClient,
1207
- statsIdentity,
1208
- signal: options.signal,
1209
- onMessage: options.onMessage,
1210
- assembleRequest: (input) => assembleAgentRequest({ ...input, agentDef: builderDef, subAgentDefs }),
1211
- getToolRegistry: () => getToolRegistryForAgent(builderDef),
1212
- onToolExecuted: (toolCall, toolResult) => {
1213
- if (toolResult.success && ["write_file", "edit_file"].includes(toolCall.name)) {
1214
- const path = toolCall.arguments["path"];
1215
- sessionManager2.addModifiedFile(sessionId, path);
1216
- }
1217
- },
1218
- injectKickoff: () => {
1219
- if (options.injectBuilderKickoff !== true) return;
1220
- const session = sessionManager2.requireSession(sessionId);
1221
- const events = eventStore.getEvents(sessionId);
1222
- const hasBuilderKickoff = events.some((e) => {
1223
- if (e.type !== "message.start") return false;
1224
- const data = e.data;
1225
- return data.messageKind === "auto-prompt" && data.content?.includes("fulfil the");
1226
- });
1227
- if (!hasBuilderKickoff) {
1228
- const kickoffMsgId = crypto.randomUUID();
1229
- const kickoffContent = BUILDER_KICKOFF_PROMPT(session.criteria.length);
1230
- eventStore.append(sessionId, createMessageStartEvent(kickoffMsgId, "user", kickoffContent, {
1231
- ...getCurrentWindowMessageOptions(sessionId) ?? {},
1232
- isSystemGenerated: true,
1233
- messageKind: "auto-prompt"
1234
- }));
1235
- eventStore.append(sessionId, { type: "message.done", data: { messageId: kickoffMsgId } });
1244
+ let stepDoneCalled = false;
1245
+ return {
1246
+ ...await runTopLevelAgentLoop({
1247
+ mode: "builder",
1248
+ sessionManager: sessionManager2,
1249
+ sessionId,
1250
+ llmClient: options.llmClient,
1251
+ statsIdentity,
1252
+ signal: options.signal,
1253
+ onMessage: options.onMessage,
1254
+ assembleRequest: (input) => assembleAgentRequest({ ...input, agentDef: builderDef, subAgentDefs }),
1255
+ getToolRegistry: () => {
1256
+ const baseRegistry = getToolRegistryForAgent(builderDef);
1257
+ if (options.injectStepDone === true) {
1258
+ return baseRegistry;
1259
+ }
1260
+ return {
1261
+ tools: baseRegistry.tools.filter((t) => t.name !== "step_done"),
1262
+ definitions: baseRegistry.definitions.filter((d) => d.type === "function" && d.function.name !== "step_done"),
1263
+ execute: baseRegistry.execute
1264
+ };
1265
+ },
1266
+ onToolExecuted: (toolCall, toolResult) => {
1267
+ if (toolCall.name === "step_done" && toolResult.success) {
1268
+ stepDoneCalled = true;
1269
+ }
1270
+ if (toolResult.success && ["write_file", "edit_file"].includes(toolCall.name)) {
1271
+ const path = toolCall.arguments["path"];
1272
+ sessionManager2.addModifiedFile(sessionId, path);
1273
+ }
1274
+ },
1275
+ injectKickoff: () => {
1276
+ if (options.injectBuilderKickoff !== true) return;
1277
+ const session = sessionManager2.requireSession(sessionId);
1278
+ const events = eventStore.getEvents(sessionId);
1279
+ const hasBuilderKickoff = events.some((e) => {
1280
+ if (e.type !== "message.start") return false;
1281
+ const data = e.data;
1282
+ return data.messageKind === "auto-prompt" && data.content?.includes("fulfil the");
1283
+ });
1284
+ if (!hasBuilderKickoff) {
1285
+ const kickoffMsgId = crypto.randomUUID();
1286
+ const kickoffContent = BUILDER_KICKOFF_PROMPT(session.criteria.length);
1287
+ eventStore.append(sessionId, createMessageStartEvent(kickoffMsgId, "user", kickoffContent, {
1288
+ ...getCurrentWindowMessageOptions(sessionId) ?? {},
1289
+ isSystemGenerated: true,
1290
+ messageKind: "auto-prompt"
1291
+ }));
1292
+ eventStore.append(sessionId, { type: "message.done", data: { messageId: kickoffMsgId } });
1293
+ }
1236
1294
  }
1237
- }
1238
- }, turnMetrics);
1295
+ }, turnMetrics),
1296
+ stepDoneCalled
1297
+ };
1239
1298
  }
1240
1299
  function buildSnapshot(sessionManager2, sessionId, lastStats) {
1241
1300
  const eventStore = getEventStore();
@@ -1467,6 +1526,18 @@ function executeShellCommand(command, cwd, timeout, signal) {
1467
1526
  }
1468
1527
 
1469
1528
  // src/server/workflows/executor.ts
1529
+ var TEMPLATE_VARIABLES = [
1530
+ { name: "workdir", description: "Working directory of the session" },
1531
+ { name: "reason", description: 'Human-readable reason (e.g. "2 criteria remaining")' },
1532
+ { name: "stepOutput", description: "Structured output from the previous step (content, stdout, stderr, exitCode, etc.)" },
1533
+ { name: "verifierFindings", description: "@deprecated Use stepOutput.content instead. Output from the last sub-agent step" },
1534
+ { name: "previousStepOutput", description: "@deprecated Use stepOutput.stdout instead. Output from the last shell step" },
1535
+ { name: "criteriaCount", description: "Total number of criteria" },
1536
+ { name: "pendingCount", description: "Number of pending/failed criteria" },
1537
+ { name: "summary", description: "Session summary generated by planner" },
1538
+ { name: "criteriaList", description: "Formatted list of all criteria with status" },
1539
+ { name: "modifiedFiles", description: "List of modified files" }
1540
+ ];
1470
1541
  function formatCriteriaList(criteria) {
1471
1542
  if (criteria.length === 0) return "(none)";
1472
1543
  return criteria.map((c) => {
@@ -1479,7 +1550,16 @@ function formatModifiedFiles(session) {
1479
1550
  return files.length > 0 ? files.map((f) => `- ${f}`).join("\n") : "(none)";
1480
1551
  }
1481
1552
  function resolveTemplate(template, ctx) {
1482
- return template.replace(/\{\{workdir\}\}/g, ctx.workdir).replace(/\{\{reason\}\}/g, ctx.reason).replace(/\{\{verifierFindings\}\}/g, ctx.verifierFindings).replace(/\{\{previousStepOutput\}\}/g, ctx.previousStepOutput).replace(/\{\{criteriaCount\}\}/g, String(ctx.criteriaCount)).replace(/\{\{pendingCount\}\}/g, String(ctx.pendingCount)).replace(/\{\{summary\}\}/g, ctx.summary).replace(/\{\{criteriaList\}\}/g, ctx.criteriaList).replace(/\{\{modifiedFiles\}\}/g, ctx.modifiedFiles);
1553
+ let result = template;
1554
+ for (const { name } of TEMPLATE_VARIABLES) {
1555
+ if (name === "stepOutput" || name === "verifierFindings" || name === "previousStepOutput") continue;
1556
+ const value = String(ctx[name]);
1557
+ result = result.replace(new RegExp(`\\{\\{${name}\\}\\}`, "g"), value);
1558
+ }
1559
+ result = result.replace(/\{\{stepOutput\.(\w+)\}\}/g, (_, key) => ctx.stepOutput[key] ?? "");
1560
+ result = result.replace(/\{\{verifierFindings\}\}/g, ctx.stepOutput["content"] ?? "");
1561
+ result = result.replace(/\{\{previousStepOutput\}\}/g, ctx.stepOutput["stdout"] ?? "");
1562
+ return result;
1483
1563
  }
1484
1564
  function evaluateCondition(condition, criteria, stepOutcome) {
1485
1565
  switch (condition.type) {
@@ -1495,7 +1575,7 @@ function evaluateCondition(condition, criteria, stepOutcome) {
1495
1575
  return criteria.some((c) => c.status.type !== "passed");
1496
1576
  case "step_result":
1497
1577
  if (!stepOutcome) return false;
1498
- return condition.result === "success" ? stepOutcome.success : !stepOutcome.success;
1578
+ return stepOutcome.result === condition.result;
1499
1579
  case "always":
1500
1580
  return true;
1501
1581
  }
@@ -1522,8 +1602,7 @@ async function executeWorkflow(workflow, options) {
1522
1602
  const startTime = performance.now();
1523
1603
  let iterations = 0;
1524
1604
  let currentStepId = workflow.entryStep;
1525
- let lastSubAgentContent = "";
1526
- let lastShellOutput = "";
1605
+ let lastStepOutput = {};
1527
1606
  let isFirstBuilderEntry = true;
1528
1607
  const messagesBeforeWorkflow = sessionManager2.requireSession(sessionId).messages.length;
1529
1608
  const stepsById = /* @__PURE__ */ new Map();
@@ -1557,6 +1636,13 @@ async function executeWorkflow(workflow, options) {
1557
1636
  { ...startWindowOpts ?? {}, isSystemGenerated: true, messageKind: "workflow-started" }
1558
1637
  ));
1559
1638
  eventStore.append(sessionId, { type: "message.done", data: { messageId: startMsgId } });
1639
+ if (options.userMessage) {
1640
+ sessionManager2.addMessage(sessionId, {
1641
+ role: "user",
1642
+ content: options.userMessage.content,
1643
+ ...options.userMessage.attachments ? { attachments: options.userMessage.attachments } : {}
1644
+ });
1645
+ }
1560
1646
  while (iterations < workflow.settings.maxIterations) {
1561
1647
  if (signal?.aborted) {
1562
1648
  logger.debug("Workflow executor aborted", { sessionId, iterations });
@@ -1582,13 +1668,14 @@ async function executeWorkflow(workflow, options) {
1582
1668
  const templateCtx = {
1583
1669
  workdir: session.workdir,
1584
1670
  reason: buildReason(criteria),
1585
- verifierFindings: lastSubAgentContent,
1586
- previousStepOutput: lastShellOutput,
1671
+ verifierFindings: lastStepOutput["content"] ?? "",
1672
+ previousStepOutput: lastStepOutput["stdout"] ?? "",
1587
1673
  criteriaCount: criteria.length,
1588
1674
  pendingCount: criteria.filter((c) => c.status.type !== "passed").length,
1589
1675
  summary: session.summary ?? "No summary available",
1590
1676
  criteriaList: formatCriteriaList(criteria),
1591
- modifiedFiles: formatModifiedFiles(session)
1677
+ modifiedFiles: formatModifiedFiles(session),
1678
+ stepOutput: lastStepOutput
1592
1679
  };
1593
1680
  sessionManager2.setPhase(sessionId, step.phase);
1594
1681
  logger.debug("Workflow step executing", { sessionId, iteration: iterations, stepId: step.id, stepType: step.type });
@@ -1596,8 +1683,13 @@ async function executeWorkflow(workflow, options) {
1596
1683
  switch (step.type) {
1597
1684
  case "agent": {
1598
1685
  const agentStep = step;
1686
+ const STEP_DONE_PROMPT = "\n\nOnce you're done, call step_done()";
1687
+ 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()";
1688
+ let promptContent = null;
1689
+ let nudgeContent = null;
1599
1690
  if (isFirstBuilderEntry && agentStep.prompt) {
1600
- const promptContent = resolveTemplate(agentStep.prompt, templateCtx);
1691
+ const resolvedPrompt = resolveTemplate(agentStep.prompt, templateCtx);
1692
+ promptContent = resolvedPrompt + STEP_DONE_PROMPT;
1601
1693
  const promptMsgId = crypto.randomUUID();
1602
1694
  eventStore.append(sessionId, createMessageStartEvent(promptMsgId, "user", promptContent, {
1603
1695
  ...currentWindowMessageOptions ?? {},
@@ -1605,8 +1697,14 @@ async function executeWorkflow(workflow, options) {
1605
1697
  messageKind: "auto-prompt"
1606
1698
  }));
1607
1699
  eventStore.append(sessionId, { type: "message.done", data: { messageId: promptMsgId } });
1608
- } else if (!isFirstBuilderEntry && agentStep.nudgePrompt) {
1609
- const nudgeContent = resolveTemplate(agentStep.nudgePrompt, templateCtx);
1700
+ } else if (!isFirstBuilderEntry) {
1701
+ const parts = [];
1702
+ if (agentStep.nudgePrompt) {
1703
+ const resolvedNudge = resolveTemplate(agentStep.nudgePrompt, templateCtx);
1704
+ parts.push(resolvedNudge);
1705
+ }
1706
+ parts.push(STEP_DONE_NUDGE);
1707
+ nudgeContent = parts.join("\n\n");
1610
1708
  const nudgeMsgId = crypto.randomUUID();
1611
1709
  eventStore.append(sessionId, createMessageStartEvent(nudgeMsgId, "user", nudgeContent, {
1612
1710
  ...currentWindowMessageOptions ?? {},
@@ -1616,17 +1714,25 @@ async function executeWorkflow(workflow, options) {
1616
1714
  eventStore.append(sessionId, { type: "message.done", data: { messageId: nudgeMsgId } });
1617
1715
  }
1618
1716
  const turnMetrics = new TurnMetrics();
1619
- await runBuilderTurn({
1717
+ const agentResult = await runBuilderTurn({
1620
1718
  sessionManager: sessionManager2,
1621
1719
  sessionId,
1622
1720
  llmClient,
1721
+ injectStepDone: true,
1623
1722
  ...options.statsIdentity ? { statsIdentity: options.statsIdentity } : {},
1624
1723
  ...isFirstBuilderEntry && !agentStep.prompt && options.injectBuilderKickoff === true ? { injectBuilderKickoff: true } : {},
1625
1724
  ...signal ? { signal } : {},
1626
1725
  ...onMessage ? { onMessage } : {}
1627
1726
  }, turnMetrics);
1628
1727
  isFirstBuilderEntry = false;
1629
- stepOutcome = { success: true };
1728
+ const agentReturnValue = agentResult.returnValueResult ?? "completed";
1729
+ const stepDoneCalled = agentResult.stepDoneCalled ?? false;
1730
+ lastStepOutput = { ...agentResult.returnValueContent ? { content: agentResult.returnValueContent } : {}, ...agentResult.returnValueResult ? { result: agentResult.returnValueResult } : {}, stepDoneCalled: String(stepDoneCalled) };
1731
+ stepOutcome = { result: agentReturnValue, output: lastStepOutput };
1732
+ if (!stepDoneCalled) {
1733
+ logger.debug("step_done not called, looping agent step", { sessionId, stepId: step.id, iteration: iterations });
1734
+ continue;
1735
+ }
1630
1736
  break;
1631
1737
  }
1632
1738
  case "sub_agent": {
@@ -1638,10 +1744,15 @@ async function executeWorkflow(workflow, options) {
1638
1744
  const agentDef = findAgentById(subStep.subAgentType, allAgents);
1639
1745
  if (!agentDef) {
1640
1746
  logger.error("Sub-agent definition not found", { subAgentType: subStep.subAgentType });
1641
- stepOutcome = { success: false };
1747
+ stepOutcome = { result: "error", output: {} };
1642
1748
  break;
1643
1749
  }
1644
1750
  const toolRegistry = getToolRegistryForAgent(agentDef);
1751
+ const filteredToolRegistry = {
1752
+ tools: toolRegistry.tools.filter((t) => t.name !== "step_done"),
1753
+ definitions: toolRegistry.definitions.filter((d) => d.type === "function" && d.function.name !== "step_done"),
1754
+ execute: toolRegistry.execute
1755
+ };
1645
1756
  const nudgeConfig = subStep.subAgentType === "verifier" && !subStep.nudgePrompt ? createVerifierNudgeConfig() : void 0;
1646
1757
  const result = await executeSubAgent({
1647
1758
  subAgentType: subStep.subAgentType,
@@ -1649,15 +1760,15 @@ async function executeWorkflow(workflow, options) {
1649
1760
  sessionManager: sessionManager2,
1650
1761
  sessionId,
1651
1762
  llmClient,
1652
- toolRegistry,
1763
+ toolRegistry: filteredToolRegistry,
1653
1764
  turnMetrics,
1654
1765
  statsIdentity: options.statsIdentity ?? { providerId: "", providerName: "", backend: "unknown", model: llmClient.getModel() },
1655
1766
  ...signal ? { signal } : {},
1656
1767
  ...onMessage ? { onMessage } : {},
1657
1768
  nudgeConfig
1658
1769
  });
1659
- lastSubAgentContent = result.content ?? "";
1660
- stepOutcome = { success: result.allPassed ?? true };
1770
+ lastStepOutput = { content: result.content ?? "", ...result.result ? { result: result.result } : {} };
1771
+ stepOutcome = { result: result.result ?? "success", output: lastStepOutput };
1661
1772
  break;
1662
1773
  }
1663
1774
  case "shell": {
@@ -1673,7 +1784,7 @@ async function executeWorkflow(workflow, options) {
1673
1784
  }));
1674
1785
  const result = await executeShellCommand(command, session.workdir, timeout, signal);
1675
1786
  const output = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
1676
- lastShellOutput = output;
1787
+ lastStepOutput = { stdout: result.stdout ?? "", stderr: result.stderr ?? "", exitCode: String(result.exitCode) };
1677
1788
  const outputContent = output ? `Exit code: ${result.exitCode}
1678
1789
  \`\`\`
1679
1790
  ${output.slice(0, 1e4)}
@@ -1686,7 +1797,7 @@ ${output.slice(0, 1e4)}
1686
1797
  messageKind: "correction"
1687
1798
  }));
1688
1799
  eventStore.append(sessionId, { type: "message.done", data: { messageId: outputMsgId } });
1689
- stepOutcome = { success: successCodes.includes(result.exitCode) };
1800
+ stepOutcome = { result: successCodes.includes(result.exitCode) ? "success" : "failure", output: lastStepOutput };
1690
1801
  break;
1691
1802
  }
1692
1803
  }
@@ -1898,7 +2009,22 @@ function listSessions() {
1898
2009
  s.updated_at,
1899
2010
  s.title,
1900
2011
  s.provider_id,
1901
- s.provider_model
2012
+ s.provider_model,
2013
+ COALESCE(
2014
+ (SELECT COUNT(*) FROM events e
2015
+ WHERE e.session_id = s.id
2016
+ AND e.event_type = 'message.start'
2017
+ AND json_extract(e.payload, '$.role') IN ('user', 'assistant'))
2018
+ +
2019
+ COALESCE(
2020
+ (SELECT SUM(json_array_length(json_extract(e.payload, '$.messages'))) FROM events e
2021
+ WHERE e.session_id = s.id
2022
+ AND e.event_type = 'turn.snapshot'
2023
+ AND json_extract(e.payload, '$.messages') IS NOT NULL),
2024
+ 0
2025
+ ),
2026
+ 0
2027
+ ) as message_count
1902
2028
  FROM sessions s
1903
2029
  ORDER BY s.updated_at DESC
1904
2030
  `).all();
@@ -1916,8 +2042,9 @@ function listSessions() {
1916
2042
  updatedAt: row.updated_at,
1917
2043
  criteriaCount: 0,
1918
2044
  // Derived from events
1919
- criteriaCompleted: 0
2045
+ criteriaCompleted: 0,
1920
2046
  // Derived from events
2047
+ messageCount: row.message_count
1921
2048
  }));
1922
2049
  }
1923
2050
  function listSessionsByProject(projectId) {
@@ -1934,7 +2061,22 @@ function listSessionsByProject(projectId) {
1934
2061
  s.updated_at,
1935
2062
  s.title,
1936
2063
  s.provider_id,
1937
- s.provider_model
2064
+ s.provider_model,
2065
+ COALESCE(
2066
+ (SELECT COUNT(*) FROM events e
2067
+ WHERE e.session_id = s.id
2068
+ AND e.event_type = 'message.start'
2069
+ AND json_extract(e.payload, '$.role') IN ('user', 'assistant'))
2070
+ +
2071
+ COALESCE(
2072
+ (SELECT SUM(json_array_length(json_extract(e.payload, '$.messages'))) FROM events e
2073
+ WHERE e.session_id = s.id
2074
+ AND e.event_type = 'turn.snapshot'
2075
+ AND json_extract(e.payload, '$.messages') IS NOT NULL),
2076
+ 0
2077
+ ),
2078
+ 0
2079
+ ) as message_count
1938
2080
  FROM sessions s
1939
2081
  WHERE s.project_id = ?
1940
2082
  ORDER BY s.updated_at DESC
@@ -1953,8 +2095,9 @@ function listSessionsByProject(projectId) {
1953
2095
  updatedAt: row.updated_at,
1954
2096
  criteriaCount: 0,
1955
2097
  // Derived from events
1956
- criteriaCompleted: 0
2098
+ criteriaCompleted: 0,
1957
2099
  // Derived from events
2100
+ messageCount: row.message_count
1958
2101
  }));
1959
2102
  }
1960
2103
  function deleteSession(id) {
@@ -1973,7 +2116,7 @@ Example inputs and outputs:
1973
2116
 
1974
2117
  User message: {message}`;
1975
2118
  async function generateSessionName(options) {
1976
- const { userMessage, llmClient } = options;
2119
+ const { userMessage, llmClient, signal } = options;
1977
2120
  try {
1978
2121
  const prompt = SESSION_NAME_PROMPT.replace("{message}", userMessage);
1979
2122
  const messages = [
@@ -1982,10 +2125,12 @@ async function generateSessionName(options) {
1982
2125
  content: prompt
1983
2126
  }
1984
2127
  ];
2128
+ const timeoutSignal = AbortSignal.timeout(6e4);
2129
+ const composedSignal = signal ? AbortSignal.any([timeoutSignal, signal]) : timeoutSignal;
1985
2130
  const response = await llmClient.complete({
1986
2131
  messages,
1987
2132
  tools: [],
1988
- signal: AbortSignal.timeout(6e4),
2133
+ signal: composedSignal,
1989
2134
  disableThinking: true
1990
2135
  });
1991
2136
  let name = response.content.trim();
@@ -2187,6 +2332,28 @@ function createWebSocketServer(httpServer, config, getLLMClient, getActiveProvid
2187
2332
  }
2188
2333
  }
2189
2334
  });
2335
+ const broadcastAll = (msg) => {
2336
+ const serialized = serializeServerMessage(msg);
2337
+ for (const [clientWs] of clients) {
2338
+ if (clientWs.readyState === WebSocket.OPEN) {
2339
+ clientWs.send(serialized);
2340
+ }
2341
+ }
2342
+ };
2343
+ devServerManager.onOutput((workdir, chunk) => {
2344
+ broadcastAll(createServerMessage("devServer.output", {
2345
+ workdir,
2346
+ stream: chunk.stream,
2347
+ content: chunk.content
2348
+ }));
2349
+ });
2350
+ devServerManager.onStateChange((workdir, state, errorMessage) => {
2351
+ broadcastAll(createServerMessage("devServer.state", {
2352
+ workdir,
2353
+ state,
2354
+ errorMessage
2355
+ }));
2356
+ });
2190
2357
  wss.on("connection", (ws) => {
2191
2358
  logger.debug("WebSocket client connected");
2192
2359
  clients.set(ws, { ws, activeSessionId: null, subscribedSessions: /* @__PURE__ */ new Map(), eventStoreSubscriptions: /* @__PURE__ */ new Map() });
@@ -2247,6 +2414,7 @@ async function handleClientMessage(ws, client, message, config, getLLMClient, ge
2247
2414
  controller.abort();
2248
2415
  cancelQuestionsForSession(sessionId, reason);
2249
2416
  cancelPathConfirmationsForSession(sessionId, reason);
2417
+ sessionManager2.clearMessageQueue(sessionId);
2250
2418
  sessionManager2.setRunning(sessionId, false);
2251
2419
  sendForSession(sessionId, createSessionRunningMessage(false));
2252
2420
  const contextState = sessionManager2.getContextState(sessionId);
@@ -2461,8 +2629,7 @@ async function handleClientMessage(ws, client, message, config, getLLMClient, ge
2461
2629
  ensureEventStoreSubscription(session.id);
2462
2630
  const eventStore = getEventStore();
2463
2631
  const events = eventStore.getEvents(session.id);
2464
- let messages;
2465
- messages = buildMessagesFromStoredEvents(events);
2632
+ const messages = buildMessagesFromStoredEvents(events);
2466
2633
  logger.debug("Loaded messages from EventStore", { sessionId: session.id, eventCount: events.length, messageCount: messages.length });
2467
2634
  sendForSession(session.id, createSessionStateMessage(session, messages, message.id));
2468
2635
  const contextState = sessionManager2.getContextState(session.id);
@@ -2558,46 +2725,6 @@ async function handleClientMessage(ws, client, message, config, getLLMClient, ge
2558
2725
  const sessionId = client.activeSessionId;
2559
2726
  const eventStore = getEventStore();
2560
2727
  ensureEventStoreSubscription(sessionId);
2561
- await maybeAutoCompactContext({
2562
- sessionManager: sessionManager2,
2563
- sessionId,
2564
- llmClient: llmForSession(sessionId),
2565
- statsIdentity: statsForSession(sessionId)
2566
- });
2567
- const userMessage = sessionManager2.addMessage(sessionId, {
2568
- role: "user",
2569
- content: message.payload.content,
2570
- ...message.payload.attachments && { attachments: message.payload.attachments },
2571
- ...message.payload.messageKind && { messageKind: message.payload.messageKind },
2572
- ...message.payload.isSystemGenerated && { isSystemGenerated: message.payload.isSystemGenerated }
2573
- });
2574
- const messageCount = getSessionMessageCount(sessionId);
2575
- if (needsNameGeneration(currentSession.metadata.title, messageCount)) {
2576
- generateSessionName({
2577
- userMessage: message.payload.content,
2578
- llmClient: llmForSession(sessionId)
2579
- }).then(async (result) => {
2580
- if (result.success && result.name) {
2581
- updateSessionMetadata(sessionId, { title: result.name });
2582
- eventStore.append(sessionId, {
2583
- type: "session.name_generated",
2584
- data: { name: result.name }
2585
- });
2586
- const updatedSession = sessionManager2.getSession(sessionId);
2587
- if (updatedSession) {
2588
- const events = eventStore.getEvents(sessionId);
2589
- const messages = buildMessagesFromStoredEvents(events);
2590
- broadcastForSession(sessionId, createSessionStateMessage(updatedSession, messages));
2591
- }
2592
- logger.info("Session name generated", { sessionId, name: result.name });
2593
- }
2594
- }).catch((error) => {
2595
- logger.warn("Session name generation failed", { sessionId, error: error instanceof Error ? error.message : error });
2596
- });
2597
- }
2598
- sessionManager2.setRunning(sessionId, true);
2599
- sendForSession(sessionId, createChatMessageMessage(userMessage));
2600
- sendForSession(sessionId, createSessionRunningMessage(true));
2601
2728
  const controller = new AbortController();
2602
2729
  const existingController = activeAgents.get(sessionId);
2603
2730
  if (existingController) {
@@ -2605,8 +2732,66 @@ async function handleClientMessage(ws, client, message, config, getLLMClient, ge
2605
2732
  existingController.abort();
2606
2733
  }
2607
2734
  activeAgents.set(sessionId, controller);
2735
+ sessionManager2.setRunning(sessionId, true);
2736
+ sendForSession(sessionId, createSessionRunningMessage(true));
2608
2737
  send({ type: "ack", payload: {}, id: message.id });
2609
- startTurnWithCompletionChain(sessionId, controller);
2738
+ try {
2739
+ await maybeAutoCompactContext({
2740
+ sessionManager: sessionManager2,
2741
+ sessionId,
2742
+ llmClient: llmForSession(sessionId),
2743
+ statsIdentity: statsForSession(sessionId),
2744
+ signal: controller.signal
2745
+ });
2746
+ if (controller.signal.aborted) {
2747
+ break;
2748
+ }
2749
+ const userMessage = sessionManager2.addMessage(sessionId, {
2750
+ role: "user",
2751
+ content: message.payload.content,
2752
+ ...message.payload.attachments && { attachments: message.payload.attachments },
2753
+ ...message.payload.messageKind && { messageKind: message.payload.messageKind },
2754
+ ...message.payload.isSystemGenerated && { isSystemGenerated: message.payload.isSystemGenerated }
2755
+ });
2756
+ sendForSession(sessionId, createChatMessageMessage(userMessage));
2757
+ const messageCount = getSessionMessageCount(sessionId);
2758
+ if (needsNameGeneration(currentSession.metadata.title, messageCount)) {
2759
+ generateSessionName({
2760
+ userMessage: message.payload.content,
2761
+ llmClient: llmForSession(sessionId),
2762
+ signal: controller.signal
2763
+ }).then(async (result) => {
2764
+ if (result.success && result.name) {
2765
+ updateSessionMetadata(sessionId, { title: result.name });
2766
+ eventStore.append(sessionId, {
2767
+ type: "session.name_generated",
2768
+ data: { name: result.name }
2769
+ });
2770
+ const updatedSession = sessionManager2.getSession(sessionId);
2771
+ if (updatedSession) {
2772
+ const events = eventStore.getEvents(sessionId);
2773
+ const messages = buildMessagesFromStoredEvents(events);
2774
+ broadcastForSession(sessionId, createSessionStateMessage(updatedSession, messages));
2775
+ }
2776
+ logger.info("Session name generated", { sessionId, name: result.name });
2777
+ }
2778
+ }).catch((error) => {
2779
+ logger.warn("Session name generation failed", { sessionId, error: error instanceof Error ? error.message : error });
2780
+ });
2781
+ }
2782
+ startTurnWithCompletionChain(sessionId, controller);
2783
+ } catch (error) {
2784
+ if (activeAgents.get(sessionId) === controller) {
2785
+ activeAgents.delete(sessionId);
2786
+ }
2787
+ sessionManager2.setRunning(sessionId, false);
2788
+ sendForSession(sessionId, createSessionRunningMessage(false));
2789
+ const contextState = sessionManager2.getContextState(sessionId);
2790
+ sendForSession(sessionId, createContextStateMessage(contextState));
2791
+ if (!(error instanceof Error && error.message === "Aborted")) {
2792
+ logger.error("Chat send pre-turn error", { sessionId, error });
2793
+ }
2794
+ }
2610
2795
  break;
2611
2796
  }
2612
2797
  case "chat.stop": {
@@ -2750,7 +2935,8 @@ async function handleClientMessage(ws, client, message, config, getLLMClient, ge
2750
2935
  send(createErrorMessage("ALREADY_RUNNING", "Session is already running", message.id));
2751
2936
  return;
2752
2937
  }
2753
- if (session.criteria.length === 0) {
2938
+ const acceptPayloadEarly = message.payload;
2939
+ if (!acceptPayloadEarly?.workflowId && session.criteria.length === 0) {
2754
2940
  send(createErrorMessage("NO_CRITERIA", "Cannot accept: no criteria defined", message.id));
2755
2941
  return;
2756
2942
  }
@@ -2815,13 +3001,18 @@ async function handleClientMessage(ws, client, message, config, getLLMClient, ge
2815
3001
  }
2816
3002
  activeAgents.set(sessionId, controller);
2817
3003
  const acceptPayload = message.payload;
3004
+ const acceptAttachments = acceptPayload?.attachments;
3005
+ const hasAcceptContent = acceptPayload?.content && typeof acceptPayload.content === "string" && acceptPayload.content.trim();
3006
+ const hasAcceptAttachments = acceptAttachments && acceptAttachments.length > 0;
3007
+ const hasAcceptMessage = hasAcceptContent || hasAcceptAttachments;
2818
3008
  await runOrchestrator({
2819
3009
  sessionManager: sessionManager2,
2820
3010
  sessionId,
2821
3011
  llmClient: llmForSession(sessionId),
2822
3012
  statsIdentity: statsForSession(sessionId),
2823
- injectBuilderKickoff: true,
3013
+ injectBuilderKickoff: !hasAcceptMessage,
2824
3014
  ...acceptPayload?.workflowId ? { workflowId: acceptPayload.workflowId } : {},
3015
+ ...hasAcceptMessage ? { userMessage: { content: hasAcceptContent ? acceptPayload.content : "", ...hasAcceptAttachments ? { attachments: acceptAttachments } : {} } } : {},
2825
3016
  signal: controller.signal,
2826
3017
  onMessage: (msg) => sendForSession(sessionId, msg)
2827
3018
  // For path confirmation dialogs
@@ -2911,7 +3102,6 @@ async function handleClientMessage(ws, client, message, config, getLLMClient, ge
2911
3102
  }
2912
3103
  const contextState = sessionManager2.getContextState(sessionId);
2913
3104
  const tokensBefore = contextState.currentTokens;
2914
- const eventStore = getEventStore();
2915
3105
  send({ type: "ack", payload: {}, id: message.id });
2916
3106
  (async () => {
2917
3107
  try {
@@ -2956,28 +3146,23 @@ async function handleClientMessage(ws, client, message, config, getLLMClient, ge
2956
3146
  send(createErrorMessage("INVALID_MODE", "Runner can only be launched in builder mode", message.id));
2957
3147
  return;
2958
3148
  }
3149
+ const launchPayloadEarly = message.payload;
2959
3150
  const pendingCriteria = session.criteria.filter((c) => c.status.type !== "passed");
2960
- if (pendingCriteria.length === 0) {
3151
+ if (!launchPayloadEarly?.workflowId && pendingCriteria.length === 0) {
2961
3152
  send(createErrorMessage("NO_WORK", "No pending criteria to work on", message.id));
2962
3153
  return;
2963
3154
  }
2964
3155
  const sessionId = client.activeSessionId;
2965
- const eventStore = getEventStore();
2966
3156
  if (session.phase === "blocked") {
2967
3157
  logger.info("User launched runner - resetting blocked state", { sessionId });
2968
3158
  sessionManager2.setPhase(sessionId, "build");
2969
3159
  sessionManager2.resetAllCriteriaAttempts(sessionId);
2970
3160
  }
2971
3161
  const launchPayload = message.payload;
2972
- const hasUserMessage = launchPayload?.content && typeof launchPayload.content === "string" && launchPayload.content.trim();
2973
- if (hasUserMessage) {
2974
- const attachments = launchPayload.attachments;
2975
- sessionManager2.addMessage(sessionId, {
2976
- role: "user",
2977
- content: launchPayload.content,
2978
- ...attachments ? { attachments } : {}
2979
- });
2980
- }
3162
+ const launchAttachments = launchPayload?.attachments;
3163
+ const hasUserContent = launchPayload?.content && typeof launchPayload.content === "string" && launchPayload.content.trim();
3164
+ const hasUserAttachments = launchAttachments && launchAttachments.length > 0;
3165
+ const hasUserMessage = hasUserContent || hasUserAttachments;
2981
3166
  sessionManager2.setRunning(sessionId, true);
2982
3167
  sendForSession(sessionId, createSessionRunningMessage(true));
2983
3168
  const controller = new AbortController();
@@ -2997,6 +3182,7 @@ async function handleClientMessage(ws, client, message, config, getLLMClient, ge
2997
3182
  statsIdentity: statsForSession(sessionId),
2998
3183
  injectBuilderKickoff: !hasUserMessage,
2999
3184
  ...launchPayload?.workflowId ? { workflowId: launchPayload.workflowId } : {},
3185
+ ...hasUserMessage ? { userMessage: { content: hasUserContent ? launchPayload.content : "", ...hasUserAttachments ? { attachments: launchAttachments } : {} } } : {},
3000
3186
  signal: controller.signal,
3001
3187
  onMessage: (msg) => sendForSession(sessionId, msg)
3002
3188
  // For path confirmation dialogs
@@ -4931,153 +5117,11 @@ async function deleteCommand(configDir, commandId) {
4931
5117
  }
4932
5118
  }
4933
5119
 
4934
- // src/server/index.ts
4935
- var __dirname2 = dirname4(fileURLToPath4(import.meta.url));
4936
- async function createServerHandle(config) {
4937
- setRuntimeConfig(config);
4938
- setLogLevel(config.logging?.level ?? void 0, config.mode);
4939
- const db = initDatabase(config);
4940
- initEventStore(db);
4941
- const configDir = getGlobalConfigDir(config.mode ?? "production");
4942
- await ensureDefaultSkills(configDir);
4943
- await ensureDefaultCommands(configDir);
4944
- await ensureDefaultAgents(configDir);
4945
- await ensureDefaultWorkflows(configDir);
4946
- const sessionManager2 = new SessionManager();
4947
- const providerManager = createProviderManager(config);
4948
- const useMock = process.env["OPENFOX_MOCK_LLM"] === "true";
4949
- const getMockClient = useMock ? createMockLLMClient : null;
4950
- const getLLMClient = () => getMockClient ? getMockClient() : providerManager.getLLMClient();
4951
- if (useMock) {
4952
- logger.info("Using MOCK LLM client - deterministic responses for testing");
4953
- }
4954
- async function initLLM() {
4955
- const llmClient = getLLMClient();
4956
- let backend = "unknown";
4957
- const useMock2 = process.env["OPENFOX_MOCK_LLM"] === "true";
4958
- if (config.llm.backend === "auto") {
4959
- backend = await detectBackend(config.llm.baseUrl, void 0, useMock2);
4960
- llmClient.setBackend(backend);
4961
- if (!useMock2) {
4962
- logger.info("Auto-detected LLM backend", { backend: getBackendDisplayName(backend) });
4963
- }
4964
- } else {
4965
- backend = config.llm.backend;
4966
- llmClient.setBackend(backend);
4967
- if (!useMock2) {
4968
- logger.info("Using configured LLM backend", { backend: getBackendDisplayName(backend) });
4969
- }
4970
- }
4971
- const detected = await detectModel(config.llm.baseUrl);
4972
- if (detected) {
4973
- llmClient.setModel(detected);
4974
- if (!useMock2) {
4975
- logger.info("Auto-detected LLM model", { model: detected, backend: getBackendDisplayName(backend) });
4976
- }
4977
- } else {
4978
- if (!useMock2) {
4979
- logger.warn("Could not auto-detect model, using config", { model: config.llm.model });
4980
- }
4981
- }
4982
- }
4983
- initLLM().catch((err) => logger.error("LLM initialization failed", { error: err instanceof Error ? err.message : String(err) }));
4984
- const toolRegistry = createToolRegistry();
4985
- const app = express();
4986
- app.use(cors());
4987
- app.use(express.json());
4988
- app.get("/api/health", (_req, res) => {
4989
- res.json({ status: "ok", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
4990
- });
4991
- app.post("/api/sessions", async (req, res) => {
4992
- const { workdir, title } = req.body;
4993
- const session = sessionManager2.createSession(workdir, title);
4994
- res.status(201).json({ session });
4995
- });
4996
- app.get("/api/sessions/:id", (req, res) => {
4997
- const session = sessionManager2.getSession(req.params.id);
4998
- if (!session) {
4999
- return res.status(404).json({ error: "Session not found" });
5000
- }
5001
- res.json({ session });
5002
- });
5003
- app.delete("/api/sessions/:id", (req, res) => {
5004
- sessionManager2.deleteSession(req.params["id"]);
5005
- res.json({ success: true });
5006
- });
5007
- app.delete("/api/projects/:projectId/sessions", (req, res) => {
5008
- const projectId = req.params["projectId"];
5009
- const project = sessionManager2.getProject(projectId);
5010
- if (!project) {
5011
- return res.status(404).json({ error: "Project not found" });
5012
- }
5013
- sessionManager2.deleteAllSessions(projectId, project.workdir);
5014
- res.json({ success: true });
5015
- });
5016
- app.get("/api/config", (_req, res) => {
5017
- const llmClient = getLLMClient();
5018
- const activeProvider = providerManager.getActiveProvider();
5019
- res.json({
5020
- model: llmClient.getModel(),
5021
- maxContext: config.context.maxTokens,
5022
- llmUrl: activeProvider?.url ?? config.llm.baseUrl,
5023
- llmStatus: getLlmStatus(),
5024
- backend: llmClient.getBackend(),
5025
- workdir: config.workdir,
5026
- // Include provider info
5027
- providers: providerManager.getProviders(),
5028
- activeProviderId: providerManager.getActiveProviderId(),
5029
- defaultModelSelection: config.defaultModelSelection
5030
- });
5031
- });
5032
- app.post("/api/model/refresh", async (_req, res) => {
5033
- const llmClient = getLLMClient();
5034
- const currentModel = providerManager.getCurrentModel();
5035
- if (currentModel === "auto") {
5036
- const activeProvider = providerManager.getActiveProvider();
5037
- const baseUrl = activeProvider?.url ?? config.llm.baseUrl;
5038
- const detected = await detectModel(baseUrl);
5039
- if (detected) {
5040
- llmClient.setModel(detected);
5041
- return res.json({ model: detected, source: "detected", llmStatus: getLlmStatus(), backend: llmClient.getBackend() });
5042
- }
5043
- }
5044
- res.json({ model: llmClient.getModel(), source: "cached", llmStatus: getLlmStatus(), backend: llmClient.getBackend() });
5045
- });
5046
- app.get("/api/providers", (_req, res) => {
5047
- const providers = providerManager.getProviders().map((p) => ({
5048
- ...p,
5049
- status: providerManager.getProviderStatus(p.id)
5050
- }));
5051
- res.json({
5052
- providers,
5053
- activeProviderId: providerManager.getActiveProviderId()
5054
- });
5055
- });
5056
- app.get("/api/providers/:id/models", async (req, res) => {
5057
- const { id } = req.params;
5058
- const models = await providerManager.getProviderModels(id);
5059
- res.json({ models });
5060
- });
5061
- app.post("/api/providers/:id/activate", async (req, res) => {
5062
- const { id } = req.params;
5063
- const body = req.body;
5064
- const result = await providerManager.activateProvider(id, body.model ? { model: body.model } : void 0);
5065
- if (!result.success) {
5066
- return res.status(400).json({ error: result.error });
5067
- }
5068
- const llmClient = getLLMClient();
5069
- const { loadGlobalConfig, saveGlobalConfig, setDefaultModelSelection } = await import("./config-6GJ4SVFH.js");
5070
- const globalConfig = await loadGlobalConfig(config.mode ?? "production");
5071
- const updatedConfig = setDefaultModelSelection(globalConfig, id, llmClient.getModel());
5072
- await saveGlobalConfig(config.mode ?? "production", updatedConfig);
5073
- res.json({
5074
- success: true,
5075
- activeProviderId: id,
5076
- model: llmClient.getModel(),
5077
- backend: llmClient.getBackend()
5078
- });
5079
- });
5080
- app.get("/api/skills", async (_req, res) => {
5120
+ // src/server/routes/skills.ts
5121
+ import { Router } from "express";
5122
+ function createSkillRoutes(configDir) {
5123
+ const router = Router();
5124
+ router.get("/", async (_req, res) => {
5081
5125
  const [skills, defaultIds, modifiedIds] = await Promise.all([
5082
5126
  loadAllSkills(configDir),
5083
5127
  getDefaultSkillIds(),
@@ -5092,15 +5136,15 @@ async function createServerHandle(config) {
5092
5136
  modifiedIds
5093
5137
  });
5094
5138
  });
5095
- app.get("/api/skills/default-ids", async (_req, res) => {
5139
+ router.get("/default-ids", async (_req, res) => {
5096
5140
  const ids = await getDefaultSkillIds();
5097
5141
  res.json({ ids });
5098
5142
  });
5099
- app.post("/api/skills/restore-all-defaults", async (_req, res) => {
5143
+ router.post("/restore-all-defaults", async (_req, res) => {
5100
5144
  const count = await restoreAllDefaultSkills(configDir);
5101
5145
  res.json({ success: true, count });
5102
5146
  });
5103
- app.post("/api/skills/:id/restore-default", async (req, res) => {
5147
+ router.post("/:id/restore-default", async (req, res) => {
5104
5148
  const { id } = req.params;
5105
5149
  const restored = await restoreDefaultSkill(configDir, id);
5106
5150
  if (!restored) {
@@ -5108,13 +5152,13 @@ async function createServerHandle(config) {
5108
5152
  }
5109
5153
  res.json({ success: true });
5110
5154
  });
5111
- app.post("/api/skills/:id/toggle", (req, res) => {
5155
+ router.post("/:id/toggle", (req, res) => {
5112
5156
  const { id } = req.params;
5113
5157
  const currentlyEnabled = isSkillEnabled(id);
5114
5158
  setSkillEnabled(id, !currentlyEnabled);
5115
5159
  res.json({ id, enabled: !currentlyEnabled });
5116
5160
  });
5117
- app.get("/api/skills/:id", async (req, res) => {
5161
+ router.get("/:id", async (req, res) => {
5118
5162
  const { id } = req.params;
5119
5163
  const skills = await loadAllSkills(configDir);
5120
5164
  const skill = findSkillById(id, skills);
@@ -5123,7 +5167,7 @@ async function createServerHandle(config) {
5123
5167
  }
5124
5168
  res.json(skill);
5125
5169
  });
5126
- app.post("/api/skills", async (req, res) => {
5170
+ router.post("/", async (req, res) => {
5127
5171
  const body = req.body;
5128
5172
  if (!body.metadata?.id || !body.metadata?.name || !body.prompt) {
5129
5173
  return res.status(400).json({ error: "Missing required fields: metadata.id, metadata.name, prompt" });
@@ -5137,7 +5181,7 @@ async function createServerHandle(config) {
5137
5181
  await saveSkill(configDir, body);
5138
5182
  res.status(201).json(body);
5139
5183
  });
5140
- app.put("/api/skills/:id", async (req, res) => {
5184
+ router.put("/:id", async (req, res) => {
5141
5185
  const { id } = req.params;
5142
5186
  if (!await skillExists(configDir, id)) {
5143
5187
  return res.status(404).json({ error: "Skill not found" });
@@ -5155,7 +5199,7 @@ async function createServerHandle(config) {
5155
5199
  await saveSkill(configDir, updated);
5156
5200
  res.json(updated);
5157
5201
  });
5158
- app.delete("/api/skills/:id", async (req, res) => {
5202
+ router.delete("/:id", async (req, res) => {
5159
5203
  const { id } = req.params;
5160
5204
  const deleted = await deleteSkill(configDir, id);
5161
5205
  if (!deleted) {
@@ -5163,7 +5207,14 @@ async function createServerHandle(config) {
5163
5207
  }
5164
5208
  res.json({ success: true });
5165
5209
  });
5166
- app.get("/api/commands", async (_req, res) => {
5210
+ return router;
5211
+ }
5212
+
5213
+ // src/server/routes/commands.ts
5214
+ import { Router as Router2 } from "express";
5215
+ function createCommandRoutes(configDir) {
5216
+ const router = Router2();
5217
+ router.get("/", async (_req, res) => {
5167
5218
  const [commands, defaultIds, modifiedIds] = await Promise.all([
5168
5219
  loadAllCommands(configDir),
5169
5220
  getDefaultCommandIds(),
@@ -5175,15 +5226,15 @@ async function createServerHandle(config) {
5175
5226
  modifiedIds
5176
5227
  });
5177
5228
  });
5178
- app.get("/api/commands/default-ids", async (_req, res) => {
5229
+ router.get("/default-ids", async (_req, res) => {
5179
5230
  const ids = await getDefaultCommandIds();
5180
5231
  res.json({ ids });
5181
5232
  });
5182
- app.post("/api/commands/restore-all-defaults", async (_req, res) => {
5233
+ router.post("/restore-all-defaults", async (_req, res) => {
5183
5234
  const count = await restoreAllDefaultCommands(configDir);
5184
5235
  res.json({ success: true, count });
5185
5236
  });
5186
- app.post("/api/commands/:id/restore-default", async (req, res) => {
5237
+ router.post("/:id/restore-default", async (req, res) => {
5187
5238
  const { id } = req.params;
5188
5239
  const restored = await restoreDefaultCommand(configDir, id);
5189
5240
  if (!restored) {
@@ -5191,7 +5242,7 @@ async function createServerHandle(config) {
5191
5242
  }
5192
5243
  res.json({ success: true });
5193
5244
  });
5194
- app.get("/api/commands/:id", async (req, res) => {
5245
+ router.get("/:id", async (req, res) => {
5195
5246
  const { id } = req.params;
5196
5247
  const commands = await loadAllCommands(configDir);
5197
5248
  const command = findCommandById(id, commands);
@@ -5200,7 +5251,7 @@ async function createServerHandle(config) {
5200
5251
  }
5201
5252
  res.json(command);
5202
5253
  });
5203
- app.post("/api/commands", async (req, res) => {
5254
+ router.post("/", async (req, res) => {
5204
5255
  const body = req.body;
5205
5256
  if (!body.metadata?.id || !body.metadata?.name || !body.prompt) {
5206
5257
  return res.status(400).json({ error: "Missing required fields: metadata.id, metadata.name, prompt" });
@@ -5214,7 +5265,7 @@ async function createServerHandle(config) {
5214
5265
  await saveCommand(configDir, body);
5215
5266
  res.status(201).json(body);
5216
5267
  });
5217
- app.put("/api/commands/:id", async (req, res) => {
5268
+ router.put("/:id", async (req, res) => {
5218
5269
  const { id } = req.params;
5219
5270
  if (!await commandExists(configDir, id)) {
5220
5271
  return res.status(404).json({ error: "Command not found" });
@@ -5232,7 +5283,7 @@ async function createServerHandle(config) {
5232
5283
  await saveCommand(configDir, updated);
5233
5284
  res.json(updated);
5234
5285
  });
5235
- app.delete("/api/commands/:id", async (req, res) => {
5286
+ router.delete("/:id", async (req, res) => {
5236
5287
  const { id } = req.params;
5237
5288
  const deleted = await deleteCommand(configDir, id);
5238
5289
  if (!deleted) {
@@ -5240,7 +5291,14 @@ async function createServerHandle(config) {
5240
5291
  }
5241
5292
  res.json({ success: true });
5242
5293
  });
5243
- app.get("/api/agents", async (_req, res) => {
5294
+ return router;
5295
+ }
5296
+
5297
+ // src/server/routes/agents.ts
5298
+ import { Router as Router3 } from "express";
5299
+ function createAgentRoutes(configDir) {
5300
+ const router = Router3();
5301
+ router.get("/", async (_req, res) => {
5244
5302
  const [agents, defaultIds, modifiedIds] = await Promise.all([
5245
5303
  loadAllAgents(configDir),
5246
5304
  getDefaultAgentIds(),
@@ -5252,15 +5310,15 @@ async function createServerHandle(config) {
5252
5310
  modifiedIds
5253
5311
  });
5254
5312
  });
5255
- app.get("/api/agents/default-ids", async (_req, res) => {
5313
+ router.get("/default-ids", async (_req, res) => {
5256
5314
  const ids = await getDefaultAgentIds();
5257
5315
  res.json({ ids });
5258
5316
  });
5259
- app.post("/api/agents/restore-all-defaults", async (_req, res) => {
5317
+ router.post("/restore-all-defaults", async (_req, res) => {
5260
5318
  const count = await restoreAllDefaultAgents(configDir);
5261
5319
  res.json({ success: true, count });
5262
5320
  });
5263
- app.post("/api/agents/:id/restore-default", async (req, res) => {
5321
+ router.post("/:id/restore-default", async (req, res) => {
5264
5322
  const { id } = req.params;
5265
5323
  const restored = await restoreDefaultAgent(configDir, id);
5266
5324
  if (!restored) {
@@ -5268,7 +5326,7 @@ async function createServerHandle(config) {
5268
5326
  }
5269
5327
  res.json({ success: true });
5270
5328
  });
5271
- app.get("/api/agents/:id", async (req, res) => {
5329
+ router.get("/:id", async (req, res) => {
5272
5330
  const { id } = req.params;
5273
5331
  const agents = await loadAllAgents(configDir);
5274
5332
  const agent = findAgentById(id, agents);
@@ -5277,7 +5335,7 @@ async function createServerHandle(config) {
5277
5335
  }
5278
5336
  res.json(agent);
5279
5337
  });
5280
- app.post("/api/agents", async (req, res) => {
5338
+ router.post("/", async (req, res) => {
5281
5339
  const body = req.body;
5282
5340
  if (!body?.metadata?.id || !body?.prompt) {
5283
5341
  return res.status(400).json({ error: "Missing required fields: metadata.id, prompt" });
@@ -5288,7 +5346,7 @@ async function createServerHandle(config) {
5288
5346
  await saveAgent(configDir, body);
5289
5347
  res.status(201).json(body);
5290
5348
  });
5291
- app.put("/api/agents/:id", async (req, res) => {
5349
+ router.put("/:id", async (req, res) => {
5292
5350
  const { id } = req.params;
5293
5351
  if (!id) {
5294
5352
  return res.status(400).json({ error: "Missing agent ID" });
@@ -5306,7 +5364,7 @@ async function createServerHandle(config) {
5306
5364
  await saveAgent(configDir, updated);
5307
5365
  res.json(updated);
5308
5366
  });
5309
- app.delete("/api/agents/:id", async (req, res) => {
5367
+ router.delete("/:id", async (req, res) => {
5310
5368
  const { id } = req.params;
5311
5369
  const deleted = await deleteAgent(configDir, id);
5312
5370
  if (!deleted) {
@@ -5314,28 +5372,38 @@ async function createServerHandle(config) {
5314
5372
  }
5315
5373
  res.json({ success: true });
5316
5374
  });
5317
- app.get("/api/workflows", async (_req, res) => {
5375
+ return router;
5376
+ }
5377
+
5378
+ // src/server/routes/workflows.ts
5379
+ import { Router as Router4 } from "express";
5380
+ function createWorkflowRoutes(configDir, config) {
5381
+ const router = Router4();
5382
+ router.get("/", async (_req, res) => {
5318
5383
  const [workflows, defaultIds, modifiedIds] = await Promise.all([
5319
5384
  loadAllWorkflows(configDir),
5320
5385
  getDefaultWorkflowIds(),
5321
5386
  getModifiedDefaultWorkflowIds(configDir)
5322
5387
  ]);
5323
5388
  res.json({
5324
- workflows: workflows.map((p) => p.metadata),
5389
+ workflows: workflows.map((p) => ({ ...p.metadata, startCondition: p.startCondition })),
5325
5390
  activeWorkflowId: config.activeWorkflowId ?? "default",
5326
5391
  defaultIds,
5327
5392
  modifiedIds
5328
5393
  });
5329
5394
  });
5330
- app.get("/api/workflows/default-ids", async (_req, res) => {
5395
+ router.get("/template-variables", (_req, res) => {
5396
+ res.json({ variables: TEMPLATE_VARIABLES });
5397
+ });
5398
+ router.get("/default-ids", async (_req, res) => {
5331
5399
  const ids = await getDefaultWorkflowIds();
5332
5400
  res.json({ ids });
5333
5401
  });
5334
- app.post("/api/workflows/restore-all-defaults", async (_req, res) => {
5402
+ router.post("/restore-all-defaults", async (_req, res) => {
5335
5403
  const count = await restoreAllDefaultWorkflows(configDir);
5336
5404
  res.json({ success: true, count });
5337
5405
  });
5338
- app.post("/api/workflows/:id/restore-default", async (req, res) => {
5406
+ router.post("/:id/restore-default", async (req, res) => {
5339
5407
  const { id } = req.params;
5340
5408
  const restored = await restoreDefaultWorkflow(configDir, id);
5341
5409
  if (!restored) {
@@ -5343,7 +5411,7 @@ async function createServerHandle(config) {
5343
5411
  }
5344
5412
  res.json({ success: true });
5345
5413
  });
5346
- app.get("/api/workflows/:id", async (req, res) => {
5414
+ router.get("/:id", async (req, res) => {
5347
5415
  const { id } = req.params;
5348
5416
  const workflows = await loadAllWorkflows(configDir);
5349
5417
  const workflow = findWorkflowById(id, workflows);
@@ -5352,7 +5420,7 @@ async function createServerHandle(config) {
5352
5420
  }
5353
5421
  res.json(workflow);
5354
5422
  });
5355
- app.post("/api/workflows", async (req, res) => {
5423
+ router.post("/", async (req, res) => {
5356
5424
  const body = req.body;
5357
5425
  if (!body?.metadata?.id || !body?.steps?.length) {
5358
5426
  return res.status(400).json({ error: "Missing required fields: metadata.id, steps" });
@@ -5363,7 +5431,7 @@ async function createServerHandle(config) {
5363
5431
  await saveWorkflow(configDir, body);
5364
5432
  res.status(201).json(body);
5365
5433
  });
5366
- app.put("/api/workflows/:id", async (req, res) => {
5434
+ router.put("/:id", async (req, res) => {
5367
5435
  const { id } = req.params;
5368
5436
  if (!await workflowExists(configDir, id)) {
5369
5437
  return res.status(404).json({ error: "Workflow not found" });
@@ -5376,7 +5444,7 @@ async function createServerHandle(config) {
5376
5444
  await saveWorkflow(configDir, updated);
5377
5445
  res.json(updated);
5378
5446
  });
5379
- app.delete("/api/workflows/:id", async (req, res) => {
5447
+ router.delete("/:id", async (req, res) => {
5380
5448
  const { id } = req.params;
5381
5449
  if (id === "default") {
5382
5450
  return res.status(400).json({ error: "Cannot delete the default workflow" });
@@ -5387,6 +5455,241 @@ async function createServerHandle(config) {
5387
5455
  }
5388
5456
  res.json({ success: true });
5389
5457
  });
5458
+ return router;
5459
+ }
5460
+
5461
+ // src/server/routes/dev-server.ts
5462
+ import { Router as Router5 } from "express";
5463
+ function createDevServerRoutes() {
5464
+ const router = Router5();
5465
+ router.get("/", async (req, res) => {
5466
+ const workdir = req.query["workdir"];
5467
+ if (!workdir) return res.status(400).json({ error: "workdir required" });
5468
+ const status = devServerManager.getStatus(workdir);
5469
+ if (!status.config) {
5470
+ const config = await devServerManager.loadConfig(workdir);
5471
+ if (config) {
5472
+ status.config = config;
5473
+ status.url = config.url;
5474
+ status.hotReload = config.hotReload;
5475
+ }
5476
+ }
5477
+ res.json(status);
5478
+ });
5479
+ router.post("/start", async (req, res) => {
5480
+ const workdir = req.query["workdir"];
5481
+ if (!workdir) return res.status(400).json({ error: "workdir required" });
5482
+ try {
5483
+ const status = await devServerManager.start(workdir);
5484
+ res.json(status);
5485
+ } catch (err) {
5486
+ res.status(500).json({ error: err instanceof Error ? err.message : "Failed to start" });
5487
+ }
5488
+ });
5489
+ router.post("/stop", async (req, res) => {
5490
+ const workdir = req.query["workdir"];
5491
+ if (!workdir) return res.status(400).json({ error: "workdir required" });
5492
+ try {
5493
+ const status = await devServerManager.stop(workdir);
5494
+ res.json(status);
5495
+ } catch (err) {
5496
+ res.status(500).json({ error: err instanceof Error ? err.message : "Failed to stop" });
5497
+ }
5498
+ });
5499
+ router.post("/restart", async (req, res) => {
5500
+ const workdir = req.query["workdir"];
5501
+ if (!workdir) return res.status(400).json({ error: "workdir required" });
5502
+ try {
5503
+ const status = await devServerManager.restart(workdir);
5504
+ res.json(status);
5505
+ } catch (err) {
5506
+ res.status(500).json({ error: err instanceof Error ? err.message : "Failed to restart" });
5507
+ }
5508
+ });
5509
+ router.get("/logs", (req, res) => {
5510
+ const workdir = req.query["workdir"];
5511
+ if (!workdir) return res.status(400).json({ error: "workdir required" });
5512
+ const logs = devServerManager.getLogs(workdir);
5513
+ res.json({ logs });
5514
+ });
5515
+ router.get("/config", async (req, res) => {
5516
+ const workdir = req.query["workdir"];
5517
+ if (!workdir) return res.status(400).json({ error: "workdir required" });
5518
+ const config = await devServerManager.loadConfig(workdir);
5519
+ res.json({ config });
5520
+ });
5521
+ router.post("/config", async (req, res) => {
5522
+ const workdir = req.query["workdir"];
5523
+ if (!workdir) return res.status(400).json({ error: "workdir required" });
5524
+ const { command, url, hotReload } = req.body;
5525
+ if (!command || !url) {
5526
+ return res.status(400).json({ error: "command and url are required" });
5527
+ }
5528
+ try {
5529
+ const config = { command, url, hotReload: hotReload ?? false };
5530
+ await devServerManager.saveConfig(workdir, config);
5531
+ res.json({ config });
5532
+ } catch (err) {
5533
+ res.status(500).json({ error: err instanceof Error ? err.message : "Failed to save config" });
5534
+ }
5535
+ });
5536
+ return router;
5537
+ }
5538
+
5539
+ // src/server/index.ts
5540
+ var __dirname2 = dirname4(fileURLToPath4(import.meta.url));
5541
+ async function createServerHandle(config) {
5542
+ setRuntimeConfig(config);
5543
+ setLogLevel(config.logging?.level ?? void 0, config.mode);
5544
+ const db = initDatabase(config);
5545
+ initEventStore(db);
5546
+ const configDir = getGlobalConfigDir(config.mode ?? "production");
5547
+ await ensureDefaultSkills(configDir);
5548
+ await ensureDefaultCommands(configDir);
5549
+ await ensureDefaultAgents(configDir);
5550
+ await ensureDefaultWorkflows(configDir);
5551
+ const sessionManager2 = new SessionManager();
5552
+ const providerManager = createProviderManager(config);
5553
+ const useMock = process.env["OPENFOX_MOCK_LLM"] === "true";
5554
+ const getMockClient = useMock ? createMockLLMClient : null;
5555
+ const getLLMClient = () => getMockClient ? getMockClient() : providerManager.getLLMClient();
5556
+ if (useMock) {
5557
+ logger.info("Using MOCK LLM client - deterministic responses for testing");
5558
+ }
5559
+ async function initLLM() {
5560
+ const llmClient = getLLMClient();
5561
+ let backend;
5562
+ const useMock2 = process.env["OPENFOX_MOCK_LLM"] === "true";
5563
+ if (config.llm.backend === "auto") {
5564
+ backend = await detectBackend(config.llm.baseUrl, void 0, useMock2);
5565
+ llmClient.setBackend(backend);
5566
+ if (!useMock2) {
5567
+ logger.info("Auto-detected LLM backend", { backend: getBackendDisplayName(backend) });
5568
+ }
5569
+ } else {
5570
+ backend = config.llm.backend;
5571
+ llmClient.setBackend(backend);
5572
+ if (!useMock2) {
5573
+ logger.info("Using configured LLM backend", { backend: getBackendDisplayName(backend) });
5574
+ }
5575
+ }
5576
+ const detected = await detectModel(config.llm.baseUrl);
5577
+ if (detected) {
5578
+ llmClient.setModel(detected);
5579
+ if (!useMock2) {
5580
+ logger.info("Auto-detected LLM model", { model: detected, backend: getBackendDisplayName(backend) });
5581
+ }
5582
+ } else {
5583
+ if (!useMock2) {
5584
+ logger.warn("Could not auto-detect model, using config", { model: config.llm.model });
5585
+ }
5586
+ }
5587
+ }
5588
+ initLLM().catch((err) => logger.error("LLM initialization failed", { error: err instanceof Error ? err.message : String(err) }));
5589
+ const toolRegistry = createToolRegistry();
5590
+ const app = express();
5591
+ app.use(cors());
5592
+ app.use(express.json());
5593
+ app.get("/api/health", (_req, res) => {
5594
+ res.json({ status: "ok", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
5595
+ });
5596
+ app.get("/api/tools", (_req, res) => {
5597
+ res.json({ tools: toolRegistry.tools.map((t) => t.name) });
5598
+ });
5599
+ app.post("/api/sessions", async (req, res) => {
5600
+ const { workdir, title } = req.body;
5601
+ const session = sessionManager2.createSession(workdir, title);
5602
+ res.status(201).json({ session });
5603
+ });
5604
+ app.get("/api/sessions/:id", (req, res) => {
5605
+ const session = sessionManager2.getSession(req.params.id);
5606
+ if (!session) {
5607
+ return res.status(404).json({ error: "Session not found" });
5608
+ }
5609
+ res.json({ session });
5610
+ });
5611
+ app.delete("/api/sessions/:id", (req, res) => {
5612
+ sessionManager2.deleteSession(req.params["id"]);
5613
+ res.json({ success: true });
5614
+ });
5615
+ app.delete("/api/projects/:projectId/sessions", (req, res) => {
5616
+ const projectId = req.params["projectId"];
5617
+ const project = sessionManager2.getProject(projectId);
5618
+ if (!project) {
5619
+ return res.status(404).json({ error: "Project not found" });
5620
+ }
5621
+ sessionManager2.deleteAllSessions(projectId, project.workdir);
5622
+ res.json({ success: true });
5623
+ });
5624
+ app.get("/api/config", (_req, res) => {
5625
+ const llmClient = getLLMClient();
5626
+ const activeProvider = providerManager.getActiveProvider();
5627
+ res.json({
5628
+ model: llmClient.getModel(),
5629
+ maxContext: config.context.maxTokens,
5630
+ llmUrl: activeProvider?.url ?? config.llm.baseUrl,
5631
+ llmStatus: getLlmStatus(),
5632
+ backend: llmClient.getBackend(),
5633
+ workdir: config.workdir,
5634
+ // Include provider info
5635
+ providers: providerManager.getProviders(),
5636
+ activeProviderId: providerManager.getActiveProviderId(),
5637
+ defaultModelSelection: config.defaultModelSelection
5638
+ });
5639
+ });
5640
+ app.post("/api/model/refresh", async (_req, res) => {
5641
+ const llmClient = getLLMClient();
5642
+ const currentModel = providerManager.getCurrentModel();
5643
+ if (currentModel === "auto") {
5644
+ const activeProvider = providerManager.getActiveProvider();
5645
+ const baseUrl = activeProvider?.url ?? config.llm.baseUrl;
5646
+ const detected = await detectModel(baseUrl);
5647
+ if (detected) {
5648
+ llmClient.setModel(detected);
5649
+ return res.json({ model: detected, source: "detected", llmStatus: getLlmStatus(), backend: llmClient.getBackend() });
5650
+ }
5651
+ }
5652
+ res.json({ model: llmClient.getModel(), source: "cached", llmStatus: getLlmStatus(), backend: llmClient.getBackend() });
5653
+ });
5654
+ app.get("/api/providers", (_req, res) => {
5655
+ const providers = providerManager.getProviders().map((p) => ({
5656
+ ...p,
5657
+ status: providerManager.getProviderStatus(p.id)
5658
+ }));
5659
+ res.json({
5660
+ providers,
5661
+ activeProviderId: providerManager.getActiveProviderId()
5662
+ });
5663
+ });
5664
+ app.get("/api/providers/:id/models", async (req, res) => {
5665
+ const { id } = req.params;
5666
+ const models = await providerManager.getProviderModels(id);
5667
+ res.json({ models });
5668
+ });
5669
+ app.post("/api/providers/:id/activate", async (req, res) => {
5670
+ const { id } = req.params;
5671
+ const body = req.body;
5672
+ const result = await providerManager.activateProvider(id, body.model ? { model: body.model } : void 0);
5673
+ if (!result.success) {
5674
+ return res.status(400).json({ error: result.error });
5675
+ }
5676
+ const llmClient = getLLMClient();
5677
+ const { loadGlobalConfig, saveGlobalConfig, setDefaultModelSelection } = await import("./config-BMXSDOZB.js");
5678
+ const globalConfig = await loadGlobalConfig(config.mode ?? "production");
5679
+ const updatedConfig = setDefaultModelSelection(globalConfig, id, llmClient.getModel());
5680
+ await saveGlobalConfig(config.mode ?? "production", updatedConfig);
5681
+ res.json({
5682
+ success: true,
5683
+ activeProviderId: id,
5684
+ model: llmClient.getModel(),
5685
+ backend: llmClient.getBackend()
5686
+ });
5687
+ });
5688
+ app.use("/api/skills", createSkillRoutes(configDir));
5689
+ app.use("/api/commands", createCommandRoutes(configDir));
5690
+ app.use("/api/agents", createAgentRoutes(configDir));
5691
+ app.use("/api/workflows", createWorkflowRoutes(configDir, config));
5692
+ app.use("/api/dev-server", createDevServerRoutes());
5390
5693
  const { getCurrentBranch } = await import("./branch.api-WDHKZJS6.js");
5391
5694
  app.get("/api/branch", async (req, res) => {
5392
5695
  await getCurrentBranch(req, res);
@@ -5432,7 +5735,7 @@ async function createServerHandle(config) {
5432
5735
  logLevel: "warn"
5433
5736
  });
5434
5737
  app.use(viteServer.middlewares);
5435
- app.get("/src/styles/*.css", async (req, res) => {
5738
+ app.get("/src/styles/{*path}", async (req, res) => {
5436
5739
  try {
5437
5740
  const result = await viteServer.transformRequest(req.path.substring(1));
5438
5741
  if (!result) {
@@ -5453,16 +5756,12 @@ async function createServerHandle(config) {
5453
5756
  res.status(404).send("Not found");
5454
5757
  });
5455
5758
  });
5456
- app.get("/sounds/*", (req, res) => {
5457
- const fullPath = join4(webDir, req.path.substring(1));
5458
- readFile3(fullPath).then((content) => {
5759
+ app.use("/sounds", express.static(join4(webDir, "public", "sounds"), {
5760
+ setHeaders: (res) => {
5459
5761
  res.set("Content-Type", "audio/mpeg");
5460
- res.send(content);
5461
- }).catch(() => {
5462
- res.status(404).send("Not found");
5463
- });
5464
- });
5465
- app.get("*", (req, res) => {
5762
+ }
5763
+ }));
5764
+ app.get("/{*path}", (req, res) => {
5466
5765
  if (req.path.startsWith("/api/")) {
5467
5766
  return;
5468
5767
  }
@@ -5490,21 +5789,17 @@ async function createServerHandle(config) {
5490
5789
  res.status(404).send("Not found");
5491
5790
  });
5492
5791
  });
5493
- app.get("/sounds/*", (req, res) => {
5494
- const fullPath = join4(distWebDir, req.path.substring(1));
5495
- readFile3(fullPath).then((content) => {
5792
+ app.use("/sounds", express.static(join4(distWebDir, "sounds"), {
5793
+ setHeaders: (res) => {
5496
5794
  res.set("Content-Type", "audio/mpeg");
5497
- res.send(content);
5498
- }).catch(() => {
5499
- res.status(404).send("Not found");
5500
- });
5501
- });
5795
+ }
5796
+ }));
5502
5797
  app.get("/", (_req, res) => {
5503
5798
  readFile3(join4(distWebDir, "index.html"), "utf-8").then((content) => res.send(content)).catch(() => {
5504
5799
  res.status(404).send("Web UI not built. Run `npm run build:web`");
5505
5800
  });
5506
5801
  });
5507
- app.get("*", (req, res) => {
5802
+ app.get("(*)", (req, res) => {
5508
5803
  if (req.path.startsWith("/api/") || req.path.startsWith("/assets/") || req.path.startsWith("/sounds/") || req.path === "/fox.svg") {
5509
5804
  return;
5510
5805
  }
@@ -5535,6 +5830,7 @@ async function createServerHandle(config) {
5535
5830
  close: () => new Promise((resolve2) => {
5536
5831
  logger.info("Shutting down...");
5537
5832
  void (async () => {
5833
+ await devServerManager.stopAll();
5538
5834
  viteServer?.close();
5539
5835
  for (const client of wss.clients) {
5540
5836
  client.terminate();
@@ -5567,4 +5863,4 @@ export {
5567
5863
  createServerHandle,
5568
5864
  createServer
5569
5865
  };
5570
- //# sourceMappingURL=chunk-YQ36AK5A.js.map
5866
+ //# sourceMappingURL=chunk-JFQEATRC.js.map