@fieldwangai/agentflow 0.1.31 → 0.1.33

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.
@@ -74,7 +74,16 @@ import {
74
74
  import { runNodeScript } from "./pipeline-scripts.mjs";
75
75
  import { readFlowSchedule, writeFlowSchedule } from "./schedule-config.mjs";
76
76
  import { listScheduleStatuses } from "./scheduler.mjs";
77
- import { installFlowDependency, listMarketplacePackages, publishNodeFromInstance } from "./marketplace.mjs";
77
+ import {
78
+ deleteMarketplaceNodePackage,
79
+ installFlowDependency,
80
+ listMarketplaceFlowSnippets,
81
+ listMarketplacePackages,
82
+ publishFlowSnippet,
83
+ publishNodeFromInstance,
84
+ } from "./marketplace.mjs";
85
+ import { buildGitContext, loadGitWorktree, normalizeGitContext, runGit, unloadGitWorktree } from "./git-worktree.mjs";
86
+ import { createGitLabMergeRequest } from "./gitlab-mr.mjs";
78
87
  import {
79
88
  authSetupRequired,
80
89
  buildClearSessionCookie,
@@ -439,6 +448,7 @@ function readBody(req) {
439
448
  const WORKSPACE_FILE_SKIP_DIRS = new Set([
440
449
  ".git",
441
450
  "node_modules",
451
+ "runBuild",
442
452
  ".next",
443
453
  ".nuxt",
444
454
  ".turbo",
@@ -447,6 +457,11 @@ const WORKSPACE_FILE_SKIP_DIRS = new Set([
447
457
  "coverage",
448
458
  ]);
449
459
 
460
+ const WORKSPACE_FILE_SKIP_FILES = new Set([
461
+ "flow.yaml",
462
+ "workspace.graph.json",
463
+ ]);
464
+
450
465
  const WORKSPACE_TEXT_EXTS = new Set([
451
466
  ".md",
452
467
  ".markdown",
@@ -510,6 +525,7 @@ function readWorkspaceFilesRecursive(dir, root, depth = 0, maxDepth = 3, budget
510
525
  children: readWorkspaceFilesRecursive(abs, root, depth + 1, maxDepth, budget),
511
526
  });
512
527
  } else if (entry.isFile()) {
528
+ if (WORKSPACE_FILE_SKIP_FILES.has(entry.name)) continue;
513
529
  const ext = path.extname(entry.name).toLowerCase();
514
530
  if (!WORKSPACE_TEXT_EXTS.has(ext)) continue;
515
531
  let size = 0;
@@ -653,6 +669,39 @@ function workspaceSlotValue(slot) {
653
669
  return "";
654
670
  }
655
671
 
672
+ function workspaceSlotByName(instance, name) {
673
+ const slots = [...(Array.isArray(instance?.input) ? instance.input : []), ...(Array.isArray(instance?.output) ? instance.output : [])];
674
+ return slots.find((slot) => String(slot?.name || "") === String(name || "")) || null;
675
+ }
676
+
677
+ function workspaceSetOutputSlot(instance, name, value) {
678
+ const text = String(value ?? "");
679
+ return {
680
+ ...(instance || {}),
681
+ output: (Array.isArray(instance?.output) ? instance.output : []).map((slot) => (
682
+ String(slot?.name || "") === String(name || "") ? { ...slot, default: text, value: text } : slot
683
+ )),
684
+ };
685
+ }
686
+
687
+ function workspaceResolvePath(baseCwd, raw) {
688
+ const text = String(raw || "").trim();
689
+ if (!text) return "";
690
+ return path.isAbsolute(text) ? path.resolve(text) : path.resolve(baseCwd, text);
691
+ }
692
+
693
+ function workspaceSanitizeRepoDirName(repoUrl) {
694
+ const raw = String(repoUrl || "").trim().replace(/\.git$/i, "");
695
+ const last = raw.split(/[/:]/).filter(Boolean).pop() || "repo";
696
+ return last.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
697
+ }
698
+
699
+ function workspaceBoolSlot(instance, name, defaultValue = false) {
700
+ const value = workspaceSlotValue(workspaceSlotByName(instance, name));
701
+ if (!value.trim()) return Boolean(defaultValue);
702
+ return ["true", "1", "yes", "on"].includes(value.trim().toLowerCase());
703
+ }
704
+
656
705
  function workspaceInstanceText(instance) {
657
706
  const body = String(instance?.body || "").trim();
658
707
  if (body) return body;
@@ -666,10 +715,12 @@ function workspaceDisplayKind(definitionId) {
666
715
  if (id === "display_markdown") return "markdown";
667
716
  if (id === "display_mermaid") return "mermaid";
668
717
  if (id === "display_ascii") return "ascii";
718
+ if (id === "display_html") return "html";
719
+ if (id === "display_image") return "image";
669
720
  return "";
670
721
  }
671
722
 
672
- function workspaceRunOrder(graph, runNodeId) {
723
+ function workspaceRunPlan(graph, runNodeId) {
673
724
  const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
674
725
  const edges = Array.isArray(graph?.edges) ? graph.edges : [];
675
726
  const target = String(runNodeId || "").trim();
@@ -685,15 +736,20 @@ function workspaceRunOrder(graph, runNodeId) {
685
736
  if (!downstream.has(source)) downstream.set(source, []);
686
737
  downstream.get(source).push(dest);
687
738
  }
688
- const reachable = new Set();
739
+ const needed = new Set();
740
+ const pauseNodeIds = new Set();
689
741
  const visit = (id) => {
690
- if (!id || reachable.has(id)) return;
691
- reachable.add(id);
742
+ if (!id || needed.has(id)) return;
743
+ const defId = String(instances[id]?.definitionId || "");
744
+ if (id !== target && defId === "workspace_run") {
745
+ pauseNodeIds.add(id);
746
+ return;
747
+ }
748
+ needed.add(id);
692
749
  for (const next of downstream.get(id) || []) visit(next);
693
750
  };
694
751
  visit(target);
695
- reachable.delete(target);
696
- const needed = reachable;
752
+ needed.delete(target);
697
753
  const indegree = new Map(Array.from(needed).map((id) => [id, 0]));
698
754
  for (const id of needed) {
699
755
  for (const prev of upstream.get(id) || []) {
@@ -715,7 +771,7 @@ function workspaceRunOrder(graph, runNodeId) {
715
771
  if (ordered.length !== needed.size) {
716
772
  throw new Error("Workspace run graph contains a cycle");
717
773
  }
718
- return ordered;
774
+ return { order: ordered, pauseNodeIds: Array.from(pauseNodeIds) };
719
775
  }
720
776
 
721
777
  function workspaceUpstreamText(graph, nodeId, outputs) {
@@ -762,14 +818,16 @@ function workspaceUpstreamSkillBlocks(graph, nodeId, outputs) {
762
818
  function workspaceWriteDisplayContent(instance, content) {
763
819
  const next = { ...(instance || {}) };
764
820
  const text = String(content || "");
821
+ const kind = workspaceDisplayKind(next.definitionId);
822
+ const primaryName = kind === "image" ? "src" : "content";
765
823
  next.body = text;
766
824
  next.input = (Array.isArray(next.input) ? next.input : []).map((slot) => (
767
- String(slot?.name || "") === "content" || String(slot?.type || "") === "text"
825
+ String(slot?.name || "") === primaryName || String(slot?.type || "") === "text"
768
826
  ? { ...slot, default: text, value: text }
769
827
  : slot
770
828
  ));
771
829
  next.output = (Array.isArray(next.output) ? next.output : []).map((slot) => (
772
- String(slot?.name || "") === "content" || String(slot?.type || "") === "text"
830
+ String(slot?.name || "") === primaryName || String(slot?.type || "") === "text"
773
831
  ? { ...slot, default: text, value: text }
774
832
  : slot
775
833
  ));
@@ -808,7 +866,7 @@ function workspaceNodePrompt(graph, nodeId, upstreamText, skillsBlock) {
808
866
  async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts = {}) {
809
867
  const graph = normalizeWorkspaceGraphPayload(payload.graph || {});
810
868
  const runNodeId = String(payload?.runNodeId || "").trim();
811
- const order = workspaceRunOrder(graph, runNodeId);
869
+ const { order, pauseNodeIds } = workspaceRunPlan(graph, runNodeId);
812
870
  const fallbackSelectedSkillKeys = Array.isArray(payload?.selectedSkills)
813
871
  ? payload.selectedSkills.map((x) => String(x || "").trim()).filter(Boolean)
814
872
  : [];
@@ -880,6 +938,14 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
880
938
  continue;
881
939
  }
882
940
 
941
+ if (defId === "provide_bool") {
942
+ const raw = workspaceSlotValue(Array.isArray(instance.output) ? instance.output[0] : null) || workspaceInstanceText(instance);
943
+ const content = ["true", "1", "yes", "on"].includes(String(raw || "").trim().toLowerCase()) ? "true" : "false";
944
+ outputs.set(nodeId, content);
945
+ emit({ type: "node-done", nodeId, definitionId: defId });
946
+ continue;
947
+ }
948
+
883
949
  if (defId === "provide_file") {
884
950
  const fileValue = workspaceSlotValue(Array.isArray(instance.output) ? instance.output[0] : null) || workspaceInstanceText(instance);
885
951
  const abs = path.resolve(scopedRoot, fileValue);
@@ -912,6 +978,179 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
912
978
  continue;
913
979
  }
914
980
 
981
+ if (defId === "tool_git_checkout") {
982
+ const repoUrl = workspaceSlotValue(workspaceSlotByName(instance, "repoUrl")).trim();
983
+ if (!repoUrl) throw new Error("Git Checkout requires repoUrl");
984
+ const branch = workspaceSlotValue(workspaceSlotByName(instance, "branch")).trim();
985
+ const targetRaw = workspaceSlotValue(workspaceSlotByName(instance, "targetDir")).trim();
986
+ const targetDir = targetRaw
987
+ ? workspaceResolvePath(cwd, targetRaw)
988
+ : path.join(scopedRoot, ".workspace", "agentflow", "git-repos", workspaceSanitizeRepoDirName(repoUrl));
989
+ const pullIfExists = workspaceBoolSlot(instance, "pullIfExists", true);
990
+ const includeSubmodules = workspaceBoolSlot(instance, "includeSubmodules", false);
991
+ fs.mkdirSync(path.dirname(targetDir), { recursive: true });
992
+ let changed = false;
993
+ if (fs.existsSync(path.join(targetDir, ".git"))) {
994
+ if (pullIfExists) {
995
+ const fetch = runGit(["fetch", "--all", "--prune"], targetDir);
996
+ if (fetch.status !== 0) throw new Error(`git fetch failed: ${fetch.stderr || fetch.stdout}`);
997
+ if (branch) {
998
+ const checkout = runGit(["checkout", branch], targetDir);
999
+ if (checkout.status !== 0) throw new Error(`git checkout failed: ${checkout.stderr || checkout.stdout}`);
1000
+ }
1001
+ const before = runGit(["rev-parse", "HEAD"], targetDir).stdout.trim();
1002
+ const pull = runGit(["pull", "--ff-only"], targetDir);
1003
+ if (pull.status !== 0) throw new Error(`git pull failed: ${pull.stderr || pull.stdout}`);
1004
+ const after = runGit(["rev-parse", "HEAD"], targetDir).stdout.trim();
1005
+ changed = before !== after;
1006
+ }
1007
+ } else {
1008
+ const args = ["clone"];
1009
+ if (includeSubmodules) args.push("--recurse-submodules");
1010
+ if (branch) args.push("--branch", branch);
1011
+ args.push(repoUrl, targetDir);
1012
+ const clone = runGit(args, cwd);
1013
+ if (clone.status !== 0) throw new Error(`git clone failed: ${clone.stderr || clone.stdout}`);
1014
+ changed = true;
1015
+ }
1016
+ if (includeSubmodules) {
1017
+ const submodule = runGit(["submodule", "update", "--init", "--recursive"], targetDir);
1018
+ if (submodule.status !== 0) throw new Error(`git submodule update failed: ${submodule.stderr || submodule.stdout}`);
1019
+ }
1020
+ const currentBranch = runGit(["rev-parse", "--abbrev-ref", "HEAD"], targetDir).stdout.trim();
1021
+ const commit = runGit(["rev-parse", "HEAD"], targetDir).stdout.trim();
1022
+ const remote = workspaceSlotValue(workspaceSlotByName(instance, "remote")).trim() || "origin";
1023
+ const gitContext = buildGitContext({
1024
+ repoPath: targetDir,
1025
+ branch: currentBranch === "HEAD" ? "DETACHED" : currentBranch,
1026
+ commit,
1027
+ remote,
1028
+ });
1029
+ const previousCwd = cwd;
1030
+ cwd = path.resolve(targetDir);
1031
+ let nextInstance = workspaceSetOutputSlot(instance, "repoPath", targetDir);
1032
+ nextInstance = workspaceSetOutputSlot(nextInstance, "branch", gitContext.branch);
1033
+ nextInstance = workspaceSetOutputSlot(nextInstance, "commit", commit);
1034
+ nextInstance = workspaceSetOutputSlot(nextInstance, "changed", changed ? "true" : "false");
1035
+ nextInstance = workspaceSetOutputSlot(nextInstance, "gitContext", JSON.stringify(gitContext));
1036
+ nextInstance = workspaceSetOutputSlot(nextInstance, "workspaceContext", JSON.stringify({
1037
+ version: 1,
1038
+ label: workspaceSanitizeRepoDirName(repoUrl),
1039
+ cwd,
1040
+ workspaceRoot: cwd,
1041
+ pipelineWorkspace: scopedRoot,
1042
+ previous: { version: 1, label: "workspace", cwd: previousCwd, workspaceRoot: previousCwd, pipelineWorkspace: scopedRoot, previous: null },
1043
+ }));
1044
+ graph.instances[nodeId] = nextInstance;
1045
+ outputs.set(nodeId, targetDir);
1046
+ emit({ type: "graph", nodeId, graph });
1047
+ emit({ type: "node-done", nodeId, definitionId: defId });
1048
+ continue;
1049
+ }
1050
+
1051
+ if (defId === "tool_git_worktree_load") {
1052
+ const gitContext = normalizeGitContext(workspaceSlotValue(workspaceSlotByName(instance, "gitContext")));
1053
+ const repoPath = workspaceResolvePath(cwd, workspaceSlotValue(workspaceSlotByName(instance, "repoPath"))) ||
1054
+ (gitContext?.repoPath ? path.resolve(gitContext.repoPath) : "");
1055
+ if (!repoPath) throw new Error("Load Worktree requires repoPath");
1056
+ const branch = workspaceSlotValue(workspaceSlotByName(instance, "branch")).trim();
1057
+ const rawWorktreePath = workspaceSlotValue(workspaceSlotByName(instance, "worktreePath")).trim();
1058
+ const worktreePath = rawWorktreePath ? workspaceResolvePath(cwd, rawWorktreePath) : (gitContext?.worktreePath ? path.resolve(gitContext.worktreePath) : "");
1059
+ const previousCwd = cwd;
1060
+ const result = loadGitWorktree({ repoPath, branch, worktreePath, pipelineWorkspace: scopedRoot });
1061
+ const outGitContext = buildGitContext({
1062
+ repoPath: result.repoRoot,
1063
+ worktreePath: result.worktreePath,
1064
+ branch: result.branch,
1065
+ commit: result.commit,
1066
+ remote: gitContext?.remote || "origin",
1067
+ remoteUrl: gitContext?.remoteUrl || "",
1068
+ });
1069
+ cwd = result.worktreePath;
1070
+ let nextInstance = workspaceSetOutputSlot(instance, "worktreePath", result.worktreePath);
1071
+ nextInstance = workspaceSetOutputSlot(nextInstance, "branch", result.branch);
1072
+ nextInstance = workspaceSetOutputSlot(nextInstance, "commit", result.commit);
1073
+ nextInstance = workspaceSetOutputSlot(nextInstance, "gitContext", JSON.stringify(outGitContext));
1074
+ nextInstance = workspaceSetOutputSlot(nextInstance, "workspaceContext", JSON.stringify({
1075
+ version: 1,
1076
+ label: result.branch === "DETACHED" ? `worktree:${result.commit.slice(0, 8)}` : `worktree:${result.branch}`,
1077
+ cwd: result.worktreePath,
1078
+ workspaceRoot: result.worktreePath,
1079
+ pipelineWorkspace: scopedRoot,
1080
+ previous: { version: 1, label: "workspace", cwd: previousCwd, workspaceRoot: previousCwd, pipelineWorkspace: scopedRoot, previous: null },
1081
+ }));
1082
+ graph.instances[nodeId] = nextInstance;
1083
+ outputs.set(nodeId, result.worktreePath);
1084
+ emit({ type: "graph", nodeId, graph });
1085
+ emit({ type: "node-done", nodeId, definitionId: defId });
1086
+ continue;
1087
+ }
1088
+
1089
+ if (defId === "tool_git_worktree_unload") {
1090
+ const gitContext = normalizeGitContext(workspaceSlotValue(workspaceSlotByName(instance, "gitContext")));
1091
+ const repoPath = workspaceResolvePath(cwd, workspaceSlotValue(workspaceSlotByName(instance, "repoPath"))) ||
1092
+ (gitContext?.repoPath ? path.resolve(gitContext.repoPath) : "");
1093
+ const worktreePath = workspaceResolvePath(cwd, workspaceSlotValue(workspaceSlotByName(instance, "worktreePath"))) ||
1094
+ (gitContext?.worktreePath ? path.resolve(gitContext.worktreePath) : "");
1095
+ if (!repoPath) throw new Error("Unload Worktree requires repoPath");
1096
+ if (!worktreePath) throw new Error("Unload Worktree requires worktreePath");
1097
+ const force = ["true", "1", "yes", "on"].includes(workspaceSlotValue(workspaceSlotByName(instance, "force")).trim().toLowerCase());
1098
+ const pruneRaw = workspaceSlotValue(workspaceSlotByName(instance, "prune")).trim().toLowerCase();
1099
+ const prune = pruneRaw !== "false";
1100
+ const result = unloadGitWorktree({ repoPath, worktreePath, force, prune });
1101
+ cwd = scopedRoot;
1102
+ let nextInstance = workspaceSetOutputSlot(instance, "removed", "true");
1103
+ nextInstance = workspaceSetOutputSlot(nextInstance, "message", result.message);
1104
+ nextInstance = workspaceSetOutputSlot(nextInstance, "workspaceContext", JSON.stringify({
1105
+ version: 1,
1106
+ label: "workspace",
1107
+ cwd,
1108
+ workspaceRoot: cwd,
1109
+ pipelineWorkspace: scopedRoot,
1110
+ previous: null,
1111
+ }));
1112
+ graph.instances[nodeId] = nextInstance;
1113
+ outputs.set(nodeId, result.message);
1114
+ emit({ type: "graph", nodeId, graph });
1115
+ emit({ type: "node-done", nodeId, definitionId: defId });
1116
+ continue;
1117
+ }
1118
+
1119
+ if (defId === "tool_gitlab_create_mr") {
1120
+ const gitContext = normalizeGitContext(workspaceSlotValue(workspaceSlotByName(instance, "gitContext")));
1121
+ const repoPath = workspaceResolvePath(cwd, workspaceSlotValue(workspaceSlotByName(instance, "repoPath")));
1122
+ const result = await createGitLabMergeRequest({
1123
+ gitContext,
1124
+ workspaceCwd: cwd,
1125
+ repoPath,
1126
+ sourceBranch: workspaceSlotValue(workspaceSlotByName(instance, "sourceBranch")),
1127
+ targetBranch: workspaceSlotValue(workspaceSlotByName(instance, "targetBranch")),
1128
+ title: workspaceSlotValue(workspaceSlotByName(instance, "title")),
1129
+ description: workspaceSlotValue(workspaceSlotByName(instance, "description")),
1130
+ draft: workspaceSlotValue(workspaceSlotByName(instance, "draft")),
1131
+ labels: workspaceSlotValue(workspaceSlotByName(instance, "labels")),
1132
+ push: workspaceSlotValue(workspaceSlotByName(instance, "push")),
1133
+ remote: workspaceSlotValue(workspaceSlotByName(instance, "remote")),
1134
+ tokenEnv: workspaceSlotValue(workspaceSlotByName(instance, "tokenEnv")),
1135
+ gitlabApiBase: workspaceSlotValue(workspaceSlotByName(instance, "gitlabApiBase")),
1136
+ removeSourceBranch: workspaceSlotValue(workspaceSlotByName(instance, "removeSourceBranch")),
1137
+ squash: workspaceSlotValue(workspaceSlotByName(instance, "squash")),
1138
+ }, runtimeEnvForUser(userCtx));
1139
+ let nextInstance = workspaceSetOutputSlot(instance, "mrUrl", result.mrUrl);
1140
+ nextInstance = workspaceSetOutputSlot(nextInstance, "created", result.created ? "true" : "false");
1141
+ nextInstance = workspaceSetOutputSlot(nextInstance, "mrIid", result.mrIid ?? "");
1142
+ nextInstance = workspaceSetOutputSlot(nextInstance, "projectId", result.projectId ?? "");
1143
+ nextInstance = workspaceSetOutputSlot(nextInstance, "sourceBranch", result.sourceBranch ?? "");
1144
+ nextInstance = workspaceSetOutputSlot(nextInstance, "targetBranch", result.targetBranch ?? "");
1145
+ nextInstance = workspaceSetOutputSlot(nextInstance, "title", result.title ?? "");
1146
+ nextInstance = workspaceSetOutputSlot(nextInstance, "message", result.message ?? "");
1147
+ graph.instances[nodeId] = nextInstance;
1148
+ outputs.set(nodeId, result.mrUrl);
1149
+ emit({ type: "graph", nodeId, graph });
1150
+ emit({ type: "node-done", nodeId, definitionId: defId });
1151
+ continue;
1152
+ }
1153
+
915
1154
  const upstreamText = workspaceUpstreamText(graph, nodeId, outputs);
916
1155
  const body = String(instance.body || "").trim();
917
1156
  if (defId === "agent_subAgent" && !body && !String(upstreamText || "").trim()) {
@@ -956,8 +1195,11 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
956
1195
  if (updatedDisplays.length) emit({ type: "graph", nodeId, displayNodeIds: updatedDisplays, graph });
957
1196
  emit({ type: "node-done", nodeId, definitionId: defId });
958
1197
  }
1198
+ if (pauseNodeIds.length > 0) {
1199
+ emit({ type: "paused", nodeIds: pauseNodeIds, message: `Workspace run paused at ${pauseNodeIds.join(", ")}` });
1200
+ }
959
1201
  graph.updatedAt = new Date().toISOString();
960
- return { graph, events, order };
1202
+ return { graph, events, order, pauseNodeIds };
961
1203
  }
962
1204
 
963
1205
  function isTransientAgentNetworkError(err) {
@@ -1609,7 +1851,7 @@ export function startUiServer({
1609
1851
  try {
1610
1852
  const result = await runWorkspaceGraph(root, scoped.root, payload, userCtx, { onEvent: writeEvent });
1611
1853
  fs.writeFileSync(graphPath, JSON.stringify(result.graph, null, 2) + "\n", "utf-8");
1612
- writeEvent({ type: "done", ok: true, path: graphPath, graph: result.graph, order: result.order });
1854
+ writeEvent({ type: "done", ok: true, path: graphPath, graph: result.graph, order: result.order, pauseNodeIds: result.pauseNodeIds || [] });
1613
1855
  res.end();
1614
1856
  } catch (e) {
1615
1857
  writeEvent({ type: "error", error: (e && e.message) || String(e) });
@@ -2280,13 +2522,38 @@ export function startUiServer({
2280
2522
 
2281
2523
  if (req.method === "GET" && url.pathname === "/api/marketplace/nodes") {
2282
2524
  try {
2283
- json(res, 200, listMarketplacePackages(root));
2525
+ json(res, 200, listMarketplacePackages(root, userCtx));
2284
2526
  } catch (e) {
2285
2527
  json(res, 500, { error: (e && e.message) || String(e) });
2286
2528
  }
2287
2529
  return;
2288
2530
  }
2289
2531
 
2532
+ if (req.method === "GET" && url.pathname === "/api/marketplace/flow-snippets") {
2533
+ try {
2534
+ json(res, 200, listMarketplaceFlowSnippets(root));
2535
+ } catch (e) {
2536
+ json(res, 500, { error: (e && e.message) || String(e) });
2537
+ }
2538
+ return;
2539
+ }
2540
+
2541
+ if (req.method === "DELETE" && url.pathname === "/api/marketplace/node") {
2542
+ const id = url.searchParams.get("id") || "";
2543
+ const version = url.searchParams.get("version") || "";
2544
+ if (!id || !version) {
2545
+ json(res, 400, { ok: false, error: "Missing marketplace node id or version" });
2546
+ return;
2547
+ }
2548
+ try {
2549
+ const result = deleteMarketplaceNodePackage(root, id, version, userCtx);
2550
+ json(res, result.ok ? 200 : 400, result);
2551
+ } catch (e) {
2552
+ json(res, 500, { ok: false, error: (e && e.message) || String(e) });
2553
+ }
2554
+ return;
2555
+ }
2556
+
2290
2557
  if (req.method === "POST" && url.pathname === "/api/marketplace/install-node") {
2291
2558
  let payload;
2292
2559
  try {
@@ -2349,6 +2616,23 @@ export function startUiServer({
2349
2616
  return;
2350
2617
  }
2351
2618
 
2619
+ if (req.method === "POST" && url.pathname === "/api/marketplace/publish-flow-snippet") {
2620
+ let payload;
2621
+ try {
2622
+ payload = JSON.parse(await readBody(req));
2623
+ } catch {
2624
+ json(res, 400, { error: "Invalid JSON body" });
2625
+ return;
2626
+ }
2627
+ try {
2628
+ const result = publishFlowSnippet(root, payload || {});
2629
+ json(res, result.ok ? 200 : 400, result);
2630
+ } catch (e) {
2631
+ json(res, 500, { ok: false, error: (e && e.message) || String(e) });
2632
+ }
2633
+ return;
2634
+ }
2635
+
2352
2636
  if (req.method === "GET" && url.pathname === "/api/flow") {
2353
2637
  const flowId = url.searchParams.get("flowId");
2354
2638
  const flowSource = url.searchParams.get("flowSource") || "user";
@@ -41,12 +41,15 @@ import { getRunDir, sanitizeAgentflowUserId } from "../lib/paths.mjs";
41
41
  import {
42
42
  buildSkillsContext,
43
43
  buildSkillsContextFromRegistry,
44
+ buildDefaultWorkspaceContext,
44
45
  expandRuntimePlaceholders,
45
46
  normalizeSkillsContext,
46
47
  normalizeWorkspaceContext,
47
48
  parseSkillKeyList,
48
49
  resolveWorkspaceTarget,
49
50
  } from "../lib/runtime-context.mjs";
51
+ import { buildGitContext, loadGitWorktree, normalizeGitContext, unloadGitWorktree } from "../lib/git-worktree.mjs";
52
+ import { createGitLabMergeRequest } from "../lib/gitlab-mr.mjs";
50
53
 
51
54
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
52
55
 
@@ -264,6 +267,12 @@ function isTruthyInput(value) {
264
267
  return text === "true" || text === "1" || text === "yes" || text === "y" || text === "on";
265
268
  }
266
269
 
270
+ function resolveMaybeWorkspacePath(raw, workspaceContext, extra = {}) {
271
+ const text = String(raw || "").trim();
272
+ if (!text) return "";
273
+ return resolveWorkspaceTarget(text, workspaceContext, extra);
274
+ }
275
+
267
276
  function emitGitCheckoutNode(workspaceRoot, flowName, uuid, instanceId, execId, resultPathRel) {
268
277
  const runDir = getRunDir(workspaceRoot, flowName, uuid);
269
278
  const { inputs, workspaceContext } = resolveNodeRuntimeContexts(workspaceRoot, flowName, uuid, instanceId);
@@ -312,6 +321,12 @@ function emitGitCheckoutNode(workspaceRoot, flowName, uuid, instanceId, execId,
312
321
 
313
322
  const currentBranch = runGit(["rev-parse", "--abbrev-ref", "HEAD"], targetDir).stdout.trim();
314
323
  const commit = runGit(["rev-parse", "HEAD"], targetDir).stdout.trim();
324
+ const gitContext = buildGitContext({
325
+ repoPath: targetDir,
326
+ branch: currentBranch === "HEAD" ? "DETACHED" : currentBranch,
327
+ commit,
328
+ remote: String(inputs.remote || "origin").trim() || "origin",
329
+ });
315
330
  const outWorkspaceContext = {
316
331
  version: 1,
317
332
  label: inputs.label || sanitizeRepoDirName(repoUrl),
@@ -326,10 +341,127 @@ function emitGitCheckoutNode(workspaceRoot, flowName, uuid, instanceId, execId,
326
341
  writeOutputSlot(runDir, instanceId, execId, "commit", commit);
327
342
  writeOutputSlot(runDir, instanceId, execId, "changed", changed ? "true" : "false");
328
343
  writeOutputSlot(runDir, instanceId, execId, "workspaceContext", JSON.stringify(outWorkspaceContext));
344
+ writeOutputSlot(runDir, instanceId, execId, "gitContext", JSON.stringify(gitContext));
329
345
  writeResult(workspaceRoot, flowName, uuid, instanceId, { status: "success", message: `git ${action}: ${currentBranch}@${commit.slice(0, 8)}` }, { execId });
330
346
  return emitLocalNoopPrompt(workspaceRoot, runDir, instanceId, "git-checkout", `Git checkout completed: ${targetDir}\n`);
331
347
  }
332
348
 
349
+ function requireWorkspaceContextInput(inputs, definitionId) {
350
+ if (!String(inputs?.workspaceContext || "").trim()) {
351
+ throw new Error(`${definitionId}: workspaceContext is required`);
352
+ }
353
+ }
354
+
355
+ function emitGitWorktreeLoadNode(workspaceRoot, flowName, uuid, instanceId, execId) {
356
+ const runDir = getRunDir(workspaceRoot, flowName, uuid);
357
+ const { inputs, workspaceContext } = resolveNodeRuntimeContexts(workspaceRoot, flowName, uuid, instanceId);
358
+ requireWorkspaceContextInput(inputs, "tool_git_worktree_load");
359
+ const gitContext = normalizeGitContext(inputs.gitContext);
360
+ const repoPath = resolveMaybeWorkspacePath(inputs.repoPath, workspaceContext) ||
361
+ (gitContext?.repoPath ? path.resolve(gitContext.repoPath) : "");
362
+ if (!repoPath) throw new Error("tool_git_worktree_load: repoPath or gitContext.repoPath is required");
363
+ const branch = String(inputs.branch || "").trim();
364
+ const worktreePath = resolveMaybeWorkspacePath(inputs.worktreePath, workspaceContext, { branch }) ||
365
+ (gitContext?.worktreePath ? path.resolve(gitContext.worktreePath) : "");
366
+ const result = loadGitWorktree({
367
+ repoPath,
368
+ branch,
369
+ worktreePath,
370
+ pipelineWorkspace: workspaceContext.pipelineWorkspace || path.resolve(workspaceRoot),
371
+ });
372
+ const outWorkspaceContext = {
373
+ version: 1,
374
+ label: result.branch === "DETACHED" ? `worktree:${result.commit.slice(0, 8)}` : `worktree:${result.branch}`,
375
+ cwd: result.worktreePath,
376
+ workspaceRoot: result.worktreePath,
377
+ pipelineWorkspace: workspaceContext.pipelineWorkspace || path.resolve(workspaceRoot),
378
+ flowDir: workspaceContext.flowDir,
379
+ previous: workspaceContext,
380
+ };
381
+ const outGitContext = buildGitContext({
382
+ repoPath: result.repoRoot,
383
+ worktreePath: result.worktreePath,
384
+ branch: result.branch,
385
+ commit: result.commit,
386
+ remote: gitContext?.remote || "origin",
387
+ remoteUrl: gitContext?.remoteUrl || "",
388
+ });
389
+ writeOutputSlot(runDir, instanceId, execId, "worktreePath", result.worktreePath);
390
+ writeOutputSlot(runDir, instanceId, execId, "branch", result.branch);
391
+ writeOutputSlot(runDir, instanceId, execId, "commit", result.commit);
392
+ writeOutputSlot(runDir, instanceId, execId, "workspaceContext", JSON.stringify(outWorkspaceContext));
393
+ writeOutputSlot(runDir, instanceId, execId, "gitContext", JSON.stringify(outGitContext));
394
+ writeResult(workspaceRoot, flowName, uuid, instanceId, {
395
+ status: "success",
396
+ message: `worktree loaded: ${result.worktreePath} (${result.branch}@${result.commit.slice(0, 8)})`,
397
+ }, { execId });
398
+ return emitLocalNoopPrompt(workspaceRoot, runDir, instanceId, "git-worktree-load", `Git worktree loaded: ${result.worktreePath}\n`);
399
+ }
400
+
401
+ function emitGitWorktreeUnloadNode(workspaceRoot, flowName, uuid, instanceId, execId) {
402
+ const runDir = getRunDir(workspaceRoot, flowName, uuid);
403
+ const { inputs, workspaceContext, flowJson } = resolveNodeRuntimeContexts(workspaceRoot, flowName, uuid, instanceId);
404
+ requireWorkspaceContextInput(inputs, "tool_git_worktree_unload");
405
+ const gitContext = normalizeGitContext(inputs.gitContext);
406
+ const repoPath = resolveMaybeWorkspacePath(inputs.repoPath, workspaceContext) ||
407
+ (gitContext?.repoPath ? path.resolve(gitContext.repoPath) : "");
408
+ const worktreePath = resolveMaybeWorkspacePath(inputs.worktreePath, workspaceContext) ||
409
+ (gitContext?.worktreePath ? path.resolve(gitContext.worktreePath) : "");
410
+ if (!repoPath) throw new Error("tool_git_worktree_unload: repoPath or gitContext.repoPath is required");
411
+ if (!worktreePath) throw new Error("tool_git_worktree_unload: worktreePath or gitContext.worktreePath is required");
412
+ const force = isTruthyInput(inputs.force);
413
+ const prune = String(inputs.prune ?? "true").trim().toLowerCase() !== "false";
414
+ const result = unloadGitWorktree({ repoPath, worktreePath, force, prune });
415
+ const nextWorkspaceContext = workspaceContext.previous
416
+ ? normalizeWorkspaceContext(workspaceContext.previous, workspaceRoot, flowName, flowJson)
417
+ : buildDefaultWorkspaceContext(workspaceRoot, flowName, flowJson);
418
+ writeOutputSlot(runDir, instanceId, execId, "removed", "true");
419
+ writeOutputSlot(runDir, instanceId, execId, "message", result.message);
420
+ writeOutputSlot(runDir, instanceId, execId, "workspaceContext", JSON.stringify(nextWorkspaceContext));
421
+ writeResult(workspaceRoot, flowName, uuid, instanceId, {
422
+ status: "success",
423
+ message: result.message,
424
+ }, { execId });
425
+ return emitLocalNoopPrompt(workspaceRoot, runDir, instanceId, "git-worktree-unload", `${result.message}\n`);
426
+ }
427
+
428
+ async function emitGitLabCreateMrNode(workspaceRoot, flowName, uuid, instanceId, execId) {
429
+ const runDir = getRunDir(workspaceRoot, flowName, uuid);
430
+ const { inputs, workspaceContext } = resolveNodeRuntimeContexts(workspaceRoot, flowName, uuid, instanceId);
431
+ const repoPath = resolveMaybeWorkspacePath(inputs.repoPath, workspaceContext);
432
+ const result = await createGitLabMergeRequest({
433
+ gitContext: inputs.gitContext,
434
+ workspaceCwd: workspaceContext.cwd,
435
+ repoPath,
436
+ sourceBranch: inputs.sourceBranch,
437
+ targetBranch: inputs.targetBranch,
438
+ title: inputs.title,
439
+ description: inputs.description,
440
+ draft: inputs.draft,
441
+ labels: inputs.labels,
442
+ push: inputs.push,
443
+ remote: inputs.remote,
444
+ tokenEnv: inputs.tokenEnv,
445
+ gitlabApiBase: inputs.gitlabApiBase,
446
+ removeSourceBranch: inputs.removeSourceBranch,
447
+ squash: inputs.squash,
448
+ }, process.env);
449
+ writeOutputSlot(runDir, instanceId, execId, "mrUrl", result.mrUrl);
450
+ writeOutputSlot(runDir, instanceId, execId, "created", result.created ? "true" : "false");
451
+ writeOutputSlot(runDir, instanceId, execId, "mrIid", result.mrIid ?? "");
452
+ writeOutputSlot(runDir, instanceId, execId, "projectId", result.projectId ?? "");
453
+ writeOutputSlot(runDir, instanceId, execId, "sourceBranch", result.sourceBranch ?? "");
454
+ writeOutputSlot(runDir, instanceId, execId, "targetBranch", result.targetBranch ?? "");
455
+ writeOutputSlot(runDir, instanceId, execId, "title", result.title ?? "");
456
+ writeOutputSlot(runDir, instanceId, execId, "message", result.message ?? "");
457
+ writeResult(workspaceRoot, flowName, uuid, instanceId, {
458
+ status: "success",
459
+ message: result.message || result.mrUrl,
460
+ body: result.mrUrl,
461
+ }, { execId });
462
+ return emitLocalNoopPrompt(workspaceRoot, runDir, instanceId, "gitlab-create-mr", `${result.message || "GitLab MR ready"}\n${result.mrUrl}\n`);
463
+ }
464
+
333
465
  function emitCdWorkspaceNode(workspaceRoot, flowName, uuid, instanceId, execId) {
334
466
  const runDir = getRunDir(workspaceRoot, flowName, uuid);
335
467
  const { inputs, workspaceContext } = resolveNodeRuntimeContexts(workspaceRoot, flowName, uuid, instanceId);
@@ -461,7 +593,7 @@ function emitToolPrintNode(workspaceRoot, flowName, uuid, instanceId, execId) {
461
593
  const flowJson = readFlowJsonObject(workspaceRoot, flowName, uuid);
462
594
  const data = getResolvedValues(workspaceRoot, flowName, uuid, instanceId);
463
595
  const inputs = data.ok ? (data.resolvedInputs || {}) : {};
464
- const skipNames = new Set(["prev", "next", "workspaceContext", "skillsContext", "workspaceRoot", "pipelineWorkspace", "flowName", "runDir", "flowDir", "cwd"]);
596
+ const skipNames = new Set(["prev", "next", "workspaceContext", "gitContext", "skillsContext", "workspaceRoot", "pipelineWorkspace", "flowName", "runDir", "flowDir", "cwd"]);
465
597
 
466
598
  let content = readPrintableValue(inputs.content, runDir);
467
599
  if (!content) {
@@ -681,7 +813,7 @@ ${directCommand}
681
813
  return { optionalPromptPath: relativePath.replace(/\\/g, "/"), directCommand };
682
814
  }
683
815
 
684
- function main() {
816
+ async function main() {
685
817
  const args = process.argv.slice(2);
686
818
  if (args.length < 4) {
687
819
  console.error(
@@ -1001,18 +1133,24 @@ function main() {
1001
1133
  return;
1002
1134
  }
1003
1135
 
1004
- if (definitionId === "tool_git_checkout" || definitionId === "control_cd_workspace" || definitionId === "control_user_workspace" || definitionId === "control_load_skills" || definitionId === "tool_print") {
1136
+ if (definitionId === "tool_git_checkout" || definitionId === "tool_git_worktree_load" || definitionId === "tool_git_worktree_unload" || definitionId === "tool_gitlab_create_mr" || definitionId === "control_cd_workspace" || definitionId === "control_user_workspace" || definitionId === "control_load_skills" || definitionId === "tool_print") {
1005
1137
  try {
1006
1138
  const promptPath =
1007
1139
  definitionId === "tool_git_checkout"
1008
1140
  ? emitGitCheckoutNode(workspaceRoot, flowName, uuid, instanceId, execId, resultPathRel)
1009
- : definitionId === "control_cd_workspace"
1010
- ? emitCdWorkspaceNode(workspaceRoot, flowName, uuid, instanceId, execId)
1011
- : definitionId === "control_user_workspace"
1012
- ? emitUserWorkspaceNode(workspaceRoot, flowName, uuid, instanceId, execId)
1013
- : definitionId === "control_load_skills"
1014
- ? emitLoadSkillsNode(workspaceRoot, flowName, uuid, instanceId, execId)
1015
- : emitToolPrintNode(workspaceRoot, flowName, uuid, instanceId, execId);
1141
+ : definitionId === "tool_git_worktree_load"
1142
+ ? emitGitWorktreeLoadNode(workspaceRoot, flowName, uuid, instanceId, execId)
1143
+ : definitionId === "tool_git_worktree_unload"
1144
+ ? emitGitWorktreeUnloadNode(workspaceRoot, flowName, uuid, instanceId, execId)
1145
+ : definitionId === "tool_gitlab_create_mr"
1146
+ ? await emitGitLabCreateMrNode(workspaceRoot, flowName, uuid, instanceId, execId)
1147
+ : definitionId === "control_cd_workspace"
1148
+ ? emitCdWorkspaceNode(workspaceRoot, flowName, uuid, instanceId, execId)
1149
+ : definitionId === "control_user_workspace"
1150
+ ? emitUserWorkspaceNode(workspaceRoot, flowName, uuid, instanceId, execId)
1151
+ : definitionId === "control_load_skills"
1152
+ ? emitLoadSkillsNode(workspaceRoot, flowName, uuid, instanceId, execId)
1153
+ : emitToolPrintNode(workspaceRoot, flowName, uuid, instanceId, execId);
1016
1154
  writeCacheJsonForNode(workspaceRoot, flowName, uuid, instanceId, execId);
1017
1155
  logToRunTag(workspaceRoot, flowName, uuid, "pre-process", { event: "runtime-context-node", instanceId, definitionId });
1018
1156
  console.log(JSON.stringify({
@@ -1103,4 +1241,7 @@ function main() {
1103
1241
  console.log(JSON.stringify(output));
1104
1242
  }
1105
1243
 
1106
- main();
1244
+ main().catch((e) => {
1245
+ console.error(JSON.stringify({ ok: false, error: e.message || String(e) }));
1246
+ process.exit(1);
1247
+ });