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.
Files changed (3) hide show
  1. package/package.json +2 -2
  2. package/src/prompt.ts +20 -8
  3. 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.5.5",
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** — Automated workflows (action: "list", "get", "create", "update", "delete", "list_runs", "trigger")
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 by ID
46
- - create: New workflow with name + nodes and edges
47
- - Node types: manual_trigger, agent_step, http_request, formatter_step, task_call, variable_set, condition, human_approval, delay, loop, convergence, output, checkpoint, note
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 (creates a run record)`,
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
- `Actions:\n` +
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, schedule, status\n` +
1162
- `- "update": Update a workflow. Required: id. Optional: name, description, status, nodes, edges, schedule\n` +
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\n` +
1166
- `NODE TYPES (use these exact types when creating nodes):\n` +
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
- ` - "manual_trigger": User clicks Execute to start\n` +
1169
- ` - "webhook_trigger": External HTTP request triggers it\n` +
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
- ` - "agent_step": Delegates task to an AI agent. data: { label, agentId, task, responseMode: "text", timeoutSec: 120 }\n` +
1173
- ` - "http_request": Calls an external API. data: { label, method: "GET"|"POST", url }\n` +
1174
- ` - "formatter_step": Formats/transforms text or JSON. data: { label, template }\n` +
1175
- ` - "task_call": Runs a saved task. data: { label, agentId, taskId }\n` +
1176
- ` - "variable_set": Stores data in a variable. data: { label, variableName, variableValue }\n` +
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
- ` - "condition": If/else branch. data: { label, expression }\n` +
1179
- ` - "human_approval": Pauses for human approval. data: { label, instructions }\n` +
1180
- ` - "delay": Waits for a set time. data: { label, delaySec: 5 }\n` +
1181
- ` - "loop": Repeats actions. data: { label, loopType: "count", maxIterations: 3 }\n` +
1182
- ` - "convergence": Waits for multiple parallel inputs. data: { label, mergeStrategy: "wait_all" }\n` +
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
- ` - "output": Returns final result. data: { label, outputMode: "return" }\n` +
1191
+ ` "output": { label, outputMode: "return"|"notification"|"webhook"|"log", template?: string, webhookUrl?: string, checkpointId?: string }\n\n` +
1185
1192
  ` SPECIAL:\n` +
1186
- ` - "checkpoint": Loop target marker. data: { label }\n` +
1187
- ` - "note": Sticky note annotation. data: { label, noteText }\n\n` +
1188
- `Each node: { type, data: { label, ... }, position?: { x, y } }. IDs and positions are auto-generated if omitted.\n` +
1189
- `Each edge: { source: "node_id", target: "node_id" }. IDs auto-generated.\n` +
1190
- `A manual_trigger node is always auto-prepended if no trigger node is included.`,
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 for runs/trigger" },
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", "schedule_trigger", "agent_step", "formatter_step", "http_request", "task_call", "variable_set", "condition", "human_approval", "delay", "loop", "convergence", "output", "checkpoint", "note"] },
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 type-specific fields." },
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. Each edge: { source, target }",
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 — ensure IDs exist
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 === false && finalNodes.length > 1 && finalEdges.length === 0) {
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
  });