omni-cortex 1.0.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.
- omni_cortex-1.0.0.data/data/share/omni-cortex/hooks/post_tool_use.py +154 -0
- omni_cortex-1.0.0.data/data/share/omni-cortex/hooks/pre_tool_use.py +144 -0
- omni_cortex-1.0.0.data/data/share/omni-cortex/hooks/stop.py +184 -0
- omni_cortex-1.0.0.data/data/share/omni-cortex/hooks/subagent_stop.py +120 -0
- omni_cortex-1.0.0.dist-info/METADATA +216 -0
- omni_cortex-1.0.0.dist-info/RECORD +9 -0
- omni_cortex-1.0.0.dist-info/WHEEL +4 -0
- omni_cortex-1.0.0.dist-info/entry_points.txt +3 -0
- omni_cortex-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,154 @@
|
|
|
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 input from stdin
|
|
91
|
+
input_data = json.load(sys.stdin)
|
|
92
|
+
|
|
93
|
+
# Extract data from hook input
|
|
94
|
+
tool_name = input_data.get("tool_name")
|
|
95
|
+
tool_input = input_data.get("tool_input", {})
|
|
96
|
+
tool_output = input_data.get("tool_output", {})
|
|
97
|
+
agent_id = input_data.get("agent_id")
|
|
98
|
+
|
|
99
|
+
# Determine success/error
|
|
100
|
+
is_error = input_data.get("is_error", False)
|
|
101
|
+
error_message = None
|
|
102
|
+
if is_error and isinstance(tool_output, dict):
|
|
103
|
+
error_message = tool_output.get("error") or tool_output.get("message")
|
|
104
|
+
|
|
105
|
+
# Skip logging our own tools to prevent recursion
|
|
106
|
+
if tool_name and tool_name.startswith("cortex_"):
|
|
107
|
+
print(json.dumps({}))
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
session_id = os.environ.get("CLAUDE_SESSION_ID")
|
|
111
|
+
project_path = os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd())
|
|
112
|
+
|
|
113
|
+
# Auto-initialize database (creates if not exists)
|
|
114
|
+
db_path = get_db_path()
|
|
115
|
+
conn = ensure_database(db_path)
|
|
116
|
+
|
|
117
|
+
# Insert activity record
|
|
118
|
+
cursor = conn.cursor()
|
|
119
|
+
cursor.execute(
|
|
120
|
+
"""
|
|
121
|
+
INSERT INTO activities (
|
|
122
|
+
id, session_id, agent_id, timestamp, event_type,
|
|
123
|
+
tool_name, tool_input, tool_output, success, error_message, project_path
|
|
124
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
125
|
+
""",
|
|
126
|
+
(
|
|
127
|
+
generate_id(),
|
|
128
|
+
session_id,
|
|
129
|
+
agent_id,
|
|
130
|
+
datetime.now(timezone.utc).isoformat(),
|
|
131
|
+
"post_tool_use",
|
|
132
|
+
tool_name,
|
|
133
|
+
truncate(json.dumps(tool_input, default=str)),
|
|
134
|
+
truncate(json.dumps(tool_output, default=str)),
|
|
135
|
+
0 if is_error else 1,
|
|
136
|
+
error_message,
|
|
137
|
+
project_path,
|
|
138
|
+
),
|
|
139
|
+
)
|
|
140
|
+
conn.commit()
|
|
141
|
+
conn.close()
|
|
142
|
+
|
|
143
|
+
# Return empty response (no modification)
|
|
144
|
+
print(json.dumps({}))
|
|
145
|
+
|
|
146
|
+
except Exception as e:
|
|
147
|
+
# Hooks should never block - log error but continue
|
|
148
|
+
print(json.dumps({"systemMessage": f"Cortex post_tool_use: {e}"}))
|
|
149
|
+
|
|
150
|
+
sys.exit(0)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
if __name__ == "__main__":
|
|
154
|
+
main()
|
|
@@ -0,0 +1,144 @@
|
|
|
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
|
|
91
|
+
input_data = json.load(sys.stdin)
|
|
92
|
+
|
|
93
|
+
# Extract data from hook input
|
|
94
|
+
tool_name = input_data.get("tool_name")
|
|
95
|
+
tool_input = input_data.get("tool_input", {})
|
|
96
|
+
agent_id = input_data.get("agent_id")
|
|
97
|
+
|
|
98
|
+
# Skip logging our own tools to prevent recursion
|
|
99
|
+
if tool_name and tool_name.startswith("cortex_"):
|
|
100
|
+
print(json.dumps({}))
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
session_id = os.environ.get("CLAUDE_SESSION_ID")
|
|
104
|
+
project_path = os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd())
|
|
105
|
+
|
|
106
|
+
# Auto-initialize database (creates if not exists)
|
|
107
|
+
db_path = get_db_path()
|
|
108
|
+
conn = ensure_database(db_path)
|
|
109
|
+
|
|
110
|
+
# Insert activity record
|
|
111
|
+
cursor = conn.cursor()
|
|
112
|
+
cursor.execute(
|
|
113
|
+
"""
|
|
114
|
+
INSERT INTO activities (
|
|
115
|
+
id, session_id, agent_id, timestamp, event_type,
|
|
116
|
+
tool_name, tool_input, project_path
|
|
117
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
118
|
+
""",
|
|
119
|
+
(
|
|
120
|
+
generate_id(),
|
|
121
|
+
session_id,
|
|
122
|
+
agent_id,
|
|
123
|
+
datetime.now(timezone.utc).isoformat(),
|
|
124
|
+
"pre_tool_use",
|
|
125
|
+
tool_name,
|
|
126
|
+
truncate(json.dumps(tool_input, default=str)),
|
|
127
|
+
project_path,
|
|
128
|
+
),
|
|
129
|
+
)
|
|
130
|
+
conn.commit()
|
|
131
|
+
conn.close()
|
|
132
|
+
|
|
133
|
+
# Return empty response (no modification to tool call)
|
|
134
|
+
print(json.dumps({}))
|
|
135
|
+
|
|
136
|
+
except Exception as e:
|
|
137
|
+
# Hooks should never block - log error but continue
|
|
138
|
+
print(json.dumps({"systemMessage": f"Cortex pre_tool_use: {e}"}))
|
|
139
|
+
|
|
140
|
+
sys.exit(0)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
if __name__ == "__main__":
|
|
144
|
+
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()
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: omni-cortex
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Universal Memory MCP for Claude Code - dual-layer activity logging and knowledge storage
|
|
5
|
+
Project-URL: Homepage, https://github.com/AllCytes/Omni-Cortex
|
|
6
|
+
Project-URL: Repository, https://github.com/AllCytes/Omni-Cortex
|
|
7
|
+
Project-URL: Issues, https://github.com/AllCytes/Omni-Cortex/issues
|
|
8
|
+
Project-URL: Documentation, https://github.com/AllCytes/Omni-Cortex#readme
|
|
9
|
+
Author: Tony
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: ai,anthropic,claude,claude-code,llm,mcp,memory,semantic-search
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Requires-Dist: httpx>=0.25.0
|
|
25
|
+
Requires-Dist: mcp>=1.0.0
|
|
26
|
+
Requires-Dist: pydantic>=2.0.0
|
|
27
|
+
Requires-Dist: pyyaml>=6.0.0
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: black>=23.0.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest>=7.0.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: ruff>=0.1.0; extra == 'dev'
|
|
33
|
+
Provides-Extra: semantic
|
|
34
|
+
Requires-Dist: numpy>=1.24.0; extra == 'semantic'
|
|
35
|
+
Requires-Dist: sentence-transformers>=2.2.0; extra == 'semantic'
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
|
|
38
|
+
# Omni Cortex MCP
|
|
39
|
+
|
|
40
|
+
A universal memory system for Claude Code that combines activity logging with intelligent knowledge storage.
|
|
41
|
+
|
|
42
|
+
## Features
|
|
43
|
+
|
|
44
|
+
- **Zero Configuration**: Works out of the box - just install and run setup
|
|
45
|
+
- **Dual-Layer Storage**: Activity logging (audit trail) + Knowledge store (memories)
|
|
46
|
+
- **15 MCP Tools**: Full-featured API for memory management, activity tracking, and session continuity
|
|
47
|
+
- **Semantic Search**: AI-powered search using sentence-transformers (optional)
|
|
48
|
+
- **Hybrid Search**: Combines keyword (FTS5) + semantic search for best results
|
|
49
|
+
- **Full-Text Search**: SQLite FTS5-powered keyword search with smart ranking
|
|
50
|
+
- **Auto-Categorization**: Automatic memory type detection and tag suggestions
|
|
51
|
+
- **Session Continuity**: "Last time you were working on..." context
|
|
52
|
+
- **Importance Decay**: Frequently accessed memories naturally surface
|
|
53
|
+
- **Auto Activity Logging**: Automatically logs all tool calls via hooks
|
|
54
|
+
|
|
55
|
+
## Installation
|
|
56
|
+
|
|
57
|
+
### Quick Install (Recommended)
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# Install the package
|
|
61
|
+
pip install omni-cortex
|
|
62
|
+
|
|
63
|
+
# Run automatic setup (configures MCP server + hooks)
|
|
64
|
+
omni-cortex-setup
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
That's it! Omni Cortex will now:
|
|
68
|
+
- Automatically log all Claude Code tool calls
|
|
69
|
+
- Provide memory tools (cortex_remember, cortex_recall, etc.)
|
|
70
|
+
- Create a per-project database at `.omni-cortex/cortex.db`
|
|
71
|
+
|
|
72
|
+
### With Semantic Search
|
|
73
|
+
|
|
74
|
+
For AI-powered semantic search capabilities:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
pip install omni-cortex[semantic]
|
|
78
|
+
omni-cortex-setup
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### From Source
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
git clone https://github.com/AllCytes/Omni-Cortex.git
|
|
85
|
+
cd omni-cortex
|
|
86
|
+
pip install -e ".[semantic]"
|
|
87
|
+
python -m omni_cortex.setup
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Manual Configuration
|
|
91
|
+
|
|
92
|
+
If you prefer manual setup, add to `~/.claude.json`:
|
|
93
|
+
|
|
94
|
+
```json
|
|
95
|
+
{
|
|
96
|
+
"mcpServers": {
|
|
97
|
+
"omni-cortex": {
|
|
98
|
+
"command": "python",
|
|
99
|
+
"args": ["-m", "omni_cortex.server"]
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
And optionally configure hooks in `~/.claude/settings.json` for activity logging:
|
|
106
|
+
|
|
107
|
+
```json
|
|
108
|
+
{
|
|
109
|
+
"hooks": {
|
|
110
|
+
"PreToolUse": [{
|
|
111
|
+
"type": "command",
|
|
112
|
+
"command": "python -m omni_cortex.hooks.pre_tool_use"
|
|
113
|
+
}],
|
|
114
|
+
"PostToolUse": [{
|
|
115
|
+
"type": "command",
|
|
116
|
+
"command": "python -m omni_cortex.hooks.post_tool_use"
|
|
117
|
+
}]
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Uninstall
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
omni-cortex-setup --uninstall
|
|
126
|
+
pip uninstall omni-cortex
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Tools
|
|
130
|
+
|
|
131
|
+
### Memory Tools (6)
|
|
132
|
+
|
|
133
|
+
| Tool | Description |
|
|
134
|
+
|------|-------------|
|
|
135
|
+
| `cortex_remember` | Store information with auto-categorization |
|
|
136
|
+
| `cortex_recall` | Search memories (modes: keyword, semantic, hybrid) |
|
|
137
|
+
| `cortex_list_memories` | List memories with filters and pagination |
|
|
138
|
+
| `cortex_update_memory` | Update memory content, tags, or status |
|
|
139
|
+
| `cortex_forget` | Delete a memory |
|
|
140
|
+
| `cortex_link_memories` | Create relationships between memories |
|
|
141
|
+
|
|
142
|
+
### Activity Tools (3)
|
|
143
|
+
|
|
144
|
+
| Tool | Description |
|
|
145
|
+
|------|-------------|
|
|
146
|
+
| `cortex_log_activity` | Manually log an activity |
|
|
147
|
+
| `cortex_get_activities` | Query the activity log |
|
|
148
|
+
| `cortex_get_timeline` | Get a chronological timeline |
|
|
149
|
+
|
|
150
|
+
### Session Tools (3)
|
|
151
|
+
|
|
152
|
+
| Tool | Description |
|
|
153
|
+
|------|-------------|
|
|
154
|
+
| `cortex_start_session` | Start a new session with context |
|
|
155
|
+
| `cortex_end_session` | End session and generate summary |
|
|
156
|
+
| `cortex_get_session_context` | Get context from previous sessions |
|
|
157
|
+
|
|
158
|
+
### Utility Tools (3)
|
|
159
|
+
|
|
160
|
+
| Tool | Description |
|
|
161
|
+
|------|-------------|
|
|
162
|
+
| `cortex_list_tags` | List all tags with usage counts |
|
|
163
|
+
| `cortex_review_memories` | Review and update memory freshness |
|
|
164
|
+
| `cortex_export` | Export data to markdown or JSON |
|
|
165
|
+
|
|
166
|
+
## Memory Types
|
|
167
|
+
|
|
168
|
+
Memories are automatically categorized into:
|
|
169
|
+
|
|
170
|
+
- `general` - General notes and information
|
|
171
|
+
- `warning` - Cautions, things to avoid
|
|
172
|
+
- `tip` - Tips, tricks, best practices
|
|
173
|
+
- `config` - Configuration and settings
|
|
174
|
+
- `troubleshooting` - Debugging and problem-solving
|
|
175
|
+
- `code` - Code snippets and algorithms
|
|
176
|
+
- `error` - Error messages and failures
|
|
177
|
+
- `solution` - Solutions to problems
|
|
178
|
+
- `command` - CLI commands
|
|
179
|
+
- `concept` - Definitions and explanations
|
|
180
|
+
- `decision` - Architectural decisions
|
|
181
|
+
|
|
182
|
+
## Storage
|
|
183
|
+
|
|
184
|
+
- **Per-project**: `.omni-cortex/cortex.db` in your project directory
|
|
185
|
+
- **Global**: `~/.omni-cortex/global.db` for cross-project search
|
|
186
|
+
|
|
187
|
+
## Configuration
|
|
188
|
+
|
|
189
|
+
Create `.omni-cortex/config.yaml` in your project:
|
|
190
|
+
|
|
191
|
+
```yaml
|
|
192
|
+
schema_version: "1.0"
|
|
193
|
+
embedding_enabled: true
|
|
194
|
+
decay_rate_per_day: 0.5
|
|
195
|
+
freshness_review_days: 30
|
|
196
|
+
auto_provide_context: true
|
|
197
|
+
context_depth: 3
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Development
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
# Install dev dependencies
|
|
204
|
+
pip install -e ".[dev]"
|
|
205
|
+
|
|
206
|
+
# Run tests
|
|
207
|
+
pytest
|
|
208
|
+
|
|
209
|
+
# Format code
|
|
210
|
+
black src tests
|
|
211
|
+
ruff check src tests
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## License
|
|
215
|
+
|
|
216
|
+
MIT
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
omni_cortex-1.0.0.data/data/share/omni-cortex/hooks/post_tool_use.py,sha256=J9NDtoPE4MbCacAX9XYhqRXF2Agaw_SRbik8U0jlovs,4828
|
|
2
|
+
omni_cortex-1.0.0.data/data/share/omni-cortex/hooks/pre_tool_use.py,sha256=jLkoleRSb_MJ0iy-uNwKVi8-gS49vlmLaF--riJoff0,4351
|
|
3
|
+
omni_cortex-1.0.0.data/data/share/omni-cortex/hooks/stop.py,sha256=T1bwcmbTLj0gzjrVvFBT1zB6wff4J2YkYBAY-ZxZI5g,5336
|
|
4
|
+
omni_cortex-1.0.0.data/data/share/omni-cortex/hooks/subagent_stop.py,sha256=V9HQSFGNOfkg8ZCstPEy4h5V8BP4AbrVr8teFzN1kNk,3314
|
|
5
|
+
omni_cortex-1.0.0.dist-info/METADATA,sha256=Q3zKLHnNw0YmcG9Kd2XnMjlKtEYcPFauZVJr931Nfus,6130
|
|
6
|
+
omni_cortex-1.0.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
7
|
+
omni_cortex-1.0.0.dist-info/entry_points.txt,sha256=eRePHG-7dXr4ZAHwI1l-GY5rgVrpa1wZPL3sYBeC5QU,99
|
|
8
|
+
omni_cortex-1.0.0.dist-info/licenses/LICENSE,sha256=oG_397owMmi-Umxp5sYocJ6RPohp9_bDNnnEu9OUphg,1072
|
|
9
|
+
omni_cortex-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Tony Simonovsky
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|