sqlsaber 0.8.1__py3-none-any.whl → 0.9.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.

sqlsaber/cli/auth.py CHANGED
@@ -1,7 +1,7 @@
1
1
  """Authentication CLI commands."""
2
2
 
3
3
  import questionary
4
- import typer
4
+ import cyclopts
5
5
  from rich.console import Console
6
6
 
7
7
  from sqlsaber.config.auth import AuthConfigManager, AuthMethod
@@ -12,15 +12,14 @@ console = Console()
12
12
  config_manager = AuthConfigManager()
13
13
 
14
14
  # Create the authentication management CLI app
15
- auth_app = typer.Typer(
15
+ auth_app = cyclopts.App(
16
16
  name="auth",
17
17
  help="Manage authentication configuration",
18
- add_completion=True,
19
18
  )
20
19
 
21
20
 
22
- @auth_app.command("setup")
23
- def setup_auth():
21
+ @auth_app.command
22
+ def setup():
24
23
  """Configure authentication method for SQLSaber."""
25
24
  console.print("\n[bold]SQLSaber Authentication Setup[/bold]\n")
26
25
 
@@ -79,8 +78,8 @@ def setup_auth():
79
78
  )
80
79
 
81
80
 
82
- @auth_app.command("status")
83
- def show_auth_status():
81
+ @auth_app.command
82
+ def status():
84
83
  """Show current authentication configuration."""
85
84
  auth_method = config_manager.get_auth_method()
86
85
 
@@ -104,8 +103,8 @@ def show_auth_status():
104
103
  console.print("[yellow]OAuth token missing or expired[/yellow]")
105
104
 
106
105
 
107
- @auth_app.command("reset")
108
- def reset_auth():
106
+ @auth_app.command
107
+ def reset():
109
108
  """Reset authentication configuration."""
110
109
  if not config_manager.has_auth_configured():
111
110
  console.print("[yellow]No authentication configuration to reset.[/yellow]")
@@ -137,6 +136,6 @@ def reset_auth():
137
136
  console.print("Reset cancelled.")
138
137
 
139
138
 
140
- def create_auth_app() -> typer.Typer:
139
+ def create_auth_app() -> cyclopts.App:
141
140
  """Return the authentication management CLI app."""
142
141
  return auth_app
sqlsaber/cli/commands.py CHANGED
@@ -1,9 +1,11 @@
1
1
  """CLI command definitions and handlers."""
2
2
 
3
3
  import asyncio
4
+ import sys
4
5
  from pathlib import Path
6
+ from typing import Annotated
5
7
 
6
- import typer
8
+ import cyclopts
7
9
  from rich.console import Console
8
10
 
9
11
  from sqlsaber.agents.anthropic import AnthropicSQLAgent
@@ -16,10 +18,18 @@ from sqlsaber.cli.streaming import StreamingQueryHandler
16
18
  from sqlsaber.config.database import DatabaseConfigManager
17
19
  from sqlsaber.database.connection import DatabaseConnection
18
20
 
19
- app = typer.Typer(
21
+
22
+ class CLIError(Exception):
23
+ """Exception raised for CLI errors that should result in exit."""
24
+
25
+ def __init__(self, message: str, exit_code: int = 1):
26
+ super().__init__(message)
27
+ self.exit_code = exit_code
28
+
29
+
30
+ app = cyclopts.App(
20
31
  name="sqlsaber",
21
32
  help="SQLSaber - Use the agent Luke!\n\nSQL assistant for your database",
22
- add_completion=True,
23
33
  )
24
34
 
25
35
 
@@ -27,40 +37,49 @@ console = Console()
27
37
  config_manager = DatabaseConfigManager()
28
38
 
29
39
 
30
- @app.callback()
31
- def main_callback(
32
- database: str | None = typer.Option(
33
- None,
34
- "--database",
35
- "-d",
36
- help="Database connection name (uses default if not specified)",
37
- ),
40
+ @app.meta.default
41
+ def meta_handler(
42
+ database: Annotated[
43
+ str | None,
44
+ cyclopts.Parameter(
45
+ ["--database", "-d"],
46
+ help="Database connection name (uses default if not specified)",
47
+ ),
48
+ ] = None,
38
49
  ):
39
50
  """
40
51
  Query your database using natural language.
41
52
 
42
53
  Examples:
43
- sb query # Start interactive mode
44
- sb query "show me all users" # Run a single query with default database
45
- sb query -d mydb "show me users" # Run a query with specific database
54
+ saber # Start interactive mode
55
+ saber "show me all users" # Run a single query with default database
56
+ saber -d mydb "show me users" # Run a query with specific database
46
57
  """
47
- pass
58
+ # Store database in app context for commands to access
59
+ app.meta["database"] = database
48
60
 
49
61
 
50
- @app.command()
62
+ @app.default
51
63
  def query(
52
- query_text: str | None = typer.Argument(
53
- None,
54
- help="SQL query in natural language (if not provided, starts interactive mode)",
55
- ),
56
- database: str | None = typer.Option(
57
- None,
58
- "--database",
59
- "-d",
60
- help="Database connection name (uses default if not specified)",
61
- ),
64
+ query_text: Annotated[
65
+ str | None,
66
+ cyclopts.Parameter(
67
+ help="SQL query in natural language (if not provided, starts interactive mode)",
68
+ ),
69
+ ] = None,
70
+ database: Annotated[
71
+ str | None,
72
+ cyclopts.Parameter(
73
+ ["--database", "-d"],
74
+ help="Database connection name (uses default if not specified)",
75
+ ),
76
+ ] = None,
62
77
  ):
63
- """Run a query against the database or start interactive mode."""
78
+ """Run a query against the database or start interactive mode.
79
+
80
+ When called without arguments, starts interactive mode.
81
+ When called with a query string, executes that query and exits.
82
+ """
64
83
 
65
84
  async def run_session():
66
85
  # Get database configuration or handle direct CSV file
@@ -69,35 +88,24 @@ def query(
69
88
  if database.endswith(".csv"):
70
89
  csv_path = Path(database).expanduser().resolve()
71
90
  if not csv_path.exists():
72
- console.print(
73
- f"[bold red]Error:[/bold red] CSV file '{database}' not found."
74
- )
75
- raise typer.Exit(1)
91
+ raise CLIError(f"CSV file '{database}' not found.")
76
92
  connection_string = f"csv:///{csv_path}"
77
93
  db_name = csv_path.stem
78
94
  else:
79
95
  # Look up configured database connection
80
96
  db_config = config_manager.get_database(database)
81
97
  if not db_config:
82
- console.print(
83
- f"[bold red]Error:[/bold red] Database connection '{database}' not found."
84
- )
85
- console.print(
86
- "Use 'sqlsaber db list' to see available connections."
98
+ raise CLIError(
99
+ f"Database connection '{database}' not found. Use 'sqlsaber db list' to see available connections."
87
100
  )
88
- raise typer.Exit(1)
89
101
  connection_string = db_config.to_connection_string()
90
102
  db_name = db_config.name
91
103
  else:
92
104
  db_config = config_manager.get_default_database()
93
105
  if not db_config:
94
- console.print(
95
- "[bold red]Error:[/bold red] No database connections configured."
96
- )
97
- console.print(
98
- "Use 'sqlsaber db add <name>' to add a database connection."
106
+ raise CLIError(
107
+ "No database connections configured. Use 'sqlsaber db add <name>' to add a database connection."
99
108
  )
100
- raise typer.Exit(1)
101
109
  connection_string = db_config.to_connection_string()
102
110
  db_name = db_config.name
103
111
 
@@ -105,10 +113,7 @@ def query(
105
113
  try:
106
114
  db_conn = DatabaseConnection(connection_string)
107
115
  except Exception as e:
108
- console.print(
109
- f"[bold red]Error creating database connection:[/bold red] {e}"
110
- )
111
- raise typer.Exit(1)
116
+ raise CLIError(f"Error creating database connection: {e}")
112
117
 
113
118
  # Create agent instance with database name for memory context
114
119
  agent = AnthropicSQLAgent(db_conn, db_name)
@@ -132,25 +137,29 @@ def query(
132
137
  await db_conn.close()
133
138
  console.print("\n[green]Goodbye![/green]")
134
139
 
135
- # Run the async function
136
- asyncio.run(run_session())
140
+ # Run the async function with proper error handling
141
+ try:
142
+ asyncio.run(run_session())
143
+ except CLIError as e:
144
+ console.print(f"[bold red]Error:[/bold red] {e}")
145
+ sys.exit(e.exit_code)
137
146
 
138
147
 
139
148
  # Add authentication management commands
140
149
  auth_app = create_auth_app()
141
- app.add_typer(auth_app, name="auth")
150
+ app.command(auth_app, name="auth")
142
151
 
143
152
  # Add database management commands after main callback is defined
144
153
  db_app = create_db_app()
145
- app.add_typer(db_app, name="db")
154
+ app.command(db_app, name="db")
146
155
 
147
156
  # Add memory management commands
148
157
  memory_app = create_memory_app()
149
- app.add_typer(memory_app, name="memory")
158
+ app.command(memory_app, name="memory")
150
159
 
151
160
  # Add model management commands
152
161
  models_app = create_models_app()
153
- app.add_typer(models_app, name="models")
162
+ app.command(models_app, name="models")
154
163
 
155
164
 
156
165
  def main():
sqlsaber/cli/database.py CHANGED
@@ -2,10 +2,12 @@
2
2
 
3
3
  import asyncio
4
4
  import getpass
5
+ import sys
5
6
  from pathlib import Path
7
+ from typing import Annotated
6
8
 
7
9
  import questionary
8
- import typer
10
+ import cyclopts
9
11
  from rich.console import Console
10
12
  from rich.table import Table
11
13
 
@@ -17,45 +19,64 @@ console = Console()
17
19
  config_manager = DatabaseConfigManager()
18
20
 
19
21
  # Create the database management CLI app
20
- db_app = typer.Typer(
22
+ db_app = cyclopts.App(
21
23
  name="db",
22
24
  help="Manage database connections",
23
- add_completion=True,
24
25
  )
25
26
 
26
27
 
27
- @db_app.command("add")
28
- def add_database(
29
- name: str = typer.Argument(..., help="Name for the database connection"),
30
- type: str = typer.Option(
31
- "postgresql",
32
- "--type",
33
- "-t",
34
- help="Database type (postgresql, mysql, sqlite)",
35
- ),
36
- host: str | None = typer.Option(None, "--host", "-h", help="Database host"),
37
- port: int | None = typer.Option(None, "--port", "-p", help="Database port"),
38
- database: str | None = typer.Option(
39
- None, "--database", "--db", help="Database name"
40
- ),
41
- username: str | None = typer.Option(None, "--username", "-u", help="Username"),
42
- ssl_mode: str | None = typer.Option(
43
- None,
44
- "--ssl-mode",
45
- help="SSL mode (disable, allow, prefer, require, verify-ca, verify-full for PostgreSQL; DISABLED, PREFERRED, REQUIRED, VERIFY_CA, VERIFY_IDENTITY for MySQL)",
46
- ),
47
- ssl_ca: str | None = typer.Option(
48
- None, "--ssl-ca", help="SSL CA certificate file path"
49
- ),
50
- ssl_cert: str | None = typer.Option(
51
- None, "--ssl-cert", help="SSL client certificate file path"
52
- ),
53
- ssl_key: str | None = typer.Option(
54
- None, "--ssl-key", help="SSL client private key file path"
55
- ),
56
- interactive: bool = typer.Option(
57
- True, "--interactive/--no-interactive", help="Use interactive mode"
58
- ),
28
+ @db_app.command
29
+ def add(
30
+ name: Annotated[str, cyclopts.Parameter(help="Name for the database connection")],
31
+ type: Annotated[
32
+ str,
33
+ cyclopts.Parameter(
34
+ ["--type", "-t"],
35
+ help="Database type (postgresql, mysql, sqlite)",
36
+ ),
37
+ ] = "postgresql",
38
+ host: Annotated[
39
+ str | None,
40
+ cyclopts.Parameter(["--host", "-h"], help="Database host"),
41
+ ] = None,
42
+ port: Annotated[
43
+ int | None,
44
+ cyclopts.Parameter(["--port", "-p"], help="Database port"),
45
+ ] = None,
46
+ database: Annotated[
47
+ str | None,
48
+ cyclopts.Parameter(["--database", "--db"], help="Database name"),
49
+ ] = None,
50
+ username: Annotated[
51
+ str | None,
52
+ cyclopts.Parameter(["--username", "-u"], help="Username"),
53
+ ] = None,
54
+ ssl_mode: Annotated[
55
+ str | None,
56
+ cyclopts.Parameter(
57
+ ["--ssl-mode"],
58
+ help="SSL mode (disable, allow, prefer, require, verify-ca, verify-full for PostgreSQL; DISABLED, PREFERRED, REQUIRED, VERIFY_CA, VERIFY_IDENTITY for MySQL)",
59
+ ),
60
+ ] = None,
61
+ ssl_ca: Annotated[
62
+ str | None,
63
+ cyclopts.Parameter(["--ssl-ca"], help="SSL CA certificate file path"),
64
+ ] = None,
65
+ ssl_cert: Annotated[
66
+ str | None,
67
+ cyclopts.Parameter(["--ssl-cert"], help="SSL client certificate file path"),
68
+ ] = None,
69
+ ssl_key: Annotated[
70
+ str | None,
71
+ cyclopts.Parameter(["--ssl-key"], help="SSL client private key file path"),
72
+ ] = None,
73
+ interactive: Annotated[
74
+ bool,
75
+ cyclopts.Parameter(
76
+ ["--interactive", "--no-interactive"],
77
+ help="Use interactive mode",
78
+ ),
79
+ ] = True,
59
80
  ):
60
81
  """Add a new database connection."""
61
82
 
@@ -157,7 +178,7 @@ def add_database(
157
178
  console.print(
158
179
  "[bold red]Error:[/bold red] Database file path is required for SQLite"
159
180
  )
160
- raise typer.Exit(1)
181
+ sys.exit(1)
161
182
  host = "localhost"
162
183
  port = 0
163
184
  username = "sqlite"
@@ -167,7 +188,7 @@ def add_database(
167
188
  console.print(
168
189
  "[bold red]Error:[/bold red] Host, database, and username are required"
169
190
  )
170
- raise typer.Exit(1)
191
+ sys.exit(1)
171
192
 
172
193
  if port is None:
173
194
  port = 5432 if type == "postgresql" else 3306
@@ -210,11 +231,11 @@ def add_database(
210
231
 
211
232
  except Exception as e:
212
233
  console.print(f"[bold red]Error adding database:[/bold red] {e}")
213
- raise typer.Exit(1)
234
+ sys.exit(1)
214
235
 
215
236
 
216
- @db_app.command("list")
217
- def list_databases():
237
+ @db_app.command
238
+ def list():
218
239
  """List all configured database connections."""
219
240
  databases = config_manager.list_databases()
220
241
  default_name = config_manager.get_default_name()
@@ -260,16 +281,18 @@ def list_databases():
260
281
  console.print(table)
261
282
 
262
283
 
263
- @db_app.command("remove")
264
- def remove_database(
265
- name: str = typer.Argument(..., help="Name of the database connection to remove"),
284
+ @db_app.command
285
+ def remove(
286
+ name: Annotated[
287
+ str, cyclopts.Parameter(help="Name of the database connection to remove")
288
+ ],
266
289
  ):
267
290
  """Remove a database connection."""
268
291
  if not config_manager.get_database(name):
269
292
  console.print(
270
293
  f"[bold red]Error:[/bold red] Database connection '{name}' not found"
271
294
  )
272
- raise typer.Exit(1)
295
+ sys.exit(1)
273
296
 
274
297
  if questionary.confirm(
275
298
  f"Are you sure you want to remove database connection '{name}'?"
@@ -282,37 +305,40 @@ def remove_database(
282
305
  console.print(
283
306
  f"[bold red]Error:[/bold red] Failed to remove database connection '{name}'"
284
307
  )
285
- raise typer.Exit(1)
308
+ sys.exit(1)
286
309
  else:
287
310
  console.print("Operation cancelled")
288
311
 
289
312
 
290
- @db_app.command("set-default")
291
- def set_default_database(
292
- name: str = typer.Argument(
293
- ..., help="Name of the database connection to set as default"
294
- ),
313
+ @db_app.command
314
+ def set_default(
315
+ name: Annotated[
316
+ str,
317
+ cyclopts.Parameter(help="Name of the database connection to set as default"),
318
+ ],
295
319
  ):
296
320
  """Set the default database connection."""
297
321
  if not config_manager.get_database(name):
298
322
  console.print(
299
323
  f"[bold red]Error:[/bold red] Database connection '{name}' not found"
300
324
  )
301
- raise typer.Exit(1)
325
+ sys.exit(1)
302
326
 
303
327
  if config_manager.set_default_database(name):
304
328
  console.print(f"[green]Successfully set '{name}' as default database[/green]")
305
329
  else:
306
330
  console.print(f"[bold red]Error:[/bold red] Failed to set '{name}' as default")
307
- raise typer.Exit(1)
331
+ sys.exit(1)
308
332
 
309
333
 
310
- @db_app.command("test")
311
- def test_database(
312
- name: str | None = typer.Argument(
313
- None,
314
- help="Name of the database connection to test (uses default if not specified)",
315
- ),
334
+ @db_app.command
335
+ def test(
336
+ name: Annotated[
337
+ str | None,
338
+ cyclopts.Parameter(
339
+ help="Name of the database connection to test (uses default if not specified)",
340
+ ),
341
+ ] = None,
316
342
  ):
317
343
  """Test a database connection."""
318
344
 
@@ -323,7 +349,7 @@ def test_database(
323
349
  console.print(
324
350
  f"[bold red]Error:[/bold red] Database connection '{name}' not found"
325
351
  )
326
- raise typer.Exit(1)
352
+ sys.exit(1)
327
353
  else:
328
354
  db_config = config_manager.get_default_database()
329
355
  if not db_config:
@@ -333,7 +359,7 @@ def test_database(
333
359
  console.print(
334
360
  "Use 'sqlsaber db add <name>' to add a database connection"
335
361
  )
336
- raise typer.Exit(1)
362
+ sys.exit(1)
337
363
 
338
364
  console.print(f"[blue]Testing connection to '{db_config.name}'...[/blue]")
339
365
 
@@ -351,11 +377,11 @@ def test_database(
351
377
 
352
378
  except Exception as e:
353
379
  console.print(f"[bold red]✗ Connection failed:[/bold red] {e}")
354
- raise typer.Exit(1)
380
+ sys.exit(1)
355
381
 
356
382
  asyncio.run(test_connection())
357
383
 
358
384
 
359
- def create_db_app() -> typer.Typer:
385
+ def create_db_app() -> cyclopts.App:
360
386
  """Return the database management CLI app."""
361
387
  return db_app
sqlsaber/cli/display.py CHANGED
@@ -4,6 +4,7 @@ import json
4
4
 
5
5
  from rich.console import Console
6
6
  from rich.markdown import Markdown
7
+ from rich.panel import Panel
7
8
  from rich.syntax import Syntax
8
9
  from rich.table import Table
9
10
 
@@ -244,7 +245,7 @@ class DisplayManager:
244
245
  self.show_error(f"Error displaying plot: {str(e)}")
245
246
 
246
247
  def show_markdown_response(self, content: list):
247
- """Display the assistant's response as rich markdown."""
248
+ """Display the assistant's response as rich markdown in a panel."""
248
249
  if not content:
249
250
  return
250
251
 
@@ -256,10 +257,11 @@ class DisplayManager:
256
257
  if text:
257
258
  text_parts.append(text)
258
259
 
259
- # Join all text parts and display as markdown
260
+ # Join all text parts and display as markdown in a panel
260
261
  full_text = "".join(text_parts).strip()
261
262
  if full_text:
262
- self.console.print() # Add spacing before markdown
263
+ self.console.print() # Add spacing before panel
263
264
  markdown = Markdown(full_text)
264
- self.console.print(markdown)
265
- self.console.print() # Add spacing after markdown
265
+ panel = Panel.fit(markdown, border_style="green")
266
+ self.console.print(panel)
267
+ self.console.print() # Add spacing after panel
@@ -36,8 +36,15 @@ class InteractiveSession:
36
36
 
37
37
  self.console.print(
38
38
  Panel.fit(
39
- "[bold green]SQLSaber - Use the agent Luke![/bold green]\n\n"
40
- "[bold]Your agentic SQL assistant.[/bold]\n\n\n"
39
+ """
40
+ ███████ ██████ ██ ███████ █████ ██████ ███████ ██████
41
+ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
42
+ ███████ ██ ██ ██ ███████ ███████ ██████ █████ ██████
43
+ ██ ██ ▄▄ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
44
+ ███████ ██████ ███████ ███████ ██ ██ ██████ ███████ ██ ██
45
+ ▀▀
46
+ """
47
+ "\n\n"
41
48
  "[dim]Use '/clear' to reset conversation, '/exit' or '/quit' to leave.[/dim]\n\n"
42
49
  "[dim]Start a message with '#' to add something to agent's memory for this database.[/dim]\n\n"
43
50
  "[dim]Type '@' to get table name completions.[/dim]",
sqlsaber/cli/memory.py CHANGED
@@ -1,6 +1,10 @@
1
1
  """Memory management CLI commands."""
2
2
 
3
- import typer
3
+ import sys
4
+ from typing import Annotated
5
+
6
+ import cyclopts
7
+ import questionary
4
8
  from rich.console import Console
5
9
  from rich.table import Table
6
10
 
@@ -13,10 +17,9 @@ config_manager = DatabaseConfigManager()
13
17
  memory_manager = MemoryManager()
14
18
 
15
19
  # Create the memory management CLI app
16
- memory_app = typer.Typer(
20
+ memory_app = cyclopts.App(
17
21
  name="memory",
18
22
  help="Manage database-specific memories",
19
- add_completion=True,
20
23
  )
21
24
 
22
25
 
@@ -28,7 +31,7 @@ def _get_database_name(database: str | None = None) -> str:
28
31
  console.print(
29
32
  f"[bold red]Error:[/bold red] Database connection '{database}' not found."
30
33
  )
31
- raise typer.Exit(1)
34
+ sys.exit(1)
32
35
  return database
33
36
  else:
34
37
  db_config = config_manager.get_default_database()
@@ -37,19 +40,20 @@ def _get_database_name(database: str | None = None) -> str:
37
40
  "[bold red]Error:[/bold red] No database connections configured."
38
41
  )
39
42
  console.print("Use 'sqlsaber db add <name>' to add a database connection.")
40
- raise typer.Exit(1)
43
+ sys.exit(1)
41
44
  return db_config.name
42
45
 
43
46
 
44
- @memory_app.command("add")
45
- def add_memory(
46
- content: str = typer.Argument(..., help="Memory content to add"),
47
- database: str | None = typer.Option(
48
- None,
49
- "--database",
50
- "-d",
51
- help="Database connection name (uses default if not specified)",
52
- ),
47
+ @memory_app.command
48
+ def add(
49
+ content: Annotated[str, cyclopts.Parameter(help="Memory content to add")],
50
+ database: Annotated[
51
+ str | None,
52
+ cyclopts.Parameter(
53
+ ["--database", "-d"],
54
+ help="Database connection name (uses default if not specified)",
55
+ ),
56
+ ] = None,
53
57
  ):
54
58
  """Add a new memory for the specified database."""
55
59
  database_name = _get_database_name(database)
@@ -61,17 +65,18 @@ def add_memory(
61
65
  console.print(f"[dim]Content:[/dim] {memory.content}")
62
66
  except Exception as e:
63
67
  console.print(f"[bold red]Error adding memory:[/bold red] {e}")
64
- raise typer.Exit(1)
65
-
66
-
67
- @memory_app.command("list")
68
- def list_memories(
69
- database: str | None = typer.Option(
70
- None,
71
- "--database",
72
- "-d",
73
- help="Database connection name (uses default if not specified)",
74
- ),
68
+ sys.exit(1)
69
+
70
+
71
+ @memory_app.command
72
+ def list(
73
+ database: Annotated[
74
+ str | None,
75
+ cyclopts.Parameter(
76
+ ["--database", "-d"],
77
+ help="Database connection name (uses default if not specified)",
78
+ ),
79
+ ] = None,
75
80
  ):
76
81
  """List all memories for the specified database."""
77
82
  database_name = _get_database_name(database)
@@ -102,15 +107,16 @@ def list_memories(
102
107
  console.print(f"\n[dim]Total memories: {len(memories)}[/dim]")
103
108
 
104
109
 
105
- @memory_app.command("show")
106
- def show_memory(
107
- memory_id: str = typer.Argument(..., help="Memory ID to show"),
108
- database: str | None = typer.Option(
109
- None,
110
- "--database",
111
- "-d",
112
- help="Database connection name (uses default if not specified)",
113
- ),
110
+ @memory_app.command
111
+ def show(
112
+ memory_id: Annotated[str, cyclopts.Parameter(help="Memory ID to show")],
113
+ database: Annotated[
114
+ str | None,
115
+ cyclopts.Parameter(
116
+ ["--database", "-d"],
117
+ help="Database connection name (uses default if not specified)",
118
+ ),
119
+ ] = None,
114
120
  ):
115
121
  """Show the full content of a specific memory."""
116
122
  database_name = _get_database_name(database)
@@ -121,7 +127,7 @@ def show_memory(
121
127
  console.print(
122
128
  f"[bold red]Error:[/bold red] Memory with ID '{memory_id}' not found for database '{database_name}'"
123
129
  )
124
- raise typer.Exit(1)
130
+ sys.exit(1)
125
131
 
126
132
  console.print(f"[bold]Memory ID:[/bold] {memory.id}")
127
133
  console.print(f"[bold]Database:[/bold] {memory.database}")
@@ -130,15 +136,16 @@ def show_memory(
130
136
  console.print(f"{memory.content}")
131
137
 
132
138
 
133
- @memory_app.command("remove")
134
- def remove_memory(
135
- memory_id: str = typer.Argument(..., help="Memory ID to remove"),
136
- database: str | None = typer.Option(
137
- None,
138
- "--database",
139
- "-d",
140
- help="Database connection name (uses default if not specified)",
141
- ),
139
+ @memory_app.command
140
+ def remove(
141
+ memory_id: Annotated[str, cyclopts.Parameter(help="Memory ID to remove")],
142
+ database: Annotated[
143
+ str | None,
144
+ cyclopts.Parameter(
145
+ ["--database", "-d"],
146
+ help="Database connection name (uses default if not specified)",
147
+ ),
148
+ ] = None,
142
149
  ):
143
150
  """Remove a specific memory by ID."""
144
151
  database_name = _get_database_name(database)
@@ -149,7 +156,7 @@ def remove_memory(
149
156
  console.print(
150
157
  f"[bold red]Error:[/bold red] Memory with ID '{memory_id}' not found for database '{database_name}'"
151
158
  )
152
- raise typer.Exit(1)
159
+ sys.exit(1)
153
160
 
154
161
  # Show memory content before removal
155
162
  console.print("[yellow]Removing memory:[/yellow]")
@@ -163,23 +170,25 @@ def remove_memory(
163
170
  console.print(
164
171
  f"[bold red]Error:[/bold red] Failed to remove memory '{memory_id}'"
165
172
  )
166
- raise typer.Exit(1)
167
-
168
-
169
- @memory_app.command("clear")
170
- def clear_memories(
171
- database: str | None = typer.Option(
172
- None,
173
- "--database",
174
- "-d",
175
- help="Database connection name (uses default if not specified)",
176
- ),
177
- force: bool = typer.Option(
178
- False,
179
- "--force",
180
- "-f",
181
- help="Skip confirmation prompt",
182
- ),
173
+ sys.exit(1)
174
+
175
+
176
+ @memory_app.command
177
+ def clear(
178
+ database: Annotated[
179
+ str | None,
180
+ cyclopts.Parameter(
181
+ ["--database", "-d"],
182
+ help="Database connection name (uses default if not specified)",
183
+ ),
184
+ ] = None,
185
+ force: Annotated[
186
+ bool,
187
+ cyclopts.Parameter(
188
+ ["--force", "-f"],
189
+ help="Skip confirmation prompt",
190
+ ),
191
+ ] = False,
183
192
  ):
184
193
  """Clear all memories for the specified database."""
185
194
  database_name = _get_database_name(database)
@@ -198,8 +207,8 @@ def clear_memories(
198
207
  console.print(
199
208
  f"[yellow]About to clear {memories_count} memories for database '{database_name}'[/yellow]"
200
209
  )
201
- confirm = typer.confirm("Are you sure you want to proceed?")
202
- if not confirm:
210
+
211
+ if not questionary.confirm("Are you sure you want to proceed?").ask():
203
212
  console.print("Operation cancelled")
204
213
  return
205
214
 
@@ -209,14 +218,15 @@ def clear_memories(
209
218
  )
210
219
 
211
220
 
212
- @memory_app.command("summary")
213
- def memory_summary(
214
- database: str | None = typer.Option(
215
- None,
216
- "--database",
217
- "-d",
218
- help="Database connection name (uses default if not specified)",
219
- ),
221
+ @memory_app.command
222
+ def summary(
223
+ database: Annotated[
224
+ str | None,
225
+ cyclopts.Parameter(
226
+ ["--database", "-d"],
227
+ help="Database connection name (uses default if not specified)",
228
+ ),
229
+ ] = None,
220
230
  ):
221
231
  """Show memory summary for the specified database."""
222
232
  database_name = _get_database_name(database)
@@ -232,6 +242,6 @@ def memory_summary(
232
242
  console.print(f"[dim]{memory['timestamp']}[/dim] - {memory['content']}")
233
243
 
234
244
 
235
- def create_memory_app() -> typer.Typer:
245
+ def create_memory_app() -> cyclopts.App:
236
246
  """Return the memory management CLI app."""
237
247
  return memory_app
sqlsaber/cli/models.py CHANGED
@@ -1,10 +1,11 @@
1
1
  """Model management CLI commands."""
2
2
 
3
3
  import asyncio
4
+ import sys
4
5
 
5
6
  import httpx
6
7
  import questionary
7
- import typer
8
+ import cyclopts
8
9
  from rich.console import Console
9
10
  from rich.table import Table
10
11
 
@@ -14,10 +15,9 @@ from sqlsaber.config.settings import Config
14
15
  console = Console()
15
16
 
16
17
  # Create the model management CLI app
17
- models_app = typer.Typer(
18
+ models_app = cyclopts.App(
18
19
  name="models",
19
20
  help="Select and manage models",
20
- add_completion=True,
21
21
  )
22
22
 
23
23
 
@@ -96,8 +96,8 @@ class ModelManager:
96
96
  model_manager = ModelManager()
97
97
 
98
98
 
99
- @models_app.command("list")
100
- def list_models():
99
+ @models_app.command
100
+ def list():
101
101
  """List available AI models."""
102
102
 
103
103
  async def fetch_and_display():
@@ -146,8 +146,8 @@ def list_models():
146
146
  asyncio.run(fetch_and_display())
147
147
 
148
148
 
149
- @models_app.command("set")
150
- def set_model():
149
+ @models_app.command
150
+ def set():
151
151
  """Set the AI model to use."""
152
152
 
153
153
  async def interactive_set():
@@ -156,7 +156,7 @@ def set_model():
156
156
 
157
157
  if not models:
158
158
  console.print("[red]Failed to fetch models. Cannot set model.[/red]")
159
- raise typer.Exit(1)
159
+ sys.exit(1)
160
160
 
161
161
  # Create choices for questionary
162
162
  choices = []
@@ -190,22 +190,22 @@ def set_model():
190
190
  console.print(f"[green]✓ Model set to: {selected_model}[/green]")
191
191
  else:
192
192
  console.print("[red]✗ Failed to set model[/red]")
193
- raise typer.Exit(1)
193
+ sys.exit(1)
194
194
  else:
195
195
  console.print("[yellow]Operation cancelled[/yellow]")
196
196
 
197
197
  asyncio.run(interactive_set())
198
198
 
199
199
 
200
- @models_app.command("current")
201
- def current_model():
200
+ @models_app.command
201
+ def current():
202
202
  """Show the currently configured model."""
203
203
  current = model_manager.get_current_model()
204
204
  console.print(f"Current model: [cyan]{current}[/cyan]")
205
205
 
206
206
 
207
- @models_app.command("reset")
208
- def reset_model():
207
+ @models_app.command
208
+ def reset():
209
209
  """Reset to the default model."""
210
210
 
211
211
  async def interactive_reset():
@@ -218,13 +218,13 @@ def reset_model():
218
218
  )
219
219
  else:
220
220
  console.print("[red]✗ Failed to reset model[/red]")
221
- raise typer.Exit(1)
221
+ sys.exit(1)
222
222
  else:
223
223
  console.print("[yellow]Operation cancelled[/yellow]")
224
224
 
225
225
  asyncio.run(interactive_reset())
226
226
 
227
227
 
228
- def create_models_app() -> typer.Typer:
228
+ def create_models_app() -> cyclopts.App:
229
229
  """Return the model management CLI app."""
230
230
  return models_app
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlsaber
3
- Version: 0.8.1
3
+ Version: 0.9.0
4
4
  Summary: SQLSaber - Agentic SQL assistant like Claude Code
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
@@ -8,6 +8,7 @@ Requires-Dist: aiomysql>=0.2.0
8
8
  Requires-Dist: aiosqlite>=0.21.0
9
9
  Requires-Dist: anthropic>=0.54.0
10
10
  Requires-Dist: asyncpg>=0.30.0
11
+ Requires-Dist: cyclopts>=3.22.1
11
12
  Requires-Dist: fastmcp>=2.9.0
12
13
  Requires-Dist: httpx>=0.28.1
13
14
  Requires-Dist: keyring>=25.6.0
@@ -15,7 +16,6 @@ Requires-Dist: pandas>=2.0.0
15
16
  Requires-Dist: platformdirs>=4.0.0
16
17
  Requires-Dist: questionary>=2.1.0
17
18
  Requires-Dist: rich>=13.7.0
18
- Requires-Dist: typer>=0.16.0
19
19
  Requires-Dist: uniplot>=0.21.2
20
20
  Description-Content-Type: text/markdown
21
21
 
@@ -126,7 +126,7 @@ saber memory list
126
126
  Start an interactive session:
127
127
 
128
128
  ```bash
129
- saber query
129
+ saber
130
130
  ```
131
131
 
132
132
  > You can also add memories in an interactive session by starting your message with the `#` sign
@@ -136,7 +136,7 @@ saber query
136
136
  Execute a single natural language query:
137
137
 
138
138
  ```bash
139
- saber query "show me all users created this month"
139
+ saber "show me all users created this month"
140
140
  ```
141
141
 
142
142
  ### Database Selection
@@ -144,33 +144,39 @@ saber query "show me all users created this month"
144
144
  Use a specific database connection:
145
145
 
146
146
  ```bash
147
- # Use named database from config
148
- saber query -d mydb "count all orders"
147
+ # Interactive mode with specific database
148
+ saber -d mydb
149
+
150
+ # Single query with specific database
151
+ saber -d mydb "count all orders"
149
152
  ```
150
153
 
151
154
  ## Examples
152
155
 
153
156
  ```bash
154
157
  # Show database schema
155
- saber query "what tables are in my database?"
158
+ saber "what tables are in my database?"
156
159
 
157
160
  # Count records
158
- saber query "how many active users do we have?"
161
+ saber "how many active users do we have?"
159
162
 
160
163
  # Complex queries with joins
161
- saber query "show me orders with customer details for this week"
164
+ saber "show me orders with customer details for this week"
162
165
 
163
166
  # Aggregations
164
- saber query "what's the total revenue by product category?"
167
+ saber "what's the total revenue by product category?"
165
168
 
166
169
  # Date filtering
167
- saber query "list users who haven't logged in for 30 days"
170
+ saber "list users who haven't logged in for 30 days"
168
171
 
169
172
  # Data exploration
170
- saber query "show me the distribution of customer ages"
173
+ saber "show me the distribution of customer ages"
171
174
 
172
175
  # Business analytics
173
- saber query "which products had the highest sales growth last quarter?"
176
+ saber "which products had the highest sales growth last quarter?"
177
+
178
+ # Start interactive mode
179
+ saber
174
180
  ```
175
181
 
176
182
  ## MCP Server Integration
@@ -6,14 +6,14 @@ sqlsaber/agents/base.py,sha256=Cl5ZV4dfgjslOAq8jbrnt5kX-NM_8QmjacWzb0hvbzs,10527
6
6
  sqlsaber/agents/mcp.py,sha256=FKtXgDrPZ2-xqUYCw2baI5JzrWekXaC5fjkYW1_Mg50,827
7
7
  sqlsaber/agents/streaming.py,sha256=LaSeMTlxuJFRArJVqDly5-_KgcePiCCKPKfMxfB4oGs,521
8
8
  sqlsaber/cli/__init__.py,sha256=qVSLVJLLJYzoC6aj6y9MFrzZvAwc4_OgxU9DlkQnZ4M,86
9
- sqlsaber/cli/auth.py,sha256=tm3f-qIuNS0nQbU2DEI7ezWG092mayNW1GuoiwdV8hI,5047
10
- sqlsaber/cli/commands.py,sha256=Ob505FV1kfaRKoW_agon4Q82772QmLjxISfvbXGOHE4,5256
9
+ sqlsaber/cli/auth.py,sha256=1yvawtS5NGj9dWMRZ8I5T6sqBiRqZpPsjEPrJSQBJAs,4979
10
+ sqlsaber/cli/commands.py,sha256=ABmw9U6HwSJI61d9iK0X3nsFKLMqfcQGQEYE9ZoZHlw,5494
11
11
  sqlsaber/cli/completers.py,sha256=HsUPjaZweLSeYCWkAcgMl8FylQ1xjWBWYTEL_9F6xfU,6430
12
- sqlsaber/cli/database.py,sha256=mWpMPcISUokYIiAMU4M_g8YeI-Fz5YU_R3PYs-GigCw,12588
13
- sqlsaber/cli/display.py,sha256=XcBkjdG7RoM_ijHgv0VWqWleT5CCTm0Hcp1sJoE1FKE,9979
14
- sqlsaber/cli/interactive.py,sha256=sQQXO8RcbVwxIBArNUlv-8ePhLn3UUdx6zUl44l8tow,7395
15
- sqlsaber/cli/memory.py,sha256=OFspjaZ2RaYrBdSDVOD-9_6T8NbqedHEn5FztGkLUlc,7621
16
- sqlsaber/cli/models.py,sha256=7bvIykGPTJu3-3tpPinr44GBkPIQhoeKI3d3Kgn3jOI,7783
12
+ sqlsaber/cli/database.py,sha256=tJ8rqGrafZpg3VgDmSiq7eZoPscoGAW3XLTYGoQw8LE,12910
13
+ sqlsaber/cli/display.py,sha256=wC-xYmmD21XyAkpRdMW7Ch2Mn5SlM1X34pbpka2ZIX8,10083
14
+ sqlsaber/cli/interactive.py,sha256=NjrhzXt6YK4WL17DMkJTY5dA1v9QMxLdkI1XgPKK8YA,8017
15
+ sqlsaber/cli/memory.py,sha256=OufHFJFwV0_GGn7LvKRTJikkWhV1IwNIUDOxFPHXOaQ,7794
16
+ sqlsaber/cli/models.py,sha256=HByezaeKqj65GzB_FmWuugjkgTq2Pvab_mzNZnHxya0,7690
17
17
  sqlsaber/cli/streaming.py,sha256=WfhFd5ntq2HStpJZwWJ0C5uyXKc3aU14eo8HdjzW1o0,3767
18
18
  sqlsaber/clients/__init__.py,sha256=jcMoVsT92U6nQrfotCp1h0ggskJPAcgeYarqQl1qEBg,171
19
19
  sqlsaber/clients/anthropic.py,sha256=umRmuzpmJdYO7hO3biAZXO9T_sb6Vv010o6zqn03is8,9947
@@ -39,8 +39,8 @@ sqlsaber/memory/storage.py,sha256=ne8szLlGj5NELheqLnI7zu21V8YS4rtpYGGC7tOmi-s,57
39
39
  sqlsaber/models/__init__.py,sha256=RJ7p3WtuSwwpFQ1Iw4_DHV2zzCtHqIzsjJzxv8kUjUE,287
40
40
  sqlsaber/models/events.py,sha256=89SXKb5GGpH01yTr2kPEBhzp9xv35RFIYuFdAZSIPoE,721
41
41
  sqlsaber/models/types.py,sha256=w-zk81V2dtveuteej36_o1fDK3So428j3P2rAejU62U,862
42
- sqlsaber-0.8.1.dist-info/METADATA,sha256=wmQlEB13kRa35a-n6oIFSg0YExXE0iF7_V4GZWkuLRA,5986
43
- sqlsaber-0.8.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
44
- sqlsaber-0.8.1.dist-info/entry_points.txt,sha256=jmFo96Ylm0zIKXJBwhv_P5wQ7SXP9qdaBcnTp8iCEe8,195
45
- sqlsaber-0.8.1.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
46
- sqlsaber-0.8.1.dist-info/RECORD,,
42
+ sqlsaber-0.9.0.dist-info/METADATA,sha256=F2LyQDJS7tE1PfA2LhHJfjNU1qo35MbloSjvJyXgCs4,6023
43
+ sqlsaber-0.9.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
44
+ sqlsaber-0.9.0.dist-info/entry_points.txt,sha256=qEbOB7OffXPFgyJc7qEIJlMEX5RN9xdzLmWZa91zCQQ,162
45
+ sqlsaber-0.9.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
46
+ sqlsaber-0.9.0.dist-info/RECORD,,
@@ -1,6 +1,5 @@
1
1
  [console_scripts]
2
2
  saber = sqlsaber.cli.commands:main
3
3
  saber-mcp = sqlsaber.mcp.mcp:main
4
- sql = sqlsaber.cli.commands:main
5
4
  sqlsaber = sqlsaber.cli.commands:main
6
5
  sqlsaber-mcp = sqlsaber.mcp.mcp:main