dsh-agy-link 0.4.18 → 0.4.20
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/CHANGELOG.md +17 -0
- package/dist/index.js +70 -21
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.20 (2026-08-27)
|
|
4
|
+
|
|
5
|
+
- **Fixed: Primary Account Quota Fetched With a STALE Token (主账号刷新/同步拿的值不对).**
|
|
6
|
+
- **Root cause (verified live)**: on macOS, agy >= 1.1.15 keeps its CURRENT credential in the Keychain; the on-disk `antigravity-oauth-token` was a stale leftover from a PREVIOUS account's login. `getStoredToken` read the disk file FIRST and only fell back to the Keychain — so every 刷新/同步 used the old account's (still-valid) token and displayed ITS quota under the current login's name (observed: agy authenticated as q98… while the disk file still held elegantmanco's token; UI showed the wrong account's 5h/weekly numbers).
|
|
7
|
+
- **Keychain-first token resolution**: for the primary / system-HOME account the Keychain credential now WINS over the on-disk file (disk remains the fallback for older agy builds and non-mac systems). Isolated pool accounts are pinned by test to never touch the shared Keychain. Verified end-to-end against live Google endpoints: the primary's token identity and quota now match the actual agy login (5h 91% / weekly 31% instead of the stale account's numbers).
|
|
8
|
+
- **Token-anchored identity on manual refresh**: `refreshAccountQuota(force=true)` additionally calls the OAuth userinfo endpoint once per explicit user click and re-labels the slot to the token's TRUE owner (`resetAccountIdentity`), so an external `agy logout` + re-login self-heals in one click even when logs disagree.
|
|
9
|
+
- **Log detection hardened**: `detectEmailFromAgyLogs` returns the LAST match per file (append-ordered logs → newest login wins), fixing first-match returning a superseded account.
|
|
10
|
+
- **Risk posture preserved**: background polls (force=false) still NEVER call userinfo — zero extra network on the automatic path (pinned by a regression test).
|
|
11
|
+
|
|
12
|
+
## 0.4.19 (2026-08-26)
|
|
13
|
+
|
|
14
|
+
- **Fixed: Antigravity Models Missing From the Model Picker (登录成功、额度正常但模型列表为空 — issue #1).**
|
|
15
|
+
- **Root cause**: when `agy models` lists a bare Gemini base alongside its effort variants (the agy 1.1.13 output shape, e.g. `gemini-3.7-flash` + `gemini-3.7-flash-medium`), `foldEfforts` emitted the base id TWICE — once as the folded base entry and once as a verbatim row. DSH's `llm.listModels` contract throws `INVALID_CATALOG` on any duplicate model id, and the host's model-catalog builder then drops the ENTIRE Antigravity provider group from the picker — so login and quota panels looked perfectly healthy while no Antigravity model could be selected.
|
|
16
|
+
- **Fix (three layers)**: (1) `foldEfforts` now absorbs the bare base into its folded entry instead of duplicating it; (2) `parseModelsOutput` dedupes repeated raw slugs (first occurrence wins); (3) `AgyAdapter.listModels` dedupes ids as a final guard, so even a user-configured `fallbackModels` list containing repeats can never nuke the whole group.
|
|
17
|
+
- Regression tests pin both bare-base-plus-variants shapes and the duplicate-slug parse, plus an adapter-level uniqueness guard test.
|
|
18
|
+
- **Observability**: when the adapter-level guard drops duplicate ids it now logs which ids were removed (`model catalog contained duplicate ids [...]`) so field instances surface in DSH server logs instead of being silently masked. When reporting picker issues, attach the `/agy doctor` report (`~/.dsh/agy-link/diagnostics/doctor-*.md`) and the raw `agy models` output.
|
|
19
|
+
|
|
3
20
|
## 0.4.18 (2026-08-25)
|
|
4
21
|
|
|
5
22
|
- **Quota Fallback Never Clobbers Good Data (5h=100%/weekly-missing 根因).**
|
package/dist/index.js
CHANGED
|
@@ -1074,7 +1074,7 @@ function parseModelsOutput(stdout) {
|
|
|
1074
1074
|
if (text === "") return [];
|
|
1075
1075
|
try {
|
|
1076
1076
|
const list = extractModelList(JSON.parse(text));
|
|
1077
|
-
if (list) return list;
|
|
1077
|
+
if (list) return dedupeBySlug(list);
|
|
1078
1078
|
} catch {}
|
|
1079
1079
|
const out = [];
|
|
1080
1080
|
for (const line of text.split(/\n/)) {
|
|
@@ -1090,6 +1090,17 @@ function parseModelsOutput(stdout) {
|
|
|
1090
1090
|
label: t
|
|
1091
1091
|
});
|
|
1092
1092
|
}
|
|
1093
|
+
return dedupeBySlug(out);
|
|
1094
|
+
}
|
|
1095
|
+
/** First occurrence wins: duplicate raw slugs would become duplicate catalog ids. */
|
|
1096
|
+
function dedupeBySlug(raw) {
|
|
1097
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1098
|
+
const out = [];
|
|
1099
|
+
for (const r of raw) {
|
|
1100
|
+
if (seen.has(r.slug)) continue;
|
|
1101
|
+
seen.add(r.slug);
|
|
1102
|
+
out.push(r);
|
|
1103
|
+
}
|
|
1093
1104
|
return out;
|
|
1094
1105
|
}
|
|
1095
1106
|
function extractModelList(parsed) {
|
|
@@ -1189,7 +1200,7 @@ function foldEfforts(raw) {
|
|
|
1189
1200
|
};
|
|
1190
1201
|
folded.sort((a, b) => rank(a) - rank(b));
|
|
1191
1202
|
verbatim.sort((a, b) => rank(a) - rank(b));
|
|
1192
|
-
return [...folded, ...verbatim];
|
|
1203
|
+
return [...folded, ...verbatim.filter((e) => !bases.has(e.id))];
|
|
1193
1204
|
}
|
|
1194
1205
|
function stripEffortLabel(label, eff) {
|
|
1195
1206
|
const re = new RegExp("\\s*\\(?" + eff + "\\)?\\s*$", "i");
|
|
@@ -2071,12 +2082,25 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
2071
2082
|
}
|
|
2072
2083
|
async listModels(_provider) {
|
|
2073
2084
|
this.deps.catalog.refreshIfNeeded();
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2085
|
+
const cat = this.deps.catalog.get();
|
|
2086
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2087
|
+
const models = [];
|
|
2088
|
+
const dropped = [];
|
|
2089
|
+
for (const m of cat.models) {
|
|
2090
|
+
if (seen.has(m.id)) {
|
|
2091
|
+
dropped.push(m.id);
|
|
2092
|
+
continue;
|
|
2093
|
+
}
|
|
2094
|
+
seen.add(m.id);
|
|
2095
|
+
models.push({
|
|
2096
|
+
provider: PROVIDER_ID,
|
|
2097
|
+
id: m.id,
|
|
2098
|
+
name: m.name,
|
|
2099
|
+
inputModalities: ["text", "image"]
|
|
2100
|
+
});
|
|
2101
|
+
}
|
|
2102
|
+
if (dropped.length > 0) this.warnOnce("catalog-dupes", "model catalog contained duplicate ids [" + dropped.join(", ") + "] — kept first occurrence so DSH does not drop the whole provider group (INVALID_CATALOG)");
|
|
2103
|
+
return models;
|
|
2080
2104
|
}
|
|
2081
2105
|
async resolveModel(_provider, model, _signal) {
|
|
2082
2106
|
const cfg = this.deps.getConfig();
|
|
@@ -27702,8 +27726,11 @@ function detectEmailFromAgyLogs(homeDir) {
|
|
|
27702
27726
|
time: statSync(join(logDir, f)).mtimeMs
|
|
27703
27727
|
})).sort((a, b) => b.time - a.time).slice(0, 5);
|
|
27704
27728
|
for (const file of files) try {
|
|
27705
|
-
const
|
|
27706
|
-
|
|
27729
|
+
const content = readFileSync(join(logDir, file.name), "utf8");
|
|
27730
|
+
const re = /(?:authenticated successfully as|applyAuthResult:\s*email=|"email"\s*:\s*"|User:\s*)\s*([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/gi;
|
|
27731
|
+
let last;
|
|
27732
|
+
for (let m = re.exec(content); m !== null; m = re.exec(content)) if (m[1]) last = m[1];
|
|
27733
|
+
if (last) return last;
|
|
27707
27734
|
} catch {}
|
|
27708
27735
|
} catch {}
|
|
27709
27736
|
}
|
|
@@ -27790,21 +27817,39 @@ var QuotaService = class {
|
|
|
27790
27817
|
return join(home, ".gemini", "antigravity-cli", "antigravity-oauth-token");
|
|
27791
27818
|
}
|
|
27792
27819
|
/**
|
|
27820
|
+
* Read the system-HOME Keychain credential. Protected so tests (and future
|
|
27821
|
+
* platforms) can substitute the reader without touching the real Keychain.
|
|
27822
|
+
*/
|
|
27823
|
+
readSystemKeychainToken() {
|
|
27824
|
+
return readMacKeychainToken();
|
|
27825
|
+
}
|
|
27826
|
+
/**
|
|
27793
27827
|
* Read the active token document and normalize it to a flat StoredToken.
|
|
27794
|
-
*
|
|
27795
|
-
*
|
|
27828
|
+
*
|
|
27829
|
+
* Precedence for the primary / system-HOME account: the macOS Keychain
|
|
27830
|
+
* WINS over the on-disk token file. agy >= 1.1.15 keeps its CURRENT
|
|
27831
|
+
* credential in the Keychain; the on-disk antigravity-oauth-token can be a
|
|
27832
|
+
* stale leftover from a PREVIOUS account's login (verified live: disk
|
|
27833
|
+
* held an old account's token while agy itself was authenticated as
|
|
27834
|
+
* someone else — disk-first precedence made every quota refresh fetch the
|
|
27835
|
+
* WRONG account's numbers). Isolated pool accounts only ever read their
|
|
27836
|
+
* own directory's file; the Keychain is one shared slot they must not see.
|
|
27796
27837
|
*/
|
|
27797
27838
|
getStoredToken(account) {
|
|
27798
|
-
const
|
|
27799
|
-
|
|
27800
|
-
|
|
27801
|
-
|
|
27802
|
-
|
|
27839
|
+
const disk = (() => {
|
|
27840
|
+
const file = this.getTokenFilePath(account);
|
|
27841
|
+
if (!existsSync(file)) return null;
|
|
27842
|
+
try {
|
|
27843
|
+
const tok = normalizeStoredToken(JSON.parse(readFileSync(file, "utf8")));
|
|
27844
|
+
if (tok && (tok.accessToken || tok.refreshToken)) return tok;
|
|
27845
|
+
} catch {}
|
|
27846
|
+
return null;
|
|
27847
|
+
})();
|
|
27803
27848
|
if (account.systemHome || !account.dir) {
|
|
27804
|
-
const keychainToken =
|
|
27805
|
-
if (keychainToken) return keychainToken;
|
|
27849
|
+
const keychainToken = this.readSystemKeychainToken();
|
|
27850
|
+
if (keychainToken && (keychainToken.accessToken || keychainToken.refreshToken)) return keychainToken;
|
|
27806
27851
|
}
|
|
27807
|
-
return
|
|
27852
|
+
return disk;
|
|
27808
27853
|
}
|
|
27809
27854
|
/** Persist refreshed tokens back in the SAME on-disk shape agy wrote. */
|
|
27810
27855
|
persistRefreshedToken(account, tokens) {
|
|
@@ -27943,9 +27988,13 @@ var QuotaService = class {
|
|
|
27943
27988
|
const detected = detectEmailFromAgyLogs(home);
|
|
27944
27989
|
if (detected) email = detected;
|
|
27945
27990
|
}
|
|
27946
|
-
if (email && email !== account.email) this.pool.resetAccountIdentity(account.id, email);
|
|
27947
27991
|
const accessToken = await this.getValidAccessToken(account);
|
|
27948
27992
|
if (!accessToken) return null;
|
|
27993
|
+
if (force) {
|
|
27994
|
+
const info = await this.fetchUserInfo(accessToken, account.proxyUrl);
|
|
27995
|
+
if (info?.email) email = info.email;
|
|
27996
|
+
}
|
|
27997
|
+
if (email && email !== account.email) this.pool.resetAccountIdentity(account.id, email);
|
|
27949
27998
|
const [summary, discovered] = await Promise.all([this.fetchQuotaSummary(accessToken, account.proxyUrl), this.fetchAvailableModels(accessToken, account.proxyUrl)]);
|
|
27950
27999
|
if (!email) {
|
|
27951
28000
|
const info = await this.fetchUserInfo(accessToken, account.proxyUrl);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-agy-link",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.20",
|
|
4
4
|
"description": "Google Antigravity (agy CLI) models for DeepSeek Harness — stream Gemini/Claude/GPT-OSS subscriptions into DSH with thinking, tool activity, token usage and in-GUI Google OAuth login.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|