ofiere-openclaw-plugin 4.17.0 → 4.18.1
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/package.json +2 -2
- package/src/prompt.ts +18 -0
- package/src/tools.ts +413 -1
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ofiere-openclaw-plugin",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.18.1",
|
|
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
|
@@ -116,6 +116,20 @@ const TOOL_DOCS: Record<string, string> = {
|
|
|
116
116
|
- delete_folder: Remove folder + all nested files recursively. Required: folder_id
|
|
117
117
|
- share_file / unshare_file: Toggle shared status. Required: file_id
|
|
118
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`,
|
|
119
133
|
};
|
|
120
134
|
|
|
121
135
|
export function getSystemPrompt(state: {
|
|
@@ -177,6 +191,10 @@ ${toolDocs}
|
|
|
177
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.
|
|
178
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.
|
|
179
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.
|
|
180
198
|
</ofiere-pm>`;
|
|
181
199
|
}
|
|
182
200
|
|
package/src/tools.ts
CHANGED
|
@@ -4044,6 +4044,417 @@ async function handleShareFile(
|
|
|
4044
4044
|
} catch (e) { return err(e instanceof Error ? e.message : String(e)); }
|
|
4045
4045
|
}
|
|
4046
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)", items: { type: "object", properties: { title: { type: "string" }, type: { type: "string", enum: ["task", "gate", "milestone"] }, description: { type: "string" }, agent_id: { type: "string" }, priority: { type: "number" }, status: { type: "string" }, start_date: { type: "string" }, due_date: { type: "string" }, parallel: { type: "boolean" } }, required: ["title"] } },
|
|
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
|
+
|
|
4047
4458
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
4048
4459
|
// Public: Register All Meta-Tools
|
|
4049
4460
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -4072,9 +4483,10 @@ export function registerTools(
|
|
|
4072
4483
|
registerPromptOps(api, supabase, userId); // 9
|
|
4073
4484
|
registerConstellationOps(api, supabase, userId); // 10
|
|
4074
4485
|
registerFileOps(api, supabase, userId); // 11
|
|
4486
|
+
registerPlanOps(api, supabase, userId, resolveAgent); // 12
|
|
4075
4487
|
|
|
4076
4488
|
// ── Count and log ──
|
|
4077
|
-
const toolCount =
|
|
4489
|
+
const toolCount = 12;
|
|
4078
4490
|
const callerName = getCallingAgentName(api);
|
|
4079
4491
|
const agentLabel = fallbackAgentId || callerName || "auto-detect";
|
|
4080
4492
|
api.logger.info(`[ofiere] ${toolCount} meta-tools registered (agent: ${agentLabel})`);
|