ofiere-openclaw-plugin 4.54.2 → 4.55.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 +104 -104
- package/dist/src/prompt.js +130 -130
- package/dist/src/staffPersona.js +3 -1
- package/dist/src/tools.js +111 -0
- package/index.ts +105 -105
- package/package.json +12 -2
- package/src/agent-resolver.ts +90 -90
- package/src/agent-tier.ts +192 -192
- package/src/attach-token.ts +106 -106
- package/src/attachments.ts +601 -601
- package/src/cli.ts +247 -247
- package/src/config.ts +78 -78
- package/src/prompt.ts +267 -267
- package/src/sop-render.ts +216 -216
- package/src/staffPersona.ts +299 -289
- package/src/supabase.ts +13 -13
- package/src/tools.ts +112 -0
- package/src/types/openclaw.d.ts +8 -8
- package/src/types.ts +10 -10
package/dist/src/tools.js
CHANGED
|
@@ -7214,6 +7214,117 @@ function registerBrainExtractionHook(api, supabase, userId, fallbackAgentId) {
|
|
|
7214
7214
|
// not the chief's. The chief gets a single L4 report entry above.
|
|
7215
7215
|
return;
|
|
7216
7216
|
}
|
|
7217
|
+
// FIX-A2A-LATE-REPLY — direct chief↔chief A2A late reply.
|
|
7218
|
+
// Dashboard's relayToOpenClaw caps the synchronous wait at 45s; when
|
|
7219
|
+
// the responding chief takes longer (e.g. while running its own tool
|
|
7220
|
+
// calls), the dashboard WS closes and the eventual reply is dropped.
|
|
7221
|
+
// dispatch_context was pre-written with { conversation_id,
|
|
7222
|
+
// source_agent_id, a2a_pair_id } so we can route the late reply back:
|
|
7223
|
+
// 1. Insert the assistant turn into conversation_messages for the
|
|
7224
|
+
// A2A conversation so reloading the page shows the reply.
|
|
7225
|
+
// 2. POST a `chief_reply` webhook so the dashboard SSE can swap the
|
|
7226
|
+
// "still processing" placeholder bubble in the active chat UI.
|
|
7227
|
+
// Guarded by: a2a_pair_id is set AND subagent_id is null AND task_id
|
|
7228
|
+
// is null — so neither the staff nor the scheduled branches above
|
|
7229
|
+
// can collide with this path.
|
|
7230
|
+
const a2aPairId = dispatchCtxRow?.a2a_pair_id ?? null;
|
|
7231
|
+
const a2aSourceAgentId = dispatchCtxRow?.source_agent_id ?? null;
|
|
7232
|
+
const a2aConversationId = dispatchCtxRow?.conversation_id ?? null;
|
|
7233
|
+
if (a2aPairId &&
|
|
7234
|
+
!staffDispatchSubagentId &&
|
|
7235
|
+
!dispatchTaskIdFromCtx &&
|
|
7236
|
+
a2aConversationId) {
|
|
7237
|
+
(async () => {
|
|
7238
|
+
try {
|
|
7239
|
+
const excerpt = lastAssistant.slice(0, 4000);
|
|
7240
|
+
if (!excerpt) {
|
|
7241
|
+
api.logger.warn?.(`[ofiere-a2a-reply] empty assistant text — skipping late-reply persistence`);
|
|
7242
|
+
return;
|
|
7243
|
+
}
|
|
7244
|
+
// Insert assistant turn into the A2A conversation. The
|
|
7245
|
+
// dashboard's synchronous path also writes the assistant row
|
|
7246
|
+
// when the reply lands in-window — UI dedup is by content +
|
|
7247
|
+
// a2a_conversation_id so a duplicate from a race is benign.
|
|
7248
|
+
try {
|
|
7249
|
+
await supabase.from("conversation_messages").insert({
|
|
7250
|
+
conversation_id: a2aConversationId,
|
|
7251
|
+
user_id: userId,
|
|
7252
|
+
role: "assistant",
|
|
7253
|
+
content: excerpt,
|
|
7254
|
+
metadata: {
|
|
7255
|
+
sender_agent_id: resolvedAgentId,
|
|
7256
|
+
target_agent_id: a2aSourceAgentId,
|
|
7257
|
+
late_reply: true,
|
|
7258
|
+
},
|
|
7259
|
+
branch_id: "main",
|
|
7260
|
+
});
|
|
7261
|
+
await supabase.from("conversations")
|
|
7262
|
+
.update({ updated_at: new Date().toISOString() })
|
|
7263
|
+
.eq("id", a2aConversationId)
|
|
7264
|
+
.eq("user_id", userId);
|
|
7265
|
+
// Best-effort message-count bump. RPC may not exist on older
|
|
7266
|
+
// schemas — swallow the error if it 404s.
|
|
7267
|
+
try {
|
|
7268
|
+
await supabase.rpc("increment_conversation_message_count", {
|
|
7269
|
+
conv_id: a2aConversationId,
|
|
7270
|
+
});
|
|
7271
|
+
}
|
|
7272
|
+
catch { /* RPC missing — count drift is acceptable */ }
|
|
7273
|
+
}
|
|
7274
|
+
catch (cmErr) {
|
|
7275
|
+
api.logger.warn?.(`[ofiere-a2a-reply] conversation_messages insert failed: ${cmErr instanceof Error ? cmErr.message : String(cmErr)}`);
|
|
7276
|
+
}
|
|
7277
|
+
// Resolve responder agent name for the webhook payload so the
|
|
7278
|
+
// UI bubble can be attributed correctly without another DB hit.
|
|
7279
|
+
let responderName = null;
|
|
7280
|
+
try {
|
|
7281
|
+
const { data: agentRow } = await supabase
|
|
7282
|
+
.from("agents")
|
|
7283
|
+
.select("name")
|
|
7284
|
+
.eq("id", resolvedAgentId)
|
|
7285
|
+
.eq("user_id", userId)
|
|
7286
|
+
.maybeSingle();
|
|
7287
|
+
responderName = agentRow?.name ?? null;
|
|
7288
|
+
}
|
|
7289
|
+
catch { /* fall back to agent id */ }
|
|
7290
|
+
const webhookUrl = process.env.OFIERE_DASHBOARD_WEBHOOK_URL || "";
|
|
7291
|
+
const webhookSecret = process.env.OPENCLAW_WEBHOOK_SECRET || "";
|
|
7292
|
+
if (webhookUrl && webhookSecret) {
|
|
7293
|
+
try {
|
|
7294
|
+
await fetch(webhookUrl, {
|
|
7295
|
+
method: "POST",
|
|
7296
|
+
headers: {
|
|
7297
|
+
"content-type": "application/json",
|
|
7298
|
+
authorization: `Bearer ${webhookSecret}`,
|
|
7299
|
+
},
|
|
7300
|
+
body: JSON.stringify({
|
|
7301
|
+
type: "chief_reply",
|
|
7302
|
+
payload: {
|
|
7303
|
+
user_id: userId,
|
|
7304
|
+
source_agent_id: a2aSourceAgentId,
|
|
7305
|
+
target_agent_id: resolvedAgentId,
|
|
7306
|
+
target_agent_name: responderName,
|
|
7307
|
+
conversation_id: a2aConversationId,
|
|
7308
|
+
a2a_pair_id: a2aPairId,
|
|
7309
|
+
response: excerpt,
|
|
7310
|
+
},
|
|
7311
|
+
}),
|
|
7312
|
+
});
|
|
7313
|
+
api.logger.info?.(`[ofiere-a2a-reply] chief_reply webhook posted (conv=${a2aConversationId})`);
|
|
7314
|
+
}
|
|
7315
|
+
catch (wErr) {
|
|
7316
|
+
api.logger.debug?.(`[ofiere-a2a-reply] webhook post failed: ${wErr instanceof Error ? wErr.message : String(wErr)}`);
|
|
7317
|
+
}
|
|
7318
|
+
}
|
|
7319
|
+
}
|
|
7320
|
+
catch (aErr) {
|
|
7321
|
+
api.logger.warn?.(`[ofiere-a2a-reply] failed: ${aErr instanceof Error ? aErr.message : String(aErr)}`);
|
|
7322
|
+
}
|
|
7323
|
+
})();
|
|
7324
|
+
// Continue to brain extraction below — A2A late-reply is purely
|
|
7325
|
+
// additive bookkeeping; the responding chief still owns the L1/L2
|
|
7326
|
+
// memory writes for its own turn.
|
|
7327
|
+
}
|
|
7217
7328
|
// BUG 9 fix (BUGSHOOT-2): mark task_dispatch_log row complete for
|
|
7218
7329
|
// non-staff scheduled dispatches too. Without this the log row stays
|
|
7219
7330
|
// at 'dispatched' even after the chief finished the run.
|
package/index.ts
CHANGED
|
@@ -1,105 +1,105 @@
|
|
|
1
|
-
// index.ts — Ofiere PM Plugin for OpenClaw
|
|
2
|
-
// Matches Composio plugin pattern: plain object export + api.on()
|
|
3
|
-
|
|
4
|
-
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
5
|
-
import { parseOfiereConfig } from "./src/config.js";
|
|
6
|
-
import { getSupabase } from "./src/supabase.js";
|
|
7
|
-
import { registerTools, probeApiForAgentName } from "./src/tools.js";
|
|
8
|
-
import { getSystemPrompt } from "./src/prompt.js";
|
|
9
|
-
import { registerCli } from "./src/cli.js";
|
|
10
|
-
import { seedAgentCache } from "./src/agent-resolver.js";
|
|
11
|
-
|
|
12
|
-
const ofierePlugin = {
|
|
13
|
-
id: "ofiere",
|
|
14
|
-
name: "Ofiere PM",
|
|
15
|
-
description:
|
|
16
|
-
"Manage Ofiere PM tasks, agents, and projects directly from the agent. " +
|
|
17
|
-
"Create tasks, update progress, assign agents — all synced to the dashboard in real time.",
|
|
18
|
-
|
|
19
|
-
register(api: OpenClawPluginApi) {
|
|
20
|
-
const config = parseOfiereConfig(api.pluginConfig);
|
|
21
|
-
|
|
22
|
-
// Always register CLI (even if disabled — so user can run `openclaw ofiere setup`)
|
|
23
|
-
registerCli(api);
|
|
24
|
-
|
|
25
|
-
if (!config.enabled) {
|
|
26
|
-
api.logger.debug?.("[ofiere] Plugin disabled via config");
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
if (!config.supabaseUrl || !config.serviceRoleKey) {
|
|
31
|
-
api.logger.warn(
|
|
32
|
-
"[ofiere] Not configured. Run: openclaw ofiere setup",
|
|
33
|
-
);
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (!config.userId) {
|
|
38
|
-
api.logger.warn(
|
|
39
|
-
"[ofiere] Missing userId. Run: openclaw ofiere setup",
|
|
40
|
-
);
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// ── Pre-seed agent cache if OFIERE_AGENT_ID is set (legacy mode) ──────
|
|
45
|
-
if (config.agentId) {
|
|
46
|
-
const callerName =
|
|
47
|
-
(api as any)?.agentContext?.accountId ||
|
|
48
|
-
(api as any)?.agentContext?.name ||
|
|
49
|
-
(api as any)?.currentAgent?.accountId ||
|
|
50
|
-
"";
|
|
51
|
-
if (callerName) {
|
|
52
|
-
seedAgentCache(callerName, config.userId, config.agentId);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// ── State for system prompt injection ──────────────────────────────────
|
|
57
|
-
const promptState = {
|
|
58
|
-
toolCount: 0,
|
|
59
|
-
agentId: config.agentId,
|
|
60
|
-
connectError: "",
|
|
61
|
-
ready: false,
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
// ── System prompt is built ONCE (it's static after init) ──────────────
|
|
65
|
-
// No hook needed here — the brain context hook in tools.ts uses
|
|
66
|
-
// prependSystemContext via getSystemPromptCached() to combine both
|
|
67
|
-
// in a single hook invocation, eliminating one serial hook call.
|
|
68
|
-
let cachedSystemPrompt = "";
|
|
69
|
-
const getSystemPromptCached = () => {
|
|
70
|
-
if (!cachedSystemPrompt) {
|
|
71
|
-
cachedSystemPrompt = getSystemPrompt(promptState);
|
|
72
|
-
}
|
|
73
|
-
return cachedSystemPrompt;
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
// Hook: inject BOTH system prompt + brain context in one call
|
|
77
|
-
api.on("before_prompt_build", () => ({
|
|
78
|
-
prependSystemContext: getSystemPromptCached(),
|
|
79
|
-
}));
|
|
80
|
-
|
|
81
|
-
// ── Connect to Supabase and register tools ────────────────────────────
|
|
82
|
-
try {
|
|
83
|
-
const supabase = getSupabase(config.supabaseUrl, config.serviceRoleKey);
|
|
84
|
-
|
|
85
|
-
// Probe the api object for any agent identity info (for debugging + fallback)
|
|
86
|
-
probeApiForAgentName(api, api.logger);
|
|
87
|
-
|
|
88
|
-
// registerTools now returns the count — no more hardcoding
|
|
89
|
-
const toolCount = registerTools(api, supabase, config);
|
|
90
|
-
promptState.toolCount = toolCount;
|
|
91
|
-
promptState.ready = true;
|
|
92
|
-
const agentLabel = config.agentId || "auto-detect";
|
|
93
|
-
api.logger.info(
|
|
94
|
-
`[ofiere] Ready — ${toolCount} meta-tools registered (agent: ${agentLabel})`,
|
|
95
|
-
);
|
|
96
|
-
} catch (e) {
|
|
97
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
98
|
-
promptState.connectError = msg;
|
|
99
|
-
promptState.ready = true;
|
|
100
|
-
api.logger.error(`[ofiere] Failed to initialize: ${msg}`);
|
|
101
|
-
}
|
|
102
|
-
},
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
export default ofierePlugin;
|
|
1
|
+
// index.ts — Ofiere PM Plugin for OpenClaw
|
|
2
|
+
// Matches Composio plugin pattern: plain object export + api.on()
|
|
3
|
+
|
|
4
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
5
|
+
import { parseOfiereConfig } from "./src/config.js";
|
|
6
|
+
import { getSupabase } from "./src/supabase.js";
|
|
7
|
+
import { registerTools, probeApiForAgentName } from "./src/tools.js";
|
|
8
|
+
import { getSystemPrompt } from "./src/prompt.js";
|
|
9
|
+
import { registerCli } from "./src/cli.js";
|
|
10
|
+
import { seedAgentCache } from "./src/agent-resolver.js";
|
|
11
|
+
|
|
12
|
+
const ofierePlugin = {
|
|
13
|
+
id: "ofiere",
|
|
14
|
+
name: "Ofiere PM",
|
|
15
|
+
description:
|
|
16
|
+
"Manage Ofiere PM tasks, agents, and projects directly from the agent. " +
|
|
17
|
+
"Create tasks, update progress, assign agents — all synced to the dashboard in real time.",
|
|
18
|
+
|
|
19
|
+
register(api: OpenClawPluginApi) {
|
|
20
|
+
const config = parseOfiereConfig(api.pluginConfig);
|
|
21
|
+
|
|
22
|
+
// Always register CLI (even if disabled — so user can run `openclaw ofiere setup`)
|
|
23
|
+
registerCli(api);
|
|
24
|
+
|
|
25
|
+
if (!config.enabled) {
|
|
26
|
+
api.logger.debug?.("[ofiere] Plugin disabled via config");
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!config.supabaseUrl || !config.serviceRoleKey) {
|
|
31
|
+
api.logger.warn(
|
|
32
|
+
"[ofiere] Not configured. Run: openclaw ofiere setup",
|
|
33
|
+
);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!config.userId) {
|
|
38
|
+
api.logger.warn(
|
|
39
|
+
"[ofiere] Missing userId. Run: openclaw ofiere setup",
|
|
40
|
+
);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ── Pre-seed agent cache if OFIERE_AGENT_ID is set (legacy mode) ──────
|
|
45
|
+
if (config.agentId) {
|
|
46
|
+
const callerName =
|
|
47
|
+
(api as any)?.agentContext?.accountId ||
|
|
48
|
+
(api as any)?.agentContext?.name ||
|
|
49
|
+
(api as any)?.currentAgent?.accountId ||
|
|
50
|
+
"";
|
|
51
|
+
if (callerName) {
|
|
52
|
+
seedAgentCache(callerName, config.userId, config.agentId);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ── State for system prompt injection ──────────────────────────────────
|
|
57
|
+
const promptState = {
|
|
58
|
+
toolCount: 0,
|
|
59
|
+
agentId: config.agentId,
|
|
60
|
+
connectError: "",
|
|
61
|
+
ready: false,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// ── System prompt is built ONCE (it's static after init) ──────────────
|
|
65
|
+
// No hook needed here — the brain context hook in tools.ts uses
|
|
66
|
+
// prependSystemContext via getSystemPromptCached() to combine both
|
|
67
|
+
// in a single hook invocation, eliminating one serial hook call.
|
|
68
|
+
let cachedSystemPrompt = "";
|
|
69
|
+
const getSystemPromptCached = () => {
|
|
70
|
+
if (!cachedSystemPrompt) {
|
|
71
|
+
cachedSystemPrompt = getSystemPrompt(promptState);
|
|
72
|
+
}
|
|
73
|
+
return cachedSystemPrompt;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// Hook: inject BOTH system prompt + brain context in one call
|
|
77
|
+
api.on("before_prompt_build", () => ({
|
|
78
|
+
prependSystemContext: getSystemPromptCached(),
|
|
79
|
+
}));
|
|
80
|
+
|
|
81
|
+
// ── Connect to Supabase and register tools ────────────────────────────
|
|
82
|
+
try {
|
|
83
|
+
const supabase = getSupabase(config.supabaseUrl, config.serviceRoleKey);
|
|
84
|
+
|
|
85
|
+
// Probe the api object for any agent identity info (for debugging + fallback)
|
|
86
|
+
probeApiForAgentName(api, api.logger);
|
|
87
|
+
|
|
88
|
+
// registerTools now returns the count — no more hardcoding
|
|
89
|
+
const toolCount = registerTools(api, supabase, config);
|
|
90
|
+
promptState.toolCount = toolCount;
|
|
91
|
+
promptState.ready = true;
|
|
92
|
+
const agentLabel = config.agentId || "auto-detect";
|
|
93
|
+
api.logger.info(
|
|
94
|
+
`[ofiere] Ready — ${toolCount} meta-tools registered (agent: ${agentLabel})`,
|
|
95
|
+
);
|
|
96
|
+
} catch (e) {
|
|
97
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
98
|
+
promptState.connectError = msg;
|
|
99
|
+
promptState.ready = true;
|
|
100
|
+
api.logger.error(`[ofiere] Failed to initialize: ${msg}`);
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export default ofierePlugin;
|
package/package.json
CHANGED
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ofiere-openclaw-plugin",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.55.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "OpenClaw plugin for Ofiere PM - 17 meta-tools covering tasks, agents, projects, scheduling, knowledge, workflows, notifications, memory, prompts, constellation, space file management, execution plan builder, SOP management, agent brain, talent management, corporate frameworks, and agent office canvas",
|
|
6
|
-
"keywords": [
|
|
6
|
+
"keywords": [
|
|
7
|
+
"openclaw",
|
|
8
|
+
"ofiere",
|
|
9
|
+
"project-management",
|
|
10
|
+
"agents",
|
|
11
|
+
"plugin"
|
|
12
|
+
],
|
|
7
13
|
"homepage": "https://github.com/gilanggemar/Ofiere",
|
|
8
14
|
"repository": {
|
|
9
15
|
"type": "git",
|
|
10
16
|
"url": "https://github.com/gilanggemar/Ofiere.git"
|
|
11
17
|
},
|
|
12
18
|
"license": "MIT",
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=20"
|
|
21
|
+
},
|
|
13
22
|
"main": "./dist/index.js",
|
|
14
23
|
"files": [
|
|
15
24
|
"index.ts",
|
|
@@ -45,6 +54,7 @@
|
|
|
45
54
|
"zod": "^3.25.11"
|
|
46
55
|
},
|
|
47
56
|
"devDependencies": {
|
|
57
|
+
"@types/node": "^25.8.0",
|
|
48
58
|
"typescript": "^5.6.0"
|
|
49
59
|
}
|
|
50
60
|
}
|
package/src/agent-resolver.ts
CHANGED
|
@@ -1,90 +1,90 @@
|
|
|
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 OR codename in a single query (v4.30.0 optimization)
|
|
39
|
-
const { data: existing } = await supabase
|
|
40
|
-
.from("agents")
|
|
41
|
-
.select("id")
|
|
42
|
-
.eq("user_id", userId)
|
|
43
|
-
.or(`name.ilike.${accountId},codename.ilike.${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. Auto-register a new agent
|
|
53
|
-
const newId = `agent-${accountId.toLowerCase()}-${Date.now()}`;
|
|
54
|
-
const { data: created } = await supabase
|
|
55
|
-
.from("agents")
|
|
56
|
-
.insert({
|
|
57
|
-
id: newId,
|
|
58
|
-
user_id: userId,
|
|
59
|
-
name: accountId.charAt(0).toUpperCase() + accountId.slice(1).toLowerCase(),
|
|
60
|
-
codename: accountId.toLowerCase(),
|
|
61
|
-
status: "active",
|
|
62
|
-
role: "operative",
|
|
63
|
-
level: 1,
|
|
64
|
-
xp: 0,
|
|
65
|
-
created_at: new Date().toISOString(),
|
|
66
|
-
})
|
|
67
|
-
.select("id")
|
|
68
|
-
.single();
|
|
69
|
-
|
|
70
|
-
const resolvedId = created?.id || newId;
|
|
71
|
-
cache.set(cacheKey, resolvedId);
|
|
72
|
-
return resolvedId;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Pre-warm the cache with a known mapping.
|
|
77
|
-
* Used when OFIERE_AGENT_ID env var is set (legacy single-agent mode).
|
|
78
|
-
*/
|
|
79
|
-
export function seedAgentCache(accountId: string, userId: string, agentId: string): void {
|
|
80
|
-
if (accountId && agentId) {
|
|
81
|
-
cache.set(`${userId}:${accountId}`, agentId);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Clear the cache (useful for testing).
|
|
87
|
-
*/
|
|
88
|
-
export function clearAgentCache(): void {
|
|
89
|
-
cache.clear();
|
|
90
|
-
}
|
|
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 OR codename in a single query (v4.30.0 optimization)
|
|
39
|
+
const { data: existing } = await supabase
|
|
40
|
+
.from("agents")
|
|
41
|
+
.select("id")
|
|
42
|
+
.eq("user_id", userId)
|
|
43
|
+
.or(`name.ilike.${accountId},codename.ilike.${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. Auto-register a new agent
|
|
53
|
+
const newId = `agent-${accountId.toLowerCase()}-${Date.now()}`;
|
|
54
|
+
const { data: created } = await supabase
|
|
55
|
+
.from("agents")
|
|
56
|
+
.insert({
|
|
57
|
+
id: newId,
|
|
58
|
+
user_id: userId,
|
|
59
|
+
name: accountId.charAt(0).toUpperCase() + accountId.slice(1).toLowerCase(),
|
|
60
|
+
codename: accountId.toLowerCase(),
|
|
61
|
+
status: "active",
|
|
62
|
+
role: "operative",
|
|
63
|
+
level: 1,
|
|
64
|
+
xp: 0,
|
|
65
|
+
created_at: new Date().toISOString(),
|
|
66
|
+
})
|
|
67
|
+
.select("id")
|
|
68
|
+
.single();
|
|
69
|
+
|
|
70
|
+
const resolvedId = created?.id || newId;
|
|
71
|
+
cache.set(cacheKey, resolvedId);
|
|
72
|
+
return resolvedId;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Pre-warm the cache with a known mapping.
|
|
77
|
+
* Used when OFIERE_AGENT_ID env var is set (legacy single-agent mode).
|
|
78
|
+
*/
|
|
79
|
+
export function seedAgentCache(accountId: string, userId: string, agentId: string): void {
|
|
80
|
+
if (accountId && agentId) {
|
|
81
|
+
cache.set(`${userId}:${accountId}`, agentId);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Clear the cache (useful for testing).
|
|
87
|
+
*/
|
|
88
|
+
export function clearAgentCache(): void {
|
|
89
|
+
cache.clear();
|
|
90
|
+
}
|