agy-cli-usage 0.4.4 → 0.4.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.
- package/CHANGELOG.md +15 -0
- package/README.md +2 -0
- package/dist/src/api.d.ts +15 -1
- package/dist/src/api.js +27 -6
- package/dist/src/credentials.d.ts +1 -0
- package/dist/src/credentials.js +12 -0
- package/dist/src/main.js +22 -1
- package/dist/src/render.d.ts +2 -2
- package/dist/src/render.js +29 -3
- package/dist/src/update.d.ts +8 -1
- package/dist/src/update.js +24 -8
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,21 @@
|
|
|
10
10
|
|
|
11
11
|
* use plain v* tags in release-please ([#12](https://github.com/abruption/agy-cli-usage/issues/12)) ([74b648d](https://github.com/abruption/agy-cli-usage/commit/74b648df24967f71a43095a80e7340a6b5ac2e39)), closes [#9](https://github.com/abruption/agy-cli-usage/issues/9)
|
|
12
12
|
|
|
13
|
+
## [0.4.6](https://github.com/abruption/agy-cli-usage/compare/v0.4.5...v0.4.6) (2026-09-07)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Bug Fixes
|
|
17
|
+
|
|
18
|
+
* restore the "antigravity" User-Agent the quota API gates on ([#48](https://github.com/abruption/agy-cli-usage/issues/48)) ([c375d5a](https://github.com/abruption/agy-cli-usage/commit/c375d5a3e9c1698cbb1dfe998be6e7c388363986)), closes [#47](https://github.com/abruption/agy-cli-usage/issues/47)
|
|
19
|
+
|
|
20
|
+
## [0.4.5](https://github.com/abruption/agy-cli-usage/compare/v0.4.4...v0.4.5) (2026-07-03)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Bug Fixes
|
|
24
|
+
|
|
25
|
+
* derive UA from package version, name error classes, bound update-check timeouts, document env vars in --help, show cache freshness in watch mode ([62becdc](https://github.com/abruption/agy-cli-usage/commit/62becdcf4175d2cda3cec8dc3f5bba13005c0d59)), closes [#32](https://github.com/abruption/agy-cli-usage/issues/32)
|
|
26
|
+
* UA version, error class names, update-check timeouts, --help env vars, watch/cache indicator ([#37](https://github.com/abruption/agy-cli-usage/issues/37)) ([62becdc](https://github.com/abruption/agy-cli-usage/commit/62becdcf4175d2cda3cec8dc3f5bba13005c0d59))
|
|
27
|
+
|
|
13
28
|
## [0.4.4](https://github.com/abruption/agy-cli-usage/compare/v0.4.3...v0.4.4) (2026-07-03)
|
|
14
29
|
|
|
15
30
|
|
package/README.md
CHANGED
|
@@ -107,6 +107,8 @@ The token is **read only** from wherever `agy` stored it. Handled per platform a
|
|
|
107
107
|
|
|
108
108
|
Read order: `keyring → OS CLI → Windows credman → token file → PTY`. Override the file path with `AGY_OAUTH_TOKEN_FILE`.
|
|
109
109
|
|
|
110
|
+
Token files are tried in order: `AGY_OAUTH_TOKEN_FILE`, then `~/.gemini/antigravity-cli/antigravity-oauth-token`, then `~/.gemini/jetski-standalone-oauth-token`. The last one is checked only after every keyring backend has failed — on macOS it sits beside the Keychain entry and can hold an older grant from a different session, so it must not win over a working one.
|
|
111
|
+
|
|
110
112
|
## HTTP endpoint (optional)
|
|
111
113
|
|
|
112
114
|
```bash
|
package/dist/src/api.d.ts
CHANGED
|
@@ -1,7 +1,21 @@
|
|
|
1
1
|
import type { FetchResult } from './types.js';
|
|
2
|
+
/** Exported for direct unit testing — keeps the UA tied to the real package name/version. */
|
|
3
|
+
export declare function buildUserAgent(): string;
|
|
4
|
+
/**
|
|
5
|
+
* Why these are distinct: the endpoint uses 401 and 403 for unrelated problems
|
|
6
|
+
* and only one of them is fixed by signing in again.
|
|
7
|
+
* unauthorized — the token was rejected (expired/revoked): re-authenticate.
|
|
8
|
+
* not-entitled — the token is fine but the account has no Antigravity
|
|
9
|
+
* license ("You do not have a valid license of this
|
|
10
|
+
* product…"): signing in again changes nothing.
|
|
11
|
+
* no-project — HTTP 200 without `cloudaicompanionProject`.
|
|
12
|
+
* http — anything else (wrong host, 5xx, …).
|
|
13
|
+
*/
|
|
14
|
+
export type ApiErrorKind = 'unauthorized' | 'not-entitled' | 'no-project' | 'http';
|
|
2
15
|
declare class ApiError extends Error {
|
|
3
16
|
status: number;
|
|
4
|
-
|
|
17
|
+
kind: ApiErrorKind;
|
|
18
|
+
constructor(message: string, status: number, kind?: ApiErrorKind);
|
|
5
19
|
}
|
|
6
20
|
export interface FetchOptions {
|
|
7
21
|
host?: string;
|
package/dist/src/api.js
CHANGED
|
@@ -8,15 +8,30 @@
|
|
|
8
8
|
//
|
|
9
9
|
// Captured from live agy traffic (mitmproxy). The internal endpoint is undocumented;
|
|
10
10
|
// the PTY fallback exists for when it changes.
|
|
11
|
-
|
|
11
|
+
import { currentVersion } from './update.js';
|
|
12
|
+
// The UA is load-bearing, not cosmetic: Cloud Code picks the product from the
|
|
13
|
+
// User-Agent, not from the request body. Without an `antigravity` substring
|
|
14
|
+
// (case-insensitive, position and version format irrelevant) `loadCodeAssist`
|
|
15
|
+
// answers HTTP 200 but omits `cloudaicompanionProject`, so the quota call can
|
|
16
|
+
// never run — sending `ideType: "ANTIGRAVITY"` does not compensate. Dropping
|
|
17
|
+
// that substring is what broke v0.4.5 (#47). Verified against the live daily
|
|
18
|
+
// host on 2026-09-07.
|
|
19
|
+
/** Exported for direct unit testing — keeps the UA tied to the real package name/version. */
|
|
20
|
+
export function buildUserAgent() {
|
|
21
|
+
return `antigravity-agy-cli-usage/${currentVersion()} ${process.platform}/${process.arch}`;
|
|
22
|
+
}
|
|
23
|
+
const UA = buildUserAgent();
|
|
12
24
|
// Antigravity ships against the "daily" Cloud Code host; stable builds use the
|
|
13
25
|
// plain host. Try daily first (matches current CLI), fall back to prod.
|
|
14
26
|
const HOSTS = ['daily-cloudcode-pa.googleapis.com', 'cloudcode-pa.googleapis.com'];
|
|
15
27
|
class ApiError extends Error {
|
|
16
28
|
status;
|
|
17
|
-
|
|
29
|
+
kind;
|
|
30
|
+
constructor(message, status, kind) {
|
|
18
31
|
super(message);
|
|
32
|
+
this.name = 'ApiError';
|
|
19
33
|
this.status = status;
|
|
34
|
+
this.kind = kind ?? (status === 401 ? 'unauthorized' : status === 403 ? 'not-entitled' : 'http');
|
|
20
35
|
}
|
|
21
36
|
}
|
|
22
37
|
// Known limitation: this is the only email source `loadCodeAssist` exposes —
|
|
@@ -71,8 +86,11 @@ export async function fetchQuotaSummary(accessToken, opts = {}) {
|
|
|
71
86
|
metadata: { ideType: 'ANTIGRAVITY' },
|
|
72
87
|
});
|
|
73
88
|
const project = lca.cloudaicompanionProject;
|
|
74
|
-
if (!project)
|
|
75
|
-
throw new ApiError(
|
|
89
|
+
if (!project) {
|
|
90
|
+
throw new ApiError(`loadCodeAssist on ${host} returned HTTP 200 without cloudaicompanionProject — either the ` +
|
|
91
|
+
`request was not recognized as Antigravity (the API reads the product off the User-Agent, ` +
|
|
92
|
+
`which must contain "antigravity") or the signed-in account has no Antigravity entitlement`, 0, 'no-project');
|
|
93
|
+
}
|
|
76
94
|
const raw = await postInternal(host, accessToken, 'retrieveUserQuotaSummary', { project });
|
|
77
95
|
return {
|
|
78
96
|
raw,
|
|
@@ -83,8 +101,11 @@ export async function fetchQuotaSummary(accessToken, opts = {}) {
|
|
|
83
101
|
}
|
|
84
102
|
catch (err) {
|
|
85
103
|
lastErr = err;
|
|
86
|
-
// 404 / wrong-host -> try next candidate
|
|
87
|
-
|
|
104
|
+
// 404 / wrong-host -> try next candidate. A rejected token or a missing
|
|
105
|
+
// license is account-level, identical on every host: stop early.
|
|
106
|
+
// `no-project` deliberately keeps going — an account can be entitled on
|
|
107
|
+
// one channel and not the other, and one extra round trip is cheap.
|
|
108
|
+
if (err instanceof ApiError && (err.kind === 'unauthorized' || err.kind === 'not-entitled'))
|
|
88
109
|
throw err;
|
|
89
110
|
}
|
|
90
111
|
}
|
package/dist/src/credentials.js
CHANGED
|
@@ -35,6 +35,10 @@ const KEYRING_SERVICE = 'gemini';
|
|
|
35
35
|
const KEYRING_ACCOUNT = 'antigravity';
|
|
36
36
|
const B64_PREFIX = 'go-keyring-base64:';
|
|
37
37
|
class CredentialError extends Error {
|
|
38
|
+
constructor(message) {
|
|
39
|
+
super(message);
|
|
40
|
+
this.name = 'CredentialError';
|
|
41
|
+
}
|
|
38
42
|
}
|
|
39
43
|
// --- raw keyring read --------------------------------------------------------
|
|
40
44
|
async function readViaNapiEsm() {
|
|
@@ -123,10 +127,18 @@ function readViaWindowsCredman() {
|
|
|
123
127
|
}
|
|
124
128
|
// On headless Linux (no Secret Service) agy persists the token to a plain-JSON
|
|
125
129
|
// file instead of the keyring. Same payload shape, no `go-keyring-base64:` prefix.
|
|
130
|
+
//
|
|
131
|
+
// The jetski path is last on purpose. macOS keeps that file around next to the
|
|
132
|
+
// Keychain entry, and the two can hold *different* grants — an observed one was
|
|
133
|
+
// three days stale and belonged to a session with no Antigravity license, which
|
|
134
|
+
// still refreshes fine and only fails later at the quota call. Every keyring
|
|
135
|
+
// backend is tried first, so this is reached only where the alternative is no
|
|
136
|
+
// credential at all.
|
|
126
137
|
function readViaFile() {
|
|
127
138
|
const candidates = [
|
|
128
139
|
process.env.AGY_OAUTH_TOKEN_FILE,
|
|
129
140
|
join(homedir(), '.gemini', 'antigravity-cli', 'antigravity-oauth-token'),
|
|
141
|
+
join(homedir(), '.gemini', 'jetski-standalone-oauth-token'),
|
|
130
142
|
].filter((p) => Boolean(p));
|
|
131
143
|
for (const path of candidates) {
|
|
132
144
|
try {
|
package/dist/src/main.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// agy-cli-usage update [--check] self-update via npm
|
|
13
13
|
// agy-cli-usage --version | -v print the installed version
|
|
14
14
|
import { getAccessToken, CredentialError } from './credentials.js';
|
|
15
|
-
import { fetchQuotaSummary } from './api.js';
|
|
15
|
+
import { fetchQuotaSummary, ApiError } from './api.js';
|
|
16
16
|
import { captureUsageViaPty } from './pty-fallback.js';
|
|
17
17
|
import { fromApi, fromPty } from './quota.js';
|
|
18
18
|
import { renderPanel } from './render.js';
|
|
@@ -24,6 +24,15 @@ import { fileURLToPath } from 'node:url';
|
|
|
24
24
|
const CACHE_DIR = join(process.env.XDG_CACHE_HOME || join(homedir(), '.cache'), 'agy-usage');
|
|
25
25
|
const CACHE_FILE = join(CACHE_DIR, 'quota.json');
|
|
26
26
|
const CACHE_TTL_MS = 5 * 60 * 1000;
|
|
27
|
+
// The API path fails in ways that look alike on the wire but need different
|
|
28
|
+
// things from the user — see ApiErrorKind in api.ts. Printing "sign in again"
|
|
29
|
+
// for a missing license just sends people round a loop that cannot help.
|
|
30
|
+
const API_ERROR_HINTS = {
|
|
31
|
+
unauthorized: 'hint: the stored token was rejected — run `agy` and sign in again.',
|
|
32
|
+
'not-entitled': 'hint: the token is valid but this Google account has no Antigravity license. Signing in again ' +
|
|
33
|
+
'will not change that — check which account `agy` is using, or its subscription.',
|
|
34
|
+
'no-project': 'hint: `--source pty` reads the panel from `agy` itself and does not need this endpoint.',
|
|
35
|
+
};
|
|
27
36
|
const VALID_SOURCES = ['auto', 'api', 'pty'];
|
|
28
37
|
const VALID_CHANNELS = ['auto', 'daily', 'prod'];
|
|
29
38
|
const errMessage = (e) => (e instanceof Error ? e.message : String(e));
|
|
@@ -82,6 +91,15 @@ const HELP = `agy-cli-usage — Antigravity CLI (agy) usage/quota monitor
|
|
|
82
91
|
agy-cli-usage --no-cache | --refresh
|
|
83
92
|
agy-cli-usage update [--check] self-update via npm (--check: report only)
|
|
84
93
|
agy-cli-usage --version | -v
|
|
94
|
+
|
|
95
|
+
Environment variables:
|
|
96
|
+
AGY_OAUTH_TOKEN_FILE Override the token file path (headless fallback).
|
|
97
|
+
AGY_BIN Path to the agy binary (PTY source). Else resolved from
|
|
98
|
+
PATH, then ~/.local/bin.
|
|
99
|
+
XDG_CACHE_HOME Cache base dir (cache lives at
|
|
100
|
+
<base>/agy-usage/quota.json; default ~/.cache).
|
|
101
|
+
NO_COLOR Disable ANSI color in the rendered panel.
|
|
102
|
+
PORT / HOST HTTP server bind (server mode only).
|
|
85
103
|
`;
|
|
86
104
|
/** Exported for direct unit testing via an injected `cacheFile` — not part of the CLI's public surface. */
|
|
87
105
|
export function readCache(source, channel, cacheFile = CACHE_FILE) {
|
|
@@ -197,6 +215,9 @@ if (isMainModule()) {
|
|
|
197
215
|
}
|
|
198
216
|
else {
|
|
199
217
|
process.stderr.write(`error: ${errMessage(err)}\n`);
|
|
218
|
+
const hint = err instanceof ApiError ? API_ERROR_HINTS[err.kind] : undefined;
|
|
219
|
+
if (hint)
|
|
220
|
+
process.stderr.write(`${hint}\n`);
|
|
200
221
|
}
|
|
201
222
|
process.exit(1);
|
|
202
223
|
});
|
package/dist/src/render.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { Snapshot } from './types.js';
|
|
2
|
-
/** Returns the full panel as a string. */
|
|
3
|
-
export declare function renderPanel(snap: Snapshot): string;
|
|
2
|
+
/** Returns the full panel as a string. `nowMs` is injectable for testing. */
|
|
3
|
+
export declare function renderPanel(snap: Snapshot, nowMs?: number): string;
|
package/dist/src/render.js
CHANGED
|
@@ -46,14 +46,40 @@ function bucketLine(b) {
|
|
|
46
46
|
}
|
|
47
47
|
return lines.join('\n');
|
|
48
48
|
}
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
// `--watch`'s default interval (60s) is faster than the 5-minute quota cache
|
|
50
|
+
// TTL, so most refreshes just re-render a cached snapshot with no visual
|
|
51
|
+
// sign the fetch didn't actually happen. `fetchedAt` is set once, when the
|
|
52
|
+
// data was truly fetched (see quota.ts), and carried through untouched on a
|
|
53
|
+
// cache hit — so comparing it to "now" at render time is enough to tell the
|
|
54
|
+
// two cases apart without threading extra state through Snapshot/the cache.
|
|
55
|
+
// A small skew is tolerated so a genuinely fresh fetch isn't mislabeled
|
|
56
|
+
// "cached" just because rendering happened slightly after fetching.
|
|
57
|
+
const FRESHNESS_SKEW_MS = 3_000;
|
|
58
|
+
function formatAge(totalSeconds) {
|
|
59
|
+
const s = Math.max(0, Math.round(totalSeconds));
|
|
60
|
+
if (s < 60)
|
|
61
|
+
return `${s}s`;
|
|
62
|
+
const h = Math.floor(s / 3600);
|
|
63
|
+
const m = Math.floor((s % 3600) / 60);
|
|
64
|
+
if (h > 0)
|
|
65
|
+
return `${h}h ${m}m`;
|
|
66
|
+
return `${m}m ${s % 60}s`;
|
|
67
|
+
}
|
|
68
|
+
function freshnessSuffix(fetchedAt, nowMs) {
|
|
69
|
+
const ageMs = nowMs - new Date(fetchedAt).getTime();
|
|
70
|
+
if (!Number.isFinite(ageMs) || ageMs < FRESHNESS_SKEW_MS)
|
|
71
|
+
return '';
|
|
72
|
+
return dim(` (cached, refreshed ${formatAge(ageMs / 1000)} ago)`);
|
|
73
|
+
}
|
|
74
|
+
/** Returns the full panel as a string. `nowMs` is injectable for testing. */
|
|
75
|
+
export function renderPanel(snap, nowMs = Date.now()) {
|
|
51
76
|
const out = [];
|
|
52
77
|
out.push('');
|
|
53
78
|
out.push(bold(' Models & Quota'));
|
|
54
79
|
if (snap.account)
|
|
55
80
|
out.push(` ${dim('Account:')} ${snap.account}`);
|
|
56
|
-
out.push(` ${dim(`source: ${snap.source}${snap.host ? ` · ${snap.host}` : ''} · ${snap.fetchedAt}`)}`
|
|
81
|
+
out.push(` ${dim(`source: ${snap.source}${snap.host ? ` · ${snap.host}` : ''} · ${snap.fetchedAt}`)}` +
|
|
82
|
+
freshnessSuffix(snap.fetchedAt, nowMs));
|
|
57
83
|
out.push('');
|
|
58
84
|
for (const g of snap.groups) {
|
|
59
85
|
out.push(bold(` ${g.name.toUpperCase()}`));
|
package/dist/src/update.d.ts
CHANGED
|
@@ -9,8 +9,15 @@ export declare function currentVersion(): string;
|
|
|
9
9
|
* Returns negative if a<b, 0 if equal, positive if a>b.
|
|
10
10
|
*/
|
|
11
11
|
export declare function semverCompare(a: string, b: string): number;
|
|
12
|
+
export declare const NPM_VIEW_TIMEOUT_MS = 8000;
|
|
13
|
+
export declare const REGISTRY_FETCH_TIMEOUT_MS = 8000;
|
|
14
|
+
/** Exported for direct unit testing via injection — not part of the public surface. */
|
|
15
|
+
export interface LatestVersionDeps {
|
|
16
|
+
execNpmView?: (pkgName: string, timeoutMs: number) => string;
|
|
17
|
+
fetchRegistry?: (pkgName: string, timeoutMs: number) => Promise<Response>;
|
|
18
|
+
}
|
|
12
19
|
/** Latest published version: prefer the user's configured registry (npm view), fall back to public. */
|
|
13
|
-
export declare function latestVersion(): Promise<string | null>;
|
|
20
|
+
export declare function latestVersion(deps?: LatestVersionDeps): Promise<string | null>;
|
|
14
21
|
/** Run the update flow. Returns the intended process exit code. */
|
|
15
22
|
export declare function runUpdate({ checkOnly }?: {
|
|
16
23
|
checkOnly?: boolean;
|
package/dist/src/update.js
CHANGED
|
@@ -34,26 +34,42 @@ export function semverCompare(a, b) {
|
|
|
34
34
|
}
|
|
35
35
|
return 0;
|
|
36
36
|
}
|
|
37
|
+
// Both the `npm view` child process and the registry fetch fallback are
|
|
38
|
+
// bounded so a slow/unreachable registry can't hang the update check
|
|
39
|
+
// indefinitely — a timed-out attempt is treated the same as "unavailable"
|
|
40
|
+
// and falls through to the next strategy (or to `latestVersion` returning
|
|
41
|
+
// null, which `runUpdate` reports as "could not determine the latest version").
|
|
42
|
+
export const NPM_VIEW_TIMEOUT_MS = 8_000;
|
|
43
|
+
export const REGISTRY_FETCH_TIMEOUT_MS = 8_000;
|
|
44
|
+
function defaultExecNpmView(pkgName, timeoutMs) {
|
|
45
|
+
return execFileSync('npm', ['view', pkgName, 'version'], {
|
|
46
|
+
encoding: 'utf8',
|
|
47
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
48
|
+
timeout: timeoutMs,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
function defaultFetchRegistry(pkgName, timeoutMs) {
|
|
52
|
+
return fetch(`https://registry.npmjs.org/${pkgName}/latest`, { signal: AbortSignal.timeout(timeoutMs) });
|
|
53
|
+
}
|
|
37
54
|
/** Latest published version: prefer the user's configured registry (npm view), fall back to public. */
|
|
38
|
-
export async function latestVersion() {
|
|
55
|
+
export async function latestVersion(deps = {}) {
|
|
56
|
+
const execNpmView = deps.execNpmView ?? defaultExecNpmView;
|
|
57
|
+
const fetchRegistry = deps.fetchRegistry ?? defaultFetchRegistry;
|
|
39
58
|
try {
|
|
40
|
-
const out =
|
|
41
|
-
encoding: 'utf8',
|
|
42
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
43
|
-
}).trim();
|
|
59
|
+
const out = execNpmView(PKG_NAME, NPM_VIEW_TIMEOUT_MS).trim();
|
|
44
60
|
if (out)
|
|
45
61
|
return out;
|
|
46
62
|
}
|
|
47
63
|
catch {
|
|
48
|
-
// npm missing or
|
|
64
|
+
// npm missing, offline, or timed out — try the public registry directly
|
|
49
65
|
}
|
|
50
66
|
try {
|
|
51
|
-
const res = await
|
|
67
|
+
const res = await fetchRegistry(PKG_NAME, REGISTRY_FETCH_TIMEOUT_MS);
|
|
52
68
|
if (res.ok)
|
|
53
69
|
return (await res.json()).version;
|
|
54
70
|
}
|
|
55
71
|
catch {
|
|
56
|
-
// offline
|
|
72
|
+
// offline or timed out
|
|
57
73
|
}
|
|
58
74
|
return null;
|
|
59
75
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agy-cli-usage",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.6",
|
|
4
4
|
"description": "Headless usage/quota monitor for the Antigravity CLI (agy) — reads Cloud Code quota directly, with a PTY fallback. No IDE required.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "dist/src/main.d.ts",
|
|
@@ -56,9 +56,9 @@
|
|
|
56
56
|
},
|
|
57
57
|
"license": "MIT",
|
|
58
58
|
"devDependencies": {
|
|
59
|
-
"@eslint/js": "^
|
|
60
|
-
"@types/node": "^26.
|
|
61
|
-
"eslint": "^
|
|
59
|
+
"@eslint/js": "^10.0.1",
|
|
60
|
+
"@types/node": "^26.1.2",
|
|
61
|
+
"eslint": "^10.6.0",
|
|
62
62
|
"typescript": "^6.0.3",
|
|
63
63
|
"typescript-eslint": "^8.62.1"
|
|
64
64
|
}
|