@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jiayunxie/aerial",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
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
@@ -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 (!nextModel) return payload;
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
- return {
465
- level: "warning",
466
- code: "github_token_missing",
467
- message: "GitHub token is not configured. Service will start, but proxy requests return 503 until you run `aerial login`."
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 ok = r.status === 0;
691
- logEvent("service_start", { platform: process.platform, status: r.status });
692
- return {
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
- ...(ok ? {} : { reason: process.platform === "darwin" ? "bootstrap_failed" : "run_failed" })
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