htmlgraph 0.26.1__py3-none-any.whl → 0.26.3__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 +66 -9
- htmlgraph/api/templates/partials/activity-feed.html +59 -0
- htmlgraph/cli.py +1 -1
- htmlgraph/config.py +173 -96
- htmlgraph/dashboard.html +631 -7277
- htmlgraph/db/schema.py +4 -5
- htmlgraph/hooks/.htmlgraph/.session-warning-state.json +1 -1
- htmlgraph/hooks/context.py +40 -8
- htmlgraph/hooks/event_tracker.py +60 -12
- htmlgraph/hooks/pretooluse.py +60 -30
- htmlgraph/hooks/subagent_stop.py +3 -2
- htmlgraph/operations/fastapi_server.py +2 -2
- htmlgraph/orchestration/headless_spawner.py +167 -1
- htmlgraph/server.py +100 -203
- htmlgraph-0.26.3.data/data/htmlgraph/dashboard.html +812 -0
- {htmlgraph-0.26.1.dist-info → htmlgraph-0.26.3.dist-info}/METADATA +1 -1
- {htmlgraph-0.26.1.dist-info → htmlgraph-0.26.3.dist-info}/RECORD +27 -24
- htmlgraph-0.26.1.data/data/htmlgraph/dashboard.html +0 -7458
- {htmlgraph-0.26.1.data → htmlgraph-0.26.3.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.26.1.data → htmlgraph-0.26.3.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
- {htmlgraph-0.26.1.data → htmlgraph-0.26.3.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
- {htmlgraph-0.26.1.data → htmlgraph-0.26.3.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
- {htmlgraph-0.26.1.dist-info → htmlgraph-0.26.3.dist-info}/WHEEL +0 -0
- {htmlgraph-0.26.1.dist-info → htmlgraph-0.26.3.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "1.0",
|
|
3
|
+
"updated": "2026-01-11T04:13:54.354401",
|
|
4
|
+
"agents": {
|
|
5
|
+
"claude": {
|
|
6
|
+
"id": "claude",
|
|
7
|
+
"name": "Claude",
|
|
8
|
+
"capabilities": [
|
|
9
|
+
"python",
|
|
10
|
+
"javascript",
|
|
11
|
+
"typescript",
|
|
12
|
+
"html",
|
|
13
|
+
"css",
|
|
14
|
+
"code-review",
|
|
15
|
+
"testing",
|
|
16
|
+
"documentation",
|
|
17
|
+
"debugging",
|
|
18
|
+
"refactoring",
|
|
19
|
+
"architecture",
|
|
20
|
+
"api-design"
|
|
21
|
+
],
|
|
22
|
+
"max_parallel_tasks": 3,
|
|
23
|
+
"preferred_complexity": [
|
|
24
|
+
"low",
|
|
25
|
+
"medium",
|
|
26
|
+
"high",
|
|
27
|
+
"very-high"
|
|
28
|
+
],
|
|
29
|
+
"active": true,
|
|
30
|
+
"metadata": {}
|
|
31
|
+
},
|
|
32
|
+
"gemini": {
|
|
33
|
+
"id": "gemini",
|
|
34
|
+
"name": "Gemini",
|
|
35
|
+
"capabilities": [
|
|
36
|
+
"python",
|
|
37
|
+
"data-analysis",
|
|
38
|
+
"documentation",
|
|
39
|
+
"testing",
|
|
40
|
+
"code-review",
|
|
41
|
+
"javascript"
|
|
42
|
+
],
|
|
43
|
+
"max_parallel_tasks": 2,
|
|
44
|
+
"preferred_complexity": [
|
|
45
|
+
"low",
|
|
46
|
+
"medium",
|
|
47
|
+
"high"
|
|
48
|
+
],
|
|
49
|
+
"active": true,
|
|
50
|
+
"metadata": {}
|
|
51
|
+
},
|
|
52
|
+
"codex": {
|
|
53
|
+
"id": "codex",
|
|
54
|
+
"name": "Codex",
|
|
55
|
+
"capabilities": [
|
|
56
|
+
"python",
|
|
57
|
+
"javascript",
|
|
58
|
+
"debugging",
|
|
59
|
+
"testing",
|
|
60
|
+
"code-generation",
|
|
61
|
+
"documentation"
|
|
62
|
+
],
|
|
63
|
+
"max_parallel_tasks": 2,
|
|
64
|
+
"preferred_complexity": [
|
|
65
|
+
"low",
|
|
66
|
+
"medium"
|
|
67
|
+
],
|
|
68
|
+
"active": true,
|
|
69
|
+
"metadata": {}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
Binary file
|
htmlgraph/__init__.py
CHANGED
htmlgraph/api/main.py
CHANGED
|
@@ -394,7 +394,7 @@ def get_app(db_path: str) -> FastAPI:
|
|
|
394
394
|
query = """
|
|
395
395
|
SELECT e.event_id, e.agent_id, e.event_type, e.timestamp, e.tool_name,
|
|
396
396
|
e.input_summary, e.output_summary, e.session_id,
|
|
397
|
-
e.status, e.model
|
|
397
|
+
e.parent_event_id, e.status, e.model
|
|
398
398
|
FROM agent_events e
|
|
399
399
|
WHERE 1=1
|
|
400
400
|
"""
|
|
@@ -428,9 +428,9 @@ def get_app(db_path: str) -> FastAPI:
|
|
|
428
428
|
input_summary=row[5],
|
|
429
429
|
output_summary=row[6],
|
|
430
430
|
session_id=row[7],
|
|
431
|
-
parent_event_id=
|
|
432
|
-
status=row[
|
|
433
|
-
model=row[
|
|
431
|
+
parent_event_id=row[8],
|
|
432
|
+
status=row[9],
|
|
433
|
+
model=row[10],
|
|
434
434
|
)
|
|
435
435
|
for row in rows
|
|
436
436
|
]
|
|
@@ -768,7 +768,8 @@ def get_app(db_path: str) -> FastAPI:
|
|
|
768
768
|
output_summary,
|
|
769
769
|
session_id,
|
|
770
770
|
status,
|
|
771
|
-
model
|
|
771
|
+
model,
|
|
772
|
+
parent_event_id
|
|
772
773
|
FROM agent_events
|
|
773
774
|
WHERE event_type IN ({event_type_placeholders})
|
|
774
775
|
"""
|
|
@@ -799,6 +800,7 @@ def get_app(db_path: str) -> FastAPI:
|
|
|
799
800
|
"session_id": row[8],
|
|
800
801
|
"status": row[9],
|
|
801
802
|
"model": row[10],
|
|
803
|
+
"parent_event_id": row[11],
|
|
802
804
|
}
|
|
803
805
|
)
|
|
804
806
|
|
|
@@ -1017,8 +1019,10 @@ def get_app(db_path: str) -> FastAPI:
|
|
|
1017
1019
|
input_summary,
|
|
1018
1020
|
execution_duration_seconds,
|
|
1019
1021
|
status,
|
|
1020
|
-
|
|
1021
|
-
model
|
|
1022
|
+
agent_id,
|
|
1023
|
+
model,
|
|
1024
|
+
context,
|
|
1025
|
+
subagent_type
|
|
1022
1026
|
FROM agent_events
|
|
1023
1027
|
WHERE parent_event_id = ?
|
|
1024
1028
|
ORDER BY timestamp ASC
|
|
@@ -1048,7 +1052,34 @@ def get_app(db_path: str) -> FastAPI:
|
|
|
1048
1052
|
duration = row[4] or 0.0
|
|
1049
1053
|
status = row[5]
|
|
1050
1054
|
agent = row[6] or "unknown"
|
|
1051
|
-
model = row[7]
|
|
1055
|
+
model = row[7]
|
|
1056
|
+
context_json = row[8]
|
|
1057
|
+
subagent_type = row[9]
|
|
1058
|
+
|
|
1059
|
+
# Parse context to extract spawner metadata
|
|
1060
|
+
context = {}
|
|
1061
|
+
spawner_type = None
|
|
1062
|
+
spawned_agent = None
|
|
1063
|
+
if context_json:
|
|
1064
|
+
try:
|
|
1065
|
+
context = json.loads(context_json)
|
|
1066
|
+
spawner_type = context.get("spawner_type")
|
|
1067
|
+
spawned_agent = context.get("spawned_agent")
|
|
1068
|
+
except (json.JSONDecodeError, TypeError):
|
|
1069
|
+
pass
|
|
1070
|
+
|
|
1071
|
+
# If no spawner_type but subagent_type is set, treat it as a spawner delegation
|
|
1072
|
+
# This handles both HeadlessSpawner (spawner_type in context) and
|
|
1073
|
+
# Claude Code plugin agents (subagent_type field)
|
|
1074
|
+
if not spawner_type and subagent_type:
|
|
1075
|
+
# Extract spawner name from subagent_type (e.g., ".claude-plugin:gemini" -> "gemini")
|
|
1076
|
+
if ":" in subagent_type:
|
|
1077
|
+
spawner_type = subagent_type.split(":")[-1]
|
|
1078
|
+
else:
|
|
1079
|
+
spawner_type = subagent_type
|
|
1080
|
+
spawned_agent = (
|
|
1081
|
+
agent # Use the agent_id as the spawned agent
|
|
1082
|
+
)
|
|
1052
1083
|
|
|
1053
1084
|
# Build summary (input_text already contains formatted summary)
|
|
1054
1085
|
summary = input_text[:80] + (
|
|
@@ -1071,9 +1102,17 @@ def get_app(db_path: str) -> FastAPI:
|
|
|
1071
1102
|
"duration_seconds": round(duration, 2),
|
|
1072
1103
|
"agent": agent,
|
|
1073
1104
|
"depth": depth,
|
|
1074
|
-
"model": model,
|
|
1105
|
+
"model": model,
|
|
1075
1106
|
}
|
|
1076
1107
|
|
|
1108
|
+
# Include spawner metadata if present
|
|
1109
|
+
if spawner_type:
|
|
1110
|
+
child_dict["spawner_type"] = spawner_type
|
|
1111
|
+
if spawned_agent:
|
|
1112
|
+
child_dict["spawned_agent"] = spawned_agent
|
|
1113
|
+
if subagent_type:
|
|
1114
|
+
child_dict["subagent_type"] = subagent_type
|
|
1115
|
+
|
|
1077
1116
|
# Only add children key if there are nested children
|
|
1078
1117
|
if nested_children:
|
|
1079
1118
|
child_dict["children"] = nested_children
|
|
@@ -1107,6 +1146,22 @@ def get_app(db_path: str) -> FastAPI:
|
|
|
1107
1146
|
0 if uq_status == "recorded" or uq_status == "success" else 1
|
|
1108
1147
|
) + children_error
|
|
1109
1148
|
|
|
1149
|
+
# Check if any child has spawner metadata
|
|
1150
|
+
def has_spawner_in_children(
|
|
1151
|
+
children_list: list[dict[str, Any]],
|
|
1152
|
+
) -> bool:
|
|
1153
|
+
"""Recursively check if any child has spawner metadata."""
|
|
1154
|
+
for child in children_list:
|
|
1155
|
+
if child.get("spawner_type") or child.get("spawned_agent"):
|
|
1156
|
+
return True
|
|
1157
|
+
if child.get("children") and has_spawner_in_children(
|
|
1158
|
+
child["children"]
|
|
1159
|
+
):
|
|
1160
|
+
return True
|
|
1161
|
+
return False
|
|
1162
|
+
|
|
1163
|
+
has_spawner = has_spawner_in_children(children)
|
|
1164
|
+
|
|
1110
1165
|
# Step 4: Build conversation turn object
|
|
1111
1166
|
conversation_turn = {
|
|
1112
1167
|
"userQuery": {
|
|
@@ -1114,8 +1169,10 @@ def get_app(db_path: str) -> FastAPI:
|
|
|
1114
1169
|
"timestamp": uq_timestamp,
|
|
1115
1170
|
"prompt": prompt_text[:200], # Truncate for display
|
|
1116
1171
|
"duration_seconds": round(uq_duration, 2),
|
|
1172
|
+
"agent_id": uq_row[5], # Include agent_id from UserQuery
|
|
1117
1173
|
},
|
|
1118
1174
|
"children": children,
|
|
1175
|
+
"has_spawner": has_spawner,
|
|
1119
1176
|
"stats": {
|
|
1120
1177
|
"tool_count": len(children),
|
|
1121
1178
|
"total_duration": round(total_duration, 2),
|
|
@@ -804,6 +804,65 @@ function collapseAllConversationTurns() {
|
|
|
804
804
|
});
|
|
805
805
|
}
|
|
806
806
|
|
|
807
|
+
/**
|
|
808
|
+
* Filter conversation turns by agent type
|
|
809
|
+
* @param {string} filterValue - Filter type: 'all', 'direct', 'spawner', or specific spawner type ('gemini', 'codex', 'copilot')
|
|
810
|
+
*/
|
|
811
|
+
function filterByAgentType(filterValue) {
|
|
812
|
+
const conversationTurns = document.querySelectorAll('.conversation-turn');
|
|
813
|
+
|
|
814
|
+
conversationTurns.forEach(turn => {
|
|
815
|
+
const spawnType = turn.getAttribute('data-spawner-type');
|
|
816
|
+
let shouldShow = false;
|
|
817
|
+
|
|
818
|
+
switch (filterValue) {
|
|
819
|
+
case 'all':
|
|
820
|
+
// Show all conversation turns
|
|
821
|
+
shouldShow = true;
|
|
822
|
+
break;
|
|
823
|
+
|
|
824
|
+
case 'direct':
|
|
825
|
+
// Show only direct (non-spawner) turns
|
|
826
|
+
shouldShow = spawnType === 'direct';
|
|
827
|
+
break;
|
|
828
|
+
|
|
829
|
+
case 'spawner':
|
|
830
|
+
// Show only spawner delegation turns
|
|
831
|
+
shouldShow = spawnType === 'spawner';
|
|
832
|
+
break;
|
|
833
|
+
|
|
834
|
+
case 'gemini':
|
|
835
|
+
case 'codex':
|
|
836
|
+
case 'copilot':
|
|
837
|
+
// Show only turns that contain child events with matching spawner type
|
|
838
|
+
shouldShow = hasChildWithSpawnerType(turn, filterValue);
|
|
839
|
+
break;
|
|
840
|
+
|
|
841
|
+
default:
|
|
842
|
+
shouldShow = true;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// Apply visibility
|
|
846
|
+
if (shouldShow) {
|
|
847
|
+
turn.style.display = '';
|
|
848
|
+
} else {
|
|
849
|
+
turn.style.display = 'none';
|
|
850
|
+
}
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
/**
|
|
855
|
+
* Check if a conversation turn has any child events with the specified spawner type
|
|
856
|
+
* @param {HTMLElement} turn - The conversation turn element
|
|
857
|
+
* @param {string} spawnerType - The spawner type to search for ('gemini', 'codex', 'copilot')
|
|
858
|
+
* @returns {boolean} True if the turn contains at least one child with the spawner type
|
|
859
|
+
*/
|
|
860
|
+
function hasChildWithSpawnerType(turn, spawnerType) {
|
|
861
|
+
// Look for spawner badges that match the spawner type
|
|
862
|
+
const spawnerBadges = turn.querySelectorAll(`.spawner-badge.spawner-${spawnerType}`);
|
|
863
|
+
return spawnerBadges.length > 0;
|
|
864
|
+
}
|
|
865
|
+
|
|
807
866
|
// ============================================
|
|
808
867
|
// Live Spawner Event Handling via WebSocket
|
|
809
868
|
// ============================================
|
htmlgraph/cli.py
CHANGED
|
@@ -150,7 +150,7 @@ def cmd_serve(args: argparse.Namespace) -> None:
|
|
|
150
150
|
# Default to database in graph dir if not specified
|
|
151
151
|
db_path = getattr(args, "db", None)
|
|
152
152
|
if not db_path:
|
|
153
|
-
db_path = str(Path(args.graph_dir) / "
|
|
153
|
+
db_path = str(Path(args.graph_dir) / "htmlgraph.db")
|
|
154
154
|
|
|
155
155
|
result = start_fastapi_server(
|
|
156
156
|
port=args.port,
|
htmlgraph/config.py
CHANGED
|
@@ -3,111 +3,188 @@ HtmlGraph Configuration Management.
|
|
|
3
3
|
|
|
4
4
|
This module provides centralized configuration management using Pydantic Settings,
|
|
5
5
|
allowing configuration from environment variables, .env files, and CLI arguments.
|
|
6
|
+
|
|
7
|
+
IMPORTANT: Database path functions (get_database_path, get_analytics_cache_path)
|
|
8
|
+
are lightweight and have NO dependencies. They can be imported anywhere.
|
|
6
9
|
"""
|
|
7
10
|
|
|
11
|
+
import os
|
|
8
12
|
from pathlib import Path
|
|
9
13
|
from typing import Any
|
|
10
14
|
|
|
11
|
-
|
|
15
|
+
# =============================================================================
|
|
16
|
+
# LIGHTWEIGHT DATABASE PATH FUNCTIONS (NO DEPENDENCIES)
|
|
17
|
+
# These MUST come before any heavy imports so spawners can use them
|
|
18
|
+
# =============================================================================
|
|
12
19
|
|
|
20
|
+
# Database filenames (SINGLE SOURCE OF TRUTH)
|
|
21
|
+
DATABASE_FILENAME = "htmlgraph.db" # Unified event database
|
|
22
|
+
ANALYTICS_CACHE_FILENAME = "index.sqlite" # Analytics cache (rebuildable)
|
|
13
23
|
|
|
14
|
-
class HtmlGraphConfig(BaseSettings):
|
|
15
|
-
"""Global HtmlGraph configuration using Pydantic Settings.
|
|
16
24
|
|
|
17
|
-
|
|
18
|
-
1. Environment variables (prefix: HTMLGRAPH_)
|
|
19
|
-
2. .env file
|
|
20
|
-
3. Direct instantiation with parameters
|
|
21
|
-
4. CLI argument overrides
|
|
25
|
+
def get_database_path(project_root: Path | str | None = None) -> Path:
|
|
22
26
|
"""
|
|
27
|
+
Get the unified database path for event tracking.
|
|
28
|
+
|
|
29
|
+
This is the SINGLE source of truth for database location.
|
|
30
|
+
All hooks, agents, and spawners MUST use this function.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
project_root: Optional project root path. If None, uses HTMLGRAPH_PROJECT_ROOT
|
|
34
|
+
env var or current working directory.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Path to htmlgraph.db (the unified event database)
|
|
38
|
+
"""
|
|
39
|
+
if project_root is None:
|
|
40
|
+
project_root = Path(os.environ.get("HTMLGRAPH_PROJECT_ROOT", os.getcwd()))
|
|
41
|
+
else:
|
|
42
|
+
project_root = Path(project_root)
|
|
43
|
+
|
|
44
|
+
return project_root / ".htmlgraph" / DATABASE_FILENAME
|
|
23
45
|
|
|
24
|
-
# Core paths
|
|
25
|
-
graph_dir: Path = Path.home() / ".htmlgraph"
|
|
26
|
-
|
|
27
|
-
# Feature tracking
|
|
28
|
-
features_dir: Path | None = None
|
|
29
|
-
sessions_dir: Path | None = None
|
|
30
|
-
spikes_dir: Path | None = None
|
|
31
|
-
tracks_dir: Path | None = None
|
|
32
|
-
archives_dir: Path | None = None
|
|
33
|
-
|
|
34
|
-
# CLI behavior
|
|
35
|
-
debug: bool = False
|
|
36
|
-
verbose: bool = False
|
|
37
|
-
auto_sync: bool = True
|
|
38
|
-
color_output: bool = True
|
|
39
|
-
|
|
40
|
-
# Session management
|
|
41
|
-
max_sessions: int = 100
|
|
42
|
-
session_retention_days: int = 30
|
|
43
|
-
auto_archive_sessions: bool = True
|
|
44
|
-
|
|
45
|
-
# Performance
|
|
46
|
-
max_query_results: int = 1000
|
|
47
|
-
cache_enabled: bool = True
|
|
48
|
-
cache_ttl_seconds: int = 3600
|
|
49
|
-
|
|
50
|
-
# Logging
|
|
51
|
-
log_level: str = "INFO"
|
|
52
|
-
log_file: Path | None = None
|
|
53
|
-
|
|
54
|
-
model_config = {
|
|
55
|
-
"env_prefix": "HTMLGRAPH_",
|
|
56
|
-
"env_file": ".env",
|
|
57
|
-
"case_sensitive": False,
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
def __init__(self, **data: Any) -> None:
|
|
61
|
-
"""Initialize config and compute derived paths."""
|
|
62
|
-
super().__init__(**data)
|
|
63
|
-
# Compute derived paths if not explicitly set
|
|
64
|
-
if self.features_dir is None:
|
|
65
|
-
self.features_dir = self.graph_dir / "features"
|
|
66
|
-
if self.sessions_dir is None:
|
|
67
|
-
self.sessions_dir = self.graph_dir / "sessions"
|
|
68
|
-
if self.spikes_dir is None:
|
|
69
|
-
self.spikes_dir = self.graph_dir / "spikes"
|
|
70
|
-
if self.tracks_dir is None:
|
|
71
|
-
self.tracks_dir = self.graph_dir / "tracks"
|
|
72
|
-
if self.archives_dir is None:
|
|
73
|
-
self.archives_dir = self.graph_dir / "archives"
|
|
74
|
-
|
|
75
|
-
def ensure_directories(self) -> None:
|
|
76
|
-
"""Create all configured directories if they don't exist."""
|
|
77
|
-
for directory in [
|
|
78
|
-
self.graph_dir,
|
|
79
|
-
self.features_dir,
|
|
80
|
-
self.sessions_dir,
|
|
81
|
-
self.spikes_dir,
|
|
82
|
-
self.tracks_dir,
|
|
83
|
-
self.archives_dir,
|
|
84
|
-
]:
|
|
85
|
-
if directory:
|
|
86
|
-
directory.mkdir(parents=True, exist_ok=True)
|
|
87
|
-
|
|
88
|
-
def get_config_dict(self) -> dict[str, Any]:
|
|
89
|
-
"""Get configuration as dictionary."""
|
|
90
|
-
return {
|
|
91
|
-
"graph_dir": str(self.graph_dir),
|
|
92
|
-
"features_dir": str(self.features_dir),
|
|
93
|
-
"sessions_dir": str(self.sessions_dir),
|
|
94
|
-
"spikes_dir": str(self.spikes_dir),
|
|
95
|
-
"tracks_dir": str(self.tracks_dir),
|
|
96
|
-
"archives_dir": str(self.archives_dir),
|
|
97
|
-
"debug": self.debug,
|
|
98
|
-
"verbose": self.verbose,
|
|
99
|
-
"auto_sync": self.auto_sync,
|
|
100
|
-
"color_output": self.color_output,
|
|
101
|
-
"max_sessions": self.max_sessions,
|
|
102
|
-
"session_retention_days": self.session_retention_days,
|
|
103
|
-
"auto_archive_sessions": self.auto_archive_sessions,
|
|
104
|
-
"max_query_results": self.max_query_results,
|
|
105
|
-
"cache_enabled": self.cache_enabled,
|
|
106
|
-
"cache_ttl_seconds": self.cache_ttl_seconds,
|
|
107
|
-
"log_level": self.log_level,
|
|
108
|
-
"log_file": str(self.log_file) if self.log_file else None,
|
|
109
|
-
}
|
|
110
46
|
|
|
47
|
+
def get_analytics_cache_path(project_root: Path | str | None = None) -> Path:
|
|
48
|
+
"""
|
|
49
|
+
Get the analytics cache database path.
|
|
50
|
+
|
|
51
|
+
This is for read-only analytics queries (rebuildable from events).
|
|
52
|
+
NOT for event tracking - use get_database_path() for that.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
project_root: Optional project root path. If None, uses HTMLGRAPH_PROJECT_ROOT
|
|
56
|
+
env var or current working directory.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Path to index.sqlite (analytics cache, gitignored)
|
|
60
|
+
"""
|
|
61
|
+
if project_root is None:
|
|
62
|
+
project_root = Path(os.environ.get("HTMLGRAPH_PROJECT_ROOT", os.getcwd()))
|
|
63
|
+
else:
|
|
64
|
+
project_root = Path(project_root)
|
|
65
|
+
|
|
66
|
+
return project_root / ".htmlgraph" / ANALYTICS_CACHE_FILENAME
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# =============================================================================
|
|
70
|
+
# PYDANTIC CONFIGURATION (Heavy imports below - spawners don't need this)
|
|
71
|
+
# =============================================================================
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
from pydantic_settings import BaseSettings
|
|
75
|
+
|
|
76
|
+
_PYDANTIC_AVAILABLE = True
|
|
77
|
+
except ImportError:
|
|
78
|
+
_PYDANTIC_AVAILABLE = False
|
|
79
|
+
BaseSettings = object # type: ignore
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
if _PYDANTIC_AVAILABLE:
|
|
83
|
+
|
|
84
|
+
class HtmlGraphConfig(BaseSettings):
|
|
85
|
+
"""Global HtmlGraph configuration using Pydantic Settings.
|
|
86
|
+
|
|
87
|
+
Configuration can be provided via:
|
|
88
|
+
1. Environment variables (prefix: HTMLGRAPH_)
|
|
89
|
+
2. .env file
|
|
90
|
+
3. Direct instantiation with parameters
|
|
91
|
+
4. CLI argument overrides
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
# Core paths
|
|
95
|
+
graph_dir: Path = Path.home() / ".htmlgraph"
|
|
96
|
+
|
|
97
|
+
# Database paths (SINGLE SOURCE OF TRUTH)
|
|
98
|
+
# All hooks, agents, and spawners MUST use these via get_database_path()
|
|
99
|
+
database_filename: str = "htmlgraph.db" # Unified event database
|
|
100
|
+
analytics_cache_filename: str = "index.sqlite" # Analytics cache (rebuildable)
|
|
101
|
+
|
|
102
|
+
# Feature tracking
|
|
103
|
+
features_dir: Path | None = None
|
|
104
|
+
sessions_dir: Path | None = None
|
|
105
|
+
spikes_dir: Path | None = None
|
|
106
|
+
tracks_dir: Path | None = None
|
|
107
|
+
archives_dir: Path | None = None
|
|
108
|
+
|
|
109
|
+
# CLI behavior
|
|
110
|
+
debug: bool = False
|
|
111
|
+
verbose: bool = False
|
|
112
|
+
auto_sync: bool = True
|
|
113
|
+
color_output: bool = True
|
|
114
|
+
|
|
115
|
+
# Session management
|
|
116
|
+
max_sessions: int = 100
|
|
117
|
+
session_retention_days: int = 30
|
|
118
|
+
auto_archive_sessions: bool = True
|
|
119
|
+
|
|
120
|
+
# Performance
|
|
121
|
+
max_query_results: int = 1000
|
|
122
|
+
cache_enabled: bool = True
|
|
123
|
+
cache_ttl_seconds: int = 3600
|
|
124
|
+
|
|
125
|
+
# Logging
|
|
126
|
+
log_level: str = "INFO"
|
|
127
|
+
log_file: Path | None = None
|
|
128
|
+
|
|
129
|
+
model_config = {
|
|
130
|
+
"env_prefix": "HTMLGRAPH_",
|
|
131
|
+
"env_file": ".env",
|
|
132
|
+
"case_sensitive": False,
|
|
133
|
+
}
|
|
111
134
|
|
|
112
|
-
|
|
113
|
-
config
|
|
135
|
+
def __init__(self, **data: Any) -> None:
|
|
136
|
+
"""Initialize config and compute derived paths."""
|
|
137
|
+
super().__init__(**data)
|
|
138
|
+
# Compute derived paths if not explicitly set
|
|
139
|
+
if self.features_dir is None:
|
|
140
|
+
self.features_dir = self.graph_dir / "features"
|
|
141
|
+
if self.sessions_dir is None:
|
|
142
|
+
self.sessions_dir = self.graph_dir / "sessions"
|
|
143
|
+
if self.spikes_dir is None:
|
|
144
|
+
self.spikes_dir = self.graph_dir / "spikes"
|
|
145
|
+
if self.tracks_dir is None:
|
|
146
|
+
self.tracks_dir = self.graph_dir / "tracks"
|
|
147
|
+
if self.archives_dir is None:
|
|
148
|
+
self.archives_dir = self.graph_dir / "archives"
|
|
149
|
+
|
|
150
|
+
def ensure_directories(self) -> None:
|
|
151
|
+
"""Create all configured directories if they don't exist."""
|
|
152
|
+
for directory in [
|
|
153
|
+
self.graph_dir,
|
|
154
|
+
self.features_dir,
|
|
155
|
+
self.sessions_dir,
|
|
156
|
+
self.spikes_dir,
|
|
157
|
+
self.tracks_dir,
|
|
158
|
+
self.archives_dir,
|
|
159
|
+
]:
|
|
160
|
+
if directory:
|
|
161
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
162
|
+
|
|
163
|
+
def get_config_dict(self) -> dict[str, Any]:
|
|
164
|
+
"""Get configuration as dictionary."""
|
|
165
|
+
return {
|
|
166
|
+
"graph_dir": str(self.graph_dir),
|
|
167
|
+
"features_dir": str(self.features_dir),
|
|
168
|
+
"sessions_dir": str(self.sessions_dir),
|
|
169
|
+
"spikes_dir": str(self.spikes_dir),
|
|
170
|
+
"tracks_dir": str(self.tracks_dir),
|
|
171
|
+
"archives_dir": str(self.archives_dir),
|
|
172
|
+
"debug": self.debug,
|
|
173
|
+
"verbose": self.verbose,
|
|
174
|
+
"auto_sync": self.auto_sync,
|
|
175
|
+
"color_output": self.color_output,
|
|
176
|
+
"max_sessions": self.max_sessions,
|
|
177
|
+
"session_retention_days": self.session_retention_days,
|
|
178
|
+
"auto_archive_sessions": self.auto_archive_sessions,
|
|
179
|
+
"max_query_results": self.max_query_results,
|
|
180
|
+
"cache_enabled": self.cache_enabled,
|
|
181
|
+
"cache_ttl_seconds": self.cache_ttl_seconds,
|
|
182
|
+
"log_level": self.log_level,
|
|
183
|
+
"log_file": str(self.log_file) if self.log_file else None,
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
# Global configuration instance
|
|
187
|
+
config: HtmlGraphConfig = HtmlGraphConfig()
|
|
188
|
+
else:
|
|
189
|
+
# Pydantic not available - config object won't work but database functions will
|
|
190
|
+
config = None # type: ignore
|