htmlgraph 0.26.23__py3-none-any.whl → 0.26.25__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 (36) hide show
  1. htmlgraph/__init__.py +1 -1
  2. htmlgraph/analytics/pattern_learning.py +771 -0
  3. htmlgraph/api/main.py +56 -23
  4. htmlgraph/api/templates/dashboard-redesign.html +3 -3
  5. htmlgraph/api/templates/dashboard.html +3 -3
  6. htmlgraph/api/templates/partials/work-items.html +613 -0
  7. htmlgraph/builders/track.py +26 -0
  8. htmlgraph/cli/base.py +31 -7
  9. htmlgraph/cli/work/__init__.py +74 -0
  10. htmlgraph/cli/work/browse.py +114 -0
  11. htmlgraph/cli/work/snapshot.py +558 -0
  12. htmlgraph/collections/base.py +34 -0
  13. htmlgraph/collections/todo.py +12 -0
  14. htmlgraph/converter.py +11 -0
  15. htmlgraph/db/schema.py +34 -1
  16. htmlgraph/hooks/orchestrator.py +88 -14
  17. htmlgraph/hooks/session_handler.py +3 -1
  18. htmlgraph/models.py +22 -2
  19. htmlgraph/orchestration/__init__.py +4 -0
  20. htmlgraph/orchestration/plugin_manager.py +1 -2
  21. htmlgraph/orchestration/spawner_event_tracker.py +383 -0
  22. htmlgraph/refs.py +343 -0
  23. htmlgraph/sdk.py +162 -1
  24. htmlgraph/session_manager.py +154 -2
  25. htmlgraph/sessions/__init__.py +23 -0
  26. htmlgraph/sessions/handoff.py +755 -0
  27. htmlgraph/track_builder.py +12 -0
  28. {htmlgraph-0.26.23.dist-info → htmlgraph-0.26.25.dist-info}/METADATA +1 -1
  29. {htmlgraph-0.26.23.dist-info → htmlgraph-0.26.25.dist-info}/RECORD +36 -28
  30. {htmlgraph-0.26.23.data → htmlgraph-0.26.25.data}/data/htmlgraph/dashboard.html +0 -0
  31. {htmlgraph-0.26.23.data → htmlgraph-0.26.25.data}/data/htmlgraph/styles.css +0 -0
  32. {htmlgraph-0.26.23.data → htmlgraph-0.26.25.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
  33. {htmlgraph-0.26.23.data → htmlgraph-0.26.25.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
  34. {htmlgraph-0.26.23.data → htmlgraph-0.26.25.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
  35. {htmlgraph-0.26.23.dist-info → htmlgraph-0.26.25.dist-info}/WHEEL +0 -0
  36. {htmlgraph-0.26.23.dist-info → htmlgraph-0.26.25.dist-info}/entry_points.txt +0 -0
htmlgraph/cli/base.py CHANGED
@@ -464,11 +464,22 @@ class Formatter(Protocol):
464
464
 
465
465
 
466
466
  def _serialize_json(value: Any) -> Any:
467
- """Recursively serialize value to JSON-compatible types."""
467
+ """Recursively serialize value to JSON-compatible types.
468
+
469
+ Sanitizes strings to remove control characters (newlines, tabs) that
470
+ would break JSON validity when using json.dumps().
471
+ """
468
472
  if value is None:
469
473
  return None
470
474
  if isinstance(value, (datetime, date)):
471
475
  return value.isoformat()
476
+ if isinstance(value, str):
477
+ # Sanitize string: replace control characters with spaces
478
+ # This prevents newlines/tabs in JSON string values from breaking JSON validity
479
+ sanitized = value.replace("\n", " ").replace("\r", " ").replace("\t", " ")
480
+ # Collapse multiple spaces to single space
481
+ sanitized = " ".join(sanitized.split())
482
+ return sanitized
472
483
  if hasattr(value, "model_dump") and callable(getattr(value, "model_dump")):
473
484
  return _serialize_json(value.model_dump())
474
485
  if hasattr(value, "to_dict") and callable(getattr(value, "to_dict")):
@@ -485,7 +496,9 @@ class JsonFormatter:
485
496
 
486
497
  def output(self, result: CommandResult) -> None:
487
498
  payload = result.json_data if result.json_data is not None else result.data
488
- _console.print(json.dumps(_serialize_json(payload), indent=2))
499
+ # Use sys.stdout.write instead of _console.print to avoid Rich's line-wrapping
500
+ # which inserts literal newlines into JSON string values, breaking JSON validity
501
+ sys.stdout.write(json.dumps(_serialize_json(payload), indent=2) + "\n")
489
502
 
490
503
 
491
504
  class TextFormatter:
@@ -507,16 +520,21 @@ class TextFormatter:
507
520
  _console.print(result.data)
508
521
  return
509
522
  if isinstance(result.text, str):
510
- _console.print(result.text)
523
+ # Use sys.stdout.write() for ANSI-formatted text to preserve colors when piped
524
+ # This bypasses Rich's reprocessing and ensures ANSI codes are preserved
525
+ sys.stdout.write(result.text)
526
+ if not result.text.endswith("\n"):
527
+ sys.stdout.write("\n")
511
528
  return
512
- _console.print("\n".join(str(line) for line in result.text))
529
+ # For text as list/iterable, write directly to preserve ANSI codes
530
+ sys.stdout.write("\n".join(str(line) for line in result.text) + "\n")
513
531
 
514
532
 
515
533
  def get_formatter(format_name: str) -> Formatter:
516
- """Get formatter by name (json, text, plain)."""
534
+ """Get formatter by name (json, text, plain, refs)."""
517
535
  if format_name == "json":
518
536
  return JsonFormatter()
519
- if format_name in ("text", "plain", ""):
537
+ if format_name in ("text", "plain", "refs", ""):
520
538
  return TextFormatter()
521
539
  raise CommandError(f"Unknown output format '{format_name}'")
522
540
 
@@ -539,6 +557,9 @@ class BaseCommand(ABC):
539
557
  self.graph_dir: str | None = None
540
558
  self.agent: str | None = None
541
559
  self._sdk: SDK | None = None
560
+ self.override_output_format: str | None = (
561
+ None # Allow commands to override formatter
562
+ )
542
563
 
543
564
  @classmethod
544
565
  @abstractmethod
@@ -648,7 +669,10 @@ class BaseCommand(ABC):
648
669
  try:
649
670
  self.validate()
650
671
  result = self.execute()
651
- formatter = get_formatter(output_format)
672
+ # Allow commands to override output format
673
+ # (e.g., snapshot command's --output-format flag overrides global --format)
674
+ actual_format = self.override_output_format or output_format
675
+ formatter = get_formatter(actual_format)
652
676
  formatter.output(result)
653
677
  except CommandError as exc:
654
678
  error_console = Console(file=sys.stderr)
@@ -23,6 +23,7 @@ def register_commands(subparsers: _SubParsersAction) -> None:
23
23
  Args:
24
24
  subparsers: Subparser action from ArgumentParser.add_subparsers()
25
25
  """
26
+ from htmlgraph.cli.work.browse import BrowseCommand
26
27
  from htmlgraph.cli.work.features import register_feature_commands
27
28
  from htmlgraph.cli.work.orchestration import (
28
29
  register_archive_commands,
@@ -31,6 +32,7 @@ def register_commands(subparsers: _SubParsersAction) -> None:
31
32
  )
32
33
  from htmlgraph.cli.work.report import register_report_commands
33
34
  from htmlgraph.cli.work.sessions import register_session_commands
35
+ from htmlgraph.cli.work.snapshot import SnapshotCommand
34
36
  from htmlgraph.cli.work.tracks import register_track_commands
35
37
 
36
38
  # Register all command groups
@@ -42,8 +44,75 @@ def register_commands(subparsers: _SubParsersAction) -> None:
42
44
  register_claude_commands(subparsers)
43
45
  register_report_commands(subparsers)
44
46
 
47
+ # Snapshot command
48
+ snapshot_parser = subparsers.add_parser(
49
+ "snapshot",
50
+ help="Output current graph state with refs",
51
+ )
52
+ snapshot_parser.add_argument(
53
+ "--output-format",
54
+ choices=["refs", "json", "text"],
55
+ default="refs",
56
+ help="Output format (default: refs)",
57
+ )
58
+ snapshot_parser.add_argument(
59
+ "--type",
60
+ help="Filter by type (feature, track, bug, spike, chore, epic, all)",
61
+ )
62
+ snapshot_parser.add_argument(
63
+ "--status",
64
+ help="Filter by status (todo, in_progress, blocked, done, all)",
65
+ )
66
+ snapshot_parser.add_argument(
67
+ "--track",
68
+ help="Show only items in a specific track (by track ID or ref)",
69
+ )
70
+ snapshot_parser.add_argument(
71
+ "--active",
72
+ action="store_true",
73
+ help="Show only TODO/IN_PROGRESS items (filters out metadata spikes)",
74
+ )
75
+ snapshot_parser.add_argument(
76
+ "--blockers",
77
+ action="store_true",
78
+ help="Show only critical/blocked items",
79
+ )
80
+ snapshot_parser.add_argument(
81
+ "--summary",
82
+ action="store_true",
83
+ help="Show counts and progress summary instead of listing all items",
84
+ )
85
+ snapshot_parser.add_argument(
86
+ "--my-work",
87
+ action="store_true",
88
+ help="Show items assigned to current agent",
89
+ )
90
+ snapshot_parser.set_defaults(func=SnapshotCommand.from_args)
91
+
92
+ # Browse command
93
+ browse_parser = subparsers.add_parser(
94
+ "browse",
95
+ help="Open dashboard in browser",
96
+ )
97
+ browse_parser.add_argument(
98
+ "--port",
99
+ type=int,
100
+ default=8080,
101
+ help="Server port (default: 8080)",
102
+ )
103
+ browse_parser.add_argument(
104
+ "--query-type",
105
+ help="Filter by type (feature, track, bug, spike, chore, epic)",
106
+ )
107
+ browse_parser.add_argument(
108
+ "--query-status",
109
+ help="Filter by status (todo, in_progress, blocked, done)",
110
+ )
111
+ browse_parser.set_defaults(func=BrowseCommand.from_args)
112
+
45
113
 
46
114
  # Re-export all command classes for backward compatibility
115
+ from htmlgraph.cli.work.browse import BrowseCommand
47
116
  from htmlgraph.cli.work.features import (
48
117
  FeatureClaimCommand,
49
118
  FeatureCompleteCommand,
@@ -71,6 +140,7 @@ from htmlgraph.cli.work.sessions import (
71
140
  SessionStartCommand,
72
141
  SessionStartInfoCommand,
73
142
  )
143
+ from htmlgraph.cli.work.snapshot import SnapshotCommand
74
144
  from htmlgraph.cli.work.tracks import (
75
145
  TrackDeleteCommand,
76
146
  TrackListCommand,
@@ -89,6 +159,10 @@ __all__ = [
89
159
  "SessionStartInfoCommand",
90
160
  # Report commands
91
161
  "SessionReportCommand",
162
+ # Snapshot commands
163
+ "SnapshotCommand",
164
+ # Browse commands
165
+ "BrowseCommand",
92
166
  # Feature commands
93
167
  "FeatureListCommand",
94
168
  "FeatureCreateCommand",
@@ -0,0 +1,114 @@
1
+ """HtmlGraph CLI - Browse command for opening dashboard in browser."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import webbrowser
7
+ from typing import TYPE_CHECKING
8
+
9
+ from htmlgraph.cli.base import BaseCommand, CommandResult
10
+
11
+ if TYPE_CHECKING:
12
+ pass
13
+
14
+
15
+ class BrowseCommand(BaseCommand):
16
+ """Open the HtmlGraph dashboard in your default browser.
17
+
18
+ Usage:
19
+ htmlgraph browse # Open dashboard
20
+ htmlgraph browse --port 8080 # Custom port
21
+ htmlgraph browse --query-type feature # Show only features
22
+ htmlgraph browse --query-status todo # Show only todo items
23
+ """
24
+
25
+ def __init__(
26
+ self,
27
+ *,
28
+ port: int = 8080,
29
+ query_type: str | None = None,
30
+ query_status: str | None = None,
31
+ ) -> None:
32
+ """Initialize BrowseCommand.
33
+
34
+ Args:
35
+ port: Server port (default: 8080)
36
+ query_type: Filter by type (feature, track, bug, spike, chore, epic)
37
+ query_status: Filter by status (todo, in_progress, blocked, done)
38
+ """
39
+ super().__init__()
40
+ self.port = port
41
+ self.query_type = query_type
42
+ self.query_status = query_status
43
+
44
+ @classmethod
45
+ def from_args(cls, args: argparse.Namespace) -> BrowseCommand:
46
+ """Create BrowseCommand from argparse arguments.
47
+
48
+ Args:
49
+ args: Argparse namespace with command arguments
50
+
51
+ Returns:
52
+ BrowseCommand instance
53
+ """
54
+ return cls(
55
+ port=args.port,
56
+ query_type=args.query_type,
57
+ query_status=args.query_status,
58
+ )
59
+
60
+ def execute(self) -> CommandResult:
61
+ """Execute the browse command.
62
+
63
+ Opens the dashboard in the default browser with optional query parameters.
64
+
65
+ Returns:
66
+ CommandResult with success status and URL
67
+ """
68
+ # Build URL with query params
69
+ url = f"http://localhost:{self.port}"
70
+
71
+ params = []
72
+ if self.query_type:
73
+ params.append(f"type={self.query_type}")
74
+ if self.query_status:
75
+ params.append(f"status={self.query_status}")
76
+
77
+ if params:
78
+ url += "?" + "&".join(params)
79
+
80
+ # Check if server is running
81
+ try:
82
+ import requests # type: ignore[import-untyped]
83
+
84
+ response = requests.head(f"http://localhost:{self.port}", timeout=1)
85
+ response.raise_for_status()
86
+ except ImportError:
87
+ # requests module not available - try to open anyway with a warning
88
+ webbrowser.open(url)
89
+ return CommandResult(
90
+ data={"url": url},
91
+ text=f"Opening dashboard at {url}\n(Note: Could not verify server is running - install 'requests' for server checks)",
92
+ exit_code=0,
93
+ )
94
+ except Exception:
95
+ # Server not running or not responding
96
+ return CommandResult(
97
+ text=f"Dashboard server not running on port {self.port}.\nStart with: htmlgraph serve --port {self.port}",
98
+ exit_code=1,
99
+ )
100
+
101
+ # Open browser
102
+ try:
103
+ webbrowser.open(url)
104
+ except Exception as e:
105
+ return CommandResult(
106
+ text=f"Failed to open browser: {e}\nYou can manually visit: {url}",
107
+ exit_code=1,
108
+ )
109
+
110
+ return CommandResult(
111
+ data={"url": url},
112
+ text=f"Opening dashboard at {url}",
113
+ exit_code=0,
114
+ )