@wrongstack/cli 1.0.32 → 1.0.35

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