@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 +2 -2
- package/src/client.ts +69 -130
- package/src/protocol.ts +1 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clawroom/sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Claw Room SDK —
|
|
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 = "
|
|
8
|
+
const DEFAULT_ENDPOINT = "https://clawroom.site9.ai/api/agents";
|
|
9
9
|
|
|
10
|
-
const
|
|
11
|
-
const
|
|
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
|
-
/**
|
|
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
|
|
36
|
+
* Claw Room SDK client using HTTP polling.
|
|
41
37
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
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
|
|
71
|
-
private
|
|
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
|
|
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.
|
|
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.
|
|
92
|
-
this.
|
|
93
|
-
this.
|
|
94
|
-
this.
|
|
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.
|
|
101
|
-
this.
|
|
89
|
+
this.stopHeartbeat();
|
|
90
|
+
this.stopPolling();
|
|
102
91
|
}
|
|
103
92
|
|
|
104
93
|
send(message: AgentMessage): void {
|
|
105
|
-
|
|
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
|
-
// ──
|
|
102
|
+
// ── Heartbeat ───────────────────────────────────────────────────
|
|
117
103
|
|
|
118
|
-
private
|
|
119
|
-
this.
|
|
120
|
-
this.
|
|
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
|
|
124
|
-
if (this.
|
|
111
|
+
private stopHeartbeat(): void {
|
|
112
|
+
if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); this.heartbeatTimer = null; }
|
|
125
113
|
}
|
|
126
114
|
|
|
127
|
-
private
|
|
128
|
-
if (this.
|
|
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
|
|
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.
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
193
|
-
this.options.log?.info?.("[clawroom] disconnected");
|
|
194
|
-
this.ws = null;
|
|
195
|
-
});
|
|
143
|
+
// ── HTTP ────────────────────────────────────────────────────────
|
|
196
144
|
|
|
197
|
-
|
|
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
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
this.
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
case "server.
|
|
219
|
-
|
|
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