@fieldwangai/agentflow 0.1.26 → 0.1.28

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/bin/lib/apply.mjs CHANGED
@@ -32,6 +32,18 @@ const PARALLEL_PREFIX_COLORS = [
32
32
  (s) => chalk.blue(s),
33
33
  ];
34
34
 
35
+ function readExistingResultBranch(workspaceRoot, flowName, uuid, instanceId) {
36
+ const resultPath = path.join(getRunDir(workspaceRoot, flowName, uuid), "intermediate", instanceId, `${instanceId}.result.md`);
37
+ if (!fs.existsSync(resultPath)) return null;
38
+ try {
39
+ const raw = fs.readFileSync(resultPath, "utf-8");
40
+ const m = raw.match(/^\s*branch:\s*["']?([^"'\s]+)["']?/m);
41
+ return m ? m[1] : null;
42
+ } catch {
43
+ return null;
44
+ }
45
+ }
46
+
35
47
  /** parallel 默认 false */
36
48
  export async function apply(workspaceRoot, flowName, uuidArg, dryRun, agentModel = null, force = true, parallel = false, cliInputs = {}) {
37
49
  ensureReference(workspaceRoot);
@@ -764,8 +776,13 @@ export async function resume(workspaceRoot, flowName, uuid, instanceIdOptional,
764
776
  const failedNodes = Object.keys(instanceStatus).filter((id) => instanceStatus[id] === "failed");
765
777
  nodesToResume = [...new Set([...pendingNodes, ...failedNodes])];
766
778
  }
767
- const payload = JSON.stringify({ status: "success", message: t("apply.user_confirmed") });
768
779
  for (const instanceId of nodesToResume) {
780
+ const existingBranch = readExistingResultBranch(workspaceRoot, flowName, uuid, instanceId);
781
+ const payload = JSON.stringify({
782
+ status: "success",
783
+ message: t("apply.user_confirmed"),
784
+ ...(existingBranch ? { branch: existingBranch } : {}),
785
+ });
769
786
  const wr = runNodeScript(
770
787
  workspaceRoot,
771
788
  "write-result.mjs",
@@ -16,6 +16,7 @@ import {
16
16
  getUserPipelinesRoot,
17
17
  } from "./paths.mjs";
18
18
  import { Table } from "./table.mjs";
19
+ import { listMarketplaceNodes, parseMarketplaceDefinitionId, resolveMarketplaceNodePackage } from "./marketplace.mjs";
19
20
 
20
21
  /** 从指定目录收集含 flow.yaml 的子目录名。 */
21
22
  export function collectPipelineNamesFromDir(dirPath) {
@@ -201,6 +202,16 @@ export function listNodesJson(workspaceRoot, flowId, flowSource, opts = {}) {
201
202
  const archived = Boolean(opts.archived);
202
203
  const byId = new Map();
203
204
  const pipelineTranslations = {};
205
+ let marketplaceFlowData = null;
206
+ if (flowId && flowSource) {
207
+ const flowPath = getFlowYamlAbs(workspaceRoot, flowId, flowSource, opts);
208
+ if (flowPath.path && fs.existsSync(flowPath.path)) {
209
+ try {
210
+ const parsed = yaml.load(fs.readFileSync(flowPath.path, "utf-8"));
211
+ if (parsed && typeof parsed === "object") marketplaceFlowData = parsed;
212
+ } catch (_) {}
213
+ }
214
+ }
204
215
  const addFromDir = (dir, source, flowIdOpt) => {
205
216
  if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) return;
206
217
  const files = fs.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".md"));
@@ -235,6 +246,26 @@ export function listNodesJson(workspaceRoot, flowId, flowSource, opts = {}) {
235
246
  addFromDir(PACKAGE_BUILTIN_NODES_DIR, "project");
236
247
  addFromDir(path.join(root, LEGACY_NODES_DIR), "project");
237
248
  addFromDir(path.join(root, PROJECT_NODES_DIR), "project");
249
+ for (const manifest of listMarketplaceNodes(root, marketplaceFlowData)) {
250
+ let type = "agent";
251
+ const runtimeType = String(manifest.runtime?.type || manifest.type || "").toLowerCase();
252
+ if (runtimeType.startsWith("control")) type = "control";
253
+ else if (runtimeType.startsWith("provide")) type = "provide";
254
+ byId.set(manifest.definitionId, {
255
+ id: manifest.definitionId,
256
+ packageId: manifest.id,
257
+ version: manifest.version,
258
+ type,
259
+ label: manifest.displayName,
260
+ displayName: manifest.displayName,
261
+ description: manifest.description,
262
+ inputs: manifest.input,
263
+ outputs: manifest.output,
264
+ source: manifest.source || "marketplace",
265
+ packageDir: manifest.packageDir,
266
+ runtime: manifest.runtime,
267
+ });
268
+ }
238
269
  if (flowId && flowSource) {
239
270
  if (flowSource === "builtin") {
240
271
  addFromDir(path.join(PACKAGE_BUILTIN_PIPELINES_DIR, flowId, "nodes"), "flow", flowId);
@@ -443,6 +474,40 @@ export function getFlowYamlAbs(workspaceRoot, flowId, flowSource, options = {})
443
474
  export function readNodeJson(workspaceRoot, nodeId, flowId, flowSource, opts = {}) {
444
475
  const root = path.resolve(workspaceRoot);
445
476
  const archived = Boolean(opts.archived);
477
+ const marketSpec = parseMarketplaceDefinitionId(nodeId);
478
+ if (marketSpec) {
479
+ let flowDir = root;
480
+ if (flowId && flowSource) {
481
+ const flowPath = getFlowYamlAbs(workspaceRoot, flowId, flowSource, opts);
482
+ if (flowPath.path) flowDir = path.dirname(flowPath.path);
483
+ if (flowPath.path && fs.existsSync(flowPath.path)) {
484
+ try {
485
+ const parsed = yaml.load(fs.readFileSync(flowPath.path, "utf-8"));
486
+ if (parsed && typeof parsed === "object") opts.flowData = parsed;
487
+ } catch (_) {}
488
+ }
489
+ }
490
+ const resolved = resolveMarketplaceNodePackage(root, flowDir, nodeId, opts.flowData || null);
491
+ if (!resolved) return { error: "Node not found: " + nodeId };
492
+ const readmePath = path.join(resolved.packageDir, "README.md");
493
+ let type = "agent";
494
+ const runtimeType = String(resolved.runtime?.type || resolved.type || "").toLowerCase();
495
+ if (runtimeType.startsWith("control")) type = "control";
496
+ else if (runtimeType.startsWith("provide")) type = "provide";
497
+ return {
498
+ type,
499
+ label: resolved.displayName,
500
+ displayName: resolved.displayName,
501
+ inputs: resolved.input,
502
+ outputs: resolved.output,
503
+ executionLogic: fs.existsSync(readmePath) ? fs.readFileSync(readmePath, "utf-8").trim() : undefined,
504
+ description: resolved.description,
505
+ packageId: resolved.id,
506
+ version: resolved.version,
507
+ packageDir: resolved.packageDir,
508
+ runtime: resolved.runtime,
509
+ };
510
+ }
446
511
  const fileName = nodeId.endsWith(".md") ? nodeId : `${nodeId}.md`;
447
512
  const pathsToTry = [];
448
513
  if (flowId && flowSource) {
@@ -13,6 +13,7 @@
13
13
  */
14
14
  import fs from "fs";
15
15
  import path from "path";
16
+ import yaml from "js-yaml";
16
17
 
17
18
  // ─── 意图模式定义 ─────────────────────────────────────────────────────────
18
19
 
@@ -124,6 +125,112 @@ function readFileCached(absPath) {
124
125
  }
125
126
  }
126
127
 
128
+ function parseSkillFile(absPath) {
129
+ const content = readFileCached(absPath);
130
+ if (!content) return null;
131
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n?/);
132
+ let meta = {};
133
+ if (fmMatch) {
134
+ try {
135
+ meta = yaml.load(fmMatch[1]) || {};
136
+ } catch {
137
+ meta = {};
138
+ }
139
+ }
140
+ const dirName = path.basename(path.dirname(absPath));
141
+ const name = String(meta.name || dirName).trim();
142
+ if (!name) return null;
143
+ const description = String(meta.description || "").trim();
144
+ return {
145
+ name,
146
+ description,
147
+ content,
148
+ body: stripFrontmatter(content),
149
+ absPath,
150
+ };
151
+ }
152
+
153
+ function listSkillDirs(rootDir) {
154
+ try {
155
+ return fs.readdirSync(rootDir, { withFileTypes: true })
156
+ .filter((e) => e.isDirectory())
157
+ .map((e) => path.join(rootDir, e.name, "SKILL.md"))
158
+ .filter((p) => fs.existsSync(p));
159
+ } catch {
160
+ return [];
161
+ }
162
+ }
163
+
164
+ function skillSources(packageRoot, workspaceRoot) {
165
+ const sources = [
166
+ { source: "builtin", label: "AgentFlow", dir: path.join(packageRoot, "skills") },
167
+ ];
168
+ if (workspaceRoot) {
169
+ sources.push(
170
+ { source: "workspace-agents", label: ".agents", dir: path.join(workspaceRoot, ".agents", "skills") },
171
+ { source: "workspace-cursor", label: ".cursor", dir: path.join(workspaceRoot, ".cursor", "skills") },
172
+ );
173
+ }
174
+ return sources;
175
+ }
176
+
177
+ export function listComposerSkills(packageRoot, workspaceRoot) {
178
+ const out = [];
179
+ const seenKeys = new Set();
180
+ for (const src of skillSources(packageRoot, workspaceRoot)) {
181
+ for (const skillPath of listSkillDirs(src.dir)) {
182
+ const skill = parseSkillFile(skillPath);
183
+ if (!skill) continue;
184
+ const key = `${src.source}:${skill.name}`;
185
+ if (seenKeys.has(key)) continue;
186
+ seenKeys.add(key);
187
+ out.push({
188
+ key,
189
+ id: skill.name,
190
+ name: skill.name,
191
+ description: skill.description,
192
+ source: src.source,
193
+ sourceLabel: src.label,
194
+ path: skill.absPath,
195
+ });
196
+ }
197
+ }
198
+ return out.sort((a, b) => {
199
+ const bySource = a.sourceLabel.localeCompare(b.sourceLabel);
200
+ if (bySource !== 0) return bySource;
201
+ return a.name.localeCompare(b.name);
202
+ });
203
+ }
204
+
205
+ export function loadResourcesForSkillKeys(skillKeys, packageRoot, workspaceRoot) {
206
+ if (!Array.isArray(skillKeys) || skillKeys.length === 0) {
207
+ return { skills: [], references: [], skillsHint: "", hasContext: false };
208
+ }
209
+ const wanted = new Set(skillKeys.map((x) => String(x || "").trim()).filter(Boolean));
210
+ if (wanted.size === 0) return { skills: [], references: [], skillsHint: "", hasContext: false };
211
+
212
+ const skills = [];
213
+ for (const item of listComposerSkills(packageRoot, workspaceRoot)) {
214
+ if (!wanted.has(item.key) && !wanted.has(item.name)) continue;
215
+ const parsed = parseSkillFile(item.path);
216
+ if (!parsed) continue;
217
+ skills.push({
218
+ id: item.name,
219
+ content: parsed.body,
220
+ absPath: item.path,
221
+ source: item.source,
222
+ sourceLabel: item.sourceLabel,
223
+ });
224
+ }
225
+
226
+ return {
227
+ skills,
228
+ references: [],
229
+ skillsHint: buildSelectedSkillsHint(skills),
230
+ hasContext: skills.length > 0,
231
+ };
232
+ }
233
+
127
234
  // ─── 意图检测 ─────────────────────────────────────────────────────────────
128
235
 
129
236
  /**
@@ -307,6 +414,16 @@ function buildSkillsHint(intents, skills, references) {
307
414
  return lines.join("\n");
308
415
  }
309
416
 
417
+ function buildSelectedSkillsHint(skills) {
418
+ if (!Array.isArray(skills) || skills.length === 0) return "";
419
+ const lines = ["## 用户选择的 skills"];
420
+ for (const s of skills) {
421
+ lines.push(`- 使用 skill \`${s.id}\`:${s.absPath}`);
422
+ }
423
+ lines.push("如任务与所选 skill 匹配,请先读取对应 SKILL.md 并遵循其说明;如果只是问答,按问题直接回答。");
424
+ return lines.join("\n");
425
+ }
426
+
310
427
  // ─── 辅助 ─────────────────────────────────────────────────────────────────
311
428
 
312
429
  function stripFrontmatter(content) {
package/bin/lib/help.mjs CHANGED
@@ -20,6 +20,10 @@ AgentFlow CLI — 使用 Cursor / OpenCode / Claude Code CLI 流式输出驱动
20
20
  agentflow ui [--host <addr>] [--port <n>] [--scheduler] [--no-open] 本地 HTTP:流水线列表 + React Flow 节点流程图编辑保存(默认 127.0.0.1:8765;可用 AGENTFLOW_UI_HOST)
21
21
  agentflow scheduler start [--poll-ms <ms>] 启动定时执行调度器(读取各流水线 schedule.json)
22
22
  agentflow scheduler status [--json] 查看定时执行配置与状态
23
+ agentflow scheduler cancel <FlowName> <uuid> 取消某次等待中的 watch/run
24
+ agentflow marketplace list [--json] 查看 workspace 本地节点市场
25
+ agentflow marketplace publish-node <dir> 发布本地节点包到 workspace market
26
+ agentflow marketplace install-node <FlowName> <nodeSpec> 将 market 节点依赖写入 flow
23
27
  agentflow apply <FlowName> [uuid] 或 agentflow apply <uuid>(由 uuid 反查 pipeline)
24
28
  agentflow validate <FlowName> [uuid] 校验流程;终端下输出易读结果,--json 或管道时输出 JSON;传 uuid 时写入 runDir/intermediate/validation.json
25
29
  agentflow resume <FlowName> <uuid> [instanceId] 将 pending 与 failed 节点标为已确认并继续 apply
@@ -85,6 +89,10 @@ Usage:
85
89
  agentflow ui [--host <addr>] [--port <n>] [--scheduler] [--no-open] Local HTTP: pipeline list + React Flow node diagram editor (default 127.0.0.1:8765; AGENTFLOW_UI_HOST supported)
86
90
  agentflow scheduler start [--poll-ms <ms>] Start the scheduled-run scheduler (reads each pipeline schedule.json)
87
91
  agentflow scheduler status [--json] Show scheduled-run configuration and state
92
+ agentflow scheduler cancel <FlowName> <uuid> Cancel a waiting watch/run
93
+ agentflow marketplace list [--json] Show workspace local marketplace packages
94
+ agentflow marketplace publish-node <dir> Publish a local node package to the workspace market
95
+ agentflow marketplace install-node <FlowName> <nodeSpec> Add a marketplace node dependency to a flow
88
96
  agentflow apply <FlowName> [uuid] Or agentflow apply <uuid> (resolve pipeline from uuid)
89
97
  agentflow validate <FlowName> [uuid] Validate flow; readable output in terminal, JSON with --json or pipe; writes to runDir/intermediate/validation.json when uuid provided
90
98
  agentflow resume <FlowName> <uuid> [instanceId] Mark pending and failed nodes as acknowledged and continue apply
@@ -241,6 +241,26 @@
241
241
  "displayName": "To Bool",
242
242
  "description": "Execute script to produce true/false prediction. Like tool_nodejs but enforces bool output. Extensible inputs."
243
243
  },
244
+ "control_delay": {
245
+ "displayName": "Delay",
246
+ "description": "Persistently wait for a relative duration, then scheduler resumes this run."
247
+ },
248
+ "control_wait_until": {
249
+ "displayName": "Wait Until",
250
+ "description": "Persistently wait until an absolute time, then scheduler resumes this run."
251
+ },
252
+ "control_deadline": {
253
+ "displayName": "Deadline",
254
+ "description": "Check whether the deadline has expired and output expired as bool."
255
+ },
256
+ "control_cancelled": {
257
+ "displayName": "Cancelled",
258
+ "description": "Check whether the current run/watch has been cancelled and output cancelled as bool."
259
+ },
260
+ "control_interval_loop": {
261
+ "displayName": "Interval Loop",
262
+ "description": "Persistently wait by interval and branch to continue, done, timeout, or cancelled based on done, cancelled, and deadline inputs."
263
+ },
244
264
  "control_agent_toBool": {
245
265
  "displayName": "Agent ToBool",
246
266
  "description": "AI-powered boolean judgment for non-deterministic scenarios"
@@ -341,4 +361,4 @@
341
361
  "control_toBool_check": { "label": "Check Result" }
342
362
  }
343
363
  }
344
- }
364
+ }
@@ -241,6 +241,26 @@
241
241
  "displayName": "转布尔",
242
242
  "description": "执行 script 脚本输出 true/false 到 prediction,类似 tool_nodejs 但强制 bool 输出,可扩展输入"
243
243
  },
244
+ "control_delay": {
245
+ "displayName": "延迟等待",
246
+ "description": "持久化等待一段时间,到点后由 scheduler 唤醒当前 run 继续执行"
247
+ },
248
+ "control_wait_until": {
249
+ "displayName": "等待到时间",
250
+ "description": "持久化等待到指定时间,到点后由 scheduler 唤醒当前 run 继续执行"
251
+ },
252
+ "control_deadline": {
253
+ "displayName": "截止判断",
254
+ "description": "判断当前时间是否已超过截止时间,输出 expired 布尔值"
255
+ },
256
+ "control_cancelled": {
257
+ "displayName": "取消判断",
258
+ "description": "判断当前 run/watch 是否已被取消,输出 cancelled 布尔值"
259
+ },
260
+ "control_interval_loop": {
261
+ "displayName": "间隔循环",
262
+ "description": "按固定间隔持久化等待,并根据 done、cancelled、deadline 分支到继续、完成、超时或取消"
263
+ },
244
264
  "control_agent_toBool": {
245
265
  "displayName": "AI 转布尔",
246
266
  "description": "由 AI 判断输入内容的布尔含义,适用于不确定性场景"
@@ -341,4 +361,4 @@
341
361
  "control_toBool_check": { "label": "检查结果" }
342
362
  }
343
363
  }
344
- }
364
+ }
package/bin/lib/main.mjs CHANGED
@@ -30,7 +30,8 @@ import { startUiServer } from "./ui-server.mjs";
30
30
  import { hubLogin, hubLogout } from "./hub-login.mjs";
31
31
  import { hubPublish } from "./hub-publish.mjs";
32
32
  import { hubListRemote, hubDownload } from "./hub-remote.mjs";
33
- import { listScheduleStatuses, startScheduler } from "./scheduler.mjs";
33
+ import { cancelScheduledRun, listScheduleStatuses, startScheduler } from "./scheduler.mjs";
34
+ import { installFlowDependency, listMarketplacePackages, publishNodePackage } from "./marketplace.mjs";
34
35
 
35
36
  async function readStdin() {
36
37
  const chunks = [];
@@ -210,6 +211,43 @@ export async function main() {
210
211
  process.stdout.write(JSON.stringify(result) + "\n");
211
212
  process.exit(result.error ? 1 : 0);
212
213
  }
214
+ if (sub === "marketplace") {
215
+ const action = shift();
216
+ if (action === "list") {
217
+ const result = listMarketplacePackages(workspaceRoot);
218
+ if (jsonMode) {
219
+ process.stdout.write(JSON.stringify(result) + "\n");
220
+ } else {
221
+ const table = new Table({ head: ["type", "id", "version", "name", "path"], style: { head: [] } });
222
+ for (const n of result.nodes) table.push(["node", n.id, n.version, n.displayName || "", n.packageDir]);
223
+ for (const c of result.collections) table.push(["collection", c.id, c.version, c.displayName || "", c.packageDir]);
224
+ process.stdout.write(table.toString() + "\n");
225
+ }
226
+ process.exit(0);
227
+ }
228
+ if (action === "publish-node") {
229
+ const sourceDir = shift();
230
+ if (!sourceDir) throw new Error("Usage: agentflow marketplace publish-node <packageDir> [--json]");
231
+ const result = publishNodePackage(workspaceRoot, sourceDir);
232
+ if (jsonMode) process.stdout.write(JSON.stringify(result) + "\n");
233
+ else if (result.ok) process.stdout.write(`Published node ${result.id}@${result.version}: ${result.definitionId}\n`);
234
+ else throw new Error(result.error || "publish-node failed");
235
+ process.exit(result.ok ? 0 : 1);
236
+ }
237
+ if (action === "install-node") {
238
+ const flowId = shift();
239
+ const spec = shift();
240
+ if (!flowId || !spec) throw new Error("Usage: agentflow marketplace install-node <flow> <nodeSpec> [--json]");
241
+ const flowDir = getFlowDir(workspaceRoot, flowId);
242
+ if (!flowDir) throw new Error(`Flow not found: ${flowId}`);
243
+ const result = installFlowDependency(workspaceRoot, flowDir, spec);
244
+ if (jsonMode) process.stdout.write(JSON.stringify(result) + "\n");
245
+ else if (result.ok) process.stdout.write(`Installed ${result.definitionId} into ${flowId}\n`);
246
+ else throw new Error(result.error || "install-node failed");
247
+ process.exit(result.ok ? 0 : 1);
248
+ }
249
+ throw new Error("Usage: agentflow marketplace <list|publish-node|install-node> [--json]");
250
+ }
213
251
  if (sub === "copy-builtin" && jsonMode) {
214
252
  const flowId = shift();
215
253
  let targetFlowId;
@@ -382,7 +420,7 @@ export async function main() {
382
420
  process.exit(0);
383
421
  }
384
422
  const rows = listScheduleStatuses(workspaceRoot);
385
- const table = new Table({ head: ["flow", "source", "enabled", "cron", "timezone", "next", "running", "lastRun", "error"], style: { head: [] } });
423
+ const table = new Table({ head: ["flow", "source", "enabled", "cron", "timezone", "next", "running", "waiting", "lastRun", "error"], style: { head: [] } });
386
424
  for (const r of rows) {
387
425
  table.push([
388
426
  r.flowId,
@@ -392,6 +430,7 @@ export async function main() {
392
430
  r.timezone || "",
393
431
  r.nextRunAt || "",
394
432
  r.running ? "yes" : "no",
433
+ String(r.waiting || 0),
395
434
  r.lastRunUuid || "",
396
435
  r.lastError || "",
397
436
  ]);
@@ -399,7 +438,21 @@ export async function main() {
399
438
  process.stdout.write(table.toString() + "\n");
400
439
  process.exit(0);
401
440
  }
402
- throw new Error("Usage: agentflow scheduler <start|status> [--once] [--poll-ms <ms>] [--json]");
441
+ if (action === "cancel") {
442
+ const flowId = shift();
443
+ const uuid = shift();
444
+ if (!flowId || !uuid) throw new Error("Usage: agentflow scheduler cancel <flow> <uuid> [--json]");
445
+ const result = cancelScheduledRun(workspaceRoot, flowId, uuid);
446
+ if (jsonMode) {
447
+ process.stdout.write(JSON.stringify(result) + "\n");
448
+ } else if (result.ok) {
449
+ process.stdout.write(`Cancelled ${flowId}/${uuid}; updated waits: ${result.updatedWaits}\n`);
450
+ } else {
451
+ throw new Error(result.error || "cancel failed");
452
+ }
453
+ process.exit(result.ok ? 0 : 1);
454
+ }
455
+ throw new Error("Usage: agentflow scheduler <start|status|cancel> [--once] [--poll-ms <ms>] [--json]");
403
456
  }
404
457
  // ──── Hub commands ────
405
458
  if (sub === "login") {