htmlgraph 0.23.2__py3-none-any.whl → 0.23.4__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/__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.23.2"
87
+ __version__ = "0.23.4"
88
88
  __all__ = [
89
89
  # Exceptions
90
90
  "HtmlGraphError",
@@ -20,8 +20,7 @@ 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. OpenCode detection (OPENCODE environment markers)
24
- 5. Fall back to "cli"
23
+ 4. Fall back to "cli"
25
24
  """
26
25
  # 1. Explicit override
27
26
  explicit = os.environ.get("HTMLGRAPH_AGENT")
@@ -36,11 +35,7 @@ def detect_agent_name() -> str:
36
35
  if _is_gemini():
37
36
  return "gemini"
38
37
 
39
- # 4. OpenCode detection
40
- if _is_opencode():
41
- return "opencode"
42
-
43
- # 5. Default to CLI
38
+ # 4. Default to CLI
44
39
  return "cli"
45
40
 
46
41
 
@@ -93,39 +88,6 @@ def _is_gemini() -> bool:
93
88
  return False
94
89
 
95
90
 
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
-
129
91
  def get_agent_display_name(agent: str) -> str:
130
92
  """
131
93
  Get a human-friendly display name for an agent.
@@ -140,7 +102,6 @@ def get_agent_display_name(agent: str) -> str:
140
102
  "claude": "Claude",
141
103
  "claude-code": "Claude",
142
104
  "gemini": "Gemini",
143
- "opencode": "OpenCode",
144
105
  "cli": "CLI",
145
106
  "haiku": "Haiku",
146
107
  "opus": "Opus",
htmlgraph/cli.py CHANGED
@@ -11,7 +11,8 @@ Usage:
11
11
  Claude Code Integration:
12
12
  htmlgraph claude # Start Claude Code
13
13
  htmlgraph claude --init # Start with orchestrator system prompt (recommended)
14
- htmlgraph claude --continue # Resume last Claude Code session
14
+ htmlgraph claude --continue # Resume last Claude Code session (with plugin loaded)
15
+ htmlgraph claude --dev # Start in development mode (load plugin from packages/)
15
16
 
16
17
  Session Management:
17
18
  htmlgraph session start [--id ID] [--agent AGENT]
@@ -170,7 +171,7 @@ def cmd_init(args: argparse.Namespace) -> None:
170
171
  generate_docs = True # Always generate in non-interactive mode
171
172
 
172
173
  def init_progress() -> tuple[Any | None, Any | None]:
173
- if args.quiet or getattr(args, "format", "text") != "text":
174
+ if getattr(args, "quiet", False) or getattr(args, "format", "text") != "text":
174
175
  return None, None
175
176
  try:
176
177
  from rich.console import Console
@@ -3589,6 +3590,19 @@ def cmd_claude(args: argparse.Namespace) -> None:
3589
3590
  """Start Claude Code with orchestrator prompt."""
3590
3591
  import textwrap
3591
3592
 
3593
+ # Load orchestration rules from plugin
3594
+ rules_file = (
3595
+ Path(__file__).parent.parent.parent.parent
3596
+ / "packages"
3597
+ / "claude-plugin"
3598
+ / "rules"
3599
+ / "orchestration.md"
3600
+ )
3601
+
3602
+ orchestration_rules = ""
3603
+ if rules_file.exists():
3604
+ orchestration_rules = rules_file.read_text(encoding="utf-8")
3605
+
3592
3606
  try:
3593
3607
  if args.init:
3594
3608
  # Load optimized orchestrator system prompt
@@ -3622,9 +3636,14 @@ def cmd_claude(args: argparse.Namespace) -> None:
3622
3636
  """
3623
3637
  )
3624
3638
 
3639
+ # Combine fallback prompt with orchestration rules
3640
+ combined_prompt = system_prompt
3641
+ if orchestration_rules:
3642
+ combined_prompt = f"{system_prompt}\n\n---\n\n{orchestration_rules}"
3643
+
3625
3644
  if args.quiet or args.format == "json":
3626
3645
  # Non-interactive: directly launch Claude with system prompt
3627
- cmd = ["claude", "--append-system-prompt", system_prompt]
3646
+ cmd = ["claude", "--append-system-prompt", combined_prompt]
3628
3647
  else:
3629
3648
  # Interactive: show summary first
3630
3649
  print("=" * 60)
@@ -3632,13 +3651,13 @@ def cmd_claude(args: argparse.Namespace) -> None:
3632
3651
  print("=" * 60)
3633
3652
  print("\nStarting Claude Code with orchestrator system prompt...")
3634
3653
  print("Key directives:")
3635
- print(" ✓ Delegate implementation to subagents")
3654
+ print(" ✓ Delegate to Gemini (FREE), Codex, Copilot first")
3655
+ print(" ✓ Use Task() only as fallback")
3636
3656
  print(" ✓ Create work items before delegating")
3637
3657
  print(" ✓ Track all work in .htmlgraph/")
3638
- print(" ✓ Respect dependency chains")
3639
3658
  print()
3640
3659
 
3641
- cmd = ["claude", "--append-system-prompt", system_prompt]
3660
+ cmd = ["claude", "--append-system-prompt", combined_prompt]
3642
3661
 
3643
3662
  try:
3644
3663
  subprocess.run(cmd, check=False)
@@ -3652,12 +3671,78 @@ def cmd_claude(args: argparse.Namespace) -> None:
3652
3671
 
3653
3672
  elif args.continue_session:
3654
3673
  # Resume last Claude Code session
3674
+ # Find plugin directory relative to project root
3675
+ plugin_dir = (
3676
+ Path(__file__).parent.parent.parent.parent
3677
+ / "packages"
3678
+ / "claude-plugin"
3679
+ / ".claude-plugin"
3680
+ )
3681
+
3655
3682
  if args.quiet or args.format == "json":
3656
3683
  cmd = ["claude", "--resume"]
3657
3684
  else:
3658
3685
  print("Resuming last Claude Code session...")
3659
3686
  cmd = ["claude", "--resume"]
3660
3687
 
3688
+ # Add orchestration rules if available
3689
+ if orchestration_rules:
3690
+ cmd.extend(["--append-system-prompt", orchestration_rules])
3691
+ if not (args.quiet or args.format == "json"):
3692
+ print(" ✓ Multi-AI delegation rules injected")
3693
+
3694
+ # Add plugin directory if it exists
3695
+ if plugin_dir.exists():
3696
+ cmd.extend(["--plugin-dir", str(plugin_dir)])
3697
+ if not (args.quiet or args.format == "json"):
3698
+ print(f" ✓ Loading plugin from: {plugin_dir}")
3699
+
3700
+ try:
3701
+ subprocess.run(cmd, check=False)
3702
+ except FileNotFoundError:
3703
+ print("Error: 'claude' command not found.", file=sys.stderr)
3704
+ print(
3705
+ "Please install Claude Code CLI: https://code.claude.com",
3706
+ file=sys.stderr,
3707
+ )
3708
+ sys.exit(1)
3709
+
3710
+ elif args.dev:
3711
+ # Development mode: load plugin from local directory
3712
+ plugin_dir = (
3713
+ Path(__file__).parent.parent.parent.parent
3714
+ / "packages"
3715
+ / "claude-plugin"
3716
+ / ".claude-plugin"
3717
+ )
3718
+
3719
+ if not plugin_dir.exists():
3720
+ print(
3721
+ f"Error: Plugin directory not found: {plugin_dir}", file=sys.stderr
3722
+ )
3723
+ print(
3724
+ "Expected location: packages/claude-plugin/.claude-plugin",
3725
+ file=sys.stderr,
3726
+ )
3727
+ sys.exit(1)
3728
+
3729
+ if args.quiet or args.format == "json":
3730
+ cmd = ["claude", "--plugin-dir", str(plugin_dir)]
3731
+ else:
3732
+ print("=" * 60)
3733
+ print("🔧 HtmlGraph Development Mode")
3734
+ print("=" * 60)
3735
+ print(f"\nLoading plugin from: {plugin_dir}")
3736
+ print(" ✓ Skills, agents, and hooks will be loaded from local files")
3737
+ print(" ✓ Multi-AI delegation rules will be injected")
3738
+ print(" ✓ Changes to plugin files will take effect after restart")
3739
+ print()
3740
+ cmd = ["claude", "--plugin-dir", str(plugin_dir)]
3741
+
3742
+ # Add orchestration rules if available
3743
+ if orchestration_rules:
3744
+ cmd.extend(["--append-system-prompt", orchestration_rules])
3745
+
3661
3746
  try:
3662
3747
  subprocess.run(cmd, check=False)
3663
3748
  except FileNotFoundError:
@@ -3670,8 +3755,16 @@ def cmd_claude(args: argparse.Namespace) -> None:
3670
3755
 
3671
3756
  else:
3672
3757
  # Default: start normal Claude Code session
3758
+ cmd = ["claude"]
3759
+
3760
+ # Add orchestration rules if available
3761
+ if orchestration_rules:
3762
+ cmd.extend(["--append-system-prompt", orchestration_rules])
3763
+ if not (args.quiet or args.format == "json"):
3764
+ print("Starting Claude Code with multi-AI delegation rules...")
3765
+
3673
3766
  try:
3674
- subprocess.run(["claude"], check=False)
3767
+ subprocess.run(cmd, check=False)
3675
3768
  except FileNotFoundError:
3676
3769
  print("Error: 'claude' command not found.", file=sys.stderr)
3677
3770
  print(
@@ -5106,6 +5199,11 @@ For more help: https://github.com/Shakes-tzd/htmlgraph
5106
5199
  action="store_true",
5107
5200
  help="Resume last Claude Code session",
5108
5201
  )
5202
+ claude_group.add_argument(
5203
+ "--dev",
5204
+ action="store_true",
5205
+ help="Start in development mode with plugin loaded from packages/claude-plugin",
5206
+ )
5109
5207
  claude_parser.set_defaults(func=cmd_claude)
5110
5208
 
5111
5209
  args = parser.parse_args()
@@ -71,7 +71,19 @@ class BaseCollection(Generic[CollectionT]):
71
71
  self._graph: HtmlGraph | None = None # Lazy-loaded
72
72
 
73
73
  def _ensure_graph(self) -> HtmlGraph:
74
- """Get the graph for this collection, using SDK's shared instances where available."""
74
+ """
75
+ Get or initialize the graph for this collection.
76
+
77
+ Uses SDK's shared graph instances where available to avoid creating
78
+ multiple graph objects for the same collection. Creates a new instance
79
+ for unrecognized collections.
80
+
81
+ Returns:
82
+ HtmlGraph instance for this collection
83
+
84
+ Note:
85
+ This method is lazy - the graph is only loaded on first access.
86
+ """
75
87
  if self._graph is None:
76
88
  # Use SDK's shared graph instances to avoid multiple graph objects
77
89
  if self._collection_name == "features" and hasattr(self._sdk, "_graph"):
@@ -92,7 +104,21 @@ class BaseCollection(Generic[CollectionT]):
92
104
  return self._graph
93
105
 
94
106
  def __getattribute__(self, name: str) -> Any:
95
- """Override to provide helpful error messages for missing attributes."""
107
+ """
108
+ Override attribute access to provide helpful error messages.
109
+
110
+ When an attribute doesn't exist, provides suggestions for common
111
+ mistakes and similar method names to improve discoverability.
112
+
113
+ Args:
114
+ name: Attribute name being accessed
115
+
116
+ Returns:
117
+ The requested attribute
118
+
119
+ Raises:
120
+ AttributeError: With helpful suggestions if attribute not found
121
+ """
96
122
  try:
97
123
  return object.__getattribute__(self, name)
98
124
  except AttributeError as e:
@@ -134,7 +160,15 @@ class BaseCollection(Generic[CollectionT]):
134
160
  raise AttributeError(error_msg) from e
135
161
 
136
162
  def __dir__(self) -> list[str]:
137
- """Return attributes with most useful ones first for discoverability."""
163
+ """
164
+ Return attributes with most useful ones first.
165
+
166
+ Orders attributes to show commonly-used methods first in auto-complete
167
+ and help() output, improving discoverability for new users.
168
+
169
+ Returns:
170
+ List of attribute names, ordered by priority then alphabetically
171
+ """
138
172
  priority = [
139
173
  # Creation and retrieval
140
174
  "create",
@@ -518,13 +552,15 @@ class BaseCollection(Generic[CollectionT]):
518
552
  """
519
553
  Start working on a node (feature/bug/etc).
520
554
 
521
- Delegates to SessionManager to:
555
+ Delegates to SessionManager if available for smart tracking:
522
556
  1. Check WIP limits
523
557
  2. Ensure not claimed by others
524
558
  3. Auto-claim for agent
525
559
  4. Link to active session
526
560
  5. Log 'FeatureStart' event
527
561
 
562
+ Falls back to simple status update if SessionManager not available.
563
+
528
564
  Args:
529
565
  node_id: Node ID to start
530
566
  agent: Agent ID (defaults to SDK agent)
@@ -534,6 +570,10 @@ class BaseCollection(Generic[CollectionT]):
534
570
 
535
571
  Raises:
536
572
  NodeNotFoundError: If node not found
573
+
574
+ Example:
575
+ >>> sdk.features.start('feat-abc123')
576
+ >>> sdk.features.start('feat-xyz', agent='claude')
537
577
  """
538
578
  agent = agent or self._sdk.agent
539
579
 
@@ -566,14 +606,17 @@ class BaseCollection(Generic[CollectionT]):
566
606
  transcript_id: str | None = None,
567
607
  ) -> Node | None:
568
608
  """
569
- Complete a node.
609
+ Mark a node as complete.
570
610
 
571
- Delegates to SessionManager to:
572
- 1. Update status
611
+ Delegates to SessionManager if available for event logging and
612
+ transcript linking:
613
+ 1. Update status to 'done'
573
614
  2. Log 'FeatureComplete' event
574
615
  3. Release claim (optional behavior)
575
616
  4. Link transcript if provided (for parallel agent tracking)
576
617
 
618
+ Falls back to simple status update if SessionManager not available.
619
+
577
620
  Args:
578
621
  node_id: Node ID to complete
579
622
  agent: Agent ID (defaults to SDK agent)
@@ -585,6 +628,10 @@ class BaseCollection(Generic[CollectionT]):
585
628
 
586
629
  Raises:
587
630
  NodeNotFoundError: If node not found
631
+
632
+ Example:
633
+ >>> sdk.features.complete('feat-abc123')
634
+ >>> sdk.features.complete('feat-xyz', agent='claude', transcript_id='trans-123')
588
635
  """
589
636
  agent = agent or self._sdk.agent
590
637
 
@@ -615,11 +662,13 @@ class BaseCollection(Generic[CollectionT]):
615
662
  """
616
663
  Claim a node for an agent.
617
664
 
618
- Delegates to SessionManager to:
665
+ Delegates to SessionManager if available for ownership tracking:
619
666
  1. Check ownership rules
620
667
  2. Update assignment
621
668
  3. Log 'FeatureClaim' event
622
669
 
670
+ Falls back to simple assignment if SessionManager not available.
671
+
623
672
  Args:
624
673
  node_id: Node ID to claim
625
674
  agent: Agent ID (defaults to SDK agent)
@@ -631,6 +680,10 @@ class BaseCollection(Generic[CollectionT]):
631
680
  ValueError: If agent not provided and SDK has no agent
632
681
  NodeNotFoundError: If node not found
633
682
  ClaimConflictError: If node already claimed by different agent
683
+
684
+ Example:
685
+ >>> sdk.features.claim('feat-abc123')
686
+ >>> sdk.features.claim('feat-xyz', agent='claude')
634
687
  """
635
688
  agent = agent or self._sdk.agent
636
689
  if not agent:
@@ -665,11 +718,13 @@ class BaseCollection(Generic[CollectionT]):
665
718
  """
666
719
  Release a claimed node.
667
720
 
668
- Delegates to SessionManager to:
721
+ Delegates to SessionManager if available for ownership tracking:
669
722
  1. Verify ownership
670
723
  2. Clear assignment
671
724
  3. Log 'FeatureRelease' event
672
725
 
726
+ Falls back to simple assignment clearing if SessionManager not available.
727
+
673
728
  Args:
674
729
  node_id: Node ID to release
675
730
  agent: Agent ID (defaults to SDK agent)
@@ -679,6 +734,10 @@ class BaseCollection(Generic[CollectionT]):
679
734
 
680
735
  Raises:
681
736
  NodeNotFoundError: If node not found
737
+
738
+ Example:
739
+ >>> sdk.features.release('feat-abc123')
740
+ >>> sdk.features.release('feat-xyz', agent='claude')
682
741
  """
683
742
  # SessionManager.release_feature requires an agent to verify ownership
684
743
  agent = agent or self._sdk.agent
htmlgraph/converter.py CHANGED
@@ -395,7 +395,8 @@ def html_to_session(filepath: Path | str) -> Session:
395
395
  parser = HtmlParser.from_file(filepath)
396
396
 
397
397
  # Get article element with session data
398
- article = parser.query_one("article[data-type='session']")
398
+ article_results = parser.query("article[data-type='session']")
399
+ article = article_results[0] if article_results else None
399
400
  if not article:
400
401
  raise ValueError(f"No session article found in: {filepath}")
401
402
 
@@ -465,7 +466,8 @@ def html_to_session(filepath: Path | str) -> Session:
465
466
  data["transcript_git_branch"] = transcript_branch
466
467
 
467
468
  # Parse title
468
- title_el = parser.query_one("h1")
469
+ title_el_results = parser.query("h1")
470
+ title_el = title_el_results[0] if title_el_results else None
469
471
  if title_el:
470
472
  data["title"] = title_el.to_text().strip()
471
473
 
@@ -482,24 +484,28 @@ def html_to_session(filepath: Path | str) -> Session:
482
484
  data["worked_on"] = worked_on
483
485
 
484
486
  # Parse continued_from edge
485
- continued_link = parser.query_one(
487
+ continued_link_results = parser.query(
486
488
  "nav[data-graph-edges] section[data-edge-type='continued-from'] a"
487
489
  )
490
+ continued_link = continued_link_results[0] if continued_link_results else None
488
491
  if continued_link:
489
492
  href = continued_link.attrs.get("href") or ""
490
493
  data["continued_from"] = href.replace(".html", "")
491
494
 
492
495
  # Parse handoff context
493
- handoff_section = parser.query_one("section[data-handoff]")
496
+ handoff_section_results = parser.query("section[data-handoff]")
497
+ handoff_section = handoff_section_results[0] if handoff_section_results else None
494
498
  if handoff_section:
495
- notes_el = parser.query_one("section[data-handoff] [data-handoff-notes]")
499
+ notes_el_results = parser.query("section[data-handoff] [data-handoff-notes]")
500
+ notes_el = notes_el_results[0] if notes_el_results else None
496
501
  if notes_el:
497
502
  notes_text = notes_el.to_text().strip()
498
503
  if notes_text.lower().startswith("notes:"):
499
504
  notes_text = notes_text.split(":", 1)[1].strip()
500
505
  data["handoff_notes"] = notes_text
501
506
 
502
- next_el = parser.query_one("section[data-handoff] [data-recommended-next]")
507
+ next_el_results = parser.query("section[data-handoff] [data-recommended-next]")
508
+ next_el = next_el_results[0] if next_el_results else None
503
509
  if next_el:
504
510
  next_text = next_el.to_text().strip()
505
511
  if next_text.lower().startswith("recommended next:"):
@@ -555,12 +561,14 @@ def html_to_session(filepath: Path | str) -> Session:
555
561
  pattern_type = tr.attrs.get("data-pattern-type", "neutral")
556
562
 
557
563
  # Extract sequence from first <td class="sequence">
558
- seq_td = tr.query_one("td.sequence")
564
+ seq_tds = tr.query("td.sequence")
565
+ seq_td = seq_tds[0] if seq_tds else None
559
566
  sequence_str = seq_td.to_text().strip() if seq_td else ""
560
567
  sequence = [s.strip() for s in sequence_str.split("→")] if sequence_str else []
561
568
 
562
569
  # Extract count from third <td>
563
- count_td = tr.query_all("td")[2] if len(tr.query_all("td")) > 2 else None
570
+ tds = tr.query("td")
571
+ count_td = tds[2] if len(tds) > 2 else None
564
572
  count_str = count_td.to_text().strip() if count_td else "0"
565
573
  try:
566
574
  count = int(count_str)
@@ -568,7 +576,7 @@ def html_to_session(filepath: Path | str) -> Session:
568
576
  count = 0
569
577
 
570
578
  # Extract timestamps from fourth <td>
571
- time_td = tr.query_all("td")[3] if len(tr.query_all("td")) > 3 else None
579
+ time_td = tds[3] if len(tds) > 3 else None
572
580
  time_str = time_td.to_text().strip() if time_td else ""
573
581
  times = time_str.split(" / ")
574
582
  first_detected = times[0].strip() if len(times) > 0 else ""
htmlgraph/models.py CHANGED
@@ -317,6 +317,24 @@ class Node(BaseModel):
317
317
  "session_ids": self.context_sessions,
318
318
  }
319
319
 
320
+ def to_dict(self) -> dict:
321
+ """
322
+ Convert Node to dictionary format.
323
+
324
+ This is a convenience alias for Pydantic's model_dump() method,
325
+ providing a more discoverable API for serialization.
326
+
327
+ Returns:
328
+ dict: Dictionary representation of the Node with all fields
329
+
330
+ Example:
331
+ >>> feature = sdk.features.create("My Feature").save()
332
+ >>> data = feature.to_dict()
333
+ >>> print(data['title'])
334
+ 'My Feature'
335
+ """
336
+ return self.model_dump()
337
+
320
338
  def to_html(self, stylesheet_path: str = "../styles.css") -> str:
321
339
  """
322
340
  Convert node to full HTML document.
htmlgraph/parser.py CHANGED
@@ -80,7 +80,8 @@ class HtmlParser:
80
80
 
81
81
  def get_article(self) -> Any | None:
82
82
  """Get the main article element (graph node root)."""
83
- return self.query_one("article[id]")
83
+ results = self.query("article[id]")
84
+ return results[0] if results else None
84
85
 
85
86
  def get_node_id(self) -> str | None:
86
87
  """Extract node ID from article element."""
@@ -200,13 +201,15 @@ class HtmlParser:
200
201
  def get_title(self) -> str | None:
201
202
  """Get node title from h1 or title element."""
202
203
  # Try h1 in header first
203
- h1 = self.query_one("article header h1")
204
+ h1_results = self.query("article header h1")
205
+ h1 = h1_results[0] if h1_results else None
204
206
  if h1:
205
207
  text: str = h1.to_text().strip()
206
208
  return text
207
209
 
208
210
  # Fall back to title element
209
- title = self.query_one("title")
211
+ title_results = self.query("title")
212
+ title = title_results[0] if title_results else None
210
213
  if title:
211
214
  text2: str = title.to_text().strip()
212
215
  return text2
@@ -225,7 +228,8 @@ class HtmlParser:
225
228
  """
226
229
  edges: dict[str, list[dict[str, Any]]] = {}
227
230
 
228
- edge_nav = self.query_one("nav[data-graph-edges]")
231
+ edge_nav_results = self.query("nav[data-graph-edges]")
232
+ edge_nav = edge_nav_results[0] if edge_nav_results else None
229
233
  if not edge_nav:
230
234
  return edges
231
235
 
@@ -363,7 +367,10 @@ class HtmlParser:
363
367
 
364
368
  def get_content(self) -> str:
365
369
  """Extract main content from section[data-content]."""
366
- content_section = self.query_one("section[data-content]")
370
+ content_section_results = self.query("section[data-content]")
371
+ content_section = (
372
+ content_section_results[0] if content_section_results else None
373
+ )
367
374
  if not content_section:
368
375
  return ""
369
376
 
@@ -381,12 +388,16 @@ class HtmlParser:
381
388
 
382
389
  def get_findings(self) -> str | None:
383
390
  """Extract findings from section[data-findings] (Spike-specific)."""
384
- findings_section = self.query_one("section[data-findings]")
391
+ findings_section_results = self.query("section[data-findings]")
392
+ findings_section = (
393
+ findings_section_results[0] if findings_section_results else None
394
+ )
385
395
  if not findings_section:
386
396
  return None
387
397
 
388
398
  # Look for findings-content div using full selector
389
- content_div = self.query_one("section[data-findings] div.findings-content")
399
+ content_div_results = self.query("section[data-findings] div.findings-content")
400
+ content_div = content_div_results[0] if content_div_results else None
390
401
  if content_div:
391
402
  text = content_div.to_text().strip()
392
403
  return text if text else None
@@ -406,7 +417,10 @@ class HtmlParser:
406
417
 
407
418
  def get_decision(self) -> str | None:
408
419
  """Extract decision from section[data-decision] (Spike-specific)."""
409
- decision_section = self.query_one("section[data-decision]")
420
+ decision_section_results = self.query("section[data-decision]")
421
+ decision_section = (
422
+ decision_section_results[0] if decision_section_results else None
423
+ )
410
424
  if not decision_section:
411
425
  return None
412
426
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: htmlgraph
3
- Version: 0.23.2
3
+ Version: 0.23.4
4
4
  Summary: HTML is All You Need - Graph database on web standards
5
5
  Project-URL: Homepage, https://github.com/Shakes-tzd/htmlgraph
6
6
  Project-URL: Documentation, https://github.com/Shakes-tzd/htmlgraph#readme
@@ -259,6 +259,7 @@ MIT
259
259
  ## Links
260
260
 
261
261
  - [GitHub](https://github.com/Shakes-tzd/htmlgraph)
262
+ - [API Reference](docs/API_REFERENCE.md) - Complete SDK API documentation
262
263
  - [Documentation](docs/) - SDK guide, workflows, development principles
263
264
  - [Examples](examples/) - Real-world usage examples
264
265
  - [PyPI](https://pypi.org/project/htmlgraph/)
@@ -1,12 +1,12 @@
1
- htmlgraph/__init__.py,sha256=CpjmLuef0RQAd4Op9FbEPyFMeLmaavFZMejnMU9FbWM,4979
2
- htmlgraph/agent_detection.py,sha256=MG1kx9S-ey_Wi84hJTwwgw6VDvYEbUpygmDjGPhHcUA,3805
1
+ htmlgraph/__init__.py,sha256=leC4_bp-4lRTKLF1Vvfsi4pvj1nx_ka59W_VTDdH-6U,4979
2
+ htmlgraph/agent_detection.py,sha256=PAYo7rU3N_y1cGRd7Dwjh5Wgu-QZ7ENblX_yOzU-gJ0,2749
3
3
  htmlgraph/agent_registry.py,sha256=Usa_35by7p5gtpvHO7K3AcGimnorw-FzgPVa3cWTQ58,9448
4
4
  htmlgraph/agents.py,sha256=Yvu6x1nOfrW2WhRTAHiCuSpvqoVJXx1Mkzd59kwEczw,33466
5
5
  htmlgraph/analytics_index.py,sha256=ba6Y4H_NNOCxI_Z4U7wSgBFFairf4IJT74WcM1PoZuI,30594
6
6
  htmlgraph/attribute_index.py,sha256=cBZUV4YfGnhh6lF59aYPCdNrRr1hK__BzSKCueSDUhQ,6593
7
- htmlgraph/cli.py,sha256=-h-_7b5xRuxyLpIYHIeNbJabe3hY3_yTql_jWxfbjTU,197621
7
+ htmlgraph/cli.py,sha256=CZC631eM4toy2l7fP63EaORRATlUa5sDmJgU98bFjKU,201518
8
8
  htmlgraph/context_analytics.py,sha256=CaLu0o2uSr6rlBM5YeaFZe7grgsy7_Hx10qdXuNcdao,11344
9
- htmlgraph/converter.py,sha256=OfcydZcJqvr2jpMxvAD4wcq8o4NXC7w4X4QzdDiYq8k,22277
9
+ htmlgraph/converter.py,sha256=fhJF2h4YbrmL1wIa1jk7kGkT_CYGHeWp_5UzXitQ6HE,22747
10
10
  htmlgraph/dashboard.html,sha256=rkZYjSnPbUuAm35QMpCNWemenYqQTdkkumCX2hhe8Dc,173537
11
11
  htmlgraph/dependency_models.py,sha256=eKpBz9y_pTE5E8baESqHyGUDj5-uXokVd2Bx3ZogAyM,4313
12
12
  htmlgraph/deploy.py,sha256=kM_IMa3PmKpQf4YVH57aL9uV5IfpVJgaj-IFsgAKIbY,17771
@@ -23,13 +23,13 @@ htmlgraph/ids.py,sha256=ibEC8xW1ZHbAW6ImOKP2wLARgW7nzkxu8voce_hkljk,8389
23
23
  htmlgraph/index.d.ts,sha256=7dvExfA16g1z5Kut8xyHnSUfZ6wiUUwWNy6R7WKiwas,6922
24
24
  htmlgraph/learning.py,sha256=6SsRdz-xJGFPjp7YagpUDTZqqjNKp2wWihcnhwkHys0,28566
25
25
  htmlgraph/mcp_server.py,sha256=AeJeGJEtX5Dqu5rfhKfT5kwF2Oe8V8xCaP8BgMEh86s,24033
26
- htmlgraph/models.py,sha256=tgFFFwOuEe7Yal5I1JTZHJnF625oaHMATEGIvJgrFnk,81818
26
+ htmlgraph/models.py,sha256=vigJmC_MkKcHH362Yoauomy7fOQ0Xrn2XNASgCDEKK8,82362
27
27
  htmlgraph/orchestrator-system-prompt-optimized.txt,sha256=8UptB4-jmyz0OrjRm5VlJqBxzk3oXNJEgUQChuTWhR8,2885
28
28
  htmlgraph/orchestrator.py,sha256=6mj70vroWjmNmdvQ7jqqRSA9O1rFUNMUYDWPzqkizLk,19697
29
29
  htmlgraph/orchestrator_mode.py,sha256=F6LNZARqieQXUri3CRSq_lsqFbnVeGXJQPno1ZP47O4,9187
30
30
  htmlgraph/orchestrator_validator.py,sha256=gd_KbHsRsNEIF7EElwcxbMYqOMlyeuYIZwClASp-L-E,4699
31
31
  htmlgraph/parallel.py,sha256=BsyqGKWY_DkSRElBdvvAkWlL6stC9BPkyxjdPdggx_w,22418
32
- htmlgraph/parser.py,sha256=w6KHfg6Bd4E6_Z-XzhWIb-YgsaqIw8wmzn-mirPbZcI,15890
32
+ htmlgraph/parser.py,sha256=mPcQ3WQdDCpflhjk8397TPbHnQcU3SzefNk-JE406UA,16570
33
33
  htmlgraph/planning.py,sha256=iqPF9mCVQwOfJ4vuqcF2Y3-yhx9koJZw0cID7CknIug,35903
34
34
  htmlgraph/query_builder.py,sha256=aNtJ05GpGl9yUSSrX0D6pX_AgqlrrH-CulI_oP11PUk,18092
35
35
  htmlgraph/routing.py,sha256=QYDY6bzYPmv6kocAXCqguB1cazN0i_xTo9EVCO3fO2Y,8803
@@ -71,7 +71,7 @@ htmlgraph/builders/phase.py,sha256=pvdG_ZiswzdRCBM1pz7hOoCS9MD-lwaOgPqVcuLXucs,3
71
71
  htmlgraph/builders/spike.py,sha256=_LXMDEJCdCjJPbNB8CA0FDlFDN-pZbdzvQDFXdrldo0,4327
72
72
  htmlgraph/builders/track.py,sha256=tdolGgYQl3PvoX6jHNCq3Vh6USrb7uN3NX555SoRHGs,23097
73
73
  htmlgraph/collections/__init__.py,sha256=qHT1UvHD-jCmNI4t1fWprWLw-5jE7NabN6kngboVRFY,1056
74
- htmlgraph/collections/base.py,sha256=gOEWzOqtOSphhjzvncIl0hrQTus7rGukXZgHwubxDa0,22730
74
+ htmlgraph/collections/base.py,sha256=yK-1Kdh1wq3Ng7PkEzKX2UXlbqQl8257-9WIOfVGSmI,24721
75
75
  htmlgraph/collections/bug.py,sha256=bDbZy_sdiIn7AK0b8CXs88mUEr1ZkOGVezG0aKwh0pw,1331
76
76
  htmlgraph/collections/chore.py,sha256=PgKPjd1WkHHmZD4p4nRr8rUhC1JxfYfNWOfANxNbXqs,1352
77
77
  htmlgraph/collections/epic.py,sha256=1qJPzEHqwlfRk-4VVyKNvlZPRuGlXq68saH3tv0fZqY,1350
@@ -134,12 +134,12 @@ htmlgraph/services/claiming.py,sha256=HcrltEJKN72mxuD7fGuXWeh1U0vwhjMvhZcFc02Eiy
134
134
  htmlgraph/templates/AGENTS.md.template,sha256=f96h7V6ygwj-v-fanVI48eYMxR6t_se4bet1H4ZsDpI,7642
135
135
  htmlgraph/templates/CLAUDE.md.template,sha256=h1kG2hTX2XYig2KszsHBfzrwa_4Cfcq2Pj4SwqzeDlM,1984
136
136
  htmlgraph/templates/GEMINI.md.template,sha256=gAGzE53Avki87BM_otqy5HdcYCoLsHgqaKjVzNzPMX8,1622
137
- htmlgraph-0.23.2.data/data/htmlgraph/dashboard.html,sha256=rkZYjSnPbUuAm35QMpCNWemenYqQTdkkumCX2hhe8Dc,173537
138
- htmlgraph-0.23.2.data/data/htmlgraph/styles.css,sha256=oDUSC8jG-V-hKojOBO9J88hxAeY2wJrBYTq0uCwX_Y4,7135
139
- htmlgraph-0.23.2.data/data/htmlgraph/templates/AGENTS.md.template,sha256=f96h7V6ygwj-v-fanVI48eYMxR6t_se4bet1H4ZsDpI,7642
140
- htmlgraph-0.23.2.data/data/htmlgraph/templates/CLAUDE.md.template,sha256=h1kG2hTX2XYig2KszsHBfzrwa_4Cfcq2Pj4SwqzeDlM,1984
141
- htmlgraph-0.23.2.data/data/htmlgraph/templates/GEMINI.md.template,sha256=gAGzE53Avki87BM_otqy5HdcYCoLsHgqaKjVzNzPMX8,1622
142
- htmlgraph-0.23.2.dist-info/METADATA,sha256=sXhGvvPyc0ZtSB0FvkXu4A_gaKqs9x0uu0pS8Yb2kh8,7753
143
- htmlgraph-0.23.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
144
- htmlgraph-0.23.2.dist-info/entry_points.txt,sha256=EaUbjA_bbDwEO_XDLEGMeK8aQP-ZnHiUTkLshyKDyB8,98
145
- htmlgraph-0.23.2.dist-info/RECORD,,
137
+ htmlgraph-0.23.4.data/data/htmlgraph/dashboard.html,sha256=rkZYjSnPbUuAm35QMpCNWemenYqQTdkkumCX2hhe8Dc,173537
138
+ htmlgraph-0.23.4.data/data/htmlgraph/styles.css,sha256=oDUSC8jG-V-hKojOBO9J88hxAeY2wJrBYTq0uCwX_Y4,7135
139
+ htmlgraph-0.23.4.data/data/htmlgraph/templates/AGENTS.md.template,sha256=f96h7V6ygwj-v-fanVI48eYMxR6t_se4bet1H4ZsDpI,7642
140
+ htmlgraph-0.23.4.data/data/htmlgraph/templates/CLAUDE.md.template,sha256=h1kG2hTX2XYig2KszsHBfzrwa_4Cfcq2Pj4SwqzeDlM,1984
141
+ htmlgraph-0.23.4.data/data/htmlgraph/templates/GEMINI.md.template,sha256=gAGzE53Avki87BM_otqy5HdcYCoLsHgqaKjVzNzPMX8,1622
142
+ htmlgraph-0.23.4.dist-info/METADATA,sha256=BtfEQ59TY_LKrZZ40lLuSVriuWuXgakO3z9VFHviYjQ,7827
143
+ htmlgraph-0.23.4.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
144
+ htmlgraph-0.23.4.dist-info/entry_points.txt,sha256=EaUbjA_bbDwEO_XDLEGMeK8aQP-ZnHiUTkLshyKDyB8,98
145
+ htmlgraph-0.23.4.dist-info/RECORD,,