ofiere-openclaw-plugin 1.1.1 → 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 +21 -9
- package/index.ts +4 -3
- package/package.json +2 -2
- package/src/prompt.ts +49 -11
- package/src/tools.ts +339 -307
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
|
48
|
-
|
|
49
|
-
| `
|
|
50
|
-
| `
|
|
51
|
-
|
|
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
|
|
78
|
-
|
|
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 —
|
|
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": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "OpenClaw plugin for Ofiere PM —
|
|
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
|
|
21
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
72
|
+
- Task statuses: PENDING, IN_PROGRESS, DONE, FAILED.
|
|
35
73
|
- Priority levels: 0=LOW, 1=MEDIUM, 2=HIGH, 3=CRITICAL.
|
|
36
|
-
- Changes
|
|
37
|
-
- Do NOT fabricate task 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 —
|
|
2
|
-
//
|
|
3
|
-
//
|
|
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
|
-
//
|
|
7
|
-
//
|
|
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
|
-
// ───
|
|
107
|
+
// ─── Shared: Agent ID Resolution ─────────────────────────────────────────────
|
|
104
108
|
|
|
105
|
-
|
|
106
|
-
api: any,
|
|
109
|
+
function createAgentResolver(
|
|
110
|
+
api: any,
|
|
107
111
|
supabase: SupabaseClient,
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
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: "
|
|
176
|
-
label: "
|
|
184
|
+
name: "OFIERE_TASK_OPS",
|
|
185
|
+
label: "Ofiere Task Operations",
|
|
177
186
|
description:
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
"
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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: "
|
|
216
|
+
description: "Task status",
|
|
190
217
|
enum: ["PENDING", "IN_PROGRESS", "DONE", "FAILED"],
|
|
191
218
|
},
|
|
192
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
)
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
"
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
-
|
|
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: "
|
|
452
|
-
label: "
|
|
435
|
+
name: "OFIERE_AGENT_OPS",
|
|
436
|
+
label: "Ofiere Agent Operations",
|
|
453
437
|
description:
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
"Use this to find the
|
|
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
|
-
|
|
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,
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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]
|
|
522
|
+
api.logger.info(`[ofiere] ${toolCount} meta-tools registered (agent: ${agentLabel})`);
|
|
523
|
+
|
|
524
|
+
return toolCount;
|
|
493
525
|
}
|