@jiayunxie/aerial 0.2.5 → 0.2.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 +9 -5
- package/docs/usage.md +7 -5
- package/package.json +2 -2
- package/src/cli/commands/config.js +1 -1
- package/src/cli/commands/setup.js +39 -8
- package/src/cli/commands/start.js +1 -1
- package/src/cli/commands/status.js +1 -1
- package/src/cli/commands/{disable.js → teardown.js} +2 -2
- package/src/cli/doctor.js +1 -1
- package/src/cli/helpers.js +92 -0
- package/src/cli/index.js +41 -9
- package/src/cli/output.js +1 -0
- package/src/cli/probe.js +6 -3
- package/src/cli/select.js +425 -0
- package/src/proxy/effort-routing.js +89 -21
- package/src/proxy/index.js +3 -3
- package/src/proxy/model-catalog.js +15 -6
- package/src/proxy/models.js +30 -0
- package/src/service/platform.js +1 -1
- package/src/setup/clients.js +24 -14
- package/src/setup/index.js +26 -3
- package/src/setup/restore.js +38 -2
- package/src/setup/toml.js +70 -19
- package/src/shared/effort.js +70 -1
- package/src/shared/paths.js +1 -1
- package/src/shared/{file-utils.js → utils.js} +19 -0
- package/src/cli/app-status.js +0 -23
- package/src/cli/args.js +0 -28
- package/src/cli/help.js +0 -23
- package/src/cli/model-selection.js +0 -119
- package/src/cli/runtime-auth.js +0 -21
- package/src/cli/setup-selection.js +0 -47
- package/src/cli/version.js +0 -19
- package/src/proxy/model-utils.js +0 -20
- package/src/setup/backup.js +0 -38
- package/src/setup/status.js +0 -24
- package/src/shared/http-utils.js +0 -9
- package/src/shared/prompt-utils.js +0 -8
package/README.md
CHANGED
|
@@ -53,6 +53,8 @@ During setup, Aerial reads your Copilot model list, shows the models that work f
|
|
|
53
53
|
aerial service install
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
+
On Windows, run this command from an Administrator PowerShell or Command Prompt. Aerial registers its background service with Task Scheduler, and Windows requires elevation for that registration step.
|
|
57
|
+
|
|
56
58
|
4. Check everything from one place:
|
|
57
59
|
|
|
58
60
|
```bash
|
|
@@ -91,10 +93,12 @@ aerial setup claude
|
|
|
91
93
|
To skip the prompts:
|
|
92
94
|
|
|
93
95
|
```bash
|
|
94
|
-
aerial setup codex --model <responses-model-id> --effort <low|medium|high|xhigh|max>
|
|
96
|
+
aerial setup codex --model <responses-model-id> --effort <minimal|low|medium|high|xhigh|max|ultra>
|
|
95
97
|
aerial setup claude --model <messages-model-id> --effort <low|medium|high|xhigh|max>
|
|
96
98
|
```
|
|
97
99
|
|
|
100
|
+
Codex effort is model-aware. Aerial preserves an effort when the selected Copilot model advertises it and otherwise selects the nearest usable effort, preferring the highest level below the request. Codex `minimal` maps to Copilot `none`; `none` is also accepted as an input alias. Claude Code keeps its own effort semantics, where `max` is an alias for `xhigh`.
|
|
101
|
+
|
|
98
102
|
To inspect the full model matrix:
|
|
99
103
|
|
|
100
104
|
```bash
|
|
@@ -120,9 +124,9 @@ Advanced override: `AERIAL_UPSTREAM_PROXY` can be set to a bare `http://`, `http
|
|
|
120
124
|
```bash
|
|
121
125
|
aerial status # setup, login, service, and health summary
|
|
122
126
|
aerial proxy status # upstream proxy mode, egress, and route visibility
|
|
123
|
-
aerial service install # install and start the background service
|
|
127
|
+
aerial service install # install and start the background service (Windows: Administrator terminal required)
|
|
124
128
|
aerial doctor # local diagnostics
|
|
125
|
-
aerial
|
|
129
|
+
aerial teardown # restore client configs and uninstall the service
|
|
126
130
|
```
|
|
127
131
|
|
|
128
132
|
`aerial start` is for foreground debugging in the current terminal. Most users should use `aerial service install`.
|
|
@@ -136,12 +140,12 @@ Advanced service lifecycle commands are documented in `docs/usage.md`.
|
|
|
136
140
|
- No models listed during setup: run `aerial login` first, then retry setup.
|
|
137
141
|
- Claude models or the `messages` route are missing: run `aerial proxy enable`, then `aerial probe`.
|
|
138
142
|
- Port conflict on `18181`: run `aerial status` to see whether another process is using the port.
|
|
139
|
-
- Need to undo setup: run `aerial
|
|
143
|
+
- Need to undo setup: run `aerial teardown`, or restore one client with `aerial setup restore <codex|claude> --latest`.
|
|
140
144
|
|
|
141
145
|
## Notes
|
|
142
146
|
|
|
143
147
|
- macOS background service support uses a user LaunchAgent.
|
|
144
|
-
- Windows background service support uses a user Task Scheduler task.
|
|
148
|
+
- Windows background service support uses a user Task Scheduler task. Run `aerial service install` from an Administrator terminal to register it.
|
|
145
149
|
- Linux service management is not built in yet; run `aerial start` or use your own init system.
|
|
146
150
|
- Copilot inference routes are an observed compatibility target and may change upstream.
|
|
147
151
|
|
package/docs/usage.md
CHANGED
|
@@ -67,6 +67,8 @@ aerial service install
|
|
|
67
67
|
|
|
68
68
|
Default URL: `http://127.0.0.1:18181`. `aerial service install` is the daily-use path on macOS and Windows because it installs and starts the local background service. Use `aerial start` only when you want a foreground debug process in the current terminal.
|
|
69
69
|
|
|
70
|
+
On Windows, run `aerial service install` from an Administrator PowerShell or Command Prompt. Aerial registers the background service with Task Scheduler, and Windows requires elevation for that registration step.
|
|
71
|
+
|
|
70
72
|
## 5. Configure Codex CLI
|
|
71
73
|
|
|
72
74
|
```bash
|
|
@@ -91,7 +93,7 @@ The local key is generated and stored by Aerial automatically. Users do not need
|
|
|
91
93
|
|
|
92
94
|
For a dry inspection without touching your real config, set `HOME`/`USERPROFILE` to a temporary directory before running this command.
|
|
93
95
|
|
|
94
|
-
To skip the prompts, pass `--model <responses-model-id>` and/or `--effort <low|medium|high|xhigh|max
|
|
96
|
+
To skip the prompts, pass `--model <responses-model-id>` and/or `--effort <minimal|low|medium|high|xhigh|max|ultra>`. `none` is accepted as an alias for Codex `minimal`. Aerial resolves the request against the selected model's live Copilot capabilities: supported values are preserved, while unsupported values fall back to the nearest usable level (`ultra` currently becomes `max` on GPT-5.6 models, and `minimal` is sent as Copilot `none`). The resolved Codex value is written as root-level `model_reasoning_effort = "<effort>"`. Codex setup does not change Aerial `defaultEffort`, which remains the Claude proxy fallback. Under non-TTY (CI/pipes) the wizard does not prompt and falls back to `medium` when the selected model supports it.
|
|
95
97
|
|
|
96
98
|
## 6. Configure Claude Code
|
|
97
99
|
|
|
@@ -171,7 +173,7 @@ Use `responses` models for Codex, `messages` models for Claude Code, and `chat`
|
|
|
171
173
|
```bash
|
|
172
174
|
aerial setup status [--json]
|
|
173
175
|
aerial setup restore <codex|claude|all> --latest
|
|
174
|
-
aerial
|
|
176
|
+
aerial teardown
|
|
175
177
|
```
|
|
176
178
|
|
|
177
179
|
`aerial setup status` reports whether each supported client is currently configured to route through Aerial, plus the local API key and GitHub token files. The state for each client is one of:
|
|
@@ -221,7 +223,7 @@ Stability rules for `aerial.setup-status.v1`: new fields may be added at any lev
|
|
|
221
223
|
|
|
222
224
|
`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.
|
|
223
225
|
|
|
224
|
-
`aerial
|
|
226
|
+
`aerial teardown` first runs `setup restore all --latest` and then, only if every client restore succeeded, uninstalls the local Aerial service via `aerial service uninstall`. If any client restore reports a failure, the service is left running and `aerial teardown` exits non-zero; resolve the restore errors, then rerun `aerial teardown` (or call `aerial service uninstall` directly once the client config is in the state you want).
|
|
225
227
|
|
|
226
228
|
## 7.6 Run Aerial As A Local Service
|
|
227
229
|
|
|
@@ -237,7 +239,7 @@ aerial service uninstall
|
|
|
237
239
|
Aerial ships a thin platform wrapper around the user-mode service primitives provided by the host OS — there is no Aerial-specific daemon. Two platforms are supported:
|
|
238
240
|
|
|
239
241
|
- macOS: a user-level launchd `LaunchAgent` at `~/Library/LaunchAgents/com.jiayunxie.aerial.plist` invokes a generated POSIX shell wrapper at `<config-dir>/bin/aerial-service.sh`. The plist is regenerated on every `aerial service install` with a `<!-- Generated by aerial; do not edit -->` header, `KeepAlive = { SuccessfulExit = false; Crashed = true }` so the agent only restarts on crash (not on a clean exit), `ThrottleInterval = 10` to cap restart cadence, and no `StandardOutPath`/`StandardErrorPath` keys (the wrapper owns stdio redirection so launchd does not hold a write fd that would race with rotation). The launchctl command path is `gui/<uid>` (per-user agent, no privilege escalation). Start/stop go through `launchctl bootstrap` and `launchctl bootout` against that domain — never `launchctl kill`.
|
|
240
|
-
- Windows: a Task Scheduler task named `AerialLocalProxy`, `/SC ONLOGON /RL LIMITED`, executes a PowerShell wrapper at `<config-dir>\bin\aerial-service.ps1` (default `%APPDATA%\aerial\bin\aerial-service.ps1`). The wrapper is regenerated on every `aerial service install`. The `/TR` argument quotes the wrapper path with normal double quotes so paths that contain spaces or non-ASCII characters work without manual quoting.
|
|
242
|
+
- Windows: a Task Scheduler task named `AerialLocalProxy`, `/SC ONLOGON /RL LIMITED`, executes a PowerShell wrapper at `<config-dir>\bin\aerial-service.ps1` (default `%APPDATA%\aerial\bin\aerial-service.ps1`). The wrapper is regenerated on every `aerial service install`. Run `aerial service install` from an Administrator PowerShell or Command Prompt so Windows allows Aerial to register or refresh the scheduled task. The task itself still runs at user logon with `/RL LIMITED`. The `/TR` argument quotes the wrapper path with normal double quotes so paths that contain spaces or non-ASCII characters work without manual quoting.
|
|
241
243
|
- Linux: not implemented in this release. `aerial service install|start|stop|restart|uninstall` throws an unsupported-platform error and exits 1; `aerial service status --json` still emits a schema-valid document with `"supported": false` and exits 1. Run `aerial start` directly or wrap it in your own init system.
|
|
242
244
|
|
|
243
245
|
Both wrappers do the same three things before exec-ing the proxy: (1) startup-rotate the captured stdio log (`aerial-stdio.log` → `.1` → `.2` → `.3`) if it has grown beyond the configured cap; (2) export `AERIAL_LOG_FILE=<config-dir>/logs/aerial.log`, plus `AERIAL_LOG_MAX_BYTES` and `AERIAL_LOG_BACKUPS` (default `5242880` / `3`, or whatever value was present in the installer's environment — see below), and — when `AERIAL_CONFIG_DIR` was set at install time — re-export `AERIAL_CONFIG_DIR` so the service sees the same config root as the installer; (3) `exec` `node src/cli/index.js start --host <host> --port <port>` with stdout and stderr appended to `aerial-stdio.log`. By default, the service wrapper uses the same Node.js binary that ran `aerial service install`. Set `AERIAL_SERVICE_NODE` before `aerial service install` to override this explicitly, for example when installing from one Node binary but running the service with another. The structured event log is opt-in via `AERIAL_LOG_FILE`: when this env var is set (always set by the wrapper, never by foreground `aerial start`), structured events go to that file only; when unset, they go to stderr only. There is no double-write.
|
|
@@ -252,7 +254,7 @@ Wrapper env values are baked in at install time. If you set `AERIAL_LOG_MAX_BYTE
|
|
|
252
254
|
4. If the port answers as Aerial AND the local service manager does NOT report the unit/task loaded → the definition is regenerated, but the service is NOT started. Exit 1 with `reason=foreground_running`, `definitionUpdated=true`, and an actionable next step: stop the foreground process, then run `aerial service start`. This guarantees `install` never causes two Aerial instances to fight for the port.
|
|
253
255
|
5. Otherwise (port absent) → write the definition, then start the service (`launchctl bootstrap` on macOS, `schtasks /Run` on Windows). Exit 0.
|
|
254
256
|
|
|
255
|
-
`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
|
|
257
|
+
`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 teardown` 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`.
|
|
256
258
|
|
|
257
259
|
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.
|
|
258
260
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jiayunxie/aerial",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
4
4
|
"description": "Local GitHub Copilot proxy for Codex CLI and Claude Code with smart HTTP(S)/SOCKS5 upstream proxy support.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -47,6 +47,6 @@
|
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"smol-toml": "^1.6.1",
|
|
49
49
|
"socks": "^2.8.9",
|
|
50
|
-
"undici": "^7.
|
|
50
|
+
"undici": "^7.28.0"
|
|
51
51
|
}
|
|
52
52
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { defaultConfig, loadConfig, saveConfig } from "../../shared/config.js";
|
|
2
2
|
import { configPath } from "../../shared/paths.js";
|
|
3
3
|
import { assertValidEffort } from "../../shared/effort.js";
|
|
4
|
-
import { parseConfigHost, parseConfigPort } from "../
|
|
4
|
+
import { parseConfigHost, parseConfigPort } from "../helpers.js";
|
|
5
5
|
|
|
6
6
|
export function runConfigCli(subcommand, rest) {
|
|
7
7
|
if (subcommand === "reset") {
|
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
import { loadConfig } from "../../shared/config.js";
|
|
2
2
|
import { configPath } from "../../shared/paths.js";
|
|
3
3
|
import { restoreAllClients, restoreClient, setupClaude, setupCodex, setupStatus } from "../../setup/index.js";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
import {
|
|
5
|
+
assertValidCodexEffort,
|
|
6
|
+
assertValidEffort,
|
|
7
|
+
chooseSetupEffort,
|
|
8
|
+
chooseSetupModel,
|
|
9
|
+
discoverModelsForRoute,
|
|
10
|
+
formatEffortSelection,
|
|
11
|
+
formatModelChoices
|
|
12
|
+
} from "../select.js";
|
|
13
|
+
import { requiredArgValue, claudeApiKeyHelper, codexAuthCommand } from "../helpers.js";
|
|
8
14
|
import { printRestoreResults, printSetupCompletionSummary } from "../output.js";
|
|
9
15
|
|
|
10
16
|
async function selectSetupOptions(target, route, args) {
|
|
11
17
|
const explicitEffort = requiredArgValue(args, "--effort");
|
|
12
|
-
if (explicitEffort !== undefined)
|
|
18
|
+
if (explicitEffort !== undefined) {
|
|
19
|
+
if (target === "Codex") assertValidCodexEffort(explicitEffort);
|
|
20
|
+
else assertValidEffort(explicitEffort);
|
|
21
|
+
}
|
|
13
22
|
const selected = await chooseSetupModel({ target, route, explicitModel: requiredArgValue(args, "--model") });
|
|
14
23
|
if (!selected.displayed) {
|
|
15
24
|
for (const line of formatModelChoices({ target, route, choices: selected.choices, selectedModel: selected.model, source: selected.source, recommended: selected.recommended })) {
|
|
@@ -18,8 +27,26 @@ async function selectSetupOptions(target, route, args) {
|
|
|
18
27
|
} else {
|
|
19
28
|
console.log(`Selected ${target} model: ${selected.model}`);
|
|
20
29
|
}
|
|
21
|
-
|
|
22
|
-
|
|
30
|
+
let selectedModel = selected.choices.find((choice) => choice.id === selected.model);
|
|
31
|
+
if (!selectedModel && selected.source === "explicit") {
|
|
32
|
+
try {
|
|
33
|
+
selectedModel = (await discoverModelsForRoute(route)).find((choice) => choice.id === selected.model);
|
|
34
|
+
} catch {
|
|
35
|
+
selectedModel = undefined;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const effortChoice = await chooseSetupEffort({
|
|
39
|
+
target,
|
|
40
|
+
explicitEffort,
|
|
41
|
+
model: selected.model,
|
|
42
|
+
supportedEfforts: selectedModel?.supportedEfforts
|
|
43
|
+
});
|
|
44
|
+
console.log(formatEffortSelection({
|
|
45
|
+
target,
|
|
46
|
+
effort: effortChoice.effort,
|
|
47
|
+
source: effortChoice.source,
|
|
48
|
+
supportedEfforts: effortChoice.supportedEfforts
|
|
49
|
+
}));
|
|
23
50
|
return {
|
|
24
51
|
model: selected.model,
|
|
25
52
|
effort: effortChoice.effort,
|
|
@@ -45,6 +72,7 @@ function printSetupStatus(status) {
|
|
|
45
72
|
console.log(`${head}${effortText} file=${cs.file}`);
|
|
46
73
|
if (cs.backups.length) console.log(` backups=${cs.backups.length}`);
|
|
47
74
|
if (cs.error) console.log(` error=${cs.error}`);
|
|
75
|
+
if (cs.migration) console.log(` migration=${cs.migration}`);
|
|
48
76
|
}
|
|
49
77
|
}
|
|
50
78
|
|
|
@@ -64,7 +92,10 @@ export async function runSetupCli(subcommand, rest) {
|
|
|
64
92
|
aerialDefaultEffort: config.defaultEffort || "missing",
|
|
65
93
|
backup: result.backup,
|
|
66
94
|
auth: "command-backed local Aerial key",
|
|
67
|
-
notes: [
|
|
95
|
+
notes: [
|
|
96
|
+
"Aerial defaultEffort is the Claude proxy fallback and was left unchanged.",
|
|
97
|
+
"restart Codex if it was already running so it reloads the profile."
|
|
98
|
+
]
|
|
68
99
|
});
|
|
69
100
|
return;
|
|
70
101
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ensureApiKey, loadConfig } from "../../shared/config.js";
|
|
2
2
|
import { startServer } from "../../proxy/server.js";
|
|
3
|
-
import { argValue } from "../
|
|
3
|
+
import { argValue } from "../helpers.js";
|
|
4
4
|
|
|
5
5
|
export function runStartCli(args) {
|
|
6
6
|
const config = loadConfig();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { setupStatus } from "../../setup/index.js";
|
|
2
2
|
import { serviceStatus } from "../../service/index.js";
|
|
3
|
-
import { computeAppStatus } from "../
|
|
3
|
+
import { computeAppStatus } from "../helpers.js";
|
|
4
4
|
import { printServiceSummary, printSetupSummary } from "../output.js";
|
|
5
5
|
|
|
6
6
|
export async function appStatus({ json = false } = {}) {
|
|
@@ -3,11 +3,11 @@ import { serviceUninstall } from "../../service/index.js";
|
|
|
3
3
|
import { printRestoreResults } from "../output.js";
|
|
4
4
|
import { printServiceUninstallResult } from "./service.js";
|
|
5
5
|
|
|
6
|
-
export function
|
|
6
|
+
export function runTeardownCli() {
|
|
7
7
|
const { ok: restoreOk, results } = restoreAllClients();
|
|
8
8
|
printRestoreResults(results);
|
|
9
9
|
if (!restoreOk) {
|
|
10
|
-
console.log("service uninstall: skipped because client restore reported failures; resolve restore errors then rerun `aerial
|
|
10
|
+
console.log("service uninstall: skipped because client restore reported failures; resolve restore errors then rerun `aerial teardown` or `aerial service uninstall`.");
|
|
11
11
|
process.exitCode = 1;
|
|
12
12
|
return;
|
|
13
13
|
}
|
package/src/cli/doctor.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { setupStatus } from "../setup/index.js";
|
|
2
2
|
import { serviceStatus } from "../service/index.js";
|
|
3
|
-
import { computeAppStatus } from "./
|
|
3
|
+
import { computeAppStatus } from "./helpers.js";
|
|
4
4
|
import { readConfigFileStatus } from "../shared/config.js";
|
|
5
5
|
|
|
6
6
|
const REPAIRS = Object.freeze({
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
const PACKAGE_JSON_URL = new URL("../../package.json", import.meta.url);
|
|
6
|
+
const CLI_ENTRY = path.join(path.dirname(fileURLToPath(import.meta.url)), "index.js");
|
|
7
|
+
|
|
8
|
+
export function readPackageVersion(packageJsonUrl = PACKAGE_JSON_URL) {
|
|
9
|
+
try {
|
|
10
|
+
const pkg = JSON.parse(fs.readFileSync(packageJsonUrl, "utf8"));
|
|
11
|
+
if (typeof pkg.version === "string" && pkg.version.length > 0) return pkg.version;
|
|
12
|
+
throw new Error("missing version field");
|
|
13
|
+
} catch (error) {
|
|
14
|
+
const reason = error?.message || String(error);
|
|
15
|
+
console.warn(`aerial: cannot read package version (${reason}); reporting "unknown"`);
|
|
16
|
+
return "unknown";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function printVersion(packageJsonUrl = PACKAGE_JSON_URL) {
|
|
21
|
+
console.log(readPackageVersion(packageJsonUrl));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function argValue(args, name) {
|
|
25
|
+
const index = args.indexOf(name);
|
|
26
|
+
return index >= 0 ? args[index + 1] : undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function requiredArgValue(args, name) {
|
|
30
|
+
const index = args.indexOf(name);
|
|
31
|
+
if (index < 0) return undefined;
|
|
32
|
+
const value = args[index + 1];
|
|
33
|
+
if (value === undefined || value.startsWith("--")) {
|
|
34
|
+
throw new Error(`${name} requires a value.`);
|
|
35
|
+
}
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function parseConfigPort(value) {
|
|
40
|
+
const text = String(value).trim();
|
|
41
|
+
if (!/^\d+$/.test(text)) throw new Error("port must be an integer between 1 and 65535");
|
|
42
|
+
const port = Number(text);
|
|
43
|
+
if (port < 1 || port > 65535) throw new Error("port must be an integer between 1 and 65535");
|
|
44
|
+
return port;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function parseConfigHost(value) {
|
|
48
|
+
const host = String(value).trim().toLowerCase();
|
|
49
|
+
if (host === "127.0.0.1" || host === "localhost" || host === "::1") return host;
|
|
50
|
+
throw new Error("host must be a loopback address: 127.0.0.1, localhost, or ::1");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function codexAuthCommand() {
|
|
54
|
+
return {
|
|
55
|
+
command: process.execPath,
|
|
56
|
+
args: [CLI_ENTRY, "key", "print"],
|
|
57
|
+
timeout_ms: 5000,
|
|
58
|
+
refresh_interval_ms: 0
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function quoteCommandPart(value) {
|
|
63
|
+
return `"${String(value).replace(/"/g, '\\"')}"`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function claudeApiKeyHelper() {
|
|
67
|
+
return [process.execPath, CLI_ENTRY, "key", "print"].map(quoteCommandPart).join(" ");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function computeAppStatus(setup, service) {
|
|
71
|
+
const githubTokenPresent = setup.auth.github_token.source !== "missing";
|
|
72
|
+
const apiKeyPresent = setup.auth.api_key.exists;
|
|
73
|
+
const hasAerialClient = setup.clients.codex.state === "aerial" || setup.clients.claude.state === "aerial";
|
|
74
|
+
const serviceHealthy = service.supported !== false && service.health?.aerial === true;
|
|
75
|
+
const ok = apiKeyPresent && githubTokenPresent && hasAerialClient && serviceHealthy;
|
|
76
|
+
const nextSteps = [];
|
|
77
|
+
const hints = [];
|
|
78
|
+
if (!githubTokenPresent) nextSteps.push("run: aerial login");
|
|
79
|
+
if (!hasAerialClient) nextSteps.push("run: aerial setup codex or aerial setup claude");
|
|
80
|
+
if (hasAerialClient && !apiKeyPresent) nextSteps.push("run: aerial setup codex or aerial setup claude to recreate the local Aerial key");
|
|
81
|
+
if (service.supported !== false && !service.service?.loaded) {
|
|
82
|
+
if (serviceHealthy) {
|
|
83
|
+
hints.push("Aerial is running in the foreground but no background service is installed; run `aerial service install` so it starts on reboot.");
|
|
84
|
+
} else {
|
|
85
|
+
nextSteps.push("run: aerial service install");
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (setup.auth.github_token.source === "env") {
|
|
89
|
+
hints.push("AERIAL_GITHUB_TOKEN is set for this process only; run aerial login without that env var to persist a service-readable login.");
|
|
90
|
+
}
|
|
91
|
+
return { schema: "aerial.status.v1", ok, setup, service, nextSteps, hints };
|
|
92
|
+
}
|
package/src/cli/index.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { printVersion } from "./
|
|
3
|
-
import { printHelp } from "./help.js";
|
|
2
|
+
import { printVersion } from "./helpers.js";
|
|
4
3
|
import { appStatus } from "./commands/status.js";
|
|
5
4
|
import { runConfigCli } from "./commands/config.js";
|
|
6
|
-
import {
|
|
5
|
+
import { runTeardownCli } from "./commands/teardown.js";
|
|
7
6
|
import { runKeyCli } from "./commands/key.js";
|
|
8
7
|
import { runLoginCli } from "./commands/login.js";
|
|
9
8
|
import { runProxyCli } from "./commands/proxy.js";
|
|
@@ -13,6 +12,30 @@ import { runStartCli } from "./commands/start.js";
|
|
|
13
12
|
import { doctor, renderDoctorText } from "./doctor.js";
|
|
14
13
|
import { formatProbeReport, runProbe } from "./probe.js";
|
|
15
14
|
|
|
15
|
+
function printHelp() {
|
|
16
|
+
console.log(`Aerial local Copilot proxy
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
aerial --version
|
|
20
|
+
aerial login
|
|
21
|
+
aerial setup codex [--model <id>] [--effort <minimal|low|medium|high|xhigh|max|ultra>]
|
|
22
|
+
aerial setup claude [--model <id>] [--effort <low|medium|high|xhigh|max>]
|
|
23
|
+
aerial service install
|
|
24
|
+
aerial status [--json]
|
|
25
|
+
aerial proxy status|enable|disable [--json]
|
|
26
|
+
|
|
27
|
+
Diagnostics and rollback:
|
|
28
|
+
aerial setup status [--json]
|
|
29
|
+
aerial setup restore <codex|claude|all> --latest
|
|
30
|
+
aerial service status [--json]
|
|
31
|
+
aerial teardown
|
|
32
|
+
aerial doctor
|
|
33
|
+
aerial probe [--live] [--json]
|
|
34
|
+
|
|
35
|
+
Debug:
|
|
36
|
+
aerial start [--host 127.0.0.1] [--port 18181]`);
|
|
37
|
+
}
|
|
38
|
+
|
|
16
39
|
async function main() {
|
|
17
40
|
const args = process.argv.slice(2);
|
|
18
41
|
const [command, subcommand, ...rest] = args;
|
|
@@ -51,8 +74,8 @@ async function main() {
|
|
|
51
74
|
}
|
|
52
75
|
}
|
|
53
76
|
|
|
54
|
-
if (command === "
|
|
55
|
-
|
|
77
|
+
if (command === "teardown") {
|
|
78
|
+
runTeardownCli();
|
|
56
79
|
return;
|
|
57
80
|
}
|
|
58
81
|
|
|
@@ -86,7 +109,16 @@ async function main() {
|
|
|
86
109
|
process.exitCode = 1;
|
|
87
110
|
}
|
|
88
111
|
|
|
89
|
-
main()
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
112
|
+
main()
|
|
113
|
+
.then(() => {
|
|
114
|
+
// One-shot commands may leave keep-alive sockets (undici pools), a SOCKS5
|
|
115
|
+
// bridge, or stdin open, which keeps the event loop alive and hangs the CLI
|
|
116
|
+
// after the work is done. `start` is the only long-running command, so for
|
|
117
|
+
// everything else exit explicitly once main() resolves.
|
|
118
|
+
const command = process.argv[2];
|
|
119
|
+
if (command !== "start") process.exit(process.exitCode ?? 0);
|
|
120
|
+
})
|
|
121
|
+
.catch((error) => {
|
|
122
|
+
console.error(error.message);
|
|
123
|
+
process.exit(1);
|
|
124
|
+
});
|
package/src/cli/output.js
CHANGED
|
@@ -18,6 +18,7 @@ export function printSetupSummary(status) {
|
|
|
18
18
|
const model = client.model ? ` model=${client.model}` : "";
|
|
19
19
|
const effort = ` effort=${client.effort || "missing"}`;
|
|
20
20
|
console.log(` ${client.target}: ${client.state}${model}${effort}`);
|
|
21
|
+
if (client.migration) console.log(` migration: ${client.migration}`);
|
|
21
22
|
}
|
|
22
23
|
console.log(`api key: ${status.auth.api_key.exists ? "present" : "missing"}`);
|
|
23
24
|
const ghSource = status.auth.github_token.source;
|
package/src/cli/probe.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { proxyChatCompletions, proxyMessages, proxyModels, proxyResponses } from "../proxy/index.js";
|
|
2
|
-
import { readJsonSafely } from "../shared/
|
|
3
|
-
import { aerialRoutes, usageSummary } from "../proxy/
|
|
2
|
+
import { readJsonSafely } from "../shared/utils.js";
|
|
3
|
+
import { aerialRoutes, usageSummary } from "../proxy/models.js";
|
|
4
|
+
import { supportedReasoningEfforts } from "../proxy/model-catalog.js";
|
|
4
5
|
|
|
5
6
|
function modelRoutes(model) {
|
|
6
7
|
return aerialRoutes(model);
|
|
@@ -59,6 +60,7 @@ export async function runProbe({ live = false } = {}) {
|
|
|
59
60
|
models: models.map((model) => ({
|
|
60
61
|
id: model.id,
|
|
61
62
|
routes: modelRoutes(model),
|
|
63
|
+
efforts: supportedReasoningEfforts(model),
|
|
62
64
|
notes: model.aerial?.notes || [],
|
|
63
65
|
supported: Boolean(model.aerial?.supported)
|
|
64
66
|
}))
|
|
@@ -117,8 +119,9 @@ export function formatProbeReport(report) {
|
|
|
117
119
|
lines.push("", "Model matrix:");
|
|
118
120
|
for (const model of report.models) {
|
|
119
121
|
const routes = model.routes.length ? model.routes.join(",") : "-";
|
|
122
|
+
const efforts = model.efforts.length ? ` efforts=${model.efforts.join(",")}` : "";
|
|
120
123
|
const notes = model.notes.length ? ` notes=${model.notes.join(",")}` : "";
|
|
121
|
-
lines.push(`- ${model.id}: routes=${routes}${notes}`);
|
|
124
|
+
lines.push(`- ${model.id}: routes=${routes}${efforts}${notes}`);
|
|
122
125
|
}
|
|
123
126
|
return lines.join("\n");
|
|
124
127
|
}
|