@fieldwangai/agentflow 0.1.25
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/LICENSE +21 -0
- package/README.md +201 -0
- package/README.zh-CN.md +201 -0
- package/agents/agentflow-node-executor-code.md +32 -0
- package/agents/agentflow-node-executor-planning.md +32 -0
- package/agents/agentflow-node-executor-requirement.md +32 -0
- package/agents/agentflow-node-executor-test.md +32 -0
- package/agents/agentflow-node-executor-ui.md +32 -0
- package/agents/agentflow-node-executor.md +32 -0
- package/agents/agents.json +8 -0
- package/agents/en/agentflow-node-executor.md +32 -0
- package/agents/zh/agentflow-node-executor.md +32 -0
- package/bin/agentflow.mjs +52 -0
- package/bin/ensure-workspace-reference.mjs +35 -0
- package/bin/lib/agent-runners.mjs +1199 -0
- package/bin/lib/agents-path.mjs +61 -0
- package/bin/lib/api-runner.mjs +361 -0
- package/bin/lib/apply.mjs +852 -0
- package/bin/lib/catalog-agents.mjs +300 -0
- package/bin/lib/catalog-flows.mjs +532 -0
- package/bin/lib/composer-agent.mjs +884 -0
- package/bin/lib/composer-flow-instances.mjs +68 -0
- package/bin/lib/composer-flow-skeleton.mjs +334 -0
- package/bin/lib/composer-flow-validate.mjs +47 -0
- package/bin/lib/composer-log.mjs +197 -0
- package/bin/lib/composer-model-router.mjs +160 -0
- package/bin/lib/composer-node-schema.mjs +299 -0
- package/bin/lib/composer-planner.mjs +749 -0
- package/bin/lib/composer-script-ops.mjs +233 -0
- package/bin/lib/composer-skill-router.mjs +384 -0
- package/bin/lib/flow-import.mjs +305 -0
- package/bin/lib/flow-normalize.mjs +71 -0
- package/bin/lib/flow-write.mjs +395 -0
- package/bin/lib/help.mjs +139 -0
- package/bin/lib/hub-login.mjs +54 -0
- package/bin/lib/hub-publish.mjs +159 -0
- package/bin/lib/hub-remote.mjs +189 -0
- package/bin/lib/hub.mjs +299 -0
- package/bin/lib/i18n.mjs +233 -0
- package/bin/lib/locales/en.json +344 -0
- package/bin/lib/locales/zh.json +344 -0
- package/bin/lib/log.mjs +37 -0
- package/bin/lib/main.mjs +611 -0
- package/bin/lib/model-config.mjs +118 -0
- package/bin/lib/model-lists.mjs +188 -0
- package/bin/lib/node-exec-context.mjs +336 -0
- package/bin/lib/node-execute.mjs +513 -0
- package/bin/lib/normalize-node-tool-command.mjs +97 -0
- package/bin/lib/paths.mjs +216 -0
- package/bin/lib/pipeline-scripts.mjs +41 -0
- package/bin/lib/recent-runs.mjs +173 -0
- package/bin/lib/run-apply-active-lock.mjs +82 -0
- package/bin/lib/run-events.mjs +85 -0
- package/bin/lib/run-node-statuses-from-disk.mjs +85 -0
- package/bin/lib/schedule-config.mjs +227 -0
- package/bin/lib/scheduler.mjs +312 -0
- package/bin/lib/table.mjs +4 -0
- package/bin/lib/terminal.mjs +42 -0
- package/bin/lib/ui-print.mjs +94 -0
- package/bin/lib/ui-server.mjs +2113 -0
- package/bin/lib/workspace-tree.mjs +266 -0
- package/bin/lib/workspace.mjs +180 -0
- package/bin/pipeline/build-node-prompt.mjs +179 -0
- package/bin/pipeline/check-cache.mjs +191 -0
- package/bin/pipeline/check-flow.mjs +543 -0
- package/bin/pipeline/collect-nodes.mjs +212 -0
- package/bin/pipeline/compute-cache-md5.mjs +177 -0
- package/bin/pipeline/ensure-run-dir.mjs +71 -0
- package/bin/pipeline/extract-thinking.mjs +308 -0
- package/bin/pipeline/gc.mjs +129 -0
- package/bin/pipeline/get-env.mjs +83 -0
- package/bin/pipeline/get-exec-id.mjs +145 -0
- package/bin/pipeline/get-ready-nodes.mjs +435 -0
- package/bin/pipeline/get-resolved-values.mjs +337 -0
- package/bin/pipeline/load-key.mjs +62 -0
- package/bin/pipeline/parse-bool.mjs +33 -0
- package/bin/pipeline/parse-flow.mjs +698 -0
- package/bin/pipeline/post-process-control-if.mjs +23 -0
- package/bin/pipeline/post-process-node.mjs +490 -0
- package/bin/pipeline/pre-process-node.mjs +449 -0
- package/bin/pipeline/resolve-inputs.mjs +201 -0
- package/bin/pipeline/run-log.mjs +34 -0
- package/bin/pipeline/run-tool-nodejs.mjs +160 -0
- package/bin/pipeline/save-key.mjs +93 -0
- package/bin/pipeline/snapshot-prior-round.mjs +70 -0
- package/bin/pipeline/validate-flow.mjs +825 -0
- package/bin/pipeline/validate-for-ui.mjs +226 -0
- package/bin/pipeline/validate-script-output.mjs +130 -0
- package/bin/pipeline/write-result.mjs +182 -0
- package/builtin/nodes/agent_subAgent.md +14 -0
- package/builtin/nodes/control_agent_toBool.md +20 -0
- package/builtin/nodes/control_anyOne.md +17 -0
- package/builtin/nodes/control_end.md +11 -0
- package/builtin/nodes/control_if.md +20 -0
- package/builtin/nodes/control_start.md +11 -0
- package/builtin/nodes/control_toBool.md +21 -0
- package/builtin/nodes/provide_file.md +11 -0
- package/builtin/nodes/provide_str.md +11 -0
- package/builtin/nodes/tool_get_env.md +14 -0
- package/builtin/nodes/tool_load_key.md +20 -0
- package/builtin/nodes/tool_nodejs.md +40 -0
- package/builtin/nodes/tool_print.md +14 -0
- package/builtin/nodes/tool_save_key.md +20 -0
- package/builtin/nodes/tool_user_ask.md +23 -0
- package/builtin/nodes/tool_user_check.md +22 -0
- package/builtin/pipelines/module-migrate/flow.yaml +819 -0
- package/builtin/pipelines/module-migrate/scripts/check_imports.mjs +700 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Makefile +362 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/.deps/Release/obj.target/node_modules/node-addon-api/node_addon_api_except.stamp.d +1 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/.deps/Release/obj.target/tree_sitter_kotlin_binding/bindings/node/binding.o.d +17 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/.deps/Release/obj.target/tree_sitter_kotlin_binding/src/parser.o.d +5 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/.deps/Release/obj.target/tree_sitter_kotlin_binding/src/scanner.o.d +8 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/.deps/Release/tree_sitter_kotlin_binding.node.d +1 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/obj.target/node_modules/node-addon-api/node_addon_api_except.stamp +0 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/obj.target/tree_sitter_kotlin_binding/bindings/node/binding.o +0 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/obj.target/tree_sitter_kotlin_binding/src/parser.o +0 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/obj.target/tree_sitter_kotlin_binding/src/scanner.o +0 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/tree_sitter_kotlin_binding.node +0 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/binding.Makefile +6 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/gyp-mac-tool +768 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/node_modules/node-addon-api/node_addon_api.Makefile +6 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/node_modules/node-addon-api/node_addon_api.target.mk +122 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/node_modules/node-addon-api/node_addon_api_except.target.mk +126 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/node_modules/node-addon-api/node_addon_api_maybe.target.mk +122 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/tree_sitter_kotlin_binding.target.mk +203 -0
- package/builtin/pipelines/new/flow.yaml +545 -0
- package/builtin/pipelines/new/scripts/check-flow.mjs +9 -0
- package/builtin/pipelines/new/scripts/collect-nodes.mjs +211 -0
- package/builtin/pipelines/scripts/adjust-node-positions.mjs +113 -0
- package/builtin/web-ui/dist/agentflow-icon.svg +23 -0
- package/builtin/web-ui/dist/assets/index-CZkUPcXE.css +1 -0
- package/builtin/web-ui/dist/assets/index-DkkhNESc.js +190 -0
- package/builtin/web-ui/dist/index.html +24 -0
- package/package.json +67 -0
- package/reference/flow-control-capabilities.md +274 -0
- package/reference/flow-layout.md +84 -0
- package/reference/flow-prompt-handler-check.md +12 -0
- package/reference/flow-result-semantics.md +14 -0
|
@@ -0,0 +1,1199 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import { createMarkdownStreamer, render as renderMarkdown } from "markdansi";
|
|
6
|
+
import { getAgentPath, loadAgentPromptWithReplacements, stripYamlFrontmatter } from "./agents-path.mjs";
|
|
7
|
+
import { machineReadable } from "./log.mjs";
|
|
8
|
+
import { normalizeCursorModelForCli } from "./model-config.mjs";
|
|
9
|
+
import { appendRunLogLine } from "./run-events.mjs";
|
|
10
|
+
import { writeWithPrefix } from "./terminal.mjs";
|
|
11
|
+
import { t } from "./i18n.mjs";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Run Cursor CLI with stream-json, forward events to stdout, return success/failure.
|
|
15
|
+
*/
|
|
16
|
+
export function runCursorAgentForNode(
|
|
17
|
+
workspaceRoot,
|
|
18
|
+
{ promptPath, nodeContext, taskBody, intermediatePath, resultPathRel, subagent, instanceId },
|
|
19
|
+
options = {},
|
|
20
|
+
) {
|
|
21
|
+
const absPromptPath = path.resolve(workspaceRoot, promptPath);
|
|
22
|
+
const absRunDir = path.resolve(workspaceRoot, intermediatePath);
|
|
23
|
+
const absResultPath = path.join(absRunDir, resultPathRel);
|
|
24
|
+
const nodeIntermediateDir = path.dirname(absPromptPath);
|
|
25
|
+
const outputDir = instanceId ? path.join(absRunDir, "output", instanceId) : path.join(absRunDir, "output");
|
|
26
|
+
if (instanceId) fs.mkdirSync(outputDir, { recursive: true });
|
|
27
|
+
const absWorkspaceRoot = path.resolve(workspaceRoot);
|
|
28
|
+
const replacements = {
|
|
29
|
+
workspaceRoot: absWorkspaceRoot,
|
|
30
|
+
promptPath: absPromptPath,
|
|
31
|
+
nodeContext: nodeContext ?? "",
|
|
32
|
+
taskBody: taskBody ?? "",
|
|
33
|
+
resultPath: absResultPath,
|
|
34
|
+
intermediatePath: path.join(absRunDir, "intermediate"),
|
|
35
|
+
outputDir,
|
|
36
|
+
flowName: options.flowName ?? "",
|
|
37
|
+
uuid: options.uuid ?? "",
|
|
38
|
+
instanceId: instanceId ?? "",
|
|
39
|
+
};
|
|
40
|
+
const agentContent = loadAgentPromptWithReplacements(workspaceRoot, subagent, replacements);
|
|
41
|
+
let agentPathForPrompt = getAgentPath(workspaceRoot, subagent);
|
|
42
|
+
if (agentContent) {
|
|
43
|
+
const resolvedAgentPath = path.join(nodeIntermediateDir, `agent-${subagent}.md`);
|
|
44
|
+
fs.mkdirSync(nodeIntermediateDir, { recursive: true });
|
|
45
|
+
fs.writeFileSync(resolvedAgentPath, agentContent, "utf8");
|
|
46
|
+
agentPathForPrompt = resolvedAgentPath;
|
|
47
|
+
}
|
|
48
|
+
const rawAgentContent =
|
|
49
|
+
agentContent != null
|
|
50
|
+
? agentContent
|
|
51
|
+
: fs.existsSync(agentPathForPrompt)
|
|
52
|
+
? fs.readFileSync(agentPathForPrompt, "utf8")
|
|
53
|
+
: "";
|
|
54
|
+
const promptText = stripYamlFrontmatter(rawAgentContent);
|
|
55
|
+
|
|
56
|
+
const modelRaw = options.model ?? process.env.CURSOR_AGENT_MODEL ?? null;
|
|
57
|
+
const model = normalizeCursorModelForCli(modelRaw);
|
|
58
|
+
const rawPrefix = options.outputPrefix != null ? `[${options.outputPrefix}] ` : "";
|
|
59
|
+
const coloredPrefix = rawPrefix && options.prefixColor ? options.prefixColor(rawPrefix) : rawPrefix;
|
|
60
|
+
const agentContentColor = options.contentColor ?? ((line) => chalk.gray(line));
|
|
61
|
+
|
|
62
|
+
return new Promise((resolve, reject) => {
|
|
63
|
+
const agentCmd = process.env.CURSOR_AGENT_CMD || "agent";
|
|
64
|
+
const args = ["--print", "--output-format", "stream-json", "--trust", "--workspace", workspaceRoot];
|
|
65
|
+
const approveMcps = process.env.AGENTFLOW_CURSOR_APPROVE_MCPS !== "0" && process.env.AGENTFLOW_CURSOR_APPROVE_MCPS !== "false";
|
|
66
|
+
if (approveMcps) args.push("--approve-mcps");
|
|
67
|
+
if (options.force) args.push("--force");
|
|
68
|
+
args.push("--model", model);
|
|
69
|
+
args.push(promptText);
|
|
70
|
+
if (options.flowName && options.uuid) {
|
|
71
|
+
const argvLog = args.slice(0, -1).concat([`(prompt ${args[args.length - 1].length} chars)`]);
|
|
72
|
+
appendRunLogLine(workspaceRoot, options.flowName, options.uuid, "cli-raw", `Cursor CLI 完整参数: ${agentCmd} ${JSON.stringify(argvLog)}`);
|
|
73
|
+
appendRunLogLine(
|
|
74
|
+
workspaceRoot,
|
|
75
|
+
options.flowName,
|
|
76
|
+
options.uuid,
|
|
77
|
+
"cli-raw",
|
|
78
|
+
`Cursor CLI prompt 前 800 字:\n${promptText.slice(0, 800)}${promptText.length > 800 ? "..." : ""}`,
|
|
79
|
+
);
|
|
80
|
+
appendRunLogLine(workspaceRoot, options.flowName, options.uuid, "cli-raw", `Cursor CLI prompt 完整:\n${promptText}`);
|
|
81
|
+
}
|
|
82
|
+
const useStderrInherit = process.env.AGENTFLOW_CURSOR_STDERR_INHERIT === "1" || process.env.AGENTFLOW_CURSOR_STDERR_INHERIT === "true";
|
|
83
|
+
const child = spawn(agentCmd, args, {
|
|
84
|
+
cwd: workspaceRoot,
|
|
85
|
+
stdio: ["ignore", "pipe", useStderrInherit ? "inherit" : "pipe"],
|
|
86
|
+
shell: false,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
let lastResult = null;
|
|
90
|
+
let hadError = false;
|
|
91
|
+
const STDERR_CAP_BYTES = 1024 * 1024;
|
|
92
|
+
const stderrChunks = [];
|
|
93
|
+
let stderrTotalBytes = 0;
|
|
94
|
+
const stderrBuffer = options.stderrBuffer || null;
|
|
95
|
+
let stderrLineBuffer = "";
|
|
96
|
+
const flowName = options.flowName ?? null;
|
|
97
|
+
const uuid = options.uuid ?? null;
|
|
98
|
+
|
|
99
|
+
const outStream = machineReadable ? process.stderr : process.stdout;
|
|
100
|
+
function writeStdout(text) {
|
|
101
|
+
if (coloredPrefix) writeWithPrefix(outStream, text, coloredPrefix, agentContentColor);
|
|
102
|
+
else if (text) outStream.write(agentContentColor(text));
|
|
103
|
+
if (text && flowName && uuid) appendRunLogLine(workspaceRoot, flowName, uuid, "cursor-stdout", text);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function flushStderrLines() {
|
|
107
|
+
if (!coloredPrefix) return;
|
|
108
|
+
let idx;
|
|
109
|
+
while ((idx = stderrLineBuffer.indexOf("\n")) !== -1) {
|
|
110
|
+
const line = stderrLineBuffer.slice(0, idx + 1);
|
|
111
|
+
stderrLineBuffer = stderrLineBuffer.slice(idx + 1);
|
|
112
|
+
writeWithPrefix(process.stderr, line, coloredPrefix, agentContentColor);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!useStderrInherit) {
|
|
117
|
+
child.stderr.on("data", (chunk) => {
|
|
118
|
+
const s = typeof chunk === "string" ? chunk : chunk.toString("utf-8");
|
|
119
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, "utf-8");
|
|
120
|
+
const len = buf.length;
|
|
121
|
+
while (stderrChunks.length > 0 && stderrTotalBytes + len > STDERR_CAP_BYTES) {
|
|
122
|
+
const drop = stderrChunks.shift();
|
|
123
|
+
stderrTotalBytes -= Buffer.isBuffer(drop) ? drop.length : Buffer.byteLength(drop, "utf-8");
|
|
124
|
+
}
|
|
125
|
+
stderrChunks.push(buf);
|
|
126
|
+
stderrTotalBytes += len;
|
|
127
|
+
if (stderrBuffer) {
|
|
128
|
+
stderrBuffer.push(chunk);
|
|
129
|
+
} else if (coloredPrefix) {
|
|
130
|
+
stderrLineBuffer += s;
|
|
131
|
+
flushStderrLines();
|
|
132
|
+
} else {
|
|
133
|
+
process.stderr.write(chunk);
|
|
134
|
+
}
|
|
135
|
+
if (flowName && uuid) appendRunLogLine(workspaceRoot, flowName, uuid, "cursor-stderr", s);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const stdoutWidth = process.stdout.columns ?? 80;
|
|
140
|
+
const mdStreamer = createMarkdownStreamer({
|
|
141
|
+
render: (md) => renderMarkdown(md, { width: stdoutWidth }),
|
|
142
|
+
spacing: "single",
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const STDOUT_RAW_CAP = 200;
|
|
146
|
+
const debugStdout = process.env.AGENTFLOW_DEBUG_STDOUT === "1" || process.env.AGENTFLOW_DEBUG_STDOUT === "true";
|
|
147
|
+
|
|
148
|
+
function isLikelyBase64(s) {
|
|
149
|
+
if (!s || typeof s !== "string") return false;
|
|
150
|
+
const t = s.trim();
|
|
151
|
+
if (t.startsWith("data:image/") && t.includes(";base64,")) return true;
|
|
152
|
+
if (t.length < 80) return false;
|
|
153
|
+
return /^[A-Za-z0-9+/]+=*$/.test(t);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
child.stdout.setEncoding("utf-8");
|
|
157
|
+
let stdoutLineBuffer = "";
|
|
158
|
+
child.stdout.on("data", (chunk) => {
|
|
159
|
+
stdoutLineBuffer += chunk;
|
|
160
|
+
const idx = stdoutLineBuffer.lastIndexOf("\n");
|
|
161
|
+
const complete = idx >= 0 ? stdoutLineBuffer.slice(0, idx) : "";
|
|
162
|
+
if (idx >= 0) stdoutLineBuffer = stdoutLineBuffer.slice(idx + 1);
|
|
163
|
+
const lines = complete.split("\n").filter(Boolean);
|
|
164
|
+
for (const line of lines) {
|
|
165
|
+
if (flowName && uuid) appendRunLogLine(workspaceRoot, flowName, uuid, "cursor-stdout-raw", line);
|
|
166
|
+
try {
|
|
167
|
+
const event = JSON.parse(line);
|
|
168
|
+
if (event.type === "assistant" && event.message?.content) {
|
|
169
|
+
let text = (event.message.content || [])
|
|
170
|
+
.filter((c) => c.type === "text" && c.text)
|
|
171
|
+
.map((c) => c.text)
|
|
172
|
+
.join("");
|
|
173
|
+
if (text) {
|
|
174
|
+
text = text.replace(/\\n/g, "\n").replace(/\\t/g, "\t");
|
|
175
|
+
const out = mdStreamer.push(text);
|
|
176
|
+
if (out) writeStdout(out);
|
|
177
|
+
}
|
|
178
|
+
} else if (event.type === "tool_call") {
|
|
179
|
+
const toolName =
|
|
180
|
+
event.tool_call && typeof event.tool_call === "object" ? Object.keys(event.tool_call)[0] ?? "?" : "?";
|
|
181
|
+
const subtype = event.subtype ?? "";
|
|
182
|
+
if (options.onToolCall) options.onToolCall(subtype, toolName);
|
|
183
|
+
} else if (event.type === "thinking") {
|
|
184
|
+
if (options.onToolCall) options.onToolCall("thinking", "");
|
|
185
|
+
} else if (event.type === "result") {
|
|
186
|
+
lastResult = event;
|
|
187
|
+
if (event.subtype === "success" && !event.is_error) {
|
|
188
|
+
hadError = false;
|
|
189
|
+
} else {
|
|
190
|
+
hadError = true;
|
|
191
|
+
}
|
|
192
|
+
} else {
|
|
193
|
+
writeStdout(`[cursor-stdout] event: ${event.type ?? "unknown"}\n`);
|
|
194
|
+
}
|
|
195
|
+
} catch (_) {
|
|
196
|
+
let out;
|
|
197
|
+
if (line.includes('"type":"tool_call"') || line.includes('"type": "tool_call"')) {
|
|
198
|
+
let subtype = "?";
|
|
199
|
+
try {
|
|
200
|
+
const ev = JSON.parse(line);
|
|
201
|
+
if (ev && ev.type === "tool_call") subtype = ev.subtype ?? "?";
|
|
202
|
+
} catch {
|
|
203
|
+
const m = line.match(/"subtype"\s*:\s*"([^"]+)"/);
|
|
204
|
+
if (m) subtype = m[1];
|
|
205
|
+
}
|
|
206
|
+
out = `[cursor] tool_call ${subtype}\n`;
|
|
207
|
+
} else if (isLikelyBase64(line)) {
|
|
208
|
+
out = `[cursor-stdout] (base64 图片/数据, ${line.length} 字符)\n`;
|
|
209
|
+
} else if (debugStdout || line.length <= STDOUT_RAW_CAP) {
|
|
210
|
+
out = line + "\n";
|
|
211
|
+
} else if (lastResult == null) {
|
|
212
|
+
out = `[cursor-stdout] (非 JSON,可能为 Cursor 报错) ${line.slice(0, 500)}${line.length > 500 ? "..." : ""}\n`;
|
|
213
|
+
} else {
|
|
214
|
+
out = `[cursor-stdout] (解析失败或未处理的一行, ${line.length} 字符)\n`;
|
|
215
|
+
}
|
|
216
|
+
writeStdout(out);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
child.on("error", (err) => {
|
|
222
|
+
child.stdout?.removeAllListeners();
|
|
223
|
+
child.stderr?.removeAllListeners();
|
|
224
|
+
child.removeAllListeners();
|
|
225
|
+
reject(new Error(`Cursor CLI failed to start: ${err.message}. Ensure '${agentCmd}' is in PATH.`));
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
child.on("close", (code) => {
|
|
229
|
+
if (stdoutLineBuffer.trim() && flowName && uuid) {
|
|
230
|
+
appendRunLogLine(workspaceRoot, flowName, uuid, "cursor-stdout-raw", stdoutLineBuffer.trim());
|
|
231
|
+
}
|
|
232
|
+
child.stdout.removeAllListeners();
|
|
233
|
+
if (!useStderrInherit) child.stderr.removeAllListeners();
|
|
234
|
+
child.removeAllListeners();
|
|
235
|
+
const tail = mdStreamer.finish();
|
|
236
|
+
if (tail) writeStdout(tail);
|
|
237
|
+
if (coloredPrefix && stderrLineBuffer) {
|
|
238
|
+
writeWithPrefix(process.stderr, stderrLineBuffer.endsWith("\n") ? stderrLineBuffer : stderrLineBuffer + "\n", coloredPrefix);
|
|
239
|
+
}
|
|
240
|
+
if (code !== 0 && lastResult == null) {
|
|
241
|
+
const stderr = Buffer.concat(stderrChunks).toString("utf-8");
|
|
242
|
+
const stderrTail = stderr ? stderr.trim().slice(-1200) : "";
|
|
243
|
+
const autoOnly =
|
|
244
|
+
/named models unavailable/i.test(stderrTail) ||
|
|
245
|
+
(/free plans?/i.test(stderrTail) && /only use auto/i.test(stderrTail)) ||
|
|
246
|
+
/only use auto/i.test(stderrTail);
|
|
247
|
+
if (autoOnly && model !== "Auto" && !options._agentflowAutoRetry) {
|
|
248
|
+
writeStdout(t("runner.cursor_account_limit") + "\n");
|
|
249
|
+
runCursorAgentForNode(
|
|
250
|
+
workspaceRoot,
|
|
251
|
+
{ promptPath, nodeContext, taskBody, intermediatePath, resultPathRel, subagent, instanceId },
|
|
252
|
+
{ ...options, model: "Auto", _agentflowAutoRetry: true },
|
|
253
|
+
).then(resolve).catch(reject);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
const logHint =
|
|
257
|
+
flowName && uuid
|
|
258
|
+
? ` 检查 run 目录 logs/log.txt 查看完整 Cursor stderr;常见原因:未登录 Cursor、模型不可用、网络/权限。若无报错内容,可设置 AGENTFLOW_CURSOR_STDERR_INHERIT=1 后重跑,使 Cursor 的 stderr 直接输出到终端。`
|
|
259
|
+
: "";
|
|
260
|
+
const err = new Error(`Cursor CLI exited ${code}. ${stderrTail || "No result event received."}${logHint}`);
|
|
261
|
+
err.cursorStderrTail = stderrTail;
|
|
262
|
+
reject(err);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (hadError || (lastResult && lastResult.is_error)) {
|
|
266
|
+
reject(new Error(lastResult?.result || "Agent reported error."));
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
resolve();
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Run OpenCode CLI in non-interactive mode for a node.
|
|
276
|
+
*/
|
|
277
|
+
export function runOpenCodeAgentForNode(
|
|
278
|
+
workspaceRoot,
|
|
279
|
+
{ promptPath, nodeContext, taskBody, intermediatePath, resultPathRel, subagent, instanceId },
|
|
280
|
+
options = {},
|
|
281
|
+
) {
|
|
282
|
+
const absPromptPath = path.resolve(workspaceRoot, promptPath);
|
|
283
|
+
const absRunDir = path.resolve(workspaceRoot, intermediatePath);
|
|
284
|
+
const absResultPath = path.join(absRunDir, resultPathRel);
|
|
285
|
+
const nodeIntermediateDir = path.dirname(absPromptPath);
|
|
286
|
+
const outputDir = instanceId ? path.join(absRunDir, "output", instanceId) : path.join(absRunDir, "output");
|
|
287
|
+
if (instanceId) fs.mkdirSync(outputDir, { recursive: true });
|
|
288
|
+
const absWorkspaceRoot = path.resolve(workspaceRoot);
|
|
289
|
+
const replacements = {
|
|
290
|
+
workspaceRoot: absWorkspaceRoot,
|
|
291
|
+
promptPath: absPromptPath,
|
|
292
|
+
nodeContext: nodeContext ?? "",
|
|
293
|
+
taskBody: taskBody ?? "",
|
|
294
|
+
resultPath: absResultPath,
|
|
295
|
+
intermediatePath: path.join(absRunDir, "intermediate"),
|
|
296
|
+
outputDir,
|
|
297
|
+
flowName: options.flowName ?? "",
|
|
298
|
+
uuid: options.uuid ?? "",
|
|
299
|
+
instanceId: instanceId ?? "",
|
|
300
|
+
};
|
|
301
|
+
const agentContent = loadAgentPromptWithReplacements(workspaceRoot, subagent, replacements);
|
|
302
|
+
let agentPathForPrompt = getAgentPath(workspaceRoot, subagent);
|
|
303
|
+
if (agentContent) {
|
|
304
|
+
const resolvedAgentPath = path.join(nodeIntermediateDir, `agent-${subagent}.md`);
|
|
305
|
+
fs.mkdirSync(nodeIntermediateDir, { recursive: true });
|
|
306
|
+
fs.writeFileSync(resolvedAgentPath, agentContent, "utf8");
|
|
307
|
+
agentPathForPrompt = resolvedAgentPath;
|
|
308
|
+
}
|
|
309
|
+
const rawAgentContent =
|
|
310
|
+
agentContent != null
|
|
311
|
+
? agentContent
|
|
312
|
+
: fs.existsSync(agentPathForPrompt)
|
|
313
|
+
? fs.readFileSync(agentPathForPrompt, "utf8")
|
|
314
|
+
: "";
|
|
315
|
+
const promptText = stripYamlFrontmatter(rawAgentContent);
|
|
316
|
+
|
|
317
|
+
const model = options.model && String(options.model).trim();
|
|
318
|
+
const rawPrefix = options.outputPrefix != null ? `[${options.outputPrefix}] ` : "";
|
|
319
|
+
const coloredPrefix = rawPrefix && options.prefixColor ? options.prefixColor(rawPrefix) : rawPrefix;
|
|
320
|
+
const agentContentColor = options.contentColor ?? ((line) => chalk.gray(line));
|
|
321
|
+
|
|
322
|
+
return new Promise((resolve, reject) => {
|
|
323
|
+
const opencodeCmd = process.env.OPENCODE_CMD || "opencode";
|
|
324
|
+
const args = ["run"];
|
|
325
|
+
if (model) {
|
|
326
|
+
args.push("--model", model);
|
|
327
|
+
}
|
|
328
|
+
args.push("--dir", workspaceRoot);
|
|
329
|
+
args.push("--", promptText);
|
|
330
|
+
const spawnOpts = {
|
|
331
|
+
cwd: workspaceRoot,
|
|
332
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
333
|
+
shell: false,
|
|
334
|
+
};
|
|
335
|
+
if (options.force) {
|
|
336
|
+
spawnOpts.env = {
|
|
337
|
+
...process.env,
|
|
338
|
+
OPENCODE_CONFIG_CONTENT: JSON.stringify({
|
|
339
|
+
permission: { external_directory: "allow" },
|
|
340
|
+
}),
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
const child = spawn(opencodeCmd, args, spawnOpts);
|
|
344
|
+
const flowName = options.flowName ?? null;
|
|
345
|
+
const uuid = options.uuid ?? null;
|
|
346
|
+
|
|
347
|
+
let stdoutLogBuf = "";
|
|
348
|
+
let stderrLogBuf = "";
|
|
349
|
+
|
|
350
|
+
function drainLogBuf(buf, tag) {
|
|
351
|
+
let idx;
|
|
352
|
+
while ((idx = buf.indexOf("\n")) !== -1) {
|
|
353
|
+
const raw = buf.slice(0, idx);
|
|
354
|
+
buf = buf.slice(idx + 1);
|
|
355
|
+
const line = stripAnsi(raw).trimEnd();
|
|
356
|
+
if (line.trim() && flowName && uuid) {
|
|
357
|
+
appendRunLogLine(workspaceRoot, flowName, uuid, tag, line);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return buf;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function flushLogBuf(buf, tag) {
|
|
364
|
+
if (!buf) return;
|
|
365
|
+
const line = stripAnsi(buf).trimEnd();
|
|
366
|
+
if (line.trim() && flowName && uuid) {
|
|
367
|
+
appendRunLogLine(workspaceRoot, flowName, uuid, tag, line);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
child.stdout.setEncoding("utf-8");
|
|
372
|
+
child.stdout.on("data", (chunk) => {
|
|
373
|
+
if (coloredPrefix) writeWithPrefix(process.stdout, chunk, coloredPrefix, agentContentColor);
|
|
374
|
+
else process.stdout.write(agentContentColor(chunk));
|
|
375
|
+
stdoutLogBuf += String(chunk).replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
376
|
+
stdoutLogBuf = drainLogBuf(stdoutLogBuf, "opencode-stdout");
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
child.stderr.on("data", (chunk) => {
|
|
380
|
+
const s = typeof chunk === "string" ? chunk : chunk.toString("utf-8");
|
|
381
|
+
if (coloredPrefix) {
|
|
382
|
+
writeWithPrefix(process.stderr, s, coloredPrefix, agentContentColor);
|
|
383
|
+
} else {
|
|
384
|
+
process.stderr.write(chunk);
|
|
385
|
+
}
|
|
386
|
+
stderrLogBuf += s.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
387
|
+
stderrLogBuf = drainLogBuf(stderrLogBuf, "opencode-stderr");
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
child.on("error", (err) => {
|
|
391
|
+
child.stdout?.removeAllListeners();
|
|
392
|
+
child.stderr?.removeAllListeners();
|
|
393
|
+
child.removeAllListeners();
|
|
394
|
+
reject(new Error(`OpenCode CLI failed to start: ${err.message}. Ensure '${opencodeCmd}' is in PATH.`));
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
child.on("close", (code) => {
|
|
398
|
+
child.stdout.removeAllListeners();
|
|
399
|
+
child.stderr.removeAllListeners();
|
|
400
|
+
child.removeAllListeners();
|
|
401
|
+
flushLogBuf(stdoutLogBuf, "opencode-stdout");
|
|
402
|
+
flushLogBuf(stderrLogBuf, "opencode-stderr");
|
|
403
|
+
if (code !== 0) {
|
|
404
|
+
reject(new Error(`OpenCode CLI exited ${code}.`));
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
resolve();
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Run Claude Code CLI (`claude`) in non-interactive stream-json mode for a node.
|
|
414
|
+
* NDJSON event schema: system(init) / assistant(message.content[]) / user(tool_result) / result.
|
|
415
|
+
* Thinking and text both live as content blocks inside assistant events (not as top-level events).
|
|
416
|
+
*/
|
|
417
|
+
export function runClaudeCodeAgentForNode(
|
|
418
|
+
workspaceRoot,
|
|
419
|
+
{ promptPath, nodeContext, taskBody, intermediatePath, resultPathRel, subagent, instanceId },
|
|
420
|
+
options = {},
|
|
421
|
+
) {
|
|
422
|
+
const absPromptPath = path.resolve(workspaceRoot, promptPath);
|
|
423
|
+
const absRunDir = path.resolve(workspaceRoot, intermediatePath);
|
|
424
|
+
const absResultPath = path.join(absRunDir, resultPathRel);
|
|
425
|
+
const nodeIntermediateDir = path.dirname(absPromptPath);
|
|
426
|
+
const outputDir = instanceId ? path.join(absRunDir, "output", instanceId) : path.join(absRunDir, "output");
|
|
427
|
+
if (instanceId) fs.mkdirSync(outputDir, { recursive: true });
|
|
428
|
+
const absWorkspaceRoot = path.resolve(workspaceRoot);
|
|
429
|
+
const replacements = {
|
|
430
|
+
workspaceRoot: absWorkspaceRoot,
|
|
431
|
+
promptPath: absPromptPath,
|
|
432
|
+
nodeContext: nodeContext ?? "",
|
|
433
|
+
taskBody: taskBody ?? "",
|
|
434
|
+
resultPath: absResultPath,
|
|
435
|
+
intermediatePath: path.join(absRunDir, "intermediate"),
|
|
436
|
+
outputDir,
|
|
437
|
+
flowName: options.flowName ?? "",
|
|
438
|
+
uuid: options.uuid ?? "",
|
|
439
|
+
instanceId: instanceId ?? "",
|
|
440
|
+
};
|
|
441
|
+
const agentContent = loadAgentPromptWithReplacements(workspaceRoot, subagent, replacements);
|
|
442
|
+
let agentPathForPrompt = getAgentPath(workspaceRoot, subagent);
|
|
443
|
+
if (agentContent) {
|
|
444
|
+
const resolvedAgentPath = path.join(nodeIntermediateDir, `agent-${subagent}.md`);
|
|
445
|
+
fs.mkdirSync(nodeIntermediateDir, { recursive: true });
|
|
446
|
+
fs.writeFileSync(resolvedAgentPath, agentContent, "utf8");
|
|
447
|
+
agentPathForPrompt = resolvedAgentPath;
|
|
448
|
+
}
|
|
449
|
+
const rawAgentContent =
|
|
450
|
+
agentContent != null
|
|
451
|
+
? agentContent
|
|
452
|
+
: fs.existsSync(agentPathForPrompt)
|
|
453
|
+
? fs.readFileSync(agentPathForPrompt, "utf8")
|
|
454
|
+
: "";
|
|
455
|
+
const promptText = stripYamlFrontmatter(rawAgentContent);
|
|
456
|
+
|
|
457
|
+
const model = options.model && String(options.model).trim();
|
|
458
|
+
const rawPrefix = options.outputPrefix != null ? `[${options.outputPrefix}] ` : "";
|
|
459
|
+
const coloredPrefix = rawPrefix && options.prefixColor ? options.prefixColor(rawPrefix) : rawPrefix;
|
|
460
|
+
const agentContentColor = options.contentColor ?? ((line) => chalk.gray(line));
|
|
461
|
+
|
|
462
|
+
return new Promise((resolve, reject) => {
|
|
463
|
+
const claudeCmd = process.env.CLAUDE_CODE_CMD || "claude";
|
|
464
|
+
const bypassPermissions =
|
|
465
|
+
process.env.AGENTFLOW_CLAUDE_CODE_BYPASS_PERMISSIONS !== "0" &&
|
|
466
|
+
process.env.AGENTFLOW_CLAUDE_CODE_BYPASS_PERMISSIONS !== "false";
|
|
467
|
+
const args = ["-p", "--output-format", "stream-json", "--verbose", "--add-dir", workspaceRoot];
|
|
468
|
+
if (bypassPermissions) args.push("--dangerously-skip-permissions");
|
|
469
|
+
if (model) args.push("--model", model);
|
|
470
|
+
args.push(promptText);
|
|
471
|
+
if (options.flowName && options.uuid) {
|
|
472
|
+
const argvLog = args.slice(0, -1).concat([`(prompt ${args[args.length - 1].length} chars)`]);
|
|
473
|
+
appendRunLogLine(
|
|
474
|
+
workspaceRoot,
|
|
475
|
+
options.flowName,
|
|
476
|
+
options.uuid,
|
|
477
|
+
"cli-raw",
|
|
478
|
+
`Claude Code CLI 完整参数: ${claudeCmd} ${JSON.stringify(argvLog)}`,
|
|
479
|
+
);
|
|
480
|
+
appendRunLogLine(
|
|
481
|
+
workspaceRoot,
|
|
482
|
+
options.flowName,
|
|
483
|
+
options.uuid,
|
|
484
|
+
"cli-raw",
|
|
485
|
+
`Claude Code CLI prompt 前 800 字:\n${promptText.slice(0, 800)}${promptText.length > 800 ? "..." : ""}`,
|
|
486
|
+
);
|
|
487
|
+
appendRunLogLine(workspaceRoot, options.flowName, options.uuid, "cli-raw", `Claude Code CLI prompt 完整:\n${promptText}`);
|
|
488
|
+
}
|
|
489
|
+
const useStderrInherit =
|
|
490
|
+
process.env.AGENTFLOW_CLAUDE_CODE_STDERR_INHERIT === "1" ||
|
|
491
|
+
process.env.AGENTFLOW_CLAUDE_CODE_STDERR_INHERIT === "true";
|
|
492
|
+
const child = spawn(claudeCmd, args, {
|
|
493
|
+
cwd: workspaceRoot,
|
|
494
|
+
stdio: ["ignore", "pipe", useStderrInherit ? "inherit" : "pipe"],
|
|
495
|
+
shell: false,
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
let lastResult = null;
|
|
499
|
+
let hadError = false;
|
|
500
|
+
let sessionId = null;
|
|
501
|
+
const STDERR_CAP_BYTES = 1024 * 1024;
|
|
502
|
+
const stderrChunks = [];
|
|
503
|
+
let stderrTotalBytes = 0;
|
|
504
|
+
const stderrBuffer = options.stderrBuffer || null;
|
|
505
|
+
let stderrLineBuffer = "";
|
|
506
|
+
const flowName = options.flowName ?? null;
|
|
507
|
+
const uuid = options.uuid ?? null;
|
|
508
|
+
|
|
509
|
+
const outStream = machineReadable ? process.stderr : process.stdout;
|
|
510
|
+
function writeStdout(text) {
|
|
511
|
+
if (coloredPrefix) writeWithPrefix(outStream, text, coloredPrefix, agentContentColor);
|
|
512
|
+
else if (text) outStream.write(agentContentColor(text));
|
|
513
|
+
if (text && flowName && uuid) appendRunLogLine(workspaceRoot, flowName, uuid, "claude-code-stdout", text);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function flushStderrLines() {
|
|
517
|
+
if (!coloredPrefix) return;
|
|
518
|
+
let idx;
|
|
519
|
+
while ((idx = stderrLineBuffer.indexOf("\n")) !== -1) {
|
|
520
|
+
const line = stderrLineBuffer.slice(0, idx + 1);
|
|
521
|
+
stderrLineBuffer = stderrLineBuffer.slice(idx + 1);
|
|
522
|
+
writeWithPrefix(process.stderr, line, coloredPrefix, agentContentColor);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if (!useStderrInherit) {
|
|
527
|
+
child.stderr.on("data", (chunk) => {
|
|
528
|
+
const s = typeof chunk === "string" ? chunk : chunk.toString("utf-8");
|
|
529
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, "utf-8");
|
|
530
|
+
const len = buf.length;
|
|
531
|
+
while (stderrChunks.length > 0 && stderrTotalBytes + len > STDERR_CAP_BYTES) {
|
|
532
|
+
const drop = stderrChunks.shift();
|
|
533
|
+
stderrTotalBytes -= Buffer.isBuffer(drop) ? drop.length : Buffer.byteLength(drop, "utf-8");
|
|
534
|
+
}
|
|
535
|
+
stderrChunks.push(buf);
|
|
536
|
+
stderrTotalBytes += len;
|
|
537
|
+
if (stderrBuffer) {
|
|
538
|
+
stderrBuffer.push(chunk);
|
|
539
|
+
} else if (coloredPrefix) {
|
|
540
|
+
stderrLineBuffer += s;
|
|
541
|
+
flushStderrLines();
|
|
542
|
+
} else {
|
|
543
|
+
process.stderr.write(chunk);
|
|
544
|
+
}
|
|
545
|
+
if (flowName && uuid) appendRunLogLine(workspaceRoot, flowName, uuid, "claude-code-stderr", s);
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const stdoutWidth = process.stdout.columns ?? 80;
|
|
550
|
+
const mdStreamer = createMarkdownStreamer({
|
|
551
|
+
render: (md) => renderMarkdown(md, { width: stdoutWidth }),
|
|
552
|
+
spacing: "single",
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
child.stdout.setEncoding("utf-8");
|
|
556
|
+
let stdoutLineBuffer = "";
|
|
557
|
+
child.stdout.on("data", (chunk) => {
|
|
558
|
+
stdoutLineBuffer += chunk;
|
|
559
|
+
const idx = stdoutLineBuffer.lastIndexOf("\n");
|
|
560
|
+
const complete = idx >= 0 ? stdoutLineBuffer.slice(0, idx) : "";
|
|
561
|
+
if (idx >= 0) stdoutLineBuffer = stdoutLineBuffer.slice(idx + 1);
|
|
562
|
+
const lines = complete.split("\n").filter(Boolean);
|
|
563
|
+
for (const line of lines) {
|
|
564
|
+
if (flowName && uuid) appendRunLogLine(workspaceRoot, flowName, uuid, "claude-code-stdout-raw", line);
|
|
565
|
+
try {
|
|
566
|
+
const event = JSON.parse(line);
|
|
567
|
+
if (event && typeof event === "object" && event.session_id && !sessionId) {
|
|
568
|
+
sessionId = event.session_id;
|
|
569
|
+
}
|
|
570
|
+
if (event.type === "system") {
|
|
571
|
+
// init 等元事件,仅记录
|
|
572
|
+
} else if (event.type === "assistant" && event.message && Array.isArray(event.message.content)) {
|
|
573
|
+
for (const block of event.message.content) {
|
|
574
|
+
if (!block || typeof block !== "object") continue;
|
|
575
|
+
if (block.type === "text" && block.text) {
|
|
576
|
+
const text = normalizeStreamTextChunk(block.text);
|
|
577
|
+
const out = mdStreamer.push(text);
|
|
578
|
+
if (out) writeStdout(out);
|
|
579
|
+
} else if (block.type === "thinking") {
|
|
580
|
+
if (options.onToolCall) options.onToolCall("thinking", "");
|
|
581
|
+
} else if (block.type === "tool_use") {
|
|
582
|
+
const toolName = block.name || "?";
|
|
583
|
+
if (options.onToolCall) options.onToolCall("tool_use", toolName);
|
|
584
|
+
writeStdout(`[claude-code] tool ${toolName}\n`);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
} else if (event.type === "user" && event.message && Array.isArray(event.message.content)) {
|
|
588
|
+
// tool_result 回传;不向用户 stdout 渲染,只记录
|
|
589
|
+
} else if (event.type === "result") {
|
|
590
|
+
lastResult = event;
|
|
591
|
+
const isSuccess = event.subtype === "success" && !event.is_error;
|
|
592
|
+
hadError = !isSuccess;
|
|
593
|
+
} else {
|
|
594
|
+
writeStdout(`[claude-code-stdout] event: ${event.type ?? "unknown"}\n`);
|
|
595
|
+
}
|
|
596
|
+
} catch (_) {
|
|
597
|
+
writeStdout(`[claude-code-stdout] (非 JSON) ${line.slice(0, 500)}${line.length > 500 ? "..." : ""}\n`);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
child.on("error", (err) => {
|
|
603
|
+
child.stdout?.removeAllListeners();
|
|
604
|
+
child.stderr?.removeAllListeners();
|
|
605
|
+
child.removeAllListeners();
|
|
606
|
+
reject(
|
|
607
|
+
new Error(
|
|
608
|
+
`Claude Code CLI failed to start: ${err.message}. Install via 'npm i -g @anthropic-ai/claude-code' and run 'claude /login', or set CLAUDE_CODE_CMD.`,
|
|
609
|
+
),
|
|
610
|
+
);
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
child.on("close", (code) => {
|
|
614
|
+
if (stdoutLineBuffer.trim() && flowName && uuid) {
|
|
615
|
+
appendRunLogLine(workspaceRoot, flowName, uuid, "claude-code-stdout-raw", stdoutLineBuffer.trim());
|
|
616
|
+
}
|
|
617
|
+
child.stdout.removeAllListeners();
|
|
618
|
+
if (!useStderrInherit) child.stderr.removeAllListeners();
|
|
619
|
+
child.removeAllListeners();
|
|
620
|
+
const tail = mdStreamer.finish();
|
|
621
|
+
if (tail) writeStdout(tail);
|
|
622
|
+
if (coloredPrefix && stderrLineBuffer) {
|
|
623
|
+
writeWithPrefix(process.stderr, stderrLineBuffer.endsWith("\n") ? stderrLineBuffer : stderrLineBuffer + "\n", coloredPrefix);
|
|
624
|
+
}
|
|
625
|
+
if (code !== 0 && lastResult == null) {
|
|
626
|
+
const stderr = Buffer.concat(stderrChunks).toString("utf-8");
|
|
627
|
+
const stderrTail = stderr ? stderr.trim().slice(-1200) : "";
|
|
628
|
+
const logHint =
|
|
629
|
+
flowName && uuid
|
|
630
|
+
? ` 检查 run 目录 logs/log.txt 查看完整 Claude Code stderr;常见原因:未登录 claude /login、模型不可用、网络/权限。若无报错内容,可设置 AGENTFLOW_CLAUDE_CODE_STDERR_INHERIT=1 后重跑。`
|
|
631
|
+
: "";
|
|
632
|
+
const err = new Error(`Claude Code CLI exited ${code}. ${stderrTail || "No result event received."}${logHint}`);
|
|
633
|
+
err.claudeCodeStderrTail = stderrTail;
|
|
634
|
+
reject(err);
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
if (hadError || (lastResult && lastResult.is_error)) {
|
|
638
|
+
const msg =
|
|
639
|
+
(lastResult && typeof lastResult.result === "string" && lastResult.result) ||
|
|
640
|
+
(lastResult && lastResult.subtype) ||
|
|
641
|
+
"Agent reported error.";
|
|
642
|
+
reject(new Error(String(msg)));
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
resolve();
|
|
646
|
+
});
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
const COMPOSER_STATUS_MAX = 200;
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* 去除 ANSI escape(颜色/光标控制 / OSC / 私有序列)。OpenCode `run` 模式 stdout 走 TUI 渲染,
|
|
654
|
+
* 含大量 \x1b[...m / \x1b]...BEL 类序列。Cursor/OpenCode 的 stderr 也常带这些。
|
|
655
|
+
*/
|
|
656
|
+
const ANSI_ESCAPE_RE = /[\u001B\u009B][[\]()#;?]*(?:(?:(?:[a-zA-Z\d]*(?:;[a-zA-Z\d]*)*)?\u0007)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PRZcf-ntqry=><~]))/g;
|
|
657
|
+
|
|
658
|
+
function stripAnsi(s) {
|
|
659
|
+
return String(s || "").replace(ANSI_ESCAPE_RE, "");
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function truncateComposerLine(s) {
|
|
663
|
+
const t = stripAnsi(s).replace(/\s+/g, " ").trim();
|
|
664
|
+
if (t.length <= COMPOSER_STATUS_MAX) return t;
|
|
665
|
+
return t.slice(0, COMPOSER_STATUS_MAX - 1) + "…";
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function normalizeStreamTextChunk(t) {
|
|
669
|
+
if (!t || typeof t !== "string") return "";
|
|
670
|
+
return t.replace(/\\n/g, "\n").replace(/\\t/g, "\t");
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/** Cursor stream-json:从 message.content 等提取可展示正文(不含 JSON 包装) */
|
|
674
|
+
function extractCursorStreamNlText(event) {
|
|
675
|
+
if (!event || typeof event !== "object") return "";
|
|
676
|
+
const content = event.message?.content;
|
|
677
|
+
if (Array.isArray(content)) {
|
|
678
|
+
const parts = content
|
|
679
|
+
.filter((c) => c && (c.type === "text" || c.type === "thinking") && c.text)
|
|
680
|
+
.map((c) => c.text);
|
|
681
|
+
if (parts.length) return normalizeStreamTextChunk(parts.join(""));
|
|
682
|
+
}
|
|
683
|
+
if (typeof event.text === "string" && event.text.trim()) return normalizeStreamTextChunk(event.text);
|
|
684
|
+
if (typeof event.thinking === "string" && event.thinking.trim()) return normalizeStreamTextChunk(event.thinking);
|
|
685
|
+
return "";
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/** result 事件中仅推送可读字符串,跳过 JSON 形态 */
|
|
689
|
+
function extractCursorResultNl(event) {
|
|
690
|
+
if (!event || typeof event !== "object") return "";
|
|
691
|
+
const r = event.result;
|
|
692
|
+
if (typeof r !== "string" || !r.trim()) return "";
|
|
693
|
+
const t = r.trim();
|
|
694
|
+
if ((t.startsWith("{") && t.endsWith("}")) || (t.startsWith("[") && t.endsWith("]"))) {
|
|
695
|
+
try {
|
|
696
|
+
JSON.parse(t);
|
|
697
|
+
return "";
|
|
698
|
+
} catch {
|
|
699
|
+
return normalizeStreamTextChunk(r);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
return normalizeStreamTextChunk(r);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function tryEmitOpenCodeLineAsNatural(line, emit) {
|
|
706
|
+
const raw = String(line || "");
|
|
707
|
+
// OpenCode run 模式输出 TUI 渲染(CR 覆盖 + ANSI 颜色),先清掉再判断
|
|
708
|
+
const cleaned = stripAnsi(raw).replace(/\r/g, "").trim();
|
|
709
|
+
if (!cleaned) return;
|
|
710
|
+
try {
|
|
711
|
+
const ev = JSON.parse(cleaned);
|
|
712
|
+
if (ev && typeof ev === "object") {
|
|
713
|
+
const ty = ev.type;
|
|
714
|
+
if (ty === "thinking" || ty === "assistant") {
|
|
715
|
+
const text = extractCursorStreamNlText(ev);
|
|
716
|
+
if (text) emit({ type: "natural", kind: ty === "thinking" ? "thinking" : "assistant", text });
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
if (ty === "result") {
|
|
720
|
+
const text = extractCursorResultNl(ev);
|
|
721
|
+
if (text) emit({ type: "natural", kind: "result", text });
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
} catch {
|
|
726
|
+
/* 非 JSON,按正文行处理 */
|
|
727
|
+
}
|
|
728
|
+
if (cleaned.startsWith("{") || cleaned.startsWith("[")) return;
|
|
729
|
+
emit({ type: "natural", kind: "assistant", text: cleaned });
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* Cursor CLI:纯文本 prompt,供 Composer / UI 流式使用;不写 process stdout/stderr。
|
|
734
|
+
* @returns {{ child: import('child_process').ChildProcess, finished: Promise<void> }}
|
|
735
|
+
*/
|
|
736
|
+
export function runCursorAgentWithPrompt(cliWorkspace, promptText, options = {}) {
|
|
737
|
+
const onStreamEvent = typeof options.onStreamEvent === "function" ? options.onStreamEvent : null;
|
|
738
|
+
const ws = path.resolve(cliWorkspace);
|
|
739
|
+
const model = normalizeCursorModelForCli(options.model ?? process.env.CURSOR_AGENT_MODEL ?? null);
|
|
740
|
+
const agentCmd = process.env.CURSOR_AGENT_CMD || "agent";
|
|
741
|
+
// Web UI Composer 需要能无交互执行本机 curl 等命令来刷新画布。
|
|
742
|
+
const args = ["--print", "--output-format", "stream-json", "--trust", "--sandbox", "disabled", "--workspace", ws];
|
|
743
|
+
const approveMcps = process.env.AGENTFLOW_CURSOR_APPROVE_MCPS !== "0" && process.env.AGENTFLOW_CURSOR_APPROVE_MCPS !== "false";
|
|
744
|
+
if (approveMcps) args.push("--approve-mcps");
|
|
745
|
+
args.push("--force");
|
|
746
|
+
args.push("--model", model);
|
|
747
|
+
args.push(promptText);
|
|
748
|
+
|
|
749
|
+
const useStderrInherit = process.env.AGENTFLOW_CURSOR_STDERR_INHERIT === "1" || process.env.AGENTFLOW_CURSOR_STDERR_INHERIT === "true";
|
|
750
|
+
const child = spawn(agentCmd, args, {
|
|
751
|
+
cwd: ws,
|
|
752
|
+
stdio: ["ignore", "pipe", useStderrInherit ? "inherit" : "pipe"],
|
|
753
|
+
shell: false,
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
let lastResult = null;
|
|
757
|
+
let hadError = false;
|
|
758
|
+
const STDERR_CAP_BYTES = 1024 * 1024;
|
|
759
|
+
const stderrChunks = [];
|
|
760
|
+
let stderrTotalBytes = 0;
|
|
761
|
+
let stderrComposerBuffer = "";
|
|
762
|
+
|
|
763
|
+
const emit = (payload) => {
|
|
764
|
+
try {
|
|
765
|
+
onStreamEvent?.(payload);
|
|
766
|
+
} catch (_) {}
|
|
767
|
+
};
|
|
768
|
+
|
|
769
|
+
if (!useStderrInherit) {
|
|
770
|
+
child.stderr.on("data", (chunk) => {
|
|
771
|
+
const s = typeof chunk === "string" ? chunk : chunk.toString("utf-8");
|
|
772
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, "utf-8");
|
|
773
|
+
const len = buf.length;
|
|
774
|
+
while (stderrChunks.length > 0 && stderrTotalBytes + len > STDERR_CAP_BYTES) {
|
|
775
|
+
const drop = stderrChunks.shift();
|
|
776
|
+
stderrTotalBytes -= Buffer.isBuffer(drop) ? drop.length : Buffer.byteLength(drop, "utf-8");
|
|
777
|
+
}
|
|
778
|
+
stderrChunks.push(buf);
|
|
779
|
+
stderrTotalBytes += len;
|
|
780
|
+
stderrComposerBuffer += s;
|
|
781
|
+
let idx;
|
|
782
|
+
while ((idx = stderrComposerBuffer.indexOf("\n")) !== -1) {
|
|
783
|
+
const line = stderrComposerBuffer.slice(0, idx);
|
|
784
|
+
stderrComposerBuffer = stderrComposerBuffer.slice(idx + 1);
|
|
785
|
+
if (line.trim()) {
|
|
786
|
+
emit({ type: "status", line: `[stderr] ${truncateComposerLine(line)}` });
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
const stdoutWidth = 80;
|
|
793
|
+
const mdStreamer = createMarkdownStreamer({
|
|
794
|
+
render: (md) => renderMarkdown(md, { width: stdoutWidth }),
|
|
795
|
+
spacing: "single",
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
const STDOUT_RAW_CAP = 200;
|
|
799
|
+
const debugStdout = process.env.AGENTFLOW_DEBUG_STDOUT === "1" || process.env.AGENTFLOW_DEBUG_STDOUT === "true";
|
|
800
|
+
|
|
801
|
+
function isLikelyBase64(s) {
|
|
802
|
+
if (!s || typeof s !== "string") return false;
|
|
803
|
+
const t = s.trim();
|
|
804
|
+
if (t.startsWith("data:image/") && t.includes(";base64,")) return true;
|
|
805
|
+
if (t.length < 80) return false;
|
|
806
|
+
return /^[A-Za-z0-9+/]+=*$/.test(t);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
child.stdout.setEncoding("utf-8");
|
|
810
|
+
let stdoutLineBuffer = "";
|
|
811
|
+
child.stdout.on("data", (chunk) => {
|
|
812
|
+
stdoutLineBuffer += chunk;
|
|
813
|
+
const idx = stdoutLineBuffer.lastIndexOf("\n");
|
|
814
|
+
const complete = idx >= 0 ? stdoutLineBuffer.slice(0, idx) : "";
|
|
815
|
+
if (idx >= 0) stdoutLineBuffer = stdoutLineBuffer.slice(idx + 1);
|
|
816
|
+
const lines = complete.split("\n").filter(Boolean);
|
|
817
|
+
for (const line of lines) {
|
|
818
|
+
try {
|
|
819
|
+
const event = JSON.parse(line);
|
|
820
|
+
if (event.type === "assistant" && event.message?.content) {
|
|
821
|
+
const text = extractCursorStreamNlText(event);
|
|
822
|
+
if (text) {
|
|
823
|
+
emit({ type: "natural", kind: "assistant", text });
|
|
824
|
+
mdStreamer.push(text);
|
|
825
|
+
emit({ type: "status", line: t("runner.generating_reply") });
|
|
826
|
+
}
|
|
827
|
+
} else if (event.type === "tool_call") {
|
|
828
|
+
const toolName =
|
|
829
|
+
event.tool_call && typeof event.tool_call === "object" ? Object.keys(event.tool_call)[0] ?? "?" : "?";
|
|
830
|
+
const subtype = event.subtype ?? "";
|
|
831
|
+
const statusLine = `工具 ${toolName}${subtype ? ` (${subtype})` : ""}`;
|
|
832
|
+
emit({ type: "status", line: statusLine });
|
|
833
|
+
if (options.onToolCall) options.onToolCall(subtype, toolName);
|
|
834
|
+
} else if (event.type === "thinking") {
|
|
835
|
+
const thinkText = extractCursorStreamNlText(event);
|
|
836
|
+
if (thinkText) emit({ type: "natural", kind: "thinking", text: thinkText });
|
|
837
|
+
emit({ type: "status", line: t("runner.thinking") });
|
|
838
|
+
if (options.onToolCall) options.onToolCall("thinking", "");
|
|
839
|
+
} else if (event.type === "result") {
|
|
840
|
+
lastResult = event;
|
|
841
|
+
const resultNl = extractCursorResultNl(event);
|
|
842
|
+
if (resultNl) emit({ type: "natural", kind: "result", text: resultNl });
|
|
843
|
+
if (event.subtype === "success" && !event.is_error) {
|
|
844
|
+
hadError = false;
|
|
845
|
+
emit({ type: "status", line: t("runner.completed") });
|
|
846
|
+
} else {
|
|
847
|
+
hadError = true;
|
|
848
|
+
const errNl = extractCursorResultNl(event);
|
|
849
|
+
if (errNl) emit({ type: "natural", kind: "error", text: errNl });
|
|
850
|
+
emit({
|
|
851
|
+
type: "status",
|
|
852
|
+
line: truncateComposerLine(String(event.result || t("runner.execution_failed"))),
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
} else {
|
|
856
|
+
emit({ type: "status", line: `${t("runner.event_label")}: ${event.type ?? "unknown"}` });
|
|
857
|
+
}
|
|
858
|
+
} catch (_) {
|
|
859
|
+
if (line.includes('"type":"tool_call"') || line.includes('"type": "tool_call"')) {
|
|
860
|
+
let subtype = "?";
|
|
861
|
+
try {
|
|
862
|
+
const ev = JSON.parse(line);
|
|
863
|
+
if (ev && ev.type === "tool_call") subtype = ev.subtype ?? "?";
|
|
864
|
+
} catch {
|
|
865
|
+
const m = line.match(/"subtype"\s*:\s*"([^"]+)"/);
|
|
866
|
+
if (m) subtype = m[1];
|
|
867
|
+
}
|
|
868
|
+
emit({ type: "status", line: t("runner.tool_call", { subtype }) });
|
|
869
|
+
} else if (isLikelyBase64(line)) {
|
|
870
|
+
emit({ type: "status", line: t("runner.base64_data", { len: line.length }) });
|
|
871
|
+
} else if (debugStdout || line.length <= STDOUT_RAW_CAP) {
|
|
872
|
+
emit({ type: "status", line: truncateComposerLine(line) });
|
|
873
|
+
} else if (lastResult == null) {
|
|
874
|
+
emit({
|
|
875
|
+
type: "status",
|
|
876
|
+
line: truncateComposerLine(t("runner.non_json_line", { preview: line.slice(0, 500) + (line.length > 500 ? "..." : "") })),
|
|
877
|
+
});
|
|
878
|
+
} else {
|
|
879
|
+
emit({ type: "status", line: t("runner.unparsed_line", { len: line.length }) });
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
const finished = new Promise((resolve, reject) => {
|
|
886
|
+
child.on("error", (err) => {
|
|
887
|
+
child.stdout?.removeAllListeners();
|
|
888
|
+
child.stderr?.removeAllListeners();
|
|
889
|
+
child.removeAllListeners();
|
|
890
|
+
reject(new Error(`Cursor CLI failed to start: ${err.message}. Ensure '${agentCmd}' is in PATH.`));
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
child.on("close", (code) => {
|
|
894
|
+
child.stdout.removeAllListeners();
|
|
895
|
+
if (!useStderrInherit) child.stderr.removeAllListeners();
|
|
896
|
+
child.removeAllListeners();
|
|
897
|
+
mdStreamer.finish();
|
|
898
|
+
if (!useStderrInherit && stderrComposerBuffer.trim()) {
|
|
899
|
+
const rest = stderrComposerBuffer.trim();
|
|
900
|
+
emit({ type: "status", line: `[stderr] ${truncateComposerLine(rest)}` });
|
|
901
|
+
}
|
|
902
|
+
if (code !== 0 && lastResult == null) {
|
|
903
|
+
const stderr = Buffer.concat(stderrChunks).toString("utf-8");
|
|
904
|
+
const stderrTail = stderr ? stderr.trim().slice(-1200) : "";
|
|
905
|
+
const err = new Error(`Cursor CLI exited ${code}. ${stderrTail || "No result event received."}`);
|
|
906
|
+
err.cursorStderrTail = stderrTail;
|
|
907
|
+
emit({ type: "status", line: truncateComposerLine(err.message) });
|
|
908
|
+
reject(err);
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
if (hadError || (lastResult && lastResult.is_error)) {
|
|
912
|
+
const msg = lastResult?.result || "Agent reported error.";
|
|
913
|
+
emit({ type: "status", line: truncateComposerLine(msg) });
|
|
914
|
+
reject(new Error(msg));
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
resolve();
|
|
918
|
+
});
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
return { child, finished };
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
/**
|
|
925
|
+
* OpenCode CLI:纯文本 prompt,供 Composer / UI;不写 process stdout/stderr。
|
|
926
|
+
* @returns {{ child: import('child_process').ChildProcess, finished: Promise<void> }}
|
|
927
|
+
*/
|
|
928
|
+
export function runOpenCodeAgentWithPrompt(cliWorkspace, promptText, options = {}) {
|
|
929
|
+
const onStreamEvent = typeof options.onStreamEvent === "function" ? options.onStreamEvent : null;
|
|
930
|
+
const ws = path.resolve(cliWorkspace);
|
|
931
|
+
const model = options.model && String(options.model).trim();
|
|
932
|
+
const opencodeCmd = process.env.OPENCODE_CMD || "opencode";
|
|
933
|
+
const args = ["run"];
|
|
934
|
+
if (model) args.push("--model", model);
|
|
935
|
+
args.push("--dir", ws);
|
|
936
|
+
args.push("--", promptText);
|
|
937
|
+
|
|
938
|
+
const spawnOpts = {
|
|
939
|
+
cwd: ws,
|
|
940
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
941
|
+
shell: false,
|
|
942
|
+
};
|
|
943
|
+
if (options.force) {
|
|
944
|
+
spawnOpts.env = {
|
|
945
|
+
...process.env,
|
|
946
|
+
OPENCODE_CONFIG_CONTENT: JSON.stringify({
|
|
947
|
+
permission: { external_directory: "allow" },
|
|
948
|
+
}),
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
const child = spawn(opencodeCmd, args, spawnOpts);
|
|
953
|
+
|
|
954
|
+
const emit = (payload) => {
|
|
955
|
+
try {
|
|
956
|
+
onStreamEvent?.(payload);
|
|
957
|
+
} catch (_) {}
|
|
958
|
+
};
|
|
959
|
+
|
|
960
|
+
let outBuf = "";
|
|
961
|
+
let errBuf = "";
|
|
962
|
+
|
|
963
|
+
child.stdout.setEncoding("utf-8");
|
|
964
|
+
child.stdout.on("data", (chunk) => {
|
|
965
|
+
const s = typeof chunk === "string" ? chunk : chunk.toString("utf-8");
|
|
966
|
+
outBuf += s;
|
|
967
|
+
let idx;
|
|
968
|
+
while ((idx = outBuf.indexOf("\n")) !== -1) {
|
|
969
|
+
const line = outBuf.slice(0, idx);
|
|
970
|
+
outBuf = outBuf.slice(idx + 1);
|
|
971
|
+
if (line) {
|
|
972
|
+
tryEmitOpenCodeLineAsNatural(line, emit);
|
|
973
|
+
emit({ type: "status", line: `[stdout] ${truncateComposerLine(line)}` });
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
});
|
|
977
|
+
|
|
978
|
+
child.stderr.setEncoding("utf-8");
|
|
979
|
+
child.stderr.on("data", (chunk) => {
|
|
980
|
+
const s = typeof chunk === "string" ? chunk : chunk.toString("utf-8");
|
|
981
|
+
errBuf += s;
|
|
982
|
+
let idx;
|
|
983
|
+
while ((idx = errBuf.indexOf("\n")) !== -1) {
|
|
984
|
+
const line = errBuf.slice(0, idx);
|
|
985
|
+
errBuf = errBuf.slice(idx + 1);
|
|
986
|
+
if (line) {
|
|
987
|
+
tryEmitOpenCodeLineAsNatural(line, emit);
|
|
988
|
+
emit({ type: "status", line: `[stderr] ${truncateComposerLine(line)}` });
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
});
|
|
992
|
+
|
|
993
|
+
const finished = new Promise((resolve, reject) => {
|
|
994
|
+
child.on("error", (err) => {
|
|
995
|
+
child.stdout?.removeAllListeners();
|
|
996
|
+
child.stderr?.removeAllListeners();
|
|
997
|
+
child.removeAllListeners();
|
|
998
|
+
reject(new Error(`OpenCode CLI failed to start: ${err.message}. Ensure '${opencodeCmd}' is in PATH.`));
|
|
999
|
+
});
|
|
1000
|
+
|
|
1001
|
+
child.on("close", (code) => {
|
|
1002
|
+
child.stdout.removeAllListeners();
|
|
1003
|
+
child.stderr.removeAllListeners();
|
|
1004
|
+
child.removeAllListeners();
|
|
1005
|
+
if (outBuf.trim()) {
|
|
1006
|
+
tryEmitOpenCodeLineAsNatural(outBuf.trim(), emit);
|
|
1007
|
+
emit({ type: "status", line: truncateComposerLine(outBuf.trim()) });
|
|
1008
|
+
}
|
|
1009
|
+
if (errBuf.trim()) {
|
|
1010
|
+
tryEmitOpenCodeLineAsNatural(errBuf.trim(), emit);
|
|
1011
|
+
emit({ type: "status", line: `[opencode_stderr] ${truncateComposerLine(errBuf.trim())}` });
|
|
1012
|
+
}
|
|
1013
|
+
if (code !== 0) {
|
|
1014
|
+
emit({ type: "status", line: t("runner.opencode_exit_code", { code }) });
|
|
1015
|
+
reject(new Error(`OpenCode CLI exited ${code}.`));
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
emit({ type: "status", line: t("runner.done") });
|
|
1019
|
+
resolve();
|
|
1020
|
+
});
|
|
1021
|
+
});
|
|
1022
|
+
|
|
1023
|
+
return { child, finished };
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
/**
|
|
1027
|
+
* Claude Code CLI:纯文本 prompt,供 Composer / UI 流式使用;不写 process stdout/stderr。
|
|
1028
|
+
* @returns {{ child: import('child_process').ChildProcess, finished: Promise<void> }}
|
|
1029
|
+
*/
|
|
1030
|
+
export function runClaudeCodeAgentWithPrompt(cliWorkspace, promptText, options = {}) {
|
|
1031
|
+
const onStreamEvent = typeof options.onStreamEvent === "function" ? options.onStreamEvent : null;
|
|
1032
|
+
const ws = path.resolve(cliWorkspace);
|
|
1033
|
+
const model = options.model && String(options.model).trim();
|
|
1034
|
+
const claudeCmd = process.env.CLAUDE_CODE_CMD || "claude";
|
|
1035
|
+
const bypassPermissions =
|
|
1036
|
+
process.env.AGENTFLOW_CLAUDE_CODE_BYPASS_PERMISSIONS !== "0" &&
|
|
1037
|
+
process.env.AGENTFLOW_CLAUDE_CODE_BYPASS_PERMISSIONS !== "false";
|
|
1038
|
+
const args = ["-p", "--output-format", "stream-json", "--verbose", "--add-dir", ws];
|
|
1039
|
+
if (bypassPermissions) args.push("--dangerously-skip-permissions");
|
|
1040
|
+
if (model) args.push("--model", model);
|
|
1041
|
+
args.push(promptText);
|
|
1042
|
+
|
|
1043
|
+
const useStderrInherit =
|
|
1044
|
+
process.env.AGENTFLOW_CLAUDE_CODE_STDERR_INHERIT === "1" ||
|
|
1045
|
+
process.env.AGENTFLOW_CLAUDE_CODE_STDERR_INHERIT === "true";
|
|
1046
|
+
const child = spawn(claudeCmd, args, {
|
|
1047
|
+
cwd: ws,
|
|
1048
|
+
stdio: ["ignore", "pipe", useStderrInherit ? "inherit" : "pipe"],
|
|
1049
|
+
shell: false,
|
|
1050
|
+
});
|
|
1051
|
+
|
|
1052
|
+
let lastResult = null;
|
|
1053
|
+
let hadError = false;
|
|
1054
|
+
const STDERR_CAP_BYTES = 1024 * 1024;
|
|
1055
|
+
const stderrChunks = [];
|
|
1056
|
+
let stderrTotalBytes = 0;
|
|
1057
|
+
let stderrComposerBuffer = "";
|
|
1058
|
+
|
|
1059
|
+
const emit = (payload) => {
|
|
1060
|
+
try {
|
|
1061
|
+
onStreamEvent?.(payload);
|
|
1062
|
+
} catch (_) {}
|
|
1063
|
+
};
|
|
1064
|
+
|
|
1065
|
+
if (!useStderrInherit) {
|
|
1066
|
+
child.stderr.on("data", (chunk) => {
|
|
1067
|
+
const s = typeof chunk === "string" ? chunk : chunk.toString("utf-8");
|
|
1068
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, "utf-8");
|
|
1069
|
+
const len = buf.length;
|
|
1070
|
+
while (stderrChunks.length > 0 && stderrTotalBytes + len > STDERR_CAP_BYTES) {
|
|
1071
|
+
const drop = stderrChunks.shift();
|
|
1072
|
+
stderrTotalBytes -= Buffer.isBuffer(drop) ? drop.length : Buffer.byteLength(drop, "utf-8");
|
|
1073
|
+
}
|
|
1074
|
+
stderrChunks.push(buf);
|
|
1075
|
+
stderrTotalBytes += len;
|
|
1076
|
+
stderrComposerBuffer += s;
|
|
1077
|
+
let idx;
|
|
1078
|
+
while ((idx = stderrComposerBuffer.indexOf("\n")) !== -1) {
|
|
1079
|
+
const line = stderrComposerBuffer.slice(0, idx);
|
|
1080
|
+
stderrComposerBuffer = stderrComposerBuffer.slice(idx + 1);
|
|
1081
|
+
if (line.trim()) {
|
|
1082
|
+
emit({ type: "status", line: `[stderr] ${truncateComposerLine(line)}` });
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
const stdoutWidth = 80;
|
|
1089
|
+
const mdStreamer = createMarkdownStreamer({
|
|
1090
|
+
render: (md) => renderMarkdown(md, { width: stdoutWidth }),
|
|
1091
|
+
spacing: "single",
|
|
1092
|
+
});
|
|
1093
|
+
|
|
1094
|
+
child.stdout.setEncoding("utf-8");
|
|
1095
|
+
let stdoutLineBuffer = "";
|
|
1096
|
+
child.stdout.on("data", (chunk) => {
|
|
1097
|
+
stdoutLineBuffer += chunk;
|
|
1098
|
+
const idx = stdoutLineBuffer.lastIndexOf("\n");
|
|
1099
|
+
const complete = idx >= 0 ? stdoutLineBuffer.slice(0, idx) : "";
|
|
1100
|
+
if (idx >= 0) stdoutLineBuffer = stdoutLineBuffer.slice(idx + 1);
|
|
1101
|
+
const lines = complete.split("\n").filter(Boolean);
|
|
1102
|
+
for (const line of lines) {
|
|
1103
|
+
try {
|
|
1104
|
+
const event = JSON.parse(line);
|
|
1105
|
+
if (event.type === "assistant" && event.message && Array.isArray(event.message.content)) {
|
|
1106
|
+
for (const block of event.message.content) {
|
|
1107
|
+
if (!block || typeof block !== "object") continue;
|
|
1108
|
+
if (block.type === "text" && block.text) {
|
|
1109
|
+
const text = normalizeStreamTextChunk(block.text);
|
|
1110
|
+
emit({ type: "natural", kind: "assistant", text });
|
|
1111
|
+
mdStreamer.push(text);
|
|
1112
|
+
emit({ type: "status", line: t("runner.generating_reply") });
|
|
1113
|
+
} else if (block.type === "thinking" && block.thinking) {
|
|
1114
|
+
const text = normalizeStreamTextChunk(block.thinking);
|
|
1115
|
+
emit({ type: "natural", kind: "thinking", text });
|
|
1116
|
+
emit({ type: "status", line: t("runner.thinking") });
|
|
1117
|
+
if (options.onToolCall) options.onToolCall("thinking", "");
|
|
1118
|
+
} else if (block.type === "tool_use") {
|
|
1119
|
+
const toolName = block.name || "?";
|
|
1120
|
+
emit({ type: "status", line: `工具 ${toolName}` });
|
|
1121
|
+
if (options.onToolCall) options.onToolCall("tool_use", toolName);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
} else if (event.type === "result") {
|
|
1125
|
+
lastResult = event;
|
|
1126
|
+
const isSuccess = event.subtype === "success" && !event.is_error;
|
|
1127
|
+
if (isSuccess) {
|
|
1128
|
+
hadError = false;
|
|
1129
|
+
if (typeof event.result === "string" && event.result.trim()) {
|
|
1130
|
+
emit({ type: "natural", kind: "result", text: normalizeStreamTextChunk(event.result) });
|
|
1131
|
+
}
|
|
1132
|
+
emit({ type: "status", line: t("runner.completed") });
|
|
1133
|
+
} else {
|
|
1134
|
+
hadError = true;
|
|
1135
|
+
const errText =
|
|
1136
|
+
(typeof event.result === "string" && event.result) ||
|
|
1137
|
+
event.subtype ||
|
|
1138
|
+
t("runner.execution_failed");
|
|
1139
|
+
emit({ type: "natural", kind: "error", text: String(errText) });
|
|
1140
|
+
emit({ type: "status", line: truncateComposerLine(String(errText)) });
|
|
1141
|
+
}
|
|
1142
|
+
} else if (event.type === "system") {
|
|
1143
|
+
// init 元事件
|
|
1144
|
+
} else if (event.type === "user") {
|
|
1145
|
+
// tool_result 回传
|
|
1146
|
+
} else {
|
|
1147
|
+
emit({ type: "status", line: `${t("runner.event_label")}: ${event.type ?? "unknown"}` });
|
|
1148
|
+
}
|
|
1149
|
+
} catch (_) {
|
|
1150
|
+
emit({ type: "status", line: truncateComposerLine(line) });
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
});
|
|
1154
|
+
|
|
1155
|
+
const finished = new Promise((resolve, reject) => {
|
|
1156
|
+
child.on("error", (err) => {
|
|
1157
|
+
child.stdout?.removeAllListeners();
|
|
1158
|
+
child.stderr?.removeAllListeners();
|
|
1159
|
+
child.removeAllListeners();
|
|
1160
|
+
reject(
|
|
1161
|
+
new Error(
|
|
1162
|
+
`Claude Code CLI failed to start: ${err.message}. Install via 'npm i -g @anthropic-ai/claude-code' and run 'claude /login', or set CLAUDE_CODE_CMD.`,
|
|
1163
|
+
),
|
|
1164
|
+
);
|
|
1165
|
+
});
|
|
1166
|
+
|
|
1167
|
+
child.on("close", (code) => {
|
|
1168
|
+
child.stdout.removeAllListeners();
|
|
1169
|
+
if (!useStderrInherit) child.stderr.removeAllListeners();
|
|
1170
|
+
child.removeAllListeners();
|
|
1171
|
+
mdStreamer.finish();
|
|
1172
|
+
if (!useStderrInherit && stderrComposerBuffer.trim()) {
|
|
1173
|
+
const rest = stderrComposerBuffer.trim();
|
|
1174
|
+
emit({ type: "status", line: `[stderr] ${truncateComposerLine(rest)}` });
|
|
1175
|
+
}
|
|
1176
|
+
if (code !== 0 && lastResult == null) {
|
|
1177
|
+
const stderr = Buffer.concat(stderrChunks).toString("utf-8");
|
|
1178
|
+
const stderrTail = stderr ? stderr.trim().slice(-1200) : "";
|
|
1179
|
+
const err = new Error(`Claude Code CLI exited ${code}. ${stderrTail || "No result event received."}`);
|
|
1180
|
+
err.claudeCodeStderrTail = stderrTail;
|
|
1181
|
+
emit({ type: "status", line: truncateComposerLine(err.message) });
|
|
1182
|
+
reject(err);
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
if (hadError || (lastResult && lastResult.is_error)) {
|
|
1186
|
+
const msg =
|
|
1187
|
+
(lastResult && typeof lastResult.result === "string" && lastResult.result) ||
|
|
1188
|
+
(lastResult && lastResult.subtype) ||
|
|
1189
|
+
"Agent reported error.";
|
|
1190
|
+
emit({ type: "status", line: truncateComposerLine(String(msg)) });
|
|
1191
|
+
reject(new Error(String(msg)));
|
|
1192
|
+
return;
|
|
1193
|
+
}
|
|
1194
|
+
resolve();
|
|
1195
|
+
});
|
|
1196
|
+
});
|
|
1197
|
+
|
|
1198
|
+
return { child, finished };
|
|
1199
|
+
}
|