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,244 @@
1
+ from __future__ import annotations
2
+
3
+ """Event and analytics index operations for HtmlGraph."""
4
+
5
+
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class EventRebuildResult:
13
+ """Result of rebuilding the event index."""
14
+
15
+ db_path: Path
16
+ inserted: int
17
+ skipped: int
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class EventStats:
22
+ """Statistics about events in the system."""
23
+
24
+ total_events: int
25
+ session_count: int
26
+ file_count: int
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class EventQueryResult:
31
+ """Result of querying events."""
32
+
33
+ events: list[dict[str, Any]]
34
+ total: int
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class EventExportResult:
39
+ """Result of exporting sessions to JSONL."""
40
+
41
+ written: int
42
+ skipped: int
43
+ failed: int
44
+
45
+
46
+ class EventOperationError(RuntimeError):
47
+ """Base error for event operations."""
48
+
49
+
50
+ def export_sessions(*, graph_dir: Path, overwrite: bool = False) -> EventExportResult:
51
+ """
52
+ Export legacy session HTML logs to JSONL events.
53
+
54
+ Args:
55
+ graph_dir: Path to .htmlgraph directory
56
+ overwrite: Whether to overwrite existing JSONL files
57
+
58
+ Returns:
59
+ EventExportResult with counts of written, skipped, failed files
60
+
61
+ Raises:
62
+ EventOperationError: If graph_dir doesn't exist or isn't a directory
63
+ """
64
+ if not graph_dir.exists():
65
+ raise EventOperationError(f"Graph directory not found: {graph_dir}")
66
+ if not graph_dir.is_dir():
67
+ raise EventOperationError(f"Not a directory: {graph_dir}")
68
+
69
+ from htmlgraph.event_migration import export_sessions_to_jsonl
70
+
71
+ sessions_dir = graph_dir / "sessions"
72
+ events_dir = graph_dir / "events"
73
+
74
+ if not sessions_dir.exists():
75
+ raise EventOperationError(f"Sessions directory not found: {sessions_dir}")
76
+
77
+ try:
78
+ result = export_sessions_to_jsonl(
79
+ sessions_dir=sessions_dir,
80
+ events_dir=events_dir,
81
+ overwrite=overwrite,
82
+ include_subdirs=False,
83
+ )
84
+ return EventExportResult(
85
+ written=result["written"],
86
+ skipped=result["skipped"],
87
+ failed=result["failed"],
88
+ )
89
+ except Exception as e:
90
+ raise EventOperationError(f"Failed to export sessions: {e}") from e
91
+
92
+
93
+ def rebuild_index(*, graph_dir: Path) -> EventRebuildResult:
94
+ """
95
+ Rebuild the SQLite analytics index from JSONL events.
96
+
97
+ Args:
98
+ graph_dir: Path to .htmlgraph directory
99
+
100
+ Returns:
101
+ EventRebuildResult with db_path and counts of inserted/skipped events
102
+
103
+ Raises:
104
+ EventOperationError: If events directory doesn't exist or rebuild fails
105
+ """
106
+ if not graph_dir.exists():
107
+ raise EventOperationError(f"Graph directory not found: {graph_dir}")
108
+ if not graph_dir.is_dir():
109
+ raise EventOperationError(f"Not a directory: {graph_dir}")
110
+
111
+ from htmlgraph.analytics_index import AnalyticsIndex
112
+ from htmlgraph.event_log import JsonlEventLog
113
+
114
+ events_dir = graph_dir / "events"
115
+ db_path = graph_dir / "index.sqlite"
116
+
117
+ if not events_dir.exists():
118
+ raise EventOperationError(f"Events directory not found: {events_dir}")
119
+
120
+ try:
121
+ log = JsonlEventLog(events_dir)
122
+ index = AnalyticsIndex(db_path)
123
+
124
+ # Stream events from all JSONL files
125
+ events = (event for _, event in log.iter_events())
126
+ result = index.rebuild_from_events(events)
127
+
128
+ return EventRebuildResult(
129
+ db_path=db_path,
130
+ inserted=result["inserted"],
131
+ skipped=result["skipped"],
132
+ )
133
+ except Exception as e:
134
+ raise EventOperationError(f"Failed to rebuild index: {e}") from e
135
+
136
+
137
+ def query_events(
138
+ *,
139
+ graph_dir: Path,
140
+ session_id: str | None = None,
141
+ tool: str | None = None,
142
+ feature_id: str | None = None,
143
+ since: str | None = None,
144
+ limit: int | None = None,
145
+ ) -> EventQueryResult:
146
+ """
147
+ Query events from JSONL logs with optional filters.
148
+
149
+ Args:
150
+ graph_dir: Path to .htmlgraph directory
151
+ session_id: Filter by session ID (None = all sessions)
152
+ tool: Filter by tool name (e.g., 'Bash', 'Edit')
153
+ feature_id: Filter by attributed feature ID
154
+ since: Only events after this timestamp (ISO string)
155
+ limit: Maximum number of events to return
156
+
157
+ Returns:
158
+ EventQueryResult with matching events and total count
159
+
160
+ Raises:
161
+ EventOperationError: If events directory doesn't exist or query fails
162
+ """
163
+ if not graph_dir.exists():
164
+ raise EventOperationError(f"Graph directory not found: {graph_dir}")
165
+ if not graph_dir.is_dir():
166
+ raise EventOperationError(f"Not a directory: {graph_dir}")
167
+
168
+ from htmlgraph.event_log import JsonlEventLog
169
+
170
+ events_dir = graph_dir / "events"
171
+
172
+ if not events_dir.exists():
173
+ raise EventOperationError(f"Events directory not found: {events_dir}")
174
+
175
+ try:
176
+ log = JsonlEventLog(events_dir)
177
+ events = log.query_events(
178
+ session_id=session_id,
179
+ tool=tool,
180
+ feature_id=feature_id,
181
+ since=since,
182
+ limit=limit,
183
+ )
184
+
185
+ return EventQueryResult(
186
+ events=events,
187
+ total=len(events),
188
+ )
189
+ except Exception as e:
190
+ raise EventOperationError(f"Failed to query events: {e}") from e
191
+
192
+
193
+ def get_event_stats(*, graph_dir: Path) -> EventStats:
194
+ """
195
+ Get statistics about events in the system.
196
+
197
+ Args:
198
+ graph_dir: Path to .htmlgraph directory
199
+
200
+ Returns:
201
+ EventStats with counts of total events, sessions, and files
202
+
203
+ Raises:
204
+ EventOperationError: If events directory doesn't exist or stats collection fails
205
+ """
206
+ if not graph_dir.exists():
207
+ raise EventOperationError(f"Graph directory not found: {graph_dir}")
208
+ if not graph_dir.is_dir():
209
+ raise EventOperationError(f"Not a directory: {graph_dir}")
210
+
211
+ from htmlgraph.event_log import JsonlEventLog
212
+
213
+ events_dir = graph_dir / "events"
214
+
215
+ if not events_dir.exists():
216
+ # No events directory means no events
217
+ return EventStats(
218
+ total_events=0,
219
+ session_count=0,
220
+ file_count=0,
221
+ )
222
+
223
+ try:
224
+ log = JsonlEventLog(events_dir)
225
+
226
+ # Count total events and track unique sessions
227
+ total_events = 0
228
+ sessions: set[str] = set()
229
+
230
+ for _, event in log.iter_events():
231
+ total_events += 1
232
+ if session_id := event.get("session_id"):
233
+ sessions.add(session_id)
234
+
235
+ # Count JSONL files
236
+ file_count = len(list(events_dir.glob("*.jsonl")))
237
+
238
+ return EventStats(
239
+ total_events=total_events,
240
+ session_count=len(sessions),
241
+ file_count=file_count,
242
+ )
243
+ except Exception as e:
244
+ raise EventOperationError(f"Failed to get event stats: {e}") from e
@@ -0,0 +1,231 @@
1
+ from __future__ import annotations
2
+
3
+ """FastAPI-based server for HtmlGraph dashboard with real-time observability."""
4
+
5
+
6
+ import logging
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from htmlgraph.mcp_server import _resolve_project_dir
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class FastAPIServerHandle:
18
+ """Handle to a running FastAPI server."""
19
+
20
+ url: str
21
+ port: int
22
+ host: str
23
+ server: Any | None = None
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class FastAPIServerStartResult:
28
+ """Result of starting FastAPI server."""
29
+
30
+ handle: FastAPIServerHandle
31
+ warnings: list[str]
32
+ config_used: dict[str, Any]
33
+
34
+
35
+ class FastAPIServerError(RuntimeError):
36
+ """FastAPI server error."""
37
+
38
+ pass
39
+
40
+
41
+ class PortInUseError(FastAPIServerError):
42
+ """Requested port is already in use."""
43
+
44
+ pass
45
+
46
+
47
+ def start_fastapi_server(
48
+ *,
49
+ port: int = 8000,
50
+ host: str = "127.0.0.1",
51
+ db_path: str | None = None,
52
+ auto_port: bool = False,
53
+ reload: bool = False,
54
+ ) -> FastAPIServerStartResult:
55
+ """
56
+ Start FastAPI-based HtmlGraph dashboard server.
57
+
58
+ Args:
59
+ port: Port to listen on (default: 8000)
60
+ host: Host to bind to (default: 127.0.0.1)
61
+ db_path: Path to SQLite database file
62
+ auto_port: Automatically find available port if in use
63
+ reload: Enable auto-reload on file changes (development mode)
64
+
65
+ Returns:
66
+ FastAPIServerStartResult with handle, warnings, and config used
67
+
68
+ Raises:
69
+ PortInUseError: If port is in use and auto_port=False
70
+ FastAPIServerError: If server fails to start
71
+ """
72
+ import uvicorn
73
+
74
+ from htmlgraph.api.main import create_app
75
+
76
+ warnings: list[str] = []
77
+ original_port = port
78
+
79
+ # Default database path - prefer project-local database if available
80
+ if db_path is None:
81
+ # Check for project-local database first
82
+ project_dir = _resolve_project_dir()
83
+ project_db = Path(project_dir) / ".htmlgraph" / "htmlgraph.db"
84
+ if project_db.exists():
85
+ db_path = str(project_db) # Use project-local database
86
+ else:
87
+ db_path = str(
88
+ Path.home() / ".htmlgraph" / "htmlgraph.db"
89
+ ) # Fall back to home
90
+
91
+ # Ensure database exists
92
+ db_path_obj = Path(db_path)
93
+ db_path_obj.parent.mkdir(parents=True, exist_ok=True)
94
+
95
+ # Handle auto-port selection
96
+ if auto_port and _check_port_in_use(port, host):
97
+ port = _find_available_port(port + 1)
98
+ warnings.append(f"Port {original_port} is in use, using {port} instead")
99
+
100
+ # Check if port is in use
101
+ if not auto_port and _check_port_in_use(port, host):
102
+ raise PortInUseError(
103
+ f"Port {port} is already in use. Use auto_port=True or choose a different port."
104
+ )
105
+
106
+ # Create FastAPI app
107
+ app = create_app(db_path=db_path)
108
+
109
+ # Create server config
110
+ config = uvicorn.Config(
111
+ app,
112
+ host=host,
113
+ port=port,
114
+ log_level="info",
115
+ reload=reload,
116
+ reload_dirs=None, # Disable file watching for now
117
+ )
118
+
119
+ # Create server instance
120
+ server = uvicorn.Server(config)
121
+
122
+ # Create handle
123
+ handle = FastAPIServerHandle(
124
+ url=f"http://{host}:{port}",
125
+ port=port,
126
+ host=host,
127
+ server=server,
128
+ )
129
+
130
+ # Configuration used
131
+ config_used = {
132
+ "port": port,
133
+ "original_port": original_port,
134
+ "host": host,
135
+ "db_path": db_path,
136
+ "auto_port": auto_port,
137
+ "reload": reload,
138
+ }
139
+
140
+ return FastAPIServerStartResult(
141
+ handle=handle,
142
+ warnings=warnings,
143
+ config_used=config_used,
144
+ )
145
+
146
+
147
+ async def run_fastapi_server(handle: FastAPIServerHandle) -> None:
148
+ """
149
+ Run FastAPI server (async).
150
+
151
+ Args:
152
+ handle: FastAPIServerHandle from start_fastapi_server()
153
+
154
+ Raises:
155
+ FastAPIServerError: If server fails
156
+ """
157
+ if handle.server is None:
158
+ raise FastAPIServerError("Invalid server handle")
159
+
160
+ try:
161
+ await handle.server.serve()
162
+ except Exception as e:
163
+ raise FastAPIServerError(f"Server error: {e}") from e
164
+
165
+
166
+ def stop_fastapi_server(handle: FastAPIServerHandle) -> None:
167
+ """
168
+ Stop FastAPI server.
169
+
170
+ Args:
171
+ handle: FastAPIServerHandle from start_fastapi_server()
172
+
173
+ Raises:
174
+ FastAPIServerError: If shutdown fails
175
+ """
176
+ if handle.server is None:
177
+ return
178
+
179
+ try:
180
+ handle.server.should_exit = True
181
+ except Exception as e:
182
+ raise FastAPIServerError(f"Failed to stop server: {e}") from e
183
+
184
+
185
+ def _check_port_in_use(port: int, host: str = "localhost") -> bool:
186
+ """
187
+ Check if a port is already in use.
188
+
189
+ Args:
190
+ port: Port number to check
191
+ host: Host to check on
192
+
193
+ Returns:
194
+ True if port is in use, False otherwise
195
+ """
196
+ import socket
197
+
198
+ try:
199
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
200
+ s.bind((host, port))
201
+ return False
202
+ except OSError:
203
+ return True
204
+
205
+
206
+ def _find_available_port(start_port: int = 8000, max_attempts: int = 10) -> int:
207
+ """
208
+ Find an available port starting from start_port.
209
+
210
+ Args:
211
+ start_port: Port to start searching from
212
+ max_attempts: Maximum number of ports to try
213
+
214
+ Returns:
215
+ Available port number
216
+
217
+ Raises:
218
+ FastAPIServerError: If no available port found
219
+ """
220
+ import socket
221
+
222
+ for port in range(start_port, start_port + max_attempts):
223
+ try:
224
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
225
+ s.bind(("", port))
226
+ return port
227
+ except OSError:
228
+ continue
229
+ raise FastAPIServerError(
230
+ f"No available ports found in range {start_port}-{start_port + max_attempts}"
231
+ )