ofiere-openclaw-plugin 4.0.0 → 4.2.0

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/tools.ts +224 -158
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ofiere-openclaw-plugin",
3
- "version": "4.0.0",
3
+ "version": "4.2.0",
4
4
  "type": "module",
5
5
  "description": "OpenClaw plugin for Ofiere PM - 10 meta-tools with 13-action workflow mastery covering tasks, agents, projects, scheduling, knowledge, workflows, notifications, memory, prompts, and constellation agent architecture",
6
6
  "keywords": ["openclaw", "ofiere", "project-management", "agents", "plugin"],
package/src/tools.ts CHANGED
@@ -1141,6 +1141,17 @@ function registerKnowledgeOps(
1141
1141
  });
1142
1142
  }
1143
1143
 
1144
+ // ─── Workflow Mutation Serialization Queue ────────────────────────────────────
1145
+ // Prevents parallel mutations on the same workflow from racing (last-write-wins).
1146
+ // Each workflow gets a sequential promise chain — mutations queue behind previous ones.
1147
+ const _wfLockChain = new Map<string, Promise<any>>();
1148
+ function sequentialWorkflowOp<T>(wfId: string, fn: () => Promise<T>): Promise<T> {
1149
+ const prev = _wfLockChain.get(wfId) || Promise.resolve();
1150
+ const next = prev.catch(() => {}).then(fn);
1151
+ _wfLockChain.set(wfId, next.catch(() => {}));
1152
+ return next;
1153
+ }
1154
+
1144
1155
  // ═══════════════════════════════════════════════════════════════════════════════
1145
1156
  // META-TOOL 6: OFIERE_WORKFLOW_OPS — Workflow Management & Execution
1146
1157
  // ═══════════════════════════════════════════════════════════════════════════════
@@ -1219,7 +1230,7 @@ function registerWorkflowOps(
1219
1230
  id: { type: "string", description: "Node ID (auto-generated if omitted)" },
1220
1231
  type: { type: "string", enum: ["manual_trigger", "webhook_trigger", "agent_step", "formatter_step", "http_request", "task_call", "variable_set", "condition", "human_approval", "delay", "loop", "convergence", "output", "checkpoint", "note"] },
1221
1232
  position: { type: "object", properties: { x: { type: "number" }, y: { type: "number" } } },
1222
- data: { type: "object", description: "Node config — always include a 'label' field. See NODE TYPES above for all configurable fields per type." },
1233
+ data: { type: "object", additionalProperties: true, description: "Node config — always include a 'label' field. See NODE TYPES above for all configurable fields per type." },
1223
1234
  },
1224
1235
  },
1225
1236
  description: "Workflow graph nodes",
@@ -1247,11 +1258,11 @@ function registerWorkflowOps(
1247
1258
  description: "Single node definition for insert_node_between. { type, data: { label, ... } }",
1248
1259
  properties: {
1249
1260
  type: { type: "string" },
1250
- data: { type: "object" },
1261
+ data: { type: "object", additionalProperties: true },
1251
1262
  position: { type: "object", properties: { x: { type: "number" }, y: { type: "number" } } },
1252
1263
  },
1253
1264
  },
1254
- data: { type: "object", description: "Data fields to merge into a node (for update_node). Only specified fields are changed." },
1265
+ data: { type: "object", additionalProperties: true, description: "Data fields to merge into a node (for update_node). Only specified fields are changed." },
1255
1266
  source_node_id: { type: "string", description: "Source node ID for insert_node_between" },
1256
1267
  target_node_id: { type: "string", description: "Target node ID for insert_node_between" },
1257
1268
  steps: { type: "array", items: { type: "object" }, description: "Legacy V1 step definitions" },
@@ -1378,8 +1389,37 @@ function registerWorkflowOps(
1378
1389
  finalNodes.unshift(triggerNode);
1379
1390
  }
1380
1391
 
1381
- // Build edges
1382
- let finalEdges = ((params.edges as any[]) || []).map((e: any, i: number) => normalizeEdge(e, i));
1392
+ // Build edges — remap IDs from pre-normalization to post-normalization
1393
+ const idRemap = new Map<string, string>();
1394
+ rawNodes.forEach((raw, i) => {
1395
+ if (raw.id && finalNodes[hasTrigger ? i : i + 1]?.id !== raw.id) {
1396
+ idRemap.set(raw.id, finalNodes[hasTrigger ? i : i + 1].id);
1397
+ }
1398
+ });
1399
+
1400
+ let finalEdges: any[];
1401
+ const suppliedEdges = (params.edges as any[]) || [];
1402
+
1403
+ if (suppliedEdges.length > 0) {
1404
+ // Remap source/target IDs in user-supplied edges
1405
+ finalEdges = suppliedEdges.map((e: any, i: number) => {
1406
+ const remapped = {
1407
+ ...e,
1408
+ source: idRemap.get(e.source) || e.source,
1409
+ target: idRemap.get(e.target) || e.target,
1410
+ };
1411
+ return normalizeEdge(remapped, i);
1412
+ });
1413
+ } else {
1414
+ // No edges supplied — auto-chain all nodes in order
1415
+ finalEdges = [];
1416
+ for (let i = 0; i < finalNodes.length - 1; i++) {
1417
+ finalEdges.push(normalizeEdge({
1418
+ source: finalNodes[i].id,
1419
+ target: finalNodes[i + 1].id,
1420
+ }, i));
1421
+ }
1422
+ }
1383
1423
 
1384
1424
  // Auto-wire trigger to first non-trigger node if no edge connects from trigger
1385
1425
  if (!hasTrigger && finalNodes.length > 1) {
@@ -1387,11 +1427,10 @@ function registerWorkflowOps(
1387
1427
  const firstStepId = finalNodes[1].id;
1388
1428
  const triggerHasEdge = finalEdges.some((e: any) => e.source === triggerId);
1389
1429
  if (!triggerHasEdge) {
1390
- finalEdges.unshift({
1391
- id: `edge-trigger-${Date.now()}`,
1430
+ finalEdges.unshift(normalizeEdge({
1392
1431
  source: triggerId,
1393
1432
  target: firstStepId,
1394
- });
1433
+ }, finalEdges.length));
1395
1434
  }
1396
1435
  }
1397
1436
 
@@ -1442,7 +1481,7 @@ function registerWorkflowOps(
1442
1481
  if (!wfId) return err("Missing required: workflow_id");
1443
1482
  const { data, error } = await supabase.from("workflow_runs").select("*")
1444
1483
  .eq("workflow_id", wfId)
1445
- .order("created_at", { ascending: false })
1484
+ .order("started_at", { ascending: false })
1446
1485
  .limit((params.limit as number) || 20);
1447
1486
  if (error) return err(error.message);
1448
1487
  return ok({ runs: data || [], count: (data || []).length });
@@ -1454,6 +1493,7 @@ function registerWorkflowOps(
1454
1493
  const { error } = await supabase.from("workflow_runs").insert({
1455
1494
  id: runId,
1456
1495
  workflow_id: wfId,
1496
+ user_id: userId,
1457
1497
  status: "running",
1458
1498
  started_at: new Date().toISOString(),
1459
1499
  trigger_type: "agent",
@@ -1470,40 +1510,40 @@ function registerWorkflowOps(
1470
1510
  const newNodes = params.nodes as any[];
1471
1511
  if (!newNodes || !Array.isArray(newNodes) || newNodes.length === 0) return err("Missing required: nodes[] (array of node definitions)");
1472
1512
 
1473
- const { wf, error: fetchErr } = await fetchWorkflow(wfId);
1474
- if (fetchErr || !wf) return err(fetchErr || "Workflow not found");
1513
+ return sequentialWorkflowOp(wfId, async () => {
1514
+ const { wf, error: fetchErr } = await fetchWorkflow(wfId);
1515
+ if (fetchErr || !wf) return err(fetchErr || "Workflow not found");
1475
1516
 
1476
- const existingNodes = (wf.nodes as any[]) || [];
1477
- const existingEdges = (wf.edges as any[]) || [];
1517
+ const existingNodes = (wf.nodes as any[]) || [];
1518
+ const existingEdges = (wf.edges as any[]) || [];
1478
1519
 
1479
- // Find the max Y position to place new nodes below existing ones
1480
- const maxY = existingNodes.reduce((max: number, n: any) => Math.max(max, n.position?.y || 0), 0);
1481
- const normalized = newNodes.map((n, i) => {
1482
- const node = normalizeNode(n, existingNodes.length + i);
1483
- if (!n.position) {
1484
- node.position = { x: 250, y: maxY + 120 + i * 150 };
1485
- }
1486
- return node;
1487
- });
1520
+ const maxY = existingNodes.reduce((max: number, n: any) => Math.max(max, n.position?.y || 0), 0);
1521
+ const normalized = newNodes.map((n, i) => {
1522
+ const node = normalizeNode(n, existingNodes.length + i);
1523
+ if (!n.position) {
1524
+ node.position = { x: 250, y: maxY + 120 + i * 150 };
1525
+ }
1526
+ return node;
1527
+ });
1488
1528
 
1489
- const allNodes = [...existingNodes, ...normalized];
1529
+ const allNodes = [...existingNodes, ...normalized];
1490
1530
 
1491
- // Also add any edges provided
1492
- let allEdges = existingEdges;
1493
- if (params.edges && Array.isArray(params.edges)) {
1494
- const newEdges = (params.edges as any[]).map((e: any, i: number) => normalizeEdge(e, existingEdges.length + i));
1495
- allEdges = [...existingEdges, ...newEdges];
1496
- }
1531
+ let allEdges = existingEdges;
1532
+ if (params.edges && Array.isArray(params.edges)) {
1533
+ const newEdges = (params.edges as any[]).map((e: any, i: number) => normalizeEdge(e, existingEdges.length + i));
1534
+ allEdges = [...existingEdges, ...newEdges];
1535
+ }
1497
1536
 
1498
- const { wf: saved, error: saveErr } = await saveGraph(wfId, allNodes, allEdges);
1499
- if (saveErr) return err(saveErr);
1537
+ const { wf: saved, error: saveErr } = await saveGraph(wfId, allNodes, allEdges);
1538
+ if (saveErr) return err(saveErr);
1500
1539
 
1501
- return ok({
1502
- message: `Added ${normalized.length} node(s) to workflow`,
1503
- added_nodes: normalized.map((n: any) => ({ id: n.id, type: n.type, label: n.data?.label })),
1504
- total_nodes: allNodes.length,
1505
- total_edges: allEdges.length,
1506
- workflow: saved,
1540
+ return ok({
1541
+ message: `Added ${normalized.length} node(s) to workflow`,
1542
+ added_nodes: normalized.map((n: any) => ({ id: n.id, type: n.type, label: n.data?.label })),
1543
+ total_nodes: allNodes.length,
1544
+ total_edges: allEdges.length,
1545
+ workflow: saved,
1546
+ });
1507
1547
  });
1508
1548
  }
1509
1549
 
@@ -1515,30 +1555,31 @@ function registerWorkflowOps(
1515
1555
  const dataUpdate = params.data as Record<string, any>;
1516
1556
  if (!dataUpdate || typeof dataUpdate !== "object") return err("Missing required: data (object with fields to update)");
1517
1557
 
1518
- const { wf, error: fetchErr } = await fetchWorkflow(wfId);
1519
- if (fetchErr || !wf) return err(fetchErr || "Workflow not found");
1558
+ return sequentialWorkflowOp(wfId, async () => {
1559
+ const { wf, error: fetchErr } = await fetchWorkflow(wfId);
1560
+ if (fetchErr || !wf) return err(fetchErr || "Workflow not found");
1520
1561
 
1521
- const nodes = (wf.nodes as any[]) || [];
1522
- const nodeIndex = nodes.findIndex((n: any) => n.id === nodeId);
1523
- if (nodeIndex === -1) return err(`Node "${nodeId}" not found in workflow. Use action "get" to see all node IDs.`);
1562
+ const nodes = (wf.nodes as any[]) || [];
1563
+ const nodeIndex = nodes.findIndex((n: any) => n.id === nodeId);
1564
+ if (nodeIndex === -1) return err(`Node "${nodeId}" not found in workflow. Use action "get" to see all node IDs.`);
1524
1565
 
1525
- // Merge new data into existing node data
1526
- const existingData = nodes[nodeIndex].data || {};
1527
- nodes[nodeIndex].data = { ...existingData, ...dataUpdate };
1566
+ // Merge new data into existing node data (preserves untouched fields)
1567
+ const existingData = nodes[nodeIndex].data || {};
1568
+ nodes[nodeIndex].data = { ...existingData, ...dataUpdate };
1528
1569
 
1529
- // Also update position if provided
1530
- if (params.node && typeof params.node === "object" && (params.node as any).position) {
1531
- nodes[nodeIndex].position = (params.node as any).position;
1532
- }
1570
+ if (params.node && typeof params.node === "object" && (params.node as any).position) {
1571
+ nodes[nodeIndex].position = (params.node as any).position;
1572
+ }
1533
1573
 
1534
- const { wf: saved, error: saveErr } = await saveGraph(wfId, nodes, wf.edges || []);
1535
- if (saveErr) return err(saveErr);
1574
+ const { wf: saved, error: saveErr } = await saveGraph(wfId, nodes, wf.edges || []);
1575
+ if (saveErr) return err(saveErr);
1536
1576
 
1537
- return ok({
1538
- message: `Node "${nodeId}" updated`,
1539
- node: { id: nodes[nodeIndex].id, type: nodes[nodeIndex].type, data: nodes[nodeIndex].data },
1540
- fields_updated: Object.keys(dataUpdate),
1541
- workflow: saved,
1577
+ return ok({
1578
+ message: `Node "${nodeId}" updated`,
1579
+ node: { id: nodes[nodeIndex].id, type: nodes[nodeIndex].type, data: nodes[nodeIndex].data },
1580
+ fields_updated: Object.keys(dataUpdate),
1581
+ workflow: saved,
1582
+ });
1542
1583
  });
1543
1584
  }
1544
1585
 
@@ -1548,26 +1589,33 @@ function registerWorkflowOps(
1548
1589
  const nodeIds = params.node_ids as string[];
1549
1590
  if (!nodeIds || !Array.isArray(nodeIds) || nodeIds.length === 0) return err("Missing required: node_ids[] (array of node IDs to delete)");
1550
1591
 
1551
- const { wf, error: fetchErr } = await fetchWorkflow(wfId);
1552
- if (fetchErr || !wf) return err(fetchErr || "Workflow not found");
1592
+ return sequentialWorkflowOp(wfId, async () => {
1593
+ const { wf, error: fetchErr } = await fetchWorkflow(wfId);
1594
+ if (fetchErr || !wf) return err(fetchErr || "Workflow not found");
1553
1595
 
1554
- const nodeIdSet = new Set(nodeIds);
1555
- const nodes = ((wf.nodes as any[]) || []).filter((n: any) => !nodeIdSet.has(n.id));
1556
- // Also remove edges connected to deleted nodes
1557
- const edges = ((wf.edges as any[]) || []).filter((e: any) => !nodeIdSet.has(e.source) && !nodeIdSet.has(e.target));
1596
+ // Track which IDs actually exist vs. not found
1597
+ const existingNodeIdSet = new Set(((wf.nodes as any[]) || []).map((n: any) => n.id));
1598
+ const actuallyDeletedIds = nodeIds.filter(id => existingNodeIdSet.has(id));
1599
+ const notFoundIds = nodeIds.filter(id => !existingNodeIdSet.has(id));
1558
1600
 
1559
- const removedNodeCount = ((wf.nodes as any[]) || []).length - nodes.length;
1560
- const removedEdgeCount = ((wf.edges as any[]) || []).length - edges.length;
1601
+ const nodeIdSet = new Set(nodeIds);
1602
+ const nodes = ((wf.nodes as any[]) || []).filter((n: any) => !nodeIdSet.has(n.id));
1603
+ // Also remove edges connected to deleted nodes
1604
+ const edges = ((wf.edges as any[]) || []).filter((e: any) => !nodeIdSet.has(e.source) && !nodeIdSet.has(e.target));
1561
1605
 
1562
- const { wf: saved, error: saveErr } = await saveGraph(wfId, nodes, edges);
1563
- if (saveErr) return err(saveErr);
1606
+ const removedEdgeCount = ((wf.edges as any[]) || []).length - edges.length;
1564
1607
 
1565
- return ok({
1566
- message: `Deleted ${removedNodeCount} node(s) and ${removedEdgeCount} connected edge(s)`,
1567
- deleted_node_ids: nodeIds,
1568
- remaining_nodes: nodes.length,
1569
- remaining_edges: edges.length,
1570
- workflow: saved,
1608
+ const { wf: saved, error: saveErr } = await saveGraph(wfId, nodes, edges);
1609
+ if (saveErr) return err(saveErr);
1610
+
1611
+ return ok({
1612
+ message: `Deleted ${actuallyDeletedIds.length} node(s) and ${removedEdgeCount} connected edge(s)`,
1613
+ deleted_node_ids: actuallyDeletedIds,
1614
+ not_found_ids: notFoundIds.length > 0 ? notFoundIds : undefined,
1615
+ remaining_nodes: nodes.length,
1616
+ remaining_edges: edges.length,
1617
+ workflow: saved,
1618
+ });
1571
1619
  });
1572
1620
  }
1573
1621
 
@@ -1577,33 +1625,49 @@ function registerWorkflowOps(
1577
1625
  const newEdges = params.edges as any[];
1578
1626
  if (!newEdges || !Array.isArray(newEdges) || newEdges.length === 0) return err("Missing required: edges[] (array of edge definitions)");
1579
1627
 
1580
- const { wf, error: fetchErr } = await fetchWorkflow(wfId);
1581
- if (fetchErr || !wf) return err(fetchErr || "Workflow not found");
1628
+ return sequentialWorkflowOp(wfId, async () => {
1629
+ const { wf, error: fetchErr } = await fetchWorkflow(wfId);
1630
+ if (fetchErr || !wf) return err(fetchErr || "Workflow not found");
1582
1631
 
1583
- const existingEdges = (wf.edges as any[]) || [];
1584
- const nodeIds = new Set(((wf.nodes as any[]) || []).map((n: any) => n.id));
1632
+ const existingEdges = (wf.edges as any[]) || [];
1633
+ const nodesList = (wf.nodes as any[]) || [];
1634
+ const nodeIdSet = new Set(nodesList.map((n: any) => n.id));
1635
+ const nodesMap = new Map(nodesList.map((n: any) => [n.id, n]));
1585
1636
 
1586
- // Validate that source/target nodes exist
1587
- const normalized = newEdges.map((e: any, i: number) => {
1588
- if (!nodeIds.has(e.source)) throw new Error(`Source node "${e.source}" not found in workflow`);
1589
- if (!nodeIds.has(e.target)) throw new Error(`Target node "${e.target}" not found in workflow`);
1590
- return normalizeEdge(e, existingEdges.length + i);
1591
- });
1637
+ try {
1638
+ // Validate source/target existence AND sourceHandle semantics
1639
+ const normalized = newEdges.map((e: any, i: number) => {
1640
+ if (!nodeIdSet.has(e.source)) throw new Error(`Source node "${e.source}" not found in workflow`);
1641
+ if (!nodeIdSet.has(e.target)) throw new Error(`Target node "${e.target}" not found in workflow`);
1642
+
1643
+ // Validate that condition/loop handles are only used on the correct node types
1644
+ if (e.sourceHandle) {
1645
+ const srcNode = nodesMap.get(e.source);
1646
+ if (e.sourceHandle.startsWith("condition-") && srcNode?.type !== "condition") {
1647
+ throw new Error(`sourceHandle "${e.sourceHandle}" is only valid on condition nodes, but source "${e.source}" is type "${srcNode?.type}"`);
1648
+ }
1649
+ if ((e.sourceHandle === "loop_body" || e.sourceHandle === "done") && srcNode?.type !== "loop") {
1650
+ throw new Error(`sourceHandle "${e.sourceHandle}" is only valid on loop nodes, but source "${e.source}" is type "${srcNode?.type}"`);
1651
+ }
1652
+ }
1653
+
1654
+ return normalizeEdge(e, existingEdges.length + i);
1655
+ });
1592
1656
 
1593
- try {
1594
- const allEdges = [...existingEdges, ...normalized];
1595
- const { wf: saved, error: saveErr } = await saveGraph(wfId, wf.nodes || [], allEdges);
1596
- if (saveErr) return err(saveErr);
1657
+ const allEdges = [...existingEdges, ...normalized];
1658
+ const { wf: saved, error: saveErr } = await saveGraph(wfId, wf.nodes || [], allEdges);
1659
+ if (saveErr) return err(saveErr);
1597
1660
 
1598
- return ok({
1599
- message: `Added ${normalized.length} edge(s)`,
1600
- added_edges: normalized.map((e: any) => ({ id: e.id, source: e.source, target: e.target })),
1601
- total_edges: allEdges.length,
1602
- workflow: saved,
1603
- });
1604
- } catch (e: any) {
1605
- return err(e.message);
1606
- }
1661
+ return ok({
1662
+ message: `Added ${normalized.length} edge(s)`,
1663
+ added_edges: normalized.map((e: any) => ({ id: e.id, source: e.source, target: e.target })),
1664
+ total_edges: allEdges.length,
1665
+ workflow: saved,
1666
+ });
1667
+ } catch (e: any) {
1668
+ return err(e.message);
1669
+ }
1670
+ });
1607
1671
  }
1608
1672
 
1609
1673
  case "delete_edges": {
@@ -1612,21 +1676,28 @@ function registerWorkflowOps(
1612
1676
  const edgeIds = params.edge_ids as string[];
1613
1677
  if (!edgeIds || !Array.isArray(edgeIds) || edgeIds.length === 0) return err("Missing required: edge_ids[] (array of edge IDs to delete)");
1614
1678
 
1615
- const { wf, error: fetchErr } = await fetchWorkflow(wfId);
1616
- if (fetchErr || !wf) return err(fetchErr || "Workflow not found");
1679
+ return sequentialWorkflowOp(wfId, async () => {
1680
+ const { wf, error: fetchErr } = await fetchWorkflow(wfId);
1681
+ if (fetchErr || !wf) return err(fetchErr || "Workflow not found");
1617
1682
 
1618
- const edgeIdSet = new Set(edgeIds);
1619
- const edges = ((wf.edges as any[]) || []).filter((e: any) => !edgeIdSet.has(e.id));
1620
- const removedCount = ((wf.edges as any[]) || []).length - edges.length;
1683
+ // Track which IDs actually exist vs. not found
1684
+ const existingEdgeIds = new Set(((wf.edges as any[]) || []).map((e: any) => e.id));
1685
+ const actuallyDeletedIds = edgeIds.filter(id => existingEdgeIds.has(id));
1686
+ const notFoundIds = edgeIds.filter(id => !existingEdgeIds.has(id));
1621
1687
 
1622
- const { wf: saved, error: saveErr } = await saveGraph(wfId, wf.nodes || [], edges);
1623
- if (saveErr) return err(saveErr);
1688
+ const edgeIdSet = new Set(edgeIds);
1689
+ const edges = ((wf.edges as any[]) || []).filter((e: any) => !edgeIdSet.has(e.id));
1624
1690
 
1625
- return ok({
1626
- message: `Deleted ${removedCount} edge(s)`,
1627
- deleted_edge_ids: edgeIds,
1628
- remaining_edges: edges.length,
1629
- workflow: saved,
1691
+ const { wf: saved, error: saveErr } = await saveGraph(wfId, wf.nodes || [], edges);
1692
+ if (saveErr) return err(saveErr);
1693
+
1694
+ return ok({
1695
+ message: `Deleted ${actuallyDeletedIds.length} edge(s)`,
1696
+ deleted_edge_ids: actuallyDeletedIds,
1697
+ not_found_ids: notFoundIds.length > 0 ? notFoundIds : undefined,
1698
+ remaining_edges: edges.length,
1699
+ workflow: saved,
1700
+ });
1630
1701
  });
1631
1702
  }
1632
1703
 
@@ -1640,66 +1711,61 @@ function registerWorkflowOps(
1640
1711
  if (!targetId) return err("Missing required: target_node_id");
1641
1712
  if (!newNodeDef || typeof newNodeDef !== "object") return err("Missing required: node (the node definition to insert)");
1642
1713
 
1643
- const { wf, error: fetchErr } = await fetchWorkflow(wfId);
1644
- if (fetchErr || !wf) return err(fetchErr || "Workflow not found");
1714
+ return sequentialWorkflowOp(wfId, async () => {
1715
+ const { wf, error: fetchErr } = await fetchWorkflow(wfId);
1716
+ if (fetchErr || !wf) return err(fetchErr || "Workflow not found");
1645
1717
 
1646
- const nodes = (wf.nodes as any[]) || [];
1647
- const edges = (wf.edges as any[]) || [];
1718
+ const nodes = (wf.nodes as any[]) || [];
1719
+ const edges = (wf.edges as any[]) || [];
1648
1720
 
1649
- // Find the source and target nodes
1650
- const sourceNode = nodes.find((n: any) => n.id === sourceId);
1651
- const targetNode = nodes.find((n: any) => n.id === targetId);
1652
- if (!sourceNode) return err(`Source node "${sourceId}" not found. Use action "get" to see node IDs.`);
1653
- if (!targetNode) return err(`Target node "${targetId}" not found. Use action "get" to see node IDs.`);
1721
+ const sourceNode = nodes.find((n: any) => n.id === sourceId);
1722
+ const targetNode = nodes.find((n: any) => n.id === targetId);
1723
+ if (!sourceNode) return err(`Source node "${sourceId}" not found. Use action "get" to see node IDs.`);
1724
+ if (!targetNode) return err(`Target node "${targetId}" not found. Use action "get" to see node IDs.`);
1654
1725
 
1655
- // Find the edge connecting source target
1656
- const connectingEdge = edges.find((e: any) => e.source === sourceId && e.target === targetId);
1657
- if (!connectingEdge) return err(`No edge found from "${sourceId}" to "${targetId}". They may not be directly connected.`);
1726
+ const connectingEdge = edges.find((e: any) => e.source === sourceId && e.target === targetId);
1727
+ if (!connectingEdge) return err(`No edge found from "${sourceId}" to "${targetId}". They may not be directly connected.`);
1658
1728
 
1659
- // Create the new node, positioned between source and target
1660
- const midX = ((sourceNode.position?.x || 0) + (targetNode.position?.x || 0)) / 2;
1661
- const midY = ((sourceNode.position?.y || 0) + (targetNode.position?.y || 0)) / 2;
1662
- const newNode = normalizeNode(
1663
- { ...newNodeDef, position: newNodeDef.position || { x: midX, y: midY } },
1664
- nodes.length,
1665
- );
1729
+ const midX = ((sourceNode.position?.x || 0) + (targetNode.position?.x || 0)) / 2;
1730
+ const midY = ((sourceNode.position?.y || 0) + (targetNode.position?.y || 0)) / 2;
1731
+ const newNode = normalizeNode(
1732
+ { ...newNodeDef, position: newNodeDef.position || { x: midX, y: midY } },
1733
+ nodes.length,
1734
+ );
1666
1735
 
1667
- // Remove the original edge
1668
- const updatedEdges = edges.filter((e: any) => e.id !== connectingEdge.id);
1736
+ const updatedEdges = edges.filter((e: any) => e.id !== connectingEdge.id);
1669
1737
 
1670
- // Create two new edges: source → newNode and newNode → target
1671
- const edgeIn = {
1672
- id: `edge-${Date.now()}-in`,
1673
- source: sourceId,
1674
- target: newNode.id,
1675
- // Preserve the sourceHandle from the original edge (important for condition/loop branches)
1676
- ...(connectingEdge.sourceHandle ? { sourceHandle: connectingEdge.sourceHandle } : {}),
1677
- };
1678
- const edgeOut = {
1679
- id: `edge-${Date.now()}-out`,
1680
- source: newNode.id,
1681
- target: targetId,
1682
- // Preserve the targetHandle from the original edge
1683
- ...(connectingEdge.targetHandle ? { targetHandle: connectingEdge.targetHandle } : {}),
1684
- };
1738
+ const edgeIn = {
1739
+ id: `edge-${Date.now()}-in`,
1740
+ source: sourceId,
1741
+ target: newNode.id,
1742
+ ...(connectingEdge.sourceHandle ? { sourceHandle: connectingEdge.sourceHandle } : {}),
1743
+ };
1744
+ const edgeOut = {
1745
+ id: `edge-${Date.now()}-out`,
1746
+ source: newNode.id,
1747
+ target: targetId,
1748
+ ...(connectingEdge.targetHandle ? { targetHandle: connectingEdge.targetHandle } : {}),
1749
+ };
1685
1750
 
1686
- updatedEdges.push(edgeIn, edgeOut);
1687
- nodes.push(newNode);
1751
+ updatedEdges.push(edgeIn, edgeOut);
1752
+ nodes.push(newNode);
1688
1753
 
1689
- const { wf: saved, error: saveErr } = await saveGraph(wfId, nodes, updatedEdges);
1690
- if (saveErr) return err(saveErr);
1754
+ const { wf: saved, error: saveErr } = await saveGraph(wfId, nodes, updatedEdges);
1755
+ if (saveErr) return err(saveErr);
1691
1756
 
1692
- return ok({
1693
- message: `Inserted "${newNode.data.label}" (${newNode.type}) between "${sourceNode.data?.label || sourceId}" and "${targetNode.data?.label || targetId}"`,
1694
- inserted_node: { id: newNode.id, type: newNode.type, label: newNode.data.label, position: newNode.position },
1695
- new_edges: [
1696
- { id: edgeIn.id, from: sourceId, to: newNode.id },
1697
- { id: edgeOut.id, from: newNode.id, to: targetId },
1698
- ],
1699
- removed_edge: connectingEdge.id,
1700
- total_nodes: nodes.length,
1701
- total_edges: updatedEdges.length,
1702
- workflow: saved,
1757
+ return ok({
1758
+ message: `Inserted "${newNode.data.label}" (${newNode.type}) between "${sourceNode.data?.label || sourceId}" and "${targetNode.data?.label || targetId}"`,
1759
+ inserted_node: { id: newNode.id, type: newNode.type, label: newNode.data.label, position: newNode.position },
1760
+ new_edges: [
1761
+ { id: edgeIn.id, from: sourceId, to: newNode.id },
1762
+ { id: edgeOut.id, from: newNode.id, to: targetId },
1763
+ ],
1764
+ removed_edge: connectingEdge.id,
1765
+ total_nodes: nodes.length,
1766
+ total_edges: updatedEdges.length,
1767
+ workflow: saved,
1768
+ });
1703
1769
  });
1704
1770
  }
1705
1771