agy-cli-usage 0.3.1 → 0.4.0
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 +8 -0
- package/README.md +86 -51
- package/dist/src/api.d.ts +12 -0
- package/dist/src/api.js +84 -0
- package/dist/src/api.js.map +1 -0
- package/dist/src/credentials.d.ts +22 -0
- package/{src → dist/src}/credentials.js +125 -143
- package/dist/src/credentials.js.map +1 -0
- package/dist/src/main.d.ts +20 -0
- package/dist/src/main.js +169 -0
- package/dist/src/main.js.map +1 -0
- package/dist/src/pty-fallback.d.ts +5 -0
- package/dist/src/pty-fallback.js +221 -0
- package/dist/src/pty-fallback.js.map +1 -0
- package/dist/src/quota.d.ts +7 -0
- package/dist/src/quota.js +81 -0
- package/dist/src/quota.js.map +1 -0
- package/dist/src/render.d.ts +3 -0
- package/dist/src/render.js +82 -0
- package/dist/src/render.js.map +1 -0
- package/dist/src/server.d.ts +2 -0
- package/dist/src/server.js +43 -0
- package/dist/src/server.js.map +1 -0
- package/dist/src/types.d.ts +67 -0
- package/dist/src/types.js +3 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/update.d.ts +17 -0
- package/dist/src/update.js +87 -0
- package/dist/src/update.js.map +1 -0
- package/package.json +17 -10
- package/server.js +0 -48
- package/src/api.js +0 -92
- package/src/main.js +0 -142
- package/src/pty-fallback.js +0 -211
- package/src/quota.js +0 -100
- package/src/render.js +0 -82
- package/src/update.js +0 -87
package/src/quota.js
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
// Normalizes quota data from either source (direct API JSON or PTY-parsed text)
|
|
2
|
-
// into one shape consumed by the renderer / JSON output / HTTP endpoint.
|
|
3
|
-
//
|
|
4
|
-
// Normalized model:
|
|
5
|
-
// {
|
|
6
|
-
// account, fetchedAt, source: 'api'|'pty', host?,
|
|
7
|
-
// note, // the API's overall description blurb
|
|
8
|
-
// groups: [
|
|
9
|
-
// { name, models, // e.g. "Gemini Models", "Gemini Flash, Gemini Pro"
|
|
10
|
-
// buckets: [
|
|
11
|
-
// { kind: 'weekly'|'5h', label,
|
|
12
|
-
// remainingFraction, // 0..1 (null if unknown)
|
|
13
|
-
// usedFraction, // 1 - remaining
|
|
14
|
-
// resetAt, // ISO string or null
|
|
15
|
-
// resetsInSeconds, // derived from resetAt - now (null if unknown)
|
|
16
|
-
// available, // true when nothing consumed (remainingFraction === 1)
|
|
17
|
-
// description } ] } ]
|
|
18
|
-
// }
|
|
19
|
-
|
|
20
|
-
function bucketKind(window, label) {
|
|
21
|
-
if (window === 'weekly' || /week/i.test(label)) return 'weekly';
|
|
22
|
-
if (window === '5h' || /5.?hour|five.?hour/i.test(label)) return '5h';
|
|
23
|
-
return window || label;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function secondsUntil(resetAt, now) {
|
|
27
|
-
if (!resetAt) return null;
|
|
28
|
-
const ms = new Date(resetAt).getTime() - now;
|
|
29
|
-
return Number.isFinite(ms) ? Math.max(0, Math.round(ms / 1000)) : null;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** Build a normalized snapshot from the raw retrieveUserQuotaSummary response. */
|
|
33
|
-
export function fromApi({ raw, host, account, tier }, nowMs = Date.now()) {
|
|
34
|
-
const groups = (raw.groups ?? []).map((g) => ({
|
|
35
|
-
name: g.displayName ?? 'Models',
|
|
36
|
-
models: (g.description ?? '').replace(/^Models within this group:\s*/i, '').trim(),
|
|
37
|
-
buckets: (g.buckets ?? []).map((b) => {
|
|
38
|
-
const remaining = typeof b.remainingFraction === 'number' ? b.remainingFraction : null;
|
|
39
|
-
return {
|
|
40
|
-
kind: bucketKind(b.window, b.displayName ?? ''),
|
|
41
|
-
label: b.displayName ?? b.window ?? '',
|
|
42
|
-
remainingFraction: remaining,
|
|
43
|
-
usedFraction: remaining == null ? null : 1 - remaining,
|
|
44
|
-
resetAt: b.resetTime ?? null,
|
|
45
|
-
resetsInSeconds: secondsUntil(b.resetTime, nowMs),
|
|
46
|
-
available: remaining === 1,
|
|
47
|
-
description: b.description ?? null,
|
|
48
|
-
};
|
|
49
|
-
}),
|
|
50
|
-
}));
|
|
51
|
-
return {
|
|
52
|
-
account: account ?? null,
|
|
53
|
-
tier: tier ?? null,
|
|
54
|
-
fetchedAt: new Date(nowMs).toISOString(),
|
|
55
|
-
source: 'api',
|
|
56
|
-
host: host ?? null,
|
|
57
|
-
note: raw.description ?? null,
|
|
58
|
-
groups,
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Build a normalized snapshot from PTY-parsed groups (see pty-fallback.js).
|
|
64
|
-
* Input groups: [{ name, models, buckets:[{ kind, label, remainingFraction,
|
|
65
|
-
* resetsInSeconds, available, description }] }]
|
|
66
|
-
*/
|
|
67
|
-
export function fromPty(parsed, nowMs = Date.now()) {
|
|
68
|
-
const groups = (parsed.groups ?? []).map((g) => ({
|
|
69
|
-
name: g.name,
|
|
70
|
-
models: g.models ?? '',
|
|
71
|
-
buckets: (g.buckets ?? []).map((b) => ({
|
|
72
|
-
kind: b.kind,
|
|
73
|
-
label: b.label,
|
|
74
|
-
remainingFraction: b.remainingFraction ?? null,
|
|
75
|
-
usedFraction: b.remainingFraction == null ? null : 1 - b.remainingFraction,
|
|
76
|
-
resetAt: b.resetsInSeconds != null ? new Date(nowMs + b.resetsInSeconds * 1000).toISOString() : null,
|
|
77
|
-
resetsInSeconds: b.resetsInSeconds ?? null,
|
|
78
|
-
available: b.available ?? b.remainingFraction === 1,
|
|
79
|
-
description: b.description ?? null,
|
|
80
|
-
})),
|
|
81
|
-
}));
|
|
82
|
-
return {
|
|
83
|
-
account: parsed.account ?? null,
|
|
84
|
-
tier: null,
|
|
85
|
-
fetchedAt: new Date(nowMs).toISOString(),
|
|
86
|
-
source: 'pty',
|
|
87
|
-
host: null,
|
|
88
|
-
note: parsed.note ?? null,
|
|
89
|
-
groups,
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/** Format a seconds duration like agy: "73h 53m" / "2h 7m" / "12m". */
|
|
94
|
-
export function formatDuration(seconds) {
|
|
95
|
-
if (seconds == null) return null;
|
|
96
|
-
const h = Math.floor(seconds / 3600);
|
|
97
|
-
const m = Math.floor((seconds % 3600) / 60);
|
|
98
|
-
if (h > 0) return `${h}h ${m}m`;
|
|
99
|
-
return `${m}m`;
|
|
100
|
-
}
|
package/src/render.js
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
// Renders a normalized quota snapshot as a terminal panel, mirroring agy's
|
|
2
|
-
// `/usage` layout (progress bar + percent + reset time per bucket).
|
|
3
|
-
|
|
4
|
-
import { formatDuration } from './quota.js';
|
|
5
|
-
|
|
6
|
-
const BAR_WIDTH = 50;
|
|
7
|
-
|
|
8
|
-
const useColor = () => process.stdout.isTTY && !process.env.NO_COLOR;
|
|
9
|
-
const c = (code, s) => (useColor() ? `\x1b[${code}m${s}\x1b[0m` : s);
|
|
10
|
-
const dim = (s) => c('2', s);
|
|
11
|
-
const bold = (s) => c('1', s);
|
|
12
|
-
|
|
13
|
-
// remaining-based color: lots left = green, getting low = yellow/red.
|
|
14
|
-
function barColor(remaining) {
|
|
15
|
-
if (remaining == null) return '37';
|
|
16
|
-
if (remaining > 0.5) return '32'; // green
|
|
17
|
-
if (remaining > 0.2) return '33'; // yellow
|
|
18
|
-
return '31'; // red
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function bar(remainingFraction) {
|
|
22
|
-
const frac = remainingFraction == null ? 0 : Math.max(0, Math.min(1, remainingFraction));
|
|
23
|
-
const filled = Math.round(frac * BAR_WIDTH);
|
|
24
|
-
const body = '█'.repeat(filled) + '░'.repeat(BAR_WIDTH - filled);
|
|
25
|
-
return useColor() ? `\x1b[${barColor(remainingFraction)}m${body}\x1b[0m` : body;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function bucketLine(b) {
|
|
29
|
-
const lines = [];
|
|
30
|
-
lines.push(` ${bold(b.label)}`);
|
|
31
|
-
if (b.available) {
|
|
32
|
-
lines.push(` [${bar(1)}] ${c('32', 'Quota available')}`);
|
|
33
|
-
} else {
|
|
34
|
-
const pct = b.remainingFraction == null ? '—' : `${(b.remainingFraction * 100).toFixed(2)}%`;
|
|
35
|
-
const remainPct = b.remainingFraction == null ? '' : `${Math.round(b.remainingFraction * 100)}% remaining`;
|
|
36
|
-
const dur = formatDuration(b.resetsInSeconds);
|
|
37
|
-
const reset = dur ? ` · ${dim(`Refreshes in ${dur}`)}` : '';
|
|
38
|
-
lines.push(` [${bar(b.remainingFraction)}] ${pct}`);
|
|
39
|
-
lines.push(` ${dim(remainPct)}${reset}`);
|
|
40
|
-
}
|
|
41
|
-
return lines.join('\n');
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** Returns the full panel as a string. */
|
|
45
|
-
export function renderPanel(snap) {
|
|
46
|
-
const out = [];
|
|
47
|
-
out.push('');
|
|
48
|
-
out.push(bold(' Models & Quota'));
|
|
49
|
-
if (snap.account) out.push(` ${dim('Account:')} ${snap.account}`);
|
|
50
|
-
out.push(` ${dim(`source: ${snap.source}${snap.host ? ` · ${snap.host}` : ''} · ${snap.fetchedAt}`)}`);
|
|
51
|
-
out.push('');
|
|
52
|
-
|
|
53
|
-
for (const g of snap.groups) {
|
|
54
|
-
out.push(bold(` ${g.name.toUpperCase()}`));
|
|
55
|
-
if (g.models) out.push(` ${dim(`Models within this group: ${g.models}`)}`);
|
|
56
|
-
out.push('');
|
|
57
|
-
for (const b of g.buckets) {
|
|
58
|
-
out.push(bucketLine(b));
|
|
59
|
-
out.push('');
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
if (snap.note) {
|
|
63
|
-
out.push(dim(wrap(snap.note, 76, ' │')));
|
|
64
|
-
}
|
|
65
|
-
return out.join('\n');
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function wrap(text, width, prefix) {
|
|
69
|
-
const words = text.split(/\s+/);
|
|
70
|
-
const lines = [];
|
|
71
|
-
let cur = '';
|
|
72
|
-
for (const w of words) {
|
|
73
|
-
if ((cur + ' ' + w).trim().length > width) {
|
|
74
|
-
lines.push(prefix + cur);
|
|
75
|
-
cur = w;
|
|
76
|
-
} else {
|
|
77
|
-
cur = (cur + ' ' + w).trim();
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
if (cur) lines.push(prefix + cur);
|
|
81
|
-
return lines.join('\n');
|
|
82
|
-
}
|
package/src/update.js
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
// Self-update + version helpers for the CLI.
|
|
2
|
-
//
|
|
3
|
-
// `agy-cli-usage update` check the registry and `npm install -g` if newer
|
|
4
|
-
// `agy-cli-usage update --check` report only, don't install
|
|
5
|
-
// `agy-cli-usage --version` print the installed version
|
|
6
|
-
|
|
7
|
-
import { execFileSync, spawnSync } from 'node:child_process';
|
|
8
|
-
import { readFileSync } from 'node:fs';
|
|
9
|
-
|
|
10
|
-
const PKG_NAME = 'agy-cli-usage';
|
|
11
|
-
|
|
12
|
-
/** Installed version, read from this package's package.json. */
|
|
13
|
-
export function currentVersion() {
|
|
14
|
-
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
15
|
-
return pkg.version;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Compare two dotted versions numerically (prerelease tags ignored).
|
|
20
|
-
* @returns negative if a<b, 0 if equal, positive if a>b
|
|
21
|
-
*/
|
|
22
|
-
export function semverCompare(a, b) {
|
|
23
|
-
const norm = (v) =>
|
|
24
|
-
String(v)
|
|
25
|
-
.replace(/^v/, '')
|
|
26
|
-
.split('-')[0]
|
|
27
|
-
.split('.')
|
|
28
|
-
.map((n) => parseInt(n, 10) || 0);
|
|
29
|
-
const pa = norm(a);
|
|
30
|
-
const pb = norm(b);
|
|
31
|
-
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
32
|
-
const d = (pa[i] || 0) - (pb[i] || 0);
|
|
33
|
-
if (d !== 0) return d;
|
|
34
|
-
}
|
|
35
|
-
return 0;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** Latest published version: prefer the user's configured registry (npm view), fall back to public. */
|
|
39
|
-
export async function latestVersion() {
|
|
40
|
-
try {
|
|
41
|
-
const out = execFileSync('npm', ['view', PKG_NAME, 'version'], {
|
|
42
|
-
encoding: 'utf8',
|
|
43
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
44
|
-
}).trim();
|
|
45
|
-
if (out) return out;
|
|
46
|
-
} catch {
|
|
47
|
-
// npm missing or offline — try the public registry directly
|
|
48
|
-
}
|
|
49
|
-
try {
|
|
50
|
-
const res = await fetch(`https://registry.npmjs.org/${PKG_NAME}/latest`);
|
|
51
|
-
if (res.ok) return (await res.json()).version;
|
|
52
|
-
} catch {
|
|
53
|
-
// offline
|
|
54
|
-
}
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Run the update flow.
|
|
60
|
-
* @param {{ checkOnly?: boolean }} opts
|
|
61
|
-
* @returns {Promise<number>} process exit code
|
|
62
|
-
*/
|
|
63
|
-
export async function runUpdate({ checkOnly = false } = {}) {
|
|
64
|
-
const current = currentVersion();
|
|
65
|
-
const latest = await latestVersion();
|
|
66
|
-
if (!latest) {
|
|
67
|
-
process.stderr.write('Could not determine the latest version (offline or npm unavailable).\n');
|
|
68
|
-
return 1;
|
|
69
|
-
}
|
|
70
|
-
if (semverCompare(latest, current) <= 0) {
|
|
71
|
-
process.stdout.write(`agy-cli-usage is up to date (${current}).\n`);
|
|
72
|
-
return 0;
|
|
73
|
-
}
|
|
74
|
-
process.stdout.write(`Update available: ${current} -> ${latest}\n`);
|
|
75
|
-
if (checkOnly) {
|
|
76
|
-
process.stdout.write('Run `agy-cli-usage update` to install it.\n');
|
|
77
|
-
return 0;
|
|
78
|
-
}
|
|
79
|
-
process.stdout.write(`Installing ${PKG_NAME}@${latest} globally…\n`);
|
|
80
|
-
const r = spawnSync('npm', ['install', '-g', `${PKG_NAME}@${latest}`], { stdio: 'inherit' });
|
|
81
|
-
if (r.error) {
|
|
82
|
-
process.stderr.write(`Failed to run npm: ${r.error.message}\nInstall manually: npm install -g ${PKG_NAME}@latest\n`);
|
|
83
|
-
return 1;
|
|
84
|
-
}
|
|
85
|
-
if (r.status === 0) process.stdout.write(`Updated to ${latest}.\n`);
|
|
86
|
-
return r.status ?? 0;
|
|
87
|
-
}
|