ofiere-openclaw-plugin 4.16.0 → 4.17.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 CHANGED
@@ -5,7 +5,7 @@ Manage your Ofiere PM dashboard directly from OpenClaw agents. Create tasks, man
5
5
  ## Quick Install (One-Click)
6
6
 
7
7
  ```bash
8
- curl -sSL https://raw.githubusercontent.com/gilanggemar/Ofiere/main/ofiere-openclaw-plugin/install.sh | bash -s -- \
8
+ curl -sSL https://ofiere.com/scripts/install.sh | bash -s -- \
9
9
  --supabase-url "https://xxx.supabase.co" \
10
10
  --service-key "eyJ..." \
11
11
  --user-id "your-uuid"
@@ -16,7 +16,7 @@ Only 3 parameters needed. All agents get the plugin automatically.
16
16
  ## Uninstall
17
17
 
18
18
  ```bash
19
- curl -sSL https://raw.githubusercontent.com/gilanggemar/Ofiere/main/ofiere-openclaw-plugin/uninstall.sh | bash
19
+ curl -sSL https://ofiere.com/scripts/uninstall.sh | bash
20
20
  ```
21
21
 
22
22
  ## How It Works
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ofiere-openclaw-plugin",
3
- "version": "4.16.0",
3
+ "version": "4.17.0",
4
4
  "type": "module",
5
5
  "description": "OpenClaw plugin for Ofiere PM - 11 meta-tools covering tasks, agents, projects, scheduling, knowledge, workflows, notifications, memory, prompts, constellation, and space file management",
6
6
  "keywords": ["openclaw", "ofiere", "project-management", "agents", "plugin"],
package/src/prompt.ts CHANGED
@@ -62,10 +62,19 @@ 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 (action: "list", "mark_read", "mark_all_read", "delete")
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")
66
66
  - list: Recent notifications. unread_only=true for unread only
67
67
  - mark_read: Mark one notification read by ID
68
- - mark_all_read: Mark all as read`,
68
+ - mark_all_read: Mark all as read
69
+ - send_report: Send a PM progress report to YOUR connected channels (Telegram, Discord, Slack, etc.)
70
+ - Required: scope_type (space|folder|project|task|all)
71
+ - Optional: scope_id, channel_types[] (filter to specific channels), include_completed
72
+ - The report is automatically generated from current PM data and sent through YOUR channel bindings ONLY
73
+ - schedule_report: Create a recurring/scheduled report
74
+ - Required: scope_type, recurrence_type (hourly|daily|weekly|monthly)
75
+ - 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
76
+ - list_schedules: View all your active report schedules
77
+ - delete_schedule: Remove a scheduled report by schedule_id`,
69
78
 
70
79
  OFIERE_MEMORY_OPS: `- **OFIERE_MEMORY_OPS** — Conversation history & knowledge memory (action: "list_conversations", "get_messages", "search_messages", "add_knowledge", "search_knowledge")
71
80
  - list_conversations: Recent chats, filter by agent_id
@@ -165,6 +174,9 @@ ${toolDocs}
165
174
  - 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.
166
175
  - Use OFIERE_FILE_OPS to create output files (reports, data, configs) in the Space Files explorer. Prefer create_text_file for text-based outputs.
167
176
  - To save task output as a file, call OFIERE_FILE_OPS action:"create_text_file" with the space_id from the task context.
177
+ - 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". The report is generated from live PM data and sent through YOUR connected channels ONLY — not other agents' channels.
178
+ - To set up recurring reports (e.g. "send me a daily report at 9am"), use OFIERE_NOTIFY_OPS action:"schedule_report" with scope_type and recurrence_type.
179
+ - 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.
168
180
  </ofiere-pm>`;
169
181
  }
170
182
 
package/src/tools.ts CHANGED
@@ -2144,24 +2144,51 @@ function registerNotifyOps(
2144
2144
  supabase: SupabaseClient,
2145
2145
  userId: string,
2146
2146
  ): void {
2147
+ // Helper: resolve the agent's own ID for agent-scoped operations
2148
+ const resolveAgentId = (): string => {
2149
+ try {
2150
+ const state = api.getState?.() || {};
2151
+ return state.agentId || process.env.OFIERE_AGENT_ID || "";
2152
+ } catch { return process.env.OFIERE_AGENT_ID || ""; }
2153
+ };
2154
+
2147
2155
  api.registerTool({
2148
2156
  name: "OFIERE_NOTIFY_OPS",
2149
- label: "Ofiere Notification Operations",
2157
+ label: "Ofiere Notification & Report Operations",
2150
2158
  description:
2151
- `Read and manage notifications.\n\n` +
2159
+ `Read/manage notifications and send PM reports to your connected channels.\n\n` +
2152
2160
  `Actions:\n` +
2153
2161
  `- "list": List notifications. Optional: unread_only (true/false), limit\n` +
2154
2162
  `- "mark_read": Mark one as read. Required: id\n` +
2155
2163
  `- "mark_all_read": Mark all as read\n` +
2156
- `- "delete": Delete a notification. Required: id`,
2164
+ `- "delete": Delete a notification. Required: id\n` +
2165
+ `- "send_report": Send a PM progress report to YOUR connected channels (Telegram, Discord, etc.)\n` +
2166
+ ` Required: scope_type (space|folder|project|task|all)\n` +
2167
+ ` Optional: scope_id, channel_types[] (filter specific channels), include_completed (default true)\n` +
2168
+ `- "schedule_report": Create a recurring/scheduled report\n` +
2169
+ ` Required: scope_type, recurrence_type (hourly|daily|weekly|monthly)\n` +
2170
+ ` Optional: scope_id, scope_label, channel_types[], recurrence_time (HH:MM UTC, default 09:00),\n` +
2171
+ ` recurrence_interval (default 1), recurrence_days_of_week (e.g. "mon,wed,fri"), include_completed\n` +
2172
+ `- "list_schedules": List your active report schedules\n` +
2173
+ `- "delete_schedule": Delete a scheduled report. Required: schedule_id`,
2157
2174
  parameters: {
2158
2175
  type: "object",
2159
2176
  required: ["action"],
2160
2177
  properties: {
2161
- action: { type: "string", enum: ["list", "mark_read", "mark_all_read", "delete"] },
2178
+ action: { type: "string", enum: ["list", "mark_read", "mark_all_read", "delete", "send_report", "schedule_report", "list_schedules", "delete_schedule"] },
2162
2179
  id: { type: "string", description: "Notification ID" },
2180
+ schedule_id: { type: "string", description: "Schedule ID (for delete_schedule)" },
2163
2181
  unread_only: { type: "boolean", description: "Only show unread" },
2164
2182
  limit: { type: "number", description: "Max results (default 50)" },
2183
+ scope_type: { type: "string", enum: ["space", "folder", "project", "task", "all"], description: "Report scope" },
2184
+ scope_id: { type: "string", description: "ID of the space/folder/project/task" },
2185
+ scope_label: { type: "string", description: "Human-readable label for the scope" },
2186
+ channel_types: { type: "array", items: { type: "string" }, description: "Filter to specific channels (e.g. ['telegram'])" },
2187
+ include_completed: { type: "boolean", description: "Include completed tasks in report" },
2188
+ recurrence_type: { type: "string", enum: ["hourly", "daily", "weekly", "monthly"], description: "How often to send" },
2189
+ recurrence_interval: { type: "number", description: "Interval multiplier (default 1)" },
2190
+ recurrence_time: { type: "string", description: "Time in HH:MM UTC (default 09:00)" },
2191
+ recurrence_days_of_week: { type: "string", description: "Comma-separated days for weekly (e.g. mon,wed,fri)" },
2165
2192
  },
2166
2193
  },
2167
2194
  async execute(_id: string, params: Record<string, unknown>) {
@@ -2194,13 +2221,267 @@ function registerNotifyOps(
2194
2221
  if (error) return err(error.message);
2195
2222
  return ok({ message: "Notification deleted", ok: true });
2196
2223
  }
2224
+
2225
+ // ── SEND REPORT — Dispatch PM report to agent's channels ──
2226
+ case "send_report": {
2227
+ const scopeType = (params.scope_type as string) || "all";
2228
+ const scopeId = params.scope_id as string | undefined;
2229
+ const channelTypes = (params.channel_types as string[]) || [];
2230
+ const includeCompleted = params.include_completed !== false;
2231
+ const agentId = resolveAgentId();
2232
+ if (!agentId) return err("Cannot resolve your agent ID. Ensure OFIERE_AGENT_ID is configured.");
2233
+
2234
+ // Call the dashboard API to generate + dispatch
2235
+ const supabaseUrl = (supabase as any).supabaseUrl || process.env.OFIERE_SUPABASE_URL || process.env.SUPABASE_URL || "";
2236
+ const serviceRoleKey = (supabase as any).supabaseKey || process.env.OFIERE_SERVICE_ROLE_KEY || process.env.SUPABASE_SERVICE_ROLE_KEY || "";
2237
+
2238
+ // Generate report directly from DB (we have service-role access)
2239
+ const reportResult = await generatePMReportDirect(supabase, userId, scopeType, scopeId, agentId, includeCompleted);
2240
+
2241
+ // Dispatch to channels
2242
+ const delivery = await dispatchReportDirect(supabase, userId, agentId, reportResult.report, channelTypes);
2243
+
2244
+ // Create audit notification
2245
+ await supabase.from("notifications").insert({
2246
+ id: crypto.randomUUID(),
2247
+ user_id: userId,
2248
+ title: `Report sent: ${reportResult.scopeLabel}`,
2249
+ message: `Dispatched via ${delivery.sent.join(", ") || "no channels"}. ${reportResult.taskCount} tasks.`,
2250
+ type: "system",
2251
+ category: "project_management",
2252
+ read: false,
2253
+ });
2254
+
2255
+ return ok({
2256
+ message: `Report sent to ${delivery.sent.length} channel(s)`,
2257
+ channels_sent: delivery.sent,
2258
+ channels_failed: delivery.failed,
2259
+ task_count: reportResult.taskCount,
2260
+ scope: reportResult.scopeLabel,
2261
+ report_preview: reportResult.report.slice(0, 500),
2262
+ });
2263
+ }
2264
+
2265
+ // ── SCHEDULE REPORT — Create recurring report ──
2266
+ case "schedule_report": {
2267
+ const scopeType = (params.scope_type as string) || "all";
2268
+ const recurrenceType = (params.recurrence_type as string) || "daily";
2269
+ const agentId = resolveAgentId();
2270
+ if (!agentId) return err("Cannot resolve your agent ID.");
2271
+
2272
+ const recurrenceTime = (params.recurrence_time as string) || "09:00";
2273
+ const recurrenceInterval = (params.recurrence_interval as number) || 1;
2274
+ const recurrenceDaysOfWeek = (params.recurrence_days_of_week as string) || null;
2275
+
2276
+ // Compute next run
2277
+ const [hours, minutes] = recurrenceTime.split(":").map(Number);
2278
+ const next = new Date();
2279
+ next.setUTCHours(hours, minutes, 0, 0);
2280
+ if (next.getTime() <= Date.now()) next.setUTCDate(next.getUTCDate() + 1);
2281
+
2282
+ const { data, error } = await supabase.from("pm_report_schedules").insert({
2283
+ user_id: userId,
2284
+ agent_id: agentId,
2285
+ scope_type: scopeType,
2286
+ scope_id: (params.scope_id as string) || null,
2287
+ scope_label: (params.scope_label as string) || null,
2288
+ channel_types: (params.channel_types as string[]) || [],
2289
+ recurrence_type: recurrenceType,
2290
+ recurrence_interval: recurrenceInterval,
2291
+ recurrence_time: recurrenceTime,
2292
+ recurrence_days_of_week: recurrenceDaysOfWeek,
2293
+ include_completed: params.include_completed !== false,
2294
+ next_run_at: next.toISOString(),
2295
+ }).select().single();
2296
+
2297
+ if (error) return err(error.message);
2298
+ return ok({
2299
+ message: `Scheduled ${recurrenceType} report created`,
2300
+ schedule_id: data.id,
2301
+ next_run: next.toISOString(),
2302
+ recurrence: `${recurrenceType} at ${recurrenceTime} UTC`,
2303
+ });
2304
+ }
2305
+
2306
+ // ── LIST SCHEDULES ──
2307
+ case "list_schedules": {
2308
+ const { data, error } = await supabase.from("pm_report_schedules")
2309
+ .select("*")
2310
+ .eq("user_id", userId)
2311
+ .order("created_at", { ascending: false });
2312
+ if (error) return err(error.message);
2313
+ return ok({ schedules: data || [], count: (data || []).length });
2314
+ }
2315
+
2316
+ // ── DELETE SCHEDULE ──
2317
+ case "delete_schedule": {
2318
+ const scheduleId = (params.schedule_id || params.id) as string;
2319
+ if (!scheduleId) return err("Missing required: schedule_id");
2320
+ const { error } = await supabase.from("pm_report_schedules")
2321
+ .delete().eq("id", scheduleId).eq("user_id", userId);
2322
+ if (error) return err(error.message);
2323
+ return ok({ message: "Schedule deleted", ok: true });
2324
+ }
2325
+
2197
2326
  default:
2198
- return err(`Unknown action "${action}".`);
2327
+ return err(`Unknown action "${action}". Valid: list, mark_read, mark_all_read, delete, send_report, schedule_report, list_schedules, delete_schedule`);
2199
2328
  }
2200
2329
  },
2201
2330
  });
2202
2331
  }
2203
2332
 
2333
+ // ── Inline report generation for plugin (avoids HTTP roundtrip) ──
2334
+
2335
+ async function generatePMReportDirect(
2336
+ supabase: SupabaseClient,
2337
+ userId: string,
2338
+ scopeType: string,
2339
+ scopeId: string | undefined,
2340
+ agentId: string,
2341
+ includeCompleted: boolean,
2342
+ ): Promise<{ report: string; scopeLabel: string; taskCount: number }> {
2343
+ let scopeLabel = "All Projects";
2344
+ let query = supabase.from("tasks").select("id, title, status, priority, progress, completed_at, due_date")
2345
+ .eq("user_id", userId).is("parent_task_id", null);
2346
+
2347
+ if (scopeType === "space" && scopeId) {
2348
+ query = query.eq("space_id", scopeId);
2349
+ const { data: s } = await supabase.from("pm_spaces").select("name").eq("id", scopeId).single();
2350
+ scopeLabel = s?.name || "Space";
2351
+ } else if (scopeType === "folder" && scopeId) {
2352
+ query = query.eq("folder_id", scopeId);
2353
+ const { data: f } = await supabase.from("pm_folders").select("name").eq("id", scopeId).single();
2354
+ scopeLabel = f?.name || "Folder";
2355
+ } else if (scopeType === "project" && scopeId) {
2356
+ query = query.eq("project_id", scopeId);
2357
+ const { data: p } = await supabase.from("pm_folders").select("name").eq("id", scopeId).single();
2358
+ scopeLabel = p?.name || "Project";
2359
+ }
2360
+
2361
+ const { data: tasks } = await query.order("priority", { ascending: false }).limit(40);
2362
+ const all = tasks || [];
2363
+
2364
+ // Agent name
2365
+ let agentName = agentId;
2366
+ const { data: agentRow } = await supabase.from("agents").select("name, codename")
2367
+ .eq("user_id", userId)
2368
+ .or(`id.eq.${agentId},name.ilike.${agentId},codename.ilike.${agentId}`)
2369
+ .limit(1).single();
2370
+ if (agentRow) agentName = agentRow.name || agentRow.codename || agentId;
2371
+
2372
+ const completed = all.filter((t: any) => t.status === "DONE");
2373
+ const inProgress = all.filter((t: any) => t.status === "IN_PROGRESS" || t.status === "RUNNING");
2374
+ const pending = all.filter((t: any) => t.status === "PENDING" || t.status === "NEW");
2375
+ const failed = all.filter((t: any) => t.status === "FAILED");
2376
+
2377
+ const lines: string[] = [];
2378
+ lines.push(`📊 Project Report — ${scopeLabel}`);
2379
+ lines.push(`Agent: ${agentName}`);
2380
+ lines.push("");
2381
+ lines.push("━━━ Summary ━━━");
2382
+ lines.push(`✅ ${completed.length} | 🔄 ${inProgress.length} | ⏳ ${pending.length} | ❌ ${failed.length}`);
2383
+
2384
+ if (inProgress.length > 0) {
2385
+ lines.push("");
2386
+ lines.push("━━━ Active ━━━");
2387
+ for (const t of inProgress.slice(0, 8)) {
2388
+ const pl = t.priority === 3 ? "CRIT" : t.priority === 2 ? "HIGH" : t.priority === 1 ? "MED" : "LOW";
2389
+ lines.push(`🔄 [${pl}] ${t.title}${t.progress ? ` — ${t.progress}%` : ""}`);
2390
+ }
2391
+ if (inProgress.length > 8) lines.push(`... +${inProgress.length - 8} more`);
2392
+ }
2393
+
2394
+ if (pending.length > 0) {
2395
+ lines.push("");
2396
+ lines.push("━━━ Pending ━━━");
2397
+ for (const t of pending.slice(0, 6)) {
2398
+ const pl = t.priority === 3 ? "CRIT" : t.priority === 2 ? "HIGH" : t.priority === 1 ? "MED" : "LOW";
2399
+ lines.push(`⏳ [${pl}] ${t.title}`);
2400
+ }
2401
+ if (pending.length > 6) lines.push(`... +${pending.length - 6} more`);
2402
+ }
2403
+
2404
+ if (includeCompleted && completed.length > 0) {
2405
+ lines.push("");
2406
+ lines.push("━━━ Done ━━━");
2407
+ const recent = completed.filter((t: any) => t.completed_at)
2408
+ .sort((a: any, b: any) => new Date(b.completed_at).getTime() - new Date(a.completed_at).getTime())
2409
+ .slice(0, 5);
2410
+ for (const t of recent) {
2411
+ const diff = Date.now() - new Date(t.completed_at).getTime();
2412
+ const mins = Math.floor(diff / 60000);
2413
+ const ago = mins < 60 ? `${mins}m ago` : mins < 1440 ? `${Math.floor(mins / 60)}h ago` : `${Math.floor(mins / 1440)}d ago`;
2414
+ lines.push(`✅ ${t.title} (${ago})`);
2415
+ }
2416
+ }
2417
+
2418
+ const now = new Date();
2419
+ lines.push("");
2420
+ lines.push(`Generated: ${now.toISOString().replace("T", " ").slice(0, 16)} UTC`);
2421
+
2422
+ return { report: lines.join("\n"), scopeLabel, taskCount: all.length };
2423
+ }
2424
+
2425
+ async function dispatchReportDirect(
2426
+ supabase: SupabaseClient,
2427
+ userId: string,
2428
+ agentId: string,
2429
+ report: string,
2430
+ channelTypes: string[],
2431
+ ): Promise<{ sent: string[]; failed: string[] }> {
2432
+ // Query agent's active channel bindings
2433
+ let query = supabase.from("channel_bindings").select("channel_type, channel_account_id, channel_config")
2434
+ .eq("user_id", userId)
2435
+ .eq("agent_id", agentId)
2436
+ .eq("notifications_enabled", true)
2437
+ .eq("is_active", true);
2438
+
2439
+ if (channelTypes.length > 0) {
2440
+ query = query.in("channel_type", channelTypes);
2441
+ }
2442
+
2443
+ const { data: bindings } = await query;
2444
+ if (!bindings || bindings.length === 0) {
2445
+ return { sent: [], failed: ["No active channel bindings found for this agent"] };
2446
+ }
2447
+
2448
+ const LIMITS: Record<string, number> = { telegram: 4096, discord: 2000, slack: 40000, whatsapp: 65536 };
2449
+ const sent: string[] = [];
2450
+ const failed: string[] = [];
2451
+
2452
+ for (const b of bindings) {
2453
+ const limit = LIMITS[b.channel_type] || 4000;
2454
+ let msg = report;
2455
+ if (msg.length > limit) {
2456
+ msg = msg.substring(0, limit - 80) + "\n\n... [Full report on dashboard]";
2457
+ }
2458
+
2459
+ // Use the Ofiere dashboard API to send via gateway
2460
+ // The dashboard URL is derived from the Supabase URL
2461
+ const dashboardUrl = process.env.OFIERE_DASHBOARD_URL || "https://ofiere.com";
2462
+ const dispatchSecret = process.env.DISPATCH_SECRET || "";
2463
+
2464
+ try {
2465
+ const res = await fetch(`${dashboardUrl}/api/channels/gateway-send`, {
2466
+ method: "POST",
2467
+ headers: { "Content-Type": "application/json", "x-dispatch-secret": dispatchSecret },
2468
+ body: JSON.stringify({
2469
+ userId,
2470
+ channelType: b.channel_type,
2471
+ accountId: b.channel_account_id || null,
2472
+ message: msg,
2473
+ }),
2474
+ });
2475
+ if (res.ok) sent.push(b.channel_type);
2476
+ else failed.push(`${b.channel_type}: HTTP ${res.status}`);
2477
+ } catch (e: any) {
2478
+ failed.push(`${b.channel_type}: ${e.message || "fetch error"}`);
2479
+ }
2480
+ }
2481
+
2482
+ return { sent, failed };
2483
+ }
2484
+
2204
2485
  // ═══════════════════════════════════════════════════════════════════════════════
2205
2486
  // META-TOOL 8: OFIERE_MEMORY_OPS — Conversations & Knowledge Fragments
2206
2487
  // ═══════════════════════════════════════════════════════════════════════════════