@annals/agent-mesh 0.16.13 → 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 +254 -304
  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
  }
@@ -2071,34 +2119,28 @@ var ClaudeSession = class {
2071
2119
  return;
2072
2120
  }
2073
2121
  }
2074
- stripWorkspacePaths(text) {
2075
- const root = this.getWorkspaceRoot();
2076
- if (!root) return text;
2077
- const prefix = root.endsWith("/") ? root : root + "/";
2078
- return text.replaceAll(prefix, "");
2079
- }
2080
2122
  emitChunk(text) {
2081
2123
  this.chunksEmitted = true;
2082
- for (const cb of this.chunkCallbacks) cb(this.stripWorkspacePaths(text));
2124
+ for (const cb of this.chunkCallbacks) cb(text);
2083
2125
  }
2084
2126
  emitToolEvent(event) {
2085
- for (const cb of this.toolCallbacks) cb({ ...event, delta: this.stripWorkspacePaths(event.delta) });
2127
+ for (const cb of this.toolCallbacks) cb(event);
2086
2128
  }
2087
2129
  emitTextAsChunks(text) {
2088
- const CHUNK_SIZE = 60;
2089
- if (text.length <= CHUNK_SIZE) {
2130
+ const CHUNK_SIZE2 = 60;
2131
+ if (text.length <= CHUNK_SIZE2) {
2090
2132
  this.emitChunk(text);
2091
2133
  return;
2092
2134
  }
2093
2135
  let pos = 0;
2094
2136
  while (pos < text.length) {
2095
- let end = Math.min(pos + CHUNK_SIZE, text.length);
2137
+ let end = Math.min(pos + CHUNK_SIZE2, text.length);
2096
2138
  if (end < text.length) {
2097
2139
  const slice = text.slice(pos, end + 20);
2098
2140
  const breakPoints = ["\n", "\u3002", "\uFF01", "\uFF1F", ". ", "! ", "? ", "\uFF0C", ", ", " "];
2099
2141
  for (const bp of breakPoints) {
2100
- const idx = slice.indexOf(bp, CHUNK_SIZE - 20);
2101
- if (idx >= 0 && idx < CHUNK_SIZE + 20) {
2142
+ const idx = slice.indexOf(bp, CHUNK_SIZE2 - 20);
2143
+ if (idx >= 0 && idx < CHUNK_SIZE2 + 20) {
2102
2144
  end = pos + idx + bp.length;
2103
2145
  break;
2104
2146
  }
@@ -3399,8 +3441,8 @@ async function asyncChat(opts) {
3399
3441
  `);
3400
3442
  }
3401
3443
  }
3402
- if (task.file_manifest) {
3403
- 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}
3404
3446
  `);
3405
3447
  }
3406
3448
  return;
@@ -3605,11 +3647,11 @@ function registerChatCommand(program2) {
3605
3647
  }
3606
3648
 
3607
3649
  // src/commands/skills.ts
3608
- 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";
3609
3651
  import { join as join9, resolve, relative as relative4 } from "path";
3610
3652
 
3611
3653
  // src/utils/skill-parser.ts
3612
- 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";
3613
3655
  import { join as join8 } from "path";
3614
3656
  function parseSkillMd(raw) {
3615
3657
  const trimmed = raw.trimStart();
@@ -3694,7 +3736,7 @@ function parseSkillMd(raw) {
3694
3736
  async function loadSkillManifest(dir) {
3695
3737
  const skillMdPath = join8(dir, "SKILL.md");
3696
3738
  try {
3697
- const raw = await readFile3(skillMdPath, "utf-8");
3739
+ const raw = await readFile2(skillMdPath, "utf-8");
3698
3740
  const { frontmatter } = parseSkillMd(raw);
3699
3741
  const name = frontmatter.name;
3700
3742
  if (!name) {
@@ -3727,7 +3769,7 @@ async function pathExists(p) {
3727
3769
  }
3728
3770
  }
3729
3771
  async function updateFrontmatterField(filePath, field, value) {
3730
- const raw = await readFile3(filePath, "utf-8");
3772
+ const raw = await readFile2(filePath, "utf-8");
3731
3773
  const trimmed = raw.trimStart();
3732
3774
  if (!trimmed.startsWith("---")) {
3733
3775
  throw new Error("SKILL.md has no frontmatter block");
@@ -3860,7 +3902,7 @@ async function packSkill(dir, manifest) {
3860
3902
  for (const relPath of fileList) {
3861
3903
  const absPath = join9(dir, relPath);
3862
3904
  try {
3863
- const data = await readFile4(absPath);
3905
+ const data = await readFile3(absPath);
3864
3906
  entries.push({ path: relPath.replace(/\\/g, "/"), data });
3865
3907
  } catch {
3866
3908
  slog.warn(`Skipping unreadable file: ${relPath}`);
@@ -3945,7 +3987,7 @@ function registerSkillsCommand(program2) {
3945
3987
  await mkdir2(dir, { recursive: true });
3946
3988
  const skillMdPath = join9(dir, "SKILL.md");
3947
3989
  if (await pathExists(skillMdPath)) {
3948
- const raw = await readFile4(skillMdPath, "utf-8");
3990
+ const raw = await readFile3(skillMdPath, "utf-8");
3949
3991
  const { frontmatter } = parseSkillMd(raw);
3950
3992
  if (frontmatter.name) {
3951
3993
  slog.info(`SKILL.md already exists with name: ${frontmatter.name}`);
@@ -4018,7 +4060,7 @@ function registerSkillsCommand(program2) {
4018
4060
  if (opts.name) manifest.name = opts.name;
4019
4061
  if (opts.version) manifest.version = opts.version;
4020
4062
  if (opts.private !== void 0) manifest.private = opts.private;
4021
- content = await readFile4(join9(dir, manifest.main || "SKILL.md"), "utf-8");
4063
+ content = await readFile3(join9(dir, manifest.main || "SKILL.md"), "utf-8");
4022
4064
  packResult = await packSkill(dir, manifest);
4023
4065
  slog.info(`Packed ${packResult.files.length} files (${packResult.size} bytes)`);
4024
4066
  }
@@ -4159,7 +4201,7 @@ function registerSkillsCommand(program2) {
4159
4201
  if (!await pathExists(skillMdPath)) {
4160
4202
  outputError("not_found", "No SKILL.md found. Run `agent-mesh skills init` first.");
4161
4203
  }
4162
- const raw = await readFile4(skillMdPath, "utf-8");
4204
+ const raw = await readFile3(skillMdPath, "utf-8");
4163
4205
  const { frontmatter } = parseSkillMd(raw);
4164
4206
  const oldVersion = frontmatter.version || "0.0.0";
4165
4207
  const newVersion = bumpVersion(oldVersion, bump);
@@ -4221,7 +4263,7 @@ function registerSkillsCommand(program2) {
4221
4263
  const skillMdPath = join9(targetDir, "SKILL.md");
4222
4264
  let localVersion = "0.0.0";
4223
4265
  if (await pathExists(skillMdPath)) {
4224
- const raw = await readFile4(skillMdPath, "utf-8");
4266
+ const raw = await readFile3(skillMdPath, "utf-8");
4225
4267
  const { frontmatter } = parseSkillMd(raw);
4226
4268
  localVersion = frontmatter.version || "0.0.0";
4227
4269
  }
@@ -4250,7 +4292,7 @@ function registerSkillsCommand(program2) {
4250
4292
  skipped.push({ slug, reason: "no_skill_md" });
4251
4293
  continue;
4252
4294
  }
4253
- const raw = await readFile4(skillMdPath, "utf-8");
4295
+ const raw = await readFile3(skillMdPath, "utf-8");
4254
4296
  const { frontmatter } = parseSkillMd(raw);
4255
4297
  const localVersion = frontmatter.version || "0.0.0";
4256
4298
  const authorLogin = frontmatter.author;
@@ -4319,7 +4361,7 @@ function registerSkillsCommand(program2) {
4319
4361
  const slug = entry.name;
4320
4362
  const skillMdPath = join9(skillsDir, slug, "SKILL.md");
4321
4363
  if (!await pathExists(skillMdPath)) continue;
4322
- const raw = await readFile4(skillMdPath, "utf-8");
4364
+ const raw = await readFile3(skillMdPath, "utf-8");
4323
4365
  const { frontmatter } = parseSkillMd(raw);
4324
4366
  const skillInfo = {
4325
4367
  slug,
@@ -4470,7 +4512,11 @@ async function asyncCall(opts) {
4470
4512
  "Content-Type": "application/json",
4471
4513
  ...selfAgentId ? { "X-Caller-Agent-Id": selfAgentId } : {}
4472
4514
  },
4473
- 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
+ }),
4474
4520
  signal: opts.signal
4475
4521
  });
4476
4522
  if (!res.ok) {
@@ -4529,7 +4575,7 @@ async function asyncCall(opts) {
4529
4575
  status: "completed",
4530
4576
  result,
4531
4577
  ...task.attachments?.length ? { attachments: task.attachments } : {},
4532
- ...Array.isArray(task.file_manifest) ? { file_manifest: task.file_manifest } : {},
4578
+ ...task.file_transfer_offer ? { file_transfer_offer: task.file_transfer_offer } : {},
4533
4579
  rate_hint: `POST /api/agents/${opts.id}/rate body: { call_id: "${call_id}", rating: 1-5 }`
4534
4580
  }));
4535
4581
  } else {
@@ -4539,9 +4585,9 @@ async function asyncCall(opts) {
4539
4585
  log.info(` ${GRAY}File:${RESET} ${att.name} ${GRAY}${att.url}${RESET}`);
4540
4586
  }
4541
4587
  }
4542
- const manifest = task.file_manifest;
4543
- if (Array.isArray(manifest)) {
4544
- 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`);
4545
4591
  }
4546
4592
  if (session_key) {
4547
4593
  log.info(` ${GRAY}Session:${RESET} ${session_key}`);
@@ -4585,7 +4631,10 @@ async function streamCall(opts) {
4585
4631
  Accept: "text/event-stream",
4586
4632
  ...selfAgentId ? { "X-Caller-Agent-Id": selfAgentId } : {}
4587
4633
  },
4588
- body: JSON.stringify({ task_description: opts.taskDescription }),
4634
+ body: JSON.stringify({
4635
+ task_description: opts.taskDescription,
4636
+ ...opts.withFiles ? { with_files: true } : {}
4637
+ }),
4589
4638
  signal: opts.signal
4590
4639
  });
4591
4640
  if (!res.ok) {
@@ -4668,13 +4717,15 @@ async function streamCall(opts) {
4668
4717
  outputBuffer += delta;
4669
4718
  }
4670
4719
  }
4671
- } else if (event.type === "done" && event.attachments?.length) {
4672
- console.log("");
4673
- for (const att of event.attachments) {
4674
- 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
+ }
4675
4726
  }
4676
- if (Array.isArray(event.file_manifest)) {
4677
- 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`);
4678
4729
  }
4679
4730
  } else if (event.type === "error") {
4680
4731
  process.stderr.write(`
@@ -4724,7 +4775,7 @@ Error: ${event.message}
4724
4775
  return { callId, ...sessionKey ? { sessionKey } : {} };
4725
4776
  }
4726
4777
  function registerCallCommand(program2) {
4727
- 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) => {
4728
4779
  try {
4729
4780
  const token = loadToken();
4730
4781
  if (!token) {
@@ -4753,7 +4804,8 @@ ${content}`;
4753
4804
  timeoutMs,
4754
4805
  json: opts.json,
4755
4806
  outputFile: opts.outputFile,
4756
- signal: abortController.signal
4807
+ signal: abortController.signal,
4808
+ withFiles: opts.withFiles
4757
4809
  };
4758
4810
  let result;
4759
4811
  if (opts.stream) {
@@ -5220,21 +5272,11 @@ function registerProfileCommand(program2) {
5220
5272
  }
5221
5273
 
5222
5274
  // src/commands/files.ts
5223
- import { writeFileSync as writeFileSync4 } from "fs";
5224
- import { basename as basename2 } from "path";
5225
5275
  function formatBytes(bytes) {
5226
5276
  if (bytes < 1024) return `${bytes}B`;
5227
5277
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
5228
5278
  return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
5229
5279
  }
5230
- function defaultOutputPath(filePath) {
5231
- const name = basename2(filePath || "") || "file";
5232
- return name;
5233
- }
5234
- function defaultZipOutputPath(sessionKey) {
5235
- const suffix = sessionKey.split(":").at(-1) || "session";
5236
- return `session-${suffix}.zip`;
5237
- }
5238
5280
  function handleError7(err) {
5239
5281
  if (err instanceof PlatformApiError) {
5240
5282
  log.error(err.message);
@@ -5247,16 +5289,8 @@ async function resolveTargetAgent(agentInput) {
5247
5289
  const client = createClient();
5248
5290
  return resolveAgentId(agentInput, client);
5249
5291
  }
5250
- async function downloadToFile(url, outputPath) {
5251
- const res = await fetch(url);
5252
- if (!res.ok) {
5253
- throw new Error(`Download failed: HTTP ${res.status}`);
5254
- }
5255
- const buf = Buffer.from(await res.arrayBuffer());
5256
- writeFileSync4(outputPath, buf);
5257
- }
5258
5292
  function registerFilesCommand(program2) {
5259
- 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)");
5260
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) => {
5261
5295
  try {
5262
5296
  const { id, name } = await resolveTargetAgent(opts.agent);
@@ -5279,90 +5313,7 @@ function registerFilesCommand(program2) {
5279
5313
  console.log(` ${f.path} ${GRAY}${formatBytes(f.size)}${RESET}`);
5280
5314
  }
5281
5315
  console.log("");
5282
- } catch (err) {
5283
- handleError7(err);
5284
- }
5285
- });
5286
- 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) => {
5287
- try {
5288
- const { id } = await resolveTargetAgent(opts.agent);
5289
- const client = createClient();
5290
- const data = await client.post(`/api/agents/${id}/files/upload`, {
5291
- session_key: opts.session,
5292
- path: opts.path
5293
- });
5294
- if (opts.json) {
5295
- console.log(JSON.stringify(data));
5296
- return;
5297
- }
5298
- if (!data.file?.url) {
5299
- throw new Error("No file URL returned");
5300
- }
5301
- log.success(`Uploaded ${opts.path}`);
5302
- console.log(` ${GRAY}URL${RESET} ${data.file.url}`);
5303
- } catch (err) {
5304
- handleError7(err);
5305
- }
5306
- });
5307
- 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) => {
5308
- try {
5309
- const { id } = await resolveTargetAgent(opts.agent);
5310
- const client = createClient();
5311
- const data = await client.post(`/api/agents/${id}/files/upload-all`, {
5312
- session_key: opts.session
5313
- });
5314
- if (opts.json) {
5315
- console.log(JSON.stringify(data));
5316
- return;
5317
- }
5318
- if (!data.file?.url) {
5319
- throw new Error("No ZIP URL returned");
5320
- }
5321
- log.success("Uploaded session ZIP");
5322
- console.log(` ${GRAY}URL${RESET} ${data.file.url}`);
5323
- } catch (err) {
5324
- handleError7(err);
5325
- }
5326
- });
5327
- 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) => {
5328
- try {
5329
- const { id } = await resolveTargetAgent(opts.agent);
5330
- const client = createClient();
5331
- const data = await client.post(`/api/agents/${id}/files/upload`, {
5332
- session_key: opts.session,
5333
- path: opts.path
5334
- });
5335
- const url = data.file?.url;
5336
- if (!url) throw new Error("No file URL returned");
5337
- const output = opts.output || defaultOutputPath(opts.path);
5338
- await downloadToFile(url, output);
5339
- if (opts.json) {
5340
- console.log(JSON.stringify({ success: true, output, url }));
5341
- return;
5342
- }
5343
- log.success(`Downloaded ${opts.path}`);
5344
- console.log(` ${GRAY}Saved${RESET} ${output}`);
5345
- } catch (err) {
5346
- handleError7(err);
5347
- }
5348
- });
5349
- 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) => {
5350
- try {
5351
- const { id } = await resolveTargetAgent(opts.agent);
5352
- const client = createClient();
5353
- const data = await client.post(`/api/agents/${id}/files/upload-all`, {
5354
- session_key: opts.session
5355
- });
5356
- const url = data.file?.url;
5357
- if (!url) throw new Error("No ZIP URL returned");
5358
- const output = opts.output || defaultZipOutputPath(opts.session);
5359
- await downloadToFile(url, output);
5360
- if (opts.json) {
5361
- console.log(JSON.stringify({ success: true, output, url }));
5362
- return;
5363
- }
5364
- log.success("Downloaded session ZIP");
5365
- console.log(` ${GRAY}Saved${RESET} ${output}`);
5316
+ console.log(` ${GRAY}Use --with-files in call/chat to receive files via WebRTC P2P${RESET}`);
5366
5317
  } catch (err) {
5367
5318
  handleError7(err);
5368
5319
  }
@@ -5372,12 +5323,9 @@ function registerFilesCommand(program2) {
5372
5323
  command: "agent-mesh files",
5373
5324
  docs: "https://agents.hot/docs/cli/files",
5374
5325
  commands: [
5375
- { name: "list", required: ["--agent", "--session"], optional: ["--json"] },
5376
- { name: "upload", required: ["--agent", "--session", "--path"], optional: ["--json"] },
5377
- { name: "upload-all", required: ["--agent", "--session"], optional: ["--json"] },
5378
- { name: "download", required: ["--agent", "--session", "--path"], optional: ["--output", "--json"] },
5379
- { name: "download-all", required: ["--agent", "--session"], optional: ["--output", "--json"] }
5380
- ]
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."
5381
5329
  };
5382
5330
  if (opts.json) {
5383
5331
  console.log(JSON.stringify(reference));
@@ -5388,6 +5336,8 @@ function registerFilesCommand(program2) {
5388
5336
  for (const item of reference.commands) {
5389
5337
  console.log(` ${item.name}`);
5390
5338
  }
5339
+ console.log("");
5340
+ console.log(` ${GRAY}${reference.notes}${RESET}`);
5391
5341
  });
5392
5342
  }
5393
5343
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@annals/agent-mesh",
3
- "version": "0.16.13",
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": {