ofiere-openclaw-plugin 2.0.0 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -36
- package/package.json +2 -2
- package/src/prompt.ts +60 -19
- package/src/tools.ts +1190 -56
package/src/tools.ts
CHANGED
|
@@ -106,6 +106,17 @@ export function probeApiForAgentName(api: any, logger?: any): string {
|
|
|
106
106
|
|
|
107
107
|
// ─── Shared: Agent ID Resolution ─────────────────────────────────────────────
|
|
108
108
|
|
|
109
|
+
// System/plugin names that should never be treated as real agent identifiers.
|
|
110
|
+
// These come from the OpenClaw gateway registration context, not from actual agents.
|
|
111
|
+
const SYSTEM_NAME_BLOCKLIST = new Set([
|
|
112
|
+
"ofiere pm", "ofiere", "openclaw", "system", "plugin", "gateway", "admin",
|
|
113
|
+
"ofiere pm plugin", "ofiere-openclaw-plugin",
|
|
114
|
+
]);
|
|
115
|
+
|
|
116
|
+
function isSystemName(name: string): boolean {
|
|
117
|
+
return SYSTEM_NAME_BLOCKLIST.has(name.toLowerCase().trim());
|
|
118
|
+
}
|
|
119
|
+
|
|
109
120
|
function createAgentResolver(
|
|
110
121
|
api: any,
|
|
111
122
|
supabase: SupabaseClient,
|
|
@@ -114,47 +125,38 @@ function createAgentResolver(
|
|
|
114
125
|
) {
|
|
115
126
|
/**
|
|
116
127
|
* Resolve the agent ID for the calling agent.
|
|
117
|
-
* Priority: explicit param >
|
|
128
|
+
* Priority: explicit param > env var > DB fallback
|
|
129
|
+
*
|
|
130
|
+
* NOTE: We intentionally skip runtime/registration detection because the
|
|
131
|
+
* OpenClaw api object returns the PLUGIN name ("Ofiere PM"), not the
|
|
132
|
+
* calling agent's name. Each agent must pass its own name via agent_id.
|
|
118
133
|
*/
|
|
119
134
|
return async function resolveAgent(explicitId?: string): Promise<string | null> {
|
|
120
|
-
// 1. Explicit agent_id passed by the LLM (e.g. "ivy", "
|
|
135
|
+
// 1. Explicit agent_id passed by the LLM (e.g. "ivy", "celia", or a UUID)
|
|
121
136
|
if (explicitId && explicitId.trim()) {
|
|
122
137
|
const trimmed = explicitId.trim();
|
|
123
|
-
// If it looks like a UUID or our ID format, use directly
|
|
124
|
-
if (trimmed.match(/^[0-9a-f]{8}-/) || trimmed.match(/^agent-/)) {
|
|
125
|
-
return trimmed;
|
|
126
|
-
}
|
|
127
|
-
// Otherwise treat as a name and resolve to the actual agent ID
|
|
128
|
-
try {
|
|
129
|
-
return await resolveAgentId(trimmed, userId, supabase);
|
|
130
|
-
} catch {
|
|
131
|
-
return trimmed; // fallback: use as-is
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
138
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
} catch {
|
|
150
|
-
// Fall through
|
|
139
|
+
// Block system names from being used as agent IDs
|
|
140
|
+
if (isSystemName(trimmed)) {
|
|
141
|
+
// Fall through to DB fallback
|
|
142
|
+
} else if (trimmed.match(/^[0-9a-f]{8}-/) || trimmed.match(/^agent-/)) {
|
|
143
|
+
// Looks like a UUID or our ID format — use directly
|
|
144
|
+
return trimmed;
|
|
145
|
+
} else {
|
|
146
|
+
// Treat as a name and resolve to the actual agent ID
|
|
147
|
+
try {
|
|
148
|
+
const resolved = await resolveAgentId(trimmed, userId, supabase);
|
|
149
|
+
if (resolved && !isSystemName(resolved)) return resolved;
|
|
150
|
+
} catch {
|
|
151
|
+
// Fall through
|
|
152
|
+
}
|
|
151
153
|
}
|
|
152
154
|
}
|
|
153
155
|
|
|
154
|
-
//
|
|
156
|
+
// 2. Env var fallback (OFIERE_AGENT_ID — legacy single-agent mode)
|
|
155
157
|
if (fallbackAgentId) return fallbackAgentId;
|
|
156
158
|
|
|
157
|
-
//
|
|
159
|
+
// 3. Nuclear fallback: query the FIRST agent for this user
|
|
158
160
|
try {
|
|
159
161
|
const { data } = await supabase
|
|
160
162
|
.from("agents")
|
|
@@ -172,7 +174,9 @@ function createAgentResolver(
|
|
|
172
174
|
};
|
|
173
175
|
}
|
|
174
176
|
|
|
175
|
-
//
|
|
177
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
178
|
+
// META-TOOL 1: OFIERE_TASK_OPS — Task Management
|
|
179
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
176
180
|
|
|
177
181
|
function registerTaskOps(
|
|
178
182
|
api: any,
|
|
@@ -186,13 +190,14 @@ function registerTaskOps(
|
|
|
186
190
|
description:
|
|
187
191
|
`Manage tasks in the Ofiere PM dashboard. All task operations go through this tool.\n\n` +
|
|
188
192
|
`Actions:\n` +
|
|
189
|
-
`- "list": List/filter tasks. Optional
|
|
190
|
-
`- "create": Create a
|
|
191
|
-
`- "update": Update
|
|
192
|
-
`- "delete": Delete
|
|
193
|
-
`
|
|
194
|
-
`
|
|
195
|
-
`
|
|
193
|
+
`- "list": List/filter tasks. Optional: status, agent_id, space_id, folder_id, limit\n` +
|
|
194
|
+
`- "create": Create a task. Required: title. Optional: agent_id, description, status, priority, space_id, folder_id, start_date, due_date, tags, instructions, execution_plan, goals, constraints, system_prompt\n` +
|
|
195
|
+
`- "update": Update a task. Required: task_id. Optional: all create fields + progress\n` +
|
|
196
|
+
`- "delete": Delete task + subtasks. Required: task_id\n\n` +
|
|
197
|
+
`For complex tasks, fill in execution_plan (step-by-step plan), goals, constraints, and system_prompt to help the executing agent.\n` +
|
|
198
|
+
`For simple tasks, just provide title and optionally description.\n` +
|
|
199
|
+
`agent_id: Pass your name to self-assign, another agent's name, or 'none'.\n` +
|
|
200
|
+
`Status: PENDING, IN_PROGRESS, DONE, FAILED | Priority: 0=LOW, 1=MEDIUM, 2=HIGH, 3=CRITICAL`,
|
|
196
201
|
parameters: {
|
|
197
202
|
type: "object",
|
|
198
203
|
required: ["action"],
|
|
@@ -202,14 +207,13 @@ function registerTaskOps(
|
|
|
202
207
|
description: "The operation to perform",
|
|
203
208
|
enum: ["list", "create", "update", "delete"],
|
|
204
209
|
},
|
|
205
|
-
// ── Shared / contextual params ──
|
|
206
210
|
task_id: { type: "string", description: "Task ID (required for update, delete)" },
|
|
207
211
|
title: { type: "string", description: "Task title (required for create)" },
|
|
208
212
|
description: { type: "string", description: "Task description" },
|
|
213
|
+
instructions: { type: "string", description: "Detailed instructions for the agent executing this task" },
|
|
209
214
|
agent_id: {
|
|
210
215
|
type: "string",
|
|
211
|
-
description:
|
|
212
|
-
"Agent name or ID. For create: your name to self-assign, another name to delegate, 'none' for unassigned. For list: filter by agent.",
|
|
216
|
+
description: "Agent name or ID. Your name to self-assign, 'none' for unassigned.",
|
|
213
217
|
},
|
|
214
218
|
status: {
|
|
215
219
|
type: "string",
|
|
@@ -227,6 +231,42 @@ function registerTaskOps(
|
|
|
227
231
|
items: { type: "string" },
|
|
228
232
|
description: "Tags for the task",
|
|
229
233
|
},
|
|
234
|
+
execution_plan: {
|
|
235
|
+
type: "array",
|
|
236
|
+
items: {
|
|
237
|
+
type: "object",
|
|
238
|
+
properties: {
|
|
239
|
+
text: { type: "string", description: "Step description" },
|
|
240
|
+
},
|
|
241
|
+
required: ["text"],
|
|
242
|
+
},
|
|
243
|
+
description: "Ordered execution steps for complex tasks. Each step: { text: '...' }",
|
|
244
|
+
},
|
|
245
|
+
goals: {
|
|
246
|
+
type: "array",
|
|
247
|
+
items: {
|
|
248
|
+
type: "object",
|
|
249
|
+
properties: {
|
|
250
|
+
type: { type: "string", enum: ["budget", "stack", "legal", "deadline", "custom"], description: "Goal category" },
|
|
251
|
+
label: { type: "string", description: "Goal description" },
|
|
252
|
+
},
|
|
253
|
+
required: ["label"],
|
|
254
|
+
},
|
|
255
|
+
description: "Task goals. Each: { type?: 'budget'|'stack'|'legal'|'deadline'|'custom', label: '...' }",
|
|
256
|
+
},
|
|
257
|
+
constraints: {
|
|
258
|
+
type: "array",
|
|
259
|
+
items: {
|
|
260
|
+
type: "object",
|
|
261
|
+
properties: {
|
|
262
|
+
type: { type: "string", enum: ["budget", "stack", "legal", "deadline", "custom"], description: "Constraint category" },
|
|
263
|
+
label: { type: "string", description: "Constraint description" },
|
|
264
|
+
},
|
|
265
|
+
required: ["label"],
|
|
266
|
+
},
|
|
267
|
+
description: "Task constraints. Each: { type?: 'budget'|'stack'|'legal'|'deadline'|'custom', label: '...' }",
|
|
268
|
+
},
|
|
269
|
+
system_prompt: { type: "string", description: "Custom system prompt injection for the executing agent" },
|
|
230
270
|
limit: { type: "number", description: "Max results for list (default 50)" },
|
|
231
271
|
},
|
|
232
272
|
},
|
|
@@ -263,7 +303,7 @@ async function handleListTasks(
|
|
|
263
303
|
.from("tasks")
|
|
264
304
|
.select(
|
|
265
305
|
"id, title, description, status, priority, agent_id, space_id, folder_id, " +
|
|
266
|
-
"start_date, due_date, progress, created_at, updated_at",
|
|
306
|
+
"start_date, due_date, progress, tags, custom_fields, created_at, updated_at",
|
|
267
307
|
)
|
|
268
308
|
.eq("user_id", userId)
|
|
269
309
|
.order("updated_at", { ascending: false });
|
|
@@ -276,7 +316,21 @@ async function handleListTasks(
|
|
|
276
316
|
|
|
277
317
|
const { data, error } = await query;
|
|
278
318
|
if (error) return err(error.message);
|
|
279
|
-
|
|
319
|
+
|
|
320
|
+
// Unpack custom_fields for readability
|
|
321
|
+
const tasks = (data || []).map((t: any) => {
|
|
322
|
+
const cf = t.custom_fields || {};
|
|
323
|
+
return {
|
|
324
|
+
...t,
|
|
325
|
+
execution_plan: cf.execution_plan || undefined,
|
|
326
|
+
goals: cf.goals || undefined,
|
|
327
|
+
constraints: cf.constraints || undefined,
|
|
328
|
+
system_prompt: cf.system_prompt || undefined,
|
|
329
|
+
instructions: cf.instructions || t.description || undefined,
|
|
330
|
+
};
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
return ok({ tasks, count: tasks.length });
|
|
280
334
|
} catch (e) {
|
|
281
335
|
return err(e instanceof Error ? e.message : String(e));
|
|
282
336
|
}
|
|
@@ -291,7 +345,7 @@ async function handleCreateTask(
|
|
|
291
345
|
try {
|
|
292
346
|
if (!params.title) return err("Missing required field: title");
|
|
293
347
|
|
|
294
|
-
const id = `task-${Date.now()}`;
|
|
348
|
+
const id = `task-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
295
349
|
const now = new Date().toISOString();
|
|
296
350
|
|
|
297
351
|
// Handle explicit "none"/"unassigned"
|
|
@@ -302,11 +356,41 @@ async function handleCreateTask(
|
|
|
302
356
|
|
|
303
357
|
const assignee = isUnassigned ? null : await resolveAgent(rawAgentId);
|
|
304
358
|
|
|
359
|
+
// Build custom_fields from task-ops extended fields
|
|
360
|
+
const cf: Record<string, unknown> = {};
|
|
361
|
+
|
|
362
|
+
if (params.execution_plan && Array.isArray(params.execution_plan) && (params.execution_plan as any[]).length > 0) {
|
|
363
|
+
cf.execution_plan = (params.execution_plan as any[]).map((step: any, i: number) => ({
|
|
364
|
+
id: `step-${Date.now()}-${i}`,
|
|
365
|
+
text: typeof step === "string" ? step : step.text || String(step),
|
|
366
|
+
order: i,
|
|
367
|
+
}));
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (params.goals && Array.isArray(params.goals) && (params.goals as any[]).length > 0) {
|
|
371
|
+
cf.goals = (params.goals as any[]).map((g: any, i: number) => ({
|
|
372
|
+
id: `goal-${Date.now()}-${i}`,
|
|
373
|
+
type: g.type || "custom",
|
|
374
|
+
label: typeof g === "string" ? g : g.label || String(g),
|
|
375
|
+
}));
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (params.constraints && Array.isArray(params.constraints) && (params.constraints as any[]).length > 0) {
|
|
379
|
+
cf.constraints = (params.constraints as any[]).map((c: any, i: number) => ({
|
|
380
|
+
id: `cstr-${Date.now()}-${i}`,
|
|
381
|
+
type: c.type || "custom",
|
|
382
|
+
label: typeof c === "string" ? c : c.label || String(c),
|
|
383
|
+
}));
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (params.system_prompt) cf.system_prompt = params.system_prompt;
|
|
387
|
+
if (params.instructions) cf.instructions = params.instructions;
|
|
388
|
+
|
|
305
389
|
const insertData: Record<string, unknown> = {
|
|
306
390
|
id,
|
|
307
391
|
user_id: userId,
|
|
308
392
|
title: params.title,
|
|
309
|
-
description: (params.description as string) || null,
|
|
393
|
+
description: (params.description as string) || (params.instructions as string) || null,
|
|
310
394
|
agent_id: assignee,
|
|
311
395
|
assignee_type: "agent",
|
|
312
396
|
status: (params.status as string) || "PENDING",
|
|
@@ -318,7 +402,7 @@ async function handleCreateTask(
|
|
|
318
402
|
tags: (params.tags as string[]) || [],
|
|
319
403
|
progress: 0,
|
|
320
404
|
sort_order: 0,
|
|
321
|
-
custom_fields: {},
|
|
405
|
+
custom_fields: Object.keys(cf).length > 0 ? cf : {},
|
|
322
406
|
created_at: now,
|
|
323
407
|
updated_at: now,
|
|
324
408
|
};
|
|
@@ -339,10 +423,55 @@ async function handleCreateTask(
|
|
|
339
423
|
return err(error.message);
|
|
340
424
|
}
|
|
341
425
|
|
|
426
|
+
// ── Auto-create scheduler event if task has a start_date ──────────────
|
|
427
|
+
// This bridges the plugin → scheduler so the pg_cron task-dispatcher
|
|
428
|
+
// Edge Function picks up the task at the right time.
|
|
429
|
+
const startDate = params.start_date as string | undefined;
|
|
430
|
+
const effectiveAgentId = (insertData.agent_id as string) || assignee;
|
|
431
|
+
if (startDate && effectiveAgentId) {
|
|
432
|
+
try {
|
|
433
|
+
const scheduledTime = (params.scheduled_time as string) || "09:00"; // default 9am
|
|
434
|
+
const datePart = startDate; // YYYY-MM-DD
|
|
435
|
+
const timePart = scheduledTime; // HH:MM
|
|
436
|
+
const dt = new Date(`${datePart}T${timePart}:00`);
|
|
437
|
+
const nextRunAt = Math.floor(dt.getTime() / 1000);
|
|
438
|
+
|
|
439
|
+
await supabase.from("scheduler_events").insert({
|
|
440
|
+
id: crypto.randomUUID(),
|
|
441
|
+
user_id: userId,
|
|
442
|
+
task_id: id,
|
|
443
|
+
agent_id: effectiveAgentId,
|
|
444
|
+
title: params.title,
|
|
445
|
+
description: (params.description as string) || (params.instructions as string) || null,
|
|
446
|
+
scheduled_date: datePart,
|
|
447
|
+
scheduled_time: timePart,
|
|
448
|
+
duration_minutes: 30,
|
|
449
|
+
recurrence_type: "none",
|
|
450
|
+
recurrence_interval: 1,
|
|
451
|
+
status: "scheduled",
|
|
452
|
+
next_run_at: nextRunAt,
|
|
453
|
+
run_count: 0,
|
|
454
|
+
priority: params.priority !== undefined ? params.priority : 1,
|
|
455
|
+
});
|
|
456
|
+
} catch (schedErr) {
|
|
457
|
+
// Non-fatal: task was created, just the scheduler event failed
|
|
458
|
+
console.error("[ofiere] Failed to auto-create scheduler event:", schedErr);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const extras = [];
|
|
463
|
+
if (cf.execution_plan) extras.push(`${(cf.execution_plan as any[]).length} execution steps`);
|
|
464
|
+
if (cf.goals) extras.push(`${(cf.goals as any[]).length} goals`);
|
|
465
|
+
if (cf.constraints) extras.push(`${(cf.constraints as any[]).length} constraints`);
|
|
466
|
+
if (cf.system_prompt) extras.push("custom system prompt");
|
|
467
|
+
if (startDate) extras.push(`scheduled for ${startDate}`);
|
|
468
|
+
const extrasStr = extras.length > 0 ? ` with ${extras.join(", ")}` : "";
|
|
469
|
+
|
|
342
470
|
return ok({
|
|
343
471
|
id,
|
|
344
|
-
message: `Task "${params.title}" created and assigned to ${assignee || "no one"}`,
|
|
472
|
+
message: `Task "${params.title}" created and assigned to ${assignee || "no one"}${extrasStr}`,
|
|
345
473
|
task: insertData,
|
|
474
|
+
scheduledExecution: startDate ? `Will auto-execute on ${startDate}` : undefined,
|
|
346
475
|
});
|
|
347
476
|
} catch (e) {
|
|
348
477
|
return err(e instanceof Error ? e.message : String(e));
|
|
@@ -367,12 +496,67 @@ async function handleUpdateTask(
|
|
|
367
496
|
}
|
|
368
497
|
if (params.status === "DONE") updates.completed_at = new Date().toISOString();
|
|
369
498
|
|
|
499
|
+
// Handle custom_fields updates (execution_plan, goals, constraints, system_prompt, instructions)
|
|
500
|
+
const hasCustomFields = params.execution_plan !== undefined ||
|
|
501
|
+
params.goals !== undefined ||
|
|
502
|
+
params.constraints !== undefined ||
|
|
503
|
+
params.system_prompt !== undefined ||
|
|
504
|
+
params.instructions !== undefined;
|
|
505
|
+
|
|
506
|
+
if (hasCustomFields) {
|
|
507
|
+
// Fetch existing custom_fields to merge
|
|
508
|
+
const { data: existing } = await supabase
|
|
509
|
+
.from("tasks")
|
|
510
|
+
.select("custom_fields")
|
|
511
|
+
.eq("id", params.task_id as string)
|
|
512
|
+
.eq("user_id", userId)
|
|
513
|
+
.single();
|
|
514
|
+
|
|
515
|
+
const existingCf = (existing?.custom_fields || {}) as Record<string, any>;
|
|
516
|
+
const mergedCf = { ...existingCf };
|
|
517
|
+
|
|
518
|
+
if (params.execution_plan !== undefined) {
|
|
519
|
+
mergedCf.execution_plan = Array.isArray(params.execution_plan)
|
|
520
|
+
? (params.execution_plan as any[]).map((step: any, i: number) => ({
|
|
521
|
+
id: step.id || `step-${Date.now()}-${i}`,
|
|
522
|
+
text: typeof step === "string" ? step : step.text || String(step),
|
|
523
|
+
order: i,
|
|
524
|
+
}))
|
|
525
|
+
: [];
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (params.goals !== undefined) {
|
|
529
|
+
mergedCf.goals = Array.isArray(params.goals)
|
|
530
|
+
? (params.goals as any[]).map((g: any, i: number) => ({
|
|
531
|
+
id: g.id || `goal-${Date.now()}-${i}`,
|
|
532
|
+
type: g.type || "custom",
|
|
533
|
+
label: typeof g === "string" ? g : g.label || String(g),
|
|
534
|
+
}))
|
|
535
|
+
: [];
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (params.constraints !== undefined) {
|
|
539
|
+
mergedCf.constraints = Array.isArray(params.constraints)
|
|
540
|
+
? (params.constraints as any[]).map((c: any, i: number) => ({
|
|
541
|
+
id: c.id || `cstr-${Date.now()}-${i}`,
|
|
542
|
+
type: c.type || "custom",
|
|
543
|
+
label: typeof c === "string" ? c : c.label || String(c),
|
|
544
|
+
}))
|
|
545
|
+
: [];
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
if (params.system_prompt !== undefined) mergedCf.system_prompt = params.system_prompt;
|
|
549
|
+
if (params.instructions !== undefined) mergedCf.instructions = params.instructions;
|
|
550
|
+
|
|
551
|
+
updates.custom_fields = mergedCf;
|
|
552
|
+
}
|
|
553
|
+
|
|
370
554
|
const { data, error } = await supabase
|
|
371
555
|
.from("tasks")
|
|
372
556
|
.update(updates)
|
|
373
557
|
.eq("id", params.task_id as string)
|
|
374
558
|
.eq("user_id", userId)
|
|
375
|
-
.select("id, title, status, priority, agent_id")
|
|
559
|
+
.select("id, title, status, priority, agent_id, start_date, due_date, progress, updated_at")
|
|
376
560
|
.single();
|
|
377
561
|
|
|
378
562
|
if (error) return err(error.message);
|
|
@@ -423,7 +607,9 @@ async function handleDeleteTask(
|
|
|
423
607
|
}
|
|
424
608
|
}
|
|
425
609
|
|
|
426
|
-
//
|
|
610
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
611
|
+
// META-TOOL 2: OFIERE_AGENT_OPS — Agent Management
|
|
612
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
427
613
|
|
|
428
614
|
function registerAgentOps(
|
|
429
615
|
api: any,
|
|
@@ -495,11 +681,952 @@ async function handleListAgents(
|
|
|
495
681
|
}
|
|
496
682
|
}
|
|
497
683
|
|
|
498
|
-
//
|
|
684
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
685
|
+
// META-TOOL 3: OFIERE_PROJECT_OPS — Spaces, Folders & Dependencies
|
|
686
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
687
|
+
|
|
688
|
+
function registerProjectOps(
|
|
689
|
+
api: any,
|
|
690
|
+
supabase: SupabaseClient,
|
|
691
|
+
userId: string,
|
|
692
|
+
): void {
|
|
693
|
+
api.registerTool({
|
|
694
|
+
name: "OFIERE_PROJECT_OPS",
|
|
695
|
+
label: "Ofiere Project Operations",
|
|
696
|
+
description:
|
|
697
|
+
`Manage PM hierarchy: spaces, folders, and task dependencies.\n\n` +
|
|
698
|
+
`Actions:\n` +
|
|
699
|
+
`- "list_spaces": List all PM spaces\n` +
|
|
700
|
+
`- "create_space": Create a space. Required: name. Optional: description, icon, icon_color\n` +
|
|
701
|
+
`- "update_space": Update a space. Required: id. Optional: name, description, icon, icon_color, sort_order\n` +
|
|
702
|
+
`- "delete_space": Delete a space. Required: id\n` +
|
|
703
|
+
`- "list_folders": List folders. Optional: space_id to filter\n` +
|
|
704
|
+
`- "create_folder": Create. Required: name, space_id. Optional: parent_folder_id, folder_type\n` +
|
|
705
|
+
`- "update_folder": Update. Required: id. Optional: name, space_id, parent_folder_id, sort_order\n` +
|
|
706
|
+
`- "delete_folder": Delete. Required: id\n` +
|
|
707
|
+
`- "list_dependencies": List task dependencies. Optional: task_id\n` +
|
|
708
|
+
`- "add_dependency": Link tasks. Required: predecessor_id, successor_id. Optional: dependency_type, lag_days\n` +
|
|
709
|
+
`- "remove_dependency": Unlink. Required: dependency_id\n` +
|
|
710
|
+
`dependency_type: finish_to_start (default), start_to_start, finish_to_finish, start_to_finish`,
|
|
711
|
+
parameters: {
|
|
712
|
+
type: "object",
|
|
713
|
+
required: ["action"],
|
|
714
|
+
properties: {
|
|
715
|
+
action: {
|
|
716
|
+
type: "string",
|
|
717
|
+
description: "The operation to perform",
|
|
718
|
+
enum: ["list_spaces", "create_space", "update_space", "delete_space",
|
|
719
|
+
"list_folders", "create_folder", "update_folder", "delete_folder",
|
|
720
|
+
"list_dependencies", "add_dependency", "remove_dependency"],
|
|
721
|
+
},
|
|
722
|
+
id: { type: "string", description: "Space, folder, or dependency ID" },
|
|
723
|
+
name: { type: "string", description: "Name for space/folder" },
|
|
724
|
+
description: { type: "string", description: "Description" },
|
|
725
|
+
icon: { type: "string", description: "Emoji icon for space" },
|
|
726
|
+
icon_color: { type: "string", description: "Hex color for space icon" },
|
|
727
|
+
space_id: { type: "string", description: "Parent space ID" },
|
|
728
|
+
parent_folder_id: { type: "string", description: "Parent folder ID for nesting" },
|
|
729
|
+
folder_type: { type: "string", enum: ["folder", "project"], description: "Folder type" },
|
|
730
|
+
sort_order: { type: "number", description: "Sort order" },
|
|
731
|
+
predecessor_id: { type: "string", description: "Task that must complete first" },
|
|
732
|
+
successor_id: { type: "string", description: "Task that depends on predecessor" },
|
|
733
|
+
dependency_type: {
|
|
734
|
+
type: "string",
|
|
735
|
+
enum: ["finish_to_start", "start_to_start", "finish_to_finish", "start_to_finish"],
|
|
736
|
+
description: "Type of dependency link",
|
|
737
|
+
},
|
|
738
|
+
lag_days: { type: "number", description: "Days of lag between tasks (default 0)" },
|
|
739
|
+
task_id: { type: "string", description: "Filter dependencies by task ID" },
|
|
740
|
+
dependency_id: { type: "string", description: "Dependency ID to remove" },
|
|
741
|
+
},
|
|
742
|
+
},
|
|
743
|
+
async execute(_id: string, params: Record<string, unknown>) {
|
|
744
|
+
const action = params.action as string;
|
|
745
|
+
switch (action) {
|
|
746
|
+
// ── Spaces ──
|
|
747
|
+
case "list_spaces": {
|
|
748
|
+
const { data, error } = await supabase.from("pm_spaces").select("*").eq("user_id", userId).order("sort_order");
|
|
749
|
+
if (error) return err(error.message);
|
|
750
|
+
return ok({ spaces: data || [], count: (data || []).length });
|
|
751
|
+
}
|
|
752
|
+
case "create_space": {
|
|
753
|
+
if (!params.name) return err("Missing required: name");
|
|
754
|
+
const { data, error } = await supabase.from("pm_spaces").insert({
|
|
755
|
+
user_id: userId,
|
|
756
|
+
name: params.name,
|
|
757
|
+
description: (params.description as string) || "",
|
|
758
|
+
icon: (params.icon as string) || "📁",
|
|
759
|
+
icon_color: (params.icon_color as string) || "#FF6D29",
|
|
760
|
+
access_type: "private",
|
|
761
|
+
sort_order: (params.sort_order as number) || 0,
|
|
762
|
+
}).select().single();
|
|
763
|
+
if (error) return err(error.message);
|
|
764
|
+
return ok({ message: `Space "${params.name}" created`, space: data });
|
|
765
|
+
}
|
|
766
|
+
case "update_space": {
|
|
767
|
+
if (!params.id) return err("Missing required: id");
|
|
768
|
+
const upd: Record<string, any> = { updated_at: new Date().toISOString() };
|
|
769
|
+
for (const f of ["name", "description", "icon", "icon_color", "sort_order"]) {
|
|
770
|
+
if ((params as any)[f] !== undefined) upd[f] = (params as any)[f];
|
|
771
|
+
}
|
|
772
|
+
const { error } = await supabase.from("pm_spaces").update(upd).eq("id", params.id).eq("user_id", userId);
|
|
773
|
+
if (error) return err(error.message);
|
|
774
|
+
return ok({ message: "Space updated", ok: true });
|
|
775
|
+
}
|
|
776
|
+
case "delete_space": {
|
|
777
|
+
if (!params.id) return err("Missing required: id");
|
|
778
|
+
const { error } = await supabase.from("pm_spaces").delete().eq("id", params.id).eq("user_id", userId);
|
|
779
|
+
if (error) return err(error.message);
|
|
780
|
+
return ok({ message: "Space deleted", ok: true });
|
|
781
|
+
}
|
|
782
|
+
// ── Folders ──
|
|
783
|
+
case "list_folders": {
|
|
784
|
+
let q = supabase.from("pm_folders").select("*").eq("user_id", userId).order("sort_order");
|
|
785
|
+
if (params.space_id) q = q.eq("space_id", params.space_id as string);
|
|
786
|
+
const { data, error } = await q;
|
|
787
|
+
if (error) return err(error.message);
|
|
788
|
+
return ok({ folders: data || [], count: (data || []).length });
|
|
789
|
+
}
|
|
790
|
+
case "create_folder": {
|
|
791
|
+
if (!params.name || !params.space_id) return err("Missing required: name, space_id");
|
|
792
|
+
const { data, error } = await supabase.from("pm_folders").insert({
|
|
793
|
+
user_id: userId,
|
|
794
|
+
space_id: params.space_id,
|
|
795
|
+
parent_folder_id: (params.parent_folder_id as string) || null,
|
|
796
|
+
name: params.name,
|
|
797
|
+
description: "",
|
|
798
|
+
folder_type: (params.folder_type as string) || "folder",
|
|
799
|
+
sort_order: (params.sort_order as number) || 0,
|
|
800
|
+
}).select().single();
|
|
801
|
+
if (error) return err(error.message);
|
|
802
|
+
return ok({ message: `Folder "${params.name}" created`, folder: data });
|
|
803
|
+
}
|
|
804
|
+
case "update_folder": {
|
|
805
|
+
if (!params.id) return err("Missing required: id");
|
|
806
|
+
const upd: Record<string, any> = { updated_at: new Date().toISOString() };
|
|
807
|
+
for (const f of ["name", "description", "space_id", "parent_folder_id", "folder_type", "sort_order"]) {
|
|
808
|
+
if ((params as any)[f] !== undefined) upd[f] = (params as any)[f];
|
|
809
|
+
}
|
|
810
|
+
const { error } = await supabase.from("pm_folders").update(upd).eq("id", params.id).eq("user_id", userId);
|
|
811
|
+
if (error) return err(error.message);
|
|
812
|
+
return ok({ message: "Folder updated", ok: true });
|
|
813
|
+
}
|
|
814
|
+
case "delete_folder": {
|
|
815
|
+
if (!params.id) return err("Missing required: id");
|
|
816
|
+
const { error } = await supabase.from("pm_folders").delete().eq("id", params.id).eq("user_id", userId);
|
|
817
|
+
if (error) return err(error.message);
|
|
818
|
+
return ok({ message: "Folder deleted", ok: true });
|
|
819
|
+
}
|
|
820
|
+
// ── Dependencies ──
|
|
821
|
+
case "list_dependencies": {
|
|
822
|
+
let q = supabase.from("pm_dependencies").select("*").eq("user_id", userId);
|
|
823
|
+
if (params.task_id) {
|
|
824
|
+
q = supabase.from("pm_dependencies").select("*").eq("user_id", userId)
|
|
825
|
+
.or(`predecessor_id.eq.${params.task_id},successor_id.eq.${params.task_id}`);
|
|
826
|
+
}
|
|
827
|
+
const { data, error } = await q;
|
|
828
|
+
if (error) return err(error.message);
|
|
829
|
+
return ok({ dependencies: data || [], count: (data || []).length });
|
|
830
|
+
}
|
|
831
|
+
case "add_dependency": {
|
|
832
|
+
if (!params.predecessor_id || !params.successor_id) return err("Missing required: predecessor_id, successor_id");
|
|
833
|
+
const { data, error } = await supabase.from("pm_dependencies").insert({
|
|
834
|
+
user_id: userId,
|
|
835
|
+
predecessor_id: params.predecessor_id,
|
|
836
|
+
successor_id: params.successor_id,
|
|
837
|
+
dependency_type: (params.dependency_type as string) || "finish_to_start",
|
|
838
|
+
lag_days: (params.lag_days as number) || 0,
|
|
839
|
+
}).select().single();
|
|
840
|
+
if (error) return err(error.message);
|
|
841
|
+
return ok({ message: "Dependency created", dependency: data });
|
|
842
|
+
}
|
|
843
|
+
case "remove_dependency": {
|
|
844
|
+
const depId = (params.dependency_id || params.id) as string;
|
|
845
|
+
if (!depId) return err("Missing required: dependency_id");
|
|
846
|
+
const { error } = await supabase.from("pm_dependencies").delete().eq("id", depId).eq("user_id", userId);
|
|
847
|
+
if (error) return err(error.message);
|
|
848
|
+
return ok({ message: "Dependency removed", ok: true });
|
|
849
|
+
}
|
|
850
|
+
default:
|
|
851
|
+
return err(`Unknown action "${action}".`);
|
|
852
|
+
}
|
|
853
|
+
},
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
858
|
+
// META-TOOL 4: OFIERE_SCHEDULE_OPS — Calendar & Scheduler Events
|
|
859
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
860
|
+
|
|
861
|
+
function registerScheduleOps(
|
|
862
|
+
api: any,
|
|
863
|
+
supabase: SupabaseClient,
|
|
864
|
+
userId: string,
|
|
865
|
+
): void {
|
|
866
|
+
api.registerTool({
|
|
867
|
+
name: "OFIERE_SCHEDULE_OPS",
|
|
868
|
+
label: "Ofiere Schedule Operations",
|
|
869
|
+
description:
|
|
870
|
+
`Manage calendar events and schedule tasks on the timeline.\n\n` +
|
|
871
|
+
`Actions:\n` +
|
|
872
|
+
`- "list": List events. Optional: start_date, end_date, agent_id\n` +
|
|
873
|
+
`- "create": Schedule an event. Required: title, scheduled_date. Optional: task_id, agent_id, scheduled_time, duration_minutes, recurrence_type, recurrence_interval, color, priority\n` +
|
|
874
|
+
`- "update": Update event. Required: id. Optional: title, scheduled_date, scheduled_time, duration_minutes, status, recurrence_type\n` +
|
|
875
|
+
`- "delete": Remove event. Required: id\n` +
|
|
876
|
+
`recurrence_type: none, hourly, daily, weekly, monthly\n` +
|
|
877
|
+
`priority: 0=low, 1=medium, 2=high, 3=critical`,
|
|
878
|
+
parameters: {
|
|
879
|
+
type: "object",
|
|
880
|
+
required: ["action"],
|
|
881
|
+
properties: {
|
|
882
|
+
action: { type: "string", enum: ["list", "create", "update", "delete"] },
|
|
883
|
+
id: { type: "string", description: "Event ID" },
|
|
884
|
+
title: { type: "string", description: "Event title" },
|
|
885
|
+
description: { type: "string" },
|
|
886
|
+
task_id: { type: "string", description: "Link to a task" },
|
|
887
|
+
agent_id: { type: "string", description: "Assigned agent" },
|
|
888
|
+
scheduled_date: { type: "string", description: "Date (YYYY-MM-DD)" },
|
|
889
|
+
scheduled_time: { type: "string", description: "Time (HH:MM)" },
|
|
890
|
+
start_date: { type: "string", description: "List filter: start (YYYY-MM-DD)" },
|
|
891
|
+
end_date: { type: "string", description: "List filter: end (YYYY-MM-DD)" },
|
|
892
|
+
duration_minutes: { type: "number", description: "Duration in minutes (default 30)" },
|
|
893
|
+
recurrence_type: { type: "string", enum: ["none", "hourly", "daily", "weekly", "monthly"] },
|
|
894
|
+
recurrence_interval: { type: "number", description: "Repeat every N periods" },
|
|
895
|
+
color: { type: "string", description: "Hex color" },
|
|
896
|
+
priority: { type: "number", description: "0-3" },
|
|
897
|
+
status: { type: "string", enum: ["scheduled", "completed", "cancelled"] },
|
|
898
|
+
},
|
|
899
|
+
},
|
|
900
|
+
async execute(_id: string, params: Record<string, unknown>) {
|
|
901
|
+
const action = params.action as string;
|
|
902
|
+
switch (action) {
|
|
903
|
+
case "list": {
|
|
904
|
+
let q = supabase.from("scheduler_events").select("*").eq("user_id", userId)
|
|
905
|
+
.order("scheduled_date", { ascending: true });
|
|
906
|
+
if (params.start_date) q = q.gte("scheduled_date", params.start_date as string);
|
|
907
|
+
if (params.end_date) q = q.lte("scheduled_date", params.end_date as string);
|
|
908
|
+
if (params.agent_id) q = q.eq("agent_id", params.agent_id as string);
|
|
909
|
+
const { data, error } = await q;
|
|
910
|
+
if (error) return err(error.message);
|
|
911
|
+
return ok({ events: data || [], count: (data || []).length });
|
|
912
|
+
}
|
|
913
|
+
case "create": {
|
|
914
|
+
if (!params.title || !params.scheduled_date) return err("Missing required: title, scheduled_date");
|
|
915
|
+
const evtId = crypto.randomUUID();
|
|
916
|
+
const priorityMap: Record<string, number> = { low: 0, medium: 1, high: 2, critical: 3 };
|
|
917
|
+
const pVal = typeof params.priority === "number" ? params.priority
|
|
918
|
+
: priorityMap[String(params.priority || "").toLowerCase()] ?? 0;
|
|
919
|
+
const insertData: Record<string, any> = {
|
|
920
|
+
id: evtId,
|
|
921
|
+
user_id: userId,
|
|
922
|
+
task_id: (params.task_id as string) || null,
|
|
923
|
+
agent_id: (params.agent_id as string) || null,
|
|
924
|
+
title: params.title,
|
|
925
|
+
description: (params.description as string) || null,
|
|
926
|
+
scheduled_date: params.scheduled_date,
|
|
927
|
+
scheduled_time: (params.scheduled_time as string) || null,
|
|
928
|
+
duration_minutes: (params.duration_minutes as number) || 30,
|
|
929
|
+
recurrence_type: (params.recurrence_type as string) || "none",
|
|
930
|
+
recurrence_interval: (params.recurrence_interval as number) || 1,
|
|
931
|
+
status: "scheduled",
|
|
932
|
+
run_count: 0,
|
|
933
|
+
color: (params.color as string) || null,
|
|
934
|
+
priority: pVal,
|
|
935
|
+
};
|
|
936
|
+
const { error } = await supabase.from("scheduler_events").insert(insertData);
|
|
937
|
+
if (error) return err(error.message);
|
|
938
|
+
return ok({ message: `Event "${params.title}" scheduled for ${params.scheduled_date}`, id: evtId });
|
|
939
|
+
}
|
|
940
|
+
case "update": {
|
|
941
|
+
if (!params.id) return err("Missing required: id");
|
|
942
|
+
const upd: Record<string, any> = { updated_at: new Date().toISOString() };
|
|
943
|
+
for (const f of ["title", "description", "scheduled_date", "scheduled_time", "duration_minutes",
|
|
944
|
+
"recurrence_type", "recurrence_interval", "status", "color", "priority", "agent_id"]) {
|
|
945
|
+
if ((params as any)[f] !== undefined) upd[f] = (params as any)[f];
|
|
946
|
+
}
|
|
947
|
+
const { error } = await supabase.from("scheduler_events").update(upd).eq("id", params.id);
|
|
948
|
+
if (error) return err(error.message);
|
|
949
|
+
return ok({ message: "Event updated", ok: true });
|
|
950
|
+
}
|
|
951
|
+
case "delete": {
|
|
952
|
+
if (!params.id) return err("Missing required: id");
|
|
953
|
+
const { error } = await supabase.from("scheduler_events").delete().eq("id", params.id);
|
|
954
|
+
if (error) return err(error.message);
|
|
955
|
+
return ok({ message: "Event deleted", ok: true });
|
|
956
|
+
}
|
|
957
|
+
default:
|
|
958
|
+
return err(`Unknown action "${action}".`);
|
|
959
|
+
}
|
|
960
|
+
},
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
965
|
+
// META-TOOL 5: OFIERE_KNOWLEDGE_OPS — Knowledge Base
|
|
966
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
967
|
+
|
|
968
|
+
function registerKnowledgeOps(
|
|
969
|
+
api: any,
|
|
970
|
+
supabase: SupabaseClient,
|
|
971
|
+
userId: string,
|
|
972
|
+
): void {
|
|
973
|
+
api.registerTool({
|
|
974
|
+
name: "OFIERE_KNOWLEDGE_OPS",
|
|
975
|
+
label: "Ofiere Knowledge Operations",
|
|
976
|
+
description:
|
|
977
|
+
`Access the Ofiere Knowledge Library — the stored knowledge base in the dashboard. ` +
|
|
978
|
+
`Use this tool whenever the user mentions "knowledge base", "knowledge library", "knowledge entries", or asks to retrieve stored knowledge.\n\n` +
|
|
979
|
+
`Actions:\n` +
|
|
980
|
+
`- "search": Search the knowledge library by keyword. Required: query. Optional: limit\n` +
|
|
981
|
+
`- "list": List recent entries from the knowledge library. Optional: page, page_size, search\n` +
|
|
982
|
+
`- "create": Add a document to the knowledge library. Required: file_name. Optional: content, text, source, source_type, author, credibility_tier\n` +
|
|
983
|
+
`- "update": Edit a document. Required: id. Optional: file_name, content, text, source, source_type, author\n` +
|
|
984
|
+
`- "delete": Remove a document. Required: id`,
|
|
985
|
+
parameters: {
|
|
986
|
+
type: "object",
|
|
987
|
+
required: ["action"],
|
|
988
|
+
properties: {
|
|
989
|
+
action: { type: "string", enum: ["search", "list", "create", "update", "delete"] },
|
|
990
|
+
id: { type: "string", description: "Document ID" },
|
|
991
|
+
query: { type: "string", description: "Search query" },
|
|
992
|
+
file_name: { type: "string", description: "Document name" },
|
|
993
|
+
content: { type: "string", description: "Raw content" },
|
|
994
|
+
text: { type: "string", description: "Processed text" },
|
|
995
|
+
source: { type: "string", description: "Source URL or reference" },
|
|
996
|
+
source_type: { type: "string", description: "e.g. web, pdf, manual" },
|
|
997
|
+
author: { type: "string", description: "Author name" },
|
|
998
|
+
credibility_tier: { type: "string", description: "Credibility level" },
|
|
999
|
+
page: { type: "number", description: "Page number (default 1)" },
|
|
1000
|
+
page_size: { type: "number", description: "Results per page (default 20)" },
|
|
1001
|
+
search: { type: "string", description: "Filter for list action" },
|
|
1002
|
+
limit: { type: "number", description: "Max results for search" },
|
|
1003
|
+
},
|
|
1004
|
+
},
|
|
1005
|
+
async execute(_id: string, params: Record<string, unknown>) {
|
|
1006
|
+
const action = params.action as string;
|
|
1007
|
+
switch (action) {
|
|
1008
|
+
case "search": {
|
|
1009
|
+
if (!params.query) return err("Missing required: query");
|
|
1010
|
+
const lim = (params.limit as number) || 20;
|
|
1011
|
+
const searchTerm = `%${params.query}%`;
|
|
1012
|
+
const { data, error } = await supabase
|
|
1013
|
+
.from("knowledge_documents")
|
|
1014
|
+
.select("id, file_name, file_type, content, source, source_type, author, credibility_tier")
|
|
1015
|
+
.or(`file_name.ilike.${searchTerm},content.ilike.${searchTerm},author.ilike.${searchTerm},source.ilike.${searchTerm}`)
|
|
1016
|
+
.order("created_at", { ascending: false })
|
|
1017
|
+
.limit(lim);
|
|
1018
|
+
if (error) return err(error.message);
|
|
1019
|
+
return ok({ documents: data || [], count: (data || []).length, query: params.query });
|
|
1020
|
+
}
|
|
1021
|
+
case "list": {
|
|
1022
|
+
const page = Math.max(1, (params.page as number) || 1);
|
|
1023
|
+
const pageSize = Math.min(100, Math.max(1, (params.page_size as number) || 20));
|
|
1024
|
+
const from = (page - 1) * pageSize;
|
|
1025
|
+
const to = from + pageSize - 1;
|
|
1026
|
+
let q = supabase.from("knowledge_documents")
|
|
1027
|
+
.select("id, file_name, file_type, content, text, source, source_type, author, credibility_tier, created_at", { count: "exact" })
|
|
1028
|
+
.order("created_at", { ascending: false })
|
|
1029
|
+
.range(from, to);
|
|
1030
|
+
if (params.search) {
|
|
1031
|
+
const s = `%${params.search}%`;
|
|
1032
|
+
q = q.or(`file_name.ilike.${s},content.ilike.${s},author.ilike.${s}`);
|
|
1033
|
+
}
|
|
1034
|
+
const { data, count, error } = await q;
|
|
1035
|
+
if (error) return err(error.message);
|
|
1036
|
+
return ok({ documents: data || [], total: count || 0, page, page_size: pageSize });
|
|
1037
|
+
}
|
|
1038
|
+
case "create": {
|
|
1039
|
+
if (!params.file_name) return err("Missing required: file_name");
|
|
1040
|
+
const docId = crypto.randomUUID();
|
|
1041
|
+
const { error } = await supabase.from("knowledge_documents").insert({
|
|
1042
|
+
id: docId,
|
|
1043
|
+
user_id: userId,
|
|
1044
|
+
file_name: params.file_name,
|
|
1045
|
+
file_type: (params.file_type as string) || null,
|
|
1046
|
+
content: (params.content as string) || null,
|
|
1047
|
+
text: (params.text as string) || null,
|
|
1048
|
+
source: (params.source as string) || null,
|
|
1049
|
+
source_type: (params.source_type as string) || null,
|
|
1050
|
+
author: (params.author as string) || null,
|
|
1051
|
+
credibility_tier: (params.credibility_tier as string) || null,
|
|
1052
|
+
size_bytes: params.content ? new TextEncoder().encode(params.content as string).length : 0,
|
|
1053
|
+
indexed: false,
|
|
1054
|
+
});
|
|
1055
|
+
if (error) return err(error.message);
|
|
1056
|
+
return ok({ message: `Knowledge doc "${params.file_name}" created`, id: docId });
|
|
1057
|
+
}
|
|
1058
|
+
case "update": {
|
|
1059
|
+
if (!params.id) return err("Missing required: id");
|
|
1060
|
+
const allowed = ["file_name", "file_type", "content", "text", "source", "source_type", "author", "credibility_tier"];
|
|
1061
|
+
const upd: Record<string, any> = {};
|
|
1062
|
+
for (const k of allowed) if ((params as any)[k] !== undefined) upd[k] = (params as any)[k];
|
|
1063
|
+
if (Object.keys(upd).length === 0) return err("No valid fields to update");
|
|
1064
|
+
const { error } = await supabase.from("knowledge_documents").update(upd).eq("id", params.id);
|
|
1065
|
+
if (error) return err(error.message);
|
|
1066
|
+
return ok({ message: "Document updated", ok: true });
|
|
1067
|
+
}
|
|
1068
|
+
case "delete": {
|
|
1069
|
+
if (!params.id) return err("Missing required: id");
|
|
1070
|
+
const { error } = await supabase.from("knowledge_documents").delete().eq("id", params.id);
|
|
1071
|
+
if (error) return err(error.message);
|
|
1072
|
+
return ok({ message: "Document deleted", ok: true });
|
|
1073
|
+
}
|
|
1074
|
+
default:
|
|
1075
|
+
return err(`Unknown action "${action}".`);
|
|
1076
|
+
}
|
|
1077
|
+
},
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1082
|
+
// META-TOOL 6: OFIERE_WORKFLOW_OPS — Workflow Management & Execution
|
|
1083
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1084
|
+
|
|
1085
|
+
function registerWorkflowOps(
|
|
1086
|
+
api: any,
|
|
1087
|
+
supabase: SupabaseClient,
|
|
1088
|
+
userId: string,
|
|
1089
|
+
): void {
|
|
1090
|
+
api.registerTool({
|
|
1091
|
+
name: "OFIERE_WORKFLOW_OPS",
|
|
1092
|
+
label: "Ofiere Workflow Operations",
|
|
1093
|
+
description:
|
|
1094
|
+
`Manage, build, and trigger automated workflows in the Ofiere dashboard.\n\n` +
|
|
1095
|
+
`Actions:\n` +
|
|
1096
|
+
`- "list": List all workflows. Optional: status\n` +
|
|
1097
|
+
`- "get": Get workflow details. Required: id\n` +
|
|
1098
|
+
`- "create": Create a workflow WITH nodes and edges. Required: name. Optional: description, nodes, edges, schedule, status\n` +
|
|
1099
|
+
`- "update": Update a workflow. Required: id. Optional: name, description, status, nodes, edges, schedule\n` +
|
|
1100
|
+
`- "delete": Delete a workflow and its run history. Required: id\n` +
|
|
1101
|
+
`- "list_runs": List recent runs. Required: workflow_id. Optional: limit\n` +
|
|
1102
|
+
`- "trigger": Start a workflow run. Required: workflow_id\n\n` +
|
|
1103
|
+
`NODE TYPES (use these exact types when creating nodes):\n` +
|
|
1104
|
+
` TRIGGERS (start of workflow — pick one):\n` +
|
|
1105
|
+
` - "manual_trigger": User clicks Execute to start\n` +
|
|
1106
|
+
` - "webhook_trigger": External HTTP request triggers it\n` +
|
|
1107
|
+
` - "schedule_trigger": Runs on cron schedule. data: { label, cron: "0 9 * * 1-5" }\n` +
|
|
1108
|
+
` STEPS (the work):\n` +
|
|
1109
|
+
` - "agent_step": Delegates task to an AI agent. data: { label, agentId, task, responseMode: "text", timeoutSec: 120 }\n` +
|
|
1110
|
+
` - "http_request": Calls an external API. data: { label, method: "GET"|"POST", url }\n` +
|
|
1111
|
+
` - "formatter_step": Formats/transforms text or JSON. data: { label, template }\n` +
|
|
1112
|
+
` - "task_call": Runs a saved task. data: { label, agentId, taskId }\n` +
|
|
1113
|
+
` - "variable_set": Stores data in a variable. data: { label, variableName, variableValue }\n` +
|
|
1114
|
+
` CONTROL FLOW:\n` +
|
|
1115
|
+
` - "condition": If/else branch. data: { label, expression }\n` +
|
|
1116
|
+
` - "human_approval": Pauses for human approval. data: { label, instructions }\n` +
|
|
1117
|
+
` - "delay": Waits for a set time. data: { label, delaySec: 5 }\n` +
|
|
1118
|
+
` - "loop": Repeats actions. data: { label, loopType: "count", maxIterations: 3 }\n` +
|
|
1119
|
+
` - "convergence": Waits for multiple parallel inputs. data: { label, mergeStrategy: "wait_all" }\n` +
|
|
1120
|
+
` END:\n` +
|
|
1121
|
+
` - "output": Returns final result. data: { label, outputMode: "return" }\n` +
|
|
1122
|
+
` SPECIAL:\n` +
|
|
1123
|
+
` - "checkpoint": Loop target marker. data: { label }\n` +
|
|
1124
|
+
` - "note": Sticky note annotation. data: { label, noteText }\n\n` +
|
|
1125
|
+
`Each node: { type, data: { label, ... }, position?: { x, y } }. IDs and positions are auto-generated if omitted.\n` +
|
|
1126
|
+
`Each edge: { source: "node_id", target: "node_id" }. IDs auto-generated.\n` +
|
|
1127
|
+
`A manual_trigger node is always auto-prepended if no trigger node is included.`,
|
|
1128
|
+
parameters: {
|
|
1129
|
+
type: "object",
|
|
1130
|
+
required: ["action"],
|
|
1131
|
+
properties: {
|
|
1132
|
+
action: { type: "string", enum: ["list", "get", "create", "update", "delete", "list_runs", "trigger"] },
|
|
1133
|
+
id: { type: "string", description: "Workflow ID" },
|
|
1134
|
+
workflow_id: { type: "string", description: "Workflow ID for runs/trigger" },
|
|
1135
|
+
name: { type: "string", description: "Workflow name" },
|
|
1136
|
+
description: { type: "string" },
|
|
1137
|
+
nodes: {
|
|
1138
|
+
type: "array",
|
|
1139
|
+
items: {
|
|
1140
|
+
type: "object",
|
|
1141
|
+
properties: {
|
|
1142
|
+
id: { type: "string", description: "Node ID (auto-generated if omitted)" },
|
|
1143
|
+
type: { type: "string", enum: ["manual_trigger", "webhook_trigger", "schedule_trigger", "agent_step", "formatter_step", "http_request", "task_call", "variable_set", "condition", "human_approval", "delay", "loop", "convergence", "output", "checkpoint", "note"] },
|
|
1144
|
+
position: { type: "object", properties: { x: { type: "number" }, y: { type: "number" } } },
|
|
1145
|
+
data: { type: "object", description: "Node config — always include a 'label' field. See NODE TYPES above for type-specific fields." },
|
|
1146
|
+
},
|
|
1147
|
+
},
|
|
1148
|
+
description: "Workflow graph nodes",
|
|
1149
|
+
},
|
|
1150
|
+
edges: {
|
|
1151
|
+
type: "array",
|
|
1152
|
+
items: {
|
|
1153
|
+
type: "object",
|
|
1154
|
+
properties: {
|
|
1155
|
+
id: { type: "string", description: "Edge ID (auto-generated if omitted)" },
|
|
1156
|
+
source: { type: "string", description: "Source node ID" },
|
|
1157
|
+
target: { type: "string", description: "Target node ID" },
|
|
1158
|
+
},
|
|
1159
|
+
},
|
|
1160
|
+
description: "Connections between nodes. Each edge: { source, target }",
|
|
1161
|
+
},
|
|
1162
|
+
steps: { type: "array", items: { type: "object" }, description: "Legacy V1 step definitions" },
|
|
1163
|
+
schedule: { type: "string", description: "Cron expression or schedule" },
|
|
1164
|
+
status: { type: "string", enum: ["draft", "active", "paused", "archived"] },
|
|
1165
|
+
limit: { type: "number", description: "Max results" },
|
|
1166
|
+
},
|
|
1167
|
+
},
|
|
1168
|
+
async execute(_id: string, params: Record<string, unknown>) {
|
|
1169
|
+
const action = params.action as string;
|
|
1170
|
+
|
|
1171
|
+
// Default data for each node type — ensures dashboard renders them properly
|
|
1172
|
+
const NODE_DEFAULTS: Record<string, Record<string, any>> = {
|
|
1173
|
+
manual_trigger: { label: "Execute Trigger" },
|
|
1174
|
+
webhook_trigger: { label: "Webhook Trigger" },
|
|
1175
|
+
schedule_trigger: { label: "Schedule Trigger", cron: "0 9 * * 1-5" },
|
|
1176
|
+
agent_step: { label: "Agent Step", agentId: "", task: "", responseMode: "text", timeoutSec: 120 },
|
|
1177
|
+
formatter_step: { label: "Formatter", template: "" },
|
|
1178
|
+
http_request: { label: "HTTP Request", method: "GET", url: "" },
|
|
1179
|
+
task_call: { label: "Task", agentId: "", taskId: "", taskTitle: "", agentName: "" },
|
|
1180
|
+
variable_set: { label: "Set Variable", variableName: "", variableValue: "" },
|
|
1181
|
+
condition: { label: "Condition", expression: "" },
|
|
1182
|
+
human_approval: { label: "Human Approval", instructions: "" },
|
|
1183
|
+
delay: { label: "Delay", delaySec: 5 },
|
|
1184
|
+
loop: { label: "Loop", loopType: "count", maxIterations: 3 },
|
|
1185
|
+
convergence: { label: "Convergence", mergeStrategy: "wait_all" },
|
|
1186
|
+
output: { label: "Output", outputMode: "return" },
|
|
1187
|
+
checkpoint: { label: "Checkpoint" },
|
|
1188
|
+
note: { label: "Note", noteText: "" },
|
|
1189
|
+
};
|
|
1190
|
+
|
|
1191
|
+
// Valid node types
|
|
1192
|
+
const VALID_TYPES = new Set(Object.keys(NODE_DEFAULTS));
|
|
1193
|
+
|
|
1194
|
+
// Helper: normalize a single node with defaults and auto-ID
|
|
1195
|
+
function normalizeNode(n: any, i: number) {
|
|
1196
|
+
let type = n.type || "agent_step";
|
|
1197
|
+
if (!VALID_TYPES.has(type)) type = "agent_step"; // fallback invalid types
|
|
1198
|
+
const defaults = NODE_DEFAULTS[type] || {};
|
|
1199
|
+
return {
|
|
1200
|
+
id: n.id || `${type}-${Date.now()}-${i}`,
|
|
1201
|
+
type,
|
|
1202
|
+
position: n.position || { x: 250, y: 80 + i * 150 },
|
|
1203
|
+
data: { ...defaults, ...(n.data || {}), label: n.data?.label || defaults.label || type },
|
|
1204
|
+
};
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
switch (action) {
|
|
1208
|
+
case "list": {
|
|
1209
|
+
let q = supabase.from("workflows").select("*").eq("user_id", userId).order("updated_at", { ascending: false });
|
|
1210
|
+
if (params.status) q = q.eq("status", params.status as string);
|
|
1211
|
+
const { data, error } = await q;
|
|
1212
|
+
if (error) return err(error.message);
|
|
1213
|
+
return ok({ workflows: data || [], count: (data || []).length });
|
|
1214
|
+
}
|
|
1215
|
+
case "get": {
|
|
1216
|
+
const wfId = (params.id || params.workflow_id) as string;
|
|
1217
|
+
if (!wfId) return err("Missing required: id");
|
|
1218
|
+
const { data, error } = await supabase.from("workflows").select("*").eq("id", wfId).eq("user_id", userId).single();
|
|
1219
|
+
if (error) return err(error.message);
|
|
1220
|
+
return ok({ workflow: data });
|
|
1221
|
+
}
|
|
1222
|
+
case "create": {
|
|
1223
|
+
if (!params.name) return err("Missing required: name");
|
|
1224
|
+
const wfId = crypto.randomUUID();
|
|
1225
|
+
const stepsWithIds = ((params.steps as any[]) || []).map((s: any, i: number) => ({
|
|
1226
|
+
...s, id: s.id || `step-${i}`,
|
|
1227
|
+
}));
|
|
1228
|
+
|
|
1229
|
+
// Build nodes — normalize provided nodes
|
|
1230
|
+
let rawNodes = (params.nodes as any[]) || [];
|
|
1231
|
+
let finalNodes = rawNodes.map((n, i) => normalizeNode(n, i));
|
|
1232
|
+
|
|
1233
|
+
// Auto-prepend a trigger node if none is present
|
|
1234
|
+
const hasTrigger = finalNodes.some(n => n.type.includes("trigger"));
|
|
1235
|
+
if (!hasTrigger) {
|
|
1236
|
+
const triggerNode = {
|
|
1237
|
+
id: `manual_trigger-${Date.now()}`,
|
|
1238
|
+
type: "manual_trigger",
|
|
1239
|
+
position: { x: 100, y: 200 },
|
|
1240
|
+
data: { label: "Execute Trigger" },
|
|
1241
|
+
};
|
|
1242
|
+
// Shift all other nodes to the right
|
|
1243
|
+
finalNodes = finalNodes.map(n => ({
|
|
1244
|
+
...n,
|
|
1245
|
+
position: { x: (n.position?.x || 250) + 200, y: n.position?.y || 200 },
|
|
1246
|
+
}));
|
|
1247
|
+
finalNodes.unshift(triggerNode);
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
// Build edges — ensure IDs exist
|
|
1251
|
+
let finalEdges = (params.edges as any[]) || [];
|
|
1252
|
+
finalEdges = finalEdges.map((e: any, i: number) => ({
|
|
1253
|
+
id: e.id || `edge-${Date.now()}-${i}`,
|
|
1254
|
+
source: e.source,
|
|
1255
|
+
target: e.target,
|
|
1256
|
+
...(e.sourceHandle ? { sourceHandle: e.sourceHandle } : {}),
|
|
1257
|
+
...(e.targetHandle ? { targetHandle: e.targetHandle } : {}),
|
|
1258
|
+
}));
|
|
1259
|
+
|
|
1260
|
+
// Auto-wire trigger to first non-trigger node if no edge connects from trigger
|
|
1261
|
+
if (hasTrigger === false && finalNodes.length > 1 && finalEdges.length === 0) {
|
|
1262
|
+
// No edges at all — auto-connect trigger → first step
|
|
1263
|
+
} else if (hasTrigger === false && finalNodes.length > 1) {
|
|
1264
|
+
const triggerId = finalNodes[0].id;
|
|
1265
|
+
const firstStepId = finalNodes[1].id;
|
|
1266
|
+
const triggerHasEdge = finalEdges.some(e => e.source === triggerId);
|
|
1267
|
+
if (!triggerHasEdge) {
|
|
1268
|
+
finalEdges.unshift({
|
|
1269
|
+
id: `edge-trigger-${Date.now()}`,
|
|
1270
|
+
source: triggerId,
|
|
1271
|
+
target: firstStepId,
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
const { data, error } = await supabase.from("workflows").insert({
|
|
1277
|
+
id: wfId, user_id: userId,
|
|
1278
|
+
name: params.name,
|
|
1279
|
+
description: (params.description as string) || null,
|
|
1280
|
+
steps: stepsWithIds,
|
|
1281
|
+
schedule: (params.schedule as string) || null,
|
|
1282
|
+
status: (params.status as string) || "draft",
|
|
1283
|
+
nodes: finalNodes,
|
|
1284
|
+
edges: finalEdges,
|
|
1285
|
+
definition_version: 2,
|
|
1286
|
+
}).select().single();
|
|
1287
|
+
if (error) return err(error.message);
|
|
1288
|
+
return ok({
|
|
1289
|
+
message: `Workflow "${params.name}" created with ${finalNodes.length} node(s) and ${finalEdges.length} edge(s)`,
|
|
1290
|
+
workflow: data,
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
case "update": {
|
|
1294
|
+
const wfId = (params.id || params.workflow_id) as string;
|
|
1295
|
+
if (!wfId) return err("Missing required: id");
|
|
1296
|
+
const upd: Record<string, any> = { updated_at: new Date().toISOString() };
|
|
1297
|
+
for (const f of ["name", "description", "status", "steps", "schedule", "nodes", "edges"]) {
|
|
1298
|
+
if ((params as any)[f] !== undefined) upd[f] = (params as any)[f];
|
|
1299
|
+
}
|
|
1300
|
+
// Normalize nodes using the same defaults as create
|
|
1301
|
+
if (upd.nodes && Array.isArray(upd.nodes)) {
|
|
1302
|
+
upd.nodes = upd.nodes.map((n: any, i: number) => normalizeNode(n, i));
|
|
1303
|
+
}
|
|
1304
|
+
if (upd.edges && Array.isArray(upd.edges)) {
|
|
1305
|
+
upd.edges = upd.edges.map((e: any, i: number) => ({
|
|
1306
|
+
id: e.id || `edge-${Date.now()}-${i}`,
|
|
1307
|
+
source: e.source,
|
|
1308
|
+
target: e.target,
|
|
1309
|
+
}));
|
|
1310
|
+
}
|
|
1311
|
+
const { data, error } = await supabase.from("workflows").update(upd).eq("id", wfId).eq("user_id", userId).select().single();
|
|
1312
|
+
if (error) return err(error.message);
|
|
1313
|
+
return ok({ message: "Workflow updated", workflow: data });
|
|
1314
|
+
}
|
|
1315
|
+
case "delete": {
|
|
1316
|
+
const wfId = (params.id || params.workflow_id) as string;
|
|
1317
|
+
if (!wfId) return err("Missing required: id");
|
|
1318
|
+
// Delete associated runs first
|
|
1319
|
+
await supabase.from("workflow_runs").delete().eq("workflow_id", wfId);
|
|
1320
|
+
const { error } = await supabase.from("workflows").delete().eq("id", wfId).eq("user_id", userId);
|
|
1321
|
+
if (error) return err(error.message);
|
|
1322
|
+
return ok({ message: "Workflow and associated runs deleted", ok: true });
|
|
1323
|
+
}
|
|
1324
|
+
case "list_runs": {
|
|
1325
|
+
const wfId = (params.workflow_id || params.id) as string;
|
|
1326
|
+
if (!wfId) return err("Missing required: workflow_id");
|
|
1327
|
+
const { data, error } = await supabase.from("workflow_runs").select("*")
|
|
1328
|
+
.eq("workflow_id", wfId)
|
|
1329
|
+
.order("created_at", { ascending: false })
|
|
1330
|
+
.limit((params.limit as number) || 20);
|
|
1331
|
+
if (error) return err(error.message);
|
|
1332
|
+
return ok({ runs: data || [], count: (data || []).length });
|
|
1333
|
+
}
|
|
1334
|
+
case "trigger": {
|
|
1335
|
+
const wfId = (params.workflow_id || params.id) as string;
|
|
1336
|
+
if (!wfId) return err("Missing required: workflow_id");
|
|
1337
|
+
const runId = crypto.randomUUID();
|
|
1338
|
+
const { error } = await supabase.from("workflow_runs").insert({
|
|
1339
|
+
id: runId,
|
|
1340
|
+
workflow_id: wfId,
|
|
1341
|
+
status: "running",
|
|
1342
|
+
started_at: new Date().toISOString(),
|
|
1343
|
+
trigger_type: "agent",
|
|
1344
|
+
});
|
|
1345
|
+
if (error) return err(error.message);
|
|
1346
|
+
return ok({ message: `Workflow run triggered`, run_id: runId, workflow_id: wfId });
|
|
1347
|
+
}
|
|
1348
|
+
default:
|
|
1349
|
+
return err(`Unknown action "${action}".`);
|
|
1350
|
+
}
|
|
1351
|
+
},
|
|
1352
|
+
});
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1356
|
+
// META-TOOL 7: OFIERE_NOTIFY_OPS — Notifications
|
|
1357
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1358
|
+
|
|
1359
|
+
function registerNotifyOps(
|
|
1360
|
+
api: any,
|
|
1361
|
+
supabase: SupabaseClient,
|
|
1362
|
+
userId: string,
|
|
1363
|
+
): void {
|
|
1364
|
+
api.registerTool({
|
|
1365
|
+
name: "OFIERE_NOTIFY_OPS",
|
|
1366
|
+
label: "Ofiere Notification Operations",
|
|
1367
|
+
description:
|
|
1368
|
+
`Read and manage notifications.\n\n` +
|
|
1369
|
+
`Actions:\n` +
|
|
1370
|
+
`- "list": List notifications. Optional: unread_only (true/false), limit\n` +
|
|
1371
|
+
`- "mark_read": Mark one as read. Required: id\n` +
|
|
1372
|
+
`- "mark_all_read": Mark all as read\n` +
|
|
1373
|
+
`- "delete": Delete a notification. Required: id`,
|
|
1374
|
+
parameters: {
|
|
1375
|
+
type: "object",
|
|
1376
|
+
required: ["action"],
|
|
1377
|
+
properties: {
|
|
1378
|
+
action: { type: "string", enum: ["list", "mark_read", "mark_all_read", "delete"] },
|
|
1379
|
+
id: { type: "string", description: "Notification ID" },
|
|
1380
|
+
unread_only: { type: "boolean", description: "Only show unread" },
|
|
1381
|
+
limit: { type: "number", description: "Max results (default 50)" },
|
|
1382
|
+
},
|
|
1383
|
+
},
|
|
1384
|
+
async execute(_id: string, params: Record<string, unknown>) {
|
|
1385
|
+
const action = params.action as string;
|
|
1386
|
+
switch (action) {
|
|
1387
|
+
case "list": {
|
|
1388
|
+
let q = supabase.from("notifications").select("*")
|
|
1389
|
+
.order("created_at", { ascending: false })
|
|
1390
|
+
.limit((params.limit as number) || 50);
|
|
1391
|
+
if (params.unread_only === true) q = q.eq("read", false);
|
|
1392
|
+
const { data, error } = await q;
|
|
1393
|
+
if (error) return err(error.message);
|
|
1394
|
+
const unread = (data || []).filter((n: any) => !n.read).length;
|
|
1395
|
+
return ok({ notifications: data || [], count: (data || []).length, unread_count: unread });
|
|
1396
|
+
}
|
|
1397
|
+
case "mark_read": {
|
|
1398
|
+
if (!params.id) return err("Missing required: id");
|
|
1399
|
+
const { error } = await supabase.from("notifications").update({ read: true }).eq("id", params.id);
|
|
1400
|
+
if (error) return err(error.message);
|
|
1401
|
+
return ok({ message: "Notification marked as read", ok: true });
|
|
1402
|
+
}
|
|
1403
|
+
case "mark_all_read": {
|
|
1404
|
+
const { error } = await supabase.from("notifications").update({ read: true }).eq("read", false);
|
|
1405
|
+
if (error) return err(error.message);
|
|
1406
|
+
return ok({ message: "All notifications marked as read", ok: true });
|
|
1407
|
+
}
|
|
1408
|
+
case "delete": {
|
|
1409
|
+
if (!params.id) return err("Missing required: id");
|
|
1410
|
+
const { error } = await supabase.from("notifications").delete().eq("id", params.id);
|
|
1411
|
+
if (error) return err(error.message);
|
|
1412
|
+
return ok({ message: "Notification deleted", ok: true });
|
|
1413
|
+
}
|
|
1414
|
+
default:
|
|
1415
|
+
return err(`Unknown action "${action}".`);
|
|
1416
|
+
}
|
|
1417
|
+
},
|
|
1418
|
+
});
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1422
|
+
// META-TOOL 8: OFIERE_MEMORY_OPS — Conversations & Knowledge Fragments
|
|
1423
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1424
|
+
|
|
1425
|
+
function registerMemoryOps(
|
|
1426
|
+
api: any,
|
|
1427
|
+
supabase: SupabaseClient,
|
|
1428
|
+
userId: string,
|
|
1429
|
+
): void {
|
|
1430
|
+
api.registerTool({
|
|
1431
|
+
name: "OFIERE_MEMORY_OPS",
|
|
1432
|
+
label: "Ofiere Memory Operations",
|
|
1433
|
+
description:
|
|
1434
|
+
`Access conversation history and knowledge memory.\n\n` +
|
|
1435
|
+
`Actions:\n` +
|
|
1436
|
+
`- "list_conversations": List recent conversations. Optional: agent_id, limit\n` +
|
|
1437
|
+
`- "get_messages": Get messages from a conversation. Required: conversation_id. Optional: limit\n` +
|
|
1438
|
+
`- "search_messages": Search all messages. Required: query. Optional: agent_id, limit\n` +
|
|
1439
|
+
`- "add_knowledge": Store a knowledge fragment. Required: agent_id, content, source. Optional: tags, importance\n` +
|
|
1440
|
+
`- "search_knowledge": Search knowledge. Required: agent_id, query. Optional: limit`,
|
|
1441
|
+
parameters: {
|
|
1442
|
+
type: "object",
|
|
1443
|
+
required: ["action"],
|
|
1444
|
+
properties: {
|
|
1445
|
+
action: { type: "string", enum: ["list_conversations", "get_messages", "search_messages", "add_knowledge", "search_knowledge"] },
|
|
1446
|
+
conversation_id: { type: "string" },
|
|
1447
|
+
agent_id: { type: "string" },
|
|
1448
|
+
query: { type: "string", description: "Search query" },
|
|
1449
|
+
content: { type: "string", description: "Knowledge content to store" },
|
|
1450
|
+
source: { type: "string", description: "Source of knowledge" },
|
|
1451
|
+
tags: { type: "array", items: { type: "string" } },
|
|
1452
|
+
importance: { type: "number", description: "1-10 importance scale" },
|
|
1453
|
+
limit: { type: "number", description: "Max results" },
|
|
1454
|
+
},
|
|
1455
|
+
},
|
|
1456
|
+
async execute(_id: string, params: Record<string, unknown>) {
|
|
1457
|
+
const action = params.action as string;
|
|
1458
|
+
switch (action) {
|
|
1459
|
+
case "list_conversations": {
|
|
1460
|
+
let q = supabase.from("conversations")
|
|
1461
|
+
.select("id, agent_id, title, created_at, updated_at")
|
|
1462
|
+
.eq("user_id", userId)
|
|
1463
|
+
.order("updated_at", { ascending: false })
|
|
1464
|
+
.limit((params.limit as number) || 20);
|
|
1465
|
+
if (params.agent_id) q = q.eq("agent_id", params.agent_id as string);
|
|
1466
|
+
const { data, error } = await q;
|
|
1467
|
+
if (error) return err(error.message);
|
|
1468
|
+
return ok({ conversations: data || [], count: (data || []).length });
|
|
1469
|
+
}
|
|
1470
|
+
case "get_messages": {
|
|
1471
|
+
if (!params.conversation_id) return err("Missing required: conversation_id");
|
|
1472
|
+
const { data, error } = await supabase.from("conversation_messages")
|
|
1473
|
+
.select("id, role, content, created_at")
|
|
1474
|
+
.eq("conversation_id", params.conversation_id as string)
|
|
1475
|
+
.order("created_at", { ascending: true })
|
|
1476
|
+
.limit((params.limit as number) || 100);
|
|
1477
|
+
if (error) return err(error.message);
|
|
1478
|
+
return ok({ messages: data || [], count: (data || []).length });
|
|
1479
|
+
}
|
|
1480
|
+
case "search_messages": {
|
|
1481
|
+
if (!params.query) return err("Missing required: query");
|
|
1482
|
+
const searchTerm = `%${params.query}%`;
|
|
1483
|
+
let q = supabase.from("conversation_messages")
|
|
1484
|
+
.select("id, conversation_id, role, content, created_at")
|
|
1485
|
+
.ilike("content", searchTerm)
|
|
1486
|
+
.order("created_at", { ascending: false })
|
|
1487
|
+
.limit((params.limit as number) || 20);
|
|
1488
|
+
const { data, error } = await q;
|
|
1489
|
+
if (error) return err(error.message);
|
|
1490
|
+
return ok({ messages: data || [], count: (data || []).length, query: params.query });
|
|
1491
|
+
}
|
|
1492
|
+
case "add_knowledge": {
|
|
1493
|
+
if (!params.agent_id || !params.content || !params.source) return err("Missing required: agent_id, content, source");
|
|
1494
|
+
const fragId = crypto.randomUUID();
|
|
1495
|
+
const { error } = await supabase.from("knowledge_fragments").insert({
|
|
1496
|
+
id: fragId,
|
|
1497
|
+
agent_id: params.agent_id,
|
|
1498
|
+
content: params.content,
|
|
1499
|
+
source: params.source,
|
|
1500
|
+
tags: (params.tags as string[]) || [],
|
|
1501
|
+
importance: (params.importance as number) || 5,
|
|
1502
|
+
});
|
|
1503
|
+
if (error) return err(error.message);
|
|
1504
|
+
return ok({ message: "Knowledge stored", id: fragId });
|
|
1505
|
+
}
|
|
1506
|
+
case "search_knowledge": {
|
|
1507
|
+
if (!params.agent_id || !params.query) return err("Missing required: agent_id, query");
|
|
1508
|
+
const searchTerm = `%${params.query}%`;
|
|
1509
|
+
const { data, error } = await supabase.from("knowledge_fragments")
|
|
1510
|
+
.select("id, content, source, tags, importance, created_at")
|
|
1511
|
+
.eq("agent_id", params.agent_id as string)
|
|
1512
|
+
.ilike("content", searchTerm)
|
|
1513
|
+
.order("importance", { ascending: false })
|
|
1514
|
+
.limit((params.limit as number) || 20);
|
|
1515
|
+
if (error) return err(error.message);
|
|
1516
|
+
return ok({ fragments: data || [], count: (data || []).length });
|
|
1517
|
+
}
|
|
1518
|
+
default:
|
|
1519
|
+
return err(`Unknown action "${action}".`);
|
|
1520
|
+
}
|
|
1521
|
+
},
|
|
1522
|
+
});
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1526
|
+
// META-TOOL 9: OFIERE_PROMPT_OPS — System Prompt Chunk Management
|
|
1527
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1528
|
+
|
|
1529
|
+
function registerPromptOps(
|
|
1530
|
+
api: any,
|
|
1531
|
+
supabase: SupabaseClient,
|
|
1532
|
+
userId: string,
|
|
1533
|
+
): void {
|
|
1534
|
+
api.registerTool({
|
|
1535
|
+
name: "OFIERE_PROMPT_OPS",
|
|
1536
|
+
label: "Ofiere Prompt Operations",
|
|
1537
|
+
description:
|
|
1538
|
+
`Manage system prompt instruction chunks. These are the building blocks of agent behavior.\n\n` +
|
|
1539
|
+
`Actions:\n` +
|
|
1540
|
+
`- "list": List all prompt chunks\n` +
|
|
1541
|
+
`- "get": Get a specific chunk. Required: id\n` +
|
|
1542
|
+
`- "create": Create a new chunk. Required: name, content. Optional: color (hex), category\n` +
|
|
1543
|
+
`- "update": Update a chunk. Required: id. Optional: name, content, color, category, order\n` +
|
|
1544
|
+
`- "delete": Delete a chunk. Required: id`,
|
|
1545
|
+
parameters: {
|
|
1546
|
+
type: "object",
|
|
1547
|
+
required: ["action"],
|
|
1548
|
+
properties: {
|
|
1549
|
+
action: { type: "string", enum: ["list", "get", "create", "update", "delete"] },
|
|
1550
|
+
id: { type: "string", description: "Chunk ID" },
|
|
1551
|
+
name: { type: "string", description: "Chunk name/label (max 30 chars)" },
|
|
1552
|
+
content: { type: "string", description: "Prompt chunk content text" },
|
|
1553
|
+
color: { type: "string", description: "Hex color for display (e.g. #6B7280)" },
|
|
1554
|
+
category: { type: "string", description: "Category grouping (e.g. Personality, Instructions)" },
|
|
1555
|
+
order: { type: "number", description: "Display order (0-based)" },
|
|
1556
|
+
},
|
|
1557
|
+
},
|
|
1558
|
+
async execute(_id: string, params: Record<string, unknown>) {
|
|
1559
|
+
const action = params.action as string;
|
|
1560
|
+
switch (action) {
|
|
1561
|
+
case "list": {
|
|
1562
|
+
const { data, error } = await supabase.from("prompt_chunks").select("*").eq("user_id", userId).order("order", { ascending: true });
|
|
1563
|
+
if (error) return err(error.message);
|
|
1564
|
+
return ok({ chunks: data || [], count: (data || []).length });
|
|
1565
|
+
}
|
|
1566
|
+
case "get": {
|
|
1567
|
+
if (!params.id) return err("Missing required: id");
|
|
1568
|
+
const { data, error } = await supabase.from("prompt_chunks").select("*").eq("id", params.id).eq("user_id", userId).single();
|
|
1569
|
+
if (error) return err(error.message);
|
|
1570
|
+
return ok({ chunk: data });
|
|
1571
|
+
}
|
|
1572
|
+
case "create": {
|
|
1573
|
+
if (!params.name || !params.content) return err("Missing required: name, content");
|
|
1574
|
+
const chunkName = String(params.name).slice(0, 30);
|
|
1575
|
+
const chunkId = crypto.randomUUID();
|
|
1576
|
+
|
|
1577
|
+
// Get current max order to append at end
|
|
1578
|
+
const { data: existing } = await supabase
|
|
1579
|
+
.from("prompt_chunks")
|
|
1580
|
+
.select("order")
|
|
1581
|
+
.eq("user_id", userId);
|
|
1582
|
+
const maxOrder = existing && existing.length > 0
|
|
1583
|
+
? Math.max(...existing.map((c: any) => c.order ?? 0))
|
|
1584
|
+
: -1;
|
|
1585
|
+
|
|
1586
|
+
const { data, error } = await supabase.from("prompt_chunks").insert({
|
|
1587
|
+
id: chunkId,
|
|
1588
|
+
user_id: userId,
|
|
1589
|
+
name: chunkName,
|
|
1590
|
+
content: params.content,
|
|
1591
|
+
color: (params.color as string) || "#6B7280",
|
|
1592
|
+
category: (params.category as string) || "Uncategorized",
|
|
1593
|
+
order: (params.order as number) ?? maxOrder + 1,
|
|
1594
|
+
}).select().single();
|
|
1595
|
+
if (error) return err(error.message);
|
|
1596
|
+
api.logger?.info?.(`[ofiere] Prompt chunk created: "${chunkName}" by agent`);
|
|
1597
|
+
return ok({ message: `Prompt chunk "${chunkName}" created`, chunk: data });
|
|
1598
|
+
}
|
|
1599
|
+
case "update": {
|
|
1600
|
+
if (!params.id) return err("Missing required: id");
|
|
1601
|
+
const upd: Record<string, any> = { updated_at: new Date().toISOString() };
|
|
1602
|
+
for (const f of ["name", "content", "color", "category", "order"]) {
|
|
1603
|
+
if ((params as any)[f] !== undefined) upd[f] = (params as any)[f];
|
|
1604
|
+
}
|
|
1605
|
+
if (upd.name) upd.name = String(upd.name).slice(0, 30);
|
|
1606
|
+
const { data, error } = await supabase.from("prompt_chunks").update(upd).eq("id", params.id).eq("user_id", userId).select().single();
|
|
1607
|
+
if (error) return err(error.message);
|
|
1608
|
+
api.logger?.info?.(`[ofiere] Prompt chunk ${params.id} updated by agent`);
|
|
1609
|
+
return ok({ message: "Prompt chunk updated", chunk: data });
|
|
1610
|
+
}
|
|
1611
|
+
case "delete": {
|
|
1612
|
+
if (!params.id) return err("Missing required: id");
|
|
1613
|
+
const { error } = await supabase.from("prompt_chunks").delete().eq("id", params.id).eq("user_id", userId);
|
|
1614
|
+
if (error) return err(error.message);
|
|
1615
|
+
api.logger?.info?.(`[ofiere] Prompt chunk ${params.id} deleted by agent`);
|
|
1616
|
+
return ok({ message: "Prompt chunk deleted", ok: true });
|
|
1617
|
+
}
|
|
1618
|
+
default:
|
|
1619
|
+
return err(`Unknown action "${action}".`);
|
|
1620
|
+
}
|
|
1621
|
+
},
|
|
1622
|
+
});
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1626
|
+
// Public: Register All Meta-Tools
|
|
1627
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
499
1628
|
// This is the single entry point called by index.ts.
|
|
500
1629
|
// Returns the number of tools registered for dynamic prompt generation.
|
|
501
|
-
//
|
|
502
|
-
// To expand: add new register*Ops() calls here and increment the count.
|
|
503
1630
|
|
|
504
1631
|
export function registerTools(
|
|
505
1632
|
api: any, // OpenClawPluginApi — typed as any to avoid import-path issues at install time
|
|
@@ -512,11 +1639,18 @@ export function registerTools(
|
|
|
512
1639
|
const resolveAgent = createAgentResolver(api, supabase, userId, fallbackAgentId);
|
|
513
1640
|
|
|
514
1641
|
// ── Register each domain meta-tool ──
|
|
515
|
-
registerTaskOps(api, supabase, userId, resolveAgent);
|
|
516
|
-
registerAgentOps(api, supabase, userId, fallbackAgentId);
|
|
1642
|
+
registerTaskOps(api, supabase, userId, resolveAgent); // 1
|
|
1643
|
+
registerAgentOps(api, supabase, userId, fallbackAgentId); // 2
|
|
1644
|
+
registerProjectOps(api, supabase, userId); // 3
|
|
1645
|
+
registerScheduleOps(api, supabase, userId); // 4
|
|
1646
|
+
registerKnowledgeOps(api, supabase, userId); // 5
|
|
1647
|
+
registerWorkflowOps(api, supabase, userId); // 6
|
|
1648
|
+
registerNotifyOps(api, supabase, userId); // 7
|
|
1649
|
+
registerMemoryOps(api, supabase, userId); // 8
|
|
1650
|
+
registerPromptOps(api, supabase, userId); // 9
|
|
517
1651
|
|
|
518
1652
|
// ── Count and log ──
|
|
519
|
-
const toolCount =
|
|
1653
|
+
const toolCount = 9;
|
|
520
1654
|
const callerName = getCallingAgentName(api);
|
|
521
1655
|
const agentLabel = fallbackAgentId || callerName || "auto-detect";
|
|
522
1656
|
api.logger.info(`[ofiere] ${toolCount} meta-tools registered (agent: ${agentLabel})`);
|