@jiayunxie/aerial 0.1.7 → 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 +1 -1
- package/src/cli.js +5 -6
- package/src/copilot.js +47 -17
- package/src/doctor.js +191 -11
- package/src/model-catalog.js +69 -0
- package/src/service.js +139 -10
- package/src/setup.js +1 -1
package/package.json
CHANGED
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
|
|
453
|
-
|
|
454
|
-
|
|
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,18 +180,23 @@ function withDefaultAnthropicCache(payload) {
|
|
|
174
180
|
return next;
|
|
175
181
|
}
|
|
176
182
|
|
|
177
|
-
function withSupportedAnthropicEffort(payload) {
|
|
183
|
+
function withSupportedAnthropicEffort(payload, models) {
|
|
178
184
|
const effort = payload?.output_config?.effort;
|
|
179
185
|
if (effort === undefined) return payload;
|
|
180
186
|
const model = typeof payload?.model === "string" ? payload.model : "";
|
|
181
|
-
|
|
187
|
+
const family = canonicalClaudeFamily(model);
|
|
188
|
+
if (!family) return payload;
|
|
182
189
|
const nextEffort = effort === "max" ? "xhigh" : effort;
|
|
183
190
|
if (!["low", "medium", "high", "xhigh"].includes(nextEffort)) return payload;
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
:
|
|
191
|
+
const routed = findCompatibleModelShared({
|
|
192
|
+
models,
|
|
193
|
+
family,
|
|
194
|
+
route: "/v1/messages",
|
|
195
|
+
adaptiveThinking: true,
|
|
196
|
+
effort: nextEffort,
|
|
197
|
+
preferredId: model
|
|
198
|
+
});
|
|
199
|
+
const nextModel = routed?.id || model;
|
|
189
200
|
if (model === nextModel && effort === nextEffort) return payload;
|
|
190
201
|
logEvent("anthropic_effort_route", { model, effort, routedModel: nextModel, routedEffort: nextEffort });
|
|
191
202
|
return { ...payload, model: nextModel, output_config: { ...payload.output_config, effort: nextEffort } };
|
|
@@ -221,8 +232,31 @@ function withSupportedAnthropicThinking(payload) {
|
|
|
221
232
|
};
|
|
222
233
|
}
|
|
223
234
|
|
|
224
|
-
function
|
|
225
|
-
|
|
235
|
+
function shouldLoadAnthropicCatalog(payload) {
|
|
236
|
+
const effort = payload?.output_config?.effort;
|
|
237
|
+
const model = typeof payload?.model === "string" ? payload.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
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function withAnthropicDefaults(payload) {
|
|
255
|
+
const next = withSupportedAnthropicThinking(withDefaultAnthropicCache(payload));
|
|
256
|
+
const models = shouldLoadAnthropicCatalog(next)
|
|
257
|
+
? await fetchModelsCatalogForCopilot().catch(() => undefined)
|
|
258
|
+
: undefined;
|
|
259
|
+
return withSupportedAnthropicEffort(next, models);
|
|
226
260
|
}
|
|
227
261
|
|
|
228
262
|
function parseJsonBody(body, contentType) {
|
|
@@ -407,7 +441,7 @@ function wrapResponseWithCacheObserver(upstream, route, requestCache) {
|
|
|
407
441
|
|
|
408
442
|
async function requestWithJsonBody(request, transform) {
|
|
409
443
|
const payload = await request.json();
|
|
410
|
-
const nextPayload = transform(payload);
|
|
444
|
+
const nextPayload = await transform(payload);
|
|
411
445
|
return new Request(request.url, {
|
|
412
446
|
method: request.method,
|
|
413
447
|
headers: request.headers,
|
|
@@ -435,12 +469,8 @@ async function proxyFetch(path, request, { extraHeaders = {}, bodyOverride } = {
|
|
|
435
469
|
return wrapResponseWithCacheObserver(upstream, path, requestCache);
|
|
436
470
|
}
|
|
437
471
|
|
|
438
|
-
async function
|
|
439
|
-
|
|
440
|
-
const response = await fetch(`${COPILOT_API_ORIGIN}/models`, { method: "GET", headers: upstreamHeaders(token) });
|
|
441
|
-
if (!response.ok) return undefined;
|
|
442
|
-
const payload = await response.json().catch(() => ({}));
|
|
443
|
-
return Array.isArray(payload.data) ? payload.data : undefined;
|
|
472
|
+
async function fetchModelsCatalog() {
|
|
473
|
+
return fetchModelsCatalogForCopilot();
|
|
444
474
|
}
|
|
445
475
|
|
|
446
476
|
function responseModelFromCatalog(modelId, models) {
|
|
@@ -461,7 +491,7 @@ export async function proxyResponses(request) {
|
|
|
461
491
|
// per-request /models lookup to avoid an extra upstream call and let the
|
|
462
492
|
// shared HTTP path handle cache_request logging via proxyFetch.
|
|
463
493
|
if (payload?.stream && isResponsesWebSocketOptIn()) {
|
|
464
|
-
const models = await
|
|
494
|
+
const models = await fetchModelsCatalog().catch(() => undefined);
|
|
465
495
|
const model = models ? responseModelFromCatalog(payload.model, models) : undefined;
|
|
466
496
|
if (shouldUseResponsesWebSocket(payload, model)) {
|
|
467
497
|
const requestCache = cacheRequestFields(payload);
|
package/src/doctor.js
CHANGED
|
@@ -1,15 +1,195 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { setupStatus } from "./setup.js";
|
|
2
|
+
import { serviceStatus } from "./service.js";
|
|
3
|
+
import { computeAppStatus } from "./app-status.js";
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
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
|
|
362
|
-
if (!
|
|
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
|
-
|
|
368
|
-
|
|
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
|
-
|
|
372
|
-
|
|
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
|
|
92
|
+
name: "Aerial",
|
|
93
93
|
base_url: `http://${config.host}:${config.port}/v1`,
|
|
94
94
|
wire_api: "responses"
|
|
95
95
|
});
|