nolo-cli 0.7.0 → 0.8.0-alpha.11
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/_executeModel-GPU2276G.js +198 -0
- package/action-PAXRPJJF.js +27 -0
- package/addContentAction-CZVWF3QZ.js +25 -0
- package/addDialogAgentAction-CCIDTPHC.js +67 -0
- package/agentDeleteCommand-4ZZFM6AT.js +323 -0
- package/agentEmailCommands-C4FIVQYV.js +621 -0
- package/agentListCommands-VQAKVNEX.js +219 -0
- package/agentMachineCommands-ONFDXZUQ.js +421 -0
- package/agentPullCommand-O2MTUAFR.js +122 -0
- package/agentRecordCommands-QANUGHLK.js +89 -0
- package/agentRunCommand-A3OSRB5Q.js +102 -0
- package/agentRunControl-K4VKNEHF.js +57 -0
- package/agentRuntimeLocal-62ZB2ODP.js +99 -0
- package/agentSlice-GCGLVR2N.js +41 -0
- package/authCommand-WXMD3IPR.js +574 -0
- package/authCommands-X74AFDTC.js +26 -0
- package/chunk-2EISRLRC.js +238 -0
- package/chunk-2FTTDCD4.js +675 -0
- package/chunk-2MALU52L.js +51 -0
- package/chunk-2UZXR7WO.js +3631 -0
- package/chunk-3ASDTTEI.js +1774 -0
- package/chunk-3EUW3WEO.js +8 -0
- package/chunk-3LY4F7SV.js +9293 -0
- package/chunk-4CQLVGRV.js +50 -0
- package/chunk-4IJYA27C.js +342 -0
- package/chunk-4SN6J65J.js +38 -0
- package/chunk-4VNS5WPM.js +42 -0
- package/chunk-53DNV5AT.js +421 -0
- package/chunk-572EKYLF.js +792 -0
- package/chunk-5MIDPSSK.js +206 -0
- package/chunk-5MMAK75Q.js +15 -0
- package/chunk-63W5N6P3.js +150 -0
- package/chunk-6B7G54GE.js +1078 -0
- package/chunk-6HYVJV7B.js +102 -0
- package/chunk-6WD3OOJS.js +20 -0
- package/chunk-7OC5VCLW.js +929 -0
- package/chunk-7YLKNMOM.js +562 -0
- package/chunk-ATNTHIZI.js +13 -0
- package/chunk-AUGHVUFE.js +69 -0
- package/chunk-AVMKZHBC.js +85 -0
- package/chunk-AZPCASRE.js +36 -0
- package/chunk-BR2WP3DF.js +548 -0
- package/chunk-BSTNFPHD.js +231 -0
- package/chunk-CF2WZF4N.js +134 -0
- package/chunk-CF7PE763.js +10 -0
- package/chunk-CJH2VTEU.js +360 -0
- package/chunk-CKOJQDKR.js +198 -0
- package/chunk-CM54RSSA.js +206 -0
- package/chunk-CQAHS7YO.js +4300 -0
- package/chunk-CWMSB67P.js +34 -0
- package/chunk-DFFIZ25G.js +30 -0
- package/chunk-DWLEOOGR.js +8 -0
- package/chunk-DZ3XJVET.js +231 -0
- package/chunk-EHLHSX7R.js +1433 -0
- package/chunk-ELRRLDWI.js +478 -0
- package/chunk-EYEEHCU4.js +3066 -0
- package/chunk-F6QQI5YI.js +19 -0
- package/chunk-G3SUCT4W.js +8 -0
- package/chunk-G5H7RNFL.js +807 -0
- package/chunk-GAKEBQD6.js +30 -0
- package/chunk-GBRG75BQ.js +86 -0
- package/chunk-GCMRDRCQ.js +190 -0
- package/chunk-GGUO4FKF.js +27158 -0
- package/chunk-GGXABIPP.js +628 -0
- package/chunk-H2RMRZJE.js +113 -0
- package/chunk-H33743XH.js +25 -0
- package/chunk-HEIIIZBX.js +104 -0
- package/chunk-HNMXYYJR.js +12 -0
- package/chunk-I3D64LRJ.js +1447 -0
- package/chunk-IAXBCTTE.js +175 -0
- package/chunk-IDZTNRP2.js +85 -0
- package/chunk-ISNCLGUZ.js +146 -0
- package/chunk-IYQV26RA.js +138 -0
- package/chunk-J4576WMJ.js +91 -0
- package/chunk-J5KV47ZW.js +222 -0
- package/chunk-JBTJAPUF.js +35 -0
- package/chunk-JZPBVBIY.js +94 -0
- package/chunk-KB7YFGL6.js +34 -0
- package/chunk-KKY4B2Q2.js +21 -0
- package/chunk-KQHGZWSV.js +63 -0
- package/chunk-KUG52IAP.js +8 -0
- package/chunk-KX6YBIHG.js +8 -0
- package/chunk-LAWBMYBC.js +171 -0
- package/chunk-LCBAMY2M.js +60 -0
- package/chunk-LM25IZBJ.js +902 -0
- package/chunk-LM6IYQ3F.js +16 -0
- package/chunk-M76ECQRD.js +13658 -0
- package/chunk-MRC4Q34M.js +188 -0
- package/chunk-NXZGF7F7.js +2878 -0
- package/chunk-OF5H3QWS.js +300 -0
- package/chunk-OLZBQWZU.js +6 -0
- package/chunk-OWOHNGCN.js +50 -0
- package/chunk-PN7AFLOK.js +3401 -0
- package/chunk-QR5IPGT3.js +24674 -0
- package/chunk-QSJ3HFEF.js +8 -0
- package/chunk-QYXLLLGL.js +4519 -0
- package/chunk-R2HXWML7.js +1949 -0
- package/chunk-ROWQEJRP.js +139 -0
- package/chunk-S7Z2HMCN.js +25 -0
- package/chunk-SYOF5TAS.js +962 -0
- package/chunk-TDIQFT2F.js +301 -0
- package/chunk-TILFE6LV.js +381 -0
- package/chunk-TZNCFAUP.js +2157 -0
- package/chunk-U4TRT5V4.js +246 -0
- package/chunk-U6VC4MU4.js +8 -0
- package/chunk-UAD2WT54.js +64 -0
- package/chunk-USZMK5UT.js +105 -0
- package/chunk-VEJLAVTC.js +2052 -0
- package/chunk-VF4K2QE6.js +21 -0
- package/chunk-VHBWHNVT.js +59 -0
- package/chunk-VIDPVFP3.js +20 -0
- package/chunk-VP4WJWKW.js +8882 -0
- package/chunk-VQ2AJRGD.js +148 -0
- package/chunk-W4NVOSFF.js +37 -0
- package/chunk-WWC7I6TA.js +8 -0
- package/chunk-X7W4TNB4.js +1550 -0
- package/chunk-XBIUN5CR.js +191 -0
- package/chunk-XDVAYP26.js +537 -0
- package/chunk-XKIMRZO2.js +221 -0
- package/chunk-Y5GCAMGP.js +9 -0
- package/chunk-YOUZDXU7.js +9013 -0
- package/chunk-Z4NR32XR.js +50 -0
- package/chunk-ZNFJWEN4.js +56 -0
- package/cliExecutor-DNE7TCC4.js +36 -0
- package/clipboardy-BVE3LVEH.js +12001 -0
- package/createAgentAutomationAction-537X433B.js +864 -0
- package/createDialogAction-YUD4RKBS.js +52 -0
- package/createPageAction-KJMFHL4C.js +169 -0
- package/db-HCJ5PRB5.js +271 -0
- package/dbSlice-C4UJLRZP.js +59 -0
- package/deleteFile-4SKGPHOZ.js +41 -0
- package/dialogCommands-H74KJKHC.js +59 -0
- package/dialogSlice-ETSD4YWS.js +146 -0
- package/docCreateCommands-ADUSU34K.js +215 -0
- package/docDeleteCommands-ZXV3S5OS.js +176 -0
- package/docReadCommands-YYYI6LT7.js +129 -0
- package/docStore-ZBF5DDKR.js +96 -0
- package/docUpdateCommands-7MGJ3BYA.js +313 -0
- package/fetchMessages-JOCYIFKL.js +16 -0
- package/getPricing-PWPJ6MNV.js +30 -0
- package/handleSendMessageAction-MGKI62Y6.js +183 -0
- package/index.js +184 -112288
- package/lib-FYYBQIXS.js +19839 -0
- package/localDialogRead-D3BELSTF.js +26 -0
- package/localFirstLog-DHNOY2NA.js +19 -0
- package/localRuntimeAuthority-TEGCOWX4.js +471 -0
- package/localRuntimeDb-LGDF6FQP.js +12 -0
- package/machineCommands-RMTMN7VQ.js +774 -0
- package/machineInfo-M6KB6HZN.js +10 -0
- package/machineWsRunDispatch-UIP355FN.js +105 -0
- package/memoryCommands-O4MIPIWQ.js +476 -0
- package/models-XLEOET3E.js +9 -0
- package/offlineMarxistsAgentCommand-BCPJITJL.js +242 -0
- package/package.json +3 -25
- package/read-RIJCIVKX.js +17 -0
- package/readDocTool-GZD2N3LO.js +16 -0
- package/readlineWorkspace-CRNQRZ6D.js +5374 -0
- package/removeDialogAgentAction-CXLST3N6.js +72 -0
- package/runAgentClientLoop-2S334SGA.js +288 -0
- package/runtimeDoctorCommands-LRHLPDRC.js +289 -0
- package/setDialogExtraReferencesAction-LP6VYGU7.js +59 -0
- package/setPrimaryDialogAgentAction-D3GGH23T.js +79 -0
- package/settingSlice-J4I3Z5H7.js +198 -0
- package/skillSummaryMarker-42HKSXYY.js +9 -0
- package/slateToRenderMarkdown-Y5IYN73U.js +7 -0
- package/slateUtils-2M7MUTWX.js +19 -0
- package/spaceCommands-NDEWMFZJ.js +450 -0
- package/spaceSlice-RLB6RTHO.js +145 -0
- package/streamAgentChatTurn-Y2TTYPBD.js +4503 -0
- package/tableCommands-GOFP7QKA.js +1286 -0
- package/toolApiClient-QI3YBNWB.js +26 -0
- package/tools-YZYLY2ZQ.js +83 -0
- package/understanding-HCSFPE7R.js +318 -0
- package/updateCommands-VOSPKXFF.js +31 -0
- package/updateDialogTitleAction-LQV6QLBW.js +185 -0
|
@@ -0,0 +1,1550 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildCliPrompt
|
|
3
|
+
} from "./chunk-ATNTHIZI.js";
|
|
4
|
+
import {
|
|
5
|
+
resolveLaunchableCodexCommand
|
|
6
|
+
} from "./chunk-JBTJAPUF.js";
|
|
7
|
+
|
|
8
|
+
// packages/ai/agent/cliExecutor.ts
|
|
9
|
+
import { exec, execSync, spawn } from "child_process";
|
|
10
|
+
import { randomUUID } from "node:crypto";
|
|
11
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
12
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { StringDecoder } from "node:string_decoder";
|
|
15
|
+
var CliProviderQuotaError = class extends Error {
|
|
16
|
+
provider;
|
|
17
|
+
constructor(provider, message) {
|
|
18
|
+
super(`[QUOTA_LIMITED:${provider}] ${message}`);
|
|
19
|
+
this.name = "CliProviderQuotaError";
|
|
20
|
+
this.provider = provider;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
function detectCliProviderQuotaLimit(provider, stdout, stderr, exitCode) {
|
|
24
|
+
const stderrText = String(stderr || "");
|
|
25
|
+
const stdoutText = String(stdout || "");
|
|
26
|
+
const combined = stderrText + "\n" + stdoutText;
|
|
27
|
+
const isQuotaText = (text) => {
|
|
28
|
+
const t = text.toLowerCase();
|
|
29
|
+
const strongPatterns = [
|
|
30
|
+
/quota (exceeded|reached|limit|used up|insufficient|hit)/i,
|
|
31
|
+
/usage limit (reached|exceeded|hit)/i,
|
|
32
|
+
/weekly usage limit/i,
|
|
33
|
+
/rate.?limit (exceeded|reached|hit)/i,
|
|
34
|
+
/\b429\b.*(limit|quota|rate)/i,
|
|
35
|
+
/too many request/i,
|
|
36
|
+
/no (more|remaining) (credit|quota|usage)/i,
|
|
37
|
+
/plan limit|membership limit/i,
|
|
38
|
+
/daily limit|hourly limit reached/i,
|
|
39
|
+
/limit reached.*reset/i,
|
|
40
|
+
/resets in .* days/i
|
|
41
|
+
// from real opencode error
|
|
42
|
+
];
|
|
43
|
+
for (const re of strongPatterns) {
|
|
44
|
+
if (re.test(t)) return true;
|
|
45
|
+
}
|
|
46
|
+
return false;
|
|
47
|
+
};
|
|
48
|
+
if (isQuotaText(stderrText)) {
|
|
49
|
+
return { limited: true, message: `quota/rate limit signal from ${provider} (stderr)` };
|
|
50
|
+
}
|
|
51
|
+
if (isQuotaText(combined)) {
|
|
52
|
+
return { limited: true, message: `quota/rate limit signal from ${provider}` };
|
|
53
|
+
}
|
|
54
|
+
return { limited: false };
|
|
55
|
+
}
|
|
56
|
+
var DEFAULT_AGY_TIMEOUT_MS = 6e5;
|
|
57
|
+
var DEFAULT_OPENCODE_TIMEOUT_MS = 6e5;
|
|
58
|
+
var DEFAULT_GROK_TIMEOUT_MS = 6e5;
|
|
59
|
+
var BUFFERED_STREAMING_PROVIDERS = /* @__PURE__ */ new Set([
|
|
60
|
+
"codex",
|
|
61
|
+
"claude",
|
|
62
|
+
"agy",
|
|
63
|
+
"qoder",
|
|
64
|
+
"opencode",
|
|
65
|
+
"grok",
|
|
66
|
+
"kimi"
|
|
67
|
+
]);
|
|
68
|
+
function isBufferedCliStreamingProvider(provider) {
|
|
69
|
+
return BUFFERED_STREAMING_PROVIDERS.has(provider);
|
|
70
|
+
}
|
|
71
|
+
var cliSessions = /* @__PURE__ */ new Map();
|
|
72
|
+
function readMacSystemProxyEnv() {
|
|
73
|
+
if (process.platform !== "darwin") return {};
|
|
74
|
+
try {
|
|
75
|
+
const raw = String(execSync("scutil --proxy", { encoding: "utf8" }) || "");
|
|
76
|
+
const values = /* @__PURE__ */ new Map();
|
|
77
|
+
for (const line of raw.split("\n")) {
|
|
78
|
+
const match = line.match(/^\s*([A-Za-z0-9_]+)\s*:\s*(.+?)\s*$/);
|
|
79
|
+
if (!match) continue;
|
|
80
|
+
values.set(match[1], match[2].replace(/^"|"$/g, ""));
|
|
81
|
+
}
|
|
82
|
+
const httpEnabled = values.get("HTTPEnable") === "1";
|
|
83
|
+
const httpsEnabled = values.get("HTTPSEnable") === "1";
|
|
84
|
+
const httpHost = values.get("HTTPProxy")?.trim();
|
|
85
|
+
const httpsHost = values.get("HTTPSProxy")?.trim();
|
|
86
|
+
const httpPort = values.get("HTTPPort")?.trim();
|
|
87
|
+
const httpsPort = values.get("HTTPSPort")?.trim();
|
|
88
|
+
const noProxy = values.get("ExceptionsList")?.trim();
|
|
89
|
+
const httpProxy = httpEnabled && httpHost && httpPort ? `http://${httpHost}:${httpPort}` : "";
|
|
90
|
+
const httpsProxy = httpsEnabled && httpsHost && httpsPort ? `http://${httpsHost}:${httpsPort}` : "";
|
|
91
|
+
const allProxy = httpsProxy || httpProxy;
|
|
92
|
+
return {
|
|
93
|
+
...httpProxy ? { HTTP_PROXY: httpProxy, http_proxy: httpProxy } : {},
|
|
94
|
+
...httpsProxy ? { HTTPS_PROXY: httpsProxy, https_proxy: httpsProxy } : {},
|
|
95
|
+
...allProxy ? { ALL_PROXY: allProxy, all_proxy: allProxy } : {},
|
|
96
|
+
...noProxy ? { NO_PROXY: noProxy, no_proxy: noProxy } : {}
|
|
97
|
+
};
|
|
98
|
+
} catch {
|
|
99
|
+
return {};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function normalizeProxyEnv(env) {
|
|
103
|
+
const explicitHttp = env.HTTP_PROXY?.trim() || env.http_proxy?.trim() || "";
|
|
104
|
+
const explicitHttps = env.HTTPS_PROXY?.trim() || env.https_proxy?.trim() || "";
|
|
105
|
+
const explicitAll = env.ALL_PROXY?.trim() || env.all_proxy?.trim() || "";
|
|
106
|
+
const explicitNoProxy = env.NO_PROXY?.trim() || env.no_proxy?.trim() || "";
|
|
107
|
+
const fallback = explicitHttp || explicitHttps || explicitAll ? {} : readMacSystemProxyEnv();
|
|
108
|
+
const httpProxy = explicitHttp || fallback.HTTP_PROXY || fallback.http_proxy || "";
|
|
109
|
+
const httpsProxy = explicitHttps || fallback.HTTPS_PROXY || fallback.https_proxy || httpProxy;
|
|
110
|
+
const allProxy = explicitAll || fallback.ALL_PROXY || fallback.all_proxy || httpsProxy || httpProxy;
|
|
111
|
+
const noProxy = explicitNoProxy || fallback.NO_PROXY || fallback.no_proxy || "";
|
|
112
|
+
return {
|
|
113
|
+
...env,
|
|
114
|
+
...httpProxy ? { HTTP_PROXY: httpProxy, http_proxy: httpProxy } : {},
|
|
115
|
+
...httpsProxy ? { HTTPS_PROXY: httpsProxy, https_proxy: httpsProxy } : {},
|
|
116
|
+
...allProxy ? { ALL_PROXY: allProxy, all_proxy: allProxy } : {},
|
|
117
|
+
...noProxy ? { NO_PROXY: noProxy, no_proxy: noProxy } : {}
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
var CLI_PROBE_BINARIES = {
|
|
121
|
+
copilot: ["gh", "copilot"],
|
|
122
|
+
gemini: ["gemini"],
|
|
123
|
+
codex: ["codex"],
|
|
124
|
+
claude: ["claude"],
|
|
125
|
+
agy: ["agy"],
|
|
126
|
+
qoder: ["qoder"],
|
|
127
|
+
opencode: ["opencode"],
|
|
128
|
+
grok: ["grok"],
|
|
129
|
+
kimi: ["kimi"]
|
|
130
|
+
};
|
|
131
|
+
function buildCliProcessEnv(extraEnv) {
|
|
132
|
+
const baseEnv = {
|
|
133
|
+
...process.env,
|
|
134
|
+
...extraEnv,
|
|
135
|
+
NO_COLOR: "1"
|
|
136
|
+
};
|
|
137
|
+
if (process.platform === "darwin") {
|
|
138
|
+
const defaultMacPaths = [
|
|
139
|
+
`${process.env.HOME || ""}/.local/bin`,
|
|
140
|
+
"/opt/homebrew/bin",
|
|
141
|
+
"/opt/homebrew/sbin",
|
|
142
|
+
"/usr/local/bin",
|
|
143
|
+
"/usr/local/sbin",
|
|
144
|
+
`${process.env.HOME || ""}/.bun/bin`,
|
|
145
|
+
`${process.env.HOME || ""}/Library/pnpm`,
|
|
146
|
+
`${process.env.HOME || ""}/.npm-global/bin`,
|
|
147
|
+
`${process.env.HOME || ""}/bin`
|
|
148
|
+
];
|
|
149
|
+
const existingPath = baseEnv.PATH || "/usr/bin:/bin:/usr/sbin:/sbin";
|
|
150
|
+
const paths = existingPath.split(":");
|
|
151
|
+
for (const p of defaultMacPaths) {
|
|
152
|
+
if (p && !paths.includes(p)) {
|
|
153
|
+
paths.unshift(p);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
baseEnv.PATH = paths.join(":");
|
|
157
|
+
}
|
|
158
|
+
return Object.fromEntries(
|
|
159
|
+
Object.entries(normalizeProxyEnv(baseEnv)).filter(
|
|
160
|
+
(entry) => typeof entry[1] === "string"
|
|
161
|
+
)
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
function getCliProviderLabel(provider) {
|
|
165
|
+
switch (provider) {
|
|
166
|
+
case "claude":
|
|
167
|
+
return "Claude CLI";
|
|
168
|
+
case "codex":
|
|
169
|
+
return "Codex CLI";
|
|
170
|
+
case "copilot":
|
|
171
|
+
return "Copilot CLI";
|
|
172
|
+
case "agy":
|
|
173
|
+
return "Antigravity CLI";
|
|
174
|
+
case "qoder":
|
|
175
|
+
return "Qoder CLI";
|
|
176
|
+
case "gemini":
|
|
177
|
+
return "Gemini CLI";
|
|
178
|
+
case "opencode":
|
|
179
|
+
return "OpenCode CLI";
|
|
180
|
+
case "grok":
|
|
181
|
+
return "Grok CLI";
|
|
182
|
+
case "kimi":
|
|
183
|
+
return "Kimi Code CLI";
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function supportsNativeSystemPrompt(provider) {
|
|
187
|
+
return provider === "claude";
|
|
188
|
+
}
|
|
189
|
+
function supportsModel(provider) {
|
|
190
|
+
return provider !== "agy" && provider !== "codex";
|
|
191
|
+
}
|
|
192
|
+
function supportsReasoningEffort(provider) {
|
|
193
|
+
return provider === "claude" || provider === "copilot" || provider === "qoder" || provider === "opencode" || provider === "grok";
|
|
194
|
+
}
|
|
195
|
+
function shouldPassClaudeModel(options) {
|
|
196
|
+
return (options.env?.NOLO_CLAUDE_CLI_ALLOW_MODEL ?? process.env.NOLO_CLAUDE_CLI_ALLOW_MODEL) === "1";
|
|
197
|
+
}
|
|
198
|
+
var DATA_URL_RE = /^data:image\/([\w.+-]+);base64,([A-Za-z0-9+/=]+)$/;
|
|
199
|
+
var MIME_EXT = {
|
|
200
|
+
"png": ".png",
|
|
201
|
+
"jpeg": ".jpg",
|
|
202
|
+
"jpg": ".jpg",
|
|
203
|
+
"gif": ".gif",
|
|
204
|
+
"webp": ".webp",
|
|
205
|
+
"svg+xml": ".svg",
|
|
206
|
+
"bmp": ".bmp",
|
|
207
|
+
"tiff": ".tiff",
|
|
208
|
+
"avif": ".avif"
|
|
209
|
+
};
|
|
210
|
+
function inferExtFromMime(extPart) {
|
|
211
|
+
return MIME_EXT[extPart.toLowerCase().trim()] ?? ".bin";
|
|
212
|
+
}
|
|
213
|
+
var FILE_URL_RE = /^file:\/\/(.+)$/;
|
|
214
|
+
function writeDataUrlToTempImage(dataUrl, tempDir, index) {
|
|
215
|
+
const match = dataUrl.match(DATA_URL_RE);
|
|
216
|
+
if (!match) {
|
|
217
|
+
throw new Error(`Invalid data URL format for image #${index + 1}; expected data:image/<type>;base64,<data>`);
|
|
218
|
+
}
|
|
219
|
+
const [, mimeExt, b64] = match;
|
|
220
|
+
const ext = inferExtFromMime(mimeExt);
|
|
221
|
+
const filePath = join(tempDir, `image-${index + 1}${ext}`);
|
|
222
|
+
writeFileSync(filePath, Buffer.from(b64, "base64"));
|
|
223
|
+
return filePath;
|
|
224
|
+
}
|
|
225
|
+
function materializeCliImageInputs(imageInputs, cwd) {
|
|
226
|
+
let tempDir = null;
|
|
227
|
+
const materialized = imageInputs.map((input, index) => {
|
|
228
|
+
const src = input.source.trim();
|
|
229
|
+
if (src.startsWith("http://") || src.startsWith("https://")) {
|
|
230
|
+
return { ...input, materializedPath: void 0 };
|
|
231
|
+
}
|
|
232
|
+
const fileUrlMatch = src.match(FILE_URL_RE);
|
|
233
|
+
if (fileUrlMatch) {
|
|
234
|
+
const localPath = fileUrlMatch[1];
|
|
235
|
+
return {
|
|
236
|
+
...input,
|
|
237
|
+
materializedPath: isAbsolute(localPath) ? localPath : resolve(cwd, localPath)
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
if (src.startsWith("data:")) {
|
|
241
|
+
if (!tempDir) {
|
|
242
|
+
tempDir = mkdtempSync(join(tmpdir(), "nolo-cli-assets-"));
|
|
243
|
+
}
|
|
244
|
+
const filePath = writeDataUrlToTempImage(src, tempDir, index);
|
|
245
|
+
return { ...input, materializedPath: filePath };
|
|
246
|
+
}
|
|
247
|
+
return {
|
|
248
|
+
...input,
|
|
249
|
+
materializedPath: isAbsolute(src) ? src : resolve(cwd, src)
|
|
250
|
+
};
|
|
251
|
+
});
|
|
252
|
+
return { materialized, tempDir };
|
|
253
|
+
}
|
|
254
|
+
function buildImageReferencePromptBlock(provider, materialized) {
|
|
255
|
+
const label = getCliProviderLabel(provider);
|
|
256
|
+
const localPaths = materialized.filter((img) => img.materializedPath).map((img) => img.materializedPath);
|
|
257
|
+
const httpUrls = materialized.filter((img) => !img.materializedPath).map((img) => img.source);
|
|
258
|
+
const lines = [];
|
|
259
|
+
if (localPaths.length > 0) {
|
|
260
|
+
lines.push(
|
|
261
|
+
`[User attached image(s) - local file references (accessible at these paths):]`,
|
|
262
|
+
...localPaths.map((p) => ` - ${p}`),
|
|
263
|
+
`Please read and analyze these image files as part of the user's request.`
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
if (httpUrls.length > 0) {
|
|
267
|
+
lines.push(
|
|
268
|
+
`[User attached image(s) - remote URLs (content not available locally):]`,
|
|
269
|
+
...httpUrls.map((u) => ` - ${u}`),
|
|
270
|
+
`These remote URLs could not be downloaded automatically. If you cannot access them, describe what you can infer from context.`
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
const warning = materialized.length > 0 ? `${label} image input is passed as local file references; native image flags are not available in this wrapper.` : null;
|
|
274
|
+
return {
|
|
275
|
+
promptSuffix: lines.length > 0 ? lines.join("\n") + "\n\n" : "",
|
|
276
|
+
warning
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
function collectCliCapabilityWarnings(provider, options) {
|
|
280
|
+
const warnings = [];
|
|
281
|
+
const label = getCliProviderLabel(provider);
|
|
282
|
+
if (options.reasoningEffort !== void 0 && !supportsReasoningEffort(provider)) {
|
|
283
|
+
warnings.push(`${label} does not support reasoning_effort; ignored.`);
|
|
284
|
+
}
|
|
285
|
+
if (provider === "claude" && options.model !== void 0 && !shouldPassClaudeModel(options)) {
|
|
286
|
+
warnings.push(
|
|
287
|
+
"Claude CLI model selection is disabled by default because current installs can reject --model; set NOLO_CLAUDE_CLI_ALLOW_MODEL=1 to pass it through."
|
|
288
|
+
);
|
|
289
|
+
} else if (options.model !== void 0 && !supportsModel(provider)) {
|
|
290
|
+
warnings.push(`${label} does not support model selection; ignored.`);
|
|
291
|
+
}
|
|
292
|
+
const unsupportedFields = [
|
|
293
|
+
["temperature", options.temperature],
|
|
294
|
+
["top_p", options.topP],
|
|
295
|
+
["frequency_penalty", options.frequencyPenalty],
|
|
296
|
+
["presence_penalty", options.presencePenalty],
|
|
297
|
+
["max_tokens", options.maxTokens],
|
|
298
|
+
["enableThinking", options.enableThinking ? true : void 0],
|
|
299
|
+
["thinkingBudget", options.thinkingBudget]
|
|
300
|
+
];
|
|
301
|
+
for (const [field, value] of unsupportedFields) {
|
|
302
|
+
if (value !== void 0) {
|
|
303
|
+
warnings.push(`${label} does not support ${field}; ignored.`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return warnings;
|
|
307
|
+
}
|
|
308
|
+
function resolveCliExecution(provider, prompt, options) {
|
|
309
|
+
const warnings = collectCliCapabilityWarnings(provider, options);
|
|
310
|
+
const imageInputs = options.imageInputs ?? [];
|
|
311
|
+
let resolvedPrompt = prompt;
|
|
312
|
+
let resolvedOptions = options;
|
|
313
|
+
let imageTempDir;
|
|
314
|
+
if (imageInputs.length > 0) {
|
|
315
|
+
const { materialized, tempDir } = materializeCliImageInputs(
|
|
316
|
+
imageInputs,
|
|
317
|
+
options.cwd ?? process.cwd()
|
|
318
|
+
);
|
|
319
|
+
imageTempDir = tempDir ?? void 0;
|
|
320
|
+
if (provider === "codex") {
|
|
321
|
+
resolvedOptions = {
|
|
322
|
+
...resolvedOptions,
|
|
323
|
+
imageInputs: materialized
|
|
324
|
+
};
|
|
325
|
+
} else {
|
|
326
|
+
const block = buildImageReferencePromptBlock(provider, materialized);
|
|
327
|
+
if (block.promptSuffix) {
|
|
328
|
+
resolvedPrompt = block.promptSuffix + resolvedPrompt;
|
|
329
|
+
}
|
|
330
|
+
if (block.warning) {
|
|
331
|
+
warnings.push(block.warning);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
if (supportsNativeSystemPrompt(provider) || !options.systemPrompt?.trim()) {
|
|
336
|
+
return { prompt: resolvedPrompt, options: resolvedOptions, warnings, imageTempDir };
|
|
337
|
+
}
|
|
338
|
+
return {
|
|
339
|
+
prompt: buildCliPrompt(options.systemPrompt, resolvedPrompt),
|
|
340
|
+
options: {
|
|
341
|
+
...resolvedOptions,
|
|
342
|
+
systemPrompt: void 0
|
|
343
|
+
},
|
|
344
|
+
warnings,
|
|
345
|
+
imageTempDir
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
function executeCopilot(prompt, options) {
|
|
349
|
+
const {
|
|
350
|
+
model,
|
|
351
|
+
reasoningEffort,
|
|
352
|
+
timeout = 12e4,
|
|
353
|
+
cwd = process.cwd(),
|
|
354
|
+
yolo = true
|
|
355
|
+
} = options;
|
|
356
|
+
return new Promise((resolve2, reject) => {
|
|
357
|
+
const args = [
|
|
358
|
+
"NO_COLOR=1",
|
|
359
|
+
"gh copilot --",
|
|
360
|
+
`-p ${JSON.stringify(prompt)}`,
|
|
361
|
+
"--silent",
|
|
362
|
+
"--disable-builtin-mcps",
|
|
363
|
+
"--stream off",
|
|
364
|
+
"--no-color"
|
|
365
|
+
];
|
|
366
|
+
if (model) args.push(`--model ${model}`);
|
|
367
|
+
if (reasoningEffort) args.push(`--reasoning-effort ${reasoningEffort}`);
|
|
368
|
+
if (yolo) args.push("--yolo");
|
|
369
|
+
const cmd = args.join(" ");
|
|
370
|
+
const start = Date.now();
|
|
371
|
+
exec(
|
|
372
|
+
cmd,
|
|
373
|
+
{ timeout, cwd, env: buildCliProcessEnv(options.env) },
|
|
374
|
+
(error, stdout, stderr) => {
|
|
375
|
+
if (error) {
|
|
376
|
+
if (error.killed && error.signal === "SIGTERM") {
|
|
377
|
+
return reject(new Error(`Copilot CLI timed out after ${timeout}ms`));
|
|
378
|
+
}
|
|
379
|
+
return reject(
|
|
380
|
+
new Error(`Copilot CLI failed: ${error.message}
|
|
381
|
+
stderr: ${stderr}`)
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
const text = stdout.split("\n").filter((l) => !l.includes("A new release of gh is available")).filter((l) => !l.includes("To upgrade, run:")).filter((l) => !l.includes("https://github.com/cli/cli/releases")).join("\n").trim();
|
|
385
|
+
resolve2({ text, raw: stdout, elapsed: Date.now() - start });
|
|
386
|
+
}
|
|
387
|
+
);
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
function normalizeGeminiContent(content) {
|
|
391
|
+
if (typeof content === "string") return content;
|
|
392
|
+
if (Array.isArray(content)) {
|
|
393
|
+
return content.map((part) => {
|
|
394
|
+
if (typeof part === "string") return part;
|
|
395
|
+
if (part && typeof part === "object" && typeof part.text === "string") {
|
|
396
|
+
return part.text;
|
|
397
|
+
}
|
|
398
|
+
return "";
|
|
399
|
+
}).join("");
|
|
400
|
+
}
|
|
401
|
+
if (content && typeof content === "object" && typeof content.text === "string") {
|
|
402
|
+
return content.text;
|
|
403
|
+
}
|
|
404
|
+
return "";
|
|
405
|
+
}
|
|
406
|
+
function extractGeminiEventText(event) {
|
|
407
|
+
if (event?.type === "message" && event.role === "assistant") {
|
|
408
|
+
return normalizeGeminiContent(event.content);
|
|
409
|
+
}
|
|
410
|
+
if (event?.type === "result" && typeof event.result === "string") {
|
|
411
|
+
return event.result;
|
|
412
|
+
}
|
|
413
|
+
return "";
|
|
414
|
+
}
|
|
415
|
+
function parseGeminiStreamJson(stdout) {
|
|
416
|
+
const parts = [];
|
|
417
|
+
for (const line of stdout.split("\n")) {
|
|
418
|
+
const trimmed = line.trim();
|
|
419
|
+
if (!trimmed) continue;
|
|
420
|
+
try {
|
|
421
|
+
const event = JSON.parse(trimmed);
|
|
422
|
+
const text = extractGeminiEventText(event);
|
|
423
|
+
if (text) parts.push(text);
|
|
424
|
+
} catch {
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return parts.join("");
|
|
428
|
+
}
|
|
429
|
+
function executeGemini(prompt, options) {
|
|
430
|
+
const {
|
|
431
|
+
model = "gemini-3.6-flash",
|
|
432
|
+
timeout = 12e4,
|
|
433
|
+
cwd = process.cwd(),
|
|
434
|
+
yolo = true
|
|
435
|
+
} = options;
|
|
436
|
+
return new Promise((resolve2, reject) => {
|
|
437
|
+
const args = [
|
|
438
|
+
"NO_COLOR=1",
|
|
439
|
+
"NODE_OPTIONS='--no-deprecation'",
|
|
440
|
+
"gemini",
|
|
441
|
+
`-p ${JSON.stringify(prompt)}`,
|
|
442
|
+
"--output-format stream-json",
|
|
443
|
+
`-m ${JSON.stringify(model)}`
|
|
444
|
+
];
|
|
445
|
+
if (yolo) args.push("--yolo");
|
|
446
|
+
args.push("-e none");
|
|
447
|
+
const cmd = args.join(" ");
|
|
448
|
+
const start = Date.now();
|
|
449
|
+
exec(
|
|
450
|
+
cmd,
|
|
451
|
+
{ timeout, cwd, env: buildCliProcessEnv(options.env) },
|
|
452
|
+
(error, stdout, stderr) => {
|
|
453
|
+
if (error) {
|
|
454
|
+
if (error.killed && error.signal === "SIGTERM") {
|
|
455
|
+
return reject(new Error(`Gemini CLI timed out after ${timeout}ms`));
|
|
456
|
+
}
|
|
457
|
+
return reject(
|
|
458
|
+
new Error(`Gemini CLI failed: ${error.message}
|
|
459
|
+
stderr: ${stderr}`)
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
resolve2({
|
|
463
|
+
text: parseGeminiStreamJson(stdout),
|
|
464
|
+
raw: stdout,
|
|
465
|
+
elapsed: Date.now() - start
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
);
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
function executeCodex(prompt, options) {
|
|
472
|
+
const {
|
|
473
|
+
cwd = process.cwd(),
|
|
474
|
+
yolo = true
|
|
475
|
+
} = options;
|
|
476
|
+
const imagePaths = (options.imageInputs ?? []).map((img) => img.materializedPath).filter((p) => Boolean(p));
|
|
477
|
+
return new Promise((resolve2, reject) => {
|
|
478
|
+
const tempDir = mkdtempSync(join(tmpdir(), "codex-exec-"));
|
|
479
|
+
const outputFile = join(tempDir, "last-message.txt");
|
|
480
|
+
const executable = resolveLaunchableCodexCommand(options.env);
|
|
481
|
+
const args = [
|
|
482
|
+
"exec",
|
|
483
|
+
"--skip-git-repo-check",
|
|
484
|
+
"--ephemeral",
|
|
485
|
+
"--color",
|
|
486
|
+
"never",
|
|
487
|
+
"--output-last-message",
|
|
488
|
+
outputFile,
|
|
489
|
+
"--cd",
|
|
490
|
+
cwd
|
|
491
|
+
];
|
|
492
|
+
if (yolo) {
|
|
493
|
+
args.push("--sandbox");
|
|
494
|
+
args.push("danger-full-access");
|
|
495
|
+
}
|
|
496
|
+
for (const imgPath of imagePaths) {
|
|
497
|
+
args.push("-i", imgPath);
|
|
498
|
+
}
|
|
499
|
+
const start = Date.now();
|
|
500
|
+
const proc = spawn(executable, [...args, prompt], {
|
|
501
|
+
cwd,
|
|
502
|
+
env: buildCliProcessEnv(options.env),
|
|
503
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
504
|
+
});
|
|
505
|
+
const stdout = createUtf8Collector();
|
|
506
|
+
const stderr = createUtf8Collector();
|
|
507
|
+
proc.stdout.on("data", (data) => {
|
|
508
|
+
stdout.write(data);
|
|
509
|
+
});
|
|
510
|
+
proc.stderr.on("data", (data) => {
|
|
511
|
+
stderr.write(data);
|
|
512
|
+
});
|
|
513
|
+
proc.on("close", (code) => {
|
|
514
|
+
const stdoutText = stdout.end();
|
|
515
|
+
const stderrText = stderr.end();
|
|
516
|
+
if (code !== 0 && code !== null) {
|
|
517
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
518
|
+
const quota = detectCliProviderQuotaLimit("codex", stdoutText, stderrText, code);
|
|
519
|
+
if (quota.limited) {
|
|
520
|
+
reject(new CliProviderQuotaError("codex", quota.message || "Codex reported quota/limit"));
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
reject(new Error(`Codex CLI exited with code ${code}
|
|
524
|
+
stderr: ${stderrText}`));
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
let text = stdoutText.trim();
|
|
528
|
+
try {
|
|
529
|
+
const lastMessage = readFileSync(outputFile, "utf8").trim();
|
|
530
|
+
if (lastMessage) text = lastMessage;
|
|
531
|
+
} catch {
|
|
532
|
+
} finally {
|
|
533
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
534
|
+
}
|
|
535
|
+
resolve2({
|
|
536
|
+
text,
|
|
537
|
+
raw: stdoutText,
|
|
538
|
+
elapsed: Date.now() - start
|
|
539
|
+
});
|
|
540
|
+
});
|
|
541
|
+
proc.on("error", (err) => {
|
|
542
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
543
|
+
reject(err);
|
|
544
|
+
});
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
function executeClaude(prompt, options) {
|
|
548
|
+
const {
|
|
549
|
+
model,
|
|
550
|
+
systemPrompt,
|
|
551
|
+
reasoningEffort,
|
|
552
|
+
timeout = 12e4,
|
|
553
|
+
cwd = process.cwd(),
|
|
554
|
+
yolo = true
|
|
555
|
+
} = options;
|
|
556
|
+
return new Promise((resolve2, reject) => {
|
|
557
|
+
const args = ["--add-dir", cwd, "-p", prompt];
|
|
558
|
+
if (model && shouldPassClaudeModel(options)) {
|
|
559
|
+
args.push("--model");
|
|
560
|
+
args.push(model);
|
|
561
|
+
}
|
|
562
|
+
if (systemPrompt?.trim()) {
|
|
563
|
+
args.push("--system-prompt");
|
|
564
|
+
args.push(systemPrompt.trim());
|
|
565
|
+
}
|
|
566
|
+
if (reasoningEffort) {
|
|
567
|
+
args.push("--effort");
|
|
568
|
+
args.push(reasoningEffort);
|
|
569
|
+
}
|
|
570
|
+
if (yolo) {
|
|
571
|
+
args.push("--permission-mode");
|
|
572
|
+
args.push("bypassPermissions");
|
|
573
|
+
}
|
|
574
|
+
const start = Date.now();
|
|
575
|
+
const proc = spawn("claude", args, {
|
|
576
|
+
cwd,
|
|
577
|
+
env: buildCliProcessEnv(options.env),
|
|
578
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
579
|
+
});
|
|
580
|
+
const stdout = createUtf8Collector();
|
|
581
|
+
const stderr = createUtf8Collector();
|
|
582
|
+
let timer;
|
|
583
|
+
if (timeout > 0) {
|
|
584
|
+
timer = setTimeout(() => {
|
|
585
|
+
proc.kill("SIGTERM");
|
|
586
|
+
reject(new Error(`Claude CLI timed out after ${timeout}ms`));
|
|
587
|
+
}, timeout);
|
|
588
|
+
}
|
|
589
|
+
proc.stdout.on("data", (data) => {
|
|
590
|
+
stdout.write(data);
|
|
591
|
+
});
|
|
592
|
+
proc.stderr.on("data", (data) => {
|
|
593
|
+
stderr.write(data);
|
|
594
|
+
});
|
|
595
|
+
proc.on("close", (code) => {
|
|
596
|
+
if (timer) clearTimeout(timer);
|
|
597
|
+
const stdoutText = stdout.end();
|
|
598
|
+
const stderrText = stderr.end();
|
|
599
|
+
if (code !== 0 && code !== null) {
|
|
600
|
+
const quota = detectCliProviderQuotaLimit("claude", stdoutText, stderrText, code);
|
|
601
|
+
if (quota.limited) {
|
|
602
|
+
reject(new CliProviderQuotaError("claude", quota.message || "Claude reported quota/limit"));
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
reject(new Error(`Claude CLI exited with code ${code}
|
|
606
|
+
stderr: ${stderrText}`));
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
resolve2({
|
|
610
|
+
text: stdoutText.trim(),
|
|
611
|
+
raw: stdoutText,
|
|
612
|
+
elapsed: Date.now() - start
|
|
613
|
+
});
|
|
614
|
+
});
|
|
615
|
+
proc.on("error", (err) => {
|
|
616
|
+
if (timer) clearTimeout(timer);
|
|
617
|
+
reject(err);
|
|
618
|
+
});
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
function formatCliTimeout(timeoutMs) {
|
|
622
|
+
return `${Math.max(1, Math.ceil(timeoutMs / 1e3))}s`;
|
|
623
|
+
}
|
|
624
|
+
function tailText(value, maxChars = 1200) {
|
|
625
|
+
if (value.length <= maxChars) return value;
|
|
626
|
+
return value.slice(value.length - maxChars);
|
|
627
|
+
}
|
|
628
|
+
function createUtf8Collector() {
|
|
629
|
+
const decoder = new StringDecoder("utf8");
|
|
630
|
+
let text = "";
|
|
631
|
+
let ended = false;
|
|
632
|
+
const finish = () => {
|
|
633
|
+
if (ended) return "";
|
|
634
|
+
ended = true;
|
|
635
|
+
const chunk = decoder.end();
|
|
636
|
+
if (chunk) text += chunk;
|
|
637
|
+
return chunk;
|
|
638
|
+
};
|
|
639
|
+
return {
|
|
640
|
+
write(data) {
|
|
641
|
+
if (ended) return "";
|
|
642
|
+
const chunk = decoder.write(data);
|
|
643
|
+
text += chunk;
|
|
644
|
+
return chunk;
|
|
645
|
+
},
|
|
646
|
+
finish,
|
|
647
|
+
end() {
|
|
648
|
+
finish();
|
|
649
|
+
return text;
|
|
650
|
+
},
|
|
651
|
+
get text() {
|
|
652
|
+
return text;
|
|
653
|
+
}
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
function terminateCliProcessGroup(proc, signal) {
|
|
657
|
+
const pid = typeof proc.pid === "number" ? proc.pid : 0;
|
|
658
|
+
if (pid > 0 && process.platform !== "win32") {
|
|
659
|
+
try {
|
|
660
|
+
process.kill(-pid, signal);
|
|
661
|
+
return;
|
|
662
|
+
} catch {
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
try {
|
|
666
|
+
proc.kill(signal);
|
|
667
|
+
} catch {
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
function createCliAbortError(message = "CLI execution aborted") {
|
|
671
|
+
return new DOMException(message, "AbortError");
|
|
672
|
+
}
|
|
673
|
+
function attachCliAbortSignal(proc, signal, onAbort) {
|
|
674
|
+
if (!signal) return () => {
|
|
675
|
+
};
|
|
676
|
+
if (signal.aborted) {
|
|
677
|
+
terminateCliProcessGroup(proc, "SIGTERM");
|
|
678
|
+
onAbort(createCliAbortError());
|
|
679
|
+
return () => {
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
const handleAbort = () => {
|
|
683
|
+
terminateCliProcessGroup(proc, "SIGTERM");
|
|
684
|
+
onAbort(createCliAbortError());
|
|
685
|
+
};
|
|
686
|
+
signal.addEventListener("abort", handleAbort, { once: true });
|
|
687
|
+
return () => signal.removeEventListener("abort", handleAbort);
|
|
688
|
+
}
|
|
689
|
+
function buildAgyTimeoutError(timeout, stdout, stderr) {
|
|
690
|
+
const parts = [
|
|
691
|
+
`Antigravity CLI timed out after ${timeout}ms.`,
|
|
692
|
+
"agy print mode is agentic and can keep waiting for the conversation to become fully idle; prefer shorter scoped prompts or split the task into multiple turns."
|
|
693
|
+
];
|
|
694
|
+
const stdoutTail = tailText(stdout).trim();
|
|
695
|
+
const stderrTail = tailText(stderr).trim();
|
|
696
|
+
if (stdoutTail) parts.push(`stdout tail:
|
|
697
|
+
${stdoutTail}`);
|
|
698
|
+
if (stderrTail) parts.push(`stderr tail:
|
|
699
|
+
${stderrTail}`);
|
|
700
|
+
return new Error(parts.join("\n"));
|
|
701
|
+
}
|
|
702
|
+
function executeAgy(prompt, options) {
|
|
703
|
+
const {
|
|
704
|
+
timeout = DEFAULT_AGY_TIMEOUT_MS,
|
|
705
|
+
cwd = process.cwd(),
|
|
706
|
+
yolo = true
|
|
707
|
+
} = options;
|
|
708
|
+
return new Promise((resolve2, reject) => {
|
|
709
|
+
const args = [
|
|
710
|
+
"--add-dir",
|
|
711
|
+
cwd,
|
|
712
|
+
"--print",
|
|
713
|
+
prompt,
|
|
714
|
+
"--print-timeout",
|
|
715
|
+
formatCliTimeout(timeout)
|
|
716
|
+
];
|
|
717
|
+
if (yolo) {
|
|
718
|
+
args.push("--dangerously-skip-permissions");
|
|
719
|
+
}
|
|
720
|
+
const start = Date.now();
|
|
721
|
+
const proc = spawn("agy", args, {
|
|
722
|
+
cwd,
|
|
723
|
+
env: buildCliProcessEnv(options.env),
|
|
724
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
725
|
+
detached: process.platform !== "win32"
|
|
726
|
+
});
|
|
727
|
+
const stdout = createUtf8Collector();
|
|
728
|
+
const stderr = createUtf8Collector();
|
|
729
|
+
let timer;
|
|
730
|
+
let forceTimer;
|
|
731
|
+
let settled = false;
|
|
732
|
+
const clearTimers = () => {
|
|
733
|
+
if (timer) clearTimeout(timer);
|
|
734
|
+
if (forceTimer) clearTimeout(forceTimer);
|
|
735
|
+
};
|
|
736
|
+
if (timeout > 0) {
|
|
737
|
+
timer = setTimeout(() => {
|
|
738
|
+
if (settled) return;
|
|
739
|
+
settled = true;
|
|
740
|
+
terminateCliProcessGroup(proc, "SIGTERM");
|
|
741
|
+
forceTimer = setTimeout(() => {
|
|
742
|
+
terminateCliProcessGroup(proc, "SIGKILL");
|
|
743
|
+
}, 3e3);
|
|
744
|
+
reject(buildAgyTimeoutError(timeout, stdout.end(), stderr.end()));
|
|
745
|
+
}, timeout + 1e3);
|
|
746
|
+
}
|
|
747
|
+
proc.stdout.on("data", (data) => {
|
|
748
|
+
stdout.write(data);
|
|
749
|
+
});
|
|
750
|
+
proc.stderr.on("data", (data) => {
|
|
751
|
+
stderr.write(data);
|
|
752
|
+
});
|
|
753
|
+
proc.on("close", (code) => {
|
|
754
|
+
if (settled) {
|
|
755
|
+
clearTimers();
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
settled = true;
|
|
759
|
+
clearTimers();
|
|
760
|
+
const stdoutText = stdout.end();
|
|
761
|
+
const stderrText = stderr.end();
|
|
762
|
+
if (code !== 0 && code !== null) {
|
|
763
|
+
console.error("[cliExecutor] agy process exited with non-zero code:", code, "stderr:", stderrText);
|
|
764
|
+
const quota = detectCliProviderQuotaLimit("agy", stdoutText, stderrText, code);
|
|
765
|
+
if (quota.limited) {
|
|
766
|
+
reject(new CliProviderQuotaError("agy", quota.message || "Agy reported quota/limit"));
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
reject(new Error(`Antigravity CLI exited with code ${code}
|
|
770
|
+
stderr: ${stderrText}`));
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
resolve2({
|
|
774
|
+
text: stdoutText.trim(),
|
|
775
|
+
raw: stdoutText,
|
|
776
|
+
elapsed: Date.now() - start
|
|
777
|
+
});
|
|
778
|
+
});
|
|
779
|
+
proc.on("error", (err) => {
|
|
780
|
+
if (settled) return;
|
|
781
|
+
settled = true;
|
|
782
|
+
clearTimers();
|
|
783
|
+
console.error("[cliExecutor] agy process error event:", err);
|
|
784
|
+
reject(err);
|
|
785
|
+
});
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
function executeQoder(prompt, options) {
|
|
789
|
+
const {
|
|
790
|
+
model,
|
|
791
|
+
reasoningEffort,
|
|
792
|
+
timeout = 12e4,
|
|
793
|
+
cwd = process.cwd(),
|
|
794
|
+
yolo = true
|
|
795
|
+
} = options;
|
|
796
|
+
return new Promise((resolve2, reject) => {
|
|
797
|
+
const args = ["-p", prompt, "--cwd", cwd];
|
|
798
|
+
if (model) {
|
|
799
|
+
args.push("--model");
|
|
800
|
+
args.push(model);
|
|
801
|
+
}
|
|
802
|
+
if (reasoningEffort) {
|
|
803
|
+
args.push("--reasoning-effort");
|
|
804
|
+
args.push(reasoningEffort);
|
|
805
|
+
}
|
|
806
|
+
if (yolo) {
|
|
807
|
+
args.push("--dangerously-skip-permissions");
|
|
808
|
+
}
|
|
809
|
+
const start = Date.now();
|
|
810
|
+
const proc = spawn("qoder", args, {
|
|
811
|
+
cwd,
|
|
812
|
+
env: buildCliProcessEnv(options.env),
|
|
813
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
814
|
+
});
|
|
815
|
+
const stdout = createUtf8Collector();
|
|
816
|
+
const stderr = createUtf8Collector();
|
|
817
|
+
let timer;
|
|
818
|
+
if (timeout > 0) {
|
|
819
|
+
timer = setTimeout(() => {
|
|
820
|
+
const partial = stdout.end() + "\n" + stderr.end();
|
|
821
|
+
const q = detectCliProviderQuotaLimit("qoder", "", partial, null);
|
|
822
|
+
if (q.limited) {
|
|
823
|
+
proc.kill("SIGTERM");
|
|
824
|
+
reject(new CliProviderQuotaError("qoder", q.message || "Qoder quota signal seen before timeout"));
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
proc.kill("SIGTERM");
|
|
828
|
+
reject(new Error(`Qoder CLI timed out after ${timeout}ms`));
|
|
829
|
+
}, timeout);
|
|
830
|
+
}
|
|
831
|
+
proc.stdout.on("data", (data) => {
|
|
832
|
+
stdout.write(data);
|
|
833
|
+
});
|
|
834
|
+
proc.stderr.on("data", (data) => {
|
|
835
|
+
stderr.write(data);
|
|
836
|
+
const currentStderr = stderr.text;
|
|
837
|
+
if (detectCliProviderQuotaLimit("qoder", "", currentStderr, null).limited) {
|
|
838
|
+
if (timer) clearTimeout(timer);
|
|
839
|
+
proc.kill("SIGTERM");
|
|
840
|
+
reject(new CliProviderQuotaError("qoder", "quota signal detected incrementally in stderr"));
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
});
|
|
844
|
+
proc.on("close", (code) => {
|
|
845
|
+
if (timer) clearTimeout(timer);
|
|
846
|
+
const stdoutText = stdout.end();
|
|
847
|
+
const stderrText = stderr.end();
|
|
848
|
+
const quota = detectCliProviderQuotaLimit("qoder", stdoutText, stderrText, code);
|
|
849
|
+
const hasRealErrorSignal = code !== 0 || /error|fail|quota|limit|429|rate limit|exceeded/i.test(stderrText);
|
|
850
|
+
if (quota.limited && hasRealErrorSignal) {
|
|
851
|
+
reject(new CliProviderQuotaError("qoder", quota.message || "Qoder reported quota/limit"));
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
if (code !== 0 && code !== null) {
|
|
855
|
+
console.error("[cliExecutor] qoder process exited with non-zero code:", code, "stderr:", stderrText);
|
|
856
|
+
reject(new Error(`Qoder CLI exited with code ${code}
|
|
857
|
+
stderr: ${stderrText}`));
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
resolve2({
|
|
861
|
+
text: stdoutText.trim(),
|
|
862
|
+
raw: stdoutText,
|
|
863
|
+
elapsed: Date.now() - start
|
|
864
|
+
});
|
|
865
|
+
});
|
|
866
|
+
proc.on("error", (err) => {
|
|
867
|
+
if (timer) clearTimeout(timer);
|
|
868
|
+
console.error("[cliExecutor] qoder process error event:", err);
|
|
869
|
+
reject(err);
|
|
870
|
+
});
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
function parseOpenCodeJsonlEvents(stdout) {
|
|
874
|
+
const parts = [];
|
|
875
|
+
for (const line of stdout.split("\n")) {
|
|
876
|
+
const trimmed = line.trim();
|
|
877
|
+
if (!trimmed) continue;
|
|
878
|
+
try {
|
|
879
|
+
const event = JSON.parse(trimmed);
|
|
880
|
+
if (event.type === "text" && event.part?.type === "text" && typeof event.part.text === "string") {
|
|
881
|
+
parts.push(event.part.text);
|
|
882
|
+
}
|
|
883
|
+
} catch {
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
return parts.join("");
|
|
887
|
+
}
|
|
888
|
+
function executeOpenCode(prompt, options) {
|
|
889
|
+
const {
|
|
890
|
+
model,
|
|
891
|
+
reasoningEffort,
|
|
892
|
+
timeout = DEFAULT_OPENCODE_TIMEOUT_MS,
|
|
893
|
+
cwd = process.cwd(),
|
|
894
|
+
yolo = true
|
|
895
|
+
} = options;
|
|
896
|
+
return new Promise((resolve2, reject) => {
|
|
897
|
+
const args = [
|
|
898
|
+
"run",
|
|
899
|
+
"--format",
|
|
900
|
+
"json",
|
|
901
|
+
"--dir",
|
|
902
|
+
cwd,
|
|
903
|
+
"--print-logs"
|
|
904
|
+
// surface stream errors / quota / limit messages to stderr for fast detection
|
|
905
|
+
];
|
|
906
|
+
if (model) {
|
|
907
|
+
args.push("--model");
|
|
908
|
+
args.push(model);
|
|
909
|
+
}
|
|
910
|
+
if (reasoningEffort) {
|
|
911
|
+
args.push("--variant");
|
|
912
|
+
args.push(reasoningEffort);
|
|
913
|
+
}
|
|
914
|
+
if (yolo) {
|
|
915
|
+
args.push("--dangerously-skip-permissions");
|
|
916
|
+
}
|
|
917
|
+
args.push(prompt);
|
|
918
|
+
const start = Date.now();
|
|
919
|
+
const proc = spawn("opencode", args, {
|
|
920
|
+
cwd,
|
|
921
|
+
env: buildCliProcessEnv(options.env),
|
|
922
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
923
|
+
});
|
|
924
|
+
const stdout = createUtf8Collector();
|
|
925
|
+
const stderr = createUtf8Collector();
|
|
926
|
+
let timer;
|
|
927
|
+
if (timeout > 0) {
|
|
928
|
+
timer = setTimeout(() => {
|
|
929
|
+
const stdoutPartial = stdout.end();
|
|
930
|
+
const stderrPartial = stderr.end();
|
|
931
|
+
const q = detectCliProviderQuotaLimit("opencode", stdoutPartial, stderrPartial, null);
|
|
932
|
+
if (q.limited) {
|
|
933
|
+
proc.kill("SIGTERM");
|
|
934
|
+
reject(new CliProviderQuotaError("opencode", q.message || "OpenCode quota signal seen before timeout"));
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
proc.kill("SIGTERM");
|
|
938
|
+
reject(new Error(`OpenCode CLI timed out after ${timeout}ms`));
|
|
939
|
+
}, timeout);
|
|
940
|
+
}
|
|
941
|
+
proc.stdout.on("data", (data) => {
|
|
942
|
+
stdout.write(data);
|
|
943
|
+
});
|
|
944
|
+
proc.stderr.on("data", (data) => {
|
|
945
|
+
stderr.write(data);
|
|
946
|
+
const currentStderr = stderr.text;
|
|
947
|
+
if (detectCliProviderQuotaLimit("opencode", "", currentStderr, null).limited) {
|
|
948
|
+
if (timer) clearTimeout(timer);
|
|
949
|
+
proc.kill("SIGTERM");
|
|
950
|
+
reject(new CliProviderQuotaError("opencode", "quota signal detected incrementally in stderr"));
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
});
|
|
954
|
+
proc.on("close", (code) => {
|
|
955
|
+
if (timer) clearTimeout(timer);
|
|
956
|
+
const stdoutText = stdout.end();
|
|
957
|
+
const stderrText = stderr.end();
|
|
958
|
+
const quota = detectCliProviderQuotaLimit("opencode", stdoutText, stderrText, code);
|
|
959
|
+
const hasRealErrorSignal = code !== 0 || /error|fail|quota|limit|429|rate limit|exceeded/i.test(stderrText);
|
|
960
|
+
if (quota.limited && hasRealErrorSignal) {
|
|
961
|
+
reject(new CliProviderQuotaError("opencode", quota.message || "OpenCode reported quota/limit"));
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
if (code !== 0 && code !== null) {
|
|
965
|
+
console.error("[cliExecutor] opencode process exited with non-zero code:", code, "stderr:", stderrText);
|
|
966
|
+
reject(new Error(`OpenCode CLI exited with code ${code}
|
|
967
|
+
stderr: ${stderrText}`));
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
resolve2({
|
|
971
|
+
text: parseOpenCodeJsonlEvents(stdoutText).trim(),
|
|
972
|
+
raw: stdoutText,
|
|
973
|
+
elapsed: Date.now() - start
|
|
974
|
+
});
|
|
975
|
+
});
|
|
976
|
+
proc.on("error", (err) => {
|
|
977
|
+
if (timer) clearTimeout(timer);
|
|
978
|
+
console.error("[cliExecutor] opencode process error event:", err);
|
|
979
|
+
reject(err);
|
|
980
|
+
});
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
function parseGrokJsonOutput(stdout) {
|
|
984
|
+
const trimmed = stdout.trim();
|
|
985
|
+
if (!trimmed) return "";
|
|
986
|
+
const parsed = JSON.parse(trimmed);
|
|
987
|
+
if (parsed.type === "error") {
|
|
988
|
+
throw new Error(typeof parsed.message === "string" ? parsed.message : "Grok CLI failed");
|
|
989
|
+
}
|
|
990
|
+
return typeof parsed.text === "string" ? parsed.text : "";
|
|
991
|
+
}
|
|
992
|
+
function buildGrokProcessEnv(extraEnv) {
|
|
993
|
+
return buildCliProcessEnv({
|
|
994
|
+
GROK_TELEMETRY_TRACE_UPLOAD: "0",
|
|
995
|
+
GROK_TELEMETRY_ENABLED: "0",
|
|
996
|
+
GROK_FEEDBACK_ENABLED: "0",
|
|
997
|
+
GROK_TELEMETRY_MIXPANEL_ENABLED: "0",
|
|
998
|
+
...extraEnv
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
function executeGrok(prompt, options) {
|
|
1002
|
+
const {
|
|
1003
|
+
model,
|
|
1004
|
+
reasoningEffort,
|
|
1005
|
+
timeout = DEFAULT_GROK_TIMEOUT_MS,
|
|
1006
|
+
cwd = process.cwd(),
|
|
1007
|
+
yolo = true
|
|
1008
|
+
} = options;
|
|
1009
|
+
return new Promise((resolve2, reject) => {
|
|
1010
|
+
const args = ["-p", prompt, "--cwd", cwd, "--output-format", "json"];
|
|
1011
|
+
if (model) {
|
|
1012
|
+
args.push("-m", model);
|
|
1013
|
+
}
|
|
1014
|
+
if (reasoningEffort) {
|
|
1015
|
+
args.push("--effort", reasoningEffort);
|
|
1016
|
+
}
|
|
1017
|
+
if (yolo) {
|
|
1018
|
+
args.push("--yolo");
|
|
1019
|
+
}
|
|
1020
|
+
const start = Date.now();
|
|
1021
|
+
const proc = spawn("grok", args, {
|
|
1022
|
+
cwd,
|
|
1023
|
+
env: buildGrokProcessEnv(options.env),
|
|
1024
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1025
|
+
});
|
|
1026
|
+
const stdout = createUtf8Collector();
|
|
1027
|
+
const stderr = createUtf8Collector();
|
|
1028
|
+
let timer;
|
|
1029
|
+
if (timeout > 0) {
|
|
1030
|
+
timer = setTimeout(() => {
|
|
1031
|
+
const partial = stdout.end() + "\n" + stderr.end();
|
|
1032
|
+
const q = detectCliProviderQuotaLimit("grok", "", partial, null);
|
|
1033
|
+
if (q.limited) {
|
|
1034
|
+
proc.kill("SIGTERM");
|
|
1035
|
+
reject(new CliProviderQuotaError("grok", q.message || "Grok quota signal seen before timeout"));
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
proc.kill("SIGTERM");
|
|
1039
|
+
reject(new Error(`Grok CLI timed out after ${timeout}ms`));
|
|
1040
|
+
}, timeout);
|
|
1041
|
+
}
|
|
1042
|
+
proc.stdout.on("data", (data) => {
|
|
1043
|
+
stdout.write(data);
|
|
1044
|
+
});
|
|
1045
|
+
proc.stderr.on("data", (data) => {
|
|
1046
|
+
stderr.write(data);
|
|
1047
|
+
const currentStderr = stderr.text;
|
|
1048
|
+
if (detectCliProviderQuotaLimit("grok", "", currentStderr, null).limited) {
|
|
1049
|
+
if (timer) clearTimeout(timer);
|
|
1050
|
+
proc.kill("SIGTERM");
|
|
1051
|
+
reject(new CliProviderQuotaError("grok", "quota signal detected incrementally in stderr"));
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
});
|
|
1055
|
+
proc.on("close", (code) => {
|
|
1056
|
+
if (timer) clearTimeout(timer);
|
|
1057
|
+
const stdoutText = stdout.end();
|
|
1058
|
+
const stderrText = stderr.end();
|
|
1059
|
+
const quota = detectCliProviderQuotaLimit("grok", stdoutText, stderrText, code);
|
|
1060
|
+
const hasRealErrorSignal = code !== 0 || /error|fail|quota|limit|429|rate limit|exceeded/i.test(stderrText);
|
|
1061
|
+
if (quota.limited && hasRealErrorSignal) {
|
|
1062
|
+
reject(new CliProviderQuotaError("grok", quota.message || "Grok reported quota/limit"));
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
if (code !== 0 && code !== null) {
|
|
1066
|
+
console.error("[cliExecutor] grok process exited with non-zero code:", code, "stderr:", stderrText);
|
|
1067
|
+
reject(new Error(`Grok CLI exited with code ${code}
|
|
1068
|
+
stderr: ${stderrText}`));
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
try {
|
|
1072
|
+
resolve2({
|
|
1073
|
+
text: parseGrokJsonOutput(stdoutText).trim(),
|
|
1074
|
+
raw: stdoutText,
|
|
1075
|
+
elapsed: Date.now() - start
|
|
1076
|
+
});
|
|
1077
|
+
} catch (error) {
|
|
1078
|
+
reject(error);
|
|
1079
|
+
}
|
|
1080
|
+
});
|
|
1081
|
+
proc.on("error", (err) => {
|
|
1082
|
+
if (timer) clearTimeout(timer);
|
|
1083
|
+
console.error("[cliExecutor] grok process error event:", err);
|
|
1084
|
+
reject(err);
|
|
1085
|
+
});
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
function parseKimiStreamJson(stdout) {
|
|
1089
|
+
const parts = [];
|
|
1090
|
+
for (const line of stdout.split("\n")) {
|
|
1091
|
+
const trimmed = line.trim();
|
|
1092
|
+
if (!trimmed) continue;
|
|
1093
|
+
try {
|
|
1094
|
+
const event = JSON.parse(trimmed);
|
|
1095
|
+
if (event.role === "assistant" && typeof event.content === "string" && event.content.length > 0) {
|
|
1096
|
+
parts.push(event.content);
|
|
1097
|
+
}
|
|
1098
|
+
} catch {
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
return parts.join("");
|
|
1102
|
+
}
|
|
1103
|
+
function executeKimi(prompt, options) {
|
|
1104
|
+
const {
|
|
1105
|
+
model,
|
|
1106
|
+
timeout = 12e4,
|
|
1107
|
+
cwd = process.cwd()
|
|
1108
|
+
} = options;
|
|
1109
|
+
return new Promise((resolve2, reject) => {
|
|
1110
|
+
const args = ["-p", prompt, "--output-format", "stream-json"];
|
|
1111
|
+
if (model) {
|
|
1112
|
+
args.push("-m", model);
|
|
1113
|
+
}
|
|
1114
|
+
const start = Date.now();
|
|
1115
|
+
const proc = spawn("kimi", args, {
|
|
1116
|
+
cwd,
|
|
1117
|
+
env: buildCliProcessEnv(options.env),
|
|
1118
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1119
|
+
});
|
|
1120
|
+
const stdout = createUtf8Collector();
|
|
1121
|
+
const stderr = createUtf8Collector();
|
|
1122
|
+
let timer;
|
|
1123
|
+
if (timeout > 0) {
|
|
1124
|
+
timer = setTimeout(() => {
|
|
1125
|
+
const partial = stdout.end() + "\n" + stderr.end();
|
|
1126
|
+
const q = detectCliProviderQuotaLimit("kimi", "", partial, null);
|
|
1127
|
+
if (q.limited) {
|
|
1128
|
+
proc.kill("SIGTERM");
|
|
1129
|
+
reject(new CliProviderQuotaError("kimi", q.message || "Kimi quota signal seen before timeout"));
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
proc.kill("SIGTERM");
|
|
1133
|
+
reject(new Error(`Kimi CLI timed out after ${timeout}ms`));
|
|
1134
|
+
}, timeout);
|
|
1135
|
+
}
|
|
1136
|
+
proc.stdout.on("data", (data) => {
|
|
1137
|
+
stdout.write(data);
|
|
1138
|
+
});
|
|
1139
|
+
proc.stderr.on("data", (data) => {
|
|
1140
|
+
stderr.write(data);
|
|
1141
|
+
const currentStderr = stderr.text;
|
|
1142
|
+
if (detectCliProviderQuotaLimit("kimi", "", currentStderr, null).limited) {
|
|
1143
|
+
if (timer) clearTimeout(timer);
|
|
1144
|
+
proc.kill("SIGTERM");
|
|
1145
|
+
reject(new CliProviderQuotaError("kimi", "quota signal detected incrementally in stderr"));
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
});
|
|
1149
|
+
proc.on("close", (code) => {
|
|
1150
|
+
if (timer) clearTimeout(timer);
|
|
1151
|
+
const stdoutText = stdout.end();
|
|
1152
|
+
const stderrText = stderr.end();
|
|
1153
|
+
const quota = detectCliProviderQuotaLimit("kimi", stdoutText, stderrText, code);
|
|
1154
|
+
const hasRealErrorSignal = code !== 0 || /error|fail|quota|limit|429|rate limit|exceeded/i.test(stderrText);
|
|
1155
|
+
if (quota.limited && hasRealErrorSignal) {
|
|
1156
|
+
reject(new CliProviderQuotaError("kimi", quota.message || "Kimi reported quota/limit"));
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
if (code !== 0 && code !== null) {
|
|
1160
|
+
console.error("[cliExecutor] kimi process exited with non-zero code:", code, "stderr:", stderrText);
|
|
1161
|
+
reject(new Error(`Kimi CLI exited with code ${code}
|
|
1162
|
+
stderr: ${stderrText}`));
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
resolve2({
|
|
1166
|
+
text: parseKimiStreamJson(stdoutText).trim(),
|
|
1167
|
+
raw: stdoutText,
|
|
1168
|
+
elapsed: Date.now() - start
|
|
1169
|
+
});
|
|
1170
|
+
});
|
|
1171
|
+
proc.on("error", (err) => {
|
|
1172
|
+
if (timer) clearTimeout(timer);
|
|
1173
|
+
console.error("[cliExecutor] kimi process error event:", err);
|
|
1174
|
+
reject(err);
|
|
1175
|
+
});
|
|
1176
|
+
});
|
|
1177
|
+
}
|
|
1178
|
+
var EXECUTORS = {
|
|
1179
|
+
copilot: executeCopilot,
|
|
1180
|
+
gemini: executeGemini,
|
|
1181
|
+
codex: executeCodex,
|
|
1182
|
+
claude: executeClaude,
|
|
1183
|
+
agy: executeAgy,
|
|
1184
|
+
qoder: executeQoder,
|
|
1185
|
+
opencode: executeOpenCode,
|
|
1186
|
+
grok: executeGrok,
|
|
1187
|
+
kimi: executeKimi
|
|
1188
|
+
};
|
|
1189
|
+
function formatCliSessionTask(messages) {
|
|
1190
|
+
return messages.map((message, index) => {
|
|
1191
|
+
const speaker = message.role === "user" ? "\u7528\u6237" : "\u52A9\u624B";
|
|
1192
|
+
return `[${index + 1}] ${speaker}
|
|
1193
|
+
${message.content.trim()}`;
|
|
1194
|
+
}).join("\n\n");
|
|
1195
|
+
}
|
|
1196
|
+
function buildCliSessionPrompt(session, userInput) {
|
|
1197
|
+
const transcript = formatCliSessionTask([
|
|
1198
|
+
...session.messages,
|
|
1199
|
+
{ role: "user", content: userInput }
|
|
1200
|
+
]);
|
|
1201
|
+
return [
|
|
1202
|
+
"\u4EE5\u4E0B\u662F\u5F53\u524D\u5BF9\u8BDD\uFF0C\u8BF7\u57FA\u4E8E\u5B8C\u6574\u4E0A\u4E0B\u6587\u7EE7\u7EED\u56DE\u7B54\u6700\u540E\u4E00\u6761\u7528\u6237\u6D88\u606F\u3002",
|
|
1203
|
+
transcript
|
|
1204
|
+
].join("\n\n");
|
|
1205
|
+
}
|
|
1206
|
+
function getCliSessionOrThrow(sessionId) {
|
|
1207
|
+
const session = cliSessions.get(sessionId);
|
|
1208
|
+
if (!session) {
|
|
1209
|
+
throw new Error(`Unknown CLI session: "${sessionId}"`);
|
|
1210
|
+
}
|
|
1211
|
+
return session;
|
|
1212
|
+
}
|
|
1213
|
+
async function executeCli(provider, prompt, options = {}) {
|
|
1214
|
+
const executor = EXECUTORS[provider];
|
|
1215
|
+
if (!executor) {
|
|
1216
|
+
throw new Error(`Unknown CLI provider: "${provider}". Supported: ${Object.keys(EXECUTORS).join(", ")}`);
|
|
1217
|
+
}
|
|
1218
|
+
const resolved = resolveCliExecution(provider, prompt, options);
|
|
1219
|
+
try {
|
|
1220
|
+
const result = await executor(resolved.prompt, resolved.options);
|
|
1221
|
+
return {
|
|
1222
|
+
...result,
|
|
1223
|
+
warnings: [...resolved.warnings, ...result.warnings ?? []]
|
|
1224
|
+
};
|
|
1225
|
+
} finally {
|
|
1226
|
+
if (resolved.imageTempDir) {
|
|
1227
|
+
rmSync(resolved.imageTempDir, { recursive: true, force: true });
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
function startCliSession(provider, options = {}) {
|
|
1232
|
+
if (!EXECUTORS[provider]) {
|
|
1233
|
+
throw new Error(`Unknown CLI provider: "${provider}". Supported: ${Object.keys(EXECUTORS).join(", ")}`);
|
|
1234
|
+
}
|
|
1235
|
+
const sessionId = randomUUID();
|
|
1236
|
+
cliSessions.set(sessionId, {
|
|
1237
|
+
sessionId,
|
|
1238
|
+
provider,
|
|
1239
|
+
systemPrompt: options.systemPrompt,
|
|
1240
|
+
options: {
|
|
1241
|
+
model: options.model,
|
|
1242
|
+
timeout: options.timeout,
|
|
1243
|
+
cwd: options.cwd,
|
|
1244
|
+
env: options.env,
|
|
1245
|
+
yolo: options.yolo,
|
|
1246
|
+
systemPrompt: options.systemPrompt,
|
|
1247
|
+
reasoningEffort: options.reasoningEffort,
|
|
1248
|
+
temperature: options.temperature,
|
|
1249
|
+
topP: options.topP,
|
|
1250
|
+
frequencyPenalty: options.frequencyPenalty,
|
|
1251
|
+
presencePenalty: options.presencePenalty,
|
|
1252
|
+
maxTokens: options.maxTokens,
|
|
1253
|
+
enableThinking: options.enableThinking,
|
|
1254
|
+
thinkingBudget: options.thinkingBudget
|
|
1255
|
+
},
|
|
1256
|
+
messages: [],
|
|
1257
|
+
createdAt: Date.now(),
|
|
1258
|
+
updatedAt: Date.now()
|
|
1259
|
+
});
|
|
1260
|
+
return { sessionId, provider };
|
|
1261
|
+
}
|
|
1262
|
+
function getCliSession(sessionId) {
|
|
1263
|
+
return cliSessions.get(sessionId) ?? null;
|
|
1264
|
+
}
|
|
1265
|
+
function closeCliSession(sessionId) {
|
|
1266
|
+
return cliSessions.delete(sessionId);
|
|
1267
|
+
}
|
|
1268
|
+
async function executeCliSessionTurn(sessionId, userInput, options = {}) {
|
|
1269
|
+
const trimmedInput = userInput.trim();
|
|
1270
|
+
if (!trimmedInput) {
|
|
1271
|
+
throw new Error("CLI session turn requires non-empty user input.");
|
|
1272
|
+
}
|
|
1273
|
+
const session = getCliSessionOrThrow(sessionId);
|
|
1274
|
+
const prompt = buildCliSessionPrompt(session, trimmedInput);
|
|
1275
|
+
const result = await executeCli(session.provider, prompt, {
|
|
1276
|
+
...session.options,
|
|
1277
|
+
...options
|
|
1278
|
+
});
|
|
1279
|
+
session.messages.push(
|
|
1280
|
+
{ role: "user", content: trimmedInput },
|
|
1281
|
+
{ role: "assistant", content: result.text }
|
|
1282
|
+
);
|
|
1283
|
+
session.updatedAt = Date.now();
|
|
1284
|
+
return {
|
|
1285
|
+
...result,
|
|
1286
|
+
sessionId: session.sessionId
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1289
|
+
async function executeCliSessionTurnStreaming(sessionId, userInput, options) {
|
|
1290
|
+
const trimmedInput = userInput.trim();
|
|
1291
|
+
if (!trimmedInput) {
|
|
1292
|
+
throw new Error("CLI session turn requires non-empty user input.");
|
|
1293
|
+
}
|
|
1294
|
+
const session = getCliSessionOrThrow(sessionId);
|
|
1295
|
+
const prompt = buildCliSessionPrompt(session, trimmedInput);
|
|
1296
|
+
const result = await executeCliStreaming(session.provider, prompt, {
|
|
1297
|
+
...session.options,
|
|
1298
|
+
...options
|
|
1299
|
+
});
|
|
1300
|
+
session.messages.push(
|
|
1301
|
+
{ role: "user", content: trimmedInput },
|
|
1302
|
+
{ role: "assistant", content: result.text }
|
|
1303
|
+
);
|
|
1304
|
+
session.updatedAt = Date.now();
|
|
1305
|
+
return {
|
|
1306
|
+
...result,
|
|
1307
|
+
sessionId: session.sessionId
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
1310
|
+
function filterNoiseLine(line) {
|
|
1311
|
+
return !line.includes("A new release of gh is available") && !line.includes("To upgrade, run:") && !line.includes("https://github.com/cli/cli/releases");
|
|
1312
|
+
}
|
|
1313
|
+
function executeCliStreaming(provider, prompt, options) {
|
|
1314
|
+
const resolved = resolveCliExecution(provider, prompt, options);
|
|
1315
|
+
const executor = EXECUTORS[provider];
|
|
1316
|
+
const cleanupImageTempDir = () => {
|
|
1317
|
+
if (resolved.imageTempDir) {
|
|
1318
|
+
rmSync(resolved.imageTempDir, { recursive: true, force: true });
|
|
1319
|
+
}
|
|
1320
|
+
};
|
|
1321
|
+
const withCleanup = (promise) => promise.finally(cleanupImageTempDir);
|
|
1322
|
+
if (provider === "gemini") {
|
|
1323
|
+
return withCleanup(
|
|
1324
|
+
executeGeminiStreaming(resolved.prompt, resolved.options).then((result) => ({ ...result, warnings: [...resolved.warnings, ...result.warnings ?? []] }))
|
|
1325
|
+
);
|
|
1326
|
+
}
|
|
1327
|
+
if (provider === "codex") {
|
|
1328
|
+
return withCleanup(
|
|
1329
|
+
executor(resolved.prompt, resolved.options).then((result) => {
|
|
1330
|
+
const merged = { ...result, warnings: [...resolved.warnings, ...result.warnings ?? []] };
|
|
1331
|
+
if (merged.text) options.onChunk(merged.text);
|
|
1332
|
+
return merged;
|
|
1333
|
+
})
|
|
1334
|
+
);
|
|
1335
|
+
}
|
|
1336
|
+
if (provider === "claude") {
|
|
1337
|
+
return withCleanup(
|
|
1338
|
+
executor(resolved.prompt, resolved.options).then((result) => {
|
|
1339
|
+
const merged = { ...result, warnings: [...resolved.warnings, ...result.warnings ?? []] };
|
|
1340
|
+
if (merged.text) options.onChunk(merged.text);
|
|
1341
|
+
return merged;
|
|
1342
|
+
})
|
|
1343
|
+
);
|
|
1344
|
+
}
|
|
1345
|
+
if (provider === "agy" || provider === "qoder" || provider === "opencode" || provider === "grok" || provider === "kimi") {
|
|
1346
|
+
return withCleanup(
|
|
1347
|
+
executor(resolved.prompt, resolved.options).then((result) => {
|
|
1348
|
+
const merged = { ...result, warnings: [...resolved.warnings, ...result.warnings ?? []] };
|
|
1349
|
+
if (merged.text) options.onChunk(merged.text);
|
|
1350
|
+
return merged;
|
|
1351
|
+
})
|
|
1352
|
+
);
|
|
1353
|
+
}
|
|
1354
|
+
if (provider !== "copilot") {
|
|
1355
|
+
return withCleanup(
|
|
1356
|
+
executor(resolved.prompt, resolved.options).then((result) => ({
|
|
1357
|
+
...result,
|
|
1358
|
+
warnings: [...resolved.warnings, ...result.warnings ?? []]
|
|
1359
|
+
}))
|
|
1360
|
+
);
|
|
1361
|
+
}
|
|
1362
|
+
const {
|
|
1363
|
+
model,
|
|
1364
|
+
timeout = 12e4,
|
|
1365
|
+
cwd = process.cwd(),
|
|
1366
|
+
yolo = true,
|
|
1367
|
+
onChunk
|
|
1368
|
+
} = resolved.options;
|
|
1369
|
+
return withCleanup(new Promise((resolve2, reject) => {
|
|
1370
|
+
const args = [
|
|
1371
|
+
"copilot",
|
|
1372
|
+
"--",
|
|
1373
|
+
"-p",
|
|
1374
|
+
resolved.prompt,
|
|
1375
|
+
"--silent",
|
|
1376
|
+
"--disable-builtin-mcps"
|
|
1377
|
+
];
|
|
1378
|
+
if (model) {
|
|
1379
|
+
args.push("--model");
|
|
1380
|
+
args.push(model);
|
|
1381
|
+
}
|
|
1382
|
+
if (yolo) args.push("--yolo");
|
|
1383
|
+
const start = Date.now();
|
|
1384
|
+
const proc = spawn("gh", args, {
|
|
1385
|
+
cwd,
|
|
1386
|
+
env: buildCliProcessEnv(resolved.options.env)
|
|
1387
|
+
});
|
|
1388
|
+
const stdout = createUtf8Collector();
|
|
1389
|
+
let timer;
|
|
1390
|
+
let settled = false;
|
|
1391
|
+
const finish = (cb) => {
|
|
1392
|
+
if (settled) return;
|
|
1393
|
+
settled = true;
|
|
1394
|
+
if (timer) clearTimeout(timer);
|
|
1395
|
+
detachAbort();
|
|
1396
|
+
cb();
|
|
1397
|
+
};
|
|
1398
|
+
const detachAbort = attachCliAbortSignal(
|
|
1399
|
+
proc,
|
|
1400
|
+
resolved.options.signal,
|
|
1401
|
+
(error) => finish(() => reject(error))
|
|
1402
|
+
);
|
|
1403
|
+
if (timeout > 0) {
|
|
1404
|
+
timer = setTimeout(() => {
|
|
1405
|
+
terminateCliProcessGroup(proc, "SIGTERM");
|
|
1406
|
+
finish(() => reject(new Error(`Copilot CLI timed out after ${timeout}ms`)));
|
|
1407
|
+
}, timeout);
|
|
1408
|
+
}
|
|
1409
|
+
proc.stdout.on("data", (data) => {
|
|
1410
|
+
const chunk = stdout.write(data);
|
|
1411
|
+
const cleaned = chunk.split("\n").filter(filterNoiseLine).join("\n");
|
|
1412
|
+
if (cleaned) onChunk(cleaned);
|
|
1413
|
+
});
|
|
1414
|
+
proc.on("close", (code) => {
|
|
1415
|
+
const raw = stdout.end();
|
|
1416
|
+
if (code !== 0 && code !== null) {
|
|
1417
|
+
finish(() => reject(new Error(`Copilot CLI exited with code ${code}`)));
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1420
|
+
const text = raw.split("\n").filter(filterNoiseLine).join("\n").trim();
|
|
1421
|
+
finish(
|
|
1422
|
+
() => resolve2({
|
|
1423
|
+
text,
|
|
1424
|
+
raw,
|
|
1425
|
+
elapsed: Date.now() - start,
|
|
1426
|
+
warnings: [...resolved.warnings]
|
|
1427
|
+
})
|
|
1428
|
+
);
|
|
1429
|
+
});
|
|
1430
|
+
proc.on("error", (err) => {
|
|
1431
|
+
finish(() => reject(err));
|
|
1432
|
+
});
|
|
1433
|
+
}));
|
|
1434
|
+
}
|
|
1435
|
+
function executeGeminiStreaming(prompt, options) {
|
|
1436
|
+
const {
|
|
1437
|
+
model = "gemini-3.6-flash",
|
|
1438
|
+
timeout = 12e4,
|
|
1439
|
+
cwd = process.cwd(),
|
|
1440
|
+
yolo = true,
|
|
1441
|
+
onChunk
|
|
1442
|
+
} = options;
|
|
1443
|
+
return new Promise((resolve2, reject) => {
|
|
1444
|
+
const args = ["-p", prompt, "--output-format", "stream-json", "-m", model];
|
|
1445
|
+
if (yolo) args.push("--yolo");
|
|
1446
|
+
args.push("-e", "none");
|
|
1447
|
+
const start = Date.now();
|
|
1448
|
+
const proc = spawn("gemini", args, {
|
|
1449
|
+
cwd,
|
|
1450
|
+
env: buildCliProcessEnv({
|
|
1451
|
+
...options.env,
|
|
1452
|
+
NODE_OPTIONS: "--no-deprecation"
|
|
1453
|
+
})
|
|
1454
|
+
});
|
|
1455
|
+
const stdout = createUtf8Collector();
|
|
1456
|
+
const stderr = createUtf8Collector();
|
|
1457
|
+
let lineBuffer = "";
|
|
1458
|
+
let timer;
|
|
1459
|
+
let settled = false;
|
|
1460
|
+
const finish = (cb) => {
|
|
1461
|
+
if (settled) return;
|
|
1462
|
+
settled = true;
|
|
1463
|
+
if (timer) clearTimeout(timer);
|
|
1464
|
+
detachAbort();
|
|
1465
|
+
cb();
|
|
1466
|
+
};
|
|
1467
|
+
const detachAbort = attachCliAbortSignal(
|
|
1468
|
+
proc,
|
|
1469
|
+
options.signal,
|
|
1470
|
+
(error) => finish(() => reject(error))
|
|
1471
|
+
);
|
|
1472
|
+
const flushLineBuffer = () => {
|
|
1473
|
+
const trimmed = lineBuffer.trim();
|
|
1474
|
+
if (!trimmed) return;
|
|
1475
|
+
try {
|
|
1476
|
+
const event = JSON.parse(trimmed);
|
|
1477
|
+
const text = extractGeminiEventText(event);
|
|
1478
|
+
if (text) onChunk(text);
|
|
1479
|
+
} catch {
|
|
1480
|
+
}
|
|
1481
|
+
lineBuffer = "";
|
|
1482
|
+
};
|
|
1483
|
+
if (timeout > 0) {
|
|
1484
|
+
timer = setTimeout(() => {
|
|
1485
|
+
terminateCliProcessGroup(proc, "SIGTERM");
|
|
1486
|
+
finish(() => reject(new Error(`Gemini CLI timed out after ${timeout}ms`)));
|
|
1487
|
+
}, timeout);
|
|
1488
|
+
}
|
|
1489
|
+
proc.stdout.on("data", (data) => {
|
|
1490
|
+
const chunk = stdout.write(data);
|
|
1491
|
+
lineBuffer += chunk;
|
|
1492
|
+
let newlineIndex = lineBuffer.indexOf("\n");
|
|
1493
|
+
while (newlineIndex !== -1) {
|
|
1494
|
+
const line = lineBuffer.slice(0, newlineIndex).trim();
|
|
1495
|
+
lineBuffer = lineBuffer.slice(newlineIndex + 1);
|
|
1496
|
+
if (line) {
|
|
1497
|
+
try {
|
|
1498
|
+
const event = JSON.parse(line);
|
|
1499
|
+
const text = extractGeminiEventText(event);
|
|
1500
|
+
if (text) onChunk(text);
|
|
1501
|
+
} catch {
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
newlineIndex = lineBuffer.indexOf("\n");
|
|
1505
|
+
}
|
|
1506
|
+
});
|
|
1507
|
+
proc.stderr.on("data", (data) => {
|
|
1508
|
+
stderr.write(data);
|
|
1509
|
+
});
|
|
1510
|
+
proc.on("close", (code) => {
|
|
1511
|
+
lineBuffer += stdout.finish();
|
|
1512
|
+
const raw = stdout.text;
|
|
1513
|
+
const stderrText = stderr.end();
|
|
1514
|
+
flushLineBuffer();
|
|
1515
|
+
if (code !== 0 && code !== null) {
|
|
1516
|
+
finish(
|
|
1517
|
+
() => reject(new Error(`Gemini CLI exited with code ${code}
|
|
1518
|
+
stderr: ${stderrText}`))
|
|
1519
|
+
);
|
|
1520
|
+
return;
|
|
1521
|
+
}
|
|
1522
|
+
finish(
|
|
1523
|
+
() => resolve2({
|
|
1524
|
+
text: parseGeminiStreamJson(raw),
|
|
1525
|
+
raw,
|
|
1526
|
+
elapsed: Date.now() - start
|
|
1527
|
+
})
|
|
1528
|
+
);
|
|
1529
|
+
});
|
|
1530
|
+
proc.on("error", (err) => {
|
|
1531
|
+
finish(() => reject(err));
|
|
1532
|
+
});
|
|
1533
|
+
});
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
export {
|
|
1537
|
+
CliProviderQuotaError,
|
|
1538
|
+
detectCliProviderQuotaLimit,
|
|
1539
|
+
isBufferedCliStreamingProvider,
|
|
1540
|
+
CLI_PROBE_BINARIES,
|
|
1541
|
+
buildCliProcessEnv,
|
|
1542
|
+
materializeCliImageInputs,
|
|
1543
|
+
executeCli,
|
|
1544
|
+
startCliSession,
|
|
1545
|
+
getCliSession,
|
|
1546
|
+
closeCliSession,
|
|
1547
|
+
executeCliSessionTurn,
|
|
1548
|
+
executeCliSessionTurnStreaming,
|
|
1549
|
+
executeCliStreaming
|
|
1550
|
+
};
|