htmlgraph 0.23.3__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 +1 -1
- htmlgraph/agent_detection.py +2 -41
- htmlgraph/cli.py +105 -7
- htmlgraph/collections/base.py +68 -9
- htmlgraph/converter.py +12 -6
- htmlgraph/models.py +18 -0
- htmlgraph/parser.py +22 -8
- {htmlgraph-0.23.3.dist-info → htmlgraph-0.23.4.dist-info}/METADATA +2 -1
- {htmlgraph-0.23.3.dist-info → htmlgraph-0.23.4.dist-info}/RECORD +16 -16
- {htmlgraph-0.23.3.data → htmlgraph-0.23.4.data}/data/htmlgraph/dashboard.html +0 -0
- {htmlgraph-0.23.3.data → htmlgraph-0.23.4.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.23.3.data → htmlgraph-0.23.4.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
- {htmlgraph-0.23.3.data → htmlgraph-0.23.4.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
- {htmlgraph-0.23.3.data → htmlgraph-0.23.4.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
- {htmlgraph-0.23.3.dist-info → htmlgraph-0.23.4.dist-info}/WHEEL +0 -0
- {htmlgraph-0.23.3.dist-info → htmlgraph-0.23.4.dist-info}/entry_points.txt +0 -0
htmlgraph/__init__.py
CHANGED
htmlgraph/agent_detection.py
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
|
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",
|
|
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
|
|
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",
|
|
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(
|
|
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()
|
htmlgraph/collections/base.py
CHANGED
|
@@ -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
|
-
"""
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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
|
|
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
|
-
|
|
609
|
+
Mark a node as complete.
|
|
570
610
|
|
|
571
|
-
Delegates to SessionManager
|
|
572
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:"):
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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=
|
|
2
|
-
htmlgraph/agent_detection.py,sha256=
|
|
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
|
|
7
|
+
htmlgraph/cli.py,sha256=CZC631eM4toy2l7fP63EaORRATlUa5sDmJgU98bFjKU,201518
|
|
8
8
|
htmlgraph/context_analytics.py,sha256=CaLu0o2uSr6rlBM5YeaFZe7grgsy7_Hx10qdXuNcdao,11344
|
|
9
|
-
htmlgraph/converter.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
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.
|
|
138
|
-
htmlgraph-0.23.
|
|
139
|
-
htmlgraph-0.23.
|
|
140
|
-
htmlgraph-0.23.
|
|
141
|
-
htmlgraph-0.23.
|
|
142
|
-
htmlgraph-0.23.
|
|
143
|
-
htmlgraph-0.23.
|
|
144
|
-
htmlgraph-0.23.
|
|
145
|
-
htmlgraph-0.23.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|