@wrongstack/cli 1.0.33 → 1.0.36

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 (145) hide show
  1. package/dist/{acp-EDGUWU7N.js → acp-QLU7T5WX.js} +470 -466
  2. package/dist/{auth-SUHKFFO4.js → auth-AED7RW7D.js} +7 -7
  3. package/dist/auth-menu/openai-codex-oauth.d.ts +1 -1
  4. package/dist/auth-menu/panel-provider-edits.d.ts +15 -0
  5. package/dist/auth-menu/panel-service.d.ts +1 -1
  6. package/dist/{automation-MCDFCIMV.js → automation-3E3AUPBF.js} +19 -4
  7. package/dist/boot/governance-evidence-candidate.d.ts +1 -2
  8. package/dist/boot/system-prompt-builder.d.ts +1 -1
  9. package/dist/boot/tui-project-spawn.d.ts +0 -1
  10. package/dist/boot/tui-session-resume-contracts.d.ts +95 -0
  11. package/dist/boot/tui-session-resume.d.ts +2 -88
  12. package/dist/boot/tui-settings-read.d.ts +3 -0
  13. package/dist/{boot-auto-thin-K2DHHYIW.js → boot-auto-thin-AGJ6TAWY.js} +3 -3
  14. package/dist/boot-launch-choices.d.ts +22 -0
  15. package/dist/boot-provider-gate.d.ts +28 -0
  16. package/dist/boot-subcommand-dispatch.d.ts +24 -0
  17. package/dist/boot.d.ts +1 -1
  18. package/dist/chimera-cascade-evidence.d.ts +1 -1
  19. package/dist/{chunk-TI7KLHYX.js → chunk-2CMMWRQH.js} +81 -80
  20. package/dist/{chunk-4BX2YDTQ.js → chunk-4JMAWKLA.js} +45 -4
  21. package/dist/{chunk-TWSQTQKR.js → chunk-4R6TVMYA.js} +2 -2
  22. package/dist/{chunk-HHRYIDOW.js → chunk-ACW64W4M.js} +34 -7
  23. package/dist/{chunk-5EA6OTLJ.js → chunk-AFOH6CHO.js} +2 -2
  24. package/dist/{chunk-WHEYHDIE.js → chunk-AKESWZH5.js} +2 -1
  25. package/dist/{chunk-JHUJUIOH.js → chunk-AT2A7RJZ.js} +4 -16
  26. package/dist/{chunk-LKFKO4JW.js → chunk-B6EHK772.js} +2 -2
  27. package/dist/{chunk-U3SS4NRH.js → chunk-BHXAKTBE.js} +1 -1
  28. package/dist/{chunk-4UWCMBPG.js → chunk-BJH44J7G.js} +3 -3
  29. package/dist/{chunk-MPQCHBDJ.js → chunk-CJSHS2K7.js} +5 -1
  30. package/dist/{chunk-RQ4UBKYH.js → chunk-GF7LUWQS.js} +5 -11
  31. package/dist/{chunk-TXAFIERF.js → chunk-L5NWFP5R.js} +2 -2
  32. package/dist/{chunk-24C6VVLM.js → chunk-LPGZ3NUF.js} +23 -23
  33. package/dist/{chunk-UQUYBDMG.js → chunk-M4G5FO7F.js} +1 -1
  34. package/dist/{chunk-P3GRLV47.js → chunk-NEWXDNF2.js} +212 -145
  35. package/dist/{chunk-3LEFXLAW.js → chunk-NOZJIN46.js} +36 -37
  36. package/dist/{chunk-4MDRHMX4.js → chunk-RE73FP2X.js} +4 -27
  37. package/dist/{chunk-5UZBUV2H.js → chunk-RKU66D2O.js} +6 -4
  38. package/dist/{chunk-SWOPJHXI.js → chunk-RYTSPBNM.js} +4 -4
  39. package/dist/{chunk-WTJDSTYD.js → chunk-T5KR6SNO.js} +2 -2
  40. package/dist/chunk-TGSPCFBT.js +1 -0
  41. package/dist/{chunk-ZYACRPLN.js → chunk-ULYAI7CP.js} +6 -3
  42. package/dist/{chunk-HLSHFXTO.js → chunk-WY74MMDB.js} +2 -2
  43. package/dist/{chunk-PGNERAZL.js → chunk-ZWBI7ORM.js} +3 -3
  44. package/dist/{cli-context-7HK67B2Z.js → cli-context-I6HJCVCV.js} +1607 -1495
  45. package/dist/cli-initial-tools.d.ts +54 -0
  46. package/dist/{cli-main-TQGLKB6D.js → cli-main-X7KD7IOC.js} +38223 -37136
  47. package/dist/cli-main-boot.d.ts +133 -0
  48. package/dist/cli-main-orchestration.d.ts +170 -0
  49. package/dist/cli-main-state.d.ts +19 -0
  50. package/dist/config-doctor.d.ts +1 -1
  51. package/dist/{config-transfer-UE37X4B5.js → config-transfer-YPD7HYXH.js} +7 -5
  52. package/dist/{execution-C2IECWF7.js → execution-M7D4KNBB.js} +171 -132
  53. package/dist/fleet/host-director-setup.d.ts +41 -0
  54. package/dist/fleet/host-spawn-types.d.ts +17 -0
  55. package/dist/fleet/host.d.ts +2 -1
  56. package/dist/goal-host-prompts.d.ts +5 -0
  57. package/dist/goal-host-types.d.ts +94 -0
  58. package/dist/goal-host.d.ts +2 -91
  59. package/dist/{hq-KBFDKTDD.js → hq-BBLGEEUH.js} +5 -6
  60. package/dist/hq-server/handle-types.d.ts +1 -1
  61. package/dist/hq-server/routes/auth-handlers.d.ts +1 -1
  62. package/dist/hq-server/routes.d.ts +1 -13
  63. package/dist/hq-server/utils.d.ts +1 -1
  64. package/dist/{hq-server-AVNKZ2PV.js → hq-server-5TR6NBGS.js} +3 -3
  65. package/dist/{hq-service-FGB7BY2D.js → hq-service-4Q64WFNT.js} +3 -3
  66. package/dist/{import-claude-code-AYG24RG2.js → import-claude-code-GHIS2F7Y.js} +3 -3
  67. package/dist/index.js +5 -5
  68. package/dist/live-settings-input.d.ts +2 -0
  69. package/dist/mailbox-bridge-bootstrap.d.ts +1 -4
  70. package/dist/{mailbox-serve-S536N66F.js → mailbox-serve-DRWJVMYU.js} +2 -2
  71. package/dist/{modeldiag-NEPTLFZW.js → modeldiag-F3LUF4VJ.js} +3 -3
  72. package/dist/picker-effort.d.ts +32 -0
  73. package/dist/picker-model-picker.d.ts +13 -2
  74. package/dist/picker-provider-list.d.ts +66 -0
  75. package/dist/picker.d.ts +12 -12
  76. package/dist/plugin-config.d.ts +6 -0
  77. package/dist/{plugin-usage-3TZGROK7.js → plugin-usage-EM752WHG.js} +3 -3
  78. package/dist/{plugins-ITH4SLO6.js → plugins-U6HQFJ7N.js} +4 -4
  79. package/dist/{providers-models-O75MKLN2.js → providers-models-S3S6VXME.js} +249 -239
  80. package/dist/repl-suggestions.d.ts +0 -5
  81. package/dist/{sage-FFKUOCQL.js → sage-BMRDJCDI.js} +9 -5
  82. package/dist/{sage-sync-ZMTHB477.js → sage-sync-PCWCNEUK.js} +2 -2
  83. package/dist/{sdd-runtime-PTOSPYI4.js → sdd-runtime-PF72WHLO.js} +3 -20
  84. package/dist/services/sdd/task-manager.d.ts +1 -12
  85. package/dist/services/sdd-runtime.d.ts +3 -4
  86. package/dist/services/statusline-config.d.ts +0 -2
  87. package/dist/{sessions-config-VE26SB56.js → sessions-config-RQRQDW5Q.js} +2 -2
  88. package/dist/settings-behavior-sections.d.ts +0 -1
  89. package/dist/short-circuit-flags-IEV3FEOT.js +10 -0
  90. package/dist/{skill-suggest-L2EVWB6R.js → skill-suggest-THANK3LV.js} +1 -1
  91. package/dist/slash-commands/command-context.d.ts +2 -0
  92. package/dist/slash-commands/doctor.d.ts +0 -9
  93. package/dist/slash-commands/fallback-chain-view.d.ts +3 -0
  94. package/dist/slash-commands/fallback-ref-validation.d.ts +34 -0
  95. package/dist/slash-commands/goal-refiner.d.ts +10 -2
  96. package/dist/slash-commands/goals.d.ts +6 -0
  97. package/dist/slash-commands/kanban-task-completion.d.ts +17 -0
  98. package/dist/slash-commands/mcp.d.ts +0 -1
  99. package/dist/slash-commands/memory-purge-domain-terms.d.ts +5 -0
  100. package/dist/slash-commands/nextsteps-mode.d.ts +13 -0
  101. package/dist/slash-commands/session-live-status.d.ts +21 -0
  102. package/dist/slash-commands/session.d.ts +1 -1
  103. package/dist/slash-commands/settings-feature-ui-mutations.d.ts +8 -0
  104. package/dist/slash-commands/settings-hq-autonomy-mutations.d.ts +8 -0
  105. package/dist/slash-commands/settings-persist-deps.d.ts +10 -0
  106. package/dist/slash-commands/settings-runtime-mutations.d.ts +12 -0
  107. package/dist/slash-commands/shadow.d.ts +0 -1
  108. package/dist/slash-commands/tool-auto-thin-commands.d.ts +15 -0
  109. package/dist/{standalone-auto-update-VQC4GYCM.js → standalone-auto-update-QUXOZ4B5.js} +13 -2
  110. package/dist/subcommands/handlers/acp-agents.d.ts +9 -0
  111. package/dist/subcommands/handlers/acp-server.d.ts +7 -0
  112. package/dist/subcommands/handlers/hq.d.ts +0 -1
  113. package/dist/subcommands/handlers/providers-models-custom.d.ts +33 -0
  114. package/dist/subcommands/handlers/providers-models-visibility.d.ts +29 -0
  115. package/dist/subcommands/handlers/skill-suggest.d.ts +0 -2
  116. package/dist/{subcommands-CLI6QTLU.js → subcommands-75IB23LS.js} +2 -2
  117. package/dist/{typesafe-NHC322W5.js → typesafe-KW2XHOG3.js} +2 -2
  118. package/dist/{update-J5WHAFPE.js → update-LHCWAWEZ.js} +3 -3
  119. package/dist/utils/win32-cmd.d.ts +1 -1
  120. package/dist/{version-help-ATOKGMTK.js → version-help-JQGRPDJW.js} +2 -2
  121. package/dist/webui-server/credential-watcher.d.ts +3 -1
  122. package/dist/webui-server/host-settings.d.ts +17 -0
  123. package/dist/webui-server/http-bridge.d.ts +32 -0
  124. package/dist/webui-server/instance-registration.d.ts +19 -0
  125. package/dist/webui-server/kanban-services.d.ts +9 -0
  126. package/dist/webui-server/message-router.d.ts +14 -0
  127. package/dist/webui-server/prefs-seeding.d.ts +1 -2
  128. package/dist/webui-server/provider-config.d.ts +0 -1
  129. package/dist/webui-server/provider-plane.d.ts +14 -0
  130. package/dist/webui-server/session-agents.d.ts +18 -0
  131. package/dist/webui-server/session-start-live.d.ts +11 -0
  132. package/dist/webui-server/stream-coalescer.d.ts +1 -1
  133. package/dist/webui-server/transport.d.ts +12 -0
  134. package/dist/{webui-server-GQOVLQS3.js → webui-server-CUIFA25W.js} +1472 -1335
  135. package/dist/webui-session-maintenance.d.ts +16 -0
  136. package/dist/wiring/brain-and-orchestration.d.ts +1 -1
  137. package/dist/wiring/cli-slash-commands-setup.d.ts +1 -0
  138. package/dist/wiring/command-host-state.d.ts +11 -2
  139. package/dist/wiring/dep-watcher-bridge.d.ts +8 -1
  140. package/dist/wiring/management-tools.d.ts +9 -1
  141. package/dist/wiring/session-command-handlers.d.ts +2 -1
  142. package/dist/worktree-command-host.d.ts +31 -0
  143. package/package.json +31 -31
  144. package/dist/chunk-WHOIR577.js +0 -1
  145. package/dist/short-circuit-flags-D6VAG47Y.js +0 -10
@@ -17,7 +17,7 @@ import {
17
17
  renderDiff,
18
18
  resolveLaunchAllowedTools,
19
19
  resolveToolRestriction
20
- } from "./chunk-TI7KLHYX.js";
20
+ } from "./chunk-2CMMWRQH.js";
21
21
  import {
22
22
  theme as theme2
23
23
  } from "./chunk-QD544J2B.js";
@@ -27,33 +27,33 @@ import {
27
27
  } from "./chunk-EWKESL4L.js";
28
28
  import {
29
29
  resolveLaunchMcpServers
30
- } from "./chunk-HLSHFXTO.js";
30
+ } from "./chunk-WY74MMDB.js";
31
31
  import {
32
32
  subcommands
33
- } from "./chunk-24C6VVLM.js";
33
+ } from "./chunk-LPGZ3NUF.js";
34
34
  import {
35
35
  renderDeepHelp,
36
36
  renderFocusedHelp
37
37
  } from "./chunk-2EZ2DHOW.js";
38
38
  import {
39
39
  handleHelpVersionShortCircuit
40
- } from "./chunk-SWOPJHXI.js";
40
+ } from "./chunk-RYTSPBNM.js";
41
41
  import {
42
42
  DEFAULT_PORT
43
- } from "./chunk-RQ4UBKYH.js";
43
+ } from "./chunk-GF7LUWQS.js";
44
44
  import "./chunk-HXJYHAR4.js";
45
45
  import {
46
46
  resolveHqPasswordInput
47
47
  } from "./chunk-AUQDGU5D.js";
48
48
  import {
49
49
  normalizeHqPublicOrigin
50
- } from "./chunk-5EA6OTLJ.js";
50
+ } from "./chunk-AFOH6CHO.js";
51
51
  import {
52
52
  parseHqIpAllowlist
53
53
  } from "./chunk-Q7E3BPDU.js";
54
54
  import {
55
55
  installProviderPersisters
56
- } from "./chunk-TWSQTQKR.js";
56
+ } from "./chunk-4R6TVMYA.js";
57
57
  import {
58
58
  discoverAndMergeProviders
59
59
  } from "./chunk-PA4VBCXS.js";
@@ -69,44 +69,44 @@ import {
69
69
  runPicker,
70
70
  saveToGlobalConfig,
71
71
  theme
72
- } from "./chunk-P3GRLV47.js";
72
+ } from "./chunk-NEWXDNF2.js";
73
73
  import "./chunk-YTEOAWOJ.js";
74
74
  import {
75
75
  isKeylessLocalProvider,
76
76
  visibleModelIds
77
- } from "./chunk-5UZBUV2H.js";
77
+ } from "./chunk-RKU66D2O.js";
78
78
  import {
79
79
  resolveActiveApiKey
80
- } from "./chunk-ZYACRPLN.js";
80
+ } from "./chunk-ULYAI7CP.js";
81
81
  import {
82
82
  activeProfileConfigPath
83
83
  } from "./chunk-YMXXOOFN.js";
84
- import "./chunk-4BX2YDTQ.js";
84
+ import "./chunk-4JMAWKLA.js";
85
85
  import {
86
86
  runUpdateCommand
87
- } from "./chunk-PGNERAZL.js";
88
- import "./chunk-U3SS4NRH.js";
87
+ } from "./chunk-ZWBI7ORM.js";
88
+ import "./chunk-BHXAKTBE.js";
89
89
  import "./chunk-DAKVBMD4.js";
90
90
  import {
91
91
  applyNodeEnvDefault,
92
92
  applySessionShellDefault,
93
93
  runPreflight
94
- } from "./chunk-WTJDSTYD.js";
94
+ } from "./chunk-T5KR6SNO.js";
95
95
  import {
96
96
  parseArgs
97
- } from "./chunk-MPQCHBDJ.js";
97
+ } from "./chunk-CJSHS2K7.js";
98
98
  import "./chunk-DIOODAYM.js";
99
99
  import {
100
100
  applyAsciiMode
101
101
  } from "./chunk-PZQBGUVZ.js";
102
- import "./chunk-WHEYHDIE.js";
102
+ import "./chunk-AKESWZH5.js";
103
103
  import {
104
104
  CLI_VERSION
105
105
  } from "./chunk-F5DCFAKI.js";
106
106
 
107
107
  // src/cli-context.ts
108
108
  import { TOKENS as TOKENS3 } from "@wrongstack/core/kernel";
109
- import { writeErr as writeErr2 } from "@wrongstack/core/utils";
109
+ import { writeErr as writeErr4 } from "@wrongstack/core/utils";
110
110
 
111
111
  // src/boot/container-wiring.ts
112
112
  import { DefaultPathResolver } from "@wrongstack/core/infrastructure";
@@ -234,7 +234,7 @@ function wireContainer(deps) {
234
234
  // src/boot/launch-menu.ts
235
235
  import * as fs from "node:fs/promises";
236
236
  import { HQ_CLI_DEFAULT_HOST } from "@wrongstack/core/hq";
237
- import { atomicWrite, color, isStdinTTY, setRawMode } from "@wrongstack/core/utils";
237
+ import { atomicWrite, color, isStdinTTY, setRawMode, withFileLock } from "@wrongstack/core/utils";
238
238
 
239
239
  // src/boot/launch-banner.ts
240
240
  var STACK_ORANGE = { r: 253, g: 159, b: 2 };
@@ -750,29 +750,31 @@ function toPersistedMenuChoice(result) {
750
750
  };
751
751
  }
752
752
  async function persistMenuChoice(globalConfigPath, choice) {
753
- let fileExists = false;
754
- try {
755
- await fs.access(globalConfigPath);
756
- fileExists = true;
757
- } catch {
758
- }
759
- let existing = {};
760
- try {
761
- const raw = await fs.readFile(globalConfigPath, "utf8");
762
- existing = JSON.parse(raw);
763
- } catch (err) {
764
- if (fileExists) {
765
- throw new Error(
766
- `Refusing to overwrite corrupt config at ${globalConfigPath} (${err.message}). Fix or move the file aside before retrying.`,
767
- { cause: err }
768
- );
753
+ await withFileLock(globalConfigPath, async () => {
754
+ let fileExists = false;
755
+ try {
756
+ await fs.access(globalConfigPath);
757
+ fileExists = true;
758
+ } catch {
769
759
  }
770
- existing = {};
771
- }
772
- const launch = existing.launch ?? {};
773
- existing.launch = { ...launch, menuChoice: choice };
774
- await atomicWrite(globalConfigPath, JSON.stringify(existing, null, 2), {
775
- mode: 384
760
+ let existing = {};
761
+ try {
762
+ const raw = await fs.readFile(globalConfigPath, "utf8");
763
+ existing = JSON.parse(raw.replace(/^\uFEFF/, ""));
764
+ } catch (err) {
765
+ if (fileExists) {
766
+ throw new Error(
767
+ `Refusing to overwrite corrupt config at ${globalConfigPath} (${err.message}). Fix or move the file aside before retrying.`,
768
+ { cause: err }
769
+ );
770
+ }
771
+ existing = {};
772
+ }
773
+ const launch = existing.launch ?? {};
774
+ existing.launch = { ...launch, menuChoice: choice };
775
+ await atomicWrite(globalConfigPath, JSON.stringify(existing, null, 2), {
776
+ mode: 384
777
+ });
776
778
  });
777
779
  }
778
780
 
@@ -929,7 +931,7 @@ async function isPortInUse(host, port) {
929
931
  }
930
932
  async function handleHqShortCircuit(flags) {
931
933
  if (flags["hq"] !== true) return null;
932
- const { startHqServer } = await import("./hq-server-AVNKZ2PV.js");
934
+ const { startHqServer } = await import("./hq-server-5TR6NBGS.js");
933
935
  const tunnelRequested = flags["tunnel"] === true;
934
936
  const rawPublicUrl = typeof flags["hq-public-url"] === "string" ? flags["hq-public-url"] : process.env.WRONGSTACK_HQ_PUBLIC_URL;
935
937
  let publicOrigin;
@@ -1174,167 +1176,18 @@ ${color3.green("Cloudflare Quick Tunnel ready:")} ${browserUrl}
1174
1176
  // src/boot.ts
1175
1177
  import * as os from "node:os";
1176
1178
  import * as path9 from "node:path";
1177
- import { toErrorMessage as toErrorMessage5 } from "@wrongstack/core/utils";
1178
-
1179
- // src/boot-provider-selection.ts
1180
- import * as fs2 from "node:fs/promises";
1181
- import { createRequire as createRequire4 } from "node:module";
1182
- import * as path4 from "node:path";
1183
- import { fileURLToPath } from "node:url";
1184
- import {
1185
- color as color4,
1186
- moduleUrlFor,
1187
- toErrorMessage,
1188
- wrongstackPackageJsonPath as wrongstackPackageJsonPath3
1189
- } from "@wrongstack/core/utils";
1190
- import { isSetupProvider } from "@wrongstack/providers";
1191
- function resolveBundledOverlayFile() {
1192
- try {
1193
- return fileURLToPath(
1194
- new URL("../data/providers.json", moduleUrlFor(import.meta.url, "@wrongstack/cli"))
1195
- );
1196
- } catch {
1197
- return void 0;
1198
- }
1199
- }
1200
- async function validateSavedProviderModel(config, modelsRegistry) {
1201
- const providerId = config.provider;
1202
- const modelId = config.model;
1203
- if (!providerId || !modelId) return { ok: false, reason: "missing provider/model" };
1204
- if (isSetupProvider(providerId)) return { ok: true };
1205
- const saved = config.providers?.[providerId];
1206
- const lookupId = saved?.type && saved.type !== providerId ? saved.type : providerId;
1207
- const catalogProvider = await modelsRegistry.getProvider(lookupId).catch(() => void 0);
1208
- if (!catalogProvider && !saved?.family)
1209
- return { ok: false, reason: `provider "${providerId}" is no longer available` };
1210
- const hasCredential = (catalogProvider?.envVars ?? saved?.envVars ?? []).some(
1211
- (envVar) => Boolean(process.env[envVar])
1212
- ) || saved !== void 0 && resolveActiveApiKey(saved) !== void 0 || isKeylessLocalProvider({
1213
- apiBase: saved?.baseUrl ?? catalogProvider?.apiBase,
1214
- envVars: saved?.envVars ?? catalogProvider?.envVars
1215
- });
1216
- if (!hasCredential) return { ok: false, reason: `provider "${providerId}" has no usable key` };
1217
- const visible = visibleModelIds(
1218
- providerId,
1219
- config,
1220
- (catalogProvider?.models ?? []).map((m) => m.id),
1221
- saved
1222
- );
1223
- if (visible.length > 0 && !visible.includes(modelId)) {
1224
- return {
1225
- ok: false,
1226
- reason: `model "${modelId}" is no longer available for provider "${providerId}"`
1227
- };
1228
- }
1229
- return { ok: true };
1230
- }
1231
- async function autoSelectSavedProvider(config, modelsRegistry) {
1232
- const providers = config.providers ?? {};
1233
- const entries = config.provider && providers[config.provider] ? [[config.provider, providers[config.provider]]] : Object.entries(providers);
1234
- for (const [id, cfg] of entries) {
1235
- if (!cfg) continue;
1236
- let model = cfg.models?.[0];
1237
- if (!model) {
1238
- const catalogId = cfg.type && cfg.type !== id ? cfg.type : id;
1239
- const catalog = await modelsRegistry.getProvider(catalogId).catch(() => void 0);
1240
- model = catalog?.models?.[0]?.id;
1241
- }
1242
- if (model) return { provider: id, model };
1243
- }
1244
- return void 0;
1245
- }
1246
- function resolveBundledSkillsDir2() {
1247
- try {
1248
- const req = createRequire4(import.meta.url);
1249
- const corePkg = wrongstackPackageJsonPath3("@wrongstack/core", (id) => req.resolve(id));
1250
- return path4.join(path4.dirname(corePkg), "skills");
1251
- } catch {
1252
- return void 0;
1253
- }
1254
- }
1255
- function resolveBundledPromptsDir2() {
1256
- try {
1257
- const req = createRequire4(import.meta.url);
1258
- const corePkg = wrongstackPackageJsonPath3("@wrongstack/core", (id) => req.resolve(id));
1259
- return path4.join(path4.dirname(corePkg), "data", "prompts");
1260
- } catch {
1261
- return void 0;
1262
- }
1263
- }
1264
- function isHomeDirectory(cwd, userHome) {
1265
- if (!cwd || !userHome) return false;
1266
- return path4.resolve(cwd) === path4.resolve(userHome);
1267
- }
1268
- function shouldPrintYoloNotice(lastChoices, yoloPinned, yolo) {
1269
- return !lastChoices && yoloPinned === void 0 && yolo;
1270
- }
1271
- async function checkGitInCwd(opts) {
1272
- const { cwd, renderer, reader } = opts;
1273
- const cwdGit = path4.join(cwd, ".git");
1274
- let hasCwdGit = false;
1275
- try {
1276
- await fs2.access(cwdGit);
1277
- hasCwdGit = true;
1278
- } catch {
1279
- }
1280
- if (!hasCwdGit) {
1281
- renderer.write(
1282
- `
1283
- ${color4.amber("\u25CB")} This folder has no ${color4.bold(".git")} repository.
1284
- `
1285
- );
1286
- const answer = (await reader.readLine(` ${color4.amber("?")} Initialize one here? ${color4.dim("[y/N]")} `)).trim().toLowerCase();
1287
- if (answer === "y" || answer === "yes") {
1288
- try {
1289
- const { spawn: spawn2 } = await import("node:child_process");
1290
- await new Promise((resolve3, reject) => {
1291
- const child = spawn2("git", ["init"], {
1292
- cwd,
1293
- signal: AbortSignal.timeout(1e4),
1294
- windowsHide: true
1295
- });
1296
- child.on("error", reject);
1297
- child.on(
1298
- "close",
1299
- (code) => code === 0 ? resolve3() : reject(new Error(`git init failed with ${code}`))
1300
- );
1301
- });
1302
- renderer.write(` ${color4.green("\u2713")} Git repository initialized
1303
- `);
1304
- hasCwdGit = true;
1305
- } catch (err) {
1306
- renderer.writeError(`git init failed: ${toErrorMessage(err)}
1307
- `);
1308
- }
1309
- }
1310
- }
1311
- const parentDir = path4.dirname(cwd);
1312
- if (parentDir !== cwd) {
1313
- try {
1314
- await fs2.access(path4.join(parentDir, ".git"));
1315
- renderer.write(
1316
- ` ${color4.dim("\u2139")} A ${color4.bold(".git")} repo exists in the parent directory: ${color4.dim(parentDir)}
1317
- `
1318
- );
1319
- } catch {
1320
- }
1321
- }
1322
- }
1323
-
1324
- // src/boot.ts
1325
1179
  import { DefaultLogger } from "@wrongstack/core/infrastructure";
1326
- import { TOKENS as TOKENS2 } from "@wrongstack/core/kernel";
1327
1180
  import { DefaultModelsRegistry, startCatalog } from "@wrongstack/core/models";
1328
- import { ToolRegistry } from "@wrongstack/core/registry";
1329
- import { normalizeTokenSavingTier } from "@wrongstack/core/types";
1330
- import { color as color11, isStdinTTY as isStdinTTY2, writeErr } from "@wrongstack/core/utils";
1331
- import { isSetupProvider as isSetupProvider2, SETUP_MODEL_ID, SETUP_PROVIDER_ID } from "@wrongstack/providers";
1332
- import { createDefaultContainer as createDefaultContainer2 } from "@wrongstack/runtime";
1333
- import { registerBuiltinToolTier } from "@wrongstack/tools/tool-tier";
1181
+ import {
1182
+ color as color13,
1183
+ isStdinTTY as isStdinTTY3,
1184
+ toErrorMessage as toErrorMessage7,
1185
+ writeErr as writeErr3
1186
+ } from "@wrongstack/core/utils";
1334
1187
 
1335
1188
  // src/boot/append-system-prompt.ts
1336
- import * as fs3 from "node:fs/promises";
1337
- import * as path5 from "node:path";
1189
+ import * as fs2 from "node:fs/promises";
1190
+ import * as path4 from "node:path";
1338
1191
  async function resolveAppendedSystemPrompt(flags, cwd) {
1339
1192
  const parts = [];
1340
1193
  const inline = flags["append-system-prompt"];
@@ -1344,10 +1197,10 @@ async function resolveAppendedSystemPrompt(flags, cwd) {
1344
1197
  throw new Error("--append-system-prompt-file needs a path");
1345
1198
  }
1346
1199
  if (typeof file === "string" && file.trim()) {
1347
- const resolved = path5.resolve(cwd, file.trim());
1200
+ const resolved = path4.resolve(cwd, file.trim());
1348
1201
  let text;
1349
1202
  try {
1350
- text = await fs3.readFile(resolved, "utf8");
1203
+ text = await fs2.readFile(resolved, "utf8");
1351
1204
  } catch (err) {
1352
1205
  const reason = err.code ?? String(err);
1353
1206
  throw new Error(`--append-system-prompt-file: cannot read ${resolved} (${reason})`);
@@ -1358,9 +1211,9 @@ async function resolveAppendedSystemPrompt(flags, cwd) {
1358
1211
  }
1359
1212
 
1360
1213
  // src/boot/config-backup-recovery.ts
1361
- import * as fs4 from "node:fs/promises";
1362
- import * as path6 from "node:path";
1363
- import { atomicWrite as atomicWrite2, backupConfigFile, color as color5, toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
1214
+ import * as fs3 from "node:fs/promises";
1215
+ import * as path5 from "node:path";
1216
+ import { atomicWrite as atomicWrite2, backupConfigFile, color as color4, toErrorMessage } from "@wrongstack/core/utils";
1364
1217
  function isRecord(value) {
1365
1218
  return value !== null && typeof value === "object" && !Array.isArray(value);
1366
1219
  }
@@ -1371,7 +1224,7 @@ function providerIds(value) {
1371
1224
  }
1372
1225
  async function defaultProfileProvidersAreEmpty(profilePath) {
1373
1226
  try {
1374
- const parsed = JSON.parse(await fs4.readFile(profilePath, "utf8"));
1227
+ const parsed = JSON.parse(await fs3.readFile(profilePath, "utf8"));
1375
1228
  return providerIds(parsed).length === 0;
1376
1229
  } catch {
1377
1230
  return false;
@@ -1394,7 +1247,7 @@ function profileSnapshotTimestamp(fileName, fallback) {
1394
1247
  );
1395
1248
  }
1396
1249
  async function findLatestProviderBackup(globalRoot, profileName = "default") {
1397
- const directory = path6.join(globalRoot, "config-history");
1250
+ const directory = path5.join(globalRoot, "config-history");
1398
1251
  const escapedProfile = profileName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1399
1252
  const filePattern = new RegExp(
1400
1253
  `^profiles-${escapedProfile}-config-\\d{4}-\\d{2}-\\d{2}T\\d{2}-\\d{2}-\\d{2}-\\d{3}\\.json$`,
@@ -1403,7 +1256,7 @@ async function findLatestProviderBackup(globalRoot, profileName = "default") {
1403
1256
  const candidates = [];
1404
1257
  let entries;
1405
1258
  try {
1406
- entries = await fs4.readdir(directory, { withFileTypes: true });
1259
+ entries = await fs3.readdir(directory, { withFileTypes: true });
1407
1260
  } catch {
1408
1261
  return void 0;
1409
1262
  }
@@ -1411,9 +1264,9 @@ async function findLatestProviderBackup(globalRoot, profileName = "default") {
1411
1264
  if (!entry.isFile() || !filePattern.test(entry.name)) {
1412
1265
  continue;
1413
1266
  }
1414
- const filePath = path6.join(directory, entry.name);
1267
+ const filePath = path5.join(directory, entry.name);
1415
1268
  try {
1416
- const stat2 = await fs4.stat(filePath);
1269
+ const stat2 = await fs3.stat(filePath);
1417
1270
  candidates.push({
1418
1271
  filePath,
1419
1272
  fileName: entry.name,
@@ -1425,7 +1278,7 @@ async function findLatestProviderBackup(globalRoot, profileName = "default") {
1425
1278
  candidates.sort((a, b) => b.modifiedAtMs - a.modifiedAtMs);
1426
1279
  for (const candidate of candidates) {
1427
1280
  try {
1428
- const raw = await fs4.readFile(candidate.filePath, "utf8");
1281
+ const raw = await fs3.readFile(candidate.filePath, "utf8");
1429
1282
  const parsed = JSON.parse(raw);
1430
1283
  const ids = providerIds(parsed);
1431
1284
  if (ids.length === 0) continue;
@@ -1437,7 +1290,7 @@ async function findLatestProviderBackup(globalRoot, profileName = "default") {
1437
1290
  }
1438
1291
  async function restoreDefaultProfileBackup(opts) {
1439
1292
  await backupConfigFile(opts.profilePath, { globalRoot: opts.globalRoot });
1440
- await fs4.mkdir(path6.dirname(opts.profilePath), { recursive: true });
1293
+ await fs3.mkdir(path5.dirname(opts.profilePath), { recursive: true });
1441
1294
  await atomicWrite2(opts.profilePath, opts.backup.raw, { mode: 384 });
1442
1295
  }
1443
1296
  async function maybeRestoreDefaultProfileFromBackup(opts) {
@@ -1450,12 +1303,12 @@ async function maybeRestoreDefaultProfileFromBackup(opts) {
1450
1303
  const providerSummary = remainder > 0 ? `${shownProviders} +${remainder}` : shownProviders;
1451
1304
  opts.renderer.write(
1452
1305
  `
1453
- ${color5.amber("\u25CB")} ${color5.bold(profileName)} profile has no saved providers.
1454
- ${color5.dim("Latest usable backup:")} ${color5.bold(backup.fileName)} ${color5.dim(`(${backup.providerIds.length} provider: ${providerSummary})`)}
1306
+ ${color4.amber("\u25CB")} ${color4.bold(profileName)} profile has no saved providers.
1307
+ ${color4.dim("Latest usable backup:")} ${color4.bold(backup.fileName)} ${color4.dim(`(${backup.providerIds.length} provider: ${providerSummary})`)}
1455
1308
  `
1456
1309
  );
1457
1310
  const answer = (await opts.reader.readLine(
1458
- ` ${color5.amber("?")} Restore all settings from this backup? ${color5.dim("[Y/n]")} `
1311
+ ` ${color4.amber("?")} Restore all settings from this backup? ${color4.dim("[Y/n]")} `
1459
1312
  )).trim().toLowerCase();
1460
1313
  if (answer === "n" || answer === "no") return false;
1461
1314
  try {
@@ -1465,12 +1318,12 @@ async function maybeRestoreDefaultProfileFromBackup(opts) {
1465
1318
  profilePath: opts.profilePath
1466
1319
  });
1467
1320
  opts.renderer.write(
1468
- ` ${color5.green("\u2713")} Restored all settings from ${color5.dim(backup.fileName)}
1321
+ ` ${color4.green("\u2713")} Restored all settings from ${color4.dim(backup.fileName)}
1469
1322
  `
1470
1323
  );
1471
1324
  return true;
1472
1325
  } catch (err) {
1473
- opts.renderer.writeWarning(`Could not restore config backup: ${toErrorMessage2(err)}
1326
+ opts.renderer.writeWarning(`Could not restore config backup: ${toErrorMessage(err)}
1474
1327
  `);
1475
1328
  return false;
1476
1329
  }
@@ -1484,6 +1337,26 @@ function applyGoalTuiDefault(flags, positional) {
1484
1337
  }
1485
1338
  }
1486
1339
 
1340
+ // src/boot-config.ts
1341
+ import { bootConfig as coreBootConfig } from "@wrongstack/core/infrastructure";
1342
+ async function bootConfig(flags) {
1343
+ const isWebui = !!flags["webui"];
1344
+ const opts = {
1345
+ flags,
1346
+ appLabel: "wstack",
1347
+ ...isWebui ? { skipIdentityValidation: true } : {}
1348
+ };
1349
+ const { cwd, projectRoot, userHome, wpaths, pathResolver, config, vault } = await coreBootConfig(opts);
1350
+ return {
1351
+ paths: { cwd, projectRoot, userHome, wpaths, pathResolver },
1352
+ config,
1353
+ vault
1354
+ };
1355
+ }
1356
+
1357
+ // src/boot-launch-choices.ts
1358
+ import { color as color10, toErrorMessage as toErrorMessage5, writeErr } from "@wrongstack/core/utils";
1359
+
1487
1360
  // src/boot/system-prompt-menu.ts
1488
1361
  import {
1489
1362
  countSystemPromptTokens,
@@ -1492,11 +1365,11 @@ import {
1492
1365
  SYSTEM_PROMPT_VARIANT_OPTIONS,
1493
1366
  SYSTEM_PROMPT_VARIANTS
1494
1367
  } from "@wrongstack/core/agent";
1495
- import { color as color9 } from "@wrongstack/core/utils";
1368
+ import { color as color8 } from "@wrongstack/core/utils";
1496
1369
 
1497
1370
  // src/pre-launch/first-run.ts
1498
1371
  import { hasProviderCredential } from "@wrongstack/core/models";
1499
- import { color as color6, toErrorMessage as toErrorMessage3 } from "@wrongstack/core/utils";
1372
+ import { color as color5, toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
1500
1373
  import { PROVIDER_DEFINITIONS } from "@wrongstack/providers/definitions";
1501
1374
  async function hasAnyCredential(config, modelsRegistry, env = process.env) {
1502
1375
  for (const cfg of Object.values(config.providers ?? {})) {
@@ -1522,39 +1395,39 @@ function renderWelcome(renderer) {
1522
1395
  renderer.write(`${boxTop("Welcome to WrongStack")}
1523
1396
  `);
1524
1397
  renderer.write(
1525
- `${boxRow(color6.dim("No model is connected yet. Pick how you want to connect one:"))}
1398
+ `${boxRow(color5.dim("No model is connected yet. Pick how you want to connect one:"))}
1526
1399
  `
1527
1400
  );
1528
1401
  renderer.write(`${boxDivider()}
1529
1402
  `);
1530
1403
  renderer.write(
1531
1404
  `${boxRow(
1532
- `${theme.accent("1")} ${color6.bold("Sign in with a subscription")} ${color6.dim("ChatGPT \xB7 Claude \xB7 GitHub Copilot")}`
1405
+ `${theme.accent("1")} ${color5.bold("Sign in with a subscription")} ${color5.dim("ChatGPT \xB7 Claude \xB7 GitHub Copilot")}`
1533
1406
  )}
1534
1407
  `
1535
1408
  );
1536
1409
  renderer.write(
1537
1410
  `${boxRow(
1538
- `${theme.accent("2")} ${color6.bold("Add an API key")} ${color6.dim("Anthropic \xB7 OpenAI \xB7 Google \xB7 OpenRouter \xB7 \u2026")}`
1411
+ `${theme.accent("2")} ${color5.bold("Add an API key")} ${color5.dim("Anthropic \xB7 OpenAI \xB7 Google \xB7 OpenRouter \xB7 \u2026")}`
1539
1412
  )}
1540
1413
  `
1541
1414
  );
1542
1415
  renderer.write(
1543
1416
  `${boxRow(
1544
- `${theme.accent("3")} ${color6.bold("Use a local server")} ${color6.dim("Ollama \xB7 LM Studio \xB7 vLLM \xB7 OmniRoute")}`
1417
+ `${theme.accent("3")} ${color5.bold("Use a local server")} ${color5.dim("Ollama \xB7 LM Studio \xB7 vLLM \xB7 OmniRoute")}`
1545
1418
  )}
1546
1419
  `
1547
1420
  );
1548
1421
  renderer.write(
1549
1422
  `${boxRow(
1550
- `${theme.accent("4")} ${color6.bold("Continue without a key")} ${color6.dim("setup mode \u2014 explore the app, no model")}`
1423
+ `${theme.accent("4")} ${color5.bold("Continue without a key")} ${color5.dim("setup mode \u2014 explore the app, no model")}`
1551
1424
  )}
1552
1425
  `
1553
1426
  );
1554
1427
  renderer.write(`${boxDivider()}
1555
1428
  `);
1556
1429
  renderer.write(
1557
- `${boxRow(color6.dim("You can change this any time with /auth inside the app, or `wstack auth`."))}
1430
+ `${boxRow(color5.dim("You can change this any time with /auth inside the app, or `wstack auth`."))}
1558
1431
  `
1559
1432
  );
1560
1433
  renderer.write(`${boxBottom()}
@@ -1573,12 +1446,12 @@ async function runFirstRunSetup(deps) {
1573
1446
  renderWelcome(renderer);
1574
1447
  const answer = (await reader.readLine(
1575
1448
  `
1576
- ${color6.amber("?")} Choose ${color6.dim("(1-4)")} ${color6.dim("[q to quit]")}: `
1449
+ ${color5.amber("?")} Choose ${color5.dim("(1-4)")} ${color5.dim("[q to quit]")}: `
1577
1450
  )).trim().toLowerCase();
1578
1451
  if (!answer || answer === "q" || answer === "quit" || answer === "exit") {
1579
1452
  renderer.write(
1580
1453
  `
1581
- ${color6.dim("No provider configured. Run")} ${color6.bold("wstack auth")} ${color6.dim("when you are ready.")}
1454
+ ${color5.dim("No provider configured. Run")} ${color5.bold("wstack auth")} ${color5.dim("when you are ready.")}
1582
1455
 
1583
1456
  `
1584
1457
  );
@@ -1595,14 +1468,14 @@ ${color6.amber("?")} Choose ${color6.dim("(1-4)")} ${color6.dim("[q to quit]")}:
1595
1468
  try {
1596
1469
  await flow();
1597
1470
  } catch (err) {
1598
- renderer.writeWarning(`Setup step did not complete: ${toErrorMessage3(err)}
1471
+ renderer.writeWarning(`Setup step did not complete: ${toErrorMessage2(err)}
1599
1472
  `);
1600
1473
  }
1601
1474
  let updated;
1602
1475
  try {
1603
1476
  updated = await deps.reloadConfig();
1604
1477
  } catch (err) {
1605
- renderer.writeWarning(`Could not re-read config: ${toErrorMessage3(err)}
1478
+ renderer.writeWarning(`Could not re-read config: ${toErrorMessage2(err)}
1606
1479
  `);
1607
1480
  continue;
1608
1481
  }
@@ -1614,8 +1487,8 @@ ${color6.amber("?")} Choose ${color6.dim("(1-4)")} ${color6.dim("[q to quit]")}:
1614
1487
  }
1615
1488
 
1616
1489
  // src/pre-launch/launch-prompts.ts
1617
- import * as fs5 from "node:fs/promises";
1618
- import { atomicWrite as atomicWrite3, color as color7 } from "@wrongstack/core/utils";
1490
+ import * as fs4 from "node:fs/promises";
1491
+ import { atomicWrite as atomicWrite3, color as color6, withFileLock as withFileLock2 } from "@wrongstack/core/utils";
1619
1492
  var LaunchAbortedError = class extends Error {
1620
1493
  exitCode = 0;
1621
1494
  constructor() {
@@ -1646,19 +1519,19 @@ async function runLaunchPrompts(opts) {
1646
1519
  yolo: yoloPinned ?? lastChoices.yolo,
1647
1520
  autonomy: autonomyPinned ?? lastChoices.autonomy
1648
1521
  };
1649
- const onOff = (v) => v ? color7.green("on") : color7.dim("off");
1522
+ const onOff = (v) => v ? color6.green("on") : color6.dim("off");
1650
1523
  const modeLabel = effective.mode.toUpperCase();
1651
1524
  renderer.write(
1652
1525
  `
1653
- ${color7.dim("Last settings:")} ${color7.bold(modeLabel)} \xB7 YOLO ${onOff(effective.yolo)} \xB7 Autonomy ${effective.autonomy === "auto" ? color7.green("auto") : color7.dim("off")}
1526
+ ${color6.dim("Last settings:")} ${color6.bold(modeLabel)} \xB7 YOLO ${onOff(effective.yolo)} \xB7 Autonomy ${effective.autonomy === "auto" ? color6.green("auto") : color6.dim("off")}
1654
1527
  `
1655
1528
  );
1656
1529
  const answer = (await reader.readLine(
1657
- ` ${color7.amber("?")} Continue with these? ${color7.dim("[Y/n/q]")} ${color7.dim("(auto Y in 5s)")} `,
1530
+ ` ${color6.amber("?")} Continue with these? ${color6.dim("[Y/n/q]")} ${color6.dim("(auto Y in 5s)")} `,
1658
1531
  { timeoutMs: 5e3, defaultAnswer: "y" }
1659
1532
  )).trim().toLowerCase();
1660
1533
  if (answer === "q") {
1661
- renderer.write(color7.dim(" Goodbye!\n"));
1534
+ renderer.write(color6.dim(" Goodbye!\n"));
1662
1535
  throw new LaunchAbortedError();
1663
1536
  }
1664
1537
  if (answer !== "n" && answer !== "no") {
@@ -1666,7 +1539,7 @@ async function runLaunchPrompts(opts) {
1666
1539
  const badgeStr2 = badges2.length > 0 ? ` (${badges2.join(" \xB7 ")})` : "";
1667
1540
  renderer.write(
1668
1541
  `
1669
- ${color7.green("\u25B6")} Launching in ${color7.bold(modeLabel)} mode${badgeStr2}
1542
+ ${color6.green("\u25B6")} Launching in ${color6.bold(modeLabel)} mode${badgeStr2}
1670
1543
 
1671
1544
  `
1672
1545
  );
@@ -1679,10 +1552,10 @@ async function runLaunchPrompts(opts) {
1679
1552
  } else {
1680
1553
  const answer = (await reader.readLine(
1681
1554
  `
1682
- ${color7.amber("?")} Interactive mode: ${color7.bold("T")}UI / ${color7.bold("R")}EPL ${color7.dim("[T/r/q]")} `
1555
+ ${color6.amber("?")} Interactive mode: ${color6.bold("T")}UI / ${color6.bold("R")}EPL ${color6.dim("[T/r/q]")} `
1683
1556
  )).trim().toLowerCase();
1684
1557
  if (answer === "q") {
1685
- renderer.write(color7.dim(" Goodbye!\n"));
1558
+ renderer.write(color6.dim(" Goodbye!\n"));
1686
1559
  throw new LaunchAbortedError();
1687
1560
  }
1688
1561
  mode = answer === "r" || answer === "repl" ? "repl" : "tui";
@@ -1692,10 +1565,10 @@ async function runLaunchPrompts(opts) {
1692
1565
  yolo = yoloPinned;
1693
1566
  } else {
1694
1567
  const answer = (await reader.readLine(
1695
- ` ${color7.amber("?")} YOLO mode ${color7.dim("(auto-approve tool calls)")} ${color7.dim("[Y/n/q]")} `
1568
+ ` ${color6.amber("?")} YOLO mode ${color6.dim("(auto-approve tool calls)")} ${color6.dim("[Y/n/q]")} `
1696
1569
  )).trim().toLowerCase();
1697
1570
  if (answer === "q") {
1698
- renderer.write(color7.dim(" Goodbye!\n"));
1571
+ renderer.write(color6.dim(" Goodbye!\n"));
1699
1572
  throw new LaunchAbortedError();
1700
1573
  }
1701
1574
  yolo = answer !== "n" && answer !== "no";
@@ -1705,10 +1578,10 @@ async function runLaunchPrompts(opts) {
1705
1578
  autonomy = autonomyPinned;
1706
1579
  } else {
1707
1580
  const answer = (await reader.readLine(
1708
- ` ${color7.amber("?")} Autonomy mode ${color7.dim("(auto-continue \u2014 agent picks next step)")} ${color7.dim("[Y/n/q]")} `
1581
+ ` ${color6.amber("?")} Autonomy mode ${color6.dim("(auto-continue \u2014 agent picks next step)")} ${color6.dim("[Y/n/q]")} `
1709
1582
  )).trim().toLowerCase();
1710
1583
  if (answer === "q") {
1711
- renderer.write(color7.dim(" Goodbye!\n"));
1584
+ renderer.write(color6.dim(" Goodbye!\n"));
1712
1585
  throw new LaunchAbortedError();
1713
1586
  }
1714
1587
  autonomy = answer !== "n" && answer !== "no" ? "auto" : "off";
@@ -1717,7 +1590,7 @@ async function runLaunchPrompts(opts) {
1717
1590
  const badgeStr = badges.length > 0 ? ` (${badges.join(" \xB7 ")})` : "";
1718
1591
  renderer.write(
1719
1592
  `
1720
- ${color7.green("\u25B6")} Launching in ${color7.bold(mode.toUpperCase())} mode${badgeStr}
1593
+ ${color6.green("\u25B6")} Launching in ${color6.bold(mode.toUpperCase())} mode${badgeStr}
1721
1594
 
1722
1595
  `
1723
1596
  );
@@ -1725,55 +1598,57 @@ async function runLaunchPrompts(opts) {
1725
1598
  }
1726
1599
  function buildBadges(chosen) {
1727
1600
  const badges = [];
1728
- if (chosen.yolo) badges.push(color7.yellow("YOLO"));
1601
+ if (chosen.yolo) badges.push(color6.yellow("YOLO"));
1729
1602
  if (chosen.autonomy !== "off")
1730
- badges.push(color7.magenta(`AUTONOMY:${chosen.autonomy.toUpperCase()}`));
1603
+ badges.push(color6.magenta(`AUTONOMY:${chosen.autonomy.toUpperCase()}`));
1731
1604
  return badges;
1732
1605
  }
1733
1606
  async function persistLaunchChoices(configPath, choices) {
1734
- let fileExists = false;
1735
- try {
1736
- await fs5.access(configPath);
1737
- fileExists = true;
1738
- } catch {
1739
- }
1740
- let existing = {};
1741
- try {
1742
- const raw = await fs5.readFile(configPath, "utf8");
1743
- existing = JSON.parse(raw);
1744
- } catch (err) {
1745
- if (fileExists) {
1746
- throw new Error(
1747
- `Refusing to overwrite corrupt config at ${configPath} (${err.message}). Fix or move the file aside before retrying.`,
1748
- { cause: err }
1749
- );
1607
+ await withFileLock2(configPath, async () => {
1608
+ let fileExists = false;
1609
+ try {
1610
+ await fs4.access(configPath);
1611
+ fileExists = true;
1612
+ } catch {
1750
1613
  }
1751
- existing = {};
1752
- }
1753
- existing.yolo = choices.yolo;
1754
- existing.launch = {
1755
- mode: choices.mode,
1756
- autonomy: choices.autonomy
1757
- };
1758
- await atomicWrite3(configPath, JSON.stringify(existing, null, 2), { mode: 384 });
1614
+ let existing = {};
1615
+ try {
1616
+ const raw = await fs4.readFile(configPath, "utf8");
1617
+ existing = JSON.parse(raw.replace(/^\uFEFF/, ""));
1618
+ } catch (err) {
1619
+ if (fileExists) {
1620
+ throw new Error(
1621
+ `Refusing to overwrite corrupt config at ${configPath} (${err.message}). Fix or move the file aside before retrying.`,
1622
+ { cause: err }
1623
+ );
1624
+ }
1625
+ existing = {};
1626
+ }
1627
+ existing.yolo = choices.yolo;
1628
+ existing.launch = {
1629
+ mode: choices.mode,
1630
+ autonomy: choices.autonomy
1631
+ };
1632
+ await atomicWrite3(configPath, JSON.stringify(existing, null, 2), { mode: 384 });
1633
+ });
1759
1634
  }
1760
1635
 
1761
1636
  // src/pre-launch/project-check.ts
1762
- import * as fs6 from "node:fs/promises";
1763
- import * as path7 from "node:path";
1637
+ import * as fs5 from "node:fs/promises";
1638
+ import * as path6 from "node:path";
1764
1639
  import {
1765
- color as color8,
1640
+ color as color7,
1766
1641
  detectProjectKind,
1767
1642
  isOutsideProject,
1768
- toErrorMessage as toErrorMessage4
1643
+ toErrorMessage as toErrorMessage3
1769
1644
  } from "@wrongstack/core/utils";
1770
1645
  async function scaffoldAgentsMd(projectRoot) {
1771
- const dir = path7.join(projectRoot, ".wrongstack");
1772
- const file = path7.join(dir, "AGENTS.md");
1646
+ const dir = path6.join(projectRoot, ".wrongstack");
1647
+ const file = path6.join(dir, "AGENTS.md");
1773
1648
  const facts = await detectProjectFacts(projectRoot);
1774
1649
  const body = renderAgentsTemplate(facts);
1775
- await fs6.mkdir(dir, { recursive: true });
1776
- await fs6.writeFile(file, body, "utf8");
1650
+ await fs5.mkdir(dir, { recursive: true });
1651
+ await fs5.writeFile(file, body, "utf8");
1777
1652
  return file;
1778
1653
  }
1779
1654
  async function runProjectCheck(opts) {
@@ -1782,7 +1657,7 @@ async function runProjectCheck(opts) {
1782
1657
  if (kind === "initialized") {
1783
1658
  renderer.write(
1784
1659
  `
1785
- ${color8.green("\u2713")} Project initialized ${color8.dim(`(${path7.join(projectRoot, ".wrongstack", "AGENTS.md")})`)}
1660
+ ${color7.green("\u2713")} Project initialized ${color7.dim(`(${path6.join(projectRoot, ".wrongstack", "AGENTS.md")})`)}
1786
1661
  `
1787
1662
  );
1788
1663
  return true;
@@ -1790,45 +1665,45 @@ async function runProjectCheck(opts) {
1790
1665
  if (kind === "project") {
1791
1666
  renderer.write(
1792
1667
  `
1793
- ${color8.amber("\u25CF")} Project detected ${color8.dim(`(${projectRoot})`)} but ${color8.bold(".wrongstack/AGENTS.md")} is missing.
1668
+ ${color7.amber("\u25CF")} Project detected ${color7.dim(`(${projectRoot})`)} but ${color7.bold(".wrongstack/AGENTS.md")} is missing.
1794
1669
  `
1795
1670
  );
1796
1671
  const answer2 = (await reader.readLine(
1797
- ` ${color8.amber("?")} Scaffold ${color8.bold("AGENTS.md")} now? ${color8.dim("[y/N/q]")} `
1672
+ ` ${color7.amber("?")} Scaffold ${color7.bold("AGENTS.md")} now? ${color7.dim("[y/N/q]")} `
1798
1673
  )).trim().toLowerCase();
1799
1674
  if (answer2 === "q") {
1800
- renderer.write(color8.dim(" Cancelled.\n"));
1675
+ renderer.write(color7.dim(" Cancelled.\n"));
1801
1676
  return false;
1802
1677
  }
1803
1678
  if (answer2 === "y" || answer2 === "yes") {
1804
1679
  try {
1805
1680
  const file = await scaffoldAgentsMd(projectRoot);
1806
- renderer.write(` ${color8.green("\u2713")} Wrote ${color8.dim(file)}
1681
+ renderer.write(` ${color7.green("\u2713")} Wrote ${color7.dim(file)}
1807
1682
  `);
1808
1683
  } catch (err) {
1809
- renderer.writeError(`Failed to scaffold AGENTS.md: ${toErrorMessage4(err)}`);
1684
+ renderer.writeError(`Failed to scaffold AGENTS.md: ${toErrorMessage3(err)}`);
1810
1685
  }
1811
1686
  }
1812
1687
  return true;
1813
1688
  }
1814
- const gitDir = path7.join(projectRoot, ".git");
1689
+ const gitDir = path6.join(projectRoot, ".git");
1815
1690
  let hasGit = false;
1816
1691
  try {
1817
- await fs6.access(gitDir);
1692
+ await fs5.access(gitDir);
1818
1693
  hasGit = true;
1819
1694
  } catch {
1820
1695
  }
1821
1696
  if (!hasGit) {
1822
1697
  renderer.write(
1823
1698
  `
1824
- ${color8.dim("\u25CB")} ${color8.dim(`No project manifest in ${projectRoot} \u2014 running in a scratch directory.`)}
1699
+ ${color7.dim("\u25CB")} ${color7.dim(`No project manifest in ${projectRoot} \u2014 running in a scratch directory.`)}
1825
1700
  `
1826
1701
  );
1827
1702
  const answer2 = (await reader.readLine(
1828
- ` ${color8.amber("?")} No git repo found. ${color8.bold("Initialize git?")} ${color8.dim("[y/N/q]")} `
1703
+ ` ${color7.amber("?")} No git repo found. ${color7.bold("Initialize git?")} ${color7.dim("[y/N/q]")} `
1829
1704
  )).trim().toLowerCase();
1830
1705
  if (answer2 === "q") {
1831
- renderer.write(color8.dim(" Cancelled.\n"));
1706
+ renderer.write(color7.dim(" Cancelled.\n"));
1832
1707
  return false;
1833
1708
  }
1834
1709
  if (answer2 === "y" || answer2 === "yes") {
@@ -1846,23 +1721,23 @@ async function runProjectCheck(opts) {
1846
1721
  (code) => code === 0 ? resolve3() : reject(new Error(`git init failed with ${code}`))
1847
1722
  );
1848
1723
  });
1849
- renderer.write(` ${color8.green("\u2713")} Git repository initialized
1724
+ renderer.write(` ${color7.green("\u2713")} Git repository initialized
1850
1725
  `);
1851
1726
  } catch (err) {
1852
- renderer.writeError(`git init failed: ${toErrorMessage4(err)}
1727
+ renderer.writeError(`git init failed: ${toErrorMessage3(err)}
1853
1728
  `);
1854
1729
  }
1855
1730
  }
1856
1731
  } else {
1857
1732
  renderer.write(
1858
1733
  `
1859
- ${color8.dim("\u25CB")} ${color8.dim(`No project manifest in ${projectRoot} \u2014 running in a scratch directory.`)}
1734
+ ${color7.dim("\u25CB")} ${color7.dim(`No project manifest in ${projectRoot} \u2014 running in a scratch directory.`)}
1860
1735
  `
1861
1736
  );
1862
1737
  }
1863
- const answer = (await reader.readLine(` ${color8.amber("?")} Continue anyway? ${color8.dim("[Y/n/q]")} `)).trim().toLowerCase();
1738
+ const answer = (await reader.readLine(` ${color7.amber("?")} Continue anyway? ${color7.dim("[Y/n/q]")} `)).trim().toLowerCase();
1864
1739
  if (answer === "q" || answer === "n" || answer === "no") {
1865
- renderer.write(color8.dim(" Cancelled.\n"));
1740
+ renderer.write(color7.dim(" Cancelled.\n"));
1866
1741
  return false;
1867
1742
  }
1868
1743
  return true;
@@ -1899,39 +1774,39 @@ async function runSystemPromptMenu(deps) {
1899
1774
  if (suggestScout && lastVariant !== "scout") {
1900
1775
  renderer.write(
1901
1776
  `
1902
- ${color9.dim("\u25CB")} ${color9.dim(`Not a project folder \u2014 ${labelFor("scout")} (general-purpose) may fit better; answer n to choose it.`)}
1777
+ ${color8.dim("\u25CB")} ${color8.dim(`Not a project folder \u2014 ${labelFor("scout")} (general-purpose) may fit better; answer n to choose it.`)}
1903
1778
  `
1904
1779
  );
1905
1780
  }
1906
1781
  const answer2 = (await reader.readLine(
1907
- ` ${color9.amber("?")} Continue with ${color9.bold(labelFor(lastVariant))} system prompt ${color9.dim(`(~${fmtTok(tokens[lastVariant])} tokens)`)}? ${color9.dim("[Y/n/q]")} ${color9.dim("(auto Y in 5s)")} `,
1782
+ ` ${color8.amber("?")} Continue with ${color8.bold(labelFor(lastVariant))} system prompt ${color8.dim(`(~${fmtTok(tokens[lastVariant])} tokens)`)}? ${color8.dim("[Y/n/q]")} ${color8.dim("(auto Y in 5s)")} `,
1908
1783
  { timeoutMs: SUMMARY_TIMEOUT_MS2, defaultAnswer: "y" }
1909
1784
  )).trim().toLowerCase();
1910
1785
  if (answer2 === "q" || answer2 === "quit") throw new LaunchAbortedError();
1911
1786
  if (answer2 !== "n" && answer2 !== "no") return lastVariant;
1912
1787
  }
1913
1788
  renderer.write(`
1914
- ${color9.amber("?")} System prompt:
1789
+ ${color8.amber("?")} System prompt:
1915
1790
  `);
1916
1791
  if (suggestScout) {
1917
1792
  renderer.write(
1918
- ` ${color9.dim(`Not a project folder \u2014 ${labelFor("scout")} is preselected.`)}
1793
+ ` ${color8.dim(`Not a project folder \u2014 ${labelFor("scout")} is preselected.`)}
1919
1794
  `
1920
1795
  );
1921
1796
  }
1922
1797
  for (const [index, opt] of SYSTEM_PROMPT_OPTIONS.entries()) {
1923
1798
  const num2 = index + 1;
1924
1799
  const isDefault = opt.variant === defaultVariant;
1925
- const marker = isDefault ? color9.cyan("\u25CF") : color9.dim("\u25CB");
1926
- const label = isDefault ? color9.bold(opt.label) : opt.label;
1800
+ const marker = isDefault ? color8.cyan("\u25CF") : color8.dim("\u25CB");
1801
+ const label = isDefault ? color8.bold(opt.label) : opt.label;
1927
1802
  renderer.write(
1928
- ` ${color9.dim(`${num2}.`)} ${marker} ${label.padEnd(9)} ${color9.dim(`~${fmtTok(tokens[opt.variant])} tokens`)} ${color9.dim(opt.hint)}
1803
+ ` ${color8.dim(`${num2}.`)} ${marker} ${label.padEnd(9)} ${color8.dim(`~${fmtTok(tokens[opt.variant])} tokens`)} ${color8.dim(opt.hint)}
1929
1804
  `
1930
1805
  );
1931
1806
  }
1932
1807
  const answer = (await reader.readLine(
1933
1808
  `
1934
- ${color9.amber("?")} Select system prompt ${color9.dim(`[1-${SYSTEM_PROMPT_OPTIONS.length}, Enter = ${labelFor(defaultVariant)}, q to quit]`)}: `
1809
+ ${color8.amber("?")} Select system prompt ${color8.dim(`[1-${SYSTEM_PROMPT_OPTIONS.length}, Enter = ${labelFor(defaultVariant)}, q to quit]`)}: `
1935
1810
  )).trim().toLowerCase();
1936
1811
  if (answer === "q" || answer === "quit") throw new LaunchAbortedError();
1937
1812
  if (!answer) return defaultVariant;
@@ -1961,7 +1836,7 @@ async function maybeRunSystemPromptMenu(opts) {
1961
1836
  if (chosen === savedVariant) return { variant: chosen, aborted: false, changed: false };
1962
1837
  if (opts.outsideProject && chosen === "scout") {
1963
1838
  opts.renderer.write(
1964
- ` ${color9.dim(`${labelFor("scout")} for this launch only; the saved default stays ${labelFor(savedVariant ?? "default")}.`)}
1839
+ ` ${color8.dim(`${labelFor("scout")} for this launch only; the saved default stays ${labelFor(savedVariant ?? "default")}.`)}
1965
1840
  `
1966
1841
  );
1967
1842
  return { variant: chosen, aborted: false, changed: true };
@@ -1978,1294 +1853,1531 @@ async function maybeRunSystemPromptMenu(opts) {
1978
1853
  }
1979
1854
  }
1980
1855
 
1981
- // src/boot-config.ts
1982
- import { bootConfig as coreBootConfig } from "@wrongstack/core/infrastructure";
1983
- async function bootConfig(flags) {
1984
- const isWebui = !!flags["webui"];
1985
- const opts = {
1986
- flags,
1987
- appLabel: "wstack",
1988
- ...isWebui ? { skipIdentityValidation: true } : {}
1989
- };
1990
- const { cwd, projectRoot, userHome, wpaths, pathResolver, config, vault } = await coreBootConfig(opts);
1991
- return {
1992
- paths: { cwd, projectRoot, userHome, wpaths, pathResolver },
1993
- config,
1994
- vault
1995
- };
1996
- }
1997
-
1998
- // src/input-reader.ts
1999
- import * as fs7 from "node:fs/promises";
2000
- import * as path8 from "node:path";
2001
- import * as readline from "node:readline";
1856
+ // src/boot-provider-selection.ts
1857
+ import * as fs6 from "node:fs/promises";
1858
+ import { createRequire as createRequire4 } from "node:module";
1859
+ import * as path7 from "node:path";
1860
+ import { fileURLToPath } from "node:url";
2002
1861
  import {
2003
- resolveWstackPaths,
2004
- setOutputLineGuard,
2005
- setRawMode as setRawMode2,
2006
- writeOut
1862
+ color as color9,
1863
+ moduleUrlFor,
1864
+ toErrorMessage as toErrorMessage4,
1865
+ wrongstackPackageJsonPath as wrongstackPackageJsonPath3
2007
1866
  } from "@wrongstack/core/utils";
2008
- var ReadlineInputReader = class {
2009
- rl;
2010
- historyFile;
2011
- history = [];
2012
- pending = false;
2013
- suppressNextHistory = false;
2014
- constructor(opts = {}) {
2015
- this.historyFile = opts.historyFile ?? resolveWstackPaths({ projectRoot: process.cwd() }).historyFile;
1867
+ import { isSetupProvider } from "@wrongstack/providers";
1868
+ function resolveBundledOverlayFile() {
1869
+ try {
1870
+ return fileURLToPath(
1871
+ new URL("../data/providers.json", moduleUrlFor(import.meta.url, "@wrongstack/cli"))
1872
+ );
1873
+ } catch {
1874
+ return void 0;
2016
1875
  }
2017
- async loadHistory() {
2018
- try {
2019
- const raw = await fs7.readFile(this.historyFile, "utf8");
2020
- this.history = raw.split("\n").filter(Boolean).slice(-1e3);
2021
- } catch {
2022
- this.history = [];
2023
- }
1876
+ }
1877
+ async function validateSavedProviderModel(config, modelsRegistry) {
1878
+ const providerId = config.provider;
1879
+ const modelId = config.model;
1880
+ if (!providerId || !modelId) return { ok: false, reason: "missing provider/model" };
1881
+ if (isSetupProvider(providerId)) return { ok: true };
1882
+ const saved = config.providers?.[providerId];
1883
+ const lookupId = saved?.type && saved.type !== providerId ? saved.type : providerId;
1884
+ const catalogProvider = await modelsRegistry.getProvider(lookupId).catch(() => void 0);
1885
+ if (!catalogProvider && !saved?.family)
1886
+ return { ok: false, reason: `provider "${providerId}" is no longer available` };
1887
+ const hasCredential = (catalogProvider?.envVars ?? saved?.envVars ?? []).some(
1888
+ (envVar) => Boolean(process.env[envVar])
1889
+ ) || saved !== void 0 && resolveActiveApiKey(saved) !== void 0 || isKeylessLocalProvider({
1890
+ apiBase: saved?.baseUrl ?? catalogProvider?.apiBase,
1891
+ envVars: saved?.envVars ?? catalogProvider?.envVars
1892
+ });
1893
+ if (!hasCredential) return { ok: false, reason: `provider "${providerId}" has no usable key` };
1894
+ const visible = visibleModelIds(
1895
+ providerId,
1896
+ config,
1897
+ (catalogProvider?.models ?? []).map((m) => m.id),
1898
+ saved
1899
+ );
1900
+ if (visible.length > 0 && !visible.includes(modelId)) {
1901
+ return {
1902
+ ok: false,
1903
+ reason: `model "${modelId}" is no longer available for provider "${providerId}"`
1904
+ };
2024
1905
  }
2025
- async saveHistory() {
2026
- try {
2027
- await fs7.mkdir(path8.dirname(this.historyFile), { recursive: true });
2028
- await fs7.writeFile(this.historyFile, this.history.slice(-1e3).join("\n"));
2029
- } catch {
1906
+ return { ok: true };
1907
+ }
1908
+ async function autoSelectSavedProvider(config, modelsRegistry) {
1909
+ const providers = config.providers ?? {};
1910
+ const entries = config.provider && providers[config.provider] ? [[config.provider, providers[config.provider]]] : Object.entries(providers);
1911
+ for (const [id, cfg] of entries) {
1912
+ if (!cfg) continue;
1913
+ let model = cfg.models?.[0];
1914
+ if (!model) {
1915
+ const catalogId = cfg.type && cfg.type !== id ? cfg.type : id;
1916
+ const catalog = await modelsRegistry.getProvider(catalogId).catch(() => void 0);
1917
+ model = catalog?.models?.[0]?.id;
2030
1918
  }
1919
+ if (model) return { provider: id, model };
2031
1920
  }
2032
- shouldPersistHistory(line) {
2033
- return !/^\s*\/(?:telegram-setup|tg-setup)\s+\d+:[A-Za-z0-9_-]+(?:\s|$)/i.test(line);
1921
+ return void 0;
1922
+ }
1923
+ function resolveBundledSkillsDir2() {
1924
+ try {
1925
+ const req = createRequire4(import.meta.url);
1926
+ const corePkg = wrongstackPackageJsonPath3("@wrongstack/core", (id) => req.resolve(id));
1927
+ return path7.join(path7.dirname(corePkg), "skills");
1928
+ } catch {
1929
+ return void 0;
2034
1930
  }
2035
- ensure() {
2036
- if (!this.rl) {
2037
- this.rl = readline.createInterface({
2038
- input: process.stdin,
2039
- output: process.stdout,
2040
- history: this.history,
2041
- terminal: process.stdin.isTTY
2042
- });
2043
- }
2044
- return this.rl;
1931
+ }
1932
+ function resolveBundledPromptsDir2() {
1933
+ try {
1934
+ const req = createRequire4(import.meta.url);
1935
+ const corePkg = wrongstackPackageJsonPath3("@wrongstack/core", (id) => req.resolve(id));
1936
+ return path7.join(path7.dirname(corePkg), "data", "prompts");
1937
+ } catch {
1938
+ return void 0;
2045
1939
  }
2046
- async readLine(prompt, timeoutOpts) {
2047
- if (this.history.length === 0) await this.loadHistory();
2048
- while (this.pending) {
2049
- await new Promise((resolve3) => setTimeout(resolve3, 50));
2050
- }
2051
- this.pending = true;
2052
- try {
2053
- if (this.rl) {
2054
- const old = this.rl;
2055
- this.rl = void 0;
2056
- await new Promise((resolve3) => {
2057
- if (old.closed) {
2058
- resolve3();
2059
- } else {
2060
- old.once("close", resolve3);
2061
- old.close();
2062
- }
1940
+ }
1941
+ function isHomeDirectory(cwd, userHome) {
1942
+ if (!cwd || !userHome) return false;
1943
+ return path7.resolve(cwd) === path7.resolve(userHome);
1944
+ }
1945
+ function shouldPrintYoloNotice(lastChoices, yoloPinned, yolo) {
1946
+ return !lastChoices && yoloPinned === void 0 && yolo;
1947
+ }
1948
+ async function checkGitInCwd(opts) {
1949
+ const { cwd, renderer, reader } = opts;
1950
+ const cwdGit = path7.join(cwd, ".git");
1951
+ let hasCwdGit = false;
1952
+ try {
1953
+ await fs6.access(cwdGit);
1954
+ hasCwdGit = true;
1955
+ } catch {
1956
+ }
1957
+ if (!hasCwdGit) {
1958
+ renderer.write(
1959
+ `
1960
+ ${color9.amber("\u25CB")} This folder has no ${color9.bold(".git")} repository.
1961
+ `
1962
+ );
1963
+ const answer = (await reader.readLine(` ${color9.amber("?")} Initialize one here? ${color9.dim("[y/N]")} `)).trim().toLowerCase();
1964
+ if (answer === "y" || answer === "yes") {
1965
+ try {
1966
+ const { spawn: spawn2 } = await import("node:child_process");
1967
+ await new Promise((resolve3, reject) => {
1968
+ const child = spawn2("git", ["init"], {
1969
+ cwd,
1970
+ signal: AbortSignal.timeout(1e4),
1971
+ windowsHide: true
1972
+ });
1973
+ child.on("error", reject);
1974
+ child.on(
1975
+ "close",
1976
+ (code) => code === 0 ? resolve3() : reject(new Error(`git init failed with ${code}`))
1977
+ );
2063
1978
  });
2064
- }
2065
- const fresh = this.ensure();
2066
- this.installPromptGuard(fresh);
2067
- const line = await new Promise((resolve3) => {
2068
- let settled = false;
2069
- let timeoutHandle;
2070
- const settle = (line2) => {
2071
- if (settled) return;
2072
- settled = true;
2073
- if (timeoutHandle) clearTimeout(timeoutHandle);
2074
- setOutputLineGuard(null);
2075
- resolve3(line2);
2076
- };
2077
- if (timeoutOpts) {
2078
- timeoutHandle = setTimeout(() => {
2079
- if (!settled) {
2080
- fresh.write(`${timeoutOpts.defaultAnswer}
1979
+ renderer.write(` ${color9.green("\u2713")} Git repository initialized
2081
1980
  `);
2082
- }
2083
- }, timeoutOpts.timeoutMs);
2084
- }
2085
- fresh.question(prompt ?? "> ", (line2) => {
2086
- const persistHistory = !this.suppressNextHistory && this.shouldPersistHistory(line2);
2087
- this.suppressNextHistory = false;
2088
- if (line2.trim() && persistHistory) {
2089
- this.history.push(line2);
2090
- this.saveHistory().then(void 0, () => {
2091
- });
2092
- }
2093
- settle(line2);
2094
- });
2095
- fresh.once("close", () => settle(""));
2096
- fresh.on?.("error", (_e) => settle(""));
2097
- }).then((result) => {
2098
- if (this.rl === fresh) this.rl = void 0;
2099
- fresh.close();
2100
- return result;
2101
- });
2102
- return line;
2103
- } finally {
2104
- this.pending = false;
1981
+ hasCwdGit = true;
1982
+ } catch (err) {
1983
+ renderer.writeError(`git init failed: ${toErrorMessage4(err)}
1984
+ `);
1985
+ }
2105
1986
  }
2106
1987
  }
2107
- /**
2108
- * Install the out-of-band write guard for the active prompt. When a log
2109
- * line or other async output lands while the user is mid-type, the guard
2110
- * clears the draft row, lets the message print, then repaints the prompt
2111
- * and the in-progress draft (cursor preserved) via readline's own
2112
- * refresh. Without it, each async write leaves the half-typed line
2113
- * stranded as a fresh scrollback row.
2114
- *
2115
- * No-op on non-TTY output (piped/redirected) — there's no draft to
2116
- * protect and the ANSI clear/repaint would be noise in a file.
2117
- */
2118
- installPromptGuard(rl) {
2119
- const out = process.stdout;
2120
- if (!out.isTTY) {
2121
- setOutputLineGuard(null);
2122
- return;
1988
+ const parentDir = path7.dirname(cwd);
1989
+ if (parentDir !== cwd) {
1990
+ try {
1991
+ await fs6.access(path7.join(parentDir, ".git"));
1992
+ renderer.write(
1993
+ ` ${color9.dim("\u2139")} A ${color9.bold(".git")} repo exists in the parent directory: ${color9.dim(parentDir)}
1994
+ `
1995
+ );
1996
+ } catch {
2123
1997
  }
2124
- setOutputLineGuard({
2125
- suspend() {
2126
- readline.cursorTo(out, 0);
2127
- readline.clearLine(out, 0);
1998
+ }
1999
+ }
2000
+
2001
+ // src/boot-launch-choices.ts
2002
+ async function applyBootLaunchChoices(input) {
2003
+ let { config } = input;
2004
+ const {
2005
+ isInteractiveTTY,
2006
+ simpleUiFullAuto,
2007
+ flags,
2008
+ renderer,
2009
+ reader,
2010
+ profileConfigPath,
2011
+ wpaths,
2012
+ projectRoot
2013
+ } = input;
2014
+ if (isInteractiveTTY) {
2015
+ const promptMenu = await maybeRunSystemPromptMenu({
2016
+ isInteractiveTTY,
2017
+ flags,
2018
+ renderer,
2019
+ reader,
2020
+ profileConfigPath,
2021
+ paths: {
2022
+ globalDir: wpaths.globalInstructions,
2023
+ projectDir: wpaths.inProjectInstructions
2128
2024
  },
2129
- resume() {
2130
- try {
2131
- rl.prompt(true);
2132
- } catch {
2133
- }
2134
- }
2025
+ outsideProject: await isOutsideProject(projectRoot)
2135
2026
  });
2136
- }
2137
- async readKey(prompt, options, opts = {}) {
2138
- setOutputLineGuard(null);
2139
- const signal = opts.signal;
2140
- if (signal?.aborted) return "";
2141
- writeOut(prompt);
2142
- return new Promise((resolve3) => {
2143
- const stdin = process.stdin;
2144
- const wasRaw = stdin.isRaw;
2145
- const wasPaused = stdin.isPaused();
2146
- setRawMode2(stdin, true);
2147
- stdin.resume();
2148
- const onData = (buf) => {
2149
- const key = buf.toString();
2150
- if (key === "") {
2151
- cleanup();
2152
- writeOut("\n");
2153
- resolve3("");
2154
- return;
2155
- }
2156
- const opt = options.find(
2157
- (o) => o.key.toLowerCase() === key.toLowerCase() || o.value === key
2158
- );
2159
- if (opt) {
2160
- cleanup();
2161
- writeOut(`${opt.key}
2162
- `);
2163
- resolve3(opt.value);
2164
- }
2165
- };
2166
- const onClose = () => {
2167
- cleanup();
2168
- resolve3("");
2169
- };
2170
- const onAbort = () => {
2171
- cleanup();
2172
- writeOut("\n");
2173
- resolve3("");
2174
- };
2175
- const cleanup = () => {
2176
- stdin.off("data", onData);
2177
- stdin.off("close", onClose);
2178
- signal?.removeEventListener("abort", onAbort);
2179
- setRawMode2(stdin, wasRaw);
2180
- if (wasPaused) stdin.pause();
2181
- };
2182
- stdin.on("data", onData);
2183
- stdin.on("close", onClose);
2184
- signal?.addEventListener("abort", onAbort, { once: true });
2185
- });
2186
- }
2187
- /**
2188
- * Read a line of input while masking each character with a bullet so the
2189
- * user gets visual confirmation that bytes are arriving (especially on
2190
- * paste, which previously felt like nothing happened). Pasted chunks
2191
- * are echoed as a run of bullets, Backspace/DEL erases one bullet, and
2192
- * Ctrl+U / Ctrl+T are honored. Non-TTY input is read normally — there's
2193
- * nothing to hide when piped, and echoing bullets to a file is noise.
2194
- *
2195
- * Returns the raw entered string (no trim — caller decides).
2196
- */
2197
- async readSecret(prompt) {
2198
- const stdin = process.stdin;
2199
- if (!stdin.isTTY) {
2200
- this.suppressNextHistory = true;
2201
- return this.readLine(prompt);
2027
+ if (promptMenu.aborted) {
2028
+ await reader.close();
2029
+ return { kind: "exit", code: 0 };
2202
2030
  }
2203
- setOutputLineGuard(null);
2204
- this.rl?.close();
2205
- this.rl = void 0;
2206
- writeOut(prompt);
2207
- return new Promise((resolve3) => {
2208
- let buf = "";
2209
- const wasRaw = stdin.isRaw;
2210
- const cleanup = () => {
2211
- stdin.off("data", onData);
2212
- setRawMode2(stdin, wasRaw);
2213
- stdin.pause();
2214
- };
2215
- try {
2216
- setRawMode2(stdin, true);
2217
- stdin.resume();
2218
- stdin.setEncoding("utf8");
2219
- } catch (setupErr) {
2220
- setRawMode2(stdin, wasRaw);
2221
- stdin.pause();
2222
- throw setupErr;
2223
- }
2224
- const eraseChar = () => {
2225
- writeOut("\b \b");
2226
- };
2227
- const eraseAll = () => {
2228
- for (let i = 0; i < buf.length; i++) eraseChar();
2229
- };
2230
- let escState = 0;
2231
- const onData = (chunk) => {
2232
- for (const ch of chunk) {
2233
- if (escState === 1) {
2234
- escState = ch === "[" ? 2 : ch === "O" ? 3 : 0;
2235
- continue;
2236
- }
2237
- if (escState === 2) {
2238
- if (ch >= "@" && ch <= "~") escState = 0;
2239
- continue;
2240
- }
2241
- if (escState === 3) {
2242
- escState = 0;
2243
- continue;
2244
- }
2245
- if (ch === "\x1B") {
2246
- escState = 1;
2247
- continue;
2248
- }
2249
- if (ch === "\r" || ch === "\n") {
2250
- cleanup();
2251
- writeOut(` ${dim(`[${buf.length} chars]`)}
2252
- `);
2253
- resolve3(buf);
2254
- return;
2255
- }
2256
- if (ch === "") {
2257
- cleanup();
2258
- writeOut("\n");
2259
- resolve3("");
2260
- return;
2261
- }
2262
- if (ch === "") {
2263
- eraseAll();
2264
- buf = "";
2265
- continue;
2266
- }
2267
- if (ch === "") {
2268
- const m = buf.match(/(\S+\s*)$/);
2269
- const drop = m ? m[0].length : buf.length;
2270
- for (let i = 0; i < drop; i++) eraseChar();
2271
- buf = buf.slice(0, buf.length - drop);
2272
- continue;
2273
- }
2274
- if (ch === "\x7F" || ch === "\b") {
2275
- if (buf.length > 0) {
2276
- buf = buf.slice(0, -1);
2277
- eraseChar();
2278
- }
2279
- continue;
2280
- }
2281
- if (ch < " ") continue;
2282
- buf += ch;
2283
- writeOut("\u2022");
2284
- }
2285
- if (escState === 1) escState = 0;
2286
- };
2287
- stdin.on("data", onData);
2288
- });
2289
- }
2290
- async close() {
2291
- setOutputLineGuard(null);
2292
- await this.saveHistory();
2293
- this.rl?.close();
2294
- this.rl = void 0;
2295
- }
2296
- };
2297
- function dim(s) {
2298
- if (!process.stdout.isTTY) return s;
2299
- return `\x1B[2m${s}\x1B[22m`;
2300
- }
2301
-
2302
- // src/renderer.ts
2303
- import { color as color10 } from "@wrongstack/core/utils";
2304
- var TerminalRenderer = class _TerminalRenderer {
2305
- out;
2306
- err;
2307
- lineStart = true;
2308
- /** When true, stdout-bound rendering is disabled (for non-terminal UI surfaces). */
2309
- silent = false;
2310
- /**
2311
- * When true, Ink exclusively owns the terminal. Both stdout and stderr
2312
- * rendering must be disabled: either stream can interleave with Ink's
2313
- * cursor updates and make the TUI flicker or leave stray debug notes.
2314
- */
2315
- tuiActive = false;
2316
- /**
2317
- * Per-tool on-screen result render mode. Defaults to `'extend'` so
2318
- * existing callers without `setResultRenderMode` see full content.
2319
- * `setResultRenderMode(name, mode)` overrides the entry for `name`; the
2320
- * override is one-shot for that name (the default reasserts after the
2321
- * next write) so callers can set the mode right before each tool call
2322
- * without leaking state across unrelated tool calls.
2323
- */
2324
- renderModes = /* @__PURE__ */ new Map();
2325
- /** Mode applied when no entry is set for a given tool name. */
2326
- static DEFAULT_RENDER_MODE = "extend";
2327
- /** Maximum number of one-shot hints retained when callers abandon a result. */
2328
- static MAX_RENDER_MODES = 256;
2329
- /** Lines kept per tool family in `extend` mode. */
2330
- static EXTEND_PREVIEW_LINES = 10;
2331
- constructor(opts = {}) {
2332
- this.out = opts.out ?? process.stdout;
2333
- this.err = opts.err ?? process.stderr;
2334
- }
2335
- /**
2336
- * Toggle stdout suppression for non-terminal UI surfaces such as WebUI.
2337
- * Diagnostics continue to flow to stderr. Idempotent.
2338
- */
2339
- setSilent(silent) {
2340
- this.silent = silent;
2341
- }
2342
- isSilent() {
2343
- return this.silent;
2344
- }
2345
- /**
2346
- * Toggle exclusive TUI ownership of both terminal streams. Unlike
2347
- * {@link setSilent}, TUI mode suppresses stderr as well as stdout and clears
2348
- * any CLI-only result mode staged before Ink took ownership. While active,
2349
- * new result-mode hints are ignored. The two flags compose with OR semantics
2350
- * for stdout suppression only; TUI ownership adds the other behaviors.
2351
- * Enable immediately before starting Ink and disable in the paired
2352
- * `finally`, only after the TUI has fully exited.
2353
- */
2354
- setTuiActive(active) {
2355
- if (active) this.renderModes.clear();
2356
- this.tuiActive = active;
2357
- }
2358
- isTuiActive() {
2359
- return this.tuiActive;
2360
- }
2361
- suppressStdout() {
2362
- return this.silent || this.tuiActive;
2363
- }
2364
- setResultRenderMode(name, mode) {
2365
- if (this.suppressStdout()) return;
2366
- if (this.renderModes.delete(name)) {
2367
- this.renderModes.set(name, mode);
2368
- return;
2031
+ if (promptMenu.changed && promptMenu.variant) {
2032
+ config = patchConfig(config, { systemPrompt: { variant: promptMenu.variant } });
2369
2033
  }
2370
- if (this.renderModes.size >= _TerminalRenderer.MAX_RENDER_MODES) {
2371
- const oldestName = this.renderModes.keys().next().value;
2372
- if (oldestName !== void 0) this.renderModes.delete(oldestName);
2034
+ if (promptMenu.persistError) {
2035
+ renderer.writeWarning(
2036
+ `Could not save system prompt variant to config: ${toErrorMessage5(promptMenu.persistError)}
2037
+ `
2038
+ );
2373
2039
  }
2374
- this.renderModes.set(name, mode);
2375
- }
2376
- getRenderMode(name) {
2377
- return this.renderModes.get(name) ?? _TerminalRenderer.DEFAULT_RENDER_MODE;
2378
- }
2379
- clearRenderMode(name) {
2380
- this.renderModes.delete(name);
2381
- }
2382
- write(input) {
2383
- if (this.suppressStdout()) return;
2384
- const text = typeof input === "string" ? input : input.text;
2385
- if (!text) return;
2386
- const rendered = renderMarkdown(text);
2387
- this.out.write(rendered);
2388
- this.lineStart = rendered.endsWith("\n");
2389
- }
2390
- writeLine(text = "") {
2391
- if (this.suppressStdout()) return;
2392
- if (!this.lineStart) this.out.write("\n");
2393
- if (text) this.out.write(`${text}
2394
- `);
2395
- else this.out.write("\n");
2396
- this.lineStart = true;
2397
- }
2398
- writeBlock(block) {
2399
- if (block.type === "tool_result") {
2400
- const text = typeof block.content === "string" ? block.content : JSON.stringify(block.content);
2401
- this.writeToolResult(block.name ?? "result", text, !!block.is_error);
2402
- return;
2040
+ let modePinned;
2041
+ if (flags["no-tui"]) modePinned = "repl";
2042
+ else if (flags["tui"]) modePinned = "tui";
2043
+ const yoloPinned = flags["no-yolo"] === true ? false : flags["yolo"] === true ? true : void 0;
2044
+ let autonomyPinned;
2045
+ if (flags["no-autonomy"] === true) autonomyPinned = "off";
2046
+ else if (typeof flags["eternal"] === "string" && flags["eternal"].trim() !== "")
2047
+ autonomyPinned = "off";
2048
+ else if (typeof flags["autonomy"] === "string") {
2049
+ const v = flags["autonomy"].toLowerCase();
2050
+ autonomyPinned = v === "off" || v === "no" || v === "false" ? "off" : "auto";
2051
+ } else if (flags["autonomy"] === true) {
2052
+ autonomyPinned = "auto";
2403
2053
  }
2404
- if (this.suppressStdout()) return;
2405
- if (block.type === "text") {
2406
- this.write(block);
2407
- } else if (block.type === "tool_use") {
2408
- this.writeToolCall(block.name, block.input);
2054
+ const lastChoices = config.launch ? {
2055
+ mode: config.launch.mode ?? "tui",
2056
+ yolo: config.yolo ?? true,
2057
+ autonomy: config.launch.autonomy ?? "auto"
2058
+ } : void 0;
2059
+ let choices;
2060
+ try {
2061
+ choices = await runLaunchPrompts({
2062
+ renderer,
2063
+ reader,
2064
+ modePinned,
2065
+ yoloPinned,
2066
+ autonomyPinned,
2067
+ lastChoices
2068
+ });
2069
+ } catch (err) {
2070
+ if (err instanceof LaunchAbortedError) {
2071
+ await reader.close();
2072
+ return { kind: "exit", code: 0 };
2073
+ }
2074
+ throw err;
2409
2075
  }
2410
- }
2411
- writeToolCall(name, input) {
2412
- if (this.suppressStdout()) return;
2413
- if (!this.lineStart) this.out.write("\n");
2414
- const arrow = theme2.primary("\u2192");
2415
- const display = formatInputSummary(input);
2416
- this.out.write(`${arrow} ${theme2.bold(name)}${display ? color10.dim(` ${display}`) : ""}
2417
- `);
2418
- this.lineStart = true;
2419
- }
2420
- writeToolResult(name, content, isError) {
2421
- if (this.suppressStdout()) {
2422
- this.clearRenderMode(name);
2423
- return;
2076
+ if (choices.mode === "tui") {
2077
+ flags["tui"] = true;
2078
+ flags["no-tui"] = false;
2079
+ } else {
2080
+ flags["tui"] = false;
2081
+ flags["no-tui"] = true;
2424
2082
  }
2425
- const txt = typeof content === "string" ? content : safeStringify(content);
2426
- const prefix = isError ? theme2.error("\u2718") : theme2.success("\u2713");
2427
- const renderMode = this.getRenderMode(name);
2428
- this.clearRenderMode(name);
2429
- const bodyText = extractBodyText(content, name);
2430
- if (isError) {
2431
- const firstLine = txt.split("\n")[0] ?? "";
2432
- const truncated = firstLine.length > 200 ? `${firstLine.slice(0, 197)}\u2026` : firstLine;
2433
- this.out.write(` ${prefix} ${color10.dim(truncated)}
2434
- `);
2435
- this.lineStart = true;
2436
- return;
2083
+ if (choices.yolo !== config.yolo) config = patchConfig(config, { yolo: choices.yolo });
2084
+ flags["autonomy"] = choices.autonomy;
2085
+ if (shouldPrintYoloNotice(lastChoices, yoloPinned, choices.yolo)) {
2086
+ writeErr(
2087
+ `
2088
+ ${color10.yellow("YOLO is on")}: non-denied tool calls, including shell and file writes, run without confirmation.
2089
+ ${color10.dim("Damaging calls and calls your deny rules forbid still ask. Use")} --no-yolo ${color10.dim("or")} /yolo off ${color10.dim("to require prompts.")}
2090
+
2091
+ `
2092
+ );
2437
2093
  }
2438
- const isEditLike = name === "edit" || name === "write";
2439
- const diff = extractDiff(content);
2440
- if (isEditLike && diff) {
2441
- this.out.write(` ${prefix} ${color10.dim(summarize(content, name))}
2442
- `);
2443
- const rendered = renderDiff(diff).split("\n").map((l) => ` ${l}`).join("\n");
2444
- this.out.write(`${rendered}
2445
- `);
2446
- this.lineStart = true;
2447
- return;
2094
+ if ((flags["skip-index"] || flags["skip"]) && config.indexing) {
2095
+ config = patchConfig(config, {
2096
+ indexing: { ...config.indexing, onSessionStart: false }
2097
+ });
2448
2098
  }
2449
- const summary = summarize(content, name);
2450
- const isReadLike = name === "read" || name === "grep" || name === "glob" || name === "bash" || name === "exec";
2451
- if (renderMode === "simple") {
2452
- this.out.write(` ${prefix} ${color10.dim(summary || compactSingleLine(bodyText))}
2453
- `);
2454
- this.lineStart = true;
2455
- return;
2099
+ if (!simpleUiFullAuto) {
2100
+ try {
2101
+ const toPersist = flags["webui"] ? { ...choices, mode: lastChoices?.mode ?? config.launch?.mode ?? "tui" } : choices;
2102
+ await persistLaunchChoices(profileConfigPath, toPersist);
2103
+ } catch {
2104
+ }
2456
2105
  }
2457
- const previewLines = isEditLike ? 0 : isReadLike ? _TerminalRenderer.EXTEND_PREVIEW_LINES : 2;
2458
- const lines = bodyText.split("\n");
2459
- const head = lines.slice(0, previewLines).map((l) => l.replace(/\s+$/, ""));
2460
- const moreCount = Math.max(0, lines.length - head.length);
2461
- this.out.write(` ${prefix} ${color10.dim(summary)}
2106
+ } else {
2107
+ const nonInteractiveAutonomy = !simpleUiFullAuto && (flags["no-autonomy"] === true || config.autonomy?.defaultMode === "off") ? "off" : "auto";
2108
+ const effectiveChoices = config.launch ? {
2109
+ mode: flags["no-tui"] ? "repl" : config.launch.mode ?? "tui",
2110
+ yolo: config.yolo ?? true,
2111
+ autonomy: nonInteractiveAutonomy
2112
+ } : {
2113
+ mode: "repl",
2114
+ yolo: true,
2115
+ autonomy: nonInteractiveAutonomy
2116
+ };
2117
+ if (effectiveChoices.mode === "repl") {
2118
+ flags["tui"] = false;
2119
+ flags["no-tui"] = true;
2120
+ }
2121
+ flags["autonomy"] = effectiveChoices.autonomy;
2122
+ }
2123
+ return { kind: "ok", config };
2124
+ }
2125
+
2126
+ // src/boot-provider-gate.ts
2127
+ import { color as color11, isStdinTTY as isStdinTTY2, toErrorMessage as toErrorMessage6, writeErr as writeErr2 } from "@wrongstack/core/utils";
2128
+ import { isSetupProvider as isSetupProvider2, SETUP_MODEL_ID, SETUP_PROVIDER_ID } from "@wrongstack/providers";
2129
+ async function resolveBootProviderModel(input) {
2130
+ let { config, vault } = input;
2131
+ const { flags, modelsRegistry, renderer, reader, profileConfigPath, noInteractiveMode } = input;
2132
+ const providerFlag = typeof flags["provider"] === "string" ? flags["provider"] : void 0;
2133
+ const modelFlag = typeof flags["model"] === "string" ? flags["model"] : void 0;
2134
+ if (noInteractiveMode && (!config.provider || !config.model)) {
2135
+ if (providerFlag && modelFlag) {
2136
+ config = patchConfig(config, { provider: providerFlag, model: modelFlag });
2137
+ } else {
2138
+ const picked = await autoSelectSavedProvider(config, modelsRegistry);
2139
+ if (picked) config = patchConfig(config, picked);
2140
+ }
2141
+ }
2142
+ if (!(!!providerFlag && !!modelFlag)) {
2143
+ if (isStdinTTY2() && !noInteractiveMode) {
2144
+ let picked;
2145
+ let skipPicker = false;
2146
+ if (isSetupProvider2(config.provider)) {
2147
+ skipPicker = true;
2148
+ renderer.write(
2149
+ `
2150
+ ${color11.amber("\u25B6")} ${color11.bold("Setup mode")} ${color11.dim("\u2014 no model connected. Run")} ${color11.bold("/auth")} ${color11.dim("to connect one.")}
2151
+
2152
+ `
2153
+ );
2154
+ } else if (!await hasAnyCredential(config, modelsRegistry)) {
2155
+ const outcome = await runFirstRunSetup({
2156
+ renderer,
2157
+ reader,
2158
+ modelsRegistry,
2159
+ vault,
2160
+ profileConfigPath,
2161
+ reloadConfig: async () => (await bootConfig(flags)).config
2162
+ });
2163
+ if (outcome.kind === "quit") {
2164
+ await reader.close();
2165
+ return { kind: "exit", code: 0 };
2166
+ }
2167
+ if (outcome.kind === "setup-mode") {
2168
+ config = patchConfig(config, { provider: SETUP_PROVIDER_ID, model: SETUP_MODEL_ID });
2169
+ await saveToGlobalConfig(profileConfigPath, SETUP_PROVIDER_ID, SETUP_MODEL_ID);
2170
+ skipPicker = true;
2171
+ } else {
2172
+ try {
2173
+ const reloaded = await bootConfig(flags);
2174
+ config = reloaded.config;
2175
+ vault = reloaded.vault;
2176
+ } catch (err) {
2177
+ writeErr2(`Config error after setup: ${toErrorMessage6(err)}
2462
2178
  `);
2463
- for (const l of head) {
2464
- const capped = l.length > 200 ? `${l.slice(0, 197)}\u2026` : l;
2465
- this.out.write(` ${color10.dim(capped)}
2179
+ await reader.close();
2180
+ return { kind: "exit", code: 2 };
2181
+ }
2182
+ }
2183
+ }
2184
+ const savedProvider = config.provider;
2185
+ const savedModel = config.model;
2186
+ if (!skipPicker && savedProvider && savedModel) {
2187
+ const savedStatus = await validateSavedProviderModel(config, modelsRegistry);
2188
+ renderer.write(
2189
+ `
2190
+ ${color11.dim("Last settings:")} ${color11.bold(savedProvider)} / ${color11.bold(savedModel)}
2191
+ `
2192
+ );
2193
+ if (!savedStatus.ok) {
2194
+ renderer.writeWarning(
2195
+ `Saved provider/model is no longer usable (${savedStatus.reason ?? "unknown reason"}); choose a provider.
2196
+ `
2197
+ );
2198
+ } else {
2199
+ const answer = (await reader.readLine(
2200
+ ` ${color11.amber("?")} Continue with these? ${color11.dim("[Y/n/q]")} ${color11.dim("(auto Y in 5s)")} `,
2201
+ { timeoutMs: 5e3, defaultAnswer: "y" }
2202
+ )).trim().toLowerCase();
2203
+ if (answer === "q") {
2204
+ renderer.write(color11.dim(" Goodbye!\n"));
2205
+ await reader.close();
2206
+ return { kind: "exit", code: 0 };
2207
+ }
2208
+ if (answer !== "n" && answer !== "no") {
2209
+ skipPicker = true;
2210
+ renderer.write(
2211
+ `
2212
+ ${color11.green("\u25B6")} ${color11.bold(savedProvider)} / ${color11.bold(savedModel)}
2213
+
2214
+ `
2215
+ );
2216
+ }
2217
+ }
2218
+ }
2219
+ if (!skipPicker) {
2220
+ picked = await runPicker({
2221
+ modelsRegistry,
2222
+ renderer,
2223
+ reader,
2224
+ config,
2225
+ defaultProvider: providerFlag ?? config.provider,
2226
+ defaultModel: modelFlag ?? config.model
2227
+ });
2228
+ }
2229
+ if (!picked && !skipPicker) {
2230
+ if (!config.provider || !config.model) {
2231
+ renderer.write(
2232
+ `
2233
+ ${color11.dim("No provider selected.")}
2234
+ ${color11.dim("Run")} ${color11.bold("wstack auth")} ${color11.dim("to add a key or sign in, or start with no model:")}
2235
+ ${color11.bold(`wstack --provider ${SETUP_PROVIDER_ID} --model ${SETUP_MODEL_ID}`)}
2236
+
2237
+ `
2238
+ );
2239
+ await reader.close();
2240
+ return { kind: "exit", code: 2 };
2241
+ }
2242
+ }
2243
+ if (picked) {
2244
+ const prevProvider = config.provider;
2245
+ const prevModel = config.model;
2246
+ const prevEffort = config.modelRuntime?.reasoning?.effort;
2247
+ config = patchConfig(config, {
2248
+ provider: picked.provider,
2249
+ model: picked.model,
2250
+ ...picked.effort ? {
2251
+ modelRuntime: {
2252
+ ...config.modelRuntime,
2253
+ reasoning: { ...config.modelRuntime?.reasoning, effort: picked.effort }
2254
+ }
2255
+ } : {}
2256
+ });
2257
+ const effortChanged = picked.effort !== void 0 && picked.effort !== prevEffort;
2258
+ if (picked.provider !== prevProvider || picked.model !== prevModel || effortChanged) {
2259
+ const label = `${picked.provider}/${picked.model}${picked.effort ? ` (effort ${picked.effort})` : ""}`;
2260
+ const saved = await saveToGlobalConfig(profileConfigPath, picked.provider, picked.model, {
2261
+ effort: picked.effort
2262
+ });
2263
+ if (saved) {
2264
+ renderer.writeInfo(`Saved ${label} as default.
2466
2265
  `);
2467
- }
2468
- if (moreCount > 0) {
2469
- this.out.write(
2470
- ` ${color10.dim(`+${moreCount} more line${moreCount === 1 ? "" : "s"}`)}
2266
+ } else {
2267
+ renderer.writeWarning(
2268
+ `Could not save ${label} to config. Check permissions or disk space.
2269
+ `
2270
+ );
2271
+ }
2272
+ }
2273
+ }
2274
+ } else if (!config.provider || !config.model) {
2275
+ writeErr2(
2276
+ `No provider or model configured. Run \`wstack auth\`, or pass --provider <id> --model <id>.
2277
+ To start the app with no model connected, pass --provider ${SETUP_PROVIDER_ID} --model ${SETUP_MODEL_ID}.
2471
2278
  `
2472
2279
  );
2280
+ await reader.close();
2281
+ return { kind: "exit", code: 2 };
2282
+ } else {
2283
+ const savedStatus = await validateSavedProviderModel(config, modelsRegistry);
2284
+ if (!savedStatus.ok) {
2285
+ writeErr2(
2286
+ `Saved provider/model is no longer usable (${savedStatus.reason ?? "unknown reason"}). Run \`wstack auth\` or pass --provider <id> --model <id>.
2287
+ `
2288
+ );
2289
+ await reader.close();
2290
+ return { kind: "exit", code: 2 };
2291
+ }
2473
2292
  }
2474
- this.lineStart = true;
2475
2293
  }
2476
- writeDiff(diff) {
2477
- if (this.suppressStdout()) return;
2478
- if (!this.lineStart) this.out.write("\n");
2479
- this.out.write(`${renderDiff(diff)}
2480
- `);
2481
- this.lineStart = true;
2294
+ return { kind: "ok", config, vault };
2295
+ }
2296
+
2297
+ // src/boot-subcommand-dispatch.ts
2298
+ import { TOKENS as TOKENS2 } from "@wrongstack/core/kernel";
2299
+ import { ToolRegistry } from "@wrongstack/core/registry";
2300
+ import { normalizeTokenSavingTier } from "@wrongstack/core/types";
2301
+ import { createDefaultContainer as createDefaultContainer2 } from "@wrongstack/runtime";
2302
+ import { registerBuiltinToolTier } from "@wrongstack/tools/tool-tier";
2303
+ async function dispatchBootSubcommand(input) {
2304
+ const {
2305
+ first,
2306
+ positional,
2307
+ flags,
2308
+ config,
2309
+ vault,
2310
+ wpaths,
2311
+ cwd,
2312
+ projectRoot,
2313
+ userHome,
2314
+ renderer,
2315
+ reader,
2316
+ logger,
2317
+ modelsRegistry
2318
+ } = input;
2319
+ const subcommandHandler = first ? subcommands[first] : void 0;
2320
+ if (first && subcommandHandler) {
2321
+ if (flags["help"] === true || flags["h"] === true) {
2322
+ const deepSub = positional[1];
2323
+ if (deepSub && renderDeepHelp(`${first}:${deepSub}`, renderer)) {
2324
+ await reader.close();
2325
+ return 0;
2326
+ }
2327
+ if (renderFocusedHelp(first, renderer)) {
2328
+ await reader.close();
2329
+ return 0;
2330
+ }
2331
+ }
2332
+ const container = createDefaultContainer2({
2333
+ config,
2334
+ wpaths,
2335
+ logger,
2336
+ modelsRegistry,
2337
+ bundledSkillsDir: config.features.skills ? resolveBundledSkillsDir2() : void 0,
2338
+ bundledPromptsDir: config.features.prompts === false ? void 0 : resolveBundledPromptsDir2()
2339
+ });
2340
+ const sessionStore = container.resolve(TOKENS2.SessionStore);
2341
+ const skillLoader = container.resolve(TOKENS2.SkillLoader);
2342
+ const toolRegistryForSubcmd = new ToolRegistry();
2343
+ registerBuiltinToolTier({
2344
+ registry: toolRegistryForSubcmd,
2345
+ tier: normalizeTokenSavingTier(config.features.tokenSavingMode)
2346
+ });
2347
+ const code = await subcommandHandler(positional.slice(1), {
2348
+ config,
2349
+ renderer,
2350
+ reader,
2351
+ sessionStore,
2352
+ skillLoader,
2353
+ toolRegistry: toolRegistryForSubcmd,
2354
+ modelsRegistry,
2355
+ paths: wpaths,
2356
+ vault,
2357
+ cwd,
2358
+ projectRoot,
2359
+ userHome,
2360
+ flags
2361
+ });
2362
+ await reader.close();
2363
+ return code;
2482
2364
  }
2483
- writeWarning(text) {
2484
- if (this.tuiActive) return;
2485
- this.err.write(`${theme2.warn("\u26A0")} ${text}
2486
- `);
2365
+ return void 0;
2366
+ }
2367
+
2368
+ // src/input-reader.ts
2369
+ import * as fs7 from "node:fs/promises";
2370
+ import * as path8 from "node:path";
2371
+ import * as readline from "node:readline";
2372
+ import {
2373
+ resolveWstackPaths,
2374
+ setOutputLineGuard,
2375
+ setRawMode as setRawMode2,
2376
+ writeOut
2377
+ } from "@wrongstack/core/utils";
2378
+ var ReadlineInputReader = class {
2379
+ rl;
2380
+ historyFile;
2381
+ history = [];
2382
+ pending = false;
2383
+ suppressNextHistory = false;
2384
+ constructor(opts = {}) {
2385
+ this.historyFile = opts.historyFile ?? resolveWstackPaths({ projectRoot: process.cwd() }).historyFile;
2487
2386
  }
2488
- writeError(text) {
2489
- if (this.tuiActive) return;
2490
- this.err.write(`${theme2.error("\u2718")} ${text}
2491
- `);
2387
+ async loadHistory() {
2388
+ try {
2389
+ const raw = await fs7.readFile(this.historyFile, "utf8");
2390
+ this.history = raw.split("\n").filter(Boolean).slice(-1e3);
2391
+ } catch {
2392
+ this.history = [];
2393
+ }
2492
2394
  }
2493
- writeInfo(text) {
2494
- if (this.tuiActive) return;
2495
- this.err.write(`${theme2.info("\u2139")} ${text}
2496
- `);
2395
+ async saveHistory() {
2396
+ try {
2397
+ await fs7.mkdir(path8.dirname(this.historyFile), { recursive: true });
2398
+ await fs7.writeFile(this.historyFile, this.history.slice(-1e3).join("\n"));
2399
+ } catch {
2400
+ }
2497
2401
  }
2498
- /**
2499
- * Write text to stderr verbatim: no glyph prefix, no markdown processing.
2500
- *
2501
- * Deliberately stricter than writeWarning/writeError/writeInfo, which emit
2502
- * *diagnostics* and are suppressed only when a TUI owns the terminal (so
2503
- * they survive silent mode). This method is for informational mirrors of
2504
- * stdout output, where the stdout copy already carries the full content —
2505
- * in silent mode the operator asked for machine-only output, so the mirror
2506
- * is dropped rather than leaked onto a quieter stream. It therefore honors
2507
- * the full {@link suppressStdout} gate.
2508
- *
2509
- * Used by `wstack mailbox serve` to route the human-readable startup banner
2510
- * away from stdout, keeping the mailbox_serve_started JSON line cleanly
2511
- * pipeable (`mailbox serve | jq`).
2512
- */
2513
- writeStderr(text) {
2514
- if (this.suppressStdout()) return;
2515
- this.err.write(text);
2402
+ shouldPersistHistory(line) {
2403
+ return !/^\s*\/(?:telegram-setup|tg-setup)\s+\d+:[A-Za-z0-9_-]+(?:\s|$)/i.test(line);
2516
2404
  }
2517
- clear() {
2518
- if (this.suppressStdout()) return;
2519
- this.out.write("\x1B[2J\x1B[H");
2520
- this.lineStart = true;
2405
+ ensure() {
2406
+ if (!this.rl) {
2407
+ this.rl = readline.createInterface({
2408
+ input: process.stdin,
2409
+ output: process.stdout,
2410
+ history: this.history,
2411
+ terminal: process.stdin.isTTY
2412
+ });
2413
+ }
2414
+ return this.rl;
2521
2415
  }
2522
- /**
2523
- * Write a flashy agent completion banner for delegate tool results.
2524
- * Renders a box like:
2525
- * ┌─────────────────────────────────────┐
2526
- * │ ✓ [role] done in 4m 32s │
2527
- * │ 127 iterations · 341 tools │
2528
- * │ Found 14 bugs across 6 files... │
2529
- * └─────────────────────────────────────┘
2530
- */
2531
- writeAgentSummary(summary, ok) {
2532
- if (this.suppressStdout()) return;
2533
- if (!this.lineStart) this.out.write("\n");
2534
- const lines = summary.split("\n");
2535
- const icon = ok ? theme2.success("\u2713") : theme2.error("\u2718");
2536
- const firstLine = `${icon} ${lines[0] ?? summary}`;
2537
- const body = lines.slice(1);
2538
- const maxWidth = Math.min(process.stdout.columns ?? 80, 120);
2539
- const contentWidth = Math.max(
2540
- firstLine.length,
2541
- body.reduce((a, l) => Math.max(a, l.length), 0)
2542
- );
2543
- const boxWidth = Math.min(Math.max(contentWidth + 4, 44), maxWidth);
2544
- const thick = "\u2501".repeat(boxWidth - 2);
2545
- const thin = "\u2500".repeat(boxWidth - 2);
2546
- this.out.write(`
2547
- ${theme2.primary("\u250C")}${thick}${theme2.primary("\u2510")}
2548
- `);
2549
- const centre = (s) => {
2550
- const inner = ` ${s} `;
2551
- const padLen = Math.max(0, boxWidth - 2 - s.length);
2552
- const left = Math.floor(padLen / 2);
2553
- const right = padLen - left;
2554
- return `${" ".repeat(left)}${inner}${" ".repeat(right)}`;
2555
- };
2556
- this.out.write(` ${theme2.primary("\u2502")}${centre(firstLine)}${theme2.primary("\u2502")}
2557
- `);
2558
- for (const l of body) {
2559
- this.out.write(
2560
- ` ${theme2.primary("\u2502")} ${l}${" ".repeat(Math.max(0, boxWidth - 3 - l.length))}${theme2.primary("\u2502")}
2561
- `
2562
- );
2416
+ async readLine(prompt, timeoutOpts) {
2417
+ if (this.history.length === 0) await this.loadHistory();
2418
+ while (this.pending) {
2419
+ await new Promise((resolve3) => setTimeout(resolve3, 50));
2563
2420
  }
2564
- this.out.write(` ${theme2.primary("\u2514")}${thin}${theme2.primary("\u2518")}
2421
+ this.pending = true;
2422
+ try {
2423
+ if (this.rl) {
2424
+ const old = this.rl;
2425
+ this.rl = void 0;
2426
+ await new Promise((resolve3) => {
2427
+ if (old.closed) {
2428
+ resolve3();
2429
+ } else {
2430
+ old.once("close", resolve3);
2431
+ old.close();
2432
+ }
2433
+ });
2434
+ }
2435
+ const fresh = this.ensure();
2436
+ this.installPromptGuard(fresh);
2437
+ const line = await new Promise((resolve3) => {
2438
+ let settled = false;
2439
+ let timeoutHandle;
2440
+ const settle = (line2) => {
2441
+ if (settled) return;
2442
+ settled = true;
2443
+ if (timeoutHandle) clearTimeout(timeoutHandle);
2444
+ setOutputLineGuard(null);
2445
+ resolve3(line2);
2446
+ };
2447
+ if (timeoutOpts) {
2448
+ timeoutHandle = setTimeout(() => {
2449
+ if (!settled) {
2450
+ fresh.write(`${timeoutOpts.defaultAnswer}
2565
2451
  `);
2566
- this.lineStart = true;
2452
+ }
2453
+ }, timeoutOpts.timeoutMs);
2454
+ }
2455
+ fresh.question(prompt ?? "> ", (line2) => {
2456
+ const persistHistory = !this.suppressNextHistory && this.shouldPersistHistory(line2);
2457
+ this.suppressNextHistory = false;
2458
+ if (line2.trim() && persistHistory) {
2459
+ this.history.push(line2);
2460
+ this.saveHistory().then(void 0, () => {
2461
+ });
2462
+ }
2463
+ settle(line2);
2464
+ });
2465
+ fresh.once("close", () => settle(""));
2466
+ fresh.on?.("error", (_e) => settle(""));
2467
+ }).then((result) => {
2468
+ if (this.rl === fresh) this.rl = void 0;
2469
+ fresh.close();
2470
+ return result;
2471
+ });
2472
+ return line;
2473
+ } finally {
2474
+ this.pending = false;
2475
+ }
2567
2476
  }
2568
2477
  /**
2569
- * Render subagent completion banners from a RunResult.
2570
- * Uses `delegateSummaries` when available (populated by delegate tool),
2571
- * otherwise falls back to scanning message history.
2478
+ * Install the out-of-band write guard for the active prompt. When a log
2479
+ * line or other async output lands while the user is mid-type, the guard
2480
+ * clears the draft row, lets the message print, then repaints the prompt
2481
+ * and the in-progress draft (cursor preserved) via readline's own
2482
+ * refresh. Without it, each async write leaves the half-typed line
2483
+ * stranded as a fresh scrollback row.
2484
+ *
2485
+ * No-op on non-TTY output (piped/redirected) — there's no draft to
2486
+ * protect and the ANSI clear/repaint would be noise in a file.
2572
2487
  */
2573
- writeDelegateSummaries(result) {
2574
- if (this.suppressStdout()) return;
2575
- if (result.delegateSummaries) {
2576
- for (const { summary, ok } of result.delegateSummaries) {
2577
- if (!summary) continue;
2578
- this.writeAgentSummary(summary, ok);
2579
- }
2488
+ installPromptGuard(rl) {
2489
+ const out = process.stdout;
2490
+ if (!out.isTTY) {
2491
+ setOutputLineGuard(null);
2580
2492
  return;
2581
2493
  }
2582
- if (!result.messages) return;
2583
- for (const msg of result.messages) {
2584
- const m = msg;
2585
- if (!Array.isArray(m.content)) continue;
2586
- for (const block of m.content) {
2587
- const b = block;
2588
- if (b.type !== "tool_result" || b.name !== "delegate") continue;
2589
- let obj;
2494
+ setOutputLineGuard({
2495
+ suspend() {
2496
+ readline.cursorTo(out, 0);
2497
+ readline.clearLine(out, 0);
2498
+ },
2499
+ resume() {
2590
2500
  try {
2591
- obj = typeof b.content === "string" ? JSON.parse(b.content) : b.content;
2501
+ rl.prompt(true);
2592
2502
  } catch {
2593
- continue;
2594
- }
2595
- const o = obj;
2596
- if (o.summary) {
2597
- this.writeAgentSummary(o.summary, o.ok ?? true);
2598
2503
  }
2599
2504
  }
2600
- }
2601
- }
2602
- };
2603
- function renderMarkdown(s) {
2604
- let out = s;
2605
- out = out.replace(/^(#{1,6}) (.+)$/gm, (_m, hashes, text) => {
2606
- return theme2.primary(theme2.bold(`${hashes} ${text}`));
2607
- });
2608
- out = out.replace(/```([a-zA-Z0-9_+-]*)\n([\s\S]*?)```/g, (_m, _lang, code) => {
2609
- return color10.gray(`
2610
- \u250C\u2500
2611
- ${code.replace(/^/gm, "\u2502 ")}\u2514\u2500`);
2612
- });
2613
- out = out.replace(/`([^`\n]+)`/g, (_m, code) => theme2.accent(code));
2614
- out = out.replace(/\*\*([^*]+)\*\*/g, (_m, text) => theme2.bold(text));
2615
- out = out.replace(
2616
- /(^|[^*])\*([^*\n]+)\*([^*]|$)/g,
2617
- (_m, l, t, r) => `${l}${color10.italic(t)}${r}`
2618
- );
2619
- return out;
2620
- }
2621
- function formatInputSummary(input) {
2622
- if (!input || typeof input !== "object") return "";
2623
- const obj = input;
2624
- if (typeof obj["path"] === "string") return obj["path"];
2625
- if (typeof obj["url"] === "string") return obj["url"];
2626
- if (typeof obj["command"] === "string") {
2627
- const cmd = obj["command"];
2628
- return cmd.length > 60 ? cmd.slice(0, 57) + "..." : cmd;
2629
- }
2630
- if (typeof obj["pattern"] === "string") return obj["pattern"];
2631
- return "";
2632
- }
2633
- function safeStringify(value) {
2634
- if (typeof value === "string") return value;
2635
- try {
2636
- return JSON.stringify(value);
2637
- } catch {
2638
- return String(value);
2639
- }
2640
- }
2641
- function extractBodyText(value, name) {
2642
- let v = value;
2643
- if (typeof value === "string") {
2644
- const trimmed = value.trimStart();
2645
- if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
2646
- try {
2647
- v = JSON.parse(value);
2648
- } catch {
2649
- return value;
2650
- }
2651
- } else {
2652
- return value;
2653
- }
2654
- }
2655
- if (typeof v !== "object" || v === null) {
2656
- return safeStringify(value);
2657
- }
2658
- const o = v;
2659
- if (name === "read" && typeof o["text"] === "string") return o["text"];
2660
- if (name === "bash") {
2661
- const output = typeof o["output"] === "string" ? o["output"] : "";
2662
- const error = typeof o["error"] === "string" ? o["error"] : "";
2663
- if (output && error) return `${output}
2664
- ${error}`;
2665
- return output || error || "";
2666
- }
2667
- if (name === "exec") {
2668
- const stdout = typeof o["stdout"] === "string" ? o["stdout"] : "";
2669
- const stderr = typeof o["stderr"] === "string" ? o["stderr"] : "";
2670
- if (stdout && stderr) return `${stdout}
2671
- ${stderr}`;
2672
- return stdout || stderr || "";
2673
- }
2674
- return safeStringify(value);
2675
- }
2676
- function extractDiff(value) {
2677
- if (typeof value === "object" && value !== null) {
2678
- const d = value.diff;
2679
- if (typeof d === "string" && d.length > 0) return d;
2505
+ });
2680
2506
  }
2681
- if (typeof value === "string") {
2682
- const trimmed = value.trimStart();
2683
- if (trimmed.startsWith("{")) {
2684
- try {
2685
- const parsed = JSON.parse(value);
2686
- if (typeof parsed.diff === "string" && parsed.diff.length > 0) {
2687
- return parsed.diff;
2507
+ async readKey(prompt, options, opts = {}) {
2508
+ setOutputLineGuard(null);
2509
+ const signal = opts.signal;
2510
+ if (signal?.aborted) return "";
2511
+ writeOut(prompt);
2512
+ return new Promise((resolve3) => {
2513
+ const stdin = process.stdin;
2514
+ const wasRaw = stdin.isRaw;
2515
+ const wasPaused = stdin.isPaused();
2516
+ setRawMode2(stdin, true);
2517
+ stdin.resume();
2518
+ const onData = (buf) => {
2519
+ const key = buf.toString();
2520
+ if (key === "") {
2521
+ cleanup();
2522
+ writeOut("\n");
2523
+ resolve3("");
2524
+ return;
2688
2525
  }
2689
- } catch {
2690
- }
2691
- }
2692
- if (/^---[^\n]*\n\+\+\+/m.test(value)) return value;
2526
+ const opt = options.find(
2527
+ (o) => o.key.toLowerCase() === key.toLowerCase() || o.value === key
2528
+ );
2529
+ if (opt) {
2530
+ cleanup();
2531
+ writeOut(`${opt.key}
2532
+ `);
2533
+ resolve3(opt.value);
2534
+ }
2535
+ };
2536
+ const onClose = () => {
2537
+ cleanup();
2538
+ resolve3("");
2539
+ };
2540
+ const onAbort = () => {
2541
+ cleanup();
2542
+ writeOut("\n");
2543
+ resolve3("");
2544
+ };
2545
+ const cleanup = () => {
2546
+ stdin.off("data", onData);
2547
+ stdin.off("close", onClose);
2548
+ signal?.removeEventListener("abort", onAbort);
2549
+ setRawMode2(stdin, wasRaw);
2550
+ if (wasPaused) stdin.pause();
2551
+ };
2552
+ stdin.on("data", onData);
2553
+ stdin.on("close", onClose);
2554
+ signal?.addEventListener("abort", onAbort, { once: true });
2555
+ });
2693
2556
  }
2694
- return null;
2695
- }
2696
- function summarize(value, name) {
2697
- let v = value;
2698
- if (typeof value === "string") {
2699
- const trimmed = value.trimStart();
2700
- if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
2557
+ /**
2558
+ * Read a line of input while masking each character with a bullet so the
2559
+ * user gets visual confirmation that bytes are arriving (especially on
2560
+ * paste, which previously felt like nothing happened). Pasted chunks
2561
+ * are echoed as a run of bullets, Backspace/DEL erases one bullet, and
2562
+ * Ctrl+U / Ctrl+T are honored. Non-TTY input is read normally — there's
2563
+ * nothing to hide when piped, and echoing bullets to a file is noise.
2564
+ *
2565
+ * Returns the raw entered string (no trim — caller decides).
2566
+ */
2567
+ async readSecret(prompt) {
2568
+ const stdin = process.stdin;
2569
+ if (!stdin.isTTY) {
2570
+ this.suppressNextHistory = true;
2571
+ return this.readLine(prompt);
2572
+ }
2573
+ setOutputLineGuard(null);
2574
+ this.rl?.close();
2575
+ this.rl = void 0;
2576
+ writeOut(prompt);
2577
+ return new Promise((resolve3) => {
2578
+ let buf = "";
2579
+ const wasRaw = stdin.isRaw;
2580
+ const cleanup = () => {
2581
+ stdin.off("data", onData);
2582
+ setRawMode2(stdin, wasRaw);
2583
+ stdin.pause();
2584
+ };
2701
2585
  try {
2702
- v = JSON.parse(value);
2703
- } catch {
2586
+ setRawMode2(stdin, true);
2587
+ stdin.resume();
2588
+ stdin.setEncoding("utf8");
2589
+ } catch (setupErr) {
2590
+ setRawMode2(stdin, wasRaw);
2591
+ stdin.pause();
2592
+ throw setupErr;
2704
2593
  }
2705
- }
2594
+ const eraseChar = () => {
2595
+ writeOut("\b \b");
2596
+ };
2597
+ const eraseAll = () => {
2598
+ for (let i = 0; i < buf.length; i++) eraseChar();
2599
+ };
2600
+ let escState = 0;
2601
+ const onData = (chunk) => {
2602
+ for (const ch of chunk) {
2603
+ if (escState === 1) {
2604
+ escState = ch === "[" ? 2 : ch === "O" ? 3 : 0;
2605
+ continue;
2606
+ }
2607
+ if (escState === 2) {
2608
+ if (ch >= "@" && ch <= "~") escState = 0;
2609
+ continue;
2610
+ }
2611
+ if (escState === 3) {
2612
+ escState = 0;
2613
+ continue;
2614
+ }
2615
+ if (ch === "\x1B") {
2616
+ escState = 1;
2617
+ continue;
2618
+ }
2619
+ if (ch === "\r" || ch === "\n") {
2620
+ cleanup();
2621
+ writeOut(` ${dim(`[${buf.length} chars]`)}
2622
+ `);
2623
+ resolve3(buf);
2624
+ return;
2625
+ }
2626
+ if (ch === "") {
2627
+ cleanup();
2628
+ writeOut("\n");
2629
+ resolve3("");
2630
+ return;
2631
+ }
2632
+ if (ch === "") {
2633
+ eraseAll();
2634
+ buf = "";
2635
+ continue;
2636
+ }
2637
+ if (ch === "") {
2638
+ const m = buf.match(/(\S+\s*)$/);
2639
+ const drop = m ? m[0].length : buf.length;
2640
+ for (let i = 0; i < drop; i++) eraseChar();
2641
+ buf = buf.slice(0, buf.length - drop);
2642
+ continue;
2643
+ }
2644
+ if (ch === "\x7F" || ch === "\b") {
2645
+ if (buf.length > 0) {
2646
+ buf = buf.slice(0, -1);
2647
+ eraseChar();
2648
+ }
2649
+ continue;
2650
+ }
2651
+ if (ch < " ") continue;
2652
+ buf += ch;
2653
+ writeOut("\u2022");
2654
+ }
2655
+ if (escState === 1) escState = 0;
2656
+ };
2657
+ stdin.on("data", onData);
2658
+ });
2706
2659
  }
2707
- if (typeof v === "object" && v !== null) {
2708
- const o = v;
2709
- if (name === "edit") {
2710
- const path10 = typeof o["path"] === "string" ? o["path"] : "";
2711
- const reps = typeof o["replacements"] === "number" ? o["replacements"] : 0;
2712
- return `${path10} ${reps} replacement${reps === 1 ? "" : "s"}`.trim();
2713
- }
2714
- if (name === "write") {
2715
- const path10 = typeof o["path"] === "string" ? o["path"] : "";
2716
- const bytes = typeof o["bytes"] === "number" ? o["bytes"] : void 0;
2717
- return bytes !== void 0 ? `${path10} ${bytes}B` : path10;
2718
- }
2719
- if (name === "read") {
2720
- return summarizeRead(o);
2721
- }
2722
- if (name === "bash") {
2723
- return summarizeBash(o);
2724
- }
2725
- if (name === "exec") {
2726
- return summarizeExec(o);
2727
- }
2728
- if (typeof o["count"] === "number") {
2729
- return `${o["count"]} match${o["count"] === 1 ? "" : "es"}`;
2730
- }
2660
+ async close() {
2661
+ setOutputLineGuard(null);
2662
+ await this.saveHistory();
2663
+ this.rl?.close();
2664
+ this.rl = void 0;
2731
2665
  }
2732
- return "";
2733
- }
2734
- function summarizeRead(o) {
2735
- const path10 = typeof o["path"] === "string" ? o["path"] : "";
2736
- const total = typeof o["total_lines"] === "number" ? o["total_lines"] : void 0;
2737
- const truncated = o["truncated"] === true;
2738
- const cached = o["cached"] === true;
2739
- const note = typeof o["note"] === "string" ? o["note"] : void 0;
2740
- const parts = [];
2741
- if (path10) parts.push(path10);
2742
- if (total !== void 0) parts.push(`${total} line${total === 1 ? "" : "s"}`);
2743
- const flags = [];
2744
- if (truncated) flags.push("truncated");
2745
- if (cached) flags.push("cached");
2746
- if (flags.length > 0) parts.push(`(${flags.join(", ")})`);
2747
- let summary = parts.join(" ").trim();
2748
- if (note) summary = summary ? `${summary} \u2014 ${note}` : note;
2749
- return summary;
2750
- }
2751
- function summarizeBash(o) {
2752
- const exit = typeof o["exit_code"] === "number" ? o["exit_code"] : void 0;
2753
- const output = typeof o["output"] === "string" ? o["output"] : "";
2754
- const lines = output ? output.split("\n").length : 0;
2755
- const parts = [];
2756
- if (exit !== void 0) parts.push(`exit=${exit}`);
2757
- else if (o["exit_code"] === null) parts.push("backgrounded");
2758
- if (o["timed_out"] === true) parts.push("timed out");
2759
- if (lines > 0) parts.push(`${lines} output line${lines === 1 ? "" : "s"}`);
2760
- if (output.length > 0) parts.push(`${output.length}B`);
2761
- return parts.join(" ");
2762
- }
2763
- function summarizeExec(o) {
2764
- const command = typeof o["command"] === "string" ? o["command"] : "";
2765
- const args = Array.isArray(o["args"]) ? o["args"].filter((a) => typeof a === "string") : [];
2766
- const exit = typeof o["exitCode"] === "number" ? o["exitCode"] : void 0;
2767
- const stdout = typeof o["stdout"] === "string" ? o["stdout"] : "";
2768
- const stderr = typeof o["stderr"] === "string" ? o["stderr"] : "";
2769
- const outLines = stdout ? stdout.split("\n").length : 0;
2770
- const errLines = stderr ? stderr.split("\n").length : 0;
2771
- const totalBytes = stdout.length + stderr.length;
2772
- const parts = [];
2773
- const invocation = [command, ...args].filter(Boolean).join(" ");
2774
- if (invocation) parts.push(`$ ${invocation}`);
2775
- if (exit !== void 0) parts.push(`exit=${exit}`);
2776
- if (outLines > 0) parts.push(`${outLines} stdout line${outLines === 1 ? "" : "s"}`);
2777
- if (errLines > 0) parts.push(`${errLines} stderr line${errLines === 1 ? "" : "s"}`);
2778
- if (totalBytes > 0) parts.push(`${totalBytes}B`);
2779
- return parts.join(" ");
2780
- }
2781
- function compactSingleLine(text) {
2782
- const first = text.split("\n")[0]?.trim() ?? "";
2783
- if (!first) return "(no output)";
2784
- return first.length > 200 ? `${first.slice(0, 197)}\u2026` : first;
2666
+ };
2667
+ function dim(s) {
2668
+ if (!process.stdout.isTTY) return s;
2669
+ return `\x1B[2m${s}\x1B[22m`;
2785
2670
  }
2786
2671
 
2787
- // src/boot.ts
2788
- var GITHUB_PROVIDERS_OVERLAY_URL = "https://raw.githubusercontent.com/WrongStack/WrongStack/main/packages/cli/data/providers.json";
2789
- async function boot(argv) {
2790
- const { flags, positional } = parseArgs(argv);
2791
- if (positional[0] === "sandbox") {
2792
- const { runSandboxCommand } = await import("./sandbox-TYGD6FI2.js");
2793
- return runSandboxCommand(positional.slice(1), flags, new TerminalRenderer());
2672
+ // src/renderer.ts
2673
+ import { color as color12, sanitizeTerminalText } from "@wrongstack/core/utils";
2674
+ var TerminalRenderer = class _TerminalRenderer {
2675
+ out;
2676
+ err;
2677
+ lineStart = true;
2678
+ /** When true, stdout-bound rendering is disabled (for non-terminal UI surfaces). */
2679
+ silent = false;
2680
+ /**
2681
+ * When true, Ink exclusively owns the terminal. Both stdout and stderr
2682
+ * rendering must be disabled: either stream can interleave with Ink's
2683
+ * cursor updates and make the TUI flicker or leave stray debug notes.
2684
+ */
2685
+ tuiActive = false;
2686
+ /**
2687
+ * Per-tool on-screen result render mode. Defaults to `'extend'` so
2688
+ * existing callers without `setResultRenderMode` see full content.
2689
+ * `setResultRenderMode(name, mode)` overrides the entry for `name`; the
2690
+ * override is one-shot for that name (the default reasserts after the
2691
+ * next write) so callers can set the mode right before each tool call
2692
+ * without leaking state across unrelated tool calls.
2693
+ */
2694
+ renderModes = /* @__PURE__ */ new Map();
2695
+ /** Mode applied when no entry is set for a given tool name. */
2696
+ static DEFAULT_RENDER_MODE = "extend";
2697
+ /** Maximum number of one-shot hints retained when callers abandon a result. */
2698
+ static MAX_RENDER_MODES = 256;
2699
+ /** Lines kept per tool family in `extend` mode. */
2700
+ static EXTEND_PREVIEW_LINES = 10;
2701
+ constructor(opts = {}) {
2702
+ this.out = opts.out ?? process.stdout;
2703
+ this.err = opts.err ?? process.stderr;
2794
2704
  }
2795
- if (positional[0] === "update") {
2796
- const renderer2 = new TerminalRenderer();
2797
- return runUpdateCommand(positional.slice(1), {
2798
- cwd: process.cwd(),
2799
- userHome: os.homedir(),
2800
- renderer: renderer2,
2801
- flags
2802
- });
2705
+ /**
2706
+ * Toggle stdout suppression for non-terminal UI surfaces such as WebUI.
2707
+ * Diagnostics continue to flow to stderr. Idempotent.
2708
+ */
2709
+ setSilent(silent) {
2710
+ this.silent = silent;
2803
2711
  }
2804
- if (positional[0] === "remote" || typeof flags["remote"] === "string") {
2805
- const { runRemoteCommand } = await import("./remote-C74BCNND.js");
2806
- return runRemoteCommand(
2807
- positional[0] === "remote" ? positional.slice(1) : positional,
2808
- flags,
2809
- new TerminalRenderer()
2810
- );
2712
+ isSilent() {
2713
+ return this.silent;
2811
2714
  }
2812
- if (positional[0] === "resume" && positional[1] && !subcommands["__noop_resume_marker"]) {
2813
- flags["resume"] = positional[1];
2814
- positional.splice(0, 2);
2715
+ /**
2716
+ * Toggle exclusive TUI ownership of both terminal streams. Unlike
2717
+ * {@link setSilent}, TUI mode suppresses stderr as well as stdout and clears
2718
+ * any CLI-only result mode staged before Ink took ownership. While active,
2719
+ * new result-mode hints are ignored. The two flags compose with OR semantics
2720
+ * for stdout suppression only; TUI ownership adds the other behaviors.
2721
+ * Enable immediately before starting Ink and disable in the paired
2722
+ * `finally`, only after the TUI has fully exited.
2723
+ */
2724
+ setTuiActive(active) {
2725
+ if (active) this.renderModes.clear();
2726
+ this.tuiActive = active;
2815
2727
  }
2816
- let bootResult;
2817
- try {
2818
- bootResult = await bootConfig(flags);
2819
- } catch (err) {
2820
- writeErr(`Config error: ${toErrorMessage5(err)}
2821
- `);
2822
- return 2;
2728
+ isTuiActive() {
2729
+ return this.tuiActive;
2823
2730
  }
2824
- let { paths, config, vault } = bootResult;
2825
- try {
2826
- resolveToolRestriction(flags);
2827
- resolveLaunchAllowedTools(flags);
2828
- parseOutputFormat(flags["output-format"]);
2829
- announceSafeMode(flags, writeErr);
2830
- activateRestrictedMode(flags, writeErr);
2831
- const launchMcp = await resolveLaunchMcpServers(flags, paths.cwd);
2832
- if (launchMcp) flags["mcp-config"] = JSON.stringify({ mcpServers: launchMcp });
2833
- const schema = await resolveJsonSchemaFlag(flags["json-schema"], paths.cwd);
2834
- if (schema) flags["json-schema"] = JSON.stringify(schema);
2835
- const appended = await resolveAppendedSystemPrompt(flags, paths.cwd);
2836
- delete flags["append-system-prompt-file"];
2837
- if (appended) flags["append-system-prompt"] = appended;
2838
- else delete flags["append-system-prompt"];
2839
- } catch (err) {
2840
- writeErr(`wstack: ${toErrorMessage5(err)}
2841
- `);
2842
- return 2;
2731
+ suppressStdout() {
2732
+ return this.silent || this.tuiActive;
2843
2733
  }
2844
- let first = positional[0];
2845
- const mayOfferConfigRecovery = isStdinTTY2() && (positional.length === 0 || first === "quick") && typeof flags["prompt"] !== "string" && !flags["webui"] && !flags["no-interactive"] && !flags["skip"];
2846
- const machineOutput = flags["output-json"] === true || flags["output-format"] === "json" || flags["output-format"] === "stream-json";
2847
- const renderer = new TerminalRenderer(machineOutput ? { out: process.stderr } : void 0);
2848
- const reader = new ReadlineInputReader({ historyFile: paths.wpaths.historyFile });
2849
- if (mayOfferConfigRecovery) {
2850
- const recoveryProfile = config.activeProfile ?? "default";
2851
- const restored = await maybeRestoreDefaultProfileFromBackup({
2852
- globalRoot: paths.wpaths.globalRoot,
2853
- profilePath: paths.wpaths.profileConfig(recoveryProfile),
2854
- profileName: recoveryProfile,
2855
- renderer,
2856
- reader
2857
- });
2858
- if (restored) {
2859
- try {
2860
- bootResult = await bootConfig(flags);
2861
- ({ paths, config, vault } = bootResult);
2862
- } catch (err) {
2863
- writeErr(`Config error after backup restore: ${toErrorMessage5(err)}
2864
- `);
2865
- await reader.close();
2866
- return 2;
2867
- }
2734
+ setResultRenderMode(name, mode) {
2735
+ if (this.suppressStdout()) return;
2736
+ if (this.renderModes.delete(name)) {
2737
+ this.renderModes.set(name, mode);
2738
+ return;
2868
2739
  }
2740
+ if (this.renderModes.size >= _TerminalRenderer.MAX_RENDER_MODES) {
2741
+ const oldestName = this.renderModes.keys().next().value;
2742
+ if (oldestName !== void 0) this.renderModes.delete(oldestName);
2743
+ }
2744
+ this.renderModes.set(name, mode);
2869
2745
  }
2870
- config = applySimpleUiFullAutoProfile(config, flags);
2871
- const simpleUiFullAuto = isSimpleUiFullAuto(flags);
2872
- const { cwd, projectRoot, userHome, wpaths, pathResolver } = paths;
2873
- const profileConfigPath = activeProfileConfigPath(wpaths, config);
2874
- void pathResolver;
2875
- if (first === "quick") {
2876
- flags["quick"] = true;
2877
- flags["tui"] = true;
2878
- positional.splice(0, 1);
2879
- first = void 0;
2880
- const plugins = config?.plugins ?? [];
2881
- if (plugins.length === 0) {
2882
- console.debug("[wrongstack:quick] No plugins configured");
2883
- } else {
2884
- for (const p of plugins) {
2885
- const name = typeof p === "string" ? p : p.name;
2886
- const enabled = typeof p === "object" && p.enabled === false ? " (disabled)" : "";
2887
- console.debug(`[wrongstack:quick] plugin: ${name}${enabled}`);
2888
- }
2746
+ getRenderMode(name) {
2747
+ return this.renderModes.get(name) ?? _TerminalRenderer.DEFAULT_RENDER_MODE;
2748
+ }
2749
+ clearRenderMode(name) {
2750
+ this.renderModes.delete(name);
2751
+ }
2752
+ write(input) {
2753
+ if (this.suppressStdout()) return;
2754
+ const text = typeof input === "string" ? input : input.text;
2755
+ if (!text) return;
2756
+ const rendered = renderMarkdown(text);
2757
+ this.out.write(rendered);
2758
+ this.lineStart = rendered.endsWith("\n");
2759
+ }
2760
+ writeLine(text = "") {
2761
+ if (this.suppressStdout()) return;
2762
+ if (!this.lineStart) this.out.write("\n");
2763
+ if (text) this.out.write(`${text}
2764
+ `);
2765
+ else this.out.write("\n");
2766
+ this.lineStart = true;
2767
+ }
2768
+ writeBlock(block) {
2769
+ if (block.type === "tool_result") {
2770
+ const text = typeof block.content === "string" ? block.content : JSON.stringify(block.content);
2771
+ this.writeToolResult(block.name ?? "result", text, !!block.is_error);
2772
+ return;
2773
+ }
2774
+ if (this.suppressStdout()) return;
2775
+ if (block.type === "text") {
2776
+ this.write(block);
2777
+ } else if (block.type === "tool_use") {
2778
+ this.writeToolCall(block.name, block.input);
2889
2779
  }
2890
2780
  }
2891
- const logger = new DefaultLogger({
2892
- level: config.log.level,
2893
- file: wpaths.logFile,
2894
- // Suppress stderr output in TUI mode: plugin/library log messages
2895
- // (e.g. Telegram "getUpdates failed") write directly to stderr and
2896
- // bypass Ink, which breaks the Static/live boundary.
2897
- // Logs still go to the disk file for post-hoc debugging.
2898
- stderr: !flags.tui
2899
- });
2900
- const modelsRegistry = new DefaultModelsRegistry({
2901
- cacheFile: wpaths.modelsCache,
2902
- // Force a refresh attempt once per CLI process. Model metadata changes faster
2903
- // than releases (new model ids, corrected context windows), and stale cache
2904
- // here directly affects runtime behavior like context bars and compaction.
2905
- // If the network is unavailable, DefaultModelsRegistry still falls back to
2906
- // stale cache or the bundled overlay instead of failing startup.
2907
- ttlSeconds: 0,
2908
- // Curated overlay merged on top of models.dev: fetched from GitHub raw for
2909
- // freshness, with the bundled file as the offline floor.
2910
- overlayUrl: GITHUB_PROVIDERS_OVERLAY_URL,
2911
- overlayFile: resolveBundledOverlayFile(),
2912
- overlayCacheFile: wpaths.modelsOverlayCache
2913
- });
2914
- installProviderPersisters({ config, paths: wpaths, vault, logger });
2915
- const subcommandHandler = first ? subcommands[first] : void 0;
2916
- if (first && subcommandHandler) {
2917
- if (flags["help"] === true || flags["h"] === true) {
2918
- const deepSub = positional[1];
2919
- if (deepSub && renderDeepHelp(`${first}:${deepSub}`, renderer)) {
2920
- await reader.close();
2921
- return 0;
2922
- }
2923
- if (renderFocusedHelp(first, renderer)) {
2924
- await reader.close();
2925
- return 0;
2926
- }
2781
+ writeToolCall(name, input) {
2782
+ if (this.suppressStdout()) return;
2783
+ if (!this.lineStart) this.out.write("\n");
2784
+ const arrow = theme2.primary("\u2192");
2785
+ const display = sanitizeTerminalText(formatInputSummary(input));
2786
+ this.out.write(
2787
+ `${arrow} ${theme2.bold(sanitizeTerminalText(name))}${display ? color12.dim(` ${display}`) : ""}
2788
+ `
2789
+ );
2790
+ this.lineStart = true;
2791
+ }
2792
+ writeToolResult(name, content, isError) {
2793
+ if (this.suppressStdout()) {
2794
+ this.clearRenderMode(name);
2795
+ return;
2796
+ }
2797
+ const txt = sanitizeTerminalText(
2798
+ typeof content === "string" ? content : safeStringify(content)
2799
+ );
2800
+ const prefix = isError ? theme2.error("\u2718") : theme2.success("\u2713");
2801
+ const renderMode = this.getRenderMode(name);
2802
+ this.clearRenderMode(name);
2803
+ const bodyText = sanitizeTerminalText(extractBodyText(content, name));
2804
+ if (isError) {
2805
+ const firstLine = txt.split("\n")[0] ?? "";
2806
+ const truncated = firstLine.length > 200 ? `${firstLine.slice(0, 197)}\u2026` : firstLine;
2807
+ this.out.write(` ${prefix} ${color12.dim(truncated)}
2808
+ `);
2809
+ this.lineStart = true;
2810
+ return;
2811
+ }
2812
+ const isEditLike = name === "edit" || name === "write";
2813
+ const diff = extractDiff(content);
2814
+ if (isEditLike && diff) {
2815
+ this.out.write(` ${prefix} ${color12.dim(sanitizeTerminalText(summarize(content, name)))}
2816
+ `);
2817
+ const rendered = renderDiff(diff).split("\n").map((l) => ` ${l}`).join("\n");
2818
+ this.out.write(`${rendered}
2819
+ `);
2820
+ this.lineStart = true;
2821
+ return;
2822
+ }
2823
+ const summary = sanitizeTerminalText(summarize(content, name));
2824
+ const isReadLike = name === "read" || name === "grep" || name === "glob" || name === "bash" || name === "exec";
2825
+ if (renderMode === "simple") {
2826
+ this.out.write(` ${prefix} ${color12.dim(summary || compactSingleLine(bodyText))}
2827
+ `);
2828
+ this.lineStart = true;
2829
+ return;
2830
+ }
2831
+ const previewLines = isEditLike ? 0 : isReadLike ? _TerminalRenderer.EXTEND_PREVIEW_LINES : 2;
2832
+ const lines = bodyText.split("\n");
2833
+ const head = lines.slice(0, previewLines).map((l) => l.replace(/\s+$/, ""));
2834
+ const moreCount = Math.max(0, lines.length - head.length);
2835
+ this.out.write(` ${prefix} ${color12.dim(summary)}
2836
+ `);
2837
+ for (const l of head) {
2838
+ const capped = l.length > 200 ? `${l.slice(0, 197)}\u2026` : l;
2839
+ this.out.write(` ${color12.dim(capped)}
2840
+ `);
2841
+ }
2842
+ if (moreCount > 0) {
2843
+ this.out.write(
2844
+ ` ${color12.dim(`+${moreCount} more line${moreCount === 1 ? "" : "s"}`)}
2845
+ `
2846
+ );
2927
2847
  }
2928
- const container = createDefaultContainer2({
2929
- config,
2930
- wpaths,
2931
- logger,
2932
- modelsRegistry,
2933
- bundledSkillsDir: config.features.skills ? resolveBundledSkillsDir2() : void 0,
2934
- bundledPromptsDir: config.features.prompts === false ? void 0 : resolveBundledPromptsDir2()
2935
- });
2936
- const sessionStore = container.resolve(TOKENS2.SessionStore);
2937
- const skillLoader = container.resolve(TOKENS2.SkillLoader);
2938
- const toolRegistryForSubcmd = new ToolRegistry();
2939
- registerBuiltinToolTier({
2940
- registry: toolRegistryForSubcmd,
2941
- tier: normalizeTokenSavingTier(config.features.tokenSavingMode)
2942
- });
2943
- const code = await subcommandHandler(positional.slice(1), {
2944
- config,
2945
- renderer,
2946
- reader,
2947
- sessionStore,
2948
- skillLoader,
2949
- toolRegistry: toolRegistryForSubcmd,
2950
- modelsRegistry,
2951
- paths: wpaths,
2952
- vault,
2953
- cwd,
2954
- projectRoot,
2955
- userHome,
2956
- flags
2957
- });
2958
- await reader.close();
2959
- return code;
2848
+ this.lineStart = true;
2960
2849
  }
2961
- if (isHomeDirectory(cwd, userHome)) {
2962
- writeErr(
2963
- `
2964
- ${color11.red(color11.bold("\u26A0 This is not a working directory."))}
2965
- ${color11.red("Please open wstack in a project folder.")}
2966
-
2967
- `
2968
- );
2969
- await reader.close();
2970
- return 1;
2850
+ writeDiff(diff) {
2851
+ if (this.suppressStdout()) return;
2852
+ if (!this.lineStart) this.out.write("\n");
2853
+ this.out.write(`${renderDiff(diff)}
2854
+ `);
2855
+ this.lineStart = true;
2971
2856
  }
2972
- if (!flags["no-models-refresh"]) {
2973
- await startCatalog({
2974
- registry: modelsRegistry,
2975
- logger,
2976
- shortLived: positional.length > 0 || typeof flags["prompt"] === "string"
2977
- });
2857
+ writeWarning(text) {
2858
+ if (this.tuiActive) return;
2859
+ this.err.write(`${theme2.warn("\u26A0")} ${text}
2860
+ `);
2978
2861
  }
2979
- try {
2980
- await discoverAndMergeProviders({
2981
- config,
2982
- registry: modelsRegistry,
2983
- cacheDir: path9.dirname(wpaths.modelsCache),
2984
- logger
2985
- });
2986
- } catch (err) {
2987
- logger.debug(`provider auto-discovery skipped: ${toErrorMessage5(err)}`);
2862
+ writeError(text) {
2863
+ if (this.tuiActive) return;
2864
+ this.err.write(`${theme2.error("\u2718")} ${text}
2865
+ `);
2988
2866
  }
2989
- const isSingleShot = positional.length > 0 || typeof flags["prompt"] === "string";
2990
- const isInteractiveTTY = isStdinTTY2() && !isSingleShot && !flags["webui"] && !flags["no-interactive"] && !flags["skip"];
2991
- if (isInteractiveTTY) {
2992
- await checkGitInCwd({ cwd, renderer, reader });
2993
- const cont = await runProjectCheck({ projectRoot, cwd, renderer, reader });
2994
- if (!cont) {
2995
- await reader.close();
2996
- return 0;
2997
- }
2867
+ writeInfo(text) {
2868
+ if (this.tuiActive) return;
2869
+ this.err.write(`${theme2.info("\u2139")} ${text}
2870
+ `);
2998
2871
  }
2999
- const wantsTui = flags["tui"] === true && flags["no-tui"] !== true;
3000
- if (wantsTui && (!process.stdout.isTTY || !process.stdin.isTTY)) {
3001
- writeErr(
3002
- "wstack: --tui requires an interactive terminal on both stdin and stdout.\n Drop the flag (use the plain REPL) or run wstack directly without piping.\n"
3003
- );
3004
- await reader.close();
3005
- return 2;
2872
+ /**
2873
+ * Write text to stderr verbatim: no glyph prefix, no markdown processing.
2874
+ *
2875
+ * Deliberately stricter than writeWarning/writeError/writeInfo, which emit
2876
+ * *diagnostics* and are suppressed only when a TUI owns the terminal (so
2877
+ * they survive silent mode). This method is for informational mirrors of
2878
+ * stdout output, where the stdout copy already carries the full content —
2879
+ * in silent mode the operator asked for machine-only output, so the mirror
2880
+ * is dropped rather than leaked onto a quieter stream. It therefore honors
2881
+ * the full {@link suppressStdout} gate.
2882
+ *
2883
+ * Used by `wstack mailbox serve` to route the human-readable startup banner
2884
+ * away from stdout, keeping the mailbox_serve_started JSON line cleanly
2885
+ * pipeable (`mailbox serve | jq`).
2886
+ */
2887
+ writeStderr(text) {
2888
+ if (this.suppressStdout()) return;
2889
+ this.err.write(text);
3006
2890
  }
3007
- const providerFlag = typeof flags["provider"] === "string" ? flags["provider"] : void 0;
3008
- const modelFlag = typeof flags["model"] === "string" ? flags["model"] : void 0;
3009
- const noInteractiveMode = flags["webui"] || flags["no-interactive"];
3010
- if (noInteractiveMode && (!config.provider || !config.model)) {
3011
- if (providerFlag && modelFlag) {
3012
- config = patchConfig(config, { provider: providerFlag, model: modelFlag });
3013
- } else {
3014
- const picked = await autoSelectSavedProvider(config, modelsRegistry);
3015
- if (picked) config = patchConfig(config, picked);
3016
- }
2891
+ clear() {
2892
+ if (this.suppressStdout()) return;
2893
+ this.out.write("\x1B[2J\x1B[H");
2894
+ this.lineStart = true;
3017
2895
  }
3018
- if (!(!!providerFlag && !!modelFlag)) {
3019
- if (isStdinTTY2() && !noInteractiveMode) {
3020
- let picked;
3021
- let skipPicker = false;
3022
- if (isSetupProvider2(config.provider)) {
3023
- skipPicker = true;
3024
- renderer.write(
3025
- `
3026
- ${color11.amber("\u25B6")} ${color11.bold("Setup mode")} ${color11.dim("\u2014 no model connected. Run")} ${color11.bold("/auth")} ${color11.dim("to connect one.")}
3027
-
2896
+ /**
2897
+ * Write a flashy agent completion banner for delegate tool results.
2898
+ * Renders a box like:
2899
+ * ┌─────────────────────────────────────┐
2900
+ * │ ✓ [role] done in 4m 32s │
2901
+ * │ 127 iterations · 341 tools │
2902
+ * │ Found 14 bugs across 6 files... │
2903
+ * └─────────────────────────────────────┘
2904
+ */
2905
+ writeAgentSummary(summary, ok) {
2906
+ if (this.suppressStdout()) return;
2907
+ if (!this.lineStart) this.out.write("\n");
2908
+ const lines = summary.split("\n");
2909
+ const icon = ok ? theme2.success("\u2713") : theme2.error("\u2718");
2910
+ const firstLine = `${icon} ${lines[0] ?? summary}`;
2911
+ const body = lines.slice(1);
2912
+ const maxWidth = Math.min(process.stdout.columns ?? 80, 120);
2913
+ const contentWidth = Math.max(
2914
+ firstLine.length,
2915
+ body.reduce((a, l) => Math.max(a, l.length), 0)
2916
+ );
2917
+ const boxWidth = Math.min(Math.max(contentWidth + 4, 44), maxWidth);
2918
+ const thick = "\u2501".repeat(boxWidth - 2);
2919
+ const thin = "\u2500".repeat(boxWidth - 2);
2920
+ this.out.write(`
2921
+ ${theme2.primary("\u250C")}${thick}${theme2.primary("\u2510")}
2922
+ `);
2923
+ const centre = (s) => {
2924
+ const inner = ` ${s} `;
2925
+ const padLen = Math.max(0, boxWidth - 2 - s.length);
2926
+ const left = Math.floor(padLen / 2);
2927
+ const right = padLen - left;
2928
+ return `${" ".repeat(left)}${inner}${" ".repeat(right)}`;
2929
+ };
2930
+ this.out.write(` ${theme2.primary("\u2502")}${centre(firstLine)}${theme2.primary("\u2502")}
2931
+ `);
2932
+ for (const l of body) {
2933
+ this.out.write(
2934
+ ` ${theme2.primary("\u2502")} ${l}${" ".repeat(Math.max(0, boxWidth - 3 - l.length))}${theme2.primary("\u2502")}
3028
2935
  `
3029
- );
3030
- } else if (!await hasAnyCredential(config, modelsRegistry)) {
3031
- const outcome = await runFirstRunSetup({
3032
- renderer,
3033
- reader,
3034
- modelsRegistry,
3035
- vault,
3036
- profileConfigPath,
3037
- reloadConfig: async () => (await bootConfig(flags)).config
3038
- });
3039
- if (outcome.kind === "quit") {
3040
- await reader.close();
3041
- return 0;
3042
- }
3043
- if (outcome.kind === "setup-mode") {
3044
- config = patchConfig(config, { provider: SETUP_PROVIDER_ID, model: SETUP_MODEL_ID });
3045
- await saveToGlobalConfig(profileConfigPath, SETUP_PROVIDER_ID, SETUP_MODEL_ID);
3046
- skipPicker = true;
3047
- } else {
3048
- try {
3049
- const reloaded = await bootConfig(flags);
3050
- config = reloaded.config;
3051
- vault = reloaded.vault;
3052
- } catch (err) {
3053
- writeErr(`Config error after setup: ${toErrorMessage5(err)}
2936
+ );
2937
+ }
2938
+ this.out.write(` ${theme2.primary("\u2514")}${thin}${theme2.primary("\u2518")}
3054
2939
  `);
3055
- await reader.close();
3056
- return 2;
3057
- }
3058
- }
2940
+ this.lineStart = true;
2941
+ }
2942
+ /**
2943
+ * Render subagent completion banners from a RunResult.
2944
+ * Uses `delegateSummaries` when available (populated by delegate tool),
2945
+ * otherwise falls back to scanning message history.
2946
+ */
2947
+ writeDelegateSummaries(result) {
2948
+ if (this.suppressStdout()) return;
2949
+ if (result.delegateSummaries) {
2950
+ for (const { summary, ok } of result.delegateSummaries) {
2951
+ if (!summary) continue;
2952
+ this.writeAgentSummary(summary, ok);
3059
2953
  }
3060
- const savedProvider = config.provider;
3061
- const savedModel = config.model;
3062
- if (!skipPicker && savedProvider && savedModel) {
3063
- const savedStatus = await validateSavedProviderModel(config, modelsRegistry);
3064
- renderer.write(
3065
- `
3066
- ${color11.dim("Last settings:")} ${color11.bold(savedProvider)} / ${color11.bold(savedModel)}
3067
- `
3068
- );
3069
- if (!savedStatus.ok) {
3070
- renderer.writeWarning(
3071
- `Saved provider/model is no longer usable (${savedStatus.reason ?? "unknown reason"}); choose a provider.
3072
- `
3073
- );
3074
- } else {
3075
- const answer = (await reader.readLine(
3076
- ` ${color11.amber("?")} Continue with these? ${color11.dim("[Y/n/q]")} ${color11.dim("(auto Y in 5s)")} `,
3077
- { timeoutMs: 5e3, defaultAnswer: "y" }
3078
- )).trim().toLowerCase();
3079
- if (answer === "q") {
3080
- renderer.write(color11.dim(" Goodbye!\n"));
3081
- await reader.close();
3082
- return 0;
3083
- }
3084
- if (answer !== "n" && answer !== "no") {
3085
- skipPicker = true;
3086
- renderer.write(
3087
- `
3088
- ${color11.green("\u25B6")} ${color11.bold(savedProvider)} / ${color11.bold(savedModel)}
3089
-
3090
- `
3091
- );
3092
- }
2954
+ return;
2955
+ }
2956
+ if (!result.messages) return;
2957
+ for (const msg of result.messages) {
2958
+ const m = msg;
2959
+ if (!Array.isArray(m.content)) continue;
2960
+ for (const block of m.content) {
2961
+ const b = block;
2962
+ if (b.type !== "tool_result" || b.name !== "delegate") continue;
2963
+ let obj;
2964
+ try {
2965
+ obj = typeof b.content === "string" ? JSON.parse(b.content) : b.content;
2966
+ } catch {
2967
+ continue;
3093
2968
  }
3094
- }
3095
- if (!skipPicker) {
3096
- picked = await runPicker({
3097
- modelsRegistry,
3098
- renderer,
3099
- reader,
3100
- config,
3101
- defaultProvider: providerFlag ?? config.provider,
3102
- defaultModel: modelFlag ?? config.model
3103
- });
3104
- }
3105
- if (!picked && !skipPicker) {
3106
- if (!config.provider || !config.model) {
3107
- renderer.write(
3108
- `
3109
- ${color11.dim("No provider selected.")}
3110
- ${color11.dim("Run")} ${color11.bold("wstack auth")} ${color11.dim("to add a key or sign in, or start with no model:")}
3111
- ${color11.bold(`wstack --provider ${SETUP_PROVIDER_ID} --model ${SETUP_MODEL_ID}`)}
3112
-
3113
- `
3114
- );
3115
- await reader.close();
3116
- return 2;
2969
+ const o = obj;
2970
+ if (o.summary) {
2971
+ this.writeAgentSummary(o.summary, o.ok ?? true);
3117
2972
  }
3118
2973
  }
3119
- if (picked) {
3120
- const prevProvider = config.provider;
3121
- const prevModel = config.model;
3122
- config = patchConfig(config, { provider: picked.provider, model: picked.model });
3123
- if (picked.provider !== prevProvider || picked.model !== prevModel) {
3124
- const saved = await saveToGlobalConfig(profileConfigPath, picked.provider, picked.model);
3125
- if (saved) {
3126
- renderer.writeInfo(`Saved ${picked.provider}/${picked.model} as default.
3127
- `);
3128
- } else {
3129
- renderer.writeWarning(
3130
- `Could not save ${picked.provider}/${picked.model} to config. Check permissions or disk space.
3131
- `
3132
- );
3133
- }
3134
- }
2974
+ }
2975
+ }
2976
+ };
2977
+ function renderMarkdown(s) {
2978
+ let out = s;
2979
+ out = out.replace(/^(#{1,6}) (.+)$/gm, (_m, hashes, text) => {
2980
+ return theme2.primary(theme2.bold(`${hashes} ${text}`));
2981
+ });
2982
+ out = out.replace(/```([a-zA-Z0-9_+-]*)\n([\s\S]*?)```/g, (_m, _lang, code) => {
2983
+ return color12.gray(`
2984
+ \u250C\u2500
2985
+ ${code.replace(/^/gm, "\u2502 ")}\u2514\u2500`);
2986
+ });
2987
+ out = out.replace(/`([^`\n]+)`/g, (_m, code) => theme2.accent(code));
2988
+ out = out.replace(/\*\*([^*]+)\*\*/g, (_m, text) => theme2.bold(text));
2989
+ out = out.replace(
2990
+ /(^|[^*])\*([^*\n]+)\*([^*]|$)/g,
2991
+ (_m, l, t, r) => `${l}${color12.italic(t)}${r}`
2992
+ );
2993
+ return out;
2994
+ }
2995
+ function formatInputSummary(input) {
2996
+ if (!input || typeof input !== "object") return "";
2997
+ const obj = input;
2998
+ if (typeof obj["path"] === "string") return obj["path"];
2999
+ if (typeof obj["url"] === "string") return obj["url"];
3000
+ if (typeof obj["command"] === "string") {
3001
+ const cmd = obj["command"];
3002
+ return cmd.length > 60 ? cmd.slice(0, 57) + "..." : cmd;
3003
+ }
3004
+ if (typeof obj["pattern"] === "string") return obj["pattern"];
3005
+ return "";
3006
+ }
3007
+ function safeStringify(value) {
3008
+ if (typeof value === "string") return value;
3009
+ try {
3010
+ return JSON.stringify(value);
3011
+ } catch {
3012
+ return String(value);
3013
+ }
3014
+ }
3015
+ function extractBodyText(value, name) {
3016
+ let v = value;
3017
+ if (typeof value === "string") {
3018
+ const trimmed = value.trimStart();
3019
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
3020
+ try {
3021
+ v = JSON.parse(value);
3022
+ } catch {
3023
+ return value;
3135
3024
  }
3136
- } else if (!config.provider || !config.model) {
3137
- writeErr(
3138
- `No provider or model configured. Run \`wstack auth\`, or pass --provider <id> --model <id>.
3139
- To start the app with no model connected, pass --provider ${SETUP_PROVIDER_ID} --model ${SETUP_MODEL_ID}.
3140
- `
3141
- );
3142
- await reader.close();
3143
- return 2;
3144
3025
  } else {
3145
- const savedStatus = await validateSavedProviderModel(config, modelsRegistry);
3146
- if (!savedStatus.ok) {
3147
- writeErr(
3148
- `Saved provider/model is no longer usable (${savedStatus.reason ?? "unknown reason"}). Run \`wstack auth\` or pass --provider <id> --model <id>.
3149
- `
3150
- );
3151
- await reader.close();
3152
- return 2;
3026
+ return value;
3027
+ }
3028
+ }
3029
+ if (typeof v !== "object" || v === null) {
3030
+ return safeStringify(value);
3031
+ }
3032
+ const o = v;
3033
+ if (name === "read" && typeof o["text"] === "string") return o["text"];
3034
+ if (name === "bash") {
3035
+ const output = typeof o["output"] === "string" ? o["output"] : "";
3036
+ const error = typeof o["error"] === "string" ? o["error"] : "";
3037
+ if (output && error) return `${output}
3038
+ ${error}`;
3039
+ return output || error || "";
3040
+ }
3041
+ if (name === "exec") {
3042
+ const stdout = typeof o["stdout"] === "string" ? o["stdout"] : "";
3043
+ const stderr = typeof o["stderr"] === "string" ? o["stderr"] : "";
3044
+ if (stdout && stderr) return `${stdout}
3045
+ ${stderr}`;
3046
+ return stdout || stderr || "";
3047
+ }
3048
+ return safeStringify(value);
3049
+ }
3050
+ function extractDiff(value) {
3051
+ if (typeof value === "object" && value !== null) {
3052
+ const d = value.diff;
3053
+ if (typeof d === "string" && d.length > 0) return d;
3054
+ }
3055
+ if (typeof value === "string") {
3056
+ const trimmed = value.trimStart();
3057
+ if (trimmed.startsWith("{")) {
3058
+ try {
3059
+ const parsed = JSON.parse(value);
3060
+ if (typeof parsed.diff === "string" && parsed.diff.length > 0) {
3061
+ return parsed.diff;
3062
+ }
3063
+ } catch {
3064
+ }
3065
+ }
3066
+ if (/^---[^\n]*\n\+\+\+/m.test(value)) return value;
3067
+ }
3068
+ return null;
3069
+ }
3070
+ function summarize(value, name) {
3071
+ let v = value;
3072
+ if (typeof value === "string") {
3073
+ const trimmed = value.trimStart();
3074
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
3075
+ try {
3076
+ v = JSON.parse(value);
3077
+ } catch {
3153
3078
  }
3154
3079
  }
3155
3080
  }
3156
- if (flags["webui"]) {
3157
- flags["tui"] = false;
3158
- flags["no-tui"] = true;
3081
+ if (typeof v === "object" && v !== null) {
3082
+ const o = v;
3083
+ if (name === "edit") {
3084
+ const path10 = typeof o["path"] === "string" ? o["path"] : "";
3085
+ const reps = typeof o["replacements"] === "number" ? o["replacements"] : 0;
3086
+ return `${path10} ${reps} replacement${reps === 1 ? "" : "s"}`.trim();
3087
+ }
3088
+ if (name === "write") {
3089
+ const path10 = typeof o["path"] === "string" ? o["path"] : "";
3090
+ const bytes = typeof o["bytes"] === "number" ? o["bytes"] : void 0;
3091
+ return bytes !== void 0 ? `${path10} ${bytes}B` : path10;
3092
+ }
3093
+ if (name === "read") {
3094
+ return summarizeRead(o);
3095
+ }
3096
+ if (name === "bash") {
3097
+ return summarizeBash(o);
3098
+ }
3099
+ if (name === "exec") {
3100
+ return summarizeExec(o);
3101
+ }
3102
+ if (typeof o["count"] === "number") {
3103
+ return `${o["count"]} match${o["count"] === 1 ? "" : "es"}`;
3104
+ }
3105
+ }
3106
+ return "";
3107
+ }
3108
+ function summarizeRead(o) {
3109
+ const path10 = typeof o["path"] === "string" ? o["path"] : "";
3110
+ const total = typeof o["total_lines"] === "number" ? o["total_lines"] : void 0;
3111
+ const truncated = o["truncated"] === true;
3112
+ const cached = o["cached"] === true;
3113
+ const note = typeof o["note"] === "string" ? o["note"] : void 0;
3114
+ const parts = [];
3115
+ if (path10) parts.push(path10);
3116
+ if (total !== void 0) parts.push(`${total} line${total === 1 ? "" : "s"}`);
3117
+ const flags = [];
3118
+ if (truncated) flags.push("truncated");
3119
+ if (cached) flags.push("cached");
3120
+ if (flags.length > 0) parts.push(`(${flags.join(", ")})`);
3121
+ let summary = parts.join(" ").trim();
3122
+ if (note) summary = summary ? `${summary} \u2014 ${note}` : note;
3123
+ return summary;
3124
+ }
3125
+ function summarizeBash(o) {
3126
+ const exit = typeof o["exit_code"] === "number" ? o["exit_code"] : void 0;
3127
+ const output = typeof o["output"] === "string" ? o["output"] : "";
3128
+ const lines = output ? output.split("\n").length : 0;
3129
+ const parts = [];
3130
+ if (exit !== void 0) parts.push(`exit=${exit}`);
3131
+ else if (o["exit_code"] === null) parts.push("backgrounded");
3132
+ if (o["timed_out"] === true) parts.push("timed out");
3133
+ if (lines > 0) parts.push(`${lines} output line${lines === 1 ? "" : "s"}`);
3134
+ if (output.length > 0) parts.push(`${output.length}B`);
3135
+ return parts.join(" ");
3136
+ }
3137
+ function summarizeExec(o) {
3138
+ const command = typeof o["command"] === "string" ? o["command"] : "";
3139
+ const args = Array.isArray(o["args"]) ? o["args"].filter((a) => typeof a === "string") : [];
3140
+ const exit = typeof o["exitCode"] === "number" ? o["exitCode"] : void 0;
3141
+ const stdout = typeof o["stdout"] === "string" ? o["stdout"] : "";
3142
+ const stderr = typeof o["stderr"] === "string" ? o["stderr"] : "";
3143
+ const outLines = stdout ? stdout.split("\n").length : 0;
3144
+ const errLines = stderr ? stderr.split("\n").length : 0;
3145
+ const totalBytes = stdout.length + stderr.length;
3146
+ const parts = [];
3147
+ const invocation = [command, ...args].filter(Boolean).join(" ");
3148
+ if (invocation) parts.push(`$ ${invocation}`);
3149
+ if (exit !== void 0) parts.push(`exit=${exit}`);
3150
+ if (outLines > 0) parts.push(`${outLines} stdout line${outLines === 1 ? "" : "s"}`);
3151
+ if (errLines > 0) parts.push(`${errLines} stderr line${errLines === 1 ? "" : "s"}`);
3152
+ if (totalBytes > 0) parts.push(`${totalBytes}B`);
3153
+ return parts.join(" ");
3154
+ }
3155
+ function compactSingleLine(text) {
3156
+ const first = text.split("\n")[0]?.trim() ?? "";
3157
+ if (!first) return "(no output)";
3158
+ return first.length > 200 ? `${first.slice(0, 197)}\u2026` : first;
3159
+ }
3160
+
3161
+ // src/boot.ts
3162
+ var GITHUB_PROVIDERS_OVERLAY_URL = "https://raw.githubusercontent.com/WrongStack/WrongStack/main/packages/cli/data/providers.json";
3163
+ async function boot(argv) {
3164
+ const { flags, positional } = parseArgs(argv);
3165
+ if (positional[0] === "sandbox") {
3166
+ const { runSandboxCommand } = await import("./sandbox-TYGD6FI2.js");
3167
+ return runSandboxCommand(positional.slice(1), flags, new TerminalRenderer());
3168
+ }
3169
+ if (positional[0] === "update") {
3170
+ const renderer2 = new TerminalRenderer();
3171
+ return runUpdateCommand(positional.slice(1), {
3172
+ cwd: process.cwd(),
3173
+ userHome: os.homedir(),
3174
+ renderer: renderer2,
3175
+ flags
3176
+ });
3177
+ }
3178
+ if (positional[0] === "remote" || typeof flags["remote"] === "string") {
3179
+ const { runRemoteCommand } = await import("./remote-C74BCNND.js");
3180
+ return runRemoteCommand(
3181
+ positional[0] === "remote" ? positional.slice(1) : positional,
3182
+ flags,
3183
+ new TerminalRenderer()
3184
+ );
3185
+ }
3186
+ if (positional[0] === "resume" && positional[1] && !subcommands["__noop_resume_marker"]) {
3187
+ flags["resume"] = positional[1];
3188
+ positional.splice(0, 2);
3159
3189
  }
3160
- if (isInteractiveTTY) {
3161
- const promptMenu = await maybeRunSystemPromptMenu({
3162
- isInteractiveTTY,
3163
- flags,
3190
+ let bootResult;
3191
+ try {
3192
+ bootResult = await bootConfig(flags);
3193
+ } catch (err) {
3194
+ writeErr3(`Config error: ${toErrorMessage7(err)}
3195
+ `);
3196
+ return 2;
3197
+ }
3198
+ let { paths, config, vault } = bootResult;
3199
+ try {
3200
+ resolveToolRestriction(flags);
3201
+ resolveLaunchAllowedTools(flags);
3202
+ parseOutputFormat(flags["output-format"]);
3203
+ announceSafeMode(flags, writeErr3);
3204
+ activateRestrictedMode(flags, writeErr3);
3205
+ const launchMcp = await resolveLaunchMcpServers(flags, paths.cwd);
3206
+ if (launchMcp) flags["mcp-config"] = JSON.stringify({ mcpServers: launchMcp });
3207
+ const schema = await resolveJsonSchemaFlag(flags["json-schema"], paths.cwd);
3208
+ if (schema) flags["json-schema"] = JSON.stringify(schema);
3209
+ const appended = await resolveAppendedSystemPrompt(flags, paths.cwd);
3210
+ delete flags["append-system-prompt-file"];
3211
+ if (appended) flags["append-system-prompt"] = appended;
3212
+ else delete flags["append-system-prompt"];
3213
+ } catch (err) {
3214
+ writeErr3(`wstack: ${toErrorMessage7(err)}
3215
+ `);
3216
+ return 2;
3217
+ }
3218
+ let first = positional[0];
3219
+ const mayOfferConfigRecovery = isStdinTTY3() && (positional.length === 0 || first === "quick") && typeof flags["prompt"] !== "string" && !flags["webui"] && !flags["no-interactive"] && !flags["skip"];
3220
+ const machineOutput = flags["output-json"] === true || flags["output-format"] === "json" || flags["output-format"] === "stream-json";
3221
+ const renderer = new TerminalRenderer(machineOutput ? { out: process.stderr } : void 0);
3222
+ const reader = new ReadlineInputReader({ historyFile: paths.wpaths.historyFile });
3223
+ if (mayOfferConfigRecovery) {
3224
+ const recoveryProfile = config.activeProfile ?? "default";
3225
+ const restored = await maybeRestoreDefaultProfileFromBackup({
3226
+ globalRoot: paths.wpaths.globalRoot,
3227
+ profilePath: paths.wpaths.profileConfig(recoveryProfile),
3228
+ profileName: recoveryProfile,
3164
3229
  renderer,
3165
- reader,
3166
- profileConfigPath,
3167
- paths: {
3168
- globalDir: wpaths.globalInstructions,
3169
- projectDir: wpaths.inProjectInstructions
3170
- },
3171
- outsideProject: await isOutsideProject(projectRoot)
3230
+ reader
3172
3231
  });
3173
- if (promptMenu.aborted) {
3174
- await reader.close();
3175
- return 0;
3176
- }
3177
- if (promptMenu.changed && promptMenu.variant) {
3178
- config = patchConfig(config, { systemPrompt: { variant: promptMenu.variant } });
3179
- }
3180
- if (promptMenu.persistError) {
3181
- renderer.writeWarning(
3182
- `Could not save system prompt variant to config: ${toErrorMessage5(promptMenu.persistError)}
3183
- `
3184
- );
3185
- }
3186
- let modePinned;
3187
- if (flags["no-tui"]) modePinned = "repl";
3188
- else if (flags["tui"]) modePinned = "tui";
3189
- const yoloPinned = flags["no-yolo"] === true ? false : flags["yolo"] === true ? true : void 0;
3190
- let autonomyPinned;
3191
- if (flags["no-autonomy"] === true) autonomyPinned = "off";
3192
- else if (flags["eternal"] === true)
3193
- autonomyPinned = "off";
3194
- else if (typeof flags["autonomy"] === "string") {
3195
- const v = flags["autonomy"].toLowerCase();
3196
- autonomyPinned = v === "off" || v === "no" || v === "false" ? "off" : "auto";
3197
- } else if (flags["autonomy"] === true) {
3198
- autonomyPinned = "auto";
3199
- }
3200
- const lastChoices = config.launch ? {
3201
- mode: config.launch.mode ?? "tui",
3202
- yolo: config.yolo ?? true,
3203
- autonomy: config.launch.autonomy ?? "auto"
3204
- } : void 0;
3205
- let choices;
3206
- try {
3207
- choices = await runLaunchPrompts({
3208
- renderer,
3209
- reader,
3210
- modePinned,
3211
- yoloPinned,
3212
- autonomyPinned,
3213
- lastChoices
3214
- });
3215
- } catch (err) {
3216
- if (err instanceof LaunchAbortedError) {
3232
+ if (restored) {
3233
+ try {
3234
+ bootResult = await bootConfig(flags);
3235
+ ({ paths, config, vault } = bootResult);
3236
+ } catch (err) {
3237
+ writeErr3(`Config error after backup restore: ${toErrorMessage7(err)}
3238
+ `);
3217
3239
  await reader.close();
3218
- return 0;
3240
+ return 2;
3219
3241
  }
3220
- throw err;
3221
3242
  }
3222
- if (choices.mode === "tui") {
3223
- flags["tui"] = true;
3224
- flags["no-tui"] = false;
3243
+ }
3244
+ config = applySimpleUiFullAutoProfile(config, flags);
3245
+ const simpleUiFullAuto = isSimpleUiFullAuto(flags);
3246
+ const { cwd, projectRoot, userHome, wpaths, pathResolver } = paths;
3247
+ const profileConfigPath = activeProfileConfigPath(wpaths, config);
3248
+ void pathResolver;
3249
+ if (first === "quick") {
3250
+ flags["quick"] = true;
3251
+ flags["tui"] = true;
3252
+ positional.splice(0, 1);
3253
+ first = void 0;
3254
+ const plugins = config?.plugins ?? [];
3255
+ if (plugins.length === 0) {
3256
+ console.debug("[wrongstack:quick] No plugins configured");
3225
3257
  } else {
3226
- flags["tui"] = false;
3227
- flags["no-tui"] = true;
3258
+ for (const p of plugins) {
3259
+ const name = typeof p === "string" ? p : p.name;
3260
+ const enabled = typeof p === "object" && p.enabled === false ? " (disabled)" : "";
3261
+ console.debug(`[wrongstack:quick] plugin: ${name}${enabled}`);
3262
+ }
3228
3263
  }
3229
- if (choices.yolo !== config.yolo) config = patchConfig(config, { yolo: choices.yolo });
3230
- flags["autonomy"] = choices.autonomy;
3231
- if (shouldPrintYoloNotice(lastChoices, yoloPinned, choices.yolo)) {
3232
- writeErr(
3233
- `
3234
- ${color11.yellow("YOLO is on")}: non-denied tool calls, including shell and file writes, run without confirmation.
3235
- ${color11.dim("Explicit deny rules still apply. Use")} --no-yolo ${color11.dim("or")} /yolo off ${color11.dim("to require prompts.")}
3264
+ }
3265
+ const logger = new DefaultLogger({
3266
+ level: config.log.level,
3267
+ file: wpaths.logFile,
3268
+ // Suppress stderr output in TUI mode: plugin/library log messages
3269
+ // (e.g. Telegram "getUpdates failed") write directly to stderr and
3270
+ // bypass Ink, which breaks the Static/live boundary.
3271
+ // Logs still go to the disk file for post-hoc debugging.
3272
+ stderr: !flags.tui
3273
+ });
3274
+ const modelsRegistry = new DefaultModelsRegistry({
3275
+ cacheFile: wpaths.modelsCache,
3276
+ // Force a refresh attempt once per CLI process. Model metadata changes faster
3277
+ // than releases (new model ids, corrected context windows), and stale cache
3278
+ // here directly affects runtime behavior like context bars and compaction.
3279
+ // If the network is unavailable, DefaultModelsRegistry still falls back to
3280
+ // stale cache or the bundled overlay instead of failing startup.
3281
+ ttlSeconds: 0,
3282
+ // Curated overlay merged on top of models.dev: fetched from GitHub raw for
3283
+ // freshness, with the bundled file as the offline floor.
3284
+ overlayUrl: GITHUB_PROVIDERS_OVERLAY_URL,
3285
+ overlayFile: resolveBundledOverlayFile(),
3286
+ overlayCacheFile: wpaths.modelsOverlayCache
3287
+ });
3288
+ installProviderPersisters({ config, paths: wpaths, vault, logger });
3289
+ const subcommandCode = await dispatchBootSubcommand({
3290
+ first,
3291
+ positional,
3292
+ flags,
3293
+ config,
3294
+ vault,
3295
+ wpaths,
3296
+ cwd,
3297
+ projectRoot,
3298
+ userHome,
3299
+ renderer,
3300
+ reader,
3301
+ logger,
3302
+ modelsRegistry
3303
+ });
3304
+ if (subcommandCode !== void 0) return subcommandCode;
3305
+ if (isHomeDirectory(cwd, userHome)) {
3306
+ writeErr3(
3307
+ `
3308
+ ${color13.red(color13.bold("\u26A0 This is not a working directory."))}
3309
+ ${color13.red("Please open wstack in a project folder.")}
3236
3310
 
3237
3311
  `
3238
- );
3239
- }
3240
- if ((flags["skip-index"] || flags["skip"]) && config.indexing) {
3241
- config = patchConfig(config, {
3242
- indexing: { ...config.indexing, onSessionStart: false }
3243
- });
3244
- }
3245
- if (!simpleUiFullAuto) {
3246
- try {
3247
- const toPersist = flags["webui"] ? { ...choices, mode: lastChoices?.mode ?? config.launch?.mode ?? "tui" } : choices;
3248
- await persistLaunchChoices(profileConfigPath, toPersist);
3249
- } catch {
3250
- }
3251
- }
3252
- } else {
3253
- const nonInteractiveAutonomy = !simpleUiFullAuto && (flags["no-autonomy"] === true || config.autonomy?.defaultMode === "off") ? "off" : "auto";
3254
- const effectiveChoices = config.launch ? {
3255
- mode: flags["no-tui"] ? "repl" : config.launch.mode ?? "tui",
3256
- yolo: config.yolo ?? true,
3257
- autonomy: nonInteractiveAutonomy
3258
- } : {
3259
- mode: "repl",
3260
- yolo: true,
3261
- autonomy: nonInteractiveAutonomy
3262
- };
3263
- if (effectiveChoices.mode === "repl") {
3264
- flags["tui"] = false;
3265
- flags["no-tui"] = true;
3312
+ );
3313
+ await reader.close();
3314
+ return 1;
3315
+ }
3316
+ if (!flags["no-models-refresh"]) {
3317
+ await startCatalog({
3318
+ registry: modelsRegistry,
3319
+ logger,
3320
+ shortLived: positional.length > 0 || typeof flags["prompt"] === "string"
3321
+ });
3322
+ }
3323
+ try {
3324
+ await discoverAndMergeProviders({
3325
+ config,
3326
+ registry: modelsRegistry,
3327
+ cacheDir: path9.dirname(wpaths.modelsCache),
3328
+ logger
3329
+ });
3330
+ } catch (err) {
3331
+ logger.debug(`provider auto-discovery skipped: ${toErrorMessage7(err)}`);
3332
+ }
3333
+ const isSingleShot = positional.length > 0 || typeof flags["prompt"] === "string";
3334
+ const isInteractiveTTY = isStdinTTY3() && !isSingleShot && !flags["webui"] && !flags["no-interactive"] && !flags["skip"];
3335
+ if (isInteractiveTTY) {
3336
+ await checkGitInCwd({ cwd, renderer, reader });
3337
+ const cont = await runProjectCheck({ projectRoot, cwd, renderer, reader });
3338
+ if (!cont) {
3339
+ await reader.close();
3340
+ return 0;
3266
3341
  }
3267
- flags["autonomy"] = effectiveChoices.autonomy;
3268
3342
  }
3343
+ const wantsTui = flags["tui"] === true && flags["no-tui"] !== true;
3344
+ if (wantsTui && (!process.stdout.isTTY || !process.stdin.isTTY)) {
3345
+ writeErr3(
3346
+ "wstack: --tui requires an interactive terminal on both stdin and stdout.\n Drop the flag (use the plain REPL) or run wstack directly without piping.\n"
3347
+ );
3348
+ await reader.close();
3349
+ return 2;
3350
+ }
3351
+ const noInteractiveMode = flags["webui"] || flags["no-interactive"];
3352
+ const providerGate = await resolveBootProviderModel({
3353
+ flags,
3354
+ config,
3355
+ vault,
3356
+ modelsRegistry,
3357
+ renderer,
3358
+ reader,
3359
+ profileConfigPath,
3360
+ noInteractiveMode
3361
+ });
3362
+ if (providerGate.kind === "exit") return providerGate.code;
3363
+ ({ config, vault } = providerGate);
3364
+ if (flags["webui"]) {
3365
+ flags["tui"] = false;
3366
+ flags["no-tui"] = true;
3367
+ }
3368
+ const launch = await applyBootLaunchChoices({
3369
+ isInteractiveTTY,
3370
+ simpleUiFullAuto,
3371
+ flags,
3372
+ config,
3373
+ renderer,
3374
+ reader,
3375
+ profileConfigPath,
3376
+ wpaths,
3377
+ projectRoot
3378
+ });
3379
+ if (launch.kind === "exit") return launch.code;
3380
+ config = launch.config;
3269
3381
  applyGoalTuiDefault(flags, positional);
3270
3382
  flags["director"] = true;
3271
3383
  return {
@@ -3311,7 +3423,7 @@ async function initializeCli(argv, options = {}) {
3311
3423
  error: err
3312
3424
  }).catch(() => void 0);
3313
3425
  }
3314
- writeErr2(
3426
+ writeErr4(
3315
3427
  `WebUI session child argument error: ${err instanceof Error ? err.message : String(err)}
3316
3428
  `
3317
3429
  );
@@ -3370,7 +3482,7 @@ async function initializeCli(argv, options = {}) {
3370
3482
  error: err
3371
3483
  }).catch(() => void 0);
3372
3484
  }
3373
- writeErr2(
3485
+ writeErr4(
3374
3486
  `WebUI session child argument error: ${err instanceof Error ? err.message : String(err)}
3375
3487
  `
3376
3488
  );
@@ -3399,7 +3511,7 @@ async function initializeCli(argv, options = {}) {
3399
3511
  recoverable: false,
3400
3512
  error: err
3401
3513
  }).catch(() => void 0);
3402
- writeErr2(
3514
+ writeErr4(
3403
3515
  `WebUI session child argument error: ${err instanceof Error ? err.message : String(err)}
3404
3516
  `
3405
3517
  );
@@ -3449,4 +3561,4 @@ async function initializeCli(argv, options = {}) {
3449
3561
  export {
3450
3562
  initializeCli
3451
3563
  };
3452
- //# sourceMappingURL=cli-context-7HK67B2Z.js.map
3564
+ //# sourceMappingURL=cli-context-I6HJCVCV.js.map