agy-cli-usage 0.4.1 → 0.4.3

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 CHANGED
@@ -10,6 +10,20 @@
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.3](https://github.com/abruption/agy-cli-usage/compare/v0.4.2...v0.4.3) (2026-07-02)
14
+
15
+
16
+ ### Bug Fixes
17
+
18
+ * key the quota cache by source/channel, validate --source/--channel ([#33](https://github.com/abruption/agy-cli-usage/issues/33)) ([19e6860](https://github.com/abruption/agy-cli-usage/commit/19e6860bcf2aa8a547b880a4430027539b4343fb)), closes [#30](https://github.com/abruption/agy-cli-usage/issues/30)
19
+
20
+ ## [0.4.2](https://github.com/abruption/agy-cli-usage/compare/v0.4.1...v0.4.2) (2026-06-30)
21
+
22
+
23
+ ### Bug Fixes
24
+
25
+ * use canonical binary name agy-cli-usage in --help output ([#27](https://github.com/abruption/agy-cli-usage/issues/27)) ([384fa64](https://github.com/abruption/agy-cli-usage/commit/384fa64aedaef5249a99dc0c2acac9a136d2dcb4))
26
+
13
27
  ## [0.4.1](https://github.com/abruption/agy-cli-usage/compare/v0.4.0...v0.4.1) (2026-06-24)
14
28
 
15
29
 
package/README.ko.md ADDED
@@ -0,0 +1,237 @@
1
+ <div align="center">
2
+
3
+ # agy-cli-usage
4
+
5
+ [![npm version](https://img.shields.io/npm/v/agy-cli-usage?color=cb3837&logo=npm)](https://www.npmjs.com/package/agy-cli-usage)
6
+ [![npm downloads](https://img.shields.io/npm/dm/agy-cli-usage?color=cb3837&logo=npm)](https://www.npmjs.com/package/agy-cli-usage)
7
+ [![CI](https://github.com/abruption/agy-cli-usage/actions/workflows/ci.yml/badge.svg)](https://github.com/abruption/agy-cli-usage/actions/workflows/ci.yml)
8
+ [![TypeScript](https://img.shields.io/badge/TypeScript-strict-3178c6?logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
9
+ [![node](https://img.shields.io/node/v/agy-cli-usage?color=339933&logo=node.js)](https://www.npmjs.com/package/agy-cli-usage)
10
+ [![license](https://img.shields.io/npm/l/agy-cli-usage?color=blue)](LICENSE)
11
+
12
+ **Antigravity CLI(`agy`) 사용량·쿼타 모니터 (headless).**
13
+
14
+ `agy`의 `/usage` 패널 — 모델 그룹별 주간·5시간 한도, 잔여율, 리프레시 시각 — 을 **헤드리스로** 조회합니다. IDE 불필요, macOS · Linux · Windows · 헤드리스 서버 지원.
15
+
16
+ [English](README.md) · **한국어**
17
+
18
+ <sub><a href="https://github.com/skainguyen1412/antigravity-usage">skainguyen1412/antigravity-usage</a>에서 영감 — 그쪽은 Antigravity <b>IDE</b> 대상, 이 프로젝트는 <b>CLI</b>(<code>agy</code>) 대상입니다.</sub>
19
+
20
+ </div>
21
+
22
+ ---
23
+
24
+ ```text
25
+ Models & Quota
26
+ Account: you@gmail.com
27
+
28
+ GEMINI MODELS
29
+ Weekly Limit
30
+ [██████████████████████████████████████████████░░░░] 91.72%
31
+ 92% remaining · Refreshes in 73h 23m
32
+ Five Hour Limit
33
+ [███████████████████████████████████████████████░░░] 94.63%
34
+ 95% remaining · Refreshes in 2h 2m
35
+
36
+ CLAUDE AND GPT MODELS
37
+ Weekly Limit
38
+ [█████████████████████████████████████████████████░] 97.78%
39
+ 98% remaining · Refreshes in 26h 17m
40
+ Five Hour Limit
41
+ [██████████████████████████████████████████████████] Quota available
42
+ ```
43
+
44
+ ---
45
+
46
+ # 사람을 위한 안내 (For Human)
47
+
48
+ ## 개요
49
+
50
+ `agy-cli-usage`는 `agy`의 인터랙티브 `/usage` 슬래시 커맨드와 동일한 사용량·쿼타 정보를 일반 셸에서 보여줍니다 — 1회성, watch 모드, 또는 머신 리더블 JSON. 잔여 쿼타 확인, 상태바 연동, 대시보드 공급 등에 사용하세요.
51
+
52
+ ## 왜 필요한가
53
+
54
+ `agy -p "<prompt>"`(헤드리스 모드)는 LLM 프롬프트 전용 경로라 `/usage` 같은 TUI 슬래시 커맨드를 렌더링하지 않습니다. 그래서 사용량을 자동화·폴링할 수 없습니다. 이 도구는 쿼타를 직접 읽어(필요 시 `agy`를 가상 터미널로 구동해) 그 공백을 메웁니다.
55
+
56
+ ## 빠른 시작
57
+
58
+ ```bash
59
+ # 설치 없이 한 번 실행
60
+ npx agy-cli-usage
61
+
62
+ # 전역 설치 → 어디서나 `agy-cli-usage` (별칭 `agy-usage`)
63
+ npm install -g agy-cli-usage
64
+ agy-cli-usage
65
+ ```
66
+
67
+ > 사전 조건: 같은 머신에서 `agy`에 로그인되어 있을 것, Node.js >= 18.
68
+
69
+ ## 사용법
70
+
71
+ ```bash
72
+ agy-cli-usage # /usage 와 동일한 패널 (1회성)
73
+ agy-cli-usage --json # 머신 리더블 JSON
74
+ agy-cli-usage --watch 60 # 60초 간격 자동 갱신 (5분 캐시 경유)
75
+ agy-cli-usage update [--check] # 자기 업데이트 (--check: 알림만)
76
+ agy-cli-usage --version # 버전 출력
77
+ ```
78
+
79
+ | 플래그 | 설명 |
80
+ |--------|------|
81
+ | `--json` | 정규화 JSON 출력 (스크립트/대시보드 연동) |
82
+ | `--watch [초]` | N초 간격 갱신 (기본 60) |
83
+ | `--source <auto\|api\|pty>` | 데이터 소스 (기본 `auto`: API → 실패 시 PTY) |
84
+ | `--channel <auto\|daily\|prod>` | Cloud Code 호스트 |
85
+ | `--no-cache` / `--refresh` | 5분 캐시 무시하고 강제 조회 |
86
+ | `-h`, `--help` | 도움말 |
87
+ | `-v`, `--version` | 버전 |
88
+
89
+ ## 동작 방식
90
+
91
+ 1. **직접 API (기본 · 빠름).** OS에서 `agy`의 OAuth 토큰을 읽어, `agy`가 시작 시 호출하는 것과 동일한 Cloud Code 내부 API를 직접 호출합니다.
92
+ - `POST /v1internal:loadCodeAssist` → `cloudaicompanionProject` 획득
93
+ - `POST /v1internal:retrieveUserQuotaSummary {project}` → 쿼타
94
+ - 만료 시 OAuth refresh 자동 처리.
95
+ 2. **PTY 폴백 (안전망).** 토큰을 못 읽거나 내부 API가 바뀌면, `agy`를 가상 터미널로 띄워 `/usage`를 보내고 `@xterm/headless`로 화면을 재구성해 파싱합니다.
96
+
97
+ ## 크로스플랫폼 자격증명
98
+
99
+ 토큰은 `agy`가 저장한 위치에서 **읽기만** 합니다. 플랫폼별 자동 처리:
100
+
101
+ | OS / 환경 | 저장 위치 | 읽는 방법 |
102
+ |-----------|----------|----------|
103
+ | macOS | Keychain | `@napi-rs/keyring` (폴백 `security`) |
104
+ | Linux 데스크톱 | Secret Service | `@napi-rs/keyring` (폴백 `secret-tool`) |
105
+ | **Windows** | Credential Manager | 내장 `powershell.exe`로 Win32 `CredRead` 호출 |
106
+ | **헤드리스 Linux** | 토큰 파일 | `~/.gemini/antigravity-cli/antigravity-oauth-token` |
107
+
108
+ 읽기 순서: `키링 → OS CLI → Windows credman → 토큰 파일 → PTY`. 파일 경로는 `AGY_OAUTH_TOKEN_FILE`로 override.
109
+
110
+ ## HTTP 엔드포인트 (선택)
111
+
112
+ ```bash
113
+ PORT=3007 npm run serve # GET /quota → 정규화 JSON (5분 캐시), GET /healthz
114
+ ```
115
+
116
+ 외부 대시보드/스크립트에서 `GET /quota`로 소비하거나, `agy-cli-usage --json`을 서브프로세스로 호출하세요.
117
+
118
+ ## 개발 & 릴리스
119
+
120
+ TypeScript(strict, ESM)로 작성하고 `tsc`로 `dist/`에 컴파일합니다.
121
+
122
+ ```bash
123
+ npm run build # tsc → dist/ (컴파일된 JS + .d.ts)
124
+ npm run check # tsc --noEmit (타입 체크)
125
+ npm test # 빌드 후 node --test (자격증명·네트워크 불필요, 순수 로직)
126
+ ```
127
+
128
+ - **CI**: push/PR마다 Ubuntu(Node 18/20/22) + macOS/Windows(Node 22)에서 테스트.
129
+ - **릴리스**: [release-please](https://github.com/googleapis/release-please) — Conventional Commits 기반 완전 자동화. main에 머지된 커밋으로 **Release PR**(버전 범프 + CHANGELOG)이 유지되고, 그 PR을 머지하면 태그·GitHub Release·`npm publish --provenance`가 자동 실행됩니다.
130
+
131
+ ## 주의
132
+
133
+ - `v1internal:retrieveUserQuotaSummary`는 **비공개·비문서 내부 엔드포인트**입니다. 스키마/호스트가 예고 없이 바뀔 수 있으며, 그때의 안전망이 PTY 폴백입니다. 본인 계정의 사용량 조회 용도로만 사용하세요.
134
+ - 자격증명은 OS 저장소에서 **읽기만** 하며, refresh 토큰을 되쓰지 않아 `agy` 세션과 충돌하지 않습니다.
135
+ - 코드에 포함된 OAuth client_id/secret은 `agy` 바이너리의 **installed-app(public)** 값으로, [Google 문서](https://developers.google.com/identity/protocols/oauth2)상 기밀이 아닙니다. 사용자 식별은 각자의 키링 토큰으로 이뤄집니다.
136
+
137
+ ## 라이선스
138
+
139
+ [MIT](LICENSE) © abruption
140
+
141
+ ---
142
+
143
+ # 에이전트(AI)를 위한 안내 (For Agent)
144
+
145
+ > 프로그래밍적 사용을 위한 머신 지향 스펙. 안정적 계약: `--json` 스냅샷과 `GET /quota` 페이로드는 동일한 형태(`Snapshot`)를 공유합니다.
146
+
147
+ ## TL;DR
148
+
149
+ - 바이너리: `agy-cli-usage` (별칭 `agy-usage`). Node >= 18. 같은 호스트에 `agy` 로그인 필요.
150
+ - 구조화 데이터: `agy-cli-usage --json` (stdout) 또는 `GET http://127.0.0.1:3007/quota`.
151
+ - `auto`의 소스 순서: 직접 API → PTY 폴백. 결과는 5분 캐시.
152
+
153
+ ## 커맨드
154
+
155
+ | 호출 | 동작 |
156
+ |------|------|
157
+ | `agy-cli-usage` | 패널을 stdout에 렌더(휴먼 포맷). |
158
+ | `agy-cli-usage --json` | `Snapshot` JSON을 stdout에 출력 후 종료. |
159
+ | `agy-cli-usage --watch [초]` | `초`(최소 5, 기본 60)마다 화면 클리어 후 재렌더. 무한 실행. |
160
+ | `agy-cli-usage --source <auto\|api\|pty>` | `api`: API 전용(실패 시 throw). `pty`: PTY 전용(캐시 무시). `auto`: API→PTY. |
161
+ | `agy-cli-usage --channel <auto\|daily\|prod>` | Cloud Code 호스트 선택. `auto`는 `daily`→`prod` 순서. |
162
+ | `agy-cli-usage --no-cache` / `--refresh` | 강제 신규 조회(5분 캐시 스킵). |
163
+ | `agy-cli-usage update [--check]` | `npm i -g` 자가 업데이트. `--check`는 알림만. |
164
+ | `agy-cli-usage --version` / `-v` | 버전 문자열을 stdout에 출력. |
165
+
166
+ ## JSON 출력 (`--json`) — 스키마
167
+
168
+ ```jsonc
169
+ {
170
+ "account": "you@gmail.com | null",
171
+ "tier": "string | null",
172
+ "fetchedAt": "ISO-8601 타임스탬프",
173
+ "source": "api | pty",
174
+ "host": "cloud code host | null",
175
+ "note": "string | null",
176
+ "groups": [
177
+ {
178
+ "name": "GEMINI MODELS",
179
+ "models": "쉼표 구분 모델 목록 (빈 문자열 가능)",
180
+ "buckets": [
181
+ {
182
+ "kind": "weekly | 5h | <기타>",
183
+ "label": "Weekly Limit",
184
+ "remainingFraction": 0.9172, // 0..1, 미상이면 null
185
+ "usedFraction": 0.0828, // 1 - remainingFraction, 또는 null
186
+ "resetAt": "ISO-8601 | null",
187
+ "resetsInSeconds": 264180, // 정수 초, 또는 null
188
+ "available": false, // remainingFraction === 1 이면 true
189
+ "description": "string | null"
190
+ }
191
+ ]
192
+ }
193
+ ]
194
+ }
195
+ ```
196
+
197
+ 파싱 시 참고:
198
+ - `remainingFraction`(잔여 비율 0–1)을 우선 사용. `available`이 `true`면 풀 쿼타로 간주(패널은 "Quota available" 표시).
199
+ - `resetsInSeconds`는 `fetchedAt` 기준 상대값, `resetAt`은 절대값. 둘 다 `null` 가능.
200
+ - `kind`는 인식 시 `weekly`/`5h`로 정규화, 아니면 원본 window/label 문자열.
201
+
202
+ ## HTTP API (`npm run serve` / `dist/src/server.js`)
203
+
204
+ | 라우트 | 응답 |
205
+ |--------|------|
206
+ | `GET /quota` | `200` `Snapshot` JSON(`--json`과 동일 형태). `?refresh=1`은 캐시 우회. 실패 시 `502 {"error":...}`. 헤더: `Cache-Control: public, max-age=300`, `Access-Control-Allow-Origin: *`. |
207
+ | `GET /healthz` | `200 {"ok":true}` |
208
+ | (그 외) | `404 {"error":"not found"}` |
209
+
210
+ `HOST`(기본 `127.0.0.1`) : `PORT`(기본 `3007`)에 바인딩.
211
+
212
+ ## 환경변수
213
+
214
+ | 변수 | 효과 |
215
+ |------|------|
216
+ | `AGY_OAUTH_TOKEN_FILE` | 토큰 파일 경로 override(헤드리스 폴백). |
217
+ | `AGY_BIN` | `agy` 바이너리 경로(PTY 소스). 없으면 `PATH`→`~/.local/bin` 순 탐색. |
218
+ | `XDG_CACHE_HOME` | 캐시 베이스 디렉토리(캐시는 `<base>/agy-usage/quota.json`, 기본 `~/.cache`). |
219
+ | `NO_COLOR` | 렌더 패널의 ANSI 색상 비활성화. |
220
+ | `PORT` / `HOST` | HTTP 서버 바인딩(서버 모드 한정). |
221
+
222
+ ## 종료 코드 & 오류
223
+
224
+ - `0` — 성공.
225
+ - `1` — 모든 오류(예: 토큰을 못 읽고 PTY도 불가할 때 `CredentialError`). 오류 텍스트는 **stderr**로, 구조화 출력은 **stdout**으로 나가므로, stderr에 경고(`auto` 모드의 `[api failed: …] falling back to PTY` 알림 등)가 있어도 `--json` stdout은 안전하게 파싱 가능.
226
+ - `update`는 내부 `npm` 종료 코드를 반환.
227
+
228
+ ## 데이터 소스 & 캐시
229
+
230
+ - **캐시**: `<XDG_CACHE_HOME|~/.cache>/agy-usage/quota.json`, TTL **5분**. `--watch`/폴링 시 업스트림 API 부하 회피. `source === 'pty'`이거나 캐시 비활성(`--no-cache`/`--refresh`, HTTP 라우트의 `?refresh=1`) 시 우회.
231
+ - **API 경로**는 토큰(키링/파일)을 읽어 `loadCodeAssist` → `retrieveUserQuotaSummary` 호출. **PTY 경로**는 `agy`를 구동(POSIX `python3 pty`, Windows `node-pty`)하며 환경에서 `agy` 실행 가능해야 함.
232
+
233
+ ## 연동 노트
234
+
235
+ - 자동화 시 `--json`(서브프로세스) 또는 `GET /quota`(상시 서비스)를 호출. 둘 다 동일 캐시를 거치므로 고빈도 폴링도 안전.
236
+ - 휴먼 패널은 파싱하지 말 것 — ANSI 이스케이프 포함, 레이아웃 지향. `Snapshot` JSON이 안정적 계약.
237
+ - 이 도구는 자격증명을 **읽기만** 하며, `agy` 세션을 변경하거나 토큰을 되쓰지 않음.
package/README.md CHANGED
@@ -11,7 +11,9 @@
11
11
 
12
12
  **Headless usage & quota monitor for the Antigravity CLI (`agy`).**
13
13
 
14
- `agy`의 `/usage` 패널(모델 그룹별 주간·5시간 한도, 잔여율, 리프레시 시각)을 **헤드리스로** 조회합니다 — IDE 불필요, macOS · Linux · Windows · 헤드리스 서버 지원.
14
+ Reads `agy`'s `/usage` panel per–model-group weekly & 5-hour limits, remaining percentage, and refresh times**headlessly**. No IDE required; works on macOS · Linux · Windows · headless servers.
15
+
16
+ **English** · [한국어](README.ko.md)
15
17
 
16
18
  <sub>Inspired by <a href="https://github.com/skainguyen1412/antigravity-usage">skainguyen1412/antigravity-usage</a> — which targets the Antigravity <b>IDE</b>; this targets the <b>CLI</b> (<code>agy</code>).</sub>
17
19
 
@@ -39,89 +41,197 @@
39
41
  [██████████████████████████████████████████████████] Quota available
40
42
  ```
41
43
 
42
- ## 🚀 빠른 시작
44
+ ---
45
+
46
+ # For Human
47
+
48
+ ## What it is
49
+
50
+ `agy-cli-usage` shows the same usage/quota information as `agy`'s interactive `/usage` slash command, but from a plain shell — one-shot, watch mode, or machine-readable JSON. Use it to keep an eye on your remaining quota, drive a status bar, or feed a dashboard.
51
+
52
+ ## Why
53
+
54
+ `agy -p "<prompt>"` (headless mode) is a prompt-only path: it does not render TUI slash commands like `/usage`, so usage can't be polled or automated. This tool fills that gap by reading the quota directly (and falling back to driving `agy` in a pseudo-terminal when needed).
55
+
56
+ ## Quick start
43
57
 
44
58
  ```bash
45
- # 설치 없이 번 실행
59
+ # Run once, no install
46
60
  npx agy-cli-usage
47
61
 
48
- # 전역 설치어디서나 `agy-cli-usage`
62
+ # Install globally → `agy-cli-usage` (alias `agy-usage`) anywhere
49
63
  npm install -g agy-cli-usage
50
64
  agy-cli-usage
51
65
  ```
52
66
 
53
- > 사전 조건: 같은 머신에서 `agy`에 로그인되어 있을 것. Node.js >= 18.
67
+ > Prerequisites: `agy` is logged in on the same machine, and Node.js >= 18.
54
68
 
55
- ## ⚡ 사용법
69
+ ## Usage
56
70
 
57
71
  ```bash
58
- agy-cli-usage # /usage 동일한 패널 (1회성)
59
- agy-cli-usage --json # 머신 리더블 JSON
60
- agy-cli-usage --watch 60 # 60초 간격 자동 갱신 (5 캐시 경유)
61
- agy-cli-usage update [--check] # 자기 업데이트 (--check: 알림만)
62
- agy-cli-usage --version # 버전 출력
72
+ agy-cli-usage # the /usage panel (one-shot)
73
+ agy-cli-usage --json # machine-readable JSON
74
+ agy-cli-usage --watch 60 # auto-refresh every 60s (via the 5-min cache)
75
+ agy-cli-usage update [--check] # self-update (--check: report only)
76
+ agy-cli-usage --version # print version
63
77
  ```
64
78
 
65
- | 플래그 | 설명 |
66
- |--------|------|
67
- | `--json` | 정규화 JSON 출력 (스크립트/대시보드 연동) |
68
- | `--watch []` | N 간격 갱신 (기본 60) |
69
- | `--source <auto\|api\|pty>` | 데이터 소스 (기본 `auto`: API → 실패 PTY) |
70
- | `--channel <auto\|daily\|prod>` | Cloud Code 호스트 |
71
- | `--no-cache` / `--refresh` | 5분 캐시 무시하고 강제 조회 |
72
-
73
- ## 🤔 필요한가
74
-
75
- `agy -p "<prompt>"`(headless)는 LLM 프롬프트 전용이라 `/usage` 같은 TUI 슬래시 커맨드를 렌더링하지 않습니다. 그래서 사용량을 자동화·폴링할 수 없습니다. 이 도구는 그 공백을 메웁니다.
79
+ | Flag | Description |
80
+ |------|-------------|
81
+ | `--json` | Normalized JSON output (for scripts/dashboards) |
82
+ | `--watch [secs]` | Refresh every N seconds (default 60) |
83
+ | `--source <auto\|api\|pty>` | Data source (default `auto`: API → PTY on failure) |
84
+ | `--channel <auto\|daily\|prod>` | Cloud Code host |
85
+ | `--no-cache` / `--refresh` | Bypass the 5-minute cache |
86
+ | `-h`, `--help` | Show help |
87
+ | `-v`, `--version` | Show version |
76
88
 
77
- ## 🔍 동작 방식
89
+ ## How it works
78
90
 
79
- 1. **직접 API (기본 · 빠름)** OS에서 `agy`의 OAuth 토큰을 읽어, `agy`가 시작 호출하는 것과 동일한 Cloud Code 내부 API 직접 호출합니다.
80
- - `POST /v1internal:loadCodeAssist` → `cloudaicompanionProject` 획득
81
- - `POST /v1internal:retrieveUserQuotaSummary {project}` → 쿼타
82
- - 만료 OAuth refresh 자동 처리.
83
- 2. **PTY 폴백 (안전망)** 토큰을 읽거나 내부 API 바뀌면, `agy`를 가상 터미널로 띄워 `/usage`를 보내고 `@xterm/headless`로 화면을 재구성해 파싱합니다.
91
+ 1. **Direct API (default · fast).** Reads `agy`'s OAuth token from the OS and calls the same internal Cloud Code API that `agy` calls on startup:
92
+ - `POST /v1internal:loadCodeAssist` → obtains `cloudaicompanionProject`
93
+ - `POST /v1internal:retrieveUserQuotaSummary {project}` → the quota
94
+ - Refreshes the OAuth token automatically when expired.
95
+ 2. **PTY fallback (safety net).** If the token can't be read or the internal API changes, it launches `agy` in a pseudo-terminal, sends `/usage`, reconstructs the screen with `@xterm/headless`, and parses it.
84
96
 
85
- ## 🔐 크로스플랫폼 자격증명
97
+ ## Cross-platform credentials
86
98
 
87
- 토큰은 `agy`가 저장한 위치에서 **읽기만** 합니다. 플랫폼별로 자동 처리:
99
+ The token is **read only** from wherever `agy` stored it. Handled per platform automatically:
88
100
 
89
- | OS / 환경 | 저장 위치 | 읽는 방법 |
90
- |-----------|----------|----------|
91
- | macOS | Keychain | `@napi-rs/keyring` (폴백 `security`) |
92
- | Linux 데스크톱 | Secret Service | `@napi-rs/keyring` (폴백 `secret-tool`) |
93
- | **Windows** | Credential Manager | 내장 `powershell.exe`로 Win32 `CredRead` 호출 |
94
- | **헤드리스 Linux** | 토큰 파일 | `~/.gemini/antigravity-cli/antigravity-oauth-token` |
101
+ | OS / environment | Storage | How it's read |
102
+ |------------------|---------|---------------|
103
+ | macOS | Keychain | `@napi-rs/keyring` (fallback `security`) |
104
+ | Linux desktop | Secret Service | `@napi-rs/keyring` (fallback `secret-tool`) |
105
+ | **Windows** | Credential Manager | Win32 `CredRead` via built-in `powershell.exe` |
106
+ | **Headless Linux** | token file | `~/.gemini/antigravity-cli/antigravity-oauth-token` |
95
107
 
96
- 읽기 체인: `키링 → OS CLI → Windows credman → 토큰 파일 → PTY`. `AGY_OAUTH_TOKEN_FILE`로 파일 경로 override.
108
+ Read order: `keyring → OS CLI → Windows credman → token file → PTY`. Override the file path with `AGY_OAUTH_TOKEN_FILE`.
97
109
 
98
- ## 🔌 HTTP 엔드포인트 (선택)
110
+ ## HTTP endpoint (optional)
99
111
 
100
112
  ```bash
101
- PORT=3007 npm run serve # GET /quota → 정규화 JSON (5 캐시), GET /healthz
113
+ PORT=3007 npm run serve # GET /quota → normalized JSON (5-min cache), GET /healthz
102
114
  ```
103
115
 
104
- 외부 대시보드/스크립트에서 `GET /quota`로 소비하거나 `agy-cli-usage --json`을 subprocess로 호출하세요.
116
+ Consume `GET /quota` from an external dashboard/script, or call `agy-cli-usage --json` as a subprocess.
105
117
 
106
- ## 🛠️ 개발 / 릴리스
118
+ ## Development & Release
107
119
 
108
- TypeScript(strict, ESM) 작성하고 `tsc`로 `dist/`에 컴파일합니다.
120
+ Written in TypeScript (strict, ESM) and compiled to `dist/` with `tsc`.
109
121
 
110
122
  ```bash
111
- npm run build # tsc → dist/ (컴파일된 JS + .d.ts)
112
- npm run check # tsc --noEmit (타입 체크)
113
- npm test # 빌드 node --test (자격증명·네트워크 불필요, 순수 로직)
123
+ npm run build # tsc → dist/ (compiled JS + .d.ts)
124
+ npm run check # tsc --noEmit (type-check)
125
+ npm test # build, then node --test (no credentials/network; pure logic)
114
126
  ```
115
127
 
116
- - **CI**: push/PR마다 ubuntu(Node 18/20/22) + macOS/Windows(Node 22)에서 테스트.
117
- - **Release**: [release-please](https://github.com/googleapis/release-please) — Conventional Commits 기반 완전 자동화. main에 머지된 커밋으로 release-please가 **Release PR**(버전 범프 + CHANGELOG) 유지하고, PR 머지하면 태그·GitHub Release·`npm publish --provenance`가 자동 실행됩니다.
128
+ - **CI**: every push/PR runs the test suite on Ubuntu (Node 18/20/22) + macOS/Windows (Node 22).
129
+ - **Release**: [release-please](https://github.com/googleapis/release-please) — fully automated from Conventional Commits. Merged commits keep a **Release PR** (version bump + CHANGELOG) up to date; merging that PR creates the tag + GitHub Release and runs `npm publish --provenance`.
118
130
 
119
- ## ⚠️ 주의
131
+ ## Caveats
120
132
 
121
- - `v1internal:retrieveUserQuotaSummary`는 **비공개 내부 엔드포인트**입니다. 스키마/호스트가 예고 없이 바뀔 있으며, 그때의 안전망이 PTY 폴백입니다. 본인 계정의 사용량 조회 용도로만 사용하세요.
122
- - 자격증명은 OS 저장소에서 **읽기만** 하며, refresh 토큰을 되쓰지 않아 `agy` 세션과 충돌하지 않습니다.
123
- - 코드에 포함된 OAuth client_id/secret `agy` 바이너리에 들어 있는 **installed-app(public) 값**으로, [Google 문서](https://developers.google.com/identity/protocols/oauth2) 기밀이 아닙니다. 사용자 식별은 각자의 키링 토큰으로 이뤄집니다.
133
+ - `v1internal:retrieveUserQuotaSummary` is a **private, undocumented endpoint**. Its schema/host may change without notice; the PTY fallback is the safety net. Use it only to check your own account's usage.
134
+ - Credentials are **read-only** from the OS store; the refresh token is never written back, so it never conflicts with `agy`'s own session.
135
+ - The OAuth client_id/secret embedded in the code are `agy`'s **installed-app (public)** values — per [Google's docs](https://developers.google.com/identity/protocols/oauth2) these are not treated as secret. Per-user identity comes from your keyring token, not the client_id.
124
136
 
125
- ## 📄 License
137
+ ## License
126
138
 
127
139
  [MIT](LICENSE) © abruption
140
+
141
+ ---
142
+
143
+ # For Agent (AI)
144
+
145
+ > Machine-oriented spec for programmatic use. Stable contract: the `--json` snapshot and the `GET /quota` payload share the same shape (`Snapshot`).
146
+
147
+ ## TL;DR
148
+
149
+ - Binary: `agy-cli-usage` (alias `agy-usage`). Node >= 18. Requires `agy` logged in on the same host.
150
+ - Get structured data: `agy-cli-usage --json` (stdout) or `GET http://127.0.0.1:3007/quota`.
151
+ - Source order in `auto`: direct API first, PTY fallback second. Results cached 5 minutes.
152
+
153
+ ## Commands
154
+
155
+ | Invocation | Behavior |
156
+ |------------|----------|
157
+ | `agy-cli-usage` | Render the panel to stdout (human format). |
158
+ | `agy-cli-usage --json` | Print the `Snapshot` JSON to stdout, then exit. |
159
+ | `agy-cli-usage --watch [secs]` | Clear screen and re-render every `secs` (min 5, default 60). Runs forever. |
160
+ | `agy-cli-usage --source <auto\|api\|pty>` | `api`: API only (throws on failure). `pty`: PTY only (ignores cache). `auto`: API→PTY. |
161
+ | `agy-cli-usage --channel <auto\|daily\|prod>` | Cloud Code host selection. `auto` tries `daily` then `prod`. |
162
+ | `agy-cli-usage --no-cache` / `--refresh` | Force a fresh fetch (skip the 5-min cache). |
163
+ | `agy-cli-usage update [--check]` | Self-update via `npm i -g`. `--check` reports only. |
164
+ | `agy-cli-usage --version` / `-v` | Print version string to stdout. |
165
+
166
+ ## JSON output (`--json`) — schema
167
+
168
+ ```jsonc
169
+ {
170
+ "account": "you@gmail.com | null",
171
+ "tier": "string | null",
172
+ "fetchedAt": "ISO-8601 timestamp",
173
+ "source": "api | pty",
174
+ "host": "cloud code host | null",
175
+ "note": "string | null",
176
+ "groups": [
177
+ {
178
+ "name": "GEMINI MODELS",
179
+ "models": "comma-separated model list (may be empty)",
180
+ "buckets": [
181
+ {
182
+ "kind": "weekly | 5h | <other>",
183
+ "label": "Weekly Limit",
184
+ "remainingFraction": 0.9172, // 0..1, or null if unknown
185
+ "usedFraction": 0.0828, // 1 - remainingFraction, or null
186
+ "resetAt": "ISO-8601 | null",
187
+ "resetsInSeconds": 264180, // integer seconds, or null
188
+ "available": false, // true iff remainingFraction === 1
189
+ "description": "string | null"
190
+ }
191
+ ]
192
+ }
193
+ ]
194
+ }
195
+ ```
196
+
197
+ Notes for parsing:
198
+ - Prefer `remainingFraction` (fraction remaining, 0–1). When `available` is `true`, treat as full quota (the panel shows "Quota available").
199
+ - `resetsInSeconds` is relative to `fetchedAt`; `resetAt` is absolute. Either may be `null`.
200
+ - `kind` is normalized to `weekly` / `5h` where recognized, otherwise the raw window/label string.
201
+
202
+ ## HTTP API (`npm run serve` / `dist/src/server.js`)
203
+
204
+ | Route | Response |
205
+ |-------|----------|
206
+ | `GET /quota` | `200` `Snapshot` JSON (same shape as `--json`). `?refresh=1` bypasses cache. `502 {"error":...}` on failure. Headers: `Cache-Control: public, max-age=300`, `Access-Control-Allow-Origin: *`. |
207
+ | `GET /healthz` | `200 {"ok":true}` |
208
+ | (other) | `404 {"error":"not found"}` |
209
+
210
+ Binds `HOST` (default `127.0.0.1`) : `PORT` (default `3007`).
211
+
212
+ ## Environment variables
213
+
214
+ | Variable | Effect |
215
+ |----------|--------|
216
+ | `AGY_OAUTH_TOKEN_FILE` | Override the token file path (headless fallback). |
217
+ | `AGY_BIN` | Path to the `agy` binary (PTY source). Else resolved from `PATH`, then `~/.local/bin`. |
218
+ | `XDG_CACHE_HOME` | Cache base dir (cache lives at `<base>/agy-usage/quota.json`; default `~/.cache`). |
219
+ | `NO_COLOR` | Disable ANSI color in the rendered panel. |
220
+ | `PORT` / `HOST` | HTTP server bind (server mode only). |
221
+
222
+ ## Exit codes & errors
223
+
224
+ - `0` — success.
225
+ - `1` — any error (e.g. `CredentialError` when no token is readable and PTY is unavailable). Error text goes to **stderr**; structured output goes to **stdout**, so `--json` stdout is safe to parse even when stderr carries warnings (e.g. the `[api failed: …] falling back to PTY` notice in `auto` mode).
226
+ - `update` returns the underlying `npm` exit status.
227
+
228
+ ## Data sources & cache
229
+
230
+ - **Cache**: `<XDG_CACHE_HOME|~/.cache>/agy-usage/quota.json`, TTL **5 minutes**. Avoids hammering the upstream API on `--watch`/polling. Bypassed when `source === 'pty'` or the cache is disabled (`--no-cache`/`--refresh`, or `?refresh=1` on the HTTP route).
231
+ - **API path** reads the token (keyring/file), then calls `loadCodeAssist` → `retrieveUserQuotaSummary`. **PTY path** drives `agy` (`python3 pty` on POSIX, `node-pty` on Windows) and needs `agy` runnable in the environment.
232
+
233
+ ## Integration notes
234
+
235
+ - For automation, call `--json` (subprocess) or `GET /quota` (long-running service). Both go through the same cache, so high-frequency polling is safe.
236
+ - Do not parse the human panel; it contains ANSI escapes and is layout-oriented. The `Snapshot` JSON is the stable contract.
237
+ - The tool only **reads** credentials; it never mutates `agy`'s session or writes tokens back.
@@ -11,10 +11,18 @@ export interface CliOptions {
11
11
  version?: boolean;
12
12
  help?: boolean;
13
13
  }
14
+ /** Exported for direct unit testing (no process.argv/exit side effects). */
15
+ export declare function parseArgs(argv: string[]): CliOptions;
16
+ /** Exported for direct unit testing via an injected `cacheFile` — not part of the CLI's public surface. */
17
+ export declare function readCache(source: SnapshotOptions['source'], channel: SnapshotOptions['channel'], cacheFile?: string): Snapshot | null;
18
+ /** Exported for direct unit testing via an injected `cacheFile` — not part of the CLI's public surface. */
19
+ export declare function writeCache(snap: Snapshot, source: SnapshotOptions['source'], channel: SnapshotOptions['channel'], cacheFile?: string): void;
14
20
  /** Subset of options needed to produce a snapshot (also usable from server.ts). */
15
21
  export interface SnapshotOptions {
16
22
  source: 'auto' | 'api' | 'pty';
17
23
  channel: 'auto' | 'daily' | 'prod';
18
24
  cache: boolean;
25
+ /** Override the cache file path — for tests only; defaults to the real user cache. */
26
+ cacheFile?: string;
19
27
  }
20
28
  export declare function getSnapshot(opts: SnapshotOptions): Promise<Snapshot>;
package/dist/src/main.js CHANGED
@@ -1,30 +1,34 @@
1
1
  #!/usr/bin/env node
2
- // agy-usage — Antigravity CLI (agy) usage/quota monitor.
2
+ // agy-cli-usage — Antigravity CLI (agy) usage/quota monitor.
3
3
  //
4
4
  // Usage:
5
- // agy-usage one-shot panel (like agy's /usage)
6
- // agy-usage --json machine-readable JSON
7
- // agy-usage --watch [secs] refresh every N seconds (default 60)
8
- // agy-usage --source api|pty|auto data source (default auto: api, fall back to pty)
9
- // agy-usage --channel daily|prod Cloud Code host (default: auto-detect)
10
- // agy-usage --no-cache bypass the 5-minute cache
11
- // agy-usage --refresh force a fresh fetch (alias for --no-cache)
12
- // agy-usage update [--check] self-update via npm
13
- // agy-usage --version | -v print the installed version
5
+ // agy-cli-usage one-shot panel (like agy's /usage)
6
+ // agy-cli-usage --json machine-readable JSON
7
+ // agy-cli-usage --watch [secs] refresh every N seconds (default 60)
8
+ // agy-cli-usage --source api|pty|auto data source (default auto: api, fall back to pty)
9
+ // agy-cli-usage --channel daily|prod Cloud Code host (default: auto-detect)
10
+ // agy-cli-usage --no-cache bypass the 5-minute cache
11
+ // agy-cli-usage --refresh force a fresh fetch (alias for --no-cache)
12
+ // agy-cli-usage update [--check] self-update via npm
13
+ // agy-cli-usage --version | -v print the installed version
14
14
  import { getAccessToken, CredentialError } from './credentials.js';
15
15
  import { fetchQuotaSummary } 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';
19
19
  import { currentVersion, runUpdate } from './update.js';
20
- import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
20
+ import { readFileSync, writeFileSync, mkdirSync, realpathSync } from 'node:fs';
21
21
  import { homedir } from 'node:os';
22
- import { join } from 'node:path';
22
+ import { join, dirname } from 'node:path';
23
+ import { fileURLToPath } from 'node:url';
23
24
  const CACHE_DIR = join(process.env.XDG_CACHE_HOME || join(homedir(), '.cache'), 'agy-usage');
24
25
  const CACHE_FILE = join(CACHE_DIR, 'quota.json');
25
26
  const CACHE_TTL_MS = 5 * 60 * 1000;
27
+ const VALID_SOURCES = ['auto', 'api', 'pty'];
28
+ const VALID_CHANNELS = ['auto', 'daily', 'prod'];
26
29
  const errMessage = (e) => (e instanceof Error ? e.message : String(e));
27
- function parseArgs(argv) {
30
+ /** Exported for direct unit testing (no process.argv/exit side effects). */
31
+ export function parseArgs(argv) {
28
32
  const o = {
29
33
  json: false, watch: null, source: 'auto', channel: 'auto', cache: true, command: null, check: false,
30
34
  };
@@ -43,10 +47,20 @@ function parseArgs(argv) {
43
47
  else
44
48
  o.watch = 60;
45
49
  }
46
- else if (a === '--source')
47
- o.source = (argv[++i] ?? 'auto');
48
- else if (a === '--channel')
49
- o.channel = (argv[++i] ?? 'auto');
50
+ else if (a === '--source') {
51
+ const v = argv[++i];
52
+ if (!VALID_SOURCES.includes(v)) {
53
+ throw new Error(`invalid --source '${v}' — expected one of: ${VALID_SOURCES.join(', ')}`);
54
+ }
55
+ o.source = v;
56
+ }
57
+ else if (a === '--channel') {
58
+ const v = argv[++i];
59
+ if (!VALID_CHANNELS.includes(v)) {
60
+ throw new Error(`invalid --channel '${v}' — expected one of: ${VALID_CHANNELS.join(', ')}`);
61
+ }
62
+ o.channel = v;
63
+ }
50
64
  else if (a === '--no-cache' || a === '--refresh')
51
65
  o.cache = false;
52
66
  else if (a === '--check')
@@ -58,33 +72,36 @@ function parseArgs(argv) {
58
72
  }
59
73
  return o;
60
74
  }
61
- const HELP = `agy-usage — Antigravity CLI (agy) usage/quota monitor
75
+ const HELP = `agy-cli-usage — Antigravity CLI (agy) usage/quota monitor
62
76
 
63
- agy-usage one-shot panel
64
- agy-usage --json machine-readable JSON
65
- agy-usage --watch [secs] auto-refresh (default 60s)
66
- agy-usage --source <auto|api|pty>
67
- agy-usage --channel <auto|daily|prod>
68
- agy-usage --no-cache | --refresh
69
- agy-usage update [--check] self-update via npm (--check: report only)
70
- agy-usage --version | -v
77
+ agy-cli-usage one-shot panel
78
+ agy-cli-usage --json machine-readable JSON
79
+ agy-cli-usage --watch [secs] auto-refresh (default 60s)
80
+ agy-cli-usage --source <auto|api|pty>
81
+ agy-cli-usage --channel <auto|daily|prod>
82
+ agy-cli-usage --no-cache | --refresh
83
+ agy-cli-usage update [--check] self-update via npm (--check: report only)
84
+ agy-cli-usage --version | -v
71
85
  `;
72
- // --- cache -------------------------------------------------------------------
73
- function readCache() {
86
+ /** Exported for direct unit testing via an injected `cacheFile` — not part of the CLI's public surface. */
87
+ export function readCache(source, channel, cacheFile = CACHE_FILE) {
74
88
  try {
75
- const { ts, snap } = JSON.parse(readFileSync(CACHE_FILE, 'utf8'));
76
- if (Date.now() - ts < CACHE_TTL_MS)
77
- return snap;
89
+ const entry = JSON.parse(readFileSync(cacheFile, 'utf8'));
90
+ if (entry.source !== source || entry.channel !== channel)
91
+ return null;
92
+ if (Date.now() - entry.ts < CACHE_TTL_MS)
93
+ return entry.snap;
78
94
  }
79
95
  catch {
80
- /* no/expired cache */
96
+ /* no/expired/incompatible-format cache */
81
97
  }
82
98
  return null;
83
99
  }
84
- function writeCache(snap) {
100
+ /** Exported for direct unit testing via an injected `cacheFile` — not part of the CLI's public surface. */
101
+ export function writeCache(snap, source, channel, cacheFile = CACHE_FILE) {
85
102
  try {
86
- mkdirSync(CACHE_DIR, { recursive: true });
87
- writeFileSync(CACHE_FILE, JSON.stringify({ ts: Date.now(), snap }));
103
+ mkdirSync(dirname(cacheFile), { recursive: true });
104
+ writeFileSync(cacheFile, JSON.stringify({ ts: Date.now(), source, channel, snap }));
88
105
  }
89
106
  catch {
90
107
  /* cache is best-effort */
@@ -92,7 +109,7 @@ function writeCache(snap) {
92
109
  }
93
110
  export async function getSnapshot(opts) {
94
111
  if (opts.cache && opts.source !== 'pty') {
95
- const cached = readCache();
112
+ const cached = readCache(opts.source, opts.channel, opts.cacheFile);
96
113
  if (cached)
97
114
  return cached;
98
115
  }
@@ -114,7 +131,7 @@ export async function getSnapshot(opts) {
114
131
  snap = fromPty(await captureUsageViaPty());
115
132
  }
116
133
  }
117
- writeCache(snap);
134
+ writeCache(snap, opts.source, opts.channel, opts.cacheFile);
118
135
  return snap;
119
136
  }
120
137
  // --- main --------------------------------------------------------------------
@@ -157,13 +174,31 @@ async function main() {
157
174
  await once(opts);
158
175
  }
159
176
  }
160
- main().catch((err) => {
161
- if (err instanceof CredentialError) {
162
- process.stderr.write(`credential error: ${err.message}\n`);
177
+ // Only run the CLI when this file is executed directly (as the `bin` entry
178
+ // point) guarded so parseArgs/readCache/writeCache/etc. can be imported
179
+ // for unit testing without triggering a full live CLI run (network calls,
180
+ // process.exit()) as an import side effect. realpathSync resolves symlinks
181
+ // on process.argv[1] (npm global `bin` entries are frequently symlinks);
182
+ // import.meta.url is already symlink-resolved by Node's ESM loader.
183
+ function isMainModule() {
184
+ if (!process.argv[1])
185
+ return false;
186
+ try {
187
+ return fileURLToPath(import.meta.url) === realpathSync(process.argv[1]);
163
188
  }
164
- else {
165
- process.stderr.write(`error: ${errMessage(err)}\n`);
189
+ catch {
190
+ return false;
166
191
  }
167
- process.exit(1);
168
- });
192
+ }
193
+ if (isMainModule()) {
194
+ main().catch((err) => {
195
+ if (err instanceof CredentialError) {
196
+ process.stderr.write(`credential error: ${err.message}\n`);
197
+ }
198
+ else {
199
+ process.stderr.write(`error: ${errMessage(err)}\n`);
200
+ }
201
+ process.exit(1);
202
+ });
203
+ }
169
204
  //# sourceMappingURL=main.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agy-cli-usage",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
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",