htmlgraph 0.25.0__py3-none-any.whl → 0.26.2__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.
- htmlgraph/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/.htmlgraph/agents.json +72 -0
- htmlgraph/.htmlgraph/htmlgraph.db +0 -0
- htmlgraph/__init__.py +1 -1
- htmlgraph/api/main.py +252 -47
- htmlgraph/api/templates/dashboard.html +11 -0
- htmlgraph/api/templates/partials/activity-feed.html +517 -8
- htmlgraph/cli.py +1 -1
- htmlgraph/config.py +173 -96
- htmlgraph/dashboard.html +632 -7237
- htmlgraph/db/schema.py +258 -9
- htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/hooks/.htmlgraph/agents.json +72 -0
- htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
- htmlgraph/hooks/cigs_pretool_enforcer.py +2 -2
- htmlgraph/hooks/concurrent_sessions.py +208 -0
- htmlgraph/hooks/context.py +88 -10
- htmlgraph/hooks/drift_handler.py +24 -20
- htmlgraph/hooks/event_tracker.py +264 -189
- htmlgraph/hooks/orchestrator.py +6 -4
- htmlgraph/hooks/orchestrator_reflector.py +4 -4
- htmlgraph/hooks/pretooluse.py +63 -36
- htmlgraph/hooks/prompt_analyzer.py +14 -25
- htmlgraph/hooks/session_handler.py +123 -69
- htmlgraph/hooks/state_manager.py +7 -4
- htmlgraph/hooks/subagent_stop.py +3 -2
- htmlgraph/hooks/validator.py +15 -11
- htmlgraph/operations/fastapi_server.py +2 -2
- htmlgraph/orchestration/headless_spawner.py +489 -16
- htmlgraph/orchestration/live_events.py +377 -0
- htmlgraph/server.py +100 -203
- htmlgraph-0.26.2.data/data/htmlgraph/dashboard.html +812 -0
- {htmlgraph-0.25.0.dist-info → htmlgraph-0.26.2.dist-info}/METADATA +1 -1
- {htmlgraph-0.25.0.dist-info → htmlgraph-0.26.2.dist-info}/RECORD +40 -32
- htmlgraph-0.25.0.data/data/htmlgraph/dashboard.html +0 -7417
- {htmlgraph-0.25.0.data → htmlgraph-0.26.2.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.25.0.data → htmlgraph-0.26.2.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
- {htmlgraph-0.25.0.data → htmlgraph-0.26.2.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
- {htmlgraph-0.25.0.data → htmlgraph-0.26.2.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
- {htmlgraph-0.25.0.dist-info → htmlgraph-0.26.2.dist-info}/WHEEL +0 -0
- {htmlgraph-0.25.0.dist-info → htmlgraph-0.26.2.dist-info}/entry_points.txt +0 -0
htmlgraph/hooks/context.py
CHANGED
|
@@ -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,69 @@ class HookContext:
|
|
|
88
91
|
resolve_project_dir,
|
|
89
92
|
)
|
|
90
93
|
|
|
91
|
-
#
|
|
92
|
-
|
|
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. Most recent active session from database (NEW)
|
|
105
|
+
# 6. "unknown" as last resort
|
|
106
|
+
#
|
|
107
|
+
# NOTE: We intentionally do NOT use SessionManager.get_active_session()
|
|
108
|
+
# as a fallback because the "active session" is stored in a global file
|
|
109
|
+
# (.htmlgraph/session.json) that's shared across all Claude windows.
|
|
110
|
+
# Using it would cause cross-window event contamination where tool calls
|
|
111
|
+
# from Window B get linked to UserQuery events from Window A.
|
|
112
|
+
#
|
|
113
|
+
# However, we DO query the database by status='active' and created_at,
|
|
114
|
+
# which is different because it retrieves the most recent session that
|
|
115
|
+
# was explicitly marked as active (e.g., by SessionStart hook), without
|
|
116
|
+
# relying on a shared global agent state file.
|
|
117
|
+
session_id = (
|
|
118
|
+
hook_input.get("session_id")
|
|
119
|
+
or hook_input.get("sessionId")
|
|
120
|
+
or os.environ.get("HTMLGRAPH_SESSION_ID")
|
|
121
|
+
or os.environ.get("CLAUDE_SESSION_ID")
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Fallback: Query database for most recent active session
|
|
125
|
+
# This solves the issue where PostToolUse hooks don't receive session_id
|
|
126
|
+
# in hook_input, but SessionStart hook already created a session in the database.
|
|
127
|
+
if not session_id:
|
|
128
|
+
db_path = graph_dir / "htmlgraph.db"
|
|
129
|
+
if db_path.exists():
|
|
130
|
+
try:
|
|
131
|
+
import sqlite3
|
|
132
|
+
|
|
133
|
+
conn = sqlite3.connect(str(db_path), timeout=1.0)
|
|
134
|
+
cursor = conn.cursor()
|
|
135
|
+
cursor.execute("""
|
|
136
|
+
SELECT session_id FROM sessions
|
|
137
|
+
WHERE status = 'active'
|
|
138
|
+
ORDER BY created_at DESC
|
|
139
|
+
LIMIT 1
|
|
140
|
+
""")
|
|
141
|
+
row = cursor.fetchone()
|
|
142
|
+
conn.close()
|
|
143
|
+
if row:
|
|
144
|
+
session_id = row[0]
|
|
145
|
+
logger.info(f"Resolved session_id from database: {session_id}")
|
|
146
|
+
except Exception as e:
|
|
147
|
+
logger.warning(f"Failed to query active session from database: {e}")
|
|
148
|
+
|
|
149
|
+
# Final fallback to "unknown" if database query fails
|
|
150
|
+
if not session_id:
|
|
151
|
+
session_id = "unknown"
|
|
152
|
+
logger.warning(
|
|
153
|
+
"Could not resolve session_id from hook_input, environment, or database. "
|
|
154
|
+
"Events will not be linked to parent UserQuery. "
|
|
155
|
+
"For multi-window support, set HTMLGRAPH_SESSION_ID env var."
|
|
156
|
+
)
|
|
93
157
|
|
|
94
158
|
# Detect agent ID (priority order)
|
|
95
159
|
# 1. Explicit agent_id in hook input
|
|
@@ -102,15 +166,28 @@ class HookContext:
|
|
|
102
166
|
or os.environ.get("CLAUDE_AGENT_NICKNAME", "unknown")
|
|
103
167
|
)
|
|
104
168
|
|
|
105
|
-
#
|
|
106
|
-
|
|
169
|
+
# Detect model name (priority order)
|
|
170
|
+
# 1. Explicit model_name in hook input
|
|
171
|
+
# 2. CLAUDE_MODEL environment variable
|
|
172
|
+
# 3. HTMLGRAPH_MODEL environment variable
|
|
173
|
+
# 4. Status line cache (from ~/.cache/claude-code/status-{session_id}.json)
|
|
174
|
+
# 5. None (not available)
|
|
175
|
+
model_name = (
|
|
176
|
+
hook_input.get("model_name")
|
|
177
|
+
or hook_input.get("model")
|
|
178
|
+
or os.environ.get("CLAUDE_MODEL")
|
|
179
|
+
or os.environ.get("HTMLGRAPH_MODEL")
|
|
180
|
+
)
|
|
107
181
|
|
|
108
|
-
#
|
|
109
|
-
|
|
182
|
+
# Fallback: Try status line cache if model not detected yet
|
|
183
|
+
if not model_name and session_id and session_id != "unknown":
|
|
184
|
+
from htmlgraph.hooks.event_tracker import get_model_from_status_cache
|
|
185
|
+
|
|
186
|
+
model_name = get_model_from_status_cache(session_id)
|
|
110
187
|
|
|
111
188
|
logger.info(
|
|
112
189
|
f"Initializing hook context: session={session_id}, "
|
|
113
|
-
f"agent={agent_id}, project={project_dir}"
|
|
190
|
+
f"agent={agent_id}, model={model_name}, project={project_dir}"
|
|
114
191
|
)
|
|
115
192
|
|
|
116
193
|
return cls(
|
|
@@ -119,6 +196,7 @@ class HookContext:
|
|
|
119
196
|
session_id=session_id,
|
|
120
197
|
agent_id=agent_id,
|
|
121
198
|
hook_input=hook_input,
|
|
199
|
+
model_name=model_name,
|
|
122
200
|
)
|
|
123
201
|
|
|
124
202
|
@property
|
htmlgraph/hooks/drift_handler.py
CHANGED
|
@@ -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
|
|
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(
|
|
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
|
-
) ->
|
|
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,
|
|
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
|
-
[
|
|
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:
|