htmlgraph 0.20.1__py3-none-any.whl → 0.27.5__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 (304) hide show
  1. htmlgraph/.htmlgraph/.session-warning-state.json +6 -0
  2. htmlgraph/.htmlgraph/agents.json +72 -0
  3. htmlgraph/.htmlgraph/htmlgraph.db +0 -0
  4. htmlgraph/__init__.py +51 -1
  5. htmlgraph/__init__.pyi +123 -0
  6. htmlgraph/agent_detection.py +26 -10
  7. htmlgraph/agent_registry.py +2 -1
  8. htmlgraph/analytics/__init__.py +8 -1
  9. htmlgraph/analytics/cli.py +86 -20
  10. htmlgraph/analytics/cost_analyzer.py +391 -0
  11. htmlgraph/analytics/cost_monitor.py +664 -0
  12. htmlgraph/analytics/cost_reporter.py +675 -0
  13. htmlgraph/analytics/cross_session.py +617 -0
  14. htmlgraph/analytics/dependency.py +10 -6
  15. htmlgraph/analytics/pattern_learning.py +771 -0
  16. htmlgraph/analytics/session_graph.py +707 -0
  17. htmlgraph/analytics/strategic/__init__.py +80 -0
  18. htmlgraph/analytics/strategic/cost_optimizer.py +611 -0
  19. htmlgraph/analytics/strategic/pattern_detector.py +876 -0
  20. htmlgraph/analytics/strategic/preference_manager.py +709 -0
  21. htmlgraph/analytics/strategic/suggestion_engine.py +747 -0
  22. htmlgraph/analytics/work_type.py +67 -27
  23. htmlgraph/analytics_index.py +53 -20
  24. htmlgraph/api/__init__.py +3 -0
  25. htmlgraph/api/cost_alerts_websocket.py +416 -0
  26. htmlgraph/api/main.py +2498 -0
  27. htmlgraph/api/static/htmx.min.js +1 -0
  28. htmlgraph/api/static/style-redesign.css +1344 -0
  29. htmlgraph/api/static/style.css +1079 -0
  30. htmlgraph/api/templates/dashboard-redesign.html +1366 -0
  31. htmlgraph/api/templates/dashboard.html +794 -0
  32. htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
  33. htmlgraph/api/templates/partials/activity-feed.html +1100 -0
  34. htmlgraph/api/templates/partials/agents-redesign.html +317 -0
  35. htmlgraph/api/templates/partials/agents.html +317 -0
  36. htmlgraph/api/templates/partials/event-traces.html +373 -0
  37. htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
  38. htmlgraph/api/templates/partials/features.html +578 -0
  39. htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
  40. htmlgraph/api/templates/partials/metrics.html +346 -0
  41. htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
  42. htmlgraph/api/templates/partials/orchestration.html +198 -0
  43. htmlgraph/api/templates/partials/spawners.html +375 -0
  44. htmlgraph/api/templates/partials/work-items.html +613 -0
  45. htmlgraph/api/websocket.py +538 -0
  46. htmlgraph/archive/__init__.py +24 -0
  47. htmlgraph/archive/bloom.py +234 -0
  48. htmlgraph/archive/fts.py +297 -0
  49. htmlgraph/archive/manager.py +583 -0
  50. htmlgraph/archive/search.py +244 -0
  51. htmlgraph/atomic_ops.py +560 -0
  52. htmlgraph/attribute_index.py +2 -1
  53. htmlgraph/bounded_paths.py +539 -0
  54. htmlgraph/builders/base.py +57 -2
  55. htmlgraph/builders/bug.py +19 -3
  56. htmlgraph/builders/chore.py +19 -3
  57. htmlgraph/builders/epic.py +19 -3
  58. htmlgraph/builders/feature.py +27 -3
  59. htmlgraph/builders/insight.py +2 -1
  60. htmlgraph/builders/metric.py +2 -1
  61. htmlgraph/builders/pattern.py +2 -1
  62. htmlgraph/builders/phase.py +19 -3
  63. htmlgraph/builders/spike.py +29 -3
  64. htmlgraph/builders/track.py +42 -1
  65. htmlgraph/cigs/__init__.py +81 -0
  66. htmlgraph/cigs/autonomy.py +385 -0
  67. htmlgraph/cigs/cost.py +475 -0
  68. htmlgraph/cigs/messages_basic.py +472 -0
  69. htmlgraph/cigs/messaging.py +365 -0
  70. htmlgraph/cigs/models.py +771 -0
  71. htmlgraph/cigs/pattern_storage.py +427 -0
  72. htmlgraph/cigs/patterns.py +503 -0
  73. htmlgraph/cigs/posttool_analyzer.py +234 -0
  74. htmlgraph/cigs/reporter.py +818 -0
  75. htmlgraph/cigs/tracker.py +317 -0
  76. htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
  77. htmlgraph/cli/.htmlgraph/agents.json +72 -0
  78. htmlgraph/cli/.htmlgraph/htmlgraph.db +0 -0
  79. htmlgraph/cli/__init__.py +42 -0
  80. htmlgraph/cli/__main__.py +6 -0
  81. htmlgraph/cli/analytics.py +1424 -0
  82. htmlgraph/cli/base.py +685 -0
  83. htmlgraph/cli/constants.py +206 -0
  84. htmlgraph/cli/core.py +954 -0
  85. htmlgraph/cli/main.py +147 -0
  86. htmlgraph/cli/models.py +475 -0
  87. htmlgraph/cli/templates/__init__.py +1 -0
  88. htmlgraph/cli/templates/cost_dashboard.py +399 -0
  89. htmlgraph/cli/work/__init__.py +239 -0
  90. htmlgraph/cli/work/browse.py +115 -0
  91. htmlgraph/cli/work/features.py +568 -0
  92. htmlgraph/cli/work/orchestration.py +676 -0
  93. htmlgraph/cli/work/report.py +728 -0
  94. htmlgraph/cli/work/sessions.py +466 -0
  95. htmlgraph/cli/work/snapshot.py +559 -0
  96. htmlgraph/cli/work/tracks.py +486 -0
  97. htmlgraph/cli_commands/__init__.py +1 -0
  98. htmlgraph/cli_commands/feature.py +195 -0
  99. htmlgraph/cli_framework.py +115 -0
  100. htmlgraph/collections/__init__.py +2 -0
  101. htmlgraph/collections/base.py +197 -14
  102. htmlgraph/collections/bug.py +2 -1
  103. htmlgraph/collections/chore.py +2 -1
  104. htmlgraph/collections/epic.py +2 -1
  105. htmlgraph/collections/feature.py +2 -1
  106. htmlgraph/collections/insight.py +2 -1
  107. htmlgraph/collections/metric.py +2 -1
  108. htmlgraph/collections/pattern.py +2 -1
  109. htmlgraph/collections/phase.py +2 -1
  110. htmlgraph/collections/session.py +194 -0
  111. htmlgraph/collections/spike.py +13 -2
  112. htmlgraph/collections/task_delegation.py +241 -0
  113. htmlgraph/collections/todo.py +14 -1
  114. htmlgraph/collections/traces.py +487 -0
  115. htmlgraph/config/cost_models.json +56 -0
  116. htmlgraph/config.py +190 -0
  117. htmlgraph/context_analytics.py +2 -1
  118. htmlgraph/converter.py +116 -7
  119. htmlgraph/cost_analysis/__init__.py +5 -0
  120. htmlgraph/cost_analysis/analyzer.py +438 -0
  121. htmlgraph/dashboard.html +2246 -248
  122. htmlgraph/dashboard.html.backup +6592 -0
  123. htmlgraph/dashboard.html.bak +7181 -0
  124. htmlgraph/dashboard.html.bak2 +7231 -0
  125. htmlgraph/dashboard.html.bak3 +7232 -0
  126. htmlgraph/db/__init__.py +38 -0
  127. htmlgraph/db/queries.py +790 -0
  128. htmlgraph/db/schema.py +1788 -0
  129. htmlgraph/decorators.py +317 -0
  130. htmlgraph/dependency_models.py +2 -1
  131. htmlgraph/deploy.py +26 -27
  132. htmlgraph/docs/API_REFERENCE.md +841 -0
  133. htmlgraph/docs/HTTP_API.md +750 -0
  134. htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
  135. htmlgraph/docs/ORCHESTRATION_PATTERNS.md +717 -0
  136. htmlgraph/docs/README.md +532 -0
  137. htmlgraph/docs/__init__.py +77 -0
  138. htmlgraph/docs/docs_version.py +55 -0
  139. htmlgraph/docs/metadata.py +93 -0
  140. htmlgraph/docs/migrations.py +232 -0
  141. htmlgraph/docs/template_engine.py +143 -0
  142. htmlgraph/docs/templates/_sections/cli_reference.md.j2 +52 -0
  143. htmlgraph/docs/templates/_sections/core_concepts.md.j2 +29 -0
  144. htmlgraph/docs/templates/_sections/sdk_basics.md.j2 +69 -0
  145. htmlgraph/docs/templates/base_agents.md.j2 +78 -0
  146. htmlgraph/docs/templates/example_user_override.md.j2 +47 -0
  147. htmlgraph/docs/version_check.py +163 -0
  148. htmlgraph/edge_index.py +2 -1
  149. htmlgraph/error_handler.py +544 -0
  150. htmlgraph/event_log.py +86 -37
  151. htmlgraph/event_migration.py +2 -1
  152. htmlgraph/file_watcher.py +12 -8
  153. htmlgraph/find_api.py +2 -1
  154. htmlgraph/git_events.py +67 -9
  155. htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
  156. htmlgraph/hooks/.htmlgraph/agents.json +72 -0
  157. htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
  158. htmlgraph/hooks/__init__.py +8 -0
  159. htmlgraph/hooks/bootstrap.py +169 -0
  160. htmlgraph/hooks/cigs_pretool_enforcer.py +354 -0
  161. htmlgraph/hooks/concurrent_sessions.py +208 -0
  162. htmlgraph/hooks/context.py +350 -0
  163. htmlgraph/hooks/drift_handler.py +525 -0
  164. htmlgraph/hooks/event_tracker.py +790 -99
  165. htmlgraph/hooks/git_commands.py +175 -0
  166. htmlgraph/hooks/installer.py +5 -1
  167. htmlgraph/hooks/orchestrator.py +327 -76
  168. htmlgraph/hooks/orchestrator_reflector.py +31 -4
  169. htmlgraph/hooks/post_tool_use_failure.py +32 -7
  170. htmlgraph/hooks/post_tool_use_handler.py +257 -0
  171. htmlgraph/hooks/posttooluse.py +92 -19
  172. htmlgraph/hooks/pretooluse.py +527 -7
  173. htmlgraph/hooks/prompt_analyzer.py +637 -0
  174. htmlgraph/hooks/session_handler.py +668 -0
  175. htmlgraph/hooks/session_summary.py +395 -0
  176. htmlgraph/hooks/state_manager.py +504 -0
  177. htmlgraph/hooks/subagent_detection.py +202 -0
  178. htmlgraph/hooks/subagent_stop.py +369 -0
  179. htmlgraph/hooks/task_enforcer.py +99 -4
  180. htmlgraph/hooks/validator.py +212 -91
  181. htmlgraph/ids.py +2 -1
  182. htmlgraph/learning.py +125 -100
  183. htmlgraph/mcp_server.py +2 -1
  184. htmlgraph/models.py +217 -18
  185. htmlgraph/operations/README.md +62 -0
  186. htmlgraph/operations/__init__.py +79 -0
  187. htmlgraph/operations/analytics.py +339 -0
  188. htmlgraph/operations/bootstrap.py +289 -0
  189. htmlgraph/operations/events.py +244 -0
  190. htmlgraph/operations/fastapi_server.py +231 -0
  191. htmlgraph/operations/hooks.py +350 -0
  192. htmlgraph/operations/initialization.py +597 -0
  193. htmlgraph/operations/initialization.py.backup +228 -0
  194. htmlgraph/operations/server.py +303 -0
  195. htmlgraph/orchestration/__init__.py +58 -0
  196. htmlgraph/orchestration/claude_launcher.py +179 -0
  197. htmlgraph/orchestration/command_builder.py +72 -0
  198. htmlgraph/orchestration/headless_spawner.py +281 -0
  199. htmlgraph/orchestration/live_events.py +377 -0
  200. htmlgraph/orchestration/model_selection.py +327 -0
  201. htmlgraph/orchestration/plugin_manager.py +140 -0
  202. htmlgraph/orchestration/prompts.py +137 -0
  203. htmlgraph/orchestration/spawner_event_tracker.py +383 -0
  204. htmlgraph/orchestration/spawners/__init__.py +16 -0
  205. htmlgraph/orchestration/spawners/base.py +194 -0
  206. htmlgraph/orchestration/spawners/claude.py +173 -0
  207. htmlgraph/orchestration/spawners/codex.py +435 -0
  208. htmlgraph/orchestration/spawners/copilot.py +294 -0
  209. htmlgraph/orchestration/spawners/gemini.py +471 -0
  210. htmlgraph/orchestration/subprocess_runner.py +36 -0
  211. htmlgraph/{orchestration.py → orchestration/task_coordination.py} +16 -8
  212. htmlgraph/orchestration.md +563 -0
  213. htmlgraph/orchestrator-system-prompt-optimized.txt +863 -0
  214. htmlgraph/orchestrator.py +2 -1
  215. htmlgraph/orchestrator_config.py +357 -0
  216. htmlgraph/orchestrator_mode.py +115 -4
  217. htmlgraph/parallel.py +2 -1
  218. htmlgraph/parser.py +86 -6
  219. htmlgraph/path_query.py +608 -0
  220. htmlgraph/pattern_matcher.py +636 -0
  221. htmlgraph/pydantic_models.py +476 -0
  222. htmlgraph/quality_gates.py +350 -0
  223. htmlgraph/query_builder.py +2 -1
  224. htmlgraph/query_composer.py +509 -0
  225. htmlgraph/reflection.py +443 -0
  226. htmlgraph/refs.py +344 -0
  227. htmlgraph/repo_hash.py +512 -0
  228. htmlgraph/repositories/__init__.py +292 -0
  229. htmlgraph/repositories/analytics_repository.py +455 -0
  230. htmlgraph/repositories/analytics_repository_standard.py +628 -0
  231. htmlgraph/repositories/feature_repository.py +581 -0
  232. htmlgraph/repositories/feature_repository_htmlfile.py +668 -0
  233. htmlgraph/repositories/feature_repository_memory.py +607 -0
  234. htmlgraph/repositories/feature_repository_sqlite.py +858 -0
  235. htmlgraph/repositories/filter_service.py +620 -0
  236. htmlgraph/repositories/filter_service_standard.py +445 -0
  237. htmlgraph/repositories/shared_cache.py +621 -0
  238. htmlgraph/repositories/shared_cache_memory.py +395 -0
  239. htmlgraph/repositories/track_repository.py +552 -0
  240. htmlgraph/repositories/track_repository_htmlfile.py +619 -0
  241. htmlgraph/repositories/track_repository_memory.py +508 -0
  242. htmlgraph/repositories/track_repository_sqlite.py +711 -0
  243. htmlgraph/sdk/__init__.py +398 -0
  244. htmlgraph/sdk/__init__.pyi +14 -0
  245. htmlgraph/sdk/analytics/__init__.py +19 -0
  246. htmlgraph/sdk/analytics/engine.py +155 -0
  247. htmlgraph/sdk/analytics/helpers.py +178 -0
  248. htmlgraph/sdk/analytics/registry.py +109 -0
  249. htmlgraph/sdk/base.py +484 -0
  250. htmlgraph/sdk/constants.py +216 -0
  251. htmlgraph/sdk/core.pyi +308 -0
  252. htmlgraph/sdk/discovery.py +120 -0
  253. htmlgraph/sdk/help/__init__.py +12 -0
  254. htmlgraph/sdk/help/mixin.py +699 -0
  255. htmlgraph/sdk/mixins/__init__.py +15 -0
  256. htmlgraph/sdk/mixins/attribution.py +113 -0
  257. htmlgraph/sdk/mixins/mixin.py +410 -0
  258. htmlgraph/sdk/operations/__init__.py +12 -0
  259. htmlgraph/sdk/operations/mixin.py +427 -0
  260. htmlgraph/sdk/orchestration/__init__.py +17 -0
  261. htmlgraph/sdk/orchestration/coordinator.py +203 -0
  262. htmlgraph/sdk/orchestration/spawner.py +204 -0
  263. htmlgraph/sdk/planning/__init__.py +19 -0
  264. htmlgraph/sdk/planning/bottlenecks.py +93 -0
  265. htmlgraph/sdk/planning/mixin.py +211 -0
  266. htmlgraph/sdk/planning/parallel.py +186 -0
  267. htmlgraph/sdk/planning/queue.py +210 -0
  268. htmlgraph/sdk/planning/recommendations.py +87 -0
  269. htmlgraph/sdk/planning/smart_planning.py +319 -0
  270. htmlgraph/sdk/session/__init__.py +19 -0
  271. htmlgraph/sdk/session/continuity.py +57 -0
  272. htmlgraph/sdk/session/handoff.py +110 -0
  273. htmlgraph/sdk/session/info.py +309 -0
  274. htmlgraph/sdk/session/manager.py +103 -0
  275. htmlgraph/sdk/strategic/__init__.py +26 -0
  276. htmlgraph/sdk/strategic/mixin.py +563 -0
  277. htmlgraph/server.py +295 -107
  278. htmlgraph/session_hooks.py +300 -0
  279. htmlgraph/session_manager.py +285 -3
  280. htmlgraph/session_registry.py +587 -0
  281. htmlgraph/session_state.py +436 -0
  282. htmlgraph/session_warning.py +2 -1
  283. htmlgraph/sessions/__init__.py +23 -0
  284. htmlgraph/sessions/handoff.py +756 -0
  285. htmlgraph/system_prompts.py +450 -0
  286. htmlgraph/templates/orchestration-view.html +350 -0
  287. htmlgraph/track_builder.py +33 -1
  288. htmlgraph/track_manager.py +38 -0
  289. htmlgraph/transcript.py +18 -5
  290. htmlgraph/validation.py +115 -0
  291. htmlgraph/watch.py +2 -1
  292. htmlgraph/work_type_utils.py +2 -1
  293. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/dashboard.html +2246 -248
  294. {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/METADATA +95 -64
  295. htmlgraph-0.27.5.dist-info/RECORD +337 -0
  296. {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/entry_points.txt +1 -1
  297. htmlgraph/cli.py +0 -4839
  298. htmlgraph/sdk.py +0 -2359
  299. htmlgraph-0.20.1.dist-info/RECORD +0 -118
  300. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/styles.css +0 -0
  301. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
  302. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
  303. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
  304. {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/WHEEL +0 -0
htmlgraph/cli/base.py ADDED
@@ -0,0 +1,685 @@
1
+ from __future__ import annotations
2
+
3
+ """Base classes and utilities for CLI commands.
4
+
5
+ Provides:
6
+ - BaseCommand: Abstract base class for all commands
7
+ - CommandResult: Structured command output
8
+ - CommandError: User-facing errors
9
+ - Formatters: JSON and text output formatting
10
+ - TableBuilder: Utility for creating Rich tables with consistent styling
11
+ - TextOutputBuilder: Utility for building formatted text output consistently
12
+ - save_traceback: Save full tracebacks to log files instead of console
13
+ """
14
+
15
+
16
+ import argparse
17
+ import json
18
+ import sys
19
+ import traceback
20
+ from abc import ABC, abstractmethod
21
+ from collections.abc import Iterable
22
+ from dataclasses import dataclass
23
+ from datetime import date, datetime
24
+ from pathlib import Path
25
+ from typing import TYPE_CHECKING, Any, Literal, Protocol
26
+
27
+ from rich import box
28
+ from rich.console import Console
29
+ from rich.table import Table
30
+ from typing_extensions import Self
31
+
32
+ if TYPE_CHECKING:
33
+ from htmlgraph.sdk import SDK
34
+
35
+ _console = Console()
36
+
37
+
38
+ class CommandError(Exception):
39
+ """User-facing CLI error with an exit code."""
40
+
41
+ def __init__(self, message: str, exit_code: int = 1) -> None:
42
+ super().__init__(message)
43
+ self.exit_code = exit_code
44
+
45
+
46
+ # ============================================================================
47
+ # Traceback Logger - Save error tracebacks to log files
48
+ # ============================================================================
49
+
50
+
51
+ def save_traceback(error: Exception, context: dict[str, Any] | None = None) -> Path:
52
+ """Save full traceback to log file instead of printing to console.
53
+
54
+ Args:
55
+ error: The exception that was raised
56
+ context: Optional context dict with command, args, cwd, etc.
57
+
58
+ Returns:
59
+ Path to the saved log file
60
+
61
+ Example:
62
+ try:
63
+ # Some operation
64
+ pass
65
+ except Exception as e:
66
+ log_file = save_traceback(e, context={"command": "serve", "cwd": os.getcwd()})
67
+ console.print(f"[red]Error:[/red] {e}")
68
+ console.print(f"[dim]Full traceback saved to:[/dim] {log_file}")
69
+ """
70
+ # Create logs directory
71
+ log_dir = Path(".htmlgraph/logs/errors")
72
+ log_dir.mkdir(parents=True, exist_ok=True)
73
+
74
+ # Generate filename with timestamp
75
+ timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
76
+ log_file = log_dir / f"error-{timestamp}.log"
77
+
78
+ # Write traceback with context
79
+ with open(log_file, "w") as f:
80
+ f.write(f"Timestamp: {datetime.now().isoformat()}\n")
81
+ if context:
82
+ f.write(f"Context: {context}\n")
83
+ f.write("\n--- Traceback ---\n")
84
+ traceback.print_exc(file=f)
85
+
86
+ return log_file
87
+
88
+
89
+ # ============================================================================
90
+ # TableBuilder - Consistent table styling across CLI
91
+ # ============================================================================
92
+
93
+
94
+ class TableBuilder:
95
+ """Builder for creating Rich tables with consistent styling.
96
+
97
+ Provides factory methods for common table patterns and column types.
98
+ Eliminates duplicated table creation code across CLI modules.
99
+
100
+ Example:
101
+ # List table with standard styling
102
+ builder = TableBuilder.create_list_table("Features")
103
+ builder.add_id_column()
104
+ builder.add_text_column("Title", max_width=40)
105
+ builder.add_status_column()
106
+ builder.add_timestamp_column("Updated")
107
+
108
+ # Add rows
109
+ for feature in features:
110
+ builder.add_row(feature.id, feature.title, feature.status, feature.updated)
111
+
112
+ # Access the table
113
+ console.print(builder.table)
114
+ """
115
+
116
+ def __init__(
117
+ self,
118
+ *,
119
+ title: str | None = None,
120
+ show_header: bool = True,
121
+ header_style: str = "bold magenta",
122
+ box_style: box.Box = box.ROUNDED,
123
+ ) -> None:
124
+ """Initialize TableBuilder with styling options.
125
+
126
+ Args:
127
+ title: Table title
128
+ show_header: Show header row
129
+ header_style: Style for header text
130
+ box_style: Box drawing style from rich.box
131
+ """
132
+ self.table = Table(
133
+ title=title,
134
+ show_header=show_header,
135
+ header_style=header_style,
136
+ box=box_style,
137
+ )
138
+
139
+ @classmethod
140
+ def create_list_table(cls, title: str | None = None) -> TableBuilder:
141
+ """Create a standard list table with rounded box."""
142
+ return cls(title=title, show_header=True, header_style="bold magenta")
143
+
144
+ @classmethod
145
+ def create_status_table(cls, title: str | None = None) -> TableBuilder:
146
+ """Create a key-value status table without header."""
147
+ return cls(title=title, show_header=False, box_style=box.SIMPLE)
148
+
149
+ @classmethod
150
+ def create_compact_table(cls) -> TableBuilder:
151
+ """Create a compact table with no header or box."""
152
+ return cls(title=None, show_header=False, box_style=box.SIMPLE)
153
+
154
+ def add_id_column(
155
+ self,
156
+ name: str = "ID",
157
+ *,
158
+ style: str = "cyan",
159
+ no_wrap: bool = False,
160
+ max_width: int | None = None,
161
+ ) -> TableBuilder:
162
+ """Add an ID column with cyan styling.
163
+
164
+ Args:
165
+ name: Column header name
166
+ style: Text style
167
+ no_wrap: Prevent text wrapping
168
+ max_width: Maximum column width in characters
169
+ """
170
+ self.table.add_column(name, style=style, no_wrap=no_wrap, max_width=max_width)
171
+ return self
172
+
173
+ def add_text_column(
174
+ self,
175
+ name: str,
176
+ *,
177
+ style: str = "yellow",
178
+ max_width: int | None = None,
179
+ no_wrap: bool = False,
180
+ ) -> TableBuilder:
181
+ """Add a text column with yellow styling.
182
+
183
+ Args:
184
+ name: Column header name
185
+ style: Text style
186
+ max_width: Maximum column width in characters
187
+ no_wrap: Prevent text wrapping
188
+ """
189
+ self.table.add_column(name, style=style, max_width=max_width, no_wrap=no_wrap)
190
+ return self
191
+
192
+ def add_status_column(
193
+ self,
194
+ name: str = "Status",
195
+ *,
196
+ style: str = "green",
197
+ width: int | None = None,
198
+ ) -> TableBuilder:
199
+ """Add a status column with green styling.
200
+
201
+ Args:
202
+ name: Column header name
203
+ style: Text style
204
+ width: Fixed column width in characters
205
+ """
206
+ self.table.add_column(name, style=style, width=width)
207
+ return self
208
+
209
+ def add_priority_column(
210
+ self,
211
+ name: str = "Priority",
212
+ *,
213
+ style: str = "blue",
214
+ width: int | None = None,
215
+ ) -> TableBuilder:
216
+ """Add a priority column with blue styling.
217
+
218
+ Args:
219
+ name: Column header name
220
+ style: Text style
221
+ width: Fixed column width in characters
222
+ """
223
+ self.table.add_column(name, style=style, width=width)
224
+ return self
225
+
226
+ def add_timestamp_column(
227
+ self,
228
+ name: str,
229
+ *,
230
+ style: str = "white",
231
+ width: int | None = None,
232
+ ) -> TableBuilder:
233
+ """Add a timestamp column with white styling.
234
+
235
+ Args:
236
+ name: Column header name
237
+ style: Text style
238
+ width: Fixed column width in characters
239
+ """
240
+ self.table.add_column(name, style=style, width=width)
241
+ return self
242
+
243
+ def add_numeric_column(
244
+ self,
245
+ name: str,
246
+ *,
247
+ style: str = "yellow",
248
+ justify: Literal["left", "center", "right"] = "right",
249
+ width: int | None = None,
250
+ ) -> TableBuilder:
251
+ """Add a numeric column with right justification.
252
+
253
+ Args:
254
+ name: Column header name
255
+ style: Text style
256
+ justify: Text alignment
257
+ width: Fixed column width in characters
258
+ """
259
+ self.table.add_column(name, style=style, justify=justify, width=width)
260
+ return self
261
+
262
+ def add_column(
263
+ self,
264
+ name: str,
265
+ *,
266
+ style: str | None = None,
267
+ justify: Literal["left", "center", "right"] = "left",
268
+ width: int | None = None,
269
+ max_width: int | None = None,
270
+ no_wrap: bool = False,
271
+ ) -> TableBuilder:
272
+ """Add a custom column with full control over styling.
273
+
274
+ Args:
275
+ name: Column header name
276
+ style: Text style (e.g., "cyan", "bold red")
277
+ justify: Text alignment
278
+ width: Fixed column width in characters
279
+ max_width: Maximum column width in characters
280
+ no_wrap: Prevent text wrapping
281
+ """
282
+ self.table.add_column(
283
+ name,
284
+ style=style,
285
+ justify=justify,
286
+ width=width,
287
+ max_width=max_width,
288
+ no_wrap=no_wrap,
289
+ )
290
+ return self
291
+
292
+ def add_row(self, *values: str) -> TableBuilder:
293
+ """Add a data row to the table.
294
+
295
+ Args:
296
+ *values: Cell values (converted to strings)
297
+ """
298
+ self.table.add_row(*values)
299
+ return self
300
+
301
+ def add_separator(self, style: str = "dim") -> TableBuilder:
302
+ """Add a separator row.
303
+
304
+ Args:
305
+ style: Style for separator row
306
+ """
307
+ # Add empty row with style
308
+ num_columns = len(self.table.columns)
309
+ self.table.add_row(*[""] * num_columns, style=style)
310
+ return self
311
+
312
+
313
+ # ============================================================================
314
+ # TextOutputBuilder - Consistent text output formatting across CLI
315
+ # ============================================================================
316
+
317
+
318
+ class TextOutputBuilder:
319
+ """Builder for creating formatted text output consistently.
320
+
321
+ Provides fluent API methods for building structured text output with
322
+ Rich console styling. Eliminates duplicated text output building code
323
+ across CLI modules.
324
+
325
+ Example:
326
+ output = TextOutputBuilder()
327
+ output.add_success(f"Session started: {session.id}")
328
+ output.add_field("Agent", session.agent)
329
+ output.add_field("Started", session.started_at.isoformat())
330
+ return CommandResult(text=output.build())
331
+ """
332
+
333
+ def __init__(self) -> None:
334
+ """Initialize TextOutputBuilder with empty lines list."""
335
+ self._lines: list[str] = []
336
+
337
+ def add_success(self, message: str) -> Self:
338
+ """Add success message with green styling.
339
+
340
+ Args:
341
+ message: Success message text
342
+
343
+ Returns:
344
+ Self for method chaining
345
+ """
346
+ from htmlgraph.cli.constants import get_style
347
+
348
+ self._lines.append(f"{get_style('success')}{message}")
349
+ return self
350
+
351
+ def add_error(self, message: str) -> Self:
352
+ """Add error message with red styling.
353
+
354
+ Args:
355
+ message: Error message text
356
+
357
+ Returns:
358
+ Self for method chaining
359
+ """
360
+ from htmlgraph.cli.constants import get_style
361
+
362
+ self._lines.append(f"{get_style('error')}{message}")
363
+ return self
364
+
365
+ def add_warning(self, message: str) -> Self:
366
+ """Add warning message with yellow styling.
367
+
368
+ Args:
369
+ message: Warning message text
370
+
371
+ Returns:
372
+ Self for method chaining
373
+ """
374
+ from htmlgraph.cli.constants import get_style
375
+
376
+ self._lines.append(f"{get_style('warning')}{message}")
377
+ return self
378
+
379
+ def add_info(self, message: str) -> Self:
380
+ """Add info message with cyan styling.
381
+
382
+ Args:
383
+ message: Info message text
384
+
385
+ Returns:
386
+ Self for method chaining
387
+ """
388
+ from htmlgraph.cli.constants import get_style
389
+
390
+ self._lines.append(f"{get_style('info')}{message}")
391
+ return self
392
+
393
+ def add_dim(self, message: str) -> Self:
394
+ """Add dimmed message with dim styling.
395
+
396
+ Args:
397
+ message: Dimmed message text
398
+
399
+ Returns:
400
+ Self for method chaining
401
+ """
402
+ from htmlgraph.cli.constants import get_style
403
+
404
+ self._lines.append(f"{get_style('dim')}{message}")
405
+ return self
406
+
407
+ def add_field(self, label: str, value: str | int | float | None) -> Self:
408
+ """Add indented field in 'Label: value' format.
409
+
410
+ Args:
411
+ label: Field label
412
+ value: Field value (converted to string)
413
+
414
+ Returns:
415
+ Self for method chaining
416
+ """
417
+ value_str = str(value) if value is not None else ""
418
+ self._lines.append(f" {label}: {value_str}")
419
+ return self
420
+
421
+ def add_line(self, text: str) -> Self:
422
+ """Add plain text line without styling.
423
+
424
+ Args:
425
+ text: Plain text to add
426
+
427
+ Returns:
428
+ Self for method chaining
429
+ """
430
+ self._lines.append(text)
431
+ return self
432
+
433
+ def add_blank(self) -> Self:
434
+ """Add blank line.
435
+
436
+ Returns:
437
+ Self for method chaining
438
+ """
439
+ self._lines.append("")
440
+ return self
441
+
442
+ def build(self) -> str:
443
+ """Build final text output by joining all lines.
444
+
445
+ Returns:
446
+ Joined string with newline separators
447
+ """
448
+ return "\n".join(self._lines)
449
+
450
+
451
+ @dataclass
452
+ class CommandResult:
453
+ """Structured command result for flexible output formatting."""
454
+
455
+ data: Any = None
456
+ text: str | Iterable[str] | None = None
457
+ json_data: Any | None = None
458
+ exit_code: int = 0 # Exit code for the command (0 = success)
459
+
460
+
461
+ class Formatter(Protocol):
462
+ """Protocol for output formatters."""
463
+
464
+ def output(self, result: CommandResult) -> None: ...
465
+
466
+
467
+ def _serialize_json(value: Any) -> Any:
468
+ """Recursively serialize value to JSON-compatible types.
469
+
470
+ Sanitizes strings to remove control characters (newlines, tabs) that
471
+ would break JSON validity when using json.dumps().
472
+ """
473
+ if value is None:
474
+ return None
475
+ if isinstance(value, (datetime, date)):
476
+ return value.isoformat()
477
+ if isinstance(value, str):
478
+ # Sanitize string: replace control characters with spaces
479
+ # This prevents newlines/tabs in JSON string values from breaking JSON validity
480
+ sanitized = value.replace("\n", " ").replace("\r", " ").replace("\t", " ")
481
+ # Collapse multiple spaces to single space
482
+ sanitized = " ".join(sanitized.split())
483
+ return sanitized
484
+ if hasattr(value, "model_dump") and callable(getattr(value, "model_dump")):
485
+ return _serialize_json(value.model_dump())
486
+ if hasattr(value, "to_dict") and callable(getattr(value, "to_dict")):
487
+ return _serialize_json(value.to_dict())
488
+ if isinstance(value, dict):
489
+ return {key: _serialize_json(val) for key, val in value.items()}
490
+ if isinstance(value, (list, tuple, set)):
491
+ return [_serialize_json(item) for item in value]
492
+ return value
493
+
494
+
495
+ class JsonFormatter:
496
+ """Format command output as JSON."""
497
+
498
+ def output(self, result: CommandResult) -> None:
499
+ payload = result.json_data if result.json_data is not None else result.data
500
+ # Use sys.stdout.write instead of _console.print to avoid Rich's line-wrapping
501
+ # which inserts literal newlines into JSON string values, breaking JSON validity
502
+ sys.stdout.write(json.dumps(_serialize_json(payload), indent=2) + "\n")
503
+
504
+
505
+ class TextFormatter:
506
+ """Format command output as plain text."""
507
+
508
+ def output(self, result: CommandResult) -> None:
509
+ # If data is provided and it's a Rich renderable, print it directly
510
+ if result.data is not None:
511
+ from rich.table import Table
512
+
513
+ # Check if data is a Rich renderable (Table, Panel, etc.)
514
+ if isinstance(result.data, (Table,)) or hasattr(result.data, "__rich__"):
515
+ _console.print(result.data)
516
+ return
517
+
518
+ # Fall back to text output
519
+ if result.text is None:
520
+ if result.data is not None:
521
+ _console.print(result.data)
522
+ return
523
+ if isinstance(result.text, str):
524
+ # Use sys.stdout.write() for ANSI-formatted text to preserve colors when piped
525
+ # This bypasses Rich's reprocessing and ensures ANSI codes are preserved
526
+ sys.stdout.write(result.text)
527
+ if not result.text.endswith("\n"):
528
+ sys.stdout.write("\n")
529
+ return
530
+ # For text as list/iterable, write directly to preserve ANSI codes
531
+ sys.stdout.write("\n".join(str(line) for line in result.text) + "\n")
532
+
533
+
534
+ def get_formatter(format_name: str) -> Formatter:
535
+ """Get formatter by name (json, text, plain, refs)."""
536
+ if format_name == "json":
537
+ return JsonFormatter()
538
+ if format_name in ("text", "plain", "refs", ""):
539
+ return TextFormatter()
540
+ raise CommandError(f"Unknown output format '{format_name}'")
541
+
542
+
543
+ class BaseCommand(ABC):
544
+ """Abstract base class for all CLI commands.
545
+
546
+ Provides:
547
+ - SDK initialization and caching
548
+ - Structured error handling
549
+ - Validation lifecycle hook
550
+ - Output formatting
551
+
552
+ Subclasses must implement:
553
+ - from_args(): Create command instance from argparse.Namespace
554
+ - execute(): Execute command logic and return CommandResult
555
+ """
556
+
557
+ def __init__(self) -> None:
558
+ self.graph_dir: str | None = None
559
+ self.agent: str | None = None
560
+ self._sdk: SDK | None = None
561
+ self.override_output_format: str | None = (
562
+ None # Allow commands to override formatter
563
+ )
564
+
565
+ @classmethod
566
+ @abstractmethod
567
+ def from_args(cls, args: argparse.Namespace) -> BaseCommand:
568
+ """Create command instance from argparse arguments.
569
+
570
+ This separates argument parsing from command execution,
571
+ making commands easier to test.
572
+ """
573
+ raise NotImplementedError
574
+
575
+ def validate(self) -> None:
576
+ """Validate command parameters before execution.
577
+
578
+ Raise CommandError if validation fails.
579
+ Default implementation does nothing.
580
+ """
581
+ return None
582
+
583
+ @abstractmethod
584
+ def execute(self) -> CommandResult:
585
+ """Execute the command and return structured result.
586
+
587
+ Raise CommandError for user-facing errors.
588
+ """
589
+ raise NotImplementedError
590
+
591
+ def get_sdk(self) -> SDK:
592
+ """Get or create SDK instance.
593
+
594
+ Caches SDK to avoid repeated initialization.
595
+ """
596
+ if self.graph_dir is None:
597
+ raise CommandError("Missing graph directory for command execution.")
598
+ if self._sdk is None:
599
+ from htmlgraph.sdk import SDK
600
+
601
+ self._sdk = SDK(directory=self.graph_dir, agent=self.agent)
602
+ return self._sdk
603
+
604
+ def require_node(self, node: Any, entity_type: str, entity_id: str) -> None:
605
+ """Validate that a node exists, raising CommandError if None.
606
+
607
+ Args:
608
+ node: The node object to validate
609
+ entity_type: Type of entity (feature, session, track, etc.)
610
+ entity_id: ID of the entity for error message
611
+
612
+ Raises:
613
+ CommandError: If node is None
614
+
615
+ Usage:
616
+ node = collection.get(feature_id)
617
+ self.require_node(node, "feature", feature_id)
618
+ """
619
+ if node is None:
620
+ from htmlgraph.cli.constants import get_error_message
621
+
622
+ error_key = f"{entity_type}_not_found"
623
+ id_key = f"{entity_type}_id"
624
+ raise CommandError(get_error_message(error_key, **{id_key: entity_id}))
625
+
626
+ def require_value(self, value: Any, message: str) -> None:
627
+ """Generic validation helper that raises CommandError if value is falsy.
628
+
629
+ Args:
630
+ value: The value to validate
631
+ message: Error message to raise if validation fails
632
+
633
+ Raises:
634
+ CommandError: If value is falsy (None, False, empty string, etc.)
635
+
636
+ Usage:
637
+ self.require_value(self.title, "Title is required")
638
+ self.require_value(len(items) > 0, "At least one item required")
639
+ """
640
+ if not value:
641
+ raise CommandError(message)
642
+
643
+ def require_collection(self, collection: Any, collection_name: str) -> None:
644
+ """Validate that a collection exists on SDK, raising CommandError if None.
645
+
646
+ Args:
647
+ collection: The collection object to validate
648
+ collection_name: Name of the collection for error message
649
+
650
+ Raises:
651
+ CommandError: If collection is None/falsy
652
+
653
+ Usage:
654
+ collection = getattr(sdk, self.collection, None)
655
+ self.require_collection(collection, self.collection)
656
+ """
657
+ if not collection:
658
+ raise CommandError(f"Collection '{collection_name}' not found in SDK")
659
+
660
+ def run(self, *, graph_dir: str, agent: str | None, output_format: str) -> None:
661
+ """Run command with context.
662
+
663
+ Args:
664
+ graph_dir: Path to .htmlgraph directory
665
+ agent: Agent name (optional)
666
+ output_format: Output format (json, text, plain)
667
+ """
668
+ self.graph_dir = graph_dir
669
+ self.agent = agent
670
+ try:
671
+ self.validate()
672
+ result = self.execute()
673
+ # Allow commands to override output format
674
+ # (e.g., snapshot command's --output-format flag overrides global --format)
675
+ actual_format = self.override_output_format or output_format
676
+ formatter = get_formatter(actual_format)
677
+ formatter.output(result)
678
+ except CommandError as exc:
679
+ error_console = Console(file=sys.stderr)
680
+ error_console.print(f"[red]Error: {exc}[/red]")
681
+ sys.exit(exc.exit_code)
682
+ except ValueError as exc:
683
+ error_console = Console(file=sys.stderr)
684
+ error_console.print(f"[red]Error: {exc}[/red]")
685
+ sys.exit(1)