@cad0p/pi-usage 0.11.3 → 0.11.4

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.
@@ -0,0 +1,56 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "id": "cad0p.pi-usage",
4
+ "name": "AI Usage",
5
+ "version": "1.0.0",
6
+ "author": "cad0p",
7
+ "license": "MIT",
8
+ "description": "Every AI subscription's limits, accounts and token use in one scrolling bar panel, one collapsible section per provider. Derived from Omarchy's Agents plugin.",
9
+ "kinds": [
10
+ "bar-widget"
11
+ ],
12
+ "activation": "on-demand",
13
+ "entryPoints": {
14
+ "barWidget": "Panel.qml"
15
+ },
16
+ "barWidget": {
17
+ "displayName": "AI Usage",
18
+ "description": "One bar icon and one panel: every provider a collapsible section with its limits, every account of a multi-account provider, tokens by day and tokens by model.",
19
+ "category": "AI",
20
+ "defaultSection": "right",
21
+ "aliases": [
22
+ "ai-usage",
23
+ "pi-usage",
24
+ "quota"
25
+ ],
26
+ "allowMultiple": false,
27
+ "defaults": {
28
+ "providers": {},
29
+ "omarchyCollectors": "Auto",
30
+ "refreshIntervalSec": 900
31
+ },
32
+ "schema": [
33
+ {
34
+ "key": "omarchyCollectors",
35
+ "type": "enum",
36
+ "label": "Run Omarchy's collectors",
37
+ "options": [
38
+ "Auto",
39
+ "On",
40
+ "Off"
41
+ ],
42
+ "defaultValue": "Auto",
43
+ "description": "Auto runs omarchy-agent-usage-update only while the built-in Agents widget is not on the bar."
44
+ },
45
+ {
46
+ "key": "refreshIntervalSec",
47
+ "type": "integer",
48
+ "label": "Collector interval (seconds)",
49
+ "min": 30,
50
+ "max": 3600,
51
+ "step": 30,
52
+ "defaultValue": 900
53
+ }
54
+ ]
55
+ }
56
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cad0p/pi-usage",
3
- "version": "0.11.3",
3
+ "version": "0.11.4",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -16,6 +16,7 @@
16
16
  "bin",
17
17
  "src",
18
18
  "extensions",
19
+ "omarchy",
19
20
  "README.md",
20
21
  "CHANGELOG.md",
21
22
  "LICENSE"
@@ -24,6 +24,9 @@ export const FAMILY_BY_PROVIDER = {
24
24
  "openai-codex": "codex",
25
25
  anthropic: "claude",
26
26
  antigravity: "antigravity",
27
+ // Pi's built-in /login stores Antigravity under this key; `antigravity` is
28
+ // kept for stores written by older Pi releases and third-party plugins.
29
+ "google-antigravity": "antigravity",
27
30
  "opencode-go": "opencode-go",
28
31
  };
29
32
 
package/src/cli/args.js CHANGED
@@ -18,6 +18,7 @@
18
18
  * @property {boolean} version
19
19
  * @property {boolean} status
20
20
  * @property {boolean} clearCache
21
+ * @property {boolean} rebuildTotals
21
22
  * @property {number} ttlMs
22
23
  * @property {number} timeoutMs
23
24
  * @property {string[]} unknown Flags this parser does not own.
@@ -70,6 +71,7 @@ export function parseArgs(argv, defaults = {}) {
70
71
  status: false,
71
72
  box: false,
72
73
  clearCache: false,
74
+ rebuildTotals: false,
73
75
  ttlMs: defaults.defaultTtlMs ?? 60_000,
74
76
  timeoutMs: defaults.defaultTimeoutMs ?? 15_000,
75
77
  unknown: [],
@@ -142,6 +144,9 @@ export function parseArgs(argv, defaults = {}) {
142
144
  case "--clear-cache":
143
145
  parsed.clearCache = true;
144
146
  break;
147
+ case "--rebuild-totals":
148
+ parsed.rebuildTotals = true;
149
+ break;
145
150
  default:
146
151
  // Owned by a subcommand, which reads it from `raw` itself.
147
152
  if (SUBCOMMAND_FLAGS.has(arg)) break;
@@ -0,0 +1,76 @@
1
+ /**
2
+ * `piquota stats`: read or rebuild the durable token history.
3
+ *
4
+ * The history is the one place tokens accumulate across runs; this command is
5
+ * the read-only view of it and the recovery path for a lost `totals.json`.
6
+ */
7
+
8
+ import { acquireHistoryLock, loadTotals, rebuildTotalsFromMonths, releaseHistoryLock, resolveHistoryDir } from "../../local/history.js";
9
+ import { resolveLocalStatsCachePath } from "../../local/pi-sessions.js";
10
+ import { redact } from "../../http.js";
11
+ import { err, out } from "../output.js";
12
+
13
+ /**
14
+ * @param {{
15
+ * rebuild?: boolean,
16
+ * env?: Record<string, string | undefined>,
17
+ * home?: string,
18
+ * historyDir?: string,
19
+ * cachePath?: string,
20
+ * }} [options]
21
+ * @returns {number} Exit code.
22
+ */
23
+ export function runStatsCommand(options = {}) {
24
+ const env = options.env ?? process.env;
25
+ const historyDir = options.historyDir ?? resolveHistoryDir({ env, home: options.home });
26
+ /** @type {string[]} */
27
+ const warnings = [];
28
+
29
+ if (options.rebuild) {
30
+ // The rebuild rewrites the same file a scan freezes into, so it takes the
31
+ // same lock: a concurrent scan must not interleave its freeze with it.
32
+ const lockPath = `${options.cachePath ?? resolveLocalStatsCachePath({ env, home: options.home })}.lock`;
33
+ const lock = acquireHistoryLock({ lockPath });
34
+ if (!lock.acquired) {
35
+ err(`stats: another piquota run holds the history lock (${lock.path}); try again in a moment`);
36
+ return 1;
37
+ }
38
+ let result;
39
+ try {
40
+ result = rebuildTotalsFromMonths({ historyDir, warnings });
41
+ } finally {
42
+ releaseHistoryLock(lock);
43
+ }
44
+ for (const warning of warnings) err(`warning: ${warning}`);
45
+ if (!result.ok) {
46
+ err(`stats: could not rebuild the history totals: ${redact(result.error ?? "unknown error")}`);
47
+ return 1;
48
+ }
49
+ out(
50
+ `stats: rebuilt ${result.path} from ${result.months} month file(s): ${result.days} frozen day(s), ` +
51
+ `${result.families} family/ies, last frozen day ${result.lastFrozenDay || "none"}`,
52
+ );
53
+ return 0;
54
+ }
55
+
56
+ const history = loadTotals({ historyDir, warnings });
57
+ for (const warning of warnings) err(`warning: ${warning}`);
58
+ if (!history.exists || history.totals.families.size === 0) {
59
+ out(`stats: no frozen token history at ${historyDir}`);
60
+ return 0;
61
+ }
62
+
63
+ let prompts = 0;
64
+ let sessions = 0;
65
+ const days = new Set();
66
+ for (const family of history.totals.families.values()) {
67
+ prompts += family.totalPrompts;
68
+ sessions += family.totalSessions;
69
+ for (const date of family.activeDates) days.add(date);
70
+ }
71
+ out(`stats: ${historyDir}`);
72
+ out(` last frozen day: ${history.totals.lastFrozenDay || "none"}`);
73
+ out(` families: ${Array.from(history.totals.families.keys()).sort().join(", ")}`);
74
+ out(` frozen days: ${days.size} · prompts: ${prompts} · session-days: ${sessions}`);
75
+ return 0;
76
+ }
package/src/engine.js CHANGED
@@ -8,6 +8,8 @@
8
8
 
9
9
  import { checkFreshness, LABEL_BY_FAMILY, loadPiCredentials, resolveAuthPaths } from "./auth/pi-auth.js";
10
10
  import { loadClaudeCodeCredential } from "./auth/claude-code-auth.js";
11
+ import { redact } from "./http.js";
12
+ import { collectLocalStats } from "./local/pi-sessions.js";
11
13
  import { degradedResult, displayIdentity, selectPrimaryWindow } from "./model.js";
12
14
  import { backoffState, clearBackoff, isAuthFailure, isThrottled, recordBackoff, retryAfterFromError } from "./providers/backoff.js";
13
15
  import { fetchQuota as fetchClaude } from "./providers/claude.js";
@@ -166,6 +168,11 @@ const PROVIDERS = {
166
168
  * @property {string[]} warnings
167
169
  * @property {import("./model.js").QuotaResult[]} providers Flattened, family-ordered.
168
170
  * @property {Record<string, import("./model.js").QuotaResult[]>} byFamily
171
+ * @property {Record<string, import("./local/pi-sessions.js").LocalFamilyStats>} localStats
172
+ * Tokens Pi's own session files recorded, per family. Empty when the
173
+ * scan is disabled, still warming up or has nothing to report.
174
+ * @property {import("./local/pi-sessions.js").LocalScanMeta | null} localStatsMeta
175
+ * How the token scan went, including `partial` and `locked`.
169
176
  */
170
177
 
171
178
  /**
@@ -186,6 +193,11 @@ const PROVIDERS = {
186
193
  * stores?: import("./browser/cookies.js").CookieStore[],
187
194
  * allowBrowser?: boolean,
188
195
  * claudeCodePaths?: string[] | null,
196
+ * localStats?: boolean | import("./local/pi-sessions.js").LocalStatsOptions,
197
+ * Local token statistics from Pi's session files. Off unless asked for, so
198
+ * tests and library callers never read a real home directory; the CLI turns
199
+ * it on with a time budget. Pass an object to inject session roots or a
200
+ * cache path, or false to disable it explicitly.
189
201
  * }} [options]
190
202
  * @returns {Promise<PiQuotaReport>}
191
203
  */
@@ -239,6 +251,17 @@ export async function collectQuota(options = {}) {
239
251
 
240
252
  const hasOpenAi = loaded.rawProviders?.includes("openai") || Boolean(env.OPENAI_API_KEY);
241
253
 
254
+ // The local scan runs beside the network calls: a first full scan of the
255
+ // session store costs seconds, and adding that to the quota round-trip would
256
+ // push the /usage extension closer to its 30 s exec timeout.
257
+ const localStatsOptions = typeof options.localStats === "object" && options.localStats !== null ? options.localStats : {};
258
+ const localStatsTask =
259
+ options.localStats === undefined || options.localStats === false
260
+ ? null
261
+ : collectLocalStats({ env, home: options.home, now, ...localStatsOptions }).catch(
262
+ (error) => /** @type {{ error: unknown }} */ ({ error }),
263
+ );
264
+
242
265
  await Promise.all(
243
266
  families.map(async (family) => {
244
267
  const credentials = credentialsFor(family);
@@ -364,6 +387,25 @@ export async function collectQuota(options = {}) {
364
387
  provider.primaryWindowId = selectPrimaryWindow(provider.windows ?? [])?.id ?? null;
365
388
  }
366
389
 
390
+ // Local statistics are a bonus, never a reason for a provider to fail: a
391
+ // rejected scan is reported as one warning and the report carries no numbers.
392
+ /** @type {Record<string, import("./local/pi-sessions.js").LocalFamilyStats>} */
393
+ let localStats = {};
394
+ /** @type {PiQuotaReport["localStatsMeta"]} */
395
+ let localStatsMeta = null;
396
+ if (localStatsTask) {
397
+ const local = await localStatsTask;
398
+ if ("error" in local) {
399
+ warnings.push(
400
+ `local token stats: ${redact(/** @type {{ message?: string }} */ (local.error)?.message ?? String(local.error))}`,
401
+ );
402
+ } else {
403
+ localStats = local.byFamily;
404
+ localStatsMeta = local.scan;
405
+ warnings.push(...local.warnings);
406
+ }
407
+ }
408
+
367
409
  return {
368
410
  engine: "pi-quota",
369
411
  schemaVersion: 1,
@@ -376,6 +418,8 @@ export async function collectQuota(options = {}) {
376
418
  warnings,
377
419
  providers,
378
420
  byFamily,
421
+ localStats,
422
+ localStatsMeta,
379
423
  };
380
424
  }
381
425