@fieldwangai/agentflow 0.1.85 → 0.1.86

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.
@@ -34,6 +34,219 @@ function writeAgentTextArtifacts(absResultPath, absRunDir, instanceId, text) {
34
34
  fs.writeFileSync(slotPath, body + "\n", "utf-8");
35
35
  }
36
36
 
37
+ function envFlag(name, defaultValue = false) {
38
+ const raw = process.env[name];
39
+ if (raw == null || raw === "") return defaultValue;
40
+ return raw !== "0" && raw !== "false";
41
+ }
42
+
43
+ function cleanModel(value, fallbackEnvName = "") {
44
+ const raw = value ?? (fallbackEnvName ? process.env[fallbackEnvName] : null) ?? "";
45
+ const text = String(raw).trim();
46
+ if (!text) return "";
47
+ const dashIdx = text.indexOf(" - ");
48
+ return dashIdx >= 0 ? text.slice(0, dashIdx).trim() : text;
49
+ }
50
+
51
+ function hasGitMetadataAncestor(startDir) {
52
+ let cur = path.resolve(startDir || ".");
53
+ while (true) {
54
+ if (fs.existsSync(path.join(cur, ".git"))) return true;
55
+ const parent = path.dirname(cur);
56
+ if (parent === cur) return false;
57
+ cur = parent;
58
+ }
59
+ }
60
+
61
+ function shouldSkipCodexGitCheck(workspace) {
62
+ const raw = process.env.AGENTFLOW_CODEX_SKIP_GIT_CHECK;
63
+ if (raw === "1" || raw === "true") return true;
64
+ if (raw === "0" || raw === "false") return false;
65
+ return !hasGitMetadataAncestor(workspace);
66
+ }
67
+
68
+ function buildCodexExecArgs({ workspace, addDirs = [], model, outputLastMessagePath, promptText, configArgs = [] }) {
69
+ const args = [];
70
+ for (const cfg of Array.isArray(configArgs) ? configArgs : []) {
71
+ const value = String(cfg || "").trim();
72
+ if (value) args.push("-c", value);
73
+ }
74
+ const approval = String(process.env.AGENTFLOW_CODEX_APPROVAL || "never").trim();
75
+ if (approval) args.push("--ask-for-approval", approval);
76
+ args.push("exec", "--json", "--color", "never", "--cd", workspace, "--output-last-message", outputLastMessagePath);
77
+ if (envFlag("AGENTFLOW_CODEX_IGNORE_USER_CONFIG", true)) args.push("--ignore-user-config");
78
+ for (const dir of addDirs) {
79
+ const abs = path.resolve(dir);
80
+ if (abs && abs !== workspace) args.push("--add-dir", abs);
81
+ }
82
+ if (envFlag("AGENTFLOW_CODEX_DANGER", false)) {
83
+ args.push("--dangerously-bypass-approvals-and-sandbox");
84
+ } else {
85
+ args.push("--sandbox", String(process.env.AGENTFLOW_CODEX_SANDBOX || "workspace-write").trim() || "workspace-write");
86
+ }
87
+ if (shouldSkipCodexGitCheck(workspace)) args.push("--skip-git-repo-check");
88
+ if (envFlag("AGENTFLOW_CODEX_EPHEMERAL", false)) args.push("--ephemeral");
89
+ if (model) args.push("--model", model);
90
+ args.push(promptText);
91
+ return args;
92
+ }
93
+
94
+ function extractCodexText(event) {
95
+ if (!event || typeof event !== "object") return "";
96
+ const candidates = [
97
+ event.delta,
98
+ event.text,
99
+ event.message,
100
+ event.content,
101
+ event.output_text,
102
+ event.final_message,
103
+ event.item?.text,
104
+ event.item?.message,
105
+ event.item?.content,
106
+ event.msg?.content,
107
+ ];
108
+ for (const c of candidates) {
109
+ if (typeof c === "string" && c.trim()) return normalizeStreamTextChunk(c);
110
+ if (Array.isArray(c)) {
111
+ const parts = c
112
+ .map((part) => {
113
+ if (typeof part === "string") return part;
114
+ if (part && typeof part === "object") return part.text || part.content || part.output_text || "";
115
+ return "";
116
+ })
117
+ .filter(Boolean);
118
+ if (parts.length) return normalizeStreamTextChunk(parts.join(""));
119
+ }
120
+ }
121
+ return "";
122
+ }
123
+
124
+ function extractCodexToolName(event) {
125
+ if (!event || typeof event !== "object") return "";
126
+ const names = [event.name, event.tool_name, event.toolName, event.call?.name, event.item?.name, event.command?.name];
127
+ for (const name of names) {
128
+ if (typeof name === "string" && name.trim()) return name.trim();
129
+ }
130
+ return "";
131
+ }
132
+
133
+ function codexEventKind(event) {
134
+ const base = String(event?.type || event?.event || event?.kind || "unknown");
135
+ const itemType = typeof event?.item?.type === "string" && event.item.type.trim() ? event.item.type.trim() : "";
136
+ return itemType ? `${base}:${itemType}` : base;
137
+ }
138
+
139
+ function codexEventBaseKind(event) {
140
+ return String(event?.type || event?.event || event?.kind || "unknown");
141
+ }
142
+
143
+ function codexItemType(event) {
144
+ return typeof event?.item?.type === "string" ? event.item.type.trim().toLowerCase() : "";
145
+ }
146
+
147
+ function codexCommandText(event) {
148
+ const item = event?.item;
149
+ const candidates = [item?.command, item?.cmd, event?.command, event?.cmd];
150
+ for (const c of candidates) {
151
+ if (typeof c === "string" && c.trim()) return c.trim();
152
+ }
153
+ if (item?.command && typeof item.command === "object") {
154
+ const name = item.command.name || item.command.command || "";
155
+ const args = Array.isArray(item.command.args) ? item.command.args.join(" ") : "";
156
+ const text = `${name} ${args}`.trim();
157
+ if (text) return text;
158
+ }
159
+ return "";
160
+ }
161
+
162
+ function isNonFatalCodexErrorMessage(message) {
163
+ const lower = String(message || "").toLowerCase();
164
+ return lower.includes("reconnecting") || lower.includes("retrying") || lower.includes("timed out waiting for") || lower.includes("transport channel closed");
165
+ }
166
+
167
+ function handleCodexEvent(event, { emitNatural, emitStatus, onToolCall }) {
168
+ const kind = codexEventKind(event);
169
+ const baseKind = codexEventBaseKind(event);
170
+ const lower = kind.toLowerCase();
171
+ const baseLower = baseKind.toLowerCase();
172
+ const itemType = codexItemType(event);
173
+ const text = extractCodexText(event);
174
+
175
+ if (itemType === "agent_message") {
176
+ if (text) emitNatural("assistant", text);
177
+ return { handled: true, hadError: false };
178
+ }
179
+ if (itemType.includes("reason") || itemType.includes("thinking")) {
180
+ if (text) emitNatural("thinking", text);
181
+ if (onToolCall) onToolCall("thinking", "");
182
+ return { handled: true, hadError: false };
183
+ }
184
+ if (itemType === "command_execution" || itemType.includes("tool_call") || itemType.includes("tool")) {
185
+ const command = codexCommandText(event);
186
+ const status = String(event?.item?.status || event?.status || baseKind || "").toLowerCase();
187
+ const exitCode = event?.item?.exit_code ?? event?.exit_code;
188
+ const name = command || extractCodexToolName(event) || itemType || kind;
189
+ if (onToolCall) onToolCall(itemType || kind, name);
190
+ if (baseLower.includes("started") || status === "in_progress") {
191
+ emitStatus(`Codex command started: ${truncateComposerLine(name)}`);
192
+ } else if (baseLower.includes("completed") || status === "completed" || status === "failed") {
193
+ const suffix = exitCode == null ? "" : ` exit ${exitCode}`;
194
+ emitStatus(`Codex command ${status === "failed" || exitCode ? "finished" : "completed"}:${suffix} ${truncateComposerLine(name)}`);
195
+ } else {
196
+ emitStatus(`Codex ${truncateComposerLine(name)}`);
197
+ }
198
+ return { handled: true, hadError: false };
199
+ }
200
+
201
+ if (text && (lower.includes("message") || lower.includes("response") || lower.includes("delta") || lower === "assistant")) {
202
+ emitNatural("assistant", text);
203
+ return { handled: true, hadError: false };
204
+ }
205
+ if (text && lower.includes("reason")) {
206
+ emitNatural("thinking", text);
207
+ if (onToolCall) onToolCall("thinking", "");
208
+ return { handled: true, hadError: false };
209
+ }
210
+ if (lower.includes("tool") || lower.includes("exec") || lower.includes("command")) {
211
+ const toolName = extractCodexToolName(event) || kind;
212
+ if (onToolCall) onToolCall(kind, toolName);
213
+ emitStatus(`Codex ${toolName}`);
214
+ return { handled: true, hadError: false };
215
+ }
216
+ if (lower.includes("error") || event?.is_error) {
217
+ const msg = text || event?.message || event?.error || kind;
218
+ emitStatus(truncateComposerLine(String(msg)));
219
+ return { handled: true, hadError: !isNonFatalCodexErrorMessage(msg) };
220
+ }
221
+ if (baseLower === "thread.started" || baseLower === "turn.started" || baseLower === "turn.completed") {
222
+ return { handled: true, hadError: false };
223
+ }
224
+ return { handled: false, hadError: false };
225
+ }
226
+
227
+ function codexFailureMessage(code, stderrTail, { flowName = "", uuid = "" } = {}) {
228
+ const tail = String(stderrTail || "").trim();
229
+ const lower = tail.toLowerCase();
230
+ const hints = [];
231
+ if (lower.includes("login") || lower.includes("auth") || lower.includes("authentication") || lower.includes("not logged")) {
232
+ hints.push("Codex 可能未登录,请先执行 `codex login`。");
233
+ }
234
+ if (lower.includes("model") && (lower.includes("not found") || lower.includes("unknown") || lower.includes("invalid"))) {
235
+ hints.push("Codex 模型可能不可用,请检查 `codex:<model>` 或刷新模型列表。");
236
+ }
237
+ if (lower.includes("mcp") || lower.includes("config") || lower.includes("invalid transport")) {
238
+ hints.push("Codex MCP/配置加载失败,请检查 MCP 名称、URL、command/env 配置。");
239
+ }
240
+ if (lower.includes("network") || lower.includes("timeout") || lower.includes("connection")) {
241
+ hints.push("Codex 网络连接失败,请检查代理或网络环境。");
242
+ }
243
+ if (flowName && uuid) {
244
+ hints.push("完整 stderr 可在 run 日志中查看;需要直接透传 stderr 可设置 `AGENTFLOW_CODEX_STDERR_INHERIT=1` 后重跑。");
245
+ }
246
+ const suffix = hints.length ? ` ${hints.join(" ")}` : "";
247
+ return `Codex CLI exited ${code}. ${tail || "No result event received."}${suffix}`;
248
+ }
249
+
37
250
  /**
38
251
  * Run Cursor CLI with stream-json, forward events to stdout, return success/failure.
39
252
  */
@@ -693,6 +906,225 @@ export function runClaudeCodeAgentForNode(
693
906
  });
694
907
  }
695
908
 
909
+ /**
910
+ * Run Codex CLI (`codex exec`) in non-interactive JSONL mode for a node.
911
+ */
912
+ export function runCodexAgentForNode(
913
+ workspaceRoot,
914
+ { promptPath, nodeContext, taskBody, intermediatePath, resultPathRel, subagent, instanceId },
915
+ options = {},
916
+ ) {
917
+ const absPromptPath = path.resolve(workspaceRoot, promptPath);
918
+ const absRunDir = path.resolve(workspaceRoot, intermediatePath);
919
+ const absResultPath = path.join(absRunDir, resultPathRel);
920
+ const nodeIntermediateDir = path.dirname(absPromptPath);
921
+ const outputDir = instanceId ? path.join(absRunDir, "output", instanceId) : path.join(absRunDir, "output");
922
+ if (instanceId) fs.mkdirSync(outputDir, { recursive: true });
923
+ const absWorkspaceRoot = path.resolve(workspaceRoot);
924
+ const execWorkspaceRoot = path.resolve(options.execWorkspaceRoot || workspaceRoot);
925
+ const replacements = {
926
+ workspaceRoot: execWorkspaceRoot,
927
+ executionWorkspaceRoot: execWorkspaceRoot,
928
+ pipelineWorkspace: absWorkspaceRoot,
929
+ promptPath: absPromptPath,
930
+ nodeContext: nodeContext ?? "",
931
+ taskBody: taskBody ?? "",
932
+ resultPath: absResultPath,
933
+ intermediatePath: path.join(absRunDir, "intermediate"),
934
+ outputDir,
935
+ flowName: options.flowName ?? "",
936
+ uuid: options.uuid ?? "",
937
+ instanceId: instanceId ?? "",
938
+ };
939
+ const agentContent = loadAgentPromptWithReplacements(workspaceRoot, subagent, replacements);
940
+ let agentPathForPrompt = getAgentPath(workspaceRoot, subagent);
941
+ if (agentContent) {
942
+ const resolvedAgentPath = path.join(nodeIntermediateDir, `agent-${subagent}.md`);
943
+ fs.mkdirSync(nodeIntermediateDir, { recursive: true });
944
+ fs.writeFileSync(resolvedAgentPath, agentContent, "utf8");
945
+ agentPathForPrompt = resolvedAgentPath;
946
+ }
947
+ const rawAgentContent =
948
+ agentContent != null
949
+ ? agentContent
950
+ : fs.existsSync(agentPathForPrompt)
951
+ ? fs.readFileSync(agentPathForPrompt, "utf8")
952
+ : "";
953
+ const promptText = stripYamlFrontmatter(rawAgentContent);
954
+
955
+ const model = cleanModel(options.model, "CODEX_MODEL");
956
+ const rawPrefix = options.outputPrefix != null ? `[${options.outputPrefix}] ` : "";
957
+ const coloredPrefix = rawPrefix && options.prefixColor ? options.prefixColor(rawPrefix) : rawPrefix;
958
+ const agentContentColor = options.contentColor ?? ((line) => chalk.gray(line));
959
+
960
+ return new Promise((resolve, reject) => {
961
+ const codexCmd = process.env.CODEX_CMD || "codex";
962
+ fs.mkdirSync(nodeIntermediateDir, { recursive: true });
963
+ const outputLastMessagePath = path.join(nodeIntermediateDir, "codex-last-message.txt");
964
+ const args = buildCodexExecArgs({
965
+ workspace: execWorkspaceRoot,
966
+ addDirs: [absWorkspaceRoot],
967
+ model,
968
+ outputLastMessagePath,
969
+ promptText,
970
+ configArgs: options.codexConfigArgs,
971
+ });
972
+ if (options.flowName && options.uuid) {
973
+ const argvLog = args.slice(0, -1).concat([`(prompt ${args[args.length - 1].length} chars)`]);
974
+ appendRunLogLine(
975
+ workspaceRoot,
976
+ options.flowName,
977
+ options.uuid,
978
+ "cli-raw",
979
+ `Codex CLI 完整参数: ${codexCmd} ${JSON.stringify(argvLog)}`,
980
+ );
981
+ appendRunLogLine(
982
+ workspaceRoot,
983
+ options.flowName,
984
+ options.uuid,
985
+ "cli-raw",
986
+ `Codex CLI prompt 前 800 字:\n${promptText.slice(0, 800)}${promptText.length > 800 ? "..." : ""}`,
987
+ );
988
+ appendRunLogLine(workspaceRoot, options.flowName, options.uuid, "cli-raw", `Codex CLI prompt 完整:\n${promptText}`);
989
+ }
990
+
991
+ const useStderrInherit =
992
+ process.env.AGENTFLOW_CODEX_STDERR_INHERIT === "1" ||
993
+ process.env.AGENTFLOW_CODEX_STDERR_INHERIT === "true";
994
+ const child = spawn(codexCmd, args, {
995
+ cwd: execWorkspaceRoot,
996
+ stdio: ["ignore", "pipe", useStderrInherit ? "inherit" : "pipe"],
997
+ shell: false,
998
+ env: childEnv(options),
999
+ });
1000
+
1001
+ let hadError = false;
1002
+ const assistantTextChunks = [];
1003
+ const STDERR_CAP_BYTES = 1024 * 1024;
1004
+ const stderrChunks = [];
1005
+ let stderrTotalBytes = 0;
1006
+ const stderrBuffer = options.stderrBuffer || null;
1007
+ let stderrLineBuffer = "";
1008
+ const flowName = options.flowName ?? null;
1009
+ const uuid = options.uuid ?? null;
1010
+
1011
+ const outStream = machineReadable ? process.stderr : process.stdout;
1012
+ function writeStdout(text) {
1013
+ if (coloredPrefix) writeWithPrefix(outStream, text, coloredPrefix, agentContentColor);
1014
+ else if (text) outStream.write(agentContentColor(text));
1015
+ if (text && flowName && uuid) appendRunLogLine(workspaceRoot, flowName, uuid, "codex-stdout", text);
1016
+ }
1017
+
1018
+ function flushStderrLines() {
1019
+ if (!coloredPrefix) return;
1020
+ let idx;
1021
+ while ((idx = stderrLineBuffer.indexOf("\n")) !== -1) {
1022
+ const line = stderrLineBuffer.slice(0, idx + 1);
1023
+ stderrLineBuffer = stderrLineBuffer.slice(idx + 1);
1024
+ writeWithPrefix(process.stderr, line, coloredPrefix, agentContentColor);
1025
+ }
1026
+ }
1027
+
1028
+ if (!useStderrInherit) {
1029
+ child.stderr.on("data", (chunk) => {
1030
+ const s = typeof chunk === "string" ? chunk : chunk.toString("utf-8");
1031
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, "utf-8");
1032
+ const len = buf.length;
1033
+ while (stderrChunks.length > 0 && stderrTotalBytes + len > STDERR_CAP_BYTES) {
1034
+ const drop = stderrChunks.shift();
1035
+ stderrTotalBytes -= Buffer.isBuffer(drop) ? drop.length : Buffer.byteLength(drop, "utf-8");
1036
+ }
1037
+ stderrChunks.push(buf);
1038
+ stderrTotalBytes += len;
1039
+ if (stderrBuffer) {
1040
+ stderrBuffer.push(chunk);
1041
+ } else if (coloredPrefix) {
1042
+ stderrLineBuffer += s;
1043
+ flushStderrLines();
1044
+ } else {
1045
+ process.stderr.write(chunk);
1046
+ }
1047
+ if (flowName && uuid) appendRunLogLine(workspaceRoot, flowName, uuid, "codex-stderr", s);
1048
+ });
1049
+ }
1050
+
1051
+ const stdoutWidth = process.stdout.columns ?? 80;
1052
+ const mdStreamer = createMarkdownStreamer({
1053
+ render: (md) => renderMarkdown(md, { width: stdoutWidth }),
1054
+ spacing: "single",
1055
+ });
1056
+
1057
+ function emitNatural(_kind, text) {
1058
+ assistantTextChunks.push(text);
1059
+ const out = mdStreamer.push(text);
1060
+ if (out) writeStdout(out);
1061
+ }
1062
+
1063
+ function emitStatus(line) {
1064
+ if (line) writeStdout(`[codex] ${line}\n`);
1065
+ }
1066
+
1067
+ child.stdout.setEncoding("utf-8");
1068
+ let stdoutLineBuffer = "";
1069
+ child.stdout.on("data", (chunk) => {
1070
+ stdoutLineBuffer += chunk;
1071
+ const idx = stdoutLineBuffer.lastIndexOf("\n");
1072
+ const complete = idx >= 0 ? stdoutLineBuffer.slice(0, idx) : "";
1073
+ if (idx >= 0) stdoutLineBuffer = stdoutLineBuffer.slice(idx + 1);
1074
+ const lines = complete.split("\n").filter(Boolean);
1075
+ for (const line of lines) {
1076
+ if (flowName && uuid) appendRunLogLine(workspaceRoot, flowName, uuid, "codex-stdout-raw", line);
1077
+ try {
1078
+ const event = JSON.parse(line);
1079
+ const result = handleCodexEvent(event, { emitNatural, emitStatus, onToolCall: options.onToolCall });
1080
+ if (result.hadError) hadError = true;
1081
+ if (!result.handled) writeStdout(`[codex-stdout] event: ${codexEventKind(event)}\n`);
1082
+ } catch (_) {
1083
+ writeStdout(`[codex-stdout] (非 JSON) ${line.slice(0, 500)}${line.length > 500 ? "..." : ""}\n`);
1084
+ }
1085
+ }
1086
+ });
1087
+
1088
+ child.on("error", (err) => {
1089
+ child.stdout?.removeAllListeners();
1090
+ child.stderr?.removeAllListeners();
1091
+ child.removeAllListeners();
1092
+ reject(new Error(`Codex CLI failed to start: ${err.message}. Ensure '${codexCmd}' is in PATH and run 'codex login'.`));
1093
+ });
1094
+
1095
+ child.on("close", (code) => {
1096
+ if (stdoutLineBuffer.trim() && flowName && uuid) {
1097
+ appendRunLogLine(workspaceRoot, flowName, uuid, "codex-stdout-raw", stdoutLineBuffer.trim());
1098
+ }
1099
+ child.stdout.removeAllListeners();
1100
+ if (!useStderrInherit) child.stderr.removeAllListeners();
1101
+ child.removeAllListeners();
1102
+ const tail = mdStreamer.finish();
1103
+ if (tail) writeStdout(tail);
1104
+ if (coloredPrefix && stderrLineBuffer) {
1105
+ writeWithPrefix(process.stderr, stderrLineBuffer.endsWith("\n") ? stderrLineBuffer : stderrLineBuffer + "\n", coloredPrefix);
1106
+ }
1107
+ const finalMessage = fs.existsSync(outputLastMessagePath)
1108
+ ? fs.readFileSync(outputLastMessagePath, "utf-8").trim()
1109
+ : assistantTextChunks.join("").trim();
1110
+ if (code !== 0) {
1111
+ const stderr = Buffer.concat(stderrChunks).toString("utf-8");
1112
+ const stderrTail = stderr ? stderr.trim().slice(-1200) : "";
1113
+ const err = new Error(codexFailureMessage(code, stderrTail, { flowName, uuid }));
1114
+ err.codexStderrTail = stderrTail;
1115
+ reject(err);
1116
+ return;
1117
+ }
1118
+ if (hadError) {
1119
+ reject(new Error(finalMessage || "Codex reported error."));
1120
+ return;
1121
+ }
1122
+ writeAgentTextArtifacts(absResultPath, absRunDir, instanceId, finalMessage || assistantTextChunks.join(""));
1123
+ resolve();
1124
+ });
1125
+ });
1126
+ }
1127
+
696
1128
  const COMPOSER_STATUS_MAX = 200;
697
1129
 
698
1130
  /**
@@ -1263,3 +1695,160 @@ export function runClaudeCodeAgentWithPrompt(cliWorkspace, promptText, options =
1263
1695
 
1264
1696
  return { child, finished };
1265
1697
  }
1698
+
1699
+ /**
1700
+ * Codex CLI:纯文本 prompt,供 Composer / UI 流式使用;不写 process stdout/stderr。
1701
+ * @returns {{ child: import('child_process').ChildProcess, finished: Promise<void> }}
1702
+ */
1703
+ export function runCodexAgentWithPrompt(cliWorkspace, promptText, options = {}) {
1704
+ const onStreamEvent = typeof options.onStreamEvent === "function" ? options.onStreamEvent : null;
1705
+ const ws = path.resolve(cliWorkspace);
1706
+ const model = cleanModel(options.model, "CODEX_MODEL");
1707
+ const codexCmd = process.env.CODEX_CMD || "codex";
1708
+ const tmpDir = path.join(ws, ".workspace", "agentflow", "tmp");
1709
+ fs.mkdirSync(tmpDir, { recursive: true });
1710
+ const outputLastMessagePath = path.join(tmpDir, `codex-last-message-${process.pid}-${Date.now()}.txt`);
1711
+ const args = buildCodexExecArgs({
1712
+ workspace: ws,
1713
+ model,
1714
+ outputLastMessagePath,
1715
+ promptText,
1716
+ configArgs: options.codexConfigArgs,
1717
+ });
1718
+
1719
+ const useStderrInherit =
1720
+ process.env.AGENTFLOW_CODEX_STDERR_INHERIT === "1" ||
1721
+ process.env.AGENTFLOW_CODEX_STDERR_INHERIT === "true";
1722
+ const child = spawn(codexCmd, args, {
1723
+ cwd: ws,
1724
+ stdio: ["ignore", "pipe", useStderrInherit ? "inherit" : "pipe"],
1725
+ shell: false,
1726
+ env: childEnv(options),
1727
+ });
1728
+
1729
+ let hadError = false;
1730
+ let emittedNatural = false;
1731
+ const STDERR_CAP_BYTES = 1024 * 1024;
1732
+ const stderrChunks = [];
1733
+ let stderrTotalBytes = 0;
1734
+ let stderrComposerBuffer = "";
1735
+
1736
+ const emit = (payload) => {
1737
+ try {
1738
+ onStreamEvent?.(payload);
1739
+ } catch (_) {}
1740
+ };
1741
+
1742
+ if (!useStderrInherit) {
1743
+ child.stderr.on("data", (chunk) => {
1744
+ const s = typeof chunk === "string" ? chunk : chunk.toString("utf-8");
1745
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, "utf-8");
1746
+ const len = buf.length;
1747
+ while (stderrChunks.length > 0 && stderrTotalBytes + len > STDERR_CAP_BYTES) {
1748
+ const drop = stderrChunks.shift();
1749
+ stderrTotalBytes -= Buffer.isBuffer(drop) ? drop.length : Buffer.byteLength(drop, "utf-8");
1750
+ }
1751
+ stderrChunks.push(buf);
1752
+ stderrTotalBytes += len;
1753
+ stderrComposerBuffer += s;
1754
+ let idx;
1755
+ while ((idx = stderrComposerBuffer.indexOf("\n")) !== -1) {
1756
+ const line = stderrComposerBuffer.slice(0, idx);
1757
+ stderrComposerBuffer = stderrComposerBuffer.slice(idx + 1);
1758
+ if (line.trim()) {
1759
+ emit({ type: "raw", source: "codex", stream: "stderr", eventType: "line", text: rawTraceText(line) });
1760
+ emit({ type: "status", line: `[stderr] ${truncateComposerLine(line)}` });
1761
+ }
1762
+ }
1763
+ });
1764
+ }
1765
+
1766
+ const stdoutWidth = 80;
1767
+ const mdStreamer = createMarkdownStreamer({
1768
+ render: (md) => renderMarkdown(md, { width: stdoutWidth }),
1769
+ spacing: "single",
1770
+ });
1771
+
1772
+ function emitNatural(kind, text) {
1773
+ emittedNatural = true;
1774
+ emit({ type: "natural", kind, text });
1775
+ mdStreamer.push(text);
1776
+ emit({ type: "status", line: kind === "thinking" ? t("runner.thinking") : t("runner.generating_reply") });
1777
+ }
1778
+
1779
+ function emitStatus(line) {
1780
+ if (line) emit({ type: "status", line: truncateComposerLine(line) });
1781
+ }
1782
+
1783
+ child.stdout.setEncoding("utf-8");
1784
+ let stdoutLineBuffer = "";
1785
+ child.stdout.on("data", (chunk) => {
1786
+ stdoutLineBuffer += chunk;
1787
+ const idx = stdoutLineBuffer.lastIndexOf("\n");
1788
+ const complete = idx >= 0 ? stdoutLineBuffer.slice(0, idx) : "";
1789
+ if (idx >= 0) stdoutLineBuffer = stdoutLineBuffer.slice(idx + 1);
1790
+ const lines = complete.split("\n").filter(Boolean);
1791
+ for (const line of lines) {
1792
+ try {
1793
+ const event = JSON.parse(line);
1794
+ emit({ type: "raw", source: "codex", stream: "stdout", eventType: codexEventKind(event), text: rawTraceText(event) });
1795
+ const result = handleCodexEvent(event, { emitNatural, emitStatus, onToolCall: options.onToolCall });
1796
+ if (result.hadError) hadError = true;
1797
+ if (!result.handled) emit({ type: "status", line: `${t("runner.event_label")}: ${codexEventKind(event)}` });
1798
+ } catch (_) {
1799
+ emit({ type: "raw", source: "codex", stream: "stdout", eventType: "line", text: rawTraceText(line) });
1800
+ emit({ type: "status", line: truncateComposerLine(line) });
1801
+ }
1802
+ }
1803
+ });
1804
+
1805
+ const finished = new Promise((resolve, reject) => {
1806
+ child.on("error", (err) => {
1807
+ child.stdout?.removeAllListeners();
1808
+ child.stderr?.removeAllListeners();
1809
+ child.removeAllListeners();
1810
+ reject(new Error(`Codex CLI failed to start: ${err.message}. Ensure '${codexCmd}' is in PATH and run 'codex login'.`));
1811
+ });
1812
+
1813
+ child.on("close", (code) => {
1814
+ if (stdoutLineBuffer.trim()) {
1815
+ emit({ type: "raw", source: "codex", stream: "stdout", eventType: "tail", text: rawTraceText(stdoutLineBuffer.trim()) });
1816
+ }
1817
+ child.stdout.removeAllListeners();
1818
+ if (!useStderrInherit) child.stderr.removeAllListeners();
1819
+ child.removeAllListeners();
1820
+ mdStreamer.finish();
1821
+ if (!useStderrInherit && stderrComposerBuffer.trim()) {
1822
+ const rest = stderrComposerBuffer.trim();
1823
+ emit({ type: "raw", source: "codex", stream: "stderr", eventType: "tail", text: rawTraceText(rest) });
1824
+ emit({ type: "status", line: `[stderr] ${truncateComposerLine(rest)}` });
1825
+ }
1826
+ const finalMessage = fs.existsSync(outputLastMessagePath) ? fs.readFileSync(outputLastMessagePath, "utf-8").trim() : "";
1827
+ if (code !== 0) {
1828
+ const stderr = Buffer.concat(stderrChunks).toString("utf-8");
1829
+ const stderrTail = stderr ? stderr.trim().slice(-1200) : "";
1830
+ const err = new Error(codexFailureMessage(code, stderrTail));
1831
+ err.codexStderrTail = stderrTail;
1832
+ emit({ type: "status", line: truncateComposerLine(err.message) });
1833
+ reject(err);
1834
+ return;
1835
+ }
1836
+ if (hadError) {
1837
+ const msg = finalMessage || "Codex reported error.";
1838
+ emit({ type: "status", line: truncateComposerLine(msg) });
1839
+ reject(new Error(msg));
1840
+ return;
1841
+ }
1842
+ if (finalMessage && !emittedNatural) {
1843
+ emit({ type: "natural", kind: "result", text: finalMessage });
1844
+ }
1845
+ emit({ type: "status", line: t("runner.completed") });
1846
+ try {
1847
+ if (fs.existsSync(outputLastMessagePath)) fs.unlinkSync(outputLastMessagePath);
1848
+ } catch (_) {}
1849
+ resolve();
1850
+ });
1851
+ });
1852
+
1853
+ return { child, finished };
1854
+ }
package/bin/lib/auth.mjs CHANGED
@@ -78,6 +78,12 @@ function parseCookies(header) {
78
78
  return out;
79
79
  }
80
80
 
81
+ function bearerTokenFromAuthHeader(header) {
82
+ const raw = String(header || "").trim();
83
+ const m = raw.match(/^Bearer\s+(.+)$/i);
84
+ return m ? m[1].trim() : "";
85
+ }
86
+
81
87
  export function readAuthUsers() {
82
88
  return readJsonObject(usersPath());
83
89
  }
@@ -240,8 +246,12 @@ export function buildClearSessionCookie() {
240
246
  return `${SESSION_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`;
241
247
  }
242
248
 
249
+ export function getSessionTokenFromRequest(req) {
250
+ return parseCookies(req.headers.cookie || "")[SESSION_COOKIE] || bearerTokenFromAuthHeader(req.headers.authorization);
251
+ }
252
+
243
253
  export function getAuthUserFromRequest(req) {
244
- const token = parseCookies(req.headers.cookie || "")[SESSION_COOKIE];
254
+ const token = getSessionTokenFromRequest(req);
245
255
  if (!token) return null;
246
256
  const sessions = readJsonObject(sessionsPath());
247
257
  const key = hashToken(token);
@@ -296,6 +296,8 @@ export function listNodesJson(workspaceRoot, flowId, flowSource, opts = {}) {
296
296
  inputs: manifest.input,
297
297
  outputs: manifest.output,
298
298
  source: manifest.source || "marketplace",
299
+ ownerUserId: manifest.ownerUserId || manifest.createdBy || "",
300
+ createdBy: manifest.createdBy || manifest.ownerUserId || "",
299
301
  packageDir: manifest.packageDir,
300
302
  runtime: manifest.runtime,
301
303
  });