acn-client 0.2.1

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/index.mjs ADDED
@@ -0,0 +1,587 @@
1
+ // src/client.ts
2
+ var ACNClient = class {
3
+ baseUrl;
4
+ timeout;
5
+ headers;
6
+ constructor(options) {
7
+ if (typeof options === "string") {
8
+ this.baseUrl = options.replace(/\/$/, "");
9
+ this.timeout = 3e4;
10
+ this.headers = {};
11
+ } else {
12
+ this.baseUrl = options.baseUrl.replace(/\/$/, "");
13
+ this.timeout = options.timeout ?? 3e4;
14
+ this.headers = options.headers ?? {};
15
+ if (options.apiKey) {
16
+ this.headers["X-API-Key"] = options.apiKey;
17
+ }
18
+ }
19
+ }
20
+ // ============================================
21
+ // Internal HTTP Methods
22
+ // ============================================
23
+ async request(method, path, options) {
24
+ const url = new URL(`${this.baseUrl}${path}`);
25
+ if (options?.params) {
26
+ Object.entries(options.params).forEach(([key, value]) => {
27
+ if (value !== void 0) {
28
+ url.searchParams.append(key, String(value));
29
+ }
30
+ });
31
+ }
32
+ const controller = new AbortController();
33
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
34
+ try {
35
+ const response = await fetch(url.toString(), {
36
+ method,
37
+ headers: {
38
+ "Content-Type": "application/json",
39
+ ...this.headers
40
+ },
41
+ body: options?.body ? JSON.stringify(options.body) : void 0,
42
+ signal: controller.signal
43
+ });
44
+ if (!response.ok) {
45
+ const error = await response.json().catch(() => ({ message: response.statusText }));
46
+ throw new ACNError(response.status, error.detail || error.message || "Request failed");
47
+ }
48
+ if (response.status === 204) {
49
+ return void 0;
50
+ }
51
+ return response.json();
52
+ } finally {
53
+ clearTimeout(timeoutId);
54
+ }
55
+ }
56
+ get(path, params) {
57
+ return this.request("GET", path, { params });
58
+ }
59
+ post(path, body) {
60
+ return this.request("POST", path, { body });
61
+ }
62
+ delete(path) {
63
+ return this.request("DELETE", path);
64
+ }
65
+ // ============================================
66
+ // Health & Status
67
+ // ============================================
68
+ /** Check if ACN server is healthy */
69
+ async health() {
70
+ return this.get("/health");
71
+ }
72
+ /** Get server statistics */
73
+ async getStats() {
74
+ return this.get("/api/v1/stats");
75
+ }
76
+ // ============================================
77
+ // Agent Management
78
+ // ============================================
79
+ /** Register a new agent */
80
+ async registerAgent(agent) {
81
+ return this.post("/api/v1/agents/register", agent);
82
+ }
83
+ /** Get agent by ID */
84
+ async getAgent(agentId) {
85
+ return this.get(`/api/v1/agents/${agentId}`);
86
+ }
87
+ /** Search agents (status: online | offline | all; public list does not include verification_code) */
88
+ async searchAgents(options) {
89
+ return this.get("/api/v1/agents", {
90
+ skill: options?.skills,
91
+ status: options?.status
92
+ });
93
+ }
94
+ /** Unregister an agent */
95
+ async unregisterAgent(agentId) {
96
+ return this.delete(`/api/v1/agents/${agentId}`);
97
+ }
98
+ /** Send agent heartbeat */
99
+ async heartbeat(agentId) {
100
+ return this.post(`/api/v1/agents/${agentId}/heartbeat`);
101
+ }
102
+ /** Get agent endpoint */
103
+ async getAgentEndpoint(agentId) {
104
+ return this.get(`/api/v1/agents/${agentId}/endpoint`);
105
+ }
106
+ /** List all available skills */
107
+ async getSkills() {
108
+ return this.get("/api/v1/skills");
109
+ }
110
+ // ============================================
111
+ // Subnet Management
112
+ // ============================================
113
+ /** Create a new subnet */
114
+ async createSubnet(request) {
115
+ return this.post("/api/v1/subnets", request);
116
+ }
117
+ /** List all subnets */
118
+ async listSubnets() {
119
+ return this.get("/api/v1/subnets");
120
+ }
121
+ /** Get subnet by ID */
122
+ async getSubnet(subnetId) {
123
+ return this.get(`/api/v1/subnets/${subnetId}`);
124
+ }
125
+ /** Delete a subnet */
126
+ async deleteSubnet(subnetId, force = false) {
127
+ return this.request("DELETE", `/api/v1/subnets/${subnetId}`, {
128
+ params: { force }
129
+ });
130
+ }
131
+ /** Get agents in a subnet */
132
+ async getSubnetAgents(subnetId) {
133
+ return this.get(`/api/v1/subnets/${subnetId}/agents`);
134
+ }
135
+ /** Join agent to subnet */
136
+ async joinSubnet(agentId, subnetId) {
137
+ return this.post(`/api/v1/agents/${agentId}/subnets/${subnetId}`);
138
+ }
139
+ /** Remove agent from subnet */
140
+ async leaveSubnet(agentId, subnetId) {
141
+ return this.delete(`/api/v1/agents/${agentId}/subnets/${subnetId}`);
142
+ }
143
+ /** Get agent's subnets */
144
+ async getAgentSubnets(agentId) {
145
+ return this.get(`/api/v1/agents/${agentId}/subnets`);
146
+ }
147
+ // ============================================
148
+ // Communication
149
+ // ============================================
150
+ /** Send message to an agent */
151
+ async sendMessage(request) {
152
+ return this.post("/api/v1/communication/send", request);
153
+ }
154
+ /** Broadcast message to multiple agents */
155
+ async broadcast(request) {
156
+ return this.post("/api/v1/communication/broadcast", request);
157
+ }
158
+ /** Broadcast message to agents with specific skill */
159
+ async broadcastBySkill(request) {
160
+ return this.post("/api/v1/communication/broadcast-by-skill", request);
161
+ }
162
+ /** Get message history for an agent */
163
+ async getMessageHistory(agentId, options) {
164
+ return this.get(`/api/v1/communication/history/${agentId}`, options);
165
+ }
166
+ // ============================================
167
+ // Payment Discovery
168
+ // ============================================
169
+ /** Set agent's payment capability */
170
+ async setPaymentCapability(agentId, capability) {
171
+ return this.post(`/api/v1/agents/${agentId}/payment-capability`, capability);
172
+ }
173
+ /** Get agent's payment capability */
174
+ async getPaymentCapability(agentId) {
175
+ return this.get(`/api/v1/agents/${agentId}/payment-capability`);
176
+ }
177
+ /** Discover agents that accept payments */
178
+ async discoverPaymentAgents(options) {
179
+ return this.get("/api/v1/payments/discover", {
180
+ method: options?.method,
181
+ network: options?.network,
182
+ min_amount: options?.min_amount,
183
+ max_amount: options?.max_amount
184
+ });
185
+ }
186
+ /** Get payment task by ID */
187
+ async getPaymentTask(taskId) {
188
+ return this.get(`/api/v1/payments/tasks/${taskId}`);
189
+ }
190
+ /** Get agent's payment tasks */
191
+ async getAgentPaymentTasks(agentId, options) {
192
+ return this.get(`/api/v1/payments/tasks/agent/${agentId}`, options);
193
+ }
194
+ /** Get agent's payment statistics */
195
+ async getPaymentStats(agentId) {
196
+ return this.get(`/api/v1/payments/stats/${agentId}`);
197
+ }
198
+ // ============================================
199
+ // Monitoring & Analytics
200
+ // ============================================
201
+ /** Get Prometheus metrics (text format) */
202
+ async getPrometheusMetrics() {
203
+ const response = await fetch(`${this.baseUrl}/metrics`);
204
+ return response.text();
205
+ }
206
+ /** Get all metrics */
207
+ async getMetrics() {
208
+ return this.get("/api/v1/monitoring/metrics");
209
+ }
210
+ /** Get system health */
211
+ async getSystemHealth() {
212
+ return this.get("/api/v1/monitoring/health");
213
+ }
214
+ /** Get dashboard data */
215
+ async getDashboard() {
216
+ return this.get("/api/v1/monitoring/dashboard");
217
+ }
218
+ /** Get agent analytics */
219
+ async getAgentAnalytics() {
220
+ return this.get("/api/v1/analytics/agents");
221
+ }
222
+ /** Get specific agent's activity */
223
+ async getAgentActivity(agentId, options) {
224
+ return this.get(`/api/v1/analytics/agents/${agentId}`, options);
225
+ }
226
+ /** Get message analytics */
227
+ async getMessageAnalytics() {
228
+ return this.get("/api/v1/analytics/messages");
229
+ }
230
+ /** Get latency analytics */
231
+ async getLatencyAnalytics() {
232
+ return this.get("/api/v1/analytics/latency");
233
+ }
234
+ /** Get subnet analytics */
235
+ async getSubnetAnalytics() {
236
+ return this.get("/api/v1/analytics/subnets");
237
+ }
238
+ // ============================================
239
+ // Audit
240
+ // ============================================
241
+ /** Get audit events */
242
+ async getAuditEvents(options) {
243
+ return this.get("/api/v1/audit/events", options);
244
+ }
245
+ /** Get recent audit events */
246
+ async getRecentAuditEvents(limit = 100) {
247
+ return this.get("/api/v1/audit/events/recent", { limit });
248
+ }
249
+ // ============================================
250
+ // ERC-8004 On-Chain Identity
251
+ // ============================================
252
+ /**
253
+ * Register the agent on ERC-8004 Identity Registry and bind to ACN.
254
+ *
255
+ * Full flow:
256
+ * 1. Generate wallet if privateKey is undefined (saved to saveWalletPath).
257
+ * 2. Construct agentURI → agent-registration.json endpoint.
258
+ * 3. Sign and broadcast register(agentURI) transaction via viem.
259
+ * 4. Extract token ID from Registered event.
260
+ * 5. POST /api/v1/onchain/agents/{agentId}/bind to inform ACN.
261
+ *
262
+ * @param agentId - ACN agent ID (from join response).
263
+ * @param options - Chain, RPC, private key, wallet save path.
264
+ */
265
+ async registerOnchain(agentId, options = {}) {
266
+ const {
267
+ chain = "base",
268
+ rpcUrl,
269
+ saveWalletPath = ".env"
270
+ } = options;
271
+ const { createWalletClient, createPublicClient, http } = await import("viem");
272
+ const { generatePrivateKey, privateKeyToAccount } = await import("viem/accounts");
273
+ const { base, baseSepolia } = await import("viem/chains");
274
+ const chainConfigs = {
275
+ base: {
276
+ viemChain: base,
277
+ identityContract: "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432",
278
+ namespace: "eip155:8453"
279
+ },
280
+ "base-sepolia": {
281
+ viemChain: baseSepolia,
282
+ identityContract: "0x8004A818BFB912233c491871b3d84c89A494BD9e",
283
+ namespace: "eip155:84532"
284
+ }
285
+ };
286
+ const cfg = chainConfigs[chain];
287
+ let walletGenerated = false;
288
+ let privateKey;
289
+ if (!options.privateKey) {
290
+ privateKey = generatePrivateKey();
291
+ walletGenerated = true;
292
+ if (saveWalletPath) {
293
+ await this._saveWalletToEnv(saveWalletPath, privateKey);
294
+ }
295
+ } else {
296
+ privateKey = options.privateKey;
297
+ }
298
+ const account = privateKeyToAccount(privateKey);
299
+ const agentRegistrationUrl = `${this.baseUrl}/api/v1/agents/${agentId}/.well-known/agent-registration.json`;
300
+ const abi = [
301
+ {
302
+ name: "register",
303
+ type: "function",
304
+ stateMutability: "nonpayable",
305
+ inputs: [{ type: "string", name: "agentURI" }],
306
+ outputs: [{ type: "uint256", name: "agentId" }]
307
+ },
308
+ {
309
+ name: "Registered",
310
+ type: "event",
311
+ inputs: [
312
+ { type: "uint256", name: "agentId", indexed: true },
313
+ { type: "string", name: "agentURI", indexed: false },
314
+ { type: "address", name: "owner", indexed: true }
315
+ ]
316
+ }
317
+ ];
318
+ const transport = http(rpcUrl ?? void 0);
319
+ const walletClient = createWalletClient({ account, chain: cfg.viemChain, transport });
320
+ const publicClient = createPublicClient({ chain: cfg.viemChain, transport });
321
+ const txHash = await walletClient.writeContract({
322
+ address: cfg.identityContract,
323
+ abi,
324
+ functionName: "register",
325
+ args: [agentRegistrationUrl]
326
+ });
327
+ const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash });
328
+ const { decodeEventLog } = await import("viem");
329
+ let tokenId;
330
+ for (const log of receipt.logs) {
331
+ try {
332
+ const decoded = decodeEventLog({ abi, data: log.data, topics: log.topics });
333
+ if (decoded.eventName === "Registered") {
334
+ tokenId = decoded.args.agentId;
335
+ break;
336
+ }
337
+ } catch {
338
+ }
339
+ }
340
+ if (tokenId === void 0) {
341
+ throw new Error("Registered event not found in transaction receipt");
342
+ }
343
+ await this.post(`/api/v1/onchain/agents/${agentId}/bind`, {
344
+ token_id: Number(tokenId),
345
+ chain: cfg.namespace,
346
+ tx_hash: txHash
347
+ });
348
+ return {
349
+ tokenId,
350
+ txHash,
351
+ chain: cfg.namespace,
352
+ agentRegistrationUrl,
353
+ walletAddress: account.address,
354
+ walletGenerated
355
+ };
356
+ }
357
+ /** @internal Save generated wallet credentials to a .env file. */
358
+ async _saveWalletToEnv(path, privateKey) {
359
+ if (typeof window !== "undefined") return;
360
+ try {
361
+ const fs = await import("fs/promises");
362
+ let content = "";
363
+ try {
364
+ content = await fs.readFile(path, "utf8");
365
+ } catch {
366
+ }
367
+ const existing = new Set(content.split("\n").map((l) => l.split("=")[0].trim()));
368
+ const toAdd = [];
369
+ if (!existing.has("WALLET_PRIVATE_KEY")) toAdd.push(`WALLET_PRIVATE_KEY=${privateKey}`);
370
+ if (toAdd.length) await fs.appendFile(path, "\n" + toAdd.join("\n") + "\n");
371
+ } catch {
372
+ }
373
+ }
374
+ /** Get audit statistics */
375
+ async getAuditStats(options) {
376
+ return this.get("/api/v1/audit/stats", options);
377
+ }
378
+ };
379
+ var ACNError = class extends Error {
380
+ constructor(status, message) {
381
+ super(message);
382
+ this.status = status;
383
+ this.name = "ACNError";
384
+ }
385
+ };
386
+
387
+ // src/realtime.ts
388
+ var ACNRealtime = class {
389
+ baseUrl;
390
+ options;
391
+ ws = null;
392
+ state = "disconnected";
393
+ reconnectAttempts = 0;
394
+ heartbeatTimer = null;
395
+ reconnectTimer = null;
396
+ channels = /* @__PURE__ */ new Map();
397
+ globalHandlers = /* @__PURE__ */ new Set();
398
+ stateHandlers = /* @__PURE__ */ new Set();
399
+ constructor(baseUrl, options) {
400
+ this.baseUrl = baseUrl.replace(/^http:/, "ws:").replace(/^https:/, "wss:").replace(/\/$/, "");
401
+ this.options = {
402
+ autoReconnect: options?.autoReconnect ?? true,
403
+ reconnectInterval: options?.reconnectInterval ?? 3e3,
404
+ maxReconnectAttempts: options?.maxReconnectAttempts ?? 10,
405
+ heartbeatInterval: options?.heartbeatInterval ?? 3e4
406
+ };
407
+ }
408
+ /** Current connection state */
409
+ get connectionState() {
410
+ return this.state;
411
+ }
412
+ /** Whether currently connected */
413
+ get isConnected() {
414
+ return this.state === "connected";
415
+ }
416
+ /**
417
+ * Connect to a channel
418
+ */
419
+ async connect(channel = "default") {
420
+ if (this.ws && this.state === "connected") {
421
+ return;
422
+ }
423
+ return new Promise((resolve, reject) => {
424
+ this.setState("connecting");
425
+ try {
426
+ this.ws = new WebSocket(`${this.baseUrl}/ws/${channel}`);
427
+ this.ws.onopen = () => {
428
+ this.setState("connected");
429
+ this.reconnectAttempts = 0;
430
+ this.startHeartbeat();
431
+ resolve();
432
+ };
433
+ this.ws.onclose = (event) => {
434
+ this.stopHeartbeat();
435
+ if (event.wasClean) {
436
+ this.setState("disconnected");
437
+ } else if (this.options.autoReconnect && this.reconnectAttempts < this.options.maxReconnectAttempts) {
438
+ this.scheduleReconnect(channel);
439
+ } else {
440
+ this.setState("disconnected");
441
+ }
442
+ };
443
+ this.ws.onerror = (error) => {
444
+ if (this.state === "connecting") {
445
+ reject(new Error("WebSocket connection failed"));
446
+ }
447
+ this.handleError(error);
448
+ };
449
+ this.ws.onmessage = (event) => {
450
+ this.handleMessage(event.data);
451
+ };
452
+ } catch (error) {
453
+ this.setState("disconnected");
454
+ reject(error);
455
+ }
456
+ });
457
+ }
458
+ /**
459
+ * Disconnect from server
460
+ */
461
+ disconnect() {
462
+ this.stopHeartbeat();
463
+ this.clearReconnectTimer();
464
+ if (this.ws) {
465
+ this.ws.close(1e3, "Client disconnect");
466
+ this.ws = null;
467
+ }
468
+ this.setState("disconnected");
469
+ }
470
+ /**
471
+ * Subscribe to a channel
472
+ */
473
+ subscribe(channel, handler) {
474
+ let handlers = this.channels.get(channel);
475
+ if (!handlers) {
476
+ handlers = /* @__PURE__ */ new Set();
477
+ this.channels.set(channel, handlers);
478
+ }
479
+ handlers.add(handler);
480
+ return () => {
481
+ handlers?.delete(handler);
482
+ if (handlers?.size === 0) {
483
+ this.channels.delete(channel);
484
+ }
485
+ };
486
+ }
487
+ /**
488
+ * Subscribe to all messages
489
+ */
490
+ onMessage(handler) {
491
+ this.globalHandlers.add(handler);
492
+ return () => {
493
+ this.globalHandlers.delete(handler);
494
+ };
495
+ }
496
+ /**
497
+ * Subscribe to state changes
498
+ */
499
+ onStateChange(handler) {
500
+ this.stateHandlers.add(handler);
501
+ return () => {
502
+ this.stateHandlers.delete(handler);
503
+ };
504
+ }
505
+ /**
506
+ * Send a message
507
+ */
508
+ send(message) {
509
+ if (!this.ws || this.state !== "connected") {
510
+ throw new Error("WebSocket not connected");
511
+ }
512
+ this.ws.send(JSON.stringify(message));
513
+ }
514
+ // ============================================
515
+ // Private Methods
516
+ // ============================================
517
+ setState(state) {
518
+ this.state = state;
519
+ this.stateHandlers.forEach((handler) => handler(state));
520
+ }
521
+ handleMessage(data) {
522
+ try {
523
+ const message = JSON.parse(data);
524
+ this.globalHandlers.forEach((handler) => handler(message));
525
+ const channelHandlers = this.channels.get(message.channel);
526
+ if (channelHandlers) {
527
+ channelHandlers.forEach((handler) => handler(message));
528
+ }
529
+ const typeHandlers = this.channels.get(message.type);
530
+ if (typeHandlers) {
531
+ typeHandlers.forEach((handler) => handler(message));
532
+ }
533
+ } catch (error) {
534
+ console.error("Failed to parse WebSocket message:", error);
535
+ }
536
+ }
537
+ handleError(error) {
538
+ console.error("WebSocket error:", error);
539
+ }
540
+ startHeartbeat() {
541
+ this.stopHeartbeat();
542
+ this.heartbeatTimer = setInterval(() => {
543
+ if (this.ws && this.state === "connected") {
544
+ this.send({ type: "ping", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
545
+ }
546
+ }, this.options.heartbeatInterval);
547
+ }
548
+ stopHeartbeat() {
549
+ if (this.heartbeatTimer) {
550
+ clearInterval(this.heartbeatTimer);
551
+ this.heartbeatTimer = null;
552
+ }
553
+ }
554
+ scheduleReconnect(channel) {
555
+ this.clearReconnectTimer();
556
+ this.setState("reconnecting");
557
+ this.reconnectAttempts++;
558
+ const delay = this.options.reconnectInterval * Math.min(this.reconnectAttempts, 5);
559
+ this.reconnectTimer = setTimeout(() => {
560
+ this.connect(channel).catch(() => {
561
+ });
562
+ }, delay);
563
+ }
564
+ clearReconnectTimer() {
565
+ if (this.reconnectTimer) {
566
+ clearTimeout(this.reconnectTimer);
567
+ this.reconnectTimer = null;
568
+ }
569
+ }
570
+ };
571
+ function subscribeToACN(baseUrl, channel, handler) {
572
+ const realtime = new ACNRealtime(baseUrl);
573
+ const unsubscribe = realtime.subscribe(channel, handler);
574
+ realtime.connect(channel).catch((error) => {
575
+ console.error("Failed to connect to ACN:", error);
576
+ });
577
+ return () => {
578
+ unsubscribe();
579
+ realtime.disconnect();
580
+ };
581
+ }
582
+ export {
583
+ ACNClient,
584
+ ACNError,
585
+ ACNRealtime,
586
+ subscribeToACN
587
+ };
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "acn-client",
3
+ "version": "0.2.1",
4
+ "description": "Official TypeScript/JavaScript client for ACN (Agent Collaboration Network)",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsup src/index.ts --format cjs,esm --dts --clean",
21
+ "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
22
+ "lint": "eslint src/",
23
+ "typecheck": "tsc --noEmit",
24
+ "test": "vitest",
25
+ "prepublishOnly": "npm run build"
26
+ },
27
+ "keywords": [
28
+ "acn",
29
+ "agent",
30
+ "ai",
31
+ "collaboration",
32
+ "a2a",
33
+ "websocket",
34
+ "sdk"
35
+ ],
36
+ "author": "acnlabs",
37
+ "license": "MIT",
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "https://github.com/acnlabs/ACN.git",
41
+ "directory": "clients/typescript"
42
+ },
43
+ "homepage": "https://github.com/acnlabs/ACN#readme",
44
+ "bugs": {
45
+ "url": "https://github.com/acnlabs/ACN/issues"
46
+ },
47
+ "dependencies": {
48
+ "viem": "^2.0.0"
49
+ },
50
+ "devDependencies": {
51
+ "@types/node": "^20.0.0",
52
+ "tsup": "^8.0.0",
53
+ "typescript": "^5.0.0",
54
+ "vitest": "^1.0.0"
55
+ },
56
+ "peerDependencies": {
57
+ "typescript": ">=4.7.0"
58
+ },
59
+ "peerDependenciesMeta": {
60
+ "typescript": {
61
+ "optional": true
62
+ }
63
+ },
64
+ "engines": {
65
+ "node": ">=18.0.0"
66
+ }
67
+ }
68
+