@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/LICENSE +21 -0
- package/README.md +264 -0
- package/docs/usage.md +176 -0
- package/package.json +46 -0
- package/src/auth.js +92 -0
- package/src/cli.js +151 -0
- package/src/config.js +81 -0
- package/src/constants.js +11 -0
- package/src/copilot.js +493 -0
- package/src/crypto.js +25 -0
- package/src/doctor.js +15 -0
- package/src/log.js +9 -0
- package/src/paths.js +45 -0
- package/src/probe.js +141 -0
- package/src/responses-websocket.js +111 -0
- package/src/server.js +101 -0
- package/src/setup.js +121 -0
- package/src/version.js +19 -0
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
|
+
}
|
package/src/constants.js
ADDED
|
@@ -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";
|