@jiayunxie/aerial 0.1.0

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/src/cli.js ADDED
@@ -0,0 +1,151 @@
1
+ #!/usr/bin/env node
2
+ import { startDeviceFlow, pollDeviceFlow } from "./auth.js";
3
+ import { ensureApiKey, loadConfig, saveConfig } from "./config.js";
4
+ import { startServer } from "./server.js";
5
+ import { setupClaude, setupCodex } from "./setup.js";
6
+ import { doctor } from "./doctor.js";
7
+ import { runProbe, formatProbeReport } from "./probe.js";
8
+ import { printVersion } from "./version.js";
9
+
10
+ function printHelp() {
11
+ console.log(`Aerial local Copilot proxy
12
+
13
+ Usage:
14
+ aerial --version
15
+ aerial login
16
+ aerial key generate
17
+ aerial key print
18
+ aerial start [--host 127.0.0.1] [--port 18181]
19
+ aerial setup codex [--model <id>]
20
+ aerial setup claude [--model <id>]
21
+ aerial setup all [--model <id>]
22
+ aerial doctor
23
+ aerial probe [--live] [--json]
24
+
25
+ MVP routes:
26
+ GET /health
27
+ GET /v1/models
28
+ POST /v1/responses
29
+ POST /v1/messages
30
+ POST /v1/messages/count_tokens
31
+ POST /v1/chat/completions`);
32
+ }
33
+
34
+ function argValue(args, name) {
35
+ const index = args.indexOf(name);
36
+ return index >= 0 ? args[index + 1] : undefined;
37
+ }
38
+
39
+ async function main() {
40
+ const args = process.argv.slice(2);
41
+ const [command, subcommand, ...rest] = args;
42
+ if (!command || command === "--help" || command === "-h") return printHelp();
43
+ if (command === "--version") return printVersion();
44
+
45
+ if (command === "login") {
46
+ const flow = await startDeviceFlow();
47
+ console.log(`Open: ${flow.verification_uri}`);
48
+ console.log(`Code: ${flow.user_code}`);
49
+ console.log("Waiting for GitHub authorization...");
50
+ await pollDeviceFlow(flow.device_code, flow.interval);
51
+ console.log("GitHub login saved.");
52
+ return;
53
+ }
54
+
55
+ if (command === "key") {
56
+ if (subcommand === "generate") {
57
+ const result = ensureApiKey();
58
+ if (result.apiKey) {
59
+ console.log("Local Aerial key generated and stored privately.");
60
+ } else {
61
+ console.log("Aerial API key already configured.");
62
+ }
63
+ return;
64
+ }
65
+ if (subcommand === "print") {
66
+ const result = ensureApiKey();
67
+ if (result.apiKey) console.log(result.apiKey);
68
+ else throw new Error("Raw API key is not available. Run: aerial key generate");
69
+ return;
70
+ }
71
+ }
72
+
73
+ if (command === "start") {
74
+ const config = loadConfig();
75
+ const host = argValue(args, "--host") || config.host;
76
+ const port = Number(argValue(args, "--port") || config.port);
77
+ ensureApiKey();
78
+ startServer({ host, port });
79
+ return;
80
+ }
81
+
82
+ if (command === "setup") {
83
+ if (subcommand === "codex") {
84
+ const result = setupCodex({ model: argValue(rest, "--model") });
85
+ console.log(`Updated Codex config: ${result.file}`);
86
+ if (result.backup) console.log(`Backup: ${result.backup}`);
87
+ if (result.env?.persisted) console.log(`Configured ${result.env.name} for new user sessions.`);
88
+ else if (result.env?.reason) console.log(`Note: ${result.env.name} is available in this process, but was not persisted (${result.env.reason}).`);
89
+ return;
90
+ }
91
+ if (subcommand === "claude") {
92
+ const result = setupClaude({ model: argValue(rest, "--model") });
93
+ console.log(`Updated Claude settings: ${result.file}`);
94
+ if (result.model) console.log(`Configured Claude default model: ${result.model}`);
95
+ if (result.backup) console.log(`Backup: ${result.backup}`);
96
+ return;
97
+ }
98
+ if (subcommand === "all") {
99
+ const model = argValue(rest, "--model");
100
+ const codex = setupCodex({ model });
101
+ const claude = setupClaude({ model });
102
+ console.log(`Updated Codex config: ${codex.file}`);
103
+ if (codex.env?.persisted) console.log(`Configured ${codex.env.name} for new user sessions.`);
104
+ else if (codex.env?.reason) console.log(`Note: ${codex.env.name} is available in this process, but was not persisted (${codex.env.reason}).`);
105
+ console.log(`Updated Claude settings: ${claude.file}`);
106
+ if (claude.model) console.log(`Configured Claude default model: ${claude.model}`);
107
+ return;
108
+ }
109
+ }
110
+
111
+ if (command === "doctor") {
112
+ const result = doctor();
113
+ for (const check of result.checks) {
114
+ console.log(`${check.ok ? "OK" : "FAIL"} ${check.name}: ${check.detail}`);
115
+ }
116
+ process.exitCode = result.ok ? 0 : 1;
117
+ return;
118
+ }
119
+
120
+ if (command === "probe") {
121
+ const report = await runProbe({ live: args.includes("--live") });
122
+ if (args.includes("--json")) console.log(JSON.stringify(report, null, 2));
123
+ else console.log(formatProbeReport(report));
124
+ process.exitCode = report.ok ? 0 : 1;
125
+ return;
126
+ }
127
+
128
+ if (command === "config") {
129
+ const config = loadConfig();
130
+ if (subcommand === "set") {
131
+ const [key, value] = rest;
132
+ if (!key || value === undefined) throw new Error("Usage: aerial config set <key> <value>");
133
+ if (!["host", "port", "defaultModel", "logLevel", "promptCacheRetention", "promptCacheKey"].includes(key)) throw new Error(`Unsupported config key: ${key}`);
134
+ if (key === "promptCacheRetention" && !["in_memory", "24h", "off"].includes(value)) throw new Error("promptCacheRetention must be one of: in_memory, 24h, off");
135
+ if (key === "promptCacheKey" && !value.trim()) throw new Error("promptCacheKey must be auto, off, or a non-empty string");
136
+ config[key] = key === "port" ? Number(value) : value;
137
+ saveConfig(config);
138
+ return;
139
+ }
140
+ console.log(JSON.stringify(config, null, 2));
141
+ return;
142
+ }
143
+
144
+ printHelp();
145
+ process.exitCode = 1;
146
+ }
147
+
148
+ main().catch((error) => {
149
+ console.error(error.message);
150
+ process.exitCode = 1;
151
+ });
package/src/config.js ADDED
@@ -0,0 +1,81 @@
1
+ import fs from "node:fs";
2
+ import { CONFIG_VERSION, DEFAULT_HOST, DEFAULT_PORT, DEFAULT_VERSIONS } from "./constants.js";
3
+ import { apiKeyPath, configPath, readJsonIfExists, writeJsonPrivate, writePrivateFile } from "./paths.js";
4
+ import { hashApiKey, randomApiKey, verifyApiKey } from "./crypto.js";
5
+
6
+ export function defaultConfig() {
7
+ return {
8
+ version: CONFIG_VERSION,
9
+ host: DEFAULT_HOST,
10
+ port: DEFAULT_PORT,
11
+ apiKeyHash: undefined,
12
+ defaultModel: undefined,
13
+ logLevel: "info",
14
+ promptCacheRetention: "in_memory",
15
+ promptCacheKey: "auto",
16
+ versions: DEFAULT_VERSIONS
17
+ };
18
+ }
19
+
20
+ export function loadConfig() {
21
+ const loaded = readJsonIfExists(configPath()) || {};
22
+ const envRetention = process.env.AERIAL_PROMPT_CACHE_RETENTION;
23
+ const envCacheKey = process.env.AERIAL_PROMPT_CACHE_KEY;
24
+ const defaults = defaultConfig();
25
+ const promptCacheRetention = envRetention === undefined ? (loaded.promptCacheRetention ?? defaults.promptCacheRetention) : envRetention;
26
+ const promptCacheKey = envCacheKey === undefined ? (loaded.promptCacheKey ?? defaults.promptCacheKey) : envCacheKey;
27
+ return { ...defaults, ...loaded, promptCacheRetention, promptCacheKey, versions: { ...DEFAULT_VERSIONS, ...(loaded.versions || {}) } };
28
+ }
29
+
30
+ export function saveConfig(config) {
31
+ writeJsonPrivate(configPath(), config);
32
+ }
33
+
34
+ export function readStoredApiKey() {
35
+ if (!fs.existsSync(apiKeyPath())) return undefined;
36
+ const apiKey = fs.readFileSync(apiKeyPath(), "utf8").trim();
37
+ return apiKey || undefined;
38
+ }
39
+
40
+ function writeStoredApiKey(apiKey) {
41
+ writePrivateFile(apiKeyPath(), `${apiKey}\n`);
42
+ }
43
+
44
+ export function ensureApiKey() {
45
+ const config = loadConfig();
46
+ const envKey = process.env.AERIAL_API_KEY;
47
+ if (envKey) {
48
+ config.apiKeyHash = hashApiKey(envKey);
49
+ writeStoredApiKey(envKey);
50
+ saveConfig(config);
51
+ return { apiKey: envKey, config, created: false, source: "env" };
52
+ }
53
+ const storedKey = readStoredApiKey();
54
+ if (storedKey) {
55
+ if (!config.apiKeyHash || !verifyApiKey(storedKey, config.apiKeyHash)) {
56
+ config.apiKeyHash = hashApiKey(storedKey);
57
+ saveConfig(config);
58
+ }
59
+ return { apiKey: storedKey, config, created: false, source: "stored" };
60
+ }
61
+ const hadHash = Boolean(config.apiKeyHash);
62
+ const apiKey = randomApiKey();
63
+ config.apiKeyHash = hashApiKey(apiKey);
64
+ writeStoredApiKey(apiKey);
65
+ saveConfig(config);
66
+ return { apiKey, config, created: true, source: hadHash ? "rotated" : "generated" };
67
+ }
68
+
69
+ export function validateLocalAuth(headers, config = loadConfig()) {
70
+ const lowerHeaders = Object.fromEntries(Object.entries(headers).map(([key, value]) => [key.toLowerCase(), value]));
71
+ const authorization = lowerHeaders.authorization;
72
+ const bearer = authorization?.startsWith("Bearer ") ? authorization.slice("Bearer ".length).trim() : undefined;
73
+ const apiKey = lowerHeaders["x-api-key"] || bearer;
74
+ return verifyApiKey(apiKey, config.apiKeyHash);
75
+ }
76
+
77
+ export function requireConfigFile() {
78
+ if (!fs.existsSync(configPath())) {
79
+ throw new Error(`Aerial is not initialized. Run: aerial key generate`);
80
+ }
81
+ }
@@ -0,0 +1,11 @@
1
+ export const DEFAULT_HOST = "127.0.0.1";
2
+ export const DEFAULT_PORT = 18181;
3
+ export const CONFIG_VERSION = 1;
4
+ export const GITHUB_CLIENT_ID = "Iv1.b507a08c87ecfe98";
5
+ export const COPILOT_TOKEN_URL = "https://api.github.com/copilot_internal/v2/token";
6
+ export const COPILOT_API_ORIGIN = "https://api.githubcopilot.com";
7
+ export const DEFAULT_VERSIONS = {
8
+ vscode: "1.105.0",
9
+ copilotChat: "0.45.1"
10
+ };
11
+ export const DEFAULT_ANTHROPIC_VERSION = "2023-06-01";