@cad0p/pi-usage 0.11.5-20261103.0 → 0.11.5
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 +7 -26
- package/extensions/quota-panel.ts +9 -117
- package/package.json +1 -1
- package/src/engine.js +1 -5
- package/src/model.js +5 -39
- package/src/providers/antigravity-oauth.js +3 -11
- package/src/providers/antigravity.js +3 -41
package/README.md
CHANGED
|
@@ -40,9 +40,7 @@ shuvquota, and it never writes to a credential file.
|
|
|
40
40
|
* Antigravity's access token **may** be refreshed, but only in memory. Google
|
|
41
41
|
does not rotate that refresh token, so Pi's stored copy stays valid. Claude and
|
|
42
42
|
Codex are never refreshed, because they **do** rotate and persisting a rotated
|
|
43
|
-
token would break Pi.
|
|
44
|
-
that succeeded clears the stored "token expired" claim, and only a failed one
|
|
45
|
-
leaves the expiry (and the warning) on screen.
|
|
43
|
+
token would break Pi.
|
|
46
44
|
* The Claude Code store (`~/.claude/.credentials.json`) is opened read-only too, and
|
|
47
45
|
its refresh token is never even read into memory, so nothing here can rotate a
|
|
48
46
|
token the installed `claude` owns. `~/.claude.json` is read only for the account
|
|
@@ -302,20 +300,13 @@ dependencies to wrap the body in a `ScrollView`.
|
|
|
302
300
|
|
|
303
301
|
`tab` (or `2`) opens **History**, the second page: the durable token history for
|
|
304
302
|
one month, with the same foldable sections. A provider section lists the month's
|
|
305
|
-
days, the
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
one busy key does not hide the others). `←`/`→` (or
|
|
303
|
+
days, the token split by model, and — for a multi-account provider — one row per
|
|
304
|
+
account with its month and all-time totals (every account the family has in the
|
|
305
|
+
month *or* all-time, so one busy key does not hide the others). `←`/`→` (or
|
|
309
306
|
`[`/`]`) walk the browsable months — those with a file, plus the live month,
|
|
310
307
|
which is where History opens even before its first freeze — and `tab` (or `1`)
|
|
311
308
|
comes back to Now. The all-time
|
|
312
|
-
figure is `totals.json` (frozen through its watermark)
|
|
313
|
-
the same open day the month counts, so today is never missing from it. A rollup
|
|
314
|
-
that cannot cover the month on screen — a freeze whose totals write failed, a
|
|
315
|
-
month file restored behind `totals.json` — is still printed, dimmed, with
|
|
316
|
-
`~ all-time lags this month; run piquota stats --rebuild-totals` above the rows:
|
|
317
|
-
the figure is the only answer to "how much has this burned ever", so it is
|
|
318
|
-
explained rather than dropped. A family's open day
|
|
309
|
+
figure is `totals.json` (frozen through its watermark). A family's open day
|
|
319
310
|
(today) is merged from the live report only when the report was *generated* today
|
|
320
311
|
and the frozen month has no row for that family's day — per family, so one family
|
|
321
312
|
frozen early never hides another's live row — and a day is counted exactly once.
|
|
@@ -330,11 +321,7 @@ the last keypress asked for.
|
|
|
330
321
|
History reads `piquota stats --json [--month YYYY-MM]`: the month list, one
|
|
331
322
|
month's days per family/slot/model, the all-time rollup and notes. That command
|
|
332
323
|
never takes the writer lock and never writes, so it is safe to run while a scan
|
|
333
|
-
is freezing a day.
|
|
334
|
-
watermark, and neither `piquota stats` nor its JSON moves an open day into a
|
|
335
|
-
total. The History page merges the live day itself, for the month and for the
|
|
336
|
-
all-time figures alike, and prints the same "comes from the live report" note
|
|
337
|
-
under both.
|
|
324
|
+
is freezing a day.
|
|
338
325
|
|
|
339
326
|
A provider with several accounts is shown as one header (`OP-Go · 7 of 14 accounts
|
|
340
327
|
ok`) plus one line per account with every window and its reset, e.g.
|
|
@@ -452,12 +439,6 @@ Rp suite) without external network daemons or closed-source intermediaries:
|
|
|
452
439
|
* **Percentages and Countdown**: `usedPercent` and `remainingPercent` report
|
|
453
440
|
percentages (0..100); `resetsInSec` provides the exact integer seconds until
|
|
454
441
|
reset, immune to local clock skew.
|
|
455
|
-
* **`expiresInMin` and `tokenRenewedInMemory`**: the freshness of the credential the
|
|
456
|
-
run actually used. Antigravity's token is renewed in memory, so its result
|
|
457
|
-
carries `tokenRenewedInMemory: true` and `expiresInMin: null` instead of Pi's
|
|
458
|
-
stale stored countdown; a renewal that failed keeps the stored value, which is
|
|
459
|
-
what the panel's `token EXPIRED` badge and the warning line describe. The field
|
|
460
|
-
is only present when a renewal happened.
|
|
461
442
|
* **`notConfigured` vs `ok`**: unconfigured providers report `notConfigured: true`,
|
|
462
443
|
allowing consumers to cleanly omit them without displaying false-positive
|
|
463
444
|
error indicators.
|
|
@@ -845,7 +826,7 @@ extensions/quota-panel.ts Pi TUI line and /usage
|
|
|
845
826
|
## Tests
|
|
846
827
|
|
|
847
828
|
```bash
|
|
848
|
-
node --test tests/*.test.mjs #
|
|
829
|
+
node --test tests/*.test.mjs # 401 tests, fake tokens only, no network
|
|
849
830
|
```
|
|
850
831
|
|
|
851
832
|
Modules covered: `auth.json` parsing and de-duplication, the Claude Code store
|
|
@@ -1036,33 +1036,9 @@ type HistoryFamily = {
|
|
|
1036
1036
|
models: Map<string, number>;
|
|
1037
1037
|
accounts: Map<string, { tokens: number; prompts: number; models: Map<string, number> }>;
|
|
1038
1038
|
allTime: number;
|
|
1039
|
-
|
|
1040
|
-
allTimeAccounts: Map<string, { tokens: number; prompts: number }>;
|
|
1041
|
-
/** The live day's delta, added to both the month and the all-time figures. */
|
|
1042
|
-
liveTokens: number;
|
|
1043
|
-
livePrompts: number;
|
|
1044
|
-
liveAccounts: Map<string, { tokens: number; prompts: number }>;
|
|
1039
|
+
allTimeAccounts: Map<string, number>;
|
|
1045
1040
|
};
|
|
1046
1041
|
|
|
1047
|
-
/** An all-time figure, and whether the rollup behind it lags the month on screen. */
|
|
1048
|
-
type AllTimeFigure = { value: number; lags: boolean };
|
|
1049
|
-
|
|
1050
|
-
/**
|
|
1051
|
-
* The all-time figure for one scope. All time contains every frozen day, so it
|
|
1052
|
-
* can never be smaller than a month of it: a smaller rollup means totals.json is
|
|
1053
|
-
* behind the month file — a freeze whose totals write failed, a month restored
|
|
1054
|
-
* behind the rollup. That is printed, dimmed and with a note, never hidden: the
|
|
1055
|
-
* figure is the only answer to "how much has this burned ever", and dropping it
|
|
1056
|
-
* silently is a worse lie than a number with a caveat.
|
|
1057
|
-
*/
|
|
1058
|
-
function allTimeFigure(month: number, allTime: number): AllTimeFigure {
|
|
1059
|
-
if (allTime <= 0) return { value: 0, lags: false };
|
|
1060
|
-
return { value: allTime, lags: allTime < month };
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
|
-
/** The note printed with an all-time figure that cannot cover the month on screen. */
|
|
1064
|
-
const ALL_TIME_LAGS_NOTE = "all-time lags this month; run piquota stats --rebuild-totals";
|
|
1065
|
-
|
|
1066
1042
|
/**
|
|
1067
1043
|
* What outlives one overlay instance. Every bare `/usage` builds a fresh view,
|
|
1068
1044
|
* so the folds and the page it was closed on live in the extension — for the pi
|
|
@@ -1843,19 +1819,7 @@ export class UsageOverlay {
|
|
|
1843
1819
|
const ensure = (family: string): HistoryFamily => {
|
|
1844
1820
|
let entry = families.get(family);
|
|
1845
1821
|
if (!entry) {
|
|
1846
|
-
entry = {
|
|
1847
|
-
tokens: 0,
|
|
1848
|
-
prompts: 0,
|
|
1849
|
-
days: new Map(),
|
|
1850
|
-
models: new Map(),
|
|
1851
|
-
accounts: new Map(),
|
|
1852
|
-
allTime: 0,
|
|
1853
|
-
allTimePrompts: 0,
|
|
1854
|
-
allTimeAccounts: new Map(),
|
|
1855
|
-
liveTokens: 0,
|
|
1856
|
-
livePrompts: 0,
|
|
1857
|
-
liveAccounts: new Map(),
|
|
1858
|
-
};
|
|
1822
|
+
entry = { tokens: 0, prompts: 0, days: new Map(), models: new Map(), accounts: new Map(), allTime: 0, allTimeAccounts: new Map() };
|
|
1859
1823
|
families.set(family, entry);
|
|
1860
1824
|
}
|
|
1861
1825
|
return entry;
|
|
@@ -1920,10 +1884,6 @@ export class UsageOverlay {
|
|
|
1920
1884
|
const entry = ensure(family);
|
|
1921
1885
|
entry.tokens += tokens;
|
|
1922
1886
|
entry.prompts += prompts;
|
|
1923
|
-
// Kept apart from the frozen month: the all-time figures add this same
|
|
1924
|
-
// delta below, and a live day may only ever be counted once in each.
|
|
1925
|
-
entry.liveTokens += tokens;
|
|
1926
|
-
entry.livePrompts += prompts;
|
|
1927
1887
|
entry.days.set(today, (entry.days.get(today) ?? 0) + tokens);
|
|
1928
1888
|
for (const [model, modelTokens] of Object.entries(familyStats.todayTokensByModel ?? {})) {
|
|
1929
1889
|
entry.models.set(model, (entry.models.get(model) ?? 0) + Math.max(0, Math.round(Number(modelTokens) || 0)));
|
|
@@ -1931,12 +1891,8 @@ export class UsageOverlay {
|
|
|
1931
1891
|
for (const [rawLabel, account] of Object.entries(familyStats.byAccount ?? {})) {
|
|
1932
1892
|
const label = rawLabel === "#1" ? "" : rawLabel;
|
|
1933
1893
|
const target = entry.accounts.get(label) ?? { tokens: 0, prompts: 0, models: new Map<string, number>() };
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
target.tokens += accountTokens;
|
|
1937
|
-
target.prompts += accountPrompts;
|
|
1938
|
-
const liveAccount = entry.liveAccounts.get(label) ?? { tokens: 0, prompts: 0 };
|
|
1939
|
-
entry.liveAccounts.set(label, { tokens: liveAccount.tokens + accountTokens, prompts: liveAccount.prompts + accountPrompts });
|
|
1894
|
+
target.tokens += Math.max(0, Math.round(Number(account?.todayTokens) || 0));
|
|
1895
|
+
target.prompts += Math.max(0, Math.round(Number(account?.todayPrompts) || 0));
|
|
1940
1896
|
// The account's own models for today: without them the merged row
|
|
1941
1897
|
// shows a month total and no model, which reads as "no usage".
|
|
1942
1898
|
for (const [model, modelTokens] of Object.entries(account?.todayTokensByModel ?? {})) {
|
|
@@ -1950,33 +1906,10 @@ export class UsageOverlay {
|
|
|
1950
1906
|
for (const [family, totals] of Object.entries(payload.totals?.families ?? {})) {
|
|
1951
1907
|
const entry = ensure(family);
|
|
1952
1908
|
entry.allTime = Object.values(totals?.modelUsage ?? {}).reduce((sum, bucket) => sum + totalsModelTokens(bucket), 0);
|
|
1953
|
-
entry.allTimePrompts = Math.max(0, Math.round(Number(totals?.totalPrompts) || 0));
|
|
1954
1909
|
for (const [rawLabel, account] of Object.entries(totals?.accounts ?? {})) {
|
|
1955
1910
|
const label = rawLabel === "#1" ? "" : rawLabel;
|
|
1956
1911
|
const tokens = Object.values(account?.modelUsage ?? {}).reduce((sum, bucket) => sum + totalsModelTokens(bucket), 0);
|
|
1957
|
-
|
|
1958
|
-
const seen = entry.allTimeAccounts.get(label) ?? { tokens: 0, prompts: 0 };
|
|
1959
|
-
entry.allTimeAccounts.set(label, { tokens: seen.tokens + tokens, prompts: seen.prompts + prompts });
|
|
1960
|
-
}
|
|
1961
|
-
}
|
|
1962
|
-
|
|
1963
|
-
// Today's live day is part of all time, not only of this month: it is the
|
|
1964
|
-
// same usage the month row and the day row above already show, so it is
|
|
1965
|
-
// added here exactly once — the single place the live delta is folded into
|
|
1966
|
-
// a rollup, and only under the merge guard above (the day frozen, or a
|
|
1967
|
-
// report from another day, never produces a delta at all).
|
|
1968
|
-
//
|
|
1969
|
-
// A family with no rollup in totals.json gets nothing: there is no all-time
|
|
1970
|
-
// figure to extend, and calling today's tokens "all-time" would hide every
|
|
1971
|
-
// frozen day before it. The page already carries the CLI's note for that.
|
|
1972
|
-
for (const [family, entry] of families) {
|
|
1973
|
-
if (!payload.totals?.families?.[family]) continue;
|
|
1974
|
-
if (entry.liveTokens === 0 && entry.livePrompts === 0) continue;
|
|
1975
|
-
entry.allTime += entry.liveTokens;
|
|
1976
|
-
entry.allTimePrompts += entry.livePrompts;
|
|
1977
|
-
for (const [label, delta] of entry.liveAccounts) {
|
|
1978
|
-
const seen = entry.allTimeAccounts.get(label) ?? { tokens: 0, prompts: 0 };
|
|
1979
|
-
entry.allTimeAccounts.set(label, { tokens: seen.tokens + delta.tokens, prompts: seen.prompts + delta.prompts });
|
|
1912
|
+
entry.allTimeAccounts.set(label, (entry.allTimeAccounts.get(label) ?? 0) + tokens);
|
|
1980
1913
|
}
|
|
1981
1914
|
}
|
|
1982
1915
|
|
|
@@ -2003,29 +1936,10 @@ export class UsageOverlay {
|
|
|
2003
1936
|
// Multi-account families use the same short name as the Now panel header.
|
|
2004
1937
|
const display = accountLabels.length > 1 ? SHORT_NAME[family] ?? FAMILY_LABEL[family] ?? family : FAMILY_LABEL[family] ?? family;
|
|
2005
1938
|
const name = paint(BRAND[family] ?? "#8B949E", display);
|
|
2006
|
-
// The month first, then the all-time rollup with today's live day folded
|
|
2007
|
-
// in: every figure here counts a live day once. Prompts are scope-labelled
|
|
2008
|
-
// in their own row below, because an unlabelled count reads as the wrong
|
|
2009
|
-
// one of the two.
|
|
2010
|
-
//
|
|
2011
|
-
// Every all-time figure is computed before the first line is built: the
|
|
2012
|
-
// note a lagging rollup earns belongs at the top of the section, where it
|
|
2013
|
-
// explains the header, the prompts row and the account rows at once.
|
|
2014
1939
|
const meta = [`${formatTokens(entry.tokens)} in ${month}`];
|
|
2015
|
-
|
|
2016
|
-
if (
|
|
2017
|
-
const promptsAllTime = allTimeFigure(entry.prompts, entry.allTimePrompts);
|
|
2018
|
-
const accountAllTime = new Map<string, AllTimeFigure>();
|
|
2019
|
-
for (const accountLabel of accountLabels) {
|
|
2020
|
-
accountAllTime.set(
|
|
2021
|
-
accountLabel,
|
|
2022
|
-
allTimeFigure(entry.accounts.get(accountLabel)?.tokens ?? 0, entry.allTimeAccounts.get(accountLabel)?.tokens ?? 0),
|
|
2023
|
-
);
|
|
2024
|
-
}
|
|
2025
|
-
const lagsMonth =
|
|
2026
|
-
familyAllTime.lags || promptsAllTime.lags || Array.from(accountAllTime.values()).some((figure) => figure.lags);
|
|
1940
|
+
if (entry.allTime > 0) meta.push(`${formatTokens(entry.allTime)} all-time`);
|
|
1941
|
+
if (entry.prompts > 0) meta.push(`${entry.prompts} prompt${entry.prompts === 1 ? "" : "s"}`);
|
|
2027
1942
|
const body: string[] = [];
|
|
2028
|
-
if (lagsMonth) body.push(` ${this.theme.fg("warning", `~ ${ALL_TIME_LAGS_NOTE}`)}`);
|
|
2029
1943
|
const label = (text: string) => ` ${this.theme.fg("dim", text.padEnd(6))} `;
|
|
2030
1944
|
const rest = ` ${" ".repeat(8)}`;
|
|
2031
1945
|
const dayCells = Array.from(entry.days.entries())
|
|
@@ -2037,22 +1951,6 @@ export class UsageOverlay {
|
|
|
2037
1951
|
rest,
|
|
2038
1952
|
bodyWidth,
|
|
2039
1953
|
));
|
|
2040
|
-
// Both scopes, always: the live day is merged into each of them once, so
|
|
2041
|
-
// the month count and the all-time count may never disagree about it.
|
|
2042
|
-
const promptCells: string[] = [];
|
|
2043
|
-
if (entry.prompts > 0) promptCells.push(`${entry.prompts} in ${month}`);
|
|
2044
|
-
if (promptsAllTime.value > 0) {
|
|
2045
|
-
// A figure that lags the month is still a figure: it is printed, dimmed
|
|
2046
|
-
// whole, and the note above the rows says why it is small.
|
|
2047
|
-
promptCells.push(
|
|
2048
|
-
promptsAllTime.lags
|
|
2049
|
-
? this.theme.fg("dim", `${promptsAllTime.value} all-time`)
|
|
2050
|
-
: `${promptsAllTime.value} ${this.theme.fg("dim", "all-time")}`,
|
|
2051
|
-
);
|
|
2052
|
-
}
|
|
2053
|
-
if (promptCells.length > 0) {
|
|
2054
|
-
body.push(...wrapCells(promptCells, label("prompts"), rest, bodyWidth));
|
|
2055
|
-
}
|
|
2056
1954
|
if (entry.models.size > 0) {
|
|
2057
1955
|
const top = Array.from(entry.models.entries()).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
|
|
2058
1956
|
const shown = top.slice(0, 5).map(([model, tokens]) => `${shortModelName(model)} ${formatTokens(tokens)}`);
|
|
@@ -2064,14 +1962,8 @@ export class UsageOverlay {
|
|
|
2064
1962
|
for (const accountLabel of accountLabels) {
|
|
2065
1963
|
const account = entry.accounts.get(accountLabel) ?? { tokens: 0, prompts: 0, models: new Map<string, number>() };
|
|
2066
1964
|
const parts = [`${this.theme.fg("dim", "month")} ${formatTokens(account.tokens)}`];
|
|
2067
|
-
const
|
|
2068
|
-
if (
|
|
2069
|
-
parts.push(
|
|
2070
|
-
accountFigure.lags
|
|
2071
|
-
? this.theme.fg("dim", `all-time ${formatTokens(accountFigure.value)}`)
|
|
2072
|
-
: `${this.theme.fg("dim", "all-time")} ${formatTokens(accountFigure.value)}`,
|
|
2073
|
-
);
|
|
2074
|
-
}
|
|
1965
|
+
const allTime = entry.allTimeAccounts.get(accountLabel) ?? 0;
|
|
1966
|
+
if (allTime > 0) parts.push(`${this.theme.fg("dim", "all-time")} ${formatTokens(allTime)}`);
|
|
2075
1967
|
const best = Array.from(account.models.entries()).sort((a, b) => b[1] - a[1])[0];
|
|
2076
1968
|
if (best) parts.push(this.theme.fg("dim", shortModelName(best[0])));
|
|
2077
1969
|
body.push(` ${this.theme.fg("dim", accountLabel.padEnd(labelWidth))} ${parts.join(this.theme.fg("dim", " · "))}`);
|
package/package.json
CHANGED
package/src/engine.js
CHANGED
|
@@ -404,11 +404,7 @@ export async function collectQuota(options = {}) {
|
|
|
404
404
|
const results = [];
|
|
405
405
|
const familyWarnList = [];
|
|
406
406
|
for (const { result, warning, crashed } of outcomes) {
|
|
407
|
-
|
|
408
|
-
// that renewed the token in memory for it knows better than the store:
|
|
409
|
-
// "the Pi token expired 271316m ago" next to two healthy Antigravity
|
|
410
|
-
// windows was reading Pi's stale copy, not the token that was used.
|
|
411
|
-
if (warning && result.tokenRenewedInMemory !== true) familyWarnList.push(warning);
|
|
407
|
+
if (warning) familyWarnList.push(warning);
|
|
412
408
|
if (!crashed) {
|
|
413
409
|
// Bookkeeping must degrade like the provider call it follows: a
|
|
414
410
|
// failing backoff write is a warning, never a rejected family.
|
package/src/model.js
CHANGED
|
@@ -33,10 +33,6 @@
|
|
|
33
33
|
* @property {string} source Credential source description.
|
|
34
34
|
* @property {"pi" | "claude-code"} [sourceKind] Which kind of store produced it.
|
|
35
35
|
* @property {string | null} expiresInMin Credential freshness, when known.
|
|
36
|
-
* @property {true} [tokenRenewedInMemory] Present only when the provider renewed
|
|
37
|
-
* this token in memory for this run: the
|
|
38
|
-
* stored credential's freshness then no
|
|
39
|
-
* longer describes the request made.
|
|
40
36
|
*/
|
|
41
37
|
|
|
42
38
|
/**
|
|
@@ -251,7 +247,6 @@ export function humanReset(seconds) {
|
|
|
251
247
|
* source: string,
|
|
252
248
|
* now?: number,
|
|
253
249
|
* expiresInMin?: number | null,
|
|
254
|
-
* tokenRenewedInMemory?: boolean,
|
|
255
250
|
* windows?: QuotaWindow[],
|
|
256
251
|
* }} input
|
|
257
252
|
* @returns {QuotaResult}
|
|
@@ -261,7 +256,7 @@ export function degradedResult(input) {
|
|
|
261
256
|
input.notConfigured ??
|
|
262
257
|
(typeof input.error === "string" && input.error.startsWith("no ") && input.error.includes("credential in the Pi store")),
|
|
263
258
|
);
|
|
264
|
-
|
|
259
|
+
return {
|
|
265
260
|
family: input.family,
|
|
266
261
|
label: input.label,
|
|
267
262
|
account: input.account ?? "unknown",
|
|
@@ -275,27 +270,6 @@ export function degradedResult(input) {
|
|
|
275
270
|
sourceKind: input.sourceKind,
|
|
276
271
|
expiresInMin: input.expiresInMin ?? null,
|
|
277
272
|
};
|
|
278
|
-
// Only ever present when a provider renewed the token in memory: a field on
|
|
279
|
-
// every result would be one more thing the JSON feed promises to keep.
|
|
280
|
-
if (input.tokenRenewedInMemory === true) result.tokenRenewedInMemory = true;
|
|
281
|
-
return result;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
/**
|
|
285
|
-
* Which field `displayIdentity` would print for this credential.
|
|
286
|
-
*
|
|
287
|
-
* A provider that adds a second identifier of its own (Antigravity's Cloud Code
|
|
288
|
-
* project) needs to know whether the identity already *is* that identifier, or
|
|
289
|
-
* it prints the same value twice.
|
|
290
|
-
*
|
|
291
|
-
* @param {{ email?: string | null, accountId?: string | null, projectId?: string | null, identity?: string }} credential
|
|
292
|
-
* @returns {"email" | "accountId" | "projectId" | "identity" | "none"}
|
|
293
|
-
*/
|
|
294
|
-
export function identityKind(credential) {
|
|
295
|
-
if (credential.email) return "email";
|
|
296
|
-
if (credential.accountId) return "accountId";
|
|
297
|
-
if (credential.projectId) return "projectId";
|
|
298
|
-
return credential.identity ? "identity" : "none";
|
|
299
273
|
}
|
|
300
274
|
|
|
301
275
|
/**
|
|
@@ -305,18 +279,10 @@ export function identityKind(credential) {
|
|
|
305
279
|
* @returns {string}
|
|
306
280
|
*/
|
|
307
281
|
export function displayIdentity(credential) {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
return `${/** @type {string} */ (credential.accountId).slice(0, 8)}…`;
|
|
313
|
-
case "projectId":
|
|
314
|
-
return `project ${/** @type {string} */ (credential.projectId).slice(0, 8)}…`;
|
|
315
|
-
case "identity":
|
|
316
|
-
return `id ${/** @type {string} */ (credential.identity).slice(0, 8)}…`;
|
|
317
|
-
default:
|
|
318
|
-
return "unknown";
|
|
319
|
-
}
|
|
282
|
+
if (credential.email) return credential.email;
|
|
283
|
+
if (credential.accountId) return `${credential.accountId.slice(0, 8)}…`;
|
|
284
|
+
if (credential.projectId) return `project ${credential.projectId.slice(0, 8)}…`;
|
|
285
|
+
return credential.identity ? `id ${credential.identity.slice(0, 8)}…` : "unknown";
|
|
320
286
|
}
|
|
321
287
|
|
|
322
288
|
/**
|
|
@@ -89,15 +89,11 @@ export function resolveOAuthClient(options = {}) {
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
/**
|
|
92
|
-
*
|
|
93
|
-
* resolved it: a description of the environment's client while the request went
|
|
94
|
-
* out with the installed package's would name the wrong credential source.
|
|
95
|
-
*
|
|
96
|
-
* @param {{ client?: { clientId: string, clientSecret: string }, env?: Record<string, string | undefined>, sourcePath?: string }} [options]
|
|
92
|
+
* @param {{ client?: { clientId: string, clientSecret: string }, fetchFn?: typeof fetch, timeoutMs?: number }} [options]
|
|
97
93
|
* @returns {string}
|
|
98
94
|
*/
|
|
99
95
|
export function describeOAuthClient(options = {}) {
|
|
100
|
-
const client = options.client ?? resolveOAuthClient(
|
|
96
|
+
const client = options.client ?? resolveOAuthClient();
|
|
101
97
|
return client.source === "env"
|
|
102
98
|
? "client from ANTIGRAVITY_CLIENT_ID/SECRET"
|
|
103
99
|
: client.source === "pi-antigravity"
|
|
@@ -112,15 +108,12 @@ export function describeOAuthClient(options = {}) {
|
|
|
112
108
|
* fetchFn?: typeof fetch,
|
|
113
109
|
* timeoutMs?: number,
|
|
114
110
|
* now?: number,
|
|
115
|
-
* env?: Record<string, string | undefined>,
|
|
116
111
|
* }} [options]
|
|
117
112
|
* @returns {Promise<{ access: string, refresh: string, expires: number } | { error: string }>}
|
|
118
113
|
*/
|
|
119
114
|
export async function refreshAccessToken(refreshToken, options = {}) {
|
|
120
115
|
if (!refreshToken) return { error: "no refresh token available" };
|
|
121
|
-
|
|
122
|
-
// failed refresh (describeOAuthClient) is the one that was actually used.
|
|
123
|
-
const client = options.client ?? resolveOAuthClient({ env: options.env });
|
|
116
|
+
const client = options.client ?? resolveOAuthClient();
|
|
124
117
|
|
|
125
118
|
const response = await requestJson(GOOGLE_TOKEN_URL, {
|
|
126
119
|
method: "POST",
|
|
@@ -159,7 +152,6 @@ export async function refreshAccessToken(refreshToken, options = {}) {
|
|
|
159
152
|
* now?: number,
|
|
160
153
|
* bufferMs?: number,
|
|
161
154
|
* enabled?: boolean,
|
|
162
|
-
* env?: Record<string, string | undefined>,
|
|
163
155
|
* }} [options]
|
|
164
156
|
* @returns {Promise<{ ok: boolean, refreshed: boolean, error?: string }>}
|
|
165
157
|
*/
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* project is read-only, so no refresh is performed here either.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import { buildWindow, clampPercent, degradedResult, displayIdentity, finiteNumber,
|
|
18
|
+
import { buildWindow, clampPercent, degradedResult, displayIdentity, finiteNumber, stringValue, windowFromName } from "../model.js";
|
|
19
19
|
import { requestJson } from "../http.js";
|
|
20
20
|
import { describeOAuthClient, ensureFreshAccessToken } from "./antigravity-oauth.js";
|
|
21
21
|
|
|
@@ -109,24 +109,6 @@ function windowFromBucket(bucket, context, now) {
|
|
|
109
109
|
});
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
/**
|
|
113
|
-
* The account line for a resolved project.
|
|
114
|
-
*
|
|
115
|
-
* Pi stores no e-mail for Antigravity, so the project id *is* the display
|
|
116
|
-
* identity; appending it again printed "project rising-f… · rising-f…". An
|
|
117
|
-
* account that has a better identity (an e-mail or an id) still gets the
|
|
118
|
-
* project, because the same login can hold several.
|
|
119
|
-
*
|
|
120
|
-
* @param {import("../auth/pi-auth.js").PiCredential} credential
|
|
121
|
-
* @param {string | null} projectId
|
|
122
|
-
* @returns {string}
|
|
123
|
-
*/
|
|
124
|
-
function accountLabel(credential, projectId) {
|
|
125
|
-
const identity = displayIdentity(credential);
|
|
126
|
-
if (!projectId || identityKind(credential) === "projectId") return identity;
|
|
127
|
-
return `${identity} · ${projectId.slice(0, 8)}…`;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
112
|
/**
|
|
131
113
|
* @param {import("../auth/pi-auth.js").PiCredential} credential
|
|
132
114
|
* @param {{ now?: number, fetchFn?: typeof fetch, timeoutMs?: number, expiresInMin?: number | null, env?: Record<string, string | undefined>, refresh?: boolean }} [options]
|
|
@@ -157,19 +139,7 @@ export async function fetchQuota(credential, options = {}) {
|
|
|
157
139
|
timeoutMs: options.timeoutMs,
|
|
158
140
|
now,
|
|
159
141
|
enabled: options.refresh,
|
|
160
|
-
env: options.env,
|
|
161
142
|
});
|
|
162
|
-
// `expiresInMin` arrives from the engine as the freshness of the *stored*
|
|
163
|
-
// credential, and that is only the truth about this run when the refresh
|
|
164
|
-
// above did not happen: a token renewed in memory expires in an hour, so
|
|
165
|
-
// repeating the stored value made every surface print "token expired" (and
|
|
166
|
-
// the engine warn about it) next to healthy windows. A refresh that failed
|
|
167
|
-
// keeps the stored value — then the expiry claim is what the user must see.
|
|
168
|
-
const renewed = freshness.refreshed === true;
|
|
169
|
-
const expiresInMin = renewed ? null : options.expiresInMin ?? null;
|
|
170
|
-
// Present only when a renewal happened: an always-false field would be one
|
|
171
|
-
// more thing the JSON feed promises to keep.
|
|
172
|
-
const renewal = renewed ? { tokenRenewedInMemory: true } : {};
|
|
173
143
|
let projectId = credential.projectId ?? null;
|
|
174
144
|
|
|
175
145
|
/** @type {string | null} */
|
|
@@ -241,9 +211,7 @@ export async function fetchQuota(credential, options = {}) {
|
|
|
241
211
|
|
|
242
212
|
return {
|
|
243
213
|
...base,
|
|
244
|
-
account:
|
|
245
|
-
expiresInMin,
|
|
246
|
-
...renewal,
|
|
214
|
+
account: projectId ? `${displayIdentity(credential)} · ${projectId.slice(0, 8)}…` : displayIdentity(credential),
|
|
247
215
|
plan: stringValue(body.paidTier) ?? stringValue(body.currentTier),
|
|
248
216
|
windows,
|
|
249
217
|
error: null,
|
|
@@ -253,11 +221,5 @@ export async function fetchQuota(credential, options = {}) {
|
|
|
253
221
|
}
|
|
254
222
|
|
|
255
223
|
const suffix = authFailed ? ` (${describeOAuthClient({ env: options.env })})` : "";
|
|
256
|
-
return degradedResult({
|
|
257
|
-
...base,
|
|
258
|
-
account: accountLabel(credential, projectId),
|
|
259
|
-
expiresInMin,
|
|
260
|
-
...renewal,
|
|
261
|
-
error: `${lastError ?? "Antigravity quota unavailable"}${suffix}`,
|
|
262
|
-
});
|
|
224
|
+
return degradedResult({ ...base, error: `${lastError ?? "Antigravity quota unavailable"}${suffix}` });
|
|
263
225
|
}
|