agy-cli-usage 0.1.0 → 0.2.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/README.md CHANGED
@@ -39,20 +39,23 @@ agy-usage --no-cache # 5분 캐시 무시하고 강제 조회
39
39
 
40
40
  `agy`는 OAuth 토큰을 OS 키링(zalando/go-keyring 규약, service=`gemini`/account=`antigravity`)에 저장한다.
41
41
 
42
- 1. **직접 API (기본)** — 키링에서 토큰을 읽고(만료 시 자동 refresh), `agy`와 동일하게:
42
+ 1. **직접 API (기본)** — 토큰을 읽고(만료 시 자동 refresh), `agy`와 동일하게:
43
43
  - `POST https://<host>/v1internal:loadCodeAssist` → `cloudaicompanionProject` 획득
44
44
  - `POST https://<host>/v1internal:retrieveUserQuotaSummary {project}` → 쿼타
45
45
  - 호스트: `daily-cloudcode-pa.googleapis.com`(현재 CLI) 또는 `cloudcode-pa.googleapis.com`
46
- 2. **PTY 폴백** 키링을 읽을 없거나(헤드리스 ) 내부 API가 바뀌면, `agy`를 가상 터미널로 띄워 `/usage`를 보내고, `@xterm/headless`로 alt-screen을 재구성해 패널을 파싱한다.
46
+ - 토큰 소스: **키링 토큰 파일 (실패 ) PTY** 순으로 자동 시도
47
+ 2. **PTY 폴백** — 위 토큰 소스를 모두 읽을 수 없거나 내부 API가 바뀌면, `agy`를 가상 터미널로 띄워 `/usage`를 보내고, `@xterm/headless`로 alt-screen을 재구성해 패널을 파싱한다.
47
48
 
48
49
  ### 크로스플랫폼 자격증명
49
50
 
50
- | OS | 키링 백엔드 | 비고 |
51
+ | OS | 토큰 소스 | 비고 |
51
52
  |----|-----------|------|
52
53
  | macOS | Keychain | `@napi-rs/keyring`, CLI 폴백 `security` |
53
54
  | Windows | Credential Manager | `@napi-rs/keyring` |
54
55
  | Linux (데스크톱) | Secret Service | `@napi-rs/keyring`, CLI 폴백 `secret-tool` |
55
- | Linux (헤드리스) | | Secret Service 부재 시 `--source pty`로 우회 (python3 필요) |
56
+ | Linux (헤드리스) | **토큰 파일** | 키링 부재 시 `~/.gemini/antigravity-cli/antigravity-oauth-token`(순수 JSON) 읽음 → **API 경로 동작**. `AGY_OAUTH_TOKEN_FILE`로 경로 override |
57
+
58
+ > 헤드리스 서버에서도 토큰 파일을 읽어 빠른 API 경로를 쓴다. 파일조차 없으면 `--source pty`로 우회(python3 필요).
56
59
 
57
60
  ## HTTP 엔드포인트 (선택)
58
61
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agy-cli-usage",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
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
  "bin": {
@@ -10,10 +10,15 @@
10
10
  // Read backends, tried in order:
11
11
  // 1. @napi-rs/keyring native module (macOS / Windows / Linux Secret Service)
12
12
  // 2. OS CLI fallback (`security` on macOS, `secret-tool` on Linux)
13
- // If every backend fails (e.g. a headless Linux box with no Secret Service),
14
- // the caller falls back to the PTY path which drives `agy` itself.
13
+ // 3. File fallback (headless Linux: agy can't reach a keyring
14
+ // and writes the token to a plain-JSON file)
15
+ // If every backend fails, the caller falls back to the PTY path which drives
16
+ // `agy` itself.
15
17
 
16
18
  import { execFileSync } from 'node:child_process';
19
+ import { readFileSync, existsSync } from 'node:fs';
20
+ import { homedir } from 'node:os';
21
+ import { join } from 'node:path';
17
22
 
18
23
  // OAuth client for the Antigravity CLI. This is an installed/desktop ("public")
19
24
  // OAuth client: per Google's own docs the client secret of an installed app is
@@ -68,17 +73,39 @@ function readViaCli() {
68
73
  return null;
69
74
  }
70
75
 
76
+ // On headless Linux (no Secret Service) agy persists the token to a plain-JSON
77
+ // file instead of the keyring. Same payload shape, no `go-keyring-base64:` prefix.
78
+ function readViaFile() {
79
+ const candidates = [
80
+ process.env.AGY_OAUTH_TOKEN_FILE,
81
+ join(homedir(), '.gemini', 'antigravity-cli', 'antigravity-oauth-token'),
82
+ ].filter(Boolean);
83
+ for (const path of candidates) {
84
+ try {
85
+ if (existsSync(path)) {
86
+ const content = readFileSync(path, 'utf8').trim();
87
+ if (content) return content;
88
+ }
89
+ } catch {
90
+ // unreadable (perms) — try next candidate
91
+ }
92
+ }
93
+ return null;
94
+ }
95
+
71
96
  async function readRawSecret() {
72
97
  const fromNapi = await readViaNapiEsm();
73
98
  if (fromNapi) return fromNapi;
74
99
  const fromCli = readViaCli();
75
100
  if (fromCli) return fromCli;
101
+ const fromFile = readViaFile();
102
+ if (fromFile) return fromFile;
76
103
  return null;
77
104
  }
78
105
 
79
106
  // --- decode ------------------------------------------------------------------
80
107
 
81
- function decodeSecret(raw) {
108
+ export function decodeSecret(raw) {
82
109
  const payload = raw.startsWith(B64_PREFIX)
83
110
  ? Buffer.from(raw.slice(B64_PREFIX.length), 'base64').toString('utf8')
84
111
  : raw;
@@ -138,8 +165,9 @@ export async function getAccessToken() {
138
165
  const raw = await readRawSecret();
139
166
  if (!raw) {
140
167
  throw new CredentialError(
141
- 'Could not read agy credential from the OS keyring. ' +
142
- 'Is agy logged in on this machine? (headless servers may lack a keyring use --source pty)',
168
+ 'Could not read agy credential from the OS keyring or token file. ' +
169
+ 'Is agy logged in on this machine? (set AGY_OAUTH_TOKEN_FILE to override the path, ' +
170
+ 'or use --source pty)',
143
171
  );
144
172
  }
145
173
  const cred = decodeSecret(raw);