openclaw-remote 0.4.5 → 0.5.1

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 (2) hide show
  1. package/dist/index.js +137 -420
  2. package/package.json +14 -8
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ var __export = (target, all) => {
8
8
  import {
9
9
  DEFAULT_ACCOUNT_ID,
10
10
  setAccountEnabledInConfigSection,
11
- waitUntilAbort,
11
+ registerPluginHttpRoute,
12
12
  optionalStringEnum
13
13
  } from "openclaw/plugin-sdk/compat";
14
14
 
@@ -2655,13 +2655,11 @@ function resolveAccount(cfg, accountId) {
2655
2655
  baseUrl: (accountOverride.baseUrl ?? channelCfg.baseUrl ?? envBaseUrl).replace(/\/+$/, ""),
2656
2656
  apiKey: accountOverride.apiKey ?? channelCfg.apiKey ?? envApiKey,
2657
2657
  projectId: accountOverride.projectId ?? channelCfg.projectId,
2658
- supabaseUrl: accountOverride.supabaseUrl ?? channelCfg.supabaseUrl ?? process.env.REMOTE_SUPABASE_URL,
2659
- supabaseKey: accountOverride.supabaseKey ?? channelCfg.supabaseKey ?? process.env.REMOTE_SUPABASE_KEY,
2660
2658
  dmPolicy: accountOverride.dmPolicy ?? channelCfg.dmPolicy ?? "open",
2661
2659
  allowedUserIds: parseAllowedUserIds(
2662
2660
  accountOverride.allowedUserIds ?? channelCfg.allowedUserIds ?? envAllowedUserIds
2663
2661
  ),
2664
- webhookPath: accountOverride.webhookPath ?? channelCfg.webhookPath
2662
+ webhookPath: accountOverride.webhookPath ?? channelCfg.webhookPath ?? `/webhooks/remote/${id}`
2665
2663
  };
2666
2664
  }
2667
2665
 
@@ -2793,353 +2791,73 @@ async function getTeam(account) {
2793
2791
  );
2794
2792
  }
2795
2793
 
2796
- // src/realtime-listener.ts
2797
- import { createClient } from "@supabase/supabase-js";
2798
- var leaderTimers = /* @__PURE__ */ new Map();
2799
- var LEADER_DEBOUNCE_MS = 6e4;
2800
- var leaderRolesCache = [];
2801
- var leaderRolesCacheTime = 0;
2802
- var LEADER_CACHE_TTL = 5 * 6e4;
2803
- async function resolveAgentIdentity(account, log) {
2804
- try {
2805
- const res = await fetch(`${account.baseUrl}/api/v1/roles`, {
2806
- headers: { Authorization: `Bearer ${account.apiKey}`, Accept: "application/json" },
2807
- signal: AbortSignal.timeout(1e4)
2808
- });
2809
- if (!res.ok) {
2810
- log?.warn?.(`Failed to resolve agent identity: HTTP ${res.status}`);
2811
- return null;
2812
- }
2813
- const data = await res.json();
2814
- const roles = data.roles || [];
2815
- const myRoles = roles.filter((r) => r.is_mine);
2816
- const myRoleIds = myRoles.map((r) => r.id);
2817
- const isLeader = myRoles.some((r) => r.is_leader);
2818
- let agentName = "";
2819
- for (const r of myRoles) {
2820
- if (r.assigned_to?.type === "agent" && r.assigned_to?.name) {
2821
- agentName = r.assigned_to.name;
2822
- break;
2823
- }
2824
- }
2825
- let agentId = "";
2826
- try {
2827
- const hbRes = await fetch(`${account.baseUrl}/api/v1/heartbeat`, {
2828
- headers: { Authorization: `Bearer ${account.apiKey}`, Accept: "application/json" },
2829
- signal: AbortSignal.timeout(1e4)
2830
- });
2831
- if (hbRes.ok) {
2832
- const hb = await hbRes.json();
2833
- agentId = hb.agent_id || "";
2834
- if (!agentName) agentName = hb.agent_name || "";
2835
- }
2836
- } catch {
2837
- }
2838
- log?.info?.(`Agent identity resolved: "${agentName}" (${agentId}), roles: [${myRoleIds.join(", ")}], leader: ${isLeader}`);
2839
- return { agentId, agentName, myRoleIds, isLeader };
2840
- } catch (err) {
2841
- log?.warn?.(`Failed to resolve agent identity: ${err}`);
2842
- return null;
2843
- }
2844
- }
2845
- async function fetchLeaderRoles(supabase, log) {
2846
- const now = Date.now();
2847
- if (now - leaderRolesCacheTime < LEADER_CACHE_TTL && leaderRolesCache.length > 0) {
2848
- return leaderRolesCache;
2849
- }
2850
- const { data: roles } = await supabase.from("project_roles").select("id").eq("is_leader", true);
2851
- if (!roles || roles.length === 0) {
2852
- leaderRolesCache = [];
2853
- leaderRolesCacheTime = now;
2854
- return [];
2855
- }
2856
- const roleIds = roles.map((r) => r.id);
2857
- const { data: assignments } = await supabase.from("agent_role_assignments").select("project_role_id, agent_id").in("project_role_id", roleIds);
2858
- const map = /* @__PURE__ */ new Map();
2859
- for (const a of assignments ?? []) {
2860
- const list = map.get(a.project_role_id) || [];
2861
- list.push(a.agent_id);
2862
- map.set(a.project_role_id, list);
2863
- }
2864
- leaderRolesCache = roleIds.map((id) => ({ roleId: id, agentIds: map.get(id) || [] }));
2865
- leaderRolesCacheTime = now;
2866
- log?.info?.(`Leader roles cache refreshed: ${leaderRolesCache.length} leader role(s)`);
2867
- return leaderRolesCache;
2868
- }
2869
- function connectRealtime(account, identity, onEvent, onError, log) {
2870
- const { supabaseUrl, supabaseKey } = account;
2871
- if (!supabaseUrl || !supabaseKey) {
2872
- throw new Error("Supabase URL and key are required for Realtime listener. Set supabaseUrl and supabaseKey in channels.remote config.");
2873
- }
2874
- log?.info?.(`Connecting to Supabase Realtime: ${supabaseUrl}`);
2875
- const supabase = createClient(supabaseUrl, supabaseKey, {
2876
- realtime: {
2877
- params: { eventsPerSecond: 10 }
2878
- }
2879
- });
2880
- const channelName = `remote-${account.accountId}-${Date.now()}`;
2881
- const channel = supabase.channel(channelName).on(
2882
- "postgres_changes",
2883
- {
2884
- event: "INSERT",
2885
- schema: "public",
2886
- table: "task_comments"
2887
- },
2888
- (payload) => {
2889
- try {
2890
- const row = payload.new;
2891
- if (row.agent_author_id && !row.author_id) {
2892
- scheduleLeaderNotification(row.task_id, row.agent_author_id, supabase, log);
2893
- if (identity && row.agent_author_id === identity.agentId) {
2894
- log?.info?.(`Skipping own comment on task ${row.task_id} (echo suppression)`);
2895
- return;
2896
- }
2897
- log?.info?.(`Delivering agent comment from ${row.agent_author_id} on task ${row.task_id}`);
2898
- }
2899
- const body = row.body || row.content || "";
2900
- if (!row.author_id && !row.agent_author_id && identity) {
2901
- if (body.startsWith("\u{1F4CC} Assigned to ")) {
2902
- const targetName = body.replace("\u{1F4CC} Assigned to ", "").trim();
2903
- if (targetName.toLowerCase() !== identity.agentName.toLowerCase()) {
2904
- log?.info?.(`Skipping assignment notification for ${targetName} (I am ${identity.agentName})`);
2905
- return;
2906
- }
2907
- }
2908
- if (body.startsWith("\u{1F440} ")) {
2909
- if (!identity.isLeader) {
2910
- log?.info?.(`Skipping leader notification (I am not a leader)`);
2911
- return;
2912
- }
2913
- }
2914
- }
2915
- const event = {
2916
- type: "task.commented",
2917
- task_id: row.task_id,
2918
- comment: {
2919
- id: row.id,
2920
- content: body,
2921
- author_name: row.author_name || "System",
2922
- author_id: row.author_id || "",
2923
- created_at: row.created_at
2924
- }
2925
- };
2926
- log?.info?.(`Realtime: new comment on task ${row.task_id}`);
2927
- onEvent(event);
2928
- } catch (err) {
2929
- log?.warn?.(`Failed to process comment event: ${err}`);
2930
- }
2794
+ // src/runtime.ts
2795
+ import { createPluginRuntimeStore } from "openclaw/plugin-sdk/compat";
2796
+ var { setRuntime: setRemoteRuntime, getRuntime: getRemoteRuntime } = createPluginRuntimeStore(
2797
+ "Remote runtime not initialized - plugin not registered"
2798
+ );
2799
+
2800
+ // src/channel.ts
2801
+ var CHANNEL_ID = "remote";
2802
+ var activeRouteUnregisters = /* @__PURE__ */ new Map();
2803
+ function waitUntilAbort(signal, onAbort) {
2804
+ return new Promise((resolve) => {
2805
+ const complete = () => {
2806
+ onAbort?.();
2807
+ resolve();
2808
+ };
2809
+ if (!signal) {
2810
+ return;
2931
2811
  }
2932
- );
2933
- channel.subscribe((status, err) => {
2934
- if (status === "SUBSCRIBED") {
2935
- log?.info?.("Supabase Realtime connected and subscribed.");
2936
- } else if (status === "CHANNEL_ERROR") {
2937
- log?.error?.(`Realtime channel error: ${err?.message || "unknown"}`);
2938
- onError(new Error(`Realtime channel error: ${err?.message || "unknown"}`));
2939
- } else if (status === "TIMED_OUT") {
2940
- log?.warn?.("Realtime subscription timed out, will auto-retry...");
2941
- } else if (status === "CLOSED") {
2942
- log?.info?.("Realtime channel closed.");
2812
+ if (signal.aborted) {
2813
+ complete();
2814
+ return;
2943
2815
  }
2816
+ signal.addEventListener("abort", complete, { once: true });
2944
2817
  });
2945
- return {
2946
- unsubscribe: async () => {
2947
- log?.info?.("Unsubscribing from Supabase Realtime...");
2948
- for (const timer of leaderTimers.values()) clearTimeout(timer);
2949
- leaderTimers.clear();
2950
- await supabase.removeChannel(channel);
2951
- log?.info?.("Realtime listener shut down cleanly.");
2952
- }
2953
- };
2954
2818
  }
2955
- async function scheduleLeaderNotification(taskId, commentingAgentId, supabase, log) {
2956
- const existing = leaderTimers.get(taskId);
2957
- if (existing) clearTimeout(existing);
2958
- const timer = setTimeout(async () => {
2959
- leaderTimers.delete(taskId);
2960
- try {
2961
- const leaders = await fetchLeaderRoles(supabase, log);
2962
- if (leaders.length === 0) return;
2963
- const { data: task } = await supabase.from("tasks").select("id, title, assigned_role_id").eq("id", taskId).single();
2964
- if (!task) return;
2965
- for (const leader of leaders) {
2966
- if (task.assigned_role_id === leader.roleId) {
2967
- log?.info?.(`Skipping leader notification for task ${taskId} \u2014 assigned to leader role ${leader.roleId}`);
2968
- continue;
2969
- }
2970
- if (leader.agentIds.includes(commentingAgentId)) continue;
2971
- if (leader.agentIds.length === 0) continue;
2972
- const { error } = await supabase.from("task_comments").insert({
2973
- task_id: taskId,
2974
- author_id: null,
2975
- agent_author_id: null,
2976
- body: `\u{1F440} Activity on this task \u2014 leader review needed`
2977
- });
2978
- if (error) {
2979
- log?.warn?.(`Failed to insert leader notification for task ${taskId}: ${error.message}`);
2980
- } else {
2981
- log?.info?.(`Leader notification sent for task ${taskId}`);
2982
- }
2983
- }
2984
- } catch (err) {
2985
- log?.warn?.(`Leader notification error for task ${taskId}: ${err}`);
2986
- }
2987
- }, LEADER_DEBOUNCE_MS);
2988
- leaderTimers.set(taskId, timer);
2989
- log?.info?.(`Leader debounce timer set for task ${taskId} (60s)`);
2990
- }
2991
-
2992
- // src/event-formatter.ts
2993
- function formatEvent(event) {
2994
- switch (event.type) {
2819
+ function formatWebhookEvent(payload) {
2820
+ const { event, task_id, task_title, comment_body, author_name, wake_reason } = payload;
2821
+ switch (event) {
2822
+ case "task.assigned":
2995
2823
  case "task.created":
2996
- return formatTaskCreated(event);
2997
- case "task.updated":
2998
- return formatTaskUpdated(event);
2824
+ return [
2825
+ `You've been assigned a new task:`,
2826
+ ``,
2827
+ `**${task_title}** (ID: ${task_id})`,
2828
+ ``,
2829
+ wake_reason ? `Reason: ${wake_reason}` : "",
2830
+ ``,
2831
+ `Use this task ID for API calls (e.g. to move it to in_progress).`,
2832
+ `Review the task and share your initial thoughts or plan.`
2833
+ ].filter(Boolean).join("\n");
2999
2834
  case "task.commented":
3000
- return formatTaskCommented(event);
3001
- case "task.assigned":
3002
- return formatTaskAssigned(event);
2835
+ return [
2836
+ `${author_name || "Someone"} commented on "${task_title}" (ID: ${task_id}):`,
2837
+ ``,
2838
+ `> ${comment_body || ""}`,
2839
+ ``,
2840
+ `Respond to their comment.`
2841
+ ].join("\n");
2842
+ case "task.mentioned":
2843
+ return [
2844
+ `You were mentioned in a comment on "${task_title}" (ID: ${task_id}):`,
2845
+ ``,
2846
+ `> ${comment_body || ""}`,
2847
+ ``,
2848
+ `Respond to the mention.`
2849
+ ].join("\n");
2850
+ case "task.status_changed":
2851
+ return [
2852
+ `Task "${task_title}" (ID: ${task_id}) status changed.`,
2853
+ wake_reason ? `Details: ${wake_reason}` : "",
2854
+ ``,
2855
+ `Acknowledge the status change if relevant to your work.`
2856
+ ].filter(Boolean).join("\n");
3003
2857
  default:
3004
- return formatGenericEvent(event);
2858
+ return `Task update on "${task_title}" (ID: ${task_id}): ${event}${wake_reason ? ` \u2014 ${wake_reason}` : ""}`;
3005
2859
  }
3006
2860
  }
3007
- function formatTaskCreated(event) {
3008
- const task = event.task;
3009
- if (!task) return null;
3010
- const meta = [task.type, task.priority].filter(Boolean).join(", ");
3011
- let body = `\u{1F4CB} New task created: **${task.title}**`;
3012
- if (meta) body += ` [${meta}]`;
3013
- if (task.description) body += `
3014
- ${task.description}`;
3015
- body += `
3016
-
3017
- [task:${task.id}]`;
3018
- return {
3019
- body,
3020
- taskId: task.id,
3021
- taskTitle: task.title,
3022
- isDirect: false
3023
- };
3024
- }
3025
- function formatTaskUpdated(event) {
3026
- const task = event.task;
3027
- if (!task) return null;
3028
- const changes = event.changes;
3029
- if (changes?.field === "assigned_role_id" || changes?.field === "assigned_to_agent" || changes?.field === "assigned_profile_id") {
3030
- if (changes.to && changes.to !== "null" && changes.to !== "") {
3031
- let body2 = `\u{1F464} You've been assigned: **${task.title}**`;
3032
- if (task.description) body2 += `
3033
-
3034
- ${task.description}`;
3035
- body2 += `
3036
-
3037
- Priority: ${task.priority || "medium"} | Type: ${task.type || "task"}`;
3038
- body2 += `
3039
-
3040
- Acknowledge this task, move it to in_progress using remote_update_task, and share your plan.`;
3041
- body2 += `
3042
-
3043
- [task:${task.id}]`;
3044
- return {
3045
- body: body2,
3046
- taskId: task.id,
3047
- taskTitle: task.title,
3048
- isDirect: true
3049
- };
3050
- }
3051
- return null;
3052
- }
3053
- if (changes?.field === "status") {
3054
- let body2 = `\u{1F504} Task **${task.title}** moved to **${changes.to}**`;
3055
- body2 += `
3056
-
3057
- [task:${task.id}]`;
3058
- return {
3059
- body: body2,
3060
- taskId: task.id,
3061
- taskTitle: task.title,
3062
- isDirect: false
3063
- };
3064
- }
3065
- let body = `\u{1F504} Task updated: **${task.title}**`;
3066
- if (changes) {
3067
- body += ` \u2014 ${changes.field} changed from "${changes.from}" to "${changes.to}"`;
3068
- }
3069
- body += `
3070
-
3071
- [task:${task.id}]`;
3072
- return {
3073
- body,
3074
- taskId: task.id,
3075
- taskTitle: task.title,
3076
- isDirect: false
3077
- };
3078
- }
3079
- function formatTaskCommented(event) {
3080
- const comment = event.comment;
3081
- const taskId = event.task_id ?? event.task?.id;
3082
- if (!comment || !taskId) return null;
3083
- const taskTitle = event.task?.title ?? `Task ${taskId}`;
3084
- let body = `\u{1F4AC} ${comment.author_name} commented on **${taskTitle}**:
3085
- ${comment.content}`;
3086
- body += `
3087
-
3088
- [task:${taskId}]`;
3089
- return {
3090
- body,
3091
- taskId,
3092
- senderName: comment.author_name,
3093
- senderId: comment.author_id,
3094
- taskTitle,
3095
- isDirect: false
3096
- };
3097
- }
3098
- function formatTaskAssigned(event) {
3099
- const task = event.task;
3100
- const assignee = event.assigned_to;
3101
- if (!task) return null;
3102
- let body = `\u{1F464} Task **${task.title}** assigned`;
3103
- if (assignee) {
3104
- body += ` to ${assignee.name} (${assignee.role})`;
3105
- }
3106
- body += `
3107
-
3108
- [task:${task.id}]`;
3109
- return {
3110
- body,
3111
- taskId: task.id,
3112
- senderName: assignee?.name,
3113
- taskTitle: task.title,
3114
- // Assigned events are direct — the agent is likely the assignee
3115
- isDirect: true
3116
- };
3117
- }
3118
- function formatGenericEvent(event) {
3119
- const task = event.task;
3120
- const taskId = event.task_id ?? task?.id;
3121
- if (!taskId) return null;
3122
- const taskTitle = task?.title ?? `Task ${taskId}`;
3123
- let body = `\u{1F514} Remote event (${event.type}): **${taskTitle}**`;
3124
- body += `
3125
-
3126
- [task:${taskId}]`;
3127
- return {
3128
- body,
3129
- taskId,
3130
- taskTitle,
3131
- isDirect: false
3132
- };
3133
- }
3134
-
3135
- // src/runtime.ts
3136
- import { createPluginRuntimeStore } from "openclaw/plugin-sdk/compat";
3137
- var { setRuntime: setRemoteRuntime, getRuntime: getRemoteRuntime } = createPluginRuntimeStore(
3138
- "Remote runtime not initialized - plugin not registered"
3139
- );
3140
-
3141
- // src/channel.ts
3142
- var CHANNEL_ID = "remote";
3143
2861
  function createRemotePlugin() {
3144
2862
  return {
3145
2863
  id: CHANNEL_ID,
@@ -3218,11 +2936,6 @@ function createRemotePlugin() {
3218
2936
  "- Remote: apiKey is not configured. The plugin cannot authenticate with the Remote API."
3219
2937
  );
3220
2938
  }
3221
- if (account.dmPolicy === "open") {
3222
- warnings.push(
3223
- '- Remote: dmPolicy="open" allows any board event to trigger agent actions. Consider "allowlist" for production use.'
3224
- );
3225
- }
3226
2939
  return warnings;
3227
2940
  }
3228
2941
  },
@@ -3268,96 +2981,100 @@ function createRemotePlugin() {
3268
2981
  },
3269
2982
  gateway: {
3270
2983
  startAccount: async (ctx) => {
3271
- const { cfg, accountId, log, abortSignal, channelRuntime } = ctx;
2984
+ const { cfg, accountId, log } = ctx;
3272
2985
  const account = resolveAccount(cfg, accountId);
3273
2986
  if (!account.enabled) {
3274
2987
  log?.info?.(`Remote account ${accountId} is disabled, skipping`);
3275
- return waitUntilAbort(abortSignal);
2988
+ return waitUntilAbort(ctx.abortSignal);
3276
2989
  }
3277
2990
  if (!account.baseUrl || !account.apiKey) {
3278
2991
  log?.warn?.(
3279
2992
  `Remote account ${accountId} not fully configured (missing baseUrl or apiKey)`
3280
2993
  );
3281
- return waitUntilAbort(abortSignal);
3282
- }
3283
- if (!account.supabaseUrl || !account.supabaseKey) {
3284
- log?.warn?.(
3285
- `Remote account ${accountId} missing supabaseUrl or supabaseKey \u2014 Realtime events disabled. Agent tools still work.`
3286
- );
2994
+ return waitUntilAbort(ctx.abortSignal);
3287
2995
  }
3288
2996
  log?.info?.(
3289
- `Starting Remote channel (account: ${accountId}, url: ${account.baseUrl})`
2997
+ `Starting Remote channel (account: ${accountId}, webhookPath: ${account.webhookPath})`
3290
2998
  );
3291
- const runtime = channelRuntime ?? getRemoteRuntime().channel;
3292
- let realtimeHandle = null;
3293
- if (account.supabaseUrl && account.supabaseKey) {
2999
+ const handler = async (req) => {
3000
+ if (req.method !== "POST") {
3001
+ return { status: 405, body: { error: "Method not allowed" } };
3002
+ }
3003
+ const payload = req.body;
3004
+ if (!payload || !payload.event || !payload.task_id) {
3005
+ return { status: 400, body: { error: "Invalid payload: missing event or task_id" } };
3006
+ }
3007
+ log?.info?.(`Webhook received: ${payload.event} for task ${payload.task_id} (agent: ${payload.agent_name})`);
3008
+ const body = formatWebhookEvent(payload);
3009
+ const sessionKey = `remote:${account.accountId}:${payload.task_id}`;
3294
3010
  try {
3295
- const identity = await resolveAgentIdentity(account, log);
3296
- realtimeHandle = connectRealtime(
3297
- account,
3298
- identity,
3299
- async (event) => {
3300
- const formatted = formatEvent(event);
3301
- if (!formatted) return;
3302
- try {
3303
- const rt = getRemoteRuntime();
3304
- const currentCfg = await rt.config.loadConfig();
3305
- const sessionKey = `remote:${account.accountId}:${formatted.taskId}`;
3306
- const msgCtx = runtime.reply.finalizeInboundContext({
3307
- Body: formatted.body,
3308
- RawBody: formatted.body,
3309
- CommandBody: formatted.body,
3310
- From: `remote:${formatted.senderId ?? "board"}`,
3311
- To: `remote:${account.accountId}`,
3312
- SessionKey: sessionKey,
3313
- AccountId: account.accountId,
3314
- OriginatingChannel: CHANNEL_ID,
3315
- OriginatingTo: `remote:${formatted.taskId}`,
3316
- ChatType: formatted.isDirect ? "direct" : "group",
3317
- SenderName: formatted.senderName ?? "Remote Board",
3318
- SenderId: formatted.senderId ?? "board",
3319
- Provider: CHANNEL_ID,
3320
- Surface: CHANNEL_ID,
3321
- ConversationLabel: formatted.taskTitle ?? `Task ${formatted.taskId}`,
3322
- Timestamp: Date.now()
3323
- });
3324
- await runtime.reply.dispatchReplyWithBufferedBlockDispatcher({
3325
- ctx: msgCtx,
3326
- cfg: currentCfg,
3327
- dispatcherOptions: {
3328
- deliver: async (payload, _info) => {
3329
- if (payload.isReasoning) return;
3330
- const text = payload?.text;
3331
- if (text) {
3332
- await postComment(account, formatted.taskId, text);
3333
- }
3334
- },
3335
- onReplyStart: () => {
3336
- log?.info?.(`Agent reply started for task ${formatted.taskId}`);
3337
- }
3338
- }
3339
- });
3340
- } catch (err) {
3341
- log?.error?.(
3342
- `Error dispatching Remote event: ${err instanceof Error ? err.message : String(err)}`
3343
- );
3011
+ const rt = getRemoteRuntime();
3012
+ const currentCfg = await rt.config.loadConfig();
3013
+ const msgCtx = rt.channel.reply.finalizeInboundContext({
3014
+ Body: body,
3015
+ RawBody: body,
3016
+ CommandBody: body,
3017
+ From: `remote:${payload.task_id}`,
3018
+ To: `remote:${account.accountId}`,
3019
+ SessionKey: sessionKey,
3020
+ AccountId: account.accountId,
3021
+ OriginatingChannel: CHANNEL_ID,
3022
+ OriginatingTo: `remote:${payload.task_id}`,
3023
+ ChatType: "direct",
3024
+ SenderName: payload.author_name || "Remote Board",
3025
+ SenderId: payload.task_id,
3026
+ Provider: CHANNEL_ID,
3027
+ Surface: CHANNEL_ID,
3028
+ ConversationLabel: payload.task_title || `Task ${payload.task_id}`,
3029
+ Timestamp: Date.now()
3030
+ });
3031
+ await rt.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
3032
+ ctx: msgCtx,
3033
+ cfg: currentCfg,
3034
+ dispatcherOptions: {
3035
+ deliver: async (deliverPayload) => {
3036
+ if (deliverPayload.isReasoning) return;
3037
+ const text = deliverPayload?.text ?? deliverPayload?.body;
3038
+ if (text) {
3039
+ await postComment(account, payload.task_id, text);
3040
+ }
3041
+ },
3042
+ onReplyStart: () => {
3043
+ log?.info?.(`Agent reply started for task ${payload.task_id}`);
3344
3044
  }
3345
- },
3346
- (err) => {
3347
- log?.warn?.(`Realtime error: ${err.message}`);
3348
- },
3349
- log
3350
- );
3045
+ }
3046
+ });
3351
3047
  } catch (err) {
3352
- log?.error?.(`Failed to connect Realtime: ${err instanceof Error ? err.message : String(err)}`);
3048
+ log?.error?.(
3049
+ `Error dispatching webhook event: ${err instanceof Error ? err.message : String(err)}`
3050
+ );
3051
+ return { status: 500, body: { error: "Internal error" } };
3353
3052
  }
3053
+ return { status: 200, body: { ok: true } };
3054
+ };
3055
+ const routeKey = `${accountId}:${account.webhookPath}`;
3056
+ const prevUnregister = activeRouteUnregisters.get(routeKey);
3057
+ if (prevUnregister) {
3058
+ log?.info?.(`Deregistering stale route before re-registering: ${account.webhookPath}`);
3059
+ prevUnregister();
3060
+ activeRouteUnregisters.delete(routeKey);
3354
3061
  }
3355
- await waitUntilAbort(abortSignal, async () => {
3356
- if (realtimeHandle) {
3357
- await realtimeHandle.unsubscribe();
3358
- }
3062
+ const unregister = registerPluginHttpRoute({
3063
+ path: account.webhookPath,
3064
+ auth: "plugin",
3065
+ replaceExisting: true,
3066
+ pluginId: CHANNEL_ID,
3067
+ accountId: account.accountId,
3068
+ log: (msg) => log?.info?.(msg),
3069
+ handler
3070
+ });
3071
+ activeRouteUnregisters.set(routeKey, unregister);
3072
+ log?.info?.(`Registered HTTP route: ${account.webhookPath} for Remote`);
3073
+ return waitUntilAbort(ctx.abortSignal, () => {
3074
+ log?.info?.(`Stopping Remote channel (account: ${accountId})`);
3075
+ if (typeof unregister === "function") unregister();
3076
+ activeRouteUnregisters.delete(routeKey);
3359
3077
  });
3360
- log?.info?.(`Stopped Remote channel (account: ${accountId})`);
3361
3078
  },
3362
3079
  stopAccount: async (ctx) => {
3363
3080
  ctx.log?.info?.(`Remote account ${ctx.accountId} stopped`);
package/package.json CHANGED
@@ -1,11 +1,21 @@
1
1
  {
2
2
  "name": "openclaw-remote",
3
- "version": "0.4.5",
3
+ "version": "0.5.1",
4
4
  "description": "Remote project board channel plugin for OpenClaw",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
- "files": ["dist/", "package.json", "openclaw.plugin.json"],
8
- "keywords": ["openclaw", "remote", "channel", "plugin", "agent"],
7
+ "files": [
8
+ "dist/",
9
+ "package.json",
10
+ "openclaw.plugin.json"
11
+ ],
12
+ "keywords": [
13
+ "openclaw",
14
+ "remote",
15
+ "channel",
16
+ "plugin",
17
+ "agent"
18
+ ],
9
19
  "license": "MIT",
10
20
  "openclaw": {
11
21
  "extensions": [
@@ -14,9 +24,5 @@
14
24
  },
15
25
  "peerDependencies": {
16
26
  "openclaw": "*"
17
- },
18
- "dependencies": {
19
- "@sinclair/typebox": "^0.34.48",
20
- "@supabase/supabase-js": "^2.99.1"
21
27
  }
22
- }
28
+ }