proxitor 0.9.0-beta.2 → 0.9.0-beta.3

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
@@ -19924,10 +19924,6 @@ function withReq(id, message) {
19924
19924
  }
19925
19925
  //#endregion
19926
19926
  //#region src/commands/config/config.ts
19927
- /**
19928
- * Throws {@link MissingConfigError} via `findConfigFile` if no config is found.
19929
- * Pass an explicit path to skip discovery and use that file directly.
19930
- */
19931
19927
  function requireConfigPath(explicitPath) {
19932
19928
  return findConfigFile(explicitPath);
19933
19929
  }
@@ -19937,6 +19933,22 @@ function readConfigRaw(path) {
19937
19933
  function writeConfigRaw(path, content) {
19938
19934
  writeFileSync(path, content, "utf-8");
19939
19935
  }
19936
+ function setGlobalConfigField(configPath, field, value) {
19937
+ const doc = (0, import_dist.parseDocument)(readConfigRaw(configPath));
19938
+ if (value === void 0) doc.delete(field);
19939
+ else doc.set(field, value);
19940
+ writeConfigRaw(configPath, doc.toString());
19941
+ }
19942
+ /**
19943
+ * Atomic batch write — updates multiple fields in a single read-parse-write
19944
+ * cycle, eliminating the risk of partial state on crash or Ctrl-C.
19945
+ */
19946
+ function setGlobalConfigFields(configPath, fields) {
19947
+ const doc = (0, import_dist.parseDocument)(readConfigRaw(configPath));
19948
+ for (const [key, value] of Object.entries(fields)) if (value === void 0) doc.delete(key);
19949
+ else doc.set(key, value);
19950
+ writeConfigRaw(configPath, doc.toString());
19951
+ }
19940
19952
  function setModelOverride(configPath, modelKey, override) {
19941
19953
  const doc = (0, import_dist.parseDocument)(readConfigRaw(configPath));
19942
19954
  let overrides = doc.get("modelOverrides");
@@ -20173,6 +20185,206 @@ async function selectIgnoreProviders(providerOptions) {
20173
20185
  return { provider: { ignore: values.length === 1 ? values[0] : values } };
20174
20186
  }
20175
20187
  //#endregion
20188
+ //#region src/commands/config/prompts.ts
20189
+ function maskKey(key) {
20190
+ if (!key) return "(none)";
20191
+ if (key.length <= 11) return "****";
20192
+ return `${key.slice(0, 7)}…${key.slice(-4)}`;
20193
+ }
20194
+ async function askApiKey(currentKey) {
20195
+ if (currentKey) log.info(`Current key: ${maskKey(currentKey)}`);
20196
+ const apiKey = await text({
20197
+ message: "OpenRouter API key",
20198
+ placeholder: currentKey ? "Press Enter to keep current key" : "sk-or-v1-...",
20199
+ validate: (v) => {
20200
+ if (!v?.trim() && !currentKey) return "API key is required";
20201
+ }
20202
+ });
20203
+ if (isCancel(apiKey)) return null;
20204
+ note("You can also set the OPENROUTER_API_KEY environment variable\nto avoid storing the key in the config file.", "Tip");
20205
+ return apiKey.trim() || currentKey;
20206
+ }
20207
+ async function askPort(current) {
20208
+ const input = await text({
20209
+ message: "Proxy port",
20210
+ initialValue: String(current),
20211
+ placeholder: String(DEFAULTS.port),
20212
+ validate: (v) => {
20213
+ if (!v?.trim()) return void 0;
20214
+ const n = Number.parseInt(v, 10);
20215
+ if (Number.isNaN(n) || n < 1 || n > 65535) return "Port must be 1–65535";
20216
+ }
20217
+ });
20218
+ if (isCancel(input)) return null;
20219
+ return input.trim() ? Number.parseInt(input, 10) : DEFAULTS.port;
20220
+ }
20221
+ async function askBaseUrl(current) {
20222
+ const url = await text({
20223
+ message: "OpenRouter API base URL",
20224
+ placeholder: DEFAULTS.openrouterBaseUrl,
20225
+ initialValue: current === DEFAULTS.openrouterBaseUrl ? "" : current,
20226
+ validate: (v) => {
20227
+ if (!v?.trim()) return void 0;
20228
+ try {
20229
+ if (!new URL(v.trim()).protocol.startsWith("http")) return "URL must start with http:// or https://";
20230
+ } catch {
20231
+ return "Invalid URL";
20232
+ }
20233
+ }
20234
+ });
20235
+ if (isCancel(url)) return null;
20236
+ return url.trim() || DEFAULTS.openrouterBaseUrl;
20237
+ }
20238
+ async function askAuthType(current) {
20239
+ const authType = await select({
20240
+ message: "Authentication type",
20241
+ initialValue: current,
20242
+ options: [{
20243
+ value: "bearer",
20244
+ label: "Bearer token",
20245
+ hint: "Standard OpenRouter"
20246
+ }, {
20247
+ value: "oauth",
20248
+ label: "OAuth token",
20249
+ hint: "Custom proxy providers"
20250
+ }]
20251
+ });
20252
+ if (isCancel(authType)) return null;
20253
+ return authType;
20254
+ }
20255
+ async function askHost(current) {
20256
+ const isPreset = (v) => v === "0.0.0.0" || v === "127.0.0.1";
20257
+ const host = await select({
20258
+ message: "Listen address",
20259
+ initialValue: isPreset(current) ? current : "__custom__",
20260
+ options: [
20261
+ {
20262
+ value: "0.0.0.0",
20263
+ label: "All interfaces (0.0.0.0)",
20264
+ hint: "Default"
20265
+ },
20266
+ {
20267
+ value: "127.0.0.1",
20268
+ label: "Localhost only (127.0.0.1)",
20269
+ hint: "More secure"
20270
+ },
20271
+ {
20272
+ value: "__custom__",
20273
+ label: "Custom address…",
20274
+ hint: "Specific IP, hostname, or unix:/path"
20275
+ }
20276
+ ]
20277
+ });
20278
+ if (isCancel(host)) return null;
20279
+ if (host === "__custom__") {
20280
+ const custom = await text({
20281
+ message: "Custom listen address",
20282
+ placeholder: "192.168.1.1",
20283
+ initialValue: isPreset(current) ? "" : current,
20284
+ validate: (v) => {
20285
+ const t = v?.trim();
20286
+ if (!t) return "Address is required";
20287
+ if (t.startsWith("unix:")) return void 0;
20288
+ if (!/^[\w.\-:]+$/.test(t)) return "Invalid host (allowed: IP, hostname, or unix:…)";
20289
+ }
20290
+ });
20291
+ if (isCancel(custom)) return null;
20292
+ return custom.trim();
20293
+ }
20294
+ return host;
20295
+ }
20296
+ /** Shared hint texts for session routing tri-state — used in add, edit, session-routing. */
20297
+ const SESSION_HINTS = {
20298
+ auto: "Passthrough client ID, generate if missing",
20299
+ always: "Always generate proxy session ID",
20300
+ never: "Don't manage session headers"
20301
+ };
20302
+ /** Shared hint texts for cache control tri-state — used in add, edit, cache-control. */
20303
+ const CACHE_HINTS = {
20304
+ auto: "Anthropic models only",
20305
+ always: "All models",
20306
+ never: "Off"
20307
+ };
20308
+ async function askTriState(message, current, hints) {
20309
+ const result = await select({
20310
+ message,
20311
+ initialValue: current ?? "auto",
20312
+ options: [
20313
+ {
20314
+ value: "auto",
20315
+ label: "auto",
20316
+ hint: hints.auto
20317
+ },
20318
+ {
20319
+ value: "always",
20320
+ label: "always",
20321
+ hint: hints.always
20322
+ },
20323
+ {
20324
+ value: "never",
20325
+ label: "never",
20326
+ hint: hints.never
20327
+ }
20328
+ ]
20329
+ });
20330
+ if (isCancel(result)) return null;
20331
+ return result;
20332
+ }
20333
+ async function askCacheControlTtl(current) {
20334
+ const options = [{
20335
+ value: "5m",
20336
+ label: "5 minutes",
20337
+ hint: "Anthropic default"
20338
+ }, {
20339
+ value: "1h",
20340
+ label: "1 hour",
20341
+ hint: "Higher write cost"
20342
+ }];
20343
+ if (current) options.push({
20344
+ value: "reset",
20345
+ label: "Reset to default",
20346
+ hint: "Remove TTL override"
20347
+ });
20348
+ const result = await select({
20349
+ message: "Cache TTL",
20350
+ initialValue: current ?? "5m",
20351
+ options
20352
+ });
20353
+ if (isCancel(result)) return null;
20354
+ if (result === "reset") return "reset";
20355
+ return result;
20356
+ }
20357
+ //#endregion
20358
+ //#region src/commands/config/tri-state.ts
20359
+ /**
20360
+ * Prompt the user for a session routing mode.
20361
+ * Returns the updated override (with `sessionId` set) or `null` on cancel.
20362
+ */
20363
+ async function collectSessionTriState(currentSid) {
20364
+ const sid = await askTriState("Session ID mode", currentSid ?? "auto", SESSION_HINTS);
20365
+ if (sid === null) return null;
20366
+ return { sessionId: sid };
20367
+ }
20368
+ /**
20369
+ * Prompt the user for a cache control mode and optional TTL.
20370
+ * Returns the updated fields or `null` on tri-state cancel.
20371
+ *
20372
+ * **TTL cancel behaviour**: when the user presses Escape on the TTL prompt
20373
+ * the existing TTL is preserved (not silently dropped).
20374
+ */
20375
+ async function collectCacheTriState(currentCc, currentTtl) {
20376
+ const cc = await askTriState("Cache control mode", currentCc ?? "auto", CACHE_HINTS);
20377
+ if (cc === null) return null;
20378
+ const result = { cacheControl: cc };
20379
+ if (cc !== "never") {
20380
+ const ttl = await askCacheControlTtl(currentTtl ?? void 0);
20381
+ if (ttl === null) {
20382
+ if (currentTtl) result.cacheControlTtl = currentTtl;
20383
+ } else if (ttl !== "reset") result.cacheControlTtl = ttl;
20384
+ }
20385
+ return result;
20386
+ }
20387
+ //#endregion
20176
20388
  //#region src/commands/config/add.ts
20177
20389
  const CUSTOM_PATTERN_VALUE = "__proxitor_custom_pattern__";
20178
20390
  /** Run the interactive "Add model override" flow. */
@@ -20262,18 +20474,49 @@ async function enterPattern(models) {
20262
20474
  async function configureProviderAndSave(configPath, client, modelKey, isPattern) {
20263
20475
  const mode = await selectRoutingMode("Configure provider routing");
20264
20476
  if (isCancel(mode)) return;
20477
+ let override = {};
20265
20478
  if (mode === "skip") {
20266
- if (!await confirmAndSave(configPath, modelKey, {}, client)) return;
20267
- outro("✓ Override saved (no provider routing)");
20479
+ if (!await confirmAndSave(configPath, modelKey, override, client)) return;
20480
+ outro("✓ Model override saved (no provider routing)");
20268
20481
  return;
20269
20482
  }
20270
20483
  const providerOptions = await fetchProvidersForModel(client, modelKey, isPattern);
20271
20484
  if (!providerOptions) return;
20272
- const override = await selectProvidersByMode(mode, providerOptions);
20273
- if (!override) return;
20485
+ const providerResult = await selectProvidersByMode(mode, providerOptions);
20486
+ if (!providerResult) return;
20487
+ override = providerResult;
20488
+ override = await collectSessionAndCache(override);
20274
20489
  if (!await confirmAndSave(configPath, modelKey, override, client)) return;
20275
20490
  outro("✓ Model override saved");
20276
20491
  }
20492
+ async function collectSessionAndCache(override) {
20493
+ override = await collectSession(override);
20494
+ override = await collectCache(override);
20495
+ return override;
20496
+ }
20497
+ async function collectSession(override) {
20498
+ const want = await confirm({
20499
+ message: "Configure session routing for this model?",
20500
+ initialValue: false
20501
+ });
20502
+ if (isCancel(want) || !want) return override;
20503
+ const result = await collectSessionTriState();
20504
+ if (result) override.sessionId = result.sessionId;
20505
+ return override;
20506
+ }
20507
+ async function collectCache(override) {
20508
+ const want = await confirm({
20509
+ message: "Configure cache control for this model?",
20510
+ initialValue: false
20511
+ });
20512
+ if (isCancel(want) || !want) return override;
20513
+ const result = await collectCacheTriState();
20514
+ if (result) {
20515
+ override.cacheControl = result.cacheControl;
20516
+ if (result.cacheControlTtl) override.cacheControlTtl = result.cacheControlTtl;
20517
+ }
20518
+ return override;
20519
+ }
20277
20520
  /**
20278
20521
  * Show the proposed override and let the user Save / Test (dry-run) / Cancel.
20279
20522
  * Returns true if the override was saved.
@@ -20330,6 +20573,9 @@ function formatOverrideYaml(override) {
20330
20573
  const p = override.provider;
20331
20574
  for (const [key, value] of Object.entries(p)) parts.push(`provider.${key}: ${JSON.stringify(value)}`);
20332
20575
  }
20576
+ if (override.sessionId) parts.push(`sessionId: ${override.sessionId}`);
20577
+ if (override.cacheControl) parts.push(`cacheControl: ${override.cacheControl}`);
20578
+ if (override.cacheControlTtl) parts.push(`cacheControlTtl: ${override.cacheControlTtl}`);
20333
20579
  return parts.join("\n ") || "(empty)";
20334
20580
  }
20335
20581
  //#endregion
@@ -20416,39 +20662,59 @@ async function browseModelsCommand(client) {
20416
20662
  //#endregion
20417
20663
  //#region src/commands/config/edit.ts
20418
20664
  function formatOverrideHint(override) {
20419
- if (!override?.provider) return "(no provider routing)";
20420
- return Object.keys(override.provider).map((k) => `${k}: ${JSON.stringify(override.provider?.[k])}`).join(", ");
20665
+ if (!override) return "(empty)";
20666
+ const parts = [];
20667
+ if (override.provider) {
20668
+ const keys = Object.keys(override.provider);
20669
+ parts.push(`provider: ${keys.join(", ")}`);
20670
+ }
20671
+ if (override.sessionId) parts.push(`session: ${override.sessionId}`);
20672
+ if (override.cacheControl) parts.push(`cache: ${override.cacheControl}`);
20673
+ if (override.headers) parts.push(`${Object.keys(override.headers).length} header(s)`);
20674
+ return parts.join(", ") || "(empty)";
20421
20675
  }
20422
20676
  function showCurrentConfig(modelKey, current) {
20423
20677
  log.info(`Current config for "${modelKey}":`);
20424
20678
  if (current.provider) for (const [field, value] of Object.entries(current.provider)) log.info(` provider.${field}: ${JSON.stringify(value)}`);
20679
+ if (current.sessionId) log.info(` sessionId: ${current.sessionId}`);
20680
+ if (current.cacheControl) log.info(` cacheControl: ${current.cacheControl}`);
20681
+ if (current.cacheControlTtl) log.info(` cacheControlTtl: ${current.cacheControlTtl}`);
20425
20682
  if (current.headers) for (const [name, value] of Object.entries(current.headers)) log.info(` headers.${name}: ${value}`);
20426
20683
  }
20427
- function withoutProvider(current) {
20428
- return current.headers ? { headers: current.headers } : {};
20429
- }
20430
- async function updateProviderRouting(configPath, modelKey, current, client) {
20684
+ async function editProvider(modelKey, current, client) {
20431
20685
  const isPattern = modelKey.includes("*");
20432
20686
  const mode = await selectRoutingMode("Routing mode");
20433
- if (isCancel(mode)) return;
20687
+ if (isCancel(mode)) return current;
20434
20688
  if (mode === "skip") {
20435
- setModelOverride(configPath, modelKey, withoutProvider(current));
20436
- outro("✓ Override updated");
20437
- return;
20689
+ const { provider: _, ...rest } = current;
20690
+ return rest;
20438
20691
  }
20439
20692
  const providerOptions = await fetchProvidersForModel(client, modelKey, isPattern);
20440
- if (!providerOptions) return;
20441
- const override = await selectProvidersByMode(mode, providerOptions);
20442
- if (override === null) return;
20443
- const updated = withoutProvider(current);
20444
- if (override.provider) updated.provider = override.provider;
20445
- const save = await confirm({ message: "Save changes?" });
20446
- if (isCancel(save) || !save) {
20447
- outro("Cancelled");
20448
- return;
20449
- }
20450
- setModelOverride(configPath, modelKey, updated);
20451
- outro("✓ Override updated");
20693
+ if (!providerOptions) return current;
20694
+ const result = await selectProvidersByMode(mode, providerOptions);
20695
+ if (!result) return current;
20696
+ return {
20697
+ ...current,
20698
+ provider: result.provider
20699
+ };
20700
+ }
20701
+ async function editSessionId(current) {
20702
+ const result = await collectSessionTriState(current.sessionId);
20703
+ if (result === null) return current;
20704
+ const { sessionId: _, ...rest } = current;
20705
+ return {
20706
+ ...rest,
20707
+ sessionId: result.sessionId
20708
+ };
20709
+ }
20710
+ async function editCacheControl(current) {
20711
+ const result = await collectCacheTriState(current.cacheControl, current.cacheControlTtl);
20712
+ if (result === null) return current;
20713
+ const { cacheControl: _cc, cacheControlTtl: _ttl, ...rest } = current;
20714
+ return {
20715
+ ...rest,
20716
+ ...result
20717
+ };
20452
20718
  }
20453
20719
  /** Run the interactive "Edit model override" flow. */
20454
20720
  async function editOverrideCommand(client, configPath) {
@@ -20471,9 +20737,53 @@ async function editOverrideCommand(client, configPath) {
20471
20737
  });
20472
20738
  if (isCancel(selected)) return;
20473
20739
  const modelKey = selected;
20474
- const current = overrides[modelKey] ?? {};
20740
+ let current = overrides[modelKey] ?? {};
20475
20741
  showCurrentConfig(modelKey, current);
20476
- await updateProviderRouting(resolvedConfigPath, modelKey, current, client);
20742
+ while (true) {
20743
+ const field = await select({
20744
+ message: "Edit which field?",
20745
+ options: [
20746
+ {
20747
+ value: "provider",
20748
+ label: "Provider routing",
20749
+ hint: formatOverrideHint({ provider: current.provider })
20750
+ },
20751
+ {
20752
+ value: "sessionId",
20753
+ label: "Session ID",
20754
+ hint: current.sessionId ?? "(inherit)"
20755
+ },
20756
+ {
20757
+ value: "cacheControl",
20758
+ label: "Cache control",
20759
+ hint: [current.cacheControl, current.cacheControlTtl].filter(Boolean).join(", ") || "(inherit)"
20760
+ },
20761
+ {
20762
+ value: "done",
20763
+ label: "✓ Done"
20764
+ }
20765
+ ]
20766
+ });
20767
+ if (isCancel(field) || field === "done") break;
20768
+ switch (field) {
20769
+ case "provider":
20770
+ current = await editProvider(modelKey, current, client);
20771
+ break;
20772
+ case "sessionId":
20773
+ current = await editSessionId(current);
20774
+ break;
20775
+ case "cacheControl":
20776
+ current = await editCacheControl(current);
20777
+ break;
20778
+ }
20779
+ }
20780
+ const save = await confirm({ message: "Save changes?" });
20781
+ if (isCancel(save) || !save) {
20782
+ outro("Cancelled");
20783
+ return;
20784
+ }
20785
+ setModelOverride(resolvedConfigPath, modelKey, current);
20786
+ outro("✓ Override updated");
20477
20787
  }
20478
20788
  //#endregion
20479
20789
  //#region src/commands/config/list.ts
@@ -20483,6 +20793,9 @@ function formatOverrideSummary(override) {
20483
20793
  for (const [field, value] of Object.entries(override.provider)) if (value !== void 0) parts.push(`${field}: ${JSON.stringify(value)}`);
20484
20794
  }
20485
20795
  if (override.headers) for (const [name, value] of Object.entries(override.headers)) parts.push(`header ${name}: ${value}`);
20796
+ if (override.sessionId) parts.push(`session: ${override.sessionId}`);
20797
+ if (override.cacheControl) parts.push(`cache: ${override.cacheControl}`);
20798
+ if (override.cacheControlTtl) parts.push(`ttl: ${override.cacheControlTtl}`);
20486
20799
  return parts.join(", ") || "(empty)";
20487
20800
  }
20488
20801
  async function listOverridesCommand(args = {}) {
@@ -20547,40 +20860,166 @@ async function removeOverrideCommand(args = {}) {
20547
20860
  outro(`✓ ${toRemove.length} override(s) removed`);
20548
20861
  }
20549
20862
  //#endregion
20550
- //#region src/openrouter/client.ts
20551
- var OpenRouterClientError = class extends Error {
20552
- status;
20553
- constructor(status, message) {
20554
- super(`OpenRouter API error (${status}): ${message}`);
20555
- this.name = "OpenRouterClientError";
20556
- this.status = status;
20863
+ //#region src/commands/config/show.ts
20864
+ const SEPARATE_KEYS = new Set([
20865
+ ...new Set(["openrouterKey"]),
20866
+ "modelOverrides",
20867
+ "provider",
20868
+ "headers"
20869
+ ]);
20870
+ function logResolved(cfg, source) {
20871
+ intro("Resolved configuration");
20872
+ if (source) log.info(`Source: ${source}`);
20873
+ else log.info("Source: defaults only");
20874
+ for (const [key, value] of Object.entries(cfg)) {
20875
+ if (SEPARATE_KEYS.has(key)) continue;
20876
+ const label = key.padEnd(20);
20877
+ if (value !== void 0 && value !== "") log.info(` ${label} ${value}`);
20557
20878
  }
20558
- };
20559
- /** HTTP client for OpenRouter REST endpoints. */
20560
- var OpenRouterClient = class {
20561
- apiKey;
20562
- baseUrl;
20563
- authType;
20564
- constructor(apiKeyOrUrl, baseUrl, authType) {
20565
- if (baseUrl !== void 0) {
20566
- this.apiKey = apiKeyOrUrl;
20567
- this.baseUrl = baseUrl;
20568
- this.authType = authType;
20569
- } else {
20570
- this.apiKey = void 0;
20571
- this.baseUrl = apiKeyOrUrl;
20572
- this.authType = void 0;
20573
- }
20879
+ log.info(` ${"openrouterKey".padEnd(20)} ${maskKey(cfg.openrouterKey)}`);
20880
+ if (cfg.provider) log.info(` ${"provider".padEnd(20)} ${JSON.stringify(cfg.provider)}`);
20881
+ if (cfg.headers && Object.keys(cfg.headers).length > 0) log.info(` ${"headers".padEnd(20)} ${JSON.stringify(cfg.headers)}`);
20882
+ if (cfg.modelOverrides && Object.keys(cfg.modelOverrides).length > 0) {
20883
+ const count = Object.keys(cfg.modelOverrides).length;
20884
+ log.info(` ${"modelOverrides".padEnd(20)} ${count} model(s)`);
20885
+ for (const key of Object.keys(cfg.modelOverrides)) log.info(` ${key}`);
20574
20886
  }
20575
- async get(path) {
20576
- const url = `${this.baseUrl}${path}`;
20577
- const headers = {};
20578
- if (this.apiKey !== void 0 && this.authType !== void 0) headers.Authorization = formatAuthHeader(this.apiKey, this.authType);
20579
- const res = await fetch(url, { headers });
20580
- if (!res.ok) {
20581
- const body = await res.text().catch(() => "");
20582
- throw new OpenRouterClientError(res.status, body || res.statusText);
20583
- }
20887
+ outro(`Defaults loaded from schema: ${Object.keys(DEFAULTS).length} keys`);
20888
+ }
20889
+ async function showConfigCommand(args) {
20890
+ let discoveredPath = null;
20891
+ if (args.configPath) {
20892
+ if (existsSync(args.configPath)) discoveredPath = resolve(args.configPath);
20893
+ } else discoveredPath = tryFindConfigFile();
20894
+ const configExists = discoveredPath !== null;
20895
+ if (!configExists && !args.openrouterKey) {
20896
+ log.warn("No config file and no --openrouter-key — cannot show a complete resolved view.");
20897
+ if (args.json) {
20898
+ process.stdout.write(`${JSON.stringify({
20899
+ ok: false,
20900
+ error: "No config available"
20901
+ })}\n`);
20902
+ return;
20903
+ }
20904
+ outro("Run `proxitor config wizard` to create one.");
20905
+ return;
20906
+ }
20907
+ const cfg = await loadConfig({
20908
+ configPath: args.configPath,
20909
+ noConfig: !configExists && !args.configPath,
20910
+ openrouterKey: args.openrouterKey
20911
+ });
20912
+ if (args.json) {
20913
+ const masked = {
20914
+ ...cfg,
20915
+ openrouterKey: maskKey(cfg.openrouterKey)
20916
+ };
20917
+ process.stdout.write(`${JSON.stringify(masked, null, 2)}\n`);
20918
+ return;
20919
+ }
20920
+ logResolved(cfg, discoveredPath);
20921
+ }
20922
+ //#endregion
20923
+ //#region src/commands/config/validate.ts
20924
+ function validate(configPath) {
20925
+ if (!configPath) return {
20926
+ ok: false,
20927
+ configPath: null,
20928
+ error: "No config file found"
20929
+ };
20930
+ try {
20931
+ const cfg = readConfigFile(configPath);
20932
+ return {
20933
+ ok: true,
20934
+ configPath,
20935
+ keyCount: Object.keys(cfg).length,
20936
+ overrideCount: cfg.modelOverrides ? Object.keys(cfg.modelOverrides).length : 0
20937
+ };
20938
+ } catch (error) {
20939
+ if (error instanceof ConfigValidationError) {
20940
+ const issues = error.zodError.issues.map((issue) => ({
20941
+ path: issue.path.length > 0 ? issue.path.join(".") : "(root)",
20942
+ message: issue.message
20943
+ }));
20944
+ return {
20945
+ ok: false,
20946
+ configPath,
20947
+ error: error.message,
20948
+ issues
20949
+ };
20950
+ }
20951
+ return {
20952
+ ok: false,
20953
+ configPath,
20954
+ error: error instanceof Error ? error.message : String(error)
20955
+ };
20956
+ }
20957
+ }
20958
+ async function validateConfigCommand(args = {}) {
20959
+ const result = validate(tryFindConfigFile(args.configPath));
20960
+ if (args.json) {
20961
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
20962
+ return result.ok ? 0 : 1;
20963
+ }
20964
+ intro("Validate Config");
20965
+ if (result.ok) {
20966
+ log.success(`Config is valid: ${result.configPath}`);
20967
+ log.info(` ${result.keyCount} top-level keys, ${result.overrideCount} model override(s)`);
20968
+ outro("Ready to run `proxitor start`.");
20969
+ return 0;
20970
+ }
20971
+ if (!result.configPath) {
20972
+ log.warn("No config file found — nothing to validate.");
20973
+ log.info("Searched:");
20974
+ outro("Run `proxitor config wizard` to create one.");
20975
+ return 1;
20976
+ }
20977
+ log.error(`Invalid config: ${result.configPath}`);
20978
+ if (result.issues && result.issues.length > 0) {
20979
+ note(result.issues.map((i) => ` ${i.path}: ${i.message}`).join("\n"), "Validation issues");
20980
+ log.info("Tips:");
20981
+ log.info(` • Open the file: \`$EDITOR ${result.configPath}\``);
20982
+ log.info(" • Run `proxitor config wizard` to start fresh");
20983
+ log.info(" • Or run `proxitor doctor` for a fuller diagnostic");
20984
+ } else log.error(result.error);
20985
+ outro("Fix the issues and re-run `proxitor config validate`.");
20986
+ return 1;
20987
+ }
20988
+ //#endregion
20989
+ //#region src/openrouter/client.ts
20990
+ var OpenRouterClientError = class extends Error {
20991
+ status;
20992
+ constructor(status, message) {
20993
+ super(`OpenRouter API error (${status}): ${message}`);
20994
+ this.name = "OpenRouterClientError";
20995
+ this.status = status;
20996
+ }
20997
+ };
20998
+ /** HTTP client for OpenRouter REST endpoints. */
20999
+ var OpenRouterClient = class {
21000
+ apiKey;
21001
+ baseUrl;
21002
+ authType;
21003
+ constructor(apiKeyOrUrl, baseUrl, authType) {
21004
+ if (baseUrl !== void 0) {
21005
+ this.apiKey = apiKeyOrUrl;
21006
+ this.baseUrl = baseUrl;
21007
+ this.authType = authType;
21008
+ } else {
21009
+ this.apiKey = void 0;
21010
+ this.baseUrl = apiKeyOrUrl;
21011
+ this.authType = void 0;
21012
+ }
21013
+ }
21014
+ async get(path) {
21015
+ const url = `${this.baseUrl}${path}`;
21016
+ const headers = {};
21017
+ if (this.apiKey !== void 0 && this.authType !== void 0) headers.Authorization = formatAuthHeader(this.apiKey, this.authType);
21018
+ const res = await fetch(url, { headers });
21019
+ if (!res.ok) {
21020
+ const body = await res.text().catch(() => "");
21021
+ throw new OpenRouterClientError(res.status, body || res.statusText);
21022
+ }
20584
21023
  return res.json();
20585
21024
  }
20586
21025
  };
@@ -20722,11 +21161,6 @@ async function probeUpstream(baseUrl, apiKey, authType, timeoutMs = 3e3) {
20722
21161
  }
20723
21162
  //#endregion
20724
21163
  //#region src/commands/config/wizard.ts
20725
- function maskKey(key) {
20726
- if (!key) return "(none)";
20727
- if (key.length <= 11) return "****";
20728
- return `${key.slice(0, 7)}…${key.slice(-4)}`;
20729
- }
20730
21164
  function resolveSavePath(location) {
20731
21165
  switch (location) {
20732
21166
  case "local": return resolve("proxitor.config.yaml");
@@ -20782,108 +21216,6 @@ function buildYaml(apiKey, port, host, baseUrl, authType, existingRaw) {
20782
21216
  if (authType !== DEFAULTS.authType) config.authType = authType;
20783
21217
  return (0, import_dist.stringify)(config);
20784
21218
  }
20785
- async function askApiKey(currentKey) {
20786
- if (currentKey) log.info(`Current key: ${maskKey(currentKey)}`);
20787
- const apiKey = await text({
20788
- message: "OpenRouter API key",
20789
- placeholder: currentKey ? "Press Enter to keep current key" : "sk-or-v1-...",
20790
- validate: (v) => {
20791
- if (!v?.trim() && !currentKey) return "API key is required";
20792
- }
20793
- });
20794
- if (isCancel(apiKey)) return null;
20795
- note("You can also set the OPENROUTER_API_KEY environment variable\nto avoid storing the key in the config file.", "Tip");
20796
- return apiKey.trim() || currentKey;
20797
- }
20798
- async function askPort(current) {
20799
- const input = await text({
20800
- message: "Proxy port",
20801
- initialValue: String(current),
20802
- placeholder: String(DEFAULTS.port),
20803
- validate: (v) => {
20804
- if (!v?.trim()) return void 0;
20805
- const n = Number.parseInt(v, 10);
20806
- if (Number.isNaN(n) || n < 1 || n > 65535) return "Port must be 1–65535";
20807
- }
20808
- });
20809
- if (isCancel(input)) return null;
20810
- return input.trim() ? Number.parseInt(input, 10) : DEFAULTS.port;
20811
- }
20812
- async function askBaseUrl(current) {
20813
- const url = await text({
20814
- message: "OpenRouter API base URL",
20815
- placeholder: DEFAULTS.openrouterBaseUrl,
20816
- initialValue: current === DEFAULTS.openrouterBaseUrl ? "" : current,
20817
- validate: (v) => {
20818
- if (!v?.trim()) return void 0;
20819
- try {
20820
- if (!new URL(v.trim()).protocol.startsWith("http")) return "URL must start with http:// or https://";
20821
- } catch {
20822
- return "Invalid URL";
20823
- }
20824
- }
20825
- });
20826
- if (isCancel(url)) return null;
20827
- return url.trim() || DEFAULTS.openrouterBaseUrl;
20828
- }
20829
- async function askAuthType(current) {
20830
- const authType = await select({
20831
- message: "Authentication type",
20832
- initialValue: current,
20833
- options: [{
20834
- value: "bearer",
20835
- label: "Bearer token",
20836
- hint: "Standard OpenRouter"
20837
- }, {
20838
- value: "oauth",
20839
- label: "OAuth token",
20840
- hint: "Custom proxy providers"
20841
- }]
20842
- });
20843
- if (isCancel(authType)) return null;
20844
- return authType;
20845
- }
20846
- async function askHost(current) {
20847
- const isPreset = (v) => v === "0.0.0.0" || v === "127.0.0.1";
20848
- const host = await select({
20849
- message: "Listen address",
20850
- initialValue: isPreset(current) ? current : "__custom__",
20851
- options: [
20852
- {
20853
- value: "0.0.0.0",
20854
- label: "All interfaces (0.0.0.0)",
20855
- hint: "Default"
20856
- },
20857
- {
20858
- value: "127.0.0.1",
20859
- label: "Localhost only (127.0.0.1)",
20860
- hint: "More secure"
20861
- },
20862
- {
20863
- value: "__custom__",
20864
- label: "Custom address…",
20865
- hint: "Specific IP, hostname, or unix:/path"
20866
- }
20867
- ]
20868
- });
20869
- if (isCancel(host)) return null;
20870
- if (host === "__custom__") {
20871
- const custom = await text({
20872
- message: "Custom listen address",
20873
- placeholder: "192.168.1.1",
20874
- initialValue: isPreset(current) ? "" : current,
20875
- validate: (v) => {
20876
- const t = v?.trim();
20877
- if (!t) return "Address is required";
20878
- if (t.startsWith("unix:")) return void 0;
20879
- if (!/^[\w.\-:]+$/.test(t)) return "Invalid host (allowed: IP, hostname, or unix:…)";
20880
- }
20881
- });
20882
- if (isCancel(custom)) return null;
20883
- return custom.trim();
20884
- }
20885
- return host;
20886
- }
20887
21219
  async function askSaveLocation(existingPath) {
20888
21220
  const options = getSaveLocationOptions(existingPath);
20889
21221
  const location = await select({
@@ -20998,197 +21330,221 @@ async function runWizard(opts = {}) {
20998
21330
  }
20999
21331
  }
21000
21332
  //#endregion
21001
- //#region src/commands/config/show.ts
21002
- const SEPARATE_KEYS = new Set([
21003
- ...new Set(["openrouterKey"]),
21004
- "modelOverrides",
21005
- "provider",
21006
- "headers"
21007
- ]);
21008
- function logResolved(cfg, source) {
21009
- intro("Resolved configuration");
21010
- if (source) log.info(`Source: ${source}`);
21011
- else log.info("Source: defaults only");
21012
- for (const [key, value] of Object.entries(cfg)) {
21013
- if (SEPARATE_KEYS.has(key)) continue;
21014
- const label = key.padEnd(20);
21015
- if (value !== void 0 && value !== "") log.info(` ${label} ${value}`);
21016
- }
21017
- log.info(` ${"openrouterKey".padEnd(20)} ${maskKey(cfg.openrouterKey)}`);
21018
- if (cfg.provider) log.info(` ${"provider".padEnd(20)} ${JSON.stringify(cfg.provider)}`);
21019
- if (cfg.headers && Object.keys(cfg.headers).length > 0) log.info(` ${"headers".padEnd(20)} ${JSON.stringify(cfg.headers)}`);
21020
- if (cfg.modelOverrides && Object.keys(cfg.modelOverrides).length > 0) {
21021
- const count = Object.keys(cfg.modelOverrides).length;
21022
- log.info(` ${"modelOverrides".padEnd(20)} ${count} model(s)`);
21023
- for (const key of Object.keys(cfg.modelOverrides)) log.info(` ${key}`);
21024
- }
21025
- outro(`Defaults loaded from schema: ${Object.keys(DEFAULTS).length} keys`);
21026
- }
21027
- async function showConfigCommand(args) {
21028
- let discoveredPath = null;
21029
- if (args.configPath) {
21030
- if (existsSync(args.configPath)) discoveredPath = resolve(args.configPath);
21031
- } else discoveredPath = tryFindConfigFile();
21032
- const configExists = discoveredPath !== null;
21033
- if (!configExists && !args.openrouterKey) {
21034
- log.warn("No config file and no --openrouter-key — cannot show a complete resolved view.");
21035
- if (args.json) {
21036
- process.stdout.write(`${JSON.stringify({
21037
- ok: false,
21038
- error: "No config available"
21039
- })}\n`);
21040
- return;
21041
- }
21042
- outro("Run `proxitor config wizard` to create one.");
21043
- return;
21044
- }
21045
- const cfg = await loadConfig({
21046
- configPath: args.configPath,
21047
- noConfig: !configExists && !args.configPath,
21048
- openrouterKey: args.openrouterKey
21049
- });
21050
- if (args.json) {
21051
- const masked = {
21052
- ...cfg,
21053
- openrouterKey: maskKey(cfg.openrouterKey)
21054
- };
21055
- process.stdout.write(`${JSON.stringify(masked, null, 2)}\n`);
21056
- return;
21057
- }
21058
- logResolved(cfg, discoveredPath);
21333
+ //#region src/commands/config/cache-control.ts
21334
+ async function cacheControlCommand(opts) {
21335
+ const configPath = requireConfigPath(opts?.configPath);
21336
+ const cfg = readConfigFile(configPath);
21337
+ const currentCc = cfg.cacheControl ?? DEFAULTS.cacheControl;
21338
+ const currentTtl = cfg.cacheControlTtl;
21339
+ log.info(`Current: cacheControl = ${currentCc}`);
21340
+ if (currentTtl) log.info(`Current: cacheControlTtl = ${currentTtl}`);
21341
+ const cc = await askTriState("Cache control mode", currentCc, CACHE_HINTS);
21342
+ if (cc === null) return;
21343
+ const fields = { cacheControl: cc };
21344
+ if (cc !== "never") {
21345
+ const ttlResult = await askCacheControlTtl(currentTtl);
21346
+ if (ttlResult === null) return;
21347
+ if (ttlResult === "reset") fields.cacheControlTtl = void 0;
21348
+ else fields.cacheControlTtl = ttlResult;
21349
+ }
21350
+ setGlobalConfigFields(configPath, fields);
21351
+ const ttlPart = fields.cacheControlTtl ? `, TTL = ${fields.cacheControlTtl}` : "";
21352
+ log.success(`cacheControl set to ${cc}${ttlPart}`);
21059
21353
  }
21060
21354
  //#endregion
21061
- //#region src/commands/config/validate.ts
21062
- function validate(configPath) {
21063
- if (!configPath) return {
21064
- ok: false,
21065
- configPath: null,
21066
- error: "No config file found"
21067
- };
21068
- try {
21069
- const cfg = readConfigFile(configPath);
21070
- return {
21071
- ok: true,
21072
- configPath,
21073
- keyCount: Object.keys(cfg).length,
21074
- overrideCount: cfg.modelOverrides ? Object.keys(cfg.modelOverrides).length : 0
21075
- };
21076
- } catch (error) {
21077
- if (error instanceof ConfigValidationError) {
21078
- const issues = error.zodError.issues.map((issue) => ({
21079
- path: issue.path.length > 0 ? issue.path.join(".") : "(root)",
21080
- message: issue.message
21081
- }));
21082
- return {
21083
- ok: false,
21084
- configPath,
21085
- error: error.message,
21086
- issues
21087
- };
21088
- }
21089
- return {
21090
- ok: false,
21091
- configPath,
21092
- error: error instanceof Error ? error.message : String(error)
21093
- };
21094
- }
21095
- }
21096
- async function validateConfigCommand(args = {}) {
21097
- const result = validate(tryFindConfigFile(args.configPath));
21098
- if (args.json) {
21099
- process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
21100
- return result.ok ? 0 : 1;
21101
- }
21102
- intro("Validate Config");
21103
- if (result.ok) {
21104
- log.success(`Config is valid: ${result.configPath}`);
21105
- log.info(` ${result.keyCount} top-level keys, ${result.overrideCount} model override(s)`);
21106
- outro("Ready to run `proxitor start`.");
21107
- return 0;
21108
- }
21109
- if (!result.configPath) {
21110
- log.warn("No config file found — nothing to validate.");
21111
- log.info("Searched:");
21112
- outro("Run `proxitor config wizard` to create one.");
21113
- return 1;
21355
+ //#region src/commands/config/connection.ts
21356
+ const FIELD_MAP = {
21357
+ apiKey: {
21358
+ configKey: "openrouterKey",
21359
+ label: "API key",
21360
+ ask: (cfg) => askApiKey(cfg.openrouterKey ?? DEFAULTS.openrouterKey)
21361
+ },
21362
+ port: {
21363
+ configKey: "port",
21364
+ label: "Proxy port",
21365
+ ask: (cfg) => askPort(cfg.port ?? DEFAULTS.port)
21366
+ },
21367
+ host: {
21368
+ configKey: "host",
21369
+ label: "Listen address",
21370
+ ask: (cfg) => askHost(cfg.host ?? DEFAULTS.host)
21371
+ },
21372
+ baseUrl: {
21373
+ configKey: "openrouterBaseUrl",
21374
+ label: "Base URL",
21375
+ ask: (cfg) => askBaseUrl(cfg.openrouterBaseUrl ?? DEFAULTS.openrouterBaseUrl)
21376
+ },
21377
+ authType: {
21378
+ configKey: "authType",
21379
+ label: "Auth type",
21380
+ ask: (cfg) => askAuthType(cfg.authType ?? DEFAULTS.authType)
21114
21381
  }
21115
- log.error(`Invalid config: ${result.configPath}`);
21116
- if (result.issues && result.issues.length > 0) {
21117
- note(result.issues.map((i) => ` ${i.path}: ${i.message}`).join("\n"), "Validation issues");
21118
- log.info("Tips:");
21119
- log.info(` • Open the file: \`$EDITOR ${result.configPath}\``);
21120
- log.info(" Run `proxitor config wizard` to start fresh");
21121
- log.info(" • Or run `proxitor doctor` for a fuller diagnostic");
21122
- } else log.error(result.error);
21123
- outro("Fix the issues and re-run `proxitor config validate`.");
21124
- return 1;
21125
- }
21126
- //#endregion
21127
- //#region src/commands/config.ts
21128
- /** Run the interactive config manager menu. */
21129
- async function runConfigMenu(client) {
21130
- intro("Proxitor Config Manager");
21131
- const action = await select({
21132
- message: "What would you like to do?",
21382
+ };
21383
+ async function connectionMenuCommand(opts) {
21384
+ const configPath = requireConfigPath(opts?.configPath);
21385
+ const cfg = readConfigFile(configPath);
21386
+ const field = await select({
21387
+ message: "Change which field?",
21133
21388
  options: [
21134
21389
  {
21135
- value: "add",
21136
- label: "➕ Add model override"
21390
+ value: "apiKey",
21391
+ label: "API key",
21392
+ hint: maskKey(cfg.openrouterKey ?? "")
21137
21393
  },
21138
21394
  {
21139
- value: "edit",
21140
- label: "✏️ Edit model override"
21395
+ value: "port",
21396
+ label: "Proxy port",
21397
+ hint: String(cfg.port ?? DEFAULTS.port)
21141
21398
  },
21142
21399
  {
21143
- value: "remove",
21144
- label: "🗑 Remove model override"
21400
+ value: "host",
21401
+ label: "Listen address",
21402
+ hint: cfg.host ?? DEFAULTS.host
21145
21403
  },
21146
21404
  {
21147
- value: "list",
21148
- label: "📋 List current overrides"
21405
+ value: "baseUrl",
21406
+ label: "Base URL"
21149
21407
  },
21150
21408
  {
21151
- value: "browse",
21152
- label: "🔍 Browse models"
21409
+ value: "authType",
21410
+ label: "Auth type",
21411
+ hint: cfg.authType ?? DEFAULTS.authType
21153
21412
  },
21154
21413
  {
21155
- value: "validate",
21156
- label: "✅ Validate config"
21157
- },
21158
- {
21159
- value: "exit",
21160
- label: "❌ Exit"
21414
+ value: "back",
21415
+ label: " Back"
21161
21416
  }
21162
21417
  ]
21163
21418
  });
21164
- if (isCancel(action) || action === "exit") {
21165
- outro("Bye!");
21166
- return;
21167
- }
21168
- switch (action) {
21169
- case "add":
21170
- await addOverrideCommand({ client });
21171
- break;
21172
- case "edit":
21173
- await editOverrideCommand(client);
21174
- break;
21175
- case "remove":
21176
- await removeOverrideCommand();
21177
- break;
21178
- case "list":
21179
- await listOverridesCommand();
21180
- break;
21181
- case "browse":
21182
- await browseModelsCommand(client);
21183
- break;
21184
- case "validate":
21185
- await validateConfigCommand();
21186
- break;
21419
+ if (isCancel(field) || field === "back") return;
21420
+ const entry = FIELD_MAP[field];
21421
+ if (!entry) return;
21422
+ const result = await entry.ask(cfg);
21423
+ if (result === null) return;
21424
+ setGlobalConfigField(configPath, entry.configKey, result);
21425
+ log.success(`${entry.label} updated`);
21426
+ }
21427
+ //#endregion
21428
+ //#region src/commands/config/session-routing.ts
21429
+ async function sessionRoutingCommand(opts) {
21430
+ const configPath = requireConfigPath(opts?.configPath);
21431
+ const current = readConfigFile(configPath).sessionId ?? DEFAULTS.sessionId;
21432
+ log.info(`Current: sessionId = ${current}`);
21433
+ const result = await askTriState("Session routing mode", current, SESSION_HINTS);
21434
+ if (result === null) return;
21435
+ setGlobalConfigField(configPath, "sessionId", result);
21436
+ log.success(`sessionId set to ${result}`);
21437
+ }
21438
+ //#endregion
21439
+ //#region src/commands/config.ts
21440
+ async function runConfigMenu(client) {
21441
+ intro("Proxitor Config Manager");
21442
+ while (true) {
21443
+ const action = await select({
21444
+ message: "What would you like to do?",
21445
+ options: [
21446
+ {
21447
+ value: "show",
21448
+ label: "📋 Show current config"
21449
+ },
21450
+ {
21451
+ value: "_sep1",
21452
+ label: "── Global Settings ──",
21453
+ disabled: true
21454
+ },
21455
+ {
21456
+ value: "connection",
21457
+ label: "🔑 API key & connection"
21458
+ },
21459
+ {
21460
+ value: "session",
21461
+ label: "🔗 Session routing"
21462
+ },
21463
+ {
21464
+ value: "cache",
21465
+ label: "💾 Cache control"
21466
+ },
21467
+ {
21468
+ value: "_sep2",
21469
+ label: "── Model Overrides ──",
21470
+ disabled: true
21471
+ },
21472
+ {
21473
+ value: "add",
21474
+ label: "➕ Add model override"
21475
+ },
21476
+ {
21477
+ value: "edit",
21478
+ label: "✏️ Edit model override"
21479
+ },
21480
+ {
21481
+ value: "remove",
21482
+ label: "🗑 Remove model override"
21483
+ },
21484
+ {
21485
+ value: "list",
21486
+ label: "📄 List current overrides"
21487
+ },
21488
+ {
21489
+ value: "browse",
21490
+ label: "🔍 Browse models"
21491
+ },
21492
+ {
21493
+ value: "_sep3",
21494
+ label: "── Tools ──",
21495
+ disabled: true
21496
+ },
21497
+ {
21498
+ value: "validate",
21499
+ label: "✅ Validate config"
21500
+ },
21501
+ {
21502
+ value: "exit",
21503
+ label: "❌ Exit"
21504
+ }
21505
+ ]
21506
+ });
21507
+ if (isCancel(action) || action === "exit") {
21508
+ outro("Bye!");
21509
+ return;
21510
+ }
21511
+ switch (action) {
21512
+ case "show":
21513
+ await showConfigCommand({});
21514
+ break;
21515
+ case "connection":
21516
+ await connectionMenuCommand();
21517
+ break;
21518
+ case "session":
21519
+ await sessionRoutingCommand();
21520
+ break;
21521
+ case "cache":
21522
+ await cacheControlCommand();
21523
+ break;
21524
+ case "add":
21525
+ await addOverrideCommand({ client });
21526
+ break;
21527
+ case "edit":
21528
+ await editOverrideCommand(client);
21529
+ break;
21530
+ case "remove":
21531
+ await removeOverrideCommand();
21532
+ break;
21533
+ case "list":
21534
+ await listOverridesCommand();
21535
+ break;
21536
+ case "browse":
21537
+ await browseModelsCommand(client);
21538
+ break;
21539
+ case "validate":
21540
+ await validateConfigCommand();
21541
+ break;
21542
+ }
21187
21543
  }
21188
21544
  }
21189
21545
  //#endregion
21190
21546
  //#region src/version.ts
21191
- const version = "0.9.0-beta.2";
21547
+ const version = "0.9.0-beta.3";
21192
21548
  //#endregion
21193
21549
  //#region src/commands/doctor.ts
21194
21550
  const DEFAULT_TIMEOUT_MS = 3e3;