@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.
Files changed (138) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +201 -0
  3. package/README.zh-CN.md +201 -0
  4. package/agents/agentflow-node-executor-code.md +32 -0
  5. package/agents/agentflow-node-executor-planning.md +32 -0
  6. package/agents/agentflow-node-executor-requirement.md +32 -0
  7. package/agents/agentflow-node-executor-test.md +32 -0
  8. package/agents/agentflow-node-executor-ui.md +32 -0
  9. package/agents/agentflow-node-executor.md +32 -0
  10. package/agents/agents.json +8 -0
  11. package/agents/en/agentflow-node-executor.md +32 -0
  12. package/agents/zh/agentflow-node-executor.md +32 -0
  13. package/bin/agentflow.mjs +52 -0
  14. package/bin/ensure-workspace-reference.mjs +35 -0
  15. package/bin/lib/agent-runners.mjs +1199 -0
  16. package/bin/lib/agents-path.mjs +61 -0
  17. package/bin/lib/api-runner.mjs +361 -0
  18. package/bin/lib/apply.mjs +852 -0
  19. package/bin/lib/catalog-agents.mjs +300 -0
  20. package/bin/lib/catalog-flows.mjs +532 -0
  21. package/bin/lib/composer-agent.mjs +884 -0
  22. package/bin/lib/composer-flow-instances.mjs +68 -0
  23. package/bin/lib/composer-flow-skeleton.mjs +334 -0
  24. package/bin/lib/composer-flow-validate.mjs +47 -0
  25. package/bin/lib/composer-log.mjs +197 -0
  26. package/bin/lib/composer-model-router.mjs +160 -0
  27. package/bin/lib/composer-node-schema.mjs +299 -0
  28. package/bin/lib/composer-planner.mjs +749 -0
  29. package/bin/lib/composer-script-ops.mjs +233 -0
  30. package/bin/lib/composer-skill-router.mjs +384 -0
  31. package/bin/lib/flow-import.mjs +305 -0
  32. package/bin/lib/flow-normalize.mjs +71 -0
  33. package/bin/lib/flow-write.mjs +395 -0
  34. package/bin/lib/help.mjs +139 -0
  35. package/bin/lib/hub-login.mjs +54 -0
  36. package/bin/lib/hub-publish.mjs +159 -0
  37. package/bin/lib/hub-remote.mjs +189 -0
  38. package/bin/lib/hub.mjs +299 -0
  39. package/bin/lib/i18n.mjs +233 -0
  40. package/bin/lib/locales/en.json +344 -0
  41. package/bin/lib/locales/zh.json +344 -0
  42. package/bin/lib/log.mjs +37 -0
  43. package/bin/lib/main.mjs +611 -0
  44. package/bin/lib/model-config.mjs +118 -0
  45. package/bin/lib/model-lists.mjs +188 -0
  46. package/bin/lib/node-exec-context.mjs +336 -0
  47. package/bin/lib/node-execute.mjs +513 -0
  48. package/bin/lib/normalize-node-tool-command.mjs +97 -0
  49. package/bin/lib/paths.mjs +216 -0
  50. package/bin/lib/pipeline-scripts.mjs +41 -0
  51. package/bin/lib/recent-runs.mjs +173 -0
  52. package/bin/lib/run-apply-active-lock.mjs +82 -0
  53. package/bin/lib/run-events.mjs +85 -0
  54. package/bin/lib/run-node-statuses-from-disk.mjs +85 -0
  55. package/bin/lib/schedule-config.mjs +227 -0
  56. package/bin/lib/scheduler.mjs +312 -0
  57. package/bin/lib/table.mjs +4 -0
  58. package/bin/lib/terminal.mjs +42 -0
  59. package/bin/lib/ui-print.mjs +94 -0
  60. package/bin/lib/ui-server.mjs +2113 -0
  61. package/bin/lib/workspace-tree.mjs +266 -0
  62. package/bin/lib/workspace.mjs +180 -0
  63. package/bin/pipeline/build-node-prompt.mjs +179 -0
  64. package/bin/pipeline/check-cache.mjs +191 -0
  65. package/bin/pipeline/check-flow.mjs +543 -0
  66. package/bin/pipeline/collect-nodes.mjs +212 -0
  67. package/bin/pipeline/compute-cache-md5.mjs +177 -0
  68. package/bin/pipeline/ensure-run-dir.mjs +71 -0
  69. package/bin/pipeline/extract-thinking.mjs +308 -0
  70. package/bin/pipeline/gc.mjs +129 -0
  71. package/bin/pipeline/get-env.mjs +83 -0
  72. package/bin/pipeline/get-exec-id.mjs +145 -0
  73. package/bin/pipeline/get-ready-nodes.mjs +435 -0
  74. package/bin/pipeline/get-resolved-values.mjs +337 -0
  75. package/bin/pipeline/load-key.mjs +62 -0
  76. package/bin/pipeline/parse-bool.mjs +33 -0
  77. package/bin/pipeline/parse-flow.mjs +698 -0
  78. package/bin/pipeline/post-process-control-if.mjs +23 -0
  79. package/bin/pipeline/post-process-node.mjs +490 -0
  80. package/bin/pipeline/pre-process-node.mjs +449 -0
  81. package/bin/pipeline/resolve-inputs.mjs +201 -0
  82. package/bin/pipeline/run-log.mjs +34 -0
  83. package/bin/pipeline/run-tool-nodejs.mjs +160 -0
  84. package/bin/pipeline/save-key.mjs +93 -0
  85. package/bin/pipeline/snapshot-prior-round.mjs +70 -0
  86. package/bin/pipeline/validate-flow.mjs +825 -0
  87. package/bin/pipeline/validate-for-ui.mjs +226 -0
  88. package/bin/pipeline/validate-script-output.mjs +130 -0
  89. package/bin/pipeline/write-result.mjs +182 -0
  90. package/builtin/nodes/agent_subAgent.md +14 -0
  91. package/builtin/nodes/control_agent_toBool.md +20 -0
  92. package/builtin/nodes/control_anyOne.md +17 -0
  93. package/builtin/nodes/control_end.md +11 -0
  94. package/builtin/nodes/control_if.md +20 -0
  95. package/builtin/nodes/control_start.md +11 -0
  96. package/builtin/nodes/control_toBool.md +21 -0
  97. package/builtin/nodes/provide_file.md +11 -0
  98. package/builtin/nodes/provide_str.md +11 -0
  99. package/builtin/nodes/tool_get_env.md +14 -0
  100. package/builtin/nodes/tool_load_key.md +20 -0
  101. package/builtin/nodes/tool_nodejs.md +40 -0
  102. package/builtin/nodes/tool_print.md +14 -0
  103. package/builtin/nodes/tool_save_key.md +20 -0
  104. package/builtin/nodes/tool_user_ask.md +23 -0
  105. package/builtin/nodes/tool_user_check.md +22 -0
  106. package/builtin/pipelines/module-migrate/flow.yaml +819 -0
  107. package/builtin/pipelines/module-migrate/scripts/check_imports.mjs +700 -0
  108. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Makefile +362 -0
  109. 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
  110. 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
  111. 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
  112. 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
  113. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/.deps/Release/tree_sitter_kotlin_binding.node.d +1 -0
  114. 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
  115. 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
  116. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/obj.target/tree_sitter_kotlin_binding/src/parser.o +0 -0
  117. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/obj.target/tree_sitter_kotlin_binding/src/scanner.o +0 -0
  118. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/tree_sitter_kotlin_binding.node +0 -0
  119. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/binding.Makefile +6 -0
  120. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/gyp-mac-tool +768 -0
  121. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/node_modules/node-addon-api/node_addon_api.Makefile +6 -0
  122. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/node_modules/node-addon-api/node_addon_api.target.mk +122 -0
  123. 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
  124. 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
  125. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/tree_sitter_kotlin_binding.target.mk +203 -0
  126. package/builtin/pipelines/new/flow.yaml +545 -0
  127. package/builtin/pipelines/new/scripts/check-flow.mjs +9 -0
  128. package/builtin/pipelines/new/scripts/collect-nodes.mjs +211 -0
  129. package/builtin/pipelines/scripts/adjust-node-positions.mjs +113 -0
  130. package/builtin/web-ui/dist/agentflow-icon.svg +23 -0
  131. package/builtin/web-ui/dist/assets/index-CZkUPcXE.css +1 -0
  132. package/builtin/web-ui/dist/assets/index-DkkhNESc.js +190 -0
  133. package/builtin/web-ui/dist/index.html +24 -0
  134. package/package.json +67 -0
  135. package/reference/flow-control-capabilities.md +274 -0
  136. package/reference/flow-layout.md +84 -0
  137. package/reference/flow-prompt-handler-check.md +12 -0
  138. package/reference/flow-result-semantics.md +14 -0
@@ -0,0 +1,266 @@
1
+ /**
2
+ * 获取工作区目录树结构,用于 UI 工作区面板展开显示
3
+ */
4
+ import fs from "fs";
5
+ import path from "path";
6
+ import {
7
+ PIPELINES_DIR,
8
+ LEGACY_PIPELINES_DIR,
9
+ ARCHIVED_PIPELINES_DIR_NAME,
10
+ getUserPipelinesRoot,
11
+ PACKAGE_BUILTIN_PIPELINES_DIR,
12
+ } from "./paths.mjs";
13
+ import { listAllRunDirs } from "./workspace.mjs";
14
+
15
+ /**
16
+ * 获取目录下的子目录列表
17
+ * @param {string} dir
18
+ * @returns {Array<{name: string, path: string}>}
19
+ */
20
+ function getSubdirectories(dir) {
21
+ if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
22
+ return [];
23
+ }
24
+ try {
25
+ return fs
26
+ .readdirSync(dir, { withFileTypes: true })
27
+ .filter((e) => e.isDirectory() && !e.name.startsWith("."))
28
+ .map((e) => ({ name: e.name, path: path.join(dir, e.name) }));
29
+ } catch {
30
+ return [];
31
+ }
32
+ }
33
+
34
+ /**
35
+ * 获取工作区树形结构
36
+ * @param {string} workspaceRoot
37
+ * @returns {{
38
+ * pipelines: Array<{id: string, source: 'workspace', archived?: boolean}>,
39
+ * runs: Array<{flowId: string, runs: Array<{runId: string, at: number}>}>
40
+ * }}
41
+ */
42
+ export function getWorkspaceTree(workspaceRoot) {
43
+ const root = path.resolve(workspaceRoot);
44
+
45
+ // 1. 获取 pipelines 列表
46
+ const pipelines = [];
47
+ const seenPipelineIds = new Set();
48
+
49
+ // 主路径: .workspace/agentflow/pipelines
50
+ const wsPipelinesDir = path.join(root, PIPELINES_DIR);
51
+ const wsPipelines = getSubdirectories(wsPipelinesDir).filter(
52
+ (d) => d.name !== ARCHIVED_PIPELINES_DIR_NAME
53
+ );
54
+ for (const p of wsPipelines) {
55
+ if (!seenPipelineIds.has(p.name)) {
56
+ seenPipelineIds.add(p.name);
57
+ pipelines.push({ id: p.name, source: "workspace" });
58
+ }
59
+ }
60
+
61
+ // 归档路径: .workspace/agentflow/pipelines/_archived
62
+ const wsArchivedDir = path.join(wsPipelinesDir, ARCHIVED_PIPELINES_DIR_NAME);
63
+ const wsArchived = getSubdirectories(wsArchivedDir);
64
+ for (const p of wsArchived) {
65
+ if (!seenPipelineIds.has(p.name)) {
66
+ seenPipelineIds.add(p.name);
67
+ pipelines.push({ id: p.name, source: "workspace", archived: true });
68
+ }
69
+ }
70
+
71
+ // 旧版路径: .cursor/agentflow/pipelines (兼容读取)
72
+ const legacyPipelinesDir = path.join(root, LEGACY_PIPELINES_DIR);
73
+ const legacyPipelines = getSubdirectories(legacyPipelinesDir).filter(
74
+ (d) => d.name !== ARCHIVED_PIPELINES_DIR_NAME
75
+ );
76
+ for (const p of legacyPipelines) {
77
+ if (!seenPipelineIds.has(p.name)) {
78
+ seenPipelineIds.add(p.name);
79
+ pipelines.push({ id: p.name, source: "workspace" });
80
+ }
81
+ }
82
+
83
+ // 旧版归档路径
84
+ const legacyArchivedDir = path.join(legacyPipelinesDir, ARCHIVED_PIPELINES_DIR_NAME);
85
+ const legacyArchived = getSubdirectories(legacyArchivedDir);
86
+ for (const p of legacyArchived) {
87
+ if (!seenPipelineIds.has(p.name)) {
88
+ seenPipelineIds.add(p.name);
89
+ pipelines.push({ id: p.name, source: "workspace", archived: true });
90
+ }
91
+ }
92
+
93
+ // 2. 获取 runs 列表 (按 flowId 分组) —— 走统一的 listAllRunDirs,新旧位置都覆盖
94
+ const runsMap = new Map();
95
+
96
+ for (const { flowName: flowId, uuid, runDir } of listAllRunDirs(root)) {
97
+ let at = 0;
98
+ try {
99
+ const memoryPath = path.join(runDir, "memory.md");
100
+ if (fs.existsSync(memoryPath)) {
101
+ const content = fs.readFileSync(memoryPath, "utf-8");
102
+ for (const line of content.split(/\r?\n/)) {
103
+ const idx = line.indexOf(": ");
104
+ if (idx > 0 && line.slice(0, idx).trim() === "runStartTime") {
105
+ const v = line.slice(idx + 2).trim();
106
+ const n = parseInt(v, 10);
107
+ if (Number.isFinite(n) && n >= 0) {
108
+ at = n;
109
+ break;
110
+ }
111
+ }
112
+ }
113
+ }
114
+ if (at === 0) {
115
+ at = fs.statSync(runDir).mtimeMs;
116
+ }
117
+ } catch {
118
+ try {
119
+ at = fs.statSync(runDir).mtimeMs;
120
+ } catch {
121
+ at = 0;
122
+ }
123
+ }
124
+
125
+ if (!runsMap.has(flowId)) {
126
+ runsMap.set(flowId, []);
127
+ }
128
+ runsMap.get(flowId).push({ runId: uuid, at });
129
+ }
130
+
131
+ // 转换 runsMap 为数组,并排序
132
+ const runs = [];
133
+ for (const [flowId, flowRuns] of runsMap) {
134
+ flowRuns.sort((a, b) => b.at - a.at); // 最新的在前
135
+ runs.push({ flowId, runs: flowRuns.slice(0, 10) }); // 每个 flow 最多显示 10 个
136
+ }
137
+ runs.sort((a, b) => {
138
+ const aLatest = a.runs[0]?.at || 0;
139
+ const bLatest = b.runs[0]?.at || 0;
140
+ return bLatest - aLatest; // 按最新运行时间排序
141
+ });
142
+
143
+ return { pipelines, runs };
144
+ }
145
+
146
+ const FILE_ICON_MAP = {
147
+ ".yaml": "description",
148
+ ".yml": "description",
149
+ ".mjs": "code",
150
+ ".js": "code",
151
+ ".ts": "code",
152
+ ".json": "data_object",
153
+ ".md": "article",
154
+ ".txt": "note",
155
+ };
156
+
157
+ function getFileIcon(fileName) {
158
+ const ext = path.extname(fileName).toLowerCase();
159
+ return FILE_ICON_MAP[ext] || "draft";
160
+ }
161
+
162
+ function getDirectoryIcon(dirName) {
163
+ if (dirName === "scripts") return "terminal";
164
+ if (dirName === "nodes") return "hub";
165
+ return "folder";
166
+ }
167
+
168
+ function readFileSize(filePath) {
169
+ try {
170
+ const stats = fs.statSync(filePath);
171
+ return stats.size;
172
+ } catch {
173
+ return 0;
174
+ }
175
+ }
176
+
177
+ function readFilesRecursive(dir, baseDir, maxDepth = 2, currentDepth = 0) {
178
+ if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
179
+ return [];
180
+ }
181
+ if (currentDepth >= maxDepth) {
182
+ return [];
183
+ }
184
+ try {
185
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
186
+ const result = [];
187
+ for (const entry of entries) {
188
+ if (entry.name.startsWith(".")) continue;
189
+ const entryPath = path.join(dir, entry.name);
190
+ const relativePath = path.relative(baseDir, entryPath);
191
+ if (entry.isDirectory()) {
192
+ const children = readFilesRecursive(entryPath, baseDir, maxDepth, currentDepth + 1);
193
+ result.push({
194
+ name: entry.name,
195
+ type: "directory",
196
+ icon: getDirectoryIcon(entry.name),
197
+ path: relativePath,
198
+ children,
199
+ });
200
+ } else if (entry.isFile()) {
201
+ result.push({
202
+ name: entry.name,
203
+ type: "file",
204
+ icon: getFileIcon(entry.name),
205
+ path: relativePath,
206
+ size: readFileSize(entryPath),
207
+ });
208
+ }
209
+ }
210
+ result.sort((a, b) => {
211
+ if (a.type !== b.type) return a.type === "directory" ? -1 : 1;
212
+ if (a.name === "flow.yaml") return -1;
213
+ if (b.name === "flow.yaml") return 1;
214
+ if (a.name === "scripts") return -1;
215
+ if (b.name === "scripts") return 1;
216
+ return a.name.localeCompare(b.name);
217
+ });
218
+ return result;
219
+ } catch {
220
+ return [];
221
+ }
222
+ }
223
+
224
+ export function getPipelineFiles(workspaceRoot, flowId, flowSource, archived = false) {
225
+ const root = path.resolve(workspaceRoot);
226
+ let pipelineDir = null;
227
+
228
+ if (archived) {
229
+ if (flowSource === "user") {
230
+ pipelineDir = path.join(getUserPipelinesRoot(), ARCHIVED_PIPELINES_DIR_NAME, flowId);
231
+ } else if (flowSource === "workspace") {
232
+ pipelineDir = path.join(root, PIPELINES_DIR, ARCHIVED_PIPELINES_DIR_NAME, flowId);
233
+ if (!fs.existsSync(pipelineDir)) {
234
+ const alt = path.join(root, LEGACY_PIPELINES_DIR, ARCHIVED_PIPELINES_DIR_NAME, flowId);
235
+ if (fs.existsSync(alt)) pipelineDir = alt;
236
+ }
237
+ }
238
+ } else {
239
+ if (flowSource === "builtin") {
240
+ pipelineDir = path.join(PACKAGE_BUILTIN_PIPELINES_DIR, flowId);
241
+ } else if (flowSource === "user") {
242
+ pipelineDir = path.join(getUserPipelinesRoot(), flowId);
243
+ if (!fs.existsSync(pipelineDir)) {
244
+ const alt = path.join(root, PIPELINES_DIR, flowId);
245
+ if (fs.existsSync(alt)) pipelineDir = alt;
246
+ }
247
+ if (!fs.existsSync(pipelineDir)) {
248
+ const altLeg = path.join(root, LEGACY_PIPELINES_DIR, flowId);
249
+ if (fs.existsSync(altLeg)) pipelineDir = altLeg;
250
+ }
251
+ } else if (flowSource === "workspace") {
252
+ pipelineDir = path.join(root, PIPELINES_DIR, flowId);
253
+ if (!fs.existsSync(pipelineDir)) {
254
+ const altLeg = path.join(root, LEGACY_PIPELINES_DIR, flowId);
255
+ if (fs.existsSync(altLeg)) pipelineDir = altLeg;
256
+ }
257
+ }
258
+ }
259
+
260
+ if (!pipelineDir || !fs.existsSync(pipelineDir) || !fs.statSync(pipelineDir).isDirectory()) {
261
+ return { files: [], error: "Pipeline directory not found" };
262
+ }
263
+
264
+ const files = readFilesRecursive(pipelineDir, pipelineDir, 2, 0);
265
+ return { files, path: pipelineDir };
266
+ }
@@ -0,0 +1,180 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import {
4
+ LEGACY_PIPELINES_DIR,
5
+ PACKAGE_BUILTIN_PIPELINES_DIR,
6
+ PACKAGE_REFERENCE_DIR,
7
+ PIPELINES_DIR,
8
+ getLegacyUserRunBuildRoot,
9
+ getReferenceRootAbs,
10
+ getWorkspaceRunBuildRoot,
11
+ getUserPipelinesRoot,
12
+ ARCHIVED_PIPELINES_DIR_NAME,
13
+ } from "./paths.mjs";
14
+
15
+ export { getRunDir } from "./paths.mjs";
16
+
17
+ /**
18
+ * 枚举所有 run 目录(新 per-flow 位置 + 旧 runBuild root 位置)。
19
+ * 返回扁平列表:{ flowName, uuid, runDir, source },source ∈ { user, workspace, legacyWorkspaceRoot, legacyUserRoot }
20
+ * - user: ~/agentflow/pipelines/<name>/runBuild/<uuid>
21
+ * - workspace: <ws>/.workspace/agentflow/pipelines/<name>/runBuild/<uuid>
22
+ * - legacyWorkspaceRoot: <ws>/.workspace/agentflow/runBuild/<name>/<uuid>
23
+ * - legacyUserRoot: ~/agentflow/runBuild/<name>/<uuid>
24
+ * 后两者仅作为向下兼容读取;新写入只走 user/workspace。
25
+ */
26
+ export function listAllRunDirs(workspaceRoot) {
27
+ const root = path.resolve(workspaceRoot);
28
+ const out = [];
29
+ const seen = new Set();
30
+ const add = (flowName, uuid, runDir, source) => {
31
+ const key = `${flowName}\t${uuid}`;
32
+ if (seen.has(key)) return;
33
+ if (!fs.existsSync(runDir)) return;
34
+ seen.add(key);
35
+ out.push({ flowName, uuid, runDir, source });
36
+ };
37
+
38
+ const scanPipelinesDir = (pipelinesDir, source) => {
39
+ if (!fs.existsSync(pipelinesDir)) return;
40
+ let entries;
41
+ try {
42
+ entries = fs.readdirSync(pipelinesDir, { withFileTypes: true });
43
+ } catch {
44
+ return;
45
+ }
46
+ for (const e of entries) {
47
+ if (!e.isDirectory()) continue;
48
+ if (e.name === ARCHIVED_PIPELINES_DIR_NAME) continue;
49
+ const runBuildDir = path.join(pipelinesDir, e.name, "runBuild");
50
+ if (!fs.existsSync(runBuildDir)) continue;
51
+ let uuids;
52
+ try {
53
+ uuids = fs.readdirSync(runBuildDir, { withFileTypes: true });
54
+ } catch {
55
+ continue;
56
+ }
57
+ for (const u of uuids) {
58
+ if (!u.isDirectory()) continue;
59
+ add(e.name, u.name, path.join(runBuildDir, u.name), source);
60
+ }
61
+ }
62
+ };
63
+
64
+ // 新位置(优先)
65
+ scanPipelinesDir(getUserPipelinesRoot(), "user");
66
+ scanPipelinesDir(path.join(root, PIPELINES_DIR), "workspace");
67
+
68
+ // 旧位置(兼容读)
69
+ const scanLegacyRoot = (runBuildDir, source) => {
70
+ if (!fs.existsSync(runBuildDir)) return;
71
+ let flowEntries;
72
+ try {
73
+ flowEntries = fs.readdirSync(runBuildDir, { withFileTypes: true });
74
+ } catch {
75
+ return;
76
+ }
77
+ for (const fe of flowEntries) {
78
+ if (!fe.isDirectory()) continue;
79
+ const flowRunDir = path.join(runBuildDir, fe.name);
80
+ let uuids;
81
+ try {
82
+ uuids = fs.readdirSync(flowRunDir, { withFileTypes: true });
83
+ } catch {
84
+ continue;
85
+ }
86
+ for (const u of uuids) {
87
+ if (!u.isDirectory()) continue;
88
+ add(fe.name, u.name, path.join(flowRunDir, u.name), source);
89
+ }
90
+ }
91
+ };
92
+ scanLegacyRoot(getWorkspaceRunBuildRoot(root), "legacyWorkspaceRoot");
93
+ scanLegacyRoot(getLegacyUserRunBuildRoot(), "legacyUserRoot");
94
+
95
+ return out;
96
+ }
97
+
98
+ /**
99
+ * 确保用户数据目录下 reference 存在且含包内 reference 文件。
100
+ */
101
+ export function ensureReference(_workspaceRoot) {
102
+ const destDir = getReferenceRootAbs();
103
+ if (!fs.existsSync(PACKAGE_REFERENCE_DIR) || !fs.statSync(PACKAGE_REFERENCE_DIR).isDirectory()) return;
104
+ try {
105
+ fs.mkdirSync(destDir, { recursive: true });
106
+ const names = fs.readdirSync(PACKAGE_REFERENCE_DIR);
107
+ for (const name of names) {
108
+ const srcFile = path.join(PACKAGE_REFERENCE_DIR, name);
109
+ if (fs.statSync(srcFile).isFile()) {
110
+ const destFile = path.join(destDir, name);
111
+ if (!fs.existsSync(destFile)) fs.copyFileSync(srcFile, destFile);
112
+ }
113
+ }
114
+ } catch (_) {}
115
+ }
116
+
117
+ /** 列出所有存在 logs/log.txt 的 run,供 extract-thinking -list 使用。 */
118
+ export function listRunsWithLogs(workspaceRoot) {
119
+ const list = [];
120
+ for (const { flowName, uuid, runDir } of listAllRunDirs(workspaceRoot)) {
121
+ const logPath = path.join(runDir, "logs", "log.txt");
122
+ if (!fs.existsSync(logPath)) continue;
123
+ let size = 0;
124
+ let lines = 0;
125
+ try {
126
+ const stat = fs.statSync(logPath);
127
+ size = stat.size;
128
+ const c = fs.readFileSync(logPath, "utf8");
129
+ lines = c.split(/\r?\n/).length;
130
+ } catch (_) {}
131
+ list.push({ flowName, uuid, logPath, size, lines });
132
+ }
133
+ list.sort((a, b) => {
134
+ const fc = a.flowName.localeCompare(b.flowName);
135
+ if (fc !== 0) return fc;
136
+ return a.uuid.localeCompare(b.uuid);
137
+ });
138
+ return list;
139
+ }
140
+
141
+ /** 解析 flow 目录:~/agentflow/pipelines → .workspace/agentflow/pipelines → .cursor/agentflow/pipelines(旧)→ builtin/pipelines */
142
+ export function getFlowDir(workspaceRoot, flowName) {
143
+ const root = path.resolve(workspaceRoot);
144
+ const hasFlow = (dir) => fs.existsSync(dir) && fs.existsSync(path.join(dir, "flow.yaml"));
145
+
146
+ // user pipelines
147
+ const userRoot = getUserPipelinesRoot();
148
+ const userFlowDir = path.join(userRoot, flowName);
149
+ if (hasFlow(userFlowDir)) return userFlowDir;
150
+ // user archived
151
+ const userArchivedDir = path.join(userRoot, ARCHIVED_PIPELINES_DIR_NAME, flowName);
152
+ if (hasFlow(userArchivedDir)) return userArchivedDir;
153
+
154
+ // workspace pipelines
155
+ const wsFlowDir = path.join(root, PIPELINES_DIR, flowName);
156
+ if (hasFlow(wsFlowDir)) return wsFlowDir;
157
+ // workspace archived
158
+ const wsArchivedDir = path.join(root, PIPELINES_DIR, ARCHIVED_PIPELINES_DIR_NAME, flowName);
159
+ if (hasFlow(wsArchivedDir)) return wsArchivedDir;
160
+
161
+ // legacy
162
+ const legacyFlowDir = path.join(root, LEGACY_PIPELINES_DIR, flowName);
163
+ if (hasFlow(legacyFlowDir)) return legacyFlowDir;
164
+
165
+ // builtin
166
+ const builtinFlowDir = path.join(PACKAGE_BUILTIN_PIPELINES_DIR, flowName);
167
+ if (hasFlow(builtinFlowDir)) return builtinFlowDir;
168
+
169
+ return null;
170
+ }
171
+
172
+ /** 两参 replay 时根据 uuid 查找 run 目录,返回 flowName 或 null。 */
173
+ export function findFlowNameByUuid(workspaceRoot, uuid) {
174
+ for (const entry of listAllRunDirs(workspaceRoot)) {
175
+ if (entry.uuid !== uuid) continue;
176
+ const flowJsonPath = path.join(entry.runDir, "intermediate", "flow.json");
177
+ if (fs.existsSync(flowJsonPath)) return entry.flowName;
178
+ }
179
+ return null;
180
+ }
@@ -0,0 +1,179 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 执行占位符替换,组装最终 prompt 并写入 intermediate 文件。
4
+ * 用法:node build-node-prompt.mjs <workspaceRoot> <flowName> <uuid> <instanceId>
5
+ * 输出(stdout JSON):{ "ok": true, "promptPath": ".workspace/agentflow/runBuild/<flowName>/<uuid>/intermediate/<instanceId>.prompt.md" }
6
+ */
7
+
8
+ import fs from "fs";
9
+ import path from "path";
10
+ import { fileURLToPath } from "url";
11
+
12
+ import { getRunDir, PIPELINES_DIR } from "../lib/paths.mjs";
13
+ import { getFlowDir } from "../lib/workspace.mjs";
14
+ import { loadFlowDefinition } from "./parse-flow.mjs";
15
+ import { getResolvedValues, getOutputPathForSlot } from "./get-resolved-values.mjs";
16
+ import { loadExecId } from "./get-exec-id.mjs";
17
+ import { intermediatePromptBasename, intermediateDirForNode } from "./get-exec-id.mjs";
18
+
19
+ function shellQuote(s) {
20
+ if (s == null) return "''";
21
+ return "'" + String(s).replace(/'/g, "'\\''") + "'";
22
+ }
23
+
24
+ function resolvePlaceholder(k, resolvedInputs, resolvedOutputs, opts) {
25
+ const { instanceId, currentExecId, runDir } = opts;
26
+ const execId = currentExecId ?? 1;
27
+ const toAbs = (rel) => (runDir && rel ? path.join(runDir, rel) : rel);
28
+ if (k.startsWith("input.")) {
29
+ const slot = k.slice(6);
30
+ return resolvedInputs[slot] ?? resolvedInputs._ ?? "";
31
+ }
32
+ if (k.startsWith("output.")) {
33
+ const slot = k.slice(7);
34
+ const v = resolvedOutputs[slot] ?? resolvedOutputs._ ?? "";
35
+ if (v) return v;
36
+ if (instanceId && slot in resolvedOutputs) {
37
+ return toAbs(getOutputPathForSlot(instanceId, execId, slot));
38
+ }
39
+ return "";
40
+ }
41
+ let v = resolvedInputs[k] ?? resolvedOutputs[k] ?? "";
42
+ if (!v && !k.includes(".")) {
43
+ v = resolvedInputs[k + ".md"] ?? resolvedOutputs[k + ".md"] ?? "";
44
+ }
45
+ if (!v && instanceId && (k in resolvedOutputs || (k + ".md") in resolvedOutputs)) {
46
+ const slot = k in resolvedOutputs ? k : k + ".md";
47
+ v = toAbs(getOutputPathForSlot(instanceId, execId, slot));
48
+ }
49
+ return v;
50
+ }
51
+
52
+ function resolvePlaceholdersInText(
53
+ text,
54
+ resolvedInputs,
55
+ resolvedOutputs,
56
+ opts = {},
57
+ ) {
58
+ if (!text || typeof text !== "string") return "";
59
+ return text.replace(/\$\{([^}]+)\}/g, (_, key) => {
60
+ return resolvePlaceholder(key.trim(), resolvedInputs, resolvedOutputs, opts);
61
+ });
62
+ }
63
+
64
+ /**
65
+ * 解析 script 字段中的 ${} 占位符,每个替换值用 shell 单引号包裹,避免路径含空格/特殊字符时被 shell 误拆分。
66
+ */
67
+ function resolveScriptCommand(
68
+ text,
69
+ resolvedInputs,
70
+ resolvedOutputs,
71
+ opts = {},
72
+ ) {
73
+ if (!text || typeof text !== "string") return "";
74
+ return text.replace(/\$\{([^}]+)\}/g, (_, key) => {
75
+ return shellQuote(resolvePlaceholder(key.trim(), resolvedInputs, resolvedOutputs, opts));
76
+ });
77
+ }
78
+
79
+ /**
80
+ * 执行占位符替换,组装 prompt 并写入 intermediate 文件(文件名带 _execId)。
81
+ * @param {number} [execId] - 本轮 execId,缺省则从 memory 读取
82
+ * @returns {{ ok: boolean, promptPath?: string, nodeContext?: string, taskBody?: string, error?: string }}
83
+ */
84
+ export function buildNodePrompt(workspaceRoot, flowName, uuid, instanceId, execId) {
85
+ const runDir = getRunDir(workspaceRoot, flowName, uuid);
86
+ const flowJsonPath = path.join(runDir, "intermediate", "flow.json");
87
+ let flowDir = getFlowDir(workspaceRoot, flowName) || path.join(workspaceRoot, PIPELINES_DIR, flowName);
88
+ if (fs.existsSync(flowJsonPath)) {
89
+ try {
90
+ const flow = JSON.parse(fs.readFileSync(flowJsonPath, "utf-8"));
91
+ if (flow?.flowDir && typeof flow.flowDir === "string" && flow.flowDir.trim()) {
92
+ flowDir = path.isAbsolute(flow.flowDir) ? flow.flowDir : path.join(workspaceRoot, flow.flowDir);
93
+ }
94
+ } catch (_) {}
95
+ }
96
+ const nodeIntermediateDir = path.join(runDir, intermediateDirForNode(instanceId));
97
+ const e = execId ?? loadExecId(workspaceRoot, flowName, uuid, instanceId);
98
+ const promptBasename = intermediatePromptBasename(instanceId, e);
99
+ const promptPath = path.join(nodeIntermediateDir, promptBasename);
100
+
101
+ const data = getResolvedValues(workspaceRoot, flowName, uuid, instanceId);
102
+ if (!data.ok) {
103
+ return { ok: false, error: data.error || "get-resolved-values failed" };
104
+ }
105
+
106
+ const flowData = loadFlowDefinition(flowDir);
107
+ const inst = flowData?.instances?.[instanceId];
108
+ const instanceBody =
109
+ inst?.body != null
110
+ ? String(inst.body || "").trim()
111
+ : "";
112
+ const instanceScript =
113
+ inst?.script != null
114
+ ? String(inst.script || "").trim()
115
+ : "";
116
+
117
+ const { resolvedInputs = {}, resolvedOutputs = {}, systemPrompt = "" } = data;
118
+ const resolveOpts = { instanceId, currentExecId: e, runDir };
119
+ const taskBody = resolvePlaceholdersInText(
120
+ instanceBody,
121
+ resolvedInputs,
122
+ resolvedOutputs,
123
+ resolveOpts,
124
+ );
125
+
126
+ const resolvedScript = instanceScript
127
+ ? resolveScriptCommand(instanceScript, resolvedInputs, resolvedOutputs, resolveOpts)
128
+ : "";
129
+
130
+ const content = `## 节点上下文
131
+
132
+ ${systemPrompt || "(无)"}
133
+
134
+ ## 执行任务
135
+
136
+ ${taskBody || "(无)"}
137
+ `;
138
+
139
+ try {
140
+ fs.mkdirSync(nodeIntermediateDir, { recursive: true });
141
+ // 备份由 snapshotPriorRoundIfNeeded 在 pre-process 入口统一处理;
142
+ // 本函数可能在单轮内被多次调用(pre-process + computeCacheMd5),
143
+ // 但 round 内内容相同,直接覆盖写即可,不会产生伪历史备份。
144
+ fs.writeFileSync(promptPath, content, "utf-8");
145
+ } catch (e) {
146
+ return { ok: false, error: e.message || "Failed to write prompt file" };
147
+ }
148
+
149
+ const relativePath = path.relative(workspaceRoot, promptPath);
150
+ return {
151
+ ok: true,
152
+ promptPath: relativePath.replace(/\\/g, "/"),
153
+ nodeContext: systemPrompt || "",
154
+ taskBody: taskBody || "",
155
+ script: resolvedScript || "",
156
+ };
157
+ }
158
+
159
+ function main() {
160
+ const args = process.argv.slice(2);
161
+ if (args.length < 4) {
162
+ console.error(
163
+ JSON.stringify({
164
+ ok: false,
165
+ error: "Usage: node build-node-prompt.mjs <workspaceRoot> <flowName> <uuid> <instanceId>",
166
+ }),
167
+ );
168
+ process.exit(1);
169
+ }
170
+
171
+ const [root, flowName, uuid, instanceId] = args;
172
+ const workspaceRoot = path.resolve(root);
173
+ const result = buildNodePrompt(workspaceRoot, flowName, uuid, instanceId, undefined);
174
+ console.log(JSON.stringify(result));
175
+ if (!result.ok) process.exit(1);
176
+ }
177
+
178
+ const isMain = process.argv[1] && path.resolve(fileURLToPath(import.meta.url)) === path.resolve(process.argv[1]);
179
+ if (isMain) main();