muonroi-cli 1.8.5 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/dist/packages/agent-harness-core/src/event-filter.js +11 -0
  2. package/dist/packages/agent-harness-core/src/event-redact.js +7 -0
  3. package/dist/packages/agent-harness-core/src/event-tee.d.ts +20 -4
  4. package/dist/packages/agent-harness-core/src/event-tee.js +32 -5
  5. package/dist/packages/agent-harness-core/src/mcp-server.d.ts +14 -0
  6. package/dist/packages/agent-harness-core/src/mcp-server.js +86 -10
  7. package/dist/packages/agent-harness-core/src/predicate.d.ts +1 -1
  8. package/dist/packages/agent-harness-core/src/protocol.d.ts +24 -2
  9. package/dist/src/agent-harness/mock-model.d.ts +10 -0
  10. package/dist/src/agent-harness/mock-model.js +6 -2
  11. package/dist/src/chat/chat-keychain.d.ts +7 -12
  12. package/dist/src/chat/chat-keychain.js +19 -86
  13. package/dist/src/cli/keys.d.ts +8 -45
  14. package/dist/src/cli/keys.js +18 -324
  15. package/dist/src/council/clarifier.d.ts +4 -2
  16. package/dist/src/council/clarifier.js +158 -36
  17. package/dist/src/council/debate-planner.js +3 -2
  18. package/dist/src/council/debate.js +59 -5
  19. package/dist/src/council/index.d.ts +23 -0
  20. package/dist/src/council/index.js +37 -2
  21. package/dist/src/council/llm.d.ts +62 -0
  22. package/dist/src/council/llm.js +123 -23
  23. package/dist/src/council/panel-select.js +13 -3
  24. package/dist/src/council/planner.js +17 -0
  25. package/dist/src/council/preflight.d.ts +10 -0
  26. package/dist/src/council/preflight.js +36 -0
  27. package/dist/src/council/prompts.d.ts +9 -1
  28. package/dist/src/council/prompts.js +23 -6
  29. package/dist/src/council/types.d.ts +10 -0
  30. package/dist/src/ee/auth.d.ts +19 -0
  31. package/dist/src/ee/auth.js +39 -0
  32. package/dist/src/ee/client.js +28 -3
  33. package/dist/src/ee/ee-onboarding.js +6 -26
  34. package/dist/src/flow/compaction/compress.d.ts +2 -2
  35. package/dist/src/flow/compaction/compress.js +21 -8
  36. package/dist/src/flow/compaction/extract.d.ts +3 -3
  37. package/dist/src/flow/compaction/extract.js +6 -6
  38. package/dist/src/flow/compaction/index.d.ts +2 -1
  39. package/dist/src/flow/compaction/index.js +29 -3
  40. package/dist/src/flow/compaction/progress.d.ts +35 -0
  41. package/dist/src/flow/compaction/progress.js +35 -0
  42. package/dist/src/generated/version.d.ts +1 -1
  43. package/dist/src/generated/version.js +1 -1
  44. package/dist/src/gsd/flags.d.ts +11 -0
  45. package/dist/src/gsd/flags.js +19 -0
  46. package/dist/src/gsd/plan-council.js +104 -72
  47. package/dist/src/gsd/verdict-schema.d.ts +1 -1
  48. package/dist/src/headless/council-answers.js +4 -0
  49. package/dist/src/index.js +129 -260
  50. package/dist/src/lsp/builtins.js +3 -1
  51. package/dist/src/lsp/manager.d.ts +5 -1
  52. package/dist/src/lsp/manager.js +249 -3
  53. package/dist/src/lsp/npm-cache.d.ts +11 -1
  54. package/dist/src/lsp/npm-cache.js +17 -1
  55. package/dist/src/lsp/runtime.d.ts +6 -1
  56. package/dist/src/lsp/runtime.js +17 -1
  57. package/dist/src/lsp/types.d.ts +83 -1
  58. package/dist/src/lsp/types.js +10 -0
  59. package/dist/src/mcp/client-pool.js +43 -15
  60. package/dist/src/mcp/lsp-tools.d.ts +5 -1
  61. package/dist/src/mcp/lsp-tools.js +93 -2
  62. package/dist/src/mcp/mcp-keychain.d.ts +3 -5
  63. package/dist/src/mcp/mcp-keychain.js +9 -49
  64. package/dist/src/mcp/setup-guide-text.d.ts +1 -1
  65. package/dist/src/mcp/setup-guide-text.js +22 -2
  66. package/dist/src/mcp/tools-server.d.ts +10 -0
  67. package/dist/src/mcp/tools-server.js +10 -2
  68. package/dist/src/models/catalog.json +19 -19
  69. package/dist/src/orchestrator/ask-user.d.ts +61 -0
  70. package/dist/src/orchestrator/ask-user.js +65 -0
  71. package/dist/src/orchestrator/compaction.d.ts +2 -3
  72. package/dist/src/orchestrator/compaction.js +8 -8
  73. package/dist/src/orchestrator/council-manager.js +9 -8
  74. package/dist/src/orchestrator/council-request.d.ts +49 -0
  75. package/dist/src/orchestrator/council-request.js +62 -0
  76. package/dist/src/orchestrator/interactive-pause.d.ts +26 -0
  77. package/dist/src/orchestrator/interactive-pause.js +36 -0
  78. package/dist/src/orchestrator/message-processor.d.ts +4 -0
  79. package/dist/src/orchestrator/message-processor.js +26 -8
  80. package/dist/src/orchestrator/orchestrator.d.ts +25 -0
  81. package/dist/src/orchestrator/orchestrator.js +204 -50
  82. package/dist/src/orchestrator/preprocessor.js +2 -2
  83. package/dist/src/orchestrator/safety-askcard.d.ts +1 -1
  84. package/dist/src/orchestrator/safety-askcard.js +5 -2
  85. package/dist/src/orchestrator/safety-intercept.d.ts +5 -0
  86. package/dist/src/orchestrator/safety-intercept.js +7 -0
  87. package/dist/src/orchestrator/stall-watchdog.d.ts +8 -1
  88. package/dist/src/orchestrator/stall-watchdog.js +24 -3
  89. package/dist/src/orchestrator/stream-runner.d.ts +13 -3
  90. package/dist/src/orchestrator/stream-runner.js +54 -21
  91. package/dist/src/orchestrator/tool-engine.d.ts +19 -0
  92. package/dist/src/orchestrator/tool-engine.js +241 -25
  93. package/dist/src/orchestrator/turn-watchdog.d.ts +7 -0
  94. package/dist/src/orchestrator/turn-watchdog.js +38 -9
  95. package/dist/src/pil/agent-operating-contract.d.ts +1 -1
  96. package/dist/src/pil/agent-operating-contract.js +6 -4
  97. package/dist/src/pil/discovery.d.ts +1 -1
  98. package/dist/src/pil/discovery.js +2 -2
  99. package/dist/src/pil/layer1_5-complexity-size.d.ts +7 -0
  100. package/dist/src/pil/layer1_5-complexity-size.js +31 -5
  101. package/dist/src/pil/llm-classify.d.ts +78 -3
  102. package/dist/src/pil/llm-classify.js +351 -111
  103. package/dist/src/pil/native-capabilities-workbook.d.ts +1 -1
  104. package/dist/src/pil/native-capabilities-workbook.js +7 -0
  105. package/dist/src/pil/pipeline.js +2 -0
  106. package/dist/src/pil/repo-grounding-probe.d.ts +15 -0
  107. package/dist/src/pil/repo-grounding-probe.js +136 -0
  108. package/dist/src/pil/repo-structure-hints.d.ts +7 -0
  109. package/dist/src/pil/repo-structure-hints.js +45 -0
  110. package/dist/src/product-loop/artifact-io.js +4 -0
  111. package/dist/src/product-loop/criteria-seed.d.ts +51 -0
  112. package/dist/src/product-loop/criteria-seed.js +200 -0
  113. package/dist/src/product-loop/discovery-interview.d.ts +9 -0
  114. package/dist/src/product-loop/discovery-interview.js +37 -18
  115. package/dist/src/product-loop/discovery-recommender.js +2 -1
  116. package/dist/src/product-loop/discovery-schema.js +14 -1
  117. package/dist/src/product-loop/discovery-triage.d.ts +23 -0
  118. package/dist/src/product-loop/discovery-triage.js +109 -0
  119. package/dist/src/product-loop/gather.js +150 -2
  120. package/dist/src/product-loop/index.js +7 -0
  121. package/dist/src/product-loop/loop-driver.js +21 -8
  122. package/dist/src/product-loop/phase-plan.d.ts +16 -0
  123. package/dist/src/product-loop/phase-plan.js +42 -4
  124. package/dist/src/product-loop/phase-rituals.d.ts +3 -0
  125. package/dist/src/product-loop/phase-rituals.js +8 -3
  126. package/dist/src/product-loop/phase-runner.js +30 -11
  127. package/dist/src/product-loop/plan-adherence-review.d.ts +26 -0
  128. package/dist/src/product-loop/plan-adherence-review.js +144 -0
  129. package/dist/src/product-loop/sprint-runner.d.ts +62 -0
  130. package/dist/src/product-loop/sprint-runner.js +309 -8
  131. package/dist/src/product-loop/types.d.ts +25 -0
  132. package/dist/src/providers/anthropic.d.ts +9 -8
  133. package/dist/src/providers/anthropic.js +13 -47
  134. package/dist/src/providers/auth/grok-oauth.d.ts +1 -0
  135. package/dist/src/providers/auth/grok-oauth.js +30 -5
  136. package/dist/src/providers/auth/openai-oauth.d.ts +1 -0
  137. package/dist/src/providers/auth/openai-oauth.js +14 -0
  138. package/dist/src/providers/auth/token-store.d.ts +9 -9
  139. package/dist/src/providers/auth/token-store.js +8 -70
  140. package/dist/src/providers/auth/types.d.ts +8 -0
  141. package/dist/src/providers/env-store.d.ts +17 -0
  142. package/dist/src/providers/env-store.js +228 -0
  143. package/dist/src/providers/keychain.d.ts +21 -17
  144. package/dist/src/providers/keychain.js +124 -135
  145. package/dist/src/providers/runtime.d.ts +24 -9
  146. package/dist/src/providers/runtime.js +48 -37
  147. package/dist/src/providers/strategies/thinking-mode.js +9 -1
  148. package/dist/src/providers/strategies/xai.strategy.js +27 -0
  149. package/dist/src/providers/warm.d.ts +65 -0
  150. package/dist/src/providers/warm.js +145 -0
  151. package/dist/src/self-qa/agentic-loop.js +3 -2
  152. package/dist/src/storage/transcript.js +56 -2
  153. package/dist/src/tools/git-safety.d.ts +19 -0
  154. package/dist/src/tools/git-safety.js +168 -0
  155. package/dist/src/tools/native-tools.d.ts +1 -1
  156. package/dist/src/tools/native-tools.js +76 -1
  157. package/dist/src/tools/registry.d.ts +17 -0
  158. package/dist/src/tools/registry.js +116 -1
  159. package/dist/src/types/index.d.ts +29 -1
  160. package/dist/src/ui/app.js +67 -4
  161. package/dist/src/ui/components/agent-rail-activities.d.ts +26 -0
  162. package/dist/src/ui/components/agent-rail-activities.js +47 -0
  163. package/dist/src/ui/components/compact-progress-card.d.ts +24 -0
  164. package/dist/src/ui/components/compact-progress-card.js +42 -0
  165. package/dist/src/ui/components/council-phase-timeline.js +17 -2
  166. package/dist/src/ui/components/council-question-card.js +1 -0
  167. package/dist/src/ui/components/message-view.d.ts +15 -0
  168. package/dist/src/ui/components/message-view.js +50 -1
  169. package/dist/src/ui/components/tool-group.d.ts +15 -3
  170. package/dist/src/ui/components/tool-group.js +69 -11
  171. package/dist/src/ui/containers/modals-layer.d.ts +2 -1
  172. package/dist/src/ui/containers/modals-layer.js +2 -2
  173. package/dist/src/ui/council-harness-event.d.ts +57 -0
  174. package/dist/src/ui/council-harness-event.js +46 -0
  175. package/dist/src/ui/heartbeat-debug.d.ts +29 -0
  176. package/dist/src/ui/heartbeat-debug.js +45 -0
  177. package/dist/src/ui/modals/api-key-modal.js +1 -1
  178. package/dist/src/ui/modals/model-picker-modal.d.ts +8 -18
  179. package/dist/src/ui/modals/model-picker-modal.js +8 -10
  180. package/dist/src/ui/slash/ee.js +81 -0
  181. package/dist/src/ui/slash/menu-items.js +11 -2
  182. package/dist/src/ui/use-app-logic.js +354 -224
  183. package/dist/src/ui/utils/agent-activities.d.ts +39 -0
  184. package/dist/src/ui/utils/agent-activities.js +96 -0
  185. package/dist/src/ui/utils/group-tool-entries.d.ts +26 -0
  186. package/dist/src/ui/utils/group-tool-entries.js +111 -0
  187. package/dist/src/ui/utils/tool-summary.d.ts +21 -0
  188. package/dist/src/ui/utils/tool-summary.js +91 -0
  189. package/dist/src/utils/event-loop-monitor.d.ts +85 -0
  190. package/dist/src/utils/event-loop-monitor.js +107 -0
  191. package/dist/src/utils/llm-deadline.d.ts +14 -0
  192. package/dist/src/utils/llm-deadline.js +19 -0
  193. package/dist/src/utils/loop-profiler.d.ts +102 -0
  194. package/dist/src/utils/loop-profiler.js +202 -0
  195. package/dist/src/utils/settings.d.ts +27 -0
  196. package/dist/src/utils/settings.js +38 -2
  197. package/dist/src/utils/side-question.d.ts +1 -2
  198. package/dist/src/utils/side-question.js +2 -2
  199. package/dist/src/verify/entrypoint.js +51 -16
  200. package/dist/src/verify/orchestrator.d.ts +1 -1
  201. package/dist/src/verify/orchestrator.js +20 -3
  202. package/package.json +1 -2
  203. package/dist/src/cli/bw-vault.d.ts +0 -55
  204. package/dist/src/cli/bw-vault.js +0 -133
  205. package/dist/src/mcp/ee-tools.d.ts +0 -46
  206. package/dist/src/mcp/ee-tools.js +0 -194
package/dist/src/index.js CHANGED
@@ -24,8 +24,7 @@ import { createAbortContext } from "./orchestrator/abort.js";
24
24
  import { completeDelegation, failDelegation, loadDelegation } from "./orchestrator/delegations.js";
25
25
  import { Agent } from "./orchestrator/orchestrator.js";
26
26
  import { createPendingCallsLog } from "./orchestrator/pending-calls.js";
27
- import { getProviderCapabilities } from "./providers/capabilities.js";
28
- import { listStoredProviders, loadKeyForProvider, setKeyForProvider } from "./providers/keychain.js";
27
+ import { loadKeyForProvider, setKeyForProvider } from "./providers/keychain.js";
29
28
  import { detectProviderForModel } from "./providers/runtime.js";
30
29
  import { loadConfig } from "./storage/config.js";
31
30
  import { loadUsage } from "./storage/usage-cap.js";
@@ -35,7 +34,7 @@ import { runScriptManagedUninstall } from "./utils/install-manager.js";
35
34
  import { getApiKey, getBaseURL, getCurrentModel, saveUserSettings } from "./utils/settings.js";
36
35
  import { runUpdate } from "./utils/update-checker.js";
37
36
  import { buildVerifyPrompt, getVerifyCliError } from "./verify/entrypoint.js";
38
- // Hydrate chat secrets from OS keychain before CLI bootstrap
37
+ // Hydrate chat secrets (no-op now: secrets live in the environment/env-store)
39
38
  await hydrateChatEnvFromKeychain();
40
39
  const exitCleanlyOnSigterm = () => {
41
40
  process.exit(0);
@@ -61,6 +60,78 @@ export function appendCrashLog(label, msg) {
61
60
  /* crash.log is best-effort diagnostics; the logger itself must never throw */
62
61
  }
63
62
  }
63
+ /**
64
+ * Arm the event-loop freeze diagnostics for this TUI session.
65
+ *
66
+ * Every existing hang guard in this codebase (provider stall watchdog, turn
67
+ * watchdog) is driven by a timer, so all of them are blind to a blocked event
68
+ * loop — a timer cannot fire while the JS thread is stuck. Measured live
69
+ * 2026-07-16 (session 90c3ff533826): the TUI froze 304.5s, input replayed on
70
+ * recovery, and the 120s stall watchdog only rescued the turn 6.7s AFTER the
71
+ * loop came back. This closes that blind spot: the monitor reports the block
72
+ * once the loop frees up, and — when armed — the CPU profiler supplies the
73
+ * stack captured DURING it.
74
+ *
75
+ * Best-effort: never let diagnostics break the CLI.
76
+ */
77
+ async function startFreezeDiagnostics() {
78
+ try {
79
+ const { getLoopBlockThresholdMs, isLoopProfileEnabled } = await import("./utils/settings.js");
80
+ const { startEventLoopMonitor } = await import("./utils/event-loop-monitor.js");
81
+ const { logger } = await import("./utils/logger.js");
82
+ const thresholdMs = getLoopBlockThresholdMs();
83
+ if (thresholdMs <= 0)
84
+ return;
85
+ const profiler = isLoopProfileEnabled()
86
+ ? await (await import("./utils/loop-profiler.js")).createLoopProfiler()
87
+ : null;
88
+ if (profiler) {
89
+ // Bound segment size. Loop-driven ON PURPOSE: it cannot rotate during a
90
+ // block, so the segment covering a freeze survives to be captured.
91
+ const rotateTimer = setInterval(() => profiler.rotate(), 30_000);
92
+ rotateTimer.unref?.();
93
+ }
94
+ logger.info("cli", `[freeze] diagnostics armed (threshold ${thresholdMs}ms)`, {
95
+ thresholdMs,
96
+ profiler: profiler ? "on" : "off",
97
+ });
98
+ startEventLoopMonitor({
99
+ thresholdMs,
100
+ onBlock: (block) => {
101
+ logger.warn("cli", `[freeze] event loop blocked for ${block.blockedMs}ms — UI was frozen and no timer could fire`, {
102
+ blockedMs: block.blockedMs,
103
+ breadcrumb: block.breadcrumb,
104
+ detectedAt: block.detectedAt,
105
+ profiler: profiler ? "armed" : "off (set MUONROI_LOOP_PROFILE=1 for the culprit stack)",
106
+ });
107
+ if (!profiler)
108
+ return;
109
+ void profiler
110
+ .capture(block.breadcrumb ?? "unknown")
111
+ .then((res) => {
112
+ if (res) {
113
+ // The leaf is usually a native primitive (`now`, a regex step) —
114
+ // true but useless as a headline. Lead with the first frame that
115
+ // has a file:line, i.e. OUR code.
116
+ const suspect = res.hotStack.find((f) => !f.endsWith("(native)")) ?? res.hotStack[0];
117
+ logger.warn("cli", `[freeze] blocked in: ${suspect ?? "(unattributed)"}`, {
118
+ hotStack: res.hotStack,
119
+ file: res.file,
120
+ blockedMs: block.blockedMs,
121
+ });
122
+ }
123
+ })
124
+ .catch((err) => {
125
+ logger.error("cli", `[freeze] profile capture failed: ${err?.message}`, { error: err });
126
+ });
127
+ },
128
+ });
129
+ }
130
+ catch (err) {
131
+ // Diagnostics must never take the CLI down with them.
132
+ appendCrashLog("FREEZE_DIAG", err instanceof Error ? (err.stack ?? err.message) : String(err));
133
+ }
134
+ }
64
135
  process.on("uncaughtException", (err) => {
65
136
  appendCrashLog("UNCAUGHT", err.stack || err.message);
66
137
  console.error("Fatal:", err.message);
@@ -111,7 +182,7 @@ setRenderSink((lineOrChunk) => {
111
182
  // Provider console URLs are sourced from providers/capabilities.ts via ProviderCapabilities.consoleSignupURL().
112
183
  /**
113
184
  * Try to find an API key for the model the CLI is about to run with.
114
- * Resolution: env (legacy MUONROI_API_KEY) OS keychain → settings.json.
185
+ * Resolution: per-provider env var (env-store), else OAuth sentinel.
115
186
  * Returns null if nothing usable is configured anywhere.
116
187
  */
117
188
  async function resolveKeyForModel(modelId) {
@@ -154,187 +225,6 @@ async function hasOAuthForModel(modelId) {
154
225
  return false;
155
226
  }
156
227
  }
157
- /**
158
- * First-run wizard. If the keychain already has keys, prints a hint
159
- * (model probably doesn't match any stored provider). Otherwise prompts
160
- * for provider + key and persists to the OS keychain.
161
- */
162
- /**
163
- * Supported splash providers — mirrors SPLASH_PROVIDERS in ui/app.tsx.
164
- * The wizard only surfaces these; other providers still work programmatically.
165
- */
166
- const WIZARD_PROVIDERS = ["deepseek", "zai", "opencode-go", "xai"];
167
- async function firstRunWizard(currentModel) {
168
- let rl;
169
- try {
170
- rl = createInterface({ input: process.stdin, output: process.stderr });
171
- const ask = (q) => new Promise((resolve) => rl.question(q, (answer) => resolve(answer)));
172
- process.stderr.write("\nWelcome to muonroi-cli!\n\n");
173
- const stored = await listStoredProviders();
174
- if (stored.length > 0) {
175
- process.stderr.write(`Keys already in keychain for: ${stored.join(", ")}\n`);
176
- if (currentModel) {
177
- const provider = detectProviderForModel(currentModel);
178
- process.stderr.write(`Current model '${currentModel}' uses provider '${provider}', which has no stored key.\n`);
179
- }
180
- process.stderr.write("\nOptions:\n" +
181
- " 1. Run with a model that matches a stored provider:\n" +
182
- " muonroi-cli --model <model-id>\n" +
183
- " 2. Open /providers inside the TUI to add another key.\n" +
184
- " 3. muonroi-cli keys set <provider>\n\n");
185
- rl.close();
186
- return null;
187
- }
188
- process.stderr.write("Pick how you want to add credentials:\n\n");
189
- process.stderr.write(" 1. Paste an API key (most common)\n");
190
- process.stderr.write(" 2. Import an encrypted bundle file (from another device)\n");
191
- process.stderr.write(" 3. Sync from a Bitwarden vault\n");
192
- process.stderr.write(" 4. Skip — set up later via /providers inside the TUI\n\n");
193
- const actionChoice = (await ask("Choice [1-4, default 1]: ")).trim() || "1";
194
- if (actionChoice === "4") {
195
- process.stderr.write("\nSkipped. Open /providers inside the TUI to add a key any time.\n");
196
- rl.close();
197
- return null;
198
- }
199
- if (actionChoice === "2") {
200
- const file = (await ask("Path to bundle file: ")).trim();
201
- if (!file) {
202
- process.stderr.write("No file provided. Aborted.\n");
203
- rl.close();
204
- return null;
205
- }
206
- const passphrase = await ask("Bundle passphrase: ");
207
- try {
208
- const { readFileSync: readFile } = await import("node:fs");
209
- const { decryptBundle } = await import("./cli/keys-bundle.js");
210
- const raw = readFile(file, "utf8");
211
- const bundle = JSON.parse(raw);
212
- const payload = decryptBundle(bundle, passphrase);
213
- let imported = 0;
214
- for (const [prov, key] of Object.entries(payload.providers)) {
215
- if (!WIZARD_PROVIDERS.includes(prov))
216
- continue;
217
- if (typeof key !== "string" || key.length < 20)
218
- continue;
219
- const ok = await setKeyForProvider(prov, key);
220
- if (ok) {
221
- imported++;
222
- process.stderr.write(` ✓ ${prov} → keychain\n`);
223
- }
224
- }
225
- process.stderr.write(`\nImported ${imported} key(s). Launch the TUI to start.\n`);
226
- rl.close();
227
- // Return any imported key just so caller treats setup as done.
228
- return imported > 0 ? "imported" : null;
229
- }
230
- catch (err) {
231
- process.stderr.write(`\nImport failed: ${err.message}\n`);
232
- rl.close();
233
- return null;
234
- }
235
- }
236
- if (actionChoice === "3") {
237
- const password = await ask("Bitwarden master password: ");
238
- try {
239
- const { unlockWithPassword, listSecureNotesByPrefix } = await import("./cli/bw-vault.js");
240
- const unlock = await unlockWithPassword(password);
241
- if (!unlock.ok || !unlock.session) {
242
- process.stderr.write(`\nBitwarden unlock failed: ${unlock.error ?? "unknown error"}\n`);
243
- rl.close();
244
- return null;
245
- }
246
- const list = await listSecureNotesByPrefix(unlock.session, "muonroi-cli/");
247
- if (!list.ok) {
248
- process.stderr.write(`\nBitwarden list failed: ${list.error}\n`);
249
- rl.close();
250
- return null;
251
- }
252
- let imported = 0;
253
- for (const item of list.items) {
254
- const prov = item.name.slice("muonroi-cli/".length);
255
- if (!WIZARD_PROVIDERS.includes(prov))
256
- continue;
257
- if (item.notes.length < 20)
258
- continue;
259
- const ok = await setKeyForProvider(prov, item.notes);
260
- if (ok) {
261
- imported++;
262
- process.stderr.write(` ✓ ${prov} → keychain\n`);
263
- }
264
- }
265
- process.stderr.write(`\nImported ${imported} key(s) from Bitwarden. Launch the TUI to start.\n`);
266
- rl.close();
267
- return imported > 0 ? "imported" : null;
268
- }
269
- catch (err) {
270
- process.stderr.write(`\nBitwarden sync failed: ${err.message}\n`);
271
- rl.close();
272
- return null;
273
- }
274
- }
275
- // Default path: paste an API key for one of WIZARD_PROVIDERS.
276
- process.stderr.write("\nSupported providers:\n\n");
277
- WIZARD_PROVIDERS.forEach((p, i) => {
278
- process.stderr.write(` ${i + 1}. ${p.padEnd(12)} ${getProviderCapabilities(p).consoleSignupURL()}\n`);
279
- });
280
- process.stderr.write("\n");
281
- const choice = (await ask(`Provider [1-${WIZARD_PROVIDERS.length}, default 1]: `)).trim();
282
- const idx = choice ? Number.parseInt(choice, 10) - 1 : 0;
283
- if (!Number.isFinite(idx) || idx < 0 || idx >= WIZARD_PROVIDERS.length) {
284
- process.stderr.write("Invalid choice — aborted.\n");
285
- rl.close();
286
- return null;
287
- }
288
- const provider = WIZARD_PROVIDERS[idx];
289
- if (!provider) {
290
- process.stderr.write("Invalid choice — aborted.\n");
291
- rl.close();
292
- return null;
293
- }
294
- process.stderr.write(`\nGet a key here: ${getProviderCapabilities(provider).consoleSignupURL()}\n`);
295
- const raw = await ask(`Paste your ${provider} API key: `);
296
- const trimmed = raw.trim();
297
- if (!trimmed) {
298
- process.stderr.write("No key provided. Aborted.\n");
299
- rl.close();
300
- return null;
301
- }
302
- if (trimmed.length < 20) {
303
- process.stderr.write("Key looks too short (< 20 chars). Aborted.\n");
304
- rl.close();
305
- return null;
306
- }
307
- try {
308
- const ok = await setKeyForProvider(provider, trimmed);
309
- if (ok) {
310
- process.stderr.write(`\nStored ${provider} key in OS keychain.\n`);
311
- process.stderr.write("Tip: run 'muonroi-cli keys export ~/keys.json' to back up + move to other devices.\n");
312
- if (currentModel) {
313
- const currentProvider = detectProviderForModel(currentModel);
314
- if (currentProvider !== provider) {
315
- process.stderr.write(`\nNote: defaultModel '${currentModel}' is on '${currentProvider}'. ` +
316
- `Edit ~/.muonroi-cli/user-settings.json or rerun with --model to use ${provider}.\n`);
317
- }
318
- }
319
- }
320
- else {
321
- process.stderr.write("\nOS keychain unavailable (keytar or secret service backend).\n" +
322
- "Linux: sudo dnf install libsecret (Fedora) or sudo apt-get install libsecret-1-0 (Ubuntu).\n" +
323
- "Key will be used for this session only. For persistence across runs:\n" +
324
- ` export ${provider.toUpperCase()}_API_KEY=...\n`);
325
- }
326
- }
327
- catch (err) {
328
- process.stderr.write(`\nWarning: failed to store key in keychain: ${err.message}\n`);
329
- }
330
- rl.close();
331
- return trimmed;
332
- }
333
- catch {
334
- rl?.close();
335
- return null;
336
- }
337
- }
338
228
  async function startInteractive(apiKey, baseURL, model, maxToolRounds, batchApi, session, initialMessage, permissionMode = "safe", injectHalt = false, injectHaltSprint = false) {
339
229
  // ── Plan 00-07 boot order ──────────────────────────────────────────────────
340
230
  // 1. redactor.installGlobalPatches() — already at top of file (line 6).
@@ -401,6 +291,14 @@ async function startInteractive(apiKey, baseURL, model, maxToolRounds, batchApi,
401
291
  const { warmMcpClients } = await import("./mcp/client-pool.js");
402
292
  const { loadMcpServers } = await import("./utils/settings.js");
403
293
  await warmMcpClients(loadMcpServers(), !session);
294
+ // Register a factory for every credentialed provider, not just the session's.
295
+ // A model resolved for another provider (compaction, classify, a sub-agent,
296
+ // a mode-specific model) can then be routed to ITS OWN endpoint instead of
297
+ // being POSTed to the session provider's — the 404 in session 0c6728ba1a25
298
+ // (`gpt-5.4` → api.x.ai) existed only because the openai factory this
299
+ // redirect needs had never been built. Best-effort: never blocks boot.
300
+ const { warmProviderFactories } = await import("./providers/warm.js");
301
+ await warmProviderFactories();
404
302
  const agent = new Agent(apiKey, baseURL, model, maxToolRounds, {
405
303
  session,
406
304
  batchApi,
@@ -445,6 +343,10 @@ async function startInteractive(apiKey, baseURL, model, maxToolRounds, batchApi,
445
343
  // process.exit(1) (see the unhandledRejection handler above). Cleared in
446
344
  // restoreTerminalSync() the moment we begin tearing the terminal back down.
447
345
  setTuiActive(true);
346
+ // Armed only once the TUI owns the terminal: this hunts a freeze of the
347
+ // interactive session, and the logger only routes to debug.log (instead of
348
+ // stdout, which would corrupt the framebuffer) once the TUI is active.
349
+ await startFreezeDiagnostics();
448
350
  // Deliberate debug-console toggle. With openConsoleOnError:false the overlay
449
351
  // never auto-pops, so F12 is the ONLY way in — and, crucially, back out:
450
352
  // console.toggle() hides it when it is visible+focused, so it can never trap.
@@ -761,7 +663,7 @@ async function runBackgroundDelegation(jobPath, options) {
761
663
  const model = explicitModel ? normalizeModelId(explicitModel) : undefined;
762
664
  // Resolve API key: explicit flag > legacy env/settings.apiKey > per-provider keychain
763
665
  // (matches the foreground flow — delegations were previously broken when the user
764
- // only had a per-provider key in the OS keychain, e.g. deepseek.)
666
+ // only had a per-provider key in the environment, e.g. deepseek.)
765
667
  let apiKey = stringOption(options.apiKey) || getApiKey();
766
668
  if (!apiKey) {
767
669
  const modelForResolve = model ?? delegation.model ?? getCurrentModel("agent");
@@ -830,8 +732,8 @@ function resolveConfig(options) {
830
732
  // flag or MUONROI_MAX_TOOL_ROUNDS env.
831
733
  const maxToolRounds = parseInt(stringOption(options.maxToolRounds) || "100", 10) || 100;
832
734
  if (typeof options.apiKey === "string" && process.env.MUONROI_TEST_NO_PERSIST !== "1") {
833
- // Persist to OS keychain (per-provider) instead of plaintext settings.json.
834
- // Fire-and-forget: keychain write is async; if it fails (no keytar), the key still
735
+ // Persist to the env-store (per-provider) instead of plaintext settings.json.
736
+ // Fire-and-forget: env-store write is sync; the key is also set in process.env so it
835
737
  // works for this run via `apiKey` above and the user can re-supply it next invocation.
836
738
  void persistApiKeyToKeychain(options.apiKey, stringOption(options.model)).catch(() => { });
837
739
  }
@@ -1015,41 +917,43 @@ program
1015
917
  await loadCatalog().catch(() => {
1016
918
  catalogLoadFailed = true;
1017
919
  });
920
+ // Load the env-store (~/.muonroi-cli/.env) into process.env and migrate any
921
+ // legacy keychain/settings keys BEFORE any key resolution. Best-effort.
922
+ try {
923
+ const { loadEnvFileIntoProcess } = await import("./providers/env-store.js");
924
+ loadEnvFileIntoProcess();
925
+ const { migrateLegacyKeysToEnv } = await import("./providers/keychain.js");
926
+ await migrateLegacyKeysToEnv();
927
+ }
928
+ catch (err) {
929
+ console.error(`[muonroi-cli] env-store init failed: ${err instanceof Error ? err.message : String(err)}`);
930
+ }
1018
931
  if (options.backgroundTaskFile) {
1019
932
  await runBackgroundDelegation(options.backgroundTaskFile, options);
1020
933
  return;
1021
934
  }
1022
935
  const config = resolveConfig(options);
1023
- // No legacy key in env / settings try the OS keychain for the resolved
1024
- // model's provider before falling back to the wizard.
1025
- if (!config.apiKey) {
936
+ // One auth mode per provider (OAuth XOR API key): if the resolved model's
937
+ // provider has an OAuth token, OAuth wins OUTRIGHT — a stored API key
938
+ // (env/flag/settings) is ignored, never blended. Only when there is no
939
+ // OAuth token do we fall back to the provider's env API key. The "oauth"
940
+ // sentinel satisfies downstream gating (which only checks for a truthy
941
+ // apiKey); the runtime injects Bearer headers.
942
+ {
1026
943
  const modelForResolve = config.model ?? getCurrentModel("agent");
1027
- const keychainKey = await resolveKeyForModel(modelForResolve);
1028
- if (keychainKey) {
1029
- config.apiKey = keychainKey;
1030
- }
1031
- else if (await hasOAuthForModel(modelForResolve)) {
1032
- // OAuth-authenticated provider — runtime will inject Bearer headers.
1033
- // Set placeholder so downstream gating code (which only checks for
1034
- // a truthy apiKey) is satisfied.
944
+ if (await hasOAuthForModel(modelForResolve)) {
1035
945
  config.apiKey = "oauth";
1036
946
  }
1037
- }
1038
- // First-run wizard (interactive only, before any TUI code)
1039
- const isInteractive = !options.prompt && !options.verify && process.stdin.isTTY;
1040
- if (!config.apiKey && isInteractive) {
1041
- const modelForWizard = config.model ?? getCurrentModel("agent");
1042
- const wizardKey = await firstRunWizard(modelForWizard);
1043
- if (wizardKey) {
1044
- // Key is already persisted to the OS keychain by the wizard. We DO NOT
1045
- // write it back to settings.json (avoids resurrecting plaintext that
1046
- // 'keys cleanup-settings' just removed).
1047
- config.apiKey = wizardKey;
1048
- }
1049
- else {
1050
- process.exit(1);
947
+ else if (!config.apiKey) {
948
+ const envKey = await resolveKeyForModel(modelForResolve);
949
+ if (envKey)
950
+ config.apiKey = envKey;
1051
951
  }
1052
952
  }
953
+ // First run with no credentials does NOT block: boot straight into the TUI.
954
+ // The in-chat provider picker (/providers) handles auth on demand.
955
+ // Headless (`--prompt`/`--verify`) with no key still fails via requireApiKey.
956
+ const isInteractive = !options.prompt && !options.verify && process.stdin.isTTY;
1053
957
  // Bootstrap EE auth (loads serverBaseUrl + token from ~/.experience/config.json)
1054
958
  const { loadEEAuthToken, getCachedServerBaseUrl } = await import("./ee/auth.js");
1055
959
  await loadEEAuthToken().catch(() => { });
@@ -1253,38 +1157,28 @@ program
1253
1157
  });
1254
1158
  const keys = program
1255
1159
  .command("keys")
1256
- .description("Manage provider API keys via the OS keychain (set, list, delete, import-bw)");
1160
+ .description("Manage provider API keys as environment variables (set, list, delete)");
1257
1161
  keys
1258
1162
  .command("set <provider>")
1259
- .description("Prompt for a provider API key and store it in the OS keychain")
1260
- .option("--bw", "Also write the key to a Bitwarden vault Secure Note (requires bw CLI + BW_SESSION)")
1261
- .option("--prefix <prefix>", "Bitwarden item name prefix when --bw is set (default: 'muonroi-cli/')", "muonroi-cli/")
1262
- .action(async (provider, opts) => {
1163
+ .description("Prompt for a provider API key and store it as an environment variable")
1164
+ .action(async (provider) => {
1263
1165
  const { runKeysSet } = await import("./cli/keys.js");
1264
- await runKeysSet(provider, { bw: opts.bw, itemPrefix: opts.prefix });
1166
+ await runKeysSet(provider);
1265
1167
  });
1266
1168
  keys
1267
1169
  .command("list")
1268
- .description("Show provider keys currently stored in the OS keychain (masked)")
1170
+ .description("Show provider keys currently set in the environment (masked)")
1269
1171
  .action(async () => {
1270
1172
  const { runKeysList } = await import("./cli/keys.js");
1271
1173
  await runKeysList();
1272
1174
  });
1273
1175
  keys
1274
1176
  .command("delete <provider>")
1275
- .description("Delete a stored provider key from the OS keychain")
1177
+ .description("Delete a stored provider key from the environment")
1276
1178
  .action(async (provider) => {
1277
1179
  const { runKeysDelete } = await import("./cli/keys.js");
1278
1180
  await runKeysDelete(provider);
1279
1181
  });
1280
- keys
1281
- .command("import-bw [providers...]")
1282
- .description("Import keys from a Bitwarden vault into the OS keychain (requires bw CLI + BW_SESSION)")
1283
- .option("--prefix <prefix>", "Vault item name prefix (default: 'muonroi-cli/')", "muonroi-cli/")
1284
- .action(async (providers, opts) => {
1285
- const { runKeysImportBw } = await import("./cli/keys.js");
1286
- await runKeysImportBw({ providers, itemPrefix: opts.prefix });
1287
- });
1288
1182
  keys
1289
1183
  .command("login <provider>")
1290
1184
  .description("Log in to a provider via OAuth subscription (supported: openai, xai).")
@@ -1301,28 +1195,21 @@ keys
1301
1195
  });
1302
1196
  keys
1303
1197
  .command("export <file>")
1304
- .description("Export all keychain keys to an encrypted portable bundle (move between devices)")
1198
+ .description("Export all provider keys to an encrypted portable bundle (move between devices)")
1305
1199
  .action(async (file) => {
1306
1200
  const { runKeysExport } = await import("./cli/keys.js");
1307
1201
  await runKeysExport(file);
1308
1202
  });
1309
1203
  keys
1310
1204
  .command("import <file>")
1311
- .description("Import an encrypted bundle into the OS keychain (created by 'keys export')")
1205
+ .description("Import an encrypted bundle of provider keys (created by 'keys export')")
1312
1206
  .action(async (file) => {
1313
1207
  const { runKeysImport } = await import("./cli/keys.js");
1314
1208
  await runKeysImport(file);
1315
1209
  });
1316
- keys
1317
- .command("cleanup-settings")
1318
- .description("Strip plaintext API keys out of ~/.muonroi-cli/user-settings.json after migrating to keychain")
1319
- .action(async () => {
1320
- const { runKeysCleanupSettings } = await import("./cli/keys.js");
1321
- await runKeysCleanupSettings();
1322
- });
1323
1210
  keys
1324
1211
  .command("set-chat <id>")
1325
- .description("Set a chat-service secret (discord-token, discord-guild-id, slack-token, slack-team-id) in the OS keychain")
1212
+ .description("Set a chat-service secret (discord-token, discord-guild-id, slack-token, slack-team-id) in the environment")
1326
1213
  .action(async (id) => {
1327
1214
  const { runChatKeySet } = await import("./cli/keys.js");
1328
1215
  if (!["discord-token", "discord-guild-id", "slack-token", "slack-team-id"].includes(id)) {
@@ -1342,14 +1229,6 @@ keys
1342
1229
  }
1343
1230
  await runChatKeySet(id, value);
1344
1231
  });
1345
- keys
1346
- .command("import-bw-chat [ids...]")
1347
- .option("--prefix <prefix>", "BW item name prefix", "muonroi-cli/chat-")
1348
- .description("Import chat-service secrets from Bitwarden vault into OS keychain")
1349
- .action(async (ids, opts) => {
1350
- const { runChatImportBw } = await import("./cli/keys.js");
1351
- await runChatImportBw({ ids: ids.length > 0 ? ids : undefined, itemPrefix: opts.prefix });
1352
- });
1353
1232
  const usage = program.command("usage").description("Inspect cost ledger and find spend bloat");
1354
1233
  usage
1355
1234
  .command("report")
@@ -1443,20 +1322,10 @@ mcp
1443
1322
  });
1444
1323
  mcp
1445
1324
  .command("set <id>")
1446
- .description("Prompt for an MCP API key (e.g. tavily) and store it in the OS keychain")
1447
- .option("--bw", "Also write the key to a Bitwarden vault Secure Note (requires bw CLI + BW_SESSION)")
1448
- .option("--prefix <prefix>", "Bitwarden item name prefix when --bw is set (default: 'muonroi-cli/')", "muonroi-cli/")
1449
- .action(async (id, opts) => {
1325
+ .description("Prompt for an MCP API key (e.g. tavily) and store it as an environment variable")
1326
+ .action(async (id) => {
1450
1327
  const { runMcpKeysSet } = await import("./cli/keys.js");
1451
- await runMcpKeysSet(id, { bw: opts.bw, itemPrefix: opts.prefix });
1452
- });
1453
- mcp
1454
- .command("import-bw [keys...]")
1455
- .description("Import MCP secrets (e.g. tavily) from a Bitwarden vault into the OS keychain (requires bw CLI + BW_SESSION)")
1456
- .option("--prefix <prefix>", "Vault item name prefix (default: 'muonroi-cli/')", "muonroi-cli/")
1457
- .action(async (keys, opts) => {
1458
- const { runMcpImportBw } = await import("./cli/keys.js");
1459
- await runMcpImportBw({ keys, itemPrefix: opts.prefix });
1328
+ await runMcpKeysSet(id);
1460
1329
  });
1461
1330
  program
1462
1331
  .command("mcp-driver")
@@ -342,7 +342,9 @@ async function resolveNodeServerLaunch(root, binary, packageName, baseArgs, auto
342
342
  }
343
343
  if (!autoInstall)
344
344
  return null;
345
- const cached = await lspNpmWhich(packageName);
345
+ // Pass `binary` explicitly: a package can ship several bins and the one named
346
+ // after the package is not always the language server (see lspNpmWhich).
347
+ const cached = await lspNpmWhich(packageName, binary);
346
348
  if (cached) {
347
349
  return { command: cached, args: baseArgs };
348
350
  }
@@ -1,6 +1,6 @@
1
1
  import { type RuntimeLspServerDefinition } from "./builtins.js";
2
2
  import { type LspClientSession } from "./client.js";
3
- import type { LspDiagnosticFile, LspQueryInput, LspToolResponse, NormalizedLspSettings } from "./types.js";
3
+ import type { ImpactOfChangeResult, LspDiagnosticFile, LspQueryInput, LspQueryResult, LspToolResponse, MutationPreviewResult, NormalizedLspSettings, PolicyAction } from "./types.js";
4
4
  interface WorkspaceLspManagerOptions {
5
5
  createClient?: (input: {
6
6
  serverId: string;
@@ -13,6 +13,10 @@ export interface WorkspaceLspManager {
13
13
  touchFile(filePath: string, waitForDiagnostics?: boolean): Promise<LspDiagnosticFile[]>;
14
14
  syncFile(filePath: string, content: string, save?: boolean, waitForDiagnostics?: boolean, diagnosticsTimeoutMs?: number): Promise<LspDiagnosticFile[]>;
15
15
  query(input: LspQueryInput): Promise<LspToolResponse>;
16
+ waitForDiagnostics(filePath: string, timeout: number): Promise<LspQueryResult>;
17
+ impactOfChange(filePath: string, query?: string, timeout?: number): Promise<ImpactOfChangeResult>;
18
+ lspMutationPreview(filePath: string, change: string): Promise<MutationPreviewResult>;
19
+ lspBeforeGrep(filePath: string, query?: string): Promise<PolicyAction>;
16
20
  close(): Promise<void>;
17
21
  }
18
22
  export declare function createWorkspaceLspManager(cwd: string, settings: NormalizedLspSettings, options?: WorkspaceLspManagerOptions): WorkspaceLspManager;