sqlsaber 0.30.2__tar.gz → 0.31.0__tar.gz

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 (123) hide show
  1. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/PKG-INFO +2 -1
  2. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/docs/src/content/docs/changelog.md +15 -3
  3. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/pyproject.toml +2 -1
  4. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/src/sqlsaber/cli/auth.py +15 -1
  5. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/src/sqlsaber/cli/commands.py +25 -0
  6. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/src/sqlsaber/cli/database.py +39 -0
  7. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/src/sqlsaber/cli/interactive.py +8 -2
  8. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/src/sqlsaber/cli/memory.py +26 -0
  9. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/src/sqlsaber/cli/models.py +19 -0
  10. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/src/sqlsaber/cli/streaming.py +5 -0
  11. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/src/sqlsaber/cli/theme.py +8 -0
  12. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/src/sqlsaber/cli/threads.py +17 -0
  13. sqlsaber-0.31.0/src/sqlsaber/config/logging.py +196 -0
  14. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/src/sqlsaber/config/oauth_flow.py +22 -10
  15. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/src/sqlsaber/config/oauth_tokens.py +15 -6
  16. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/src/sqlsaber/threads/storage.py +31 -17
  17. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/uv.lock +12 -1
  18. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/.github/workflows/claude-code-review.yml +0 -0
  19. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/.github/workflows/claude.yml +0 -0
  20. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/.github/workflows/deploy-docs.yml +0 -0
  21. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/.github/workflows/publish.yml +0 -0
  22. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/.github/workflows/test.yml +0 -0
  23. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/.gitignore +0 -0
  24. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/.python-version +0 -0
  25. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/AGENT.md +0 -0
  26. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/CLAUDE.md +0 -0
  27. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/LICENSE +0 -0
  28. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/README.md +0 -0
  29. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/docs/.gitignore +0 -0
  30. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/docs/.vscode/extensions.json +0 -0
  31. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/docs/.vscode/launch.json +0 -0
  32. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/docs/CLAUDE.md +0 -0
  33. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/docs/astro.config.mjs +0 -0
  34. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/docs/package-lock.json +0 -0
  35. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/docs/package.json +0 -0
  36. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/docs/public/CNAME +0 -0
  37. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/docs/public/favicon.svg +0 -0
  38. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/docs/src/assets/sqlsaber.gif +0 -0
  39. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/docs/src/content/docs/guides/authentication.mdx +0 -0
  40. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/docs/src/content/docs/guides/database-setup.mdx +0 -0
  41. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/docs/src/content/docs/guides/getting-started.mdx +0 -0
  42. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/docs/src/content/docs/guides/memory.mdx +0 -0
  43. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/docs/src/content/docs/guides/models.mdx +0 -0
  44. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/docs/src/content/docs/guides/queries.mdx +0 -0
  45. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/docs/src/content/docs/guides/threads.md +0 -0
  46. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/docs/src/content/docs/index.mdx +0 -0
  47. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/docs/src/content/docs/installation.mdx +0 -0
  48. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/docs/src/content/docs/reference/commands.md +0 -0
  49. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/docs/src/content.config.ts +0 -0
  50. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/docs/src/styles/global.css +0 -0
  51. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/docs/tsconfig.json +0 -0
  52. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/legislators.db +0 -0
  53. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/pytest.ini +0 -0
  54. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/sqlsaber.gif +0 -0
  55. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/sqlsaber.svg +0 -0
  56. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/src/sqlsaber/__init__.py +0 -0
  57. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/src/sqlsaber/__main__.py +0 -0
  58. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/src/sqlsaber/agents/__init__.py +0 -0
  59. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/src/sqlsaber/agents/base.py +0 -0
  60. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/src/sqlsaber/agents/pydantic_ai_agent.py +0 -0
  61. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/src/sqlsaber/application/__init__.py +0 -0
  62. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/src/sqlsaber/application/auth_setup.py +0 -0
  63. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/src/sqlsaber/application/db_setup.py +0 -0
  64. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/src/sqlsaber/application/model_selection.py +0 -0
  65. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/src/sqlsaber/application/prompts.py +0 -0
  66. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/src/sqlsaber/cli/__init__.py +0 -0
  67. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/src/sqlsaber/cli/completers.py +0 -0
  68. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/src/sqlsaber/cli/display.py +0 -0
  69. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/src/sqlsaber/cli/onboarding.py +0 -0
  70. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/src/sqlsaber/config/__init__.py +0 -0
  71. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/src/sqlsaber/config/api_keys.py +0 -0
  72. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/src/sqlsaber/config/auth.py +0 -0
  73. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/src/sqlsaber/config/database.py +0 -0
  74. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/src/sqlsaber/config/providers.py +0 -0
  75. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/src/sqlsaber/config/settings.py +0 -0
  76. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/src/sqlsaber/database/__init__.py +0 -0
  77. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/src/sqlsaber/database/base.py +0 -0
  78. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/src/sqlsaber/database/csv.py +0 -0
  79. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/src/sqlsaber/database/duckdb.py +0 -0
  80. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/src/sqlsaber/database/mysql.py +0 -0
  81. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/src/sqlsaber/database/postgresql.py +0 -0
  82. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/src/sqlsaber/database/resolver.py +0 -0
  83. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/src/sqlsaber/database/schema.py +0 -0
  84. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/src/sqlsaber/database/sqlite.py +0 -0
  85. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/src/sqlsaber/memory/__init__.py +0 -0
  86. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/src/sqlsaber/memory/manager.py +0 -0
  87. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/src/sqlsaber/memory/storage.py +0 -0
  88. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/src/sqlsaber/theme/__init__.py +0 -0
  89. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/src/sqlsaber/theme/manager.py +0 -0
  90. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/src/sqlsaber/threads/__init__.py +0 -0
  91. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/src/sqlsaber/tools/__init__.py +0 -0
  92. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/src/sqlsaber/tools/base.py +0 -0
  93. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/src/sqlsaber/tools/registry.py +0 -0
  94. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/src/sqlsaber/tools/sql_guard.py +0 -0
  95. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/src/sqlsaber/tools/sql_tools.py +0 -0
  96. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/tests/__init__.py +0 -0
  97. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/tests/conftest.py +0 -0
  98. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/tests/test_cli/__init__.py +0 -0
  99. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/tests/test_cli/test_auth_reset.py +0 -0
  100. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/tests/test_cli/test_commands.py +0 -0
  101. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/tests/test_cli/test_threads.py +0 -0
  102. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/tests/test_config/__init__.py +0 -0
  103. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/tests/test_config/test_database.py +0 -0
  104. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/tests/test_config/test_oauth.py +0 -0
  105. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/tests/test_config/test_providers.py +0 -0
  106. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/tests/test_config/test_settings.py +0 -0
  107. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/tests/test_database/__init__.py +0 -0
  108. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/tests/test_database/test_connection.py +0 -0
  109. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/tests/test_database/test_csv_connection.py +0 -0
  110. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/tests/test_database/test_csv_module.py +0 -0
  111. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/tests/test_database/test_duckdb_module.py +0 -0
  112. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/tests/test_database/test_schema.py +0 -0
  113. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/tests/test_database/test_schema_display.py +0 -0
  114. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/tests/test_database/test_sqlite_module.py +0 -0
  115. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/tests/test_database/test_timeout.py +0 -0
  116. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/tests/test_database_resolver.py +0 -0
  117. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/tests/test_theme/test_manager.py +0 -0
  118. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/tests/test_threads_storage.py +0 -0
  119. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/tests/test_tools/__init__.py +0 -0
  120. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/tests/test_tools/test_base.py +0 -0
  121. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/tests/test_tools/test_registry.py +0 -0
  122. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/tests/test_tools/test_sql_guard.py +0 -0
  123. {sqlsaber-0.30.2 → sqlsaber-0.31.0}/tests/test_tools/test_sql_tools.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlsaber
3
- Version: 0.30.2
3
+ Version: 0.31.0
4
4
  Summary: SQLsaber - Open-source agentic SQL assistant
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
@@ -18,6 +18,7 @@ Requires-Dist: pydantic-ai
18
18
  Requires-Dist: questionary>=2.1.0
19
19
  Requires-Dist: rich>=13.7.0
20
20
  Requires-Dist: sqlglot[rs]>=27.20.0
21
+ Requires-Dist: structlog>=25.4.0
21
22
  Description-Content-Type: text/markdown
22
23
 
23
24
  # SQLsaber
@@ -7,14 +7,26 @@ All notable changes to SQLsaber will be documented here.
7
7
 
8
8
  ### Unreleased
9
9
 
10
- ### v0.30.2
10
+ ---
11
+
12
+
13
+ ### v0.31.0 - 2025-10-13
14
+
15
+ #### Added
16
+
17
+ - Structured logging across the project
18
+ - Centralized setup with JSON logs to a rotating file by default
19
+ - Daily rotation at midnight (configurable), with backups retained
20
+ - Optional pretty console output in development via `SQLSABER_LOG_TO_STDERR=1` or `SQLSABER_DEBUG=1`
21
+ - Captures stdlib logging and Python warnings into the same pipeline
22
+ - Environment variables to control behavior: `SQLSABER_LOG_LEVEL`, `SQLSABER_LOG_FILE`, `SQLSABER_LOG_ROTATION`, `SQLSABER_LOG_WHEN`, `SQLSABER_LOG_INTERVAL`, `SQLSABER_LOG_BACKUP_COUNT`, `SQLSABER_LOG_MAX_BYTES`
23
+
24
+ ### v0.30.2 - 2025-10-10
11
25
 
12
26
  #### Changed
13
27
 
14
28
  - Added `keyrings.cryptfile` as Linux-only dependency for better keyring support on Linux systems
15
29
 
16
- ---
17
-
18
30
  ### v0.30.1 - 2025-10-07
19
31
 
20
32
  #### Fixed
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sqlsaber"
3
- version = "0.30.2"
3
+ version = "0.31.0"
4
4
  description = "SQLsaber - Open-source agentic SQL assistant"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -19,6 +19,7 @@ dependencies = [
19
19
  "cyclopts>=3.22.1",
20
20
  "prompt-toolkit>3.0.51",
21
21
  "sqlglot[rs]>=27.20.0",
22
+ "structlog>=25.4.0",
22
23
  ]
23
24
 
24
25
  [dependency-groups]
@@ -11,10 +11,12 @@ from sqlsaber.config.api_keys import APIKeyManager
11
11
  from sqlsaber.config.auth import AuthConfigManager, AuthMethod
12
12
  from sqlsaber.config.oauth_tokens import OAuthTokenManager
13
13
  from sqlsaber.theme.manager import create_console
14
+ from sqlsaber.config.logging import get_logger
14
15
 
15
16
  # Global instances for CLI commands
16
17
  console = create_console()
17
18
  config_manager = AuthConfigManager()
19
+ logger = get_logger(__name__)
18
20
 
19
21
  # Create the authentication management CLI app
20
22
  auth_app = cyclopts.App(
@@ -46,7 +48,9 @@ def setup():
46
48
  )
47
49
  return success, provider
48
50
 
49
- success, _ = asyncio.run(run_setup())
51
+ logger.info("auth.setup.start")
52
+ success, provider = asyncio.run(run_setup())
53
+ logger.info("auth.setup.complete", success=bool(success), provider=str(provider))
50
54
 
51
55
  if not success:
52
56
  console.print("\n[warning]No authentication configured.[/warning]")
@@ -59,6 +63,7 @@ def setup():
59
63
  @auth_app.command
60
64
  def status():
61
65
  """Show current authentication configuration and provider key status."""
66
+ logger.info("auth.status.start")
62
67
  auth_method = config_manager.get_auth_method()
63
68
 
64
69
  console.print("\n[bold blue]Authentication Status[/bold blue]")
@@ -68,6 +73,7 @@ def status():
68
73
  console.print(
69
74
  "Run [primary]saber auth setup[/primary] to configure authentication."
70
75
  )
76
+ logger.info("auth.status.none_configured")
71
77
  return
72
78
 
73
79
  # Show configured method summary
@@ -93,6 +99,7 @@ def status():
93
99
  console.print(f"> {provider}: [green]configured[/green]")
94
100
  else:
95
101
  console.print(f"> {provider}: [warning]not configured[/warning]")
102
+ logger.info("auth.status.complete", method=str(auth_method))
96
103
 
97
104
 
98
105
  @auth_app.command
@@ -108,6 +115,7 @@ def reset():
108
115
 
109
116
  if provider is None:
110
117
  console.print("[warning]Reset cancelled.[/warning]")
118
+ logger.info("auth.reset.cancelled_no_provider")
111
119
  return
112
120
 
113
121
  api_key_manager = APIKeyManager()
@@ -125,6 +133,7 @@ def reset():
125
133
  console.print(
126
134
  f"[warning]No stored credentials found for {provider}. Nothing to reset.[/warning]"
127
135
  )
136
+ logger.info("auth.reset.nothing_to_reset", provider=provider)
128
137
  return
129
138
 
130
139
  # Build confirmation message
@@ -142,6 +151,7 @@ def reset():
142
151
 
143
152
  if not confirmed:
144
153
  console.print("Reset cancelled.")
154
+ logger.info("auth.reset.cancelled_confirm", provider=provider)
145
155
  return
146
156
 
147
157
  # Perform deletions
@@ -151,11 +161,13 @@ def reset():
151
161
  try:
152
162
  keyring.delete_password(service, provider)
153
163
  console.print(f"Removed {provider} API key from keyring", style="green")
164
+ logger.info("auth.reset.api_key_removed", provider=provider)
154
165
  except keyring.errors.PasswordDeleteError:
155
166
  # Already absent; treat as success
156
167
  pass
157
168
  except Exception as e:
158
169
  console.print(f"Warning: Could not remove API key: {e}", style="warning")
170
+ logger.warning("auth.reset.api_key_remove_failed", provider=provider, error=str(e))
159
171
 
160
172
  # Optionally clear global auth method if removing Anthropic OAuth configuration
161
173
  if provider == "anthropic" and oauth_present:
@@ -170,8 +182,10 @@ def reset():
170
182
  config["auth_method"] = None
171
183
  config_manager._save_config(config)
172
184
  console.print("Global auth method unset.", style="green")
185
+ logger.info("auth.reset.global_method_unset")
173
186
 
174
187
  console.print("\n[success]✓ Reset complete.[/success]")
188
+ logger.info("auth.reset.complete", provider=provider)
175
189
  console.print(
176
190
  "Environment variables are not modified by this command.", style="dim"
177
191
  )
@@ -16,6 +16,7 @@ from sqlsaber.cli.threads import create_threads_app
16
16
 
17
17
  # Lazy imports - only import what's needed for CLI parsing
18
18
  from sqlsaber.config.database import DatabaseConfigManager
19
+ from sqlsaber.config.logging import get_logger, setup_logging
19
20
  from sqlsaber.theme.manager import create_console
20
21
 
21
22
 
@@ -111,6 +112,14 @@ def query(
111
112
  """
112
113
 
113
114
  async def run_session():
115
+ log = get_logger(__name__)
116
+ log.info(
117
+ "cli.session.start",
118
+ argv=sys.argv[1:],
119
+ database=database,
120
+ has_query=query_text is not None,
121
+ thinking=thinking,
122
+ )
114
123
  # Import heavy dependencies only when actually running a query
115
124
  # This is only done to speed up startup time
116
125
  from sqlsaber.agents import SQLSaberAgent
@@ -134,25 +143,34 @@ def query(
134
143
  # Check if onboarding is needed (only for interactive mode or when no database is configured)
135
144
  if needs_onboarding(database):
136
145
  # Run onboarding flow
146
+ log.debug("cli.onboarding.start")
137
147
  onboarding_success = await run_onboarding()
138
148
  if not onboarding_success:
139
149
  # User cancelled or onboarding failed
140
150
  raise CLIError(
141
151
  "Setup incomplete. Please configure your database and try again."
142
152
  )
153
+ log.info("cli.onboarding.complete", success=True)
143
154
 
144
155
  # Resolve database from CLI input
145
156
  try:
146
157
  resolved = resolve_database(database, config_manager)
147
158
  connection_string = resolved.connection_string
148
159
  db_name = resolved.name
160
+ log.info(
161
+ "db.resolve.success",
162
+ name=db_name,
163
+ )
149
164
  except DatabaseResolutionError as e:
165
+ log.error("db.resolve.error", error=str(e))
150
166
  raise CLIError(str(e))
151
167
 
152
168
  # Create database connection
153
169
  try:
154
170
  db_conn = DatabaseConnection(connection_string)
171
+ log.info("db.connection.created", db_type=type(db_conn).__name__)
155
172
  except Exception as e:
173
+ log.exception("db.connection.error", error=str(e))
156
174
  raise CLIError(f"Error creating database connection: {e}")
157
175
 
158
176
  # Create pydantic-ai agent instance with database name for memory context
@@ -166,6 +184,7 @@ def query(
166
184
  console.print(
167
185
  f"[primary]Connected to:[/primary] {db_name} ({db_type})\n"
168
186
  )
187
+ log.info("query.execute.start", db_name=db_name, db_type=db_type)
169
188
  run = await streaming_handler.execute_streaming_query(
170
189
  actual_query, sqlsaber_agent
171
190
  )
@@ -187,8 +206,10 @@ def query(
187
206
  console.print(
188
207
  f"[dim]You can continue this thread using:[/dim] saber threads resume {thread_id}"
189
208
  )
209
+ log.info("thread.save.success", thread_id=thread_id)
190
210
  except Exception:
191
211
  # best-effort persistence; don't fail the CLI on storage errors
212
+ log.warning("thread.save.failed", exc_info=True)
192
213
  pass
193
214
  finally:
194
215
  await threads.prune_threads()
@@ -200,16 +221,20 @@ def query(
200
221
  finally:
201
222
  # Clean up
202
223
  await db_conn.close()
224
+ log.info("db.connection.closed")
203
225
  console.print("\n[success]Goodbye![/success]")
204
226
 
205
227
  # Run the async function with proper error handling
206
228
  try:
207
229
  asyncio.run(run_session())
208
230
  except CLIError as e:
231
+ get_logger(__name__).error("cli.error", error=str(e))
209
232
  console.print(f"[error]Error:[/error] {e}")
210
233
  sys.exit(e.exit_code)
211
234
 
212
235
 
213
236
  def main():
214
237
  """Entry point for the CLI application."""
238
+ setup_logging()
239
+ get_logger(__name__).info("cli.start")
215
240
  app()
@@ -11,11 +11,13 @@ import questionary
11
11
  from rich.table import Table
12
12
 
13
13
  from sqlsaber.config.database import DatabaseConfig, DatabaseConfigManager
14
+ from sqlsaber.config.logging import get_logger
14
15
  from sqlsaber.theme.manager import create_console
15
16
 
16
17
  # Global instances for CLI commands
17
18
  console = create_console()
18
19
  config_manager = DatabaseConfigManager()
20
+ logger = get_logger(__name__)
19
21
 
20
22
  # Create the database management CLI app
21
23
  db_app = cyclopts.App(
@@ -78,6 +80,13 @@ def add(
78
80
  ] = True,
79
81
  ):
80
82
  """Add a new database connection."""
83
+ logger.info(
84
+ "db.add.start",
85
+ name=name,
86
+ type=type,
87
+ interactive=bool(interactive),
88
+ has_password=False,
89
+ )
81
90
 
82
91
  if interactive:
83
92
  # Interactive mode - prompt for all required fields
@@ -96,6 +105,7 @@ def add(
96
105
 
97
106
  if db_input is None:
98
107
  console.print("[warning]Operation cancelled[/warning]")
108
+ logger.info("db.add.cancelled")
99
109
  return
100
110
 
101
111
  # Extract values from db_input
@@ -116,6 +126,7 @@ def add(
116
126
  console.print(
117
127
  "[bold error]Error:[/bold error] Database file path is required for SQLite"
118
128
  )
129
+ logger.error("db.add.missing_path", db_type="sqlite")
119
130
  sys.exit(1)
120
131
  host = "localhost"
121
132
  port = 0
@@ -126,6 +137,7 @@ def add(
126
137
  console.print(
127
138
  "[bold error]Error:[/bold error] Database file path is required for DuckDB"
128
139
  )
140
+ logger.error("db.add.missing_path", db_type="duckdb")
129
141
  sys.exit(1)
130
142
  database = str(Path(database).expanduser().resolve())
131
143
  host = "localhost"
@@ -137,6 +149,7 @@ def add(
137
149
  console.print(
138
150
  "[bold error]Error:[/bold error] Host, database, and username are required"
139
151
  )
152
+ logger.error("db.add.missing_fields")
140
153
  sys.exit(1)
141
154
 
142
155
  if port is None:
@@ -173,12 +186,15 @@ def add(
173
186
  # Add the configuration
174
187
  config_manager.add_database(db_config, password if password else None)
175
188
  console.print(f"[green]Successfully added database connection '{name}'[/green]")
189
+ logger.info("db.add.success", name=name, type=type)
176
190
 
177
191
  # Set as default if it's the first one
178
192
  if len(config_manager.list_databases()) == 1:
179
193
  console.print(f"[blue]Set '{name}' as default database[/blue]")
194
+ logger.info("db.default.set", name=name)
180
195
 
181
196
  except Exception as e:
197
+ logger.exception("db.add.error", name=name, error=str(e))
182
198
  console.print(f"[bold error]Error adding database:[/bold error] {e}")
183
199
  sys.exit(1)
184
200
 
@@ -186,12 +202,14 @@ def add(
186
202
  @db_app.command
187
203
  def list():
188
204
  """List all configured database connections."""
205
+ logger.info("db.list.start")
189
206
  databases = config_manager.list_databases()
190
207
  default_name = config_manager.get_default_name()
191
208
 
192
209
  if not databases:
193
210
  console.print("[warning]No database connections configured[/warning]")
194
211
  console.print("Use 'sqlsaber db add <name>' to add a database connection")
212
+ logger.info("db.list.empty")
195
213
  return
196
214
 
197
215
  table = Table(title="Database Connections")
@@ -228,6 +246,7 @@ def list():
228
246
  )
229
247
 
230
248
  console.print(table)
249
+ logger.info("db.list.complete", count=len(databases))
231
250
 
232
251
 
233
252
  @db_app.command
@@ -237,10 +256,12 @@ def remove(
237
256
  ],
238
257
  ):
239
258
  """Remove a database connection."""
259
+ logger.info("db.remove.start", name=name)
240
260
  if not config_manager.get_database(name):
241
261
  console.print(
242
262
  f"[bold error]Error:[/bold error] Database connection '{name}' not found"
243
263
  )
264
+ logger.error("db.remove.not_found", name=name)
244
265
  sys.exit(1)
245
266
 
246
267
  if questionary.confirm(
@@ -250,13 +271,16 @@ def remove(
250
271
  console.print(
251
272
  f"[green]Successfully removed database connection '{name}'[/green]"
252
273
  )
274
+ logger.info("db.remove.success", name=name)
253
275
  else:
254
276
  console.print(
255
277
  f"[bold error]Error:[/bold error] Failed to remove database connection '{name}'"
256
278
  )
279
+ logger.error("db.remove.failed", name=name)
257
280
  sys.exit(1)
258
281
  else:
259
282
  console.print("Operation cancelled")
283
+ logger.info("db.remove.cancelled", name=name)
260
284
 
261
285
 
262
286
  @db_app.command
@@ -267,18 +291,22 @@ def set_default(
267
291
  ],
268
292
  ):
269
293
  """Set the default database connection."""
294
+ logger.info("db.default.start", name=name)
270
295
  if not config_manager.get_database(name):
271
296
  console.print(
272
297
  f"[bold error]Error:[/bold error] Database connection '{name}' not found"
273
298
  )
299
+ logger.error("db.default.not_found", name=name)
274
300
  sys.exit(1)
275
301
 
276
302
  if config_manager.set_default_database(name):
277
303
  console.print(f"[green]Successfully set '{name}' as default database[/green]")
304
+ logger.info("db.default.success", name=name)
278
305
  else:
279
306
  console.print(
280
307
  f"[bold error]Error:[/bold error] Failed to set '{name}' as default"
281
308
  )
309
+ logger.error("db.default.failed", name=name)
282
310
  sys.exit(1)
283
311
 
284
312
 
@@ -292,6 +320,7 @@ def test(
292
320
  ] = None,
293
321
  ):
294
322
  """Test a database connection."""
323
+ logger.info("db.test.start")
295
324
 
296
325
  async def test_connection():
297
326
  # Lazy import to keep CLI startup fast
@@ -303,6 +332,7 @@ def test(
303
332
  console.print(
304
333
  f"[bold error]Error:[/bold error] Database connection '{name}' not found"
305
334
  )
335
+ logger.error("db.test.not_found", name=name)
306
336
  sys.exit(1)
307
337
  else:
308
338
  db_config = config_manager.get_default_database()
@@ -313,6 +343,7 @@ def test(
313
343
  console.print(
314
344
  "Use 'sqlsaber db add <name>' to add a database connection"
315
345
  )
346
+ logger.error("db.test.no_default")
316
347
  sys.exit(1)
317
348
 
318
349
  console.print(f"[blue]Testing connection to '{db_config.name}'...[/blue]")
@@ -328,8 +359,16 @@ def test(
328
359
  console.print(
329
360
  f"[green]✓ Connection to '{db_config.name}' successful[/green]"
330
361
  )
362
+ logger.info("db.test.success", name=db_config.name)
331
363
 
332
364
  except Exception as e:
365
+ logger.exception(
366
+ "db.test.failed",
367
+ name=(
368
+ db_config.name if "db_config" in locals() and db_config else name
369
+ ),
370
+ error=str(e),
371
+ )
333
372
  console.print(f"[bold error]✗ Connection failed:[/bold error] {e}")
334
373
  sys.exit(1)
335
374
 
@@ -30,6 +30,7 @@ from sqlsaber.database import (
30
30
  from sqlsaber.database.schema import SchemaManager
31
31
  from sqlsaber.theme.manager import get_theme_manager
32
32
  from sqlsaber.threads import ThreadStorage
33
+ from sqlsaber.config.logging import get_logger
33
34
 
34
35
  if TYPE_CHECKING:
35
36
  from sqlsaber.agents.pydantic_ai_agent import SQLSaberAgent
@@ -66,6 +67,7 @@ class InteractiveSession:
66
67
  self._threads = ThreadStorage()
67
68
  self._thread_id: str | None = initial_thread_id
68
69
  self.first_message = not self._thread_id
70
+ self.log = get_logger(__name__)
69
71
 
70
72
  def _history_path(self) -> Path:
71
73
  """Get the history file path, ensuring directory exists."""
@@ -240,6 +242,7 @@ class InteractiveSession:
240
242
 
241
243
  async def _execute_query_with_cancellation(self, user_query: str):
242
244
  """Execute a query with cancellation support."""
245
+ self.log.info("interactive.query.start", database=self.database_name)
243
246
  # Create cancellation token
244
247
  self.cancellation_token = asyncio.Event()
245
248
 
@@ -276,16 +279,18 @@ class InteractiveSession:
276
279
  model_name=self.sqlsaber_agent.agent.model.model_name,
277
280
  )
278
281
  self.first_message = False
279
- except Exception:
280
- pass
282
+ except Exception as e:
283
+ self.log.warning("interactive.thread.save_failed", error=str(e))
281
284
  finally:
282
285
  await self._threads.prune_threads()
283
286
  finally:
284
287
  self.current_task = None
285
288
  self.cancellation_token = None
289
+ self.log.info("interactive.query.end")
286
290
 
287
291
  async def run(self):
288
292
  """Run the interactive session loop."""
293
+ self.log.info("interactive.start", database=self.database_name)
289
294
  self.show_welcome_message()
290
295
  await self.before_prompt_loop()
291
296
 
@@ -348,3 +353,4 @@ class InteractiveSession:
348
353
  break
349
354
  except Exception as exc:
350
355
  self.console.print(f"[error]Error:[/error] {exc}")
356
+ self.log.exception("interactive.error", error=str(exc))
@@ -8,6 +8,7 @@ import questionary
8
8
  from rich.table import Table
9
9
 
10
10
  from sqlsaber.config.database import DatabaseConfigManager
11
+ from sqlsaber.config.logging import get_logger
11
12
  from sqlsaber.memory.manager import MemoryManager
12
13
  from sqlsaber.theme.manager import create_console
13
14
 
@@ -15,6 +16,7 @@ from sqlsaber.theme.manager import create_console
15
16
  console = create_console()
16
17
  config_manager = DatabaseConfigManager()
17
18
  memory_manager = MemoryManager()
19
+ logger = get_logger(__name__)
18
20
 
19
21
  # Create the memory management CLI app
20
22
  memory_app = cyclopts.App(
@@ -31,6 +33,7 @@ def _get_database_name(database: str | None = None) -> str:
31
33
  console.print(
32
34
  f"[bold error]Error:[/bold error] Database connection '{database}' not found."
33
35
  )
36
+ logger.error("memory.db.not_found", database=database)
34
37
  sys.exit(1)
35
38
  return database
36
39
  else:
@@ -40,6 +43,7 @@ def _get_database_name(database: str | None = None) -> str:
40
43
  "[bold error]Error:[/bold error] No database connections configured."
41
44
  )
42
45
  console.print("Use 'sqlsaber db add <name>' to add a database connection.")
46
+ logger.error("memory.db.none_configured")
43
47
  sys.exit(1)
44
48
  return db_config.name
45
49
 
@@ -57,14 +61,17 @@ def add(
57
61
  ):
58
62
  """Add a new memory for the specified database."""
59
63
  database_name = _get_database_name(database)
64
+ logger.info("memory.add.start", database=database_name)
60
65
 
61
66
  try:
62
67
  memory = memory_manager.add_memory(database_name, content)
63
68
  console.print(f"[green]✓ Memory added for database '{database_name}'[/green]")
64
69
  console.print(f"[dim]Memory ID:[/dim] {memory.id}")
65
70
  console.print(f"[dim]Content:[/dim] {memory.content}")
71
+ logger.info("memory.add.success", database=database_name, id=memory.id)
66
72
  except Exception as e:
67
73
  console.print(f"[bold error]Error adding memory:[/bold error] {e}")
74
+ logger.exception("memory.add.error", database=database_name, error=str(e))
68
75
  sys.exit(1)
69
76
 
70
77
 
@@ -80,6 +87,7 @@ def list(
80
87
  ):
81
88
  """List all memories for the specified database."""
82
89
  database_name = _get_database_name(database)
90
+ logger.info("memory.list.start", database=database_name)
83
91
 
84
92
  memories = memory_manager.get_memories(database_name)
85
93
 
@@ -88,6 +96,7 @@ def list(
88
96
  f"[warning]No memories found for database '{database_name}'[/warning]"
89
97
  )
90
98
  console.print("Use 'sqlsaber memory add \"<content>\"' to add memories")
99
+ logger.info("memory.list.empty", database=database_name)
91
100
  return
92
101
 
93
102
  table = Table(title=f"Memories for Database: {database_name}")
@@ -105,6 +114,7 @@ def list(
105
114
 
106
115
  console.print(table)
107
116
  console.print(f"\n[dim]Total memories: {len(memories)}[/dim]")
117
+ logger.info("memory.list.complete", database=database_name, count=len(memories))
108
118
 
109
119
 
110
120
  @memory_app.command
@@ -120,6 +130,7 @@ def show(
120
130
  ):
121
131
  """Show the full content of a specific memory."""
122
132
  database_name = _get_database_name(database)
133
+ logger.info("memory.show.start", database=database_name, id=memory_id)
123
134
 
124
135
  memory = memory_manager.get_memory_by_id(database_name, memory_id)
125
136
 
@@ -127,6 +138,7 @@ def show(
127
138
  console.print(
128
139
  f"[bold error]Error:[/bold error] Memory with ID '{memory_id}' not found for database '{database_name}'"
129
140
  )
141
+ logger.error("memory.show.not_found", database=database_name, id=memory_id)
130
142
  sys.exit(1)
131
143
 
132
144
  console.print(f"[bold]Memory ID:[/bold] {memory.id}")
@@ -149,6 +161,7 @@ def remove(
149
161
  ):
150
162
  """Remove a specific memory by ID."""
151
163
  database_name = _get_database_name(database)
164
+ logger.info("memory.remove.start", database=database_name, id=memory_id)
152
165
 
153
166
  # First check if memory exists
154
167
  memory = memory_manager.get_memory_by_id(database_name, memory_id)
@@ -156,6 +169,7 @@ def remove(
156
169
  console.print(
157
170
  f"[bold error]Error:[/bold error] Memory with ID '{memory_id}' not found for database '{database_name}'"
158
171
  )
172
+ logger.error("memory.remove.not_found", database=database_name, id=memory_id)
159
173
  sys.exit(1)
160
174
 
161
175
  # Show memory content before removal
@@ -166,10 +180,12 @@ def remove(
166
180
  console.print(
167
181
  f"[green]✓ Memory removed from database '{database_name}'[/green]"
168
182
  )
183
+ logger.info("memory.remove.success", database=database_name, id=memory_id)
169
184
  else:
170
185
  console.print(
171
186
  f"[bold error]Error:[/bold error] Failed to remove memory '{memory_id}'"
172
187
  )
188
+ logger.error("memory.remove.failed", database=database_name, id=memory_id)
173
189
  sys.exit(1)
174
190
 
175
191
 
@@ -192,6 +208,7 @@ def clear(
192
208
  ):
193
209
  """Clear all memories for the specified database."""
194
210
  database_name = _get_database_name(database)
211
+ logger.info("memory.clear.start", database=database_name, force=bool(force))
195
212
 
196
213
  # Count memories first
197
214
  memories_count = len(memory_manager.get_memories(database_name))
@@ -200,6 +217,7 @@ def clear(
200
217
  console.print(
201
218
  f"[warning]No memories to clear for database '{database_name}'[/warning]"
202
219
  )
220
+ logger.info("memory.clear.nothing", database=database_name)
203
221
  return
204
222
 
205
223
  if not force:
@@ -210,12 +228,14 @@ def clear(
210
228
 
211
229
  if not questionary.confirm("Are you sure you want to proceed?").ask():
212
230
  console.print("Operation cancelled")
231
+ logger.info("memory.clear.cancelled", database=database_name)
213
232
  return
214
233
 
215
234
  cleared_count = memory_manager.clear_memories(database_name)
216
235
  console.print(
217
236
  f"[green]✓ Cleared {cleared_count} memories for database '{database_name}'[/green]"
218
237
  )
238
+ logger.info("memory.clear.success", database=database_name, deleted=cleared_count)
219
239
 
220
240
 
221
241
  @memory_app.command
@@ -230,6 +250,7 @@ def summary(
230
250
  ):
231
251
  """Show memory summary for the specified database."""
232
252
  database_name = _get_database_name(database)
253
+ logger.info("memory.summary.start", database=database_name)
233
254
 
234
255
  summary = memory_manager.get_memories_summary(database_name)
235
256
 
@@ -240,6 +261,11 @@ def summary(
240
261
  console.print("\n[bold]Recent memories:[/bold]")
241
262
  for memory in summary["memories"][-5:]: # Show last 5 memories
242
263
  console.print(f"[dim]{memory['timestamp']}[/dim] - {memory['content']}")
264
+ logger.info(
265
+ "memory.summary.complete",
266
+ database=database_name,
267
+ total=summary["total_memories"],
268
+ )
243
269
 
244
270
 
245
271
  def create_memory_app() -> cyclopts.App: