turbollm 1.9.0 → 1.9.2

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 (120) hide show
  1. package/README.md +78 -0
  2. package/bin/turbollm.mjs +1 -1
  3. package/dist/{chunk-U4OPDHSX.js → chunk-5MCP2ZAJ.js} +2 -0
  4. package/dist/cli.js +1778 -525
  5. package/dist/{distiller-MRW6VEYO.js → distiller-6E6IOZ5G.js} +1 -1
  6. package/dist/webdist/assets/{AgentEditPage-CmdThoLF.js → AgentEditPage-CyzZ8vDr.js} +1 -1
  7. package/dist/webdist/assets/{ChatScreen-ChVYoy7Z.js → ChatScreen-DWVblPWw.js} +3 -3
  8. package/dist/webdist/assets/CodeComposer-BvNMY6PI.js +1 -0
  9. package/dist/webdist/assets/{CodeHomeScreen-D1VHqWAF.js → CodeHomeScreen-CmzaqOmH.js} +1 -1
  10. package/dist/webdist/assets/CodeSessionScreen-BtA5u3li.js +20 -0
  11. package/dist/webdist/assets/CodeSessionScreen-DDGTF8rc.css +1 -0
  12. package/dist/webdist/assets/{CustomizeScreen-C053iQLd.js → CustomizeScreen-ACC_4SMn.js} +3 -3
  13. package/dist/webdist/assets/{DeveloperScreen-BYk0rndu.js → DeveloperScreen-CSJZh91v.js} +1 -1
  14. package/dist/webdist/assets/{EnginesScreen-DkLzY4sp.js → EnginesScreen-ChGdOOW9.js} +4 -4
  15. package/dist/webdist/assets/{FsBrowser-CNit4h3B.js → FsBrowser-D9RjrjvO.js} +1 -1
  16. package/dist/webdist/assets/ModelLoadMenu-DzaxKmIV.js +2 -0
  17. package/dist/webdist/assets/{ModelsScreen-5TvL8kCj.js → ModelsScreen-wuI7h511.js} +2 -2
  18. package/dist/webdist/assets/SettingsScreen-DsKi9vxM.js +1 -0
  19. package/dist/webdist/assets/{SkillEditPage-DmJWJjhQ.js → SkillEditPage-XGTaeQ8o.js} +1 -1
  20. package/dist/webdist/assets/{TokensScreen-Dyd5sSGT.js → TokensScreen-B02y3G-H.js} +1 -1
  21. package/dist/webdist/assets/{ToolApprovalBar-0Rv_eC33.js → ToolApprovalBar-D4YRUMat.js} +9 -9
  22. package/dist/webdist/assets/WorkspaceScreen-DRoUxluw.js +1 -0
  23. package/dist/webdist/assets/{agent-api-C6XrgjWw.js → agent-api-D86F96i6.js} +1 -1
  24. package/dist/webdist/assets/{arc-V_ckaw1n.js → arc-BScxM0vj.js} +1 -1
  25. package/dist/webdist/assets/{architectureDiagram-3BPJPVTR-BaKHxCMv.js → architectureDiagram-3BPJPVTR-DbIjWGrc.js} +1 -1
  26. package/dist/webdist/assets/{arrow-left-BRhcceVg.js → arrow-left-Cz7XgLgo.js} +1 -1
  27. package/dist/webdist/assets/{badge-Df-MaEdJ.js → badge-CRVn2top.js} +1 -1
  28. package/dist/webdist/assets/{blockDiagram-GPEHLZMM-W3tqVN_9.js → blockDiagram-GPEHLZMM-COy0nYNX.js} +1 -1
  29. package/dist/webdist/assets/{bot-D2sMRmrf.js → bot-C-sGABri.js} +1 -1
  30. package/dist/webdist/assets/{c4Diagram-AAUBKEIU-Ct_fQADG.js → c4Diagram-AAUBKEIU-CoiZua-v.js} +1 -1
  31. package/dist/webdist/assets/channel-8eXCTkKl.js +1 -0
  32. package/dist/webdist/assets/{chat-api-DS1CWqFc.js → chat-api-CdVOTSst.js} +1 -1
  33. package/dist/webdist/assets/{chat-queries-BNnEQoUl.js → chat-queries-DINfS8uv.js} +1 -1
  34. package/dist/webdist/assets/check-Do9IgpFj.js +1 -0
  35. package/dist/webdist/assets/{chevron-left-Cg7-M3fC.js → chevron-left-CbPqt11D.js} +1 -1
  36. package/dist/webdist/assets/{chevron-right-B44Qnalv.js → chevron-right-C8EPnhrg.js} +1 -1
  37. package/dist/webdist/assets/{chunk-2J33WTMH-BhQKciZ6.js → chunk-2J33WTMH-B-ggs358.js} +1 -1
  38. package/dist/webdist/assets/{chunk-4BX2VUAB-CPVQRcKK.js → chunk-4BX2VUAB-CBSHARpe.js} +1 -1
  39. package/dist/webdist/assets/{chunk-55IACEB6-BeD2m5Zd.js → chunk-55IACEB6-BikCaTgS.js} +1 -1
  40. package/dist/webdist/assets/{chunk-727SXJPM-1eXg0vd4.js → chunk-727SXJPM-B8Uooh_X.js} +1 -1
  41. package/dist/webdist/assets/{chunk-AQP2D5EJ-BI_WWvfb.js → chunk-AQP2D5EJ-C-wZiAmx.js} +1 -1
  42. package/dist/webdist/assets/{chunk-FMBD7UC4-DmkZ5E8i.js → chunk-FMBD7UC4-lkRMU7V5.js} +1 -1
  43. package/dist/webdist/assets/{chunk-ND2GUHAM-DgKanzz6.js → chunk-ND2GUHAM-CTBUDTXK.js} +1 -1
  44. package/dist/webdist/assets/{chunk-QZHKN3VN-C0-EOMS6.js → chunk-QZHKN3VN-CKOXFQ-c.js} +1 -1
  45. package/dist/webdist/assets/{circle-check-CXWe3y-e.js → circle-check-dbo4u23c.js} +1 -1
  46. package/dist/webdist/assets/{circle-x-BYUS86Mo.js → circle-x-CuPWHB4Y.js} +1 -1
  47. package/dist/webdist/assets/classDiagram-4FO5ZUOK-D5bPDMhb.js +1 -0
  48. package/dist/webdist/assets/classDiagram-v2-Q7XG4LA2-D5bPDMhb.js +1 -0
  49. package/dist/webdist/assets/collapsible-tffcj0HQ.js +1 -0
  50. package/dist/webdist/assets/{common-ClITqWVP.js → common-RZkQjs09.js} +1 -1
  51. package/dist/webdist/assets/{copy-button-DOnlD7sA.js → copy-button-DKUAZ6Ig.js} +1 -1
  52. package/dist/webdist/assets/{cose-bilkent-S5V4N54A-CJ-OWUMU.js → cose-bilkent-S5V4N54A-L3BPlYev.js} +1 -1
  53. package/dist/webdist/assets/{dagre-BM42HDAG-CP41T21v.js → dagre-BM42HDAG-DdFZFhxI.js} +1 -1
  54. package/dist/webdist/assets/{diagram-2AECGRRQ-CniIf6PF.js → diagram-2AECGRRQ-Bx4JyVtd.js} +1 -1
  55. package/dist/webdist/assets/{diagram-5GNKFQAL-C81Uep65.js → diagram-5GNKFQAL-DYxtCUYO.js} +1 -1
  56. package/dist/webdist/assets/{diagram-KO2AKTUF-BcWsZVCH.js → diagram-KO2AKTUF-BSV0zTVR.js} +1 -1
  57. package/dist/webdist/assets/{diagram-LMA3HP47-D4gsQ5wi.js → diagram-LMA3HP47-CS-L_g7_.js} +1 -1
  58. package/dist/webdist/assets/{diagram-OG6HWLK6-CKw2XvNh.js → diagram-OG6HWLK6-D8uK9mOu.js} +1 -1
  59. package/dist/webdist/assets/dialog-fsfhY_Ib.js +1 -0
  60. package/dist/webdist/assets/dropdown-menu-B6pYEdad.js +1 -0
  61. package/dist/webdist/assets/{erDiagram-TEJ5UH35-DiMRASCw.js → erDiagram-TEJ5UH35-DX-3blsY.js} +1 -1
  62. package/dist/webdist/assets/{flowDiagram-I6XJVG4X-OYvJz7NK.js → flowDiagram-I6XJVG4X-DUTsLqgt.js} +1 -1
  63. package/dist/webdist/assets/{ganttDiagram-6RSMTGT7-TNBmycEa.js → ganttDiagram-6RSMTGT7-CwH0oq0S.js} +1 -1
  64. package/dist/webdist/assets/{git-branch-CpH7bdLO.js → git-branch-BiMfcJ1j.js} +1 -1
  65. package/dist/webdist/assets/{gitGraphDiagram-PVQCEYII-BhEsrTFm.js → gitGraphDiagram-PVQCEYII-CmLeqvWh.js} +1 -1
  66. package/dist/webdist/assets/{index-Cl9fRADn.js → index-BP-7GzAI.js} +1 -1
  67. package/dist/webdist/assets/index-CXwfn7-U.css +1 -0
  68. package/dist/webdist/assets/{index-BGFp5nvZ.js → index-DaNuMYPC.js} +4 -4
  69. package/dist/webdist/assets/{infoDiagram-5YYISTIA-BDEgDbkv.js → infoDiagram-5YYISTIA-BFTUzkbq.js} +1 -1
  70. package/dist/webdist/assets/{ishikawaDiagram-YF4QCWOH-JTmVtiNo.js → ishikawaDiagram-YF4QCWOH-BdQIrqQU.js} +1 -1
  71. package/dist/webdist/assets/{journeyDiagram-JHISSGLW-CSlH-DCp.js → journeyDiagram-JHISSGLW-D1HmHaNc.js} +1 -1
  72. package/dist/webdist/assets/{kanban-definition-UN3LZRKU-BIfzX5mh.js → kanban-definition-UN3LZRKU-C5USgc8m.js} +1 -1
  73. package/dist/webdist/assets/{linear-Csu7LYv_.js → linear-BDGxyxLo.js} +1 -1
  74. package/dist/webdist/assets/{lock-Dx47ssSz.js → lock-DCb387O_.js} +1 -1
  75. package/dist/webdist/assets/{mermaid.core-BF2_JyC4.js → mermaid.core-DWCJxsCl.js} +4 -4
  76. package/dist/webdist/assets/{mindmap-definition-RKZ34NQL-C4HOUf0T.js → mindmap-definition-RKZ34NQL-BV3H5rWG.js} +1 -1
  77. package/dist/webdist/assets/{pencil-DiDWTtDe.js → pencil-BegDY0Zk.js} +1 -1
  78. package/dist/webdist/assets/{personas-K7vGADki.js → personas-DNCENHUd.js} +6 -0
  79. package/dist/webdist/assets/{pieDiagram-4H26LBE5-Civ4ky7C.js → pieDiagram-4H26LBE5-DDN9VAOL.js} +1 -1
  80. package/dist/webdist/assets/{plus-rMCCWOjM.js → plus-DrZmKBTt.js} +1 -1
  81. package/dist/webdist/assets/{quadrantDiagram-W4KKPZXB-Cu5aaIeO.js → quadrantDiagram-W4KKPZXB-Bx16Ru8w.js} +1 -1
  82. package/dist/webdist/assets/{radix-C-sz7VcM.js → radix-j_81dozL.js} +1 -1
  83. package/dist/webdist/assets/{refresh-cw-zhCl4OJc.js → refresh-cw-B7Qjdj47.js} +1 -1
  84. package/dist/webdist/assets/{requirementDiagram-4Y6WPE33-owXfcooA.js → requirementDiagram-4Y6WPE33-k0upGaul.js} +1 -1
  85. package/dist/webdist/assets/{rotate-ccw-DSFeEfs1.js → rotate-ccw-B7o8hLk6.js} +1 -1
  86. package/dist/webdist/assets/{sankeyDiagram-5OEKKPKP-B7o6U2uP.js → sankeyDiagram-5OEKKPKP-xyOWBpSr.js} +1 -1
  87. package/dist/webdist/assets/{sequenceDiagram-3UESZ5HK-D0g36FR_.js → sequenceDiagram-3UESZ5HK-BhBclCRW.js} +1 -1
  88. package/dist/webdist/assets/skeleton-D15eL_7e.js +1 -0
  89. package/dist/webdist/assets/{sliders-horizontal-YIE_L0IY.js → sliders-horizontal-C4_t3PES.js} +1 -1
  90. package/dist/webdist/assets/{sparkles-FpLiZDCb.js → sparkles-BT45JGMR.js} +1 -1
  91. package/dist/webdist/assets/{star-D0lj4klT.js → star-SskkfhIb.js} +1 -1
  92. package/dist/webdist/assets/{stateDiagram-AJRCARHV-BGbyDa8P.js → stateDiagram-AJRCARHV-902Ow1i4.js} +1 -1
  93. package/dist/webdist/assets/stateDiagram-v2-BHNVJYJU-B5LKF3CJ.js +1 -0
  94. package/dist/webdist/assets/{switch-D7jMdkCs.js → switch-C-l-ej5M.js} +1 -1
  95. package/dist/webdist/assets/{terminal-Dfyn1hK2.js → terminal-55Og1Fyx.js} +1 -1
  96. package/dist/webdist/assets/{timeline-definition-PNZ67QCA-DlGxVI3e.js → timeline-definition-PNZ67QCA-BEyTzrTZ.js} +1 -1
  97. package/dist/webdist/assets/{trash-2-DpcKQJdU.js → trash-2-B9nMCHMA.js} +1 -1
  98. package/dist/webdist/assets/{useIsDesktop-BuJChlQF.js → useIsDesktop-D2KX8NOA.js} +1 -1
  99. package/dist/webdist/assets/{vennDiagram-CIIHVFJN-D8cIiNWM.js → vennDiagram-CIIHVFJN-C77U6Nzk.js} +1 -1
  100. package/dist/webdist/assets/{wardley-L42UT6IY-wEcDRtc5.js → wardley-L42UT6IY-BZ10FiAs.js} +1 -1
  101. package/dist/webdist/assets/{wardleyDiagram-YWT4CUSO-E26Pag8Q.js → wardleyDiagram-YWT4CUSO-DgrAaqyf.js} +1 -1
  102. package/dist/webdist/assets/{wrench-CNEP_hb3.js → wrench-DMY_WSQ9.js} +1 -1
  103. package/dist/webdist/assets/{xychartDiagram-2RQKCTM6-lyn53sKq.js → xychartDiagram-2RQKCTM6-BlzVJUn3.js} +1 -1
  104. package/dist/webdist/index.html +3 -3
  105. package/package.json +8 -2
  106. package/dist/webdist/assets/CodeComposer-Bj-OBoKJ.js +0 -1
  107. package/dist/webdist/assets/CodeSessionScreen-BqvzH7ux.js +0 -5
  108. package/dist/webdist/assets/ModelLoadMenu-CDHS7gAw.js +0 -2
  109. package/dist/webdist/assets/SettingsScreen-BhqvsFo_.js +0 -1
  110. package/dist/webdist/assets/WorkspaceScreen-BlWA58me.js +0 -1
  111. package/dist/webdist/assets/channel-Cg9TpvpM.js +0 -1
  112. package/dist/webdist/assets/check-d_QTpKyE.js +0 -1
  113. package/dist/webdist/assets/classDiagram-4FO5ZUOK-CUIGvGnh.js +0 -1
  114. package/dist/webdist/assets/classDiagram-v2-Q7XG4LA2-CUIGvGnh.js +0 -1
  115. package/dist/webdist/assets/collapsible-CSr2FFbS.js +0 -1
  116. package/dist/webdist/assets/dialog-CwglpOgK.js +0 -1
  117. package/dist/webdist/assets/external-link-vubZvvSL.js +0 -1
  118. package/dist/webdist/assets/index-mhXL7TR_.css +0 -1
  119. package/dist/webdist/assets/skeleton-B6viaKty.js +0 -1
  120. package/dist/webdist/assets/stateDiagram-v2-BHNVJYJU-CFpzxmQe.js +0 -1
package/dist/cli.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  engineModelAlias,
6
6
  engineRejectsAudioModel,
7
7
  evaluateVariant
8
- } from "./chunk-U4OPDHSX.js";
8
+ } from "./chunk-5MCP2ZAJ.js";
9
9
 
10
10
  // src/util/hide-console-windows.ts
11
11
  import { ChildProcess } from "child_process";
@@ -23,9 +23,9 @@ function hideChildConsoleWindows() {
23
23
 
24
24
  // src/cli.ts
25
25
  import { spawn as spawn8 } from "child_process";
26
- import { openSync as openSync3, readFileSync as readFileSync15 } from "fs";
26
+ import { openSync as openSync3, readFileSync as readFileSync16 } from "fs";
27
27
  import { serve } from "@hono/node-server";
28
- import { dirname as dirname12, join as join32 } from "path";
28
+ import { dirname as dirname12, join as join33 } from "path";
29
29
  import { fileURLToPath as fileURLToPath2 } from "url";
30
30
 
31
31
  // src/config/config.ts
@@ -124,7 +124,7 @@ function defaultConfig() {
124
124
  customAgents: [],
125
125
  builtinAgentOverrides: {},
126
126
  build: { toolchainDirs: [] },
127
- code: { agentsMdProjectCandidates: ["AGENTS.md", "agents.md", "CLAUDE.md"], agentsMdGlobalCandidates: ["agents.md", "AGENTS.md", "CLAUDE.md"] },
127
+ code: { agentsMdProjectCandidates: ["AGENTS.md", "agents.md", "CLAUDE.md"], agentsMdGlobalCandidates: ["agents.md", "AGENTS.md", "CLAUDE.md"], defaultAgent: "turbollm" },
128
128
  cloudDeploy: { runpodTemplateId: "" }
129
129
  };
130
130
  }
@@ -368,9 +368,12 @@ function normalize(c) {
368
368
  const filtered = Array.isArray(v) ? v.filter((p) => typeof p === "string" && p.trim() !== "").map((p) => p.trim()) : [];
369
369
  return filtered.length > 0 ? filtered : fallback;
370
370
  };
371
+ const ALLOWED_AGENTS = /* @__PURE__ */ new Set(["turbollm", "pi", "claude", "opencode"]);
371
372
  c.code = {
372
373
  agentsMdProjectCandidates: cleanCandidates(cc.agentsMdProjectCandidates, ["AGENTS.md", "agents.md", "CLAUDE.md"]),
373
- agentsMdGlobalCandidates: cleanCandidates(cc.agentsMdGlobalCandidates, ["agents.md", "AGENTS.md", "CLAUDE.md"])
374
+ agentsMdGlobalCandidates: cleanCandidates(cc.agentsMdGlobalCandidates, ["agents.md", "AGENTS.md", "CLAUDE.md"]),
375
+ // Unrecognized/absent → 'turbollm' (the built-in chat UI), never a silently-broken terminal launch.
376
+ defaultAgent: ALLOWED_AGENTS.has(cc.defaultAgent) ? cc.defaultAgent : "turbollm"
374
377
  };
375
378
  const cd = c.cloudDeploy ?? {};
376
379
  c.cloudDeploy = { runpodTemplateId: typeof cd.runpodTemplateId === "string" ? cd.runpodTemplateId.trim() : "" };
@@ -577,6 +580,9 @@ function availableBackends(tag = LLAMA_BUILD) {
577
580
  ];
578
581
  return a === "arm64" ? list.filter((b) => b.id === "vulkan" || b.id === "cpu") : list;
579
582
  }
583
+ if (plat() === "android") {
584
+ return [];
585
+ }
580
586
  throw new Error(`unsupported platform: ${plat()}/${process.arch}`);
581
587
  }
582
588
  function recommendBackendId(vendor, hasGpu, tag = LLAMA_BUILD) {
@@ -674,8 +680,8 @@ async function downloadFile(url, dest, onProgress, signal) {
674
680
  const total = Number(res.headers.get("content-length") ?? 0);
675
681
  let got = 0;
676
682
  let lastPct = -1;
677
- const body6 = Readable.fromWeb(res.body);
678
- body6.on("data", (chunk) => {
683
+ const body7 = Readable.fromWeb(res.body);
684
+ body7.on("data", (chunk) => {
679
685
  got += chunk.length;
680
686
  if (total && onProgress) {
681
687
  const pct = got / total;
@@ -685,7 +691,7 @@ async function downloadFile(url, dest, onProgress, signal) {
685
691
  }
686
692
  }
687
693
  });
688
- await pipeline(body6, createWriteStream(dest), { signal });
694
+ await pipeline(body7, createWriteStream(dest), { signal });
689
695
  }
690
696
  async function extractArchive(archive, destDir) {
691
697
  if (process.platform === "win32") {
@@ -2213,9 +2219,9 @@ var ProbeError = class extends Error {
2213
2219
  var RE_VERSION = /^\s*version:\s*(.+?)\s*$/im;
2214
2220
  var RE_FLAG = /--[a-z0-9][a-z0-9-]+/g;
2215
2221
  var KNOWN_KV = ["f16", "q8_0", "q4_0", "q4_1", "q5_0", "q5_1", "q8_1"];
2216
- function isTimeout(err6) {
2217
- if (!err6) return false;
2218
- const e = err6;
2222
+ function isTimeout(err7) {
2223
+ if (!err7) return false;
2224
+ const e = err7;
2219
2225
  return e.killed === true && (e.signal === "SIGTERM" || e.signal != null);
2220
2226
  }
2221
2227
  function runCaptured(bin, arg) {
@@ -3679,15 +3685,15 @@ function getSysInfo() {
3679
3685
  if (cached) return cached;
3680
3686
  cached = {
3681
3687
  os: `${process.platform}/${process.arch}`,
3682
- cpu: os.cpus()[0]?.model?.trim() ?? "",
3683
- cores: os.cpus().length || 1,
3688
+ cpu: getCpuModel(),
3689
+ cores: getCpuCoreCount(),
3684
3690
  ramMB: Math.round(os.totalmem() / 1e6),
3685
3691
  gpus: detectGpus()
3686
3692
  };
3687
3693
  return cached;
3688
3694
  }
3689
3695
  function primaryVendor(info = getSysInfo()) {
3690
- const rank = { nvidia: 4, amd: 3, apple: 3, intel: 2, unknown: 1 };
3696
+ const rank = { nvidia: 4, amd: 3, apple: 3, intel: 2, arm: 2, qualcomm: 2, unknown: 1 };
3691
3697
  let best = "unknown";
3692
3698
  for (const g of info.gpus) if (rank[g.vendor] > rank[best]) best = g.vendor;
3693
3699
  return best;
@@ -3698,6 +3704,8 @@ function classifyVendor(name) {
3698
3704
  if (/amd|radeon|\brx\b|instinct|vega|firepro/.test(n)) return "amd";
3699
3705
  if (/intel|arc|iris|\buhd\b|\bhd graphics\b/.test(n)) return "intel";
3700
3706
  if (/apple/.test(n)) return "apple";
3707
+ if (/mali|immortalis/.test(n)) return "arm";
3708
+ if (/adreno/.test(n)) return "qualcomm";
3701
3709
  return "unknown";
3702
3710
  }
3703
3711
  function isIntegratedGpuName(name) {
@@ -3705,7 +3713,10 @@ function isIntegratedGpuName(name) {
3705
3713
  if (/iris|uhd graphics|hd graphics/.test(n)) return true;
3706
3714
  if (/\barc\b/.test(n) && !/\b(?:pro\s+)?[ab]\d{2,4}\b/.test(n)) return true;
3707
3715
  if (/radeon.*graphics/.test(n) && !/\b(rx|pro|instinct|vega\s*\d|firepro)\b/.test(n)) return true;
3716
+ if (/radeon\s*(?:[6-9]\d{2}|\d{4})\s*[ms]\b/.test(n) && !/\b(rx|pro|instinct|vega\s*\d|firepro)\b/.test(n)) return true;
3708
3717
  if (/intel/.test(n) && /graphics/.test(n) && !/\barc\b/.test(n)) return true;
3718
+ if (/mali|immortalis/.test(n)) return true;
3719
+ if (/adreno/.test(n)) return true;
3709
3720
  return false;
3710
3721
  }
3711
3722
  function detectGpus() {
@@ -3729,7 +3740,7 @@ function detectGpus() {
3729
3740
  const out = execFileSync2("system_profiler", ["SPDisplaysDataType"], { timeout: 8e3 }).toString();
3730
3741
  const m = out.match(/Chipset Model:\s*(.+)/);
3731
3742
  if (m && /Apple/.test(out)) {
3732
- return [{ name: m[1].trim(), vramMb: Math.round(os.totalmem() / 1e6 * 0.65), vendor: "apple" }];
3743
+ return [{ name: m[1].trim(), vramMb: Math.round(os.totalmem() / 1e6 * 0.65), vendor: "apple", unified: true }];
3733
3744
  }
3734
3745
  } catch {
3735
3746
  }
@@ -3756,8 +3767,9 @@ function enumWindowsGpus() {
3756
3767
  const [name, ram] = line.split("|");
3757
3768
  const nm = (name ?? "").trim();
3758
3769
  const bytes = parseInt((ram ?? "").trim(), 10) || 0;
3759
- const vramMb = isIntegratedGpuName(nm) ? Math.round(os.totalmem() / 1e6 * 0.5) : bytes > 0 ? Math.round(bytes / 1e6) : 0;
3760
- return { name: nm, vramMb, vendor: classifyVendor(nm) };
3770
+ const integrated = isIntegratedGpuName(nm);
3771
+ const vramMb = integrated ? Math.round(os.totalmem() / 1e6 * 0.5) : bytes > 0 ? Math.round(bytes / 1e6) : 0;
3772
+ return { name: nm, vramMb, vendor: classifyVendor(nm), ...integrated ? { unified: true } : {} };
3761
3773
  }).filter((g) => g.name && g.vendor !== "unknown");
3762
3774
  const registryVram = readWindowsVramRegistry();
3763
3775
  return wmiGpus.map((g) => {
@@ -3826,28 +3838,181 @@ function parseRocmSmi(memJson, nameJson = "") {
3826
3838
  return gpus;
3827
3839
  }
3828
3840
  function enumLinuxGpus() {
3829
- const out = execFileSync2("sh", ["-c", "lspci -mm 2>/dev/null | grep -iE 'VGA|3D|Display'"], {
3830
- timeout: 8e3
3831
- }).toString();
3832
- return out.trim().split("\n").filter(Boolean).map((line) => {
3833
- const vendor = classifyVendor(line);
3834
- const slot = line.trim().split(/\s+/)[0] ?? "";
3835
- const name = line.replace(/"/g, "").trim().slice(0, 80);
3836
- const sysfsVramMb = linuxVramMb(slot);
3837
- const vramMb = sysfsVramMb > 0 ? sysfsVramMb : isIntegratedGpuName(name) ? Math.round(os.totalmem() / 1e6 * 0.5) : 0;
3838
- return { name, vramMb, vendor };
3839
- }).filter((g) => g.vendor !== "unknown");
3840
- }
3841
- function linuxVramMb(pciSlot) {
3842
- if (!pciSlot) return 0;
3841
+ try {
3842
+ const out = execFileSync2("sh", ["-c", "lspci -mm 2>/dev/null | grep -iE 'VGA|3D|Display'"], {
3843
+ timeout: 8e3
3844
+ }).toString();
3845
+ if (out.trim()) {
3846
+ const gpus = out.trim().split("\n").filter(Boolean).map((line) => linuxGpuFromLspci(line, linuxAmdgpuMem, linuxAmdIsApu, os.totalmem(), console.log)).filter((g) => g !== null && g.vendor !== "unknown");
3847
+ if (gpus.length) return gpus;
3848
+ }
3849
+ } catch {
3850
+ }
3851
+ return enumVulkanGpus();
3852
+ }
3853
+ function enumVulkanGpus() {
3854
+ try {
3855
+ const isAndroid = process.platform === "android";
3856
+ const env = isAndroid ? {
3857
+ ...process.env,
3858
+ LD_LIBRARY_PATH: `/system/lib64:${process.env.LD_LIBRARY_PATH ?? ""}`,
3859
+ LD_PRELOAD: "/system/lib64/liblzma.so"
3860
+ } : process.env;
3861
+ const out = execFileSync2("vulkaninfo", ["--summary"], { timeout: 8e3, env }).toString();
3862
+ const gpus = [];
3863
+ const lines = out.split("\n");
3864
+ let inDevice = false;
3865
+ let deviceName = "";
3866
+ let deviceType = "";
3867
+ for (const line of lines) {
3868
+ if (/^GPU\d+:/.test(line.trim())) {
3869
+ if (deviceName) gpus.push(makeVulkanGpu(deviceName, deviceType));
3870
+ inDevice = true;
3871
+ deviceName = "";
3872
+ deviceType = "";
3873
+ continue;
3874
+ }
3875
+ if (inDevice) {
3876
+ const m1 = line.match(/deviceName\s*=\s*(.+)/i);
3877
+ if (m1) deviceName = m1[1].trim();
3878
+ const m2 = line.match(/deviceType\s*=\s*(.+)/i);
3879
+ if (m2) deviceType = m2[1].trim();
3880
+ }
3881
+ }
3882
+ if (deviceName) gpus.push(makeVulkanGpu(deviceName, deviceType));
3883
+ return gpus;
3884
+ } catch {
3885
+ return [];
3886
+ }
3887
+ }
3888
+ function makeVulkanGpu(name, deviceType) {
3889
+ const vendor = classifyVendor(name);
3890
+ const integrated = /INTEGRATED_GPU/i.test(deviceType) || isIntegratedGpuName(name);
3891
+ const vramMb = integrated ? Math.round(os.totalmem() / 1e6 * 0.5) : 0;
3892
+ return { name, vramMb, vendor, ...integrated ? { unified: true } : {} };
3893
+ }
3894
+ function parseLspciMm(line) {
3895
+ const slot = line.trim().split(/\s+/)[0] ?? "";
3896
+ const fields = (line.match(/"[^"]*"/g) ?? []).map((f) => f.slice(1, -1).trim());
3897
+ if (!slot || slot.includes('"') || fields.length < 3) return null;
3898
+ return { slot, deviceClass: fields[0], vendor: fields[1], device: fields[2] };
3899
+ }
3900
+ function linuxGpuFromLspci(line, readMem, isApu, totalRamBytes, log = () => {
3901
+ }) {
3902
+ const entry = parseLspciMm(line);
3903
+ const slot = entry?.slot ?? (line.trim().split(/\s+/)[0] ?? "");
3904
+ if (!slot) return null;
3905
+ const label = entry ? `${entry.vendor} ${entry.device}` : line;
3906
+ const device = entry ? /^device\s+[0-9a-f]{4}$/i.test(entry.device) ? `${entry.vendor} ${entry.device}` : entry.device : "";
3907
+ const name = entry ? device.slice(0, 80) : line.replace(/"/g, "").trim().slice(0, 80);
3908
+ const vendor = classifyVendor(label);
3909
+ const mem = readMem(slot);
3910
+ let vramMb = mem.vramMb;
3911
+ let unified = false;
3912
+ if (mem.vramMb > 0 && vendor === "amd" && mem.gttMb > 0 && isApu(slot, label)) {
3913
+ vramMb = amdApuVramMb(mem.vramMb, mem.gttMb, totalRamBytes);
3914
+ unified = true;
3915
+ log(`[sysinfo] amdgpu APU at ${slot}: vram=${mem.vramMb}MB + gtt=${mem.gttMb}MB \u2192 ${vramMb}MB usable`);
3916
+ } else if (vramMb === 0 && isIntegratedGpuName(label)) {
3917
+ vramMb = Math.round(totalRamBytes / 1e6 * 0.5);
3918
+ unified = true;
3919
+ }
3920
+ return { name, vramMb, vendor, ...unified ? { unified: true } : {} };
3921
+ }
3922
+ function linuxAmdgpuMem(pciSlot) {
3923
+ if (!pciSlot) return { vramMb: 0, gttMb: 0 };
3843
3924
  for (const dev of [`/sys/bus/pci/devices/0000:${pciSlot}`, `/sys/bus/pci/devices/${pciSlot}`]) {
3925
+ const vram = readSysfsBytes(dev, "mem_info_vram_total");
3926
+ if (vram > 0) return { vramMb: Math.round(vram / 1e6), gttMb: Math.round(readSysfsBytes(dev, "mem_info_gtt_total") / 1e6) };
3927
+ }
3928
+ return { vramMb: 0, gttMb: 0 };
3929
+ }
3930
+ function readSysfsBytes(dir, attr) {
3931
+ try {
3932
+ const bytes = parseInt(fs.readFileSync(`${dir}/${attr}`, "utf8").trim(), 10);
3933
+ return Number.isFinite(bytes) && bytes > 0 ? bytes : 0;
3934
+ } catch {
3935
+ return 0;
3936
+ }
3937
+ }
3938
+ function amdApuVramMb(vramMb, gttMb, totalRamBytes) {
3939
+ const cap = Math.round(totalRamBytes / 1e6 * 0.9);
3940
+ return Math.max(vramMb, Math.min(vramMb + gttMb, cap));
3941
+ }
3942
+ function parseKfdNodes(propsTexts) {
3943
+ const nodes = [];
3944
+ for (const text of propsTexts) {
3945
+ const prop = (key) => {
3946
+ const m = new RegExp(`^${key}\\s+(\\d+)\\s*$`, "m").exec(text);
3947
+ return m ? parseInt(m[1], 10) : 0;
3948
+ };
3949
+ if (prop("simd_count") <= 0) continue;
3950
+ nodes.push({ domain: prop("domain"), locationId: prop("location_id"), fused: prop("cpu_cores_count") > 0 });
3951
+ }
3952
+ return nodes;
3953
+ }
3954
+ function pciSlotToKfdIds(slot) {
3955
+ const m = /^(?:([0-9a-f]{1,4}):)?([0-9a-f]{1,2}):([0-9a-f]{1,2})\.([0-7])$/i.exec(slot.trim());
3956
+ if (!m) return null;
3957
+ return {
3958
+ domain: m[1] === void 0 ? 0 : parseInt(m[1], 16),
3959
+ locationId: parseInt(m[2], 16) << 8 | (parseInt(m[3], 16) & 31) << 3 | parseInt(m[4], 10)
3960
+ };
3961
+ }
3962
+ function linuxAmdIsApu(pciSlot, label) {
3963
+ if (kfdReportsFusedNode(pciSlot)) return true;
3964
+ return isIntegratedGpuName(label);
3965
+ }
3966
+ function kfdReportsFusedNode(pciSlot) {
3967
+ const ids = pciSlotToKfdIds(pciSlot);
3968
+ if (!ids) return false;
3969
+ return readKfdNodes().some((n) => n.domain === ids.domain && n.locationId === ids.locationId && n.fused);
3970
+ }
3971
+ function readKfdNodes() {
3972
+ const base2 = "/sys/class/kfd/kfd/topology/nodes";
3973
+ try {
3974
+ const texts = [];
3975
+ for (const entry of fs.readdirSync(base2)) {
3976
+ try {
3977
+ texts.push(fs.readFileSync(`${base2}/${entry}/properties`, "utf8"));
3978
+ } catch {
3979
+ }
3980
+ }
3981
+ return parseKfdNodes(texts);
3982
+ } catch {
3983
+ return [];
3984
+ }
3985
+ }
3986
+ function getCpuModel() {
3987
+ const model = os.cpus()[0]?.model?.trim();
3988
+ if (model && model.length > 0) return model;
3989
+ if (process.platform === "linux" || process.platform === "android") {
3990
+ try {
3991
+ const cpuinfo = fs.readFileSync("/proc/cpuinfo", "utf8");
3992
+ const m = cpuinfo.match(/Hardware\s*:\s*(.+)/) || cpuinfo.match(/model name\s*:\s*(.+)/);
3993
+ if (m) return m[1].trim();
3994
+ } catch {
3995
+ }
3996
+ }
3997
+ return model ?? "";
3998
+ }
3999
+ function getCpuCoreCount() {
4000
+ const cpus = os.cpus();
4001
+ if (cpus.length > 1) return cpus.length;
4002
+ try {
4003
+ const n = parseInt(execFileSync2("nproc", { timeout: 2e3 }).toString().trim(), 10);
4004
+ if (n > 0) return n;
4005
+ } catch {
4006
+ }
4007
+ if (process.platform === "linux" || process.platform === "android") {
3844
4008
  try {
3845
- const bytes = parseInt(fs.readFileSync(`${dev}/mem_info_vram_total`, "utf8").trim(), 10);
3846
- if (Number.isFinite(bytes) && bytes > 0) return Math.round(bytes / 1e6);
4009
+ const cpuinfo = fs.readFileSync("/proc/cpuinfo", "utf8");
4010
+ const matches = cpuinfo.match(/processor\s*:/g);
4011
+ if (matches && matches.length > 0) return matches.length;
3847
4012
  } catch {
3848
4013
  }
3849
4014
  }
3850
- return 0;
4015
+ return cpus.length || 1;
3851
4016
  }
3852
4017
 
3853
4018
  // src/engines/update-apply.ts
@@ -5193,6 +5358,7 @@ function rowToAgentRun(r) {
5193
5358
  completion: r.completion ?? void 0,
5194
5359
  repoRoot: r.repo_root ?? void 0,
5195
5360
  repoBranch: r.repo_branch ?? void 0,
5361
+ codeAgent: r.code_agent ?? void 0,
5196
5362
  useWorktree: r.use_worktree === null ? void 0 : r.use_worktree === 1,
5197
5363
  worktreeBranch: r.worktree_branch ?? void 0,
5198
5364
  worktreeBase: r.worktree_base ?? void 0,
@@ -5203,7 +5369,8 @@ function rowToAgentRun(r) {
5203
5369
  compactionTokensBefore: r.compaction_tokens_before ?? void 0,
5204
5370
  clearedUpToMessageId: r.cleared_upto_message_id ?? void 0,
5205
5371
  revertedFromMessageId: r.reverted_from_message_id ?? void 0,
5206
- titleAutoSynced: r.title_auto_synced === 1
5372
+ titleAutoSynced: r.title_auto_synced === 1,
5373
+ terminalLaunchedOnce: r.terminal_launched_once === 1
5207
5374
  };
5208
5375
  }
5209
5376
  function rowToMsg(r) {
@@ -5489,6 +5656,29 @@ var ConversationStore = class {
5489
5656
  PRAGMA user_version = 34;
5490
5657
  `);
5491
5658
  }
5659
+ if (v < 35) {
5660
+ if (!this.hasColumn("agent_runs", "code_agent")) this.db.exec(`ALTER TABLE agent_runs ADD COLUMN code_agent TEXT;`);
5661
+ this.db.exec(`PRAGMA user_version = 35;`);
5662
+ }
5663
+ if (v < 36) {
5664
+ if (!this.hasColumn("api_usage", "code_session_id")) this.db.exec(`ALTER TABLE api_usage ADD COLUMN code_session_id TEXT;`);
5665
+ if (!this.hasColumn("api_usage", "duration_ms")) this.db.exec(`ALTER TABLE api_usage ADD COLUMN duration_ms INTEGER;`);
5666
+ this.db.exec(`CREATE INDEX IF NOT EXISTS idx_api_usage_code_session ON api_usage(code_session_id, created_at);`);
5667
+ this.db.exec(`PRAGMA user_version = 36;`);
5668
+ }
5669
+ if (v < 37) {
5670
+ if (!this.hasColumn("agent_runs", "terminal_launched_once")) this.db.exec(`ALTER TABLE agent_runs ADD COLUMN terminal_launched_once INTEGER NOT NULL DEFAULT 0;`);
5671
+ this.db.exec(`PRAGMA user_version = 37;`);
5672
+ }
5673
+ if (v < 38) {
5674
+ this.db.exec(`UPDATE agent_runs SET terminal_launched_once = 0;`);
5675
+ this.db.exec(`PRAGMA user_version = 38;`);
5676
+ }
5677
+ if (v < 39) {
5678
+ if (!this.hasColumn("api_usage", "prompt_tps")) this.db.exec(`ALTER TABLE api_usage ADD COLUMN prompt_tps REAL;`);
5679
+ if (!this.hasColumn("api_usage", "gen_tps")) this.db.exec(`ALTER TABLE api_usage ADD COLUMN gen_tps REAL;`);
5680
+ this.db.exec(`PRAGMA user_version = 39;`);
5681
+ }
5492
5682
  }
5493
5683
  listConversations(q, kind = "all") {
5494
5684
  if (q) {
@@ -5853,20 +6043,46 @@ var ConversationStore = class {
5853
6043
  }
5854
6044
  /** Insert one completed gateway request (GitHub #71) — called from the two gateway entry
5855
6045
  * points (gateway.ts) right where in-app chat already records into `messages`. Fully
5856
- * additive/fail-safe: callers wrap this the same way `recordCompletion` is wrapped. */
6046
+ * additive/fail-safe: callers wrap this the same way `recordCompletion` is wrapped.
6047
+ * `codeSessionId`/`durationMs` (ADR-284) are only populated for a terminal-agent session's
6048
+ * own token-identified traffic — undefined for every other gateway client, same as before. */
5857
6049
  recordApiUsage(rec) {
6050
+ const rate = (v) => v != null && Number.isFinite(v) && v > 0 ? v : null;
5858
6051
  this.db.prepare(`
5859
- INSERT INTO api_usage (id, created_at, source, model_key, prompt_tokens, gen_tokens)
5860
- VALUES ($id, $createdAt, $source, $modelKey, $promptTokens, $genTokens)
6052
+ INSERT INTO api_usage (id, created_at, source, model_key, prompt_tokens, gen_tokens, code_session_id, duration_ms, prompt_tps, gen_tps)
6053
+ VALUES ($id, $createdAt, $source, $modelKey, $promptTokens, $genTokens, $codeSessionId, $durationMs, $promptTps, $genTps)
5861
6054
  `).run({
5862
6055
  $id: randomUUID3(),
5863
6056
  $createdAt: (/* @__PURE__ */ new Date()).toISOString(),
5864
6057
  $source: rec.source,
5865
6058
  $modelKey: rec.modelKey,
5866
6059
  $promptTokens: Math.max(0, Math.floor(rec.promptTokens) || 0),
5867
- $genTokens: Math.max(0, Math.floor(rec.genTokens) || 0)
6060
+ $genTokens: Math.max(0, Math.floor(rec.genTokens) || 0),
6061
+ $codeSessionId: rec.codeSessionId ?? null,
6062
+ $durationMs: rec.durationMs != null && Number.isFinite(rec.durationMs) ? Math.max(0, Math.floor(rec.durationMs)) : null,
6063
+ $promptTps: rate(rec.promptTps),
6064
+ $genTps: rate(rec.genTps)
5868
6065
  });
5869
6066
  }
6067
+ /** Most recent completed gateway request attributed to a terminal-agent Code session
6068
+ * (ADR-284) — powers TerminalToolbar.tsx's composer-parity stats row the same way
6069
+ * `lastRealStats` already does for a 'turbollm' chat session's last turn. null when the
6070
+ * session has made no gateway requests yet (fresh session, or a non-terminal-agent one). */
6071
+ getLastApiUsageForSession(codeSessionId) {
6072
+ const row = this.db.prepare(`
6073
+ SELECT prompt_tokens, gen_tokens, duration_ms, prompt_tps, gen_tps FROM api_usage
6074
+ WHERE code_session_id = $codeSessionId
6075
+ ORDER BY created_at DESC LIMIT 1
6076
+ `).get({ $codeSessionId: codeSessionId });
6077
+ if (!row) return null;
6078
+ const seconds = row.duration_ms != null && row.duration_ms > 0 ? row.duration_ms / 1e3 : null;
6079
+ return {
6080
+ promptTokens: row.prompt_tokens,
6081
+ genTokens: row.gen_tokens,
6082
+ promptTps: row.prompt_tps ?? (seconds ? row.prompt_tokens / seconds : null),
6083
+ genTps: row.gen_tps ?? (seconds ? row.gen_tokens / seconds : null)
6084
+ };
6085
+ }
5870
6086
  /** Gateway (external-client) token usage — see `ApiUsageStats`'s doc comment for why this
5871
6087
  * is a separate bucket from `tokenUsageStats`. Same local-day range scoping as the chat
5872
6088
  * stats above, but no streak/session concepts (a gateway request isn't a chat turn). */
@@ -6115,7 +6331,7 @@ var ConversationStore = class {
6115
6331
  const id = randomUUID3();
6116
6332
  const now = (/* @__PURE__ */ new Date()).toISOString();
6117
6333
  const agentId = params.agentId ?? null;
6118
- this.db.prepare(`INSERT INTO agent_runs (id,conv_id,title,status,allowed_tools,agent_id,repo_root,repo_branch,use_worktree,worktree_branch,worktree_base,lines_added,lines_removed,created_at,updated_at) VALUES ($id,$cid,$title,'queued',$at,$aid,$rr,$rb,$uw,$wb,$wbase,$la,$lr,$now,$now)`).run({
6334
+ this.db.prepare(`INSERT INTO agent_runs (id,conv_id,title,status,allowed_tools,agent_id,repo_root,repo_branch,use_worktree,worktree_branch,worktree_base,lines_added,lines_removed,code_agent,created_at,updated_at) VALUES ($id,$cid,$title,'queued',$at,$aid,$rr,$rb,$uw,$wb,$wbase,$la,$lr,$ca,$now,$now)`).run({
6119
6335
  $id: id,
6120
6336
  $cid: params.convId,
6121
6337
  $title: params.title,
@@ -6130,6 +6346,7 @@ var ConversationStore = class {
6130
6346
  // sidebar shows +0/-0 rather than a blank; a background 'agent' run leaves them NULL.
6131
6347
  $la: params.repoRoot !== void 0 ? 0 : null,
6132
6348
  $lr: params.repoRoot !== void 0 ? 0 : null,
6349
+ $ca: params.codeAgent ?? null,
6133
6350
  $now: now
6134
6351
  });
6135
6352
  return this.getAgentRun(id);
@@ -6189,6 +6406,10 @@ var ConversationStore = class {
6189
6406
  sets.push("title_auto_synced = $tas");
6190
6407
  params.$tas = patch.titleAutoSynced ? 1 : 0;
6191
6408
  }
6409
+ if (patch.terminalLaunchedOnce !== void 0) {
6410
+ sets.push("terminal_launched_once = $tlo");
6411
+ params.$tlo = patch.terminalLaunchedOnce ? 1 : 0;
6412
+ }
6192
6413
  return this.db.prepare(`UPDATE agent_runs SET ${sets.join(", ")} WHERE id = $id`).run(params).changes > 0;
6193
6414
  }
6194
6415
  // ── Code session lifecycle (archive/delete/clear) ───────────────────────────
@@ -7020,8 +7241,8 @@ var DownloadManager = class {
7020
7241
  const verifyHash = hash !== null && startAt === 0;
7021
7242
  let lastTick = Date.now();
7022
7243
  let lastBytes = startAt;
7023
- const body6 = Readable2.fromWeb(res.body);
7024
- body6.on("data", (chunk) => {
7244
+ const body7 = Readable2.fromWeb(res.body);
7245
+ body7.on("data", (chunk) => {
7025
7246
  rec.received += chunk.length;
7026
7247
  if (verifyHash) hash.update(chunk);
7027
7248
  const now = Date.now();
@@ -7033,7 +7254,7 @@ var DownloadManager = class {
7033
7254
  }
7034
7255
  });
7035
7256
  const out = createWriteStream3(part, startAt > 0 ? { flags: "a" } : { flags: "w" });
7036
- await pipeline2(body6, out, { signal: ac.signal });
7257
+ await pipeline2(body7, out, { signal: ac.signal });
7037
7258
  if (rec.total > 0 && rec.received !== rec.total) {
7038
7259
  rmSync9(part, { force: true });
7039
7260
  throw new DownloadError("size_mismatch", "Download corrupt \u2014 size did not match. Removed the partial file.");
@@ -7551,8 +7772,8 @@ var BenchRunner = class {
7551
7772
  };
7552
7773
  } else {
7553
7774
  const memoryBound = results.length > 0 && results.every((r) => r.outcome === "oom");
7554
- const err6 = this.cancelled ? void 0 : memoryBound ? `This model doesn't fit on your GPU at ${baseProfile.ctx.toLocaleString()} context \u2014 even with maximum CPU offload it ran out of VRAM. Lower the context length and try again.` : "No candidate completed successfully.";
7555
- this.state = { running: false, modelKey, done: true, error: err6, candidates: results };
7775
+ const err7 = this.cancelled ? void 0 : memoryBound ? `This model doesn't fit on your GPU at ${baseProfile.ctx.toLocaleString()} context \u2014 even with maximum CPU offload it ran out of VRAM. Lower the context length and try again.` : "No candidate completed successfully.";
7776
+ this.state = { running: false, modelKey, done: true, error: err7, candidates: results };
7556
7777
  }
7557
7778
  }
7558
7779
  /** Dense models: pin `ngl` by VRAM probing (Phase 1), then run the full bench once (Phase 2).
@@ -8320,8 +8541,8 @@ function readNvidiaVramMb() {
8320
8541
  "nvidia-smi",
8321
8542
  ["--query-gpu=memory.used", "--format=csv,noheader,nounits"],
8322
8543
  { timeout: 8e3, windowsHide: true },
8323
- (err6, stdout) => {
8324
- if (err6 || !stdout) return resolve5(null);
8544
+ (err7, stdout) => {
8545
+ if (err7 || !stdout) return resolve5(null);
8325
8546
  const total = stdout.trim().split("\n").map((l) => parseInt(l.trim(), 10)).filter((n) => Number.isFinite(n)).reduce((a, b) => a + b, 0);
8326
8547
  resolve5(total > 0 ? total : null);
8327
8548
  }
@@ -8349,8 +8570,8 @@ function parseRocmVramUsed(memJson) {
8349
8570
  function readRocmVramMb() {
8350
8571
  return new Promise((resolve5) => {
8351
8572
  try {
8352
- execFile10("rocm-smi", ["--showmeminfo", "vram", "--json"], { timeout: 8e3, windowsHide: true }, (err6, stdout) => {
8353
- resolve5(err6 || !stdout ? null : parseRocmVramUsed(stdout));
8573
+ execFile10("rocm-smi", ["--showmeminfo", "vram", "--json"], { timeout: 8e3, windowsHide: true }, (err7, stdout) => {
8574
+ resolve5(err7 || !stdout ? null : parseRocmVramUsed(stdout));
8354
8575
  });
8355
8576
  } catch {
8356
8577
  resolve5(null);
@@ -8428,6 +8649,28 @@ var ModelRouter = class {
8428
8649
  }
8429
8650
  this.extraSlots.delete(entry.key);
8430
8651
  }
8652
+ return this.withSwapLock(() => this.doLoad(entry));
8653
+ }
8654
+ /** Acquire the SAME swap-serialization queue `route()` uses, then run `fn` exclusively with
8655
+ * respect to any other swap (manual or auto). `route()` itself is just this wrapping
8656
+ * `doLoad()` — exposed publicly so a MANUAL model switch (routes.ts's `/api/v1/engine/start`,
8657
+ * which loads the primary manager directly, entirely outside this router) can coordinate too.
8658
+ *
8659
+ * Why this was missing mattered in practice: the lower-level `Manager.runExclusive` static
8660
+ * gate already stops two loads from physically racing (no double-spawn), but it only
8661
+ * serialises EXECUTION order — it doesn't stop a router-triggered auto-swap (e.g. a
8662
+ * terminal-agent session's own gateway traffic) from independently deciding, mid-manual-
8663
+ * switch, "the primary is occupied, evict it and load MY model" (`evictChatLru()` picks
8664
+ * the primary as LRU whenever it's the only occupied slot, `'starting'`/`'stopping'`
8665
+ * included per ADR-285's `isOccupied()` fix). That decision would then queue behind the
8666
+ * manual switch at the `Manager` gate and win once it finally ran — so the model that ends
8667
+ * up loaded silently isn't the one the user just picked in the UI, which reads exactly like
8668
+ * "my switch reverted" even though nothing crashed or errored. Wrapping the manual switch in
8669
+ * this same queue means a concurrent auto-swap request now waits for the manual switch to
8670
+ * fully settle before it even re-checks "is the model I want already running" — so it either
8671
+ * fast-path no-ops (the manual switch happened to satisfy it) or proceeds cleanly AFTER,
8672
+ * never mid-flight. */
8673
+ async withSwapLock(fn) {
8431
8674
  let unlock;
8432
8675
  const prev = this.swapChain;
8433
8676
  this.swapChain = new Promise((r) => {
@@ -8435,11 +8678,18 @@ var ModelRouter = class {
8435
8678
  });
8436
8679
  try {
8437
8680
  await prev;
8438
- return await this.doLoad(entry);
8681
+ return await fn();
8439
8682
  } finally {
8440
8683
  unlock();
8441
8684
  }
8442
8685
  }
8686
+ /** A manual switch (routes.ts) always loads directly into the PRIMARY manager, never an
8687
+ * extra pool slot — so unlike `doLoad()`'s own bookkeeping, only `primaryLastUsed` needs
8688
+ * updating on success. Without this, a manual switch left the router's own LRU timestamp
8689
+ * stale, which could bias `evictChatLru()`'s choice on a later auto-swap. */
8690
+ markPrimaryLoaded() {
8691
+ this.primaryLastUsed = Date.now();
8692
+ }
8443
8693
  /** Every model key currently loaded (or loading) across the WHOLE pool — the primary
8444
8694
  * manager plus every alive extra slot (F-033). "Alive" = running OR starting, matching
8445
8695
  * the delete-guard's notion of "loaded" (routes.ts), so a model loaded via gateway
@@ -8513,32 +8763,45 @@ var ModelRouter = class {
8513
8763
  });
8514
8764
  return { target };
8515
8765
  }
8516
- /** Count of alive chat (non-embedding) slots. Embedding models don't consume
8766
+ /** A slot counts as occupied while 'stopping' too, not just 'running'/'starting'
8767
+ * a manual swap (routes.ts's /api/v1/engine/start, going straight to the primary
8768
+ * Manager, outside this router entirely) passes the primary through 'stopping' on
8769
+ * its way to the new model. A concurrent gateway request landing in that window
8770
+ * (e.g. a terminal-agent CLI's own request, racing a founder's manual model switch
8771
+ * in the UI) used to read the narrower running/starting-only check as "no slot is
8772
+ * occupied" and spin up a whole SECOND, independently-tracked Manager/llama-server
8773
+ * process — invisible to the primary manager's own status() and never cleaned up,
8774
+ * since nothing outside the router's own extraSlots map ever stops it. Found live:
8775
+ * a founder-reported "it loaded 2 models" during a manual switch while a terminal
8776
+ * session was open, confirmed via two concurrent llama-server.exe processes on
8777
+ * 8081/8082 where only 8081 was known to /api/v1/status. */
8778
+ isOccupied(state) {
8779
+ return state === "running" || state === "starting" || state === "stopping";
8780
+ }
8781
+ /** Count of occupied chat (non-embedding) slots. Embedding models don't consume
8517
8782
  * a keepN slot so chat models and embedding models can coexist independently. */
8518
8783
  chatSlotCount() {
8519
- const isAlive2 = (s) => s === "running" || s === "starting";
8520
8784
  const ms = this.manager.status();
8521
- const primaryAlive = isAlive2(ms.state);
8785
+ const primaryAlive = this.isOccupied(ms.state);
8522
8786
  const primaryEmbed = primaryAlive && !!ms.model && (this.scanner.get(ms.model.key)?.embedding ?? false);
8523
8787
  const extraChat = [...this.extraSlots.values()].filter(
8524
- (s) => isAlive2(s.manager.status().state) && !(this.scanner.get(s.modelKey)?.embedding ?? false)
8788
+ (s) => this.isOccupied(s.manager.status().state) && !(this.scanner.get(s.modelKey)?.embedding ?? false)
8525
8789
  ).length;
8526
8790
  return (primaryAlive && !primaryEmbed ? 1 : 0) + extraChat;
8527
8791
  }
8528
- /** Evict the least-recently-used chat (non-embedding) slot. Embedding slots are
8529
- * skipped; if every alive slot is an embedding model the true LRU is used as
8530
- * a fallback so we never deadlock. */
8792
+ /** Evict the least-recently-used occupied chat (non-embedding) slot. Embedding
8793
+ * slots are skipped; if every occupied slot is an embedding model the true LRU is
8794
+ * used as a fallback so we never deadlock. */
8531
8795
  evictChatLru() {
8532
- const isAlive2 = (s) => s === "running" || s === "starting";
8533
8796
  const ms = this.manager.status();
8534
- const primaryAlive = isAlive2(ms.state);
8797
+ const primaryAlive = this.isOccupied(ms.state);
8535
8798
  const primaryEmbed = primaryAlive && !!ms.model && (this.scanner.get(ms.model.key)?.embedding ?? false);
8536
8799
  let lruManager = this.manager;
8537
8800
  let lruTime = primaryAlive && !primaryEmbed ? this.primaryLastUsed : Infinity;
8538
8801
  let lruKey = null;
8539
8802
  for (const slot of this.extraSlots.values()) {
8540
8803
  const slotEmbed = this.scanner.get(slot.modelKey)?.embedding ?? false;
8541
- if (isAlive2(slot.manager.status().state) && !slotEmbed && slot.lastUsedMs < lruTime) {
8804
+ if (this.isOccupied(slot.manager.status().state) && !slotEmbed && slot.lastUsedMs < lruTime) {
8542
8805
  lruTime = slot.lastUsedMs;
8543
8806
  lruManager = slot.manager;
8544
8807
  lruKey = slot.modelKey;
@@ -8549,7 +8812,7 @@ var ModelRouter = class {
8549
8812
  lruManager = this.manager;
8550
8813
  lruKey = null;
8551
8814
  for (const slot of this.extraSlots.values()) {
8552
- if (isAlive2(slot.manager.status().state) && slot.lastUsedMs < lruTime) {
8815
+ if (this.isOccupied(slot.manager.status().state) && slot.lastUsedMs < lruTime) {
8553
8816
  lruTime = slot.lastUsedMs;
8554
8817
  lruManager = slot.manager;
8555
8818
  lruKey = slot.modelKey;
@@ -9987,12 +10250,12 @@ var GenerationGate = class {
9987
10250
  clearTimeout(timer);
9988
10251
  resolvePromise(release);
9989
10252
  },
9990
- giveUp: (err6) => {
10253
+ giveUp: (err7) => {
9991
10254
  if (settled) return;
9992
10255
  settled = true;
9993
10256
  signal?.removeEventListener("abort", onAbort);
9994
10257
  clearTimeout(timer);
9995
- rejectPromise(err6);
10258
+ rejectPromise(err7);
9996
10259
  }
9997
10260
  };
9998
10261
  const removeFromQueue = () => {
@@ -10096,11 +10359,21 @@ import { readFile, writeFile, mkdir } from "fs/promises";
10096
10359
  import { join as join20, dirname as dirname8 } from "path";
10097
10360
  var AUTH_TOKEN = "turbollm-local";
10098
10361
  var SUPPORTED = {
10099
- claude: { bin: "claude", label: "Claude Code", install: "npm install -g @anthropic-ai/claude-code" },
10362
+ claude: {
10363
+ bin: "claude",
10364
+ label: "Claude Code",
10365
+ install: "npm install -g @anthropic-ai/claude-code",
10366
+ // Measured against the real CLI: both mismatches exit 1 during startup, before any TUI
10367
+ // paints or any model call is made.
10368
+ // --session-id <existing> -> "Error: Session ID <uuid> is already in use."
10369
+ // --resume <unknown> -> "No conversation found with session ID: <uuid>"
10370
+ sessionFlags: { register: "--session-id", resume: "--resume" }
10371
+ },
10100
10372
  opencode: { bin: "opencode", label: "opencode", install: "npm install -g opencode-ai", prepareConfig: prepareOpencode },
10101
10373
  kilo: { bin: "kilo", label: "Kilo Code", install: "npm install -g @kilocode/cli", prepareConfig: prepareKilo },
10102
10374
  openclaw: { bin: "openclaw", label: "openclaw", install: "npm install -g openclaw@latest", prepareConfig: prepareOpenclaw },
10103
- hermes: { bin: "hermes", label: "Hermes Agent", install: "npm install -g hermes-agent", prepareConfig: prepareHermes }
10375
+ hermes: { bin: "hermes", label: "Hermes Agent", install: "npm install -g hermes-agent", prepareConfig: prepareHermes },
10376
+ pi: { bin: "pi", label: "pi", install: "npm install -g @earendil-works/pi-coding-agent", prepareConfig: preparePi }
10104
10377
  };
10105
10378
  async function fetchStatus(base2, _fetch = fetch) {
10106
10379
  try {
@@ -10289,6 +10562,39 @@ async function prepareOpenclaw(base2, apiKey, modelKey, modelName, fs2 = realFs)
10289
10562
  await fs2.writeFile(path, JSON.stringify(cfg2, null, 2) + "\n");
10290
10563
  return { ok: true };
10291
10564
  }
10565
+ async function preparePi(base2, apiKey, modelKey, modelName, fs2 = realFs) {
10566
+ const modelsPath = join20(fs2.home, ".pi", "agent", "models.json");
10567
+ const modelsRead = await readConfigObject(fs2, modelsPath);
10568
+ if ("corrupt" in modelsRead) return corruptConfigError(modelsPath, "pi");
10569
+ const modelsCfg = modelsRead.obj;
10570
+ const providers = asObject(modelsCfg.providers);
10571
+ if (!providers) return corruptConfigError(modelsPath, "pi");
10572
+ if (modelsRead.lenient) {
10573
+ if (!providerAlreadyPointsHere(providers.turbollm, base2)) return commentedConfigError(modelsPath, "pi");
10574
+ } else {
10575
+ providers.turbollm = {
10576
+ baseUrl: `${base2}/v1`,
10577
+ api: "openai-completions",
10578
+ apiKey,
10579
+ models: [{ id: modelKey, name: modelName }]
10580
+ };
10581
+ modelsCfg.providers = providers;
10582
+ await fs2.mkdir(dirname8(modelsPath));
10583
+ await fs2.writeFile(modelsPath, JSON.stringify(modelsCfg, null, 2) + "\n");
10584
+ }
10585
+ const settingsPath = join20(fs2.home, ".pi", "agent", "settings.json");
10586
+ const settingsRead = await readConfigObject(fs2, settingsPath);
10587
+ if ("corrupt" in settingsRead) return corruptConfigError(settingsPath, "pi");
10588
+ const settingsCfg = settingsRead.obj;
10589
+ if (settingsRead.lenient) {
10590
+ return settingsCfg.defaultProvider === "turbollm" && settingsCfg.defaultModel === modelKey ? { ok: true } : commentedConfigError(settingsPath, "pi");
10591
+ }
10592
+ settingsCfg.defaultProvider = "turbollm";
10593
+ settingsCfg.defaultModel = modelKey;
10594
+ await fs2.mkdir(dirname8(settingsPath));
10595
+ await fs2.writeFile(settingsPath, JSON.stringify(settingsCfg, null, 2) + "\n");
10596
+ return { ok: true };
10597
+ }
10292
10598
  var realRunCommand = (bin, args) => new Promise((resolve5) => {
10293
10599
  const child = spawn4(bin, args, { stdio: "ignore" });
10294
10600
  child.on("error", () => resolve5(false));
@@ -10333,7 +10639,7 @@ async function loadAndWait(base2, modelKey, timeoutMs = 18e4, _fetch = fetch) {
10333
10639
  }
10334
10640
  return false;
10335
10641
  }
10336
- async function launchCli(target, port2, passthrough, _spawn = spawn4, modelKey, _fetch = fetch) {
10642
+ async function launchCli(target, port2, passthrough, _spawn = spawn4, modelKey, _fetch = fetch, authToken) {
10337
10643
  const spec = SUPPORTED[target];
10338
10644
  if (!target || !spec) {
10339
10645
  const list = Object.keys(SUPPORTED).join(", ");
@@ -10429,18 +10735,19 @@ Open ${base2} \u2192 Models \u2192 Load a model, then run this again.
10429
10735
  process.stderr.write(prep.message + "\n");
10430
10736
  return 1;
10431
10737
  }
10432
- const child2 = _spawn(spec.bin, passthrough, {
10738
+ return await spawnWithSessionRecovery(spec, passthrough, {
10433
10739
  stdio: "inherit",
10434
10740
  shell: process.platform === "win32",
10435
10741
  env: process.env
10436
- });
10437
- return await waitForChild(child2, spec);
10742
+ }, _spawn);
10438
10743
  }
10439
10744
  const env = {
10440
10745
  ...process.env,
10441
10746
  ANTHROPIC_BASE_URL: base2,
10442
- // No auth is enforced on the local gateway; the CLI just needs a non-empty token.
10443
- ANTHROPIC_AUTH_TOKEN: AUTH_TOKEN,
10747
+ // No auth is enforced on the local gateway; the CLI just needs a non-empty token. A
10748
+ // session-scoped token (embedded terminal launches) takes priority over the shared static
10749
+ // one so the gateway can attribute this session's requests correctly (session-auth.ts).
10750
+ ANTHROPIC_AUTH_TOKEN: authToken ?? AUTH_TOKEN,
10444
10751
  // Local LLMs are 30–120 s per response — raise Claude Code's request timeout so it
10445
10752
  // doesn't abort mid-generation. 300 s (5 min) covers even the slowest local model.
10446
10753
  // Zero retries: retrying a slow local model cold-starts it again and makes things worse.
@@ -10457,15 +10764,40 @@ Open ${base2} \u2192 Models \u2192 Load a model, then run this again.
10457
10764
  // populates the /model picker with the local library (gateway synthesises the entries).
10458
10765
  CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY: "1"
10459
10766
  };
10460
- const child = _spawn(spec.bin, passthrough, {
10767
+ return await spawnWithSessionRecovery(spec, passthrough, {
10461
10768
  stdio: "inherit",
10462
10769
  // On Windows the CLI is usually a `.cmd`/`.ps1` shim; a shell resolves it via PATHEXT.
10463
10770
  shell: process.platform === "win32",
10464
10771
  env
10465
- });
10466
- return await waitForChild(child, spec);
10772
+ }, _spawn);
10773
+ }
10774
+ function swapSessionFlag(args, flags) {
10775
+ const i = args.findIndex((a) => a === flags.resume || a === flags.register);
10776
+ if (i === -1) return null;
10777
+ const out = [...args];
10778
+ out[i] = out[i] === flags.resume ? flags.register : flags.resume;
10779
+ return out;
10780
+ }
10781
+ var SESSION_MISMATCH_WINDOW_MS = 3e4;
10782
+ async function spawnWithSessionRecovery(spec, args, opts, _spawn) {
10783
+ const flags = spec.sessionFlags;
10784
+ const retryArgs = flags ? swapSessionFlag(args, flags) : null;
10785
+ if (!flags || !retryArgs) return await waitForChild(_spawn(spec.bin, args, opts), spec);
10786
+ const startedAt2 = Date.now();
10787
+ const first = await waitForChildExit(_spawn(spec.bin, args, opts), spec);
10788
+ const diedDuringStartup = Date.now() - startedAt2 < SESSION_MISMATCH_WINDOW_MS;
10789
+ if (first.code === 0 || first.signal || !diedDuringStartup) return first.code;
10790
+ process.stdout.write(
10791
+ retryArgs.includes(flags.register) ? `\u25B8 That ${spec.label} session is gone \u2014 starting a fresh one.
10792
+ ` : `\u25B8 That ${spec.label} session already exists \u2014 resuming it.
10793
+ `
10794
+ );
10795
+ return await waitForChild(_spawn(spec.bin, retryArgs, opts), spec);
10796
+ }
10797
+ async function waitForChild(child, spec) {
10798
+ return (await waitForChildExit(child, spec)).code;
10467
10799
  }
10468
- function waitForChild(child, spec) {
10800
+ function waitForChildExit(child, spec) {
10469
10801
  return new Promise((resolve5) => {
10470
10802
  child.on("error", (e) => {
10471
10803
  if (e.code === "ENOENT") {
@@ -10479,9 +10811,9 @@ Install it: ${spec.install}
10479
10811
  process.stderr.write(`Failed to launch ${spec.label}: ${e.message}
10480
10812
  `);
10481
10813
  }
10482
- resolve5(127);
10814
+ resolve5({ code: 127, signal: null });
10483
10815
  });
10484
- child.on("exit", (code, signal) => resolve5(code ?? (signal ? 1 : 0)));
10816
+ child.on("exit", (code, signal) => resolve5({ code: code ?? (signal ? 1 : 0), signal: signal ?? null }));
10485
10817
  });
10486
10818
  }
10487
10819
 
@@ -10538,8 +10870,8 @@ var defaultHooks = {
10538
10870
  try {
10539
10871
  const res = await fetch(`http://127.0.0.1:${port2}/api/v1/status`, { signal: AbortSignal.timeout(2e3) });
10540
10872
  if (!res.ok) return false;
10541
- const body6 = await res.json();
10542
- return typeof body6 === "object" && body6 !== null && "version" in body6 && "engine" in body6;
10873
+ const body7 = await res.json();
10874
+ return typeof body7 === "object" && body7 !== null && "version" in body7 && "engine" in body7;
10543
10875
  } catch {
10544
10876
  return false;
10545
10877
  }
@@ -10742,16 +11074,16 @@ function runMcpServer(baseUrl, version2) {
10742
11074
  // src/server.ts
10743
11075
  import { Hono } from "hono";
10744
11076
  import { cors } from "hono/cors";
10745
- import { existsSync as existsSync24, readFileSync as readFileSync13, statSync as statSync6 } from "fs";
11077
+ import { existsSync as existsSync24, readFileSync as readFileSync14, statSync as statSync6 } from "fs";
10746
11078
  import { fileURLToPath } from "url";
10747
- import { dirname as dirname11, join as join29, normalize as normalize2 } from "path";
11079
+ import { dirname as dirname11, join as join30, normalize as normalize2 } from "path";
10748
11080
  import { Agent, setGlobalDispatcher } from "undici";
10749
11081
 
10750
11082
  // src/api/routes.ts
10751
11083
  import { streamSSE as streamSSE2 } from "hono/streaming";
10752
- import { existsSync as existsSync22, mkdirSync as mkdirSync13, readFileSync as readFileSync10, readdirSync as readdirSync8, realpathSync, rmSync as rmSync12, statSync as statSync5, writeFileSync as writeFileSync11 } from "fs";
10753
- import { execFileSync as execFileSync5 } from "child_process";
10754
- import { basename as basename3, dirname as dirname9, join as join25, resolve } from "path";
11084
+ import { existsSync as existsSync22, mkdirSync as mkdirSync14, readFileSync as readFileSync11, readdirSync as readdirSync9, realpathSync, rmSync as rmSync13, statSync as statSync5, writeFileSync as writeFileSync12 } from "fs";
11085
+ import { execFileSync as execFileSync6 } from "child_process";
11086
+ import { basename as basename3, dirname as dirname9, join as join26, resolve } from "path";
10755
11087
 
10756
11088
  // src/comfyui/gate-template.ts
10757
11089
  var GATE_VERSION = 2;
@@ -12688,6 +13020,12 @@ function isLocalRequest(c, d) {
12688
13020
  if (!d.store.snapshot().daemon.lanBind) return true;
12689
13021
  return isLoopback(c) === true;
12690
13022
  }
13023
+ function isLocalUpgrade(remoteAddress, headers, d) {
13024
+ const tunneled = !!d.tunnel?.active() && !!(headers["cf-ray"] || headers["cf-connecting-ip"]);
13025
+ if (tunneled) return false;
13026
+ if (!d.store.snapshot().daemon.lanBind) return true;
13027
+ return !!remoteAddress && LOOPBACK.has(remoteAddress);
13028
+ }
12691
13029
  function bypassesAuth(opts) {
12692
13030
  const { lanBind, requireApiKey, tunneled, loopback, exempt } = opts;
12693
13031
  if (!lanBind && !tunneled) return true;
@@ -12703,8 +13041,7 @@ function isLocalOrAuthenticated(c, d) {
12703
13041
  if (isLoopback(c) === true && !tunneled) return true;
12704
13042
  return daemon.requireApiKey === true;
12705
13043
  }
12706
- function verifyPresentedKey(c, d) {
12707
- const key = presentedKey(c);
13044
+ function verifyKeyValue(key, d) {
12708
13045
  if (!key) return false;
12709
13046
  const hash = hashKey(key);
12710
13047
  const cfg2 = d.store.snapshot();
@@ -12719,6 +13056,9 @@ function verifyPresentedKey(c, d) {
12719
13056
  }
12720
13057
  return true;
12721
13058
  }
13059
+ function verifyPresentedKey(c, d) {
13060
+ return verifyKeyValue(presentedKey(c), d);
13061
+ }
12722
13062
  function lanAuth(d) {
12723
13063
  return async (c, next) => {
12724
13064
  const daemon = d.store.snapshot().daemon;
@@ -12758,6 +13098,730 @@ function enabledFeatures() {
12758
13098
  return KNOWN_FEATURES.filter((f) => requested.has(f));
12759
13099
  }
12760
13100
 
13101
+ // src/terminal/terminal-routes.ts
13102
+ import { createRequire as createRequire2 } from "module";
13103
+
13104
+ // src/code/session-auth.ts
13105
+ import { randomBytes as randomBytes2 } from "crypto";
13106
+ var SessionAuthRegistry = class {
13107
+ sessionToToken = /* @__PURE__ */ new Map();
13108
+ tokenToSession = /* @__PURE__ */ new Map();
13109
+ sessionThinkingBudget = /* @__PURE__ */ new Map();
13110
+ /** Mint a token for a Code session — idempotent: returns the SAME token on every call for a
13111
+ * given session (a reconnect/remount must never invalidate the CLI's already-running auth). */
13112
+ mint(codeSessionId) {
13113
+ const existing = this.sessionToToken.get(codeSessionId);
13114
+ if (existing) return existing;
13115
+ const token = `tllm-cs-${randomBytes2(24).toString("hex")}`;
13116
+ this.sessionToToken.set(codeSessionId, token);
13117
+ this.tokenToSession.set(token, codeSessionId);
13118
+ return token;
13119
+ }
13120
+ /** The Code session a presented token belongs to, or null for an unrecognized/expired token. */
13121
+ resolve(token) {
13122
+ return this.tokenToSession.get(token) ?? null;
13123
+ }
13124
+ /** Drop a session's token (terminal killed/exited) — safe to call even if none was ever
13125
+ * minted. Deliberately does NOT clear the session's stored thinking budget: a model-relaunch
13126
+ * (revoke + mint again under the same codeSessionId) should keep whatever the user had the
13127
+ * slider set to, not silently reset it. */
13128
+ revoke(codeSessionId) {
13129
+ const token = this.sessionToToken.get(codeSessionId);
13130
+ if (token) this.tokenToSession.delete(token);
13131
+ this.sessionToToken.delete(codeSessionId);
13132
+ }
13133
+ /** Set (or clear, with `null`) the live thinking-budget override for a session — takes effect
13134
+ * on the session's NEXT gateway request, no CLI restart involved (gateway.ts injects it into
13135
+ * the outbound request server-side). Keyed by codeSessionId, not by token, so it can be set
13136
+ * before a terminal/token even exists yet (composer mounts before the terminal connects). */
13137
+ setThinkingBudget(codeSessionId, tokens) {
13138
+ if (tokens === null) this.sessionThinkingBudget.delete(codeSessionId);
13139
+ else this.sessionThinkingBudget.set(codeSessionId, tokens);
13140
+ }
13141
+ /** The thinking-budget override for whichever session `token` belongs to, or null when the
13142
+ * token is unrecognized or no override has been set. */
13143
+ getThinkingBudgetForToken(token) {
13144
+ const sessionId = this.tokenToSession.get(token);
13145
+ if (!sessionId) return null;
13146
+ return this.sessionThinkingBudget.get(sessionId) ?? null;
13147
+ }
13148
+ };
13149
+ var sessionAuth = new SessionAuthRegistry();
13150
+
13151
+ // src/terminal/agent-modes.ts
13152
+ var CLAUDE_MODE_PREFERENCE = {
13153
+ auto: ["auto", "acceptEdits"],
13154
+ plan: ["plan"],
13155
+ ask: ["manual", "default"]
13156
+ };
13157
+ var CODE_MODES = /* @__PURE__ */ new Set(["auto", "plan", "ask"]);
13158
+ function parsePermissionModeChoices(helpText) {
13159
+ const m = helpText.match(/--permission-mode[\s\S]{0,400}?\(choices:([^)]*)\)/);
13160
+ if (!m) return [];
13161
+ return m[1].split(",").map((s) => s.trim().replace(/^["']|["']$/g, "").trim()).filter((s) => /^[A-Za-z]+$/.test(s));
13162
+ }
13163
+ function resolveClaudePermissionMode(mode, choices) {
13164
+ if (!CODE_MODES.has(mode)) return null;
13165
+ const prefs = CLAUDE_MODE_PREFERENCE[mode];
13166
+ if (choices.length === 0) return prefs[prefs.length - 1];
13167
+ return prefs.find((p) => choices.includes(p)) ?? null;
13168
+ }
13169
+ var realHelpRunner = async () => {
13170
+ const { spawn: spawn9 } = await import("child_process");
13171
+ return await new Promise((resolve5) => {
13172
+ let out = "";
13173
+ let settled = false;
13174
+ const done = (v) => {
13175
+ if (!settled) {
13176
+ settled = true;
13177
+ resolve5(v);
13178
+ }
13179
+ };
13180
+ try {
13181
+ const child = spawn9("claude", ["--help"], {
13182
+ shell: process.platform === "win32",
13183
+ stdio: ["ignore", "pipe", "ignore"]
13184
+ });
13185
+ const timer = setTimeout(() => {
13186
+ try {
13187
+ child.kill();
13188
+ } catch {
13189
+ }
13190
+ ;
13191
+ done("");
13192
+ }, 5e3);
13193
+ child.stdout?.on("data", (b) => {
13194
+ out += b.toString("utf8");
13195
+ });
13196
+ child.on("error", () => {
13197
+ clearTimeout(timer);
13198
+ done("");
13199
+ });
13200
+ child.on("close", () => {
13201
+ clearTimeout(timer);
13202
+ done(out);
13203
+ });
13204
+ } catch {
13205
+ done("");
13206
+ }
13207
+ });
13208
+ };
13209
+ var cachedChoices = null;
13210
+ async function claudePermissionModeChoices(run = realHelpRunner) {
13211
+ if (!cachedChoices) cachedChoices = run().then(parsePermissionModeChoices).catch(() => []);
13212
+ return await cachedChoices;
13213
+ }
13214
+
13215
+ // src/terminal/terminal-manager.ts
13216
+ import { spawnSync as spawnSync2 } from "child_process";
13217
+ import { mkdirSync as mkdirSync12, readFileSync as readFileSync10, readdirSync as readdirSync7, rmSync as rmSync11, writeFileSync as writeFileSync11 } from "fs";
13218
+ import { join as join23 } from "path";
13219
+
13220
+ // src/terminal/pty-session.ts
13221
+ import { EventEmitter } from "events";
13222
+ import { createRequire } from "module";
13223
+ import { execFileSync as execFileSync4 } from "child_process";
13224
+ var require2 = createRequire(import.meta.url);
13225
+ var cachedWindowsShell = null;
13226
+ function resolveWindowsShell() {
13227
+ if (cachedWindowsShell) return cachedWindowsShell;
13228
+ try {
13229
+ execFileSync4("where", ["pwsh.exe"], { stdio: "ignore" });
13230
+ cachedWindowsShell = "pwsh.exe";
13231
+ } catch {
13232
+ cachedWindowsShell = "powershell.exe";
13233
+ }
13234
+ return cachedWindowsShell;
13235
+ }
13236
+ var SCROLLBACK_CAP = 512 * 1024;
13237
+ var PTYSession = class _PTYSession {
13238
+ pty = null;
13239
+ emitter = new EventEmitter();
13240
+ exited = false;
13241
+ exitCode = null;
13242
+ processId = null;
13243
+ // Every byte the PTY has ever written, capped — replayed to a NEWLY connecting WebSocket
13244
+ // client (terminal-manager.ts's registerWsListener) so a reconnect (tab reload, WS drop,
13245
+ // navigating away and back) shows the terminal's actual current state instead of a blank
13246
+ // screen until the next byte happens to arrive. xterm.js replaying the same escape
13247
+ // sequences that produced the original screen reconstructs it correctly, the same way
13248
+ // `tmux attach`/`screen -r` catch a reconnecting client up.
13249
+ scrollback = "";
13250
+ /** Spawn a new PTY session. When `launchCommand` is given, the shell runs it as its
13251
+ * own startup command (never typed into stdin — see the module header comment). */
13252
+ static spawn(cwd, rows = 24, cols = 80, launchCommand) {
13253
+ const s = new _PTYSession();
13254
+ s.doSpawn(cwd, rows, cols, launchCommand);
13255
+ return s;
13256
+ }
13257
+ doSpawn(cwd, rows, cols, launchCommand) {
13258
+ const ptyModule = require2("node-pty");
13259
+ const win32 = process.platform === "win32";
13260
+ const shell = win32 ? resolveWindowsShell() : "bash";
13261
+ const args = launchCommand ? win32 ? ["-NoExit", "-Command", launchCommand] : ["-lc", `${launchCommand}; exec bash`] : [];
13262
+ this.pty = ptyModule.spawn(shell, args, {
13263
+ name: "xterm-256color",
13264
+ cols,
13265
+ rows,
13266
+ cwd,
13267
+ env: {
13268
+ ...process.env,
13269
+ TERM: "xterm-256color",
13270
+ COLORTERM: "truecolor"
13271
+ }
13272
+ });
13273
+ const termPty = this.pty;
13274
+ this.processId = termPty.pid;
13275
+ termPty.onData((data) => {
13276
+ this.scrollback += data;
13277
+ if (this.scrollback.length > SCROLLBACK_CAP) {
13278
+ this.scrollback = this.scrollback.slice(this.scrollback.length - SCROLLBACK_CAP);
13279
+ }
13280
+ this.emitter.emit("data", data);
13281
+ });
13282
+ termPty.onExit(({ exitCode, signal }) => {
13283
+ this.exited = true;
13284
+ this.exitCode = exitCode ?? signal ?? 1;
13285
+ this.emitter.emit("exit", { exitCode: this.exitCode, signal });
13286
+ });
13287
+ }
13288
+ // ── public API ──────────────────────────────────────────────────────
13289
+ /** Write raw bytes to the PTY's stdin. */
13290
+ write(data) {
13291
+ if (this.exited || !this.pty) return;
13292
+ this.pty.write(data);
13293
+ }
13294
+ /** Resize the PTY (cols, rows). */
13295
+ resize(cols, rows) {
13296
+ if (!this.pty) return;
13297
+ try {
13298
+ this.pty.resize(cols, rows);
13299
+ } catch {
13300
+ }
13301
+ }
13302
+ /** Kill the PTY and all child processes. */
13303
+ kill() {
13304
+ if (!this.pty || this.exited) return;
13305
+ try {
13306
+ this.pty.kill();
13307
+ } catch {
13308
+ }
13309
+ this.exited = true;
13310
+ }
13311
+ isExited() {
13312
+ return this.exited;
13313
+ }
13314
+ getExitCode() {
13315
+ return this.exitCode;
13316
+ }
13317
+ getPid() {
13318
+ return this.processId;
13319
+ }
13320
+ /** Everything written so far (capped, oldest trimmed) — replay this to a newly-attached
13321
+ * client before it starts receiving live `onData` output. */
13322
+ getScrollback() {
13323
+ return this.scrollback;
13324
+ }
13325
+ onData(cb) {
13326
+ this.emitter.on("data", cb);
13327
+ }
13328
+ onExit(cb) {
13329
+ this.emitter.on("exit", cb);
13330
+ }
13331
+ /** Remove all listeners and kill the PTY. */
13332
+ dispose() {
13333
+ this.kill();
13334
+ this.emitter.removeAllListeners();
13335
+ }
13336
+ };
13337
+
13338
+ // src/terminal/terminal-manager.ts
13339
+ function terminalPidDir(dataDir) {
13340
+ return join23(dataDir, "run");
13341
+ }
13342
+ function terminalPidFile(dataDir, pid) {
13343
+ return join23(terminalPidDir(dataDir), `terminal-${pid}.pid`);
13344
+ }
13345
+ function writeTerminalPid(dataDir, pid) {
13346
+ try {
13347
+ mkdirSync12(terminalPidDir(dataDir), { recursive: true });
13348
+ writeFileSync11(terminalPidFile(dataDir, pid), JSON.stringify({ pid, owner: process.pid }));
13349
+ } catch {
13350
+ }
13351
+ }
13352
+ function clearTerminalPid(dataDir, pid) {
13353
+ try {
13354
+ rmSync11(terminalPidFile(dataDir, pid), { force: true });
13355
+ } catch {
13356
+ }
13357
+ }
13358
+ function pidAlive2(pid) {
13359
+ if (!pid) return false;
13360
+ try {
13361
+ process.kill(pid, 0);
13362
+ return true;
13363
+ } catch (e) {
13364
+ return e.code === "EPERM";
13365
+ }
13366
+ }
13367
+ function readTerminalPidFiles(dataDir) {
13368
+ const dir = terminalPidDir(dataDir);
13369
+ let names;
13370
+ try {
13371
+ names = readdirSync7(dir).filter((n) => /^terminal-\d+\.pid$/.test(n));
13372
+ } catch {
13373
+ return [];
13374
+ }
13375
+ const out = [];
13376
+ for (const name of names) {
13377
+ const file = join23(dir, name);
13378
+ try {
13379
+ const { pid, owner } = JSON.parse(readFileSync10(file, "utf8"));
13380
+ if (typeof pid === "number" && pid > 0) out.push({ pid, owner: typeof owner === "number" ? owner : 0, file });
13381
+ else rmSync11(file, { force: true });
13382
+ } catch {
13383
+ try {
13384
+ rmSync11(file, { force: true });
13385
+ } catch {
13386
+ }
13387
+ }
13388
+ }
13389
+ return out;
13390
+ }
13391
+ function reapStaleTerminals(dataDir) {
13392
+ let killed = 0;
13393
+ for (const { pid, owner, file } of readTerminalPidFiles(dataDir)) {
13394
+ if (owner && pidAlive2(owner)) continue;
13395
+ try {
13396
+ if (process.platform === "win32") spawnSync2("taskkill", ["/PID", String(pid), "/F", "/T"]);
13397
+ else process.kill(pid, "SIGKILL");
13398
+ killed++;
13399
+ } catch {
13400
+ }
13401
+ try {
13402
+ rmSync11(file, { force: true });
13403
+ } catch {
13404
+ }
13405
+ }
13406
+ return killed;
13407
+ }
13408
+ function killTrackedTerminalsSync(dataDir) {
13409
+ for (const { pid, owner, file } of readTerminalPidFiles(dataDir)) {
13410
+ if (owner !== process.pid) continue;
13411
+ try {
13412
+ if (process.platform === "win32") spawnSync2("taskkill", ["/PID", String(pid), "/F", "/T"]);
13413
+ else process.kill(pid, "SIGKILL");
13414
+ } catch {
13415
+ }
13416
+ try {
13417
+ rmSync11(file, { force: true });
13418
+ } catch {
13419
+ }
13420
+ }
13421
+ }
13422
+ var IDLE_SWEEP_MS = 6e4;
13423
+ var TerminalManager = class {
13424
+ /** `dataDir` — the daemon's own data directory (ConfigStore.dir()), for the orphan-safety
13425
+ * pidfile tracking above. Terminal-manager unit tests never spawn a real PTY (see
13426
+ * terminal-manager.test.ts's header), so an empty string is a safe default there. */
13427
+ constructor(dataDir = "") {
13428
+ this.dataDir = dataDir;
13429
+ setInterval(() => this.cleanupIdle(), IDLE_SWEEP_MS).unref();
13430
+ }
13431
+ dataDir;
13432
+ sessions = /* @__PURE__ */ new Map();
13433
+ infoMap = /* @__PURE__ */ new Map();
13434
+ /** When each terminal last had ZERO attached WebSocket listeners — the idle clock. Absent
13435
+ * entirely while at least one listener is attached (registerWsListener deletes the entry),
13436
+ * so an actively-viewed terminal is NEVER a cleanupIdle candidate no matter how old it is.
13437
+ * Seeded at create() too, so a terminal that's created but never actually connected to
13438
+ * (a client-side failure right after the create() response) still starts its idle clock
13439
+ * rather than living forever untracked. */
13440
+ idleSince = /* @__PURE__ */ new Map();
13441
+ /**
13442
+ * Create a new terminal session.
13443
+ * @param repoRoot Working directory for the PTY
13444
+ * @param codeSessionId Optional Code session to associate with
13445
+ * @param cols Terminal columns (default 80)
13446
+ * @param rows Terminal rows (default 24)
13447
+ * @param launchCommand Optional command the shell runs at startup (see pty-session.ts) —
13448
+ * e.g. `turbollm launch claude`, built server-side (terminal-routes.ts), never sent by
13449
+ * the client. Absent for a plain interactive shell.
13450
+ * @returns The session ID
13451
+ */
13452
+ create(repoRoot, codeSessionId = null, cols = 80, rows = 24, launchCommand) {
13453
+ const id = crypto.randomUUID();
13454
+ const session = PTYSession.spawn(repoRoot, rows, cols, launchCommand);
13455
+ const shellPid = session.getPid();
13456
+ if (shellPid) writeTerminalPid(this.dataDir, shellPid);
13457
+ session.onData((data) => {
13458
+ const ev = this.infoMap.get(id);
13459
+ if (ev) this.broadcast(id, (h) => h.onData?.(data));
13460
+ });
13461
+ session.onExit(() => {
13462
+ this.broadcast(id, (h) => h.onClose?.());
13463
+ this.sessions.delete(id);
13464
+ this.infoMap.delete(id);
13465
+ this.idleSince.delete(id);
13466
+ if (shellPid) clearTerminalPid(this.dataDir, shellPid);
13467
+ if (codeSessionId) sessionAuth.revoke(codeSessionId);
13468
+ });
13469
+ this.sessions.set(id, session);
13470
+ this.infoMap.set(id, {
13471
+ id,
13472
+ codeSessionId,
13473
+ cwd: repoRoot,
13474
+ cols,
13475
+ rows,
13476
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
13477
+ });
13478
+ this.idleSince.set(id, Date.now());
13479
+ return id;
13480
+ }
13481
+ /**
13482
+ * Resize a terminal session.
13483
+ */
13484
+ resize(id, cols, rows) {
13485
+ const session = this.sessions.get(id);
13486
+ if (session) session.resize(cols, rows);
13487
+ const info = this.infoMap.get(id);
13488
+ if (info) {
13489
+ info.cols = cols;
13490
+ info.rows = rows;
13491
+ }
13492
+ }
13493
+ /**
13494
+ * Write data to a terminal's PTY stdin.
13495
+ */
13496
+ write(id, data) {
13497
+ const session = this.sessions.get(id);
13498
+ if (session) session.write(data);
13499
+ }
13500
+ /**
13501
+ * Kill a terminal session.
13502
+ */
13503
+ kill(id) {
13504
+ const session = this.sessions.get(id);
13505
+ if (session) {
13506
+ const shellPid = session.getPid();
13507
+ session.dispose();
13508
+ this.sessions.delete(id);
13509
+ this.infoMap.delete(id);
13510
+ this.idleSince.delete(id);
13511
+ if (shellPid) clearTerminalPid(this.dataDir, shellPid);
13512
+ }
13513
+ }
13514
+ /**
13515
+ * Check if a session exists and is alive.
13516
+ */
13517
+ isActive(id) {
13518
+ const session = this.sessions.get(id);
13519
+ const info = this.infoMap.get(id);
13520
+ return !!session && !!info && !session.isExited();
13521
+ }
13522
+ /**
13523
+ * Get info about a session.
13524
+ */
13525
+ getInfo(id) {
13526
+ return this.infoMap.get(id);
13527
+ }
13528
+ /**
13529
+ * List all session IDs.
13530
+ */
13531
+ listIds() {
13532
+ return Array.from(this.infoMap.keys());
13533
+ }
13534
+ /**
13535
+ * Look up a terminal ID by the Code session ID it was scoped to.
13536
+ * Returns null if no active terminal is associated with that Code session.
13537
+ */
13538
+ findByCodeSessionId(codeSessionId) {
13539
+ for (const [id, info] of this.infoMap) {
13540
+ if (info.codeSessionId === codeSessionId && this.sessions.get(id) && !this.sessions.get(id).isExited()) {
13541
+ return id;
13542
+ }
13543
+ }
13544
+ return null;
13545
+ }
13546
+ /** Record that the agent CLI in this Code session's terminal has exited (see
13547
+ * TerminalSessionInfo.agentExited). No-op when the session has no live terminal. */
13548
+ markAgentExited(codeSessionId) {
13549
+ const id = this.findByCodeSessionId(codeSessionId);
13550
+ if (!id) return;
13551
+ const info = this.infoMap.get(id);
13552
+ if (info) info.agentExited = true;
13553
+ }
13554
+ /** Whether this terminal's agent CLI has exited, leaving only a bare shell behind. */
13555
+ isAgentExited(terminalId) {
13556
+ return this.infoMap.get(terminalId)?.agentExited === true;
13557
+ }
13558
+ /**
13559
+ * Kill any terminal that's had ZERO attached WebSocket listeners for longer than `maxAgeMs`
13560
+ * (default 5 min) — e.g. every browser tab viewing it was closed/navigated away and nobody
13561
+ * came back. Deliberately NOT based on the terminal's age since creation: switching between
13562
+ * Chat and Code sessions (or between two Code sessions) must never kill a still-open CLI —
13563
+ * only genuine, sustained abandonment does. A terminal with at least one listener currently
13564
+ * attached is never a candidate, no matter how long it's been running.
13565
+ */
13566
+ cleanupIdle(maxAgeMs = 3e5) {
13567
+ const now = Date.now();
13568
+ for (const [id, since] of this.idleSince) {
13569
+ if ((this.listeners.get(id)?.size ?? 0) > 0) continue;
13570
+ if (now - since > maxAgeMs) this.kill(id);
13571
+ }
13572
+ }
13573
+ // ── WebSocket handler registration ─────────────────────────────────
13574
+ // A Code session's terminal can be attached from more than one browser tab at once (the
13575
+ // PTY itself already supports this — see the scrollback replay below, tmux-attach style) —
13576
+ // so each terminal id maps to a SET of handlers, not a single one. A single-handler map
13577
+ // would let a second tab's connect silently steal the first tab's slot, and either tab
13578
+ // closing would silence (or fully unregister) the other.
13579
+ listeners = /* @__PURE__ */ new Map();
13580
+ /** Call `fn` for every handler currently attached to `id`. No-op if none are attached. */
13581
+ broadcast(id, fn) {
13582
+ const handlers = this.listeners.get(id);
13583
+ if (!handlers) return;
13584
+ for (const h of handlers) fn(h);
13585
+ }
13586
+ /**
13587
+ * Register a WebSocket handler for a session. Called when the WebSocket
13588
+ * connects, and removed (via `unregisterWsListener`, passing the SAME handler
13589
+ * reference) when it closes.
13590
+ *
13591
+ * Replays the PTY's buffered scrollback to the handler BEFORE registering it for live
13592
+ * `onData` — otherwise a reconnecting client (tab reload, a dropped WS, navigating away
13593
+ * and back to the session) shows a blank terminal until the next byte happens to arrive,
13594
+ * even though the CLI underneath is still running and has already printed real output.
13595
+ */
13596
+ registerWsListener(id, handler) {
13597
+ const session = this.sessions.get(id);
13598
+ if (session) {
13599
+ const scrollback = session.getScrollback();
13600
+ if (scrollback) handler.onData(scrollback);
13601
+ }
13602
+ let handlers = this.listeners.get(id);
13603
+ if (!handlers) {
13604
+ handlers = /* @__PURE__ */ new Set();
13605
+ this.listeners.set(id, handlers);
13606
+ }
13607
+ handlers.add(handler);
13608
+ this.idleSince.delete(id);
13609
+ }
13610
+ /**
13611
+ * Unregister one WebSocket handler. Called when that WebSocket closes — must pass the
13612
+ * exact handler object given to `registerWsListener`, so a still-open second tab's
13613
+ * handler on the same terminal id is untouched.
13614
+ */
13615
+ unregisterWsListener(id, handler) {
13616
+ const handlers = this.listeners.get(id);
13617
+ if (!handlers) return;
13618
+ handlers.delete(handler);
13619
+ if (handlers.size === 0) {
13620
+ this.listeners.delete(id);
13621
+ if (this.infoMap.has(id)) this.idleSince.set(id, Date.now());
13622
+ }
13623
+ }
13624
+ };
13625
+
13626
+ // src/terminal/terminal-routes.ts
13627
+ async function body2(c) {
13628
+ try {
13629
+ return await c.req.json();
13630
+ } catch {
13631
+ return {};
13632
+ }
13633
+ }
13634
+ var require3 = createRequire2(import.meta.url);
13635
+ var AGENT_SESSION_ID_FLAGS = {
13636
+ claude: { first: "--session-id", resume: "--resume" }
13637
+ };
13638
+ function buildTerminalLaunchCommand(agent, port2, token, sessionId, launchedOnce, permissionMode) {
13639
+ const flags = AGENT_SESSION_ID_FLAGS[agent];
13640
+ const sessionArg = flags ? ` ${launchedOnce ? flags.resume : flags.first} ${sessionId}` : "";
13641
+ const modeArg = permissionMode ? ` --permission-mode ${permissionMode}` : "";
13642
+ return `turbollm launch ${agent} --port ${port2} --token ${token}${sessionArg}${modeArg}`;
13643
+ }
13644
+ function err2(c, s, code, msg2) {
13645
+ return c.json({ error: { code, message: msg2 } }, s);
13646
+ }
13647
+ var terminalManager = null;
13648
+ var backendAvailable = null;
13649
+ function isTerminalBackendAvailable() {
13650
+ if (backendAvailable === null) {
13651
+ try {
13652
+ require3.resolve("node-pty");
13653
+ backendAvailable = true;
13654
+ } catch {
13655
+ backendAvailable = false;
13656
+ }
13657
+ }
13658
+ return backendAvailable;
13659
+ }
13660
+ function getManager(_d) {
13661
+ if (!terminalManager) {
13662
+ if (isTerminalBackendAvailable()) {
13663
+ terminalManager = new TerminalManager(_d.store.dir());
13664
+ }
13665
+ }
13666
+ return terminalManager ?? {
13667
+ create: () => {
13668
+ throw new Error("node-pty not available \u2014 install with: npm install node-pty --save");
13669
+ },
13670
+ isActive: () => false,
13671
+ getInfo: () => void 0,
13672
+ listIds: () => [],
13673
+ kill: () => {
13674
+ },
13675
+ registerWsListener: () => {
13676
+ },
13677
+ unregisterWsListener: () => {
13678
+ },
13679
+ cleanupIdle: () => {
13680
+ },
13681
+ findByCodeSessionId: () => null
13682
+ };
13683
+ }
13684
+ function registerTerminalRoutes(app2, d) {
13685
+ app2.post("/api/v1/code/sessions/:sessionId/terminal", async (c) => {
13686
+ const run = d.db.getAgentRun(c.req.param("sessionId"));
13687
+ if (!run) return err2(c, 404, "not_found", "Session not found.");
13688
+ if (!run.repoRoot) return err2(c, 400, "invalid_input", "Session has no repo root.");
13689
+ const agent = run.codeAgent ?? "turbollm";
13690
+ if (agent === "turbollm") {
13691
+ return err2(c, 400, "invalid_input", "This session uses the built-in chat UI, not a terminal agent.");
13692
+ }
13693
+ const b = await body2(c);
13694
+ const cols = typeof b.cols === "number" && b.cols > 0 ? Math.floor(b.cols) : 80;
13695
+ const rows = typeof b.rows === "number" && b.rows > 0 ? Math.floor(b.rows) : 24;
13696
+ const m = getManager(d);
13697
+ const existing = m.findByCodeSessionId(run.id);
13698
+ if (existing && !m.isAgentExited(existing)) {
13699
+ m.resize(existing, cols, rows);
13700
+ return c.json({ terminalId: existing }, 200);
13701
+ }
13702
+ if (existing) {
13703
+ m.kill(existing);
13704
+ }
13705
+ try {
13706
+ const port2 = d.store.snapshot().daemon.port;
13707
+ const token = sessionAuth.mint(run.id);
13708
+ const mode = d.db.getConversation(run.convId)?.agentMode ?? "auto";
13709
+ const permissionMode = resolveClaudePermissionMode(mode, await claudePermissionModeChoices());
13710
+ const launchCommand = buildTerminalLaunchCommand(
13711
+ agent,
13712
+ port2,
13713
+ token,
13714
+ run.id,
13715
+ !!run.terminalLaunchedOnce,
13716
+ agent === "claude" ? permissionMode : null
13717
+ );
13718
+ const terminalId = m.create(run.repoRoot, run.id, cols, rows, launchCommand);
13719
+ if (!run.terminalLaunchedOnce) d.db.updateAgentRun(run.id, { terminalLaunchedOnce: true });
13720
+ return c.json({ terminalId }, 201);
13721
+ } catch (e) {
13722
+ const msg2 = e instanceof Error ? e.message : "Failed to create terminal session.";
13723
+ console.error("[terminal] create failed for session", run.id, ":", e);
13724
+ if (msg2.includes("node-pty not available")) return err2(c, 501, "not_available", msg2);
13725
+ return err2(c, 500, "create_failed", msg2);
13726
+ }
13727
+ });
13728
+ app2.post("/api/v1/code/sessions/:sessionId/terminal/kill", (c) => {
13729
+ const run = d.db.getAgentRun(c.req.param("sessionId"));
13730
+ if (!run) return err2(c, 404, "not_found", "Session not found.");
13731
+ const m = getManager(d);
13732
+ const terminalId = m.findByCodeSessionId(run.id);
13733
+ if (terminalId) m.kill(terminalId);
13734
+ sessionAuth.revoke(run.id);
13735
+ return c.json({ ok: true }, 200);
13736
+ });
13737
+ app2.post("/api/v1/code/sessions/:sessionId/terminal/agent-exited", (c) => {
13738
+ const run = d.db.getAgentRun(c.req.param("sessionId"));
13739
+ if (!run) return err2(c, 404, "not_found", "Session not found.");
13740
+ getManager(d).markAgentExited(run.id);
13741
+ return c.json({ ok: true }, 200);
13742
+ });
13743
+ app2.get("/api/v1/code/terminals", (c) => {
13744
+ const m = getManager(d);
13745
+ const ids = m.listIds();
13746
+ const sessions = ids.map((id) => m.getInfo(id)).filter(Boolean);
13747
+ return c.json({ terminals: sessions });
13748
+ });
13749
+ }
13750
+ var WS_OPEN = 1;
13751
+ function registerTerminalWs(server2, _d) {
13752
+ const wsLib = require3("ws");
13753
+ const WebSocketServer = wsLib.Server;
13754
+ const wss = new WebSocketServer({ noServer: true });
13755
+ server2.on("upgrade", (request, socket, head) => {
13756
+ const url = new URL(request.url ?? "", "http://localhost");
13757
+ if (url.pathname !== "/api/v1/code/terminal/ws") {
13758
+ socket.destroy();
13759
+ return;
13760
+ }
13761
+ const terminalId = url.searchParams.get("terminalId") ?? "";
13762
+ if (!terminalId) {
13763
+ socket.destroy();
13764
+ return;
13765
+ }
13766
+ const local = isLocalUpgrade(socket.remoteAddress, request.headers, _d);
13767
+ if (!local && !verifyKeyValue(url.searchParams.get("key") ?? "", _d)) {
13768
+ socket.write("HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n");
13769
+ socket.destroy();
13770
+ return;
13771
+ }
13772
+ const m = getManager(_d);
13773
+ if (!m.isActive(terminalId)) {
13774
+ socket.destroy();
13775
+ return;
13776
+ }
13777
+ wss.handleUpgrade(request, socket, head, (ws) => {
13778
+ wss.emit("connection", ws, request);
13779
+ });
13780
+ });
13781
+ wss.on("connection", (ws, _request) => {
13782
+ const url = new URL(_request.url ?? "", "http://localhost");
13783
+ const terminalId = url.searchParams.get("terminalId") ?? "";
13784
+ const m = getManager(_d);
13785
+ if (!m.isActive(terminalId)) {
13786
+ ws.close(4004, "Terminal session not found or exited");
13787
+ return;
13788
+ }
13789
+ const handler = {
13790
+ onData(data) {
13791
+ if (ws.readyState === WS_OPEN) {
13792
+ ws.send(data);
13793
+ }
13794
+ },
13795
+ onClose() {
13796
+ if (ws.readyState === WS_OPEN) {
13797
+ ws.close(1001, "Terminal exited");
13798
+ }
13799
+ }
13800
+ };
13801
+ m.registerWsListener(terminalId, handler);
13802
+ ws.on("message", (data, isBinary) => {
13803
+ if (isBinary && Buffer.isBuffer(data)) {
13804
+ try {
13805
+ const { cols, rows } = JSON.parse(data.toString("utf-8"));
13806
+ if (typeof cols === "number" && typeof rows === "number" && cols > 0 && rows > 0) {
13807
+ m.resize(terminalId, cols, rows);
13808
+ }
13809
+ } catch {
13810
+ }
13811
+ return;
13812
+ }
13813
+ const input = typeof data === "string" ? data : data.toString("utf-8");
13814
+ m.write(terminalId, input);
13815
+ });
13816
+ ws.on("close", () => {
13817
+ m.unregisterWsListener(terminalId, handler);
13818
+ });
13819
+ ws.on("error", () => {
13820
+ m.unregisterWsListener(terminalId, handler);
13821
+ });
13822
+ });
13823
+ }
13824
+
12761
13825
  // src/engines/catalog.ts
12762
13826
  var LLAMA_BACKEND_REQ = {
12763
13827
  cuda: { requires: { gpuVendor: ["nvidia"], backend: "cuda" }, speed: "fast" },
@@ -13177,9 +14241,9 @@ function catalogEngine(id) {
13177
14241
 
13178
14242
  // src/engines/cuda-provision.ts
13179
14243
  import { execFile as execFile11 } from "child_process";
13180
- import { cpSync as cpSync2, existsSync as existsSync20, mkdirSync as mkdirSync12, readdirSync as readdirSync7, rmSync as rmSync11 } from "fs";
14244
+ import { cpSync as cpSync2, existsSync as existsSync20, mkdirSync as mkdirSync13, readdirSync as readdirSync8, rmSync as rmSync12 } from "fs";
13181
14245
  import { tmpdir } from "os";
13182
- import { join as join23 } from "path";
14246
+ import { join as join24 } from "path";
13183
14247
  import { promisify as promisify8 } from "util";
13184
14248
  var execFileP8 = promisify8(execFile11);
13185
14249
  var REDIST_BASE = "https://developer.download.nvidia.com/compute/cuda/redist/";
@@ -13234,9 +14298,9 @@ async function provisionCuda(dataDir, onProgress, signal) {
13234
14298
  phase: "resolving",
13235
14299
  message: `Using CUDA ${version2}${driverMax ? ` (driver supports up to ${driverMax})` : ""}.`
13236
14300
  });
13237
- const root = join23(dataDir, "cuda", version2);
13238
- const binDir = join23(root, "bin");
13239
- if (existsSync20(join23(binDir, "nvcc.exe"))) {
14301
+ const root = join24(dataDir, "cuda", version2);
14302
+ const binDir = join24(root, "bin");
14303
+ if (existsSync20(join24(binDir, "nvcc.exe"))) {
13240
14304
  onProgress({ phase: "assembling", message: `CUDA ${version2} already downloaded \u2014 reusing it.` });
13241
14305
  return { root, binDir, version: version2 };
13242
14306
  }
@@ -13251,38 +14315,38 @@ async function provisionCuda(dataDir, onProgress, signal) {
13251
14315
  const rel = relOf(id);
13252
14316
  if (rel) components.push({ id, url: REDIST_BASE + rel });
13253
14317
  }
13254
- const work = join23(tmpdir(), `tllm-cuda-${version2}`);
13255
- rmSync11(work, { recursive: true, force: true });
13256
- mkdirSync12(work, { recursive: true });
14318
+ const work = join24(tmpdir(), `tllm-cuda-${version2}`);
14319
+ rmSync12(work, { recursive: true, force: true });
14320
+ mkdirSync13(work, { recursive: true });
13257
14321
  const stageRoot = `${root}.partial`;
13258
- rmSync11(stageRoot, { recursive: true, force: true });
13259
- mkdirSync12(stageRoot, { recursive: true });
14322
+ rmSync12(stageRoot, { recursive: true, force: true });
14323
+ mkdirSync13(stageRoot, { recursive: true });
13260
14324
  try {
13261
14325
  for (let i = 0; i < components.length; i++) {
13262
14326
  const { id, url } = components[i];
13263
- const zip = join23(work, `${id}.zip`);
14327
+ const zip = join24(work, `${id}.zip`);
13264
14328
  onProgress({ phase: "downloading", message: `Downloading ${id} (${i + 1}/${components.length})\u2026`, pct: 0 });
13265
14329
  await downloadFile(url, zip, (p) => onProgress({ phase: "downloading", message: `Downloading ${id} (${i + 1}/${components.length})\u2026`, pct: p.pct }), signal);
13266
- const exDir = join23(work, id);
13267
- mkdirSync12(exDir, { recursive: true });
14330
+ const exDir = join24(work, id);
14331
+ mkdirSync13(exDir, { recursive: true });
13268
14332
  onProgress({ phase: "extracting", message: `Extracting ${id}\u2026` });
13269
14333
  await extractArchive(zip, exDir);
13270
- const inner = readdirSync7(exDir).map((n) => join23(exDir, n)).find((p) => existsSync20(join23(p, "include")) || existsSync20(join23(p, "bin")) || existsSync20(join23(p, "lib")));
13271
- const srcRoot = inner ?? join23(exDir, readdirSync7(exDir)[0] ?? "");
14334
+ const inner = readdirSync8(exDir).map((n) => join24(exDir, n)).find((p) => existsSync20(join24(p, "include")) || existsSync20(join24(p, "bin")) || existsSync20(join24(p, "lib")));
14335
+ const srcRoot = inner ?? join24(exDir, readdirSync8(exDir)[0] ?? "");
13272
14336
  cpSync2(srcRoot, stageRoot, { recursive: true, force: true });
13273
- rmSync11(zip, { force: true });
14337
+ rmSync12(zip, { force: true });
13274
14338
  }
13275
- if (!existsSync20(join23(stageRoot, "bin", "nvcc.exe"))) {
14339
+ if (!existsSync20(join24(stageRoot, "bin", "nvcc.exe"))) {
13276
14340
  throw new Error("Assembled CUDA toolkit is missing bin/nvcc.exe \u2014 the component set may have changed.");
13277
14341
  }
13278
- rmSync11(root, { recursive: true, force: true });
13279
- mkdirSync12(join23(dataDir, "cuda"), { recursive: true });
14342
+ rmSync12(root, { recursive: true, force: true });
14343
+ mkdirSync13(join24(dataDir, "cuda"), { recursive: true });
13280
14344
  cpSync2(stageRoot, root, { recursive: true, force: true });
13281
14345
  onProgress({ phase: "assembling", message: `CUDA ${version2} ready.` });
13282
14346
  return { root, binDir, version: version2 };
13283
14347
  } finally {
13284
- rmSync11(work, { recursive: true, force: true });
13285
- rmSync11(stageRoot, { recursive: true, force: true });
14348
+ rmSync12(work, { recursive: true, force: true });
14349
+ rmSync12(stageRoot, { recursive: true, force: true });
13286
14350
  }
13287
14351
  }
13288
14352
 
@@ -13291,7 +14355,8 @@ function detectHardware(info = getSysInfo()) {
13291
14355
  const arch2 = process.arch === "arm64" ? "arm64" : "x64";
13292
14356
  const gpuVendor = primaryVendor(info);
13293
14357
  const hasGpu = gpuVendor !== "unknown" && info.gpus.length > 0;
13294
- const ofVendor = info.gpus.filter((g) => g.vendor === gpuVendor);
14358
+ const allOfVendor = info.gpus.filter((g) => g.vendor === gpuVendor);
14359
+ const ofVendor = allOfVendor.some((g) => !g.unified) ? allOfVendor.filter((g) => !g.unified) : allOfVendor;
13295
14360
  const pool = ofVendor.length ? ofVendor : info.gpus;
13296
14361
  const headline = pool.reduce(
13297
14362
  (best, g) => !best || g.vramMb > best.vramMb ? g : best,
@@ -13357,8 +14422,8 @@ function recommendEngines(p, engines) {
13357
14422
 
13358
14423
  // src/artifacts/screenshot.ts
13359
14424
  import { existsSync as existsSync21 } from "fs";
13360
- import { execFileSync as execFileSync4 } from "child_process";
13361
- import { join as join24 } from "path";
14425
+ import { execFileSync as execFileSync5 } from "child_process";
14426
+ import { join as join25 } from "path";
13362
14427
  function chromeCandidates() {
13363
14428
  const env = process.env.PUPPETEER_EXECUTABLE_PATH || process.env.CHROME_PATH;
13364
14429
  const out = env ? [env] : [];
@@ -13367,10 +14432,10 @@ function chromeCandidates() {
13367
14432
  const pfx86 = process.env["PROGRAMFILES(X86)"] || "C:\\Program Files (x86)";
13368
14433
  const local = process.env["LOCALAPPDATA"] || "";
13369
14434
  for (const base2 of [pf, pfx86, local].filter(Boolean)) {
13370
- out.push(join24(base2, "Google", "Chrome", "Application", "chrome.exe"));
14435
+ out.push(join25(base2, "Google", "Chrome", "Application", "chrome.exe"));
13371
14436
  }
13372
14437
  for (const base2 of [pfx86, pf].filter(Boolean)) {
13373
- out.push(join24(base2, "Microsoft", "Edge", "Application", "msedge.exe"));
14438
+ out.push(join25(base2, "Microsoft", "Edge", "Application", "msedge.exe"));
13374
14439
  }
13375
14440
  } else if (process.platform === "darwin") {
13376
14441
  out.push(
@@ -13389,7 +14454,7 @@ function chromeCandidates() {
13389
14454
  );
13390
14455
  for (const name of ["google-chrome", "google-chrome-stable", "chromium", "chromium-browser", "microsoft-edge"]) {
13391
14456
  try {
13392
- const p = execFileSync4("which", [name], { encoding: "utf-8" }).trim();
14457
+ const p = execFileSync5("which", [name], { encoding: "utf-8" }).trim();
13393
14458
  if (p) out.push(p);
13394
14459
  } catch {
13395
14460
  }
@@ -13452,10 +14517,10 @@ async function screenshotArtifact(html, opts) {
13452
14517
  }
13453
14518
 
13454
14519
  // src/api/routes.ts
13455
- function err2(c, status, code, message) {
14520
+ function err3(c, status, code, message) {
13456
14521
  return c.json({ error: { code, message } }, status);
13457
14522
  }
13458
- async function body2(c) {
14523
+ async function body3(c) {
13459
14524
  try {
13460
14525
  return await c.req.json();
13461
14526
  } catch {
@@ -13499,6 +14564,10 @@ function registerApi(app2, d) {
13499
14564
  agentTasks: d.agentTasks?.list() ?? [],
13500
14565
  // In-app compile-from-source status (ADR-100): live phase + log tail while a build runs.
13501
14566
  engineBuild: d.build.get(),
14567
+ // Whether this install can spawn a PTY at all (node-pty is an optional native dependency,
14568
+ // so a healthy install may legitimately not have it). The Code agent picker reads this to
14569
+ // decide whether a terminal-only agent is even offerable on this machine.
14570
+ terminalAvailable: isTerminalBackendAvailable(),
13502
14571
  // ComfyUI GPU coordination: lets the UI explain a paused/unloaded engine.
13503
14572
  // Also expose the installed gate node version so the UI can prompt an upgrade.
13504
14573
  comfyui: (() => {
@@ -13508,7 +14577,7 @@ function registerApi(app2, d) {
13508
14577
  let installedVersion = null;
13509
14578
  if (gatePath) {
13510
14579
  try {
13511
- const src = readFileSync10(join25(gatePath, "__init__.py"), "utf-8");
14580
+ const src = readFileSync11(join26(gatePath, "__init__.py"), "utf-8");
13512
14581
  const m = src.match(/^GATE_VERSION\s*=\s*(\d+)/m);
13513
14582
  if (m) installedVersion = Number(m[1]);
13514
14583
  } catch {
@@ -13527,13 +14596,13 @@ function registerApi(app2, d) {
13527
14596
  });
13528
14597
  app2.get("/api/v1/artifacts/screenshot/available", (c) => c.json({ available: findChrome() !== null }));
13529
14598
  app2.post("/api/v1/artifacts/screenshot", async (c) => {
13530
- if (!isLocalRequest(c, d)) return err2(c, 403, "forbidden", "Screenshot is local-only.");
13531
- const { html, width } = await body2(c);
13532
- if (!html || typeof html !== "string") return err2(c, 400, "bad_request", "html is required");
14599
+ if (!isLocalRequest(c, d)) return err3(c, 403, "forbidden", "Screenshot is local-only.");
14600
+ const { html, width } = await body3(c);
14601
+ if (!html || typeof html !== "string") return err3(c, 400, "bad_request", "html is required");
13533
14602
  const w = Math.max(360, Math.min(Math.round(width || 1e3), 1280));
13534
14603
  const h = Math.max(480, Math.round(w * 800 / 1280));
13535
14604
  const png = await screenshotArtifact(html, { width: w, height: h });
13536
- if (!png) return err2(c, 503, "no_browser", "No system Chrome/Chromium/Edge found.");
14605
+ if (!png) return err3(c, 503, "no_browser", "No system Chrome/Chromium/Edge found.");
13537
14606
  return new Response(png, { status: 200, headers: { "Content-Type": "image/png", "Cache-Control": "no-store" } });
13538
14607
  });
13539
14608
  app2.get("/api/v1/engines", (c) => {
@@ -13548,7 +14617,7 @@ function registerApi(app2, d) {
13548
14617
  const sys = getSysInfo();
13549
14618
  const vendor = primaryVendor(sys);
13550
14619
  const recommended = recommendBackendId(vendor, sys.gpus.length > 0);
13551
- const root = join25(d.store.dir(), "engines");
14620
+ const root = join26(d.store.dir(), "engines");
13552
14621
  const active = d.registry.active();
13553
14622
  const regEngines = d.registry.list().engines;
13554
14623
  const backendOf = (p) => p.match(/llama\.cpp-[^/\\]+-(cuda|rocm|sycl|vulkan|metal|cpu)/i)?.[1]?.toLowerCase();
@@ -13569,7 +14638,7 @@ function registerApi(app2, d) {
13569
14638
  engineId: eng?.id ?? ""
13570
14639
  };
13571
14640
  });
13572
- const mlxVenvPy = join25(root, "mlx", "venv", process.platform === "win32" ? "Scripts/python.exe" : "bin/python");
14641
+ const mlxVenvPy = join26(root, "mlx", "venv", process.platform === "win32" ? "Scripts/python.exe" : "bin/python");
13573
14642
  const mlxInstalledOnDisk = existsSync22(mlxVenvPy);
13574
14643
  const mlxEngine = regEngines.find((e) => e.kind === "mlx");
13575
14644
  const mlx = {
@@ -13582,17 +14651,17 @@ function registerApi(app2, d) {
13582
14651
  return c.json({ vendor, recommended, gpus: sys.gpus, backends, mlx });
13583
14652
  });
13584
14653
  app2.post("/api/v1/engines/backends/install", async (c) => {
13585
- const b = await body2(c);
14654
+ const b = await body3(c);
13586
14655
  const def = availableBackends().find((x) => x.id === b.backend);
13587
- if (!def) return err2(c, 400, "invalid_config_value", "Unknown backend for this platform.");
13588
- const root = join25(d.store.dir(), "engines");
14656
+ if (!def) return err3(c, 400, "invalid_config_value", "Unknown backend for this platform.");
14657
+ const root = join26(d.store.dir(), "engines");
13589
14658
  const existing = installedBackendBuild(root, def.id);
13590
14659
  if (existing) {
13591
14660
  return c.json({ accepted: false, alreadyInstalled: true, build: existing.tag });
13592
14661
  }
13593
14662
  {
13594
14663
  const busy = engineWorkBusy(d);
13595
- if (busy) return err2(c, 409, "engine_already_running", busy);
14664
+ if (busy) return err3(c, 409, "engine_already_running", busy);
13596
14665
  }
13597
14666
  const ac = new AbortController();
13598
14667
  provisionAbort = ac;
@@ -13628,27 +14697,27 @@ function registerApi(app2, d) {
13628
14697
  });
13629
14698
  app2.post("/api/v1/engines/backends/:id/update", async (c) => {
13630
14699
  const def = availableBackends().find((x) => x.id === c.req.param("id"));
13631
- if (!def) return err2(c, 400, "invalid_config_value", "Unknown backend for this platform.");
13632
- const root = join25(d.store.dir(), "engines");
14700
+ if (!def) return err3(c, 400, "invalid_config_value", "Unknown backend for this platform.");
14701
+ const root = join26(d.store.dir(), "engines");
13633
14702
  const oldBin = installedBackendBuild(root, def.id)?.bin ?? null;
13634
- if (!oldBin) return err2(c, 409, "not_installed", "Backend is not installed \u2014 download it first.");
14703
+ if (!oldBin) return err3(c, 409, "not_installed", "Backend is not installed \u2014 download it first.");
13635
14704
  {
13636
14705
  const busy = engineWorkBusy(d);
13637
- if (busy) return err2(c, 409, "engine_already_running", busy);
14706
+ if (busy) return err3(c, 409, "engine_already_running", busy);
13638
14707
  }
13639
14708
  let latestTag;
13640
14709
  try {
13641
14710
  latestTag = await latestReleaseTag("ggml-org/llama.cpp", AbortSignal.timeout(15e3));
13642
14711
  } catch {
13643
- return err2(c, 503, "offline", "Couldn't reach GitHub to check for a newer build. Try again when online.");
14712
+ return err3(c, 503, "offline", "Couldn't reach GitHub to check for a newer build. Try again when online.");
13644
14713
  }
13645
- if (!latestTag) return err2(c, 503, "offline", "GitHub didn't report a latest build. Try again later.");
14714
+ if (!latestTag) return err3(c, 503, "offline", "GitHub didn't report a latest build. Try again later.");
13646
14715
  const installedTag = tagFromManagedBinPath(oldBin);
13647
14716
  if (installedTag && installedTag === latestTag) {
13648
14717
  return c.json({ accepted: false, alreadyLatest: true, build: latestTag });
13649
14718
  }
13650
14719
  const newDef = backendDefAt(def.id, latestTag);
13651
- if (!newDef) return err2(c, 500, "internal", "No upstream build of this backend for your platform.");
14720
+ if (!newDef) return err3(c, 500, "internal", "No upstream build of this backend for your platform.");
13652
14721
  const ac = new AbortController();
13653
14722
  provisionAbort = ac;
13654
14723
  void (async () => {
@@ -13673,7 +14742,7 @@ function registerApi(app2, d) {
13673
14742
  }
13674
14743
  }
13675
14744
  if (installedTag && installedTag !== latestTag) {
13676
- rmSync12(backendDir(root, def.id, installedTag), { recursive: true, force: true });
14745
+ rmSync13(backendDir(root, def.id, installedTag), { recursive: true, force: true });
13677
14746
  }
13678
14747
  d.provision.done();
13679
14748
  } catch (e) {
@@ -13687,10 +14756,10 @@ function registerApi(app2, d) {
13687
14756
  });
13688
14757
  app2.post("/api/v1/engines/backends/:id/enable", async (c) => {
13689
14758
  const def = availableBackends().find((x) => x.id === c.req.param("id"));
13690
- if (!def) return err2(c, 400, "invalid_config_value", "Unknown backend for this platform.");
13691
- const root = join25(d.store.dir(), "engines");
14759
+ if (!def) return err3(c, 400, "invalid_config_value", "Unknown backend for this platform.");
14760
+ const root = join26(d.store.dir(), "engines");
13692
14761
  const build2 = installedBackendBuild(root, def.id);
13693
- if (!build2) return err2(c, 409, "not_installed", "Backend is not installed on disk \u2014 download it first.");
14762
+ if (!build2) return err3(c, 409, "not_installed", "Backend is not installed on disk \u2014 download it first.");
13694
14763
  let eng = d.registry.list().engines.find((e) => e.binPath === build2.bin);
13695
14764
  if (!eng) eng = (await d.registry.add(`llama.cpp ${build2.tag} (${def.id})`, build2.bin)).engine;
13696
14765
  d.registry.activate(eng.id);
@@ -13698,8 +14767,8 @@ function registerApi(app2, d) {
13698
14767
  });
13699
14768
  app2.delete("/api/v1/engines/backends/:id", async (c) => {
13700
14769
  const def = availableBackends().find((x) => x.id === c.req.param("id"));
13701
- if (!def) return err2(c, 400, "invalid_config_value", "Unknown backend for this platform.");
13702
- const root = join25(d.store.dir(), "engines");
14770
+ if (!def) return err3(c, 400, "invalid_config_value", "Unknown backend for this platform.");
14771
+ const root = join26(d.store.dir(), "engines");
13703
14772
  const backendDirRe = new RegExp(`[\\\\/]engines[\\\\/]llama\\.cpp-[^\\\\/]+-${def.id}[\\\\/]`);
13704
14773
  const engs = d.registry.list().engines.filter((e) => backendDirRe.test(e.binPath));
13705
14774
  const activeId = d.registry.active()?.id;
@@ -13715,13 +14784,13 @@ function registerApi(app2, d) {
13715
14784
  });
13716
14785
  app2.post("/api/v1/engines/mlx", (c) => {
13717
14786
  if (process.platform !== "darwin") {
13718
- return err2(c, 409, "unsupported_platform", "MLX is only available on macOS (Apple Silicon).");
14787
+ return err3(c, 409, "unsupported_platform", "MLX is only available on macOS (Apple Silicon).");
13719
14788
  }
13720
14789
  {
13721
14790
  const busy = engineWorkBusy(d);
13722
- if (busy) return err2(c, 409, "engine_already_running", busy);
14791
+ if (busy) return err3(c, 409, "engine_already_running", busy);
13723
14792
  }
13724
- const root = join25(d.store.dir(), "engines");
14793
+ const root = join26(d.store.dir(), "engines");
13725
14794
  const upgrade = c.req.query("update") === "1";
13726
14795
  void (async () => {
13727
14796
  try {
@@ -13738,13 +14807,13 @@ function registerApi(app2, d) {
13738
14807
  });
13739
14808
  app2.post("/api/v1/engines/rapid-mlx", (c) => {
13740
14809
  if (process.platform !== "darwin") {
13741
- return err2(c, 409, "unsupported_platform", "Rapid-MLX is only available on macOS (Apple Silicon).");
14810
+ return err3(c, 409, "unsupported_platform", "Rapid-MLX is only available on macOS (Apple Silicon).");
13742
14811
  }
13743
14812
  {
13744
14813
  const busy = engineWorkBusy(d);
13745
- if (busy) return err2(c, 409, "engine_already_running", busy);
14814
+ if (busy) return err3(c, 409, "engine_already_running", busy);
13746
14815
  }
13747
- const root = join25(d.store.dir(), "engines");
14816
+ const root = join26(d.store.dir(), "engines");
13748
14817
  const upgrade = c.req.query("update") === "1";
13749
14818
  void (async () => {
13750
14819
  try {
@@ -13761,12 +14830,12 @@ function registerApi(app2, d) {
13761
14830
  });
13762
14831
  app2.get("/api/v1/engines/catalog", (c) => {
13763
14832
  const regEngines = d.registry.list().engines;
13764
- const enginesRoot = join25(d.store.dir(), "engines");
14833
+ const enginesRoot = join26(d.store.dir(), "engines");
13765
14834
  const items = catalogForPlatform().map((e) => {
13766
14835
  let installed2;
13767
14836
  let enabled;
13768
14837
  if (e.provision === "pip") {
13769
- const pyPath = join25(
14838
+ const pyPath = join26(
13770
14839
  enginesRoot,
13771
14840
  e.id,
13772
14841
  "venv",
@@ -13775,7 +14844,7 @@ function registerApi(app2, d) {
13775
14844
  installed2 = existsSync22(pyPath);
13776
14845
  enabled = regEngines.some((x) => x.kind === e.kind);
13777
14846
  } else if (e.id === "turboquant") {
13778
- const tqDir = join25(enginesRoot, "turboquant");
14847
+ const tqDir = join26(enginesRoot, "turboquant");
13779
14848
  installed2 = existsSync22(tqDir);
13780
14849
  enabled = regEngines.some((x) => /[\\/]engines[\\/]turboquant[\\/]/.test(x.binPath));
13781
14850
  } else if (e.id === "koboldcpp") {
@@ -13821,33 +14890,33 @@ function registerApi(app2, d) {
13821
14890
  );
13822
14891
  app2.post("/api/v1/build/run", async (c) => {
13823
14892
  if (!isLocalRequest(c, d))
13824
- return err2(c, 403, "forbidden", "Building an engine is only available on the machine running TurboLLM.");
14893
+ return err3(c, 403, "forbidden", "Building an engine is only available on the machine running TurboLLM.");
13825
14894
  if (process.platform !== "win32" && process.platform !== "linux" && process.platform !== "darwin")
13826
- return err2(c, 409, "unsupported_platform", "In-app build is currently Windows, Linux, or macOS only.");
13827
- const b = await body2(c);
14895
+ return err3(c, 409, "unsupported_platform", "In-app build is currently Windows, Linux, or macOS only.");
14896
+ const b = await body3(c);
13828
14897
  const repoUrl = (b.repoUrl ?? "").trim();
13829
14898
  if (!/^https?:\/\//i.test(repoUrl))
13830
- return err2(c, 400, "invalid_config_value", "A http(s) repo URL is required.");
14899
+ return err3(c, 400, "invalid_config_value", "A http(s) repo URL is required.");
13831
14900
  const branch = (b.branch ?? "").trim() || void 0;
13832
14901
  if (branch && branch.startsWith("-"))
13833
- return err2(c, 400, "invalid_config_value", 'Branch name cannot start with "-".');
14902
+ return err3(c, 400, "invalid_config_value", 'Branch name cannot start with "-".');
13834
14903
  const commit = (b.commit ?? "").trim() || void 0;
13835
14904
  if (commit && !/^[0-9a-f]{7,40}$/i.test(commit))
13836
- return err2(c, 400, "invalid_config_value", "commit must be a hex SHA (7-40 chars).");
14905
+ return err3(c, 400, "invalid_config_value", "commit must be a hex SHA (7-40 chars).");
13837
14906
  const patchUrl = (b.patchUrl ?? "").trim() || void 0;
13838
14907
  const patchSha256 = (b.patchSha256 ?? "").trim() || void 0;
13839
14908
  if (!!patchUrl !== !!patchSha256)
13840
- return err2(c, 400, "invalid_config_value", "patchUrl and patchSha256 must be provided together \u2014 a patch may only be applied with a pinned checksum.");
14909
+ return err3(c, 400, "invalid_config_value", "patchUrl and patchSha256 must be provided together \u2014 a patch may only be applied with a pinned checksum.");
13841
14910
  if (patchUrl && !/^https?:\/\//i.test(patchUrl))
13842
- return err2(c, 400, "invalid_config_value", "patchUrl must be an http(s) URL.");
14911
+ return err3(c, 400, "invalid_config_value", "patchUrl must be an http(s) URL.");
13843
14912
  if (patchSha256 && !/^[0-9a-f]{64}$/i.test(patchSha256))
13844
- return err2(c, 400, "invalid_config_value", "patchSha256 must be a 64-char hex SHA-256.");
14913
+ return err3(c, 400, "invalid_config_value", "patchSha256 must be a 64-char hex SHA-256.");
13845
14914
  const busy = engineWorkBusy(d);
13846
- if (busy) return err2(c, 409, "engine_already_running", busy);
14915
+ if (busy) return err3(c, 409, "engine_already_running", busy);
13847
14916
  const name = (b.name ?? "").trim() || void 0;
13848
- const enginesRoot = join25(d.store.dir(), "engines");
14917
+ const enginesRoot = join26(d.store.dir(), "engines");
13849
14918
  const toolchainDirs = d.store.snapshot().build.toolchainDirs;
13850
- const buildRoot = join25(enginesRoot, "build", buildDirName(repoUrl, branch, commit));
14919
+ const buildRoot = join26(enginesRoot, "build", buildDirName(repoUrl, branch, commit));
13851
14920
  const ac = new AbortController();
13852
14921
  buildAbort = ac;
13853
14922
  d.build.start(name ?? repoUrl);
@@ -13862,7 +14931,7 @@ function registerApi(app2, d) {
13862
14931
  );
13863
14932
  } catch (e) {
13864
14933
  try {
13865
- rmSync12(buildRoot, { recursive: true, force: true });
14934
+ rmSync13(buildRoot, { recursive: true, force: true });
13866
14935
  } catch {
13867
14936
  }
13868
14937
  if (e?.name === "AbortError") {
@@ -13917,11 +14986,11 @@ function registerApi(app2, d) {
13917
14986
  });
13918
14987
  app2.post("/api/v1/build/cuda", async (c) => {
13919
14988
  if (!isLocalRequest(c, d))
13920
- return err2(c, 403, "forbidden", "Downloading CUDA is only available on the machine running TurboLLM.");
14989
+ return err3(c, 403, "forbidden", "Downloading CUDA is only available on the machine running TurboLLM.");
13921
14990
  if (process.platform !== "win32")
13922
- return err2(c, 409, "unsupported_platform", "Automatic CUDA download is currently Windows x86_64 only.");
14991
+ return err3(c, 409, "unsupported_platform", "Automatic CUDA download is currently Windows x86_64 only.");
13923
14992
  const busy = engineWorkBusy(d);
13924
- if (busy) return err2(c, 409, "engine_already_running", busy);
14993
+ if (busy) return err3(c, 409, "engine_already_running", busy);
13925
14994
  const dataDir = d.store.dir();
13926
14995
  const ac = new AbortController();
13927
14996
  buildAbort = ac;
@@ -13955,9 +15024,9 @@ function registerApi(app2, d) {
13955
15024
  app2.post("/api/v1/engines/vllm", (c) => {
13956
15025
  {
13957
15026
  const busy = engineWorkBusy(d);
13958
- if (busy) return err2(c, 409, "engine_already_running", busy);
15027
+ if (busy) return err3(c, 409, "engine_already_running", busy);
13959
15028
  }
13960
- const root = join25(d.store.dir(), "engines");
15029
+ const root = join26(d.store.dir(), "engines");
13961
15030
  const upgrade = c.req.query("update") === "1";
13962
15031
  void (async () => {
13963
15032
  try {
@@ -13975,9 +15044,9 @@ function registerApi(app2, d) {
13975
15044
  app2.post("/api/v1/engines/sglang", (c) => {
13976
15045
  {
13977
15046
  const busy = engineWorkBusy(d);
13978
- if (busy) return err2(c, 409, "engine_already_running", busy);
15047
+ if (busy) return err3(c, 409, "engine_already_running", busy);
13979
15048
  }
13980
- const root = join25(d.store.dir(), "engines");
15049
+ const root = join26(d.store.dir(), "engines");
13981
15050
  const upgrade = c.req.query("update") === "1";
13982
15051
  void (async () => {
13983
15052
  try {
@@ -13994,22 +15063,22 @@ function registerApi(app2, d) {
13994
15063
  });
13995
15064
  app2.post("/api/v1/engines/turboquant", (c) => {
13996
15065
  const entry = catalogEngine("turboquant");
13997
- if (!entry?.repo) return err2(c, 500, "internal", "TurboQuant catalog entry is misconfigured.");
15066
+ if (!entry?.repo) return err3(c, 500, "internal", "TurboQuant catalog entry is misconfigured.");
13998
15067
  if (!entry.platforms.includes(process.platform)) {
13999
- return err2(c, 409, "unsupported_platform", "TurboQuant has no prebuilt binary for this operating system yet.");
15068
+ return err3(c, 409, "unsupported_platform", "TurboQuant has no prebuilt binary for this operating system yet.");
14000
15069
  }
14001
15070
  {
14002
15071
  const busy = engineWorkBusy(d);
14003
- if (busy) return err2(c, 409, "engine_already_running", busy);
15072
+ if (busy) return err3(c, 409, "engine_already_running", busy);
14004
15073
  }
14005
- const root = join25(d.store.dir(), "engines");
15074
+ const root = join26(d.store.dir(), "engines");
14006
15075
  const upgrade = c.req.query("update") === "1";
14007
15076
  void (async () => {
14008
15077
  try {
14009
15078
  d.provision.start("turboquant");
14010
15079
  if (upgrade) {
14011
- const tqDir = join25(root, "turboquant");
14012
- if (existsSync22(tqDir)) rmSync12(tqDir, { recursive: true, force: true });
15080
+ const tqDir = join26(root, "turboquant");
15081
+ if (existsSync22(tqDir)) rmSync13(tqDir, { recursive: true, force: true });
14013
15082
  }
14014
15083
  const bin = await provisionTurboquant(
14015
15084
  root,
@@ -14029,15 +15098,15 @@ function registerApi(app2, d) {
14029
15098
  });
14030
15099
  app2.post("/api/v1/engines/koboldcpp", (c) => {
14031
15100
  const entry = catalogEngine("koboldcpp");
14032
- if (!entry?.repo) return err2(c, 500, "internal", "KoboldCpp catalog entry is misconfigured.");
15101
+ if (!entry?.repo) return err3(c, 500, "internal", "KoboldCpp catalog entry is misconfigured.");
14033
15102
  if (!entry.platforms.includes(process.platform)) {
14034
- return err2(c, 409, "unsupported_platform", "KoboldCpp has no prebuilt binary for this operating system yet.");
15103
+ return err3(c, 409, "unsupported_platform", "KoboldCpp has no prebuilt binary for this operating system yet.");
14035
15104
  }
14036
15105
  {
14037
15106
  const busy = engineWorkBusy(d);
14038
- if (busy) return err2(c, 409, "engine_already_running", busy);
15107
+ if (busy) return err3(c, 409, "engine_already_running", busy);
14039
15108
  }
14040
- const root = join25(d.store.dir(), "engines");
15109
+ const root = join26(d.store.dir(), "engines");
14041
15110
  const upgrade = c.req.query("update") === "1";
14042
15111
  const hasNvidia = primaryVendor(getSysInfo()) === "nvidia";
14043
15112
  void (async () => {
@@ -14045,7 +15114,7 @@ function registerApi(app2, d) {
14045
15114
  d.provision.start("koboldcpp");
14046
15115
  if (upgrade) {
14047
15116
  const dir = koboldcppDir(root);
14048
- if (existsSync22(dir)) rmSync12(dir, { recursive: true, force: true });
15117
+ if (existsSync22(dir)) rmSync13(dir, { recursive: true, force: true });
14049
15118
  }
14050
15119
  const rt = await ensureKoboldcpp(root, hasNvidia, (p) => d.provision.progress(p.phase, p.pct, p.part, p.parts));
14051
15120
  const eng = d.registry.addKoboldcpp(`KoboldCpp (${rt.version})`, rt.binPath, rt.version);
@@ -14060,22 +15129,22 @@ function registerApi(app2, d) {
14060
15129
  });
14061
15130
  app2.post("/api/v1/engines/llamafile", (c) => {
14062
15131
  const entry = catalogEngine("llamafile");
14063
- if (!entry?.repo) return err2(c, 500, "internal", "llamafile catalog entry is misconfigured.");
15132
+ if (!entry?.repo) return err3(c, 500, "internal", "llamafile catalog entry is misconfigured.");
14064
15133
  if (!entry.platforms.includes(process.platform)) {
14065
- return err2(c, 409, "unsupported_platform", "llamafile is not available for this operating system yet.");
15134
+ return err3(c, 409, "unsupported_platform", "llamafile is not available for this operating system yet.");
14066
15135
  }
14067
15136
  {
14068
15137
  const busy = engineWorkBusy(d);
14069
- if (busy) return err2(c, 409, "engine_already_running", busy);
15138
+ if (busy) return err3(c, 409, "engine_already_running", busy);
14070
15139
  }
14071
- const root = join25(d.store.dir(), "engines");
15140
+ const root = join26(d.store.dir(), "engines");
14072
15141
  const upgrade = c.req.query("update") === "1";
14073
15142
  void (async () => {
14074
15143
  try {
14075
15144
  d.provision.start("llamafile");
14076
15145
  if (upgrade) {
14077
15146
  const dir = llamafileDir(root);
14078
- if (existsSync22(dir)) rmSync12(dir, { recursive: true, force: true });
15147
+ if (existsSync22(dir)) rmSync13(dir, { recursive: true, force: true });
14079
15148
  }
14080
15149
  const rt = await ensureLlamafile(root, (p) => d.provision.progress(p.phase, p.pct, p.part, p.parts));
14081
15150
  const eng = d.registry.addLlamafile(`llamafile (${rt.version})`, rt.binPath, rt.version);
@@ -14090,9 +15159,9 @@ function registerApi(app2, d) {
14090
15159
  });
14091
15160
  app2.post("/api/v1/engines", async (c) => {
14092
15161
  if (!isLocalRequest(c, d))
14093
- return err2(c, 403, "forbidden", "Engines can only be added from the machine running TurboLLM.");
14094
- const b = await body2(c);
14095
- if (!b.binPath || !b.binPath.trim()) return err2(c, 400, "invalid_config_value", "binPath is required.");
15162
+ return err3(c, 403, "forbidden", "Engines can only be added from the machine running TurboLLM.");
15163
+ const b = await body3(c);
15164
+ if (!b.binPath || !b.binPath.trim()) return err3(c, 400, "invalid_config_value", "binPath is required.");
14096
15165
  try {
14097
15166
  const { engine, warning } = await d.registry.add(b.name ?? "", b.binPath, {
14098
15167
  sourceRepo: b.sourceRepo,
@@ -14104,17 +15173,17 @@ function registerApi(app2, d) {
14104
15173
  }
14105
15174
  return c.json({ ...engine, warning: warning ?? null }, 201);
14106
15175
  } catch (e) {
14107
- if (e instanceof NameTakenError) return err2(c, 400, "name_already_taken", e.message);
14108
- if (e instanceof ProbeError) return err2(c, 400, e.code, e.message);
14109
- return err2(c, 500, "internal", e.message);
15176
+ if (e instanceof NameTakenError) return err3(c, 400, "name_already_taken", e.message);
15177
+ if (e instanceof ProbeError) return err3(c, 400, e.code, e.message);
15178
+ return err3(c, 500, "internal", e.message);
14110
15179
  }
14111
15180
  });
14112
15181
  app2.post("/api/v1/engines/scan", async (c) => {
14113
15182
  if (!isLocalRequest(c, d))
14114
- return err2(c, 403, "forbidden", "Engine scanning is only available on the machine running TurboLLM.");
14115
- const b = await body2(c);
15183
+ return err3(c, 403, "forbidden", "Engine scanning is only available on the machine running TurboLLM.");
15184
+ const b = await body3(c);
14116
15185
  const path = (b.path ?? "").trim();
14117
- if (!path) return err2(c, 400, "invalid_config_value", "path is required.");
15186
+ if (!path) return err3(c, 400, "invalid_config_value", "path is required.");
14118
15187
  const binPath = resolveServerBinary(path);
14119
15188
  if (!binPath) return c.json({ found: false });
14120
15189
  try {
@@ -14127,12 +15196,12 @@ function registerApi(app2, d) {
14127
15196
  suggestedName: suggestEngineName(binPath, version2)
14128
15197
  });
14129
15198
  } catch (e) {
14130
- if (e instanceof ProbeError) return err2(c, 400, e.code, e.message);
14131
- return err2(c, 500, "internal", e.message);
15199
+ if (e instanceof ProbeError) return err3(c, 400, e.code, e.message);
15200
+ return err3(c, 500, "internal", e.message);
14132
15201
  }
14133
15202
  });
14134
15203
  app2.put("/api/v1/engines/:id", async (c) => {
14135
- const b = await body2(c);
15204
+ const b = await body3(c);
14136
15205
  const id = c.req.param("id");
14137
15206
  try {
14138
15207
  if (b.sourceRepo !== void 0 || b.sourceBranch !== void 0) {
@@ -14156,7 +15225,7 @@ function registerApi(app2, d) {
14156
15225
  const id = c.req.param("id");
14157
15226
  const { activeEngineId } = d.registry.list();
14158
15227
  if (id === activeEngineId && engineBusy(d)) {
14159
- return err2(c, 409, "engine_in_use", "Stop the engine before removing it.");
15228
+ return err3(c, 409, "engine_in_use", "Stop the engine before removing it.");
14160
15229
  }
14161
15230
  const purge = c.req.query("purge") === "1";
14162
15231
  const recordSource = c.req.query("recordSource") === "1";
@@ -14175,10 +15244,10 @@ function registerApi(app2, d) {
14175
15244
  }
14176
15245
  d.registry.remove(id);
14177
15246
  if (purge && eng) {
14178
- const enginesRoot = join25(d.store.dir(), "engines");
15247
+ const enginesRoot = join26(d.store.dir(), "engines");
14179
15248
  const purgeDir = engineInstallDir(eng, enginesRoot);
14180
15249
  if (purgeDir && existsSync22(purgeDir)) {
14181
- rmSync12(purgeDir, { recursive: true, force: true });
15250
+ rmSync13(purgeDir, { recursive: true, force: true });
14182
15251
  }
14183
15252
  d.registry.forgetCustomSource(customSourceKey(eng));
14184
15253
  }
@@ -14188,7 +15257,7 @@ function registerApi(app2, d) {
14188
15257
  }
14189
15258
  });
14190
15259
  app2.post("/api/v1/engines/:id/activate", (c) => {
14191
- if (engineBusy(d)) return err2(c, 409, "engine_running", "Stop the running engine before switching the active engine.");
15260
+ if (engineBusy(d)) return err3(c, 409, "engine_running", "Stop the running engine before switching the active engine.");
14192
15261
  try {
14193
15262
  d.registry.activate(c.req.param("id"));
14194
15263
  d.manager.clearError();
@@ -14201,7 +15270,7 @@ function registerApi(app2, d) {
14201
15270
  try {
14202
15271
  return c.json(await d.registry.reprobe(c.req.param("id")));
14203
15272
  } catch (e) {
14204
- if (e instanceof ProbeError) return err2(c, 400, e.code, e.message);
15273
+ if (e instanceof ProbeError) return err3(c, 400, e.code, e.message);
14205
15274
  return regErr(c, e);
14206
15275
  }
14207
15276
  });
@@ -14234,9 +15303,9 @@ function registerApi(app2, d) {
14234
15303
  return c.json(d.appUpdates.get() ?? fallback);
14235
15304
  });
14236
15305
  app2.put("/api/v1/engines/:id/update-policy", async (c) => {
14237
- const b = await body2(c);
15306
+ const b = await body3(c);
14238
15307
  if (b.policy !== "off" && b.policy !== "notify" && b.policy !== "auto") {
14239
- return err2(c, 400, "invalid_config_value", "policy must be off, notify, or auto.");
15308
+ return err3(c, 400, "invalid_config_value", "policy must be off, notify, or auto.");
14240
15309
  }
14241
15310
  try {
14242
15311
  return c.json(d.registry.setUpdatePolicy(c.req.param("id"), b.policy));
@@ -14246,7 +15315,7 @@ function registerApi(app2, d) {
14246
15315
  });
14247
15316
  app2.get("/api/v1/fs/browse", (c) => {
14248
15317
  if (!isLocalRequest(c, d))
14249
- return err2(c, 403, "forbidden", "Browsing the filesystem is only available on the machine running TurboLLM.");
15318
+ return err3(c, 403, "forbidden", "Browsing the filesystem is only available on the machine running TurboLLM.");
14250
15319
  const raw = (c.req.query("path") ?? "").trim();
14251
15320
  if (raw === FS_DRIVES_ROOT && process.platform === "win32") {
14252
15321
  return c.json({ path: FS_DRIVES_ROOT, parent: null, entries: listWindowsDrives() });
@@ -14260,19 +15329,19 @@ function registerApi(app2, d) {
14260
15329
  }
14261
15330
  let entries;
14262
15331
  try {
14263
- entries = readdirSync8(real, { withFileTypes: true }).filter((d2) => !d2.name.startsWith(".")).map((d2) => {
15332
+ entries = readdirSync9(real, { withFileTypes: true }).filter((d2) => !d2.name.startsWith(".")).map((d2) => {
14264
15333
  let isDir = d2.isDirectory();
14265
15334
  if (d2.isSymbolicLink()) {
14266
15335
  try {
14267
- isDir = statSync5(join25(real, d2.name)).isDirectory();
15336
+ isDir = statSync5(join26(real, d2.name)).isDirectory();
14268
15337
  } catch {
14269
15338
  isDir = false;
14270
15339
  }
14271
15340
  }
14272
- return { name: d2.name, path: join25(real, d2.name), isDir };
15341
+ return { name: d2.name, path: join26(real, d2.name), isDir };
14273
15342
  }).sort((a, b) => a.isDir === b.isDir ? a.name.localeCompare(b.name) : a.isDir ? -1 : 1);
14274
15343
  } catch {
14275
- return err2(c, 400, "fs_read_failed", "Could not read that folder (permission denied or not a directory).");
15344
+ return err3(c, 400, "fs_read_failed", "Could not read that folder (permission denied or not a directory).");
14276
15345
  }
14277
15346
  const parentDir = dirname9(real);
14278
15347
  const parent = parentDir === real ? process.platform === "win32" ? FS_DRIVES_ROOT : null : parentDir;
@@ -14280,14 +15349,14 @@ function registerApi(app2, d) {
14280
15349
  });
14281
15350
  app2.get("/api/v1/fs/git-branch", (c) => {
14282
15351
  if (!isLocalRequest(c, d))
14283
- return err2(c, 403, "forbidden", "Reading git info is only available on the machine running TurboLLM.");
15352
+ return err3(c, 403, "forbidden", "Reading git info is only available on the machine running TurboLLM.");
14284
15353
  const raw = (c.req.query("path") ?? "").trim();
14285
- if (!raw) return err2(c, 400, "invalid_input", "path is required.");
15354
+ if (!raw) return err3(c, 400, "invalid_input", "path is required.");
14286
15355
  const cwd = resolve(raw);
14287
15356
  if (!existsSync22(cwd) || !statSync5(cwd).isDirectory()) {
14288
15357
  return c.json({ isRepo: false, branch: "", branches: [] });
14289
15358
  }
14290
- const git2 = (args) => execFileSync5("git", args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 5e3, windowsHide: true }).trim();
15359
+ const git2 = (args) => execFileSync6("git", args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 5e3, windowsHide: true }).trim();
14291
15360
  try {
14292
15361
  if (git2(["rev-parse", "--is-inside-work-tree"]) !== "true") return c.json({ isRepo: false, branch: "", branches: [] });
14293
15362
  let branch = "";
@@ -14307,10 +15376,10 @@ function registerApi(app2, d) {
14307
15376
  }
14308
15377
  });
14309
15378
  app2.post("/api/v1/engine/start", async (c) => {
14310
- const b = await body2(c);
15379
+ const b = await body3(c);
14311
15380
  const active = d.registry.active();
14312
- if (!active) return err2(c, 409, "no_active_engine", "Register and select an engine first.");
14313
- if (d.comfy?.isBlocked()) return err2(c, 409, "comfyui_busy", "ComfyUI is rendering \u2014 model loading is paused until its queue finishes.");
15381
+ if (!active) return err3(c, 409, "no_active_engine", "Register and select an engine first.");
15382
+ if (d.comfy?.isBlocked()) return err3(c, 409, "comfyui_busy", "ComfyUI is rendering \u2014 model loading is paused until its queue finishes.");
14314
15383
  d.bench.cancel();
14315
15384
  abortAllInFlightChats();
14316
15385
  await d.bench.waitIdle();
@@ -14321,13 +15390,13 @@ function registerApi(app2, d) {
14321
15390
  const entry = key ? d.scanner.get(key) : void 0;
14322
15391
  if (entry) {
14323
15392
  if (entry.incomplete || entry.parseError) {
14324
- return err2(c, 409, "model_not_loadable", "This model is incomplete or unreadable.");
15393
+ return err3(c, 409, "model_not_loadable", "This model is incomplete or unreadable.");
14325
15394
  }
14326
15395
  if (!engineAcceptsFormat(active.kind, entry.format)) {
14327
- return err2(c, 409, "engine_model_mismatch", formatMismatchMessage(active.kind, entry.format));
15396
+ return err3(c, 409, "engine_model_mismatch", formatMismatchMessage(active.kind, entry.format));
14328
15397
  }
14329
15398
  if (entry.audio && engineRejectsAudioModel(active.kind)) {
14330
- return err2(
15399
+ return err3(
14331
15400
  c,
14332
15401
  409,
14333
15402
  "engine_model_mismatch",
@@ -14358,7 +15427,7 @@ function registerApi(app2, d) {
14358
15427
  preferredPort: profile.port
14359
15428
  };
14360
15429
  }
14361
- void d.manager.load(opts2, { beforeStart: () => d.comfy?.freeComfyUIBeforeLoad() ?? Promise.resolve() }).catch((e) => console.warn(`engine load failed: ${e}`));
15430
+ void d.modelRouter.withSwapLock(() => d.manager.load(opts2, { beforeStart: () => d.comfy?.freeComfyUIBeforeLoad() ?? Promise.resolve() })).then(() => d.modelRouter.markPrimaryLoaded()).catch((e) => console.warn(`engine load failed: ${e}`));
14362
15431
  d.store.update((x) => {
14363
15432
  x.lastLoaded = { modelKey: entry.key, engineId: active.id };
14364
15433
  });
@@ -14372,9 +15441,9 @@ function registerApi(app2, d) {
14372
15441
  extra = cfg2.devModel.extraArgs;
14373
15442
  name = cfg2.devModel.label;
14374
15443
  }
14375
- if (!modelPath) return err2(c, 409, "no_such_model", "No model specified. Pick one from the Models screen.");
15444
+ if (!modelPath) return err3(c, 409, "no_such_model", "No model specified. Pick one from the Models screen.");
14376
15445
  const opts = { engine: active, model: deriveModel(modelPath, name, extra), modelPath, extraArgs: extra };
14377
- void d.manager.load(opts, { beforeStart: () => d.comfy?.freeComfyUIBeforeLoad() ?? Promise.resolve() }).catch((e) => console.warn(`engine load failed: ${e}`));
15446
+ void d.modelRouter.withSwapLock(() => d.manager.load(opts, { beforeStart: () => d.comfy?.freeComfyUIBeforeLoad() ?? Promise.resolve() })).then(() => d.modelRouter.markPrimaryLoaded()).catch((e) => console.warn(`engine load failed: ${e}`));
14378
15447
  return c.json({ ok: true }, 202);
14379
15448
  });
14380
15449
  app2.post("/api/v1/engine/stop", (c) => {
@@ -14403,28 +15472,28 @@ function registerApi(app2, d) {
14403
15472
  return c.json({ ok: true });
14404
15473
  });
14405
15474
  app2.post("/api/v1/comfyui/install", async (c) => {
14406
- const b = await body2(c);
15475
+ const b = await body3(c);
14407
15476
  const raw = (b.path ?? "").trim();
14408
- if (!raw) return err2(c, 400, "invalid_config_value", "Enter the path to your ComfyUI folder.");
15477
+ if (!raw) return err3(c, 400, "invalid_config_value", "Enter the path to your ComfyUI folder.");
14409
15478
  const root = resolve(raw);
14410
15479
  if (!existsSync22(root) || !statSync5(root).isDirectory()) {
14411
- return err2(c, 400, "invalid_config_value", "That folder does not exist.");
15480
+ return err3(c, 400, "invalid_config_value", "That folder does not exist.");
14412
15481
  }
14413
15482
  let customNodes;
14414
15483
  if (basename3(root).toLowerCase() === "custom_nodes") customNodes = root;
14415
- else if (existsSync22(join25(root, "custom_nodes"))) customNodes = join25(root, "custom_nodes");
15484
+ else if (existsSync22(join26(root, "custom_nodes"))) customNodes = join26(root, "custom_nodes");
14416
15485
  else {
14417
- return err2(c, 400, "invalid_config_value", "No 'custom_nodes' folder here. Point me at your ComfyUI folder or its custom_nodes folder.");
15486
+ return err3(c, 400, "invalid_config_value", "No 'custom_nodes' folder here. Point me at your ComfyUI folder or its custom_nodes folder.");
14418
15487
  }
14419
15488
  const reqUrl = new URL(c.req.url);
14420
15489
  const port2 = reqUrl.port || (reqUrl.protocol === "https:" ? "443" : "80");
14421
15490
  const base2 = `http://127.0.0.1:${port2}`;
14422
- const gateDir = join25(customNodes, "turbollm_gate");
15491
+ const gateDir = join26(customNodes, "turbollm_gate");
14423
15492
  try {
14424
- mkdirSync13(gateDir, { recursive: true });
14425
- writeFileSync11(join25(gateDir, "__init__.py"), gateNodeSource(base2));
15493
+ mkdirSync14(gateDir, { recursive: true });
15494
+ writeFileSync12(join26(gateDir, "__init__.py"), gateNodeSource(base2));
14426
15495
  } catch (e) {
14427
- return err2(c, 500, "fs_write_failed", `Could not write the gate node: ${e instanceof Error ? e.message : e}`);
15496
+ return err3(c, 500, "fs_write_failed", `Could not write the gate node: ${e instanceof Error ? e.message : e}`);
14428
15497
  }
14429
15498
  d.store.update((x) => {
14430
15499
  x.comfyui.gatePath = gateDir;
@@ -14435,9 +15504,9 @@ function registerApi(app2, d) {
14435
15504
  const dir = d.store.snapshot().comfyui.gatePath;
14436
15505
  if (dir && existsSync22(dir)) {
14437
15506
  try {
14438
- rmSync12(dir, { recursive: true, force: true });
15507
+ rmSync13(dir, { recursive: true, force: true });
14439
15508
  } catch (e) {
14440
- return err2(c, 500, "fs_write_failed", `Could not remove the gate node: ${e instanceof Error ? e.message : e}`);
15509
+ return err3(c, 500, "fs_write_failed", `Could not remove the gate node: ${e instanceof Error ? e.message : e}`);
14441
15510
  }
14442
15511
  }
14443
15512
  d.store.update((x) => {
@@ -14447,7 +15516,7 @@ function registerApi(app2, d) {
14447
15516
  });
14448
15517
  app2.post("/api/v1/daemon/restart", (c) => {
14449
15518
  if (!d.requestRestart) {
14450
- return err2(c, 501, "not_supported", "Daemon restart is not available in this run mode.");
15519
+ return err3(c, 501, "not_supported", "Daemon restart is not available in this run mode.");
14451
15520
  }
14452
15521
  const restart = d.requestRestart;
14453
15522
  setTimeout(() => restart(), 200).unref();
@@ -14469,7 +15538,7 @@ function registerApi(app2, d) {
14469
15538
  while (!aborted) {
14470
15539
  const path = d.manager.logPath();
14471
15540
  if (path && existsSync22(path)) {
14472
- const lines = readFileSync10(path, "utf8").split("\n");
15541
+ const lines = readFileSync11(path, "utf8").split("\n");
14473
15542
  for (; sent < lines.length - 1; sent++) {
14474
15543
  await stream.writeSSE({ event: "line", data: JSON.stringify({ line: lines[sent].replace(/\r$/, "") }) });
14475
15544
  }
@@ -14480,10 +15549,10 @@ function registerApi(app2, d) {
14480
15549
  })
14481
15550
  );
14482
15551
  app2.post("/api/v1/bench", async (c) => {
14483
- const b = await body2(c);
15552
+ const b = await body3(c);
14484
15553
  const key = (b.modelKey ?? "").trim();
14485
- if (!key) return err2(c, 400, "invalid_config_value", "modelKey is required.");
14486
- if (d.comfy?.isBlocked()) return err2(c, 409, "comfyui_busy", "ComfyUI is rendering \u2014 benchmarking is paused until its queue finishes.");
15554
+ if (!key) return err3(c, 400, "invalid_config_value", "modelKey is required.");
15555
+ if (d.comfy?.isBlocked()) return err3(c, 409, "comfyui_busy", "ComfyUI is rendering \u2014 benchmarking is paused until its queue finishes.");
14487
15556
  await d.comfy?.freeComfyUIBeforeLoad();
14488
15557
  try {
14489
15558
  d.bench.start(key, b.base && typeof b.base === "object" ? b.base : void 0);
@@ -14498,12 +15567,12 @@ function registerApi(app2, d) {
14498
15567
  });
14499
15568
  app2.post("/api/v1/bench/save", (c) => {
14500
15569
  const saved = d.bench.saveResult();
14501
- if (!saved) return err2(c, 409, "no_bench_result", "No auto-tune result to save.");
15570
+ if (!saved) return err3(c, 409, "no_bench_result", "No auto-tune result to save.");
14502
15571
  return c.json({ ok: true });
14503
15572
  });
14504
15573
  app2.get("/api/v1/bench/log", (c) => {
14505
15574
  const log = d.bench.getLog();
14506
- if (!log) return err2(c, 404, "no_bench_log", "No auto-tune log available.");
15575
+ if (!log) return err3(c, 404, "no_bench_log", "No auto-tune log available.");
14507
15576
  const slug = log.modelKey.replace(/[^a-z0-9]/gi, "-").replace(/-+/g, "-").slice(0, 60);
14508
15577
  c.header("Content-Disposition", `attachment; filename="autotune-${slug}-${log.startedAt.slice(0, 10)}.json"`);
14509
15578
  return c.json(log);
@@ -14520,23 +15589,23 @@ function registerApi(app2, d) {
14520
15589
  app2.delete("/api/v1/models/:key", async (c) => {
14521
15590
  const key = decodeURIComponent(c.req.param("key"));
14522
15591
  const e = d.scanner.get(key);
14523
- if (!e) return err2(c, 404, "no_such_model", "No model with that key.");
15592
+ if (!e) return err3(c, 404, "no_such_model", "No model with that key.");
14524
15593
  const ms = d.manager.status();
14525
15594
  const loadedKey = ms.state === "running" || ms.state === "starting" ? ms.model?.key : void 0;
14526
15595
  if (loadedKey === e.path || loadedKey === e.key) {
14527
- return err2(c, 409, "model_loaded", "This model is currently loaded. Eject it before deleting.");
15596
+ return err3(c, 409, "model_loaded", "This model is currently loaded. Eject it before deleting.");
14528
15597
  }
14529
15598
  try {
14530
15599
  const deleted = await d.scanner.delete(key);
14531
15600
  return c.json({ ok: true, deleted });
14532
15601
  } catch (e2) {
14533
- if (e2 instanceof ScannerError) return err2(c, 404, e2.code, e2.message);
14534
- return err2(c, 500, "delete_failed", e2.message);
15602
+ if (e2 instanceof ScannerError) return err3(c, 404, e2.code, e2.message);
15603
+ return err3(c, 500, "delete_failed", e2.message);
14535
15604
  }
14536
15605
  });
14537
15606
  app2.get("/api/v1/models/:key", (c) => {
14538
15607
  const e = d.scanner.get(decodeURIComponent(c.req.param("key")));
14539
- if (!e) return err2(c, 404, "no_such_model", "No model with that key.");
15608
+ if (!e) return err3(c, 404, "no_such_model", "No model with that key.");
14540
15609
  const sys = getSysInfo();
14541
15610
  const snap = d.store.snapshot();
14542
15611
  const engineId = c.req.query("engine") || d.registry.active()?.id || "*";
@@ -14547,24 +15616,24 @@ function registerApi(app2, d) {
14547
15616
  app2.put("/api/v1/models/:key/profile", async (c) => {
14548
15617
  const key = decodeURIComponent(c.req.param("key"));
14549
15618
  const e = d.scanner.get(key);
14550
- if (!e) return err2(c, 404, "no_such_model", "No model with that key.");
14551
- const p = await body2(c);
15619
+ if (!e) return err3(c, 404, "no_such_model", "No model with that key.");
15620
+ const p = await body3(c);
14552
15621
  if (!p || typeof p.ctx !== "number" || p.ctx < 256) {
14553
- return err2(c, 400, "invalid_profile_value", "ctx must be at least 256.");
15622
+ return err3(c, 400, "invalid_profile_value", "ctx must be at least 256.");
14554
15623
  }
14555
15624
  if (p.gpu) {
14556
15625
  const g = p.gpu;
14557
15626
  if (!["layer", "row", "none"].includes(g.splitMode)) {
14558
- return err2(c, 400, "invalid_profile_value", "gpu.splitMode must be layer, row, or none.");
15627
+ return err3(c, 400, "invalid_profile_value", "gpu.splitMode must be layer, row, or none.");
14559
15628
  }
14560
15629
  if (!Array.isArray(g.tensorSplit) || g.tensorSplit.some((n) => typeof n !== "number" || !(n >= 0))) {
14561
- return err2(c, 400, "invalid_profile_value", "gpu.tensorSplit must be an array of non-negative numbers.");
15630
+ return err3(c, 400, "invalid_profile_value", "gpu.tensorSplit must be an array of non-negative numbers.");
14562
15631
  }
14563
15632
  if (!Number.isInteger(g.mainGpu) || g.mainGpu < -1) {
14564
- return err2(c, 400, "invalid_profile_value", "gpu.mainGpu must be an integer \u2265 -1.");
15633
+ return err3(c, 400, "invalid_profile_value", "gpu.mainGpu must be an integer \u2265 -1.");
14565
15634
  }
14566
15635
  if (!Number.isInteger(g.tensorParallelSize) || g.tensorParallelSize < 1) {
14567
- return err2(c, 400, "invalid_profile_value", "gpu.tensorParallelSize must be an integer \u2265 1.");
15636
+ return err3(c, 400, "invalid_profile_value", "gpu.tensorParallelSize must be an integer \u2265 1.");
14568
15637
  }
14569
15638
  }
14570
15639
  const engineId = c.req.query("engine") || d.registry.active()?.id || "*";
@@ -14584,20 +15653,20 @@ function registerApi(app2, d) {
14584
15653
  app2.get("/api/v1/sysinfo", (c) => c.json(getSysInfo()));
14585
15654
  app2.get("/api/v1/settings", (c) => c.json(settingsPayload(d)));
14586
15655
  app2.patch("/api/v1/settings", async (c) => {
14587
- const b = await body2(c);
15656
+ const b = await body3(c);
14588
15657
  const updates2 = {};
14589
15658
  if (b.idleTtlMinutes !== void 0) {
14590
15659
  const v = Number(b.idleTtlMinutes);
14591
- if (!Number.isFinite(v) || v < 0) return err2(c, 400, "invalid_config_value", "idleTtlMinutes must be a non-negative number.");
15660
+ if (!Number.isFinite(v) || v < 0) return err3(c, 400, "invalid_config_value", "idleTtlMinutes must be a non-negative number.");
14592
15661
  updates2.idleTtlMinutes = v;
14593
15662
  }
14594
15663
  if (b.port !== void 0) {
14595
15664
  const v = Number(b.port);
14596
- if (!Number.isInteger(v) || v < 1024 || v > 65535) return err2(c, 400, "invalid_config_value", "port must be 1024\u201365535.");
15665
+ if (!Number.isInteger(v) || v < 1024 || v > 65535) return err3(c, 400, "invalid_config_value", "port must be 1024\u201365535.");
14597
15666
  updates2.port = v;
14598
15667
  }
14599
15668
  if (b.theme !== void 0) {
14600
- if (!["system", "light", "dark"].includes(b.theme)) return err2(c, 400, "invalid_config_value", "theme must be system, light, or dark.");
15669
+ if (!["system", "light", "dark"].includes(b.theme)) return err3(c, 400, "invalid_config_value", "theme must be system, light, or dark.");
14601
15670
  updates2.theme = b.theme;
14602
15671
  }
14603
15672
  if (b.autoGenerateTitles !== void 0) updates2.autoGenerateTitles = !!b.autoGenerateTitles;
@@ -14608,7 +15677,7 @@ function registerApi(app2, d) {
14608
15677
  const v = Number(b.vramHeadroomMb);
14609
15678
  const valid = v === VRAM_HEADROOM_SPILL_MB || v >= VRAM_HEADROOM_MIN_MB && v <= VRAM_HEADROOM_MAX_MB;
14610
15679
  if (!Number.isFinite(v) || !valid) {
14611
- return err2(c, 400, "invalid_config_value", `vramHeadroomMb must be ${VRAM_HEADROOM_SPILL_MB} (allow VRAM spill) or ${VRAM_HEADROOM_MIN_MB}\u2013${VRAM_HEADROOM_MAX_MB}.`);
15680
+ return err3(c, 400, "invalid_config_value", `vramHeadroomMb must be ${VRAM_HEADROOM_SPILL_MB} (allow VRAM spill) or ${VRAM_HEADROOM_MIN_MB}\u2013${VRAM_HEADROOM_MAX_MB}.`);
14612
15681
  }
14613
15682
  vramHeadroomMb = Math.round(v);
14614
15683
  }
@@ -14617,7 +15686,7 @@ function registerApi(app2, d) {
14617
15686
  let telemetryLevel;
14618
15687
  if (b.telemetryLevel !== void 0) {
14619
15688
  if (!["off", "anon", "full"].includes(b.telemetryLevel)) {
14620
- return err2(c, 400, "invalid_config_value", "telemetryLevel must be off, anon, or full.");
15689
+ return err3(c, 400, "invalid_config_value", "telemetryLevel must be off, anon, or full.");
14621
15690
  }
14622
15691
  telemetryLevel = b.telemetryLevel;
14623
15692
  }
@@ -14626,22 +15695,22 @@ function registerApi(app2, d) {
14626
15695
  if (md) {
14627
15696
  if (md.ctx !== void 0) {
14628
15697
  const v = Number(md.ctx);
14629
- if (!Number.isFinite(v) || v < 256) return err2(c, 400, "invalid_config_value", "modelDefaults.ctx must be at least 256.");
15698
+ if (!Number.isFinite(v) || v < 256) return err3(c, 400, "invalid_config_value", "modelDefaults.ctx must be at least 256.");
14630
15699
  mdUpdates.ctx = Math.floor(v);
14631
15700
  }
14632
15701
  if (md.ngl !== void 0) {
14633
15702
  const v = Number(md.ngl);
14634
- if (!Number.isFinite(v) || v < 0 || v > 99) return err2(c, 400, "invalid_config_value", "modelDefaults.ngl must be 0\u201399.");
15703
+ if (!Number.isFinite(v) || v < 0 || v > 99) return err3(c, 400, "invalid_config_value", "modelDefaults.ngl must be 0\u201399.");
14635
15704
  mdUpdates.ngl = Math.floor(v);
14636
15705
  }
14637
15706
  if (md.imageMaxTokens !== void 0) {
14638
15707
  const v = Number(md.imageMaxTokens);
14639
- if (!Number.isFinite(v) || v < 0) return err2(c, 400, "invalid_config_value", "modelDefaults.imageMaxTokens must be a non-negative number.");
15708
+ if (!Number.isFinite(v) || v < 0) return err3(c, 400, "invalid_config_value", "modelDefaults.imageMaxTokens must be a non-negative number.");
14640
15709
  mdUpdates.imageMaxTokens = Math.floor(v);
14641
15710
  }
14642
15711
  if (md.maxTokens !== void 0) {
14643
15712
  const v = Number(md.maxTokens);
14644
- if (!Number.isFinite(v) || v < 0) return err2(c, 400, "invalid_config_value", "modelDefaults.maxTokens must be a non-negative number (0 = unlimited).");
15713
+ if (!Number.isFinite(v) || v < 0) return err3(c, 400, "invalid_config_value", "modelDefaults.maxTokens must be a non-negative number (0 = unlimited).");
14645
15714
  mdUpdates.maxTokens = Math.floor(v);
14646
15715
  }
14647
15716
  }
@@ -14651,7 +15720,7 @@ function registerApi(app2, d) {
14651
15720
  if (b.comfyui?.url !== void 0) {
14652
15721
  const u = b.comfyui.url.trim();
14653
15722
  if (u && !/^https?:\/\//i.test(u)) {
14654
- return err2(c, 400, "invalid_config_value", "comfyui.url must be an http(s):// origin (e.g. http://127.0.0.1:8188).");
15723
+ return err3(c, 400, "invalid_config_value", "comfyui.url must be an http(s):// origin (e.g. http://127.0.0.1:8188).");
14655
15724
  }
14656
15725
  cuUpdates.url = u;
14657
15726
  }
@@ -14659,18 +15728,18 @@ function registerApi(app2, d) {
14659
15728
  if (b.gateway?.autoSwap !== void 0) gwUpdates.autoSwap = !!b.gateway.autoSwap;
14660
15729
  if (b.gateway?.keepN !== void 0) {
14661
15730
  const v = Number(b.gateway.keepN);
14662
- if (!Number.isInteger(v) || v < 1 || v > 4) return err2(c, 400, "invalid_config_value", "gateway.keepN must be 1\u20134.");
15731
+ if (!Number.isInteger(v) || v < 1 || v > 4) return err3(c, 400, "invalid_config_value", "gateway.keepN must be 1\u20134.");
14663
15732
  gwUpdates.keepN = v;
14664
15733
  }
14665
15734
  let toolchainDirs;
14666
15735
  if (b.build?.toolchainDirs !== void 0) {
14667
15736
  if (!Array.isArray(b.build.toolchainDirs)) {
14668
- return err2(c, 400, "invalid_config_value", "build.toolchainDirs must be an array of paths.");
15737
+ return err3(c, 400, "invalid_config_value", "build.toolchainDirs must be an array of paths.");
14669
15738
  }
14670
15739
  const dirs = b.build.toolchainDirs.map((p) => String(p).trim()).filter(Boolean);
14671
15740
  for (const dir of dirs) {
14672
15741
  if (!/^([a-zA-Z]:[\\/]|[\\/])/.test(dir)) {
14673
- return err2(c, 400, "invalid_config_value", `build.toolchainDirs: "${dir}" must be an absolute path.`);
15742
+ return err3(c, 400, "invalid_config_value", `build.toolchainDirs: "${dir}" must be an absolute path.`);
14674
15743
  }
14675
15744
  }
14676
15745
  toolchainDirs = dirs;
@@ -14688,25 +15757,33 @@ function registerApi(app2, d) {
14688
15757
  ]) {
14689
15758
  const raw = b.code?.[key];
14690
15759
  if (raw === void 0) continue;
14691
- if (!Array.isArray(raw)) return err2(c, 400, "invalid_config_value", `code.${key} must be an array of filenames.`);
15760
+ if (!Array.isArray(raw)) return err3(c, 400, "invalid_config_value", `code.${key} must be an array of filenames.`);
14692
15761
  const list = raw.map((p) => String(p).trim()).filter(Boolean);
14693
- if (list.length > MAX_AGENTS_MD_CANDIDATES2) return err2(c, 400, "invalid_config_value", `code.${key}: at most ${MAX_AGENTS_MD_CANDIDATES2} candidates.`);
15762
+ if (list.length > MAX_AGENTS_MD_CANDIDATES2) return err3(c, 400, "invalid_config_value", `code.${key}: at most ${MAX_AGENTS_MD_CANDIDATES2} candidates.`);
14694
15763
  for (const entry of list) {
14695
15764
  if (/^([a-zA-Z]:[\\/]|[\\/])/.test(entry)) {
14696
- return err2(c, 400, "invalid_config_value", `code.${key}: "${entry}" must be a relative filename/path, not absolute.`);
15765
+ return err3(c, 400, "invalid_config_value", `code.${key}: "${entry}" must be a relative filename/path, not absolute.`);
14697
15766
  }
14698
15767
  }
14699
15768
  out(list);
14700
15769
  }
15770
+ let defaultAgent;
15771
+ if (b.code?.defaultAgent !== void 0) {
15772
+ const v = b.code.defaultAgent;
15773
+ if (v !== "turbollm" && v !== "pi" && v !== "claude" && v !== "opencode") {
15774
+ return err3(c, 400, "invalid_config_value", "code.defaultAgent must be one of: turbollm, pi, claude, opencode.");
15775
+ }
15776
+ defaultAgent = v;
15777
+ }
14701
15778
  let toolPolicies;
14702
15779
  if (b.toolPolicies !== void 0) {
14703
15780
  if (typeof b.toolPolicies !== "object" || b.toolPolicies === null || Array.isArray(b.toolPolicies)) {
14704
- return err2(c, 400, "invalid_config_value", "toolPolicies must be an object mapping tool name to ask, allow, or deny.");
15781
+ return err3(c, 400, "invalid_config_value", "toolPolicies must be an object mapping tool name to ask, allow, or deny.");
14705
15782
  }
14706
15783
  const validated = {};
14707
15784
  for (const [toolName, v] of Object.entries(b.toolPolicies)) {
14708
15785
  if (v !== "ask" && v !== "allow" && v !== "deny") {
14709
- return err2(c, 400, "invalid_config_value", `toolPolicies.${toolName} must be ask, allow, or deny.`);
15786
+ return err3(c, 400, "invalid_config_value", `toolPolicies.${toolName} must be ask, allow, or deny.`);
14710
15787
  }
14711
15788
  validated[toolName] = v;
14712
15789
  }
@@ -14721,6 +15798,7 @@ function registerApi(app2, d) {
14721
15798
  if (toolchainDirs !== void 0) cfg2.build.toolchainDirs = toolchainDirs;
14722
15799
  if (agentsMdProjectCandidates !== void 0) cfg2.code.agentsMdProjectCandidates = agentsMdProjectCandidates;
14723
15800
  if (agentsMdGlobalCandidates !== void 0) cfg2.code.agentsMdGlobalCandidates = agentsMdGlobalCandidates;
15801
+ if (defaultAgent !== void 0) cfg2.code.defaultAgent = defaultAgent;
14724
15802
  if (toolPolicies !== void 0) cfg2.tools.toolPolicies = toolPolicies;
14725
15803
  if (b.autoAllowAll !== void 0) cfg2.tools.autoAllowAll = !!b.autoAllowAll;
14726
15804
  if (b.autoLoadOnStart !== void 0) cfg2.autoLoadOnStart = !!b.autoLoadOnStart;
@@ -14763,12 +15841,12 @@ function registerApi(app2, d) {
14763
15841
  return c.json({ ...settingsPayload(d), rebind });
14764
15842
  });
14765
15843
  app2.post("/api/v1/mcp/servers", async (c) => {
14766
- const b = await body2(c);
15844
+ const b = await body3(c);
14767
15845
  const transport = b.transport === "sse" ? "sse" : "stdio";
14768
- if (!b.name?.trim()) return err2(c, 400, "invalid_config_value", "name is required.");
14769
- if (transport === "stdio" && !b.command?.trim()) return err2(c, 400, "invalid_config_value", "command is required for stdio transport.");
14770
- if (transport === "sse" && !b.url?.trim()) return err2(c, 400, "invalid_config_value", "url is required for sse transport.");
14771
- if (transport === "sse" && !/^https?:\/\//i.test(b.url ?? "")) return err2(c, 400, "invalid_config_value", "url must be an http(s):// address.");
15846
+ if (!b.name?.trim()) return err3(c, 400, "invalid_config_value", "name is required.");
15847
+ if (transport === "stdio" && !b.command?.trim()) return err3(c, 400, "invalid_config_value", "command is required for stdio transport.");
15848
+ if (transport === "sse" && !b.url?.trim()) return err3(c, 400, "invalid_config_value", "url is required for sse transport.");
15849
+ if (transport === "sse" && !/^https?:\/\//i.test(b.url ?? "")) return err3(c, 400, "invalid_config_value", "url must be an http(s):// address.");
14772
15850
  const { randomUUID: randomUUID11 } = await import("crypto");
14773
15851
  const server2 = {
14774
15852
  id: randomUUID11(),
@@ -14779,7 +15857,7 @@ function registerApi(app2, d) {
14779
15857
  };
14780
15858
  const isMemoryServer = server2.transport === "stdio" && (server2.command?.includes("server-memory") || server2.args?.some((a) => a.includes("server-memory")));
14781
15859
  if (isMemoryServer && !server2.env?.MEMORY_FILE_PATH) {
14782
- server2.env = { ...server2.env, MEMORY_FILE_PATH: join25(homedir4(), ".turbollm", "mcp-memory.jsonl") };
15860
+ server2.env = { ...server2.env, MEMORY_FILE_PATH: join26(homedir4(), ".turbollm", "mcp-memory.jsonl") };
14783
15861
  }
14784
15862
  d.store.update((cfg2) => {
14785
15863
  cfg2.mcp.servers.push(server2);
@@ -14794,11 +15872,11 @@ function registerApi(app2, d) {
14794
15872
  });
14795
15873
  app2.put("/api/v1/mcp/servers/:id", async (c) => {
14796
15874
  const id = c.req.param("id");
14797
- const b = await body2(c);
15875
+ const b = await body3(c);
14798
15876
  const cfg2 = d.store.snapshot();
14799
- if (!cfg2.mcp.servers.some((s) => s.id === id)) return err2(c, 404, "not_found", "MCP server not found.");
14800
- if (b.transport && b.transport !== "stdio" && b.transport !== "sse") return err2(c, 400, "invalid_config_value", "transport must be stdio or sse.");
14801
- if (b.url && !/^https?:\/\//i.test(b.url)) return err2(c, 400, "invalid_config_value", "url must be an http(s):// address.");
15877
+ if (!cfg2.mcp.servers.some((s) => s.id === id)) return err3(c, 404, "not_found", "MCP server not found.");
15878
+ if (b.transport && b.transport !== "stdio" && b.transport !== "sse") return err3(c, 400, "invalid_config_value", "transport must be stdio or sse.");
15879
+ if (b.url && !/^https?:\/\//i.test(b.url)) return err3(c, 400, "invalid_config_value", "url must be an http(s):// address.");
14802
15880
  d.store.update((c2) => {
14803
15881
  const s = c2.mcp.servers.find((x) => x.id === id);
14804
15882
  if (b.name !== void 0) s.name = b.name.trim();
@@ -14820,7 +15898,7 @@ function registerApi(app2, d) {
14820
15898
  app2.delete("/api/v1/mcp/servers/:id", async (c) => {
14821
15899
  const id = c.req.param("id");
14822
15900
  const cfg2 = d.store.snapshot();
14823
- if (!cfg2.mcp.servers.some((s) => s.id === id)) return err2(c, 404, "not_found", "MCP server not found.");
15901
+ if (!cfg2.mcp.servers.some((s) => s.id === id)) return err3(c, 404, "not_found", "MCP server not found.");
14824
15902
  d.store.update((c2) => {
14825
15903
  c2.mcp.servers = c2.mcp.servers.filter((s) => s.id !== id);
14826
15904
  });
@@ -14845,10 +15923,10 @@ function registerApi(app2, d) {
14845
15923
  });
14846
15924
  app2.get("/api/v1/modeldirs", (c) => c.json(modelDirsPayload(d)));
14847
15925
  app2.post("/api/v1/modeldirs", async (c) => {
14848
- const b = await body2(c);
15926
+ const b = await body3(c);
14849
15927
  const dir = (b.dir ?? "").trim();
14850
- if (!dir || !/^([a-zA-Z]:[\\/]|[\\/])/.test(dir)) return err2(c, 400, "invalid_config_value", "Path must be absolute.");
14851
- if (!existsSync22(dir)) return err2(c, 400, "invalid_config_value", "That folder does not exist.");
15928
+ if (!dir || !/^([a-zA-Z]:[\\/]|[\\/])/.test(dir)) return err3(c, 400, "invalid_config_value", "Path must be absolute.");
15929
+ if (!existsSync22(dir)) return err3(c, 400, "invalid_config_value", "That folder does not exist.");
14852
15930
  try {
14853
15931
  d.store.update((cfg2) => {
14854
15932
  if (!cfg2.modelDirs.includes(dir)) cfg2.modelDirs.push(dir);
@@ -14860,7 +15938,7 @@ function registerApi(app2, d) {
14860
15938
  return c.json(modelDirsPayload(d), 201);
14861
15939
  });
14862
15940
  app2.delete("/api/v1/modeldirs", async (c) => {
14863
- const b = await body2(c);
15941
+ const b = await body3(c);
14864
15942
  d.store.update((cfg2) => {
14865
15943
  cfg2.modelDirs = cfg2.modelDirs.filter((x) => x !== b.dir);
14866
15944
  if (cfg2.primaryModelDir === b.dir) cfg2.primaryModelDir = "";
@@ -14869,10 +15947,10 @@ function registerApi(app2, d) {
14869
15947
  return c.json(modelDirsPayload(d));
14870
15948
  });
14871
15949
  app2.post("/api/v1/modeldirs/primary", async (c) => {
14872
- const b = await body2(c);
15950
+ const b = await body3(c);
14873
15951
  const dir = (b.dir ?? "").trim();
14874
15952
  if (!dir || !d.store.snapshot().modelDirs.includes(dir)) {
14875
- return err2(c, 400, "invalid_config_value", "Primary folder must be one of your model folders.");
15953
+ return err3(c, 400, "invalid_config_value", "Primary folder must be one of your model folders.");
14876
15954
  }
14877
15955
  d.store.update((cfg2) => {
14878
15956
  cfg2.primaryModelDir = dir;
@@ -14931,7 +16009,7 @@ function registerApi(app2, d) {
14931
16009
  }
14932
16010
  });
14933
16011
  app2.post("/api/v1/hf/token/test", async (c) => {
14934
- const b = await body2(c);
16012
+ const b = await body3(c);
14935
16013
  try {
14936
16014
  return c.json(await d.hf.testToken(b.token ?? ""));
14937
16015
  } catch (e) {
@@ -14940,7 +16018,7 @@ function registerApi(app2, d) {
14940
16018
  });
14941
16019
  app2.get("/api/v1/downloads", (c) => c.json({ downloads: d.downloads.list() }));
14942
16020
  app2.post("/api/v1/downloads", async (c) => {
14943
- const b = await body2(c);
16021
+ const b = await body3(c);
14944
16022
  try {
14945
16023
  const recs = await d.downloads.enqueue(b);
14946
16024
  return c.json({ downloads: recs }, 202);
@@ -14950,24 +16028,24 @@ function registerApi(app2, d) {
14950
16028
  });
14951
16029
  app2.post("/api/v1/downloads/:id/cancel", (c) => {
14952
16030
  const ok = d.downloads.cancel(c.req.param("id"));
14953
- if (!ok) return err2(c, 404, "no_such_download", "No download with that id.");
16031
+ if (!ok) return err3(c, 404, "no_such_download", "No download with that id.");
14954
16032
  return c.json({ ok: true });
14955
16033
  });
14956
16034
  app2.post("/api/v1/downloads/:id/resume", (c) => {
14957
16035
  const ok = d.downloads.resume(c.req.param("id"));
14958
- if (!ok) return err2(c, 404, "no_such_download", "No paused or errored download with that id.");
16036
+ if (!ok) return err3(c, 404, "no_such_download", "No paused or errored download with that id.");
14959
16037
  return c.json({ ok: true });
14960
16038
  });
14961
16039
  app2.delete("/api/v1/downloads/:id", (c) => {
14962
16040
  const ok = d.downloads.remove(c.req.param("id"));
14963
- if (!ok) return err2(c, 404, "no_such_download", "No download with that id.");
16041
+ if (!ok) return err3(c, 404, "no_such_download", "No download with that id.");
14964
16042
  return c.json({ ok: true });
14965
16043
  });
14966
16044
  function keysHostGate(c) {
14967
16045
  return isLocalRequest(c, d) || d.store.snapshot().daemon.requireApiKey;
14968
16046
  }
14969
16047
  app2.get("/api/v1/keys", (c) => {
14970
- if (!keysHostGate(c)) return err2(c, 403, "forbidden", 'API keys are only visible from this machine until "Require an API key" is turned on.');
16048
+ if (!keysHostGate(c)) return err3(c, 403, "forbidden", 'API keys are only visible from this machine until "Require an API key" is turned on.');
14971
16049
  const keys = d.store.snapshot().apiKeys.map(({ id, name, prefix, createdAt, lastUsedAt }) => ({
14972
16050
  id,
14973
16051
  name,
@@ -14978,10 +16056,10 @@ function registerApi(app2, d) {
14978
16056
  return c.json({ keys });
14979
16057
  });
14980
16058
  app2.post("/api/v1/keys", async (c) => {
14981
- if (!keysHostGate(c)) return err2(c, 403, "forbidden", 'API keys can only be created from this machine until "Require an API key" is turned on.');
14982
- const b = await body2(c);
16059
+ if (!keysHostGate(c)) return err3(c, 403, "forbidden", 'API keys can only be created from this machine until "Require an API key" is turned on.');
16060
+ const b = await body3(c);
14983
16061
  const name = (b.name ?? "").trim();
14984
- if (!name) return err2(c, 400, "invalid_config_value", "name is required.");
16062
+ if (!name) return err3(c, 400, "invalid_config_value", "name is required.");
14985
16063
  const { full, hash, prefix } = generateApiKey();
14986
16064
  const key = {
14987
16065
  id: randomUUID7(),
@@ -14998,7 +16076,7 @@ function registerApi(app2, d) {
14998
16076
  );
14999
16077
  });
15000
16078
  app2.delete("/api/v1/keys/:id", (c) => {
15001
- if (!keysHostGate(c)) return err2(c, 403, "forbidden", 'API keys can only be revoked from this machine until "Require an API key" is turned on.');
16079
+ if (!keysHostGate(c)) return err3(c, 403, "forbidden", 'API keys can only be revoked from this machine until "Require an API key" is turned on.');
15002
16080
  const id = c.req.param("id");
15003
16081
  d.store.update((cfg2) => {
15004
16082
  cfg2.apiKeys = cfg2.apiKeys.filter((k) => k.id !== id);
@@ -15006,7 +16084,7 @@ function registerApi(app2, d) {
15006
16084
  return c.json({ ok: true });
15007
16085
  });
15008
16086
  app2.get("/api/v1/connect/:cli", (c) => {
15009
- if (!keysHostGate(c)) return err2(c, 403, "forbidden", 'Connect setup is only available from this machine until "Require an API key" is turned on.');
16087
+ if (!keysHostGate(c)) return err3(c, 403, "forbidden", 'Connect setup is only available from this machine until "Require an API key" is turned on.');
15010
16088
  const cli = c.req.param("cli");
15011
16089
  const cfg2 = d.store.snapshot();
15012
16090
  const { port: port2, lanBind } = cfg2.daemon;
@@ -15179,23 +16257,23 @@ function engineWorkBusy(d) {
15179
16257
  function hfErr(c, e) {
15180
16258
  if (e instanceof HfError) {
15181
16259
  const status = e.code === "hf_unauthorized" ? 401 : e.code === "hf_gated" ? 403 : e.code === "hf_not_found" ? 404 : 503;
15182
- return err2(c, status, e.code, e.message);
16260
+ return err3(c, status, e.code, e.message);
15183
16261
  }
15184
- return err2(c, 500, "internal", e.message);
16262
+ return err3(c, 500, "internal", e.message);
15185
16263
  }
15186
16264
  function dlErr(c, e) {
15187
16265
  if (e instanceof DownloadError) {
15188
16266
  const status = e.code === "no_model_dir" ? 409 : e.code === "hf_unauthorized" ? 401 : e.code === "hf_gated" ? 403 : 400;
15189
- return err2(c, status, e.code, e.message);
16267
+ return err3(c, status, e.code, e.message);
15190
16268
  }
15191
- return err2(c, 500, "internal", e.message);
16269
+ return err3(c, 500, "internal", e.message);
15192
16270
  }
15193
16271
  function benchError(c, e) {
15194
16272
  if (e instanceof BenchError) {
15195
16273
  const status = e.code === "bench_running" || e.code === "engine_in_use" || e.code === "no_active_engine" ? 409 : e.code === "no_such_model" ? 404 : 400;
15196
- return err2(c, status, e.code, e.message);
16274
+ return err3(c, status, e.code, e.message);
15197
16275
  }
15198
- return err2(c, 500, "internal", e.message);
16276
+ return err3(c, 500, "internal", e.message);
15199
16277
  }
15200
16278
  function engineBusy(d) {
15201
16279
  const s = d.manager.status().state;
@@ -15206,9 +16284,9 @@ function isCatalogRepo(repoUrl) {
15206
16284
  return catalogForPlatform().some((e) => sameRepo(repoUrl, e.homepage));
15207
16285
  }
15208
16286
  function regErr(c, e) {
15209
- if (e instanceof NotFoundError) return err2(c, 404, "engine_not_found", "No engine with that id.");
15210
- if (e instanceof ValueError) return err2(c, 400, "invalid_config_value", e.message);
15211
- return err2(c, 500, "internal", e.message);
16287
+ if (e instanceof NotFoundError) return err3(c, 404, "engine_not_found", "No engine with that id.");
16288
+ if (e instanceof ValueError) return err3(c, 400, "invalid_config_value", e.message);
16289
+ return err3(c, 500, "internal", e.message);
15212
16290
  }
15213
16291
  function engineInstallDir(eng, enginesRoot) {
15214
16292
  const norm = enginesRoot.replace(/[\\/]+$/, "");
@@ -15217,19 +16295,19 @@ function engineInstallDir(eng, enginesRoot) {
15217
16295
  return n.startsWith(norm + "/") || n.startsWith(norm + "\\");
15218
16296
  };
15219
16297
  if (eng.kind === "mlx") {
15220
- const d = join25(enginesRoot, "mlx", "venv");
16298
+ const d = join26(enginesRoot, "mlx", "venv");
15221
16299
  return inside(d) ? d : null;
15222
16300
  }
15223
16301
  if (eng.kind === "rapid-mlx") {
15224
- const d = join25(enginesRoot, "rapid-mlx", "venv");
16302
+ const d = join26(enginesRoot, "rapid-mlx", "venv");
15225
16303
  return inside(d) ? d : null;
15226
16304
  }
15227
16305
  if (eng.kind === "vllm") {
15228
- const d = join25(enginesRoot, "vllm", "venv");
16306
+ const d = join26(enginesRoot, "vllm", "venv");
15229
16307
  return inside(d) ? d : null;
15230
16308
  }
15231
16309
  if (/[\\/]engines[\\/]turboquant[\\/]/.test(eng.binPath)) {
15232
- const d = join25(enginesRoot, "turboquant");
16310
+ const d = join26(enginesRoot, "turboquant");
15233
16311
  return inside(d) ? d : null;
15234
16312
  }
15235
16313
  if (eng.kind === "koboldcpp") {
@@ -15244,7 +16322,7 @@ function engineInstallDir(eng, enginesRoot) {
15244
16322
  if (srcDir) return inside(srcDir) ? srcDir : null;
15245
16323
  const llamaMatch = eng.binPath.replace(/\\/g, "/").match(/\/(llama\.cpp-[^/]+)\//i);
15246
16324
  if (llamaMatch) {
15247
- const d = join25(enginesRoot, llamaMatch[1]);
16325
+ const d = join26(enginesRoot, llamaMatch[1]);
15248
16326
  return inside(d) ? d : null;
15249
16327
  }
15250
16328
  return null;
@@ -15262,7 +16340,7 @@ function cleanModelName(p) {
15262
16340
  function readTail2(path, n) {
15263
16341
  if (!path || !existsSync22(path)) return [];
15264
16342
  try {
15265
- const lines = readFileSync10(path, "utf8").replace(/[\r\n]+$/, "").split("\n").map((l) => l.replace(/\r$/, ""));
16343
+ const lines = readFileSync11(path, "utf8").replace(/[\r\n]+$/, "").split("\n").map((l) => l.replace(/\r$/, ""));
15266
16344
  return lines.length > n ? lines.slice(-n) : lines;
15267
16345
  } catch {
15268
16346
  return [];
@@ -15453,10 +16531,10 @@ function buildConnectSnippets(cli, base2, apiKey, modelName, modelKey = modelNam
15453
16531
 
15454
16532
  // src/chat/chat-agent-routes.ts
15455
16533
  import { randomUUID as randomUUID8 } from "crypto";
15456
- function err3(c, status, code, message) {
16534
+ function err4(c, status, code, message) {
15457
16535
  return c.json({ error: { code, message } }, status);
15458
16536
  }
15459
- async function body3(c) {
16537
+ async function body4(c) {
15460
16538
  try {
15461
16539
  return await c.req.json();
15462
16540
  } catch {
@@ -15471,8 +16549,8 @@ function registerChatAgentRoutes(app2, d) {
15471
16549
  app2.get("/api/v1/chat-agents/builtin-overrides", (c) => c.json(d.store.snapshot().builtinAgentOverrides));
15472
16550
  app2.put("/api/v1/chat-agents/builtin-overrides/:id", async (c) => {
15473
16551
  const id = c.req.param("id");
15474
- if (!id.trim()) return err3(c, 400, "invalid_config_value", "id is required.");
15475
- const b = await body3(c);
16552
+ if (!id.trim()) return err4(c, 400, "invalid_config_value", "id is required.");
16553
+ const b = await body4(c);
15476
16554
  const override = {};
15477
16555
  if (b.name !== void 0) override.name = b.name.trim();
15478
16556
  if (b.description !== void 0) override.description = b.description.trim();
@@ -15480,7 +16558,7 @@ function registerChatAgentRoutes(app2, d) {
15480
16558
  if (b.skillIds !== void 0) override.skillIds = sanitizeStringArray(b.skillIds);
15481
16559
  if (b.tools !== void 0) override.tools = sanitizeStringArray(b.tools);
15482
16560
  if (Object.keys(d.store.snapshot().builtinAgentOverrides).length >= AGENT_CAP && !(id in d.store.snapshot().builtinAgentOverrides)) {
15483
- return err3(c, 400, "too_many_agents", `Override limit reached (${AGENT_CAP}).`);
16561
+ return err4(c, 400, "too_many_agents", `Override limit reached (${AGENT_CAP}).`);
15484
16562
  }
15485
16563
  d.store.update((cfg2) => {
15486
16564
  cfg2.builtinAgentOverrides[id] = override;
@@ -15496,10 +16574,10 @@ function registerChatAgentRoutes(app2, d) {
15496
16574
  });
15497
16575
  app2.get("/api/v1/chat-agents", (c) => c.json(d.store.snapshot().customAgents));
15498
16576
  app2.post("/api/v1/chat-agents", async (c) => {
15499
- const b = await body3(c);
15500
- if (!b.name?.trim()) return err3(c, 400, "invalid_config_value", "name is required.");
16577
+ const b = await body4(c);
16578
+ if (!b.name?.trim()) return err4(c, 400, "invalid_config_value", "name is required.");
15501
16579
  if (d.store.snapshot().customAgents.length >= AGENT_CAP) {
15502
- return err3(c, 400, "too_many_agents", `Agent limit reached (${AGENT_CAP}).`);
16580
+ return err4(c, 400, "too_many_agents", `Agent limit reached (${AGENT_CAP}).`);
15503
16581
  }
15504
16582
  const agent = {
15505
16583
  id: randomUUID8(),
@@ -15517,10 +16595,10 @@ function registerChatAgentRoutes(app2, d) {
15517
16595
  app2.put("/api/v1/chat-agents/:id", async (c) => {
15518
16596
  const id = c.req.param("id");
15519
16597
  if (!d.store.snapshot().customAgents.some((a) => a.id === id)) {
15520
- return err3(c, 404, "not_found", "Agent not found.");
16598
+ return err4(c, 404, "not_found", "Agent not found.");
15521
16599
  }
15522
- const b = await body3(c);
15523
- if (b.name !== void 0 && !b.name.trim()) return err3(c, 400, "invalid_config_value", "name is required.");
16600
+ const b = await body4(c);
16601
+ if (b.name !== void 0 && !b.name.trim()) return err4(c, 400, "invalid_config_value", "name is required.");
15524
16602
  d.store.update((cfg2) => {
15525
16603
  const a = cfg2.customAgents.find((x) => x.id === id);
15526
16604
  if (b.name !== void 0) a.name = b.name.trim();
@@ -15534,7 +16612,7 @@ function registerChatAgentRoutes(app2, d) {
15534
16612
  app2.delete("/api/v1/chat-agents/:id", (c) => {
15535
16613
  const id = c.req.param("id");
15536
16614
  if (!d.store.snapshot().customAgents.some((a) => a.id === id)) {
15537
- return err3(c, 404, "not_found", "Agent not found.");
16615
+ return err4(c, 404, "not_found", "Agent not found.");
15538
16616
  }
15539
16617
  d.store.update((cfg2) => {
15540
16618
  cfg2.customAgents = cfg2.customAgents.filter((a) => a.id !== id);
@@ -15544,10 +16622,10 @@ function registerChatAgentRoutes(app2, d) {
15544
16622
  }
15545
16623
 
15546
16624
  // src/agents/agent-routes.ts
15547
- function err4(c, status, code, message) {
16625
+ function err5(c, status, code, message) {
15548
16626
  return c.json({ error: { code, message } }, status);
15549
16627
  }
15550
- async function body4(c) {
16628
+ async function body5(c) {
15551
16629
  try {
15552
16630
  return await c.req.json();
15553
16631
  } catch {
@@ -15559,14 +16637,14 @@ function registerAgentRoutes(app2, d) {
15559
16637
  const SKILL_CAP = 200;
15560
16638
  app2.get("/api/v1/skills", (c) => c.json(skills().list()));
15561
16639
  app2.post("/api/v1/skills", async (c) => {
15562
- if (!isLocalOrAuthenticated(c, d)) return err4(c, 403, "forbidden", "Skills can only be authored on the machine running TurboLLM.");
15563
- const b = await body4(c);
15564
- if (!b.id?.trim() || !/^[a-z0-9-]+$/.test(b.id)) return err4(c, 400, "invalid_config_value", "id must be kebab-case (a-z0-9-).");
15565
- if (!b.name?.trim()) return err4(c, 400, "invalid_config_value", "name is required.");
15566
- if (isBuiltinSkill(b.id)) return err4(c, 400, "builtin_skill", "Cannot overwrite a built-in skill.");
15567
- if (!b.instructions?.trim()) return err4(c, 400, "invalid_config_value", "instructions are required.");
16640
+ if (!isLocalOrAuthenticated(c, d)) return err5(c, 403, "forbidden", "Skills can only be authored on the machine running TurboLLM.");
16641
+ const b = await body5(c);
16642
+ if (!b.id?.trim() || !/^[a-z0-9-]+$/.test(b.id)) return err5(c, 400, "invalid_config_value", "id must be kebab-case (a-z0-9-).");
16643
+ if (!b.name?.trim()) return err5(c, 400, "invalid_config_value", "name is required.");
16644
+ if (isBuiltinSkill(b.id)) return err5(c, 400, "builtin_skill", "Cannot overwrite a built-in skill.");
16645
+ if (!b.instructions?.trim()) return err5(c, 400, "invalid_config_value", "instructions are required.");
15568
16646
  const lib = skills().list();
15569
- if (!lib.some((s) => s.id === b.id) && lib.length >= SKILL_CAP) return err4(c, 400, "too_many_skills", `Skill limit reached (${SKILL_CAP}).`);
16647
+ if (!lib.some((s) => s.id === b.id) && lib.length >= SKILL_CAP) return err5(c, 400, "too_many_skills", `Skill limit reached (${SKILL_CAP}).`);
15570
16648
  const skill = {
15571
16649
  id: b.id.trim(),
15572
16650
  name: b.name.trim(),
@@ -15578,37 +16656,37 @@ function registerAgentRoutes(app2, d) {
15578
16656
  return c.json(skill, 201);
15579
16657
  });
15580
16658
  app2.delete("/api/v1/skills/:id", (c) => {
15581
- if (!isLocalOrAuthenticated(c, d)) return err4(c, 403, "forbidden", "Skills can only be modified on the machine running TurboLLM.");
16659
+ if (!isLocalOrAuthenticated(c, d)) return err5(c, 403, "forbidden", "Skills can only be modified on the machine running TurboLLM.");
15582
16660
  const id = c.req.param("id");
15583
- if (!isValidSkillId(id)) return err4(c, 400, "invalid_config_value", "invalid skill id.");
15584
- if (isBuiltinSkill(id)) return err4(c, 400, "builtin_skill", "Built-in skills cannot be deleted.");
16661
+ if (!isValidSkillId(id)) return err5(c, 400, "invalid_config_value", "invalid skill id.");
16662
+ if (isBuiltinSkill(id)) return err5(c, 400, "builtin_skill", "Built-in skills cannot be deleted.");
15585
16663
  skills().delete(id);
15586
16664
  return c.json({ ok: true });
15587
16665
  });
15588
16666
  app2.post("/api/v1/skills/import", async (c) => {
15589
- if (!isLocalOrAuthenticated(c, d)) return err4(c, 403, "forbidden", "Skills can only be authored on the machine running TurboLLM.");
15590
- const b = await body4(c);
16667
+ if (!isLocalOrAuthenticated(c, d)) return err5(c, 403, "forbidden", "Skills can only be authored on the machine running TurboLLM.");
16668
+ const b = await body5(c);
15591
16669
  const text = b.text?.trim();
15592
- if (!text) return err4(c, 400, "invalid_input", "text is required.");
16670
+ if (!text) return err5(c, 400, "invalid_input", "text is required.");
15593
16671
  const draft = fromSkillMd("draft", text);
15594
- if (!draft?.name) return err4(c, 400, "invalid_input", "Could not parse a SKILL.md \u2014 expected YAML frontmatter (name/description/tools) followed by an instructions body.");
16672
+ if (!draft?.name) return err5(c, 400, "invalid_input", "Could not parse a SKILL.md \u2014 expected YAML frontmatter (name/description/tools) followed by an instructions body.");
15595
16673
  const id = toSkillId(draft.name);
15596
- if (!id) return err4(c, 400, "invalid_input", "Could not derive a valid id from the skill name.");
15597
- if (isBuiltinSkill(id)) return err4(c, 400, "builtin_skill", "Cannot overwrite a built-in skill.");
16674
+ if (!id) return err5(c, 400, "invalid_input", "Could not derive a valid id from the skill name.");
16675
+ if (isBuiltinSkill(id)) return err5(c, 400, "builtin_skill", "Cannot overwrite a built-in skill.");
15598
16676
  const store2 = skills();
15599
16677
  const lib = store2.list();
15600
- if (!lib.some((s) => s.id === id) && lib.length >= SKILL_CAP) return err4(c, 400, "too_many_skills", `Skill limit reached (${SKILL_CAP}).`);
16678
+ if (!lib.some((s) => s.id === id) && lib.length >= SKILL_CAP) return err5(c, 400, "too_many_skills", `Skill limit reached (${SKILL_CAP}).`);
15601
16679
  const skill = { id, name: draft.name, description: draft.description, instructions: draft.instructions, tools: draft.tools };
15602
16680
  store2.write(skill);
15603
16681
  return c.json(skill, 201);
15604
16682
  });
15605
16683
  app2.post("/api/v1/skills/learn-folder", async (c) => {
15606
- if (!isLocalOrAuthenticated(c, d)) return err4(c, 403, "forbidden", "Local host only.");
15607
- const b = await body4(c);
16684
+ if (!isLocalOrAuthenticated(c, d)) return err5(c, 403, "forbidden", "Local host only.");
16685
+ const b = await body5(c);
15608
16686
  const folder = b.folder?.trim();
15609
- if (!folder) return err4(c, 400, "invalid_input", "folder is required.");
16687
+ if (!folder) return err5(c, 400, "invalid_input", "folder is required.");
15610
16688
  const store2 = skills();
15611
- if (store2.userSkills().length >= SKILL_CAP) return err4(c, 400, "skill_cap", `Skill limit reached (${SKILL_CAP}).`);
16689
+ if (store2.userSkills().length >= SKILL_CAP) return err5(c, 400, "skill_cap", `Skill limit reached (${SKILL_CAP}).`);
15612
16690
  const taskId = d.agentTasks?.start("skill_from_folder", "skills", `Learning from ${folder}`);
15613
16691
  void (async () => {
15614
16692
  try {
@@ -15629,7 +16707,7 @@ function registerAgentRoutes(app2, d) {
15629
16707
  return;
15630
16708
  }
15631
16709
  if (taskId) d.agentTasks?.step(taskId, "No skill files found \u2014 distilling from the folder\u2026");
15632
- const { distillSkillsFromFolder } = await import("./distiller-MRW6VEYO.js");
16710
+ const { distillSkillsFromFolder } = await import("./distiller-6E6IOZ5G.js");
15633
16711
  const distilled = await distillSkillsFromFolder(d, folder, {
15634
16712
  max: Math.max(0, SKILL_CAP - store2.userSkills().length),
15635
16713
  onProgress: (done, total, file) => {
@@ -15663,10 +16741,10 @@ function registerAgentRoutes(app2, d) {
15663
16741
  import { streamSSE as streamSSE3 } from "hono/streaming";
15664
16742
 
15665
16743
  // src/code/code-run-manager.ts
15666
- import { EventEmitter } from "events";
16744
+ import { EventEmitter as EventEmitter2 } from "events";
15667
16745
 
15668
16746
  // src/code/code-session.ts
15669
- import { join as join28, resolve as resolve3 } from "path";
16747
+ import { join as join29, resolve as resolve3 } from "path";
15670
16748
  import { readFile as readFile2 } from "fs/promises";
15671
16749
  import { Type, Unsafe } from "typebox";
15672
16750
  import {
@@ -15734,11 +16812,11 @@ function waitForChildProcess(child) {
15734
16812
  stderrEnded = true;
15735
16813
  maybeFinalizeAfterExit();
15736
16814
  };
15737
- const onError = (err6) => {
16815
+ const onError = (err7) => {
15738
16816
  if (settled) return;
15739
16817
  settled = true;
15740
16818
  cleanup();
15741
- reject(err6);
16819
+ reject(err7);
15742
16820
  };
15743
16821
  const onExit = (code) => {
15744
16822
  exited = true;
@@ -15895,7 +16973,7 @@ Cannot execute bash commands.`);
15895
16973
 
15896
16974
  // src/code/containment.ts
15897
16975
  import { realpathSync as realpathSync2 } from "fs";
15898
- import { resolve as resolve2, dirname as dirname10, basename as basename4, join as join26, sep } from "path";
16976
+ import { resolve as resolve2, dirname as dirname10, basename as basename4, join as join27, sep } from "path";
15899
16977
  var isWin = process.platform === "win32";
15900
16978
  function canonicalize(input) {
15901
16979
  if (typeof input !== "string" || input.length === 0) return null;
@@ -15911,7 +16989,7 @@ function canonicalize(input) {
15911
16989
  for (let i = 0; i < 4096; i++) {
15912
16990
  try {
15913
16991
  const real = realpathSync2(cur);
15914
- const full = tail.length ? join26(real, ...[...tail].reverse()) : real;
16992
+ const full = tail.length ? join27(real, ...[...tail].reverse()) : real;
15915
16993
  return normForCompare(full);
15916
16994
  } catch {
15917
16995
  const parent = dirname10(cur);
@@ -15954,8 +17032,8 @@ function isContainedFromRoot(input, root) {
15954
17032
  }
15955
17033
 
15956
17034
  // src/code/persona.ts
15957
- import { readFileSync as readFileSync11 } from "fs";
15958
- import { join as join27 } from "path";
17035
+ import { readFileSync as readFileSync12 } from "fs";
17036
+ import { join as join28 } from "path";
15959
17037
  var MAX_SKILL_CATALOG_CHARS = 16e3;
15960
17038
  var MAX_AGENTS_MD_CHARS = 2e4;
15961
17039
  var MAX_AGENTS_MD_CANDIDATES = 8;
@@ -16073,7 +17151,7 @@ function skillCatalogBlock(skills) {
16073
17151
  }
16074
17152
  function readAgentsFile(path) {
16075
17153
  try {
16076
- const text = readFileSync11(path, "utf8").trim();
17154
+ const text = readFileSync12(path, "utf8").trim();
16077
17155
  if (!text) return null;
16078
17156
  return text.length > MAX_AGENTS_MD_CHARS ? text.slice(0, MAX_AGENTS_MD_CHARS) : text;
16079
17157
  } catch {
@@ -16083,7 +17161,7 @@ function readAgentsFile(path) {
16083
17161
  function resolveAgentsFile(root, candidates) {
16084
17162
  for (const entry of candidates.slice(0, MAX_AGENTS_MD_CANDIDATES)) {
16085
17163
  if (!isContainedFromRoot(entry, root)) continue;
16086
- const text = readAgentsFile(join27(root, entry));
17164
+ const text = readAgentsFile(join28(root, entry));
16087
17165
  if (text !== null) return { name: entry, text };
16088
17166
  }
16089
17167
  return null;
@@ -16397,6 +17475,7 @@ function resolveEffectiveHistory(d, convId, sessionId) {
16397
17475
  }
16398
17476
  var PI_DEFAULT_RESERVE_TOKENS = 16384;
16399
17477
  var PI_DEFAULT_KEEP_RECENT_TOKENS = 2e4;
17478
+ var PI_MAX_OUTPUT_TOKENS = 8192;
16400
17479
  function compactionSettingsFor(contextWindow) {
16401
17480
  const reserveTokens = Math.max(512, Math.min(PI_DEFAULT_RESERVE_TOKENS, Math.round(contextWindow * 0.15)));
16402
17481
  const keepRecentTokens = Math.max(1024, Math.min(PI_DEFAULT_KEEP_RECENT_TOKENS, Math.round(contextWindow * 0.35)));
@@ -16613,7 +17692,7 @@ async function runCodeSession(params) {
16613
17692
  console.warn(`[code-session] history seed for session ${sessionId} produced ${sessionManager.getEntries().length}/${expectedEntries} expected entries \u2014 falling back to a plain-text history prefix on this turn's prompt`);
16614
17693
  task = `${HISTORY_FALLBACK_PREFIX}${renderHistoryFallbackText(summaryText, priorMessages)}${HISTORY_FALLBACK_SUFFIX}${rawTask}`;
16615
17694
  }
16616
- const agentDir = join28(d.store.dir(), "pi-agent");
17695
+ const agentDir = join29(d.store.dir(), "pi-agent");
16617
17696
  modelRegistry.registerProvider("local", {
16618
17697
  baseUrl: `${target}/v1`,
16619
17698
  apiKey: "agent-key",
@@ -16627,7 +17706,7 @@ async function runCodeSession(params) {
16627
17706
  input: ["text"],
16628
17707
  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
16629
17708
  contextWindow,
16630
- maxTokens: 8192,
17709
+ maxTokens: PI_MAX_OUTPUT_TOKENS,
16631
17710
  // qwen-chat-template thinking control matches how TurboLLM drives local chat models.
16632
17711
  compat: { supportsDeveloperRole: false, thinkingFormat: "qwen-chat-template" }
16633
17712
  }
@@ -16672,7 +17751,7 @@ async function runCodeSession(params) {
16672
17751
  input: ["text"],
16673
17752
  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
16674
17753
  contextWindow,
16675
- maxTokens: 8192,
17754
+ maxTokens: PI_MAX_OUTPUT_TOKENS,
16676
17755
  compat: { supportsDeveloperRole: false, thinkingFormat: "qwen-chat-template" }
16677
17756
  }]
16678
17757
  });
@@ -16744,7 +17823,7 @@ async function runCodeSession(params) {
16744
17823
  await sink({ event: "tool_call", data });
16745
17824
  });
16746
17825
  };
16747
- const skillAgentDir = join28(d.store.dir(), "pi-agent");
17826
+ const skillAgentDir = join29(d.store.dir(), "pi-agent");
16748
17827
  const skillResourceLoader = new DefaultResourceLoader({
16749
17828
  cwd: repoRoot,
16750
17829
  agentDir: skillAgentDir,
@@ -16806,7 +17885,7 @@ async function runCodeSession(params) {
16806
17885
  input: ["text"],
16807
17886
  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
16808
17887
  contextWindow,
16809
- maxTokens: 8192,
17888
+ maxTokens: PI_MAX_OUTPUT_TOKENS,
16810
17889
  compat: { supportsDeveloperRole: false, thinkingFormat: "qwen-chat-template" }
16811
17890
  }]
16812
17891
  });
@@ -16887,7 +17966,7 @@ async function runCodeSession(params) {
16887
17966
  await sink({ event: "tool_call", data });
16888
17967
  });
16889
17968
  };
16890
- const subAgentDir = join28(d.store.dir(), "pi-agent");
17969
+ const subAgentDir = join29(d.store.dir(), "pi-agent");
16891
17970
  const subResourceLoader = new DefaultResourceLoader({
16892
17971
  cwd: repoRoot,
16893
17972
  agentDir: subAgentDir,
@@ -17426,7 +18505,7 @@ async function compactCodeSession(params) {
17426
18505
  input: ["text"],
17427
18506
  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
17428
18507
  contextWindow,
17429
- maxTokens: 8192,
18508
+ maxTokens: PI_MAX_OUTPUT_TOKENS,
17430
18509
  compat: { supportsDeveloperRole: false, thinkingFormat: "qwen-chat-template" }
17431
18510
  }]
17432
18511
  });
@@ -17445,7 +18524,7 @@ async function compactCodeSession(params) {
17445
18524
  return payload;
17446
18525
  });
17447
18526
  };
17448
- const compactAgentDir = join28(d.store.dir(), "pi-agent");
18527
+ const compactAgentDir = join29(d.store.dir(), "pi-agent");
17449
18528
  const compactResourceLoader = new DefaultResourceLoader({
17450
18529
  cwd: repoRoot,
17451
18530
  agentDir: compactAgentDir,
@@ -17529,7 +18608,7 @@ async function lookbackPreCompactionHistory(params) {
17529
18608
  input: ["text"],
17530
18609
  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
17531
18610
  contextWindow,
17532
- maxTokens: 8192,
18611
+ maxTokens: PI_MAX_OUTPUT_TOKENS,
17533
18612
  compat: { supportsDeveloperRole: false, thinkingFormat: "qwen-chat-template" }
17534
18613
  }]
17535
18614
  });
@@ -17547,7 +18626,7 @@ async function lookbackPreCompactionHistory(params) {
17547
18626
  return payload;
17548
18627
  });
17549
18628
  };
17550
- const lookbackAgentDir = join28(d.store.dir(), "pi-agent");
18629
+ const lookbackAgentDir = join29(d.store.dir(), "pi-agent");
17551
18630
  const lookbackResourceLoader = new DefaultResourceLoader({
17552
18631
  cwd: repoRoot,
17553
18632
  agentDir: lookbackAgentDir,
@@ -17659,7 +18738,7 @@ var CodeRunManager = class {
17659
18738
  ensure(sessionId, convId, repoRoot) {
17660
18739
  let s = this.sessions.get(sessionId);
17661
18740
  if (!s) {
17662
- const emitter = new EventEmitter();
18741
+ const emitter = new EventEmitter2();
17663
18742
  emitter.setMaxListeners(0);
17664
18743
  s = { convId, repoRoot, buffer: new RingBuffer(), emitter, queue: [], active: null, replayFloor: 0, todos: [] };
17665
18744
  this.sessions.set(sessionId, s);
@@ -17932,7 +19011,7 @@ var CodeRunManager = class {
17932
19011
  */
17933
19012
  subscribe(sessionId, fromSeq) {
17934
19013
  const s = this.sessions.get(sessionId);
17935
- if (!s) return subscribeToBuffer(new RingBuffer(), new EventEmitter(), { fromSeq: 0, replayFloor: 0, idleAtStart: true });
19014
+ if (!s) return subscribeToBuffer(new RingBuffer(), new EventEmitter2(), { fromSeq: 0, replayFloor: 0, idleAtStart: true });
17936
19015
  return subscribeToBuffer(s.buffer, s.emitter, {
17937
19016
  fromSeq,
17938
19017
  replayFloor: s.replayFloor,
@@ -17994,7 +19073,7 @@ function subscribeToBuffer(buffer, emitter, opts) {
17994
19073
  }
17995
19074
 
17996
19075
  // src/code/revert.ts
17997
- import { existsSync as existsSync23, readFileSync as readFileSync12, writeFileSync as writeFileSync12 } from "fs";
19076
+ import { existsSync as existsSync23, readFileSync as readFileSync13, writeFileSync as writeFileSync13 } from "fs";
17998
19077
  import { resolve as resolve4 } from "path";
17999
19078
  import { applyPatch, parsePatch, reversePatch } from "diff";
18000
19079
  function revertFileEdits(messages, repoRoot) {
@@ -18023,7 +19102,7 @@ function revertFileEdits(messages, repoRoot) {
18023
19102
  }
18024
19103
  let content;
18025
19104
  try {
18026
- content = readFileSync12(absPath, "utf8");
19105
+ content = readFileSync13(absPath, "utf8");
18027
19106
  } catch {
18028
19107
  failed.push(path);
18029
19108
  continue;
@@ -18052,7 +19131,7 @@ function revertFileEdits(messages, repoRoot) {
18052
19131
  continue;
18053
19132
  }
18054
19133
  try {
18055
- writeFileSync12(absPath, content);
19134
+ writeFileSync13(absPath, content);
18056
19135
  reverted.push(path);
18057
19136
  } catch {
18058
19137
  failed.push(path);
@@ -18070,9 +19149,9 @@ function truncateForExport(text, limit = EXPORT_TRUNCATE_LIMIT) {
18070
19149
 
18071
19150
  \u2026 [truncated \u2014 ${cut.toLocaleString()} more character${cut === 1 ? "" : "s"}] \u2026`;
18072
19151
  }
18073
- function fence(lang, body6) {
19152
+ function fence(lang, body7) {
18074
19153
  return `\`\`\`${lang}
18075
- ${body6}
19154
+ ${body7}
18076
19155
  \`\`\``;
18077
19156
  }
18078
19157
  function renderReasoning(reasoning) {
@@ -18152,14 +19231,14 @@ function serializeCodeSessionMarkdown(run, conv, opts = {}) {
18152
19231
  `- **Exported:** ${exportedAt}`
18153
19232
  ].filter((line) => !!line);
18154
19233
  const messages = (conv.messages ?? []).slice().sort((a, b) => a.seq - b.seq);
18155
- const body6 = messages.length ? messages.map(renderMessage).join("\n\n---\n\n") : "*(no messages in this session yet)*";
19234
+ const body7 = messages.length ? messages.map(renderMessage).join("\n\n---\n\n") : "*(no messages in this session yet)*";
18156
19235
  return `# ${title}
18157
19236
 
18158
19237
  ${meta.join("\n")}
18159
19238
 
18160
19239
  ---
18161
19240
 
18162
- ${body6}
19241
+ ${body7}
18163
19242
  `;
18164
19243
  }
18165
19244
  var MAX_FILENAME_SLUG = 80;
@@ -18342,21 +19421,21 @@ async function runShellCommand(command, repoRoot, sessionLabel, opts) {
18342
19421
  }
18343
19422
  }
18344
19423
  function shellContextText(result) {
18345
- const body6 = result.output.trim() || "(no output)";
19424
+ const body7 = result.output.trim() || "(no output)";
18346
19425
  const status = result.timedOut ? ` (timed out after ${SHELL_TIMEOUT_SEC}s)` : result.exitCode !== null && result.exitCode !== 0 ? ` (exit code ${result.exitCode})` : "";
18347
19426
  const trunc = result.truncated ? "\n\u2026(output truncated)" : "";
18348
19427
  return `I ran a shell command in the repo${status}:
18349
19428
 
18350
19429
  $ ${result.command}
18351
19430
 
18352
- ${body6}${trunc}`;
19431
+ ${body7}${trunc}`;
18353
19432
  }
18354
19433
 
18355
19434
  // src/code/code-routes.ts
18356
- function err5(c, s, code, msg2) {
19435
+ function err6(c, s, code, msg2) {
18357
19436
  return c.json({ error: { code, message: msg2 } }, s);
18358
19437
  }
18359
- async function body5(c) {
19438
+ async function body6(c) {
18360
19439
  try {
18361
19440
  return await c.req.json();
18362
19441
  } catch {
@@ -18420,6 +19499,7 @@ function toSidebarRow(run) {
18420
19499
  // filled from CodeRunManager below when available
18421
19500
  createdAt: run.createdAt,
18422
19501
  repoRoot: run.repoRoot ?? "",
19502
+ codeAgent: run.codeAgent ?? "turbollm",
18423
19503
  error: run.error,
18424
19504
  archivedAt: run.archivedAt,
18425
19505
  clearedUpToMessageId: run.clearedUpToMessageId,
@@ -18431,15 +19511,16 @@ function registerCodeRoutes(app2, d) {
18431
19511
  const runs = new CodeRunManager(d);
18432
19512
  runs.reconcileOnStartup();
18433
19513
  app2.post("/api/v1/code/sessions", async (c) => {
18434
- const b = await body5(c);
19514
+ const b = await body6(c);
18435
19515
  const repoRoot = (b.repoRoot ?? "").trim();
18436
19516
  const task = (b.task ?? "").trim();
18437
19517
  const mode = b.mode ?? "auto";
18438
- if (!repoRoot) return err5(c, 400, "invalid_input", "repoRoot is required.");
18439
- if (!task) return err5(c, 400, "invalid_input", "A task description is required.");
18440
- if (!VALID_MODES.has(mode)) return err5(c, 400, "invalid_input", "mode must be one of: auto, plan, ask.");
19518
+ if (!repoRoot) return err6(c, 400, "invalid_input", "repoRoot is required.");
19519
+ if (!task) return err6(c, 400, "invalid_input", "A task description is required.");
19520
+ if (!VALID_MODES.has(mode)) return err6(c, 400, "invalid_input", "mode must be one of: auto, plan, ask.");
18441
19521
  const conv = db2.createConversation({ kind: "code", modelKey: b.modelKey });
18442
19522
  db2.setConversationMode(conv.id, mode);
19523
+ const codeAgent = d.store.snapshot().code.defaultAgent;
18443
19524
  const run = db2.createAgentRun({
18444
19525
  convId: conv.id,
18445
19526
  title: task.slice(0, 60),
@@ -18450,8 +19531,9 @@ function registerCodeRoutes(app2, d) {
18450
19531
  // captured; NOT acted on in Phase 1 (fast-follow)
18451
19532
  worktreeBranch: b.worktreeBranch,
18452
19533
  // captured; NOT acted on in Phase 1
18453
- worktreeBase: b.worktreeBase
19534
+ worktreeBase: b.worktreeBase,
18454
19535
  // captured; NOT acted on in Phase 1
19536
+ codeAgent
18455
19537
  });
18456
19538
  db2.addMessage(conv.id, "user", task, { textAttachments: b.contextFiles });
18457
19539
  return c.json({ sessionId: run.id, convId: conv.id }, 201);
@@ -18480,9 +19562,9 @@ function registerCodeRoutes(app2, d) {
18480
19562
  });
18481
19563
  app2.get("/api/v1/code/sessions/:id", (c) => {
18482
19564
  const run = db2.getAgentRun(c.req.param("id"));
18483
- if (!run) return err5(c, 404, "not_found", "Session not found.");
19565
+ if (!run) return err6(c, 404, "not_found", "Session not found.");
18484
19566
  const conv = db2.getConversation(run.convId, true);
18485
- if (!conv) return err5(c, 404, "not_found", "Session conversation not found.");
19567
+ if (!conv) return err6(c, 404, "not_found", "Session conversation not found.");
18486
19568
  return c.json({
18487
19569
  session: { ...toSidebarRow(run), mode: conv.agentMode },
18488
19570
  conversation: conv,
@@ -18512,11 +19594,11 @@ function registerCodeRoutes(app2, d) {
18512
19594
  app2.get("/api/v1/code/sessions/:id/export", (c) => {
18513
19595
  const id = c.req.param("id");
18514
19596
  const run = db2.getAgentRun(id);
18515
- if (!run) return err5(c, 404, "not_found", "Session not found.");
19597
+ if (!run) return err6(c, 404, "not_found", "Session not found.");
18516
19598
  const conv = db2.getConversation(run.convId, true);
18517
- if (!conv) return err5(c, 404, "not_found", "Session conversation not found.");
19599
+ if (!conv) return err6(c, 404, "not_found", "Session conversation not found.");
18518
19600
  const format = c.req.query("format") === "html" ? "html" : "markdown";
18519
- if (format === "html") return err5(c, 400, "unsupported_format", "HTML export is not available yet \u2014 use format=markdown.");
19601
+ if (format === "html") return err6(c, 400, "unsupported_format", "HTML export is not available yet \u2014 use format=markdown.");
18520
19602
  const md = serializeCodeSessionMarkdown(run, conv);
18521
19603
  const filename = codeSessionExportFilename(run.title, run.createdAt, "md");
18522
19604
  c.header("Content-Disposition", `attachment; filename="${filename}"`);
@@ -18526,38 +19608,53 @@ function registerCodeRoutes(app2, d) {
18526
19608
  app2.patch("/api/v1/code/sessions/:id/mode", async (c) => {
18527
19609
  const id = c.req.param("id");
18528
19610
  const run = db2.getAgentRun(id);
18529
- if (!run) return err5(c, 404, "not_found", "Session not found.");
18530
- const b = await body5(c);
19611
+ if (!run) return err6(c, 404, "not_found", "Session not found.");
19612
+ const b = await body6(c);
18531
19613
  const mode = b.mode ?? "";
18532
- if (!VALID_MODES.has(mode)) return err5(c, 400, "invalid_input", "mode must be one of: auto, plan, ask.");
19614
+ if (!VALID_MODES.has(mode)) return err6(c, 400, "invalid_input", "mode must be one of: auto, plan, ask.");
18533
19615
  db2.setConversationMode(run.convId, mode);
18534
19616
  return c.json({ ok: true, mode });
18535
19617
  });
19618
+ app2.patch("/api/v1/code/sessions/:id/thinking-budget", async (c) => {
19619
+ const id = c.req.param("id");
19620
+ const run = db2.getAgentRun(id);
19621
+ if (!run) return err6(c, 404, "not_found", "Session not found.");
19622
+ const b = await body6(c);
19623
+ const tokens = typeof b.tokens === "number" && Number.isFinite(b.tokens) ? Math.floor(b.tokens) : -1;
19624
+ sessionAuth.setThinkingBudget(run.id, tokens === -1 ? null : tokens);
19625
+ return c.json({ ok: true, tokens });
19626
+ });
19627
+ app2.get("/api/v1/code/sessions/:id/last-usage", (c) => {
19628
+ const id = c.req.param("id");
19629
+ const run = db2.getAgentRun(id);
19630
+ if (!run) return err6(c, 404, "not_found", "Session not found.");
19631
+ return c.json({ usage: db2.getLastApiUsageForSession(run.id) });
19632
+ });
18536
19633
  app2.patch("/api/v1/code/sessions/:id/title", async (c) => {
18537
19634
  const id = c.req.param("id");
18538
19635
  const run = db2.getAgentRun(id);
18539
- if (!run) return err5(c, 404, "not_found", "Session not found.");
18540
- const b = await body5(c);
19636
+ if (!run) return err6(c, 404, "not_found", "Session not found.");
19637
+ const b = await body6(c);
18541
19638
  const title = (b.title ?? "").trim();
18542
- if (!title) return err5(c, 400, "invalid_input", "title is required.");
19639
+ if (!title) return err6(c, 400, "invalid_input", "title is required.");
18543
19640
  db2.updateAgentRun(id, { title });
18544
19641
  return c.json({ ok: true, title });
18545
19642
  });
18546
19643
  app2.post("/api/v1/code/sessions/:id/archive", async (c) => {
18547
19644
  const id = c.req.param("id");
18548
19645
  const run = db2.getAgentRun(id);
18549
- if (!run) return err5(c, 404, "not_found", "Session not found.");
18550
- const b = await body5(c);
19646
+ if (!run) return err6(c, 404, "not_found", "Session not found.");
19647
+ const b = await body6(c);
18551
19648
  const archived = b.archived !== false;
18552
- if (archived && runs.isActive(id)) return err5(c, 409, "run_active", "Stop the current run before archiving this session.");
19649
+ if (archived && runs.isActive(id)) return err6(c, 409, "run_active", "Stop the current run before archiving this session.");
18553
19650
  db2.setAgentRunArchived(id, archived);
18554
19651
  return c.json({ ok: true, archived });
18555
19652
  });
18556
19653
  app2.delete("/api/v1/code/sessions/:id", (c) => {
18557
19654
  const id = c.req.param("id");
18558
19655
  const run = db2.getAgentRun(id);
18559
- if (!run) return err5(c, 404, "not_found", "Session not found.");
18560
- if (runs.isActive(id)) return err5(c, 409, "run_active", "Stop the current run before deleting this session.");
19656
+ if (!run) return err6(c, 404, "not_found", "Session not found.");
19657
+ if (runs.isActive(id)) return err6(c, 409, "run_active", "Stop the current run before deleting this session.");
18561
19658
  disposeLspClientsForConv(run.convId);
18562
19659
  db2.deleteCodeSession(id);
18563
19660
  return c.json({ ok: true });
@@ -18565,13 +19662,13 @@ function registerCodeRoutes(app2, d) {
18565
19662
  app2.post("/api/v1/code/sessions/:id/compact", async (c) => {
18566
19663
  const id = c.req.param("id");
18567
19664
  const run = db2.getAgentRun(id);
18568
- if (!run) return err5(c, 404, "not_found", "Session not found.");
18569
- if (!run.repoRoot) return err5(c, 400, "invalid_input", "Session has no repo root.");
18570
- if (runs.isActive(id)) return err5(c, 409, "run_active", "Stop or wait for the current run before compacting.");
18571
- if (run.clearedUpToMessageId) return err5(c, 409, "session_cleared", "Resume this session before compacting \u2014 compacting a cleared session would lose the hidden history.");
19665
+ if (!run) return err6(c, 404, "not_found", "Session not found.");
19666
+ if (!run.repoRoot) return err6(c, 400, "invalid_input", "Session has no repo root.");
19667
+ if (runs.isActive(id)) return err6(c, 409, "run_active", "Stop or wait for the current run before compacting.");
19668
+ if (run.clearedUpToMessageId) return err6(c, 409, "session_cleared", "Resume this session before compacting \u2014 compacting a cleared session would lose the hidden history.");
18572
19669
  const ms = d.manager.status();
18573
- if (ms.state !== "running" || !ms.model) return err5(c, 409, "model_not_loaded", "Load a model first.");
18574
- const b = await body5(c);
19670
+ if (ms.state !== "running" || !ms.model) return err6(c, 409, "model_not_loaded", "Load a model first.");
19671
+ const b = await body6(c);
18575
19672
  try {
18576
19673
  const result = await compactCodeSession({
18577
19674
  d,
@@ -18583,21 +19680,21 @@ function registerCodeRoutes(app2, d) {
18583
19680
  return c.json({ ok: true, ...result });
18584
19681
  } catch (e) {
18585
19682
  const message = e instanceof Error ? e.message : String(e);
18586
- if (message === "nothing_to_compact") return err5(c, 400, "nothing_to_compact", "Nothing to compact yet \u2014 history is already short enough.");
18587
- if (message === "session_cleared") return err5(c, 409, "session_cleared", "Resume this session before compacting \u2014 compacting a cleared session would lose the hidden history.");
18588
- return err5(c, 500, "compact_failed", message);
19683
+ if (message === "nothing_to_compact") return err6(c, 400, "nothing_to_compact", "Nothing to compact yet \u2014 history is already short enough.");
19684
+ if (message === "session_cleared") return err6(c, 409, "session_cleared", "Resume this session before compacting \u2014 compacting a cleared session would lose the hidden history.");
19685
+ return err6(c, 500, "compact_failed", message);
18589
19686
  }
18590
19687
  });
18591
19688
  app2.post("/api/v1/code/sessions/:id/clear", (c) => {
18592
19689
  const id = c.req.param("id");
18593
19690
  const run = db2.getAgentRun(id);
18594
- if (!run) return err5(c, 404, "not_found", "Session not found.");
18595
- if (runs.isActive(id)) return err5(c, 409, "run_active", "Stop or wait for the current run before clearing.");
18596
- if (run.revertedFromMessageId) return err5(c, 409, "session_reverted", "Resume this session before clearing \u2014 it currently has a reverted message pending.");
19691
+ if (!run) return err6(c, 404, "not_found", "Session not found.");
19692
+ if (runs.isActive(id)) return err6(c, 409, "run_active", "Stop or wait for the current run before clearing.");
19693
+ if (run.revertedFromMessageId) return err6(c, 409, "session_reverted", "Resume this session before clearing \u2014 it currently has a reverted message pending.");
18597
19694
  const conv = db2.getConversation(run.convId, true);
18598
19695
  const lastMsg = (conv?.messages ?? []).at(-1);
18599
- if (!lastMsg) return err5(c, 400, "nothing_to_clear", "Nothing to clear yet.");
18600
- if (run.clearedUpToMessageId === lastMsg.id) return err5(c, 400, "nothing_to_clear", "Already cleared up to the latest message.");
19696
+ if (!lastMsg) return err6(c, 400, "nothing_to_clear", "Nothing to clear yet.");
19697
+ if (run.clearedUpToMessageId === lastMsg.id) return err6(c, 400, "nothing_to_clear", "Already cleared up to the latest message.");
18601
19698
  db2.deactivateMessagesUpTo(run.convId, lastMsg.id);
18602
19699
  db2.setClearedUpToMessageId(id, lastMsg.id);
18603
19700
  return c.json({ ok: true, clearedUpToMessageId: lastMsg.id });
@@ -18605,36 +19702,36 @@ function registerCodeRoutes(app2, d) {
18605
19702
  app2.post("/api/v1/code/sessions/:id/resume", (c) => {
18606
19703
  const id = c.req.param("id");
18607
19704
  const run = db2.getAgentRun(id);
18608
- if (!run) return err5(c, 404, "not_found", "Session not found.");
18609
- if (runs.isActive(id)) return err5(c, 409, "run_active", "Stop or wait for the current run before resuming.");
19705
+ if (!run) return err6(c, 404, "not_found", "Session not found.");
19706
+ if (runs.isActive(id)) return err6(c, 409, "run_active", "Stop or wait for the current run before resuming.");
18610
19707
  if (run.revertedFromMessageId) {
18611
19708
  db2.reactivateMessagesFrom(run.convId, run.revertedFromMessageId);
18612
19709
  db2.setRevertedFromMessageId(id, null);
18613
19710
  return c.json({ ok: true });
18614
19711
  }
18615
- if (!run.clearedUpToMessageId) return err5(c, 400, "not_cleared", "This session has not been cleared.");
19712
+ if (!run.clearedUpToMessageId) return err6(c, 400, "not_cleared", "This session has not been cleared.");
18616
19713
  db2.reactivateMessagesUpTo(run.convId, run.clearedUpToMessageId);
18617
19714
  db2.setClearedUpToMessageId(id, null);
18618
19715
  return c.json({ ok: true });
18619
19716
  });
18620
19717
  app2.post("/api/v1/code/sessions/:id/revert", async (c) => {
18621
19718
  const id = c.req.param("id");
18622
- const b = await body5(c);
19719
+ const b = await body6(c);
18623
19720
  const messageId = (b.messageId ?? "").trim();
18624
- if (!messageId) return err5(c, 400, "invalid_input", "messageId is required.");
19721
+ if (!messageId) return err6(c, 400, "invalid_input", "messageId is required.");
18625
19722
  const run = db2.getAgentRun(id);
18626
- if (!run) return err5(c, 404, "not_found", "Session not found.");
18627
- if (!run.repoRoot) return err5(c, 400, "invalid_input", "Session has no repo root.");
18628
- if (runs.isActive(id)) return err5(c, 409, "run_active", "Stop or wait for the current run before reverting.");
18629
- if (run.clearedUpToMessageId) return err5(c, 409, "session_cleared", "Resume this session before reverting \u2014 it currently has cleared messages pending.");
19723
+ if (!run) return err6(c, 404, "not_found", "Session not found.");
19724
+ if (!run.repoRoot) return err6(c, 400, "invalid_input", "Session has no repo root.");
19725
+ if (runs.isActive(id)) return err6(c, 409, "run_active", "Stop or wait for the current run before reverting.");
19726
+ if (run.clearedUpToMessageId) return err6(c, 409, "session_cleared", "Resume this session before reverting \u2014 it currently has cleared messages pending.");
18630
19727
  const conv = db2.getConversation(run.convId, true);
18631
- if (!conv) return err5(c, 404, "not_found", "Session conversation not found.");
19728
+ if (!conv) return err6(c, 404, "not_found", "Session conversation not found.");
18632
19729
  const messages = conv.messages ?? [];
18633
19730
  const cut = resolveRevertCut(messages, messageId);
18634
19731
  if (!cut.ok) {
18635
- if (cut.error === "not_found") return err5(c, 404, "not_found", "Message not found.");
18636
- if (cut.error === "not_a_user_message") return err5(c, 400, "invalid_input", "Can only revert to a user message.");
18637
- return err5(c, 400, "invalid_input", "This is the first message in the session \u2014 nothing before it to revert to.");
19732
+ if (cut.error === "not_found") return err6(c, 404, "not_found", "Message not found.");
19733
+ if (cut.error === "not_a_user_message") return err6(c, 400, "invalid_input", "Can only revert to a user message.");
19734
+ return err6(c, 400, "invalid_input", "This is the first message in the session \u2014 nothing before it to revert to.");
18638
19735
  }
18639
19736
  let revertedFiles = [];
18640
19737
  let failedFiles = [];
@@ -18650,48 +19747,48 @@ function registerCodeRoutes(app2, d) {
18650
19747
  });
18651
19748
  app2.get("/api/v1/code/sessions/:id/git/status", async (c) => {
18652
19749
  const run = db2.getAgentRun(c.req.param("id"));
18653
- if (!run) return err5(c, 404, "not_found", "Session not found.");
18654
- if (!run.repoRoot) return err5(c, 400, "invalid_input", "Session has no repo root.");
19750
+ if (!run) return err6(c, 404, "not_found", "Session not found.");
19751
+ if (!run.repoRoot) return err6(c, 400, "invalid_input", "Session has no repo root.");
18655
19752
  const status = await getGitStatus(run.repoRoot);
18656
19753
  return c.json({ ok: true, status });
18657
19754
  });
18658
19755
  app2.post("/api/v1/code/sessions/:id/git/commit", async (c) => {
18659
19756
  const id = c.req.param("id");
18660
19757
  const run = db2.getAgentRun(id);
18661
- if (!run) return err5(c, 404, "not_found", "Session not found.");
18662
- if (!run.repoRoot) return err5(c, 400, "invalid_input", "Session has no repo root.");
18663
- if (runs.isActive(id)) return err5(c, 409, "run_active", "Stop or wait for the current run before committing.");
18664
- const b = await body5(c);
19758
+ if (!run) return err6(c, 404, "not_found", "Session not found.");
19759
+ if (!run.repoRoot) return err6(c, 400, "invalid_input", "Session has no repo root.");
19760
+ if (runs.isActive(id)) return err6(c, 409, "run_active", "Stop or wait for the current run before committing.");
19761
+ const b = await body6(c);
18665
19762
  try {
18666
19763
  const result = await commitGitChanges(run.repoRoot, b.message ?? "", b.files);
18667
19764
  return c.json({ ok: true, ...result });
18668
19765
  } catch (e) {
18669
19766
  const message = e instanceof Error ? e.message : "Commit failed.";
18670
- if (/commit message is required/i.test(message)) return err5(c, 400, "invalid_input", message);
18671
- if (/not a git repository/i.test(message)) return err5(c, 400, "not_a_git_repo", message);
18672
- if (/nothing to commit|nothing staged/i.test(message)) return err5(c, 400, "nothing_to_commit", message);
18673
- if (/outside the repo/i.test(message)) return err5(c, 400, "invalid_input", message);
18674
- return err5(c, 500, "git_commit_failed", message);
19767
+ if (/commit message is required/i.test(message)) return err6(c, 400, "invalid_input", message);
19768
+ if (/not a git repository/i.test(message)) return err6(c, 400, "not_a_git_repo", message);
19769
+ if (/nothing to commit|nothing staged/i.test(message)) return err6(c, 400, "nothing_to_commit", message);
19770
+ if (/outside the repo/i.test(message)) return err6(c, 400, "invalid_input", message);
19771
+ return err6(c, 500, "git_commit_failed", message);
18675
19772
  }
18676
19773
  });
18677
19774
  app2.post("/api/v1/code/sessions/:id/git/push", async (c) => {
18678
19775
  const id = c.req.param("id");
18679
19776
  const run = db2.getAgentRun(id);
18680
- if (!run) return err5(c, 404, "not_found", "Session not found.");
18681
- if (!run.repoRoot) return err5(c, 400, "invalid_input", "Session has no repo root.");
18682
- if (runs.isActive(id)) return err5(c, 409, "run_active", "Stop or wait for the current run before pushing.");
19777
+ if (!run) return err6(c, 404, "not_found", "Session not found.");
19778
+ if (!run.repoRoot) return err6(c, 400, "invalid_input", "Session has no repo root.");
19779
+ if (runs.isActive(id)) return err6(c, 409, "run_active", "Stop or wait for the current run before pushing.");
18683
19780
  const result = await pushGitBranch(run.repoRoot);
18684
19781
  if (!result.ok) {
18685
19782
  const status = result.reason === "diverged" ? 409 : 400;
18686
- return err5(c, status, result.reason, result.message);
19783
+ return err6(c, status, result.reason, result.message);
18687
19784
  }
18688
19785
  const compareUrl = await getGithubCompareUrl(run.repoRoot, result.branch);
18689
19786
  return c.json({ ok: true, remote: result.remote, branch: result.branch, compareUrl });
18690
19787
  });
18691
19788
  app2.get("/api/v1/code/sessions/:id/git/compare-url", async (c) => {
18692
19789
  const run = db2.getAgentRun(c.req.param("id"));
18693
- if (!run) return err5(c, 404, "not_found", "Session not found.");
18694
- if (!run.repoRoot) return err5(c, 400, "invalid_input", "Session has no repo root.");
19790
+ if (!run) return err6(c, 404, "not_found", "Session not found.");
19791
+ if (!run.repoRoot) return err6(c, 400, "invalid_input", "Session has no repo root.");
18695
19792
  const status = await getGitStatus(run.repoRoot);
18696
19793
  if (!status.isRepo || status.detached || !status.branch) return c.json({ ok: true, compareUrl: null });
18697
19794
  const compareUrl = await getGithubCompareUrl(run.repoRoot, status.branch);
@@ -18700,18 +19797,18 @@ function registerCodeRoutes(app2, d) {
18700
19797
  app2.post("/api/v1/code/sessions/:id/exec", async (c) => {
18701
19798
  const id = c.req.param("id");
18702
19799
  const run = db2.getAgentRun(id);
18703
- if (!run) return err5(c, 404, "not_found", "Session not found.");
18704
- if (!run.repoRoot) return err5(c, 400, "invalid_input", "Session has no repo root.");
18705
- if (runs.isActive(id)) return err5(c, 409, "run_active", "Stop or wait for the current run before running a command.");
18706
- const b = await body5(c);
19800
+ if (!run) return err6(c, 404, "not_found", "Session not found.");
19801
+ if (!run.repoRoot) return err6(c, 400, "invalid_input", "Session has no repo root.");
19802
+ if (runs.isActive(id)) return err6(c, 409, "run_active", "Stop or wait for the current run before running a command.");
19803
+ const b = await body6(c);
18707
19804
  const command = (b.command ?? "").trim();
18708
- if (!command) return err5(c, 400, "invalid_input", "A command is required.");
19805
+ if (!command) return err6(c, 400, "invalid_input", "A command is required.");
18709
19806
  const feedToModel = b.feedToModel !== false;
18710
19807
  let result;
18711
19808
  try {
18712
19809
  result = await runShellCommand(command, run.repoRoot, id);
18713
19810
  } catch (e) {
18714
- return err5(c, 500, "exec_failed", e instanceof Error ? e.message : "Command failed to run.");
19811
+ return err6(c, 500, "exec_failed", e instanceof Error ? e.message : "Command failed to run.");
18715
19812
  }
18716
19813
  let messageId;
18717
19814
  if (feedToModel) {
@@ -18748,16 +19845,16 @@ function registerCodeRoutes(app2, d) {
18748
19845
  });
18749
19846
  app2.post("/api/v1/code/sessions/:id/messages", async (c) => {
18750
19847
  const id = c.req.param("id");
18751
- const b = await body5(c);
19848
+ const b = await body6(c);
18752
19849
  const kind = b.kind === "steer" ? "steer" : "followUp";
18753
19850
  const run = db2.getAgentRun(id);
18754
- if (!run) return err5(c, 404, "not_found", "Session not found.");
19851
+ if (!run) return err6(c, 404, "not_found", "Session not found.");
18755
19852
  const conv = db2.getConversation(run.convId, true);
18756
- if (!conv) return err5(c, 404, "not_found", "Session conversation not found.");
18757
- if (!run.repoRoot) return err5(c, 400, "invalid_input", "Session has no repo root.");
19853
+ if (!conv) return err6(c, 404, "not_found", "Session conversation not found.");
19854
+ if (!run.repoRoot) return err6(c, 400, "invalid_input", "Session has no repo root.");
18758
19855
  const ms = d.manager.status();
18759
- if (ms.state !== "running" || !ms.model) return err5(c, 409, "model_not_loaded", "Load a model first.");
18760
- if (!d.manager.target()) return err5(c, 409, "model_not_loaded", "Engine not running.");
19856
+ if (ms.state !== "running" || !ms.model) return err6(c, 409, "model_not_loaded", "Load a model first.");
19857
+ if (!d.manager.target()) return err6(c, 409, "model_not_loaded", "Engine not running.");
18761
19858
  const newContent = (b.content ?? "").trim();
18762
19859
  let userMsgId;
18763
19860
  let task;
@@ -18766,7 +19863,7 @@ function registerCodeRoutes(app2, d) {
18766
19863
  task = contextFilesBlock(b.contextFiles) + ((b.promptOverride ?? "").trim() || newContent);
18767
19864
  } else {
18768
19865
  const lastUser = (conv.messages ?? []).filter((m) => m.role === "user").at(-1);
18769
- if (!lastUser) return err5(c, 400, "no_task", "No task to run.");
19866
+ if (!lastUser) return err6(c, 400, "no_task", "No task to run.");
18770
19867
  userMsgId = lastUser.id;
18771
19868
  task = contextFilesBlock(lastUser.textAttachments) + lastUser.content;
18772
19869
  }
@@ -18781,7 +19878,7 @@ function registerCodeRoutes(app2, d) {
18781
19878
  app2.get("/api/v1/code/sessions/:id/stream", (c) => {
18782
19879
  const id = c.req.param("id");
18783
19880
  const run = db2.getAgentRun(id);
18784
- if (!run) return err5(c, 404, "not_found", "Session not found.");
19881
+ if (!run) return err6(c, 404, "not_found", "Session not found.");
18785
19882
  const fromSeqRaw = Number.parseInt(c.req.query("fromSeq") ?? "", 10);
18786
19883
  const fromSeq = Number.isFinite(fromSeqRaw) && fromSeqRaw >= 0 ? fromSeqRaw : 0;
18787
19884
  return streamSSE3(c, async (stream) => {
@@ -18807,13 +19904,56 @@ import { streamSSE as streamSSE4 } from "hono/streaming";
18807
19904
 
18808
19905
  // src/gateway/anthropic.ts
18809
19906
  import { randomUUID as randomUUID9 } from "crypto";
19907
+ var UNSUPPORTED_SCHEMA_KEYWORDS = /* @__PURE__ */ new Set([
19908
+ "format",
19909
+ "pattern",
19910
+ "minLength",
19911
+ "maxLength",
19912
+ "minItems",
19913
+ "maxItems",
19914
+ "minimum",
19915
+ "maximum",
19916
+ "exclusiveMinimum",
19917
+ "exclusiveMaximum",
19918
+ "multipleOf"
19919
+ ]);
19920
+ var SCHEMA_NAME_MAP_KEYS = /* @__PURE__ */ new Set(["properties", "patternProperties", "$defs", "definitions"]);
19921
+ function stripUnsupportedSchemaKeywords(schema) {
19922
+ if (Array.isArray(schema)) return schema.map(stripUnsupportedSchemaKeywords);
19923
+ if (schema && typeof schema === "object") {
19924
+ const out = {};
19925
+ for (const [key, value] of Object.entries(schema)) {
19926
+ if (UNSUPPORTED_SCHEMA_KEYWORDS.has(key)) continue;
19927
+ out[key] = SCHEMA_NAME_MAP_KEYS.has(key) ? stripSchemaNameMap(value) : stripUnsupportedSchemaKeywords(value);
19928
+ }
19929
+ return out;
19930
+ }
19931
+ return schema;
19932
+ }
19933
+ function stripSchemaNameMap(map) {
19934
+ if (!map || typeof map !== "object" || Array.isArray(map)) return stripUnsupportedSchemaKeywords(map);
19935
+ const out = {};
19936
+ for (const [name, sub] of Object.entries(map)) {
19937
+ out[name] = stripUnsupportedSchemaKeywords(sub);
19938
+ }
19939
+ return out;
19940
+ }
18810
19941
  function mapToOpenAI(req) {
18811
19942
  const messages = [];
19943
+ const systemParts = [];
18812
19944
  if (req.system) {
18813
19945
  const text = typeof req.system === "string" ? req.system : req.system.filter((b) => b.type === "text").map((b) => b.text ?? "").join("\n");
18814
- if (text) messages.push({ role: "system", content: text });
19946
+ if (text) systemParts.push(text);
18815
19947
  }
18816
19948
  for (const msg2 of req.messages) {
19949
+ if (msg2.role !== "system") continue;
19950
+ const raw = msg2.content;
19951
+ const text = typeof raw === "string" ? raw : raw.filter((b) => b.type === "text").map((b) => b.text ?? "").join("\n");
19952
+ if (text) systemParts.push(text);
19953
+ }
19954
+ if (systemParts.length > 0) messages.push({ role: "system", content: systemParts.join("\n\n") });
19955
+ for (const msg2 of req.messages) {
19956
+ if (msg2.role === "system") continue;
18817
19957
  const raw = msg2.content;
18818
19958
  if (typeof raw === "string") {
18819
19959
  messages.push({ role: msg2.role, content: raw });
@@ -18840,9 +19980,6 @@ function mapToOpenAI(req) {
18840
19980
  const content = parts.length === 1 && parts[0].type === "text" ? parts[0].text : parts;
18841
19981
  messages.push({ role: "user", content });
18842
19982
  }
18843
- } else if (msg2.role === "system") {
18844
- const text = raw.filter((b) => b.type === "text").map((b) => b.text ?? "").join("\n");
18845
- if (text) messages.push({ role: "system", content: text });
18846
19983
  } else {
18847
19984
  let text = "";
18848
19985
  const toolCalls = [];
@@ -18882,7 +20019,7 @@ function mapToOpenAI(req) {
18882
20019
  if (req.tools?.length) {
18883
20020
  oai.tools = req.tools.map((t) => ({
18884
20021
  type: "function",
18885
- function: { name: t.name, description: t.description, parameters: t.input_schema }
20022
+ function: { name: t.name, description: t.description, parameters: stripUnsupportedSchemaKeywords(t.input_schema) }
18886
20023
  }));
18887
20024
  const tc = req.tool_choice;
18888
20025
  if (tc)
@@ -18954,6 +20091,8 @@ async function* streamToAnthropic(oaiStream, modelName, msgId, onUsage, onLive)
18954
20091
  let outputTokens = 0;
18955
20092
  let inputTokens = 0;
18956
20093
  let cacheReadTokens = 0;
20094
+ let promptTps = 0;
20095
+ let genTps = 0;
18957
20096
  let liveOut = 0;
18958
20097
  yield sse("message_start", {
18959
20098
  message: {
@@ -18999,6 +20138,8 @@ async function* streamToAnthropic(oaiStream, modelName, msgId, onUsage, onLive)
18999
20138
  const timings = chunk.timings;
19000
20139
  const cacheN = timings?.cache_n ?? timings?.prompt_n_reuse;
19001
20140
  if (cacheN != null) cacheReadTokens = cacheN;
20141
+ if (timings?.prompt_per_second) promptTps = timings.prompt_per_second;
20142
+ if (timings?.predicted_per_second) genTps = timings.predicted_per_second;
19002
20143
  const choices = chunk.choices;
19003
20144
  if (!choices?.length) continue;
19004
20145
  const choice = choices[0];
@@ -19085,7 +20226,12 @@ async function* streamToAnthropic(oaiStream, modelName, msgId, onUsage, onLive)
19085
20226
  yield sse("message_stop", {});
19086
20227
  if (onUsage) {
19087
20228
  try {
19088
- onUsage({ inputTokens, outputTokens });
20229
+ onUsage({
20230
+ inputTokens,
20231
+ outputTokens,
20232
+ promptTps: promptTps > 0 ? promptTps : void 0,
20233
+ genTps: genTps > 0 ? genTps : void 0
20234
+ });
19089
20235
  } catch {
19090
20236
  }
19091
20237
  }
@@ -19114,6 +20260,10 @@ function cbStop(index) {
19114
20260
  }
19115
20261
 
19116
20262
  // src/gateway/gateway.ts
20263
+ function resolveCodeSession(c) {
20264
+ const token = presentedKey(c);
20265
+ return { token, codeSessionId: token ? sessionAuth.resolve(token) : null };
20266
+ }
19117
20267
  function clientAbort(c) {
19118
20268
  const ac = new AbortController();
19119
20269
  const sig = c.req.raw.signal;
@@ -19123,6 +20273,25 @@ function clientAbort(c) {
19123
20273
  }
19124
20274
  return ac;
19125
20275
  }
20276
+ async function describeEngineError(res) {
20277
+ const raw = await res.text().catch(() => "");
20278
+ try {
20279
+ const parsed = JSON.parse(raw);
20280
+ if (parsed.error?.message) return { message: parsed.error.message, type: parsed.error.type };
20281
+ } catch {
20282
+ }
20283
+ return { message: raw.slice(0, 2e3) || `Engine returned HTTP ${res.status}.` };
20284
+ }
20285
+ function anthropicErrorType(status) {
20286
+ if (status === 400) return "invalid_request_error";
20287
+ if (status === 404) return "not_found_error";
20288
+ if (status === 429) return "rate_limit_error";
20289
+ if (status === 503) return "overloaded_error";
20290
+ return "api_error";
20291
+ }
20292
+ function asClientStatus(status) {
20293
+ return status >= 400 && status <= 599 ? status : 500;
20294
+ }
19126
20295
  function registerGateway(app2, d) {
19127
20296
  app2.post("/v1/messages", async (c) => {
19128
20297
  let req;
@@ -19135,6 +20304,13 @@ function registerGateway(app2, d) {
19135
20304
  );
19136
20305
  }
19137
20306
  if (req.model?.startsWith("claude-")) req.model = req.model.slice(7);
20307
+ const { token: anthropicToken, codeSessionId: anthropicCodeSessionId } = resolveCodeSession(c);
20308
+ if (anthropicCodeSessionId) {
20309
+ const override = sessionAuth.getThinkingBudgetForToken(anthropicToken);
20310
+ if (override !== null) {
20311
+ req.thinking = override > 0 ? { type: "enabled", budget_tokens: override } : void 0;
20312
+ }
20313
+ }
19138
20314
  if (!req.max_tokens) {
19139
20315
  return c.json(
19140
20316
  { type: "error", error: { type: "invalid_request_error", message: "max_tokens is required." } },
@@ -19158,6 +20334,7 @@ function registerGateway(app2, d) {
19158
20334
  if (oaiAlias) oaiBody.model = oaiAlias;
19159
20335
  d.manager.generationStart();
19160
20336
  const ac = clientAbort(c);
20337
+ const requestStart = Date.now();
19161
20338
  let res;
19162
20339
  try {
19163
20340
  res = await fetch(`${target}/v1/chat/completions`, {
@@ -19168,17 +20345,26 @@ function registerGateway(app2, d) {
19168
20345
  });
19169
20346
  } catch (e) {
19170
20347
  d.manager.generationEnd();
20348
+ const err7 = e;
20349
+ const isAbort = err7.name === "AbortError" || ac.signal.aborted;
20350
+ const cause = err7.cause instanceof Error ? `: ${err7.cause.message}` : "";
19171
20351
  return c.json(
19172
- { type: "error", error: { type: "api_error", message: e.message || "Engine error." } },
20352
+ {
20353
+ type: "error",
20354
+ error: {
20355
+ type: "api_error",
20356
+ message: isAbort ? "Client disconnected before the engine responded." : `${err7.message || "Engine unreachable."}${cause}`
20357
+ }
20358
+ },
19173
20359
  500
19174
20360
  );
19175
20361
  }
19176
20362
  if (!res.ok || !res.body) {
19177
20363
  d.manager.generationEnd();
19178
- const text = await res.text().catch(() => "");
20364
+ const { message, type } = await describeEngineError(res);
19179
20365
  return c.json(
19180
- { type: "error", error: { type: "api_error", message: text || "Engine error." } },
19181
- 500
20366
+ { type: "error", error: { type: type ?? anthropicErrorType(res.status), message } },
20367
+ asClientStatus(res.status)
19182
20368
  );
19183
20369
  }
19184
20370
  if (req.stream) {
@@ -19189,8 +20375,22 @@ function registerGateway(app2, d) {
19189
20375
  msgId,
19190
20376
  (u) => {
19191
20377
  try {
19192
- d.manager.recordCompletion({ inputTokens: u.inputTokens, outputTokens: u.outputTokens });
19193
- d.db.recordApiUsage({ source: "anthropic", modelKey: req.model ?? null, promptTokens: u.inputTokens, genTokens: u.outputTokens });
20378
+ d.manager.recordCompletion({
20379
+ inputTokens: u.inputTokens,
20380
+ outputTokens: u.outputTokens,
20381
+ promptTps: u.promptTps,
20382
+ genTps: u.genTps
20383
+ });
20384
+ d.db.recordApiUsage({
20385
+ source: "anthropic",
20386
+ modelKey: req.model ?? null,
20387
+ promptTokens: u.inputTokens,
20388
+ genTokens: u.outputTokens,
20389
+ codeSessionId: anthropicCodeSessionId,
20390
+ durationMs: Date.now() - requestStart,
20391
+ promptTps: u.promptTps,
20392
+ genTps: u.genTps
20393
+ });
19194
20394
  } catch {
19195
20395
  }
19196
20396
  },
@@ -19217,7 +20417,7 @@ function registerGateway(app2, d) {
19217
20417
  }
19218
20418
  try {
19219
20419
  const oaiRes = await res.json();
19220
- recordOpenAiUsage(d, oaiRes, "anthropic", req.model ?? null);
20420
+ recordOpenAiUsage(d, oaiRes, "anthropic", req.model ?? null, anthropicCodeSessionId, Date.now() - requestStart);
19221
20421
  return c.json(mapFromOpenAI(oaiRes, modelName));
19222
20422
  } finally {
19223
20423
  d.manager.generationEnd();
@@ -19270,6 +20470,7 @@ function registerGateway(app2, d) {
19270
20470
  parsedBody = null;
19271
20471
  }
19272
20472
  }
20473
+ const { token: chatToken, codeSessionId: chatCodeSessionId } = resolveCodeSession(c);
19273
20474
  const requestedModel = isChat ? parsedBody?.model ?? "" : "";
19274
20475
  const routeResult = await d.modelRouter.route(requestedModel);
19275
20476
  if ("status" in routeResult) {
@@ -19294,6 +20495,13 @@ function registerGateway(app2, d) {
19294
20495
  const alias = engineModelAlias(d.registry.active()?.kind ?? "");
19295
20496
  if (alias) parsedBody.model = alias;
19296
20497
  }
20498
+ if (parsedBody && chatCodeSessionId) {
20499
+ const override = sessionAuth.getThinkingBudgetForToken(chatToken);
20500
+ if (override !== null) {
20501
+ if (override > 0) parsedBody.thinking_budget_tokens = override;
20502
+ else delete parsedBody.thinking_budget_tokens;
20503
+ }
20504
+ }
19297
20505
  if (parsedBody?.stream === true && parsedBody.stream_options === void 0) {
19298
20506
  parsedBody.stream_options = { include_usage: true };
19299
20507
  }
@@ -19304,17 +20512,18 @@ function registerGateway(app2, d) {
19304
20512
  init.duplex = "half";
19305
20513
  }
19306
20514
  }
20515
+ const requestStart = Date.now();
19307
20516
  let res;
19308
20517
  try {
19309
20518
  res = await fetch(upstream, init);
19310
20519
  } catch (e) {
19311
- const err6 = e;
19312
- const isAbort = err6.name === "AbortError" || ac.signal.aborted;
19313
- const cause = err6.cause instanceof Error ? `: ${err6.cause.message}` : "";
20520
+ const err7 = e;
20521
+ const isAbort = err7.name === "AbortError" || ac.signal.aborted;
20522
+ const cause = err7.cause instanceof Error ? `: ${err7.cause.message}` : "";
19314
20523
  return c.json(
19315
20524
  {
19316
20525
  error: {
19317
- message: isAbort ? "Client disconnected before the engine responded." : `${err6.message || "Engine unreachable."}${cause}`,
20526
+ message: isAbort ? "Client disconnected before the engine responded." : `${err7.message || "Engine unreachable."}${cause}`,
19318
20527
  type: "api_error",
19319
20528
  code: isAbort ? "client_disconnected" : "engine_unreachable"
19320
20529
  }
@@ -19326,7 +20535,7 @@ function registerGateway(app2, d) {
19326
20535
  try {
19327
20536
  const [a, b] = res.body.tee();
19328
20537
  d.manager.generationStart();
19329
- const drain = parsedBody?.stream === true ? recordOpenAiStreamUsage(d, b, "openai", requestedModel || null) : recordOpenAiJsonUsage(d, b, "openai", requestedModel || null);
20538
+ const drain = parsedBody?.stream === true ? recordOpenAiStreamUsage(d, b, "openai", requestedModel || null, chatCodeSessionId, requestStart) : recordOpenAiJsonUsage(d, b, "openai", requestedModel || null, chatCodeSessionId, requestStart);
19330
20539
  void drain.finally(() => d.manager.generationEnd());
19331
20540
  return new Response(a, { status: res.status, headers: res.headers });
19332
20541
  } catch {
@@ -19336,7 +20545,7 @@ function registerGateway(app2, d) {
19336
20545
  return new Response(res.body, { status: res.status, headers: res.headers });
19337
20546
  });
19338
20547
  }
19339
- function recordOpenAiUsage(d, oai, source, modelKey) {
20548
+ function recordOpenAiUsage(d, oai, source, modelKey, codeSessionId = null, durationMs = null) {
19340
20549
  try {
19341
20550
  const usage = oai.usage;
19342
20551
  const timings = oai.timings;
@@ -19346,21 +20555,32 @@ function recordOpenAiUsage(d, oai, source, modelKey) {
19346
20555
  promptTps: timings?.prompt_per_second,
19347
20556
  genTps: timings?.predicted_per_second
19348
20557
  });
19349
- d.db.recordApiUsage({ source, modelKey, promptTokens: usage?.prompt_tokens ?? 0, genTokens: usage?.completion_tokens ?? 0 });
20558
+ d.db.recordApiUsage({
20559
+ source,
20560
+ modelKey,
20561
+ promptTokens: usage?.prompt_tokens ?? 0,
20562
+ genTokens: usage?.completion_tokens ?? 0,
20563
+ codeSessionId,
20564
+ durationMs,
20565
+ // Already extracted for the engine card two lines up; persisting them is what lets the
20566
+ // stats row report the engine's real rates instead of deriving both from `durationMs`.
20567
+ promptTps: timings?.prompt_per_second,
20568
+ genTps: timings?.predicted_per_second
20569
+ });
19350
20570
  } catch {
19351
20571
  }
19352
20572
  }
19353
- async function recordOpenAiJsonUsage(d, body6, source, modelKey) {
20573
+ async function recordOpenAiJsonUsage(d, body7, source, modelKey, codeSessionId = null, startedAt2 = null) {
19354
20574
  try {
19355
- const text = await new Response(body6).text();
20575
+ const text = await new Response(body7).text();
19356
20576
  const oai = JSON.parse(text);
19357
- recordOpenAiUsage(d, oai, source, modelKey);
20577
+ recordOpenAiUsage(d, oai, source, modelKey, codeSessionId, startedAt2 != null ? Date.now() - startedAt2 : null);
19358
20578
  } catch {
19359
20579
  }
19360
20580
  }
19361
- async function recordOpenAiStreamUsage(d, body6, source, modelKey) {
20581
+ async function recordOpenAiStreamUsage(d, body7, source, modelKey, codeSessionId = null, startedAt2 = null) {
19362
20582
  try {
19363
- const reader = body6.getReader();
20583
+ const reader = body7.getReader();
19364
20584
  const dec = new TextDecoder();
19365
20585
  let buf = "";
19366
20586
  let promptTokens = 0;
@@ -19404,14 +20624,25 @@ async function recordOpenAiStreamUsage(d, body6, source, modelKey) {
19404
20624
  }
19405
20625
  }
19406
20626
  d.manager.recordCompletion({ inputTokens: promptTokens, outputTokens: completionTokens, promptTps, genTps });
19407
- d.db.recordApiUsage({ source, modelKey, promptTokens, genTokens: completionTokens });
20627
+ d.db.recordApiUsage({
20628
+ source,
20629
+ modelKey,
20630
+ promptTokens,
20631
+ genTokens: completionTokens,
20632
+ codeSessionId,
20633
+ durationMs: startedAt2 != null ? Date.now() - startedAt2 : null,
20634
+ // Accumulated from the stream's own `timings` above (0 when the engine reported none —
20635
+ // recordApiUsage stores that as null, and the reader falls back to the old derivation).
20636
+ promptTps,
20637
+ genTps
20638
+ });
19408
20639
  } catch {
19409
20640
  }
19410
20641
  }
19411
20642
 
19412
20643
  // src/server.ts
19413
20644
  setGlobalDispatcher(new Agent({ keepAliveMaxTimeout: 6e4, connections: 10 }));
19414
- var WEB_ROOT = join29(dirname11(fileURLToPath(import.meta.url)), "webdist");
20645
+ var WEB_ROOT = join30(dirname11(fileURLToPath(import.meta.url)), "webdist");
19415
20646
  function createApp(d) {
19416
20647
  const app2 = new Hono();
19417
20648
  app2.use(
@@ -19442,18 +20673,19 @@ function createApp(d) {
19442
20673
  registerChatAgentRoutes(app2, d);
19443
20674
  registerAgentRoutes(app2, d);
19444
20675
  registerCodeRoutes(app2, d);
20676
+ registerTerminalRoutes(app2, d);
19445
20677
  registerGateway(app2, d);
19446
20678
  app2.get("/*", (c) => {
19447
20679
  const path = decodeURIComponent(new URL(c.req.url).pathname).replace(/^\/+/, "");
19448
20680
  if (path.startsWith("api/") || path.startsWith("v1/")) {
19449
20681
  return c.json({ error: { code: "not_found", message: "Unknown endpoint." } }, 404);
19450
20682
  }
19451
- let file = normalize2(join29(WEB_ROOT, path || "index.html"));
20683
+ let file = normalize2(join30(WEB_ROOT, path || "index.html"));
19452
20684
  if (!file.startsWith(WEB_ROOT) || !existsSync24(file) || statSync6(file).isDirectory()) {
19453
- file = join29(WEB_ROOT, "index.html");
20685
+ file = join30(WEB_ROOT, "index.html");
19454
20686
  }
19455
20687
  if (!existsSync24(file)) return c.text("web ui not built \u2014 run `npm run build:web`", 500);
19456
- return new Response(readFileSync13(file), { status: 200, headers: { "Content-Type": contentType(file) } });
20688
+ return new Response(readFileSync14(file), { status: 200, headers: { "Content-Type": contentType(file) } });
19457
20689
  });
19458
20690
  return app2;
19459
20691
  }
@@ -19478,22 +20710,22 @@ function contentType(file) {
19478
20710
  }
19479
20711
 
19480
20712
  // src/tunnel/manager.ts
19481
- import { execFile as execFile13, spawn as spawn7, spawnSync as spawnSync2 } from "child_process";
19482
- import { mkdirSync as mkdirSync15, readFileSync as readFileSync14, readdirSync as readdirSync9, rmSync as rmSync14, writeFileSync as writeFileSync13 } from "fs";
19483
- import { join as join31 } from "path";
20713
+ import { execFile as execFile13, spawn as spawn7, spawnSync as spawnSync3 } from "child_process";
20714
+ import { mkdirSync as mkdirSync16, readFileSync as readFileSync15, readdirSync as readdirSync10, rmSync as rmSync15, writeFileSync as writeFileSync14 } from "fs";
20715
+ import { join as join32 } from "path";
19484
20716
 
19485
20717
  // src/tunnel/provision.ts
19486
- import { chmodSync as chmodSync3, existsSync as existsSync25, mkdirSync as mkdirSync14, rmSync as rmSync13 } from "fs";
19487
- import { join as join30 } from "path";
20718
+ import { chmodSync as chmodSync3, existsSync as existsSync25, mkdirSync as mkdirSync15, rmSync as rmSync14 } from "fs";
20719
+ import { join as join31 } from "path";
19488
20720
  var CLOUDFLARED_REPO = "cloudflare/cloudflared";
19489
20721
  function cloudflaredDir(dataDir) {
19490
- return join30(dataDir, "cloudflared");
20722
+ return join31(dataDir, "cloudflared");
19491
20723
  }
19492
20724
  function cloudflaredBinName(platform = process.platform) {
19493
20725
  return platform === "win32" ? "cloudflared.exe" : "cloudflared";
19494
20726
  }
19495
20727
  function cloudflaredBinPath(dataDir, platform = process.platform) {
19496
- return join30(cloudflaredDir(dataDir), cloudflaredBinName(platform));
20728
+ return join31(cloudflaredDir(dataDir), cloudflaredBinName(platform));
19497
20729
  }
19498
20730
  function cloudflaredAssetName(platform = process.platform, archStr = process.arch) {
19499
20731
  if (platform === "win32") {
@@ -19522,18 +20754,18 @@ async function ensureCloudflared(dataDir, onProgress, signal) {
19522
20754
  if (existsSync25(binPath)) return { binPath, version: version2 };
19523
20755
  const asset = pickCloudflaredAsset(rel.assets ?? []);
19524
20756
  if (!asset) throw new Error("no_release_asset");
19525
- mkdirSync14(dir, { recursive: true });
20757
+ mkdirSync15(dir, { recursive: true });
19526
20758
  onProgress?.({ phase: "downloading", pct: 0 });
19527
20759
  if (asset.name.endsWith(".tgz")) {
19528
- const tmp = join30(dir, asset.name);
20760
+ const tmp = join31(dir, asset.name);
19529
20761
  try {
19530
20762
  await downloadFile(asset.browser_download_url, tmp, onProgress, signal);
19531
20763
  onProgress?.({ phase: "extracting", pct: -1 });
19532
20764
  await extractArchive(tmp, dir);
19533
- rmSync13(tmp, { force: true });
20765
+ rmSync14(tmp, { force: true });
19534
20766
  } catch (e) {
19535
- rmSync13(tmp, { force: true });
19536
- rmSync13(dir, { recursive: true, force: true });
20767
+ rmSync14(tmp, { force: true });
20768
+ rmSync14(dir, { recursive: true, force: true });
19537
20769
  throw e;
19538
20770
  }
19539
20771
  const found = findFile(dir, "cloudflared");
@@ -19569,25 +20801,25 @@ function forceKill2(child) {
19569
20801
  else child.kill("SIGKILL");
19570
20802
  }
19571
20803
  function tunnelPidDir(dataDir) {
19572
- return join31(dataDir, "run");
20804
+ return join32(dataDir, "run");
19573
20805
  }
19574
20806
  function tunnelPidFile(dataDir, pid) {
19575
- return join31(tunnelPidDir(dataDir), `tunnel-${pid}.pid`);
20807
+ return join32(tunnelPidDir(dataDir), `tunnel-${pid}.pid`);
19576
20808
  }
19577
20809
  function writeTunnelPid(dataDir, pid) {
19578
20810
  try {
19579
- mkdirSync15(tunnelPidDir(dataDir), { recursive: true });
19580
- writeFileSync13(tunnelPidFile(dataDir, pid), JSON.stringify({ pid, owner: process.pid }));
20811
+ mkdirSync16(tunnelPidDir(dataDir), { recursive: true });
20812
+ writeFileSync14(tunnelPidFile(dataDir, pid), JSON.stringify({ pid, owner: process.pid }));
19581
20813
  } catch {
19582
20814
  }
19583
20815
  }
19584
20816
  function clearTunnelPid(dataDir, pid) {
19585
20817
  try {
19586
- rmSync14(tunnelPidFile(dataDir, pid), { force: true });
20818
+ rmSync15(tunnelPidFile(dataDir, pid), { force: true });
19587
20819
  } catch {
19588
20820
  }
19589
20821
  }
19590
- function pidAlive2(pid) {
20822
+ function pidAlive3(pid) {
19591
20823
  if (!pid) return false;
19592
20824
  try {
19593
20825
  process.kill(pid, 0);
@@ -19600,20 +20832,20 @@ function readTunnelPidFiles(dataDir) {
19600
20832
  const dir = tunnelPidDir(dataDir);
19601
20833
  let names;
19602
20834
  try {
19603
- names = readdirSync9(dir).filter((n) => /^tunnel-\d+\.pid$/.test(n));
20835
+ names = readdirSync10(dir).filter((n) => /^tunnel-\d+\.pid$/.test(n));
19604
20836
  } catch {
19605
20837
  return [];
19606
20838
  }
19607
20839
  const out = [];
19608
20840
  for (const name of names) {
19609
- const file = join31(dir, name);
20841
+ const file = join32(dir, name);
19610
20842
  try {
19611
- const { pid, owner } = JSON.parse(readFileSync14(file, "utf8"));
20843
+ const { pid, owner } = JSON.parse(readFileSync15(file, "utf8"));
19612
20844
  if (typeof pid === "number" && pid > 0) out.push({ pid, owner: typeof owner === "number" ? owner : 0, file });
19613
- else rmSync14(file, { force: true });
20845
+ else rmSync15(file, { force: true });
19614
20846
  } catch {
19615
20847
  try {
19616
- rmSync14(file, { force: true });
20848
+ rmSync15(file, { force: true });
19617
20849
  } catch {
19618
20850
  }
19619
20851
  }
@@ -19623,15 +20855,15 @@ function readTunnelPidFiles(dataDir) {
19623
20855
  function reapStaleTunnels(dataDir) {
19624
20856
  let killed = 0;
19625
20857
  for (const { pid, owner, file } of readTunnelPidFiles(dataDir)) {
19626
- if (owner && pidAlive2(owner)) continue;
20858
+ if (owner && pidAlive3(owner)) continue;
19627
20859
  try {
19628
- if (process.platform === "win32") spawnSync2("taskkill", ["/PID", String(pid), "/F", "/T"]);
20860
+ if (process.platform === "win32") spawnSync3("taskkill", ["/PID", String(pid), "/F", "/T"]);
19629
20861
  else process.kill(pid, "SIGKILL");
19630
20862
  killed++;
19631
20863
  } catch {
19632
20864
  }
19633
20865
  try {
19634
- rmSync14(file, { force: true });
20866
+ rmSync15(file, { force: true });
19635
20867
  } catch {
19636
20868
  }
19637
20869
  }
@@ -19641,12 +20873,12 @@ function killTrackedTunnelsSync(dataDir) {
19641
20873
  for (const { pid, owner, file } of readTunnelPidFiles(dataDir)) {
19642
20874
  if (owner !== process.pid) continue;
19643
20875
  try {
19644
- if (process.platform === "win32") spawnSync2("taskkill", ["/PID", String(pid), "/F", "/T"]);
20876
+ if (process.platform === "win32") spawnSync3("taskkill", ["/PID", String(pid), "/F", "/T"]);
19645
20877
  else process.kill(pid, "SIGKILL");
19646
20878
  } catch {
19647
20879
  }
19648
20880
  try {
19649
- rmSync14(file, { force: true });
20881
+ rmSync15(file, { force: true });
19650
20882
  } catch {
19651
20883
  }
19652
20884
  }
@@ -19739,8 +20971,8 @@ var TunnelManager = class {
19739
20971
  hideChildConsoleWindows();
19740
20972
  var version = "0.1.1";
19741
20973
  try {
19742
- const pkgPath = join32(dirname12(fileURLToPath2(import.meta.url)), "..", "package.json");
19743
- version = JSON.parse(readFileSync15(pkgPath, "utf8")).version ?? version;
20974
+ const pkgPath = join33(dirname12(fileURLToPath2(import.meta.url)), "..", "package.json");
20975
+ version = JSON.parse(readFileSync16(pkgPath, "utf8")).version ?? version;
19744
20976
  } catch {
19745
20977
  }
19746
20978
  var [nodeMajor, nodeMinor] = process.versions.node.split(".").map(Number);
@@ -19767,7 +20999,7 @@ function argValue(name, fallback) {
19767
20999
  }
19768
21000
  function configuredDaemonPort(configPath) {
19769
21001
  try {
19770
- const cfg2 = JSON.parse(readFileSync15(configPath, "utf8"));
21002
+ const cfg2 = JSON.parse(readFileSync16(configPath, "utf8"));
19771
21003
  if (typeof cfg2.daemon?.port === "number" && cfg2.daemon.port > 0) return cfg2.daemon.port;
19772
21004
  } catch {
19773
21005
  }
@@ -19783,10 +21015,21 @@ if (argv[0] === "launch") {
19783
21015
  explicitPort,
19784
21016
  configuredDaemonPort(launchConfigPath)
19785
21017
  );
21018
+ const authToken = argValue("--token", "") || void 0;
19786
21019
  const passthrough = argv.slice(2).filter(
19787
- (a, i, arr) => a !== "--port" && arr[i - 1] !== "--port" && a !== "--model" && arr[i - 1] !== "--model" && a !== "--config" && arr[i - 1] !== "--config"
21020
+ (a, i, arr) => a !== "--port" && arr[i - 1] !== "--port" && a !== "--model" && arr[i - 1] !== "--model" && a !== "--config" && arr[i - 1] !== "--config" && a !== "--token" && arr[i - 1] !== "--token"
19788
21021
  );
19789
- const code = await launchCli(target, port2, passthrough, void 0, modelKey);
21022
+ const code = await launchCli(target, port2, passthrough, void 0, modelKey, void 0, authToken);
21023
+ const codeSessionId = argValue("--session-id", "") || argValue("--resume", "");
21024
+ if (codeSessionId) {
21025
+ try {
21026
+ await fetch(`http://127.0.0.1:${port2}/api/v1/code/sessions/${codeSessionId}/terminal/agent-exited`, {
21027
+ method: "POST",
21028
+ signal: AbortSignal.timeout(2e3)
21029
+ });
21030
+ } catch {
21031
+ }
21032
+ }
19790
21033
  process.exit(code);
19791
21034
  }
19792
21035
  if (argv[0] === "mcp-server") {
@@ -19865,13 +21108,18 @@ try {
19865
21108
  if (reapedTunnels > 0) console.log(`reaped ${reapedTunnels} orphaned tunnel process(es) from a previous run`);
19866
21109
  } catch {
19867
21110
  }
21111
+ try {
21112
+ const reapedTerminals = reapStaleTerminals(store.dir());
21113
+ if (reapedTerminals > 0) console.log(`reaped ${reapedTerminals} orphaned terminal-agent process(es) from a previous run`);
21114
+ } catch {
21115
+ }
19868
21116
  var registry = new Registry(store);
19869
21117
  var pruned = registry.pruneDeadManagedBuilds();
19870
21118
  if (pruned > 0) console.log(`pruned ${pruned} dangling engine build(s)`);
19871
21119
  var provision = new ProvisionState();
19872
21120
  var build = new BuildState();
19873
21121
  var updates = new UpdateChecker();
19874
- var enginesDir = join32(store.dir(), "engines");
21122
+ var enginesDir = join33(store.dir(), "engines");
19875
21123
  void seedDefaultEngines(registry, enginesDir, provision).then(() => registry.ensureProbed());
19876
21124
  var manager = new Manager(store);
19877
21125
  var scanner = new Scanner(store);
@@ -20015,6 +21263,7 @@ function listen(attempt = 0) {
20015
21263
  server = s;
20016
21264
  }
20017
21265
  listen();
21266
+ registerTerminalWs(server, deps);
20018
21267
  deps.rebind = () => {
20019
21268
  const c = store.snapshot();
20020
21269
  const want = c.daemon.lanBind ? "0.0.0.0" : c.daemon.host || "127.0.0.1";
@@ -20044,7 +21293,7 @@ var restarting = false;
20044
21293
  function spawnReplacement() {
20045
21294
  let out = "ignore";
20046
21295
  try {
20047
- out = openSync3(join32(store.dir(), "restart.log"), "a");
21296
+ out = openSync3(join33(store.dir(), "restart.log"), "a");
20048
21297
  } catch {
20049
21298
  out = "ignore";
20050
21299
  }
@@ -20163,6 +21412,10 @@ process.on("exit", () => {
20163
21412
  killTrackedTunnelsSync(store.dir());
20164
21413
  } catch {
20165
21414
  }
21415
+ try {
21416
+ killTrackedTerminalsSync(store.dir());
21417
+ } catch {
21418
+ }
20166
21419
  try {
20167
21420
  removePidfile(store.dir());
20168
21421
  } catch {