stravinsky 0.2.40__py3-none-any.whl → 0.3.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mcp_bridge/__init__.py +1 -1
- mcp_bridge/auth/token_refresh.py +130 -0
- mcp_bridge/cli/__init__.py +6 -0
- mcp_bridge/cli/install_hooks.py +1265 -0
- mcp_bridge/cli/session_report.py +585 -0
- mcp_bridge/hooks/HOOKS_SETTINGS.json +175 -0
- mcp_bridge/hooks/README.md +215 -0
- mcp_bridge/hooks/__init__.py +119 -43
- mcp_bridge/hooks/edit_recovery.py +42 -37
- mcp_bridge/hooks/git_noninteractive.py +89 -0
- mcp_bridge/hooks/keyword_detector.py +30 -0
- mcp_bridge/hooks/manager.py +50 -0
- mcp_bridge/hooks/notification_hook.py +103 -0
- mcp_bridge/hooks/parallel_enforcer.py +127 -0
- mcp_bridge/hooks/parallel_execution.py +111 -0
- mcp_bridge/hooks/pre_compact.py +123 -0
- mcp_bridge/hooks/preemptive_compaction.py +81 -7
- mcp_bridge/hooks/rules_injector.py +507 -0
- mcp_bridge/hooks/session_idle.py +116 -0
- mcp_bridge/hooks/session_notifier.py +125 -0
- mcp_bridge/{native_hooks → hooks}/stravinsky_mode.py +51 -16
- mcp_bridge/hooks/subagent_stop.py +98 -0
- mcp_bridge/hooks/task_validator.py +73 -0
- mcp_bridge/hooks/tmux_manager.py +141 -0
- mcp_bridge/hooks/todo_continuation.py +90 -0
- mcp_bridge/hooks/todo_delegation.py +88 -0
- mcp_bridge/hooks/tool_messaging.py +164 -0
- mcp_bridge/hooks/truncator.py +21 -17
- mcp_bridge/notifications.py +151 -0
- mcp_bridge/prompts/__init__.py +3 -1
- mcp_bridge/prompts/dewey.py +30 -20
- mcp_bridge/prompts/explore.py +46 -8
- mcp_bridge/prompts/multimodal.py +24 -3
- mcp_bridge/prompts/planner.py +222 -0
- mcp_bridge/prompts/stravinsky.py +107 -28
- mcp_bridge/server.py +170 -10
- mcp_bridge/server_tools.py +554 -32
- mcp_bridge/tools/agent_manager.py +316 -106
- mcp_bridge/tools/background_tasks.py +2 -1
- mcp_bridge/tools/code_search.py +97 -11
- mcp_bridge/tools/lsp/__init__.py +7 -0
- mcp_bridge/tools/lsp/manager.py +448 -0
- mcp_bridge/tools/lsp/tools.py +637 -150
- mcp_bridge/tools/model_invoke.py +270 -47
- mcp_bridge/tools/semantic_search.py +2492 -0
- mcp_bridge/tools/templates.py +32 -18
- stravinsky-0.3.4.dist-info/METADATA +420 -0
- stravinsky-0.3.4.dist-info/RECORD +79 -0
- stravinsky-0.3.4.dist-info/entry_points.txt +5 -0
- mcp_bridge/native_hooks/edit_recovery.py +0 -46
- mcp_bridge/native_hooks/truncator.py +0 -23
- stravinsky-0.2.40.dist-info/METADATA +0 -204
- stravinsky-0.2.40.dist-info/RECORD +0 -57
- stravinsky-0.2.40.dist-info/entry_points.txt +0 -3
- /mcp_bridge/{native_hooks → hooks}/context.py +0 -0
- {stravinsky-0.2.40.dist-info → stravinsky-0.3.4.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Session Report CLI
|
|
4
|
+
|
|
5
|
+
A rich CLI tool for viewing Claude Code sessions with tool/agent/model summaries.
|
|
6
|
+
Optionally uses Gemini for session summarization.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
import sys
|
|
14
|
+
from collections import Counter
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from rich.console import Console
|
|
20
|
+
from rich.panel import Panel
|
|
21
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
22
|
+
from rich.table import Table
|
|
23
|
+
from rich.text import Text
|
|
24
|
+
from rich.tree import Tree
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
console = Console()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_sessions_directory() -> Path:
|
|
31
|
+
"""Get the Claude sessions directory."""
|
|
32
|
+
return Path.home() / ".claude" / "projects"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_sessions(limit: int = 10, search_id: str | None = None) -> list[dict]:
|
|
36
|
+
"""Get recent sessions sorted by modification time.
|
|
37
|
+
|
|
38
|
+
If search_id is provided, searches ALL sessions (ignores limit).
|
|
39
|
+
"""
|
|
40
|
+
sessions_dir = get_sessions_directory()
|
|
41
|
+
if not sessions_dir.exists():
|
|
42
|
+
return []
|
|
43
|
+
|
|
44
|
+
sessions = []
|
|
45
|
+
for project_dir in sessions_dir.iterdir():
|
|
46
|
+
if not project_dir.is_dir():
|
|
47
|
+
continue
|
|
48
|
+
|
|
49
|
+
for session_file in project_dir.glob("*.jsonl"):
|
|
50
|
+
try:
|
|
51
|
+
stat = session_file.stat()
|
|
52
|
+
mtime = datetime.fromtimestamp(stat.st_mtime)
|
|
53
|
+
sessions.append({
|
|
54
|
+
"id": session_file.stem,
|
|
55
|
+
"path": str(session_file),
|
|
56
|
+
"project": project_dir.name,
|
|
57
|
+
"modified": mtime,
|
|
58
|
+
"size": stat.st_size,
|
|
59
|
+
})
|
|
60
|
+
except Exception:
|
|
61
|
+
continue
|
|
62
|
+
|
|
63
|
+
sessions.sort(key=lambda s: s["modified"], reverse=True)
|
|
64
|
+
|
|
65
|
+
# If searching by ID, don't limit
|
|
66
|
+
if search_id:
|
|
67
|
+
return sessions
|
|
68
|
+
|
|
69
|
+
return sessions[:limit]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def read_session_messages(session_path: str) -> list[dict]:
|
|
73
|
+
"""Read all messages from a session file."""
|
|
74
|
+
messages = []
|
|
75
|
+
try:
|
|
76
|
+
with open(session_path) as f:
|
|
77
|
+
for line in f:
|
|
78
|
+
if line.strip():
|
|
79
|
+
try:
|
|
80
|
+
msg = json.loads(line)
|
|
81
|
+
messages.append(msg)
|
|
82
|
+
except json.JSONDecodeError:
|
|
83
|
+
continue
|
|
84
|
+
except Exception:
|
|
85
|
+
pass
|
|
86
|
+
return messages
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def extract_tool_usage(messages: list[dict]) -> dict[str, Any]:
|
|
90
|
+
"""Extract tool, agent, and model usage from session messages.
|
|
91
|
+
|
|
92
|
+
Claude Code session format:
|
|
93
|
+
- Top-level: {type: "user"|"assistant", message: {role, content, model?}}
|
|
94
|
+
- content can be string or list of {type: "text"|"tool_use"|"tool_result", ...}
|
|
95
|
+
- tool_use has: {type: "tool_use", name: "ToolName", input: {...}}
|
|
96
|
+
|
|
97
|
+
Captures:
|
|
98
|
+
1. Subagents spawned (Task tool with subagent_type, MCP agent_spawn)
|
|
99
|
+
2. External models invoked (invoke_gemini, invoke_openai with model param)
|
|
100
|
+
3. All tools used (native and MCP)
|
|
101
|
+
4. LSP tools specifically
|
|
102
|
+
"""
|
|
103
|
+
# Native Claude tools (Read, Write, Edit, Bash, etc.)
|
|
104
|
+
native_tools = Counter()
|
|
105
|
+
# MCP tools by server (stravinsky, github, grep-app, etc.)
|
|
106
|
+
mcp_tools_by_server: dict[str, Counter] = {}
|
|
107
|
+
# Subagents from Task tool
|
|
108
|
+
subagents = Counter()
|
|
109
|
+
# MCP agents from agent_spawn
|
|
110
|
+
mcp_agents = Counter()
|
|
111
|
+
# Claude model used for responses
|
|
112
|
+
claude_models = Counter()
|
|
113
|
+
# External models invoked (gemini, openai)
|
|
114
|
+
external_models = Counter()
|
|
115
|
+
# LSP tools specifically
|
|
116
|
+
lsp_tools = Counter()
|
|
117
|
+
|
|
118
|
+
for msg in messages:
|
|
119
|
+
msg_type = msg.get("type", "")
|
|
120
|
+
inner_msg = msg.get("message", {})
|
|
121
|
+
|
|
122
|
+
# Skip non-message types (snapshots, etc.)
|
|
123
|
+
if msg_type not in ("user", "assistant"):
|
|
124
|
+
continue
|
|
125
|
+
|
|
126
|
+
# Extract Claude model from assistant messages
|
|
127
|
+
model = inner_msg.get("model", "")
|
|
128
|
+
if model and msg_type == "assistant":
|
|
129
|
+
claude_models[model] += 1
|
|
130
|
+
|
|
131
|
+
content = inner_msg.get("content", "")
|
|
132
|
+
|
|
133
|
+
# Handle content as list (tool_use, text blocks, etc.)
|
|
134
|
+
if isinstance(content, list):
|
|
135
|
+
for block in content:
|
|
136
|
+
if not isinstance(block, dict):
|
|
137
|
+
continue
|
|
138
|
+
|
|
139
|
+
block_type = block.get("type", "")
|
|
140
|
+
|
|
141
|
+
# Tool use blocks
|
|
142
|
+
if block_type == "tool_use":
|
|
143
|
+
tool_name = block.get("name", "unknown")
|
|
144
|
+
tool_input = block.get("input", {})
|
|
145
|
+
|
|
146
|
+
# MCP tools (format: mcp__server__tool)
|
|
147
|
+
if tool_name.startswith("mcp__"):
|
|
148
|
+
parts = tool_name.split("__")
|
|
149
|
+
if len(parts) >= 3:
|
|
150
|
+
server = parts[1]
|
|
151
|
+
tool = parts[2]
|
|
152
|
+
|
|
153
|
+
# Track by server
|
|
154
|
+
if server not in mcp_tools_by_server:
|
|
155
|
+
mcp_tools_by_server[server] = Counter()
|
|
156
|
+
mcp_tools_by_server[server][tool] += 1
|
|
157
|
+
|
|
158
|
+
# Stravinsky-specific: agent_spawn
|
|
159
|
+
if server == "stravinsky" and tool == "agent_spawn":
|
|
160
|
+
agent_type = tool_input.get("agent_type", "explore")
|
|
161
|
+
model_used = tool_input.get("model", "gemini-3-flash")
|
|
162
|
+
mcp_agents[f"{agent_type} ({model_used})"] += 1
|
|
163
|
+
|
|
164
|
+
# Stravinsky-specific: invoke_gemini
|
|
165
|
+
if server == "stravinsky" and tool == "invoke_gemini":
|
|
166
|
+
model_used = tool_input.get("model", "gemini-3-flash")
|
|
167
|
+
external_models[f"gemini:{model_used}"] += 1
|
|
168
|
+
|
|
169
|
+
# Stravinsky-specific: invoke_openai
|
|
170
|
+
if server == "stravinsky" and tool == "invoke_openai":
|
|
171
|
+
model_used = tool_input.get("model", "gpt-5.2-codex")
|
|
172
|
+
external_models[f"openai:{model_used}"] += 1
|
|
173
|
+
|
|
174
|
+
# LSP tools
|
|
175
|
+
if server == "stravinsky" and tool.startswith("lsp_"):
|
|
176
|
+
lsp_tools[tool] += 1
|
|
177
|
+
|
|
178
|
+
# Native Task tool (spawns subagents)
|
|
179
|
+
elif tool_name == "Task":
|
|
180
|
+
subagent = tool_input.get("subagent_type", "")
|
|
181
|
+
if subagent:
|
|
182
|
+
subagents[subagent] += 1
|
|
183
|
+
|
|
184
|
+
# Other native tools
|
|
185
|
+
else:
|
|
186
|
+
native_tools[tool_name] += 1
|
|
187
|
+
|
|
188
|
+
# Build categorized MCP tools dict
|
|
189
|
+
mcp_tools_flat = {}
|
|
190
|
+
for server, tools in mcp_tools_by_server.items():
|
|
191
|
+
for tool, count in tools.items():
|
|
192
|
+
mcp_tools_flat[f"{server}:{tool}"] = count
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
"native_tools": dict(native_tools),
|
|
196
|
+
"mcp_tools": mcp_tools_flat,
|
|
197
|
+
"mcp_tools_by_server": {s: dict(t) for s, t in mcp_tools_by_server.items()},
|
|
198
|
+
"subagents": dict(subagents),
|
|
199
|
+
"mcp_agents": dict(mcp_agents),
|
|
200
|
+
"claude_models": dict(claude_models),
|
|
201
|
+
"external_models": dict(external_models),
|
|
202
|
+
"lsp_tools": dict(lsp_tools),
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def format_size(size_bytes: int) -> str:
|
|
207
|
+
"""Format bytes to human readable size."""
|
|
208
|
+
if size_bytes < 1024:
|
|
209
|
+
return f"{size_bytes} B"
|
|
210
|
+
elif size_bytes < 1024 * 1024:
|
|
211
|
+
return f"{size_bytes / 1024:.1f} KB"
|
|
212
|
+
else:
|
|
213
|
+
return f"{size_bytes / (1024 * 1024):.1f} MB"
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def display_session_list(sessions: list[dict]) -> None:
|
|
217
|
+
"""Display sessions in a rich table."""
|
|
218
|
+
table = Table(title="Recent Claude Code Sessions", show_header=True, header_style="bold magenta")
|
|
219
|
+
table.add_column("#", style="dim", width=3)
|
|
220
|
+
table.add_column("Session ID", style="cyan", width=15)
|
|
221
|
+
table.add_column("Modified", style="green", width=20)
|
|
222
|
+
table.add_column("Size", justify="right", style="yellow", width=10)
|
|
223
|
+
table.add_column("Project Hash", style="dim", width=12)
|
|
224
|
+
|
|
225
|
+
for i, session in enumerate(sessions, 1):
|
|
226
|
+
table.add_row(
|
|
227
|
+
str(i),
|
|
228
|
+
session["id"][:12] + "...",
|
|
229
|
+
session["modified"].strftime("%Y-%m-%d %H:%M"),
|
|
230
|
+
format_size(session["size"]),
|
|
231
|
+
session["project"][:10] + "...",
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
console.print(table)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def extract_hooks(messages: list[dict]) -> dict[str, int]:
|
|
238
|
+
"""Extract hooks triggered from session messages.
|
|
239
|
+
|
|
240
|
+
Hooks appear in:
|
|
241
|
+
- system-reminder tags in user messages
|
|
242
|
+
- tool_result content with hook output
|
|
243
|
+
"""
|
|
244
|
+
hooks = Counter()
|
|
245
|
+
|
|
246
|
+
for msg in messages:
|
|
247
|
+
inner_msg = msg.get("message", {})
|
|
248
|
+
content = inner_msg.get("content", "")
|
|
249
|
+
|
|
250
|
+
# Check all content - could be string or list
|
|
251
|
+
texts_to_check = []
|
|
252
|
+
|
|
253
|
+
if isinstance(content, str):
|
|
254
|
+
texts_to_check.append(content)
|
|
255
|
+
elif isinstance(content, list):
|
|
256
|
+
for block in content:
|
|
257
|
+
if isinstance(block, dict):
|
|
258
|
+
# Text blocks
|
|
259
|
+
if block.get("type") == "text":
|
|
260
|
+
texts_to_check.append(block.get("text", ""))
|
|
261
|
+
# Tool result blocks (hooks often appear here)
|
|
262
|
+
elif block.get("type") == "tool_result":
|
|
263
|
+
result_content = block.get("content", "")
|
|
264
|
+
if isinstance(result_content, str):
|
|
265
|
+
texts_to_check.append(result_content)
|
|
266
|
+
|
|
267
|
+
# Check all collected texts for hook patterns
|
|
268
|
+
for text in texts_to_check:
|
|
269
|
+
if "UserPromptSubmit hook" in text:
|
|
270
|
+
hooks["UserPromptSubmit"] += 1
|
|
271
|
+
if "PreToolUse hook" in text:
|
|
272
|
+
hooks["PreToolUse"] += 1
|
|
273
|
+
if "PostToolUse hook" in text:
|
|
274
|
+
hooks["PostToolUse"] += 1
|
|
275
|
+
if "<system-reminder>" in text:
|
|
276
|
+
# Count system-reminder injections
|
|
277
|
+
hooks["system-reminder"] += text.count("<system-reminder>")
|
|
278
|
+
|
|
279
|
+
return dict(hooks)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def display_session_details(session: dict, usage: dict[str, Any], messages: list[dict]) -> None:
|
|
283
|
+
"""Display detailed session information with rich formatting."""
|
|
284
|
+
# Session header
|
|
285
|
+
header = Text()
|
|
286
|
+
header.append("Session: ", style="bold")
|
|
287
|
+
header.append(session["id"], style="cyan")
|
|
288
|
+
console.print(Panel(header, title="Session Details", border_style="blue"))
|
|
289
|
+
|
|
290
|
+
# Statistics
|
|
291
|
+
stats_table = Table(show_header=False, box=None)
|
|
292
|
+
stats_table.add_column("Key", style="bold")
|
|
293
|
+
stats_table.add_column("Value")
|
|
294
|
+
stats_table.add_row("Path", session["path"])
|
|
295
|
+
stats_table.add_row("Modified", session["modified"].strftime("%Y-%m-%d %H:%M:%S"))
|
|
296
|
+
stats_table.add_row("Size", format_size(session["size"]))
|
|
297
|
+
stats_table.add_row("Messages", str(len(messages)))
|
|
298
|
+
|
|
299
|
+
# Count roles (Claude Code format: type field at top level)
|
|
300
|
+
user_msgs = sum(1 for m in messages if m.get("type") == "user")
|
|
301
|
+
assistant_msgs = sum(1 for m in messages if m.get("type") == "assistant")
|
|
302
|
+
stats_table.add_row("User Messages", str(user_msgs))
|
|
303
|
+
stats_table.add_row("Assistant Messages", str(assistant_msgs))
|
|
304
|
+
|
|
305
|
+
console.print(Panel(stats_table, title="Statistics", border_style="green"))
|
|
306
|
+
|
|
307
|
+
# Claude Models (the model Claude Code uses)
|
|
308
|
+
if usage.get("claude_models"):
|
|
309
|
+
models_tree = Tree("[bold yellow]Claude Models")
|
|
310
|
+
for model, count in sorted(usage["claude_models"].items(), key=lambda x: -x[1]):
|
|
311
|
+
models_tree.add(f"{model}: [cyan]{count}[/cyan] responses")
|
|
312
|
+
console.print(Panel(models_tree, title="Claude Model", border_style="yellow"))
|
|
313
|
+
|
|
314
|
+
# External Models (gemini, openai invoked via MCP)
|
|
315
|
+
if usage.get("external_models"):
|
|
316
|
+
ext_tree = Tree("[bold green]External Models Invoked")
|
|
317
|
+
for model, count in sorted(usage["external_models"].items(), key=lambda x: -x[1]):
|
|
318
|
+
ext_tree.add(f"{model}: [cyan]{count}[/cyan] calls")
|
|
319
|
+
console.print(Panel(ext_tree, title="External Models (Gemini/OpenAI)", border_style="green"))
|
|
320
|
+
|
|
321
|
+
# Native tools
|
|
322
|
+
if usage.get("native_tools"):
|
|
323
|
+
tools_tree = Tree("[bold magenta]Native Tools")
|
|
324
|
+
for tool, count in sorted(usage["native_tools"].items(), key=lambda x: -x[1]):
|
|
325
|
+
tools_tree.add(f"{tool}: [yellow]{count}[/yellow] calls")
|
|
326
|
+
console.print(Panel(tools_tree, title="Native Tool Usage", border_style="magenta"))
|
|
327
|
+
|
|
328
|
+
# MCP tools by server
|
|
329
|
+
if usage.get("mcp_tools_by_server"):
|
|
330
|
+
for server, tools in sorted(usage["mcp_tools_by_server"].items()):
|
|
331
|
+
server_tree = Tree(f"[bold cyan]{server}")
|
|
332
|
+
for tool, count in sorted(tools.items(), key=lambda x: -x[1]):
|
|
333
|
+
server_tree.add(f"{tool}: [yellow]{count}[/yellow] calls")
|
|
334
|
+
console.print(Panel(server_tree, title=f"MCP: {server}", border_style="cyan"))
|
|
335
|
+
|
|
336
|
+
# Subagents (Task tool)
|
|
337
|
+
if usage.get("subagents"):
|
|
338
|
+
subagents_tree = Tree("[bold blue]Subagents (Task tool)")
|
|
339
|
+
for agent, count in sorted(usage["subagents"].items(), key=lambda x: -x[1]):
|
|
340
|
+
subagents_tree.add(f"{agent}: [yellow]{count}[/yellow] spawned")
|
|
341
|
+
console.print(Panel(subagents_tree, title="Subagent Usage", border_style="blue"))
|
|
342
|
+
|
|
343
|
+
# MCP Agents (agent_spawn)
|
|
344
|
+
if usage.get("mcp_agents"):
|
|
345
|
+
agents_tree = Tree("[bold cyan]MCP Agents (agent_spawn)")
|
|
346
|
+
for agent, count in sorted(usage["mcp_agents"].items(), key=lambda x: -x[1]):
|
|
347
|
+
agents_tree.add(f"{agent}: [yellow]{count}[/yellow] spawned")
|
|
348
|
+
console.print(Panel(agents_tree, title="MCP Agent Usage", border_style="cyan"))
|
|
349
|
+
|
|
350
|
+
# LSP Tools
|
|
351
|
+
if usage.get("lsp_tools"):
|
|
352
|
+
lsp_tree = Tree("[bold red]LSP Tools")
|
|
353
|
+
for tool, count in sorted(usage["lsp_tools"].items(), key=lambda x: -x[1]):
|
|
354
|
+
lsp_tree.add(f"{tool}: [yellow]{count}[/yellow] calls")
|
|
355
|
+
console.print(Panel(lsp_tree, title="LSP Usage", border_style="red"))
|
|
356
|
+
|
|
357
|
+
# Hooks
|
|
358
|
+
hooks = extract_hooks(messages)
|
|
359
|
+
if hooks:
|
|
360
|
+
hooks_tree = Tree("[bold white]Hooks Triggered")
|
|
361
|
+
for hook, count in sorted(hooks.items(), key=lambda x: -x[1]):
|
|
362
|
+
hooks_tree.add(f"{hook}: [yellow]{count}[/yellow] times")
|
|
363
|
+
console.print(Panel(hooks_tree, title="Hooks", border_style="white"))
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def summarize_with_gemini(session: dict, messages: list[dict], usage: dict[str, Any]) -> str | None:
|
|
367
|
+
"""Use Gemini to summarize the session."""
|
|
368
|
+
try:
|
|
369
|
+
from mcp_bridge.tools.model_invoke import invoke_gemini
|
|
370
|
+
except ImportError:
|
|
371
|
+
console.print("[red]Error: Could not import invoke_gemini[/red]")
|
|
372
|
+
return None
|
|
373
|
+
|
|
374
|
+
# Build context for Gemini
|
|
375
|
+
user_msgs = sum(1 for m in messages if m.get("type") == "user")
|
|
376
|
+
assistant_msgs = sum(1 for m in messages if m.get("type") == "assistant")
|
|
377
|
+
|
|
378
|
+
context_parts = [
|
|
379
|
+
"# Claude Code Session Analysis Request\n\n",
|
|
380
|
+
f"**Session ID:** {session['id']}\n",
|
|
381
|
+
f"**Modified:** {session['modified']}\n",
|
|
382
|
+
f"**Total Messages:** {len(messages)} ({user_msgs} user, {assistant_msgs} assistant)\n\n",
|
|
383
|
+
"## Tool/Agent/Model Usage Summary\n\n",
|
|
384
|
+
f"**Tools Used:** {json.dumps(usage['tools'], indent=2)}\n\n",
|
|
385
|
+
f"**MCP Tools:** {json.dumps(usage['mcp_tools'], indent=2)}\n\n",
|
|
386
|
+
f"**Subagents (Task):** {usage.get('subagents', {})}\n\n",
|
|
387
|
+
f"**MCP Agents:** {usage['agents']}\n\n",
|
|
388
|
+
f"**Models:** {usage['models']}\n\n",
|
|
389
|
+
"## Session Transcript\n\n",
|
|
390
|
+
]
|
|
391
|
+
|
|
392
|
+
# Add messages (Gemini has 1M context window)
|
|
393
|
+
# Parse Claude Code session format
|
|
394
|
+
for i, msg in enumerate(messages):
|
|
395
|
+
msg_type = msg.get("type", "")
|
|
396
|
+
if msg_type not in ("user", "assistant"):
|
|
397
|
+
continue
|
|
398
|
+
|
|
399
|
+
inner_msg = msg.get("message", {})
|
|
400
|
+
role = inner_msg.get("role", msg_type)
|
|
401
|
+
content = inner_msg.get("content", "")
|
|
402
|
+
|
|
403
|
+
# Extract text from content blocks
|
|
404
|
+
if isinstance(content, list):
|
|
405
|
+
text_parts = []
|
|
406
|
+
for block in content:
|
|
407
|
+
if isinstance(block, dict):
|
|
408
|
+
if block.get("type") == "text":
|
|
409
|
+
text_parts.append(block.get("text", ""))
|
|
410
|
+
elif block.get("type") == "tool_use":
|
|
411
|
+
tool_name = block.get("name", "unknown")
|
|
412
|
+
text_parts.append(f"[TOOL: {tool_name}]")
|
|
413
|
+
elif block.get("type") == "thinking":
|
|
414
|
+
text_parts.append("[THINKING]")
|
|
415
|
+
content = " ".join(text_parts)
|
|
416
|
+
|
|
417
|
+
# Truncate very long messages
|
|
418
|
+
if len(content) > 2000:
|
|
419
|
+
content = content[:2000] + "... [truncated]"
|
|
420
|
+
|
|
421
|
+
context_parts.append(f"**[{i+1}] {role}:** {content}\n\n")
|
|
422
|
+
|
|
423
|
+
context = "".join(context_parts)
|
|
424
|
+
|
|
425
|
+
prompt = f"""Analyze this Claude Code session and provide a concise summary:
|
|
426
|
+
|
|
427
|
+
{context}
|
|
428
|
+
|
|
429
|
+
Please provide:
|
|
430
|
+
1. **Session Purpose**: What was the user trying to accomplish?
|
|
431
|
+
2. **Key Actions**: Main tools, agents, and operations used
|
|
432
|
+
3. **Outcome**: Was the task successful? Any notable issues?
|
|
433
|
+
4. **Recommendations**: Any suggestions for improvement?
|
|
434
|
+
|
|
435
|
+
Keep the summary concise but informative."""
|
|
436
|
+
|
|
437
|
+
with Progress(
|
|
438
|
+
SpinnerColumn(),
|
|
439
|
+
TextColumn("[progress.description]{task.description}"),
|
|
440
|
+
console=console,
|
|
441
|
+
) as progress:
|
|
442
|
+
progress.add_task(description="Analyzing session with Gemini...", total=None)
|
|
443
|
+
try:
|
|
444
|
+
# Run synchronously
|
|
445
|
+
import asyncio
|
|
446
|
+
result = asyncio.run(invoke_gemini(
|
|
447
|
+
prompt=prompt,
|
|
448
|
+
model="gemini-3-flash",
|
|
449
|
+
max_tokens=2048,
|
|
450
|
+
))
|
|
451
|
+
return result
|
|
452
|
+
except Exception as e:
|
|
453
|
+
console.print(f"[red]Gemini error: {e}[/red]")
|
|
454
|
+
return None
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def main():
|
|
458
|
+
parser = argparse.ArgumentParser(
|
|
459
|
+
description="Analyze Claude Code sessions with rich formatting",
|
|
460
|
+
prog="stravinsky-sessions",
|
|
461
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
462
|
+
epilog="""
|
|
463
|
+
Examples:
|
|
464
|
+
stravinsky-sessions # List 10 recent sessions
|
|
465
|
+
stravinsky-sessions --limit 20 # List 20 recent sessions
|
|
466
|
+
stravinsky-sessions --select 1 # Show details for session #1
|
|
467
|
+
stravinsky-sessions --select 1 --summarize # Summarize with Gemini
|
|
468
|
+
stravinsky-sessions --id abc123 # Show session by ID prefix
|
|
469
|
+
""",
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
parser.add_argument(
|
|
473
|
+
"--limit", "-n",
|
|
474
|
+
type=int,
|
|
475
|
+
default=10,
|
|
476
|
+
help="Number of sessions to list (default: 10)",
|
|
477
|
+
)
|
|
478
|
+
parser.add_argument(
|
|
479
|
+
"--select", "-s",
|
|
480
|
+
type=int,
|
|
481
|
+
help="Select session by number from list (1-indexed)",
|
|
482
|
+
)
|
|
483
|
+
parser.add_argument(
|
|
484
|
+
"--id",
|
|
485
|
+
type=str,
|
|
486
|
+
help="Select session by ID or ID prefix",
|
|
487
|
+
)
|
|
488
|
+
parser.add_argument(
|
|
489
|
+
"--summarize",
|
|
490
|
+
action="store_true",
|
|
491
|
+
help="Use Gemini to summarize the session",
|
|
492
|
+
)
|
|
493
|
+
parser.add_argument(
|
|
494
|
+
"--json",
|
|
495
|
+
action="store_true",
|
|
496
|
+
help="Output as JSON instead of rich formatting",
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
args = parser.parse_args()
|
|
500
|
+
|
|
501
|
+
# Get sessions (search all if --id is specified)
|
|
502
|
+
sessions = get_sessions(limit=args.limit, search_id=args.id)
|
|
503
|
+
|
|
504
|
+
if not sessions:
|
|
505
|
+
console.print("[yellow]No sessions found[/yellow]")
|
|
506
|
+
return 1
|
|
507
|
+
|
|
508
|
+
# Select session by number or ID
|
|
509
|
+
selected_session = None
|
|
510
|
+
|
|
511
|
+
if args.select:
|
|
512
|
+
# For --select, re-get with limit
|
|
513
|
+
display_sessions = get_sessions(limit=args.limit)
|
|
514
|
+
if 1 <= args.select <= len(display_sessions):
|
|
515
|
+
selected_session = display_sessions[args.select - 1]
|
|
516
|
+
else:
|
|
517
|
+
console.print(f"[red]Invalid selection: {args.select}. Must be 1-{len(display_sessions)}[/red]")
|
|
518
|
+
return 1
|
|
519
|
+
elif args.id:
|
|
520
|
+
for s in sessions:
|
|
521
|
+
if s["id"].startswith(args.id):
|
|
522
|
+
selected_session = s
|
|
523
|
+
break
|
|
524
|
+
if not selected_session:
|
|
525
|
+
console.print(f"[red]Session not found: {args.id}[/red]")
|
|
526
|
+
return 1
|
|
527
|
+
|
|
528
|
+
if selected_session:
|
|
529
|
+
# Read and analyze session
|
|
530
|
+
messages = read_session_messages(selected_session["path"])
|
|
531
|
+
usage = extract_tool_usage(messages)
|
|
532
|
+
|
|
533
|
+
if args.json:
|
|
534
|
+
user_msgs = sum(1 for m in messages if m.get("type") == "user")
|
|
535
|
+
assistant_msgs = sum(1 for m in messages if m.get("type") == "assistant")
|
|
536
|
+
hooks = extract_hooks(messages)
|
|
537
|
+
output = {
|
|
538
|
+
"session": {
|
|
539
|
+
"id": selected_session["id"],
|
|
540
|
+
"path": selected_session["path"],
|
|
541
|
+
"modified": selected_session["modified"].isoformat(),
|
|
542
|
+
"size": selected_session["size"],
|
|
543
|
+
"message_count": len(messages),
|
|
544
|
+
"user_messages": user_msgs,
|
|
545
|
+
"assistant_messages": assistant_msgs,
|
|
546
|
+
},
|
|
547
|
+
"claude_models": usage.get("claude_models", {}),
|
|
548
|
+
"external_models": usage.get("external_models", {}),
|
|
549
|
+
"native_tools": usage.get("native_tools", {}),
|
|
550
|
+
"mcp_tools_by_server": usage.get("mcp_tools_by_server", {}),
|
|
551
|
+
"subagents": usage.get("subagents", {}),
|
|
552
|
+
"mcp_agents": usage.get("mcp_agents", {}),
|
|
553
|
+
"lsp_tools": usage.get("lsp_tools", {}),
|
|
554
|
+
"hooks": hooks,
|
|
555
|
+
}
|
|
556
|
+
print(json.dumps(output, indent=2))
|
|
557
|
+
else:
|
|
558
|
+
display_session_details(selected_session, usage, messages)
|
|
559
|
+
|
|
560
|
+
if args.summarize:
|
|
561
|
+
console.print()
|
|
562
|
+
summary = summarize_with_gemini(selected_session, messages, usage)
|
|
563
|
+
if summary:
|
|
564
|
+
console.print(Panel(summary, title="Gemini Summary", border_style="green"))
|
|
565
|
+
else:
|
|
566
|
+
# List sessions
|
|
567
|
+
if args.json:
|
|
568
|
+
output = [
|
|
569
|
+
{
|
|
570
|
+
"id": s["id"],
|
|
571
|
+
"modified": s["modified"].isoformat(),
|
|
572
|
+
"size": s["size"],
|
|
573
|
+
}
|
|
574
|
+
for s in sessions
|
|
575
|
+
]
|
|
576
|
+
print(json.dumps(output, indent=2))
|
|
577
|
+
else:
|
|
578
|
+
display_session_list(sessions)
|
|
579
|
+
console.print("\n[dim]Use --select N or --id PREFIX to view session details[/dim]")
|
|
580
|
+
|
|
581
|
+
return 0
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
if __name__ == "__main__":
|
|
585
|
+
sys.exit(main())
|