ofiere-openclaw-plugin 2.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/package.json +2 -2
  2. package/src/prompt.ts +50 -19
  3. package/src/tools.ts +933 -23
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "ofiere-openclaw-plugin",
3
- "version": "2.0.0",
3
+ "version": "3.0.0",
4
4
  "type": "module",
5
- "description": "OpenClaw plugin for Ofiere PM — scalable meta-tool architecture for task, agent, and project management",
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"],
7
7
  "homepage": "https://github.com/gilanggemar/Ofiere",
8
8
  "repository": {
package/src/prompt.ts CHANGED
@@ -10,30 +10,58 @@
10
10
 
11
11
  const TOOL_DOCS: Record<string, string> = {
12
12
  OFIERE_TASK_OPS: `- **OFIERE_TASK_OPS** — Manage tasks (action: "list", "create", "update", "delete")
13
- - list: Filter by status, agent_id, space_id, folder_id, limit
14
- - create: Requires title + agent_id. Pass your name to self-assign, 'none' for unassigned
15
- - update: Requires task_id. Change title, status, priority, progress, etc.
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
15
+ - For COMPLEX tasks: include execution_plan (step-by-step), goals, constraints, and system_prompt
16
+ - For SIMPLE tasks: just title and optionally description
17
+ - update: Requires task_id. All create fields + progress
16
18
  - delete: Requires task_id. Removes task and subtasks`,
17
19
 
18
20
  OFIERE_AGENT_OPS: `- **OFIERE_AGENT_OPS** — Query agents (action: "list")
19
21
  - list: See all agents with IDs, names, roles for task assignment`,
20
22
 
21
- // ── Future meta-tools uncomment when registered ──
22
- // OFIERE_PROJECT_OPS: `- **OFIERE_PROJECT_OPS** Manage projects (action: "list", "create", "update", "delete")
23
- // - list: List spaces, folders, and projects
24
- // - create: Create a new space or folder
25
- // - update: Rename, move, or archive
26
- // - delete: Remove space/folder and reassign tasks`,
27
- //
28
- // OFIERE_SCHEDULE_OPS: `- **OFIERE_SCHEDULE_OPS** — Calendar & timeline (action: "list", "schedule", "reschedule")
29
- // - list: View scheduled events for a date range
30
- // - schedule: Assign a task to a time slot
31
- // - reschedule: Move an event to a new time`,
32
- //
33
- // OFIERE_KNOWLEDGE_OPS: `- **OFIERE_KNOWLEDGE_OPS** — Knowledge base (action: "search", "create", "update")
34
- // - search: Find knowledge entries by query
35
- // - create: Add a new knowledge entry
36
- // - update: Edit an existing entry`,
23
+ OFIERE_PROJECT_OPS: `- **OFIERE_PROJECT_OPS**Manage PM hierarchy (action: "list_spaces", "create_space", "update_space", "delete_space", "list_folders", "create_folder", "update_folder", "delete_folder", "list_dependencies", "add_dependency", "remove_dependency")
24
+ - Spaces: Top-level containers. CRUD with name, icon, icon_color
25
+ - Folders: Live inside spaces. Can nest via parent_folder_id. Types: folder, project
26
+ - Dependencies: Link tasks with predecessor/successor relationships
27
+ - Types: finish_to_start (default), start_to_start, finish_to_finish, start_to_finish
28
+ - lag_days: optional delay between linked tasks`,
29
+
30
+ OFIERE_SCHEDULE_OPS: `- **OFIERE_SCHEDULE_OPS** — Calendar events (action: "list", "create", "update", "delete")
31
+ - list: Filter by start_date, end_date, agent_id
32
+ - create: Requires title + scheduled_date (YYYY-MM-DD). Optional: scheduled_time, duration_minutes, task_id, agent_id, recurrence_type, color
33
+ - Recurrence: none, hourly, daily, weekly, monthly with interval`,
34
+
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
39
+ - update/delete: By document ID`,
40
+
41
+ OFIERE_WORKFLOW_OPS: `- **OFIERE_WORKFLOW_OPS** — Automated workflows (action: "list", "get", "create", "list_runs", "trigger")
42
+ - list: All workflows, filter by status (draft, active, paused, archived)
43
+ - get: Full workflow details by ID
44
+ - create: New workflow with name, steps, schedule
45
+ - list_runs: Recent execution history for a workflow
46
+ - trigger: Start a workflow run (creates a run record)`,
47
+
48
+ OFIERE_NOTIFY_OPS: `- **OFIERE_NOTIFY_OPS** — Notifications (action: "list", "mark_read", "mark_all_read", "delete")
49
+ - list: Recent notifications. unread_only=true for unread only
50
+ - mark_read: Mark one notification read by ID
51
+ - mark_all_read: Mark all as read`,
52
+
53
+ OFIERE_MEMORY_OPS: `- **OFIERE_MEMORY_OPS** — Conversation history & knowledge memory (action: "list_conversations", "get_messages", "search_messages", "add_knowledge", "search_knowledge")
54
+ - list_conversations: Recent chats, filter by agent_id
55
+ - get_messages: Full message history for a conversation
56
+ - search_messages: Search across all messages by keyword
57
+ - add_knowledge: Store a knowledge fragment (requires agent_id, content, source)
58
+ - search_knowledge: Search stored knowledge for an agent`,
59
+
60
+ 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
64
+ - All modifications are logged for audit`,
37
65
  };
38
66
 
39
67
  export function getSystemPrompt(state: {
@@ -73,6 +101,9 @@ ${toolDocs}
73
101
  - Priority levels: 0=LOW, 1=MEDIUM, 2=HIGH, 3=CRITICAL.
74
102
  - Changes appear in the Ofiere dashboard immediately via real-time sync.
75
103
  - Do NOT fabricate task IDs — use OFIERE_TASK_OPS action:"list" to look up real IDs.
104
+ - For complex tasks, ALWAYS include execution_plan, goals, and constraints. For simple tasks, just title is enough.
105
+ - When creating dependencies, use OFIERE_PROJECT_OPS to link predecessor/successor tasks.
106
+ - Prompt chunk modifications (OFIERE_PROMPT_OPS) are powerful — use thoughtfully as they change agent behavior.
76
107
  </ofiere-pm>`;
77
108
  }
78
109
 
package/src/tools.ts CHANGED
@@ -172,7 +172,9 @@ function createAgentResolver(
172
172
  };
173
173
  }
174
174
 
175
- // ─── META-TOOL: OFIERE_TASK_OPS ──────────────────────────────────────────────
175
+ // ═══════════════════════════════════════════════════════════════════════════════
176
+ // META-TOOL 1: OFIERE_TASK_OPS — Task Management
177
+ // ═══════════════════════════════════════════════════════════════════════════════
176
178
 
177
179
  function registerTaskOps(
178
180
  api: any,
@@ -186,13 +188,14 @@ function registerTaskOps(
186
188
  description:
187
189
  `Manage tasks in the Ofiere PM dashboard. All task operations go through this tool.\n\n` +
188
190
  `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`,
191
+ `- "list": List/filter tasks. Optional: status, agent_id, space_id, folder_id, limit\n` +
192
+ `- "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` +
193
+ `- "update": Update a task. Required: task_id. Optional: all create fields + progress\n` +
194
+ `- "delete": Delete task + subtasks. Required: task_id\n\n` +
195
+ `For complex tasks, fill in execution_plan (step-by-step plan), goals, constraints, and system_prompt to help the executing agent.\n` +
196
+ `For simple tasks, just provide title and optionally description.\n` +
197
+ `agent_id: Pass your name to self-assign, another agent's name, or 'none'.\n` +
198
+ `Status: PENDING, IN_PROGRESS, DONE, FAILED | Priority: 0=LOW, 1=MEDIUM, 2=HIGH, 3=CRITICAL`,
196
199
  parameters: {
197
200
  type: "object",
198
201
  required: ["action"],
@@ -202,14 +205,13 @@ function registerTaskOps(
202
205
  description: "The operation to perform",
203
206
  enum: ["list", "create", "update", "delete"],
204
207
  },
205
- // ── Shared / contextual params ──
206
208
  task_id: { type: "string", description: "Task ID (required for update, delete)" },
207
209
  title: { type: "string", description: "Task title (required for create)" },
208
210
  description: { type: "string", description: "Task description" },
211
+ instructions: { type: "string", description: "Detailed instructions for the agent executing this task" },
209
212
  agent_id: {
210
213
  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.",
214
+ description: "Agent name or ID. Your name to self-assign, 'none' for unassigned.",
213
215
  },
214
216
  status: {
215
217
  type: "string",
@@ -227,6 +229,42 @@ function registerTaskOps(
227
229
  items: { type: "string" },
228
230
  description: "Tags for the task",
229
231
  },
232
+ execution_plan: {
233
+ type: "array",
234
+ items: {
235
+ type: "object",
236
+ properties: {
237
+ text: { type: "string", description: "Step description" },
238
+ },
239
+ required: ["text"],
240
+ },
241
+ description: "Ordered execution steps for complex tasks. Each step: { text: '...' }",
242
+ },
243
+ goals: {
244
+ type: "array",
245
+ items: {
246
+ type: "object",
247
+ properties: {
248
+ type: { type: "string", enum: ["budget", "stack", "legal", "deadline", "custom"], description: "Goal category" },
249
+ label: { type: "string", description: "Goal description" },
250
+ },
251
+ required: ["label"],
252
+ },
253
+ description: "Task goals. Each: { type?: 'budget'|'stack'|'legal'|'deadline'|'custom', label: '...' }",
254
+ },
255
+ constraints: {
256
+ type: "array",
257
+ items: {
258
+ type: "object",
259
+ properties: {
260
+ type: { type: "string", enum: ["budget", "stack", "legal", "deadline", "custom"], description: "Constraint category" },
261
+ label: { type: "string", description: "Constraint description" },
262
+ },
263
+ required: ["label"],
264
+ },
265
+ description: "Task constraints. Each: { type?: 'budget'|'stack'|'legal'|'deadline'|'custom', label: '...' }",
266
+ },
267
+ system_prompt: { type: "string", description: "Custom system prompt injection for the executing agent" },
230
268
  limit: { type: "number", description: "Max results for list (default 50)" },
231
269
  },
232
270
  },
@@ -263,7 +301,7 @@ async function handleListTasks(
263
301
  .from("tasks")
264
302
  .select(
265
303
  "id, title, description, status, priority, agent_id, space_id, folder_id, " +
266
- "start_date, due_date, progress, created_at, updated_at",
304
+ "start_date, due_date, progress, tags, custom_fields, created_at, updated_at",
267
305
  )
268
306
  .eq("user_id", userId)
269
307
  .order("updated_at", { ascending: false });
@@ -276,7 +314,21 @@ async function handleListTasks(
276
314
 
277
315
  const { data, error } = await query;
278
316
  if (error) return err(error.message);
279
- return ok({ tasks: data || [], count: (data || []).length });
317
+
318
+ // Unpack custom_fields for readability
319
+ const tasks = (data || []).map((t: any) => {
320
+ const cf = t.custom_fields || {};
321
+ return {
322
+ ...t,
323
+ execution_plan: cf.execution_plan || undefined,
324
+ goals: cf.goals || undefined,
325
+ constraints: cf.constraints || undefined,
326
+ system_prompt: cf.system_prompt || undefined,
327
+ instructions: cf.instructions || t.description || undefined,
328
+ };
329
+ });
330
+
331
+ return ok({ tasks, count: tasks.length });
280
332
  } catch (e) {
281
333
  return err(e instanceof Error ? e.message : String(e));
282
334
  }
@@ -302,11 +354,41 @@ async function handleCreateTask(
302
354
 
303
355
  const assignee = isUnassigned ? null : await resolveAgent(rawAgentId);
304
356
 
357
+ // Build custom_fields from task-ops extended fields
358
+ const cf: Record<string, unknown> = {};
359
+
360
+ if (params.execution_plan && Array.isArray(params.execution_plan) && (params.execution_plan as any[]).length > 0) {
361
+ cf.execution_plan = (params.execution_plan as any[]).map((step: any, i: number) => ({
362
+ id: `step-${Date.now()}-${i}`,
363
+ text: typeof step === "string" ? step : step.text || String(step),
364
+ order: i,
365
+ }));
366
+ }
367
+
368
+ if (params.goals && Array.isArray(params.goals) && (params.goals as any[]).length > 0) {
369
+ cf.goals = (params.goals as any[]).map((g: any, i: number) => ({
370
+ id: `goal-${Date.now()}-${i}`,
371
+ type: g.type || "custom",
372
+ label: typeof g === "string" ? g : g.label || String(g),
373
+ }));
374
+ }
375
+
376
+ if (params.constraints && Array.isArray(params.constraints) && (params.constraints as any[]).length > 0) {
377
+ cf.constraints = (params.constraints as any[]).map((c: any, i: number) => ({
378
+ id: `cstr-${Date.now()}-${i}`,
379
+ type: c.type || "custom",
380
+ label: typeof c === "string" ? c : c.label || String(c),
381
+ }));
382
+ }
383
+
384
+ if (params.system_prompt) cf.system_prompt = params.system_prompt;
385
+ if (params.instructions) cf.instructions = params.instructions;
386
+
305
387
  const insertData: Record<string, unknown> = {
306
388
  id,
307
389
  user_id: userId,
308
390
  title: params.title,
309
- description: (params.description as string) || null,
391
+ description: (params.description as string) || (params.instructions as string) || null,
310
392
  agent_id: assignee,
311
393
  assignee_type: "agent",
312
394
  status: (params.status as string) || "PENDING",
@@ -318,7 +400,7 @@ async function handleCreateTask(
318
400
  tags: (params.tags as string[]) || [],
319
401
  progress: 0,
320
402
  sort_order: 0,
321
- custom_fields: {},
403
+ custom_fields: Object.keys(cf).length > 0 ? cf : {},
322
404
  created_at: now,
323
405
  updated_at: now,
324
406
  };
@@ -339,9 +421,16 @@ async function handleCreateTask(
339
421
  return err(error.message);
340
422
  }
341
423
 
424
+ const extras = [];
425
+ if (cf.execution_plan) extras.push(`${(cf.execution_plan as any[]).length} execution steps`);
426
+ if (cf.goals) extras.push(`${(cf.goals as any[]).length} goals`);
427
+ if (cf.constraints) extras.push(`${(cf.constraints as any[]).length} constraints`);
428
+ if (cf.system_prompt) extras.push("custom system prompt");
429
+ const extrasStr = extras.length > 0 ? ` with ${extras.join(", ")}` : "";
430
+
342
431
  return ok({
343
432
  id,
344
- message: `Task "${params.title}" created and assigned to ${assignee || "no one"}`,
433
+ message: `Task "${params.title}" created and assigned to ${assignee || "no one"}${extrasStr}`,
345
434
  task: insertData,
346
435
  });
347
436
  } catch (e) {
@@ -367,6 +456,61 @@ async function handleUpdateTask(
367
456
  }
368
457
  if (params.status === "DONE") updates.completed_at = new Date().toISOString();
369
458
 
459
+ // Handle custom_fields updates (execution_plan, goals, constraints, system_prompt, instructions)
460
+ const hasCustomFields = params.execution_plan !== undefined ||
461
+ params.goals !== undefined ||
462
+ params.constraints !== undefined ||
463
+ params.system_prompt !== undefined ||
464
+ params.instructions !== undefined;
465
+
466
+ if (hasCustomFields) {
467
+ // Fetch existing custom_fields to merge
468
+ const { data: existing } = await supabase
469
+ .from("tasks")
470
+ .select("custom_fields")
471
+ .eq("id", params.task_id as string)
472
+ .eq("user_id", userId)
473
+ .single();
474
+
475
+ const existingCf = (existing?.custom_fields || {}) as Record<string, any>;
476
+ const mergedCf = { ...existingCf };
477
+
478
+ if (params.execution_plan !== undefined) {
479
+ mergedCf.execution_plan = Array.isArray(params.execution_plan)
480
+ ? (params.execution_plan as any[]).map((step: any, i: number) => ({
481
+ id: step.id || `step-${Date.now()}-${i}`,
482
+ text: typeof step === "string" ? step : step.text || String(step),
483
+ order: i,
484
+ }))
485
+ : [];
486
+ }
487
+
488
+ if (params.goals !== undefined) {
489
+ mergedCf.goals = Array.isArray(params.goals)
490
+ ? (params.goals as any[]).map((g: any, i: number) => ({
491
+ id: g.id || `goal-${Date.now()}-${i}`,
492
+ type: g.type || "custom",
493
+ label: typeof g === "string" ? g : g.label || String(g),
494
+ }))
495
+ : [];
496
+ }
497
+
498
+ if (params.constraints !== undefined) {
499
+ mergedCf.constraints = Array.isArray(params.constraints)
500
+ ? (params.constraints as any[]).map((c: any, i: number) => ({
501
+ id: c.id || `cstr-${Date.now()}-${i}`,
502
+ type: c.type || "custom",
503
+ label: typeof c === "string" ? c : c.label || String(c),
504
+ }))
505
+ : [];
506
+ }
507
+
508
+ if (params.system_prompt !== undefined) mergedCf.system_prompt = params.system_prompt;
509
+ if (params.instructions !== undefined) mergedCf.instructions = params.instructions;
510
+
511
+ updates.custom_fields = mergedCf;
512
+ }
513
+
370
514
  const { data, error } = await supabase
371
515
  .from("tasks")
372
516
  .update(updates)
@@ -423,7 +567,9 @@ async function handleDeleteTask(
423
567
  }
424
568
  }
425
569
 
426
- // ─── META-TOOL: OFIERE_AGENT_OPS ────────────────────────────────────────────
570
+ // ═══════════════════════════════════════════════════════════════════════════════
571
+ // META-TOOL 2: OFIERE_AGENT_OPS — Agent Management
572
+ // ═══════════════════════════════════════════════════════════════════════════════
427
573
 
428
574
  function registerAgentOps(
429
575
  api: any,
@@ -495,11 +641,768 @@ async function handleListAgents(
495
641
  }
496
642
  }
497
643
 
498
- // ─── Public: Register All Meta-Tools ─────────────────────────────────────────
644
+ // ═══════════════════════════════════════════════════════════════════════════════
645
+ // META-TOOL 3: OFIERE_PROJECT_OPS — Spaces, Folders & Dependencies
646
+ // ═══════════════════════════════════════════════════════════════════════════════
647
+
648
+ function registerProjectOps(
649
+ api: any,
650
+ supabase: SupabaseClient,
651
+ userId: string,
652
+ ): void {
653
+ api.registerTool({
654
+ name: "OFIERE_PROJECT_OPS",
655
+ label: "Ofiere Project Operations",
656
+ description:
657
+ `Manage PM hierarchy: spaces, folders, and task dependencies.\n\n` +
658
+ `Actions:\n` +
659
+ `- "list_spaces": List all PM spaces\n` +
660
+ `- "create_space": Create a space. Required: name. Optional: description, icon, icon_color\n` +
661
+ `- "update_space": Update a space. Required: id. Optional: name, description, icon, icon_color, sort_order\n` +
662
+ `- "delete_space": Delete a space. Required: id\n` +
663
+ `- "list_folders": List folders. Optional: space_id to filter\n` +
664
+ `- "create_folder": Create. Required: name, space_id. Optional: parent_folder_id, folder_type\n` +
665
+ `- "update_folder": Update. Required: id. Optional: name, space_id, parent_folder_id, sort_order\n` +
666
+ `- "delete_folder": Delete. Required: id\n` +
667
+ `- "list_dependencies": List task dependencies. Optional: task_id\n` +
668
+ `- "add_dependency": Link tasks. Required: predecessor_id, successor_id. Optional: dependency_type, lag_days\n` +
669
+ `- "remove_dependency": Unlink. Required: dependency_id\n` +
670
+ `dependency_type: finish_to_start (default), start_to_start, finish_to_finish, start_to_finish`,
671
+ parameters: {
672
+ type: "object",
673
+ required: ["action"],
674
+ properties: {
675
+ action: {
676
+ type: "string",
677
+ description: "The operation to perform",
678
+ enum: ["list_spaces", "create_space", "update_space", "delete_space",
679
+ "list_folders", "create_folder", "update_folder", "delete_folder",
680
+ "list_dependencies", "add_dependency", "remove_dependency"],
681
+ },
682
+ id: { type: "string", description: "Space, folder, or dependency ID" },
683
+ name: { type: "string", description: "Name for space/folder" },
684
+ description: { type: "string", description: "Description" },
685
+ icon: { type: "string", description: "Emoji icon for space" },
686
+ icon_color: { type: "string", description: "Hex color for space icon" },
687
+ space_id: { type: "string", description: "Parent space ID" },
688
+ parent_folder_id: { type: "string", description: "Parent folder ID for nesting" },
689
+ folder_type: { type: "string", enum: ["folder", "project"], description: "Folder type" },
690
+ sort_order: { type: "number", description: "Sort order" },
691
+ predecessor_id: { type: "string", description: "Task that must complete first" },
692
+ successor_id: { type: "string", description: "Task that depends on predecessor" },
693
+ dependency_type: {
694
+ type: "string",
695
+ enum: ["finish_to_start", "start_to_start", "finish_to_finish", "start_to_finish"],
696
+ description: "Type of dependency link",
697
+ },
698
+ lag_days: { type: "number", description: "Days of lag between tasks (default 0)" },
699
+ task_id: { type: "string", description: "Filter dependencies by task ID" },
700
+ dependency_id: { type: "string", description: "Dependency ID to remove" },
701
+ },
702
+ },
703
+ async execute(_id: string, params: Record<string, unknown>) {
704
+ const action = params.action as string;
705
+ switch (action) {
706
+ // ── Spaces ──
707
+ case "list_spaces": {
708
+ const { data, error } = await supabase.from("pm_spaces").select("*").eq("user_id", userId).order("sort_order");
709
+ if (error) return err(error.message);
710
+ return ok({ spaces: data || [], count: (data || []).length });
711
+ }
712
+ case "create_space": {
713
+ if (!params.name) return err("Missing required: name");
714
+ const { data, error } = await supabase.from("pm_spaces").insert({
715
+ user_id: userId,
716
+ name: params.name,
717
+ description: (params.description as string) || "",
718
+ icon: (params.icon as string) || "📁",
719
+ icon_color: (params.icon_color as string) || "#FF6D29",
720
+ access_type: "private",
721
+ sort_order: (params.sort_order as number) || 0,
722
+ }).select().single();
723
+ if (error) return err(error.message);
724
+ return ok({ message: `Space "${params.name}" created`, space: data });
725
+ }
726
+ case "update_space": {
727
+ if (!params.id) return err("Missing required: id");
728
+ const upd: Record<string, any> = { updated_at: new Date().toISOString() };
729
+ for (const f of ["name", "description", "icon", "icon_color", "sort_order"]) {
730
+ if ((params as any)[f] !== undefined) upd[f] = (params as any)[f];
731
+ }
732
+ const { error } = await supabase.from("pm_spaces").update(upd).eq("id", params.id).eq("user_id", userId);
733
+ if (error) return err(error.message);
734
+ return ok({ message: "Space updated", ok: true });
735
+ }
736
+ case "delete_space": {
737
+ if (!params.id) return err("Missing required: id");
738
+ const { error } = await supabase.from("pm_spaces").delete().eq("id", params.id).eq("user_id", userId);
739
+ if (error) return err(error.message);
740
+ return ok({ message: "Space deleted", ok: true });
741
+ }
742
+ // ── Folders ──
743
+ case "list_folders": {
744
+ let q = supabase.from("pm_folders").select("*").eq("user_id", userId).order("sort_order");
745
+ if (params.space_id) q = q.eq("space_id", params.space_id as string);
746
+ const { data, error } = await q;
747
+ if (error) return err(error.message);
748
+ return ok({ folders: data || [], count: (data || []).length });
749
+ }
750
+ case "create_folder": {
751
+ if (!params.name || !params.space_id) return err("Missing required: name, space_id");
752
+ const { data, error } = await supabase.from("pm_folders").insert({
753
+ user_id: userId,
754
+ space_id: params.space_id,
755
+ parent_folder_id: (params.parent_folder_id as string) || null,
756
+ name: params.name,
757
+ description: "",
758
+ folder_type: (params.folder_type as string) || "folder",
759
+ sort_order: (params.sort_order as number) || 0,
760
+ }).select().single();
761
+ if (error) return err(error.message);
762
+ return ok({ message: `Folder "${params.name}" created`, folder: data });
763
+ }
764
+ case "update_folder": {
765
+ if (!params.id) return err("Missing required: id");
766
+ const upd: Record<string, any> = { updated_at: new Date().toISOString() };
767
+ for (const f of ["name", "description", "space_id", "parent_folder_id", "folder_type", "sort_order"]) {
768
+ if ((params as any)[f] !== undefined) upd[f] = (params as any)[f];
769
+ }
770
+ const { error } = await supabase.from("pm_folders").update(upd).eq("id", params.id).eq("user_id", userId);
771
+ if (error) return err(error.message);
772
+ return ok({ message: "Folder updated", ok: true });
773
+ }
774
+ case "delete_folder": {
775
+ if (!params.id) return err("Missing required: id");
776
+ const { error } = await supabase.from("pm_folders").delete().eq("id", params.id).eq("user_id", userId);
777
+ if (error) return err(error.message);
778
+ return ok({ message: "Folder deleted", ok: true });
779
+ }
780
+ // ── Dependencies ──
781
+ case "list_dependencies": {
782
+ let q = supabase.from("pm_dependencies").select("*").eq("user_id", userId);
783
+ if (params.task_id) {
784
+ q = supabase.from("pm_dependencies").select("*").eq("user_id", userId)
785
+ .or(`predecessor_id.eq.${params.task_id},successor_id.eq.${params.task_id}`);
786
+ }
787
+ const { data, error } = await q;
788
+ if (error) return err(error.message);
789
+ return ok({ dependencies: data || [], count: (data || []).length });
790
+ }
791
+ case "add_dependency": {
792
+ if (!params.predecessor_id || !params.successor_id) return err("Missing required: predecessor_id, successor_id");
793
+ const { data, error } = await supabase.from("pm_dependencies").insert({
794
+ user_id: userId,
795
+ predecessor_id: params.predecessor_id,
796
+ successor_id: params.successor_id,
797
+ dependency_type: (params.dependency_type as string) || "finish_to_start",
798
+ lag_days: (params.lag_days as number) || 0,
799
+ }).select().single();
800
+ if (error) return err(error.message);
801
+ return ok({ message: "Dependency created", dependency: data });
802
+ }
803
+ case "remove_dependency": {
804
+ const depId = (params.dependency_id || params.id) as string;
805
+ if (!depId) return err("Missing required: dependency_id");
806
+ const { error } = await supabase.from("pm_dependencies").delete().eq("id", depId).eq("user_id", userId);
807
+ if (error) return err(error.message);
808
+ return ok({ message: "Dependency removed", ok: true });
809
+ }
810
+ default:
811
+ return err(`Unknown action "${action}".`);
812
+ }
813
+ },
814
+ });
815
+ }
816
+
817
+ // ═══════════════════════════════════════════════════════════════════════════════
818
+ // META-TOOL 4: OFIERE_SCHEDULE_OPS — Calendar & Scheduler Events
819
+ // ═══════════════════════════════════════════════════════════════════════════════
820
+
821
+ function registerScheduleOps(
822
+ api: any,
823
+ supabase: SupabaseClient,
824
+ userId: string,
825
+ ): void {
826
+ api.registerTool({
827
+ name: "OFIERE_SCHEDULE_OPS",
828
+ label: "Ofiere Schedule Operations",
829
+ description:
830
+ `Manage calendar events and schedule tasks on the timeline.\n\n` +
831
+ `Actions:\n` +
832
+ `- "list": List events. Optional: start_date, end_date, agent_id\n` +
833
+ `- "create": Schedule an event. Required: title, scheduled_date. Optional: task_id, agent_id, scheduled_time, duration_minutes, recurrence_type, recurrence_interval, color, priority\n` +
834
+ `- "update": Update event. Required: id. Optional: title, scheduled_date, scheduled_time, duration_minutes, status, recurrence_type\n` +
835
+ `- "delete": Remove event. Required: id\n` +
836
+ `recurrence_type: none, hourly, daily, weekly, monthly\n` +
837
+ `priority: 0=low, 1=medium, 2=high, 3=critical`,
838
+ parameters: {
839
+ type: "object",
840
+ required: ["action"],
841
+ properties: {
842
+ action: { type: "string", enum: ["list", "create", "update", "delete"] },
843
+ id: { type: "string", description: "Event ID" },
844
+ title: { type: "string", description: "Event title" },
845
+ description: { type: "string" },
846
+ task_id: { type: "string", description: "Link to a task" },
847
+ agent_id: { type: "string", description: "Assigned agent" },
848
+ scheduled_date: { type: "string", description: "Date (YYYY-MM-DD)" },
849
+ scheduled_time: { type: "string", description: "Time (HH:MM)" },
850
+ start_date: { type: "string", description: "List filter: start (YYYY-MM-DD)" },
851
+ end_date: { type: "string", description: "List filter: end (YYYY-MM-DD)" },
852
+ duration_minutes: { type: "number", description: "Duration in minutes (default 30)" },
853
+ recurrence_type: { type: "string", enum: ["none", "hourly", "daily", "weekly", "monthly"] },
854
+ recurrence_interval: { type: "number", description: "Repeat every N periods" },
855
+ color: { type: "string", description: "Hex color" },
856
+ priority: { type: "number", description: "0-3" },
857
+ status: { type: "string", enum: ["scheduled", "completed", "cancelled"] },
858
+ },
859
+ },
860
+ async execute(_id: string, params: Record<string, unknown>) {
861
+ const action = params.action as string;
862
+ switch (action) {
863
+ case "list": {
864
+ let q = supabase.from("scheduler_events").select("*").eq("user_id", userId)
865
+ .order("scheduled_date", { ascending: true });
866
+ if (params.start_date) q = q.gte("scheduled_date", params.start_date as string);
867
+ if (params.end_date) q = q.lte("scheduled_date", params.end_date as string);
868
+ if (params.agent_id) q = q.eq("agent_id", params.agent_id as string);
869
+ const { data, error } = await q;
870
+ if (error) return err(error.message);
871
+ return ok({ events: data || [], count: (data || []).length });
872
+ }
873
+ case "create": {
874
+ if (!params.title || !params.scheduled_date) return err("Missing required: title, scheduled_date");
875
+ const evtId = crypto.randomUUID();
876
+ const priorityMap: Record<string, number> = { low: 0, medium: 1, high: 2, critical: 3 };
877
+ const pVal = typeof params.priority === "number" ? params.priority
878
+ : priorityMap[String(params.priority || "").toLowerCase()] ?? 0;
879
+ const insertData: Record<string, any> = {
880
+ id: evtId,
881
+ user_id: userId,
882
+ task_id: (params.task_id as string) || null,
883
+ agent_id: (params.agent_id as string) || null,
884
+ title: params.title,
885
+ description: (params.description as string) || null,
886
+ scheduled_date: params.scheduled_date,
887
+ scheduled_time: (params.scheduled_time as string) || null,
888
+ duration_minutes: (params.duration_minutes as number) || 30,
889
+ recurrence_type: (params.recurrence_type as string) || "none",
890
+ recurrence_interval: (params.recurrence_interval as number) || 1,
891
+ status: "scheduled",
892
+ run_count: 0,
893
+ color: (params.color as string) || null,
894
+ priority: pVal,
895
+ };
896
+ const { error } = await supabase.from("scheduler_events").insert(insertData);
897
+ if (error) return err(error.message);
898
+ return ok({ message: `Event "${params.title}" scheduled for ${params.scheduled_date}`, id: evtId });
899
+ }
900
+ case "update": {
901
+ if (!params.id) return err("Missing required: id");
902
+ const upd: Record<string, any> = { updated_at: new Date().toISOString() };
903
+ for (const f of ["title", "description", "scheduled_date", "scheduled_time", "duration_minutes",
904
+ "recurrence_type", "recurrence_interval", "status", "color", "priority", "agent_id"]) {
905
+ if ((params as any)[f] !== undefined) upd[f] = (params as any)[f];
906
+ }
907
+ const { error } = await supabase.from("scheduler_events").update(upd).eq("id", params.id);
908
+ if (error) return err(error.message);
909
+ return ok({ message: "Event updated", ok: true });
910
+ }
911
+ case "delete": {
912
+ if (!params.id) return err("Missing required: id");
913
+ const { error } = await supabase.from("scheduler_events").delete().eq("id", params.id);
914
+ if (error) return err(error.message);
915
+ return ok({ message: "Event deleted", ok: true });
916
+ }
917
+ default:
918
+ return err(`Unknown action "${action}".`);
919
+ }
920
+ },
921
+ });
922
+ }
923
+
924
+ // ═══════════════════════════════════════════════════════════════════════════════
925
+ // META-TOOL 5: OFIERE_KNOWLEDGE_OPS — Knowledge Base
926
+ // ═══════════════════════════════════════════════════════════════════════════════
927
+
928
+ function registerKnowledgeOps(
929
+ api: any,
930
+ supabase: SupabaseClient,
931
+ userId: string,
932
+ ): void {
933
+ api.registerTool({
934
+ name: "OFIERE_KNOWLEDGE_OPS",
935
+ label: "Ofiere Knowledge Operations",
936
+ description:
937
+ `Search, add, and manage knowledge documents. This is the long-term memory system.\n\n` +
938
+ `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` +
942
+ `- "update": Edit a document. Required: id. Optional: file_name, content, text, source, source_type, author\n` +
943
+ `- "delete": Remove a document. Required: id`,
944
+ parameters: {
945
+ type: "object",
946
+ required: ["action"],
947
+ properties: {
948
+ action: { type: "string", enum: ["search", "list", "create", "update", "delete"] },
949
+ id: { type: "string", description: "Document ID" },
950
+ query: { type: "string", description: "Search query" },
951
+ file_name: { type: "string", description: "Document name" },
952
+ content: { type: "string", description: "Raw content" },
953
+ text: { type: "string", description: "Processed text" },
954
+ source: { type: "string", description: "Source URL or reference" },
955
+ source_type: { type: "string", description: "e.g. web, pdf, manual" },
956
+ author: { type: "string", description: "Author name" },
957
+ credibility_tier: { type: "string", description: "Credibility level" },
958
+ page: { type: "number", description: "Page number (default 1)" },
959
+ page_size: { type: "number", description: "Results per page (default 20)" },
960
+ search: { type: "string", description: "Filter for list action" },
961
+ limit: { type: "number", description: "Max results for search" },
962
+ },
963
+ },
964
+ async execute(_id: string, params: Record<string, unknown>) {
965
+ const action = params.action as string;
966
+ switch (action) {
967
+ case "search": {
968
+ if (!params.query) return err("Missing required: query");
969
+ const lim = (params.limit as number) || 20;
970
+ const searchTerm = `%${params.query}%`;
971
+ const { data, error } = await supabase
972
+ .from("knowledge_documents")
973
+ .select("id, file_name, file_type, content, source, source_type, author, credibility_tier")
974
+ .or(`file_name.ilike.${searchTerm},content.ilike.${searchTerm},author.ilike.${searchTerm},source.ilike.${searchTerm}`)
975
+ .order("created_at", { ascending: false })
976
+ .limit(lim);
977
+ if (error) return err(error.message);
978
+ return ok({ documents: data || [], count: (data || []).length, query: params.query });
979
+ }
980
+ case "list": {
981
+ const page = Math.max(1, (params.page as number) || 1);
982
+ const pageSize = Math.min(100, Math.max(1, (params.page_size as number) || 20));
983
+ const from = (page - 1) * pageSize;
984
+ const to = from + pageSize - 1;
985
+ let q = supabase.from("knowledge_documents")
986
+ .select("id, file_name, file_type, source, source_type, author, credibility_tier, created_at", { count: "exact" })
987
+ .order("created_at", { ascending: false })
988
+ .range(from, to);
989
+ if (params.search) {
990
+ const s = `%${params.search}%`;
991
+ q = q.or(`file_name.ilike.${s},content.ilike.${s},author.ilike.${s}`);
992
+ }
993
+ const { data, count, error } = await q;
994
+ if (error) return err(error.message);
995
+ return ok({ documents: data || [], total: count || 0, page, page_size: pageSize });
996
+ }
997
+ case "create": {
998
+ if (!params.file_name) return err("Missing required: file_name");
999
+ const docId = crypto.randomUUID();
1000
+ const { error } = await supabase.from("knowledge_documents").insert({
1001
+ id: docId,
1002
+ user_id: userId,
1003
+ file_name: params.file_name,
1004
+ file_type: (params.file_type as string) || null,
1005
+ content: (params.content as string) || null,
1006
+ text: (params.text as string) || null,
1007
+ source: (params.source as string) || null,
1008
+ source_type: (params.source_type as string) || null,
1009
+ author: (params.author as string) || null,
1010
+ credibility_tier: (params.credibility_tier as string) || null,
1011
+ size_bytes: params.content ? new TextEncoder().encode(params.content as string).length : 0,
1012
+ indexed: false,
1013
+ });
1014
+ if (error) return err(error.message);
1015
+ return ok({ message: `Knowledge doc "${params.file_name}" created`, id: docId });
1016
+ }
1017
+ case "update": {
1018
+ if (!params.id) return err("Missing required: id");
1019
+ const allowed = ["file_name", "file_type", "content", "text", "source", "source_type", "author", "credibility_tier"];
1020
+ const upd: Record<string, any> = {};
1021
+ for (const k of allowed) if ((params as any)[k] !== undefined) upd[k] = (params as any)[k];
1022
+ if (Object.keys(upd).length === 0) return err("No valid fields to update");
1023
+ const { error } = await supabase.from("knowledge_documents").update(upd).eq("id", params.id);
1024
+ if (error) return err(error.message);
1025
+ return ok({ message: "Document updated", ok: true });
1026
+ }
1027
+ case "delete": {
1028
+ if (!params.id) return err("Missing required: id");
1029
+ const { error } = await supabase.from("knowledge_documents").delete().eq("id", params.id);
1030
+ if (error) return err(error.message);
1031
+ return ok({ message: "Document deleted", ok: true });
1032
+ }
1033
+ default:
1034
+ return err(`Unknown action "${action}".`);
1035
+ }
1036
+ },
1037
+ });
1038
+ }
1039
+
1040
+ // ═══════════════════════════════════════════════════════════════════════════════
1041
+ // META-TOOL 6: OFIERE_WORKFLOW_OPS — Workflow Management & Execution
1042
+ // ═══════════════════════════════════════════════════════════════════════════════
1043
+
1044
+ function registerWorkflowOps(
1045
+ api: any,
1046
+ supabase: SupabaseClient,
1047
+ userId: string,
1048
+ ): void {
1049
+ api.registerTool({
1050
+ name: "OFIERE_WORKFLOW_OPS",
1051
+ label: "Ofiere Workflow Operations",
1052
+ description:
1053
+ `Manage and trigger automated workflows.\n\n` +
1054
+ `Actions:\n` +
1055
+ `- "list": List all workflows. Optional: status\n` +
1056
+ `- "get": Get workflow details. Required: id\n` +
1057
+ `- "create": Create a workflow. Required: name. Optional: description, steps, schedule, status\n` +
1058
+ `- "list_runs": List recent runs. Required: workflow_id. Optional: limit\n` +
1059
+ `- "trigger": Start a workflow run. Required: workflow_id`,
1060
+ parameters: {
1061
+ type: "object",
1062
+ required: ["action"],
1063
+ properties: {
1064
+ action: { type: "string", enum: ["list", "get", "create", "list_runs", "trigger"] },
1065
+ id: { type: "string", description: "Workflow ID" },
1066
+ workflow_id: { type: "string", description: "Workflow ID for runs/trigger" },
1067
+ name: { type: "string", description: "Workflow name" },
1068
+ description: { type: "string" },
1069
+ steps: { type: "array", items: { type: "object" }, description: "Workflow step definitions" },
1070
+ schedule: { type: "string", description: "Cron expression or schedule" },
1071
+ status: { type: "string", enum: ["draft", "active", "paused", "archived"] },
1072
+ limit: { type: "number", description: "Max results" },
1073
+ },
1074
+ },
1075
+ async execute(_id: string, params: Record<string, unknown>) {
1076
+ const action = params.action as string;
1077
+ switch (action) {
1078
+ case "list": {
1079
+ let q = supabase.from("workflows").select("*").eq("user_id", userId).order("updated_at", { ascending: false });
1080
+ if (params.status) q = q.eq("status", params.status as string);
1081
+ const { data, error } = await q;
1082
+ if (error) return err(error.message);
1083
+ return ok({ workflows: data || [], count: (data || []).length });
1084
+ }
1085
+ case "get": {
1086
+ const wfId = (params.id || params.workflow_id) as string;
1087
+ if (!wfId) return err("Missing required: id");
1088
+ const { data, error } = await supabase.from("workflows").select("*").eq("id", wfId).eq("user_id", userId).single();
1089
+ if (error) return err(error.message);
1090
+ return ok({ workflow: data });
1091
+ }
1092
+ case "create": {
1093
+ if (!params.name) return err("Missing required: name");
1094
+ const wfId = crypto.randomUUID();
1095
+ const stepsWithIds = ((params.steps as any[]) || []).map((s: any, i: number) => ({
1096
+ ...s, id: s.id || `step-${i}`,
1097
+ }));
1098
+ const { data, error } = await supabase.from("workflows").insert({
1099
+ id: wfId, user_id: userId,
1100
+ name: params.name,
1101
+ description: (params.description as string) || null,
1102
+ steps: stepsWithIds,
1103
+ schedule: (params.schedule as string) || null,
1104
+ status: (params.status as string) || "draft",
1105
+ nodes: [], edges: [], definition_version: 1,
1106
+ }).select().single();
1107
+ if (error) return err(error.message);
1108
+ return ok({ message: `Workflow "${params.name}" created`, workflow: data });
1109
+ }
1110
+ case "list_runs": {
1111
+ const wfId = (params.workflow_id || params.id) as string;
1112
+ if (!wfId) return err("Missing required: workflow_id");
1113
+ const { data, error } = await supabase.from("workflow_runs").select("*")
1114
+ .eq("workflow_id", wfId)
1115
+ .order("created_at", { ascending: false })
1116
+ .limit((params.limit as number) || 20);
1117
+ if (error) return err(error.message);
1118
+ return ok({ runs: data || [], count: (data || []).length });
1119
+ }
1120
+ case "trigger": {
1121
+ const wfId = (params.workflow_id || params.id) as string;
1122
+ if (!wfId) return err("Missing required: workflow_id");
1123
+ const runId = crypto.randomUUID();
1124
+ const { error } = await supabase.from("workflow_runs").insert({
1125
+ id: runId,
1126
+ workflow_id: wfId,
1127
+ status: "running",
1128
+ started_at: new Date().toISOString(),
1129
+ trigger_type: "agent",
1130
+ });
1131
+ if (error) return err(error.message);
1132
+ return ok({ message: `Workflow run triggered`, run_id: runId, workflow_id: wfId });
1133
+ }
1134
+ default:
1135
+ return err(`Unknown action "${action}".`);
1136
+ }
1137
+ },
1138
+ });
1139
+ }
1140
+
1141
+ // ═══════════════════════════════════════════════════════════════════════════════
1142
+ // META-TOOL 7: OFIERE_NOTIFY_OPS — Notifications
1143
+ // ═══════════════════════════════════════════════════════════════════════════════
1144
+
1145
+ function registerNotifyOps(
1146
+ api: any,
1147
+ supabase: SupabaseClient,
1148
+ userId: string,
1149
+ ): void {
1150
+ api.registerTool({
1151
+ name: "OFIERE_NOTIFY_OPS",
1152
+ label: "Ofiere Notification Operations",
1153
+ description:
1154
+ `Read and manage notifications.\n\n` +
1155
+ `Actions:\n` +
1156
+ `- "list": List notifications. Optional: unread_only (true/false), limit\n` +
1157
+ `- "mark_read": Mark one as read. Required: id\n` +
1158
+ `- "mark_all_read": Mark all as read\n` +
1159
+ `- "delete": Delete a notification. Required: id`,
1160
+ parameters: {
1161
+ type: "object",
1162
+ required: ["action"],
1163
+ properties: {
1164
+ action: { type: "string", enum: ["list", "mark_read", "mark_all_read", "delete"] },
1165
+ id: { type: "string", description: "Notification ID" },
1166
+ unread_only: { type: "boolean", description: "Only show unread" },
1167
+ limit: { type: "number", description: "Max results (default 50)" },
1168
+ },
1169
+ },
1170
+ async execute(_id: string, params: Record<string, unknown>) {
1171
+ const action = params.action as string;
1172
+ switch (action) {
1173
+ case "list": {
1174
+ let q = supabase.from("notifications").select("*")
1175
+ .order("created_at", { ascending: false })
1176
+ .limit((params.limit as number) || 50);
1177
+ if (params.unread_only === true) q = q.eq("read", false);
1178
+ const { data, error } = await q;
1179
+ if (error) return err(error.message);
1180
+ const unread = (data || []).filter((n: any) => !n.read).length;
1181
+ return ok({ notifications: data || [], count: (data || []).length, unread_count: unread });
1182
+ }
1183
+ case "mark_read": {
1184
+ if (!params.id) return err("Missing required: id");
1185
+ const { error } = await supabase.from("notifications").update({ read: true }).eq("id", params.id);
1186
+ if (error) return err(error.message);
1187
+ return ok({ message: "Notification marked as read", ok: true });
1188
+ }
1189
+ case "mark_all_read": {
1190
+ const { error } = await supabase.from("notifications").update({ read: true }).eq("read", false);
1191
+ if (error) return err(error.message);
1192
+ return ok({ message: "All notifications marked as read", ok: true });
1193
+ }
1194
+ case "delete": {
1195
+ if (!params.id) return err("Missing required: id");
1196
+ const { error } = await supabase.from("notifications").delete().eq("id", params.id);
1197
+ if (error) return err(error.message);
1198
+ return ok({ message: "Notification deleted", ok: true });
1199
+ }
1200
+ default:
1201
+ return err(`Unknown action "${action}".`);
1202
+ }
1203
+ },
1204
+ });
1205
+ }
1206
+
1207
+ // ═══════════════════════════════════════════════════════════════════════════════
1208
+ // META-TOOL 8: OFIERE_MEMORY_OPS — Conversations & Knowledge Fragments
1209
+ // ═══════════════════════════════════════════════════════════════════════════════
1210
+
1211
+ function registerMemoryOps(
1212
+ api: any,
1213
+ supabase: SupabaseClient,
1214
+ userId: string,
1215
+ ): void {
1216
+ api.registerTool({
1217
+ name: "OFIERE_MEMORY_OPS",
1218
+ label: "Ofiere Memory Operations",
1219
+ description:
1220
+ `Access conversation history and knowledge memory.\n\n` +
1221
+ `Actions:\n` +
1222
+ `- "list_conversations": List recent conversations. Optional: agent_id, limit\n` +
1223
+ `- "get_messages": Get messages from a conversation. Required: conversation_id. Optional: limit\n` +
1224
+ `- "search_messages": Search all messages. Required: query. Optional: agent_id, limit\n` +
1225
+ `- "add_knowledge": Store a knowledge fragment. Required: agent_id, content, source. Optional: tags, importance\n` +
1226
+ `- "search_knowledge": Search knowledge. Required: agent_id, query. Optional: limit`,
1227
+ parameters: {
1228
+ type: "object",
1229
+ required: ["action"],
1230
+ properties: {
1231
+ action: { type: "string", enum: ["list_conversations", "get_messages", "search_messages", "add_knowledge", "search_knowledge"] },
1232
+ conversation_id: { type: "string" },
1233
+ agent_id: { type: "string" },
1234
+ query: { type: "string", description: "Search query" },
1235
+ content: { type: "string", description: "Knowledge content to store" },
1236
+ source: { type: "string", description: "Source of knowledge" },
1237
+ tags: { type: "array", items: { type: "string" } },
1238
+ importance: { type: "number", description: "1-10 importance scale" },
1239
+ limit: { type: "number", description: "Max results" },
1240
+ },
1241
+ },
1242
+ async execute(_id: string, params: Record<string, unknown>) {
1243
+ const action = params.action as string;
1244
+ switch (action) {
1245
+ case "list_conversations": {
1246
+ let q = supabase.from("conversations")
1247
+ .select("id, agent_id, title, created_at, updated_at")
1248
+ .eq("user_id", userId)
1249
+ .order("updated_at", { ascending: false })
1250
+ .limit((params.limit as number) || 20);
1251
+ if (params.agent_id) q = q.eq("agent_id", params.agent_id as string);
1252
+ const { data, error } = await q;
1253
+ if (error) return err(error.message);
1254
+ return ok({ conversations: data || [], count: (data || []).length });
1255
+ }
1256
+ case "get_messages": {
1257
+ if (!params.conversation_id) return err("Missing required: conversation_id");
1258
+ const { data, error } = await supabase.from("conversation_messages")
1259
+ .select("id, role, content, created_at")
1260
+ .eq("conversation_id", params.conversation_id as string)
1261
+ .order("created_at", { ascending: true })
1262
+ .limit((params.limit as number) || 100);
1263
+ if (error) return err(error.message);
1264
+ return ok({ messages: data || [], count: (data || []).length });
1265
+ }
1266
+ case "search_messages": {
1267
+ if (!params.query) return err("Missing required: query");
1268
+ const searchTerm = `%${params.query}%`;
1269
+ let q = supabase.from("conversation_messages")
1270
+ .select("id, conversation_id, role, content, created_at")
1271
+ .ilike("content", searchTerm)
1272
+ .order("created_at", { ascending: false })
1273
+ .limit((params.limit as number) || 20);
1274
+ const { data, error } = await q;
1275
+ if (error) return err(error.message);
1276
+ return ok({ messages: data || [], count: (data || []).length, query: params.query });
1277
+ }
1278
+ case "add_knowledge": {
1279
+ if (!params.agent_id || !params.content || !params.source) return err("Missing required: agent_id, content, source");
1280
+ const fragId = crypto.randomUUID();
1281
+ const { error } = await supabase.from("knowledge_fragments").insert({
1282
+ id: fragId,
1283
+ agent_id: params.agent_id,
1284
+ content: params.content,
1285
+ source: params.source,
1286
+ tags: (params.tags as string[]) || [],
1287
+ importance: (params.importance as number) || 5,
1288
+ });
1289
+ if (error) return err(error.message);
1290
+ return ok({ message: "Knowledge stored", id: fragId });
1291
+ }
1292
+ case "search_knowledge": {
1293
+ if (!params.agent_id || !params.query) return err("Missing required: agent_id, query");
1294
+ const searchTerm = `%${params.query}%`;
1295
+ const { data, error } = await supabase.from("knowledge_fragments")
1296
+ .select("id, content, source, tags, importance, created_at")
1297
+ .eq("agent_id", params.agent_id as string)
1298
+ .ilike("content", searchTerm)
1299
+ .order("importance", { ascending: false })
1300
+ .limit((params.limit as number) || 20);
1301
+ if (error) return err(error.message);
1302
+ return ok({ fragments: data || [], count: (data || []).length });
1303
+ }
1304
+ default:
1305
+ return err(`Unknown action "${action}".`);
1306
+ }
1307
+ },
1308
+ });
1309
+ }
1310
+
1311
+ // ═══════════════════════════════════════════════════════════════════════════════
1312
+ // META-TOOL 9: OFIERE_PROMPT_OPS — System Prompt Chunk Management
1313
+ // ═══════════════════════════════════════════════════════════════════════════════
1314
+
1315
+ function registerPromptOps(
1316
+ api: any,
1317
+ supabase: SupabaseClient,
1318
+ userId: string,
1319
+ ): void {
1320
+ api.registerTool({
1321
+ name: "OFIERE_PROMPT_OPS",
1322
+ label: "Ofiere Prompt Operations",
1323
+ description:
1324
+ `Manage system prompt instruction chunks. These are the building blocks of agent behavior.\n\n` +
1325
+ `Actions:\n` +
1326
+ `- "list": List all prompt chunks. Optional: agent_id\n` +
1327
+ `- "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` +
1330
+ `- "delete": Delete a chunk. Required: id`,
1331
+ parameters: {
1332
+ type: "object",
1333
+ required: ["action"],
1334
+ properties: {
1335
+ action: { type: "string", enum: ["list", "get", "create", "update", "delete"] },
1336
+ 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" },
1342
+ },
1343
+ },
1344
+ async execute(_id: string, params: Record<string, unknown>) {
1345
+ const action = params.action as string;
1346
+ switch (action) {
1347
+ 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;
1351
+ if (error) return err(error.message);
1352
+ return ok({ chunks: data || [], count: (data || []).length });
1353
+ }
1354
+ case "get": {
1355
+ if (!params.id) return err("Missing required: id");
1356
+ const { data, error } = await supabase.from("prompt_chunks").select("*").eq("id", params.id).single();
1357
+ if (error) return err(error.message);
1358
+ return ok({ chunk: data });
1359
+ }
1360
+ case "create": {
1361
+ if (!params.label || !params.content) return err("Missing required: label, content");
1362
+ const chunkId = crypto.randomUUID();
1363
+ const { data, error } = await supabase.from("prompt_chunks").insert({
1364
+ id: chunkId,
1365
+ user_id: userId,
1366
+ label: params.label,
1367
+ content: params.content,
1368
+ agent_id: (params.agent_id as string) || null,
1369
+ enabled: true,
1370
+ sort_order: (params.sort_order as number) || 0,
1371
+ }).select().single();
1372
+ 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 });
1375
+ }
1376
+ case "update": {
1377
+ if (!params.id) return err("Missing required: id");
1378
+ const upd: Record<string, any> = { updated_at: new Date().toISOString() };
1379
+ for (const f of ["label", "content", "enabled", "sort_order", "agent_id"]) {
1380
+ if ((params as any)[f] !== undefined) upd[f] = (params as any)[f];
1381
+ }
1382
+ const { error } = await supabase.from("prompt_chunks").update(upd).eq("id", params.id);
1383
+ if (error) return err(error.message);
1384
+ api.logger?.info?.(`[ofiere] Prompt chunk ${params.id} updated by agent`);
1385
+ return ok({ message: "Prompt chunk updated", ok: true });
1386
+ }
1387
+ case "delete": {
1388
+ if (!params.id) return err("Missing required: id");
1389
+ const { error } = await supabase.from("prompt_chunks").delete().eq("id", params.id);
1390
+ if (error) return err(error.message);
1391
+ api.logger?.info?.(`[ofiere] Prompt chunk ${params.id} deleted by agent`);
1392
+ return ok({ message: "Prompt chunk deleted", ok: true });
1393
+ }
1394
+ default:
1395
+ return err(`Unknown action "${action}".`);
1396
+ }
1397
+ },
1398
+ });
1399
+ }
1400
+
1401
+ // ═══════════════════════════════════════════════════════════════════════════════
1402
+ // Public: Register All Meta-Tools
1403
+ // ═══════════════════════════════════════════════════════════════════════════════
499
1404
  // This is the single entry point called by index.ts.
500
1405
  // 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
1406
 
504
1407
  export function registerTools(
505
1408
  api: any, // OpenClawPluginApi — typed as any to avoid import-path issues at install time
@@ -512,11 +1415,18 @@ export function registerTools(
512
1415
  const resolveAgent = createAgentResolver(api, supabase, userId, fallbackAgentId);
513
1416
 
514
1417
  // ── Register each domain meta-tool ──
515
- registerTaskOps(api, supabase, userId, resolveAgent);
516
- registerAgentOps(api, supabase, userId, fallbackAgentId);
1418
+ registerTaskOps(api, supabase, userId, resolveAgent); // 1
1419
+ registerAgentOps(api, supabase, userId, fallbackAgentId); // 2
1420
+ registerProjectOps(api, supabase, userId); // 3
1421
+ registerScheduleOps(api, supabase, userId); // 4
1422
+ registerKnowledgeOps(api, supabase, userId); // 5
1423
+ registerWorkflowOps(api, supabase, userId); // 6
1424
+ registerNotifyOps(api, supabase, userId); // 7
1425
+ registerMemoryOps(api, supabase, userId); // 8
1426
+ registerPromptOps(api, supabase, userId); // 9
517
1427
 
518
1428
  // ── Count and log ──
519
- const toolCount = 2; // Update this when adding new meta-tools
1429
+ const toolCount = 9;
520
1430
  const callerName = getCallingAgentName(api);
521
1431
  const agentLabel = fallbackAgentId || callerName || "auto-detect";
522
1432
  api.logger.info(`[ofiere] ${toolCount} meta-tools registered (agent: ${agentLabel})`);