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.
- htmlgraph/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/.htmlgraph/agents.json +72 -0
- htmlgraph/.htmlgraph/htmlgraph.db +0 -0
- htmlgraph/__init__.py +51 -1
- htmlgraph/__init__.pyi +123 -0
- htmlgraph/agent_detection.py +26 -10
- htmlgraph/agent_registry.py +2 -1
- htmlgraph/analytics/__init__.py +8 -1
- htmlgraph/analytics/cli.py +86 -20
- htmlgraph/analytics/cost_analyzer.py +391 -0
- htmlgraph/analytics/cost_monitor.py +664 -0
- htmlgraph/analytics/cost_reporter.py +675 -0
- htmlgraph/analytics/cross_session.py +617 -0
- htmlgraph/analytics/dependency.py +10 -6
- htmlgraph/analytics/pattern_learning.py +771 -0
- htmlgraph/analytics/session_graph.py +707 -0
- htmlgraph/analytics/strategic/__init__.py +80 -0
- htmlgraph/analytics/strategic/cost_optimizer.py +611 -0
- htmlgraph/analytics/strategic/pattern_detector.py +876 -0
- htmlgraph/analytics/strategic/preference_manager.py +709 -0
- htmlgraph/analytics/strategic/suggestion_engine.py +747 -0
- htmlgraph/analytics/work_type.py +67 -27
- htmlgraph/analytics_index.py +53 -20
- htmlgraph/api/__init__.py +3 -0
- htmlgraph/api/cost_alerts_websocket.py +416 -0
- htmlgraph/api/main.py +2498 -0
- htmlgraph/api/static/htmx.min.js +1 -0
- htmlgraph/api/static/style-redesign.css +1344 -0
- htmlgraph/api/static/style.css +1079 -0
- htmlgraph/api/templates/dashboard-redesign.html +1366 -0
- htmlgraph/api/templates/dashboard.html +794 -0
- htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
- htmlgraph/api/templates/partials/activity-feed.html +1100 -0
- htmlgraph/api/templates/partials/agents-redesign.html +317 -0
- htmlgraph/api/templates/partials/agents.html +317 -0
- htmlgraph/api/templates/partials/event-traces.html +373 -0
- htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
- htmlgraph/api/templates/partials/features.html +578 -0
- htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
- htmlgraph/api/templates/partials/metrics.html +346 -0
- htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
- htmlgraph/api/templates/partials/orchestration.html +198 -0
- htmlgraph/api/templates/partials/spawners.html +375 -0
- htmlgraph/api/templates/partials/work-items.html +613 -0
- htmlgraph/api/websocket.py +538 -0
- htmlgraph/archive/__init__.py +24 -0
- htmlgraph/archive/bloom.py +234 -0
- htmlgraph/archive/fts.py +297 -0
- htmlgraph/archive/manager.py +583 -0
- htmlgraph/archive/search.py +244 -0
- htmlgraph/atomic_ops.py +560 -0
- htmlgraph/attribute_index.py +2 -1
- htmlgraph/bounded_paths.py +539 -0
- htmlgraph/builders/base.py +57 -2
- htmlgraph/builders/bug.py +19 -3
- htmlgraph/builders/chore.py +19 -3
- htmlgraph/builders/epic.py +19 -3
- htmlgraph/builders/feature.py +27 -3
- htmlgraph/builders/insight.py +2 -1
- htmlgraph/builders/metric.py +2 -1
- htmlgraph/builders/pattern.py +2 -1
- htmlgraph/builders/phase.py +19 -3
- htmlgraph/builders/spike.py +29 -3
- htmlgraph/builders/track.py +42 -1
- htmlgraph/cigs/__init__.py +81 -0
- htmlgraph/cigs/autonomy.py +385 -0
- htmlgraph/cigs/cost.py +475 -0
- htmlgraph/cigs/messages_basic.py +472 -0
- htmlgraph/cigs/messaging.py +365 -0
- htmlgraph/cigs/models.py +771 -0
- htmlgraph/cigs/pattern_storage.py +427 -0
- htmlgraph/cigs/patterns.py +503 -0
- htmlgraph/cigs/posttool_analyzer.py +234 -0
- htmlgraph/cigs/reporter.py +818 -0
- htmlgraph/cigs/tracker.py +317 -0
- htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/cli/.htmlgraph/agents.json +72 -0
- htmlgraph/cli/.htmlgraph/htmlgraph.db +0 -0
- htmlgraph/cli/__init__.py +42 -0
- htmlgraph/cli/__main__.py +6 -0
- htmlgraph/cli/analytics.py +1424 -0
- htmlgraph/cli/base.py +685 -0
- htmlgraph/cli/constants.py +206 -0
- htmlgraph/cli/core.py +954 -0
- htmlgraph/cli/main.py +147 -0
- htmlgraph/cli/models.py +475 -0
- htmlgraph/cli/templates/__init__.py +1 -0
- htmlgraph/cli/templates/cost_dashboard.py +399 -0
- htmlgraph/cli/work/__init__.py +239 -0
- htmlgraph/cli/work/browse.py +115 -0
- htmlgraph/cli/work/features.py +568 -0
- htmlgraph/cli/work/orchestration.py +676 -0
- htmlgraph/cli/work/report.py +728 -0
- htmlgraph/cli/work/sessions.py +466 -0
- htmlgraph/cli/work/snapshot.py +559 -0
- htmlgraph/cli/work/tracks.py +486 -0
- htmlgraph/cli_commands/__init__.py +1 -0
- htmlgraph/cli_commands/feature.py +195 -0
- htmlgraph/cli_framework.py +115 -0
- htmlgraph/collections/__init__.py +2 -0
- htmlgraph/collections/base.py +197 -14
- htmlgraph/collections/bug.py +2 -1
- htmlgraph/collections/chore.py +2 -1
- htmlgraph/collections/epic.py +2 -1
- htmlgraph/collections/feature.py +2 -1
- htmlgraph/collections/insight.py +2 -1
- htmlgraph/collections/metric.py +2 -1
- htmlgraph/collections/pattern.py +2 -1
- htmlgraph/collections/phase.py +2 -1
- htmlgraph/collections/session.py +194 -0
- htmlgraph/collections/spike.py +13 -2
- htmlgraph/collections/task_delegation.py +241 -0
- htmlgraph/collections/todo.py +14 -1
- htmlgraph/collections/traces.py +487 -0
- htmlgraph/config/cost_models.json +56 -0
- htmlgraph/config.py +190 -0
- htmlgraph/context_analytics.py +2 -1
- htmlgraph/converter.py +116 -7
- htmlgraph/cost_analysis/__init__.py +5 -0
- htmlgraph/cost_analysis/analyzer.py +438 -0
- htmlgraph/dashboard.html +2246 -248
- htmlgraph/dashboard.html.backup +6592 -0
- htmlgraph/dashboard.html.bak +7181 -0
- htmlgraph/dashboard.html.bak2 +7231 -0
- htmlgraph/dashboard.html.bak3 +7232 -0
- htmlgraph/db/__init__.py +38 -0
- htmlgraph/db/queries.py +790 -0
- htmlgraph/db/schema.py +1788 -0
- htmlgraph/decorators.py +317 -0
- htmlgraph/dependency_models.py +2 -1
- htmlgraph/deploy.py +26 -27
- htmlgraph/docs/API_REFERENCE.md +841 -0
- htmlgraph/docs/HTTP_API.md +750 -0
- htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
- htmlgraph/docs/ORCHESTRATION_PATTERNS.md +717 -0
- htmlgraph/docs/README.md +532 -0
- htmlgraph/docs/__init__.py +77 -0
- htmlgraph/docs/docs_version.py +55 -0
- htmlgraph/docs/metadata.py +93 -0
- htmlgraph/docs/migrations.py +232 -0
- htmlgraph/docs/template_engine.py +143 -0
- htmlgraph/docs/templates/_sections/cli_reference.md.j2 +52 -0
- htmlgraph/docs/templates/_sections/core_concepts.md.j2 +29 -0
- htmlgraph/docs/templates/_sections/sdk_basics.md.j2 +69 -0
- htmlgraph/docs/templates/base_agents.md.j2 +78 -0
- htmlgraph/docs/templates/example_user_override.md.j2 +47 -0
- htmlgraph/docs/version_check.py +163 -0
- htmlgraph/edge_index.py +2 -1
- htmlgraph/error_handler.py +544 -0
- htmlgraph/event_log.py +86 -37
- htmlgraph/event_migration.py +2 -1
- htmlgraph/file_watcher.py +12 -8
- htmlgraph/find_api.py +2 -1
- htmlgraph/git_events.py +67 -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/__init__.py +8 -0
- htmlgraph/hooks/bootstrap.py +169 -0
- htmlgraph/hooks/cigs_pretool_enforcer.py +354 -0
- htmlgraph/hooks/concurrent_sessions.py +208 -0
- htmlgraph/hooks/context.py +350 -0
- htmlgraph/hooks/drift_handler.py +525 -0
- htmlgraph/hooks/event_tracker.py +790 -99
- htmlgraph/hooks/git_commands.py +175 -0
- htmlgraph/hooks/installer.py +5 -1
- htmlgraph/hooks/orchestrator.py +327 -76
- htmlgraph/hooks/orchestrator_reflector.py +31 -4
- htmlgraph/hooks/post_tool_use_failure.py +32 -7
- htmlgraph/hooks/post_tool_use_handler.py +257 -0
- htmlgraph/hooks/posttooluse.py +92 -19
- htmlgraph/hooks/pretooluse.py +527 -7
- htmlgraph/hooks/prompt_analyzer.py +637 -0
- htmlgraph/hooks/session_handler.py +668 -0
- htmlgraph/hooks/session_summary.py +395 -0
- htmlgraph/hooks/state_manager.py +504 -0
- htmlgraph/hooks/subagent_detection.py +202 -0
- htmlgraph/hooks/subagent_stop.py +369 -0
- htmlgraph/hooks/task_enforcer.py +99 -4
- htmlgraph/hooks/validator.py +212 -91
- htmlgraph/ids.py +2 -1
- htmlgraph/learning.py +125 -100
- htmlgraph/mcp_server.py +2 -1
- htmlgraph/models.py +217 -18
- htmlgraph/operations/README.md +62 -0
- htmlgraph/operations/__init__.py +79 -0
- htmlgraph/operations/analytics.py +339 -0
- htmlgraph/operations/bootstrap.py +289 -0
- htmlgraph/operations/events.py +244 -0
- htmlgraph/operations/fastapi_server.py +231 -0
- htmlgraph/operations/hooks.py +350 -0
- htmlgraph/operations/initialization.py +597 -0
- htmlgraph/operations/initialization.py.backup +228 -0
- htmlgraph/operations/server.py +303 -0
- htmlgraph/orchestration/__init__.py +58 -0
- htmlgraph/orchestration/claude_launcher.py +179 -0
- htmlgraph/orchestration/command_builder.py +72 -0
- htmlgraph/orchestration/headless_spawner.py +281 -0
- htmlgraph/orchestration/live_events.py +377 -0
- htmlgraph/orchestration/model_selection.py +327 -0
- htmlgraph/orchestration/plugin_manager.py +140 -0
- htmlgraph/orchestration/prompts.py +137 -0
- htmlgraph/orchestration/spawner_event_tracker.py +383 -0
- htmlgraph/orchestration/spawners/__init__.py +16 -0
- htmlgraph/orchestration/spawners/base.py +194 -0
- htmlgraph/orchestration/spawners/claude.py +173 -0
- htmlgraph/orchestration/spawners/codex.py +435 -0
- htmlgraph/orchestration/spawners/copilot.py +294 -0
- htmlgraph/orchestration/spawners/gemini.py +471 -0
- htmlgraph/orchestration/subprocess_runner.py +36 -0
- htmlgraph/{orchestration.py → orchestration/task_coordination.py} +16 -8
- htmlgraph/orchestration.md +563 -0
- htmlgraph/orchestrator-system-prompt-optimized.txt +863 -0
- htmlgraph/orchestrator.py +2 -1
- htmlgraph/orchestrator_config.py +357 -0
- htmlgraph/orchestrator_mode.py +115 -4
- htmlgraph/parallel.py +2 -1
- htmlgraph/parser.py +86 -6
- htmlgraph/path_query.py +608 -0
- htmlgraph/pattern_matcher.py +636 -0
- htmlgraph/pydantic_models.py +476 -0
- htmlgraph/quality_gates.py +350 -0
- htmlgraph/query_builder.py +2 -1
- htmlgraph/query_composer.py +509 -0
- htmlgraph/reflection.py +443 -0
- htmlgraph/refs.py +344 -0
- htmlgraph/repo_hash.py +512 -0
- htmlgraph/repositories/__init__.py +292 -0
- htmlgraph/repositories/analytics_repository.py +455 -0
- htmlgraph/repositories/analytics_repository_standard.py +628 -0
- htmlgraph/repositories/feature_repository.py +581 -0
- htmlgraph/repositories/feature_repository_htmlfile.py +668 -0
- htmlgraph/repositories/feature_repository_memory.py +607 -0
- htmlgraph/repositories/feature_repository_sqlite.py +858 -0
- htmlgraph/repositories/filter_service.py +620 -0
- htmlgraph/repositories/filter_service_standard.py +445 -0
- htmlgraph/repositories/shared_cache.py +621 -0
- htmlgraph/repositories/shared_cache_memory.py +395 -0
- htmlgraph/repositories/track_repository.py +552 -0
- htmlgraph/repositories/track_repository_htmlfile.py +619 -0
- htmlgraph/repositories/track_repository_memory.py +508 -0
- htmlgraph/repositories/track_repository_sqlite.py +711 -0
- htmlgraph/sdk/__init__.py +398 -0
- htmlgraph/sdk/__init__.pyi +14 -0
- htmlgraph/sdk/analytics/__init__.py +19 -0
- htmlgraph/sdk/analytics/engine.py +155 -0
- htmlgraph/sdk/analytics/helpers.py +178 -0
- htmlgraph/sdk/analytics/registry.py +109 -0
- htmlgraph/sdk/base.py +484 -0
- htmlgraph/sdk/constants.py +216 -0
- htmlgraph/sdk/core.pyi +308 -0
- htmlgraph/sdk/discovery.py +120 -0
- htmlgraph/sdk/help/__init__.py +12 -0
- htmlgraph/sdk/help/mixin.py +699 -0
- htmlgraph/sdk/mixins/__init__.py +15 -0
- htmlgraph/sdk/mixins/attribution.py +113 -0
- htmlgraph/sdk/mixins/mixin.py +410 -0
- htmlgraph/sdk/operations/__init__.py +12 -0
- htmlgraph/sdk/operations/mixin.py +427 -0
- htmlgraph/sdk/orchestration/__init__.py +17 -0
- htmlgraph/sdk/orchestration/coordinator.py +203 -0
- htmlgraph/sdk/orchestration/spawner.py +204 -0
- htmlgraph/sdk/planning/__init__.py +19 -0
- htmlgraph/sdk/planning/bottlenecks.py +93 -0
- htmlgraph/sdk/planning/mixin.py +211 -0
- htmlgraph/sdk/planning/parallel.py +186 -0
- htmlgraph/sdk/planning/queue.py +210 -0
- htmlgraph/sdk/planning/recommendations.py +87 -0
- htmlgraph/sdk/planning/smart_planning.py +319 -0
- htmlgraph/sdk/session/__init__.py +19 -0
- htmlgraph/sdk/session/continuity.py +57 -0
- htmlgraph/sdk/session/handoff.py +110 -0
- htmlgraph/sdk/session/info.py +309 -0
- htmlgraph/sdk/session/manager.py +103 -0
- htmlgraph/sdk/strategic/__init__.py +26 -0
- htmlgraph/sdk/strategic/mixin.py +563 -0
- htmlgraph/server.py +295 -107
- htmlgraph/session_hooks.py +300 -0
- htmlgraph/session_manager.py +285 -3
- htmlgraph/session_registry.py +587 -0
- htmlgraph/session_state.py +436 -0
- htmlgraph/session_warning.py +2 -1
- htmlgraph/sessions/__init__.py +23 -0
- htmlgraph/sessions/handoff.py +756 -0
- htmlgraph/system_prompts.py +450 -0
- htmlgraph/templates/orchestration-view.html +350 -0
- htmlgraph/track_builder.py +33 -1
- htmlgraph/track_manager.py +38 -0
- htmlgraph/transcript.py +18 -5
- htmlgraph/validation.py +115 -0
- htmlgraph/watch.py +2 -1
- htmlgraph/work_type_utils.py +2 -1
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/dashboard.html +2246 -248
- {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/METADATA +95 -64
- htmlgraph-0.27.5.dist-info/RECORD +337 -0
- {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/entry_points.txt +1 -1
- htmlgraph/cli.py +0 -4839
- htmlgraph/sdk.py +0 -2359
- htmlgraph-0.20.1.dist-info/RECORD +0 -118
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
- {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
|
+
)
|