@fieldwangai/agentflow 0.1.25 → 0.1.27

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) {
@@ -89,49 +89,6 @@ export function buildScriptContentBlockForInstances(flowYamlAbs, instanceIds) {
89
89
  }
90
90
  }
91
91
 
92
- /**
93
- * 为 query 意图构建选中节点的完整上下文块(YAML excerpt + 脚本内容)。
94
- * 供单步轻量 prompt 使用,不注入编辑规则。
95
- */
96
- export function buildQueryContextBlock(flowYamlAbs, instanceIds) {
97
- if (!flowYamlAbs || !instanceIds?.length) return "";
98
- try {
99
- const flowDir = path.dirname(flowYamlAbs);
100
- const scriptsDirAbs = path.join(flowDir, "scripts");
101
- const flowRaw = fs.readFileSync(flowYamlAbs, "utf-8");
102
- const flowDoc = yaml.load(flowRaw);
103
- const instances = flowDoc?.instances || {};
104
- const parts = [];
105
- let totalBytes = 0;
106
- for (const id of instanceIds) {
107
- const inst = instances[id];
108
- if (!inst) continue;
109
- // YAML excerpt
110
- const instYaml = yaml.dump({ [id]: inst }, { lineWidth: -1 });
111
- if (totalBytes + instYaml.length > MAX_SCRIPT_INJECT_BYTES) break;
112
- totalBytes += instYaml.length;
113
- parts.push(`### 节点 \`${id}\`(${inst.definitionId || "unknown"})\n\`\`\`yaml\n${instYaml.trimEnd()}\n\`\`\``);
114
-
115
- // Script content for tool_nodejs
116
- if (inst.definitionId === "tool_nodejs" && inst.script && fs.existsSync(scriptsDirAbs)) {
117
- const filenames = extractScriptFilenames(String(inst.script));
118
- for (const fn of filenames) {
119
- try {
120
- const content = fs.readFileSync(path.join(scriptsDirAbs, fn), "utf-8");
121
- if (totalBytes + content.length > MAX_SCRIPT_INJECT_BYTES) break;
122
- totalBytes += content.length;
123
- parts.push(`### 脚本 \`${fn}\`\n\`\`\`javascript\n${content.trimEnd()}\n\`\`\``);
124
- } catch { /* skip */ }
125
- }
126
- }
127
- }
128
- if (!parts.length) return "";
129
- return `## 选中节点上下文\n\n${parts.join("\n\n")}`;
130
- } catch {
131
- return "";
132
- }
133
- }
134
-
135
92
  // ─── 单步模式(向后兼容) ──────────────────────────────────────────────────
136
93
 
137
94
  /**
@@ -206,8 +163,6 @@ function extractInstanceYamlExcerpt(flowYamlAbs, instanceId) {
206
163
 
207
164
  function buildAgentStepPrompt(step, flowContext) {
208
165
  const parts = [];
209
- const intentCategory = flowContext?.intentCategory || "generic";
210
- const isQuery = intentCategory === "query";
211
166
 
212
167
  const nodeRole = step.nodeRole != null ? String(step.nodeRole).trim() : "";
213
168
  if (nodeRole) {
@@ -215,8 +170,7 @@ function buildAgentStepPrompt(step, flowContext) {
215
170
  parts.push("");
216
171
  }
217
172
 
218
- // 编辑上下文(query 模式跳过编辑规则/skill/sync)
219
- if (flowContext && !isQuery) {
173
+ if (flowContext) {
220
174
  parts.push(t("composer.edit_context"));
221
175
  if (flowContext.flowYamlAbs) {
222
176
  parts.push(`- 图定义文件:${flowContext.flowYamlAbs}`);
@@ -243,16 +197,12 @@ function buildAgentStepPrompt(step, flowContext) {
243
197
  parts.push(flowContext.skillInjectionBlock);
244
198
  parts.push("");
245
199
  }
246
- } else if (flowContext && isQuery) {
247
- parts.push("## AgentFlow 问答上下文");
248
- if (flowContext.flowYamlAbs) parts.push(`- 图定义文件:${flowContext.flowYamlAbs}`);
249
- parts.push("");
250
200
  }
251
201
 
252
202
  const sid = step.instanceId != null ? String(step.instanceId).trim() : "";
253
203
  const instMap = flowContext?._instanceMap;
254
204
  const targetInst = sid && instMap && instMap[sid];
255
- if (!isQuery && targetInst && targetInst.definitionId === "tool_nodejs") {
205
+ if (targetInst && targetInst.definitionId === "tool_nodejs") {
256
206
  parts.push(t("composer.tool_nodejs_rules_title"));
257
207
  parts.push(t("composer.tool_nodejs_rules_body"));
258
208
  parts.push("");
@@ -263,21 +213,19 @@ function buildAgentStepPrompt(step, flowContext) {
263
213
  parts.push("");
264
214
 
265
215
  // 节点 schema 与目标 instance 上下文
266
- // query 模式:只注入 instance excerpt + script,跳过 schema
267
216
  try {
268
- if (!isQuery) {
269
- const targetIsExtensible = targetInst && EXTENSIBLE_DEFINITIONS.has(targetInst.definitionId);
270
- const promptText = String(step.prompt || step.description || "");
271
- const promptMentionsSlots = /input\s*:|output\s*:|追加|扩展槽|business\s*slot|业务槽/i.test(promptText);
272
- const useFullSchema = Boolean(targetIsExtensible || promptMentionsSlots);
273
- const schemaSection = useFullSchema
274
- ? buildNodeSchemaPromptSection()
275
- : buildNodeSchemaCompactSection();
276
- if (schemaSection) {
277
- parts.push(schemaSection);
278
- parts.push("");
279
- }
217
+ const targetIsExtensible = targetInst && EXTENSIBLE_DEFINITIONS.has(targetInst.definitionId);
218
+ const promptText = String(step.prompt || step.description || "");
219
+ const promptMentionsSlots = /input\s*:|output\s*:|追加|扩展槽|business\s*slot|业务槽/i.test(promptText);
220
+ const useFullSchema = Boolean(targetIsExtensible || promptMentionsSlots);
221
+ const schemaSection = useFullSchema
222
+ ? buildNodeSchemaPromptSection()
223
+ : buildNodeSchemaCompactSection();
224
+ if (schemaSection) {
225
+ parts.push(schemaSection);
226
+ parts.push("");
280
227
  }
228
+
281
229
  // Inject YAML excerpt + script content for target instance (or canvas fallback)
282
230
  const idsToInject = sid ? [sid] : (flowContext?.canvasInstanceIds || []);
283
231
  if (idsToInject.length > 0 && flowContext?.flowYamlAbs) {
@@ -304,30 +252,18 @@ function buildAgentStepPrompt(step, flowContext) {
304
252
  }
305
253
  }
306
254
  }
307
- if (isQuery) {
308
- parts.push(
309
- "## 上下文已就绪\n" +
310
- "- 节点 YAML 与脚本内容已附上。请基于上方信息回答用户的问题,**不要修改任何文件**。\n" +
311
- "- 如需查看整份 flow.yaml 可读取一次。"
312
- );
313
- } else {
314
- parts.push(
315
- "## 上下文已就绪(禁止 forage)\n" +
316
- "- 节点定义见上方 schema 表,**禁止** Glob/Read `builtin/nodes/`、`.workspace/agentflow/nodes/`、历史 `runBuild/` 来推断节点结构。\n" +
317
- "- 目标 instance 的当前 YAML 已附上(若 instanceId 已知);tool_nodejs 节点引用的 .mjs 脚本内容也已附上(若存在)。\n" +
318
- "- 如需查看整份 flow,仅在确实需要时读取一次。"
319
- );
320
- }
255
+ parts.push(
256
+ "## 上下文已就绪(禁止 forage)\n" +
257
+ "- 节点定义见上方 schema 表,**禁止** Glob/Read `builtin/nodes/`、`.workspace/agentflow/nodes/`、历史 `runBuild/` 来推断节点结构。\n" +
258
+ "- 目标 instance 的当前 YAML 已附上(若 instanceId 已知);tool_nodejs 节点引用的 .mjs 脚本内容也已附上(若存在)。\n" +
259
+ "- 如需查看整份 flow,仅在确实需要时读取一次。"
260
+ );
321
261
  parts.push("");
322
262
  } catch {
323
263
  /* schema 注入失败不影响主流程 */
324
264
  }
325
265
 
326
- if (isQuery) {
327
- parts.push("请基于上方注入的节点 YAML 与脚本内容回答用户的问题。**不要修改任何文件。**");
328
- } else {
329
- parts.push(t("composer.task_instruction"));
330
- }
266
+ parts.push(t("composer.task_instruction"));
331
267
  return parts.join("\n");
332
268
  }
333
269
 
@@ -149,24 +149,6 @@ export function detectIntents(userPrompt) {
149
149
  return [...new Set(matched)];
150
150
  }
151
151
 
152
- /**
153
- * 将意图列表归类为注入策略类别。
154
- * @param {string[]} intents
155
- * @returns {"query" | "edit-node" | "add-node" | "add-flow" | "edit-flow" | "generic"}
156
- */
157
- export function classifyIntentCategory(intents) {
158
- if (!intents || intents.length === 0) return "generic";
159
- // 纯 query(无任何编辑意图混入)
160
- const editIntents = intents.filter(i => i !== "query-explain");
161
- if (editIntents.length === 0) return "query";
162
- // 混合 query + 编辑 → 按编辑侧分类
163
- if (editIntents.includes("create-flow")) return "add-flow";
164
- if (editIntents.includes("optimize-flow")) return "edit-flow";
165
- if (editIntents.includes("add-instances")) return "add-node";
166
- if (editIntents.includes("edit-fields") || editIntents.includes("optimize-nodes")) return "edit-node";
167
- return "generic";
168
- }
169
-
170
152
  // ─── 加载 skill 和 reference 内容 ─────────────────────────────────────────
171
153
 
172
154
  /**
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") {