ofiere-openclaw-plugin 3.0.0 → 3.2.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 +292 -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.2.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,55 @@ 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
|
+
const scheduledTime = (params.scheduled_time as string) || "09:00"; // default 9am
|
|
434
|
+
const datePart = startDate; // YYYY-MM-DD
|
|
435
|
+
const timePart = scheduledTime; // HH:MM
|
|
436
|
+
const dt = new Date(`${datePart}T${timePart}:00`);
|
|
437
|
+
const nextRunAt = Math.floor(dt.getTime() / 1000);
|
|
438
|
+
|
|
439
|
+
await supabase.from("scheduler_events").insert({
|
|
440
|
+
id: crypto.randomUUID(),
|
|
441
|
+
user_id: userId,
|
|
442
|
+
task_id: id,
|
|
443
|
+
agent_id: effectiveAgentId,
|
|
444
|
+
title: params.title,
|
|
445
|
+
description: (params.description as string) || (params.instructions as string) || null,
|
|
446
|
+
scheduled_date: datePart,
|
|
447
|
+
scheduled_time: timePart,
|
|
448
|
+
duration_minutes: 30,
|
|
449
|
+
recurrence_type: "none",
|
|
450
|
+
recurrence_interval: 1,
|
|
451
|
+
status: "scheduled",
|
|
452
|
+
next_run_at: nextRunAt,
|
|
453
|
+
run_count: 0,
|
|
454
|
+
priority: params.priority !== undefined ? params.priority : 1,
|
|
455
|
+
});
|
|
456
|
+
} catch (schedErr) {
|
|
457
|
+
// Non-fatal: task was created, just the scheduler event failed
|
|
458
|
+
console.error("[ofiere] Failed to auto-create scheduler event:", schedErr);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
424
462
|
const extras = [];
|
|
425
463
|
if (cf.execution_plan) extras.push(`${(cf.execution_plan as any[]).length} execution steps`);
|
|
426
464
|
if (cf.goals) extras.push(`${(cf.goals as any[]).length} goals`);
|
|
427
465
|
if (cf.constraints) extras.push(`${(cf.constraints as any[]).length} constraints`);
|
|
428
466
|
if (cf.system_prompt) extras.push("custom system prompt");
|
|
467
|
+
if (startDate) extras.push(`scheduled for ${startDate}`);
|
|
429
468
|
const extrasStr = extras.length > 0 ? ` with ${extras.join(", ")}` : "";
|
|
430
469
|
|
|
431
470
|
return ok({
|
|
432
471
|
id,
|
|
433
472
|
message: `Task "${params.title}" created and assigned to ${assignee || "no one"}${extrasStr}`,
|
|
434
473
|
task: insertData,
|
|
474
|
+
scheduledExecution: startDate ? `Will auto-execute on ${startDate}` : undefined,
|
|
435
475
|
});
|
|
436
476
|
} catch (e) {
|
|
437
477
|
return err(e instanceof Error ? e.message : String(e));
|
|
@@ -516,7 +556,7 @@ async function handleUpdateTask(
|
|
|
516
556
|
.update(updates)
|
|
517
557
|
.eq("id", params.task_id as string)
|
|
518
558
|
.eq("user_id", userId)
|
|
519
|
-
.select("id, title, status, priority, agent_id")
|
|
559
|
+
.select("id, title, status, priority, agent_id, start_date, due_date, progress, updated_at")
|
|
520
560
|
.single();
|
|
521
561
|
|
|
522
562
|
if (error) return err(error.message);
|
|
@@ -934,11 +974,12 @@ function registerKnowledgeOps(
|
|
|
934
974
|
name: "OFIERE_KNOWLEDGE_OPS",
|
|
935
975
|
label: "Ofiere Knowledge Operations",
|
|
936
976
|
description:
|
|
937
|
-
`
|
|
977
|
+
`Access the Ofiere Knowledge Library — the stored knowledge base in the dashboard. ` +
|
|
978
|
+
`Use this tool whenever the user mentions "knowledge base", "knowledge library", "knowledge entries", or asks to retrieve stored knowledge.\n\n` +
|
|
938
979
|
`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` +
|
|
980
|
+
`- "search": Search the knowledge library by keyword. Required: query. Optional: limit\n` +
|
|
981
|
+
`- "list": List recent entries from the knowledge library. Optional: page, page_size, search\n` +
|
|
982
|
+
`- "create": Add a document to the knowledge library. Required: file_name. Optional: content, text, source, source_type, author, credibility_tier\n` +
|
|
942
983
|
`- "update": Edit a document. Required: id. Optional: file_name, content, text, source, source_type, author\n` +
|
|
943
984
|
`- "delete": Remove a document. Required: id`,
|
|
944
985
|
parameters: {
|
|
@@ -983,7 +1024,7 @@ function registerKnowledgeOps(
|
|
|
983
1024
|
const from = (page - 1) * pageSize;
|
|
984
1025
|
const to = from + pageSize - 1;
|
|
985
1026
|
let q = supabase.from("knowledge_documents")
|
|
986
|
-
.select("id, file_name, file_type, source, source_type, author, credibility_tier, created_at", { count: "exact" })
|
|
1027
|
+
.select("id, file_name, file_type, content, text, source, source_type, author, credibility_tier, created_at", { count: "exact" })
|
|
987
1028
|
.order("created_at", { ascending: false })
|
|
988
1029
|
.range(from, to);
|
|
989
1030
|
if (params.search) {
|
|
@@ -1050,23 +1091,75 @@ function registerWorkflowOps(
|
|
|
1050
1091
|
name: "OFIERE_WORKFLOW_OPS",
|
|
1051
1092
|
label: "Ofiere Workflow Operations",
|
|
1052
1093
|
description:
|
|
1053
|
-
`Manage and trigger automated workflows.\n\n` +
|
|
1094
|
+
`Manage, build, and trigger automated workflows in the Ofiere dashboard.\n\n` +
|
|
1054
1095
|
`Actions:\n` +
|
|
1055
1096
|
`- "list": List all workflows. Optional: status\n` +
|
|
1056
1097
|
`- "get": Get workflow details. Required: id\n` +
|
|
1057
|
-
`- "create": Create a workflow. Required: name. Optional: description,
|
|
1098
|
+
`- "create": Create a workflow WITH nodes and edges. Required: name. Optional: description, nodes, edges, schedule, status\n` +
|
|
1099
|
+
`- "update": Update a workflow. Required: id. Optional: name, description, status, nodes, edges, schedule\n` +
|
|
1100
|
+
`- "delete": Delete a workflow and its run history. Required: id\n` +
|
|
1058
1101
|
`- "list_runs": List recent runs. Required: workflow_id. Optional: limit\n` +
|
|
1059
|
-
`- "trigger": Start a workflow run. Required: workflow_id
|
|
1102
|
+
`- "trigger": Start a workflow run. Required: workflow_id\n\n` +
|
|
1103
|
+
`NODE TYPES (use these exact types when creating nodes):\n` +
|
|
1104
|
+
` TRIGGERS (start of workflow — pick one):\n` +
|
|
1105
|
+
` - "manual_trigger": User clicks Execute to start\n` +
|
|
1106
|
+
` - "webhook_trigger": External HTTP request triggers it\n` +
|
|
1107
|
+
` - "schedule_trigger": Runs on cron schedule. data: { label, cron: "0 9 * * 1-5" }\n` +
|
|
1108
|
+
` STEPS (the work):\n` +
|
|
1109
|
+
` - "agent_step": Delegates task to an AI agent. data: { label, agentId, task, responseMode: "text", timeoutSec: 120 }\n` +
|
|
1110
|
+
` - "http_request": Calls an external API. data: { label, method: "GET"|"POST", url }\n` +
|
|
1111
|
+
` - "formatter_step": Formats/transforms text or JSON. data: { label, template }\n` +
|
|
1112
|
+
` - "task_call": Runs a saved task. data: { label, agentId, taskId }\n` +
|
|
1113
|
+
` - "variable_set": Stores data in a variable. data: { label, variableName, variableValue }\n` +
|
|
1114
|
+
` CONTROL FLOW:\n` +
|
|
1115
|
+
` - "condition": If/else branch. data: { label, expression }\n` +
|
|
1116
|
+
` - "human_approval": Pauses for human approval. data: { label, instructions }\n` +
|
|
1117
|
+
` - "delay": Waits for a set time. data: { label, delaySec: 5 }\n` +
|
|
1118
|
+
` - "loop": Repeats actions. data: { label, loopType: "count", maxIterations: 3 }\n` +
|
|
1119
|
+
` - "convergence": Waits for multiple parallel inputs. data: { label, mergeStrategy: "wait_all" }\n` +
|
|
1120
|
+
` END:\n` +
|
|
1121
|
+
` - "output": Returns final result. data: { label, outputMode: "return" }\n` +
|
|
1122
|
+
` SPECIAL:\n` +
|
|
1123
|
+
` - "checkpoint": Loop target marker. data: { label }\n` +
|
|
1124
|
+
` - "note": Sticky note annotation. data: { label, noteText }\n\n` +
|
|
1125
|
+
`Each node: { type, data: { label, ... }, position?: { x, y } }. IDs and positions are auto-generated if omitted.\n` +
|
|
1126
|
+
`Each edge: { source: "node_id", target: "node_id" }. IDs auto-generated.\n` +
|
|
1127
|
+
`A manual_trigger node is always auto-prepended if no trigger node is included.`,
|
|
1060
1128
|
parameters: {
|
|
1061
1129
|
type: "object",
|
|
1062
1130
|
required: ["action"],
|
|
1063
1131
|
properties: {
|
|
1064
|
-
action: { type: "string", enum: ["list", "get", "create", "list_runs", "trigger"] },
|
|
1132
|
+
action: { type: "string", enum: ["list", "get", "create", "update", "delete", "list_runs", "trigger"] },
|
|
1065
1133
|
id: { type: "string", description: "Workflow ID" },
|
|
1066
1134
|
workflow_id: { type: "string", description: "Workflow ID for runs/trigger" },
|
|
1067
1135
|
name: { type: "string", description: "Workflow name" },
|
|
1068
1136
|
description: { type: "string" },
|
|
1069
|
-
|
|
1137
|
+
nodes: {
|
|
1138
|
+
type: "array",
|
|
1139
|
+
items: {
|
|
1140
|
+
type: "object",
|
|
1141
|
+
properties: {
|
|
1142
|
+
id: { type: "string", description: "Node ID (auto-generated if omitted)" },
|
|
1143
|
+
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"] },
|
|
1144
|
+
position: { type: "object", properties: { x: { type: "number" }, y: { type: "number" } } },
|
|
1145
|
+
data: { type: "object", description: "Node config — always include a 'label' field. See NODE TYPES above for type-specific fields." },
|
|
1146
|
+
},
|
|
1147
|
+
},
|
|
1148
|
+
description: "Workflow graph nodes",
|
|
1149
|
+
},
|
|
1150
|
+
edges: {
|
|
1151
|
+
type: "array",
|
|
1152
|
+
items: {
|
|
1153
|
+
type: "object",
|
|
1154
|
+
properties: {
|
|
1155
|
+
id: { type: "string", description: "Edge ID (auto-generated if omitted)" },
|
|
1156
|
+
source: { type: "string", description: "Source node ID" },
|
|
1157
|
+
target: { type: "string", description: "Target node ID" },
|
|
1158
|
+
},
|
|
1159
|
+
},
|
|
1160
|
+
description: "Connections between nodes. Each edge: { source, target }",
|
|
1161
|
+
},
|
|
1162
|
+
steps: { type: "array", items: { type: "object" }, description: "Legacy V1 step definitions" },
|
|
1070
1163
|
schedule: { type: "string", description: "Cron expression or schedule" },
|
|
1071
1164
|
status: { type: "string", enum: ["draft", "active", "paused", "archived"] },
|
|
1072
1165
|
limit: { type: "number", description: "Max results" },
|
|
@@ -1074,6 +1167,43 @@ function registerWorkflowOps(
|
|
|
1074
1167
|
},
|
|
1075
1168
|
async execute(_id: string, params: Record<string, unknown>) {
|
|
1076
1169
|
const action = params.action as string;
|
|
1170
|
+
|
|
1171
|
+
// Default data for each node type — ensures dashboard renders them properly
|
|
1172
|
+
const NODE_DEFAULTS: Record<string, Record<string, any>> = {
|
|
1173
|
+
manual_trigger: { label: "Execute Trigger" },
|
|
1174
|
+
webhook_trigger: { label: "Webhook Trigger" },
|
|
1175
|
+
schedule_trigger: { label: "Schedule Trigger", cron: "0 9 * * 1-5" },
|
|
1176
|
+
agent_step: { label: "Agent Step", agentId: "", task: "", responseMode: "text", timeoutSec: 120 },
|
|
1177
|
+
formatter_step: { label: "Formatter", template: "" },
|
|
1178
|
+
http_request: { label: "HTTP Request", method: "GET", url: "" },
|
|
1179
|
+
task_call: { label: "Task", agentId: "", taskId: "", taskTitle: "", agentName: "" },
|
|
1180
|
+
variable_set: { label: "Set Variable", variableName: "", variableValue: "" },
|
|
1181
|
+
condition: { label: "Condition", expression: "" },
|
|
1182
|
+
human_approval: { label: "Human Approval", instructions: "" },
|
|
1183
|
+
delay: { label: "Delay", delaySec: 5 },
|
|
1184
|
+
loop: { label: "Loop", loopType: "count", maxIterations: 3 },
|
|
1185
|
+
convergence: { label: "Convergence", mergeStrategy: "wait_all" },
|
|
1186
|
+
output: { label: "Output", outputMode: "return" },
|
|
1187
|
+
checkpoint: { label: "Checkpoint" },
|
|
1188
|
+
note: { label: "Note", noteText: "" },
|
|
1189
|
+
};
|
|
1190
|
+
|
|
1191
|
+
// Valid node types
|
|
1192
|
+
const VALID_TYPES = new Set(Object.keys(NODE_DEFAULTS));
|
|
1193
|
+
|
|
1194
|
+
// Helper: normalize a single node with defaults and auto-ID
|
|
1195
|
+
function normalizeNode(n: any, i: number) {
|
|
1196
|
+
let type = n.type || "agent_step";
|
|
1197
|
+
if (!VALID_TYPES.has(type)) type = "agent_step"; // fallback invalid types
|
|
1198
|
+
const defaults = NODE_DEFAULTS[type] || {};
|
|
1199
|
+
return {
|
|
1200
|
+
id: n.id || `${type}-${Date.now()}-${i}`,
|
|
1201
|
+
type,
|
|
1202
|
+
position: n.position || { x: 250, y: 80 + i * 150 },
|
|
1203
|
+
data: { ...defaults, ...(n.data || {}), label: n.data?.label || defaults.label || type },
|
|
1204
|
+
};
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1077
1207
|
switch (action) {
|
|
1078
1208
|
case "list": {
|
|
1079
1209
|
let q = supabase.from("workflows").select("*").eq("user_id", userId).order("updated_at", { ascending: false });
|
|
@@ -1095,6 +1225,54 @@ function registerWorkflowOps(
|
|
|
1095
1225
|
const stepsWithIds = ((params.steps as any[]) || []).map((s: any, i: number) => ({
|
|
1096
1226
|
...s, id: s.id || `step-${i}`,
|
|
1097
1227
|
}));
|
|
1228
|
+
|
|
1229
|
+
// Build nodes — normalize provided nodes
|
|
1230
|
+
let rawNodes = (params.nodes as any[]) || [];
|
|
1231
|
+
let finalNodes = rawNodes.map((n, i) => normalizeNode(n, i));
|
|
1232
|
+
|
|
1233
|
+
// Auto-prepend a trigger node if none is present
|
|
1234
|
+
const hasTrigger = finalNodes.some(n => n.type.includes("trigger"));
|
|
1235
|
+
if (!hasTrigger) {
|
|
1236
|
+
const triggerNode = {
|
|
1237
|
+
id: `manual_trigger-${Date.now()}`,
|
|
1238
|
+
type: "manual_trigger",
|
|
1239
|
+
position: { x: 100, y: 200 },
|
|
1240
|
+
data: { label: "Execute Trigger" },
|
|
1241
|
+
};
|
|
1242
|
+
// Shift all other nodes to the right
|
|
1243
|
+
finalNodes = finalNodes.map(n => ({
|
|
1244
|
+
...n,
|
|
1245
|
+
position: { x: (n.position?.x || 250) + 200, y: n.position?.y || 200 },
|
|
1246
|
+
}));
|
|
1247
|
+
finalNodes.unshift(triggerNode);
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
// Build edges — ensure IDs exist
|
|
1251
|
+
let finalEdges = (params.edges as any[]) || [];
|
|
1252
|
+
finalEdges = finalEdges.map((e: any, i: number) => ({
|
|
1253
|
+
id: e.id || `edge-${Date.now()}-${i}`,
|
|
1254
|
+
source: e.source,
|
|
1255
|
+
target: e.target,
|
|
1256
|
+
...(e.sourceHandle ? { sourceHandle: e.sourceHandle } : {}),
|
|
1257
|
+
...(e.targetHandle ? { targetHandle: e.targetHandle } : {}),
|
|
1258
|
+
}));
|
|
1259
|
+
|
|
1260
|
+
// Auto-wire trigger to first non-trigger node if no edge connects from trigger
|
|
1261
|
+
if (hasTrigger === false && finalNodes.length > 1 && finalEdges.length === 0) {
|
|
1262
|
+
// No edges at all — auto-connect trigger → first step
|
|
1263
|
+
} else if (hasTrigger === false && finalNodes.length > 1) {
|
|
1264
|
+
const triggerId = finalNodes[0].id;
|
|
1265
|
+
const firstStepId = finalNodes[1].id;
|
|
1266
|
+
const triggerHasEdge = finalEdges.some(e => e.source === triggerId);
|
|
1267
|
+
if (!triggerHasEdge) {
|
|
1268
|
+
finalEdges.unshift({
|
|
1269
|
+
id: `edge-trigger-${Date.now()}`,
|
|
1270
|
+
source: triggerId,
|
|
1271
|
+
target: firstStepId,
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1098
1276
|
const { data, error } = await supabase.from("workflows").insert({
|
|
1099
1277
|
id: wfId, user_id: userId,
|
|
1100
1278
|
name: params.name,
|
|
@@ -1102,10 +1280,46 @@ function registerWorkflowOps(
|
|
|
1102
1280
|
steps: stepsWithIds,
|
|
1103
1281
|
schedule: (params.schedule as string) || null,
|
|
1104
1282
|
status: (params.status as string) || "draft",
|
|
1105
|
-
nodes:
|
|
1283
|
+
nodes: finalNodes,
|
|
1284
|
+
edges: finalEdges,
|
|
1285
|
+
definition_version: 2,
|
|
1106
1286
|
}).select().single();
|
|
1107
1287
|
if (error) return err(error.message);
|
|
1108
|
-
return ok({
|
|
1288
|
+
return ok({
|
|
1289
|
+
message: `Workflow "${params.name}" created with ${finalNodes.length} node(s) and ${finalEdges.length} edge(s)`,
|
|
1290
|
+
workflow: data,
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
case "update": {
|
|
1294
|
+
const wfId = (params.id || params.workflow_id) as string;
|
|
1295
|
+
if (!wfId) return err("Missing required: id");
|
|
1296
|
+
const upd: Record<string, any> = { updated_at: new Date().toISOString() };
|
|
1297
|
+
for (const f of ["name", "description", "status", "steps", "schedule", "nodes", "edges"]) {
|
|
1298
|
+
if ((params as any)[f] !== undefined) upd[f] = (params as any)[f];
|
|
1299
|
+
}
|
|
1300
|
+
// Normalize nodes using the same defaults as create
|
|
1301
|
+
if (upd.nodes && Array.isArray(upd.nodes)) {
|
|
1302
|
+
upd.nodes = upd.nodes.map((n: any, i: number) => normalizeNode(n, i));
|
|
1303
|
+
}
|
|
1304
|
+
if (upd.edges && Array.isArray(upd.edges)) {
|
|
1305
|
+
upd.edges = upd.edges.map((e: any, i: number) => ({
|
|
1306
|
+
id: e.id || `edge-${Date.now()}-${i}`,
|
|
1307
|
+
source: e.source,
|
|
1308
|
+
target: e.target,
|
|
1309
|
+
}));
|
|
1310
|
+
}
|
|
1311
|
+
const { data, error } = await supabase.from("workflows").update(upd).eq("id", wfId).eq("user_id", userId).select().single();
|
|
1312
|
+
if (error) return err(error.message);
|
|
1313
|
+
return ok({ message: "Workflow updated", workflow: data });
|
|
1314
|
+
}
|
|
1315
|
+
case "delete": {
|
|
1316
|
+
const wfId = (params.id || params.workflow_id) as string;
|
|
1317
|
+
if (!wfId) return err("Missing required: id");
|
|
1318
|
+
// Delete associated runs first
|
|
1319
|
+
await supabase.from("workflow_runs").delete().eq("workflow_id", wfId);
|
|
1320
|
+
const { error } = await supabase.from("workflows").delete().eq("id", wfId).eq("user_id", userId);
|
|
1321
|
+
if (error) return err(error.message);
|
|
1322
|
+
return ok({ message: "Workflow and associated runs deleted", ok: true });
|
|
1109
1323
|
}
|
|
1110
1324
|
case "list_runs": {
|
|
1111
1325
|
const wfId = (params.workflow_id || params.id) as string;
|
|
@@ -1323,10 +1537,10 @@ function registerPromptOps(
|
|
|
1323
1537
|
description:
|
|
1324
1538
|
`Manage system prompt instruction chunks. These are the building blocks of agent behavior.\n\n` +
|
|
1325
1539
|
`Actions:\n` +
|
|
1326
|
-
`- "list": List all prompt chunks
|
|
1540
|
+
`- "list": List all prompt chunks\n` +
|
|
1327
1541
|
`- "get": Get a specific chunk. Required: id\n` +
|
|
1328
|
-
`- "create": Create a new chunk. Required:
|
|
1329
|
-
`- "update": Update a chunk. Required: id. Optional:
|
|
1542
|
+
`- "create": Create a new chunk. Required: name, content. Optional: color (hex), category\n` +
|
|
1543
|
+
`- "update": Update a chunk. Required: id. Optional: name, content, color, category, order\n` +
|
|
1330
1544
|
`- "delete": Delete a chunk. Required: id`,
|
|
1331
1545
|
parameters: {
|
|
1332
1546
|
type: "object",
|
|
@@ -1334,59 +1548,69 @@ function registerPromptOps(
|
|
|
1334
1548
|
properties: {
|
|
1335
1549
|
action: { type: "string", enum: ["list", "get", "create", "update", "delete"] },
|
|
1336
1550
|
id: { type: "string", description: "Chunk ID" },
|
|
1337
|
-
|
|
1338
|
-
content: { type: "string", description: "Prompt chunk content" },
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1551
|
+
name: { type: "string", description: "Chunk name/label (max 30 chars)" },
|
|
1552
|
+
content: { type: "string", description: "Prompt chunk content text" },
|
|
1553
|
+
color: { type: "string", description: "Hex color for display (e.g. #6B7280)" },
|
|
1554
|
+
category: { type: "string", description: "Category grouping (e.g. Personality, Instructions)" },
|
|
1555
|
+
order: { type: "number", description: "Display order (0-based)" },
|
|
1342
1556
|
},
|
|
1343
1557
|
},
|
|
1344
1558
|
async execute(_id: string, params: Record<string, unknown>) {
|
|
1345
1559
|
const action = params.action as string;
|
|
1346
1560
|
switch (action) {
|
|
1347
1561
|
case "list": {
|
|
1348
|
-
|
|
1349
|
-
if (params.agent_id) q = q.eq("agent_id", params.agent_id as string);
|
|
1350
|
-
const { data, error } = await q;
|
|
1562
|
+
const { data, error } = await supabase.from("prompt_chunks").select("*").eq("user_id", userId).order("order", { ascending: true });
|
|
1351
1563
|
if (error) return err(error.message);
|
|
1352
1564
|
return ok({ chunks: data || [], count: (data || []).length });
|
|
1353
1565
|
}
|
|
1354
1566
|
case "get": {
|
|
1355
1567
|
if (!params.id) return err("Missing required: id");
|
|
1356
|
-
const { data, error } = await supabase.from("prompt_chunks").select("*").eq("id", params.id).single();
|
|
1568
|
+
const { data, error } = await supabase.from("prompt_chunks").select("*").eq("id", params.id).eq("user_id", userId).single();
|
|
1357
1569
|
if (error) return err(error.message);
|
|
1358
1570
|
return ok({ chunk: data });
|
|
1359
1571
|
}
|
|
1360
1572
|
case "create": {
|
|
1361
|
-
if (!params.
|
|
1573
|
+
if (!params.name || !params.content) return err("Missing required: name, content");
|
|
1574
|
+
const chunkName = String(params.name).slice(0, 30);
|
|
1362
1575
|
const chunkId = crypto.randomUUID();
|
|
1576
|
+
|
|
1577
|
+
// Get current max order to append at end
|
|
1578
|
+
const { data: existing } = await supabase
|
|
1579
|
+
.from("prompt_chunks")
|
|
1580
|
+
.select("order")
|
|
1581
|
+
.eq("user_id", userId);
|
|
1582
|
+
const maxOrder = existing && existing.length > 0
|
|
1583
|
+
? Math.max(...existing.map((c: any) => c.order ?? 0))
|
|
1584
|
+
: -1;
|
|
1585
|
+
|
|
1363
1586
|
const { data, error } = await supabase.from("prompt_chunks").insert({
|
|
1364
1587
|
id: chunkId,
|
|
1365
1588
|
user_id: userId,
|
|
1366
|
-
|
|
1589
|
+
name: chunkName,
|
|
1367
1590
|
content: params.content,
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1591
|
+
color: (params.color as string) || "#6B7280",
|
|
1592
|
+
category: (params.category as string) || "Uncategorized",
|
|
1593
|
+
order: (params.order as number) ?? maxOrder + 1,
|
|
1371
1594
|
}).select().single();
|
|
1372
1595
|
if (error) return err(error.message);
|
|
1373
|
-
api.logger?.info?.(`[ofiere] Prompt chunk created: "${
|
|
1374
|
-
return ok({ message: `Prompt chunk "${
|
|
1596
|
+
api.logger?.info?.(`[ofiere] Prompt chunk created: "${chunkName}" by agent`);
|
|
1597
|
+
return ok({ message: `Prompt chunk "${chunkName}" created`, chunk: data });
|
|
1375
1598
|
}
|
|
1376
1599
|
case "update": {
|
|
1377
1600
|
if (!params.id) return err("Missing required: id");
|
|
1378
1601
|
const upd: Record<string, any> = { updated_at: new Date().toISOString() };
|
|
1379
|
-
for (const f of ["
|
|
1602
|
+
for (const f of ["name", "content", "color", "category", "order"]) {
|
|
1380
1603
|
if ((params as any)[f] !== undefined) upd[f] = (params as any)[f];
|
|
1381
1604
|
}
|
|
1382
|
-
|
|
1605
|
+
if (upd.name) upd.name = String(upd.name).slice(0, 30);
|
|
1606
|
+
const { data, error } = await supabase.from("prompt_chunks").update(upd).eq("id", params.id).eq("user_id", userId).select().single();
|
|
1383
1607
|
if (error) return err(error.message);
|
|
1384
1608
|
api.logger?.info?.(`[ofiere] Prompt chunk ${params.id} updated by agent`);
|
|
1385
|
-
return ok({ message: "Prompt chunk updated",
|
|
1609
|
+
return ok({ message: "Prompt chunk updated", chunk: data });
|
|
1386
1610
|
}
|
|
1387
1611
|
case "delete": {
|
|
1388
1612
|
if (!params.id) return err("Missing required: id");
|
|
1389
|
-
const { error } = await supabase.from("prompt_chunks").delete().eq("id", params.id);
|
|
1613
|
+
const { error } = await supabase.from("prompt_chunks").delete().eq("id", params.id).eq("user_id", userId);
|
|
1390
1614
|
if (error) return err(error.message);
|
|
1391
1615
|
api.logger?.info?.(`[ofiere] Prompt chunk ${params.id} deleted by agent`);
|
|
1392
1616
|
return ok({ message: "Prompt chunk deleted", ok: true });
|