@jiayunxie/aerial 0.1.6 → 0.1.7
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 +22 -0
- package/src/copilot.js +37 -10
- package/src/service.js +118 -13
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -314,6 +314,17 @@ async function main() {
|
|
|
314
314
|
if (r.bootstrap?.stderr) console.log(` bootstrap stderr: ${r.bootstrap.stderr.trim()}`);
|
|
315
315
|
if (r.create?.stderr) console.log(` schtasks stderr: ${r.create.stderr.trim()}`);
|
|
316
316
|
if (r.run?.stderr) console.log(` schtasks /Run stderr: ${r.run.stderr.trim()}`);
|
|
317
|
+
if (r.diagnostics) {
|
|
318
|
+
if (r.diagnostics.stdioLog) console.log(` stdio log: ${r.diagnostics.stdioLog}`);
|
|
319
|
+
if (r.diagnostics.aerialLog) console.log(` aerial log: ${r.diagnostics.aerialLog}`);
|
|
320
|
+
if (r.diagnostics.wrapperNode) console.log(` wrapper node: ${r.diagnostics.wrapperNode}`);
|
|
321
|
+
if (r.diagnostics.health) {
|
|
322
|
+
const h = r.diagnostics.health;
|
|
323
|
+
const tail = h.lastError ? `, last error: ${h.lastError}` : (h.lastStatus !== undefined ? `, last status: ${h.lastStatus}` : "");
|
|
324
|
+
console.log(` health probe: ${h.attempts} attempts over ${h.elapsedMs}ms${tail}`);
|
|
325
|
+
}
|
|
326
|
+
console.log(` Run: aerial service status --json`);
|
|
327
|
+
}
|
|
317
328
|
if (r.warning) console.log(` WARNING: ${r.warning.message}`);
|
|
318
329
|
process.exitCode = r.ok ? 0 : 1;
|
|
319
330
|
} catch (err) {
|
|
@@ -329,6 +340,17 @@ async function main() {
|
|
|
329
340
|
else if (r.ok) console.log(`Service start: ok (${r.platform})`);
|
|
330
341
|
else console.log(`Service start: FAILED (${r.reason || `status=${r.status}`})${r.message ? ": " + r.message : ""}`);
|
|
331
342
|
if (r.stderr) console.log(` ${r.stderr.trim()}`);
|
|
343
|
+
if (r.diagnostics) {
|
|
344
|
+
if (r.diagnostics.stdioLog) console.log(` stdio log: ${r.diagnostics.stdioLog}`);
|
|
345
|
+
if (r.diagnostics.aerialLog) console.log(` aerial log: ${r.diagnostics.aerialLog}`);
|
|
346
|
+
if (r.diagnostics.wrapperNode) console.log(` wrapper node: ${r.diagnostics.wrapperNode}`);
|
|
347
|
+
if (r.diagnostics.health) {
|
|
348
|
+
const h = r.diagnostics.health;
|
|
349
|
+
const tail = h.lastError ? `, last error: ${h.lastError}` : (h.lastStatus !== undefined ? `, last status: ${h.lastStatus}` : "");
|
|
350
|
+
console.log(` health probe: ${h.attempts} attempts over ${h.elapsedMs}ms${tail}`);
|
|
351
|
+
}
|
|
352
|
+
console.log(` Run: aerial service status --json`);
|
|
353
|
+
}
|
|
332
354
|
if (r.warning) console.log(` WARNING: ${r.warning.message}`);
|
|
333
355
|
process.exitCode = r.ok ? 0 : 1;
|
|
334
356
|
} catch (err) {
|
package/src/copilot.js
CHANGED
|
@@ -179,23 +179,50 @@ function withSupportedAnthropicEffort(payload) {
|
|
|
179
179
|
if (effort === undefined) return payload;
|
|
180
180
|
const model = typeof payload?.model === "string" ? payload.model : "";
|
|
181
181
|
if (!/^claude-opus-4[.-]7(?:-|$)/.test(model)) return payload;
|
|
182
|
-
const routes = {
|
|
183
|
-
low: "claude-opus-4.7-1m-internal",
|
|
184
|
-
medium: "claude-opus-4.7",
|
|
185
|
-
high: "claude-opus-4.7-high",
|
|
186
|
-
xhigh: "claude-opus-4.7-xhigh",
|
|
187
|
-
max: "claude-opus-4.7-xhigh"
|
|
188
|
-
};
|
|
189
|
-
const nextModel = routes[effort];
|
|
190
182
|
const nextEffort = effort === "max" ? "xhigh" : effort;
|
|
191
|
-
if (!
|
|
183
|
+
if (!["low", "medium", "high", "xhigh"].includes(nextEffort)) return payload;
|
|
184
|
+
const baseModel = /^claude-opus-4[.-]7$/.test(model);
|
|
185
|
+
const legacyEffortModel = /^claude-opus-4[.-]7-(?:high|xhigh)$/.test(model);
|
|
186
|
+
const nextModel = legacyEffortModel || (baseModel && nextEffort !== "medium")
|
|
187
|
+
? "claude-opus-4.7-1m-internal"
|
|
188
|
+
: model;
|
|
192
189
|
if (model === nextModel && effort === nextEffort) return payload;
|
|
193
190
|
logEvent("anthropic_effort_route", { model, effort, routedModel: nextModel, routedEffort: nextEffort });
|
|
194
191
|
return { ...payload, model: nextModel, output_config: { ...payload.output_config, effort: nextEffort } };
|
|
195
192
|
}
|
|
196
193
|
|
|
194
|
+
function legacyThinkingEffort(thinking) {
|
|
195
|
+
if (typeof thinking?.effort === "string" && thinking.effort.trim()) return thinking.effort.trim();
|
|
196
|
+
const budget = Number(thinking?.budget_tokens);
|
|
197
|
+
if (!Number.isFinite(budget) || budget <= 0) return "medium";
|
|
198
|
+
if (budget <= 4096) return "low";
|
|
199
|
+
if (budget <= 16000) return "medium";
|
|
200
|
+
if (budget <= 64000) return "high";
|
|
201
|
+
return "xhigh";
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function isLegacyThinkingEnabled(thinking) {
|
|
205
|
+
if (!thinking || typeof thinking !== "object" || Array.isArray(thinking)) return false;
|
|
206
|
+
if (thinking.type === "enabled") return true;
|
|
207
|
+
return Boolean(thinking.type && typeof thinking.type === "object" && thinking.type.enabled);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function withSupportedAnthropicThinking(payload) {
|
|
211
|
+
if (!isLegacyThinkingEnabled(payload?.thinking)) return payload;
|
|
212
|
+
const outputConfig = payload?.output_config && typeof payload.output_config === "object" && !Array.isArray(payload.output_config)
|
|
213
|
+
? payload.output_config
|
|
214
|
+
: {};
|
|
215
|
+
const effort = outputConfig.effort ?? legacyThinkingEffort(payload.thinking);
|
|
216
|
+
logEvent("anthropic_thinking_route", { model: payload.model, routedType: "adaptive", routedEffort: effort });
|
|
217
|
+
return {
|
|
218
|
+
...payload,
|
|
219
|
+
thinking: { type: "adaptive" },
|
|
220
|
+
output_config: { ...outputConfig, effort }
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
197
224
|
function withAnthropicDefaults(payload) {
|
|
198
|
-
return withSupportedAnthropicEffort(withDefaultAnthropicCache(payload));
|
|
225
|
+
return withSupportedAnthropicEffort(withSupportedAnthropicThinking(withDefaultAnthropicCache(payload)));
|
|
199
226
|
}
|
|
200
227
|
|
|
201
228
|
function parseJsonBody(body, contentType) {
|
package/src/service.js
CHANGED
|
@@ -15,6 +15,7 @@ const DEFAULT_WRAPPER_LOG_MAX_BYTES = 5 * 1024 * 1024;
|
|
|
15
15
|
const DEFAULT_WRAPPER_LOG_BACKUPS = 3;
|
|
16
16
|
const HEALTH_TIMEOUT_MS = 1500;
|
|
17
17
|
const HEALTH_START_TIMEOUT_MS = 5000;
|
|
18
|
+
const HEALTH_POLL_INTERVAL_MS = 250;
|
|
18
19
|
|
|
19
20
|
function wrapperLogConfig() {
|
|
20
21
|
const out = { maxBytes: DEFAULT_WRAPPER_LOG_MAX_BYTES, backups: DEFAULT_WRAPPER_LOG_BACKUPS };
|
|
@@ -339,6 +340,53 @@ function classifyHealth(probe) {
|
|
|
339
340
|
return { mode: "aerial_running", body: probe.body };
|
|
340
341
|
}
|
|
341
342
|
|
|
343
|
+
async function pollForAerialUp(host, port, healthFetch, deadlineMs = HEALTH_START_TIMEOUT_MS) {
|
|
344
|
+
const fetcher = healthFetch || defaultHealthFetch;
|
|
345
|
+
const start = Date.now();
|
|
346
|
+
let lastProbe;
|
|
347
|
+
let lastCls;
|
|
348
|
+
let attempts = 0;
|
|
349
|
+
while (Date.now() - start < deadlineMs) {
|
|
350
|
+
attempts += 1;
|
|
351
|
+
lastProbe = await fetcher(host, port);
|
|
352
|
+
lastCls = classifyHealth(lastProbe);
|
|
353
|
+
if (lastCls.mode === "aerial_running" || lastCls.mode === "port_conflict") {
|
|
354
|
+
return { cls: lastCls, probe: lastProbe, attempts, elapsedMs: Date.now() - start };
|
|
355
|
+
}
|
|
356
|
+
await new Promise((r) => setTimeout(r, HEALTH_POLL_INTERVAL_MS));
|
|
357
|
+
}
|
|
358
|
+
return { cls: lastCls || { mode: "absent" }, probe: lastProbe, attempts, elapsedMs: Date.now() - start };
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function readWrapperNodePath(wrapperFile) {
|
|
362
|
+
if (!wrapperFile) return undefined;
|
|
363
|
+
try {
|
|
364
|
+
if (!fs.existsSync(wrapperFile)) return undefined;
|
|
365
|
+
const data = fs.readFileSync(wrapperFile, "utf8");
|
|
366
|
+
if (wrapperFile.endsWith(".sh")) {
|
|
367
|
+
const m = data.match(/^NODE_BIN='([^']*)'/m);
|
|
368
|
+
return m ? m[1] : undefined;
|
|
369
|
+
}
|
|
370
|
+
if (wrapperFile.endsWith(".ps1")) {
|
|
371
|
+
const m = data.match(/^\$node\s*=\s*'([^']*)'/m);
|
|
372
|
+
return m ? m[1] : undefined;
|
|
373
|
+
}
|
|
374
|
+
} catch {}
|
|
375
|
+
return undefined;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function healthFailedDiagnostics({ wrapper, probe, attempts, elapsedMs }) {
|
|
379
|
+
const out = {
|
|
380
|
+
stdioLog: stdioLogPath(),
|
|
381
|
+
aerialLog: aerialLogPath(),
|
|
382
|
+
wrapperNode: readWrapperNodePath(wrapper),
|
|
383
|
+
health: { attempts, elapsedMs }
|
|
384
|
+
};
|
|
385
|
+
if (probe && probe.error) out.health.lastError = probe.error;
|
|
386
|
+
if (probe && probe.status !== undefined) out.health.lastStatus = probe.status;
|
|
387
|
+
return out;
|
|
388
|
+
}
|
|
389
|
+
|
|
342
390
|
function authFileStatus(file) {
|
|
343
391
|
if (!fs.existsSync(file)) return { file, state: "missing" };
|
|
344
392
|
try {
|
|
@@ -461,11 +509,12 @@ function tokenWarning() {
|
|
|
461
509
|
if (data && data.trim()) return undefined;
|
|
462
510
|
} catch {}
|
|
463
511
|
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
512
|
+
const envToken = process.env.AERIAL_GITHUB_TOKEN;
|
|
513
|
+
const envOnly = envToken && envToken.trim();
|
|
514
|
+
const message = envOnly
|
|
515
|
+
? "AERIAL_GITHUB_TOKEN is set in this shell only; the background service does not inherit it. Run `aerial login` to persist a service-readable GitHub token, otherwise proxy requests return 503."
|
|
516
|
+
: "GitHub token is not configured. Service will start, but proxy requests return 503 until you run `aerial login`.";
|
|
517
|
+
return { level: "warning", code: "github_token_missing", message };
|
|
469
518
|
}
|
|
470
519
|
|
|
471
520
|
function darwinWriteDefinition() {
|
|
@@ -534,7 +583,7 @@ async function describeRunning(ctx, host, port, healthFetch) {
|
|
|
534
583
|
return { cls, probe, supervisor };
|
|
535
584
|
}
|
|
536
585
|
|
|
537
|
-
export async function serviceInstall({ run = defaultRunCommand, healthFetch } = {}) {
|
|
586
|
+
export async function serviceInstall({ run = defaultRunCommand, healthFetch, healthDeadlineMs } = {}) {
|
|
538
587
|
if (isUnsupportedPlatform()) throw unsupportedError("install");
|
|
539
588
|
const ctx = { run };
|
|
540
589
|
const config = loadConfig();
|
|
@@ -634,12 +683,34 @@ export async function serviceInstall({ run = defaultRunCommand, healthFetch } =
|
|
|
634
683
|
if (!ok) result.reason = "run_failed";
|
|
635
684
|
}
|
|
636
685
|
}
|
|
686
|
+
if (result.ok) {
|
|
687
|
+
if (process.env.AERIAL_SERVICE_DRYRUN === "1") {
|
|
688
|
+
result.health = { ok: true, attempts: 0, elapsedMs: 0, dryRun: true };
|
|
689
|
+
} else {
|
|
690
|
+
const poll = await pollForAerialUp(config.host, config.port, healthFetch, healthDeadlineMs);
|
|
691
|
+
if (poll.cls.mode === "aerial_running") {
|
|
692
|
+
result.health = { ok: true, attempts: poll.attempts, elapsedMs: poll.elapsedMs };
|
|
693
|
+
} else if (poll.cls.mode === "port_conflict") {
|
|
694
|
+
result.ok = false;
|
|
695
|
+
result.reason = "port_conflict";
|
|
696
|
+
result.message = `After install, port ${config.port} is responding as a non-Aerial process: ${poll.cls.reason}. Free the port and rerun.`;
|
|
697
|
+
result.diagnostics = healthFailedDiagnostics({ wrapper: result.wrapper, probe: poll.probe, attempts: poll.attempts, elapsedMs: poll.elapsedMs });
|
|
698
|
+
} else {
|
|
699
|
+
result.ok = false;
|
|
700
|
+
result.reason = "health_check_failed";
|
|
701
|
+
result.definitionWritten = true;
|
|
702
|
+
result.startAttempted = true;
|
|
703
|
+
result.message = `Service definition was written and start was triggered, but /health did not become Aerial within ${poll.elapsedMs}ms (${poll.attempts} attempts). Inspect logs and rerun \`aerial service status --json\`.`;
|
|
704
|
+
result.diagnostics = healthFailedDiagnostics({ wrapper: result.wrapper, probe: poll.probe, attempts: poll.attempts, elapsedMs: poll.elapsedMs });
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
637
708
|
result.warning = tokenWarning();
|
|
638
|
-
logEvent("service_install", { platform: process.platform, ok: result.ok });
|
|
709
|
+
logEvent("service_install", { platform: process.platform, ok: result.ok, reason: result.reason });
|
|
639
710
|
return result;
|
|
640
711
|
}
|
|
641
712
|
|
|
642
|
-
export async function serviceStart({ run = defaultRunCommand, healthFetch } = {}) {
|
|
713
|
+
export async function serviceStart({ run = defaultRunCommand, healthFetch, healthDeadlineMs } = {}) {
|
|
643
714
|
if (isUnsupportedPlatform()) throw unsupportedError("start");
|
|
644
715
|
const ctx = { run };
|
|
645
716
|
const config = loadConfig();
|
|
@@ -687,16 +758,50 @@ export async function serviceStart({ run = defaultRunCommand, healthFetch } = {}
|
|
|
687
758
|
} else {
|
|
688
759
|
r = ctx.run("schtasks.exe", buildSchtasksArgs("run"));
|
|
689
760
|
}
|
|
690
|
-
const
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
ok,
|
|
761
|
+
const triggerOk = r.status === 0;
|
|
762
|
+
const base = {
|
|
763
|
+
ok: triggerOk,
|
|
694
764
|
action: "start",
|
|
695
765
|
platform: process.platform,
|
|
696
766
|
status: r.status,
|
|
697
767
|
stderr: r.stderr,
|
|
698
768
|
warning: tokenWarning(),
|
|
699
|
-
...(
|
|
769
|
+
...(triggerOk ? {} : { reason: process.platform === "darwin" ? "bootstrap_failed" : "run_failed" })
|
|
770
|
+
};
|
|
771
|
+
if (!triggerOk) {
|
|
772
|
+
logEvent("service_start", { platform: process.platform, status: r.status, reason: base.reason });
|
|
773
|
+
return base;
|
|
774
|
+
}
|
|
775
|
+
if (process.env.AERIAL_SERVICE_DRYRUN === "1") {
|
|
776
|
+
base.health = { ok: true, attempts: 0, elapsedMs: 0, dryRun: true };
|
|
777
|
+
logEvent("service_start", { platform: process.platform, status: r.status, ok: true, dryRun: true });
|
|
778
|
+
return base;
|
|
779
|
+
}
|
|
780
|
+
const poll = await pollForAerialUp(config.host, config.port, healthFetch, healthDeadlineMs);
|
|
781
|
+
if (poll.cls.mode === "aerial_running") {
|
|
782
|
+
base.health = { ok: true, attempts: poll.attempts, elapsedMs: poll.elapsedMs };
|
|
783
|
+
logEvent("service_start", { platform: process.platform, status: r.status, ok: true });
|
|
784
|
+
return base;
|
|
785
|
+
}
|
|
786
|
+
const wrapper = process.platform === "darwin" ? darwinWrapperPath() : winWrapperPath();
|
|
787
|
+
if (poll.cls.mode === "port_conflict") {
|
|
788
|
+
logEvent("service_start", { platform: process.platform, ok: false, reason: "port_conflict" });
|
|
789
|
+
return {
|
|
790
|
+
...base,
|
|
791
|
+
ok: false,
|
|
792
|
+
reason: "port_conflict",
|
|
793
|
+
message: `After start, port ${config.port} is responding as a non-Aerial process: ${poll.cls.reason}. Free the port and rerun.`,
|
|
794
|
+
diagnostics: healthFailedDiagnostics({ wrapper, probe: poll.probe, attempts: poll.attempts, elapsedMs: poll.elapsedMs })
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
logEvent("service_start", { platform: process.platform, ok: false, reason: "health_check_failed" });
|
|
798
|
+
return {
|
|
799
|
+
...base,
|
|
800
|
+
ok: false,
|
|
801
|
+
reason: "health_check_failed",
|
|
802
|
+
startAttempted: true,
|
|
803
|
+
message: `Start was triggered, but /health did not become Aerial within ${poll.elapsedMs}ms (${poll.attempts} attempts). Inspect logs and rerun \`aerial service status --json\`.`,
|
|
804
|
+
diagnostics: healthFailedDiagnostics({ wrapper, probe: poll.probe, attempts: poll.attempts, elapsedMs: poll.elapsedMs })
|
|
700
805
|
};
|
|
701
806
|
}
|
|
702
807
|
|