@jiayunxie/aerial 0.1.3 → 0.1.4

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
@@ -91,7 +91,7 @@ Configure your local clients. This also creates Aerial's local API key and wires
91
91
  aerial setup all --model <available-copilot-model-id>
92
92
  ```
93
93
 
94
- On Windows, newly persisted user environment variables are visible to new terminals and newly launched apps. Restart your terminal or VS Code after the first setup if Codex was already open.
94
+ If Codex or Claude Code was already open, restart it after setup so it rereads the updated client config.
95
95
 
96
96
  Log in to GitHub with device flow:
97
97
 
@@ -125,10 +125,15 @@ This updates `~/.codex/config.toml` and creates a timestamped backup first. If y
125
125
  [model_providers.aerial]
126
126
  base_url = "http://127.0.0.1:18181/v1"
127
127
  wire_api = "responses"
128
- env_key = "AERIAL_API_KEY"
128
+
129
+ [model_providers.aerial.auth]
130
+ command = "<node>"
131
+ args = ["<aerial-cli.js>", "key", "print"]
132
+ timeout_ms = 5000
133
+ refresh_interval_ms = 0
129
134
  ```
130
135
 
131
- Aerial creates the key automatically and persists `AERIAL_API_KEY` for new user sessions when the platform supports it.
136
+ 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`.
132
137
 
133
138
  ## Claude Code Setup
134
139
 
package/docs/usage.md CHANGED
@@ -26,7 +26,7 @@ node src/cli.js --help
26
26
  aerial setup all --model <model-id>
27
27
  ```
28
28
 
29
- Aerial creates a local API key, stores it privately, and configures supported clients to use it. On Windows, restart the terminal or VS Code after first setup so newly persisted user environment variables are visible to Codex.
29
+ Aerial creates a local API key, stores it privately, and configures supported clients to use it. If Codex or Claude Code was already open, restart it after setup so it rereads the updated client config.
30
30
 
31
31
  ## 3. Login To GitHub
32
32
 
@@ -50,7 +50,21 @@ Default URL: `http://127.0.0.1:18181`.
50
50
  aerial setup codex --model <model-id>
51
51
  ```
52
52
 
53
- The setup command backs up and merges `~/.codex/config.toml`, then persists the local key for new user sessions when the platform supports it.
53
+ 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:
54
+
55
+ ```toml
56
+ [model_providers.aerial]
57
+ base_url = "http://127.0.0.1:18181/v1"
58
+ wire_api = "responses"
59
+
60
+ [model_providers.aerial.auth]
61
+ command = "<node>"
62
+ args = ["<aerial-cli.js>", "key", "print"]
63
+ timeout_ms = 5000
64
+ refresh_interval_ms = 0
65
+ ```
66
+
67
+ The local key is generated and stored by Aerial automatically. Users do not need to run `aerial key generate`, copy a key into `~/.codex/config.toml`, or export `AERIAL_API_KEY`.
54
68
 
55
69
  For a dry inspection without touching your real config, set `HOME`/`USERPROFILE` to a temporary directory before running this command.
56
70
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jiayunxie/aerial",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Local-only GitHub Copilot proxy for Codex CLI and Claude Code.",
5
5
  "type": "module",
6
6
  "private": false,
package/src/cli.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { fileURLToPath } from "node:url";
2
3
  import { startDeviceFlow, pollDeviceFlow } from "./auth.js";
3
4
  import { ensureApiKey, loadConfig, saveConfig } from "./config.js";
4
5
  import { startServer } from "./server.js";
@@ -8,6 +9,17 @@ import { doctor } from "./doctor.js";
8
9
  import { runProbe, formatProbeReport } from "./probe.js";
9
10
  import { printVersion } from "./version.js";
10
11
 
12
+ const CLI_ENTRY = fileURLToPath(import.meta.url);
13
+
14
+ function codexAuthCommand() {
15
+ return {
16
+ command: process.execPath,
17
+ args: [CLI_ENTRY, "key", "print"],
18
+ timeout_ms: 5000,
19
+ refresh_interval_ms: 0
20
+ };
21
+ }
22
+
11
23
  function printHelp() {
12
24
  console.log(`Aerial local Copilot proxy
13
25
 
@@ -91,11 +103,10 @@ async function main() {
91
103
 
92
104
  if (command === "setup") {
93
105
  if (subcommand === "codex") {
94
- const result = setupCodex({ model: argValue(rest, "--model") });
106
+ const result = setupCodex({ model: argValue(rest, "--model"), authCommand: codexAuthCommand() });
95
107
  console.log(`Updated Codex config: ${result.file}`);
96
108
  if (result.backup) console.log(`Backup: ${result.backup}`);
97
- if (result.env?.persisted) console.log(`Configured ${result.env.name} for new user sessions.`);
98
- else if (result.env?.reason) console.log(`Note: ${result.env.name} is available in this process, but was not persisted (${result.env.reason}).`);
109
+ console.log("Configured Codex to read the local Aerial key automatically.");
99
110
  return;
100
111
  }
101
112
  if (subcommand === "claude") {
@@ -107,11 +118,10 @@ async function main() {
107
118
  }
108
119
  if (subcommand === "all") {
109
120
  const model = argValue(rest, "--model");
110
- const codex = setupCodex({ model });
121
+ const codex = setupCodex({ model, authCommand: codexAuthCommand() });
111
122
  const claude = setupClaude({ model });
112
123
  console.log(`Updated Codex config: ${codex.file}`);
113
- if (codex.env?.persisted) console.log(`Configured ${codex.env.name} for new user sessions.`);
114
- else if (codex.env?.reason) console.log(`Note: ${codex.env.name} is available in this process, but was not persisted (${codex.env.reason}).`);
124
+ console.log("Configured Codex to read the local Aerial key automatically.");
115
125
  console.log(`Updated Claude settings: ${claude.file}`);
116
126
  if (claude.model) console.log(`Configured Claude default model: ${claude.model}`);
117
127
  return;
package/src/setup.js CHANGED
@@ -1,16 +1,20 @@
1
1
  import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
- import { spawnSync } from "node:child_process";
5
4
  import { parse as parseToml } from "smol-toml";
6
5
  import { ensureApiKey, loadConfig } from "./config.js";
7
6
  import { apiKeyPath, githubTokenPath } from "./paths.js";
8
7
  import { logEvent } from "./log.js";
9
8
 
10
- const AERIAL_ENV_KEY = "AERIAL_API_KEY";
11
9
  const BACKUP_PREFIX = ".aerial-backup-";
12
10
  const PRE_RESTORE_PREFIX = ".aerial-pre-restore-";
13
11
  const ISO_STAMP_RE = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z$/;
12
+ const DEFAULT_CODEX_AUTH = Object.freeze({
13
+ command: "aerial",
14
+ args: ["key", "print"],
15
+ timeout_ms: 5000,
16
+ refresh_interval_ms: 0
17
+ });
14
18
 
15
19
  function backupIfExists(file) {
16
20
  if (!fs.existsSync(file)) return undefined;
@@ -23,15 +27,21 @@ function ensureParent(file) {
23
27
  fs.mkdirSync(path.dirname(file), { recursive: true });
24
28
  }
25
29
 
30
+ function tomlValue(value) {
31
+ if (Array.isArray(value)) return `[${value.map(tomlValue).join(", ")}]`;
32
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
33
+ return JSON.stringify(String(value));
34
+ }
35
+
26
36
  function setTomlString(content, key, value) {
27
- const line = `${key} = "${value}"`;
37
+ const line = `${key} = ${tomlValue(value)}`;
28
38
  const re = new RegExp(`^${key}\\s*=.*$`, "m");
29
39
  return re.test(content) ? content.replace(re, line) : `${content.trimEnd()}\n${line}\n`;
30
40
  }
31
41
 
32
42
  function upsertTomlSection(content, section, values) {
33
43
  const heading = `[${section}]`;
34
- const lines = Object.entries(values).map(([key, value]) => `${key} = "${value}"`).join("\n");
44
+ const lines = Object.entries(values).map(([key, value]) => `${key} = ${tomlValue(value)}`).join("\n");
35
45
  const block = `${heading}\n${lines}\n`;
36
46
  const source = content.split(/\r?\n/);
37
47
  const start = source.findIndex((line) => line.trim() === heading);
@@ -47,25 +57,6 @@ function upsertTomlSection(content, section, values) {
47
57
  return `${source.join("\n").trimEnd()}\n`;
48
58
  }
49
59
 
50
- function persistUserEnv(name, value) {
51
- process.env[name] = value;
52
- if (process.env.AERIAL_SKIP_ENV_PERSIST === "1") return { persisted: false, reason: "skipped" };
53
- if (process.platform === "win32") {
54
- const escaped = String(value).replace(/'/g, "''");
55
- const command = `[Environment]::SetEnvironmentVariable('${name}', '${escaped}', 'User')`;
56
- const { status, error } = spawnSync("powershell.exe", ["-NoProfile", "-Command", command], { stdio: "ignore" });
57
- if (status === 0) return { persisted: true, target: "user" };
58
- return { persisted: false, reason: error?.message || `powershell exited ${status}` };
59
- }
60
- return { persisted: false, reason: "unsupported_platform" };
61
- }
62
-
63
- function ensureClientApiKeyEnv() {
64
- const result = ensureApiKey();
65
- if (!result.apiKey) return { ...result, env: { name: AERIAL_ENV_KEY, persisted: false, reason: "raw_key_unavailable" } };
66
- return { ...result, env: { name: AERIAL_ENV_KEY, ...persistUserEnv(AERIAL_ENV_KEY, result.apiKey) } };
67
- }
68
-
69
60
  function claudeEnvForAerial(currentEnv, config) {
70
61
  const {
71
62
  ANTHROPIC_API_KEY,
@@ -82,8 +73,8 @@ function claudeEnvForAerial(currentEnv, config) {
82
73
  };
83
74
  }
84
75
 
85
- export function setupCodex({ model } = {}) {
86
- const apiKey = ensureClientApiKeyEnv();
76
+ export function setupCodex({ model, authCommand = DEFAULT_CODEX_AUTH } = {}) {
77
+ ensureApiKey();
87
78
  const config = loadConfig();
88
79
  const selectedModel = model || config.defaultModel || "gpt-4.1";
89
80
  const file = path.join(os.homedir(), ".codex", "config.toml");
@@ -95,13 +86,18 @@ export function setupCodex({ model } = {}) {
95
86
  content = upsertTomlSection(content, "model_providers.aerial", {
96
87
  name: "Aerial Copilot Local",
97
88
  base_url: `http://${config.host}:${config.port}/v1`,
98
- wire_api: "responses",
99
- env_key: AERIAL_ENV_KEY
89
+ wire_api: "responses"
90
+ });
91
+ content = upsertTomlSection(content, "model_providers.aerial.auth", {
92
+ command: authCommand.command,
93
+ args: authCommand.args || [],
94
+ timeout_ms: authCommand.timeout_ms || DEFAULT_CODEX_AUTH.timeout_ms,
95
+ refresh_interval_ms: authCommand.refresh_interval_ms ?? DEFAULT_CODEX_AUTH.refresh_interval_ms
100
96
  });
101
97
  content = upsertTomlSection(content, "profiles.aerial", { model_provider: "aerial", model: selectedModel });
102
98
  fs.writeFileSync(file, content, "utf8");
103
- logEvent("setup_write", { target: "codex", file, backup, env: apiKey.env });
104
- return { file, backup, model: selectedModel, env: apiKey.env };
99
+ logEvent("setup_write", { target: "codex", file, backup, auth: "command" });
100
+ return { file, backup, model: selectedModel, auth: { type: "command", command: authCommand.command, args: authCommand.args || [] } };
105
101
  }
106
102
 
107
103
  export function setupClaude({ model } = {}) {
@@ -161,9 +157,13 @@ function backupPathsFor(file) {
161
157
  function codexStateFromDoc(doc, expectedBaseUrl) {
162
158
  const providerSection = doc && typeof doc === "object" ? doc.model_providers?.aerial : undefined;
163
159
  const providerSet = doc?.model_provider === "aerial";
160
+ const authArgs = Array.isArray(providerSection?.auth?.args) ? providerSection.auth.args : [];
161
+ const authLooksAerial = typeof providerSection?.auth?.command === "string"
162
+ && providerSection.auth.command.trim()
163
+ && authArgs.slice(-2).join(" ") === "key print";
164
164
  const providerShapeComplete = providerSection
165
165
  && providerSection.wire_api === "responses"
166
- && providerSection.env_key === AERIAL_ENV_KEY
166
+ && (authLooksAerial || providerSection.env_key === "AERIAL_API_KEY")
167
167
  && typeof providerSection.base_url === "string";
168
168
  const providerBaseMatches = providerSection?.base_url === expectedBaseUrl;
169
169
  if (!providerSection && !providerSet) return "not-aerial";