htmlgraph 0.25.0__py3-none-any.whl → 0.26.1__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 (32) hide show
  1. htmlgraph/__init__.py +1 -1
  2. htmlgraph/api/main.py +193 -45
  3. htmlgraph/api/templates/dashboard.html +11 -0
  4. htmlgraph/api/templates/partials/activity-feed.html +458 -8
  5. htmlgraph/dashboard.html +41 -0
  6. htmlgraph/db/schema.py +254 -4
  7. htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
  8. htmlgraph/hooks/.htmlgraph/agents.json +72 -0
  9. htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
  10. htmlgraph/hooks/cigs_pretool_enforcer.py +2 -2
  11. htmlgraph/hooks/concurrent_sessions.py +208 -0
  12. htmlgraph/hooks/context.py +57 -10
  13. htmlgraph/hooks/drift_handler.py +24 -20
  14. htmlgraph/hooks/event_tracker.py +204 -177
  15. htmlgraph/hooks/orchestrator.py +6 -4
  16. htmlgraph/hooks/orchestrator_reflector.py +4 -4
  17. htmlgraph/hooks/pretooluse.py +3 -6
  18. htmlgraph/hooks/prompt_analyzer.py +14 -25
  19. htmlgraph/hooks/session_handler.py +123 -69
  20. htmlgraph/hooks/state_manager.py +7 -4
  21. htmlgraph/hooks/validator.py +15 -11
  22. htmlgraph/orchestration/headless_spawner.py +322 -15
  23. htmlgraph/orchestration/live_events.py +377 -0
  24. {htmlgraph-0.25.0.data → htmlgraph-0.26.1.data}/data/htmlgraph/dashboard.html +41 -0
  25. {htmlgraph-0.25.0.dist-info → htmlgraph-0.26.1.dist-info}/METADATA +1 -1
  26. {htmlgraph-0.25.0.dist-info → htmlgraph-0.26.1.dist-info}/RECORD +32 -27
  27. {htmlgraph-0.25.0.data → htmlgraph-0.26.1.data}/data/htmlgraph/styles.css +0 -0
  28. {htmlgraph-0.25.0.data → htmlgraph-0.26.1.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
  29. {htmlgraph-0.25.0.data → htmlgraph-0.26.1.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
  30. {htmlgraph-0.25.0.data → htmlgraph-0.26.1.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
  31. {htmlgraph-0.25.0.dist-info → htmlgraph-0.26.1.dist-info}/WHEEL +0 -0
  32. {htmlgraph-0.25.0.dist-info → htmlgraph-0.26.1.dist-info}/entry_points.txt +0 -0
@@ -37,6 +37,7 @@ class HookContext:
37
37
  session_id: Unique session identifier for this execution
38
38
  agent_id: Agent/tool that's executing (e.g., 'claude-code', 'codex')
39
39
  hook_input: Raw hook input data from Claude Code
40
+ model_name: Specific Claude model name (e.g., 'claude-haiku', 'claude-opus', 'claude-sonnet')
40
41
  _session_manager: Cached SessionManager instance (lazy-loaded)
41
42
  _database: Cached HtmlGraphDB instance (lazy-loaded)
42
43
  """
@@ -45,18 +46,20 @@ class HookContext:
45
46
  graph_dir: Path
46
47
  session_id: str
47
48
  agent_id: str
48
- hook_input: dict
49
+ hook_input: dict[str, Any]
50
+ model_name: str | None = field(default=None, repr=False)
49
51
  _session_manager: Any | None = field(default=None, repr=False)
50
52
  _database: Any | None = field(default=None, repr=False)
51
53
 
52
54
  @classmethod
53
- def from_input(cls, hook_input: dict) -> "HookContext":
55
+ def from_input(cls, hook_input: dict[str, Any]) -> "HookContext":
54
56
  """
55
57
  Create HookContext from raw hook input.
56
58
 
57
59
  Performs automatic environment resolution:
58
60
  - Extracts session_id from hook_input
59
61
  - Detects agent_id from environment or hook_input
62
+ - Detects model_name (e.g., claude-haiku, claude-opus, claude-sonnet)
60
63
  - Resolves project directory via bootstrap
61
64
  - Initializes graph directory
62
65
 
@@ -79,7 +82,7 @@ class HookContext:
79
82
  ...
80
83
  }
81
84
  context = HookContext.from_input(hook_input)
82
- logger.info(f"Session: {context.session_id}, Agent: {context.agent_id}")
85
+ logger.info(f"Session: {context.session_id}, Agent: {context.agent_id}, Model: {context.model_name}")
83
86
  ```
84
87
  """
85
88
  # Import bootstrap locally to avoid circular imports
@@ -88,8 +91,38 @@ class HookContext:
88
91
  resolve_project_dir,
89
92
  )
90
93
 
91
- # Extract session ID from hook input
92
- session_id = hook_input.get("session_id", "unknown")
94
+ # Resolve project directory first
95
+ project_dir = resolve_project_dir()
96
+ graph_dir = get_graph_dir(project_dir)
97
+
98
+ # Extract session ID with multiple fallbacks
99
+ # Priority order:
100
+ # 1. hook_input["session_id"] (if Claude Code passes it)
101
+ # 2. hook_input["sessionId"] (camelCase variant)
102
+ # 3. HTMLGRAPH_SESSION_ID environment variable
103
+ # 4. CLAUDE_SESSION_ID environment variable
104
+ # 5. "unknown" as last resort
105
+ #
106
+ # NOTE: We intentionally do NOT use SessionManager.get_active_session()
107
+ # as a fallback because the "active session" is stored in a global file
108
+ # (.htmlgraph/session.json) that's shared across all Claude windows.
109
+ # Using it would cause cross-window event contamination where tool calls
110
+ # from Window B get linked to UserQuery events from Window A.
111
+ session_id = (
112
+ hook_input.get("session_id")
113
+ or hook_input.get("sessionId")
114
+ or os.environ.get("HTMLGRAPH_SESSION_ID")
115
+ or os.environ.get("CLAUDE_SESSION_ID")
116
+ )
117
+
118
+ # Fallback to "unknown" - better than cross-window contamination
119
+ if not session_id:
120
+ session_id = "unknown"
121
+ logger.warning(
122
+ "Could not resolve session_id from hook_input or environment. "
123
+ "Events will not be linked to parent UserQuery. "
124
+ "For multi-window support, set HTMLGRAPH_SESSION_ID env var."
125
+ )
93
126
 
94
127
  # Detect agent ID (priority order)
95
128
  # 1. Explicit agent_id in hook input
@@ -102,15 +135,28 @@ class HookContext:
102
135
  or os.environ.get("CLAUDE_AGENT_NICKNAME", "unknown")
103
136
  )
104
137
 
105
- # Resolve project directory with fallbacks
106
- project_dir = resolve_project_dir()
138
+ # Detect model name (priority order)
139
+ # 1. Explicit model_name in hook input
140
+ # 2. CLAUDE_MODEL environment variable
141
+ # 3. HTMLGRAPH_MODEL environment variable
142
+ # 4. Status line cache (from ~/.cache/claude-code/status-{session_id}.json)
143
+ # 5. None (not available)
144
+ model_name = (
145
+ hook_input.get("model_name")
146
+ or hook_input.get("model")
147
+ or os.environ.get("CLAUDE_MODEL")
148
+ or os.environ.get("HTMLGRAPH_MODEL")
149
+ )
107
150
 
108
- # Get or create graph directory
109
- graph_dir = get_graph_dir(project_dir)
151
+ # Fallback: Try status line cache if model not detected yet
152
+ if not model_name and session_id and session_id != "unknown":
153
+ from htmlgraph.hooks.event_tracker import get_model_from_status_cache
154
+
155
+ model_name = get_model_from_status_cache(session_id)
110
156
 
111
157
  logger.info(
112
158
  f"Initializing hook context: session={session_id}, "
113
- f"agent={agent_id}, project={project_dir}"
159
+ f"agent={agent_id}, model={model_name}, project={project_dir}"
114
160
  )
115
161
 
116
162
  return cls(
@@ -119,6 +165,7 @@ class HookContext:
119
165
  session_id=session_id,
120
166
  agent_id=agent_id,
121
167
  hook_input=hook_input,
168
+ model_name=model_name,
122
169
  )
123
170
 
124
171
  @property
@@ -25,10 +25,9 @@ import os
25
25
  import subprocess
26
26
  from datetime import datetime, timedelta
27
27
  from pathlib import Path
28
- from typing import Any, Optional
28
+ from typing import Any
29
29
 
30
30
  from htmlgraph.hooks.context import HookContext
31
- from htmlgraph.hooks.state_manager import DriftQueueManager
32
31
 
33
32
  logger = logging.getLogger(__name__)
34
33
 
@@ -113,7 +112,7 @@ DEFAULT_DRIFT_CONFIG = {
113
112
  }
114
113
 
115
114
 
116
- def load_drift_config(graph_dir: Path) -> dict:
115
+ def load_drift_config(graph_dir: Path) -> dict[str, Any]:
117
116
  """
118
117
  Load drift configuration from project or fallback to defaults.
119
118
 
@@ -151,7 +150,7 @@ def load_drift_config(graph_dir: Path) -> dict:
151
150
  if config_path.exists() and config_path.is_file():
152
151
  try:
153
152
  with open(config_path) as f:
154
- config = json.load(f)
153
+ config: dict[str, Any] = json.load(f)
155
154
  logger.debug(f"Loaded drift config from {config_path}")
156
155
  return config
157
156
  except json.JSONDecodeError as e:
@@ -163,7 +162,9 @@ def load_drift_config(graph_dir: Path) -> dict:
163
162
  return DEFAULT_DRIFT_CONFIG
164
163
 
165
164
 
166
- def detect_drift(activity_result: dict, config: dict) -> tuple[float, Optional[str]]:
165
+ def detect_drift(
166
+ activity_result: dict[str, Any], config: dict[str, Any]
167
+ ) -> tuple[float, str | None]:
167
168
  """
168
169
  Calculate drift score from activity result and check thresholds.
169
170
 
@@ -201,9 +202,7 @@ def detect_drift(activity_result: dict, config: dict) -> tuple[float, Optional[s
201
202
  drift_score = getattr(activity_result, "drift_score", 0.0) or 0.0
202
203
  feature_id = getattr(activity_result, "feature_id", None)
203
204
 
204
- logger.debug(
205
- f"Drift detected: score={drift_score:.2f}, feature={feature_id}"
206
- )
205
+ logger.debug(f"Drift detected: score={drift_score:.2f}, feature={feature_id}")
207
206
 
208
207
  return (drift_score, feature_id)
209
208
 
@@ -211,9 +210,9 @@ def detect_drift(activity_result: dict, config: dict) -> tuple[float, Optional[s
211
210
  def handle_high_drift(
212
211
  context: HookContext,
213
212
  drift_score: float,
214
- queue: dict,
215
- config: dict,
216
- ) -> Optional[str]:
213
+ queue: dict[str, Any],
214
+ config: dict[str, Any],
215
+ ) -> str | None:
217
216
  """
218
217
  Generate nudge message for high-drift activities.
219
218
 
@@ -255,7 +254,6 @@ def handle_high_drift(
255
254
  return None
256
255
 
257
256
  # Get queue size for nudge message
258
- queue_manager = DriftQueueManager(context.graph_dir)
259
257
  min_activities = drift_config.get("min_activities_before_classify", 3)
260
258
  current_count = len(queue.get("activities", []))
261
259
 
@@ -275,9 +273,9 @@ def handle_high_drift(
275
273
 
276
274
  def trigger_auto_classification(
277
275
  context: HookContext,
278
- queue: dict,
276
+ queue: dict[str, Any],
279
277
  feature_id: str,
280
- config: dict,
278
+ config: dict[str, Any],
281
279
  ) -> bool:
282
280
  """
283
281
  Check if auto-classification should be triggered.
@@ -344,7 +342,7 @@ def trigger_auto_classification(
344
342
  return True
345
343
 
346
344
 
347
- def build_classification_prompt(queue: dict, feature_id: str) -> str:
345
+ def build_classification_prompt(queue: dict[str, Any], feature_id: str) -> str:
348
346
  """
349
347
  Build structured prompt for auto-classification agent.
350
348
 
@@ -423,8 +421,8 @@ Create the work item now using Write tool."""
423
421
 
424
422
 
425
423
  def run_headless_classification(
426
- context: HookContext, prompt: str, config: dict
427
- ) -> tuple[bool, Optional[str]]:
424
+ context: HookContext, prompt: str, config: dict[str, Any]
425
+ ) -> tuple[bool, str | None]:
428
426
  """
429
427
  Attempt to run auto-classification via headless claude subprocess.
430
428
 
@@ -460,7 +458,14 @@ def run_headless_classification(
460
458
 
461
459
  try:
462
460
  result = subprocess.run(
463
- ["claude", "-p", prompt, "--model", model, "--dangerously-skip-permissions"],
461
+ [
462
+ "claude",
463
+ "-p",
464
+ prompt,
465
+ "--model",
466
+ model,
467
+ "--dangerously-skip-permissions",
468
+ ],
464
469
  capture_output=True,
465
470
  text=True,
466
471
  timeout=120,
@@ -497,8 +502,7 @@ def run_headless_classification(
497
502
  except FileNotFoundError:
498
503
  logger.error("claude command not found")
499
504
  nudge = (
500
- "HIGH DRIFT - claude not available. "
501
- "Please classify manually in .htmlgraph/"
505
+ "HIGH DRIFT - claude not available. Please classify manually in .htmlgraph/"
502
506
  )
503
507
  return (False, nudge)
504
508
  except Exception as e: