ofiere-openclaw-plugin 4.50.0-probe.0 → 4.50.0-probe.2
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/dist/index.js +82 -0
- package/dist/src/agent-resolver.js +75 -0
- package/dist/src/agent-tier.js +147 -0
- package/dist/src/attach-token.js +71 -0
- package/dist/src/attachments.js +466 -0
- package/dist/src/cli.js +208 -0
- package/dist/src/config.js +59 -0
- package/dist/src/prompt.js +229 -0
- package/dist/src/sop-render.js +190 -0
- package/dist/src/staffPersona.js +189 -0
- package/dist/src/supabase.js +10 -0
- package/dist/src/tools.js +6985 -0
- package/dist/src/types.js +1 -0
- package/openclaw.plugin.json +23 -0
- package/package.json +8 -2
- package/src/tools.ts +5 -4
- package/tsconfig.json +6 -2
package/dist/index.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// index.ts — Ofiere PM Plugin for OpenClaw
|
|
2
|
+
// Matches Composio plugin pattern: plain object export + api.on()
|
|
3
|
+
import { parseOfiereConfig } from "./src/config.js";
|
|
4
|
+
import { getSupabase } from "./src/supabase.js";
|
|
5
|
+
import { registerTools, probeApiForAgentName } from "./src/tools.js";
|
|
6
|
+
import { getSystemPrompt } from "./src/prompt.js";
|
|
7
|
+
import { registerCli } from "./src/cli.js";
|
|
8
|
+
import { seedAgentCache } from "./src/agent-resolver.js";
|
|
9
|
+
const ofierePlugin = {
|
|
10
|
+
id: "ofiere",
|
|
11
|
+
name: "Ofiere PM",
|
|
12
|
+
description: "Manage Ofiere PM tasks, agents, and projects directly from the agent. " +
|
|
13
|
+
"Create tasks, update progress, assign agents — all synced to the dashboard in real time.",
|
|
14
|
+
register(api) {
|
|
15
|
+
const config = parseOfiereConfig(api.pluginConfig);
|
|
16
|
+
// Always register CLI (even if disabled — so user can run `openclaw ofiere setup`)
|
|
17
|
+
registerCli(api);
|
|
18
|
+
if (!config.enabled) {
|
|
19
|
+
api.logger.debug?.("[ofiere] Plugin disabled via config");
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (!config.supabaseUrl || !config.serviceRoleKey) {
|
|
23
|
+
api.logger.warn("[ofiere] Not configured. Run: openclaw ofiere setup");
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (!config.userId) {
|
|
27
|
+
api.logger.warn("[ofiere] Missing userId. Run: openclaw ofiere setup");
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
// ── Pre-seed agent cache if OFIERE_AGENT_ID is set (legacy mode) ──────
|
|
31
|
+
if (config.agentId) {
|
|
32
|
+
const callerName = api?.agentContext?.accountId ||
|
|
33
|
+
api?.agentContext?.name ||
|
|
34
|
+
api?.currentAgent?.accountId ||
|
|
35
|
+
"";
|
|
36
|
+
if (callerName) {
|
|
37
|
+
seedAgentCache(callerName, config.userId, config.agentId);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// ── State for system prompt injection ──────────────────────────────────
|
|
41
|
+
const promptState = {
|
|
42
|
+
toolCount: 0,
|
|
43
|
+
agentId: config.agentId,
|
|
44
|
+
connectError: "",
|
|
45
|
+
ready: false,
|
|
46
|
+
};
|
|
47
|
+
// ── System prompt is built ONCE (it's static after init) ──────────────
|
|
48
|
+
// No hook needed here — the brain context hook in tools.ts uses
|
|
49
|
+
// prependSystemContext via getSystemPromptCached() to combine both
|
|
50
|
+
// in a single hook invocation, eliminating one serial hook call.
|
|
51
|
+
let cachedSystemPrompt = "";
|
|
52
|
+
const getSystemPromptCached = () => {
|
|
53
|
+
if (!cachedSystemPrompt) {
|
|
54
|
+
cachedSystemPrompt = getSystemPrompt(promptState);
|
|
55
|
+
}
|
|
56
|
+
return cachedSystemPrompt;
|
|
57
|
+
};
|
|
58
|
+
// Hook: inject BOTH system prompt + brain context in one call
|
|
59
|
+
api.on("before_prompt_build", () => ({
|
|
60
|
+
prependSystemContext: getSystemPromptCached(),
|
|
61
|
+
}));
|
|
62
|
+
// ── Connect to Supabase and register tools ────────────────────────────
|
|
63
|
+
try {
|
|
64
|
+
const supabase = getSupabase(config.supabaseUrl, config.serviceRoleKey);
|
|
65
|
+
// Probe the api object for any agent identity info (for debugging + fallback)
|
|
66
|
+
probeApiForAgentName(api, api.logger);
|
|
67
|
+
// registerTools now returns the count — no more hardcoding
|
|
68
|
+
const toolCount = registerTools(api, supabase, config);
|
|
69
|
+
promptState.toolCount = toolCount;
|
|
70
|
+
promptState.ready = true;
|
|
71
|
+
const agentLabel = config.agentId || "auto-detect";
|
|
72
|
+
api.logger.info(`[ofiere] Ready — ${toolCount} meta-tools registered (agent: ${agentLabel})`);
|
|
73
|
+
}
|
|
74
|
+
catch (e) {
|
|
75
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
76
|
+
promptState.connectError = msg;
|
|
77
|
+
promptState.ready = true;
|
|
78
|
+
api.logger.error(`[ofiere] Failed to initialize: ${msg}`);
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
export default ofierePlugin;
|
|
@@ -0,0 +1,75 @@
|
|
|
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
|
+
const cache = new Map();
|
|
6
|
+
/**
|
|
7
|
+
* Resolves an OpenClaw accountId to a Ofiere agent UUID.
|
|
8
|
+
*
|
|
9
|
+
* Strategy:
|
|
10
|
+
* 1. Check in-memory cache
|
|
11
|
+
* 2. Look up by name (case-insensitive) in agents table
|
|
12
|
+
* 3. If not found, auto-register a new agent record
|
|
13
|
+
* 4. Cache the result for subsequent calls
|
|
14
|
+
*
|
|
15
|
+
* @param accountId - The OpenClaw account name (e.g. "ivy", "daisy")
|
|
16
|
+
* @param userId - The Ofiere user UUID who owns this agent
|
|
17
|
+
* @param supabase - Supabase client
|
|
18
|
+
* @returns The Ofiere agent UUID
|
|
19
|
+
*/
|
|
20
|
+
export async function resolveAgentId(accountId, userId, supabase) {
|
|
21
|
+
if (!accountId)
|
|
22
|
+
return "";
|
|
23
|
+
const cacheKey = `${userId}:${accountId}`;
|
|
24
|
+
// 1. Cache hit
|
|
25
|
+
if (cache.has(cacheKey)) {
|
|
26
|
+
return cache.get(cacheKey);
|
|
27
|
+
}
|
|
28
|
+
// 2. Look up by name OR codename in a single query (v4.30.0 optimization)
|
|
29
|
+
const { data: existing } = await supabase
|
|
30
|
+
.from("agents")
|
|
31
|
+
.select("id")
|
|
32
|
+
.eq("user_id", userId)
|
|
33
|
+
.or(`name.ilike.${accountId},codename.ilike.${accountId}`)
|
|
34
|
+
.limit(1)
|
|
35
|
+
.single();
|
|
36
|
+
if (existing?.id) {
|
|
37
|
+
cache.set(cacheKey, existing.id);
|
|
38
|
+
return existing.id;
|
|
39
|
+
}
|
|
40
|
+
// 3. Auto-register a new agent
|
|
41
|
+
const newId = `agent-${accountId.toLowerCase()}-${Date.now()}`;
|
|
42
|
+
const { data: created } = await supabase
|
|
43
|
+
.from("agents")
|
|
44
|
+
.insert({
|
|
45
|
+
id: newId,
|
|
46
|
+
user_id: userId,
|
|
47
|
+
name: accountId.charAt(0).toUpperCase() + accountId.slice(1).toLowerCase(),
|
|
48
|
+
codename: accountId.toLowerCase(),
|
|
49
|
+
status: "active",
|
|
50
|
+
role: "operative",
|
|
51
|
+
level: 1,
|
|
52
|
+
xp: 0,
|
|
53
|
+
created_at: new Date().toISOString(),
|
|
54
|
+
})
|
|
55
|
+
.select("id")
|
|
56
|
+
.single();
|
|
57
|
+
const resolvedId = created?.id || newId;
|
|
58
|
+
cache.set(cacheKey, resolvedId);
|
|
59
|
+
return resolvedId;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Pre-warm the cache with a known mapping.
|
|
63
|
+
* Used when OFIERE_AGENT_ID env var is set (legacy single-agent mode).
|
|
64
|
+
*/
|
|
65
|
+
export function seedAgentCache(accountId, userId, agentId) {
|
|
66
|
+
if (accountId && agentId) {
|
|
67
|
+
cache.set(`${userId}:${accountId}`, agentId);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Clear the cache (useful for testing).
|
|
72
|
+
*/
|
|
73
|
+
export function clearAgentCache() {
|
|
74
|
+
cache.clear();
|
|
75
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// src/agent-tier.ts — Plugin-side agent tier resolver.
|
|
2
|
+
// Manual override (agent_tier_overrides) wins over auto-detection.
|
|
3
|
+
// Mirrors dashboard/lib/agentTier.ts contracts so the dashboard UI
|
|
4
|
+
// and plugin enforcement agree on every agent's tier.
|
|
5
|
+
// Hard-coded executive roster ids — match dashboard/lib/agentRoster.ts.
|
|
6
|
+
// Anything in this set defaults to c-suite when no manual override exists.
|
|
7
|
+
const ROSTER_IDS = new Set([
|
|
8
|
+
"ofie",
|
|
9
|
+
"daisy",
|
|
10
|
+
"ivy",
|
|
11
|
+
"celia",
|
|
12
|
+
"thalia",
|
|
13
|
+
"sasha",
|
|
14
|
+
"agent-zero",
|
|
15
|
+
]);
|
|
16
|
+
const TTL_MS = 5 * 60 * 1000;
|
|
17
|
+
const cache = new Map();
|
|
18
|
+
const k = (userId, agentId) => `${userId}::${agentId}`;
|
|
19
|
+
export function invalidateAgentTier(userId, agentId) {
|
|
20
|
+
if (!agentId) {
|
|
21
|
+
for (const key of cache.keys())
|
|
22
|
+
if (key.startsWith(`${userId}::`))
|
|
23
|
+
cache.delete(key);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
cache.delete(k(userId, agentId));
|
|
27
|
+
}
|
|
28
|
+
export async function resolveAgentTier(supabase, agentId, userId) {
|
|
29
|
+
const id = (agentId || "").trim();
|
|
30
|
+
if (!id || !userId)
|
|
31
|
+
return { tier: null, source: "none" };
|
|
32
|
+
const key = k(userId, id);
|
|
33
|
+
// 1. Manual override — ALWAYS fresh (uncached). Direct-SQL writes to
|
|
34
|
+
// agent_tier_overrides must win immediately, even when a non-override
|
|
35
|
+
// branch is already cached from a prior call.
|
|
36
|
+
let overrideRow = null;
|
|
37
|
+
let overrideQueryFailed = false;
|
|
38
|
+
try {
|
|
39
|
+
const { data } = await supabase
|
|
40
|
+
.from("agent_tier_overrides")
|
|
41
|
+
.select("tier")
|
|
42
|
+
.eq("user_id", userId)
|
|
43
|
+
.eq("agent_id", id)
|
|
44
|
+
.maybeSingle();
|
|
45
|
+
overrideRow = data ?? null;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// Table missing in older installs — skip override path
|
|
49
|
+
overrideQueryFailed = true;
|
|
50
|
+
}
|
|
51
|
+
if (overrideRow?.tier === "c-suite" || overrideRow?.tier === "staff") {
|
|
52
|
+
const result = { tier: overrideRow.tier, source: "manual" };
|
|
53
|
+
cache.set(key, { value: result, expires: Date.now() + TTL_MS });
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
// Override absent: if cache holds a stale `manual` entry from a deleted
|
|
57
|
+
// override row, drop it so the auto branches re-resolve.
|
|
58
|
+
if (!overrideQueryFailed) {
|
|
59
|
+
const cachedManual = cache.get(key);
|
|
60
|
+
if (cachedManual && cachedManual.value.source === "manual") {
|
|
61
|
+
cache.delete(key);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// 2. Cache lookup for non-override branches.
|
|
65
|
+
const hit = cache.get(key);
|
|
66
|
+
if (hit && hit.expires > Date.now())
|
|
67
|
+
return hit.value;
|
|
68
|
+
let result = { tier: null, source: "none" };
|
|
69
|
+
// 3. C-Suite: hardcoded roster
|
|
70
|
+
if (ROSTER_IDS.has(id)) {
|
|
71
|
+
result = { tier: "c-suite", source: "roster" };
|
|
72
|
+
cache.set(key, { value: result, expires: Date.now() + TTL_MS });
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
// 4. agent_architectures.executive_role / department_role
|
|
76
|
+
try {
|
|
77
|
+
const { data } = await supabase
|
|
78
|
+
.from("agent_architectures")
|
|
79
|
+
.select("executive_role, department_role")
|
|
80
|
+
.eq("user_id", userId)
|
|
81
|
+
.eq("agent_id", id)
|
|
82
|
+
.maybeSingle();
|
|
83
|
+
if (data?.department_role === "chief" ||
|
|
84
|
+
(data?.executive_role && String(data.executive_role).trim())) {
|
|
85
|
+
result = { tier: "c-suite", source: "executiveRole" };
|
|
86
|
+
cache.set(key, { value: result, expires: Date.now() + TTL_MS });
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
if (data?.department_role === "staff") {
|
|
90
|
+
result = { tier: "staff", source: "departmentRole" };
|
|
91
|
+
cache.set(key, { value: result, expires: Date.now() + TTL_MS });
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// Table missing — fall through
|
|
97
|
+
}
|
|
98
|
+
// 5. Staff: registered as subagent
|
|
99
|
+
try {
|
|
100
|
+
const { data } = await supabase
|
|
101
|
+
.from("agent_subagents")
|
|
102
|
+
.select("id")
|
|
103
|
+
.eq("user_id", userId)
|
|
104
|
+
.eq("id", id)
|
|
105
|
+
.maybeSingle();
|
|
106
|
+
if (data?.id) {
|
|
107
|
+
result = { tier: "staff", source: "subagent" };
|
|
108
|
+
cache.set(key, { value: result, expires: Date.now() + TTL_MS });
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// ignore
|
|
114
|
+
}
|
|
115
|
+
cache.set(key, { value: result, expires: Date.now() + TTL_MS });
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
export async function getAgentTier(supabase, agentId, userId) {
|
|
119
|
+
return (await resolveAgentTier(supabase, agentId, userId)).tier;
|
|
120
|
+
}
|
|
121
|
+
export async function resolveTargetTier(supabase, target, userId) {
|
|
122
|
+
if (target.subagentId) {
|
|
123
|
+
try {
|
|
124
|
+
const { data } = await supabase
|
|
125
|
+
.from("agent_subagents")
|
|
126
|
+
.select("id")
|
|
127
|
+
.eq("user_id", userId)
|
|
128
|
+
.eq("id", target.subagentId)
|
|
129
|
+
.maybeSingle();
|
|
130
|
+
if (data?.id)
|
|
131
|
+
return { tier: "staff", source: "subagent" };
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// Fall through to chief lookup
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (target.agentId)
|
|
138
|
+
return resolveAgentTier(supabase, target.agentId, userId);
|
|
139
|
+
return { tier: null, source: "none" };
|
|
140
|
+
}
|
|
141
|
+
export function isDocKindValidForTier(docKind, tier) {
|
|
142
|
+
if (tier === "c-suite")
|
|
143
|
+
return docKind === "framework";
|
|
144
|
+
if (tier === "staff")
|
|
145
|
+
return docKind === "sop";
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// src/attach-token.ts — HMAC confirmation token for agent self-attach.
|
|
2
|
+
// Mirrors dashboard/lib/attachmentToken.ts EXACTLY so tokens issued
|
|
3
|
+
// here verify against the dashboard and vice-versa.
|
|
4
|
+
//
|
|
5
|
+
// Format: base64url(payload).base64url(sig)
|
|
6
|
+
// payload = JSON({ u, k, t, d, dk, exp })
|
|
7
|
+
// sig = HMAC_SHA256(secret, payload)
|
|
8
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
9
|
+
const TOKEN_TTL_MS = 5 * 60 * 1000;
|
|
10
|
+
function getSecret() {
|
|
11
|
+
const s = process.env.OFIERE_ATTACH_SECRET;
|
|
12
|
+
if (!s)
|
|
13
|
+
throw new Error("OFIERE_ATTACH_SECRET is not configured");
|
|
14
|
+
return s;
|
|
15
|
+
}
|
|
16
|
+
function b64urlEncode(buf) {
|
|
17
|
+
const b = typeof buf === "string" ? Buffer.from(buf, "utf8") : buf;
|
|
18
|
+
return b.toString("base64").replace(/=+$/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
19
|
+
}
|
|
20
|
+
function b64urlDecode(s) {
|
|
21
|
+
const pad = s.length % 4 === 0 ? "" : "=".repeat(4 - (s.length % 4));
|
|
22
|
+
return Buffer.from(s.replace(/-/g, "+").replace(/_/g, "/") + pad, "base64");
|
|
23
|
+
}
|
|
24
|
+
export function issueAttachmentToken(args) {
|
|
25
|
+
const exp = Date.now() + (args.ttlMs ?? TOKEN_TTL_MS);
|
|
26
|
+
const payload = {
|
|
27
|
+
u: args.userId,
|
|
28
|
+
k: args.targetKind,
|
|
29
|
+
t: args.targetId,
|
|
30
|
+
d: [...args.docIds].sort(),
|
|
31
|
+
dk: args.docKind,
|
|
32
|
+
exp,
|
|
33
|
+
};
|
|
34
|
+
const json = JSON.stringify(payload);
|
|
35
|
+
const sig = createHmac("sha256", getSecret()).update(json).digest();
|
|
36
|
+
return `${b64urlEncode(json)}.${b64urlEncode(sig)}`;
|
|
37
|
+
}
|
|
38
|
+
export function verifyAttachmentToken(args) {
|
|
39
|
+
const parts = args.token.split(".");
|
|
40
|
+
if (parts.length !== 2)
|
|
41
|
+
return { ok: false, reason: "malformed" };
|
|
42
|
+
let payload;
|
|
43
|
+
try {
|
|
44
|
+
payload = JSON.parse(b64urlDecode(parts[0]).toString("utf8"));
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return { ok: false, reason: "malformed" };
|
|
48
|
+
}
|
|
49
|
+
const expectedJson = JSON.stringify(payload);
|
|
50
|
+
const expectedSig = createHmac("sha256", getSecret()).update(expectedJson).digest();
|
|
51
|
+
const providedSig = b64urlDecode(parts[1]);
|
|
52
|
+
if (expectedSig.length !== providedSig.length)
|
|
53
|
+
return { ok: false, reason: "bad_signature" };
|
|
54
|
+
if (!timingSafeEqual(expectedSig, providedSig))
|
|
55
|
+
return { ok: false, reason: "bad_signature" };
|
|
56
|
+
if (typeof payload.exp !== "number" || payload.exp < Date.now()) {
|
|
57
|
+
return { ok: false, reason: "expired" };
|
|
58
|
+
}
|
|
59
|
+
if (payload.u !== args.userId)
|
|
60
|
+
return { ok: false, reason: "mismatch" };
|
|
61
|
+
if (payload.k !== args.targetKind || payload.t !== args.targetId)
|
|
62
|
+
return { ok: false, reason: "mismatch" };
|
|
63
|
+
if (payload.dk !== args.docKind)
|
|
64
|
+
return { ok: false, reason: "mismatch" };
|
|
65
|
+
const sortedRequested = [...args.docIds].sort();
|
|
66
|
+
if (payload.d.length !== sortedRequested.length ||
|
|
67
|
+
payload.d.some((v, i) => v !== sortedRequested[i])) {
|
|
68
|
+
return { ok: false, reason: "mismatch" };
|
|
69
|
+
}
|
|
70
|
+
return { ok: true };
|
|
71
|
+
}
|