@wrongstack/cli 0.299.0 → 0.301.0

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.
Files changed (82) hide show
  1. package/dist/{acp-S4LZQB66.js → acp-DATHHPNT.js} +36 -17
  2. package/dist/acp-server-agent.d.ts +1 -1
  3. package/dist/{audit-2ZSFGNIF.js → audit-QRCLEHHW.js} +5 -1
  4. package/dist/{auth-G5YKKSRH.js → auth-JASC4K3F.js} +151 -38
  5. package/dist/auth-menu/loopback-server.d.ts +41 -55
  6. package/dist/auth-menu/openai-codex-oauth.d.ts +36 -123
  7. package/dist/auto-discover-providers-DRW4OZHG.js +8 -0
  8. package/dist/boot/dispatch-webui.d.ts +3 -0
  9. package/dist/boot/launch-menu.d.ts +2 -4
  10. package/dist/boot/tool-registry.d.ts +3 -0
  11. package/dist/chimera-reviewer-policy.d.ts +10 -3
  12. package/dist/chimera-work-registry.d.ts +19 -0
  13. package/dist/{chronicle-QQEQFSSE.js → chronicle-63MFCZN6.js} +26 -5
  14. package/dist/{chunk-YWUPHRGB.js → chunk-263G3FMB.js} +33 -10
  15. package/dist/{chunk-2SLSUDYS.js → chunk-2KDOFTTM.js} +312 -211
  16. package/dist/{chunk-TGDHN4LM.js → chunk-4GVH7V3S.js} +4 -24
  17. package/dist/chunk-CSAPOCBP.js +24 -0
  18. package/dist/{chunk-66T43Q4Y.js → chunk-ETOEGL3V.js} +6 -9
  19. package/dist/{chunk-PEKUKUCH.js → chunk-GUHQQG63.js} +32 -14
  20. package/dist/chunk-KE7E7DPX.js +38 -0
  21. package/dist/{chunk-ZNZJ34RF.js → chunk-MHF2IJDE.js} +21 -4
  22. package/dist/{chunk-4HH3YGG4.js → chunk-MRLYDHQ7.js} +8 -5
  23. package/dist/{chunk-L5CE52XW.js → chunk-O7XNGCRT.js} +317 -309
  24. package/dist/chunk-PYTFS4MC.js +238 -0
  25. package/dist/{chunk-DPJZQCVP.js → chunk-YMXXOOFN.js} +1 -1
  26. package/dist/{chunk-4U3DMA2Y.js → chunk-ZECLGUTF.js} +28 -21
  27. package/dist/{cli-main-3MQDFSYK.js → cli-main-XWC57MGE.js} +849 -242
  28. package/dist/{execution-ANOYMWPO.js → execution-LHMSMDHB.js} +174 -55
  29. package/dist/execution-chimera-cascade.d.ts +3 -4
  30. package/dist/execution-chimera-review.d.ts +2 -3
  31. package/dist/execution-cleanup.d.ts +3 -2
  32. package/dist/execution.d.ts +1 -1
  33. package/dist/{export-TPORYVRZ.js → export-DOBGPY23.js} +12 -1
  34. package/dist/fleet/budget-source.d.ts +18 -0
  35. package/dist/fleet/host-context.d.ts +1 -1
  36. package/dist/fleet/host-helpers.d.ts +14 -0
  37. package/dist/fleet/host-status.d.ts +31 -0
  38. package/dist/fleet/host-types.d.ts +8 -0
  39. package/dist/fleet/host.d.ts +5 -0
  40. package/dist/fleet/status-broadcast.d.ts +1 -1
  41. package/dist/{hq-BDO56CX6.js → hq-LN5CJHCM.js} +22 -3
  42. package/dist/hq-server/auth-state.d.ts +29 -1
  43. package/dist/hq-server/auth.d.ts +16 -0
  44. package/dist/hq-server/client-address.d.ts +56 -0
  45. package/dist/hq-server/login-attempt-store.d.ts +30 -10
  46. package/dist/hq-server/routes/auth-handlers.d.ts +66 -26
  47. package/dist/hq-server/routes.d.ts +23 -1
  48. package/dist/hq-server/types.d.ts +10 -0
  49. package/dist/hq-server/ws.d.ts +17 -6
  50. package/dist/{hq-server-RJLJJZU7.js → hq-server-S7HZG3PA.js} +3 -2
  51. package/dist/hq-server.d.ts +19 -0
  52. package/dist/index.js +69 -104
  53. package/dist/live-settings-input.d.ts +5 -0
  54. package/dist/{mcp-CYLPUEHC.js → mcp-OHPJH73U.js} +6 -2
  55. package/dist/{modeldiag-HENYJ4N7.js → modeldiag-3DOK4BWW.js} +3 -3
  56. package/dist/{plugin-usage-VTL7ZTVC.js → plugin-usage-YEVLOR33.js} +7 -3
  57. package/dist/{plugins-ETY6W3DC.js → plugins-K3BAUP4D.js} +3 -3
  58. package/dist/profile-config-path.d.ts +0 -2
  59. package/dist/provider-helpers.d.ts +0 -5
  60. package/dist/{providers-models-GDELBHZK.js → providers-models-WEZ4EGLL.js} +38 -12
  61. package/dist/{replay-KCTXNMZQ.js → replay-FCBNHW3K.js} +5 -1
  62. package/dist/{rewind-EJMVFVLS.js → rewind-AOD3JIYX.js} +5 -1
  63. package/dist/{sessions-config-NLCMK3BY.js → sessions-config-XXN7267N.js} +8 -3
  64. package/dist/slash-commands/command-context.d.ts +19 -0
  65. package/dist/subcommands/flags.d.ts +23 -0
  66. package/dist/terminal-format.d.ts +15 -0
  67. package/dist/webui-server/setup-events.d.ts +17 -0
  68. package/dist/webui-server/terminal-log-view.d.ts +29 -12
  69. package/dist/{webui-server-PCLSXX6L.js → webui-server-DD35QFNO.js} +113 -50
  70. package/dist/webui-server-options.d.ts +16 -0
  71. package/dist/wiring/brain-and-orchestration.d.ts +4 -1
  72. package/dist/wiring/director-setup.d.ts +4 -0
  73. package/dist/wiring/fallback-gate.d.ts +31 -0
  74. package/dist/wiring/fleet-command-handlers.d.ts +1 -1
  75. package/dist/wiring/mailbox-bridge-bootstrap.d.ts +3 -1
  76. package/dist/wiring/management-tools.d.ts +9 -1
  77. package/dist/wiring/pipeline.d.ts +5 -5
  78. package/dist/wiring/plugins.d.ts +3 -10
  79. package/dist/wiring/provider-utility-tools.d.ts +27 -1
  80. package/dist/wiring/tools.d.ts +4 -5
  81. package/package.json +23 -23
  82. package/dist/chunk-GO3TJICK.js +0 -489
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  LOCAL_LLM_PRESETS,
3
3
  runLiveProviderPicker
4
- } from "./chunk-ZNZJ34RF.js";
4
+ } from "./chunk-MHF2IJDE.js";
5
5
  import {
6
6
  openBrowser,
7
7
  runCodexOAuthLogin,
8
8
  startLoopbackServer
9
- } from "./chunk-GO3TJICK.js";
9
+ } from "./chunk-PYTFS4MC.js";
10
10
  import {
11
11
  activeLabel,
12
12
  loadConfigProviders,
@@ -146,9 +146,10 @@ async function runClaudeOAuthLogin(deps, opts = {}) {
146
146
  const ac = new AbortController();
147
147
  const onSig = () => ac.abort();
148
148
  const external = opts.signal;
149
+ const onExternalAbort = () => ac.abort();
149
150
  if (external) {
150
151
  if (external.aborted) ac.abort();
151
- else external.addEventListener("abort", () => ac.abort(), { once: true });
152
+ else external.addEventListener("abort", onExternalAbort, { once: true });
152
153
  } else {
153
154
  process.on("SIGINT", onSig);
154
155
  }
@@ -222,7 +223,8 @@ async function runClaudeOAuthLogin(deps, opts = {}) {
222
223
  return 1;
223
224
  } finally {
224
225
  server.close();
225
- if (!external) process.off("SIGINT", onSig);
226
+ if (external) external.removeEventListener("abort", onExternalAbort);
227
+ else process.off("SIGINT", onSig);
226
228
  }
227
229
  }
228
230
  async function saveClaudeTokens(deps, providerId, tokens, models) {
@@ -257,7 +259,6 @@ async function saveClaudeTokens(deps, providerId, tokens, models) {
257
259
  }
258
260
 
259
261
  // src/auth-menu/github-copilot-oauth.ts
260
- import { spawn } from "node:child_process";
261
262
  import { color as color2 } from "@wrongstack/core/utils";
262
263
  import { FetchError as FetchError2, ParseError as ParseError2 } from "@wrongstack/core/types";
263
264
  import { copilotBaseUrlFromToken, refreshCopilotToken } from "@wrongstack/providers";
@@ -272,17 +273,6 @@ var COPILOT_HEADERS = {
272
273
  };
273
274
  var COPILOT_API_VERSION = "2026-06-01";
274
275
  var COPILOT_PROVIDER_ID = "github-copilot";
275
- function openBrowser2(url) {
276
- try {
277
- const platform = process.platform;
278
- const { command, args } = platform === "win32" ? { command: "cmd", args: ["/c", "start", "", url] } : platform === "darwin" ? { command: "open", args: [url] } : { command: "xdg-open", args: [url] };
279
- const child = spawn(command, args, { stdio: "ignore", windowsHide: true });
280
- child.on("error", () => {
281
- });
282
- child.unref();
283
- } catch {
284
- }
285
- }
286
276
  function sleep(ms, signal) {
287
277
  return new Promise((resolve, reject) => {
288
278
  if (signal.aborted) return reject(new DOMException("Aborted", "AbortError"));
@@ -430,7 +420,7 @@ async function runCopilotOAuthLogin(deps, opts = {}) {
430
420
 
431
421
  `)
432
422
  );
433
- openBrowser2(device.verification_uri);
423
+ openBrowser(device.verification_uri);
434
424
  deps.renderer.write(color2.dim(" Waiting for you to authorize in the browser...\n"));
435
425
  const githubToken = await pollForGitHubToken(device, ac.signal);
436
426
  deps.renderer.write(color2.dim(" Authorized. Fetching your Copilot token...\n"));
@@ -545,6 +535,10 @@ async function runOAuthLoginMenu(deps) {
545
535
  await runOAuthLoginChoice(deps, pick);
546
536
  }
547
537
 
538
+ // src/auth-menu/local.ts
539
+ import { color as color5 } from "@wrongstack/core/utils";
540
+ import { DefaultSecretScrubber as DefaultSecretScrubber2 } from "@wrongstack/core/security";
541
+
548
542
  // src/auth-menu/helpers.ts
549
543
  async function loadProviders(deps) {
550
544
  return loadConfigProviders(deps.profileConfigPath, deps.vault, {
@@ -713,268 +707,7 @@ function validateFamily(raw) {
713
707
  return valid.includes(raw) ? raw : null;
714
708
  }
715
709
 
716
- // src/auth-menu/add-provider.ts
717
- import { atomicWrite, color as color5 } from "@wrongstack/core/utils";
718
- import * as fs from "node:fs/promises";
719
-
720
- // src/provider-id.ts
721
- function nextCustomProviderId(existing) {
722
- const taken = new Set(existing);
723
- let n = 1;
724
- while (taken.has(`custom-${n}`)) n++;
725
- return `custom-${n}`;
726
- }
727
-
728
- // src/auth-menu/add-provider.ts
729
- async function addFromCatalog(deps) {
730
- let catalog = [];
731
- try {
732
- catalog = (await deps.modelsRegistry.listProviders()).filter((p) => p.family !== "unsupported");
733
- } catch {
734
- deps.renderer.writeWarning("Catalog unavailable \u2014 falling back to manual entry.\n");
735
- }
736
- if (catalog.length === 0) {
737
- return addManualEntry(deps);
738
- }
739
- const saved = new Set(Object.keys(await loadProviders(deps)));
740
- deps.renderer.write(
741
- color5.dim(
742
- ` Catalog: ${catalog.length} providers. Type to filter, \u2191/\u2193 navigate, Enter to select.
743
- `
744
- )
745
- );
746
- deps.renderer.write(` ${color5.dim("\u25C9 already saved \u25CB no key yet")}
747
-
748
- `);
749
- renderOAuthLoginOptions(deps);
750
- deps.renderer.write("\n");
751
- const chosen = await runLiveProviderPicker(catalog, saved);
752
- if (!chosen) {
753
- deps.renderer.write(
754
- `
755
- ${color5.dim("OAuth login options:")} ${color5.dim("chatgpt")}, ${color5.dim("claude")}, or ${color5.dim("copilot")}?
756
- `
757
- );
758
- const answer = (await deps.reader.readLine(
759
- ` ${color5.amber("?")} OAuth or q to quit ${color5.dim("[chatgpt/claude/copilot]")}: `
760
- )).trim();
761
- if (answer && await runOAuthLoginChoice(deps, answer, { allowNumeric: false })) {
762
- return true;
763
- }
764
- return false;
765
- }
766
- return addKeyForCatalogProvider(deps, chosen);
767
- }
768
- async function addKeyForCatalogProvider(deps, chosen) {
769
- deps.renderer.write(
770
- color5.dim(`
771
- Defaults from models.dev \u2014 press Enter to keep, or type overrides.
772
- `)
773
- );
774
- const famRaw = (await deps.reader.readLine(
775
- ` ${color5.amber("?")} Family ${color5.dim(`[${chosen.family}]`)} ${color5.dim("(q to quit)")}: `
776
- )).trim();
777
- if (famRaw === "q") return false;
778
- let family = chosen.family;
779
- if (famRaw) {
780
- const validated = validateFamily(famRaw);
781
- if (!validated) {
782
- deps.renderer.writeError(
783
- `Invalid family: "${famRaw}" (must be: anthropic, openai, openai-compatible, google).`
784
- );
785
- return false;
786
- }
787
- family = validated;
788
- }
789
- const baseRaw = (await deps.reader.readLine(
790
- ` ${color5.amber("?")} Base URL ${color5.dim(`[${chosen.apiBase ?? "unset"}]`)} ${color5.dim("(q to quit)")}: `
791
- )).trim();
792
- if (baseRaw === "q") return false;
793
- const baseUrl = baseRaw || chosen.apiBase;
794
- const providersNow = await loadProviders(deps);
795
- let suggestedAlias = chosen.id;
796
- if (family !== chosen.family) {
797
- let candidate = `${chosen.id}-${family}`;
798
- let n = 2;
799
- while (providersNow[candidate]) {
800
- candidate = `${chosen.id}-${family}-${n}`;
801
- n++;
802
- }
803
- suggestedAlias = candidate;
804
- }
805
- const aliasRaw = (await deps.reader.readLine(
806
- ` ${color5.amber("?")} Save as alias ${color5.dim(`[${suggestedAlias}]`)} ${color5.dim("(used with --provider <alias>)")}: `
807
- )).trim();
808
- const alias = aliasRaw || suggestedAlias;
809
- const existing = providersNow[alias];
810
- if (existing) {
811
- const sameFamily = (existing.family ?? chosen.family) === family;
812
- const sameBase = (existing.baseUrl ?? chosen.apiBase) === baseUrl;
813
- if (!sameFamily || !sameBase) {
814
- deps.renderer.writeError(
815
- `Alias "${alias}" already exists with different family/baseUrl.
816
- Existing: family=${existing.family ?? "(unset)"}, baseUrl=${existing.baseUrl ?? "(unset)"}
817
- New: family=${family}, baseUrl=${baseUrl ?? "(unset)"}
818
- Pick a different alias to keep them separate.`
819
- );
820
- return false;
821
- }
822
- }
823
- return addKeyForProvider(alias, deps, {
824
- type: chosen.id,
825
- family,
826
- baseUrl,
827
- envVars: chosen.envVars
828
- });
829
- }
830
- async function addCustomProvider(deps) {
831
- deps.renderer.write(
832
- `
833
- ${color5.bold("Custom provider")} ${color5.dim("\u2014 for local models or proxies not in the catalog.")}
834
- `
835
- );
836
- const providersNow = await loadProviders(deps);
837
- const idRaw = (await deps.reader.readLine(
838
- ` ${color5.amber("?")} Provider id ${color5.dim('(e.g. "local-llama", "my-proxy"; blank = auto custom-N, q to quit)')}: `
839
- )).trim();
840
- if (idRaw === "q") return false;
841
- let type = idRaw;
842
- if (!type) {
843
- type = nextCustomProviderId(Object.keys(providersNow));
844
- deps.renderer.write(` ${color5.dim("Using generated id:")} ${color5.bold(type)}
845
- `);
846
- } else if (providersNow[type]) {
847
- deps.renderer.writeWarning(`"${type}" already exists. Pick it from the main menu to edit.`);
848
- return false;
849
- }
850
- const familyRaw = (await deps.reader.readLine(
851
- ` ${color5.amber("?")} Wire family ${color5.dim("(anthropic | openai | openai-compatible | google)")} ${color5.dim("(q to quit)")}: `
852
- )).trim();
853
- if (familyRaw === "q") return false;
854
- const family = validateFamily(familyRaw);
855
- if (!family) {
856
- deps.renderer.writeError(`Invalid family: "${familyRaw}"`);
857
- return false;
858
- }
859
- const baseUrl = (await deps.reader.readLine(
860
- ` ${color5.amber("?")} Base URL ${color5.dim("(e.g. http://localhost:11434/v1, optional)")}: `
861
- )).trim();
862
- const modelsRaw = (await deps.reader.readLine(
863
- ` ${color5.amber("?")} Model ids ${color5.dim("(comma-separated, optional)")}: `
864
- )).trim();
865
- const models = modelsRaw ? modelsRaw.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
866
- const envVarsRaw = (await deps.reader.readLine(
867
- ` ${color5.amber("?")} Env var names ${color5.dim("(comma-separated, optional)")}: `
868
- )).trim();
869
- const envVars = envVarsRaw ? envVarsRaw.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
870
- return addKeyForProvider(type, deps, {
871
- type,
872
- family,
873
- ...baseUrl ? { baseUrl } : {},
874
- ...models ? { models } : {},
875
- ...envVars ? { envVars } : {}
876
- });
877
- }
878
- async function addManualEntry(deps) {
879
- const pid = (await deps.reader.readLine(` ${color5.amber("?")} Provider id ${color5.dim("[q to quit]")}: `)).trim();
880
- if (!pid || pid === "q") return false;
881
- const famRaw = (await deps.reader.readLine(
882
- ` ${color5.amber("?")} Family ${color5.dim("(anthropic/openai/openai-compatible/google)")}: `
883
- )).trim();
884
- const family = validateFamily(famRaw);
885
- if (!family) {
886
- deps.renderer.writeError(`Invalid family: "${famRaw}"`);
887
- return false;
888
- }
889
- const baseUrl = (await deps.reader.readLine(` ${color5.amber("?")} Base URL ${color5.dim("(optional)")}: `)).trim();
890
- return addKeyForProvider(pid, deps, {
891
- type: pid,
892
- family,
893
- ...baseUrl ? { baseUrl } : {}
894
- });
895
- }
896
- async function addKeyForProvider(providerId, deps, template) {
897
- const providers = await loadProviders(deps);
898
- const existing = providers[providerId];
899
- const existingKeys = existing ? normalizeKeys(existing) : [];
900
- const usedLabels = new Set(existingKeys.map((k) => k.label));
901
- const label = await promptForLabel(deps, usedLabels);
902
- if (!label) return false;
903
- const apiKey = await readKeyInput(deps, `API key for ${providerId}/${label}`);
904
- if (!apiKey) return false;
905
- await mutateConfigProviders(deps.profileConfigPath, deps.vault, (all) => {
906
- const existingProv = all[providerId] ?? {
907
- type: providerId,
908
- ...template
909
- };
910
- if (!existingProv.type) existingProv.type = providerId;
911
- if (!existingProv.family && template.family) {
912
- existingProv.family = template.family;
913
- }
914
- if (!existingProv.baseUrl && template.baseUrl) {
915
- existingProv.baseUrl = template.baseUrl;
916
- }
917
- if (!existingProv.envVars && template.envVars) {
918
- existingProv.envVars = template.envVars;
919
- }
920
- const list = normalizeKeys(existingProv);
921
- list.push({ label, apiKey, createdAt: nowIso() });
922
- writeKeysBack(existingProv, list);
923
- if (!existingProv.activeKey) existingProv.activeKey = label;
924
- all[providerId] = existingProv;
925
- }, deps.profileConfigPath);
926
- deps.renderer.write(
927
- ` ${color5.green("\u2713")} Saved ${color5.bold(providerId)}/${color5.bold(label)}.
928
- `
929
- );
930
- deps.renderer.write(color5.dim(` Launch: wstack --provider ${providerId} "<task>"
931
- `));
932
- try {
933
- await adoptAsDefaultIfUnset(deps, providerId, template);
934
- } catch {
935
- }
936
- return true;
937
- }
938
- async function resolveDefaultModelId(deps, providerId, template) {
939
- if (template.models && template.models.length > 0) return template.models[0];
940
- const catalogId = template.type && template.type !== providerId ? template.type : providerId;
941
- const provider = await deps.modelsRegistry.getProvider(catalogId).catch(() => void 0);
942
- return provider?.models?.[0]?.id;
943
- }
944
- async function adoptAsDefaultIfUnset(deps, providerId, template) {
945
- const targetPath = deps.profileConfigPath;
946
- let cfg = {};
947
- try {
948
- cfg = JSON.parse(await fs.readFile(targetPath, "utf8"));
949
- } catch {
950
- }
951
- if (cfg["provider"] && cfg["model"]) return;
952
- const modelId = await resolveDefaultModelId(deps, providerId, template);
953
- if (!modelId) return;
954
- cfg["provider"] = providerId;
955
- cfg["model"] = modelId;
956
- await atomicWrite(targetPath, JSON.stringify(cfg, null, 2), { mode: 384 });
957
- deps.renderer.write(
958
- ` ${color5.green("\u2713")} Set ${color5.bold(providerId)}/${color5.bold(modelId)} as the default.
959
- `
960
- );
961
- }
962
- async function promptForLabel(deps, usedLabels) {
963
- const defaultLabel = suggestLabel(usedLabels);
964
- const labelRaw = (await deps.reader.readLine(
965
- ` ${color5.amber("?")} Label for this key ${color5.dim(`[${defaultLabel}]`)}: `
966
- )).trim();
967
- const label = labelRaw || defaultLabel;
968
- if (usedLabels.has(label)) {
969
- deps.renderer.writeError(`Label "${label}" is already used. Use update (u) instead.`);
970
- return null;
971
- }
972
- return label;
973
- }
974
-
975
710
  // src/auth-menu/local.ts
976
- import { color as color6 } from "@wrongstack/core/utils";
977
- import { DefaultSecretScrubber as DefaultSecretScrubber2 } from "@wrongstack/core/security";
978
711
  import { probeLocalLlm } from "@wrongstack/runtime/probe";
979
712
 
980
713
  // src/auth-menu/auth-menu-audit.ts
@@ -1091,7 +824,7 @@ async function runAuthLocal(deps, opts = {}) {
1091
824
  if (!probe.ok) {
1092
825
  if (chosen.noAuth) {
1093
826
  deps.renderer.write(
1094
- color6.dim(
827
+ color5.dim(
1095
828
  ` Saving ${chosen.label} anyway \u2014 keyless local gateway needs no key and discovers models when it comes up.
1096
829
  `
1097
830
  )
@@ -1100,7 +833,7 @@ async function runAuthLocal(deps, opts = {}) {
1100
833
  } else {
1101
834
  const saveAnyway = await promptSaveAnyway(deps);
1102
835
  if (!saveAnyway) {
1103
- deps.renderer.write(color6.dim(" Cancelled. Nothing saved.\n"));
836
+ deps.renderer.write(color5.dim(" Cancelled. Nothing saved.\n"));
1104
837
  return 0;
1105
838
  }
1106
839
  probeFailedAndSaved = true;
@@ -1158,19 +891,19 @@ async function runAuthLocal(deps, opts = {}) {
1158
891
  }
1159
892
  if (apiKey) {
1160
893
  deps.renderer.write(
1161
- ` ${color6.green("\u2713")} Saved ${color6.bold(chosen.id)}/${color6.bold(finalLabel)} \u2192 ${color6.cyan(baseUrl)}
894
+ ` ${color5.green("\u2713")} Saved ${color5.bold(chosen.id)}/${color5.bold(finalLabel)} \u2192 ${color5.cyan(baseUrl)}
1162
895
  `
1163
896
  );
1164
897
  } else {
1165
898
  deps.renderer.write(
1166
- ` ${color6.green("\u2713")} Saved ${color6.bold(chosen.id)} (no key) \u2192 ${color6.cyan(baseUrl)}
899
+ ` ${color5.green("\u2713")} Saved ${color5.bold(chosen.id)} (no key) \u2192 ${color5.cyan(baseUrl)}
1167
900
  `
1168
901
  );
1169
902
  }
1170
903
  const firstModel = resolvedModels && resolvedModels.length > 0 ? resolvedModels[0] : null;
1171
- const modelHint = firstModel ? color6.cyan(firstModel) : color6.dim("<model-id>");
904
+ const modelHint = firstModel ? color5.cyan(firstModel) : color5.dim("<model-id>");
1172
905
  deps.renderer.write(
1173
- color6.dim(
906
+ color5.dim(
1174
907
  ` Launch: wstack --provider ${chosen.id} --model ${modelHint} "<task>"
1175
908
  `
1176
909
  )
@@ -1204,51 +937,51 @@ function resolveModelList(models, probe, scrubber) {
1204
937
  return out;
1205
938
  }
1206
939
  function renderProbeResult(deps, probe, opts = {}) {
1207
- const label = opts.presetLabel ? ` ${color6.bold(opts.presetLabel)}` : "";
1208
- const elapsed = probe.elapsedMs !== void 0 ? ` ${color6.dim(`(${probe.elapsedMs}ms)`)}` : "";
940
+ const label = opts.presetLabel ? ` ${color5.bold(opts.presetLabel)}` : "";
941
+ const elapsed = probe.elapsedMs !== void 0 ? ` ${color5.dim(`(${probe.elapsedMs}ms)`)}` : "";
1209
942
  switch (probe.status) {
1210
943
  case "ok": {
1211
- const models = probe.modelCount !== void 0 ? color6.dim(`, ${probe.modelCount} model${probe.modelCount === 1 ? "" : "s"}`) : "";
944
+ const models = probe.modelCount !== void 0 ? color5.dim(`, ${probe.modelCount} model${probe.modelCount === 1 ? "" : "s"}`) : "";
1212
945
  deps.renderer.write(
1213
- ` ${color6.green("\u25CF")} ${label} health probe ok${models}${elapsed}
946
+ ` ${color5.green("\u25CF")} ${label} health probe ok${models}${elapsed}
1214
947
  `
1215
948
  );
1216
949
  break;
1217
950
  }
1218
951
  case "timeout": {
1219
952
  deps.renderer.write(
1220
- ` ${color6.red("\u25CF")} ${label} health probe timed out${elapsed}
1221
- ${color6.dim("The server did not respond within the timeout.")}
1222
- ${color6.dim("Is the local server running? Check the port and try again.")}
953
+ ` ${color5.red("\u25CF")} ${label} health probe timed out${elapsed}
954
+ ${color5.dim("The server did not respond within the timeout.")}
955
+ ${color5.dim("Is the local server running? Check the port and try again.")}
1223
956
  `
1224
957
  );
1225
958
  break;
1226
959
  }
1227
960
  case "unreachable": {
1228
- const detail = probe.detail ? ` ${color6.dim(`(${probe.detail})`)}` : "";
961
+ const detail = probe.detail ? ` ${color5.dim(`(${probe.detail})`)}` : "";
1229
962
  deps.renderer.write(
1230
- ` ${color6.red("\u25CF")} ${label} server unreachable${detail}
1231
- ${color6.dim("Could not connect. Common causes:")}
1232
- ${color6.dim(" - The local server is not running")}
1233
- ${color6.dim(" - The port is blocked by a firewall")}
1234
- ${color6.dim(" - The base URL is wrong")}
963
+ ` ${color5.red("\u25CF")} ${label} server unreachable${detail}
964
+ ${color5.dim("Could not connect. Common causes:")}
965
+ ${color5.dim(" - The local server is not running")}
966
+ ${color5.dim(" - The port is blocked by a firewall")}
967
+ ${color5.dim(" - The base URL is wrong")}
1235
968
  `
1236
969
  );
1237
970
  break;
1238
971
  }
1239
972
  case "http_error": {
1240
973
  const status = probe.httpStatus !== void 0 ? ` HTTP ${probe.httpStatus}` : "";
1241
- const detail = probe.detail ? ` ${color6.dim(`\u2014 ${probe.detail}`)}` : "";
974
+ const detail = probe.detail ? ` ${color5.dim(`\u2014 ${probe.detail}`)}` : "";
1242
975
  deps.renderer.write(
1243
- ` ${color6.yellow("\u25CF")} ${label} health probe got${status}${detail}
976
+ ` ${color5.yellow("\u25CF")} ${label} health probe got${status}${detail}
1244
977
  `
1245
978
  );
1246
979
  break;
1247
980
  }
1248
981
  case "invalid_response": {
1249
982
  deps.renderer.write(
1250
- ` ${color6.yellow("\u25CF")} ${label} health probe returned an unexpected shape
1251
- ` + (probe.detail ? ` ${color6.dim(probe.detail)}
983
+ ` ${color5.yellow("\u25CF")} ${label} health probe returned an unexpected shape
984
+ ` + (probe.detail ? ` ${color5.dim(probe.detail)}
1252
985
  ` : "")
1253
986
  );
1254
987
  break;
@@ -1261,33 +994,33 @@ function renderProbeResult(deps, probe, opts = {}) {
1261
994
  async function promptSaveAnyway(deps) {
1262
995
  const answer = (await deps.reader.readLine(
1263
996
  `
1264
- ${color6.amber("?")} ${color6.bold("Save anyway?")} ${color6.dim("[y/N]")}: `
997
+ ${color5.amber("?")} ${color5.bold("Save anyway?")} ${color5.dim("[y/N]")}: `
1265
998
  )).trim().toLowerCase();
1266
999
  return answer === "y" || answer === "yes";
1267
1000
  }
1268
1001
  async function pickLocalPreset(deps) {
1269
1002
  deps.renderer.write(
1270
1003
  `
1271
- ${color6.bold("Local LLM servers")} ${color6.dim("\u2014 pick one to pre-fill the base URL")}
1004
+ ${color5.bold("Local LLM servers")} ${color5.dim("\u2014 pick one to pre-fill the base URL")}
1272
1005
 
1273
1006
  `
1274
1007
  );
1275
1008
  let idx = 1;
1276
1009
  for (const entry of LOCAL_LLM_PRESETS) {
1277
- const auth = entry.noAuth ? color6.dim("(no auth)") : color6.dim("(optional auth)");
1010
+ const auth = entry.noAuth ? color5.dim("(no auth)") : color5.dim("(optional auth)");
1278
1011
  deps.renderer.write(
1279
- ` ${color6.dim(`${idx}.`.padStart(4))} ${color6.bold(entry.label.padEnd(12))} ${color6.cyan(entry.defaultBaseUrl.padEnd(34))} ${auth}
1012
+ ` ${color5.dim(`${idx}.`.padStart(4))} ${color5.bold(entry.label.padEnd(12))} ${color5.cyan(entry.defaultBaseUrl.padEnd(34))} ${auth}
1280
1013
  `
1281
1014
  );
1282
1015
  deps.renderer.write(
1283
- ` ${color6.dim(entry.hint)}
1016
+ ` ${color5.dim(entry.hint)}
1284
1017
  `
1285
1018
  );
1286
1019
  idx++;
1287
1020
  }
1288
1021
  const answer = (await deps.reader.readLine(
1289
1022
  `
1290
- ${color6.amber("?")} Pick ${color6.dim(`(1-${LOCAL_LLM_PRESETS.length}, q to quit)`)}: `
1023
+ ${color5.amber("?")} Pick ${color5.dim(`(1-${LOCAL_LLM_PRESETS.length}, q to quit)`)}: `
1291
1024
  )).trim().toLowerCase();
1292
1025
  if (!answer || answer === "q" || answer === "quit") return void 0;
1293
1026
  const num = Number.parseInt(answer, 10);
@@ -1301,12 +1034,287 @@ ${color6.amber("?")} Pick ${color6.dim(`(1-${LOCAL_LLM_PRESETS.length}, q to qui
1301
1034
  }
1302
1035
  async function promptOptionalKey(deps, preset) {
1303
1036
  const answer = (await deps.reader.readSecret(
1304
- ` ${color6.amber("?")} ${color6.bold(preset.label)} API key ${color6.dim("(hidden \u2014 Enter to skip if auth is disabled on the server)")}: `
1037
+ ` ${color5.amber("?")} ${color5.bold(preset.label)} API key ${color5.dim("(hidden \u2014 Enter to skip if auth is disabled on the server)")}: `
1305
1038
  )).trim();
1306
1039
  if (!answer) return void 0;
1307
1040
  return answer;
1308
1041
  }
1309
1042
 
1043
+ // src/auth-menu/add-provider.ts
1044
+ import { atomicWrite, color as color6 } from "@wrongstack/core/utils";
1045
+ import { catalogProviderIdFor, PROVIDER_DEFINITIONS } from "@wrongstack/providers";
1046
+ import * as fs from "node:fs/promises";
1047
+
1048
+ // src/provider-id.ts
1049
+ function nextCustomProviderId(existing) {
1050
+ const taken = new Set(existing);
1051
+ let n = 1;
1052
+ while (taken.has(`custom-${n}`)) n++;
1053
+ return `custom-${n}`;
1054
+ }
1055
+
1056
+ // src/auth-menu/add-provider.ts
1057
+ function ownDefinitionsAsCatalog(known) {
1058
+ return Object.values(PROVIDER_DEFINITIONS).filter((d) => !known.has(d.id) && d.family !== "unsupported" && d.usage !== "local").map((d) => ({
1059
+ id: d.id,
1060
+ name: d.name,
1061
+ family: d.family,
1062
+ envVars: [...d.envVars],
1063
+ models: [],
1064
+ // `discoveryOnlyBaseUrl` definitions keep their model-list URL out of the
1065
+ // Base URL prompt: offering it as a default persists it as the provider's
1066
+ // wire base, which is a different endpoint (see the AI Gateway entry).
1067
+ ...d.baseUrl && !d.discoveryOnlyBaseUrl ? { apiBase: d.baseUrl } : {},
1068
+ ...d.docsUrl ? { doc: d.docsUrl } : {}
1069
+ }));
1070
+ }
1071
+ async function addFromCatalog(deps) {
1072
+ let catalog = [];
1073
+ try {
1074
+ catalog = (await deps.modelsRegistry.listProviders()).filter((p) => p.family !== "unsupported");
1075
+ } catch {
1076
+ deps.renderer.writeWarning("Catalog unavailable \u2014 falling back to manual entry.\n");
1077
+ }
1078
+ if (catalog.length === 0) {
1079
+ return addManualEntry(deps);
1080
+ }
1081
+ catalog = [...catalog, ...ownDefinitionsAsCatalog(new Set(catalog.map((p) => p.id)))];
1082
+ const saved = new Set(Object.keys(await loadProviders(deps)));
1083
+ deps.renderer.write(
1084
+ color6.dim(
1085
+ ` Catalog: ${catalog.length} providers. Type to filter, \u2191/\u2193 navigate, Enter to select.
1086
+ `
1087
+ )
1088
+ );
1089
+ deps.renderer.write(` ${color6.dim("\u25C9 already saved \u25CB no key yet")}
1090
+
1091
+ `);
1092
+ renderOAuthLoginOptions(deps);
1093
+ deps.renderer.write("\n");
1094
+ const chosen = await runLiveProviderPicker(catalog, saved);
1095
+ if (!chosen) {
1096
+ deps.renderer.write(
1097
+ `
1098
+ ${color6.dim("OAuth login options:")} ${color6.dim("chatgpt")}, ${color6.dim("claude")}, or ${color6.dim("copilot")}?
1099
+ `
1100
+ );
1101
+ const answer = (await deps.reader.readLine(
1102
+ ` ${color6.amber("?")} OAuth or q to quit ${color6.dim("[chatgpt/claude/copilot]")}: `
1103
+ )).trim();
1104
+ if (answer && await runOAuthLoginChoice(deps, answer, { allowNumeric: false })) {
1105
+ return true;
1106
+ }
1107
+ return false;
1108
+ }
1109
+ return addKeyForCatalogProvider(deps, chosen);
1110
+ }
1111
+ async function addKeyForCatalogProvider(deps, chosen) {
1112
+ deps.renderer.write(
1113
+ color6.dim(`
1114
+ Defaults from models.dev \u2014 press Enter to keep, or type overrides.
1115
+ `)
1116
+ );
1117
+ const famRaw = (await deps.reader.readLine(
1118
+ ` ${color6.amber("?")} Family ${color6.dim(`[${chosen.family}]`)} ${color6.dim("(q to quit)")}: `
1119
+ )).trim();
1120
+ if (famRaw === "q") return false;
1121
+ let family = chosen.family;
1122
+ if (famRaw) {
1123
+ const validated = validateFamily(famRaw);
1124
+ if (!validated) {
1125
+ deps.renderer.writeError(
1126
+ `Invalid family: "${famRaw}" (must be: anthropic, openai, openai-compatible, google).`
1127
+ );
1128
+ return false;
1129
+ }
1130
+ family = validated;
1131
+ }
1132
+ const baseRaw = (await deps.reader.readLine(
1133
+ ` ${color6.amber("?")} Base URL ${color6.dim(`[${chosen.apiBase ?? "unset"}]`)} ${color6.dim("(q to quit)")}: `
1134
+ )).trim();
1135
+ if (baseRaw === "q") return false;
1136
+ const baseUrl = baseRaw || chosen.apiBase;
1137
+ const providersNow = await loadProviders(deps);
1138
+ let suggestedAlias = chosen.id;
1139
+ if (family !== chosen.family) {
1140
+ let candidate = `${chosen.id}-${family}`;
1141
+ let n = 2;
1142
+ while (providersNow[candidate]) {
1143
+ candidate = `${chosen.id}-${family}-${n}`;
1144
+ n++;
1145
+ }
1146
+ suggestedAlias = candidate;
1147
+ }
1148
+ const aliasRaw = (await deps.reader.readLine(
1149
+ ` ${color6.amber("?")} Save as alias ${color6.dim(`[${suggestedAlias}]`)} ${color6.dim("(used with --provider <alias>)")}: `
1150
+ )).trim();
1151
+ const alias = aliasRaw || suggestedAlias;
1152
+ const existing = providersNow[alias];
1153
+ if (existing) {
1154
+ const sameFamily = (existing.family ?? chosen.family) === family;
1155
+ const sameBase = (existing.baseUrl ?? chosen.apiBase) === baseUrl;
1156
+ if (!sameFamily || !sameBase) {
1157
+ deps.renderer.writeError(
1158
+ `Alias "${alias}" already exists with different family/baseUrl.
1159
+ Existing: family=${existing.family ?? "(unset)"}, baseUrl=${existing.baseUrl ?? "(unset)"}
1160
+ New: family=${family}, baseUrl=${baseUrl ?? "(unset)"}
1161
+ Pick a different alias to keep them separate.`
1162
+ );
1163
+ return false;
1164
+ }
1165
+ }
1166
+ return addKeyForProvider(alias, deps, {
1167
+ type: chosen.id,
1168
+ family,
1169
+ baseUrl,
1170
+ envVars: chosen.envVars
1171
+ });
1172
+ }
1173
+ async function addCustomProvider(deps) {
1174
+ deps.renderer.write(
1175
+ `
1176
+ ${color6.bold("Custom provider")} ${color6.dim("\u2014 for local models or proxies not in the catalog.")}
1177
+ `
1178
+ );
1179
+ const providersNow = await loadProviders(deps);
1180
+ const idRaw = (await deps.reader.readLine(
1181
+ ` ${color6.amber("?")} Provider id ${color6.dim('(e.g. "local-llama", "my-proxy"; blank = auto custom-N, q to quit)')}: `
1182
+ )).trim();
1183
+ if (idRaw === "q") return false;
1184
+ let type = idRaw;
1185
+ if (!type) {
1186
+ type = nextCustomProviderId(Object.keys(providersNow));
1187
+ deps.renderer.write(` ${color6.dim("Using generated id:")} ${color6.bold(type)}
1188
+ `);
1189
+ } else if (providersNow[type]) {
1190
+ deps.renderer.writeWarning(`"${type}" already exists. Pick it from the main menu to edit.`);
1191
+ return false;
1192
+ }
1193
+ const familyRaw = (await deps.reader.readLine(
1194
+ ` ${color6.amber("?")} Wire family ${color6.dim("(anthropic | openai | openai-compatible | google)")} ${color6.dim("(q to quit)")}: `
1195
+ )).trim();
1196
+ if (familyRaw === "q") return false;
1197
+ const family = validateFamily(familyRaw);
1198
+ if (!family) {
1199
+ deps.renderer.writeError(`Invalid family: "${familyRaw}"`);
1200
+ return false;
1201
+ }
1202
+ const baseUrl = (await deps.reader.readLine(
1203
+ ` ${color6.amber("?")} Base URL ${color6.dim("(e.g. http://localhost:11434/v1, optional)")}: `
1204
+ )).trim();
1205
+ const modelsRaw = (await deps.reader.readLine(
1206
+ ` ${color6.amber("?")} Model ids ${color6.dim("(comma-separated, optional)")}: `
1207
+ )).trim();
1208
+ const models = modelsRaw ? modelsRaw.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
1209
+ const envVarsRaw = (await deps.reader.readLine(
1210
+ ` ${color6.amber("?")} Env var names ${color6.dim("(comma-separated, optional)")}: `
1211
+ )).trim();
1212
+ const envVars = envVarsRaw ? envVarsRaw.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
1213
+ return addKeyForProvider(type, deps, {
1214
+ type,
1215
+ family,
1216
+ ...baseUrl ? { baseUrl } : {},
1217
+ ...models ? { models } : {},
1218
+ ...envVars ? { envVars } : {}
1219
+ });
1220
+ }
1221
+ async function addManualEntry(deps) {
1222
+ const pid = (await deps.reader.readLine(` ${color6.amber("?")} Provider id ${color6.dim("[q to quit]")}: `)).trim();
1223
+ if (!pid || pid === "q") return false;
1224
+ const famRaw = (await deps.reader.readLine(
1225
+ ` ${color6.amber("?")} Family ${color6.dim("(anthropic/openai/openai-compatible/google)")}: `
1226
+ )).trim();
1227
+ const family = validateFamily(famRaw);
1228
+ if (!family) {
1229
+ deps.renderer.writeError(`Invalid family: "${famRaw}"`);
1230
+ return false;
1231
+ }
1232
+ const baseUrl = (await deps.reader.readLine(` ${color6.amber("?")} Base URL ${color6.dim("(optional)")}: `)).trim();
1233
+ return addKeyForProvider(pid, deps, {
1234
+ type: pid,
1235
+ family,
1236
+ ...baseUrl ? { baseUrl } : {}
1237
+ });
1238
+ }
1239
+ async function addKeyForProvider(providerId, deps, template) {
1240
+ const providers = await loadProviders(deps);
1241
+ const existing = providers[providerId];
1242
+ const existingKeys = existing ? normalizeKeys(existing) : [];
1243
+ const usedLabels = new Set(existingKeys.map((k) => k.label));
1244
+ const label = await promptForLabel(deps, usedLabels);
1245
+ if (!label) return false;
1246
+ const apiKey = await readKeyInput(deps, `API key for ${providerId}/${label}`);
1247
+ if (!apiKey) return false;
1248
+ await mutateConfigProviders(deps.profileConfigPath, deps.vault, (all) => {
1249
+ const existingProv = all[providerId] ?? {
1250
+ type: providerId,
1251
+ ...template
1252
+ };
1253
+ if (!existingProv.type) existingProv.type = providerId;
1254
+ if (!existingProv.family && template.family) {
1255
+ existingProv.family = template.family;
1256
+ }
1257
+ if (!existingProv.baseUrl && template.baseUrl) {
1258
+ existingProv.baseUrl = template.baseUrl;
1259
+ }
1260
+ if (!existingProv.envVars && template.envVars) {
1261
+ existingProv.envVars = template.envVars;
1262
+ }
1263
+ const list = normalizeKeys(existingProv);
1264
+ list.push({ label, apiKey, createdAt: nowIso() });
1265
+ writeKeysBack(existingProv, list);
1266
+ if (!existingProv.activeKey) existingProv.activeKey = label;
1267
+ all[providerId] = existingProv;
1268
+ }, deps.profileConfigPath);
1269
+ deps.renderer.write(
1270
+ ` ${color6.green("\u2713")} Saved ${color6.bold(providerId)}/${color6.bold(label)}.
1271
+ `
1272
+ );
1273
+ deps.renderer.write(color6.dim(` Launch: wstack --provider ${providerId} "<task>"
1274
+ `));
1275
+ try {
1276
+ await adoptAsDefaultIfUnset(deps, providerId, template);
1277
+ } catch {
1278
+ }
1279
+ return true;
1280
+ }
1281
+ async function resolveDefaultModelId(deps, providerId, template) {
1282
+ if (template.models && template.models.length > 0) return template.models[0];
1283
+ const catalogId = catalogProviderIdFor(providerId, template.type);
1284
+ const provider = await deps.modelsRegistry.getProvider(catalogId).catch(() => void 0);
1285
+ return provider?.models?.[0]?.id;
1286
+ }
1287
+ async function adoptAsDefaultIfUnset(deps, providerId, template) {
1288
+ const targetPath = deps.profileConfigPath;
1289
+ let cfg = {};
1290
+ try {
1291
+ cfg = JSON.parse(await fs.readFile(targetPath, "utf8"));
1292
+ } catch {
1293
+ }
1294
+ if (cfg["provider"] && cfg["model"]) return;
1295
+ const modelId = await resolveDefaultModelId(deps, providerId, template);
1296
+ if (!modelId) return;
1297
+ cfg["provider"] = providerId;
1298
+ cfg["model"] = modelId;
1299
+ await atomicWrite(targetPath, JSON.stringify(cfg, null, 2), { mode: 384 });
1300
+ deps.renderer.write(
1301
+ ` ${color6.green("\u2713")} Set ${color6.bold(providerId)}/${color6.bold(modelId)} as the default.
1302
+ `
1303
+ );
1304
+ }
1305
+ async function promptForLabel(deps, usedLabels) {
1306
+ const defaultLabel = suggestLabel(usedLabels);
1307
+ const labelRaw = (await deps.reader.readLine(
1308
+ ` ${color6.amber("?")} Label for this key ${color6.dim(`[${defaultLabel}]`)}: `
1309
+ )).trim();
1310
+ const label = labelRaw || defaultLabel;
1311
+ if (usedLabels.has(label)) {
1312
+ deps.renderer.writeError(`Label "${label}" is already used. Use update (u) instead.`);
1313
+ return null;
1314
+ }
1315
+ return label;
1316
+ }
1317
+
1310
1318
  export {
1311
1319
  loadProviders,
1312
1320
  renderProviderHeader,
@@ -1327,4 +1335,4 @@ export {
1327
1335
  addKeyForProvider,
1328
1336
  runAuthLocal
1329
1337
  };
1330
- //# sourceMappingURL=chunk-L5CE52XW.js.map
1338
+ //# sourceMappingURL=chunk-O7XNGCRT.js.map