sqlsaber 0.25.0__py3-none-any.whl → 0.27.0__py3-none-any.whl

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.

Potentially problematic release.


This version of sqlsaber might be problematic. Click here for more details.

Files changed (38) hide show
  1. sqlsaber/agents/__init__.py +2 -2
  2. sqlsaber/agents/base.py +1 -1
  3. sqlsaber/agents/mcp.py +1 -1
  4. sqlsaber/agents/pydantic_ai_agent.py +207 -135
  5. sqlsaber/application/__init__.py +1 -0
  6. sqlsaber/application/auth_setup.py +164 -0
  7. sqlsaber/application/db_setup.py +223 -0
  8. sqlsaber/application/model_selection.py +98 -0
  9. sqlsaber/application/prompts.py +115 -0
  10. sqlsaber/cli/auth.py +22 -50
  11. sqlsaber/cli/commands.py +22 -28
  12. sqlsaber/cli/completers.py +2 -0
  13. sqlsaber/cli/database.py +25 -86
  14. sqlsaber/cli/display.py +29 -9
  15. sqlsaber/cli/interactive.py +150 -127
  16. sqlsaber/cli/models.py +18 -28
  17. sqlsaber/cli/onboarding.py +325 -0
  18. sqlsaber/cli/streaming.py +15 -17
  19. sqlsaber/cli/threads.py +10 -6
  20. sqlsaber/config/api_keys.py +2 -2
  21. sqlsaber/config/settings.py +25 -2
  22. sqlsaber/database/__init__.py +55 -1
  23. sqlsaber/database/base.py +124 -0
  24. sqlsaber/database/csv.py +133 -0
  25. sqlsaber/database/duckdb.py +313 -0
  26. sqlsaber/database/mysql.py +345 -0
  27. sqlsaber/database/postgresql.py +328 -0
  28. sqlsaber/database/schema.py +66 -963
  29. sqlsaber/database/sqlite.py +258 -0
  30. sqlsaber/mcp/mcp.py +1 -1
  31. sqlsaber/tools/sql_tools.py +1 -1
  32. {sqlsaber-0.25.0.dist-info → sqlsaber-0.27.0.dist-info}/METADATA +43 -9
  33. sqlsaber-0.27.0.dist-info/RECORD +58 -0
  34. sqlsaber/database/connection.py +0 -535
  35. sqlsaber-0.25.0.dist-info/RECORD +0 -47
  36. {sqlsaber-0.25.0.dist-info → sqlsaber-0.27.0.dist-info}/WHEEL +0 -0
  37. {sqlsaber-0.25.0.dist-info → sqlsaber-0.27.0.dist-info}/entry_points.txt +0 -0
  38. {sqlsaber-0.25.0.dist-info → sqlsaber-0.27.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,7 +1,7 @@
1
1
  """Agents module for SQLSaber."""
2
2
 
3
- from .pydantic_ai_agent import build_sqlsaber_agent
3
+ from .pydantic_ai_agent import SQLSaberAgent
4
4
 
5
5
  __all__ = [
6
- "build_sqlsaber_agent",
6
+ "SQLSaberAgent",
7
7
  ]
sqlsaber/agents/base.py CHANGED
@@ -5,7 +5,7 @@ import json
5
5
  from abc import ABC, abstractmethod
6
6
  from typing import Any, AsyncIterator
7
7
 
8
- from sqlsaber.database.connection import (
8
+ from sqlsaber.database import (
9
9
  BaseDatabaseConnection,
10
10
  CSVConnection,
11
11
  DuckDBConnection,
sqlsaber/agents/mcp.py CHANGED
@@ -3,7 +3,7 @@
3
3
  from typing import AsyncIterator
4
4
 
5
5
  from sqlsaber.agents.base import BaseSQLAgent
6
- from sqlsaber.database.connection import BaseDatabaseConnection
6
+ from sqlsaber.database import BaseDatabaseConnection
7
7
 
8
8
 
9
9
  class MCPSQLAgent(BaseSQLAgent):
@@ -6,15 +6,16 @@ function tools, and streaming event types directly.
6
6
 
7
7
  import httpx
8
8
  from pydantic_ai import Agent, RunContext
9
- from pydantic_ai.models.anthropic import AnthropicModel
10
- from pydantic_ai.models.google import GoogleModel
11
- from pydantic_ai.models.openai import OpenAIResponsesModel
9
+ from pydantic_ai.models.anthropic import AnthropicModel, AnthropicModelSettings
10
+ from pydantic_ai.models.google import GoogleModel, GoogleModelSettings
11
+ from pydantic_ai.models.groq import GroqModelSettings
12
+ from pydantic_ai.models.openai import OpenAIResponsesModel, OpenAIResponsesModelSettings
12
13
  from pydantic_ai.providers.anthropic import AnthropicProvider
13
14
  from pydantic_ai.providers.google import GoogleProvider
14
15
 
15
16
  from sqlsaber.config import providers
16
17
  from sqlsaber.config.settings import Config
17
- from sqlsaber.database.connection import (
18
+ from sqlsaber.database import (
18
19
  BaseDatabaseConnection,
19
20
  CSVConnection,
20
21
  DuckDBConnection,
@@ -28,47 +29,119 @@ from sqlsaber.tools.registry import tool_registry
28
29
  from sqlsaber.tools.sql_tools import SQLTool
29
30
 
30
31
 
31
- def build_sqlsaber_agent(
32
- db_connection: BaseDatabaseConnection,
33
- database_name: str | None,
34
- ) -> Agent:
35
- """Create and configure a pydantic-ai Agent for SQLSaber.
36
-
37
- - Registers function tools that delegate to the existing tool registry
38
- - Attaches dynamic system prompt built from InstructionBuilder + MemoryManager
39
- - Ensures SQL tools have the active DB connection
40
- """
41
- # Ensure SQL tools receive the active connection
42
- for tool_name in tool_registry.list_tools(category="sql"):
43
- tool = tool_registry.get_tool(tool_name)
44
- if isinstance(tool, SQLTool):
45
- tool.set_connection(db_connection)
46
-
47
- cfg = Config()
48
- # Ensure provider env var is hydrated from keyring for current provider (Config.validate handles it)
49
- cfg.validate()
50
-
51
- # Build model/agent. For some providers (e.g., google), construct provider model explicitly to
52
- # allow arbitrary model IDs even if not in pydantic-ai's KnownModelName.
53
- model_name_only = (
54
- cfg.model_name.split(":", 1)[1] if ":" in cfg.model_name else cfg.model_name
55
- )
56
-
57
- provider = providers.provider_from_model(cfg.model_name) or ""
58
- if provider == "google":
59
- model_obj = GoogleModel(
60
- model_name_only, provider=GoogleProvider(api_key=cfg.api_key)
32
+ class SQLSaberAgent:
33
+ """Pydantic-AI Agent wrapper for SQLSaber with enhanced state management."""
34
+
35
+ def __init__(
36
+ self,
37
+ db_connection: BaseDatabaseConnection,
38
+ database_name: str | None = None,
39
+ memory_manager: MemoryManager | None = None,
40
+ thinking_enabled: bool | None = None,
41
+ ):
42
+ self.db_connection = db_connection
43
+ self.database_name = database_name
44
+ self.config = Config()
45
+ self.memory_manager = memory_manager or MemoryManager()
46
+ self.instruction_builder = InstructionBuilder(tool_registry)
47
+ self.db_type = self._get_database_type_name()
48
+
49
+ # Thinking configuration (CLI override or config default)
50
+ self.thinking_enabled = (
51
+ thinking_enabled
52
+ if thinking_enabled is not None
53
+ else self.config.thinking_enabled
61
54
  )
62
- agent = Agent(model_obj, name="sqlsaber")
63
- elif provider == "anthropic" and bool(getattr(cfg, "oauth_token", None)):
64
- # Build custom httpx client to inject OAuth headers for Anthropic
55
+
56
+ # Configure SQL tools with the database connection
57
+ self._configure_sql_tools()
58
+
59
+ # Create the pydantic-ai agent
60
+ self.agent = self._build_agent()
61
+
62
+ def _configure_sql_tools(self) -> None:
63
+ """Ensure SQL tools receive the active database connection."""
64
+ for tool_name in tool_registry.list_tools(category="sql"):
65
+ tool = tool_registry.get_tool(tool_name)
66
+ if isinstance(tool, SQLTool):
67
+ tool.set_connection(self.db_connection)
68
+
69
+ def _build_agent(self) -> Agent:
70
+ """Create and configure the pydantic-ai Agent."""
71
+ self.config.validate()
72
+
73
+ model_name_only = (
74
+ self.config.model_name.split(":", 1)[1]
75
+ if ":" in self.config.model_name
76
+ else self.config.model_name
77
+ )
78
+
79
+ provider = providers.provider_from_model(self.config.model_name) or ""
80
+ self.is_oauth = provider == "anthropic" and bool(
81
+ getattr(self.config, "oauth_token", None)
82
+ )
83
+
84
+ agent = self._create_agent_for_provider(provider, model_name_only)
85
+ self._setup_system_prompt(agent)
86
+ self._register_tools(agent)
87
+
88
+ return agent
89
+
90
+ def _create_agent_for_provider(self, provider: str, model_name: str) -> Agent:
91
+ """Create the agent based on the provider type."""
92
+ if provider == "google":
93
+ model_obj = GoogleModel(
94
+ model_name, provider=GoogleProvider(api_key=self.config.api_key)
95
+ )
96
+ if self.thinking_enabled:
97
+ settings = GoogleModelSettings(
98
+ google_thinking_config={"include_thoughts": True}
99
+ )
100
+ return Agent(model_obj, name="sqlsaber", model_settings=settings)
101
+ return Agent(model_obj, name="sqlsaber")
102
+ elif provider == "anthropic" and self.is_oauth:
103
+ return self._create_oauth_anthropic_agent(model_name)
104
+ elif provider == "anthropic":
105
+ if self.thinking_enabled:
106
+ settings = AnthropicModelSettings(
107
+ anthropic_thinking={
108
+ "type": "enabled",
109
+ "budget_tokens": 2048,
110
+ },
111
+ max_tokens=8192,
112
+ )
113
+ return Agent(
114
+ self.config.model_name, name="sqlsaber", model_settings=settings
115
+ )
116
+ return Agent(self.config.model_name, name="sqlsaber")
117
+ elif provider == "openai":
118
+ model_obj = OpenAIResponsesModel(model_name)
119
+ if self.thinking_enabled:
120
+ settings = OpenAIResponsesModelSettings(
121
+ openai_reasoning_effort="medium",
122
+ openai_reasoning_summary="auto",
123
+ )
124
+ return Agent(model_obj, name="sqlsaber", model_settings=settings)
125
+ return Agent(model_obj, name="sqlsaber")
126
+ elif provider == "groq":
127
+ if self.thinking_enabled:
128
+ settings = GroqModelSettings(groq_reasoning_format="parsed")
129
+ return Agent(
130
+ self.config.model_name, name="sqlsaber", model_settings=settings
131
+ )
132
+ return Agent(self.config.model_name, name="sqlsaber")
133
+ else:
134
+ return Agent(self.config.model_name, name="sqlsaber")
135
+
136
+ def _create_oauth_anthropic_agent(self, model_name: str) -> Agent:
137
+ """Create an Anthropic agent with OAuth configuration."""
138
+
65
139
  async def add_oauth_headers(request: httpx.Request) -> None: # type: ignore[override]
66
- # Remove API-key header if present and add OAuth headers
67
140
  if "x-api-key" in request.headers:
68
141
  del request.headers["x-api-key"]
69
142
  request.headers.update(
70
143
  {
71
- "Authorization": f"Bearer {cfg.oauth_token}",
144
+ "Authorization": f"Bearer {self.config.oauth_token}",
72
145
  "anthropic-version": "2023-06-01",
73
146
  "anthropic-beta": "oauth-2025-04-20",
74
147
  "User-Agent": "ClaudeCode/1.0 (Anthropic Claude Code CLI)",
@@ -79,100 +152,99 @@ def build_sqlsaber_agent(
79
152
 
80
153
  http_client = httpx.AsyncClient(event_hooks={"request": [add_oauth_headers]})
81
154
  provider_obj = AnthropicProvider(api_key="placeholder", http_client=http_client)
82
- model_obj = AnthropicModel(model_name_only, provider=provider_obj)
83
- agent = Agent(model_obj, name="sqlsaber")
84
- elif provider == "openai":
85
- # Use OpenAI Responses Model for structured output capabilities
86
- model_obj = OpenAIResponsesModel(model_name_only)
87
- agent = Agent(model_obj, name="sqlsaber")
88
- else:
89
- agent = Agent(cfg.model_name, name="sqlsaber")
90
-
91
- # Memory + dynamic system prompt
92
- memory_manager = MemoryManager()
93
- instruction_builder = InstructionBuilder(tool_registry)
94
-
95
- is_oauth = provider == "anthropic" and bool(getattr(cfg, "oauth_token", None))
96
-
97
- if not is_oauth:
98
-
99
- @agent.system_prompt(dynamic=True)
100
- async def sqlsaber_system_prompt(ctx: RunContext) -> str:
101
- db_type = _get_database_type_name(db_connection)
102
- instructions = instruction_builder.build_instructions(db_type=db_type)
103
-
104
- # Add memory context if available
105
- if database_name:
106
- mem = memory_manager.format_memories_for_prompt(database_name)
107
- else:
108
- mem = ""
155
+ model_obj = AnthropicModel(model_name, provider=provider_obj)
156
+ if self.thinking_enabled:
157
+ settings = AnthropicModelSettings(
158
+ anthropic_thinking={
159
+ "type": "enabled",
160
+ "budget_tokens": 2048,
161
+ },
162
+ max_tokens=8192,
163
+ )
164
+ return Agent(model_obj, name="sqlsaber", model_settings=settings)
165
+ return Agent(model_obj, name="sqlsaber")
166
+
167
+ def _setup_system_prompt(self, agent: Agent) -> None:
168
+ """Set up the dynamic system prompt for the agent."""
169
+ if not self.is_oauth:
109
170
 
110
- parts = [p for p in (instructions, mem) if p and p.strip()]
111
- return "\n\n".join(parts) if parts else ""
112
- else:
113
-
114
- @agent.system_prompt(dynamic=True)
115
- async def sqlsaber_system_prompt(ctx: RunContext) -> str:
116
- # Minimal system prompt in OAuth mode to match Claude Code identity
117
- return "You are Claude Code, Anthropic's official CLI for Claude."
118
-
119
- # Expose helpers and context on agent instance
120
- agent._sqlsaber_memory_manager = memory_manager # type: ignore[attr-defined]
121
- agent._sqlsaber_database_name = database_name # type: ignore[attr-defined]
122
- agent._sqlsaber_instruction_builder = instruction_builder # type: ignore[attr-defined]
123
- agent._sqlsaber_db_type = _get_database_type_name(db_connection) # type: ignore[attr-defined]
124
- agent._sqlsaber_is_oauth = is_oauth # type: ignore[attr-defined]
125
-
126
- # Tool wrappers that invoke the registered tools
127
- @agent.tool(name="list_tables")
128
- async def list_tables(ctx: RunContext) -> str:
129
- """
130
- Get a list of all tables in the database with row counts.
131
- Use this first to discover available tables.
132
- """
133
- tool = tool_registry.get_tool("list_tables")
134
- return await tool.execute()
135
-
136
- @agent.tool(name="introspect_schema")
137
- async def introspect_schema(
138
- ctx: RunContext, table_pattern: str | None = None
139
- ) -> str:
140
- """
141
- Introspect database schema to understand table structures.
142
-
143
- Args:
144
- table_pattern: Optional pattern to filter tables (e.g., 'public.users', 'user%', '%order%')
145
- """
146
- tool = tool_registry.get_tool("introspect_schema")
147
- return await tool.execute(table_pattern=table_pattern)
148
-
149
- @agent.tool(name="execute_sql")
150
- async def execute_sql(ctx: RunContext, query: str, limit: int | None = 100) -> str:
151
- """
152
- Execute a SQL query and return the results.
153
-
154
- Args:
155
- query: SQL query to execute
156
- limit: Maximum number of rows to return (default: 100)
157
- """
158
- tool = tool_registry.get_tool("execute_sql")
159
- return await tool.execute(query=query, limit=limit)
160
-
161
- return agent
162
-
163
-
164
- def _get_database_type_name(db: BaseDatabaseConnection) -> str:
165
- """Get the human-readable database type name (mirrors BaseSQLAgent)."""
166
-
167
- if isinstance(db, PostgreSQLConnection):
168
- return "PostgreSQL"
169
- elif isinstance(db, MySQLConnection):
170
- return "MySQL"
171
- elif isinstance(db, SQLiteConnection):
172
- return "SQLite"
173
- elif isinstance(db, DuckDBConnection):
174
- return "DuckDB"
175
- elif isinstance(db, CSVConnection):
176
- return "DuckDB"
177
- else:
178
- return "database"
171
+ @agent.system_prompt(dynamic=True)
172
+ async def sqlsaber_system_prompt(ctx: RunContext) -> str:
173
+ instructions = self.instruction_builder.build_instructions(
174
+ db_type=self.db_type
175
+ )
176
+
177
+ # Add memory context if available
178
+ mem = ""
179
+ if self.database_name:
180
+ mem = self.memory_manager.format_memories_for_prompt(
181
+ self.database_name
182
+ )
183
+
184
+ parts = [p for p in (instructions, mem) if p and p.strip()]
185
+ return "\n\n".join(parts) if parts else ""
186
+ else:
187
+
188
+ @agent.system_prompt(dynamic=True)
189
+ async def sqlsaber_system_prompt(ctx: RunContext) -> str:
190
+ return "You are Claude Code, Anthropic's official CLI for Claude."
191
+
192
+ def _register_tools(self, agent: Agent) -> None:
193
+ """Register all the SQL tools with the agent."""
194
+
195
+ @agent.tool(name="list_tables")
196
+ async def list_tables(ctx: RunContext) -> str:
197
+ """
198
+ Get a list of all tables in the database with row counts.
199
+ Use this first to discover available tables.
200
+ """
201
+ tool = tool_registry.get_tool("list_tables")
202
+ return await tool.execute()
203
+
204
+ @agent.tool(name="introspect_schema")
205
+ async def introspect_schema(
206
+ ctx: RunContext, table_pattern: str | None = None
207
+ ) -> str:
208
+ """
209
+ Introspect database schema to understand table structures.
210
+
211
+ Args:
212
+ table_pattern: Optional pattern to filter tables (e.g., 'public.users', 'user%', '%order%')
213
+ """
214
+ tool = tool_registry.get_tool("introspect_schema")
215
+ return await tool.execute(table_pattern=table_pattern)
216
+
217
+ @agent.tool(name="execute_sql")
218
+ async def execute_sql(
219
+ ctx: RunContext, query: str, limit: int | None = 100
220
+ ) -> str:
221
+ """
222
+ Execute a SQL query and return the results.
223
+
224
+ Args:
225
+ query: SQL query to execute
226
+ limit: Maximum number of rows to return (default: 100)
227
+ """
228
+ tool = tool_registry.get_tool("execute_sql")
229
+ return await tool.execute(query=query, limit=limit)
230
+
231
+ def set_thinking(self, enabled: bool) -> None:
232
+ """Update thinking settings and rebuild the agent."""
233
+ self.thinking_enabled = enabled
234
+ # Rebuild agent with new thinking settings
235
+ self.agent = self._build_agent()
236
+
237
+ def _get_database_type_name(self) -> str:
238
+ """Get the human-readable database type name."""
239
+ if isinstance(self.db_connection, PostgreSQLConnection):
240
+ return "PostgreSQL"
241
+ elif isinstance(self.db_connection, MySQLConnection):
242
+ return "MySQL"
243
+ elif isinstance(self.db_connection, SQLiteConnection):
244
+ return "SQLite"
245
+ elif isinstance(self.db_connection, DuckDBConnection):
246
+ return "DuckDB"
247
+ elif isinstance(self.db_connection, CSVConnection):
248
+ return "DuckDB"
249
+ else:
250
+ return "database"
@@ -0,0 +1 @@
1
+ """Application layer for SQLsaber - shared business logic and interactive flows."""
@@ -0,0 +1,164 @@
1
+ """Shared auth setup logic for onboarding and CLI."""
2
+
3
+ import asyncio
4
+
5
+ from questionary import Choice
6
+ from rich.console import Console
7
+
8
+ from sqlsaber.application.prompts import Prompter
9
+ from sqlsaber.config import providers
10
+ from sqlsaber.config.api_keys import APIKeyManager
11
+ from sqlsaber.config.auth import AuthConfigManager, AuthMethod
12
+ from sqlsaber.config.oauth_flow import AnthropicOAuthFlow
13
+
14
+ console = Console()
15
+
16
+
17
+ async def select_provider(prompter: Prompter, default: str = "anthropic") -> str | None:
18
+ """Interactive provider selection.
19
+
20
+ Args:
21
+ prompter: Prompter instance for interaction
22
+ default: Default provider to select
23
+
24
+ Returns:
25
+ Selected provider name or None if cancelled
26
+ """
27
+ provider = await prompter.select(
28
+ "Select AI provider:", choices=providers.all_keys(), default=default
29
+ )
30
+ return provider
31
+
32
+
33
+ async def configure_oauth_anthropic(
34
+ auth_manager: AuthConfigManager, run_in_thread: bool = False
35
+ ) -> bool:
36
+ """Configure Anthropic OAuth.
37
+
38
+ Args:
39
+ auth_manager: AuthConfigManager instance
40
+ run_in_thread: Whether to run OAuth flow in a separate thread (for onboarding)
41
+
42
+ Returns:
43
+ True if OAuth configured successfully, False otherwise
44
+ """
45
+ flow = AnthropicOAuthFlow()
46
+
47
+ if run_in_thread:
48
+ # Run in thread to avoid event loop conflicts (onboarding)
49
+ oauth_success = await asyncio.to_thread(flow.authenticate)
50
+ else:
51
+ # Run directly (CLI)
52
+ oauth_success = flow.authenticate()
53
+
54
+ if oauth_success:
55
+ auth_manager.set_auth_method(AuthMethod.CLAUDE_PRO)
56
+ return True
57
+
58
+ return False
59
+
60
+
61
+ async def configure_api_key(
62
+ provider: str, api_key_manager: APIKeyManager, auth_manager: AuthConfigManager
63
+ ) -> bool:
64
+ """Configure API key for a provider.
65
+
66
+ Args:
67
+ provider: Provider name
68
+ api_key_manager: APIKeyManager instance
69
+ auth_manager: AuthConfigManager instance
70
+
71
+ Returns:
72
+ True if API key configured successfully, False otherwise
73
+ """
74
+ # Get API key (cascades env -> keyring -> prompt)
75
+ api_key = api_key_manager.get_api_key(provider)
76
+
77
+ if api_key:
78
+ auth_manager.set_auth_method(AuthMethod.API_KEY)
79
+ return True
80
+
81
+ return False
82
+
83
+
84
+ async def setup_auth(
85
+ prompter: Prompter,
86
+ auth_manager: AuthConfigManager,
87
+ api_key_manager: APIKeyManager,
88
+ allow_oauth: bool = True,
89
+ default_provider: str = "anthropic",
90
+ run_oauth_in_thread: bool = False,
91
+ ) -> tuple[bool, str | None]:
92
+ """Interactive authentication setup.
93
+
94
+ Args:
95
+ prompter: Prompter instance for interaction
96
+ auth_manager: AuthConfigManager instance
97
+ api_key_manager: APIKeyManager instance
98
+ allow_oauth: Whether to offer OAuth option for Anthropic
99
+ default_provider: Default provider to select
100
+ run_oauth_in_thread: Whether to run OAuth in thread (for onboarding)
101
+
102
+ Returns:
103
+ Tuple of (success: bool, provider: str | None)
104
+ """
105
+ # Check if auth is already configured
106
+ if auth_manager.has_auth_configured():
107
+ console.print("[green]✓ Authentication already configured![/green]")
108
+ return True, None
109
+
110
+ # Select provider
111
+ provider = await select_provider(prompter, default=default_provider)
112
+
113
+ if provider is None:
114
+ return False, None
115
+
116
+ # For Anthropic, offer OAuth or API key
117
+ if provider == "anthropic" and allow_oauth:
118
+ method_choice = await prompter.select(
119
+ "Authentication method:",
120
+ choices=[
121
+ Choice("API Key", value=AuthMethod.API_KEY),
122
+ Choice("Claude Pro/Max (OAuth)", value=AuthMethod.CLAUDE_PRO),
123
+ ],
124
+ )
125
+
126
+ if method_choice is None:
127
+ return False, None
128
+
129
+ if method_choice == AuthMethod.CLAUDE_PRO:
130
+ console.print()
131
+ oauth_success = await configure_oauth_anthropic(
132
+ auth_manager, run_in_thread=run_oauth_in_thread
133
+ )
134
+ if oauth_success:
135
+ console.print(
136
+ "[green]✓ Anthropic OAuth configured successfully![/green]"
137
+ )
138
+ return True, provider
139
+ else:
140
+ console.print("[red]✗ Anthropic OAuth setup failed.[/red]")
141
+ return False, None
142
+
143
+ # API key flow
144
+ env_var = api_key_manager.get_env_var_name(provider)
145
+
146
+ console.print()
147
+ console.print(f"[dim]To use {provider.title()}, you need an API key.[/dim]")
148
+ console.print(f"[dim]You can set the {env_var} environment variable,[/dim]")
149
+ console.print("[dim]or enter it now to store securely in your OS keychain.[/dim]")
150
+ console.print()
151
+
152
+ # Configure API key
153
+ api_key_configured = await configure_api_key(
154
+ provider, api_key_manager, auth_manager
155
+ )
156
+
157
+ if api_key_configured:
158
+ console.print(
159
+ f"[green]✓ {provider.title()} API key configured successfully![/green]"
160
+ )
161
+ return True, provider
162
+ else:
163
+ console.print("[yellow]No API key provided.[/yellow]")
164
+ return False, None