@botcord/openclaw-plugin 0.0.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/README.md +161 -0
- package/index.ts +64 -0
- package/openclaw.plugin.json +55 -0
- package/package.json +61 -0
- package/skills/botcord/SKILL.md +322 -0
- package/src/channel.ts +446 -0
- package/src/client.ts +616 -0
- package/src/commands/healthcheck.ts +143 -0
- package/src/commands/register.ts +302 -0
- package/src/commands/token.ts +43 -0
- package/src/config.ts +92 -0
- package/src/credentials.ts +113 -0
- package/src/crypto.ts +155 -0
- package/src/inbound.ts +305 -0
- package/src/poller.ts +70 -0
- package/src/runtime.ts +25 -0
- package/src/session-key.ts +59 -0
- package/src/tools/account.ts +94 -0
- package/src/tools/contacts.ts +120 -0
- package/src/tools/directory.ts +89 -0
- package/src/tools/messaging.ts +234 -0
- package/src/tools/notify.ts +55 -0
- package/src/tools/rooms.ts +177 -0
- package/src/tools/topics.ts +106 -0
- package/src/tools/wallet.ts +208 -0
- package/src/topic-tracker.ts +204 -0
- package/src/types.ts +203 -0
- package/src/ws-client.ts +187 -0
package/src/client.ts
ADDED
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP client for BotCord Hub REST API.
|
|
3
|
+
* Handles JWT token lifecycle and request signing.
|
|
4
|
+
*/
|
|
5
|
+
import { randomBytes, randomUUID } from "node:crypto";
|
|
6
|
+
import { buildSignedEnvelope, signChallenge } from "./crypto.js";
|
|
7
|
+
import type {
|
|
8
|
+
BotCordAccountConfig,
|
|
9
|
+
BotCordMessageEnvelope,
|
|
10
|
+
InboxPollResponse,
|
|
11
|
+
SendResponse,
|
|
12
|
+
RoomInfo,
|
|
13
|
+
AgentInfo,
|
|
14
|
+
ContactInfo,
|
|
15
|
+
ContactRequestInfo,
|
|
16
|
+
FileUploadResponse,
|
|
17
|
+
MessageAttachment,
|
|
18
|
+
WalletSummary,
|
|
19
|
+
WalletTransaction,
|
|
20
|
+
WalletLedgerResponse,
|
|
21
|
+
TopupResponse,
|
|
22
|
+
WithdrawalResponse,
|
|
23
|
+
} from "./types.js";
|
|
24
|
+
|
|
25
|
+
const MAX_RETRIES = 2;
|
|
26
|
+
const RETRY_BASE_MS = 1000;
|
|
27
|
+
|
|
28
|
+
export class BotCordClient {
|
|
29
|
+
private hubUrl: string;
|
|
30
|
+
private agentId: string;
|
|
31
|
+
private keyId: string;
|
|
32
|
+
private privateKey: string;
|
|
33
|
+
private jwtToken: string | null = null;
|
|
34
|
+
private tokenExpiresAt = 0;
|
|
35
|
+
|
|
36
|
+
constructor(config: BotCordAccountConfig) {
|
|
37
|
+
if (!config.hubUrl || !config.agentId || !config.keyId || !config.privateKey) {
|
|
38
|
+
throw new Error("BotCord client requires hubUrl, agentId, keyId, and privateKey");
|
|
39
|
+
}
|
|
40
|
+
this.hubUrl = config.hubUrl.replace(/\/$/, "");
|
|
41
|
+
this.agentId = config.agentId;
|
|
42
|
+
this.keyId = config.keyId;
|
|
43
|
+
this.privateKey = config.privateKey;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ── Token management ──────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
async ensureToken(): Promise<string> {
|
|
49
|
+
if (this.jwtToken && Date.now() / 1000 < this.tokenExpiresAt - 60) {
|
|
50
|
+
return this.jwtToken;
|
|
51
|
+
}
|
|
52
|
+
return this.refreshToken();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private async refreshToken(): Promise<string> {
|
|
56
|
+
// POST /registry/agents/{id}/token/refresh with nonce signature
|
|
57
|
+
// Generate a random 32-byte nonce as base64 (matches Hub expectation)
|
|
58
|
+
const nonce = randomBytes(32).toString("base64");
|
|
59
|
+
const sig = signChallenge(this.privateKey, nonce);
|
|
60
|
+
|
|
61
|
+
const resp = await fetch(`${this.hubUrl}/registry/agents/${this.agentId}/token/refresh`, {
|
|
62
|
+
method: "POST",
|
|
63
|
+
headers: { "Content-Type": "application/json" },
|
|
64
|
+
body: JSON.stringify({
|
|
65
|
+
key_id: this.keyId,
|
|
66
|
+
nonce,
|
|
67
|
+
sig,
|
|
68
|
+
}),
|
|
69
|
+
signal: AbortSignal.timeout(10000),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
if (!resp.ok) {
|
|
73
|
+
const body = await resp.text().catch(() => "");
|
|
74
|
+
throw new Error(`Token refresh failed: ${resp.status} ${body}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const data = (await resp.json()) as { agent_token: string; token?: string; expires_at?: number };
|
|
78
|
+
this.jwtToken = data.agent_token || data.token!;
|
|
79
|
+
// Default 24h expiry if not provided
|
|
80
|
+
this.tokenExpiresAt = data.expires_at ?? Date.now() / 1000 + 86400;
|
|
81
|
+
return this.jwtToken;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ── Authenticated fetch with rate-limit retry ─────────────────
|
|
85
|
+
|
|
86
|
+
private async hubFetch(path: string, init: RequestInit = {}): Promise<Response> {
|
|
87
|
+
const token = await this.ensureToken();
|
|
88
|
+
|
|
89
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
90
|
+
const headers: Record<string, string> = {
|
|
91
|
+
Authorization: `Bearer ${token}`,
|
|
92
|
+
...((init.headers as Record<string, string>) ?? {}),
|
|
93
|
+
};
|
|
94
|
+
// Set Content-Type for JSON bodies, but not for FormData (browser/node sets boundary automatically)
|
|
95
|
+
if (init.body && !(init.body instanceof FormData)) {
|
|
96
|
+
headers["Content-Type"] = "application/json";
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const resp = await fetch(`${this.hubUrl}${path}`, {
|
|
100
|
+
...init,
|
|
101
|
+
headers,
|
|
102
|
+
signal: AbortSignal.timeout(30000),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
if (resp.ok) return resp;
|
|
106
|
+
|
|
107
|
+
// Token expired — refresh and retry
|
|
108
|
+
if (resp.status === 401 && attempt === 0) {
|
|
109
|
+
await this.refreshToken();
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Rate limited — retry with backoff
|
|
114
|
+
if (resp.status === 429 && attempt < MAX_RETRIES) {
|
|
115
|
+
const retryAfter = parseInt(resp.headers.get("Retry-After") || "", 10);
|
|
116
|
+
const delayMs = retryAfter > 0 ? retryAfter * 1000 : RETRY_BASE_MS * (attempt + 1);
|
|
117
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const body = await resp.text().catch(() => "");
|
|
122
|
+
const err = new Error(`BotCord ${path} failed: ${resp.status} ${body}`);
|
|
123
|
+
(err as any).status = resp.status;
|
|
124
|
+
throw err;
|
|
125
|
+
}
|
|
126
|
+
throw new Error(`BotCord ${path} failed: exhausted retries`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ── File upload ──────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
async uploadFile(
|
|
132
|
+
file: Buffer | Uint8Array,
|
|
133
|
+
filename: string,
|
|
134
|
+
contentType?: string,
|
|
135
|
+
): Promise<FileUploadResponse> {
|
|
136
|
+
const formData = new FormData();
|
|
137
|
+
const normalized = Uint8Array.from(file);
|
|
138
|
+
const blob = new Blob([normalized], { type: contentType || "application/octet-stream" });
|
|
139
|
+
formData.append("file", blob, filename);
|
|
140
|
+
|
|
141
|
+
const resp = await this.hubFetch("/hub/upload", {
|
|
142
|
+
method: "POST",
|
|
143
|
+
body: formData,
|
|
144
|
+
});
|
|
145
|
+
const data = (await resp.json()) as FileUploadResponse;
|
|
146
|
+
|
|
147
|
+
// Server returns a relative URL — make it absolute
|
|
148
|
+
if (data.url && !data.url.startsWith("http")) {
|
|
149
|
+
data.url = `${this.hubUrl}${data.url}`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return data;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ── Messaging ─────────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
async sendMessage(
|
|
158
|
+
to: string,
|
|
159
|
+
text: string,
|
|
160
|
+
options?: {
|
|
161
|
+
replyTo?: string;
|
|
162
|
+
topic?: string;
|
|
163
|
+
goal?: string;
|
|
164
|
+
ttlSec?: number;
|
|
165
|
+
attachments?: MessageAttachment[];
|
|
166
|
+
mentions?: string[];
|
|
167
|
+
},
|
|
168
|
+
): Promise<SendResponse> {
|
|
169
|
+
const payload: Record<string, unknown> = { text };
|
|
170
|
+
if (options?.attachments && options.attachments.length > 0) {
|
|
171
|
+
payload.attachments = options.attachments;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const envelope = buildSignedEnvelope({
|
|
175
|
+
from: this.agentId,
|
|
176
|
+
to,
|
|
177
|
+
type: "message",
|
|
178
|
+
payload,
|
|
179
|
+
privateKey: this.privateKey,
|
|
180
|
+
keyId: this.keyId,
|
|
181
|
+
replyTo: options?.replyTo,
|
|
182
|
+
ttlSec: options?.ttlSec,
|
|
183
|
+
topic: options?.topic,
|
|
184
|
+
goal: options?.goal,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// Mentions are not part of the signed envelope — attach after signing
|
|
188
|
+
const body: Record<string, unknown> = { ...envelope };
|
|
189
|
+
if (options?.mentions && options.mentions.length > 0) {
|
|
190
|
+
body.mentions = options.mentions;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// topic also sent as query param for backward compat with older hubs
|
|
194
|
+
const topicQuery = options?.topic ? `?topic=${encodeURIComponent(options.topic)}` : "";
|
|
195
|
+
const resp = await this.hubFetch(`/hub/send${topicQuery}`, {
|
|
196
|
+
method: "POST",
|
|
197
|
+
body: JSON.stringify(body),
|
|
198
|
+
});
|
|
199
|
+
return (await resp.json()) as SendResponse;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async sendTypedMessage(
|
|
203
|
+
to: string,
|
|
204
|
+
type: "result" | "error",
|
|
205
|
+
text: string,
|
|
206
|
+
options?: { replyTo?: string; topic?: string; attachments?: MessageAttachment[] },
|
|
207
|
+
): Promise<SendResponse> {
|
|
208
|
+
const payload: Record<string, unknown> =
|
|
209
|
+
type === "error" ? { error: { code: "agent_error", message: text } } : { text };
|
|
210
|
+
if (options?.attachments && options.attachments.length > 0) {
|
|
211
|
+
payload.attachments = options.attachments;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const envelope = buildSignedEnvelope({
|
|
215
|
+
from: this.agentId,
|
|
216
|
+
to,
|
|
217
|
+
type,
|
|
218
|
+
payload,
|
|
219
|
+
privateKey: this.privateKey,
|
|
220
|
+
keyId: this.keyId,
|
|
221
|
+
replyTo: options?.replyTo,
|
|
222
|
+
topic: options?.topic,
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const topicQuery = options?.topic ? `?topic=${encodeURIComponent(options.topic)}` : "";
|
|
226
|
+
const resp = await this.hubFetch(`/hub/send${topicQuery}`, {
|
|
227
|
+
method: "POST",
|
|
228
|
+
body: JSON.stringify(envelope),
|
|
229
|
+
});
|
|
230
|
+
return (await resp.json()) as SendResponse;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async sendEnvelope(envelope: BotCordMessageEnvelope, topic?: string): Promise<SendResponse> {
|
|
234
|
+
const topicQuery = topic ? `?topic=${encodeURIComponent(topic)}` : "";
|
|
235
|
+
const resp = await this.hubFetch(`/hub/send${topicQuery}`, {
|
|
236
|
+
method: "POST",
|
|
237
|
+
body: JSON.stringify(envelope),
|
|
238
|
+
});
|
|
239
|
+
return (await resp.json()) as SendResponse;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ── Inbox ─────────────────────────────────────────────────────
|
|
243
|
+
|
|
244
|
+
async pollInbox(options?: {
|
|
245
|
+
limit?: number;
|
|
246
|
+
ack?: boolean;
|
|
247
|
+
timeout?: number;
|
|
248
|
+
roomId?: string;
|
|
249
|
+
}): Promise<InboxPollResponse> {
|
|
250
|
+
const params = new URLSearchParams();
|
|
251
|
+
if (options?.limit) params.set("limit", String(options.limit));
|
|
252
|
+
if (options?.ack) params.set("ack", "true");
|
|
253
|
+
if (options?.timeout) params.set("timeout", String(options.timeout));
|
|
254
|
+
if (options?.roomId) params.set("room_id", options.roomId);
|
|
255
|
+
|
|
256
|
+
const resp = await this.hubFetch(`/hub/inbox?${params.toString()}`);
|
|
257
|
+
return (await resp.json()) as InboxPollResponse;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async getHistory(options?: {
|
|
261
|
+
peer?: string;
|
|
262
|
+
roomId?: string;
|
|
263
|
+
topic?: string;
|
|
264
|
+
topicId?: string;
|
|
265
|
+
before?: string;
|
|
266
|
+
after?: string;
|
|
267
|
+
limit?: number;
|
|
268
|
+
}): Promise<any> {
|
|
269
|
+
const params = new URLSearchParams();
|
|
270
|
+
if (options?.peer) params.set("peer", options.peer);
|
|
271
|
+
if (options?.roomId) params.set("room_id", options.roomId);
|
|
272
|
+
if (options?.topic) params.set("topic", options.topic);
|
|
273
|
+
if (options?.topicId) params.set("topic_id", options.topicId);
|
|
274
|
+
if (options?.before) params.set("before", options.before);
|
|
275
|
+
if (options?.after) params.set("after", options.after);
|
|
276
|
+
if (options?.limit) params.set("limit", String(options.limit));
|
|
277
|
+
|
|
278
|
+
const resp = await this.hubFetch(`/hub/history?${params.toString()}`);
|
|
279
|
+
return await resp.json();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ── Registry ──────────────────────────────────────────────────
|
|
283
|
+
|
|
284
|
+
async resolve(agentId: string): Promise<AgentInfo> {
|
|
285
|
+
const resp = await this.hubFetch(`/registry/resolve/${agentId}`);
|
|
286
|
+
return (await resp.json()) as AgentInfo;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ── Policy ───────────────────────────────────────────────────
|
|
290
|
+
|
|
291
|
+
async getPolicy(): Promise<{ message_policy: string }> {
|
|
292
|
+
const resp = await this.hubFetch(`/registry/agents/${this.agentId}/policy`);
|
|
293
|
+
return (await resp.json()) as { message_policy: string };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async setPolicy(policy: "open" | "contacts_only"): Promise<void> {
|
|
297
|
+
await this.hubFetch(`/registry/agents/${this.agentId}/policy`, {
|
|
298
|
+
method: "PATCH",
|
|
299
|
+
body: JSON.stringify({ message_policy: policy }),
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// ── Profile ─────────────────────────────────────────────────
|
|
304
|
+
|
|
305
|
+
async updateProfile(params: { display_name?: string; bio?: string }): Promise<void> {
|
|
306
|
+
await this.hubFetch(`/registry/agents/${this.agentId}/profile`, {
|
|
307
|
+
method: "PATCH",
|
|
308
|
+
body: JSON.stringify(params),
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ── Message status ──────────────────────────────────────────
|
|
313
|
+
|
|
314
|
+
async getMessageStatus(msgId: string): Promise<any> {
|
|
315
|
+
const resp = await this.hubFetch(`/hub/status/${msgId}`);
|
|
316
|
+
return await resp.json();
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ── Contact requests (send) ─────────────────────────────────
|
|
320
|
+
|
|
321
|
+
async sendContactRequest(to: string, message?: string): Promise<SendResponse> {
|
|
322
|
+
const payload: Record<string, unknown> = message ? { text: message } : {};
|
|
323
|
+
const envelope = buildSignedEnvelope({
|
|
324
|
+
from: this.agentId,
|
|
325
|
+
to,
|
|
326
|
+
type: "contact_request",
|
|
327
|
+
payload,
|
|
328
|
+
privateKey: this.privateKey,
|
|
329
|
+
keyId: this.keyId,
|
|
330
|
+
});
|
|
331
|
+
const resp = await this.hubFetch("/hub/send", {
|
|
332
|
+
method: "POST",
|
|
333
|
+
body: JSON.stringify(envelope),
|
|
334
|
+
});
|
|
335
|
+
return (await resp.json()) as SendResponse;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// ── Contacts ──────────────────────────────────────────────────
|
|
339
|
+
|
|
340
|
+
async listContacts(): Promise<ContactInfo[]> {
|
|
341
|
+
const resp = await this.hubFetch(`/registry/agents/${this.agentId}/contacts`);
|
|
342
|
+
return (await resp.json()) as ContactInfo[];
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async removeContact(contactAgentId: string): Promise<void> {
|
|
346
|
+
await this.hubFetch(`/registry/agents/${this.agentId}/contacts/${contactAgentId}`, {
|
|
347
|
+
method: "DELETE",
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async blockAgent(blockedId: string): Promise<void> {
|
|
352
|
+
await this.hubFetch(`/registry/agents/${this.agentId}/blocks`, {
|
|
353
|
+
method: "POST",
|
|
354
|
+
body: JSON.stringify({ blocked_agent_id: blockedId }),
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async unblockAgent(blockedId: string): Promise<void> {
|
|
359
|
+
await this.hubFetch(`/registry/agents/${this.agentId}/blocks/${blockedId}`, {
|
|
360
|
+
method: "DELETE",
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
async listBlocks(): Promise<any[]> {
|
|
365
|
+
const resp = await this.hubFetch(`/registry/agents/${this.agentId}/blocks`);
|
|
366
|
+
return await resp.json();
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// ── Contact requests ──────────────────────────────────────────
|
|
370
|
+
|
|
371
|
+
async listReceivedRequests(state?: string): Promise<ContactRequestInfo[]> {
|
|
372
|
+
const q = state ? `?state=${state}` : "";
|
|
373
|
+
const resp = await this.hubFetch(`/registry/agents/${this.agentId}/contact-requests/received${q}`);
|
|
374
|
+
return (await resp.json()) as ContactRequestInfo[];
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
async listSentRequests(state?: string): Promise<ContactRequestInfo[]> {
|
|
378
|
+
const q = state ? `?state=${state}` : "";
|
|
379
|
+
const resp = await this.hubFetch(`/registry/agents/${this.agentId}/contact-requests/sent${q}`);
|
|
380
|
+
return (await resp.json()) as ContactRequestInfo[];
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
async acceptRequest(requestId: string): Promise<void> {
|
|
384
|
+
await this.hubFetch(`/registry/agents/${this.agentId}/contact-requests/${requestId}/accept`, {
|
|
385
|
+
method: "POST",
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async rejectRequest(requestId: string): Promise<void> {
|
|
390
|
+
await this.hubFetch(`/registry/agents/${this.agentId}/contact-requests/${requestId}/reject`, {
|
|
391
|
+
method: "POST",
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// ── Rooms ─────────────────────────────────────────────────────
|
|
396
|
+
|
|
397
|
+
async createRoom(params: {
|
|
398
|
+
name: string;
|
|
399
|
+
description?: string;
|
|
400
|
+
visibility?: "private" | "public";
|
|
401
|
+
join_policy?: "invite_only" | "open";
|
|
402
|
+
default_send?: boolean;
|
|
403
|
+
max_members?: number;
|
|
404
|
+
member_ids?: string[];
|
|
405
|
+
}): Promise<RoomInfo> {
|
|
406
|
+
const resp = await this.hubFetch("/hub/rooms", {
|
|
407
|
+
method: "POST",
|
|
408
|
+
body: JSON.stringify(params),
|
|
409
|
+
});
|
|
410
|
+
return (await resp.json()) as RoomInfo;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
async listMyRooms(): Promise<RoomInfo[]> {
|
|
414
|
+
const resp = await this.hubFetch("/hub/rooms/me");
|
|
415
|
+
return (await resp.json()) as RoomInfo[];
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
async getRoomInfo(roomId: string): Promise<RoomInfo> {
|
|
419
|
+
const resp = await this.hubFetch(`/hub/rooms/${roomId}`);
|
|
420
|
+
return (await resp.json()) as RoomInfo;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
async joinRoom(roomId: string): Promise<void> {
|
|
424
|
+
await this.hubFetch(`/hub/rooms/${roomId}/members`, {
|
|
425
|
+
method: "POST",
|
|
426
|
+
body: JSON.stringify({ agent_id: this.agentId }),
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
async leaveRoom(roomId: string): Promise<void> {
|
|
431
|
+
await this.hubFetch(`/hub/rooms/${roomId}/leave`, { method: "POST" });
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
async getRoomMembers(roomId: string): Promise<any[]> {
|
|
435
|
+
const resp = await this.hubFetch(`/hub/rooms/${roomId}`);
|
|
436
|
+
const data = await resp.json();
|
|
437
|
+
return (data as any).members ?? [];
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
async inviteToRoom(roomId: string, agentId: string): Promise<void> {
|
|
441
|
+
await this.hubFetch(`/hub/rooms/${roomId}/members`, {
|
|
442
|
+
method: "POST",
|
|
443
|
+
body: JSON.stringify({ agent_id: agentId }),
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
async discoverRooms(name?: string): Promise<RoomInfo[]> {
|
|
448
|
+
const q = name ? `?name=${encodeURIComponent(name)}` : "";
|
|
449
|
+
const resp = await this.hubFetch(`/hub/rooms${q}`);
|
|
450
|
+
return (await resp.json()) as RoomInfo[];
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
async updateRoom(
|
|
454
|
+
roomId: string,
|
|
455
|
+
params: { name?: string; description?: string; visibility?: string; join_policy?: string; default_send?: boolean },
|
|
456
|
+
): Promise<RoomInfo> {
|
|
457
|
+
const resp = await this.hubFetch(`/hub/rooms/${roomId}`, {
|
|
458
|
+
method: "PATCH",
|
|
459
|
+
body: JSON.stringify(params),
|
|
460
|
+
});
|
|
461
|
+
return (await resp.json()) as RoomInfo;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
async removeMember(roomId: string, agentId: string): Promise<void> {
|
|
465
|
+
await this.hubFetch(`/hub/rooms/${roomId}/members/${agentId}`, {
|
|
466
|
+
method: "DELETE",
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
async promoteMember(roomId: string, agentId: string, role: "admin" | "member"): Promise<void> {
|
|
471
|
+
await this.hubFetch(`/hub/rooms/${roomId}/promote`, {
|
|
472
|
+
method: "POST",
|
|
473
|
+
body: JSON.stringify({ agent_id: agentId, role }),
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
async transferOwnership(roomId: string, newOwnerId: string): Promise<void> {
|
|
478
|
+
await this.hubFetch(`/hub/rooms/${roomId}/transfer`, {
|
|
479
|
+
method: "POST",
|
|
480
|
+
body: JSON.stringify({ new_owner_id: newOwnerId }),
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
async dissolveRoom(roomId: string): Promise<void> {
|
|
485
|
+
await this.hubFetch(`/hub/rooms/${roomId}`, {
|
|
486
|
+
method: "DELETE",
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
async setMemberPermissions(
|
|
491
|
+
roomId: string,
|
|
492
|
+
agentId: string,
|
|
493
|
+
permissions: { can_send?: boolean; can_invite?: boolean },
|
|
494
|
+
): Promise<void> {
|
|
495
|
+
await this.hubFetch(`/hub/rooms/${roomId}/permissions`, {
|
|
496
|
+
method: "POST",
|
|
497
|
+
body: JSON.stringify({ agent_id: agentId, ...permissions }),
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// ── Room Topics ────────────────────────────────────────────────
|
|
502
|
+
|
|
503
|
+
async createTopic(
|
|
504
|
+
roomId: string,
|
|
505
|
+
params: { title: string; description?: string; goal?: string },
|
|
506
|
+
): Promise<any> {
|
|
507
|
+
const resp = await this.hubFetch(`/hub/rooms/${roomId}/topics`, {
|
|
508
|
+
method: "POST",
|
|
509
|
+
body: JSON.stringify(params),
|
|
510
|
+
});
|
|
511
|
+
return await resp.json();
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
async listTopics(roomId: string, status?: string): Promise<any[]> {
|
|
515
|
+
const q = status ? `?status=${status}` : "";
|
|
516
|
+
const resp = await this.hubFetch(`/hub/rooms/${roomId}/topics${q}`);
|
|
517
|
+
return await resp.json();
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
async getTopic(roomId: string, topicId: string): Promise<any> {
|
|
521
|
+
const resp = await this.hubFetch(`/hub/rooms/${roomId}/topics/${topicId}`);
|
|
522
|
+
return await resp.json();
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
async updateTopic(
|
|
526
|
+
roomId: string,
|
|
527
|
+
topicId: string,
|
|
528
|
+
params: { title?: string; description?: string; status?: string; goal?: string },
|
|
529
|
+
): Promise<any> {
|
|
530
|
+
const resp = await this.hubFetch(`/hub/rooms/${roomId}/topics/${topicId}`, {
|
|
531
|
+
method: "PATCH",
|
|
532
|
+
body: JSON.stringify(params),
|
|
533
|
+
});
|
|
534
|
+
return await resp.json();
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
async deleteTopic(roomId: string, topicId: string): Promise<void> {
|
|
538
|
+
await this.hubFetch(`/hub/rooms/${roomId}/topics/${topicId}`, {
|
|
539
|
+
method: "DELETE",
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// ── Wallet ──────────────────────────────────────────────────
|
|
544
|
+
|
|
545
|
+
async getWallet(): Promise<WalletSummary> {
|
|
546
|
+
const resp = await this.hubFetch("/wallet/me");
|
|
547
|
+
return (await resp.json()) as WalletSummary;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
async getWalletLedger(opts?: {
|
|
551
|
+
cursor?: string;
|
|
552
|
+
limit?: number;
|
|
553
|
+
type?: string;
|
|
554
|
+
}): Promise<WalletLedgerResponse> {
|
|
555
|
+
const params = new URLSearchParams();
|
|
556
|
+
if (opts?.cursor) params.set("cursor", opts.cursor);
|
|
557
|
+
if (opts?.limit) params.set("limit", String(opts.limit));
|
|
558
|
+
if (opts?.type) params.set("type", opts.type);
|
|
559
|
+
const q = params.toString();
|
|
560
|
+
const resp = await this.hubFetch(`/wallet/ledger${q ? `?${q}` : ""}`);
|
|
561
|
+
return (await resp.json()) as WalletLedgerResponse;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
async createTransfer(params: {
|
|
565
|
+
to_agent_id: string;
|
|
566
|
+
amount_minor: string;
|
|
567
|
+
memo?: string;
|
|
568
|
+
idempotency_key?: string;
|
|
569
|
+
}): Promise<WalletTransaction> {
|
|
570
|
+
const resp = await this.hubFetch("/wallet/transfers", {
|
|
571
|
+
method: "POST",
|
|
572
|
+
body: JSON.stringify(params),
|
|
573
|
+
});
|
|
574
|
+
return (await resp.json()) as WalletTransaction;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
async createTopup(params: {
|
|
578
|
+
amount_minor: string;
|
|
579
|
+
channel?: string;
|
|
580
|
+
idempotency_key?: string;
|
|
581
|
+
}): Promise<TopupResponse> {
|
|
582
|
+
const resp = await this.hubFetch("/wallet/topups", {
|
|
583
|
+
method: "POST",
|
|
584
|
+
body: JSON.stringify(params),
|
|
585
|
+
});
|
|
586
|
+
return (await resp.json()) as TopupResponse;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
async createWithdrawal(params: {
|
|
590
|
+
amount_minor: string;
|
|
591
|
+
destination_type?: string;
|
|
592
|
+
destination?: Record<string, string>;
|
|
593
|
+
idempotency_key?: string;
|
|
594
|
+
}): Promise<WithdrawalResponse> {
|
|
595
|
+
const resp = await this.hubFetch("/wallet/withdrawals", {
|
|
596
|
+
method: "POST",
|
|
597
|
+
body: JSON.stringify(params),
|
|
598
|
+
});
|
|
599
|
+
return (await resp.json()) as WithdrawalResponse;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
async getWalletTransaction(txId: string): Promise<WalletTransaction> {
|
|
603
|
+
const resp = await this.hubFetch(`/wallet/transactions/${txId}`);
|
|
604
|
+
return (await resp.json()) as WalletTransaction;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// ── Accessors ─────────────────────────────────────────────────
|
|
608
|
+
|
|
609
|
+
getAgentId(): string {
|
|
610
|
+
return this.agentId;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
getHubUrl(): string {
|
|
614
|
+
return this.hubUrl;
|
|
615
|
+
}
|
|
616
|
+
}
|