ofiere-openclaw-plugin 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +89 -0
- package/index.ts +92 -0
- package/openclaw.plugin.json +54 -0
- package/package.json +36 -0
- package/src/agent-resolver.ts +104 -0
- package/src/cli.ts +247 -0
- package/src/config.ts +55 -0
- package/src/prompt.ts +96 -0
- package/src/supabase.ts +13 -0
- package/src/tools.ts +403 -0
- package/src/types.ts +8 -0
package/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Ofiere PM Plugin for OpenClaw
|
|
2
|
+
|
|
3
|
+
Manage your Ofiere PM dashboard directly from OpenClaw agents. Create tasks, update progress, assign agents — all synced to the dashboard in real time.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
openclaw plugins install @ofiere-ai/openclaw-plugin
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or install from the local repo (for development):
|
|
12
|
+
|
|
13
|
+
```bash
|
|
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:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
openclaw ofiere setup
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Then restart the gateway:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
openclaw gateway restart
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## How it works
|
|
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.
|
|
40
|
+
|
|
41
|
+
Changes made by the agent are immediately visible on the Ofiere dashboard (Vercel) via Supabase real-time subscriptions.
|
|
42
|
+
|
|
43
|
+
## AI Tools
|
|
44
|
+
|
|
45
|
+
| Tool | Description |
|
|
46
|
+
|---|---|
|
|
47
|
+
| `OFIERE_LIST_TASKS` | List and filter PM tasks |
|
|
48
|
+
| `OFIERE_CREATE_TASK` | Create a new task (auto-assigns to calling agent) |
|
|
49
|
+
| `OFIERE_UPDATE_TASK` | Update task fields (status, priority, progress, etc.) |
|
|
50
|
+
| `OFIERE_DELETE_TASK` | Delete a task and its subtasks |
|
|
51
|
+
| `OFIERE_LIST_AGENTS` | List available agents for task assignment |
|
|
52
|
+
|
|
53
|
+
## CLI Commands
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
openclaw ofiere setup # Configure Supabase connection and agent identity
|
|
57
|
+
openclaw ofiere status # View current configuration
|
|
58
|
+
openclaw ofiere doctor # Test connection and list agents
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Configuration
|
|
62
|
+
|
|
63
|
+
Set via `openclaw ofiere setup` or environment variables:
|
|
64
|
+
|
|
65
|
+
| Option | Env Var | Description |
|
|
66
|
+
|---|---|---|
|
|
67
|
+
| `supabaseUrl` | `OFIERE_SUPABASE_URL` | Supabase project URL |
|
|
68
|
+
| `serviceRoleKey` | `OFIERE_SERVICE_ROLE_KEY` | Supabase service role key |
|
|
69
|
+
| `userId` | `OFIERE_USER_ID` | Your user UUID |
|
|
70
|
+
| `agentId` | `OFIERE_AGENT_ID` | This agent's ID (e.g. `sasha`) |
|
|
71
|
+
| `enabled` | — | Enable/disable the plugin (default: `true`) |
|
|
72
|
+
|
|
73
|
+
## Architecture
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
OpenClaw Agent (VPS)
|
|
77
|
+
│ plugin runs IN-PROCESS
|
|
78
|
+
Ofiere Plugin ──► Supabase (shared database)
|
|
79
|
+
▲
|
|
80
|
+
Ofiere Dashboard ─────┘ (Vercel, real-time)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Both the agent plugin and the Vercel dashboard talk to the same Supabase instance. When the agent creates/updates a task, the dashboard sees it instantly through Supabase real-time subscriptions.
|
|
84
|
+
|
|
85
|
+
## Links
|
|
86
|
+
|
|
87
|
+
- [Ofiere Dashboard](https://github.com/gilanggemar/Ofiere)
|
|
88
|
+
- [OpenClaw](https://openclaw.ai)
|
|
89
|
+
- [Supabase](https://supabase.com)
|
package/index.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// index.ts — Ofiere PM Plugin for OpenClaw
|
|
2
|
+
// Uses definePluginEntry from the official plugin-entry subpath (NOT the deprecated monolithic root)
|
|
3
|
+
// Pattern: https://docs.openclaw.ai/plugins/building-plugins#quick-start-tool-plugin
|
|
4
|
+
|
|
5
|
+
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
|
6
|
+
import { parseOfiereConfig } from "./src/config.js";
|
|
7
|
+
import { getSupabase } from "./src/supabase.js";
|
|
8
|
+
import { registerTools } from "./src/tools.js";
|
|
9
|
+
import { getSystemPrompt } from "./src/prompt.js";
|
|
10
|
+
import { registerCli } from "./src/cli.js";
|
|
11
|
+
import { seedAgentCache } from "./src/agent-resolver.js";
|
|
12
|
+
|
|
13
|
+
export default definePluginEntry({
|
|
14
|
+
id: "ofiere",
|
|
15
|
+
name: "Ofiere PM",
|
|
16
|
+
description:
|
|
17
|
+
"Manage Ofiere PM tasks, agents, and projects directly from the agent. " +
|
|
18
|
+
"Create tasks, update progress, assign agents — all synced to the dashboard in real time.",
|
|
19
|
+
|
|
20
|
+
register(api) {
|
|
21
|
+
const config = parseOfiereConfig(api.pluginConfig);
|
|
22
|
+
|
|
23
|
+
// Always register CLI (even if disabled — so user can run `openclaw ofiere setup`)
|
|
24
|
+
registerCli(api);
|
|
25
|
+
|
|
26
|
+
if (!config.enabled) {
|
|
27
|
+
api.logger.debug("[ofiere] Plugin disabled via config");
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!config.supabaseUrl || !config.serviceRoleKey) {
|
|
32
|
+
api.logger.warn(
|
|
33
|
+
"[ofiere] Not configured. Run: openclaw ofiere setup",
|
|
34
|
+
);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!config.userId) {
|
|
39
|
+
api.logger.warn(
|
|
40
|
+
"[ofiere] Missing userId. Run: openclaw ofiere setup",
|
|
41
|
+
);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ── Pre-seed agent cache if OFIERE_AGENT_ID is set (legacy mode) ──────
|
|
46
|
+
if (config.agentId) {
|
|
47
|
+
// Try to extract the calling agent's name from OpenClaw context
|
|
48
|
+
const callerName =
|
|
49
|
+
(api as any)?.agentContext?.accountId ||
|
|
50
|
+
(api as any)?.agentContext?.name ||
|
|
51
|
+
(api as any)?.currentAgent?.accountId ||
|
|
52
|
+
"";
|
|
53
|
+
if (callerName) {
|
|
54
|
+
seedAgentCache(callerName, config.userId, config.agentId);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ── State for system prompt injection ──────────────────────────────────
|
|
59
|
+
const promptState = {
|
|
60
|
+
toolCount: 0,
|
|
61
|
+
agentId: config.agentId,
|
|
62
|
+
connectError: "",
|
|
63
|
+
ready: false,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// ── Hook: inject Ofiere context into every agent prompt ────────────────
|
|
67
|
+
// Uses api.registerHook (the documented API) instead of api.on shorthand
|
|
68
|
+
api.registerHook(
|
|
69
|
+
["before_prompt_build"],
|
|
70
|
+
() => ({
|
|
71
|
+
prependSystemContext: getSystemPrompt(promptState),
|
|
72
|
+
}),
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
// ── Connect to Supabase and register tools ────────────────────────────
|
|
76
|
+
try {
|
|
77
|
+
const supabase = getSupabase(config.supabaseUrl, config.serviceRoleKey);
|
|
78
|
+
registerTools(api, supabase, config);
|
|
79
|
+
promptState.toolCount = 5;
|
|
80
|
+
promptState.ready = true;
|
|
81
|
+
const agentLabel = config.agentId || "auto-detect";
|
|
82
|
+
api.logger.info(
|
|
83
|
+
`[ofiere] Ready — 5 tools registered (agent: ${agentLabel})`,
|
|
84
|
+
);
|
|
85
|
+
} catch (e) {
|
|
86
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
87
|
+
promptState.connectError = msg;
|
|
88
|
+
promptState.ready = true;
|
|
89
|
+
api.logger.error(`[ofiere] Failed to initialize: ${msg}`);
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "ofiere",
|
|
3
|
+
"name": "Ofiere PM",
|
|
4
|
+
"description": "Manage Ofiere PM tasks, agents, and projects directly from the agent. The agent can create tasks, update progress, list work items, and more — all synced to the Ofiere dashboard in real time.",
|
|
5
|
+
"configSchema": {
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"properties": {
|
|
9
|
+
"enabled": {
|
|
10
|
+
"type": "boolean",
|
|
11
|
+
"description": "Enable or disable the Ofiere PM integration"
|
|
12
|
+
},
|
|
13
|
+
"supabaseUrl": {
|
|
14
|
+
"type": "string",
|
|
15
|
+
"description": "Your Supabase project URL (e.g. https://xxx.supabase.co)"
|
|
16
|
+
},
|
|
17
|
+
"serviceRoleKey": {
|
|
18
|
+
"type": "string",
|
|
19
|
+
"description": "Supabase service role key for full database access"
|
|
20
|
+
},
|
|
21
|
+
"userId": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"description": "Your Ofiere user ID (UUID from auth.users)"
|
|
24
|
+
},
|
|
25
|
+
"agentId": {
|
|
26
|
+
"type": "string",
|
|
27
|
+
"description": "This agent's ID in Ofiere (e.g. 'sasha', 'ivy', 'thalia')"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"uiHints": {
|
|
32
|
+
"enabled": {
|
|
33
|
+
"label": "Enable Ofiere PM",
|
|
34
|
+
"help": "Enable or disable the Ofiere PM integration"
|
|
35
|
+
},
|
|
36
|
+
"supabaseUrl": {
|
|
37
|
+
"label": "Supabase URL",
|
|
38
|
+
"help": "Your Supabase project URL"
|
|
39
|
+
},
|
|
40
|
+
"serviceRoleKey": {
|
|
41
|
+
"label": "Service Role Key",
|
|
42
|
+
"help": "Supabase service role key (keep secret!)",
|
|
43
|
+
"sensitive": true
|
|
44
|
+
},
|
|
45
|
+
"userId": {
|
|
46
|
+
"label": "User ID",
|
|
47
|
+
"help": "Your user UUID from the Ofiere dashboard"
|
|
48
|
+
},
|
|
49
|
+
"agentId": {
|
|
50
|
+
"label": "Agent ID",
|
|
51
|
+
"help": "This agent's ID (e.g. 'sasha')"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ofiere-openclaw-plugin",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "OpenClaw plugin for Ofiere PM — manage tasks, agents, and projects from your agent",
|
|
6
|
+
"keywords": ["openclaw", "ofiere", "project-management", "agents", "plugin"],
|
|
7
|
+
"homepage": "https://github.com/gilanggemar/Ofiere",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/gilanggemar/Ofiere.git"
|
|
11
|
+
},
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"files": [
|
|
14
|
+
"index.ts",
|
|
15
|
+
"src",
|
|
16
|
+
"openclaw.plugin.json",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"openclaw": {
|
|
20
|
+
"extensions": [
|
|
21
|
+
"./index.ts"
|
|
22
|
+
],
|
|
23
|
+
"compat": {
|
|
24
|
+
"pluginApi": ">=2026.3.24-beta.2",
|
|
25
|
+
"minGatewayVersion": "2026.3.24-beta.2"
|
|
26
|
+
},
|
|
27
|
+
"build": {
|
|
28
|
+
"openclawVersion": "2026.3.24-beta.2",
|
|
29
|
+
"pluginSdkVersion": "2026.3.24-beta.2"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@supabase/supabase-js": "^2.98.0",
|
|
34
|
+
"zod": "^3.25.11"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// src/agent-resolver.ts — Dynamic agent identity resolution
|
|
2
|
+
// Resolves an OpenClaw account name (e.g. "ivy") to a Ofiere agent UUID.
|
|
3
|
+
// Caches lookups so only the first call per agent hits Supabase.
|
|
4
|
+
// Auto-registers unknown agents so multi-agent setups work out of the box.
|
|
5
|
+
|
|
6
|
+
import type { SupabaseClient } from "@supabase/supabase-js";
|
|
7
|
+
|
|
8
|
+
const cache = new Map<string, string>();
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Resolves an OpenClaw accountId to a Ofiere agent UUID.
|
|
12
|
+
*
|
|
13
|
+
* Strategy:
|
|
14
|
+
* 1. Check in-memory cache
|
|
15
|
+
* 2. Look up by name (case-insensitive) in agents table
|
|
16
|
+
* 3. If not found, auto-register a new agent record
|
|
17
|
+
* 4. Cache the result for subsequent calls
|
|
18
|
+
*
|
|
19
|
+
* @param accountId - The OpenClaw account name (e.g. "ivy", "daisy")
|
|
20
|
+
* @param userId - The Ofiere user UUID who owns this agent
|
|
21
|
+
* @param supabase - Supabase client
|
|
22
|
+
* @returns The Ofiere agent UUID
|
|
23
|
+
*/
|
|
24
|
+
export async function resolveAgentId(
|
|
25
|
+
accountId: string,
|
|
26
|
+
userId: string,
|
|
27
|
+
supabase: SupabaseClient,
|
|
28
|
+
): Promise<string> {
|
|
29
|
+
if (!accountId) return "";
|
|
30
|
+
|
|
31
|
+
const cacheKey = `${userId}:${accountId}`;
|
|
32
|
+
|
|
33
|
+
// 1. Cache hit
|
|
34
|
+
if (cache.has(cacheKey)) {
|
|
35
|
+
return cache.get(cacheKey)!;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 2. Look up by name (case-insensitive)
|
|
39
|
+
const { data: existing } = await supabase
|
|
40
|
+
.from("agents")
|
|
41
|
+
.select("id")
|
|
42
|
+
.eq("user_id", userId)
|
|
43
|
+
.ilike("name", accountId)
|
|
44
|
+
.limit(1)
|
|
45
|
+
.single();
|
|
46
|
+
|
|
47
|
+
if (existing?.id) {
|
|
48
|
+
cache.set(cacheKey, existing.id);
|
|
49
|
+
return existing.id;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 3. Also try matching by codename
|
|
53
|
+
const { data: byCodename } = await supabase
|
|
54
|
+
.from("agents")
|
|
55
|
+
.select("id")
|
|
56
|
+
.eq("user_id", userId)
|
|
57
|
+
.ilike("codename", accountId)
|
|
58
|
+
.limit(1)
|
|
59
|
+
.single();
|
|
60
|
+
|
|
61
|
+
if (byCodename?.id) {
|
|
62
|
+
cache.set(cacheKey, byCodename.id);
|
|
63
|
+
return byCodename.id;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 4. Auto-register a new agent
|
|
67
|
+
const newId = `agent-${accountId.toLowerCase()}-${Date.now()}`;
|
|
68
|
+
const { data: created } = await supabase
|
|
69
|
+
.from("agents")
|
|
70
|
+
.insert({
|
|
71
|
+
id: newId,
|
|
72
|
+
user_id: userId,
|
|
73
|
+
name: accountId.charAt(0).toUpperCase() + accountId.slice(1).toLowerCase(),
|
|
74
|
+
codename: accountId.toLowerCase(),
|
|
75
|
+
status: "active",
|
|
76
|
+
role: "operative",
|
|
77
|
+
level: 1,
|
|
78
|
+
xp: 0,
|
|
79
|
+
created_at: new Date().toISOString(),
|
|
80
|
+
})
|
|
81
|
+
.select("id")
|
|
82
|
+
.single();
|
|
83
|
+
|
|
84
|
+
const resolvedId = created?.id || newId;
|
|
85
|
+
cache.set(cacheKey, resolvedId);
|
|
86
|
+
return resolvedId;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Pre-warm the cache with a known mapping.
|
|
91
|
+
* Used when OFIERE_AGENT_ID env var is set (legacy single-agent mode).
|
|
92
|
+
*/
|
|
93
|
+
export function seedAgentCache(accountId: string, userId: string, agentId: string): void {
|
|
94
|
+
if (accountId && agentId) {
|
|
95
|
+
cache.set(`${userId}:${accountId}`, agentId);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Clear the cache (useful for testing).
|
|
101
|
+
*/
|
|
102
|
+
export function clearAgentCache(): void {
|
|
103
|
+
cache.clear();
|
|
104
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
// src/cli.ts — CLI registration for Ofiere PM plugin
|
|
2
|
+
// Uses api.registerCli with descriptors as documented:
|
|
3
|
+
// https://docs.openclaw.ai/plugins/sdk-overview#cli-registration-metadata
|
|
4
|
+
//
|
|
5
|
+
// The descriptors array enables lazy plugin CLI registration and root help text.
|
|
6
|
+
// The async ({ program }) => {} pattern is shown in the official Matrix plugin example.
|
|
7
|
+
|
|
8
|
+
import * as fs from "node:fs";
|
|
9
|
+
import * as os from "node:os";
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
import * as readline from "node:readline";
|
|
12
|
+
import { createClient } from "@supabase/supabase-js";
|
|
13
|
+
|
|
14
|
+
// ── Config paths ─────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
/** Auto-detect OpenClaw home for env file */
|
|
17
|
+
function getOpenClawHome(): string {
|
|
18
|
+
if (fs.existsSync("/data/.openclaw")) return "/data/.openclaw";
|
|
19
|
+
return path.join(os.homedir(), ".openclaw");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getEnvFile(): string {
|
|
23
|
+
return path.join(getOpenClawHome(), ".env");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Read env vars from the .env file */
|
|
27
|
+
function readEnvFile(): Record<string, string> {
|
|
28
|
+
const envPath = getEnvFile();
|
|
29
|
+
const result: Record<string, string> = {};
|
|
30
|
+
try {
|
|
31
|
+
const content = fs.readFileSync(envPath, "utf-8");
|
|
32
|
+
for (const line of content.split("\n")) {
|
|
33
|
+
const match = line.match(/^([^#=]+)=(.*)$/);
|
|
34
|
+
if (match) {
|
|
35
|
+
result[match[1].trim()] = match[2].trim();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
} catch { /* file doesn't exist yet */ }
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Append or update env vars in the .env file (idempotent) */
|
|
43
|
+
function setEnvVars(vars: Record<string, string>): void {
|
|
44
|
+
const envPath = getEnvFile();
|
|
45
|
+
const dir = path.dirname(envPath);
|
|
46
|
+
if (!fs.existsSync(dir)) {
|
|
47
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let content = "";
|
|
51
|
+
try {
|
|
52
|
+
content = fs.readFileSync(envPath, "utf-8");
|
|
53
|
+
} catch { /* file doesn't exist yet */ }
|
|
54
|
+
|
|
55
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
56
|
+
const regex = new RegExp(`^${key}=.*$`, "m");
|
|
57
|
+
if (regex.test(content)) {
|
|
58
|
+
content = content.replace(regex, `${key}=${value}`);
|
|
59
|
+
} else {
|
|
60
|
+
content = content.trimEnd() + `\n${key}=${value}`;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
fs.writeFileSync(envPath, content.trim() + "\n");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function getPluginConfig(): {
|
|
68
|
+
supabaseUrl?: string;
|
|
69
|
+
serviceRoleKey?: string;
|
|
70
|
+
userId?: string;
|
|
71
|
+
agentId?: string;
|
|
72
|
+
enabled?: boolean;
|
|
73
|
+
} {
|
|
74
|
+
const env = readEnvFile();
|
|
75
|
+
return {
|
|
76
|
+
supabaseUrl: process.env.OFIERE_SUPABASE_URL || env.OFIERE_SUPABASE_URL || undefined,
|
|
77
|
+
serviceRoleKey: process.env.OFIERE_SERVICE_ROLE_KEY || env.OFIERE_SERVICE_ROLE_KEY || undefined,
|
|
78
|
+
userId: process.env.OFIERE_USER_ID || env.OFIERE_USER_ID || undefined,
|
|
79
|
+
agentId: process.env.OFIERE_AGENT_ID || env.OFIERE_AGENT_ID || undefined,
|
|
80
|
+
enabled: true,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function ask(rl: readline.Interface, question: string): Promise<string> {
|
|
85
|
+
return new Promise((resolve) => rl.question(question, resolve));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ── CLI Registration ─────────────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
export function registerCli(api: any): void {
|
|
91
|
+
api.registerCli(
|
|
92
|
+
// Async registrar — follows the pattern from the official Matrix plugin example
|
|
93
|
+
async ({ program }: { program: any }) => {
|
|
94
|
+
const cmd = program
|
|
95
|
+
.command("ofiere")
|
|
96
|
+
.description("Ofiere PM dashboard integration");
|
|
97
|
+
|
|
98
|
+
// ── setup ──────────────────────────────────────────────────────────
|
|
99
|
+
cmd
|
|
100
|
+
.command("setup")
|
|
101
|
+
.description("Configure Ofiere PM connection (writes to .env, never touches openclaw.json)")
|
|
102
|
+
.option("--supabase-url <url>", "Supabase project URL")
|
|
103
|
+
.option("--service-key <key>", "Supabase service role key")
|
|
104
|
+
.option("--user-id <id>", "Ofiere user UUID")
|
|
105
|
+
.action(async (opts: {
|
|
106
|
+
supabaseUrl?: string;
|
|
107
|
+
serviceKey?: string;
|
|
108
|
+
userId?: string;
|
|
109
|
+
}) => {
|
|
110
|
+
let supabaseUrl = opts.supabaseUrl?.trim();
|
|
111
|
+
let serviceKey = opts.serviceKey?.trim();
|
|
112
|
+
let userId = opts.userId?.trim();
|
|
113
|
+
|
|
114
|
+
// Interactive mode if any field is missing
|
|
115
|
+
if (!supabaseUrl || !serviceKey || !userId) {
|
|
116
|
+
console.log("\nOfiere PM Setup\n");
|
|
117
|
+
const rl = readline.createInterface({
|
|
118
|
+
input: process.stdin,
|
|
119
|
+
output: process.stdout,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
if (!supabaseUrl) {
|
|
123
|
+
supabaseUrl = (await ask(rl, "Supabase URL (https://xxx.supabase.co): ")).trim();
|
|
124
|
+
}
|
|
125
|
+
if (!serviceKey) {
|
|
126
|
+
serviceKey = (await ask(rl, "Service role key: ")).trim();
|
|
127
|
+
}
|
|
128
|
+
if (!userId) {
|
|
129
|
+
userId = (await ask(rl, "User ID (UUID): ")).trim();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
rl.close();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!supabaseUrl || !serviceKey || !userId) {
|
|
136
|
+
console.log("\nAll fields are required. Setup cancelled.");
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Write to .env file (NEVER touches openclaw.json)
|
|
141
|
+
setEnvVars({
|
|
142
|
+
OFIERE_SUPABASE_URL: supabaseUrl,
|
|
143
|
+
OFIERE_SERVICE_ROLE_KEY: serviceKey,
|
|
144
|
+
OFIERE_USER_ID: userId,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const envFile = getEnvFile();
|
|
148
|
+
console.log(`\nDone. Saved to ${envFile}`);
|
|
149
|
+
console.log(` Supabase URL: ${supabaseUrl}`);
|
|
150
|
+
console.log(` User ID: ${userId}`);
|
|
151
|
+
console.log(` Plugin: enabled (global — all agents)`);
|
|
152
|
+
console.log("\nRestart to apply: openclaw gateway restart\n");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// ── status ─────────────────────────────────────────────────────────
|
|
156
|
+
cmd
|
|
157
|
+
.command("status")
|
|
158
|
+
.description("Show Ofiere PM plugin configuration")
|
|
159
|
+
.action(async () => {
|
|
160
|
+
const cfg = getPluginConfig();
|
|
161
|
+
console.log("\nOfiere PM Status\n");
|
|
162
|
+
|
|
163
|
+
if (!cfg.supabaseUrl) {
|
|
164
|
+
console.log(" Not configured. Run: openclaw ofiere setup\n");
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const maskedKey = cfg.serviceRoleKey
|
|
169
|
+
? `${cfg.serviceRoleKey.slice(0, 10)}...${cfg.serviceRoleKey.slice(-4)}`
|
|
170
|
+
: "not set";
|
|
171
|
+
|
|
172
|
+
console.log(` Supabase URL: ${cfg.supabaseUrl}`);
|
|
173
|
+
console.log(` Service Key: ${maskedKey}`);
|
|
174
|
+
console.log(` User ID: ${cfg.userId || "not set"}`);
|
|
175
|
+
console.log(` Agent ID: ${cfg.agentId || "(auto-detect — all agents)"}`);
|
|
176
|
+
console.log(` Config Source: ${getEnvFile()}`);
|
|
177
|
+
console.log("");
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// ── doctor ─────────────────────────────────────────────────────────
|
|
181
|
+
cmd
|
|
182
|
+
.command("doctor")
|
|
183
|
+
.description("Test Ofiere PM connection")
|
|
184
|
+
.action(async () => {
|
|
185
|
+
const cfg = getPluginConfig();
|
|
186
|
+
console.log("\nOfiere PM Doctor\n");
|
|
187
|
+
|
|
188
|
+
if (!cfg.supabaseUrl || !cfg.serviceRoleKey || !cfg.userId) {
|
|
189
|
+
console.log(" Not configured. Run: openclaw ofiere setup\n");
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
console.log(` Supabase URL: ${cfg.supabaseUrl}`);
|
|
194
|
+
console.log(` Agent Mode: ${cfg.agentId ? `fixed (${cfg.agentId})` : "auto-detect (all agents)"}`);
|
|
195
|
+
console.log("\n Testing connection...");
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
const sb = createClient(cfg.supabaseUrl, cfg.serviceRoleKey, {
|
|
199
|
+
auth: { persistSession: false },
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const { data, error } = await sb
|
|
203
|
+
.from("agents")
|
|
204
|
+
.select("id, name, status")
|
|
205
|
+
.eq("user_id", cfg.userId)
|
|
206
|
+
.order("name");
|
|
207
|
+
|
|
208
|
+
if (error) {
|
|
209
|
+
console.log(`\n ✗ Connection failed: ${error.message}\n`);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
console.log(` ✓ Found ${(data || []).length} agents\n`);
|
|
214
|
+
for (const agent of data || []) {
|
|
215
|
+
const marker = agent.id === cfg.agentId ? " ← PINNED" : "";
|
|
216
|
+
console.log(` ${agent.id} — ${agent.name} (${agent.status})${marker}`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const { count } = await sb
|
|
220
|
+
.from("tasks")
|
|
221
|
+
.select("id", { count: "exact", head: true })
|
|
222
|
+
.eq("user_id", cfg.userId);
|
|
223
|
+
|
|
224
|
+
console.log(`\n Tasks accessible: ${count ?? 0}`);
|
|
225
|
+
console.log("\n Status: healthy ✓\n");
|
|
226
|
+
} catch (e) {
|
|
227
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
228
|
+
console.log(`\n ✗ Connection failed: ${msg}`);
|
|
229
|
+
console.log("\n Possible causes:");
|
|
230
|
+
console.log(" - Invalid Supabase URL or service key");
|
|
231
|
+
console.log(" - Network issue reaching Supabase");
|
|
232
|
+
console.log(" - Invalid user ID\n");
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
},
|
|
236
|
+
// Descriptors enable lazy registration and root help text
|
|
237
|
+
{
|
|
238
|
+
descriptors: [
|
|
239
|
+
{
|
|
240
|
+
name: "ofiere",
|
|
241
|
+
description: "Manage Ofiere PM dashboard integration (setup, status, diagnostics)",
|
|
242
|
+
hasSubcommands: true,
|
|
243
|
+
},
|
|
244
|
+
],
|
|
245
|
+
},
|
|
246
|
+
);
|
|
247
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { OfiereConfig } from "./types.js";
|
|
3
|
+
|
|
4
|
+
export const OfiereConfigSchema = z.object({
|
|
5
|
+
enabled: z.boolean().default(true),
|
|
6
|
+
supabaseUrl: z.string().default(""),
|
|
7
|
+
serviceRoleKey: z.string().default(""),
|
|
8
|
+
userId: z.string().default(""),
|
|
9
|
+
agentId: z.string().default(""),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export function parseOfiereConfig(value: unknown): OfiereConfig {
|
|
13
|
+
const raw =
|
|
14
|
+
value && typeof value === "object" && !Array.isArray(value)
|
|
15
|
+
? (value as Record<string, unknown>)
|
|
16
|
+
: {};
|
|
17
|
+
|
|
18
|
+
const configObj = raw.config as Record<string, unknown> | undefined;
|
|
19
|
+
|
|
20
|
+
// Support both nested (config.supabaseUrl) and flat (supabaseUrl) access
|
|
21
|
+
// Also fall back to env vars
|
|
22
|
+
const supabaseUrl =
|
|
23
|
+
(typeof configObj?.supabaseUrl === "string" && configObj.supabaseUrl.trim()) ||
|
|
24
|
+
(typeof raw.supabaseUrl === "string" && raw.supabaseUrl.trim()) ||
|
|
25
|
+
process.env.OFIERE_SUPABASE_URL ||
|
|
26
|
+
process.env.SUPABASE_URL ||
|
|
27
|
+
"";
|
|
28
|
+
|
|
29
|
+
const serviceRoleKey =
|
|
30
|
+
(typeof configObj?.serviceRoleKey === "string" && configObj.serviceRoleKey.trim()) ||
|
|
31
|
+
(typeof raw.serviceRoleKey === "string" && raw.serviceRoleKey.trim()) ||
|
|
32
|
+
process.env.OFIERE_SERVICE_ROLE_KEY ||
|
|
33
|
+
process.env.SUPABASE_SERVICE_ROLE_KEY ||
|
|
34
|
+
"";
|
|
35
|
+
|
|
36
|
+
const userId =
|
|
37
|
+
(typeof configObj?.userId === "string" && configObj.userId.trim()) ||
|
|
38
|
+
(typeof raw.userId === "string" && raw.userId.trim()) ||
|
|
39
|
+
process.env.OFIERE_USER_ID ||
|
|
40
|
+
"";
|
|
41
|
+
|
|
42
|
+
const agentId =
|
|
43
|
+
(typeof configObj?.agentId === "string" && configObj.agentId.trim()) ||
|
|
44
|
+
(typeof raw.agentId === "string" && raw.agentId.trim()) ||
|
|
45
|
+
process.env.OFIERE_AGENT_ID ||
|
|
46
|
+
"";
|
|
47
|
+
|
|
48
|
+
return OfiereConfigSchema.parse({
|
|
49
|
+
...raw,
|
|
50
|
+
supabaseUrl,
|
|
51
|
+
serviceRoleKey,
|
|
52
|
+
userId,
|
|
53
|
+
agentId,
|
|
54
|
+
});
|
|
55
|
+
}
|
package/src/prompt.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
export function getSystemPrompt(state: {
|
|
2
|
+
ready: boolean;
|
|
3
|
+
toolCount: number;
|
|
4
|
+
agentId: string;
|
|
5
|
+
connectError: string;
|
|
6
|
+
}): string {
|
|
7
|
+
if (state.ready && state.toolCount > 0) {
|
|
8
|
+
const agentLine = state.agentId
|
|
9
|
+
? `Your agent ID is "${state.agentId}". You are registered in the Ofiere system.`
|
|
10
|
+
: `Your agent identity will be auto-detected at runtime. When you call any OFIERE tool, the system knows who you are.`;
|
|
11
|
+
|
|
12
|
+
const assignRule = state.agentId
|
|
13
|
+
? `When you create a task without specifying agent_id, it is assigned to YOU (${state.agentId}).`
|
|
14
|
+
: `When you create a task without specifying agent_id, it is assigned to YOU automatically.`;
|
|
15
|
+
|
|
16
|
+
return `<ofiere-pm>
|
|
17
|
+
You are connected to the Ofiere Project Management dashboard via the Ofiere PM plugin.
|
|
18
|
+
${agentLine}
|
|
19
|
+
|
|
20
|
+
## Your Ofiere PM Capabilities
|
|
21
|
+
You have ${state.toolCount} tools to manage the PM dashboard:
|
|
22
|
+
|
|
23
|
+
- **OFIERE_LIST_TASKS** — List and filter tasks from the PM dashboard
|
|
24
|
+
- **OFIERE_CREATE_TASK** — Create new tasks (auto-assigned to you if no agent_id given)
|
|
25
|
+
- **OFIERE_UPDATE_TASK** — Update task status, priority, progress, etc.
|
|
26
|
+
- **OFIERE_DELETE_TASK** — Delete tasks
|
|
27
|
+
- **OFIERE_LIST_AGENTS** — See all available agents for task assignment
|
|
28
|
+
|
|
29
|
+
## Rules
|
|
30
|
+
- ${assignRule}
|
|
31
|
+
- To create an unassigned task, pass agent_id as "none" or "unassigned".
|
|
32
|
+
- When the user says "create a task for [agent name]", use OFIERE_LIST_AGENTS to find the agent ID, then pass it as agent_id.
|
|
33
|
+
- Always confirm task creation/updates by reporting back what was done.
|
|
34
|
+
- Task statuses are: PENDING, IN_PROGRESS, DONE, FAILED.
|
|
35
|
+
- Priority levels: 0=LOW, 1=MEDIUM, 2=HIGH, 3=CRITICAL.
|
|
36
|
+
- Changes you make appear in the Ofiere dashboard immediately in real time.
|
|
37
|
+
- Do NOT fabricate task IDs — always use OFIERE_LIST_TASKS to look up real IDs.
|
|
38
|
+
</ofiere-pm>`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (state.ready) {
|
|
42
|
+
const diagnostic = diagnoseError(state.connectError);
|
|
43
|
+
return `<ofiere-pm>
|
|
44
|
+
The Ofiere PM plugin failed to connect.${state.connectError ? ` Error: ${state.connectError}` : ""}
|
|
45
|
+
|
|
46
|
+
Diagnosis: ${diagnostic.reason}
|
|
47
|
+
|
|
48
|
+
When the user asks about task management or the Ofiere dashboard, respond with:
|
|
49
|
+
"${diagnostic.userMessage}"
|
|
50
|
+
|
|
51
|
+
Do NOT pretend Ofiere tools exist or hallucinate tool calls. You have zero Ofiere tools available.
|
|
52
|
+
</ofiere-pm>`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return `<ofiere-pm>
|
|
56
|
+
The Ofiere PM plugin is loading. Tools should be available shortly.
|
|
57
|
+
If the user asks about tasks right now, ask them to wait a moment.
|
|
58
|
+
</ofiere-pm>`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function diagnoseError(error: string): { reason: string; userMessage: string } {
|
|
62
|
+
const lower = error.toLowerCase();
|
|
63
|
+
|
|
64
|
+
if (!error) {
|
|
65
|
+
return {
|
|
66
|
+
reason: "Connected but no tools were registered.",
|
|
67
|
+
userMessage:
|
|
68
|
+
"The Ofiere PM plugin connected but could not register tools. " +
|
|
69
|
+
"Run `openclaw ofiere doctor` to diagnose.",
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (lower.includes("supabase") || lower.includes("url") || lower.includes("key")) {
|
|
74
|
+
return {
|
|
75
|
+
reason: "Supabase connection configuration issue.",
|
|
76
|
+
userMessage:
|
|
77
|
+
"The Ofiere PM plugin could not connect to Supabase. " +
|
|
78
|
+
"Check your configuration with `openclaw ofiere status` and re-run " +
|
|
79
|
+
"`openclaw ofiere setup` if needed, then `openclaw gateway restart`.",
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (lower.includes("user_id") || lower.includes("userid")) {
|
|
84
|
+
return {
|
|
85
|
+
reason: "Missing or invalid user ID in configuration.",
|
|
86
|
+
userMessage:
|
|
87
|
+
"The Ofiere PM plugin needs a valid user ID. " +
|
|
88
|
+
"Run `openclaw ofiere setup` with your user UUID, then `openclaw gateway restart`.",
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
reason: `Unexpected error: ${error}`,
|
|
94
|
+
userMessage: `The Ofiere PM plugin encountered an error: ${error}. Run \`openclaw ofiere doctor\` for details.`,
|
|
95
|
+
};
|
|
96
|
+
}
|
package/src/supabase.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createClient, type SupabaseClient } from "@supabase/supabase-js";
|
|
2
|
+
|
|
3
|
+
let _client: SupabaseClient | null = null;
|
|
4
|
+
|
|
5
|
+
export function getSupabase(supabaseUrl: string, serviceRoleKey: string): SupabaseClient {
|
|
6
|
+
if (_client) return _client;
|
|
7
|
+
|
|
8
|
+
_client = createClient(supabaseUrl, serviceRoleKey, {
|
|
9
|
+
auth: { persistSession: false },
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
return _client;
|
|
13
|
+
}
|
package/src/tools.ts
ADDED
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
// src/tools.ts — Tool registration for Ofiere PM plugin
|
|
2
|
+
// Uses api.registerTool(tool, opts?) as documented:
|
|
3
|
+
// https://docs.openclaw.ai/plugins/sdk-overview#tools-and-commands
|
|
4
|
+
// https://docs.openclaw.ai/plugins/building-plugins#registering-agent-tools
|
|
5
|
+
//
|
|
6
|
+
// - Required tools: always available (no opts)
|
|
7
|
+
// - Optional tools: { optional: true } — user must allowlist or allowlist the plugin id
|
|
8
|
+
|
|
9
|
+
import type { SupabaseClient } from "@supabase/supabase-js";
|
|
10
|
+
import type { OfiereConfig } from "./types.js";
|
|
11
|
+
import { resolveAgentId } from "./agent-resolver.js";
|
|
12
|
+
|
|
13
|
+
// ─── Tool result shape (matches OpenClaw SDK) ────────────────────────────────
|
|
14
|
+
|
|
15
|
+
interface ToolResult {
|
|
16
|
+
content: Array<{ type: "text"; text: string }>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function ok(data: unknown): ToolResult {
|
|
20
|
+
return {
|
|
21
|
+
content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function err(message: string): ToolResult {
|
|
26
|
+
return {
|
|
27
|
+
content: [{ type: "text" as const, text: `Error: ${message}` }],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ─── Helper: extract calling agent's accountId from OpenClaw context ─────────
|
|
32
|
+
|
|
33
|
+
function getCallingAgentName(api: any): string {
|
|
34
|
+
// OpenClaw passes agent context in various ways — try all known paths
|
|
35
|
+
try {
|
|
36
|
+
return (
|
|
37
|
+
api?.agentContext?.accountId ||
|
|
38
|
+
api?.agentContext?.name ||
|
|
39
|
+
api?.currentAgent?.accountId ||
|
|
40
|
+
api?.currentAgent?.name ||
|
|
41
|
+
""
|
|
42
|
+
);
|
|
43
|
+
} catch {
|
|
44
|
+
return "";
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ─── Tool Registration ───────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
export function registerTools(
|
|
51
|
+
api: any, // OpenClawPluginApi — typed as any to avoid import-path issues at install time
|
|
52
|
+
supabase: SupabaseClient,
|
|
53
|
+
config: OfiereConfig,
|
|
54
|
+
): void {
|
|
55
|
+
const userId = config.userId;
|
|
56
|
+
const fallbackAgentId = config.agentId; // May be empty — that's fine
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Resolve the agent ID for the calling agent.
|
|
60
|
+
* Priority: explicit param > runtime context > env var fallback
|
|
61
|
+
*/
|
|
62
|
+
async function resolveAgent(explicitId?: string): Promise<string | null> {
|
|
63
|
+
// 1. Explicit agent_id passed by the LLM (e.g. "create task for Daisy")
|
|
64
|
+
if (explicitId && explicitId.trim()) return explicitId.trim();
|
|
65
|
+
|
|
66
|
+
// 2. Runtime: read calling agent's name from OpenClaw context
|
|
67
|
+
const callerName = getCallingAgentName(api);
|
|
68
|
+
if (callerName) {
|
|
69
|
+
try {
|
|
70
|
+
return await resolveAgentId(callerName, userId, supabase);
|
|
71
|
+
} catch {
|
|
72
|
+
// Fall through to env var
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 3. Env var fallback (OFIERE_AGENT_ID — legacy single-agent mode)
|
|
77
|
+
return fallbackAgentId || null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── OFIERE_LIST_TASKS — Required (read-only, no side effects) ────────
|
|
81
|
+
|
|
82
|
+
api.registerTool({
|
|
83
|
+
name: "OFIERE_LIST_TASKS",
|
|
84
|
+
label: "List Ofiere Tasks",
|
|
85
|
+
description:
|
|
86
|
+
"List tasks from the Ofiere PM dashboard. " +
|
|
87
|
+
"Optionally filter by space_id, folder_id, agent_id, or status. " +
|
|
88
|
+
"Returns an array of task objects with their details.",
|
|
89
|
+
parameters: {
|
|
90
|
+
type: "object",
|
|
91
|
+
properties: {
|
|
92
|
+
space_id: { type: "string", description: "Filter by PM space ID" },
|
|
93
|
+
folder_id: { type: "string", description: "Filter by PM folder ID" },
|
|
94
|
+
agent_id: { type: "string", description: "Filter by assigned agent ID" },
|
|
95
|
+
status: {
|
|
96
|
+
type: "string",
|
|
97
|
+
description: "Filter by status: PENDING, IN_PROGRESS, DONE, FAILED",
|
|
98
|
+
enum: ["PENDING", "IN_PROGRESS", "DONE", "FAILED"],
|
|
99
|
+
},
|
|
100
|
+
limit: { type: "number", description: "Max results (default 50)" },
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
async execute(_id: string, params: Record<string, unknown>) {
|
|
104
|
+
try {
|
|
105
|
+
let query = supabase
|
|
106
|
+
.from("tasks")
|
|
107
|
+
.select(
|
|
108
|
+
"id, title, description, status, priority, agent_id, space_id, folder_id, " +
|
|
109
|
+
"start_date, due_date, progress, created_at, updated_at",
|
|
110
|
+
)
|
|
111
|
+
.eq("user_id", userId)
|
|
112
|
+
.order("updated_at", { ascending: false });
|
|
113
|
+
|
|
114
|
+
if (params.space_id) query = query.eq("space_id", params.space_id as string);
|
|
115
|
+
if (params.folder_id) query = query.eq("folder_id", params.folder_id as string);
|
|
116
|
+
if (params.agent_id) query = query.eq("agent_id", params.agent_id as string);
|
|
117
|
+
if (params.status) query = query.eq("status", params.status as string);
|
|
118
|
+
query = query.limit((params.limit as number) || 50);
|
|
119
|
+
|
|
120
|
+
const { data, error } = await query;
|
|
121
|
+
if (error) return err(error.message);
|
|
122
|
+
return ok({ tasks: data || [], count: (data || []).length });
|
|
123
|
+
} catch (e) {
|
|
124
|
+
return err(e instanceof Error ? e.message : String(e));
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// ── OFIERE_CREATE_TASK — Optional (has side effects: writes to DB) ───
|
|
130
|
+
|
|
131
|
+
api.registerTool(
|
|
132
|
+
{
|
|
133
|
+
name: "OFIERE_CREATE_TASK",
|
|
134
|
+
label: "Create Ofiere Task",
|
|
135
|
+
description:
|
|
136
|
+
"Create a new task in the Ofiere PM dashboard. " +
|
|
137
|
+
"If agent_id is not provided, the task is automatically assigned to you (the calling agent). " +
|
|
138
|
+
"Pass agent_id as 'none' or 'unassigned' to create an unassigned task. " +
|
|
139
|
+
"The task will appear in the dashboard immediately via real-time sync.",
|
|
140
|
+
parameters: {
|
|
141
|
+
type: "object",
|
|
142
|
+
required: ["title"],
|
|
143
|
+
properties: {
|
|
144
|
+
title: { type: "string", description: "Task title (required)" },
|
|
145
|
+
description: { type: "string", description: "Task description" },
|
|
146
|
+
agent_id: {
|
|
147
|
+
type: "string",
|
|
148
|
+
description:
|
|
149
|
+
"Agent ID to assign the task to. If omitted, assigns to yourself. " +
|
|
150
|
+
"Pass 'none' or 'unassigned' to create a task with no assignee. " +
|
|
151
|
+
"Use OFIERE_LIST_AGENTS to see available agents.",
|
|
152
|
+
},
|
|
153
|
+
status: {
|
|
154
|
+
type: "string",
|
|
155
|
+
description: "Initial status (default: PENDING)",
|
|
156
|
+
enum: ["PENDING", "IN_PROGRESS", "DONE", "FAILED"],
|
|
157
|
+
},
|
|
158
|
+
priority: {
|
|
159
|
+
type: "number",
|
|
160
|
+
description: "Priority: 0=LOW, 1=MEDIUM, 2=HIGH, 3=CRITICAL (default: 1)",
|
|
161
|
+
},
|
|
162
|
+
space_id: { type: "string", description: "PM Space ID to place the task in" },
|
|
163
|
+
folder_id: { type: "string", description: "PM Folder ID to place the task in" },
|
|
164
|
+
start_date: { type: "string", description: "Start date (ISO 8601 format)" },
|
|
165
|
+
due_date: { type: "string", description: "Due date (ISO 8601 format)" },
|
|
166
|
+
tags: {
|
|
167
|
+
type: "array",
|
|
168
|
+
items: { type: "string" },
|
|
169
|
+
description: "Tags for the task",
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
async execute(_id: string, params: Record<string, unknown>) {
|
|
174
|
+
try {
|
|
175
|
+
if (!params.title) return err("Missing required field: title");
|
|
176
|
+
|
|
177
|
+
const id = `task-${Date.now()}`;
|
|
178
|
+
const now = new Date().toISOString();
|
|
179
|
+
|
|
180
|
+
// Handle explicit "none"/"unassigned"
|
|
181
|
+
const rawAgentId = params.agent_id as string | undefined;
|
|
182
|
+
const isUnassigned =
|
|
183
|
+
rawAgentId &&
|
|
184
|
+
["none", "unassigned", "null", ""].includes(rawAgentId.toLowerCase().trim());
|
|
185
|
+
|
|
186
|
+
const assignee = isUnassigned ? null : await resolveAgent(rawAgentId);
|
|
187
|
+
|
|
188
|
+
const insertData: Record<string, unknown> = {
|
|
189
|
+
id,
|
|
190
|
+
user_id: userId,
|
|
191
|
+
title: params.title,
|
|
192
|
+
description: (params.description as string) || null,
|
|
193
|
+
agent_id: assignee,
|
|
194
|
+
assignee_type: "agent",
|
|
195
|
+
status: (params.status as string) || "PENDING",
|
|
196
|
+
priority: params.priority !== undefined ? params.priority : 1,
|
|
197
|
+
space_id: (params.space_id as string) || null,
|
|
198
|
+
folder_id: (params.folder_id as string) || null,
|
|
199
|
+
start_date: (params.start_date as string) || null,
|
|
200
|
+
due_date: (params.due_date as string) || null,
|
|
201
|
+
tags: (params.tags as string[]) || [],
|
|
202
|
+
progress: 0,
|
|
203
|
+
sort_order: 0,
|
|
204
|
+
custom_fields: {},
|
|
205
|
+
created_at: now,
|
|
206
|
+
updated_at: now,
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const { error } = await supabase.from("tasks").insert(insertData);
|
|
210
|
+
|
|
211
|
+
if (error) {
|
|
212
|
+
if (error.message?.includes("agent_id") || error.message?.includes("foreign key")) {
|
|
213
|
+
insertData.agent_id = null;
|
|
214
|
+
const retry = await supabase.from("tasks").insert(insertData);
|
|
215
|
+
if (retry.error) return err(retry.error.message);
|
|
216
|
+
return ok({
|
|
217
|
+
id,
|
|
218
|
+
message: `Task "${params.title}" created (agent_id "${assignee}" was invalid, assigned to none)`,
|
|
219
|
+
task: insertData,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
return err(error.message);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return ok({
|
|
226
|
+
id,
|
|
227
|
+
message: `Task "${params.title}" created and assigned to ${assignee || "no one"}`,
|
|
228
|
+
task: insertData,
|
|
229
|
+
});
|
|
230
|
+
} catch (e) {
|
|
231
|
+
return err(e instanceof Error ? e.message : String(e));
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
{ optional: true },
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
// ── OFIERE_UPDATE_TASK — Optional (has side effects) ─────────────────
|
|
239
|
+
|
|
240
|
+
api.registerTool(
|
|
241
|
+
{
|
|
242
|
+
name: "OFIERE_UPDATE_TASK",
|
|
243
|
+
label: "Update Ofiere Task",
|
|
244
|
+
description:
|
|
245
|
+
"Update an existing task in the Ofiere PM dashboard. Only provided fields are changed. " +
|
|
246
|
+
"Changes appear in the dashboard immediately via real-time sync.",
|
|
247
|
+
parameters: {
|
|
248
|
+
type: "object",
|
|
249
|
+
required: ["task_id"],
|
|
250
|
+
properties: {
|
|
251
|
+
task_id: { type: "string", description: "The task ID to update (required)" },
|
|
252
|
+
title: { type: "string", description: "New title" },
|
|
253
|
+
description: { type: "string", description: "New description" },
|
|
254
|
+
status: {
|
|
255
|
+
type: "string",
|
|
256
|
+
description: "New status",
|
|
257
|
+
enum: ["PENDING", "IN_PROGRESS", "DONE", "FAILED"],
|
|
258
|
+
},
|
|
259
|
+
priority: { type: "number", description: "New priority (0-3)" },
|
|
260
|
+
progress: { type: "number", description: "Progress percentage (0-100)" },
|
|
261
|
+
agent_id: { type: "string", description: "Reassign to a different agent" },
|
|
262
|
+
start_date: { type: "string", description: "New start date (ISO 8601)" },
|
|
263
|
+
due_date: { type: "string", description: "New due date (ISO 8601)" },
|
|
264
|
+
tags: {
|
|
265
|
+
type: "array",
|
|
266
|
+
items: { type: "string" },
|
|
267
|
+
description: "New tags",
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
async execute(_id: string, params: Record<string, unknown>) {
|
|
272
|
+
try {
|
|
273
|
+
if (!params.task_id) return err("Missing required field: task_id");
|
|
274
|
+
|
|
275
|
+
const updates: Record<string, unknown> = { updated_at: new Date().toISOString() };
|
|
276
|
+
const fields = [
|
|
277
|
+
"title", "description", "status", "priority", "progress",
|
|
278
|
+
"agent_id", "start_date", "due_date", "tags",
|
|
279
|
+
];
|
|
280
|
+
for (const f of fields) {
|
|
281
|
+
if (params[f] !== undefined) updates[f] = params[f];
|
|
282
|
+
}
|
|
283
|
+
if (params.status === "DONE") updates.completed_at = new Date().toISOString();
|
|
284
|
+
|
|
285
|
+
const { data, error } = await supabase
|
|
286
|
+
.from("tasks")
|
|
287
|
+
.update(updates)
|
|
288
|
+
.eq("id", params.task_id as string)
|
|
289
|
+
.eq("user_id", userId)
|
|
290
|
+
.select("id, title, status, priority, agent_id")
|
|
291
|
+
.single();
|
|
292
|
+
|
|
293
|
+
if (error) return err(error.message);
|
|
294
|
+
return ok({ message: `Task "${data?.title}" updated`, task: data });
|
|
295
|
+
} catch (e) {
|
|
296
|
+
return err(e instanceof Error ? e.message : String(e));
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
{ optional: true },
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
// ── OFIERE_DELETE_TASK — Optional (destructive side effect) ──────────
|
|
304
|
+
|
|
305
|
+
api.registerTool(
|
|
306
|
+
{
|
|
307
|
+
name: "OFIERE_DELETE_TASK",
|
|
308
|
+
label: "Delete Ofiere Task",
|
|
309
|
+
description:
|
|
310
|
+
"Delete a task from the Ofiere PM dashboard. Also removes subtasks and linked scheduler events.",
|
|
311
|
+
parameters: {
|
|
312
|
+
type: "object",
|
|
313
|
+
required: ["task_id"],
|
|
314
|
+
properties: {
|
|
315
|
+
task_id: { type: "string", description: "The task ID to delete (required)" },
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
async execute(_id: string, params: Record<string, unknown>) {
|
|
319
|
+
try {
|
|
320
|
+
if (!params.task_id) return err("Missing required field: task_id");
|
|
321
|
+
const taskId = params.task_id as string;
|
|
322
|
+
|
|
323
|
+
await supabase.from("scheduler_events").delete().eq("task_id", taskId);
|
|
324
|
+
|
|
325
|
+
const { data: subtasks } = await supabase
|
|
326
|
+
.from("tasks")
|
|
327
|
+
.select("id")
|
|
328
|
+
.eq("parent_task_id", taskId)
|
|
329
|
+
.eq("user_id", userId);
|
|
330
|
+
|
|
331
|
+
if (subtasks && subtasks.length > 0) {
|
|
332
|
+
for (const sub of subtasks) {
|
|
333
|
+
await supabase.from("scheduler_events").delete().eq("task_id", sub.id);
|
|
334
|
+
}
|
|
335
|
+
await supabase
|
|
336
|
+
.from("tasks")
|
|
337
|
+
.delete()
|
|
338
|
+
.in("id", subtasks.map((s: { id: string }) => s.id))
|
|
339
|
+
.eq("user_id", userId);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const { error } = await supabase
|
|
343
|
+
.from("tasks")
|
|
344
|
+
.delete()
|
|
345
|
+
.eq("id", taskId)
|
|
346
|
+
.eq("user_id", userId);
|
|
347
|
+
|
|
348
|
+
if (error) return err(error.message);
|
|
349
|
+
return ok({ message: `Task ${taskId} deleted`, deleted: true });
|
|
350
|
+
} catch (e) {
|
|
351
|
+
return err(e instanceof Error ? e.message : String(e));
|
|
352
|
+
}
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
{ optional: true },
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
// ── OFIERE_LIST_AGENTS — Required (read-only, no side effects) ───────
|
|
359
|
+
|
|
360
|
+
api.registerTool({
|
|
361
|
+
name: "OFIERE_LIST_AGENTS",
|
|
362
|
+
label: "List Ofiere Agents",
|
|
363
|
+
description:
|
|
364
|
+
"List all available agents in the Ofiere system. " +
|
|
365
|
+
"Shows agent IDs, names, roles, and current status. " +
|
|
366
|
+
"Use this to find the right agent_id for task assignment.",
|
|
367
|
+
parameters: {
|
|
368
|
+
type: "object",
|
|
369
|
+
properties: {},
|
|
370
|
+
},
|
|
371
|
+
async execute(_id: string, _params: Record<string, unknown>) {
|
|
372
|
+
try {
|
|
373
|
+
// Resolve calling agent's ID for the "your_agent_id" hint
|
|
374
|
+
const callerName = getCallingAgentName(api);
|
|
375
|
+
let yourAgentId = fallbackAgentId || "";
|
|
376
|
+
if (callerName && !yourAgentId) {
|
|
377
|
+
try {
|
|
378
|
+
yourAgentId = await resolveAgentId(callerName, userId, supabase);
|
|
379
|
+
} catch { /* ignore */ }
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const { data, error } = await supabase
|
|
383
|
+
.from("agents")
|
|
384
|
+
.select("id, name, codename, role, status")
|
|
385
|
+
.eq("user_id", userId)
|
|
386
|
+
.order("name");
|
|
387
|
+
|
|
388
|
+
if (error) return err(error.message);
|
|
389
|
+
return ok({
|
|
390
|
+
agents: data || [],
|
|
391
|
+
count: (data || []).length,
|
|
392
|
+
your_agent_id: yourAgentId,
|
|
393
|
+
});
|
|
394
|
+
} catch (e) {
|
|
395
|
+
return err(e instanceof Error ? e.message : String(e));
|
|
396
|
+
}
|
|
397
|
+
},
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
const callerName = getCallingAgentName(api);
|
|
401
|
+
const agentLabel = fallbackAgentId || callerName || "auto-detect";
|
|
402
|
+
api.logger.info(`[ofiere] 5 tools registered (agent: ${agentLabel})`);
|
|
403
|
+
}
|