@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.
- package/CHANGELOG.md +28 -0
- package/README.md +274 -12
- package/bin/piquota.js +49 -4
- package/extensions/quota-panel.ts +188 -31
- package/omarchy/cad0p.pi-usage/Agent.qml +33 -0
- package/omarchy/cad0p.pi-usage/Main.qml +333 -0
- package/omarchy/cad0p.pi-usage/Panel.qml +136 -0
- package/omarchy/cad0p.pi-usage/README.md +40 -0
- package/omarchy/cad0p.pi-usage/UsageContent.qml +1001 -0
- package/omarchy/cad0p.pi-usage/UsageLogic.js +603 -0
- package/omarchy/cad0p.pi-usage/assets/antigravity.svg +3 -0
- package/omarchy/cad0p.pi-usage/assets/claude.svg +1 -0
- package/omarchy/cad0p.pi-usage/assets/codex-light.svg +1 -0
- package/omarchy/cad0p.pi-usage/assets/codex.svg +1 -0
- package/omarchy/cad0p.pi-usage/assets/fireworks.svg +7 -0
- package/omarchy/cad0p.pi-usage/assets/github-actions.svg +3 -0
- package/omarchy/cad0p.pi-usage/assets/opencode-go.svg +3 -0
- package/omarchy/cad0p.pi-usage/manifest.json +56 -0
- package/package.json +2 -1
- package/src/auth/pi-auth.js +3 -0
- package/src/cli/args.js +5 -0
- package/src/cli/commands/stats.js +76 -0
- package/src/engine.js +44 -0
- package/src/local/history.js +1160 -0
- package/src/local/pi-sessions.js +1511 -0
- package/src/omarchy/publish.js +1 -65
- package/src/omarchy/record.js +82 -7
|
@@ -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
package/src/auth/pi-auth.js
CHANGED
|
@@ -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
|
|