@fieldwangai/agentflow 0.1.43 → 0.1.44

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.
@@ -50,6 +50,13 @@ function readUserMcpPrivateEnvObject(userId) {
50
50
  return env;
51
51
  }
52
52
 
53
+ function readUserMcpPrivateServers(userId) {
54
+ const safe = sanitizeAgentflowUserId(userId);
55
+ if (!safe) return {};
56
+ const data = readJsonObject(path.join(getAgentflowUserDataRoot(safe), "mcp-private.json"));
57
+ return data?.servers && typeof data.servers === "object" && !Array.isArray(data.servers) ? data.servers : {};
58
+ }
59
+
53
60
  function pruneCursorMcpPrivateEnvPlaceholders() {
54
61
  const filePath = path.join(os.homedir(), ".cursor", "mcp.json");
55
62
  const config = readJsonObject(filePath);
@@ -83,12 +90,98 @@ function pruneCursorMcpPrivateEnvPlaceholders() {
83
90
  fs.writeFileSync(filePath, JSON.stringify({ ...config, mcpServers: nextServers }, null, 2) + "\n", "utf-8");
84
91
  }
85
92
 
93
+ function cursorMcpServersFromFile(filePath) {
94
+ const config = readJsonObject(filePath);
95
+ return config?.mcpServers && typeof config.mcpServers === "object" && !Array.isArray(config.mcpServers)
96
+ ? config.mcpServers
97
+ : {};
98
+ }
99
+
100
+ function materializeWorkspaceCursorMcpPrivateConfig(workspaceRoot, userId) {
101
+ const safe = sanitizeAgentflowUserId(userId);
102
+ if (!safe) return () => {};
103
+ const privateServers = readUserMcpPrivateServers(safe);
104
+ if (!Object.keys(privateServers).length) return () => {};
105
+
106
+ const workspace = path.resolve(workspaceRoot || process.cwd());
107
+ const filePath = path.join(workspace, ".cursor", "mcp.json");
108
+ const globalFilePath = path.join(os.homedir(), ".cursor", "mcp.json");
109
+ const existed = fs.existsSync(filePath);
110
+ const original = existed ? fs.readFileSync(filePath, "utf-8") : "";
111
+ const config = readJsonObject(filePath);
112
+ const localServers = cursorMcpServersFromFile(filePath);
113
+ const globalServers = cursorMcpServersFromFile(globalFilePath);
114
+ const nextServers = { ...localServers };
115
+ let changed = false;
116
+
117
+ for (const [name, privateServer] of Object.entries(privateServers)) {
118
+ const current = nextServers[name] || globalServers[name];
119
+ if (!current || typeof current !== "object" || Array.isArray(current)) continue;
120
+ const privateEnv = privateServer?.env && typeof privateServer.env === "object" && !Array.isArray(privateServer.env) ? privateServer.env : {};
121
+ const privateHeaders = privateServer?.headers && typeof privateServer.headers === "object" && !Array.isArray(privateServer.headers) ? privateServer.headers : {};
122
+ if (!Object.keys(privateEnv).length && !Object.keys(privateHeaders).length) continue;
123
+
124
+ const next = { ...current };
125
+ if (Object.keys(privateEnv).length) {
126
+ const currentEnv = current.env && typeof current.env === "object" && !Array.isArray(current.env) ? current.env : {};
127
+ next.env = { ...currentEnv, ...privateEnv };
128
+ changed = true;
129
+ }
130
+ if (Object.keys(privateHeaders).length) {
131
+ const currentHeaders = current.headers && typeof current.headers === "object" && !Array.isArray(current.headers) ? current.headers : {};
132
+ next.headers = { ...currentHeaders, ...privateHeaders };
133
+ changed = true;
134
+ }
135
+ nextServers[name] = next;
136
+ }
137
+
138
+ if (!changed) return () => {};
139
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
140
+ fs.writeFileSync(filePath, JSON.stringify({ ...config, mcpServers: nextServers }, null, 2) + "\n", "utf-8");
141
+
142
+ let restored = false;
143
+ return () => {
144
+ if (restored) return;
145
+ restored = true;
146
+ try {
147
+ if (existed) fs.writeFileSync(filePath, original, "utf-8");
148
+ else if (fs.existsSync(filePath)) fs.rmSync(filePath, { force: true });
149
+ } catch {
150
+ // Best-effort restore; do not fail an already-running agent on cleanup.
151
+ }
152
+ };
153
+ }
154
+
86
155
  function agentflowUserEnv(userId) {
87
156
  const safe = sanitizeAgentflowUserId(userId);
88
157
  pruneCursorMcpPrivateEnvPlaceholders();
89
158
  return { ...readMergedEnvObject(safe), ...(safe ? readUserMcpPrivateEnvObject(safe) : {}), AGENTFLOW_USER_ID: safe };
90
159
  }
91
160
 
161
+ function runCursorAgentWithPrivateMcp(cliWorkspace, prompt, options, userId) {
162
+ const restore = materializeWorkspaceCursorMcpPrivateConfig(cliWorkspace, userId);
163
+ let handle;
164
+ try {
165
+ handle = runCursorAgentWithPrompt(cliWorkspace, prompt, options);
166
+ } catch (e) {
167
+ restore();
168
+ throw e;
169
+ }
170
+
171
+ let restored = false;
172
+ const safeRestore = () => {
173
+ if (restored) return;
174
+ restored = true;
175
+ restore();
176
+ };
177
+ if (handle?.child?.once) {
178
+ handle.child.once("exit", safeRestore);
179
+ handle.child.once("error", safeRestore);
180
+ }
181
+ const finished = Promise.resolve(handle.finished).finally(safeRestore);
182
+ return { ...handle, finished };
183
+ }
184
+
92
185
  // ─── script 内容注入辅助 ─────────────────────────────────────────────────
93
186
 
94
187
  /**
@@ -203,10 +296,10 @@ export function startComposerAgent(opts) {
203
296
  });
204
297
  }
205
298
 
206
- return runCursorAgentWithPrompt(cliWs, prompt, {
299
+ return runCursorAgentWithPrivateMcp(cliWs, prompt, {
207
300
  ...common,
208
301
  model: model || undefined,
209
- });
302
+ }, opts.agentflowUserId);
210
303
  }
211
304
 
212
305
  // ─── 为单个 agent 步骤构建 prompt ──────────────────────────────────────────
@@ -496,12 +589,12 @@ export async function runComposerPostFlowValidationAndRepair(opts) {
496
589
  setChild(handle.child);
497
590
  await handle.finished;
498
591
  } else {
499
- const handle = runCursorAgentWithPrompt(cliWs, agentPrompt, {
592
+ const handle = runCursorAgentWithPrivateMcp(cliWs, agentPrompt, {
500
593
  onStreamEvent: stepEmit,
501
594
  model: model || undefined,
502
595
  force: Boolean(opts.force),
503
596
  env,
504
- });
597
+ }, opts.agentflowUserId || opts.flowContext?.userId);
505
598
  setChild(handle.child);
506
599
  await handle.finished;
507
600
  }
@@ -769,12 +862,12 @@ export function startComposerMultiStep(opts) {
769
862
  currentChild = handle.child;
770
863
  await handle.finished;
771
864
  } else {
772
- const handle = runCursorAgentWithPrompt(cliWs, agentPrompt, {
865
+ const handle = runCursorAgentWithPrivateMcp(cliWs, agentPrompt, {
773
866
  onStreamEvent: stepEmit,
774
867
  model: model || undefined,
775
868
  force: Boolean(opts.force),
776
869
  env,
777
- });
870
+ }, opts.agentflowUserId || opts.flowContext?.userId);
778
871
  currentChild = handle.child;
779
872
  await handle.finished;
780
873
  }
@@ -1177,6 +1177,32 @@ function uniqueWorkspaceRelPath(workspaceRoot, relPath) {
1177
1177
  return { abs, rel };
1178
1178
  }
1179
1179
 
1180
+ function workspaceDownloadContentDisposition(relPath) {
1181
+ const fallbackName = path.basename(String(relPath || "download")) || "download";
1182
+ const quotedName = fallbackName.replace(/[\r\n"\\]/g, "_");
1183
+ return `attachment; filename="${quotedName}"; filename*=UTF-8''${encodeURIComponent(fallbackName)}`;
1184
+ }
1185
+
1186
+ const WORKSPACE_FILE_SKIP_REL_PREFIXES = [
1187
+ ".workspace/agentflow/worktrees",
1188
+ ".workspace/agentflow/git-repos",
1189
+ ".workspace/agentflow/runBuild",
1190
+ ".workspace/agentflow/composer-logs",
1191
+ ];
1192
+
1193
+ function workspacePathInside(parent, candidate) {
1194
+ const base = path.resolve(parent);
1195
+ const target = path.resolve(candidate);
1196
+ return target === base || target.startsWith(base + path.sep);
1197
+ }
1198
+
1199
+ function shouldSkipWorkspaceFileRelPath(relPath) {
1200
+ const normalized = String(relPath || "").replace(/\\/g, "/").replace(/^\/+/, "");
1201
+ return WORKSPACE_FILE_SKIP_REL_PREFIXES.some((prefix) => (
1202
+ normalized === prefix || normalized.startsWith(`${prefix}/`)
1203
+ ));
1204
+ }
1205
+
1180
1206
  function readWorkspaceFilesRecursive(dir, root, depth = 0, maxDepth = 3, budget = { count: 0 }) {
1181
1207
  if (depth > maxDepth || budget.count > 500) return [];
1182
1208
  let entries;
@@ -1191,6 +1217,7 @@ function readWorkspaceFilesRecursive(dir, root, depth = 0, maxDepth = 3, budget
1191
1217
  if (entry.name.startsWith(".") && entry.name !== ".agents" && entry.name !== ".codex") continue;
1192
1218
  const abs = path.join(dir, entry.name);
1193
1219
  const rel = path.relative(root, abs).replace(/\\/g, "/");
1220
+ if (shouldSkipWorkspaceFileRelPath(rel)) continue;
1194
1221
  if (entry.isDirectory()) {
1195
1222
  if (WORKSPACE_FILE_SKIP_DIRS.has(entry.name)) continue;
1196
1223
  budget.count++;
@@ -1262,6 +1289,43 @@ function normalizeWorkspaceGraphPayload(payload) {
1262
1289
  };
1263
1290
  }
1264
1291
 
1292
+ function workspaceRunTouchedNodeIds(result) {
1293
+ const ids = new Set();
1294
+ for (const id of Array.isArray(result?.order) ? result.order : []) {
1295
+ const text = String(id || "").trim();
1296
+ if (text) ids.add(text);
1297
+ }
1298
+ for (const event of Array.isArray(result?.events) ? result.events : []) {
1299
+ const nodeId = String(event?.nodeId || "").trim();
1300
+ if (nodeId) ids.add(nodeId);
1301
+ for (const displayId of Array.isArray(event?.displayNodeIds) ? event.displayNodeIds : []) {
1302
+ const text = String(displayId || "").trim();
1303
+ if (text) ids.add(text);
1304
+ }
1305
+ }
1306
+ return ids;
1307
+ }
1308
+
1309
+ function mergeWorkspaceRunGraph(currentGraph, runGraph, touchedIds) {
1310
+ const current = normalizeWorkspaceGraphPayload(currentGraph || {});
1311
+ const run = normalizeWorkspaceGraphPayload(runGraph || {});
1312
+ const ids = touchedIds instanceof Set ? touchedIds : new Set(touchedIds || []);
1313
+ const instances = { ...(current.instances || {}) };
1314
+ for (const id of ids) {
1315
+ if (run.instances && Object.prototype.hasOwnProperty.call(run.instances, id)) {
1316
+ instances[id] = run.instances[id];
1317
+ }
1318
+ }
1319
+ return {
1320
+ ...current,
1321
+ version: 1,
1322
+ instances,
1323
+ edges: Array.isArray(current.edges) ? current.edges : [],
1324
+ ui: current.ui && typeof current.ui === "object" ? current.ui : { nodePositions: {} },
1325
+ updatedAt: new Date().toISOString(),
1326
+ };
1327
+ }
1328
+
1265
1329
  function resolveWorkspaceScopeRoot(workspaceRoot, params = {}, opts = {}) {
1266
1330
  const flowId = params.flowId != null ? String(params.flowId).trim() : "";
1267
1331
  if (!flowId) return { root: path.resolve(workspaceRoot), flowId: "", flowSource: "", archived: false };
@@ -1982,6 +2046,39 @@ function workspaceTaskUpstreamText(graph, nodeId, outputs) {
1982
2046
  return workspaceOutputSlotValueForEdge(graph, outputs, contentEdge);
1983
2047
  }
1984
2048
 
2049
+ function workspaceInputValues(graph, nodeId, outputs) {
2050
+ const values = {};
2051
+ const edges = Array.isArray(graph?.edges) ? graph.edges : [];
2052
+ const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
2053
+ const target = instances[String(nodeId || "")] || {};
2054
+ const inputSlots = Array.isArray(target.input) ? target.input : [];
2055
+ for (const edge of edges) {
2056
+ if (String(edge?.target || "") !== String(nodeId)) continue;
2057
+ const index = workspaceHandleIndex(edge?.targetHandle, "input");
2058
+ const slot = inputSlots[index] || null;
2059
+ const name = String(slot?.name || "").trim();
2060
+ if (!name || isWorkspaceSemanticInputSlot(slot)) continue;
2061
+ const value = workspaceOutputSlotValueForEdge(graph, outputs, edge);
2062
+ if (String(value || "").trim()) values[name] = String(value);
2063
+ }
2064
+ for (const slot of inputSlots) {
2065
+ const name = String(slot?.name || "").trim();
2066
+ if (!name || isWorkspaceSemanticInputSlot(slot) || Object.prototype.hasOwnProperty.call(values, name)) continue;
2067
+ const value = workspaceSlotValue(slot);
2068
+ if (String(value || "").trim()) values[name] = String(value);
2069
+ }
2070
+ return values;
2071
+ }
2072
+
2073
+ function workspaceResolveBodyPlaceholders(body, inputValues = {}) {
2074
+ const raw = String(body || "");
2075
+ if (!raw.includes("${")) return raw;
2076
+ return raw.replace(/\$\{([A-Za-z_][A-Za-z0-9_-]*)\}/g, (match, name) => {
2077
+ if (!Object.prototype.hasOwnProperty.call(inputValues, name)) return match;
2078
+ return String(inputValues[name] ?? "");
2079
+ });
2080
+ }
2081
+
1985
2082
  function parseWorkspaceSkillKeys(raw) {
1986
2083
  const text = String(raw || "").trim();
1987
2084
  if (!text) return [];
@@ -2146,9 +2243,9 @@ function workspaceUpdateDirectDisplays(graph, sourceId, content, outputs = null)
2146
2243
  return updated;
2147
2244
  }
2148
2245
 
2149
- function workspaceNodePrompt(graph, nodeId, upstreamText, skillsBlock, mcpBlock = "") {
2246
+ function workspaceNodePrompt(graph, nodeId, upstreamText, skillsBlock, mcpBlock = "", inputValues = {}) {
2150
2247
  const instance = graph.instances[nodeId] || {};
2151
- const body = String(instance.body || "").trim();
2248
+ const body = workspaceResolveBodyPlaceholders(instance.body || "", inputValues).trim();
2152
2249
  const label = String(instance.label || nodeId).trim();
2153
2250
  const downstreamRequirements = workspaceDownstreamDisplayRequirements(graph, nodeId);
2154
2251
  const outputProtocolRequirements = workspaceOutputProtocolRequirements(graph, nodeId);
@@ -2166,6 +2263,72 @@ function workspaceNodePrompt(graph, nodeId, upstreamText, skillsBlock, mcpBlock
2166
2263
  ].filter(Boolean).join("\n");
2167
2264
  }
2168
2265
 
2266
+ function workspaceDefaultWorktreeRoot(scopedRoot) {
2267
+ return path.join(path.resolve(scopedRoot), ".workspace", "agentflow", "worktrees");
2268
+ }
2269
+
2270
+ function workspaceShouldAutoCleanupWorktree(scopedRoot, worktreePath, hasExplicitWorktreePath) {
2271
+ if (hasExplicitWorktreePath || !worktreePath) return false;
2272
+ return workspacePathInside(workspaceDefaultWorktreeRoot(scopedRoot), worktreePath);
2273
+ }
2274
+
2275
+ function workspaceTrackAutoCleanupWorktree(list, item) {
2276
+ const rawTarget = String(item?.worktreePath || "").trim();
2277
+ if (!rawTarget) return;
2278
+ const target = path.resolve(rawTarget);
2279
+ if (list.some((entry) => path.resolve(entry.worktreePath) === target)) return;
2280
+ list.push({ ...item, worktreePath: target });
2281
+ }
2282
+
2283
+ function workspaceUntrackAutoCleanupWorktree(list, worktreePath) {
2284
+ const rawTarget = String(worktreePath || "").trim();
2285
+ if (!rawTarget) return;
2286
+ const target = path.resolve(rawTarget);
2287
+ for (let i = list.length - 1; i >= 0; i -= 1) {
2288
+ if (path.resolve(list[i].worktreePath) === target) list.splice(i, 1);
2289
+ }
2290
+ }
2291
+
2292
+ function workspaceMarkAutoWorktreeCleaned(graph, entry) {
2293
+ const instance = graph?.instances?.[entry.nodeId];
2294
+ if (!instance) return false;
2295
+ let nextInstance = workspaceSetOutputSlot(instance, "worktreePath", "");
2296
+ nextInstance = workspaceSetOutputSlot(nextInstance, "gitContext", "");
2297
+ nextInstance = workspaceSetOutputSlot(nextInstance, "workspaceContext", "");
2298
+ graph.instances[entry.nodeId] = nextInstance;
2299
+ return true;
2300
+ }
2301
+
2302
+ function workspaceCleanupAutoWorktrees(list, graph, emit) {
2303
+ for (const entry of [...list].reverse()) {
2304
+ try {
2305
+ const result = unloadGitWorktree({
2306
+ repoPath: entry.repoPath,
2307
+ worktreePath: entry.worktreePath,
2308
+ force: false,
2309
+ prune: true,
2310
+ });
2311
+ emit({
2312
+ type: "natural",
2313
+ kind: "status",
2314
+ nodeId: entry.nodeId,
2315
+ text: `已清理临时 worktree:${result.worktreePath}`,
2316
+ });
2317
+ if (workspaceMarkAutoWorktreeCleaned(graph, entry)) {
2318
+ emit({ type: "graph", nodeId: entry.nodeId, graph });
2319
+ }
2320
+ } catch (e) {
2321
+ emit({
2322
+ type: "natural",
2323
+ kind: "warning",
2324
+ nodeId: entry.nodeId,
2325
+ text: `临时 worktree 未自动清理:${entry.worktreePath}\n原因:${e?.message || String(e)}`,
2326
+ });
2327
+ }
2328
+ }
2329
+ list.splice(0, list.length);
2330
+ }
2331
+
2169
2332
  async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts = {}) {
2170
2333
  const graph = normalizeWorkspaceGraphPayload(payload.graph || {});
2171
2334
  const runNodeId = String(payload?.runNodeId || "").trim();
@@ -2223,7 +2386,9 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
2223
2386
  };
2224
2387
  let cwd = scopedRoot;
2225
2388
  const modelKey = typeof payload?.model === "string" ? payload.model.trim() : "";
2389
+ const autoCleanupWorktrees = [];
2226
2390
 
2391
+ try {
2227
2392
  for (const nodeId of order) {
2228
2393
  throwIfAborted();
2229
2394
  const instance = graph.instances[nodeId];
@@ -2408,13 +2573,23 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
2408
2573
  (gitContext?.repoPath ? path.resolve(gitContext.repoPath) : "");
2409
2574
  if (!repoPath) throw new Error("Load Worktree requires repoPath");
2410
2575
  const branch = workspaceSlotValue(workspaceSlotByName(instance, "branch")).trim();
2411
- const rawWorktreePath = workspaceSlotValue(workspaceSlotByName(instance, "worktreePath")).trim();
2576
+ const worktreeInputSlot = (Array.isArray(instance.input) ? instance.input : [])
2577
+ .find((slot) => String(slot?.name || "") === "worktreePath") || null;
2578
+ const rawWorktreePath = workspaceSlotValue(worktreeInputSlot || workspaceSlotByName(instance, "worktreePath")).trim();
2412
2579
  const worktreePath = rawWorktreePath ? workspaceResolvePath(cwd, rawWorktreePath) : (gitContext?.worktreePath ? path.resolve(gitContext.worktreePath) : "");
2580
+ const hasExplicitWorktreePath = Boolean(rawWorktreePath) || Boolean(gitContext?.worktreePath);
2413
2581
  const previousCwd = cwd;
2414
2582
  const force = ["true", "1", "yes", "on"].includes(workspaceSlotValue(workspaceSlotByName(instance, "force")).trim().toLowerCase());
2415
2583
  const pruneMissingRaw = workspaceSlotValue(workspaceSlotByName(instance, "pruneMissing")).trim().toLowerCase();
2416
2584
  const pruneMissing = pruneMissingRaw !== "false";
2417
2585
  const result = loadGitWorktree({ repoPath, branch, worktreePath, pipelineWorkspace: scopedRoot, force, pruneMissing });
2586
+ if (workspaceShouldAutoCleanupWorktree(scopedRoot, result.worktreePath, hasExplicitWorktreePath)) {
2587
+ workspaceTrackAutoCleanupWorktree(autoCleanupWorktrees, {
2588
+ nodeId,
2589
+ repoPath: result.repoRoot,
2590
+ worktreePath: result.worktreePath,
2591
+ });
2592
+ }
2418
2593
  const outGitContext = buildGitContext({
2419
2594
  repoPath: result.repoRoot,
2420
2595
  worktreePath: result.worktreePath,
@@ -2457,6 +2632,7 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
2457
2632
  const pruneRaw = workspaceSlotValue(workspaceSlotByName(instance, "prune")).trim().toLowerCase();
2458
2633
  const prune = pruneRaw !== "false";
2459
2634
  const result = unloadGitWorktree({ repoPath, worktreePath, force, prune });
2635
+ workspaceUntrackAutoCleanupWorktree(autoCleanupWorktrees, result.worktreePath);
2460
2636
  const previousContext = workspaceContext?.previous && typeof workspaceContext.previous === "object" ? workspaceContext.previous : null;
2461
2637
  cwd = previousContext?.cwd ? path.resolve(String(previousContext.cwd)) : scopedRoot;
2462
2638
  let nextInstance = workspaceSetOutputSlot(instance, "removed", "true");
@@ -2513,14 +2689,15 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
2513
2689
 
2514
2690
  const prepareStartedAt = Date.now();
2515
2691
  const upstreamText = workspaceTaskUpstreamText(graph, nodeId, outputs);
2516
- const body = String(instance.body || "").trim();
2692
+ const inputValues = workspaceInputValues(graph, nodeId, outputs);
2693
+ const body = workspaceResolveBodyPlaceholders(instance.body || "", inputValues).trim();
2517
2694
  if (defId === "agent_subAgent" && !body && !String(upstreamText || "").trim()) {
2518
2695
  throw new Error(`Workspace node ${nodeId} has no task. Fill the node body or connect upstream text.`);
2519
2696
  }
2520
2697
  const upstreamSkillBlocks = workspaceUpstreamSkillBlocks(graph, nodeId, outputs);
2521
2698
  const promptSkillsBlock = mergeWorkspaceSkillBlocks(upstreamSkillBlocks, upstreamSkillBlocks ? "" : loadSkillsBlockForKeys(fallbackSelectedSkillKeys));
2522
2699
  const promptMcpBlock = workspaceUpstreamMcpBlocks(graph, nodeId, outputs);
2523
- const prompt = workspaceNodePrompt(graph, nodeId, upstreamText, promptSkillsBlock, promptMcpBlock);
2700
+ const prompt = workspaceNodePrompt(graph, nodeId, upstreamText, promptSkillsBlock, promptMcpBlock, inputValues);
2524
2701
  emitTiming(nodeId, "prepare-agent-prompt", prepareStartedAt, { promptChars: prompt.length, upstreamChars: String(upstreamText || "").length, skillsChars: promptSkillsBlock.length, mcpChars: promptMcpBlock.length });
2525
2702
  emit({ type: "natural", kind: "prompt", nodeId, text: prompt });
2526
2703
  let content = "";
@@ -2581,6 +2758,9 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
2581
2758
  if (slotUpdate.changed || updatedDisplays.length) emit({ type: "graph", nodeId, displayNodeIds: updatedDisplays, graph });
2582
2759
  emit({ type: "node-done", nodeId, definitionId: defId });
2583
2760
  }
2761
+ } finally {
2762
+ workspaceCleanupAutoWorktrees(autoCleanupWorktrees, graph, emit);
2763
+ }
2584
2764
  if (pauseNodeIds.length > 0) {
2585
2765
  emit({ type: "paused", nodeIds: pauseNodeIds, message: `Workspace run paused at ${pauseNodeIds.join(", ")}` });
2586
2766
  }
@@ -3402,8 +3582,11 @@ export function startUiServer({
3402
3582
  signal: controller.signal,
3403
3583
  onActiveChild: setActiveChild,
3404
3584
  });
3405
- fs.writeFileSync(graphPath, JSON.stringify(result.graph, null, 2) + "\n", "utf-8");
3406
- writeEvent({ type: "done", ok: true, path: graphPath, graph: result.graph, order: result.order, pauseNodeIds: result.pauseNodeIds || [] });
3585
+ const currentGraph = readWorkspaceGraph(scoped.root).graph;
3586
+ const touchedIds = workspaceRunTouchedNodeIds(result);
3587
+ const mergedGraph = mergeWorkspaceRunGraph(currentGraph, result.graph, touchedIds);
3588
+ fs.writeFileSync(graphPath, JSON.stringify(mergedGraph, null, 2) + "\n", "utf-8");
3589
+ writeEvent({ type: "done", ok: true, path: graphPath, graph: mergedGraph, order: result.order, touchedNodeIds: Array.from(touchedIds), pauseNodeIds: result.pauseNodeIds || [] });
3407
3590
  res.end();
3408
3591
  } catch (e) {
3409
3592
  if (isWorkspaceRunAbortError(e) || controller.signal.aborted) {
@@ -3423,8 +3606,11 @@ export function startUiServer({
3423
3606
  onActiveChild: setActiveChild,
3424
3607
  });
3425
3608
  const graphPath = workspaceGraphPath(scoped.root);
3426
- fs.writeFileSync(graphPath, JSON.stringify(result.graph, null, 2) + "\n", "utf-8");
3427
- json(res, 200, { ok: true, path: graphPath, ...result });
3609
+ const currentGraph = readWorkspaceGraph(scoped.root).graph;
3610
+ const touchedIds = workspaceRunTouchedNodeIds(result);
3611
+ const mergedGraph = mergeWorkspaceRunGraph(currentGraph, result.graph, touchedIds);
3612
+ fs.writeFileSync(graphPath, JSON.stringify(mergedGraph, null, 2) + "\n", "utf-8");
3613
+ json(res, 200, { ok: true, path: graphPath, ...result, graph: mergedGraph, touchedNodeIds: Array.from(touchedIds) });
3428
3614
  } catch (e) {
3429
3615
  if (isWorkspaceRunAbortError(e) || controller.signal.aborted) {
3430
3616
  json(res, 200, { ok: false, stopped: true, message: "Workspace run stopped" });
@@ -3504,7 +3690,7 @@ export function startUiServer({
3504
3690
  json(res, 400, { error: scoped.error });
3505
3691
  return;
3506
3692
  }
3507
- const { abs } = resolveWorkspaceFilePath(scoped.root, url.searchParams.get("path") || "");
3693
+ const { abs, rel } = resolveWorkspaceFilePath(scoped.root, url.searchParams.get("path") || "");
3508
3694
  if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) {
3509
3695
  json(res, 404, { error: "File not found" });
3510
3696
  return;
@@ -3512,11 +3698,15 @@ export function startUiServer({
3512
3698
  const ext = path.extname(abs).toLowerCase();
3513
3699
  const type = MIME[ext] || "application/octet-stream";
3514
3700
  const data = fs.readFileSync(abs);
3515
- res.writeHead(200, {
3701
+ const headers = {
3516
3702
  "Content-Type": type,
3517
3703
  "Content-Length": data.length,
3518
3704
  "Cache-Control": "no-store",
3519
- });
3705
+ };
3706
+ if (url.searchParams.get("download") === "1") {
3707
+ headers["Content-Disposition"] = workspaceDownloadContentDisposition(rel);
3708
+ }
3709
+ res.writeHead(200, headers);
3520
3710
  res.end(data);
3521
3711
  } catch (e) {
3522
3712
  json(res, /traversal/i.test(String(e.message || e)) ? 403 : 500, { error: (e && e.message) || String(e) });
@@ -15,6 +15,7 @@ output:
15
15
  - type: text
16
16
  name: content
17
17
  default: ""
18
+ showOnNode: true
18
19
  - type: node
19
20
  name: next
20
21
  default: ""
@@ -23,7 +23,7 @@ output:
23
23
  - type: text
24
24
  name: content
25
25
  default: ""
26
- showOnNode: false
26
+ showOnNode: true
27
27
  - type: node
28
28
  name: next
29
29
  default: ""
@@ -23,7 +23,7 @@ output:
23
23
  - type: text
24
24
  name: content
25
25
  default: ""
26
- showOnNode: false
26
+ showOnNode: true
27
27
  - type: node
28
28
  name: next
29
29
  default: ""
@@ -27,7 +27,7 @@ output:
27
27
  - type: text
28
28
  name: src
29
29
  default: ""
30
- showOnNode: false
30
+ showOnNode: true
31
31
  - type: node
32
32
  name: next
33
33
  default: ""
@@ -15,6 +15,7 @@ output:
15
15
  - type: text
16
16
  name: content
17
17
  default: ""
18
+ showOnNode: true
18
19
  - type: node
19
20
  name: next
20
21
  default: ""
@@ -15,6 +15,7 @@ output:
15
15
  - type: text
16
16
  name: content
17
17
  default: ""
18
+ showOnNode: true
18
19
  - type: node
19
20
  name: next
20
21
  default: ""
@@ -23,7 +23,7 @@ output:
23
23
  - type: text
24
24
  name: content
25
25
  default: ""
26
- showOnNode: false
26
+ showOnNode: true
27
27
  - type: node
28
28
  name: next
29
29
  default: ""