@jiayunxie/aerial 0.1.8 → 0.1.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jiayunxie/aerial",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
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
@@ -5,7 +5,7 @@ import { ensureApiKey, loadConfig, saveConfig } from "./config.js";
5
5
  import { startServer } from "./server.js";
6
6
  import { setupClaude, setupCodex, setupStatus, restoreClient, restoreAllClients } from "./setup.js";
7
7
  import { serviceInstall, serviceStart, serviceStop, serviceRestart, serviceUninstall, serviceStatus } from "./service.js";
8
- import { doctor } from "./doctor.js";
8
+ import { doctor, renderDoctorText } from "./doctor.js";
9
9
  import { runProbe, formatProbeReport } from "./probe.js";
10
10
  import { chooseSetupModel, formatModelChoices } from "./model-selection.js";
11
11
  import { printVersion } from "./version.js";
@@ -449,11 +449,10 @@ async function main() {
449
449
  }
450
450
 
451
451
  if (command === "doctor") {
452
- const result = doctor();
453
- for (const check of result.checks) {
454
- console.log(`${check.ok ? "OK" : "FAIL"} ${check.name}: ${check.detail}`);
455
- }
456
- process.exitCode = result.ok ? 0 : 1;
452
+ const report = await doctor();
453
+ if (args.includes("--json")) console.log(JSON.stringify(report, null, 2));
454
+ else console.log(renderDoctorText(report));
455
+ process.exitCode = report.ok ? 0 : 1;
457
456
  return;
458
457
  }
459
458
 
package/src/copilot.js CHANGED
@@ -4,6 +4,12 @@ import { loadConfig } from "./config.js";
4
4
  import { getCopilotToken } from "./auth.js";
5
5
  import { logEvent } from "./log.js";
6
6
  import { isResponsesWebSocketOptIn, proxyResponsesWebSocket, shouldUseResponsesWebSocket } from "./responses-websocket.js";
7
+ import {
8
+ fetchModelsCatalog as fetchModelsCatalogShared,
9
+ findCompatibleModel as findCompatibleModelShared,
10
+ canonicalClaudeFamily,
11
+ tokenFingerprintOf
12
+ } from "./model-catalog.js";
7
13
 
8
14
  function upstreamHeaders(token, extra = {}) {
9
15
  const config = loadConfig();
@@ -174,51 +180,22 @@ function withDefaultAnthropicCache(payload) {
174
180
  return next;
175
181
  }
176
182
 
177
- function canonicalClaudeOpus47Family(model) {
178
- return /^claude-opus-4[.-]7(?:-|$)/.test(model) ? "claude-opus-4.7" : undefined;
179
- }
180
-
181
- function modelHasMessagesRoute(model) {
182
- const endpoints = Array.isArray(model?.supported_endpoints) ? model.supported_endpoints : [];
183
- const routes = Array.isArray(model?.aerial?.routes) ? model.aerial.routes : [];
184
- return endpoints.includes("/v1/messages") || routes.includes("messages");
185
- }
186
-
187
- function supportedReasoningEfforts(model) {
188
- const supports = model?.capabilities?.supports;
189
- const values = supports?.reasoning_effort ?? supports?.reasoning_efforts;
190
- if (Array.isArray(values)) return values.map(String);
191
- if (typeof values === "string") return [values];
192
- return [];
193
- }
194
-
195
- function modelSupportsAdaptiveThinking(model) {
196
- const supports = model?.capabilities?.supports;
197
- return supports?.adaptive_thinking === true || supports?.thinking?.adaptive === true;
198
- }
199
-
200
- function findAnthropicEffortModel(modelId, effort, models) {
201
- if (!Array.isArray(models)) return undefined;
202
- const family = canonicalClaudeOpus47Family(modelId);
203
- if (!family) return undefined;
204
- const candidates = models.filter((model) => {
205
- const id = typeof model?.id === "string" ? model.id : "";
206
- return canonicalClaudeOpus47Family(id) === family &&
207
- modelHasMessagesRoute(model) &&
208
- modelSupportsAdaptiveThinking(model) &&
209
- supportedReasoningEfforts(model).includes(effort);
210
- });
211
- return candidates.find((model) => model.id === modelId) || candidates[0];
212
- }
213
-
214
183
  function withSupportedAnthropicEffort(payload, models) {
215
184
  const effort = payload?.output_config?.effort;
216
185
  if (effort === undefined) return payload;
217
186
  const model = typeof payload?.model === "string" ? payload.model : "";
218
- if (!canonicalClaudeOpus47Family(model)) return payload;
187
+ const family = canonicalClaudeFamily(model);
188
+ if (!family) return payload;
219
189
  const nextEffort = effort === "max" ? "xhigh" : effort;
220
190
  if (!["low", "medium", "high", "xhigh"].includes(nextEffort)) return payload;
221
- const routed = findAnthropicEffortModel(model, nextEffort, models);
191
+ const routed = findCompatibleModelShared({
192
+ models,
193
+ family,
194
+ route: "/v1/messages",
195
+ adaptiveThinking: true,
196
+ effort: nextEffort,
197
+ preferredId: model
198
+ });
222
199
  const nextModel = routed?.id || model;
223
200
  if (model === nextModel && effort === nextEffort) return payload;
224
201
  logEvent("anthropic_effort_route", { model, effort, routedModel: nextModel, routedEffort: nextEffort });
@@ -258,13 +235,26 @@ function withSupportedAnthropicThinking(payload) {
258
235
  function shouldLoadAnthropicCatalog(payload) {
259
236
  const effort = payload?.output_config?.effort;
260
237
  const model = typeof payload?.model === "string" ? payload.model : "";
261
- return effort !== undefined && Boolean(canonicalClaudeOpus47Family(model));
238
+ return effort !== undefined && Boolean(canonicalClaudeFamily(model));
239
+ }
240
+
241
+ async function fetchModelsCatalogForCopilot() {
242
+ const token = await getCopilotToken();
243
+ return fetchModelsCatalogShared({
244
+ tokenFingerprint: tokenFingerprintOf(token),
245
+ fetchImpl: async () => {
246
+ const response = await fetch(`${COPILOT_API_ORIGIN}/models`, { method: "GET", headers: upstreamHeaders(token) });
247
+ if (!response.ok) return undefined;
248
+ const payload = await response.json().catch(() => ({}));
249
+ return Array.isArray(payload?.data) ? payload.data : undefined;
250
+ }
251
+ });
262
252
  }
263
253
 
264
254
  async function withAnthropicDefaults(payload) {
265
255
  const next = withSupportedAnthropicThinking(withDefaultAnthropicCache(payload));
266
256
  const models = shouldLoadAnthropicCatalog(next)
267
- ? await fetchModelsCatalog().catch(() => undefined)
257
+ ? await fetchModelsCatalogForCopilot().catch(() => undefined)
268
258
  : undefined;
269
259
  return withSupportedAnthropicEffort(next, models);
270
260
  }
@@ -480,11 +470,7 @@ async function proxyFetch(path, request, { extraHeaders = {}, bodyOverride } = {
480
470
  }
481
471
 
482
472
  async function fetchModelsCatalog() {
483
- const token = await getCopilotToken();
484
- const response = await fetch(`${COPILOT_API_ORIGIN}/models`, { method: "GET", headers: upstreamHeaders(token) });
485
- if (!response.ok) return undefined;
486
- const payload = await response.json().catch(() => ({}));
487
- return Array.isArray(payload.data) ? payload.data : undefined;
473
+ return fetchModelsCatalogForCopilot();
488
474
  }
489
475
 
490
476
  function responseModelFromCatalog(modelId, models) {
package/src/doctor.js CHANGED
@@ -1,15 +1,195 @@
1
- import fs from "node:fs";
2
- import { apiKeyPath, configPath, githubTokenPath } from "./paths.js";
3
- import { loadConfig } from "./config.js";
1
+ import { setupStatus } from "./setup.js";
2
+ import { serviceStatus } from "./service.js";
3
+ import { computeAppStatus } from "./app-status.js";
4
4
 
5
- export function doctor() {
6
- const config = loadConfig();
5
+ const REPAIRS = Object.freeze({
6
+ LOGIN: Object.freeze({ command: "aerial", args: ["login"] }),
7
+ SETUP_CODEX: Object.freeze({ command: "aerial", args: ["setup", "codex"] }),
8
+ SERVICE_INSTALL: Object.freeze({ command: "aerial", args: ["service", "install"] }),
9
+ SERVICE_STATUS_JSON: Object.freeze({ command: "aerial", args: ["service", "status", "--json"] })
10
+ });
11
+
12
+ function buildAuthChecks(setup) {
13
+ const checks = [];
14
+ const apiKey = setup.auth.api_key;
15
+ if (!apiKey.exists) {
16
+ checks.push({
17
+ id: "auth.api_key",
18
+ ok: false,
19
+ severity: "fail",
20
+ message: "Aerial local API key is missing. Run setup for the client you use; this repair shows the Codex path.",
21
+ repair: REPAIRS.SETUP_CODEX
22
+ });
23
+ } else {
24
+ checks.push({ id: "auth.api_key", ok: true, severity: "info", message: "Aerial local API key is present." });
25
+ }
26
+ const gh = setup.auth.github_token;
27
+ if (gh.source === "missing") {
28
+ checks.push({
29
+ id: "auth.github_token",
30
+ ok: false,
31
+ severity: "fail",
32
+ message: "GitHub token is not configured. Aerial cannot reach Copilot until you log in.",
33
+ repair: REPAIRS.LOGIN
34
+ });
35
+ } else if (gh.source === "env") {
36
+ checks.push({
37
+ id: "auth.github_token",
38
+ ok: true,
39
+ severity: "warn",
40
+ message: "AERIAL_GITHUB_TOKEN is set in this shell only. The background service does not inherit it; run `aerial login` to persist a service-readable token.",
41
+ repair: REPAIRS.LOGIN
42
+ });
43
+ } else {
44
+ checks.push({ id: "auth.github_token", ok: true, severity: "info", message: "GitHub token is present in the persisted credential file." });
45
+ }
46
+ return checks;
47
+ }
48
+
49
+ function buildClientChecks(setup) {
50
+ const checks = [];
51
+ const codex = setup.clients.codex;
52
+ const claude = setup.clients.claude;
53
+ const aerialCodex = codex.state === "aerial";
54
+ const aerialClaude = claude.state === "aerial";
55
+ if (!aerialCodex && !aerialClaude) {
56
+ checks.push({
57
+ id: "clients.aerial_client",
58
+ ok: false,
59
+ severity: "fail",
60
+ message: "No client is wired to Aerial. Run setup for the client you use; this repair shows the Codex path.",
61
+ repair: REPAIRS.SETUP_CODEX
62
+ });
63
+ } else {
64
+ const wired = [aerialCodex && "codex", aerialClaude && "claude"].filter(Boolean).join(", ");
65
+ checks.push({ id: "clients.aerial_client", ok: true, severity: "info", message: `Client wired to Aerial: ${wired}.` });
66
+ }
67
+ return checks;
68
+ }
69
+
70
+ function buildServiceChecks(service) {
71
+ const checks = [];
72
+ if (service.supported === false) {
73
+ checks.push({
74
+ id: "service.supported",
75
+ ok: false,
76
+ severity: "warn",
77
+ message: `Background service is not supported on ${service.platform}; Aerial must run in the foreground.`
78
+ });
79
+ return checks;
80
+ }
81
+ const installed = Boolean(service.service?.installed);
82
+ const loaded = Boolean(service.service?.loaded);
83
+ const healthy = service.health?.aerial === true;
84
+ if (healthy && !installed) {
85
+ checks.push({
86
+ id: "service.foreground_only",
87
+ ok: true,
88
+ severity: "warn",
89
+ message: "Aerial is running in the foreground but no background service is installed; it will not start on reboot.",
90
+ repair: REPAIRS.SERVICE_INSTALL
91
+ });
92
+ } else if (installed && !healthy) {
93
+ checks.push({
94
+ id: "service.health",
95
+ ok: false,
96
+ severity: "fail",
97
+ message: loaded
98
+ ? "Service manager reports the task running, but Aerial /health is not responding."
99
+ : "Service is installed but not running.",
100
+ repair: REPAIRS.SERVICE_STATUS_JSON
101
+ });
102
+ } else if (!installed && !healthy) {
103
+ checks.push({
104
+ id: "service.health",
105
+ ok: false,
106
+ severity: "fail",
107
+ message: "Aerial is not running and no background service is installed.",
108
+ repair: REPAIRS.SERVICE_INSTALL
109
+ });
110
+ } else {
111
+ checks.push({ id: "service.health", ok: true, severity: "info", message: "Aerial /health is responding." });
112
+ }
113
+ if (service.health?.portConflict) {
114
+ checks.push({
115
+ id: "service.port_conflict",
116
+ ok: false,
117
+ severity: "fail",
118
+ message: `Port ${service.config?.port} is held by a non-Aerial process: ${service.health.conflictReason || "unknown"}.`,
119
+ repair: REPAIRS.SERVICE_STATUS_JSON
120
+ });
121
+ }
122
+ const wrapper = service.service?.wrapper;
123
+ if (wrapper && wrapper.stale === true) {
124
+ const reasons = Array.isArray(wrapper.staleReasons) ? wrapper.staleReasons.join(", ") : "unknown";
125
+ checks.push({
126
+ id: "service.wrapper_stale",
127
+ ok: true,
128
+ severity: "warn",
129
+ message: `Installed service wrapper is stale (${reasons}); reinstall to regenerate it.`,
130
+ repair: REPAIRS.SERVICE_INSTALL
131
+ });
132
+ }
133
+ return checks;
134
+ }
135
+
136
+ function summarize(ok, checks) {
137
+ const fails = checks.filter((c) => c.severity === "fail").length;
138
+ const warns = checks.filter((c) => c.severity === "warn").length;
139
+ if (ok && warns === 0) return "All checks passed.";
140
+ if (ok && warns > 0) return `Aerial is functional with ${warns} warning(s).`;
141
+ return `${fails} check(s) failed; ${warns} warning(s).`;
142
+ }
143
+
144
+ export async function doctor({ run, healthFetch, setup, service } = {}) {
145
+ const setupOut = setup ?? setupStatus();
146
+ const serviceOut = service ?? await serviceStatus({ ...(run ? { run } : {}), ...(healthFetch ? { healthFetch } : {}) });
147
+ const app = computeAppStatus(setupOut, serviceOut);
7
148
  const checks = [
8
- { name: "config", ok: fs.existsSync(configPath()), detail: configPath() },
9
- { name: "api_key", ok: Boolean(config.apiKeyHash), detail: fs.existsSync(apiKeyPath()) ? apiKeyPath() : config.apiKeyHash ? "hash configured" : "run: aerial setup codex or aerial setup claude" },
10
- { name: "github_token", ok: fs.existsSync(githubTokenPath()) || Boolean(process.env.AERIAL_GITHUB_TOKEN), detail: fs.existsSync(githubTokenPath()) ? githubTokenPath() : "run: aerial login" },
11
- { name: "node", ok: Number(process.versions.node.split(".")[0]) >= 22, detail: process.version },
12
- { name: "bind", ok: config.host === "127.0.0.1", detail: `${config.host}:${config.port}` }
149
+ ...buildAuthChecks(setupOut),
150
+ ...buildClientChecks(setupOut),
151
+ ...buildServiceChecks(serviceOut)
152
+ ];
153
+ const ok = app.ok && checks.every((c) => c.severity !== "fail");
154
+ return {
155
+ schema: "aerial.doctor.v1",
156
+ ok,
157
+ summary: summarize(ok, checks),
158
+ checks,
159
+ status: {
160
+ schema: app.schema,
161
+ ok: app.ok,
162
+ setup: app.setup,
163
+ service: app.service
164
+ }
165
+ };
166
+ }
167
+
168
+ export function renderRepairCommand(repair) {
169
+ if (!repair || typeof repair !== "object") return "";
170
+ const args = Array.isArray(repair.args) ? repair.args : [];
171
+ return [repair.command, ...args].join(" ");
172
+ }
173
+
174
+ export function renderDoctorText(report) {
175
+ const lines = [];
176
+ lines.push(`aerial doctor: ${report.summary}`);
177
+ const groups = [
178
+ ["fail", "Failures"],
179
+ ["warn", "Warnings"],
180
+ ["info", "Info"]
13
181
  ];
14
- return { ok: checks.every((check) => check.ok), checks };
182
+ for (const [severity, label] of groups) {
183
+ const items = report.checks.filter((c) => c.severity === severity);
184
+ if (items.length === 0) continue;
185
+ lines.push("");
186
+ lines.push(`${label}:`);
187
+ for (const check of items) {
188
+ const tag = severity.toUpperCase();
189
+ let line = ` ${tag} ${check.id}: ${check.message}`;
190
+ if (check.repair) line += ` -> run: ${renderRepairCommand(check.repair)}`;
191
+ lines.push(line);
192
+ }
193
+ }
194
+ return lines.join("\n");
15
195
  }
@@ -0,0 +1,69 @@
1
+ import crypto from "node:crypto";
2
+
3
+ const TTL_MS = 30000;
4
+ const cache = new Map();
5
+
6
+ export function tokenFingerprintOf(token) {
7
+ if (typeof token !== "string" || token.length === 0) return "anonymous";
8
+ return crypto.createHash("sha256").update(token).digest("hex").slice(0, 16);
9
+ }
10
+
11
+ export async function fetchModelsCatalog({ fetchImpl, tokenFingerprint } = {}) {
12
+ if (typeof fetchImpl !== "function") return undefined;
13
+ const key = tokenFingerprint || "anonymous";
14
+ const cached = cache.get(key);
15
+ const now = Date.now();
16
+ if (cached && cached.expiresAt > now) return cached.models;
17
+ const models = await fetchImpl();
18
+ if (!Array.isArray(models)) return undefined;
19
+ cache.set(key, { models, expiresAt: now + TTL_MS });
20
+ return models;
21
+ }
22
+
23
+ export function clearModelCatalogCacheForTests() {
24
+ cache.clear();
25
+ }
26
+
27
+ export function canonicalClaudeFamily(modelId) {
28
+ if (typeof modelId !== "string") return undefined;
29
+ if (/^claude-opus-4[.-]7(?:-|$)/.test(modelId)) return "claude-opus-4.7";
30
+ return undefined;
31
+ }
32
+
33
+ function modelHasRoute(model, route) {
34
+ const endpoints = Array.isArray(model?.supported_endpoints) ? model.supported_endpoints : [];
35
+ const routes = Array.isArray(model?.aerial?.routes) ? model.aerial.routes : [];
36
+ if (route === "/v1/messages") return endpoints.includes("/v1/messages") || routes.includes("messages");
37
+ return endpoints.includes(route);
38
+ }
39
+
40
+ function modelSupportsAdaptiveThinking(model) {
41
+ const supports = model?.capabilities?.supports;
42
+ return supports?.adaptive_thinking === true || supports?.thinking?.adaptive === true;
43
+ }
44
+
45
+ function supportedReasoningEfforts(model) {
46
+ const supports = model?.capabilities?.supports;
47
+ const values = supports?.reasoning_effort ?? supports?.reasoning_efforts;
48
+ if (Array.isArray(values)) return values.map(String);
49
+ if (typeof values === "string") return [values];
50
+ return [];
51
+ }
52
+
53
+ export function findCompatibleModel({ models, family, route, adaptiveThinking, effort, preferredId } = {}) {
54
+ if (!Array.isArray(models) || !family) return undefined;
55
+ const candidates = models.filter((model) => {
56
+ const id = typeof model?.id === "string" ? model.id : "";
57
+ if (canonicalClaudeFamily(id) !== family) return false;
58
+ if (route && !modelHasRoute(model, route)) return false;
59
+ if (adaptiveThinking === true && !modelSupportsAdaptiveThinking(model)) return false;
60
+ if (effort && !supportedReasoningEfforts(model).includes(effort)) return false;
61
+ return true;
62
+ });
63
+ if (candidates.length === 0) return undefined;
64
+ if (preferredId) {
65
+ const preferred = candidates.find((model) => model.id === preferredId);
66
+ if (preferred) return preferred;
67
+ }
68
+ return candidates[0];
69
+ }
package/src/service.js CHANGED
@@ -358,21 +358,147 @@ async function pollForAerialUp(host, port, healthFetch, deadlineMs = HEALTH_STAR
358
358
  return { cls: lastCls || { mode: "absent" }, probe: lastProbe, attempts, elapsedMs: Date.now() - start };
359
359
  }
360
360
 
361
- function readWrapperNodePath(wrapperFile) {
362
- if (!wrapperFile) return undefined;
361
+ function unescapeShSingleQuoted(line, prefix) {
362
+ if (!line.startsWith(prefix)) return undefined;
363
+ const rest = line.slice(prefix.length);
364
+ if (!rest.startsWith("'")) return undefined;
365
+ let i = 1;
366
+ let out = "";
367
+ while (i < rest.length) {
368
+ const ch = rest[i];
369
+ if (ch === "'") {
370
+ if (rest.slice(i, i + 4) === "'\\''") {
371
+ out += "'";
372
+ i += 4;
373
+ continue;
374
+ }
375
+ return out;
376
+ }
377
+ out += ch;
378
+ i += 1;
379
+ }
380
+ return undefined;
381
+ }
382
+
383
+ function unescapePsSingleQuoted(line, prefix) {
384
+ if (!line.startsWith(prefix)) return undefined;
385
+ const rest = line.slice(prefix.length);
386
+ if (!rest.startsWith("'")) return undefined;
387
+ let i = 1;
388
+ let out = "";
389
+ while (i < rest.length) {
390
+ const ch = rest[i];
391
+ if (ch === "'") {
392
+ if (rest[i + 1] === "'") {
393
+ out += "'";
394
+ i += 2;
395
+ continue;
396
+ }
397
+ return out;
398
+ }
399
+ out += ch;
400
+ i += 1;
401
+ }
402
+ return undefined;
403
+ }
404
+
405
+ function parseWrapperPaths(wrapperFile) {
406
+ if (!wrapperFile) return { node: undefined, cli: undefined };
363
407
  try {
364
- if (!fs.existsSync(wrapperFile)) return undefined;
408
+ if (!fs.existsSync(wrapperFile)) return { node: undefined, cli: undefined };
365
409
  const data = fs.readFileSync(wrapperFile, "utf8");
410
+ const lines = data.split(/\r?\n/);
366
411
  if (wrapperFile.endsWith(".sh")) {
367
- const m = data.match(/^NODE_BIN='([^']*)'/m);
368
- return m ? m[1] : undefined;
412
+ let node;
413
+ let cli;
414
+ for (const line of lines) {
415
+ if (node === undefined) {
416
+ const candidate = unescapeShSingleQuoted(line, "NODE_BIN=");
417
+ if (candidate !== undefined) node = candidate;
418
+ }
419
+ if (cli === undefined) {
420
+ const candidate = unescapeShSingleQuoted(line, "CLI_ENTRY=");
421
+ if (candidate !== undefined) cli = candidate;
422
+ }
423
+ if (node !== undefined && cli !== undefined) break;
424
+ }
425
+ return { node, cli };
369
426
  }
370
427
  if (wrapperFile.endsWith(".ps1")) {
371
- const m = data.match(/^\$node\s*=\s*'([^']*)'/m);
372
- return m ? m[1] : undefined;
428
+ let node;
429
+ let cli;
430
+ for (const line of lines) {
431
+ if (node === undefined) {
432
+ const m = line.match(/^\$node\s*=\s*(.*)$/);
433
+ if (m) {
434
+ const candidate = unescapePsSingleQuoted(m[1].trim(), "");
435
+ if (candidate !== undefined) node = candidate;
436
+ }
437
+ }
438
+ if (cli === undefined) {
439
+ const m = line.match(/^\$cli\s*=\s*(.*)$/);
440
+ if (m) {
441
+ const candidate = unescapePsSingleQuoted(m[1].trim(), "");
442
+ if (candidate !== undefined) cli = candidate;
443
+ }
444
+ }
445
+ if (node !== undefined && cli !== undefined) break;
446
+ }
447
+ return { node, cli };
373
448
  }
374
449
  } catch {}
375
- return undefined;
450
+ return { node: undefined, cli: undefined };
451
+ }
452
+
453
+ function readWrapperNodePath(wrapperFile) {
454
+ return parseWrapperPaths(wrapperFile).node;
455
+ }
456
+
457
+ export const STALE_REASONS = Object.freeze({
458
+ WRAPPER_MISSING: "wrapper_missing",
459
+ WRAPPER_NODE_MISSING: "wrapper_node_missing",
460
+ WRAPPER_CLI_MISSING: "wrapper_cli_missing",
461
+ WRAPPER_LOG_CONFIG_UNPARSEABLE: "wrapper_log_config_unparseable"
462
+ });
463
+
464
+ function wrapperBlock(state) {
465
+ if (!state || state.installed !== true) {
466
+ return { stale: false, staleReasons: [] };
467
+ }
468
+ let wrapperPath;
469
+ if (process.platform === "darwin") wrapperPath = darwinWrapperPath();
470
+ else if (process.platform === "win32") wrapperPath = winWrapperPath();
471
+ const wrapperFileExists = wrapperPath ? fs.existsSync(wrapperPath) : false;
472
+ const staleReasons = [];
473
+ if (!wrapperFileExists) {
474
+ return {
475
+ path: wrapperPath,
476
+ nodePath: undefined,
477
+ nodeExists: undefined,
478
+ cliPath: undefined,
479
+ cliExists: undefined,
480
+ logConfigParseable: undefined,
481
+ stale: true,
482
+ staleReasons: [STALE_REASONS.WRAPPER_MISSING]
483
+ };
484
+ }
485
+ const { node: nodePath, cli: cliPath } = parseWrapperPaths(wrapperPath);
486
+ const nodeExists = nodePath ? fs.existsSync(nodePath) : false;
487
+ const cliExists = cliPath ? fs.existsSync(cliPath) : false;
488
+ const logConfigParseable = parseWrapperLogValues(wrapperPath) !== null;
489
+ if (!nodeExists) staleReasons.push(STALE_REASONS.WRAPPER_NODE_MISSING);
490
+ if (!cliExists) staleReasons.push(STALE_REASONS.WRAPPER_CLI_MISSING);
491
+ if (!logConfigParseable) staleReasons.push(STALE_REASONS.WRAPPER_LOG_CONFIG_UNPARSEABLE);
492
+ return {
493
+ path: wrapperPath,
494
+ nodePath,
495
+ nodeExists,
496
+ cliPath,
497
+ cliExists,
498
+ logConfigParseable,
499
+ stale: staleReasons.length > 0,
500
+ staleReasons
501
+ };
376
502
  }
377
503
 
378
504
  function healthFailedDiagnostics({ wrapper, probe, attempts, elapsedMs }) {
@@ -904,7 +1030,7 @@ export async function serviceStatus({ run = defaultRunCommand, healthFetch } = {
904
1030
  platform: process.platform,
905
1031
  supported: false,
906
1032
  config: { host: config.host, port: config.port },
907
- service: { installed: false, loaded: false, reason: "unsupported_platform", platform: process.platform },
1033
+ service: { installed: false, loaded: false, reason: "unsupported_platform", platform: process.platform, wrapper: wrapperBlock({ installed: false }) },
908
1034
  health: { ok: false, error: "unsupported_platform" },
909
1035
  logs: logsBlock(),
910
1036
  auth: authBlock(),
@@ -914,6 +1040,7 @@ export async function serviceStatus({ run = defaultRunCommand, healthFetch } = {
914
1040
  const state = serviceState(ctx);
915
1041
  const probe = await (healthFetch || defaultHealthFetch)(config.host, config.port);
916
1042
  const cls = classifyHealth(probe);
1043
+ const wrapper = wrapperBlock(state);
917
1044
  let supervisor;
918
1045
  if (cls.mode === "aerial_running") {
919
1046
  supervisor = state.installed && state.loaded ? "service-managed" : "foreground";
@@ -939,7 +1066,7 @@ export async function serviceStatus({ run = defaultRunCommand, healthFetch } = {
939
1066
  platform: process.platform,
940
1067
  supported: true,
941
1068
  config: { host: config.host, port: config.port },
942
- service: { platform: process.platform, ...state },
1069
+ service: { platform: process.platform, ...state, wrapper },
943
1070
  health,
944
1071
  logs: logsBlock(),
945
1072
  auth: authBlock(),
@@ -956,6 +1083,8 @@ export const _internal = {
956
1083
  aerialLogPath,
957
1084
  stdioLogPath,
958
1085
  parseWrapperLogValues,
1086
+ parseWrapperPaths,
1087
+ wrapperBlock,
959
1088
  buildSchtasksArgs,
960
1089
  quoteSchtasksTR,
961
1090
  classifyHealth
package/src/setup.js CHANGED
@@ -89,7 +89,7 @@ export function setupCodex({ model, authCommand = DEFAULT_CODEX_AUTH } = {}) {
89
89
  content = setTomlString(content, "model_provider", "aerial");
90
90
  content = setTomlString(content, "model", selectedModel);
91
91
  content = upsertTomlSection(content, "model_providers.aerial", {
92
- name: "Aerial Copilot Local",
92
+ name: "Aerial",
93
93
  base_url: `http://${config.host}:${config.port}/v1`,
94
94
  wire_api: "responses"
95
95
  });