ofiere-openclaw-plugin 4.33.0 → 4.35.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/package.json +1 -1
- package/src/config.ts +9 -0
- package/src/prompt.ts +15 -1
- package/src/tools.ts +242 -22
- package/src/types.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ofiere-openclaw-plugin",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.35.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "OpenClaw plugin for Ofiere PM - 14 meta-tools covering tasks, agents, projects, scheduling, knowledge, workflows, notifications, memory, prompts, constellation, space file management, execution plan builder, SOP management, and agent brain (memory + self-improvement)",
|
|
6
6
|
"keywords": ["openclaw", "ofiere", "project-management", "agents", "plugin"],
|
package/src/config.ts
CHANGED
|
@@ -7,6 +7,7 @@ export const OfiereConfigSchema = z.object({
|
|
|
7
7
|
serviceRoleKey: z.string().default(""),
|
|
8
8
|
userId: z.string().default(""),
|
|
9
9
|
agentId: z.string().default(""),
|
|
10
|
+
timezone: z.string().default("Asia/Jakarta"),
|
|
10
11
|
});
|
|
11
12
|
|
|
12
13
|
// Cache the first successful config so per-agent re-registrations don't lose it
|
|
@@ -52,12 +53,20 @@ export function parseOfiereConfig(value: unknown): OfiereConfig {
|
|
|
52
53
|
_cachedConfig?.agentId ||
|
|
53
54
|
"";
|
|
54
55
|
|
|
56
|
+
const timezone =
|
|
57
|
+
(typeof configObj?.timezone === "string" && configObj.timezone.trim()) ||
|
|
58
|
+
(typeof raw.timezone === "string" && raw.timezone.trim()) ||
|
|
59
|
+
process.env.OFIERE_TIMEZONE ||
|
|
60
|
+
_cachedConfig?.timezone ||
|
|
61
|
+
"Asia/Jakarta";
|
|
62
|
+
|
|
55
63
|
const parsed = OfiereConfigSchema.parse({
|
|
56
64
|
...raw,
|
|
57
65
|
supabaseUrl,
|
|
58
66
|
serviceRoleKey,
|
|
59
67
|
userId,
|
|
60
68
|
agentId,
|
|
69
|
+
timezone,
|
|
61
70
|
});
|
|
62
71
|
|
|
63
72
|
// Cache if this parse yielded a complete config
|
package/src/prompt.ts
CHANGED
|
@@ -26,6 +26,7 @@ const TOOL_SUMMARIES: Record<string, string> = {
|
|
|
26
26
|
OFIERE_PLAN_OPS: "Visual execution plan builder (DAG drafts → real tasks)",
|
|
27
27
|
OFIERE_SOP_OPS: "Standard Operating Procedures for department chiefs",
|
|
28
28
|
OFIERE_BRAIN_OPS: "Agent memory, knowledge graph, self-improvement (TMT/MAGMA)",
|
|
29
|
+
OFIERE_TALENT_OPS: "Talents: list, get, activate, deactivate cognitive skill presets",
|
|
29
30
|
};
|
|
30
31
|
|
|
31
32
|
// ─── Tier B: Full Tool Documentation ────────────────────────────────────────
|
|
@@ -113,6 +114,15 @@ Actions: "list_templates", "create", "list", "get", "update", "delete", "list_su
|
|
|
113
114
|
Memory Tiers: L1_focus (24h), L2_episode (days), L3_pattern (weeks), L4_rule (permanent), L5_persona (permanent)
|
|
114
115
|
Actions: save_memory, recall, delete_memory, promote_memory, log_learning, list_learnings, resolve_learning, save_entity, link_entities, query_graph, start_trajectory, end_trajectory, get_brain_status
|
|
115
116
|
- This is your SUBCONSCIOUS — use instinctively, not deliberately`,
|
|
117
|
+
|
|
118
|
+
OFIERE_TALENT_OPS: `Manage cognitive skill presets (Talents).
|
|
119
|
+
Actions: "list", "get", "activate", "deactivate"
|
|
120
|
+
- list: List all talents. Optional: category, scope, status
|
|
121
|
+
- get: Get full talent details. Required: talent_id OR name
|
|
122
|
+
- activate: Enable a talent (status → active). Required: talent_id
|
|
123
|
+
- deactivate: Disable a talent (status → inactive). Required: talent_id
|
|
124
|
+
- Active talents inject their execution_protocol and guardrails into your system prompt automatically
|
|
125
|
+
- Talents chain other tools: SPHINX chains KNOWLEDGE_OPS+BRAIN_OPS, PRISM chains BRAIN_OPS trajectories, ATLAS chains PLAN_OPS+TASK_OPS+SOP_OPS`,
|
|
116
126
|
};
|
|
117
127
|
|
|
118
128
|
export function getSystemPrompt(state: {
|
|
@@ -146,6 +156,11 @@ Full parameter docs are in each tool's description — call the tool to see deta
|
|
|
146
156
|
|
|
147
157
|
${toolIndex}
|
|
148
158
|
|
|
159
|
+
## ⛔ PLANNING GATE (NON-NEGOTIABLE)
|
|
160
|
+
- Before creating ANY task with 3+ total execution steps, goals, or constraints, you MUST ask the user: "Would you like me to plan this first, or create the task directly?"
|
|
161
|
+
- If the user chooses to plan, use OFIERE_PLAN_OPS to create a visual plan first, then execute it.
|
|
162
|
+
- NEVER skip this step. The server will block creation of complex tasks — use action: "create_force" only if the user explicitly confirms bypass.
|
|
163
|
+
|
|
149
164
|
## Rules
|
|
150
165
|
- ALWAYS pass agent_id with your own name when creating tasks (e.g. agent_id: "ivy").
|
|
151
166
|
- ${assignRule}
|
|
@@ -159,7 +174,6 @@ ${toolIndex}
|
|
|
159
174
|
- CONSTELLATION: ALWAYS read_blueprint before creating/editing agents. ALWAYS confirm before delete.
|
|
160
175
|
- WORKFLOWS: ALWAYS "get" before modifying. Use "insert_node_between" for mid-flow additions.
|
|
161
176
|
- CHANNEL REPORTS: ALWAYS include agent_id (YOUR name) in send_report. Use get_task_detail for drill-down.
|
|
162
|
-
- **PLANNING GATE (MANDATORY):** Before creating ANY task with 3+ execution steps, goals, constraints, OR a due_date, you MUST ask the user: "Would you like me to plan this first, or create the task directly?" If the user chooses to plan, use OFIERE_PLAN_OPS to create a visual plan first, then execute it to generate the real tasks. Do NOT skip this step.
|
|
163
177
|
- File refs @[name](file:ID): Use OFIERE_FILE_OPS read_text_file to retrieve — don't ask user.
|
|
164
178
|
- Task approvals (OFIERE_TASK_OPS) ≠ workflow gate approvals (human_approval nodes).
|
|
165
179
|
|
package/src/tools.ts
CHANGED
|
@@ -189,6 +189,7 @@ function registerTaskOps(
|
|
|
189
189
|
supabase: SupabaseClient,
|
|
190
190
|
userId: string,
|
|
191
191
|
resolveAgent: (id?: string) => Promise<string | null>,
|
|
192
|
+
timezone: string,
|
|
192
193
|
): void {
|
|
193
194
|
api.registerTool({
|
|
194
195
|
name: "OFIERE_TASK_OPS",
|
|
@@ -217,7 +218,7 @@ function registerTaskOps(
|
|
|
217
218
|
action: {
|
|
218
219
|
type: "string",
|
|
219
220
|
description: "The operation to perform",
|
|
220
|
-
enum: ["list", "get", "create", "update", "delete", "add_approval", "list_approvals", "resolve_approval"],
|
|
221
|
+
enum: ["list", "get", "create", "create_force", "update", "delete", "add_approval", "list_approvals", "resolve_approval"],
|
|
221
222
|
},
|
|
222
223
|
title: { type: "string", description: "Task title (required for create)" },
|
|
223
224
|
description: { type: "string", description: "Task description" },
|
|
@@ -314,8 +315,25 @@ function registerTaskOps(
|
|
|
314
315
|
case "get":
|
|
315
316
|
if (!params.task_id) return err("Missing required field: task_id");
|
|
316
317
|
return handleListTasks(supabase, userId, { ...params, limit: 1 });
|
|
317
|
-
case "create":
|
|
318
|
-
|
|
318
|
+
case "create": {
|
|
319
|
+
// ── PLANNING GATE — server-side enforcement ──
|
|
320
|
+
// Trigger only on steps/goals/constraints count >= 3
|
|
321
|
+
const execSteps = Array.isArray(params.execution_plan) ? (params.execution_plan as any[]).length : 0;
|
|
322
|
+
const goalCount = Array.isArray(params.goals) ? (params.goals as any[]).length : 0;
|
|
323
|
+
const constraintCount = Array.isArray(params.constraints) ? (params.constraints as any[]).length : 0;
|
|
324
|
+
const complexityCount = execSteps + goalCount + constraintCount;
|
|
325
|
+
if (complexityCount >= 3) {
|
|
326
|
+
return ok({
|
|
327
|
+
blocked: true,
|
|
328
|
+
message: `⚠️ PLANNING GATE: This task has ${complexityCount} execution steps/goals/constraints (${execSteps} steps, ${goalCount} goals, ${constraintCount} constraints). Planning Mode is mandatory for complex tasks. Ask the user: "Would you like me to plan this first using OFIERE_PLAN_OPS, or force-create with action: 'create_force'?"`,
|
|
329
|
+
hint: "To bypass: re-call with action 'create_force'. To plan first: use OFIERE_PLAN_OPS.",
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
return handleCreateTask(supabase, userId, resolveAgent, params, timezone);
|
|
333
|
+
}
|
|
334
|
+
case "create_force":
|
|
335
|
+
// Bypasses planning gate — agent explicitly chose to skip planning
|
|
336
|
+
return handleCreateTask(supabase, userId, resolveAgent, params, timezone);
|
|
319
337
|
case "update":
|
|
320
338
|
return handleUpdateTask(supabase, userId, params);
|
|
321
339
|
case "delete":
|
|
@@ -328,7 +346,7 @@ function registerTaskOps(
|
|
|
328
346
|
return handleResolveApproval(supabase, userId, params);
|
|
329
347
|
default:
|
|
330
348
|
return err(
|
|
331
|
-
`Unknown action "${action}". Valid actions: list, get, create, update, delete, add_approval, list_approvals, resolve_approval`,
|
|
349
|
+
`Unknown action "${action}". Valid actions: list, get, create, create_force, update, delete, add_approval, list_approvals, resolve_approval`,
|
|
332
350
|
);
|
|
333
351
|
}
|
|
334
352
|
},
|
|
@@ -397,6 +415,7 @@ async function handleCreateTask(
|
|
|
397
415
|
userId: string,
|
|
398
416
|
resolveAgent: (id?: string) => Promise<string | null>,
|
|
399
417
|
params: Record<string, unknown>,
|
|
418
|
+
timezone: string = "Asia/Jakarta",
|
|
400
419
|
): Promise<ToolResult> {
|
|
401
420
|
try {
|
|
402
421
|
if (!params.title) return err("Missing required field: title");
|
|
@@ -547,7 +566,20 @@ async function handleCreateTask(
|
|
|
547
566
|
// We convert to UTC epoch for next_run_at so the edge function fires correctly.
|
|
548
567
|
const startDate = params.start_date as string | undefined;
|
|
549
568
|
const effectiveAgentId = (insertData.agent_id as string) || assignee;
|
|
550
|
-
|
|
569
|
+
|
|
570
|
+
// Resolve timezone offset from IANA timezone string (configurable per user)
|
|
571
|
+
const getTimezoneOffsetHours = (tz: string): number => {
|
|
572
|
+
try {
|
|
573
|
+
const now = new Date();
|
|
574
|
+
const utcStr = now.toLocaleString('en-US', { timeZone: 'UTC' });
|
|
575
|
+
const tzStr = now.toLocaleString('en-US', { timeZone: tz });
|
|
576
|
+
const diffMs = new Date(tzStr).getTime() - new Date(utcStr).getTime();
|
|
577
|
+
return Math.round(diffMs / 3600000);
|
|
578
|
+
} catch {
|
|
579
|
+
return 7; // Fallback to WIB (UTC+7)
|
|
580
|
+
}
|
|
581
|
+
};
|
|
582
|
+
const TZ_OFFSET_HOURS = getTimezoneOffsetHours(timezone);
|
|
551
583
|
if (startDate && effectiveAgentId) {
|
|
552
584
|
try {
|
|
553
585
|
// Parse start_date robustly — it can be:
|
|
@@ -567,17 +599,19 @@ async function handleCreateTask(
|
|
|
567
599
|
const hasTimeInfo = /[T ]\d{2}:\d{2}/.test(startDate);
|
|
568
600
|
|
|
569
601
|
if (explicitScheduledTime) {
|
|
570
|
-
// Agent explicitly passed a scheduled_time — treat as
|
|
602
|
+
// Agent explicitly passed a scheduled_time — treat as user’s local time
|
|
571
603
|
const dateStr = parsedDate.toISOString().split("T")[0]; // YYYY-MM-DD
|
|
572
604
|
const [localH, localM] = explicitScheduledTime.split(":").map(Number);
|
|
573
|
-
const utcH = localH -
|
|
605
|
+
const utcH = localH - TZ_OFFSET_HOURS;
|
|
574
606
|
const dt = new Date(`${dateStr}T00:00:00Z`);
|
|
575
607
|
dt.setUTCHours(utcH, localM, 0, 0);
|
|
576
608
|
nextRunAtEpoch = Math.floor(dt.getTime() / 1000);
|
|
577
609
|
scheduledTimeFinal = explicitScheduledTime; // Store as user's local time
|
|
578
610
|
scheduledDateFinal = dateStr;
|
|
611
|
+
// Normalize start_date to ISO UTC for consistent downstream parsing
|
|
612
|
+
insertData.start_date = dt.toISOString();
|
|
579
613
|
} else if (hasTimeInfo) {
|
|
580
|
-
// start_date already contains time — assume it
|
|
614
|
+
// start_date already contains time — assume it’s in the user’s local timezone
|
|
581
615
|
// Extract the local hour:minute from the string, NOT from UTC parsing
|
|
582
616
|
const timeMatch = startDate.match(/(\d{2}):(\d{2})/);
|
|
583
617
|
if (timeMatch) {
|
|
@@ -585,10 +619,12 @@ async function handleCreateTask(
|
|
|
585
619
|
const localM = parseInt(timeMatch[2], 10);
|
|
586
620
|
const dateStr = parsedDate.toISOString().split("T")[0];
|
|
587
621
|
const dt = new Date(`${dateStr}T00:00:00Z`);
|
|
588
|
-
dt.setUTCHours(localH -
|
|
622
|
+
dt.setUTCHours(localH - TZ_OFFSET_HOURS, localM, 0, 0);
|
|
589
623
|
nextRunAtEpoch = Math.floor(dt.getTime() / 1000);
|
|
590
624
|
scheduledTimeFinal = `${String(localH).padStart(2, "0")}:${String(localM).padStart(2, "0")}`;
|
|
591
625
|
scheduledDateFinal = dateStr;
|
|
626
|
+
// Normalize start_date to ISO UTC
|
|
627
|
+
insertData.start_date = dt.toISOString();
|
|
592
628
|
} else {
|
|
593
629
|
// Can't extract time — fall back to UTC parsing
|
|
594
630
|
nextRunAtEpoch = Math.floor(parsedDate.getTime() / 1000);
|
|
@@ -596,13 +632,15 @@ async function handleCreateTask(
|
|
|
596
632
|
scheduledDateFinal = parsedDate.toISOString().split("T")[0];
|
|
597
633
|
}
|
|
598
634
|
} else {
|
|
599
|
-
// Date only, no time — default to 09:00
|
|
635
|
+
// Date only, no time — default to 09:00 user local time
|
|
600
636
|
const dateStr = parsedDate.toISOString().split("T")[0];
|
|
601
637
|
const dt = new Date(`${dateStr}T00:00:00Z`);
|
|
602
|
-
dt.setUTCHours(9 -
|
|
638
|
+
dt.setUTCHours(9 - TZ_OFFSET_HOURS, 0, 0, 0); // 09:00 local = UTC - offset
|
|
603
639
|
nextRunAtEpoch = Math.floor(dt.getTime() / 1000);
|
|
604
|
-
scheduledTimeFinal = "09:00"; // Stored as
|
|
640
|
+
scheduledTimeFinal = "09:00"; // Stored as user's local time
|
|
605
641
|
scheduledDateFinal = dateStr;
|
|
642
|
+
// Normalize start_date to ISO UTC
|
|
643
|
+
insertData.start_date = dt.toISOString();
|
|
606
644
|
}
|
|
607
645
|
} else {
|
|
608
646
|
// Unparseable date — fallback to now + 60s
|
|
@@ -633,7 +671,7 @@ async function handleCreateTask(
|
|
|
633
671
|
next_run_at: nextRunAtEpoch,
|
|
634
672
|
run_count: 0,
|
|
635
673
|
priority: params.priority !== undefined ? params.priority : 1,
|
|
636
|
-
timezone:
|
|
674
|
+
timezone: timezone,
|
|
637
675
|
});
|
|
638
676
|
} catch (schedErr) {
|
|
639
677
|
// Non-fatal: task was created, just the scheduler event failed
|
|
@@ -5594,6 +5632,191 @@ function registerBrainOps(
|
|
|
5594
5632
|
}));
|
|
5595
5633
|
}
|
|
5596
5634
|
|
|
5635
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
5636
|
+
// TALENT OPS — Cognitive Skill Presets (list / get / activate / deactivate)
|
|
5637
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
5638
|
+
|
|
5639
|
+
function registerTalentOps(
|
|
5640
|
+
api: any, supabase: SupabaseClient, userId: string,
|
|
5641
|
+
) {
|
|
5642
|
+
api.registerTool(api.createToolDefinition({
|
|
5643
|
+
name: "OFIERE_TALENT_OPS",
|
|
5644
|
+
label: "Ofiere Talent Operations",
|
|
5645
|
+
description:
|
|
5646
|
+
`Manage cognitive skill presets (Talents).\n` +
|
|
5647
|
+
`Actions: "list", "get", "activate", "deactivate"\n` +
|
|
5648
|
+
`- list: List all talents. Optional: category, scope, status\n` +
|
|
5649
|
+
`- get: Get full talent details. Required: talent_id OR name\n` +
|
|
5650
|
+
`- activate: Enable a talent (status → active). Required: talent_id\n` +
|
|
5651
|
+
`- deactivate: Disable a talent (status → inactive). Required: talent_id\n` +
|
|
5652
|
+
`- Active talents inject their execution_protocol and guardrails into your system prompt automatically\n` +
|
|
5653
|
+
`- Talents chain other tools: SPHINX chains KNOWLEDGE_OPS+BRAIN_OPS, PRISM chains BRAIN_OPS trajectories, ATLAS chains PLAN_OPS+TASK_OPS+SOP_OPS`,
|
|
5654
|
+
parameters: {
|
|
5655
|
+
type: "object",
|
|
5656
|
+
required: ["action"],
|
|
5657
|
+
properties: {
|
|
5658
|
+
action: { type: "string", description: "list, get, activate, deactivate" },
|
|
5659
|
+
talent_id: { type: "string", description: "Talent UUID" },
|
|
5660
|
+
name: { type: "string", description: "Talent name (for get by name)" },
|
|
5661
|
+
category: { type: "string", description: "Filter: research, analysis, operations, general" },
|
|
5662
|
+
scope: { type: "string", description: "Filter: global, agent_specific" },
|
|
5663
|
+
status: { type: "string", description: "Filter: active, inactive" },
|
|
5664
|
+
},
|
|
5665
|
+
},
|
|
5666
|
+
async execute(_id: string, params: Record<string, unknown>) {
|
|
5667
|
+
const action = params.action as string;
|
|
5668
|
+
try {
|
|
5669
|
+
switch (action) {
|
|
5670
|
+
case "list": {
|
|
5671
|
+
let q = supabase.from("ofiere_talents")
|
|
5672
|
+
.select("id, name, codename, description, category, icon, scope, status, tags, version")
|
|
5673
|
+
.eq("user_id", userId)
|
|
5674
|
+
.order("created_at", { ascending: false });
|
|
5675
|
+
if (params.category) q = q.eq("category", params.category as string);
|
|
5676
|
+
if (params.scope) q = q.eq("scope", params.scope as string);
|
|
5677
|
+
if (params.status) q = q.eq("status", params.status as string);
|
|
5678
|
+
const { data, error } = await q.limit(50);
|
|
5679
|
+
if (error) return { success: false, result: error.message };
|
|
5680
|
+
return { success: true, result: { talents: data || [], count: data?.length || 0 } };
|
|
5681
|
+
}
|
|
5682
|
+
case "get": {
|
|
5683
|
+
const tid = params.talent_id as string;
|
|
5684
|
+
const tname = params.name as string;
|
|
5685
|
+
if (!tid && !tname) return { success: false, result: "Required: talent_id or name" };
|
|
5686
|
+
let q = supabase.from("ofiere_talents").select("*").eq("user_id", userId);
|
|
5687
|
+
if (tid) q = q.eq("id", tid);
|
|
5688
|
+
else q = q.ilike("name", `%${tname}%`);
|
|
5689
|
+
const { data, error } = await q.limit(1).maybeSingle();
|
|
5690
|
+
if (error) return { success: false, result: error.message };
|
|
5691
|
+
if (!data) return { success: false, result: "Talent not found" };
|
|
5692
|
+
return { success: true, result: data };
|
|
5693
|
+
}
|
|
5694
|
+
case "activate": {
|
|
5695
|
+
const tid = params.talent_id as string;
|
|
5696
|
+
if (!tid) return { success: false, result: "Required: talent_id" };
|
|
5697
|
+
const { error } = await supabase.from("ofiere_talents")
|
|
5698
|
+
.update({ status: "active", updated_at: new Date().toISOString() })
|
|
5699
|
+
.eq("id", tid).eq("user_id", userId);
|
|
5700
|
+
if (error) return { success: false, result: error.message };
|
|
5701
|
+
return { success: true, result: { message: "Talent activated", talent_id: tid } };
|
|
5702
|
+
}
|
|
5703
|
+
case "deactivate": {
|
|
5704
|
+
const tid = params.talent_id as string;
|
|
5705
|
+
if (!tid) return { success: false, result: "Required: talent_id" };
|
|
5706
|
+
const { error } = await supabase.from("ofiere_talents")
|
|
5707
|
+
.update({ status: "inactive", updated_at: new Date().toISOString() })
|
|
5708
|
+
.eq("id", tid).eq("user_id", userId);
|
|
5709
|
+
if (error) return { success: false, result: error.message };
|
|
5710
|
+
return { success: true, result: { message: "Talent deactivated", talent_id: tid } };
|
|
5711
|
+
}
|
|
5712
|
+
default:
|
|
5713
|
+
return { success: false, result: `Unknown action: ${action}. Valid: list, get, activate, deactivate` };
|
|
5714
|
+
}
|
|
5715
|
+
} catch (e) {
|
|
5716
|
+
return { success: false, result: e instanceof Error ? e.message : String(e) };
|
|
5717
|
+
}
|
|
5718
|
+
},
|
|
5719
|
+
}));
|
|
5720
|
+
}
|
|
5721
|
+
|
|
5722
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
5723
|
+
// TALENT CONTEXT HOOK — Injects active talent protocols into system prompt
|
|
5724
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
5725
|
+
// This is the engine that makes talents WORK. Without this hook, talents are
|
|
5726
|
+
// just database rows. The hook turns them into cognitive directives injected
|
|
5727
|
+
// before every agent turn via appendSystemContext.
|
|
5728
|
+
|
|
5729
|
+
const talentCache = new Map<string, { text: string; at: number }>();
|
|
5730
|
+
const TALENT_CACHE_TTL = 300_000; // 5 min
|
|
5731
|
+
|
|
5732
|
+
function registerTalentContextHook(
|
|
5733
|
+
api: any, supabase: SupabaseClient, userId: string, fallbackAgentId: string,
|
|
5734
|
+
) {
|
|
5735
|
+
try {
|
|
5736
|
+
api.on("before_prompt_build", async (_event: any, ctx: any) => {
|
|
5737
|
+
try {
|
|
5738
|
+
const ctxAgentId = ctx?.agentId || "";
|
|
5739
|
+
let resolvedAgentId = fallbackAgentId;
|
|
5740
|
+
if (ctxAgentId && !isSystemName(ctxAgentId)) {
|
|
5741
|
+
try {
|
|
5742
|
+
const resolved = await resolveAgentId(ctxAgentId, userId, supabase);
|
|
5743
|
+
if (resolved) resolvedAgentId = resolved;
|
|
5744
|
+
} catch { /* fallback */ }
|
|
5745
|
+
}
|
|
5746
|
+
|
|
5747
|
+
const cacheKey = `talent:${resolvedAgentId || "global"}`;
|
|
5748
|
+
const cached = talentCache.get(cacheKey);
|
|
5749
|
+
if (cached && (Date.now() - cached.at) < TALENT_CACHE_TTL) {
|
|
5750
|
+
return cached.text ? { appendSystemContext: cached.text } : undefined;
|
|
5751
|
+
}
|
|
5752
|
+
|
|
5753
|
+
// Query active talents: global OR assigned to this agent
|
|
5754
|
+
const { data: talents } = await supabase
|
|
5755
|
+
.from("ofiere_talents")
|
|
5756
|
+
.select("name, codename, execution_protocol, guardrails, trigger_conditions, config")
|
|
5757
|
+
.eq("user_id", userId)
|
|
5758
|
+
.eq("status", "active")
|
|
5759
|
+
.order("created_at", { ascending: true });
|
|
5760
|
+
|
|
5761
|
+
if (!talents || talents.length === 0) {
|
|
5762
|
+
talentCache.set(cacheKey, { text: "", at: Date.now() });
|
|
5763
|
+
return;
|
|
5764
|
+
}
|
|
5765
|
+
|
|
5766
|
+
// Filter: global talents + agent-specific talents assigned to this agent
|
|
5767
|
+
// (assigned_agent_ids check is done client-side since JSONB contains is complex)
|
|
5768
|
+
const sections: string[] = [];
|
|
5769
|
+
for (const t of talents) {
|
|
5770
|
+
const ep = t.execution_protocol || [];
|
|
5771
|
+
const gr = t.guardrails || {};
|
|
5772
|
+
const tc = t.trigger_conditions || {};
|
|
5773
|
+
|
|
5774
|
+
let block = `### 🎯 Talent: ${t.name} (${t.codename || "custom"})`;
|
|
5775
|
+
|
|
5776
|
+
// Triggers
|
|
5777
|
+
const triggers = [
|
|
5778
|
+
...(tc.keywords || []),
|
|
5779
|
+
...(tc.explicit_commands || []),
|
|
5780
|
+
];
|
|
5781
|
+
if (triggers.length > 0) {
|
|
5782
|
+
block += `\nTriggers: ${triggers.join(", ")}`;
|
|
5783
|
+
}
|
|
5784
|
+
|
|
5785
|
+
// Execution Protocol (compact)
|
|
5786
|
+
if (ep.length > 0) {
|
|
5787
|
+
block += "\nExecution Protocol:";
|
|
5788
|
+
for (let i = 0; i < ep.length; i++) {
|
|
5789
|
+
const step = ep[i] as any;
|
|
5790
|
+
block += `\n${i + 1}. **${step.phase}**: ${step.instruction}`;
|
|
5791
|
+
if (step.checkpoint) block += `\n ✓ Checkpoint: ${step.checkpoint}`;
|
|
5792
|
+
}
|
|
5793
|
+
}
|
|
5794
|
+
|
|
5795
|
+
// Guardrails (compact)
|
|
5796
|
+
const mustNever = gr.must_never || [];
|
|
5797
|
+
const hardConstraints = gr.hard_constraints || [];
|
|
5798
|
+
if (mustNever.length > 0 || hardConstraints.length > 0) {
|
|
5799
|
+
block += "\n⚠️ Guardrails:";
|
|
5800
|
+
for (const mn of mustNever) block += `\n- 🚫 NEVER: ${mn}`;
|
|
5801
|
+
for (const hc of hardConstraints) block += `\n- ⛔ CONSTRAINT: ${hc}`;
|
|
5802
|
+
}
|
|
5803
|
+
|
|
5804
|
+
sections.push(block);
|
|
5805
|
+
}
|
|
5806
|
+
|
|
5807
|
+
const talentContext = `<agent-talents>\n## Your Active Talents (${talents.length})\n\nThese are cognitive skill presets that define HOW you should approach specific types of tasks.\nWhen a user request matches a talent's triggers, follow that talent's execution protocol.\n\n${sections.join("\n\n---\n\n")}\n</agent-talents>`;
|
|
5808
|
+
|
|
5809
|
+
talentCache.set(cacheKey, { text: talentContext, at: Date.now() });
|
|
5810
|
+
return { appendSystemContext: talentContext };
|
|
5811
|
+
} catch (e) {
|
|
5812
|
+
api.logger.debug?.(`[ofiere-talent] before_prompt_build error: ${e instanceof Error ? e.message : e}`);
|
|
5813
|
+
}
|
|
5814
|
+
});
|
|
5815
|
+
} catch {
|
|
5816
|
+
api.logger.debug?.("[ofiere] Could not register talent context hook");
|
|
5817
|
+
}
|
|
5818
|
+
}
|
|
5819
|
+
|
|
5597
5820
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
5598
5821
|
// Public: Register All Meta-Tools
|
|
5599
5822
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -5611,7 +5834,7 @@ export function registerTools(
|
|
|
5611
5834
|
const resolveAgent = createAgentResolver(api, supabase, userId, fallbackAgentId);
|
|
5612
5835
|
|
|
5613
5836
|
// ── Register each domain meta-tool ──
|
|
5614
|
-
registerTaskOps(api, supabase, userId, resolveAgent); // 1
|
|
5837
|
+
registerTaskOps(api, supabase, userId, resolveAgent, config.timezone); // 1
|
|
5615
5838
|
registerAgentOps(api, supabase, userId, fallbackAgentId); // 2
|
|
5616
5839
|
registerProjectOps(api, supabase, userId); // 3
|
|
5617
5840
|
registerScheduleOps(api, supabase, userId); // 4
|
|
@@ -5625,22 +5848,19 @@ export function registerTools(
|
|
|
5625
5848
|
registerPlanOps(api, supabase, userId, resolveAgent); // 12
|
|
5626
5849
|
registerSOPOps(api, supabase, userId, resolveAgent); // 13
|
|
5627
5850
|
registerBrainOps(api, supabase, userId, resolveAgent); // 14
|
|
5851
|
+
registerTalentOps(api, supabase, userId); // 15
|
|
5628
5852
|
|
|
5629
5853
|
// ── Register dynamic brain context hook ──
|
|
5630
|
-
// FIX (v4.30.0): Was injectBrainContext() which loaded once at registration.
|
|
5631
|
-
// Now registers a before_prompt_build hook that dynamically resolves the
|
|
5632
|
-
// calling agent from ctx.agentId and loads THAT agent's brain memories.
|
|
5633
|
-
// Each agent sees its own brain context — Daisy sees Daisy's, Ivy sees Ivy's.
|
|
5634
5854
|
registerBrainContextHook(api, supabase, userId, fallbackAgentId);
|
|
5635
5855
|
|
|
5856
|
+
// ── Register talent context hook ──
|
|
5857
|
+
registerTalentContextHook(api, supabase, userId, fallbackAgentId);
|
|
5858
|
+
|
|
5636
5859
|
// ── Register agent_end hook for server-side brain extraction ──
|
|
5637
|
-
// This is the FIX for Bug 2: extraction was client-side only (useSocket.ts).
|
|
5638
|
-
// Now every completed agent turn — from ANY channel (Telegram, Discord,
|
|
5639
|
-
// webchat, scheduled) — triggers memory extraction server-side.
|
|
5640
5860
|
registerBrainExtractionHook(api, supabase, userId, fallbackAgentId);
|
|
5641
5861
|
|
|
5642
5862
|
// ── Count and log ──
|
|
5643
|
-
const toolCount =
|
|
5863
|
+
const toolCount = 15;
|
|
5644
5864
|
const callerName = getCallingAgentName(api);
|
|
5645
5865
|
const agentLabel = fallbackAgentId || callerName || "auto-detect";
|
|
5646
5866
|
api.logger.info(`[ofiere] ${toolCount} meta-tools registered (agent: ${agentLabel})`);
|
package/src/types.ts
CHANGED
|
@@ -5,4 +5,6 @@ export interface OfiereConfig {
|
|
|
5
5
|
userId: string;
|
|
6
6
|
/** Optional — if not set, agent identity is resolved at runtime from OpenClaw context */
|
|
7
7
|
agentId: string;
|
|
8
|
+
/** IANA timezone string for the user (e.g. 'Asia/Jakarta', 'America/New_York'). Default: 'Asia/Jakarta' */
|
|
9
|
+
timezone: string;
|
|
8
10
|
}
|