dsh-agy 0.4.0 → 0.4.1
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/README.md +21 -10
- package/lib/{accounts-BN4h0AU4.mjs → accounts-BjdDqUq1.mjs} +180 -13
- package/lib/accounts-BjdDqUq1.mjs.map +1 -0
- package/lib/cli/index.d.mts.map +1 -1
- package/lib/cli/index.mjs +4 -2
- package/lib/cli/index.mjs.map +1 -1
- package/lib/client.js +382 -34
- package/lib/index.mjs +2 -2
- package/lib/{plugin-common-N7p9Cm-3.mjs → plugin-common-B7OSfnyl.mjs} +300 -14
- package/lib/plugin-common-B7OSfnyl.mjs.map +1 -0
- package/lib/web/plugin.mjs +35 -13
- package/lib/web/plugin.mjs.map +1 -1
- package/package.json +1 -1
- package/lib/accounts-BN4h0AU4.mjs.map +0 -1
- package/lib/plugin-common-N7p9Cm-3.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 (
|
|
140
|
+
# 3. Optional: delete local account data (the whole agy data folder)
|
|
141
141
|
dsh-agy logout # remove accounts first (or skip)
|
|
142
|
-
rm -
|
|
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
|
|
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
|
-
|
|
272
|
-
|
|
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.
|
|
281
|
-
|
|
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
|
|
|
@@ -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
|
|
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();
|
|
@@ -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)
|
|
2049
|
-
|
|
2050
|
-
|
|
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) {
|
|
@@ -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
|
}
|
|
@@ -2864,6 +3031,6 @@ var JsonAccountStore = class {
|
|
|
2864
3031
|
}
|
|
2865
3032
|
};
|
|
2866
3033
|
//#endregion
|
|
2867
|
-
export {
|
|
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-
|
|
3036
|
+
//# sourceMappingURL=accounts-BjdDqUq1.mjs.map
|