omni-cortex 1.0.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,82 @@
1
+ """WebSocket manager for real-time updates."""
2
+
3
+ import asyncio
4
+ import json
5
+ from datetime import datetime
6
+ from typing import Any
7
+ from uuid import uuid4
8
+
9
+ from fastapi import WebSocket
10
+
11
+
12
+ class WebSocketManager:
13
+ """Manages WebSocket connections and broadcasts."""
14
+
15
+ def __init__(self):
16
+ self.connections: dict[str, WebSocket] = {}
17
+ self._lock = asyncio.Lock()
18
+
19
+ async def connect(self, websocket: WebSocket, client_id: str | None = None) -> str:
20
+ """Accept a new WebSocket connection."""
21
+ await websocket.accept()
22
+ client_id = client_id or str(uuid4())
23
+ async with self._lock:
24
+ self.connections[client_id] = websocket
25
+ print(f"[WS] Client connected: {client_id} (total: {len(self.connections)})")
26
+ return client_id
27
+
28
+ async def disconnect(self, client_id: str):
29
+ """Remove a WebSocket connection."""
30
+ async with self._lock:
31
+ if client_id in self.connections:
32
+ del self.connections[client_id]
33
+ print(f"[WS] Client disconnected: {client_id} (total: {len(self.connections)})")
34
+
35
+ async def broadcast(self, event_type: str, data: dict[str, Any]):
36
+ """Broadcast a message to all connected clients."""
37
+ if not self.connections:
38
+ return
39
+
40
+ message = json.dumps({
41
+ "event_type": event_type,
42
+ "data": data,
43
+ "timestamp": datetime.now().isoformat(),
44
+ })
45
+
46
+ disconnected = []
47
+ async with self._lock:
48
+ for client_id, websocket in self.connections.items():
49
+ try:
50
+ await websocket.send_text(message)
51
+ except Exception as e:
52
+ print(f"[WS] Failed to send to {client_id}: {e}")
53
+ disconnected.append(client_id)
54
+
55
+ # Clean up disconnected clients
56
+ for client_id in disconnected:
57
+ del self.connections[client_id]
58
+
59
+ async def send_to_client(self, client_id: str, event_type: str, data: dict[str, Any]):
60
+ """Send a message to a specific client."""
61
+ message = json.dumps({
62
+ "event_type": event_type,
63
+ "data": data,
64
+ "timestamp": datetime.now().isoformat(),
65
+ })
66
+
67
+ async with self._lock:
68
+ if client_id in self.connections:
69
+ try:
70
+ await self.connections[client_id].send_text(message)
71
+ except Exception as e:
72
+ print(f"[WS] Failed to send to {client_id}: {e}")
73
+ del self.connections[client_id]
74
+
75
+ @property
76
+ def connection_count(self) -> int:
77
+ """Get the number of active connections."""
78
+ return len(self.connections)
79
+
80
+
81
+ # Global manager instance
82
+ manager = WebSocketManager()
@@ -0,0 +1,160 @@
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 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 ensure_database(db_path: Path) -> sqlite3.Connection:
35
+ """Ensure database exists and is initialized.
36
+
37
+ Auto-creates the database and schema if it doesn't exist.
38
+ This enables 'out of the box' functionality.
39
+ """
40
+ db_path.parent.mkdir(parents=True, exist_ok=True)
41
+ conn = sqlite3.connect(str(db_path))
42
+
43
+ # Check if schema exists
44
+ cursor = conn.cursor()
45
+ cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='activities'")
46
+ if cursor.fetchone() is None:
47
+ # Apply minimal schema for activities (full schema applied by MCP)
48
+ conn.executescript("""
49
+ CREATE TABLE IF NOT EXISTS activities (
50
+ id TEXT PRIMARY KEY,
51
+ session_id TEXT,
52
+ agent_id TEXT,
53
+ timestamp TEXT NOT NULL,
54
+ event_type TEXT NOT NULL,
55
+ tool_name TEXT,
56
+ tool_input TEXT,
57
+ tool_output TEXT,
58
+ duration_ms INTEGER,
59
+ success INTEGER DEFAULT 1,
60
+ error_message TEXT,
61
+ project_path TEXT,
62
+ file_path TEXT,
63
+ metadata TEXT
64
+ );
65
+ CREATE INDEX IF NOT EXISTS idx_activities_timestamp ON activities(timestamp DESC);
66
+ CREATE INDEX IF NOT EXISTS idx_activities_tool ON activities(tool_name);
67
+ """)
68
+ conn.commit()
69
+
70
+ return conn
71
+
72
+
73
+ def generate_id() -> str:
74
+ """Generate a unique activity ID."""
75
+ timestamp_ms = int(datetime.now().timestamp() * 1000)
76
+ random_hex = os.urandom(4).hex()
77
+ return f"act_{timestamp_ms}_{random_hex}"
78
+
79
+
80
+ def truncate(text: str, max_length: int = 10000) -> str:
81
+ """Truncate text to max length."""
82
+ if len(text) <= max_length:
83
+ return text
84
+ return text[:max_length - 20] + "\n... [truncated]"
85
+
86
+
87
+ def main():
88
+ """Process PostToolUse hook."""
89
+ try:
90
+ # Read all input at once (more reliable than json.load on stdin)
91
+ raw_input = sys.stdin.read()
92
+ if not raw_input or not raw_input.strip():
93
+ print(json.dumps({}))
94
+ return
95
+
96
+ input_data = json.loads(raw_input)
97
+
98
+ # Extract data from hook input
99
+ tool_name = input_data.get("tool_name")
100
+ tool_input = input_data.get("tool_input", {})
101
+ tool_output = input_data.get("tool_output", {})
102
+ agent_id = input_data.get("agent_id")
103
+
104
+ # Determine success/error
105
+ is_error = input_data.get("is_error", False)
106
+ error_message = None
107
+ if is_error and isinstance(tool_output, dict):
108
+ error_message = tool_output.get("error") or tool_output.get("message")
109
+
110
+ # Skip logging our own tools to prevent recursion
111
+ # MCP tools are named like "mcp__omni-cortex__cortex_remember"
112
+ if tool_name and ("cortex_" in tool_name or "omni-cortex" in tool_name):
113
+ print(json.dumps({}))
114
+ return
115
+
116
+ session_id = os.environ.get("CLAUDE_SESSION_ID")
117
+ project_path = os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd())
118
+
119
+ # Auto-initialize database (creates if not exists)
120
+ db_path = get_db_path()
121
+ conn = ensure_database(db_path)
122
+
123
+ # Insert activity record
124
+ cursor = conn.cursor()
125
+ cursor.execute(
126
+ """
127
+ INSERT INTO activities (
128
+ id, session_id, agent_id, timestamp, event_type,
129
+ tool_name, tool_input, tool_output, success, error_message, project_path
130
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
131
+ """,
132
+ (
133
+ generate_id(),
134
+ session_id,
135
+ agent_id,
136
+ datetime.now(timezone.utc).isoformat(),
137
+ "post_tool_use",
138
+ tool_name,
139
+ truncate(json.dumps(tool_input, default=str)),
140
+ truncate(json.dumps(tool_output, default=str)),
141
+ 0 if is_error else 1,
142
+ error_message,
143
+ project_path,
144
+ ),
145
+ )
146
+ conn.commit()
147
+ conn.close()
148
+
149
+ # Return empty response (no modification)
150
+ print(json.dumps({}))
151
+
152
+ except Exception as e:
153
+ # Hooks should never block - log error but continue
154
+ print(json.dumps({"systemMessage": f"Cortex post_tool_use: {e}"}))
155
+
156
+ sys.exit(0)
157
+
158
+
159
+ if __name__ == "__main__":
160
+ main()
@@ -0,0 +1,159 @@
1
+ #!/usr/bin/env python3
2
+ """PreToolUse hook - logs tool call before execution.
3
+
4
+ This hook is called by Claude Code before each tool is executed.
5
+ It logs the tool name and input to the Cortex activity database.
6
+
7
+ Hook configuration for settings.json:
8
+ {
9
+ "hooks": {
10
+ "PreToolUse": [
11
+ {
12
+ "type": "command",
13
+ "command": "python hooks/pre_tool_use.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 ensure_database(db_path: Path) -> sqlite3.Connection:
35
+ """Ensure database exists and is initialized.
36
+
37
+ Auto-creates the database and schema if it doesn't exist.
38
+ This enables 'out of the box' functionality.
39
+ """
40
+ db_path.parent.mkdir(parents=True, exist_ok=True)
41
+ conn = sqlite3.connect(str(db_path))
42
+
43
+ # Check if schema exists
44
+ cursor = conn.cursor()
45
+ cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='activities'")
46
+ if cursor.fetchone() is None:
47
+ # Apply minimal schema for activities (full schema applied by MCP)
48
+ conn.executescript("""
49
+ CREATE TABLE IF NOT EXISTS activities (
50
+ id TEXT PRIMARY KEY,
51
+ session_id TEXT,
52
+ agent_id TEXT,
53
+ timestamp TEXT NOT NULL,
54
+ event_type TEXT NOT NULL,
55
+ tool_name TEXT,
56
+ tool_input TEXT,
57
+ tool_output TEXT,
58
+ duration_ms INTEGER,
59
+ success INTEGER DEFAULT 1,
60
+ error_message TEXT,
61
+ project_path TEXT,
62
+ file_path TEXT,
63
+ metadata TEXT
64
+ );
65
+ CREATE INDEX IF NOT EXISTS idx_activities_timestamp ON activities(timestamp DESC);
66
+ CREATE INDEX IF NOT EXISTS idx_activities_tool ON activities(tool_name);
67
+ """)
68
+ conn.commit()
69
+
70
+ return conn
71
+
72
+
73
+ def generate_id() -> str:
74
+ """Generate a unique activity ID."""
75
+ timestamp_ms = int(datetime.now().timestamp() * 1000)
76
+ random_hex = os.urandom(4).hex()
77
+ return f"act_{timestamp_ms}_{random_hex}"
78
+
79
+
80
+ def truncate(text: str, max_length: int = 10000) -> str:
81
+ """Truncate text to max length."""
82
+ if len(text) <= max_length:
83
+ return text
84
+ return text[:max_length - 20] + "\n... [truncated]"
85
+
86
+
87
+ def main():
88
+ """Process PreToolUse hook."""
89
+ try:
90
+ # Read input from stdin with timeout protection
91
+ import select
92
+ if sys.platform != "win32":
93
+ # Unix: use select for timeout
94
+ ready, _, _ = select.select([sys.stdin], [], [], 5.0)
95
+ if not ready:
96
+ print(json.dumps({}))
97
+ return
98
+
99
+ # Read all input at once
100
+ raw_input = sys.stdin.read()
101
+ if not raw_input or not raw_input.strip():
102
+ print(json.dumps({}))
103
+ return
104
+
105
+ input_data = json.loads(raw_input)
106
+
107
+ # Extract data from hook input
108
+ tool_name = input_data.get("tool_name")
109
+ tool_input = input_data.get("tool_input", {})
110
+ agent_id = input_data.get("agent_id")
111
+
112
+ # Skip logging our own tools to prevent recursion
113
+ # MCP tools are named like "mcp__omni-cortex__cortex_remember"
114
+ if tool_name and ("cortex_" in tool_name or "omni-cortex" in tool_name):
115
+ print(json.dumps({}))
116
+ return
117
+
118
+ session_id = os.environ.get("CLAUDE_SESSION_ID")
119
+ project_path = os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd())
120
+
121
+ # Auto-initialize database (creates if not exists)
122
+ db_path = get_db_path()
123
+ conn = ensure_database(db_path)
124
+
125
+ # Insert activity record
126
+ cursor = conn.cursor()
127
+ cursor.execute(
128
+ """
129
+ INSERT INTO activities (
130
+ id, session_id, agent_id, timestamp, event_type,
131
+ tool_name, tool_input, project_path
132
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
133
+ """,
134
+ (
135
+ generate_id(),
136
+ session_id,
137
+ agent_id,
138
+ datetime.now(timezone.utc).isoformat(),
139
+ "pre_tool_use",
140
+ tool_name,
141
+ truncate(json.dumps(tool_input, default=str)),
142
+ project_path,
143
+ ),
144
+ )
145
+ conn.commit()
146
+ conn.close()
147
+
148
+ # Return empty response (no modification to tool call)
149
+ print(json.dumps({}))
150
+
151
+ except Exception as e:
152
+ # Hooks should never block - log error but continue
153
+ print(json.dumps({"systemMessage": f"Cortex pre_tool_use: {e}"}))
154
+
155
+ sys.exit(0)
156
+
157
+
158
+ if __name__ == "__main__":
159
+ main()
@@ -0,0 +1,184 @@
1
+ #!/usr/bin/env python3
2
+ """Stop hook - logs session end when Claude Code stops.
3
+
4
+ This hook is called when Claude Code exits or the session ends.
5
+ It finalizes the session and generates a summary.
6
+
7
+ Hook configuration for settings.json:
8
+ {
9
+ "hooks": {
10
+ "Stop": [
11
+ {
12
+ "type": "command",
13
+ "command": "python hooks/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(prefix: str) -> str:
35
+ """Generate a unique ID."""
36
+ timestamp_ms = int(datetime.now().timestamp() * 1000)
37
+ random_hex = os.urandom(4).hex()
38
+ return f"{prefix}_{timestamp_ms}_{random_hex}"
39
+
40
+
41
+ def main():
42
+ """Process Stop hook."""
43
+ try:
44
+ # Read input from stdin
45
+ input_data = json.load(sys.stdin)
46
+
47
+ db_path = get_db_path()
48
+
49
+ # Only process if database exists
50
+ if not db_path.exists():
51
+ print(json.dumps({}))
52
+ return
53
+
54
+ session_id = os.environ.get("CLAUDE_SESSION_ID")
55
+ if not session_id:
56
+ print(json.dumps({}))
57
+ return
58
+
59
+ now = datetime.now(timezone.utc).isoformat()
60
+
61
+ # Connect to database
62
+ conn = sqlite3.connect(str(db_path))
63
+ conn.row_factory = sqlite3.Row
64
+ cursor = conn.cursor()
65
+
66
+ # Check if session exists
67
+ cursor.execute("SELECT id FROM sessions WHERE id = ?", (session_id,))
68
+ if not cursor.fetchone():
69
+ print(json.dumps({}))
70
+ conn.close()
71
+ return
72
+
73
+ # End the session
74
+ cursor.execute(
75
+ "UPDATE sessions SET ended_at = ? WHERE id = ? AND ended_at IS NULL",
76
+ (now, session_id),
77
+ )
78
+
79
+ # Gather session statistics
80
+ cursor.execute(
81
+ "SELECT COUNT(*) as cnt FROM activities WHERE session_id = ?",
82
+ (session_id,),
83
+ )
84
+ total_activities = cursor.fetchone()["cnt"]
85
+
86
+ cursor.execute(
87
+ "SELECT COUNT(*) as cnt FROM memories WHERE source_session_id = ?",
88
+ (session_id,),
89
+ )
90
+ total_memories = cursor.fetchone()["cnt"]
91
+
92
+ # Get tools used
93
+ cursor.execute(
94
+ """
95
+ SELECT tool_name, COUNT(*) as cnt
96
+ FROM activities
97
+ WHERE session_id = ? AND tool_name IS NOT NULL
98
+ GROUP BY tool_name
99
+ """,
100
+ (session_id,),
101
+ )
102
+ tools_used = {row["tool_name"]: row["cnt"] for row in cursor.fetchall()}
103
+
104
+ # Get files modified
105
+ cursor.execute(
106
+ """
107
+ SELECT DISTINCT file_path
108
+ FROM activities
109
+ WHERE session_id = ? AND file_path IS NOT NULL
110
+ """,
111
+ (session_id,),
112
+ )
113
+ files_modified = [row["file_path"] for row in cursor.fetchall()]
114
+
115
+ # Get errors
116
+ cursor.execute(
117
+ """
118
+ SELECT error_message
119
+ FROM activities
120
+ WHERE session_id = ? AND success = 0 AND error_message IS NOT NULL
121
+ LIMIT 10
122
+ """,
123
+ (session_id,),
124
+ )
125
+ key_errors = [row["error_message"] for row in cursor.fetchall()]
126
+
127
+ # Create or update summary
128
+ cursor.execute(
129
+ "SELECT id FROM session_summaries WHERE session_id = ?",
130
+ (session_id,),
131
+ )
132
+ existing = cursor.fetchone()
133
+
134
+ if existing:
135
+ cursor.execute(
136
+ """
137
+ UPDATE session_summaries
138
+ SET key_errors = ?, files_modified = ?, tools_used = ?,
139
+ total_activities = ?, total_memories_created = ?
140
+ WHERE session_id = ?
141
+ """,
142
+ (
143
+ json.dumps(key_errors) if key_errors else None,
144
+ json.dumps(files_modified) if files_modified else None,
145
+ json.dumps(tools_used) if tools_used else None,
146
+ total_activities,
147
+ total_memories,
148
+ session_id,
149
+ ),
150
+ )
151
+ else:
152
+ cursor.execute(
153
+ """
154
+ INSERT INTO session_summaries (
155
+ id, session_id, key_errors, files_modified, tools_used,
156
+ total_activities, total_memories_created, created_at
157
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
158
+ """,
159
+ (
160
+ generate_id("sum"),
161
+ session_id,
162
+ json.dumps(key_errors) if key_errors else None,
163
+ json.dumps(files_modified) if files_modified else None,
164
+ json.dumps(tools_used) if tools_used else None,
165
+ total_activities,
166
+ total_memories,
167
+ now,
168
+ ),
169
+ )
170
+
171
+ conn.commit()
172
+ conn.close()
173
+
174
+ print(json.dumps({}))
175
+
176
+ except Exception as e:
177
+ # Hooks should never block
178
+ print(json.dumps({"systemMessage": f"Cortex stop: {e}"}))
179
+
180
+ sys.exit(0)
181
+
182
+
183
+ if __name__ == "__main__":
184
+ main()
@@ -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()