@annals/agent-mesh 0.16.12 → 0.17.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.
Files changed (2) hide show
  1. package/dist/index.js +252 -296
  2. package/package.json +2 -1
package/dist/index.js CHANGED
@@ -269,6 +269,106 @@ var BridgeWSClient = class extends EventEmitter {
269
269
  // src/bridge/manager.ts
270
270
  import { BridgeErrorCode } from "@annals/bridge-protocol";
271
271
 
272
+ // src/utils/webrtc-transfer.ts
273
+ import { createHash } from "crypto";
274
+ var ICE_SERVERS = ["stun:stun.l.google.com:19302"];
275
+ var CHUNK_SIZE = 64 * 1024;
276
+ var ndcModule;
277
+ async function loadNdc() {
278
+ if (ndcModule !== void 0) return ndcModule;
279
+ try {
280
+ ndcModule = await import("node-datachannel");
281
+ return ndcModule;
282
+ } catch {
283
+ log.warn("node-datachannel not available \u2014 WebRTC file transfer disabled");
284
+ ndcModule = null;
285
+ return null;
286
+ }
287
+ }
288
+ var FileSender = class {
289
+ peer = null;
290
+ transferId;
291
+ zipBuffer;
292
+ pendingCandidates = [];
293
+ signalCallback = null;
294
+ closed = false;
295
+ constructor(transferId, zipBuffer) {
296
+ this.transferId = transferId;
297
+ this.zipBuffer = zipBuffer;
298
+ }
299
+ onSignal(cb) {
300
+ this.signalCallback = cb;
301
+ }
302
+ async handleSignal(signal) {
303
+ const ndc = await loadNdc();
304
+ if (!ndc || this.closed) return;
305
+ if (!this.peer) {
306
+ this.peer = new ndc.PeerConnection(`sender-${this.transferId}`, {
307
+ iceServers: ICE_SERVERS
308
+ });
309
+ this.peer.onLocalDescription((sdp, type) => {
310
+ this.signalCallback?.({ signal_type: type, payload: sdp });
311
+ });
312
+ this.peer.onLocalCandidate((candidate, mid) => {
313
+ this.signalCallback?.({
314
+ signal_type: "candidate",
315
+ payload: JSON.stringify({ candidate, mid })
316
+ });
317
+ });
318
+ this.peer.onDataChannel((dc) => {
319
+ dc.onOpen(() => {
320
+ void this.sendZip(dc);
321
+ });
322
+ });
323
+ }
324
+ if (signal.signal_type === "offer" || signal.signal_type === "answer") {
325
+ this.peer.setRemoteDescription(signal.payload, signal.signal_type);
326
+ for (const c of this.pendingCandidates) {
327
+ this.peer.addRemoteCandidate(c.candidate, c.mid);
328
+ }
329
+ this.pendingCandidates = [];
330
+ } else if (signal.signal_type === "candidate") {
331
+ const { candidate, mid } = JSON.parse(signal.payload);
332
+ if (this.peer.remoteDescription()) {
333
+ this.peer.addRemoteCandidate(candidate, mid);
334
+ } else {
335
+ this.pendingCandidates.push({ candidate, mid });
336
+ }
337
+ }
338
+ }
339
+ async sendZip(dc) {
340
+ try {
341
+ dc.sendMessage(JSON.stringify({
342
+ type: "header",
343
+ transfer_id: this.transferId,
344
+ zip_size: this.zipBuffer.length
345
+ }));
346
+ let offset = 0;
347
+ while (offset < this.zipBuffer.length) {
348
+ const end = Math.min(offset + CHUNK_SIZE, this.zipBuffer.length);
349
+ const chunk = this.zipBuffer.subarray(offset, end);
350
+ dc.sendMessageBinary(chunk);
351
+ offset = end;
352
+ }
353
+ dc.sendMessage(JSON.stringify({ type: "complete" }));
354
+ log.info(`[WebRTC] Sent ${this.zipBuffer.length} bytes in ${Math.ceil(this.zipBuffer.length / CHUNK_SIZE)} chunks`);
355
+ } catch (err) {
356
+ log.error(`[WebRTC] Send failed: ${err}`);
357
+ }
358
+ }
359
+ close() {
360
+ this.closed = true;
361
+ try {
362
+ this.peer?.close();
363
+ } catch {
364
+ }
365
+ this.peer = null;
366
+ }
367
+ };
368
+ function sha256Hex(data) {
369
+ return createHash("sha256").update(data).digest("hex");
370
+ }
371
+
272
372
  // src/bridge/session-pool.ts
273
373
  var SessionPool = class {
274
374
  sessions = /* @__PURE__ */ new Map();
@@ -688,6 +788,8 @@ var BridgeManager = class {
688
788
  requestDispatches = /* @__PURE__ */ new Map();
689
789
  cleanupTimer = null;
690
790
  runtimeQueue;
791
+ /** Pending WebRTC file transfers: transfer_id → FileSender + cleanup timer */
792
+ pendingTransfers = /* @__PURE__ */ new Map();
691
793
  constructor(opts) {
692
794
  this.wsClient = opts.wsClient;
693
795
  this.adapter = opts.adapter;
@@ -715,6 +817,7 @@ var BridgeManager = class {
715
817
  this.wiredSessions.clear();
716
818
  this.sessionLastSeenAt.clear();
717
819
  this.cleanupRequestDispatches("shutdown");
820
+ this.cleanupPendingTransfers();
718
821
  log.info("Bridge manager stopped");
719
822
  }
720
823
  /**
@@ -746,6 +849,9 @@ var BridgeManager = class {
746
849
  break;
747
850
  case "registered":
748
851
  break;
852
+ case "rtc_signal_relay":
853
+ this.handleRtcSignalRelay(msg);
854
+ break;
749
855
  default:
750
856
  log.warn(`Unknown message type from worker: ${msg.type}`);
751
857
  }
@@ -811,7 +917,7 @@ var BridgeManager = class {
811
917
  }
812
918
  async dispatchWithLocalQueue(opts) {
813
919
  const { msg, handle, requestKey } = opts;
814
- const { session_id, request_id, content, attachments, upload_url, upload_token, client_id, platform_task } = msg;
920
+ const { session_id, request_id, content, attachments, client_id, with_files } = msg;
815
921
  const state = this.requestDispatches.get(requestKey);
816
922
  if (!state) return;
817
923
  try {
@@ -830,9 +936,8 @@ var BridgeManager = class {
830
936
  await this.releaseRequestLease(session_id, request_id, "cancel");
831
937
  return;
832
938
  }
833
- const uploadCredentials = upload_url && upload_token ? { uploadUrl: upload_url, uploadToken: upload_token } : void 0;
834
939
  try {
835
- handle.send(content, attachments, uploadCredentials, client_id, platform_task);
940
+ handle.send(content, attachments, client_id, with_files);
836
941
  this.sessionLastSeenAt.set(session_id, Date.now());
837
942
  } catch (err) {
838
943
  log.error(`Failed to send to adapter: ${err}`);
@@ -893,20 +998,24 @@ var BridgeManager = class {
893
998
  handle.onDone((payload) => {
894
999
  void this.releaseRequestLease(sessionId, requestRef.requestId, "done");
895
1000
  const attachments = payload?.attachments;
896
- const fileManifest = payload?.fileManifest;
1001
+ const fileTransferOffer = payload?.fileTransferOffer;
1002
+ const zipBuffer = payload?.zipBuffer;
1003
+ if (fileTransferOffer && zipBuffer) {
1004
+ this.registerPendingTransfer(fileTransferOffer, zipBuffer);
1005
+ }
897
1006
  const done = {
898
1007
  type: "done",
899
1008
  session_id: sessionId,
900
1009
  request_id: requestRef.requestId,
901
1010
  ...attachments && attachments.length > 0 && { attachments },
902
- ...fileManifest && fileManifest.length > 0 && { file_manifest: fileManifest },
1011
+ ...fileTransferOffer && { file_transfer_offer: fileTransferOffer },
903
1012
  ...fullResponseBuffer && { result: fullResponseBuffer }
904
1013
  };
905
1014
  this.trackRequest(sessionId, requestRef.requestId, "done");
906
1015
  this.wsClient.send(done);
907
1016
  fullResponseBuffer = "";
908
1017
  this.sessionLastSeenAt.set(sessionId, Date.now());
909
- const fileInfo = attachments && attachments.length > 0 ? ` (${attachments.length} files)` : "";
1018
+ const fileInfo = fileTransferOffer ? ` (${fileTransferOffer.file_count} files, transfer=${fileTransferOffer.transfer_id.slice(0, 8)}...)` : "";
910
1019
  log.info(`Request done: session=${sessionId.slice(0, 8)}... request=${requestRef.requestId.slice(0, 8)}...${fileInfo}`);
911
1020
  });
912
1021
  handle.onError((err) => {
@@ -1087,6 +1196,50 @@ var BridgeManager = class {
1087
1196
  }
1088
1197
  }
1089
1198
  }
1199
+ // ========================================================
1200
+ // WebRTC signaling relay
1201
+ // ========================================================
1202
+ registerPendingTransfer(offer, zipBuffer) {
1203
+ const sender = new FileSender(offer.transfer_id, zipBuffer);
1204
+ sender.onSignal((signal) => {
1205
+ const entry = this.pendingTransfers.get(offer.transfer_id);
1206
+ if (!entry) return;
1207
+ const rtcSignal = {
1208
+ type: "rtc_signal",
1209
+ transfer_id: offer.transfer_id,
1210
+ target_agent_id: entry.targetAgentId || "",
1211
+ signal_type: signal.signal_type,
1212
+ payload: signal.payload
1213
+ };
1214
+ this.wsClient.send(rtcSignal);
1215
+ });
1216
+ const timer = setTimeout(() => {
1217
+ sender.close();
1218
+ this.pendingTransfers.delete(offer.transfer_id);
1219
+ log.debug(`Transfer ${offer.transfer_id.slice(0, 8)}... expired`);
1220
+ }, 5 * 6e4);
1221
+ timer.unref?.();
1222
+ this.pendingTransfers.set(offer.transfer_id, { sender, timer });
1223
+ }
1224
+ handleRtcSignalRelay(msg) {
1225
+ const entry = this.pendingTransfers.get(msg.transfer_id);
1226
+ if (!entry) {
1227
+ log.debug(`No pending transfer for ${msg.transfer_id.slice(0, 8)}...`);
1228
+ return;
1229
+ }
1230
+ entry.targetAgentId = msg.from_agent_id;
1231
+ void entry.sender.handleSignal({
1232
+ signal_type: msg.signal_type,
1233
+ payload: msg.payload
1234
+ });
1235
+ }
1236
+ cleanupPendingTransfers() {
1237
+ for (const [id, entry] of this.pendingTransfers) {
1238
+ clearTimeout(entry.timer);
1239
+ entry.sender.close();
1240
+ }
1241
+ this.pendingTransfers.clear();
1242
+ }
1090
1243
  updateSessionCount() {
1091
1244
  this.wsClient.setActiveSessions(this.pool.size);
1092
1245
  }
@@ -1423,7 +1576,7 @@ function createClientWorkspace(projectPath, clientId) {
1423
1576
  }
1424
1577
 
1425
1578
  // src/adapters/claude.ts
1426
- import { readFile as readFile2, writeFile, mkdir, stat as stat2 } from "fs/promises";
1579
+ import { writeFile, mkdir, stat as stat2 } from "fs/promises";
1427
1580
  import { join as join5, relative as relative3, basename } from "path";
1428
1581
 
1429
1582
  // src/utils/auto-upload.ts
@@ -1440,23 +1593,6 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([
1440
1593
  "coverage",
1441
1594
  ".turbo"
1442
1595
  ]);
1443
- var MIME_MAP = {
1444
- md: "text/markdown",
1445
- txt: "text/plain",
1446
- json: "application/json",
1447
- js: "text/javascript",
1448
- ts: "text/typescript",
1449
- py: "text/x-python",
1450
- html: "text/html",
1451
- css: "text/css",
1452
- csv: "text/csv",
1453
- png: "image/png",
1454
- jpg: "image/jpeg",
1455
- jpeg: "image/jpeg",
1456
- gif: "image/gif",
1457
- svg: "image/svg+xml",
1458
- pdf: "application/pdf"
1459
- };
1460
1596
  async function collectRealFiles(dir, maxFiles = Infinity) {
1461
1597
  const files = [];
1462
1598
  const walk = async (d) => {
@@ -1641,7 +1777,6 @@ var CLAUDE_RUNTIME_ALLOW_WRITE_PATHS = [
1641
1777
  `${HOME_DIR}/.claude.json.tmp`,
1642
1778
  `${HOME_DIR}/.local/state/claude`
1643
1779
  ];
1644
- var MAX_UPLOAD_FILE_SIZE = 200 * 1024 * 1024;
1645
1780
  var MAX_COLLECT_FILES = 5e3;
1646
1781
  var DEFAULT_ZIP_MAX_BYTES = 200 * 1024 * 1024;
1647
1782
  function resolveIdleTimeoutMs() {
@@ -1675,29 +1810,23 @@ var ClaudeSession = class {
1675
1810
  activeToolName = null;
1676
1811
  /** Track current content block type to distinguish thinking vs text deltas */
1677
1812
  currentBlockType = null;
1678
- /** Upload credentials provided by the platform for auto-uploading output files */
1679
- uploadCredentials = null;
1680
1813
  /** Per-client workspace path (symlink-based), set on each send() */
1681
1814
  currentWorkspace;
1682
- send(message, attachments, uploadCredentials, clientId, platformTask) {
1815
+ /** Whether caller requested file transfer */
1816
+ withFiles = false;
1817
+ send(message, attachments, clientId, withFiles) {
1683
1818
  this.resetIdleTimer();
1684
1819
  this.doneFired = false;
1685
1820
  this.chunksEmitted = false;
1686
1821
  this.activeToolCallId = null;
1687
1822
  this.activeToolName = null;
1688
1823
  this.currentBlockType = null;
1689
- if (uploadCredentials) {
1690
- this.uploadCredentials = uploadCredentials;
1691
- }
1824
+ this.withFiles = withFiles || false;
1692
1825
  if (clientId && this.config.project) {
1693
1826
  this.currentWorkspace = createClientWorkspace(this.config.project, clientId);
1694
1827
  } else {
1695
1828
  this.currentWorkspace = void 0;
1696
1829
  }
1697
- if (platformTask) {
1698
- void this.runPlatformTask(platformTask);
1699
- return;
1700
- }
1701
1830
  const args = ["-p", message, "--continue", "--output-format", "stream-json", "--verbose", "--include-partial-messages", "--dangerously-skip-permissions"];
1702
1831
  void this.downloadAttachments(attachments).then(() => {
1703
1832
  this.launchProcess(args);
@@ -1786,145 +1915,64 @@ var ClaudeSession = class {
1786
1915
  getWorkspaceRoot() {
1787
1916
  return this.currentWorkspace || this.config.project || process.cwd();
1788
1917
  }
1789
- normalizeRelativePath(inputPath) {
1790
- const normalized = inputPath.replace(/\\/g, "/").replace(/^\/+/, "");
1791
- if (!normalized || normalized.includes("\0")) return null;
1792
- if (normalized.split("/").some((seg) => seg === "..")) return null;
1793
- return normalized;
1794
- }
1795
- async runPlatformTask(task) {
1796
- try {
1797
- if (!this.uploadCredentials) {
1798
- throw new Error("Missing upload credentials for platform task");
1799
- }
1800
- if (task.type === "upload_file") {
1801
- await this.runUploadFileTask(task.path);
1802
- } else if (task.type === "upload_all_zip") {
1803
- await this.runUploadAllZipTask(task.zip_name, task.max_bytes);
1804
- } else {
1805
- throw new Error(`Unsupported platform task: ${task.type || "unknown"}`);
1806
- }
1807
- this.doneFired = true;
1808
- } catch (error) {
1809
- this.emitError(new Error(`Platform task failed: ${error instanceof Error ? error.message : String(error)}`));
1810
- }
1811
- }
1812
- async runUploadFileTask(path) {
1813
- const workspaceRoot = this.getWorkspaceRoot();
1814
- const relPath = this.normalizeRelativePath(path);
1815
- if (!relPath) {
1816
- throw new Error("Invalid file path");
1817
- }
1818
- const absPath = join5(workspaceRoot, relPath);
1819
- const info = await stat2(absPath);
1820
- if (!info.isFile()) {
1821
- throw new Error("Path is not a regular file");
1822
- }
1823
- const buffer = await readFile2(absPath);
1824
- if (buffer.length === 0 || buffer.length > MAX_UPLOAD_FILE_SIZE) {
1825
- throw new Error(`File size out of bounds: ${buffer.length}`);
1826
- }
1827
- const uploaded = await this.uploadBuffer(relPath, buffer);
1828
- for (const cb of this.doneCallbacks) cb({ attachments: [uploaded] });
1829
- }
1830
- async runUploadAllZipTask(zipName, maxBytes) {
1831
- const workspaceRoot = this.getWorkspaceRoot();
1832
- const files = await this.collectWorkspaceFiles(workspaceRoot);
1833
- if (files.length === 0) {
1834
- throw new Error("No files found");
1835
- }
1836
- const maxZipBytes = typeof maxBytes === "number" && Number.isFinite(maxBytes) && maxBytes > 0 ? Math.floor(maxBytes) : DEFAULT_ZIP_MAX_BYTES;
1837
- const entries = [];
1838
- let totalBytes = 0;
1839
- for (const absPath of files) {
1840
- this.resetIdleTimer();
1841
- const relPath = relative3(workspaceRoot, absPath).replace(/\\/g, "/");
1842
- if (!relPath || relPath.startsWith("..")) continue;
1843
- const buffer = await readFile2(absPath);
1844
- if (buffer.length === 0) continue;
1845
- totalBytes += buffer.length;
1846
- if (totalBytes > maxZipBytes) {
1847
- throw new Error(`ZIP_TOO_LARGE:${totalBytes}`);
1848
- }
1849
- entries.push({ path: relPath, data: buffer });
1850
- }
1851
- const zipBuffer = createZipBuffer(entries);
1852
- if (zipBuffer.length > maxZipBytes) {
1853
- throw new Error(`ZIP_TOO_LARGE:${zipBuffer.length}`);
1854
- }
1855
- const safeZipName = this.normalizeRelativePath(zipName || "") || `workspace-${this.sessionId.slice(0, 8)}.zip`;
1856
- const uploaded = await this.uploadBuffer(safeZipName.endsWith(".zip") ? safeZipName : `${safeZipName}.zip`, zipBuffer);
1857
- for (const cb of this.doneCallbacks) cb({ attachments: [uploaded] });
1858
- }
1859
- async uploadBuffer(filename, buffer) {
1860
- const creds = this.uploadCredentials;
1861
- if (!creds) {
1862
- throw new Error("Upload credentials missing");
1863
- }
1864
- const response = await fetch(creds.uploadUrl, {
1865
- method: "POST",
1866
- headers: {
1867
- "X-Upload-Token": creds.uploadToken,
1868
- "Content-Type": "application/json"
1869
- },
1870
- body: JSON.stringify({
1871
- filename,
1872
- content: buffer.toString("base64")
1873
- })
1874
- });
1875
- if (!response.ok) {
1876
- throw new Error(`Upload failed (${response.status}) for ${filename}`);
1877
- }
1878
- const payload = await response.json();
1879
- if (typeof payload.url !== "string" || payload.url.length === 0) {
1880
- throw new Error(`Upload response missing url for ${filename}`);
1881
- }
1882
- const ext = filename.split(".").pop()?.toLowerCase() || "";
1883
- return {
1884
- name: filename,
1885
- url: payload.url,
1886
- type: MIME_MAP[ext] || "application/octet-stream"
1887
- };
1888
- }
1889
1918
  async collectWorkspaceFiles(workspaceRoot) {
1890
1919
  return collectRealFiles(workspaceRoot, MAX_COLLECT_FILES);
1891
1920
  }
1892
- async collectWorkspaceManifest(workspaceRoot) {
1921
+ /**
1922
+ * Collect workspace files into a ZIP buffer + compute SHA-256.
1923
+ * Only called when with_files is true.
1924
+ */
1925
+ async createWorkspaceZip(workspaceRoot) {
1893
1926
  const files = await this.collectWorkspaceFiles(workspaceRoot);
1894
- const manifest = [];
1927
+ if (files.length === 0) return null;
1928
+ const entries = [];
1929
+ let totalBytes = 0;
1895
1930
  for (const absPath of files) {
1896
1931
  const relPath = relative3(workspaceRoot, absPath).replace(/\\/g, "/");
1897
1932
  if (!relPath || relPath.startsWith("..")) continue;
1898
1933
  try {
1899
1934
  const fileStat = await stat2(absPath);
1900
1935
  if (!fileStat.isFile()) continue;
1901
- const ext = relPath.split(".").pop()?.toLowerCase() || "";
1902
- manifest.push({
1903
- path: relPath,
1904
- size: fileStat.size,
1905
- mtime_ms: Math.floor(fileStat.mtimeMs),
1906
- type: MIME_MAP[ext] || "application/octet-stream"
1907
- });
1936
+ const { readFile: readFile4 } = await import("fs/promises");
1937
+ const buffer = await readFile4(absPath);
1938
+ if (buffer.length === 0) continue;
1939
+ totalBytes += buffer.length;
1940
+ if (totalBytes > DEFAULT_ZIP_MAX_BYTES) {
1941
+ log.warn(`Workspace exceeds ${DEFAULT_ZIP_MAX_BYTES / 1024 / 1024}MB limit, truncating`);
1942
+ break;
1943
+ }
1944
+ entries.push({ path: relPath, data: buffer });
1908
1945
  } catch {
1909
1946
  }
1910
1947
  }
1911
- manifest.sort((a, b) => a.path.localeCompare(b.path));
1912
- return manifest;
1948
+ if (entries.length === 0) return null;
1949
+ const zipBuffer = createZipBuffer(entries);
1950
+ return { zipBuffer, fileCount: entries.length };
1913
1951
  }
1914
1952
  async finalizeDone(attachments) {
1915
- const workspaceRoot = this.getWorkspaceRoot();
1916
- let fileManifest;
1917
- try {
1918
- fileManifest = await this.collectWorkspaceManifest(workspaceRoot);
1919
- } catch (error) {
1920
- log.warn(`Manifest collection failed: ${error}`);
1921
- }
1922
1953
  const payload = {};
1923
1954
  if (attachments && attachments.length > 0) {
1924
1955
  payload.attachments = attachments;
1925
1956
  }
1926
- if (fileManifest) {
1927
- payload.fileManifest = fileManifest;
1957
+ if (this.withFiles) {
1958
+ const workspaceRoot = this.getWorkspaceRoot();
1959
+ try {
1960
+ const result = await this.createWorkspaceZip(workspaceRoot);
1961
+ if (result) {
1962
+ const transferId = crypto.randomUUID();
1963
+ const zipSha256 = sha256Hex(result.zipBuffer);
1964
+ payload.fileTransferOffer = {
1965
+ transfer_id: transferId,
1966
+ zip_size: result.zipBuffer.length,
1967
+ zip_sha256: zipSha256,
1968
+ file_count: result.fileCount
1969
+ };
1970
+ payload.zipBuffer = result.zipBuffer;
1971
+ log.info(`[WebRTC] ZIP ready: ${result.fileCount} files, ${result.zipBuffer.length} bytes, transfer=${transferId.slice(0, 8)}...`);
1972
+ }
1973
+ } catch (error) {
1974
+ log.warn(`ZIP creation failed: ${error}`);
1975
+ }
1928
1976
  }
1929
1977
  for (const cb of this.doneCallbacks) cb(payload);
1930
1978
  }
@@ -2079,20 +2127,20 @@ var ClaudeSession = class {
2079
2127
  for (const cb of this.toolCallbacks) cb(event);
2080
2128
  }
2081
2129
  emitTextAsChunks(text) {
2082
- const CHUNK_SIZE = 60;
2083
- if (text.length <= CHUNK_SIZE) {
2130
+ const CHUNK_SIZE2 = 60;
2131
+ if (text.length <= CHUNK_SIZE2) {
2084
2132
  this.emitChunk(text);
2085
2133
  return;
2086
2134
  }
2087
2135
  let pos = 0;
2088
2136
  while (pos < text.length) {
2089
- let end = Math.min(pos + CHUNK_SIZE, text.length);
2137
+ let end = Math.min(pos + CHUNK_SIZE2, text.length);
2090
2138
  if (end < text.length) {
2091
2139
  const slice = text.slice(pos, end + 20);
2092
2140
  const breakPoints = ["\n", "\u3002", "\uFF01", "\uFF1F", ". ", "! ", "? ", "\uFF0C", ", ", " "];
2093
2141
  for (const bp of breakPoints) {
2094
- const idx = slice.indexOf(bp, CHUNK_SIZE - 20);
2095
- if (idx >= 0 && idx < CHUNK_SIZE + 20) {
2142
+ const idx = slice.indexOf(bp, CHUNK_SIZE2 - 20);
2143
+ if (idx >= 0 && idx < CHUNK_SIZE2 + 20) {
2096
2144
  end = pos + idx + bp.length;
2097
2145
  break;
2098
2146
  }
@@ -3393,8 +3441,8 @@ async function asyncChat(opts) {
3393
3441
  `);
3394
3442
  }
3395
3443
  }
3396
- if (task.file_manifest) {
3397
- process.stdout.write(`${GRAY}[manifest: ${task.file_manifest.length} files]${RESET}
3444
+ if (task.file_transfer_offer) {
3445
+ process.stdout.write(`${GRAY}[files: ${task.file_transfer_offer.file_count} available via WebRTC]${RESET}
3398
3446
  `);
3399
3447
  }
3400
3448
  return;
@@ -3599,11 +3647,11 @@ function registerChatCommand(program2) {
3599
3647
  }
3600
3648
 
3601
3649
  // src/commands/skills.ts
3602
- import { readFile as readFile4, writeFile as writeFile3, readdir as readdir2, mkdir as mkdir2, rm, symlink, unlink } from "fs/promises";
3650
+ import { readFile as readFile3, writeFile as writeFile3, readdir as readdir2, mkdir as mkdir2, rm, symlink, unlink } from "fs/promises";
3603
3651
  import { join as join9, resolve, relative as relative4 } from "path";
3604
3652
 
3605
3653
  // src/utils/skill-parser.ts
3606
- import { readFile as readFile3, writeFile as writeFile2, stat as stat3 } from "fs/promises";
3654
+ import { readFile as readFile2, writeFile as writeFile2, stat as stat3 } from "fs/promises";
3607
3655
  import { join as join8 } from "path";
3608
3656
  function parseSkillMd(raw) {
3609
3657
  const trimmed = raw.trimStart();
@@ -3688,7 +3736,7 @@ function parseSkillMd(raw) {
3688
3736
  async function loadSkillManifest(dir) {
3689
3737
  const skillMdPath = join8(dir, "SKILL.md");
3690
3738
  try {
3691
- const raw = await readFile3(skillMdPath, "utf-8");
3739
+ const raw = await readFile2(skillMdPath, "utf-8");
3692
3740
  const { frontmatter } = parseSkillMd(raw);
3693
3741
  const name = frontmatter.name;
3694
3742
  if (!name) {
@@ -3721,7 +3769,7 @@ async function pathExists(p) {
3721
3769
  }
3722
3770
  }
3723
3771
  async function updateFrontmatterField(filePath, field, value) {
3724
- const raw = await readFile3(filePath, "utf-8");
3772
+ const raw = await readFile2(filePath, "utf-8");
3725
3773
  const trimmed = raw.trimStart();
3726
3774
  if (!trimmed.startsWith("---")) {
3727
3775
  throw new Error("SKILL.md has no frontmatter block");
@@ -3854,7 +3902,7 @@ async function packSkill(dir, manifest) {
3854
3902
  for (const relPath of fileList) {
3855
3903
  const absPath = join9(dir, relPath);
3856
3904
  try {
3857
- const data = await readFile4(absPath);
3905
+ const data = await readFile3(absPath);
3858
3906
  entries.push({ path: relPath.replace(/\\/g, "/"), data });
3859
3907
  } catch {
3860
3908
  slog.warn(`Skipping unreadable file: ${relPath}`);
@@ -3939,7 +3987,7 @@ function registerSkillsCommand(program2) {
3939
3987
  await mkdir2(dir, { recursive: true });
3940
3988
  const skillMdPath = join9(dir, "SKILL.md");
3941
3989
  if (await pathExists(skillMdPath)) {
3942
- const raw = await readFile4(skillMdPath, "utf-8");
3990
+ const raw = await readFile3(skillMdPath, "utf-8");
3943
3991
  const { frontmatter } = parseSkillMd(raw);
3944
3992
  if (frontmatter.name) {
3945
3993
  slog.info(`SKILL.md already exists with name: ${frontmatter.name}`);
@@ -4012,7 +4060,7 @@ function registerSkillsCommand(program2) {
4012
4060
  if (opts.name) manifest.name = opts.name;
4013
4061
  if (opts.version) manifest.version = opts.version;
4014
4062
  if (opts.private !== void 0) manifest.private = opts.private;
4015
- content = await readFile4(join9(dir, manifest.main || "SKILL.md"), "utf-8");
4063
+ content = await readFile3(join9(dir, manifest.main || "SKILL.md"), "utf-8");
4016
4064
  packResult = await packSkill(dir, manifest);
4017
4065
  slog.info(`Packed ${packResult.files.length} files (${packResult.size} bytes)`);
4018
4066
  }
@@ -4153,7 +4201,7 @@ function registerSkillsCommand(program2) {
4153
4201
  if (!await pathExists(skillMdPath)) {
4154
4202
  outputError("not_found", "No SKILL.md found. Run `agent-mesh skills init` first.");
4155
4203
  }
4156
- const raw = await readFile4(skillMdPath, "utf-8");
4204
+ const raw = await readFile3(skillMdPath, "utf-8");
4157
4205
  const { frontmatter } = parseSkillMd(raw);
4158
4206
  const oldVersion = frontmatter.version || "0.0.0";
4159
4207
  const newVersion = bumpVersion(oldVersion, bump);
@@ -4215,7 +4263,7 @@ function registerSkillsCommand(program2) {
4215
4263
  const skillMdPath = join9(targetDir, "SKILL.md");
4216
4264
  let localVersion = "0.0.0";
4217
4265
  if (await pathExists(skillMdPath)) {
4218
- const raw = await readFile4(skillMdPath, "utf-8");
4266
+ const raw = await readFile3(skillMdPath, "utf-8");
4219
4267
  const { frontmatter } = parseSkillMd(raw);
4220
4268
  localVersion = frontmatter.version || "0.0.0";
4221
4269
  }
@@ -4244,7 +4292,7 @@ function registerSkillsCommand(program2) {
4244
4292
  skipped.push({ slug, reason: "no_skill_md" });
4245
4293
  continue;
4246
4294
  }
4247
- const raw = await readFile4(skillMdPath, "utf-8");
4295
+ const raw = await readFile3(skillMdPath, "utf-8");
4248
4296
  const { frontmatter } = parseSkillMd(raw);
4249
4297
  const localVersion = frontmatter.version || "0.0.0";
4250
4298
  const authorLogin = frontmatter.author;
@@ -4313,7 +4361,7 @@ function registerSkillsCommand(program2) {
4313
4361
  const slug = entry.name;
4314
4362
  const skillMdPath = join9(skillsDir, slug, "SKILL.md");
4315
4363
  if (!await pathExists(skillMdPath)) continue;
4316
- const raw = await readFile4(skillMdPath, "utf-8");
4364
+ const raw = await readFile3(skillMdPath, "utf-8");
4317
4365
  const { frontmatter } = parseSkillMd(raw);
4318
4366
  const skillInfo = {
4319
4367
  slug,
@@ -4464,7 +4512,11 @@ async function asyncCall(opts) {
4464
4512
  "Content-Type": "application/json",
4465
4513
  ...selfAgentId ? { "X-Caller-Agent-Id": selfAgentId } : {}
4466
4514
  },
4467
- body: JSON.stringify({ task_description: opts.taskDescription, mode: "async" }),
4515
+ body: JSON.stringify({
4516
+ task_description: opts.taskDescription,
4517
+ mode: "async",
4518
+ ...opts.withFiles ? { with_files: true } : {}
4519
+ }),
4468
4520
  signal: opts.signal
4469
4521
  });
4470
4522
  if (!res.ok) {
@@ -4523,7 +4575,7 @@ async function asyncCall(opts) {
4523
4575
  status: "completed",
4524
4576
  result,
4525
4577
  ...task.attachments?.length ? { attachments: task.attachments } : {},
4526
- ...Array.isArray(task.file_manifest) ? { file_manifest: task.file_manifest } : {},
4578
+ ...task.file_transfer_offer ? { file_transfer_offer: task.file_transfer_offer } : {},
4527
4579
  rate_hint: `POST /api/agents/${opts.id}/rate body: { call_id: "${call_id}", rating: 1-5 }`
4528
4580
  }));
4529
4581
  } else {
@@ -4533,9 +4585,9 @@ async function asyncCall(opts) {
4533
4585
  log.info(` ${GRAY}File:${RESET} ${att.name} ${GRAY}${att.url}${RESET}`);
4534
4586
  }
4535
4587
  }
4536
- const manifest = task.file_manifest;
4537
- if (Array.isArray(manifest)) {
4538
- log.info(` ${GRAY}Manifest:${RESET} ${manifest.length} file(s)`);
4588
+ const offer = task.file_transfer_offer;
4589
+ if (offer) {
4590
+ log.info(` ${GRAY}Files:${RESET} ${offer.file_count} file(s) available via WebRTC`);
4539
4591
  }
4540
4592
  if (session_key) {
4541
4593
  log.info(` ${GRAY}Session:${RESET} ${session_key}`);
@@ -4579,7 +4631,10 @@ async function streamCall(opts) {
4579
4631
  Accept: "text/event-stream",
4580
4632
  ...selfAgentId ? { "X-Caller-Agent-Id": selfAgentId } : {}
4581
4633
  },
4582
- body: JSON.stringify({ task_description: opts.taskDescription }),
4634
+ body: JSON.stringify({
4635
+ task_description: opts.taskDescription,
4636
+ ...opts.withFiles ? { with_files: true } : {}
4637
+ }),
4583
4638
  signal: opts.signal
4584
4639
  });
4585
4640
  if (!res.ok) {
@@ -4662,13 +4717,15 @@ async function streamCall(opts) {
4662
4717
  outputBuffer += delta;
4663
4718
  }
4664
4719
  }
4665
- } else if (event.type === "done" && event.attachments?.length) {
4666
- console.log("");
4667
- for (const att of event.attachments) {
4668
- log.info(` ${GRAY}File:${RESET} ${att.name} ${GRAY}${att.url}${RESET}`);
4720
+ } else if (event.type === "done") {
4721
+ if (event.attachments?.length) {
4722
+ console.log("");
4723
+ for (const att of event.attachments) {
4724
+ log.info(` ${GRAY}File:${RESET} ${att.name} ${GRAY}${att.url}${RESET}`);
4725
+ }
4669
4726
  }
4670
- if (Array.isArray(event.file_manifest)) {
4671
- log.info(` ${GRAY}Manifest:${RESET} ${event.file_manifest.length} file(s)`);
4727
+ if (event.file_transfer_offer) {
4728
+ log.info(` ${GRAY}Files:${RESET} ${event.file_transfer_offer.file_count} file(s) available via WebRTC`);
4672
4729
  }
4673
4730
  } else if (event.type === "error") {
4674
4731
  process.stderr.write(`
@@ -4718,7 +4775,7 @@ Error: ${event.message}
4718
4775
  return { callId, ...sessionKey ? { sessionKey } : {} };
4719
4776
  }
4720
4777
  function registerCallCommand(program2) {
4721
- program2.command("call <agent>").description("Call an agent on the A2A network (default: async polling)").requiredOption("--task <description>", "Task description").option("--input-file <path>", "Read file and append to task description").option("--output-file <path>", "Save response text to file").option("--stream", "Use SSE streaming instead of async polling").option("--json", "Output JSONL events").option("--timeout <seconds>", "Timeout in seconds", "300").option("--rate <rating>", "Rate the agent after call (1-5)", parseInt).action(async (agentInput, opts) => {
4778
+ program2.command("call <agent>").description("Call an agent on the A2A network (default: async polling)").requiredOption("--task <description>", "Task description").option("--input-file <path>", "Read file and append to task description").option("--output-file <path>", "Save response text to file").option("--stream", "Use SSE streaming instead of async polling").option("--with-files", "Request file transfer via WebRTC after task completion").option("--json", "Output JSONL events").option("--timeout <seconds>", "Timeout in seconds", "300").option("--rate <rating>", "Rate the agent after call (1-5)", parseInt).action(async (agentInput, opts) => {
4722
4779
  try {
4723
4780
  const token = loadToken();
4724
4781
  if (!token) {
@@ -4747,7 +4804,8 @@ ${content}`;
4747
4804
  timeoutMs,
4748
4805
  json: opts.json,
4749
4806
  outputFile: opts.outputFile,
4750
- signal: abortController.signal
4807
+ signal: abortController.signal,
4808
+ withFiles: opts.withFiles
4751
4809
  };
4752
4810
  let result;
4753
4811
  if (opts.stream) {
@@ -5214,21 +5272,11 @@ function registerProfileCommand(program2) {
5214
5272
  }
5215
5273
 
5216
5274
  // src/commands/files.ts
5217
- import { writeFileSync as writeFileSync4 } from "fs";
5218
- import { basename as basename2 } from "path";
5219
5275
  function formatBytes(bytes) {
5220
5276
  if (bytes < 1024) return `${bytes}B`;
5221
5277
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
5222
5278
  return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
5223
5279
  }
5224
- function defaultOutputPath(filePath) {
5225
- const name = basename2(filePath || "") || "file";
5226
- return name;
5227
- }
5228
- function defaultZipOutputPath(sessionKey) {
5229
- const suffix = sessionKey.split(":").at(-1) || "session";
5230
- return `session-${suffix}.zip`;
5231
- }
5232
5280
  function handleError7(err) {
5233
5281
  if (err instanceof PlatformApiError) {
5234
5282
  log.error(err.message);
@@ -5241,16 +5289,8 @@ async function resolveTargetAgent(agentInput) {
5241
5289
  const client = createClient();
5242
5290
  return resolveAgentId(agentInput, client);
5243
5291
  }
5244
- async function downloadToFile(url, outputPath) {
5245
- const res = await fetch(url);
5246
- if (!res.ok) {
5247
- throw new Error(`Download failed: HTTP ${res.status}`);
5248
- }
5249
- const buf = Buffer.from(await res.arrayBuffer());
5250
- writeFileSync4(outputPath, buf);
5251
- }
5252
5292
  function registerFilesCommand(program2) {
5253
- const files = program2.command("files").description("Session file manifest and on-demand upload/download commands");
5293
+ const files = program2.command("files").description("Session file commands (WebRTC P2P transfer)");
5254
5294
  files.command("list").requiredOption("--agent <agent>", "Target agent ID or name").requiredOption("--session <session_key>", "Session key").option("--json", "Output raw JSON").action(async (opts) => {
5255
5295
  try {
5256
5296
  const { id, name } = await resolveTargetAgent(opts.agent);
@@ -5273,90 +5313,7 @@ function registerFilesCommand(program2) {
5273
5313
  console.log(` ${f.path} ${GRAY}${formatBytes(f.size)}${RESET}`);
5274
5314
  }
5275
5315
  console.log("");
5276
- } catch (err) {
5277
- handleError7(err);
5278
- }
5279
- });
5280
- files.command("upload").requiredOption("--agent <agent>", "Target agent ID or name").requiredOption("--session <session_key>", "Session key").requiredOption("--path <file_path>", "Relative file path in session workspace").option("--json", "Output raw JSON").action(async (opts) => {
5281
- try {
5282
- const { id } = await resolveTargetAgent(opts.agent);
5283
- const client = createClient();
5284
- const data = await client.post(`/api/agents/${id}/files/upload`, {
5285
- session_key: opts.session,
5286
- path: opts.path
5287
- });
5288
- if (opts.json) {
5289
- console.log(JSON.stringify(data));
5290
- return;
5291
- }
5292
- if (!data.file?.url) {
5293
- throw new Error("No file URL returned");
5294
- }
5295
- log.success(`Uploaded ${opts.path}`);
5296
- console.log(` ${GRAY}URL${RESET} ${data.file.url}`);
5297
- } catch (err) {
5298
- handleError7(err);
5299
- }
5300
- });
5301
- files.command("upload-all").requiredOption("--agent <agent>", "Target agent ID or name").requiredOption("--session <session_key>", "Session key").option("--json", "Output raw JSON").action(async (opts) => {
5302
- try {
5303
- const { id } = await resolveTargetAgent(opts.agent);
5304
- const client = createClient();
5305
- const data = await client.post(`/api/agents/${id}/files/upload-all`, {
5306
- session_key: opts.session
5307
- });
5308
- if (opts.json) {
5309
- console.log(JSON.stringify(data));
5310
- return;
5311
- }
5312
- if (!data.file?.url) {
5313
- throw new Error("No ZIP URL returned");
5314
- }
5315
- log.success("Uploaded session ZIP");
5316
- console.log(` ${GRAY}URL${RESET} ${data.file.url}`);
5317
- } catch (err) {
5318
- handleError7(err);
5319
- }
5320
- });
5321
- files.command("download").requiredOption("--agent <agent>", "Target agent ID or name").requiredOption("--session <session_key>", "Session key").requiredOption("--path <file_path>", "Relative file path in session workspace").option("--output <path>", "Local output path").option("--json", "Output raw JSON").action(async (opts) => {
5322
- try {
5323
- const { id } = await resolveTargetAgent(opts.agent);
5324
- const client = createClient();
5325
- const data = await client.post(`/api/agents/${id}/files/upload`, {
5326
- session_key: opts.session,
5327
- path: opts.path
5328
- });
5329
- const url = data.file?.url;
5330
- if (!url) throw new Error("No file URL returned");
5331
- const output = opts.output || defaultOutputPath(opts.path);
5332
- await downloadToFile(url, output);
5333
- if (opts.json) {
5334
- console.log(JSON.stringify({ success: true, output, url }));
5335
- return;
5336
- }
5337
- log.success(`Downloaded ${opts.path}`);
5338
- console.log(` ${GRAY}Saved${RESET} ${output}`);
5339
- } catch (err) {
5340
- handleError7(err);
5341
- }
5342
- });
5343
- files.command("download-all").requiredOption("--agent <agent>", "Target agent ID or name").requiredOption("--session <session_key>", "Session key").option("--output <path>", "Local output path").option("--json", "Output raw JSON").action(async (opts) => {
5344
- try {
5345
- const { id } = await resolveTargetAgent(opts.agent);
5346
- const client = createClient();
5347
- const data = await client.post(`/api/agents/${id}/files/upload-all`, {
5348
- session_key: opts.session
5349
- });
5350
- const url = data.file?.url;
5351
- if (!url) throw new Error("No ZIP URL returned");
5352
- const output = opts.output || defaultZipOutputPath(opts.session);
5353
- await downloadToFile(url, output);
5354
- if (opts.json) {
5355
- console.log(JSON.stringify({ success: true, output, url }));
5356
- return;
5357
- }
5358
- log.success("Downloaded session ZIP");
5359
- console.log(` ${GRAY}Saved${RESET} ${output}`);
5316
+ console.log(` ${GRAY}Use --with-files in call/chat to receive files via WebRTC P2P${RESET}`);
5360
5317
  } catch (err) {
5361
5318
  handleError7(err);
5362
5319
  }
@@ -5366,12 +5323,9 @@ function registerFilesCommand(program2) {
5366
5323
  command: "agent-mesh files",
5367
5324
  docs: "https://agents.hot/docs/cli/files",
5368
5325
  commands: [
5369
- { name: "list", required: ["--agent", "--session"], optional: ["--json"] },
5370
- { name: "upload", required: ["--agent", "--session", "--path"], optional: ["--json"] },
5371
- { name: "upload-all", required: ["--agent", "--session"], optional: ["--json"] },
5372
- { name: "download", required: ["--agent", "--session", "--path"], optional: ["--output", "--json"] },
5373
- { name: "download-all", required: ["--agent", "--session"], optional: ["--output", "--json"] }
5374
- ]
5326
+ { name: "list", required: ["--agent", "--session"], optional: ["--json"] }
5327
+ ],
5328
+ notes: "File transfer now uses WebRTC P2P. Use --with-files flag in call/chat commands."
5375
5329
  };
5376
5330
  if (opts.json) {
5377
5331
  console.log(JSON.stringify(reference));
@@ -5382,6 +5336,8 @@ function registerFilesCommand(program2) {
5382
5336
  for (const item of reference.commands) {
5383
5337
  console.log(` ${item.name}`);
5384
5338
  }
5339
+ console.log("");
5340
+ console.log(` ${GRAY}${reference.notes}${RESET}`);
5385
5341
  });
5386
5342
  }
5387
5343
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@annals/agent-mesh",
3
- "version": "0.16.12",
3
+ "version": "0.17.0",
4
4
  "description": "CLI bridge connecting local AI agents to the Agents.Hot platform",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,6 +10,7 @@
10
10
  "dependencies": {
11
11
  "@annals/bridge-protocol": "^0.2.0",
12
12
  "commander": "^13.0.0",
13
+ "node-datachannel": "^0.32.0",
13
14
  "ws": "^8.18.0"
14
15
  },
15
16
  "devDependencies": {