omni-cortex 1.17.1__py3-none-any.whl → 1.17.3__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.
Files changed (87) hide show
  1. omni_cortex/__init__.py +3 -0
  2. omni_cortex/_bundled/dashboard/backend/.env.example +12 -0
  3. omni_cortex/_bundled/dashboard/backend/backfill_summaries.py +280 -0
  4. omni_cortex/_bundled/dashboard/backend/chat_service.py +631 -0
  5. omni_cortex/_bundled/dashboard/backend/database.py +1773 -0
  6. omni_cortex/_bundled/dashboard/backend/image_service.py +552 -0
  7. omni_cortex/_bundled/dashboard/backend/logging_config.py +122 -0
  8. omni_cortex/_bundled/dashboard/backend/main.py +1888 -0
  9. omni_cortex/_bundled/dashboard/backend/models.py +472 -0
  10. omni_cortex/_bundled/dashboard/backend/project_config.py +170 -0
  11. omni_cortex/_bundled/dashboard/backend/project_scanner.py +164 -0
  12. omni_cortex/_bundled/dashboard/backend/prompt_security.py +111 -0
  13. omni_cortex/_bundled/dashboard/backend/pyproject.toml +23 -0
  14. omni_cortex/_bundled/dashboard/backend/security.py +104 -0
  15. omni_cortex/_bundled/dashboard/backend/uv.lock +1110 -0
  16. omni_cortex/_bundled/dashboard/backend/websocket_manager.py +104 -0
  17. omni_cortex/_bundled/hooks/post_tool_use.py +497 -0
  18. omni_cortex/_bundled/hooks/pre_tool_use.py +277 -0
  19. omni_cortex/_bundled/hooks/session_utils.py +186 -0
  20. omni_cortex/_bundled/hooks/stop.py +219 -0
  21. omni_cortex/_bundled/hooks/subagent_stop.py +120 -0
  22. omni_cortex/_bundled/hooks/user_prompt.py +220 -0
  23. omni_cortex/categorization/__init__.py +9 -0
  24. omni_cortex/categorization/auto_tags.py +166 -0
  25. omni_cortex/categorization/auto_type.py +165 -0
  26. omni_cortex/config.py +141 -0
  27. omni_cortex/dashboard.py +238 -0
  28. omni_cortex/database/__init__.py +24 -0
  29. omni_cortex/database/connection.py +137 -0
  30. omni_cortex/database/migrations.py +210 -0
  31. omni_cortex/database/schema.py +212 -0
  32. omni_cortex/database/sync.py +421 -0
  33. omni_cortex/decay/__init__.py +7 -0
  34. omni_cortex/decay/importance.py +147 -0
  35. omni_cortex/embeddings/__init__.py +35 -0
  36. omni_cortex/embeddings/local.py +442 -0
  37. omni_cortex/models/__init__.py +20 -0
  38. omni_cortex/models/activity.py +265 -0
  39. omni_cortex/models/agent.py +144 -0
  40. omni_cortex/models/memory.py +395 -0
  41. omni_cortex/models/relationship.py +206 -0
  42. omni_cortex/models/session.py +290 -0
  43. omni_cortex/resources/__init__.py +1 -0
  44. omni_cortex/search/__init__.py +22 -0
  45. omni_cortex/search/hybrid.py +197 -0
  46. omni_cortex/search/keyword.py +204 -0
  47. omni_cortex/search/ranking.py +127 -0
  48. omni_cortex/search/semantic.py +232 -0
  49. omni_cortex/server.py +360 -0
  50. omni_cortex/setup.py +284 -0
  51. omni_cortex/tools/__init__.py +13 -0
  52. omni_cortex/tools/activities.py +453 -0
  53. omni_cortex/tools/memories.py +536 -0
  54. omni_cortex/tools/sessions.py +311 -0
  55. omni_cortex/tools/utilities.py +477 -0
  56. omni_cortex/utils/__init__.py +13 -0
  57. omni_cortex/utils/formatting.py +282 -0
  58. omni_cortex/utils/ids.py +72 -0
  59. omni_cortex/utils/timestamps.py +129 -0
  60. omni_cortex/utils/truncation.py +111 -0
  61. {omni_cortex-1.17.1.dist-info → omni_cortex-1.17.3.dist-info}/METADATA +1 -1
  62. omni_cortex-1.17.3.dist-info/RECORD +86 -0
  63. omni_cortex-1.17.1.dist-info/RECORD +0 -26
  64. {omni_cortex-1.17.1.data → omni_cortex-1.17.3.data}/data/share/omni-cortex/dashboard/backend/.env.example +0 -0
  65. {omni_cortex-1.17.1.data → omni_cortex-1.17.3.data}/data/share/omni-cortex/dashboard/backend/backfill_summaries.py +0 -0
  66. {omni_cortex-1.17.1.data → omni_cortex-1.17.3.data}/data/share/omni-cortex/dashboard/backend/chat_service.py +0 -0
  67. {omni_cortex-1.17.1.data → omni_cortex-1.17.3.data}/data/share/omni-cortex/dashboard/backend/database.py +0 -0
  68. {omni_cortex-1.17.1.data → omni_cortex-1.17.3.data}/data/share/omni-cortex/dashboard/backend/image_service.py +0 -0
  69. {omni_cortex-1.17.1.data → omni_cortex-1.17.3.data}/data/share/omni-cortex/dashboard/backend/logging_config.py +0 -0
  70. {omni_cortex-1.17.1.data → omni_cortex-1.17.3.data}/data/share/omni-cortex/dashboard/backend/main.py +0 -0
  71. {omni_cortex-1.17.1.data → omni_cortex-1.17.3.data}/data/share/omni-cortex/dashboard/backend/models.py +0 -0
  72. {omni_cortex-1.17.1.data → omni_cortex-1.17.3.data}/data/share/omni-cortex/dashboard/backend/project_config.py +0 -0
  73. {omni_cortex-1.17.1.data → omni_cortex-1.17.3.data}/data/share/omni-cortex/dashboard/backend/project_scanner.py +0 -0
  74. {omni_cortex-1.17.1.data → omni_cortex-1.17.3.data}/data/share/omni-cortex/dashboard/backend/prompt_security.py +0 -0
  75. {omni_cortex-1.17.1.data → omni_cortex-1.17.3.data}/data/share/omni-cortex/dashboard/backend/pyproject.toml +0 -0
  76. {omni_cortex-1.17.1.data → omni_cortex-1.17.3.data}/data/share/omni-cortex/dashboard/backend/security.py +0 -0
  77. {omni_cortex-1.17.1.data → omni_cortex-1.17.3.data}/data/share/omni-cortex/dashboard/backend/uv.lock +0 -0
  78. {omni_cortex-1.17.1.data → omni_cortex-1.17.3.data}/data/share/omni-cortex/dashboard/backend/websocket_manager.py +0 -0
  79. {omni_cortex-1.17.1.data → omni_cortex-1.17.3.data}/data/share/omni-cortex/hooks/post_tool_use.py +0 -0
  80. {omni_cortex-1.17.1.data → omni_cortex-1.17.3.data}/data/share/omni-cortex/hooks/pre_tool_use.py +0 -0
  81. {omni_cortex-1.17.1.data → omni_cortex-1.17.3.data}/data/share/omni-cortex/hooks/session_utils.py +0 -0
  82. {omni_cortex-1.17.1.data → omni_cortex-1.17.3.data}/data/share/omni-cortex/hooks/stop.py +0 -0
  83. {omni_cortex-1.17.1.data → omni_cortex-1.17.3.data}/data/share/omni-cortex/hooks/subagent_stop.py +0 -0
  84. {omni_cortex-1.17.1.data → omni_cortex-1.17.3.data}/data/share/omni-cortex/hooks/user_prompt.py +0 -0
  85. {omni_cortex-1.17.1.dist-info → omni_cortex-1.17.3.dist-info}/WHEEL +0 -0
  86. {omni_cortex-1.17.1.dist-info → omni_cortex-1.17.3.dist-info}/entry_points.txt +0 -0
  87. {omni_cortex-1.17.1.dist-info → omni_cortex-1.17.3.dist-info}/licenses/LICENSE +0 -0
omni_cortex/config.py ADDED
@@ -0,0 +1,141 @@
1
+ """Configuration management for Omni Cortex."""
2
+
3
+ import os
4
+ from pathlib import Path
5
+ from typing import Optional
6
+ from dataclasses import dataclass, field
7
+
8
+ import yaml
9
+
10
+
11
+ @dataclass
12
+ class CortexConfig:
13
+ """Configuration settings for Omni Cortex."""
14
+
15
+ # Database
16
+ schema_version: str = "1.0"
17
+
18
+ # Embedding (disabled by default - model loading can be slow)
19
+ embedding_model: str = "all-MiniLM-L6-v2"
20
+ embedding_enabled: bool = False
21
+
22
+ # Decay
23
+ decay_rate_per_day: float = 0.5
24
+ freshness_review_days: int = 30
25
+
26
+ # Output
27
+ max_output_truncation: int = 10000
28
+ max_tool_input_size: int = 10000
29
+
30
+ # Session
31
+ auto_provide_context: bool = True
32
+ context_depth: int = 3
33
+
34
+ # Search (default to keyword since embeddings are disabled by default)
35
+ default_search_mode: str = "keyword"
36
+
37
+ # Global
38
+ global_sync_enabled: bool = True
39
+ api_fallback_enabled: bool = False
40
+ api_key: str = ""
41
+
42
+
43
+ def get_project_path() -> Path:
44
+ """Get the current project path from environment or cwd."""
45
+ return Path(os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd()))
46
+
47
+
48
+ def get_project_db_dir() -> Path:
49
+ """Get the project-local .omni-cortex directory."""
50
+ return get_project_path() / ".omni-cortex"
51
+
52
+
53
+ def get_project_db_path() -> Path:
54
+ """Get the path to the project database."""
55
+ return get_project_db_dir() / "cortex.db"
56
+
57
+
58
+ def get_global_db_dir() -> Path:
59
+ """Get the global ~/.omni-cortex directory."""
60
+ return Path.home() / ".omni-cortex"
61
+
62
+
63
+ def get_global_db_path() -> Path:
64
+ """Get the path to the global database."""
65
+ return get_global_db_dir() / "global.db"
66
+
67
+
68
+ def get_session_id() -> Optional[str]:
69
+ """Get the current session ID from environment."""
70
+ return os.environ.get("CLAUDE_SESSION_ID")
71
+
72
+
73
+ def load_config(project_path: Optional[Path] = None) -> CortexConfig:
74
+ """Load configuration from project and global config files.
75
+
76
+ Priority: project config > global config > defaults
77
+ """
78
+ config = CortexConfig()
79
+
80
+ # Load global config
81
+ global_config_path = get_global_db_dir() / "config.yaml"
82
+ if global_config_path.exists():
83
+ try:
84
+ with open(global_config_path, "r") as f:
85
+ global_cfg = yaml.safe_load(f) or {}
86
+ _apply_config(config, global_cfg)
87
+ except Exception:
88
+ pass
89
+
90
+ # Load project config
91
+ if project_path is None:
92
+ project_path = get_project_path()
93
+ project_config_path = project_path / ".omni-cortex" / "config.yaml"
94
+ if project_config_path.exists():
95
+ try:
96
+ with open(project_config_path, "r") as f:
97
+ project_cfg = yaml.safe_load(f) or {}
98
+ _apply_config(config, project_cfg)
99
+ except Exception:
100
+ pass
101
+
102
+ return config
103
+
104
+
105
+ def _apply_config(config: CortexConfig, data: dict) -> None:
106
+ """Apply configuration data to config object."""
107
+ for key, value in data.items():
108
+ if hasattr(config, key) and value is not None:
109
+ setattr(config, key, value)
110
+
111
+
112
+ def save_config(config: CortexConfig, project: bool = True) -> None:
113
+ """Save configuration to file.
114
+
115
+ Args:
116
+ config: Configuration to save
117
+ project: If True, save to project config; otherwise global
118
+ """
119
+ if project:
120
+ config_dir = get_project_db_dir()
121
+ else:
122
+ config_dir = get_global_db_dir()
123
+
124
+ config_dir.mkdir(parents=True, exist_ok=True)
125
+ config_path = config_dir / "config.yaml"
126
+
127
+ data = {
128
+ "schema_version": config.schema_version,
129
+ "embedding_model": config.embedding_model,
130
+ "embedding_enabled": config.embedding_enabled,
131
+ "decay_rate_per_day": config.decay_rate_per_day,
132
+ "freshness_review_days": config.freshness_review_days,
133
+ "max_output_truncation": config.max_output_truncation,
134
+ "auto_provide_context": config.auto_provide_context,
135
+ "context_depth": config.context_depth,
136
+ "default_search_mode": config.default_search_mode,
137
+ "global_sync_enabled": config.global_sync_enabled,
138
+ }
139
+
140
+ with open(config_path, "w") as f:
141
+ yaml.dump(data, f, default_flow_style=False)
@@ -0,0 +1,238 @@
1
+ """Dashboard CLI for Omni-Cortex.
2
+
3
+ Starts the web dashboard server for viewing and managing memories.
4
+ """
5
+
6
+ import argparse
7
+ import os
8
+ import subprocess
9
+ import sys
10
+ import webbrowser
11
+ from pathlib import Path
12
+ from time import sleep
13
+
14
+
15
+ def check_editable_install() -> bool:
16
+ """Check if package is installed in editable (development) mode.
17
+
18
+ Returns True if editable, False if installed from PyPI.
19
+ """
20
+ try:
21
+ import importlib.metadata as metadata
22
+ dist = metadata.distribution("omni-cortex")
23
+ # Editable installs have a direct_url.json with editable=true
24
+ # or are installed via .egg-link
25
+ direct_url = dist.read_text("direct_url.json")
26
+ if direct_url and '"editable":true' in direct_url.replace(" ", ""):
27
+ return True
28
+ except Exception:
29
+ pass
30
+
31
+ # Alternative check: see if we're running from source directory
32
+ package_dir = Path(__file__).parent
33
+ repo_root = package_dir.parent.parent
34
+ if (repo_root / "pyproject.toml").exists() and (repo_root / ".git").exists():
35
+ # We're in a repo, check if there's an egg-link or editable marker
36
+ import site
37
+ for site_dir in [site.getusersitepackages()] + site.getsitepackages():
38
+ egg_link = Path(site_dir) / "omni-cortex.egg-link"
39
+ if egg_link.exists():
40
+ return True
41
+ # Check for __editable__ marker (PEP 660) - any version
42
+ for pth_file in Path(site_dir).glob("__editable__.omni_cortex*.pth"):
43
+ return True
44
+
45
+ return False
46
+
47
+
48
+ def warn_non_editable_install() -> None:
49
+ """Warn if not running in editable mode during development."""
50
+ if not check_editable_install():
51
+ # Check if we appear to be in a development context
52
+ package_dir = Path(__file__).parent
53
+ repo_root = package_dir.parent.parent
54
+ if (repo_root / "pyproject.toml").exists() and (repo_root / ".git").exists():
55
+ print("[Dashboard] Note: Package may not be in editable mode.")
56
+ print("[Dashboard] If you see import errors, run: pip install -e .")
57
+ print()
58
+
59
+
60
+ def find_dashboard_dir() -> Path | None:
61
+ """Find the dashboard directory.
62
+
63
+ Searches in order:
64
+ 1. Bundled inside package (works for user and system installs)
65
+ 2. Development directory (cloned repo)
66
+ 3. Package shared-data (installed via pip system-wide)
67
+ 4. Site-packages share location
68
+ """
69
+ package_dir = Path(__file__).parent
70
+
71
+ # Check for bundled dashboard inside package (most reliable for pip installs)
72
+ bundled_dashboard = package_dir / "_bundled" / "dashboard"
73
+ if bundled_dashboard.exists() and (bundled_dashboard / "backend" / "main.py").exists():
74
+ return bundled_dashboard
75
+
76
+ # Check for development directory (repo structure)
77
+ # Go up from src/omni_cortex to repo root, then dashboard
78
+ repo_root = package_dir.parent.parent
79
+ dashboard_in_repo = repo_root / "dashboard"
80
+ if dashboard_in_repo.exists() and (dashboard_in_repo / "backend" / "main.py").exists():
81
+ return dashboard_in_repo
82
+
83
+ # Check pip shared-data location (for backwards compatibility)
84
+ # On Unix: ~/.local/share/omni-cortex/dashboard
85
+ # On Windows: %APPDATA%/Python/share/omni-cortex/dashboard
86
+ import site
87
+ for site_dir in site.getsitepackages() + [site.getusersitepackages()]:
88
+ share_dir = Path(site_dir).parent / "share" / "omni-cortex" / "dashboard"
89
+ if share_dir.exists() and (share_dir / "backend" / "main.py").exists():
90
+ return share_dir
91
+
92
+ # Check relative to sys.prefix (virtualenv)
93
+ share_in_prefix = Path(sys.prefix) / "share" / "omni-cortex" / "dashboard"
94
+ if share_in_prefix.exists() and (share_in_prefix / "backend" / "main.py").exists():
95
+ return share_in_prefix
96
+
97
+ return None
98
+
99
+
100
+ def check_dependencies() -> bool:
101
+ """Check if dashboard dependencies are installed."""
102
+ try:
103
+ import uvicorn # noqa: F401
104
+ import fastapi # noqa: F401
105
+ return True
106
+ except ImportError:
107
+ return False
108
+
109
+
110
+ def install_dependencies() -> bool:
111
+ """Install dashboard dependencies."""
112
+ required_packages = ["uvicorn", "fastapi"]
113
+
114
+ print("[Dashboard] Installing dependencies...")
115
+ try:
116
+ subprocess.check_call(
117
+ [sys.executable, "-m", "pip", "install", *required_packages, "-q"],
118
+ stdout=subprocess.DEVNULL,
119
+ stderr=subprocess.DEVNULL,
120
+ )
121
+ return True
122
+ except subprocess.CalledProcessError:
123
+ return False
124
+
125
+
126
+ def start_server(dashboard_dir: Path, host: str, port: int, no_browser: bool) -> None:
127
+ """Start the dashboard server."""
128
+ backend_dir = dashboard_dir / "backend"
129
+
130
+ # Add backend to path
131
+ sys.path.insert(0, str(backend_dir))
132
+
133
+ # Change to backend directory for relative imports
134
+ original_cwd = os.getcwd()
135
+ os.chdir(backend_dir)
136
+
137
+ try:
138
+ import uvicorn
139
+
140
+ print(f"\n[Dashboard] Starting Omni-Cortex Dashboard")
141
+ print(f"[Dashboard] URL: http://{host}:{port}")
142
+ print(f"[Dashboard] API Docs: http://{host}:{port}/docs")
143
+ print(f"[Dashboard] Press Ctrl+C to stop\n")
144
+
145
+ # Open browser after short delay
146
+ if not no_browser:
147
+ def open_browser():
148
+ sleep(1.5)
149
+ webbrowser.open(f"http://{host}:{port}")
150
+
151
+ import threading
152
+ threading.Thread(target=open_browser, daemon=True).start()
153
+
154
+ # Run the server
155
+ uvicorn.run(
156
+ "main:app",
157
+ host=host,
158
+ port=port,
159
+ reload=False,
160
+ log_level="info",
161
+ )
162
+ finally:
163
+ os.chdir(original_cwd)
164
+
165
+
166
+ def main():
167
+ """Main entry point for omni-cortex dashboard command."""
168
+ # Check for potential editable install issues early
169
+ warn_non_editable_install()
170
+
171
+ parser = argparse.ArgumentParser(
172
+ description="Start the Omni-Cortex web dashboard",
173
+ formatter_class=argparse.RawDescriptionHelpFormatter,
174
+ epilog="""
175
+ Examples:
176
+ omni-cortex dashboard Start on default port 8765
177
+ omni-cortex dashboard --port 9000 Start on custom port
178
+ omni-cortex dashboard --no-browser Don't auto-open browser
179
+ """
180
+ )
181
+ parser.add_argument(
182
+ "--host",
183
+ default="127.0.0.1",
184
+ help="Host to bind to (default: 127.0.0.1)"
185
+ )
186
+ parser.add_argument(
187
+ "--port", "-p",
188
+ type=int,
189
+ default=8765,
190
+ help="Port to run on (default: 8765)"
191
+ )
192
+ parser.add_argument(
193
+ "--no-browser",
194
+ action="store_true",
195
+ help="Don't automatically open browser"
196
+ )
197
+
198
+ args = parser.parse_args()
199
+
200
+ # Find dashboard directory
201
+ dashboard_dir = find_dashboard_dir()
202
+ if not dashboard_dir:
203
+ print("[Dashboard] Error: Dashboard files not found.")
204
+ print("[Dashboard] If you installed via pip, try reinstalling:")
205
+ print(" pip install --force-reinstall omni-cortex")
206
+ print("\nOr clone the repository:")
207
+ print(" git clone https://github.com/AllCytes/Omni-Cortex.git")
208
+ sys.exit(1)
209
+
210
+ print(f"[Dashboard] Found dashboard at: {dashboard_dir}")
211
+
212
+ # Check/install dependencies
213
+ if not check_dependencies():
214
+ print("[Dashboard] Installing required dependencies...")
215
+ if not install_dependencies():
216
+ print("[Dashboard] Error: Failed to install dependencies.")
217
+ print("[Dashboard] Try manually: pip install uvicorn fastapi")
218
+ sys.exit(1)
219
+
220
+ # Check if dist exists (built frontend)
221
+ dist_dir = dashboard_dir / "frontend" / "dist"
222
+ if not dist_dir.exists():
223
+ print(f"[Dashboard] Warning: Frontend not built ({dist_dir})")
224
+ print("[Dashboard] API will work but web UI may not be available.")
225
+ print("[Dashboard] To build: cd dashboard/frontend && npm install && npm run build")
226
+
227
+ # Start the server
228
+ try:
229
+ start_server(dashboard_dir, args.host, args.port, args.no_browser)
230
+ except KeyboardInterrupt:
231
+ print("\n[Dashboard] Stopped")
232
+ except Exception as e:
233
+ print(f"[Dashboard] Error: {e}")
234
+ sys.exit(1)
235
+
236
+
237
+ if __name__ == "__main__":
238
+ main()
@@ -0,0 +1,24 @@
1
+ """Database layer for Omni Cortex - SQLite with FTS5."""
2
+
3
+ from .connection import get_connection, init_database, close_connection
4
+ from .schema import SCHEMA_VERSION, get_schema_sql
5
+ from .sync import (
6
+ sync_memory_to_global,
7
+ delete_memory_from_global,
8
+ search_global_memories,
9
+ get_global_stats,
10
+ sync_all_project_memories,
11
+ )
12
+
13
+ __all__ = [
14
+ "get_connection",
15
+ "init_database",
16
+ "close_connection",
17
+ "SCHEMA_VERSION",
18
+ "get_schema_sql",
19
+ "sync_memory_to_global",
20
+ "delete_memory_from_global",
21
+ "search_global_memories",
22
+ "get_global_stats",
23
+ "sync_all_project_memories",
24
+ ]
@@ -0,0 +1,137 @@
1
+ """SQLite connection management for Omni Cortex."""
2
+
3
+ import sqlite3
4
+ import threading
5
+ from pathlib import Path
6
+ from typing import Optional
7
+ from contextlib import contextmanager
8
+
9
+ from ..config import get_project_db_path, get_global_db_path, get_project_db_dir, get_global_db_dir
10
+ from .schema import get_schema_sql, SCHEMA_VERSION
11
+
12
+
13
+ # Thread-local storage for connections
14
+ _local = threading.local()
15
+
16
+ # Connection cache by path
17
+ _connections: dict[str, sqlite3.Connection] = {}
18
+ _lock = threading.Lock()
19
+
20
+
21
+ def _configure_connection(conn: sqlite3.Connection) -> None:
22
+ """Configure a SQLite connection with optimal settings."""
23
+ conn.row_factory = sqlite3.Row
24
+ conn.execute("PRAGMA foreign_keys = ON")
25
+ conn.execute("PRAGMA journal_mode = WAL")
26
+ conn.execute("PRAGMA synchronous = NORMAL")
27
+ conn.execute("PRAGMA cache_size = -64000") # 64MB cache
28
+
29
+
30
+ def get_connection(db_path: Optional[Path] = None, is_global: bool = False) -> sqlite3.Connection:
31
+ """Get a database connection, creating it if necessary.
32
+
33
+ Args:
34
+ db_path: Explicit path to database file
35
+ is_global: If True and no db_path, use global database
36
+
37
+ Returns:
38
+ SQLite connection
39
+ """
40
+ if db_path is None:
41
+ db_path = get_global_db_path() if is_global else get_project_db_path()
42
+
43
+ path_str = str(db_path)
44
+
45
+ with _lock:
46
+ if path_str not in _connections:
47
+ # Ensure directory exists
48
+ db_path.parent.mkdir(parents=True, exist_ok=True)
49
+
50
+ # Create connection
51
+ conn = sqlite3.connect(path_str, check_same_thread=False)
52
+ _configure_connection(conn)
53
+ _connections[path_str] = conn
54
+
55
+ return _connections[path_str]
56
+
57
+
58
+ def init_database(db_path: Optional[Path] = None, is_global: bool = False) -> sqlite3.Connection:
59
+ """Initialize the database with schema.
60
+
61
+ Args:
62
+ db_path: Explicit path to database file
63
+ is_global: If True and no db_path, use global database
64
+
65
+ Returns:
66
+ SQLite connection
67
+ """
68
+ if db_path is None:
69
+ if is_global:
70
+ db_path = get_global_db_path()
71
+ db_dir = get_global_db_dir()
72
+ else:
73
+ db_path = get_project_db_path()
74
+ db_dir = get_project_db_dir()
75
+ else:
76
+ db_dir = db_path.parent
77
+
78
+ # Ensure directory exists
79
+ db_dir.mkdir(parents=True, exist_ok=True)
80
+
81
+ conn = get_connection(db_path)
82
+
83
+ # Check if schema needs initialization
84
+ cursor = conn.cursor()
85
+
86
+ # Check if tables exist
87
+ cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='memories'")
88
+ if cursor.fetchone() is None:
89
+ # Apply schema
90
+ conn.executescript(get_schema_sql())
91
+
92
+ # Record schema version
93
+ from ..utils.timestamps import now_iso
94
+ cursor.execute(
95
+ "INSERT OR REPLACE INTO schema_migrations (version, applied_at) VALUES (?, ?)",
96
+ (SCHEMA_VERSION, now_iso())
97
+ )
98
+ conn.commit()
99
+
100
+ return conn
101
+
102
+
103
+ def close_connection(db_path: Optional[Path] = None, is_global: bool = False) -> None:
104
+ """Close a database connection.
105
+
106
+ Args:
107
+ db_path: Explicit path to database file
108
+ is_global: If True and no db_path, use global database
109
+ """
110
+ if db_path is None:
111
+ db_path = get_global_db_path() if is_global else get_project_db_path()
112
+
113
+ path_str = str(db_path)
114
+
115
+ with _lock:
116
+ if path_str in _connections:
117
+ _connections[path_str].close()
118
+ del _connections[path_str]
119
+
120
+
121
+ def close_all_connections() -> None:
122
+ """Close all database connections."""
123
+ with _lock:
124
+ for conn in _connections.values():
125
+ conn.close()
126
+ _connections.clear()
127
+
128
+
129
+ @contextmanager
130
+ def transaction(conn: sqlite3.Connection):
131
+ """Context manager for database transactions."""
132
+ try:
133
+ yield conn
134
+ conn.commit()
135
+ except Exception:
136
+ conn.rollback()
137
+ raise