@jiayunxie/aerial 0.1.5 → 0.1.7

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
@@ -3,278 +3,122 @@
3
3
  [![CI](https://github.com/Xiejiayun/aerial/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/Xiejiayun/aerial/actions/workflows/ci.yml)
4
4
  [![npm version](https://img.shields.io/npm/v/@jiayunxie/aerial.svg)](https://www.npmjs.com/package/@jiayunxie/aerial)
5
5
 
6
- Aerial is a lightweight local-only proxy that lets one user connect their own GitHub Copilot subscription to local coding CLIs.
6
+ Aerial lets Codex CLI and Claude Code use your own GitHub Copilot subscription through a local proxy on your machine.
7
7
 
8
- The MVP runs on `127.0.0.1:18181`, requires a local Aerial API key for model routes, stores credentials on the user's machine, and avoids public deployment, account sharing, quota bypass, dashboards, analytics, and image APIs.
8
+ It runs locally on `127.0.0.1:18181`. Your GitHub token and Aerial key stay on your machine. It is built for personal local use, not for public hosting or account sharing.
9
9
 
10
- ## MVP Support
11
-
12
- Implemented routes:
13
-
14
- - `GET /health` without auth
15
- - `GET /v1/models` for model discovery, with an `aerial` support annotation per model
16
- - `POST /v1/responses` for Codex CLI
17
- - `POST /v1/messages` for Claude Code's Anthropic gateway path
18
- - `POST /v1/messages/count_tokens` with a local estimate
19
- - `POST /v1/chat/completions` as a simple passthrough fallback
20
-
21
- Implemented CLI commands:
22
-
23
- ```bash
24
- aerial login
25
- aerial key generate
26
- aerial key print
27
- aerial start
28
- aerial setup codex [--model <responses-model-id>]
29
- aerial setup claude [--model <messages-model-id>]
30
- aerial setup status
31
- aerial setup restore <codex|claude|all> --latest
32
- aerial service install
33
- aerial service start
34
- aerial service status
35
- aerial service stop
36
- aerial service restart
37
- aerial service uninstall
38
- aerial disable
39
- aerial doctor
40
- aerial probe
41
- ```
42
-
43
- Rollback safety net, log file rotation, and bundled service management (macOS launchd + Windows Task Scheduler) are part of this release. Gemini CLI support, Linux service management, dashboards, and analytics are intentionally out of this MVP. For Codex, local clients keep using HTTP POST /v1/responses. Aerial can optionally use Copilot's upstream `ws:/responses` transport for streaming Responses and translate events back to SSE — this transport is opt-in (`AERIAL_RESPONSES_WEBSOCKET=on`) and HTTP is the default.
10
+ ## Install
44
11
 
45
- ## Requirements
12
+ Requirements:
46
13
 
47
14
  - Node.js 22+
48
15
  - A GitHub account with an active Copilot subscription
49
- - Codex CLI and/or Claude Code installed locally
50
-
51
- ## Install
16
+ - Codex CLI and/or Claude Code installed
52
17
 
53
18
  ```bash
54
19
  npm install -g @jiayunxie/aerial
55
20
  ```
56
21
 
57
- After install, the CLI is available as `aerial`.
58
-
59
- ### Nightly builds (advanced, unstable)
60
-
61
- Nightly pre-releases are published by the same `release.yml` workflow as stable releases, dispatched with `mode=nightly` (and, once scheduled publishing is enabled, by a daily cron on the same workflow). They are routed through a single workflow because npm only allows one trusted publisher per package. The default install command above is unaffected — nightlies are gated behind the `@nightly` dist-tag and will never be served as `latest`. To opt in:
62
-
63
- ```bash
64
- npm install -g @jiayunxie/aerial@nightly
65
- ```
66
-
67
- Nightly versions look like `0.1.1-nightly.YYYYMMDD.<sha7>`. They reflect the current `main` and may break; report regressions with `aerial --version` so the exact build can be reproduced. Switch back to stable any time with `npm install -g @jiayunxie/aerial`.
68
-
69
- ### From source (local development)
22
+ ## Quick Start
70
23
 
71
- For working directly on the Aerial codebase:
24
+ 1. Sign in to GitHub:
72
25
 
73
26
  ```bash
74
- git clone https://github.com/Xiejiayun/aerial.git
75
- cd aerial
76
- npm install -g .
77
- ```
78
-
79
- Run without global install:
80
-
81
- ```bash
82
- node src/cli.js --help
27
+ aerial login
83
28
  ```
84
29
 
85
- ## First Run
86
-
87
- Configure the local client you use. These commands also create Aerial's local API key and wire it into the selected client:
30
+ 2. Configure the client you use:
88
31
 
89
32
  ```bash
33
+ # Codex CLI
90
34
  aerial setup codex
91
- # optional, if you use Claude Code too:
35
+
36
+ # Claude Code
92
37
  aerial setup claude
93
38
  ```
94
39
 
95
- If Codex or Claude Code was already open, restart it after setup so it rereads the updated client config.
96
-
97
- Log in to GitHub with device flow:
98
-
99
- ```bash
100
- aerial login
101
- ```
40
+ During setup, Aerial reads your Copilot model list, shows the models that work for that client, and asks which one to write into the client config. Codex uses models with the `responses` route. Claude Code uses models with the `messages` route.
102
41
 
103
- Install and start the background service:
42
+ 3. Start Aerial in the background:
104
43
 
105
44
  ```bash
106
45
  aerial service install
107
46
  ```
108
47
 
109
- Check health:
48
+ 4. Check everything from one place:
110
49
 
111
50
  ```bash
112
- curl http://127.0.0.1:18181/health
51
+ aerial status
113
52
  ```
114
53
 
115
- ## Codex CLI Setup
54
+ If Aerial prints a hint, follow it before expecting the background service to work after reboot.
116
55
 
117
- Aerial configures Codex through the Responses wire API provider path:
56
+ Restart Codex CLI or Claude Code if either one was already open while you ran setup.
118
57
 
119
- ```bash
120
- aerial setup codex
121
- ```
58
+ ## What Setup Changes
122
59
 
123
- This updates `~/.codex/config.toml` and creates a timestamped backup first. If you only want to inspect the exact change, set `HOME`/`USERPROFILE` to a temporary directory before running setup. The inserted provider uses:
60
+ `aerial setup codex` updates `~/.codex/config.toml`.
124
61
 
125
- ```toml
126
- [model_providers.aerial]
127
- base_url = "http://127.0.0.1:18181/v1"
128
- wire_api = "responses"
62
+ - It points Codex at `http://127.0.0.1:18181/v1`.
63
+ - It uses Codex's command-backed auth helper so Codex can read the local Aerial key automatically.
64
+ - It creates a timestamped backup before writing.
129
65
 
130
- [model_providers.aerial.auth]
131
- command = "<node>"
132
- args = ["<aerial-cli.js>", "key", "print"]
133
- timeout_ms = 5000
134
- refresh_interval_ms = 0
135
- ```
66
+ `aerial setup claude` updates `~/.claude/settings.json`.
67
+
68
+ - It points Claude Code at `http://127.0.0.1:18181`.
69
+ - It configures an API key helper so Claude Code can read the local Aerial key automatically.
70
+ - It creates a timestamped backup before writing.
136
71
 
137
- Aerial creates and stores the local key automatically. Codex reads it through the configured helper command, so users do not need to run `aerial key generate`, copy a key, or export `AERIAL_API_KEY`.
72
+ You normally do not need to create, copy, or export an API key yourself.
138
73
 
139
- ## Claude Code Setup
74
+ ## Choosing A Model
140
75
 
141
- Aerial configures Claude Code through its Anthropic-compatible gateway settings:
76
+ The easiest path is to let setup show the compatible models:
142
77
 
143
78
  ```bash
79
+ aerial setup codex
144
80
  aerial setup claude
145
81
  ```
146
82
 
147
- This updates `~/.claude/settings.json` and creates a timestamped backup first. If you only want to inspect the exact change, set `HOME`/`USERPROFILE` to a temporary directory before running setup. It sets:
148
-
149
- ```json
150
- {
151
- "model": "<available-copilot-model-id>",
152
- "apiKeyHelper": "aerial key print",
153
- "env": {
154
- "ANTHROPIC_BASE_URL": "http://127.0.0.1:18181",
155
- "CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY": "1"
156
- }
157
- }
158
- ```
159
-
160
- The `model` field is written when you pass `--model` or set Aerial's `defaultModel`; otherwise Aerial leaves any existing Claude Code model choice alone while still switching the gateway to the local Aerial endpoint.
161
-
162
- If you need explicit models, pass `--model <responses-model-id>` to `aerial setup codex` and `--model <messages-model-id>` to `aerial setup claude`. Aerial intentionally does not provide `aerial setup all` as a setup shortcut because Codex and Claude Code may need different model IDs.
163
-
164
- `aerial key print` prints the locally stored Aerial API key for Claude Code's helper flow. Users normally do not need to call it directly.
165
-
166
- When switching from another Anthropic-compatible gateway, setup removes stale `ANTHROPIC_API_KEY`, `ANTHROPIC_AUTH_TOKEN`, `ANTHROPIC_MODEL`, and `ANTHROPIC_DEFAULT_*_MODEL` entries from Claude Code's managed `env` block so `apiKeyHelper` and the Aerial gateway are the active route.
167
-
168
- ## Diagnostics
83
+ To pin a model without the prompt:
169
84
 
170
85
  ```bash
171
- aerial doctor
172
- aerial probe
86
+ aerial setup codex --model <responses-model-id>
87
+ aerial setup claude --model <messages-model-id>
173
88
  ```
174
89
 
175
- The doctor checks config, local API key presence, GitHub login state, Node version, and local bind address.
176
-
177
- ## Security Boundary
178
-
179
- Aerial is for personal local use only.
180
-
181
- - It binds to `127.0.0.1` by default.
182
- - Model routes require `Authorization: Bearer <AERIAL_API_KEY>` or `x-api-key: <AERIAL_API_KEY>`.
183
- - GitHub tokens are stored under the user config directory with private file permissions where supported.
184
- - Aerial does not log raw GitHub tokens, Copilot JWTs, API keys, or request bodies by default.
185
- - Do not expose this service publicly or use it for account sharing.
186
-
187
- ## Current Limitations
188
-
189
- - Copilot inference routes are an observed compatibility target, not a public stable GitHub REST API.
190
- - `/v1/messages/count_tokens` is a local estimate, not upstream tokenization.
191
- - Bundled service management is implemented for macOS (launchd) and Windows (Task Scheduler). On Linux, run `aerial start` directly or wrap it in your own init system.
192
- - For explicit model overrides, query `/v1/models`, choose a `responses` model for Codex and a `messages` model for Claude Code, then rerun the client-specific setup command with `--model`.
193
- - Chat Completions requests normalize `max_tokens` to `max_completion_tokens` for newer OpenAI models that reject the older field.
194
- - Prompt caching is upstream-managed: Aerial does not store prompt bodies locally, and it preserves or injects cache protocol fields before forwarding.
195
-
196
-
197
-
198
- ## Capability Probe
199
-
200
- Use `aerial probe` to inspect the live Copilot model matrix through the same local credentials Aerial uses for proxying:
90
+ To inspect the full model matrix:
201
91
 
202
92
  ```bash
203
93
  aerial probe
204
94
  ```
205
95
 
206
- This prints model IDs, Aerial routes, and unsupported notes such as `embeddings_not_implemented` or `no_supported_endpoint_advertised`. Models with upstream `ws:/responses` support are shown with the `responses_websocket` route.
207
-
208
- Run low-cost live route checks when you want to verify end-to-end behavior:
96
+ ## Daily Commands
209
97
 
210
98
  ```bash
211
- aerial probe --live
212
- # machine-readable output
213
- aerial probe --live --json
99
+ aerial status # setup, login, service, and health summary
100
+ aerial service install # install and start the background service
101
+ aerial doctor # local diagnostics
102
+ aerial disable # restore client configs and uninstall the service
214
103
  ```
215
104
 
216
- `--live` sends one small request through the first available `responses`, `messages`, and `chat` model. It does not test every model by default, which keeps the command lightweight.
217
- ## Prompt Cache
218
-
219
- Aerial uses the upstream Copilot/OpenAI/Anthropic cache protocols instead of keeping a local prompt-content cache. That keeps the MVP lightweight and avoids storing prompts on disk.
220
-
221
- Supported behavior:
222
-
223
- - Responses and Chat Completions preserve `prompt_cache_retention` and `prompt_cache_key` when the client sends them.
224
- - Responses and Chat Completions automatically add `prompt_cache_retention: "in_memory"` and a stable hashed `prompt_cache_key` when the client omits them.
225
- - Anthropic Messages preserves client `cache_control` blocks. When none are present and caching is enabled, Aerial automatically adds `cache_control: { "type": "ephemeral" }` to stable `system` content, or to the final tool definition when there is no system content.
226
- - Usage fields from upstream are returned unchanged, including `cached_tokens`, `cache_creation_input_tokens`, `cache_read_input_tokens`, and Copilot `cache_read` / `cache_write` token details when present.
227
- - Aerial logs cache metadata to stderr as `cache_request` and `cache_observe` events. These logs do not include prompt text or request bodies.
228
-
229
- OpenAI's prompt caching documentation says cache hits require an exact prompt prefix match and are only possible once the prompt is at least 1024 tokens. Confirm cache behavior from returned usage fields such as `usage.prompt_tokens_details.cached_tokens` or `usage.input_tokens_details.cached_tokens`; for shorter prompts this value is expected to be zero.
230
-
231
- Aerial enables ephemeral prompt caching by default for OpenAI-style routes and Anthropic Messages. Override it only when needed:
105
+ `aerial start` is for foreground debugging in the current terminal. Most users should use `aerial service install`.
232
106
 
233
- ```bash
234
- aerial config set promptCacheRetention in_memory
235
- # or
236
- aerial config set promptCacheRetention 24h
237
- # disable automatic cache hints
238
- aerial config set promptCacheRetention off
239
- aerial config set promptCacheKey off
240
- # pin all requests to an explicit cache partition
241
- aerial config set promptCacheKey my-project
242
- ```
243
-
244
- You can also set it per process:
245
-
246
- ```bash
247
- export AERIAL_PROMPT_CACHE_RETENTION=in_memory
248
- export AERIAL_PROMPT_CACHE_KEY=my-project
249
- ```
250
-
251
- Per-request fields win over the configured default. Use `24h` only with models whose upstream route accepts extended retention; Anthropic Messages still uses Anthropic-style `cache_control: { "type": "ephemeral" }`. Set `promptCacheKey` to `auto` to use Aerial's hashed stable key, `off` to omit it, or a string to force a specific cache partition.
252
- ## Model Support
253
-
254
- `GET /v1/models` returns Copilot's raw model metadata and adds an Aerial-specific field:
255
-
256
- ```json
257
- {
258
- "id": "gpt-5.4-mini",
259
- "supported_endpoints": ["/responses", "ws:/responses"],
260
- "aerial": {
261
- "supported": true,
262
- "routes": ["responses", "responses_websocket"],
263
- "notes": []
264
- }
265
- }
266
- ```
107
+ Advanced service lifecycle commands are documented in `docs/usage.md`.
267
108
 
268
- Route meanings:
109
+ ## Troubleshooting
269
110
 
270
- - `responses`: usable by Codex through HTTP `POST /v1/responses`.
271
- - `responses_websocket`: Aerial can use Copilot's upstream `ws:/responses` transport for streaming `/v1/responses` requests when `AERIAL_RESPONSES_WEBSOCKET=on` is set, then return SSE to the local client. When the opt-in is off (default), Aerial always uses HTTP upstream Responses even if a model advertises `ws:/responses`.
272
- - `messages`: usable by Claude Code through `POST /v1/messages`.
273
- - `chat`: usable by generic OpenAI Chat clients through `POST /v1/chat/completions`.
111
+ - `Missing GitHub token`: run `aerial login`.
112
+ - `Invalid or missing Aerial API key`: rerun `aerial setup codex` or `aerial setup claude`, then restart the client.
113
+ - No models listed during setup: run `aerial login` first, then retry setup.
114
+ - Port conflict on `18181`: run `aerial status` to see whether another process is using the port.
115
+ - Need to undo setup: run `aerial disable`, or restore one client with `aerial setup restore <codex|claude> --latest`.
274
116
 
275
- Local clients should not open WebSocket connections to Aerial directly. Direct WebSocket upgrades still return `501 Not Implemented`; use HTTP `POST /v1/responses` and let Aerial choose the upstream transport. HTTP upstream Responses is the default; set `AERIAL_RESPONSES_WEBSOCKET=on` to opt into Copilot's upstream WebSocket transport for streaming Responses calls (still experimental, only effective for streaming requests on models that advertise `ws:/responses`).
117
+ ## Notes
276
118
 
277
- Known unsupported notes:
119
+ - macOS background service support uses a user LaunchAgent.
120
+ - Windows background service support uses a user Task Scheduler task.
121
+ - Linux service management is not built in yet; run `aerial start` or use your own init system.
122
+ - Copilot inference routes are an observed compatibility target and may change upstream.
278
123
 
279
- - `embeddings_not_implemented`: Aerial does not expose `/v1/embeddings` yet.
280
- - `no_supported_endpoint_advertised`: the model did not declare a route Aerial can safely select.
124
+ More details: `docs/usage.md`.
package/docs/usage.md CHANGED
@@ -23,12 +23,13 @@ node src/cli.js --help
23
23
  ## 2. Configure Local Clients
24
24
 
25
25
  ```bash
26
+ aerial login
26
27
  aerial setup codex
27
28
  # optional, if you use Claude Code too:
28
29
  aerial setup claude
29
30
  ```
30
31
 
31
- Aerial creates a local API key, stores it privately, and configures the selected clients to use it. There is intentionally no `aerial setup all` setup shortcut: Codex and Claude Code can need different model IDs, so setup stays client-specific. If Codex or Claude Code was already open, restart it after setup so it rereads the updated client config.
32
+ Aerial creates a local API key, stores it privately, asks you to choose from compatible Copilot models, and configures the selected clients to use it. Run `aerial login` before setup so model discovery can read your Copilot model list. There is intentionally no `aerial setup all` setup shortcut: Codex and Claude Code can need different model IDs, so setup stays client-specific. If Codex or Claude Code was already open, restart it after setup so it rereads the updated client config.
32
33
 
33
34
  ## 3. Login To GitHub
34
35
 
@@ -52,7 +53,7 @@ Default URL: `http://127.0.0.1:18181`. `aerial service install` is the daily-use
52
53
  aerial setup codex
53
54
  ```
54
55
 
55
- The setup command backs up and merges `~/.codex/config.toml`, then configures Codex to fetch the local Aerial key through a command-backed provider auth helper:
56
+ The setup command backs up and merges `~/.codex/config.toml`, asks you to choose a model whose Aerial routes include `responses`, then configures Codex to fetch the local Aerial key through a command-backed provider auth helper:
56
57
 
57
58
  ```toml
58
59
  [model_providers.aerial]
@@ -70,7 +71,7 @@ The local key is generated and stored by Aerial automatically. Users do not need
70
71
 
71
72
  For a dry inspection without touching your real config, set `HOME`/`USERPROFILE` to a temporary directory before running this command.
72
73
 
73
- If you need to pin a model, first run `aerial probe` or call `/v1/models`, choose a model whose Aerial routes include `responses`, then run `aerial setup codex --model <responses-model-id>`.
74
+ To skip the prompt, pass `--model <responses-model-id>`.
74
75
 
75
76
  ## 6. Configure Claude Code
76
77
 
@@ -78,21 +79,50 @@ If you need to pin a model, first run `aerial probe` or call `/v1/models`, choos
78
79
  aerial setup claude
79
80
  ```
80
81
 
81
- The setup command backs up and merges `~/.claude/settings.json`, using `apiKeyHelper = "aerial key print"` and `ANTHROPIC_BASE_URL=http://127.0.0.1:18181`. When you pass `--model` or set Aerial's `defaultModel`, it also writes Claude Code's default `model` to that Aerial-routed model.
82
+ The setup command backs up and merges `~/.claude/settings.json`, asks you to choose a model whose Aerial routes include `messages`, then writes an absolute `apiKeyHelper` command and `ANTHROPIC_BASE_URL=http://127.0.0.1:18181`. The helper lets Claude Code read the local Aerial key automatically without relying on a shell `AERIAL_API_KEY` export or a refreshed `PATH`.
82
83
 
83
84
  If Claude Code was previously pointed at another Anthropic-compatible gateway, setup removes stale `ANTHROPIC_API_KEY`, `ANTHROPIC_AUTH_TOKEN`, `ANTHROPIC_MODEL`, and `ANTHROPIC_DEFAULT_*_MODEL` entries from the managed `env` block.
84
85
 
85
86
  For a dry inspection without touching your real config, set `HOME`/`USERPROFILE` to a temporary directory before running this command.
86
87
 
87
- If you need to pin a model, first run `aerial probe` or call `/v1/models`, choose a model whose Aerial routes include `messages`, then run `aerial setup claude --model <messages-model-id>`.
88
+ To skip the prompt, pass `--model <messages-model-id>`.
88
89
 
89
90
  ## 7. Verify
90
91
 
91
92
  ```bash
93
+ aerial status
92
94
  aerial doctor
93
95
  aerial probe
94
96
  ```
95
97
 
98
+ `aerial status` is the top-level daily check. It combines setup state, local auth files, service state, and health into one short report. Use `--json` for a machine-readable `aerial.status.v1` document.
99
+
100
+ `aerial status --json` top-level fields: `schema`, `ok`, `setup`, `service`, `nextSteps`, `hints`. `nextSteps` lists actions you must take to reach `ok: true` (login / setup / service install / recover local key). `hints` lists non-blocking advisories that you should still pay attention to, such as `AERIAL_GITHUB_TOKEN` being set for the current shell only (the background service may not see it after reboot). Example:
101
+
102
+ ```json
103
+ {
104
+ "schema": "aerial.status.v1",
105
+ "ok": false,
106
+ "nextSteps": ["run: aerial setup codex or aerial setup claude"],
107
+ "hints": ["AERIAL_GITHUB_TOKEN is set for this process only; run aerial login without that env var to persist a service-readable login."],
108
+ "setup": { "...": "aerial.setup-status.v1 fields here" },
109
+ "service": { "...": "aerial.service-status.v1 fields here" }
110
+ }
111
+ ```
112
+
113
+ ### Local HTTP endpoints
114
+
115
+ | Request | Local Aerial API key | Behavior |
116
+ | --- | --- | --- |
117
+ | `GET /` | not required | 200 + friendly status JSON pointing at `/health` and `aerial status`; no secrets in body |
118
+ | `GET /health` | not required | 200 + minimal `{ ok, service, host, port }` |
119
+ | `GET /v1/models` (with GitHub login) | not required | 200 + Copilot model list with Aerial route annotations |
120
+ | `GET /v1/models` (no GitHub login) | not required | 401 + `error.aerial.status = "login_required"` |
121
+ | `GET /v1/models` (GitHub login rejected by Copilot) | not required | 401/403 + `error.aerial.status = "upstream_auth_failed"` and `upstream_status` |
122
+ | `POST /v1/responses`, `/v1/messages`, `/v1/messages/count_tokens`, `/v1/chat/completions` | required | 401 if key missing/invalid; otherwise proxied to Copilot |
123
+
124
+ None of these endpoints emit `WWW-Authenticate` or open CORS (`Access-Control-Allow-Origin: *`). The `login_required` and `upstream_auth_failed` JSON shapes never include tokens, keys, or filesystem paths.
125
+
96
126
  Each model returned by `/v1/models` includes an `aerial` field that tells you whether the MVP can route it:
97
127
 
98
128
  ```json
@@ -131,7 +161,7 @@ aerial disable
131
161
  "config": { "host": "127.0.0.1", "port": 18181 },
132
162
  "auth": {
133
163
  "api_key": { "file": "/Users/you/Library/Application Support/aerial/api_key", "exists": true },
134
- "github_token": { "file": "/Users/you/Library/Application Support/aerial/github_token", "exists": false }
164
+ "github_token": { "file": "/Users/you/Library/Application Support/aerial/github_token", "exists": true, "source": "file" }
135
165
  },
136
166
  "clients": {
137
167
  "codex": {
@@ -139,7 +169,7 @@ aerial disable
139
169
  "state": "aerial",
140
170
  "file": "/Users/you/.codex/config.toml",
141
171
  "backups": ["/Users/you/.codex/config.toml.aerial-backup-2026-05-20T10-00-00-000Z"],
142
- "model": "gpt-4.1",
172
+ "model": "gpt-5.5",
143
173
  "baseUrl": "http://127.0.0.1:18181/v1"
144
174
  },
145
175
  "claude": {
@@ -154,7 +184,9 @@ aerial disable
154
184
  }
155
185
  ```
156
186
 
157
- Stability rules for `aerial.setup-status.v1`: new fields may be added at any level in future Aerial releases consumers must ignore unknown keys. Existing fields will not be removed or repurposed without bumping the `schema` value. Field types remain constant within a schema version.
187
+ `auth.github_token.source` is one of `"missing"`, `"file"`, or `"env"`. `exists` is derived: `source !== "missing"` ("the current process can read a GitHub token"). `source = "env"` means the token came from `AERIAL_GITHUB_TOKEN`, which the background service generally cannot see — run `aerial login` without that env var to persist a service-readable login. To detect a persisted file login specifically, read `source === "file"`.
188
+
189
+ Stability rules for `aerial.setup-status.v1`: new fields may be added at any level in future Aerial releases — consumers must ignore unknown keys. Existing fields will not be removed or repurposed without bumping the `schema` value. Field types remain constant within a schema version. Note: in 0.1.6, `auth.github_token.exists` reflects "process-readable" (file or env), not strictly "token file present". Downstream consumers that need the old file-only semantics should read `auth.github_token.source === "file"`.
158
190
 
159
191
  `aerial setup restore <codex|claude|all> --latest` restores the most recent `*.aerial-backup-<ISO>` snapshot for the named client. Before overwriting, it takes a `*.aerial-pre-restore-<ISO>` snapshot of the current file so the restore itself is reversible. With `all`, both clients are restored best-effort and the command exits non-zero if any individual restore failed. If there is no backup to restore, the command prints a note and exits 0.
160
192
 
@@ -191,7 +223,7 @@ Wrapper env values are baked in at install time. If you set `AERIAL_LOG_MAX_BYTE
191
223
 
192
224
  `aerial service start` enforces a similar shape: it refuses with `reason=not_installed` and exit 1 if the unit/task does not exist; it refuses with `reason=port_conflict` and exit 1 if a non-Aerial process owns the port; it refuses with `reason=foreground_running` and exit 1 if Aerial is already running in the foreground (not via the service manager); it returns idempotent success with `note=already running (service-managed)` if the service is already up; otherwise it starts the service. `aerial service stop` is idempotent (exit 0 + `note` when nothing is installed or nothing is running). `aerial service uninstall` is idempotent on the "no service installed" branch, but does NOT swallow real teardown failures: on macOS, if the service is loaded and `launchctl bootout` returns non-zero, the plist and wrapper are preserved and the command exits 1 with `reason=bootout_failed`; on Windows, if `schtasks /Delete` returns non-zero, the wrapper is preserved and the command exits 1 with `reason=delete_failed`. In both cases the message includes a retry pointer. `aerial service restart` blocks the start step when the stop step fails: if `stop` returns `ok=false`, the response includes `reason=stop_failed` and `start` is not attempted. `aerial disable` follows the same contract: it restores client configs first, then calls `serviceUninstall`. Only an `unsupported platform` exception is treated as a silent skip (Linux); any supported-platform uninstall failure (`ok=false` or non-`unsupported-platform` throw) propagates as exit 1 with a retry pointer at `aerial service uninstall`.
193
225
 
194
- If GitHub login is not yet configured, install and start still succeed (when reachable) and emit a structured warning pointing at `aerial login`; proxy requests return 503 until you log in. After rotating the local API key or moving the config directory, restart the service with `aerial service restart` so the new credentials are picked up.
226
+ If GitHub login is not yet configured, install and start still succeed (when reachable) and emit a structured warning pointing at `aerial login`; inference proxy requests return 503 until you log in, while `GET /v1/models` returns 401 with `error.aerial.status = "login_required"`. After rotating the local API key or moving the config directory, restart the service with `aerial service restart` so the new credentials are picked up.
195
227
 
196
228
  `aerial service status` reports a single aggregated view:
197
229
 
@@ -321,7 +353,7 @@ Best practice: put stable system/project/tool context first, put changing user i
321
353
 
322
354
  - `503 Missing GitHub token`: run `aerial login`.
323
355
  - `401 Invalid or missing Aerial API key`: run `aerial setup codex` or `aerial setup claude` for the client you use, then restart the client terminal or VS Code.
324
- - Claude Code cannot read key: ensure `aerial` is on `PATH`; it uses `aerial key print` as its helper.
356
+ - Claude Code cannot read key: rerun `aerial setup claude` so the settings file gets a fresh absolute API-key helper command.
325
357
  - Upstream compatibility error: run `aerial doctor`, then retry with a model returned by `/v1/models`.
326
358
  - `Unsupported parameter: max_tokens`: Aerial normalizes Chat Completions `max_tokens` into `max_completion_tokens` before forwarding to newer OpenAI models.
327
359
  - Cache hit stays zero: ensure the stable prefix is long enough, unchanged, and placed before variable content. For Responses/Chat, try a stable `prompt_cache_key`; for Messages, keep stable content in `system` or put manual `cache_control` on the stable content block.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jiayunxie/aerial",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Local-only GitHub Copilot proxy for Codex CLI and Claude Code.",
5
5
  "type": "module",
6
6
  "private": false,
@@ -0,0 +1,23 @@
1
+ export function computeAppStatus(setup, service) {
2
+ const githubTokenPresent = setup.auth.github_token.source !== "missing";
3
+ const apiKeyPresent = setup.auth.api_key.exists;
4
+ const hasAerialClient = setup.clients.codex.state === "aerial" || setup.clients.claude.state === "aerial";
5
+ const serviceHealthy = service.supported !== false && service.health?.aerial === true;
6
+ const ok = apiKeyPresent && githubTokenPresent && hasAerialClient && serviceHealthy;
7
+ const nextSteps = [];
8
+ const hints = [];
9
+ if (!githubTokenPresent) nextSteps.push("run: aerial login");
10
+ if (!hasAerialClient) nextSteps.push("run: aerial setup codex or aerial setup claude");
11
+ if (hasAerialClient && !apiKeyPresent) nextSteps.push("run: aerial setup codex or aerial setup claude to recreate the local Aerial key");
12
+ if (service.supported !== false && !service.service?.loaded) {
13
+ if (serviceHealthy) {
14
+ hints.push("Aerial is running in the foreground but no background service is installed; run `aerial service install` so it starts on reboot.");
15
+ } else {
16
+ nextSteps.push("run: aerial service install");
17
+ }
18
+ }
19
+ if (setup.auth.github_token.source === "env") {
20
+ hints.push("AERIAL_GITHUB_TOKEN is set for this process only; run aerial login without that env var to persist a service-readable login.");
21
+ }
22
+ return { schema: "aerial.status.v1", ok, setup, service, nextSteps, hints };
23
+ }
package/src/auth.js CHANGED
@@ -48,9 +48,20 @@ export async function pollDeviceFlow(deviceCode, intervalSeconds) {
48
48
  }
49
49
 
50
50
  export function readGitHubToken() {
51
- if (process.env.AERIAL_GITHUB_TOKEN) return process.env.AERIAL_GITHUB_TOKEN;
51
+ const envToken = process.env.AERIAL_GITHUB_TOKEN?.trim();
52
+ if (envToken) return envToken;
52
53
  if (!fs.existsSync(githubTokenPath())) return undefined;
53
- return fs.readFileSync(githubTokenPath(), "utf8").trim();
54
+ const fileToken = fs.readFileSync(githubTokenPath(), "utf8").trim();
55
+ return fileToken || undefined;
56
+ }
57
+
58
+ export function gitHubTokenSource() {
59
+ if (process.env.AERIAL_GITHUB_TOKEN?.trim()) return "env";
60
+ if (fs.existsSync(githubTokenPath())) {
61
+ const fileToken = fs.readFileSync(githubTokenPath(), "utf8").trim();
62
+ if (fileToken) return "file";
63
+ }
64
+ return "missing";
54
65
  }
55
66
 
56
67
  function jwtExpirySeconds(token) {
@@ -71,7 +82,11 @@ export async function exchangeCopilotToken(githubToken = readGitHubToken()) {
71
82
  "user-agent": "Aerial/0.1"
72
83
  }
73
84
  });
74
- if (!response.ok) throw new Error(`Copilot token exchange failed: ${response.status} ${await response.text()}`);
85
+ if (!response.ok) {
86
+ const err = new Error(`Copilot token exchange failed: ${response.status} ${await response.text()}`);
87
+ err.aerialUpstreamStatus = response.status;
88
+ throw err;
89
+ }
75
90
  const payload = await response.json();
76
91
  const token = payload.token;
77
92
  if (!token) throw new Error("Copilot token exchange response did not include token");
@@ -90,3 +105,8 @@ export async function getCopilotToken({ force = false } = {}) {
90
105
  }
91
106
  return refreshPromise;
92
107
  }
108
+
109
+ export function _resetCopilotTokenCacheForTests() {
110
+ cachedCopilotToken = undefined;
111
+ refreshPromise = undefined;
112
+ }