ofiere-openclaw-plugin 2.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/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")
@@ -172,7 +174,9 @@ function createAgentResolver(
172
174
  };
173
175
  }
174
176
 
175
- // ─── META-TOOL: OFIERE_TASK_OPS ──────────────────────────────────────────────
177
+ // ═══════════════════════════════════════════════════════════════════════════════
178
+ // META-TOOL 1: OFIERE_TASK_OPS — Task Management
179
+ // ═══════════════════════════════════════════════════════════════════════════════
176
180
 
177
181
  function registerTaskOps(
178
182
  api: any,
@@ -186,13 +190,14 @@ function registerTaskOps(
186
190
  description:
187
191
  `Manage tasks in the Ofiere PM dashboard. All task operations go through this tool.\n\n` +
188
192
  `Actions:\n` +
189
- `- "list": List/filter tasks. Optional params: status, agent_id, space_id, folder_id, limit\n` +
190
- `- "create": Create a new task. Required: title, agent_id. Optional: description, status, priority, space_id, folder_id, start_date, due_date, tags\n` +
191
- `- "update": Update an existing task. Required: task_id. Optional: title, description, status, priority, progress, agent_id, start_date, due_date, tags\n` +
192
- `- "delete": Delete a task and its subtasks. Required: task_id\n\n` +
193
- `agent_id for create: Pass your own name (e.g. 'ivy') to self-assign, another agent's name to assign to them, or 'none'/'unassigned' for no assignee.\n` +
194
- `Status values: PENDING, IN_PROGRESS, DONE, FAILED\n` +
195
- `Priority values: 0=LOW, 1=MEDIUM, 2=HIGH, 3=CRITICAL`,
193
+ `- "list": List/filter tasks. Optional: status, agent_id, space_id, folder_id, limit\n` +
194
+ `- "create": Create a task. Required: title. Optional: agent_id, description, status, priority, space_id, folder_id, start_date, due_date, tags, instructions, execution_plan, goals, constraints, system_prompt\n` +
195
+ `- "update": Update a task. Required: task_id. Optional: all create fields + progress\n` +
196
+ `- "delete": Delete task + subtasks. Required: task_id\n\n` +
197
+ `For complex tasks, fill in execution_plan (step-by-step plan), goals, constraints, and system_prompt to help the executing agent.\n` +
198
+ `For simple tasks, just provide title and optionally description.\n` +
199
+ `agent_id: Pass your name to self-assign, another agent's name, or 'none'.\n` +
200
+ `Status: PENDING, IN_PROGRESS, DONE, FAILED | Priority: 0=LOW, 1=MEDIUM, 2=HIGH, 3=CRITICAL`,
196
201
  parameters: {
197
202
  type: "object",
198
203
  required: ["action"],
@@ -202,14 +207,13 @@ function registerTaskOps(
202
207
  description: "The operation to perform",
203
208
  enum: ["list", "create", "update", "delete"],
204
209
  },
205
- // ── Shared / contextual params ──
206
210
  task_id: { type: "string", description: "Task ID (required for update, delete)" },
207
211
  title: { type: "string", description: "Task title (required for create)" },
208
212
  description: { type: "string", description: "Task description" },
213
+ instructions: { type: "string", description: "Detailed instructions for the agent executing this task" },
209
214
  agent_id: {
210
215
  type: "string",
211
- description:
212
- "Agent name or ID. For create: your name to self-assign, another name to delegate, 'none' for unassigned. For list: filter by agent.",
216
+ description: "Agent name or ID. Your name to self-assign, 'none' for unassigned.",
213
217
  },
214
218
  status: {
215
219
  type: "string",
@@ -227,6 +231,42 @@ function registerTaskOps(
227
231
  items: { type: "string" },
228
232
  description: "Tags for the task",
229
233
  },
234
+ execution_plan: {
235
+ type: "array",
236
+ items: {
237
+ type: "object",
238
+ properties: {
239
+ text: { type: "string", description: "Step description" },
240
+ },
241
+ required: ["text"],
242
+ },
243
+ description: "Ordered execution steps for complex tasks. Each step: { text: '...' }",
244
+ },
245
+ goals: {
246
+ type: "array",
247
+ items: {
248
+ type: "object",
249
+ properties: {
250
+ type: { type: "string", enum: ["budget", "stack", "legal", "deadline", "custom"], description: "Goal category" },
251
+ label: { type: "string", description: "Goal description" },
252
+ },
253
+ required: ["label"],
254
+ },
255
+ description: "Task goals. Each: { type?: 'budget'|'stack'|'legal'|'deadline'|'custom', label: '...' }",
256
+ },
257
+ constraints: {
258
+ type: "array",
259
+ items: {
260
+ type: "object",
261
+ properties: {
262
+ type: { type: "string", enum: ["budget", "stack", "legal", "deadline", "custom"], description: "Constraint category" },
263
+ label: { type: "string", description: "Constraint description" },
264
+ },
265
+ required: ["label"],
266
+ },
267
+ description: "Task constraints. Each: { type?: 'budget'|'stack'|'legal'|'deadline'|'custom', label: '...' }",
268
+ },
269
+ system_prompt: { type: "string", description: "Custom system prompt injection for the executing agent" },
230
270
  limit: { type: "number", description: "Max results for list (default 50)" },
231
271
  },
232
272
  },
@@ -263,7 +303,7 @@ async function handleListTasks(
263
303
  .from("tasks")
264
304
  .select(
265
305
  "id, title, description, status, priority, agent_id, space_id, folder_id, " +
266
- "start_date, due_date, progress, created_at, updated_at",
306
+ "start_date, due_date, progress, tags, custom_fields, created_at, updated_at",
267
307
  )
268
308
  .eq("user_id", userId)
269
309
  .order("updated_at", { ascending: false });
@@ -276,7 +316,21 @@ async function handleListTasks(
276
316
 
277
317
  const { data, error } = await query;
278
318
  if (error) return err(error.message);
279
- return ok({ tasks: data || [], count: (data || []).length });
319
+
320
+ // Unpack custom_fields for readability
321
+ const tasks = (data || []).map((t: any) => {
322
+ const cf = t.custom_fields || {};
323
+ return {
324
+ ...t,
325
+ execution_plan: cf.execution_plan || undefined,
326
+ goals: cf.goals || undefined,
327
+ constraints: cf.constraints || undefined,
328
+ system_prompt: cf.system_prompt || undefined,
329
+ instructions: cf.instructions || t.description || undefined,
330
+ };
331
+ });
332
+
333
+ return ok({ tasks, count: tasks.length });
280
334
  } catch (e) {
281
335
  return err(e instanceof Error ? e.message : String(e));
282
336
  }
@@ -291,7 +345,7 @@ async function handleCreateTask(
291
345
  try {
292
346
  if (!params.title) return err("Missing required field: title");
293
347
 
294
- const id = `task-${Date.now()}`;
348
+ const id = `task-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
295
349
  const now = new Date().toISOString();
296
350
 
297
351
  // Handle explicit "none"/"unassigned"
@@ -302,11 +356,41 @@ async function handleCreateTask(
302
356
 
303
357
  const assignee = isUnassigned ? null : await resolveAgent(rawAgentId);
304
358
 
359
+ // Build custom_fields from task-ops extended fields
360
+ const cf: Record<string, unknown> = {};
361
+
362
+ if (params.execution_plan && Array.isArray(params.execution_plan) && (params.execution_plan as any[]).length > 0) {
363
+ cf.execution_plan = (params.execution_plan as any[]).map((step: any, i: number) => ({
364
+ id: `step-${Date.now()}-${i}`,
365
+ text: typeof step === "string" ? step : step.text || String(step),
366
+ order: i,
367
+ }));
368
+ }
369
+
370
+ if (params.goals && Array.isArray(params.goals) && (params.goals as any[]).length > 0) {
371
+ cf.goals = (params.goals as any[]).map((g: any, i: number) => ({
372
+ id: `goal-${Date.now()}-${i}`,
373
+ type: g.type || "custom",
374
+ label: typeof g === "string" ? g : g.label || String(g),
375
+ }));
376
+ }
377
+
378
+ if (params.constraints && Array.isArray(params.constraints) && (params.constraints as any[]).length > 0) {
379
+ cf.constraints = (params.constraints as any[]).map((c: any, i: number) => ({
380
+ id: `cstr-${Date.now()}-${i}`,
381
+ type: c.type || "custom",
382
+ label: typeof c === "string" ? c : c.label || String(c),
383
+ }));
384
+ }
385
+
386
+ if (params.system_prompt) cf.system_prompt = params.system_prompt;
387
+ if (params.instructions) cf.instructions = params.instructions;
388
+
305
389
  const insertData: Record<string, unknown> = {
306
390
  id,
307
391
  user_id: userId,
308
392
  title: params.title,
309
- description: (params.description as string) || null,
393
+ description: (params.description as string) || (params.instructions as string) || null,
310
394
  agent_id: assignee,
311
395
  assignee_type: "agent",
312
396
  status: (params.status as string) || "PENDING",
@@ -318,7 +402,7 @@ async function handleCreateTask(
318
402
  tags: (params.tags as string[]) || [],
319
403
  progress: 0,
320
404
  sort_order: 0,
321
- custom_fields: {},
405
+ custom_fields: Object.keys(cf).length > 0 ? cf : {},
322
406
  created_at: now,
323
407
  updated_at: now,
324
408
  };
@@ -339,10 +423,55 @@ async function handleCreateTask(
339
423
  return err(error.message);
340
424
  }
341
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
+
462
+ const extras = [];
463
+ if (cf.execution_plan) extras.push(`${(cf.execution_plan as any[]).length} execution steps`);
464
+ if (cf.goals) extras.push(`${(cf.goals as any[]).length} goals`);
465
+ if (cf.constraints) extras.push(`${(cf.constraints as any[]).length} constraints`);
466
+ if (cf.system_prompt) extras.push("custom system prompt");
467
+ if (startDate) extras.push(`scheduled for ${startDate}`);
468
+ const extrasStr = extras.length > 0 ? ` with ${extras.join(", ")}` : "";
469
+
342
470
  return ok({
343
471
  id,
344
- message: `Task "${params.title}" created and assigned to ${assignee || "no one"}`,
472
+ message: `Task "${params.title}" created and assigned to ${assignee || "no one"}${extrasStr}`,
345
473
  task: insertData,
474
+ scheduledExecution: startDate ? `Will auto-execute on ${startDate}` : undefined,
346
475
  });
347
476
  } catch (e) {
348
477
  return err(e instanceof Error ? e.message : String(e));
@@ -367,12 +496,67 @@ async function handleUpdateTask(
367
496
  }
368
497
  if (params.status === "DONE") updates.completed_at = new Date().toISOString();
369
498
 
499
+ // Handle custom_fields updates (execution_plan, goals, constraints, system_prompt, instructions)
500
+ const hasCustomFields = params.execution_plan !== undefined ||
501
+ params.goals !== undefined ||
502
+ params.constraints !== undefined ||
503
+ params.system_prompt !== undefined ||
504
+ params.instructions !== undefined;
505
+
506
+ if (hasCustomFields) {
507
+ // Fetch existing custom_fields to merge
508
+ const { data: existing } = await supabase
509
+ .from("tasks")
510
+ .select("custom_fields")
511
+ .eq("id", params.task_id as string)
512
+ .eq("user_id", userId)
513
+ .single();
514
+
515
+ const existingCf = (existing?.custom_fields || {}) as Record<string, any>;
516
+ const mergedCf = { ...existingCf };
517
+
518
+ if (params.execution_plan !== undefined) {
519
+ mergedCf.execution_plan = Array.isArray(params.execution_plan)
520
+ ? (params.execution_plan as any[]).map((step: any, i: number) => ({
521
+ id: step.id || `step-${Date.now()}-${i}`,
522
+ text: typeof step === "string" ? step : step.text || String(step),
523
+ order: i,
524
+ }))
525
+ : [];
526
+ }
527
+
528
+ if (params.goals !== undefined) {
529
+ mergedCf.goals = Array.isArray(params.goals)
530
+ ? (params.goals as any[]).map((g: any, i: number) => ({
531
+ id: g.id || `goal-${Date.now()}-${i}`,
532
+ type: g.type || "custom",
533
+ label: typeof g === "string" ? g : g.label || String(g),
534
+ }))
535
+ : [];
536
+ }
537
+
538
+ if (params.constraints !== undefined) {
539
+ mergedCf.constraints = Array.isArray(params.constraints)
540
+ ? (params.constraints as any[]).map((c: any, i: number) => ({
541
+ id: c.id || `cstr-${Date.now()}-${i}`,
542
+ type: c.type || "custom",
543
+ label: typeof c === "string" ? c : c.label || String(c),
544
+ }))
545
+ : [];
546
+ }
547
+
548
+ if (params.system_prompt !== undefined) mergedCf.system_prompt = params.system_prompt;
549
+ if (params.instructions !== undefined) mergedCf.instructions = params.instructions;
550
+
551
+ updates.custom_fields = mergedCf;
552
+ }
553
+
370
554
  const { data, error } = await supabase
371
555
  .from("tasks")
372
556
  .update(updates)
373
557
  .eq("id", params.task_id as string)
374
558
  .eq("user_id", userId)
375
- .select("id, title, status, priority, agent_id")
559
+ .select("id, title, status, priority, agent_id, start_date, due_date, progress, updated_at")
376
560
  .single();
377
561
 
378
562
  if (error) return err(error.message);
@@ -423,7 +607,9 @@ async function handleDeleteTask(
423
607
  }
424
608
  }
425
609
 
426
- // ─── META-TOOL: OFIERE_AGENT_OPS ────────────────────────────────────────────
610
+ // ═══════════════════════════════════════════════════════════════════════════════
611
+ // META-TOOL 2: OFIERE_AGENT_OPS — Agent Management
612
+ // ═══════════════════════════════════════════════════════════════════════════════
427
613
 
428
614
  function registerAgentOps(
429
615
  api: any,
@@ -495,11 +681,952 @@ async function handleListAgents(
495
681
  }
496
682
  }
497
683
 
498
- // ─── Public: Register All Meta-Tools ─────────────────────────────────────────
684
+ // ═══════════════════════════════════════════════════════════════════════════════
685
+ // META-TOOL 3: OFIERE_PROJECT_OPS — Spaces, Folders & Dependencies
686
+ // ═══════════════════════════════════════════════════════════════════════════════
687
+
688
+ function registerProjectOps(
689
+ api: any,
690
+ supabase: SupabaseClient,
691
+ userId: string,
692
+ ): void {
693
+ api.registerTool({
694
+ name: "OFIERE_PROJECT_OPS",
695
+ label: "Ofiere Project Operations",
696
+ description:
697
+ `Manage PM hierarchy: spaces, folders, and task dependencies.\n\n` +
698
+ `Actions:\n` +
699
+ `- "list_spaces": List all PM spaces\n` +
700
+ `- "create_space": Create a space. Required: name. Optional: description, icon, icon_color\n` +
701
+ `- "update_space": Update a space. Required: id. Optional: name, description, icon, icon_color, sort_order\n` +
702
+ `- "delete_space": Delete a space. Required: id\n` +
703
+ `- "list_folders": List folders. Optional: space_id to filter\n` +
704
+ `- "create_folder": Create. Required: name, space_id. Optional: parent_folder_id, folder_type\n` +
705
+ `- "update_folder": Update. Required: id. Optional: name, space_id, parent_folder_id, sort_order\n` +
706
+ `- "delete_folder": Delete. Required: id\n` +
707
+ `- "list_dependencies": List task dependencies. Optional: task_id\n` +
708
+ `- "add_dependency": Link tasks. Required: predecessor_id, successor_id. Optional: dependency_type, lag_days\n` +
709
+ `- "remove_dependency": Unlink. Required: dependency_id\n` +
710
+ `dependency_type: finish_to_start (default), start_to_start, finish_to_finish, start_to_finish`,
711
+ parameters: {
712
+ type: "object",
713
+ required: ["action"],
714
+ properties: {
715
+ action: {
716
+ type: "string",
717
+ description: "The operation to perform",
718
+ enum: ["list_spaces", "create_space", "update_space", "delete_space",
719
+ "list_folders", "create_folder", "update_folder", "delete_folder",
720
+ "list_dependencies", "add_dependency", "remove_dependency"],
721
+ },
722
+ id: { type: "string", description: "Space, folder, or dependency ID" },
723
+ name: { type: "string", description: "Name for space/folder" },
724
+ description: { type: "string", description: "Description" },
725
+ icon: { type: "string", description: "Emoji icon for space" },
726
+ icon_color: { type: "string", description: "Hex color for space icon" },
727
+ space_id: { type: "string", description: "Parent space ID" },
728
+ parent_folder_id: { type: "string", description: "Parent folder ID for nesting" },
729
+ folder_type: { type: "string", enum: ["folder", "project"], description: "Folder type" },
730
+ sort_order: { type: "number", description: "Sort order" },
731
+ predecessor_id: { type: "string", description: "Task that must complete first" },
732
+ successor_id: { type: "string", description: "Task that depends on predecessor" },
733
+ dependency_type: {
734
+ type: "string",
735
+ enum: ["finish_to_start", "start_to_start", "finish_to_finish", "start_to_finish"],
736
+ description: "Type of dependency link",
737
+ },
738
+ lag_days: { type: "number", description: "Days of lag between tasks (default 0)" },
739
+ task_id: { type: "string", description: "Filter dependencies by task ID" },
740
+ dependency_id: { type: "string", description: "Dependency ID to remove" },
741
+ },
742
+ },
743
+ async execute(_id: string, params: Record<string, unknown>) {
744
+ const action = params.action as string;
745
+ switch (action) {
746
+ // ── Spaces ──
747
+ case "list_spaces": {
748
+ const { data, error } = await supabase.from("pm_spaces").select("*").eq("user_id", userId).order("sort_order");
749
+ if (error) return err(error.message);
750
+ return ok({ spaces: data || [], count: (data || []).length });
751
+ }
752
+ case "create_space": {
753
+ if (!params.name) return err("Missing required: name");
754
+ const { data, error } = await supabase.from("pm_spaces").insert({
755
+ user_id: userId,
756
+ name: params.name,
757
+ description: (params.description as string) || "",
758
+ icon: (params.icon as string) || "📁",
759
+ icon_color: (params.icon_color as string) || "#FF6D29",
760
+ access_type: "private",
761
+ sort_order: (params.sort_order as number) || 0,
762
+ }).select().single();
763
+ if (error) return err(error.message);
764
+ return ok({ message: `Space "${params.name}" created`, space: data });
765
+ }
766
+ case "update_space": {
767
+ if (!params.id) return err("Missing required: id");
768
+ const upd: Record<string, any> = { updated_at: new Date().toISOString() };
769
+ for (const f of ["name", "description", "icon", "icon_color", "sort_order"]) {
770
+ if ((params as any)[f] !== undefined) upd[f] = (params as any)[f];
771
+ }
772
+ const { error } = await supabase.from("pm_spaces").update(upd).eq("id", params.id).eq("user_id", userId);
773
+ if (error) return err(error.message);
774
+ return ok({ message: "Space updated", ok: true });
775
+ }
776
+ case "delete_space": {
777
+ if (!params.id) return err("Missing required: id");
778
+ const { error } = await supabase.from("pm_spaces").delete().eq("id", params.id).eq("user_id", userId);
779
+ if (error) return err(error.message);
780
+ return ok({ message: "Space deleted", ok: true });
781
+ }
782
+ // ── Folders ──
783
+ case "list_folders": {
784
+ let q = supabase.from("pm_folders").select("*").eq("user_id", userId).order("sort_order");
785
+ if (params.space_id) q = q.eq("space_id", params.space_id as string);
786
+ const { data, error } = await q;
787
+ if (error) return err(error.message);
788
+ return ok({ folders: data || [], count: (data || []).length });
789
+ }
790
+ case "create_folder": {
791
+ if (!params.name || !params.space_id) return err("Missing required: name, space_id");
792
+ const { data, error } = await supabase.from("pm_folders").insert({
793
+ user_id: userId,
794
+ space_id: params.space_id,
795
+ parent_folder_id: (params.parent_folder_id as string) || null,
796
+ name: params.name,
797
+ description: "",
798
+ folder_type: (params.folder_type as string) || "folder",
799
+ sort_order: (params.sort_order as number) || 0,
800
+ }).select().single();
801
+ if (error) return err(error.message);
802
+ return ok({ message: `Folder "${params.name}" created`, folder: data });
803
+ }
804
+ case "update_folder": {
805
+ if (!params.id) return err("Missing required: id");
806
+ const upd: Record<string, any> = { updated_at: new Date().toISOString() };
807
+ for (const f of ["name", "description", "space_id", "parent_folder_id", "folder_type", "sort_order"]) {
808
+ if ((params as any)[f] !== undefined) upd[f] = (params as any)[f];
809
+ }
810
+ const { error } = await supabase.from("pm_folders").update(upd).eq("id", params.id).eq("user_id", userId);
811
+ if (error) return err(error.message);
812
+ return ok({ message: "Folder updated", ok: true });
813
+ }
814
+ case "delete_folder": {
815
+ if (!params.id) return err("Missing required: id");
816
+ const { error } = await supabase.from("pm_folders").delete().eq("id", params.id).eq("user_id", userId);
817
+ if (error) return err(error.message);
818
+ return ok({ message: "Folder deleted", ok: true });
819
+ }
820
+ // ── Dependencies ──
821
+ case "list_dependencies": {
822
+ let q = supabase.from("pm_dependencies").select("*").eq("user_id", userId);
823
+ if (params.task_id) {
824
+ q = supabase.from("pm_dependencies").select("*").eq("user_id", userId)
825
+ .or(`predecessor_id.eq.${params.task_id},successor_id.eq.${params.task_id}`);
826
+ }
827
+ const { data, error } = await q;
828
+ if (error) return err(error.message);
829
+ return ok({ dependencies: data || [], count: (data || []).length });
830
+ }
831
+ case "add_dependency": {
832
+ if (!params.predecessor_id || !params.successor_id) return err("Missing required: predecessor_id, successor_id");
833
+ const { data, error } = await supabase.from("pm_dependencies").insert({
834
+ user_id: userId,
835
+ predecessor_id: params.predecessor_id,
836
+ successor_id: params.successor_id,
837
+ dependency_type: (params.dependency_type as string) || "finish_to_start",
838
+ lag_days: (params.lag_days as number) || 0,
839
+ }).select().single();
840
+ if (error) return err(error.message);
841
+ return ok({ message: "Dependency created", dependency: data });
842
+ }
843
+ case "remove_dependency": {
844
+ const depId = (params.dependency_id || params.id) as string;
845
+ if (!depId) return err("Missing required: dependency_id");
846
+ const { error } = await supabase.from("pm_dependencies").delete().eq("id", depId).eq("user_id", userId);
847
+ if (error) return err(error.message);
848
+ return ok({ message: "Dependency removed", ok: true });
849
+ }
850
+ default:
851
+ return err(`Unknown action "${action}".`);
852
+ }
853
+ },
854
+ });
855
+ }
856
+
857
+ // ═══════════════════════════════════════════════════════════════════════════════
858
+ // META-TOOL 4: OFIERE_SCHEDULE_OPS — Calendar & Scheduler Events
859
+ // ═══════════════════════════════════════════════════════════════════════════════
860
+
861
+ function registerScheduleOps(
862
+ api: any,
863
+ supabase: SupabaseClient,
864
+ userId: string,
865
+ ): void {
866
+ api.registerTool({
867
+ name: "OFIERE_SCHEDULE_OPS",
868
+ label: "Ofiere Schedule Operations",
869
+ description:
870
+ `Manage calendar events and schedule tasks on the timeline.\n\n` +
871
+ `Actions:\n` +
872
+ `- "list": List events. Optional: start_date, end_date, agent_id\n` +
873
+ `- "create": Schedule an event. Required: title, scheduled_date. Optional: task_id, agent_id, scheduled_time, duration_minutes, recurrence_type, recurrence_interval, color, priority\n` +
874
+ `- "update": Update event. Required: id. Optional: title, scheduled_date, scheduled_time, duration_minutes, status, recurrence_type\n` +
875
+ `- "delete": Remove event. Required: id\n` +
876
+ `recurrence_type: none, hourly, daily, weekly, monthly\n` +
877
+ `priority: 0=low, 1=medium, 2=high, 3=critical`,
878
+ parameters: {
879
+ type: "object",
880
+ required: ["action"],
881
+ properties: {
882
+ action: { type: "string", enum: ["list", "create", "update", "delete"] },
883
+ id: { type: "string", description: "Event ID" },
884
+ title: { type: "string", description: "Event title" },
885
+ description: { type: "string" },
886
+ task_id: { type: "string", description: "Link to a task" },
887
+ agent_id: { type: "string", description: "Assigned agent" },
888
+ scheduled_date: { type: "string", description: "Date (YYYY-MM-DD)" },
889
+ scheduled_time: { type: "string", description: "Time (HH:MM)" },
890
+ start_date: { type: "string", description: "List filter: start (YYYY-MM-DD)" },
891
+ end_date: { type: "string", description: "List filter: end (YYYY-MM-DD)" },
892
+ duration_minutes: { type: "number", description: "Duration in minutes (default 30)" },
893
+ recurrence_type: { type: "string", enum: ["none", "hourly", "daily", "weekly", "monthly"] },
894
+ recurrence_interval: { type: "number", description: "Repeat every N periods" },
895
+ color: { type: "string", description: "Hex color" },
896
+ priority: { type: "number", description: "0-3" },
897
+ status: { type: "string", enum: ["scheduled", "completed", "cancelled"] },
898
+ },
899
+ },
900
+ async execute(_id: string, params: Record<string, unknown>) {
901
+ const action = params.action as string;
902
+ switch (action) {
903
+ case "list": {
904
+ let q = supabase.from("scheduler_events").select("*").eq("user_id", userId)
905
+ .order("scheduled_date", { ascending: true });
906
+ if (params.start_date) q = q.gte("scheduled_date", params.start_date as string);
907
+ if (params.end_date) q = q.lte("scheduled_date", params.end_date as string);
908
+ if (params.agent_id) q = q.eq("agent_id", params.agent_id as string);
909
+ const { data, error } = await q;
910
+ if (error) return err(error.message);
911
+ return ok({ events: data || [], count: (data || []).length });
912
+ }
913
+ case "create": {
914
+ if (!params.title || !params.scheduled_date) return err("Missing required: title, scheduled_date");
915
+ const evtId = crypto.randomUUID();
916
+ const priorityMap: Record<string, number> = { low: 0, medium: 1, high: 2, critical: 3 };
917
+ const pVal = typeof params.priority === "number" ? params.priority
918
+ : priorityMap[String(params.priority || "").toLowerCase()] ?? 0;
919
+ const insertData: Record<string, any> = {
920
+ id: evtId,
921
+ user_id: userId,
922
+ task_id: (params.task_id as string) || null,
923
+ agent_id: (params.agent_id as string) || null,
924
+ title: params.title,
925
+ description: (params.description as string) || null,
926
+ scheduled_date: params.scheduled_date,
927
+ scheduled_time: (params.scheduled_time as string) || null,
928
+ duration_minutes: (params.duration_minutes as number) || 30,
929
+ recurrence_type: (params.recurrence_type as string) || "none",
930
+ recurrence_interval: (params.recurrence_interval as number) || 1,
931
+ status: "scheduled",
932
+ run_count: 0,
933
+ color: (params.color as string) || null,
934
+ priority: pVal,
935
+ };
936
+ const { error } = await supabase.from("scheduler_events").insert(insertData);
937
+ if (error) return err(error.message);
938
+ return ok({ message: `Event "${params.title}" scheduled for ${params.scheduled_date}`, id: evtId });
939
+ }
940
+ case "update": {
941
+ if (!params.id) return err("Missing required: id");
942
+ const upd: Record<string, any> = { updated_at: new Date().toISOString() };
943
+ for (const f of ["title", "description", "scheduled_date", "scheduled_time", "duration_minutes",
944
+ "recurrence_type", "recurrence_interval", "status", "color", "priority", "agent_id"]) {
945
+ if ((params as any)[f] !== undefined) upd[f] = (params as any)[f];
946
+ }
947
+ const { error } = await supabase.from("scheduler_events").update(upd).eq("id", params.id);
948
+ if (error) return err(error.message);
949
+ return ok({ message: "Event updated", ok: true });
950
+ }
951
+ case "delete": {
952
+ if (!params.id) return err("Missing required: id");
953
+ const { error } = await supabase.from("scheduler_events").delete().eq("id", params.id);
954
+ if (error) return err(error.message);
955
+ return ok({ message: "Event deleted", ok: true });
956
+ }
957
+ default:
958
+ return err(`Unknown action "${action}".`);
959
+ }
960
+ },
961
+ });
962
+ }
963
+
964
+ // ═══════════════════════════════════════════════════════════════════════════════
965
+ // META-TOOL 5: OFIERE_KNOWLEDGE_OPS — Knowledge Base
966
+ // ═══════════════════════════════════════════════════════════════════════════════
967
+
968
+ function registerKnowledgeOps(
969
+ api: any,
970
+ supabase: SupabaseClient,
971
+ userId: string,
972
+ ): void {
973
+ api.registerTool({
974
+ name: "OFIERE_KNOWLEDGE_OPS",
975
+ label: "Ofiere Knowledge Operations",
976
+ description:
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` +
979
+ `Actions:\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` +
983
+ `- "update": Edit a document. Required: id. Optional: file_name, content, text, source, source_type, author\n` +
984
+ `- "delete": Remove a document. Required: id`,
985
+ parameters: {
986
+ type: "object",
987
+ required: ["action"],
988
+ properties: {
989
+ action: { type: "string", enum: ["search", "list", "create", "update", "delete"] },
990
+ id: { type: "string", description: "Document ID" },
991
+ query: { type: "string", description: "Search query" },
992
+ file_name: { type: "string", description: "Document name" },
993
+ content: { type: "string", description: "Raw content" },
994
+ text: { type: "string", description: "Processed text" },
995
+ source: { type: "string", description: "Source URL or reference" },
996
+ source_type: { type: "string", description: "e.g. web, pdf, manual" },
997
+ author: { type: "string", description: "Author name" },
998
+ credibility_tier: { type: "string", description: "Credibility level" },
999
+ page: { type: "number", description: "Page number (default 1)" },
1000
+ page_size: { type: "number", description: "Results per page (default 20)" },
1001
+ search: { type: "string", description: "Filter for list action" },
1002
+ limit: { type: "number", description: "Max results for search" },
1003
+ },
1004
+ },
1005
+ async execute(_id: string, params: Record<string, unknown>) {
1006
+ const action = params.action as string;
1007
+ switch (action) {
1008
+ case "search": {
1009
+ if (!params.query) return err("Missing required: query");
1010
+ const lim = (params.limit as number) || 20;
1011
+ const searchTerm = `%${params.query}%`;
1012
+ const { data, error } = await supabase
1013
+ .from("knowledge_documents")
1014
+ .select("id, file_name, file_type, content, source, source_type, author, credibility_tier")
1015
+ .or(`file_name.ilike.${searchTerm},content.ilike.${searchTerm},author.ilike.${searchTerm},source.ilike.${searchTerm}`)
1016
+ .order("created_at", { ascending: false })
1017
+ .limit(lim);
1018
+ if (error) return err(error.message);
1019
+ return ok({ documents: data || [], count: (data || []).length, query: params.query });
1020
+ }
1021
+ case "list": {
1022
+ const page = Math.max(1, (params.page as number) || 1);
1023
+ const pageSize = Math.min(100, Math.max(1, (params.page_size as number) || 20));
1024
+ const from = (page - 1) * pageSize;
1025
+ const to = from + pageSize - 1;
1026
+ let q = supabase.from("knowledge_documents")
1027
+ .select("id, file_name, file_type, content, text, source, source_type, author, credibility_tier, created_at", { count: "exact" })
1028
+ .order("created_at", { ascending: false })
1029
+ .range(from, to);
1030
+ if (params.search) {
1031
+ const s = `%${params.search}%`;
1032
+ q = q.or(`file_name.ilike.${s},content.ilike.${s},author.ilike.${s}`);
1033
+ }
1034
+ const { data, count, error } = await q;
1035
+ if (error) return err(error.message);
1036
+ return ok({ documents: data || [], total: count || 0, page, page_size: pageSize });
1037
+ }
1038
+ case "create": {
1039
+ if (!params.file_name) return err("Missing required: file_name");
1040
+ const docId = crypto.randomUUID();
1041
+ const { error } = await supabase.from("knowledge_documents").insert({
1042
+ id: docId,
1043
+ user_id: userId,
1044
+ file_name: params.file_name,
1045
+ file_type: (params.file_type as string) || null,
1046
+ content: (params.content as string) || null,
1047
+ text: (params.text as string) || null,
1048
+ source: (params.source as string) || null,
1049
+ source_type: (params.source_type as string) || null,
1050
+ author: (params.author as string) || null,
1051
+ credibility_tier: (params.credibility_tier as string) || null,
1052
+ size_bytes: params.content ? new TextEncoder().encode(params.content as string).length : 0,
1053
+ indexed: false,
1054
+ });
1055
+ if (error) return err(error.message);
1056
+ return ok({ message: `Knowledge doc "${params.file_name}" created`, id: docId });
1057
+ }
1058
+ case "update": {
1059
+ if (!params.id) return err("Missing required: id");
1060
+ const allowed = ["file_name", "file_type", "content", "text", "source", "source_type", "author", "credibility_tier"];
1061
+ const upd: Record<string, any> = {};
1062
+ for (const k of allowed) if ((params as any)[k] !== undefined) upd[k] = (params as any)[k];
1063
+ if (Object.keys(upd).length === 0) return err("No valid fields to update");
1064
+ const { error } = await supabase.from("knowledge_documents").update(upd).eq("id", params.id);
1065
+ if (error) return err(error.message);
1066
+ return ok({ message: "Document updated", ok: true });
1067
+ }
1068
+ case "delete": {
1069
+ if (!params.id) return err("Missing required: id");
1070
+ const { error } = await supabase.from("knowledge_documents").delete().eq("id", params.id);
1071
+ if (error) return err(error.message);
1072
+ return ok({ message: "Document deleted", ok: true });
1073
+ }
1074
+ default:
1075
+ return err(`Unknown action "${action}".`);
1076
+ }
1077
+ },
1078
+ });
1079
+ }
1080
+
1081
+ // ═══════════════════════════════════════════════════════════════════════════════
1082
+ // META-TOOL 6: OFIERE_WORKFLOW_OPS — Workflow Management & Execution
1083
+ // ═══════════════════════════════════════════════════════════════════════════════
1084
+
1085
+ function registerWorkflowOps(
1086
+ api: any,
1087
+ supabase: SupabaseClient,
1088
+ userId: string,
1089
+ ): void {
1090
+ api.registerTool({
1091
+ name: "OFIERE_WORKFLOW_OPS",
1092
+ label: "Ofiere Workflow Operations",
1093
+ description:
1094
+ `Manage, build, and trigger automated workflows in the Ofiere dashboard.\n\n` +
1095
+ `Actions:\n` +
1096
+ `- "list": List all workflows. Optional: status\n` +
1097
+ `- "get": Get workflow details. Required: id\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` +
1101
+ `- "list_runs": List recent runs. Required: workflow_id. Optional: limit\n` +
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.`,
1128
+ parameters: {
1129
+ type: "object",
1130
+ required: ["action"],
1131
+ properties: {
1132
+ action: { type: "string", enum: ["list", "get", "create", "update", "delete", "list_runs", "trigger"] },
1133
+ id: { type: "string", description: "Workflow ID" },
1134
+ workflow_id: { type: "string", description: "Workflow ID for runs/trigger" },
1135
+ name: { type: "string", description: "Workflow name" },
1136
+ description: { type: "string" },
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" },
1163
+ schedule: { type: "string", description: "Cron expression or schedule" },
1164
+ status: { type: "string", enum: ["draft", "active", "paused", "archived"] },
1165
+ limit: { type: "number", description: "Max results" },
1166
+ },
1167
+ },
1168
+ async execute(_id: string, params: Record<string, unknown>) {
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
+
1207
+ switch (action) {
1208
+ case "list": {
1209
+ let q = supabase.from("workflows").select("*").eq("user_id", userId).order("updated_at", { ascending: false });
1210
+ if (params.status) q = q.eq("status", params.status as string);
1211
+ const { data, error } = await q;
1212
+ if (error) return err(error.message);
1213
+ return ok({ workflows: data || [], count: (data || []).length });
1214
+ }
1215
+ case "get": {
1216
+ const wfId = (params.id || params.workflow_id) as string;
1217
+ if (!wfId) return err("Missing required: id");
1218
+ const { data, error } = await supabase.from("workflows").select("*").eq("id", wfId).eq("user_id", userId).single();
1219
+ if (error) return err(error.message);
1220
+ return ok({ workflow: data });
1221
+ }
1222
+ case "create": {
1223
+ if (!params.name) return err("Missing required: name");
1224
+ const wfId = crypto.randomUUID();
1225
+ const stepsWithIds = ((params.steps as any[]) || []).map((s: any, i: number) => ({
1226
+ ...s, id: s.id || `step-${i}`,
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
+
1276
+ const { data, error } = await supabase.from("workflows").insert({
1277
+ id: wfId, user_id: userId,
1278
+ name: params.name,
1279
+ description: (params.description as string) || null,
1280
+ steps: stepsWithIds,
1281
+ schedule: (params.schedule as string) || null,
1282
+ status: (params.status as string) || "draft",
1283
+ nodes: finalNodes,
1284
+ edges: finalEdges,
1285
+ definition_version: 2,
1286
+ }).select().single();
1287
+ if (error) return err(error.message);
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 });
1323
+ }
1324
+ case "list_runs": {
1325
+ const wfId = (params.workflow_id || params.id) as string;
1326
+ if (!wfId) return err("Missing required: workflow_id");
1327
+ const { data, error } = await supabase.from("workflow_runs").select("*")
1328
+ .eq("workflow_id", wfId)
1329
+ .order("created_at", { ascending: false })
1330
+ .limit((params.limit as number) || 20);
1331
+ if (error) return err(error.message);
1332
+ return ok({ runs: data || [], count: (data || []).length });
1333
+ }
1334
+ case "trigger": {
1335
+ const wfId = (params.workflow_id || params.id) as string;
1336
+ if (!wfId) return err("Missing required: workflow_id");
1337
+ const runId = crypto.randomUUID();
1338
+ const { error } = await supabase.from("workflow_runs").insert({
1339
+ id: runId,
1340
+ workflow_id: wfId,
1341
+ status: "running",
1342
+ started_at: new Date().toISOString(),
1343
+ trigger_type: "agent",
1344
+ });
1345
+ if (error) return err(error.message);
1346
+ return ok({ message: `Workflow run triggered`, run_id: runId, workflow_id: wfId });
1347
+ }
1348
+ default:
1349
+ return err(`Unknown action "${action}".`);
1350
+ }
1351
+ },
1352
+ });
1353
+ }
1354
+
1355
+ // ═══════════════════════════════════════════════════════════════════════════════
1356
+ // META-TOOL 7: OFIERE_NOTIFY_OPS — Notifications
1357
+ // ═══════════════════════════════════════════════════════════════════════════════
1358
+
1359
+ function registerNotifyOps(
1360
+ api: any,
1361
+ supabase: SupabaseClient,
1362
+ userId: string,
1363
+ ): void {
1364
+ api.registerTool({
1365
+ name: "OFIERE_NOTIFY_OPS",
1366
+ label: "Ofiere Notification Operations",
1367
+ description:
1368
+ `Read and manage notifications.\n\n` +
1369
+ `Actions:\n` +
1370
+ `- "list": List notifications. Optional: unread_only (true/false), limit\n` +
1371
+ `- "mark_read": Mark one as read. Required: id\n` +
1372
+ `- "mark_all_read": Mark all as read\n` +
1373
+ `- "delete": Delete a notification. Required: id`,
1374
+ parameters: {
1375
+ type: "object",
1376
+ required: ["action"],
1377
+ properties: {
1378
+ action: { type: "string", enum: ["list", "mark_read", "mark_all_read", "delete"] },
1379
+ id: { type: "string", description: "Notification ID" },
1380
+ unread_only: { type: "boolean", description: "Only show unread" },
1381
+ limit: { type: "number", description: "Max results (default 50)" },
1382
+ },
1383
+ },
1384
+ async execute(_id: string, params: Record<string, unknown>) {
1385
+ const action = params.action as string;
1386
+ switch (action) {
1387
+ case "list": {
1388
+ let q = supabase.from("notifications").select("*")
1389
+ .order("created_at", { ascending: false })
1390
+ .limit((params.limit as number) || 50);
1391
+ if (params.unread_only === true) q = q.eq("read", false);
1392
+ const { data, error } = await q;
1393
+ if (error) return err(error.message);
1394
+ const unread = (data || []).filter((n: any) => !n.read).length;
1395
+ return ok({ notifications: data || [], count: (data || []).length, unread_count: unread });
1396
+ }
1397
+ case "mark_read": {
1398
+ if (!params.id) return err("Missing required: id");
1399
+ const { error } = await supabase.from("notifications").update({ read: true }).eq("id", params.id);
1400
+ if (error) return err(error.message);
1401
+ return ok({ message: "Notification marked as read", ok: true });
1402
+ }
1403
+ case "mark_all_read": {
1404
+ const { error } = await supabase.from("notifications").update({ read: true }).eq("read", false);
1405
+ if (error) return err(error.message);
1406
+ return ok({ message: "All notifications marked as read", ok: true });
1407
+ }
1408
+ case "delete": {
1409
+ if (!params.id) return err("Missing required: id");
1410
+ const { error } = await supabase.from("notifications").delete().eq("id", params.id);
1411
+ if (error) return err(error.message);
1412
+ return ok({ message: "Notification deleted", ok: true });
1413
+ }
1414
+ default:
1415
+ return err(`Unknown action "${action}".`);
1416
+ }
1417
+ },
1418
+ });
1419
+ }
1420
+
1421
+ // ═══════════════════════════════════════════════════════════════════════════════
1422
+ // META-TOOL 8: OFIERE_MEMORY_OPS — Conversations & Knowledge Fragments
1423
+ // ═══════════════════════════════════════════════════════════════════════════════
1424
+
1425
+ function registerMemoryOps(
1426
+ api: any,
1427
+ supabase: SupabaseClient,
1428
+ userId: string,
1429
+ ): void {
1430
+ api.registerTool({
1431
+ name: "OFIERE_MEMORY_OPS",
1432
+ label: "Ofiere Memory Operations",
1433
+ description:
1434
+ `Access conversation history and knowledge memory.\n\n` +
1435
+ `Actions:\n` +
1436
+ `- "list_conversations": List recent conversations. Optional: agent_id, limit\n` +
1437
+ `- "get_messages": Get messages from a conversation. Required: conversation_id. Optional: limit\n` +
1438
+ `- "search_messages": Search all messages. Required: query. Optional: agent_id, limit\n` +
1439
+ `- "add_knowledge": Store a knowledge fragment. Required: agent_id, content, source. Optional: tags, importance\n` +
1440
+ `- "search_knowledge": Search knowledge. Required: agent_id, query. Optional: limit`,
1441
+ parameters: {
1442
+ type: "object",
1443
+ required: ["action"],
1444
+ properties: {
1445
+ action: { type: "string", enum: ["list_conversations", "get_messages", "search_messages", "add_knowledge", "search_knowledge"] },
1446
+ conversation_id: { type: "string" },
1447
+ agent_id: { type: "string" },
1448
+ query: { type: "string", description: "Search query" },
1449
+ content: { type: "string", description: "Knowledge content to store" },
1450
+ source: { type: "string", description: "Source of knowledge" },
1451
+ tags: { type: "array", items: { type: "string" } },
1452
+ importance: { type: "number", description: "1-10 importance scale" },
1453
+ limit: { type: "number", description: "Max results" },
1454
+ },
1455
+ },
1456
+ async execute(_id: string, params: Record<string, unknown>) {
1457
+ const action = params.action as string;
1458
+ switch (action) {
1459
+ case "list_conversations": {
1460
+ let q = supabase.from("conversations")
1461
+ .select("id, agent_id, title, created_at, updated_at")
1462
+ .eq("user_id", userId)
1463
+ .order("updated_at", { ascending: false })
1464
+ .limit((params.limit as number) || 20);
1465
+ if (params.agent_id) q = q.eq("agent_id", params.agent_id as string);
1466
+ const { data, error } = await q;
1467
+ if (error) return err(error.message);
1468
+ return ok({ conversations: data || [], count: (data || []).length });
1469
+ }
1470
+ case "get_messages": {
1471
+ if (!params.conversation_id) return err("Missing required: conversation_id");
1472
+ const { data, error } = await supabase.from("conversation_messages")
1473
+ .select("id, role, content, created_at")
1474
+ .eq("conversation_id", params.conversation_id as string)
1475
+ .order("created_at", { ascending: true })
1476
+ .limit((params.limit as number) || 100);
1477
+ if (error) return err(error.message);
1478
+ return ok({ messages: data || [], count: (data || []).length });
1479
+ }
1480
+ case "search_messages": {
1481
+ if (!params.query) return err("Missing required: query");
1482
+ const searchTerm = `%${params.query}%`;
1483
+ let q = supabase.from("conversation_messages")
1484
+ .select("id, conversation_id, role, content, created_at")
1485
+ .ilike("content", searchTerm)
1486
+ .order("created_at", { ascending: false })
1487
+ .limit((params.limit as number) || 20);
1488
+ const { data, error } = await q;
1489
+ if (error) return err(error.message);
1490
+ return ok({ messages: data || [], count: (data || []).length, query: params.query });
1491
+ }
1492
+ case "add_knowledge": {
1493
+ if (!params.agent_id || !params.content || !params.source) return err("Missing required: agent_id, content, source");
1494
+ const fragId = crypto.randomUUID();
1495
+ const { error } = await supabase.from("knowledge_fragments").insert({
1496
+ id: fragId,
1497
+ agent_id: params.agent_id,
1498
+ content: params.content,
1499
+ source: params.source,
1500
+ tags: (params.tags as string[]) || [],
1501
+ importance: (params.importance as number) || 5,
1502
+ });
1503
+ if (error) return err(error.message);
1504
+ return ok({ message: "Knowledge stored", id: fragId });
1505
+ }
1506
+ case "search_knowledge": {
1507
+ if (!params.agent_id || !params.query) return err("Missing required: agent_id, query");
1508
+ const searchTerm = `%${params.query}%`;
1509
+ const { data, error } = await supabase.from("knowledge_fragments")
1510
+ .select("id, content, source, tags, importance, created_at")
1511
+ .eq("agent_id", params.agent_id as string)
1512
+ .ilike("content", searchTerm)
1513
+ .order("importance", { ascending: false })
1514
+ .limit((params.limit as number) || 20);
1515
+ if (error) return err(error.message);
1516
+ return ok({ fragments: data || [], count: (data || []).length });
1517
+ }
1518
+ default:
1519
+ return err(`Unknown action "${action}".`);
1520
+ }
1521
+ },
1522
+ });
1523
+ }
1524
+
1525
+ // ═══════════════════════════════════════════════════════════════════════════════
1526
+ // META-TOOL 9: OFIERE_PROMPT_OPS — System Prompt Chunk Management
1527
+ // ═══════════════════════════════════════════════════════════════════════════════
1528
+
1529
+ function registerPromptOps(
1530
+ api: any,
1531
+ supabase: SupabaseClient,
1532
+ userId: string,
1533
+ ): void {
1534
+ api.registerTool({
1535
+ name: "OFIERE_PROMPT_OPS",
1536
+ label: "Ofiere Prompt Operations",
1537
+ description:
1538
+ `Manage system prompt instruction chunks. These are the building blocks of agent behavior.\n\n` +
1539
+ `Actions:\n` +
1540
+ `- "list": List all prompt chunks\n` +
1541
+ `- "get": Get a specific chunk. Required: id\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` +
1544
+ `- "delete": Delete a chunk. Required: id`,
1545
+ parameters: {
1546
+ type: "object",
1547
+ required: ["action"],
1548
+ properties: {
1549
+ action: { type: "string", enum: ["list", "get", "create", "update", "delete"] },
1550
+ id: { type: "string", description: "Chunk ID" },
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)" },
1556
+ },
1557
+ },
1558
+ async execute(_id: string, params: Record<string, unknown>) {
1559
+ const action = params.action as string;
1560
+ switch (action) {
1561
+ case "list": {
1562
+ const { data, error } = await supabase.from("prompt_chunks").select("*").eq("user_id", userId).order("order", { ascending: true });
1563
+ if (error) return err(error.message);
1564
+ return ok({ chunks: data || [], count: (data || []).length });
1565
+ }
1566
+ case "get": {
1567
+ if (!params.id) return err("Missing required: id");
1568
+ const { data, error } = await supabase.from("prompt_chunks").select("*").eq("id", params.id).eq("user_id", userId).single();
1569
+ if (error) return err(error.message);
1570
+ return ok({ chunk: data });
1571
+ }
1572
+ case "create": {
1573
+ if (!params.name || !params.content) return err("Missing required: name, content");
1574
+ const chunkName = String(params.name).slice(0, 30);
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
+
1586
+ const { data, error } = await supabase.from("prompt_chunks").insert({
1587
+ id: chunkId,
1588
+ user_id: userId,
1589
+ name: chunkName,
1590
+ content: params.content,
1591
+ color: (params.color as string) || "#6B7280",
1592
+ category: (params.category as string) || "Uncategorized",
1593
+ order: (params.order as number) ?? maxOrder + 1,
1594
+ }).select().single();
1595
+ if (error) return err(error.message);
1596
+ api.logger?.info?.(`[ofiere] Prompt chunk created: "${chunkName}" by agent`);
1597
+ return ok({ message: `Prompt chunk "${chunkName}" created`, chunk: data });
1598
+ }
1599
+ case "update": {
1600
+ if (!params.id) return err("Missing required: id");
1601
+ const upd: Record<string, any> = { updated_at: new Date().toISOString() };
1602
+ for (const f of ["name", "content", "color", "category", "order"]) {
1603
+ if ((params as any)[f] !== undefined) upd[f] = (params as any)[f];
1604
+ }
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();
1607
+ if (error) return err(error.message);
1608
+ api.logger?.info?.(`[ofiere] Prompt chunk ${params.id} updated by agent`);
1609
+ return ok({ message: "Prompt chunk updated", chunk: data });
1610
+ }
1611
+ case "delete": {
1612
+ if (!params.id) return err("Missing required: id");
1613
+ const { error } = await supabase.from("prompt_chunks").delete().eq("id", params.id).eq("user_id", userId);
1614
+ if (error) return err(error.message);
1615
+ api.logger?.info?.(`[ofiere] Prompt chunk ${params.id} deleted by agent`);
1616
+ return ok({ message: "Prompt chunk deleted", ok: true });
1617
+ }
1618
+ default:
1619
+ return err(`Unknown action "${action}".`);
1620
+ }
1621
+ },
1622
+ });
1623
+ }
1624
+
1625
+ // ═══════════════════════════════════════════════════════════════════════════════
1626
+ // Public: Register All Meta-Tools
1627
+ // ═══════════════════════════════════════════════════════════════════════════════
499
1628
  // This is the single entry point called by index.ts.
500
1629
  // Returns the number of tools registered for dynamic prompt generation.
501
- //
502
- // To expand: add new register*Ops() calls here and increment the count.
503
1630
 
504
1631
  export function registerTools(
505
1632
  api: any, // OpenClawPluginApi — typed as any to avoid import-path issues at install time
@@ -512,11 +1639,18 @@ export function registerTools(
512
1639
  const resolveAgent = createAgentResolver(api, supabase, userId, fallbackAgentId);
513
1640
 
514
1641
  // ── Register each domain meta-tool ──
515
- registerTaskOps(api, supabase, userId, resolveAgent);
516
- registerAgentOps(api, supabase, userId, fallbackAgentId);
1642
+ registerTaskOps(api, supabase, userId, resolveAgent); // 1
1643
+ registerAgentOps(api, supabase, userId, fallbackAgentId); // 2
1644
+ registerProjectOps(api, supabase, userId); // 3
1645
+ registerScheduleOps(api, supabase, userId); // 4
1646
+ registerKnowledgeOps(api, supabase, userId); // 5
1647
+ registerWorkflowOps(api, supabase, userId); // 6
1648
+ registerNotifyOps(api, supabase, userId); // 7
1649
+ registerMemoryOps(api, supabase, userId); // 8
1650
+ registerPromptOps(api, supabase, userId); // 9
517
1651
 
518
1652
  // ── Count and log ──
519
- const toolCount = 2; // Update this when adding new meta-tools
1653
+ const toolCount = 9;
520
1654
  const callerName = getCallingAgentName(api);
521
1655
  const agentLabel = fallbackAgentId || callerName || "auto-detect";
522
1656
  api.logger.info(`[ofiere] ${toolCount} meta-tools registered (agent: ${agentLabel})`);