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
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/providers/env-store.ts
|
|
3
|
+
*
|
|
4
|
+
* Canonical CLI key store: a `~/.muonroi-cli/.env` file (mode 0600) that is
|
|
5
|
+
* loaded into process.env at startup and written when the user sets a key.
|
|
6
|
+
* On Windows we ALSO mirror to the User-scope registry env so other OS
|
|
7
|
+
* processes see the key. Replaces the OS keychain (keytar).
|
|
8
|
+
*/
|
|
9
|
+
import { execFileSync } from "node:child_process";
|
|
10
|
+
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { homedir } from "node:os";
|
|
12
|
+
import { dirname, join } from "node:path";
|
|
13
|
+
import { redactor } from "../utils/redactor.js";
|
|
14
|
+
export function envFilePath() {
|
|
15
|
+
return process.env.MUONROI_ENV_FILE ?? join(homedir(), ".muonroi-cli", ".env");
|
|
16
|
+
}
|
|
17
|
+
function readLines() {
|
|
18
|
+
try {
|
|
19
|
+
return readFileSync(envFilePath(), "utf8").split(/\r?\n/);
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
// Missing file is the normal first-run case — no key store yet.
|
|
23
|
+
if (err?.code === "ENOENT")
|
|
24
|
+
return [];
|
|
25
|
+
// Anything else (locked, permissions, corrupt) must NOT read as "no keys":
|
|
26
|
+
// persistEnvVar rebuilds the file from these lines, so returning [] here
|
|
27
|
+
// silently rewrites the store WITHOUT the keys it failed to read — the user
|
|
28
|
+
// sets one key and the others are gone. Fail loudly instead.
|
|
29
|
+
throw new Error(`[env-store] cannot read ${envFilePath()}: ${err instanceof Error ? err.message : String(err)}. ` +
|
|
30
|
+
`Refusing to continue — writing now would drop the keys already in it.`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function writeLines(lines) {
|
|
34
|
+
const p = envFilePath();
|
|
35
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
36
|
+
const body = lines.filter((l, i) => !(l === "" && i === lines.length - 1)).join("\n");
|
|
37
|
+
writeFileSync(p, body === "" ? "" : `${body}\n`, { encoding: "utf8", mode: 0o600 });
|
|
38
|
+
try {
|
|
39
|
+
chmodSync(p, 0o600);
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
// Non-fatal on filesystems without POSIX perms (e.g. some Windows FS).
|
|
43
|
+
if (process.env.MUONROI_DEBUG_ENVSTORE) {
|
|
44
|
+
console.error(`[env-store] chmod failed for ${p}: ${err instanceof Error ? err.message : String(err)}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** Parse "KEY=value" — value may contain "="; comments/blank lines ignored. */
|
|
49
|
+
function parseLine(line) {
|
|
50
|
+
if (!line || line.trimStart().startsWith("#"))
|
|
51
|
+
return null;
|
|
52
|
+
const eq = line.indexOf("=");
|
|
53
|
+
if (eq <= 0)
|
|
54
|
+
return null;
|
|
55
|
+
return { key: line.slice(0, eq).trim(), value: line.slice(eq + 1) };
|
|
56
|
+
}
|
|
57
|
+
/** Env-var names we will ever mirror are plain identifiers; reject anything else. */
|
|
58
|
+
function isValidEnvName(name) {
|
|
59
|
+
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(name);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Credentials this CLI owns: it writes them itself (`/login`, `keys set` →
|
|
63
|
+
* persistEnvVar) so its own store is the user's latest intent. Matched by the
|
|
64
|
+
* `*_API_KEY` naming convention shared by ENV_BY_PROVIDER / ENV_BY_CHAT /
|
|
65
|
+
* ENV_BY_MCP — a structural test, not a provider-id list, and importing
|
|
66
|
+
* keychain.ts here would be circular (keychain imports this module).
|
|
67
|
+
*/
|
|
68
|
+
function isCliOwnedSecret(name) {
|
|
69
|
+
return /_API_KEY$/.test(name);
|
|
70
|
+
}
|
|
71
|
+
/** Last 4 chars + length — enough to tell two keys apart, never enough to use one. */
|
|
72
|
+
function maskSecret(value) {
|
|
73
|
+
return `len=${value.length} …${value.slice(-4)}`;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Which side wins when `.env` and the ambient OS environment disagree.
|
|
77
|
+
*
|
|
78
|
+
* Default `store`: for `*_API_KEY` only, `.env` wins. A stale Windows User-scope
|
|
79
|
+
* var silently beating the key the user just set through the CLI cost a full
|
|
80
|
+
* session of debugging — two council panelists died on a forgotten placeholder
|
|
81
|
+
* key while `.env` held the working one, and nothing said so.
|
|
82
|
+
*
|
|
83
|
+
* `MUONROI_ENV_PRECEDENCE=ambient` restores the old behaviour for CI and
|
|
84
|
+
* scripts that intentionally inject a key for one run.
|
|
85
|
+
*/
|
|
86
|
+
function ambientWinsForSecrets() {
|
|
87
|
+
return process.env.MUONROI_ENV_PRECEDENCE === "ambient";
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* True while a test runner is driving. Vitest sets both `VITEST` and
|
|
91
|
+
* `NODE_ENV=test`; either is enough.
|
|
92
|
+
*/
|
|
93
|
+
function isTestRunner() {
|
|
94
|
+
return process.env.VITEST !== undefined || process.env.NODE_ENV === "test";
|
|
95
|
+
}
|
|
96
|
+
function mirrorToWindowsRegistry(name, value) {
|
|
97
|
+
if (process.platform !== "win32")
|
|
98
|
+
return;
|
|
99
|
+
// A unit test must never write the developer's OS-global environment. It did:
|
|
100
|
+
// `bunx vitest run` on Windows left the fixture values from env-store.test.ts,
|
|
101
|
+
// migrate-legacy-keys.test.ts and auth-exclusivity.test.ts sitting in
|
|
102
|
+
// HKCU:\Environment. Those tests point MUONROI_ENV_FILE at a temp dir and
|
|
103
|
+
// clean it up, so they look hermetic — but persistEnvVar also mirrors here,
|
|
104
|
+
// none of them mocks child_process, and nothing cleaned the registry.
|
|
105
|
+
//
|
|
106
|
+
// The credential fixtures then outlived the suite and shadowed the real
|
|
107
|
+
// provider credentials for every NEW process: sub-agents 401'd, the AI SDK
|
|
108
|
+
// reported the resulting empty stream as AI_NoOutputGeneratedError, and
|
|
109
|
+
// /ideal's implementation stage died in 0.6s across three runs — bug "G1",
|
|
110
|
+
// whose own comment blames "gpt-5.4 reasoning models". The model was never
|
|
111
|
+
// involved. Guarding here rather than mocking per-test: the next test to call
|
|
112
|
+
// persistEnvVar would reintroduce the leak, and this is invisible on POSIX.
|
|
113
|
+
if (isTestRunner())
|
|
114
|
+
return;
|
|
115
|
+
if (!isValidEnvName(name)) {
|
|
116
|
+
// Defense-in-depth: never let a non-identifier name reach the shell.
|
|
117
|
+
if (process.env.MUONROI_DEBUG_ENVSTORE) {
|
|
118
|
+
console.error(`[env-store] refusing to mirror invalid env name: ${name}`);
|
|
119
|
+
}
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
// Neither name nor value is interpolated into the script — both are read
|
|
124
|
+
// from the child's environment as literal strings, so no PowerShell
|
|
125
|
+
// metacharacters in either can be interpreted as commands (injection-safe).
|
|
126
|
+
const script = value === null
|
|
127
|
+
? "[Environment]::SetEnvironmentVariable($env:MUONROI_ENVSTORE_NAME, $null, 'User')"
|
|
128
|
+
: "[Environment]::SetEnvironmentVariable($env:MUONROI_ENVSTORE_NAME, $env:MUONROI_ENVSTORE_VALUE, 'User')";
|
|
129
|
+
execFileSync("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], {
|
|
130
|
+
env: value === null
|
|
131
|
+
? { ...process.env, MUONROI_ENVSTORE_NAME: name }
|
|
132
|
+
: { ...process.env, MUONROI_ENVSTORE_NAME: name, MUONROI_ENVSTORE_VALUE: value },
|
|
133
|
+
stdio: "ignore",
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
// .env + in-memory write already succeeded; OS-wide mirror is best-effort.
|
|
138
|
+
if (process.env.MUONROI_DEBUG_ENVSTORE) {
|
|
139
|
+
console.error(`[env-store] registry mirror failed for ${name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Upsert `name=value` in the `.env` store, set `process.env[name]` for the
|
|
145
|
+
* current process, and (on Windows) mirror to the User registry env.
|
|
146
|
+
*/
|
|
147
|
+
export function persistEnvVar(name, value) {
|
|
148
|
+
redactor.enrollSecret(value);
|
|
149
|
+
const lines = readLines();
|
|
150
|
+
const out = [];
|
|
151
|
+
let replaced = false;
|
|
152
|
+
for (const line of lines) {
|
|
153
|
+
const parsed = parseLine(line);
|
|
154
|
+
if (parsed?.key === name) {
|
|
155
|
+
if (!replaced) {
|
|
156
|
+
out.push(`${name}=${value}`);
|
|
157
|
+
replaced = true;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
else if (line !== "") {
|
|
161
|
+
out.push(line);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (!replaced)
|
|
165
|
+
out.push(`${name}=${value}`);
|
|
166
|
+
writeLines(out);
|
|
167
|
+
process.env[name] = value;
|
|
168
|
+
mirrorToWindowsRegistry(name, value);
|
|
169
|
+
}
|
|
170
|
+
/** Remove `name` from the `.env` store, `process.env`, and Windows registry. */
|
|
171
|
+
export function clearEnvVar(name) {
|
|
172
|
+
const lines = readLines();
|
|
173
|
+
const out = lines.filter((line) => parseLine(line)?.key !== name && line !== "");
|
|
174
|
+
writeLines(out);
|
|
175
|
+
delete process.env[name];
|
|
176
|
+
mirrorToWindowsRegistry(name, null);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Load the `.env` store into `process.env` at startup.
|
|
180
|
+
*
|
|
181
|
+
* For ordinary variables the ambient OS environment stays authoritative and
|
|
182
|
+
* `.env` only fills gaps. For `*_API_KEY` credentials the CLI writes itself,
|
|
183
|
+
* `.env` wins and the conflict is logged — see {@link ambientWinsForSecrets}
|
|
184
|
+
* for why, and `MUONROI_ENV_PRECEDENCE=ambient` to opt out.
|
|
185
|
+
*/
|
|
186
|
+
export function loadEnvFileIntoProcess() {
|
|
187
|
+
let lines;
|
|
188
|
+
try {
|
|
189
|
+
lines = readLines();
|
|
190
|
+
}
|
|
191
|
+
catch (err) {
|
|
192
|
+
// Boot must survive an unreadable store, but never in silence: every
|
|
193
|
+
// provider would 401 and the CLI would look like it lost the keys.
|
|
194
|
+
console.error(`${err instanceof Error ? err.message : String(err)} — continuing without the stored keys.`);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
for (const line of lines) {
|
|
198
|
+
const parsed = parseLine(line);
|
|
199
|
+
if (!parsed)
|
|
200
|
+
continue;
|
|
201
|
+
const ambient = process.env[parsed.key];
|
|
202
|
+
if (ambient === undefined) {
|
|
203
|
+
process.env[parsed.key] = parsed.value;
|
|
204
|
+
redactor.enrollSecret(parsed.value);
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (ambient === parsed.value)
|
|
208
|
+
continue;
|
|
209
|
+
// The two disagree. For non-secrets the ambient stays authoritative, as
|
|
210
|
+
// before. For a CLI-owned credential a silent divergence is exactly the
|
|
211
|
+
// failure that must never happen again, so it is always reported.
|
|
212
|
+
if (!isCliOwnedSecret(parsed.key))
|
|
213
|
+
continue;
|
|
214
|
+
redactor.enrollSecret(ambient);
|
|
215
|
+
if (ambientWinsForSecrets()) {
|
|
216
|
+
console.error(`[env-store] ${parsed.key}: OS environment (${maskSecret(ambient)}) overrides the CLI store ` +
|
|
217
|
+
`(${maskSecret(parsed.value)}) — MUONROI_ENV_PRECEDENCE=ambient is set. If auth fails, the ` +
|
|
218
|
+
`OS variable is the one being used.`);
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
console.error(`[env-store] ${parsed.key}: the CLI store (${maskSecret(parsed.value)}) takes precedence over a ` +
|
|
222
|
+
`conflicting OS environment variable (${maskSecret(ambient)}). Remove the OS variable to silence ` +
|
|
223
|
+
`this, or set MUONROI_ENV_PRECEDENCE=ambient to let it win.`);
|
|
224
|
+
process.env[parsed.key] = parsed.value;
|
|
225
|
+
redactor.enrollSecret(parsed.value);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
//# sourceMappingURL=env-store.js.map
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* src/providers/keychain.ts
|
|
3
3
|
*
|
|
4
|
-
* Per-provider
|
|
5
|
-
*
|
|
6
|
-
* PROV-02 / PROV-03 requirements.
|
|
4
|
+
* Per-provider key store. Backed by the env-store (`.env` file + process.env +
|
|
5
|
+
* Windows registry mirror) — the OS keychain (keytar) has been removed.
|
|
7
6
|
*
|
|
8
|
-
*
|
|
7
|
+
* Source of truth: `process.env[ENV_BY_PROVIDER[provider]]`. Writes go through
|
|
8
|
+
* the env-store; reads come straight from process.env.
|
|
9
9
|
* Exception: ollama is keyless by default.
|
|
10
10
|
*/
|
|
11
11
|
import type { ProviderId } from "./types.js";
|
|
12
|
+
export declare const ENV_BY_PROVIDER: Record<ProviderId, string>;
|
|
12
13
|
/**
|
|
13
14
|
* Thrown when no API key can be found for a provider.
|
|
14
15
|
*/
|
|
@@ -17,37 +18,33 @@ export declare class ProviderKeyMissingError extends Error {
|
|
|
17
18
|
constructor(provider: ProviderId);
|
|
18
19
|
}
|
|
19
20
|
/**
|
|
20
|
-
* Providers that store an API key
|
|
21
|
-
*
|
|
22
|
-
* (keyless local server). Preserves the original ordering.
|
|
21
|
+
* Providers that store an API key (all except keyless ollama). Derived from
|
|
22
|
+
* `ALL_PROVIDER_IDS`; preserves the original ordering.
|
|
23
23
|
*/
|
|
24
24
|
export declare const KEYCHAIN_PROVIDER_IDS: readonly ProviderId[];
|
|
25
25
|
/**
|
|
26
|
-
* Store a provider API key in the OS
|
|
27
|
-
*
|
|
26
|
+
* Store a provider API key in the OS environment (via the env-store: `.env`
|
|
27
|
+
* file + process.env + Windows registry mirror). Returns true on success.
|
|
28
28
|
*/
|
|
29
29
|
export declare function setKeyForProvider(provider: string | ProviderId, key: string): Promise<boolean>;
|
|
30
30
|
/**
|
|
31
|
-
* Delete a stored key. Returns true if a key was
|
|
32
|
-
* present or keytar is unavailable.
|
|
31
|
+
* Delete a stored key from the env-store. Returns true if a key was present.
|
|
33
32
|
*/
|
|
34
33
|
export declare function deleteKeyForProvider(provider: string | ProviderId): Promise<boolean>;
|
|
35
34
|
/**
|
|
36
|
-
* List provider IDs that currently have a key
|
|
37
|
-
* Empty array if keytar is unavailable.
|
|
35
|
+
* List provider IDs that currently have a key set in the environment.
|
|
38
36
|
*/
|
|
39
37
|
export declare function listStoredProviders(): Promise<ProviderId[]>;
|
|
40
38
|
/**
|
|
41
|
-
* Load the API key for a given provider.
|
|
42
|
-
* Priority: OS keychain (keytar) > environment variable.
|
|
39
|
+
* Load the API key for a given provider from the environment.
|
|
43
40
|
* Ollama returns '' when no key is found (keyless).
|
|
44
41
|
*
|
|
45
42
|
* @throws {ProviderKeyMissingError} when no key found for non-ollama providers.
|
|
46
43
|
*/
|
|
47
44
|
export declare function loadKeyForProvider(provider: ProviderId): Promise<string>;
|
|
48
45
|
/**
|
|
49
|
-
* Return the list of providers that currently have credentials available —
|
|
50
|
-
*
|
|
46
|
+
* Return the list of providers that currently have credentials available —
|
|
47
|
+
* either an env API key or a stored OAuth token. `ollama` is always included
|
|
51
48
|
* because it is keyless. Order is stable for UI rendering.
|
|
52
49
|
*/
|
|
53
50
|
export declare function getConfiguredProviders(): Promise<ProviderId[]>;
|
|
@@ -64,3 +61,10 @@ export declare function getOAuthTokens(provider: ProviderId): Promise<import("./
|
|
|
64
61
|
* Returns null if no provider has a key (unlikely — ollama is keyless fallback).
|
|
65
62
|
*/
|
|
66
63
|
export declare function firstAvailableProvider(): Promise<ProviderId | null>;
|
|
64
|
+
/**
|
|
65
|
+
* One-time migration: move any legacy API keys — OS keychain (keytar) or
|
|
66
|
+
* `settings.json` (`providers.<p>.apiKey`) — into the env-store, then remove
|
|
67
|
+
* the legacy copies. Best-effort, idempotent, and never throws. Guarded by
|
|
68
|
+
* `settings.keysMigratedToEnv`. Runs once at startup before key resolution.
|
|
69
|
+
*/
|
|
70
|
+
export declare function migrateLegacyKeysToEnv(): Promise<void>;
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* src/providers/keychain.ts
|
|
3
3
|
*
|
|
4
|
-
* Per-provider
|
|
5
|
-
*
|
|
6
|
-
* PROV-02 / PROV-03 requirements.
|
|
4
|
+
* Per-provider key store. Backed by the env-store (`.env` file + process.env +
|
|
5
|
+
* Windows registry mirror) — the OS keychain (keytar) has been removed.
|
|
7
6
|
*
|
|
8
|
-
*
|
|
7
|
+
* Source of truth: `process.env[ENV_BY_PROVIDER[provider]]`. Writes go through
|
|
8
|
+
* the env-store; reads come straight from process.env.
|
|
9
9
|
* Exception: ollama is keyless by default.
|
|
10
10
|
*/
|
|
11
11
|
import { redactor } from "../utils/redactor.js";
|
|
12
|
+
import { clearEnvVar, persistEnvVar } from "./env-store.js";
|
|
12
13
|
import { ALL_PROVIDER_IDS } from "./types.js";
|
|
13
14
|
function normalizeKeychainProvider(p) {
|
|
14
15
|
const lower = p.toLowerCase();
|
|
@@ -16,25 +17,7 @@ function normalizeKeychainProvider(p) {
|
|
|
16
17
|
return lower;
|
|
17
18
|
return null;
|
|
18
19
|
}
|
|
19
|
-
const
|
|
20
|
-
anthropic: "anthropic",
|
|
21
|
-
openai: "openai",
|
|
22
|
-
deepseek: "deepseek",
|
|
23
|
-
xai: "xai",
|
|
24
|
-
zai: "zai",
|
|
25
|
-
"opencode-go": "opencode-go",
|
|
26
|
-
};
|
|
27
|
-
const KEYCHAIN_SERVICE = "muonroi-cli";
|
|
28
|
-
const ACCOUNT_BY_PROVIDER = {
|
|
29
|
-
anthropic: "anthropic",
|
|
30
|
-
openai: "openai",
|
|
31
|
-
deepseek: "deepseek",
|
|
32
|
-
xai: "xai",
|
|
33
|
-
ollama: "ollama",
|
|
34
|
-
zai: "zai",
|
|
35
|
-
"opencode-go": "opencode-go",
|
|
36
|
-
};
|
|
37
|
-
const ENV_BY_PROVIDER = {
|
|
20
|
+
export const ENV_BY_PROVIDER = {
|
|
38
21
|
anthropic: "ANTHROPIC_API_KEY",
|
|
39
22
|
openai: "OPENAI_API_KEY",
|
|
40
23
|
deepseek: "DEEPSEEK_API_KEY",
|
|
@@ -54,147 +37,83 @@ export class ProviderKeyMissingError extends Error {
|
|
|
54
37
|
this.name = "ProviderKeyMissingError";
|
|
55
38
|
}
|
|
56
39
|
}
|
|
57
|
-
async function loadKeytar() {
|
|
58
|
-
try {
|
|
59
|
-
return (await import("keytar"));
|
|
60
|
-
}
|
|
61
|
-
catch {
|
|
62
|
-
return null;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
40
|
/**
|
|
66
|
-
* Providers that store an API key
|
|
67
|
-
*
|
|
68
|
-
* (keyless local server). Preserves the original ordering.
|
|
41
|
+
* Providers that store an API key (all except keyless ollama). Derived from
|
|
42
|
+
* `ALL_PROVIDER_IDS`; preserves the original ordering.
|
|
69
43
|
*/
|
|
70
44
|
export const KEYCHAIN_PROVIDER_IDS = ALL_PROVIDER_IDS.filter((p) => p !== "ollama");
|
|
71
45
|
/**
|
|
72
|
-
* Store a provider API key in the OS
|
|
73
|
-
*
|
|
46
|
+
* Store a provider API key in the OS environment (via the env-store: `.env`
|
|
47
|
+
* file + process.env + Windows registry mirror). Returns true on success.
|
|
74
48
|
*/
|
|
75
49
|
export async function setKeyForProvider(provider, key) {
|
|
76
50
|
const norm = normalizeKeychainProvider(provider) ?? provider;
|
|
77
51
|
if (!key || key.length < 20) {
|
|
78
52
|
throw new Error(`Key for '${provider}' is too short (< 20 chars).`);
|
|
79
53
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
redactor.enrollSecret(key);
|
|
54
|
+
persistEnvVar(ENV_BY_PROVIDER[norm], key);
|
|
55
|
+
// One auth mode per provider (OAuth XOR API key): setting an API key logs out
|
|
56
|
+
// any OAuth session for this provider so the two can never coexist.
|
|
84
57
|
try {
|
|
85
|
-
await
|
|
86
|
-
|
|
58
|
+
const { listOAuthProviderIds } = await import("./auth/registry.js");
|
|
59
|
+
if ((await listOAuthProviderIds()).includes(norm)) {
|
|
60
|
+
const { deleteTokens } = await import("./auth/token-store.js");
|
|
61
|
+
await deleteTokens(norm);
|
|
62
|
+
}
|
|
87
63
|
}
|
|
88
64
|
catch (err) {
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
console.error(`[keychain] setPassword backend error for ${provider}:`, err?.message || err);
|
|
65
|
+
// Best-effort: the key is already persisted; failing to clear tokens is
|
|
66
|
+
// non-fatal (resolution still prefers the freshly-set key path only if no
|
|
67
|
+
// token — but we log so a lingering token is diagnosable).
|
|
68
|
+
if (process.env.MUONROI_DEBUG_ENVSTORE) {
|
|
69
|
+
console.error(`[keychain] failed to clear OAuth for ${norm}: ${err instanceof Error ? err.message : String(err)}`);
|
|
95
70
|
}
|
|
96
|
-
return false;
|
|
97
71
|
}
|
|
72
|
+
return true;
|
|
98
73
|
}
|
|
99
74
|
/**
|
|
100
|
-
* Delete a stored key. Returns true if a key was
|
|
101
|
-
* present or keytar is unavailable.
|
|
75
|
+
* Delete a stored key from the env-store. Returns true if a key was present.
|
|
102
76
|
*/
|
|
103
77
|
export async function deleteKeyForProvider(provider) {
|
|
104
78
|
const norm = normalizeKeychainProvider(provider) ?? provider;
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
try {
|
|
109
|
-
return await kt.deletePassword(KEYCHAIN_SERVICE, ACCOUNT_BY_PROVIDER[norm]);
|
|
110
|
-
}
|
|
111
|
-
catch (err) {
|
|
112
|
-
if (process.env.DEBUG || process.env.MUONROI_DEBUG_KEYCHAIN) {
|
|
113
|
-
console.error(`[keychain] deletePassword backend error for ${provider}:`, err?.message || err);
|
|
114
|
-
}
|
|
115
|
-
return false;
|
|
116
|
-
}
|
|
79
|
+
const had = !!process.env[ENV_BY_PROVIDER[norm]];
|
|
80
|
+
clearEnvVar(ENV_BY_PROVIDER[norm]);
|
|
81
|
+
return had;
|
|
117
82
|
}
|
|
118
83
|
/**
|
|
119
|
-
* List provider IDs that currently have a key
|
|
120
|
-
* Empty array if keytar is unavailable.
|
|
84
|
+
* List provider IDs that currently have a key set in the environment.
|
|
121
85
|
*/
|
|
122
86
|
export async function listStoredProviders() {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
return
|
|
126
|
-
|
|
127
|
-
const creds = await kt.findCredentials(KEYCHAIN_SERVICE);
|
|
128
|
-
const validAccounts = new Set(Object.values(ACCOUNT_BY_PROVIDER));
|
|
129
|
-
return creds.filter((c) => validAccounts.has(c.account)).map((c) => c.account);
|
|
130
|
-
}
|
|
131
|
-
catch {
|
|
132
|
-
return [];
|
|
133
|
-
}
|
|
87
|
+
return KEYCHAIN_PROVIDER_IDS.filter((p) => {
|
|
88
|
+
const v = process.env[ENV_BY_PROVIDER[p]];
|
|
89
|
+
return !!v && v.length >= 20;
|
|
90
|
+
});
|
|
134
91
|
}
|
|
135
92
|
/**
|
|
136
|
-
* Load the API key for a given provider.
|
|
137
|
-
* Priority: OS keychain (keytar) > environment variable.
|
|
93
|
+
* Load the API key for a given provider from the environment.
|
|
138
94
|
* Ollama returns '' when no key is found (keyless).
|
|
139
95
|
*
|
|
140
96
|
* @throws {ProviderKeyMissingError} when no key found for non-ollama providers.
|
|
141
97
|
*/
|
|
142
98
|
export async function loadKeyForProvider(provider) {
|
|
143
|
-
const kt = await loadKeytar();
|
|
144
|
-
if (kt) {
|
|
145
|
-
try {
|
|
146
|
-
const k = await kt.getPassword(KEYCHAIN_SERVICE, ACCOUNT_BY_PROVIDER[provider]);
|
|
147
|
-
if (k && k.length >= 20) {
|
|
148
|
-
redactor.enrollSecret(k);
|
|
149
|
-
return k;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
catch {
|
|
153
|
-
/* ignore keytar backend failures */
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
99
|
const envKey = process.env[ENV_BY_PROVIDER[provider]];
|
|
157
100
|
if (envKey && envKey.length >= 20) {
|
|
158
101
|
redactor.enrollSecret(envKey);
|
|
159
102
|
return envKey;
|
|
160
103
|
}
|
|
161
|
-
// Fallback: check user-settings.json providers config (lazy import to avoid circular deps)
|
|
162
|
-
const settingsField = SETTINGS_KEY_MAP[provider];
|
|
163
|
-
if (settingsField) {
|
|
164
|
-
try {
|
|
165
|
-
const { loadUserSettings } = await import("../utils/settings.js");
|
|
166
|
-
const providers = loadUserSettings().providers;
|
|
167
|
-
const settingsKey = providers?.[settingsField]?.apiKey;
|
|
168
|
-
if (settingsKey && settingsKey.length >= 20) {
|
|
169
|
-
redactor.enrollSecret(settingsKey);
|
|
170
|
-
return settingsKey;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
catch {
|
|
174
|
-
/* settings load failed — continue to error */
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
104
|
// Ollama may be keyless
|
|
178
105
|
if (provider === "ollama")
|
|
179
106
|
return "";
|
|
180
107
|
throw new ProviderKeyMissingError(provider);
|
|
181
108
|
}
|
|
182
109
|
/**
|
|
183
|
-
* Return the list of providers that currently have credentials available —
|
|
184
|
-
*
|
|
110
|
+
* Return the list of providers that currently have credentials available —
|
|
111
|
+
* either an env API key or a stored OAuth token. `ollama` is always included
|
|
185
112
|
* because it is keyless. Order is stable for UI rendering.
|
|
186
113
|
*/
|
|
187
114
|
export async function getConfiguredProviders() {
|
|
188
115
|
const order = ALL_PROVIDER_IDS;
|
|
189
116
|
const stored = new Set(await listStoredProviders());
|
|
190
|
-
let settingsProviders = {};
|
|
191
|
-
try {
|
|
192
|
-
const { loadUserSettings } = await import("../utils/settings.js");
|
|
193
|
-
settingsProviders = (loadUserSettings().providers ?? {});
|
|
194
|
-
}
|
|
195
|
-
catch {
|
|
196
|
-
/* settings unreadable — keychain + env still work */
|
|
197
|
-
}
|
|
198
117
|
// OAuth-authenticated providers (no API key, but tokens stored via `keys
|
|
199
118
|
// login`) need to count as configured — otherwise the model picker silently
|
|
200
119
|
// hides them. Source the eligible provider list from the OAuth registry so
|
|
@@ -214,25 +133,8 @@ export async function getConfiguredProviders() {
|
|
|
214
133
|
}
|
|
215
134
|
const configured = [];
|
|
216
135
|
for (const p of order) {
|
|
217
|
-
if (p === "ollama") {
|
|
136
|
+
if (p === "ollama" || stored.has(p) || oauthAuthenticated.has(p)) {
|
|
218
137
|
configured.push(p);
|
|
219
|
-
continue;
|
|
220
|
-
}
|
|
221
|
-
if (stored.has(p) || oauthAuthenticated.has(p)) {
|
|
222
|
-
configured.push(p);
|
|
223
|
-
continue;
|
|
224
|
-
}
|
|
225
|
-
const envKey = process.env[ENV_BY_PROVIDER[p]];
|
|
226
|
-
if (envKey && envKey.length >= 20) {
|
|
227
|
-
configured.push(p);
|
|
228
|
-
continue;
|
|
229
|
-
}
|
|
230
|
-
const settingsField = SETTINGS_KEY_MAP[p];
|
|
231
|
-
if (settingsField) {
|
|
232
|
-
const k = settingsProviders[settingsField]?.apiKey;
|
|
233
|
-
if (k && k.length >= 20) {
|
|
234
|
-
configured.push(p);
|
|
235
|
-
}
|
|
236
138
|
}
|
|
237
139
|
}
|
|
238
140
|
return configured;
|
|
@@ -275,4 +177,91 @@ export async function firstAvailableProvider() {
|
|
|
275
177
|
}
|
|
276
178
|
return null;
|
|
277
179
|
}
|
|
180
|
+
/**
|
|
181
|
+
* One-time migration: move any legacy API keys — OS keychain (keytar) or
|
|
182
|
+
* `settings.json` (`providers.<p>.apiKey`) — into the env-store, then remove
|
|
183
|
+
* the legacy copies. Best-effort, idempotent, and never throws. Guarded by
|
|
184
|
+
* `settings.keysMigratedToEnv`. Runs once at startup before key resolution.
|
|
185
|
+
*/
|
|
186
|
+
export async function migrateLegacyKeysToEnv() {
|
|
187
|
+
let settingsMod;
|
|
188
|
+
try {
|
|
189
|
+
settingsMod = await import("../utils/settings.js");
|
|
190
|
+
}
|
|
191
|
+
catch (err) {
|
|
192
|
+
if (process.env.MUONROI_DEBUG_ENVSTORE) {
|
|
193
|
+
console.error(`[keychain] migration: settings unavailable: ${err instanceof Error ? err.message : String(err)}`);
|
|
194
|
+
}
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
let settings;
|
|
198
|
+
try {
|
|
199
|
+
settings = settingsMod.loadUserSettings();
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (settings.keysMigratedToEnv)
|
|
205
|
+
return;
|
|
206
|
+
// Legacy keytar reader — the dependency has been removed, so absence is the
|
|
207
|
+
// normal case. The specifier is assembled at runtime so the bundler
|
|
208
|
+
// (`bun build --compile`) does not try to statically resolve the missing
|
|
209
|
+
// module; when keytar isn't installed this import simply throws → null.
|
|
210
|
+
let keytar = null;
|
|
211
|
+
try {
|
|
212
|
+
const legacyKeytarSpecifier = ["key", "tar"].join("");
|
|
213
|
+
keytar = (await import(legacyKeytarSpecifier));
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
keytar = null;
|
|
217
|
+
}
|
|
218
|
+
const providersPatch = { ...(settings.providers ?? {}) };
|
|
219
|
+
let providersDirty = false;
|
|
220
|
+
for (const p of KEYCHAIN_PROVIDER_IDS) {
|
|
221
|
+
const envName = ENV_BY_PROVIDER[p];
|
|
222
|
+
if (process.env[envName])
|
|
223
|
+
continue; // already in env
|
|
224
|
+
let legacy = null;
|
|
225
|
+
if (keytar?.getPassword) {
|
|
226
|
+
try {
|
|
227
|
+
legacy = await keytar.getPassword("muonroi-cli", p);
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
legacy = null;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (!legacy)
|
|
234
|
+
legacy = providersPatch[p]?.apiKey ?? null;
|
|
235
|
+
if (legacy && legacy.length >= 20) {
|
|
236
|
+
persistEnvVar(envName, legacy);
|
|
237
|
+
if (keytar?.deletePassword) {
|
|
238
|
+
try {
|
|
239
|
+
await keytar.deletePassword("muonroi-cli", p);
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
/* best-effort keychain cleanup */
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (providersPatch[p]?.apiKey) {
|
|
246
|
+
delete providersPatch[p].apiKey;
|
|
247
|
+
providersDirty = true;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
const patch = { keysMigratedToEnv: true };
|
|
252
|
+
// Strip the legacy plaintext main key — it is no longer read (getApiKey is
|
|
253
|
+
// env-only). Passing undefined drops it from the persisted JSON.
|
|
254
|
+
if (settings.apiKey)
|
|
255
|
+
patch.apiKey = undefined;
|
|
256
|
+
if (providersDirty)
|
|
257
|
+
patch.providers = providersPatch;
|
|
258
|
+
try {
|
|
259
|
+
settingsMod.saveUserSettings(patch);
|
|
260
|
+
}
|
|
261
|
+
catch (err) {
|
|
262
|
+
if (process.env.MUONROI_DEBUG_ENVSTORE) {
|
|
263
|
+
console.error(`[keychain] migration: save failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
278
267
|
//# sourceMappingURL=keychain.js.map
|