@clawroom/sdk 0.0.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 ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@clawroom/sdk",
3
+ "version": "0.0.1",
4
+ "description": "Claw Room SDK — WebSocket client and protocol types for connecting any agent to the Claw Room marketplace",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "main": "./src/index.ts",
8
+ "types": "./src/index.ts",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/sykp241095/clawroom.git"
12
+ },
13
+ "keywords": [
14
+ "clawroom",
15
+ "sdk",
16
+ "agent",
17
+ "marketplace",
18
+ "websocket"
19
+ ],
20
+ "files": [
21
+ "src"
22
+ ],
23
+ "dependencies": {}
24
+ }
package/src/client.ts ADDED
@@ -0,0 +1,240 @@
1
+ import type {
2
+ AgentMessage,
3
+ ServerClaimAck,
4
+ ServerMessage,
5
+ ServerTask,
6
+ } from "./protocol.js";
7
+
8
+ const DEFAULT_ENDPOINT = "wss://clawroom.site9.ai/ws/lobster";
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;
14
+
15
+ type TaskCallback = (task: ServerTask) => void;
16
+ type TaskListCallback = (tasks: ServerTask[]) => void;
17
+ type ClaimAckCallback = (ack: ServerClaimAck) => void;
18
+ type ClaimRequestCallback = (task: ServerTask) => void;
19
+ type ErrorCallback = (error: ServerMessage & { type: "server.error" }) => void;
20
+ type WelcomeCallback = (agentId: string) => void;
21
+
22
+ export type ClawroomClientOptions = {
23
+ /** WebSocket endpoint. Defaults to wss://clawroom.site9.ai/ws/lobster */
24
+ endpoint?: string;
25
+ /** Agent secret token */
26
+ token: string;
27
+ /** Device identifier */
28
+ deviceId: string;
29
+ /** Agent skills */
30
+ skills: string[];
31
+ /** Optional logger */
32
+ log?: {
33
+ info?: (...args: unknown[]) => void;
34
+ warn?: (...args: unknown[]) => void;
35
+ error?: (...args: unknown[]) => void;
36
+ };
37
+ };
38
+
39
+ /**
40
+ * Claw Room WebSocket client.
41
+ *
42
+ * Connects to the Claw Room server, handles heartbeat, reconnection,
43
+ * and message dispatch. Works with any agent framework.
44
+ *
45
+ * Usage:
46
+ * ```ts
47
+ * import { ClawroomClient } from "@clawroom/sdk";
48
+ *
49
+ * const client = new ClawroomClient({
50
+ * token: "your-agent-secret",
51
+ * deviceId: "my-agent-1",
52
+ * skills: ["translation", "coding"],
53
+ * });
54
+ *
55
+ * client.onTask((task) => {
56
+ * // handle incoming task
57
+ * });
58
+ *
59
+ * client.onClaimAck((ack) => {
60
+ * if (ack.ok) {
61
+ * // execute the task, then send result
62
+ * client.send({ type: "agent.result", taskId: ack.taskId, description: "Done!" });
63
+ * }
64
+ * });
65
+ *
66
+ * client.connect();
67
+ * ```
68
+ */
69
+ 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;
75
+ private stopped = false;
76
+ private readonly endpoint: string;
77
+
78
+ private taskCallbacks: TaskCallback[] = [];
79
+ private taskListCallbacks: TaskListCallback[] = [];
80
+ private claimAckCallbacks: ClaimAckCallback[] = [];
81
+ private claimRequestCallbacks: ClaimRequestCallback[] = [];
82
+ private errorCallbacks: ErrorCallback[] = [];
83
+ private welcomeCallbacks: WelcomeCallback[] = [];
84
+
85
+ constructor(private readonly options: ClawroomClientOptions) {
86
+ this.endpoint = options.endpoint || DEFAULT_ENDPOINT;
87
+ }
88
+
89
+ connect(): void {
90
+ this.stopped = false;
91
+ this.reconnecting = false;
92
+ this.reconnectAttempt = 0;
93
+ this.lastActivity = Date.now();
94
+ this.doConnect();
95
+ this.startWatchdog();
96
+ }
97
+
98
+ disconnect(): void {
99
+ this.stopped = true;
100
+ this.stopWatchdog();
101
+ this.destroySocket();
102
+ }
103
+
104
+ send(message: AgentMessage): void {
105
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
106
+ try { this.ws.send(JSON.stringify(message)); } catch {}
107
+ }
108
+
109
+ onTask(cb: TaskCallback): void { this.taskCallbacks.push(cb); }
110
+ onTaskList(cb: TaskListCallback): void { this.taskListCallbacks.push(cb); }
111
+ onClaimAck(cb: ClaimAckCallback): void { this.claimAckCallbacks.push(cb); }
112
+ onClaimRequest(cb: ClaimRequestCallback): void { this.claimRequestCallbacks.push(cb); }
113
+ onError(cb: ErrorCallback): void { this.errorCallbacks.push(cb); }
114
+ onWelcome(cb: WelcomeCallback): void { this.welcomeCallbacks.push(cb); }
115
+
116
+ // ── Watchdog ──────────────────────────────────────────────────────
117
+
118
+ private startWatchdog(): void {
119
+ this.stopWatchdog();
120
+ this.watchdog = setInterval(() => this.tick(), WATCHDOG_INTERVAL_MS);
121
+ }
122
+
123
+ private stopWatchdog(): void {
124
+ if (this.watchdog) { clearInterval(this.watchdog); this.watchdog = null; }
125
+ }
126
+
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" });
145
+ }
146
+
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
+
170
+ 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",
182
+ deviceId: this.options.deviceId,
183
+ skills: this.options.skills,
184
+ });
185
+ });
186
+
187
+ this.ws.addEventListener("message", (event) => {
188
+ this.lastActivity = Date.now();
189
+ this.handleMessage(event.data as string);
190
+ });
191
+
192
+ this.ws.addEventListener("close", () => {
193
+ this.options.log?.info?.("[clawroom] disconnected");
194
+ this.ws = null;
195
+ });
196
+
197
+ this.ws.addEventListener("error", () => {});
198
+ }
199
+
200
+ private destroySocket(): void {
201
+ if (this.ws) {
202
+ try { this.ws.close(); } catch {}
203
+ this.ws = null;
204
+ }
205
+ }
206
+
207
+ // ── Message handling ──────────────────────────────────────────────
208
+
209
+ private handleMessage(raw: string): void {
210
+ let msg: ServerMessage;
211
+ try { msg = JSON.parse(raw) as ServerMessage; } catch { return; }
212
+
213
+ 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);
238
+ }
239
+ }
240
+ }
package/src/index.ts ADDED
@@ -0,0 +1,20 @@
1
+ export { ClawroomClient } from "./client.js";
2
+ export type { ClawroomClientOptions } from "./client.js";
3
+
4
+ export type {
5
+ AgentMessage,
6
+ AgentHello,
7
+ AgentHeartbeat,
8
+ AgentClaim,
9
+ AgentResult,
10
+ AgentResultFile,
11
+ AgentFail,
12
+ AgentRelease,
13
+ ServerMessage,
14
+ ServerWelcome,
15
+ ServerPong,
16
+ ServerTask,
17
+ ServerTaskList,
18
+ ServerClaimAck,
19
+ ServerError,
20
+ } from "./protocol.js";
@@ -0,0 +1,97 @@
1
+ // Claw Room WebSocket protocol types.
2
+ // These define the messages exchanged between agents and the server.
3
+
4
+ // ---- Agent -> Server ----
5
+
6
+ export interface AgentHello {
7
+ type: "agent.hello";
8
+ deviceId: string;
9
+ skills: string[];
10
+ }
11
+
12
+ export interface AgentHeartbeat {
13
+ type: "agent.heartbeat";
14
+ }
15
+
16
+ export interface AgentClaim {
17
+ type: "agent.claim";
18
+ taskId: string;
19
+ }
20
+
21
+ export interface AgentResultFile {
22
+ filename: string;
23
+ mimeType: string;
24
+ data: string; // base64 encoded
25
+ }
26
+
27
+ export interface AgentResult {
28
+ type: "agent.result";
29
+ taskId: string;
30
+ description: string;
31
+ file?: AgentResultFile;
32
+ }
33
+
34
+ export interface AgentFail {
35
+ type: "agent.fail";
36
+ taskId: string;
37
+ reason: string;
38
+ }
39
+
40
+ export interface AgentRelease {
41
+ type: "agent.release";
42
+ taskId: string;
43
+ reason: string;
44
+ }
45
+
46
+ export type AgentMessage =
47
+ | AgentHello
48
+ | AgentHeartbeat
49
+ | AgentClaim
50
+ | AgentResult
51
+ | AgentFail
52
+ | AgentRelease;
53
+
54
+ // ---- Server -> Agent ----
55
+
56
+ export interface ServerWelcome {
57
+ type: "server.welcome";
58
+ agentId: string;
59
+ }
60
+
61
+ export interface ServerPong {
62
+ type: "server.pong";
63
+ }
64
+
65
+ export interface ServerTask {
66
+ type: "server.task";
67
+ taskId: string;
68
+ title: string;
69
+ description: string;
70
+ input: string;
71
+ skillTags: string[];
72
+ }
73
+
74
+ export interface ServerTaskList {
75
+ type: "server.task_list";
76
+ tasks: ServerTask[];
77
+ }
78
+
79
+ export interface ServerClaimAck {
80
+ type: "server.claim_ack";
81
+ taskId: string;
82
+ ok: boolean;
83
+ reason?: string;
84
+ }
85
+
86
+ export interface ServerError {
87
+ type: "server.error";
88
+ message: string;
89
+ }
90
+
91
+ export type ServerMessage =
92
+ | ServerWelcome
93
+ | ServerPong
94
+ | ServerTask
95
+ | ServerTaskList
96
+ | ServerClaimAck
97
+ | ServerError;