@swarmroom/sdk 0.1.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/dist/client.d.ts +44 -0
- package/dist/client.js +258 -0
- package/dist/discovery.d.ts +5 -0
- package/dist/discovery.js +50 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/types.d.ts +46 -0
- package/dist/types.js +1 -0
- package/package.json +35 -0
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Agent } from '@swarmroom/shared';
|
|
2
|
+
import type { SwarmRoomClientOptions, MessageHandler, QueryHandler, StatusChangeHandler, SendMessageOptions } from './types.js';
|
|
3
|
+
export declare class SwarmRoomClient {
|
|
4
|
+
private readonly hubUrl;
|
|
5
|
+
private readonly _agentName;
|
|
6
|
+
private readonly agentCardPayload?;
|
|
7
|
+
private readonly autoHeartbeat;
|
|
8
|
+
private readonly autoReconnect;
|
|
9
|
+
private readonly heartbeatInterval;
|
|
10
|
+
private readonly reconnectDelay;
|
|
11
|
+
private ws;
|
|
12
|
+
private heartbeatTimer;
|
|
13
|
+
private reconnectTimer;
|
|
14
|
+
private reconnectAttempts;
|
|
15
|
+
private intentionalDisconnect;
|
|
16
|
+
private _agentId;
|
|
17
|
+
private _status;
|
|
18
|
+
private messageHandlers;
|
|
19
|
+
private queryHandlers;
|
|
20
|
+
private statusChangeHandlers;
|
|
21
|
+
get isConnected(): boolean;
|
|
22
|
+
get agentId(): string | null;
|
|
23
|
+
get agentName(): string;
|
|
24
|
+
constructor(options: SwarmRoomClientOptions);
|
|
25
|
+
connect(): Promise<void>;
|
|
26
|
+
disconnect(): Promise<void>;
|
|
27
|
+
sendMessage(opts: SendMessageOptions): Promise<unknown>;
|
|
28
|
+
listAgents(): Promise<Agent[]>;
|
|
29
|
+
getAgent(id: string): Promise<Agent>;
|
|
30
|
+
onMessage(handler: MessageHandler): () => void;
|
|
31
|
+
onQuery(handler: QueryHandler): () => void;
|
|
32
|
+
onStatusChange(handler: StatusChangeHandler): () => void;
|
|
33
|
+
private registerAgent;
|
|
34
|
+
private connectWebSocket;
|
|
35
|
+
private sendWsRegister;
|
|
36
|
+
private handleWsMessage;
|
|
37
|
+
private dispatchMessage;
|
|
38
|
+
private startHeartbeat;
|
|
39
|
+
private stopHeartbeat;
|
|
40
|
+
private scheduleReconnect;
|
|
41
|
+
private clearReconnectTimer;
|
|
42
|
+
private setStatus;
|
|
43
|
+
private httpRequest;
|
|
44
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import WebSocket from 'ws';
|
|
2
|
+
import { HEARTBEAT_INTERVAL_MS, WS_RECONNECT_DELAY_MS, } from '@swarmroom/shared';
|
|
3
|
+
const MAX_RECONNECT_DELAY_MS = 30_000;
|
|
4
|
+
export class SwarmRoomClient {
|
|
5
|
+
hubUrl;
|
|
6
|
+
_agentName;
|
|
7
|
+
agentCardPayload;
|
|
8
|
+
autoHeartbeat;
|
|
9
|
+
autoReconnect;
|
|
10
|
+
heartbeatInterval;
|
|
11
|
+
reconnectDelay;
|
|
12
|
+
ws = null;
|
|
13
|
+
heartbeatTimer = null;
|
|
14
|
+
reconnectTimer = null;
|
|
15
|
+
reconnectAttempts = 0;
|
|
16
|
+
intentionalDisconnect = false;
|
|
17
|
+
_agentId = null;
|
|
18
|
+
_status = 'disconnected';
|
|
19
|
+
messageHandlers = [];
|
|
20
|
+
queryHandlers = [];
|
|
21
|
+
statusChangeHandlers = [];
|
|
22
|
+
get isConnected() {
|
|
23
|
+
return this._status === 'connected';
|
|
24
|
+
}
|
|
25
|
+
get agentId() {
|
|
26
|
+
return this._agentId;
|
|
27
|
+
}
|
|
28
|
+
get agentName() {
|
|
29
|
+
return this._agentName;
|
|
30
|
+
}
|
|
31
|
+
constructor(options) {
|
|
32
|
+
this.hubUrl = options.hubUrl.replace(/\/+$/, '');
|
|
33
|
+
this._agentName = options.name;
|
|
34
|
+
this.agentCardPayload = options.agentCard;
|
|
35
|
+
this.autoHeartbeat = options.autoHeartbeat ?? true;
|
|
36
|
+
this.autoReconnect = options.autoReconnect ?? true;
|
|
37
|
+
this.heartbeatInterval = options.heartbeatInterval ?? HEARTBEAT_INTERVAL_MS;
|
|
38
|
+
this.reconnectDelay = options.reconnectDelay ?? WS_RECONNECT_DELAY_MS;
|
|
39
|
+
}
|
|
40
|
+
async connect() {
|
|
41
|
+
this.intentionalDisconnect = false;
|
|
42
|
+
this.setStatus('connecting');
|
|
43
|
+
const agent = await this.registerAgent();
|
|
44
|
+
this._agentId = agent.id;
|
|
45
|
+
this.connectWebSocket();
|
|
46
|
+
this.startHeartbeat();
|
|
47
|
+
}
|
|
48
|
+
async disconnect() {
|
|
49
|
+
this.intentionalDisconnect = true;
|
|
50
|
+
this.stopHeartbeat();
|
|
51
|
+
this.clearReconnectTimer();
|
|
52
|
+
if (this._agentId) {
|
|
53
|
+
try {
|
|
54
|
+
await this.httpRequest('DELETE', `/api/agents/${this._agentId}`);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (this.ws) {
|
|
60
|
+
this.ws.close(1000, 'client disconnect');
|
|
61
|
+
this.ws = null;
|
|
62
|
+
}
|
|
63
|
+
this._agentId = null;
|
|
64
|
+
this.setStatus('disconnected');
|
|
65
|
+
}
|
|
66
|
+
async sendMessage(opts) {
|
|
67
|
+
if (!this._agentId) {
|
|
68
|
+
throw new Error('Client not connected. Call connect() first.');
|
|
69
|
+
}
|
|
70
|
+
const body = {
|
|
71
|
+
from: this._agentId,
|
|
72
|
+
to: opts.to,
|
|
73
|
+
senderType: 'agent',
|
|
74
|
+
content: opts.content,
|
|
75
|
+
type: opts.type ?? 'notification',
|
|
76
|
+
};
|
|
77
|
+
return this.httpRequest('POST', '/api/messages', body);
|
|
78
|
+
}
|
|
79
|
+
async listAgents() {
|
|
80
|
+
const response = await this.httpRequest('GET', '/api/agents');
|
|
81
|
+
return response.data;
|
|
82
|
+
}
|
|
83
|
+
async getAgent(id) {
|
|
84
|
+
const response = await this.httpRequest('GET', `/api/agents/${id}`);
|
|
85
|
+
return response.data;
|
|
86
|
+
}
|
|
87
|
+
onMessage(handler) {
|
|
88
|
+
this.messageHandlers.push(handler);
|
|
89
|
+
return () => {
|
|
90
|
+
this.messageHandlers = this.messageHandlers.filter((h) => h !== handler);
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
onQuery(handler) {
|
|
94
|
+
this.queryHandlers.push(handler);
|
|
95
|
+
return () => {
|
|
96
|
+
this.queryHandlers = this.queryHandlers.filter((h) => h !== handler);
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
onStatusChange(handler) {
|
|
100
|
+
this.statusChangeHandlers.push(handler);
|
|
101
|
+
return () => {
|
|
102
|
+
this.statusChangeHandlers = this.statusChangeHandlers.filter((h) => h !== handler);
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
async registerAgent() {
|
|
106
|
+
const body = { name: this._agentName };
|
|
107
|
+
if (this.agentCardPayload) {
|
|
108
|
+
body.agentCard = this.agentCardPayload;
|
|
109
|
+
}
|
|
110
|
+
const response = await this.httpRequest('POST', '/api/agents', body);
|
|
111
|
+
return response.data;
|
|
112
|
+
}
|
|
113
|
+
connectWebSocket() {
|
|
114
|
+
const wsUrl = this.hubUrl.replace(/^http/, 'ws') + '/ws';
|
|
115
|
+
this.ws = new WebSocket(wsUrl);
|
|
116
|
+
this.ws.on('open', () => {
|
|
117
|
+
this.reconnectAttempts = 0;
|
|
118
|
+
this.sendWsRegister();
|
|
119
|
+
this.setStatus('connected');
|
|
120
|
+
});
|
|
121
|
+
this.ws.on('message', (data) => {
|
|
122
|
+
this.handleWsMessage(String(data));
|
|
123
|
+
});
|
|
124
|
+
this.ws.on('close', () => {
|
|
125
|
+
if (!this.intentionalDisconnect && this.autoReconnect) {
|
|
126
|
+
this.scheduleReconnect();
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
this.ws.on('error', () => {
|
|
130
|
+
// Error is followed by close event, which handles reconnection
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
sendWsRegister() {
|
|
134
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN || !this._agentId)
|
|
135
|
+
return;
|
|
136
|
+
const msg = {
|
|
137
|
+
type: 'register',
|
|
138
|
+
payload: { agentId: this._agentId },
|
|
139
|
+
timestamp: new Date().toISOString(),
|
|
140
|
+
};
|
|
141
|
+
this.ws.send(JSON.stringify(msg));
|
|
142
|
+
}
|
|
143
|
+
handleWsMessage(raw) {
|
|
144
|
+
let msg;
|
|
145
|
+
try {
|
|
146
|
+
msg = JSON.parse(raw);
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
switch (msg.type) {
|
|
152
|
+
case 'message': {
|
|
153
|
+
const message = msg.payload;
|
|
154
|
+
this.dispatchMessage(message);
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
case 'heartbeat': {
|
|
158
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
159
|
+
const pong = {
|
|
160
|
+
type: 'heartbeat',
|
|
161
|
+
payload: { pong: true },
|
|
162
|
+
timestamp: new Date().toISOString(),
|
|
163
|
+
};
|
|
164
|
+
this.ws.send(JSON.stringify(pong));
|
|
165
|
+
}
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
case 'register':
|
|
169
|
+
case 'agent_online':
|
|
170
|
+
case 'agent_offline':
|
|
171
|
+
case 'error':
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
dispatchMessage(message) {
|
|
176
|
+
if (message.type === 'query' && this.queryHandlers.length > 0) {
|
|
177
|
+
const handler = this.queryHandlers[0];
|
|
178
|
+
handler(message)
|
|
179
|
+
.then((responseContent) => {
|
|
180
|
+
this.sendMessage({
|
|
181
|
+
to: message.from,
|
|
182
|
+
content: responseContent,
|
|
183
|
+
type: 'response',
|
|
184
|
+
}).catch(() => { });
|
|
185
|
+
})
|
|
186
|
+
.catch(() => { });
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
for (const handler of this.messageHandlers) {
|
|
190
|
+
handler(message);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
startHeartbeat() {
|
|
194
|
+
if (!this.autoHeartbeat)
|
|
195
|
+
return;
|
|
196
|
+
this.stopHeartbeat();
|
|
197
|
+
this.heartbeatTimer = setInterval(async () => {
|
|
198
|
+
if (!this._agentId)
|
|
199
|
+
return;
|
|
200
|
+
try {
|
|
201
|
+
await this.httpRequest('POST', `/api/agents/${this._agentId}/heartbeat`, {});
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
}
|
|
205
|
+
}, this.heartbeatInterval);
|
|
206
|
+
}
|
|
207
|
+
stopHeartbeat() {
|
|
208
|
+
if (this.heartbeatTimer) {
|
|
209
|
+
clearInterval(this.heartbeatTimer);
|
|
210
|
+
this.heartbeatTimer = null;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
scheduleReconnect() {
|
|
214
|
+
this.clearReconnectTimer();
|
|
215
|
+
this.setStatus('reconnecting');
|
|
216
|
+
const delay = Math.min(this.reconnectDelay * Math.pow(2, this.reconnectAttempts), MAX_RECONNECT_DELAY_MS);
|
|
217
|
+
this.reconnectAttempts++;
|
|
218
|
+
this.reconnectTimer = setTimeout(() => {
|
|
219
|
+
this.connectWebSocket();
|
|
220
|
+
}, delay);
|
|
221
|
+
}
|
|
222
|
+
clearReconnectTimer() {
|
|
223
|
+
if (this.reconnectTimer) {
|
|
224
|
+
clearTimeout(this.reconnectTimer);
|
|
225
|
+
this.reconnectTimer = null;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
setStatus(status) {
|
|
229
|
+
if (this._status === status)
|
|
230
|
+
return;
|
|
231
|
+
this._status = status;
|
|
232
|
+
for (const handler of this.statusChangeHandlers) {
|
|
233
|
+
handler(status);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
async httpRequest(method, path, body) {
|
|
237
|
+
const url = `${this.hubUrl}${path}`;
|
|
238
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
239
|
+
const init = { method, headers };
|
|
240
|
+
if (body !== undefined) {
|
|
241
|
+
init.body = JSON.stringify(body);
|
|
242
|
+
}
|
|
243
|
+
const response = await fetch(url, init);
|
|
244
|
+
if (!response.ok) {
|
|
245
|
+
let errorMessage = `HTTP ${response.status} ${response.statusText}`;
|
|
246
|
+
try {
|
|
247
|
+
const errorBody = (await response.json());
|
|
248
|
+
if (errorBody.error?.message) {
|
|
249
|
+
errorMessage = errorBody.error.message;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
}
|
|
254
|
+
throw new Error(errorMessage);
|
|
255
|
+
}
|
|
256
|
+
return (await response.json());
|
|
257
|
+
}
|
|
258
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { MDNS_SERVICE_TYPE, DEFAULT_PORT } from '@swarmroom/shared';
|
|
2
|
+
/**
|
|
3
|
+
* Discover a SwarmRoom hub via mDNS (_swarmroom._tcp).
|
|
4
|
+
* Requires `@homebridge/ciao` as optional peer dependency — returns null if unavailable.
|
|
5
|
+
*/
|
|
6
|
+
export async function discoverHub(timeoutMs = 5000) {
|
|
7
|
+
let ciao;
|
|
8
|
+
try {
|
|
9
|
+
ciao = await import('@homebridge/ciao');
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
return new Promise((resolve) => {
|
|
15
|
+
let resolved = false;
|
|
16
|
+
const done = (result) => {
|
|
17
|
+
if (resolved)
|
|
18
|
+
return;
|
|
19
|
+
resolved = true;
|
|
20
|
+
clearTimeout(timer);
|
|
21
|
+
resolve(result);
|
|
22
|
+
};
|
|
23
|
+
const responder = ciao.getResponder();
|
|
24
|
+
const timer = setTimeout(() => {
|
|
25
|
+
responder.shutdown();
|
|
26
|
+
done(null);
|
|
27
|
+
}, timeoutMs);
|
|
28
|
+
try {
|
|
29
|
+
const service = responder.createService({
|
|
30
|
+
name: 'swarmroom-browser',
|
|
31
|
+
type: MDNS_SERVICE_TYPE,
|
|
32
|
+
port: DEFAULT_PORT,
|
|
33
|
+
});
|
|
34
|
+
service.on('name-change', () => {
|
|
35
|
+
const port = service.port;
|
|
36
|
+
service.destroy();
|
|
37
|
+
responder.shutdown();
|
|
38
|
+
done(`http://localhost:${port}`);
|
|
39
|
+
});
|
|
40
|
+
service.advertise().catch(() => {
|
|
41
|
+
responder.shutdown();
|
|
42
|
+
done(null);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
responder.shutdown();
|
|
47
|
+
done(null);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { SwarmRoomClient } from './client.js';
|
|
2
|
+
export { discoverHub } from './discovery.js';
|
|
3
|
+
export type { SwarmRoomClientOptions, ConnectionStatus, MessageHandler, QueryHandler, StatusChangeHandler, IncomingMessage, SendMessageOptions, Agent, AgentStatus, AgentCard, Skill, Message, MessageType, SenderType, WSMessage, WSMessageType, } from './types.js';
|
package/dist/index.js
ADDED
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export type { Agent, AgentStatus, AgentCard, Skill, RegisterAgentRequest, HeartbeatRequest, Message, MessageType, SenderType, SendMessageRequest, WSMessage, WSMessageType, ApiResponse, } from '@swarmroom/shared';
|
|
2
|
+
export interface SwarmRoomClientOptions {
|
|
3
|
+
/** Hub URL, e.g. 'http://localhost:3000' */
|
|
4
|
+
hubUrl: string;
|
|
5
|
+
/** Agent name for registration */
|
|
6
|
+
name: string;
|
|
7
|
+
/** Optional agent card metadata */
|
|
8
|
+
agentCard?: {
|
|
9
|
+
description?: string;
|
|
10
|
+
skills?: Array<{
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
description: string;
|
|
14
|
+
tags: string[];
|
|
15
|
+
}>;
|
|
16
|
+
};
|
|
17
|
+
/** Send heartbeats automatically (default: true) */
|
|
18
|
+
autoHeartbeat?: boolean;
|
|
19
|
+
/** Reconnect WebSocket on close (default: true) */
|
|
20
|
+
autoReconnect?: boolean;
|
|
21
|
+
/** Heartbeat interval in ms (default: 30000) */
|
|
22
|
+
heartbeatInterval?: number;
|
|
23
|
+
/** Initial reconnect delay in ms (default: 3000) */
|
|
24
|
+
reconnectDelay?: number;
|
|
25
|
+
}
|
|
26
|
+
export type ConnectionStatus = 'disconnected' | 'connecting' | 'connected' | 'reconnecting';
|
|
27
|
+
export type MessageHandler = (message: IncomingMessage) => void;
|
|
28
|
+
export type QueryHandler = (query: IncomingMessage) => Promise<string>;
|
|
29
|
+
export type StatusChangeHandler = (status: ConnectionStatus) => void;
|
|
30
|
+
export interface IncomingMessage {
|
|
31
|
+
id: string;
|
|
32
|
+
from: string;
|
|
33
|
+
to: string;
|
|
34
|
+
senderType: string;
|
|
35
|
+
content: string;
|
|
36
|
+
type: string;
|
|
37
|
+
replyTo?: string;
|
|
38
|
+
metadata?: Record<string, unknown>;
|
|
39
|
+
read: boolean;
|
|
40
|
+
createdAt: string;
|
|
41
|
+
}
|
|
42
|
+
export interface SendMessageOptions {
|
|
43
|
+
to: string;
|
|
44
|
+
content: string;
|
|
45
|
+
type?: string;
|
|
46
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@swarmroom/sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "TypeScript SDK for SwarmRoom - connect AI agents to the SwarmRoom hub",
|
|
6
|
+
"keywords": ["swarmroom", "sdk", "agent", "mcp", "ai", "client"],
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"publishConfig": {
|
|
9
|
+
"access": "public"
|
|
10
|
+
},
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "https://github.com/visual-z/swarm.git",
|
|
14
|
+
"directory": "packages/sdk"
|
|
15
|
+
},
|
|
16
|
+
"main": "dist/index.js",
|
|
17
|
+
"types": "dist/index.d.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": "./dist/index.js",
|
|
20
|
+
"./dist/*": "./dist/*"
|
|
21
|
+
},
|
|
22
|
+
"files": ["dist", "README.md", "LICENSE"],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsc",
|
|
25
|
+
"dev": "tsc --watch",
|
|
26
|
+
"prepublishOnly": "npm run build"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@swarmroom/shared": "^0.1.0",
|
|
30
|
+
"ws": "^8.19.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/ws": "^8.18.1"
|
|
34
|
+
}
|
|
35
|
+
}
|