@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 +3 -3
- package/src/client.ts +58 -29
- package/src/index.ts +5 -2
- package/src/machine-client.ts +127 -0
- package/src/protocol.ts +28 -13
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clawroom/sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "ClawRoom SDK — polling client and protocol types for connecting any agent to
|
|
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
|
-
"
|
|
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
|
|
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
|
|
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
|
-
* //
|
|
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.
|
|
53
|
-
*
|
|
54
|
-
* client.send({ type: "agent.
|
|
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
|
-
|
|
65
|
-
|
|
67
|
+
protected stopped = false;
|
|
68
|
+
protected readonly httpBase: string;
|
|
69
|
+
protected readonly options: ClawroomClientOptions;
|
|
66
70
|
|
|
67
|
-
|
|
68
|
-
|
|
71
|
+
protected taskCallbacks: TaskCallback[] = [];
|
|
72
|
+
protected chatCallbacks: ChatCallback[] = [];
|
|
69
73
|
|
|
70
|
-
constructor(
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
117
|
+
protected async register(): Promise<void> {
|
|
113
118
|
try {
|
|
114
|
-
await this.
|
|
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
|
-
|
|
131
|
+
protected async pollTick(): Promise<void> {
|
|
124
132
|
if (this.stopped) return;
|
|
125
133
|
try {
|
|
126
|
-
const res = await this.
|
|
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
|
-
|
|
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.
|
|
141
|
-
case "agent.fail": await this.
|
|
142
|
-
case "agent.progress": await this.
|
|
143
|
-
case "agent.heartbeat": await this.
|
|
144
|
-
case "agent.
|
|
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
|
-
|
|
170
|
+
protected async httpRequest(method: string, path: string, body: unknown): Promise<any> {
|
|
149
171
|
const res = await fetch(`${this.httpBase}${path}`, {
|
|
150
|
-
method
|
|
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)
|
|
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
|
-
|
|
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
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
|
83
|
+
| ServerChatMessage;
|