@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
package/src/omarchy/publish.js
CHANGED
|
@@ -7,10 +7,9 @@
|
|
|
7
7
|
* Omarchy's own `claude.json` / `codex.json` records are never touched.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
11
11
|
import { homedir } from "node:os";
|
|
12
12
|
import { join } from "node:path";
|
|
13
|
-
import { fileURLToPath } from "node:url";
|
|
14
13
|
|
|
15
14
|
import { isPiRecordId, RECORD_PREFIX } from "./record.js";
|
|
16
15
|
|
|
@@ -57,68 +56,6 @@ function writeRecord(dir, id, record) {
|
|
|
57
56
|
return target;
|
|
58
57
|
}
|
|
59
58
|
|
|
60
|
-
/**
|
|
61
|
-
* Copy a file only if the target does not exist or its content differs.
|
|
62
|
-
* This is critical when writing to directories watched by inotify, such as
|
|
63
|
-
* Omarchy's plugins directory, where redundant writes trigger full shell reloads.
|
|
64
|
-
*
|
|
65
|
-
* @param {string} srcPath
|
|
66
|
-
* @param {string} dstPath
|
|
67
|
-
* @returns {boolean} Whether the file was written.
|
|
68
|
-
*/
|
|
69
|
-
function copyIfChanged(srcPath, dstPath) {
|
|
70
|
-
try {
|
|
71
|
-
if (existsSync(dstPath)) {
|
|
72
|
-
const srcBuf = readFileSync(srcPath);
|
|
73
|
-
const dstBuf = readFileSync(dstPath);
|
|
74
|
-
if (srcBuf.equals(dstBuf)) return false;
|
|
75
|
-
}
|
|
76
|
-
copyFileSync(srcPath, dstPath);
|
|
77
|
-
return true;
|
|
78
|
-
} catch {
|
|
79
|
-
return false;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Synchronize provider SVG icons into Omarchy plugin assets directories if present.
|
|
85
|
-
*
|
|
86
|
-
* @param {{ env?: Record<string, string | undefined>, home?: string }} [options]
|
|
87
|
-
* @returns {string[]} Paths of written assets.
|
|
88
|
-
*/
|
|
89
|
-
export function syncOmarchyAssets(options = {}) {
|
|
90
|
-
const env = options.env ?? process.env;
|
|
91
|
-
const home = options.home ?? (env.HOME || homedir());
|
|
92
|
-
const pluginsDir = join(home, ".config", "omarchy", "plugins");
|
|
93
|
-
if (!existsSync(pluginsDir)) return [];
|
|
94
|
-
|
|
95
|
-
const assetsSrc = fileURLToPath(new URL("assets", import.meta.url));
|
|
96
|
-
if (!existsSync(assetsSrc)) return [];
|
|
97
|
-
|
|
98
|
-
const copied = [];
|
|
99
|
-
try {
|
|
100
|
-
for (const plugin of readdirSync(pluginsDir)) {
|
|
101
|
-
const pluginDir = join(pluginsDir, plugin);
|
|
102
|
-
const manifestPath = join(pluginDir, "manifest.json");
|
|
103
|
-
if (!existsSync(manifestPath)) continue;
|
|
104
|
-
const targetAssetsDir = join(pluginDir, "assets");
|
|
105
|
-
if (!existsSync(targetAssetsDir)) {
|
|
106
|
-
mkdirSync(targetAssetsDir, { recursive: true });
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
for (const file of readdirSync(assetsSrc)) {
|
|
110
|
-
if (!file.endsWith(".svg")) continue;
|
|
111
|
-
const srcPath = join(assetsSrc, file);
|
|
112
|
-
const dstPath = join(targetAssetsDir, file);
|
|
113
|
-
const piDstPath = join(targetAssetsDir, `pi-${file}`);
|
|
114
|
-
if (copyIfChanged(srcPath, dstPath)) copied.push(dstPath);
|
|
115
|
-
if (copyIfChanged(srcPath, piDstPath)) copied.push(piDstPath);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
} catch {}
|
|
119
|
-
return copied;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
59
|
/**
|
|
123
60
|
* Write the given records and remove stale `pi-*.json` files whose provider is
|
|
124
61
|
* no longer present.
|
|
@@ -146,7 +83,6 @@ export function publishRecords(records, options = {}) {
|
|
|
146
83
|
removed.push(join(dir, name));
|
|
147
84
|
}
|
|
148
85
|
|
|
149
|
-
syncOmarchyAssets(options);
|
|
150
86
|
|
|
151
87
|
return { dir, written, removed };
|
|
152
88
|
}
|
package/src/omarchy/record.js
CHANGED
|
@@ -10,9 +10,15 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Record contract (see Omarchy's plugins/agents/Main.qml):
|
|
12
12
|
* id, name, ready, updatedAt, schemaVersion, tierLabel, usageStatusText,
|
|
13
|
-
* authHelpText and `limits[{label, title, percent, resetsAt}]`.
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* authHelpText and `limits[{label, title, percent, resetsAt}]`. When the
|
|
14
|
+
* report carries local token statistics for the family, the panel's optional
|
|
15
|
+
* stats fields are merged in (today's tokens and prompts, the last seven
|
|
16
|
+
* days, all-time per-model buckets) and `hasLocalStats` says they are there.
|
|
17
|
+
*
|
|
18
|
+
* A family with several accounts also carries `accounts[{id, ok, status,
|
|
19
|
+
* limits}]`: every window of every account, for panels that can draw them
|
|
20
|
+
* (this package's own Omarchy plugin). Omarchy's built-in panel ignores the
|
|
21
|
+
* key and keeps reading the flattened `limits`.
|
|
16
22
|
*
|
|
17
23
|
* `percent` is a FRACTION in 0..1 (the panel multiplies it by 100), so 40.3% used
|
|
18
24
|
* is written as 0.403.
|
|
@@ -296,6 +302,18 @@ function compactLabels(labels) {
|
|
|
296
302
|
return parts.join(", ");
|
|
297
303
|
}
|
|
298
304
|
|
|
305
|
+
/**
|
|
306
|
+
* The per-account status line: the same reason as the compact summary, as a
|
|
307
|
+
* sentence-case label ("Key rejected", "No Go plan").
|
|
308
|
+
*
|
|
309
|
+
* @param {string | null | undefined} error
|
|
310
|
+
* @returns {string}
|
|
311
|
+
*/
|
|
312
|
+
function accountStatus(error) {
|
|
313
|
+
const reason = failureReason(error);
|
|
314
|
+
return reason.charAt(0).toUpperCase() + reason.slice(1);
|
|
315
|
+
}
|
|
316
|
+
|
|
299
317
|
/**
|
|
300
318
|
* Explain failures compactly, grouped by reason in slot order, e.g.
|
|
301
319
|
* "#8–#10 key rejected · #11–#14 no Go plan".
|
|
@@ -330,6 +348,9 @@ function compactFailureList(failures) {
|
|
|
330
348
|
* summarised in the status and the compact help text, which is what makes a
|
|
331
349
|
* revoked key distinguishable from a workspace without a plan.
|
|
332
350
|
*
|
|
351
|
+
* `accounts` repeats the family per account with every window, untitled by
|
|
352
|
+
* slot (the account id already says which one), for panels that show them all.
|
|
353
|
+
*
|
|
333
354
|
* @param {string} family
|
|
334
355
|
* @param {import("../model.js").QuotaResult[]} providers
|
|
335
356
|
* @param {{ now?: number }} [options]
|
|
@@ -345,6 +366,8 @@ function buildFamilyRecord(family, providers, options = {}) {
|
|
|
345
366
|
const limits = [];
|
|
346
367
|
/** @type {Array<{ label: string, reason: string }>} */
|
|
347
368
|
const failures = [];
|
|
369
|
+
/** @type {Array<{ id: string, ok: boolean, status: string, limits: Array<{ label: string, title: string, percent: number, resetsAt: string }> }>} */
|
|
370
|
+
const accounts = [];
|
|
348
371
|
let okCount = 0;
|
|
349
372
|
|
|
350
373
|
providers.forEach((provider, index) => {
|
|
@@ -354,9 +377,11 @@ function buildFamilyRecord(family, providers, options = {}) {
|
|
|
354
377
|
const window = bindingWindow(provider.windows);
|
|
355
378
|
const mapped = window ? mapLimits([window], provider, `${label} · `)[0] : null;
|
|
356
379
|
if (mapped) limits.push(mapped);
|
|
380
|
+
accounts.push({ id: redact(label), ok: true, status: "", limits: mapLimits(provider.windows, provider) });
|
|
357
381
|
return;
|
|
358
382
|
}
|
|
359
383
|
failures.push({ label, reason: failureReason(provider.error) });
|
|
384
|
+
accounts.push({ id: redact(label), ok: false, status: accountStatus(provider.error), limits: [] });
|
|
360
385
|
});
|
|
361
386
|
|
|
362
387
|
const failedCount = providers.length - okCount;
|
|
@@ -378,22 +403,68 @@ function buildFamilyRecord(family, providers, options = {}) {
|
|
|
378
403
|
ready: okCount > 0,
|
|
379
404
|
tierLabel: `${okCount} of ${providers.length} accounts`,
|
|
380
405
|
limits: limits.length > 0 ? limits : [{ ...PLACEHOLDER_LIMIT }],
|
|
406
|
+
accounts,
|
|
381
407
|
usageStatusText: status.usageStatusText,
|
|
382
408
|
authHelpText: failures.length > 0 ? compactFailureList(failures) : status.authHelpText,
|
|
383
409
|
hasLocalStats: false,
|
|
384
410
|
hasPromptStats: false,
|
|
411
|
+
...localStatsFields(options.localStats),
|
|
385
412
|
updatedAt: new Date(now).toISOString(),
|
|
386
413
|
},
|
|
387
414
|
};
|
|
388
415
|
}
|
|
389
416
|
|
|
417
|
+
/**
|
|
418
|
+
* The optional token fields Omarchy's Agents panel reads. Only aggregate counts
|
|
419
|
+
* are copied: no prompt text, no session path, no account identity.
|
|
420
|
+
*
|
|
421
|
+
* `totalSessions` counts session-DAYS, not distinct session files: a session
|
|
422
|
+
* that runs past midnight is one session in 20 files of `sessions/` but two
|
|
423
|
+
* rows here, because history is frozen per local day and a session's identity is
|
|
424
|
+
* gone once its day is (see the README's token-stats section).
|
|
425
|
+
*
|
|
426
|
+
* @param {import("../local/pi-sessions.js").LocalFamilyStats | undefined} stats
|
|
427
|
+
* @returns {Record<string, unknown>}
|
|
428
|
+
*/
|
|
429
|
+
function localStatsFields(stats) {
|
|
430
|
+
if (!stats || stats.hasLocalStats !== true) return {};
|
|
431
|
+
const modelUsage = {};
|
|
432
|
+
for (const [model, bucket] of Object.entries(stats.modelUsage ?? {})) {
|
|
433
|
+
modelUsage[model] = {
|
|
434
|
+
inputTokens: Number(bucket?.inputTokens) || 0,
|
|
435
|
+
outputTokens: Number(bucket?.outputTokens) || 0,
|
|
436
|
+
cacheReadInputTokens: Number(bucket?.cacheReadInputTokens) || 0,
|
|
437
|
+
cacheCreationInputTokens: Number(bucket?.cacheCreationInputTokens) || 0,
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
return {
|
|
441
|
+
hasLocalStats: true,
|
|
442
|
+
hasPromptStats: true,
|
|
443
|
+
todayPrompts: Number(stats.todayPrompts) || 0,
|
|
444
|
+
todaySessions: Number(stats.todaySessions) || 0,
|
|
445
|
+
todayTotalTokens: Number(stats.todayTotalTokens) || 0,
|
|
446
|
+
todayTokensByModel: Object.fromEntries(
|
|
447
|
+
Object.entries(stats.todayTokensByModel ?? {}).map(([model, tokens]) => [model, Number(tokens) || 0]),
|
|
448
|
+
),
|
|
449
|
+
recentDays: (stats.recentDays ?? []).map((day) => ({
|
|
450
|
+
date: String(day?.date ?? ""),
|
|
451
|
+
messageCount: Number(day?.messageCount) || 0,
|
|
452
|
+
})),
|
|
453
|
+
totalPrompts: Number(stats.totalPrompts) || 0,
|
|
454
|
+
totalSessions: Number(stats.totalSessions) || 0,
|
|
455
|
+
activeDays: Number(stats.activeDays) || 0,
|
|
456
|
+
activeDates: [...(stats.activeDates ?? [])],
|
|
457
|
+
modelUsage,
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
|
|
390
461
|
/**
|
|
391
462
|
* Build the record for one provider, or `null` when the provider is simply not
|
|
392
463
|
* configured: a tab that only says "not configured" is noise, so it is omitted
|
|
393
464
|
* (and any record from an earlier run is removed by the publisher).
|
|
394
465
|
*
|
|
395
466
|
* @param {import("../model.js").QuotaResult} provider
|
|
396
|
-
* @param {{ now?: number }} [options]
|
|
467
|
+
* @param {{ now?: number, localStats?: import("../local/pi-sessions.js").LocalFamilyStats }} [options]
|
|
397
468
|
* @returns {{ id: string, record: Record<string, unknown> } | null}
|
|
398
469
|
*/
|
|
399
470
|
export function buildRecord(provider, options = {}) {
|
|
@@ -421,6 +492,7 @@ export function buildRecord(provider, options = {}) {
|
|
|
421
492
|
authHelpText: status.authHelpText,
|
|
422
493
|
hasLocalStats: false,
|
|
423
494
|
hasPromptStats: false,
|
|
495
|
+
...localStatsFields(options.localStats),
|
|
424
496
|
updatedAt: new Date(now).toISOString(),
|
|
425
497
|
},
|
|
426
498
|
};
|
|
@@ -434,7 +506,7 @@ export function buildRecord(provider, options = {}) {
|
|
|
434
506
|
* Never throws: a malformed provider entry is skipped.
|
|
435
507
|
*
|
|
436
508
|
* @param {import("../engine.js").PiQuotaReport} report
|
|
437
|
-
* @param {{ now?: number }} [options]
|
|
509
|
+
* @param {{ now?: number, localStats?: Record<string, import("../local/pi-sessions.js").LocalFamilyStats> }} [options]
|
|
438
510
|
* @returns {Array<{ id: string, record: Record<string, unknown> }>}
|
|
439
511
|
*/
|
|
440
512
|
export function buildRecords(report, options = {}) {
|
|
@@ -450,9 +522,12 @@ export function buildRecords(report, options = {}) {
|
|
|
450
522
|
const records = [];
|
|
451
523
|
for (const [family, providers] of groups) {
|
|
452
524
|
try {
|
|
525
|
+
// Local stats belong to the family, not to one account: Omarchy's panel
|
|
526
|
+
// draws one token section per tab.
|
|
527
|
+
const familyOptions = { ...options, localStats: report?.localStats?.[family] };
|
|
453
528
|
const built = providers.length > 1
|
|
454
|
-
? buildFamilyRecord(family, providers,
|
|
455
|
-
: buildRecord(providers[0],
|
|
529
|
+
? buildFamilyRecord(family, providers, familyOptions)
|
|
530
|
+
: buildRecord(providers[0], familyOptions);
|
|
456
531
|
if (built) records.push(built);
|
|
457
532
|
} catch {
|
|
458
533
|
// A broken entry must not take the other providers' records down with it.
|