From ca9f3686d85d531f31763f2f62d27676889ca34f Mon Sep 17 00:00:00 2001 From: 404-Page-Found <139850808+404-Page-Found@users.noreply.github.com> Date: Tue, 22 Sep 2026 12:51:34 +1000 Subject: [PATCH 01/22] fix: scope init API keys to selected provider (#312) --- src/commands/init.ts | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/commands/init.ts b/src/commands/init.ts index 8861e00..32472fa 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -9,7 +9,7 @@ import { getProviderInfo, fetchModels, } from '../providers/index.js'; -import { saveConfig, configExists, loadConfig } from '../config/store.js'; +import { saveConfig, configExists, loadConfig, loadRawConfig } from '../config/store.js'; import type { Config } from '../types.js'; import { getAvailableTemplateVars } from '../llm/prompt.js'; import { installCommitHooks, uninstallCommitHooks } from '../git/hook.js'; @@ -25,11 +25,18 @@ export function resolveBaseUrl(providerKey: string, existingBaseUrl?: string): s export function buildApiKeyPrompt(existingKey: string, apiKeyEnv: string) { return { - message: `Enter your API key (will be stored in config), or leave blank to use ${pc.cyan(`$${apiKeyEnv}`)} env var:`, + message: `Enter your API key (will be stored in config), or leave blank to use ${pc.cyan(`${apiKeyEnv}`)} env var:`, placeholder: existingKey ? '•••••••• (already configured)' : '', }; } +export function getStoredApiKeyForProvider( + selectedProvider: string, + storedConfig: Pick | null, +): string | undefined { + return storedConfig?.provider === selectedProvider ? storedConfig.apiKey : undefined; +} + /** * A template file takes precedence over inline templates at runtime, so drop * inline values when a templatePath is set to keep the saved config consistent @@ -146,11 +153,11 @@ async function promptProvider(existingConfig: Config | null): Promise | null, ): Promise { if (!provider.needsApiKey) return undefined; - const existingKey = existingConfig?.apiKey ?? process.env[provider.apiKeyEnv] ?? ''; + const existingKey = getStoredApiKeyForProvider(provider.providerKey, storedConfig) ?? process.env[provider.apiKeyEnv] ?? ''; const keyResult = await text(buildApiKeyPrompt(existingKey, provider.apiKeyEnv)); if (isCancel(keyResult)) return null; return keyResult || existingKey || ''; @@ -339,11 +346,14 @@ interface CollectedSetup { provider: ProviderSetup; } -async function collectConfig(existingConfig: Config | null): Promise { +async function collectConfig( + existingConfig: Config | null, + storedConfig: Pick | null, +): Promise { const provider = await promptProvider(existingConfig); if (!provider) return null; - const apiKey = await promptApiKey(provider, existingConfig); + const apiKey = await promptApiKey(provider, storedConfig); if (apiKey === null) return null; const selectedModel = await promptModel(provider, apiKey, existingConfig); @@ -379,6 +389,7 @@ async function runInteractiveSetup(options: { installHook?: boolean; uninstallHo const isReconfig = configExists(); const existingConfig = isReconfig ? await loadConfig().catch(() => null) : null; + const storedConfig = isReconfig ? await loadRawConfig().catch(() => null) : null; if (isReconfig) { const reconfirm = await confirm({ @@ -391,7 +402,7 @@ async function runInteractiveSetup(options: { installHook?: boolean; uninstallHo } } - const setup = await collectConfig(existingConfig); + const setup = await collectConfig(existingConfig, storedConfig); if (!setup) { outro('Setup cancelled.'); return; From e62d7ca52c04e311b29f188c1bc881927d1fc53c Mon Sep 17 00:00:00 2001 From: 404-Page-Found <139850808+404-Page-Found@users.noreply.github.com> Date: Tue, 22 Sep 2026 12:51:38 +1000 Subject: [PATCH 02/22] fix: clear API key when changing provider (#312) --- src/commands/config.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/commands/config.ts b/src/commands/config.ts index 9b3ed33..d9e0b05 100644 --- a/src/commands/config.ts +++ b/src/commands/config.ts @@ -117,13 +117,21 @@ function updateConfigField(config: Config, key: K, value function applyProviderChange(config: Config, provider: Config['provider']): Config { const nextConfig = updateConfigField(config, 'provider', provider); - if (provider === CUSTOM_PROVIDER_KEY) { + if (config.provider === provider) { return nextConfig; } + if (provider === CUSTOM_PROVIDER_KEY) { + return { + ...nextConfig, + apiKey: undefined, + }; + } + return { ...nextConfig, baseUrl: undefined, + apiKey: undefined, }; } From 69063a8d42cb4afdae9c3caf2cc1cea37fe54d33 Mon Sep 17 00:00:00 2001 From: 404-Page-Found <139850808+404-Page-Found@users.noreply.github.com> Date: Tue, 22 Sep 2026 12:51:43 +1000 Subject: [PATCH 03/22] test: cover provider-scoped init API keys (#312) --- tests/init-api-key-prompt.test.mjs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/init-api-key-prompt.test.mjs b/tests/init-api-key-prompt.test.mjs index 2bac976..702a103 100644 --- a/tests/init-api-key-prompt.test.mjs +++ b/tests/init-api-key-prompt.test.mjs @@ -1,7 +1,7 @@ import assert from 'node:assert/strict'; import test from 'node:test'; -import { buildApiKeyPrompt } from '../dist/commands/init.js'; +import { buildApiKeyPrompt, getStoredApiKeyForProvider } from '../dist/commands/init.js'; test('does not prefill an existing API key in the init prompt', () => { const prompt = buildApiKeyPrompt('sk-live-secret', 'OPENAI_API_KEY'); @@ -16,3 +16,15 @@ test('leaves the API key prompt blank for new configs', () => { assert.equal(prompt.placeholder, ''); assert.equal(Object.hasOwn(prompt, 'initialValue'), false); }); + + +test('reuses a stored API key only for the same provider', () => { + assert.equal( + getStoredApiKeyForProvider('openai', { provider: 'openai', apiKey: 'sk-openai' }), + 'sk-openai', + ); + assert.equal( + getStoredApiKeyForProvider('anthropic', { provider: 'openai', apiKey: 'sk-openai' }), + undefined, + ); +}); From 5e5f98a76022c2187c4256777763b651c74c08b1 Mon Sep 17 00:00:00 2001 From: 404-Page-Found <139850808+404-Page-Found@users.noreply.github.com> Date: Tue, 22 Sep 2026 12:51:48 +1000 Subject: [PATCH 04/22] test: cover provider-switch API key fallback (#312) --- tests/config-command.test.mjs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/config-command.test.mjs b/tests/config-command.test.mjs index bd5cf89..077fd99 100644 --- a/tests/config-command.test.mjs +++ b/tests/config-command.test.mjs @@ -6,6 +6,7 @@ import { join } from 'node:path'; import test from 'node:test'; import { promisify } from 'node:util'; import { maskApiKey } from '../dist/commands/config.js'; +import { resolveApiKey } from '../dist/llm/client.js'; import { assertFriendlyCommandError, assertJsonCommandError, writeInvalidConfig } from './cli-error-helpers.mjs'; const execFileAsync = promisify(execFile); @@ -375,6 +376,33 @@ test('config set clears stale baseUrl when switching away from custom provider', }); }); +test('config set clears the API key when switching providers and uses the new provider env key', async () => { + await withTempHome(async (homeDir) => { + writeConfig(homeDir, { + provider: 'openai', + apiKey: 'sk-openai', + }); + + await runConfigWithArgs(homeDir, ['set', 'provider', 'anthropic']); + const config = readConfig(homeDir); + + assert.equal(config.provider, 'anthropic'); + assert.equal(config.apiKey, undefined); + + const original = process.env.ANTHROPIC_API_KEY; + process.env.ANTHROPIC_API_KEY = 'sk-anthropic'; + try { + assert.equal(resolveApiKey(config), 'sk-anthropic'); + } finally { + if (original === undefined) { + delete process.env.ANTHROPIC_API_KEY; + } else { + process.env.ANTHROPIC_API_KEY = original; + } + } + }); +}); + test('config set clears stale baseUrl when switching between built-in providers', async () => { await withTempHome(async (homeDir) => { writeConfig(homeDir, { From b0c2169542f2a25503290d8a35eda21ce21488b1 Mon Sep 17 00:00:00 2001 From: 404-Page-Found <139850808+404-Page-Found@users.noreply.github.com> Date: Tue, 22 Sep 2026 12:51:59 +1000 Subject: [PATCH 05/22] fix: allow raw provider config provenance to be optional --- src/commands/init.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/init.ts b/src/commands/init.ts index 32472fa..7bdabc9 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -32,7 +32,7 @@ export function buildApiKeyPrompt(existingKey: string, apiKeyEnv: string) { export function getStoredApiKeyForProvider( selectedProvider: string, - storedConfig: Pick | null, + storedConfig: Pick, 'provider' | 'apiKey'> | null, ): string | undefined { return storedConfig?.provider === selectedProvider ? storedConfig.apiKey : undefined; } From f3d4cb88873739637373e11ff145bf2414fe7a66 Mon Sep 17 00:00:00 2001 From: 404-Page-Found <139850808+404-Page-Found@users.noreply.github.com> Date: Tue, 22 Sep 2026 12:52:17 +1000 Subject: [PATCH 06/22] fix: keep init prompt wording and format provider key lookup --- src/commands/init.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/commands/init.ts b/src/commands/init.ts index 7bdabc9..fd2598a 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -157,7 +157,8 @@ async function promptApiKey( ): Promise { if (!provider.needsApiKey) return undefined; - const existingKey = getStoredApiKeyForProvider(provider.providerKey, storedConfig) ?? process.env[provider.apiKeyEnv] ?? ''; + const existingKey = + getStoredApiKeyForProvider(provider.providerKey, storedConfig) ?? process.env[provider.apiKeyEnv] ?? ''; const keyResult = await text(buildApiKeyPrompt(existingKey, provider.apiKeyEnv)); if (isCancel(keyResult)) return null; return keyResult || existingKey || ''; From 8cfea72a84886392ec8e0494f30311f3b364e4b4 Mon Sep 17 00:00:00 2001 From: 404-Page-Found <139850808+404-Page-Found@users.noreply.github.com> Date: Tue, 22 Sep 2026 12:52:56 +1000 Subject: [PATCH 07/22] fix: preserve init API key prompt and provenance types --- src/commands/init.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/commands/init.ts b/src/commands/init.ts index fd2598a..04f8687 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -25,7 +25,7 @@ export function resolveBaseUrl(providerKey: string, existingBaseUrl?: string): s export function buildApiKeyPrompt(existingKey: string, apiKeyEnv: string) { return { - message: `Enter your API key (will be stored in config), or leave blank to use ${pc.cyan(`${apiKeyEnv}`)} env var:`, + message: `Enter your API key (will be stored in config), or leave blank to use ${pc.cyan(`$${apiKeyEnv}`)} env var:`, placeholder: existingKey ? '•••••••• (already configured)' : '', }; } @@ -153,7 +153,7 @@ async function promptProvider(existingConfig: Config | null): Promise | null, + storedConfig: Pick, 'provider' | 'apiKey'> | null, ): Promise { if (!provider.needsApiKey) return undefined; @@ -349,7 +349,7 @@ interface CollectedSetup { async function collectConfig( existingConfig: Config | null, - storedConfig: Pick | null, + storedConfig: Pick, 'provider' | 'apiKey'> | null, ): Promise { const provider = await promptProvider(existingConfig); if (!provider) return null; From b7b5e5351ebce7fa946976477663d05840355ef0 Mon Sep 17 00:00:00 2001 From: 404-Page-Found <139850808+404-Page-Found@users.noreply.github.com> Date: Tue, 22 Sep 2026 12:53:39 +1000 Subject: [PATCH 08/22] test: update provider-switch key expectation --- tests/config-command.test.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/config-command.test.mjs b/tests/config-command.test.mjs index 077fd99..5924768 100644 --- a/tests/config-command.test.mjs +++ b/tests/config-command.test.mjs @@ -359,7 +359,7 @@ test('config set rejects unknown provider keys and lists valid options', async ( }); }); -test('config set clears stale baseUrl when switching away from custom provider', async () => { +test('config set clears stale baseUrl and API key when switching away from custom provider', async () => { await withTempHome(async (homeDir) => { writeConfig(homeDir, { apiKey: 'sk-still-valid-for-next-provider', @@ -372,7 +372,7 @@ test('config set clears stale baseUrl when switching away from custom provider', assert.equal(config.provider, 'openai'); assert.equal(config.baseUrl, undefined); - assert.equal(config.apiKey, 'sk-still-valid-for-next-provider'); + assert.equal(config.apiKey, undefined); }); }); From 847bc12cb613dc6d2529844e7a719ee4d6b607f4 Mon Sep 17 00:00:00 2001 From: 404-Page-Found <139850808+404-Page-Found@users.noreply.github.com> Date: Tue, 22 Sep 2026 13:15:41 +1000 Subject: [PATCH 09/22] fix: preserve generic API key env during init reconfiguration --- src/commands/init.ts | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/commands/init.ts b/src/commands/init.ts index 04f8687..f7cbf28 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -34,7 +34,21 @@ export function getStoredApiKeyForProvider( selectedProvider: string, storedConfig: Pick, 'provider' | 'apiKey'> | null, ): string | undefined { - return storedConfig?.provider === selectedProvider ? storedConfig.apiKey : undefined; + return storedConfig?.provider === selectedProvider ? storedConfig.apiKey?.trim() : undefined; +} + +export function getExistingApiKeyForProvider( + provider: string, + storedConfig: Pick, 'provider' | 'apiKey'> | null, + apiKeyEnv: string, + env: NodeJS.ProcessEnv = process.env, +): string { + return ( + env['COMMIT_ECHO_API_KEY'] ?? + getStoredApiKeyForProvider(provider, storedConfig) ?? + env[apiKeyEnv] ?? + '' + ); } /** @@ -157,8 +171,11 @@ async function promptApiKey( ): Promise { if (!provider.needsApiKey) return undefined; - const existingKey = - getStoredApiKeyForProvider(provider.providerKey, storedConfig) ?? process.env[provider.apiKeyEnv] ?? ''; + const existingKey = getExistingApiKeyForProvider( + provider.providerKey, + storedConfig, + provider.apiKeyEnv, + ); const keyResult = await text(buildApiKeyPrompt(existingKey, provider.apiKeyEnv)); if (isCancel(keyResult)) return null; return keyResult || existingKey || ''; From ccdecbcf97fd8b6834c820d77d0a4e16fdd1ee49 Mon Sep 17 00:00:00 2001 From: 404-Page-Found <139850808+404-Page-Found@users.noreply.github.com> Date: Tue, 22 Sep 2026 13:15:49 +1000 Subject: [PATCH 10/22] test: cover scoped init API key fallback behavior --- tests/init-api-key-prompt.test.mjs | 31 +++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/tests/init-api-key-prompt.test.mjs b/tests/init-api-key-prompt.test.mjs index 702a103..ef39aad 100644 --- a/tests/init-api-key-prompt.test.mjs +++ b/tests/init-api-key-prompt.test.mjs @@ -1,7 +1,7 @@ import assert from 'node:assert/strict'; import test from 'node:test'; -import { buildApiKeyPrompt, getStoredApiKeyForProvider } from '../dist/commands/init.js'; +import { buildApiKeyPrompt, getExistingApiKeyForProvider, getStoredApiKeyForProvider } from '../dist/commands/init.js'; test('does not prefill an existing API key in the init prompt', () => { const prompt = buildApiKeyPrompt('sk-live-secret', 'OPENAI_API_KEY'); @@ -18,9 +18,9 @@ test('leaves the API key prompt blank for new configs', () => { }); -test('reuses a stored API key only for the same provider', () => { +test('reuses a trimmed stored API key only for the same provider', () => { assert.equal( - getStoredApiKeyForProvider('openai', { provider: 'openai', apiKey: 'sk-openai' }), + getStoredApiKeyForProvider('openai', { provider: 'openai', apiKey: ' sk-openai ' }), 'sk-openai', ); assert.equal( @@ -28,3 +28,28 @@ test('reuses a stored API key only for the same provider', () => { undefined, ); }); + +test('prefers the generic API key env override during init reconfiguration', () => { + const env = { + COMMIT_ECHO_API_KEY: 'sk-generic', + OPENAI_API_KEY: 'sk-openai', + }; + + assert.equal( + getExistingApiKeyForProvider('openai', { provider: 'openai', apiKey: 'sk-stored' }, 'OPENAI_API_KEY', env), + 'sk-generic', + ); +}); + +test('falls back to the provider-specific API key env when no generic or matching stored key exists', () => { + const env = { OPENAI_API_KEY: 'sk-openai' }; + + assert.equal( + getExistingApiKeyForProvider('openai', { provider: 'anthropic', apiKey: 'sk-anthropic' }, 'OPENAI_API_KEY', env), + 'sk-openai', + ); + assert.equal( + getExistingApiKeyForProvider('openai', null, 'OPENAI_API_KEY', {}), + '', + ); +}); From b644ce32cc4537d4d877c85c35c4e71d39443ea0 Mon Sep 17 00:00:00 2001 From: 404-Page-Found <139850808+404-Page-Found@users.noreply.github.com> Date: Tue, 22 Sep 2026 13:41:56 +1000 Subject: [PATCH 11/22] fix: validate and trim init API keys --- src/commands/init.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/commands/init.ts b/src/commands/init.ts index f7cbf28..29d09b0 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -34,7 +34,10 @@ export function getStoredApiKeyForProvider( selectedProvider: string, storedConfig: Pick, 'provider' | 'apiKey'> | null, ): string | undefined { - return storedConfig?.provider === selectedProvider ? storedConfig.apiKey?.trim() : undefined; + if (storedConfig?.provider !== selectedProvider || typeof storedConfig.apiKey !== 'string') { + return undefined; + } + return storedConfig.apiKey.trim(); } export function getExistingApiKeyForProvider( @@ -44,7 +47,7 @@ export function getExistingApiKeyForProvider( env: NodeJS.ProcessEnv = process.env, ): string { return ( - env['COMMIT_ECHO_API_KEY'] ?? + env['COMMIT_ECHO_API_KEY']?.trim() ?? getStoredApiKeyForProvider(provider, storedConfig) ?? env[apiKeyEnv] ?? '' @@ -171,11 +174,7 @@ async function promptApiKey( ): Promise { if (!provider.needsApiKey) return undefined; - const existingKey = getExistingApiKeyForProvider( - provider.providerKey, - storedConfig, - provider.apiKeyEnv, - ); + const existingKey = getExistingApiKeyForProvider(provider.providerKey, storedConfig, provider.apiKeyEnv); const keyResult = await text(buildApiKeyPrompt(existingKey, provider.apiKeyEnv)); if (isCancel(keyResult)) return null; return keyResult || existingKey || ''; From 7f406949c68d7c64e4943e5bdfb9a65a386efe5e Mon Sep 17 00:00:00 2001 From: 404-Page-Found <139850808+404-Page-Found@users.noreply.github.com> Date: Tue, 22 Sep 2026 13:42:00 +1000 Subject: [PATCH 12/22] test: cover malformed and padded init API keys --- tests/init-api-key-prompt.test.mjs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/tests/init-api-key-prompt.test.mjs b/tests/init-api-key-prompt.test.mjs index ef39aad..3fdb85c 100644 --- a/tests/init-api-key-prompt.test.mjs +++ b/tests/init-api-key-prompt.test.mjs @@ -17,7 +17,6 @@ test('leaves the API key prompt blank for new configs', () => { assert.equal(Object.hasOwn(prompt, 'initialValue'), false); }); - test('reuses a trimmed stored API key only for the same provider', () => { assert.equal( getStoredApiKeyForProvider('openai', { provider: 'openai', apiKey: ' sk-openai ' }), @@ -29,9 +28,17 @@ test('reuses a trimmed stored API key only for the same provider', () => { ); }); -test('prefers the generic API key env override during init reconfiguration', () => { +test('ignores a non-string stored API key for the selected provider', () => { + assert.equal( + getStoredApiKeyForProvider('openai', { provider: 'openai', apiKey: 123 }), + undefined, + ); +}); + + +test('prefers the trimmed generic API key env override during init reconfiguration', () => { const env = { - COMMIT_ECHO_API_KEY: 'sk-generic', + COMMIT_ECHO_API_KEY: ' sk-generic ', OPENAI_API_KEY: 'sk-openai', }; From bfa95a7c82561eb07d724e2f0ad638da338e4e6c Mon Sep 17 00:00:00 2001 From: 404-Page-Found <139850808+404-Page-Found@users.noreply.github.com> Date: Tue, 22 Sep 2026 13:43:05 +1000 Subject: [PATCH 13/22] chore: expose prettier formatting diff in CI --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3a83423..19d9222 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "prepublishOnly": "npm run build", "start": "node dist/index.js", "format": "prettier --write \"src/**/*.ts\"", - "format:check": "prettier --check \"src/**/*.ts\"" + "format:check": "prettier --write \"src/**/*.ts\" && git diff -- src/commands/init.ts && exit 1" }, "files": [ "dist" From 8fad086200c8223530a1920e3153b34bd7ab554e Mon Sep 17 00:00:00 2001 From: 404-Page-Found <139850808+404-Page-Found@users.noreply.github.com> Date: Tue, 22 Sep 2026 13:43:35 +1000 Subject: [PATCH 14/22] style: match prettier formatting for init --- src/commands/init.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/commands/init.ts b/src/commands/init.ts index 29d09b0..1f3068e 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -47,10 +47,7 @@ export function getExistingApiKeyForProvider( env: NodeJS.ProcessEnv = process.env, ): string { return ( - env['COMMIT_ECHO_API_KEY']?.trim() ?? - getStoredApiKeyForProvider(provider, storedConfig) ?? - env[apiKeyEnv] ?? - '' + env['COMMIT_ECHO_API_KEY']?.trim() ?? getStoredApiKeyForProvider(provider, storedConfig) ?? env[apiKeyEnv] ?? '' ); } From 00564c5ead2a1de4a1cd322f24e8719a0ceaa008 Mon Sep 17 00:00:00 2001 From: 404-Page-Found <139850808+404-Page-Found@users.noreply.github.com> Date: Tue, 22 Sep 2026 13:43:38 +1000 Subject: [PATCH 15/22] chore: restore format check --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 19d9222..3a83423 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "prepublishOnly": "npm run build", "start": "node dist/index.js", "format": "prettier --write \"src/**/*.ts\"", - "format:check": "prettier --write \"src/**/*.ts\" && git diff -- src/commands/init.ts && exit 1" + "format:check": "prettier --check \"src/**/*.ts\"" }, "files": [ "dist" From ef5623537c35942668fcdb30fe6d73ad23c1bc66 Mon Sep 17 00:00:00 2001 From: 404-Page-Found <139850808+404-Page-Found@users.noreply.github.com> Date: Wed, 23 Sep 2026 07:40:38 +1000 Subject: [PATCH 16/22] fix: trim provider API key env fallback --- src/commands/init.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/commands/init.ts b/src/commands/init.ts index 1f3068e..5101ba2 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -47,7 +47,10 @@ export function getExistingApiKeyForProvider( env: NodeJS.ProcessEnv = process.env, ): string { return ( - env['COMMIT_ECHO_API_KEY']?.trim() ?? getStoredApiKeyForProvider(provider, storedConfig) ?? env[apiKeyEnv] ?? '' + env['COMMIT_ECHO_API_KEY']?.trim() ?? + getStoredApiKeyForProvider(provider, storedConfig) ?? + env[apiKeyEnv]?.trim() ?? + '' ); } From 7af6652872c93a7266bedb27cea5e7961b43cb25 Mon Sep 17 00:00:00 2001 From: 404-Page-Found <139850808+404-Page-Found@users.noreply.github.com> Date: Wed, 23 Sep 2026 07:40:43 +1000 Subject: [PATCH 17/22] test: cover trimmed provider API key env --- tests/init-api-key-prompt.test.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/init-api-key-prompt.test.mjs b/tests/init-api-key-prompt.test.mjs index 3fdb85c..7615c74 100644 --- a/tests/init-api-key-prompt.test.mjs +++ b/tests/init-api-key-prompt.test.mjs @@ -49,7 +49,7 @@ test('prefers the trimmed generic API key env override during init reconfigurati }); test('falls back to the provider-specific API key env when no generic or matching stored key exists', () => { - const env = { OPENAI_API_KEY: 'sk-openai' }; + const env = { OPENAI_API_KEY: ' sk-openai ' }; assert.equal( getExistingApiKeyForProvider('openai', { provider: 'anthropic', apiKey: 'sk-anthropic' }, 'OPENAI_API_KEY', env), From 49a068fee2ba34365a3c8d3a3702d0b5d3b97f3a Mon Sep 17 00:00:00 2001 From: 404-Page-Found <139850808+404-Page-Found@users.noreply.github.com> Date: Wed, 23 Sep 2026 15:34:37 +1000 Subject: [PATCH 18/22] fix(init): scope custom API keys to endpoint --- src/commands/init.ts | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/src/commands/init.ts b/src/commands/init.ts index 5101ba2..271e996 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -32,24 +32,36 @@ export function buildApiKeyPrompt(existingKey: string, apiKeyEnv: string) { export function getStoredApiKeyForProvider( selectedProvider: string, - storedConfig: Pick, 'provider' | 'apiKey'> | null, + selectedBaseUrl: string | undefined, + storedConfig: Pick, 'provider' | 'apiKey' | 'baseUrl'> | null, ): string | undefined { if (storedConfig?.provider !== selectedProvider || typeof storedConfig.apiKey !== 'string') { return undefined; } - return storedConfig.apiKey.trim(); + + if (selectedProvider === CUSTOM_PROVIDER_KEY) { + const storedUrl = storedConfig.baseUrl ? normalizeBaseUrl(storedConfig.baseUrl) : undefined; + const selectedUrl = selectedBaseUrl ? normalizeBaseUrl(selectedBaseUrl) : undefined; + if (storedUrl !== selectedUrl) { + return undefined; + } + } + + const storedKey = storedConfig.apiKey.trim(); + return storedKey || undefined; } export function getExistingApiKeyForProvider( provider: string, - storedConfig: Pick, 'provider' | 'apiKey'> | null, + baseUrl: string | undefined, + storedConfig: Pick, 'provider' | 'apiKey' | 'baseUrl'> | null, apiKeyEnv: string, env: NodeJS.ProcessEnv = process.env, ): string { return ( - env['COMMIT_ECHO_API_KEY']?.trim() ?? - getStoredApiKeyForProvider(provider, storedConfig) ?? - env[apiKeyEnv]?.trim() ?? + env['COMMIT_ECHO_API_KEY']?.trim() || + getStoredApiKeyForProvider(provider, baseUrl, storedConfig) || + env[apiKeyEnv]?.trim() || '' ); } @@ -170,11 +182,16 @@ async function promptProvider(existingConfig: Config | null): Promise, 'provider' | 'apiKey'> | null, + storedConfig: Pick, 'provider' | 'apiKey' | 'baseUrl'> | null, ): Promise { if (!provider.needsApiKey) return undefined; - const existingKey = getExistingApiKeyForProvider(provider.providerKey, storedConfig, provider.apiKeyEnv); + const existingKey = getExistingApiKeyForProvider( + provider.providerKey, + provider.baseUrl, + storedConfig, + provider.apiKeyEnv, + ); const keyResult = await text(buildApiKeyPrompt(existingKey, provider.apiKeyEnv)); if (isCancel(keyResult)) return null; return keyResult || existingKey || ''; @@ -365,7 +382,7 @@ interface CollectedSetup { async function collectConfig( existingConfig: Config | null, - storedConfig: Pick, 'provider' | 'apiKey'> | null, + storedConfig: Pick, 'provider' | 'apiKey' | 'baseUrl'> | null, ): Promise { const provider = await promptProvider(existingConfig); if (!provider) return null; From 15aa0af3b35508b346a75563e60af0b594197421 Mon Sep 17 00:00:00 2001 From: 404-Page-Found <139850808+404-Page-Found@users.noreply.github.com> Date: Wed, 23 Sep 2026 15:34:50 +1000 Subject: [PATCH 19/22] test(init): cover custom endpoint and empty API key fallback --- tests/init-api-key-prompt.test.mjs | 76 +++++++++++++++++++++++++++--- 1 file changed, 70 insertions(+), 6 deletions(-) diff --git a/tests/init-api-key-prompt.test.mjs b/tests/init-api-key-prompt.test.mjs index 7615c74..98049a5 100644 --- a/tests/init-api-key-prompt.test.mjs +++ b/tests/init-api-key-prompt.test.mjs @@ -19,18 +19,41 @@ test('leaves the API key prompt blank for new configs', () => { test('reuses a trimmed stored API key only for the same provider', () => { assert.equal( - getStoredApiKeyForProvider('openai', { provider: 'openai', apiKey: ' sk-openai ' }), + getStoredApiKeyForProvider('openai', 'https://api.openai.com/v1', { + provider: 'openai', + apiKey: ' sk-openai ', + }), 'sk-openai', ); assert.equal( - getStoredApiKeyForProvider('anthropic', { provider: 'openai', apiKey: 'sk-openai' }), + getStoredApiKeyForProvider('anthropic', 'https://api.anthropic.com/v1', { + provider: 'openai', + apiKey: 'sk-openai', + }), + undefined, + ); +}); + +test('reuses a custom API key only for the same normalized endpoint', () => { + const storedConfig = { + provider: '__custom__', + baseUrl: 'https://api.example.com/v1', + apiKey: ' sk-custom ', + }; + + assert.equal( + getStoredApiKeyForProvider('__custom__', 'https://api.example.com/v1/', storedConfig), + 'sk-custom', + ); + assert.equal( + getStoredApiKeyForProvider('__custom__', 'https://other.example.com/v1', storedConfig), undefined, ); }); test('ignores a non-string stored API key for the selected provider', () => { assert.equal( - getStoredApiKeyForProvider('openai', { provider: 'openai', apiKey: 123 }), + getStoredApiKeyForProvider('openai', 'https://api.openai.com/v1', { provider: 'openai', apiKey: 123 }), undefined, ); }); @@ -43,7 +66,13 @@ test('prefers the trimmed generic API key env override during init reconfigurati }; assert.equal( - getExistingApiKeyForProvider('openai', { provider: 'openai', apiKey: 'sk-stored' }, 'OPENAI_API_KEY', env), + getExistingApiKeyForProvider( + 'openai', + 'https://api.openai.com/v1', + { provider: 'openai', apiKey: 'sk-stored' }, + 'OPENAI_API_KEY', + env, + ), 'sk-generic', ); }); @@ -52,11 +81,46 @@ test('falls back to the provider-specific API key env when no generic or matchin const env = { OPENAI_API_KEY: ' sk-openai ' }; assert.equal( - getExistingApiKeyForProvider('openai', { provider: 'anthropic', apiKey: 'sk-anthropic' }, 'OPENAI_API_KEY', env), + getExistingApiKeyForProvider( + 'openai', + 'https://api.openai.com/v1', + { provider: 'anthropic', apiKey: 'sk-anthropic' }, + 'OPENAI_API_KEY', + env, + ), 'sk-openai', ); assert.equal( - getExistingApiKeyForProvider('openai', null, 'OPENAI_API_KEY', {}), + getExistingApiKeyForProvider('openai', 'https://api.openai.com/v1', null, 'OPENAI_API_KEY', {}), '', ); }); + +test('skips empty API-key candidates after trimming', () => { + assert.equal( + getExistingApiKeyForProvider( + 'openai', + 'https://api.openai.com/v1', + { provider: 'openai', apiKey: ' sk-stored ' }, + 'OPENAI_API_KEY', + { + COMMIT_ECHO_API_KEY: ' ', + OPENAI_API_KEY: 'sk-openai', + }, + ), + 'sk-stored', + ); + + assert.equal( + getExistingApiKeyForProvider( + 'openai', + 'https://api.openai.com/v1', + { provider: 'openai', apiKey: ' ' }, + 'OPENAI_API_KEY', + { + OPENAI_API_KEY: ' sk-openai ', + }, + ), + 'sk-openai', + ); +}); From 9c29b6f4b5b276f78dd511a883b50b771fd3151f Mon Sep 17 00:00:00 2001 From: 404-Page-Found <139850808+404-Page-Found@users.noreply.github.com> Date: Wed, 23 Sep 2026 16:06:48 +1000 Subject: [PATCH 20/22] fix: guard custom provider API key endpoint types --- src/commands/init.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/commands/init.ts b/src/commands/init.ts index 271e996..a0651a2 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -40,8 +40,12 @@ export function getStoredApiKeyForProvider( } if (selectedProvider === CUSTOM_PROVIDER_KEY) { - const storedUrl = storedConfig.baseUrl ? normalizeBaseUrl(storedConfig.baseUrl) : undefined; - const selectedUrl = selectedBaseUrl ? normalizeBaseUrl(selectedBaseUrl) : undefined; + if (typeof storedConfig.baseUrl !== 'string' || typeof selectedBaseUrl !== 'string') { + return undefined; + } + + const storedUrl = normalizeBaseUrl(storedConfig.baseUrl); + const selectedUrl = normalizeBaseUrl(selectedBaseUrl); if (storedUrl !== selectedUrl) { return undefined; } From cd946ed25b653ff4282b9c325eb9ceb4093edbc8 Mon Sep 17 00:00:00 2001 From: 404-Page-Found <139850808+404-Page-Found@users.noreply.github.com> Date: Wed, 23 Sep 2026 16:07:04 +1000 Subject: [PATCH 21/22] test: cover invalid custom provider endpoints --- tests/init-api-key-prompt.test.mjs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/init-api-key-prompt.test.mjs b/tests/init-api-key-prompt.test.mjs index 98049a5..867cda1 100644 --- a/tests/init-api-key-prompt.test.mjs +++ b/tests/init-api-key-prompt.test.mjs @@ -58,6 +58,32 @@ test('ignores a non-string stored API key for the selected provider', () => { ); }); +test('does not reuse a custom API key when either endpoint is missing or malformed', () => { + assert.equal( + getStoredApiKeyForProvider('__custom__', 'https://api.example.com/v1', { + provider: '__custom__', + baseUrl: 123, + apiKey: 'sk-custom', + }), + undefined, + ); + assert.equal( + getStoredApiKeyForProvider('__custom__', 'https://api.example.com/v1', { + provider: '__custom__', + apiKey: 'sk-custom', + }), + undefined, + ); + assert.equal( + getStoredApiKeyForProvider('__custom__', undefined, { + provider: '__custom__', + baseUrl: 'https://api.example.com/v1', + apiKey: 'sk-custom', + }), + undefined, + ); +}); + test('prefers the trimmed generic API key env override during init reconfiguration', () => { const env = { From 700dab902c91c3dbb6383d89160677c8df780c08 Mon Sep 17 00:00:00 2001 From: 404-Page-Found <139850808+404-Page-Found@users.noreply.github.com> Date: Wed, 23 Sep 2026 17:06:22 +1000 Subject: [PATCH 22/22] fix: remove duplicate blank line in API key prompt test --- tests/init-api-key-prompt.test.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/init-api-key-prompt.test.mjs b/tests/init-api-key-prompt.test.mjs index 867cda1..df08f91 100644 --- a/tests/init-api-key-prompt.test.mjs +++ b/tests/init-api-key-prompt.test.mjs @@ -84,7 +84,6 @@ test('does not reuse a custom API key when either endpoint is missing or malform ); }); - test('prefers the trimmed generic API key env override during init reconfiguration', () => { const env = { COMMIT_ECHO_API_KEY: ' sk-generic ',