ofiere-openclaw-plugin 1.1.1 → 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.
package/src/tools.ts CHANGED
@@ -1,10 +1,14 @@
1
- // src/tools.ts — Tool registration for Ofiere PM plugin
2
- // Uses api.registerTool(tool, opts?) as documented:
3
- // https://docs.openclaw.ai/plugins/sdk-overview#tools-and-commands
4
- // https://docs.openclaw.ai/plugins/building-plugins#registering-agent-tools
1
+ // src/tools.ts — Meta-tool registration for Ofiere PM plugin
2
+ // Architecture: Each meta-tool handles one domain (tasks, agents, etc.)
3
+ // with an "action" parameter that routes to the correct handler.
5
4
  //
6
- // - Required tools: always available (no opts)
7
- // - Optional tools: { optional: true } — user must allowlist or allowlist the plugin id
5
+ // To add a new domain:
6
+ // 1. Create a handler function (e.g. registerProjectOps)
7
+ // 2. Add it to the registerAllTools() call at the bottom
8
+ // 3. Update prompt.ts to document the new meta-tool
9
+ //
10
+ // This pattern keeps the tool count low (1 tool per domain)
11
+ // while supporting unlimited operations within each domain.
8
12
 
9
13
  import type { SupabaseClient } from "@supabase/supabase-js";
10
14
  import type { OfiereConfig } from "./types.js";
@@ -100,21 +104,19 @@ export function probeApiForAgentName(api: any, logger?: any): string {
100
104
  return "";
101
105
  }
102
106
 
103
- // ─── Tool Registration ───────────────────────────────────────────────────────
107
+ // ─── Shared: Agent ID Resolution ─────────────────────────────────────────────
104
108
 
105
- export function registerTools(
106
- api: any, // OpenClawPluginApi — typed as any to avoid import-path issues at install time
109
+ function createAgentResolver(
110
+ api: any,
107
111
  supabase: SupabaseClient,
108
- config: OfiereConfig,
109
- ): void {
110
- const userId = config.userId;
111
- const fallbackAgentId = config.agentId; // May be empty — that's fine
112
-
112
+ userId: string,
113
+ fallbackAgentId: string,
114
+ ) {
113
115
  /**
114
116
  * Resolve the agent ID for the calling agent.
115
117
  * Priority: explicit param > runtime context > registration-time detection > env var > DB fallback
116
118
  */
117
- async function resolveAgent(explicitId?: string): Promise<string | null> {
119
+ return async function resolveAgent(explicitId?: string): Promise<string | null> {
118
120
  // 1. Explicit agent_id passed by the LLM (e.g. "ivy", "daisy", or a UUID)
119
121
  if (explicitId && explicitId.trim()) {
120
122
  const trimmed = explicitId.trim();
@@ -167,327 +169,1267 @@ export function registerTools(
167
169
  }
168
170
 
169
171
  return null;
170
- }
172
+ };
173
+ }
171
174
 
172
- // ── OFIERE_LIST_TASKS — Required (read-only, no side effects) ────────
175
+ // ═══════════════════════════════════════════════════════════════════════════════
176
+ // META-TOOL 1: OFIERE_TASK_OPS — Task Management
177
+ // ═══════════════════════════════════════════════════════════════════════════════
173
178
 
179
+ function registerTaskOps(
180
+ api: any,
181
+ supabase: SupabaseClient,
182
+ userId: string,
183
+ resolveAgent: (id?: string) => Promise<string | null>,
184
+ ): void {
174
185
  api.registerTool({
175
- name: "OFIERE_LIST_TASKS",
176
- label: "List Ofiere Tasks",
186
+ name: "OFIERE_TASK_OPS",
187
+ label: "Ofiere Task Operations",
177
188
  description:
178
- "List tasks from the Ofiere PM dashboard. " +
179
- "Optionally filter by space_id, folder_id, agent_id, or status. " +
180
- "Returns an array of task objects with their details.",
189
+ `Manage tasks in the Ofiere PM dashboard. All task operations go through this tool.\n\n` +
190
+ `Actions:\n` +
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`,
181
199
  parameters: {
182
200
  type: "object",
201
+ required: ["action"],
183
202
  properties: {
184
- space_id: { type: "string", description: "Filter by PM space ID" },
185
- folder_id: { type: "string", description: "Filter by PM folder ID" },
186
- agent_id: { type: "string", description: "Filter by assigned agent ID" },
203
+ action: {
204
+ type: "string",
205
+ description: "The operation to perform",
206
+ enum: ["list", "create", "update", "delete"],
207
+ },
208
+ task_id: { type: "string", description: "Task ID (required for update, delete)" },
209
+ title: { type: "string", description: "Task title (required for create)" },
210
+ description: { type: "string", description: "Task description" },
211
+ instructions: { type: "string", description: "Detailed instructions for the agent executing this task" },
212
+ agent_id: {
213
+ type: "string",
214
+ description: "Agent name or ID. Your name to self-assign, 'none' for unassigned.",
215
+ },
187
216
  status: {
188
217
  type: "string",
189
- description: "Filter by status: PENDING, IN_PROGRESS, DONE, FAILED",
218
+ description: "Task status",
190
219
  enum: ["PENDING", "IN_PROGRESS", "DONE", "FAILED"],
191
220
  },
192
- limit: { type: "number", description: "Max results (default 50)" },
221
+ priority: { type: "number", description: "Priority: 0=LOW, 1=MEDIUM, 2=HIGH, 3=CRITICAL" },
222
+ progress: { type: "number", description: "Progress percentage 0-100 (update only)" },
223
+ space_id: { type: "string", description: "PM Space ID" },
224
+ folder_id: { type: "string", description: "PM Folder ID" },
225
+ start_date: { type: "string", description: "Start date (ISO 8601)" },
226
+ due_date: { type: "string", description: "Due date (ISO 8601)" },
227
+ tags: {
228
+ type: "array",
229
+ items: { type: "string" },
230
+ description: "Tags for the task",
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" },
268
+ limit: { type: "number", description: "Max results for list (default 50)" },
193
269
  },
194
270
  },
195
271
  async execute(_id: string, params: Record<string, unknown>) {
196
- try {
197
- let query = supabase
198
- .from("tasks")
199
- .select(
200
- "id, title, description, status, priority, agent_id, space_id, folder_id, " +
201
- "start_date, due_date, progress, created_at, updated_at",
202
- )
203
- .eq("user_id", userId)
204
- .order("updated_at", { ascending: false });
205
-
206
- if (params.space_id) query = query.eq("space_id", params.space_id as string);
207
- if (params.folder_id) query = query.eq("folder_id", params.folder_id as string);
208
- if (params.agent_id) query = query.eq("agent_id", params.agent_id as string);
209
- if (params.status) query = query.eq("status", params.status as string);
210
- query = query.limit((params.limit as number) || 50);
211
-
212
- const { data, error } = await query;
213
- if (error) return err(error.message);
214
- return ok({ tasks: data || [], count: (data || []).length });
215
- } catch (e) {
216
- return err(e instanceof Error ? e.message : String(e));
272
+ const action = params.action as string;
273
+
274
+ switch (action) {
275
+ case "list":
276
+ return handleListTasks(supabase, userId, params);
277
+ case "create":
278
+ return handleCreateTask(supabase, userId, resolveAgent, params);
279
+ case "update":
280
+ return handleUpdateTask(supabase, userId, params);
281
+ case "delete":
282
+ return handleDeleteTask(supabase, userId, params);
283
+ default:
284
+ return err(
285
+ `Unknown action "${action}". Valid actions: list, create, update, delete`,
286
+ );
217
287
  }
218
288
  },
219
289
  });
290
+ }
220
291
 
221
- // ── OFIERE_CREATE_TASK Optional (has side effects: writes to DB) ───
222
-
223
- api.registerTool(
224
- {
225
- name: "OFIERE_CREATE_TASK",
226
- label: "Create Ofiere Task",
227
- description:
228
- "Create a new task in the Ofiere PM dashboard. " +
229
- "IMPORTANT: You MUST always pass your own name as agent_id (e.g. 'ivy', 'daisy') to assign the task to yourself. " +
230
- "If you want to assign to a different agent, pass their name instead. " +
231
- "Pass agent_id as 'none' or 'unassigned' to create an unassigned task. " +
232
- "The task will appear in the dashboard immediately via real-time sync.",
233
- parameters: {
234
- type: "object",
235
- required: ["title", "agent_id"],
236
- properties: {
237
- title: { type: "string", description: "Task title (required)" },
238
- description: { type: "string", description: "Task description" },
239
- agent_id: {
240
- type: "string",
241
- description:
242
- "REQUIRED. Your own agent name (e.g. 'ivy', 'daisy', 'celia') to self-assign, " +
243
- "or another agent's name to assign to them. " +
244
- "Pass 'none' or 'unassigned' to create a task with no assignee.",
245
- },
246
- status: {
247
- type: "string",
248
- description: "Initial status (default: PENDING)",
249
- enum: ["PENDING", "IN_PROGRESS", "DONE", "FAILED"],
250
- },
251
- priority: {
252
- type: "number",
253
- description: "Priority: 0=LOW, 1=MEDIUM, 2=HIGH, 3=CRITICAL (default: 1)",
254
- },
255
- space_id: { type: "string", description: "PM Space ID to place the task in" },
256
- folder_id: { type: "string", description: "PM Folder ID to place the task in" },
257
- start_date: { type: "string", description: "Start date (ISO 8601 format)" },
258
- due_date: { type: "string", description: "Due date (ISO 8601 format)" },
259
- tags: {
260
- type: "array",
261
- items: { type: "string" },
262
- description: "Tags for the task",
263
- },
292
+ // ── Task action handlers ─────────────────────────────────────────────────────
293
+
294
+ async function handleListTasks(
295
+ supabase: SupabaseClient,
296
+ userId: string,
297
+ params: Record<string, unknown>,
298
+ ): Promise<ToolResult> {
299
+ try {
300
+ let query = supabase
301
+ .from("tasks")
302
+ .select(
303
+ "id, title, description, status, priority, agent_id, space_id, folder_id, " +
304
+ "start_date, due_date, progress, tags, custom_fields, created_at, updated_at",
305
+ )
306
+ .eq("user_id", userId)
307
+ .order("updated_at", { ascending: false });
308
+
309
+ if (params.space_id) query = query.eq("space_id", params.space_id as string);
310
+ if (params.folder_id) query = query.eq("folder_id", params.folder_id as string);
311
+ if (params.agent_id) query = query.eq("agent_id", params.agent_id as string);
312
+ if (params.status) query = query.eq("status", params.status as string);
313
+ query = query.limit((params.limit as number) || 50);
314
+
315
+ const { data, error } = await query;
316
+ if (error) return err(error.message);
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 });
332
+ } catch (e) {
333
+ return err(e instanceof Error ? e.message : String(e));
334
+ }
335
+ }
336
+
337
+ async function handleCreateTask(
338
+ supabase: SupabaseClient,
339
+ userId: string,
340
+ resolveAgent: (id?: string) => Promise<string | null>,
341
+ params: Record<string, unknown>,
342
+ ): Promise<ToolResult> {
343
+ try {
344
+ if (!params.title) return err("Missing required field: title");
345
+
346
+ const id = `task-${Date.now()}`;
347
+ const now = new Date().toISOString();
348
+
349
+ // Handle explicit "none"/"unassigned"
350
+ const rawAgentId = params.agent_id as string | undefined;
351
+ const isUnassigned =
352
+ rawAgentId &&
353
+ ["none", "unassigned", "null", ""].includes(rawAgentId.toLowerCase().trim());
354
+
355
+ const assignee = isUnassigned ? null : await resolveAgent(rawAgentId);
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
+
387
+ const insertData: Record<string, unknown> = {
388
+ id,
389
+ user_id: userId,
390
+ title: params.title,
391
+ description: (params.description as string) || (params.instructions as string) || null,
392
+ agent_id: assignee,
393
+ assignee_type: "agent",
394
+ status: (params.status as string) || "PENDING",
395
+ priority: params.priority !== undefined ? params.priority : 1,
396
+ space_id: (params.space_id as string) || null,
397
+ folder_id: (params.folder_id as string) || null,
398
+ start_date: (params.start_date as string) || null,
399
+ due_date: (params.due_date as string) || null,
400
+ tags: (params.tags as string[]) || [],
401
+ progress: 0,
402
+ sort_order: 0,
403
+ custom_fields: Object.keys(cf).length > 0 ? cf : {},
404
+ created_at: now,
405
+ updated_at: now,
406
+ };
407
+
408
+ const { error } = await supabase.from("tasks").insert(insertData);
409
+
410
+ if (error) {
411
+ if (error.message?.includes("agent_id") || error.message?.includes("foreign key")) {
412
+ insertData.agent_id = null;
413
+ const retry = await supabase.from("tasks").insert(insertData);
414
+ if (retry.error) return err(retry.error.message);
415
+ return ok({
416
+ id,
417
+ message: `Task "${params.title}" created (agent_id "${assignee}" was invalid, assigned to none)`,
418
+ task: insertData,
419
+ });
420
+ }
421
+ return err(error.message);
422
+ }
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
+
431
+ return ok({
432
+ id,
433
+ message: `Task "${params.title}" created and assigned to ${assignee || "no one"}${extrasStr}`,
434
+ task: insertData,
435
+ });
436
+ } catch (e) {
437
+ return err(e instanceof Error ? e.message : String(e));
438
+ }
439
+ }
440
+
441
+ async function handleUpdateTask(
442
+ supabase: SupabaseClient,
443
+ userId: string,
444
+ params: Record<string, unknown>,
445
+ ): Promise<ToolResult> {
446
+ try {
447
+ if (!params.task_id) return err("Missing required field: task_id");
448
+
449
+ const updates: Record<string, unknown> = { updated_at: new Date().toISOString() };
450
+ const fields = [
451
+ "title", "description", "status", "priority", "progress",
452
+ "agent_id", "start_date", "due_date", "tags",
453
+ ];
454
+ for (const f of fields) {
455
+ if (params[f] !== undefined) updates[f] = params[f];
456
+ }
457
+ if (params.status === "DONE") updates.completed_at = new Date().toISOString();
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
+
514
+ const { data, error } = await supabase
515
+ .from("tasks")
516
+ .update(updates)
517
+ .eq("id", params.task_id as string)
518
+ .eq("user_id", userId)
519
+ .select("id, title, status, priority, agent_id")
520
+ .single();
521
+
522
+ if (error) return err(error.message);
523
+ return ok({ message: `Task "${data?.title}" updated`, task: data });
524
+ } catch (e) {
525
+ return err(e instanceof Error ? e.message : String(e));
526
+ }
527
+ }
528
+
529
+ async function handleDeleteTask(
530
+ supabase: SupabaseClient,
531
+ userId: string,
532
+ params: Record<string, unknown>,
533
+ ): Promise<ToolResult> {
534
+ try {
535
+ if (!params.task_id) return err("Missing required field: task_id");
536
+ const taskId = params.task_id as string;
537
+
538
+ await supabase.from("scheduler_events").delete().eq("task_id", taskId);
539
+
540
+ const { data: subtasks } = await supabase
541
+ .from("tasks")
542
+ .select("id")
543
+ .eq("parent_task_id", taskId)
544
+ .eq("user_id", userId);
545
+
546
+ if (subtasks && subtasks.length > 0) {
547
+ for (const sub of subtasks) {
548
+ await supabase.from("scheduler_events").delete().eq("task_id", sub.id);
549
+ }
550
+ await supabase
551
+ .from("tasks")
552
+ .delete()
553
+ .in("id", subtasks.map((s: { id: string }) => s.id))
554
+ .eq("user_id", userId);
555
+ }
556
+
557
+ const { error } = await supabase
558
+ .from("tasks")
559
+ .delete()
560
+ .eq("id", taskId)
561
+ .eq("user_id", userId);
562
+
563
+ if (error) return err(error.message);
564
+ return ok({ message: `Task ${taskId} deleted`, deleted: true });
565
+ } catch (e) {
566
+ return err(e instanceof Error ? e.message : String(e));
567
+ }
568
+ }
569
+
570
+ // ═══════════════════════════════════════════════════════════════════════════════
571
+ // META-TOOL 2: OFIERE_AGENT_OPS — Agent Management
572
+ // ═══════════════════════════════════════════════════════════════════════════════
573
+
574
+ function registerAgentOps(
575
+ api: any,
576
+ supabase: SupabaseClient,
577
+ userId: string,
578
+ fallbackAgentId: string,
579
+ ): void {
580
+ api.registerTool({
581
+ name: "OFIERE_AGENT_OPS",
582
+ label: "Ofiere Agent Operations",
583
+ description:
584
+ `Query agents in the Ofiere PM system.\n\n` +
585
+ `Actions:\n` +
586
+ `- "list": List all available agents with their IDs, names, roles, and status. Use this to find the correct agent_id for task assignment.`,
587
+ parameters: {
588
+ type: "object",
589
+ required: ["action"],
590
+ properties: {
591
+ action: {
592
+ type: "string",
593
+ description: "The operation to perform",
594
+ enum: ["list"],
264
595
  },
265
596
  },
266
- async execute(_id: string, params: Record<string, unknown>) {
267
- try {
268
- if (!params.title) return err("Missing required field: title");
597
+ },
598
+ async execute(_id: string, params: Record<string, unknown>) {
599
+ const action = params.action as string;
269
600
 
270
- const id = `task-${Date.now()}`;
271
- const now = new Date().toISOString();
601
+ switch (action) {
602
+ case "list":
603
+ return handleListAgents(api, supabase, userId, fallbackAgentId);
604
+ default:
605
+ return err(`Unknown action "${action}". Valid actions: list`);
606
+ }
607
+ },
608
+ });
609
+ }
610
+
611
+ async function handleListAgents(
612
+ api: any,
613
+ supabase: SupabaseClient,
614
+ userId: string,
615
+ fallbackAgentId: string,
616
+ ): Promise<ToolResult> {
617
+ try {
618
+ // Resolve calling agent's ID for the "your_agent_id" hint
619
+ const callerName = getCallingAgentName(api);
620
+ let yourAgentId = fallbackAgentId || "";
621
+ if (callerName && !yourAgentId) {
622
+ try {
623
+ yourAgentId = await resolveAgentId(callerName, userId, supabase);
624
+ } catch { /* ignore */ }
625
+ }
626
+
627
+ const { data, error } = await supabase
628
+ .from("agents")
629
+ .select("id, name, codename, role, status")
630
+ .eq("user_id", userId)
631
+ .order("name");
632
+
633
+ if (error) return err(error.message);
634
+ return ok({
635
+ agents: data || [],
636
+ count: (data || []).length,
637
+ your_agent_id: yourAgentId,
638
+ });
639
+ } catch (e) {
640
+ return err(e instanceof Error ? e.message : String(e));
641
+ }
642
+ }
272
643
 
273
- // Handle explicit "none"/"unassigned"
274
- const rawAgentId = params.agent_id as string | undefined;
275
- const isUnassigned =
276
- rawAgentId &&
277
- ["none", "unassigned", "null", ""].includes(rawAgentId.toLowerCase().trim());
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
+ }
278
816
 
279
- const assignee = isUnassigned ? null : await resolveAgent(rawAgentId);
817
+ // ═══════════════════════════════════════════════════════════════════════════════
818
+ // META-TOOL 4: OFIERE_SCHEDULE_OPS — Calendar & Scheduler Events
819
+ // ═══════════════════════════════════════════════════════════════════════════════
280
820
 
281
- const insertData: Record<string, unknown> = {
282
- id,
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,
283
881
  user_id: userId,
882
+ task_id: (params.task_id as string) || null,
883
+ agent_id: (params.agent_id as string) || null,
284
884
  title: params.title,
285
885
  description: (params.description as string) || null,
286
- agent_id: assignee,
287
- assignee_type: "agent",
288
- status: (params.status as string) || "PENDING",
289
- priority: params.priority !== undefined ? params.priority : 1,
290
- space_id: (params.space_id as string) || null,
291
- folder_id: (params.folder_id as string) || null,
292
- start_date: (params.start_date as string) || null,
293
- due_date: (params.due_date as string) || null,
294
- tags: (params.tags as string[]) || [],
295
- progress: 0,
296
- sort_order: 0,
297
- custom_fields: {},
298
- created_at: now,
299
- updated_at: now,
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,
300
895
  };
301
-
302
- const { error } = await supabase.from("tasks").insert(insertData);
303
-
304
- if (error) {
305
- if (error.message?.includes("agent_id") || error.message?.includes("foreign key")) {
306
- insertData.agent_id = null;
307
- const retry = await supabase.from("tasks").insert(insertData);
308
- if (retry.error) return err(retry.error.message);
309
- return ok({
310
- id,
311
- message: `Task "${params.title}" created (agent_id "${assignee}" was invalid, assigned to none)`,
312
- task: insertData,
313
- });
314
- }
315
- return err(error.message);
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];
316
906
  }
317
-
318
- return ok({
319
- id,
320
- message: `Task "${params.title}" created and assigned to ${assignee || "no one"}`,
321
- task: insertData,
322
- });
323
- } catch (e) {
324
- return err(e instanceof Error ? e.message : String(e));
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 });
325
910
  }
326
- },
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
+ }
327
920
  },
328
- );
329
-
330
- // ── OFIERE_UPDATE_TASK — Optional (has side effects) ─────────────────
331
-
332
- api.registerTool(
333
- {
334
- name: "OFIERE_UPDATE_TASK",
335
- label: "Update Ofiere Task",
336
- description:
337
- "Update an existing task in the Ofiere PM dashboard. Only provided fields are changed. " +
338
- "Changes appear in the dashboard immediately via real-time sync.",
339
- parameters: {
340
- type: "object",
341
- required: ["task_id"],
342
- properties: {
343
- task_id: { type: "string", description: "The task ID to update (required)" },
344
- title: { type: "string", description: "New title" },
345
- description: { type: "string", description: "New description" },
346
- status: {
347
- type: "string",
348
- description: "New status",
349
- enum: ["PENDING", "IN_PROGRESS", "DONE", "FAILED"],
350
- },
351
- priority: { type: "number", description: "New priority (0-3)" },
352
- progress: { type: "number", description: "Progress percentage (0-100)" },
353
- agent_id: { type: "string", description: "Reassign to a different agent" },
354
- start_date: { type: "string", description: "New start date (ISO 8601)" },
355
- due_date: { type: "string", description: "New due date (ISO 8601)" },
356
- tags: {
357
- type: "array",
358
- items: { type: "string" },
359
- description: "New tags",
360
- },
361
- },
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" },
362
962
  },
363
- async execute(_id: string, params: Record<string, unknown>) {
364
- try {
365
- if (!params.task_id) return err("Missing required field: task_id");
366
-
367
- const updates: Record<string, unknown> = { updated_at: new Date().toISOString() };
368
- const fields = [
369
- "title", "description", "status", "priority", "progress",
370
- "agent_id", "start_date", "due_date", "tags",
371
- ];
372
- for (const f of fields) {
373
- if (params[f] !== undefined) updates[f] = params[f];
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}`);
374
992
  }
375
- if (params.status === "DONE") updates.completed_at = new Date().toISOString();
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
+ }
376
1039
 
377
- const { data, error } = await supabase
378
- .from("tasks")
379
- .update(updates)
380
- .eq("id", params.task_id as string)
381
- .eq("user_id", userId)
382
- .select("id, title, status, priority, agent_id")
383
- .single();
1040
+ // ═══════════════════════════════════════════════════════════════════════════════
1041
+ // META-TOOL 6: OFIERE_WORKFLOW_OPS — Workflow Management & Execution
1042
+ // ═══════════════════════════════════════════════════════════════════════════════
384
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;
385
1082
  if (error) return err(error.message);
386
- return ok({ message: `Task "${data?.title}" updated`, task: data });
387
- } catch (e) {
388
- return err(e instanceof Error ? e.message : String(e));
1083
+ return ok({ workflows: data || [], count: (data || []).length });
389
1084
  }
390
- },
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
+ }
391
1137
  },
392
- );
393
-
394
- // ── OFIERE_DELETE_TASK — Optional (destructive side effect) ──────────
395
-
396
- api.registerTool(
397
- {
398
- name: "OFIERE_DELETE_TASK",
399
- label: "Delete Ofiere Task",
400
- description:
401
- "Delete a task from the Ofiere PM dashboard. Also removes subtasks and linked scheduler events.",
402
- parameters: {
403
- type: "object",
404
- required: ["task_id"],
405
- properties: {
406
- task_id: { type: "string", description: "The task ID to delete (required)" },
407
- },
408
- },
409
- async execute(_id: string, params: Record<string, unknown>) {
410
- try {
411
- if (!params.task_id) return err("Missing required field: task_id");
412
- const taskId = params.task_id as string;
413
-
414
- await supabase.from("scheduler_events").delete().eq("task_id", taskId);
415
-
416
- const { data: subtasks } = await supabase
417
- .from("tasks")
418
- .select("id")
419
- .eq("parent_task_id", taskId)
420
- .eq("user_id", userId);
421
-
422
- if (subtasks && subtasks.length > 0) {
423
- for (const sub of subtasks) {
424
- await supabase.from("scheduler_events").delete().eq("task_id", sub.id);
425
- }
426
- await supabase
427
- .from("tasks")
428
- .delete()
429
- .in("id", subtasks.map((s: { id: string }) => s.id))
430
- .eq("user_id", userId);
431
- }
1138
+ });
1139
+ }
432
1140
 
433
- const { error } = await supabase
434
- .from("tasks")
435
- .delete()
436
- .eq("id", taskId)
437
- .eq("user_id", userId);
1141
+ // ═══════════════════════════════════════════════════════════════════════════════
1142
+ // META-TOOL 7: OFIERE_NOTIFY_OPS — Notifications
1143
+ // ═══════════════════════════════════════════════════════════════════════════════
438
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);
439
1197
  if (error) return err(error.message);
440
- return ok({ message: `Task ${taskId} deleted`, deleted: true });
441
- } catch (e) {
442
- return err(e instanceof Error ? e.message : String(e));
1198
+ return ok({ message: "Notification deleted", ok: true });
443
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" },
444
1240
  },
445
1241
  },
446
- );
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
+ }
447
1310
 
448
- // ── OFIERE_LIST_AGENTS — Required (read-only, no side effects) ───────
1311
+ // ═══════════════════════════════════════════════════════════════════════════════
1312
+ // META-TOOL 9: OFIERE_PROMPT_OPS — System Prompt Chunk Management
1313
+ // ═══════════════════════════════════════════════════════════════════════════════
449
1314
 
1315
+ function registerPromptOps(
1316
+ api: any,
1317
+ supabase: SupabaseClient,
1318
+ userId: string,
1319
+ ): void {
450
1320
  api.registerTool({
451
- name: "OFIERE_LIST_AGENTS",
452
- label: "List Ofiere Agents",
1321
+ name: "OFIERE_PROMPT_OPS",
1322
+ label: "Ofiere Prompt Operations",
453
1323
  description:
454
- "List all available agents in the Ofiere system. " +
455
- "Shows agent IDs, names, roles, and current status. " +
456
- "Use this to find the right agent_id for task assignment.",
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`,
457
1331
  parameters: {
458
1332
  type: "object",
459
- properties: {},
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
+ },
460
1343
  },
461
- async execute(_id: string, _params: Record<string, unknown>) {
462
- try {
463
- // Resolve calling agent's ID for the "your_agent_id" hint
464
- const callerName = getCallingAgentName(api);
465
- let yourAgentId = fallbackAgentId || "";
466
- if (callerName && !yourAgentId) {
467
- try {
468
- yourAgentId = await resolveAgentId(callerName, userId, supabase);
469
- } catch { /* ignore */ }
470
- }
471
-
472
- const { data, error } = await supabase
473
- .from("agents")
474
- .select("id, name, codename, role, status")
475
- .eq("user_id", userId)
476
- .order("name");
477
-
478
- if (error) return err(error.message);
479
- return ok({
480
- agents: data || [],
481
- count: (data || []).length,
482
- your_agent_id: yourAgentId,
483
- });
484
- } catch (e) {
485
- return err(e instanceof Error ? e.message : String(e));
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}".`);
486
1396
  }
487
1397
  },
488
1398
  });
1399
+ }
1400
+
1401
+ // ═══════════════════════════════════════════════════════════════════════════════
1402
+ // Public: Register All Meta-Tools
1403
+ // ═══════════════════════════════════════════════════════════════════════════════
1404
+ // This is the single entry point called by index.ts.
1405
+ // Returns the number of tools registered for dynamic prompt generation.
489
1406
 
1407
+ export function registerTools(
1408
+ api: any, // OpenClawPluginApi — typed as any to avoid import-path issues at install time
1409
+ supabase: SupabaseClient,
1410
+ config: OfiereConfig,
1411
+ ): number {
1412
+ const userId = config.userId;
1413
+ const fallbackAgentId = config.agentId; // May be empty — that's fine
1414
+
1415
+ const resolveAgent = createAgentResolver(api, supabase, userId, fallbackAgentId);
1416
+
1417
+ // ── Register each domain meta-tool ──
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
1427
+
1428
+ // ── Count and log ──
1429
+ const toolCount = 9;
490
1430
  const callerName = getCallingAgentName(api);
491
1431
  const agentLabel = fallbackAgentId || callerName || "auto-detect";
492
- api.logger.info(`[ofiere] 5 tools registered (agent: ${agentLabel})`);
1432
+ api.logger.info(`[ofiere] ${toolCount} meta-tools registered (agent: ${agentLabel})`);
1433
+
1434
+ return toolCount;
493
1435
  }