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.
- omni_cortex/__init__.py +3 -0
- omni_cortex/_bundled/dashboard/backend/.env.example +12 -0
- omni_cortex/_bundled/dashboard/backend/backfill_summaries.py +280 -0
- omni_cortex/_bundled/dashboard/backend/chat_service.py +631 -0
- omni_cortex/_bundled/dashboard/backend/database.py +1773 -0
- omni_cortex/_bundled/dashboard/backend/image_service.py +552 -0
- omni_cortex/_bundled/dashboard/backend/logging_config.py +122 -0
- omni_cortex/_bundled/dashboard/backend/main.py +1888 -0
- omni_cortex/_bundled/dashboard/backend/models.py +472 -0
- omni_cortex/_bundled/dashboard/backend/project_config.py +170 -0
- omni_cortex/_bundled/dashboard/backend/project_scanner.py +164 -0
- omni_cortex/_bundled/dashboard/backend/prompt_security.py +111 -0
- omni_cortex/_bundled/dashboard/backend/pyproject.toml +23 -0
- omni_cortex/_bundled/dashboard/backend/security.py +104 -0
- omni_cortex/_bundled/dashboard/backend/uv.lock +1110 -0
- omni_cortex/_bundled/dashboard/backend/websocket_manager.py +104 -0
- omni_cortex/_bundled/hooks/post_tool_use.py +497 -0
- omni_cortex/_bundled/hooks/pre_tool_use.py +277 -0
- omni_cortex/_bundled/hooks/session_utils.py +186 -0
- omni_cortex/_bundled/hooks/stop.py +219 -0
- omni_cortex/_bundled/hooks/subagent_stop.py +120 -0
- omni_cortex/_bundled/hooks/user_prompt.py +220 -0
- omni_cortex/categorization/__init__.py +9 -0
- omni_cortex/categorization/auto_tags.py +166 -0
- omni_cortex/categorization/auto_type.py +165 -0
- omni_cortex/config.py +141 -0
- omni_cortex/dashboard.py +238 -0
- omni_cortex/database/__init__.py +24 -0
- omni_cortex/database/connection.py +137 -0
- omni_cortex/database/migrations.py +210 -0
- omni_cortex/database/schema.py +212 -0
- omni_cortex/database/sync.py +421 -0
- omni_cortex/decay/__init__.py +7 -0
- omni_cortex/decay/importance.py +147 -0
- omni_cortex/embeddings/__init__.py +35 -0
- omni_cortex/embeddings/local.py +442 -0
- omni_cortex/models/__init__.py +20 -0
- omni_cortex/models/activity.py +265 -0
- omni_cortex/models/agent.py +144 -0
- omni_cortex/models/memory.py +395 -0
- omni_cortex/models/relationship.py +206 -0
- omni_cortex/models/session.py +290 -0
- omni_cortex/resources/__init__.py +1 -0
- omni_cortex/search/__init__.py +22 -0
- omni_cortex/search/hybrid.py +197 -0
- omni_cortex/search/keyword.py +204 -0
- omni_cortex/search/ranking.py +127 -0
- omni_cortex/search/semantic.py +232 -0
- omni_cortex/server.py +360 -0
- omni_cortex/setup.py +284 -0
- omni_cortex/tools/__init__.py +13 -0
- omni_cortex/tools/activities.py +453 -0
- omni_cortex/tools/memories.py +536 -0
- omni_cortex/tools/sessions.py +311 -0
- omni_cortex/tools/utilities.py +477 -0
- omni_cortex/utils/__init__.py +13 -0
- omni_cortex/utils/formatting.py +282 -0
- omni_cortex/utils/ids.py +72 -0
- omni_cortex/utils/timestamps.py +129 -0
- omni_cortex/utils/truncation.py +111 -0
- {omni_cortex-1.17.1.dist-info → omni_cortex-1.17.3.dist-info}/METADATA +1 -1
- omni_cortex-1.17.3.dist-info/RECORD +86 -0
- omni_cortex-1.17.1.dist-info/RECORD +0 -26
- {omni_cortex-1.17.1.data → omni_cortex-1.17.3.data}/data/share/omni-cortex/dashboard/backend/.env.example +0 -0
- {omni_cortex-1.17.1.data → omni_cortex-1.17.3.data}/data/share/omni-cortex/dashboard/backend/backfill_summaries.py +0 -0
- {omni_cortex-1.17.1.data → omni_cortex-1.17.3.data}/data/share/omni-cortex/dashboard/backend/chat_service.py +0 -0
- {omni_cortex-1.17.1.data → omni_cortex-1.17.3.data}/data/share/omni-cortex/dashboard/backend/database.py +0 -0
- {omni_cortex-1.17.1.data → omni_cortex-1.17.3.data}/data/share/omni-cortex/dashboard/backend/image_service.py +0 -0
- {omni_cortex-1.17.1.data → omni_cortex-1.17.3.data}/data/share/omni-cortex/dashboard/backend/logging_config.py +0 -0
- {omni_cortex-1.17.1.data → omni_cortex-1.17.3.data}/data/share/omni-cortex/dashboard/backend/main.py +0 -0
- {omni_cortex-1.17.1.data → omni_cortex-1.17.3.data}/data/share/omni-cortex/dashboard/backend/models.py +0 -0
- {omni_cortex-1.17.1.data → omni_cortex-1.17.3.data}/data/share/omni-cortex/dashboard/backend/project_config.py +0 -0
- {omni_cortex-1.17.1.data → omni_cortex-1.17.3.data}/data/share/omni-cortex/dashboard/backend/project_scanner.py +0 -0
- {omni_cortex-1.17.1.data → omni_cortex-1.17.3.data}/data/share/omni-cortex/dashboard/backend/prompt_security.py +0 -0
- {omni_cortex-1.17.1.data → omni_cortex-1.17.3.data}/data/share/omni-cortex/dashboard/backend/pyproject.toml +0 -0
- {omni_cortex-1.17.1.data → omni_cortex-1.17.3.data}/data/share/omni-cortex/dashboard/backend/security.py +0 -0
- {omni_cortex-1.17.1.data → omni_cortex-1.17.3.data}/data/share/omni-cortex/dashboard/backend/uv.lock +0 -0
- {omni_cortex-1.17.1.data → omni_cortex-1.17.3.data}/data/share/omni-cortex/dashboard/backend/websocket_manager.py +0 -0
- {omni_cortex-1.17.1.data → omni_cortex-1.17.3.data}/data/share/omni-cortex/hooks/post_tool_use.py +0 -0
- {omni_cortex-1.17.1.data → omni_cortex-1.17.3.data}/data/share/omni-cortex/hooks/pre_tool_use.py +0 -0
- {omni_cortex-1.17.1.data → omni_cortex-1.17.3.data}/data/share/omni-cortex/hooks/session_utils.py +0 -0
- {omni_cortex-1.17.1.data → omni_cortex-1.17.3.data}/data/share/omni-cortex/hooks/stop.py +0 -0
- {omni_cortex-1.17.1.data → omni_cortex-1.17.3.data}/data/share/omni-cortex/hooks/subagent_stop.py +0 -0
- {omni_cortex-1.17.1.data → omni_cortex-1.17.3.data}/data/share/omni-cortex/hooks/user_prompt.py +0 -0
- {omni_cortex-1.17.1.dist-info → omni_cortex-1.17.3.dist-info}/WHEEL +0 -0
- {omni_cortex-1.17.1.dist-info → omni_cortex-1.17.3.dist-info}/entry_points.txt +0 -0
- {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,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
|