@jiayunxie/aerial 0.2.1 → 0.2.2

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.
Files changed (63) hide show
  1. package/README.md +1 -1
  2. package/docs/usage.md +2 -2
  3. package/package.json +5 -5
  4. package/src/cli/args.js +28 -0
  5. package/src/cli/config-command.js +39 -0
  6. package/src/cli/disable-command.js +28 -0
  7. package/src/{doctor.js → cli/doctor.js} +3 -3
  8. package/src/cli/help.js +23 -0
  9. package/src/cli/index.js +92 -0
  10. package/src/cli/key-command.js +20 -0
  11. package/src/cli/login-command.js +28 -0
  12. package/src/{model-selection.js → cli/model-selection.js} +5 -5
  13. package/src/cli/output.js +75 -0
  14. package/src/{probe.js → cli/probe.js} +3 -3
  15. package/src/cli/proxy-command.js +120 -0
  16. package/src/cli/runtime-auth.js +21 -0
  17. package/src/cli/service-command.js +120 -0
  18. package/src/cli/setup-command.js +122 -0
  19. package/src/{setup-selection.js → cli/setup-selection.js} +3 -20
  20. package/src/cli/start-command.js +11 -0
  21. package/src/cli/status-command.js +26 -0
  22. package/src/{version.js → cli/version.js} +1 -1
  23. package/src/proxy/cache-policy.js +89 -0
  24. package/src/proxy/cache-telemetry.js +172 -0
  25. package/src/proxy/effort.js +120 -0
  26. package/src/proxy/headers.js +33 -0
  27. package/src/proxy/index.js +85 -0
  28. package/src/proxy/models.js +27 -0
  29. package/src/{responses-websocket.js → proxy/responses-websocket.js} +2 -2
  30. package/src/{server.js → proxy/server.js} +5 -5
  31. package/src/proxy/transport.js +55 -0
  32. package/src/service/health.js +54 -0
  33. package/src/service/index.js +38 -0
  34. package/src/service/lifecycle.js +301 -0
  35. package/src/service/platform.js +227 -0
  36. package/src/service/runner.js +45 -0
  37. package/src/service/status.js +296 -0
  38. package/src/service/wrapper-render.js +228 -0
  39. package/src/setup/backup.js +38 -0
  40. package/src/{setup.js → setup/clients.js} +11 -198
  41. package/src/setup/index.js +4 -0
  42. package/src/setup/restore.js +90 -0
  43. package/src/setup/status.js +24 -0
  44. package/src/setup/toml.js +41 -0
  45. package/src/{auth.js → shared/auth.js} +1 -1
  46. package/src/{config.js → shared/config.js} +2 -2
  47. package/src/shared/effort.js +19 -0
  48. package/src/shared/file-utils.js +31 -0
  49. package/src/{paths.js → shared/paths.js} +2 -3
  50. package/src/{upstream-fetch.js → upstream/fetch.js} +1 -1
  51. package/src/cli.js +0 -684
  52. package/src/copilot.js +0 -572
  53. package/src/service.js +0 -1182
  54. /package/src/{app-status.js → cli/app-status.js} +0 -0
  55. /package/src/{model-catalog.js → proxy/model-catalog.js} +0 -0
  56. /package/src/{model-utils.js → proxy/model-utils.js} +0 -0
  57. /package/src/{constants.js → shared/constants.js} +0 -0
  58. /package/src/{crypto.js → shared/crypto.js} +0 -0
  59. /package/src/{http-utils.js → shared/http-utils.js} +0 -0
  60. /package/src/{log.js → shared/log.js} +0 -0
  61. /package/src/{prompt-utils.js → shared/prompt-utils.js} +0 -0
  62. /package/src/{proxy-config.js → upstream/proxy-config.js} +0 -0
  63. /package/src/{socks5-bridge.js → upstream/socks5-bridge.js} +0 -0
package/README.md CHANGED
@@ -11,7 +11,7 @@ It runs locally on `127.0.0.1:18181`. Your GitHub token and Aerial key stay on y
11
11
 
12
12
  Requirements:
13
13
 
14
- - Node.js 24+
14
+ - Node.js 20.18.1+
15
15
  - A GitHub account with an active Copilot subscription
16
16
  - Codex CLI and/or Claude Code installed
17
17
 
package/docs/usage.md CHANGED
@@ -17,7 +17,7 @@ git clone https://github.com/Xiejiayun/aerial.git
17
17
  cd aerial
18
18
  npm install -g .
19
19
  # or run without global install:
20
- node src/cli.js --help
20
+ node src/cli/index.js --help
21
21
  ```
22
22
 
23
23
  ## 2. Configure Local Clients
@@ -240,7 +240,7 @@ Aerial ships a thin platform wrapper around the user-mode service primitives pro
240
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 is wrapped in escaped quotes so paths that contain spaces or non-ASCII characters work without manual quoting.
241
241
  - 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
242
 
243
- 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.js start --host <host> --port <port>` with stdout and stderr appended to `aerial-stdio.log`. The service wrapper prefers a Node.js 24+ binary; on macOS, if the installer is running under an older Node and Codex's bundled Node 24+ exists at `/Applications/Codex.app/Contents/Resources/node`, Aerial uses that for the generated wrapper. Set `AERIAL_SERVICE_NODE` before `aerial service install` to override this selection explicitly. 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.
243
+ 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.
244
244
 
245
245
  Wrapper env values are baked in at install time. If you set `AERIAL_LOG_MAX_BYTES` and/or `AERIAL_LOG_BACKUPS` in the shell before running `aerial service install`, the generated wrapper hard-codes those values and the next start of the service inherits them. Changing the env in another shell after install has no effect on the already-installed wrapper. To apply new values to an already-managed service, rerun `aerial service install` (which always regenerates the definition, including the wrapper) and then `aerial service restart` to swap the running process onto the regenerated wrapper.
246
246
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jiayunxie/aerial",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
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,
@@ -29,16 +29,16 @@
29
29
  "LICENSE"
30
30
  ],
31
31
  "bin": {
32
- "aerial": "src/cli.js"
32
+ "aerial": "src/cli/index.js"
33
33
  },
34
34
  "scripts": {
35
- "start": "node src/cli.js start",
36
- "doctor": "node src/cli.js doctor",
35
+ "start": "node src/cli/index.js start",
36
+ "doctor": "node src/cli/index.js doctor",
37
37
  "test": "node --test",
38
38
  "prepublishOnly": "npm test"
39
39
  },
40
40
  "engines": {
41
- "node": ">=24"
41
+ "node": ">=20.18.1"
42
42
  },
43
43
  "license": "MIT",
44
44
  "publishConfig": {
@@ -0,0 +1,28 @@
1
+ export function argValue(args, name) {
2
+ const index = args.indexOf(name);
3
+ return index >= 0 ? args[index + 1] : undefined;
4
+ }
5
+
6
+ export function requiredArgValue(args, name) {
7
+ const index = args.indexOf(name);
8
+ if (index < 0) return undefined;
9
+ const value = args[index + 1];
10
+ if (value === undefined || value.startsWith("--")) {
11
+ throw new Error(`${name} requires a value.`);
12
+ }
13
+ return value;
14
+ }
15
+
16
+ export function parseConfigPort(value) {
17
+ const text = String(value).trim();
18
+ if (!/^\d+$/.test(text)) throw new Error("port must be an integer between 1 and 65535");
19
+ const port = Number(text);
20
+ if (port < 1 || port > 65535) throw new Error("port must be an integer between 1 and 65535");
21
+ return port;
22
+ }
23
+
24
+ export function parseConfigHost(value) {
25
+ const host = String(value).trim().toLowerCase();
26
+ if (host === "127.0.0.1" || host === "localhost" || host === "::1") return host;
27
+ throw new Error("host must be a loopback address: 127.0.0.1, localhost, or ::1");
28
+ }
@@ -0,0 +1,39 @@
1
+ import { defaultConfig, loadConfig, saveConfig } from "../shared/config.js";
2
+ import { configPath } from "../shared/paths.js";
3
+ import { assertValidEffort } from "../shared/effort.js";
4
+ import { parseConfigHost, parseConfigPort } from "./args.js";
5
+
6
+ export function runConfigCli(subcommand, rest) {
7
+ if (subcommand === "reset") {
8
+ saveConfig(defaultConfig());
9
+ console.log(`Reset Aerial config: ${configPath()}`);
10
+ return;
11
+ }
12
+ const config = loadConfig();
13
+ if (subcommand === "set") {
14
+ const [key, value] = rest;
15
+ if (!key || value === undefined) throw new Error("Usage: aerial config set <key> <value>");
16
+ if (!["host", "port", "defaultModel", "defaultEffort", "logLevel", "promptCacheRetention", "promptCacheKey"].includes(key)) throw new Error(`Unsupported config key: ${key}`);
17
+ if (key === "promptCacheRetention" && !["in_memory", "24h", "off"].includes(value)) throw new Error("promptCacheRetention must be one of: in_memory, 24h, off");
18
+ if (key === "promptCacheKey" && !value.trim()) throw new Error("promptCacheKey must be auto, off, or a non-empty string");
19
+ if (key === "host") {
20
+ config.host = parseConfigHost(value);
21
+ saveConfig(config);
22
+ return;
23
+ }
24
+ if (key === "port") {
25
+ config.port = parseConfigPort(value);
26
+ saveConfig(config);
27
+ return;
28
+ }
29
+ if (key === "defaultEffort") {
30
+ config.defaultEffort = assertValidEffort(value);
31
+ saveConfig(config);
32
+ return;
33
+ }
34
+ config[key] = value;
35
+ saveConfig(config);
36
+ return;
37
+ }
38
+ console.log(JSON.stringify(config, null, 2));
39
+ }
@@ -0,0 +1,28 @@
1
+ import { restoreAllClients } from "../setup/index.js";
2
+ import { serviceUninstall } from "../service/index.js";
3
+ import { printRestoreResults } from "./output.js";
4
+ import { printServiceUninstallResult } from "./service-command.js";
5
+
6
+ export function runDisableCli() {
7
+ const { ok: restoreOk, results } = restoreAllClients();
8
+ printRestoreResults(results);
9
+ if (!restoreOk) {
10
+ console.log("service uninstall: skipped because client restore reported failures; resolve restore errors then rerun `aerial disable` or `aerial service uninstall`.");
11
+ process.exitCode = 1;
12
+ return;
13
+ }
14
+ try {
15
+ const r = serviceUninstall();
16
+ printServiceUninstallResult(r, { prefix: "service uninstall" });
17
+ process.exitCode = r.ok ? 0 : 1;
18
+ } catch (err) {
19
+ if (/unsupported platform/.test(err.message)) {
20
+ console.log(`service uninstall: skipped (${err.message})`);
21
+ process.exitCode = 0;
22
+ } else {
23
+ console.log(`service uninstall: FAILED (${err.message})`);
24
+ console.log(` Retry with: aerial service uninstall`);
25
+ process.exitCode = 1;
26
+ }
27
+ }
28
+ }
@@ -1,7 +1,7 @@
1
- import { setupStatus } from "./setup.js";
2
- import { serviceStatus } from "./service.js";
1
+ import { setupStatus } from "../setup/index.js";
2
+ import { serviceStatus } from "../service/index.js";
3
3
  import { computeAppStatus } from "./app-status.js";
4
- import { readConfigFileStatus } from "./config.js";
4
+ import { readConfigFileStatus } from "../shared/config.js";
5
5
 
6
6
  const REPAIRS = Object.freeze({
7
7
  CONFIG_RESET: Object.freeze({ command: "aerial", args: ["config", "reset"] }),
@@ -0,0 +1,23 @@
1
+ export function printHelp() {
2
+ console.log(`Aerial local Copilot proxy
3
+
4
+ Usage:
5
+ aerial --version
6
+ aerial login
7
+ aerial setup codex [--model <id>] [--effort <low|medium|high|xhigh|max>]
8
+ aerial setup claude [--model <id>] [--effort <low|medium|high|xhigh|max>]
9
+ aerial service install
10
+ aerial status [--json]
11
+ aerial proxy status|enable|disable [--json]
12
+
13
+ Diagnostics and rollback:
14
+ aerial setup status [--json]
15
+ aerial setup restore <codex|claude|all> --latest
16
+ aerial service status [--json]
17
+ aerial disable
18
+ aerial doctor
19
+ aerial probe [--live] [--json]
20
+
21
+ Debug:
22
+ aerial start [--host 127.0.0.1] [--port 18181]`);
23
+ }
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env node
2
+ import { printVersion } from "./version.js";
3
+ import { printHelp } from "./help.js";
4
+ import { appStatus } from "./status-command.js";
5
+ import { runConfigCli } from "./config-command.js";
6
+ import { runDisableCli } from "./disable-command.js";
7
+ import { runKeyCli } from "./key-command.js";
8
+ import { runLoginCli } from "./login-command.js";
9
+ import { runProxyCli } from "./proxy-command.js";
10
+ import { runServiceCli } from "./service-command.js";
11
+ import { runSetupCli } from "./setup-command.js";
12
+ import { runStartCli } from "./start-command.js";
13
+ import { doctor, renderDoctorText } from "./doctor.js";
14
+ import { formatProbeReport, runProbe } from "./probe.js";
15
+
16
+ async function main() {
17
+ const args = process.argv.slice(2);
18
+ const [command, subcommand, ...rest] = args;
19
+ if (!command || command === "--help" || command === "-h") return printHelp();
20
+ if (command === "--version") return printVersion();
21
+
22
+ if (command === "status") {
23
+ const status = await appStatus({ json: args.includes("--json") });
24
+ process.exitCode = status.ok ? 0 : 1;
25
+ return;
26
+ }
27
+
28
+ if (command === "proxy") {
29
+ await runProxyCli(subcommand, rest);
30
+ return;
31
+ }
32
+
33
+ if (command === "login") {
34
+ await runLoginCli(args.slice(1));
35
+ return;
36
+ }
37
+
38
+ if (command === "key") {
39
+ if (runKeyCli(subcommand)) return;
40
+ }
41
+
42
+ if (command === "start") {
43
+ runStartCli(args);
44
+ return;
45
+ }
46
+
47
+ if (command === "setup") {
48
+ if (["codex", "claude", "all", "status", "restore"].includes(subcommand)) {
49
+ await runSetupCli(subcommand, rest);
50
+ return;
51
+ }
52
+ }
53
+
54
+ if (command === "disable") {
55
+ runDisableCli();
56
+ return;
57
+ }
58
+
59
+ if (command === "service") {
60
+ await runServiceCli(subcommand, rest);
61
+ return;
62
+ }
63
+
64
+ if (command === "doctor") {
65
+ const report = await doctor();
66
+ if (args.includes("--json")) console.log(JSON.stringify(report, null, 2));
67
+ else console.log(renderDoctorText(report));
68
+ process.exitCode = report.ok ? 0 : 1;
69
+ return;
70
+ }
71
+
72
+ if (command === "probe") {
73
+ const report = await runProbe({ live: args.includes("--live") });
74
+ if (args.includes("--json")) console.log(JSON.stringify(report, null, 2));
75
+ else console.log(formatProbeReport(report));
76
+ process.exitCode = report.ok ? 0 : 1;
77
+ return;
78
+ }
79
+
80
+ if (command === "config") {
81
+ runConfigCli(subcommand, rest);
82
+ return;
83
+ }
84
+
85
+ printHelp();
86
+ process.exitCode = 1;
87
+ }
88
+
89
+ main().catch((error) => {
90
+ console.error(error.message);
91
+ process.exitCode = 1;
92
+ });
@@ -0,0 +1,20 @@
1
+ import { ensureApiKey } from "../shared/config.js";
2
+
3
+ export function runKeyCli(subcommand) {
4
+ if (subcommand === "generate") {
5
+ const result = ensureApiKey();
6
+ if (result.apiKey) {
7
+ console.log("Local Aerial key generated and stored privately.");
8
+ } else {
9
+ console.log("Aerial API key already configured.");
10
+ }
11
+ return true;
12
+ }
13
+ if (subcommand === "print") {
14
+ const result = ensureApiKey();
15
+ if (result.apiKey) console.log(result.apiKey);
16
+ else throw new Error("Raw API key is not available. Run: aerial key generate");
17
+ return true;
18
+ }
19
+ return false;
20
+ }
@@ -0,0 +1,28 @@
1
+ import { gitHubTokenSource, pollDeviceFlow, startDeviceFlow } from "../shared/auth.js";
2
+
3
+ export async function runLoginCli(args) {
4
+ const force = args.includes("--force");
5
+ const source = gitHubTokenSource();
6
+ if (force && source === "env") {
7
+ console.error("AERIAL_GITHUB_TOKEN is set; unset it before running `aerial login --force`, otherwise the env value will continue to shadow any new file token.");
8
+ process.exit(1);
9
+ }
10
+ if (!force && source === "env") {
11
+ console.log("GitHub login is provided by AERIAL_GITHUB_TOKEN (not verified). To use a different account, unset it or run with a different environment.");
12
+ return;
13
+ }
14
+ if (!force && source === "file") {
15
+ console.log("GitHub login already exists (not verified). To sign in again, run aerial login --force.");
16
+ return;
17
+ }
18
+ if (process.env.AERIAL_TEST_LOGIN_NO_NETWORK === "1") {
19
+ console.log("AERIAL_TEST_LOGIN_NO_NETWORK=1 set; skipping GitHub device flow (test mode).");
20
+ return;
21
+ }
22
+ const flow = await startDeviceFlow();
23
+ console.log(`Open: ${flow.verification_uri}`);
24
+ console.log(`Code: ${flow.user_code}`);
25
+ console.log("Waiting for GitHub authorization...");
26
+ await pollDeviceFlow(flow.device_code, flow.interval);
27
+ console.log("GitHub login saved.");
28
+ }
@@ -1,15 +1,15 @@
1
1
  import { createInterface } from "node:readline/promises";
2
2
  import { stdin as input, stdout as output } from "node:process";
3
- import { proxyModels } from "./copilot.js";
4
- import { readJsonSafely } from "./http-utils.js";
5
- import { modelsForRoute } from "./model-utils.js";
6
- import { parseNumberChoice } from "./prompt-utils.js";
3
+ import { proxyModels } from "../proxy/index.js";
4
+ import { readJsonSafely } from "../shared/http-utils.js";
5
+ import { modelsForRoute } from "../proxy/model-utils.js";
6
+ import { parseNumberChoice } from "../shared/prompt-utils.js";
7
7
 
8
8
  const MAX_LISTED_MODELS = 20;
9
9
  const GPT_VERSION_RE = /^gpt-(\d+)(?:\.(\d+))?/i;
10
10
  const STABLE_GPT_RE = /^gpt-\d+(?:\.\d+)?$/i;
11
11
 
12
- export { modelsForRoute } from "./model-utils.js";
12
+ export { modelsForRoute } from "../proxy/model-utils.js";
13
13
 
14
14
  function gptVersionScore(id) {
15
15
  const match = GPT_VERSION_RE.exec(id);
@@ -0,0 +1,75 @@
1
+ export function printSetupCompletionSummary({ heading, cli, model, effort, proxy, configFile, aerialConfigFile, aerialDefaultEffort, backup, auth, notes = [] }) {
2
+ console.log(heading);
3
+ console.log(` cli: ${cli}`);
4
+ console.log(` model: ${model}`);
5
+ console.log(` effort: ${effort}`);
6
+ console.log(` proxy: ${proxy}`);
7
+ console.log(` client config: ${configFile}`);
8
+ console.log(` aerial config: ${aerialConfigFile}`);
9
+ console.log(` aerial defaultEffort: ${aerialDefaultEffort}`);
10
+ console.log(` backup: ${backup || "none"}`);
11
+ console.log(` auth: ${auth}`);
12
+ for (const note of notes) console.log(` note: ${note}`);
13
+ }
14
+
15
+ export function printSetupSummary(status) {
16
+ console.log("clients:");
17
+ for (const client of Object.values(status.clients)) {
18
+ const model = client.model ? ` model=${client.model}` : "";
19
+ const effort = ` effort=${client.effort || "missing"}`;
20
+ console.log(` ${client.target}: ${client.state}${model}${effort}`);
21
+ }
22
+ console.log(`api key: ${status.auth.api_key.exists ? "present" : "missing"}`);
23
+ const ghSource = status.auth.github_token.source;
24
+ const ghText = ghSource === "missing" ? "missing" : `present (${ghSource})`;
25
+ console.log(`github login: ${ghText}`);
26
+ }
27
+
28
+ export function printServiceSummary(status) {
29
+ console.log(`service: ${status.summary}`);
30
+ if (status.supported === false) {
31
+ console.log(`platform: ${status.platform} (service unsupported)`);
32
+ return;
33
+ }
34
+ const health = status.health?.aerial ? `ok (${status.health.supervisor})`
35
+ : status.health?.portConflict ? `port conflict (${status.health.conflictReason})`
36
+ : status.health?.ok ? "ok"
37
+ : `unreachable (${status.health?.error || `http ${status.health?.status}`})`;
38
+ console.log(`health: ${health}`);
39
+ }
40
+
41
+ export function printRestoreResults(results) {
42
+ for (const r of Object.values(results)) {
43
+ if (r.restored) {
44
+ console.log(`Restored ${r.target}: ${r.file} <- ${r.from}`);
45
+ if (r.snapshot) console.log(` pre-restore snapshot: ${r.snapshot}`);
46
+ } else if (r.reason === "no_backup") {
47
+ console.log(`Restored ${r.target}: no backup to restore`);
48
+ } else if (r.error) {
49
+ console.log(`Restored ${r.target}: FAILED ${r.error}`);
50
+ }
51
+ }
52
+ }
53
+
54
+ export function printServiceDiagnostics(diagnostics) {
55
+ if (!diagnostics) return;
56
+ if (diagnostics.stdioLog) console.log(` stdio log: ${diagnostics.stdioLog}`);
57
+ if (diagnostics.aerialLog) console.log(` aerial log: ${diagnostics.aerialLog}`);
58
+ if (diagnostics.wrapperNode) console.log(` wrapper node: ${diagnostics.wrapperNode}`);
59
+ if (diagnostics.health) {
60
+ const h = diagnostics.health;
61
+ const tail = h.lastError ? `, last error: ${h.lastError}` : (h.lastStatus !== undefined ? `, last status: ${h.lastStatus}` : "");
62
+ console.log(` health probe: ${h.attempts} attempts over ${h.elapsedMs}ms${tail}`);
63
+ }
64
+ console.log(" Run: aerial service status --json");
65
+ }
66
+
67
+ export function printServiceWarning(result) {
68
+ if (result.warning) console.log(` WARNING: ${result.warning.message}`);
69
+ }
70
+
71
+ export function formatEgress(egress) {
72
+ if (!egress?.ok) return `unavailable (${egress?.error || "unknown"})`;
73
+ const place = [egress.city, egress.region, egress.country].filter(Boolean).join(", ");
74
+ return [egress.ip, place].filter(Boolean).join(" ");
75
+ }
@@ -1,6 +1,6 @@
1
- import { proxyChatCompletions, proxyMessages, proxyModels, proxyResponses } from "./copilot.js";
2
- import { readJsonSafely } from "./http-utils.js";
3
- import { aerialRoutes, usageSummary } from "./model-utils.js";
1
+ import { proxyChatCompletions, proxyMessages, proxyModels, proxyResponses } from "../proxy/index.js";
2
+ import { readJsonSafely } from "../shared/http-utils.js";
3
+ import { aerialRoutes, usageSummary } from "../proxy/model-utils.js";
4
4
 
5
5
  function modelRoutes(model) {
6
6
  return aerialRoutes(model);
@@ -0,0 +1,120 @@
1
+ import { loadConfig } from "../shared/config.js";
2
+ import { disableUpstreamProxy, enableUpstreamProxy, probeEgress, upstreamProxyState } from "../upstream/fetch.js";
3
+ import { redactProxyEndpoint, redactProxySource } from "../upstream/proxy-config.js";
4
+ import { runProbe } from "./probe.js";
5
+ import { formatEgress } from "./output.js";
6
+
7
+ async function proxyRouteSummary() {
8
+ try {
9
+ const report = await runProbe();
10
+ if (!report.ok) return { ok: false, error: JSON.stringify(report.error) };
11
+ return {
12
+ ok: true,
13
+ models: report.models.length,
14
+ responses: report.summary.responses,
15
+ responsesWebSocket: report.summary.websocketResponses,
16
+ messages: report.summary.messages,
17
+ chat: report.summary.chat
18
+ };
19
+ } catch (err) {
20
+ return { ok: false, error: err.message };
21
+ }
22
+ }
23
+
24
+ async function proxyStatus({ includeRoutes = true } = {}) {
25
+ const state = upstreamProxyState(loadConfig());
26
+ const egress = await probeEgress({ endpoint: state.endpoint });
27
+ return {
28
+ schema: "aerial.proxy-status.v1",
29
+ mode: state.mode,
30
+ enabled: state.enabled,
31
+ endpoint: state.endpoint,
32
+ source: state.source,
33
+ egress,
34
+ routes: includeRoutes ? await proxyRouteSummary() : undefined
35
+ };
36
+ }
37
+
38
+ function publicProxyCandidate(candidate) {
39
+ if (!candidate || typeof candidate !== "object") return candidate;
40
+ return {
41
+ ...candidate,
42
+ endpoint: redactProxyEndpoint(candidate.endpoint),
43
+ source: redactProxySource(candidate.source)
44
+ };
45
+ }
46
+
47
+ function publicProxyStatus(status) {
48
+ if (!status || typeof status !== "object") return status;
49
+ return {
50
+ ...status,
51
+ endpoint: redactProxyEndpoint(status.endpoint),
52
+ source: redactProxySource(status.source)
53
+ };
54
+ }
55
+
56
+ function publicProxyEnableResult(result) {
57
+ if (!result || typeof result !== "object") return result;
58
+ return {
59
+ ...result,
60
+ selected: publicProxyCandidate(result.selected),
61
+ candidates: Array.isArray(result.candidates) ? result.candidates.map(publicProxyCandidate) : result.candidates,
62
+ status: publicProxyStatus(result.status)
63
+ };
64
+ }
65
+
66
+ function printProxyStatus(status) {
67
+ const safeStatus = publicProxyStatus(status);
68
+ console.log(`proxy: ${status.enabled ? `enabled (${status.mode})` : "disabled"}`);
69
+ console.log(`endpoint: ${status.enabled ? `${safeStatus.endpoint} (${safeStatus.source})` : "direct"}`);
70
+ console.log(`egress: ${formatEgress(status.egress)}`);
71
+ if (status.routes) {
72
+ if (status.routes.ok) {
73
+ console.log(`copilot routes: responses=${status.routes.responses}, responsesWebSocket=${status.routes.responsesWebSocket}, messages=${status.routes.messages}, chat=${status.routes.chat}`);
74
+ } else {
75
+ console.log(`copilot routes: unavailable (${status.routes.error})`);
76
+ }
77
+ }
78
+ }
79
+
80
+ export async function runProxyCli(subcommand, rest) {
81
+ const json = rest.includes("--json") || subcommand === "--json";
82
+ if (!subcommand || subcommand === "status" || subcommand === "--json") {
83
+ const status = await proxyStatus();
84
+ if (json) console.log(JSON.stringify(publicProxyStatus(status), null, 2));
85
+ else printProxyStatus(status);
86
+ return;
87
+ }
88
+ if (subcommand === "enable") {
89
+ const result = await enableUpstreamProxy();
90
+ if (!result.ok) {
91
+ if (json) console.log(JSON.stringify(publicProxyEnableResult(result), null, 2));
92
+ else {
93
+ console.error(`Proxy enable failed: ${result.error}`);
94
+ if (result.candidates?.length) {
95
+ console.error(`Checked: ${result.candidates.map((c) => redactProxyEndpoint(c.endpoint)).join(", ")}`);
96
+ }
97
+ }
98
+ process.exitCode = 1;
99
+ return;
100
+ }
101
+ const status = await proxyStatus();
102
+ if (json) console.log(JSON.stringify(publicProxyEnableResult({ ...result, status }), null, 2));
103
+ else {
104
+ console.log(`Proxy enabled: ${redactProxyEndpoint(result.selected.endpoint)} (${redactProxySource(result.selected.source)})`);
105
+ printProxyStatus(status);
106
+ }
107
+ return;
108
+ }
109
+ if (subcommand === "disable") {
110
+ disableUpstreamProxy();
111
+ const status = await proxyStatus();
112
+ if (json) console.log(JSON.stringify(publicProxyStatus(status), null, 2));
113
+ else {
114
+ console.log("Proxy disabled.");
115
+ printProxyStatus(status);
116
+ }
117
+ return;
118
+ }
119
+ throw new Error("Usage: aerial proxy status|enable|disable [--json]");
120
+ }
@@ -0,0 +1,21 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+
4
+ const CLI_ENTRY = path.join(path.dirname(fileURLToPath(import.meta.url)), "index.js");
5
+
6
+ export function codexAuthCommand() {
7
+ return {
8
+ command: process.execPath,
9
+ args: [CLI_ENTRY, "key", "print"],
10
+ timeout_ms: 5000,
11
+ refresh_interval_ms: 0
12
+ };
13
+ }
14
+
15
+ function quoteCommandPart(value) {
16
+ return `"${String(value).replace(/"/g, '\\"')}"`;
17
+ }
18
+
19
+ export function claudeApiKeyHelper() {
20
+ return [process.execPath, CLI_ENTRY, "key", "print"].map(quoteCommandPart).join(" ");
21
+ }