ofiere-openclaw-plugin 3.5.5 → 4.0.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 +2 -2
- package/src/prompt.ts +20 -8
- package/src/tools.ts +356 -61
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ofiere-openclaw-plugin",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "OpenClaw plugin for Ofiere PM - 10 meta-tools covering tasks, agents, projects, scheduling, knowledge, workflows, notifications, memory, prompts, and constellation agent architecture",
|
|
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"],
|
|
7
7
|
"homepage": "https://github.com/gilanggemar/Ofiere",
|
|
8
8
|
"repository": {
|
package/src/prompt.ts
CHANGED
|
@@ -40,17 +40,24 @@ const TOOL_DOCS: Record<string, string> = {
|
|
|
40
40
|
- create: Add knowledge to the library. Requires: file_name. Optional: content, source, author, credibility_tier
|
|
41
41
|
- update/delete: By document ID`,
|
|
42
42
|
|
|
43
|
-
OFIERE_WORKFLOW_OPS: `- **OFIERE_WORKFLOW_OPS** —
|
|
43
|
+
OFIERE_WORKFLOW_OPS: `- **OFIERE_WORKFLOW_OPS** — Full workflow automation control (13 actions)
|
|
44
44
|
- list: All workflows, filter by status (draft, active, paused, archived)
|
|
45
|
-
- get: Full workflow details
|
|
46
|
-
- create: New workflow with name + nodes and edges
|
|
47
|
-
|
|
48
|
-
- Each node: { type, data: { label, ...type-specific fields } }. IDs/positions auto-generated
|
|
49
|
-
- Edges: { source, target }. A manual_trigger is auto-prepended if no trigger exists
|
|
50
|
-
- update: Modify workflow (name, description, status, nodes, edges)
|
|
45
|
+
- get: Full workflow details with all node IDs, types, data, edges — ALWAYS call this before surgical edits
|
|
46
|
+
- create: New workflow with name + nodes[] and edges[]. A manual_trigger is auto-prepended if missing
|
|
47
|
+
- update: Replace entire workflow graph (name, description, status, nodes, edges)
|
|
51
48
|
- delete: Remove workflow and all run history
|
|
52
49
|
- list_runs: Recent execution history for a workflow
|
|
53
|
-
- trigger: Start a workflow run
|
|
50
|
+
- trigger: Start a workflow run
|
|
51
|
+
- **add_nodes**: Add new nodes to an existing workflow. Required: workflow_id, nodes[]. Optional: edges[] to connect them
|
|
52
|
+
- **update_node**: Edit a specific node's data fields (e.g. change task instructions, agentId, template). Required: workflow_id, node_id, data. Only specified fields are changed — others are preserved
|
|
53
|
+
- **delete_nodes**: Remove specific nodes and all their connected edges. Required: workflow_id, node_ids[]
|
|
54
|
+
- **add_edges**: Add new connections between existing nodes. Required: workflow_id, edges[]. Each edge: { source, target, sourceHandle?, targetHandle? }
|
|
55
|
+
- **delete_edges**: Remove specific edges by ID. Required: workflow_id, edge_ids[]
|
|
56
|
+
- **insert_node_between**: Insert a new node between two connected nodes (auto-rewires edges). Required: workflow_id, source_node_id, target_node_id, node. Use this to add steps in the middle of a flow
|
|
57
|
+
- Node types: manual_trigger, webhook_trigger, agent_step, formatter_step, http_request, task_call, variable_set, condition, human_approval, delay, loop, convergence, output, checkpoint, note
|
|
58
|
+
- Key fields: agent_step(agentId, task, responseMode, timeoutSec), formatter_step(template, formatMode), condition(expression, varCheck, operator, varMatch), human_approval(instructions), variable_set(variableName, variableValue, operation)
|
|
59
|
+
- Edge handles: condition edges use sourceHandle "condition-true"/"condition-false". Loop edges use "loop_body"/"done"
|
|
60
|
+
- Variables: Use {{prev.nodeId.outputText}} for prior outputs, {{variables.key}} for stored variables`,
|
|
54
61
|
|
|
55
62
|
OFIERE_NOTIFY_OPS: `- **OFIERE_NOTIFY_OPS** — Notifications (action: "list", "mark_read", "mark_all_read", "delete")
|
|
56
63
|
- list: Recent notifications. unread_only=true for unread only
|
|
@@ -130,6 +137,11 @@ ${toolDocs}
|
|
|
130
137
|
- When creating or editing an agent's architecture, ALWAYS use OFIERE_CONSTELLATION_OPS action:"read_blueprint" first to understand the required structure.
|
|
131
138
|
- When creating a new agent, use OFIERE_CONSTELLATION_OPS action:"create_agent" with all available structured params. The agent will be auto-registered in OpenClaw.
|
|
132
139
|
- When deleting an agent, ALWAYS ask the user for explicit confirmation BEFORE calling OFIERE_CONSTELLATION_OPS action:"delete_agent" with confirm: true. Show them what will be deleted. This action is IRREVERSIBLE.
|
|
140
|
+
- WORKFLOW MASTERY: When modifying existing workflows, ALWAYS call "get" first to see all node IDs and the current graph structure.
|
|
141
|
+
- To add a step in the middle of a flow, use "insert_node_between" with the source and target node IDs. Do NOT rebuild the entire graph.
|
|
142
|
+
- To change a node's configuration (e.g. update agent instructions, change template text), use "update_node" with just the fields that changed.
|
|
143
|
+
- When creating workflows with condition or loop nodes, specify sourceHandle on edges: "condition-true"/"condition-false" for conditions, "loop_body"/"done" for loops.
|
|
144
|
+
- Fill node data fields with actual content — include real task instructions, templates, and variable references. Do NOT leave fields empty unless intentionally blank.
|
|
133
145
|
</ofiere-pm>`;
|
|
134
146
|
}
|
|
135
147
|
|
package/src/tools.ts
CHANGED
|
@@ -1155,46 +1155,60 @@ function registerWorkflowOps(
|
|
|
1155
1155
|
label: "Ofiere Workflow Operations",
|
|
1156
1156
|
description:
|
|
1157
1157
|
`Manage, build, and trigger automated workflows in the Ofiere dashboard.\n\n` +
|
|
1158
|
-
`
|
|
1158
|
+
`ACTIONS:\n` +
|
|
1159
1159
|
`- "list": List all workflows. Optional: status\n` +
|
|
1160
|
-
`- "get": Get workflow details. Required: id\n` +
|
|
1161
|
-
`- "create": Create a workflow WITH nodes and edges. Required: name. Optional: description, nodes, edges,
|
|
1162
|
-
`- "update": Update
|
|
1160
|
+
`- "get": Get full workflow details including all nodes/edges. Required: id\n` +
|
|
1161
|
+
`- "create": Create a workflow WITH nodes and edges. Required: name. Optional: description, nodes, edges, status\n` +
|
|
1162
|
+
`- "update": Update workflow metadata or replace entire graph. Required: id. Optional: name, description, status, nodes, edges\n` +
|
|
1163
1163
|
`- "delete": Delete a workflow and its run history. Required: id\n` +
|
|
1164
1164
|
`- "list_runs": List recent runs. Required: workflow_id. Optional: limit\n` +
|
|
1165
|
-
`- "trigger": Start a workflow run. Required: workflow_id\n
|
|
1166
|
-
|
|
1165
|
+
`- "trigger": Start a workflow run. Required: workflow_id\n` +
|
|
1166
|
+
`- "add_nodes": Add new nodes to an existing workflow. Required: workflow_id, nodes[]\n` +
|
|
1167
|
+
`- "update_node": Edit a specific node's data fields without replacing the graph. Required: workflow_id, node_id, data\n` +
|
|
1168
|
+
`- "delete_nodes": Remove specific nodes and their edges. Required: workflow_id, node_ids[]\n` +
|
|
1169
|
+
`- "add_edges": Add new connections between nodes. Required: workflow_id, edges[]\n` +
|
|
1170
|
+
`- "delete_edges": Remove specific edges. Required: workflow_id, edge_ids[]\n` +
|
|
1171
|
+
`- "insert_node_between": Insert a new node between two connected nodes (auto-rewires edges). Required: workflow_id, source_node_id, target_node_id, node\n\n` +
|
|
1172
|
+
`NODE TYPES & CONFIGURABLE FIELDS:\n` +
|
|
1167
1173
|
` TRIGGERS (start of workflow — pick one):\n` +
|
|
1168
|
-
`
|
|
1169
|
-
`
|
|
1170
|
-
` - "schedule_trigger": Runs on cron schedule. data: { label, cron: "0 9 * * 1-5" }\n` +
|
|
1174
|
+
` "manual_trigger": { label }\n` +
|
|
1175
|
+
` "webhook_trigger": { label, triggerName?, webhookPayload? }\n\n` +
|
|
1171
1176
|
` STEPS (the work):\n` +
|
|
1172
|
-
`
|
|
1173
|
-
`
|
|
1174
|
-
`
|
|
1175
|
-
`
|
|
1176
|
-
`
|
|
1177
|
+
` "agent_step": { label, agentId: string, agentName?: string, task: string (prompt/instructions), responseMode: "text"|"json", timeoutSec: number(120) }\n` +
|
|
1178
|
+
` "formatter_step": { label, template: string (use {{prev.nodeId.outputText}}), formatMode: "template"|"uppercase"|"lowercase"|"extract_json"|"remove_whitespace", outputKey?: string }\n` +
|
|
1179
|
+
` "http_request": { label, method: "GET"|"POST"|"PUT"|"PATCH"|"DELETE", url: string, body?: string (JSON), headers?: object, timeoutSec: number(30) }\n` +
|
|
1180
|
+
` "task_call": { label, agentId: string, taskId: string, taskTitle?: string, agentName?: string, systemPromptOverride?: string }\n` +
|
|
1181
|
+
` "variable_set": { label, variableName: string, variableValue: string, operation: "set"|"append"|"prepend" }\n\n` +
|
|
1177
1182
|
` CONTROL FLOW:\n` +
|
|
1178
|
-
`
|
|
1179
|
-
`
|
|
1180
|
-
`
|
|
1181
|
-
`
|
|
1182
|
-
`
|
|
1183
|
+
` "condition": { label, expression: string (JS), varCheck?: string, operator?: "=="|"!="|"contains"|"not_contains"|"starts_with"|"ends_with"|"is_empty"|"not_empty", varMatch?: string }\n` +
|
|
1184
|
+
` → EDGES: use sourceHandle "condition-true" and "condition-false" for the two branches\n` +
|
|
1185
|
+
` "human_approval": { label, instructions: string, checkpointId?: string (for rejection cycle) }\n` +
|
|
1186
|
+
` "delay": { label, delaySec: number(1-3600) }\n` +
|
|
1187
|
+
` "loop": { label, loopType: "count"|"for_each", maxIterations: number, iterateOver?: string, checkpointId?: string }\n` +
|
|
1188
|
+
` → EDGES: use sourceHandle "loop_body" for the loop body path and "done" for the exit path\n` +
|
|
1189
|
+
` "convergence": { label, mergeStrategy: "wait_all"|"first_arrives" }\n\n` +
|
|
1183
1190
|
` END:\n` +
|
|
1184
|
-
`
|
|
1191
|
+
` "output": { label, outputMode: "return"|"notification"|"webhook"|"log", template?: string, webhookUrl?: string, checkpointId?: string }\n\n` +
|
|
1185
1192
|
` SPECIAL:\n` +
|
|
1186
|
-
`
|
|
1187
|
-
`
|
|
1188
|
-
`
|
|
1189
|
-
`
|
|
1190
|
-
`
|
|
1193
|
+
` "checkpoint": { label } — Loop/cycle target marker\n` +
|
|
1194
|
+
` "note": { label, noteText: string } — Annotation, does not affect execution\n\n` +
|
|
1195
|
+
`VARIABLE SYNTAX: Use {{prev.nodeId.outputText}} to reference prior node outputs. Use {{variables.key}} for stored variables.\n\n` +
|
|
1196
|
+
`STRUCTURE:\n` +
|
|
1197
|
+
` Each node: { type, data: { label, ... }, position?: { x, y } }. IDs/positions auto-generated if omitted.\n` +
|
|
1198
|
+
` Each edge: { source, target, sourceHandle?, targetHandle? }. IDs auto-generated.\n` +
|
|
1199
|
+
` A manual_trigger node is auto-prepended if no trigger node is included on create.\n\n` +
|
|
1200
|
+
`TIPS:\n` +
|
|
1201
|
+
` - Use "insert_node_between" to add a step in the middle of an existing flow (e.g. add a review gate between formatter and output).\n` +
|
|
1202
|
+
` - Use "update_node" to fill in or change a specific node's fields without rebuilding the whole graph.\n` +
|
|
1203
|
+
` - Use "get" first to see the current graph before making surgical edits.\n` +
|
|
1204
|
+
` - For condition/loop nodes, specify sourceHandle on edges to control which branch path is taken.`,
|
|
1191
1205
|
parameters: {
|
|
1192
1206
|
type: "object",
|
|
1193
1207
|
required: ["action"],
|
|
1194
1208
|
properties: {
|
|
1195
|
-
action: { type: "string", enum: ["list", "get", "create", "update", "delete", "list_runs", "trigger"] },
|
|
1209
|
+
action: { type: "string", enum: ["list", "get", "create", "update", "delete", "list_runs", "trigger", "add_nodes", "update_node", "delete_nodes", "add_edges", "delete_edges", "insert_node_between"] },
|
|
1196
1210
|
id: { type: "string", description: "Workflow ID" },
|
|
1197
|
-
workflow_id: { type: "string", description: "Workflow ID
|
|
1211
|
+
workflow_id: { type: "string", description: "Workflow ID" },
|
|
1198
1212
|
name: { type: "string", description: "Workflow name" },
|
|
1199
1213
|
description: { type: "string" },
|
|
1200
1214
|
nodes: {
|
|
@@ -1203,9 +1217,9 @@ function registerWorkflowOps(
|
|
|
1203
1217
|
type: "object",
|
|
1204
1218
|
properties: {
|
|
1205
1219
|
id: { type: "string", description: "Node ID (auto-generated if omitted)" },
|
|
1206
|
-
type: { type: "string", enum: ["manual_trigger", "webhook_trigger", "
|
|
1220
|
+
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"] },
|
|
1207
1221
|
position: { type: "object", properties: { x: { type: "number" }, y: { type: "number" } } },
|
|
1208
|
-
data: { type: "object", description: "Node config — always include a 'label' field. See NODE TYPES above for
|
|
1222
|
+
data: { type: "object", description: "Node config — always include a 'label' field. See NODE TYPES above for all configurable fields per type." },
|
|
1209
1223
|
},
|
|
1210
1224
|
},
|
|
1211
1225
|
description: "Workflow graph nodes",
|
|
@@ -1218,10 +1232,28 @@ function registerWorkflowOps(
|
|
|
1218
1232
|
id: { type: "string", description: "Edge ID (auto-generated if omitted)" },
|
|
1219
1233
|
source: { type: "string", description: "Source node ID" },
|
|
1220
1234
|
target: { type: "string", description: "Target node ID" },
|
|
1235
|
+
sourceHandle: { type: "string", description: "Source handle: 'condition-true'/'condition-false' for conditions, 'loop_body'/'done' for loops" },
|
|
1236
|
+
targetHandle: { type: "string", description: "Target handle (rarely needed)" },
|
|
1221
1237
|
},
|
|
1222
1238
|
},
|
|
1223
|
-
description: "Connections between nodes
|
|
1239
|
+
description: "Connections between nodes",
|
|
1240
|
+
},
|
|
1241
|
+
// Granular edit params
|
|
1242
|
+
node_id: { type: "string", description: "Specific node ID for update_node" },
|
|
1243
|
+
node_ids: { type: "array", items: { type: "string" }, description: "Node IDs for delete_nodes" },
|
|
1244
|
+
edge_ids: { type: "array", items: { type: "string" }, description: "Edge IDs for delete_edges" },
|
|
1245
|
+
node: {
|
|
1246
|
+
type: "object",
|
|
1247
|
+
description: "Single node definition for insert_node_between. { type, data: { label, ... } }",
|
|
1248
|
+
properties: {
|
|
1249
|
+
type: { type: "string" },
|
|
1250
|
+
data: { type: "object" },
|
|
1251
|
+
position: { type: "object", properties: { x: { type: "number" }, y: { type: "number" } } },
|
|
1252
|
+
},
|
|
1224
1253
|
},
|
|
1254
|
+
data: { type: "object", description: "Data fields to merge into a node (for update_node). Only specified fields are changed." },
|
|
1255
|
+
source_node_id: { type: "string", description: "Source node ID for insert_node_between" },
|
|
1256
|
+
target_node_id: { type: "string", description: "Target node ID for insert_node_between" },
|
|
1225
1257
|
steps: { type: "array", items: { type: "object" }, description: "Legacy V1 step definitions" },
|
|
1226
1258
|
schedule: { type: "string", description: "Cron expression or schedule" },
|
|
1227
1259
|
status: { type: "string", enum: ["draft", "active", "paused", "archived"] },
|
|
@@ -1232,21 +1264,22 @@ function registerWorkflowOps(
|
|
|
1232
1264
|
const action = params.action as string;
|
|
1233
1265
|
|
|
1234
1266
|
// Default data for each node type — ensures dashboard renders them properly
|
|
1267
|
+
// schedule_trigger kept for backward compatibility with existing workflows
|
|
1235
1268
|
const NODE_DEFAULTS: Record<string, Record<string, any>> = {
|
|
1236
1269
|
manual_trigger: { label: "Execute Trigger" },
|
|
1237
1270
|
webhook_trigger: { label: "Webhook Trigger" },
|
|
1238
1271
|
schedule_trigger: { label: "Schedule Trigger", cron: "0 9 * * 1-5" },
|
|
1239
|
-
agent_step: { label: "Agent Step", agentId: "", task: "", responseMode: "text", timeoutSec: 120 },
|
|
1240
|
-
formatter_step: { label: "Formatter", template: "" },
|
|
1241
|
-
http_request: { label: "HTTP Request", method: "GET", url: "" },
|
|
1242
|
-
task_call: { label: "Task", agentId: "", taskId: "", taskTitle: "", agentName: "" },
|
|
1243
|
-
variable_set: { label: "Set Variable", variableName: "", variableValue: "" },
|
|
1244
|
-
condition: { label: "Condition", expression: "" },
|
|
1245
|
-
human_approval: { label: "Human Approval", instructions: "" },
|
|
1272
|
+
agent_step: { label: "Agent Step", agentId: "", agentName: "", task: "", responseMode: "text", timeoutSec: 120 },
|
|
1273
|
+
formatter_step: { label: "Formatter", template: "", formatMode: "template", outputKey: "" },
|
|
1274
|
+
http_request: { label: "HTTP Request", method: "GET", url: "", body: "", headers: {}, timeoutSec: 30 },
|
|
1275
|
+
task_call: { label: "Task", agentId: "", taskId: "", taskTitle: "", agentName: "", systemPromptOverride: "" },
|
|
1276
|
+
variable_set: { label: "Set Variable", variableName: "", variableValue: "", operation: "set" },
|
|
1277
|
+
condition: { label: "Condition", expression: "", varCheck: "", operator: "==", varMatch: "" },
|
|
1278
|
+
human_approval: { label: "Human Approval", instructions: "", checkpointId: "" },
|
|
1246
1279
|
delay: { label: "Delay", delaySec: 5 },
|
|
1247
|
-
loop: { label: "Loop", loopType: "count", maxIterations: 3 },
|
|
1280
|
+
loop: { label: "Loop", loopType: "count", maxIterations: 3, iterateOver: "", checkpointId: "" },
|
|
1248
1281
|
convergence: { label: "Convergence", mergeStrategy: "wait_all" },
|
|
1249
|
-
output: { label: "Output", outputMode: "return" },
|
|
1282
|
+
output: { label: "Output", outputMode: "return", template: "", webhookUrl: "", checkpointId: "" },
|
|
1250
1283
|
checkpoint: { label: "Checkpoint" },
|
|
1251
1284
|
note: { label: "Note", noteText: "" },
|
|
1252
1285
|
};
|
|
@@ -1267,6 +1300,42 @@ function registerWorkflowOps(
|
|
|
1267
1300
|
};
|
|
1268
1301
|
}
|
|
1269
1302
|
|
|
1303
|
+
// Helper: normalize a single edge with auto-ID
|
|
1304
|
+
function normalizeEdge(e: any, i: number) {
|
|
1305
|
+
return {
|
|
1306
|
+
id: e.id || `edge-${Date.now()}-${i}`,
|
|
1307
|
+
source: e.source,
|
|
1308
|
+
target: e.target,
|
|
1309
|
+
...(e.sourceHandle ? { sourceHandle: e.sourceHandle } : {}),
|
|
1310
|
+
...(e.targetHandle ? { targetHandle: e.targetHandle } : {}),
|
|
1311
|
+
};
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
// Helper: fetch current workflow graph
|
|
1315
|
+
async function fetchWorkflow(wfId: string) {
|
|
1316
|
+
const { data, error } = await supabase
|
|
1317
|
+
.from("workflows")
|
|
1318
|
+
.select("*")
|
|
1319
|
+
.eq("id", wfId)
|
|
1320
|
+
.eq("user_id", userId)
|
|
1321
|
+
.single();
|
|
1322
|
+
if (error) return { wf: null, error: error.message };
|
|
1323
|
+
return { wf: data, error: null };
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
// Helper: save updated graph back to DB
|
|
1327
|
+
async function saveGraph(wfId: string, nodes: any[], edges: any[]) {
|
|
1328
|
+
const { data, error } = await supabase
|
|
1329
|
+
.from("workflows")
|
|
1330
|
+
.update({ nodes, edges, updated_at: new Date().toISOString() })
|
|
1331
|
+
.eq("id", wfId)
|
|
1332
|
+
.eq("user_id", userId)
|
|
1333
|
+
.select()
|
|
1334
|
+
.single();
|
|
1335
|
+
if (error) return { wf: null, error: error.message };
|
|
1336
|
+
return { wf: data, error: null };
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1270
1339
|
switch (action) {
|
|
1271
1340
|
case "list": {
|
|
1272
1341
|
let q = supabase.from("workflows").select("*").eq("user_id", userId).order("updated_at", { ascending: false });
|
|
@@ -1302,7 +1371,6 @@ function registerWorkflowOps(
|
|
|
1302
1371
|
position: { x: 100, y: 200 },
|
|
1303
1372
|
data: { label: "Execute Trigger" },
|
|
1304
1373
|
};
|
|
1305
|
-
// Shift all other nodes to the right
|
|
1306
1374
|
finalNodes = finalNodes.map(n => ({
|
|
1307
1375
|
...n,
|
|
1308
1376
|
position: { x: (n.position?.x || 250) + 200, y: n.position?.y || 200 },
|
|
@@ -1310,23 +1378,14 @@ function registerWorkflowOps(
|
|
|
1310
1378
|
finalNodes.unshift(triggerNode);
|
|
1311
1379
|
}
|
|
1312
1380
|
|
|
1313
|
-
// Build edges
|
|
1314
|
-
let finalEdges = (params.edges as any[]) || [];
|
|
1315
|
-
finalEdges = finalEdges.map((e: any, i: number) => ({
|
|
1316
|
-
id: e.id || `edge-${Date.now()}-${i}`,
|
|
1317
|
-
source: e.source,
|
|
1318
|
-
target: e.target,
|
|
1319
|
-
...(e.sourceHandle ? { sourceHandle: e.sourceHandle } : {}),
|
|
1320
|
-
...(e.targetHandle ? { targetHandle: e.targetHandle } : {}),
|
|
1321
|
-
}));
|
|
1381
|
+
// Build edges
|
|
1382
|
+
let finalEdges = ((params.edges as any[]) || []).map((e: any, i: number) => normalizeEdge(e, i));
|
|
1322
1383
|
|
|
1323
1384
|
// Auto-wire trigger to first non-trigger node if no edge connects from trigger
|
|
1324
|
-
if (hasTrigger
|
|
1325
|
-
// No edges at all — auto-connect trigger → first step
|
|
1326
|
-
} else if (hasTrigger === false && finalNodes.length > 1) {
|
|
1385
|
+
if (!hasTrigger && finalNodes.length > 1) {
|
|
1327
1386
|
const triggerId = finalNodes[0].id;
|
|
1328
1387
|
const firstStepId = finalNodes[1].id;
|
|
1329
|
-
const triggerHasEdge = finalEdges.some(e => e.source === triggerId);
|
|
1388
|
+
const triggerHasEdge = finalEdges.some((e: any) => e.source === triggerId);
|
|
1330
1389
|
if (!triggerHasEdge) {
|
|
1331
1390
|
finalEdges.unshift({
|
|
1332
1391
|
id: `edge-trigger-${Date.now()}`,
|
|
@@ -1360,16 +1419,11 @@ function registerWorkflowOps(
|
|
|
1360
1419
|
for (const f of ["name", "description", "status", "steps", "schedule", "nodes", "edges"]) {
|
|
1361
1420
|
if ((params as any)[f] !== undefined) upd[f] = (params as any)[f];
|
|
1362
1421
|
}
|
|
1363
|
-
// Normalize nodes using the same defaults as create
|
|
1364
1422
|
if (upd.nodes && Array.isArray(upd.nodes)) {
|
|
1365
1423
|
upd.nodes = upd.nodes.map((n: any, i: number) => normalizeNode(n, i));
|
|
1366
1424
|
}
|
|
1367
1425
|
if (upd.edges && Array.isArray(upd.edges)) {
|
|
1368
|
-
upd.edges = upd.edges.map((e: any, i: number) => (
|
|
1369
|
-
id: e.id || `edge-${Date.now()}-${i}`,
|
|
1370
|
-
source: e.source,
|
|
1371
|
-
target: e.target,
|
|
1372
|
-
}));
|
|
1426
|
+
upd.edges = upd.edges.map((e: any, i: number) => normalizeEdge(e, i));
|
|
1373
1427
|
}
|
|
1374
1428
|
const { data, error } = await supabase.from("workflows").update(upd).eq("id", wfId).eq("user_id", userId).select().single();
|
|
1375
1429
|
if (error) return err(error.message);
|
|
@@ -1378,7 +1432,6 @@ function registerWorkflowOps(
|
|
|
1378
1432
|
case "delete": {
|
|
1379
1433
|
const wfId = (params.id || params.workflow_id) as string;
|
|
1380
1434
|
if (!wfId) return err("Missing required: id");
|
|
1381
|
-
// Delete associated runs first
|
|
1382
1435
|
await supabase.from("workflow_runs").delete().eq("workflow_id", wfId);
|
|
1383
1436
|
const { error } = await supabase.from("workflows").delete().eq("id", wfId).eq("user_id", userId);
|
|
1384
1437
|
if (error) return err(error.message);
|
|
@@ -1408,8 +1461,250 @@ function registerWorkflowOps(
|
|
|
1408
1461
|
if (error) return err(error.message);
|
|
1409
1462
|
return ok({ message: `Workflow run triggered`, run_id: runId, workflow_id: wfId });
|
|
1410
1463
|
}
|
|
1464
|
+
|
|
1465
|
+
// ─── Granular Node Operations ────────────────────────────────────
|
|
1466
|
+
|
|
1467
|
+
case "add_nodes": {
|
|
1468
|
+
const wfId = (params.workflow_id || params.id) as string;
|
|
1469
|
+
if (!wfId) return err("Missing required: workflow_id");
|
|
1470
|
+
const newNodes = params.nodes as any[];
|
|
1471
|
+
if (!newNodes || !Array.isArray(newNodes) || newNodes.length === 0) return err("Missing required: nodes[] (array of node definitions)");
|
|
1472
|
+
|
|
1473
|
+
const { wf, error: fetchErr } = await fetchWorkflow(wfId);
|
|
1474
|
+
if (fetchErr || !wf) return err(fetchErr || "Workflow not found");
|
|
1475
|
+
|
|
1476
|
+
const existingNodes = (wf.nodes as any[]) || [];
|
|
1477
|
+
const existingEdges = (wf.edges as any[]) || [];
|
|
1478
|
+
|
|
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
|
+
});
|
|
1488
|
+
|
|
1489
|
+
const allNodes = [...existingNodes, ...normalized];
|
|
1490
|
+
|
|
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
|
+
}
|
|
1497
|
+
|
|
1498
|
+
const { wf: saved, error: saveErr } = await saveGraph(wfId, allNodes, allEdges);
|
|
1499
|
+
if (saveErr) return err(saveErr);
|
|
1500
|
+
|
|
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,
|
|
1507
|
+
});
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
case "update_node": {
|
|
1511
|
+
const wfId = (params.workflow_id || params.id) as string;
|
|
1512
|
+
if (!wfId) return err("Missing required: workflow_id");
|
|
1513
|
+
const nodeId = params.node_id as string;
|
|
1514
|
+
if (!nodeId) return err("Missing required: node_id");
|
|
1515
|
+
const dataUpdate = params.data as Record<string, any>;
|
|
1516
|
+
if (!dataUpdate || typeof dataUpdate !== "object") return err("Missing required: data (object with fields to update)");
|
|
1517
|
+
|
|
1518
|
+
const { wf, error: fetchErr } = await fetchWorkflow(wfId);
|
|
1519
|
+
if (fetchErr || !wf) return err(fetchErr || "Workflow not found");
|
|
1520
|
+
|
|
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.`);
|
|
1524
|
+
|
|
1525
|
+
// Merge new data into existing node data
|
|
1526
|
+
const existingData = nodes[nodeIndex].data || {};
|
|
1527
|
+
nodes[nodeIndex].data = { ...existingData, ...dataUpdate };
|
|
1528
|
+
|
|
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
|
+
}
|
|
1533
|
+
|
|
1534
|
+
const { wf: saved, error: saveErr } = await saveGraph(wfId, nodes, wf.edges || []);
|
|
1535
|
+
if (saveErr) return err(saveErr);
|
|
1536
|
+
|
|
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,
|
|
1542
|
+
});
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
case "delete_nodes": {
|
|
1546
|
+
const wfId = (params.workflow_id || params.id) as string;
|
|
1547
|
+
if (!wfId) return err("Missing required: workflow_id");
|
|
1548
|
+
const nodeIds = params.node_ids as string[];
|
|
1549
|
+
if (!nodeIds || !Array.isArray(nodeIds) || nodeIds.length === 0) return err("Missing required: node_ids[] (array of node IDs to delete)");
|
|
1550
|
+
|
|
1551
|
+
const { wf, error: fetchErr } = await fetchWorkflow(wfId);
|
|
1552
|
+
if (fetchErr || !wf) return err(fetchErr || "Workflow not found");
|
|
1553
|
+
|
|
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));
|
|
1558
|
+
|
|
1559
|
+
const removedNodeCount = ((wf.nodes as any[]) || []).length - nodes.length;
|
|
1560
|
+
const removedEdgeCount = ((wf.edges as any[]) || []).length - edges.length;
|
|
1561
|
+
|
|
1562
|
+
const { wf: saved, error: saveErr } = await saveGraph(wfId, nodes, edges);
|
|
1563
|
+
if (saveErr) return err(saveErr);
|
|
1564
|
+
|
|
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,
|
|
1571
|
+
});
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
case "add_edges": {
|
|
1575
|
+
const wfId = (params.workflow_id || params.id) as string;
|
|
1576
|
+
if (!wfId) return err("Missing required: workflow_id");
|
|
1577
|
+
const newEdges = params.edges as any[];
|
|
1578
|
+
if (!newEdges || !Array.isArray(newEdges) || newEdges.length === 0) return err("Missing required: edges[] (array of edge definitions)");
|
|
1579
|
+
|
|
1580
|
+
const { wf, error: fetchErr } = await fetchWorkflow(wfId);
|
|
1581
|
+
if (fetchErr || !wf) return err(fetchErr || "Workflow not found");
|
|
1582
|
+
|
|
1583
|
+
const existingEdges = (wf.edges as any[]) || [];
|
|
1584
|
+
const nodeIds = new Set(((wf.nodes as any[]) || []).map((n: any) => n.id));
|
|
1585
|
+
|
|
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
|
+
});
|
|
1592
|
+
|
|
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);
|
|
1597
|
+
|
|
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
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
case "delete_edges": {
|
|
1610
|
+
const wfId = (params.workflow_id || params.id) as string;
|
|
1611
|
+
if (!wfId) return err("Missing required: workflow_id");
|
|
1612
|
+
const edgeIds = params.edge_ids as string[];
|
|
1613
|
+
if (!edgeIds || !Array.isArray(edgeIds) || edgeIds.length === 0) return err("Missing required: edge_ids[] (array of edge IDs to delete)");
|
|
1614
|
+
|
|
1615
|
+
const { wf, error: fetchErr } = await fetchWorkflow(wfId);
|
|
1616
|
+
if (fetchErr || !wf) return err(fetchErr || "Workflow not found");
|
|
1617
|
+
|
|
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;
|
|
1621
|
+
|
|
1622
|
+
const { wf: saved, error: saveErr } = await saveGraph(wfId, wf.nodes || [], edges);
|
|
1623
|
+
if (saveErr) return err(saveErr);
|
|
1624
|
+
|
|
1625
|
+
return ok({
|
|
1626
|
+
message: `Deleted ${removedCount} edge(s)`,
|
|
1627
|
+
deleted_edge_ids: edgeIds,
|
|
1628
|
+
remaining_edges: edges.length,
|
|
1629
|
+
workflow: saved,
|
|
1630
|
+
});
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
case "insert_node_between": {
|
|
1634
|
+
const wfId = (params.workflow_id || params.id) as string;
|
|
1635
|
+
if (!wfId) return err("Missing required: workflow_id");
|
|
1636
|
+
const sourceId = params.source_node_id as string;
|
|
1637
|
+
const targetId = params.target_node_id as string;
|
|
1638
|
+
const newNodeDef = params.node as any;
|
|
1639
|
+
if (!sourceId) return err("Missing required: source_node_id");
|
|
1640
|
+
if (!targetId) return err("Missing required: target_node_id");
|
|
1641
|
+
if (!newNodeDef || typeof newNodeDef !== "object") return err("Missing required: node (the node definition to insert)");
|
|
1642
|
+
|
|
1643
|
+
const { wf, error: fetchErr } = await fetchWorkflow(wfId);
|
|
1644
|
+
if (fetchErr || !wf) return err(fetchErr || "Workflow not found");
|
|
1645
|
+
|
|
1646
|
+
const nodes = (wf.nodes as any[]) || [];
|
|
1647
|
+
const edges = (wf.edges as any[]) || [];
|
|
1648
|
+
|
|
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.`);
|
|
1654
|
+
|
|
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.`);
|
|
1658
|
+
|
|
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
|
+
);
|
|
1666
|
+
|
|
1667
|
+
// Remove the original edge
|
|
1668
|
+
const updatedEdges = edges.filter((e: any) => e.id !== connectingEdge.id);
|
|
1669
|
+
|
|
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
|
+
};
|
|
1685
|
+
|
|
1686
|
+
updatedEdges.push(edgeIn, edgeOut);
|
|
1687
|
+
nodes.push(newNode);
|
|
1688
|
+
|
|
1689
|
+
const { wf: saved, error: saveErr } = await saveGraph(wfId, nodes, updatedEdges);
|
|
1690
|
+
if (saveErr) return err(saveErr);
|
|
1691
|
+
|
|
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,
|
|
1703
|
+
});
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1411
1706
|
default:
|
|
1412
|
-
return err(`Unknown action "${action}"
|
|
1707
|
+
return err(`Unknown action "${action}". Valid: list, get, create, update, delete, list_runs, trigger, add_nodes, update_node, delete_nodes, add_edges, delete_edges, insert_node_between`);
|
|
1413
1708
|
}
|
|
1414
1709
|
},
|
|
1415
1710
|
});
|