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
@@ -32,6 +32,10 @@ export interface IterationState {
32
32
  criteriaMet: number;
33
33
  criteriaPartial: number;
34
34
  criteriaUnmet: number;
35
+ /** Total criteria evaluated this sprint. Optional for back-compat with older
36
+ * persisted iterations; when present it fixes the phase-runner exit ratio + the
37
+ * sprint-review "met X/Y" count that previously read undefined. */
38
+ totalCriteria?: number;
35
39
  costUsd: number;
36
40
  lastVerifyResult: string;
37
41
  /** Alias of costUsd kept for circuit-breaker history adapters (CB-1 reads actualCost). */
@@ -40,6 +44,16 @@ export interface IterationState {
40
44
  score?: number;
41
45
  crashed?: boolean;
42
46
  retryOf?: number;
47
+ /**
48
+ * Carry-over focus for the NEXT sprint when this one did not satisfy the
49
+ * done-gate: the failed-condition focus (feedback-routing) plus any plan
50
+ * deviations that survived the adherence review. The phase-runner adapter
51
+ * threads this into the next sprint's `carryOver` so each iteration continues
52
+ * the specific unmet / risky parts instead of restarting blind — the mechanism
53
+ * behind "spawn to continue the risk parts → finished product after N sprints".
54
+ * Absent when the sprint passed.
55
+ */
56
+ nextFocus?: string;
43
57
  }
44
58
  export interface DoneVerdict {
45
59
  pass: boolean;
@@ -278,6 +292,17 @@ export interface ProjectContext {
278
292
  idea: string;
279
293
  detection: ExistingProjectSignals;
280
294
  context: DiscoveryContext;
295
+ /**
296
+ * Agent-driven interview output. When the gather phase runs the unified
297
+ * clarifier (the same LLM-generates-its-own-questions engine `/council`
298
+ * uses — see runGatherPhase), it produces a rich `ClarifiedSpec` directly
299
+ * instead of the fixed-question `DiscoveryContext`. Stashed here (plain data,
300
+ * JSON round-trips through project-context.md) so `clarifiedSpecFromContext`
301
+ * can return the real LLM-synthesized successCriteria/constraints/scope
302
+ * verbatim rather than re-deriving a thin 2-item spec from persona+scale.
303
+ * Absent on the legacy fixed-question path (MUONROI_IDEAL_AGENT_INTERVIEW=0).
304
+ */
305
+ clarified?: import("../council/types.js").ClarifiedSpec;
281
306
  recommendations: {
282
307
  byField: Record<string, RecommendationEntry>;
283
308
  constraints: {
@@ -2,16 +2,15 @@
2
2
  * src/providers/anthropic.ts
3
3
  *
4
4
  * Anthropic-only provider shell for Phase 0.
5
- * Implements TUI-02 (stub conversation), PROV-03 (BYOK keychain), PROV-07 (log redaction).
5
+ * Implements TUI-02 (stub conversation), PROV-03 (BYOK), PROV-07 (log redaction).
6
6
  *
7
- * Key loading priority:
8
- * 1. OS keychain via keytar (service="muonroi-cli", account="anthropic") — B-2: dynamic import
9
- * 2. ANTHROPIC_API_KEY env var fallback with redacted warning
10
- * 3. AnthropicKeyMissingError — user-facing error with remediation instructions
7
+ * Key loading:
8
+ * 1. ANTHROPIC_API_KEY env var (the env-store loads ~/.muonroi-cli/.env into
9
+ * process.env at startup; the real OS env also applies)
10
+ * 2. AnthropicKeyMissingError — user-facing error with remediation instructions
11
11
  *
12
12
  * Security invariants:
13
13
  * - redactor.enrollSecret(key) is called BEFORE any log line that might contain the key.
14
- * - keytar is loaded via dynamic import() so a missing/broken native module cannot crash boot.
15
14
  * - Keys shorter than 20 characters are rejected early.
16
15
  *
17
16
  * Streaming (AI SDK v6 with @ai-sdk/anthropic):
@@ -32,13 +31,15 @@ export declare class AnthropicKeyMissingError extends Error {
32
31
  constructor();
33
32
  }
34
33
  /**
35
- * Load the Anthropic API key from the OS keychain first, then env var fallback.
34
+ * Load the Anthropic API key from the environment (the env-store loads
35
+ * `~/.muonroi-cli/.env` into process.env at startup; the real OS env also
36
+ * applies).
36
37
  *
37
38
  * Security contract:
38
39
  * - redactor.enrollSecret(key) is called BEFORE any subsequent log line.
39
40
  * - Keys < 20 chars are rejected (T-00.05-05: truncated key guard).
40
41
  *
41
- * @throws {AnthropicKeyMissingError} when both keychain and env var are absent.
42
+ * @throws {AnthropicKeyMissingError} when no ANTHROPIC_API_KEY is present.
42
43
  */
43
44
  export declare function loadAnthropicKey(): Promise<string>;
44
45
  /**
@@ -2,16 +2,15 @@
2
2
  * src/providers/anthropic.ts
3
3
  *
4
4
  * Anthropic-only provider shell for Phase 0.
5
- * Implements TUI-02 (stub conversation), PROV-03 (BYOK keychain), PROV-07 (log redaction).
5
+ * Implements TUI-02 (stub conversation), PROV-03 (BYOK), PROV-07 (log redaction).
6
6
  *
7
- * Key loading priority:
8
- * 1. OS keychain via keytar (service="muonroi-cli", account="anthropic") — B-2: dynamic import
9
- * 2. ANTHROPIC_API_KEY env var fallback with redacted warning
10
- * 3. AnthropicKeyMissingError — user-facing error with remediation instructions
7
+ * Key loading:
8
+ * 1. ANTHROPIC_API_KEY env var (the env-store loads ~/.muonroi-cli/.env into
9
+ * process.env at startup; the real OS env also applies)
10
+ * 2. AnthropicKeyMissingError — user-facing error with remediation instructions
11
11
  *
12
12
  * Security invariants:
13
13
  * - redactor.enrollSecret(key) is called BEFORE any log line that might contain the key.
14
- * - keytar is loaded via dynamic import() so a missing/broken native module cannot crash boot.
15
14
  * - Keys shorter than 20 characters are rejected early.
16
15
  *
17
16
  * Streaming (AI SDK v6 with @ai-sdk/anthropic):
@@ -43,58 +42,25 @@ export class AnthropicKeyMissingError extends Error {
43
42
  this.name = "AnthropicKeyMissingError";
44
43
  }
45
44
  }
45
+ // ---------------------------------------------------------------------------
46
+ // Key loader
47
+ // ---------------------------------------------------------------------------
46
48
  /**
47
- * Dynamic keytar loader B-2 mitigation.
48
- * A missing or broken keytar native module (common on minimal Linux environments)
49
- * must NOT crash process boot. The env-var fallback path is always available.
50
- */
51
- async function loadKeytar() {
52
- try {
53
- // keytar exports named functions directly (no default export)
54
- const mod = await import("keytar");
55
- return mod;
56
- }
57
- catch {
58
- return null;
59
- }
60
- }
61
- /**
62
- * Load the Anthropic API key from the OS keychain first, then env var fallback.
49
+ * Load the Anthropic API key from the environment (the env-store loads
50
+ * `~/.muonroi-cli/.env` into process.env at startup; the real OS env also
51
+ * applies).
63
52
  *
64
53
  * Security contract:
65
54
  * - redactor.enrollSecret(key) is called BEFORE any subsequent log line.
66
55
  * - Keys < 20 chars are rejected (T-00.05-05: truncated key guard).
67
56
  *
68
- * @throws {AnthropicKeyMissingError} when both keychain and env var are absent.
57
+ * @throws {AnthropicKeyMissingError} when no ANTHROPIC_API_KEY is present.
69
58
  */
70
59
  export async function loadAnthropicKey() {
71
- // --- Primary: OS keychain via dynamic import (B-2) ---
72
- const keytarMod = await loadKeytar();
73
- if (keytarMod) {
74
- try {
75
- const key = await keytarMod.getPassword("muonroi-cli", "anthropic");
76
- if (key && key.length >= 20) {
77
- // Enroll BEFORE any log that might emit the key
78
- redactor.enrollSecret(key);
79
- return key;
80
- }
81
- }
82
- catch (err) {
83
- // keytar present but backend unavailable (e.g. Linux without libsecret/dbus)
84
- console.warn("[muonroi-cli] keytar backend unavailable, falling back to env var:", redactor.redactError(err));
85
- }
86
- }
87
- else {
88
- console.warn("[muonroi-cli] keytar module not installed — using env-var path. " +
89
- "Install keytar to enable OS keychain key storage.");
90
- }
91
- // --- Fallback: ANTHROPIC_API_KEY environment variable ---
92
60
  const envKey = process.env.ANTHROPIC_API_KEY;
93
61
  if (envKey && envKey.length >= 20) {
94
- // Enroll BEFORE the warning that mentions we're using env path
62
+ // Enroll BEFORE any log that might mention the key path.
95
63
  redactor.enrollSecret(envKey);
96
- console.warn("[muonroi-cli] Using ANTHROPIC_API_KEY from environment. " +
97
- "Prefer storing the key in your OS keychain (Phase 1 `muonroi-cli login` helper).");
98
64
  return envKey;
99
65
  }
100
66
  throw new AnthropicKeyMissingError();
@@ -49,6 +49,7 @@ export declare class GrokOAuthProvider implements ProviderOAuth {
49
49
  });
50
50
  login(opts?: {
51
51
  onUserCode?: (code: string, url: string) => void;
52
+ signal?: AbortSignal;
52
53
  }): Promise<OAuthTokens>;
53
54
  refresh(tokens: OAuthTokens): Promise<OAuthTokens>;
54
55
  revoke(tokens: OAuthTokens): Promise<void>;
@@ -114,6 +114,7 @@ export class GrokOAuthProvider {
114
114
  const state = randomBytes(16).toString("base64url");
115
115
  const nonce = randomBytes(16).toString("base64url");
116
116
  let callbackServer;
117
+ let manualPasteRl;
117
118
  let authCode;
118
119
  let receivedState = "";
119
120
  try {
@@ -125,6 +126,19 @@ export class GrokOAuthProvider {
125
126
  const loginTimeout = setTimeout(() => {
126
127
  reject(new Error("OAuth browser callback timed out"));
127
128
  }, CALLBACK_TIMEOUT_MS);
129
+ // Cancelling must END the flow, not just abandon it: the `finally`
130
+ // below is what closes the loopback server (and the readline), and an
131
+ // abandoned server holds its port for the full callback timeout, so the
132
+ // next sign-in cannot bind.
133
+ const onAbort = () => {
134
+ clearTimeout(loginTimeout);
135
+ reject(new OAuthLoginError("xai", "Sign-in cancelled."));
136
+ };
137
+ if (opts.signal?.aborted) {
138
+ onAbort();
139
+ return;
140
+ }
141
+ opts.signal?.addEventListener("abort", onAbort, { once: true });
128
142
  this.callbackServerFn({
129
143
  onCode: (code, s) => {
130
144
  receivedState = s;
@@ -146,15 +160,23 @@ export class GrokOAuthProvider {
146
160
  });
147
161
  opts.onUserCode?.(authorizeUrl, authorizeUrl);
148
162
  this.openBrowserFn(authorizeUrl);
149
- // Simple manual token/code input fallback (when redirect fails and xAI page shows a code)
150
- if (process.stdin.isTTY) {
151
- const rl = readline.createInterface({ input: process.stdin });
163
+ // Manual code-paste fallback (xAI's page sometimes shows a code
164
+ // instead of redirecting to the loopback URI).
165
+ //
166
+ // CLI only. `isRaw` means another consumer already owns stdin —
167
+ // inside the TUI that is OpenTUI's key handler. Attaching a readline
168
+ // there stole every keystroke, and on the normal HTTP-callback path
169
+ // it was never closed, so the TUI kept receiving nothing: Esc did
170
+ // not dismiss the provider dialog and only restarting the session
171
+ // recovered it. The console.log pair also printed into the TUI's
172
+ // alternate screen. Both are correct for `keys login`, wrong here.
173
+ if (process.stdin.isTTY && !process.stdin.isRaw) {
174
+ manualPasteRl = readline.createInterface({ input: process.stdin });
152
175
  console.log(' If the xAI page shows a code ("Could not establish connection") instead of redirecting,');
153
176
  console.log(" copy the code and paste it here then press Enter:");
154
- rl.on("line", (line) => {
177
+ manualPasteRl.on("line", (line) => {
155
178
  const c = line.trim();
156
179
  if (c.length > 30) {
157
- rl.close();
158
180
  clearTimeout(loginTimeout);
159
181
  resolve(c);
160
182
  }
@@ -174,6 +196,9 @@ export class GrokOAuthProvider {
174
196
  }
175
197
  finally {
176
198
  callbackServer?.close();
199
+ // Unconditional: the old code only closed this when the user actually
200
+ // pasted a code, so the normal HTTP-callback path left stdin captured.
201
+ manualPasteRl?.close();
177
202
  }
178
203
  if (receivedState && receivedState !== state) {
179
204
  throw new OAuthLoginError("xai", "OAuth state mismatch — possible CSRF, aborting.");
@@ -50,6 +50,7 @@ export declare class OpenAIOAuthProvider implements ProviderOAuth {
50
50
  });
51
51
  login(opts?: {
52
52
  onUserCode?: (code: string, url: string) => void;
53
+ signal?: AbortSignal;
53
54
  }): Promise<OAuthTokens>;
54
55
  refresh(tokens: OAuthTokens): Promise<OAuthTokens>;
55
56
  revoke(tokens: OAuthTokens): Promise<void>;
@@ -117,6 +117,20 @@ export class OpenAIOAuthProvider {
117
117
  const loginTimeout = setTimeout(() => {
118
118
  reject(new Error("OAuth browser callback timed out"));
119
119
  }, CALLBACK_TIMEOUT_MS);
120
+ // Cancelling used to abandon this promise rather than end it: the
121
+ // loopback server then held its port for the full CALLBACK_TIMEOUT_MS
122
+ // (5 min) across only two ports, so the next sign-in could not bind and
123
+ // the user had to restart the CLI. Rejecting here runs the `finally`
124
+ // below, which closes the server and frees the port immediately.
125
+ const onAbort = () => {
126
+ clearTimeout(loginTimeout);
127
+ reject(new OAuthLoginError("openai", "Sign-in cancelled."));
128
+ };
129
+ if (opts.signal?.aborted) {
130
+ onAbort();
131
+ return;
132
+ }
133
+ opts.signal?.addEventListener("abort", onAbort, { once: true });
120
134
  const tryPorts = async () => {
121
135
  let bindError;
122
136
  for (const port of CODEX_PORTS) {
@@ -2,10 +2,8 @@
2
2
  * src/providers/auth/token-store.ts
3
3
  *
4
4
  * Persistent store for provider OAuth tokens.
5
- * Primary: OS keychain via keytar (same loader as keychain.ts).
6
- * Account key: `oauth:<provider>`, service: "muonroi-cli".
7
- * Value: JSON-serialized OAuthTokens.
8
- * Fallback: ~/.muonroi-cli/auth/<provider>.json, mode 0600.
5
+ * Storage: ~/.muonroi-cli/auth/<provider>.json, mode 0600 (JSON-serialized
6
+ * OAuthTokens). The OS keychain (keytar) has been removed.
9
7
  *
10
8
  * All token fields are enrolled in the redactor immediately after load.
11
9
  */
@@ -14,15 +12,17 @@ import type { OAuthTokens } from "./types.js";
14
12
  * Enroll all token secrets so they are scrubbed from logs.
15
13
  */
16
14
  export declare function enrollTokensInRedactor(tokens: OAuthTokens): void;
15
+ /**
16
+ * Persist OAuth tokens for a provider to ~/.muonroi-cli/auth/<provider>.json
17
+ * (mode 0600).
18
+ */
17
19
  export declare function saveTokens(provider: string, tokens: OAuthTokens): Promise<void>;
18
20
  /**
19
- * Load OAuth tokens for a provider.
20
- * Returns null if no tokens are stored.
21
- * Enrolled in redactor on load.
21
+ * Load OAuth tokens for a provider from the 0600 file store.
22
+ * Returns null if no tokens are stored. Enrolled in redactor on load.
22
23
  */
23
24
  export declare function loadTokens(provider: string): Promise<OAuthTokens | null>;
24
25
  /**
25
- * Delete stored OAuth tokens for a provider.
26
- * Attempts both keychain and file removal (either may or may not exist).
26
+ * Delete stored OAuth tokens for a provider (removes the 0600 file).
27
27
  */
28
28
  export declare function deleteTokens(provider: string): Promise<void>;
@@ -2,10 +2,8 @@
2
2
  * src/providers/auth/token-store.ts
3
3
  *
4
4
  * Persistent store for provider OAuth tokens.
5
- * Primary: OS keychain via keytar (same loader as keychain.ts).
6
- * Account key: `oauth:<provider>`, service: "muonroi-cli".
7
- * Value: JSON-serialized OAuthTokens.
8
- * Fallback: ~/.muonroi-cli/auth/<provider>.json, mode 0600.
5
+ * Storage: ~/.muonroi-cli/auth/<provider>.json, mode 0600 (JSON-serialized
6
+ * OAuthTokens). The OS keychain (keytar) has been removed.
9
7
  *
10
8
  * All token fields are enrolled in the redactor immediately after load.
11
9
  */
@@ -13,10 +11,6 @@ import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
13
11
  import * as os from "node:os";
14
12
  import * as path from "node:path";
15
13
  import { redactor } from "../../utils/redactor.js";
16
- const KEYCHAIN_SERVICE = "muonroi-cli";
17
- function oauthAccount(provider) {
18
- return `oauth:${provider}`;
19
- }
20
14
  /**
21
15
  * Override for tests — set process.env.MUONROI_AUTH_DIR to point at a temp dir.
22
16
  */
@@ -38,71 +32,24 @@ export function enrollTokensInRedactor(tokens) {
38
32
  if (tokens.idToken)
39
33
  redactor.enrollSecret(tokens.idToken);
40
34
  }
41
- async function loadKeytar() {
42
- try {
43
- return (await import("keytar"));
44
- }
45
- catch {
46
- return null;
47
- }
48
- }
49
35
  // ---------------------------------------------------------------------------
50
36
  // Public API
51
37
  // ---------------------------------------------------------------------------
52
38
  /**
53
- * Persist OAuth tokens for a provider.
54
- * Tries keychain first; falls back to filesystem (0600).
39
+ * Persist OAuth tokens for a provider to ~/.muonroi-cli/auth/<provider>.json
40
+ * (mode 0600).
55
41
  */
56
- // Windows Credential Manager's stub rejects credentials larger than ~2.5 KiB
57
- // with "The stub received bad data" — OAuth blobs routinely exceed that, so
58
- // for anything over this threshold we skip keytar and go straight to the
59
- // file fallback (mode 0600).
60
- const KEYCHAIN_BLOB_LIMIT_BYTES = 2000;
61
42
  export async function saveTokens(provider, tokens) {
62
43
  enrollTokensInRedactor(tokens);
63
44
  const json = JSON.stringify(tokens);
64
- // Try keytar only for payloads small enough that Credential Manager will
65
- // accept them. For larger blobs, write to the 0600 file fallback directly.
66
- if (json.length <= KEYCHAIN_BLOB_LIMIT_BYTES) {
67
- const kt = await loadKeytar();
68
- if (kt) {
69
- try {
70
- await kt.setPassword(KEYCHAIN_SERVICE, oauthAccount(provider), json);
71
- return;
72
- }
73
- catch {
74
- // Keychain write failed (locked keyring, Credential Manager stub, etc.)
75
- // — fall through to the 0600 file fallback below. Never console.* here:
76
- // the TUI captures console output and OpenTUI's openConsoleOnError would
77
- // pop an un-dismissable overlay.
78
- }
79
- }
80
- }
81
45
  await mkdir(fallbackDir(), { recursive: true });
82
46
  await writeFile(fallbackPath(provider), json, { mode: 0o600, encoding: "utf8" });
83
47
  }
84
48
  /**
85
- * Load OAuth tokens for a provider.
86
- * Returns null if no tokens are stored.
87
- * Enrolled in redactor on load.
49
+ * Load OAuth tokens for a provider from the 0600 file store.
50
+ * Returns null if no tokens are stored. Enrolled in redactor on load.
88
51
  */
89
52
  export async function loadTokens(provider) {
90
- // Try keychain first
91
- const kt = await loadKeytar();
92
- if (kt) {
93
- try {
94
- const raw = await kt.getPassword(KEYCHAIN_SERVICE, oauthAccount(provider));
95
- if (raw) {
96
- const tokens = JSON.parse(raw);
97
- enrollTokensInRedactor(tokens);
98
- return tokens;
99
- }
100
- }
101
- catch {
102
- // fall through to file
103
- }
104
- }
105
- // Try file fallback
106
53
  try {
107
54
  const raw = await readFile(fallbackPath(provider), "utf8");
108
55
  const tokens = JSON.parse(raw);
@@ -110,23 +57,14 @@ export async function loadTokens(provider) {
110
57
  return tokens;
111
58
  }
112
59
  catch {
60
+ // Missing/unreadable file → no tokens stored.
113
61
  return null;
114
62
  }
115
63
  }
116
64
  /**
117
- * Delete stored OAuth tokens for a provider.
118
- * Attempts both keychain and file removal (either may or may not exist).
65
+ * Delete stored OAuth tokens for a provider (removes the 0600 file).
119
66
  */
120
67
  export async function deleteTokens(provider) {
121
- const kt = await loadKeytar();
122
- if (kt) {
123
- try {
124
- await kt.deletePassword(KEYCHAIN_SERVICE, oauthAccount(provider));
125
- }
126
- catch {
127
- // ignore
128
- }
129
- }
130
68
  try {
131
69
  await unlink(fallbackPath(provider));
132
70
  }
@@ -26,8 +26,16 @@ export interface ProviderOAuth {
26
26
  * Calls onUserCode with the human-readable code + verification URL so the
27
27
  * caller can display them to the user before polling.
28
28
  */
29
+ /**
30
+ * `signal` aborts an in-flight sign-in. Browser flows bind a loopback
31
+ * callback server on a fixed, tiny port set; without an abort the server
32
+ * outlives a cancelled sign-in for the full callback timeout and the next
33
+ * attempt cannot bind — which reads to the user as "OAuth is broken until I
34
+ * restart the CLI".
35
+ */
29
36
  login(opts: {
30
37
  onUserCode?: (code: string, url: string) => void;
38
+ signal?: AbortSignal;
31
39
  }): Promise<OAuthTokens>;
32
40
  /**
33
41
  * Exchange a refresh token for new tokens.
@@ -0,0 +1,17 @@
1
+ export declare function envFilePath(): string;
2
+ /**
3
+ * Upsert `name=value` in the `.env` store, set `process.env[name]` for the
4
+ * current process, and (on Windows) mirror to the User registry env.
5
+ */
6
+ export declare function persistEnvVar(name: string, value: string): void;
7
+ /** Remove `name` from the `.env` store, `process.env`, and Windows registry. */
8
+ export declare function clearEnvVar(name: string): void;
9
+ /**
10
+ * Load the `.env` store into `process.env` at startup.
11
+ *
12
+ * For ordinary variables the ambient OS environment stays authoritative and
13
+ * `.env` only fills gaps. For `*_API_KEY` credentials the CLI writes itself,
14
+ * `.env` wins and the conflict is logged — see {@link ambientWinsForSecrets}
15
+ * for why, and `MUONROI_ENV_PRECEDENCE=ambient` to opt out.
16
+ */
17
+ export declare function loadEnvFileIntoProcess(): void;