omni-cortex 1.6.0__py3-none-any.whl → 1.7.0__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 (26) hide show
  1. {omni_cortex-1.6.0.data → omni_cortex-1.7.0.data}/data/share/omni-cortex/hooks/post_tool_use.py +173 -129
  2. {omni_cortex-1.6.0.data → omni_cortex-1.7.0.data}/data/share/omni-cortex/hooks/pre_tool_use.py +2 -126
  3. omni_cortex-1.7.0.data/data/share/omni-cortex/hooks/session_utils.py +186 -0
  4. {omni_cortex-1.6.0.dist-info → omni_cortex-1.7.0.dist-info}/METADATA +36 -7
  5. omni_cortex-1.7.0.dist-info/RECORD +25 -0
  6. omni_cortex-1.6.0.dist-info/RECORD +0 -24
  7. {omni_cortex-1.6.0.data → omni_cortex-1.7.0.data}/data/share/omni-cortex/dashboard/backend/.env.example +0 -0
  8. {omni_cortex-1.6.0.data → omni_cortex-1.7.0.data}/data/share/omni-cortex/dashboard/backend/backfill_summaries.py +0 -0
  9. {omni_cortex-1.6.0.data → omni_cortex-1.7.0.data}/data/share/omni-cortex/dashboard/backend/chat_service.py +0 -0
  10. {omni_cortex-1.6.0.data → omni_cortex-1.7.0.data}/data/share/omni-cortex/dashboard/backend/database.py +0 -0
  11. {omni_cortex-1.6.0.data → omni_cortex-1.7.0.data}/data/share/omni-cortex/dashboard/backend/image_service.py +0 -0
  12. {omni_cortex-1.6.0.data → omni_cortex-1.7.0.data}/data/share/omni-cortex/dashboard/backend/logging_config.py +0 -0
  13. {omni_cortex-1.6.0.data → omni_cortex-1.7.0.data}/data/share/omni-cortex/dashboard/backend/main.py +0 -0
  14. {omni_cortex-1.6.0.data → omni_cortex-1.7.0.data}/data/share/omni-cortex/dashboard/backend/models.py +0 -0
  15. {omni_cortex-1.6.0.data → omni_cortex-1.7.0.data}/data/share/omni-cortex/dashboard/backend/project_config.py +0 -0
  16. {omni_cortex-1.6.0.data → omni_cortex-1.7.0.data}/data/share/omni-cortex/dashboard/backend/project_scanner.py +0 -0
  17. {omni_cortex-1.6.0.data → omni_cortex-1.7.0.data}/data/share/omni-cortex/dashboard/backend/prompt_security.py +0 -0
  18. {omni_cortex-1.6.0.data → omni_cortex-1.7.0.data}/data/share/omni-cortex/dashboard/backend/pyproject.toml +0 -0
  19. {omni_cortex-1.6.0.data → omni_cortex-1.7.0.data}/data/share/omni-cortex/dashboard/backend/security.py +0 -0
  20. {omni_cortex-1.6.0.data → omni_cortex-1.7.0.data}/data/share/omni-cortex/dashboard/backend/uv.lock +0 -0
  21. {omni_cortex-1.6.0.data → omni_cortex-1.7.0.data}/data/share/omni-cortex/dashboard/backend/websocket_manager.py +0 -0
  22. {omni_cortex-1.6.0.data → omni_cortex-1.7.0.data}/data/share/omni-cortex/hooks/stop.py +0 -0
  23. {omni_cortex-1.6.0.data → omni_cortex-1.7.0.data}/data/share/omni-cortex/hooks/subagent_stop.py +0 -0
  24. {omni_cortex-1.6.0.dist-info → omni_cortex-1.7.0.dist-info}/WHEEL +0 -0
  25. {omni_cortex-1.6.0.dist-info → omni_cortex-1.7.0.dist-info}/entry_points.txt +0 -0
  26. {omni_cortex-1.6.0.dist-info → omni_cortex-1.7.0.dist-info}/licenses/LICENSE +0 -0
@@ -22,14 +22,12 @@ import re
22
22
  import sys
23
23
  import os
24
24
  import sqlite3
25
- import time
26
25
  from datetime import datetime, timezone
27
26
  from pathlib import Path
28
- from typing import Optional
29
27
 
28
+ # Import shared session management
29
+ from session_utils import get_or_create_session
30
30
 
31
- # Session timeout in seconds (4 hours of inactivity = new session)
32
- SESSION_TIMEOUT_SECONDS = 4 * 60 * 60
33
31
 
34
32
  # Patterns for sensitive field names that should be redacted
35
33
  SENSITIVE_FIELD_PATTERNS = [
@@ -41,128 +39,6 @@ SENSITIVE_FIELD_PATTERNS = [
41
39
  ]
42
40
 
43
41
 
44
- def generate_session_id() -> str:
45
- """Generate a unique session ID matching the MCP format."""
46
- timestamp_ms = int(time.time() * 1000)
47
- random_hex = os.urandom(4).hex()
48
- return f"sess_{timestamp_ms}_{random_hex}"
49
-
50
-
51
- def get_session_file_path() -> Path:
52
- """Get the path to the current session file."""
53
- project_path = os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd())
54
- return Path(project_path) / ".omni-cortex" / "current_session.json"
55
-
56
-
57
- def load_session_file() -> Optional[dict]:
58
- """Load the current session from file if it exists and is valid."""
59
- session_file = get_session_file_path()
60
- if not session_file.exists():
61
- return None
62
-
63
- try:
64
- with open(session_file, "r") as f:
65
- return json.load(f)
66
- except (json.JSONDecodeError, IOError):
67
- return None
68
-
69
-
70
- def save_session_file(session_data: dict) -> None:
71
- """Save the current session to file."""
72
- session_file = get_session_file_path()
73
- session_file.parent.mkdir(parents=True, exist_ok=True)
74
-
75
- with open(session_file, "w") as f:
76
- json.dump(session_data, f, indent=2)
77
-
78
-
79
- def is_session_valid(session_data: dict) -> bool:
80
- """Check if a session is still valid (not timed out)."""
81
- last_activity = session_data.get("last_activity_at")
82
- if not last_activity:
83
- return False
84
-
85
- try:
86
- last_time = datetime.fromisoformat(last_activity.replace("Z", "+00:00"))
87
- now = datetime.now(timezone.utc)
88
- elapsed_seconds = (now - last_time).total_seconds()
89
- return elapsed_seconds < SESSION_TIMEOUT_SECONDS
90
- except (ValueError, TypeError):
91
- return False
92
-
93
-
94
- def create_session_in_db(conn: sqlite3.Connection, session_id: str, project_path: str) -> None:
95
- """Create a new session record in the database."""
96
- cursor = conn.cursor()
97
- now = datetime.now(timezone.utc).isoformat()
98
-
99
- # Check if sessions table exists (it might not if only activities table was created)
100
- cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='sessions'")
101
- if cursor.fetchone() is None:
102
- # Create sessions table with minimal schema
103
- conn.executescript("""
104
- CREATE TABLE IF NOT EXISTS sessions (
105
- id TEXT PRIMARY KEY,
106
- project_path TEXT NOT NULL,
107
- started_at TEXT NOT NULL,
108
- ended_at TEXT,
109
- summary TEXT,
110
- tags TEXT,
111
- metadata TEXT
112
- );
113
- CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at DESC);
114
- CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_path);
115
- """)
116
- conn.commit()
117
-
118
- cursor.execute(
119
- """
120
- INSERT OR IGNORE INTO sessions (id, project_path, started_at)
121
- VALUES (?, ?, ?)
122
- """,
123
- (session_id, project_path, now),
124
- )
125
- conn.commit()
126
-
127
-
128
- def get_or_create_session(conn: sqlite3.Connection, project_path: str) -> str:
129
- """Get the current session ID, creating a new one if needed.
130
-
131
- Session management logic:
132
- 1. Check for existing session file
133
- 2. If exists and not timed out, use it and update last_activity
134
- 3. If doesn't exist or timed out, create new session
135
-
136
- Returns:
137
- The session ID to use for activity logging
138
- """
139
- session_data = load_session_file()
140
- now_iso = datetime.now(timezone.utc).isoformat()
141
-
142
- if session_data and is_session_valid(session_data):
143
- # Update last activity time
144
- session_data["last_activity_at"] = now_iso
145
- save_session_file(session_data)
146
- return session_data["session_id"]
147
-
148
- # Create new session
149
- session_id = generate_session_id()
150
-
151
- # Create in database
152
- create_session_in_db(conn, session_id, project_path)
153
-
154
- # Save to file
155
- session_data = {
156
- "session_id": session_id,
157
- "project_path": project_path,
158
- "started_at": now_iso,
159
- "last_activity_at": now_iso,
160
- }
161
- save_session_file(session_data)
162
-
163
- return session_id
164
-
165
-
166
42
  def redact_sensitive_fields(data: dict) -> dict:
167
43
  """Redact sensitive fields from a dictionary for safe logging.
168
44
 
@@ -253,6 +129,144 @@ def truncate(text: str, max_length: int = 10000) -> str:
253
129
  return text[:max_length - 20] + "\n... [truncated]"
254
130
 
255
131
 
132
+ def extract_skill_info(tool_input: dict, project_path: str) -> tuple:
133
+ """Extract skill name and scope from Skill tool input.
134
+
135
+ Returns:
136
+ Tuple of (skill_name, command_scope)
137
+ """
138
+ try:
139
+ skill_name = tool_input.get("skill", "")
140
+ if not skill_name:
141
+ return None, None
142
+
143
+ # Determine scope by checking file locations
144
+ project_cmd = Path(project_path) / ".claude" / "commands" / f"{skill_name}.md"
145
+ if project_cmd.exists():
146
+ return skill_name, "project"
147
+
148
+ universal_cmd = Path.home() / ".claude" / "commands" / f"{skill_name}.md"
149
+ if universal_cmd.exists():
150
+ return skill_name, "universal"
151
+
152
+ return skill_name, "unknown"
153
+ except Exception:
154
+ return None, None
155
+
156
+
157
+ def extract_mcp_server(tool_name: str) -> str:
158
+ """Extract MCP server name from tool name pattern mcp__servername__toolname."""
159
+ if not tool_name or not tool_name.startswith("mcp__"):
160
+ return None
161
+
162
+ parts = tool_name.split("__")
163
+ if len(parts) >= 3:
164
+ return parts[1]
165
+ return None
166
+
167
+
168
+ def ensure_analytics_columns(conn: sqlite3.Connection) -> None:
169
+ """Ensure command analytics columns exist in activities table."""
170
+ cursor = conn.cursor()
171
+ columns = cursor.execute("PRAGMA table_info(activities)").fetchall()
172
+ column_names = [col[1] for col in columns]
173
+
174
+ new_columns = [
175
+ ("command_name", "TEXT"),
176
+ ("command_scope", "TEXT"),
177
+ ("mcp_server", "TEXT"),
178
+ ("skill_name", "TEXT"),
179
+ ("summary", "TEXT"),
180
+ ("summary_detail", "TEXT"),
181
+ ]
182
+
183
+ for col_name, col_type in new_columns:
184
+ if col_name not in column_names:
185
+ cursor.execute(f"ALTER TABLE activities ADD COLUMN {col_name} {col_type}")
186
+
187
+ conn.commit()
188
+
189
+
190
+ def generate_summary(tool_name: str, tool_input: dict, success: bool) -> tuple:
191
+ """Generate short and detailed summaries for an activity.
192
+
193
+ Returns:
194
+ Tuple of (summary, summary_detail)
195
+ """
196
+ if not tool_name:
197
+ return None, None
198
+
199
+ input_data = tool_input if isinstance(tool_input, dict) else {}
200
+ short = ""
201
+ detail = ""
202
+
203
+ if tool_name == "Read":
204
+ path = input_data.get("file_path", "unknown")
205
+ filename = Path(path).name if path else "file"
206
+ short = f"Read file: {filename}"
207
+ detail = f"Reading contents of {path}"
208
+
209
+ elif tool_name == "Write":
210
+ path = input_data.get("file_path", "unknown")
211
+ filename = Path(path).name if path else "file"
212
+ short = f"Write file: {filename}"
213
+ detail = f"Writing/creating file at {path}"
214
+
215
+ elif tool_name == "Edit":
216
+ path = input_data.get("file_path", "unknown")
217
+ filename = Path(path).name if path else "file"
218
+ short = f"Edit file: {filename}"
219
+ detail = f"Editing {path}"
220
+
221
+ elif tool_name == "Bash":
222
+ cmd = str(input_data.get("command", ""))[:50]
223
+ short = f"Run: {cmd}..."
224
+ detail = f"Executing: {input_data.get('command', 'unknown')}"
225
+
226
+ elif tool_name == "Grep":
227
+ pattern = input_data.get("pattern", "")
228
+ short = f"Search: {pattern[:30]}"
229
+ detail = f"Searching for pattern: {pattern}"
230
+
231
+ elif tool_name == "Glob":
232
+ pattern = input_data.get("pattern", "")
233
+ short = f"Find files: {pattern[:30]}"
234
+ detail = f"Finding files matching: {pattern}"
235
+
236
+ elif tool_name == "Skill":
237
+ skill = input_data.get("skill", "unknown")
238
+ short = f"Run skill: /{skill}"
239
+ detail = f"Executing slash command /{skill}"
240
+
241
+ elif tool_name == "Task":
242
+ desc = input_data.get("description", "task")
243
+ short = f"Spawn agent: {desc[:30]}"
244
+ detail = f"Launching sub-agent: {desc}"
245
+
246
+ elif tool_name == "TodoWrite":
247
+ todos = input_data.get("todos", [])
248
+ count = len(todos) if isinstance(todos, list) else 0
249
+ short = f"Update todo: {count} items"
250
+ detail = f"Managing task list with {count} items"
251
+
252
+ elif tool_name.startswith("mcp__"):
253
+ parts = tool_name.split("__")
254
+ server = parts[1] if len(parts) > 1 else "unknown"
255
+ tool = parts[2] if len(parts) > 2 else tool_name
256
+ short = f"MCP: {server}/{tool}"
257
+ detail = f"Calling {tool} from MCP server {server}"
258
+
259
+ else:
260
+ short = f"Tool: {tool_name}"
261
+ detail = f"Using tool {tool_name}"
262
+
263
+ if not success:
264
+ short = f"[FAILED] {short}"
265
+ detail = f"[FAILED] {detail}"
266
+
267
+ return short, detail
268
+
269
+
256
270
  def main():
257
271
  """Process PostToolUse hook."""
258
272
  try:
@@ -288,6 +302,9 @@ def main():
288
302
  db_path = get_db_path()
289
303
  conn = ensure_database(db_path)
290
304
 
305
+ # Ensure analytics columns exist
306
+ ensure_analytics_columns(conn)
307
+
291
308
  # Get or create session (auto-manages session lifecycle)
292
309
  session_id = get_or_create_session(conn, project_path)
293
310
 
@@ -295,14 +312,36 @@ def main():
295
312
  safe_input = redact_sensitive_fields(tool_input) if isinstance(tool_input, dict) else tool_input
296
313
  safe_output = redact_sensitive_fields(tool_output) if isinstance(tool_output, dict) else tool_output
297
314
 
298
- # Insert activity record
315
+ # Extract command analytics
316
+ skill_name = None
317
+ command_scope = None
318
+ mcp_server = None
319
+
320
+ # Extract skill info from Skill tool calls
321
+ if tool_name == "Skill" and isinstance(tool_input, dict):
322
+ skill_name, command_scope = extract_skill_info(tool_input, project_path)
323
+
324
+ # Extract MCP server from tool name (mcp__servername__toolname pattern)
325
+ if tool_name and tool_name.startswith("mcp__"):
326
+ mcp_server = extract_mcp_server(tool_name)
327
+
328
+ # Generate summary for activity
329
+ summary = None
330
+ summary_detail = None
331
+ try:
332
+ summary, summary_detail = generate_summary(tool_name, safe_input, not is_error)
333
+ except Exception:
334
+ pass
335
+
336
+ # Insert activity record with analytics columns
299
337
  cursor = conn.cursor()
300
338
  cursor.execute(
301
339
  """
302
340
  INSERT INTO activities (
303
341
  id, session_id, agent_id, timestamp, event_type,
304
- tool_name, tool_input, tool_output, success, error_message, project_path
305
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
342
+ tool_name, tool_input, tool_output, success, error_message, project_path,
343
+ skill_name, command_scope, mcp_server, summary, summary_detail
344
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
306
345
  """,
307
346
  (
308
347
  generate_id(),
@@ -316,6 +355,11 @@ def main():
316
355
  0 if is_error else 1,
317
356
  error_message,
318
357
  project_path,
358
+ skill_name,
359
+ command_scope,
360
+ mcp_server,
361
+ summary,
362
+ summary_detail,
319
363
  ),
320
364
  )
321
365
  conn.commit()
@@ -22,14 +22,12 @@ import re
22
22
  import sys
23
23
  import os
24
24
  import sqlite3
25
- import time
26
25
  from datetime import datetime, timezone
27
26
  from pathlib import Path
28
- from typing import Optional
29
27
 
28
+ # Import shared session management
29
+ from session_utils import get_or_create_session
30
30
 
31
- # Session timeout in seconds (4 hours of inactivity = new session)
32
- SESSION_TIMEOUT_SECONDS = 4 * 60 * 60
33
31
 
34
32
  # Patterns for sensitive field names that should be redacted
35
33
  SENSITIVE_FIELD_PATTERNS = [
@@ -41,128 +39,6 @@ SENSITIVE_FIELD_PATTERNS = [
41
39
  ]
42
40
 
43
41
 
44
- def generate_session_id() -> str:
45
- """Generate a unique session ID matching the MCP format."""
46
- timestamp_ms = int(time.time() * 1000)
47
- random_hex = os.urandom(4).hex()
48
- return f"sess_{timestamp_ms}_{random_hex}"
49
-
50
-
51
- def get_session_file_path() -> Path:
52
- """Get the path to the current session file."""
53
- project_path = os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd())
54
- return Path(project_path) / ".omni-cortex" / "current_session.json"
55
-
56
-
57
- def load_session_file() -> Optional[dict]:
58
- """Load the current session from file if it exists and is valid."""
59
- session_file = get_session_file_path()
60
- if not session_file.exists():
61
- return None
62
-
63
- try:
64
- with open(session_file, "r") as f:
65
- return json.load(f)
66
- except (json.JSONDecodeError, IOError):
67
- return None
68
-
69
-
70
- def save_session_file(session_data: dict) -> None:
71
- """Save the current session to file."""
72
- session_file = get_session_file_path()
73
- session_file.parent.mkdir(parents=True, exist_ok=True)
74
-
75
- with open(session_file, "w") as f:
76
- json.dump(session_data, f, indent=2)
77
-
78
-
79
- def is_session_valid(session_data: dict) -> bool:
80
- """Check if a session is still valid (not timed out)."""
81
- last_activity = session_data.get("last_activity_at")
82
- if not last_activity:
83
- return False
84
-
85
- try:
86
- last_time = datetime.fromisoformat(last_activity.replace("Z", "+00:00"))
87
- now = datetime.now(timezone.utc)
88
- elapsed_seconds = (now - last_time).total_seconds()
89
- return elapsed_seconds < SESSION_TIMEOUT_SECONDS
90
- except (ValueError, TypeError):
91
- return False
92
-
93
-
94
- def create_session_in_db(conn: sqlite3.Connection, session_id: str, project_path: str) -> None:
95
- """Create a new session record in the database."""
96
- cursor = conn.cursor()
97
- now = datetime.now(timezone.utc).isoformat()
98
-
99
- # Check if sessions table exists (it might not if only activities table was created)
100
- cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='sessions'")
101
- if cursor.fetchone() is None:
102
- # Create sessions table with minimal schema
103
- conn.executescript("""
104
- CREATE TABLE IF NOT EXISTS sessions (
105
- id TEXT PRIMARY KEY,
106
- project_path TEXT NOT NULL,
107
- started_at TEXT NOT NULL,
108
- ended_at TEXT,
109
- summary TEXT,
110
- tags TEXT,
111
- metadata TEXT
112
- );
113
- CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at DESC);
114
- CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_path);
115
- """)
116
- conn.commit()
117
-
118
- cursor.execute(
119
- """
120
- INSERT OR IGNORE INTO sessions (id, project_path, started_at)
121
- VALUES (?, ?, ?)
122
- """,
123
- (session_id, project_path, now),
124
- )
125
- conn.commit()
126
-
127
-
128
- def get_or_create_session(conn: sqlite3.Connection, project_path: str) -> str:
129
- """Get the current session ID, creating a new one if needed.
130
-
131
- Session management logic:
132
- 1. Check for existing session file
133
- 2. If exists and not timed out, use it and update last_activity
134
- 3. If doesn't exist or timed out, create new session
135
-
136
- Returns:
137
- The session ID to use for activity logging
138
- """
139
- session_data = load_session_file()
140
- now_iso = datetime.now(timezone.utc).isoformat()
141
-
142
- if session_data and is_session_valid(session_data):
143
- # Update last activity time
144
- session_data["last_activity_at"] = now_iso
145
- save_session_file(session_data)
146
- return session_data["session_id"]
147
-
148
- # Create new session
149
- session_id = generate_session_id()
150
-
151
- # Create in database
152
- create_session_in_db(conn, session_id, project_path)
153
-
154
- # Save to file
155
- session_data = {
156
- "session_id": session_id,
157
- "project_path": project_path,
158
- "started_at": now_iso,
159
- "last_activity_at": now_iso,
160
- }
161
- save_session_file(session_data)
162
-
163
- return session_id
164
-
165
-
166
42
  def redact_sensitive_fields(data: dict) -> dict:
167
43
  """Redact sensitive fields from a dictionary for safe logging.
168
44
 
@@ -0,0 +1,186 @@
1
+ #!/usr/bin/env python3
2
+ """Shared session management utilities for Claude Code hooks.
3
+
4
+ This module provides session management functionality that can be shared
5
+ across pre_tool_use.py and post_tool_use.py hooks to ensure consistent
6
+ session tracking.
7
+
8
+ Session Management Logic:
9
+ 1. Check for existing session file at `.omni-cortex/current_session.json`
10
+ 2. If session exists and is valid (not timed out), use it
11
+ 3. If no valid session, create a new one in both file and database
12
+ 4. Update last_activity_at on each use to track session activity
13
+ """
14
+
15
+ import json
16
+ import os
17
+ import sqlite3
18
+ import time
19
+ from datetime import datetime, timezone
20
+ from pathlib import Path
21
+ from typing import Optional
22
+
23
+
24
+ # Session timeout in seconds (4 hours of inactivity = new session)
25
+ SESSION_TIMEOUT_SECONDS = 4 * 60 * 60
26
+
27
+
28
+ def generate_session_id() -> str:
29
+ """Generate a unique session ID matching the MCP format.
30
+
31
+ Returns:
32
+ Session ID in format: sess_{timestamp_ms}_{random_hex}
33
+ """
34
+ timestamp_ms = int(time.time() * 1000)
35
+ random_hex = os.urandom(4).hex()
36
+ return f"sess_{timestamp_ms}_{random_hex}"
37
+
38
+
39
+ def get_session_file_path() -> Path:
40
+ """Get the path to the current session file.
41
+
42
+ Returns:
43
+ Path to .omni-cortex/current_session.json
44
+ """
45
+ project_path = os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd())
46
+ return Path(project_path) / ".omni-cortex" / "current_session.json"
47
+
48
+
49
+ def load_session_file() -> Optional[dict]:
50
+ """Load the current session from file if it exists.
51
+
52
+ Returns:
53
+ Session data dict or None if file doesn't exist or is invalid
54
+ """
55
+ session_file = get_session_file_path()
56
+ if not session_file.exists():
57
+ return None
58
+
59
+ try:
60
+ with open(session_file, "r") as f:
61
+ return json.load(f)
62
+ except (json.JSONDecodeError, IOError):
63
+ return None
64
+
65
+
66
+ def save_session_file(session_data: dict) -> None:
67
+ """Save the current session to file.
68
+
69
+ Args:
70
+ session_data: Dict containing session_id, project_path, started_at, last_activity_at
71
+ """
72
+ session_file = get_session_file_path()
73
+ session_file.parent.mkdir(parents=True, exist_ok=True)
74
+
75
+ with open(session_file, "w") as f:
76
+ json.dump(session_data, f, indent=2)
77
+
78
+
79
+ def is_session_valid(session_data: dict) -> bool:
80
+ """Check if a session is still valid (not timed out).
81
+
82
+ A session is valid if:
83
+ - It has a last_activity_at timestamp
84
+ - The timestamp is within SESSION_TIMEOUT_SECONDS of now
85
+
86
+ Args:
87
+ session_data: Session dict with last_activity_at field
88
+
89
+ Returns:
90
+ True if session is valid, False otherwise
91
+ """
92
+ last_activity = session_data.get("last_activity_at")
93
+ if not last_activity:
94
+ return False
95
+
96
+ try:
97
+ last_time = datetime.fromisoformat(last_activity.replace("Z", "+00:00"))
98
+ now = datetime.now(timezone.utc)
99
+ elapsed_seconds = (now - last_time).total_seconds()
100
+ return elapsed_seconds < SESSION_TIMEOUT_SECONDS
101
+ except (ValueError, TypeError):
102
+ return False
103
+
104
+
105
+ def create_session_in_db(conn: sqlite3.Connection, session_id: str, project_path: str) -> None:
106
+ """Create a new session record in the database.
107
+
108
+ Also creates the sessions table if it doesn't exist (for first-run scenarios).
109
+
110
+ Args:
111
+ conn: SQLite database connection
112
+ session_id: The session ID to create
113
+ project_path: The project directory path
114
+ """
115
+ cursor = conn.cursor()
116
+ now = datetime.now(timezone.utc).isoformat()
117
+
118
+ # Check if sessions table exists (it might not if only activities table was created)
119
+ cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='sessions'")
120
+ if cursor.fetchone() is None:
121
+ # Create sessions table with minimal schema
122
+ conn.executescript("""
123
+ CREATE TABLE IF NOT EXISTS sessions (
124
+ id TEXT PRIMARY KEY,
125
+ project_path TEXT NOT NULL,
126
+ started_at TEXT NOT NULL,
127
+ ended_at TEXT,
128
+ summary TEXT,
129
+ tags TEXT,
130
+ metadata TEXT
131
+ );
132
+ CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at DESC);
133
+ CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_path);
134
+ """)
135
+ conn.commit()
136
+
137
+ cursor.execute(
138
+ """
139
+ INSERT OR IGNORE INTO sessions (id, project_path, started_at)
140
+ VALUES (?, ?, ?)
141
+ """,
142
+ (session_id, project_path, now),
143
+ )
144
+ conn.commit()
145
+
146
+
147
+ def get_or_create_session(conn: sqlite3.Connection, project_path: str) -> str:
148
+ """Get the current session ID, creating a new one if needed.
149
+
150
+ Session management logic:
151
+ 1. Check for existing session file
152
+ 2. If exists and not timed out, use it and update last_activity
153
+ 3. If doesn't exist or timed out, create new session
154
+
155
+ Args:
156
+ conn: SQLite database connection
157
+ project_path: The project directory path
158
+
159
+ Returns:
160
+ The session ID to use for activity logging
161
+ """
162
+ session_data = load_session_file()
163
+ now_iso = datetime.now(timezone.utc).isoformat()
164
+
165
+ if session_data and is_session_valid(session_data):
166
+ # Update last activity time
167
+ session_data["last_activity_at"] = now_iso
168
+ save_session_file(session_data)
169
+ return session_data["session_id"]
170
+
171
+ # Create new session
172
+ session_id = generate_session_id()
173
+
174
+ # Create in database
175
+ create_session_in_db(conn, session_id, project_path)
176
+
177
+ # Save to file
178
+ session_data = {
179
+ "session_id": session_id,
180
+ "project_path": project_path,
181
+ "started_at": now_iso,
182
+ "last_activity_at": now_iso,
183
+ }
184
+ save_session_file(session_data)
185
+
186
+ return session_id
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: omni-cortex
3
- Version: 1.6.0
3
+ Version: 1.7.0
4
4
  Summary: Give Claude Code a perfect memory - auto-logs everything, searches smartly, and gets smarter over time
5
5
  Project-URL: Homepage, https://github.com/AllCytes/Omni-Cortex
6
6
  Project-URL: Repository, https://github.com/AllCytes/Omni-Cortex
@@ -294,18 +294,47 @@ The PDFs use a light theme with blue/purple/green accents. Edit `docs/create_pdf
294
294
 
295
295
  ## Development
296
296
 
297
+ ### Quick Setup (with Claude Code)
298
+
299
+ If you're using Claude Code, just run:
300
+
301
+ ```bash
302
+ /dev-setup
303
+ ```
304
+
305
+ This will guide you through setting up the development environment.
306
+
307
+ ### Manual Setup
308
+
297
309
  ```bash
298
- # Install dev dependencies
299
- pip install -e ".[dev]"
310
+ # Clone and install in editable mode
311
+ git clone https://github.com/AllCytes/Omni-Cortex.git
312
+ cd Omni-Cortex
313
+ pip install -e .
314
+
315
+ # Install dashboard dependencies
316
+ cd dashboard/backend && pip install -r requirements.txt
317
+ cd ../frontend && npm install
318
+ cd ../..
319
+
320
+ # Verify installation
321
+ omni-cortex --help
322
+ omni-cortex-dashboard --help
323
+ ```
300
324
 
301
- # Run tests
325
+ **Important**: Always use `pip install -e .` (editable mode) so changes are immediately reflected without reinstalling.
326
+
327
+ ### Running Tests
328
+
329
+ ```bash
302
330
  pytest
303
331
 
304
- # Format code
305
- black src tests
306
- ruff check src tests
332
+ # With coverage
333
+ pytest --cov=src/omni_cortex
307
334
  ```
308
335
 
336
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for full development guidelines.
337
+
309
338
  ## Security
310
339
 
311
340
  Omni Cortex v1.0.3 has been security reviewed:
@@ -0,0 +1,25 @@
1
+ omni_cortex-1.7.0.data/data/share/omni-cortex/hooks/post_tool_use.py,sha256=yBLoYvEdUunbG8fclzIEWszCptworkUUcOUSKzNsvms,12271
2
+ omni_cortex-1.7.0.data/data/share/omni-cortex/hooks/pre_tool_use.py,sha256=mkZ7eeBnjWkIgNnrqfYSXIhhLNYYk4hQx_6F0pNrGoc,6395
3
+ omni_cortex-1.7.0.data/data/share/omni-cortex/hooks/session_utils.py,sha256=3SKPCytqWuRPOupWdzmwBoKBDJqtLcT1Nle_pueDQUY,5746
4
+ omni_cortex-1.7.0.data/data/share/omni-cortex/hooks/stop.py,sha256=T1bwcmbTLj0gzjrVvFBT1zB6wff4J2YkYBAY-ZxZI5g,5336
5
+ omni_cortex-1.7.0.data/data/share/omni-cortex/hooks/subagent_stop.py,sha256=V9HQSFGNOfkg8ZCstPEy4h5V8BP4AbrVr8teFzN1kNk,3314
6
+ omni_cortex-1.7.0.data/data/share/omni-cortex/dashboard/backend/.env.example,sha256=LenAe1A9dnwaS5UaRFPR0m_dghUcjsK-yMXZTSLIL8o,667
7
+ omni_cortex-1.7.0.data/data/share/omni-cortex/dashboard/backend/backfill_summaries.py,sha256=ElchfcBv4pmVr2PsePCgFlCyuvf4_jDJj_C3AmMhu7U,8973
8
+ omni_cortex-1.7.0.data/data/share/omni-cortex/dashboard/backend/chat_service.py,sha256=2Dfxlj0xwR47EI1jrbOF8t1E0m_OaXl-wdJoo6rqiHE,9187
9
+ omni_cortex-1.7.0.data/data/share/omni-cortex/dashboard/backend/database.py,sha256=Zh4Hl5I0DTZCosWIvkOYpo3Nyta8f54vnj72giDQ8vE,34942
10
+ omni_cortex-1.7.0.data/data/share/omni-cortex/dashboard/backend/image_service.py,sha256=wkWjUtLTLqIFgJ9WU99O8IWDvEUbv9-P8pizkV-TvhM,19059
11
+ omni_cortex-1.7.0.data/data/share/omni-cortex/dashboard/backend/logging_config.py,sha256=WnunFGET9zlsn9WBpVsio2zI7BiUQanE0xzAQQxIhII,3944
12
+ omni_cortex-1.7.0.data/data/share/omni-cortex/dashboard/backend/main.py,sha256=-sJg8GIOzX07xuUxvE6cZ6THqLblA0ObxQiOyNfvrFk,37232
13
+ omni_cortex-1.7.0.data/data/share/omni-cortex/dashboard/backend/models.py,sha256=Lv_qIrDNRlQNiveRwDrlhVz1QTeWD4DPpr5BBuA5Ty0,5968
14
+ omni_cortex-1.7.0.data/data/share/omni-cortex/dashboard/backend/project_config.py,sha256=ZxGoeRpHvN5qQyf2hRxrAZiHrPSwdQp59f0di6O1LKM,4352
15
+ omni_cortex-1.7.0.data/data/share/omni-cortex/dashboard/backend/project_scanner.py,sha256=lwFXS8iJbOoxf7FAyo2TjH25neaMHiJ8B3jS57XxtDI,5713
16
+ omni_cortex-1.7.0.data/data/share/omni-cortex/dashboard/backend/prompt_security.py,sha256=LcdZhYy1CfpSq_4BPO6lMJ15phc2ZXLUSBAnAvODVCI,3423
17
+ omni_cortex-1.7.0.data/data/share/omni-cortex/dashboard/backend/pyproject.toml,sha256=9pbbGQXLe1Xd06nZAtDySCHIlfMWvPaB-C6tGZR6umc,502
18
+ omni_cortex-1.7.0.data/data/share/omni-cortex/dashboard/backend/security.py,sha256=nQsoPE0n5dtY9ive00d33W1gL48GgK7C5Ae0BK2oW2k,3479
19
+ omni_cortex-1.7.0.data/data/share/omni-cortex/dashboard/backend/uv.lock,sha256=miB9zGGSirBkjDE-OZTPCnv43Yc98xuAz_Ne8vTNFHg,186004
20
+ omni_cortex-1.7.0.data/data/share/omni-cortex/dashboard/backend/websocket_manager.py,sha256=ABXAtlhBI5vnTcwdQUS-UQcDyTn-rWZL5OKEP9YY-kU,3619
21
+ omni_cortex-1.7.0.dist-info/METADATA,sha256=Oij28FeAaP2iylNabfycTMGdR2ynLdhia8T0OGB_GAc,10521
22
+ omni_cortex-1.7.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
23
+ omni_cortex-1.7.0.dist-info/entry_points.txt,sha256=rohx4mFH2ffZmMb9QXPZmFf-ZGjA3jpKVDVeET-ttiM,150
24
+ omni_cortex-1.7.0.dist-info/licenses/LICENSE,sha256=oG_397owMmi-Umxp5sYocJ6RPohp9_bDNnnEu9OUphg,1072
25
+ omni_cortex-1.7.0.dist-info/RECORD,,
@@ -1,24 +0,0 @@
1
- omni_cortex-1.6.0.data/data/share/omni-cortex/hooks/post_tool_use.py,sha256=aHaT_Q4_6mLYiYEqppAjQlQ5HqU13MUHq3g8T5agZjI,10779
2
- omni_cortex-1.6.0.data/data/share/omni-cortex/hooks/pre_tool_use.py,sha256=4WrzuJHeyafGM0e1mp3MzPot_mPL4ZBFQOZBw_dow4E,10464
3
- omni_cortex-1.6.0.data/data/share/omni-cortex/hooks/stop.py,sha256=T1bwcmbTLj0gzjrVvFBT1zB6wff4J2YkYBAY-ZxZI5g,5336
4
- omni_cortex-1.6.0.data/data/share/omni-cortex/hooks/subagent_stop.py,sha256=V9HQSFGNOfkg8ZCstPEy4h5V8BP4AbrVr8teFzN1kNk,3314
5
- omni_cortex-1.6.0.data/data/share/omni-cortex/dashboard/backend/.env.example,sha256=LenAe1A9dnwaS5UaRFPR0m_dghUcjsK-yMXZTSLIL8o,667
6
- omni_cortex-1.6.0.data/data/share/omni-cortex/dashboard/backend/backfill_summaries.py,sha256=ElchfcBv4pmVr2PsePCgFlCyuvf4_jDJj_C3AmMhu7U,8973
7
- omni_cortex-1.6.0.data/data/share/omni-cortex/dashboard/backend/chat_service.py,sha256=2Dfxlj0xwR47EI1jrbOF8t1E0m_OaXl-wdJoo6rqiHE,9187
8
- omni_cortex-1.6.0.data/data/share/omni-cortex/dashboard/backend/database.py,sha256=Zh4Hl5I0DTZCosWIvkOYpo3Nyta8f54vnj72giDQ8vE,34942
9
- omni_cortex-1.6.0.data/data/share/omni-cortex/dashboard/backend/image_service.py,sha256=wkWjUtLTLqIFgJ9WU99O8IWDvEUbv9-P8pizkV-TvhM,19059
10
- omni_cortex-1.6.0.data/data/share/omni-cortex/dashboard/backend/logging_config.py,sha256=WnunFGET9zlsn9WBpVsio2zI7BiUQanE0xzAQQxIhII,3944
11
- omni_cortex-1.6.0.data/data/share/omni-cortex/dashboard/backend/main.py,sha256=-sJg8GIOzX07xuUxvE6cZ6THqLblA0ObxQiOyNfvrFk,37232
12
- omni_cortex-1.6.0.data/data/share/omni-cortex/dashboard/backend/models.py,sha256=Lv_qIrDNRlQNiveRwDrlhVz1QTeWD4DPpr5BBuA5Ty0,5968
13
- omni_cortex-1.6.0.data/data/share/omni-cortex/dashboard/backend/project_config.py,sha256=ZxGoeRpHvN5qQyf2hRxrAZiHrPSwdQp59f0di6O1LKM,4352
14
- omni_cortex-1.6.0.data/data/share/omni-cortex/dashboard/backend/project_scanner.py,sha256=lwFXS8iJbOoxf7FAyo2TjH25neaMHiJ8B3jS57XxtDI,5713
15
- omni_cortex-1.6.0.data/data/share/omni-cortex/dashboard/backend/prompt_security.py,sha256=LcdZhYy1CfpSq_4BPO6lMJ15phc2ZXLUSBAnAvODVCI,3423
16
- omni_cortex-1.6.0.data/data/share/omni-cortex/dashboard/backend/pyproject.toml,sha256=9pbbGQXLe1Xd06nZAtDySCHIlfMWvPaB-C6tGZR6umc,502
17
- omni_cortex-1.6.0.data/data/share/omni-cortex/dashboard/backend/security.py,sha256=nQsoPE0n5dtY9ive00d33W1gL48GgK7C5Ae0BK2oW2k,3479
18
- omni_cortex-1.6.0.data/data/share/omni-cortex/dashboard/backend/uv.lock,sha256=miB9zGGSirBkjDE-OZTPCnv43Yc98xuAz_Ne8vTNFHg,186004
19
- omni_cortex-1.6.0.data/data/share/omni-cortex/dashboard/backend/websocket_manager.py,sha256=ABXAtlhBI5vnTcwdQUS-UQcDyTn-rWZL5OKEP9YY-kU,3619
20
- omni_cortex-1.6.0.dist-info/METADATA,sha256=ZCe4O8ZALiljSLbQA5JQ8Y6tm8XwwArKnX-v4dEK44Y,9855
21
- omni_cortex-1.6.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
22
- omni_cortex-1.6.0.dist-info/entry_points.txt,sha256=rohx4mFH2ffZmMb9QXPZmFf-ZGjA3jpKVDVeET-ttiM,150
23
- omni_cortex-1.6.0.dist-info/licenses/LICENSE,sha256=oG_397owMmi-Umxp5sYocJ6RPohp9_bDNnnEu9OUphg,1072
24
- omni_cortex-1.6.0.dist-info/RECORD,,