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.
- omni_cortex-1.0.4.data/data/share/omni-cortex/dashboard/backend/chat_service.py +140 -0
- omni_cortex-1.0.4.data/data/share/omni-cortex/dashboard/backend/database.py +729 -0
- omni_cortex-1.0.4.data/data/share/omni-cortex/dashboard/backend/main.py +661 -0
- omni_cortex-1.0.4.data/data/share/omni-cortex/dashboard/backend/models.py +140 -0
- omni_cortex-1.0.4.data/data/share/omni-cortex/dashboard/backend/project_scanner.py +141 -0
- omni_cortex-1.0.4.data/data/share/omni-cortex/dashboard/backend/pyproject.toml +23 -0
- omni_cortex-1.0.4.data/data/share/omni-cortex/dashboard/backend/uv.lock +697 -0
- omni_cortex-1.0.4.data/data/share/omni-cortex/dashboard/backend/websocket_manager.py +82 -0
- omni_cortex-1.0.4.data/data/share/omni-cortex/hooks/post_tool_use.py +160 -0
- omni_cortex-1.0.4.data/data/share/omni-cortex/hooks/pre_tool_use.py +159 -0
- omni_cortex-1.0.4.data/data/share/omni-cortex/hooks/stop.py +184 -0
- omni_cortex-1.0.4.data/data/share/omni-cortex/hooks/subagent_stop.py +120 -0
- omni_cortex-1.0.4.dist-info/METADATA +295 -0
- omni_cortex-1.0.4.dist-info/RECORD +17 -0
- omni_cortex-1.0.4.dist-info/WHEEL +4 -0
- omni_cortex-1.0.4.dist-info/entry_points.txt +4 -0
- omni_cortex-1.0.4.dist-info/licenses/LICENSE +21 -0
|
@@ -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()
|