omni-cortex 1.17.2__py3-none-any.whl → 1.17.4__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 (93) hide show
  1. omni_cortex/_bundled/dashboard/backend/.env.example +12 -0
  2. omni_cortex/_bundled/dashboard/backend/backfill_summaries.py +280 -0
  3. omni_cortex/_bundled/dashboard/backend/chat_service.py +631 -0
  4. omni_cortex/_bundled/dashboard/backend/database.py +1773 -0
  5. omni_cortex/_bundled/dashboard/backend/image_service.py +552 -0
  6. omni_cortex/_bundled/dashboard/backend/logging_config.py +122 -0
  7. omni_cortex/_bundled/dashboard/backend/main.py +1888 -0
  8. omni_cortex/_bundled/dashboard/backend/models.py +472 -0
  9. omni_cortex/_bundled/dashboard/backend/project_config.py +170 -0
  10. omni_cortex/_bundled/dashboard/backend/project_scanner.py +164 -0
  11. omni_cortex/_bundled/dashboard/backend/prompt_security.py +111 -0
  12. omni_cortex/_bundled/dashboard/backend/pyproject.toml +23 -0
  13. omni_cortex/_bundled/dashboard/backend/security.py +104 -0
  14. omni_cortex/_bundled/dashboard/backend/test_database.py +301 -0
  15. omni_cortex/_bundled/dashboard/backend/tmpclaude-2dfa-cwd +1 -0
  16. omni_cortex/_bundled/dashboard/backend/tmpclaude-c460-cwd +1 -0
  17. omni_cortex/_bundled/dashboard/backend/uv.lock +1110 -0
  18. omni_cortex/_bundled/dashboard/backend/websocket_manager.py +104 -0
  19. omni_cortex/_bundled/dashboard/frontend/dist/assets/index-CQlQK3nE.js +551 -0
  20. omni_cortex/_bundled/dashboard/frontend/dist/assets/index-CmUNNfe4.css +1 -0
  21. omni_cortex/_bundled/dashboard/frontend/dist/index.html +14 -0
  22. omni_cortex/_bundled/hooks/post_tool_use.py +497 -0
  23. omni_cortex/_bundled/hooks/pre_tool_use.py +277 -0
  24. omni_cortex/_bundled/hooks/session_utils.py +186 -0
  25. omni_cortex/_bundled/hooks/stop.py +219 -0
  26. omni_cortex/_bundled/hooks/subagent_stop.py +120 -0
  27. omni_cortex/_bundled/hooks/user_prompt.py +331 -0
  28. {omni_cortex-1.17.2.data → omni_cortex-1.17.4.data}/data/share/omni-cortex/dashboard/backend/main.py +2 -2
  29. {omni_cortex-1.17.2.data → omni_cortex-1.17.4.data}/data/share/omni-cortex/hooks/user_prompt.py +113 -2
  30. {omni_cortex-1.17.2.dist-info → omni_cortex-1.17.4.dist-info}/METADATA +6 -1
  31. omni_cortex-1.17.4.dist-info/RECORD +53 -0
  32. omni_cortex/__init__.py +0 -3
  33. omni_cortex/categorization/__init__.py +0 -9
  34. omni_cortex/categorization/auto_tags.py +0 -166
  35. omni_cortex/categorization/auto_type.py +0 -165
  36. omni_cortex/config.py +0 -141
  37. omni_cortex/dashboard.py +0 -232
  38. omni_cortex/database/__init__.py +0 -24
  39. omni_cortex/database/connection.py +0 -137
  40. omni_cortex/database/migrations.py +0 -210
  41. omni_cortex/database/schema.py +0 -212
  42. omni_cortex/database/sync.py +0 -421
  43. omni_cortex/decay/__init__.py +0 -7
  44. omni_cortex/decay/importance.py +0 -147
  45. omni_cortex/embeddings/__init__.py +0 -35
  46. omni_cortex/embeddings/local.py +0 -442
  47. omni_cortex/models/__init__.py +0 -20
  48. omni_cortex/models/activity.py +0 -265
  49. omni_cortex/models/agent.py +0 -144
  50. omni_cortex/models/memory.py +0 -395
  51. omni_cortex/models/relationship.py +0 -206
  52. omni_cortex/models/session.py +0 -290
  53. omni_cortex/resources/__init__.py +0 -1
  54. omni_cortex/search/__init__.py +0 -22
  55. omni_cortex/search/hybrid.py +0 -197
  56. omni_cortex/search/keyword.py +0 -204
  57. omni_cortex/search/ranking.py +0 -127
  58. omni_cortex/search/semantic.py +0 -232
  59. omni_cortex/server.py +0 -360
  60. omni_cortex/setup.py +0 -278
  61. omni_cortex/tools/__init__.py +0 -13
  62. omni_cortex/tools/activities.py +0 -453
  63. omni_cortex/tools/memories.py +0 -536
  64. omni_cortex/tools/sessions.py +0 -311
  65. omni_cortex/tools/utilities.py +0 -477
  66. omni_cortex/utils/__init__.py +0 -13
  67. omni_cortex/utils/formatting.py +0 -282
  68. omni_cortex/utils/ids.py +0 -72
  69. omni_cortex/utils/timestamps.py +0 -129
  70. omni_cortex/utils/truncation.py +0 -111
  71. omni_cortex-1.17.2.dist-info/RECORD +0 -65
  72. {omni_cortex-1.17.2.data → omni_cortex-1.17.4.data}/data/share/omni-cortex/dashboard/backend/.env.example +0 -0
  73. {omni_cortex-1.17.2.data → omni_cortex-1.17.4.data}/data/share/omni-cortex/dashboard/backend/backfill_summaries.py +0 -0
  74. {omni_cortex-1.17.2.data → omni_cortex-1.17.4.data}/data/share/omni-cortex/dashboard/backend/chat_service.py +0 -0
  75. {omni_cortex-1.17.2.data → omni_cortex-1.17.4.data}/data/share/omni-cortex/dashboard/backend/database.py +0 -0
  76. {omni_cortex-1.17.2.data → omni_cortex-1.17.4.data}/data/share/omni-cortex/dashboard/backend/image_service.py +0 -0
  77. {omni_cortex-1.17.2.data → omni_cortex-1.17.4.data}/data/share/omni-cortex/dashboard/backend/logging_config.py +0 -0
  78. {omni_cortex-1.17.2.data → omni_cortex-1.17.4.data}/data/share/omni-cortex/dashboard/backend/models.py +0 -0
  79. {omni_cortex-1.17.2.data → omni_cortex-1.17.4.data}/data/share/omni-cortex/dashboard/backend/project_config.py +0 -0
  80. {omni_cortex-1.17.2.data → omni_cortex-1.17.4.data}/data/share/omni-cortex/dashboard/backend/project_scanner.py +0 -0
  81. {omni_cortex-1.17.2.data → omni_cortex-1.17.4.data}/data/share/omni-cortex/dashboard/backend/prompt_security.py +0 -0
  82. {omni_cortex-1.17.2.data → omni_cortex-1.17.4.data}/data/share/omni-cortex/dashboard/backend/pyproject.toml +0 -0
  83. {omni_cortex-1.17.2.data → omni_cortex-1.17.4.data}/data/share/omni-cortex/dashboard/backend/security.py +0 -0
  84. {omni_cortex-1.17.2.data → omni_cortex-1.17.4.data}/data/share/omni-cortex/dashboard/backend/uv.lock +0 -0
  85. {omni_cortex-1.17.2.data → omni_cortex-1.17.4.data}/data/share/omni-cortex/dashboard/backend/websocket_manager.py +0 -0
  86. {omni_cortex-1.17.2.data → omni_cortex-1.17.4.data}/data/share/omni-cortex/hooks/post_tool_use.py +0 -0
  87. {omni_cortex-1.17.2.data → omni_cortex-1.17.4.data}/data/share/omni-cortex/hooks/pre_tool_use.py +0 -0
  88. {omni_cortex-1.17.2.data → omni_cortex-1.17.4.data}/data/share/omni-cortex/hooks/session_utils.py +0 -0
  89. {omni_cortex-1.17.2.data → omni_cortex-1.17.4.data}/data/share/omni-cortex/hooks/stop.py +0 -0
  90. {omni_cortex-1.17.2.data → omni_cortex-1.17.4.data}/data/share/omni-cortex/hooks/subagent_stop.py +0 -0
  91. {omni_cortex-1.17.2.dist-info → omni_cortex-1.17.4.dist-info}/WHEEL +0 -0
  92. {omni_cortex-1.17.2.dist-info → omni_cortex-1.17.4.dist-info}/entry_points.txt +0 -0
  93. {omni_cortex-1.17.2.dist-info → omni_cortex-1.17.4.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,164 @@
1
+ """Scanner to discover all omni-cortex databases on the system."""
2
+
3
+ import sqlite3
4
+ from datetime import datetime
5
+ from pathlib import Path
6
+
7
+ from models import ProjectInfo
8
+ from project_config import load_config
9
+
10
+
11
+ def get_global_db_path() -> Path:
12
+ """Get path to the global index database."""
13
+ return Path.home() / ".omni-cortex" / "global.db"
14
+
15
+
16
+ def get_memory_count(db_path: Path) -> int:
17
+ """Get the number of memories in a database."""
18
+ try:
19
+ conn = sqlite3.connect(str(db_path))
20
+ cursor = conn.execute("SELECT COUNT(*) FROM memories")
21
+ count = cursor.fetchone()[0]
22
+ conn.close()
23
+ return count
24
+ except Exception:
25
+ return 0
26
+
27
+
28
+ def get_projects_from_global_db() -> list[str]:
29
+ """Get unique project paths from the global index."""
30
+ global_path = get_global_db_path()
31
+ if not global_path.exists():
32
+ return []
33
+
34
+ try:
35
+ conn = sqlite3.connect(str(global_path))
36
+ cursor = conn.execute("SELECT DISTINCT source_project FROM global_memories")
37
+ paths = [row[0] for row in cursor.fetchall() if row[0]]
38
+ conn.close()
39
+ return paths
40
+ except Exception:
41
+ return []
42
+
43
+
44
+ def scan_directory_for_cortex(base_dir: Path) -> list[Path]:
45
+ """Scan a directory for .omni-cortex/cortex.db files."""
46
+ found = []
47
+ try:
48
+ for item in base_dir.iterdir():
49
+ if item.is_dir():
50
+ cortex_dir = item / ".omni-cortex"
51
+ cortex_db = cortex_dir / "cortex.db"
52
+ if cortex_db.exists():
53
+ found.append(cortex_db)
54
+ except PermissionError:
55
+ pass
56
+ return found
57
+
58
+
59
+ def scan_projects() -> list[ProjectInfo]:
60
+ """
61
+ Scan for all omni-cortex databases.
62
+
63
+ Returns list of ProjectInfo with name, path, db_path, last_modified, memory_count.
64
+ """
65
+ projects: list[ProjectInfo] = []
66
+ seen_paths: set[str] = set()
67
+
68
+ # Load user config
69
+ config = load_config()
70
+
71
+ # 1. Add global index if exists
72
+ global_path = get_global_db_path()
73
+ if global_path.exists():
74
+ stat = global_path.stat()
75
+ global_project_path = str(global_path.parent)
76
+ projects.append(
77
+ ProjectInfo(
78
+ name="Global Index",
79
+ path=global_project_path,
80
+ db_path=str(global_path),
81
+ last_modified=datetime.fromtimestamp(stat.st_mtime),
82
+ memory_count=get_memory_count(global_path),
83
+ is_global=True,
84
+ is_favorite=global_project_path in config.favorites,
85
+ )
86
+ )
87
+ seen_paths.add(str(global_path))
88
+
89
+ # 2. Use CONFIGURABLE scan directories
90
+ for scan_dir in config.scan_directories:
91
+ scan_path = Path(scan_dir).expanduser()
92
+ if scan_path.exists():
93
+ for db_path in scan_directory_for_cortex(scan_path):
94
+ if str(db_path) not in seen_paths:
95
+ project_dir = db_path.parent.parent
96
+ stat = db_path.stat()
97
+ project_path = str(project_dir)
98
+ projects.append(
99
+ ProjectInfo(
100
+ name=project_dir.name,
101
+ path=project_path,
102
+ db_path=str(db_path),
103
+ last_modified=datetime.fromtimestamp(stat.st_mtime),
104
+ memory_count=get_memory_count(db_path),
105
+ is_global=False,
106
+ is_favorite=project_path in config.favorites,
107
+ )
108
+ )
109
+ seen_paths.add(str(db_path))
110
+
111
+ # 3. Add REGISTERED projects (manual additions)
112
+ for reg in config.registered_projects:
113
+ db_path = Path(reg.path) / ".omni-cortex" / "cortex.db"
114
+ if db_path.exists() and str(db_path) not in seen_paths:
115
+ stat = db_path.stat()
116
+ projects.append(
117
+ ProjectInfo(
118
+ name=Path(reg.path).name,
119
+ path=reg.path,
120
+ db_path=str(db_path),
121
+ last_modified=datetime.fromtimestamp(stat.st_mtime),
122
+ memory_count=get_memory_count(db_path),
123
+ is_global=False,
124
+ is_favorite=reg.path in config.favorites,
125
+ is_registered=True,
126
+ display_name=reg.display_name,
127
+ )
128
+ )
129
+ seen_paths.add(str(db_path))
130
+
131
+ # 4. Add paths from global db that we haven't seen
132
+ for project_path in get_projects_from_global_db():
133
+ db_path = Path(project_path) / ".omni-cortex" / "cortex.db"
134
+ if db_path.exists() and str(db_path) not in seen_paths:
135
+ stat = db_path.stat()
136
+ projects.append(
137
+ ProjectInfo(
138
+ name=Path(project_path).name,
139
+ path=project_path,
140
+ db_path=str(db_path),
141
+ last_modified=datetime.fromtimestamp(stat.st_mtime),
142
+ memory_count=get_memory_count(db_path),
143
+ is_global=False,
144
+ is_favorite=project_path in config.favorites,
145
+ )
146
+ )
147
+ seen_paths.add(str(db_path))
148
+
149
+ # Sort: favorites first, then by last_modified (most recent first), with global always first
150
+ projects.sort(
151
+ key=lambda p: (
152
+ not p.is_global,
153
+ not p.is_favorite,
154
+ -(p.last_modified.timestamp() if p.last_modified else 0),
155
+ )
156
+ )
157
+
158
+ return projects
159
+
160
+
161
+ if __name__ == "__main__":
162
+ # Test the scanner
163
+ for project in scan_projects():
164
+ print(f"{project.name}: {project.db_path} ({project.memory_count} memories)")
@@ -0,0 +1,111 @@
1
+ """Prompt injection protection for Omni-Cortex."""
2
+
3
+ import re
4
+ import logging
5
+ from html import escape as html_escape
6
+ from typing import Optional
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ def xml_escape(text: str) -> str:
12
+ """Escape text for safe inclusion in XML-structured prompts.
13
+
14
+ Converts special characters to prevent prompt injection via
15
+ XML/HTML-like delimiters.
16
+ """
17
+ return html_escape(text, quote=True)
18
+
19
+
20
+ def build_safe_prompt(
21
+ system_instruction: str,
22
+ user_data: dict[str, str],
23
+ user_question: str
24
+ ) -> str:
25
+ """Build a prompt with clear instruction/data separation.
26
+
27
+ Uses XML tags to separate trusted instructions from untrusted data,
28
+ making it harder for injected content to be interpreted as instructions.
29
+
30
+ Args:
31
+ system_instruction: Trusted system prompt (not escaped)
32
+ user_data: Dict of data sections to include (escaped)
33
+ user_question: User's question (escaped)
34
+
35
+ Returns:
36
+ Safely structured prompt string
37
+ """
38
+ parts = [system_instruction, ""]
39
+
40
+ # Add data sections with XML escaping
41
+ for section_name, content in user_data.items():
42
+ if content:
43
+ parts.append(f"<{section_name}>")
44
+ parts.append(xml_escape(content))
45
+ parts.append(f"</{section_name}>")
46
+ parts.append("")
47
+
48
+ # Add user question
49
+ parts.append("<user_question>")
50
+ parts.append(xml_escape(user_question))
51
+ parts.append("</user_question>")
52
+
53
+ return "\n".join(parts)
54
+
55
+
56
+ # Known prompt injection patterns
57
+ INJECTION_PATTERNS = [
58
+ (r'(?i)(ignore|disregard|forget)\s+(all\s+)?(previous|prior|above)\s+instructions?',
59
+ 'instruction override attempt'),
60
+ (r'(?i)(new\s+)?system\s+(prompt|instruction|message)',
61
+ 'system prompt manipulation'),
62
+ (r'(?i)you\s+(must|should|will|are\s+required\s+to)\s+now',
63
+ 'imperative command injection'),
64
+ (r'(?i)(hidden|secret|special)\s+instruction',
65
+ 'hidden instruction claim'),
66
+ (r'(?i)\[/?system\]|\[/?inst\]|<\/?system>|<\/?instruction>',
67
+ 'fake delimiter injection'),
68
+ (r'(?i)bypass|jailbreak|DAN|GODMODE',
69
+ 'known jailbreak signature'),
70
+ ]
71
+
72
+
73
+ def detect_injection_patterns(content: str) -> list[str]:
74
+ """Detect potential prompt injection patterns in content.
75
+
76
+ Returns list of detected patterns (empty if clean).
77
+ """
78
+ detected = []
79
+ for pattern, description in INJECTION_PATTERNS:
80
+ if re.search(pattern, content):
81
+ detected.append(description)
82
+
83
+ return detected
84
+
85
+
86
+ def sanitize_memory_content(content: str, warn_on_detection: bool = True) -> tuple[str, list[str]]:
87
+ """Sanitize memory content and detect injection attempts.
88
+
89
+ Args:
90
+ content: Raw memory content
91
+ warn_on_detection: If True, log warnings for detected patterns
92
+
93
+ Returns:
94
+ Tuple of (sanitized_content, list_of_detected_patterns)
95
+ """
96
+ detected = detect_injection_patterns(content)
97
+
98
+ if detected and warn_on_detection:
99
+ logger.warning(f"Potential injection patterns detected: {detected}")
100
+
101
+ # Content is still returned - we sanitize via XML escaping when used in prompts
102
+ return content, detected
103
+
104
+
105
+ def sanitize_context_data(data: str) -> str:
106
+ """Escape context data for safe inclusion in prompts.
107
+
108
+ This is the primary defense - all user-supplied data should be
109
+ escaped before inclusion in prompts to prevent injection.
110
+ """
111
+ return xml_escape(data)
@@ -0,0 +1,23 @@
1
+ [project]
2
+ name = "omni-cortex-dashboard"
3
+ version = "0.1.0"
4
+ description = "Web dashboard for Omni-Cortex memory system"
5
+ requires-python = ">=3.11"
6
+ dependencies = [
7
+ "fastapi>=0.115.0",
8
+ "uvicorn[standard]>=0.30.0",
9
+ "websockets>=12.0",
10
+ "watchdog>=4.0.0",
11
+ "google-generativeai>=0.8.0",
12
+ "python-dotenv>=1.0.0",
13
+ ]
14
+
15
+ [project.optional-dependencies]
16
+ dev = ["pytest", "ruff", "httpx"]
17
+
18
+ [tool.ruff]
19
+ line-length = 100
20
+ target-version = "py311"
21
+
22
+ [tool.ruff.lint]
23
+ select = ["E", "F", "I", "W"]
@@ -0,0 +1,104 @@
1
+ """Security utilities for Omni-Cortex Dashboard."""
2
+
3
+ import os
4
+ import re
5
+ from pathlib import Path
6
+ from typing import Optional
7
+
8
+
9
+ class PathValidator:
10
+ """Validate and sanitize file paths to prevent traversal attacks."""
11
+
12
+ # Pattern for valid omni-cortex database paths
13
+ VALID_DB_PATTERN = re.compile(r'^.*[/\\]\.omni-cortex[/\\]cortex\.db$')
14
+ GLOBAL_DB_PATTERN = re.compile(r'^.*[/\\]\.omni-cortex[/\\]global\.db$')
15
+
16
+ @staticmethod
17
+ def is_valid_project_db(path: str) -> bool:
18
+ """Check if path is a valid omni-cortex project database."""
19
+ try:
20
+ resolved = Path(path).resolve()
21
+ path_str = str(resolved)
22
+
23
+ # Must match expected patterns
24
+ if PathValidator.VALID_DB_PATTERN.match(path_str):
25
+ return resolved.exists() and resolved.is_file()
26
+ if PathValidator.GLOBAL_DB_PATTERN.match(path_str):
27
+ return resolved.exists() and resolved.is_file()
28
+
29
+ return False
30
+ except (ValueError, OSError):
31
+ return False
32
+
33
+ @staticmethod
34
+ def validate_project_path(path: str) -> Path:
35
+ """Validate and return resolved path, or raise ValueError."""
36
+ if not PathValidator.is_valid_project_db(path):
37
+ raise ValueError(f"Invalid project database path: {path}")
38
+ return Path(path).resolve()
39
+
40
+ @staticmethod
41
+ def is_safe_static_path(base_dir: Path, requested_path: str) -> Optional[Path]:
42
+ """Validate static file path is within base directory.
43
+
44
+ Returns resolved path if safe, None if traversal detected.
45
+ """
46
+ try:
47
+ # Resolve both paths to absolute
48
+ base_resolved = base_dir.resolve()
49
+ requested = (base_dir / requested_path).resolve()
50
+
51
+ # Check if requested path is under base directory
52
+ if base_resolved in requested.parents or requested == base_resolved:
53
+ if requested.exists() and requested.is_file():
54
+ return requested
55
+
56
+ return None
57
+ except (ValueError, OSError):
58
+ return None
59
+
60
+
61
+ def sanitize_log_input(value: str, max_length: int = 200) -> str:
62
+ """Sanitize user input for safe logging.
63
+
64
+ Prevents log injection by:
65
+ - Escaping newlines
66
+ - Limiting length
67
+ - Removing control characters
68
+ """
69
+ if not isinstance(value, str):
70
+ value = str(value)
71
+
72
+ # Remove control characters except spaces
73
+ sanitized = ''.join(c if c.isprintable() or c == ' ' else '?' for c in value)
74
+
75
+ # Escape potential log injection patterns
76
+ sanitized = sanitized.replace('\n', '\\n').replace('\r', '\\r')
77
+
78
+ # Truncate
79
+ if len(sanitized) > max_length:
80
+ sanitized = sanitized[:max_length] + '...'
81
+
82
+ return sanitized
83
+
84
+
85
+ # Environment-based configuration
86
+ IS_PRODUCTION = os.getenv("ENVIRONMENT", "development") == "production"
87
+
88
+
89
+ def get_cors_config():
90
+ """Get CORS configuration based on environment."""
91
+ if IS_PRODUCTION:
92
+ origins = os.getenv("CORS_ORIGINS", "").split(",")
93
+ origins = [o.strip() for o in origins if o.strip()]
94
+ return {
95
+ "allow_origins": origins,
96
+ "allow_methods": ["GET", "POST", "PUT", "DELETE"],
97
+ "allow_headers": ["Content-Type", "Authorization", "X-API-Key"],
98
+ }
99
+ else:
100
+ return {
101
+ "allow_origins": ["http://localhost:5173", "http://127.0.0.1:5173"],
102
+ "allow_methods": ["*"],
103
+ "allow_headers": ["*"],
104
+ }
@@ -0,0 +1,301 @@
1
+ """Unit tests for dashboard database functions.
2
+
3
+ Run with: cd dashboard/backend && .venv/Scripts/python -m pytest test_database.py -v
4
+ """
5
+
6
+ import json
7
+ import os
8
+ import sqlite3
9
+ import tempfile
10
+ from pathlib import Path
11
+
12
+ import pytest
13
+
14
+ from database import create_memory, get_memories
15
+
16
+
17
+ @pytest.fixture
18
+ def test_db():
19
+ """Create a temporary database with the required schema."""
20
+ with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
21
+ db_path = f.name
22
+
23
+ # Create the database schema
24
+ conn = sqlite3.connect(db_path)
25
+ conn.execute("""
26
+ CREATE TABLE memories (
27
+ id TEXT PRIMARY KEY,
28
+ content TEXT NOT NULL,
29
+ context TEXT,
30
+ type TEXT DEFAULT 'other',
31
+ status TEXT DEFAULT 'fresh',
32
+ importance_score INTEGER DEFAULT 50,
33
+ access_count INTEGER DEFAULT 0,
34
+ created_at TEXT NOT NULL,
35
+ last_accessed TEXT NOT NULL,
36
+ updated_at TEXT NOT NULL,
37
+ tags TEXT
38
+ )
39
+ """)
40
+ conn.execute("""
41
+ CREATE TABLE memory_relationships (
42
+ source_memory_id TEXT NOT NULL,
43
+ target_memory_id TEXT NOT NULL,
44
+ relationship_type TEXT NOT NULL,
45
+ strength REAL DEFAULT 0.5,
46
+ PRIMARY KEY (source_memory_id, target_memory_id)
47
+ )
48
+ """)
49
+ conn.commit()
50
+ conn.close()
51
+
52
+ yield db_path
53
+
54
+ # Cleanup
55
+ os.unlink(db_path)
56
+
57
+
58
+ class TestCreateMemory:
59
+ """Tests for the create_memory function."""
60
+
61
+ def test_create_memory_basic(self, test_db):
62
+ """Test basic memory creation with minimal fields."""
63
+ memory_id = create_memory(
64
+ db_path=test_db,
65
+ content="Test memory content",
66
+ )
67
+
68
+ assert memory_id is not None
69
+ assert memory_id.startswith("mem_")
70
+
71
+ def test_create_memory_with_type(self, test_db):
72
+ """Test memory creation with custom type."""
73
+ memory_id = create_memory(
74
+ db_path=test_db,
75
+ content="A decision was made",
76
+ memory_type="decision",
77
+ )
78
+
79
+ # Verify the type was saved
80
+ conn = sqlite3.connect(test_db)
81
+ conn.row_factory = sqlite3.Row
82
+ row = conn.execute(
83
+ "SELECT * FROM memories WHERE id = ?", (memory_id,)
84
+ ).fetchone()
85
+ conn.close()
86
+
87
+ assert row["type"] == "decision"
88
+
89
+ def test_create_memory_with_tags(self, test_db):
90
+ """Test memory creation with tags."""
91
+ tags = ["python", "testing", "database"]
92
+ memory_id = create_memory(
93
+ db_path=test_db,
94
+ content="Tagged memory",
95
+ tags=tags,
96
+ )
97
+
98
+ # Verify tags were saved as JSON
99
+ conn = sqlite3.connect(test_db)
100
+ conn.row_factory = sqlite3.Row
101
+ row = conn.execute(
102
+ "SELECT * FROM memories WHERE id = ?", (memory_id,)
103
+ ).fetchone()
104
+ conn.close()
105
+
106
+ saved_tags = json.loads(row["tags"])
107
+ assert saved_tags == tags
108
+
109
+ def test_create_memory_with_importance(self, test_db):
110
+ """Test memory creation with custom importance score."""
111
+ memory_id = create_memory(
112
+ db_path=test_db,
113
+ content="Important memory",
114
+ importance_score=95,
115
+ )
116
+
117
+ conn = sqlite3.connect(test_db)
118
+ conn.row_factory = sqlite3.Row
119
+ row = conn.execute(
120
+ "SELECT * FROM memories WHERE id = ?", (memory_id,)
121
+ ).fetchone()
122
+ conn.close()
123
+
124
+ assert row["importance_score"] == 95
125
+
126
+ def test_create_memory_has_updated_at(self, test_db):
127
+ """Test that created memory has updated_at field (regression test for bug)."""
128
+ memory_id = create_memory(
129
+ db_path=test_db,
130
+ content="Memory with updated_at",
131
+ )
132
+
133
+ conn = sqlite3.connect(test_db)
134
+ conn.row_factory = sqlite3.Row
135
+ row = conn.execute(
136
+ "SELECT * FROM memories WHERE id = ?", (memory_id,)
137
+ ).fetchone()
138
+ conn.close()
139
+
140
+ # This was the bug - updated_at was missing from INSERT
141
+ assert row["updated_at"] is not None
142
+ assert len(row["updated_at"]) > 0
143
+
144
+ def test_create_memory_with_context(self, test_db):
145
+ """Test memory creation with context."""
146
+ memory_id = create_memory(
147
+ db_path=test_db,
148
+ content="Memory with context",
149
+ context="This is additional context",
150
+ )
151
+
152
+ conn = sqlite3.connect(test_db)
153
+ conn.row_factory = sqlite3.Row
154
+ row = conn.execute(
155
+ "SELECT * FROM memories WHERE id = ?", (memory_id,)
156
+ ).fetchone()
157
+ conn.close()
158
+
159
+ assert row["context"] == "This is additional context"
160
+
161
+ def test_create_memory_default_status_is_fresh(self, test_db):
162
+ """Test that new memories have 'fresh' status by default."""
163
+ memory_id = create_memory(
164
+ db_path=test_db,
165
+ content="Fresh memory",
166
+ )
167
+
168
+ conn = sqlite3.connect(test_db)
169
+ conn.row_factory = sqlite3.Row
170
+ row = conn.execute(
171
+ "SELECT * FROM memories WHERE id = ?", (memory_id,)
172
+ ).fetchone()
173
+ conn.close()
174
+
175
+ assert row["status"] == "fresh"
176
+
177
+ def test_create_memory_default_access_count_is_zero(self, test_db):
178
+ """Test that new memories have access_count of 0."""
179
+ memory_id = create_memory(
180
+ db_path=test_db,
181
+ content="New memory",
182
+ )
183
+
184
+ conn = sqlite3.connect(test_db)
185
+ conn.row_factory = sqlite3.Row
186
+ row = conn.execute(
187
+ "SELECT * FROM memories WHERE id = ?", (memory_id,)
188
+ ).fetchone()
189
+ conn.close()
190
+
191
+ assert row["access_count"] == 0
192
+
193
+ def test_create_memory_all_fields(self, test_db):
194
+ """Test memory creation with all fields populated."""
195
+ memory_id = create_memory(
196
+ db_path=test_db,
197
+ content="Complete memory",
198
+ memory_type="solution",
199
+ context="Full context here",
200
+ tags=["tag1", "tag2"],
201
+ importance_score=80,
202
+ )
203
+
204
+ conn = sqlite3.connect(test_db)
205
+ conn.row_factory = sqlite3.Row
206
+ row = conn.execute(
207
+ "SELECT * FROM memories WHERE id = ?", (memory_id,)
208
+ ).fetchone()
209
+ conn.close()
210
+
211
+ assert row["content"] == "Complete memory"
212
+ assert row["type"] == "solution"
213
+ assert row["context"] == "Full context here"
214
+ assert json.loads(row["tags"]) == ["tag1", "tag2"]
215
+ assert row["importance_score"] == 80
216
+ assert row["status"] == "fresh"
217
+ assert row["access_count"] == 0
218
+ assert row["created_at"] is not None
219
+ assert row["last_accessed"] is not None
220
+ assert row["updated_at"] is not None
221
+
222
+
223
+ class TestCreateMemoryEdgeCases:
224
+ """Edge case tests for create_memory."""
225
+
226
+ def test_create_memory_empty_tags_list(self, test_db):
227
+ """Test memory creation with empty tags list.
228
+
229
+ Note: Current behavior stores empty list as None (falsy check in code).
230
+ This is intentional - empty tags are treated same as no tags.
231
+ """
232
+ memory_id = create_memory(
233
+ db_path=test_db,
234
+ content="Memory with empty tags",
235
+ tags=[],
236
+ )
237
+
238
+ conn = sqlite3.connect(test_db)
239
+ conn.row_factory = sqlite3.Row
240
+ row = conn.execute(
241
+ "SELECT * FROM memories WHERE id = ?", (memory_id,)
242
+ ).fetchone()
243
+ conn.close()
244
+
245
+ # Empty list is treated as None (falsy) in the code
246
+ assert row["tags"] is None
247
+
248
+ def test_create_memory_none_tags(self, test_db):
249
+ """Test memory creation with None tags (default)."""
250
+ memory_id = create_memory(
251
+ db_path=test_db,
252
+ content="Memory with no tags",
253
+ tags=None,
254
+ )
255
+
256
+ conn = sqlite3.connect(test_db)
257
+ conn.row_factory = sqlite3.Row
258
+ row = conn.execute(
259
+ "SELECT * FROM memories WHERE id = ?", (memory_id,)
260
+ ).fetchone()
261
+ conn.close()
262
+
263
+ assert row["tags"] is None
264
+
265
+ def test_create_memory_unicode_content(self, test_db):
266
+ """Test memory creation with unicode content."""
267
+ content = "Memory with unicode: 日本語 🚀 émojis"
268
+ memory_id = create_memory(
269
+ db_path=test_db,
270
+ content=content,
271
+ )
272
+
273
+ conn = sqlite3.connect(test_db)
274
+ conn.row_factory = sqlite3.Row
275
+ row = conn.execute(
276
+ "SELECT * FROM memories WHERE id = ?", (memory_id,)
277
+ ).fetchone()
278
+ conn.close()
279
+
280
+ assert row["content"] == content
281
+
282
+ def test_create_memory_long_content(self, test_db):
283
+ """Test memory creation with very long content."""
284
+ content = "A" * 100000 # 100KB of content
285
+ memory_id = create_memory(
286
+ db_path=test_db,
287
+ content=content,
288
+ )
289
+
290
+ conn = sqlite3.connect(test_db)
291
+ conn.row_factory = sqlite3.Row
292
+ row = conn.execute(
293
+ "SELECT * FROM memories WHERE id = ?", (memory_id,)
294
+ ).fetchone()
295
+ conn.close()
296
+
297
+ assert len(row["content"]) == 100000
298
+
299
+
300
+ if __name__ == "__main__":
301
+ pytest.main([__file__, "-v"])