stravinsky 0.2.67__py3-none-any.whl ā 0.4.66__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.
Potentially problematic release.
This version of stravinsky might be problematic. Click here for more details.
- mcp_bridge/__init__.py +1 -1
- mcp_bridge/auth/__init__.py +16 -6
- mcp_bridge/auth/cli.py +202 -11
- mcp_bridge/auth/oauth.py +1 -2
- mcp_bridge/auth/openai_oauth.py +4 -7
- mcp_bridge/auth/token_store.py +112 -11
- mcp_bridge/cli/__init__.py +1 -1
- mcp_bridge/cli/install_hooks.py +503 -107
- mcp_bridge/cli/session_report.py +0 -3
- mcp_bridge/config/MANIFEST_SCHEMA.md +305 -0
- mcp_bridge/config/README.md +276 -0
- mcp_bridge/config/__init__.py +2 -2
- mcp_bridge/config/hook_config.py +247 -0
- mcp_bridge/config/hooks_manifest.json +138 -0
- mcp_bridge/config/rate_limits.py +317 -0
- mcp_bridge/config/skills_manifest.json +128 -0
- mcp_bridge/hooks/HOOKS_SETTINGS.json +17 -4
- mcp_bridge/hooks/__init__.py +19 -4
- mcp_bridge/hooks/agent_reminder.py +4 -4
- mcp_bridge/hooks/auto_slash_command.py +5 -5
- mcp_bridge/hooks/budget_optimizer.py +2 -2
- mcp_bridge/hooks/claude_limits_hook.py +114 -0
- mcp_bridge/hooks/comment_checker.py +3 -4
- mcp_bridge/hooks/compaction.py +2 -2
- mcp_bridge/hooks/context.py +2 -1
- mcp_bridge/hooks/context_monitor.py +2 -2
- mcp_bridge/hooks/delegation_policy.py +85 -0
- mcp_bridge/hooks/directory_context.py +3 -3
- mcp_bridge/hooks/edit_recovery.py +3 -2
- mcp_bridge/hooks/edit_recovery_policy.py +49 -0
- mcp_bridge/hooks/empty_message_sanitizer.py +2 -2
- mcp_bridge/hooks/events.py +160 -0
- mcp_bridge/hooks/git_noninteractive.py +4 -4
- mcp_bridge/hooks/keyword_detector.py +8 -10
- mcp_bridge/hooks/manager.py +43 -22
- mcp_bridge/hooks/notification_hook.py +13 -6
- mcp_bridge/hooks/parallel_enforcement_policy.py +67 -0
- mcp_bridge/hooks/parallel_enforcer.py +5 -5
- mcp_bridge/hooks/parallel_execution.py +22 -10
- mcp_bridge/hooks/post_tool/parallel_validation.py +103 -0
- mcp_bridge/hooks/pre_compact.py +8 -9
- mcp_bridge/hooks/pre_tool/agent_spawn_validator.py +115 -0
- mcp_bridge/hooks/preemptive_compaction.py +2 -3
- mcp_bridge/hooks/routing_notifications.py +80 -0
- mcp_bridge/hooks/rules_injector.py +11 -19
- mcp_bridge/hooks/session_idle.py +4 -4
- mcp_bridge/hooks/session_notifier.py +4 -4
- mcp_bridge/hooks/session_recovery.py +4 -5
- mcp_bridge/hooks/stravinsky_mode.py +1 -1
- mcp_bridge/hooks/subagent_stop.py +1 -3
- mcp_bridge/hooks/task_validator.py +2 -2
- mcp_bridge/hooks/tmux_manager.py +7 -8
- mcp_bridge/hooks/todo_delegation.py +4 -1
- mcp_bridge/hooks/todo_enforcer.py +180 -10
- mcp_bridge/hooks/tool_messaging.py +113 -10
- mcp_bridge/hooks/truncation_policy.py +37 -0
- mcp_bridge/hooks/truncator.py +1 -2
- mcp_bridge/metrics/cost_tracker.py +115 -0
- mcp_bridge/native_search.py +93 -0
- mcp_bridge/native_watcher.py +118 -0
- mcp_bridge/notifications.py +150 -0
- mcp_bridge/orchestrator/enums.py +11 -0
- mcp_bridge/orchestrator/router.py +165 -0
- mcp_bridge/orchestrator/state.py +32 -0
- mcp_bridge/orchestrator/visualization.py +14 -0
- mcp_bridge/orchestrator/wisdom.py +34 -0
- mcp_bridge/prompts/__init__.py +1 -8
- mcp_bridge/prompts/dewey.py +1 -1
- mcp_bridge/prompts/planner.py +2 -4
- mcp_bridge/prompts/stravinsky.py +53 -31
- mcp_bridge/proxy/__init__.py +0 -0
- mcp_bridge/proxy/client.py +70 -0
- mcp_bridge/proxy/model_server.py +157 -0
- mcp_bridge/routing/__init__.py +43 -0
- mcp_bridge/routing/config.py +250 -0
- mcp_bridge/routing/model_tiers.py +135 -0
- mcp_bridge/routing/provider_state.py +261 -0
- mcp_bridge/routing/task_classifier.py +190 -0
- mcp_bridge/server.py +542 -59
- mcp_bridge/server_tools.py +738 -6
- mcp_bridge/tools/__init__.py +40 -25
- mcp_bridge/tools/agent_manager.py +616 -697
- mcp_bridge/tools/background_tasks.py +13 -17
- mcp_bridge/tools/code_search.py +70 -53
- mcp_bridge/tools/continuous_loop.py +0 -1
- mcp_bridge/tools/dashboard.py +19 -0
- mcp_bridge/tools/find_code.py +296 -0
- mcp_bridge/tools/init.py +1 -0
- mcp_bridge/tools/list_directory.py +42 -0
- mcp_bridge/tools/lsp/__init__.py +12 -5
- mcp_bridge/tools/lsp/manager.py +471 -0
- mcp_bridge/tools/lsp/tools.py +723 -207
- mcp_bridge/tools/model_invoke.py +1195 -273
- mcp_bridge/tools/mux_client.py +75 -0
- mcp_bridge/tools/project_context.py +1 -2
- mcp_bridge/tools/query_classifier.py +406 -0
- mcp_bridge/tools/read_file.py +84 -0
- mcp_bridge/tools/replace.py +45 -0
- mcp_bridge/tools/run_shell_command.py +38 -0
- mcp_bridge/tools/search_enhancements.py +347 -0
- mcp_bridge/tools/semantic_search.py +3627 -0
- mcp_bridge/tools/session_manager.py +0 -2
- mcp_bridge/tools/skill_loader.py +0 -1
- mcp_bridge/tools/task_runner.py +5 -7
- mcp_bridge/tools/templates.py +3 -3
- mcp_bridge/tools/tool_search.py +331 -0
- mcp_bridge/tools/write_file.py +29 -0
- mcp_bridge/update_manager.py +585 -0
- mcp_bridge/update_manager_pypi.py +297 -0
- mcp_bridge/utils/cache.py +82 -0
- mcp_bridge/utils/process.py +71 -0
- mcp_bridge/utils/session_state.py +51 -0
- mcp_bridge/utils/truncation.py +76 -0
- stravinsky-0.4.66.dist-info/METADATA +517 -0
- stravinsky-0.4.66.dist-info/RECORD +198 -0
- {stravinsky-0.2.67.dist-info ā stravinsky-0.4.66.dist-info}/entry_points.txt +1 -0
- stravinsky_claude_assets/HOOKS_INTEGRATION.md +316 -0
- stravinsky_claude_assets/agents/HOOKS.md +437 -0
- stravinsky_claude_assets/agents/code-reviewer.md +210 -0
- stravinsky_claude_assets/agents/comment_checker.md +580 -0
- stravinsky_claude_assets/agents/debugger.md +254 -0
- stravinsky_claude_assets/agents/delphi.md +495 -0
- stravinsky_claude_assets/agents/dewey.md +248 -0
- stravinsky_claude_assets/agents/explore.md +1198 -0
- stravinsky_claude_assets/agents/frontend.md +472 -0
- stravinsky_claude_assets/agents/implementation-lead.md +164 -0
- stravinsky_claude_assets/agents/momus.md +464 -0
- stravinsky_claude_assets/agents/research-lead.md +141 -0
- stravinsky_claude_assets/agents/stravinsky.md +730 -0
- stravinsky_claude_assets/commands/delphi.md +9 -0
- stravinsky_claude_assets/commands/dewey.md +54 -0
- stravinsky_claude_assets/commands/git-master.md +112 -0
- stravinsky_claude_assets/commands/index.md +49 -0
- stravinsky_claude_assets/commands/publish.md +86 -0
- stravinsky_claude_assets/commands/review.md +73 -0
- stravinsky_claude_assets/commands/str/agent_cancel.md +70 -0
- stravinsky_claude_assets/commands/str/agent_list.md +56 -0
- stravinsky_claude_assets/commands/str/agent_output.md +92 -0
- stravinsky_claude_assets/commands/str/agent_progress.md +74 -0
- stravinsky_claude_assets/commands/str/agent_retry.md +94 -0
- stravinsky_claude_assets/commands/str/cancel.md +51 -0
- stravinsky_claude_assets/commands/str/clean.md +97 -0
- stravinsky_claude_assets/commands/str/continue.md +38 -0
- stravinsky_claude_assets/commands/str/index.md +199 -0
- stravinsky_claude_assets/commands/str/list_watchers.md +96 -0
- stravinsky_claude_assets/commands/str/search.md +205 -0
- stravinsky_claude_assets/commands/str/start_filewatch.md +136 -0
- stravinsky_claude_assets/commands/str/stats.md +71 -0
- stravinsky_claude_assets/commands/str/stop_filewatch.md +89 -0
- stravinsky_claude_assets/commands/str/unwatch.md +42 -0
- stravinsky_claude_assets/commands/str/watch.md +45 -0
- stravinsky_claude_assets/commands/strav.md +53 -0
- stravinsky_claude_assets/commands/stravinsky.md +292 -0
- stravinsky_claude_assets/commands/verify.md +60 -0
- stravinsky_claude_assets/commands/version.md +5 -0
- stravinsky_claude_assets/hooks/README.md +248 -0
- stravinsky_claude_assets/hooks/comment_checker.py +193 -0
- stravinsky_claude_assets/hooks/context.py +38 -0
- stravinsky_claude_assets/hooks/context_monitor.py +153 -0
- stravinsky_claude_assets/hooks/dependency_tracker.py +73 -0
- stravinsky_claude_assets/hooks/edit_recovery.py +46 -0
- stravinsky_claude_assets/hooks/execution_state_tracker.py +68 -0
- stravinsky_claude_assets/hooks/notification_hook.py +103 -0
- stravinsky_claude_assets/hooks/notification_hook_v2.py +96 -0
- stravinsky_claude_assets/hooks/parallel_execution.py +241 -0
- stravinsky_claude_assets/hooks/parallel_reinforcement.py +106 -0
- stravinsky_claude_assets/hooks/parallel_reinforcement_v2.py +112 -0
- stravinsky_claude_assets/hooks/pre_compact.py +123 -0
- stravinsky_claude_assets/hooks/ralph_loop.py +173 -0
- stravinsky_claude_assets/hooks/session_recovery.py +263 -0
- stravinsky_claude_assets/hooks/stop_hook.py +89 -0
- stravinsky_claude_assets/hooks/stravinsky_metrics.py +164 -0
- stravinsky_claude_assets/hooks/stravinsky_mode.py +146 -0
- stravinsky_claude_assets/hooks/subagent_stop.py +98 -0
- stravinsky_claude_assets/hooks/todo_continuation.py +111 -0
- stravinsky_claude_assets/hooks/todo_delegation.py +96 -0
- stravinsky_claude_assets/hooks/tool_messaging.py +281 -0
- stravinsky_claude_assets/hooks/truncator.py +23 -0
- stravinsky_claude_assets/rules/deployment_safety.md +51 -0
- stravinsky_claude_assets/rules/integration_wiring.md +89 -0
- stravinsky_claude_assets/rules/pypi_deployment.md +220 -0
- stravinsky_claude_assets/rules/stravinsky_orchestrator.md +32 -0
- stravinsky_claude_assets/settings.json +152 -0
- stravinsky_claude_assets/skills/chrome-devtools/SKILL.md +81 -0
- stravinsky_claude_assets/skills/sqlite/SKILL.md +77 -0
- stravinsky_claude_assets/skills/supabase/SKILL.md +74 -0
- stravinsky_claude_assets/task_dependencies.json +34 -0
- stravinsky-0.2.67.dist-info/METADATA +0 -284
- stravinsky-0.2.67.dist-info/RECORD +0 -76
- {stravinsky-0.2.67.dist-info ā stravinsky-0.4.66.dist-info}/WHEEL +0 -0
mcp_bridge/cli/install_hooks.py
CHANGED
|
@@ -17,16 +17,12 @@ Hooks provide enhanced Claude Code behavior including:
|
|
|
17
17
|
"""
|
|
18
18
|
|
|
19
19
|
import json
|
|
20
|
-
import os
|
|
21
|
-
import shutil
|
|
22
20
|
import sys
|
|
23
21
|
from pathlib import Path
|
|
24
|
-
from typing import Dict, List, Any
|
|
25
|
-
|
|
26
22
|
|
|
27
23
|
# Hook file contents - these will be written to ~/.claude/hooks/
|
|
28
24
|
HOOKS = {
|
|
29
|
-
"truncator.py":
|
|
25
|
+
"truncator.py": """import os
|
|
30
26
|
import sys
|
|
31
27
|
import json
|
|
32
28
|
|
|
@@ -49,8 +45,7 @@ def main():
|
|
|
49
45
|
|
|
50
46
|
if __name__ == "__main__":
|
|
51
47
|
main()
|
|
52
|
-
|
|
53
|
-
|
|
48
|
+
""",
|
|
54
49
|
"edit_recovery.py": '''#!/usr/bin/env python3
|
|
55
50
|
"""Edit error recovery hook - detects edit failures and forces file reading."""
|
|
56
51
|
import json
|
|
@@ -94,8 +89,7 @@ def main():
|
|
|
94
89
|
if __name__ == "__main__":
|
|
95
90
|
sys.exit(main())
|
|
96
91
|
''',
|
|
97
|
-
|
|
98
|
-
"context.py": '''import os
|
|
92
|
+
"context.py": """import os
|
|
99
93
|
import sys
|
|
100
94
|
import json
|
|
101
95
|
from pathlib import Path
|
|
@@ -133,8 +127,7 @@ def main():
|
|
|
133
127
|
|
|
134
128
|
if __name__ == "__main__":
|
|
135
129
|
main()
|
|
136
|
-
|
|
137
|
-
|
|
130
|
+
""",
|
|
138
131
|
"parallel_execution.py": '''#!/usr/bin/env python3
|
|
139
132
|
"""
|
|
140
133
|
UserPromptSubmit hook: Pre-emptive parallel execution enforcement.
|
|
@@ -157,11 +150,10 @@ STRAVINSKY_MODE_FILE = Path.home() / ".stravinsky_mode"
|
|
|
157
150
|
def detect_stravinsky_invocation(prompt):
|
|
158
151
|
"""Detect if /stravinsky skill is being invoked."""
|
|
159
152
|
patterns = [
|
|
160
|
-
r
|
|
161
|
-
r
|
|
162
|
-
r
|
|
163
|
-
r
|
|
164
|
-
r'ultrathink',
|
|
153
|
+
r"/stravinsky",
|
|
154
|
+
r"<command-name>/stravinsky</command-name>",
|
|
155
|
+
r"stravinsky orchestrator",
|
|
156
|
+
r"\bultrawork\b",
|
|
165
157
|
]
|
|
166
158
|
prompt_lower = prompt.lower()
|
|
167
159
|
return any(re.search(p, prompt_lower) for p in patterns)
|
|
@@ -247,7 +239,6 @@ Task(subagent_type="Explore", prompt="Task 3 details", description="Task 3", run
|
|
|
247
239
|
if __name__ == "__main__":
|
|
248
240
|
sys.exit(main())
|
|
249
241
|
''',
|
|
250
|
-
|
|
251
242
|
"todo_continuation.py": '''#!/usr/bin/env python3
|
|
252
243
|
"""
|
|
253
244
|
UserPromptSubmit hook: Todo Continuation Enforcer
|
|
@@ -339,7 +330,6 @@ def main():
|
|
|
339
330
|
if __name__ == "__main__":
|
|
340
331
|
sys.exit(main())
|
|
341
332
|
''',
|
|
342
|
-
|
|
343
333
|
"todo_delegation.py": '''#!/usr/bin/env python3
|
|
344
334
|
"""
|
|
345
335
|
PostToolUse hook for TodoWrite: CRITICAL parallel execution enforcer.
|
|
@@ -429,7 +419,6 @@ Your NEXT action MUST be multiple Task() calls, one for each independent TODO.
|
|
|
429
419
|
if __name__ == "__main__":
|
|
430
420
|
sys.exit(main())
|
|
431
421
|
''',
|
|
432
|
-
|
|
433
422
|
"stravinsky_mode.py": '''#!/usr/bin/env python3
|
|
434
423
|
"""
|
|
435
424
|
Stravinsky Mode Enforcer Hook
|
|
@@ -575,7 +564,6 @@ To exit stravinsky mode, run:
|
|
|
575
564
|
if __name__ == "__main__":
|
|
576
565
|
main()
|
|
577
566
|
''',
|
|
578
|
-
|
|
579
567
|
"tool_messaging.py": '''#!/usr/bin/env python3
|
|
580
568
|
"""
|
|
581
569
|
PostToolUse hook for user-friendly tool messaging.
|
|
@@ -741,7 +729,6 @@ def main():
|
|
|
741
729
|
if __name__ == "__main__":
|
|
742
730
|
main()
|
|
743
731
|
''',
|
|
744
|
-
|
|
745
732
|
"notification_hook.py": '''#!/usr/bin/env python3
|
|
746
733
|
"""
|
|
747
734
|
Notification hook for agent spawn messages.
|
|
@@ -846,7 +833,6 @@ def main():
|
|
|
846
833
|
if __name__ == "__main__":
|
|
847
834
|
sys.exit(main())
|
|
848
835
|
''',
|
|
849
|
-
|
|
850
836
|
"subagent_stop.py": '''#!/usr/bin/env python3
|
|
851
837
|
"""
|
|
852
838
|
SubagentStop hook: Handler for agent/subagent completion events.
|
|
@@ -946,7 +932,6 @@ def main():
|
|
|
946
932
|
if __name__ == "__main__":
|
|
947
933
|
sys.exit(main())
|
|
948
934
|
''',
|
|
949
|
-
|
|
950
935
|
"pre_compact.py": '''#!/usr/bin/env python3
|
|
951
936
|
"""
|
|
952
937
|
PreCompact hook: Context preservation before compaction.
|
|
@@ -1024,7 +1009,7 @@ def log_compaction(preserved: List[str], stravinsky_active: bool):
|
|
|
1024
1009
|
ensure_state_dir()
|
|
1025
1010
|
|
|
1026
1011
|
entry = {
|
|
1027
|
-
"timestamp": datetime.
|
|
1012
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
1028
1013
|
"preserved_count": len(preserved),
|
|
1029
1014
|
"stravinsky_mode": stravinsky_active,
|
|
1030
1015
|
"preview": [p[:50] for p in preserved[:3]],
|
|
@@ -1070,6 +1055,456 @@ def main():
|
|
|
1070
1055
|
|
|
1071
1056
|
if __name__ == "__main__":
|
|
1072
1057
|
sys.exit(main())
|
|
1058
|
+
''',
|
|
1059
|
+
"stop_hook.py": '''#!/usr/bin/env -S uv run --script
|
|
1060
|
+
|
|
1061
|
+
# /// script
|
|
1062
|
+
# requires-python = ">=3.8"
|
|
1063
|
+
# dependencies = [
|
|
1064
|
+
# "anthropic",
|
|
1065
|
+
# "python-dotenv",
|
|
1066
|
+
# ]
|
|
1067
|
+
|
|
1068
|
+
"""
|
|
1069
|
+
Fix Stop Hook - Calls stravinsky_metrics.py on Stop events.
|
|
1070
|
+
|
|
1071
|
+
This hook is triggered by .claude/settings.json on Stop/SubagentStop.
|
|
1072
|
+
It queries Stravinsky's cost tracker and sends a StravinskyMetrics event to the dashboard.
|
|
1073
|
+
"""
|
|
1074
|
+
|
|
1075
|
+
import os
|
|
1076
|
+
import sys
|
|
1077
|
+
import subprocess
|
|
1078
|
+
from pathlib import Path
|
|
1079
|
+
|
|
1080
|
+
# Add hooks directory to path for script imports
|
|
1081
|
+
hooks_dir = Path(__file__).parent
|
|
1082
|
+
sys.path.insert(0, str(hooks_dir))
|
|
1083
|
+
|
|
1084
|
+
|
|
1085
|
+
def send_stravinsky_metrics(session_id: str) -> bool:
|
|
1086
|
+
"""Call stravinsky_metrics.py to query and send metrics to dashboard."""
|
|
1087
|
+
script_path = hooks_dir / "stravinsky_metrics.py"
|
|
1088
|
+
|
|
1089
|
+
if not script_path.exists():
|
|
1090
|
+
print(f"Error: stravinsky_metrics.py not found at {script_path}", file=sys.stderr)
|
|
1091
|
+
return False
|
|
1092
|
+
|
|
1093
|
+
try:
|
|
1094
|
+
# Build command
|
|
1095
|
+
cmd = [
|
|
1096
|
+
"uv",
|
|
1097
|
+
"run",
|
|
1098
|
+
"--script",
|
|
1099
|
+
str(script_path),
|
|
1100
|
+
"--session-id",
|
|
1101
|
+
session_id,
|
|
1102
|
+
"--summarize", # Generate summary and send as event
|
|
1103
|
+
]
|
|
1104
|
+
|
|
1105
|
+
# Run command
|
|
1106
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10, check=False)
|
|
1107
|
+
|
|
1108
|
+
if result.returncode != 0:
|
|
1109
|
+
print(
|
|
1110
|
+
f"Error: stravinsky_metrics.py failed with exit code {result.returncode}",
|
|
1111
|
+
file=sys.stderr,
|
|
1112
|
+
)
|
|
1113
|
+
print(f"stderr: {result.stderr}", file=sys.stderr)
|
|
1114
|
+
return False
|
|
1115
|
+
|
|
1116
|
+
return True
|
|
1117
|
+
|
|
1118
|
+
except subprocess.TimeoutExpired:
|
|
1119
|
+
print(f"Error: stravinsky_metrics.py timed out after 10 seconds", file=sys.stderr)
|
|
1120
|
+
return False
|
|
1121
|
+
|
|
1122
|
+
except Exception as e:
|
|
1123
|
+
print(f"Error: stravinsky_metrics.py execution failed: {e}", file=sys.stderr)
|
|
1124
|
+
return False
|
|
1125
|
+
|
|
1126
|
+
|
|
1127
|
+
def main():
|
|
1128
|
+
# Get session_id from environment or use default
|
|
1129
|
+
session_id = os.environ.get("CLAUDE_SESSION_ID", "default")
|
|
1130
|
+
|
|
1131
|
+
# Determine hook type
|
|
1132
|
+
hook_type = os.environ.get("CLAUDE_HOOK_EVENT_TYPE", "Stop")
|
|
1133
|
+
|
|
1134
|
+
# Send metrics to dashboard
|
|
1135
|
+
success = send_stravinsky_metrics(session_id)
|
|
1136
|
+
|
|
1137
|
+
if not success:
|
|
1138
|
+
sys.exit(1)
|
|
1139
|
+
|
|
1140
|
+
print(
|
|
1141
|
+
f"ā Successfully sent Stravinsky metrics for session {session_id} to dashboard",
|
|
1142
|
+
file=sys.stderr,
|
|
1143
|
+
)
|
|
1144
|
+
|
|
1145
|
+
|
|
1146
|
+
if __name__ == "__main__":
|
|
1147
|
+
main()
|
|
1148
|
+
''',
|
|
1149
|
+
"stravinsky_metrics.py": '''#!/usr/bin/env python3
|
|
1150
|
+
"""
|
|
1151
|
+
Stravinsky Metrics Hook Script
|
|
1152
|
+
|
|
1153
|
+
Queries Stravinsky's internal metrics and sends them to the observability dashboard.
|
|
1154
|
+
This hook is triggered on Stop/SubagentStop events.
|
|
1155
|
+
|
|
1156
|
+
Usage:
|
|
1157
|
+
stravinsky_metrics.py --session-id <session_id>
|
|
1158
|
+
|
|
1159
|
+
Environment Variables:
|
|
1160
|
+
CLAUDE_SESSION_ID: Fallback session ID if --session-id not provided
|
|
1161
|
+
|
|
1162
|
+
Output:
|
|
1163
|
+
Sends StravinskyMetrics event to dashboard via send_event.py
|
|
1164
|
+
|
|
1165
|
+
Metrics Collected:
|
|
1166
|
+
- Total session cost (USD)
|
|
1167
|
+
- Total tokens used
|
|
1168
|
+
- Per-agent cost and token breakdown
|
|
1169
|
+
- Per-model cost and token breakdown (TODO)
|
|
1170
|
+
- Agent count (active/total) (TODO)
|
|
1171
|
+
|
|
1172
|
+
Error Handling:
|
|
1173
|
+
Returns non-zero exit code on error to prevent blocking Claude Code operations.
|
|
1174
|
+
"""
|
|
1175
|
+
|
|
1176
|
+
import sys
|
|
1177
|
+
import os
|
|
1178
|
+
import json
|
|
1179
|
+
import argparse
|
|
1180
|
+
import subprocess
|
|
1181
|
+
from pathlib import Path
|
|
1182
|
+
from datetime import datetime
|
|
1183
|
+
from typing import Dict, Any
|
|
1184
|
+
from mcp_bridge.metrics.cost_tracker import CostTracker
|
|
1185
|
+
|
|
1186
|
+
|
|
1187
|
+
def load_usage_data(session_id: str) -> Dict[str, Any]:
|
|
1188
|
+
"""
|
|
1189
|
+
Load usage data from Stravinsky's cost tracker.
|
|
1190
|
+
|
|
1191
|
+
Uses CostTracker.get_session_summary() to get metrics for a session.
|
|
1192
|
+
|
|
1193
|
+
Args:
|
|
1194
|
+
session_id: Claude Code session ID
|
|
1195
|
+
|
|
1196
|
+
Returns:
|
|
1197
|
+
Dictionary with metrics data
|
|
1198
|
+
"""
|
|
1199
|
+
summary = CostTracker.get_instance().get_session_summary(session_id)
|
|
1200
|
+
|
|
1201
|
+
# Transform CostTracker summary to expected format
|
|
1202
|
+
metrics = {
|
|
1203
|
+
"total_cost": summary.get("total_cost", 0),
|
|
1204
|
+
"total_tokens": summary.get("total_tokens", 0),
|
|
1205
|
+
"by_agent": summary.get("by_agent", {}),
|
|
1206
|
+
"by_model": {}, # Extract from summary if available
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
return metrics
|
|
1210
|
+
|
|
1211
|
+
|
|
1212
|
+
def send_metrics_event(session_id: str, metrics: Dict[str, Any]) -> bool:
|
|
1213
|
+
"""
|
|
1214
|
+
Send metrics event to observability dashboard via send_event.py.
|
|
1215
|
+
|
|
1216
|
+
Args:
|
|
1217
|
+
session_id: Claude Code session ID
|
|
1218
|
+
metrics: Metrics data dictionary
|
|
1219
|
+
|
|
1220
|
+
Returns:
|
|
1221
|
+
True if successful, False otherwise
|
|
1222
|
+
"""
|
|
1223
|
+
hook_dir = Path(__file__).parent
|
|
1224
|
+
send_event_script = hook_dir / "send_event.py"
|
|
1225
|
+
|
|
1226
|
+
if not send_event_script.exists():
|
|
1227
|
+
print(f"Error: send_event.py not found at {send_event_script}", file=sys.stderr)
|
|
1228
|
+
return False
|
|
1229
|
+
|
|
1230
|
+
event_data = {
|
|
1231
|
+
"session_id": session_id,
|
|
1232
|
+
"metrics": metrics,
|
|
1233
|
+
"timestamp": datetime.now().isoformat(),
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
cmd = [
|
|
1237
|
+
"uv",
|
|
1238
|
+
"run",
|
|
1239
|
+
str(send_event_script),
|
|
1240
|
+
"--source-app",
|
|
1241
|
+
"stravinsky",
|
|
1242
|
+
"--event-type",
|
|
1243
|
+
"StravinskyMetrics",
|
|
1244
|
+
"--summarize",
|
|
1245
|
+
]
|
|
1246
|
+
|
|
1247
|
+
try:
|
|
1248
|
+
result = subprocess.run(
|
|
1249
|
+
cmd, input=json.dumps(event_data), capture_output=True, text=True, timeout=10
|
|
1250
|
+
)
|
|
1251
|
+
|
|
1252
|
+
if result.returncode != 0:
|
|
1253
|
+
print(f"Error from send_event.py: {result.stderr}", file=sys.stderr)
|
|
1254
|
+
return False
|
|
1255
|
+
|
|
1256
|
+
return True
|
|
1257
|
+
|
|
1258
|
+
except subprocess.TimeoutExpired:
|
|
1259
|
+
print("Error: send_event.py timed out", file=sys.stderr)
|
|
1260
|
+
return False
|
|
1261
|
+
except Exception as e:
|
|
1262
|
+
print(f"Error running send_event.py: {e}", file=sys.stderr)
|
|
1263
|
+
return False
|
|
1264
|
+
|
|
1265
|
+
|
|
1266
|
+
def main():
|
|
1267
|
+
parser = argparse.ArgumentParser(description="Query Stravinsky metrics and send to dashboard")
|
|
1268
|
+
parser.add_argument(
|
|
1269
|
+
"--session-id", help="Claude Code session ID (falls back to CLAUDE_SESSION_ID env var)"
|
|
1270
|
+
)
|
|
1271
|
+
parser.add_argument(
|
|
1272
|
+
"--dry-run",
|
|
1273
|
+
action="store_true",
|
|
1274
|
+
help="Print metrics without sending to dashboard (for testing)",
|
|
1275
|
+
)
|
|
1276
|
+
|
|
1277
|
+
args = parser.parse_args()
|
|
1278
|
+
|
|
1279
|
+
session_id = args.session_id or os.environ.get("CLAUDE_SESSION_ID", "default")
|
|
1280
|
+
|
|
1281
|
+
if not session_id or session_id == "default":
|
|
1282
|
+
print("Warning: No session ID provided, using 'default'", file=sys.stderr)
|
|
1283
|
+
|
|
1284
|
+
metrics = load_usage_data(session_id)
|
|
1285
|
+
|
|
1286
|
+
if metrics is None:
|
|
1287
|
+
print("Error: Failed to load metrics", file=sys.stderr)
|
|
1288
|
+
sys.exit(1)
|
|
1289
|
+
|
|
1290
|
+
if args.dry_run:
|
|
1291
|
+
print(json.dumps(metrics, indent=2))
|
|
1292
|
+
sys.exit(0)
|
|
1293
|
+
|
|
1294
|
+
print(f"Stravinsky Metrics for session {session_id}:", file=sys.stderr)
|
|
1295
|
+
print(f" Total Cost: ${metrics['total_cost']:.6f}", file=sys.stderr)
|
|
1296
|
+
print(f" Total Tokens: {metrics['total_tokens']:,}", file=sys.stderr)
|
|
1297
|
+
print(f" Agents: {len(metrics['by_agent'])}", file=sys.stderr)
|
|
1298
|
+
|
|
1299
|
+
success = send_metrics_event(session_id, metrics)
|
|
1300
|
+
|
|
1301
|
+
if success:
|
|
1302
|
+
print("Metrics sent to dashboard successfully", file=sys.stderr)
|
|
1303
|
+
sys.exit(0)
|
|
1304
|
+
else:
|
|
1305
|
+
print("Failed to send metrics to dashboard", file=sys.stderr)
|
|
1306
|
+
sys.exit(1)
|
|
1307
|
+
|
|
1308
|
+
|
|
1309
|
+
if __name__ == "__main__":
|
|
1310
|
+
main()
|
|
1311
|
+
''',
|
|
1312
|
+
"send_event.py": '''#!/usr/bin/env -S uv run --script
|
|
1313
|
+
# /// script
|
|
1314
|
+
# requires-python = ">=3.8"
|
|
1315
|
+
# dependencies = [
|
|
1316
|
+
# "anthropic",
|
|
1317
|
+
# "python-dotenv",
|
|
1318
|
+
# ]
|
|
1319
|
+
# ///
|
|
1320
|
+
|
|
1321
|
+
"""
|
|
1322
|
+
Multi-Agent Observability Hook Script
|
|
1323
|
+
Sends Claude Code hook events to the observability server.
|
|
1324
|
+
"""
|
|
1325
|
+
|
|
1326
|
+
import json
|
|
1327
|
+
import sys
|
|
1328
|
+
import os
|
|
1329
|
+
import argparse
|
|
1330
|
+
import urllib.request
|
|
1331
|
+
import urllib.error
|
|
1332
|
+
import logging
|
|
1333
|
+
from datetime import datetime
|
|
1334
|
+
from pathlib import Path
|
|
1335
|
+
# from utils.summarizer import generate_event_summary
|
|
1336
|
+
# from utils.model_extractor import get_model_from_transcript
|
|
1337
|
+
# Note: commented out utils imports to prevent failure if utils are missing
|
|
1338
|
+
# If you need full functionality, ensure utils/ directory is also installed.
|
|
1339
|
+
|
|
1340
|
+
def generate_event_summary(event_data):
|
|
1341
|
+
return None
|
|
1342
|
+
|
|
1343
|
+
def get_model_from_transcript(session_id, transcript_path):
|
|
1344
|
+
return ""
|
|
1345
|
+
|
|
1346
|
+
log_dir = Path.home() / ".claude/hooks/logs"
|
|
1347
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
1348
|
+
logging.basicConfig(
|
|
1349
|
+
filename=log_dir / "event_sender.log",
|
|
1350
|
+
level=logging.DEBUG,
|
|
1351
|
+
format="%(asctime)s - %(levelname)s - %(message)s",
|
|
1352
|
+
)
|
|
1353
|
+
|
|
1354
|
+
|
|
1355
|
+
def send_event_to_server(event_data, server_url="http://localhost:4000/events"):
|
|
1356
|
+
"""Send event data to the observability server."""
|
|
1357
|
+
try:
|
|
1358
|
+
# Prepare the request
|
|
1359
|
+
req = urllib.request.Request(
|
|
1360
|
+
server_url,
|
|
1361
|
+
data=json.dumps(event_data).encode("utf-8"),
|
|
1362
|
+
headers={"Content-Type": "application/json", "User-Agent": "Claude-Code-Hook/1.0"},
|
|
1363
|
+
)
|
|
1364
|
+
|
|
1365
|
+
# Send the request
|
|
1366
|
+
with urllib.request.urlopen(req, timeout=5) as response:
|
|
1367
|
+
if response.status == 200:
|
|
1368
|
+
return True
|
|
1369
|
+
else:
|
|
1370
|
+
logging.debug(f"Server returned status: {response.status}")
|
|
1371
|
+
return False
|
|
1372
|
+
|
|
1373
|
+
except urllib.error.URLError as e:
|
|
1374
|
+
logging.error(f"Failed to send event: {e}")
|
|
1375
|
+
return False
|
|
1376
|
+
except Exception as e:
|
|
1377
|
+
logging.error(f"Unexpected error: {e}")
|
|
1378
|
+
return False
|
|
1379
|
+
|
|
1380
|
+
|
|
1381
|
+
def main():
|
|
1382
|
+
# Parse command line arguments
|
|
1383
|
+
parser = argparse.ArgumentParser(
|
|
1384
|
+
description="Send Claude Code hook events to observability server"
|
|
1385
|
+
)
|
|
1386
|
+
parser.add_argument("--source-app", required=True, help="Source application name")
|
|
1387
|
+
parser.add_argument(
|
|
1388
|
+
"--event-type", required=True, help="Hook event type (PreToolUse, PostToolUse, etc.)"
|
|
1389
|
+
)
|
|
1390
|
+
parser.add_argument("--server-url", default="http://localhost:4000/events", help="Server URL")
|
|
1391
|
+
parser.add_argument(
|
|
1392
|
+
"--add-chat", action="store_true", help="Include chat transcript if available"
|
|
1393
|
+
)
|
|
1394
|
+
parser.add_argument("--summarize", action="store_true", help="Generate AI summary of the event")
|
|
1395
|
+
|
|
1396
|
+
# Stravinsky-specific metrics (optional)
|
|
1397
|
+
parser.add_argument("--cost", type=float, help="Cost in USD (Stravinsky metrics)")
|
|
1398
|
+
parser.add_argument(
|
|
1399
|
+
"--prompt-tokens", type=int, help="Prompt token count (Stravinsky metrics)"
|
|
1400
|
+
)
|
|
1401
|
+
parser.add_argument(
|
|
1402
|
+
"--completion-tokens", type=int, help="Completion token count (Stravinsky metrics)"
|
|
1403
|
+
)
|
|
1404
|
+
parser.add_argument("--agent-type", type=str, help="Agent type (Stravinsky metrics)")
|
|
1405
|
+
parser.add_argument("--task-id", type=str, help="Task tracking ID (Stravinsky metrics)")
|
|
1406
|
+
parser.add_argument(
|
|
1407
|
+
"--hierarchy-level", type=int, help="Nesting depth (Stravinsky metrics)")
|
|
1408
|
+
parser.add_argument(
|
|
1409
|
+
"--parent-session-id", type=str, help="Parent session ID for hierarchy (Stravinsky metrics)")
|
|
1410
|
+
|
|
1411
|
+
args = parser.parse_args()
|
|
1412
|
+
|
|
1413
|
+
try:
|
|
1414
|
+
# Read hook data from stdin
|
|
1415
|
+
if not sys.stdin.isatty():
|
|
1416
|
+
input_data = json.load(sys.stdin)
|
|
1417
|
+
else:
|
|
1418
|
+
input_data = {}
|
|
1419
|
+
except json.JSONDecodeError as e:
|
|
1420
|
+
# print(f"Failed to parse JSON input: {e}", file=sys.stderr)
|
|
1421
|
+
# Fail gracefully if no input
|
|
1422
|
+
input_data = {}
|
|
1423
|
+
|
|
1424
|
+
# Extract model name from transcript (with caching)
|
|
1425
|
+
session_id = input_data.get("session_id", "unknown")
|
|
1426
|
+
transcript_path = input_data.get("transcript_path", "")
|
|
1427
|
+
model_name = ""
|
|
1428
|
+
if transcript_path:
|
|
1429
|
+
model_name = get_model_from_transcript(session_id, transcript_path)
|
|
1430
|
+
|
|
1431
|
+
# Build Stravinsky metrics (if any flags provided)
|
|
1432
|
+
stravinsky_metrics = {}
|
|
1433
|
+
if any(
|
|
1434
|
+
[
|
|
1435
|
+
args.cost,
|
|
1436
|
+
args.prompt_tokens,
|
|
1437
|
+
args.completion_tokens,
|
|
1438
|
+
args.agent_type,
|
|
1439
|
+
args.task_id,
|
|
1440
|
+
args.parent_session_id,
|
|
1441
|
+
args.hierarchy_level,
|
|
1442
|
+
]
|
|
1443
|
+
):
|
|
1444
|
+
stravinsky_metrics = {
|
|
1445
|
+
"cost_usd": args.cost,
|
|
1446
|
+
"prompt_tokens": args.prompt_tokens,
|
|
1447
|
+
"completion_tokens": args.completion_tokens,
|
|
1448
|
+
"total_tokens": (args.prompt_tokens or 0) + (args.completion_tokens or 0),
|
|
1449
|
+
"model": model_name,
|
|
1450
|
+
"provider": "anthropic",
|
|
1451
|
+
"agent_type": args.agent_type,
|
|
1452
|
+
"task_id": args.task_id,
|
|
1453
|
+
"parent_session_id": args.parent_session_id,
|
|
1454
|
+
"hierarchy_level": args.hierarchy_level or 0,
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
# Prepare event data for server
|
|
1458
|
+
event_data = {
|
|
1459
|
+
"source_app": args.source_app,
|
|
1460
|
+
"session_id": session_id,
|
|
1461
|
+
"hook_event_type": args.event_type,
|
|
1462
|
+
"payload": input_data,
|
|
1463
|
+
"timestamp": int(datetime.now().timestamp() * 1000),
|
|
1464
|
+
"model_name": model_name,
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
# Add Stravinsky metrics if provided
|
|
1468
|
+
if stravinsky_metrics:
|
|
1469
|
+
event_data["stravinsky"] = stravinsky_metrics
|
|
1470
|
+
|
|
1471
|
+
# Handle --add-chat option
|
|
1472
|
+
if args.add_chat and "transcript_path" in input_data:
|
|
1473
|
+
transcript_path = input_data["transcript_path"]
|
|
1474
|
+
if os.path.exists(transcript_path):
|
|
1475
|
+
# Read .jsonl file and convert to JSON array
|
|
1476
|
+
chat_data = []
|
|
1477
|
+
try:
|
|
1478
|
+
with open(transcript_path, "r") as f:
|
|
1479
|
+
for line in f:
|
|
1480
|
+
line = line.strip()
|
|
1481
|
+
if line:
|
|
1482
|
+
try:
|
|
1483
|
+
chat_data.append(json.loads(line))
|
|
1484
|
+
except json.JSONDecodeError:
|
|
1485
|
+
pass # Skip invalid lines
|
|
1486
|
+
|
|
1487
|
+
# Add chat to event data
|
|
1488
|
+
event_data["chat"] = chat_data
|
|
1489
|
+
except Exception as e:
|
|
1490
|
+
print(f"Failed to read transcript: {e}", file=sys.stderr)
|
|
1491
|
+
|
|
1492
|
+
# Generate summary if requested
|
|
1493
|
+
if args.summarize:
|
|
1494
|
+
summary = generate_event_summary(event_data)
|
|
1495
|
+
if summary:
|
|
1496
|
+
event_data["summary"] = summary
|
|
1497
|
+
# Continue even if summary generation fails
|
|
1498
|
+
|
|
1499
|
+
# Send to server
|
|
1500
|
+
success = send_event_to_server(event_data, args.server_url)
|
|
1501
|
+
|
|
1502
|
+
# Always exit with 0 to not block Claude Code operations
|
|
1503
|
+
sys.exit(0)
|
|
1504
|
+
|
|
1505
|
+
|
|
1506
|
+
if __name__ == "__main__":
|
|
1507
|
+
main()
|
|
1073
1508
|
''',
|
|
1074
1509
|
}
|
|
1075
1510
|
|
|
@@ -1077,176 +1512,136 @@ if __name__ == "__main__":
|
|
|
1077
1512
|
# Hook registration configuration for settings.json
|
|
1078
1513
|
# IMPORTANT: Uses ~/.claude/hooks/ (global) paths so hooks work in ANY project
|
|
1079
1514
|
HOOK_REGISTRATIONS = {
|
|
1515
|
+
"Stop": [
|
|
1516
|
+
{
|
|
1517
|
+
"matcher": "*",
|
|
1518
|
+
"hooks": [{"type": "command", "command": "python3 ~/.claude/hooks/stop_hook.py"}],
|
|
1519
|
+
}
|
|
1520
|
+
],
|
|
1080
1521
|
"Notification": [
|
|
1081
1522
|
{
|
|
1082
1523
|
"matcher": "*",
|
|
1083
1524
|
"hooks": [
|
|
1084
|
-
{
|
|
1085
|
-
|
|
1086
|
-
"command": "python3 ~/.claude/hooks/notification_hook.py"
|
|
1087
|
-
}
|
|
1088
|
-
]
|
|
1525
|
+
{"type": "command", "command": "python3 ~/.claude/hooks/notification_hook.py"}
|
|
1526
|
+
],
|
|
1089
1527
|
}
|
|
1090
1528
|
],
|
|
1091
1529
|
"SubagentStop": [
|
|
1092
1530
|
{
|
|
1093
1531
|
"matcher": "*",
|
|
1094
|
-
"hooks": [
|
|
1095
|
-
{
|
|
1096
|
-
"type": "command",
|
|
1097
|
-
"command": "python3 ~/.claude/hooks/subagent_stop.py"
|
|
1098
|
-
}
|
|
1099
|
-
]
|
|
1532
|
+
"hooks": [{"type": "command", "command": "python3 ~/.claude/hooks/subagent_stop.py"}],
|
|
1100
1533
|
}
|
|
1101
1534
|
],
|
|
1102
1535
|
"PreCompact": [
|
|
1103
1536
|
{
|
|
1104
1537
|
"matcher": "*",
|
|
1105
|
-
"hooks": [
|
|
1106
|
-
{
|
|
1107
|
-
"type": "command",
|
|
1108
|
-
"command": "python3 ~/.claude/hooks/pre_compact.py"
|
|
1109
|
-
}
|
|
1110
|
-
]
|
|
1538
|
+
"hooks": [{"type": "command", "command": "python3 ~/.claude/hooks/pre_compact.py"}],
|
|
1111
1539
|
}
|
|
1112
1540
|
],
|
|
1113
1541
|
"PreToolUse": [
|
|
1114
1542
|
{
|
|
1115
1543
|
"matcher": "Read,Search,Grep,Bash,Edit,MultiEdit",
|
|
1116
|
-
"hooks": [
|
|
1117
|
-
{
|
|
1118
|
-
"type": "command",
|
|
1119
|
-
"command": "python3 ~/.claude/hooks/stravinsky_mode.py"
|
|
1120
|
-
}
|
|
1121
|
-
]
|
|
1544
|
+
"hooks": [{"type": "command", "command": "python3 ~/.claude/hooks/stravinsky_mode.py"}],
|
|
1122
1545
|
}
|
|
1123
1546
|
],
|
|
1124
1547
|
"UserPromptSubmit": [
|
|
1125
1548
|
{
|
|
1126
1549
|
"matcher": "*",
|
|
1127
1550
|
"hooks": [
|
|
1128
|
-
{
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
{
|
|
1133
|
-
"type": "command",
|
|
1134
|
-
"command": "python3 ~/.claude/hooks/context.py"
|
|
1135
|
-
},
|
|
1136
|
-
{
|
|
1137
|
-
"type": "command",
|
|
1138
|
-
"command": "python3 ~/.claude/hooks/todo_continuation.py"
|
|
1139
|
-
}
|
|
1140
|
-
]
|
|
1551
|
+
{"type": "command", "command": "python3 ~/.claude/hooks/parallel_execution.py"},
|
|
1552
|
+
{"type": "command", "command": "python3 ~/.claude/hooks/context.py"},
|
|
1553
|
+
{"type": "command", "command": "python3 ~/.claude/hooks/todo_continuation.py"},
|
|
1554
|
+
],
|
|
1141
1555
|
}
|
|
1142
1556
|
],
|
|
1143
1557
|
"PostToolUse": [
|
|
1144
1558
|
{
|
|
1145
1559
|
"matcher": "*",
|
|
1146
|
-
"hooks": [
|
|
1147
|
-
{
|
|
1148
|
-
"type": "command",
|
|
1149
|
-
"command": "python3 ~/.claude/hooks/truncator.py"
|
|
1150
|
-
}
|
|
1151
|
-
]
|
|
1560
|
+
"hooks": [{"type": "command", "command": "python3 ~/.claude/hooks/truncator.py"}],
|
|
1152
1561
|
},
|
|
1153
1562
|
{
|
|
1154
1563
|
"matcher": "mcp__stravinsky__*,mcp__grep-app__*,Task",
|
|
1155
|
-
"hooks": [
|
|
1156
|
-
{
|
|
1157
|
-
"type": "command",
|
|
1158
|
-
"command": "python3 ~/.claude/hooks/tool_messaging.py"
|
|
1159
|
-
}
|
|
1160
|
-
]
|
|
1564
|
+
"hooks": [{"type": "command", "command": "python3 ~/.claude/hooks/tool_messaging.py"}],
|
|
1161
1565
|
},
|
|
1162
1566
|
{
|
|
1163
1567
|
"matcher": "Edit,MultiEdit",
|
|
1164
|
-
"hooks": [
|
|
1165
|
-
{
|
|
1166
|
-
"type": "command",
|
|
1167
|
-
"command": "python3 ~/.claude/hooks/edit_recovery.py"
|
|
1168
|
-
}
|
|
1169
|
-
]
|
|
1568
|
+
"hooks": [{"type": "command", "command": "python3 ~/.claude/hooks/edit_recovery.py"}],
|
|
1170
1569
|
},
|
|
1171
1570
|
{
|
|
1172
1571
|
"matcher": "TodoWrite",
|
|
1173
|
-
"hooks": [
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
"command": "python3 ~/.claude/hooks/todo_delegation.py"
|
|
1177
|
-
}
|
|
1178
|
-
]
|
|
1179
|
-
}
|
|
1180
|
-
]
|
|
1572
|
+
"hooks": [{"type": "command", "command": "python3 ~/.claude/hooks/todo_delegation.py"}],
|
|
1573
|
+
},
|
|
1574
|
+
],
|
|
1181
1575
|
}
|
|
1182
1576
|
|
|
1183
1577
|
|
|
1184
1578
|
def install_hooks():
|
|
1185
1579
|
"""Install Stravinsky hooks to ~/.claude/hooks/"""
|
|
1186
|
-
|
|
1580
|
+
|
|
1187
1581
|
# Get home directory
|
|
1188
1582
|
home = Path.home()
|
|
1189
1583
|
claude_dir = home / ".claude"
|
|
1190
1584
|
hooks_dir = claude_dir / "hooks"
|
|
1191
1585
|
settings_file = claude_dir / "settings.json"
|
|
1192
|
-
|
|
1586
|
+
|
|
1193
1587
|
print("š Stravinsky Hook Installer")
|
|
1194
1588
|
print("=" * 60)
|
|
1195
|
-
|
|
1589
|
+
|
|
1196
1590
|
# Create hooks directory if it doesn't exist
|
|
1197
1591
|
if not hooks_dir.exists():
|
|
1198
1592
|
print(f"š Creating hooks directory: {hooks_dir}")
|
|
1199
1593
|
hooks_dir.mkdir(parents=True, exist_ok=True)
|
|
1200
1594
|
else:
|
|
1201
1595
|
print(f"š Hooks directory exists: {hooks_dir}")
|
|
1202
|
-
|
|
1596
|
+
|
|
1203
1597
|
# Install each hook file
|
|
1204
1598
|
print(f"\\nš Installing {len(HOOKS)} hook files...")
|
|
1205
1599
|
for filename, content in HOOKS.items():
|
|
1206
1600
|
hook_path = hooks_dir / filename
|
|
1601
|
+
hook_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1207
1602
|
print(f" ā {filename}")
|
|
1208
1603
|
hook_path.write_text(content)
|
|
1209
1604
|
hook_path.chmod(0o755) # Make executable
|
|
1210
|
-
|
|
1605
|
+
|
|
1211
1606
|
# Merge hook registrations into settings.json
|
|
1212
|
-
print(
|
|
1213
|
-
|
|
1607
|
+
print("\\nāļø Updating settings.json...")
|
|
1608
|
+
|
|
1214
1609
|
# Load existing settings or create new
|
|
1215
1610
|
if settings_file.exists():
|
|
1216
1611
|
print(f" š Reading existing settings: {settings_file}")
|
|
1217
|
-
with settings_file.open(
|
|
1612
|
+
with settings_file.open("r") as f:
|
|
1218
1613
|
settings = json.load(f)
|
|
1219
1614
|
else:
|
|
1220
1615
|
print(f" š Creating new settings file: {settings_file}")
|
|
1221
1616
|
settings = {}
|
|
1222
|
-
|
|
1617
|
+
|
|
1223
1618
|
# Merge hooks configuration
|
|
1224
1619
|
if "hooks" not in settings:
|
|
1225
1620
|
settings["hooks"] = {}
|
|
1226
|
-
|
|
1621
|
+
|
|
1227
1622
|
for hook_type, registrations in HOOK_REGISTRATIONS.items():
|
|
1228
1623
|
settings["hooks"][hook_type] = registrations
|
|
1229
1624
|
print(f" ā Registered {hook_type} hooks")
|
|
1230
|
-
|
|
1625
|
+
|
|
1231
1626
|
# Write updated settings
|
|
1232
|
-
with settings_file.open(
|
|
1627
|
+
with settings_file.open("w") as f:
|
|
1233
1628
|
json.dump(settings, f, indent=2)
|
|
1234
|
-
|
|
1235
|
-
print(
|
|
1629
|
+
|
|
1630
|
+
print("\\nā
Installation complete!")
|
|
1236
1631
|
print("=" * 60)
|
|
1237
|
-
print(
|
|
1238
|
-
for filename in HOOKS
|
|
1632
|
+
print("\\nš Installed hooks:")
|
|
1633
|
+
for filename in HOOKS:
|
|
1239
1634
|
print(f" ⢠{filename}")
|
|
1240
|
-
|
|
1241
|
-
print(
|
|
1242
|
-
for hook_type in HOOK_REGISTRATIONS
|
|
1635
|
+
|
|
1636
|
+
print("\\nš§ Hook types registered:")
|
|
1637
|
+
for hook_type in HOOK_REGISTRATIONS:
|
|
1243
1638
|
print(f" ⢠{hook_type}")
|
|
1244
|
-
|
|
1639
|
+
|
|
1245
1640
|
print(f"\\nš Installation directory: {hooks_dir}")
|
|
1246
1641
|
print(f"āļø Settings file: {settings_file}")
|
|
1247
1642
|
print("\\nš Stravinsky hooks are now active!")
|
|
1248
1643
|
print("\\nš” Tip: Run '/stravinsky' to activate orchestrator mode")
|
|
1249
|
-
|
|
1644
|
+
|
|
1250
1645
|
return 0
|
|
1251
1646
|
|
|
1252
1647
|
|
|
@@ -1257,6 +1652,7 @@ def main():
|
|
|
1257
1652
|
except Exception as e:
|
|
1258
1653
|
print(f"\\nā Installation failed: {e}", file=sys.stderr)
|
|
1259
1654
|
import traceback
|
|
1655
|
+
|
|
1260
1656
|
traceback.print_exc()
|
|
1261
1657
|
return 1
|
|
1262
1658
|
|