ofiere-openclaw-plugin 4.33.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ofiere-openclaw-plugin",
3
- "version": "4.33.0",
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 (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
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",
@@ -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
- return handleCreateTask(supabase, userId, resolveAgent, params);
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
- const WIB_OFFSET_HOURS = 7; // Asia/Jakarta = UTC+7
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 WIB local time
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 - WIB_OFFSET_HOURS;
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's in the user's local timezone (WIB)
614
+ // start_date already contains time — assume its in the users 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 - WIB_OFFSET_HOURS, localM, 0, 0);
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 WIB (= 02:00 UTC)
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 - WIB_OFFSET_HOURS, 0, 0, 0); // 09:00 WIB = 02:00 UTC
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 WIB local time
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: "Asia/Jakarta",
674
+ timezone: timezone,
637
675
  });
638
676
  } catch (schedErr) {
639
677
  // Non-fatal: task was created, just the scheduler event failed
@@ -5611,7 +5649,7 @@ export function registerTools(
5611
5649
  const resolveAgent = createAgentResolver(api, supabase, userId, fallbackAgentId);
5612
5650
 
5613
5651
  // ── Register each domain meta-tool ──
5614
- registerTaskOps(api, supabase, userId, resolveAgent); // 1
5652
+ registerTaskOps(api, supabase, userId, resolveAgent, config.timezone); // 1
5615
5653
  registerAgentOps(api, supabase, userId, fallbackAgentId); // 2
5616
5654
  registerProjectOps(api, supabase, userId); // 3
5617
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
  }