@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,118 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
import { LEGACY_MODEL_CONFIG_REL, MODEL_CONFIG_REL } from "./paths.mjs";
|
|
5
|
+
|
|
6
|
+
const modelConfigCache = new Map();
|
|
7
|
+
|
|
8
|
+
/** UI 格式为「模型 ID - 描述」,传参只用前面的模型 ID。若为 "auto" 则规范为 Cursor 可识别的 "Auto"。 */
|
|
9
|
+
export function normalizeCursorModelForCli(value) {
|
|
10
|
+
if (value == null || value === false || value === "") return "Auto";
|
|
11
|
+
let s = String(value).trim();
|
|
12
|
+
if (!s) return "Auto";
|
|
13
|
+
const dashIdx = s.indexOf(" - ");
|
|
14
|
+
if (dashIdx >= 0) s = s.slice(0, dashIdx).trim();
|
|
15
|
+
if (!s) return "Auto";
|
|
16
|
+
if (/^auto$/i.test(s)) return "Auto";
|
|
17
|
+
return s;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function loadModelConfig(workspaceRoot) {
|
|
21
|
+
const root = path.resolve(workspaceRoot);
|
|
22
|
+
if (modelConfigCache.has(root)) return modelConfigCache.get(root);
|
|
23
|
+
const primaryPath = path.join(root, MODEL_CONFIG_REL);
|
|
24
|
+
const legacyPath = path.join(root, LEGACY_MODEL_CONFIG_REL);
|
|
25
|
+
const configPath = fs.existsSync(primaryPath) ? primaryPath : legacyPath;
|
|
26
|
+
let config = { models: {} };
|
|
27
|
+
try {
|
|
28
|
+
if (fs.existsSync(configPath)) {
|
|
29
|
+
const raw = fs.readFileSync(configPath, "utf-8");
|
|
30
|
+
const parsed = JSON.parse(raw);
|
|
31
|
+
if (parsed && typeof parsed === "object" && parsed.models && typeof parsed.models === "object") {
|
|
32
|
+
config = { models: parsed.models };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
} catch (_) {
|
|
36
|
+
// ignore parse errors
|
|
37
|
+
}
|
|
38
|
+
modelConfigCache.set(root, config);
|
|
39
|
+
return config;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function resolveCliAndModel(workspaceRoot, nodeModel, agentModelOverride) {
|
|
43
|
+
if (agentModelOverride && String(agentModelOverride).trim()) {
|
|
44
|
+
const raw = String(agentModelOverride).trim();
|
|
45
|
+
if (raw.startsWith("api:")) {
|
|
46
|
+
return { cli: "api", model: raw, label: raw };
|
|
47
|
+
}
|
|
48
|
+
if (raw.startsWith("claude-code:")) {
|
|
49
|
+
const m = raw.slice("claude-code:".length).trim();
|
|
50
|
+
return {
|
|
51
|
+
cli: "claude-code",
|
|
52
|
+
model: m || null,
|
|
53
|
+
label: m ? `claude-code: ${m}` : "claude-code (default)",
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
if (raw.startsWith("opencode:")) {
|
|
57
|
+
const m = raw.slice("opencode:".length).trim();
|
|
58
|
+
return {
|
|
59
|
+
cli: "opencode",
|
|
60
|
+
model: m || null,
|
|
61
|
+
label: m ? `opencode: ${m}` : "opencode (default)",
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const model = normalizeCursorModelForCli(raw);
|
|
65
|
+
return { cli: "cursor", model, label: `cursor: ${model}` };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const key = nodeModel && String(nodeModel).trim() ? String(nodeModel).trim() : "";
|
|
69
|
+
if (key) {
|
|
70
|
+
const { models } = loadModelConfig(workspaceRoot);
|
|
71
|
+
const cfg = models[key];
|
|
72
|
+
if (cfg && typeof cfg === "object" && cfg.cli && cfg.model) {
|
|
73
|
+
const cli =
|
|
74
|
+
cfg.cli === "opencode"
|
|
75
|
+
? "opencode"
|
|
76
|
+
: cfg.cli === "api"
|
|
77
|
+
? "api"
|
|
78
|
+
: cfg.cli === "claude-code"
|
|
79
|
+
? "claude-code"
|
|
80
|
+
: "cursor";
|
|
81
|
+
const model = String(cfg.model).trim();
|
|
82
|
+
return { cli, model, label: `${cli}: ${model}` };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (key && key.startsWith("api:")) {
|
|
87
|
+
return { cli: "api", model: key, label: key };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (key && key.startsWith("claude-code:")) {
|
|
91
|
+
const model = key.slice("claude-code:".length) || "";
|
|
92
|
+
return {
|
|
93
|
+
cli: "claude-code",
|
|
94
|
+
model: model || null,
|
|
95
|
+
label: model ? `claude-code: ${model}` : "claude-code (default)",
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (key && key.startsWith("opencode:")) {
|
|
100
|
+
const model = key.slice("opencode:".length) || "";
|
|
101
|
+
return {
|
|
102
|
+
cli: "opencode",
|
|
103
|
+
model: model || null,
|
|
104
|
+
label: model ? `opencode: ${model}` : "opencode (default)",
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const envModel = process.env.CURSOR_AGENT_MODEL && String(process.env.CURSOR_AGENT_MODEL).trim();
|
|
109
|
+
if (envModel && envModel.startsWith("api:")) {
|
|
110
|
+
return { cli: "api", model: envModel, label: envModel };
|
|
111
|
+
}
|
|
112
|
+
const model = normalizeCursorModelForCli(key || envModel || "Auto");
|
|
113
|
+
return {
|
|
114
|
+
cli: "cursor",
|
|
115
|
+
model,
|
|
116
|
+
label: model === "Auto" ? "cursor: Auto" : `cursor: ${model}`,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { CURSOR_NON_MODEL_PATTERNS, getAgentflowUserConfigAbs, getModelListsAbs } from "./paths.mjs";
|
|
6
|
+
|
|
7
|
+
/** GUI / 精简 PATH 下常见找不到 Homebrew 等目录下的 CLI,为子进程补上 */
|
|
8
|
+
function envWithCommonBinPaths() {
|
|
9
|
+
const env = { ...process.env };
|
|
10
|
+
const extra = [
|
|
11
|
+
path.join(os.homedir(), ".local", "bin"),
|
|
12
|
+
"/opt/homebrew/bin",
|
|
13
|
+
"/usr/local/bin",
|
|
14
|
+
].filter((p) => {
|
|
15
|
+
try {
|
|
16
|
+
return fs.existsSync(p);
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
if (extra.length === 0) return env;
|
|
22
|
+
const sep = path.delimiter;
|
|
23
|
+
const prefix = extra.join(sep);
|
|
24
|
+
env.PATH = env.PATH && String(env.PATH).trim() !== "" ? `${prefix}${sep}${env.PATH}` : prefix;
|
|
25
|
+
return env;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function stripAnsiModelList(text) {
|
|
29
|
+
return String(text || "").replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function isCursorModelLine(line) {
|
|
33
|
+
const lower = line.toLowerCase();
|
|
34
|
+
if (lower.startsWith("name") || lower === "models" || lower === "model") return false;
|
|
35
|
+
if (lower.startsWith("error") || lower.startsWith("fatal") || lower.startsWith("warning:")) return false;
|
|
36
|
+
if (CURSOR_NON_MODEL_PATTERNS.some((re) => re.test(line))) return false;
|
|
37
|
+
return line.length > 0 && line.length < 200;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function parseModelLines(stdout) {
|
|
41
|
+
const cleaned = stripAnsiModelList(stdout);
|
|
42
|
+
const lines = cleaned.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
|
|
43
|
+
return lines.filter(isCursorModelLine);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function runCursorModels(workspaceRoot) {
|
|
47
|
+
return new Promise((resolve) => {
|
|
48
|
+
const agentCmd = process.env.CURSOR_AGENT_CMD || "agent";
|
|
49
|
+
const child = spawn(agentCmd, ["models"], { cwd: workspaceRoot, shell: false });
|
|
50
|
+
let out = "";
|
|
51
|
+
child.stdout?.on("data", (chunk) => {
|
|
52
|
+
out += chunk.toString("utf-8");
|
|
53
|
+
});
|
|
54
|
+
child.stderr?.on("data", (chunk) => {
|
|
55
|
+
out += chunk.toString("utf-8");
|
|
56
|
+
});
|
|
57
|
+
child.on("close", (code) => (code === 0 ? resolve(parseModelLines(out)) : resolve([])));
|
|
58
|
+
child.on("error", () => resolve([]));
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Claude Code CLI 不暴露模型列表子命令 — 使用内置列表。
|
|
64
|
+
* 新模型发布时更新此常量即可。
|
|
65
|
+
*/
|
|
66
|
+
export const BUILTIN_CLAUDE_CODE_MODELS = [
|
|
67
|
+
"claude-opus-4-7",
|
|
68
|
+
"claude-sonnet-4-6",
|
|
69
|
+
"claude-haiku-4-5",
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
export function getBuiltinClaudeCodeModels() {
|
|
73
|
+
return BUILTIN_CLAUDE_CODE_MODELS.slice();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 探测 Claude Code CLI 是否可用(仅判定能否启动,不拉取模型)。
|
|
78
|
+
*/
|
|
79
|
+
export function probeClaudeCodeAvailable() {
|
|
80
|
+
return new Promise((resolve) => {
|
|
81
|
+
const claudeCmd = process.env.CLAUDE_CODE_CMD || "claude";
|
|
82
|
+
const child = spawn(claudeCmd, ["--version"], {
|
|
83
|
+
shell: false,
|
|
84
|
+
env: envWithCommonBinPaths(),
|
|
85
|
+
});
|
|
86
|
+
let settled = false;
|
|
87
|
+
const finish = (ok) => {
|
|
88
|
+
if (settled) return;
|
|
89
|
+
settled = true;
|
|
90
|
+
resolve(ok);
|
|
91
|
+
};
|
|
92
|
+
child.on("close", (code) => finish(code === 0));
|
|
93
|
+
child.on("error", () => finish(false));
|
|
94
|
+
setTimeout(() => {
|
|
95
|
+
try {
|
|
96
|
+
child.kill("SIGTERM");
|
|
97
|
+
} catch {}
|
|
98
|
+
finish(false);
|
|
99
|
+
}, 5000);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function runOpencodeModels(workspaceRoot, provider) {
|
|
104
|
+
if (!provider || !String(provider).trim()) return Promise.resolve([]);
|
|
105
|
+
return new Promise((resolve) => {
|
|
106
|
+
const opencodeCmd = process.env.OPENCODE_CMD || "opencode";
|
|
107
|
+
const child = spawn(opencodeCmd, ["models", String(provider).trim()], {
|
|
108
|
+
cwd: workspaceRoot,
|
|
109
|
+
shell: false,
|
|
110
|
+
env: envWithCommonBinPaths(),
|
|
111
|
+
});
|
|
112
|
+
let stdout = "";
|
|
113
|
+
let stderr = "";
|
|
114
|
+
child.stdout?.on("data", (chunk) => {
|
|
115
|
+
stdout += chunk.toString("utf-8");
|
|
116
|
+
});
|
|
117
|
+
child.stderr?.on("data", (chunk) => {
|
|
118
|
+
stderr += chunk.toString("utf-8");
|
|
119
|
+
});
|
|
120
|
+
child.on("close", (code) => {
|
|
121
|
+
if (code === 0) resolve(parseModelLines(stdout || stderr));
|
|
122
|
+
else resolve([]);
|
|
123
|
+
});
|
|
124
|
+
child.on("error", () => resolve([]));
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 拉取 Cursor / OpenCode 模型列表并写入 ~/agentflow/model-lists.json
|
|
130
|
+
* @param {string} workspaceRoot
|
|
131
|
+
* @param {{ opencodeProviderOverride?: string }} [opts]
|
|
132
|
+
*/
|
|
133
|
+
export async function updateModelLists(workspaceRoot, opts = {}) {
|
|
134
|
+
const root = path.resolve(workspaceRoot);
|
|
135
|
+
const override =
|
|
136
|
+
typeof opts.opencodeProviderOverride === "string" ? opts.opencodeProviderOverride.trim() : "";
|
|
137
|
+
let opencodeProvider = override;
|
|
138
|
+
if (!opencodeProvider) {
|
|
139
|
+
try {
|
|
140
|
+
const configPath = getAgentflowUserConfigAbs();
|
|
141
|
+
if (fs.existsSync(configPath)) {
|
|
142
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
143
|
+
if (typeof config?.opencodeProvider === "string") opencodeProvider = config.opencodeProvider.trim();
|
|
144
|
+
}
|
|
145
|
+
} catch (_) {}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const cachePath = getModelListsAbs();
|
|
149
|
+
let prev = {
|
|
150
|
+
cursor: [],
|
|
151
|
+
opencode: [],
|
|
152
|
+
claudeCode: [],
|
|
153
|
+
cursorFetchedAt: null,
|
|
154
|
+
opencodeFetchedAt: null,
|
|
155
|
+
claudeCodeFetchedAt: null,
|
|
156
|
+
};
|
|
157
|
+
try {
|
|
158
|
+
if (fs.existsSync(cachePath)) {
|
|
159
|
+
const raw = JSON.parse(fs.readFileSync(cachePath, "utf-8"));
|
|
160
|
+
if (raw && typeof raw === "object" && !Array.isArray(raw)) prev = { ...prev, ...raw };
|
|
161
|
+
}
|
|
162
|
+
} catch (_) {}
|
|
163
|
+
|
|
164
|
+
const [cursorRaw, opencode, claudeCodeAvailable] = await Promise.all([
|
|
165
|
+
runCursorModels(root),
|
|
166
|
+
runOpencodeModels(root, opencodeProvider),
|
|
167
|
+
probeClaudeCodeAvailable(),
|
|
168
|
+
]);
|
|
169
|
+
const cursor = cursorRaw.filter(isCursorModelLine);
|
|
170
|
+
const claudeCode = claudeCodeAvailable ? getBuiltinClaudeCodeModels() : [];
|
|
171
|
+
const now = Date.now();
|
|
172
|
+
|
|
173
|
+
const data = {
|
|
174
|
+
cursor: cursor.length > 0 ? cursor : prev.cursor ?? [],
|
|
175
|
+
opencode: opencode.length > 0 ? opencode : prev.opencode ?? [],
|
|
176
|
+
claudeCode: claudeCode.length > 0 ? claudeCode : prev.claudeCode ?? [],
|
|
177
|
+
cursorFetchedAt: cursor.length > 0 ? now : prev.cursorFetchedAt ?? null,
|
|
178
|
+
opencodeFetchedAt: opencode.length > 0 ? now : prev.opencodeFetchedAt ?? null,
|
|
179
|
+
claudeCodeFetchedAt: claudeCode.length > 0 ? now : prev.claudeCodeFetchedAt ?? null,
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
|
|
184
|
+
fs.writeFileSync(cachePath, JSON.stringify(data, null, 2), "utf-8");
|
|
185
|
+
} catch (_) {}
|
|
186
|
+
|
|
187
|
+
return { cursor: data.cursor, opencode: data.opencode, claudeCode: data.claudeCode };
|
|
188
|
+
}
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads intermediate + output files for a given node instance across all execution rounds.
|
|
3
|
+
* Used by the UI run-mode sidebar to show prompt, result, and output content per execId.
|
|
4
|
+
*/
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import { getWorkspaceRunBuildRoot } from "./paths.mjs";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {string} workspaceRoot
|
|
11
|
+
* @param {string} flowId
|
|
12
|
+
* @param {string} instanceId
|
|
13
|
+
* @param {string} [runId] - specific uuid; if empty, picks the latest run
|
|
14
|
+
* @returns {{ ok: boolean, rounds: Array, runId?: string, error?: string }}
|
|
15
|
+
*/
|
|
16
|
+
export function getNodeExecContext(workspaceRoot, flowId, instanceId, runId) {
|
|
17
|
+
const runBuildRoot = getWorkspaceRunBuildRoot(workspaceRoot);
|
|
18
|
+
const flowRunDir = path.join(runBuildRoot, flowId);
|
|
19
|
+
if (!fs.existsSync(flowRunDir)) return { ok: true, rounds: [], runId: "" };
|
|
20
|
+
|
|
21
|
+
let uuid = runId;
|
|
22
|
+
if (!uuid) {
|
|
23
|
+
const entries = fs.readdirSync(flowRunDir).filter((e) => {
|
|
24
|
+
const s = fs.statSync(path.join(flowRunDir, e));
|
|
25
|
+
return s.isDirectory();
|
|
26
|
+
});
|
|
27
|
+
entries.sort((a, b) => b.localeCompare(a));
|
|
28
|
+
uuid = entries[0] || "";
|
|
29
|
+
}
|
|
30
|
+
if (!uuid) return { ok: true, rounds: [], runId: "" };
|
|
31
|
+
|
|
32
|
+
const runDir = path.join(flowRunDir, uuid);
|
|
33
|
+
const interDir = path.join(runDir, "intermediate", instanceId);
|
|
34
|
+
const outDir = path.join(runDir, "output", instanceId);
|
|
35
|
+
|
|
36
|
+
const rounds = collectRounds(interDir, outDir, instanceId);
|
|
37
|
+
|
|
38
|
+
return { ok: true, rounds, runId: uuid };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function collectRounds(interDir, outDir, instanceId) {
|
|
42
|
+
const roundMap = new Map();
|
|
43
|
+
const currentBasename = `${instanceId}.result.md`;
|
|
44
|
+
|
|
45
|
+
if (fs.existsSync(interDir)) {
|
|
46
|
+
const files = fs.readdirSync(interDir);
|
|
47
|
+
for (const f of files) {
|
|
48
|
+
const fp = path.join(interDir, f);
|
|
49
|
+
if (!fs.statSync(fp).isFile()) continue;
|
|
50
|
+
|
|
51
|
+
if (f === currentBasename) {
|
|
52
|
+
ensureRound(roundMap, "current");
|
|
53
|
+
roundMap.get("current").resultFile = fp;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const promptCurrentName = `${instanceId}.prompt.md`;
|
|
58
|
+
if (f === promptCurrentName) {
|
|
59
|
+
ensureRound(roundMap, "current");
|
|
60
|
+
roundMap.get("current").promptFile = fp;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const cacheCurrentName = `${instanceId}.cache.json`;
|
|
65
|
+
if (f === cacheCurrentName) {
|
|
66
|
+
ensureRound(roundMap, "current");
|
|
67
|
+
roundMap.get("current").cacheFile = fp;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const backupResult = f.match(new RegExp(`^${escRe(instanceId)}\\.result_(\\d+)\\.md$`));
|
|
72
|
+
if (backupResult) {
|
|
73
|
+
const eid = backupResult[1];
|
|
74
|
+
ensureRound(roundMap, eid);
|
|
75
|
+
roundMap.get(eid).resultFile = fp;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const backupPrompt = f.match(new RegExp(`^${escRe(instanceId)}\\.prompt_(\\d+)\\.md$`));
|
|
80
|
+
if (backupPrompt) {
|
|
81
|
+
const eid = backupPrompt[1];
|
|
82
|
+
ensureRound(roundMap, eid);
|
|
83
|
+
roundMap.get(eid).promptFile = fp;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (fs.existsSync(outDir)) {
|
|
90
|
+
const files = fs.readdirSync(outDir);
|
|
91
|
+
const prefix = `node_${instanceId}_`;
|
|
92
|
+
for (const f of files) {
|
|
93
|
+
const fp = path.join(outDir, f);
|
|
94
|
+
if (!fs.statSync(fp).isFile()) continue;
|
|
95
|
+
if (!f.startsWith(prefix)) continue;
|
|
96
|
+
|
|
97
|
+
const backupOut = f.match(
|
|
98
|
+
new RegExp(`^${escRe(prefix)}(.+?)_(\\d+)(\\.[a-zA-Z0-9]+)$`),
|
|
99
|
+
);
|
|
100
|
+
if (backupOut) {
|
|
101
|
+
const slot = backupOut[1];
|
|
102
|
+
const eid = backupOut[2];
|
|
103
|
+
ensureRound(roundMap, eid);
|
|
104
|
+
const r = roundMap.get(eid);
|
|
105
|
+
if (!r.outputFiles) r.outputFiles = [];
|
|
106
|
+
r.outputFiles.push({ slot, path: fp });
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const currentOut = f.match(new RegExp(`^${escRe(prefix)}(.+)(\\.[a-zA-Z0-9]+)$`));
|
|
111
|
+
if (currentOut) {
|
|
112
|
+
const slot = currentOut[1];
|
|
113
|
+
ensureRound(roundMap, "current");
|
|
114
|
+
const r = roundMap.get("current");
|
|
115
|
+
if (!r.outputFiles) r.outputFiles = [];
|
|
116
|
+
r.outputFiles.push({ slot, path: fp });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const result = [];
|
|
122
|
+
for (const [key, data] of roundMap) {
|
|
123
|
+
// Skip phantom rounds that only have a prompt backup but no result/cache/output
|
|
124
|
+
// (caused by prior buildNodePrompt double-call bug creating spurious _N backups)
|
|
125
|
+
if (key !== "current" && !data.resultFile && !data.cacheFile && !data.outputFiles) continue;
|
|
126
|
+
|
|
127
|
+
const round = { execId: key === "current" ? "latest" : key };
|
|
128
|
+
|
|
129
|
+
if (data.resultFile) {
|
|
130
|
+
const raw = safeRead(data.resultFile);
|
|
131
|
+
const fm = parseResultFrontmatter(raw);
|
|
132
|
+
round.status = fm.status || null;
|
|
133
|
+
round.finishedAt = fm.finishedAt || null;
|
|
134
|
+
round.message = fm.message || null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (data.cacheFile) {
|
|
138
|
+
try {
|
|
139
|
+
const cacheRaw = safeRead(data.cacheFile, 200000);
|
|
140
|
+
const cacheJson = JSON.parse(cacheRaw);
|
|
141
|
+
if (cacheJson.cacheInputInfo) {
|
|
142
|
+
const inputInfo = typeof cacheJson.cacheInputInfo === "string"
|
|
143
|
+
? JSON.parse(cacheJson.cacheInputInfo)
|
|
144
|
+
: cacheJson.cacheInputInfo;
|
|
145
|
+
if (Array.isArray(inputInfo.inputPaths)) {
|
|
146
|
+
round.inputs = inputInfo.inputPaths
|
|
147
|
+
.filter((p) => p.slot !== "upstreamMd5")
|
|
148
|
+
.map((p) => ({ slot: p.slot, value: p.value ?? "" }));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
} catch {
|
|
152
|
+
/* ignore parse errors */
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (data.promptFile) {
|
|
157
|
+
round.prompt = safeRead(data.promptFile, 50000);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (data.outputFiles && data.outputFiles.length > 0) {
|
|
161
|
+
round.outputs = data.outputFiles.map((o) => readOutputForUi(o.path, o.slot));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
result.push(round);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
result.sort((a, b) => {
|
|
168
|
+
if (a.execId === "latest") return 1;
|
|
169
|
+
if (b.execId === "latest") return -1;
|
|
170
|
+
return Number(a.execId) - Number(b.execId);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
return result;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function ensureRound(map, key) {
|
|
177
|
+
if (!map.has(key)) map.set(key, {});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function escRe(s) {
|
|
181
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const TEXT_MAX = 50000;
|
|
185
|
+
const BINARY_MAX = 5 * 1024 * 1024;
|
|
186
|
+
|
|
187
|
+
function safeRead(fp, maxLen) {
|
|
188
|
+
try {
|
|
189
|
+
const cap = maxLen != null && maxLen > 0 ? maxLen : TEXT_MAX;
|
|
190
|
+
const st = fs.statSync(fp);
|
|
191
|
+
if (st.size > cap) {
|
|
192
|
+
const fd = fs.openSync(fp, "r");
|
|
193
|
+
try {
|
|
194
|
+
const buf = Buffer.alloc(Math.min(cap + 4, st.size));
|
|
195
|
+
fs.readSync(fd, buf, 0, buf.length, 0);
|
|
196
|
+
return buf.toString("utf-8").slice(0, cap) + "\n…(truncated)";
|
|
197
|
+
} finally {
|
|
198
|
+
fs.closeSync(fd);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
let content = fs.readFileSync(fp, "utf-8");
|
|
202
|
+
if (content.length > cap) content = content.slice(0, cap) + "\n…(truncated)";
|
|
203
|
+
return content;
|
|
204
|
+
} catch {
|
|
205
|
+
return "";
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Sniff binary image / common formats from the first bytes (handles e.g. PNG written under a .md path).
|
|
211
|
+
* @param {Buffer} buf
|
|
212
|
+
* @returns {{ mime: string, displayKind: string } | null}
|
|
213
|
+
*/
|
|
214
|
+
function sniffBinaryKind(buf) {
|
|
215
|
+
if (!buf || buf.length < 4) return null;
|
|
216
|
+
if (buf.length >= 8 && buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4e && buf[3] === 0x47) {
|
|
217
|
+
return { mime: "image/png", displayKind: "image" };
|
|
218
|
+
}
|
|
219
|
+
if (buf.length >= 3 && buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff) {
|
|
220
|
+
return { mime: "image/jpeg", displayKind: "image" };
|
|
221
|
+
}
|
|
222
|
+
if (buf.length >= 6 && buf[0] === 0x47 && buf[1] === 0x49 && buf[2] === 0x46 && buf[3] === 0x38) {
|
|
223
|
+
return { mime: "image/gif", displayKind: "image" };
|
|
224
|
+
}
|
|
225
|
+
if (buf.length >= 12 && buf.toString("ascii", 0, 4) === "RIFF" && buf.toString("ascii", 8, 12) === "WEBP") {
|
|
226
|
+
return { mime: "image/webp", displayKind: "image" };
|
|
227
|
+
}
|
|
228
|
+
if (buf.length >= 2 && buf.toString("ascii", 0, 2) === "BM") {
|
|
229
|
+
return { mime: "image/bmp", displayKind: "image" };
|
|
230
|
+
}
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function extMime(ext) {
|
|
235
|
+
const e = (ext || "").toLowerCase();
|
|
236
|
+
const map = {
|
|
237
|
+
".png": "image/png",
|
|
238
|
+
".jpg": "image/jpeg",
|
|
239
|
+
".jpeg": "image/jpeg",
|
|
240
|
+
".gif": "image/gif",
|
|
241
|
+
".webp": "image/webp",
|
|
242
|
+
".bmp": "image/bmp",
|
|
243
|
+
".json": "application/json",
|
|
244
|
+
".md": "text/markdown",
|
|
245
|
+
".markdown": "text/markdown",
|
|
246
|
+
};
|
|
247
|
+
return map[e] || null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function isLikelyJsonText(s) {
|
|
251
|
+
const t = (s || "").trim();
|
|
252
|
+
if (!t || (t[0] !== "{" && t[0] !== "[")) return false;
|
|
253
|
+
try {
|
|
254
|
+
JSON.parse(t);
|
|
255
|
+
return true;
|
|
256
|
+
} catch {
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* @param {string} fp
|
|
263
|
+
* @param {string} slot
|
|
264
|
+
* @returns {{ slot: string, content: string, encoding: string, mimeType?: string, displayKind: string }}
|
|
265
|
+
*/
|
|
266
|
+
function readOutputForUi(fp, slot) {
|
|
267
|
+
try {
|
|
268
|
+
const st = fs.statSync(fp);
|
|
269
|
+
const ext = path.extname(fp);
|
|
270
|
+
const readSize = Math.min(st.size, BINARY_MAX);
|
|
271
|
+
const buf = Buffer.alloc(readSize);
|
|
272
|
+
const fd = fs.openSync(fp, "r");
|
|
273
|
+
try {
|
|
274
|
+
fs.readSync(fd, buf, 0, readSize, 0);
|
|
275
|
+
} finally {
|
|
276
|
+
fs.closeSync(fd);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const sniffed = sniffBinaryKind(buf);
|
|
280
|
+
if (sniffed) {
|
|
281
|
+
const truncated = st.size > BINARY_MAX;
|
|
282
|
+
return {
|
|
283
|
+
slot,
|
|
284
|
+
content: buf.toString("base64"),
|
|
285
|
+
encoding: "base64",
|
|
286
|
+
mimeType: sniffed.mime,
|
|
287
|
+
displayKind: sniffed.displayKind,
|
|
288
|
+
...(truncated ? { truncated: true } : {}),
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
let text = buf.toString("utf8");
|
|
293
|
+
const cap = TEXT_MAX;
|
|
294
|
+
let truncatedText = false;
|
|
295
|
+
if (text.length > cap) {
|
|
296
|
+
text = text.slice(0, cap);
|
|
297
|
+
truncatedText = true;
|
|
298
|
+
} else if (st.size > readSize) {
|
|
299
|
+
truncatedText = true;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const mimeFromExt = extMime(ext);
|
|
303
|
+
let displayKind = "text";
|
|
304
|
+
if (mimeFromExt === "application/json" || isLikelyJsonText(text)) {
|
|
305
|
+
displayKind = "json";
|
|
306
|
+
} else if (mimeFromExt === "text/markdown" || ext === ".md" || ext === ".markdown") {
|
|
307
|
+
displayKind = "markdown";
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const out = {
|
|
311
|
+
slot,
|
|
312
|
+
content: truncatedText ? text + "\n…(truncated)" : text,
|
|
313
|
+
encoding: "utf-8",
|
|
314
|
+
displayKind,
|
|
315
|
+
};
|
|
316
|
+
if (mimeFromExt) out.mimeType = mimeFromExt;
|
|
317
|
+
return out;
|
|
318
|
+
} catch {
|
|
319
|
+
return { slot, content: "", encoding: "utf-8", displayKind: "text" };
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function parseResultFrontmatter(raw) {
|
|
324
|
+
const m = raw.match(/^---\s*\r?\n([\s\S]*?)\r?\n---/);
|
|
325
|
+
if (!m) return {};
|
|
326
|
+
const obj = {};
|
|
327
|
+
for (const line of m[1].split(/\r?\n/)) {
|
|
328
|
+
const idx = line.indexOf(": ");
|
|
329
|
+
if (idx <= 0) continue;
|
|
330
|
+
const k = line.slice(0, idx).trim();
|
|
331
|
+
let v = line.slice(idx + 2).trim();
|
|
332
|
+
if (v.startsWith('"') && v.endsWith('"')) v = v.slice(1, -1).replace(/\\n/g, "\n").replace(/\\"/g, '"');
|
|
333
|
+
obj[k] = v;
|
|
334
|
+
}
|
|
335
|
+
return obj;
|
|
336
|
+
}
|