ofiere-openclaw-plugin 4.24.0 → 4.25.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.
Files changed (3) hide show
  1. package/package.json +1 -1
  2. package/src/prompt.ts +10 -3
  3. package/src/tools.ts +128 -16
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ofiere-openclaw-plugin",
3
- "version": "4.24.0",
3
+ "version": "4.25.0",
4
4
  "type": "module",
5
5
  "description": "OpenClaw plugin for Ofiere PM - 13 meta-tools covering tasks, agents, projects, scheduling, knowledge, workflows, notifications, memory, prompts, constellation, space file management, execution plan builder, and SOP management",
6
6
  "keywords": ["openclaw", "ofiere", "project-management", "agents", "plugin"],
package/src/prompt.ts CHANGED
@@ -62,14 +62,20 @@ const TOOL_DOCS: Record<string, string> = {
62
62
  - Edge handles: condition edges use sourceHandle "condition-true"/"condition-false". Loop edges use "loop_body"/"done"
63
63
  - Variables: Use {{prev.nodeId.outputText}} for prior outputs, {{variables.key}} for stored variables`,
64
64
 
65
- OFIERE_NOTIFY_OPS: `- **OFIERE_NOTIFY_OPS** — Notifications & Channel Reports (action: "list", "mark_read", "mark_all_read", "delete", "send_report", "schedule_report", "list_schedules", "delete_schedule")
65
+ OFIERE_NOTIFY_OPS: `- **OFIERE_NOTIFY_OPS** — Notifications & Channel Reports (action: "list", "mark_read", "mark_all_read", "delete", "send_report", "get_task_detail", "schedule_report", "list_schedules", "delete_schedule")
66
66
  - list: Recent notifications. unread_only=true for unread only
67
67
  - mark_read: Mark one notification read by ID
68
68
  - mark_all_read: Mark all as read
69
- - send_report: Send a PM progress report to YOUR connected channels (Telegram, Discord, Slack, etc.)
69
+ - send_report: Send a numbered PM progress report to YOUR connected channels (Telegram, Discord, Slack, etc.)
70
70
  - Required: scope_type (space|folder|project|task|all), agent_id (YOUR name, e.g. "thalia")
71
71
  - Optional: scope_id, channel_types[] (filter to specific channels), include_completed
72
72
  - The report is automatically generated from current PM data and sent through YOUR channel bindings ONLY
73
+ - Tasks are numbered globally (1, 2, 3...) across Active/Pending/Done sections. Recurring tasks that executed recently appear in BOTH Done (today's run) and Pending (next cycle)
74
+ - get_task_detail: Get the FULL result/content of a task by its report number
75
+ - Required: task_number (1-indexed number from the report)
76
+ - Optional: scope_type (default all), scope_id
77
+ - Returns: complete task description (where execution results live), custom_fields, status, tags, and all metadata
78
+ - Use when user asks "details on number X", "what's task 2 result", "show me #3", etc.
73
79
  - schedule_report: Create a recurring/scheduled report
74
80
  - Required: scope_type, recurrence_type (hourly|daily|weekly|monthly), agent_id (YOUR name)
75
81
  - Optional: scope_id, scope_label, channel_types[], recurrence_time (HH:MM UTC, default 09:00), recurrence_interval, recurrence_days_of_week (e.g. "mon,wed,fri"), include_completed
@@ -202,7 +208,8 @@ ${toolDocs}
202
208
  - When task instructions or system prompts contain file references like @[filename](file:FILE_ID), use OFIERE_FILE_OPS action:"read_text_file" file_id:"FILE_ID" to read the file content. Do NOT ask the user for the file — retrieve it yourself.
203
209
  - Use OFIERE_FILE_OPS to create output files (reports, data, configs) in the Space Files explorer. Prefer create_text_file for text-based outputs.
204
210
  - To save task output as a file, call OFIERE_FILE_OPS action:"create_text_file" with the space_id from the task context.
205
- - CHANNEL REPORTS: When the user asks you to "send a report", "send progress", "update me on Telegram/Discord/Slack/WhatsApp", use OFIERE_NOTIFY_OPS action:"send_report" with agent_id set to YOUR name. ALWAYS include agent_id — without it the report will fail. The report is generated from live PM data and sent through YOUR connected channels ONLY — not other agents' channels.
211
+ - CHANNEL REPORTS: When the user asks you to "send a report", "send progress", "update me on Telegram/Discord/Slack/WhatsApp", use OFIERE_NOTIFY_OPS action:"send_report" with agent_id set to YOUR name. ALWAYS include agent_id — without it the report will fail. The report is generated from live PM data and sent through YOUR connected channels ONLY — not other agents' channels. Tasks are numbered (1, 2, 3...) for easy reference.
212
+ - TASK DRILL-DOWN: When the user asks "details on number X", "what's task X result", "show me #X", or any variation referencing a numbered task from a report, use OFIERE_NOTIFY_OPS action:"get_task_detail" with task_number set to the number they mentioned. The description field contains the actual execution result (e.g. scan findings, analysis output). Present the description content as the primary response — that IS the task result.
206
213
  - To set up recurring reports (e.g. "send me a daily report at 9am"), use OFIERE_NOTIFY_OPS action:"schedule_report" with scope_type, recurrence_type, and agent_id set to YOUR name.
207
214
  - If a report is too long for the channel's message limit, save the full report as a markdown file using OFIERE_FILE_OPS action:"create_text_file" and send a summary to the channel instead.
208
215
  - PLANNING WORKFLOW: Use OFIERE_PLAN_OPS to build complex multi-step execution flows BEFORE creating individual tasks. Build the plan → let the user review in the Planning Tab → execute when approved.
package/src/tools.ts CHANGED
@@ -2156,9 +2156,13 @@ function registerNotifyOps(
2156
2156
  `- "mark_read": Mark one as read. Required: id\n` +
2157
2157
  `- "mark_all_read": Mark all as read\n` +
2158
2158
  `- "delete": Delete a notification. Required: id\n` +
2159
- `- "send_report": Send a PM progress report to YOUR connected channels (Telegram, Discord, etc.)\n` +
2159
+ `- "send_report": Send a numbered PM progress report to YOUR connected channels (Telegram, Discord, etc.)\n` +
2160
2160
  ` Required: scope_type (space|folder|project|task|all), agent_id (YOUR name e.g. "thalia")\n` +
2161
2161
  ` Optional: scope_id, channel_types[] (filter specific channels), include_completed (default true)\n` +
2162
+ ` Tasks are numbered globally (1, 2, 3...) across Active/Pending/Done sections for easy reference\n` +
2163
+ `- "get_task_detail": Get the FULL result/content of a task by its report number. Use when user asks "details on number X"\n` +
2164
+ ` Required: task_number (1-indexed from the report). Optional: scope_type, scope_id (defaults to all)\n` +
2165
+ ` Returns the complete task description, execution results, status, and metadata\n` +
2162
2166
  `- "schedule_report": Create a recurring/scheduled report\n` +
2163
2167
  ` Required: scope_type, recurrence_type (hourly|daily|weekly|monthly), agent_id (YOUR name)\n` +
2164
2168
  ` Optional: scope_id, scope_label, channel_types[], recurrence_time (HH:MM UTC, default 09:00),\n` +
@@ -2169,10 +2173,11 @@ function registerNotifyOps(
2169
2173
  type: "object",
2170
2174
  required: ["action"],
2171
2175
  properties: {
2172
- action: { type: "string", enum: ["list", "mark_read", "mark_all_read", "delete", "send_report", "schedule_report", "list_schedules", "delete_schedule"] },
2176
+ action: { type: "string", enum: ["list", "mark_read", "mark_all_read", "delete", "send_report", "get_task_detail", "schedule_report", "list_schedules", "delete_schedule"] },
2173
2177
  agent_id: { type: "string", description: "Your agent name or ID (required for send_report/schedule_report)" },
2174
2178
  id: { type: "string", description: "Notification ID" },
2175
2179
  schedule_id: { type: "string", description: "Schedule ID (for delete_schedule)" },
2180
+ task_number: { type: "number", description: "1-indexed task number from the report (for get_task_detail)" },
2176
2181
  unread_only: { type: "boolean", description: "Only show unread" },
2177
2182
  limit: { type: "number", description: "Max results (default 50)" },
2178
2183
  scope_type: { type: "string", enum: ["space", "folder", "project", "task", "all"], description: "Report scope" },
@@ -2257,6 +2262,69 @@ function registerNotifyOps(
2257
2262
  });
2258
2263
  }
2259
2264
 
2265
+ // ── GET TASK DETAIL — Drill into task result by report number ──
2266
+ case "get_task_detail": {
2267
+ const taskNumber = params.task_number as number;
2268
+ if (!taskNumber || taskNumber < 1) return err("Missing required: task_number (1-indexed number from the report)");
2269
+ const detailScopeType = (params.scope_type as string) || "all";
2270
+ const detailScopeId = params.scope_id as string | undefined;
2271
+
2272
+ // Re-run the same query the report uses to get consistent ordering
2273
+ let dq = supabase.from("tasks").select("id, title, description, status, priority, progress, completed_at, due_date, updated_at, custom_fields, tags, agent_id")
2274
+ .eq("user_id", userId).is("parent_task_id", null);
2275
+ if (detailScopeType === "space" && detailScopeId) dq = dq.eq("space_id", detailScopeId);
2276
+ else if (detailScopeType === "folder" && detailScopeId) dq = dq.eq("folder_id", detailScopeId);
2277
+ else if (detailScopeType === "project" && detailScopeId) dq = dq.eq("project_id", detailScopeId);
2278
+ else if (detailScopeType === "task" && detailScopeId) dq = dq.eq("id", detailScopeId);
2279
+
2280
+ const { data: detailTasks } = await dq.order("priority", { ascending: false }).limit(40);
2281
+ const dtAll = detailTasks || [];
2282
+
2283
+ // Build same ordered list as report: Active → Pending → Done
2284
+ const dtInProgress = dtAll.filter((t: any) => t.status === "IN_PROGRESS" || t.status === "RUNNING");
2285
+ const dtPending = dtAll.filter((t: any) => t.status === "PENDING" || t.status === "NEW");
2286
+ const dtCompleted = dtAll.filter((t: any) => t.status === "DONE");
2287
+ const dtRecentThreshold = 24 * 60 * 60 * 1000;
2288
+ const dtNowMs = Date.now();
2289
+ const dtRecentlyExecuted = dtPending.filter((t: any) =>
2290
+ t.description && t.description.length > 50 &&
2291
+ t.updated_at && (dtNowMs - new Date(t.updated_at).getTime()) < dtRecentThreshold
2292
+ );
2293
+
2294
+ // Same ordering as report formatter
2295
+ const dtOrdered: any[] = [];
2296
+ const dtSeen = new Set<string>();
2297
+ const dtAdd = (t: any) => { if (!dtSeen.has(t.id)) { dtSeen.add(t.id); dtOrdered.push(t); } };
2298
+ for (const t of dtInProgress.slice(0, 8)) dtAdd(t);
2299
+ for (const t of dtPending.slice(0, 6)) dtAdd(t);
2300
+ for (const t of dtRecentlyExecuted) dtAdd(t);
2301
+ for (const t of dtCompleted.slice(0, 5)) dtAdd(t);
2302
+
2303
+ if (taskNumber > dtOrdered.length) {
2304
+ return err(`Task #${taskNumber} not found. The report has ${dtOrdered.length} numbered task(s). Valid range: 1-${dtOrdered.length}.`);
2305
+ }
2306
+
2307
+ const target = dtOrdered[taskNumber - 1];
2308
+ const isRecurring = dtRecentlyExecuted.some((r: any) => r.id === target.id);
2309
+
2310
+ return ok({
2311
+ task_number: taskNumber,
2312
+ task_id: target.id,
2313
+ title: target.title,
2314
+ status: target.status,
2315
+ priority: target.priority === 3 ? "CRITICAL" : target.priority === 2 ? "HIGH" : target.priority === 1 ? "MEDIUM" : "LOW",
2316
+ progress: target.progress,
2317
+ is_recurring_today: isRecurring,
2318
+ description: target.description || "(no description)",
2319
+ custom_fields: target.custom_fields || {},
2320
+ tags: target.tags || [],
2321
+ agent_id: target.agent_id,
2322
+ due_date: target.due_date,
2323
+ completed_at: target.completed_at,
2324
+ updated_at: target.updated_at,
2325
+ });
2326
+ }
2327
+
2260
2328
  // ── SCHEDULE REPORT — Create recurring report ──
2261
2329
  case "schedule_report": {
2262
2330
  const scopeType = (params.scope_type as string) || "all";
@@ -2319,7 +2387,7 @@ function registerNotifyOps(
2319
2387
  }
2320
2388
 
2321
2389
  default:
2322
- return err(`Unknown action "${action}". Valid: list, mark_read, mark_all_read, delete, send_report, schedule_report, list_schedules, delete_schedule`);
2390
+ return err(`Unknown action "${action}". Valid: list, mark_read, mark_all_read, delete, send_report, get_task_detail, schedule_report, list_schedules, delete_schedule`);
2323
2391
  }
2324
2392
  },
2325
2393
  });
@@ -2336,7 +2404,7 @@ async function generatePMReportDirect(
2336
2404
  includeCompleted: boolean,
2337
2405
  ): Promise<{ report: string; scopeLabel: string; taskCount: number }> {
2338
2406
  let scopeLabel = "All Projects";
2339
- let query = supabase.from("tasks").select("id, title, status, priority, progress, completed_at, due_date")
2407
+ let query = supabase.from("tasks").select("id, title, status, priority, progress, completed_at, due_date, description, updated_at")
2340
2408
  .eq("user_id", userId).is("parent_task_id", null);
2341
2409
 
2342
2410
  if (scopeType === "space" && scopeId) {
@@ -2367,24 +2435,53 @@ async function generatePMReportDirect(
2367
2435
  .limit(1).single();
2368
2436
  if (agentRow) agentName = agentRow.name || agentRow.codename || agentId;
2369
2437
 
2438
+ // Detect recently-executed recurring tasks: PENDING but updated within 24h with content
2439
+ const recentThreshold = 24 * 60 * 60 * 1000;
2440
+ const nowMs = Date.now();
2441
+ const isRecentlyExecuted = (t: any) =>
2442
+ (t.status === "PENDING" || t.status === "NEW") &&
2443
+ t.description && t.description.length > 50 &&
2444
+ t.updated_at && (nowMs - new Date(t.updated_at).getTime()) < recentThreshold;
2445
+
2446
+ const recentlyExecuted = all.filter(isRecentlyExecuted);
2370
2447
  const completed = all.filter((t: any) => t.status === "DONE");
2371
2448
  const inProgress = all.filter((t: any) => t.status === "IN_PROGRESS" || t.status === "RUNNING");
2372
2449
  const pending = all.filter((t: any) => t.status === "PENDING" || t.status === "NEW");
2373
2450
  const failed = all.filter((t: any) => t.status === "FAILED");
2374
2451
 
2452
+ // Build ordered task list for consistent numbering (same order as report sections)
2453
+ const orderedTaskIds: string[] = [];
2454
+ const taskNumMap = new Map<string, number>();
2455
+ const assignNum = (t: any) => {
2456
+ if (!taskNumMap.has(t.id)) {
2457
+ orderedTaskIds.push(t.id);
2458
+ taskNumMap.set(t.id, orderedTaskIds.length);
2459
+ }
2460
+ return taskNumMap.get(t.id)!;
2461
+ };
2462
+
2463
+ // Pre-assign numbers in display order: Active → Pending → Recently Executed → Done
2464
+ for (const t of inProgress.slice(0, 8)) assignNum(t);
2465
+ for (const t of pending.slice(0, 6)) assignNum(t);
2466
+ for (const t of recentlyExecuted) assignNum(t);
2467
+ for (const t of completed.slice(0, 5)) assignNum(t);
2468
+
2469
+ const doneCount = completed.length + recentlyExecuted.length;
2470
+
2375
2471
  const lines: string[] = [];
2376
2472
  lines.push(`📊 Project Report — ${scopeLabel}`);
2377
2473
  lines.push(`Agent: ${agentName}`);
2378
2474
  lines.push("");
2379
2475
  lines.push("━━━ Summary ━━━");
2380
- lines.push(`✅ ${completed.length} | 🔄 ${inProgress.length} | ⏳ ${pending.length} | ❌ ${failed.length}`);
2476
+ lines.push(`✅ ${doneCount} | 🔄 ${inProgress.length} | ⏳ ${pending.length} | ❌ ${failed.length}`);
2381
2477
 
2382
2478
  if (inProgress.length > 0) {
2383
2479
  lines.push("");
2384
2480
  lines.push("━━━ Active ━━━");
2385
2481
  for (const t of inProgress.slice(0, 8)) {
2482
+ const n = taskNumMap.get(t.id)!;
2386
2483
  const pl = t.priority === 3 ? "CRIT" : t.priority === 2 ? "HIGH" : t.priority === 1 ? "MED" : "LOW";
2387
- lines.push(`🔄 [${pl}] ${t.title}${t.progress ? ` — ${t.progress}%` : ""}`);
2484
+ lines.push(`${n}. 🔄 [${pl}] ${t.title}${t.progress ? ` — ${t.progress}%` : ""}`);
2388
2485
  }
2389
2486
  if (inProgress.length > 8) lines.push(`... +${inProgress.length - 8} more`);
2390
2487
  }
@@ -2393,26 +2490,41 @@ async function generatePMReportDirect(
2393
2490
  lines.push("");
2394
2491
  lines.push("━━━ Pending ━━━");
2395
2492
  for (const t of pending.slice(0, 6)) {
2493
+ const n = taskNumMap.get(t.id)!;
2396
2494
  const pl = t.priority === 3 ? "CRIT" : t.priority === 2 ? "HIGH" : t.priority === 1 ? "MED" : "LOW";
2397
- lines.push(`⏳ [${pl}] ${t.title}`);
2495
+ const suffix = isRecentlyExecuted(t) ? " (next cycle)" : "";
2496
+ lines.push(`${n}. ⏳ [${pl}] ${t.title}${suffix}`);
2398
2497
  }
2399
2498
  if (pending.length > 6) lines.push(`... +${pending.length - 6} more`);
2400
2499
  }
2401
2500
 
2402
- if (includeCompleted && completed.length > 0) {
2501
+ // Done section: recently-executed recurring tasks + regular completed
2502
+ const doneItems: { task: any; isRecurring: boolean }[] = [
2503
+ ...recentlyExecuted.map((t: any) => ({ task: t, isRecurring: true })),
2504
+ ...(includeCompleted ? completed.filter((t: any) => t.completed_at)
2505
+ .sort((a: any, b: any) => new Date(b.completed_at).getTime() - new Date(a.completed_at).getTime())
2506
+ .slice(0, 5).map((t: any) => ({ task: t, isRecurring: false })) : []),
2507
+ ];
2508
+
2509
+ if (doneItems.length > 0) {
2403
2510
  lines.push("");
2404
2511
  lines.push("━━━ Done ━━━");
2405
- const recent = completed.filter((t: any) => t.completed_at)
2406
- .sort((a: any, b: any) => new Date(b.completed_at).getTime() - new Date(a.completed_at).getTime())
2407
- .slice(0, 5);
2408
- for (const t of recent) {
2409
- const diff = Date.now() - new Date(t.completed_at).getTime();
2410
- const mins = Math.floor(diff / 60000);
2411
- const ago = mins < 60 ? `${mins}m ago` : mins < 1440 ? `${Math.floor(mins / 60)}h ago` : `${Math.floor(mins / 1440)}d ago`;
2412
- lines.push(`✅ ${t.title} (${ago})`);
2512
+ for (const { task: t, isRecurring } of doneItems) {
2513
+ const n = assignNum(t);
2514
+ if (isRecurring) {
2515
+ lines.push(`${n}. ✅ ${t.title} (today's run)`);
2516
+ } else {
2517
+ const diff = Date.now() - new Date(t.completed_at).getTime();
2518
+ const mins = Math.floor(diff / 60000);
2519
+ const ago = mins < 60 ? `${mins}m ago` : mins < 1440 ? `${Math.floor(mins / 60)}h ago` : `${Math.floor(mins / 1440)}d ago`;
2520
+ lines.push(`${n}. ✅ ${t.title} (${ago})`);
2521
+ }
2413
2522
  }
2414
2523
  }
2415
2524
 
2525
+ lines.push("");
2526
+ lines.push(`💡 Reply "details on #N" for full task result`);
2527
+
2416
2528
  const now = new Date();
2417
2529
  lines.push("");
2418
2530
  lines.push(`Generated: ${now.toISOString().replace("T", " ").slice(0, 16)} UTC`);