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.
- package/README.md +78 -0
- package/bin/turbollm.mjs +1 -1
- package/dist/{chunk-U4OPDHSX.js → chunk-5MCP2ZAJ.js} +2 -0
- package/dist/cli.js +1778 -525
- package/dist/{distiller-MRW6VEYO.js → distiller-6E6IOZ5G.js} +1 -1
- package/dist/webdist/assets/{AgentEditPage-CmdThoLF.js → AgentEditPage-CyzZ8vDr.js} +1 -1
- package/dist/webdist/assets/{ChatScreen-ChVYoy7Z.js → ChatScreen-DWVblPWw.js} +3 -3
- package/dist/webdist/assets/CodeComposer-BvNMY6PI.js +1 -0
- package/dist/webdist/assets/{CodeHomeScreen-D1VHqWAF.js → CodeHomeScreen-CmzaqOmH.js} +1 -1
- package/dist/webdist/assets/CodeSessionScreen-BtA5u3li.js +20 -0
- package/dist/webdist/assets/CodeSessionScreen-DDGTF8rc.css +1 -0
- package/dist/webdist/assets/{CustomizeScreen-C053iQLd.js → CustomizeScreen-ACC_4SMn.js} +3 -3
- package/dist/webdist/assets/{DeveloperScreen-BYk0rndu.js → DeveloperScreen-CSJZh91v.js} +1 -1
- package/dist/webdist/assets/{EnginesScreen-DkLzY4sp.js → EnginesScreen-ChGdOOW9.js} +4 -4
- package/dist/webdist/assets/{FsBrowser-CNit4h3B.js → FsBrowser-D9RjrjvO.js} +1 -1
- package/dist/webdist/assets/ModelLoadMenu-DzaxKmIV.js +2 -0
- package/dist/webdist/assets/{ModelsScreen-5TvL8kCj.js → ModelsScreen-wuI7h511.js} +2 -2
- package/dist/webdist/assets/SettingsScreen-DsKi9vxM.js +1 -0
- package/dist/webdist/assets/{SkillEditPage-DmJWJjhQ.js → SkillEditPage-XGTaeQ8o.js} +1 -1
- package/dist/webdist/assets/{TokensScreen-Dyd5sSGT.js → TokensScreen-B02y3G-H.js} +1 -1
- package/dist/webdist/assets/{ToolApprovalBar-0Rv_eC33.js → ToolApprovalBar-D4YRUMat.js} +9 -9
- package/dist/webdist/assets/WorkspaceScreen-DRoUxluw.js +1 -0
- package/dist/webdist/assets/{agent-api-C6XrgjWw.js → agent-api-D86F96i6.js} +1 -1
- package/dist/webdist/assets/{arc-V_ckaw1n.js → arc-BScxM0vj.js} +1 -1
- package/dist/webdist/assets/{architectureDiagram-3BPJPVTR-BaKHxCMv.js → architectureDiagram-3BPJPVTR-DbIjWGrc.js} +1 -1
- package/dist/webdist/assets/{arrow-left-BRhcceVg.js → arrow-left-Cz7XgLgo.js} +1 -1
- package/dist/webdist/assets/{badge-Df-MaEdJ.js → badge-CRVn2top.js} +1 -1
- package/dist/webdist/assets/{blockDiagram-GPEHLZMM-W3tqVN_9.js → blockDiagram-GPEHLZMM-COy0nYNX.js} +1 -1
- package/dist/webdist/assets/{bot-D2sMRmrf.js → bot-C-sGABri.js} +1 -1
- package/dist/webdist/assets/{c4Diagram-AAUBKEIU-Ct_fQADG.js → c4Diagram-AAUBKEIU-CoiZua-v.js} +1 -1
- package/dist/webdist/assets/channel-8eXCTkKl.js +1 -0
- package/dist/webdist/assets/{chat-api-DS1CWqFc.js → chat-api-CdVOTSst.js} +1 -1
- package/dist/webdist/assets/{chat-queries-BNnEQoUl.js → chat-queries-DINfS8uv.js} +1 -1
- package/dist/webdist/assets/check-Do9IgpFj.js +1 -0
- package/dist/webdist/assets/{chevron-left-Cg7-M3fC.js → chevron-left-CbPqt11D.js} +1 -1
- package/dist/webdist/assets/{chevron-right-B44Qnalv.js → chevron-right-C8EPnhrg.js} +1 -1
- package/dist/webdist/assets/{chunk-2J33WTMH-BhQKciZ6.js → chunk-2J33WTMH-B-ggs358.js} +1 -1
- package/dist/webdist/assets/{chunk-4BX2VUAB-CPVQRcKK.js → chunk-4BX2VUAB-CBSHARpe.js} +1 -1
- package/dist/webdist/assets/{chunk-55IACEB6-BeD2m5Zd.js → chunk-55IACEB6-BikCaTgS.js} +1 -1
- package/dist/webdist/assets/{chunk-727SXJPM-1eXg0vd4.js → chunk-727SXJPM-B8Uooh_X.js} +1 -1
- package/dist/webdist/assets/{chunk-AQP2D5EJ-BI_WWvfb.js → chunk-AQP2D5EJ-C-wZiAmx.js} +1 -1
- package/dist/webdist/assets/{chunk-FMBD7UC4-DmkZ5E8i.js → chunk-FMBD7UC4-lkRMU7V5.js} +1 -1
- package/dist/webdist/assets/{chunk-ND2GUHAM-DgKanzz6.js → chunk-ND2GUHAM-CTBUDTXK.js} +1 -1
- package/dist/webdist/assets/{chunk-QZHKN3VN-C0-EOMS6.js → chunk-QZHKN3VN-CKOXFQ-c.js} +1 -1
- package/dist/webdist/assets/{circle-check-CXWe3y-e.js → circle-check-dbo4u23c.js} +1 -1
- package/dist/webdist/assets/{circle-x-BYUS86Mo.js → circle-x-CuPWHB4Y.js} +1 -1
- package/dist/webdist/assets/classDiagram-4FO5ZUOK-D5bPDMhb.js +1 -0
- package/dist/webdist/assets/classDiagram-v2-Q7XG4LA2-D5bPDMhb.js +1 -0
- package/dist/webdist/assets/collapsible-tffcj0HQ.js +1 -0
- package/dist/webdist/assets/{common-ClITqWVP.js → common-RZkQjs09.js} +1 -1
- package/dist/webdist/assets/{copy-button-DOnlD7sA.js → copy-button-DKUAZ6Ig.js} +1 -1
- package/dist/webdist/assets/{cose-bilkent-S5V4N54A-CJ-OWUMU.js → cose-bilkent-S5V4N54A-L3BPlYev.js} +1 -1
- package/dist/webdist/assets/{dagre-BM42HDAG-CP41T21v.js → dagre-BM42HDAG-DdFZFhxI.js} +1 -1
- package/dist/webdist/assets/{diagram-2AECGRRQ-CniIf6PF.js → diagram-2AECGRRQ-Bx4JyVtd.js} +1 -1
- package/dist/webdist/assets/{diagram-5GNKFQAL-C81Uep65.js → diagram-5GNKFQAL-DYxtCUYO.js} +1 -1
- package/dist/webdist/assets/{diagram-KO2AKTUF-BcWsZVCH.js → diagram-KO2AKTUF-BSV0zTVR.js} +1 -1
- package/dist/webdist/assets/{diagram-LMA3HP47-D4gsQ5wi.js → diagram-LMA3HP47-CS-L_g7_.js} +1 -1
- package/dist/webdist/assets/{diagram-OG6HWLK6-CKw2XvNh.js → diagram-OG6HWLK6-D8uK9mOu.js} +1 -1
- package/dist/webdist/assets/dialog-fsfhY_Ib.js +1 -0
- package/dist/webdist/assets/dropdown-menu-B6pYEdad.js +1 -0
- package/dist/webdist/assets/{erDiagram-TEJ5UH35-DiMRASCw.js → erDiagram-TEJ5UH35-DX-3blsY.js} +1 -1
- package/dist/webdist/assets/{flowDiagram-I6XJVG4X-OYvJz7NK.js → flowDiagram-I6XJVG4X-DUTsLqgt.js} +1 -1
- package/dist/webdist/assets/{ganttDiagram-6RSMTGT7-TNBmycEa.js → ganttDiagram-6RSMTGT7-CwH0oq0S.js} +1 -1
- package/dist/webdist/assets/{git-branch-CpH7bdLO.js → git-branch-BiMfcJ1j.js} +1 -1
- package/dist/webdist/assets/{gitGraphDiagram-PVQCEYII-BhEsrTFm.js → gitGraphDiagram-PVQCEYII-CmLeqvWh.js} +1 -1
- package/dist/webdist/assets/{index-Cl9fRADn.js → index-BP-7GzAI.js} +1 -1
- package/dist/webdist/assets/index-CXwfn7-U.css +1 -0
- package/dist/webdist/assets/{index-BGFp5nvZ.js → index-DaNuMYPC.js} +4 -4
- package/dist/webdist/assets/{infoDiagram-5YYISTIA-BDEgDbkv.js → infoDiagram-5YYISTIA-BFTUzkbq.js} +1 -1
- package/dist/webdist/assets/{ishikawaDiagram-YF4QCWOH-JTmVtiNo.js → ishikawaDiagram-YF4QCWOH-BdQIrqQU.js} +1 -1
- package/dist/webdist/assets/{journeyDiagram-JHISSGLW-CSlH-DCp.js → journeyDiagram-JHISSGLW-D1HmHaNc.js} +1 -1
- package/dist/webdist/assets/{kanban-definition-UN3LZRKU-BIfzX5mh.js → kanban-definition-UN3LZRKU-C5USgc8m.js} +1 -1
- package/dist/webdist/assets/{linear-Csu7LYv_.js → linear-BDGxyxLo.js} +1 -1
- package/dist/webdist/assets/{lock-Dx47ssSz.js → lock-DCb387O_.js} +1 -1
- package/dist/webdist/assets/{mermaid.core-BF2_JyC4.js → mermaid.core-DWCJxsCl.js} +4 -4
- package/dist/webdist/assets/{mindmap-definition-RKZ34NQL-C4HOUf0T.js → mindmap-definition-RKZ34NQL-BV3H5rWG.js} +1 -1
- package/dist/webdist/assets/{pencil-DiDWTtDe.js → pencil-BegDY0Zk.js} +1 -1
- package/dist/webdist/assets/{personas-K7vGADki.js → personas-DNCENHUd.js} +6 -0
- package/dist/webdist/assets/{pieDiagram-4H26LBE5-Civ4ky7C.js → pieDiagram-4H26LBE5-DDN9VAOL.js} +1 -1
- package/dist/webdist/assets/{plus-rMCCWOjM.js → plus-DrZmKBTt.js} +1 -1
- package/dist/webdist/assets/{quadrantDiagram-W4KKPZXB-Cu5aaIeO.js → quadrantDiagram-W4KKPZXB-Bx16Ru8w.js} +1 -1
- package/dist/webdist/assets/{radix-C-sz7VcM.js → radix-j_81dozL.js} +1 -1
- package/dist/webdist/assets/{refresh-cw-zhCl4OJc.js → refresh-cw-B7Qjdj47.js} +1 -1
- package/dist/webdist/assets/{requirementDiagram-4Y6WPE33-owXfcooA.js → requirementDiagram-4Y6WPE33-k0upGaul.js} +1 -1
- package/dist/webdist/assets/{rotate-ccw-DSFeEfs1.js → rotate-ccw-B7o8hLk6.js} +1 -1
- package/dist/webdist/assets/{sankeyDiagram-5OEKKPKP-B7o6U2uP.js → sankeyDiagram-5OEKKPKP-xyOWBpSr.js} +1 -1
- package/dist/webdist/assets/{sequenceDiagram-3UESZ5HK-D0g36FR_.js → sequenceDiagram-3UESZ5HK-BhBclCRW.js} +1 -1
- package/dist/webdist/assets/skeleton-D15eL_7e.js +1 -0
- package/dist/webdist/assets/{sliders-horizontal-YIE_L0IY.js → sliders-horizontal-C4_t3PES.js} +1 -1
- package/dist/webdist/assets/{sparkles-FpLiZDCb.js → sparkles-BT45JGMR.js} +1 -1
- package/dist/webdist/assets/{star-D0lj4klT.js → star-SskkfhIb.js} +1 -1
- package/dist/webdist/assets/{stateDiagram-AJRCARHV-BGbyDa8P.js → stateDiagram-AJRCARHV-902Ow1i4.js} +1 -1
- package/dist/webdist/assets/stateDiagram-v2-BHNVJYJU-B5LKF3CJ.js +1 -0
- package/dist/webdist/assets/{switch-D7jMdkCs.js → switch-C-l-ej5M.js} +1 -1
- package/dist/webdist/assets/{terminal-Dfyn1hK2.js → terminal-55Og1Fyx.js} +1 -1
- package/dist/webdist/assets/{timeline-definition-PNZ67QCA-DlGxVI3e.js → timeline-definition-PNZ67QCA-BEyTzrTZ.js} +1 -1
- package/dist/webdist/assets/{trash-2-DpcKQJdU.js → trash-2-B9nMCHMA.js} +1 -1
- package/dist/webdist/assets/{useIsDesktop-BuJChlQF.js → useIsDesktop-D2KX8NOA.js} +1 -1
- package/dist/webdist/assets/{vennDiagram-CIIHVFJN-D8cIiNWM.js → vennDiagram-CIIHVFJN-C77U6Nzk.js} +1 -1
- package/dist/webdist/assets/{wardley-L42UT6IY-wEcDRtc5.js → wardley-L42UT6IY-BZ10FiAs.js} +1 -1
- package/dist/webdist/assets/{wardleyDiagram-YWT4CUSO-E26Pag8Q.js → wardleyDiagram-YWT4CUSO-DgrAaqyf.js} +1 -1
- package/dist/webdist/assets/{wrench-CNEP_hb3.js → wrench-DMY_WSQ9.js} +1 -1
- package/dist/webdist/assets/{xychartDiagram-2RQKCTM6-lyn53sKq.js → xychartDiagram-2RQKCTM6-BlzVJUn3.js} +1 -1
- package/dist/webdist/index.html +3 -3
- package/package.json +8 -2
- package/dist/webdist/assets/CodeComposer-Bj-OBoKJ.js +0 -1
- package/dist/webdist/assets/CodeSessionScreen-BqvzH7ux.js +0 -5
- package/dist/webdist/assets/ModelLoadMenu-CDHS7gAw.js +0 -2
- package/dist/webdist/assets/SettingsScreen-BhqvsFo_.js +0 -1
- package/dist/webdist/assets/WorkspaceScreen-BlWA58me.js +0 -1
- package/dist/webdist/assets/channel-Cg9TpvpM.js +0 -1
- package/dist/webdist/assets/check-d_QTpKyE.js +0 -1
- package/dist/webdist/assets/classDiagram-4FO5ZUOK-CUIGvGnh.js +0 -1
- package/dist/webdist/assets/classDiagram-v2-Q7XG4LA2-CUIGvGnh.js +0 -1
- package/dist/webdist/assets/collapsible-CSr2FFbS.js +0 -1
- package/dist/webdist/assets/dialog-CwglpOgK.js +0 -1
- package/dist/webdist/assets/external-link-vubZvvSL.js +0 -1
- package/dist/webdist/assets/index-mhXL7TR_.css +0 -1
- package/dist/webdist/assets/skeleton-B6viaKty.js +0 -1
- 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-
|
|
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
|
|
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
|
|
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
|
|
678
|
-
|
|
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(
|
|
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(
|
|
2217
|
-
if (!
|
|
2218
|
-
const e =
|
|
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:
|
|
3683
|
-
cores:
|
|
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
|
|
3760
|
-
|
|
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
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
}
|
|
3841
|
-
function
|
|
3842
|
-
|
|
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
|
|
3846
|
-
|
|
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
|
|
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
|
|
7024
|
-
|
|
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(
|
|
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
|
|
7555
|
-
this.state = { running: false, modelKey, done: true, error:
|
|
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
|
-
(
|
|
8324
|
-
if (
|
|
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 }, (
|
|
8353
|
-
resolve5(
|
|
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
|
|
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
|
-
/**
|
|
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 =
|
|
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) =>
|
|
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
|
|
8529
|
-
* skipped; if every
|
|
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 =
|
|
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 (
|
|
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 (
|
|
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: (
|
|
10253
|
+
giveUp: (err7) => {
|
|
9991
10254
|
if (settled) return;
|
|
9992
10255
|
settled = true;
|
|
9993
10256
|
signal?.removeEventListener("abort", onAbort);
|
|
9994
10257
|
clearTimeout(timer);
|
|
9995
|
-
rejectPromise(
|
|
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: {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
10542
|
-
return typeof
|
|
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
|
|
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
|
|
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
|
|
10753
|
-
import { execFileSync as
|
|
10754
|
-
import { basename as basename3, dirname as dirname9, join as
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
13238
|
-
const binDir =
|
|
13239
|
-
if (existsSync20(
|
|
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 =
|
|
13255
|
-
|
|
13256
|
-
|
|
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
|
-
|
|
13259
|
-
|
|
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 =
|
|
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 =
|
|
13267
|
-
|
|
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 =
|
|
13271
|
-
const srcRoot = inner ??
|
|
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
|
-
|
|
14337
|
+
rmSync12(zip, { force: true });
|
|
13274
14338
|
}
|
|
13275
|
-
if (!existsSync20(
|
|
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
|
-
|
|
13279
|
-
|
|
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
|
-
|
|
13285
|
-
|
|
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
|
|
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
|
|
13361
|
-
import { join as
|
|
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(
|
|
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(
|
|
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 =
|
|
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
|
|
14520
|
+
function err3(c, status, code, message) {
|
|
13456
14521
|
return c.json({ error: { code, message } }, status);
|
|
13457
14522
|
}
|
|
13458
|
-
async function
|
|
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 =
|
|
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
|
|
13531
|
-
const { html, width } = await
|
|
13532
|
-
if (!html || typeof html !== "string") return
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
14654
|
+
const b = await body3(c);
|
|
13586
14655
|
const def = availableBackends().find((x) => x.id === b.backend);
|
|
13587
|
-
if (!def) return
|
|
13588
|
-
const root =
|
|
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
|
|
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
|
|
13632
|
-
const root =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
13691
|
-
const root =
|
|
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
|
|
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
|
|
13702
|
-
const root =
|
|
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
|
|
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
|
|
14791
|
+
if (busy) return err3(c, 409, "engine_already_running", busy);
|
|
13723
14792
|
}
|
|
13724
|
-
const root =
|
|
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
|
|
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
|
|
14814
|
+
if (busy) return err3(c, 409, "engine_already_running", busy);
|
|
13746
14815
|
}
|
|
13747
|
-
const root =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
13827
|
-
const b = await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
14915
|
+
if (busy) return err3(c, 409, "engine_already_running", busy);
|
|
13847
14916
|
const name = (b.name ?? "").trim() || void 0;
|
|
13848
|
-
const enginesRoot =
|
|
14917
|
+
const enginesRoot = join26(d.store.dir(), "engines");
|
|
13849
14918
|
const toolchainDirs = d.store.snapshot().build.toolchainDirs;
|
|
13850
|
-
const buildRoot =
|
|
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
|
-
|
|
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
|
|
14989
|
+
return err3(c, 403, "forbidden", "Downloading CUDA is only available on the machine running TurboLLM.");
|
|
13921
14990
|
if (process.platform !== "win32")
|
|
13922
|
-
return
|
|
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
|
|
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
|
|
15027
|
+
if (busy) return err3(c, 409, "engine_already_running", busy);
|
|
13959
15028
|
}
|
|
13960
|
-
const root =
|
|
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
|
|
15047
|
+
if (busy) return err3(c, 409, "engine_already_running", busy);
|
|
13979
15048
|
}
|
|
13980
|
-
const root =
|
|
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
|
|
15066
|
+
if (!entry?.repo) return err3(c, 500, "internal", "TurboQuant catalog entry is misconfigured.");
|
|
13998
15067
|
if (!entry.platforms.includes(process.platform)) {
|
|
13999
|
-
return
|
|
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
|
|
15072
|
+
if (busy) return err3(c, 409, "engine_already_running", busy);
|
|
14004
15073
|
}
|
|
14005
|
-
const root =
|
|
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 =
|
|
14012
|
-
if (existsSync22(tqDir))
|
|
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
|
|
15101
|
+
if (!entry?.repo) return err3(c, 500, "internal", "KoboldCpp catalog entry is misconfigured.");
|
|
14033
15102
|
if (!entry.platforms.includes(process.platform)) {
|
|
14034
|
-
return
|
|
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
|
|
15107
|
+
if (busy) return err3(c, 409, "engine_already_running", busy);
|
|
14039
15108
|
}
|
|
14040
|
-
const root =
|
|
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))
|
|
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
|
|
15132
|
+
if (!entry?.repo) return err3(c, 500, "internal", "llamafile catalog entry is misconfigured.");
|
|
14064
15133
|
if (!entry.platforms.includes(process.platform)) {
|
|
14065
|
-
return
|
|
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
|
|
15138
|
+
if (busy) return err3(c, 409, "engine_already_running", busy);
|
|
14070
15139
|
}
|
|
14071
|
-
const root =
|
|
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))
|
|
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
|
|
14094
|
-
const b = await
|
|
14095
|
-
if (!b.binPath || !b.binPath.trim()) return
|
|
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
|
|
14108
|
-
if (e instanceof ProbeError) return
|
|
14109
|
-
return
|
|
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
|
|
14115
|
-
const b = await
|
|
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
|
|
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
|
|
14131
|
-
return
|
|
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
|
|
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
|
|
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 =
|
|
15247
|
+
const enginesRoot = join26(d.store.dir(), "engines");
|
|
14179
15248
|
const purgeDir = engineInstallDir(eng, enginesRoot);
|
|
14180
15249
|
if (purgeDir && existsSync22(purgeDir)) {
|
|
14181
|
-
|
|
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
|
|
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
|
|
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
|
|
15306
|
+
const b = await body3(c);
|
|
14238
15307
|
if (b.policy !== "off" && b.policy !== "notify" && b.policy !== "auto") {
|
|
14239
|
-
return
|
|
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
|
|
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 =
|
|
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(
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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) =>
|
|
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
|
|
15379
|
+
const b = await body3(c);
|
|
14311
15380
|
const active = d.registry.active();
|
|
14312
|
-
if (!active) return
|
|
14313
|
-
if (d.comfy?.isBlocked()) return
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
15475
|
+
const b = await body3(c);
|
|
14407
15476
|
const raw = (b.path ?? "").trim();
|
|
14408
|
-
if (!raw) return
|
|
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
|
|
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(
|
|
15484
|
+
else if (existsSync22(join26(root, "custom_nodes"))) customNodes = join26(root, "custom_nodes");
|
|
14416
15485
|
else {
|
|
14417
|
-
return
|
|
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 =
|
|
15491
|
+
const gateDir = join26(customNodes, "turbollm_gate");
|
|
14423
15492
|
try {
|
|
14424
|
-
|
|
14425
|
-
|
|
15493
|
+
mkdirSync14(gateDir, { recursive: true });
|
|
15494
|
+
writeFileSync12(join26(gateDir, "__init__.py"), gateNodeSource(base2));
|
|
14426
15495
|
} catch (e) {
|
|
14427
|
-
return
|
|
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
|
-
|
|
15507
|
+
rmSync13(dir, { recursive: true, force: true });
|
|
14439
15508
|
} catch (e) {
|
|
14440
|
-
return
|
|
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
|
|
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 =
|
|
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
|
|
15552
|
+
const b = await body3(c);
|
|
14484
15553
|
const key = (b.modelKey ?? "").trim();
|
|
14485
|
-
if (!key) return
|
|
14486
|
-
if (d.comfy?.isBlocked()) return
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
14534
|
-
return
|
|
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
|
|
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
|
|
14551
|
-
const p = await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
15844
|
+
const b = await body3(c);
|
|
14767
15845
|
const transport = b.transport === "sse" ? "sse" : "stdio";
|
|
14768
|
-
if (!b.name?.trim()) return
|
|
14769
|
-
if (transport === "stdio" && !b.command?.trim()) return
|
|
14770
|
-
if (transport === "sse" && !b.url?.trim()) return
|
|
14771
|
-
if (transport === "sse" && !/^https?:\/\//i.test(b.url ?? "")) return
|
|
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:
|
|
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
|
|
15875
|
+
const b = await body3(c);
|
|
14798
15876
|
const cfg2 = d.store.snapshot();
|
|
14799
|
-
if (!cfg2.mcp.servers.some((s) => s.id === id)) return
|
|
14800
|
-
if (b.transport && b.transport !== "stdio" && b.transport !== "sse") return
|
|
14801
|
-
if (b.url && !/^https?:\/\//i.test(b.url)) return
|
|
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
|
|
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
|
|
15926
|
+
const b = await body3(c);
|
|
14849
15927
|
const dir = (b.dir ?? "").trim();
|
|
14850
|
-
if (!dir || !/^([a-zA-Z]:[\\/]|[\\/])/.test(dir)) return
|
|
14851
|
-
if (!existsSync22(dir)) return
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
14982
|
-
const b = await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
16260
|
+
return err3(c, status, e.code, e.message);
|
|
15183
16261
|
}
|
|
15184
|
-
return
|
|
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
|
|
16267
|
+
return err3(c, status, e.code, e.message);
|
|
15190
16268
|
}
|
|
15191
|
-
return
|
|
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
|
|
16274
|
+
return err3(c, status, e.code, e.message);
|
|
15197
16275
|
}
|
|
15198
|
-
return
|
|
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
|
|
15210
|
-
if (e instanceof ValueError) return
|
|
15211
|
-
return
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
16534
|
+
function err4(c, status, code, message) {
|
|
15457
16535
|
return c.json({ error: { code, message } }, status);
|
|
15458
16536
|
}
|
|
15459
|
-
async function
|
|
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
|
|
15475
|
-
const b = await
|
|
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
|
|
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
|
|
15500
|
-
if (!b.name?.trim()) return
|
|
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
|
|
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
|
|
16598
|
+
return err4(c, 404, "not_found", "Agent not found.");
|
|
15521
16599
|
}
|
|
15522
|
-
const b = await
|
|
15523
|
-
if (b.name !== void 0 && !b.name.trim()) return
|
|
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
|
|
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
|
|
16625
|
+
function err5(c, status, code, message) {
|
|
15548
16626
|
return c.json({ error: { code, message } }, status);
|
|
15549
16627
|
}
|
|
15550
|
-
async function
|
|
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
|
|
15563
|
-
const b = await
|
|
15564
|
-
if (!b.id?.trim() || !/^[a-z0-9-]+$/.test(b.id)) return
|
|
15565
|
-
if (!b.name?.trim()) return
|
|
15566
|
-
if (isBuiltinSkill(b.id)) return
|
|
15567
|
-
if (!b.instructions?.trim()) return
|
|
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
|
|
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
|
|
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
|
|
15584
|
-
if (isBuiltinSkill(id)) return
|
|
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
|
|
15590
|
-
const b = await
|
|
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
|
|
16670
|
+
if (!text) return err5(c, 400, "invalid_input", "text is required.");
|
|
15593
16671
|
const draft = fromSkillMd("draft", text);
|
|
15594
|
-
if (!draft?.name) return
|
|
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
|
|
15597
|
-
if (isBuiltinSkill(id)) return
|
|
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
|
|
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
|
|
15607
|
-
const b = await
|
|
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
|
|
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
|
|
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-
|
|
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
|
|
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 = (
|
|
16815
|
+
const onError = (err7) => {
|
|
15738
16816
|
if (settled) return;
|
|
15739
16817
|
settled = true;
|
|
15740
16818
|
cleanup();
|
|
15741
|
-
reject(
|
|
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
|
|
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 ?
|
|
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
|
|
15958
|
-
import { join as
|
|
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 =
|
|
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(
|
|
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 =
|
|
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:
|
|
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:
|
|
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 =
|
|
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:
|
|
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 =
|
|
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:
|
|
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 =
|
|
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:
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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,
|
|
19152
|
+
function fence(lang, body7) {
|
|
18074
19153
|
return `\`\`\`${lang}
|
|
18075
|
-
${
|
|
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
|
|
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
|
-
${
|
|
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
|
|
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
|
-
${
|
|
19431
|
+
${body7}${trunc}`;
|
|
18353
19432
|
}
|
|
18354
19433
|
|
|
18355
19434
|
// src/code/code-routes.ts
|
|
18356
|
-
function
|
|
19435
|
+
function err6(c, s, code, msg2) {
|
|
18357
19436
|
return c.json({ error: { code, message: msg2 } }, s);
|
|
18358
19437
|
}
|
|
18359
|
-
async function
|
|
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
|
|
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
|
|
18439
|
-
if (!task) return
|
|
18440
|
-
if (!VALID_MODES.has(mode)) return
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
18530
|
-
const b = await
|
|
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
|
|
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
|
|
18540
|
-
const b = await
|
|
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
|
|
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
|
|
18550
|
-
const b = await
|
|
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
|
|
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
|
|
18560
|
-
if (runs.isActive(id)) return
|
|
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
|
|
18569
|
-
if (!run.repoRoot) return
|
|
18570
|
-
if (runs.isActive(id)) return
|
|
18571
|
-
if (run.clearedUpToMessageId) return
|
|
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
|
|
18574
|
-
const b = await
|
|
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
|
|
18587
|
-
if (message === "session_cleared") return
|
|
18588
|
-
return
|
|
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
|
|
18595
|
-
if (runs.isActive(id)) return
|
|
18596
|
-
if (run.revertedFromMessageId) return
|
|
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
|
|
18600
|
-
if (run.clearedUpToMessageId === lastMsg.id) return
|
|
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
|
|
18609
|
-
if (runs.isActive(id)) return
|
|
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
|
|
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
|
|
19719
|
+
const b = await body6(c);
|
|
18623
19720
|
const messageId = (b.messageId ?? "").trim();
|
|
18624
|
-
if (!messageId) return
|
|
19721
|
+
if (!messageId) return err6(c, 400, "invalid_input", "messageId is required.");
|
|
18625
19722
|
const run = db2.getAgentRun(id);
|
|
18626
|
-
if (!run) return
|
|
18627
|
-
if (!run.repoRoot) return
|
|
18628
|
-
if (runs.isActive(id)) return
|
|
18629
|
-
if (run.clearedUpToMessageId) return
|
|
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
|
|
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
|
|
18636
|
-
if (cut.error === "not_a_user_message") return
|
|
18637
|
-
return
|
|
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
|
|
18654
|
-
if (!run.repoRoot) return
|
|
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
|
|
18662
|
-
if (!run.repoRoot) return
|
|
18663
|
-
if (runs.isActive(id)) return
|
|
18664
|
-
const b = await
|
|
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
|
|
18671
|
-
if (/not a git repository/i.test(message)) return
|
|
18672
|
-
if (/nothing to commit|nothing staged/i.test(message)) return
|
|
18673
|
-
if (/outside the repo/i.test(message)) return
|
|
18674
|
-
return
|
|
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
|
|
18681
|
-
if (!run.repoRoot) return
|
|
18682
|
-
if (runs.isActive(id)) return
|
|
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
|
|
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
|
|
18694
|
-
if (!run.repoRoot) return
|
|
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
|
|
18704
|
-
if (!run.repoRoot) return
|
|
18705
|
-
if (runs.isActive(id)) return
|
|
18706
|
-
const b = await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
18757
|
-
if (!run.repoRoot) return
|
|
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
|
|
18760
|
-
if (!d.manager.target()) return
|
|
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
|
|
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
|
|
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)
|
|
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({
|
|
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
|
-
{
|
|
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
|
|
20364
|
+
const { message, type } = await describeEngineError(res);
|
|
19179
20365
|
return c.json(
|
|
19180
|
-
{ type: "error", error: { type:
|
|
19181
|
-
|
|
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({
|
|
19193
|
-
|
|
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
|
|
19312
|
-
const isAbort =
|
|
19313
|
-
const cause =
|
|
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." : `${
|
|
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({
|
|
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,
|
|
20573
|
+
async function recordOpenAiJsonUsage(d, body7, source, modelKey, codeSessionId = null, startedAt2 = null) {
|
|
19354
20574
|
try {
|
|
19355
|
-
const text = await new Response(
|
|
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,
|
|
20581
|
+
async function recordOpenAiStreamUsage(d, body7, source, modelKey, codeSessionId = null, startedAt2 = null) {
|
|
19362
20582
|
try {
|
|
19363
|
-
const reader =
|
|
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({
|
|
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 =
|
|
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(
|
|
20683
|
+
let file = normalize2(join30(WEB_ROOT, path || "index.html"));
|
|
19452
20684
|
if (!file.startsWith(WEB_ROOT) || !existsSync24(file) || statSync6(file).isDirectory()) {
|
|
19453
|
-
file =
|
|
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(
|
|
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
|
|
19482
|
-
import { mkdirSync as
|
|
19483
|
-
import { join as
|
|
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
|
|
19487
|
-
import { join as
|
|
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
|
|
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
|
|
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
|
-
|
|
20757
|
+
mkdirSync15(dir, { recursive: true });
|
|
19526
20758
|
onProgress?.({ phase: "downloading", pct: 0 });
|
|
19527
20759
|
if (asset.name.endsWith(".tgz")) {
|
|
19528
|
-
const tmp =
|
|
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
|
-
|
|
20765
|
+
rmSync14(tmp, { force: true });
|
|
19534
20766
|
} catch (e) {
|
|
19535
|
-
|
|
19536
|
-
|
|
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
|
|
20804
|
+
return join32(dataDir, "run");
|
|
19573
20805
|
}
|
|
19574
20806
|
function tunnelPidFile(dataDir, pid) {
|
|
19575
|
-
return
|
|
20807
|
+
return join32(tunnelPidDir(dataDir), `tunnel-${pid}.pid`);
|
|
19576
20808
|
}
|
|
19577
20809
|
function writeTunnelPid(dataDir, pid) {
|
|
19578
20810
|
try {
|
|
19579
|
-
|
|
19580
|
-
|
|
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
|
-
|
|
20818
|
+
rmSync15(tunnelPidFile(dataDir, pid), { force: true });
|
|
19587
20819
|
} catch {
|
|
19588
20820
|
}
|
|
19589
20821
|
}
|
|
19590
|
-
function
|
|
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 =
|
|
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 =
|
|
20841
|
+
const file = join32(dir, name);
|
|
19610
20842
|
try {
|
|
19611
|
-
const { pid, owner } = JSON.parse(
|
|
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
|
|
20845
|
+
else rmSync15(file, { force: true });
|
|
19614
20846
|
} catch {
|
|
19615
20847
|
try {
|
|
19616
|
-
|
|
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 &&
|
|
20858
|
+
if (owner && pidAlive3(owner)) continue;
|
|
19627
20859
|
try {
|
|
19628
|
-
if (process.platform === "win32")
|
|
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
|
-
|
|
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")
|
|
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
|
-
|
|
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 =
|
|
19743
|
-
version = JSON.parse(
|
|
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(
|
|
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 =
|
|
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(
|
|
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 {
|