omni-cortex 1.5.0__tar.gz → 1.7.0__tar.gz

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 (69) hide show
  1. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/.gitignore +5 -0
  2. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/PKG-INFO +36 -7
  3. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/README.md +35 -6
  4. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/dashboard/backend/chat_service.py +5 -1
  5. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/dashboard/backend/image_service.py +4 -4
  6. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/dashboard/backend/main.py +29 -2
  7. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/dashboard/backend/websocket_manager.py +22 -0
  8. omni_cortex-1.7.0/hooks/post_tool_use.py +379 -0
  9. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/hooks/pre_tool_use.py +6 -1
  10. omni_cortex-1.7.0/hooks/session_utils.py +186 -0
  11. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/omni_cortex/__init__.py +1 -1
  12. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/omni_cortex/tools/memories.py +13 -2
  13. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/omni_cortex/utils/formatting.py +43 -6
  14. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/pyproject.toml +1 -1
  15. omni_cortex-1.5.0/hooks/post_tool_use.py +0 -160
  16. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/LICENSE +0 -0
  17. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/dashboard/backend/.env.example +0 -0
  18. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/dashboard/backend/backfill_summaries.py +0 -0
  19. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/dashboard/backend/database.py +0 -0
  20. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/dashboard/backend/logging_config.py +0 -0
  21. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/dashboard/backend/models.py +0 -0
  22. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/dashboard/backend/project_config.py +0 -0
  23. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/dashboard/backend/project_scanner.py +0 -0
  24. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/dashboard/backend/prompt_security.py +0 -0
  25. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/dashboard/backend/pyproject.toml +0 -0
  26. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/dashboard/backend/security.py +0 -0
  27. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/dashboard/backend/uv.lock +0 -0
  28. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/hooks/stop.py +0 -0
  29. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/hooks/subagent_stop.py +0 -0
  30. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/omni_cortex/categorization/__init__.py +0 -0
  31. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/omni_cortex/categorization/auto_tags.py +0 -0
  32. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/omni_cortex/categorization/auto_type.py +0 -0
  33. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/omni_cortex/config.py +0 -0
  34. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/omni_cortex/dashboard.py +0 -0
  35. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/omni_cortex/database/__init__.py +0 -0
  36. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/omni_cortex/database/connection.py +0 -0
  37. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/omni_cortex/database/migrations.py +0 -0
  38. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/omni_cortex/database/schema.py +0 -0
  39. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/omni_cortex/database/sync.py +0 -0
  40. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/omni_cortex/decay/__init__.py +0 -0
  41. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/omni_cortex/decay/importance.py +0 -0
  42. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/omni_cortex/embeddings/__init__.py +0 -0
  43. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/omni_cortex/embeddings/local.py +0 -0
  44. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/omni_cortex/models/__init__.py +0 -0
  45. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/omni_cortex/models/activity.py +0 -0
  46. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/omni_cortex/models/agent.py +0 -0
  47. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/omni_cortex/models/memory.py +0 -0
  48. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/omni_cortex/models/relationship.py +0 -0
  49. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/omni_cortex/models/session.py +0 -0
  50. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/omni_cortex/resources/__init__.py +0 -0
  51. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/omni_cortex/search/__init__.py +0 -0
  52. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/omni_cortex/search/hybrid.py +0 -0
  53. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/omni_cortex/search/keyword.py +0 -0
  54. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/omni_cortex/search/ranking.py +0 -0
  55. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/omni_cortex/search/semantic.py +0 -0
  56. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/omni_cortex/server.py +0 -0
  57. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/omni_cortex/setup.py +0 -0
  58. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/omni_cortex/tools/__init__.py +0 -0
  59. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/omni_cortex/tools/activities.py +0 -0
  60. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/omni_cortex/tools/sessions.py +0 -0
  61. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/omni_cortex/tools/utilities.py +0 -0
  62. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/omni_cortex/utils/__init__.py +0 -0
  63. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/omni_cortex/utils/ids.py +0 -0
  64. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/omni_cortex/utils/timestamps.py +0 -0
  65. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/omni_cortex/utils/truncation.py +0 -0
  66. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/scripts/check-venv.py +0 -0
  67. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/scripts/import_ken_memories.py +0 -0
  68. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/scripts/populate_session_data.py +0 -0
  69. {omni_cortex-1.5.0 → omni_cortex-1.7.0}/scripts/setup.py +0 -0
@@ -155,3 +155,8 @@ PROMPT.md
155
155
  memories.json
156
156
  .claude/
157
157
  .mcp.json
158
+
159
+ # Personal/local files
160
+ adws/
161
+ specs/omni-cortex-adw-system.md
162
+ nul
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: omni-cortex
3
- Version: 1.5.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:
@@ -257,18 +257,47 @@ The PDFs use a light theme with blue/purple/green accents. Edit `docs/create_pdf
257
257
 
258
258
  ## Development
259
259
 
260
+ ### Quick Setup (with Claude Code)
261
+
262
+ If you're using Claude Code, just run:
263
+
264
+ ```bash
265
+ /dev-setup
266
+ ```
267
+
268
+ This will guide you through setting up the development environment.
269
+
270
+ ### Manual Setup
271
+
260
272
  ```bash
261
- # Install dev dependencies
262
- pip install -e ".[dev]"
273
+ # Clone and install in editable mode
274
+ git clone https://github.com/AllCytes/Omni-Cortex.git
275
+ cd Omni-Cortex
276
+ pip install -e .
277
+
278
+ # Install dashboard dependencies
279
+ cd dashboard/backend && pip install -r requirements.txt
280
+ cd ../frontend && npm install
281
+ cd ../..
282
+
283
+ # Verify installation
284
+ omni-cortex --help
285
+ omni-cortex-dashboard --help
286
+ ```
263
287
 
264
- # Run tests
288
+ **Important**: Always use `pip install -e .` (editable mode) so changes are immediately reflected without reinstalling.
289
+
290
+ ### Running Tests
291
+
292
+ ```bash
265
293
  pytest
266
294
 
267
- # Format code
268
- black src tests
269
- ruff check src tests
295
+ # With coverage
296
+ pytest --cov=src/omni_cortex
270
297
  ```
271
298
 
299
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for full development guidelines.
300
+
272
301
  ## Security
273
302
 
274
303
  Omni Cortex v1.0.3 has been security reviewed:
@@ -214,9 +214,13 @@ async def save_conversation(
214
214
  client = get_client()
215
215
  if client:
216
216
  try:
217
+ # Escape content to prevent injection in summary generation
218
+ safe_content = xml_escape(content[:2000])
217
219
  summary_prompt = f"""Summarize this conversation in one concise sentence (max 100 chars):
218
220
 
219
- {content[:2000]}
221
+ <conversation>
222
+ {safe_content}
223
+ </conversation>
220
224
 
221
225
  Summary:"""
222
226
  response = client.models.generate_content(
@@ -209,9 +209,9 @@ Tags: {', '.join(memory.tags) if memory.tags else 'N/A'}
209
209
  if preset_prompt:
210
210
  parts.append(f"\nImage style guidance:\n{preset_prompt}")
211
211
 
212
- # Add user's custom prompt
212
+ # Add user's custom prompt (escaped to prevent injection)
213
213
  if request.custom_prompt:
214
- parts.append(f"\nUser request: {request.custom_prompt}")
214
+ parts.append(f"\nUser request: {xml_escape(request.custom_prompt)}")
215
215
 
216
216
  parts.append("\nGenerate a professional, high-quality image optimized for social media sharing.")
217
217
 
@@ -461,10 +461,10 @@ Tags: {', '.join(memory.tags) if memory.tags else 'N/A'}
461
461
  "parts": parts
462
462
  })
463
463
 
464
- # Add refinement prompt
464
+ # Add refinement prompt (escaped to prevent injection)
465
465
  contents.append({
466
466
  "role": "user",
467
- "parts": [{"text": refinement_prompt}]
467
+ "parts": [{"text": xml_escape(refinement_prompt)}]
468
468
  })
469
469
 
470
470
  # Configure - use defaults or provided values
@@ -135,6 +135,7 @@ class DatabaseChangeHandler(FileSystemEventHandler):
135
135
  self.loop = loop
136
136
  self._debounce_task: Optional[asyncio.Task] = None
137
137
  self._last_path: Optional[str] = None
138
+ self._last_activity_count: dict[str, int] = {}
138
139
 
139
140
  def on_modified(self, event):
140
141
  if event.src_path.endswith("cortex.db") or event.src_path.endswith("global.db"):
@@ -146,9 +147,35 @@ class DatabaseChangeHandler(FileSystemEventHandler):
146
147
  )
147
148
 
148
149
  async def _debounced_notify(self):
149
- await asyncio.sleep(0.5) # Wait for rapid changes to settle
150
+ await asyncio.sleep(0.3) # Reduced from 0.5s for faster updates
150
151
  if self._last_path:
151
- await self.ws_manager.broadcast("database_changed", {"path": self._last_path})
152
+ db_path = self._last_path
153
+
154
+ # Broadcast general database change
155
+ await self.ws_manager.broadcast("database_changed", {"path": db_path})
156
+
157
+ # Fetch and broadcast latest activities (IndyDevDan pattern)
158
+ try:
159
+ # Get recent activities
160
+ recent = get_activities(db_path, limit=5, offset=0)
161
+ if recent:
162
+ # Broadcast each new activity
163
+ for activity in recent:
164
+ await self.ws_manager.broadcast_activity_logged(
165
+ db_path,
166
+ activity if isinstance(activity, dict) else activity.model_dump()
167
+ )
168
+
169
+ # Also broadcast session update
170
+ sessions = get_recent_sessions(db_path, limit=1)
171
+ if sessions:
172
+ session = sessions[0]
173
+ await self.ws_manager.broadcast_session_updated(
174
+ db_path,
175
+ session if isinstance(session, dict) else dict(session)
176
+ )
177
+ except Exception as e:
178
+ print(f"[WS] Error broadcasting activities: {e}")
152
179
 
153
180
 
154
181
  # File watcher
@@ -77,6 +77,28 @@ class WebSocketManager:
77
77
  """Get the number of active connections."""
78
78
  return len(self.connections)
79
79
 
80
+ # Typed broadcast methods (IndyDevDan pattern)
81
+ async def broadcast_activity_logged(self, project: str, activity: dict[str, Any]):
82
+ """Broadcast when a new activity is logged."""
83
+ await self.broadcast("activity_logged", {
84
+ "project": project,
85
+ "activity": activity,
86
+ })
87
+
88
+ async def broadcast_session_updated(self, project: str, session: dict[str, Any]):
89
+ """Broadcast when a session is updated."""
90
+ await self.broadcast("session_updated", {
91
+ "project": project,
92
+ "session": session,
93
+ })
94
+
95
+ async def broadcast_stats_updated(self, project: str, stats: dict[str, Any]):
96
+ """Broadcast when stats change (for charts/panels)."""
97
+ await self.broadcast("stats_updated", {
98
+ "project": project,
99
+ "stats": stats,
100
+ })
101
+
80
102
 
81
103
  # Global manager instance
82
104
  manager = WebSocketManager()
@@ -0,0 +1,379 @@
1
+ #!/usr/bin/env python3
2
+ """PostToolUse hook - logs tool result after execution.
3
+
4
+ This hook is called by Claude Code after each tool completes.
5
+ It logs the tool output, duration, and success/error status.
6
+
7
+ Hook configuration for settings.json:
8
+ {
9
+ "hooks": {
10
+ "PostToolUse": [
11
+ {
12
+ "type": "command",
13
+ "command": "python hooks/post_tool_use.py"
14
+ }
15
+ ]
16
+ }
17
+ }
18
+ """
19
+
20
+ import json
21
+ import re
22
+ import sys
23
+ import os
24
+ import sqlite3
25
+ from datetime import datetime, timezone
26
+ from pathlib import Path
27
+
28
+ # Import shared session management
29
+ from session_utils import get_or_create_session
30
+
31
+
32
+ # Patterns for sensitive field names that should be redacted
33
+ SENSITIVE_FIELD_PATTERNS = [
34
+ r'(?i)(api[_-]?key|apikey)',
35
+ r'(?i)(password|passwd|pwd)',
36
+ r'(?i)(secret|token|credential)',
37
+ r'(?i)(auth[_-]?token|access[_-]?token)',
38
+ r'(?i)(private[_-]?key|ssh[_-]?key)',
39
+ ]
40
+
41
+
42
+ def redact_sensitive_fields(data: dict) -> dict:
43
+ """Redact sensitive fields from a dictionary for safe logging.
44
+
45
+ Recursively processes nested dicts and lists.
46
+ """
47
+ if not isinstance(data, dict):
48
+ return data
49
+
50
+ result = {}
51
+ for key, value in data.items():
52
+ # Check if key matches sensitive patterns
53
+ is_sensitive = any(
54
+ re.search(pattern, str(key))
55
+ for pattern in SENSITIVE_FIELD_PATTERNS
56
+ )
57
+
58
+ if is_sensitive:
59
+ result[key] = '[REDACTED]'
60
+ elif isinstance(value, dict):
61
+ result[key] = redact_sensitive_fields(value)
62
+ elif isinstance(value, list):
63
+ result[key] = [
64
+ redact_sensitive_fields(item) if isinstance(item, dict) else item
65
+ for item in value
66
+ ]
67
+ else:
68
+ result[key] = value
69
+
70
+ return result
71
+
72
+
73
+ def get_db_path() -> Path:
74
+ """Get the database path for the current project."""
75
+ project_path = os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd())
76
+ return Path(project_path) / ".omni-cortex" / "cortex.db"
77
+
78
+
79
+ def ensure_database(db_path: Path) -> sqlite3.Connection:
80
+ """Ensure database exists and is initialized.
81
+
82
+ Auto-creates the database and schema if it doesn't exist.
83
+ This enables 'out of the box' functionality.
84
+ """
85
+ db_path.parent.mkdir(parents=True, exist_ok=True)
86
+ conn = sqlite3.connect(str(db_path))
87
+
88
+ # Check if schema exists
89
+ cursor = conn.cursor()
90
+ cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='activities'")
91
+ if cursor.fetchone() is None:
92
+ # Apply minimal schema for activities (full schema applied by MCP)
93
+ conn.executescript("""
94
+ CREATE TABLE IF NOT EXISTS activities (
95
+ id TEXT PRIMARY KEY,
96
+ session_id TEXT,
97
+ agent_id TEXT,
98
+ timestamp TEXT NOT NULL,
99
+ event_type TEXT NOT NULL,
100
+ tool_name TEXT,
101
+ tool_input TEXT,
102
+ tool_output TEXT,
103
+ duration_ms INTEGER,
104
+ success INTEGER DEFAULT 1,
105
+ error_message TEXT,
106
+ project_path TEXT,
107
+ file_path TEXT,
108
+ metadata TEXT
109
+ );
110
+ CREATE INDEX IF NOT EXISTS idx_activities_timestamp ON activities(timestamp DESC);
111
+ CREATE INDEX IF NOT EXISTS idx_activities_tool ON activities(tool_name);
112
+ """)
113
+ conn.commit()
114
+
115
+ return conn
116
+
117
+
118
+ def generate_id() -> str:
119
+ """Generate a unique activity ID."""
120
+ timestamp_ms = int(datetime.now().timestamp() * 1000)
121
+ random_hex = os.urandom(4).hex()
122
+ return f"act_{timestamp_ms}_{random_hex}"
123
+
124
+
125
+ def truncate(text: str, max_length: int = 10000) -> str:
126
+ """Truncate text to max length."""
127
+ if len(text) <= max_length:
128
+ return text
129
+ return text[:max_length - 20] + "\n... [truncated]"
130
+
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
+
270
+ def main():
271
+ """Process PostToolUse hook."""
272
+ try:
273
+ # Read all input at once (more reliable than json.load on stdin)
274
+ raw_input = sys.stdin.read()
275
+ if not raw_input or not raw_input.strip():
276
+ print(json.dumps({}))
277
+ return
278
+
279
+ input_data = json.loads(raw_input)
280
+
281
+ # Extract data from hook input
282
+ tool_name = input_data.get("tool_name")
283
+ tool_input = input_data.get("tool_input", {})
284
+ tool_output = input_data.get("tool_output", {})
285
+ agent_id = input_data.get("agent_id")
286
+
287
+ # Determine success/error
288
+ is_error = input_data.get("is_error", False)
289
+ error_message = None
290
+ if is_error and isinstance(tool_output, dict):
291
+ error_message = tool_output.get("error") or tool_output.get("message")
292
+
293
+ # Skip logging our own tools to prevent recursion
294
+ # MCP tools are named like "mcp__omni-cortex__cortex_remember"
295
+ if tool_name and ("cortex_" in tool_name or "omni-cortex" in tool_name):
296
+ print(json.dumps({}))
297
+ return
298
+
299
+ project_path = os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd())
300
+
301
+ # Auto-initialize database (creates if not exists)
302
+ db_path = get_db_path()
303
+ conn = ensure_database(db_path)
304
+
305
+ # Ensure analytics columns exist
306
+ ensure_analytics_columns(conn)
307
+
308
+ # Get or create session (auto-manages session lifecycle)
309
+ session_id = get_or_create_session(conn, project_path)
310
+
311
+ # Redact sensitive fields before logging
312
+ safe_input = redact_sensitive_fields(tool_input) if isinstance(tool_input, dict) else tool_input
313
+ safe_output = redact_sensitive_fields(tool_output) if isinstance(tool_output, dict) else tool_output
314
+
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
337
+ cursor = conn.cursor()
338
+ cursor.execute(
339
+ """
340
+ INSERT INTO activities (
341
+ id, session_id, agent_id, timestamp, event_type,
342
+ tool_name, tool_input, tool_output, success, error_message, project_path,
343
+ skill_name, command_scope, mcp_server, summary, summary_detail
344
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
345
+ """,
346
+ (
347
+ generate_id(),
348
+ session_id,
349
+ agent_id,
350
+ datetime.now(timezone.utc).isoformat(),
351
+ "post_tool_use",
352
+ tool_name,
353
+ truncate(json.dumps(safe_input, default=str)),
354
+ truncate(json.dumps(safe_output, default=str)),
355
+ 0 if is_error else 1,
356
+ error_message,
357
+ project_path,
358
+ skill_name,
359
+ command_scope,
360
+ mcp_server,
361
+ summary,
362
+ summary_detail,
363
+ ),
364
+ )
365
+ conn.commit()
366
+ conn.close()
367
+
368
+ # Return empty response (no modification)
369
+ print(json.dumps({}))
370
+
371
+ except Exception as e:
372
+ # Hooks should never block - log error but continue
373
+ print(json.dumps({"systemMessage": f"Cortex post_tool_use: {e}"}))
374
+
375
+ sys.exit(0)
376
+
377
+
378
+ if __name__ == "__main__":
379
+ main()
@@ -25,6 +25,9 @@ import sqlite3
25
25
  from datetime import datetime, timezone
26
26
  from pathlib import Path
27
27
 
28
+ # Import shared session management
29
+ from session_utils import get_or_create_session
30
+
28
31
 
29
32
  # Patterns for sensitive field names that should be redacted
30
33
  SENSITIVE_FIELD_PATTERNS = [
@@ -157,13 +160,15 @@ def main():
157
160
  print(json.dumps({}))
158
161
  return
159
162
 
160
- session_id = os.environ.get("CLAUDE_SESSION_ID")
161
163
  project_path = os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd())
162
164
 
163
165
  # Auto-initialize database (creates if not exists)
164
166
  db_path = get_db_path()
165
167
  conn = ensure_database(db_path)
166
168
 
169
+ # Get or create session (auto-manages session lifecycle)
170
+ session_id = get_or_create_session(conn, project_path)
171
+
167
172
  # Redact sensitive fields before logging
168
173
  safe_input = redact_sensitive_fields(tool_input) if isinstance(tool_input, dict) else tool_input
169
174