ofiere-openclaw-plugin 3.0.0 → 3.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/README.md CHANGED
@@ -1,42 +1,27 @@
1
1
  # Ofiere PM Plugin for OpenClaw
2
2
 
3
- Manage your Ofiere PM dashboard directly from OpenClaw agents. Create tasks, update progress, assign agents — all synced to the dashboard in real time.
3
+ Manage your Ofiere PM dashboard directly from OpenClaw agents. Create tasks, manage projects, build workflows, store knowledge — all synced to the dashboard in real time.
4
4
 
5
- ## Install
5
+ ## Quick Install (One-Click)
6
6
 
7
7
  ```bash
8
- openclaw plugins install @ofiere-ai/openclaw-plugin
8
+ curl -sSL https://raw.githubusercontent.com/gilanggemar/Ofiere/main/ofiere-openclaw-plugin/install.sh | bash -s -- \
9
+ --supabase-url "https://xxx.supabase.co" \
10
+ --service-key "eyJ..." \
11
+ --user-id "your-uuid"
9
12
  ```
10
13
 
11
- Or install from the local repo (for development):
14
+ Only 3 parameters needed. All agents get the plugin automatically.
12
15
 
13
- ```bash
14
- openclaw plugins install ./ofiere-openclaw-plugin
15
- ```
16
-
17
- Restart OpenClaw after installing.
18
-
19
- ## Setup
20
-
21
- ```bash
22
- openclaw ofiere setup --supabase-url "https://xxx.supabase.co" --service-key "eyJ..." --user-id "your-uuid" --agent-id "sasha"
23
- ```
24
-
25
- Or run interactively:
16
+ ## Uninstall
26
17
 
27
18
  ```bash
28
- openclaw ofiere setup
19
+ curl -sSL https://raw.githubusercontent.com/gilanggemar/Ofiere/main/ofiere-openclaw-plugin/uninstall.sh | bash
29
20
  ```
30
21
 
31
- Then restart the gateway:
32
-
33
- ```bash
34
- openclaw gateway restart
35
- ```
22
+ ## How It Works
36
23
 
37
- ## How it works
38
-
39
- Once configured, the plugin connects to your Supabase database at gateway startup and registers PM tools directly into the agent. There's no separate MCP server process — it runs inside the OpenClaw gateway for maximum reliability.
24
+ Once configured, the plugin connects to your Supabase database at gateway startup and registers 9 PM meta-tools directly into the agent. There's no separate MCP server process — it runs inside the OpenClaw gateway for maximum reliability.
40
25
 
41
26
  Changes made by the agent are immediately visible on the Ofiere dashboard (Vercel) via Supabase real-time subscriptions.
42
27
 
@@ -46,20 +31,38 @@ The plugin uses a scalable meta-tool architecture. Each tool handles one domain
46
31
 
47
32
  | Tool | Actions | Description |
48
33
  |---|---|---|
49
- | `OFIERE_TASK_OPS` | `list`, `create`, `update`, `delete` | Manage PM tasks list, create, update status/priority, delete |
34
+ | `OFIERE_TASK_OPS` | `list`, `create`, `update`, `delete` | Rich task management with execution plans, goals, constraints |
50
35
  | `OFIERE_AGENT_OPS` | `list` | Query available agents for task assignment |
36
+ | `OFIERE_PROJECT_OPS` | `list_spaces`, `create_space`, `create_folder`, `bulk_create_tasks`, etc. | Full project hierarchy: spaces → folders → tasks |
37
+ | `OFIERE_SCHEDULE_OPS` | `list`, `create`, `update`, `delete` | Calendar events with recurrence |
38
+ | `OFIERE_KNOWLEDGE_OPS` | `search`, `list`, `create`, `update`, `delete` | Knowledge library with full content retrieval |
39
+ | `OFIERE_WORKFLOW_OPS` | `list`, `get`, `create`, `update`, `delete`, `list_runs`, `trigger` | Visual workflow builder with 16 node types |
40
+ | `OFIERE_NOTIFY_OPS` | `list`, `mark_read`, `create` | In-app notifications |
41
+ | `OFIERE_MEMORY_OPS` | `list_conversations`, `search_knowledge` | Conversation history & agent memory |
42
+ | `OFIERE_PROMPT_OPS` | `list`, `get`, `create`, `update`, `delete` | System prompt chunk management |
51
43
 
52
44
  ### Example
53
45
 
54
46
  ```
55
- // Create a task
56
- OFIERE_TASK_OPS({ action: "create", title: "Deploy v2", agent_id: "ivy" })
57
-
58
- // List tasks
59
- OFIERE_TASK_OPS({ action: "list", status: "PENDING", limit: 10 })
60
-
61
- // Update a task
62
- OFIERE_TASK_OPS({ action: "update", task_id: "task-123", status: "DONE" })
47
+ // Create a task with execution plan
48
+ OFIERE_TASK_OPS({ action: "create", title: "Deploy v2", agent_id: "ivy",
49
+ execution_plan: [{ step: 1, action: "Build", detail: "Run production build" }] })
50
+
51
+ // Create a workflow with nodes
52
+ OFIERE_WORKFLOW_OPS({ action: "create", name: "Deploy Pipeline",
53
+ nodes: [
54
+ { type: "agent_step", data: { label: "Build", task: "Run npm build" } },
55
+ { type: "human_approval", data: { label: "Review", instructions: "Check build output" } },
56
+ { type: "output", data: { label: "Done" } }
57
+ ],
58
+ edges: [
59
+ { source: "agent_step-...", target: "human_approval-..." },
60
+ { source: "human_approval-...", target: "output-..." }
61
+ ]
62
+ })
63
+
64
+ // Search knowledge library
65
+ OFIERE_KNOWLEDGE_OPS({ action: "search", query: "API rate limits" })
63
66
  ```
64
67
 
65
68
  ## CLI Commands
@@ -79,7 +82,7 @@ Set via `openclaw ofiere setup` or environment variables:
79
82
  | `supabaseUrl` | `OFIERE_SUPABASE_URL` | Supabase project URL |
80
83
  | `serviceRoleKey` | `OFIERE_SERVICE_ROLE_KEY` | Supabase service role key |
81
84
  | `userId` | `OFIERE_USER_ID` | Your user UUID |
82
- | `agentId` | `OFIERE_AGENT_ID` | This agent's ID (e.g. `sasha`) |
85
+ | `agentId` | `OFIERE_AGENT_ID` | This agent's ID (optional — auto-detected) |
83
86
  | `enabled` | — | Enable/disable the plugin (default: `true`) |
84
87
 
85
88
  ## Architecture
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ofiere-openclaw-plugin",
3
- "version": "3.0.0",
3
+ "version": "3.2.0",
4
4
  "type": "module",
5
5
  "description": "OpenClaw plugin for Ofiere PM — 9 meta-tools covering tasks, agents, projects, scheduling, knowledge, workflows, notifications, memory, and prompts",
6
6
  "keywords": ["openclaw", "ofiere", "project-management", "agents", "plugin"],
package/src/prompt.ts CHANGED
@@ -11,7 +11,8 @@
11
11
  const TOOL_DOCS: Record<string, string> = {
12
12
  OFIERE_TASK_OPS: `- **OFIERE_TASK_OPS** — Manage tasks (action: "list", "create", "update", "delete")
13
13
  - list: Filter by status, agent_id, space_id, folder_id, limit. Returns execution_plan, goals, constraints if present
14
- - create: Requires title. Optional: agent_id, description, instructions, execution_plan, goals, constraints, system_prompt, priority, tags, dates
14
+ - create: Requires title. IMPORTANT: Always pass agent_id with your own name to self-assign (e.g. agent_id: "celia")
15
+ - Optional: description, instructions, execution_plan, goals, constraints, system_prompt, priority, tags, dates
15
16
  - For COMPLEX tasks: include execution_plan (step-by-step), goals, constraints, and system_prompt
16
17
  - For SIMPLE tasks: just title and optionally description
17
18
  - update: Requires task_id. All create fields + progress
@@ -32,16 +33,22 @@ const TOOL_DOCS: Record<string, string> = {
32
33
  - create: Requires title + scheduled_date (YYYY-MM-DD). Optional: scheduled_time, duration_minutes, task_id, agent_id, recurrence_type, color
33
34
  - Recurrence: none, hourly, daily, weekly, monthly with interval`,
34
35
 
35
- OFIERE_KNOWLEDGE_OPS: `- **OFIERE_KNOWLEDGE_OPS** — Knowledge base documents (action: "search", "list", "create", "update", "delete")
36
- - search: Keyword search across all docs. Requires: query
37
- - list: Paginated listing with optional search filter
38
- - create: Add knowledge. Requires: file_name. Optional: content, source, author, credibility_tier
36
+ OFIERE_KNOWLEDGE_OPS: `- **OFIERE_KNOWLEDGE_OPS** — Ofiere Knowledge Library (action: "search", "list", "create", "update", "delete")
37
+ - ALWAYS use this tool when the user mentions "knowledge base", "knowledge library", "knowledge entries", or asks to recall/retrieve stored knowledge
38
+ - search: Keyword search across all stored documents. Requires: query
39
+ - list: Paginated listing of knowledge entries with full content. Optional: search filter
40
+ - create: Add knowledge to the library. Requires: file_name. Optional: content, source, author, credibility_tier
39
41
  - update/delete: By document ID`,
40
42
 
41
- OFIERE_WORKFLOW_OPS: `- **OFIERE_WORKFLOW_OPS** — Automated workflows (action: "list", "get", "create", "list_runs", "trigger")
43
+ OFIERE_WORKFLOW_OPS: `- **OFIERE_WORKFLOW_OPS** — Automated workflows (action: "list", "get", "create", "update", "delete", "list_runs", "trigger")
42
44
  - list: All workflows, filter by status (draft, active, paused, archived)
43
45
  - get: Full workflow details by ID
44
- - create: New workflow with name, steps, schedule
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)
51
+ - delete: Remove workflow and all run history
45
52
  - list_runs: Recent execution history for a workflow
46
53
  - trigger: Start a workflow run (creates a run record)`,
47
54
 
@@ -58,9 +65,10 @@ const TOOL_DOCS: Record<string, string> = {
58
65
  - search_knowledge: Search stored knowledge for an agent`,
59
66
 
60
67
  OFIERE_PROMPT_OPS: `- **OFIERE_PROMPT_OPS** — Manage prompt instruction chunks (action: "list", "get", "create", "update", "delete")
61
- - list: All prompt chunks, filter by agent_id
62
- - create: New chunk with label + content. These modify agent behavior
63
- - update: Change label, content, enabled state, or sort_order
68
+ - list: All prompt chunks ordered by display order
69
+ - create: New chunk with name (max 30 chars) + content. Optional: color (hex), category
70
+ - update: Change name, content, color, category, or order
71
+ - delete: Remove a chunk by ID
64
72
  - All modifications are logged for audit`,
65
73
  };
66
74
 
@@ -93,6 +101,7 @@ Each tool uses an "action" parameter to select the operation. Always include act
93
101
  ${toolDocs}
94
102
 
95
103
  ## Rules
104
+ - ALWAYS pass agent_id with your own name when creating tasks (e.g. agent_id: "ivy"). Auto-detection is NOT reliable.
96
105
  - ${assignRule}
97
106
  - To create an unassigned task, pass agent_id as "none" or "unassigned".
98
107
  - When the user says "create a task for [agent name]", use OFIERE_AGENT_OPS action:"list" to find the agent ID, then use OFIERE_TASK_OPS action:"create" with that agent_id.
@@ -104,6 +113,7 @@ ${toolDocs}
104
113
  - For complex tasks, ALWAYS include execution_plan, goals, and constraints. For simple tasks, just title is enough.
105
114
  - When creating dependencies, use OFIERE_PROJECT_OPS to link predecessor/successor tasks.
106
115
  - Prompt chunk modifications (OFIERE_PROMPT_OPS) are powerful — use thoughtfully as they change agent behavior.
116
+ - When the user asks about "knowledge base", "knowledge library", "knowledge entries", or wants to recall stored knowledge, ALWAYS use OFIERE_KNOWLEDGE_OPS — do NOT rely on your own memory for this.
107
117
  </ofiere-pm>`;
108
118
  }
109
119
 
package/src/tools.ts CHANGED
@@ -106,6 +106,17 @@ export function probeApiForAgentName(api: any, logger?: any): string {
106
106
 
107
107
  // ─── Shared: Agent ID Resolution ─────────────────────────────────────────────
108
108
 
109
+ // System/plugin names that should never be treated as real agent identifiers.
110
+ // These come from the OpenClaw gateway registration context, not from actual agents.
111
+ const SYSTEM_NAME_BLOCKLIST = new Set([
112
+ "ofiere pm", "ofiere", "openclaw", "system", "plugin", "gateway", "admin",
113
+ "ofiere pm plugin", "ofiere-openclaw-plugin",
114
+ ]);
115
+
116
+ function isSystemName(name: string): boolean {
117
+ return SYSTEM_NAME_BLOCKLIST.has(name.toLowerCase().trim());
118
+ }
119
+
109
120
  function createAgentResolver(
110
121
  api: any,
111
122
  supabase: SupabaseClient,
@@ -114,47 +125,38 @@ function createAgentResolver(
114
125
  ) {
115
126
  /**
116
127
  * Resolve the agent ID for the calling agent.
117
- * Priority: explicit param > runtime context > registration-time detection > env var > DB fallback
128
+ * Priority: explicit param > env var > DB fallback
129
+ *
130
+ * NOTE: We intentionally skip runtime/registration detection because the
131
+ * OpenClaw api object returns the PLUGIN name ("Ofiere PM"), not the
132
+ * calling agent's name. Each agent must pass its own name via agent_id.
118
133
  */
119
134
  return async function resolveAgent(explicitId?: string): Promise<string | null> {
120
- // 1. Explicit agent_id passed by the LLM (e.g. "ivy", "daisy", or a UUID)
135
+ // 1. Explicit agent_id passed by the LLM (e.g. "ivy", "celia", or a UUID)
121
136
  if (explicitId && explicitId.trim()) {
122
137
  const trimmed = explicitId.trim();
123
- // If it looks like a UUID or our ID format, use directly
124
- if (trimmed.match(/^[0-9a-f]{8}-/) || trimmed.match(/^agent-/)) {
125
- return trimmed;
126
- }
127
- // Otherwise treat as a name and resolve to the actual agent ID
128
- try {
129
- return await resolveAgentId(trimmed, userId, supabase);
130
- } catch {
131
- return trimmed; // fallback: use as-is
132
- }
133
- }
134
138
 
135
- // 2. Runtime: read calling agent's name from OpenClaw context
136
- const callerName = getCallingAgentName(api);
137
- if (callerName) {
138
- try {
139
- return await resolveAgentId(callerName, userId, supabase);
140
- } catch {
141
- // Fall through
142
- }
143
- }
144
-
145
- // 3. Registration-time detection (set when plugin was loaded)
146
- if (_registrationAgentName) {
147
- try {
148
- return await resolveAgentId(_registrationAgentName, userId, supabase);
149
- } catch {
150
- // Fall through
139
+ // Block system names from being used as agent IDs
140
+ if (isSystemName(trimmed)) {
141
+ // Fall through to DB fallback
142
+ } else if (trimmed.match(/^[0-9a-f]{8}-/) || trimmed.match(/^agent-/)) {
143
+ // Looks like a UUID or our ID format — use directly
144
+ return trimmed;
145
+ } else {
146
+ // Treat as a name and resolve to the actual agent ID
147
+ try {
148
+ const resolved = await resolveAgentId(trimmed, userId, supabase);
149
+ if (resolved && !isSystemName(resolved)) return resolved;
150
+ } catch {
151
+ // Fall through
152
+ }
151
153
  }
152
154
  }
153
155
 
154
- // 4. Env var fallback (OFIERE_AGENT_ID — legacy single-agent mode)
156
+ // 2. Env var fallback (OFIERE_AGENT_ID — legacy single-agent mode)
155
157
  if (fallbackAgentId) return fallbackAgentId;
156
158
 
157
- // 5. Nuclear fallback: query the FIRST agent for this user
159
+ // 3. Nuclear fallback: query the FIRST agent for this user
158
160
  try {
159
161
  const { data } = await supabase
160
162
  .from("agents")
@@ -343,7 +345,7 @@ async function handleCreateTask(
343
345
  try {
344
346
  if (!params.title) return err("Missing required field: title");
345
347
 
346
- const id = `task-${Date.now()}`;
348
+ const id = `task-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
347
349
  const now = new Date().toISOString();
348
350
 
349
351
  // Handle explicit "none"/"unassigned"
@@ -421,17 +423,55 @@ async function handleCreateTask(
421
423
  return err(error.message);
422
424
  }
423
425
 
426
+ // ── Auto-create scheduler event if task has a start_date ──────────────
427
+ // This bridges the plugin → scheduler so the pg_cron task-dispatcher
428
+ // Edge Function picks up the task at the right time.
429
+ const startDate = params.start_date as string | undefined;
430
+ const effectiveAgentId = (insertData.agent_id as string) || assignee;
431
+ if (startDate && effectiveAgentId) {
432
+ try {
433
+ const scheduledTime = (params.scheduled_time as string) || "09:00"; // default 9am
434
+ const datePart = startDate; // YYYY-MM-DD
435
+ const timePart = scheduledTime; // HH:MM
436
+ const dt = new Date(`${datePart}T${timePart}:00`);
437
+ const nextRunAt = Math.floor(dt.getTime() / 1000);
438
+
439
+ await supabase.from("scheduler_events").insert({
440
+ id: crypto.randomUUID(),
441
+ user_id: userId,
442
+ task_id: id,
443
+ agent_id: effectiveAgentId,
444
+ title: params.title,
445
+ description: (params.description as string) || (params.instructions as string) || null,
446
+ scheduled_date: datePart,
447
+ scheduled_time: timePart,
448
+ duration_minutes: 30,
449
+ recurrence_type: "none",
450
+ recurrence_interval: 1,
451
+ status: "scheduled",
452
+ next_run_at: nextRunAt,
453
+ run_count: 0,
454
+ priority: params.priority !== undefined ? params.priority : 1,
455
+ });
456
+ } catch (schedErr) {
457
+ // Non-fatal: task was created, just the scheduler event failed
458
+ console.error("[ofiere] Failed to auto-create scheduler event:", schedErr);
459
+ }
460
+ }
461
+
424
462
  const extras = [];
425
463
  if (cf.execution_plan) extras.push(`${(cf.execution_plan as any[]).length} execution steps`);
426
464
  if (cf.goals) extras.push(`${(cf.goals as any[]).length} goals`);
427
465
  if (cf.constraints) extras.push(`${(cf.constraints as any[]).length} constraints`);
428
466
  if (cf.system_prompt) extras.push("custom system prompt");
467
+ if (startDate) extras.push(`scheduled for ${startDate}`);
429
468
  const extrasStr = extras.length > 0 ? ` with ${extras.join(", ")}` : "";
430
469
 
431
470
  return ok({
432
471
  id,
433
472
  message: `Task "${params.title}" created and assigned to ${assignee || "no one"}${extrasStr}`,
434
473
  task: insertData,
474
+ scheduledExecution: startDate ? `Will auto-execute on ${startDate}` : undefined,
435
475
  });
436
476
  } catch (e) {
437
477
  return err(e instanceof Error ? e.message : String(e));
@@ -516,7 +556,7 @@ async function handleUpdateTask(
516
556
  .update(updates)
517
557
  .eq("id", params.task_id as string)
518
558
  .eq("user_id", userId)
519
- .select("id, title, status, priority, agent_id")
559
+ .select("id, title, status, priority, agent_id, start_date, due_date, progress, updated_at")
520
560
  .single();
521
561
 
522
562
  if (error) return err(error.message);
@@ -934,11 +974,12 @@ function registerKnowledgeOps(
934
974
  name: "OFIERE_KNOWLEDGE_OPS",
935
975
  label: "Ofiere Knowledge Operations",
936
976
  description:
937
- `Search, add, and manage knowledge documents. This is the long-term memory system.\n\n` +
977
+ `Access the Ofiere Knowledge Library — the stored knowledge base in the dashboard. ` +
978
+ `Use this tool whenever the user mentions "knowledge base", "knowledge library", "knowledge entries", or asks to retrieve stored knowledge.\n\n` +
938
979
  `Actions:\n` +
939
- `- "search": Search docs by keyword. Required: query. Optional: limit\n` +
940
- `- "list": List recent docs. Optional: page, page_size, search\n` +
941
- `- "create": Add a document. Required: file_name. Optional: content, text, source, source_type, author, credibility_tier\n` +
980
+ `- "search": Search the knowledge library by keyword. Required: query. Optional: limit\n` +
981
+ `- "list": List recent entries from the knowledge library. Optional: page, page_size, search\n` +
982
+ `- "create": Add a document to the knowledge library. Required: file_name. Optional: content, text, source, source_type, author, credibility_tier\n` +
942
983
  `- "update": Edit a document. Required: id. Optional: file_name, content, text, source, source_type, author\n` +
943
984
  `- "delete": Remove a document. Required: id`,
944
985
  parameters: {
@@ -983,7 +1024,7 @@ function registerKnowledgeOps(
983
1024
  const from = (page - 1) * pageSize;
984
1025
  const to = from + pageSize - 1;
985
1026
  let q = supabase.from("knowledge_documents")
986
- .select("id, file_name, file_type, source, source_type, author, credibility_tier, created_at", { count: "exact" })
1027
+ .select("id, file_name, file_type, content, text, source, source_type, author, credibility_tier, created_at", { count: "exact" })
987
1028
  .order("created_at", { ascending: false })
988
1029
  .range(from, to);
989
1030
  if (params.search) {
@@ -1050,23 +1091,75 @@ function registerWorkflowOps(
1050
1091
  name: "OFIERE_WORKFLOW_OPS",
1051
1092
  label: "Ofiere Workflow Operations",
1052
1093
  description:
1053
- `Manage and trigger automated workflows.\n\n` +
1094
+ `Manage, build, and trigger automated workflows in the Ofiere dashboard.\n\n` +
1054
1095
  `Actions:\n` +
1055
1096
  `- "list": List all workflows. Optional: status\n` +
1056
1097
  `- "get": Get workflow details. Required: id\n` +
1057
- `- "create": Create a workflow. Required: name. Optional: description, steps, schedule, status\n` +
1098
+ `- "create": Create a workflow WITH nodes and edges. Required: name. Optional: description, nodes, edges, schedule, status\n` +
1099
+ `- "update": Update a workflow. Required: id. Optional: name, description, status, nodes, edges, schedule\n` +
1100
+ `- "delete": Delete a workflow and its run history. Required: id\n` +
1058
1101
  `- "list_runs": List recent runs. Required: workflow_id. Optional: limit\n` +
1059
- `- "trigger": Start a workflow run. Required: workflow_id`,
1102
+ `- "trigger": Start a workflow run. Required: workflow_id\n\n` +
1103
+ `NODE TYPES (use these exact types when creating nodes):\n` +
1104
+ ` TRIGGERS (start of workflow — pick one):\n` +
1105
+ ` - "manual_trigger": User clicks Execute to start\n` +
1106
+ ` - "webhook_trigger": External HTTP request triggers it\n` +
1107
+ ` - "schedule_trigger": Runs on cron schedule. data: { label, cron: "0 9 * * 1-5" }\n` +
1108
+ ` STEPS (the work):\n` +
1109
+ ` - "agent_step": Delegates task to an AI agent. data: { label, agentId, task, responseMode: "text", timeoutSec: 120 }\n` +
1110
+ ` - "http_request": Calls an external API. data: { label, method: "GET"|"POST", url }\n` +
1111
+ ` - "formatter_step": Formats/transforms text or JSON. data: { label, template }\n` +
1112
+ ` - "task_call": Runs a saved task. data: { label, agentId, taskId }\n` +
1113
+ ` - "variable_set": Stores data in a variable. data: { label, variableName, variableValue }\n` +
1114
+ ` CONTROL FLOW:\n` +
1115
+ ` - "condition": If/else branch. data: { label, expression }\n` +
1116
+ ` - "human_approval": Pauses for human approval. data: { label, instructions }\n` +
1117
+ ` - "delay": Waits for a set time. data: { label, delaySec: 5 }\n` +
1118
+ ` - "loop": Repeats actions. data: { label, loopType: "count", maxIterations: 3 }\n` +
1119
+ ` - "convergence": Waits for multiple parallel inputs. data: { label, mergeStrategy: "wait_all" }\n` +
1120
+ ` END:\n` +
1121
+ ` - "output": Returns final result. data: { label, outputMode: "return" }\n` +
1122
+ ` SPECIAL:\n` +
1123
+ ` - "checkpoint": Loop target marker. data: { label }\n` +
1124
+ ` - "note": Sticky note annotation. data: { label, noteText }\n\n` +
1125
+ `Each node: { type, data: { label, ... }, position?: { x, y } }. IDs and positions are auto-generated if omitted.\n` +
1126
+ `Each edge: { source: "node_id", target: "node_id" }. IDs auto-generated.\n` +
1127
+ `A manual_trigger node is always auto-prepended if no trigger node is included.`,
1060
1128
  parameters: {
1061
1129
  type: "object",
1062
1130
  required: ["action"],
1063
1131
  properties: {
1064
- action: { type: "string", enum: ["list", "get", "create", "list_runs", "trigger"] },
1132
+ action: { type: "string", enum: ["list", "get", "create", "update", "delete", "list_runs", "trigger"] },
1065
1133
  id: { type: "string", description: "Workflow ID" },
1066
1134
  workflow_id: { type: "string", description: "Workflow ID for runs/trigger" },
1067
1135
  name: { type: "string", description: "Workflow name" },
1068
1136
  description: { type: "string" },
1069
- steps: { type: "array", items: { type: "object" }, description: "Workflow step definitions" },
1137
+ nodes: {
1138
+ type: "array",
1139
+ items: {
1140
+ type: "object",
1141
+ properties: {
1142
+ id: { type: "string", description: "Node ID (auto-generated if omitted)" },
1143
+ 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"] },
1144
+ position: { type: "object", properties: { x: { type: "number" }, y: { type: "number" } } },
1145
+ data: { type: "object", description: "Node config — always include a 'label' field. See NODE TYPES above for type-specific fields." },
1146
+ },
1147
+ },
1148
+ description: "Workflow graph nodes",
1149
+ },
1150
+ edges: {
1151
+ type: "array",
1152
+ items: {
1153
+ type: "object",
1154
+ properties: {
1155
+ id: { type: "string", description: "Edge ID (auto-generated if omitted)" },
1156
+ source: { type: "string", description: "Source node ID" },
1157
+ target: { type: "string", description: "Target node ID" },
1158
+ },
1159
+ },
1160
+ description: "Connections between nodes. Each edge: { source, target }",
1161
+ },
1162
+ steps: { type: "array", items: { type: "object" }, description: "Legacy V1 step definitions" },
1070
1163
  schedule: { type: "string", description: "Cron expression or schedule" },
1071
1164
  status: { type: "string", enum: ["draft", "active", "paused", "archived"] },
1072
1165
  limit: { type: "number", description: "Max results" },
@@ -1074,6 +1167,43 @@ function registerWorkflowOps(
1074
1167
  },
1075
1168
  async execute(_id: string, params: Record<string, unknown>) {
1076
1169
  const action = params.action as string;
1170
+
1171
+ // Default data for each node type — ensures dashboard renders them properly
1172
+ const NODE_DEFAULTS: Record<string, Record<string, any>> = {
1173
+ manual_trigger: { label: "Execute Trigger" },
1174
+ webhook_trigger: { label: "Webhook Trigger" },
1175
+ schedule_trigger: { label: "Schedule Trigger", cron: "0 9 * * 1-5" },
1176
+ agent_step: { label: "Agent Step", agentId: "", task: "", responseMode: "text", timeoutSec: 120 },
1177
+ formatter_step: { label: "Formatter", template: "" },
1178
+ http_request: { label: "HTTP Request", method: "GET", url: "" },
1179
+ task_call: { label: "Task", agentId: "", taskId: "", taskTitle: "", agentName: "" },
1180
+ variable_set: { label: "Set Variable", variableName: "", variableValue: "" },
1181
+ condition: { label: "Condition", expression: "" },
1182
+ human_approval: { label: "Human Approval", instructions: "" },
1183
+ delay: { label: "Delay", delaySec: 5 },
1184
+ loop: { label: "Loop", loopType: "count", maxIterations: 3 },
1185
+ convergence: { label: "Convergence", mergeStrategy: "wait_all" },
1186
+ output: { label: "Output", outputMode: "return" },
1187
+ checkpoint: { label: "Checkpoint" },
1188
+ note: { label: "Note", noteText: "" },
1189
+ };
1190
+
1191
+ // Valid node types
1192
+ const VALID_TYPES = new Set(Object.keys(NODE_DEFAULTS));
1193
+
1194
+ // Helper: normalize a single node with defaults and auto-ID
1195
+ function normalizeNode(n: any, i: number) {
1196
+ let type = n.type || "agent_step";
1197
+ if (!VALID_TYPES.has(type)) type = "agent_step"; // fallback invalid types
1198
+ const defaults = NODE_DEFAULTS[type] || {};
1199
+ return {
1200
+ id: n.id || `${type}-${Date.now()}-${i}`,
1201
+ type,
1202
+ position: n.position || { x: 250, y: 80 + i * 150 },
1203
+ data: { ...defaults, ...(n.data || {}), label: n.data?.label || defaults.label || type },
1204
+ };
1205
+ }
1206
+
1077
1207
  switch (action) {
1078
1208
  case "list": {
1079
1209
  let q = supabase.from("workflows").select("*").eq("user_id", userId).order("updated_at", { ascending: false });
@@ -1095,6 +1225,54 @@ function registerWorkflowOps(
1095
1225
  const stepsWithIds = ((params.steps as any[]) || []).map((s: any, i: number) => ({
1096
1226
  ...s, id: s.id || `step-${i}`,
1097
1227
  }));
1228
+
1229
+ // Build nodes — normalize provided nodes
1230
+ let rawNodes = (params.nodes as any[]) || [];
1231
+ let finalNodes = rawNodes.map((n, i) => normalizeNode(n, i));
1232
+
1233
+ // Auto-prepend a trigger node if none is present
1234
+ const hasTrigger = finalNodes.some(n => n.type.includes("trigger"));
1235
+ if (!hasTrigger) {
1236
+ const triggerNode = {
1237
+ id: `manual_trigger-${Date.now()}`,
1238
+ type: "manual_trigger",
1239
+ position: { x: 100, y: 200 },
1240
+ data: { label: "Execute Trigger" },
1241
+ };
1242
+ // Shift all other nodes to the right
1243
+ finalNodes = finalNodes.map(n => ({
1244
+ ...n,
1245
+ position: { x: (n.position?.x || 250) + 200, y: n.position?.y || 200 },
1246
+ }));
1247
+ finalNodes.unshift(triggerNode);
1248
+ }
1249
+
1250
+ // Build edges — ensure IDs exist
1251
+ let finalEdges = (params.edges as any[]) || [];
1252
+ finalEdges = finalEdges.map((e: any, i: number) => ({
1253
+ id: e.id || `edge-${Date.now()}-${i}`,
1254
+ source: e.source,
1255
+ target: e.target,
1256
+ ...(e.sourceHandle ? { sourceHandle: e.sourceHandle } : {}),
1257
+ ...(e.targetHandle ? { targetHandle: e.targetHandle } : {}),
1258
+ }));
1259
+
1260
+ // Auto-wire trigger to first non-trigger node if no edge connects from trigger
1261
+ if (hasTrigger === false && finalNodes.length > 1 && finalEdges.length === 0) {
1262
+ // No edges at all — auto-connect trigger → first step
1263
+ } else if (hasTrigger === false && finalNodes.length > 1) {
1264
+ const triggerId = finalNodes[0].id;
1265
+ const firstStepId = finalNodes[1].id;
1266
+ const triggerHasEdge = finalEdges.some(e => e.source === triggerId);
1267
+ if (!triggerHasEdge) {
1268
+ finalEdges.unshift({
1269
+ id: `edge-trigger-${Date.now()}`,
1270
+ source: triggerId,
1271
+ target: firstStepId,
1272
+ });
1273
+ }
1274
+ }
1275
+
1098
1276
  const { data, error } = await supabase.from("workflows").insert({
1099
1277
  id: wfId, user_id: userId,
1100
1278
  name: params.name,
@@ -1102,10 +1280,46 @@ function registerWorkflowOps(
1102
1280
  steps: stepsWithIds,
1103
1281
  schedule: (params.schedule as string) || null,
1104
1282
  status: (params.status as string) || "draft",
1105
- nodes: [], edges: [], definition_version: 1,
1283
+ nodes: finalNodes,
1284
+ edges: finalEdges,
1285
+ definition_version: 2,
1106
1286
  }).select().single();
1107
1287
  if (error) return err(error.message);
1108
- return ok({ message: `Workflow "${params.name}" created`, workflow: data });
1288
+ return ok({
1289
+ message: `Workflow "${params.name}" created with ${finalNodes.length} node(s) and ${finalEdges.length} edge(s)`,
1290
+ workflow: data,
1291
+ });
1292
+ }
1293
+ case "update": {
1294
+ const wfId = (params.id || params.workflow_id) as string;
1295
+ if (!wfId) return err("Missing required: id");
1296
+ const upd: Record<string, any> = { updated_at: new Date().toISOString() };
1297
+ for (const f of ["name", "description", "status", "steps", "schedule", "nodes", "edges"]) {
1298
+ if ((params as any)[f] !== undefined) upd[f] = (params as any)[f];
1299
+ }
1300
+ // Normalize nodes using the same defaults as create
1301
+ if (upd.nodes && Array.isArray(upd.nodes)) {
1302
+ upd.nodes = upd.nodes.map((n: any, i: number) => normalizeNode(n, i));
1303
+ }
1304
+ if (upd.edges && Array.isArray(upd.edges)) {
1305
+ upd.edges = upd.edges.map((e: any, i: number) => ({
1306
+ id: e.id || `edge-${Date.now()}-${i}`,
1307
+ source: e.source,
1308
+ target: e.target,
1309
+ }));
1310
+ }
1311
+ const { data, error } = await supabase.from("workflows").update(upd).eq("id", wfId).eq("user_id", userId).select().single();
1312
+ if (error) return err(error.message);
1313
+ return ok({ message: "Workflow updated", workflow: data });
1314
+ }
1315
+ case "delete": {
1316
+ const wfId = (params.id || params.workflow_id) as string;
1317
+ if (!wfId) return err("Missing required: id");
1318
+ // Delete associated runs first
1319
+ await supabase.from("workflow_runs").delete().eq("workflow_id", wfId);
1320
+ const { error } = await supabase.from("workflows").delete().eq("id", wfId).eq("user_id", userId);
1321
+ if (error) return err(error.message);
1322
+ return ok({ message: "Workflow and associated runs deleted", ok: true });
1109
1323
  }
1110
1324
  case "list_runs": {
1111
1325
  const wfId = (params.workflow_id || params.id) as string;
@@ -1323,10 +1537,10 @@ function registerPromptOps(
1323
1537
  description:
1324
1538
  `Manage system prompt instruction chunks. These are the building blocks of agent behavior.\n\n` +
1325
1539
  `Actions:\n` +
1326
- `- "list": List all prompt chunks. Optional: agent_id\n` +
1540
+ `- "list": List all prompt chunks\n` +
1327
1541
  `- "get": Get a specific chunk. Required: id\n` +
1328
- `- "create": Create a new chunk. Required: label, content. Optional: agent_id, sort_order\n` +
1329
- `- "update": Update a chunk. Required: id. Optional: label, content, enabled, sort_order\n` +
1542
+ `- "create": Create a new chunk. Required: name, content. Optional: color (hex), category\n` +
1543
+ `- "update": Update a chunk. Required: id. Optional: name, content, color, category, order\n` +
1330
1544
  `- "delete": Delete a chunk. Required: id`,
1331
1545
  parameters: {
1332
1546
  type: "object",
@@ -1334,59 +1548,69 @@ function registerPromptOps(
1334
1548
  properties: {
1335
1549
  action: { type: "string", enum: ["list", "get", "create", "update", "delete"] },
1336
1550
  id: { type: "string", description: "Chunk ID" },
1337
- label: { type: "string", description: "Chunk label/name" },
1338
- content: { type: "string", description: "Prompt chunk content" },
1339
- agent_id: { type: "string", description: "Associate with specific agent" },
1340
- enabled: { type: "boolean", description: "Whether chunk is active" },
1341
- sort_order: { type: "number", description: "Display order" },
1551
+ name: { type: "string", description: "Chunk name/label (max 30 chars)" },
1552
+ content: { type: "string", description: "Prompt chunk content text" },
1553
+ color: { type: "string", description: "Hex color for display (e.g. #6B7280)" },
1554
+ category: { type: "string", description: "Category grouping (e.g. Personality, Instructions)" },
1555
+ order: { type: "number", description: "Display order (0-based)" },
1342
1556
  },
1343
1557
  },
1344
1558
  async execute(_id: string, params: Record<string, unknown>) {
1345
1559
  const action = params.action as string;
1346
1560
  switch (action) {
1347
1561
  case "list": {
1348
- let q = supabase.from("prompt_chunks").select("*").eq("user_id", userId).order("sort_order");
1349
- if (params.agent_id) q = q.eq("agent_id", params.agent_id as string);
1350
- const { data, error } = await q;
1562
+ const { data, error } = await supabase.from("prompt_chunks").select("*").eq("user_id", userId).order("order", { ascending: true });
1351
1563
  if (error) return err(error.message);
1352
1564
  return ok({ chunks: data || [], count: (data || []).length });
1353
1565
  }
1354
1566
  case "get": {
1355
1567
  if (!params.id) return err("Missing required: id");
1356
- const { data, error } = await supabase.from("prompt_chunks").select("*").eq("id", params.id).single();
1568
+ const { data, error } = await supabase.from("prompt_chunks").select("*").eq("id", params.id).eq("user_id", userId).single();
1357
1569
  if (error) return err(error.message);
1358
1570
  return ok({ chunk: data });
1359
1571
  }
1360
1572
  case "create": {
1361
- if (!params.label || !params.content) return err("Missing required: label, content");
1573
+ if (!params.name || !params.content) return err("Missing required: name, content");
1574
+ const chunkName = String(params.name).slice(0, 30);
1362
1575
  const chunkId = crypto.randomUUID();
1576
+
1577
+ // Get current max order to append at end
1578
+ const { data: existing } = await supabase
1579
+ .from("prompt_chunks")
1580
+ .select("order")
1581
+ .eq("user_id", userId);
1582
+ const maxOrder = existing && existing.length > 0
1583
+ ? Math.max(...existing.map((c: any) => c.order ?? 0))
1584
+ : -1;
1585
+
1363
1586
  const { data, error } = await supabase.from("prompt_chunks").insert({
1364
1587
  id: chunkId,
1365
1588
  user_id: userId,
1366
- label: params.label,
1589
+ name: chunkName,
1367
1590
  content: params.content,
1368
- agent_id: (params.agent_id as string) || null,
1369
- enabled: true,
1370
- sort_order: (params.sort_order as number) || 0,
1591
+ color: (params.color as string) || "#6B7280",
1592
+ category: (params.category as string) || "Uncategorized",
1593
+ order: (params.order as number) ?? maxOrder + 1,
1371
1594
  }).select().single();
1372
1595
  if (error) return err(error.message);
1373
- api.logger?.info?.(`[ofiere] Prompt chunk created: "${params.label}" by agent`);
1374
- return ok({ message: `Prompt chunk "${params.label}" created`, chunk: data });
1596
+ api.logger?.info?.(`[ofiere] Prompt chunk created: "${chunkName}" by agent`);
1597
+ return ok({ message: `Prompt chunk "${chunkName}" created`, chunk: data });
1375
1598
  }
1376
1599
  case "update": {
1377
1600
  if (!params.id) return err("Missing required: id");
1378
1601
  const upd: Record<string, any> = { updated_at: new Date().toISOString() };
1379
- for (const f of ["label", "content", "enabled", "sort_order", "agent_id"]) {
1602
+ for (const f of ["name", "content", "color", "category", "order"]) {
1380
1603
  if ((params as any)[f] !== undefined) upd[f] = (params as any)[f];
1381
1604
  }
1382
- const { error } = await supabase.from("prompt_chunks").update(upd).eq("id", params.id);
1605
+ if (upd.name) upd.name = String(upd.name).slice(0, 30);
1606
+ const { data, error } = await supabase.from("prompt_chunks").update(upd).eq("id", params.id).eq("user_id", userId).select().single();
1383
1607
  if (error) return err(error.message);
1384
1608
  api.logger?.info?.(`[ofiere] Prompt chunk ${params.id} updated by agent`);
1385
- return ok({ message: "Prompt chunk updated", ok: true });
1609
+ return ok({ message: "Prompt chunk updated", chunk: data });
1386
1610
  }
1387
1611
  case "delete": {
1388
1612
  if (!params.id) return err("Missing required: id");
1389
- const { error } = await supabase.from("prompt_chunks").delete().eq("id", params.id);
1613
+ const { error } = await supabase.from("prompt_chunks").delete().eq("id", params.id).eq("user_id", userId);
1390
1614
  if (error) return err(error.message);
1391
1615
  api.logger?.info?.(`[ofiere] Prompt chunk ${params.id} deleted by agent`);
1392
1616
  return ok({ message: "Prompt chunk deleted", ok: true });