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.

Files changed (190) hide show
  1. mcp_bridge/__init__.py +1 -1
  2. mcp_bridge/auth/__init__.py +16 -6
  3. mcp_bridge/auth/cli.py +202 -11
  4. mcp_bridge/auth/oauth.py +1 -2
  5. mcp_bridge/auth/openai_oauth.py +4 -7
  6. mcp_bridge/auth/token_store.py +112 -11
  7. mcp_bridge/cli/__init__.py +1 -1
  8. mcp_bridge/cli/install_hooks.py +503 -107
  9. mcp_bridge/cli/session_report.py +0 -3
  10. mcp_bridge/config/MANIFEST_SCHEMA.md +305 -0
  11. mcp_bridge/config/README.md +276 -0
  12. mcp_bridge/config/__init__.py +2 -2
  13. mcp_bridge/config/hook_config.py +247 -0
  14. mcp_bridge/config/hooks_manifest.json +138 -0
  15. mcp_bridge/config/rate_limits.py +317 -0
  16. mcp_bridge/config/skills_manifest.json +128 -0
  17. mcp_bridge/hooks/HOOKS_SETTINGS.json +17 -4
  18. mcp_bridge/hooks/__init__.py +19 -4
  19. mcp_bridge/hooks/agent_reminder.py +4 -4
  20. mcp_bridge/hooks/auto_slash_command.py +5 -5
  21. mcp_bridge/hooks/budget_optimizer.py +2 -2
  22. mcp_bridge/hooks/claude_limits_hook.py +114 -0
  23. mcp_bridge/hooks/comment_checker.py +3 -4
  24. mcp_bridge/hooks/compaction.py +2 -2
  25. mcp_bridge/hooks/context.py +2 -1
  26. mcp_bridge/hooks/context_monitor.py +2 -2
  27. mcp_bridge/hooks/delegation_policy.py +85 -0
  28. mcp_bridge/hooks/directory_context.py +3 -3
  29. mcp_bridge/hooks/edit_recovery.py +3 -2
  30. mcp_bridge/hooks/edit_recovery_policy.py +49 -0
  31. mcp_bridge/hooks/empty_message_sanitizer.py +2 -2
  32. mcp_bridge/hooks/events.py +160 -0
  33. mcp_bridge/hooks/git_noninteractive.py +4 -4
  34. mcp_bridge/hooks/keyword_detector.py +8 -10
  35. mcp_bridge/hooks/manager.py +43 -22
  36. mcp_bridge/hooks/notification_hook.py +13 -6
  37. mcp_bridge/hooks/parallel_enforcement_policy.py +67 -0
  38. mcp_bridge/hooks/parallel_enforcer.py +5 -5
  39. mcp_bridge/hooks/parallel_execution.py +22 -10
  40. mcp_bridge/hooks/post_tool/parallel_validation.py +103 -0
  41. mcp_bridge/hooks/pre_compact.py +8 -9
  42. mcp_bridge/hooks/pre_tool/agent_spawn_validator.py +115 -0
  43. mcp_bridge/hooks/preemptive_compaction.py +2 -3
  44. mcp_bridge/hooks/routing_notifications.py +80 -0
  45. mcp_bridge/hooks/rules_injector.py +11 -19
  46. mcp_bridge/hooks/session_idle.py +4 -4
  47. mcp_bridge/hooks/session_notifier.py +4 -4
  48. mcp_bridge/hooks/session_recovery.py +4 -5
  49. mcp_bridge/hooks/stravinsky_mode.py +1 -1
  50. mcp_bridge/hooks/subagent_stop.py +1 -3
  51. mcp_bridge/hooks/task_validator.py +2 -2
  52. mcp_bridge/hooks/tmux_manager.py +7 -8
  53. mcp_bridge/hooks/todo_delegation.py +4 -1
  54. mcp_bridge/hooks/todo_enforcer.py +180 -10
  55. mcp_bridge/hooks/tool_messaging.py +113 -10
  56. mcp_bridge/hooks/truncation_policy.py +37 -0
  57. mcp_bridge/hooks/truncator.py +1 -2
  58. mcp_bridge/metrics/cost_tracker.py +115 -0
  59. mcp_bridge/native_search.py +93 -0
  60. mcp_bridge/native_watcher.py +118 -0
  61. mcp_bridge/notifications.py +150 -0
  62. mcp_bridge/orchestrator/enums.py +11 -0
  63. mcp_bridge/orchestrator/router.py +165 -0
  64. mcp_bridge/orchestrator/state.py +32 -0
  65. mcp_bridge/orchestrator/visualization.py +14 -0
  66. mcp_bridge/orchestrator/wisdom.py +34 -0
  67. mcp_bridge/prompts/__init__.py +1 -8
  68. mcp_bridge/prompts/dewey.py +1 -1
  69. mcp_bridge/prompts/planner.py +2 -4
  70. mcp_bridge/prompts/stravinsky.py +53 -31
  71. mcp_bridge/proxy/__init__.py +0 -0
  72. mcp_bridge/proxy/client.py +70 -0
  73. mcp_bridge/proxy/model_server.py +157 -0
  74. mcp_bridge/routing/__init__.py +43 -0
  75. mcp_bridge/routing/config.py +250 -0
  76. mcp_bridge/routing/model_tiers.py +135 -0
  77. mcp_bridge/routing/provider_state.py +261 -0
  78. mcp_bridge/routing/task_classifier.py +190 -0
  79. mcp_bridge/server.py +542 -59
  80. mcp_bridge/server_tools.py +738 -6
  81. mcp_bridge/tools/__init__.py +40 -25
  82. mcp_bridge/tools/agent_manager.py +616 -697
  83. mcp_bridge/tools/background_tasks.py +13 -17
  84. mcp_bridge/tools/code_search.py +70 -53
  85. mcp_bridge/tools/continuous_loop.py +0 -1
  86. mcp_bridge/tools/dashboard.py +19 -0
  87. mcp_bridge/tools/find_code.py +296 -0
  88. mcp_bridge/tools/init.py +1 -0
  89. mcp_bridge/tools/list_directory.py +42 -0
  90. mcp_bridge/tools/lsp/__init__.py +12 -5
  91. mcp_bridge/tools/lsp/manager.py +471 -0
  92. mcp_bridge/tools/lsp/tools.py +723 -207
  93. mcp_bridge/tools/model_invoke.py +1195 -273
  94. mcp_bridge/tools/mux_client.py +75 -0
  95. mcp_bridge/tools/project_context.py +1 -2
  96. mcp_bridge/tools/query_classifier.py +406 -0
  97. mcp_bridge/tools/read_file.py +84 -0
  98. mcp_bridge/tools/replace.py +45 -0
  99. mcp_bridge/tools/run_shell_command.py +38 -0
  100. mcp_bridge/tools/search_enhancements.py +347 -0
  101. mcp_bridge/tools/semantic_search.py +3627 -0
  102. mcp_bridge/tools/session_manager.py +0 -2
  103. mcp_bridge/tools/skill_loader.py +0 -1
  104. mcp_bridge/tools/task_runner.py +5 -7
  105. mcp_bridge/tools/templates.py +3 -3
  106. mcp_bridge/tools/tool_search.py +331 -0
  107. mcp_bridge/tools/write_file.py +29 -0
  108. mcp_bridge/update_manager.py +585 -0
  109. mcp_bridge/update_manager_pypi.py +297 -0
  110. mcp_bridge/utils/cache.py +82 -0
  111. mcp_bridge/utils/process.py +71 -0
  112. mcp_bridge/utils/session_state.py +51 -0
  113. mcp_bridge/utils/truncation.py +76 -0
  114. stravinsky-0.4.66.dist-info/METADATA +517 -0
  115. stravinsky-0.4.66.dist-info/RECORD +198 -0
  116. {stravinsky-0.2.67.dist-info → stravinsky-0.4.66.dist-info}/entry_points.txt +1 -0
  117. stravinsky_claude_assets/HOOKS_INTEGRATION.md +316 -0
  118. stravinsky_claude_assets/agents/HOOKS.md +437 -0
  119. stravinsky_claude_assets/agents/code-reviewer.md +210 -0
  120. stravinsky_claude_assets/agents/comment_checker.md +580 -0
  121. stravinsky_claude_assets/agents/debugger.md +254 -0
  122. stravinsky_claude_assets/agents/delphi.md +495 -0
  123. stravinsky_claude_assets/agents/dewey.md +248 -0
  124. stravinsky_claude_assets/agents/explore.md +1198 -0
  125. stravinsky_claude_assets/agents/frontend.md +472 -0
  126. stravinsky_claude_assets/agents/implementation-lead.md +164 -0
  127. stravinsky_claude_assets/agents/momus.md +464 -0
  128. stravinsky_claude_assets/agents/research-lead.md +141 -0
  129. stravinsky_claude_assets/agents/stravinsky.md +730 -0
  130. stravinsky_claude_assets/commands/delphi.md +9 -0
  131. stravinsky_claude_assets/commands/dewey.md +54 -0
  132. stravinsky_claude_assets/commands/git-master.md +112 -0
  133. stravinsky_claude_assets/commands/index.md +49 -0
  134. stravinsky_claude_assets/commands/publish.md +86 -0
  135. stravinsky_claude_assets/commands/review.md +73 -0
  136. stravinsky_claude_assets/commands/str/agent_cancel.md +70 -0
  137. stravinsky_claude_assets/commands/str/agent_list.md +56 -0
  138. stravinsky_claude_assets/commands/str/agent_output.md +92 -0
  139. stravinsky_claude_assets/commands/str/agent_progress.md +74 -0
  140. stravinsky_claude_assets/commands/str/agent_retry.md +94 -0
  141. stravinsky_claude_assets/commands/str/cancel.md +51 -0
  142. stravinsky_claude_assets/commands/str/clean.md +97 -0
  143. stravinsky_claude_assets/commands/str/continue.md +38 -0
  144. stravinsky_claude_assets/commands/str/index.md +199 -0
  145. stravinsky_claude_assets/commands/str/list_watchers.md +96 -0
  146. stravinsky_claude_assets/commands/str/search.md +205 -0
  147. stravinsky_claude_assets/commands/str/start_filewatch.md +136 -0
  148. stravinsky_claude_assets/commands/str/stats.md +71 -0
  149. stravinsky_claude_assets/commands/str/stop_filewatch.md +89 -0
  150. stravinsky_claude_assets/commands/str/unwatch.md +42 -0
  151. stravinsky_claude_assets/commands/str/watch.md +45 -0
  152. stravinsky_claude_assets/commands/strav.md +53 -0
  153. stravinsky_claude_assets/commands/stravinsky.md +292 -0
  154. stravinsky_claude_assets/commands/verify.md +60 -0
  155. stravinsky_claude_assets/commands/version.md +5 -0
  156. stravinsky_claude_assets/hooks/README.md +248 -0
  157. stravinsky_claude_assets/hooks/comment_checker.py +193 -0
  158. stravinsky_claude_assets/hooks/context.py +38 -0
  159. stravinsky_claude_assets/hooks/context_monitor.py +153 -0
  160. stravinsky_claude_assets/hooks/dependency_tracker.py +73 -0
  161. stravinsky_claude_assets/hooks/edit_recovery.py +46 -0
  162. stravinsky_claude_assets/hooks/execution_state_tracker.py +68 -0
  163. stravinsky_claude_assets/hooks/notification_hook.py +103 -0
  164. stravinsky_claude_assets/hooks/notification_hook_v2.py +96 -0
  165. stravinsky_claude_assets/hooks/parallel_execution.py +241 -0
  166. stravinsky_claude_assets/hooks/parallel_reinforcement.py +106 -0
  167. stravinsky_claude_assets/hooks/parallel_reinforcement_v2.py +112 -0
  168. stravinsky_claude_assets/hooks/pre_compact.py +123 -0
  169. stravinsky_claude_assets/hooks/ralph_loop.py +173 -0
  170. stravinsky_claude_assets/hooks/session_recovery.py +263 -0
  171. stravinsky_claude_assets/hooks/stop_hook.py +89 -0
  172. stravinsky_claude_assets/hooks/stravinsky_metrics.py +164 -0
  173. stravinsky_claude_assets/hooks/stravinsky_mode.py +146 -0
  174. stravinsky_claude_assets/hooks/subagent_stop.py +98 -0
  175. stravinsky_claude_assets/hooks/todo_continuation.py +111 -0
  176. stravinsky_claude_assets/hooks/todo_delegation.py +96 -0
  177. stravinsky_claude_assets/hooks/tool_messaging.py +281 -0
  178. stravinsky_claude_assets/hooks/truncator.py +23 -0
  179. stravinsky_claude_assets/rules/deployment_safety.md +51 -0
  180. stravinsky_claude_assets/rules/integration_wiring.md +89 -0
  181. stravinsky_claude_assets/rules/pypi_deployment.md +220 -0
  182. stravinsky_claude_assets/rules/stravinsky_orchestrator.md +32 -0
  183. stravinsky_claude_assets/settings.json +152 -0
  184. stravinsky_claude_assets/skills/chrome-devtools/SKILL.md +81 -0
  185. stravinsky_claude_assets/skills/sqlite/SKILL.md +77 -0
  186. stravinsky_claude_assets/skills/supabase/SKILL.md +74 -0
  187. stravinsky_claude_assets/task_dependencies.json +34 -0
  188. stravinsky-0.2.67.dist-info/METADATA +0 -284
  189. stravinsky-0.2.67.dist-info/RECORD +0 -76
  190. {stravinsky-0.2.67.dist-info → stravinsky-0.4.66.dist-info}/WHEEL +0 -0
@@ -3,49 +3,84 @@ Todo Continuation Enforcer Hook.
3
3
 
4
4
  Prevents early stopping when pending todos exist.
5
5
  Injects a system reminder forcing the agent to complete all todos.
6
+ Includes evidence extraction and verification to prevent vague completion claims.
6
7
  """
7
8
 
8
9
  import logging
9
- from typing import Any, Dict, Optional
10
+ import re
11
+ from typing import Any
10
12
 
11
13
  logger = logging.getLogger(__name__)
12
14
 
13
15
  TODO_CONTINUATION_REMINDER = """
14
- [SYSTEM REMINDER - TODO CONTINUATION]
16
+ [SYSTEM REMINDER - TODO CONTINUATION & VERIFICATION]
15
17
 
16
18
  You have pending todos that are NOT yet completed. You MUST continue working.
17
19
 
18
20
  **Pending Todos:**
19
21
  {pending_todos}
20
22
 
21
- **Rules:**
22
- 1. You CANNOT stop or deliver a final answer while todos remain pending
23
- 2. Mark each todo `in_progress` before starting, `completed` immediately after
24
- 3. If a todo is blocked, mark it `cancelled` with explanation and create new actionable todos
25
- 4. Only after ALL todos are `completed` or `cancelled` can you deliver your final answer
23
+ **CRITICAL RULES:**
24
+ 1. You CANNOT mark a todo completed without CONCRETE EVIDENCE
25
+ 2. Evidence = file paths with line numbers (e.g., src/auth.ts:45-67) or tool output
26
+ 3. Vague claims like "I created the file" will be REJECTED
27
+ 4. Each completed todo MUST include: `✅ [Todo] - Evidence: path/to/file.py:123`
28
+ 5. If you cannot provide evidence, the todo is NOT complete - keep working
29
+ 6. Use Read tool to verify file contents before claiming completion
26
30
 
27
- CONTINUE WORKING NOW. Do not acknowledge this message - just proceed with the next pending todo.
31
+ **Example GOOD completion:**
32
+ ✅ Create auth validation → Evidence: src/auth.ts:45-67 (validateJWT function implemented)
33
+
34
+ **Example BAD completion (will be REJECTED):**
35
+ ✅ Create auth validation → I created the validation logic
36
+
37
+ {verification_failures}
38
+
39
+ CONTINUE WORKING NOW with evidence-backed completions.
28
40
  """
29
41
 
30
42
 
31
- async def todo_continuation_hook(params: Dict[str, Any]) -> Optional[Dict[str, Any]]:
43
+ async def todo_continuation_hook(params: dict[str, Any]) -> dict[str, Any] | None:
32
44
  """
33
45
  Pre-model invoke hook that checks for pending todos.
34
46
 
35
47
  If pending todos exist, injects a reminder into the prompt
36
48
  forcing the agent to continue working.
49
+
50
+ Also extracts evidence from agent output and verifies claims.
37
51
  """
38
52
  prompt = params.get("prompt", "")
39
53
 
54
+ # Extract pending todos
40
55
  pending_todos = _extract_pending_todos(prompt)
41
56
 
57
+ # Extract verification failures from previous output (if any)
58
+ verification_failures = ""
59
+ skip_verification = params.get("skip_verification", False)
60
+
61
+ if not skip_verification:
62
+ # Check if there's recent output to verify
63
+ # This would come from previous agent turns
64
+ previous_output = params.get("previous_output", "")
65
+ if previous_output:
66
+ verification_failures = _verify_agent_claims(previous_output)
67
+
42
68
  if pending_todos:
43
69
  logger.info(
44
70
  f"[TodoEnforcer] Found {len(pending_todos)} pending todos, injecting continuation reminder"
45
71
  )
46
72
 
47
73
  todos_formatted = "\n".join(f"- [ ] {todo}" for todo in pending_todos)
48
- reminder = TODO_CONTINUATION_REMINDER.format(pending_todos=todos_formatted)
74
+
75
+ # Format verification failures if any
76
+ failures_text = ""
77
+ if verification_failures:
78
+ failures_text = f"\n\n⚠️ VERIFICATION FAILURES FROM PREVIOUS TURN:\n{verification_failures}\n"
79
+
80
+ reminder = TODO_CONTINUATION_REMINDER.format(
81
+ pending_todos=todos_formatted,
82
+ verification_failures=failures_text
83
+ )
49
84
 
50
85
  modified_prompt = prompt + "\n\n" + reminder
51
86
  params["prompt"] = modified_prompt
@@ -73,3 +108,138 @@ def _extract_pending_todos(prompt: str) -> list:
73
108
  pass
74
109
 
75
110
  return pending
111
+
112
+
113
+ def _extract_evidence(output: str) -> dict[str, list[str]]:
114
+ """
115
+ Extract evidence references (file paths, URLs) from agent output.
116
+
117
+ Returns:
118
+ Dict with keys: 'files', 'urls', 'commands'
119
+ """
120
+ evidence = {
121
+ "files": [],
122
+ "urls": [],
123
+ "commands": []
124
+ }
125
+
126
+ # File path pattern: src/auth.ts:45 or /path/to/file.py or path/file.js:10-20
127
+ # Matches common file extensions and optional line numbers
128
+ file_pattern = r'(?:^|[\s\(\[])([\w/\._-]+\.(?:py|ts|js|tsx|jsx|go|rs|java|c|cpp|h|hpp|md|json|yaml|yml|toml|sh|rb|php|swift|kt))(?::(\d+)(?:-(\d+))?)?'
129
+
130
+ for match in re.finditer(file_pattern, output, re.MULTILINE):
131
+ file_path = match.group(1)
132
+ line_start = match.group(2)
133
+ line_end = match.group(3)
134
+
135
+ # Build reference string
136
+ if line_start:
137
+ if line_end:
138
+ ref = f"{file_path}:{line_start}-{line_end}"
139
+ else:
140
+ ref = f"{file_path}:{line_start}"
141
+ else:
142
+ ref = file_path
143
+
144
+ evidence["files"].append(ref)
145
+
146
+ # URL pattern
147
+ url_pattern = r'https?://[^\s\)\]>]+'
148
+ evidence["urls"] = re.findall(url_pattern, output)
149
+
150
+ # Command/tool usage pattern (e.g., "Used Read tool", "Ran grep")
151
+ command_pattern = r'(?:Used|Ran|Called|Executed)\s+(\w+(?:\s+\w+)?)\s+(?:tool|command)'
152
+ evidence["commands"] = re.findall(command_pattern, output, re.IGNORECASE)
153
+
154
+ return evidence
155
+
156
+
157
+ def _verify_file_claim(claim: str, file_references: list[str]) -> dict[str, Any]:
158
+ """
159
+ Verify a completion claim has file evidence.
160
+
161
+ This is a synchronous check - actual file existence verification
162
+ would require async Read tool access (not available in hooks).
163
+
164
+ Returns:
165
+ Dict with 'verified' (bool) and 'reason' (str)
166
+ """
167
+ # Check if claim has any file references
168
+ if not file_references:
169
+ return {
170
+ "verified": False,
171
+ "reason": "No file paths provided as evidence"
172
+ }
173
+
174
+ # Check for vague language that indicates lack of actual work
175
+ vague_patterns = [
176
+ r'\bI\s+(?:created|made|wrote|added|implemented)\b', # "I created..."
177
+ r'\b(?:should|will|would)\s+(?:create|add|implement)\b', # Future tense
178
+ r'\b(?:basically|essentially|just|simply)\b', # Minimizing language
179
+ ]
180
+
181
+ claim_lower = claim.lower()
182
+ vague_count = sum(1 for pattern in vague_patterns if re.search(pattern, claim_lower))
183
+
184
+ if vague_count >= 2:
185
+ return {
186
+ "verified": False,
187
+ "reason": f"Claim uses vague language without concrete evidence. Files mentioned: {', '.join(file_references[:3])}"
188
+ }
189
+
190
+ # If we have file references and no vague language, consider it verified
191
+ # (Actual file content verification would happen in a post-hook with Read access)
192
+ return {
193
+ "verified": True,
194
+ "reason": f"Evidence provided: {', '.join(file_references[:3])}"
195
+ }
196
+
197
+
198
+ def _verify_agent_claims(output: str) -> str:
199
+ """
200
+ Verify agent claims against actual evidence.
201
+
202
+ Extracts completion claims and checks for concrete evidence.
203
+
204
+ Returns:
205
+ Formatted string of verification failures (empty if all verified)
206
+ """
207
+ # Extract evidence from output
208
+ evidence = _extract_evidence(output)
209
+
210
+ # Look for completion claims (✅, "completed", "done", etc.)
211
+ completion_patterns = [
212
+ r'✅\s+(.+?)(?:\n|$)', # Checkmark pattern
213
+ r'(?:Completed|Finished|Done):\s*(.+?)(?:\n|$)', # Explicit completion
214
+ r'"status":\s*"completed".*?"content":\s*"(.+?)"', # JSON todo format
215
+ ]
216
+
217
+ claims = []
218
+ for pattern in completion_patterns:
219
+ matches = re.finditer(pattern, output, re.IGNORECASE | re.DOTALL)
220
+ for match in matches:
221
+ claim_text = match.group(1).strip()
222
+ if claim_text:
223
+ claims.append(claim_text)
224
+
225
+ if not claims:
226
+ # No completion claims found, nothing to verify
227
+ return ""
228
+
229
+ # Verify each claim
230
+ failures = []
231
+ for claim in claims:
232
+ verification = _verify_file_claim(claim, evidence["files"])
233
+ if not verification["verified"]:
234
+ failures.append(f"- {claim[:100]}... → {verification['reason']}")
235
+
236
+ if failures:
237
+ return "\n".join([
238
+ "The following completion claims lack concrete evidence:",
239
+ *failures,
240
+ "",
241
+ "REQUIRED: Provide file paths with line numbers (e.g., src/auth.ts:45-67)",
242
+ "Use the Read tool to verify files exist before claiming completion."
243
+ ])
244
+
245
+ return ""
@@ -4,9 +4,13 @@ PostToolUse hook for user-friendly tool messaging.
4
4
 
5
5
  Outputs concise messages about which agent/tool was used and what it did.
6
6
  Format examples:
7
- - ast-grep('Searching for authentication patterns')
8
- - delphi:openai/gpt-5.2-medium('Analyzing architecture trade-offs')
9
- - explore:gemini-3-flash('Finding all API endpoints')
7
+ - 🔧 ast-grep:stravinsky('Searching for authentication patterns')
8
+ - 🟡 get_file_contents:github('Fetching src/main.py from user/repo')
9
+ - 🟣 searchCode:grep-app('Searching GitHub for auth patterns')
10
+ - 🔵 web_search_exa:MCP_DOCKER('Web search for Docker best practices')
11
+ - 🟤 find_code:ast-grep('AST search for class definitions')
12
+ - 🎯 delphi:gpt-5.2-medium('Analyzing architecture trade-offs')
13
+ - 🎯 explore:gemini-3-flash('Finding all API endpoints')
10
14
  """
11
15
 
12
16
  import json
@@ -23,7 +27,16 @@ AGENT_MODELS = {
23
27
  "delphi": "gpt-5.2-medium",
24
28
  }
25
29
 
26
- # Tool display names
30
+ # MCP Server emoji mappings
31
+ SERVER_EMOJIS = {
32
+ "github": "🟡",
33
+ "ast-grep": "🟤",
34
+ "grep-app": "🟣",
35
+ "MCP_DOCKER": "🔵",
36
+ "stravinsky": "🔧",
37
+ }
38
+
39
+ # Tool display names (legacy mapping for simple tools)
27
40
  TOOL_NAMES = {
28
41
  "mcp__stravinsky__ast_grep_search": "ast-grep",
29
42
  "mcp__stravinsky__grep_search": "grep",
@@ -41,10 +54,98 @@ TOOL_NAMES = {
41
54
  }
42
55
 
43
56
 
57
+ def parse_mcp_tool_name(tool_name: str) -> tuple[str, str, str]:
58
+ """
59
+ Parse MCP tool name into (server, tool_type, emoji).
60
+
61
+ Examples:
62
+ mcp__github__get_file_contents -> ("github", "get_file_contents", "🟡")
63
+ mcp__stravinsky__grep_search -> ("stravinsky", "grep", "🔧")
64
+ mcp__ast-grep__find_code -> ("ast-grep", "find_code", "🟤")
65
+ """
66
+ if not tool_name.startswith("mcp__"):
67
+ return ("unknown", tool_name, "🔧")
68
+
69
+ # Remove mcp__ prefix and split by __
70
+ parts = tool_name[5:].split("__", 1)
71
+ if len(parts) != 2:
72
+ return ("unknown", tool_name, "🔧")
73
+
74
+ server = parts[0]
75
+ tool_type = parts[1]
76
+
77
+ # Get emoji for server
78
+ emoji = SERVER_EMOJIS.get(server, "🔧")
79
+
80
+ # Get simplified tool name if available
81
+ simple_name = TOOL_NAMES.get(tool_name, tool_type)
82
+
83
+ return (server, simple_name, emoji)
84
+
85
+
44
86
  def extract_description(tool_name: str, params: dict) -> str:
45
87
  """Extract a concise description of what the tool did."""
46
88
 
47
- # AST-grep
89
+ # GitHub tools
90
+ if "github" in tool_name.lower():
91
+ if "get_file_contents" in tool_name:
92
+ path = params.get("path", "")
93
+ repo = params.get("repo", "")
94
+ owner = params.get("owner", "")
95
+ return f"Fetching {path} from {owner}/{repo}"
96
+ elif "create_or_update_file" in tool_name:
97
+ path = params.get("path", "")
98
+ return f"Updating {path}"
99
+ elif "search_repositories" in tool_name:
100
+ query = params.get("query", "")
101
+ return f"Searching repos for '{query[:40]}'"
102
+ elif "search_code" in tool_name:
103
+ q = params.get("q", "")
104
+ return f"Searching code for '{q[:40]}'"
105
+ elif "create_pull_request" in tool_name:
106
+ title = params.get("title", "")
107
+ return f"Creating PR: {title[:40]}"
108
+ elif "get_pull_request" in tool_name or "list_pull_requests" in tool_name:
109
+ return "Fetching PR details"
110
+ return "GitHub operation"
111
+
112
+ # MCP_DOCKER tools
113
+ if "MCP_DOCKER" in tool_name:
114
+ if "web_search_exa" in tool_name:
115
+ query = params.get("query", "")
116
+ return f"Web search: '{query[:40]}'"
117
+ elif "create_entities" in tool_name:
118
+ entities = params.get("entities", [])
119
+ count = len(entities)
120
+ return f"Creating {count} knowledge graph entities"
121
+ elif "search_nodes" in tool_name:
122
+ query = params.get("query", "")
123
+ return f"Searching knowledge graph for '{query[:40]}'"
124
+ return "Knowledge graph operation"
125
+
126
+ # ast-grep tools
127
+ if "ast-grep" in tool_name or "ast_grep" in tool_name:
128
+ if "find_code" in tool_name or "search" in tool_name:
129
+ pattern = params.get("pattern", "")
130
+ return f"AST search for '{pattern[:40]}'"
131
+ elif "test_match" in tool_name:
132
+ return "Testing AST pattern"
133
+ elif "dump_syntax" in tool_name:
134
+ return "Dumping syntax tree"
135
+ return "AST operation"
136
+
137
+ # grep-app tools
138
+ if "grep-app" in tool_name or "grep_app" in tool_name:
139
+ if "searchCode" in tool_name:
140
+ query = params.get("query", "")
141
+ return f"Searching GitHub for '{query[:40]}'"
142
+ elif "github_file" in tool_name:
143
+ path = params.get("path", "")
144
+ repo = params.get("repo", "")
145
+ return f"Fetching {path} from {repo}"
146
+ return "grep.app search"
147
+
148
+ # AST-grep (stravinsky)
48
149
  if "ast_grep" in tool_name:
49
150
  pattern = params.get("pattern", "")
50
151
  directory = params.get("directory", ".")
@@ -136,9 +237,6 @@ def main():
136
237
  if not (tool_name.startswith("mcp__") or tool_name == "Task"):
137
238
  sys.exit(0)
138
239
 
139
- # Get tool display name
140
- display_name = TOOL_NAMES.get(tool_name, tool_name)
141
-
142
240
  # Special handling for Task delegations
143
241
  if tool_name == "Task":
144
242
  subagent_type = params.get("subagent_type", "unknown")
@@ -148,9 +246,14 @@ def main():
148
246
  # Show full agent delegation message
149
247
  print(f"🎯 {subagent_type}:{model}('{description}')", file=sys.stderr)
150
248
  else:
151
- # Regular tool usage
249
+ # Parse MCP tool name to get server, tool_type, and emoji
250
+ server, tool_type, emoji = parse_mcp_tool_name(tool_name)
251
+
252
+ # Get description of what the tool did
152
253
  description = extract_description(tool_name, params)
153
- print(f"🔧 {display_name}('{description}')", file=sys.stderr)
254
+
255
+ # Format output: emoji tool_type:server('description')
256
+ print(f"{emoji} {tool_type}:{server}('{description}')", file=sys.stderr)
154
257
 
155
258
  sys.exit(0)
156
259
 
@@ -0,0 +1,37 @@
1
+ from .events import EventType, HookPolicy, PolicyResult, ToolCallEvent
2
+ from ..utils.truncation import truncate_output, TruncationStrategy
3
+
4
+
5
+ class TruncationPolicy(HookPolicy):
6
+ def __init__(self, max_chars: int = 20000):
7
+ self.max_chars = max_chars
8
+
9
+ @property
10
+ def event_type(self) -> EventType:
11
+ return EventType.POST_TOOL_CALL
12
+
13
+ async def evaluate(self, event: ToolCallEvent) -> PolicyResult:
14
+ if not event.output or len(event.output) <= self.max_chars:
15
+ return PolicyResult(modified_data=event.output)
16
+
17
+ # Skip truncation for read_file since it handles its own truncation with log-awareness
18
+ if event.tool_name == "read_file":
19
+ return PolicyResult(modified_data=event.output)
20
+
21
+ # Use middle truncation for general tool outputs
22
+ modified = truncate_output(
23
+ event.output,
24
+ limit=self.max_chars,
25
+ strategy=TruncationStrategy.MIDDLE
26
+ )
27
+
28
+ return PolicyResult(
29
+ modified_data=modified,
30
+ message=modified, # Message is what gets printed in run_as_native
31
+ )
32
+
33
+
34
+ if __name__ == "__main__":
35
+ policy = TruncationPolicy()
36
+ policy.run_as_native()
37
+
@@ -1,6 +1,5 @@
1
- import os
2
- import sys
3
1
  import json
2
+ import sys
4
3
 
5
4
  MAX_CHARS = 30000
6
5
 
@@ -0,0 +1,115 @@
1
+ import json
2
+ import time
3
+ import os
4
+ from pathlib import Path
5
+ from dataclasses import dataclass, asdict
6
+ import threading
7
+ import logging
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+ # Approximate costs per 1M tokens (Input/Output)
12
+ MODEL_COSTS = {
13
+ "gemini-3-flash": (0.075, 0.30),
14
+ "gemini-3-pro": (1.25, 5.00),
15
+ "gpt-5.2-codex": (2.50, 10.00), # Estimated based on GPT-4o
16
+ "gpt-4o": (2.50, 10.00),
17
+ "claude-3-5-sonnet": (3.00, 15.00),
18
+ "claude-3-5-haiku": (0.25, 1.25),
19
+ }
20
+
21
+ @dataclass
22
+ class CostRecord:
23
+ timestamp: float
24
+ model: str
25
+ input_tokens: int
26
+ output_tokens: int
27
+ cost: float
28
+ agent_type: str
29
+ task_id: str
30
+ session_id: str
31
+
32
+ class CostTracker:
33
+ _instance = None
34
+ _lock = threading.Lock()
35
+
36
+ def __init__(self):
37
+ self.file_path = Path.home() / ".stravinsky" / "usage.jsonl"
38
+ self.file_path.parent.mkdir(parents=True, exist_ok=True)
39
+
40
+ @classmethod
41
+ def get_instance(cls):
42
+ if cls._instance is None:
43
+ with cls._lock:
44
+ if cls._instance is None:
45
+ cls._instance = cls()
46
+ return cls._instance
47
+
48
+ def calculate_cost(self, model: str, input_tokens: int, output_tokens: int) -> float:
49
+ # Default to Flash pricing if unknown
50
+ input_price, output_price = MODEL_COSTS.get(model, MODEL_COSTS["gemini-3-flash"])
51
+ return (input_tokens / 1_000_000 * input_price) + (output_tokens / 1_000_000 * output_price)
52
+
53
+ def track_usage(self, model: str, input_tokens: int, output_tokens: int, agent_type: str = "unknown", task_id: str = ""):
54
+ cost = self.calculate_cost(model, input_tokens, output_tokens)
55
+ session_id = os.environ.get("CLAUDE_SESSION_ID", "default")
56
+
57
+ record = CostRecord(
58
+ timestamp=time.time(),
59
+ model=model,
60
+ input_tokens=input_tokens,
61
+ output_tokens=output_tokens,
62
+ cost=cost,
63
+ agent_type=agent_type,
64
+ task_id=task_id,
65
+ session_id=session_id
66
+ )
67
+
68
+ try:
69
+ with open(self.file_path, "a") as f:
70
+ f.write(json.dumps(asdict(record)) + "\n")
71
+ except Exception as e:
72
+ logger.error(f"Failed to write usage record: {e}")
73
+
74
+ def get_session_summary(self, session_id: str | None = None) -> dict:
75
+ if session_id is None:
76
+ session_id = os.environ.get("CLAUDE_SESSION_ID", "default")
77
+
78
+ total_cost = 0.0
79
+ total_tokens = 0
80
+ by_agent = {}
81
+
82
+ if not self.file_path.exists():
83
+ return {"total_cost": 0.0, "total_tokens": 0, "by_agent": {}}
84
+
85
+ try:
86
+ with open(self.file_path, "r") as f:
87
+ for line in f:
88
+ try:
89
+ data = json.loads(line)
90
+ if data.get("session_id") == session_id:
91
+ cost = data.get("cost", 0.0)
92
+ tokens = data.get("input_tokens", 0) + data.get("output_tokens", 0)
93
+ agent = data.get("agent_type", "unknown")
94
+
95
+ total_cost += cost
96
+ total_tokens += tokens
97
+
98
+ if agent not in by_agent:
99
+ by_agent[agent] = {"cost": 0.0, "tokens": 0}
100
+ by_agent[agent]["cost"] += cost
101
+ by_agent[agent]["tokens"] += tokens
102
+
103
+ except json.JSONDecodeError:
104
+ continue
105
+ except Exception as e:
106
+ logger.error(f"Failed to read usage records: {e}")
107
+
108
+ return {
109
+ "total_cost": total_cost,
110
+ "total_tokens": total_tokens,
111
+ "by_agent": by_agent
112
+ }
113
+
114
+ def get_cost_tracker():
115
+ return CostTracker.get_instance()
@@ -0,0 +1,93 @@
1
+ """
2
+ Native Search Wrapper - Optional Rust integration for performance.
3
+ """
4
+
5
+ import os
6
+ import logging
7
+ import asyncio
8
+ from concurrent.futures import ThreadPoolExecutor
9
+ from typing import List, Dict, Any, Optional
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ # Attempt to import the native module
14
+ try:
15
+ import stravinsky_native
16
+ HAS_NATIVE = True
17
+ except ImportError:
18
+ HAS_NATIVE = False
19
+ logger.debug("stravinsky_native module not found. Falling back to CLI tools.")
20
+
21
+ _executor: Optional[ThreadPoolExecutor] = None
22
+
23
+ def get_executor() -> ThreadPoolExecutor:
24
+ """Get the singleton thread pool executor."""
25
+ global _executor
26
+ if _executor is None:
27
+ # Limit worker threads to avoid overwhelming the system
28
+ _executor = ThreadPoolExecutor(max_workers=4, thread_name_prefix="native_ffi")
29
+ return _executor
30
+
31
+ async def native_glob_files(pattern: str, directory: str = ".") -> Optional[List[str]]:
32
+ """
33
+ Find files matching a glob pattern using Rust implementation.
34
+ """
35
+ if not HAS_NATIVE:
36
+ return None
37
+
38
+ try:
39
+ # Convert to absolute path for Rust
40
+ abs_dir = os.path.abspath(directory)
41
+ loop = asyncio.get_running_loop()
42
+
43
+ # Offload blocking FFI call to thread pool
44
+ return await loop.run_in_executor(
45
+ get_executor(),
46
+ stravinsky_native.glob_files,
47
+ abs_dir,
48
+ pattern
49
+ )
50
+ except Exception as e:
51
+ logger.error(f"Native glob_files failed: {e}")
52
+ return None
53
+
54
+ async def native_grep_search(pattern: str, directory: str = ".", case_sensitive: bool = False) -> Optional[List[Dict[str, Any]]]:
55
+ """
56
+ Fast text search using Rust implementation.
57
+ """
58
+ if not HAS_NATIVE:
59
+ return None
60
+
61
+ try:
62
+ abs_dir = os.path.abspath(directory)
63
+ loop = asyncio.get_running_loop()
64
+
65
+ return await loop.run_in_executor(
66
+ get_executor(),
67
+ stravinsky_native.grep_search,
68
+ pattern,
69
+ abs_dir,
70
+ case_sensitive
71
+ )
72
+ except Exception as e:
73
+ logger.error(f"Native grep_search failed: {e}")
74
+ return None
75
+
76
+ async def native_chunk_code(content: str, language: str) -> Optional[List[Dict[str, Any]]]:
77
+ """
78
+ AST-aware code chunking using Rust/tree-sitter.
79
+ """
80
+ if not HAS_NATIVE:
81
+ return None
82
+
83
+ try:
84
+ loop = asyncio.get_running_loop()
85
+ return await loop.run_in_executor(
86
+ get_executor(),
87
+ stravinsky_native.chunk_code,
88
+ content,
89
+ language
90
+ )
91
+ except Exception as e:
92
+ logger.error(f"Native chunk_code failed: {e}")
93
+ return None