ofiere-openclaw-plugin 4.0.0 → 4.1.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 +190 -152
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ofiere-openclaw-plugin",
3
- "version": "4.0.0",
3
+ "version": "4.1.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" },
@@ -1454,6 +1465,7 @@ function registerWorkflowOps(
1454
1465
  const { error } = await supabase.from("workflow_runs").insert({
1455
1466
  id: runId,
1456
1467
  workflow_id: wfId,
1468
+ user_id: userId,
1457
1469
  status: "running",
1458
1470
  started_at: new Date().toISOString(),
1459
1471
  trigger_type: "agent",
@@ -1470,40 +1482,40 @@ function registerWorkflowOps(
1470
1482
  const newNodes = params.nodes as any[];
1471
1483
  if (!newNodes || !Array.isArray(newNodes) || newNodes.length === 0) return err("Missing required: nodes[] (array of node definitions)");
1472
1484
 
1473
- const { wf, error: fetchErr } = await fetchWorkflow(wfId);
1474
- if (fetchErr || !wf) return err(fetchErr || "Workflow not found");
1485
+ return sequentialWorkflowOp(wfId, async () => {
1486
+ const { wf, error: fetchErr } = await fetchWorkflow(wfId);
1487
+ if (fetchErr || !wf) return err(fetchErr || "Workflow not found");
1475
1488
 
1476
- const existingNodes = (wf.nodes as any[]) || [];
1477
- const existingEdges = (wf.edges as any[]) || [];
1489
+ const existingNodes = (wf.nodes as any[]) || [];
1490
+ const existingEdges = (wf.edges as any[]) || [];
1478
1491
 
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
- });
1492
+ const maxY = existingNodes.reduce((max: number, n: any) => Math.max(max, n.position?.y || 0), 0);
1493
+ const normalized = newNodes.map((n, i) => {
1494
+ const node = normalizeNode(n, existingNodes.length + i);
1495
+ if (!n.position) {
1496
+ node.position = { x: 250, y: maxY + 120 + i * 150 };
1497
+ }
1498
+ return node;
1499
+ });
1488
1500
 
1489
- const allNodes = [...existingNodes, ...normalized];
1501
+ const allNodes = [...existingNodes, ...normalized];
1490
1502
 
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
- }
1503
+ let allEdges = existingEdges;
1504
+ if (params.edges && Array.isArray(params.edges)) {
1505
+ const newEdges = (params.edges as any[]).map((e: any, i: number) => normalizeEdge(e, existingEdges.length + i));
1506
+ allEdges = [...existingEdges, ...newEdges];
1507
+ }
1497
1508
 
1498
- const { wf: saved, error: saveErr } = await saveGraph(wfId, allNodes, allEdges);
1499
- if (saveErr) return err(saveErr);
1509
+ const { wf: saved, error: saveErr } = await saveGraph(wfId, allNodes, allEdges);
1510
+ if (saveErr) return err(saveErr);
1500
1511
 
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,
1512
+ return ok({
1513
+ message: `Added ${normalized.length} node(s) to workflow`,
1514
+ added_nodes: normalized.map((n: any) => ({ id: n.id, type: n.type, label: n.data?.label })),
1515
+ total_nodes: allNodes.length,
1516
+ total_edges: allEdges.length,
1517
+ workflow: saved,
1518
+ });
1507
1519
  });
1508
1520
  }
1509
1521
 
@@ -1515,30 +1527,31 @@ function registerWorkflowOps(
1515
1527
  const dataUpdate = params.data as Record<string, any>;
1516
1528
  if (!dataUpdate || typeof dataUpdate !== "object") return err("Missing required: data (object with fields to update)");
1517
1529
 
1518
- const { wf, error: fetchErr } = await fetchWorkflow(wfId);
1519
- if (fetchErr || !wf) return err(fetchErr || "Workflow not found");
1530
+ return sequentialWorkflowOp(wfId, async () => {
1531
+ const { wf, error: fetchErr } = await fetchWorkflow(wfId);
1532
+ if (fetchErr || !wf) return err(fetchErr || "Workflow not found");
1520
1533
 
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.`);
1534
+ const nodes = (wf.nodes as any[]) || [];
1535
+ const nodeIndex = nodes.findIndex((n: any) => n.id === nodeId);
1536
+ if (nodeIndex === -1) return err(`Node "${nodeId}" not found in workflow. Use action "get" to see all node IDs.`);
1524
1537
 
1525
- // Merge new data into existing node data
1526
- const existingData = nodes[nodeIndex].data || {};
1527
- nodes[nodeIndex].data = { ...existingData, ...dataUpdate };
1538
+ // Merge new data into existing node data (preserves untouched fields)
1539
+ const existingData = nodes[nodeIndex].data || {};
1540
+ nodes[nodeIndex].data = { ...existingData, ...dataUpdate };
1528
1541
 
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
- }
1542
+ if (params.node && typeof params.node === "object" && (params.node as any).position) {
1543
+ nodes[nodeIndex].position = (params.node as any).position;
1544
+ }
1533
1545
 
1534
- const { wf: saved, error: saveErr } = await saveGraph(wfId, nodes, wf.edges || []);
1535
- if (saveErr) return err(saveErr);
1546
+ const { wf: saved, error: saveErr } = await saveGraph(wfId, nodes, wf.edges || []);
1547
+ if (saveErr) return err(saveErr);
1536
1548
 
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,
1549
+ return ok({
1550
+ message: `Node "${nodeId}" updated`,
1551
+ node: { id: nodes[nodeIndex].id, type: nodes[nodeIndex].type, data: nodes[nodeIndex].data },
1552
+ fields_updated: Object.keys(dataUpdate),
1553
+ workflow: saved,
1554
+ });
1542
1555
  });
1543
1556
  }
1544
1557
 
@@ -1548,26 +1561,33 @@ function registerWorkflowOps(
1548
1561
  const nodeIds = params.node_ids as string[];
1549
1562
  if (!nodeIds || !Array.isArray(nodeIds) || nodeIds.length === 0) return err("Missing required: node_ids[] (array of node IDs to delete)");
1550
1563
 
1551
- const { wf, error: fetchErr } = await fetchWorkflow(wfId);
1552
- if (fetchErr || !wf) return err(fetchErr || "Workflow not found");
1564
+ return sequentialWorkflowOp(wfId, async () => {
1565
+ const { wf, error: fetchErr } = await fetchWorkflow(wfId);
1566
+ if (fetchErr || !wf) return err(fetchErr || "Workflow not found");
1553
1567
 
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));
1568
+ // Track which IDs actually exist vs. not found
1569
+ const existingNodeIdSet = new Set(((wf.nodes as any[]) || []).map((n: any) => n.id));
1570
+ const actuallyDeletedIds = nodeIds.filter(id => existingNodeIdSet.has(id));
1571
+ const notFoundIds = nodeIds.filter(id => !existingNodeIdSet.has(id));
1558
1572
 
1559
- const removedNodeCount = ((wf.nodes as any[]) || []).length - nodes.length;
1560
- const removedEdgeCount = ((wf.edges as any[]) || []).length - edges.length;
1573
+ const nodeIdSet = new Set(nodeIds);
1574
+ const nodes = ((wf.nodes as any[]) || []).filter((n: any) => !nodeIdSet.has(n.id));
1575
+ // Also remove edges connected to deleted nodes
1576
+ const edges = ((wf.edges as any[]) || []).filter((e: any) => !nodeIdSet.has(e.source) && !nodeIdSet.has(e.target));
1561
1577
 
1562
- const { wf: saved, error: saveErr } = await saveGraph(wfId, nodes, edges);
1563
- if (saveErr) return err(saveErr);
1578
+ const removedEdgeCount = ((wf.edges as any[]) || []).length - edges.length;
1564
1579
 
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,
1580
+ const { wf: saved, error: saveErr } = await saveGraph(wfId, nodes, edges);
1581
+ if (saveErr) return err(saveErr);
1582
+
1583
+ return ok({
1584
+ message: `Deleted ${actuallyDeletedIds.length} node(s) and ${removedEdgeCount} connected edge(s)`,
1585
+ deleted_node_ids: actuallyDeletedIds,
1586
+ not_found_ids: notFoundIds.length > 0 ? notFoundIds : undefined,
1587
+ remaining_nodes: nodes.length,
1588
+ remaining_edges: edges.length,
1589
+ workflow: saved,
1590
+ });
1571
1591
  });
1572
1592
  }
1573
1593
 
@@ -1577,33 +1597,49 @@ function registerWorkflowOps(
1577
1597
  const newEdges = params.edges as any[];
1578
1598
  if (!newEdges || !Array.isArray(newEdges) || newEdges.length === 0) return err("Missing required: edges[] (array of edge definitions)");
1579
1599
 
1580
- const { wf, error: fetchErr } = await fetchWorkflow(wfId);
1581
- if (fetchErr || !wf) return err(fetchErr || "Workflow not found");
1600
+ return sequentialWorkflowOp(wfId, async () => {
1601
+ const { wf, error: fetchErr } = await fetchWorkflow(wfId);
1602
+ if (fetchErr || !wf) return err(fetchErr || "Workflow not found");
1582
1603
 
1583
- const existingEdges = (wf.edges as any[]) || [];
1584
- const nodeIds = new Set(((wf.nodes as any[]) || []).map((n: any) => n.id));
1604
+ const existingEdges = (wf.edges as any[]) || [];
1605
+ const nodesList = (wf.nodes as any[]) || [];
1606
+ const nodeIdSet = new Set(nodesList.map((n: any) => n.id));
1607
+ const nodesMap = new Map(nodesList.map((n: any) => [n.id, n]));
1585
1608
 
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
- });
1609
+ try {
1610
+ // Validate source/target existence AND sourceHandle semantics
1611
+ const normalized = newEdges.map((e: any, i: number) => {
1612
+ if (!nodeIdSet.has(e.source)) throw new Error(`Source node "${e.source}" not found in workflow`);
1613
+ if (!nodeIdSet.has(e.target)) throw new Error(`Target node "${e.target}" not found in workflow`);
1614
+
1615
+ // Validate that condition/loop handles are only used on the correct node types
1616
+ if (e.sourceHandle) {
1617
+ const srcNode = nodesMap.get(e.source);
1618
+ if (e.sourceHandle.startsWith("condition-") && srcNode?.type !== "condition") {
1619
+ throw new Error(`sourceHandle "${e.sourceHandle}" is only valid on condition nodes, but source "${e.source}" is type "${srcNode?.type}"`);
1620
+ }
1621
+ if ((e.sourceHandle === "loop_body" || e.sourceHandle === "done") && srcNode?.type !== "loop") {
1622
+ throw new Error(`sourceHandle "${e.sourceHandle}" is only valid on loop nodes, but source "${e.source}" is type "${srcNode?.type}"`);
1623
+ }
1624
+ }
1625
+
1626
+ return normalizeEdge(e, existingEdges.length + i);
1627
+ });
1592
1628
 
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);
1629
+ const allEdges = [...existingEdges, ...normalized];
1630
+ const { wf: saved, error: saveErr } = await saveGraph(wfId, wf.nodes || [], allEdges);
1631
+ if (saveErr) return err(saveErr);
1597
1632
 
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
- }
1633
+ return ok({
1634
+ message: `Added ${normalized.length} edge(s)`,
1635
+ added_edges: normalized.map((e: any) => ({ id: e.id, source: e.source, target: e.target })),
1636
+ total_edges: allEdges.length,
1637
+ workflow: saved,
1638
+ });
1639
+ } catch (e: any) {
1640
+ return err(e.message);
1641
+ }
1642
+ });
1607
1643
  }
1608
1644
 
1609
1645
  case "delete_edges": {
@@ -1612,21 +1648,28 @@ function registerWorkflowOps(
1612
1648
  const edgeIds = params.edge_ids as string[];
1613
1649
  if (!edgeIds || !Array.isArray(edgeIds) || edgeIds.length === 0) return err("Missing required: edge_ids[] (array of edge IDs to delete)");
1614
1650
 
1615
- const { wf, error: fetchErr } = await fetchWorkflow(wfId);
1616
- if (fetchErr || !wf) return err(fetchErr || "Workflow not found");
1651
+ return sequentialWorkflowOp(wfId, async () => {
1652
+ const { wf, error: fetchErr } = await fetchWorkflow(wfId);
1653
+ if (fetchErr || !wf) return err(fetchErr || "Workflow not found");
1617
1654
 
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;
1655
+ // Track which IDs actually exist vs. not found
1656
+ const existingEdgeIds = new Set(((wf.edges as any[]) || []).map((e: any) => e.id));
1657
+ const actuallyDeletedIds = edgeIds.filter(id => existingEdgeIds.has(id));
1658
+ const notFoundIds = edgeIds.filter(id => !existingEdgeIds.has(id));
1621
1659
 
1622
- const { wf: saved, error: saveErr } = await saveGraph(wfId, wf.nodes || [], edges);
1623
- if (saveErr) return err(saveErr);
1660
+ const edgeIdSet = new Set(edgeIds);
1661
+ const edges = ((wf.edges as any[]) || []).filter((e: any) => !edgeIdSet.has(e.id));
1624
1662
 
1625
- return ok({
1626
- message: `Deleted ${removedCount} edge(s)`,
1627
- deleted_edge_ids: edgeIds,
1628
- remaining_edges: edges.length,
1629
- workflow: saved,
1663
+ const { wf: saved, error: saveErr } = await saveGraph(wfId, wf.nodes || [], edges);
1664
+ if (saveErr) return err(saveErr);
1665
+
1666
+ return ok({
1667
+ message: `Deleted ${actuallyDeletedIds.length} edge(s)`,
1668
+ deleted_edge_ids: actuallyDeletedIds,
1669
+ not_found_ids: notFoundIds.length > 0 ? notFoundIds : undefined,
1670
+ remaining_edges: edges.length,
1671
+ workflow: saved,
1672
+ });
1630
1673
  });
1631
1674
  }
1632
1675
 
@@ -1640,66 +1683,61 @@ function registerWorkflowOps(
1640
1683
  if (!targetId) return err("Missing required: target_node_id");
1641
1684
  if (!newNodeDef || typeof newNodeDef !== "object") return err("Missing required: node (the node definition to insert)");
1642
1685
 
1643
- const { wf, error: fetchErr } = await fetchWorkflow(wfId);
1644
- if (fetchErr || !wf) return err(fetchErr || "Workflow not found");
1686
+ return sequentialWorkflowOp(wfId, async () => {
1687
+ const { wf, error: fetchErr } = await fetchWorkflow(wfId);
1688
+ if (fetchErr || !wf) return err(fetchErr || "Workflow not found");
1645
1689
 
1646
- const nodes = (wf.nodes as any[]) || [];
1647
- const edges = (wf.edges as any[]) || [];
1690
+ const nodes = (wf.nodes as any[]) || [];
1691
+ const edges = (wf.edges as any[]) || [];
1648
1692
 
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.`);
1693
+ const sourceNode = nodes.find((n: any) => n.id === sourceId);
1694
+ const targetNode = nodes.find((n: any) => n.id === targetId);
1695
+ if (!sourceNode) return err(`Source node "${sourceId}" not found. Use action "get" to see node IDs.`);
1696
+ if (!targetNode) return err(`Target node "${targetId}" not found. Use action "get" to see node IDs.`);
1654
1697
 
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.`);
1698
+ const connectingEdge = edges.find((e: any) => e.source === sourceId && e.target === targetId);
1699
+ if (!connectingEdge) return err(`No edge found from "${sourceId}" to "${targetId}". They may not be directly connected.`);
1658
1700
 
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
- );
1701
+ const midX = ((sourceNode.position?.x || 0) + (targetNode.position?.x || 0)) / 2;
1702
+ const midY = ((sourceNode.position?.y || 0) + (targetNode.position?.y || 0)) / 2;
1703
+ const newNode = normalizeNode(
1704
+ { ...newNodeDef, position: newNodeDef.position || { x: midX, y: midY } },
1705
+ nodes.length,
1706
+ );
1666
1707
 
1667
- // Remove the original edge
1668
- const updatedEdges = edges.filter((e: any) => e.id !== connectingEdge.id);
1708
+ const updatedEdges = edges.filter((e: any) => e.id !== connectingEdge.id);
1669
1709
 
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
- };
1710
+ const edgeIn = {
1711
+ id: `edge-${Date.now()}-in`,
1712
+ source: sourceId,
1713
+ target: newNode.id,
1714
+ ...(connectingEdge.sourceHandle ? { sourceHandle: connectingEdge.sourceHandle } : {}),
1715
+ };
1716
+ const edgeOut = {
1717
+ id: `edge-${Date.now()}-out`,
1718
+ source: newNode.id,
1719
+ target: targetId,
1720
+ ...(connectingEdge.targetHandle ? { targetHandle: connectingEdge.targetHandle } : {}),
1721
+ };
1685
1722
 
1686
- updatedEdges.push(edgeIn, edgeOut);
1687
- nodes.push(newNode);
1723
+ updatedEdges.push(edgeIn, edgeOut);
1724
+ nodes.push(newNode);
1688
1725
 
1689
- const { wf: saved, error: saveErr } = await saveGraph(wfId, nodes, updatedEdges);
1690
- if (saveErr) return err(saveErr);
1726
+ const { wf: saved, error: saveErr } = await saveGraph(wfId, nodes, updatedEdges);
1727
+ if (saveErr) return err(saveErr);
1691
1728
 
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,
1729
+ return ok({
1730
+ message: `Inserted "${newNode.data.label}" (${newNode.type}) between "${sourceNode.data?.label || sourceId}" and "${targetNode.data?.label || targetId}"`,
1731
+ inserted_node: { id: newNode.id, type: newNode.type, label: newNode.data.label, position: newNode.position },
1732
+ new_edges: [
1733
+ { id: edgeIn.id, from: sourceId, to: newNode.id },
1734
+ { id: edgeOut.id, from: newNode.id, to: targetId },
1735
+ ],
1736
+ removed_edge: connectingEdge.id,
1737
+ total_nodes: nodes.length,
1738
+ total_edges: updatedEdges.length,
1739
+ workflow: saved,
1740
+ });
1703
1741
  });
1704
1742
  }
1705
1743