ofiere-openclaw-plugin 1.1.0 → 2.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/README.md CHANGED
@@ -40,15 +40,27 @@ Once configured, the plugin connects to your Supabase database at gateway startu
40
40
 
41
41
  Changes made by the agent are immediately visible on the Ofiere dashboard (Vercel) via Supabase real-time subscriptions.
42
42
 
43
- ## AI Tools
44
-
45
- | Tool | Description |
46
- |---|---|
47
- | `OFIERE_LIST_TASKS` | List and filter PM tasks |
48
- | `OFIERE_CREATE_TASK` | Create a new task (auto-assigns to calling agent) |
49
- | `OFIERE_UPDATE_TASK` | Update task fields (status, priority, progress, etc.) |
50
- | `OFIERE_DELETE_TASK` | Delete a task and its subtasks |
51
- | `OFIERE_LIST_AGENTS` | List available agents for task assignment |
43
+ ## AI Meta-Tools
44
+
45
+ The plugin uses a scalable meta-tool architecture. Each tool handles one domain with an `action` parameter to select the operation.
46
+
47
+ | Tool | Actions | Description |
48
+ |---|---|---|
49
+ | `OFIERE_TASK_OPS` | `list`, `create`, `update`, `delete` | Manage PM tasks — list, create, update status/priority, delete |
50
+ | `OFIERE_AGENT_OPS` | `list` | Query available agents for task assignment |
51
+
52
+ ### Example
53
+
54
+ ```
55
+ // Create a task
56
+ OFIERE_TASK_OPS({ action: "create", title: "Deploy v2", agent_id: "ivy" })
57
+
58
+ // List tasks
59
+ OFIERE_TASK_OPS({ action: "list", status: "PENDING", limit: 10 })
60
+
61
+ // Update a task
62
+ OFIERE_TASK_OPS({ action: "update", task_id: "task-123", status: "DONE" })
63
+ ```
52
64
 
53
65
  ## CLI Commands
54
66
 
package/index.ts CHANGED
@@ -74,12 +74,13 @@ const ofierePlugin = {
74
74
  // Probe the api object for any agent identity info (for debugging + fallback)
75
75
  probeApiForAgentName(api, api.logger);
76
76
 
77
- registerTools(api, supabase, config);
78
- promptState.toolCount = 5;
77
+ // registerTools now returns the count — no more hardcoding
78
+ const toolCount = registerTools(api, supabase, config);
79
+ promptState.toolCount = toolCount;
79
80
  promptState.ready = true;
80
81
  const agentLabel = config.agentId || "auto-detect";
81
82
  api.logger.info(
82
- `[ofiere] Ready — 5 tools registered (agent: ${agentLabel})`,
83
+ `[ofiere] Ready — ${toolCount} meta-tools registered (agent: ${agentLabel})`,
83
84
  );
84
85
  } catch (e) {
85
86
  const msg = e instanceof Error ? e.message : String(e);
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "ofiere-openclaw-plugin",
3
- "version": "1.1.0",
3
+ "version": "2.0.0",
4
4
  "type": "module",
5
- "description": "OpenClaw plugin for Ofiere PM — manage tasks, agents, and projects from your agent",
5
+ "description": "OpenClaw plugin for Ofiere PM — scalable meta-tool architecture for task, agent, and project management",
6
6
  "keywords": ["openclaw", "ofiere", "project-management", "agents", "plugin"],
7
7
  "homepage": "https://github.com/gilanggemar/Ofiere",
8
8
  "repository": {
package/src/prompt.ts CHANGED
@@ -1,3 +1,41 @@
1
+ // src/prompt.ts — Dynamic system prompt for Ofiere PM plugin
2
+ //
3
+ // The prompt is built dynamically based on plugin state.
4
+ // Tool documentation is structured so adding a new meta-tool
5
+ // only requires adding a new entry to TOOL_DOCS below.
6
+
7
+ // ─── Tool Documentation Registry ────────────────────────────────────────────
8
+ // Add new meta-tool docs here when expanding. Each entry maps to one
9
+ // registered meta-tool and will be included in the system prompt.
10
+
11
+ const TOOL_DOCS: Record<string, string> = {
12
+ OFIERE_TASK_OPS: `- **OFIERE_TASK_OPS** — Manage tasks (action: "list", "create", "update", "delete")
13
+ - list: Filter by status, agent_id, space_id, folder_id, limit
14
+ - create: Requires title + agent_id. Pass your name to self-assign, 'none' for unassigned
15
+ - update: Requires task_id. Change title, status, priority, progress, etc.
16
+ - delete: Requires task_id. Removes task and subtasks`,
17
+
18
+ OFIERE_AGENT_OPS: `- **OFIERE_AGENT_OPS** — Query agents (action: "list")
19
+ - list: See all agents with IDs, names, roles for task assignment`,
20
+
21
+ // ── Future meta-tools — uncomment when registered ──
22
+ // OFIERE_PROJECT_OPS: `- **OFIERE_PROJECT_OPS** — Manage projects (action: "list", "create", "update", "delete")
23
+ // - list: List spaces, folders, and projects
24
+ // - create: Create a new space or folder
25
+ // - update: Rename, move, or archive
26
+ // - delete: Remove space/folder and reassign tasks`,
27
+ //
28
+ // OFIERE_SCHEDULE_OPS: `- **OFIERE_SCHEDULE_OPS** — Calendar & timeline (action: "list", "schedule", "reschedule")
29
+ // - list: View scheduled events for a date range
30
+ // - schedule: Assign a task to a time slot
31
+ // - reschedule: Move an event to a new time`,
32
+ //
33
+ // OFIERE_KNOWLEDGE_OPS: `- **OFIERE_KNOWLEDGE_OPS** — Knowledge base (action: "search", "create", "update")
34
+ // - search: Find knowledge entries by query
35
+ // - create: Add a new knowledge entry
36
+ // - update: Edit an existing entry`,
37
+ };
38
+
1
39
  export function getSystemPrompt(state: {
2
40
  ready: boolean;
3
41
  toolCount: number;
@@ -13,28 +51,28 @@ export function getSystemPrompt(state: {
13
51
  ? `When you create a task without specifying agent_id, it is assigned to YOU (${state.agentId}).`
14
52
  : `When you create a task without specifying agent_id, it is assigned to YOU automatically.`;
15
53
 
54
+ // Build tool docs from registry — only include docs for tools that exist
55
+ const toolDocs = Object.values(TOOL_DOCS).join("\n");
56
+
16
57
  return `<ofiere-pm>
17
58
  You are connected to the Ofiere Project Management dashboard via the Ofiere PM plugin.
18
59
  ${agentLine}
19
60
 
20
- ## Your Ofiere PM Capabilities
21
- You have ${state.toolCount} tools to manage the PM dashboard:
61
+ ## Your Ofiere PM Tools (${state.toolCount} meta-tools)
62
+
63
+ Each tool uses an "action" parameter to select the operation. Always include action.
22
64
 
23
- - **OFIERE_LIST_TASKS** — List and filter tasks from the PM dashboard
24
- - **OFIERE_CREATE_TASK** — Create new tasks (auto-assigned to you if no agent_id given)
25
- - **OFIERE_UPDATE_TASK** — Update task status, priority, progress, etc.
26
- - **OFIERE_DELETE_TASK** — Delete tasks
27
- - **OFIERE_LIST_AGENTS** — See all available agents for task assignment
65
+ ${toolDocs}
28
66
 
29
67
  ## Rules
30
68
  - ${assignRule}
31
69
  - To create an unassigned task, pass agent_id as "none" or "unassigned".
32
- - When the user says "create a task for [agent name]", use OFIERE_LIST_AGENTS to find the agent ID, then pass it as agent_id.
70
+ - When the user says "create a task for [agent name]", use OFIERE_AGENT_OPS action:"list" to find the agent ID, then use OFIERE_TASK_OPS action:"create" with that agent_id.
33
71
  - Always confirm task creation/updates by reporting back what was done.
34
- - Task statuses are: PENDING, IN_PROGRESS, DONE, FAILED.
72
+ - Task statuses: PENDING, IN_PROGRESS, DONE, FAILED.
35
73
  - Priority levels: 0=LOW, 1=MEDIUM, 2=HIGH, 3=CRITICAL.
36
- - Changes you make appear in the Ofiere dashboard immediately in real time.
37
- - Do NOT fabricate task IDs — always use OFIERE_LIST_TASKS to look up real IDs.
74
+ - Changes appear in the Ofiere dashboard immediately via real-time sync.
75
+ - Do NOT fabricate task IDs — use OFIERE_TASK_OPS action:"list" to look up real IDs.
38
76
  </ofiere-pm>`;
39
77
  }
40
78
 
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,357 @@ 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
+ // ─── META-TOOL: OFIERE_TASK_OPS ──────────────────────────────────────────────
173
176
 
177
+ function registerTaskOps(
178
+ api: any,
179
+ supabase: SupabaseClient,
180
+ userId: string,
181
+ resolveAgent: (id?: string) => Promise<string | null>,
182
+ ): void {
174
183
  api.registerTool({
175
- name: "OFIERE_LIST_TASKS",
176
- label: "List Ofiere Tasks",
184
+ name: "OFIERE_TASK_OPS",
185
+ label: "Ofiere Task Operations",
177
186
  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.",
187
+ `Manage tasks in the Ofiere PM dashboard. All task operations go through this tool.\n\n` +
188
+ `Actions:\n` +
189
+ `- "list": List/filter tasks. Optional params: status, agent_id, space_id, folder_id, limit\n` +
190
+ `- "create": Create a new task. Required: title, agent_id. Optional: description, status, priority, space_id, folder_id, start_date, due_date, tags\n` +
191
+ `- "update": Update an existing task. Required: task_id. Optional: title, description, status, priority, progress, agent_id, start_date, due_date, tags\n` +
192
+ `- "delete": Delete a task and its subtasks. Required: task_id\n\n` +
193
+ `agent_id for create: Pass your own name (e.g. 'ivy') to self-assign, another agent's name to assign to them, or 'none'/'unassigned' for no assignee.\n` +
194
+ `Status values: PENDING, IN_PROGRESS, DONE, FAILED\n` +
195
+ `Priority values: 0=LOW, 1=MEDIUM, 2=HIGH, 3=CRITICAL`,
181
196
  parameters: {
182
197
  type: "object",
198
+ required: ["action"],
183
199
  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" },
200
+ action: {
201
+ type: "string",
202
+ description: "The operation to perform",
203
+ enum: ["list", "create", "update", "delete"],
204
+ },
205
+ // ── Shared / contextual params ──
206
+ task_id: { type: "string", description: "Task ID (required for update, delete)" },
207
+ title: { type: "string", description: "Task title (required for create)" },
208
+ description: { type: "string", description: "Task description" },
209
+ agent_id: {
210
+ 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.",
213
+ },
187
214
  status: {
188
215
  type: "string",
189
- description: "Filter by status: PENDING, IN_PROGRESS, DONE, FAILED",
216
+ description: "Task status",
190
217
  enum: ["PENDING", "IN_PROGRESS", "DONE", "FAILED"],
191
218
  },
192
- limit: { type: "number", description: "Max results (default 50)" },
219
+ priority: { type: "number", description: "Priority: 0=LOW, 1=MEDIUM, 2=HIGH, 3=CRITICAL" },
220
+ progress: { type: "number", description: "Progress percentage 0-100 (update only)" },
221
+ space_id: { type: "string", description: "PM Space ID" },
222
+ folder_id: { type: "string", description: "PM Folder ID" },
223
+ start_date: { type: "string", description: "Start date (ISO 8601)" },
224
+ due_date: { type: "string", description: "Due date (ISO 8601)" },
225
+ tags: {
226
+ type: "array",
227
+ items: { type: "string" },
228
+ description: "Tags for the task",
229
+ },
230
+ limit: { type: "number", description: "Max results for list (default 50)" },
193
231
  },
194
232
  },
195
233
  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));
234
+ const action = params.action as string;
235
+
236
+ switch (action) {
237
+ case "list":
238
+ return handleListTasks(supabase, userId, params);
239
+ case "create":
240
+ return handleCreateTask(supabase, userId, resolveAgent, params);
241
+ case "update":
242
+ return handleUpdateTask(supabase, userId, params);
243
+ case "delete":
244
+ return handleDeleteTask(supabase, userId, params);
245
+ default:
246
+ return err(
247
+ `Unknown action "${action}". Valid actions: list, create, update, delete`,
248
+ );
217
249
  }
218
250
  },
219
251
  });
252
+ }
220
253
 
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
- },
264
- },
265
- },
266
- async execute(_id: string, params: Record<string, unknown>) {
267
- try {
268
- if (!params.title) return err("Missing required field: title");
269
-
270
- const id = `task-${Date.now()}`;
271
- const now = new Date().toISOString();
272
-
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());
278
-
279
- const assignee = isUnassigned ? null : await resolveAgent(rawAgentId);
280
-
281
- const insertData: Record<string, unknown> = {
282
- id,
283
- user_id: userId,
284
- title: params.title,
285
- 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,
300
- };
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);
316
- }
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));
325
- }
326
- },
327
- },
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
- },
362
- },
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];
374
- }
375
- if (params.status === "DONE") updates.completed_at = new Date().toISOString();
376
-
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();
384
-
385
- 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));
389
- }
390
- },
391
- },
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
- }
432
-
433
- const { error } = await supabase
434
- .from("tasks")
435
- .delete()
436
- .eq("id", taskId)
437
- .eq("user_id", userId);
438
-
439
- 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));
443
- }
444
- },
445
- },
446
- );
254
+ // ── Task action handlers ─────────────────────────────────────────────────────
255
+
256
+ async function handleListTasks(
257
+ supabase: SupabaseClient,
258
+ userId: string,
259
+ params: Record<string, unknown>,
260
+ ): Promise<ToolResult> {
261
+ try {
262
+ let query = supabase
263
+ .from("tasks")
264
+ .select(
265
+ "id, title, description, status, priority, agent_id, space_id, folder_id, " +
266
+ "start_date, due_date, progress, created_at, updated_at",
267
+ )
268
+ .eq("user_id", userId)
269
+ .order("updated_at", { ascending: false });
270
+
271
+ if (params.space_id) query = query.eq("space_id", params.space_id as string);
272
+ if (params.folder_id) query = query.eq("folder_id", params.folder_id as string);
273
+ if (params.agent_id) query = query.eq("agent_id", params.agent_id as string);
274
+ if (params.status) query = query.eq("status", params.status as string);
275
+ query = query.limit((params.limit as number) || 50);
276
+
277
+ const { data, error } = await query;
278
+ if (error) return err(error.message);
279
+ return ok({ tasks: data || [], count: (data || []).length });
280
+ } catch (e) {
281
+ return err(e instanceof Error ? e.message : String(e));
282
+ }
283
+ }
284
+
285
+ async function handleCreateTask(
286
+ supabase: SupabaseClient,
287
+ userId: string,
288
+ resolveAgent: (id?: string) => Promise<string | null>,
289
+ params: Record<string, unknown>,
290
+ ): Promise<ToolResult> {
291
+ try {
292
+ if (!params.title) return err("Missing required field: title");
293
+
294
+ const id = `task-${Date.now()}`;
295
+ const now = new Date().toISOString();
296
+
297
+ // Handle explicit "none"/"unassigned"
298
+ const rawAgentId = params.agent_id as string | undefined;
299
+ const isUnassigned =
300
+ rawAgentId &&
301
+ ["none", "unassigned", "null", ""].includes(rawAgentId.toLowerCase().trim());
302
+
303
+ const assignee = isUnassigned ? null : await resolveAgent(rawAgentId);
304
+
305
+ const insertData: Record<string, unknown> = {
306
+ id,
307
+ user_id: userId,
308
+ title: params.title,
309
+ description: (params.description as string) || null,
310
+ agent_id: assignee,
311
+ assignee_type: "agent",
312
+ status: (params.status as string) || "PENDING",
313
+ priority: params.priority !== undefined ? params.priority : 1,
314
+ space_id: (params.space_id as string) || null,
315
+ folder_id: (params.folder_id as string) || null,
316
+ start_date: (params.start_date as string) || null,
317
+ due_date: (params.due_date as string) || null,
318
+ tags: (params.tags as string[]) || [],
319
+ progress: 0,
320
+ sort_order: 0,
321
+ custom_fields: {},
322
+ created_at: now,
323
+ updated_at: now,
324
+ };
325
+
326
+ const { error } = await supabase.from("tasks").insert(insertData);
327
+
328
+ if (error) {
329
+ if (error.message?.includes("agent_id") || error.message?.includes("foreign key")) {
330
+ insertData.agent_id = null;
331
+ const retry = await supabase.from("tasks").insert(insertData);
332
+ if (retry.error) return err(retry.error.message);
333
+ return ok({
334
+ id,
335
+ message: `Task "${params.title}" created (agent_id "${assignee}" was invalid, assigned to none)`,
336
+ task: insertData,
337
+ });
338
+ }
339
+ return err(error.message);
340
+ }
341
+
342
+ return ok({
343
+ id,
344
+ message: `Task "${params.title}" created and assigned to ${assignee || "no one"}`,
345
+ task: insertData,
346
+ });
347
+ } catch (e) {
348
+ return err(e instanceof Error ? e.message : String(e));
349
+ }
350
+ }
351
+
352
+ async function handleUpdateTask(
353
+ supabase: SupabaseClient,
354
+ userId: string,
355
+ params: Record<string, unknown>,
356
+ ): Promise<ToolResult> {
357
+ try {
358
+ if (!params.task_id) return err("Missing required field: task_id");
447
359
 
448
- // ── OFIERE_LIST_AGENTS Required (read-only, no side effects) ───────
360
+ const updates: Record<string, unknown> = { updated_at: new Date().toISOString() };
361
+ const fields = [
362
+ "title", "description", "status", "priority", "progress",
363
+ "agent_id", "start_date", "due_date", "tags",
364
+ ];
365
+ for (const f of fields) {
366
+ if (params[f] !== undefined) updates[f] = params[f];
367
+ }
368
+ if (params.status === "DONE") updates.completed_at = new Date().toISOString();
369
+
370
+ const { data, error } = await supabase
371
+ .from("tasks")
372
+ .update(updates)
373
+ .eq("id", params.task_id as string)
374
+ .eq("user_id", userId)
375
+ .select("id, title, status, priority, agent_id")
376
+ .single();
377
+
378
+ if (error) return err(error.message);
379
+ return ok({ message: `Task "${data?.title}" updated`, task: data });
380
+ } catch (e) {
381
+ return err(e instanceof Error ? e.message : String(e));
382
+ }
383
+ }
384
+
385
+ async function handleDeleteTask(
386
+ supabase: SupabaseClient,
387
+ userId: string,
388
+ params: Record<string, unknown>,
389
+ ): Promise<ToolResult> {
390
+ try {
391
+ if (!params.task_id) return err("Missing required field: task_id");
392
+ const taskId = params.task_id as string;
393
+
394
+ await supabase.from("scheduler_events").delete().eq("task_id", taskId);
395
+
396
+ const { data: subtasks } = await supabase
397
+ .from("tasks")
398
+ .select("id")
399
+ .eq("parent_task_id", taskId)
400
+ .eq("user_id", userId);
401
+
402
+ if (subtasks && subtasks.length > 0) {
403
+ for (const sub of subtasks) {
404
+ await supabase.from("scheduler_events").delete().eq("task_id", sub.id);
405
+ }
406
+ await supabase
407
+ .from("tasks")
408
+ .delete()
409
+ .in("id", subtasks.map((s: { id: string }) => s.id))
410
+ .eq("user_id", userId);
411
+ }
412
+
413
+ const { error } = await supabase
414
+ .from("tasks")
415
+ .delete()
416
+ .eq("id", taskId)
417
+ .eq("user_id", userId);
449
418
 
419
+ if (error) return err(error.message);
420
+ return ok({ message: `Task ${taskId} deleted`, deleted: true });
421
+ } catch (e) {
422
+ return err(e instanceof Error ? e.message : String(e));
423
+ }
424
+ }
425
+
426
+ // ─── META-TOOL: OFIERE_AGENT_OPS ────────────────────────────────────────────
427
+
428
+ function registerAgentOps(
429
+ api: any,
430
+ supabase: SupabaseClient,
431
+ userId: string,
432
+ fallbackAgentId: string,
433
+ ): void {
450
434
  api.registerTool({
451
- name: "OFIERE_LIST_AGENTS",
452
- label: "List Ofiere Agents",
435
+ name: "OFIERE_AGENT_OPS",
436
+ label: "Ofiere Agent Operations",
453
437
  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.",
438
+ `Query agents in the Ofiere PM system.\n\n` +
439
+ `Actions:\n` +
440
+ `- "list": List all available agents with their IDs, names, roles, and status. Use this to find the correct agent_id for task assignment.`,
457
441
  parameters: {
458
442
  type: "object",
459
- properties: {},
443
+ required: ["action"],
444
+ properties: {
445
+ action: {
446
+ type: "string",
447
+ description: "The operation to perform",
448
+ enum: ["list"],
449
+ },
450
+ },
460
451
  },
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));
452
+ async execute(_id: string, params: Record<string, unknown>) {
453
+ const action = params.action as string;
454
+
455
+ switch (action) {
456
+ case "list":
457
+ return handleListAgents(api, supabase, userId, fallbackAgentId);
458
+ default:
459
+ return err(`Unknown action "${action}". Valid actions: list`);
486
460
  }
487
461
  },
488
462
  });
463
+ }
464
+
465
+ async function handleListAgents(
466
+ api: any,
467
+ supabase: SupabaseClient,
468
+ userId: string,
469
+ fallbackAgentId: string,
470
+ ): Promise<ToolResult> {
471
+ try {
472
+ // Resolve calling agent's ID for the "your_agent_id" hint
473
+ const callerName = getCallingAgentName(api);
474
+ let yourAgentId = fallbackAgentId || "";
475
+ if (callerName && !yourAgentId) {
476
+ try {
477
+ yourAgentId = await resolveAgentId(callerName, userId, supabase);
478
+ } catch { /* ignore */ }
479
+ }
480
+
481
+ const { data, error } = await supabase
482
+ .from("agents")
483
+ .select("id, name, codename, role, status")
484
+ .eq("user_id", userId)
485
+ .order("name");
486
+
487
+ if (error) return err(error.message);
488
+ return ok({
489
+ agents: data || [],
490
+ count: (data || []).length,
491
+ your_agent_id: yourAgentId,
492
+ });
493
+ } catch (e) {
494
+ return err(e instanceof Error ? e.message : String(e));
495
+ }
496
+ }
489
497
 
498
+ // ─── Public: Register All Meta-Tools ─────────────────────────────────────────
499
+ // This is the single entry point called by index.ts.
500
+ // 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
+
504
+ export function registerTools(
505
+ api: any, // OpenClawPluginApi — typed as any to avoid import-path issues at install time
506
+ supabase: SupabaseClient,
507
+ config: OfiereConfig,
508
+ ): number {
509
+ const userId = config.userId;
510
+ const fallbackAgentId = config.agentId; // May be empty — that's fine
511
+
512
+ const resolveAgent = createAgentResolver(api, supabase, userId, fallbackAgentId);
513
+
514
+ // ── Register each domain meta-tool ──
515
+ registerTaskOps(api, supabase, userId, resolveAgent);
516
+ registerAgentOps(api, supabase, userId, fallbackAgentId);
517
+
518
+ // ── Count and log ──
519
+ const toolCount = 2; // Update this when adding new meta-tools
490
520
  const callerName = getCallingAgentName(api);
491
521
  const agentLabel = fallbackAgentId || callerName || "auto-detect";
492
- api.logger.info(`[ofiere] 5 tools registered (agent: ${agentLabel})`);
522
+ api.logger.info(`[ofiere] ${toolCount} meta-tools registered (agent: ${agentLabel})`);
523
+
524
+ return toolCount;
493
525
  }