@cad0p/pi-usage 0.11.5 → 0.11.6

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.
@@ -11,11 +11,11 @@
11
11
  * Response shape: `groups[].buckets[]` with
12
12
  * `{ bucketId, displayName, window, resetTime, remainingFraction, description }`.
13
13
  *
14
- * Google does not rotate this refresh token, but Pi owns the credential and this
15
- * project is read-only, so no refresh is performed here either.
14
+ * Google does not rotate this refresh token, so an expired access token may be
15
+ * renewed in memory. The renewed value is cached only for this process lifetime.
16
16
  */
17
17
 
18
- import { buildWindow, clampPercent, degradedResult, displayIdentity, finiteNumber, stringValue, windowFromName } from "../model.js";
18
+ import { buildWindow, clampPercent, degradedResult, displayIdentity, finiteNumber, identityKind, stringValue, windowFromName } from "../model.js";
19
19
  import { requestJson } from "../http.js";
20
20
  import { describeOAuthClient, ensureFreshAccessToken } from "./antigravity-oauth.js";
21
21
 
@@ -109,6 +109,24 @@ 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
+
112
130
  /**
113
131
  * @param {import("../auth/pi-auth.js").PiCredential} credential
114
132
  * @param {{ now?: number, fetchFn?: typeof fetch, timeoutMs?: number, expiresInMin?: number | null, env?: Record<string, string | undefined>, refresh?: boolean }} [options]
@@ -139,7 +157,20 @@ export async function fetchQuota(credential, options = {}) {
139
157
  timeoutMs: options.timeoutMs,
140
158
  now,
141
159
  enabled: options.refresh,
160
+ env: options.env,
161
+ cacheKey: `${credential.source}\\u0000${credential.identity}`,
142
162
  });
163
+ // `expiresInMin` arrives from the engine as the freshness of the *stored*
164
+ // credential, and that is only the truth about this run when the refresh
165
+ // above did not happen: a token renewed in memory expires in an hour, so
166
+ // repeating the stored value made every surface print "token expired" (and
167
+ // the engine warn about it) next to healthy windows. A refresh that failed
168
+ // keeps the stored value — then the expiry claim is what the user must see.
169
+ const renewed = freshness.refreshed === true;
170
+ const expiresInMin = renewed ? null : options.expiresInMin ?? null;
171
+ // Present only when a renewal happened: an always-false field would be one
172
+ // more thing the JSON feed promises to keep.
173
+ const renewal = renewed ? { tokenRenewedInMemory: true } : {};
143
174
  let projectId = credential.projectId ?? null;
144
175
 
145
176
  /** @type {string | null} */
@@ -211,7 +242,9 @@ export async function fetchQuota(credential, options = {}) {
211
242
 
212
243
  return {
213
244
  ...base,
214
- account: projectId ? `${displayIdentity(credential)} · ${projectId.slice(0, 8)}…` : displayIdentity(credential),
245
+ account: accountLabel(credential, projectId),
246
+ expiresInMin,
247
+ ...renewal,
215
248
  plan: stringValue(body.paidTier) ?? stringValue(body.currentTier),
216
249
  windows,
217
250
  error: null,
@@ -221,5 +254,11 @@ export async function fetchQuota(credential, options = {}) {
221
254
  }
222
255
 
223
256
  const suffix = authFailed ? ` (${describeOAuthClient({ env: options.env })})` : "";
224
- return degradedResult({ ...base, error: `${lastError ?? "Antigravity quota unavailable"}${suffix}` });
257
+ return degradedResult({
258
+ ...base,
259
+ account: accountLabel(credential, projectId),
260
+ expiresInMin,
261
+ ...renewal,
262
+ error: `${lastError ?? "Antigravity quota unavailable"}${suffix}`,
263
+ });
225
264
  }
package/src/refresh.js CHANGED
@@ -278,7 +278,10 @@ export async function collectWithCadence(options) {
278
278
  : null;
279
279
 
280
280
  const report = mergeReports([cachedPart, fresh], { families, now, reused });
281
- writeCache(report, { path: cachePath, now });
281
+ // The cache carries the same per-family clocks this layer decided on, so a
282
+ // reader that only sees the cache (the CLI, the extension's first frame)
283
+ // judges each family by the time it was actually fetched.
284
+ writeCache(report, { path: cachePath, now, fetchedAtMs });
282
285
  writeRefreshState({ fetchedAtMs }, { statePath });
283
286
 
284
287
  /** @type {Record<string, number>} */