@clawroom/sdk 0.2.3 → 0.4.0

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.2.3",
4
- "description": "ClawRoom SDK — polling client and protocol types for connecting any agent to the ClawRoom marketplace",
3
+ "version": "0.4.0",
4
+ "description": "ClawRoom SDK — polling client and protocol types for connecting any agent to ClawRoom",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "main": "./src/index.ts",
@@ -14,7 +14,7 @@
14
14
  "clawroom",
15
15
  "sdk",
16
16
  "agent",
17
- "marketplace"
17
+ "workspace"
18
18
  ],
19
19
  "files": [
20
20
  "src"
package/src/client.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type {
2
2
  AgentMessage,
3
- ServerClaimAck,
4
3
  ServerTask,
4
+ ServerChatMessage,
5
5
  } from "./protocol.js";
6
6
 
7
7
  const DEFAULT_ENDPOINT = "https://clawroom.site9.ai/api/agents";
@@ -9,8 +9,8 @@ const DEFAULT_ENDPOINT = "https://clawroom.site9.ai/api/agents";
9
9
  const HEARTBEAT_INTERVAL_MS = 30_000;
10
10
  const POLL_INTERVAL_MS = 10_000;
11
11
 
12
- type TaskCallback = (task: ServerTask) => void;
13
- type ClaimAckCallback = (ack: ServerClaimAck) => void;
12
+ export type TaskCallback = (task: ServerTask) => void;
13
+ export type ChatCallback = (messages: ServerChatMessage[]) => void;
14
14
 
15
15
  export type ClawroomClientOptions = {
16
16
  /** HTTP base URL. Defaults to https://clawroom.site9.ai/api/agents */
@@ -21,6 +21,8 @@ export type ClawroomClientOptions = {
21
21
  deviceId: string;
22
22
  /** Agent skills */
23
23
  skills: string[];
24
+ /** Agent kind (e.g. "openclaw", "claude-code", "codex", "custom"). Defaults to "openclaw" */
25
+ kind?: string;
24
26
  /** Optional logger */
25
27
  log?: {
26
28
  info?: (message: string, ...args: unknown[]) => void;
@@ -32,8 +34,8 @@ export type ClawroomClientOptions = {
32
34
  /**
33
35
  * ClawRoom SDK client using HTTP polling.
34
36
  *
35
- * Agents register with /heartbeat and fetch work from /poll.
36
- * All agent actions (complete, fail, progress) use HTTP POST.
37
+ * Agents register with /heartbeat and receive assigned tasks + chat via /poll.
38
+ * All agent actions (complete, fail, progress, chat reply) use HTTP POST.
37
39
  *
38
40
  * Usage:
39
41
  * ```ts
@@ -46,12 +48,13 @@ export type ClawroomClientOptions = {
46
48
  * });
47
49
  *
48
50
  * client.onTask((task) => {
49
- * // handle incoming task
51
+ * // task was assigned to this agent — execute it
52
+ * client.send({ type: "agent.complete", taskId: task.taskId, output: "Done!" });
50
53
  * });
51
54
  *
52
- * client.onClaimAck((ack) => {
53
- * if (ack.ok) {
54
- * client.send({ type: "agent.complete", taskId: ack.taskId, output: "Done!" });
55
+ * client.onChatMessage((messages) => {
56
+ * for (const msg of messages) {
57
+ * client.send({ type: "agent.chat.reply", channelId: msg.channelId, content: "Hello!" });
55
58
  * }
56
59
  * });
57
60
  *
@@ -61,13 +64,15 @@ export type ClawroomClientOptions = {
61
64
  export class ClawroomClient {
62
65
  private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
63
66
  private pollTimer: ReturnType<typeof setInterval> | null = null;
64
- private stopped = false;
65
- private readonly httpBase: string;
67
+ protected stopped = false;
68
+ protected readonly httpBase: string;
69
+ protected readonly options: ClawroomClientOptions;
66
70
 
67
- private taskCallbacks: TaskCallback[] = [];
68
- private claimAckCallbacks: ClaimAckCallback[] = [];
71
+ protected taskCallbacks: TaskCallback[] = [];
72
+ protected chatCallbacks: ChatCallback[] = [];
69
73
 
70
- constructor(private readonly options: ClawroomClientOptions) {
74
+ constructor(options: ClawroomClientOptions) {
75
+ this.options = options;
71
76
  this.httpBase = (options.endpoint || DEFAULT_ENDPOINT).replace(/\/+$/, "");
72
77
  }
73
78
 
@@ -90,14 +95,14 @@ export class ClawroomClient {
90
95
  }
91
96
 
92
97
  onTask(cb: TaskCallback): void { this.taskCallbacks.push(cb); }
93
- onClaimAck(cb: ClaimAckCallback): void { this.claimAckCallbacks.push(cb); }
98
+ onChatMessage(cb: ChatCallback): void { this.chatCallbacks.push(cb); }
94
99
 
95
100
  // ── Heartbeat ───────────────────────────────────────────────────
96
101
 
97
102
  private startHeartbeat(): void {
98
103
  this.stopHeartbeat();
99
104
  this.heartbeatTimer = setInterval(() => {
100
- if (!this.stopped) this.httpPost("/heartbeat", {}).catch(() => {});
105
+ if (!this.stopped) void this.register();
101
106
  }, HEARTBEAT_INTERVAL_MS);
102
107
  }
103
108
 
@@ -109,49 +114,73 @@ export class ClawroomClient {
109
114
  if (this.pollTimer) { clearInterval(this.pollTimer); this.pollTimer = null; }
110
115
  }
111
116
 
112
- private async register(): Promise<void> {
117
+ protected async register(): Promise<void> {
113
118
  try {
114
- await this.httpPost("/heartbeat", {
119
+ await this.httpRequest("POST", "/heartbeat", {
115
120
  deviceId: this.options.deviceId,
116
121
  skills: this.options.skills,
122
+ kind: this.options.kind ?? "openclaw",
117
123
  });
124
+ this.onPollSuccess(undefined);
118
125
  } catch (err) {
119
126
  this.options.log?.warn?.(`[clawroom] heartbeat error: ${err}`);
127
+ this.onPollError(err);
120
128
  }
121
129
  }
122
130
 
123
- private async pollTick(): Promise<void> {
131
+ protected async pollTick(): Promise<void> {
124
132
  if (this.stopped) return;
125
133
  try {
126
- const res = await this.httpPost("/poll", {});
134
+ const res = await this.httpRequest("POST", "/poll", {});
135
+ this.onPollSuccess(res?.agentId);
136
+
127
137
  if (res.task) {
138
+ this.options.log?.info?.(`[clawroom] received task ${res.task.taskId}: ${res.task.title}`);
128
139
  for (const cb of this.taskCallbacks) cb(res.task);
129
- for (const cb of this.claimAckCallbacks) cb({ type: "server.claim_ack", taskId: res.task.taskId, ok: true });
140
+ }
141
+
142
+ if (res.chat && Array.isArray(res.chat) && res.chat.length > 0) {
143
+ this.options.log?.info?.(`[clawroom] received ${res.chat.length} chat mention(s)`);
144
+ for (const cb of this.chatCallbacks) cb(res.chat);
130
145
  }
131
146
  } catch (err) {
132
147
  this.options.log?.warn?.(`[clawroom] poll error: ${err}`);
148
+ this.onPollError(err);
133
149
  }
134
150
  }
135
151
 
152
+ /** Override in subclass for lifecycle tracking */
153
+ protected onPollSuccess(_agentId: string | undefined): void {}
154
+ /** Override in subclass for lifecycle tracking */
155
+ protected onPollError(_err: unknown): void {}
156
+
136
157
  // ── HTTP ────────────────────────────────────────────────────────
137
158
 
138
159
  private async sendViaHttp(message: AgentMessage): Promise<void> {
139
160
  switch (message.type) {
140
- case "agent.complete": await this.httpPost("/complete", { taskId: message.taskId, output: message.output, attachments: message.attachments }); break;
141
- case "agent.fail": await this.httpPost("/fail", { taskId: message.taskId, reason: message.reason }); break;
142
- case "agent.progress": await this.httpPost("/progress", { taskId: message.taskId, message: message.message, percent: message.percent }); break;
143
- case "agent.heartbeat": await this.httpPost("/heartbeat", {}); break;
144
- case "agent.claim": await this.httpPost("/claim", { taskId: message.taskId }); break;
161
+ case "agent.complete": await this.httpRequest("POST", "/complete", { taskId: message.taskId, output: message.output, attachments: message.attachments }); break;
162
+ case "agent.fail": await this.httpRequest("POST", "/fail", { taskId: message.taskId, reason: message.reason }); break;
163
+ case "agent.progress": await this.httpRequest("POST", "/progress", { taskId: message.taskId, message: message.message, percent: message.percent }); break;
164
+ case "agent.heartbeat": await this.httpRequest("POST", "/heartbeat", {}); break;
165
+ case "agent.chat.reply": await this.httpRequest("POST", "/chat/reply", { channelId: message.channelId, content: message.content, replyTo: message.replyTo }); break;
166
+ case "agent.typing": await this.httpRequest("POST", "/typing", { channelId: message.channelId }); break;
145
167
  }
146
168
  }
147
169
 
148
- private async httpPost(path: string, body: unknown): Promise<any> {
170
+ protected async httpRequest(method: string, path: string, body: unknown): Promise<any> {
149
171
  const res = await fetch(`${this.httpBase}${path}`, {
150
- method: "POST",
172
+ method,
151
173
  headers: { "Content-Type": "application/json", "Authorization": `Bearer ${this.options.token}` },
152
174
  body: JSON.stringify(body),
153
175
  });
154
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
176
+ if (!res.ok) {
177
+ const text = await res.text().catch(() => "");
178
+ this.onHttpError(res.status, text);
179
+ throw new Error(`HTTP ${res.status}: ${text}`);
180
+ }
155
181
  return res.json();
156
182
  }
183
+
184
+ /** Override in subclass for error handling (e.g. 401 auto-stop) */
185
+ protected onHttpError(_status: number, _text: string): void {}
157
186
  }
package/src/index.ts CHANGED
@@ -1,15 +1,18 @@
1
1
  export { ClawroomClient } from "./client.js";
2
2
  export type { ClawroomClientOptions } from "./client.js";
3
+ export { ClawroomMachineClient } from "./machine-client.js";
4
+ export type { ClawroomMachineClientOptions } from "./machine-client.js";
3
5
 
4
6
  export type {
5
7
  AgentMessage,
6
8
  AgentHeartbeat,
7
- AgentClaim,
8
9
  AgentComplete,
9
10
  AgentProgress,
10
11
  AgentResultFile,
11
12
  AgentFail,
13
+ AgentChatReply,
14
+ AgentTyping,
12
15
  ServerMessage,
13
16
  ServerTask,
14
- ServerClaimAck,
17
+ ServerChatMessage,
15
18
  } from "./protocol.js";
@@ -0,0 +1,127 @@
1
+ /**
2
+ * ClawRoom Machine Client — machine-level auth with multi-agent support.
3
+ */
4
+
5
+ import * as os from "node:os";
6
+
7
+ export type ClawroomMachineClientOptions = {
8
+ endpoint?: string;
9
+ apiKey: string;
10
+ hostname?: string;
11
+ capabilities?: string[];
12
+ log?: {
13
+ info?: (message: string, ...args: unknown[]) => void;
14
+ warn?: (message: string, ...args: unknown[]) => void;
15
+ error?: (message: string, ...args: unknown[]) => void;
16
+ };
17
+ };
18
+
19
+ type AgentWork = {
20
+ agentId: string;
21
+ agentName: string;
22
+ task: { type: "server.task"; taskId: string; title: string; description: string; input: string; skillTags: string[] } | null;
23
+ chat: Array<{ messageId: string; channelId: string; content: string; isMention: boolean; context: unknown[] }> | null;
24
+ };
25
+
26
+ export class ClawroomMachineClient {
27
+ private options: ClawroomMachineClientOptions;
28
+ private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
29
+ private pollTimer: ReturnType<typeof setInterval> | null = null;
30
+ private taskHandler: ((agentId: string, task: AgentWork["task"]) => void) | null = null;
31
+ private chatHandler: ((agentId: string, messages: NonNullable<AgentWork["chat"]>) => void) | null = null;
32
+ private _connected = false;
33
+ private _stopped = false;
34
+
35
+ constructor(options: ClawroomMachineClientOptions) {
36
+ this.options = options;
37
+ }
38
+
39
+ private get baseUrl(): string {
40
+ return this.options.endpoint ?? "http://localhost:3000/api/machines";
41
+ }
42
+
43
+ private async httpRequest(method: string, path: string, body?: unknown): Promise<unknown> {
44
+ const url = `${this.baseUrl}${path}`;
45
+ const res = await fetch(url, {
46
+ method,
47
+ headers: {
48
+ "Content-Type": "application/json",
49
+ Authorization: `Bearer ${this.options.apiKey}`,
50
+ },
51
+ body: body ? JSON.stringify(body) : undefined,
52
+ });
53
+ if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
54
+ return res.json();
55
+ }
56
+
57
+ onAgentTask(handler: (agentId: string, task: AgentWork["task"]) => void) {
58
+ this.taskHandler = handler;
59
+ }
60
+
61
+ onAgentChat(handler: (agentId: string, messages: NonNullable<AgentWork["chat"]>) => void) {
62
+ this.chatHandler = handler;
63
+ }
64
+
65
+ async sendAgentComplete(agentId: string, taskId: string, output: string) {
66
+ // Use agent-level API via machine auth proxy (or direct)
67
+ await this.httpRequest("POST", "/complete", { agentId, taskId, output });
68
+ }
69
+
70
+ async sendAgentFail(agentId: string, taskId: string, reason: string) {
71
+ await this.httpRequest("POST", "/fail", { agentId, taskId, reason });
72
+ }
73
+
74
+ async sendAgentChatReply(agentId: string, channelId: string, content: string) {
75
+ await this.httpRequest("POST", "/chat-reply", { agentId, channelId, content });
76
+ }
77
+
78
+ async sendAgentTyping(agentId: string, channelId: string) {
79
+ await this.httpRequest("POST", "/typing", { agentId, channelId });
80
+ }
81
+
82
+ get connected() { return this._connected; }
83
+ get stopped() { return this._stopped; }
84
+
85
+ connect() {
86
+ this._stopped = false;
87
+ const hostname = this.options.hostname ?? os.hostname();
88
+ const hbBody = { hostname, capabilities: this.options.capabilities };
89
+
90
+ // Heartbeat every 30s
91
+ this.heartbeatTimer = setInterval(async () => {
92
+ try {
93
+ await this.httpRequest("POST", "/heartbeat", hbBody);
94
+ if (!this._connected) { this._connected = true; this.options.log?.info?.("[machine] connected"); }
95
+ } catch (err) {
96
+ if (this._connected) { this._connected = false; this.options.log?.warn?.("[machine] disconnected"); }
97
+ this.options.log?.warn?.(`[machine] heartbeat error: ${err}`);
98
+ }
99
+ }, 30_000);
100
+
101
+ // Initial heartbeat
102
+ this.httpRequest("POST", "/heartbeat", hbBody)
103
+ .then(() => { this._connected = true; this.options.log?.info?.("[machine] connected"); })
104
+ .catch((err) => this.options.log?.warn?.(`[machine] initial heartbeat failed: ${err}`));
105
+
106
+ // Poll every 10s
107
+ this.pollTimer = setInterval(async () => {
108
+ if (!this._connected) return;
109
+ try {
110
+ const result = (await this.httpRequest("POST", "/poll", {})) as { machineId: string; agents: AgentWork[] };
111
+ for (const agent of result.agents) {
112
+ if (agent.task && this.taskHandler) this.taskHandler(agent.agentId, agent.task);
113
+ if (agent.chat && agent.chat.length > 0 && this.chatHandler) this.chatHandler(agent.agentId, agent.chat);
114
+ }
115
+ } catch (err) {
116
+ this.options.log?.warn?.(`[machine] poll error: ${err}`);
117
+ }
118
+ }, 10_000);
119
+ }
120
+
121
+ disconnect() {
122
+ this._stopped = true;
123
+ this._connected = false;
124
+ if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
125
+ if (this.pollTimer) clearInterval(this.pollTimer);
126
+ }
127
+ }
package/src/protocol.ts CHANGED
@@ -7,11 +7,6 @@ export interface AgentHeartbeat {
7
7
  type: "agent.heartbeat";
8
8
  }
9
9
 
10
- export interface AgentClaim {
11
- type: "agent.claim";
12
- taskId: string;
13
- }
14
-
15
10
  export interface AgentResultFile {
16
11
  filename: string;
17
12
  mimeType: string;
@@ -38,12 +33,25 @@ export interface AgentFail {
38
33
  reason: string;
39
34
  }
40
35
 
36
+ export interface AgentChatReply {
37
+ type: "agent.chat.reply";
38
+ channelId: string;
39
+ content: string;
40
+ replyTo?: string;
41
+ }
42
+
43
+ export interface AgentTyping {
44
+ type: "agent.typing";
45
+ channelId: string;
46
+ }
47
+
41
48
  export type AgentMessage =
42
49
  | AgentHeartbeat
43
- | AgentClaim
44
50
  | AgentComplete
45
51
  | AgentProgress
46
- | AgentFail;
52
+ | AgentFail
53
+ | AgentChatReply
54
+ | AgentTyping;
47
55
 
48
56
  // ---- Server -> Agent ----
49
57
 
@@ -56,13 +64,20 @@ export interface ServerTask {
56
64
  skillTags: string[];
57
65
  }
58
66
 
59
- export interface ServerClaimAck {
60
- type: "server.claim_ack";
61
- taskId: string;
62
- ok: boolean;
63
- reason?: string;
67
+ export interface ServerChatMessage {
68
+ messageId: string;
69
+ channelId: string;
70
+ content: string;
71
+ isMention: boolean;
72
+ context: Array<{
73
+ id: string;
74
+ senderType: string;
75
+ senderName: string;
76
+ content: string;
77
+ createdAt: number;
78
+ }>;
64
79
  }
65
80
 
66
81
  export type ServerMessage =
67
82
  | ServerTask
68
- | ServerClaimAck;
83
+ | ServerChatMessage;