@clawroom/sdk 0.1.0 → 0.2.2

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,7 +1,7 @@
1
1
  {
2
2
  "name": "@clawroom/sdk",
3
- "version": "0.1.0",
4
- "description": "Claw Room SDK — WebSocket client and protocol types for connecting any agent to the Claw Room marketplace",
3
+ "version": "0.2.2",
4
+ "description": "Claw Room SDK — polling client and protocol types for connecting any agent to the Claw Room marketplace",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "main": "./src/index.ts",
package/src/client.ts CHANGED
@@ -5,22 +5,18 @@ import type {
5
5
  ServerTask,
6
6
  } from "./protocol.js";
7
7
 
8
- const DEFAULT_ENDPOINT = "wss://clawroom.site9.ai/ws/lobster";
8
+ const DEFAULT_ENDPOINT = "https://clawroom.site9.ai/api/agents";
9
9
 
10
- const WATCHDOG_INTERVAL_MS = 10_000;
11
- const DEAD_THRESHOLD_MS = 25_000;
12
- const RECONNECT_BASE_MS = 1_000;
13
- const RECONNECT_MAX_MS = 30_000;
10
+ const HEARTBEAT_INTERVAL_MS = 30_000;
11
+ const POLL_INTERVAL_MS = 10_000;
14
12
 
15
13
  type TaskCallback = (task: ServerTask) => void;
16
14
  type TaskListCallback = (tasks: ServerTask[]) => void;
17
15
  type ClaimAckCallback = (ack: ServerClaimAck) => void;
18
- type ClaimRequestCallback = (task: ServerTask) => void;
19
16
  type ErrorCallback = (error: ServerMessage & { type: "server.error" }) => void;
20
- type WelcomeCallback = (agentId: string) => void;
21
17
 
22
18
  export type ClawroomClientOptions = {
23
- /** WebSocket endpoint. Defaults to wss://clawroom.site9.ai/ws/lobster */
19
+ /** HTTP base URL. Defaults to https://clawroom.site9.ai/api/agents */
24
20
  endpoint?: string;
25
21
  /** Agent secret token */
26
22
  token: string;
@@ -37,10 +33,10 @@ export type ClawroomClientOptions = {
37
33
  };
38
34
 
39
35
  /**
40
- * Claw Room WebSocket client.
36
+ * Claw Room SDK client using HTTP polling.
41
37
  *
42
- * Connects to the Claw Room server, handles heartbeat, reconnection,
43
- * and message dispatch. Works with any agent framework.
38
+ * Agents register with /heartbeat and fetch work from /poll.
39
+ * All agent actions (complete, fail, progress) use HTTP POST.
44
40
  *
45
41
  * Usage:
46
42
  * ```ts
@@ -58,7 +54,6 @@ export type ClawroomClientOptions = {
58
54
  *
59
55
  * client.onClaimAck((ack) => {
60
56
  * if (ack.ok) {
61
- * // execute the task, then send complete
62
57
  * client.send({ type: "agent.complete", taskId: ack.taskId, output: "Done!" });
63
58
  * }
64
59
  * });
@@ -67,141 +62,104 @@ export type ClawroomClientOptions = {
67
62
  * ```
68
63
  */
69
64
  export class ClawroomClient {
70
- private ws: WebSocket | null = null;
71
- private watchdog: ReturnType<typeof setInterval> | null = null;
72
- private lastActivity = 0;
73
- private reconnectAttempt = 0;
74
- private reconnecting = false;
65
+ private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
66
+ private pollTimer: ReturnType<typeof setInterval> | null = null;
75
67
  private stopped = false;
76
- private readonly endpoint: string;
68
+ private readonly httpBase: string;
77
69
 
78
70
  private taskCallbacks: TaskCallback[] = [];
79
71
  private taskListCallbacks: TaskListCallback[] = [];
80
72
  private claimAckCallbacks: ClaimAckCallback[] = [];
81
- private claimRequestCallbacks: ClaimRequestCallback[] = [];
82
73
  private errorCallbacks: ErrorCallback[] = [];
83
- private welcomeCallbacks: WelcomeCallback[] = [];
84
74
 
85
75
  constructor(private readonly options: ClawroomClientOptions) {
86
- this.endpoint = options.endpoint || DEFAULT_ENDPOINT;
76
+ this.httpBase = (options.endpoint || DEFAULT_ENDPOINT).replace(/\/stream\/?$/, "").replace(/\/+$/, "");
87
77
  }
88
78
 
89
79
  connect(): void {
90
80
  this.stopped = false;
91
- this.reconnecting = false;
92
- this.reconnectAttempt = 0;
93
- this.lastActivity = Date.now();
94
- this.doConnect();
95
- this.startWatchdog();
81
+ this.startHeartbeat();
82
+ this.stopPolling();
83
+ this.pollTimer = setInterval(() => void this.pollTick(), POLL_INTERVAL_MS);
84
+ void this.register();
96
85
  }
97
86
 
98
87
  disconnect(): void {
99
88
  this.stopped = true;
100
- this.stopWatchdog();
101
- this.destroySocket();
89
+ this.stopHeartbeat();
90
+ this.stopPolling();
102
91
  }
103
92
 
104
93
  send(message: AgentMessage): void {
105
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
106
- try { this.ws.send(JSON.stringify(message)); } catch {}
94
+ this.sendViaHttp(message).catch(() => {});
107
95
  }
108
96
 
109
97
  onTask(cb: TaskCallback): void { this.taskCallbacks.push(cb); }
110
98
  onTaskList(cb: TaskListCallback): void { this.taskListCallbacks.push(cb); }
111
99
  onClaimAck(cb: ClaimAckCallback): void { this.claimAckCallbacks.push(cb); }
112
- onClaimRequest(cb: ClaimRequestCallback): void { this.claimRequestCallbacks.push(cb); }
113
100
  onError(cb: ErrorCallback): void { this.errorCallbacks.push(cb); }
114
- onWelcome(cb: WelcomeCallback): void { this.welcomeCallbacks.push(cb); }
115
101
 
116
- // ── Watchdog ──────────────────────────────────────────────────────
102
+ // ── Heartbeat ───────────────────────────────────────────────────
117
103
 
118
- private startWatchdog(): void {
119
- this.stopWatchdog();
120
- this.watchdog = setInterval(() => this.tick(), WATCHDOG_INTERVAL_MS);
104
+ private startHeartbeat(): void {
105
+ this.stopHeartbeat();
106
+ this.heartbeatTimer = setInterval(() => {
107
+ if (!this.stopped) this.httpPost("/heartbeat", {}).catch(() => {});
108
+ }, HEARTBEAT_INTERVAL_MS);
121
109
  }
122
110
 
123
- private stopWatchdog(): void {
124
- if (this.watchdog) { clearInterval(this.watchdog); this.watchdog = null; }
111
+ private stopHeartbeat(): void {
112
+ if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); this.heartbeatTimer = null; }
125
113
  }
126
114
 
127
- private tick(): void {
128
- if (this.stopped || this.reconnecting) return;
129
-
130
- const ws = this.ws;
131
- if (!ws || ws.readyState === WebSocket.CLOSED || ws.readyState === WebSocket.CLOSING) {
132
- this.triggerReconnect("no socket");
133
- return;
134
- }
135
- if (ws.readyState === WebSocket.CONNECTING) return;
136
-
137
- const elapsed = Date.now() - this.lastActivity;
138
- if (elapsed > DEAD_THRESHOLD_MS) {
139
- this.options.log?.warn?.(`[clawroom] watchdog: no response for ${Math.round(elapsed / 1000)}s, forcing reconnect`);
140
- this.triggerReconnect("dead connection");
141
- return;
142
- }
143
-
144
- this.send({ type: "agent.heartbeat" });
115
+ private stopPolling(): void {
116
+ if (this.pollTimer) { clearInterval(this.pollTimer); this.pollTimer = null; }
145
117
  }
146
118
 
147
- private triggerReconnect(reason: string): void {
148
- if (this.reconnecting || this.stopped) return;
149
- this.reconnecting = true;
150
- this.destroySocket();
151
-
152
- const delayMs = Math.min(RECONNECT_BASE_MS * 2 ** this.reconnectAttempt, RECONNECT_MAX_MS);
153
- this.reconnectAttempt++;
154
- this.options.log?.info?.(`[clawroom] reconnecting in ${delayMs}ms (${reason}, attempt ${this.reconnectAttempt})`);
155
-
156
- setTimeout(() => {
157
- this.reconnecting = false;
158
- if (!this.stopped) this.doConnect();
159
- }, delayMs);
160
- }
161
-
162
- // ── Connection ────────────────────────────────────────────────────
163
-
164
- private doConnect(): void {
165
- this.destroySocket();
166
- const url = `${this.endpoint}?token=${encodeURIComponent(this.options.token)}`;
167
-
168
- this.options.log?.info?.(`[clawroom] connecting to ${this.endpoint}`);
169
-
119
+ private async register(): Promise<void> {
170
120
  try {
171
- this.ws = new WebSocket(url);
172
- } catch {
173
- return;
174
- }
175
-
176
- this.ws.addEventListener("open", () => {
177
- this.options.log?.info?.("[clawroom] connected");
178
- this.reconnectAttempt = 0;
179
- this.lastActivity = Date.now();
180
- this.send({
181
- type: "agent.hello",
121
+ await this.httpPost("/heartbeat", {
182
122
  deviceId: this.options.deviceId,
183
123
  skills: this.options.skills,
184
124
  });
185
- });
125
+ } catch (err) {
126
+ this.options.log?.warn?.(`[clawroom] heartbeat error: ${err}`);
127
+ }
128
+ }
186
129
 
187
- this.ws.addEventListener("message", (event) => {
188
- this.lastActivity = Date.now();
189
- this.handleMessage(event.data as string);
190
- });
130
+ private async pollTick(): Promise<void> {
131
+ if (this.stopped) return;
132
+ try {
133
+ const res = await this.httpPost("/poll", {});
134
+ if (res.task) {
135
+ for (const cb of this.taskCallbacks) cb(res.task);
136
+ for (const cb of this.claimAckCallbacks) cb({ type: "server.claim_ack", taskId: res.task.taskId, ok: true });
137
+ }
138
+ } catch (err) {
139
+ this.options.log?.warn?.(`[clawroom] poll error: ${err}`);
140
+ }
141
+ }
191
142
 
192
- this.ws.addEventListener("close", () => {
193
- this.options.log?.info?.("[clawroom] disconnected");
194
- this.ws = null;
195
- });
143
+ // ── HTTP ────────────────────────────────────────────────────────
196
144
 
197
- this.ws.addEventListener("error", () => {});
145
+ private async sendViaHttp(message: AgentMessage): Promise<void> {
146
+ switch (message.type) {
147
+ case "agent.complete": await this.httpPost("/complete", { taskId: message.taskId, output: message.output, attachments: message.attachments }); break;
148
+ case "agent.fail": await this.httpPost("/fail", { taskId: message.taskId, reason: message.reason }); break;
149
+ case "agent.progress": await this.httpPost("/progress", { taskId: message.taskId, message: message.message, percent: message.percent }); break;
150
+ case "agent.heartbeat": await this.httpPost("/heartbeat", {}); break;
151
+ case "agent.claim": await this.httpPost("/claim", { taskId: message.taskId }); break;
152
+ }
198
153
  }
199
154
 
200
- private destroySocket(): void {
201
- if (this.ws) {
202
- try { this.ws.close(); } catch {}
203
- this.ws = null;
204
- }
155
+ private async httpPost(path: string, body: unknown): Promise<any> {
156
+ const res = await fetch(`${this.httpBase}${path}`, {
157
+ method: "POST",
158
+ headers: { "Content-Type": "application/json", "Authorization": `Bearer ${this.options.token}` },
159
+ body: JSON.stringify(body),
160
+ });
161
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
162
+ return res.json();
205
163
  }
206
164
 
207
165
  // ── Message handling ──────────────────────────────────────────────
@@ -209,32 +167,13 @@ export class ClawroomClient {
209
167
  private handleMessage(raw: string): void {
210
168
  let msg: ServerMessage;
211
169
  try { msg = JSON.parse(raw) as ServerMessage; } catch { return; }
212
-
213
170
  switch (msg.type) {
214
- case "server.welcome":
215
- this.options.log?.info?.(`[clawroom] welcome, agentId=${msg.agentId}`);
216
- for (const cb of this.welcomeCallbacks) cb(msg.agentId);
217
- break;
218
- case "server.pong":
219
- break;
220
- case "server.task":
221
- this.options.log?.info?.(`[clawroom] received task ${msg.taskId}: ${msg.title}`);
222
- for (const cb of this.taskCallbacks) cb(msg);
223
- break;
224
- case "server.task_list":
225
- this.options.log?.info?.(`[clawroom] received ${msg.tasks.length} open task(s)`);
226
- for (const cb of this.taskListCallbacks) cb(msg.tasks);
227
- break;
228
- case "server.claim_ack":
229
- this.options.log?.info?.(`[clawroom] claim_ack taskId=${msg.taskId} ok=${msg.ok}${msg.reason ? ` reason=${msg.reason}` : ""}`);
230
- for (const cb of this.claimAckCallbacks) cb(msg);
231
- break;
232
- case "server.error":
233
- this.options.log?.error?.(`[clawroom] server error: ${msg.message}`);
234
- for (const cb of this.errorCallbacks) cb(msg);
235
- break;
236
- default:
237
- this.options.log?.warn?.("[clawroom] unknown message type", msg);
171
+ case "server.welcome": this.options.log?.info?.(`[clawroom] welcome, agentId=${msg.agentId}`); break;
172
+ case "server.pong": break;
173
+ case "server.task": for (const cb of this.taskCallbacks) cb(msg); break;
174
+ case "server.task_list": for (const cb of this.taskListCallbacks) cb(msg.tasks); break;
175
+ case "server.claim_ack": for (const cb of this.claimAckCallbacks) cb(msg); break;
176
+ case "server.error": for (const cb of this.errorCallbacks) cb(msg); break;
238
177
  }
239
178
  }
240
179
  }
package/src/protocol.ts CHANGED
@@ -1,4 +1,4 @@
1
- // Claw Room WebSocket protocol types.
1
+ // Claw Room protocol types.
2
2
  // These define the messages exchanged between agents and the server.
3
3
 
4
4
  // ---- Agent -> Server ----