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.
- package/package.json +1 -1
- 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.
|
|
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
|
-
|
|
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("
|
|
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
|
-
|
|
1474
|
-
|
|
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
|
-
|
|
1477
|
-
|
|
1517
|
+
const existingNodes = (wf.nodes as any[]) || [];
|
|
1518
|
+
const existingEdges = (wf.edges as any[]) || [];
|
|
1478
1519
|
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
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
|
-
|
|
1529
|
+
const allNodes = [...existingNodes, ...normalized];
|
|
1490
1530
|
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
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
|
-
|
|
1499
|
-
|
|
1537
|
+
const { wf: saved, error: saveErr } = await saveGraph(wfId, allNodes, allEdges);
|
|
1538
|
+
if (saveErr) return err(saveErr);
|
|
1500
1539
|
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
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
|
-
|
|
1519
|
-
|
|
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
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
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
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
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
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
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
|
-
|
|
1535
|
-
|
|
1574
|
+
const { wf: saved, error: saveErr } = await saveGraph(wfId, nodes, wf.edges || []);
|
|
1575
|
+
if (saveErr) return err(saveErr);
|
|
1536
1576
|
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
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
|
-
|
|
1552
|
-
|
|
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
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
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
|
-
|
|
1560
|
-
|
|
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
|
-
|
|
1563
|
-
if (saveErr) return err(saveErr);
|
|
1606
|
+
const removedEdgeCount = ((wf.edges as any[]) || []).length - edges.length;
|
|
1564
1607
|
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
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
|
-
|
|
1581
|
-
|
|
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
|
-
|
|
1584
|
-
|
|
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
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
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
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
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
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
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
|
-
|
|
1616
|
-
|
|
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
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
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
|
-
|
|
1623
|
-
|
|
1688
|
+
const edgeIdSet = new Set(edgeIds);
|
|
1689
|
+
const edges = ((wf.edges as any[]) || []).filter((e: any) => !edgeIdSet.has(e.id));
|
|
1624
1690
|
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
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
|
-
|
|
1644
|
-
|
|
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
|
-
|
|
1647
|
-
|
|
1718
|
+
const nodes = (wf.nodes as any[]) || [];
|
|
1719
|
+
const edges = (wf.edges as any[]) || [];
|
|
1648
1720
|
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
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
|
-
|
|
1656
|
-
|
|
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
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
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
|
-
|
|
1687
|
-
|
|
1751
|
+
updatedEdges.push(edgeIn, edgeOut);
|
|
1752
|
+
nodes.push(newNode);
|
|
1688
1753
|
|
|
1689
|
-
|
|
1690
|
-
|
|
1754
|
+
const { wf: saved, error: saveErr } = await saveGraph(wfId, nodes, updatedEdges);
|
|
1755
|
+
if (saveErr) return err(saveErr);
|
|
1691
1756
|
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
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
|
|