@wrongstack/cli 0.300.0 → 0.302.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 (62) hide show
  1. package/dist/{acp-IDNS2YVE.js → acp-DATHHPNT.js} +34 -15
  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-6T6ZOT2R.js → auth-PJD3JRH2.js} +150 -37
  5. package/dist/boot/dispatch-webui.d.ts +3 -16
  6. package/dist/boot/launch-menu.d.ts +2 -4
  7. package/dist/boot/tool-registry.d.ts +3 -0
  8. package/dist/boot/webui-session-child.d.ts +91 -0
  9. package/dist/chimera-reviewer-policy.d.ts +10 -3
  10. package/dist/chimera-work-registry.d.ts +19 -0
  11. package/dist/{chronicle-QQEQFSSE.js → chronicle-63MFCZN6.js} +26 -5
  12. package/dist/{chunk-YWUPHRGB.js → chunk-263G3FMB.js} +33 -10
  13. package/dist/{chunk-GYDQABMA.js → chunk-2KDOFTTM.js} +44 -32
  14. package/dist/{chunk-V76R7DC5.js → chunk-3MOUBRKZ.js} +311 -309
  15. package/dist/chunk-CSAPOCBP.js +24 -0
  16. package/dist/{chunk-N7ULWBO5.js → chunk-ETOEGL3V.js} +3 -7
  17. package/dist/{chunk-3MWUZMOL.js → chunk-GUHQQG63.js} +14 -3
  18. package/dist/chunk-KE7E7DPX.js +38 -0
  19. package/dist/{chunk-PEMN4T5O.js → chunk-MT6FGXO3.js} +23 -4
  20. package/dist/chunk-PFVVUH5B.js +127 -0
  21. package/dist/{chunk-DPJZQCVP.js → chunk-YMXXOOFN.js} +1 -1
  22. package/dist/{chunk-4WJKVQJY.js → chunk-ZECLGUTF.js} +15 -3
  23. package/dist/cli-context.d.ts +2 -0
  24. package/dist/{cli-main-7B3G2YSG.js → cli-main-EOJ74CI4.js} +336 -99
  25. package/dist/execute-deps.d.ts +3 -0
  26. package/dist/{execution-SHGM7BVC.js → execution-CQ772VPS.js} +246 -97
  27. package/dist/execution-chimera-cascade.d.ts +3 -4
  28. package/dist/execution-chimera-review.d.ts +2 -3
  29. package/dist/execution-cleanup.d.ts +3 -2
  30. package/dist/execution.d.ts +1 -1
  31. package/dist/{export-TPORYVRZ.js → export-DOBGPY23.js} +12 -1
  32. package/dist/fleet/host-context.d.ts +1 -1
  33. package/dist/fleet/host-helpers.d.ts +14 -0
  34. package/dist/fleet/status-broadcast.d.ts +1 -1
  35. package/dist/{hq-DVDD4XPV.js → hq-LN5CJHCM.js} +2 -2
  36. package/dist/hq-server/routes.d.ts +10 -1
  37. package/dist/hq-server/ws.d.ts +17 -6
  38. package/dist/{hq-server-DQHLNHDL.js → hq-server-S7HZG3PA.js} +3 -2
  39. package/dist/index.js +142 -100
  40. package/dist/live-settings-input.d.ts +5 -0
  41. package/dist/{mcp-CYLPUEHC.js → mcp-OHPJH73U.js} +6 -2
  42. package/dist/{modeldiag-YOQZHBQV.js → modeldiag-3DOK4BWW.js} +2 -2
  43. package/dist/{plugin-usage-EOG4ET4S.js → plugin-usage-YEVLOR33.js} +7 -3
  44. package/dist/{plugins-56CMWMPR.js → plugins-K3BAUP4D.js} +3 -3
  45. package/dist/profile-config-path.d.ts +0 -2
  46. package/dist/provider-helpers.d.ts +0 -5
  47. package/dist/{providers-models-K6RT7ABI.js → providers-models-WEZ4EGLL.js} +6 -5
  48. package/dist/{replay-KCTXNMZQ.js → replay-FCBNHW3K.js} +5 -1
  49. package/dist/{rewind-EJMVFVLS.js → rewind-AOD3JIYX.js} +5 -1
  50. package/dist/{sessions-config-NLCMK3BY.js → sessions-config-XXN7267N.js} +8 -3
  51. package/dist/subcommands/flags.d.ts +23 -0
  52. package/dist/terminal-format.d.ts +15 -0
  53. package/dist/{webui-server-G3EKFXLL.js → webui-server-NFNRGNNT.js} +54 -29
  54. package/dist/webui-server-options.d.ts +7 -0
  55. package/dist/wiring/brain-and-orchestration.d.ts +1 -1
  56. package/dist/wiring/mailbox-bridge-bootstrap.d.ts +3 -1
  57. package/dist/wiring/management-tools.d.ts +9 -1
  58. package/dist/wiring/pipeline.d.ts +5 -5
  59. package/dist/wiring/plugins.d.ts +2 -3
  60. package/dist/wiring/provider-utility-tools.d.ts +1 -0
  61. package/dist/wiring/tools.d.ts +4 -5
  62. package/package.json +23 -23
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  LOCAL_LLM_PRESETS,
3
3
  runLiveProviderPicker
4
- } from "./chunk-PEMN4T5O.js";
4
+ } from "./chunk-MT6FGXO3.js";
5
5
  import {
6
6
  openBrowser,
7
7
  runCodexOAuthLogin,
@@ -535,6 +535,10 @@ async function runOAuthLoginMenu(deps) {
535
535
  await runOAuthLoginChoice(deps, pick);
536
536
  }
537
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
+
538
542
  // src/auth-menu/helpers.ts
539
543
  async function loadProviders(deps) {
540
544
  return loadConfigProviders(deps.profileConfigPath, deps.vault, {
@@ -703,284 +707,7 @@ function validateFamily(raw) {
703
707
  return valid.includes(raw) ? raw : null;
704
708
  }
705
709
 
706
- // src/auth-menu/add-provider.ts
707
- import { atomicWrite, color as color5 } from "@wrongstack/core/utils";
708
- import { catalogProviderIdFor, PROVIDER_DEFINITIONS } from "@wrongstack/providers";
709
- import * as fs from "node:fs/promises";
710
-
711
- // src/provider-id.ts
712
- function nextCustomProviderId(existing) {
713
- const taken = new Set(existing);
714
- let n = 1;
715
- while (taken.has(`custom-${n}`)) n++;
716
- return `custom-${n}`;
717
- }
718
-
719
- // src/auth-menu/add-provider.ts
720
- function ownDefinitionsAsCatalog(known) {
721
- return Object.values(PROVIDER_DEFINITIONS).filter((d) => !known.has(d.id) && d.family !== "unsupported" && d.usage !== "local").map((d) => ({
722
- id: d.id,
723
- name: d.name,
724
- family: d.family,
725
- envVars: [...d.envVars],
726
- models: [],
727
- // `discoveryOnlyBaseUrl` definitions keep their model-list URL out of the
728
- // Base URL prompt: offering it as a default persists it as the provider's
729
- // wire base, which is a different endpoint (see the AI Gateway entry).
730
- ...d.baseUrl && !d.discoveryOnlyBaseUrl ? { apiBase: d.baseUrl } : {},
731
- ...d.docsUrl ? { doc: d.docsUrl } : {}
732
- }));
733
- }
734
- async function addFromCatalog(deps) {
735
- let catalog = [];
736
- try {
737
- catalog = (await deps.modelsRegistry.listProviders()).filter((p) => p.family !== "unsupported");
738
- } catch {
739
- deps.renderer.writeWarning("Catalog unavailable \u2014 falling back to manual entry.\n");
740
- }
741
- if (catalog.length === 0) {
742
- return addManualEntry(deps);
743
- }
744
- catalog = [...catalog, ...ownDefinitionsAsCatalog(new Set(catalog.map((p) => p.id)))];
745
- const saved = new Set(Object.keys(await loadProviders(deps)));
746
- deps.renderer.write(
747
- color5.dim(
748
- ` Catalog: ${catalog.length} providers. Type to filter, \u2191/\u2193 navigate, Enter to select.
749
- `
750
- )
751
- );
752
- deps.renderer.write(` ${color5.dim("\u25C9 already saved \u25CB no key yet")}
753
-
754
- `);
755
- renderOAuthLoginOptions(deps);
756
- deps.renderer.write("\n");
757
- const chosen = await runLiveProviderPicker(catalog, saved);
758
- if (!chosen) {
759
- deps.renderer.write(
760
- `
761
- ${color5.dim("OAuth login options:")} ${color5.dim("chatgpt")}, ${color5.dim("claude")}, or ${color5.dim("copilot")}?
762
- `
763
- );
764
- const answer = (await deps.reader.readLine(
765
- ` ${color5.amber("?")} OAuth or q to quit ${color5.dim("[chatgpt/claude/copilot]")}: `
766
- )).trim();
767
- if (answer && await runOAuthLoginChoice(deps, answer, { allowNumeric: false })) {
768
- return true;
769
- }
770
- return false;
771
- }
772
- return addKeyForCatalogProvider(deps, chosen);
773
- }
774
- async function addKeyForCatalogProvider(deps, chosen) {
775
- deps.renderer.write(
776
- color5.dim(`
777
- Defaults from models.dev \u2014 press Enter to keep, or type overrides.
778
- `)
779
- );
780
- const famRaw = (await deps.reader.readLine(
781
- ` ${color5.amber("?")} Family ${color5.dim(`[${chosen.family}]`)} ${color5.dim("(q to quit)")}: `
782
- )).trim();
783
- if (famRaw === "q") return false;
784
- let family = chosen.family;
785
- if (famRaw) {
786
- const validated = validateFamily(famRaw);
787
- if (!validated) {
788
- deps.renderer.writeError(
789
- `Invalid family: "${famRaw}" (must be: anthropic, openai, openai-compatible, google).`
790
- );
791
- return false;
792
- }
793
- family = validated;
794
- }
795
- const baseRaw = (await deps.reader.readLine(
796
- ` ${color5.amber("?")} Base URL ${color5.dim(`[${chosen.apiBase ?? "unset"}]`)} ${color5.dim("(q to quit)")}: `
797
- )).trim();
798
- if (baseRaw === "q") return false;
799
- const baseUrl = baseRaw || chosen.apiBase;
800
- const providersNow = await loadProviders(deps);
801
- let suggestedAlias = chosen.id;
802
- if (family !== chosen.family) {
803
- let candidate = `${chosen.id}-${family}`;
804
- let n = 2;
805
- while (providersNow[candidate]) {
806
- candidate = `${chosen.id}-${family}-${n}`;
807
- n++;
808
- }
809
- suggestedAlias = candidate;
810
- }
811
- const aliasRaw = (await deps.reader.readLine(
812
- ` ${color5.amber("?")} Save as alias ${color5.dim(`[${suggestedAlias}]`)} ${color5.dim("(used with --provider <alias>)")}: `
813
- )).trim();
814
- const alias = aliasRaw || suggestedAlias;
815
- const existing = providersNow[alias];
816
- if (existing) {
817
- const sameFamily = (existing.family ?? chosen.family) === family;
818
- const sameBase = (existing.baseUrl ?? chosen.apiBase) === baseUrl;
819
- if (!sameFamily || !sameBase) {
820
- deps.renderer.writeError(
821
- `Alias "${alias}" already exists with different family/baseUrl.
822
- Existing: family=${existing.family ?? "(unset)"}, baseUrl=${existing.baseUrl ?? "(unset)"}
823
- New: family=${family}, baseUrl=${baseUrl ?? "(unset)"}
824
- Pick a different alias to keep them separate.`
825
- );
826
- return false;
827
- }
828
- }
829
- return addKeyForProvider(alias, deps, {
830
- type: chosen.id,
831
- family,
832
- baseUrl,
833
- envVars: chosen.envVars
834
- });
835
- }
836
- async function addCustomProvider(deps) {
837
- deps.renderer.write(
838
- `
839
- ${color5.bold("Custom provider")} ${color5.dim("\u2014 for local models or proxies not in the catalog.")}
840
- `
841
- );
842
- const providersNow = await loadProviders(deps);
843
- const idRaw = (await deps.reader.readLine(
844
- ` ${color5.amber("?")} Provider id ${color5.dim('(e.g. "local-llama", "my-proxy"; blank = auto custom-N, q to quit)')}: `
845
- )).trim();
846
- if (idRaw === "q") return false;
847
- let type = idRaw;
848
- if (!type) {
849
- type = nextCustomProviderId(Object.keys(providersNow));
850
- deps.renderer.write(` ${color5.dim("Using generated id:")} ${color5.bold(type)}
851
- `);
852
- } else if (providersNow[type]) {
853
- deps.renderer.writeWarning(`"${type}" already exists. Pick it from the main menu to edit.`);
854
- return false;
855
- }
856
- const familyRaw = (await deps.reader.readLine(
857
- ` ${color5.amber("?")} Wire family ${color5.dim("(anthropic | openai | openai-compatible | google)")} ${color5.dim("(q to quit)")}: `
858
- )).trim();
859
- if (familyRaw === "q") return false;
860
- const family = validateFamily(familyRaw);
861
- if (!family) {
862
- deps.renderer.writeError(`Invalid family: "${familyRaw}"`);
863
- return false;
864
- }
865
- const baseUrl = (await deps.reader.readLine(
866
- ` ${color5.amber("?")} Base URL ${color5.dim("(e.g. http://localhost:11434/v1, optional)")}: `
867
- )).trim();
868
- const modelsRaw = (await deps.reader.readLine(
869
- ` ${color5.amber("?")} Model ids ${color5.dim("(comma-separated, optional)")}: `
870
- )).trim();
871
- const models = modelsRaw ? modelsRaw.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
872
- const envVarsRaw = (await deps.reader.readLine(
873
- ` ${color5.amber("?")} Env var names ${color5.dim("(comma-separated, optional)")}: `
874
- )).trim();
875
- const envVars = envVarsRaw ? envVarsRaw.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
876
- return addKeyForProvider(type, deps, {
877
- type,
878
- family,
879
- ...baseUrl ? { baseUrl } : {},
880
- ...models ? { models } : {},
881
- ...envVars ? { envVars } : {}
882
- });
883
- }
884
- async function addManualEntry(deps) {
885
- const pid = (await deps.reader.readLine(` ${color5.amber("?")} Provider id ${color5.dim("[q to quit]")}: `)).trim();
886
- if (!pid || pid === "q") return false;
887
- const famRaw = (await deps.reader.readLine(
888
- ` ${color5.amber("?")} Family ${color5.dim("(anthropic/openai/openai-compatible/google)")}: `
889
- )).trim();
890
- const family = validateFamily(famRaw);
891
- if (!family) {
892
- deps.renderer.writeError(`Invalid family: "${famRaw}"`);
893
- return false;
894
- }
895
- const baseUrl = (await deps.reader.readLine(` ${color5.amber("?")} Base URL ${color5.dim("(optional)")}: `)).trim();
896
- return addKeyForProvider(pid, deps, {
897
- type: pid,
898
- family,
899
- ...baseUrl ? { baseUrl } : {}
900
- });
901
- }
902
- async function addKeyForProvider(providerId, deps, template) {
903
- const providers = await loadProviders(deps);
904
- const existing = providers[providerId];
905
- const existingKeys = existing ? normalizeKeys(existing) : [];
906
- const usedLabels = new Set(existingKeys.map((k) => k.label));
907
- const label = await promptForLabel(deps, usedLabels);
908
- if (!label) return false;
909
- const apiKey = await readKeyInput(deps, `API key for ${providerId}/${label}`);
910
- if (!apiKey) return false;
911
- await mutateConfigProviders(deps.profileConfigPath, deps.vault, (all) => {
912
- const existingProv = all[providerId] ?? {
913
- type: providerId,
914
- ...template
915
- };
916
- if (!existingProv.type) existingProv.type = providerId;
917
- if (!existingProv.family && template.family) {
918
- existingProv.family = template.family;
919
- }
920
- if (!existingProv.baseUrl && template.baseUrl) {
921
- existingProv.baseUrl = template.baseUrl;
922
- }
923
- if (!existingProv.envVars && template.envVars) {
924
- existingProv.envVars = template.envVars;
925
- }
926
- const list = normalizeKeys(existingProv);
927
- list.push({ label, apiKey, createdAt: nowIso() });
928
- writeKeysBack(existingProv, list);
929
- if (!existingProv.activeKey) existingProv.activeKey = label;
930
- all[providerId] = existingProv;
931
- }, deps.profileConfigPath);
932
- deps.renderer.write(
933
- ` ${color5.green("\u2713")} Saved ${color5.bold(providerId)}/${color5.bold(label)}.
934
- `
935
- );
936
- deps.renderer.write(color5.dim(` Launch: wstack --provider ${providerId} "<task>"
937
- `));
938
- try {
939
- await adoptAsDefaultIfUnset(deps, providerId, template);
940
- } catch {
941
- }
942
- return true;
943
- }
944
- async function resolveDefaultModelId(deps, providerId, template) {
945
- if (template.models && template.models.length > 0) return template.models[0];
946
- const catalogId = catalogProviderIdFor(providerId, template.type);
947
- const provider = await deps.modelsRegistry.getProvider(catalogId).catch(() => void 0);
948
- return provider?.models?.[0]?.id;
949
- }
950
- async function adoptAsDefaultIfUnset(deps, providerId, template) {
951
- const targetPath = deps.profileConfigPath;
952
- let cfg = {};
953
- try {
954
- cfg = JSON.parse(await fs.readFile(targetPath, "utf8"));
955
- } catch {
956
- }
957
- if (cfg["provider"] && cfg["model"]) return;
958
- const modelId = await resolveDefaultModelId(deps, providerId, template);
959
- if (!modelId) return;
960
- cfg["provider"] = providerId;
961
- cfg["model"] = modelId;
962
- await atomicWrite(targetPath, JSON.stringify(cfg, null, 2), { mode: 384 });
963
- deps.renderer.write(
964
- ` ${color5.green("\u2713")} Set ${color5.bold(providerId)}/${color5.bold(modelId)} as the default.
965
- `
966
- );
967
- }
968
- async function promptForLabel(deps, usedLabels) {
969
- const defaultLabel = suggestLabel(usedLabels);
970
- const labelRaw = (await deps.reader.readLine(
971
- ` ${color5.amber("?")} Label for this key ${color5.dim(`[${defaultLabel}]`)}: `
972
- )).trim();
973
- const label = labelRaw || defaultLabel;
974
- if (usedLabels.has(label)) {
975
- deps.renderer.writeError(`Label "${label}" is already used. Use update (u) instead.`);
976
- return null;
977
- }
978
- return label;
979
- }
980
-
981
710
  // src/auth-menu/local.ts
982
- import { color as color6 } from "@wrongstack/core/utils";
983
- import { DefaultSecretScrubber as DefaultSecretScrubber2 } from "@wrongstack/core/security";
984
711
  import { probeLocalLlm } from "@wrongstack/runtime/probe";
985
712
 
986
713
  // src/auth-menu/auth-menu-audit.ts
@@ -1097,7 +824,7 @@ async function runAuthLocal(deps, opts = {}) {
1097
824
  if (!probe.ok) {
1098
825
  if (chosen.noAuth) {
1099
826
  deps.renderer.write(
1100
- color6.dim(
827
+ color5.dim(
1101
828
  ` Saving ${chosen.label} anyway \u2014 keyless local gateway needs no key and discovers models when it comes up.
1102
829
  `
1103
830
  )
@@ -1106,7 +833,7 @@ async function runAuthLocal(deps, opts = {}) {
1106
833
  } else {
1107
834
  const saveAnyway = await promptSaveAnyway(deps);
1108
835
  if (!saveAnyway) {
1109
- deps.renderer.write(color6.dim(" Cancelled. Nothing saved.\n"));
836
+ deps.renderer.write(color5.dim(" Cancelled. Nothing saved.\n"));
1110
837
  return 0;
1111
838
  }
1112
839
  probeFailedAndSaved = true;
@@ -1164,19 +891,19 @@ async function runAuthLocal(deps, opts = {}) {
1164
891
  }
1165
892
  if (apiKey) {
1166
893
  deps.renderer.write(
1167
- ` ${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)}
1168
895
  `
1169
896
  );
1170
897
  } else {
1171
898
  deps.renderer.write(
1172
- ` ${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)}
1173
900
  `
1174
901
  );
1175
902
  }
1176
903
  const firstModel = resolvedModels && resolvedModels.length > 0 ? resolvedModels[0] : null;
1177
- const modelHint = firstModel ? color6.cyan(firstModel) : color6.dim("<model-id>");
904
+ const modelHint = firstModel ? color5.cyan(firstModel) : color5.dim("<model-id>");
1178
905
  deps.renderer.write(
1179
- color6.dim(
906
+ color5.dim(
1180
907
  ` Launch: wstack --provider ${chosen.id} --model ${modelHint} "<task>"
1181
908
  `
1182
909
  )
@@ -1210,51 +937,51 @@ function resolveModelList(models, probe, scrubber) {
1210
937
  return out;
1211
938
  }
1212
939
  function renderProbeResult(deps, probe, opts = {}) {
1213
- const label = opts.presetLabel ? ` ${color6.bold(opts.presetLabel)}` : "";
1214
- 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)`)}` : "";
1215
942
  switch (probe.status) {
1216
943
  case "ok": {
1217
- 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"}`) : "";
1218
945
  deps.renderer.write(
1219
- ` ${color6.green("\u25CF")} ${label} health probe ok${models}${elapsed}
946
+ ` ${color5.green("\u25CF")} ${label} health probe ok${models}${elapsed}
1220
947
  `
1221
948
  );
1222
949
  break;
1223
950
  }
1224
951
  case "timeout": {
1225
952
  deps.renderer.write(
1226
- ` ${color6.red("\u25CF")} ${label} health probe timed out${elapsed}
1227
- ${color6.dim("The server did not respond within the timeout.")}
1228
- ${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.")}
1229
956
  `
1230
957
  );
1231
958
  break;
1232
959
  }
1233
960
  case "unreachable": {
1234
- const detail = probe.detail ? ` ${color6.dim(`(${probe.detail})`)}` : "";
961
+ const detail = probe.detail ? ` ${color5.dim(`(${probe.detail})`)}` : "";
1235
962
  deps.renderer.write(
1236
- ` ${color6.red("\u25CF")} ${label} server unreachable${detail}
1237
- ${color6.dim("Could not connect. Common causes:")}
1238
- ${color6.dim(" - The local server is not running")}
1239
- ${color6.dim(" - The port is blocked by a firewall")}
1240
- ${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")}
1241
968
  `
1242
969
  );
1243
970
  break;
1244
971
  }
1245
972
  case "http_error": {
1246
973
  const status = probe.httpStatus !== void 0 ? ` HTTP ${probe.httpStatus}` : "";
1247
- const detail = probe.detail ? ` ${color6.dim(`\u2014 ${probe.detail}`)}` : "";
974
+ const detail = probe.detail ? ` ${color5.dim(`\u2014 ${probe.detail}`)}` : "";
1248
975
  deps.renderer.write(
1249
- ` ${color6.yellow("\u25CF")} ${label} health probe got${status}${detail}
976
+ ` ${color5.yellow("\u25CF")} ${label} health probe got${status}${detail}
1250
977
  `
1251
978
  );
1252
979
  break;
1253
980
  }
1254
981
  case "invalid_response": {
1255
982
  deps.renderer.write(
1256
- ` ${color6.yellow("\u25CF")} ${label} health probe returned an unexpected shape
1257
- ` + (probe.detail ? ` ${color6.dim(probe.detail)}
983
+ ` ${color5.yellow("\u25CF")} ${label} health probe returned an unexpected shape
984
+ ` + (probe.detail ? ` ${color5.dim(probe.detail)}
1258
985
  ` : "")
1259
986
  );
1260
987
  break;
@@ -1267,33 +994,33 @@ function renderProbeResult(deps, probe, opts = {}) {
1267
994
  async function promptSaveAnyway(deps) {
1268
995
  const answer = (await deps.reader.readLine(
1269
996
  `
1270
- ${color6.amber("?")} ${color6.bold("Save anyway?")} ${color6.dim("[y/N]")}: `
997
+ ${color5.amber("?")} ${color5.bold("Save anyway?")} ${color5.dim("[y/N]")}: `
1271
998
  )).trim().toLowerCase();
1272
999
  return answer === "y" || answer === "yes";
1273
1000
  }
1274
1001
  async function pickLocalPreset(deps) {
1275
1002
  deps.renderer.write(
1276
1003
  `
1277
- ${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")}
1278
1005
 
1279
1006
  `
1280
1007
  );
1281
1008
  let idx = 1;
1282
1009
  for (const entry of LOCAL_LLM_PRESETS) {
1283
- 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)");
1284
1011
  deps.renderer.write(
1285
- ` ${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}
1286
1013
  `
1287
1014
  );
1288
1015
  deps.renderer.write(
1289
- ` ${color6.dim(entry.hint)}
1016
+ ` ${color5.dim(entry.hint)}
1290
1017
  `
1291
1018
  );
1292
1019
  idx++;
1293
1020
  }
1294
1021
  const answer = (await deps.reader.readLine(
1295
1022
  `
1296
- ${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)`)}: `
1297
1024
  )).trim().toLowerCase();
1298
1025
  if (!answer || answer === "q" || answer === "quit") return void 0;
1299
1026
  const num = Number.parseInt(answer, 10);
@@ -1307,12 +1034,287 @@ ${color6.amber("?")} Pick ${color6.dim(`(1-${LOCAL_LLM_PRESETS.length}, q to qui
1307
1034
  }
1308
1035
  async function promptOptionalKey(deps, preset) {
1309
1036
  const answer = (await deps.reader.readSecret(
1310
- ` ${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)")}: `
1311
1038
  )).trim();
1312
1039
  if (!answer) return void 0;
1313
1040
  return answer;
1314
1041
  }
1315
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
+
1316
1318
  export {
1317
1319
  loadProviders,
1318
1320
  renderProviderHeader,
@@ -1333,4 +1335,4 @@ export {
1333
1335
  addKeyForProvider,
1334
1336
  runAuthLocal
1335
1337
  };
1336
- //# sourceMappingURL=chunk-V76R7DC5.js.map
1338
+ //# sourceMappingURL=chunk-3MOUBRKZ.js.map