@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 +8 -3
- package/docs/usage.md +16 -2
- package/package.json +1 -1
- package/src/cli.js +16 -6
- package/src/setup.js +30 -30
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
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
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
|
-
|
|
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
|
-
|
|
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} =
|
|
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} =
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
104
|
-
return { file, backup, model: selectedModel,
|
|
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 ===
|
|
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";
|