stravinsky 0.2.52__py3-none-any.whl → 0.2.67__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 (41) hide show
  1. mcp_bridge/__init__.py +1 -1
  2. mcp_bridge/cli/__init__.py +6 -0
  3. mcp_bridge/cli/install_hooks.py +1265 -0
  4. mcp_bridge/cli/session_report.py +585 -0
  5. mcp_bridge/hooks/HOOKS_SETTINGS.json +175 -0
  6. mcp_bridge/hooks/README.md +215 -0
  7. mcp_bridge/hooks/__init__.py +117 -63
  8. mcp_bridge/hooks/edit_recovery.py +42 -37
  9. mcp_bridge/hooks/git_noninteractive.py +89 -0
  10. mcp_bridge/hooks/keyword_detector.py +30 -0
  11. mcp_bridge/hooks/notification_hook.py +103 -0
  12. mcp_bridge/hooks/parallel_execution.py +111 -0
  13. mcp_bridge/hooks/pre_compact.py +82 -183
  14. mcp_bridge/hooks/rules_injector.py +507 -0
  15. mcp_bridge/hooks/session_notifier.py +125 -0
  16. mcp_bridge/{native_hooks → hooks}/stravinsky_mode.py +51 -16
  17. mcp_bridge/hooks/subagent_stop.py +98 -0
  18. mcp_bridge/hooks/task_validator.py +73 -0
  19. mcp_bridge/hooks/tmux_manager.py +141 -0
  20. mcp_bridge/hooks/todo_continuation.py +90 -0
  21. mcp_bridge/hooks/todo_delegation.py +88 -0
  22. mcp_bridge/hooks/tool_messaging.py +164 -0
  23. mcp_bridge/hooks/truncator.py +21 -17
  24. mcp_bridge/prompts/multimodal.py +24 -3
  25. mcp_bridge/server.py +12 -1
  26. mcp_bridge/server_tools.py +5 -0
  27. mcp_bridge/tools/agent_manager.py +30 -11
  28. mcp_bridge/tools/code_search.py +81 -9
  29. mcp_bridge/tools/lsp/tools.py +6 -2
  30. mcp_bridge/tools/model_invoke.py +76 -1
  31. mcp_bridge/tools/templates.py +32 -18
  32. stravinsky-0.2.67.dist-info/METADATA +284 -0
  33. {stravinsky-0.2.52.dist-info → stravinsky-0.2.67.dist-info}/RECORD +36 -23
  34. stravinsky-0.2.67.dist-info/entry_points.txt +5 -0
  35. mcp_bridge/native_hooks/edit_recovery.py +0 -46
  36. mcp_bridge/native_hooks/todo_delegation.py +0 -54
  37. mcp_bridge/native_hooks/truncator.py +0 -23
  38. stravinsky-0.2.52.dist-info/METADATA +0 -204
  39. stravinsky-0.2.52.dist-info/entry_points.txt +0 -3
  40. /mcp_bridge/{native_hooks → hooks}/context.py +0 -0
  41. {stravinsky-0.2.52.dist-info → stravinsky-0.2.67.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())