ofiere-openclaw-plugin 4.16.0 → 4.18.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 +2 -2
- package/package.json +2 -2
- package/src/prompt.ts +32 -2
- package/src/tools.ts +699 -6
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ Manage your Ofiere PM dashboard directly from OpenClaw agents. Create tasks, man
|
|
|
5
5
|
## Quick Install (One-Click)
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
curl -sSL https://
|
|
8
|
+
curl -sSL https://ofiere.com/scripts/install.sh | bash -s -- \
|
|
9
9
|
--supabase-url "https://xxx.supabase.co" \
|
|
10
10
|
--service-key "eyJ..." \
|
|
11
11
|
--user-id "your-uuid"
|
|
@@ -16,7 +16,7 @@ Only 3 parameters needed. All agents get the plugin automatically.
|
|
|
16
16
|
## Uninstall
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
|
-
curl -sSL https://
|
|
19
|
+
curl -sSL https://ofiere.com/scripts/uninstall.sh | bash
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
## How It Works
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ofiere-openclaw-plugin",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.18.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "OpenClaw plugin for Ofiere PM -
|
|
5
|
+
"description": "OpenClaw plugin for Ofiere PM - 12 meta-tools covering tasks, agents, projects, scheduling, knowledge, workflows, notifications, memory, prompts, constellation, space file management, and execution plan builder",
|
|
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
|
@@ -62,10 +62,19 @@ const TOOL_DOCS: Record<string, string> = {
|
|
|
62
62
|
- Edge handles: condition edges use sourceHandle "condition-true"/"condition-false". Loop edges use "loop_body"/"done"
|
|
63
63
|
- Variables: Use {{prev.nodeId.outputText}} for prior outputs, {{variables.key}} for stored variables`,
|
|
64
64
|
|
|
65
|
-
OFIERE_NOTIFY_OPS: `- **OFIERE_NOTIFY_OPS** — Notifications (action: "list", "mark_read", "mark_all_read", "delete")
|
|
65
|
+
OFIERE_NOTIFY_OPS: `- **OFIERE_NOTIFY_OPS** — Notifications & Channel Reports (action: "list", "mark_read", "mark_all_read", "delete", "send_report", "schedule_report", "list_schedules", "delete_schedule")
|
|
66
66
|
- list: Recent notifications. unread_only=true for unread only
|
|
67
67
|
- mark_read: Mark one notification read by ID
|
|
68
|
-
- mark_all_read: Mark all as read
|
|
68
|
+
- mark_all_read: Mark all as read
|
|
69
|
+
- send_report: Send a PM progress report to YOUR connected channels (Telegram, Discord, Slack, etc.)
|
|
70
|
+
- Required: scope_type (space|folder|project|task|all)
|
|
71
|
+
- Optional: scope_id, channel_types[] (filter to specific channels), include_completed
|
|
72
|
+
- The report is automatically generated from current PM data and sent through YOUR channel bindings ONLY
|
|
73
|
+
- schedule_report: Create a recurring/scheduled report
|
|
74
|
+
- Required: scope_type, recurrence_type (hourly|daily|weekly|monthly)
|
|
75
|
+
- Optional: scope_id, scope_label, channel_types[], recurrence_time (HH:MM UTC, default 09:00), recurrence_interval, recurrence_days_of_week (e.g. "mon,wed,fri"), include_completed
|
|
76
|
+
- list_schedules: View all your active report schedules
|
|
77
|
+
- delete_schedule: Remove a scheduled report by schedule_id`,
|
|
69
78
|
|
|
70
79
|
OFIERE_MEMORY_OPS: `- **OFIERE_MEMORY_OPS** — Conversation history & knowledge memory (action: "list_conversations", "get_messages", "search_messages", "add_knowledge", "search_knowledge")
|
|
71
80
|
- list_conversations: Recent chats, filter by agent_id
|
|
@@ -107,6 +116,20 @@ const TOOL_DOCS: Record<string, string> = {
|
|
|
107
116
|
- delete_folder: Remove folder + all nested files recursively. Required: folder_id
|
|
108
117
|
- share_file / unshare_file: Toggle shared status. Required: file_id
|
|
109
118
|
- Files created here appear in the PM Space Files tab immediately`,
|
|
119
|
+
|
|
120
|
+
OFIERE_PLAN_OPS: `- **OFIERE_PLAN_OPS** — Visual execution plan builder (action: "list", "get", "create", "update", "delete", "add_nodes", "execute")
|
|
121
|
+
- list: All saved plans. Optional: space_id filter
|
|
122
|
+
- get: Full plan with complete node tree. Required: plan_id
|
|
123
|
+
- create: New plan. Required: name. Optional: description, space_id, nodes[] (initial tree)
|
|
124
|
+
- update: Modify plan. Required: plan_id. Optional: name, description, nodes[] (full tree replace)
|
|
125
|
+
- delete: Remove plan. Required: plan_id
|
|
126
|
+
- add_nodes: Add nodes to existing plan. Required: plan_id, nodes[]. Optional: parent_node_id (null = add as root)
|
|
127
|
+
- execute: Deploy plan into real PM tasks/folders/dependencies. Required: plan_id. Optional: create_folder (default true), create_scheduler (default true), space_id, folder_id
|
|
128
|
+
- Node types: task, gate, milestone
|
|
129
|
+
- Node fields: type, title, description, agent_id, priority (0-3), status (PENDING/IN_PROGRESS/DONE/FAILED), start_date, due_date, tags[], execution_steps[{text}], goals[{label, type?}], constraints[{label, type?}], system_prompt, children[], parallel (boolean)
|
|
130
|
+
- Plans are visual DAG drafts — they don't become real tasks until you call "execute"
|
|
131
|
+
- Execution maps ALL node fields into real tasks: execution_steps → custom_fields.execution_plan, goals → custom_fields.goals, constraints → custom_fields.constraints, system_prompt → custom_fields.system_prompt
|
|
132
|
+
- The user can see and edit your plans in the Planning Tab of the dashboard in real-time`,
|
|
110
133
|
};
|
|
111
134
|
|
|
112
135
|
export function getSystemPrompt(state: {
|
|
@@ -165,6 +188,13 @@ ${toolDocs}
|
|
|
165
188
|
- When task instructions or system prompts contain file references like @[filename](file:FILE_ID), use OFIERE_FILE_OPS action:"read_text_file" file_id:"FILE_ID" to read the file content. Do NOT ask the user for the file — retrieve it yourself.
|
|
166
189
|
- Use OFIERE_FILE_OPS to create output files (reports, data, configs) in the Space Files explorer. Prefer create_text_file for text-based outputs.
|
|
167
190
|
- To save task output as a file, call OFIERE_FILE_OPS action:"create_text_file" with the space_id from the task context.
|
|
191
|
+
- CHANNEL REPORTS: When the user asks you to "send a report", "send progress", "update me on Telegram/Discord/Slack/WhatsApp", use OFIERE_NOTIFY_OPS action:"send_report". The report is generated from live PM data and sent through YOUR connected channels ONLY — not other agents' channels.
|
|
192
|
+
- To set up recurring reports (e.g. "send me a daily report at 9am"), use OFIERE_NOTIFY_OPS action:"schedule_report" with scope_type and recurrence_type.
|
|
193
|
+
- If a report is too long for the channel's message limit, save the full report as a markdown file using OFIERE_FILE_OPS action:"create_text_file" and send a summary to the channel instead.
|
|
194
|
+
- PLANNING WORKFLOW: Use OFIERE_PLAN_OPS to build complex multi-step execution flows BEFORE creating individual tasks. Build the plan → let the user review in the Planning Tab → execute when approved.
|
|
195
|
+
- When creating a plan with nodes, nest children inside each node's children[] array. Sequential children execute in order; set parallel: true on a parent node to fork its children into parallel branches.
|
|
196
|
+
- Always call OFIERE_PLAN_OPS action:"get" before action:"add_nodes" or action:"update" to see the current tree structure and node IDs.
|
|
197
|
+
- Execution ("execute") maps plan nodes 1:1 into real PM tasks with ALL enrichment fields preserved: execution_steps, goals, constraints, system_prompt. No data is lost in the handoff.
|
|
168
198
|
</ofiere-pm>`;
|
|
169
199
|
}
|
|
170
200
|
|
package/src/tools.ts
CHANGED
|
@@ -2144,24 +2144,51 @@ function registerNotifyOps(
|
|
|
2144
2144
|
supabase: SupabaseClient,
|
|
2145
2145
|
userId: string,
|
|
2146
2146
|
): void {
|
|
2147
|
+
// Helper: resolve the agent's own ID for agent-scoped operations
|
|
2148
|
+
const resolveAgentId = (): string => {
|
|
2149
|
+
try {
|
|
2150
|
+
const state = api.getState?.() || {};
|
|
2151
|
+
return state.agentId || process.env.OFIERE_AGENT_ID || "";
|
|
2152
|
+
} catch { return process.env.OFIERE_AGENT_ID || ""; }
|
|
2153
|
+
};
|
|
2154
|
+
|
|
2147
2155
|
api.registerTool({
|
|
2148
2156
|
name: "OFIERE_NOTIFY_OPS",
|
|
2149
|
-
label: "Ofiere Notification Operations",
|
|
2157
|
+
label: "Ofiere Notification & Report Operations",
|
|
2150
2158
|
description:
|
|
2151
|
-
`Read and
|
|
2159
|
+
`Read/manage notifications and send PM reports to your connected channels.\n\n` +
|
|
2152
2160
|
`Actions:\n` +
|
|
2153
2161
|
`- "list": List notifications. Optional: unread_only (true/false), limit\n` +
|
|
2154
2162
|
`- "mark_read": Mark one as read. Required: id\n` +
|
|
2155
2163
|
`- "mark_all_read": Mark all as read\n` +
|
|
2156
|
-
`- "delete": Delete a notification. Required: id
|
|
2164
|
+
`- "delete": Delete a notification. Required: id\n` +
|
|
2165
|
+
`- "send_report": Send a PM progress report to YOUR connected channels (Telegram, Discord, etc.)\n` +
|
|
2166
|
+
` Required: scope_type (space|folder|project|task|all)\n` +
|
|
2167
|
+
` Optional: scope_id, channel_types[] (filter specific channels), include_completed (default true)\n` +
|
|
2168
|
+
`- "schedule_report": Create a recurring/scheduled report\n` +
|
|
2169
|
+
` Required: scope_type, recurrence_type (hourly|daily|weekly|monthly)\n` +
|
|
2170
|
+
` Optional: scope_id, scope_label, channel_types[], recurrence_time (HH:MM UTC, default 09:00),\n` +
|
|
2171
|
+
` recurrence_interval (default 1), recurrence_days_of_week (e.g. "mon,wed,fri"), include_completed\n` +
|
|
2172
|
+
`- "list_schedules": List your active report schedules\n` +
|
|
2173
|
+
`- "delete_schedule": Delete a scheduled report. Required: schedule_id`,
|
|
2157
2174
|
parameters: {
|
|
2158
2175
|
type: "object",
|
|
2159
2176
|
required: ["action"],
|
|
2160
2177
|
properties: {
|
|
2161
|
-
action: { type: "string", enum: ["list", "mark_read", "mark_all_read", "delete"] },
|
|
2178
|
+
action: { type: "string", enum: ["list", "mark_read", "mark_all_read", "delete", "send_report", "schedule_report", "list_schedules", "delete_schedule"] },
|
|
2162
2179
|
id: { type: "string", description: "Notification ID" },
|
|
2180
|
+
schedule_id: { type: "string", description: "Schedule ID (for delete_schedule)" },
|
|
2163
2181
|
unread_only: { type: "boolean", description: "Only show unread" },
|
|
2164
2182
|
limit: { type: "number", description: "Max results (default 50)" },
|
|
2183
|
+
scope_type: { type: "string", enum: ["space", "folder", "project", "task", "all"], description: "Report scope" },
|
|
2184
|
+
scope_id: { type: "string", description: "ID of the space/folder/project/task" },
|
|
2185
|
+
scope_label: { type: "string", description: "Human-readable label for the scope" },
|
|
2186
|
+
channel_types: { type: "array", items: { type: "string" }, description: "Filter to specific channels (e.g. ['telegram'])" },
|
|
2187
|
+
include_completed: { type: "boolean", description: "Include completed tasks in report" },
|
|
2188
|
+
recurrence_type: { type: "string", enum: ["hourly", "daily", "weekly", "monthly"], description: "How often to send" },
|
|
2189
|
+
recurrence_interval: { type: "number", description: "Interval multiplier (default 1)" },
|
|
2190
|
+
recurrence_time: { type: "string", description: "Time in HH:MM UTC (default 09:00)" },
|
|
2191
|
+
recurrence_days_of_week: { type: "string", description: "Comma-separated days for weekly (e.g. mon,wed,fri)" },
|
|
2165
2192
|
},
|
|
2166
2193
|
},
|
|
2167
2194
|
async execute(_id: string, params: Record<string, unknown>) {
|
|
@@ -2194,13 +2221,267 @@ function registerNotifyOps(
|
|
|
2194
2221
|
if (error) return err(error.message);
|
|
2195
2222
|
return ok({ message: "Notification deleted", ok: true });
|
|
2196
2223
|
}
|
|
2224
|
+
|
|
2225
|
+
// ── SEND REPORT — Dispatch PM report to agent's channels ──
|
|
2226
|
+
case "send_report": {
|
|
2227
|
+
const scopeType = (params.scope_type as string) || "all";
|
|
2228
|
+
const scopeId = params.scope_id as string | undefined;
|
|
2229
|
+
const channelTypes = (params.channel_types as string[]) || [];
|
|
2230
|
+
const includeCompleted = params.include_completed !== false;
|
|
2231
|
+
const agentId = resolveAgentId();
|
|
2232
|
+
if (!agentId) return err("Cannot resolve your agent ID. Ensure OFIERE_AGENT_ID is configured.");
|
|
2233
|
+
|
|
2234
|
+
// Call the dashboard API to generate + dispatch
|
|
2235
|
+
const supabaseUrl = (supabase as any).supabaseUrl || process.env.OFIERE_SUPABASE_URL || process.env.SUPABASE_URL || "";
|
|
2236
|
+
const serviceRoleKey = (supabase as any).supabaseKey || process.env.OFIERE_SERVICE_ROLE_KEY || process.env.SUPABASE_SERVICE_ROLE_KEY || "";
|
|
2237
|
+
|
|
2238
|
+
// Generate report directly from DB (we have service-role access)
|
|
2239
|
+
const reportResult = await generatePMReportDirect(supabase, userId, scopeType, scopeId, agentId, includeCompleted);
|
|
2240
|
+
|
|
2241
|
+
// Dispatch to channels
|
|
2242
|
+
const delivery = await dispatchReportDirect(supabase, userId, agentId, reportResult.report, channelTypes);
|
|
2243
|
+
|
|
2244
|
+
// Create audit notification
|
|
2245
|
+
await supabase.from("notifications").insert({
|
|
2246
|
+
id: crypto.randomUUID(),
|
|
2247
|
+
user_id: userId,
|
|
2248
|
+
title: `Report sent: ${reportResult.scopeLabel}`,
|
|
2249
|
+
message: `Dispatched via ${delivery.sent.join(", ") || "no channels"}. ${reportResult.taskCount} tasks.`,
|
|
2250
|
+
type: "system",
|
|
2251
|
+
category: "project_management",
|
|
2252
|
+
read: false,
|
|
2253
|
+
});
|
|
2254
|
+
|
|
2255
|
+
return ok({
|
|
2256
|
+
message: `Report sent to ${delivery.sent.length} channel(s)`,
|
|
2257
|
+
channels_sent: delivery.sent,
|
|
2258
|
+
channels_failed: delivery.failed,
|
|
2259
|
+
task_count: reportResult.taskCount,
|
|
2260
|
+
scope: reportResult.scopeLabel,
|
|
2261
|
+
report_preview: reportResult.report.slice(0, 500),
|
|
2262
|
+
});
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2265
|
+
// ── SCHEDULE REPORT — Create recurring report ──
|
|
2266
|
+
case "schedule_report": {
|
|
2267
|
+
const scopeType = (params.scope_type as string) || "all";
|
|
2268
|
+
const recurrenceType = (params.recurrence_type as string) || "daily";
|
|
2269
|
+
const agentId = resolveAgentId();
|
|
2270
|
+
if (!agentId) return err("Cannot resolve your agent ID.");
|
|
2271
|
+
|
|
2272
|
+
const recurrenceTime = (params.recurrence_time as string) || "09:00";
|
|
2273
|
+
const recurrenceInterval = (params.recurrence_interval as number) || 1;
|
|
2274
|
+
const recurrenceDaysOfWeek = (params.recurrence_days_of_week as string) || null;
|
|
2275
|
+
|
|
2276
|
+
// Compute next run
|
|
2277
|
+
const [hours, minutes] = recurrenceTime.split(":").map(Number);
|
|
2278
|
+
const next = new Date();
|
|
2279
|
+
next.setUTCHours(hours, minutes, 0, 0);
|
|
2280
|
+
if (next.getTime() <= Date.now()) next.setUTCDate(next.getUTCDate() + 1);
|
|
2281
|
+
|
|
2282
|
+
const { data, error } = await supabase.from("pm_report_schedules").insert({
|
|
2283
|
+
user_id: userId,
|
|
2284
|
+
agent_id: agentId,
|
|
2285
|
+
scope_type: scopeType,
|
|
2286
|
+
scope_id: (params.scope_id as string) || null,
|
|
2287
|
+
scope_label: (params.scope_label as string) || null,
|
|
2288
|
+
channel_types: (params.channel_types as string[]) || [],
|
|
2289
|
+
recurrence_type: recurrenceType,
|
|
2290
|
+
recurrence_interval: recurrenceInterval,
|
|
2291
|
+
recurrence_time: recurrenceTime,
|
|
2292
|
+
recurrence_days_of_week: recurrenceDaysOfWeek,
|
|
2293
|
+
include_completed: params.include_completed !== false,
|
|
2294
|
+
next_run_at: next.toISOString(),
|
|
2295
|
+
}).select().single();
|
|
2296
|
+
|
|
2297
|
+
if (error) return err(error.message);
|
|
2298
|
+
return ok({
|
|
2299
|
+
message: `Scheduled ${recurrenceType} report created`,
|
|
2300
|
+
schedule_id: data.id,
|
|
2301
|
+
next_run: next.toISOString(),
|
|
2302
|
+
recurrence: `${recurrenceType} at ${recurrenceTime} UTC`,
|
|
2303
|
+
});
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2306
|
+
// ── LIST SCHEDULES ──
|
|
2307
|
+
case "list_schedules": {
|
|
2308
|
+
const { data, error } = await supabase.from("pm_report_schedules")
|
|
2309
|
+
.select("*")
|
|
2310
|
+
.eq("user_id", userId)
|
|
2311
|
+
.order("created_at", { ascending: false });
|
|
2312
|
+
if (error) return err(error.message);
|
|
2313
|
+
return ok({ schedules: data || [], count: (data || []).length });
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
// ── DELETE SCHEDULE ──
|
|
2317
|
+
case "delete_schedule": {
|
|
2318
|
+
const scheduleId = (params.schedule_id || params.id) as string;
|
|
2319
|
+
if (!scheduleId) return err("Missing required: schedule_id");
|
|
2320
|
+
const { error } = await supabase.from("pm_report_schedules")
|
|
2321
|
+
.delete().eq("id", scheduleId).eq("user_id", userId);
|
|
2322
|
+
if (error) return err(error.message);
|
|
2323
|
+
return ok({ message: "Schedule deleted", ok: true });
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2197
2326
|
default:
|
|
2198
|
-
return err(`Unknown action "${action}"
|
|
2327
|
+
return err(`Unknown action "${action}". Valid: list, mark_read, mark_all_read, delete, send_report, schedule_report, list_schedules, delete_schedule`);
|
|
2199
2328
|
}
|
|
2200
2329
|
},
|
|
2201
2330
|
});
|
|
2202
2331
|
}
|
|
2203
2332
|
|
|
2333
|
+
// ── Inline report generation for plugin (avoids HTTP roundtrip) ──
|
|
2334
|
+
|
|
2335
|
+
async function generatePMReportDirect(
|
|
2336
|
+
supabase: SupabaseClient,
|
|
2337
|
+
userId: string,
|
|
2338
|
+
scopeType: string,
|
|
2339
|
+
scopeId: string | undefined,
|
|
2340
|
+
agentId: string,
|
|
2341
|
+
includeCompleted: boolean,
|
|
2342
|
+
): Promise<{ report: string; scopeLabel: string; taskCount: number }> {
|
|
2343
|
+
let scopeLabel = "All Projects";
|
|
2344
|
+
let query = supabase.from("tasks").select("id, title, status, priority, progress, completed_at, due_date")
|
|
2345
|
+
.eq("user_id", userId).is("parent_task_id", null);
|
|
2346
|
+
|
|
2347
|
+
if (scopeType === "space" && scopeId) {
|
|
2348
|
+
query = query.eq("space_id", scopeId);
|
|
2349
|
+
const { data: s } = await supabase.from("pm_spaces").select("name").eq("id", scopeId).single();
|
|
2350
|
+
scopeLabel = s?.name || "Space";
|
|
2351
|
+
} else if (scopeType === "folder" && scopeId) {
|
|
2352
|
+
query = query.eq("folder_id", scopeId);
|
|
2353
|
+
const { data: f } = await supabase.from("pm_folders").select("name").eq("id", scopeId).single();
|
|
2354
|
+
scopeLabel = f?.name || "Folder";
|
|
2355
|
+
} else if (scopeType === "project" && scopeId) {
|
|
2356
|
+
query = query.eq("project_id", scopeId);
|
|
2357
|
+
const { data: p } = await supabase.from("pm_folders").select("name").eq("id", scopeId).single();
|
|
2358
|
+
scopeLabel = p?.name || "Project";
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
const { data: tasks } = await query.order("priority", { ascending: false }).limit(40);
|
|
2362
|
+
const all = tasks || [];
|
|
2363
|
+
|
|
2364
|
+
// Agent name
|
|
2365
|
+
let agentName = agentId;
|
|
2366
|
+
const { data: agentRow } = await supabase.from("agents").select("name, codename")
|
|
2367
|
+
.eq("user_id", userId)
|
|
2368
|
+
.or(`id.eq.${agentId},name.ilike.${agentId},codename.ilike.${agentId}`)
|
|
2369
|
+
.limit(1).single();
|
|
2370
|
+
if (agentRow) agentName = agentRow.name || agentRow.codename || agentId;
|
|
2371
|
+
|
|
2372
|
+
const completed = all.filter((t: any) => t.status === "DONE");
|
|
2373
|
+
const inProgress = all.filter((t: any) => t.status === "IN_PROGRESS" || t.status === "RUNNING");
|
|
2374
|
+
const pending = all.filter((t: any) => t.status === "PENDING" || t.status === "NEW");
|
|
2375
|
+
const failed = all.filter((t: any) => t.status === "FAILED");
|
|
2376
|
+
|
|
2377
|
+
const lines: string[] = [];
|
|
2378
|
+
lines.push(`📊 Project Report — ${scopeLabel}`);
|
|
2379
|
+
lines.push(`Agent: ${agentName}`);
|
|
2380
|
+
lines.push("");
|
|
2381
|
+
lines.push("━━━ Summary ━━━");
|
|
2382
|
+
lines.push(`✅ ${completed.length} | 🔄 ${inProgress.length} | ⏳ ${pending.length} | ❌ ${failed.length}`);
|
|
2383
|
+
|
|
2384
|
+
if (inProgress.length > 0) {
|
|
2385
|
+
lines.push("");
|
|
2386
|
+
lines.push("━━━ Active ━━━");
|
|
2387
|
+
for (const t of inProgress.slice(0, 8)) {
|
|
2388
|
+
const pl = t.priority === 3 ? "CRIT" : t.priority === 2 ? "HIGH" : t.priority === 1 ? "MED" : "LOW";
|
|
2389
|
+
lines.push(`🔄 [${pl}] ${t.title}${t.progress ? ` — ${t.progress}%` : ""}`);
|
|
2390
|
+
}
|
|
2391
|
+
if (inProgress.length > 8) lines.push(`... +${inProgress.length - 8} more`);
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
if (pending.length > 0) {
|
|
2395
|
+
lines.push("");
|
|
2396
|
+
lines.push("━━━ Pending ━━━");
|
|
2397
|
+
for (const t of pending.slice(0, 6)) {
|
|
2398
|
+
const pl = t.priority === 3 ? "CRIT" : t.priority === 2 ? "HIGH" : t.priority === 1 ? "MED" : "LOW";
|
|
2399
|
+
lines.push(`⏳ [${pl}] ${t.title}`);
|
|
2400
|
+
}
|
|
2401
|
+
if (pending.length > 6) lines.push(`... +${pending.length - 6} more`);
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2404
|
+
if (includeCompleted && completed.length > 0) {
|
|
2405
|
+
lines.push("");
|
|
2406
|
+
lines.push("━━━ Done ━━━");
|
|
2407
|
+
const recent = completed.filter((t: any) => t.completed_at)
|
|
2408
|
+
.sort((a: any, b: any) => new Date(b.completed_at).getTime() - new Date(a.completed_at).getTime())
|
|
2409
|
+
.slice(0, 5);
|
|
2410
|
+
for (const t of recent) {
|
|
2411
|
+
const diff = Date.now() - new Date(t.completed_at).getTime();
|
|
2412
|
+
const mins = Math.floor(diff / 60000);
|
|
2413
|
+
const ago = mins < 60 ? `${mins}m ago` : mins < 1440 ? `${Math.floor(mins / 60)}h ago` : `${Math.floor(mins / 1440)}d ago`;
|
|
2414
|
+
lines.push(`✅ ${t.title} (${ago})`);
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
|
|
2418
|
+
const now = new Date();
|
|
2419
|
+
lines.push("");
|
|
2420
|
+
lines.push(`Generated: ${now.toISOString().replace("T", " ").slice(0, 16)} UTC`);
|
|
2421
|
+
|
|
2422
|
+
return { report: lines.join("\n"), scopeLabel, taskCount: all.length };
|
|
2423
|
+
}
|
|
2424
|
+
|
|
2425
|
+
async function dispatchReportDirect(
|
|
2426
|
+
supabase: SupabaseClient,
|
|
2427
|
+
userId: string,
|
|
2428
|
+
agentId: string,
|
|
2429
|
+
report: string,
|
|
2430
|
+
channelTypes: string[],
|
|
2431
|
+
): Promise<{ sent: string[]; failed: string[] }> {
|
|
2432
|
+
// Query agent's active channel bindings
|
|
2433
|
+
let query = supabase.from("channel_bindings").select("channel_type, channel_account_id, channel_config")
|
|
2434
|
+
.eq("user_id", userId)
|
|
2435
|
+
.eq("agent_id", agentId)
|
|
2436
|
+
.eq("notifications_enabled", true)
|
|
2437
|
+
.eq("is_active", true);
|
|
2438
|
+
|
|
2439
|
+
if (channelTypes.length > 0) {
|
|
2440
|
+
query = query.in("channel_type", channelTypes);
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
const { data: bindings } = await query;
|
|
2444
|
+
if (!bindings || bindings.length === 0) {
|
|
2445
|
+
return { sent: [], failed: ["No active channel bindings found for this agent"] };
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
const LIMITS: Record<string, number> = { telegram: 4096, discord: 2000, slack: 40000, whatsapp: 65536 };
|
|
2449
|
+
const sent: string[] = [];
|
|
2450
|
+
const failed: string[] = [];
|
|
2451
|
+
|
|
2452
|
+
for (const b of bindings) {
|
|
2453
|
+
const limit = LIMITS[b.channel_type] || 4000;
|
|
2454
|
+
let msg = report;
|
|
2455
|
+
if (msg.length > limit) {
|
|
2456
|
+
msg = msg.substring(0, limit - 80) + "\n\n... [Full report on dashboard]";
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
// Use the Ofiere dashboard API to send via gateway
|
|
2460
|
+
// The dashboard URL is derived from the Supabase URL
|
|
2461
|
+
const dashboardUrl = process.env.OFIERE_DASHBOARD_URL || "https://ofiere.com";
|
|
2462
|
+
const dispatchSecret = process.env.DISPATCH_SECRET || "";
|
|
2463
|
+
|
|
2464
|
+
try {
|
|
2465
|
+
const res = await fetch(`${dashboardUrl}/api/channels/gateway-send`, {
|
|
2466
|
+
method: "POST",
|
|
2467
|
+
headers: { "Content-Type": "application/json", "x-dispatch-secret": dispatchSecret },
|
|
2468
|
+
body: JSON.stringify({
|
|
2469
|
+
userId,
|
|
2470
|
+
channelType: b.channel_type,
|
|
2471
|
+
accountId: b.channel_account_id || null,
|
|
2472
|
+
message: msg,
|
|
2473
|
+
}),
|
|
2474
|
+
});
|
|
2475
|
+
if (res.ok) sent.push(b.channel_type);
|
|
2476
|
+
else failed.push(`${b.channel_type}: HTTP ${res.status}`);
|
|
2477
|
+
} catch (e: any) {
|
|
2478
|
+
failed.push(`${b.channel_type}: ${e.message || "fetch error"}`);
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2482
|
+
return { sent, failed };
|
|
2483
|
+
}
|
|
2484
|
+
|
|
2204
2485
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
2205
2486
|
// META-TOOL 8: OFIERE_MEMORY_OPS — Conversations & Knowledge Fragments
|
|
2206
2487
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -3763,6 +4044,417 @@ async function handleShareFile(
|
|
|
3763
4044
|
} catch (e) { return err(e instanceof Error ? e.message : String(e)); }
|
|
3764
4045
|
}
|
|
3765
4046
|
|
|
4047
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
4048
|
+
// META-TOOL 12: OFIERE_PLAN_OPS — Execution Plan Builder
|
|
4049
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
4050
|
+
|
|
4051
|
+
function registerPlanOps(
|
|
4052
|
+
api: any,
|
|
4053
|
+
supabase: SupabaseClient,
|
|
4054
|
+
userId: string,
|
|
4055
|
+
resolveAgent: (id?: string) => Promise<string | null>,
|
|
4056
|
+
): void {
|
|
4057
|
+
api.registerTool({
|
|
4058
|
+
name: "OFIERE_PLAN_OPS",
|
|
4059
|
+
label: "Ofiere Plan Operations",
|
|
4060
|
+
description:
|
|
4061
|
+
`Build and deploy visual execution plans (DAGs) in the Ofiere Planning Tab.\n\n` +
|
|
4062
|
+
`Actions:\n` +
|
|
4063
|
+
`- "list": List saved plans. Optional: space_id\n` +
|
|
4064
|
+
`- "get": Get full plan with node tree. Required: plan_id\n` +
|
|
4065
|
+
`- "create": New plan. Required: name. Optional: description, space_id, nodes[]\n` +
|
|
4066
|
+
`- "update": Modify plan. Required: plan_id. Optional: name, description, nodes[]\n` +
|
|
4067
|
+
`- "delete": Remove plan. Required: plan_id\n` +
|
|
4068
|
+
`- "add_nodes": Add nodes to plan. Required: plan_id, nodes[]. Optional: parent_node_id\n` +
|
|
4069
|
+
`- "execute": Deploy plan to real PM tasks. Required: plan_id. Optional: create_folder, create_scheduler, space_id, folder_id\n\n` +
|
|
4070
|
+
`Node structure: { type, title, description?, agent_id?, priority?, status?, start_date?, due_date?, tags?, execution_steps?[{text}], goals?[{label,type?}], constraints?[{label,type?}], system_prompt?, children?[], parallel? }`,
|
|
4071
|
+
parameters: {
|
|
4072
|
+
type: "object",
|
|
4073
|
+
required: ["action"],
|
|
4074
|
+
properties: {
|
|
4075
|
+
action: { type: "string", enum: ["list", "get", "create", "update", "delete", "add_nodes", "execute"] },
|
|
4076
|
+
plan_id: { type: "string", description: "Plan ID (required for get, update, delete, add_nodes, execute)" },
|
|
4077
|
+
name: { type: "string", description: "Plan name (required for create)" },
|
|
4078
|
+
description: { type: "string", description: "Plan description" },
|
|
4079
|
+
space_id: { type: "string", description: "PM Space ID" },
|
|
4080
|
+
folder_id: { type: "string", description: "PM Folder ID (for execute)" },
|
|
4081
|
+
parent_node_id: { type: "string", description: "Parent node ID to attach new nodes to (add_nodes). Null = root level" },
|
|
4082
|
+
create_folder: { type: "boolean", description: "Create a project folder on execute (default true)" },
|
|
4083
|
+
create_scheduler: { type: "boolean", description: "Create scheduler events for dated tasks on execute (default true)" },
|
|
4084
|
+
nodes: {
|
|
4085
|
+
type: "array",
|
|
4086
|
+
description: "Array of plan nodes. Each node can have children[] for nesting.",
|
|
4087
|
+
items: {
|
|
4088
|
+
type: "object",
|
|
4089
|
+
properties: {
|
|
4090
|
+
type: { type: "string", enum: ["task", "gate", "milestone"] },
|
|
4091
|
+
title: { type: "string" },
|
|
4092
|
+
description: { type: "string" },
|
|
4093
|
+
agent_id: { type: "string" },
|
|
4094
|
+
priority: { type: "number" },
|
|
4095
|
+
status: { type: "string", enum: ["PENDING", "IN_PROGRESS", "DONE", "FAILED"] },
|
|
4096
|
+
start_date: { type: "string" },
|
|
4097
|
+
due_date: { type: "string" },
|
|
4098
|
+
tags: { type: "array", items: { type: "string" } },
|
|
4099
|
+
execution_steps: { type: "array", items: { type: "object", properties: { text: { type: "string" } }, required: ["text"] } },
|
|
4100
|
+
goals: { type: "array", items: { type: "object", properties: { label: { type: "string" }, type: { type: "string" } }, required: ["label"] } },
|
|
4101
|
+
constraints: { type: "array", items: { type: "object", properties: { label: { type: "string" }, type: { type: "string" } }, required: ["label"] } },
|
|
4102
|
+
system_prompt: { type: "string" },
|
|
4103
|
+
parallel: { type: "boolean" },
|
|
4104
|
+
children: { type: "array", description: "Nested child nodes (recursive)" },
|
|
4105
|
+
},
|
|
4106
|
+
required: ["title"],
|
|
4107
|
+
},
|
|
4108
|
+
},
|
|
4109
|
+
},
|
|
4110
|
+
},
|
|
4111
|
+
async execute(_id: string, params: Record<string, unknown>) {
|
|
4112
|
+
const action = params.action as string;
|
|
4113
|
+
switch (action) {
|
|
4114
|
+
case "list": return handleListPlans(supabase, userId, params);
|
|
4115
|
+
case "get": return handleGetPlan(supabase, userId, params);
|
|
4116
|
+
case "create": return handleCreatePlan(supabase, userId, params);
|
|
4117
|
+
case "update": return handleUpdatePlan(supabase, userId, params);
|
|
4118
|
+
case "delete": return handleDeletePlan(supabase, userId, params);
|
|
4119
|
+
case "add_nodes": return handleAddNodes(supabase, userId, params);
|
|
4120
|
+
case "execute": return handleExecutePlan(supabase, userId, resolveAgent, params);
|
|
4121
|
+
default: return err(`Unknown action "${action}". Valid: list, get, create, update, delete, add_nodes, execute`);
|
|
4122
|
+
}
|
|
4123
|
+
},
|
|
4124
|
+
});
|
|
4125
|
+
}
|
|
4126
|
+
|
|
4127
|
+
// ── Plan helpers ─────────────────────────────────────────────────────────────
|
|
4128
|
+
|
|
4129
|
+
function generatePlanNodeId(): string {
|
|
4130
|
+
return `pn-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
|
|
4131
|
+
}
|
|
4132
|
+
|
|
4133
|
+
function generatePlanId(): string {
|
|
4134
|
+
return `plan-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
|
|
4135
|
+
}
|
|
4136
|
+
|
|
4137
|
+
/** Normalize agent-provided node into the PlanNode shape stored in plan_data */
|
|
4138
|
+
function normalizeNode(raw: any): any {
|
|
4139
|
+
const id = raw.id || generatePlanNodeId();
|
|
4140
|
+
const children = Array.isArray(raw.children) ? raw.children.map(normalizeNode) : [];
|
|
4141
|
+
return {
|
|
4142
|
+
id,
|
|
4143
|
+
type: raw.type || "task",
|
|
4144
|
+
title: raw.title || "Untitled",
|
|
4145
|
+
description: raw.description || undefined,
|
|
4146
|
+
agentId: raw.agent_id || raw.agentId || undefined,
|
|
4147
|
+
assignees: raw.agent_id ? [{ id: raw.agent_id, type: "agent" }] : undefined,
|
|
4148
|
+
priority: raw.priority ?? 1,
|
|
4149
|
+
status: raw.status || "PENDING",
|
|
4150
|
+
startDate: raw.start_date || raw.startDate || undefined,
|
|
4151
|
+
dueDate: raw.due_date || raw.dueDate || undefined,
|
|
4152
|
+
tags: raw.tags || [],
|
|
4153
|
+
executionSteps: Array.isArray(raw.execution_steps)
|
|
4154
|
+
? raw.execution_steps.map((s: any, i: number) => ({ id: `step-${Date.now()}-${i}`, text: typeof s === "string" ? s : s.text || String(s), order: i }))
|
|
4155
|
+
: undefined,
|
|
4156
|
+
goals: Array.isArray(raw.goals)
|
|
4157
|
+
? raw.goals.map((g: any, i: number) => ({ id: `goal-${Date.now()}-${i}`, type: g.type || "custom", label: typeof g === "string" ? g : g.label || String(g) }))
|
|
4158
|
+
: undefined,
|
|
4159
|
+
constraints: Array.isArray(raw.constraints)
|
|
4160
|
+
? raw.constraints.map((c: any, i: number) => ({ id: `cstr-${Date.now()}-${i}`, type: c.type || "custom", label: typeof c === "string" ? c : c.label || String(c) }))
|
|
4161
|
+
: undefined,
|
|
4162
|
+
systemPrompt: raw.system_prompt || raw.systemPrompt || undefined,
|
|
4163
|
+
gateCondition: raw.gate_condition || raw.gateCondition || (raw.type === "gate" ? "all_predecessors_complete" : undefined),
|
|
4164
|
+
children,
|
|
4165
|
+
parallel: raw.parallel || false,
|
|
4166
|
+
collapsed: false,
|
|
4167
|
+
};
|
|
4168
|
+
}
|
|
4169
|
+
|
|
4170
|
+
/** Count all nodes in a tree (BFS) */
|
|
4171
|
+
function countAllNodes(nodes: any[]): number {
|
|
4172
|
+
let count = 0;
|
|
4173
|
+
const queue = [...nodes];
|
|
4174
|
+
while (queue.length > 0) {
|
|
4175
|
+
const n = queue.shift()!;
|
|
4176
|
+
count++;
|
|
4177
|
+
if (Array.isArray(n.children)) queue.push(...n.children);
|
|
4178
|
+
}
|
|
4179
|
+
return count;
|
|
4180
|
+
}
|
|
4181
|
+
|
|
4182
|
+
/** Collect unique agent IDs from tree */
|
|
4183
|
+
function collectAgents(nodes: any[]): string[] {
|
|
4184
|
+
const ids = new Set<string>();
|
|
4185
|
+
const queue = [...nodes];
|
|
4186
|
+
while (queue.length > 0) {
|
|
4187
|
+
const n = queue.shift()!;
|
|
4188
|
+
if (n.agentId) ids.add(n.agentId);
|
|
4189
|
+
if (Array.isArray(n.assignees)) n.assignees.forEach((a: any) => { if (a.id) ids.add(a.id); });
|
|
4190
|
+
if (Array.isArray(n.children)) queue.push(...n.children);
|
|
4191
|
+
}
|
|
4192
|
+
return Array.from(ids);
|
|
4193
|
+
}
|
|
4194
|
+
|
|
4195
|
+
/** Build a full ExecutionPlan JSON from metadata + nodes */
|
|
4196
|
+
function buildPlanJson(id: string, name: string, description: string | undefined, rootNodes: any[]): string {
|
|
4197
|
+
const plan = {
|
|
4198
|
+
id,
|
|
4199
|
+
name,
|
|
4200
|
+
description,
|
|
4201
|
+
rootNodes,
|
|
4202
|
+
createdAt: new Date().toISOString(),
|
|
4203
|
+
updatedAt: new Date().toISOString(),
|
|
4204
|
+
metadata: { totalTasks: countAllNodes(rootNodes), totalAgents: collectAgents(rootNodes) },
|
|
4205
|
+
};
|
|
4206
|
+
return JSON.stringify(plan, null, 2);
|
|
4207
|
+
}
|
|
4208
|
+
|
|
4209
|
+
/** Find a node by ID in a tree */
|
|
4210
|
+
function findNodeInTree(nodes: any[], nodeId: string): any | null {
|
|
4211
|
+
for (const n of nodes) {
|
|
4212
|
+
if (n.id === nodeId) return n;
|
|
4213
|
+
if (Array.isArray(n.children)) {
|
|
4214
|
+
const found = findNodeInTree(n.children, nodeId);
|
|
4215
|
+
if (found) return found;
|
|
4216
|
+
}
|
|
4217
|
+
}
|
|
4218
|
+
return null;
|
|
4219
|
+
}
|
|
4220
|
+
|
|
4221
|
+
// ── Plan action handlers ─────────────────────────────────────────────────────
|
|
4222
|
+
|
|
4223
|
+
async function handleListPlans(supabase: SupabaseClient, userId: string, params: Record<string, unknown>): Promise<ToolResult> {
|
|
4224
|
+
try {
|
|
4225
|
+
let query = supabase.from("pm_plans").select("id, name, description, space_id, is_deployed, deployed_at, created_at, updated_at").eq("user_id", userId).order("updated_at", { ascending: false });
|
|
4226
|
+
if (params.space_id) query = query.eq("space_id", params.space_id as string);
|
|
4227
|
+
const { data, error } = await query;
|
|
4228
|
+
if (error) return err(error.message);
|
|
4229
|
+
return ok({ plans: data || [], count: (data || []).length });
|
|
4230
|
+
} catch (e) { return err(e instanceof Error ? e.message : String(e)); }
|
|
4231
|
+
}
|
|
4232
|
+
|
|
4233
|
+
async function handleGetPlan(supabase: SupabaseClient, userId: string, params: Record<string, unknown>): Promise<ToolResult> {
|
|
4234
|
+
try {
|
|
4235
|
+
if (!params.plan_id) return err("Missing required field: plan_id");
|
|
4236
|
+
const { data, error } = await supabase.from("pm_plans").select("*").eq("id", params.plan_id as string).eq("user_id", userId).single();
|
|
4237
|
+
if (error) return err(error.message);
|
|
4238
|
+
let parsed: any = null;
|
|
4239
|
+
try { parsed = JSON.parse(data.plan_data || "{}"); } catch { parsed = {}; }
|
|
4240
|
+
return ok({ plan: { ...data, plan_data: parsed } });
|
|
4241
|
+
} catch (e) { return err(e instanceof Error ? e.message : String(e)); }
|
|
4242
|
+
}
|
|
4243
|
+
|
|
4244
|
+
async function handleCreatePlan(supabase: SupabaseClient, userId: string, params: Record<string, unknown>): Promise<ToolResult> {
|
|
4245
|
+
try {
|
|
4246
|
+
if (!params.name) return err("Missing required field: name");
|
|
4247
|
+
const planId = generatePlanId();
|
|
4248
|
+
const rootNodes = Array.isArray(params.nodes) ? (params.nodes as any[]).map(normalizeNode) : [];
|
|
4249
|
+
const planData = buildPlanJson(planId, params.name as string, params.description as string | undefined, rootNodes);
|
|
4250
|
+
const row = {
|
|
4251
|
+
id: planId, user_id: userId, space_id: (params.space_id as string) || null,
|
|
4252
|
+
name: params.name as string, description: (params.description as string) || null,
|
|
4253
|
+
plan_data: planData, is_deployed: false, deployed_at: null, updated_at: new Date().toISOString(),
|
|
4254
|
+
};
|
|
4255
|
+
const { error } = await supabase.from("pm_plans").insert(row);
|
|
4256
|
+
if (error) return err(error.message);
|
|
4257
|
+
return ok({ id: planId, message: `Plan "${params.name}" created with ${countAllNodes(rootNodes)} nodes`, node_count: countAllNodes(rootNodes) });
|
|
4258
|
+
} catch (e) { return err(e instanceof Error ? e.message : String(e)); }
|
|
4259
|
+
}
|
|
4260
|
+
|
|
4261
|
+
async function handleUpdatePlan(supabase: SupabaseClient, userId: string, params: Record<string, unknown>): Promise<ToolResult> {
|
|
4262
|
+
try {
|
|
4263
|
+
if (!params.plan_id) return err("Missing required field: plan_id");
|
|
4264
|
+
const updates: Record<string, any> = { updated_at: new Date().toISOString() };
|
|
4265
|
+
if (params.name !== undefined) updates.name = params.name;
|
|
4266
|
+
if (params.description !== undefined) updates.description = params.description;
|
|
4267
|
+
if (params.nodes !== undefined) {
|
|
4268
|
+
const rootNodes = Array.isArray(params.nodes) ? (params.nodes as any[]).map(normalizeNode) : [];
|
|
4269
|
+
updates.plan_data = buildPlanJson(params.plan_id as string, (params.name as string) || "Plan", params.description as string | undefined, rootNodes);
|
|
4270
|
+
}
|
|
4271
|
+
const { error } = await supabase.from("pm_plans").update(updates).eq("id", params.plan_id as string).eq("user_id", userId);
|
|
4272
|
+
if (error) return err(error.message);
|
|
4273
|
+
return ok({ message: `Plan updated` });
|
|
4274
|
+
} catch (e) { return err(e instanceof Error ? e.message : String(e)); }
|
|
4275
|
+
}
|
|
4276
|
+
|
|
4277
|
+
async function handleDeletePlan(supabase: SupabaseClient, userId: string, params: Record<string, unknown>): Promise<ToolResult> {
|
|
4278
|
+
try {
|
|
4279
|
+
if (!params.plan_id) return err("Missing required field: plan_id");
|
|
4280
|
+
const { error } = await supabase.from("pm_plans").delete().eq("id", params.plan_id as string).eq("user_id", userId);
|
|
4281
|
+
if (error) return err(error.message);
|
|
4282
|
+
return ok({ message: `Plan deleted`, deleted: true });
|
|
4283
|
+
} catch (e) { return err(e instanceof Error ? e.message : String(e)); }
|
|
4284
|
+
}
|
|
4285
|
+
|
|
4286
|
+
async function handleAddNodes(supabase: SupabaseClient, userId: string, params: Record<string, unknown>): Promise<ToolResult> {
|
|
4287
|
+
try {
|
|
4288
|
+
if (!params.plan_id) return err("Missing required field: plan_id");
|
|
4289
|
+
if (!params.nodes || !Array.isArray(params.nodes)) return err("Missing required field: nodes[]");
|
|
4290
|
+
const { data, error: fetchErr } = await supabase.from("pm_plans").select("plan_data, name").eq("id", params.plan_id as string).eq("user_id", userId).single();
|
|
4291
|
+
if (fetchErr || !data) return err("Plan not found");
|
|
4292
|
+
let plan: any;
|
|
4293
|
+
try { plan = JSON.parse(data.plan_data || "{}"); } catch { return err("Invalid plan data"); }
|
|
4294
|
+
const newNodes = (params.nodes as any[]).map(normalizeNode);
|
|
4295
|
+
const parentId = params.parent_node_id as string | undefined;
|
|
4296
|
+
if (parentId) {
|
|
4297
|
+
const parent = findNodeInTree(plan.rootNodes || [], parentId);
|
|
4298
|
+
if (!parent) return err(`Parent node "${parentId}" not found in plan`);
|
|
4299
|
+
if (!Array.isArray(parent.children)) parent.children = [];
|
|
4300
|
+
parent.children.push(...newNodes);
|
|
4301
|
+
} else {
|
|
4302
|
+
if (!Array.isArray(plan.rootNodes)) plan.rootNodes = [];
|
|
4303
|
+
plan.rootNodes.push(...newNodes);
|
|
4304
|
+
}
|
|
4305
|
+
plan.updatedAt = new Date().toISOString();
|
|
4306
|
+
plan.metadata = { totalTasks: countAllNodes(plan.rootNodes), totalAgents: collectAgents(plan.rootNodes) };
|
|
4307
|
+
const { error } = await supabase.from("pm_plans").update({ plan_data: JSON.stringify(plan, null, 2), updated_at: new Date().toISOString() }).eq("id", params.plan_id as string).eq("user_id", userId);
|
|
4308
|
+
if (error) return err(error.message);
|
|
4309
|
+
return ok({ message: `Added ${newNodes.length} nodes${parentId ? ` under ${parentId}` : " as roots"}`, total_nodes: countAllNodes(plan.rootNodes) });
|
|
4310
|
+
} catch (e) { return err(e instanceof Error ? e.message : String(e)); }
|
|
4311
|
+
}
|
|
4312
|
+
|
|
4313
|
+
async function handleExecutePlan(
|
|
4314
|
+
supabase: SupabaseClient, userId: string,
|
|
4315
|
+
resolveAgent: (id?: string) => Promise<string | null>,
|
|
4316
|
+
params: Record<string, unknown>,
|
|
4317
|
+
): Promise<ToolResult> {
|
|
4318
|
+
try {
|
|
4319
|
+
if (!params.plan_id) return err("Missing required field: plan_id");
|
|
4320
|
+
const { data, error: fetchErr } = await supabase.from("pm_plans").select("*").eq("id", params.plan_id as string).eq("user_id", userId).single();
|
|
4321
|
+
if (fetchErr || !data) return err("Plan not found");
|
|
4322
|
+
let plan: any;
|
|
4323
|
+
try { plan = JSON.parse(data.plan_data || "{}"); } catch { return err("Invalid plan data"); }
|
|
4324
|
+
const rootNodes: any[] = plan.rootNodes || [];
|
|
4325
|
+
if (rootNodes.length === 0) return err("Plan has no nodes to execute");
|
|
4326
|
+
|
|
4327
|
+
const createFolder = params.create_folder !== false;
|
|
4328
|
+
const createScheduler = params.create_scheduler !== false;
|
|
4329
|
+
const spaceId = (params.space_id as string) || data.space_id || null;
|
|
4330
|
+
let folderId = (params.folder_id as string) || null;
|
|
4331
|
+
|
|
4332
|
+
// Step 1: Create project folder
|
|
4333
|
+
if (createFolder && spaceId) {
|
|
4334
|
+
const folderRow = {
|
|
4335
|
+
id: `folder-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
4336
|
+
user_id: userId, space_id: spaceId, name: plan.name || "Untitled Plan",
|
|
4337
|
+
type: "project", parent_folder_id: folderId, sort_order: 0,
|
|
4338
|
+
created_at: new Date().toISOString(), updated_at: new Date().toISOString(),
|
|
4339
|
+
};
|
|
4340
|
+
const { error: folderErr } = await supabase.from("pm_folders").insert(folderRow);
|
|
4341
|
+
if (!folderErr) folderId = folderRow.id;
|
|
4342
|
+
}
|
|
4343
|
+
|
|
4344
|
+
// Step 2: BFS — create tasks with full field mapping
|
|
4345
|
+
const idMap = new Map<string, string>();
|
|
4346
|
+
const queue: { node: any; parentTaskId: string | null }[] = rootNodes.map((n: any) => ({ node: n, parentTaskId: null }));
|
|
4347
|
+
let tasksCreated = 0;
|
|
4348
|
+
|
|
4349
|
+
while (queue.length > 0) {
|
|
4350
|
+
const { node, parentTaskId } = queue.shift()!;
|
|
4351
|
+
if (node.type === "task" || node.type === "milestone") {
|
|
4352
|
+
const taskId = `task-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
4353
|
+
const now = new Date().toISOString();
|
|
4354
|
+
const agentId = node.agentId ? await resolveAgent(node.agentId) : null;
|
|
4355
|
+
|
|
4356
|
+
// Build custom_fields — the seamless handoff
|
|
4357
|
+
const cf: Record<string, any> = { pm_only: true };
|
|
4358
|
+
if (node.executionSteps && node.executionSteps.length > 0) cf.execution_plan = node.executionSteps;
|
|
4359
|
+
if (node.goals && node.goals.length > 0) cf.goals = node.goals;
|
|
4360
|
+
if (node.constraints && node.constraints.length > 0) cf.constraints = node.constraints;
|
|
4361
|
+
if (node.systemPrompt) cf.system_prompt = node.systemPrompt;
|
|
4362
|
+
if (agentId) cf.assignees = [{ id: agentId, type: "agent" }];
|
|
4363
|
+
|
|
4364
|
+
const taskRow = {
|
|
4365
|
+
id: taskId, user_id: userId, title: node.title, description: node.description || null,
|
|
4366
|
+
agent_id: agentId, assignee_type: agentId ? "agent" : "auto",
|
|
4367
|
+
priority: node.priority ?? 1, status: node.status || "PENDING",
|
|
4368
|
+
start_date: node.startDate || null, due_date: node.dueDate || null,
|
|
4369
|
+
tags: node.tags || [], space_id: spaceId, folder_id: folderId,
|
|
4370
|
+
parent_task_id: parentTaskId, progress: 0, sort_order: 0,
|
|
4371
|
+
custom_fields: cf, created_at: now, updated_at: now,
|
|
4372
|
+
};
|
|
4373
|
+
|
|
4374
|
+
const { error: taskErr } = await supabase.from("tasks").insert(taskRow);
|
|
4375
|
+
if (!taskErr) {
|
|
4376
|
+
idMap.set(node.id, taskId);
|
|
4377
|
+
tasksCreated++;
|
|
4378
|
+
|
|
4379
|
+
// Scheduler event for dated tasks
|
|
4380
|
+
if (createScheduler && node.startDate && agentId) {
|
|
4381
|
+
try {
|
|
4382
|
+
const parsedDate = new Date(node.startDate);
|
|
4383
|
+
const scheduledDate = parsedDate.toISOString().split("T")[0];
|
|
4384
|
+
const hasTime = /[T ]\d{2}:\d{2}/.test(node.startDate);
|
|
4385
|
+
const scheduledTime = hasTime
|
|
4386
|
+
? `${String(parsedDate.getUTCHours()).padStart(2, "0")}:${String(parsedDate.getUTCMinutes()).padStart(2, "0")}`
|
|
4387
|
+
: "09:00";
|
|
4388
|
+
let nextRunAt = Math.floor(parsedDate.getTime() / 1000);
|
|
4389
|
+
if (nextRunAt <= Math.floor(Date.now() / 1000)) nextRunAt = Math.floor(Date.now() / 1000) + 60;
|
|
4390
|
+
|
|
4391
|
+
await supabase.from("scheduler_events").insert({
|
|
4392
|
+
id: crypto.randomUUID(), user_id: userId, task_id: taskId, agent_id: agentId,
|
|
4393
|
+
title: node.title, description: node.description || null,
|
|
4394
|
+
scheduled_date: scheduledDate, scheduled_time: scheduledTime,
|
|
4395
|
+
duration_minutes: 30, recurrence_type: "none", recurrence_interval: 1,
|
|
4396
|
+
status: "scheduled", next_run_at: nextRunAt, run_count: 0, priority: node.priority ?? 1,
|
|
4397
|
+
});
|
|
4398
|
+
} catch { /* scheduler is best-effort */ }
|
|
4399
|
+
}
|
|
4400
|
+
}
|
|
4401
|
+
}
|
|
4402
|
+
|
|
4403
|
+
// Enqueue children
|
|
4404
|
+
const realId = idMap.get(node.id) || null;
|
|
4405
|
+
if (Array.isArray(node.children)) {
|
|
4406
|
+
for (const child of node.children) {
|
|
4407
|
+
queue.push({ node: child, parentTaskId: node.parallel ? parentTaskId : realId });
|
|
4408
|
+
}
|
|
4409
|
+
}
|
|
4410
|
+
}
|
|
4411
|
+
|
|
4412
|
+
// Step 3: Create dependencies (sequential chain)
|
|
4413
|
+
const depsQueue = [...rootNodes];
|
|
4414
|
+
let depsCreated = 0;
|
|
4415
|
+
while (depsQueue.length > 0) {
|
|
4416
|
+
const node = depsQueue.shift()!;
|
|
4417
|
+
const nodeRealId = idMap.get(node.id);
|
|
4418
|
+
if (nodeRealId && !node.parallel && node.children?.length > 0) {
|
|
4419
|
+
const firstChildId = idMap.get(node.children[0].id);
|
|
4420
|
+
if (firstChildId) {
|
|
4421
|
+
await supabase.from("task_dependencies").insert({ user_id: userId, predecessor_id: nodeRealId, successor_id: firstChildId, dependency_type: "finish_to_start", lag_days: 0 });
|
|
4422
|
+
depsCreated++;
|
|
4423
|
+
}
|
|
4424
|
+
for (let i = 1; i < node.children.length; i++) {
|
|
4425
|
+
const prevId = idMap.get(node.children[i - 1].id);
|
|
4426
|
+
const currId = idMap.get(node.children[i].id);
|
|
4427
|
+
if (prevId && currId) {
|
|
4428
|
+
await supabase.from("task_dependencies").insert({ user_id: userId, predecessor_id: prevId, successor_id: currId, dependency_type: "finish_to_start", lag_days: 0 });
|
|
4429
|
+
depsCreated++;
|
|
4430
|
+
}
|
|
4431
|
+
}
|
|
4432
|
+
}
|
|
4433
|
+
if (nodeRealId && node.parallel && node.children?.length > 0) {
|
|
4434
|
+
for (const child of node.children) {
|
|
4435
|
+
const childId = idMap.get(child.id);
|
|
4436
|
+
if (childId) {
|
|
4437
|
+
await supabase.from("task_dependencies").insert({ user_id: userId, predecessor_id: nodeRealId, successor_id: childId, dependency_type: "finish_to_start", lag_days: 0 });
|
|
4438
|
+
depsCreated++;
|
|
4439
|
+
}
|
|
4440
|
+
}
|
|
4441
|
+
}
|
|
4442
|
+
if (Array.isArray(node.children)) depsQueue.push(...node.children);
|
|
4443
|
+
}
|
|
4444
|
+
|
|
4445
|
+
// Mark plan as deployed
|
|
4446
|
+
await supabase.from("pm_plans").update({ is_deployed: true, deployed_at: new Date().toISOString(), updated_at: new Date().toISOString() }).eq("id", params.plan_id as string).eq("user_id", userId);
|
|
4447
|
+
|
|
4448
|
+
return ok({
|
|
4449
|
+
message: `Plan "${plan.name}" executed successfully`,
|
|
4450
|
+
tasks_created: tasksCreated,
|
|
4451
|
+
dependencies_created: depsCreated,
|
|
4452
|
+
folder_id: folderId,
|
|
4453
|
+
space_id: spaceId,
|
|
4454
|
+
});
|
|
4455
|
+
} catch (e) { return err(e instanceof Error ? e.message : String(e)); }
|
|
4456
|
+
}
|
|
4457
|
+
|
|
3766
4458
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
3767
4459
|
// Public: Register All Meta-Tools
|
|
3768
4460
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -3791,9 +4483,10 @@ export function registerTools(
|
|
|
3791
4483
|
registerPromptOps(api, supabase, userId); // 9
|
|
3792
4484
|
registerConstellationOps(api, supabase, userId); // 10
|
|
3793
4485
|
registerFileOps(api, supabase, userId); // 11
|
|
4486
|
+
registerPlanOps(api, supabase, userId, resolveAgent); // 12
|
|
3794
4487
|
|
|
3795
4488
|
// ── Count and log ──
|
|
3796
|
-
const toolCount =
|
|
4489
|
+
const toolCount = 12;
|
|
3797
4490
|
const callerName = getCallingAgentName(api);
|
|
3798
4491
|
const agentLabel = fallbackAgentId || callerName || "auto-detect";
|
|
3799
4492
|
api.logger.info(`[ofiere] ${toolCount} meta-tools registered (agent: ${agentLabel})`);
|