nolo-cli 0.1.8 → 0.1.10
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 +32 -0
- package/agentRuntimeCommands.ts +3 -3
- package/ai/agent/_executeModel.ts +118 -0
- package/ai/agent/agentSlice.ts +525 -0
- package/ai/agent/appWorkingMemory.ts +126 -0
- package/ai/agent/avatarUtils.ts +24 -0
- package/ai/agent/buildEditingContext.ts +373 -0
- package/ai/agent/buildSystemPrompt.ts +532 -0
- package/ai/agent/cleanAgentMessages.ts +140 -0
- package/ai/agent/cliChatClient.ts +119 -0
- package/ai/agent/cliExecutor.ts +733 -0
- package/ai/agent/cliPrompt.ts +10 -0
- package/ai/agent/contextCompiler.ts +107 -0
- package/ai/agent/contextLayerContract.ts +44 -0
- package/ai/agent/createAgentSchema.ts +234 -0
- package/ai/agent/executeToolCall.ts +58 -0
- package/ai/agent/fetchAgentContexts.ts +42 -0
- package/ai/agent/generatePrompt.ts +3 -0
- package/ai/agent/getFullChatContextKeys.ts +168 -0
- package/ai/agent/hooks/fetchPublicAgents.ts +133 -0
- package/ai/agent/hooks/useAgentConfig.ts +61 -0
- package/ai/agent/hooks/useAgentDialog.ts +35 -0
- package/ai/agent/hooks/useAgentFormValidation.ts +202 -0
- package/ai/agent/hooks/usePublicAgents.ts +473 -0
- package/ai/agent/machineRunPermissions.ts +95 -0
- package/ai/agent/persistMessageWithFixedId.ts +37 -0
- package/ai/agent/planSlice.ts +259 -0
- package/ai/agent/referenceUtils.ts +229 -0
- package/ai/agent/runAgentBackground.ts +238 -0
- package/ai/agent/runAgentClientLoop.ts +138 -0
- package/ai/agent/runtimeGuidance.ts +97 -0
- package/ai/agent/runtimeServerBase.ts +37 -0
- package/ai/agent/server/fetchPublicAgents.ts +128 -0
- package/ai/agent/startParallelAgentStreams.ts +424 -0
- package/ai/agent/startupProtocol.ts +53 -0
- package/ai/agent/streamAgentChatTurn.ts +1278 -0
- package/ai/agent/streamAgentChatTurnUtils.ts +738 -0
- package/ai/agent/types.ts +71 -0
- package/ai/agent/utils/imageOutput.ts +33 -0
- package/ai/agent/utils/sortUtils.ts +250 -0
- package/ai/agent/web/referencePickerUtils.ts +146 -0
- package/ai/ai.locale.ts +1079 -0
- package/ai/chat/accumulateToolCallChunks.ts +95 -0
- package/ai/chat/fetchUtils.native.ts +276 -0
- package/ai/chat/fetchUtils.ts +153 -0
- package/ai/chat/parseApiError.ts +64 -0
- package/ai/chat/parseMultilineSSE.ts +95 -0
- package/ai/chat/sendOpenAICompletionsRequest.native.ts +682 -0
- package/ai/chat/sendOpenAICompletionsRequest.ts +703 -0
- package/ai/chat/sendOpenAIResponseRequest.ts +491 -0
- package/ai/chat/shouldUseServerProxy.ts +18 -0
- package/ai/chat/sseClient.native.ts +91 -0
- package/ai/chat/sseClient.ts +67 -0
- package/ai/chat/streamReader.native.ts +31 -0
- package/ai/chat/streamReader.ts +62 -0
- package/ai/chat/updateTotalUsage.ts +72 -0
- package/ai/context/buildReferenceContext.ts +437 -0
- package/ai/context/calculateContextUsage.ts +133 -0
- package/ai/context/retention.ts +165 -0
- package/ai/context/tokenUtils.ts +78 -0
- package/ai/index.ts +1 -0
- package/ai/llm/calculateGeminiImageTokens.ts +57 -0
- package/ai/llm/deepinfra.ts +28 -0
- package/ai/llm/fireworks.ts +50 -0
- package/ai/llm/generateRequestBody.ts +165 -0
- package/ai/llm/getModelContextWindow.ts +84 -0
- package/ai/llm/getNoloKey.ts +31 -0
- package/ai/llm/getPricing.ts +199 -0
- package/ai/llm/hooks/useModelPricing.ts +75 -0
- package/ai/llm/imagePricing.ts +40 -0
- package/ai/llm/isResponseAPIModel.ts +13 -0
- package/ai/llm/mimo.ts +71 -0
- package/ai/llm/mistral.ts +22 -0
- package/ai/llm/modelAvatar.ts +427 -0
- package/ai/llm/models.ts +45 -0
- package/ai/llm/openrouterModels.ts +269 -0
- package/ai/llm/providers.ts +306 -0
- package/ai/llm/reasoningModels.ts +28 -0
- package/ai/llm/types.ts +59 -0
- package/ai/llm/usageRequestOptions.ts +59 -0
- package/ai/memory/capture.ts +148 -0
- package/ai/memory/consolidate.ts +104 -0
- package/ai/memory/delete.ts +147 -0
- package/ai/memory/overlay.ts +84 -0
- package/ai/memory/query.ts +38 -0
- package/ai/memory/queryShared.ts +160 -0
- package/ai/memory/rank.ts +105 -0
- package/ai/memory/recentRelationshipRecap.ts +249 -0
- package/ai/memory/remember.ts +167 -0
- package/ai/memory/runtime.ts +76 -0
- package/ai/memory/store.ts +20 -0
- package/ai/memory/storeShared.ts +76 -0
- package/ai/memory/types.ts +46 -0
- package/ai/memory/understanding.ts +349 -0
- package/ai/memory/understandingGreeting.ts +264 -0
- package/ai/messages/type.ts +20 -0
- package/ai/policy/personalizationDialog.ts +333 -0
- package/ai/policy/runtimePolicy.ts +440 -0
- package/ai/policy/selfUpdateFields.ts +48 -0
- package/ai/policy/types.ts +64 -0
- package/ai/skills/referenceRuntime.ts +274 -0
- package/ai/skills/skillDiagnostics.ts +251 -0
- package/ai/skills/skillDocBuilder.ts +139 -0
- package/ai/skills/skillDocProtocol.ts +434 -0
- package/ai/skills/skillReferenceSummary.ts +63 -0
- package/ai/skills/skillSummaryMarker.ts +26 -0
- package/ai/token/calculatePrice.ts +544 -0
- package/ai/token/db.ts +98 -0
- package/ai/token/externalToolCost.ts +330 -0
- package/ai/token/hooks/useRecords.ts +65 -0
- package/ai/token/missingUsageEstimate.ts +42 -0
- package/ai/token/modelUsageQuery.ts +252 -0
- package/ai/token/normalizeUsage.ts +84 -0
- package/ai/token/openaiImageGenerationUsage.ts +56 -0
- package/ai/token/prepareTokenUsageData.ts +88 -0
- package/ai/token/query.ts +88 -0
- package/ai/token/queryUserTokens.ts +59 -0
- package/ai/token/resolveBillingTarget.ts +52 -0
- package/ai/token/saveTokenRecord.ts +53 -0
- package/ai/token/serverDialogProjection.ts +78 -0
- package/ai/token/serverTokenWriter.ts +143 -0
- package/ai/token/stats.ts +21 -0
- package/ai/token/tokenThunks.ts +24 -0
- package/ai/token/types.ts +93 -0
- package/ai/tools/agent/agentTools.ts +176 -0
- package/ai/tools/agent/agentUpdateShared.ts +311 -0
- package/ai/tools/agent/callAgentTool.ts +139 -0
- package/ai/tools/agent/createAgentTool.ts +512 -0
- package/ai/tools/agent/createDialogTool.ts +69 -0
- package/ai/tools/agent/createSkillAgentTool.ts +62 -0
- package/ai/tools/agent/parallelBudget.ts +221 -0
- package/ai/tools/agent/presets/appBuilderPreset.ts +145 -0
- package/ai/tools/agent/runLlmTool.ts +96 -0
- package/ai/tools/agent/runStreamingAgentTool.ts +73 -0
- package/ai/tools/agent/skillAgentArgs.ts +106 -0
- package/ai/tools/agent/skillAgentPreset.ts +89 -0
- package/ai/tools/agent/streamParallelAgentsTool.ts +122 -0
- package/ai/tools/agent/updateAgentTool.ts +96 -0
- package/ai/tools/agent/updateSelfTool.ts +113 -0
- package/ai/tools/amazonProductScraperTool.ts +86 -0
- package/ai/tools/apifyActorClient.ts +45 -0
- package/ai/tools/appEditGuard.ts +372 -0
- package/ai/tools/appReadSnapshot.ts +153 -0
- package/ai/tools/appTools.ts +1549 -0
- package/ai/tools/applyEditTool.ts +256 -0
- package/ai/tools/applyLineEditsTool.ts +312 -0
- package/ai/tools/browserTools/click.ts +33 -0
- package/ai/tools/browserTools/closeSession.ts +29 -0
- package/ai/tools/browserTools/common.ts +27 -0
- package/ai/tools/browserTools/openSession.ts +48 -0
- package/ai/tools/browserTools/readContent.ts +38 -0
- package/ai/tools/browserTools/selectOption.ts +46 -0
- package/ai/tools/browserTools/typeText.ts +42 -0
- package/ai/tools/category/createCategoryTool.ts +66 -0
- package/ai/tools/category/queryContentsByCategoryTool.ts +69 -0
- package/ai/tools/category/updateContentCategoryTool.ts +75 -0
- package/ai/tools/cfBrowserTools.ts +319 -0
- package/ai/tools/cfSpeechToTextTool.ts +49 -0
- package/ai/tools/checkEnvTool.ts +65 -0
- package/ai/tools/cloudflareCrawlTool.ts +289 -0
- package/ai/tools/codeSearchTool.ts +111 -0
- package/ai/tools/codeTools.ts +101 -0
- package/ai/tools/createDocTool.ts +132 -0
- package/ai/tools/createPlanTool.ts +999 -0
- package/ai/tools/createSkillDocTool.ts +155 -0
- package/ai/tools/createWorkflowTool.ts +154 -0
- package/ai/tools/deepseekOcrTool.ts +34 -0
- package/ai/tools/delayTool.ts +31 -0
- package/ai/tools/deleteSpacesTool.ts +325 -0
- package/ai/tools/deleteSpacesToolModel.ts +159 -0
- package/ai/tools/devReloadUtils.ts +29 -0
- package/ai/tools/dialogMessageSearch.ts +137 -0
- package/ai/tools/doctorSkillTool.ts +72 -0
- package/ai/tools/ecommerceScraperTool.ts +86 -0
- package/ai/tools/emailTools.ts +549 -0
- package/ai/tools/evalSkillTool.ts +92 -0
- package/ai/tools/exaSearchTool.ts +64 -0
- package/ai/tools/execBashTool.ts +379 -0
- package/ai/tools/executeSqlTool.ts +192 -0
- package/ai/tools/fetchWebpageSupport.ts +309 -0
- package/ai/tools/fetchWebpageTool.ts +84 -0
- package/ai/tools/geminiImagePreviewTool.ts +361 -0
- package/ai/tools/generateDocxTool.ts +215 -0
- package/ai/tools/googleSearchScraperTool.ts +106 -0
- package/ai/tools/importDataTool.ts +133 -0
- package/ai/tools/importSkillTool.ts +162 -0
- package/ai/tools/index.ts +1858 -0
- package/ai/tools/listFilesTool.ts +82 -0
- package/ai/tools/listUserSpacesTool.ts +113 -0
- package/ai/tools/modelUsageTools.ts +142 -0
- package/ai/tools/olmOcrTool.ts +34 -0
- package/ai/tools/openaiImageTool.ts +218 -0
- package/ai/tools/paddleOcrTool.ts +34 -0
- package/ai/tools/prepareTools.ts +23 -0
- package/ai/tools/readDocTool.ts +84 -0
- package/ai/tools/readFileTool.ts +211 -0
- package/ai/tools/readTool.ts +163 -0
- package/ai/tools/readXPostTool.ts +233 -0
- package/ai/tools/rememberMemoryTool.ts +84 -0
- package/ai/tools/remotionVideoTool.ts +151 -0
- package/ai/tools/searchDialogMessagesTool.ts +222 -0
- package/ai/tools/searchRepoTool.ts +115 -0
- package/ai/tools/searchWorkspaceTool.ts +259 -0
- package/ai/tools/skillFollowup.ts +86 -0
- package/ai/tools/surfWeatherTool.ts +169 -0
- package/ai/tools/table/addTableRowTool.ts +217 -0
- package/ai/tools/table/createTableTool.ts +315 -0
- package/ai/tools/table/rowTools.ts +366 -0
- package/ai/tools/table/schemaTools.ts +244 -0
- package/ai/tools/table/shareTableTool.ts +148 -0
- package/ai/tools/table/toolShared.ts +129 -0
- package/ai/tools/toolApiClient.ts +198 -0
- package/ai/tools/toolNameAliases.ts +57 -0
- package/ai/tools/toolResultError.ts +42 -0
- package/ai/tools/toolRunSlice.ts +303 -0
- package/ai/tools/toolSchemaCompatibility.ts +53 -0
- package/ai/tools/toolVisibility.ts +4 -0
- package/ai/tools/types.ts +20 -0
- package/ai/tools/uiAskChoiceTool.ts +104 -0
- package/ai/tools/updateContentTitleTool.ts +84 -0
- package/ai/tools/updateDocTool.ts +105 -0
- package/ai/tools/updateUserPreferenceProfileTool.ts +145 -0
- package/ai/tools/whisperTool.ts +77 -0
- package/ai/tools/writeFileTool.ts +210 -0
- package/ai/tools/youtubeScraperTool.ts +116 -0
- package/ai/tools/ziweiChartTool.ts +678 -0
- package/ai/types.ts +55 -0
- package/ai/workflow/workflowExecutor.ts +323 -0
- package/ai/workflow/workflowSlice.ts +73 -0
- package/ai/workflow/workflowTypes.ts +106 -0
- package/client/compactDialog.ts +222 -0
- package/connector-experimental/capabilities.ts +73 -0
- package/connector-experimental/codexBinary.ts +41 -0
- package/connector-experimental/heartbeatLoop.ts +22 -0
- package/connector-experimental/index.ts +5 -0
- package/connector-experimental/machineInfo.ts +46 -0
- package/connector-experimental/protocol.ts +54 -0
- package/machineCommands.ts +4 -4
- package/package.json +22 -6
|
@@ -0,0 +1,733 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Executor - 通过命令行工具执行 AI 任务
|
|
3
|
+
*
|
|
4
|
+
* 设计为可扩展:当前支持 Copilot CLI、Gemini CLI、Codex CLI 与 Claude CLI。
|
|
5
|
+
*
|
|
6
|
+
* 注意:
|
|
7
|
+
* - CLI provider 与普通 model 路由共享 prompt / model / 最近文本上下文这些能力面
|
|
8
|
+
* - 但 CLI 不暴露本仓库可编排的 tool-calls 协议,因此这里只返回文本结果
|
|
9
|
+
* - 对缺少稳定增量输出协议的 CLI,流式接口允许退化为“完成后一次性回传”
|
|
10
|
+
*
|
|
11
|
+
* 使用方式:
|
|
12
|
+
* import { executeCli } from "ai/agent/cliExecutor";
|
|
13
|
+
* const result = await executeCli("copilot", prompt, { model: "claude-haiku-4.5" });
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { exec, spawn } from "child_process";
|
|
17
|
+
import { randomUUID } from "node:crypto";
|
|
18
|
+
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
import { tmpdir } from "node:os";
|
|
21
|
+
import { buildCliPrompt } from "./cliPrompt";
|
|
22
|
+
|
|
23
|
+
// ── 类型定义 ──────────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
/** 已支持的 CLI 工具。新增时在此联合类型追加,并在 EXECUTORS 里注册实现 */
|
|
26
|
+
export type CliProvider = "copilot" | "gemini" | "codex" | "claude";
|
|
27
|
+
|
|
28
|
+
export interface CliExecuteOptions {
|
|
29
|
+
model?: string;
|
|
30
|
+
timeout?: number; // ms,默认 120_000
|
|
31
|
+
cwd?: string;
|
|
32
|
+
yolo?: boolean; // 允许所有工具,默认 true(后台任务常用)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface CliExecuteResult {
|
|
36
|
+
text: string; // 解析后的纯文本回复
|
|
37
|
+
raw: string; // 原始 stdout
|
|
38
|
+
elapsed: number; // 耗时 ms
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type CliSessionMessage = {
|
|
42
|
+
role: "user" | "assistant";
|
|
43
|
+
content: string;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export interface CliSessionHandle {
|
|
47
|
+
sessionId: string;
|
|
48
|
+
provider: CliProvider;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface CliSessionState extends CliSessionHandle {
|
|
52
|
+
systemPrompt?: string;
|
|
53
|
+
options: CliExecuteOptions;
|
|
54
|
+
messages: CliSessionMessage[];
|
|
55
|
+
createdAt: number;
|
|
56
|
+
updatedAt: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface CliSessionTurnResult extends CliExecuteResult {
|
|
60
|
+
sessionId: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const cliSessions = new Map<string, CliSessionState>();
|
|
64
|
+
|
|
65
|
+
// ── 各 provider 实现 ─────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Copilot CLI 执行器
|
|
69
|
+
* 调用 gh copilot -- -p "..." --silent
|
|
70
|
+
*/
|
|
71
|
+
function executeCopilot(
|
|
72
|
+
prompt: string,
|
|
73
|
+
options: CliExecuteOptions
|
|
74
|
+
): Promise<CliExecuteResult> {
|
|
75
|
+
const {
|
|
76
|
+
model,
|
|
77
|
+
timeout = 120_000,
|
|
78
|
+
cwd = process.cwd(),
|
|
79
|
+
yolo = true,
|
|
80
|
+
} = options;
|
|
81
|
+
|
|
82
|
+
return new Promise((resolve, reject) => {
|
|
83
|
+
const args = [
|
|
84
|
+
"NO_COLOR=1",
|
|
85
|
+
"gh copilot --",
|
|
86
|
+
`-p ${JSON.stringify(prompt)}`,
|
|
87
|
+
"--silent",
|
|
88
|
+
"--disable-builtin-mcps",
|
|
89
|
+
"--stream off",
|
|
90
|
+
"--no-color",
|
|
91
|
+
];
|
|
92
|
+
if (model) args.push(`--model ${model}`);
|
|
93
|
+
if (yolo) args.push("--yolo");
|
|
94
|
+
|
|
95
|
+
const cmd = args.join(" ");
|
|
96
|
+
const start = Date.now();
|
|
97
|
+
|
|
98
|
+
exec(
|
|
99
|
+
cmd,
|
|
100
|
+
{ timeout, cwd, env: { ...process.env } },
|
|
101
|
+
(error, stdout, stderr) => {
|
|
102
|
+
if (error) {
|
|
103
|
+
if (error.killed && (error as any).signal === "SIGTERM") {
|
|
104
|
+
return reject(new Error(`Copilot CLI timed out after ${timeout}ms`));
|
|
105
|
+
}
|
|
106
|
+
return reject(
|
|
107
|
+
new Error(`Copilot CLI failed: ${error.message}\nstderr: ${stderr}`)
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const text = stdout
|
|
112
|
+
.split("\n")
|
|
113
|
+
.filter((l) => !l.includes("A new release of gh is available"))
|
|
114
|
+
.filter((l) => !l.includes("To upgrade, run:"))
|
|
115
|
+
.filter((l) => !l.includes("https://github.com/cli/cli/releases"))
|
|
116
|
+
.join("\n")
|
|
117
|
+
.trim();
|
|
118
|
+
|
|
119
|
+
resolve({ text, raw: stdout, elapsed: Date.now() - start });
|
|
120
|
+
}
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function normalizeGeminiContent(content: unknown): string {
|
|
126
|
+
if (typeof content === "string") return content;
|
|
127
|
+
if (Array.isArray(content)) {
|
|
128
|
+
return content
|
|
129
|
+
.map((part) => {
|
|
130
|
+
if (typeof part === "string") return part;
|
|
131
|
+
if (part && typeof part === "object" && typeof (part as any).text === "string") {
|
|
132
|
+
return (part as any).text;
|
|
133
|
+
}
|
|
134
|
+
return "";
|
|
135
|
+
})
|
|
136
|
+
.join("");
|
|
137
|
+
}
|
|
138
|
+
if (content && typeof content === "object" && typeof (content as any).text === "string") {
|
|
139
|
+
return (content as any).text;
|
|
140
|
+
}
|
|
141
|
+
return "";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function extractGeminiEventText(event: any): string {
|
|
145
|
+
if (event?.type === "message" && event.role === "assistant") {
|
|
146
|
+
return normalizeGeminiContent(event.content);
|
|
147
|
+
}
|
|
148
|
+
if (event?.type === "result" && typeof event.result === "string") {
|
|
149
|
+
return event.result;
|
|
150
|
+
}
|
|
151
|
+
return "";
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function parseGeminiStreamJson(stdout: string): string {
|
|
155
|
+
const parts: string[] = [];
|
|
156
|
+
|
|
157
|
+
for (const line of stdout.split("\n")) {
|
|
158
|
+
const trimmed = line.trim();
|
|
159
|
+
if (!trimmed) continue;
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
const event = JSON.parse(trimmed);
|
|
163
|
+
const text = extractGeminiEventText(event);
|
|
164
|
+
if (text) parts.push(text);
|
|
165
|
+
} catch {
|
|
166
|
+
// ignore malformed lines
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return parts.join("");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function executeGemini(
|
|
174
|
+
prompt: string,
|
|
175
|
+
options: CliExecuteOptions
|
|
176
|
+
): Promise<CliExecuteResult> {
|
|
177
|
+
const {
|
|
178
|
+
model = "gemini-3-flash-preview",
|
|
179
|
+
timeout = 120_000,
|
|
180
|
+
cwd = process.cwd(),
|
|
181
|
+
yolo = true,
|
|
182
|
+
} = options;
|
|
183
|
+
|
|
184
|
+
return new Promise((resolve, reject) => {
|
|
185
|
+
const args = [
|
|
186
|
+
"NO_COLOR=1",
|
|
187
|
+
"NODE_OPTIONS='--no-deprecation'",
|
|
188
|
+
"gemini",
|
|
189
|
+
`-p ${JSON.stringify(prompt)}`,
|
|
190
|
+
"--output-format stream-json",
|
|
191
|
+
`-m ${JSON.stringify(model)}`,
|
|
192
|
+
];
|
|
193
|
+
if (yolo) args.push("--yolo");
|
|
194
|
+
args.push("-e none");
|
|
195
|
+
|
|
196
|
+
const cmd = args.join(" ");
|
|
197
|
+
const start = Date.now();
|
|
198
|
+
|
|
199
|
+
exec(
|
|
200
|
+
cmd,
|
|
201
|
+
{ timeout, cwd, env: { ...process.env } },
|
|
202
|
+
(error, stdout, stderr) => {
|
|
203
|
+
if (error) {
|
|
204
|
+
if (error.killed && (error as any).signal === "SIGTERM") {
|
|
205
|
+
return reject(new Error(`Gemini CLI timed out after ${timeout}ms`));
|
|
206
|
+
}
|
|
207
|
+
return reject(
|
|
208
|
+
new Error(`Gemini CLI failed: ${error.message}\nstderr: ${stderr}`)
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
resolve({
|
|
213
|
+
text: parseGeminiStreamJson(stdout),
|
|
214
|
+
raw: stdout,
|
|
215
|
+
elapsed: Date.now() - start,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function executeCodex(
|
|
223
|
+
prompt: string,
|
|
224
|
+
options: CliExecuteOptions
|
|
225
|
+
): Promise<CliExecuteResult> {
|
|
226
|
+
const {
|
|
227
|
+
model,
|
|
228
|
+
timeout = 120_000,
|
|
229
|
+
cwd = process.cwd(),
|
|
230
|
+
yolo = true,
|
|
231
|
+
} = options;
|
|
232
|
+
|
|
233
|
+
return new Promise((resolve, reject) => {
|
|
234
|
+
const tempDir = mkdtempSync(join(tmpdir(), "codex-exec-"));
|
|
235
|
+
const outputFile = join(tempDir, "last-message.txt");
|
|
236
|
+
const args = [
|
|
237
|
+
"exec",
|
|
238
|
+
"--skip-git-repo-check",
|
|
239
|
+
"--ephemeral",
|
|
240
|
+
"--color",
|
|
241
|
+
"never",
|
|
242
|
+
"--output-last-message",
|
|
243
|
+
outputFile,
|
|
244
|
+
"--cd",
|
|
245
|
+
cwd,
|
|
246
|
+
];
|
|
247
|
+
if (model) {
|
|
248
|
+
args.push("--model");
|
|
249
|
+
args.push(model);
|
|
250
|
+
}
|
|
251
|
+
if (yolo) {
|
|
252
|
+
args.push("--sandbox");
|
|
253
|
+
args.push("danger-full-access");
|
|
254
|
+
}
|
|
255
|
+
const start = Date.now();
|
|
256
|
+
const proc = spawn("codex", [...args, prompt], {
|
|
257
|
+
cwd,
|
|
258
|
+
env: { ...process.env, NO_COLOR: "1" },
|
|
259
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
260
|
+
});
|
|
261
|
+
let stdout = "";
|
|
262
|
+
let stderr = "";
|
|
263
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
264
|
+
|
|
265
|
+
if (timeout > 0) {
|
|
266
|
+
timer = setTimeout(() => {
|
|
267
|
+
proc.kill("SIGTERM");
|
|
268
|
+
reject(new Error(`Codex CLI timed out after ${timeout}ms`));
|
|
269
|
+
}, timeout);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
proc.stdout.on("data", (data: Buffer) => {
|
|
273
|
+
stdout += data.toString();
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
proc.stderr.on("data", (data: Buffer) => {
|
|
277
|
+
stderr += data.toString();
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
proc.on("close", (code) => {
|
|
281
|
+
if (timer) clearTimeout(timer);
|
|
282
|
+
if (code !== 0 && code !== null) {
|
|
283
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
284
|
+
reject(new Error(`Codex CLI exited with code ${code}\nstderr: ${stderr}`));
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
let text = stdout.trim();
|
|
289
|
+
try {
|
|
290
|
+
const lastMessage = readFileSync(outputFile, "utf8").trim();
|
|
291
|
+
if (lastMessage) text = lastMessage;
|
|
292
|
+
} catch {
|
|
293
|
+
// Fall back to raw stdout when the output file is missing.
|
|
294
|
+
} finally {
|
|
295
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
resolve({
|
|
299
|
+
text,
|
|
300
|
+
raw: stdout,
|
|
301
|
+
elapsed: Date.now() - start,
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
proc.on("error", (err) => {
|
|
306
|
+
if (timer) clearTimeout(timer);
|
|
307
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
308
|
+
reject(err);
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function executeClaude(
|
|
314
|
+
prompt: string,
|
|
315
|
+
options: CliExecuteOptions
|
|
316
|
+
): Promise<CliExecuteResult> {
|
|
317
|
+
const {
|
|
318
|
+
model,
|
|
319
|
+
timeout = 120_000,
|
|
320
|
+
cwd = process.cwd(),
|
|
321
|
+
yolo = true,
|
|
322
|
+
} = options;
|
|
323
|
+
|
|
324
|
+
return new Promise((resolve, reject) => {
|
|
325
|
+
const args = ["-p", prompt];
|
|
326
|
+
if (model) {
|
|
327
|
+
args.push("--model");
|
|
328
|
+
args.push(model);
|
|
329
|
+
}
|
|
330
|
+
if (yolo) {
|
|
331
|
+
args.push("--permission-mode");
|
|
332
|
+
args.push("bypassPermissions");
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const start = Date.now();
|
|
336
|
+
const proc = spawn("claude", args, {
|
|
337
|
+
cwd,
|
|
338
|
+
env: { ...process.env, NO_COLOR: "1" },
|
|
339
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
340
|
+
});
|
|
341
|
+
let stdout = "";
|
|
342
|
+
let stderr = "";
|
|
343
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
344
|
+
|
|
345
|
+
if (timeout > 0) {
|
|
346
|
+
timer = setTimeout(() => {
|
|
347
|
+
proc.kill("SIGTERM");
|
|
348
|
+
reject(new Error(`Claude CLI timed out after ${timeout}ms`));
|
|
349
|
+
}, timeout);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
proc.stdout.on("data", (data: Buffer) => {
|
|
353
|
+
stdout += data.toString();
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
proc.stderr.on("data", (data: Buffer) => {
|
|
357
|
+
stderr += data.toString();
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
proc.on("close", (code) => {
|
|
361
|
+
if (timer) clearTimeout(timer);
|
|
362
|
+
if (code !== 0 && code !== null) {
|
|
363
|
+
reject(new Error(`Claude CLI exited with code ${code}\nstderr: ${stderr}`));
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
resolve({
|
|
368
|
+
text: stdout.trim(),
|
|
369
|
+
raw: stdout,
|
|
370
|
+
elapsed: Date.now() - start,
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
proc.on("error", (err) => {
|
|
375
|
+
if (timer) clearTimeout(timer);
|
|
376
|
+
reject(err);
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// ── 注册表(新增 CLI 工具时在这里加) ────────────────────────────────────────
|
|
382
|
+
|
|
383
|
+
const EXECUTORS: Record<
|
|
384
|
+
CliProvider,
|
|
385
|
+
(prompt: string, options: CliExecuteOptions) => Promise<CliExecuteResult>
|
|
386
|
+
> = {
|
|
387
|
+
copilot: executeCopilot,
|
|
388
|
+
gemini: executeGemini,
|
|
389
|
+
codex: executeCodex,
|
|
390
|
+
claude: executeClaude,
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
function formatCliSessionTask(messages: CliSessionMessage[]): string {
|
|
394
|
+
return messages
|
|
395
|
+
.map((message, index) => {
|
|
396
|
+
const speaker = message.role === "user" ? "用户" : "助手";
|
|
397
|
+
return `[${index + 1}] ${speaker}\n${message.content.trim()}`;
|
|
398
|
+
})
|
|
399
|
+
.join("\n\n");
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function buildCliSessionPrompt(session: CliSessionState, userInput: string): string {
|
|
403
|
+
const transcript = formatCliSessionTask([
|
|
404
|
+
...session.messages,
|
|
405
|
+
{ role: "user", content: userInput },
|
|
406
|
+
]);
|
|
407
|
+
|
|
408
|
+
return buildCliPrompt(
|
|
409
|
+
session.systemPrompt,
|
|
410
|
+
[
|
|
411
|
+
"以下是当前对话,请基于完整上下文继续回答最后一条用户消息。",
|
|
412
|
+
transcript,
|
|
413
|
+
].join("\n\n"),
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function getCliSessionOrThrow(sessionId: string): CliSessionState {
|
|
418
|
+
const session = cliSessions.get(sessionId);
|
|
419
|
+
if (!session) {
|
|
420
|
+
throw new Error(`Unknown CLI session: "${sessionId}"`);
|
|
421
|
+
}
|
|
422
|
+
return session;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// ── 公开 API ──────────────────────────────────────────────────────────────────
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* 执行 CLI 任务
|
|
429
|
+
*
|
|
430
|
+
* @param provider CLI 工具类型(如 "copilot" | "gemini" | "codex" | "claude")
|
|
431
|
+
* @param prompt 完整 prompt(system prompt 已由调用方拼好)
|
|
432
|
+
* @param options 执行选项
|
|
433
|
+
*/
|
|
434
|
+
export async function executeCli(
|
|
435
|
+
provider: CliProvider,
|
|
436
|
+
prompt: string,
|
|
437
|
+
options: CliExecuteOptions = {}
|
|
438
|
+
): Promise<CliExecuteResult> {
|
|
439
|
+
const executor = EXECUTORS[provider];
|
|
440
|
+
if (!executor) {
|
|
441
|
+
throw new Error(`Unknown CLI provider: "${provider}". Supported: ${Object.keys(EXECUTORS).join(", ")}`);
|
|
442
|
+
}
|
|
443
|
+
return executor(prompt, options);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
export { buildCliPrompt };
|
|
447
|
+
|
|
448
|
+
export function startCliSession(
|
|
449
|
+
provider: CliProvider,
|
|
450
|
+
options: CliExecuteOptions & { systemPrompt?: string } = {},
|
|
451
|
+
): CliSessionHandle {
|
|
452
|
+
if (!EXECUTORS[provider]) {
|
|
453
|
+
throw new Error(`Unknown CLI provider: "${provider}". Supported: ${Object.keys(EXECUTORS).join(", ")}`);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const sessionId = randomUUID();
|
|
457
|
+
cliSessions.set(sessionId, {
|
|
458
|
+
sessionId,
|
|
459
|
+
provider,
|
|
460
|
+
systemPrompt: options.systemPrompt,
|
|
461
|
+
options: {
|
|
462
|
+
model: options.model,
|
|
463
|
+
timeout: options.timeout,
|
|
464
|
+
cwd: options.cwd,
|
|
465
|
+
yolo: options.yolo,
|
|
466
|
+
},
|
|
467
|
+
messages: [],
|
|
468
|
+
createdAt: Date.now(),
|
|
469
|
+
updatedAt: Date.now(),
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
return { sessionId, provider };
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
export function getCliSession(sessionId: string): CliSessionState | null {
|
|
476
|
+
return cliSessions.get(sessionId) ?? null;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
export function closeCliSession(sessionId: string): boolean {
|
|
480
|
+
return cliSessions.delete(sessionId);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
export async function executeCliSessionTurn(
|
|
484
|
+
sessionId: string,
|
|
485
|
+
userInput: string,
|
|
486
|
+
options: Partial<CliExecuteOptions> = {},
|
|
487
|
+
): Promise<CliSessionTurnResult> {
|
|
488
|
+
const trimmedInput = userInput.trim();
|
|
489
|
+
if (!trimmedInput) {
|
|
490
|
+
throw new Error("CLI session turn requires non-empty user input.");
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const session = getCliSessionOrThrow(sessionId);
|
|
494
|
+
const prompt = buildCliSessionPrompt(session, trimmedInput);
|
|
495
|
+
const result = await executeCli(session.provider, prompt, {
|
|
496
|
+
...session.options,
|
|
497
|
+
...options,
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
session.messages.push(
|
|
501
|
+
{ role: "user", content: trimmedInput },
|
|
502
|
+
{ role: "assistant", content: result.text },
|
|
503
|
+
);
|
|
504
|
+
session.updatedAt = Date.now();
|
|
505
|
+
|
|
506
|
+
return {
|
|
507
|
+
...result,
|
|
508
|
+
sessionId: session.sessionId,
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
export async function executeCliSessionTurnStreaming(
|
|
513
|
+
sessionId: string,
|
|
514
|
+
userInput: string,
|
|
515
|
+
options: Partial<CliExecuteOptions> & { onChunk: (chunk: string) => void },
|
|
516
|
+
): Promise<CliSessionTurnResult> {
|
|
517
|
+
const trimmedInput = userInput.trim();
|
|
518
|
+
if (!trimmedInput) {
|
|
519
|
+
throw new Error("CLI session turn requires non-empty user input.");
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const session = getCliSessionOrThrow(sessionId);
|
|
523
|
+
const prompt = buildCliSessionPrompt(session, trimmedInput);
|
|
524
|
+
const result = await executeCliStreaming(session.provider, prompt, {
|
|
525
|
+
...session.options,
|
|
526
|
+
...options,
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
session.messages.push(
|
|
530
|
+
{ role: "user", content: trimmedInput },
|
|
531
|
+
{ role: "assistant", content: result.text },
|
|
532
|
+
);
|
|
533
|
+
session.updatedAt = Date.now();
|
|
534
|
+
|
|
535
|
+
return {
|
|
536
|
+
...result,
|
|
537
|
+
sessionId: session.sessionId,
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/** 忽略 gh 版本更新提示行 */
|
|
542
|
+
function filterNoiseLine(line: string): boolean {
|
|
543
|
+
return (
|
|
544
|
+
!line.includes("A new release of gh is available") &&
|
|
545
|
+
!line.includes("To upgrade, run:") &&
|
|
546
|
+
!line.includes("https://github.com/cli/cli/releases")
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* 流式执行 Copilot CLI,通过 onChunk 回调逐块返回输出
|
|
552
|
+
* 使用 spawn 替代 exec,不缓冲 stdout
|
|
553
|
+
*/
|
|
554
|
+
export function executeCliStreaming(
|
|
555
|
+
provider: CliProvider,
|
|
556
|
+
prompt: string,
|
|
557
|
+
options: CliExecuteOptions & { onChunk: (chunk: string) => void }
|
|
558
|
+
): Promise<CliExecuteResult> {
|
|
559
|
+
if (provider === "gemini") {
|
|
560
|
+
return executeGeminiStreaming(prompt, options);
|
|
561
|
+
}
|
|
562
|
+
if (provider === "codex") {
|
|
563
|
+
return executeCli(provider, prompt, options).then((result) => {
|
|
564
|
+
if (result.text) options.onChunk(result.text);
|
|
565
|
+
return result;
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
if (provider === "claude") {
|
|
569
|
+
return executeCli(provider, prompt, options).then((result) => {
|
|
570
|
+
if (result.text) options.onChunk(result.text);
|
|
571
|
+
return result;
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
if (provider !== "copilot") {
|
|
575
|
+
return executeCli(provider, prompt, options);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const { model, timeout = 120_000, cwd = process.cwd(), yolo = true, onChunk } = options;
|
|
579
|
+
|
|
580
|
+
return new Promise((resolve, reject) => {
|
|
581
|
+
const args = [
|
|
582
|
+
"copilot", "--",
|
|
583
|
+
"-p", prompt,
|
|
584
|
+
"--silent",
|
|
585
|
+
"--disable-builtin-mcps",
|
|
586
|
+
];
|
|
587
|
+
if (model) { args.push("--model"); args.push(model); }
|
|
588
|
+
if (yolo) args.push("--yolo");
|
|
589
|
+
|
|
590
|
+
const start = Date.now();
|
|
591
|
+
const proc = spawn("gh", args, {
|
|
592
|
+
cwd,
|
|
593
|
+
env: { ...process.env, NO_COLOR: "1" },
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
let raw = "";
|
|
597
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
598
|
+
|
|
599
|
+
if (timeout > 0) {
|
|
600
|
+
timer = setTimeout(() => {
|
|
601
|
+
proc.kill("SIGTERM");
|
|
602
|
+
reject(new Error(`Copilot CLI timed out after ${timeout}ms`));
|
|
603
|
+
}, timeout);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
proc.stdout.on("data", (data: Buffer) => {
|
|
607
|
+
const chunk = data.toString();
|
|
608
|
+
raw += chunk;
|
|
609
|
+
// 过滤噪音行后再推送
|
|
610
|
+
const cleaned = chunk
|
|
611
|
+
.split("\n")
|
|
612
|
+
.filter(filterNoiseLine)
|
|
613
|
+
.join("\n");
|
|
614
|
+
if (cleaned) onChunk(cleaned);
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
proc.on("close", (code) => {
|
|
618
|
+
if (timer) clearTimeout(timer);
|
|
619
|
+
if (code !== 0 && code !== null) {
|
|
620
|
+
reject(new Error(`Copilot CLI exited with code ${code}`));
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
const text = raw
|
|
624
|
+
.split("\n")
|
|
625
|
+
.filter(filterNoiseLine)
|
|
626
|
+
.join("\n")
|
|
627
|
+
.trim();
|
|
628
|
+
resolve({ text, raw, elapsed: Date.now() - start });
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
proc.on("error", (err) => {
|
|
632
|
+
if (timer) clearTimeout(timer);
|
|
633
|
+
reject(err);
|
|
634
|
+
});
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function executeGeminiStreaming(
|
|
639
|
+
prompt: string,
|
|
640
|
+
options: CliExecuteOptions & { onChunk: (chunk: string) => void }
|
|
641
|
+
): Promise<CliExecuteResult> {
|
|
642
|
+
const {
|
|
643
|
+
model = "gemini-3-flash-preview",
|
|
644
|
+
timeout = 120_000,
|
|
645
|
+
cwd = process.cwd(),
|
|
646
|
+
yolo = true,
|
|
647
|
+
onChunk,
|
|
648
|
+
} = options;
|
|
649
|
+
|
|
650
|
+
return new Promise((resolve, reject) => {
|
|
651
|
+
const args = ["-p", prompt, "--output-format", "stream-json", "-m", model];
|
|
652
|
+
if (yolo) args.push("--yolo");
|
|
653
|
+
args.push("-e", "none");
|
|
654
|
+
|
|
655
|
+
const start = Date.now();
|
|
656
|
+
const proc = spawn("gemini", args, {
|
|
657
|
+
cwd,
|
|
658
|
+
env: { ...process.env, NO_COLOR: "1", NODE_OPTIONS: "--no-deprecation" },
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
let raw = "";
|
|
662
|
+
let stderr = "";
|
|
663
|
+
let lineBuffer = "";
|
|
664
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
665
|
+
|
|
666
|
+
const flushLineBuffer = () => {
|
|
667
|
+
const trimmed = lineBuffer.trim();
|
|
668
|
+
if (!trimmed) return;
|
|
669
|
+
try {
|
|
670
|
+
const event = JSON.parse(trimmed);
|
|
671
|
+
const text = extractGeminiEventText(event);
|
|
672
|
+
if (text) onChunk(text);
|
|
673
|
+
} catch {
|
|
674
|
+
// ignore partial or malformed lines
|
|
675
|
+
}
|
|
676
|
+
lineBuffer = "";
|
|
677
|
+
};
|
|
678
|
+
|
|
679
|
+
if (timeout > 0) {
|
|
680
|
+
timer = setTimeout(() => {
|
|
681
|
+
proc.kill("SIGTERM");
|
|
682
|
+
reject(new Error(`Gemini CLI timed out after ${timeout}ms`));
|
|
683
|
+
}, timeout);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
proc.stdout.on("data", (data: Buffer) => {
|
|
687
|
+
const chunk = data.toString();
|
|
688
|
+
raw += chunk;
|
|
689
|
+
lineBuffer += chunk;
|
|
690
|
+
|
|
691
|
+
let newlineIndex = lineBuffer.indexOf("\n");
|
|
692
|
+
while (newlineIndex !== -1) {
|
|
693
|
+
const line = lineBuffer.slice(0, newlineIndex).trim();
|
|
694
|
+
lineBuffer = lineBuffer.slice(newlineIndex + 1);
|
|
695
|
+
if (line) {
|
|
696
|
+
try {
|
|
697
|
+
const event = JSON.parse(line);
|
|
698
|
+
const text = extractGeminiEventText(event);
|
|
699
|
+
if (text) onChunk(text);
|
|
700
|
+
} catch {
|
|
701
|
+
// ignore malformed lines
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
newlineIndex = lineBuffer.indexOf("\n");
|
|
705
|
+
}
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
proc.stderr.on("data", (data: Buffer) => {
|
|
709
|
+
stderr += data.toString();
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
proc.on("close", (code) => {
|
|
713
|
+
if (timer) clearTimeout(timer);
|
|
714
|
+
flushLineBuffer();
|
|
715
|
+
|
|
716
|
+
if (code !== 0 && code !== null) {
|
|
717
|
+
reject(new Error(`Gemini CLI exited with code ${code}\nstderr: ${stderr}`));
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
resolve({
|
|
722
|
+
text: parseGeminiStreamJson(raw),
|
|
723
|
+
raw,
|
|
724
|
+
elapsed: Date.now() - start,
|
|
725
|
+
});
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
proc.on("error", (err) => {
|
|
729
|
+
if (timer) clearTimeout(timer);
|
|
730
|
+
reject(err);
|
|
731
|
+
});
|
|
732
|
+
});
|
|
733
|
+
}
|