proxitor 0.9.0-beta.4 → 0.9.0-beta.5

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/dist/cli.mjs CHANGED
@@ -8,7 +8,7 @@ import { formatWithOptions, styleText } from "node:util";
8
8
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
9
9
  import { dirname, join, resolve, sep } from "node:path";
10
10
  import * as l$1 from "node:readline";
11
- import f from "node:readline";
11
+ import l__default from "node:readline";
12
12
  import { createHash } from "node:crypto";
13
13
  import { createServer } from "node:net";
14
14
  import { STATUS_CODES, createServer as createServer$1 } from "node:http";
@@ -10809,15 +10809,10 @@ function applyOverride(result, override) {
10809
10809
  ...override.headers
10810
10810
  };
10811
10811
  if (override.cacheControl !== void 0) result.cacheControl = override.cacheControl;
10812
- if (override.cacheControlTtl === null) result.cacheControlTtl = void 0;
10813
- else if (override.cacheControlTtl !== void 0) result.cacheControlTtl = override.cacheControlTtl;
10812
+ if (override.cacheControlTtl !== void 0) result.cacheControlTtl = override.cacheControlTtl ?? void 0;
10814
10813
  if (override.sessionId !== void 0) result.sessionId = override.sessionId;
10815
10814
  }
10816
- /**
10817
- * Throw if a URL ends with /v1 — a common misconfiguration after the URL routing change.
10818
- * Paths like /v1/chat/completions are now forwarded as-is, so including /v1 in the base
10819
- * URL would produce doubled paths like /v1/v1/chat/completions.
10820
- */
10815
+ /** Reject base URLs ending in /v1 — paths are forwarded as-is, so /v1 suffix causes doubled paths. */
10821
10816
  function throwIfV1Suffix(url, field) {
10822
10817
  const { pathname } = new URL(url);
10823
10818
  if (pathname.endsWith("/v1") || pathname.endsWith("/v1/")) throw new Error(`${field} "${url}" ends with /v1 — paths are now forwarded as-is, so this would produce doubled paths like /v1/v1/chat/completions. Remove the /v1 suffix (use "${url.replace(/\/v1\/?$/, "")}")`);
@@ -10834,9 +10829,8 @@ async function loadConfig(options) {
10834
10829
  ...options.host !== void 0 ? { host: options.host } : {},
10835
10830
  ...options.port !== void 0 ? { port: options.port } : {},
10836
10831
  ...options.verbose !== void 0 ? { verbose: options.verbose } : {},
10837
- ...options.openrouterKey ? { openrouterKey: options.openrouterKey } : {}
10832
+ openrouterKey: options.openrouterKey || fileConfig.openrouterKey || process.env.OPENROUTER_API_KEY || ""
10838
10833
  };
10839
- if (!merged.openrouterKey) merged.openrouterKey = process.env.OPENROUTER_API_KEY ?? "";
10840
10834
  const result = proxyConfigSchema.safeParse(merged);
10841
10835
  if (!result.success) throw new ConfigValidationError("(merged config)", result.error);
10842
10836
  if (!result.data.openrouterKey) throw new Error("OpenRouter API key is required. Set OPENROUTER_API_KEY env var, pass --openrouter-key flag, or set it in config file.");
@@ -10864,30 +10858,16 @@ const XDG_CONFIG_CANDIDATES = [
10864
10858
  function getConfigSearchPaths() {
10865
10859
  return [...LOCAL_CONFIG_CANDIDATES.map((c) => resolve(c)), ...XDG_CONFIG_CANDIDATES.map((c) => join(getXdgConfigDir(), c))];
10866
10860
  }
10867
- /**
10868
- * Return the path of the first existing config file, or `null` if none was found.
10869
- * Use this when "no config" is a valid outcome (e.g. wizard, doctor, validate).
10870
- */
10861
+ /** Returns first existing config file, or null. Use when "no config" is valid (wizard, doctor, validate). */
10871
10862
  function tryFindConfigFile(explicitPath) {
10872
10863
  if (explicitPath) {
10873
10864
  if (!existsSync(explicitPath)) throw new Error(`Config file not found: ${explicitPath}`);
10874
10865
  return resolve(explicitPath);
10875
10866
  }
10876
- for (const candidate of LOCAL_CONFIG_CANDIDATES) {
10877
- const fullPath = resolve(candidate);
10878
- if (existsSync(fullPath)) return fullPath;
10879
- }
10880
- const xdgDir = getXdgConfigDir();
10881
- for (const candidate of XDG_CONFIG_CANDIDATES) {
10882
- const fullPath = join(xdgDir, candidate);
10883
- if (existsSync(fullPath)) return fullPath;
10884
- }
10867
+ for (const fullPath of getConfigSearchPaths()) if (existsSync(fullPath)) return fullPath;
10885
10868
  return null;
10886
10869
  }
10887
- /**
10888
- * Resolve a config file path, throwing {@link MissingConfigError} if discovery
10889
- * (with no explicit path) fails. Use this when the config is required.
10890
- */
10870
+ /** Like tryFindConfigFile but throws MissingConfigError when no config is found. */
10891
10871
  function findConfigFile(explicitPath) {
10892
10872
  const found = tryFindConfigFile(explicitPath);
10893
10873
  if (found) return found;
@@ -11463,7 +11443,7 @@ var V = class {
11463
11443
  this.state = "cancel", this.close();
11464
11444
  }, { once: true });
11465
11445
  }
11466
- this.rl = f.createInterface({
11446
+ this.rl = l__default.createInterface({
11467
11447
  input: this.input,
11468
11448
  tabSize: 2,
11469
11449
  prompt: "",
@@ -19607,7 +19587,7 @@ const r = Object.create(null), i = (e) => globalThis.process?.env || import.meta
19607
19587
  const e = i(true);
19608
19588
  return Object.keys(e);
19609
19589
  }
19610
- }), t = typeof process < "u" && process.env && process.env.NODE_ENV || "", f$1 = [
19590
+ }), t = typeof process < "u" && process.env && process.env.NODE_ENV || "", f = [
19611
19591
  ["APPVEYOR"],
19612
19592
  [
19613
19593
  "AWS_AMPLIFY",
@@ -19699,7 +19679,7 @@ const r = Object.create(null), i = (e) => globalThis.process?.env || import.meta
19699
19679
  ]
19700
19680
  ];
19701
19681
  function b() {
19702
- if (globalThis.process?.env) for (const e of f$1) {
19682
+ if (globalThis.process?.env) for (const e of f) {
19703
19683
  const s = e[1] || e[0];
19704
19684
  if (globalThis.process?.env[s]) return {
19705
19685
  name: e[0].toLowerCase(),
@@ -21025,25 +21005,21 @@ var OpenRouterClient = class {
21025
21005
  };
21026
21006
  //#endregion
21027
21007
  //#region src/openrouter/data-client.ts
21028
- const OPENROUTER_FALLBACK_URL = OPENROUTER_API_URL;
21029
21008
  const MODELS_CACHE_TTL_MS = 300 * 1e3;
21030
21009
  const modelsCache = /* @__PURE__ */ new Map();
21031
- function isValidProvidersResponse(data) {
21010
+ function isValidArrayDataResponse(data) {
21032
21011
  return typeof data === "object" && data !== null && "data" in data && Array.isArray(data.data);
21033
21012
  }
21013
+ function isValidProvidersResponse(data) {
21014
+ return isValidArrayDataResponse(data);
21015
+ }
21034
21016
  function isValidModelsResponse(data) {
21035
- return typeof data === "object" && data !== null && "data" in data && Array.isArray(data.data);
21017
+ return isValidArrayDataResponse(data);
21036
21018
  }
21037
21019
  function isValidEndpointsResponse(data) {
21038
21020
  return typeof data === "object" && data !== null && "data" in data && typeof data.data === "object" && data.data !== null && "endpoints" in data.data && Array.isArray(data.data.endpoints);
21039
21021
  }
21040
- /**
21041
- * Client for fetching provider/model data with automatic fallback to OpenRouter.
21042
- *
21043
- * When the primary API (openrouterDataUrl or openrouterBaseUrl) doesn't support
21044
- * OpenRouter-specific data endpoints, falls back to https://openrouter.ai/api
21045
- * which hosts public, unauthenticated endpoints for /providers, /models, etc.
21046
- */
21022
+ /** Fetches provider/model data with fallback to OpenRouter. */
21047
21023
  var OpenRouterDataClient = class {
21048
21024
  primaryClient;
21049
21025
  fallbackClient;
@@ -21052,9 +21028,9 @@ var OpenRouterDataClient = class {
21052
21028
  cacheKey;
21053
21029
  constructor(config) {
21054
21030
  const primaryUrl = config.openrouterDataUrl ?? config.openrouterBaseUrl;
21055
- this.skipFallback = primaryUrl === OPENROUTER_FALLBACK_URL;
21031
+ this.skipFallback = primaryUrl === OPENROUTER_API_URL;
21056
21032
  this.primaryClient = new OpenRouterClient(config.apiKey, primaryUrl, config.authType);
21057
- this.fallbackClient = new OpenRouterClient(OPENROUTER_FALLBACK_URL);
21033
+ this.fallbackClient = new OpenRouterClient(OPENROUTER_API_URL);
21058
21034
  this.onFallback = config.onFallback;
21059
21035
  const keyHash = createHash("sha256").update(config.apiKey).digest("hex").slice(0, 8);
21060
21036
  this.cacheKey = `${primaryUrl}|${config.authType}|${keyHash}`;
@@ -21065,7 +21041,6 @@ var OpenRouterDataClient = class {
21065
21041
  async fetchModels() {
21066
21042
  const cached = modelsCache.get(this.cacheKey);
21067
21043
  if (cached && Date.now() - cached.at < MODELS_CACHE_TTL_MS) return cached.models;
21068
- if (cached) modelsCache.delete(this.cacheKey);
21069
21044
  const models = (await this.withFallback("/v1/models", () => this.primaryClient.get("/v1/models"), isValidModelsResponse)).data.data;
21070
21045
  modelsCache.set(this.cacheKey, {
21071
21046
  at: Date.now(),
@@ -21077,10 +21052,7 @@ var OpenRouterDataClient = class {
21077
21052
  const path = `/v1/models/${author}/${slug}/endpoints`;
21078
21053
  return (await this.withFallback(path, () => this.primaryClient.get(path), isValidEndpointsResponse)).data.data.endpoints ?? [];
21079
21054
  }
21080
- /**
21081
- * Try primary, validate response, fallback on failure.
21082
- * Network errors get 1 retry before fallback.
21083
- */
21055
+ /** Primary with retry, then fallback. */
21084
21056
  async withFallback(path, primaryFn, validate) {
21085
21057
  if (this.skipFallback) {
21086
21058
  const data = await primaryFn();
@@ -21118,24 +21090,19 @@ function isNetworkError(error) {
21118
21090
  const message = error.message.toLowerCase();
21119
21091
  return message.includes("fetch") || message.includes("network") || message.includes("econnrefused") || message.includes("enotfound") || message.includes("timeout") || message.includes("aborted") || error.name === "TypeError";
21120
21092
  }
21121
- /**
21122
- * Best-effort probe of the upstream API. Non-blocking — used by the wizard
21123
- * to give early feedback on key validity without preventing save.
21124
- */
21093
+ /** Probes upstream to validate key and count models. */
21125
21094
  async function probeUpstream(baseUrl, apiKey, authType, timeoutMs = 3e3) {
21095
+ if (!apiKey) return {
21096
+ ok: false,
21097
+ reason: "No API key provided"
21098
+ };
21126
21099
  const url = `${baseUrl.replace(/\/$/, "")}/v1/models`;
21127
21100
  const controller = new AbortController();
21128
21101
  const timer = setTimeout(() => controller.abort(), timeoutMs);
21129
21102
  try {
21130
- if (!apiKey) return {
21131
- ok: false,
21132
- reason: "No API key provided"
21133
- };
21134
- const headers = {};
21135
- headers.Authorization = formatAuthHeader(apiKey, authType);
21136
21103
  const res = await fetch(url, {
21137
21104
  signal: controller.signal,
21138
- headers
21105
+ headers: { Authorization: formatAuthHeader(apiKey, authType) }
21139
21106
  });
21140
21107
  if (res.status === 401 || res.status === 403) return {
21141
21108
  ok: false,
@@ -21544,7 +21511,7 @@ async function runConfigMenu(client) {
21544
21511
  }
21545
21512
  //#endregion
21546
21513
  //#region src/version.ts
21547
- const version = "0.9.0-beta.4";
21514
+ const version = "0.9.0-beta.5";
21548
21515
  //#endregion
21549
21516
  //#region src/commands/doctor.ts
21550
21517
  const DEFAULT_TIMEOUT_MS = 3e3;
@@ -24515,6 +24482,19 @@ function filterHeaders(incoming, blocklist) {
24515
24482
  }
24516
24483
  return headers;
24517
24484
  }
24485
+ /**
24486
+ * Canonicalize a header record to lowercase keys.
24487
+ *
24488
+ * HTTP header names are case-insensitive (RFC 9110 §5.1), but a plain object
24489
+ * treats `Content-Type` and `content-type` as distinct keys. Lowercasing folds
24490
+ * case-variant keys into one so the merged record can never carry two headers
24491
+ * that differ only by case. Returns a new object; does not mutate the input.
24492
+ */
24493
+ function lowercaseKeys(record) {
24494
+ const result = {};
24495
+ for (const [key, value] of Object.entries(record)) result[key.toLowerCase()] = value;
24496
+ return result;
24497
+ }
24518
24498
  /** Filter response headers and add SSE-friendly defaults */
24519
24499
  function buildResponseHeaders(from) {
24520
24500
  const headers = filterHeaders(from, STRIP_RESPONSE);
@@ -24524,48 +24504,65 @@ function buildResponseHeaders(from) {
24524
24504
  }
24525
24505
  //#endregion
24526
24506
  //#region src/proxy/middleware/build-upstream-req.ts
24507
+ const encoder = new TextEncoder();
24527
24508
  const PROTO_POLLUTION_KEYS = new Set([
24528
24509
  "__proto__",
24529
24510
  "constructor",
24530
24511
  "prototype"
24531
24512
  ]);
24532
- function resolveForwardBody(c) {
24533
- if (c.var.bodyMutated && c.var.parsedBody) try {
24534
- return new TextEncoder().encode(JSON.stringify(c.var.parsedBody)).buffer;
24513
+ function resolveForwardBody(opts) {
24514
+ if (opts.bodyMutated && opts.parsedBody) try {
24515
+ return encoder.encode(JSON.stringify(opts.parsedBody)).buffer;
24535
24516
  } catch (err) {
24536
- logger.warn(withReq(c.var.reqId, `Failed to re-serialize mutated body (${err instanceof Error ? err.message : "unknown"}); forwarding raw body as-is`));
24537
- return c.var.rawBody;
24517
+ logger.warn(withReq(opts.reqId, `Failed to re-serialize mutated body (${err instanceof Error ? err.message : "unknown"}); forwarding raw body as-is`));
24518
+ return opts.rawBody;
24538
24519
  }
24539
- return c.var.rawBody;
24520
+ return opts.rawBody;
24540
24521
  }
24541
- function applyProxyHeaders(headers, config) {
24542
- headers.Authorization = formatAuthHeader(config.openrouterKey, config.authType);
24543
- headers["HTTP-Referer"] = config.attributionReferer;
24544
- headers["X-OpenRouter-Title"] = config.attributionTitle;
24545
- headers["Accept-Encoding"] = "identity";
24522
+ function proxyHeaders(config) {
24523
+ return {
24524
+ Authorization: formatAuthHeader(config.openrouterKey, config.authType),
24525
+ "HTTP-Referer": config.attributionReferer,
24526
+ "X-OpenRouter-Title": config.attributionTitle,
24527
+ "Accept-Encoding": "identity"
24528
+ };
24546
24529
  }
24547
- function applyExtraHeaders(headers, extraHeaders) {
24548
- if (!extraHeaders) return;
24530
+ function sanitizeExtraHeaders(extraHeaders) {
24531
+ const safe = {};
24532
+ if (!extraHeaders) return safe;
24549
24533
  for (const [key, value] of Object.entries(extraHeaders)) {
24550
24534
  if (PROTO_POLLUTION_KEYS.has(key)) continue;
24551
- headers[key] = value;
24535
+ safe[key] = value;
24552
24536
  }
24537
+ return safe;
24538
+ }
24539
+ function withSessionId(headers, sessionId) {
24540
+ const { "x-claude-code-session-id": _omit, ...rest } = headers;
24541
+ return {
24542
+ ...rest,
24543
+ "x-session-id": sessionId
24544
+ };
24553
24545
  }
24554
- function forceJsonContentType(headers) {
24555
- for (const key of Object.keys(headers)) if (key.toLowerCase() === "content-type") delete headers[key];
24556
- headers["Content-Type"] = "application/json";
24546
+ function withJsonContentType(headers) {
24547
+ return {
24548
+ ...headers,
24549
+ "content-type": "application/json"
24550
+ };
24557
24551
  }
24558
24552
  const buildUpstreamReq = createMiddleware(async (c, next) => {
24559
- c.set("forwardBody", resolveForwardBody(c));
24560
- const headers = filterHeaders(c.req.raw.headers, STRIP_REQUEST);
24561
- applyProxyHeaders(headers, c.var.config);
24562
- applyExtraHeaders(headers, c.var.resolvedConfig.headers);
24563
- if (c.var.effectiveSessionId !== void 0) {
24564
- delete headers["x-claude-code-session-id"];
24565
- delete headers["x-session-id"];
24566
- headers["x-session-id"] = c.var.effectiveSessionId;
24567
- }
24568
- if (c.var.bodyMutated) forceJsonContentType(headers);
24553
+ c.set("forwardBody", resolveForwardBody({
24554
+ reqId: c.var.reqId,
24555
+ bodyMutated: c.var.bodyMutated,
24556
+ parsedBody: c.var.parsedBody,
24557
+ rawBody: c.var.rawBody
24558
+ }));
24559
+ let headers = lowercaseKeys({
24560
+ ...filterHeaders(c.req.raw.headers, STRIP_REQUEST),
24561
+ ...proxyHeaders(c.var.config),
24562
+ ...sanitizeExtraHeaders(c.var.resolvedConfig.headers)
24563
+ });
24564
+ if (c.var.effectiveSessionId !== void 0) headers = withSessionId(headers, c.var.effectiveSessionId);
24565
+ if (c.var.bodyMutated) headers = withJsonContentType(headers);
24569
24566
  c.set("upstreamHeaders", headers);
24570
24567
  await next();
24571
24568
  });
@@ -24687,10 +24684,7 @@ function buildUpstreamResponseWithLogging(upstream, method, reqId) {
24687
24684
  }
24688
24685
  //#endregion
24689
24686
  //#region src/proxy/utils/error.ts
24690
- /**
24691
- * OpenRouter error format:
24692
- * { error: { code, message, metadata: { raw, provider_name } } }
24693
- */
24687
+ /** OpenRouter error: { error: { code, message, metadata: { raw, provider_name } } } */
24694
24688
  function formatMetadata(meta) {
24695
24689
  const parts = [];
24696
24690
  if (meta.provider_name) parts.push(`provider=${meta.provider_name}`);
@@ -24718,6 +24712,7 @@ function extractErrorDetail(bodyText) {
24718
24712
  }
24719
24713
  //#endregion
24720
24714
  //#region src/proxy/middleware/forward-request.ts
24715
+ const DUPLEX_HALF = { duplex: "half" };
24721
24716
  function buildErrorResponse(err, ctx) {
24722
24717
  if (err instanceof TypeError) {
24723
24718
  logger.error(withReq(ctx.reqId, "Upstream fetch error:"), err);
@@ -24754,14 +24749,14 @@ const forwardRequest = createMiddleware(async (c) => {
24754
24749
  method,
24755
24750
  path,
24756
24751
  startedAt,
24757
- upstreamShort: upstreamUrl.replace(/^https?:\/\//, ""),
24758
- modelLog: c.var.modelName ? ` model=${c.var.modelName}` : "",
24759
24752
  bodyMutated: c.var.bodyMutated
24760
24753
  };
24761
24754
  const controller = new AbortController();
24762
24755
  const onClientAbort = () => controller.abort();
24763
24756
  c.req.raw.signal.addEventListener("abort", onClientAbort);
24764
- logger.info(withReq(reqId, `${method} ${path} → ${ctx.upstreamShort}${ctx.bodyMutated ? " [inject]" : ""}${ctx.modelLog}`));
24757
+ const upstreamShort = upstreamUrl.replace(/^https?:\/\//, "");
24758
+ const modelLog = c.var.modelName ? ` model=${c.var.modelName}` : "";
24759
+ logger.info(withReq(reqId, `${method} ${path} → ${upstreamShort}${ctx.bodyMutated ? " [inject]" : ""}${modelLog}`));
24765
24760
  let upstream;
24766
24761
  try {
24767
24762
  upstream = await fetch(upstreamUrl, {
@@ -24769,7 +24764,7 @@ const forwardRequest = createMiddleware(async (c) => {
24769
24764
  headers: upstreamHeaders,
24770
24765
  body: forwardBody,
24771
24766
  signal: controller.signal,
24772
- ...forwardBody ? { duplex: "half" } : {}
24767
+ ...forwardBody ? DUPLEX_HALF : {}
24773
24768
  });
24774
24769
  } catch (err) {
24775
24770
  return buildErrorResponse(err, ctx);
@@ -24813,16 +24808,11 @@ function shouldInjectCacheControl(mode, modelName, path) {
24813
24808
  if (mode === "always") return true;
24814
24809
  return isAnthropicEndpoint(modelName, path);
24815
24810
  }
24816
- /**
24817
- * Build cache_control value for injection.
24818
- * Merges existing cache_control with configured TTL.
24819
- * If TTL is configured and the endpoint is Anthropic, it always overrides.
24820
- */
24821
24811
  function buildCacheControl(existing, ttl, isAnthropic) {
24822
- const result = existing !== null && typeof existing === "object" && !Array.isArray(existing) ? { ...existing } : { type: "ephemeral" };
24823
- if (!("type" in result)) result.type = "ephemeral";
24824
- if (ttl && isAnthropic) result.ttl = ttl;
24825
- return result;
24812
+ const base = existing !== null && typeof existing === "object" && !Array.isArray(existing) ? { ...existing } : {};
24813
+ if (!("type" in base)) base.type = "ephemeral";
24814
+ if (ttl && isAnthropic) base.ttl = ttl;
24815
+ return base;
24826
24816
  }
24827
24817
  //#endregion
24828
24818
  //#region src/proxy/middleware/inject-cache-control.ts
@@ -24860,6 +24850,18 @@ const injectProvider = createMiddleware(async (c, next) => {
24860
24850
  //#endregion
24861
24851
  //#region src/proxy/utils/session-id.ts
24862
24852
  const PROXY_SESSION_ID = crypto.randomUUID();
24853
+ function firstContent(messages, ...roles) {
24854
+ if (!Array.isArray(messages)) return void 0;
24855
+ return messages.find((m) => roles.includes(m.role ?? "") && m.content != null)?.content;
24856
+ }
24857
+ /** Returns '' for non-serializable values (e.g. BigInt). */
24858
+ function safeStringify(value) {
24859
+ try {
24860
+ return JSON.stringify(value);
24861
+ } catch {
24862
+ return "";
24863
+ }
24864
+ }
24863
24865
  function extractConversationFingerprint(parsedBody, path) {
24864
24866
  let system;
24865
24867
  let user;
@@ -24868,30 +24870,15 @@ function extractConversationFingerprint(parsedBody, path) {
24868
24870
  system = parsedBody.instructions;
24869
24871
  user = parsedBody.input;
24870
24872
  break;
24871
- case "messages": {
24873
+ case "messages":
24872
24874
  system = parsedBody.system;
24873
- const messages = parsedBody.messages;
24874
- if (Array.isArray(messages)) user = messages.find((m) => m.role === "user" && m.content != null)?.content;
24875
+ user = firstContent(parsedBody.messages, "user");
24875
24876
  break;
24876
- }
24877
- default: {
24878
- const messages = parsedBody.messages;
24879
- if (Array.isArray(messages)) {
24880
- const firstSystem = messages.find((m) => (m.role === "system" || m.role === "developer") && m.content != null);
24881
- const firstUser = messages.find((m) => m.role === "user" && m.content != null);
24882
- system = firstSystem?.content;
24883
- user = firstUser?.content;
24884
- }
24885
- }
24877
+ default:
24878
+ system = firstContent(parsedBody.messages, "system", "developer");
24879
+ user = firstContent(parsedBody.messages, "user");
24886
24880
  }
24887
24881
  if (system == null && user == null) return null;
24888
- const safeStringify = (value) => {
24889
- try {
24890
- return JSON.stringify(value);
24891
- } catch {
24892
- return "";
24893
- }
24894
- };
24895
24882
  const hash = createHash("sha256");
24896
24883
  hash.update(String(parsedBody.model ?? ""));
24897
24884
  if (system != null) hash.update(safeStringify(system));
@@ -25065,14 +25052,10 @@ function startProxyServer(config, onReady) {
25065
25052
  //#endregion
25066
25053
  //#region src/cli-commands.ts
25067
25054
  /**
25068
- * Command tree for the proxitor CLI.
25069
- *
25070
- * Kept in a separate module from `cli.ts` so tests can import the commands
25071
- * without triggering the top-level `run()` invocation that `cli.ts` performs
25072
- * on import.
25055
+ * CLI command tree. Separated from `cli.ts` so tests can import commands
25056
+ * without triggering `run()`.
25073
25057
  */
25074
- /** Flags that every command touching the config file accepts. */
25075
- const configArgs$1 = {
25058
+ const configArgs = {
25076
25059
  configPath: (0, import_cjs.option)({
25077
25060
  long: "config",
25078
25061
  short: "c",
@@ -25087,16 +25070,14 @@ const configArgs$1 = {
25087
25070
  description: "OpenRouter API key (overrides config file & env)"
25088
25071
  })
25089
25072
  };
25090
- /** `--json` flag for commands that can produce structured output. */
25091
25073
  const jsonFlag = { json: (0, import_cjs.flag)({
25092
25074
  long: "json",
25093
25075
  description: "Output as JSON instead of formatted text"
25094
25076
  }) };
25095
- /** Build an OpenRouterDataClient from already-parsed args. */
25096
25077
  async function makeClient(args) {
25097
25078
  const cfg = await loadConfig({
25098
- configPath: args.configPath ?? void 0,
25099
- openrouterKey: args.openrouterKey ?? void 0
25079
+ configPath: args.configPath,
25080
+ openrouterKey: args.openrouterKey
25100
25081
  });
25101
25082
  return new OpenRouterDataClient({
25102
25083
  openrouterBaseUrl: cfg.openrouterBaseUrl,
@@ -25136,7 +25117,7 @@ const startCommand = (0, import_cjs.command)({
25136
25117
  }
25137
25118
  ],
25138
25119
  args: {
25139
- configPath: configArgs$1.configPath,
25120
+ configPath: configArgs.configPath,
25140
25121
  port: (0, import_cjs.option)({
25141
25122
  long: "port",
25142
25123
  short: "p",
@@ -25156,7 +25137,7 @@ const startCommand = (0, import_cjs.command)({
25156
25137
  long: "no-config",
25157
25138
  description: "Skip config file discovery"
25158
25139
  }),
25159
- openrouterKey: configArgs$1.openrouterKey,
25140
+ openrouterKey: configArgs.openrouterKey,
25160
25141
  verbose: (0, import_cjs.flag)({
25161
25142
  long: "verbose",
25162
25143
  description: "Enable verbose logging"
@@ -25165,11 +25146,11 @@ const startCommand = (0, import_cjs.command)({
25165
25146
  handler: async ({ configPath, port, host, noConfig, openrouterKey, verbose }) => {
25166
25147
  try {
25167
25148
  const cfg = await loadConfig({
25168
- configPath: configPath ?? void 0,
25149
+ configPath,
25169
25150
  noConfig,
25170
25151
  port,
25171
25152
  host,
25172
- openrouterKey: openrouterKey ?? void 0,
25153
+ openrouterKey,
25173
25154
  verbose
25174
25155
  });
25175
25156
  startProxyServer(cfg, () => {
@@ -25207,7 +25188,7 @@ const configCli = (0, import_cjs.subcommands)({
25207
25188
  add: (0, import_cjs.command)({
25208
25189
  name: "add",
25209
25190
  description: "Add a model override (interactive)",
25210
- args: { ...configArgs$1 },
25191
+ args: { ...configArgs },
25211
25192
  handler: async (args) => {
25212
25193
  await addOverrideCommand({
25213
25194
  client: await makeClient(args),
@@ -25218,7 +25199,7 @@ const configCli = (0, import_cjs.subcommands)({
25218
25199
  edit: (0, import_cjs.command)({
25219
25200
  name: "edit",
25220
25201
  description: "Edit an existing model override (interactive)",
25221
- args: { ...configArgs$1 },
25202
+ args: { ...configArgs },
25222
25203
  handler: async (args) => {
25223
25204
  await editOverrideCommand(await makeClient(args), args.configPath);
25224
25205
  }
@@ -25226,7 +25207,7 @@ const configCli = (0, import_cjs.subcommands)({
25226
25207
  remove: (0, import_cjs.command)({
25227
25208
  name: "remove",
25228
25209
  description: "Remove one or more model overrides (interactive)",
25229
- args: { ...configArgs$1 },
25210
+ args: { ...configArgs },
25230
25211
  handler: async (args) => {
25231
25212
  await removeOverrideCommand({ configPath: args.configPath });
25232
25213
  }
@@ -25235,7 +25216,7 @@ const configCli = (0, import_cjs.subcommands)({
25235
25216
  name: "list",
25236
25217
  description: "List all model overrides",
25237
25218
  args: {
25238
- ...configArgs$1,
25219
+ ...configArgs,
25239
25220
  ...jsonFlag
25240
25221
  },
25241
25222
  handler: async (args) => {
@@ -25248,7 +25229,7 @@ const configCli = (0, import_cjs.subcommands)({
25248
25229
  browse: (0, import_cjs.command)({
25249
25230
  name: "browse",
25250
25231
  description: "Browse OpenRouter models (interactive)",
25251
- args: { ...configArgs$1 },
25232
+ args: { ...configArgs },
25252
25233
  handler: async (args) => {
25253
25234
  await browseModelsCommand(await makeClient(args));
25254
25235
  }
@@ -25257,7 +25238,7 @@ const configCli = (0, import_cjs.subcommands)({
25257
25238
  name: "validate",
25258
25239
  description: "Validate the current config (exit 0 ok, 1 invalid)",
25259
25240
  args: {
25260
- ...configArgs$1,
25241
+ ...configArgs,
25261
25242
  ...jsonFlag
25262
25243
  },
25263
25244
  handler: async (args) => {
@@ -25272,7 +25253,7 @@ const configCli = (0, import_cjs.subcommands)({
25272
25253
  name: "show",
25273
25254
  description: "Show resolved configuration (merged from defaults + file + env + flags)",
25274
25255
  args: {
25275
- ...configArgs$1,
25256
+ ...configArgs,
25276
25257
  ...jsonFlag
25277
25258
  },
25278
25259
  handler: async (args) => {
@@ -25286,7 +25267,7 @@ const configCli = (0, import_cjs.subcommands)({
25286
25267
  wizard: (0, import_cjs.command)({
25287
25268
  name: "wizard",
25288
25269
  description: "Run interactive setup wizard",
25289
- args: { ...configArgs$1 },
25270
+ args: { ...configArgs },
25290
25271
  handler: async (args) => {
25291
25272
  await runWizard({ configPath: args.configPath });
25292
25273
  }
@@ -25294,7 +25275,7 @@ const configCli = (0, import_cjs.subcommands)({
25294
25275
  menu: (0, import_cjs.command)({
25295
25276
  name: "menu",
25296
25277
  description: "Open interactive configuration menu",
25297
- args: { ...configArgs$1 },
25278
+ args: { ...configArgs },
25298
25279
  handler: async (args) => {
25299
25280
  await runConfigMenu(await makeClient(args));
25300
25281
  }
@@ -25319,10 +25300,7 @@ const doctorCli = (0, import_cjs.command)({
25319
25300
  }
25320
25301
  ],
25321
25302
  args: {
25322
- json: (0, import_cjs.flag)({
25323
- long: "json",
25324
- description: "Output as JSON instead of formatted text"
25325
- }),
25303
+ ...jsonFlag,
25326
25304
  offline: (0, import_cjs.flag)({
25327
25305
  long: "offline",
25328
25306
  description: "Skip network checks (upstream, npm)"
@@ -25356,8 +25334,14 @@ const rootCli = (0, import_cjs.subcommands)({
25356
25334
  });
25357
25335
  //#endregion
25358
25336
  //#region src/cli.ts
25337
+ const INFO_FLAGS = [
25338
+ "--help",
25339
+ "-h",
25340
+ "--version",
25341
+ "-v"
25342
+ ];
25359
25343
  const userArgs = process.argv.slice(2);
25360
- const isInfo = userArgs.includes("--help") || userArgs.includes("-h") || userArgs.includes("--version") || userArgs.includes("-v");
25344
+ const isInfo = userArgs.some((a) => INFO_FLAGS.includes(a));
25361
25345
  if (!isInfo) (0, import_main.config)({ quiet: true });
25362
25346
  async function handleStartupError(err) {
25363
25347
  if (err instanceof MissingConfigError && process.stdin.isTTY) {
@@ -25399,17 +25383,11 @@ const finalArgv = needsDefault && !isInfo ? [
25399
25383
  "proxitor",
25400
25384
  "start",
25401
25385
  ...userArgs
25402
- ] : [...process.argv];
25403
- const configArgs = !needsDefault && firstNonFlag === "config" ? userArgs.slice(userArgs.indexOf("config") + 1) : [];
25404
- const configSub = configArgs.find((a) => !a.startsWith("-"));
25405
- const configHasInfo = configArgs.some((a) => [
25406
- "--help",
25407
- "-h",
25408
- "--version",
25409
- "-v"
25410
- ].includes(a));
25411
- if (!needsDefault && firstNonFlag === "config" && !configHasInfo) {
25412
- if (!configSub || !KNOWN_CONFIG_SUBS.has(configSub)) finalArgv.splice(finalArgv.lastIndexOf("config") + 1, 0, "menu");
25386
+ ] : process.argv;
25387
+ if (!needsDefault && firstNonFlag === "config") {
25388
+ const configArgs = userArgs.slice(userArgs.indexOf("config") + 1);
25389
+ const configSub = configArgs.find((a) => !a.startsWith("-"));
25390
+ if (!configArgs.some((a) => INFO_FLAGS.includes(a)) && (!configSub || !KNOWN_CONFIG_SUBS.has(configSub))) finalArgv.splice(finalArgv.lastIndexOf("config") + 1, 0, "menu");
25413
25391
  }
25414
25392
  (0, import_cjs.run)((0, import_cjs.binary)(rootCli), finalArgv).catch((err) => void handleStartupError(err));
25415
25393
  //#endregion