ofiere-openclaw-plugin 4.32.0 → 4.34.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 +5 -1
- package/src/tools.ts +78 -17
- 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.34.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
|
@@ -146,6 +146,11 @@ Full parameter docs are in each tool's description — call the tool to see deta
|
|
|
146
146
|
|
|
147
147
|
${toolIndex}
|
|
148
148
|
|
|
149
|
+
## ⛔ PLANNING GATE (NON-NEGOTIABLE)
|
|
150
|
+
- 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?"
|
|
151
|
+
- If the user chooses to plan, use OFIERE_PLAN_OPS to create a visual plan first, then execute it.
|
|
152
|
+
- NEVER skip this step. The server will block creation of complex tasks — use action: "create_force" only if the user explicitly confirms bypass.
|
|
153
|
+
|
|
149
154
|
## Rules
|
|
150
155
|
- ALWAYS pass agent_id with your own name when creating tasks (e.g. agent_id: "ivy").
|
|
151
156
|
- ${assignRule}
|
|
@@ -159,7 +164,6 @@ ${toolIndex}
|
|
|
159
164
|
- CONSTELLATION: ALWAYS read_blueprint before creating/editing agents. ALWAYS confirm before delete.
|
|
160
165
|
- WORKFLOWS: ALWAYS "get" before modifying. Use "insert_node_between" for mid-flow additions.
|
|
161
166
|
- CHANNEL REPORTS: ALWAYS include agent_id (YOUR name) in send_report. Use get_task_detail for drill-down.
|
|
162
|
-
- PLANNING GATE: Before creating tasks with 3+ steps or due_dates, ask if user wants a Plan first.
|
|
163
167
|
- File refs @[name](file:ID): Use OFIERE_FILE_OPS read_text_file to retrieve — don't ask user.
|
|
164
168
|
- Task approvals (OFIERE_TASK_OPS) ≠ workflow gate approvals (human_approval nodes).
|
|
165
169
|
|
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",
|
|
@@ -198,7 +199,7 @@ function registerTaskOps(
|
|
|
198
199
|
`Actions:\n` +
|
|
199
200
|
`- "list": List/filter tasks. Optional: status, agent_id, space_id, folder_id, task_id, limit\n` +
|
|
200
201
|
`- "get": Get a single task by ID. Required: task_id\n` +
|
|
201
|
-
`- "create": Create a task. Required: title. Optional: agent_id, description, status, priority, space_id, folder_id, start_date, due_date, tags, instructions, execution_plan, goals, constraints, system_prompt, recurrence_type, recurrence_interval, scheduled_time
|
|
202
|
+
`- "create": Create a task. Required: title. Optional: agent_id, description, status, priority, space_id, folder_id, start_date, due_date, tags, instructions, execution_plan, goals, constraints, system_prompt, recurrence_type, recurrence_interval, scheduled_time. ⚠️ PLANNING GATE: If the task has 3+ execution steps, goals, or constraints, FIRST ask the user "Plan first or create directly?" If they choose to plan, use OFIERE_PLAN_OPS instead.\n` +
|
|
202
203
|
`- "update": Update a task. Required: task_id. Optional: all create fields + progress\n` +
|
|
203
204
|
`- "delete": Delete task + subtasks. Required: task_id\n` +
|
|
204
205
|
`- "add_approval": Request approval on a task. Required: task_id, approver_name. Optional: approver_type (human|agent, auto-detected), due_date, comment\n` +
|
|
@@ -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" },
|
|
@@ -241,7 +242,7 @@ function registerTaskOps(
|
|
|
241
242
|
folder_id: { type: "string", description: "PM Folder ID" },
|
|
242
243
|
start_date: { type: "string", description: "Start date (ISO 8601). Required for scheduled/recurring tasks." },
|
|
243
244
|
due_date: { type: "string", description: "Due date (ISO 8601)" },
|
|
244
|
-
scheduled_time: { type: "string", description: "Time to execute in HH:MM format (
|
|
245
|
+
scheduled_time: { type: "string", description: "Time to execute in HH:MM format (user's LOCAL time, e.g. 10:00 for 10 AM WIB). The system converts to UTC automatically." },
|
|
245
246
|
recurrence_type: {
|
|
246
247
|
type: "string",
|
|
247
248
|
description: "How often the task recurs. 'none' for one-shot.",
|
|
@@ -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");
|
|
@@ -542,8 +561,25 @@ async function handleCreateTask(
|
|
|
542
561
|
// ── Auto-create scheduler event if task has a start_date ──────────────
|
|
543
562
|
// This bridges the plugin → scheduler so the pg_cron task-dispatcher
|
|
544
563
|
// Edge Function picks up the task at the right time.
|
|
564
|
+
//
|
|
565
|
+
// TIMEZONE: scheduled_time is treated as the user's LOCAL time (default WIB, UTC+7).
|
|
566
|
+
// We convert to UTC epoch for next_run_at so the edge function fires correctly.
|
|
545
567
|
const startDate = params.start_date as string | undefined;
|
|
546
568
|
const effectiveAgentId = (insertData.agent_id as string) || assignee;
|
|
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);
|
|
547
583
|
if (startDate && effectiveAgentId) {
|
|
548
584
|
try {
|
|
549
585
|
// Parse start_date robustly — it can be:
|
|
@@ -563,24 +599,48 @@ async function handleCreateTask(
|
|
|
563
599
|
const hasTimeInfo = /[T ]\d{2}:\d{2}/.test(startDate);
|
|
564
600
|
|
|
565
601
|
if (explicitScheduledTime) {
|
|
566
|
-
// Agent explicitly passed a scheduled_time —
|
|
602
|
+
// Agent explicitly passed a scheduled_time — treat as user’s local time
|
|
567
603
|
const dateStr = parsedDate.toISOString().split("T")[0]; // YYYY-MM-DD
|
|
568
|
-
const
|
|
604
|
+
const [localH, localM] = explicitScheduledTime.split(":").map(Number);
|
|
605
|
+
const utcH = localH - TZ_OFFSET_HOURS;
|
|
606
|
+
const dt = new Date(`${dateStr}T00:00:00Z`);
|
|
607
|
+
dt.setUTCHours(utcH, localM, 0, 0);
|
|
569
608
|
nextRunAtEpoch = Math.floor(dt.getTime() / 1000);
|
|
570
|
-
scheduledTimeFinal = explicitScheduledTime;
|
|
609
|
+
scheduledTimeFinal = explicitScheduledTime; // Store as user's local time
|
|
571
610
|
scheduledDateFinal = dateStr;
|
|
611
|
+
// Normalize start_date to ISO UTC for consistent downstream parsing
|
|
612
|
+
insertData.start_date = dt.toISOString();
|
|
572
613
|
} else if (hasTimeInfo) {
|
|
573
|
-
// start_date already contains time —
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
614
|
+
// start_date already contains time — assume it’s in the user’s local timezone
|
|
615
|
+
// Extract the local hour:minute from the string, NOT from UTC parsing
|
|
616
|
+
const timeMatch = startDate.match(/(\d{2}):(\d{2})/);
|
|
617
|
+
if (timeMatch) {
|
|
618
|
+
const localH = parseInt(timeMatch[1], 10);
|
|
619
|
+
const localM = parseInt(timeMatch[2], 10);
|
|
620
|
+
const dateStr = parsedDate.toISOString().split("T")[0];
|
|
621
|
+
const dt = new Date(`${dateStr}T00:00:00Z`);
|
|
622
|
+
dt.setUTCHours(localH - TZ_OFFSET_HOURS, localM, 0, 0);
|
|
623
|
+
nextRunAtEpoch = Math.floor(dt.getTime() / 1000);
|
|
624
|
+
scheduledTimeFinal = `${String(localH).padStart(2, "0")}:${String(localM).padStart(2, "0")}`;
|
|
625
|
+
scheduledDateFinal = dateStr;
|
|
626
|
+
// Normalize start_date to ISO UTC
|
|
627
|
+
insertData.start_date = dt.toISOString();
|
|
628
|
+
} else {
|
|
629
|
+
// Can't extract time — fall back to UTC parsing
|
|
630
|
+
nextRunAtEpoch = Math.floor(parsedDate.getTime() / 1000);
|
|
631
|
+
scheduledTimeFinal = `${String(parsedDate.getUTCHours()).padStart(2, "0")}:${String(parsedDate.getUTCMinutes()).padStart(2, "0")}`;
|
|
632
|
+
scheduledDateFinal = parsedDate.toISOString().split("T")[0];
|
|
633
|
+
}
|
|
577
634
|
} else {
|
|
578
|
-
// Date only, no time — default to 09:00
|
|
635
|
+
// Date only, no time — default to 09:00 user local time
|
|
579
636
|
const dateStr = parsedDate.toISOString().split("T")[0];
|
|
580
|
-
const dt = new Date(`${dateStr}
|
|
637
|
+
const dt = new Date(`${dateStr}T00:00:00Z`);
|
|
638
|
+
dt.setUTCHours(9 - TZ_OFFSET_HOURS, 0, 0, 0); // 09:00 local = UTC - offset
|
|
581
639
|
nextRunAtEpoch = Math.floor(dt.getTime() / 1000);
|
|
582
|
-
scheduledTimeFinal = "09:00";
|
|
640
|
+
scheduledTimeFinal = "09:00"; // Stored as user's local time
|
|
583
641
|
scheduledDateFinal = dateStr;
|
|
642
|
+
// Normalize start_date to ISO UTC
|
|
643
|
+
insertData.start_date = dt.toISOString();
|
|
584
644
|
}
|
|
585
645
|
} else {
|
|
586
646
|
// Unparseable date — fallback to now + 60s
|
|
@@ -611,6 +671,7 @@ async function handleCreateTask(
|
|
|
611
671
|
next_run_at: nextRunAtEpoch,
|
|
612
672
|
run_count: 0,
|
|
613
673
|
priority: params.priority !== undefined ? params.priority : 1,
|
|
674
|
+
timezone: timezone,
|
|
614
675
|
});
|
|
615
676
|
} catch (schedErr) {
|
|
616
677
|
// Non-fatal: task was created, just the scheduler event failed
|
|
@@ -5588,7 +5649,7 @@ export function registerTools(
|
|
|
5588
5649
|
const resolveAgent = createAgentResolver(api, supabase, userId, fallbackAgentId);
|
|
5589
5650
|
|
|
5590
5651
|
// ── Register each domain meta-tool ──
|
|
5591
|
-
registerTaskOps(api, supabase, userId, resolveAgent); // 1
|
|
5652
|
+
registerTaskOps(api, supabase, userId, resolveAgent, config.timezone); // 1
|
|
5592
5653
|
registerAgentOps(api, supabase, userId, fallbackAgentId); // 2
|
|
5593
5654
|
registerProjectOps(api, supabase, userId); // 3
|
|
5594
5655
|
registerScheduleOps(api, supabase, userId); // 4
|
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
|
}
|