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.
- package/dist/packages/agent-harness-core/src/event-filter.js +11 -0
- package/dist/packages/agent-harness-core/src/event-redact.js +7 -0
- package/dist/packages/agent-harness-core/src/event-tee.d.ts +20 -4
- package/dist/packages/agent-harness-core/src/event-tee.js +32 -5
- package/dist/packages/agent-harness-core/src/mcp-server.d.ts +14 -0
- package/dist/packages/agent-harness-core/src/mcp-server.js +86 -10
- package/dist/packages/agent-harness-core/src/predicate.d.ts +1 -1
- package/dist/packages/agent-harness-core/src/protocol.d.ts +24 -2
- package/dist/src/agent-harness/mock-model.d.ts +10 -0
- package/dist/src/agent-harness/mock-model.js +6 -2
- package/dist/src/chat/chat-keychain.d.ts +7 -12
- package/dist/src/chat/chat-keychain.js +19 -86
- package/dist/src/cli/keys.d.ts +8 -45
- package/dist/src/cli/keys.js +18 -324
- package/dist/src/council/clarifier.d.ts +4 -2
- package/dist/src/council/clarifier.js +158 -36
- package/dist/src/council/debate-planner.js +3 -2
- package/dist/src/council/debate.js +59 -5
- package/dist/src/council/index.d.ts +23 -0
- package/dist/src/council/index.js +37 -2
- package/dist/src/council/llm.d.ts +62 -0
- package/dist/src/council/llm.js +123 -23
- package/dist/src/council/panel-select.js +13 -3
- package/dist/src/council/planner.js +17 -0
- package/dist/src/council/preflight.d.ts +10 -0
- package/dist/src/council/preflight.js +36 -0
- package/dist/src/council/prompts.d.ts +9 -1
- package/dist/src/council/prompts.js +23 -6
- package/dist/src/council/types.d.ts +10 -0
- package/dist/src/ee/auth.d.ts +19 -0
- package/dist/src/ee/auth.js +39 -0
- package/dist/src/ee/client.js +28 -3
- package/dist/src/ee/ee-onboarding.js +6 -26
- package/dist/src/flow/compaction/compress.d.ts +2 -2
- package/dist/src/flow/compaction/compress.js +21 -8
- package/dist/src/flow/compaction/extract.d.ts +3 -3
- package/dist/src/flow/compaction/extract.js +6 -6
- package/dist/src/flow/compaction/index.d.ts +2 -1
- package/dist/src/flow/compaction/index.js +29 -3
- package/dist/src/flow/compaction/progress.d.ts +35 -0
- package/dist/src/flow/compaction/progress.js +35 -0
- package/dist/src/generated/version.d.ts +1 -1
- package/dist/src/generated/version.js +1 -1
- package/dist/src/gsd/flags.d.ts +11 -0
- package/dist/src/gsd/flags.js +19 -0
- package/dist/src/gsd/plan-council.js +104 -72
- package/dist/src/gsd/verdict-schema.d.ts +1 -1
- package/dist/src/headless/council-answers.js +4 -0
- package/dist/src/index.js +129 -260
- package/dist/src/lsp/builtins.js +3 -1
- package/dist/src/lsp/manager.d.ts +5 -1
- package/dist/src/lsp/manager.js +249 -3
- package/dist/src/lsp/npm-cache.d.ts +11 -1
- package/dist/src/lsp/npm-cache.js +17 -1
- package/dist/src/lsp/runtime.d.ts +6 -1
- package/dist/src/lsp/runtime.js +17 -1
- package/dist/src/lsp/types.d.ts +83 -1
- package/dist/src/lsp/types.js +10 -0
- package/dist/src/mcp/client-pool.js +43 -15
- package/dist/src/mcp/lsp-tools.d.ts +5 -1
- package/dist/src/mcp/lsp-tools.js +93 -2
- package/dist/src/mcp/mcp-keychain.d.ts +3 -5
- package/dist/src/mcp/mcp-keychain.js +9 -49
- package/dist/src/mcp/setup-guide-text.d.ts +1 -1
- package/dist/src/mcp/setup-guide-text.js +22 -2
- package/dist/src/mcp/tools-server.d.ts +10 -0
- package/dist/src/mcp/tools-server.js +10 -2
- package/dist/src/models/catalog.json +19 -19
- package/dist/src/orchestrator/ask-user.d.ts +61 -0
- package/dist/src/orchestrator/ask-user.js +65 -0
- package/dist/src/orchestrator/compaction.d.ts +2 -3
- package/dist/src/orchestrator/compaction.js +8 -8
- package/dist/src/orchestrator/council-manager.js +9 -8
- package/dist/src/orchestrator/council-request.d.ts +49 -0
- package/dist/src/orchestrator/council-request.js +62 -0
- package/dist/src/orchestrator/interactive-pause.d.ts +26 -0
- package/dist/src/orchestrator/interactive-pause.js +36 -0
- package/dist/src/orchestrator/message-processor.d.ts +4 -0
- package/dist/src/orchestrator/message-processor.js +26 -8
- package/dist/src/orchestrator/orchestrator.d.ts +25 -0
- package/dist/src/orchestrator/orchestrator.js +204 -50
- package/dist/src/orchestrator/preprocessor.js +2 -2
- package/dist/src/orchestrator/safety-askcard.d.ts +1 -1
- package/dist/src/orchestrator/safety-askcard.js +5 -2
- package/dist/src/orchestrator/safety-intercept.d.ts +5 -0
- package/dist/src/orchestrator/safety-intercept.js +7 -0
- package/dist/src/orchestrator/stall-watchdog.d.ts +8 -1
- package/dist/src/orchestrator/stall-watchdog.js +24 -3
- package/dist/src/orchestrator/stream-runner.d.ts +13 -3
- package/dist/src/orchestrator/stream-runner.js +54 -21
- package/dist/src/orchestrator/tool-engine.d.ts +19 -0
- package/dist/src/orchestrator/tool-engine.js +241 -25
- package/dist/src/orchestrator/turn-watchdog.d.ts +7 -0
- package/dist/src/orchestrator/turn-watchdog.js +38 -9
- package/dist/src/pil/agent-operating-contract.d.ts +1 -1
- package/dist/src/pil/agent-operating-contract.js +6 -4
- package/dist/src/pil/discovery.d.ts +1 -1
- package/dist/src/pil/discovery.js +2 -2
- package/dist/src/pil/layer1_5-complexity-size.d.ts +7 -0
- package/dist/src/pil/layer1_5-complexity-size.js +31 -5
- package/dist/src/pil/llm-classify.d.ts +78 -3
- package/dist/src/pil/llm-classify.js +351 -111
- package/dist/src/pil/native-capabilities-workbook.d.ts +1 -1
- package/dist/src/pil/native-capabilities-workbook.js +7 -0
- package/dist/src/pil/pipeline.js +2 -0
- package/dist/src/pil/repo-grounding-probe.d.ts +15 -0
- package/dist/src/pil/repo-grounding-probe.js +136 -0
- package/dist/src/pil/repo-structure-hints.d.ts +7 -0
- package/dist/src/pil/repo-structure-hints.js +45 -0
- package/dist/src/product-loop/artifact-io.js +4 -0
- package/dist/src/product-loop/criteria-seed.d.ts +51 -0
- package/dist/src/product-loop/criteria-seed.js +200 -0
- package/dist/src/product-loop/discovery-interview.d.ts +9 -0
- package/dist/src/product-loop/discovery-interview.js +37 -18
- package/dist/src/product-loop/discovery-recommender.js +2 -1
- package/dist/src/product-loop/discovery-schema.js +14 -1
- package/dist/src/product-loop/discovery-triage.d.ts +23 -0
- package/dist/src/product-loop/discovery-triage.js +109 -0
- package/dist/src/product-loop/gather.js +150 -2
- package/dist/src/product-loop/index.js +7 -0
- package/dist/src/product-loop/loop-driver.js +21 -8
- package/dist/src/product-loop/phase-plan.d.ts +16 -0
- package/dist/src/product-loop/phase-plan.js +42 -4
- package/dist/src/product-loop/phase-rituals.d.ts +3 -0
- package/dist/src/product-loop/phase-rituals.js +8 -3
- package/dist/src/product-loop/phase-runner.js +30 -11
- package/dist/src/product-loop/plan-adherence-review.d.ts +26 -0
- package/dist/src/product-loop/plan-adherence-review.js +144 -0
- package/dist/src/product-loop/sprint-runner.d.ts +62 -0
- package/dist/src/product-loop/sprint-runner.js +309 -8
- package/dist/src/product-loop/types.d.ts +25 -0
- package/dist/src/providers/anthropic.d.ts +9 -8
- package/dist/src/providers/anthropic.js +13 -47
- package/dist/src/providers/auth/grok-oauth.d.ts +1 -0
- package/dist/src/providers/auth/grok-oauth.js +30 -5
- package/dist/src/providers/auth/openai-oauth.d.ts +1 -0
- package/dist/src/providers/auth/openai-oauth.js +14 -0
- package/dist/src/providers/auth/token-store.d.ts +9 -9
- package/dist/src/providers/auth/token-store.js +8 -70
- package/dist/src/providers/auth/types.d.ts +8 -0
- package/dist/src/providers/env-store.d.ts +17 -0
- package/dist/src/providers/env-store.js +228 -0
- package/dist/src/providers/keychain.d.ts +21 -17
- package/dist/src/providers/keychain.js +124 -135
- package/dist/src/providers/runtime.d.ts +24 -9
- package/dist/src/providers/runtime.js +48 -37
- package/dist/src/providers/strategies/thinking-mode.js +9 -1
- package/dist/src/providers/strategies/xai.strategy.js +27 -0
- package/dist/src/providers/warm.d.ts +65 -0
- package/dist/src/providers/warm.js +145 -0
- package/dist/src/self-qa/agentic-loop.js +3 -2
- package/dist/src/storage/transcript.js +56 -2
- package/dist/src/tools/git-safety.d.ts +19 -0
- package/dist/src/tools/git-safety.js +168 -0
- package/dist/src/tools/native-tools.d.ts +1 -1
- package/dist/src/tools/native-tools.js +76 -1
- package/dist/src/tools/registry.d.ts +17 -0
- package/dist/src/tools/registry.js +116 -1
- package/dist/src/types/index.d.ts +29 -1
- package/dist/src/ui/app.js +67 -4
- package/dist/src/ui/components/agent-rail-activities.d.ts +26 -0
- package/dist/src/ui/components/agent-rail-activities.js +47 -0
- package/dist/src/ui/components/compact-progress-card.d.ts +24 -0
- package/dist/src/ui/components/compact-progress-card.js +42 -0
- package/dist/src/ui/components/council-phase-timeline.js +17 -2
- package/dist/src/ui/components/council-question-card.js +1 -0
- package/dist/src/ui/components/message-view.d.ts +15 -0
- package/dist/src/ui/components/message-view.js +50 -1
- package/dist/src/ui/components/tool-group.d.ts +15 -3
- package/dist/src/ui/components/tool-group.js +69 -11
- package/dist/src/ui/containers/modals-layer.d.ts +2 -1
- package/dist/src/ui/containers/modals-layer.js +2 -2
- package/dist/src/ui/council-harness-event.d.ts +57 -0
- package/dist/src/ui/council-harness-event.js +46 -0
- package/dist/src/ui/heartbeat-debug.d.ts +29 -0
- package/dist/src/ui/heartbeat-debug.js +45 -0
- package/dist/src/ui/modals/api-key-modal.js +1 -1
- package/dist/src/ui/modals/model-picker-modal.d.ts +8 -18
- package/dist/src/ui/modals/model-picker-modal.js +8 -10
- package/dist/src/ui/slash/ee.js +81 -0
- package/dist/src/ui/slash/menu-items.js +11 -2
- package/dist/src/ui/use-app-logic.js +354 -224
- package/dist/src/ui/utils/agent-activities.d.ts +39 -0
- package/dist/src/ui/utils/agent-activities.js +96 -0
- package/dist/src/ui/utils/group-tool-entries.d.ts +26 -0
- package/dist/src/ui/utils/group-tool-entries.js +111 -0
- package/dist/src/ui/utils/tool-summary.d.ts +21 -0
- package/dist/src/ui/utils/tool-summary.js +91 -0
- package/dist/src/utils/event-loop-monitor.d.ts +85 -0
- package/dist/src/utils/event-loop-monitor.js +107 -0
- package/dist/src/utils/llm-deadline.d.ts +14 -0
- package/dist/src/utils/llm-deadline.js +19 -0
- package/dist/src/utils/loop-profiler.d.ts +102 -0
- package/dist/src/utils/loop-profiler.js +202 -0
- package/dist/src/utils/settings.d.ts +27 -0
- package/dist/src/utils/settings.js +38 -2
- package/dist/src/utils/side-question.d.ts +1 -2
- package/dist/src/utils/side-question.js +2 -2
- package/dist/src/verify/entrypoint.js +51 -16
- package/dist/src/verify/orchestrator.d.ts +1 -1
- package/dist/src/verify/orchestrator.js +20 -3
- package/package.json +1 -2
- package/dist/src/cli/bw-vault.d.ts +0 -55
- package/dist/src/cli/bw-vault.js +0 -133
- package/dist/src/mcp/ee-tools.d.ts +0 -46
- 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
|
|
5
|
+
* Implements TUI-02 (stub conversation), PROV-03 (BYOK), PROV-07 (log redaction).
|
|
6
6
|
*
|
|
7
|
-
* Key loading
|
|
8
|
-
* 1.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
|
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
|
|
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
|
|
5
|
+
* Implements TUI-02 (stub conversation), PROV-03 (BYOK), PROV-07 (log redaction).
|
|
6
6
|
*
|
|
7
|
-
* Key loading
|
|
8
|
-
* 1.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
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
|
|
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
|
|
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
|
-
//
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
6
|
-
*
|
|
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
|
-
*
|
|
6
|
-
*
|
|
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
|
-
*
|
|
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;
|