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
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env python3
2
+ """SubagentStop hook - logs when a subagent completes.
3
+
4
+ This hook is called when a subagent (spawned by the Task tool) finishes.
5
+ It logs the subagent completion and any results.
6
+
7
+ Hook configuration for settings.json:
8
+ {
9
+ "hooks": {
10
+ "SubagentStop": [
11
+ {
12
+ "type": "command",
13
+ "command": "python hooks/subagent_stop.py"
14
+ }
15
+ ]
16
+ }
17
+ }
18
+ """
19
+
20
+ import json
21
+ import sys
22
+ import os
23
+ import sqlite3
24
+ from datetime import datetime, timezone
25
+ from pathlib import Path
26
+
27
+
28
+ def get_db_path() -> Path:
29
+ """Get the database path for the current project."""
30
+ project_path = os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd())
31
+ return Path(project_path) / ".omni-cortex" / "cortex.db"
32
+
33
+
34
+ def generate_id() -> str:
35
+ """Generate a unique activity ID."""
36
+ timestamp_ms = int(datetime.now().timestamp() * 1000)
37
+ random_hex = os.urandom(4).hex()
38
+ return f"act_{timestamp_ms}_{random_hex}"
39
+
40
+
41
+ def truncate(text: str, max_length: int = 10000) -> str:
42
+ """Truncate text to max length."""
43
+ if len(text) <= max_length:
44
+ return text
45
+ return text[:max_length - 20] + "\n... [truncated]"
46
+
47
+
48
+ def main():
49
+ """Process SubagentStop hook."""
50
+ try:
51
+ # Read input from stdin
52
+ input_data = json.load(sys.stdin)
53
+
54
+ db_path = get_db_path()
55
+
56
+ # Only log if database exists
57
+ if not db_path.exists():
58
+ print(json.dumps({}))
59
+ return
60
+
61
+ # Extract data from hook input
62
+ subagent_id = input_data.get("subagent_id")
63
+ subagent_type = input_data.get("subagent_type", "subagent")
64
+ result = input_data.get("result", {})
65
+
66
+ session_id = os.environ.get("CLAUDE_SESSION_ID")
67
+ project_path = os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd())
68
+ now = datetime.now(timezone.utc).isoformat()
69
+
70
+ # Connect to database
71
+ conn = sqlite3.connect(str(db_path))
72
+ cursor = conn.cursor()
73
+
74
+ # Log the subagent completion as an activity
75
+ cursor.execute(
76
+ """
77
+ INSERT INTO activities (
78
+ id, session_id, agent_id, timestamp, event_type,
79
+ tool_name, tool_output, success, project_path
80
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
81
+ """,
82
+ (
83
+ generate_id(),
84
+ session_id,
85
+ subagent_id,
86
+ now,
87
+ "subagent_stop",
88
+ f"subagent_{subagent_type}",
89
+ truncate(json.dumps(result, default=str)),
90
+ 1,
91
+ project_path,
92
+ ),
93
+ )
94
+
95
+ # Update or create agent record
96
+ cursor.execute(
97
+ """
98
+ INSERT INTO agents (id, name, type, first_seen, last_seen, total_activities)
99
+ VALUES (?, ?, ?, ?, ?, 1)
100
+ ON CONFLICT(id) DO UPDATE SET
101
+ last_seen = ?,
102
+ total_activities = total_activities + 1
103
+ """,
104
+ (subagent_id, None, "subagent", now, now, now),
105
+ )
106
+
107
+ conn.commit()
108
+ conn.close()
109
+
110
+ print(json.dumps({}))
111
+
112
+ except Exception as e:
113
+ # Hooks should never block
114
+ print(json.dumps({"systemMessage": f"Cortex subagent_stop: {e}"}))
115
+
116
+ sys.exit(0)
117
+
118
+
119
+ if __name__ == "__main__":
120
+ main()
@@ -0,0 +1,220 @@
1
+ #!/usr/bin/env python3
2
+ """UserPromptSubmit hook - captures user messages for style analysis.
3
+
4
+ This hook is called by Claude Code when the user submits a prompt.
5
+ It logs the user message to the Cortex database for later style analysis.
6
+
7
+ Hook configuration for settings.json:
8
+ {
9
+ "hooks": {
10
+ "UserPromptSubmit": [
11
+ {
12
+ "hooks": [
13
+ {
14
+ "type": "command",
15
+ "command": "python hooks/user_prompt.py"
16
+ }
17
+ ]
18
+ }
19
+ ]
20
+ }
21
+ }
22
+ """
23
+
24
+ import json
25
+ import re
26
+ import sys
27
+ import os
28
+ import sqlite3
29
+ from datetime import datetime, timezone
30
+ from pathlib import Path
31
+
32
+ # Import shared session management
33
+ from session_utils import get_or_create_session
34
+
35
+
36
+ def get_db_path() -> Path:
37
+ """Get the database path for the current project."""
38
+ project_path = os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd())
39
+ return Path(project_path) / ".omni-cortex" / "cortex.db"
40
+
41
+
42
+ def ensure_database(db_path: Path) -> sqlite3.Connection:
43
+ """Ensure database exists and has user_messages table."""
44
+ db_path.parent.mkdir(parents=True, exist_ok=True)
45
+ conn = sqlite3.connect(str(db_path))
46
+
47
+ # Check if user_messages table exists
48
+ cursor = conn.cursor()
49
+ cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='user_messages'")
50
+ if cursor.fetchone() is None:
51
+ # Apply minimal schema for user_messages
52
+ conn.executescript("""
53
+ CREATE TABLE IF NOT EXISTS user_messages (
54
+ id TEXT PRIMARY KEY,
55
+ session_id TEXT,
56
+ timestamp TEXT NOT NULL,
57
+ content TEXT NOT NULL,
58
+ word_count INTEGER,
59
+ char_count INTEGER,
60
+ line_count INTEGER,
61
+ has_code_blocks INTEGER DEFAULT 0,
62
+ has_questions INTEGER DEFAULT 0,
63
+ has_commands INTEGER DEFAULT 0,
64
+ tone_indicators TEXT,
65
+ project_path TEXT,
66
+ metadata TEXT
67
+ );
68
+ CREATE INDEX IF NOT EXISTS idx_user_messages_timestamp ON user_messages(timestamp DESC);
69
+ """)
70
+ conn.commit()
71
+
72
+ return conn
73
+
74
+
75
+ def generate_id() -> str:
76
+ """Generate a unique message ID."""
77
+ timestamp_ms = int(datetime.now().timestamp() * 1000)
78
+ random_hex = os.urandom(4).hex()
79
+ return f"msg_{timestamp_ms}_{random_hex}"
80
+
81
+
82
+ def analyze_message(content: str) -> dict:
83
+ """Analyze message characteristics for style profiling."""
84
+ # Basic counts
85
+ word_count = len(content.split())
86
+ char_count = len(content)
87
+ line_count = len(content.splitlines()) or 1
88
+
89
+ # Detect code blocks
90
+ has_code_blocks = 1 if re.search(r'```[\s\S]*?```|`[^`]+`', content) else 0
91
+
92
+ # Detect questions
93
+ has_questions = 1 if re.search(r'\?|^(what|how|why|when|where|who|which|can|could|would|should|is|are|do|does|did)\b', content, re.IGNORECASE | re.MULTILINE) else 0
94
+
95
+ # Detect slash commands
96
+ has_commands = 1 if content.strip().startswith('/') else 0
97
+
98
+ # Tone indicators
99
+ tone_indicators = []
100
+
101
+ # Urgency markers
102
+ if re.search(r'\b(urgent|asap|immediately|quick|fast|hurry)\b', content, re.IGNORECASE):
103
+ tone_indicators.append("urgent")
104
+
105
+ # Polite markers
106
+ if re.search(r'\b(please|thanks|thank you|appreciate|kindly)\b', content, re.IGNORECASE):
107
+ tone_indicators.append("polite")
108
+
109
+ # Direct/imperative
110
+ if re.match(r'^(fix|add|remove|update|change|create|delete|run|test|check|show|list|find)\b', content.strip(), re.IGNORECASE):
111
+ tone_indicators.append("direct")
112
+
113
+ # Questioning/exploratory
114
+ if has_questions:
115
+ tone_indicators.append("inquisitive")
116
+
117
+ # Technical
118
+ if re.search(r'\b(function|class|method|variable|api|database|server|error|bug|issue)\b', content, re.IGNORECASE):
119
+ tone_indicators.append("technical")
120
+
121
+ # Casual
122
+ if re.search(r'\b(hey|hi|yo|cool|awesome|great|nice)\b', content, re.IGNORECASE):
123
+ tone_indicators.append("casual")
124
+
125
+ return {
126
+ "word_count": word_count,
127
+ "char_count": char_count,
128
+ "line_count": line_count,
129
+ "has_code_blocks": has_code_blocks,
130
+ "has_questions": has_questions,
131
+ "has_commands": has_commands,
132
+ "tone_indicators": json.dumps(tone_indicators),
133
+ }
134
+
135
+
136
+ def main():
137
+ """Process UserPromptSubmit hook."""
138
+ try:
139
+ # Read input from stdin
140
+ import select
141
+ if sys.platform != "win32":
142
+ ready, _, _ = select.select([sys.stdin], [], [], 5.0)
143
+ if not ready:
144
+ print(json.dumps({}))
145
+ return
146
+
147
+ raw_input = sys.stdin.read()
148
+ if not raw_input or not raw_input.strip():
149
+ print(json.dumps({}))
150
+ return
151
+
152
+ input_data = json.loads(raw_input)
153
+
154
+ # Extract user prompt
155
+ prompt = input_data.get("prompt", "")
156
+ if not prompt or not prompt.strip():
157
+ print(json.dumps({}))
158
+ return
159
+
160
+ # Skip very short messages (likely just commands)
161
+ if len(prompt.strip()) < 3:
162
+ print(json.dumps({}))
163
+ return
164
+
165
+ project_path = os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd())
166
+
167
+ # Initialize database
168
+ db_path = get_db_path()
169
+ conn = ensure_database(db_path)
170
+
171
+ # Get or create session
172
+ session_id = get_or_create_session(conn, project_path)
173
+
174
+ # Analyze message
175
+ analysis = analyze_message(prompt)
176
+
177
+ # Generate message ID
178
+ message_id = generate_id()
179
+
180
+ # Insert message record
181
+ cursor = conn.cursor()
182
+ cursor.execute(
183
+ """
184
+ INSERT INTO user_messages (
185
+ id, session_id, timestamp, content, word_count, char_count,
186
+ line_count, has_code_blocks, has_questions, has_commands,
187
+ tone_indicators, project_path
188
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
189
+ """,
190
+ (
191
+ message_id,
192
+ session_id,
193
+ datetime.now(timezone.utc).isoformat(),
194
+ prompt,
195
+ analysis["word_count"],
196
+ analysis["char_count"],
197
+ analysis["line_count"],
198
+ analysis["has_code_blocks"],
199
+ analysis["has_questions"],
200
+ analysis["has_commands"],
201
+ analysis["tone_indicators"],
202
+ project_path,
203
+ ),
204
+ )
205
+ conn.commit()
206
+ conn.close()
207
+
208
+ # Return empty response (don't modify prompt)
209
+ print(json.dumps({}))
210
+
211
+ except Exception as e:
212
+ # Hooks should never block - log error but continue
213
+ # Don't print system message to avoid polluting user experience
214
+ print(json.dumps({}))
215
+
216
+ sys.exit(0)
217
+
218
+
219
+ if __name__ == "__main__":
220
+ main()
@@ -0,0 +1,9 @@
1
+ """Auto-categorization for memories."""
2
+
3
+ from .auto_type import detect_memory_type
4
+ from .auto_tags import suggest_tags
5
+
6
+ __all__ = [
7
+ "detect_memory_type",
8
+ "suggest_tags",
9
+ ]
@@ -0,0 +1,166 @@
1
+ """Auto-suggest tags based on content."""
2
+
3
+ import re
4
+ from typing import Optional
5
+
6
+ # Tag patterns organized by category
7
+ TAG_PATTERNS: dict[str, list[tuple[str, str]]] = {
8
+ # Programming languages
9
+ "languages": [
10
+ (r"\b(python|\.py)\b", "python"),
11
+ (r"\b(javascript|\.js|\.jsx)\b", "javascript"),
12
+ (r"\b(typescript|\.ts|\.tsx)\b", "typescript"),
13
+ (r"\b(rust|\.rs|cargo)\b", "rust"),
14
+ (r"\b(go|golang|\.go)\b", "go"),
15
+ (r"\b(java|\.java)\b", "java"),
16
+ (r"\b(c\+\+|cpp|\.cpp|\.hpp)\b", "cpp"),
17
+ (r"\b(c#|csharp|\.cs)\b", "csharp"),
18
+ (r"\b(ruby|\.rb)\b", "ruby"),
19
+ (r"\b(php|\.php)\b", "php"),
20
+ (r"\b(swift|\.swift)\b", "swift"),
21
+ (r"\b(kotlin|\.kt)\b", "kotlin"),
22
+ (r"\b(sql|mysql|postgres|sqlite)\b", "sql"),
23
+ (r"\b(html|\.html)\b", "html"),
24
+ (r"\b(css|\.css|scss|sass)\b", "css"),
25
+ (r"\b(shell|bash|\.sh|zsh)\b", "shell"),
26
+ ],
27
+ # Frameworks and libraries
28
+ "frameworks": [
29
+ (r"\b(react|reactjs|jsx)\b", "react"),
30
+ (r"\b(vue|vuejs)\b", "vue"),
31
+ (r"\b(angular)\b", "angular"),
32
+ (r"\b(svelte)\b", "svelte"),
33
+ (r"\b(nextjs|next\.js)\b", "nextjs"),
34
+ (r"\b(express|expressjs)\b", "express"),
35
+ (r"\b(fastapi)\b", "fastapi"),
36
+ (r"\b(django)\b", "django"),
37
+ (r"\b(flask)\b", "flask"),
38
+ (r"\b(spring)\b", "spring"),
39
+ (r"\b(rails|ruby on rails)\b", "rails"),
40
+ (r"\b(laravel)\b", "laravel"),
41
+ (r"\b(tailwind|tailwindcss)\b", "tailwind"),
42
+ (r"\b(bootstrap)\b", "bootstrap"),
43
+ ],
44
+ # Tools and platforms
45
+ "tools": [
46
+ (r"\b(git|github|gitlab)\b", "git"),
47
+ (r"\b(docker|dockerfile|container)\b", "docker"),
48
+ (r"\b(kubernetes|k8s|kubectl)\b", "kubernetes"),
49
+ (r"\b(aws|amazon web services|s3|ec2|lambda)\b", "aws"),
50
+ (r"\b(gcp|google cloud)\b", "gcp"),
51
+ (r"\b(azure|microsoft azure)\b", "azure"),
52
+ (r"\b(terraform)\b", "terraform"),
53
+ (r"\b(jenkins|ci\/cd|github actions)\b", "ci-cd"),
54
+ (r"\b(npm|yarn|pnpm)\b", "npm"),
55
+ (r"\b(pip|poetry|pipenv)\b", "pip"),
56
+ (r"\b(vscode|visual studio code)\b", "vscode"),
57
+ (r"\b(vim|neovim)\b", "vim"),
58
+ ],
59
+ # Concepts
60
+ "concepts": [
61
+ (r"\b(api|rest|graphql|endpoint)\b", "api"),
62
+ (r"\b(database|db|query|schema)\b", "database"),
63
+ (r"\b(auth|authentication|authorization|oauth|jwt)\b", "auth"),
64
+ (r"\b(testing|test|unittest|pytest|jest)\b", "testing"),
65
+ (r"\b(security|vulnerability|xss|csrf|injection)\b", "security"),
66
+ (r"\b(performance|optimization|cache|speed)\b", "performance"),
67
+ (r"\b(deploy|deployment|release)\b", "deployment"),
68
+ (r"\b(debug|debugging|breakpoint)\b", "debugging"),
69
+ (r"\b(error handling|exception|try catch)\b", "error-handling"),
70
+ (r"\b(async|await|promise|concurrent)\b", "async"),
71
+ (r"\b(regex|regular expression)\b", "regex"),
72
+ (r"\b(json|yaml|xml|toml)\b", "config-format"),
73
+ ],
74
+ # Project-specific
75
+ "project": [
76
+ (r"\b(frontend|front-end|ui)\b", "frontend"),
77
+ (r"\b(backend|back-end|server)\b", "backend"),
78
+ (r"\b(fullstack|full-stack)\b", "fullstack"),
79
+ (r"\b(cli|command line|terminal)\b", "cli"),
80
+ (r"\b(mobile|ios|android|react native)\b", "mobile"),
81
+ (r"\b(web|website|webapp)\b", "web"),
82
+ ],
83
+ }
84
+
85
+ # Compile patterns
86
+ _compiled_patterns: list[tuple[re.Pattern, str]] = []
87
+
88
+
89
+ def _ensure_compiled() -> None:
90
+ """Ensure patterns are compiled."""
91
+ global _compiled_patterns
92
+ if not _compiled_patterns:
93
+ for category_patterns in TAG_PATTERNS.values():
94
+ for pattern, tag in category_patterns:
95
+ _compiled_patterns.append((
96
+ re.compile(pattern, re.IGNORECASE),
97
+ tag
98
+ ))
99
+
100
+
101
+ def suggest_tags(
102
+ content: str,
103
+ context: Optional[str] = None,
104
+ max_tags: int = 5
105
+ ) -> list[str]:
106
+ """Suggest tags based on content analysis.
107
+
108
+ Args:
109
+ content: The memory content
110
+ context: Optional context string
111
+ max_tags: Maximum number of tags to suggest
112
+
113
+ Returns:
114
+ List of suggested tag strings
115
+ """
116
+ _ensure_compiled()
117
+
118
+ if not content:
119
+ return []
120
+
121
+ text = content.lower()
122
+ if context:
123
+ text = f"{text}\n{context.lower()}"
124
+
125
+ # Track tag occurrence counts
126
+ tag_counts: dict[str, int] = {}
127
+
128
+ for pattern, tag in _compiled_patterns:
129
+ matches = pattern.findall(text)
130
+ if matches:
131
+ tag_counts[tag] = tag_counts.get(tag, 0) + len(matches)
132
+
133
+ # Sort by count and return top tags
134
+ sorted_tags = sorted(tag_counts.items(), key=lambda x: x[1], reverse=True)
135
+ return [tag for tag, _ in sorted_tags[:max_tags]]
136
+
137
+
138
+ def merge_tags(
139
+ existing: list[str],
140
+ suggested: list[str],
141
+ user_provided: Optional[list[str]] = None
142
+ ) -> list[str]:
143
+ """Merge tag lists, removing duplicates.
144
+
145
+ Args:
146
+ existing: Existing tags on the memory
147
+ suggested: Auto-suggested tags
148
+ user_provided: Tags explicitly provided by user
149
+
150
+ Returns:
151
+ Merged list of unique tags
152
+ """
153
+ # Start with user-provided tags (highest priority)
154
+ result = list(user_provided or [])
155
+
156
+ # Add existing tags
157
+ for tag in existing:
158
+ if tag not in result:
159
+ result.append(tag)
160
+
161
+ # Add suggested tags
162
+ for tag in suggested:
163
+ if tag not in result:
164
+ result.append(tag)
165
+
166
+ return result
@@ -0,0 +1,165 @@
1
+ """Auto-detect memory type based on content."""
2
+
3
+ import re
4
+ from typing import Optional
5
+
6
+ # Memory types
7
+ MEMORY_TYPES = [
8
+ "warning",
9
+ "tip",
10
+ "config",
11
+ "troubleshooting",
12
+ "code",
13
+ "error",
14
+ "solution",
15
+ "command",
16
+ "concept",
17
+ "decision",
18
+ "general",
19
+ ]
20
+
21
+ # Pattern definitions for each type (case-insensitive)
22
+ TYPE_PATTERNS: dict[str, list[str]] = {
23
+ "warning": [
24
+ r"\b(warning|caution|don't|dont|avoid|never|careful|danger|risk)\b",
25
+ r"\b(do not|should not|shouldn't|mustn't|must not)\b",
26
+ r"\b(beware|watch out|important note)\b",
27
+ ],
28
+ "tip": [
29
+ r"\b(tip|trick|best practice|recommend|suggestion|pro tip)\b",
30
+ r"\b(you can|try|consider|it's better|better to)\b",
31
+ r"\b(shortcut|hack|optimization|improve)\b",
32
+ ],
33
+ "config": [
34
+ r"\b(config|configuration|setting|setup|environment|env)\b",
35
+ r"\b(\.env|\.yaml|\.json|\.toml|\.ini)\b",
36
+ r"\b(variable|parameter|option|flag)\b",
37
+ r"(API_KEY|DATABASE_URL|SECRET|TOKEN)",
38
+ ],
39
+ "troubleshooting": [
40
+ r"\b(fix|solve|debug|troubleshoot|resolve|workaround)\b",
41
+ r"\b(issue|problem|bug|broken|not working)\b",
42
+ r"\b(symptoms?|cause|root cause)\b",
43
+ ],
44
+ "code": [
45
+ r"```[\w]*\n", # Code block
46
+ r"\b(function|def|class|const|let|var|import|export)\s+\w+",
47
+ r"\b(async|await|return|yield)\b",
48
+ r"^\s*(public|private|protected)\s+",
49
+ ],
50
+ "error": [
51
+ r"\b(error|exception|failed|failure|crash)\b",
52
+ r"\b(traceback|stack trace|line \d+)\b",
53
+ r"\b(TypeError|ValueError|ImportError|SyntaxError)\b",
54
+ r"\b(500|404|403|401)\s+(error|status)\b",
55
+ ],
56
+ "solution": [
57
+ r"\b(solution|solved|fixed|resolved|works?)\b",
58
+ r"\b(answer|resolution|the fix|working now)\b",
59
+ r"\b(here's how|the way to|correct approach)\b",
60
+ ],
61
+ "command": [
62
+ r"^\s*[$>]\s+\S+", # Shell prompt
63
+ r"\b(npm|pip|git|docker|kubectl|yarn|pnpm)\s+\w+",
64
+ r"\b(run|install|build|start|test|deploy)\s+",
65
+ r"^(curl|wget|ssh|scp)\s+",
66
+ ],
67
+ "concept": [
68
+ r"\b(is|are|means|defined as|refers to)\b",
69
+ r"\b(concept|definition|explanation|understanding)\b",
70
+ r"\b(basically|essentially|in other words)\b",
71
+ ],
72
+ "decision": [
73
+ r"\b(decided|decision|approach|choice|chose|choosing)\b",
74
+ r"\b(we will|going to|plan to|opted for)\b",
75
+ r"\b(strategy|architecture|design|pattern)\b",
76
+ ],
77
+ }
78
+
79
+ # Compiled patterns (case-insensitive)
80
+ _compiled_patterns: dict[str, list[re.Pattern]] = {}
81
+
82
+
83
+ def _get_patterns(mem_type: str) -> list[re.Pattern]:
84
+ """Get compiled patterns for a memory type."""
85
+ if mem_type not in _compiled_patterns:
86
+ patterns = TYPE_PATTERNS.get(mem_type, [])
87
+ _compiled_patterns[mem_type] = [
88
+ re.compile(p, re.IGNORECASE | re.MULTILINE)
89
+ for p in patterns
90
+ ]
91
+ return _compiled_patterns[mem_type]
92
+
93
+
94
+ def detect_memory_type(content: str, context: Optional[str] = None) -> str:
95
+ """Detect the most likely memory type from content.
96
+
97
+ Args:
98
+ content: The memory content
99
+ context: Optional context string
100
+
101
+ Returns:
102
+ Memory type string
103
+ """
104
+ if not content:
105
+ return "general"
106
+
107
+ # Combine content and context for analysis
108
+ text = content
109
+ if context:
110
+ text = f"{content}\n{context}"
111
+
112
+ # Track match scores
113
+ scores: dict[str, int] = {t: 0 for t in MEMORY_TYPES if t != "general"}
114
+
115
+ # Check each type's patterns
116
+ for mem_type, patterns in TYPE_PATTERNS.items():
117
+ for pattern in _get_patterns(mem_type):
118
+ matches = pattern.findall(text)
119
+ if matches:
120
+ scores[mem_type] += len(matches)
121
+
122
+ # Find the type with highest score
123
+ if scores:
124
+ best_type = max(scores.items(), key=lambda x: x[1])
125
+ if best_type[1] > 0:
126
+ return best_type[0]
127
+
128
+ return "general"
129
+
130
+
131
+ def get_type_confidence(content: str, context: Optional[str] = None) -> dict[str, float]:
132
+ """Get confidence scores for each memory type.
133
+
134
+ Args:
135
+ content: The memory content
136
+ context: Optional context string
137
+
138
+ Returns:
139
+ Dictionary of type -> confidence (0.0 to 1.0)
140
+ """
141
+ if not content:
142
+ return {"general": 1.0}
143
+
144
+ text = content
145
+ if context:
146
+ text = f"{content}\n{context}"
147
+
148
+ # Track raw scores
149
+ scores: dict[str, int] = {t: 0 for t in MEMORY_TYPES if t != "general"}
150
+
151
+ for mem_type, patterns in TYPE_PATTERNS.items():
152
+ for pattern in _get_patterns(mem_type):
153
+ matches = pattern.findall(text)
154
+ scores[mem_type] += len(matches)
155
+
156
+ # Normalize to confidence
157
+ total = sum(scores.values())
158
+ if total == 0:
159
+ return {"general": 1.0}
160
+
161
+ confidences = {t: s / total for t, s in scores.items() if s > 0}
162
+ if not confidences:
163
+ return {"general": 1.0}
164
+
165
+ return confidences