htmlgraph 0.9.3__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 (331) 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 +173 -17
  5. htmlgraph/__init__.pyi +123 -0
  6. htmlgraph/agent_detection.py +127 -0
  7. htmlgraph/agent_registry.py +45 -30
  8. htmlgraph/agents.py +160 -107
  9. htmlgraph/analytics/__init__.py +9 -2
  10. htmlgraph/analytics/cli.py +190 -51
  11. htmlgraph/analytics/cost_analyzer.py +391 -0
  12. htmlgraph/analytics/cost_monitor.py +664 -0
  13. htmlgraph/analytics/cost_reporter.py +675 -0
  14. htmlgraph/analytics/cross_session.py +617 -0
  15. htmlgraph/analytics/dependency.py +192 -100
  16. htmlgraph/analytics/pattern_learning.py +771 -0
  17. htmlgraph/analytics/session_graph.py +707 -0
  18. htmlgraph/analytics/strategic/__init__.py +80 -0
  19. htmlgraph/analytics/strategic/cost_optimizer.py +611 -0
  20. htmlgraph/analytics/strategic/pattern_detector.py +876 -0
  21. htmlgraph/analytics/strategic/preference_manager.py +709 -0
  22. htmlgraph/analytics/strategic/suggestion_engine.py +747 -0
  23. htmlgraph/analytics/work_type.py +190 -14
  24. htmlgraph/analytics_index.py +135 -51
  25. htmlgraph/api/__init__.py +3 -0
  26. htmlgraph/api/cost_alerts_websocket.py +416 -0
  27. htmlgraph/api/main.py +2498 -0
  28. htmlgraph/api/static/htmx.min.js +1 -0
  29. htmlgraph/api/static/style-redesign.css +1344 -0
  30. htmlgraph/api/static/style.css +1079 -0
  31. htmlgraph/api/templates/dashboard-redesign.html +1366 -0
  32. htmlgraph/api/templates/dashboard.html +794 -0
  33. htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
  34. htmlgraph/api/templates/partials/activity-feed.html +1100 -0
  35. htmlgraph/api/templates/partials/agents-redesign.html +317 -0
  36. htmlgraph/api/templates/partials/agents.html +317 -0
  37. htmlgraph/api/templates/partials/event-traces.html +373 -0
  38. htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
  39. htmlgraph/api/templates/partials/features.html +578 -0
  40. htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
  41. htmlgraph/api/templates/partials/metrics.html +346 -0
  42. htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
  43. htmlgraph/api/templates/partials/orchestration.html +198 -0
  44. htmlgraph/api/templates/partials/spawners.html +375 -0
  45. htmlgraph/api/templates/partials/work-items.html +613 -0
  46. htmlgraph/api/websocket.py +538 -0
  47. htmlgraph/archive/__init__.py +24 -0
  48. htmlgraph/archive/bloom.py +234 -0
  49. htmlgraph/archive/fts.py +297 -0
  50. htmlgraph/archive/manager.py +583 -0
  51. htmlgraph/archive/search.py +244 -0
  52. htmlgraph/atomic_ops.py +560 -0
  53. htmlgraph/attribute_index.py +208 -0
  54. htmlgraph/bounded_paths.py +539 -0
  55. htmlgraph/builders/__init__.py +14 -0
  56. htmlgraph/builders/base.py +118 -29
  57. htmlgraph/builders/bug.py +150 -0
  58. htmlgraph/builders/chore.py +119 -0
  59. htmlgraph/builders/epic.py +150 -0
  60. htmlgraph/builders/feature.py +31 -6
  61. htmlgraph/builders/insight.py +195 -0
  62. htmlgraph/builders/metric.py +217 -0
  63. htmlgraph/builders/pattern.py +202 -0
  64. htmlgraph/builders/phase.py +162 -0
  65. htmlgraph/builders/spike.py +52 -19
  66. htmlgraph/builders/track.py +148 -72
  67. htmlgraph/cigs/__init__.py +81 -0
  68. htmlgraph/cigs/autonomy.py +385 -0
  69. htmlgraph/cigs/cost.py +475 -0
  70. htmlgraph/cigs/messages_basic.py +472 -0
  71. htmlgraph/cigs/messaging.py +365 -0
  72. htmlgraph/cigs/models.py +771 -0
  73. htmlgraph/cigs/pattern_storage.py +427 -0
  74. htmlgraph/cigs/patterns.py +503 -0
  75. htmlgraph/cigs/posttool_analyzer.py +234 -0
  76. htmlgraph/cigs/reporter.py +818 -0
  77. htmlgraph/cigs/tracker.py +317 -0
  78. htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
  79. htmlgraph/cli/.htmlgraph/agents.json +72 -0
  80. htmlgraph/cli/.htmlgraph/htmlgraph.db +0 -0
  81. htmlgraph/cli/__init__.py +42 -0
  82. htmlgraph/cli/__main__.py +6 -0
  83. htmlgraph/cli/analytics.py +1424 -0
  84. htmlgraph/cli/base.py +685 -0
  85. htmlgraph/cli/constants.py +206 -0
  86. htmlgraph/cli/core.py +954 -0
  87. htmlgraph/cli/main.py +147 -0
  88. htmlgraph/cli/models.py +475 -0
  89. htmlgraph/cli/templates/__init__.py +1 -0
  90. htmlgraph/cli/templates/cost_dashboard.py +399 -0
  91. htmlgraph/cli/work/__init__.py +239 -0
  92. htmlgraph/cli/work/browse.py +115 -0
  93. htmlgraph/cli/work/features.py +568 -0
  94. htmlgraph/cli/work/orchestration.py +676 -0
  95. htmlgraph/cli/work/report.py +728 -0
  96. htmlgraph/cli/work/sessions.py +466 -0
  97. htmlgraph/cli/work/snapshot.py +559 -0
  98. htmlgraph/cli/work/tracks.py +486 -0
  99. htmlgraph/cli_commands/__init__.py +1 -0
  100. htmlgraph/cli_commands/feature.py +195 -0
  101. htmlgraph/cli_framework.py +115 -0
  102. htmlgraph/collections/__init__.py +18 -0
  103. htmlgraph/collections/base.py +415 -98
  104. htmlgraph/collections/bug.py +53 -0
  105. htmlgraph/collections/chore.py +53 -0
  106. htmlgraph/collections/epic.py +53 -0
  107. htmlgraph/collections/feature.py +12 -26
  108. htmlgraph/collections/insight.py +100 -0
  109. htmlgraph/collections/metric.py +92 -0
  110. htmlgraph/collections/pattern.py +97 -0
  111. htmlgraph/collections/phase.py +53 -0
  112. htmlgraph/collections/session.py +194 -0
  113. htmlgraph/collections/spike.py +56 -16
  114. htmlgraph/collections/task_delegation.py +241 -0
  115. htmlgraph/collections/todo.py +511 -0
  116. htmlgraph/collections/traces.py +487 -0
  117. htmlgraph/config/cost_models.json +56 -0
  118. htmlgraph/config.py +190 -0
  119. htmlgraph/context_analytics.py +344 -0
  120. htmlgraph/converter.py +216 -28
  121. htmlgraph/cost_analysis/__init__.py +5 -0
  122. htmlgraph/cost_analysis/analyzer.py +438 -0
  123. htmlgraph/dashboard.html +2406 -307
  124. htmlgraph/dashboard.html.backup +6592 -0
  125. htmlgraph/dashboard.html.bak +7181 -0
  126. htmlgraph/dashboard.html.bak2 +7231 -0
  127. htmlgraph/dashboard.html.bak3 +7232 -0
  128. htmlgraph/db/__init__.py +38 -0
  129. htmlgraph/db/queries.py +790 -0
  130. htmlgraph/db/schema.py +1788 -0
  131. htmlgraph/decorators.py +317 -0
  132. htmlgraph/dependency_models.py +19 -2
  133. htmlgraph/deploy.py +142 -125
  134. htmlgraph/deployment_models.py +474 -0
  135. htmlgraph/docs/API_REFERENCE.md +841 -0
  136. htmlgraph/docs/HTTP_API.md +750 -0
  137. htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
  138. htmlgraph/docs/ORCHESTRATION_PATTERNS.md +717 -0
  139. htmlgraph/docs/README.md +532 -0
  140. htmlgraph/docs/__init__.py +77 -0
  141. htmlgraph/docs/docs_version.py +55 -0
  142. htmlgraph/docs/metadata.py +93 -0
  143. htmlgraph/docs/migrations.py +232 -0
  144. htmlgraph/docs/template_engine.py +143 -0
  145. htmlgraph/docs/templates/_sections/cli_reference.md.j2 +52 -0
  146. htmlgraph/docs/templates/_sections/core_concepts.md.j2 +29 -0
  147. htmlgraph/docs/templates/_sections/sdk_basics.md.j2 +69 -0
  148. htmlgraph/docs/templates/base_agents.md.j2 +78 -0
  149. htmlgraph/docs/templates/example_user_override.md.j2 +47 -0
  150. htmlgraph/docs/version_check.py +163 -0
  151. htmlgraph/edge_index.py +182 -27
  152. htmlgraph/error_handler.py +544 -0
  153. htmlgraph/event_log.py +100 -52
  154. htmlgraph/event_migration.py +13 -4
  155. htmlgraph/exceptions.py +49 -0
  156. htmlgraph/file_watcher.py +101 -28
  157. htmlgraph/find_api.py +75 -63
  158. htmlgraph/git_events.py +145 -63
  159. htmlgraph/graph.py +1122 -106
  160. htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
  161. htmlgraph/hooks/.htmlgraph/agents.json +72 -0
  162. htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
  163. htmlgraph/hooks/__init__.py +45 -0
  164. htmlgraph/hooks/bootstrap.py +169 -0
  165. htmlgraph/hooks/cigs_pretool_enforcer.py +354 -0
  166. htmlgraph/hooks/concurrent_sessions.py +208 -0
  167. htmlgraph/hooks/context.py +350 -0
  168. htmlgraph/hooks/drift_handler.py +525 -0
  169. htmlgraph/hooks/event_tracker.py +1314 -0
  170. htmlgraph/hooks/git_commands.py +175 -0
  171. htmlgraph/hooks/hooks-config.example.json +12 -0
  172. htmlgraph/hooks/installer.py +343 -0
  173. htmlgraph/hooks/orchestrator.py +674 -0
  174. htmlgraph/hooks/orchestrator_reflector.py +223 -0
  175. htmlgraph/hooks/post-checkout.sh +28 -0
  176. htmlgraph/hooks/post-commit.sh +24 -0
  177. htmlgraph/hooks/post-merge.sh +26 -0
  178. htmlgraph/hooks/post_tool_use_failure.py +273 -0
  179. htmlgraph/hooks/post_tool_use_handler.py +257 -0
  180. htmlgraph/hooks/posttooluse.py +408 -0
  181. htmlgraph/hooks/pre-commit.sh +94 -0
  182. htmlgraph/hooks/pre-push.sh +28 -0
  183. htmlgraph/hooks/pretooluse.py +819 -0
  184. htmlgraph/hooks/prompt_analyzer.py +637 -0
  185. htmlgraph/hooks/session_handler.py +668 -0
  186. htmlgraph/hooks/session_summary.py +395 -0
  187. htmlgraph/hooks/state_manager.py +504 -0
  188. htmlgraph/hooks/subagent_detection.py +202 -0
  189. htmlgraph/hooks/subagent_stop.py +369 -0
  190. htmlgraph/hooks/task_enforcer.py +255 -0
  191. htmlgraph/hooks/task_validator.py +177 -0
  192. htmlgraph/hooks/validator.py +628 -0
  193. htmlgraph/ids.py +41 -27
  194. htmlgraph/index.d.ts +286 -0
  195. htmlgraph/learning.py +767 -0
  196. htmlgraph/mcp_server.py +69 -23
  197. htmlgraph/models.py +1586 -87
  198. htmlgraph/operations/README.md +62 -0
  199. htmlgraph/operations/__init__.py +79 -0
  200. htmlgraph/operations/analytics.py +339 -0
  201. htmlgraph/operations/bootstrap.py +289 -0
  202. htmlgraph/operations/events.py +244 -0
  203. htmlgraph/operations/fastapi_server.py +231 -0
  204. htmlgraph/operations/hooks.py +350 -0
  205. htmlgraph/operations/initialization.py +597 -0
  206. htmlgraph/operations/initialization.py.backup +228 -0
  207. htmlgraph/operations/server.py +303 -0
  208. htmlgraph/orchestration/__init__.py +58 -0
  209. htmlgraph/orchestration/claude_launcher.py +179 -0
  210. htmlgraph/orchestration/command_builder.py +72 -0
  211. htmlgraph/orchestration/headless_spawner.py +281 -0
  212. htmlgraph/orchestration/live_events.py +377 -0
  213. htmlgraph/orchestration/model_selection.py +327 -0
  214. htmlgraph/orchestration/plugin_manager.py +140 -0
  215. htmlgraph/orchestration/prompts.py +137 -0
  216. htmlgraph/orchestration/spawner_event_tracker.py +383 -0
  217. htmlgraph/orchestration/spawners/__init__.py +16 -0
  218. htmlgraph/orchestration/spawners/base.py +194 -0
  219. htmlgraph/orchestration/spawners/claude.py +173 -0
  220. htmlgraph/orchestration/spawners/codex.py +435 -0
  221. htmlgraph/orchestration/spawners/copilot.py +294 -0
  222. htmlgraph/orchestration/spawners/gemini.py +471 -0
  223. htmlgraph/orchestration/subprocess_runner.py +36 -0
  224. htmlgraph/orchestration/task_coordination.py +343 -0
  225. htmlgraph/orchestration.md +563 -0
  226. htmlgraph/orchestrator-system-prompt-optimized.txt +863 -0
  227. htmlgraph/orchestrator.py +669 -0
  228. htmlgraph/orchestrator_config.py +357 -0
  229. htmlgraph/orchestrator_mode.py +328 -0
  230. htmlgraph/orchestrator_validator.py +133 -0
  231. htmlgraph/parallel.py +646 -0
  232. htmlgraph/parser.py +160 -35
  233. htmlgraph/path_query.py +608 -0
  234. htmlgraph/pattern_matcher.py +636 -0
  235. htmlgraph/planning.py +147 -52
  236. htmlgraph/pydantic_models.py +476 -0
  237. htmlgraph/quality_gates.py +350 -0
  238. htmlgraph/query_builder.py +109 -72
  239. htmlgraph/query_composer.py +509 -0
  240. htmlgraph/reflection.py +443 -0
  241. htmlgraph/refs.py +344 -0
  242. htmlgraph/repo_hash.py +512 -0
  243. htmlgraph/repositories/__init__.py +292 -0
  244. htmlgraph/repositories/analytics_repository.py +455 -0
  245. htmlgraph/repositories/analytics_repository_standard.py +628 -0
  246. htmlgraph/repositories/feature_repository.py +581 -0
  247. htmlgraph/repositories/feature_repository_htmlfile.py +668 -0
  248. htmlgraph/repositories/feature_repository_memory.py +607 -0
  249. htmlgraph/repositories/feature_repository_sqlite.py +858 -0
  250. htmlgraph/repositories/filter_service.py +620 -0
  251. htmlgraph/repositories/filter_service_standard.py +445 -0
  252. htmlgraph/repositories/shared_cache.py +621 -0
  253. htmlgraph/repositories/shared_cache_memory.py +395 -0
  254. htmlgraph/repositories/track_repository.py +552 -0
  255. htmlgraph/repositories/track_repository_htmlfile.py +619 -0
  256. htmlgraph/repositories/track_repository_memory.py +508 -0
  257. htmlgraph/repositories/track_repository_sqlite.py +711 -0
  258. htmlgraph/routing.py +8 -19
  259. htmlgraph/scripts/deploy.py +1 -2
  260. htmlgraph/sdk/__init__.py +398 -0
  261. htmlgraph/sdk/__init__.pyi +14 -0
  262. htmlgraph/sdk/analytics/__init__.py +19 -0
  263. htmlgraph/sdk/analytics/engine.py +155 -0
  264. htmlgraph/sdk/analytics/helpers.py +178 -0
  265. htmlgraph/sdk/analytics/registry.py +109 -0
  266. htmlgraph/sdk/base.py +484 -0
  267. htmlgraph/sdk/constants.py +216 -0
  268. htmlgraph/sdk/core.pyi +308 -0
  269. htmlgraph/sdk/discovery.py +120 -0
  270. htmlgraph/sdk/help/__init__.py +12 -0
  271. htmlgraph/sdk/help/mixin.py +699 -0
  272. htmlgraph/sdk/mixins/__init__.py +15 -0
  273. htmlgraph/sdk/mixins/attribution.py +113 -0
  274. htmlgraph/sdk/mixins/mixin.py +410 -0
  275. htmlgraph/sdk/operations/__init__.py +12 -0
  276. htmlgraph/sdk/operations/mixin.py +427 -0
  277. htmlgraph/sdk/orchestration/__init__.py +17 -0
  278. htmlgraph/sdk/orchestration/coordinator.py +203 -0
  279. htmlgraph/sdk/orchestration/spawner.py +204 -0
  280. htmlgraph/sdk/planning/__init__.py +19 -0
  281. htmlgraph/sdk/planning/bottlenecks.py +93 -0
  282. htmlgraph/sdk/planning/mixin.py +211 -0
  283. htmlgraph/sdk/planning/parallel.py +186 -0
  284. htmlgraph/sdk/planning/queue.py +210 -0
  285. htmlgraph/sdk/planning/recommendations.py +87 -0
  286. htmlgraph/sdk/planning/smart_planning.py +319 -0
  287. htmlgraph/sdk/session/__init__.py +19 -0
  288. htmlgraph/sdk/session/continuity.py +57 -0
  289. htmlgraph/sdk/session/handoff.py +110 -0
  290. htmlgraph/sdk/session/info.py +309 -0
  291. htmlgraph/sdk/session/manager.py +103 -0
  292. htmlgraph/sdk/strategic/__init__.py +26 -0
  293. htmlgraph/sdk/strategic/mixin.py +563 -0
  294. htmlgraph/server.py +685 -180
  295. htmlgraph/services/__init__.py +10 -0
  296. htmlgraph/services/claiming.py +199 -0
  297. htmlgraph/session_hooks.py +300 -0
  298. htmlgraph/session_manager.py +1392 -175
  299. htmlgraph/session_registry.py +587 -0
  300. htmlgraph/session_state.py +436 -0
  301. htmlgraph/session_warning.py +201 -0
  302. htmlgraph/sessions/__init__.py +23 -0
  303. htmlgraph/sessions/handoff.py +756 -0
  304. htmlgraph/setup.py +34 -17
  305. htmlgraph/spike_index.py +143 -0
  306. htmlgraph/sync_docs.py +12 -15
  307. htmlgraph/system_prompts.py +450 -0
  308. htmlgraph/templates/AGENTS.md.template +366 -0
  309. htmlgraph/templates/CLAUDE.md.template +97 -0
  310. htmlgraph/templates/GEMINI.md.template +87 -0
  311. htmlgraph/templates/orchestration-view.html +350 -0
  312. htmlgraph/track_builder.py +146 -15
  313. htmlgraph/track_manager.py +69 -21
  314. htmlgraph/transcript.py +890 -0
  315. htmlgraph/transcript_analytics.py +699 -0
  316. htmlgraph/types.py +323 -0
  317. htmlgraph/validation.py +115 -0
  318. htmlgraph/watch.py +8 -5
  319. htmlgraph/work_type_utils.py +3 -2
  320. {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/dashboard.html +2406 -307
  321. htmlgraph-0.27.5.data/data/htmlgraph/templates/AGENTS.md.template +366 -0
  322. htmlgraph-0.27.5.data/data/htmlgraph/templates/CLAUDE.md.template +97 -0
  323. htmlgraph-0.27.5.data/data/htmlgraph/templates/GEMINI.md.template +87 -0
  324. {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/METADATA +97 -64
  325. htmlgraph-0.27.5.dist-info/RECORD +337 -0
  326. {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/entry_points.txt +1 -1
  327. htmlgraph/cli.py +0 -2688
  328. htmlgraph/sdk.py +0 -709
  329. htmlgraph-0.9.3.dist-info/RECORD +0 -61
  330. {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/styles.css +0 -0
  331. {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/WHEEL +0 -0
@@ -0,0 +1,668 @@
1
+ from __future__ import annotations
2
+
3
+ """
4
+ HtmlGraph Session Handler Module
5
+
6
+ Centralizes session lifecycle and tracking logic for hooks.
7
+ Provides unified functions for session initialization, tracking, and cleanup.
8
+
9
+ This module extracts common patterns from session-start.py and session-end.py
10
+ hooks to provide reusable session management operations.
11
+
12
+ Public API:
13
+ init_or_get_session(context: HookContext) -> Session | None
14
+ Get or create session from SessionManager
15
+
16
+ handle_session_start(context: HookContext, session: Session | None) -> dict
17
+ Initialize HtmlGraph tracking and build feature context
18
+
19
+ handle_session_end(context: HookContext) -> dict
20
+ Close session gracefully and record final metrics
21
+
22
+ record_user_query_event(context: HookContext, prompt: str) -> str | None
23
+ Create UserQuery event in database
24
+
25
+ check_version_status() -> dict | None
26
+ Check if HtmlGraph has updates available
27
+ """
28
+
29
+
30
+ import json
31
+ import logging
32
+ import os
33
+ import subprocess
34
+ from datetime import datetime, timezone
35
+ from pathlib import Path
36
+ from typing import TYPE_CHECKING, Any
37
+
38
+ if TYPE_CHECKING:
39
+ from htmlgraph.hooks.context import HookContext
40
+
41
+ logger = logging.getLogger(__name__)
42
+
43
+
44
+ def init_or_get_session(context: HookContext) -> Any | None:
45
+ """
46
+ Get or create session from SessionManager.
47
+
48
+ Attempts to get an active session for the current agent.
49
+ If none exists, creates a new one with automatic initialization.
50
+
51
+ Args:
52
+ context: HookContext with project and graph directory information
53
+
54
+ Returns:
55
+ Session object if successful, None if SessionManager unavailable or error occurs
56
+
57
+ Note:
58
+ - Handles graceful degradation if SessionManager cannot be imported
59
+ - Caches session in context for reuse
60
+ - Logs session ID for debugging
61
+ """
62
+ try:
63
+ manager = context.session_manager
64
+ agent = context.agent_id
65
+
66
+ # Try to get existing session for this agent
67
+ active = manager.get_active_session_for_agent(agent=agent)
68
+ if not active:
69
+ # Create new session with commit info
70
+ try:
71
+ head_commit = _get_head_commit(context.project_dir)
72
+ except Exception:
73
+ head_commit = None
74
+
75
+ active = manager.start_session(
76
+ session_id=None,
77
+ agent=agent,
78
+ start_commit=head_commit,
79
+ title=f"Session {datetime.now().strftime('%Y-%m-%d %H:%M')}",
80
+ )
81
+
82
+ context.log("info", f"Session initialized: {active.id if active else 'None'}")
83
+ return active
84
+
85
+ except ImportError as e:
86
+ context.log("error", f"SessionManager not available: {e}")
87
+ return None
88
+ except Exception as e:
89
+ context.log("error", f"Failed to initialize session: {e}")
90
+ return None
91
+
92
+
93
+ def handle_session_start(context: HookContext, session: Any | None) -> dict[str, Any]:
94
+ """
95
+ Initialize HtmlGraph tracking for the session.
96
+
97
+ Performs session startup operations:
98
+ - Initializes database entry if needed
99
+ - Loads active features and spikes from project
100
+ - Builds feature context string
101
+ - Records session start event
102
+ - Creates conversation-init spike if new conversation
103
+ - Injects concurrent session and recent work context
104
+
105
+ Args:
106
+ context: HookContext with project and graph directory information
107
+ session: Session object from SessionManager (optional)
108
+
109
+ Returns:
110
+ dict with:
111
+ {
112
+ "continue": True,
113
+ "hookSpecificOutput": {
114
+ "sessionFeatureContext": str with feature context,
115
+ "sessionContext": optional concurrent/recent work context,
116
+ "versionInfo": optional version check result
117
+ }
118
+ }
119
+ """
120
+ output: dict[str, Any] = {
121
+ "continue": True,
122
+ "hookSpecificOutput": {
123
+ "sessionFeatureContext": "",
124
+ "sessionContext": "",
125
+ "versionInfo": None,
126
+ },
127
+ }
128
+
129
+ if not session:
130
+ return output
131
+
132
+ # Ensure session exists in database
133
+ try:
134
+ db = context.database
135
+ cursor = db.connection.cursor()
136
+ cursor.execute(
137
+ "SELECT COUNT(*) FROM sessions WHERE session_id = ?",
138
+ (session.id,),
139
+ )
140
+ session_exists = cursor.fetchone()[0] > 0
141
+
142
+ if not session_exists:
143
+ cursor.execute(
144
+ """
145
+ INSERT INTO sessions (session_id, agent_assigned, created_at, status)
146
+ VALUES (?, ?, ?, 'active')
147
+ """,
148
+ (
149
+ session.id,
150
+ context.agent_id,
151
+ datetime.now(timezone.utc).isoformat(),
152
+ ),
153
+ )
154
+ db.connection.commit()
155
+ context.log("info", f"Created database session: {session.id}")
156
+ except ImportError:
157
+ context.log("debug", "Database not available, skipping session entry")
158
+ except Exception as e:
159
+ context.log("warning", f"Could not create database session: {e}")
160
+
161
+ # Track session start activity
162
+ try:
163
+ external_session_id = context.hook_input.get("session_id", "unknown")
164
+ context.session_manager.track_activity(
165
+ session_id=session.id,
166
+ tool="SessionStart",
167
+ summary=f"Session started: {external_session_id}",
168
+ payload={
169
+ "agent": context.agent_id,
170
+ "external_session_id": external_session_id,
171
+ },
172
+ )
173
+ except Exception as e:
174
+ context.log("warning", f"Could not track session start activity: {e}")
175
+
176
+ # Load features and build context
177
+ try:
178
+ features = _load_features(context.graph_dir)
179
+ active_features = [f for f in features if f.get("status") == "in-progress"]
180
+
181
+ if active_features:
182
+ feature_list = "\n".join(
183
+ [f"- **{f['id']}**: {f['title']}" for f in active_features[:3]]
184
+ )
185
+ context_str = f"""## Active Features
186
+
187
+ {feature_list}
188
+
189
+ Activity will be attributed to these features based on file patterns and keywords.
190
+
191
+ **To view all work and progress:** `htmlgraph snapshot --summary`"""
192
+ output["hookSpecificOutput"]["sessionFeatureContext"] = context_str
193
+ context.log("info", f"Loaded {len(active_features)} active features")
194
+
195
+ except Exception as e:
196
+ context.log("warning", f"Could not load features: {e}")
197
+
198
+ # Check version status
199
+ try:
200
+ version_info = check_version_status()
201
+ if version_info and version_info.get("is_outdated"):
202
+ output["hookSpecificOutput"]["versionInfo"] = version_info
203
+ context.log(
204
+ "info",
205
+ f"Update available: {version_info.get('installed')} → {version_info.get('latest')}",
206
+ )
207
+ except Exception as e:
208
+ context.log("debug", f"Could not check version: {e}")
209
+
210
+ # Build concurrent session context
211
+ try:
212
+ from htmlgraph.hooks.concurrent_sessions import (
213
+ format_concurrent_sessions_markdown,
214
+ format_recent_work_markdown,
215
+ get_concurrent_sessions,
216
+ get_recent_completed_sessions,
217
+ )
218
+
219
+ db = context.database
220
+
221
+ # Get concurrent sessions (other active windows)
222
+ concurrent = get_concurrent_sessions(db, context.session_id, minutes=30)
223
+ concurrent_md = format_concurrent_sessions_markdown(concurrent)
224
+
225
+ # Get recent completed work
226
+ recent = get_recent_completed_sessions(db, hours=24, limit=5)
227
+ recent_md = format_recent_work_markdown(recent)
228
+
229
+ # Build session context
230
+ session_context = ""
231
+ if concurrent_md:
232
+ session_context += concurrent_md + "\n"
233
+ if recent_md:
234
+ session_context += recent_md + "\n"
235
+
236
+ if session_context:
237
+ output["hookSpecificOutput"]["sessionContext"] = session_context.strip()
238
+ context.log(
239
+ "info",
240
+ f"Injected context: {len(concurrent)} concurrent, {len(recent)} recent",
241
+ )
242
+
243
+ except ImportError:
244
+ context.log("debug", "Concurrent session module not available")
245
+ except Exception as e:
246
+ context.log("warning", f"Failed to get concurrent session context: {e}")
247
+
248
+ # Update session with user's current query (if available from hook input)
249
+ try:
250
+ user_query = context.hook_input.get("prompt", "")
251
+ if user_query and session:
252
+ context.session_manager.track_activity(
253
+ session_id=session.id,
254
+ tool="UserQuery",
255
+ summary=user_query[:100],
256
+ payload={"query_length": len(user_query)},
257
+ )
258
+ except Exception as e:
259
+ context.log("warning", f"Failed to update session activity: {e}")
260
+
261
+ return output
262
+
263
+
264
+ def handle_session_end(context: HookContext) -> dict[str, Any]:
265
+ """
266
+ Close session gracefully and record final metrics.
267
+
268
+ Performs session end operations:
269
+ - Captures handoff notes if provided
270
+ - Links transcript if available
271
+ - Records session end event
272
+ - Cleans up temporary state files
273
+
274
+ Args:
275
+ context: HookContext with project and graph directory information
276
+
277
+ Returns:
278
+ dict with:
279
+ {
280
+ "continue": True,
281
+ "status": "success" | "partial" | "error"
282
+ }
283
+ """
284
+ output: dict[str, Any] = {
285
+ "continue": True,
286
+ "status": "success",
287
+ }
288
+
289
+ try:
290
+ session = context.session_manager.get_active_session()
291
+ if not session:
292
+ context.log("debug", "No active session to close")
293
+ else:
294
+ # Capture handoff context if provided
295
+ handoff_notes = context.hook_input.get("handoff_notes") or os.environ.get(
296
+ "HTMLGRAPH_HANDOFF_NOTES"
297
+ )
298
+ recommended_next = context.hook_input.get(
299
+ "recommended_next"
300
+ ) or os.environ.get("HTMLGRAPH_HANDOFF_RECOMMEND")
301
+ blockers_raw = context.hook_input.get("blockers") or os.environ.get(
302
+ "HTMLGRAPH_HANDOFF_BLOCKERS"
303
+ )
304
+
305
+ blockers = None
306
+ if isinstance(blockers_raw, str):
307
+ blockers = [b.strip() for b in blockers_raw.split(",") if b.strip()]
308
+ elif isinstance(blockers_raw, list):
309
+ blockers = [str(b).strip() for b in blockers_raw if str(b).strip()]
310
+
311
+ if handoff_notes or recommended_next or blockers:
312
+ try:
313
+ context.session_manager.set_session_handoff(
314
+ session_id=session.id,
315
+ handoff_notes=handoff_notes,
316
+ recommended_next=recommended_next,
317
+ blockers=blockers,
318
+ )
319
+ context.log("info", "Session handoff recorded")
320
+ except Exception as e:
321
+ context.log("warning", f"Could not set handoff: {e}")
322
+ output["status"] = "partial"
323
+
324
+ # Link transcript if external session ID provided
325
+ external_session_id = context.hook_input.get(
326
+ "session_id"
327
+ ) or os.environ.get("CLAUDE_SESSION_ID")
328
+ if external_session_id:
329
+ try:
330
+ from htmlgraph.transcript import TranscriptReader
331
+
332
+ reader = TranscriptReader()
333
+ transcript = reader.read_session(external_session_id)
334
+ if transcript:
335
+ context.session_manager.link_transcript(
336
+ session_id=session.id,
337
+ transcript_id=external_session_id,
338
+ transcript_path=str(transcript.path),
339
+ git_branch=transcript.git_branch
340
+ if hasattr(transcript, "git_branch")
341
+ else None,
342
+ )
343
+ context.log("info", "Transcript linked to session")
344
+ except ImportError:
345
+ context.log("debug", "Transcript reader not available")
346
+ except Exception as e:
347
+ context.log("warning", f"Could not link transcript: {e}")
348
+ output["status"] = "partial"
349
+
350
+ # Record session end activity
351
+ try:
352
+ context.session_manager.track_activity(
353
+ session_id=session.id,
354
+ tool="SessionEnd",
355
+ summary="Session ended",
356
+ )
357
+ except Exception as e:
358
+ context.log("warning", f"Could not track session end: {e}")
359
+ output["status"] = "partial"
360
+
361
+ context.log("info", f"Session closed: {session.id}")
362
+
363
+ except ImportError:
364
+ context.log("error", "SessionManager not available")
365
+ output["status"] = "error"
366
+ except Exception as e:
367
+ context.log("error", f"Failed to close session: {e}")
368
+ output["status"] = "error"
369
+
370
+ # Always cleanup temp files
371
+ try:
372
+ _cleanup_temp_files(context.graph_dir)
373
+ except Exception as e:
374
+ context.log("warning", f"Could not cleanup temp files: {e}")
375
+
376
+ return output
377
+
378
+
379
+ def record_user_query_event(context: HookContext, prompt: str) -> str | None:
380
+ """
381
+ Create UserQuery event in database.
382
+
383
+ Records a user query prompt as an event in the database for later
384
+ reference by tool calls in the same conversation turn.
385
+
386
+ Args:
387
+ context: HookContext with project and graph directory information
388
+ prompt: The user query prompt text
389
+
390
+ Returns:
391
+ event_id if successful, None otherwise
392
+
393
+ Note:
394
+ - Event ID is stored for parent-child linking of subsequent tool calls
395
+ - Events expire after 10 minutes (conversation turn boundary)
396
+ - Safe to call even if database unavailable (graceful degradation)
397
+ """
398
+ try:
399
+ from htmlgraph.ids import generate_id
400
+
401
+ db = context.database
402
+ event_id = generate_id("event")
403
+
404
+ # Preview for logging
405
+ preview = prompt[:100].replace("\n", " ")
406
+ if len(prompt) > 100:
407
+ preview += "..."
408
+
409
+ # Insert UserQuery event
410
+ success = db.insert_event(
411
+ event_id=event_id,
412
+ agent_id=context.agent_id,
413
+ event_type="user_query",
414
+ session_id=context.session_id,
415
+ tool_name="UserQuery",
416
+ input_summary=preview,
417
+ output_summary="Query recorded",
418
+ context={"full_prompt_length": len(prompt)},
419
+ )
420
+
421
+ if success:
422
+ context.log("info", f"Recorded UserQuery event: {event_id}")
423
+ return event_id
424
+ else:
425
+ context.log("warning", "Failed to insert UserQuery event")
426
+ return None
427
+
428
+ except ImportError:
429
+ context.log("debug", "Database not available for UserQuery event")
430
+ return None
431
+ except Exception as e:
432
+ context.log("error", f"Failed to record UserQuery event: {e}")
433
+ return None
434
+
435
+
436
+ def check_version_status() -> dict | None:
437
+ """
438
+ Check if HtmlGraph has updates available.
439
+
440
+ Compares installed version with latest version on PyPI.
441
+ Attempts multiple methods to get version information:
442
+ 1. import htmlgraph and check __version__
443
+ 2. pip show htmlgraph
444
+ 3. PyPI JSON API (requires network)
445
+
446
+ Returns:
447
+ dict with version info if outdated:
448
+ {
449
+ "installed": "0.9.0",
450
+ "latest": "0.9.1",
451
+ "is_outdated": True
452
+ }
453
+ None if versions match or cannot be determined
454
+
455
+ Note:
456
+ - Never blocks on network errors (5 second timeout)
457
+ - Gracefully degrades if methods unavailable
458
+ - Safe for use in hooks (catches all exceptions)
459
+ """
460
+ try:
461
+ installed_version = _get_installed_version()
462
+ latest_version = _get_latest_pypi_version()
463
+
464
+ if not (installed_version and latest_version):
465
+ return None
466
+
467
+ if installed_version == latest_version:
468
+ return None
469
+
470
+ # Compare versions
471
+ is_outdated = _compare_versions(installed_version, latest_version)
472
+ if is_outdated:
473
+ return {
474
+ "installed": installed_version,
475
+ "latest": latest_version,
476
+ "is_outdated": True,
477
+ }
478
+
479
+ return None
480
+
481
+ except Exception:
482
+ return None
483
+
484
+
485
+ # ============================================================================
486
+ # Private Helper Functions
487
+ # ============================================================================
488
+
489
+
490
+ def _get_head_commit(project_dir: str) -> str | None:
491
+ """Get current HEAD commit hash (short form)."""
492
+ try:
493
+ result = subprocess.run(
494
+ ["git", "rev-parse", "--short", "HEAD"],
495
+ capture_output=True,
496
+ text=True,
497
+ cwd=project_dir,
498
+ timeout=5,
499
+ )
500
+ if result.returncode == 0:
501
+ return result.stdout.strip()
502
+ except Exception:
503
+ pass
504
+ return None
505
+
506
+
507
+ def _load_features(graph_dir: Path) -> list[dict]:
508
+ """Load all features as dicts."""
509
+ try:
510
+ from htmlgraph.converter import node_to_dict # type: ignore[import]
511
+ from htmlgraph.graph import HtmlGraph
512
+
513
+ features_dir = graph_dir / "features"
514
+ if not features_dir.exists():
515
+ return []
516
+
517
+ graph = HtmlGraph(features_dir, auto_load=True)
518
+ return [node_to_dict(node) for node in graph.nodes.values()]
519
+
520
+ except Exception:
521
+ return []
522
+
523
+
524
+ def _get_installed_version() -> str | None:
525
+ """Get installed htmlgraph version."""
526
+ # Method 1: Import and check __version__
527
+ try:
528
+ import htmlgraph
529
+
530
+ return htmlgraph.__version__
531
+ except Exception:
532
+ pass
533
+
534
+ # Method 2: pip show
535
+ try:
536
+ result = subprocess.run(
537
+ ["pip", "show", "htmlgraph"],
538
+ capture_output=True,
539
+ text=True,
540
+ timeout=10,
541
+ )
542
+ if result.returncode == 0:
543
+ for line in result.stdout.splitlines():
544
+ if line.startswith("Version:"):
545
+ return line.split(":", 1)[1].strip()
546
+ except Exception:
547
+ pass
548
+
549
+ return None
550
+
551
+
552
+ def _get_latest_pypi_version() -> str | None:
553
+ """Get latest htmlgraph version from PyPI."""
554
+ try:
555
+ import urllib.request
556
+
557
+ req = urllib.request.Request(
558
+ "https://pypi.org/pypi/htmlgraph/json",
559
+ headers={
560
+ "Accept": "application/json",
561
+ "User-Agent": "htmlgraph-version-check",
562
+ },
563
+ )
564
+ with urllib.request.urlopen(req, timeout=5) as response:
565
+ data = json.loads(response.read().decode())
566
+ version: str | None = data.get("info", {}).get("version")
567
+ return version
568
+ except Exception:
569
+ return None
570
+
571
+
572
+ def _compare_versions(installed: str, latest: str) -> bool:
573
+ """
574
+ Check if installed version is older than latest.
575
+
576
+ Args:
577
+ installed: Installed version string
578
+ latest: Latest version string
579
+
580
+ Returns:
581
+ True if installed < latest, False otherwise
582
+
583
+ Note:
584
+ - Uses semantic versioning comparison
585
+ - Falls back to string comparison for non-semver versions
586
+ """
587
+ try:
588
+ # Try semantic version comparison
589
+ installed_parts = [int(x) for x in installed.split(".")]
590
+ latest_parts = [int(x) for x in latest.split(".")]
591
+ return installed_parts < latest_parts
592
+ except (ValueError, IndexError):
593
+ # Fallback to string comparison
594
+ return installed != latest
595
+
596
+
597
+ def _cleanup_temp_files(graph_dir: Path) -> None:
598
+ """
599
+ Clean up temporary state files after session end.
600
+
601
+ Removes session-scoped temporary files that are no longer needed.
602
+ Safe to call even if files don't exist (idempotent).
603
+
604
+ Args:
605
+ graph_dir: Path to .htmlgraph directory
606
+ """
607
+ temp_patterns = [
608
+ "parent-activity.json",
609
+ "user-query-event-*.json",
610
+ ]
611
+
612
+ for pattern in temp_patterns:
613
+ if "*" in pattern:
614
+ # Handle glob patterns
615
+ import glob
616
+
617
+ for path in glob.glob(str(graph_dir / pattern)):
618
+ try:
619
+ Path(path).unlink()
620
+ logger.debug(f"Cleaned up: {path}")
621
+ except Exception as e:
622
+ logger.debug(f"Could not clean up {path}: {e}")
623
+ else:
624
+ # Handle single file
625
+ file_path: Path = graph_dir / pattern
626
+ try:
627
+ if file_path.exists():
628
+ file_path.unlink()
629
+ logger.debug(f"Cleaned up: {file_path}")
630
+ except Exception as e:
631
+ logger.debug(f"Could not clean up {file_path}: {e}")
632
+
633
+
634
+ __all__ = [
635
+ "init_or_get_session",
636
+ "handle_session_start",
637
+ "handle_session_end",
638
+ "record_user_query_event",
639
+ "check_version_status",
640
+ ]
641
+
642
+
643
+ def main() -> None:
644
+ """Hook entry point for SessionEnd hook."""
645
+ import json
646
+ import os
647
+ import sys
648
+
649
+ # Check if tracking is disabled
650
+ if os.environ.get("HTMLGRAPH_DISABLE_TRACKING") == "1":
651
+ print(json.dumps({"continue": True}))
652
+ sys.exit(0)
653
+
654
+ try:
655
+ hook_input = json.load(sys.stdin)
656
+ except json.JSONDecodeError:
657
+ hook_input = {}
658
+
659
+ # Create context from hook input
660
+ from htmlgraph.hooks.context import HookContext
661
+
662
+ context = HookContext.from_input(hook_input)
663
+
664
+ # Handle session end
665
+ response = handle_session_end(context)
666
+
667
+ # Output JSON response
668
+ print(json.dumps(response))