@clawroom/openclaw 0.2.0 → 0.2.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawroom/openclaw",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "OpenClaw channel plugin for the Claw Room task marketplace",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/channel.ts CHANGED
@@ -2,7 +2,7 @@ import type { ChannelPlugin } from "openclaw/plugin-sdk/core";
2
2
  import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
3
3
  import { collectSkills } from "./skill-reporter.js";
4
4
  import { getClawroomRuntime } from "./runtime.js";
5
- import { ClawroomWsClient } from "./ws-client.js";
5
+ import { ClawroomClient } from "./client.js";
6
6
  import { setupTaskExecutor } from "./task-executor.js";
7
7
 
8
8
  // ── Config resolution ────────────────────────────────────────────────
@@ -54,9 +54,9 @@ function resolveClawroomAccount(opts: {
54
54
  };
55
55
  }
56
56
 
57
- // ── Persistent WS client per gateway lifecycle ───────────────────────
57
+ // ── Persistent client per gateway lifecycle ───────────────────────
58
58
 
59
- let activeClient: ClawroomWsClient | null = null;
59
+ let activeClient: ClawroomClient | null = null;
60
60
 
61
61
  // ── Channel plugin definition ────────────────────────────────────────
62
62
 
@@ -103,8 +103,8 @@ export const clawroomPlugin: ChannelPlugin<ResolvedClawroomAccount> = {
103
103
  deliveryMode: "direct",
104
104
 
105
105
  sendText: async ({ to, text }) => {
106
- // Outbound messages are task results sent via the WS client.
107
- // The task-executor sends results directly through ws.send();
106
+ // Outbound messages are task results sent via HTTP.
107
+ // The task-executor sends results directly through client.send();
108
108
  // this adapter exists for completeness if openclaw routing tries
109
109
  // to deliver a reply through the channel outbound path.
110
110
  if (activeClient) {
@@ -137,7 +137,7 @@ export const clawroomPlugin: ChannelPlugin<ResolvedClawroomAccount> = {
137
137
  const deviceId = resolveDeviceId(ctx);
138
138
  const log = ctx.log ?? undefined;
139
139
 
140
- const client = new ClawroomWsClient({
140
+ const client = new ClawroomClient({
141
141
  endpoint: account.endpoint,
142
142
  token: account.token,
143
143
  deviceId,
@@ -169,7 +169,7 @@ export const clawroomPlugin: ChannelPlugin<ResolvedClawroomAccount> = {
169
169
  });
170
170
  };
171
171
 
172
- // Wire up WS events to OpenClaw health status
172
+ // Wire up SSE events to OpenClaw health status
173
173
  client.onWelcome(() => publishConnected());
174
174
  client.onTask(() => {
175
175
  // Any server message = update lastEventAt so gateway knows we're alive
@@ -208,7 +208,7 @@ export const clawroomPlugin: ChannelPlugin<ResolvedClawroomAccount> = {
208
208
  });
209
209
  });
210
210
 
211
- setupTaskExecutor({ ws: client, runtime, log });
211
+ setupTaskExecutor({ client: client, runtime, log });
212
212
  client.connect();
213
213
  activeClient = client;
214
214
 
@@ -34,7 +34,7 @@ type WelcomeCallback = (agentId: string) => void;
34
34
  type FatalCallback = (reason: string) => void;
35
35
  type ModeChangeCallback = (mode: "sse" | "polling") => void;
36
36
 
37
- export type ClawroomWsClientOptions = {
37
+ export type ClawroomClientOptions = {
38
38
  endpoint: string;
39
39
  token: string;
40
40
  deviceId: string;
@@ -55,8 +55,7 @@ export type ClawroomWsClientOptions = {
55
55
  * Agent→Server actions always use HTTP POST:
56
56
  * /api/agents/heartbeat, /complete, /fail, /progress, /claim
57
57
  */
58
- export class ClawroomWsClient {
59
- private eventSource: EventSource | null = null;
58
+ export class ClawroomClient {
60
59
  private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
61
60
  private pollTimer: ReturnType<typeof setInterval> | null = null;
62
61
  private reconnectAttempt = 0;
@@ -77,9 +76,9 @@ export class ClawroomWsClient {
77
76
  private fatalCallbacks: FatalCallback[] = [];
78
77
  private modeChangeCallbacks: ModeChangeCallback[] = [];
79
78
 
80
- constructor(private readonly options: ClawroomWsClientOptions) {
79
+ constructor(private readonly options: ClawroomClientOptions) {
81
80
  // Derive HTTP base from endpoint
82
- // wss://clawroom.site9.ai/ws/agent → https://clawroom.site9.ai/api/agents
81
+ // Legacy: wss://host/ws/agent → https://clawroom.site9.ai/api/agents
83
82
  // https://clawroom.site9.ai/api/agents/stream → https://clawroom.site9.ai/api/agents
84
83
  const ep = options.endpoint;
85
84
  if (ep.includes("/api/agents")) {
@@ -116,7 +115,7 @@ export class ClawroomWsClient {
116
115
  get isAlive(): boolean { return !this.stopped; }
117
116
  get isConnected(): boolean {
118
117
  if (this.mode === "polling") return true;
119
- return this.eventSource?.readyState === EventSource.OPEN;
118
+ return this.sseAbort !== null && !this.sseAbort.signal.aborted;
120
119
  }
121
120
  get isFatal(): boolean { return false; }
122
121
  get currentMode(): "sse" | "polling" { return this.mode; }
@@ -145,53 +144,77 @@ export class ClawroomWsClient {
145
144
  if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); this.heartbeatTimer = null; }
146
145
  }
147
146
 
148
- // ── SSE Connection ──────────────────────────────────────────────
147
+ // ── SSE Connection (fetch-based, works in Node.js) ───────────────
148
+
149
+ private sseAbort: AbortController | null = null;
149
150
 
150
151
  private doConnectSSE(): void {
151
152
  this.destroySSE();
152
153
  const { token, deviceId, skills } = this.options;
153
- const params = new URLSearchParams({
154
- token,
155
- deviceId,
156
- skills: skills.join(","),
157
- });
154
+ const params = new URLSearchParams({ token, deviceId, skills: skills.join(",") });
158
155
  const url = `${this.httpBase}/stream?${params}`;
159
156
 
160
157
  this.options.log?.info?.(`[clawroom] SSE connecting to ${this.httpBase}/stream`);
161
158
 
162
- try {
163
- this.eventSource = new EventSource(url);
164
- } catch {
165
- this.triggerReconnect("EventSource creation failed");
166
- return;
167
- }
159
+ this.sseAbort = new AbortController();
160
+ const signal = this.sseAbort.signal;
161
+
162
+ fetch(url, {
163
+ headers: { "Accept": "text/event-stream", "Authorization": `Bearer ${token}` },
164
+ signal,
165
+ }).then(async (res) => {
166
+ if (!res.ok || !res.body) {
167
+ if (res.status === 401) {
168
+ this.stopped = true;
169
+ this.stopHeartbeat();
170
+ this.stopPolling();
171
+ for (const cb of this.fatalCallbacks) cb("Unauthorized");
172
+ return;
173
+ }
174
+ throw new Error(`HTTP ${res.status}`);
175
+ }
168
176
 
169
- this.eventSource.onopen = () => {
170
177
  this.options.log?.info?.("[clawroom] SSE connected");
171
178
  this.reconnectAttempt = 0;
172
179
  this.consecutiveSseFails = 0;
173
-
174
- if (this.mode === "polling") {
175
- this.switchToSSE();
180
+ if (this.mode === "polling") this.switchToSSE();
181
+
182
+ const reader = res.body.getReader();
183
+ const decoder = new TextDecoder();
184
+ let buffer = "";
185
+
186
+ while (true) {
187
+ const { done, value } = await reader.read();
188
+ if (done) break;
189
+
190
+ buffer += decoder.decode(value, { stream: true });
191
+ const lines = buffer.split("\n");
192
+ buffer = lines.pop() ?? "";
193
+
194
+ for (const line of lines) {
195
+ if (line.startsWith("data: ")) {
196
+ this.handleMessage(line.slice(6));
197
+ }
198
+ // Ignore comments (: keepalive) and empty lines
199
+ }
176
200
  }
177
- };
178
-
179
- this.eventSource.onmessage = (event) => {
180
- this.handleMessage(event.data);
181
- };
182
201
 
183
- this.eventSource.onerror = () => {
184
- this.options.log?.info?.("[clawroom] SSE error/disconnected");
185
- this.destroySSE();
202
+ // Stream ended
203
+ this.options.log?.info?.("[clawroom] SSE stream ended");
204
+ for (const cb of this.disconnectCallbacks) cb();
205
+ this.triggerReconnect("stream ended");
206
+ }).catch((err) => {
207
+ if (signal.aborted) return;
208
+ this.options.log?.warn?.(`[clawroom] SSE error: ${err}`);
186
209
  for (const cb of this.disconnectCallbacks) cb();
187
210
  this.triggerReconnect("SSE error");
188
- };
211
+ });
189
212
  }
190
213
 
191
214
  private destroySSE(): void {
192
- if (this.eventSource) {
193
- try { this.eventSource.close(); } catch {}
194
- this.eventSource = null;
215
+ if (this.sseAbort) {
216
+ try { this.sseAbort.abort(); } catch {}
217
+ this.sseAbort = null;
195
218
  }
196
219
  }
197
220
 
@@ -1,6 +1,6 @@
1
1
  import type { ServerClaimAck, ServerTask, AgentResultFile } from "@clawroom/sdk";
2
2
  import type { PluginRuntime } from "openclaw/plugin-sdk/core";
3
- import type { ClawroomWsClient } from "./ws-client.js";
3
+ import type { ClawroomClient } from "./client.js";
4
4
  import fs from "node:fs";
5
5
  import path from "node:path";
6
6
 
@@ -18,7 +18,7 @@ const MAX_FILE_SIZE = 10 * 1024 * 1024;
18
18
  * Claiming is triggered externally (by the owner via Dashboard).
19
19
  */
20
20
  export function setupTaskExecutor(opts: {
21
- ws: ClawroomWsClient;
21
+ client: ClawroomClient;
22
22
  runtime: PluginRuntime;
23
23
  log?: {
24
24
  info?: (...args: unknown[]) => void;
@@ -26,19 +26,19 @@ export function setupTaskExecutor(opts: {
26
26
  error?: (...args: unknown[]) => void;
27
27
  };
28
28
  }): void {
29
- const { ws, runtime, log } = opts;
29
+ const { client, runtime, log } = opts;
30
30
 
31
31
  // Track received tasks (from broadcast or server push after claim)
32
32
  const knownTasks = new Map<string, ServerTask>();
33
33
 
34
34
  // New task received — store it for potential execution
35
- ws.onTask((task: ServerTask) => {
35
+ client.onTask((task: ServerTask) => {
36
36
  log?.info?.(`[clawroom:executor] received task ${task.taskId}: ${task.title}`);
37
37
  knownTasks.set(task.taskId, task);
38
38
  });
39
39
 
40
40
  // Task list on connect — store all
41
- ws.onTaskList((tasks: ServerTask[]) => {
41
+ client.onTaskList((tasks: ServerTask[]) => {
42
42
  log?.info?.(`[clawroom:executor] ${tasks.length} open task(s) available`);
43
43
  for (const t of tasks) {
44
44
  log?.info?.(`[clawroom:executor] - ${t.taskId}: ${t.title}`);
@@ -47,7 +47,7 @@ export function setupTaskExecutor(opts: {
47
47
  });
48
48
 
49
49
  // Claim ack — either from plugin-initiated claim or Dashboard-initiated claim
50
- ws.onClaimAck((ack: ServerClaimAck) => {
50
+ client.onClaimAck((ack: ServerClaimAck) => {
51
51
  if (!ack.ok) {
52
52
  log?.warn?.(
53
53
  `[clawroom:executor] claim rejected for ${ack.taskId}: ${ack.reason ?? "unknown"}`,
@@ -67,7 +67,7 @@ export function setupTaskExecutor(opts: {
67
67
  }
68
68
 
69
69
  log?.info?.(`[clawroom:executor] executing task ${task.taskId}`);
70
- void executeTask({ ws, runtime, task, log });
70
+ void executeTask({ client, runtime, task, log });
71
71
  });
72
72
  }
73
73
 
@@ -76,7 +76,7 @@ export function setupTaskExecutor(opts: {
76
76
  * result back to the ClawRoom server.
77
77
  */
78
78
  async function executeTask(opts: {
79
- ws: ClawroomWsClient;
79
+ client: ClawroomClient;
80
80
  runtime: PluginRuntime;
81
81
  task: ServerTask;
82
82
  log?: {
@@ -84,7 +84,7 @@ async function executeTask(opts: {
84
84
  error?: (...args: unknown[]) => void;
85
85
  };
86
86
  }): Promise<void> {
87
- const { ws, runtime, task, log } = opts;
87
+ const { client, runtime, task, log } = opts;
88
88
  const sessionKey = `clawroom:task:${task.taskId}`;
89
89
 
90
90
  const agentMessage = buildAgentMessage(task);
@@ -116,7 +116,7 @@ async function executeTask(opts: {
116
116
  log?.error?.(
117
117
  `[clawroom:executor] subagent error for task ${task.taskId}: ${waitResult.error}`,
118
118
  );
119
- ws.send({
119
+ client.send({
120
120
  type: "agent.fail",
121
121
  taskId: task.taskId,
122
122
  reason: waitResult.error ?? "Agent execution failed",
@@ -126,7 +126,7 @@ async function executeTask(opts: {
126
126
 
127
127
  if (waitResult.status === "timeout") {
128
128
  log?.error?.(`[clawroom:executor] subagent timeout for task ${task.taskId}`);
129
- ws.send({
129
+ client.send({
130
130
  type: "agent.fail",
131
131
  taskId: task.taskId,
132
132
  reason: "Agent execution timed out",
@@ -155,7 +155,7 @@ async function executeTask(opts: {
155
155
  .trim();
156
156
 
157
157
  log?.info?.(`[clawroom:executor] task ${task.taskId} completed${files.length > 0 ? ` (with ${files.length} file(s))` : ""}`);
158
- ws.send({
158
+ client.send({
159
159
  type: "agent.complete",
160
160
  taskId: task.taskId,
161
161
  output,
@@ -169,7 +169,7 @@ async function executeTask(opts: {
169
169
  } catch (err) {
170
170
  const reason = err instanceof Error ? err.message : String(err);
171
171
  log?.error?.(`[clawroom:executor] unexpected error for task ${task.taskId}: ${reason}`);
172
- ws.send({
172
+ client.send({
173
173
  type: "agent.release",
174
174
  taskId: task.taskId,
175
175
  reason,