ofiere-openclaw-plugin 3.0.0 → 3.3.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.3.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,98 @@ 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
+ // Parse start_date robustly — it can be:
434
+ // "2026-04-19" (date only)
435
+ // "2026-04-19T18:45:00" (local datetime)
436
+ // "2026-04-19 11:45:00+00" (Supabase timestamptz)
437
+ // "2026-04-19T11:45:00.000Z" (ISO UTC)
438
+ const parsedDate = new Date(startDate);
439
+ const explicitScheduledTime = params.scheduled_time as string | undefined;
440
+
441
+ let nextRunAtEpoch: number;
442
+ let scheduledTimeFinal: string;
443
+ let scheduledDateFinal: string;
444
+
445
+ if (!isNaN(parsedDate.getTime())) {
446
+ // Valid date — check if it includes a meaningful time component
447
+ const hasTimeInfo = /[T ]\d{2}:\d{2}/.test(startDate);
448
+
449
+ if (explicitScheduledTime) {
450
+ // Agent explicitly passed a scheduled_time — use date from start_date + explicit time
451
+ const dateStr = parsedDate.toISOString().split("T")[0]; // YYYY-MM-DD
452
+ const dt = new Date(`${dateStr}T${explicitScheduledTime}:00Z`);
453
+ nextRunAtEpoch = Math.floor(dt.getTime() / 1000);
454
+ scheduledTimeFinal = explicitScheduledTime;
455
+ scheduledDateFinal = dateStr;
456
+ } else if (hasTimeInfo) {
457
+ // start_date already contains time — use it directly
458
+ nextRunAtEpoch = Math.floor(parsedDate.getTime() / 1000);
459
+ scheduledTimeFinal = `${String(parsedDate.getUTCHours()).padStart(2, "0")}:${String(parsedDate.getUTCMinutes()).padStart(2, "0")}`;
460
+ scheduledDateFinal = parsedDate.toISOString().split("T")[0];
461
+ } else {
462
+ // Date only, no time — default to 09:00 UTC
463
+ const dateStr = parsedDate.toISOString().split("T")[0];
464
+ const dt = new Date(`${dateStr}T09:00:00Z`);
465
+ nextRunAtEpoch = Math.floor(dt.getTime() / 1000);
466
+ scheduledTimeFinal = "09:00";
467
+ scheduledDateFinal = dateStr;
468
+ }
469
+ } else {
470
+ // Unparseable date — fallback to now + 60s
471
+ nextRunAtEpoch = Math.floor(Date.now() / 1000) + 60;
472
+ scheduledTimeFinal = "00:00";
473
+ scheduledDateFinal = new Date().toISOString().split("T")[0];
474
+ }
475
+
476
+ // Safety net: if computed time is in the past, schedule for now + 60s
477
+ const nowEpoch = Math.floor(Date.now() / 1000);
478
+ if (nextRunAtEpoch <= nowEpoch) {
479
+ nextRunAtEpoch = nowEpoch + 60;
480
+ }
481
+
482
+ await supabase.from("scheduler_events").insert({
483
+ id: crypto.randomUUID(),
484
+ user_id: userId,
485
+ task_id: id,
486
+ agent_id: effectiveAgentId,
487
+ title: params.title,
488
+ description: (params.description as string) || (params.instructions as string) || null,
489
+ scheduled_date: scheduledDateFinal,
490
+ scheduled_time: scheduledTimeFinal,
491
+ duration_minutes: 30,
492
+ recurrence_type: "none",
493
+ recurrence_interval: 1,
494
+ status: "scheduled",
495
+ next_run_at: nextRunAtEpoch,
496
+ run_count: 0,
497
+ priority: params.priority !== undefined ? params.priority : 1,
498
+ });
499
+ } catch (schedErr) {
500
+ // Non-fatal: task was created, just the scheduler event failed
501
+ console.error("[ofiere] Failed to auto-create scheduler event:", schedErr);
502
+ }
503
+ }
504
+
424
505
  const extras = [];
425
506
  if (cf.execution_plan) extras.push(`${(cf.execution_plan as any[]).length} execution steps`);
426
507
  if (cf.goals) extras.push(`${(cf.goals as any[]).length} goals`);
427
508
  if (cf.constraints) extras.push(`${(cf.constraints as any[]).length} constraints`);
428
509
  if (cf.system_prompt) extras.push("custom system prompt");
510
+ if (startDate) extras.push(`scheduled for ${startDate}`);
429
511
  const extrasStr = extras.length > 0 ? ` with ${extras.join(", ")}` : "";
430
512
 
431
513
  return ok({
432
514
  id,
433
515
  message: `Task "${params.title}" created and assigned to ${assignee || "no one"}${extrasStr}`,
434
516
  task: insertData,
517
+ scheduledExecution: startDate ? `Will auto-execute on ${startDate}` : undefined,
435
518
  });
436
519
  } catch (e) {
437
520
  return err(e instanceof Error ? e.message : String(e));
@@ -516,7 +599,7 @@ async function handleUpdateTask(
516
599
  .update(updates)
517
600
  .eq("id", params.task_id as string)
518
601
  .eq("user_id", userId)
519
- .select("id, title, status, priority, agent_id")
602
+ .select("id, title, status, priority, agent_id, start_date, due_date, progress, updated_at")
520
603
  .single();
521
604
 
522
605
  if (error) return err(error.message);
@@ -934,11 +1017,12 @@ function registerKnowledgeOps(
934
1017
  name: "OFIERE_KNOWLEDGE_OPS",
935
1018
  label: "Ofiere Knowledge Operations",
936
1019
  description:
937
- `Search, add, and manage knowledge documents. This is the long-term memory system.\n\n` +
1020
+ `Access the Ofiere Knowledge Library — the stored knowledge base in the dashboard. ` +
1021
+ `Use this tool whenever the user mentions "knowledge base", "knowledge library", "knowledge entries", or asks to retrieve stored knowledge.\n\n` +
938
1022
  `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` +
1023
+ `- "search": Search the knowledge library by keyword. Required: query. Optional: limit\n` +
1024
+ `- "list": List recent entries from the knowledge library. Optional: page, page_size, search\n` +
1025
+ `- "create": Add a document to the knowledge library. Required: file_name. Optional: content, text, source, source_type, author, credibility_tier\n` +
942
1026
  `- "update": Edit a document. Required: id. Optional: file_name, content, text, source, source_type, author\n` +
943
1027
  `- "delete": Remove a document. Required: id`,
944
1028
  parameters: {
@@ -983,7 +1067,7 @@ function registerKnowledgeOps(
983
1067
  const from = (page - 1) * pageSize;
984
1068
  const to = from + pageSize - 1;
985
1069
  let q = supabase.from("knowledge_documents")
986
- .select("id, file_name, file_type, source, source_type, author, credibility_tier, created_at", { count: "exact" })
1070
+ .select("id, file_name, file_type, content, text, source, source_type, author, credibility_tier, created_at", { count: "exact" })
987
1071
  .order("created_at", { ascending: false })
988
1072
  .range(from, to);
989
1073
  if (params.search) {
@@ -1050,23 +1134,75 @@ function registerWorkflowOps(
1050
1134
  name: "OFIERE_WORKFLOW_OPS",
1051
1135
  label: "Ofiere Workflow Operations",
1052
1136
  description:
1053
- `Manage and trigger automated workflows.\n\n` +
1137
+ `Manage, build, and trigger automated workflows in the Ofiere dashboard.\n\n` +
1054
1138
  `Actions:\n` +
1055
1139
  `- "list": List all workflows. Optional: status\n` +
1056
1140
  `- "get": Get workflow details. Required: id\n` +
1057
- `- "create": Create a workflow. Required: name. Optional: description, steps, schedule, status\n` +
1141
+ `- "create": Create a workflow WITH nodes and edges. Required: name. Optional: description, nodes, edges, schedule, status\n` +
1142
+ `- "update": Update a workflow. Required: id. Optional: name, description, status, nodes, edges, schedule\n` +
1143
+ `- "delete": Delete a workflow and its run history. Required: id\n` +
1058
1144
  `- "list_runs": List recent runs. Required: workflow_id. Optional: limit\n` +
1059
- `- "trigger": Start a workflow run. Required: workflow_id`,
1145
+ `- "trigger": Start a workflow run. Required: workflow_id\n\n` +
1146
+ `NODE TYPES (use these exact types when creating nodes):\n` +
1147
+ ` TRIGGERS (start of workflow — pick one):\n` +
1148
+ ` - "manual_trigger": User clicks Execute to start\n` +
1149
+ ` - "webhook_trigger": External HTTP request triggers it\n` +
1150
+ ` - "schedule_trigger": Runs on cron schedule. data: { label, cron: "0 9 * * 1-5" }\n` +
1151
+ ` STEPS (the work):\n` +
1152
+ ` - "agent_step": Delegates task to an AI agent. data: { label, agentId, task, responseMode: "text", timeoutSec: 120 }\n` +
1153
+ ` - "http_request": Calls an external API. data: { label, method: "GET"|"POST", url }\n` +
1154
+ ` - "formatter_step": Formats/transforms text or JSON. data: { label, template }\n` +
1155
+ ` - "task_call": Runs a saved task. data: { label, agentId, taskId }\n` +
1156
+ ` - "variable_set": Stores data in a variable. data: { label, variableName, variableValue }\n` +
1157
+ ` CONTROL FLOW:\n` +
1158
+ ` - "condition": If/else branch. data: { label, expression }\n` +
1159
+ ` - "human_approval": Pauses for human approval. data: { label, instructions }\n` +
1160
+ ` - "delay": Waits for a set time. data: { label, delaySec: 5 }\n` +
1161
+ ` - "loop": Repeats actions. data: { label, loopType: "count", maxIterations: 3 }\n` +
1162
+ ` - "convergence": Waits for multiple parallel inputs. data: { label, mergeStrategy: "wait_all" }\n` +
1163
+ ` END:\n` +
1164
+ ` - "output": Returns final result. data: { label, outputMode: "return" }\n` +
1165
+ ` SPECIAL:\n` +
1166
+ ` - "checkpoint": Loop target marker. data: { label }\n` +
1167
+ ` - "note": Sticky note annotation. data: { label, noteText }\n\n` +
1168
+ `Each node: { type, data: { label, ... }, position?: { x, y } }. IDs and positions are auto-generated if omitted.\n` +
1169
+ `Each edge: { source: "node_id", target: "node_id" }. IDs auto-generated.\n` +
1170
+ `A manual_trigger node is always auto-prepended if no trigger node is included.`,
1060
1171
  parameters: {
1061
1172
  type: "object",
1062
1173
  required: ["action"],
1063
1174
  properties: {
1064
- action: { type: "string", enum: ["list", "get", "create", "list_runs", "trigger"] },
1175
+ action: { type: "string", enum: ["list", "get", "create", "update", "delete", "list_runs", "trigger"] },
1065
1176
  id: { type: "string", description: "Workflow ID" },
1066
1177
  workflow_id: { type: "string", description: "Workflow ID for runs/trigger" },
1067
1178
  name: { type: "string", description: "Workflow name" },
1068
1179
  description: { type: "string" },
1069
- steps: { type: "array", items: { type: "object" }, description: "Workflow step definitions" },
1180
+ nodes: {
1181
+ type: "array",
1182
+ items: {
1183
+ type: "object",
1184
+ properties: {
1185
+ id: { type: "string", description: "Node ID (auto-generated if omitted)" },
1186
+ 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"] },
1187
+ position: { type: "object", properties: { x: { type: "number" }, y: { type: "number" } } },
1188
+ data: { type: "object", description: "Node config — always include a 'label' field. See NODE TYPES above for type-specific fields." },
1189
+ },
1190
+ },
1191
+ description: "Workflow graph nodes",
1192
+ },
1193
+ edges: {
1194
+ type: "array",
1195
+ items: {
1196
+ type: "object",
1197
+ properties: {
1198
+ id: { type: "string", description: "Edge ID (auto-generated if omitted)" },
1199
+ source: { type: "string", description: "Source node ID" },
1200
+ target: { type: "string", description: "Target node ID" },
1201
+ },
1202
+ },
1203
+ description: "Connections between nodes. Each edge: { source, target }",
1204
+ },
1205
+ steps: { type: "array", items: { type: "object" }, description: "Legacy V1 step definitions" },
1070
1206
  schedule: { type: "string", description: "Cron expression or schedule" },
1071
1207
  status: { type: "string", enum: ["draft", "active", "paused", "archived"] },
1072
1208
  limit: { type: "number", description: "Max results" },
@@ -1074,6 +1210,43 @@ function registerWorkflowOps(
1074
1210
  },
1075
1211
  async execute(_id: string, params: Record<string, unknown>) {
1076
1212
  const action = params.action as string;
1213
+
1214
+ // Default data for each node type — ensures dashboard renders them properly
1215
+ const NODE_DEFAULTS: Record<string, Record<string, any>> = {
1216
+ manual_trigger: { label: "Execute Trigger" },
1217
+ webhook_trigger: { label: "Webhook Trigger" },
1218
+ schedule_trigger: { label: "Schedule Trigger", cron: "0 9 * * 1-5" },
1219
+ agent_step: { label: "Agent Step", agentId: "", task: "", responseMode: "text", timeoutSec: 120 },
1220
+ formatter_step: { label: "Formatter", template: "" },
1221
+ http_request: { label: "HTTP Request", method: "GET", url: "" },
1222
+ task_call: { label: "Task", agentId: "", taskId: "", taskTitle: "", agentName: "" },
1223
+ variable_set: { label: "Set Variable", variableName: "", variableValue: "" },
1224
+ condition: { label: "Condition", expression: "" },
1225
+ human_approval: { label: "Human Approval", instructions: "" },
1226
+ delay: { label: "Delay", delaySec: 5 },
1227
+ loop: { label: "Loop", loopType: "count", maxIterations: 3 },
1228
+ convergence: { label: "Convergence", mergeStrategy: "wait_all" },
1229
+ output: { label: "Output", outputMode: "return" },
1230
+ checkpoint: { label: "Checkpoint" },
1231
+ note: { label: "Note", noteText: "" },
1232
+ };
1233
+
1234
+ // Valid node types
1235
+ const VALID_TYPES = new Set(Object.keys(NODE_DEFAULTS));
1236
+
1237
+ // Helper: normalize a single node with defaults and auto-ID
1238
+ function normalizeNode(n: any, i: number) {
1239
+ let type = n.type || "agent_step";
1240
+ if (!VALID_TYPES.has(type)) type = "agent_step"; // fallback invalid types
1241
+ const defaults = NODE_DEFAULTS[type] || {};
1242
+ return {
1243
+ id: n.id || `${type}-${Date.now()}-${i}`,
1244
+ type,
1245
+ position: n.position || { x: 250, y: 80 + i * 150 },
1246
+ data: { ...defaults, ...(n.data || {}), label: n.data?.label || defaults.label || type },
1247
+ };
1248
+ }
1249
+
1077
1250
  switch (action) {
1078
1251
  case "list": {
1079
1252
  let q = supabase.from("workflows").select("*").eq("user_id", userId).order("updated_at", { ascending: false });
@@ -1095,6 +1268,54 @@ function registerWorkflowOps(
1095
1268
  const stepsWithIds = ((params.steps as any[]) || []).map((s: any, i: number) => ({
1096
1269
  ...s, id: s.id || `step-${i}`,
1097
1270
  }));
1271
+
1272
+ // Build nodes — normalize provided nodes
1273
+ let rawNodes = (params.nodes as any[]) || [];
1274
+ let finalNodes = rawNodes.map((n, i) => normalizeNode(n, i));
1275
+
1276
+ // Auto-prepend a trigger node if none is present
1277
+ const hasTrigger = finalNodes.some(n => n.type.includes("trigger"));
1278
+ if (!hasTrigger) {
1279
+ const triggerNode = {
1280
+ id: `manual_trigger-${Date.now()}`,
1281
+ type: "manual_trigger",
1282
+ position: { x: 100, y: 200 },
1283
+ data: { label: "Execute Trigger" },
1284
+ };
1285
+ // Shift all other nodes to the right
1286
+ finalNodes = finalNodes.map(n => ({
1287
+ ...n,
1288
+ position: { x: (n.position?.x || 250) + 200, y: n.position?.y || 200 },
1289
+ }));
1290
+ finalNodes.unshift(triggerNode);
1291
+ }
1292
+
1293
+ // Build edges — ensure IDs exist
1294
+ let finalEdges = (params.edges as any[]) || [];
1295
+ finalEdges = finalEdges.map((e: any, i: number) => ({
1296
+ id: e.id || `edge-${Date.now()}-${i}`,
1297
+ source: e.source,
1298
+ target: e.target,
1299
+ ...(e.sourceHandle ? { sourceHandle: e.sourceHandle } : {}),
1300
+ ...(e.targetHandle ? { targetHandle: e.targetHandle } : {}),
1301
+ }));
1302
+
1303
+ // Auto-wire trigger to first non-trigger node if no edge connects from trigger
1304
+ if (hasTrigger === false && finalNodes.length > 1 && finalEdges.length === 0) {
1305
+ // No edges at all — auto-connect trigger → first step
1306
+ } else if (hasTrigger === false && finalNodes.length > 1) {
1307
+ const triggerId = finalNodes[0].id;
1308
+ const firstStepId = finalNodes[1].id;
1309
+ const triggerHasEdge = finalEdges.some(e => e.source === triggerId);
1310
+ if (!triggerHasEdge) {
1311
+ finalEdges.unshift({
1312
+ id: `edge-trigger-${Date.now()}`,
1313
+ source: triggerId,
1314
+ target: firstStepId,
1315
+ });
1316
+ }
1317
+ }
1318
+
1098
1319
  const { data, error } = await supabase.from("workflows").insert({
1099
1320
  id: wfId, user_id: userId,
1100
1321
  name: params.name,
@@ -1102,10 +1323,46 @@ function registerWorkflowOps(
1102
1323
  steps: stepsWithIds,
1103
1324
  schedule: (params.schedule as string) || null,
1104
1325
  status: (params.status as string) || "draft",
1105
- nodes: [], edges: [], definition_version: 1,
1326
+ nodes: finalNodes,
1327
+ edges: finalEdges,
1328
+ definition_version: 2,
1106
1329
  }).select().single();
1107
1330
  if (error) return err(error.message);
1108
- return ok({ message: `Workflow "${params.name}" created`, workflow: data });
1331
+ return ok({
1332
+ message: `Workflow "${params.name}" created with ${finalNodes.length} node(s) and ${finalEdges.length} edge(s)`,
1333
+ workflow: data,
1334
+ });
1335
+ }
1336
+ case "update": {
1337
+ const wfId = (params.id || params.workflow_id) as string;
1338
+ if (!wfId) return err("Missing required: id");
1339
+ const upd: Record<string, any> = { updated_at: new Date().toISOString() };
1340
+ for (const f of ["name", "description", "status", "steps", "schedule", "nodes", "edges"]) {
1341
+ if ((params as any)[f] !== undefined) upd[f] = (params as any)[f];
1342
+ }
1343
+ // Normalize nodes using the same defaults as create
1344
+ if (upd.nodes && Array.isArray(upd.nodes)) {
1345
+ upd.nodes = upd.nodes.map((n: any, i: number) => normalizeNode(n, i));
1346
+ }
1347
+ if (upd.edges && Array.isArray(upd.edges)) {
1348
+ upd.edges = upd.edges.map((e: any, i: number) => ({
1349
+ id: e.id || `edge-${Date.now()}-${i}`,
1350
+ source: e.source,
1351
+ target: e.target,
1352
+ }));
1353
+ }
1354
+ const { data, error } = await supabase.from("workflows").update(upd).eq("id", wfId).eq("user_id", userId).select().single();
1355
+ if (error) return err(error.message);
1356
+ return ok({ message: "Workflow updated", workflow: data });
1357
+ }
1358
+ case "delete": {
1359
+ const wfId = (params.id || params.workflow_id) as string;
1360
+ if (!wfId) return err("Missing required: id");
1361
+ // Delete associated runs first
1362
+ await supabase.from("workflow_runs").delete().eq("workflow_id", wfId);
1363
+ const { error } = await supabase.from("workflows").delete().eq("id", wfId).eq("user_id", userId);
1364
+ if (error) return err(error.message);
1365
+ return ok({ message: "Workflow and associated runs deleted", ok: true });
1109
1366
  }
1110
1367
  case "list_runs": {
1111
1368
  const wfId = (params.workflow_id || params.id) as string;
@@ -1323,10 +1580,10 @@ function registerPromptOps(
1323
1580
  description:
1324
1581
  `Manage system prompt instruction chunks. These are the building blocks of agent behavior.\n\n` +
1325
1582
  `Actions:\n` +
1326
- `- "list": List all prompt chunks. Optional: agent_id\n` +
1583
+ `- "list": List all prompt chunks\n` +
1327
1584
  `- "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` +
1585
+ `- "create": Create a new chunk. Required: name, content. Optional: color (hex), category\n` +
1586
+ `- "update": Update a chunk. Required: id. Optional: name, content, color, category, order\n` +
1330
1587
  `- "delete": Delete a chunk. Required: id`,
1331
1588
  parameters: {
1332
1589
  type: "object",
@@ -1334,59 +1591,69 @@ function registerPromptOps(
1334
1591
  properties: {
1335
1592
  action: { type: "string", enum: ["list", "get", "create", "update", "delete"] },
1336
1593
  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" },
1594
+ name: { type: "string", description: "Chunk name/label (max 30 chars)" },
1595
+ content: { type: "string", description: "Prompt chunk content text" },
1596
+ color: { type: "string", description: "Hex color for display (e.g. #6B7280)" },
1597
+ category: { type: "string", description: "Category grouping (e.g. Personality, Instructions)" },
1598
+ order: { type: "number", description: "Display order (0-based)" },
1342
1599
  },
1343
1600
  },
1344
1601
  async execute(_id: string, params: Record<string, unknown>) {
1345
1602
  const action = params.action as string;
1346
1603
  switch (action) {
1347
1604
  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;
1605
+ const { data, error } = await supabase.from("prompt_chunks").select("*").eq("user_id", userId).order("order", { ascending: true });
1351
1606
  if (error) return err(error.message);
1352
1607
  return ok({ chunks: data || [], count: (data || []).length });
1353
1608
  }
1354
1609
  case "get": {
1355
1610
  if (!params.id) return err("Missing required: id");
1356
- const { data, error } = await supabase.from("prompt_chunks").select("*").eq("id", params.id).single();
1611
+ const { data, error } = await supabase.from("prompt_chunks").select("*").eq("id", params.id).eq("user_id", userId).single();
1357
1612
  if (error) return err(error.message);
1358
1613
  return ok({ chunk: data });
1359
1614
  }
1360
1615
  case "create": {
1361
- if (!params.label || !params.content) return err("Missing required: label, content");
1616
+ if (!params.name || !params.content) return err("Missing required: name, content");
1617
+ const chunkName = String(params.name).slice(0, 30);
1362
1618
  const chunkId = crypto.randomUUID();
1619
+
1620
+ // Get current max order to append at end
1621
+ const { data: existing } = await supabase
1622
+ .from("prompt_chunks")
1623
+ .select("order")
1624
+ .eq("user_id", userId);
1625
+ const maxOrder = existing && existing.length > 0
1626
+ ? Math.max(...existing.map((c: any) => c.order ?? 0))
1627
+ : -1;
1628
+
1363
1629
  const { data, error } = await supabase.from("prompt_chunks").insert({
1364
1630
  id: chunkId,
1365
1631
  user_id: userId,
1366
- label: params.label,
1632
+ name: chunkName,
1367
1633
  content: params.content,
1368
- agent_id: (params.agent_id as string) || null,
1369
- enabled: true,
1370
- sort_order: (params.sort_order as number) || 0,
1634
+ color: (params.color as string) || "#6B7280",
1635
+ category: (params.category as string) || "Uncategorized",
1636
+ order: (params.order as number) ?? maxOrder + 1,
1371
1637
  }).select().single();
1372
1638
  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 });
1639
+ api.logger?.info?.(`[ofiere] Prompt chunk created: "${chunkName}" by agent`);
1640
+ return ok({ message: `Prompt chunk "${chunkName}" created`, chunk: data });
1375
1641
  }
1376
1642
  case "update": {
1377
1643
  if (!params.id) return err("Missing required: id");
1378
1644
  const upd: Record<string, any> = { updated_at: new Date().toISOString() };
1379
- for (const f of ["label", "content", "enabled", "sort_order", "agent_id"]) {
1645
+ for (const f of ["name", "content", "color", "category", "order"]) {
1380
1646
  if ((params as any)[f] !== undefined) upd[f] = (params as any)[f];
1381
1647
  }
1382
- const { error } = await supabase.from("prompt_chunks").update(upd).eq("id", params.id);
1648
+ if (upd.name) upd.name = String(upd.name).slice(0, 30);
1649
+ const { data, error } = await supabase.from("prompt_chunks").update(upd).eq("id", params.id).eq("user_id", userId).select().single();
1383
1650
  if (error) return err(error.message);
1384
1651
  api.logger?.info?.(`[ofiere] Prompt chunk ${params.id} updated by agent`);
1385
- return ok({ message: "Prompt chunk updated", ok: true });
1652
+ return ok({ message: "Prompt chunk updated", chunk: data });
1386
1653
  }
1387
1654
  case "delete": {
1388
1655
  if (!params.id) return err("Missing required: id");
1389
- const { error } = await supabase.from("prompt_chunks").delete().eq("id", params.id);
1656
+ const { error } = await supabase.from("prompt_chunks").delete().eq("id", params.id).eq("user_id", userId);
1390
1657
  if (error) return err(error.message);
1391
1658
  api.logger?.info?.(`[ofiere] Prompt chunk ${params.id} deleted by agent`);
1392
1659
  return ok({ message: "Prompt chunk deleted", ok: true });