@fieldwangai/agentflow 0.1.162 → 0.1.164

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 (42) hide show
  1. package/bin/lib/flow-dsl/codegen.mjs +23 -2
  2. package/bin/lib/flow-dsl/lint.mjs +44 -0
  3. package/bin/lib/flow-dsl/parser.mjs +68 -1
  4. package/bin/lib/marketplace-usage.mjs +218 -0
  5. package/bin/lib/marketplace.mjs +183 -3
  6. package/bin/lib/node-package-manifest.mjs +1 -1
  7. package/bin/lib/spaces.mjs +200 -0
  8. package/bin/lib/ui-server.mjs +314 -76
  9. package/bin/lib/workspace-routes.mjs +396 -5
  10. package/bin/lib/workspace-run-logs.mjs +2 -0
  11. package/bin/lib/workspace-server.mjs +698 -48
  12. package/bin/lib/workspace-state.mjs +2 -1
  13. package/builtin/nodes/agent_subAgent.md +5 -1
  14. package/builtin/nodes/context_bundle.md +44 -0
  15. package/builtin/nodes/context_knowledge.md +29 -0
  16. package/builtin/nodes/context_skills.md +29 -0
  17. package/builtin/nodes/context_workspace.md +36 -0
  18. package/builtin/nodes/control_while.md +7 -0
  19. package/builtin/nodes/tool_git_worktree_load.md +4 -3
  20. package/builtin/web-ui/dist/assets/{WorkflowAssistantThread-CTrXOZ00.js → WorkflowAssistantThread-CffIx5BY.js} +1 -1
  21. package/builtin/web-ui/dist/assets/index-CDItaRfX.css +1 -0
  22. package/builtin/web-ui/dist/assets/index-CWIcfWHO.js +873 -0
  23. package/builtin/web-ui/dist/index.html +2 -2
  24. package/package.json +1 -1
  25. package/shared/slot-types.js +1 -0
  26. package/skills/agentflow-cli/SKILL.md +50 -3
  27. package/skills/agentflow-cli/runtime/bin/lib/skill-runtime.mjs +120 -6
  28. package/skills/agentflow-cli/runtime/builtin/nodes/agent_subAgent.md +5 -1
  29. package/skills/agentflow-cli/runtime/builtin/nodes/context_bundle.md +44 -0
  30. package/skills/agentflow-cli/runtime/builtin/nodes/context_knowledge.md +29 -0
  31. package/skills/agentflow-cli/runtime/builtin/nodes/context_skills.md +29 -0
  32. package/skills/agentflow-cli/runtime/builtin/nodes/context_workspace.md +36 -0
  33. package/skills/agentflow-cli/runtime/builtin/nodes/control_while.md +7 -0
  34. package/skills/agentflow-cli/runtime/builtin/nodes/tool_git_worktree_load.md +4 -3
  35. package/skills/agentflow-cli/runtime/package.json +1 -1
  36. package/skills/agentflow-cli/scripts/agentflow-cli.mjs +64 -0
  37. package/skills/agentflow-flow-dsl/SKILL.md +48 -2
  38. package/skills/agentflow-flow-dsl/references/node-calls.md +6 -2
  39. package/skills/agentflow-flow-dsl/references/subflow-authoring.md +34 -7
  40. package/skills/agentflow-node-reference/references/builtin-nodes.md +37 -5
  41. package/builtin/web-ui/dist/assets/index-5uJFccdX.css +0 -1
  42. package/builtin/web-ui/dist/assets/index-BeUfNQRL.js +0 -873
@@ -17,12 +17,23 @@ import { buildSkillCompactInjectionBlock, loadResourcesForSkillKeys } from "./co
17
17
  import { execFileBuffered } from "./exec-buffered.mjs";
18
18
  import { runGit } from "./git-worktree.mjs";
19
19
  import {
20
+ listMarketplaceFlowSnippets,
21
+ listMarketplaceFlows,
20
22
  listMarketplacePackages,
21
23
  nodePackageArchive,
24
+ publishMarketplaceFlow,
22
25
  publishNodePackage,
23
26
  publishNodePackageArchive,
27
+ readMarketplaceFlow,
24
28
  resolveMarketplaceNodePackage,
29
+ setMarketplaceVisibility,
25
30
  } from "./marketplace.mjs";
31
+ import {
32
+ appendMarketplaceUsageEvent,
33
+ marketplaceResourcesForRun,
34
+ readMarketplaceFlowOrigin,
35
+ writeMarketplaceFlowOrigin,
36
+ } from "./marketplace-usage.mjs";
26
37
  import { NODE_PACKAGE_ENTRY, nodePackageExportsRun, readNodePackageManifest } from "./node-package-manifest.mjs";
27
38
  import { inspectNodePackageDirectory } from "./node-package-archive.mjs";
28
39
  import { json, readBody } from "./http-util.mjs";
@@ -38,8 +49,8 @@ import { mergeWorkspaceGraphs, workspaceDesignRevision, workspaceRuntimeRevision
38
49
  import { DEFAULT_WORKSPACE_PREVIEW_TTL_MS, createWorkspacePreviewId, normalizeWorkspacePreviewTtlMs, readWorkspacePreviewMetadata, workspaceSharedPreviewFlowDir, writeWorkspacePreviewMetadata } from "./workspace-preview.mjs";
39
50
  import { DEFAULT_WORKSPACE_DRAFT_TTL_MS, createWorkspaceDraftId, normalizeWorkspaceDraftTtlMs, readWorkspaceDraftMetadata, workspaceDraftFlowDir, writeWorkspaceDraftMetadata } from "./workspace-draft.mjs";
40
51
  import { appendWorkspaceRunLogEvent, createWorkspaceRunLogSession, finishWorkspaceRunLogSession, listWorkspaceRunLogs, readWorkspaceRunLogEvents } from "./workspace-run-logs.mjs";
41
- import { activeWorkspaceRuns, appendWorkspaceRunFinished, appendWorkspaceRunStarted, hydrateWorkspaceGraphForRuntime, isReadonlyBuiltinFlowSource, isTransientAgentNetworkError, isValidFlowSourceRead, isWorkspaceRunAbortError, listWorkspaceScheduleStatusesForFlow, mergeWorkspacePersistentNodeRefs, mergeWorkspaceRunGraph, normalizeWorkspaceEntry, normalizeWorkspaceScheduledRunConfig, readWorkspaceConversations, readWorkspaceFiles, readWorkspaceGraph, removeWorkspaceDeferredRun, resolveWorkspaceFilePath, resolveWorkspaceScopeRoot, runWorkspaceGraph, sleepMs, syncWorkspaceSchedulesForGraph, upsertWorkspaceDeferredRun, workspaceActiveRunsForScope, workspaceCollaborationEventKey, workspaceCollaborationSequences, workspaceCollaborationSubscribers, workspaceCollaborationSummaryWithUsers, workspaceDeferredRunsForScope, workspaceDesignPath, workspaceDownloadContentDisposition, workspaceFindActiveRunConflict, workspaceGraphAsSource, workspaceOptimizeRunImplementations, workspaceRepoUrlWithCredential, workspaceRunControl, workspaceRunEntryKey, workspaceRunKey, workspaceRunPlan, workspaceRunPlanNodeIds, workspaceRunTouchedNodeIds, workspaceRuntimeNodeLabel, workspaceScheduleNextRunAt, workspaceScopedUserContext, workspaceSearchGuardrailsBlock, workspaceUnwrapOutputEnvelopeForDisplay, workspacesPath, writeWorkspaceConversations, writeWorkspaceGraph } from "./workspace-server.mjs";
42
- import { WORKSPACE_STATE_FILENAME } from "./workspace-state.mjs";
52
+ import { activeWorkspaceRuns, appendWorkspaceRunFinished, appendWorkspaceRunStarted, cleanupWorkspaceRunResources, hydrateWorkspaceGraphForRuntime, isReadonlyBuiltinFlowSource, isTransientAgentNetworkError, isValidFlowSourceRead, isWorkspaceRunAbortError, listWorkspaceScheduleStatusesForFlow, mergeWorkspacePersistentNodeRefs, mergeWorkspaceRunGraph, normalizeWorkspaceEntry, normalizeWorkspaceScheduledRunConfig, publishWorkspaceRelease, readWorkspaceConversations, readWorkspaceFiles, readWorkspaceGraph, readWorkspaceReleaseStatus, removeWorkspaceDeferredRun, resolveWorkspaceFilePath, resolveWorkspaceScopeRoot, rollbackWorkspaceRelease, runWorkspaceGraph, sleepMs, syncWorkspaceSchedulesForGraph, upsertWorkspaceDeferredRun, workspaceActiveRunsForScope, workspaceCollaborationEventKey, workspaceCollaborationSequences, workspaceCollaborationSubscribers, workspaceCollaborationSummaryWithUsers, workspaceDeferredRunsForScope, workspaceDesignPath, workspaceDownloadContentDisposition, workspaceFindActiveRunConflict, workspaceGraphAsSource, workspaceOptimizeRunImplementations, workspaceRepoUrlWithCredential, workspaceRunControl, workspaceRunEntryKey, workspaceRunKey, workspaceRunPlan, workspaceRunPlanNodeIds, workspaceRunTouchedNodeIds, workspaceRuntimeNodeLabel, workspaceScheduleNextRunAt, workspaceScopedUserContext, workspaceSearchGuardrailsBlock, workspaceUnwrapOutputEnvelopeForDisplay, workspacesPath, writeWorkspaceConversations, writeWorkspaceGraph } from "./workspace-server.mjs";
53
+ import { splitWorkspaceGraph, WORKSPACE_STATE_FILENAME } from "./workspace-state.mjs";
43
54
  import { getWorkspaceTree } from "./workspace-tree.mjs";
44
55
  import busboy from "busboy";
45
56
  import crypto from "crypto";
@@ -899,6 +910,45 @@ function workspaceBufferLooksLikeZip(buf) {
899
910
  );
900
911
  }
901
912
 
913
+ function installedMarketplaceFlowCopies(userId) {
914
+ const copies = new Map();
915
+ const pipelinesRoot = getUserPipelinesRoot(userId);
916
+ if (!fs.existsSync(pipelinesRoot)) return copies;
917
+ for (const entry of fs.readdirSync(pipelinesRoot, { withFileTypes: true })) {
918
+ if (!entry.isDirectory()) continue;
919
+ const origin = readMarketplaceFlowOrigin(path.join(pipelinesRoot, entry.name));
920
+ if (!origin) continue;
921
+ const key = `${origin.id}@${origin.version}`;
922
+ const flowIds = copies.get(key) || [];
923
+ flowIds.push(entry.name);
924
+ copies.set(key, flowIds);
925
+ }
926
+ return copies;
927
+ }
928
+
929
+ function marketplaceResourceMatches(item, queryText) {
930
+ if (!queryText) return true;
931
+ return [
932
+ item.id,
933
+ item.definitionId,
934
+ item.displayName,
935
+ item.description,
936
+ item.ownerUserId,
937
+ item.source,
938
+ ...(Array.isArray(item.tags) ? item.tags : []),
939
+ ].filter(Boolean).join("\n").toLowerCase().includes(queryText);
940
+ }
941
+
942
+ function sortMarketplaceResources(items) {
943
+ return items.sort((a, b) => (
944
+ Number(b.useCount || 0) - Number(a.useCount || 0)
945
+ || Number(b.installCount || 0) - Number(a.installCount || 0)
946
+ || String(b.updatedAt || b.createdAt || "").localeCompare(String(a.updatedAt || a.createdAt || ""))
947
+ || String(a.displayName || a.id).localeCompare(String(b.displayName || b.id))
948
+ || String(b.version || "").localeCompare(String(a.version || ""), undefined, { numeric: true, sensitivity: "base" })
949
+ ));
950
+ }
951
+
902
952
  /**
903
953
  * @param {import('http').IncomingMessage} req
904
954
  * @param {import('http').ServerResponse} res
@@ -921,6 +971,11 @@ async function workspaceRoutes(req, res, ctx) {
921
971
  inputs: node.inputs,
922
972
  outputs: node.outputs,
923
973
  ownerUserId: node.ownerUserId || node.createdBy || "",
974
+ visibility: node.visibility || "public",
975
+ useCount: Number(node.useCount || 0),
976
+ installCount: Number(node.installCount || 0),
977
+ uniqueUserCount: Number(node.uniqueUserCount || 0),
978
+ lastUsedAt: node.lastUsedAt || "",
924
979
  fileList: node.fileList || [],
925
980
  fileCount: node.fileCount || 0,
926
981
  totalBytes: node.totalBytes || 0,
@@ -980,6 +1035,14 @@ async function workspaceRoutes(req, res, ctx) {
980
1035
  json(res, 404, { error: result.error || "Node package not found" });
981
1036
  return;
982
1037
  }
1038
+ appendMarketplaceUsageEvent(root, {
1039
+ kind: "node",
1040
+ id,
1041
+ version,
1042
+ action: "install",
1043
+ actorUserId: userCtx.userId || "",
1044
+ eventId: `install:node:${id}@${version}:${userCtx.userId || "anonymous"}`,
1045
+ });
983
1046
  res.writeHead(200, {
984
1047
  "Content-Type": "application/zip",
985
1048
  "Content-Length": result.archive.length,
@@ -995,6 +1058,199 @@ async function workspaceRoutes(req, res, ctx) {
995
1058
  return;
996
1059
  }
997
1060
 
1061
+ if (req.method === "GET" && url.pathname === "/api/marketplace/resources") {
1062
+ try {
1063
+ const kind = String(url.searchParams.get("kind") || "flow").trim();
1064
+ const requestedScope = String(url.searchParams.get("scope") || "all").trim();
1065
+ const scope = ["all", "owned", "installed"].includes(requestedScope) ? requestedScope : "all";
1066
+ const marketplaceScope = scope === "owned" ? "owned" : "all";
1067
+ const queryText = String(url.searchParams.get("q") || "").trim().toLowerCase();
1068
+ if (kind === "node") {
1069
+ const marketplaceNodes = listMarketplacePackages(root, { ...userCtx, marketplaceScope }).nodes
1070
+ .map((node) => ({ ...node, resourceType: "node", installed: true }));
1071
+ let nodes = marketplaceNodes;
1072
+ if (scope === "installed") {
1073
+ const marketplaceByDefinition = new Map(marketplaceNodes.map((node) => [node.definitionId, node]));
1074
+ nodes = listNodesJson(root, "", "", { ...userCtx, marketplaceScope: "all" }).nodes.map((node) => {
1075
+ const marketplaceNode = marketplaceByDefinition.get(node.id);
1076
+ if (marketplaceNode) {
1077
+ return {
1078
+ ...marketplaceNode,
1079
+ definitionId: node.id,
1080
+ source: node.source || "marketplace",
1081
+ installed: true,
1082
+ };
1083
+ }
1084
+ return {
1085
+ id: node.packageId || node.id,
1086
+ version: node.version || "",
1087
+ definitionId: node.id,
1088
+ displayName: node.displayName || node.label || node.id,
1089
+ description: node.description || "",
1090
+ inputs: node.inputs || {},
1091
+ outputs: node.outputs || {},
1092
+ source: node.source || "project",
1093
+ resourceType: "node",
1094
+ localCatalog: true,
1095
+ installed: true,
1096
+ };
1097
+ });
1098
+ }
1099
+ nodes = sortMarketplaceResources(nodes.filter((item) => marketplaceResourceMatches(item, queryText)));
1100
+ json(res, 200, { kind, scope, sort: "useCount", order: "desc", items: nodes });
1101
+ return;
1102
+ }
1103
+ if (kind !== "flow") {
1104
+ json(res, 400, { error: "kind must be flow or node" });
1105
+ return;
1106
+ }
1107
+ const installedCopies = installedMarketplaceFlowCopies(userCtx.userId);
1108
+ const flows = listMarketplaceFlows(root, { ...userCtx, marketplaceScope }).flows.map((flow) => {
1109
+ const installedFlowIds = installedCopies.get(`${flow.id}@${flow.version}`) || [];
1110
+ return {
1111
+ ...flow,
1112
+ resourceType: "flow",
1113
+ installed: installedFlowIds.length > 0,
1114
+ installedFlowIds,
1115
+ };
1116
+ });
1117
+ const snippets = scope === "installed" ? [] : listMarketplaceFlowSnippets(root, { ...userCtx, marketplaceScope }).snippets
1118
+ .map((snippet) => ({ ...snippet, resourceType: "flow-snippet", installed: false }));
1119
+ const items = sortMarketplaceResources(
1120
+ [...flows, ...snippets]
1121
+ .filter((item) => scope !== "installed" || item.installed)
1122
+ .filter((item) => marketplaceResourceMatches(item, queryText)),
1123
+ );
1124
+ json(res, 200, { kind, scope, sort: "useCount", order: "desc", items });
1125
+ } catch (e) {
1126
+ json(res, 500, { error: (e && e.message) || String(e) });
1127
+ }
1128
+ return;
1129
+ }
1130
+
1131
+ if (req.method === "POST" && url.pathname === "/api/marketplace/flows/publish") {
1132
+ let payload;
1133
+ try {
1134
+ payload = JSON.parse(await readBody(req));
1135
+ } catch {
1136
+ json(res, 400, { error: "Invalid JSON body" });
1137
+ return;
1138
+ }
1139
+ try {
1140
+ const flowId = String(payload?.flowId || "").trim();
1141
+ const flowSource = String(payload?.flowSource || "user").trim();
1142
+ const scoped = resolveWorkspaceScopeRoot(root, {
1143
+ flowId,
1144
+ flowSource,
1145
+ archived: payload?.archived === true,
1146
+ }, userCtx);
1147
+ if (scoped.error) {
1148
+ json(res, scoped.status || 400, { error: scoped.error });
1149
+ return;
1150
+ }
1151
+ if (scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource)) {
1152
+ json(res, 400, { error: "Only active editable flows can be published" });
1153
+ return;
1154
+ }
1155
+ const current = readWorkspaceGraph(scoped.root, root).graph;
1156
+ const { design } = splitWorkspaceGraph(current);
1157
+ const graph = workspaceGraphWithScheduleMode(design, "disabled");
1158
+ const result = publishMarketplaceFlow(root, {
1159
+ id: payload?.id || flowId,
1160
+ version: payload?.version || "1.0.0",
1161
+ displayName: payload?.displayName || flowId,
1162
+ description: payload?.description || "",
1163
+ tags: payload?.tags || [],
1164
+ visibility: payload?.visibility || "public",
1165
+ graph,
1166
+ }, userCtx);
1167
+ json(res, result.ok ? (result.alreadyExists ? 200 : 201) : (result.conflict ? 409 : 400), result);
1168
+ } catch (e) {
1169
+ json(res, 500, { error: (e && e.message) || String(e) });
1170
+ }
1171
+ return;
1172
+ }
1173
+
1174
+ if (req.method === "POST" && url.pathname === "/api/marketplace/flows/install") {
1175
+ let payload;
1176
+ try {
1177
+ payload = JSON.parse(await readBody(req));
1178
+ } catch {
1179
+ json(res, 400, { error: "Invalid JSON body" });
1180
+ return;
1181
+ }
1182
+ const id = String(payload?.id || "").trim();
1183
+ const version = String(payload?.version || "").trim();
1184
+ const idCheck = validateUserPipelineId(String(payload?.flowId || id));
1185
+ if (!id || !version || !idCheck.ok) {
1186
+ json(res, 400, { error: idCheck.error || "Missing marketplace flow id or version" });
1187
+ return;
1188
+ }
1189
+ const flowId = idCheck.flowId;
1190
+ const targetDir = path.join(getUserPipelinesRoot(userCtx.userId), flowId);
1191
+ if (fs.existsSync(targetDir)) {
1192
+ json(res, 409, { error: `已存在同名流水线 ${flowId}` });
1193
+ return;
1194
+ }
1195
+ try {
1196
+ const source = readMarketplaceFlow(root, id, version, { ...userCtx, marketplaceScope: "all" });
1197
+ if (!source.ok) {
1198
+ json(res, 404, { error: source.error || "Marketplace flow not found" });
1199
+ return;
1200
+ }
1201
+ fs.mkdirSync(targetDir, { recursive: true });
1202
+ const graph = workspaceGraphWithScheduleMode(source.graph, "disabled");
1203
+ writeWorkspaceGraph(targetDir, graph, root);
1204
+ writeMarketplaceFlowOrigin(targetDir, { id, version });
1205
+ appendMarketplaceUsageEvent(root, {
1206
+ kind: "flow",
1207
+ id,
1208
+ version,
1209
+ action: "install",
1210
+ actorUserId: userCtx.userId || "",
1211
+ eventId: `install:flow:${id}@${version}:${userCtx.userId || "anonymous"}`,
1212
+ });
1213
+ json(res, 201, {
1214
+ ok: true,
1215
+ id,
1216
+ version,
1217
+ flowId,
1218
+ flowSource: "user",
1219
+ url: `/workspace?flowId=${encodeURIComponent(flowId)}&flowSource=user`,
1220
+ });
1221
+ } catch (e) {
1222
+ try {
1223
+ if (fs.existsSync(targetDir)) fs.rmSync(targetDir, { recursive: true, force: true });
1224
+ } catch {}
1225
+ json(res, 500, { error: (e && e.message) || String(e) });
1226
+ }
1227
+ return;
1228
+ }
1229
+
1230
+ if (req.method === "PATCH" && url.pathname === "/api/marketplace/visibility") {
1231
+ let payload;
1232
+ try {
1233
+ payload = JSON.parse(await readBody(req));
1234
+ } catch {
1235
+ json(res, 400, { error: "Invalid JSON body" });
1236
+ return;
1237
+ }
1238
+ try {
1239
+ const result = setMarketplaceVisibility(
1240
+ root,
1241
+ String(payload?.kind || ""),
1242
+ String(payload?.id || ""),
1243
+ String(payload?.version || ""),
1244
+ String(payload?.visibility || "public"),
1245
+ userCtx,
1246
+ );
1247
+ json(res, result.ok ? 200 : 400, result);
1248
+ } catch (e) {
1249
+ json(res, 500, { error: (e && e.message) || String(e) });
1250
+ }
1251
+ return;
1252
+ }
1253
+
998
1254
  if (req.method === "GET" && url.pathname === "/api/workspace-tree") {
999
1255
  try {
1000
1256
  json(res, 200, getWorkspaceTree(root));
@@ -1416,7 +1672,7 @@ async function workspaceRoutes(req, res, ctx) {
1416
1672
  const written = writeWorkspaceGraph(flowDir, graph, root);
1417
1673
  writeWorkspaceDraftMetadata(flowDir, metadata);
1418
1674
  const persisted = readWorkspaceGraph(flowDir, root).graph;
1419
- const baseUrl = `${url.protocol}//${url.host}`;
1675
+ const baseUrl = requestPublicBaseUrl(req);
1420
1676
  json(res, 200, {
1421
1677
  ok: true,
1422
1678
  flowId,
@@ -1489,7 +1745,7 @@ async function workspaceRoutes(req, res, ctx) {
1489
1745
  if (scoped.error) throw new Error(scoped.error);
1490
1746
  const persisted = readWorkspaceGraph(scoped.root, root).graph;
1491
1747
  const workspaceSchedules = syncWorkspaceSchedulesForGraph(root, scoped, persisted, authUser, userCtx);
1492
- const baseUrl = `${url.protocol}//${url.host}`;
1748
+ const baseUrl = requestPublicBaseUrl(req);
1493
1749
  json(res, 200, {
1494
1750
  ok: true,
1495
1751
  success: true,
@@ -1578,7 +1834,7 @@ async function workspaceRoutes(req, res, ctx) {
1578
1834
  json(res, 500, { error: (e && e.message) || String(e) });
1579
1835
  return;
1580
1836
  }
1581
- const baseUrl = `${url.protocol}//${url.host}`;
1837
+ const baseUrl = requestPublicBaseUrl(req);
1582
1838
  const workspaceUrl = `${baseUrl}/workspace?flowId=${encodeURIComponent(flowId)}&flowSource=workspace&archived=1`;
1583
1839
  json(res, 200, { ok: true, flowId, flowSource: "workspace", archived: true, preview: true, expiresAt: metadata.expiresAt, url: workspaceUrl });
1584
1840
  return;
@@ -1690,6 +1946,7 @@ async function workspaceRoutes(req, res, ctx) {
1690
1946
  ownerUserId: scoped.ownerUserId,
1691
1947
  ownerUsername: scoped.ownerUsername,
1692
1948
  } : null,
1949
+ release: readWorkspaceReleaseStatus(scoped.root, root, hydratedGraph),
1693
1950
  workspaceSchedules: listWorkspaceScheduleStatusesForFlow(scopedUserCtx, scoped.flowSource || "user", scoped.flowId || ""),
1694
1951
  });
1695
1952
  } catch (e) {
@@ -1704,6 +1961,126 @@ async function workspaceRoutes(req, res, ctx) {
1704
1961
  return;
1705
1962
  }
1706
1963
 
1964
+ if (req.method === "GET" && url.pathname === "/api/workspace/releases") {
1965
+ try {
1966
+ const scoped = resolveWorkspaceScopeRoot(root, {
1967
+ flowId: url.searchParams.get("flowId") || "",
1968
+ flowSource: url.searchParams.get("flowSource") || "user",
1969
+ workspaceId: url.searchParams.get("workspaceId") || "",
1970
+ adminOwnerId: url.searchParams.get("adminOwnerId") || "",
1971
+ archived: url.searchParams.get("archived") === "1",
1972
+ }, userCtx);
1973
+ if (scoped.error) {
1974
+ json(res, scoped.status || 400, { error: scoped.error });
1975
+ return;
1976
+ }
1977
+ json(res, 200, { ok: true, release: readWorkspaceReleaseStatus(scoped.root, root) });
1978
+ } catch (e) {
1979
+ json(res, 500, { error: (e && e.message) || String(e) });
1980
+ }
1981
+ return;
1982
+ }
1983
+
1984
+ if (req.method === "POST" && url.pathname === "/api/workspace/releases/publish") {
1985
+ let payload;
1986
+ try {
1987
+ payload = JSON.parse(await readBody(req));
1988
+ } catch {
1989
+ json(res, 400, { error: "Invalid JSON body" });
1990
+ return;
1991
+ }
1992
+ try {
1993
+ const scoped = resolveWorkspaceScopeRoot(root, {
1994
+ flowId: payload.flowId || "",
1995
+ flowSource: payload.flowSource || "user",
1996
+ workspaceId: payload.workspaceId || "",
1997
+ archived: payload.archived === true || payload.flowArchived === true,
1998
+ }, userCtx);
1999
+ if (scoped.error) {
2000
+ json(res, scoped.status || 400, { error: scoped.error });
2001
+ return;
2002
+ }
2003
+ if (
2004
+ scoped.archived
2005
+ || scoped.draft === true
2006
+ || isReadonlyBuiltinFlowSource(scoped.flowSource)
2007
+ || scoped.collaborationAccess?.writable === false
2008
+ ) {
2009
+ json(res, 403, { error: "Workspace release publish permission denied" });
2010
+ return;
2011
+ }
2012
+ const result = publishWorkspaceRelease(scoped.root, root, {
2013
+ expectedRevision: payload.expectedRevision || "",
2014
+ createdBy: userCtx.userId || authUser?.userId || "",
2015
+ notes: payload.notes || "",
2016
+ });
2017
+ if (result.error) {
2018
+ json(res, result.conflict ? 409 : 400, result);
2019
+ return;
2020
+ }
2021
+ const graph = readWorkspaceGraph(scoped.root, root).graph;
2022
+ const workspaceSchedules = syncWorkspaceSchedulesForGraph(root, scoped, graph, authUser, userCtx);
2023
+ broadcastWorkspaceCollaborationEvent(userCtx, scoped.flowSource, scoped.flowId, scoped.archived, {
2024
+ type: "release.published",
2025
+ releaseId: result.release.id,
2026
+ revision: result.release.designRevision,
2027
+ actorId: userCtx.userId || "",
2028
+ });
2029
+ json(res, 200, { ...result, workspaceSchedules });
2030
+ } catch (e) {
2031
+ json(res, 500, { error: (e && e.message) || String(e) });
2032
+ }
2033
+ return;
2034
+ }
2035
+
2036
+ if (req.method === "POST" && url.pathname === "/api/workspace/releases/rollback") {
2037
+ let payload;
2038
+ try {
2039
+ payload = JSON.parse(await readBody(req));
2040
+ } catch {
2041
+ json(res, 400, { error: "Invalid JSON body" });
2042
+ return;
2043
+ }
2044
+ try {
2045
+ const scoped = resolveWorkspaceScopeRoot(root, {
2046
+ flowId: payload.flowId || "",
2047
+ flowSource: payload.flowSource || "user",
2048
+ workspaceId: payload.workspaceId || "",
2049
+ archived: payload.archived === true || payload.flowArchived === true,
2050
+ }, userCtx);
2051
+ if (scoped.error) {
2052
+ json(res, scoped.status || 400, { error: scoped.error });
2053
+ return;
2054
+ }
2055
+ if (
2056
+ scoped.archived
2057
+ || scoped.draft === true
2058
+ || isReadonlyBuiltinFlowSource(scoped.flowSource)
2059
+ || scoped.collaborationAccess?.writable === false
2060
+ ) {
2061
+ json(res, 403, { error: "Workspace release rollback permission denied" });
2062
+ return;
2063
+ }
2064
+ const result = rollbackWorkspaceRelease(scoped.root, payload.releaseId || "", root);
2065
+ if (result.error) {
2066
+ json(res, 404, result);
2067
+ return;
2068
+ }
2069
+ const graph = readWorkspaceGraph(scoped.root, root).graph;
2070
+ const workspaceSchedules = syncWorkspaceSchedulesForGraph(root, scoped, graph, authUser, userCtx);
2071
+ broadcastWorkspaceCollaborationEvent(userCtx, scoped.flowSource, scoped.flowId, scoped.archived, {
2072
+ type: "release.rollback",
2073
+ releaseId: result.release.id,
2074
+ revision: result.release.designRevision,
2075
+ actorId: userCtx.userId || "",
2076
+ });
2077
+ json(res, 200, { ...result, workspaceSchedules });
2078
+ } catch (e) {
2079
+ json(res, 500, { error: (e && e.message) || String(e) });
2080
+ }
2081
+ return;
2082
+ }
2083
+
1707
2084
  if (req.method === "POST" && url.pathname === "/api/workspace/graph") {
1708
2085
  let payload;
1709
2086
  try {
@@ -1836,6 +2213,7 @@ async function workspaceRoutes(req, res, ctx) {
1836
2213
  designRevision: revision,
1837
2214
  runtimeRevision,
1838
2215
  merged,
2216
+ release: readWorkspaceReleaseStatus(scoped.root, root, committed.graph),
1839
2217
  workspaceSchedules,
1840
2218
  });
1841
2219
  } catch (e) {
@@ -2158,6 +2536,8 @@ async function workspaceRoutes(req, res, ctx) {
2158
2536
  flowSource: scoped.flowSource || payload.flowSource || "user",
2159
2537
  plannedNodeIds,
2160
2538
  startedAt: Date.now(),
2539
+ workspaceRoot: root,
2540
+ marketplaceResources: marketplaceResourcesForRun(scoped.root, runtimeGraph, plannedNodeIds),
2161
2541
  };
2162
2542
  const runLog = createWorkspaceRunLogSession({
2163
2543
  runId,
@@ -2255,6 +2635,7 @@ async function workspaceRoutes(req, res, ctx) {
2255
2635
  ...runEntry,
2256
2636
  endedAt,
2257
2637
  durationMs: endedAt - runEntry.startedAt,
2638
+ marketplaceResources: marketplaceResourcesForRun(scoped.root, runtimeGraph, result.order || Array.from(touchedIds)),
2258
2639
  }, "success");
2259
2640
  finishWorkspaceRunLogSession(runLog.runId, "success", {
2260
2641
  endedAt,
@@ -2350,6 +2731,7 @@ async function workspaceRoutes(req, res, ctx) {
2350
2731
  ...runEntry,
2351
2732
  endedAt,
2352
2733
  durationMs: endedAt - runEntry.startedAt,
2734
+ marketplaceResources: marketplaceResourcesForRun(scoped.root, runtimeGraph, result.order || Array.from(touchedIds)),
2353
2735
  }, "success");
2354
2736
  finishWorkspaceRunLogSession(runLog.runId, "success", {
2355
2737
  endedAt,
@@ -2574,6 +2956,11 @@ async function workspaceRoutes(req, res, ctx) {
2574
2956
  return;
2575
2957
  }
2576
2958
  removeWorkspaceDeferredRun(deferred.key);
2959
+ const resourceCleanup = cleanupWorkspaceRunResources(scoped.root, deferred.runId, {
2960
+ force: false,
2961
+ status: "stopped",
2962
+ emit: (event) => appendWorkspaceRunLogEvent(deferred.runId, event),
2963
+ });
2577
2964
  const endedAt = Date.now();
2578
2965
  appendWorkspaceRunLogEvent(deferred.runId, {
2579
2966
  type: "stop-completed",
@@ -2602,6 +2989,10 @@ async function workspaceRoutes(req, res, ctx) {
2602
2989
  ok: true,
2603
2990
  stopped: true,
2604
2991
  monitoringOnly: true,
2992
+ resources: {
2993
+ cleaned: resourceCleanup.cleaned,
2994
+ preserved: resourceCleanup.preserved,
2995
+ },
2605
2996
  });
2606
2997
  return;
2607
2998
  }
@@ -90,6 +90,8 @@ function indexRecord(meta = {}, patch = {}) {
90
90
  flowSource: String(meta.flowSource || "user"),
91
91
  scheduleNodeId: String(meta.scheduleNodeId || ""),
92
92
  runNodeId: String(meta.runNodeId || ""),
93
+ releaseId: String(meta.releaseId || ""),
94
+ designRevision: String(meta.designRevision || ""),
93
95
  scheduled: meta.scheduled === true,
94
96
  trigger: String(meta.trigger || (meta.scheduled === true ? "scheduled" : "manual")),
95
97
  label: String(meta.label || ""),