htmlgraph 0.22.0__py3-none-any.whl → 0.23.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 (29) hide show
  1. htmlgraph/__init__.py +1 -1
  2. htmlgraph/agent_detection.py +41 -2
  3. htmlgraph/analytics/cli.py +86 -20
  4. htmlgraph/cli.py +280 -87
  5. htmlgraph/collections/base.py +68 -4
  6. htmlgraph/git_events.py +61 -7
  7. htmlgraph/operations/README.md +62 -0
  8. htmlgraph/operations/__init__.py +61 -0
  9. htmlgraph/operations/analytics.py +338 -0
  10. htmlgraph/operations/events.py +243 -0
  11. htmlgraph/operations/hooks.py +349 -0
  12. htmlgraph/operations/server.py +302 -0
  13. htmlgraph/orchestration/__init__.py +39 -0
  14. htmlgraph/orchestration/headless_spawner.py +566 -0
  15. htmlgraph/orchestration/model_selection.py +323 -0
  16. htmlgraph/orchestrator-system-prompt-optimized.txt +92 -0
  17. htmlgraph/parser.py +56 -1
  18. htmlgraph/sdk.py +529 -7
  19. htmlgraph/server.py +153 -60
  20. {htmlgraph-0.22.0.dist-info → htmlgraph-0.23.1.dist-info}/METADATA +3 -1
  21. {htmlgraph-0.22.0.dist-info → htmlgraph-0.23.1.dist-info}/RECORD +29 -19
  22. /htmlgraph/{orchestration.py → orchestration/task_coordination.py} +0 -0
  23. {htmlgraph-0.22.0.data → htmlgraph-0.23.1.data}/data/htmlgraph/dashboard.html +0 -0
  24. {htmlgraph-0.22.0.data → htmlgraph-0.23.1.data}/data/htmlgraph/styles.css +0 -0
  25. {htmlgraph-0.22.0.data → htmlgraph-0.23.1.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
  26. {htmlgraph-0.22.0.data → htmlgraph-0.23.1.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
  27. {htmlgraph-0.22.0.data → htmlgraph-0.23.1.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
  28. {htmlgraph-0.22.0.dist-info → htmlgraph-0.23.1.dist-info}/WHEEL +0 -0
  29. {htmlgraph-0.22.0.dist-info → htmlgraph-0.23.1.dist-info}/entry_points.txt +0 -0
htmlgraph/__init__.py CHANGED
@@ -84,7 +84,7 @@ from htmlgraph.types import (
84
84
  )
85
85
  from htmlgraph.work_type_utils import infer_work_type, infer_work_type_from_id
86
86
 
87
- __version__ = "0.22.0"
87
+ __version__ = "0.23.1"
88
88
  __all__ = [
89
89
  # Exceptions
90
90
  "HtmlGraphError",
@@ -20,7 +20,8 @@ def detect_agent_name() -> str:
20
20
  1. HTMLGRAPH_AGENT environment variable (explicit override)
21
21
  2. Claude Code detection (CLAUDE_CODE_VERSION, parent process)
22
22
  3. Gemini detection (GEMINI environment markers)
23
- 4. Fall back to "cli"
23
+ 4. OpenCode detection (OPENCODE environment markers)
24
+ 5. Fall back to "cli"
24
25
  """
25
26
  # 1. Explicit override
26
27
  explicit = os.environ.get("HTMLGRAPH_AGENT")
@@ -35,7 +36,11 @@ def detect_agent_name() -> str:
35
36
  if _is_gemini():
36
37
  return "gemini"
37
38
 
38
- # 4. Default to CLI
39
+ # 4. OpenCode detection
40
+ if _is_opencode():
41
+ return "opencode"
42
+
43
+ # 5. Default to CLI
39
44
  return "cli"
40
45
 
41
46
 
@@ -88,6 +93,39 @@ def _is_gemini() -> bool:
88
93
  return False
89
94
 
90
95
 
96
+ def _is_opencode() -> bool:
97
+ """Check if running in OpenCode environment."""
98
+ # Check for OpenCode-specific environment variables
99
+ if os.environ.get("OPENCODE_VERSION"):
100
+ return True
101
+
102
+ if os.environ.get("OPENCODE_API_KEY"):
103
+ return True
104
+
105
+ if os.environ.get("OPENCODE_SESSION_ID"):
106
+ return True
107
+
108
+ # Check for opencode in command line args
109
+ if any("opencode" in arg.lower() for arg in sys.argv):
110
+ return True
111
+
112
+ # Check for OpenCode configuration
113
+ try:
114
+ # Look for OpenCode config in common locations
115
+ opencode_config = Path.home() / ".opencode"
116
+ if opencode_config.exists():
117
+ return True
118
+
119
+ # Check project-level OpenCode config
120
+ project_config = Path.cwd() / ".opencode"
121
+ if project_config.exists():
122
+ return True
123
+ except Exception:
124
+ pass
125
+
126
+ return False
127
+
128
+
91
129
  def get_agent_display_name(agent: str) -> str:
92
130
  """
93
131
  Get a human-friendly display name for an agent.
@@ -102,6 +140,7 @@ def get_agent_display_name(agent: str) -> str:
102
140
  "claude": "Claude",
103
141
  "claude-code": "Claude",
104
142
  "gemini": "Gemini",
143
+ "opencode": "OpenCode",
105
144
  "cli": "CLI",
106
145
  "haiku": "Haiku",
107
146
  "opus": "Opus",
@@ -5,6 +5,8 @@ This module provides the `htmlgraph analytics` command for analyzing work patter
5
5
  """
6
6
 
7
7
  import argparse
8
+ from collections.abc import Iterator
9
+ from contextlib import AbstractContextManager, nullcontext
8
10
  from pathlib import Path
9
11
 
10
12
  from rich import box
@@ -19,6 +21,7 @@ from htmlgraph.converter import html_to_session
19
21
  def cmd_analytics(args: argparse.Namespace) -> int:
20
22
  """Display work type analytics with beautiful rich formatting."""
21
23
  console = Console()
24
+ quiet = getattr(args, "quiet", False)
22
25
 
23
26
  try:
24
27
  sdk = SDK(agent=args.agent or "cli")
@@ -46,21 +49,67 @@ def cmd_analytics(args: argparse.Namespace) -> int:
46
49
  # Determine scope
47
50
  if args.session_id:
48
51
  # Single session analysis
49
- _display_session_analytics(console, sdk, args.session_id, args.graph_dir)
52
+ _display_session_analytics(
53
+ console, sdk, args.session_id, args.graph_dir, quiet=quiet
54
+ )
50
55
  elif args.recent:
51
56
  # Recent sessions
52
57
  _display_recent_sessions(
53
- console, sdk, session_files[: args.recent], args.graph_dir
58
+ console, sdk, session_files[: args.recent], args.graph_dir, quiet=quiet
54
59
  )
55
60
  else:
56
61
  # Project-wide overview
57
- _display_project_analytics(console, sdk, session_files, args.graph_dir)
62
+ _display_project_analytics(
63
+ console, sdk, session_files, args.graph_dir, quiet=quiet
64
+ )
58
65
 
59
66
  return 0
60
67
 
61
68
 
69
+ def _status_context(
70
+ console: Console, quiet: bool, message: str
71
+ ) -> AbstractContextManager[object]:
72
+ if quiet:
73
+ return nullcontext()
74
+ return console.status(message)
75
+
76
+
77
+ def _iter_with_progress(
78
+ console: Console, quiet: bool, items: list[Path], description: str
79
+ ) -> Iterator[Path]:
80
+ if quiet:
81
+ for item in items:
82
+ yield item
83
+ return
84
+ try:
85
+ from rich.progress import (
86
+ BarColumn,
87
+ Progress,
88
+ SpinnerColumn,
89
+ TextColumn,
90
+ TimeElapsedColumn,
91
+ )
92
+ except Exception:
93
+ for item in items:
94
+ yield item
95
+ return
96
+
97
+ with Progress(
98
+ SpinnerColumn(),
99
+ TextColumn("{task.description}"),
100
+ BarColumn(),
101
+ TimeElapsedColumn(),
102
+ console=console,
103
+ transient=True,
104
+ ) as progress:
105
+ task_id = progress.add_task(description, total=len(items))
106
+ for item in items:
107
+ yield item
108
+ progress.advance(task_id)
109
+
110
+
62
111
  def _display_session_analytics(
63
- console: Console, sdk: SDK, session_id: str, graph_dir: str
112
+ console: Console, sdk: SDK, session_id: str, graph_dir: str, quiet: bool
64
113
  ) -> None:
65
114
  """Display analytics for a single session."""
66
115
  from htmlgraph.converter import html_to_session
@@ -72,19 +121,23 @@ def _display_session_analytics(
72
121
  return
73
122
 
74
123
  try:
75
- session = html_to_session(session_path)
124
+ with _status_context(console, quiet, "Loading session data..."):
125
+ session = html_to_session(session_path)
76
126
  except Exception as e:
77
127
  console.print(f"[red]Error loading session: {e}[/red]")
78
128
  return
79
129
 
80
130
  # Get analytics
81
- dist = sdk.analytics.work_type_distribution(session_id=session_id)
82
- ratio = sdk.analytics.spike_to_feature_ratio(session_id=session_id)
83
- burden = sdk.analytics.maintenance_burden(session_id=session_id)
84
- primary = sdk.analytics.calculate_session_primary_work_type(session_id)
85
- breakdown = sdk.analytics.calculate_session_work_breakdown(session_id)
86
- total_events = sum(breakdown.values()) if breakdown else session.event_count
87
- transition_metrics = sdk.analytics.transition_time_metrics(session_id=session_id)
131
+ with _status_context(console, quiet, "Computing session analytics..."):
132
+ dist = sdk.analytics.work_type_distribution(session_id=session_id)
133
+ ratio = sdk.analytics.spike_to_feature_ratio(session_id=session_id)
134
+ burden = sdk.analytics.maintenance_burden(session_id=session_id)
135
+ primary = sdk.analytics.calculate_session_primary_work_type(session_id)
136
+ breakdown = sdk.analytics.calculate_session_work_breakdown(session_id)
137
+ total_events = sum(breakdown.values()) if breakdown else session.event_count
138
+ transition_metrics = sdk.analytics.transition_time_metrics(
139
+ session_id=session_id
140
+ )
88
141
 
89
142
  # Header panel
90
143
  header = Panel(
@@ -167,7 +220,11 @@ def _display_session_analytics(
167
220
 
168
221
 
169
222
  def _display_recent_sessions(
170
- console: Console, sdk: SDK, session_files: list[Path], graph_dir: str
223
+ console: Console,
224
+ sdk: SDK,
225
+ session_files: list[Path],
226
+ graph_dir: str,
227
+ quiet: bool,
171
228
  ) -> None:
172
229
  """Display analytics for recent sessions."""
173
230
  console.print(
@@ -188,7 +245,9 @@ def _display_recent_sessions(
188
245
  table.add_column("Primary Type", style="yellow")
189
246
  table.add_column("Spike Ratio", justify="right")
190
247
 
191
- for session_path in session_files:
248
+ for session_path in _iter_with_progress(
249
+ console, quiet, session_files, "Processing sessions"
250
+ ):
192
251
  try:
193
252
  session = html_to_session(session_path)
194
253
  session_id = session.id
@@ -226,7 +285,11 @@ def _display_recent_sessions(
226
285
 
227
286
 
228
287
  def _display_project_analytics(
229
- console: Console, sdk: SDK, session_files: list[Path], graph_dir: str
288
+ console: Console,
289
+ sdk: SDK,
290
+ session_files: list[Path],
291
+ graph_dir: str,
292
+ quiet: bool,
230
293
  ) -> None:
231
294
  """Display project-wide analytics."""
232
295
  console.print(
@@ -240,10 +303,11 @@ def _display_project_analytics(
240
303
  console.print()
241
304
 
242
305
  # Get project-wide metrics
243
- all_dist = sdk.analytics.work_type_distribution()
244
- all_ratio = sdk.analytics.spike_to_feature_ratio()
245
- all_burden = sdk.analytics.maintenance_burden()
246
- all_transition = sdk.analytics.transition_time_metrics()
306
+ with _status_context(console, quiet, "Computing project analytics..."):
307
+ all_dist = sdk.analytics.work_type_distribution()
308
+ all_ratio = sdk.analytics.spike_to_feature_ratio()
309
+ all_burden = sdk.analytics.maintenance_burden()
310
+ all_transition = sdk.analytics.transition_time_metrics()
247
311
 
248
312
  # Work distribution table
249
313
  if all_dist:
@@ -341,7 +405,9 @@ def _display_project_analytics(
341
405
  console.print("[bold]Recent Sessions:[/bold]")
342
406
  recent_files = session_files[:5]
343
407
 
344
- for session_path in recent_files:
408
+ for session_path in _iter_with_progress(
409
+ console, quiet, recent_files, "Loading recent sessions"
410
+ ):
345
411
  try:
346
412
  session = html_to_session(session_path)
347
413
  primary = (