htmlgraph 0.21.0__py3-none-any.whl → 0.23.0__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 (40) 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 +519 -87
  5. htmlgraph/collections/base.py +68 -4
  6. htmlgraph/docs/__init__.py +77 -0
  7. htmlgraph/docs/docs_version.py +55 -0
  8. htmlgraph/docs/metadata.py +93 -0
  9. htmlgraph/docs/migrations.py +232 -0
  10. htmlgraph/docs/template_engine.py +143 -0
  11. htmlgraph/docs/templates/_sections/cli_reference.md.j2 +52 -0
  12. htmlgraph/docs/templates/_sections/core_concepts.md.j2 +29 -0
  13. htmlgraph/docs/templates/_sections/sdk_basics.md.j2 +69 -0
  14. htmlgraph/docs/templates/base_agents.md.j2 +78 -0
  15. htmlgraph/docs/templates/example_user_override.md.j2 +47 -0
  16. htmlgraph/docs/version_check.py +161 -0
  17. htmlgraph/git_events.py +61 -7
  18. htmlgraph/operations/README.md +62 -0
  19. htmlgraph/operations/__init__.py +61 -0
  20. htmlgraph/operations/analytics.py +338 -0
  21. htmlgraph/operations/events.py +243 -0
  22. htmlgraph/operations/hooks.py +349 -0
  23. htmlgraph/operations/server.py +302 -0
  24. htmlgraph/orchestration/__init__.py +39 -0
  25. htmlgraph/orchestration/headless_spawner.py +566 -0
  26. htmlgraph/orchestration/model_selection.py +323 -0
  27. htmlgraph/orchestrator-system-prompt-optimized.txt +47 -0
  28. htmlgraph/parser.py +56 -1
  29. htmlgraph/sdk.py +529 -7
  30. htmlgraph/server.py +153 -60
  31. {htmlgraph-0.21.0.dist-info → htmlgraph-0.23.0.dist-info}/METADATA +3 -1
  32. {htmlgraph-0.21.0.dist-info → htmlgraph-0.23.0.dist-info}/RECORD +40 -19
  33. /htmlgraph/{orchestration.py → orchestration/task_coordination.py} +0 -0
  34. {htmlgraph-0.21.0.data → htmlgraph-0.23.0.data}/data/htmlgraph/dashboard.html +0 -0
  35. {htmlgraph-0.21.0.data → htmlgraph-0.23.0.data}/data/htmlgraph/styles.css +0 -0
  36. {htmlgraph-0.21.0.data → htmlgraph-0.23.0.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
  37. {htmlgraph-0.21.0.data → htmlgraph-0.23.0.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
  38. {htmlgraph-0.21.0.data → htmlgraph-0.23.0.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
  39. {htmlgraph-0.21.0.dist-info → htmlgraph-0.23.0.dist-info}/WHEEL +0 -0
  40. {htmlgraph-0.21.0.dist-info → htmlgraph-0.23.0.dist-info}/entry_points.txt +0 -0
@@ -91,6 +91,48 @@ class BaseCollection(Generic[CollectionT]):
91
91
 
92
92
  return self._graph
93
93
 
94
+ def __getattribute__(self, name: str) -> Any:
95
+ """Override to provide helpful error messages for missing attributes."""
96
+ try:
97
+ return object.__getattribute__(self, name)
98
+ except AttributeError as e:
99
+ # Get available methods
100
+ available = [m for m in dir(self) if not m.startswith("_")]
101
+
102
+ # Common mistakes mapping
103
+ common_mistakes = {
104
+ "mark_complete": "mark_done",
105
+ "complete": "Use complete(node_id) for single item or mark_done([ids]) for batch",
106
+ "finish": "mark_done",
107
+ "end": "mark_done",
108
+ "update_status": "edit() context manager or batch_update()",
109
+ "mark_as_done": "mark_done",
110
+ "set_done": "mark_done",
111
+ "complete_all": "mark_done",
112
+ }
113
+
114
+ suggestions = []
115
+ if name in common_mistakes:
116
+ suggestions.append(f"Did you mean: {common_mistakes[name]}")
117
+
118
+ # Find similar method names
119
+ similar = [
120
+ m
121
+ for m in available
122
+ if name.lower() in m.lower() or m.lower() in name.lower()
123
+ ]
124
+ if similar:
125
+ suggestions.append(f"Similar methods: {', '.join(similar[:5])}")
126
+
127
+ # Build helpful error message
128
+ error_msg = f"'{type(self).__name__}' has no attribute '{name}'."
129
+ if suggestions:
130
+ error_msg += "\n\n" + "\n".join(suggestions)
131
+ error_msg += f"\n\nAvailable methods: {', '.join(available[:15])}"
132
+ error_msg += "\n\nTip: Use sdk.help() to see all available operations."
133
+
134
+ raise AttributeError(error_msg) from e
135
+
94
136
  def __dir__(self) -> list[str]:
95
137
  """Return attributes with most useful ones first for discoverability."""
96
138
  priority = [
@@ -418,7 +460,7 @@ class BaseCollection(Generic[CollectionT]):
418
460
 
419
461
  return count
420
462
 
421
- def mark_done(self, node_ids: list[str]) -> int:
463
+ def mark_done(self, node_ids: list[str]) -> dict[str, Any]:
422
464
  """
423
465
  Batch mark nodes as done.
424
466
 
@@ -426,12 +468,34 @@ class BaseCollection(Generic[CollectionT]):
426
468
  node_ids: List of node IDs to mark as done
427
469
 
428
470
  Returns:
429
- Number of nodes updated
471
+ Dict with 'success_count', 'failed_ids', and 'warnings'
430
472
 
431
473
  Example:
432
- >>> sdk.features.mark_done(["feat-001", "feat-002"])
474
+ >>> result = sdk.features.mark_done(["feat-001", "feat-002"])
475
+ >>> print(f"Completed {result['success_count']} of {len(node_ids)}")
476
+ >>> if result['failed_ids']:
477
+ ... print(f"Failed: {result['failed_ids']}")
433
478
  """
434
- return self.batch_update(node_ids, {"status": "done"})
479
+ graph = self._ensure_graph()
480
+ results: dict[str, Any] = {"success_count": 0, "failed_ids": [], "warnings": []}
481
+
482
+ for node_id in node_ids:
483
+ try:
484
+ node = graph.get(node_id)
485
+ if not node:
486
+ results["failed_ids"].append(node_id)
487
+ results["warnings"].append(f"Node {node_id} not found")
488
+ continue
489
+
490
+ node.status = "done"
491
+ node.updated = datetime.now()
492
+ graph.update(node)
493
+ results["success_count"] += 1
494
+ except Exception as e:
495
+ results["failed_ids"].append(node_id)
496
+ results["warnings"].append(f"Failed to mark {node_id}: {str(e)}")
497
+
498
+ return results
435
499
 
436
500
  def assign(self, node_ids: list[str], agent: str) -> int:
437
501
  """
@@ -0,0 +1,77 @@
1
+ """
2
+ Documentation version tracking and migration system with template-based user customization.
3
+ """
4
+
5
+ from pathlib import Path
6
+
7
+ from htmlgraph import __version__
8
+ from htmlgraph.docs.docs_version import (
9
+ DOC_VERSIONS,
10
+ DocVersion,
11
+ get_current_doc_version,
12
+ is_compatible,
13
+ )
14
+ from htmlgraph.docs.metadata import DocsMetadata
15
+ from htmlgraph.docs.migrations import MIGRATIONS, MigrationScript, get_migration
16
+ from htmlgraph.docs.template_engine import DocTemplateEngine
17
+ from htmlgraph.docs.version_check import check_docs_version, upgrade_docs_interactive
18
+
19
+ __all__ = [
20
+ "DOC_VERSIONS",
21
+ "DocVersion",
22
+ "get_current_doc_version",
23
+ "is_compatible",
24
+ "DocsMetadata",
25
+ "MigrationScript",
26
+ "MIGRATIONS",
27
+ "get_migration",
28
+ "check_docs_version",
29
+ "upgrade_docs_interactive",
30
+ "DocTemplateEngine",
31
+ "get_agents_md",
32
+ "sync_docs_to_file",
33
+ ]
34
+
35
+
36
+ def get_agents_md(htmlgraph_dir: Path, platform: str = "claude") -> str:
37
+ """Get AGENTS.md content with user customizations merged.
38
+
39
+ Args:
40
+ htmlgraph_dir: Path to .htmlgraph directory
41
+ platform: Platform name (claude, gemini, etc.)
42
+
43
+ Returns:
44
+ Merged documentation content
45
+
46
+ Example:
47
+ >>> from pathlib import Path
48
+ >>> content = get_agents_md(Path(".htmlgraph"), "claude")
49
+ """
50
+ engine = DocTemplateEngine(htmlgraph_dir)
51
+ return engine.render_agents_md(__version__, platform)
52
+
53
+
54
+ def sync_docs_to_file(
55
+ htmlgraph_dir: Path, output_file: Path, platform: str = "claude"
56
+ ) -> Path:
57
+ """Generate and write documentation to file.
58
+
59
+ Args:
60
+ htmlgraph_dir: Path to .htmlgraph directory
61
+ output_file: Path where documentation should be written
62
+ platform: Platform name (claude, gemini, etc.)
63
+
64
+ Returns:
65
+ Path to written file
66
+
67
+ Example:
68
+ >>> from pathlib import Path
69
+ >>> sync_docs_to_file(
70
+ ... Path(".htmlgraph"),
71
+ ... Path("AGENTS.md"),
72
+ ... "claude"
73
+ ... )
74
+ """
75
+ content = get_agents_md(htmlgraph_dir, platform)
76
+ output_file.write_text(content)
77
+ return output_file
@@ -0,0 +1,55 @@
1
+ """
2
+ Documentation schema version management.
3
+
4
+ This module defines version compatibility rules for HtmlGraph documentation files.
5
+ """
6
+
7
+ from dataclasses import dataclass
8
+
9
+
10
+ @dataclass
11
+ class DocVersion:
12
+ """Documentation schema version."""
13
+
14
+ version: int # Schema version (1, 2, 3)
15
+ package_version: str # Minimum package version (e.g., "0.20.0")
16
+ breaking_changes: list[str]
17
+ migration_required: bool = False
18
+
19
+
20
+ # Version compatibility matrix
21
+ DOC_VERSIONS = {
22
+ 1: DocVersion(
23
+ version=1,
24
+ package_version="0.1.0",
25
+ breaking_changes=[],
26
+ migration_required=False,
27
+ ),
28
+ 2: DocVersion(
29
+ version=2,
30
+ package_version="0.20.0",
31
+ breaking_changes=[
32
+ "AGENTS.md structure changed to use Jinja2 templates",
33
+ "Removed root-level CLAUDE.md/GEMINI.md (moved to .htmlgraph/docs/)",
34
+ ],
35
+ migration_required=True,
36
+ ),
37
+ }
38
+
39
+
40
+ def get_current_doc_version() -> int:
41
+ """Get current documentation schema version for this package."""
42
+ return 2 # Current version
43
+
44
+
45
+ def is_compatible(user_version: int, package_version: int) -> bool:
46
+ """Check if user's doc version is compatible with package.
47
+
48
+ Args:
49
+ user_version: User's documentation schema version
50
+ package_version: Package's required documentation schema version
51
+
52
+ Returns:
53
+ True if compatible (supports N-1 versions)
54
+ """
55
+ return user_version >= package_version - 1 # Support N-1 versions
@@ -0,0 +1,93 @@
1
+ """
2
+ Documentation metadata storage and management.
3
+ """
4
+
5
+ from datetime import datetime
6
+ from pathlib import Path
7
+
8
+ from pydantic import BaseModel, Field
9
+
10
+
11
+ class DocsMetadata(BaseModel):
12
+ """Metadata for project documentation."""
13
+
14
+ schema_version: int = 2
15
+ last_updated: datetime = Field(default_factory=datetime.now)
16
+ customizations: list[str] = [] # List of user-customized sections
17
+ base_version_on_last_update: str = "0.21.0"
18
+
19
+ @classmethod
20
+ def load(cls, htmlgraph_dir: Path) -> "DocsMetadata":
21
+ """Load metadata from .docs-metadata.json.
22
+
23
+ Args:
24
+ htmlgraph_dir: Path to .htmlgraph directory
25
+
26
+ Returns:
27
+ DocsMetadata instance (default if file doesn't exist)
28
+ """
29
+ import json
30
+
31
+ metadata_file = htmlgraph_dir / ".docs-metadata.json"
32
+ if metadata_file.exists():
33
+ data = json.loads(metadata_file.read_text())
34
+ return cls(**data)
35
+ return cls() # Default
36
+
37
+ def save(self, htmlgraph_dir: Path) -> None:
38
+ """Save metadata to .docs-metadata.json.
39
+
40
+ Args:
41
+ htmlgraph_dir: Path to .htmlgraph directory
42
+ """
43
+ metadata_file = htmlgraph_dir / ".docs-metadata.json"
44
+ metadata_file.write_text(self.model_dump_json(indent=2))
45
+
46
+ @classmethod
47
+ def create_initial(
48
+ cls, htmlgraph_dir: Path, schema_version: int = 2
49
+ ) -> "DocsMetadata":
50
+ """Create initial metadata file.
51
+
52
+ Args:
53
+ htmlgraph_dir: Path to .htmlgraph directory
54
+ schema_version: Documentation schema version
55
+
56
+ Returns:
57
+ New DocsMetadata instance
58
+ """
59
+ from htmlgraph import __version__
60
+
61
+ metadata = cls(
62
+ schema_version=schema_version,
63
+ base_version_on_last_update=__version__,
64
+ customizations=[],
65
+ )
66
+ metadata.save(htmlgraph_dir)
67
+ return metadata
68
+
69
+ def add_customization(self, section_name: str) -> None:
70
+ """Mark a section as customized.
71
+
72
+ Args:
73
+ section_name: Name of the customized section
74
+ """
75
+ if section_name not in self.customizations:
76
+ self.customizations.append(section_name)
77
+
78
+ def remove_customization(self, section_name: str) -> None:
79
+ """Remove a customization marker.
80
+
81
+ Args:
82
+ section_name: Name of the section to unmark
83
+ """
84
+ if section_name in self.customizations:
85
+ self.customizations.remove(section_name)
86
+
87
+ def has_customizations(self) -> bool:
88
+ """Check if any customizations exist.
89
+
90
+ Returns:
91
+ True if user has customized documentation
92
+ """
93
+ return len(self.customizations) > 0
@@ -0,0 +1,232 @@
1
+ """
2
+ Documentation migration system.
3
+
4
+ Handles version migrations with automatic customization preservation.
5
+ """
6
+
7
+ import shutil
8
+ from abc import ABC, abstractmethod
9
+ from datetime import datetime
10
+ from pathlib import Path
11
+
12
+ from htmlgraph.docs.metadata import DocsMetadata
13
+
14
+
15
+ class MigrationScript(ABC):
16
+ """Base class for documentation migrations."""
17
+
18
+ from_version: int
19
+ to_version: int
20
+
21
+ @abstractmethod
22
+ def migrate(self, htmlgraph_dir: Path, backup_dir: Path) -> bool:
23
+ """Execute migration. Returns True if successful.
24
+
25
+ Args:
26
+ htmlgraph_dir: Path to .htmlgraph directory
27
+ backup_dir: Path to backup directory
28
+
29
+ Returns:
30
+ True if migration successful
31
+ """
32
+ pass
33
+
34
+ @abstractmethod
35
+ def rollback(self, htmlgraph_dir: Path, backup_dir: Path) -> None:
36
+ """Rollback migration to previous state.
37
+
38
+ Args:
39
+ htmlgraph_dir: Path to .htmlgraph directory
40
+ backup_dir: Path to backup directory
41
+ """
42
+ pass
43
+
44
+ def backup_docs(self, htmlgraph_dir: Path, backup_dir: Path) -> Path:
45
+ """Backup current documentation before migration.
46
+
47
+ Args:
48
+ htmlgraph_dir: Path to .htmlgraph directory
49
+ backup_dir: Path to backup directory
50
+
51
+ Returns:
52
+ Path to created backup subdirectory
53
+ """
54
+ backup_subdir = (
55
+ backup_dir / f"v{self.from_version}_{datetime.now():%Y%m%d_%H%M%S}"
56
+ )
57
+ backup_subdir.mkdir(parents=True, exist_ok=True)
58
+
59
+ # Backup all docs
60
+ docs_dir = htmlgraph_dir / "docs"
61
+ if docs_dir.exists():
62
+ for doc_file in docs_dir.glob("*.md"):
63
+ shutil.copy2(doc_file, backup_subdir / doc_file.name)
64
+
65
+ # Also backup root-level docs (legacy)
66
+ for doc_file in htmlgraph_dir.glob("*.md"):
67
+ shutil.copy2(doc_file, backup_subdir / doc_file.name)
68
+
69
+ return backup_subdir
70
+
71
+
72
+ class V1toV2Migration(MigrationScript):
73
+ """Migrate from v1 (root-level docs) to v2 (template-based docs)."""
74
+
75
+ from_version = 1
76
+ to_version = 2
77
+
78
+ def migrate(self, htmlgraph_dir: Path, backup_dir: Path) -> bool:
79
+ """Migrate v1 docs to v2 template structure.
80
+
81
+ Args:
82
+ htmlgraph_dir: Path to .htmlgraph directory
83
+ backup_dir: Path to backup directory
84
+
85
+ Returns:
86
+ True if migration successful
87
+ """
88
+ # Backup first
89
+ backup_path = self.backup_docs(htmlgraph_dir, backup_dir)
90
+ print(f"✅ Backed up to {backup_path}")
91
+
92
+ # Detect user customizations in old AGENTS.md
93
+ customizations = self._detect_customizations(htmlgraph_dir / "AGENTS.md")
94
+ if customizations:
95
+ print(f"📝 Detected customizations: {', '.join(customizations)}")
96
+
97
+ # Move old docs to archive
98
+ old_agents_md = htmlgraph_dir / "AGENTS.md"
99
+ if old_agents_md.exists():
100
+ old_agents_md.rename(htmlgraph_dir / "AGENTS.md.v1.backup")
101
+ print("📦 Archived old AGENTS.md")
102
+
103
+ # Generate new v2 docs with customizations preserved
104
+ self._generate_v2_docs(htmlgraph_dir, customizations)
105
+ print("✨ Generated v2 documentation")
106
+
107
+ # Update metadata
108
+ metadata = DocsMetadata.load(htmlgraph_dir)
109
+ metadata.schema_version = 2
110
+ metadata.customizations = customizations
111
+ metadata.save(htmlgraph_dir)
112
+ print("💾 Updated metadata")
113
+
114
+ return True
115
+
116
+ def _detect_customizations(self, agents_md_path: Path) -> list[str]:
117
+ """Detect user-customized sections in old AGENTS.md.
118
+
119
+ Args:
120
+ agents_md_path: Path to AGENTS.md file
121
+
122
+ Returns:
123
+ List of customized section names
124
+ """
125
+ if not agents_md_path.exists():
126
+ return []
127
+
128
+ content = agents_md_path.read_text()
129
+ customizations = []
130
+
131
+ # Simple heuristic: sections not in base template
132
+ if "## Our Team" in content:
133
+ customizations.append("custom_workflows")
134
+ if "## Project Conventions" in content:
135
+ customizations.append("project_conventions")
136
+ if "## Custom Workflows" in content:
137
+ customizations.append("custom_workflows")
138
+
139
+ return customizations
140
+
141
+ def _generate_v2_docs(self, htmlgraph_dir: Path, customizations: list[str]) -> None:
142
+ """Generate v2 template-based docs with customizations.
143
+
144
+ Args:
145
+ htmlgraph_dir: Path to .htmlgraph directory
146
+ customizations: List of customized sections to preserve
147
+ """
148
+ # NOTE: This would integrate with sync_docs.py
149
+ # For now, just create placeholder
150
+ docs_dir = htmlgraph_dir / "docs"
151
+ docs_dir.mkdir(exist_ok=True)
152
+
153
+ placeholder = docs_dir / "AGENTS.md"
154
+ placeholder.write_text(
155
+ """# HtmlGraph Agent Documentation (v2)
156
+
157
+ This file was migrated from v1 to v2.
158
+
159
+ Run `uv run htmlgraph sync-docs` to regenerate from templates.
160
+ """
161
+ )
162
+
163
+ # Inject customizations as template overrides if needed
164
+ if customizations:
165
+ self._create_override_template(htmlgraph_dir, customizations)
166
+
167
+ def _create_override_template(
168
+ self, htmlgraph_dir: Path, customizations: list[str]
169
+ ) -> None:
170
+ """Create template overrides for customizations.
171
+
172
+ Args:
173
+ htmlgraph_dir: Path to .htmlgraph directory
174
+ customizations: List of customizations to preserve
175
+ """
176
+ overrides_file = htmlgraph_dir / "docs" / "overrides.md"
177
+ overrides_file.write_text(
178
+ f"""# Documentation Customizations
179
+
180
+ The following sections were customized in v1:
181
+
182
+ {chr(10).join(f"- {c}" for c in customizations)}
183
+
184
+ To preserve these customizations, add them back manually after running sync-docs.
185
+ """
186
+ )
187
+
188
+ def rollback(self, htmlgraph_dir: Path, backup_dir: Path) -> None:
189
+ """Rollback to v1 docs from backup.
190
+
191
+ Args:
192
+ htmlgraph_dir: Path to .htmlgraph directory
193
+ backup_dir: Path to backup directory
194
+ """
195
+ # Find latest backup
196
+ backups = sorted(backup_dir.glob("v1_*"))
197
+ if not backups:
198
+ raise ValueError("No backup found for rollback")
199
+
200
+ latest_backup = backups[-1]
201
+ print(f"🔄 Rolling back from {latest_backup}")
202
+
203
+ # Restore old AGENTS.md
204
+ backup_agents = latest_backup / "AGENTS.md"
205
+ if backup_agents.exists():
206
+ shutil.copy2(backup_agents, htmlgraph_dir / "AGENTS.md")
207
+ print("✅ Restored AGENTS.md")
208
+
209
+ # Update metadata
210
+ metadata = DocsMetadata.load(htmlgraph_dir)
211
+ metadata.schema_version = 1
212
+ metadata.save(htmlgraph_dir)
213
+ print("💾 Updated metadata to v1")
214
+
215
+
216
+ # Migration registry
217
+ MIGRATIONS: dict[tuple[int, int], MigrationScript] = {
218
+ (1, 2): V1toV2Migration(),
219
+ }
220
+
221
+
222
+ def get_migration(from_version: int, to_version: int) -> MigrationScript | None:
223
+ """Get migration script for version transition.
224
+
225
+ Args:
226
+ from_version: Source schema version
227
+ to_version: Target schema version
228
+
229
+ Returns:
230
+ MigrationScript instance or None if no migration exists
231
+ """
232
+ return MIGRATIONS.get((from_version, to_version))
@@ -0,0 +1,143 @@
1
+ """Jinja2-based template engine for documentation with user customization."""
2
+
3
+ from datetime import datetime
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from jinja2 import ChoiceLoader, Environment, FileSystemLoader
8
+
9
+
10
+ class DocTemplateEngine:
11
+ """Renders documentation templates with user customization support.
12
+
13
+ Template Priority:
14
+ 1. User templates in .htmlgraph/docs/templates/ (highest priority)
15
+ 2. Package templates in htmlgraph/docs/templates/ (fallback)
16
+
17
+ Example:
18
+ >>> engine = DocTemplateEngine(Path(".htmlgraph"))
19
+ >>> content = engine.render_agents_md("0.21.0", "claude")
20
+ >>> print(content)
21
+ """
22
+
23
+ def __init__(self, htmlgraph_dir: Path):
24
+ """Initialize template engine with multi-loader.
25
+
26
+ Args:
27
+ htmlgraph_dir: Path to .htmlgraph directory
28
+ """
29
+ # Package templates (bundled with pip install)
30
+ package_templates = Path(__file__).parent / "templates"
31
+
32
+ # User templates (project-specific customizations)
33
+ user_templates = htmlgraph_dir / "docs" / "templates"
34
+
35
+ # Multi-loader: User templates have priority over package templates
36
+ loaders = []
37
+ if user_templates.exists():
38
+ loaders.append(FileSystemLoader(str(user_templates)))
39
+ loaders.append(FileSystemLoader(str(package_templates)))
40
+
41
+ self.env = Environment(loader=ChoiceLoader(loaders))
42
+
43
+ # Add custom filters
44
+ self.env.filters["format_date"] = self._format_date
45
+ self.env.filters["highlight_code"] = self._highlight_code
46
+
47
+ def render(self, template_name: str, context: dict[str, Any]) -> str:
48
+ """Render template with context, merging user overrides.
49
+
50
+ Args:
51
+ template_name: Name of template file (e.g., "agents.md.j2")
52
+ context: Template variables
53
+
54
+ Returns:
55
+ Rendered content with user customizations applied
56
+
57
+ Example:
58
+ >>> engine.render("agents.md.j2", {"platform": "claude"})
59
+ """
60
+ template = self.env.get_template(template_name)
61
+ return template.render(**context)
62
+
63
+ def render_agents_md(self, sdk_version: str, platform: str = "claude") -> str:
64
+ """Render AGENTS.md with platform-specific customizations.
65
+
66
+ Args:
67
+ sdk_version: HtmlGraph SDK version (e.g., "0.21.0")
68
+ platform: Platform name (claude, gemini, api, etc.)
69
+
70
+ Returns:
71
+ Rendered AGENTS.md content
72
+
73
+ Example:
74
+ >>> content = engine.render_agents_md("0.21.0", "claude")
75
+ """
76
+ context = {
77
+ "sdk_version": sdk_version,
78
+ "platform": platform,
79
+ "features_enabled": self._get_enabled_features(),
80
+ "custom_workflows": self._load_custom_workflows(),
81
+ "generated_at": datetime.now().isoformat(),
82
+ }
83
+ # User templates should extend "base_agents.md.j2" to avoid recursion
84
+ # Priority: agents.md.j2 (user override) → base_agents.md.j2 (package default)
85
+ template_name = "agents.md.j2"
86
+ try:
87
+ # Try to get user template first (will succeed if it exists in user dir)
88
+ self.env.get_template(template_name)
89
+ return self.render(template_name, context)
90
+ except: # noqa
91
+ # Fall back to base template (always exists in package)
92
+ return self.render("base_agents.md.j2", context)
93
+
94
+ def _format_date(self, value: str) -> str:
95
+ """Format ISO date string for display.
96
+
97
+ Args:
98
+ value: ISO 8601 date string
99
+
100
+ Returns:
101
+ Formatted date string
102
+ """
103
+ try:
104
+ dt = datetime.fromisoformat(value)
105
+ return dt.strftime("%Y-%m-%d %H:%M")
106
+ except (ValueError, AttributeError):
107
+ return value
108
+
109
+ def _highlight_code(self, code: str, language: str = "python") -> str:
110
+ """Add markdown code fence with syntax highlighting.
111
+
112
+ Args:
113
+ code: Code snippet
114
+ language: Programming language for syntax highlighting
115
+
116
+ Returns:
117
+ Markdown code block
118
+ """
119
+ return f"```{language}\n{code}\n```"
120
+
121
+ def _get_enabled_features(self) -> dict[str, bool]:
122
+ """Get enabled features for this installation.
123
+
124
+ Returns:
125
+ Dictionary of feature flags
126
+ """
127
+ # TODO: Read from config or detect dynamically
128
+ return {
129
+ "sessions": True,
130
+ "tracks": True,
131
+ "analytics": True,
132
+ "mcp": True,
133
+ "cli": True,
134
+ }
135
+
136
+ def _load_custom_workflows(self) -> str | None:
137
+ """Load custom workflows from user template.
138
+
139
+ Returns:
140
+ Custom workflow markdown or None
141
+ """
142
+ # TODO: Load from .htmlgraph/docs/workflows.md if exists
143
+ return None