dsh-agy 0.4.0 → 0.4.2

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 (28) hide show
  1. package/README.md +21 -10
  2. package/lib/{accounts-BN4h0AU4.mjs → accounts-D605JE2i.mjs} +186 -19
  3. package/lib/accounts-D605JE2i.mjs.map +1 -0
  4. package/lib/cli/index.d.mts.map +1 -1
  5. package/lib/cli/index.mjs +9 -7
  6. package/lib/cli/index.mjs.map +1 -1
  7. package/lib/client.js +402 -51
  8. package/lib/{constants-BpyRirvk.mjs → constants-CmA9wXNe.mjs} +3 -3
  9. package/lib/{constants-BpyRirvk.mjs.map → constants-CmA9wXNe.mjs.map} +1 -1
  10. package/lib/{exchange-LzEyxve1.mjs → exchange-C-tndv7j.mjs} +3 -3
  11. package/lib/{exchange-LzEyxve1.mjs.map → exchange-C-tndv7j.mjs.map} +1 -1
  12. package/lib/{import-CotRHNDk.mjs → import-SxPDjH3u.mjs} +4 -4
  13. package/lib/{import-CotRHNDk.mjs.map → import-SxPDjH3u.mjs.map} +1 -1
  14. package/lib/index.mjs +2 -2
  15. package/lib/{models-Qn0-sh6X.mjs → models-B6bD7zPi.mjs} +3 -3
  16. package/lib/{models-Qn0-sh6X.mjs.map → models-B6bD7zPi.mjs.map} +1 -1
  17. package/lib/{plugin-common-N7p9Cm-3.mjs → plugin-common-kSBwIaFi.mjs} +303 -17
  18. package/lib/plugin-common-kSBwIaFi.mjs.map +1 -0
  19. package/lib/{proxy-DfaL73mL.mjs → proxy-BbuQX5U9.mjs} +28 -8
  20. package/lib/proxy-BbuQX5U9.mjs.map +1 -0
  21. package/lib/{quota-summary-adID82I2.mjs → quota-summary-DEA3P3Zh.mjs} +3 -3
  22. package/lib/{quota-summary-adID82I2.mjs.map → quota-summary-DEA3P3Zh.mjs.map} +1 -1
  23. package/lib/web/plugin.mjs +40 -18
  24. package/lib/web/plugin.mjs.map +1 -1
  25. package/package.json +1 -4
  26. package/lib/accounts-BN4h0AU4.mjs.map +0 -1
  27. package/lib/plugin-common-N7p9Cm-3.mjs.map +0 -1
  28. package/lib/proxy-DfaL73mL.mjs.map +0 -1
package/README.md CHANGED
@@ -137,11 +137,11 @@ dsh plugin --profile web remove dsh-agy
137
137
  # 2. Uninstall the CLI
138
138
  npm uninstall -g dsh-agy
139
139
 
140
- # 3. Optional: delete local account data (accounts + master key + fingerprint override)
140
+ # 3. Optional: delete local account data (the whole agy data folder)
141
141
  dsh-agy logout # remove accounts first (or skip)
142
- rm -f ~/.dsh/agy-accounts.json ~/.dsh/agy-stats.json ~/.dsh/agy-models.json
142
+ rm -rf ~/.dsh/agy # accounts, usage stats, model visibility, thinking budgets, recent ring
143
143
  # remove only the AGY_MASTER_KEY line from ~/.dsh/.credentials.yaml — keep other keys!
144
- rm -f ~/.dsh/agy-fingerprint-data.json # only if you created an override
144
+ # (an upgrade from ≤0.4 may leave legacy files in ~/.dsh itself: rm -f ~/.dsh/agy-*.json)
145
145
 
146
146
  # 4. Optional: revoke the Google-side authorization
147
147
  # Google account security → Third-party access → revoke "Antigravity"
@@ -268,17 +268,28 @@ the context grows, bounded by the model's context window.
268
268
 
269
269
  ### Storage & secrets
270
270
 
271
- - Accounts: `~/.dsh/agy-accounts.json` — AES-256-GCM encrypted; the master key lives
272
- in `~/.dsh/.credentials.yaml` (`AGY_MASTER_KEY`, 0600). `$DSH_HOME` relocates both.
271
+ All agy data files live in one folder, `~/.dsh/agy/` (relocated wholesale by
272
+ `$DSH_HOME`). Upgrading from ≤0.4 moves the legacy files in `~/.dsh/` there on
273
+ first start — a one-shot atomic rename; if a legacy file reappears next to the
274
+ folder afterwards, an old-version process is still running, and a warning says so.
275
+
276
+ - Accounts: `~/.dsh/agy/agy-accounts.json` —
277
+ AES-256-GCM encrypted; the master key lives in `~/.dsh/.credentials.yaml`
278
+ (`AGY_MASTER_KEY`, 0600). `$DSH_HOME` relocates the whole tree.
273
279
  - Fingerprint pools (version strings, SDK clients) are user-overridable via
274
- `~/.dsh/agy-fingerprint-data.json` — no code release needed to keep them current.
275
- - Model visibility: `~/.dsh/agy-models.json` (0600) — the hidden-model blacklist.
280
+ `~/.dsh/agy/agy-fingerprint-data.json` — no code release needed to keep them current.
281
+ - Model visibility: `~/.dsh/agy/agy-models.json` (0600) — the hidden-model blacklist.
276
282
  Holds only the models you switched off, so re-enabling one removes its entry.
277
- - Usage statistics: `~/.dsh/agy-stats.json` (0600) — cumulative counters plus a
283
+ - Usage statistics: `~/.dsh/agy/agy-stats.json` (0600) — cumulative counters plus a
278
284
  rolling 30-day window. Counts are merged under a file lock, so several
279
285
  processes (Desktop, a web-profile server, the CLI) record concurrently without
280
- losing each other's data. It stores account emails but never tokens, proxies,
281
- or project ids.
286
+ losing each other's data.
287
+ - Recent activity: `~/.dsh/agy/agy-recent.json` (0600) — the last 200 requests and
288
+ rotations, persisted so the panel survives a restart and shows the other
289
+ processes' history too (external processes' new records arrive within 30s).
290
+ - None of these files store tokens, proxies, or project ids — account emails
291
+ only; the tokens live encrypted in the accounts file and the master key in
292
+ the credentials document.
282
293
 
283
294
  ## ⚠️ Disclaimer
284
295
 
@@ -1,5 +1,5 @@
1
- import { _ as setResolvedAgyVersion, a as AGY_PLATFORM_ENUM, b as QUOTA_WINDOWS, d as antigravityUserAgent, g as resolveAgyClientCredentials, l as OAUTH_TOKEN_URL, p as currentAgyVersion, t as AGY_CLIENT_ID, v as AgyAuthError, x as isProxyRouted, y as AgyPoolBlockedError } from "./constants-BpyRirvk.mjs";
2
- import { a as probeFetch, l as redactCredentials, o as proxiedFetch, r as isProxyUnreachableError, t as accountFetch } from "./proxy-DfaL73mL.mjs";
1
+ import { _ as setResolvedAgyVersion, a as AGY_PLATFORM_ENUM, b as QUOTA_WINDOWS, d as antigravityUserAgent, g as resolveAgyClientCredentials, l as OAUTH_TOKEN_URL, p as currentAgyVersion, t as AGY_CLIENT_ID, v as AgyAuthError, x as isProxyRouted, y as AgyPoolBlockedError } from "./constants-CmA9wXNe.mjs";
2
+ import { a as probeFetch, l as redactCredentials, o as proxiedFetch, r as isProxyUnreachableError, t as accountFetch } from "./proxy-BbuQX5U9.mjs";
3
3
  import { i as generateAntigravityRequestId, r as deriveAntigravitySessionId } from "./identity-Ci8EGc1F.mjs";
4
4
  import { i as parseRefreshParts, n as calculateTokenExpiry, r as formatRefreshParts, t as accessTokenExpired } from "./auth-DPPVFwio.mjs";
5
5
  import { createCipheriv, createDecipheriv, createHash, randomBytes, randomUUID } from "node:crypto";
@@ -544,6 +544,87 @@ function resolveMasterKeyCodec(dshHome) {
544
544
  };
545
545
  }
546
546
  //#endregion
547
+ //#region src/store/paths.ts
548
+ /**
549
+ * The `agy` data folder: every data file this plugin creates lives under
550
+ * `$DSH_HOME/agy/` instead of scattered across the DSH home.
551
+ *
552
+ * Why one folder: agy owned five JSON files sitting directly in `$DSH_HOME`
553
+ * (accounts, stats, models, thinking, fingerprint override) next to the host's
554
+ * own files. A folder is the boundary a user can see, back up, or delete whole.
555
+ *
556
+ * Migration is a ONE-SHOT ATOMIC RENAME, not a read-merge with lazy fallback:
557
+ *
558
+ * - `agy/` sits on the same filesystem as `$DSH_HOME`, so `rename` is atomic
559
+ * and preserves the 0600 mode.
560
+ * - A read-merge would need every store to carry two paths, two file locks,
561
+ * and per-store merge logic — and the merge itself can still lose updates
562
+ * (accounts mutate, stats counts).
563
+ * - A lazy fallback reads the old path forever, which defeats the point: the
564
+ * old file would never leave the home directory.
565
+ *
566
+ * Version-skew window contract: after a migration an OLD build recreates its
567
+ * legacy file, and its writes are invisible to the new layout — the both-files
568
+ * warning in `migrateToAgyDir` is the detector for exactly that (it fires when
569
+ * a later process finds both files present). Local SOP re-syncs all profiles at
570
+ * once, so the window is one app restart; npm users restart every dsh surface
571
+ * per the release note. Accounts logged in by an old CLI during skew need one
572
+ * re-login — enumerable, one-time, accepted in exchange for zero merge code.
573
+ */
574
+ /** The directory under `$DSH_HOME` holding every agy data file. */
575
+ function agyDataDir(dshHome = resolveDshHome()) {
576
+ return join(dshHome, "agy");
577
+ }
578
+ /** A data file inside the agy directory. */
579
+ function agyDataFile(name, dshHome = resolveDshHome()) {
580
+ return join(agyDataDir(dshHome), name);
581
+ }
582
+ /**
583
+ * One-time legacy -> `agy/` migration for one data file.
584
+ *
585
+ * 1. New file already present: warn-once (via `skew`) when the legacy file is
586
+ * ALSO present — an old-version process is still writing the old path.
587
+ * Return the new path regardless: the new layout wins once it exists.
588
+ * 2. No legacy file: fresh install (or already migrated), nothing to do.
589
+ * 3. Otherwise create the directory and rename. On ANY failure (`agy/` cannot
590
+ * be created, EXDEV, permissions, a locked target on Windows) fall back to
591
+ * the LEGACY path for this process: for accounts, the old layout with the
592
+ * real pool beats a new empty one, and `skew` surfaces the reason instead
593
+ * of hiding it.
594
+ *
595
+ * The caller decides what `skew` becomes; `plugin-common.ts` and the CLI log
596
+ * it once per process at most.
597
+ */
598
+ function migrateToAgyDir(name, dshHome = resolveDshHome()) {
599
+ const legacy = join(dshHome, name);
600
+ const file = agyDataFile(name, dshHome);
601
+ if (existsSync(file)) return {
602
+ file,
603
+ migrated: false,
604
+ skew: existsSync(legacy)
605
+ };
606
+ if (!existsSync(legacy)) return {
607
+ file,
608
+ migrated: false,
609
+ skew: false
610
+ };
611
+ try {
612
+ mkdirSync(agyDataDir(dshHome), { recursive: true });
613
+ renameSync(legacy, file);
614
+ return {
615
+ file,
616
+ migrated: true,
617
+ skew: false
618
+ };
619
+ } catch {
620
+ return {
621
+ file: legacy,
622
+ migrated: false,
623
+ skew: true
624
+ };
625
+ }
626
+ }
627
+ //#endregion
547
628
  //#region src/runtime/rotation.ts
548
629
  const BACKOFF_TIERS_MS = [
549
630
  5e3,
@@ -1539,18 +1620,17 @@ const DEFAULT_FINGERPRINT_DATA = {
1539
1620
  ],
1540
1621
  ideTypes: ["ANTIGRAVITY"]
1541
1622
  };
1542
- const USER_OVERRIDE_FILE = "agy-fingerprint-data.json";
1543
1623
  /**
1544
- * Effective fingerprint data: a user override at `$DSH_HOME/agy-fingerprint-data.json`
1624
+ * Effective fingerprint data: a user override at `$DSH_HOME/agy/agy-fingerprint-data.json`
1545
1625
  * wins when present and parseable (hot-updatable without a code release — the
1546
- * bundled copy is compiled in), otherwise the bundled defaults.
1626
+ * bundled copy is compiled in), otherwise the bundled defaults. The legacy
1627
+ * `$DSH_HOME/agy-fingerprint-data.json` location is migrated once by rename.
1547
1628
  */
1548
1629
  let cachedData = null;
1549
1630
  function getFingerprintData() {
1550
1631
  if (cachedData) return cachedData;
1551
1632
  try {
1552
- const dshHome = process.env.DSH_HOME ? process.env.DSH_HOME : join(process.env.HOME ?? ".", ".dsh");
1553
- const overrideFile = join(dshHome, USER_OVERRIDE_FILE);
1633
+ const overrideFile = migrateToAgyDir("agy-fingerprint-data.json", process.env.DSH_HOME ? process.env.DSH_HOME : join(process.env.HOME ?? ".", ".dsh")).file;
1554
1634
  if (existsSync(overrideFile)) {
1555
1635
  const parsed = JSON.parse(readFileSync(overrideFile, "utf8"));
1556
1636
  if (parsed && Array.isArray(parsed.versionPool) && parsed.versionPool.length > 0) {
@@ -1648,6 +1728,30 @@ function fingerprintMode() {
1648
1728
  return process.env.DSH_AGY_FINGERPRINT_MODE === "stable" ? "stable" : "dynamic";
1649
1729
  }
1650
1730
  /**
1731
+ * Fraction-per-hour per window bucket, from two snapshots.
1732
+ *
1733
+ * Pure and exported for a direct unit test. Rules:
1734
+ * - the FIRST sample yields nothing (no baseline yet), and so does a pair
1735
+ * closer than `MIN_BURN_SAMPLE_MS` — two probes seconds apart measure the
1736
+ * probe jitter, not the burn;
1737
+ * - a RISING fraction is a reset (or refresh noise), never negative burn;
1738
+ * - a bucket with no baseline simply has no rate yet.
1739
+ *
1740
+ * Returns null when no bucket produced a rate, so the caller can keep the
1741
+ * previous rates instead of overwriting them with an empty read.
1742
+ */
1743
+ function sampleLimitBurn(prev, next) {
1744
+ if (prev === void 0) return null;
1745
+ const elapsedH = (next.at - prev.at) / 36e5;
1746
+ if (elapsedH < 3e5 / 36e5) return null;
1747
+ const perHour = {};
1748
+ for (const [bucketId, fraction] of Object.entries(next.fractions)) {
1749
+ const before = prev.fractions[bucketId];
1750
+ if (before !== void 0 && before > fraction) perHour[bucketId] = (before - fraction) / elapsedH;
1751
+ }
1752
+ return Object.keys(perHour).length > 0 ? perHour : null;
1753
+ }
1754
+ /**
1651
1755
  * Resolve the impersonation headers for one request from the account's
1652
1756
  * persistent fingerprint (stable identity).
1653
1757
  *
@@ -1656,7 +1760,7 @@ function fingerprintMode() {
1656
1760
  * deliberately DETERMINISTIC rather than randomized: a per-request platform or
1657
1761
  * version would make one account appear to be several different machines, which
1658
1762
  * is the anomaly this identity exists to avoid. It reads
1659
- * {@link getFingerprintData} so a `$DSH_HOME/agy-fingerprint-data.json` override
1763
+ * {@link getFingerprintData} so a `$DSH_HOME/agy/agy-fingerprint-data.json` override
1660
1764
  * still applies on this path.
1661
1765
  */
1662
1766
  function impersonationHeadersFor(account) {
@@ -1761,6 +1865,62 @@ var AgySessionManager = class AgySessionManager {
1761
1865
  at: now
1762
1866
  });
1763
1867
  }
1868
+ /**
1869
+ * Which accounts have upstream requests in flight, for the UI's live line.
1870
+ *
1871
+ * The join runs HERE, not in the RPC layer: the in-flight map is keyed by
1872
+ * `accountKey` (id ?? email ?? idx-<refresh>), a derivation the management
1873
+ * surface must not have to replicate — its own email-first key would miss
1874
+ * every account carrying both fields. Counts go through `inFlightCount`, so
1875
+ * an entry leaked past its TTL reads as idle instead of busy forever.
1876
+ */
1877
+ async inFlightAccounts() {
1878
+ const storage = await this.store.load();
1879
+ const now = Date.now();
1880
+ const busy = [];
1881
+ for (const [index, account] of storage.accounts.entries()) {
1882
+ const count = this.inFlightCount(this.accountKey(account), now);
1883
+ if (count > 0) busy.push({
1884
+ index,
1885
+ email: account.email ?? null,
1886
+ count
1887
+ });
1888
+ }
1889
+ return busy;
1890
+ }
1891
+ /** Last fraction snapshot per account, for `sampleLimitBurn` (memory only). */
1892
+ limitFractions = /* @__PURE__ */ new Map();
1893
+ /** Latest burn rate per account: bucketId -> fraction/hour (memory only). */
1894
+ limitBurn = /* @__PURE__ */ new Map();
1895
+ noteLimitSample(key, groups, at) {
1896
+ const fractions = {};
1897
+ for (const group of groups) for (const window of group.windows) if (window.remainingFraction !== null) fractions[window.bucketId] = window.remainingFraction;
1898
+ const next = {
1899
+ at,
1900
+ fractions
1901
+ };
1902
+ const burn = sampleLimitBurn(this.limitFractions.get(key), next);
1903
+ if (burn !== null) this.limitBurn.set(key, { perHour: burn });
1904
+ this.limitFractions.set(key, next);
1905
+ }
1906
+ /**
1907
+ * Burn rates per account INDEX, for the `account.limits` reply.
1908
+ *
1909
+ * The index join runs here, like `inFlightAccounts`: the rate map is keyed
1910
+ * by `accountKey`, a derivation the management surface must not replicate.
1911
+ */
1912
+ async limitBurnRates() {
1913
+ const storage = await this.store.load();
1914
+ const out = [];
1915
+ for (const [index, account] of storage.accounts.entries()) {
1916
+ const entry = this.limitBurn.get(this.accountKey(account));
1917
+ if (entry !== void 0) out.push({
1918
+ index,
1919
+ perHour: entry.perHour
1920
+ });
1921
+ }
1922
+ return out;
1923
+ }
1764
1924
  /** The map key for a conversation; anonymous callers share one bucket. */
1765
1925
  conversationKeyFor(conversationKey) {
1766
1926
  const trimmed = conversationKey?.trim();
@@ -1947,7 +2107,7 @@ var AgySessionManager = class AgySessionManager {
1947
2107
  try {
1948
2108
  const auth = await this.accessTokenFor(account);
1949
2109
  if (!auth) return [];
1950
- const { fetchQuotaSummary } = await import("./quota-summary-adID82I2.mjs");
2110
+ const { fetchQuotaSummary } = await import("./quota-summary-DEA3P3Zh.mjs");
1951
2111
  const routed = accountFetch({ proxyUrl: account.proxy });
1952
2112
  const bounded = (input, init) => {
1953
2113
  const timeout = AbortSignal.timeout(AgySessionManager.QUOTA_FETCH_TIMEOUT_MS);
@@ -2045,11 +2205,17 @@ var AgySessionManager = class AgySessionManager {
2045
2205
  if (updates.length === 0) return result;
2046
2206
  for (const update of updates) {
2047
2207
  const target = storage.accounts.find((candidate) => this.accountKey(candidate) === update.key);
2048
- if (target) target.cachedLimits = {
2049
- groups: update.groups,
2050
- updatedAt: update.updatedAt
2051
- };
2208
+ if (target) {
2209
+ target.cachedLimits = {
2210
+ groups: update.groups,
2211
+ updatedAt: update.updatedAt
2212
+ };
2213
+ this.noteLimitSample(update.key, update.groups, update.updatedAt);
2214
+ }
2052
2215
  }
2216
+ const live = new Set(storage.accounts.map((account) => this.accountKey(account)));
2217
+ for (const key of this.limitFractions.keys()) if (!live.has(key)) this.limitFractions.delete(key);
2218
+ for (const key of this.limitBurn.keys()) if (!live.has(key)) this.limitBurn.delete(key);
2053
2219
  try {
2054
2220
  await this.store.mutate((s) => {
2055
2221
  for (const update of updates) {
@@ -2079,7 +2245,7 @@ var AgySessionManager = class AgySessionManager {
2079
2245
  try {
2080
2246
  const auth = await this.accessTokenFor(account);
2081
2247
  if (!auth) return null;
2082
- const { fetchAvailableModels } = await import("./models-Qn0-sh6X.mjs").then((n) => n.r);
2248
+ const { fetchAvailableModels } = await import("./models-B6bD7zPi.mjs").then((n) => n.r);
2083
2249
  const routed = accountFetch({ proxyUrl: account.proxy });
2084
2250
  const bounded = (input, init) => {
2085
2251
  const timeout = AbortSignal.timeout(AgySessionManager.QUOTA_FETCH_TIMEOUT_MS);
@@ -2261,7 +2427,7 @@ var AgySessionManager = class AgySessionManager {
2261
2427
  }
2262
2428
  const key = this.accountKey(picked.account);
2263
2429
  if (!picked.account.projectId && !this.projectRetryFailed.has(key)) try {
2264
- const { loadCodeAssist } = await import("./exchange-LzEyxve1.mjs").then((n) => n.n);
2430
+ const { loadCodeAssist } = await import("./exchange-C-tndv7j.mjs").then((n) => n.n);
2265
2431
  const { projectId } = await loadCodeAssist(auth.access, { proxyUrl: picked.account.proxy });
2266
2432
  if (projectId) {
2267
2433
  await this.store.mutate((s) => {
@@ -2383,7 +2549,8 @@ var AgySessionManager = class AgySessionManager {
2383
2549
  source: "chat",
2384
2550
  ok: false,
2385
2551
  rotated: true,
2386
- poolEvent: true
2552
+ poolEvent: true,
2553
+ reason: kind
2387
2554
  });
2388
2555
  }
2389
2556
  }
@@ -2424,7 +2591,7 @@ var AgySessionManager = class AgySessionManager {
2424
2591
  try {
2425
2592
  const account = session.account.email ?? session.account.id;
2426
2593
  const { toAgyRequestBody } = await import("./translate-YhGDUuVa.mjs").then((n) => n.n);
2427
- const { fetchAgyFirstOk } = await import("./constants-BpyRirvk.mjs").then((n) => n.f);
2594
+ const { fetchAgyFirstOk } = await import("./constants-CmA9wXNe.mjs").then((n) => n.f);
2428
2595
  const { parseAgySse } = await import("./parse-Bs4rPkHA.mjs").then((n) => n.n);
2429
2596
  const requestId = generateAntigravityRequestId();
2430
2597
  const body = toAgyRequestBody({
@@ -2864,6 +3031,6 @@ var JsonAccountStore = class {
2864
3031
  }
2865
3032
  };
2866
3033
  //#endregion
2867
- export { resolveMasterKeyCodec as _, generateFingerprint as a, describeFetchError as b, resolveAntigravityVersionBounded as c, MASTER_KEY_REF as d, createAesGcmCodec as f, resolveDshHome as g, persistMasterKey as h, isAgyDisabled as i, clearExpiredState as l, loadMasterKey as m, maskProxyUrl as n, recordFingerprintVersion as o, deriveKey as p, AgySessionManager as r, resolveAntigravityVersion as s, JsonAccountStore as t, pickProbeProxyUrl as u, classifyFetchError as v, isSessionAccumulationOverflow as x, classifyHttpError as y };
3034
+ export { isSessionAccumulationOverflow as S, resolveDshHome as _, generateFingerprint as a, classifyHttpError as b, resolveAntigravityVersionBounded as c, migrateToAgyDir as d, MASTER_KEY_REF as f, persistMasterKey as g, loadMasterKey as h, isAgyDisabled as i, clearExpiredState as l, deriveKey as m, maskProxyUrl as n, recordFingerprintVersion as o, createAesGcmCodec as p, AgySessionManager as r, resolveAntigravityVersion as s, JsonAccountStore as t, pickProbeProxyUrl as u, resolveMasterKeyCodec as v, describeFetchError as x, classifyFetchError as y };
2868
3035
 
2869
- //# sourceMappingURL=accounts-BN4h0AU4.mjs.map
3036
+ //# sourceMappingURL=accounts-D605JE2i.mjs.map