@clawroom/sdk 0.5.25 → 0.5.26

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/src/client.ts DELETED
@@ -1,430 +0,0 @@
1
- import type {
2
- AgentChatProfile,
3
- AgentResultFile,
4
- AgentMessage,
5
- AgentWorkRef,
6
- ServerTask,
7
- ServerChatMessage,
8
- } from "./protocol.js";
9
- import { WsTransport } from "./ws-transport.js";
10
-
11
- const DEFAULT_ENDPOINT = "https://clawroom.site9.ai/api/agents";
12
- const HEARTBEAT_INTERVAL_MS = 30_000;
13
- const POLL_INTERVAL_MS = 10_000;
14
- const DEFAULT_AGENT_CHAT_PROFILE: AgentChatProfile = {
15
- role: "No role defined",
16
- systemPrompt: "No system prompt configured",
17
- memory: "No memory recorded yet",
18
- continuityPacket: "No continuity packet available yet",
19
- };
20
-
21
- export type TaskCallback = (task: ServerTask) => void;
22
- export type ChatCallback = (messages: ServerChatMessage[]) => void;
23
- export type ConnectedCallback = (agentId: string) => void;
24
- export type DisconnectedCallback = () => void;
25
- export type Unsubscribe = () => void;
26
-
27
- export type ClawroomClientOptions = {
28
- endpoint?: string;
29
- token: string;
30
- deviceId: string;
31
- skills: string[];
32
- kind?: string;
33
- wsUrl?: string;
34
- log?: {
35
- info?: (message: string, ...args: unknown[]) => void;
36
- warn?: (message: string, ...args: unknown[]) => void;
37
- error?: (message: string, ...args: unknown[]) => void;
38
- };
39
- };
40
-
41
- type AgentHeartbeatResponse = {
42
- ok?: boolean;
43
- agentId?: string;
44
- };
45
-
46
- type AgentPollResponse = {
47
- agentId?: string;
48
- task: ServerTask | null;
49
- chat: ServerChatMessage[] | null;
50
- };
51
-
52
- /**
53
- * ClawRoom SDK client.
54
- * WebSocket primary for real-time push, HTTP polling as fallback.
55
- */
56
- export class ClawroomClient {
57
- private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
58
- private pollTimer: ReturnType<typeof setInterval> | null = null;
59
- private heartbeatInFlight = false;
60
- private pollInFlight = false;
61
- protected stopped = false;
62
- protected readonly httpBase: string;
63
- protected readonly options: ClawroomClientOptions;
64
- protected taskCallbacks: TaskCallback[] = [];
65
- protected chatCallbacks: ChatCallback[] = [];
66
- private connectedCallbacks: ConnectedCallback[] = [];
67
- private disconnectedCallbacks: DisconnectedCallback[] = [];
68
- private wsTransport: WsTransport | null = null;
69
- private recentChatIds = new Set<string>();
70
- private connectedAgentId: string | null = null;
71
- private isConnectedValue = false;
72
-
73
- constructor(options: ClawroomClientOptions) {
74
- this.options = options;
75
- this.httpBase = (options.endpoint || DEFAULT_ENDPOINT).replace(/\/+$/, "");
76
- }
77
-
78
- private get wsUrl(): string {
79
- if (this.options.wsUrl) return this.options.wsUrl;
80
- return this.httpBase.replace(/\/api\/agents$/, "").replace(/^http/, "ws") + "/api/ws";
81
- }
82
-
83
- get connected(): boolean {
84
- return this.isConnectedValue;
85
- }
86
-
87
- get agentId(): string | null {
88
- return this.connectedAgentId;
89
- }
90
-
91
- get isAlive(): boolean {
92
- return !this.stopped;
93
- }
94
-
95
- connect(): void {
96
- this.stopped = false;
97
- this.startHeartbeat();
98
- void this.register();
99
-
100
- // WebSocket transport
101
- this.wsTransport?.disconnect();
102
- this.wsTransport = null;
103
- this.wsTransport = new WsTransport({
104
- url: this.wsUrl,
105
- token: this.options.token,
106
- log: { info: (m) => this.options.log?.info?.(m), warn: (m) => this.options.log?.warn?.(m) },
107
- onConnected: () => { this.options.log?.info?.("[clawroom] WebSocket connected"); },
108
- onDisconnected: () => { this.options.log?.info?.("[clawroom] WebSocket disconnected"); },
109
- onMessage: (msg) => {
110
- if (msg.type === "task" && msg.task) {
111
- const task = msg.task as ServerTask;
112
- void this.dispatchTask(task);
113
- }
114
- if (msg.type === "chat" && Array.isArray(msg.messages)) {
115
- const fresh = msg.messages.filter((m: ServerChatMessage) => this.rememberChat(m.workId ?? m.messageId));
116
- if (fresh.length > 0) {
117
- for (const cb of this.chatCallbacks) cb(fresh);
118
- }
119
- }
120
- const message = getRecord(msg.message);
121
- if (msg.type === "message" && message) {
122
- const agentProfile = resolveAgentChatProfile(msg.agentProfile, message.agentProfile);
123
- const delivered = [{
124
- workId: getString(message.workId),
125
- leaseToken: getString(message.leaseToken),
126
- messageId: getString(message.id, getString(msg.messageId)),
127
- channelId: getString(message.channelId),
128
- content: getString(message.content),
129
- attachments: getAttachments(msg.attachments) ?? getAttachments(message.attachments),
130
- context: getContext(msg.context),
131
- isMention: typeof msg.isMention === "boolean" ? msg.isMention : false,
132
- wakeReason: getWakeReason(msg.wakeReason) ?? getWakeReason(message.wakeReason),
133
- triggerReason: getOptionalString(msg.triggerReason) ?? getOptionalString(message.triggerReason),
134
- agentProfile,
135
- }] as ServerChatMessage[];
136
- const fresh = delivered.filter((m) => this.rememberChat(m.workId ?? m.messageId));
137
- if (fresh.length > 0) {
138
- for (const cb of this.chatCallbacks) cb(fresh);
139
- }
140
- }
141
- },
142
- });
143
- this.wsTransport.connect();
144
-
145
- // Keep HTTP polling active even when WS is up.
146
- // WS is the fast path; polling is the durable delivery path.
147
- this.stopPolling();
148
- this.pollTimer = setInterval(() => {
149
- void this.pollTick();
150
- }, POLL_INTERVAL_MS);
151
-
152
- void this.pollTick();
153
- }
154
-
155
- disconnect(): void {
156
- this.stopped = true;
157
- this.stopHeartbeat();
158
- this.stopPolling();
159
- this.wsTransport?.disconnect();
160
- this.wsTransport = null;
161
- this.recentChatIds.clear();
162
- this.markDisconnected();
163
- this.connectedAgentId = null;
164
- }
165
-
166
- async send(message: AgentMessage): Promise<void> {
167
- await this.sendViaHttp(message);
168
- }
169
-
170
- onTask(cb: TaskCallback): Unsubscribe {
171
- this.taskCallbacks.push(cb);
172
- return () => this.offTask(cb);
173
- }
174
-
175
- offTask(cb: TaskCallback): void {
176
- removeCallback(this.taskCallbacks, cb);
177
- }
178
-
179
- onChatMessage(cb: ChatCallback): Unsubscribe {
180
- this.chatCallbacks.push(cb);
181
- return () => this.offChatMessage(cb);
182
- }
183
-
184
- offChatMessage(cb: ChatCallback): void {
185
- removeCallback(this.chatCallbacks, cb);
186
- }
187
-
188
- onConnected(cb: ConnectedCallback): Unsubscribe {
189
- this.connectedCallbacks.push(cb);
190
- return () => this.offConnected(cb);
191
- }
192
-
193
- offConnected(cb: ConnectedCallback): void {
194
- removeCallback(this.connectedCallbacks, cb);
195
- }
196
-
197
- onDisconnected(cb: DisconnectedCallback): Unsubscribe {
198
- this.disconnectedCallbacks.push(cb);
199
- return () => this.offDisconnected(cb);
200
- }
201
-
202
- offDisconnected(cb: DisconnectedCallback): void {
203
- removeCallback(this.disconnectedCallbacks, cb);
204
- }
205
-
206
- async sendComplete(taskId: string, output: string, attachments?: AgentResultFile[], workRef?: AgentWorkRef): Promise<void> {
207
- await this.send({ type: "agent.complete", taskId, output, attachments, workRef });
208
- }
209
-
210
- async sendFail(taskId: string, reason: string, workRef?: AgentWorkRef): Promise<void> {
211
- await this.send({ type: "agent.fail", taskId, reason, workRef });
212
- }
213
-
214
- async sendProgress(taskId: string, message: string, workRef?: AgentWorkRef): Promise<void> {
215
- await this.send({ type: "agent.progress", taskId, message, workRef });
216
- }
217
-
218
- async sendChatReply(channelId: string, content: string, replyTo?: string, workRefs?: AgentWorkRef[]): Promise<void> {
219
- await this.send({ type: "agent.chat.reply", channelId, content, replyTo, workRefs });
220
- }
221
-
222
- async sendTyping(channelId: string): Promise<void> {
223
- await this.send({ type: "agent.typing", channelId });
224
- }
225
-
226
- async sendReflection(reflection: {
227
- scope: "chat" | "task";
228
- status: string;
229
- summary: string;
230
- channelId?: string | null;
231
- taskId?: string | null;
232
- messageId?: string | null;
233
- toolsUsed?: string[];
234
- responseExcerpt?: string | null;
235
- detail?: Record<string, unknown>;
236
- }): Promise<void> {
237
- await this.httpRequest("POST", "/reflections", reflection);
238
- }
239
-
240
- // ── Heartbeat ─────────────────────────────────────────
241
-
242
- private startHeartbeat(): void {
243
- this.stopHeartbeat();
244
- this.heartbeatTimer = setInterval(() => {
245
- if (!this.stopped) void this.register();
246
- }, HEARTBEAT_INTERVAL_MS);
247
- }
248
-
249
- private stopHeartbeat(): void {
250
- if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); this.heartbeatTimer = null; }
251
- }
252
-
253
- private stopPolling(): void {
254
- if (this.pollTimer) { clearInterval(this.pollTimer); this.pollTimer = null; }
255
- }
256
-
257
- protected async register(): Promise<void> {
258
- if (this.heartbeatInFlight) return;
259
- this.heartbeatInFlight = true;
260
- try {
261
- const response = await this.httpRequest("POST", "/heartbeat", {
262
- deviceId: this.options.deviceId,
263
- skills: this.options.skills,
264
- kind: this.options.kind ?? "openclaw",
265
- }) as AgentHeartbeatResponse;
266
- this.onPollSuccess(response.agentId);
267
- } catch (err) {
268
- this.options.log?.warn?.(`[clawroom] heartbeat error: ${err}`);
269
- if (!this.wsTransport?.connected) {
270
- this.onPollError(err);
271
- }
272
- } finally {
273
- this.heartbeatInFlight = false;
274
- }
275
- }
276
-
277
- protected async pollTick(): Promise<void> {
278
- if (this.stopped || this.pollInFlight) return;
279
- this.pollInFlight = true;
280
- try {
281
- const res = await this.httpRequest("POST", "/poll", {}) as AgentPollResponse;
282
- this.onPollSuccess(res?.agentId);
283
- if (res.task) {
284
- await this.dispatchTask(res.task);
285
- }
286
- if (res.chat && Array.isArray(res.chat) && res.chat.length > 0) {
287
- const fresh = res.chat.filter((m: ServerChatMessage) => this.rememberChat(m.workId ?? m.messageId));
288
- if (fresh.length > 0) {
289
- this.options.log?.info?.(`[clawroom] received ${fresh.length} chat mention(s)`);
290
- for (const cb of this.chatCallbacks) cb(fresh);
291
- }
292
- }
293
- } catch (err) {
294
- this.options.log?.warn?.(`[clawroom] poll error: ${err}`);
295
- this.onPollError(err);
296
- } finally {
297
- this.pollInFlight = false;
298
- }
299
- }
300
-
301
- protected onPollSuccess(agentId: string | undefined): void {
302
- this.markConnected(agentId);
303
- }
304
-
305
- protected onPollError(err: unknown): void {
306
- void err;
307
- if (this.wsTransport?.connected) return;
308
- this.markDisconnected();
309
- }
310
-
311
- // ── HTTP ──────────────────────────────────────────────
312
-
313
- private async sendViaHttp(message: AgentMessage): Promise<void> {
314
- switch (message.type) {
315
- case "agent.complete": await this.httpRequest("POST", "/complete", { taskId: message.taskId, output: message.output, attachments: message.attachments, workRef: message.workRef }); break;
316
- case "agent.fail": await this.httpRequest("POST", "/fail", { taskId: message.taskId, reason: message.reason, workRef: message.workRef }); break;
317
- case "agent.progress": await this.httpRequest("POST", "/progress", { taskId: message.taskId, message: message.message, workRef: message.workRef }); break;
318
- case "agent.heartbeat": await this.httpRequest("POST", "/heartbeat", {}); break;
319
- case "agent.chat.reply": await this.httpRequest("POST", "/chat/reply", {
320
- channelId: message.channelId,
321
- content: message.content,
322
- replyTo: message.replyTo,
323
- workRefs: message.workRefs,
324
- }); break;
325
- case "agent.typing": await this.httpRequest("POST", "/typing", { channelId: message.channelId }); break;
326
- }
327
- }
328
-
329
- protected async httpRequest(method: string, path: string, body: unknown): Promise<unknown> {
330
- const res = await fetch(`${this.httpBase}${path}`, {
331
- method,
332
- headers: { "Content-Type": "application/json", "Authorization": `Bearer ${this.options.token}` },
333
- body: JSON.stringify(body),
334
- });
335
- const text = await res.text().catch(() => "");
336
- if (!res.ok) {
337
- this.onHttpError(res.status, text);
338
- throw new Error(`HTTP ${res.status}: ${text}`);
339
- }
340
- if (!text) return null;
341
- try {
342
- return JSON.parse(text) as unknown;
343
- } catch {
344
- throw new Error(`Invalid JSON response from ${path}`);
345
- }
346
- }
347
-
348
- protected onHttpError(status: number, text: string): void {
349
- void status;
350
- void text;
351
- }
352
-
353
- private rememberChat(messageId?: string): boolean {
354
- if (!messageId) return false;
355
- if (this.recentChatIds.has(messageId)) return false;
356
- this.recentChatIds.add(messageId);
357
- if (this.recentChatIds.size > 1000) {
358
- const oldest = this.recentChatIds.values().next().value;
359
- if (oldest) this.recentChatIds.delete(oldest);
360
- }
361
- return true;
362
- }
363
-
364
- private async dispatchTask(task: ServerTask): Promise<void> {
365
- this.options.log?.info?.(`[clawroom] received task ${task.taskId}: ${task.title}`);
366
- for (const cb of this.taskCallbacks) cb(task);
367
- }
368
-
369
- private markConnected(agentId?: string): void {
370
- if (agentId) this.connectedAgentId = agentId;
371
- if (!this.connectedAgentId || this.isConnectedValue) return;
372
- this.isConnectedValue = true;
373
- for (const callback of this.connectedCallbacks) callback(this.connectedAgentId);
374
- }
375
-
376
- private markDisconnected(): void {
377
- if (!this.isConnectedValue) return;
378
- this.isConnectedValue = false;
379
- for (const callback of this.disconnectedCallbacks) callback();
380
- }
381
- }
382
-
383
- function removeCallback<T>(callbacks: T[], callback: T): void {
384
- const index = callbacks.indexOf(callback);
385
- if (index !== -1) callbacks.splice(index, 1);
386
- }
387
-
388
- function getRecord(value: unknown): Record<string, unknown> | null {
389
- if (!value || typeof value !== "object" || Array.isArray(value)) return null;
390
- return value as Record<string, unknown>;
391
- }
392
-
393
- function getString(value: unknown, fallback = ""): string {
394
- return typeof value === "string" ? value : fallback;
395
- }
396
-
397
- function getOptionalString(value: unknown): string | undefined {
398
- return typeof value === "string" ? value : undefined;
399
- }
400
-
401
- function getAttachments(value: unknown): ServerChatMessage["attachments"] | undefined {
402
- return Array.isArray(value) ? value as ServerChatMessage["attachments"] : undefined;
403
- }
404
-
405
- function getContext(value: unknown): ServerChatMessage["context"] {
406
- return Array.isArray(value) ? value as ServerChatMessage["context"] : [];
407
- }
408
-
409
- function getWakeReason(value: unknown): ServerChatMessage["wakeReason"] | undefined {
410
- return typeof value === "string" ? value as ServerChatMessage["wakeReason"] : undefined;
411
- }
412
-
413
- function getAgentChatProfile(value: unknown): AgentChatProfile | null {
414
- const record = getRecord(value);
415
- if (!record) return null;
416
- return {
417
- role: getString(record.role, DEFAULT_AGENT_CHAT_PROFILE.role),
418
- systemPrompt: getString(record.systemPrompt, DEFAULT_AGENT_CHAT_PROFILE.systemPrompt),
419
- memory: getString(record.memory, DEFAULT_AGENT_CHAT_PROFILE.memory),
420
- continuityPacket: getString(record.continuityPacket, DEFAULT_AGENT_CHAT_PROFILE.continuityPacket),
421
- };
422
- }
423
-
424
- function resolveAgentChatProfile(...values: unknown[]): AgentChatProfile {
425
- for (const value of values) {
426
- const profile = getAgentChatProfile(value);
427
- if (profile) return profile;
428
- }
429
- return DEFAULT_AGENT_CHAT_PROFILE;
430
- }