diff --git a/SKILL.md b/SKILL.md index da2836cf..65eac4f3 100644 --- a/SKILL.md +++ b/SKILL.md @@ -185,6 +185,7 @@ See [references/SKILLS.md](references/SKILLS.md) for details. | Action | Command | |--------|---------| | **Authenticate** | `smithery login` | +| **Log out** | `smithery logout` | | **Check auth** | `smithery whoami` | | **Search MCP servers** | `smithery search [term]` | | **Search skills** | `smithery skills search [term]` | diff --git a/references/AUTH.md b/references/AUTH.md index 05a072b0..74b07c61 100644 --- a/references/AUTH.md +++ b/references/AUTH.md @@ -18,6 +18,19 @@ This will: The CLI will wait up to 5 minutes for confirmation. +## Logout + +To remove all local credentials: + +```bash +smithery logout +``` + +This removes: +- API key from local settings +- Namespace configuration +- All server configurations from keychain + ## Check Auth Status ```bash diff --git a/src/index.ts b/src/index.ts index ddc98b92..dc191c08 100644 --- a/src/index.ts +++ b/src/index.ts @@ -459,6 +459,31 @@ program } }) +// Logout command +program + .command("logout") + .description("Log out and remove all local credentials") + .action(async () => { + const { clearApiKey, clearNamespace } = await import( + "./utils/smithery-settings" + ) + const { clearAllConfigs } = await import("./lib/keychain.js") + + console.log(chalk.cyan("Logging out of Smithery...")) + + // Clear API key + await clearApiKey() + + // Clear namespace + await clearNamespace() + + // Clear keychain entries + await clearAllConfigs() + + console.log(chalk.green("✓ Successfully logged out")) + console.log(chalk.gray("All local credentials have been removed")) + }) + // Show API key command program .command("whoami") @@ -539,11 +564,9 @@ program console.log(chalk.cyan("API Key:"), masked) console.log(chalk.gray("Use --full to display the complete key")) } - } catch (error) { - console.error(chalk.red("✗ Failed to retrieve API key")) - const errorMessage = - error instanceof Error ? error.message : String(error) - console.error(chalk.gray(errorMessage)) + } catch (_error) { + console.log(chalk.yellow("Not logged in")) + console.log(chalk.gray("Run 'smithery login' to authenticate")) process.exit(1) } }) diff --git a/src/lib/keychain.ts b/src/lib/keychain.ts index 390fcb59..47a624e6 100644 --- a/src/lib/keychain.ts +++ b/src/lib/keychain.ts @@ -134,3 +134,35 @@ export async function deleteConfig(qualifiedName: string): Promise { ) } } + +/** + * Clear all server configurations from OS keychain + */ +export async function clearAllConfigs(): Promise { + const kt = getKeytar() + if (!kt) { + verbose("Keychain not available, skipping clear all configs") + return + } + + verbose("Clearing all configs from keychain") + try { + // keytar v7.9+ has findCredentials + const credentials = await ( + kt as Keytar & { + findCredentials( + service: string, + ): Promise> + } + ).findCredentials(SERVICE_NAME) + for (const cred of credentials) { + await kt.deletePassword(SERVICE_NAME, cred.account) + verbose(`Deleted keychain entry: ${cred.account}`) + } + verbose(`Cleared ${credentials.length} keychain entries`) + } catch (error) { + verbose( + `Failed to clear keychain configs: ${error instanceof Error ? error.message : String(error)}`, + ) + } +} diff --git a/src/utils/smithery-settings.ts b/src/utils/smithery-settings.ts index 89c83d40..e81dd697 100644 --- a/src/utils/smithery-settings.ts +++ b/src/utils/smithery-settings.ts @@ -285,6 +285,19 @@ export const clearApiKey = async (): Promise => { return await saveSettings(settingsData, getSettingsPath()) } +export const clearNamespace = async (): Promise => { + const initResult = await initializeSettings() + if (!initResult.success || !initResult.data) { + return initResult + } + + // Remove namespace from settings + const { namespace: _removed, ...settingsWithoutNamespace } = initResult.data + settingsData = settingsWithoutNamespace as Settings + + return await saveSettings(settingsData, getSettingsPath()) +} + export const hasAskedConsent = async (): Promise => { await initializeSettings() return settingsData?.askedConsent || false