@annals/agent-mesh 0.17.6 → 0.17.7

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.
Files changed (2) hide show
  1. package/dist/index.js +207 -85
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -290,6 +290,7 @@ import { existsSync, copyFileSync, mkdirSync } from "fs";
290
290
  import { join, dirname } from "path";
291
291
  var ICE_SERVERS = ["stun:stun.l.google.com:19302"];
292
292
  var CHUNK_SIZE = 64 * 1024;
293
+ var CONNECT_TIMEOUT_MS = 1e4;
293
294
  var ndcModule;
294
295
  function ensurePrebuilt() {
295
296
  try {
@@ -410,6 +411,126 @@ var FileSender = class {
410
411
  this.peer = null;
411
412
  }
412
413
  };
414
+ var FileReceiver = class {
415
+ peer = null;
416
+ dc = null;
417
+ expectedSize;
418
+ expectedSha256;
419
+ chunks = [];
420
+ receivedBytes = 0;
421
+ resolveComplete = null;
422
+ rejectComplete = null;
423
+ signalCallback = null;
424
+ pendingCandidates = [];
425
+ closed = false;
426
+ constructor(expectedSize, expectedSha256) {
427
+ this.expectedSize = expectedSize;
428
+ this.expectedSha256 = expectedSha256;
429
+ }
430
+ onSignal(cb) {
431
+ this.signalCallback = cb;
432
+ }
433
+ async createOffer() {
434
+ const ndc = await loadNdc();
435
+ if (!ndc) return null;
436
+ this.peer = new ndc.PeerConnection("receiver", {
437
+ iceServers: ICE_SERVERS
438
+ });
439
+ return new Promise((resolve2) => {
440
+ this.peer.onLocalDescription((sdp, type) => {
441
+ if (type === "offer") {
442
+ resolve2(sdp);
443
+ }
444
+ this.signalCallback?.({ signal_type: type, payload: sdp });
445
+ });
446
+ this.peer.onLocalCandidate((candidate, mid) => {
447
+ this.signalCallback?.({
448
+ signal_type: "candidate",
449
+ payload: JSON.stringify({ candidate, mid })
450
+ });
451
+ });
452
+ this.dc = this.peer.createDataChannel("file-transfer");
453
+ this.dc.onMessage((msg) => {
454
+ if (typeof msg === "string") {
455
+ try {
456
+ const ctrl = JSON.parse(msg);
457
+ if (ctrl.type === "complete") {
458
+ this.finalizeReceive();
459
+ }
460
+ } catch {
461
+ }
462
+ return;
463
+ }
464
+ const buf = Buffer.isBuffer(msg) ? msg : Buffer.from(msg);
465
+ this.chunks.push(buf);
466
+ this.receivedBytes += buf.length;
467
+ });
468
+ });
469
+ }
470
+ async handleSignal(signal) {
471
+ if (!this.peer || this.closed) return;
472
+ if (signal.signal_type === "answer" || signal.signal_type === "offer") {
473
+ this.peer.setRemoteDescription(signal.payload, signal.signal_type);
474
+ for (const c of this.pendingCandidates) {
475
+ this.peer.addRemoteCandidate(c.candidate, c.mid);
476
+ }
477
+ this.pendingCandidates = [];
478
+ } else if (signal.signal_type === "candidate") {
479
+ const { candidate, mid } = JSON.parse(signal.payload);
480
+ if (this.peer.remoteDescription()) {
481
+ this.peer.addRemoteCandidate(candidate, mid);
482
+ } else {
483
+ this.pendingCandidates.push({ candidate, mid });
484
+ }
485
+ }
486
+ }
487
+ waitForCompletion(timeoutMs = CONNECT_TIMEOUT_MS) {
488
+ return new Promise((resolve2, reject) => {
489
+ this.resolveComplete = resolve2;
490
+ this.rejectComplete = reject;
491
+ setTimeout(() => {
492
+ if (!this.closed) {
493
+ reject(new Error(`WebRTC file transfer timed out after ${timeoutMs}ms`));
494
+ this.close();
495
+ }
496
+ }, timeoutMs);
497
+ });
498
+ }
499
+ finalizeReceive() {
500
+ const zipBuffer = Buffer.concat(this.chunks);
501
+ const actualSha256 = createHash("sha256").update(zipBuffer).digest("hex");
502
+ if (zipBuffer.length !== this.expectedSize) {
503
+ this.rejectComplete?.(
504
+ new Error(`Size mismatch: expected ${this.expectedSize}, got ${zipBuffer.length}`)
505
+ );
506
+ this.close();
507
+ return;
508
+ }
509
+ if (actualSha256 !== this.expectedSha256) {
510
+ this.rejectComplete?.(
511
+ new Error(`SHA-256 mismatch: expected ${this.expectedSha256}, got ${actualSha256}`)
512
+ );
513
+ this.close();
514
+ return;
515
+ }
516
+ log.info(`[WebRTC] Received ${zipBuffer.length} bytes, SHA-256 verified`);
517
+ this.resolveComplete?.(zipBuffer);
518
+ this.close();
519
+ }
520
+ close() {
521
+ this.closed = true;
522
+ try {
523
+ this.dc?.close();
524
+ } catch {
525
+ }
526
+ try {
527
+ this.peer?.close();
528
+ } catch {
529
+ }
530
+ this.peer = null;
531
+ this.dc = null;
532
+ }
533
+ };
413
534
  function sha256Hex(data) {
414
535
  return createHash("sha256").update(data).digest("hex");
415
536
  }
@@ -1045,9 +1166,6 @@ var BridgeManager = class {
1045
1166
  const attachments = payload?.attachments;
1046
1167
  const fileTransferOffer = payload?.fileTransferOffer;
1047
1168
  const zipBuffer = payload?.zipBuffer;
1048
- if (fileTransferOffer && zipBuffer) {
1049
- this.registerPendingTransfer(fileTransferOffer, zipBuffer);
1050
- }
1051
1169
  const done = {
1052
1170
  type: "done",
1053
1171
  session_id: sessionId,
@@ -1060,6 +1178,9 @@ var BridgeManager = class {
1060
1178
  this.wsClient.send(done);
1061
1179
  fullResponseBuffer = "";
1062
1180
  this.sessionLastSeenAt.set(sessionId, Date.now());
1181
+ if (fileTransferOffer && zipBuffer) {
1182
+ this.registerPendingTransfer(fileTransferOffer, zipBuffer);
1183
+ }
1063
1184
  const fileInfo = fileTransferOffer ? ` (${fileTransferOffer.file_count} files, transfer=${fileTransferOffer.transfer_id.slice(0, 8)}...)` : "";
1064
1185
  log.info(`Request done: session=${sessionId.slice(0, 8)}... request=${requestRef.requestId.slice(0, 8)}...${fileInfo}`);
1065
1186
  });
@@ -1265,32 +1386,7 @@ var BridgeManager = class {
1265
1386
  }, 5 * 6e4);
1266
1387
  timer.unref?.();
1267
1388
  this.pendingTransfers.set(offer.transfer_id, { sender, timer });
1268
- this.pushZipToWorker(offer.transfer_id, zipBuffer);
1269
- }
1270
- /** Push ZIP buffer to Worker DO via chunked transfer_upload messages */
1271
- pushZipToWorker(transferId, zipBuffer) {
1272
- const CHUNK_SIZE2 = 64 * 1024;
1273
- const totalChunks = Math.ceil(zipBuffer.length / CHUNK_SIZE2);
1274
- log.debug(`Pushing ZIP to Worker: transfer=${transferId.slice(0, 8)}... size=${zipBuffer.length} chunks=${totalChunks}`);
1275
- for (let i = 0; i < totalChunks; i++) {
1276
- const start = i * CHUNK_SIZE2;
1277
- const end = Math.min(start + CHUNK_SIZE2, zipBuffer.length);
1278
- const chunk = zipBuffer.subarray(start, end);
1279
- const msg = {
1280
- type: "transfer_upload",
1281
- transfer_id: transferId,
1282
- chunk_index: i,
1283
- total_chunks: totalChunks,
1284
- data: chunk.toString("base64")
1285
- };
1286
- this.wsClient.send(msg);
1287
- }
1288
- const complete = {
1289
- type: "transfer_upload_complete",
1290
- transfer_id: transferId
1291
- };
1292
- this.wsClient.send(complete);
1293
- log.info(`ZIP pushed to Worker: transfer=${transferId.slice(0, 8)}... (${totalChunks} chunks)`);
1389
+ log.info(`WebRTC transfer registered: transfer=${offer.transfer_id.slice(0, 8)}... (${(zipBuffer.length / 1024).toFixed(1)} KB in memory)`);
1294
1390
  }
1295
1391
  handleRtcSignalRelay(msg) {
1296
1392
  const entry = this.pendingTransfers.get(msg.transfer_id);
@@ -4561,7 +4657,6 @@ function registerDiscoverCommand(program2) {
4561
4657
 
4562
4658
  // src/commands/call.ts
4563
4659
  import { readFileSync as readFileSync2, writeFileSync as writeFileSync3, mkdirSync as mkdirSync5 } from "fs";
4564
- import { createHash as createHash2 } from "crypto";
4565
4660
  import { execSync as execSync4 } from "child_process";
4566
4661
  import { join as join11 } from "path";
4567
4662
  var DEFAULT_BASE_URL4 = "https://agents.hot";
@@ -4595,74 +4690,101 @@ function handleError2(err) {
4595
4690
  }
4596
4691
  process.exit(1);
4597
4692
  }
4598
- async function downloadTransferFiles(agentId, offer, token, outputDir, json) {
4599
- const url = `${DEFAULT_BASE_URL4}/api/agents/${agentId}/transfer/${offer.transfer_id}`;
4693
+ async function webrtcDownload(agentId, offer, token, outputDir, json) {
4600
4694
  if (!json) {
4601
- log.info(`Downloading ${offer.file_count} file(s) (${(offer.zip_size / 1024).toFixed(1)} KB)...`);
4695
+ log.info(`[WebRTC] Downloading ${offer.file_count} file(s) (${(offer.zip_size / 1024).toFixed(1)} KB)...`);
4602
4696
  }
4603
- await sleep6(1e3);
4604
- let lastError = null;
4605
- for (let attempt = 0; attempt < 5; attempt++) {
4697
+ const receiver = new FileReceiver(offer.zip_size, offer.zip_sha256);
4698
+ const exchangeSignals = async (signal) => {
4606
4699
  try {
4607
- const res = await fetch(url, {
4608
- headers: { Authorization: `Bearer ${token}` }
4700
+ const res = await fetch(`${DEFAULT_BASE_URL4}/api/agents/${agentId}/rtc-signal`, {
4701
+ method: "POST",
4702
+ headers: {
4703
+ Authorization: `Bearer ${token}`,
4704
+ "Content-Type": "application/json"
4705
+ },
4706
+ body: JSON.stringify({
4707
+ transfer_id: offer.transfer_id,
4708
+ signal_type: signal.signal_type,
4709
+ payload: signal.payload
4710
+ })
4609
4711
  });
4610
- if (res.status === 202) {
4611
- await sleep6(2e3);
4612
- continue;
4613
- }
4614
- if (res.status === 404) {
4615
- if (attempt < 4) {
4616
- await sleep6(2e3);
4617
- continue;
4712
+ if (res.ok) {
4713
+ const { signals } = await res.json();
4714
+ for (const s of signals) {
4715
+ await receiver.handleSignal(s);
4618
4716
  }
4619
- log.warn("Transfer expired or not found");
4620
- return;
4621
- }
4622
- if (!res.ok) {
4623
- log.warn(`Download failed: HTTP ${res.status}`);
4624
- return;
4625
- }
4626
- const arrayBuffer = await res.arrayBuffer();
4627
- const zipBuffer = Buffer.from(arrayBuffer);
4628
- const hash = createHash2("sha256").update(zipBuffer).digest("hex");
4629
- if (hash !== offer.zip_sha256) {
4630
- log.warn(`SHA-256 mismatch: expected ${offer.zip_sha256.slice(0, 12)}... got ${hash.slice(0, 12)}...`);
4631
- return;
4632
4717
  }
4633
- mkdirSync5(outputDir, { recursive: true });
4634
- const zipPath = join11(outputDir, `.transfer-${offer.transfer_id.slice(0, 8)}.zip`);
4635
- writeFileSync3(zipPath, zipBuffer);
4718
+ } catch {
4719
+ }
4720
+ };
4721
+ receiver.onSignal(exchangeSignals);
4722
+ await receiver.createOffer();
4723
+ const poll = async () => {
4724
+ for (let i = 0; i < 20; i++) {
4725
+ await sleep6(500);
4636
4726
  try {
4637
- execSync4(`unzip -o -q "${zipPath}" -d "${outputDir}"`);
4638
- try {
4639
- execSync4(`rm "${zipPath}"`);
4640
- } catch {
4727
+ const res = await fetch(`${DEFAULT_BASE_URL4}/api/agents/${agentId}/rtc-signal`, {
4728
+ method: "POST",
4729
+ headers: {
4730
+ Authorization: `Bearer ${token}`,
4731
+ "Content-Type": "application/json"
4732
+ },
4733
+ body: JSON.stringify({
4734
+ transfer_id: offer.transfer_id,
4735
+ signal_type: "poll",
4736
+ payload: ""
4737
+ })
4738
+ });
4739
+ if (res.ok) {
4740
+ const { signals } = await res.json();
4741
+ for (const s of signals) {
4742
+ await receiver.handleSignal(s);
4743
+ }
4641
4744
  }
4642
4745
  } catch {
4643
- log.warn(`Failed to extract ZIP. Saved to: ${zipPath}`);
4644
- return;
4645
4746
  }
4646
- if (json) {
4647
- console.log(JSON.stringify({
4648
- type: "files_downloaded",
4649
- file_count: offer.file_count,
4650
- output_dir: outputDir,
4651
- zip_size: zipBuffer.length,
4652
- sha256_verified: true
4653
- }));
4654
- } else {
4655
- log.success(`${offer.file_count} file(s) extracted to ${outputDir}`);
4747
+ }
4748
+ };
4749
+ try {
4750
+ const [zipBuffer] = await Promise.all([
4751
+ receiver.waitForCompletion(3e4),
4752
+ poll()
4753
+ ]);
4754
+ mkdirSync5(outputDir, { recursive: true });
4755
+ const zipPath = join11(outputDir, ".transfer.zip");
4756
+ writeFileSync3(zipPath, zipBuffer);
4757
+ try {
4758
+ execSync4(`unzip -o -q "${zipPath}" -d "${outputDir}"`);
4759
+ try {
4760
+ execSync4(`rm "${zipPath}"`);
4761
+ } catch {
4656
4762
  }
4763
+ } catch {
4764
+ log.warn(`Failed to extract ZIP. Saved to: ${zipPath}`);
4657
4765
  return;
4658
- } catch (err) {
4659
- lastError = err;
4660
- if (attempt < 4) {
4661
- await sleep6(2e3);
4662
- }
4663
4766
  }
4767
+ if (json) {
4768
+ console.log(JSON.stringify({
4769
+ type: "files_downloaded",
4770
+ file_count: offer.file_count,
4771
+ output_dir: outputDir,
4772
+ zip_size: zipBuffer.length,
4773
+ sha256_verified: true
4774
+ }));
4775
+ } else {
4776
+ log.success(`[WebRTC] ${offer.file_count} file(s) extracted to ${outputDir}`);
4777
+ }
4778
+ } catch (err) {
4779
+ const msg = err.message;
4780
+ if (json) {
4781
+ console.log(JSON.stringify({ type: "file_transfer_failed", error: msg }));
4782
+ } else {
4783
+ log.warn(`[WebRTC] File transfer failed: ${msg}`);
4784
+ }
4785
+ } finally {
4786
+ receiver.close();
4664
4787
  }
4665
- log.warn(`Download failed after retries: ${lastError?.message || "unknown error"}`);
4666
4788
  }
4667
4789
  async function asyncCall(opts) {
4668
4790
  const selfAgentId = process.env.AGENT_BRIDGE_AGENT_ID;
@@ -4757,7 +4879,7 @@ async function asyncCall(opts) {
4757
4879
  }
4758
4880
  if (offer && opts.withFiles) {
4759
4881
  const outputDir = opts.outputFile ? join11(opts.outputFile, "..", "files") : join11(process.cwd(), "agent-output");
4760
- await downloadTransferFiles(opts.id, offer, opts.token, outputDir, opts.json);
4882
+ await webrtcDownload(opts.id, offer, opts.token, outputDir, opts.json);
4761
4883
  }
4762
4884
  if (!opts.json) {
4763
4885
  log.info(`${GRAY}Rate this call: agent-mesh rate ${call_id} <1-5> --agent ${opts.id}${RESET}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@annals/agent-mesh",
3
- "version": "0.17.6",
3
+ "version": "0.17.7",
4
4
  "description": "CLI bridge connecting local AI agents to the Agents.Hot platform",
5
5
  "type": "module",
6
6
  "bin": {