htmlgraph 0.20.1__py3-none-any.whl → 0.27.5__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.
Files changed (304) hide show
  1. htmlgraph/.htmlgraph/.session-warning-state.json +6 -0
  2. htmlgraph/.htmlgraph/agents.json +72 -0
  3. htmlgraph/.htmlgraph/htmlgraph.db +0 -0
  4. htmlgraph/__init__.py +51 -1
  5. htmlgraph/__init__.pyi +123 -0
  6. htmlgraph/agent_detection.py +26 -10
  7. htmlgraph/agent_registry.py +2 -1
  8. htmlgraph/analytics/__init__.py +8 -1
  9. htmlgraph/analytics/cli.py +86 -20
  10. htmlgraph/analytics/cost_analyzer.py +391 -0
  11. htmlgraph/analytics/cost_monitor.py +664 -0
  12. htmlgraph/analytics/cost_reporter.py +675 -0
  13. htmlgraph/analytics/cross_session.py +617 -0
  14. htmlgraph/analytics/dependency.py +10 -6
  15. htmlgraph/analytics/pattern_learning.py +771 -0
  16. htmlgraph/analytics/session_graph.py +707 -0
  17. htmlgraph/analytics/strategic/__init__.py +80 -0
  18. htmlgraph/analytics/strategic/cost_optimizer.py +611 -0
  19. htmlgraph/analytics/strategic/pattern_detector.py +876 -0
  20. htmlgraph/analytics/strategic/preference_manager.py +709 -0
  21. htmlgraph/analytics/strategic/suggestion_engine.py +747 -0
  22. htmlgraph/analytics/work_type.py +67 -27
  23. htmlgraph/analytics_index.py +53 -20
  24. htmlgraph/api/__init__.py +3 -0
  25. htmlgraph/api/cost_alerts_websocket.py +416 -0
  26. htmlgraph/api/main.py +2498 -0
  27. htmlgraph/api/static/htmx.min.js +1 -0
  28. htmlgraph/api/static/style-redesign.css +1344 -0
  29. htmlgraph/api/static/style.css +1079 -0
  30. htmlgraph/api/templates/dashboard-redesign.html +1366 -0
  31. htmlgraph/api/templates/dashboard.html +794 -0
  32. htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
  33. htmlgraph/api/templates/partials/activity-feed.html +1100 -0
  34. htmlgraph/api/templates/partials/agents-redesign.html +317 -0
  35. htmlgraph/api/templates/partials/agents.html +317 -0
  36. htmlgraph/api/templates/partials/event-traces.html +373 -0
  37. htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
  38. htmlgraph/api/templates/partials/features.html +578 -0
  39. htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
  40. htmlgraph/api/templates/partials/metrics.html +346 -0
  41. htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
  42. htmlgraph/api/templates/partials/orchestration.html +198 -0
  43. htmlgraph/api/templates/partials/spawners.html +375 -0
  44. htmlgraph/api/templates/partials/work-items.html +613 -0
  45. htmlgraph/api/websocket.py +538 -0
  46. htmlgraph/archive/__init__.py +24 -0
  47. htmlgraph/archive/bloom.py +234 -0
  48. htmlgraph/archive/fts.py +297 -0
  49. htmlgraph/archive/manager.py +583 -0
  50. htmlgraph/archive/search.py +244 -0
  51. htmlgraph/atomic_ops.py +560 -0
  52. htmlgraph/attribute_index.py +2 -1
  53. htmlgraph/bounded_paths.py +539 -0
  54. htmlgraph/builders/base.py +57 -2
  55. htmlgraph/builders/bug.py +19 -3
  56. htmlgraph/builders/chore.py +19 -3
  57. htmlgraph/builders/epic.py +19 -3
  58. htmlgraph/builders/feature.py +27 -3
  59. htmlgraph/builders/insight.py +2 -1
  60. htmlgraph/builders/metric.py +2 -1
  61. htmlgraph/builders/pattern.py +2 -1
  62. htmlgraph/builders/phase.py +19 -3
  63. htmlgraph/builders/spike.py +29 -3
  64. htmlgraph/builders/track.py +42 -1
  65. htmlgraph/cigs/__init__.py +81 -0
  66. htmlgraph/cigs/autonomy.py +385 -0
  67. htmlgraph/cigs/cost.py +475 -0
  68. htmlgraph/cigs/messages_basic.py +472 -0
  69. htmlgraph/cigs/messaging.py +365 -0
  70. htmlgraph/cigs/models.py +771 -0
  71. htmlgraph/cigs/pattern_storage.py +427 -0
  72. htmlgraph/cigs/patterns.py +503 -0
  73. htmlgraph/cigs/posttool_analyzer.py +234 -0
  74. htmlgraph/cigs/reporter.py +818 -0
  75. htmlgraph/cigs/tracker.py +317 -0
  76. htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
  77. htmlgraph/cli/.htmlgraph/agents.json +72 -0
  78. htmlgraph/cli/.htmlgraph/htmlgraph.db +0 -0
  79. htmlgraph/cli/__init__.py +42 -0
  80. htmlgraph/cli/__main__.py +6 -0
  81. htmlgraph/cli/analytics.py +1424 -0
  82. htmlgraph/cli/base.py +685 -0
  83. htmlgraph/cli/constants.py +206 -0
  84. htmlgraph/cli/core.py +954 -0
  85. htmlgraph/cli/main.py +147 -0
  86. htmlgraph/cli/models.py +475 -0
  87. htmlgraph/cli/templates/__init__.py +1 -0
  88. htmlgraph/cli/templates/cost_dashboard.py +399 -0
  89. htmlgraph/cli/work/__init__.py +239 -0
  90. htmlgraph/cli/work/browse.py +115 -0
  91. htmlgraph/cli/work/features.py +568 -0
  92. htmlgraph/cli/work/orchestration.py +676 -0
  93. htmlgraph/cli/work/report.py +728 -0
  94. htmlgraph/cli/work/sessions.py +466 -0
  95. htmlgraph/cli/work/snapshot.py +559 -0
  96. htmlgraph/cli/work/tracks.py +486 -0
  97. htmlgraph/cli_commands/__init__.py +1 -0
  98. htmlgraph/cli_commands/feature.py +195 -0
  99. htmlgraph/cli_framework.py +115 -0
  100. htmlgraph/collections/__init__.py +2 -0
  101. htmlgraph/collections/base.py +197 -14
  102. htmlgraph/collections/bug.py +2 -1
  103. htmlgraph/collections/chore.py +2 -1
  104. htmlgraph/collections/epic.py +2 -1
  105. htmlgraph/collections/feature.py +2 -1
  106. htmlgraph/collections/insight.py +2 -1
  107. htmlgraph/collections/metric.py +2 -1
  108. htmlgraph/collections/pattern.py +2 -1
  109. htmlgraph/collections/phase.py +2 -1
  110. htmlgraph/collections/session.py +194 -0
  111. htmlgraph/collections/spike.py +13 -2
  112. htmlgraph/collections/task_delegation.py +241 -0
  113. htmlgraph/collections/todo.py +14 -1
  114. htmlgraph/collections/traces.py +487 -0
  115. htmlgraph/config/cost_models.json +56 -0
  116. htmlgraph/config.py +190 -0
  117. htmlgraph/context_analytics.py +2 -1
  118. htmlgraph/converter.py +116 -7
  119. htmlgraph/cost_analysis/__init__.py +5 -0
  120. htmlgraph/cost_analysis/analyzer.py +438 -0
  121. htmlgraph/dashboard.html +2246 -248
  122. htmlgraph/dashboard.html.backup +6592 -0
  123. htmlgraph/dashboard.html.bak +7181 -0
  124. htmlgraph/dashboard.html.bak2 +7231 -0
  125. htmlgraph/dashboard.html.bak3 +7232 -0
  126. htmlgraph/db/__init__.py +38 -0
  127. htmlgraph/db/queries.py +790 -0
  128. htmlgraph/db/schema.py +1788 -0
  129. htmlgraph/decorators.py +317 -0
  130. htmlgraph/dependency_models.py +2 -1
  131. htmlgraph/deploy.py +26 -27
  132. htmlgraph/docs/API_REFERENCE.md +841 -0
  133. htmlgraph/docs/HTTP_API.md +750 -0
  134. htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
  135. htmlgraph/docs/ORCHESTRATION_PATTERNS.md +717 -0
  136. htmlgraph/docs/README.md +532 -0
  137. htmlgraph/docs/__init__.py +77 -0
  138. htmlgraph/docs/docs_version.py +55 -0
  139. htmlgraph/docs/metadata.py +93 -0
  140. htmlgraph/docs/migrations.py +232 -0
  141. htmlgraph/docs/template_engine.py +143 -0
  142. htmlgraph/docs/templates/_sections/cli_reference.md.j2 +52 -0
  143. htmlgraph/docs/templates/_sections/core_concepts.md.j2 +29 -0
  144. htmlgraph/docs/templates/_sections/sdk_basics.md.j2 +69 -0
  145. htmlgraph/docs/templates/base_agents.md.j2 +78 -0
  146. htmlgraph/docs/templates/example_user_override.md.j2 +47 -0
  147. htmlgraph/docs/version_check.py +163 -0
  148. htmlgraph/edge_index.py +2 -1
  149. htmlgraph/error_handler.py +544 -0
  150. htmlgraph/event_log.py +86 -37
  151. htmlgraph/event_migration.py +2 -1
  152. htmlgraph/file_watcher.py +12 -8
  153. htmlgraph/find_api.py +2 -1
  154. htmlgraph/git_events.py +67 -9
  155. htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
  156. htmlgraph/hooks/.htmlgraph/agents.json +72 -0
  157. htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
  158. htmlgraph/hooks/__init__.py +8 -0
  159. htmlgraph/hooks/bootstrap.py +169 -0
  160. htmlgraph/hooks/cigs_pretool_enforcer.py +354 -0
  161. htmlgraph/hooks/concurrent_sessions.py +208 -0
  162. htmlgraph/hooks/context.py +350 -0
  163. htmlgraph/hooks/drift_handler.py +525 -0
  164. htmlgraph/hooks/event_tracker.py +790 -99
  165. htmlgraph/hooks/git_commands.py +175 -0
  166. htmlgraph/hooks/installer.py +5 -1
  167. htmlgraph/hooks/orchestrator.py +327 -76
  168. htmlgraph/hooks/orchestrator_reflector.py +31 -4
  169. htmlgraph/hooks/post_tool_use_failure.py +32 -7
  170. htmlgraph/hooks/post_tool_use_handler.py +257 -0
  171. htmlgraph/hooks/posttooluse.py +92 -19
  172. htmlgraph/hooks/pretooluse.py +527 -7
  173. htmlgraph/hooks/prompt_analyzer.py +637 -0
  174. htmlgraph/hooks/session_handler.py +668 -0
  175. htmlgraph/hooks/session_summary.py +395 -0
  176. htmlgraph/hooks/state_manager.py +504 -0
  177. htmlgraph/hooks/subagent_detection.py +202 -0
  178. htmlgraph/hooks/subagent_stop.py +369 -0
  179. htmlgraph/hooks/task_enforcer.py +99 -4
  180. htmlgraph/hooks/validator.py +212 -91
  181. htmlgraph/ids.py +2 -1
  182. htmlgraph/learning.py +125 -100
  183. htmlgraph/mcp_server.py +2 -1
  184. htmlgraph/models.py +217 -18
  185. htmlgraph/operations/README.md +62 -0
  186. htmlgraph/operations/__init__.py +79 -0
  187. htmlgraph/operations/analytics.py +339 -0
  188. htmlgraph/operations/bootstrap.py +289 -0
  189. htmlgraph/operations/events.py +244 -0
  190. htmlgraph/operations/fastapi_server.py +231 -0
  191. htmlgraph/operations/hooks.py +350 -0
  192. htmlgraph/operations/initialization.py +597 -0
  193. htmlgraph/operations/initialization.py.backup +228 -0
  194. htmlgraph/operations/server.py +303 -0
  195. htmlgraph/orchestration/__init__.py +58 -0
  196. htmlgraph/orchestration/claude_launcher.py +179 -0
  197. htmlgraph/orchestration/command_builder.py +72 -0
  198. htmlgraph/orchestration/headless_spawner.py +281 -0
  199. htmlgraph/orchestration/live_events.py +377 -0
  200. htmlgraph/orchestration/model_selection.py +327 -0
  201. htmlgraph/orchestration/plugin_manager.py +140 -0
  202. htmlgraph/orchestration/prompts.py +137 -0
  203. htmlgraph/orchestration/spawner_event_tracker.py +383 -0
  204. htmlgraph/orchestration/spawners/__init__.py +16 -0
  205. htmlgraph/orchestration/spawners/base.py +194 -0
  206. htmlgraph/orchestration/spawners/claude.py +173 -0
  207. htmlgraph/orchestration/spawners/codex.py +435 -0
  208. htmlgraph/orchestration/spawners/copilot.py +294 -0
  209. htmlgraph/orchestration/spawners/gemini.py +471 -0
  210. htmlgraph/orchestration/subprocess_runner.py +36 -0
  211. htmlgraph/{orchestration.py → orchestration/task_coordination.py} +16 -8
  212. htmlgraph/orchestration.md +563 -0
  213. htmlgraph/orchestrator-system-prompt-optimized.txt +863 -0
  214. htmlgraph/orchestrator.py +2 -1
  215. htmlgraph/orchestrator_config.py +357 -0
  216. htmlgraph/orchestrator_mode.py +115 -4
  217. htmlgraph/parallel.py +2 -1
  218. htmlgraph/parser.py +86 -6
  219. htmlgraph/path_query.py +608 -0
  220. htmlgraph/pattern_matcher.py +636 -0
  221. htmlgraph/pydantic_models.py +476 -0
  222. htmlgraph/quality_gates.py +350 -0
  223. htmlgraph/query_builder.py +2 -1
  224. htmlgraph/query_composer.py +509 -0
  225. htmlgraph/reflection.py +443 -0
  226. htmlgraph/refs.py +344 -0
  227. htmlgraph/repo_hash.py +512 -0
  228. htmlgraph/repositories/__init__.py +292 -0
  229. htmlgraph/repositories/analytics_repository.py +455 -0
  230. htmlgraph/repositories/analytics_repository_standard.py +628 -0
  231. htmlgraph/repositories/feature_repository.py +581 -0
  232. htmlgraph/repositories/feature_repository_htmlfile.py +668 -0
  233. htmlgraph/repositories/feature_repository_memory.py +607 -0
  234. htmlgraph/repositories/feature_repository_sqlite.py +858 -0
  235. htmlgraph/repositories/filter_service.py +620 -0
  236. htmlgraph/repositories/filter_service_standard.py +445 -0
  237. htmlgraph/repositories/shared_cache.py +621 -0
  238. htmlgraph/repositories/shared_cache_memory.py +395 -0
  239. htmlgraph/repositories/track_repository.py +552 -0
  240. htmlgraph/repositories/track_repository_htmlfile.py +619 -0
  241. htmlgraph/repositories/track_repository_memory.py +508 -0
  242. htmlgraph/repositories/track_repository_sqlite.py +711 -0
  243. htmlgraph/sdk/__init__.py +398 -0
  244. htmlgraph/sdk/__init__.pyi +14 -0
  245. htmlgraph/sdk/analytics/__init__.py +19 -0
  246. htmlgraph/sdk/analytics/engine.py +155 -0
  247. htmlgraph/sdk/analytics/helpers.py +178 -0
  248. htmlgraph/sdk/analytics/registry.py +109 -0
  249. htmlgraph/sdk/base.py +484 -0
  250. htmlgraph/sdk/constants.py +216 -0
  251. htmlgraph/sdk/core.pyi +308 -0
  252. htmlgraph/sdk/discovery.py +120 -0
  253. htmlgraph/sdk/help/__init__.py +12 -0
  254. htmlgraph/sdk/help/mixin.py +699 -0
  255. htmlgraph/sdk/mixins/__init__.py +15 -0
  256. htmlgraph/sdk/mixins/attribution.py +113 -0
  257. htmlgraph/sdk/mixins/mixin.py +410 -0
  258. htmlgraph/sdk/operations/__init__.py +12 -0
  259. htmlgraph/sdk/operations/mixin.py +427 -0
  260. htmlgraph/sdk/orchestration/__init__.py +17 -0
  261. htmlgraph/sdk/orchestration/coordinator.py +203 -0
  262. htmlgraph/sdk/orchestration/spawner.py +204 -0
  263. htmlgraph/sdk/planning/__init__.py +19 -0
  264. htmlgraph/sdk/planning/bottlenecks.py +93 -0
  265. htmlgraph/sdk/planning/mixin.py +211 -0
  266. htmlgraph/sdk/planning/parallel.py +186 -0
  267. htmlgraph/sdk/planning/queue.py +210 -0
  268. htmlgraph/sdk/planning/recommendations.py +87 -0
  269. htmlgraph/sdk/planning/smart_planning.py +319 -0
  270. htmlgraph/sdk/session/__init__.py +19 -0
  271. htmlgraph/sdk/session/continuity.py +57 -0
  272. htmlgraph/sdk/session/handoff.py +110 -0
  273. htmlgraph/sdk/session/info.py +309 -0
  274. htmlgraph/sdk/session/manager.py +103 -0
  275. htmlgraph/sdk/strategic/__init__.py +26 -0
  276. htmlgraph/sdk/strategic/mixin.py +563 -0
  277. htmlgraph/server.py +295 -107
  278. htmlgraph/session_hooks.py +300 -0
  279. htmlgraph/session_manager.py +285 -3
  280. htmlgraph/session_registry.py +587 -0
  281. htmlgraph/session_state.py +436 -0
  282. htmlgraph/session_warning.py +2 -1
  283. htmlgraph/sessions/__init__.py +23 -0
  284. htmlgraph/sessions/handoff.py +756 -0
  285. htmlgraph/system_prompts.py +450 -0
  286. htmlgraph/templates/orchestration-view.html +350 -0
  287. htmlgraph/track_builder.py +33 -1
  288. htmlgraph/track_manager.py +38 -0
  289. htmlgraph/transcript.py +18 -5
  290. htmlgraph/validation.py +115 -0
  291. htmlgraph/watch.py +2 -1
  292. htmlgraph/work_type_utils.py +2 -1
  293. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/dashboard.html +2246 -248
  294. {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/METADATA +95 -64
  295. htmlgraph-0.27.5.dist-info/RECORD +337 -0
  296. {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/entry_points.txt +1 -1
  297. htmlgraph/cli.py +0 -4839
  298. htmlgraph/sdk.py +0 -2359
  299. htmlgraph-0.20.1.dist-info/RECORD +0 -118
  300. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/styles.css +0 -0
  301. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
  302. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
  303. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
  304. {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/WHEEL +0 -0
@@ -0,0 +1,728 @@
1
+ from __future__ import annotations
2
+
3
+ """HtmlGraph CLI - Session report commands.
4
+
5
+ Commands for generating "What Did Claude Do?" reports:
6
+ - report: Show chronological timeline of tool calls in a session
7
+
8
+ THE killer feature that differentiates HtmlGraph - complete observability
9
+ of AI agent activities with cost attribution and tool usage analysis.
10
+ """
11
+
12
+
13
+ import argparse
14
+ import sqlite3
15
+ from datetime import datetime, timedelta
16
+ from pathlib import Path
17
+ from typing import TYPE_CHECKING, Any
18
+
19
+ from rich.console import Console
20
+
21
+ from htmlgraph.cli.base import BaseCommand, CommandError, CommandResult
22
+ from htmlgraph.cli.constants import DEFAULT_GRAPH_DIR
23
+
24
+ if TYPE_CHECKING:
25
+ from argparse import _SubParsersAction
26
+
27
+ console = Console()
28
+
29
+
30
+ def register_report_commands(subparsers: _SubParsersAction) -> None:
31
+ """Register report commands."""
32
+ report_parser = subparsers.add_parser(
33
+ "report", help="Generate 'What Did Claude Do?' session report"
34
+ )
35
+ report_parser.add_argument(
36
+ "--session",
37
+ help="Session ID (or 'latest', 'today')",
38
+ default="latest",
39
+ )
40
+ report_parser.add_argument(
41
+ "--report-format",
42
+ choices=["terminal", "html", "markdown"],
43
+ default="terminal",
44
+ help="Report output format (terminal=rich formatting, html=self-contained HTML, markdown=markdown file)",
45
+ )
46
+ report_parser.add_argument(
47
+ "--detail",
48
+ choices=["basic", "full"],
49
+ default="basic",
50
+ help="Detail level (basic=summary, full=inputs/outputs)",
51
+ )
52
+ report_parser.add_argument(
53
+ "--output", "-o", help="Output file path (for html/markdown formats)"
54
+ )
55
+ report_parser.add_argument(
56
+ "--graph-dir", "-g", default=DEFAULT_GRAPH_DIR, help="Graph directory"
57
+ )
58
+ report_parser.set_defaults(func=SessionReportCommand.from_args)
59
+
60
+
61
+ class SessionReportCommand(BaseCommand):
62
+ """Generate 'What Did Claude Do?' session report."""
63
+
64
+ def __init__(
65
+ self,
66
+ *,
67
+ session: str,
68
+ report_format: str,
69
+ detail: str,
70
+ output: str | None,
71
+ ) -> None:
72
+ super().__init__()
73
+ self.session = session
74
+ self.report_format = report_format
75
+ self.detail = detail
76
+ self.output = output
77
+
78
+ @classmethod
79
+ def from_args(cls, args: argparse.Namespace) -> SessionReportCommand:
80
+ return cls(
81
+ session=getattr(args, "session", "latest"),
82
+ report_format=getattr(args, "report_format", "terminal"),
83
+ detail=getattr(args, "detail", "basic"),
84
+ output=getattr(args, "output", None),
85
+ )
86
+
87
+ def execute(self) -> CommandResult:
88
+ """Generate and display session report."""
89
+ if not self.graph_dir:
90
+ raise CommandError("Graph directory not specified")
91
+
92
+ graph_dir = Path(self.graph_dir)
93
+ db_path = graph_dir / "htmlgraph.db"
94
+
95
+ if not db_path.exists():
96
+ console.print(
97
+ f"[yellow]No database found at {db_path}[/yellow]\n"
98
+ "Run some work to generate reports!"
99
+ )
100
+ raise CommandError("No database found", exit_code=1)
101
+
102
+ # Resolve session ID
103
+ conn = sqlite3.connect(str(db_path))
104
+ conn.row_factory = sqlite3.Row
105
+ try:
106
+ session_id = self._resolve_session_id(conn, self.session)
107
+ if not session_id:
108
+ console.print(f"[red]Session not found: {self.session}[/red]")
109
+ raise CommandError("Session not found", exit_code=1)
110
+
111
+ # Get session data
112
+ session_data = self._get_session_data(conn, session_id)
113
+ events = self._get_session_events(conn, session_id)
114
+
115
+ if not events:
116
+ console.print(
117
+ f"[yellow]No events found for session {session_id}[/yellow]"
118
+ )
119
+ # Still return success, just no events to report
120
+ return CommandResult(text="")
121
+
122
+ # Generate report in requested format
123
+ if self.report_format == "terminal":
124
+ self._render_terminal_report(session_data, events)
125
+ elif self.report_format == "html":
126
+ self._render_html_report(session_data, events)
127
+ elif self.report_format == "markdown":
128
+ self._render_markdown_report(session_data, events)
129
+
130
+ # Return empty result to prevent default formatter output
131
+ # (report commands handle their own output)
132
+ return CommandResult(text="")
133
+
134
+ finally:
135
+ conn.close()
136
+
137
+ def _resolve_session_id(self, conn: sqlite3.Connection, session: str) -> str | None:
138
+ """Resolve session identifier to actual session_id."""
139
+ cursor = conn.cursor()
140
+
141
+ if session == "latest":
142
+ # Get most recent session
143
+ cursor.execute(
144
+ """
145
+ SELECT session_id FROM sessions
146
+ ORDER BY created_at DESC
147
+ LIMIT 1
148
+ """
149
+ )
150
+ row = cursor.fetchone()
151
+ return row[0] if row else None
152
+
153
+ elif session == "today":
154
+ # Get all sessions from today and combine them
155
+ # For now, just return the most recent today
156
+ today_start = (
157
+ datetime.now()
158
+ .replace(hour=0, minute=0, second=0, microsecond=0)
159
+ .isoformat()
160
+ )
161
+ cursor.execute(
162
+ """
163
+ SELECT session_id FROM sessions
164
+ WHERE created_at >= ?
165
+ ORDER BY created_at DESC
166
+ LIMIT 1
167
+ """,
168
+ (today_start,),
169
+ )
170
+ row = cursor.fetchone()
171
+ return row[0] if row else None
172
+
173
+ else:
174
+ # Assume it's a session ID (or partial match)
175
+ cursor.execute(
176
+ """
177
+ SELECT session_id FROM sessions
178
+ WHERE session_id LIKE ?
179
+ LIMIT 1
180
+ """,
181
+ (f"%{session}%",),
182
+ )
183
+ row = cursor.fetchone()
184
+ return row[0] if row else None
185
+
186
+ def _get_session_data(
187
+ self, conn: sqlite3.Connection, session_id: str
188
+ ) -> dict[str, Any]:
189
+ """Get session metadata."""
190
+ cursor = conn.cursor()
191
+ cursor.execute(
192
+ """
193
+ SELECT
194
+ session_id,
195
+ agent_assigned,
196
+ created_at,
197
+ completed_at,
198
+ total_events,
199
+ total_tokens_used,
200
+ status
201
+ FROM sessions
202
+ WHERE session_id = ?
203
+ """,
204
+ (session_id,),
205
+ )
206
+
207
+ row = cursor.fetchone()
208
+ if not row:
209
+ return {}
210
+
211
+ data = dict(row)
212
+
213
+ # Calculate duration (handle both timezone-aware and naive datetimes)
214
+ if data.get("created_at") and data.get("completed_at"):
215
+ start = datetime.fromisoformat(data["created_at"])
216
+ end = datetime.fromisoformat(data["completed_at"])
217
+ # Remove timezone info if present to avoid comparison issues
218
+ if start.tzinfo is not None:
219
+ start = start.replace(tzinfo=None)
220
+ if end.tzinfo is not None:
221
+ end = end.replace(tzinfo=None)
222
+ duration = end - start
223
+ elif data.get("created_at"):
224
+ start = datetime.fromisoformat(data["created_at"])
225
+ # Remove timezone info if present
226
+ if start.tzinfo is not None:
227
+ start = start.replace(tzinfo=None)
228
+ duration = datetime.now() - start
229
+ else:
230
+ duration = timedelta(0)
231
+
232
+ data["duration"] = duration
233
+ return data
234
+
235
+ def _get_session_events(
236
+ self, conn: sqlite3.Connection, session_id: str
237
+ ) -> list[dict[str, Any]]:
238
+ """Get all tool_call events for a session in chronological order."""
239
+ cursor = conn.cursor()
240
+ cursor.execute(
241
+ """
242
+ SELECT
243
+ event_id,
244
+ tool_name,
245
+ timestamp,
246
+ input_summary,
247
+ output_summary,
248
+ status,
249
+ parent_event_id,
250
+ cost_tokens,
251
+ execution_duration_seconds,
252
+ subagent_type
253
+ FROM agent_events
254
+ WHERE session_id = ? AND event_type = 'tool_call'
255
+ ORDER BY timestamp ASC
256
+ """,
257
+ (session_id,),
258
+ )
259
+
260
+ events = []
261
+ for row in cursor.fetchall():
262
+ event = dict(row)
263
+ # Parse timestamp
264
+ if event.get("timestamp"):
265
+ if isinstance(event["timestamp"], str):
266
+ event["timestamp"] = datetime.fromisoformat(event["timestamp"])
267
+ events.append(event)
268
+
269
+ return events
270
+
271
+ def _render_terminal_report(
272
+ self, session_data: dict[str, Any], events: list[dict[str, Any]]
273
+ ) -> None:
274
+ """Render report to terminal using Rich."""
275
+ # Header
276
+ session_id = session_data.get("session_id", "unknown")
277
+ agent = session_data.get("agent_assigned", "unknown")
278
+ duration = session_data.get("duration", timedelta(0))
279
+ total_tokens = session_data.get("total_tokens_used", 0)
280
+
281
+ # Calculate estimated cost (rough approximation)
282
+ # Average: $4.50 per 1M tokens
283
+ est_cost = (total_tokens / 1_000_000) * 4.5 if total_tokens else 0
284
+
285
+ # Format duration
286
+ duration_mins = int(duration.total_seconds() / 60)
287
+ duration_str = f"{duration_mins} minutes" if duration_mins > 0 else "< 1 minute"
288
+
289
+ console.print(f"\n[bold cyan]Session Report: {session_id[:16]}...[/bold cyan]")
290
+ console.print(
291
+ f"[dim]Agent: {agent} | Duration: {duration_str} | "
292
+ f"Tokens: {total_tokens:,} | Est. Cost: ${est_cost:.2f}[/dim]\n"
293
+ )
294
+
295
+ # Timeline
296
+ console.print("[bold]TIMELINE:[/bold]")
297
+ console.print("─" * 80)
298
+
299
+ prev_timestamp = None
300
+ for i, event in enumerate(events, 1):
301
+ timestamp = event.get("timestamp")
302
+ tool_name = event.get("tool_name", "unknown")
303
+ status = event.get("status", "")
304
+ subagent = event.get("subagent_type")
305
+
306
+ # Format timestamp
307
+ time_str = timestamp.strftime("%H:%M:%S") if timestamp else "??:??:??"
308
+
309
+ # Format status indicator
310
+ if status == "completed" or not status:
311
+ status_icon = "✓"
312
+ status_color = "green"
313
+ elif status == "failed":
314
+ status_icon = "✗"
315
+ status_color = "red"
316
+ else:
317
+ status_icon = "○"
318
+ status_color = "yellow"
319
+
320
+ # Calculate time since last event (thinking time)
321
+ think_time = ""
322
+ if prev_timestamp and timestamp:
323
+ delta = (timestamp - prev_timestamp).total_seconds()
324
+ if delta > 5: # Only show if > 5 seconds
325
+ think_time = f" [dim](+{int(delta)}s)[/dim]"
326
+
327
+ prev_timestamp = timestamp
328
+
329
+ # Format tool name with subagent context
330
+ tool_display = tool_name
331
+ if subagent:
332
+ tool_display = f"{tool_name} [dim]({subagent})[/dim]"
333
+
334
+ # Get input/output summaries (sanitized)
335
+ input_summary = self._sanitize_summary(event.get("input_summary", ""))
336
+ output_summary = self._sanitize_summary(event.get("output_summary", ""))
337
+
338
+ # Basic display
339
+ console.print(
340
+ f"{time_str} [{status_color}]{status_icon}[/{status_color}] "
341
+ f"[cyan]{tool_display}[/cyan]{think_time}"
342
+ )
343
+
344
+ # Full detail mode
345
+ if self.detail == "full":
346
+ if input_summary:
347
+ console.print(f" [dim]→ {input_summary[:80]}[/dim]")
348
+ if output_summary:
349
+ console.print(f" [dim]← {output_summary[:80]}[/dim]")
350
+
351
+ console.print("─" * 80)
352
+
353
+ # Summary statistics
354
+ self._render_summary_stats(session_data, events)
355
+
356
+ def _render_summary_stats(
357
+ self, session_data: dict[str, Any], events: list[dict[str, Any]]
358
+ ) -> None:
359
+ """Render summary statistics."""
360
+ console.print("\n[bold]SUMMARY:[/bold]")
361
+
362
+ # Tool usage counts
363
+ tool_counts: dict[str, int] = {}
364
+ total_cost = 0
365
+ for event in events:
366
+ tool = event.get("tool_name", "unknown")
367
+ tool_counts[tool] = tool_counts.get(tool, 0) + 1
368
+ total_cost += event.get("cost_tokens", 0)
369
+
370
+ # Sort by count
371
+ sorted_tools = sorted(tool_counts.items(), key=lambda x: x[1], reverse=True)
372
+
373
+ console.print(
374
+ f"Tools used: {', '.join(f'{tool}({count})' for tool, count in sorted_tools[:5])}"
375
+ )
376
+
377
+ # Files touched (extract from input summaries)
378
+ files_touched = self._extract_files_from_events(events)
379
+ if files_touched:
380
+ console.print(f"Files touched: {', '.join(list(files_touched)[:5])}")
381
+ if len(files_touched) > 5:
382
+ console.print(f" [dim](+{len(files_touched) - 5} more)[/dim]")
383
+
384
+ # Tests run (look for Bash events with pytest/test)
385
+ test_events = [
386
+ e
387
+ for e in events
388
+ if e.get("tool_name") == "Bash"
389
+ and e.get("input_summary")
390
+ and ("pytest" in e["input_summary"] or "test" in e["input_summary"])
391
+ ]
392
+ if test_events:
393
+ console.print(f"Tests run: {len(test_events)}")
394
+
395
+ # Cost breakdown
396
+ if total_cost > 0:
397
+ console.print(f"Total cost: {total_cost:,} tokens")
398
+
399
+ console.print()
400
+
401
+ def _extract_files_from_events(self, events: list[dict[str, Any]]) -> set[str]:
402
+ """Extract file paths from event summaries."""
403
+ files = set()
404
+ for event in events:
405
+ tool = event.get("tool_name", "")
406
+ input_summary = event.get("input_summary", "")
407
+
408
+ # Read/Write/Edit events typically have file paths
409
+ if tool in ["Read", "Write", "Edit"] and input_summary:
410
+ # Extract file path (simple heuristic)
411
+ parts = input_summary.split()
412
+ for part in parts:
413
+ if "/" in part and len(part) > 3:
414
+ # Extract filename only
415
+ files.add(part.split("/")[-1])
416
+ if len(files) >= 10: # Limit collection
417
+ break
418
+
419
+ return files
420
+
421
+ def _sanitize_summary(self, summary: str | None) -> str:
422
+ """Sanitize summary to remove secrets."""
423
+ if not summary:
424
+ return ""
425
+
426
+ # Simple sanitization: remove potential secrets
427
+ # (passwords, tokens, keys)
428
+ sensitive_patterns = [
429
+ "password",
430
+ "token",
431
+ "secret",
432
+ "key",
433
+ "api_key",
434
+ "auth",
435
+ ]
436
+
437
+ for pattern in sensitive_patterns:
438
+ if pattern.lower() in summary.lower():
439
+ return "[REDACTED - contains sensitive data]"
440
+
441
+ return summary
442
+
443
+ def _render_html_report(
444
+ self, session_data: dict[str, Any], events: list[dict[str, Any]]
445
+ ) -> None:
446
+ """Render report to HTML file."""
447
+ html_content = self._generate_html(session_data, events)
448
+
449
+ # Determine output path
450
+ if self.output:
451
+ output_path = Path(self.output)
452
+ else:
453
+ output_path = Path(self.graph_dir or ".") / "session-report.html"
454
+
455
+ output_path.write_text(html_content)
456
+ console.print(f"[green]✓ HTML report saved to: {output_path}[/green]")
457
+
458
+ def _generate_html(
459
+ self, session_data: dict[str, Any], events: list[dict[str, Any]]
460
+ ) -> str:
461
+ """Generate self-contained HTML report."""
462
+ session_id = session_data.get("session_id", "unknown")
463
+ agent = session_data.get("agent_assigned", "unknown")
464
+ duration = session_data.get("duration", timedelta(0))
465
+ total_tokens = session_data.get("total_tokens_used", 0)
466
+ est_cost = (total_tokens / 1_000_000) * 4.5 if total_tokens else 0
467
+
468
+ duration_mins = int(duration.total_seconds() / 60)
469
+
470
+ # Build timeline HTML
471
+ timeline_html = ""
472
+ prev_timestamp = None
473
+
474
+ for event in events:
475
+ timestamp = event.get("timestamp")
476
+ tool_name = event.get("tool_name", "unknown")
477
+ status = event.get("status", "")
478
+ input_summary = self._sanitize_summary(event.get("input_summary", ""))
479
+ output_summary = self._sanitize_summary(event.get("output_summary", ""))
480
+
481
+ time_str = timestamp.strftime("%H:%M:%S") if timestamp else "??:??:??"
482
+
483
+ # Status indicator
484
+ if status == "completed" or not status:
485
+ status_class = "success"
486
+ status_icon = "✓"
487
+ elif status == "failed":
488
+ status_class = "error"
489
+ status_icon = "✗"
490
+ else:
491
+ status_class = "pending"
492
+ status_icon = "○"
493
+
494
+ # Calculate thinking time
495
+ think_time = ""
496
+ if prev_timestamp and timestamp:
497
+ delta = (timestamp - prev_timestamp).total_seconds()
498
+ if delta > 5:
499
+ think_time = f'<span class="think-time">(+{int(delta)}s)</span>'
500
+
501
+ prev_timestamp = timestamp
502
+
503
+ # Build event row
504
+ timeline_html += f"""
505
+ <div class="event">
506
+ <span class="time">{time_str}</span>
507
+ <span class="status {status_class}">{status_icon}</span>
508
+ <span class="tool">{tool_name}</span>
509
+ {think_time}
510
+ """
511
+
512
+ if self.detail == "full":
513
+ if input_summary:
514
+ timeline_html += (
515
+ f'<div class="detail input">→ {input_summary[:100]}</div>'
516
+ )
517
+ if output_summary:
518
+ timeline_html += (
519
+ f'<div class="detail output">← {output_summary[:100]}</div>'
520
+ )
521
+
522
+ timeline_html += "</div>\n"
523
+
524
+ # Complete HTML document
525
+ html = f"""<!DOCTYPE html>
526
+ <html>
527
+ <head>
528
+ <meta charset="UTF-8">
529
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
530
+ <title>Session Report: {session_id[:16]}</title>
531
+ <style>
532
+ body {{
533
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
534
+ max-width: 1200px;
535
+ margin: 0 auto;
536
+ padding: 20px;
537
+ background: #f5f5f5;
538
+ }}
539
+ .header {{
540
+ background: white;
541
+ padding: 20px;
542
+ border-radius: 8px;
543
+ margin-bottom: 20px;
544
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
545
+ }}
546
+ .header h1 {{
547
+ margin: 0 0 10px 0;
548
+ color: #333;
549
+ }}
550
+ .header .meta {{
551
+ color: #666;
552
+ font-size: 14px;
553
+ }}
554
+ .timeline {{
555
+ background: white;
556
+ padding: 20px;
557
+ border-radius: 8px;
558
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
559
+ }}
560
+ .event {{
561
+ padding: 10px;
562
+ border-bottom: 1px solid #eee;
563
+ font-family: 'Courier New', monospace;
564
+ font-size: 14px;
565
+ }}
566
+ .event:last-child {{
567
+ border-bottom: none;
568
+ }}
569
+ .time {{
570
+ color: #666;
571
+ margin-right: 10px;
572
+ }}
573
+ .status {{
574
+ margin-right: 10px;
575
+ font-weight: bold;
576
+ }}
577
+ .status.success {{
578
+ color: #28a745;
579
+ }}
580
+ .status.error {{
581
+ color: #dc3545;
582
+ }}
583
+ .status.pending {{
584
+ color: #ffc107;
585
+ }}
586
+ .tool {{
587
+ color: #007bff;
588
+ font-weight: bold;
589
+ }}
590
+ .think-time {{
591
+ color: #999;
592
+ margin-left: 10px;
593
+ }}
594
+ .detail {{
595
+ margin-left: 120px;
596
+ color: #666;
597
+ font-size: 12px;
598
+ margin-top: 5px;
599
+ }}
600
+ .detail.input {{
601
+ color: #666;
602
+ }}
603
+ .detail.output {{
604
+ color: #28a745;
605
+ }}
606
+ .summary {{
607
+ background: white;
608
+ padding: 20px;
609
+ border-radius: 8px;
610
+ margin-top: 20px;
611
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
612
+ }}
613
+ .summary h2 {{
614
+ margin-top: 0;
615
+ }}
616
+ </style>
617
+ </head>
618
+ <body>
619
+ <div class="header">
620
+ <h1>Session Report: {session_id[:16]}...</h1>
621
+ <div class="meta">
622
+ Agent: {agent} | Duration: {duration_mins} minutes |
623
+ Tokens: {total_tokens:,} | Est. Cost: ${est_cost:.2f}
624
+ </div>
625
+ </div>
626
+
627
+ <div class="timeline">
628
+ <h2>Timeline</h2>
629
+ {timeline_html}
630
+ </div>
631
+
632
+ <div class="summary">
633
+ <h2>Summary</h2>
634
+ <p>Total events: {len(events)}</p>
635
+ <p>Generated by HtmlGraph - "HTML is All You Need"</p>
636
+ </div>
637
+ </body>
638
+ </html>"""
639
+
640
+ return html
641
+
642
+ def _render_markdown_report(
643
+ self, session_data: dict[str, Any], events: list[dict[str, Any]]
644
+ ) -> None:
645
+ """Render report to Markdown file."""
646
+ markdown_content = self._generate_markdown(session_data, events)
647
+
648
+ # Determine output path
649
+ if self.output:
650
+ output_path = Path(self.output)
651
+ else:
652
+ output_path = Path(self.graph_dir or ".") / "session-report.md"
653
+
654
+ output_path.write_text(markdown_content)
655
+ console.print(f"[green]✓ Markdown report saved to: {output_path}[/green]")
656
+
657
+ def _generate_markdown(
658
+ self, session_data: dict[str, Any], events: list[dict[str, Any]]
659
+ ) -> str:
660
+ """Generate Markdown report."""
661
+ session_id = session_data.get("session_id", "unknown")
662
+ agent = session_data.get("agent_assigned", "unknown")
663
+ duration = session_data.get("duration", timedelta(0))
664
+ total_tokens = session_data.get("total_tokens_used", 0)
665
+ est_cost = (total_tokens / 1_000_000) * 4.5 if total_tokens else 0
666
+
667
+ duration_mins = int(duration.total_seconds() / 60)
668
+
669
+ # Build timeline markdown
670
+ timeline_md = ""
671
+ prev_timestamp = None
672
+
673
+ for event in events:
674
+ timestamp = event.get("timestamp")
675
+ tool_name = event.get("tool_name", "unknown")
676
+ status = event.get("status", "")
677
+ input_summary = self._sanitize_summary(event.get("input_summary", ""))
678
+
679
+ time_str = timestamp.strftime("%H:%M:%S") if timestamp else "??:??:??"
680
+
681
+ # Status indicator
682
+ if status == "completed" or not status:
683
+ status_icon = "✓"
684
+ elif status == "failed":
685
+ status_icon = "✗"
686
+ else:
687
+ status_icon = "○"
688
+
689
+ # Calculate thinking time
690
+ think_time = ""
691
+ if prev_timestamp and timestamp:
692
+ delta = (timestamp - prev_timestamp).total_seconds()
693
+ if delta > 5:
694
+ think_time = f" *(+{int(delta)}s)*"
695
+
696
+ prev_timestamp = timestamp
697
+
698
+ timeline_md += f"**{time_str}** {status_icon} `{tool_name}`{think_time}\n"
699
+
700
+ if self.detail == "full" and input_summary:
701
+ timeline_md += f" → {input_summary[:100]}\n"
702
+
703
+ timeline_md += "\n"
704
+
705
+ # Complete markdown document
706
+ markdown = f"""# Session Report: {session_id}
707
+
708
+ **Agent:** {agent}
709
+ **Duration:** {duration_mins} minutes
710
+ **Tokens:** {total_tokens:,}
711
+ **Estimated Cost:** ${est_cost:.2f}
712
+
713
+ ---
714
+
715
+ ## Timeline
716
+
717
+ {timeline_md}
718
+
719
+ ---
720
+
721
+ ## Summary
722
+
723
+ Total events: {len(events)}
724
+
725
+ *Generated by HtmlGraph - "HTML is All You Need"*
726
+ """
727
+
728
+ return markdown