ofiere-openclaw-plugin 3.0.0 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -36
- package/package.json +1 -1
- package/src/prompt.ts +20 -10
- package/src/tools.ts +335 -68
package/README.md
CHANGED
|
@@ -1,42 +1,27 @@
|
|
|
1
1
|
# Ofiere PM Plugin for OpenClaw
|
|
2
2
|
|
|
3
|
-
Manage your Ofiere PM dashboard directly from OpenClaw agents. Create tasks,
|
|
3
|
+
Manage your Ofiere PM dashboard directly from OpenClaw agents. Create tasks, manage projects, build workflows, store knowledge — all synced to the dashboard in real time.
|
|
4
4
|
|
|
5
|
-
## Install
|
|
5
|
+
## Quick Install (One-Click)
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
|
|
8
|
+
curl -sSL https://raw.githubusercontent.com/gilanggemar/Ofiere/main/ofiere-openclaw-plugin/install.sh | bash -s -- \
|
|
9
|
+
--supabase-url "https://xxx.supabase.co" \
|
|
10
|
+
--service-key "eyJ..." \
|
|
11
|
+
--user-id "your-uuid"
|
|
9
12
|
```
|
|
10
13
|
|
|
11
|
-
|
|
14
|
+
Only 3 parameters needed. All agents get the plugin automatically.
|
|
12
15
|
|
|
13
|
-
|
|
14
|
-
openclaw plugins install ./ofiere-openclaw-plugin
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
Restart OpenClaw after installing.
|
|
18
|
-
|
|
19
|
-
## Setup
|
|
20
|
-
|
|
21
|
-
```bash
|
|
22
|
-
openclaw ofiere setup --supabase-url "https://xxx.supabase.co" --service-key "eyJ..." --user-id "your-uuid" --agent-id "sasha"
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
Or run interactively:
|
|
16
|
+
## Uninstall
|
|
26
17
|
|
|
27
18
|
```bash
|
|
28
|
-
|
|
19
|
+
curl -sSL https://raw.githubusercontent.com/gilanggemar/Ofiere/main/ofiere-openclaw-plugin/uninstall.sh | bash
|
|
29
20
|
```
|
|
30
21
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
```bash
|
|
34
|
-
openclaw gateway restart
|
|
35
|
-
```
|
|
22
|
+
## How It Works
|
|
36
23
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
Once configured, the plugin connects to your Supabase database at gateway startup and registers PM tools directly into the agent. There's no separate MCP server process — it runs inside the OpenClaw gateway for maximum reliability.
|
|
24
|
+
Once configured, the plugin connects to your Supabase database at gateway startup and registers 9 PM meta-tools directly into the agent. There's no separate MCP server process — it runs inside the OpenClaw gateway for maximum reliability.
|
|
40
25
|
|
|
41
26
|
Changes made by the agent are immediately visible on the Ofiere dashboard (Vercel) via Supabase real-time subscriptions.
|
|
42
27
|
|
|
@@ -46,20 +31,38 @@ The plugin uses a scalable meta-tool architecture. Each tool handles one domain
|
|
|
46
31
|
|
|
47
32
|
| Tool | Actions | Description |
|
|
48
33
|
|---|---|---|
|
|
49
|
-
| `OFIERE_TASK_OPS` | `list`, `create`, `update`, `delete` |
|
|
34
|
+
| `OFIERE_TASK_OPS` | `list`, `create`, `update`, `delete` | Rich task management with execution plans, goals, constraints |
|
|
50
35
|
| `OFIERE_AGENT_OPS` | `list` | Query available agents for task assignment |
|
|
36
|
+
| `OFIERE_PROJECT_OPS` | `list_spaces`, `create_space`, `create_folder`, `bulk_create_tasks`, etc. | Full project hierarchy: spaces → folders → tasks |
|
|
37
|
+
| `OFIERE_SCHEDULE_OPS` | `list`, `create`, `update`, `delete` | Calendar events with recurrence |
|
|
38
|
+
| `OFIERE_KNOWLEDGE_OPS` | `search`, `list`, `create`, `update`, `delete` | Knowledge library with full content retrieval |
|
|
39
|
+
| `OFIERE_WORKFLOW_OPS` | `list`, `get`, `create`, `update`, `delete`, `list_runs`, `trigger` | Visual workflow builder with 16 node types |
|
|
40
|
+
| `OFIERE_NOTIFY_OPS` | `list`, `mark_read`, `create` | In-app notifications |
|
|
41
|
+
| `OFIERE_MEMORY_OPS` | `list_conversations`, `search_knowledge` | Conversation history & agent memory |
|
|
42
|
+
| `OFIERE_PROMPT_OPS` | `list`, `get`, `create`, `update`, `delete` | System prompt chunk management |
|
|
51
43
|
|
|
52
44
|
### Example
|
|
53
45
|
|
|
54
46
|
```
|
|
55
|
-
// Create a task
|
|
56
|
-
OFIERE_TASK_OPS({ action: "create", title: "Deploy v2", agent_id: "ivy"
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
47
|
+
// Create a task with execution plan
|
|
48
|
+
OFIERE_TASK_OPS({ action: "create", title: "Deploy v2", agent_id: "ivy",
|
|
49
|
+
execution_plan: [{ step: 1, action: "Build", detail: "Run production build" }] })
|
|
50
|
+
|
|
51
|
+
// Create a workflow with nodes
|
|
52
|
+
OFIERE_WORKFLOW_OPS({ action: "create", name: "Deploy Pipeline",
|
|
53
|
+
nodes: [
|
|
54
|
+
{ type: "agent_step", data: { label: "Build", task: "Run npm build" } },
|
|
55
|
+
{ type: "human_approval", data: { label: "Review", instructions: "Check build output" } },
|
|
56
|
+
{ type: "output", data: { label: "Done" } }
|
|
57
|
+
],
|
|
58
|
+
edges: [
|
|
59
|
+
{ source: "agent_step-...", target: "human_approval-..." },
|
|
60
|
+
{ source: "human_approval-...", target: "output-..." }
|
|
61
|
+
]
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
// Search knowledge library
|
|
65
|
+
OFIERE_KNOWLEDGE_OPS({ action: "search", query: "API rate limits" })
|
|
63
66
|
```
|
|
64
67
|
|
|
65
68
|
## CLI Commands
|
|
@@ -79,7 +82,7 @@ Set via `openclaw ofiere setup` or environment variables:
|
|
|
79
82
|
| `supabaseUrl` | `OFIERE_SUPABASE_URL` | Supabase project URL |
|
|
80
83
|
| `serviceRoleKey` | `OFIERE_SERVICE_ROLE_KEY` | Supabase service role key |
|
|
81
84
|
| `userId` | `OFIERE_USER_ID` | Your user UUID |
|
|
82
|
-
| `agentId` | `OFIERE_AGENT_ID` | This agent's ID (
|
|
85
|
+
| `agentId` | `OFIERE_AGENT_ID` | This agent's ID (optional — auto-detected) |
|
|
83
86
|
| `enabled` | — | Enable/disable the plugin (default: `true`) |
|
|
84
87
|
|
|
85
88
|
## Architecture
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ofiere-openclaw-plugin",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "OpenClaw plugin for Ofiere PM — 9 meta-tools covering tasks, agents, projects, scheduling, knowledge, workflows, notifications, memory, and prompts",
|
|
6
6
|
"keywords": ["openclaw", "ofiere", "project-management", "agents", "plugin"],
|
package/src/prompt.ts
CHANGED
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
const TOOL_DOCS: Record<string, string> = {
|
|
12
12
|
OFIERE_TASK_OPS: `- **OFIERE_TASK_OPS** — Manage tasks (action: "list", "create", "update", "delete")
|
|
13
13
|
- list: Filter by status, agent_id, space_id, folder_id, limit. Returns execution_plan, goals, constraints if present
|
|
14
|
-
- create: Requires title.
|
|
14
|
+
- create: Requires title. IMPORTANT: Always pass agent_id with your own name to self-assign (e.g. agent_id: "celia")
|
|
15
|
+
- Optional: description, instructions, execution_plan, goals, constraints, system_prompt, priority, tags, dates
|
|
15
16
|
- For COMPLEX tasks: include execution_plan (step-by-step), goals, constraints, and system_prompt
|
|
16
17
|
- For SIMPLE tasks: just title and optionally description
|
|
17
18
|
- update: Requires task_id. All create fields + progress
|
|
@@ -32,16 +33,22 @@ const TOOL_DOCS: Record<string, string> = {
|
|
|
32
33
|
- create: Requires title + scheduled_date (YYYY-MM-DD). Optional: scheduled_time, duration_minutes, task_id, agent_id, recurrence_type, color
|
|
33
34
|
- Recurrence: none, hourly, daily, weekly, monthly with interval`,
|
|
34
35
|
|
|
35
|
-
OFIERE_KNOWLEDGE_OPS: `- **OFIERE_KNOWLEDGE_OPS** — Knowledge
|
|
36
|
-
-
|
|
37
|
-
-
|
|
38
|
-
-
|
|
36
|
+
OFIERE_KNOWLEDGE_OPS: `- **OFIERE_KNOWLEDGE_OPS** — Ofiere Knowledge Library (action: "search", "list", "create", "update", "delete")
|
|
37
|
+
- ALWAYS use this tool when the user mentions "knowledge base", "knowledge library", "knowledge entries", or asks to recall/retrieve stored knowledge
|
|
38
|
+
- search: Keyword search across all stored documents. Requires: query
|
|
39
|
+
- list: Paginated listing of knowledge entries with full content. Optional: search filter
|
|
40
|
+
- create: Add knowledge to the library. Requires: file_name. Optional: content, source, author, credibility_tier
|
|
39
41
|
- update/delete: By document ID`,
|
|
40
42
|
|
|
41
|
-
OFIERE_WORKFLOW_OPS: `- **OFIERE_WORKFLOW_OPS** — Automated workflows (action: "list", "get", "create", "list_runs", "trigger")
|
|
43
|
+
OFIERE_WORKFLOW_OPS: `- **OFIERE_WORKFLOW_OPS** — Automated workflows (action: "list", "get", "create", "update", "delete", "list_runs", "trigger")
|
|
42
44
|
- list: All workflows, filter by status (draft, active, paused, archived)
|
|
43
45
|
- get: Full workflow details by ID
|
|
44
|
-
- create: New workflow with name
|
|
46
|
+
- create: New workflow with name + nodes and edges
|
|
47
|
+
- Node types: manual_trigger, agent_step, http_request, formatter_step, task_call, variable_set, condition, human_approval, delay, loop, convergence, output, checkpoint, note
|
|
48
|
+
- Each node: { type, data: { label, ...type-specific fields } }. IDs/positions auto-generated
|
|
49
|
+
- Edges: { source, target }. A manual_trigger is auto-prepended if no trigger exists
|
|
50
|
+
- update: Modify workflow (name, description, status, nodes, edges)
|
|
51
|
+
- delete: Remove workflow and all run history
|
|
45
52
|
- list_runs: Recent execution history for a workflow
|
|
46
53
|
- trigger: Start a workflow run (creates a run record)`,
|
|
47
54
|
|
|
@@ -58,9 +65,10 @@ const TOOL_DOCS: Record<string, string> = {
|
|
|
58
65
|
- search_knowledge: Search stored knowledge for an agent`,
|
|
59
66
|
|
|
60
67
|
OFIERE_PROMPT_OPS: `- **OFIERE_PROMPT_OPS** — Manage prompt instruction chunks (action: "list", "get", "create", "update", "delete")
|
|
61
|
-
- list: All prompt chunks
|
|
62
|
-
- create: New chunk with
|
|
63
|
-
- update: Change
|
|
68
|
+
- list: All prompt chunks ordered by display order
|
|
69
|
+
- create: New chunk with name (max 30 chars) + content. Optional: color (hex), category
|
|
70
|
+
- update: Change name, content, color, category, or order
|
|
71
|
+
- delete: Remove a chunk by ID
|
|
64
72
|
- All modifications are logged for audit`,
|
|
65
73
|
};
|
|
66
74
|
|
|
@@ -93,6 +101,7 @@ Each tool uses an "action" parameter to select the operation. Always include act
|
|
|
93
101
|
${toolDocs}
|
|
94
102
|
|
|
95
103
|
## Rules
|
|
104
|
+
- ALWAYS pass agent_id with your own name when creating tasks (e.g. agent_id: "ivy"). Auto-detection is NOT reliable.
|
|
96
105
|
- ${assignRule}
|
|
97
106
|
- To create an unassigned task, pass agent_id as "none" or "unassigned".
|
|
98
107
|
- 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.
|
|
@@ -104,6 +113,7 @@ ${toolDocs}
|
|
|
104
113
|
- For complex tasks, ALWAYS include execution_plan, goals, and constraints. For simple tasks, just title is enough.
|
|
105
114
|
- When creating dependencies, use OFIERE_PROJECT_OPS to link predecessor/successor tasks.
|
|
106
115
|
- Prompt chunk modifications (OFIERE_PROMPT_OPS) are powerful — use thoughtfully as they change agent behavior.
|
|
116
|
+
- When the user asks about "knowledge base", "knowledge library", "knowledge entries", or wants to recall stored knowledge, ALWAYS use OFIERE_KNOWLEDGE_OPS — do NOT rely on your own memory for this.
|
|
107
117
|
</ofiere-pm>`;
|
|
108
118
|
}
|
|
109
119
|
|
package/src/tools.ts
CHANGED
|
@@ -106,6 +106,17 @@ export function probeApiForAgentName(api: any, logger?: any): string {
|
|
|
106
106
|
|
|
107
107
|
// ─── Shared: Agent ID Resolution ─────────────────────────────────────────────
|
|
108
108
|
|
|
109
|
+
// System/plugin names that should never be treated as real agent identifiers.
|
|
110
|
+
// These come from the OpenClaw gateway registration context, not from actual agents.
|
|
111
|
+
const SYSTEM_NAME_BLOCKLIST = new Set([
|
|
112
|
+
"ofiere pm", "ofiere", "openclaw", "system", "plugin", "gateway", "admin",
|
|
113
|
+
"ofiere pm plugin", "ofiere-openclaw-plugin",
|
|
114
|
+
]);
|
|
115
|
+
|
|
116
|
+
function isSystemName(name: string): boolean {
|
|
117
|
+
return SYSTEM_NAME_BLOCKLIST.has(name.toLowerCase().trim());
|
|
118
|
+
}
|
|
119
|
+
|
|
109
120
|
function createAgentResolver(
|
|
110
121
|
api: any,
|
|
111
122
|
supabase: SupabaseClient,
|
|
@@ -114,47 +125,38 @@ function createAgentResolver(
|
|
|
114
125
|
) {
|
|
115
126
|
/**
|
|
116
127
|
* Resolve the agent ID for the calling agent.
|
|
117
|
-
* Priority: explicit param >
|
|
128
|
+
* Priority: explicit param > env var > DB fallback
|
|
129
|
+
*
|
|
130
|
+
* NOTE: We intentionally skip runtime/registration detection because the
|
|
131
|
+
* OpenClaw api object returns the PLUGIN name ("Ofiere PM"), not the
|
|
132
|
+
* calling agent's name. Each agent must pass its own name via agent_id.
|
|
118
133
|
*/
|
|
119
134
|
return async function resolveAgent(explicitId?: string): Promise<string | null> {
|
|
120
|
-
// 1. Explicit agent_id passed by the LLM (e.g. "ivy", "
|
|
135
|
+
// 1. Explicit agent_id passed by the LLM (e.g. "ivy", "celia", or a UUID)
|
|
121
136
|
if (explicitId && explicitId.trim()) {
|
|
122
137
|
const trimmed = explicitId.trim();
|
|
123
|
-
// If it looks like a UUID or our ID format, use directly
|
|
124
|
-
if (trimmed.match(/^[0-9a-f]{8}-/) || trimmed.match(/^agent-/)) {
|
|
125
|
-
return trimmed;
|
|
126
|
-
}
|
|
127
|
-
// Otherwise treat as a name and resolve to the actual agent ID
|
|
128
|
-
try {
|
|
129
|
-
return await resolveAgentId(trimmed, userId, supabase);
|
|
130
|
-
} catch {
|
|
131
|
-
return trimmed; // fallback: use as-is
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
138
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
} catch {
|
|
150
|
-
// Fall through
|
|
139
|
+
// Block system names from being used as agent IDs
|
|
140
|
+
if (isSystemName(trimmed)) {
|
|
141
|
+
// Fall through to DB fallback
|
|
142
|
+
} else if (trimmed.match(/^[0-9a-f]{8}-/) || trimmed.match(/^agent-/)) {
|
|
143
|
+
// Looks like a UUID or our ID format — use directly
|
|
144
|
+
return trimmed;
|
|
145
|
+
} else {
|
|
146
|
+
// Treat as a name and resolve to the actual agent ID
|
|
147
|
+
try {
|
|
148
|
+
const resolved = await resolveAgentId(trimmed, userId, supabase);
|
|
149
|
+
if (resolved && !isSystemName(resolved)) return resolved;
|
|
150
|
+
} catch {
|
|
151
|
+
// Fall through
|
|
152
|
+
}
|
|
151
153
|
}
|
|
152
154
|
}
|
|
153
155
|
|
|
154
|
-
//
|
|
156
|
+
// 2. Env var fallback (OFIERE_AGENT_ID — legacy single-agent mode)
|
|
155
157
|
if (fallbackAgentId) return fallbackAgentId;
|
|
156
158
|
|
|
157
|
-
//
|
|
159
|
+
// 3. Nuclear fallback: query the FIRST agent for this user
|
|
158
160
|
try {
|
|
159
161
|
const { data } = await supabase
|
|
160
162
|
.from("agents")
|
|
@@ -343,7 +345,7 @@ async function handleCreateTask(
|
|
|
343
345
|
try {
|
|
344
346
|
if (!params.title) return err("Missing required field: title");
|
|
345
347
|
|
|
346
|
-
const id = `task-${Date.now()}`;
|
|
348
|
+
const id = `task-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
347
349
|
const now = new Date().toISOString();
|
|
348
350
|
|
|
349
351
|
// Handle explicit "none"/"unassigned"
|
|
@@ -421,17 +423,98 @@ async function handleCreateTask(
|
|
|
421
423
|
return err(error.message);
|
|
422
424
|
}
|
|
423
425
|
|
|
426
|
+
// ── Auto-create scheduler event if task has a start_date ──────────────
|
|
427
|
+
// This bridges the plugin → scheduler so the pg_cron task-dispatcher
|
|
428
|
+
// Edge Function picks up the task at the right time.
|
|
429
|
+
const startDate = params.start_date as string | undefined;
|
|
430
|
+
const effectiveAgentId = (insertData.agent_id as string) || assignee;
|
|
431
|
+
if (startDate && effectiveAgentId) {
|
|
432
|
+
try {
|
|
433
|
+
// Parse start_date robustly — it can be:
|
|
434
|
+
// "2026-04-19" (date only)
|
|
435
|
+
// "2026-04-19T18:45:00" (local datetime)
|
|
436
|
+
// "2026-04-19 11:45:00+00" (Supabase timestamptz)
|
|
437
|
+
// "2026-04-19T11:45:00.000Z" (ISO UTC)
|
|
438
|
+
const parsedDate = new Date(startDate);
|
|
439
|
+
const explicitScheduledTime = params.scheduled_time as string | undefined;
|
|
440
|
+
|
|
441
|
+
let nextRunAtEpoch: number;
|
|
442
|
+
let scheduledTimeFinal: string;
|
|
443
|
+
let scheduledDateFinal: string;
|
|
444
|
+
|
|
445
|
+
if (!isNaN(parsedDate.getTime())) {
|
|
446
|
+
// Valid date — check if it includes a meaningful time component
|
|
447
|
+
const hasTimeInfo = /[T ]\d{2}:\d{2}/.test(startDate);
|
|
448
|
+
|
|
449
|
+
if (explicitScheduledTime) {
|
|
450
|
+
// Agent explicitly passed a scheduled_time — use date from start_date + explicit time
|
|
451
|
+
const dateStr = parsedDate.toISOString().split("T")[0]; // YYYY-MM-DD
|
|
452
|
+
const dt = new Date(`${dateStr}T${explicitScheduledTime}:00Z`);
|
|
453
|
+
nextRunAtEpoch = Math.floor(dt.getTime() / 1000);
|
|
454
|
+
scheduledTimeFinal = explicitScheduledTime;
|
|
455
|
+
scheduledDateFinal = dateStr;
|
|
456
|
+
} else if (hasTimeInfo) {
|
|
457
|
+
// start_date already contains time — use it directly
|
|
458
|
+
nextRunAtEpoch = Math.floor(parsedDate.getTime() / 1000);
|
|
459
|
+
scheduledTimeFinal = `${String(parsedDate.getUTCHours()).padStart(2, "0")}:${String(parsedDate.getUTCMinutes()).padStart(2, "0")}`;
|
|
460
|
+
scheduledDateFinal = parsedDate.toISOString().split("T")[0];
|
|
461
|
+
} else {
|
|
462
|
+
// Date only, no time — default to 09:00 UTC
|
|
463
|
+
const dateStr = parsedDate.toISOString().split("T")[0];
|
|
464
|
+
const dt = new Date(`${dateStr}T09:00:00Z`);
|
|
465
|
+
nextRunAtEpoch = Math.floor(dt.getTime() / 1000);
|
|
466
|
+
scheduledTimeFinal = "09:00";
|
|
467
|
+
scheduledDateFinal = dateStr;
|
|
468
|
+
}
|
|
469
|
+
} else {
|
|
470
|
+
// Unparseable date — fallback to now + 60s
|
|
471
|
+
nextRunAtEpoch = Math.floor(Date.now() / 1000) + 60;
|
|
472
|
+
scheduledTimeFinal = "00:00";
|
|
473
|
+
scheduledDateFinal = new Date().toISOString().split("T")[0];
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Safety net: if computed time is in the past, schedule for now + 60s
|
|
477
|
+
const nowEpoch = Math.floor(Date.now() / 1000);
|
|
478
|
+
if (nextRunAtEpoch <= nowEpoch) {
|
|
479
|
+
nextRunAtEpoch = nowEpoch + 60;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
await supabase.from("scheduler_events").insert({
|
|
483
|
+
id: crypto.randomUUID(),
|
|
484
|
+
user_id: userId,
|
|
485
|
+
task_id: id,
|
|
486
|
+
agent_id: effectiveAgentId,
|
|
487
|
+
title: params.title,
|
|
488
|
+
description: (params.description as string) || (params.instructions as string) || null,
|
|
489
|
+
scheduled_date: scheduledDateFinal,
|
|
490
|
+
scheduled_time: scheduledTimeFinal,
|
|
491
|
+
duration_minutes: 30,
|
|
492
|
+
recurrence_type: "none",
|
|
493
|
+
recurrence_interval: 1,
|
|
494
|
+
status: "scheduled",
|
|
495
|
+
next_run_at: nextRunAtEpoch,
|
|
496
|
+
run_count: 0,
|
|
497
|
+
priority: params.priority !== undefined ? params.priority : 1,
|
|
498
|
+
});
|
|
499
|
+
} catch (schedErr) {
|
|
500
|
+
// Non-fatal: task was created, just the scheduler event failed
|
|
501
|
+
console.error("[ofiere] Failed to auto-create scheduler event:", schedErr);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
424
505
|
const extras = [];
|
|
425
506
|
if (cf.execution_plan) extras.push(`${(cf.execution_plan as any[]).length} execution steps`);
|
|
426
507
|
if (cf.goals) extras.push(`${(cf.goals as any[]).length} goals`);
|
|
427
508
|
if (cf.constraints) extras.push(`${(cf.constraints as any[]).length} constraints`);
|
|
428
509
|
if (cf.system_prompt) extras.push("custom system prompt");
|
|
510
|
+
if (startDate) extras.push(`scheduled for ${startDate}`);
|
|
429
511
|
const extrasStr = extras.length > 0 ? ` with ${extras.join(", ")}` : "";
|
|
430
512
|
|
|
431
513
|
return ok({
|
|
432
514
|
id,
|
|
433
515
|
message: `Task "${params.title}" created and assigned to ${assignee || "no one"}${extrasStr}`,
|
|
434
516
|
task: insertData,
|
|
517
|
+
scheduledExecution: startDate ? `Will auto-execute on ${startDate}` : undefined,
|
|
435
518
|
});
|
|
436
519
|
} catch (e) {
|
|
437
520
|
return err(e instanceof Error ? e.message : String(e));
|
|
@@ -516,7 +599,7 @@ async function handleUpdateTask(
|
|
|
516
599
|
.update(updates)
|
|
517
600
|
.eq("id", params.task_id as string)
|
|
518
601
|
.eq("user_id", userId)
|
|
519
|
-
.select("id, title, status, priority, agent_id")
|
|
602
|
+
.select("id, title, status, priority, agent_id, start_date, due_date, progress, updated_at")
|
|
520
603
|
.single();
|
|
521
604
|
|
|
522
605
|
if (error) return err(error.message);
|
|
@@ -934,11 +1017,12 @@ function registerKnowledgeOps(
|
|
|
934
1017
|
name: "OFIERE_KNOWLEDGE_OPS",
|
|
935
1018
|
label: "Ofiere Knowledge Operations",
|
|
936
1019
|
description:
|
|
937
|
-
`
|
|
1020
|
+
`Access the Ofiere Knowledge Library — the stored knowledge base in the dashboard. ` +
|
|
1021
|
+
`Use this tool whenever the user mentions "knowledge base", "knowledge library", "knowledge entries", or asks to retrieve stored knowledge.\n\n` +
|
|
938
1022
|
`Actions:\n` +
|
|
939
|
-
`- "search": Search
|
|
940
|
-
`- "list": List recent
|
|
941
|
-
`- "create": Add a document. Required: file_name. Optional: content, text, source, source_type, author, credibility_tier\n` +
|
|
1023
|
+
`- "search": Search the knowledge library by keyword. Required: query. Optional: limit\n` +
|
|
1024
|
+
`- "list": List recent entries from the knowledge library. Optional: page, page_size, search\n` +
|
|
1025
|
+
`- "create": Add a document to the knowledge library. Required: file_name. Optional: content, text, source, source_type, author, credibility_tier\n` +
|
|
942
1026
|
`- "update": Edit a document. Required: id. Optional: file_name, content, text, source, source_type, author\n` +
|
|
943
1027
|
`- "delete": Remove a document. Required: id`,
|
|
944
1028
|
parameters: {
|
|
@@ -983,7 +1067,7 @@ function registerKnowledgeOps(
|
|
|
983
1067
|
const from = (page - 1) * pageSize;
|
|
984
1068
|
const to = from + pageSize - 1;
|
|
985
1069
|
let q = supabase.from("knowledge_documents")
|
|
986
|
-
.select("id, file_name, file_type, source, source_type, author, credibility_tier, created_at", { count: "exact" })
|
|
1070
|
+
.select("id, file_name, file_type, content, text, source, source_type, author, credibility_tier, created_at", { count: "exact" })
|
|
987
1071
|
.order("created_at", { ascending: false })
|
|
988
1072
|
.range(from, to);
|
|
989
1073
|
if (params.search) {
|
|
@@ -1050,23 +1134,75 @@ function registerWorkflowOps(
|
|
|
1050
1134
|
name: "OFIERE_WORKFLOW_OPS",
|
|
1051
1135
|
label: "Ofiere Workflow Operations",
|
|
1052
1136
|
description:
|
|
1053
|
-
`Manage and trigger automated workflows.\n\n` +
|
|
1137
|
+
`Manage, build, and trigger automated workflows in the Ofiere dashboard.\n\n` +
|
|
1054
1138
|
`Actions:\n` +
|
|
1055
1139
|
`- "list": List all workflows. Optional: status\n` +
|
|
1056
1140
|
`- "get": Get workflow details. Required: id\n` +
|
|
1057
|
-
`- "create": Create a workflow. Required: name. Optional: description,
|
|
1141
|
+
`- "create": Create a workflow WITH nodes and edges. Required: name. Optional: description, nodes, edges, schedule, status\n` +
|
|
1142
|
+
`- "update": Update a workflow. Required: id. Optional: name, description, status, nodes, edges, schedule\n` +
|
|
1143
|
+
`- "delete": Delete a workflow and its run history. Required: id\n` +
|
|
1058
1144
|
`- "list_runs": List recent runs. Required: workflow_id. Optional: limit\n` +
|
|
1059
|
-
`- "trigger": Start a workflow run. Required: workflow_id
|
|
1145
|
+
`- "trigger": Start a workflow run. Required: workflow_id\n\n` +
|
|
1146
|
+
`NODE TYPES (use these exact types when creating nodes):\n` +
|
|
1147
|
+
` TRIGGERS (start of workflow — pick one):\n` +
|
|
1148
|
+
` - "manual_trigger": User clicks Execute to start\n` +
|
|
1149
|
+
` - "webhook_trigger": External HTTP request triggers it\n` +
|
|
1150
|
+
` - "schedule_trigger": Runs on cron schedule. data: { label, cron: "0 9 * * 1-5" }\n` +
|
|
1151
|
+
` STEPS (the work):\n` +
|
|
1152
|
+
` - "agent_step": Delegates task to an AI agent. data: { label, agentId, task, responseMode: "text", timeoutSec: 120 }\n` +
|
|
1153
|
+
` - "http_request": Calls an external API. data: { label, method: "GET"|"POST", url }\n` +
|
|
1154
|
+
` - "formatter_step": Formats/transforms text or JSON. data: { label, template }\n` +
|
|
1155
|
+
` - "task_call": Runs a saved task. data: { label, agentId, taskId }\n` +
|
|
1156
|
+
` - "variable_set": Stores data in a variable. data: { label, variableName, variableValue }\n` +
|
|
1157
|
+
` CONTROL FLOW:\n` +
|
|
1158
|
+
` - "condition": If/else branch. data: { label, expression }\n` +
|
|
1159
|
+
` - "human_approval": Pauses for human approval. data: { label, instructions }\n` +
|
|
1160
|
+
` - "delay": Waits for a set time. data: { label, delaySec: 5 }\n` +
|
|
1161
|
+
` - "loop": Repeats actions. data: { label, loopType: "count", maxIterations: 3 }\n` +
|
|
1162
|
+
` - "convergence": Waits for multiple parallel inputs. data: { label, mergeStrategy: "wait_all" }\n` +
|
|
1163
|
+
` END:\n` +
|
|
1164
|
+
` - "output": Returns final result. data: { label, outputMode: "return" }\n` +
|
|
1165
|
+
` SPECIAL:\n` +
|
|
1166
|
+
` - "checkpoint": Loop target marker. data: { label }\n` +
|
|
1167
|
+
` - "note": Sticky note annotation. data: { label, noteText }\n\n` +
|
|
1168
|
+
`Each node: { type, data: { label, ... }, position?: { x, y } }. IDs and positions are auto-generated if omitted.\n` +
|
|
1169
|
+
`Each edge: { source: "node_id", target: "node_id" }. IDs auto-generated.\n` +
|
|
1170
|
+
`A manual_trigger node is always auto-prepended if no trigger node is included.`,
|
|
1060
1171
|
parameters: {
|
|
1061
1172
|
type: "object",
|
|
1062
1173
|
required: ["action"],
|
|
1063
1174
|
properties: {
|
|
1064
|
-
action: { type: "string", enum: ["list", "get", "create", "list_runs", "trigger"] },
|
|
1175
|
+
action: { type: "string", enum: ["list", "get", "create", "update", "delete", "list_runs", "trigger"] },
|
|
1065
1176
|
id: { type: "string", description: "Workflow ID" },
|
|
1066
1177
|
workflow_id: { type: "string", description: "Workflow ID for runs/trigger" },
|
|
1067
1178
|
name: { type: "string", description: "Workflow name" },
|
|
1068
1179
|
description: { type: "string" },
|
|
1069
|
-
|
|
1180
|
+
nodes: {
|
|
1181
|
+
type: "array",
|
|
1182
|
+
items: {
|
|
1183
|
+
type: "object",
|
|
1184
|
+
properties: {
|
|
1185
|
+
id: { type: "string", description: "Node ID (auto-generated if omitted)" },
|
|
1186
|
+
type: { type: "string", enum: ["manual_trigger", "webhook_trigger", "schedule_trigger", "agent_step", "formatter_step", "http_request", "task_call", "variable_set", "condition", "human_approval", "delay", "loop", "convergence", "output", "checkpoint", "note"] },
|
|
1187
|
+
position: { type: "object", properties: { x: { type: "number" }, y: { type: "number" } } },
|
|
1188
|
+
data: { type: "object", description: "Node config — always include a 'label' field. See NODE TYPES above for type-specific fields." },
|
|
1189
|
+
},
|
|
1190
|
+
},
|
|
1191
|
+
description: "Workflow graph nodes",
|
|
1192
|
+
},
|
|
1193
|
+
edges: {
|
|
1194
|
+
type: "array",
|
|
1195
|
+
items: {
|
|
1196
|
+
type: "object",
|
|
1197
|
+
properties: {
|
|
1198
|
+
id: { type: "string", description: "Edge ID (auto-generated if omitted)" },
|
|
1199
|
+
source: { type: "string", description: "Source node ID" },
|
|
1200
|
+
target: { type: "string", description: "Target node ID" },
|
|
1201
|
+
},
|
|
1202
|
+
},
|
|
1203
|
+
description: "Connections between nodes. Each edge: { source, target }",
|
|
1204
|
+
},
|
|
1205
|
+
steps: { type: "array", items: { type: "object" }, description: "Legacy V1 step definitions" },
|
|
1070
1206
|
schedule: { type: "string", description: "Cron expression or schedule" },
|
|
1071
1207
|
status: { type: "string", enum: ["draft", "active", "paused", "archived"] },
|
|
1072
1208
|
limit: { type: "number", description: "Max results" },
|
|
@@ -1074,6 +1210,43 @@ function registerWorkflowOps(
|
|
|
1074
1210
|
},
|
|
1075
1211
|
async execute(_id: string, params: Record<string, unknown>) {
|
|
1076
1212
|
const action = params.action as string;
|
|
1213
|
+
|
|
1214
|
+
// Default data for each node type — ensures dashboard renders them properly
|
|
1215
|
+
const NODE_DEFAULTS: Record<string, Record<string, any>> = {
|
|
1216
|
+
manual_trigger: { label: "Execute Trigger" },
|
|
1217
|
+
webhook_trigger: { label: "Webhook Trigger" },
|
|
1218
|
+
schedule_trigger: { label: "Schedule Trigger", cron: "0 9 * * 1-5" },
|
|
1219
|
+
agent_step: { label: "Agent Step", agentId: "", task: "", responseMode: "text", timeoutSec: 120 },
|
|
1220
|
+
formatter_step: { label: "Formatter", template: "" },
|
|
1221
|
+
http_request: { label: "HTTP Request", method: "GET", url: "" },
|
|
1222
|
+
task_call: { label: "Task", agentId: "", taskId: "", taskTitle: "", agentName: "" },
|
|
1223
|
+
variable_set: { label: "Set Variable", variableName: "", variableValue: "" },
|
|
1224
|
+
condition: { label: "Condition", expression: "" },
|
|
1225
|
+
human_approval: { label: "Human Approval", instructions: "" },
|
|
1226
|
+
delay: { label: "Delay", delaySec: 5 },
|
|
1227
|
+
loop: { label: "Loop", loopType: "count", maxIterations: 3 },
|
|
1228
|
+
convergence: { label: "Convergence", mergeStrategy: "wait_all" },
|
|
1229
|
+
output: { label: "Output", outputMode: "return" },
|
|
1230
|
+
checkpoint: { label: "Checkpoint" },
|
|
1231
|
+
note: { label: "Note", noteText: "" },
|
|
1232
|
+
};
|
|
1233
|
+
|
|
1234
|
+
// Valid node types
|
|
1235
|
+
const VALID_TYPES = new Set(Object.keys(NODE_DEFAULTS));
|
|
1236
|
+
|
|
1237
|
+
// Helper: normalize a single node with defaults and auto-ID
|
|
1238
|
+
function normalizeNode(n: any, i: number) {
|
|
1239
|
+
let type = n.type || "agent_step";
|
|
1240
|
+
if (!VALID_TYPES.has(type)) type = "agent_step"; // fallback invalid types
|
|
1241
|
+
const defaults = NODE_DEFAULTS[type] || {};
|
|
1242
|
+
return {
|
|
1243
|
+
id: n.id || `${type}-${Date.now()}-${i}`,
|
|
1244
|
+
type,
|
|
1245
|
+
position: n.position || { x: 250, y: 80 + i * 150 },
|
|
1246
|
+
data: { ...defaults, ...(n.data || {}), label: n.data?.label || defaults.label || type },
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1077
1250
|
switch (action) {
|
|
1078
1251
|
case "list": {
|
|
1079
1252
|
let q = supabase.from("workflows").select("*").eq("user_id", userId).order("updated_at", { ascending: false });
|
|
@@ -1095,6 +1268,54 @@ function registerWorkflowOps(
|
|
|
1095
1268
|
const stepsWithIds = ((params.steps as any[]) || []).map((s: any, i: number) => ({
|
|
1096
1269
|
...s, id: s.id || `step-${i}`,
|
|
1097
1270
|
}));
|
|
1271
|
+
|
|
1272
|
+
// Build nodes — normalize provided nodes
|
|
1273
|
+
let rawNodes = (params.nodes as any[]) || [];
|
|
1274
|
+
let finalNodes = rawNodes.map((n, i) => normalizeNode(n, i));
|
|
1275
|
+
|
|
1276
|
+
// Auto-prepend a trigger node if none is present
|
|
1277
|
+
const hasTrigger = finalNodes.some(n => n.type.includes("trigger"));
|
|
1278
|
+
if (!hasTrigger) {
|
|
1279
|
+
const triggerNode = {
|
|
1280
|
+
id: `manual_trigger-${Date.now()}`,
|
|
1281
|
+
type: "manual_trigger",
|
|
1282
|
+
position: { x: 100, y: 200 },
|
|
1283
|
+
data: { label: "Execute Trigger" },
|
|
1284
|
+
};
|
|
1285
|
+
// Shift all other nodes to the right
|
|
1286
|
+
finalNodes = finalNodes.map(n => ({
|
|
1287
|
+
...n,
|
|
1288
|
+
position: { x: (n.position?.x || 250) + 200, y: n.position?.y || 200 },
|
|
1289
|
+
}));
|
|
1290
|
+
finalNodes.unshift(triggerNode);
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
// Build edges — ensure IDs exist
|
|
1294
|
+
let finalEdges = (params.edges as any[]) || [];
|
|
1295
|
+
finalEdges = finalEdges.map((e: any, i: number) => ({
|
|
1296
|
+
id: e.id || `edge-${Date.now()}-${i}`,
|
|
1297
|
+
source: e.source,
|
|
1298
|
+
target: e.target,
|
|
1299
|
+
...(e.sourceHandle ? { sourceHandle: e.sourceHandle } : {}),
|
|
1300
|
+
...(e.targetHandle ? { targetHandle: e.targetHandle } : {}),
|
|
1301
|
+
}));
|
|
1302
|
+
|
|
1303
|
+
// Auto-wire trigger to first non-trigger node if no edge connects from trigger
|
|
1304
|
+
if (hasTrigger === false && finalNodes.length > 1 && finalEdges.length === 0) {
|
|
1305
|
+
// No edges at all — auto-connect trigger → first step
|
|
1306
|
+
} else if (hasTrigger === false && finalNodes.length > 1) {
|
|
1307
|
+
const triggerId = finalNodes[0].id;
|
|
1308
|
+
const firstStepId = finalNodes[1].id;
|
|
1309
|
+
const triggerHasEdge = finalEdges.some(e => e.source === triggerId);
|
|
1310
|
+
if (!triggerHasEdge) {
|
|
1311
|
+
finalEdges.unshift({
|
|
1312
|
+
id: `edge-trigger-${Date.now()}`,
|
|
1313
|
+
source: triggerId,
|
|
1314
|
+
target: firstStepId,
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1098
1319
|
const { data, error } = await supabase.from("workflows").insert({
|
|
1099
1320
|
id: wfId, user_id: userId,
|
|
1100
1321
|
name: params.name,
|
|
@@ -1102,10 +1323,46 @@ function registerWorkflowOps(
|
|
|
1102
1323
|
steps: stepsWithIds,
|
|
1103
1324
|
schedule: (params.schedule as string) || null,
|
|
1104
1325
|
status: (params.status as string) || "draft",
|
|
1105
|
-
nodes:
|
|
1326
|
+
nodes: finalNodes,
|
|
1327
|
+
edges: finalEdges,
|
|
1328
|
+
definition_version: 2,
|
|
1106
1329
|
}).select().single();
|
|
1107
1330
|
if (error) return err(error.message);
|
|
1108
|
-
return ok({
|
|
1331
|
+
return ok({
|
|
1332
|
+
message: `Workflow "${params.name}" created with ${finalNodes.length} node(s) and ${finalEdges.length} edge(s)`,
|
|
1333
|
+
workflow: data,
|
|
1334
|
+
});
|
|
1335
|
+
}
|
|
1336
|
+
case "update": {
|
|
1337
|
+
const wfId = (params.id || params.workflow_id) as string;
|
|
1338
|
+
if (!wfId) return err("Missing required: id");
|
|
1339
|
+
const upd: Record<string, any> = { updated_at: new Date().toISOString() };
|
|
1340
|
+
for (const f of ["name", "description", "status", "steps", "schedule", "nodes", "edges"]) {
|
|
1341
|
+
if ((params as any)[f] !== undefined) upd[f] = (params as any)[f];
|
|
1342
|
+
}
|
|
1343
|
+
// Normalize nodes using the same defaults as create
|
|
1344
|
+
if (upd.nodes && Array.isArray(upd.nodes)) {
|
|
1345
|
+
upd.nodes = upd.nodes.map((n: any, i: number) => normalizeNode(n, i));
|
|
1346
|
+
}
|
|
1347
|
+
if (upd.edges && Array.isArray(upd.edges)) {
|
|
1348
|
+
upd.edges = upd.edges.map((e: any, i: number) => ({
|
|
1349
|
+
id: e.id || `edge-${Date.now()}-${i}`,
|
|
1350
|
+
source: e.source,
|
|
1351
|
+
target: e.target,
|
|
1352
|
+
}));
|
|
1353
|
+
}
|
|
1354
|
+
const { data, error } = await supabase.from("workflows").update(upd).eq("id", wfId).eq("user_id", userId).select().single();
|
|
1355
|
+
if (error) return err(error.message);
|
|
1356
|
+
return ok({ message: "Workflow updated", workflow: data });
|
|
1357
|
+
}
|
|
1358
|
+
case "delete": {
|
|
1359
|
+
const wfId = (params.id || params.workflow_id) as string;
|
|
1360
|
+
if (!wfId) return err("Missing required: id");
|
|
1361
|
+
// Delete associated runs first
|
|
1362
|
+
await supabase.from("workflow_runs").delete().eq("workflow_id", wfId);
|
|
1363
|
+
const { error } = await supabase.from("workflows").delete().eq("id", wfId).eq("user_id", userId);
|
|
1364
|
+
if (error) return err(error.message);
|
|
1365
|
+
return ok({ message: "Workflow and associated runs deleted", ok: true });
|
|
1109
1366
|
}
|
|
1110
1367
|
case "list_runs": {
|
|
1111
1368
|
const wfId = (params.workflow_id || params.id) as string;
|
|
@@ -1323,10 +1580,10 @@ function registerPromptOps(
|
|
|
1323
1580
|
description:
|
|
1324
1581
|
`Manage system prompt instruction chunks. These are the building blocks of agent behavior.\n\n` +
|
|
1325
1582
|
`Actions:\n` +
|
|
1326
|
-
`- "list": List all prompt chunks
|
|
1583
|
+
`- "list": List all prompt chunks\n` +
|
|
1327
1584
|
`- "get": Get a specific chunk. Required: id\n` +
|
|
1328
|
-
`- "create": Create a new chunk. Required:
|
|
1329
|
-
`- "update": Update a chunk. Required: id. Optional:
|
|
1585
|
+
`- "create": Create a new chunk. Required: name, content. Optional: color (hex), category\n` +
|
|
1586
|
+
`- "update": Update a chunk. Required: id. Optional: name, content, color, category, order\n` +
|
|
1330
1587
|
`- "delete": Delete a chunk. Required: id`,
|
|
1331
1588
|
parameters: {
|
|
1332
1589
|
type: "object",
|
|
@@ -1334,59 +1591,69 @@ function registerPromptOps(
|
|
|
1334
1591
|
properties: {
|
|
1335
1592
|
action: { type: "string", enum: ["list", "get", "create", "update", "delete"] },
|
|
1336
1593
|
id: { type: "string", description: "Chunk ID" },
|
|
1337
|
-
|
|
1338
|
-
content: { type: "string", description: "Prompt chunk content" },
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1594
|
+
name: { type: "string", description: "Chunk name/label (max 30 chars)" },
|
|
1595
|
+
content: { type: "string", description: "Prompt chunk content text" },
|
|
1596
|
+
color: { type: "string", description: "Hex color for display (e.g. #6B7280)" },
|
|
1597
|
+
category: { type: "string", description: "Category grouping (e.g. Personality, Instructions)" },
|
|
1598
|
+
order: { type: "number", description: "Display order (0-based)" },
|
|
1342
1599
|
},
|
|
1343
1600
|
},
|
|
1344
1601
|
async execute(_id: string, params: Record<string, unknown>) {
|
|
1345
1602
|
const action = params.action as string;
|
|
1346
1603
|
switch (action) {
|
|
1347
1604
|
case "list": {
|
|
1348
|
-
|
|
1349
|
-
if (params.agent_id) q = q.eq("agent_id", params.agent_id as string);
|
|
1350
|
-
const { data, error } = await q;
|
|
1605
|
+
const { data, error } = await supabase.from("prompt_chunks").select("*").eq("user_id", userId).order("order", { ascending: true });
|
|
1351
1606
|
if (error) return err(error.message);
|
|
1352
1607
|
return ok({ chunks: data || [], count: (data || []).length });
|
|
1353
1608
|
}
|
|
1354
1609
|
case "get": {
|
|
1355
1610
|
if (!params.id) return err("Missing required: id");
|
|
1356
|
-
const { data, error } = await supabase.from("prompt_chunks").select("*").eq("id", params.id).single();
|
|
1611
|
+
const { data, error } = await supabase.from("prompt_chunks").select("*").eq("id", params.id).eq("user_id", userId).single();
|
|
1357
1612
|
if (error) return err(error.message);
|
|
1358
1613
|
return ok({ chunk: data });
|
|
1359
1614
|
}
|
|
1360
1615
|
case "create": {
|
|
1361
|
-
if (!params.
|
|
1616
|
+
if (!params.name || !params.content) return err("Missing required: name, content");
|
|
1617
|
+
const chunkName = String(params.name).slice(0, 30);
|
|
1362
1618
|
const chunkId = crypto.randomUUID();
|
|
1619
|
+
|
|
1620
|
+
// Get current max order to append at end
|
|
1621
|
+
const { data: existing } = await supabase
|
|
1622
|
+
.from("prompt_chunks")
|
|
1623
|
+
.select("order")
|
|
1624
|
+
.eq("user_id", userId);
|
|
1625
|
+
const maxOrder = existing && existing.length > 0
|
|
1626
|
+
? Math.max(...existing.map((c: any) => c.order ?? 0))
|
|
1627
|
+
: -1;
|
|
1628
|
+
|
|
1363
1629
|
const { data, error } = await supabase.from("prompt_chunks").insert({
|
|
1364
1630
|
id: chunkId,
|
|
1365
1631
|
user_id: userId,
|
|
1366
|
-
|
|
1632
|
+
name: chunkName,
|
|
1367
1633
|
content: params.content,
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1634
|
+
color: (params.color as string) || "#6B7280",
|
|
1635
|
+
category: (params.category as string) || "Uncategorized",
|
|
1636
|
+
order: (params.order as number) ?? maxOrder + 1,
|
|
1371
1637
|
}).select().single();
|
|
1372
1638
|
if (error) return err(error.message);
|
|
1373
|
-
api.logger?.info?.(`[ofiere] Prompt chunk created: "${
|
|
1374
|
-
return ok({ message: `Prompt chunk "${
|
|
1639
|
+
api.logger?.info?.(`[ofiere] Prompt chunk created: "${chunkName}" by agent`);
|
|
1640
|
+
return ok({ message: `Prompt chunk "${chunkName}" created`, chunk: data });
|
|
1375
1641
|
}
|
|
1376
1642
|
case "update": {
|
|
1377
1643
|
if (!params.id) return err("Missing required: id");
|
|
1378
1644
|
const upd: Record<string, any> = { updated_at: new Date().toISOString() };
|
|
1379
|
-
for (const f of ["
|
|
1645
|
+
for (const f of ["name", "content", "color", "category", "order"]) {
|
|
1380
1646
|
if ((params as any)[f] !== undefined) upd[f] = (params as any)[f];
|
|
1381
1647
|
}
|
|
1382
|
-
|
|
1648
|
+
if (upd.name) upd.name = String(upd.name).slice(0, 30);
|
|
1649
|
+
const { data, error } = await supabase.from("prompt_chunks").update(upd).eq("id", params.id).eq("user_id", userId).select().single();
|
|
1383
1650
|
if (error) return err(error.message);
|
|
1384
1651
|
api.logger?.info?.(`[ofiere] Prompt chunk ${params.id} updated by agent`);
|
|
1385
|
-
return ok({ message: "Prompt chunk updated",
|
|
1652
|
+
return ok({ message: "Prompt chunk updated", chunk: data });
|
|
1386
1653
|
}
|
|
1387
1654
|
case "delete": {
|
|
1388
1655
|
if (!params.id) return err("Missing required: id");
|
|
1389
|
-
const { error } = await supabase.from("prompt_chunks").delete().eq("id", params.id);
|
|
1656
|
+
const { error } = await supabase.from("prompt_chunks").delete().eq("id", params.id).eq("user_id", userId);
|
|
1390
1657
|
if (error) return err(error.message);
|
|
1391
1658
|
api.logger?.info?.(`[ofiere] Prompt chunk ${params.id} deleted by agent`);
|
|
1392
1659
|
return ok({ message: "Prompt chunk deleted", ok: true });
|