htmlgraph 0.9.3__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 (331) 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 +173 -17
  5. htmlgraph/__init__.pyi +123 -0
  6. htmlgraph/agent_detection.py +127 -0
  7. htmlgraph/agent_registry.py +45 -30
  8. htmlgraph/agents.py +160 -107
  9. htmlgraph/analytics/__init__.py +9 -2
  10. htmlgraph/analytics/cli.py +190 -51
  11. htmlgraph/analytics/cost_analyzer.py +391 -0
  12. htmlgraph/analytics/cost_monitor.py +664 -0
  13. htmlgraph/analytics/cost_reporter.py +675 -0
  14. htmlgraph/analytics/cross_session.py +617 -0
  15. htmlgraph/analytics/dependency.py +192 -100
  16. htmlgraph/analytics/pattern_learning.py +771 -0
  17. htmlgraph/analytics/session_graph.py +707 -0
  18. htmlgraph/analytics/strategic/__init__.py +80 -0
  19. htmlgraph/analytics/strategic/cost_optimizer.py +611 -0
  20. htmlgraph/analytics/strategic/pattern_detector.py +876 -0
  21. htmlgraph/analytics/strategic/preference_manager.py +709 -0
  22. htmlgraph/analytics/strategic/suggestion_engine.py +747 -0
  23. htmlgraph/analytics/work_type.py +190 -14
  24. htmlgraph/analytics_index.py +135 -51
  25. htmlgraph/api/__init__.py +3 -0
  26. htmlgraph/api/cost_alerts_websocket.py +416 -0
  27. htmlgraph/api/main.py +2498 -0
  28. htmlgraph/api/static/htmx.min.js +1 -0
  29. htmlgraph/api/static/style-redesign.css +1344 -0
  30. htmlgraph/api/static/style.css +1079 -0
  31. htmlgraph/api/templates/dashboard-redesign.html +1366 -0
  32. htmlgraph/api/templates/dashboard.html +794 -0
  33. htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
  34. htmlgraph/api/templates/partials/activity-feed.html +1100 -0
  35. htmlgraph/api/templates/partials/agents-redesign.html +317 -0
  36. htmlgraph/api/templates/partials/agents.html +317 -0
  37. htmlgraph/api/templates/partials/event-traces.html +373 -0
  38. htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
  39. htmlgraph/api/templates/partials/features.html +578 -0
  40. htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
  41. htmlgraph/api/templates/partials/metrics.html +346 -0
  42. htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
  43. htmlgraph/api/templates/partials/orchestration.html +198 -0
  44. htmlgraph/api/templates/partials/spawners.html +375 -0
  45. htmlgraph/api/templates/partials/work-items.html +613 -0
  46. htmlgraph/api/websocket.py +538 -0
  47. htmlgraph/archive/__init__.py +24 -0
  48. htmlgraph/archive/bloom.py +234 -0
  49. htmlgraph/archive/fts.py +297 -0
  50. htmlgraph/archive/manager.py +583 -0
  51. htmlgraph/archive/search.py +244 -0
  52. htmlgraph/atomic_ops.py +560 -0
  53. htmlgraph/attribute_index.py +208 -0
  54. htmlgraph/bounded_paths.py +539 -0
  55. htmlgraph/builders/__init__.py +14 -0
  56. htmlgraph/builders/base.py +118 -29
  57. htmlgraph/builders/bug.py +150 -0
  58. htmlgraph/builders/chore.py +119 -0
  59. htmlgraph/builders/epic.py +150 -0
  60. htmlgraph/builders/feature.py +31 -6
  61. htmlgraph/builders/insight.py +195 -0
  62. htmlgraph/builders/metric.py +217 -0
  63. htmlgraph/builders/pattern.py +202 -0
  64. htmlgraph/builders/phase.py +162 -0
  65. htmlgraph/builders/spike.py +52 -19
  66. htmlgraph/builders/track.py +148 -72
  67. htmlgraph/cigs/__init__.py +81 -0
  68. htmlgraph/cigs/autonomy.py +385 -0
  69. htmlgraph/cigs/cost.py +475 -0
  70. htmlgraph/cigs/messages_basic.py +472 -0
  71. htmlgraph/cigs/messaging.py +365 -0
  72. htmlgraph/cigs/models.py +771 -0
  73. htmlgraph/cigs/pattern_storage.py +427 -0
  74. htmlgraph/cigs/patterns.py +503 -0
  75. htmlgraph/cigs/posttool_analyzer.py +234 -0
  76. htmlgraph/cigs/reporter.py +818 -0
  77. htmlgraph/cigs/tracker.py +317 -0
  78. htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
  79. htmlgraph/cli/.htmlgraph/agents.json +72 -0
  80. htmlgraph/cli/.htmlgraph/htmlgraph.db +0 -0
  81. htmlgraph/cli/__init__.py +42 -0
  82. htmlgraph/cli/__main__.py +6 -0
  83. htmlgraph/cli/analytics.py +1424 -0
  84. htmlgraph/cli/base.py +685 -0
  85. htmlgraph/cli/constants.py +206 -0
  86. htmlgraph/cli/core.py +954 -0
  87. htmlgraph/cli/main.py +147 -0
  88. htmlgraph/cli/models.py +475 -0
  89. htmlgraph/cli/templates/__init__.py +1 -0
  90. htmlgraph/cli/templates/cost_dashboard.py +399 -0
  91. htmlgraph/cli/work/__init__.py +239 -0
  92. htmlgraph/cli/work/browse.py +115 -0
  93. htmlgraph/cli/work/features.py +568 -0
  94. htmlgraph/cli/work/orchestration.py +676 -0
  95. htmlgraph/cli/work/report.py +728 -0
  96. htmlgraph/cli/work/sessions.py +466 -0
  97. htmlgraph/cli/work/snapshot.py +559 -0
  98. htmlgraph/cli/work/tracks.py +486 -0
  99. htmlgraph/cli_commands/__init__.py +1 -0
  100. htmlgraph/cli_commands/feature.py +195 -0
  101. htmlgraph/cli_framework.py +115 -0
  102. htmlgraph/collections/__init__.py +18 -0
  103. htmlgraph/collections/base.py +415 -98
  104. htmlgraph/collections/bug.py +53 -0
  105. htmlgraph/collections/chore.py +53 -0
  106. htmlgraph/collections/epic.py +53 -0
  107. htmlgraph/collections/feature.py +12 -26
  108. htmlgraph/collections/insight.py +100 -0
  109. htmlgraph/collections/metric.py +92 -0
  110. htmlgraph/collections/pattern.py +97 -0
  111. htmlgraph/collections/phase.py +53 -0
  112. htmlgraph/collections/session.py +194 -0
  113. htmlgraph/collections/spike.py +56 -16
  114. htmlgraph/collections/task_delegation.py +241 -0
  115. htmlgraph/collections/todo.py +511 -0
  116. htmlgraph/collections/traces.py +487 -0
  117. htmlgraph/config/cost_models.json +56 -0
  118. htmlgraph/config.py +190 -0
  119. htmlgraph/context_analytics.py +344 -0
  120. htmlgraph/converter.py +216 -28
  121. htmlgraph/cost_analysis/__init__.py +5 -0
  122. htmlgraph/cost_analysis/analyzer.py +438 -0
  123. htmlgraph/dashboard.html +2406 -307
  124. htmlgraph/dashboard.html.backup +6592 -0
  125. htmlgraph/dashboard.html.bak +7181 -0
  126. htmlgraph/dashboard.html.bak2 +7231 -0
  127. htmlgraph/dashboard.html.bak3 +7232 -0
  128. htmlgraph/db/__init__.py +38 -0
  129. htmlgraph/db/queries.py +790 -0
  130. htmlgraph/db/schema.py +1788 -0
  131. htmlgraph/decorators.py +317 -0
  132. htmlgraph/dependency_models.py +19 -2
  133. htmlgraph/deploy.py +142 -125
  134. htmlgraph/deployment_models.py +474 -0
  135. htmlgraph/docs/API_REFERENCE.md +841 -0
  136. htmlgraph/docs/HTTP_API.md +750 -0
  137. htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
  138. htmlgraph/docs/ORCHESTRATION_PATTERNS.md +717 -0
  139. htmlgraph/docs/README.md +532 -0
  140. htmlgraph/docs/__init__.py +77 -0
  141. htmlgraph/docs/docs_version.py +55 -0
  142. htmlgraph/docs/metadata.py +93 -0
  143. htmlgraph/docs/migrations.py +232 -0
  144. htmlgraph/docs/template_engine.py +143 -0
  145. htmlgraph/docs/templates/_sections/cli_reference.md.j2 +52 -0
  146. htmlgraph/docs/templates/_sections/core_concepts.md.j2 +29 -0
  147. htmlgraph/docs/templates/_sections/sdk_basics.md.j2 +69 -0
  148. htmlgraph/docs/templates/base_agents.md.j2 +78 -0
  149. htmlgraph/docs/templates/example_user_override.md.j2 +47 -0
  150. htmlgraph/docs/version_check.py +163 -0
  151. htmlgraph/edge_index.py +182 -27
  152. htmlgraph/error_handler.py +544 -0
  153. htmlgraph/event_log.py +100 -52
  154. htmlgraph/event_migration.py +13 -4
  155. htmlgraph/exceptions.py +49 -0
  156. htmlgraph/file_watcher.py +101 -28
  157. htmlgraph/find_api.py +75 -63
  158. htmlgraph/git_events.py +145 -63
  159. htmlgraph/graph.py +1122 -106
  160. htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
  161. htmlgraph/hooks/.htmlgraph/agents.json +72 -0
  162. htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
  163. htmlgraph/hooks/__init__.py +45 -0
  164. htmlgraph/hooks/bootstrap.py +169 -0
  165. htmlgraph/hooks/cigs_pretool_enforcer.py +354 -0
  166. htmlgraph/hooks/concurrent_sessions.py +208 -0
  167. htmlgraph/hooks/context.py +350 -0
  168. htmlgraph/hooks/drift_handler.py +525 -0
  169. htmlgraph/hooks/event_tracker.py +1314 -0
  170. htmlgraph/hooks/git_commands.py +175 -0
  171. htmlgraph/hooks/hooks-config.example.json +12 -0
  172. htmlgraph/hooks/installer.py +343 -0
  173. htmlgraph/hooks/orchestrator.py +674 -0
  174. htmlgraph/hooks/orchestrator_reflector.py +223 -0
  175. htmlgraph/hooks/post-checkout.sh +28 -0
  176. htmlgraph/hooks/post-commit.sh +24 -0
  177. htmlgraph/hooks/post-merge.sh +26 -0
  178. htmlgraph/hooks/post_tool_use_failure.py +273 -0
  179. htmlgraph/hooks/post_tool_use_handler.py +257 -0
  180. htmlgraph/hooks/posttooluse.py +408 -0
  181. htmlgraph/hooks/pre-commit.sh +94 -0
  182. htmlgraph/hooks/pre-push.sh +28 -0
  183. htmlgraph/hooks/pretooluse.py +819 -0
  184. htmlgraph/hooks/prompt_analyzer.py +637 -0
  185. htmlgraph/hooks/session_handler.py +668 -0
  186. htmlgraph/hooks/session_summary.py +395 -0
  187. htmlgraph/hooks/state_manager.py +504 -0
  188. htmlgraph/hooks/subagent_detection.py +202 -0
  189. htmlgraph/hooks/subagent_stop.py +369 -0
  190. htmlgraph/hooks/task_enforcer.py +255 -0
  191. htmlgraph/hooks/task_validator.py +177 -0
  192. htmlgraph/hooks/validator.py +628 -0
  193. htmlgraph/ids.py +41 -27
  194. htmlgraph/index.d.ts +286 -0
  195. htmlgraph/learning.py +767 -0
  196. htmlgraph/mcp_server.py +69 -23
  197. htmlgraph/models.py +1586 -87
  198. htmlgraph/operations/README.md +62 -0
  199. htmlgraph/operations/__init__.py +79 -0
  200. htmlgraph/operations/analytics.py +339 -0
  201. htmlgraph/operations/bootstrap.py +289 -0
  202. htmlgraph/operations/events.py +244 -0
  203. htmlgraph/operations/fastapi_server.py +231 -0
  204. htmlgraph/operations/hooks.py +350 -0
  205. htmlgraph/operations/initialization.py +597 -0
  206. htmlgraph/operations/initialization.py.backup +228 -0
  207. htmlgraph/operations/server.py +303 -0
  208. htmlgraph/orchestration/__init__.py +58 -0
  209. htmlgraph/orchestration/claude_launcher.py +179 -0
  210. htmlgraph/orchestration/command_builder.py +72 -0
  211. htmlgraph/orchestration/headless_spawner.py +281 -0
  212. htmlgraph/orchestration/live_events.py +377 -0
  213. htmlgraph/orchestration/model_selection.py +327 -0
  214. htmlgraph/orchestration/plugin_manager.py +140 -0
  215. htmlgraph/orchestration/prompts.py +137 -0
  216. htmlgraph/orchestration/spawner_event_tracker.py +383 -0
  217. htmlgraph/orchestration/spawners/__init__.py +16 -0
  218. htmlgraph/orchestration/spawners/base.py +194 -0
  219. htmlgraph/orchestration/spawners/claude.py +173 -0
  220. htmlgraph/orchestration/spawners/codex.py +435 -0
  221. htmlgraph/orchestration/spawners/copilot.py +294 -0
  222. htmlgraph/orchestration/spawners/gemini.py +471 -0
  223. htmlgraph/orchestration/subprocess_runner.py +36 -0
  224. htmlgraph/orchestration/task_coordination.py +343 -0
  225. htmlgraph/orchestration.md +563 -0
  226. htmlgraph/orchestrator-system-prompt-optimized.txt +863 -0
  227. htmlgraph/orchestrator.py +669 -0
  228. htmlgraph/orchestrator_config.py +357 -0
  229. htmlgraph/orchestrator_mode.py +328 -0
  230. htmlgraph/orchestrator_validator.py +133 -0
  231. htmlgraph/parallel.py +646 -0
  232. htmlgraph/parser.py +160 -35
  233. htmlgraph/path_query.py +608 -0
  234. htmlgraph/pattern_matcher.py +636 -0
  235. htmlgraph/planning.py +147 -52
  236. htmlgraph/pydantic_models.py +476 -0
  237. htmlgraph/quality_gates.py +350 -0
  238. htmlgraph/query_builder.py +109 -72
  239. htmlgraph/query_composer.py +509 -0
  240. htmlgraph/reflection.py +443 -0
  241. htmlgraph/refs.py +344 -0
  242. htmlgraph/repo_hash.py +512 -0
  243. htmlgraph/repositories/__init__.py +292 -0
  244. htmlgraph/repositories/analytics_repository.py +455 -0
  245. htmlgraph/repositories/analytics_repository_standard.py +628 -0
  246. htmlgraph/repositories/feature_repository.py +581 -0
  247. htmlgraph/repositories/feature_repository_htmlfile.py +668 -0
  248. htmlgraph/repositories/feature_repository_memory.py +607 -0
  249. htmlgraph/repositories/feature_repository_sqlite.py +858 -0
  250. htmlgraph/repositories/filter_service.py +620 -0
  251. htmlgraph/repositories/filter_service_standard.py +445 -0
  252. htmlgraph/repositories/shared_cache.py +621 -0
  253. htmlgraph/repositories/shared_cache_memory.py +395 -0
  254. htmlgraph/repositories/track_repository.py +552 -0
  255. htmlgraph/repositories/track_repository_htmlfile.py +619 -0
  256. htmlgraph/repositories/track_repository_memory.py +508 -0
  257. htmlgraph/repositories/track_repository_sqlite.py +711 -0
  258. htmlgraph/routing.py +8 -19
  259. htmlgraph/scripts/deploy.py +1 -2
  260. htmlgraph/sdk/__init__.py +398 -0
  261. htmlgraph/sdk/__init__.pyi +14 -0
  262. htmlgraph/sdk/analytics/__init__.py +19 -0
  263. htmlgraph/sdk/analytics/engine.py +155 -0
  264. htmlgraph/sdk/analytics/helpers.py +178 -0
  265. htmlgraph/sdk/analytics/registry.py +109 -0
  266. htmlgraph/sdk/base.py +484 -0
  267. htmlgraph/sdk/constants.py +216 -0
  268. htmlgraph/sdk/core.pyi +308 -0
  269. htmlgraph/sdk/discovery.py +120 -0
  270. htmlgraph/sdk/help/__init__.py +12 -0
  271. htmlgraph/sdk/help/mixin.py +699 -0
  272. htmlgraph/sdk/mixins/__init__.py +15 -0
  273. htmlgraph/sdk/mixins/attribution.py +113 -0
  274. htmlgraph/sdk/mixins/mixin.py +410 -0
  275. htmlgraph/sdk/operations/__init__.py +12 -0
  276. htmlgraph/sdk/operations/mixin.py +427 -0
  277. htmlgraph/sdk/orchestration/__init__.py +17 -0
  278. htmlgraph/sdk/orchestration/coordinator.py +203 -0
  279. htmlgraph/sdk/orchestration/spawner.py +204 -0
  280. htmlgraph/sdk/planning/__init__.py +19 -0
  281. htmlgraph/sdk/planning/bottlenecks.py +93 -0
  282. htmlgraph/sdk/planning/mixin.py +211 -0
  283. htmlgraph/sdk/planning/parallel.py +186 -0
  284. htmlgraph/sdk/planning/queue.py +210 -0
  285. htmlgraph/sdk/planning/recommendations.py +87 -0
  286. htmlgraph/sdk/planning/smart_planning.py +319 -0
  287. htmlgraph/sdk/session/__init__.py +19 -0
  288. htmlgraph/sdk/session/continuity.py +57 -0
  289. htmlgraph/sdk/session/handoff.py +110 -0
  290. htmlgraph/sdk/session/info.py +309 -0
  291. htmlgraph/sdk/session/manager.py +103 -0
  292. htmlgraph/sdk/strategic/__init__.py +26 -0
  293. htmlgraph/sdk/strategic/mixin.py +563 -0
  294. htmlgraph/server.py +685 -180
  295. htmlgraph/services/__init__.py +10 -0
  296. htmlgraph/services/claiming.py +199 -0
  297. htmlgraph/session_hooks.py +300 -0
  298. htmlgraph/session_manager.py +1392 -175
  299. htmlgraph/session_registry.py +587 -0
  300. htmlgraph/session_state.py +436 -0
  301. htmlgraph/session_warning.py +201 -0
  302. htmlgraph/sessions/__init__.py +23 -0
  303. htmlgraph/sessions/handoff.py +756 -0
  304. htmlgraph/setup.py +34 -17
  305. htmlgraph/spike_index.py +143 -0
  306. htmlgraph/sync_docs.py +12 -15
  307. htmlgraph/system_prompts.py +450 -0
  308. htmlgraph/templates/AGENTS.md.template +366 -0
  309. htmlgraph/templates/CLAUDE.md.template +97 -0
  310. htmlgraph/templates/GEMINI.md.template +87 -0
  311. htmlgraph/templates/orchestration-view.html +350 -0
  312. htmlgraph/track_builder.py +146 -15
  313. htmlgraph/track_manager.py +69 -21
  314. htmlgraph/transcript.py +890 -0
  315. htmlgraph/transcript_analytics.py +699 -0
  316. htmlgraph/types.py +323 -0
  317. htmlgraph/validation.py +115 -0
  318. htmlgraph/watch.py +8 -5
  319. htmlgraph/work_type_utils.py +3 -2
  320. {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/dashboard.html +2406 -307
  321. htmlgraph-0.27.5.data/data/htmlgraph/templates/AGENTS.md.template +366 -0
  322. htmlgraph-0.27.5.data/data/htmlgraph/templates/CLAUDE.md.template +97 -0
  323. htmlgraph-0.27.5.data/data/htmlgraph/templates/GEMINI.md.template +87 -0
  324. {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/METADATA +97 -64
  325. htmlgraph-0.27.5.dist-info/RECORD +337 -0
  326. {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/entry_points.txt +1 -1
  327. htmlgraph/cli.py +0 -2688
  328. htmlgraph/sdk.py +0 -709
  329. htmlgraph-0.9.3.dist-info/RECORD +0 -61
  330. {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/styles.css +0 -0
  331. {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/WHEEL +0 -0
htmlgraph/cli.py DELETED
@@ -1,2688 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- HtmlGraph CLI.
4
-
5
- Usage:
6
- htmlgraph serve [--port PORT] [--dir DIR]
7
- htmlgraph init [DIR]
8
- htmlgraph status [--dir DIR]
9
- htmlgraph query SELECTOR [--dir DIR]
10
-
11
- Session Management:
12
- htmlgraph session start [--id ID] [--agent AGENT]
13
- htmlgraph session end ID [--notes NOTES] [--recommend NEXT] [--blocker BLOCKER]
14
- htmlgraph session list
15
- htmlgraph session handoff [--session-id ID] [--notes NOTES] [--recommend NEXT] [--blocker BLOCKER] [--show]
16
- htmlgraph activity TOOL SUMMARY [--session ID] [--files FILE...]
17
-
18
- Feature Management:
19
- htmlgraph feature start ID
20
- htmlgraph feature complete ID
21
- htmlgraph feature primary ID
22
- htmlgraph feature claim ID
23
- htmlgraph feature release ID
24
- htmlgraph feature auto-release
25
-
26
- Track Management (Conductor-Style Planning):
27
- htmlgraph track new TITLE [--priority PRIORITY]
28
- htmlgraph track list
29
- htmlgraph track spec TRACK_ID TITLE
30
- htmlgraph track plan TRACK_ID TITLE
31
- htmlgraph track delete TRACK_ID
32
-
33
- Analytics:
34
- htmlgraph analytics # Project-wide analytics
35
- htmlgraph analytics --session-id SESSION_ID # Single session analysis
36
- htmlgraph analytics --recent N # Analyze recent N sessions
37
- """
38
-
39
- import argparse
40
- import os
41
- import sys
42
- import subprocess
43
- from pathlib import Path
44
-
45
-
46
- def cmd_install_gemini_extension(args):
47
- """Install the Gemini CLI extension from the bundled package files."""
48
- import htmlgraph
49
-
50
- # Find the extension path in the installed package
51
- package_dir = Path(htmlgraph.__file__).parent
52
- extension_dir = package_dir / "extensions" / "gemini"
53
-
54
- if not extension_dir.exists():
55
- print(f"Error: Gemini extension not found at {extension_dir}", file=sys.stderr)
56
- print("The extension may not be bundled with this version of htmlgraph.", file=sys.stderr)
57
- sys.exit(1)
58
-
59
- print(f"Installing Gemini extension from: {extension_dir}")
60
-
61
- # Run gemini extensions install with the bundled path
62
- try:
63
- result = subprocess.run(
64
- ["gemini", "extensions", "install", str(extension_dir), "--consent"],
65
- capture_output=True,
66
- text=True,
67
- check=True
68
- )
69
- print(result.stdout)
70
- print("\nāœ… Gemini extension installed successfully!")
71
- print("\nTo verify installation:")
72
- print(" gemini extensions list")
73
- except subprocess.CalledProcessError as e:
74
- print(f"Error installing extension: {e.stderr}", file=sys.stderr)
75
- sys.exit(1)
76
- except FileNotFoundError:
77
- print("Error: 'gemini' command not found.", file=sys.stderr)
78
- print("Please install Gemini CLI first:", file=sys.stderr)
79
- print(" npm install -g @google/gemini-cli", file=sys.stderr)
80
- sys.exit(1)
81
-
82
-
83
- def cmd_serve(args):
84
- """Start the HtmlGraph server."""
85
- from htmlgraph.server import serve
86
- serve(
87
- port=args.port,
88
- graph_dir=args.graph_dir,
89
- static_dir=args.static_dir,
90
- host=args.host,
91
- watch=not args.no_watch
92
- )
93
-
94
-
95
- def cmd_init(args):
96
- """Initialize a new .htmlgraph directory."""
97
- from htmlgraph.server import HtmlGraphAPIHandler
98
- from htmlgraph.analytics_index import AnalyticsIndex
99
- import shutil
100
-
101
- graph_dir = Path(args.dir) / ".htmlgraph"
102
- graph_dir.mkdir(parents=True, exist_ok=True)
103
-
104
- for collection in HtmlGraphAPIHandler.COLLECTIONS:
105
- (graph_dir / collection).mkdir(exist_ok=True)
106
-
107
- # Event stream directory (Git-friendly source of truth)
108
- events_dir = graph_dir / "events"
109
- events_dir.mkdir(exist_ok=True)
110
- if not args.no_events_keep:
111
- keep = events_dir / ".gitkeep"
112
- if not keep.exists():
113
- keep.write_text("", encoding="utf-8")
114
-
115
- # Copy stylesheet
116
- styles_src = Path(__file__).parent / "styles.css"
117
- styles_dest = graph_dir / "styles.css"
118
- if styles_src.exists() and not styles_dest.exists():
119
- styles_dest.write_text(styles_src.read_text())
120
-
121
- # Create default index.html if not exists
122
- index_path = Path(args.dir) / "index.html"
123
- if not index_path.exists():
124
- create_default_index(index_path)
125
-
126
- # Create analytics cache DB (rebuildable; typically gitignored)
127
- if not args.no_index:
128
- try:
129
- AnalyticsIndex(graph_dir / "index.sqlite").ensure_schema()
130
- except Exception:
131
- # Never fail init because of analytics cache.
132
- pass
133
-
134
- def ensure_gitignore_entries(project_dir: Path, lines: list[str]) -> None:
135
- if args.no_update_gitignore:
136
- return
137
- gitignore_path = project_dir / ".gitignore"
138
- existing = ""
139
- if gitignore_path.exists():
140
- try:
141
- existing = gitignore_path.read_text(encoding="utf-8")
142
- except Exception:
143
- existing = ""
144
- existing_lines = set(existing.splitlines())
145
- missing = [ln for ln in lines if ln not in existing_lines]
146
- if not missing:
147
- return
148
- block = "\n".join(
149
- ["", "# HtmlGraph analytics index (rebuildable cache)", *missing, ""]
150
- if "# HtmlGraph analytics index (rebuildable cache)" not in existing_lines
151
- else ["", *missing, ""]
152
- )
153
- try:
154
- gitignore_path.write_text(existing + block, encoding="utf-8")
155
- except Exception:
156
- # Don't fail init on .gitignore issues.
157
- pass
158
-
159
- ensure_gitignore_entries(
160
- Path(args.dir),
161
- [
162
- ".htmlgraph/index.sqlite",
163
- ".htmlgraph/index.sqlite-wal",
164
- ".htmlgraph/index.sqlite-shm",
165
- ".htmlgraph/git-hook-errors.log",
166
- ],
167
- )
168
-
169
- # Ensure versioned hook scripts exist (installation into .git/hooks is optional)
170
- hooks_dir = graph_dir / "hooks"
171
- hooks_dir.mkdir(exist_ok=True)
172
-
173
- # Hook templates (used when htmlgraph is installed without this repo layout).
174
- post_commit = """#!/bin/bash
175
- #
176
- # HtmlGraph Post-Commit Hook
177
- # Logs Git commit events for agent-agnostic continuity tracking
178
- #
179
-
180
- set +e
181
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
182
- PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
183
- cd "$PROJECT_ROOT" || exit 0
184
-
185
- if [ ! -d ".htmlgraph" ]; then
186
- exit 0
187
- fi
188
-
189
- if ! command -v htmlgraph &> /dev/null; then
190
- if command -v python3 &> /dev/null; then
191
- python3 -m htmlgraph.git_events commit &> /dev/null &
192
- fi
193
- exit 0
194
- fi
195
-
196
- htmlgraph git-event commit &> /dev/null &
197
- exit 0
198
- """
199
-
200
- post_checkout = """#!/bin/bash
201
- #
202
- # HtmlGraph Post-Checkout Hook
203
- # Logs branch switches / checkouts for continuity tracking
204
- #
205
-
206
- set +e
207
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
208
- PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
209
- cd "$PROJECT_ROOT" || exit 0
210
-
211
- if [ ! -d ".htmlgraph" ]; then
212
- exit 0
213
- fi
214
-
215
- OLD_HEAD="$1"
216
- NEW_HEAD="$2"
217
- FLAG="$3"
218
-
219
- if ! command -v htmlgraph &> /dev/null; then
220
- if command -v python3 &> /dev/null; then
221
- python3 -m htmlgraph.git_events checkout "$OLD_HEAD" "$NEW_HEAD" "$FLAG" &> /dev/null &
222
- fi
223
- exit 0
224
- fi
225
-
226
- htmlgraph git-event checkout "$OLD_HEAD" "$NEW_HEAD" "$FLAG" &> /dev/null &
227
- exit 0
228
- """
229
-
230
- post_merge = """#!/bin/bash
231
- #
232
- # HtmlGraph Post-Merge Hook
233
- # Logs successful merges for continuity tracking
234
- #
235
-
236
- set +e
237
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
238
- PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
239
- cd "$PROJECT_ROOT" || exit 0
240
-
241
- if [ ! -d ".htmlgraph" ]; then
242
- exit 0
243
- fi
244
-
245
- SQUASH_FLAG="$1"
246
-
247
- if ! command -v htmlgraph &> /dev/null; then
248
- if command -v python3 &> /dev/null; then
249
- python3 -m htmlgraph.git_events merge "$SQUASH_FLAG" &> /dev/null &
250
- fi
251
- exit 0
252
- fi
253
-
254
- htmlgraph git-event merge "$SQUASH_FLAG" &> /dev/null &
255
- exit 0
256
- """
257
-
258
- pre_push = """#!/bin/bash
259
- #
260
- # HtmlGraph Pre-Push Hook
261
- # Logs pushes for continuity tracking / team boundary events
262
- #
263
-
264
- set +e
265
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
266
- PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
267
- cd "$PROJECT_ROOT" || exit 0
268
-
269
- if [ ! -d ".htmlgraph" ]; then
270
- exit 0
271
- fi
272
-
273
- REMOTE_NAME="$1"
274
- REMOTE_URL="$2"
275
- UPDATES="$(cat)"
276
-
277
- if ! command -v htmlgraph &> /dev/null; then
278
- if command -v python3 &> /dev/null; then
279
- printf "%s" "$UPDATES" | python3 -m htmlgraph.git_events push "$REMOTE_NAME" "$REMOTE_URL" &> /dev/null &
280
- fi
281
- exit 0
282
- fi
283
-
284
- printf "%s" "$UPDATES" | htmlgraph git-event push "$REMOTE_NAME" "$REMOTE_URL" &> /dev/null &
285
- exit 0
286
- """
287
-
288
- pre_commit = """#!/bin/bash
289
- #
290
- # HtmlGraph Pre-Commit Hook
291
- # Reminds developers to create/start features for non-trivial work
292
- #
293
- # To disable: git config htmlgraph.precommit false
294
- # To bypass once: git commit --no-verify
295
-
296
- # Check if hook is disabled via config
297
- if [ "$(git config --type=bool htmlgraph.precommit)" = "false" ]; then
298
- exit 0
299
- fi
300
-
301
- # Check if HtmlGraph is initialized
302
- if [ ! -d ".htmlgraph" ]; then
303
- # Not an HtmlGraph project, skip silently
304
- exit 0
305
- fi
306
-
307
- # Fast check for in-progress features using grep (avoids Python startup)
308
- # This is 10-100x faster than calling the CLI
309
- ACTIVE_COUNT=$(find .htmlgraph/features -name "*.html" -exec grep -l 'data-status="in-progress"' {} \\; 2>/dev/null | wc -l | tr -d ' ')
310
-
311
- # If we have active features and htmlgraph CLI is available, get details
312
- if [ "$ACTIVE_COUNT" -gt 0 ] && command -v htmlgraph &> /dev/null; then
313
- ACTIVE_FEATURES=$(htmlgraph feature list --status in-progress 2>/dev/null)
314
- else
315
- ACTIVE_FEATURES=""
316
- fi
317
-
318
- # Redirect output to stderr (standard for git hooks)
319
- exec 1>&2
320
-
321
- if [ "$ACTIVE_COUNT" -gt 0 ]; then
322
- # Active features exist - show them
323
- echo ""
324
- echo "āœ“ HtmlGraph: $ACTIVE_COUNT active feature(s)"
325
- echo ""
326
- echo "$ACTIVE_FEATURES"
327
- echo ""
328
- else
329
- # No active features - show reminder
330
- echo ""
331
- echo "āš ļø HtmlGraph Feature Reminder"
332
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
333
- echo "No active features found. Did you forget to start one?"
334
- echo ""
335
- echo "For non-trivial work, consider:"
336
- echo " 1. Create feature: (use Python API or dashboard)"
337
- echo " 2. Start feature: htmlgraph feature start <feature-id>"
338
- echo ""
339
- echo "Quick decision:"
340
- echo " • >30 min work? → Create feature"
341
- echo " • 3+ files? → Create feature"
342
- echo " • Needs tests? → Create feature"
343
- echo " • Simple fix? → Direct commit OK"
344
- echo ""
345
- echo "To disable this reminder: git config htmlgraph.precommit false"
346
- echo "To bypass once: git commit --no-verify"
347
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
348
- echo ""
349
- echo "Proceeding with commit..."
350
- echo ""
351
- fi
352
-
353
- # Always exit 0 (allow commit)
354
- exit 0
355
- """
356
-
357
- def ensure_hook_file(hook_name: str, hook_content: str) -> Path:
358
- hook_dest = hooks_dir / f"{hook_name}.sh"
359
- if not hook_dest.exists():
360
- hook_dest.write_text(hook_content)
361
- try:
362
- hook_dest.chmod(0o755)
363
- except Exception:
364
- pass
365
- return hook_dest
366
-
367
- hook_files = {
368
- "pre-commit": ensure_hook_file("pre-commit", pre_commit),
369
- "post-commit": ensure_hook_file("post-commit", post_commit),
370
- "post-checkout": ensure_hook_file("post-checkout", post_checkout),
371
- "post-merge": ensure_hook_file("post-merge", post_merge),
372
- "pre-push": ensure_hook_file("pre-push", pre_push),
373
- }
374
-
375
- print(f"Initialized HtmlGraph in {graph_dir}")
376
- print(f"Collections: {', '.join(HtmlGraphAPIHandler.COLLECTIONS)}")
377
- print(f"\nStart server with: htmlgraph serve")
378
- if not args.no_index:
379
- print(f"Analytics cache: {graph_dir / 'index.sqlite'} (rebuildable; typically gitignored)")
380
- print(f"Events: {events_dir}/ (append-only JSONL)")
381
-
382
- # Install Git hooks if requested
383
- if args.install_hooks:
384
- git_dir = Path(args.dir) / ".git"
385
- if not git_dir.exists():
386
- print(f"\nāš ļø Warning: No .git directory found. Git hooks not installed.")
387
- print(f" Initialize git first: git init")
388
- return
389
-
390
- def install_hook(hook_name: str, hook_dest: Path, hook_content: str | None) -> None:
391
- """
392
- Install one Git hook:
393
- - Ensure `.htmlgraph/hooks/<hook>.sh` exists (copy template if present; else inline)
394
- - Install to `.git/hooks/<hook>` (symlink or chained wrapper if existing)
395
- """
396
- # Try to copy a template from this repo layout (dev), otherwise inline.
397
- hook_src = Path(__file__).parent.parent.parent.parent / ".htmlgraph" / "hooks" / f"{hook_name}.sh"
398
- if hook_src.exists() and hook_src.resolve() != hook_dest.resolve():
399
- shutil.copy(hook_src, hook_dest)
400
- elif not hook_dest.exists():
401
- if not hook_content:
402
- raise RuntimeError(f"Missing hook content for {hook_name}")
403
- hook_dest.write_text(hook_content)
404
- # Ensure executable (covers the case where the file already existed)
405
- try:
406
- hook_dest.chmod(0o755)
407
- except Exception:
408
- pass
409
-
410
- git_hook_path = git_dir / "hooks" / hook_name
411
-
412
- if git_hook_path.exists():
413
- print(f"\nāš ļø Existing {hook_name} hook found")
414
- backup_path = git_hook_path.with_suffix(".existing")
415
- if not backup_path.exists():
416
- shutil.copy(git_hook_path, backup_path)
417
- print(f" Backed up to: {backup_path}")
418
-
419
- chain_content = f'''#!/bin/bash
420
- # Chained hook - runs existing hook then HtmlGraph hook
421
-
422
- if [ -f "{backup_path}" ]; then
423
- "{backup_path}" || exit $?
424
- fi
425
-
426
- if [ -f "{hook_dest}" ]; then
427
- "{hook_dest}" || true
428
- fi
429
- '''
430
- git_hook_path.write_text(chain_content)
431
- git_hook_path.chmod(0o755)
432
- print(f" Installed chained hook at: {git_hook_path}")
433
- return
434
-
435
- try:
436
- git_hook_path.symlink_to(hook_dest.resolve())
437
- print(f"\nāœ“ Git hooks installed")
438
- print(f" {hook_name}: {git_hook_path} -> {hook_dest}")
439
- except OSError:
440
- shutil.copy(hook_dest, git_hook_path)
441
- git_hook_path.chmod(0o755)
442
- print(f"\nāœ“ Git hooks installed")
443
- print(f" {hook_name}: {git_hook_path}")
444
-
445
- install_hook("pre-commit", hook_files["pre-commit"], pre_commit)
446
- install_hook("post-commit", hook_files["post-commit"], post_commit)
447
- install_hook("post-checkout", hook_files["post-checkout"], post_checkout)
448
- install_hook("post-merge", hook_files["post-merge"], post_merge)
449
- install_hook("pre-push", hook_files["pre-push"], pre_push)
450
-
451
- print("\nGit events will now be logged to HtmlGraph automatically.")
452
-
453
-
454
- def cmd_status(args):
455
- """Show status of the graph."""
456
- from htmlgraph.sdk import SDK
457
- from collections import Counter
458
-
459
- # Use SDK to query all collections
460
- sdk = SDK(directory=args.graph_dir)
461
-
462
- total = 0
463
- by_status = Counter()
464
- by_collection = {}
465
-
466
- # All available collections
467
- collections = ['features', 'bugs', 'chores', 'spikes', 'epics', 'phases', 'sessions', 'tracks', 'agents']
468
-
469
- for coll_name in collections:
470
- coll = getattr(sdk, coll_name)
471
- try:
472
- nodes = coll.all()
473
- count = len(nodes)
474
- if count > 0:
475
- by_collection[coll_name] = count
476
- total += count
477
-
478
- # Count by status
479
- for node in nodes:
480
- status = getattr(node, 'status', 'unknown')
481
- by_status[status] += 1
482
- except Exception:
483
- # Collection might not exist yet
484
- pass
485
-
486
- print(f"HtmlGraph Status: {args.graph_dir}")
487
- print(f"{'=' * 40}")
488
- print(f"Total nodes: {total}")
489
- print(f"\nBy Collection:")
490
- for coll, count in sorted(by_collection.items()):
491
- print(f" {coll}: {count}")
492
- print(f"\nBy Status:")
493
- for status, count in sorted(by_status.items()):
494
- print(f" {status}: {count}")
495
-
496
-
497
- def cmd_query(args):
498
- """Query nodes with CSS selector."""
499
- from htmlgraph.graph import HtmlGraph
500
- from htmlgraph.converter import node_to_dict
501
- import json
502
-
503
- graph_dir = Path(args.graph_dir)
504
- if not graph_dir.exists():
505
- print(f"Error: {graph_dir} not found.", file=sys.stderr)
506
- sys.exit(1)
507
-
508
- results = []
509
- for collection_dir in graph_dir.iterdir():
510
- if collection_dir.is_dir() and not collection_dir.name.startswith("."):
511
- graph = HtmlGraph(collection_dir, auto_load=True)
512
- for node in graph.query(args.selector):
513
- data = node_to_dict(node)
514
- data["_collection"] = collection_dir.name
515
- results.append(data)
516
-
517
- if args.format == "json":
518
- print(json.dumps(results, indent=2, default=str))
519
- else:
520
- for node in results:
521
- status = node.get("status", "?")
522
- priority = node.get("priority", "?")
523
- print(f"[{node['_collection']}] {node['id']}: {node['title']} ({status}, {priority})")
524
-
525
-
526
- # =============================================================================
527
- # Session Management Commands
528
- # =============================================================================
529
-
530
- def cmd_session_start(args):
531
- """Start a new session."""
532
- from htmlgraph.sdk import SDK
533
- import json
534
-
535
- sdk = SDK(directory=args.graph_dir, agent=args.agent)
536
- session = sdk.start_session(
537
- session_id=args.id,
538
- title=args.title,
539
- agent=args.agent
540
- )
541
-
542
- if args.format == "json":
543
- from htmlgraph.converter import session_to_dict
544
- print(json.dumps(session_to_dict(session), indent=2))
545
- else:
546
- print(f"Session started: {session.id}")
547
- print(f" Agent: {session.agent}")
548
- print(f" Started: {session.started_at.isoformat()}")
549
- if session.title:
550
- print(f" Title: {session.title}")
551
-
552
-
553
- def cmd_session_end(args):
554
- """End a session."""
555
- from htmlgraph.sdk import SDK
556
- import json
557
-
558
- sdk = SDK(directory=args.graph_dir)
559
- blockers = args.blocker if args.blocker else None
560
- session = sdk.end_session(
561
- args.id,
562
- handoff_notes=args.notes,
563
- recommended_next=args.recommend,
564
- blockers=blockers,
565
- )
566
-
567
- if session is None:
568
- print(f"Error: Session '{args.id}' not found.", file=sys.stderr)
569
- sys.exit(1)
570
-
571
- if args.format == "json":
572
- from htmlgraph.converter import session_to_dict
573
- print(json.dumps(session_to_dict(session), indent=2))
574
- else:
575
- print(f"Session ended: {session.id}")
576
- print(f" Duration: {session.ended_at - session.started_at}")
577
- print(f" Events: {session.event_count}")
578
- if session.worked_on:
579
- print(f" Worked on: {', '.join(session.worked_on)}")
580
-
581
-
582
- def cmd_session_handoff(args):
583
- """Set or show session handoff context."""
584
- from htmlgraph.sdk import SDK
585
- import json
586
-
587
- sdk = SDK(directory=args.graph_dir, agent=args.agent)
588
-
589
- if args.show:
590
- # For showing, we might still need direct manager access or add more methods to SDK
591
- # But for now, let's keep using SessionManager logic via SDK property if needed
592
- # or implement show logic here using SDK collections
593
-
594
- # If args.session_id, use SDK.sessions.get()
595
- if args.session_id:
596
- session = sdk.sessions.get(args.session_id)
597
- else:
598
- # Need "last ended session" - SDK doesn't expose this yet.
599
- # Fallback to session_manager logic exposed on SDK
600
- session = sdk.session_manager.get_last_ended_session(agent=args.agent)
601
-
602
- if not session:
603
- if args.format == "json":
604
- print(json.dumps({}))
605
- else:
606
- print("No handoff context found.")
607
- return
608
-
609
- if args.format == "json":
610
- from htmlgraph.converter import session_to_dict
611
- print(json.dumps(session_to_dict(session), indent=2))
612
- else:
613
- print(f"Session: {session.id}")
614
- if session.handoff_notes:
615
- print(f"Notes: {session.handoff_notes}")
616
- if session.recommended_next:
617
- print(f"Recommended next: {session.recommended_next}")
618
- if session.blockers:
619
- print(f"Blockers: {', '.join(session.blockers)}")
620
- return
621
-
622
- # Setting handoff
623
- if not (args.notes or args.recommend or args.blocker):
624
- print("Error: Provide --notes, --recommend, or --blocker (or use --show).", file=sys.stderr)
625
- sys.exit(1)
626
-
627
- session = sdk.set_session_handoff(
628
- session_id=args.session_id, # Optional, defaults to active
629
- handoff_notes=args.notes,
630
- recommended_next=args.recommend,
631
- blockers=args.blocker if args.blocker else None,
632
- )
633
-
634
- if session is None:
635
- if args.session_id:
636
- print(f"Error: Session '{args.session_id}' not found.", file=sys.stderr)
637
- else:
638
- print(f"Error: No active session found. Provide --session-id.", file=sys.stderr)
639
- sys.exit(1)
640
-
641
- if args.format == "json":
642
- from htmlgraph.converter import session_to_dict
643
- print(json.dumps(session_to_dict(session), indent=2))
644
- else:
645
- print(f"Session handoff updated: {session.id}")
646
-
647
- def cmd_session_list(args):
648
- """List all sessions."""
649
- from htmlgraph.converter import SessionConverter
650
- import json
651
-
652
- sessions_dir = Path(args.graph_dir) / "sessions"
653
- if not sessions_dir.exists():
654
- print("No sessions found.")
655
- return
656
-
657
- converter = SessionConverter(sessions_dir)
658
- sessions = converter.load_all()
659
-
660
- # Sort by started_at descending (handle mixed tz-aware/naive datetimes)
661
- def sort_key(s):
662
- ts = s.started_at
663
- # Make naive datetimes comparable by assuming UTC
664
- if ts.tzinfo is None:
665
- return ts.replace(tzinfo=None)
666
- return ts.replace(tzinfo=None) # Compare as naive for sorting
667
- sessions.sort(key=sort_key, reverse=True)
668
-
669
- if args.format == "json":
670
- from htmlgraph.converter import session_to_dict
671
- print(json.dumps([session_to_dict(s) for s in sessions], indent=2))
672
- else:
673
- if not sessions:
674
- print("No sessions found.")
675
- return
676
-
677
- print(f"{'ID':<30} {'Status':<10} {'Agent':<15} {'Events':<8} {'Started'}")
678
- print("=" * 90)
679
- for session in sessions:
680
- started = session.started_at.strftime("%Y-%m-%d %H:%M")
681
- print(f"{session.id:<30} {session.status:<10} {session.agent:<15} {session.event_count:<8} {started}")
682
-
683
-
684
- def cmd_session_status_report(args):
685
- """Print a comprehensive status report (Markdown)."""
686
- from htmlgraph.sdk import SDK
687
- import subprocess
688
-
689
- sdk = SDK(directory=args.graph_dir)
690
- status = sdk.get_status()
691
-
692
- # Git log
693
- try:
694
- git_log = subprocess.check_output(
695
- ["git", "log", "--oneline", "-n", "3"],
696
- text=True,
697
- stderr=subprocess.DEVNULL
698
- ).strip()
699
- except Exception:
700
- git_log = "(Git log unavailable)"
701
-
702
- # Active features detail
703
- active_features_text = ""
704
- if status['active_features']:
705
- active_features_text = "\n### Current Feature(s)\n"
706
- for fid in status['active_features']:
707
- # Use SDK to get nodes
708
- node = sdk.features.get(fid) or sdk.bugs.get(fid)
709
- if node:
710
- active_features_text += f"**Working On:** {node.title} ({node.id})\n"
711
- active_features_text += f"**Status:** {node.status}\n"
712
- if node.steps:
713
- active_features_text += "**Step Progress**\n"
714
- for step in node.steps:
715
- mark = "[x]" if step.completed else "[ ]"
716
- active_features_text += f"- {mark} {step.description}\n"
717
- active_features_text += "\n"
718
- else:
719
- active_features_text = "\n### Current Feature(s)\nNo active features. Start one with `htmlgraph feature start <id>`.\n"
720
-
721
- # Project Name (from directory)
722
- project_name = Path(args.graph_dir).resolve().parent.name
723
-
724
- completed = status['by_status'].get('done', 0)
725
- total = status['total_features']
726
- pct = int(completed / max(1, total) * 100)
727
-
728
- print(f"""## Session Status
729
-
730
- **Project:** {project_name}
731
- **Progress:** {completed}/{total} features ({pct}%)
732
- **Active Features (WIP):** {status['wip_count']}
733
-
734
- ---
735
- {active_features_text}---
736
-
737
- ### Recent Commits
738
- {git_log}
739
-
740
- ---
741
-
742
- ### What's Next
743
- Use `htmlgraph feature list --status todo` to see backlog.
744
- """)
745
-
746
-
747
- def cmd_session_dedupe(args):
748
- """Move low-signal session files out of the main sessions directory."""
749
- from htmlgraph import SDK
750
-
751
- sdk = SDK(directory=args.graph_dir)
752
- result = sdk.dedupe_sessions(
753
- max_events=args.max_events,
754
- move_dir_name=args.move_dir,
755
- dry_run=args.dry_run,
756
- stale_extra_active=not args.no_stale_active,
757
- )
758
-
759
- print(f"Scanned: {result['scanned']}")
760
- print(f"Moved: {result['moved']}")
761
- if result.get("missing"):
762
- print(f"Missing: {result['missing']}")
763
- if not args.dry_run:
764
- if result.get("staled_active"):
765
- print(f"Staled: {result['staled_active']} extra active sessions")
766
- if result.get("kept_active"):
767
- print(f"Kept: {result['kept_active']} canonical active sessions")
768
-
769
-
770
- def cmd_session_link(args):
771
- """Link a feature to a session retroactively."""
772
- from htmlgraph.graph import HtmlGraph
773
- from htmlgraph.models import Edge
774
- import json
775
-
776
- graph_dir = Path(args.graph_dir)
777
- sessions_dir = graph_dir / "sessions"
778
- feature_dir = graph_dir / args.collection
779
-
780
- # Load session
781
- session_file = sessions_dir / f"{args.session_id}.html"
782
- if not session_file.exists():
783
- print(f"Error: Session '{args.session_id}' not found at {session_file}", file=sys.stderr)
784
- sys.exit(1)
785
-
786
- session_graph = HtmlGraph(sessions_dir)
787
- session = session_graph.get(args.session_id)
788
- if not session:
789
- print(f"Error: Failed to load session '{args.session_id}'", file=sys.stderr)
790
- sys.exit(1)
791
-
792
- # Load feature
793
- feature_file = feature_dir / f"{args.feature_id}.html"
794
- if not feature_file.exists():
795
- print(f"Error: Feature '{args.feature_id}' not found at {feature_file}", file=sys.stderr)
796
- sys.exit(1)
797
-
798
- feature_graph = HtmlGraph(feature_dir)
799
- feature = feature_graph.get(args.feature_id)
800
- if not feature:
801
- print(f"Error: Failed to load feature '{args.feature_id}'", file=sys.stderr)
802
- sys.exit(1)
803
-
804
- # Check if already linked
805
- worked_on = session.edges.get("worked-on", [])
806
- already_linked = any(e.target_id == args.feature_id for e in worked_on)
807
-
808
- if already_linked:
809
- print(f"Feature '{args.feature_id}' is already linked to session '{args.session_id}'")
810
- if not args.bidirectional:
811
- sys.exit(0)
812
-
813
- # Add edge from session to feature
814
- if not already_linked:
815
- new_edge = Edge(
816
- target_id=args.feature_id,
817
- relationship="worked-on",
818
- title=feature.title
819
- )
820
- if "worked-on" not in session.edges:
821
- session.edges["worked-on"] = []
822
- session.edges["worked-on"].append(new_edge)
823
- session_graph.update(session)
824
- print(f"āœ“ Linked feature '{args.feature_id}' to session '{args.session_id}'")
825
-
826
- # Optionally add reciprocal edge from feature to session
827
- if args.bidirectional:
828
- implemented_in = feature.edges.get("implemented-in", [])
829
- feature_already_linked = any(e.target_id == args.session_id for e in implemented_in)
830
-
831
- if not feature_already_linked:
832
- reciprocal_edge = Edge(
833
- target_id=args.session_id,
834
- relationship="implemented-in",
835
- title=f"Session {session.id}"
836
- )
837
- if "implemented-in" not in feature.edges:
838
- feature.edges["implemented-in"] = []
839
- feature.edges["implemented-in"].append(reciprocal_edge)
840
- feature_graph.update(feature)
841
- print(f"āœ“ Added reciprocal link from feature '{args.feature_id}' to session '{args.session_id}'")
842
- else:
843
- print(f"Feature '{args.feature_id}' already has reciprocal link to session")
844
-
845
- if args.format == "json":
846
- result = {
847
- "session_id": args.session_id,
848
- "feature_id": args.feature_id,
849
- "bidirectional": args.bidirectional,
850
- "linked": not already_linked
851
- }
852
- print(json.dumps(result, indent=2))
853
-
854
-
855
- def cmd_session_validate_attribution(args):
856
- """Validate feature attribution and tracking."""
857
- from htmlgraph.graph import HtmlGraph
858
- from htmlgraph.converter import SessionConverter
859
- import json
860
- from datetime import datetime
861
-
862
- graph_dir = Path(args.graph_dir)
863
- feature_dir = graph_dir / args.collection
864
- sessions_dir = graph_dir / "sessions"
865
- events_dir = graph_dir / "events"
866
-
867
- # Load feature
868
- feature_graph = HtmlGraph(feature_dir)
869
- feature = feature_graph.get(args.feature_id)
870
- if not feature:
871
- print(f"Error: Feature '{args.feature_id}' not found", file=sys.stderr)
872
- sys.exit(1)
873
-
874
- # Find sessions that worked on this feature
875
- sessions_graph = HtmlGraph(sessions_dir)
876
- all_sessions = sessions_graph.query('[data-type="session"]')
877
- linked_sessions = []
878
-
879
- for session in all_sessions:
880
- worked_on = session.edges.get("worked-on", [])
881
- if any(e.target_id == args.feature_id for e in worked_on):
882
- linked_sessions.append(session)
883
-
884
- # Count events attributed to this feature
885
- event_count = 0
886
- last_activity = None
887
- high_drift_events = []
888
-
889
- for session in linked_sessions:
890
- session_events_file = events_dir / f"{session.id}.jsonl"
891
- if session_events_file.exists():
892
- with open(session_events_file, 'r') as f:
893
- for line in f:
894
- try:
895
- event = json.loads(line.strip())
896
- if event.get('feature_id') == args.feature_id:
897
- event_count += 1
898
- timestamp = event.get('timestamp')
899
- if timestamp:
900
- event_time = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
901
- if not last_activity or event_time > last_activity:
902
- last_activity = event_time
903
-
904
- # Check for high drift
905
- drift_score = event.get('drift_score')
906
- if drift_score and drift_score > 0.8:
907
- high_drift_events.append({
908
- 'timestamp': timestamp,
909
- 'tool': event.get('tool'),
910
- 'drift': drift_score
911
- })
912
- except json.JSONDecodeError:
913
- continue
914
-
915
- # Calculate attribution health
916
- health = "UNKNOWN"
917
- issues = []
918
-
919
- if len(linked_sessions) == 0:
920
- health = "CRITICAL"
921
- issues.append("Feature not linked to any session")
922
- elif event_count == 0:
923
- health = "CRITICAL"
924
- issues.append("No events attributed to feature")
925
- elif event_count < 5:
926
- health = "WARNING"
927
- issues.append(f"Only {event_count} events attributed (unusually low)")
928
- else:
929
- health = "GOOD"
930
-
931
- if len(high_drift_events) > 3:
932
- if health == "GOOD":
933
- health = "WARNING"
934
- issues.append(f"{len(high_drift_events)} events with drift > 0.8 (may be misattributed)")
935
-
936
- # Output results
937
- if args.format == "json":
938
- result = {
939
- "feature_id": args.feature_id,
940
- "feature_title": feature.title,
941
- "health": health,
942
- "linked_sessions": len(linked_sessions),
943
- "event_count": event_count,
944
- "last_activity": last_activity.isoformat() if last_activity else None,
945
- "high_drift_count": len(high_drift_events),
946
- "issues": issues
947
- }
948
- print(json.dumps(result, indent=2))
949
- else:
950
- status_symbol = "āœ“" if health == "GOOD" else "⚠" if health == "WARNING" else "āœ—"
951
- print(f"{status_symbol} Feature '{args.feature_id}' validation:")
952
- print(f" Title: {feature.title}")
953
- print(f" Health: {health}")
954
- print(f" - Linked to {len(linked_sessions)} session(s)")
955
- print(f" - {event_count} events attributed")
956
- if last_activity:
957
- print(f" - Last activity: {last_activity.strftime('%Y-%m-%d %H:%M:%S')}")
958
-
959
- if issues:
960
- print(f"\n⚠ Issues detected:")
961
- for issue in issues:
962
- print(f" - {issue}")
963
-
964
- if len(high_drift_events) > 0 and len(high_drift_events) <= 5:
965
- print(f"\n⚠ High drift events:")
966
- for event in high_drift_events[:5]:
967
- print(f" - {event['timestamp']}: {event['tool']} (drift: {event['drift']:.2f})")
968
-
969
-
970
- def cmd_track(args):
971
- """Track an activity in the current session."""
972
- from htmlgraph import SDK
973
- import json
974
-
975
- agent = os.environ.get("HTMLGRAPH_AGENT")
976
- sdk = SDK(directory=args.graph_dir, agent=agent)
977
-
978
- try:
979
- entry = sdk.track_activity(
980
- tool=args.tool,
981
- summary=args.summary,
982
- file_paths=args.files,
983
- success=not args.failed,
984
- session_id=args.session # None if not specified, SDK will find active session
985
- )
986
- except ValueError as e:
987
- print(f"Error: {e}", file=sys.stderr)
988
- sys.exit(1)
989
-
990
- if args.format == "json":
991
- data = {
992
- "id": entry.id,
993
- "timestamp": entry.timestamp.isoformat(),
994
- "tool": entry.tool,
995
- "summary": entry.summary,
996
- "success": entry.success,
997
- "feature_id": entry.feature_id,
998
- "drift_score": entry.drift_score
999
- }
1000
- print(json.dumps(data, indent=2))
1001
- else:
1002
- print(f"Tracked: [{entry.tool}] {entry.summary}")
1003
- if entry.feature_id:
1004
- print(f" Attributed to: {entry.feature_id}")
1005
- if entry.drift_score and entry.drift_score > 0.3:
1006
- print(f" Drift warning: {entry.drift_score:.2f}")
1007
-
1008
-
1009
- # =============================================================================
1010
- # Events & Index Commands
1011
- # =============================================================================
1012
-
1013
- def cmd_events_export(args):
1014
- """Export legacy session HTML activity logs to JSONL event logs."""
1015
- from htmlgraph.event_migration import export_sessions_to_jsonl
1016
-
1017
- graph_dir = Path(args.graph_dir)
1018
- sessions_dir = graph_dir / "sessions"
1019
- events_dir = graph_dir / "events"
1020
-
1021
- result = export_sessions_to_jsonl(
1022
- sessions_dir=sessions_dir,
1023
- events_dir=events_dir,
1024
- overwrite=args.overwrite,
1025
- include_subdirs=args.include_subdirs,
1026
- )
1027
-
1028
- print(f"Written: {result['written']}")
1029
- print(f"Skipped: {result['skipped']}")
1030
- print(f"Failed: {result['failed']}")
1031
-
1032
-
1033
- def cmd_index_rebuild(args):
1034
- """Rebuild the SQLite analytics index from JSONL event logs."""
1035
- from htmlgraph.event_log import JsonlEventLog
1036
- from htmlgraph.analytics_index import AnalyticsIndex
1037
-
1038
- graph_dir = Path(args.graph_dir)
1039
- events_dir = graph_dir / "events"
1040
- db_path = graph_dir / "index.sqlite"
1041
-
1042
- log = JsonlEventLog(events_dir)
1043
- index = AnalyticsIndex(db_path)
1044
-
1045
- events = (event for _, event in log.iter_events())
1046
- result = index.rebuild_from_events(events)
1047
-
1048
- print(f"DB: {db_path}")
1049
- print(f"Inserted: {result['inserted']}")
1050
- print(f"Skipped: {result['skipped']}")
1051
-
1052
-
1053
- def cmd_watch(args):
1054
- """Watch filesystem changes and record them as activity events."""
1055
- from htmlgraph.watch import watch_and_track
1056
-
1057
- root = Path(args.root).resolve()
1058
- graph_dir = Path(args.graph_dir)
1059
-
1060
- watch_and_track(
1061
- root=root,
1062
- graph_dir=graph_dir,
1063
- session_id=args.session_id,
1064
- agent=args.agent,
1065
- interval_seconds=args.interval,
1066
- batch_seconds=args.batch_seconds,
1067
- )
1068
-
1069
-
1070
- def cmd_git_event(args):
1071
- """Log a Git event (commit, checkout, merge, push)."""
1072
- import sys
1073
- from htmlgraph.git_events import (
1074
- log_git_checkout,
1075
- log_git_commit,
1076
- log_git_merge,
1077
- log_git_push,
1078
- )
1079
-
1080
- if args.event_type == "commit":
1081
- success = log_git_commit()
1082
- if not success:
1083
- sys.exit(1)
1084
- return
1085
-
1086
- if args.event_type == "checkout":
1087
- if len(args.args) < 3:
1088
- print("Error: checkout requires args: <old_head> <new_head> <flag>", file=sys.stderr)
1089
- sys.exit(1)
1090
- old_head, new_head, flag = args.args[0], args.args[1], args.args[2]
1091
- if not log_git_checkout(old_head, new_head, flag):
1092
- sys.exit(1)
1093
- return
1094
-
1095
- if args.event_type == "merge":
1096
- squash_flag = args.args[0] if args.args else "0"
1097
- if not log_git_merge(squash_flag):
1098
- sys.exit(1)
1099
- return
1100
-
1101
- if args.event_type == "push":
1102
- if len(args.args) < 2:
1103
- print("Error: push requires args: <remote_name> <remote_url>", file=sys.stderr)
1104
- sys.exit(1)
1105
- remote_name, remote_url = args.args[0], args.args[1]
1106
- updates_text = sys.stdin.read()
1107
- if not log_git_push(remote_name, remote_url, updates_text):
1108
- sys.exit(1)
1109
- return
1110
- else:
1111
- print(f"Error: Unknown event type '{args.event_type}'", file=sys.stderr)
1112
- sys.exit(1)
1113
-
1114
-
1115
- def cmd_mcp_serve(args):
1116
- """Run the minimal MCP server over stdio."""
1117
- from htmlgraph.mcp_server import serve_stdio
1118
-
1119
- serve_stdio(graph_dir=Path(args.graph_dir), default_agent=args.agent)
1120
-
1121
-
1122
- # =============================================================================
1123
- # Work Management Commands (Smart Routing)
1124
- # =============================================================================
1125
-
1126
- def cmd_work_next(args):
1127
- """Get next best task using smart routing."""
1128
- from htmlgraph.sdk import SDK
1129
- from htmlgraph.converter import node_to_dict
1130
- import json
1131
-
1132
- sdk = SDK(directory=args.graph_dir, agent=args.agent)
1133
-
1134
- try:
1135
- task = sdk.work_next(
1136
- agent_id=args.agent,
1137
- auto_claim=args.auto_claim,
1138
- min_score=args.min_score
1139
- )
1140
- except ValueError as e:
1141
- print(f"Error: {e}", file=sys.stderr)
1142
- sys.exit(1)
1143
-
1144
- if args.format == "json":
1145
- if task:
1146
- print(json.dumps(node_to_dict(task), indent=2, default=str))
1147
- else:
1148
- print(json.dumps({"task": None, "message": "No suitable tasks found"}, indent=2))
1149
- else:
1150
- if task:
1151
- print(f"Next task: {task.id}")
1152
- print(f" Title: {task.title}")
1153
- print(f" Priority: {task.priority}")
1154
- print(f" Status: {task.status}")
1155
- if task.required_capabilities:
1156
- print(f" Required capabilities: {', '.join(task.required_capabilities)}")
1157
- if task.complexity:
1158
- print(f" Complexity: {task.complexity}")
1159
- if task.estimated_effort:
1160
- print(f" Estimated effort: {task.estimated_effort}h")
1161
- if args.auto_claim:
1162
- print(f" āœ“ Task claimed by {args.agent}")
1163
- else:
1164
- print("No suitable tasks found.")
1165
- print("Try lowering --min-score or check available tasks with 'htmlgraph feature list --status todo'")
1166
-
1167
-
1168
- def cmd_work_queue(args):
1169
- """Get prioritized work queue for an agent."""
1170
- from htmlgraph.sdk import SDK
1171
- import json
1172
-
1173
- sdk = SDK(directory=args.graph_dir, agent=args.agent)
1174
-
1175
- try:
1176
- queue = sdk.get_work_queue(
1177
- agent_id=args.agent,
1178
- limit=args.limit,
1179
- min_score=args.min_score
1180
- )
1181
- except ValueError as e:
1182
- print(f"Error: {e}", file=sys.stderr)
1183
- sys.exit(1)
1184
-
1185
- if args.format == "json":
1186
- print(json.dumps({"queue": queue, "count": len(queue)}, indent=2))
1187
- else:
1188
- if not queue:
1189
- print(f"No tasks found for agent '{args.agent}'.")
1190
- print("Try lowering --min-score or check available tasks with 'htmlgraph feature list --status todo'")
1191
- return
1192
-
1193
- print(f"Work queue for {args.agent} ({len(queue)} tasks):")
1194
- print("=" * 90)
1195
- print(f"{'Score':<8} {'Priority':<10} {'Complexity':<12} {'ID':<25} {'Title'}")
1196
- print("=" * 90)
1197
-
1198
- for item in queue:
1199
- complexity = item.get('complexity', 'N/A') or 'N/A'
1200
- title = item['title'][:30] + "..." if len(item['title']) > 33 else item['title']
1201
- print(f"{item['score']:<8.1f} {item['priority']:<10} {complexity:<12} {item['task_id']:<25} {title}")
1202
-
1203
-
1204
- def cmd_agent_list(args):
1205
- """List all registered agents."""
1206
- from htmlgraph.sdk import SDK
1207
- import json
1208
-
1209
- sdk = SDK(directory=args.graph_dir)
1210
- agents = sdk.list_agents(active_only=args.active_only)
1211
-
1212
- if args.format == "json":
1213
- print(json.dumps(
1214
- {"agents": [agent.to_dict() for agent in agents], "count": len(agents)},
1215
- indent=2
1216
- ))
1217
- else:
1218
- if not agents:
1219
- print("No agents registered.")
1220
- print("Agents are automatically registered in .htmlgraph/agents.json")
1221
- return
1222
-
1223
- print(f"Registered agents ({len(agents)}):")
1224
- print("=" * 90)
1225
-
1226
- for agent in agents:
1227
- status = "āœ“ active" if agent.active else "āœ— inactive"
1228
- print(f"\n{agent.id} ({agent.name}) - {status}")
1229
- print(f" Capabilities: {', '.join(agent.capabilities)}")
1230
- print(f" Max parallel tasks: {agent.max_parallel_tasks}")
1231
- print(f" Preferred complexity: {', '.join(agent.preferred_complexity)}")
1232
-
1233
-
1234
- # =============================================================================
1235
- # Feature Management Commands
1236
- # =============================================================================
1237
-
1238
- def cmd_feature_create(args):
1239
- """Create a new feature."""
1240
- from htmlgraph.sdk import SDK
1241
- import json
1242
-
1243
- # Use SDK for feature creation (which now handles logging)
1244
- sdk = SDK(directory=args.graph_dir, agent=args.agent)
1245
-
1246
- try:
1247
- # Determine collection (features -> create builder, others -> manual create?)
1248
- # For now, only 'features' has a builder in SDK.features.create()
1249
- # But BaseCollection doesn't have create().
1250
-
1251
- # If collection is 'features', use builder
1252
- if args.collection == "features":
1253
- builder = sdk.features.create(
1254
- title=args.title,
1255
- description=args.description or "",
1256
- priority=args.priority
1257
- )
1258
- if args.steps:
1259
- builder.add_steps(args.steps)
1260
- node = builder.save()
1261
- else:
1262
- # Fallback to SessionManager directly for non-feature collections
1263
- # (or extend SDK to support create on all collections)
1264
- # For consistency with old CLI, we use SessionManager here if not features.
1265
- # But wait, SDK initializes SessionManager.
1266
-
1267
- # Creating bugs/chores via SDK isn't fully fluent yet.
1268
- # Let's use the low-level SessionManager.create_feature logic for now via SDK's session_manager
1269
- # IF we want to strictly use SDK. But SDK.session_manager IS exposed now.
1270
- node = sdk.session_manager.create_feature(
1271
- title=args.title,
1272
- collection=args.collection,
1273
- description=args.description or "",
1274
- priority=args.priority,
1275
- steps=args.steps,
1276
- agent=args.agent
1277
- )
1278
-
1279
- except ValueError as e:
1280
- print(f"Error: {e}", file=sys.stderr)
1281
- sys.exit(1)
1282
-
1283
- if args.format == "json":
1284
- from htmlgraph.converter import node_to_dict
1285
- print(json.dumps(node_to_dict(node), indent=2))
1286
- else:
1287
- print(f"Created: {node.id}")
1288
- print(f" Title: {node.title}")
1289
- print(f" Status: {node.status}")
1290
- print(f" Path: {args.graph_dir}/{args.collection}/{node.id}.html")
1291
-
1292
-
1293
- def cmd_feature_start(args):
1294
- """Start working on a feature."""
1295
- from htmlgraph.sdk import SDK
1296
- import json
1297
-
1298
- sdk = SDK(directory=args.graph_dir, agent=args.agent)
1299
- collection = getattr(sdk, args.collection, None)
1300
-
1301
- if not collection:
1302
- print(f"Error: Collection '{args.collection}' not found in SDK.", file=sys.stderr)
1303
- sys.exit(1)
1304
-
1305
- try:
1306
- node = collection.start(args.id)
1307
- except ValueError as e:
1308
- print(f"Error: {e}", file=sys.stderr)
1309
- sys.exit(1)
1310
-
1311
- if node is None:
1312
- print(f"Error: Feature '{args.id}' not found in {args.collection}.", file=sys.stderr)
1313
- sys.exit(1)
1314
-
1315
- if args.format == "json":
1316
- from htmlgraph.converter import node_to_dict
1317
- print(json.dumps(node_to_dict(node), indent=2))
1318
- else:
1319
- print(f"Started: {node.id}")
1320
- print(f" Title: {node.title}")
1321
- print(f" Status: {node.status}")
1322
-
1323
- # Show WIP status
1324
- status = sdk.session_manager.get_status()
1325
- print(f" WIP: {status['wip_count']}/{status['wip_limit']}")
1326
-
1327
-
1328
- def cmd_feature_complete(args):
1329
- """Mark a feature as complete."""
1330
- from htmlgraph.sdk import SDK
1331
- import json
1332
-
1333
- sdk = SDK(directory=args.graph_dir, agent=args.agent)
1334
- collection = getattr(sdk, args.collection, None)
1335
-
1336
- if not collection:
1337
- print(f"Error: Collection '{args.collection}' not found in SDK.", file=sys.stderr)
1338
- sys.exit(1)
1339
-
1340
- try:
1341
- node = collection.complete(args.id)
1342
- except ValueError as e:
1343
- print(f"Error: {e}", file=sys.stderr)
1344
- sys.exit(1)
1345
-
1346
- if node is None:
1347
- print(f"Error: Feature '{args.id}' not found in {args.collection}.", file=sys.stderr)
1348
- sys.exit(1)
1349
-
1350
- if args.format == "json":
1351
- from htmlgraph.converter import node_to_dict
1352
- print(json.dumps(node_to_dict(node), indent=2))
1353
- else:
1354
- print(f"Completed: {node.id}")
1355
- print(f" Title: {node.title}")
1356
-
1357
-
1358
- def cmd_feature_primary(args):
1359
- """Set the primary feature for attribution."""
1360
- from htmlgraph.sdk import SDK
1361
- import json
1362
-
1363
- sdk = SDK(directory=args.graph_dir, agent=args.agent)
1364
-
1365
- # Only FeatureCollection has set_primary currently
1366
- if args.collection == "features":
1367
- node = sdk.features.set_primary(args.id)
1368
- else:
1369
- # Fallback to direct session manager for other collections
1370
- node = sdk.session_manager.set_primary_feature(args.id, collection=args.collection, agent=args.agent)
1371
-
1372
- if node is None:
1373
- print(f"Error: Feature '{args.id}' not found in {args.collection}.", file=sys.stderr)
1374
- sys.exit(1)
1375
-
1376
- if args.format == "json":
1377
- from htmlgraph.converter import node_to_dict
1378
- print(json.dumps(node_to_dict(node), indent=2))
1379
- else:
1380
- print(f"Primary feature set: {node.id}")
1381
- print(f" Title: {node.title}")
1382
-
1383
-
1384
- def cmd_feature_claim(args):
1385
- """Claim a feature."""
1386
- from htmlgraph.sdk import SDK
1387
- import json
1388
-
1389
- sdk = SDK(directory=args.graph_dir, agent=args.agent)
1390
- collection = getattr(sdk, args.collection, None)
1391
-
1392
- if not collection:
1393
- print(f"Error: Collection '{args.collection}' not found in SDK.", file=sys.stderr)
1394
- sys.exit(1)
1395
-
1396
- try:
1397
- node = collection.claim(args.id)
1398
- except ValueError as e:
1399
- print(f"Error: {e}", file=sys.stderr)
1400
- sys.exit(1)
1401
-
1402
- if node is None:
1403
- print(f"Error: Feature '{args.id}' not found in {args.collection}.", file=sys.stderr)
1404
- sys.exit(1)
1405
-
1406
- if args.format == "json":
1407
- from htmlgraph.converter import node_to_dict
1408
- print(json.dumps(node_to_dict(node), indent=2))
1409
- else:
1410
- print(f"Claimed: {node.id}")
1411
- print(f" Agent: {node.agent_assigned}")
1412
- print(f" Session: {node.claimed_by_session}")
1413
-
1414
-
1415
- def cmd_feature_release(args):
1416
- """Release a feature."""
1417
- from htmlgraph.sdk import SDK
1418
- import json
1419
-
1420
- sdk = SDK(directory=args.graph_dir, agent=args.agent)
1421
- collection = getattr(sdk, args.collection, None)
1422
-
1423
- if not collection:
1424
- print(f"Error: Collection '{args.collection}' not found in SDK.", file=sys.stderr)
1425
- sys.exit(1)
1426
-
1427
- try:
1428
- node = collection.release(args.id)
1429
- except ValueError as e:
1430
- print(f"Error: {e}", file=sys.stderr)
1431
- sys.exit(1)
1432
-
1433
- if node is None:
1434
- print(f"Error: Feature '{args.id}' not found in {args.collection}.", file=sys.stderr)
1435
- sys.exit(1)
1436
-
1437
- if args.format == "json":
1438
- from htmlgraph.converter import node_to_dict
1439
- print(json.dumps(node_to_dict(node), indent=2))
1440
- else:
1441
- print(f"Released: {node.id}")
1442
-
1443
-
1444
- def cmd_feature_auto_release(args):
1445
- """Release all features claimed by an agent."""
1446
- from htmlgraph.sdk import SDK
1447
- import json
1448
-
1449
- sdk = SDK(directory=args.graph_dir, agent=args.agent)
1450
- # auto_release_features is on SessionManager, exposed via SDK
1451
- released = sdk.session_manager.auto_release_features(agent=args.agent)
1452
-
1453
- if args.format == "json":
1454
- print(json.dumps({"released": released}, indent=2))
1455
- else:
1456
- if not released:
1457
- print(f"No features claimed by agent '{args.agent}'.")
1458
- else:
1459
- print(f"Released {len(released)} feature(s):")
1460
- for node_id in released:
1461
- print(f" - {node_id}")
1462
-
1463
-
1464
- def cmd_publish(args):
1465
- """Build and publish the package to PyPI (Interoperable)."""
1466
- import shutil
1467
- import subprocess
1468
-
1469
- # Ensure we are in project root
1470
- if not Path("pyproject.toml").exists():
1471
- print("Error: pyproject.toml not found. Run this from the project root.", file=sys.stderr)
1472
- sys.exit(1)
1473
-
1474
- # 1. Clean dist/
1475
- dist_dir = Path("dist")
1476
- if dist_dir.exists():
1477
- print("Cleaning dist/...")
1478
- shutil.rmtree(dist_dir)
1479
-
1480
- # 2. Build
1481
- print("Building package with uv...")
1482
- try:
1483
- subprocess.run(["uv", "build"], check=True)
1484
- except subprocess.CalledProcessError:
1485
- print("Error: Build failed.", file=sys.stderr)
1486
- sys.exit(1)
1487
- except FileNotFoundError:
1488
- print("Error: 'uv' command not found.", file=sys.stderr)
1489
- sys.exit(1)
1490
-
1491
- # 3. Publish
1492
- if args.dry_run:
1493
- print("Dry run: Skipping publish.")
1494
- return
1495
-
1496
- print("Publishing to PyPI...")
1497
- env = os.environ.copy()
1498
-
1499
- # Smart credential loading from .env
1500
- # Maps PyPI_API_TOKEN (common in .env) to UV_PUBLISH_TOKEN (needed by uv)
1501
- if "UV_PUBLISH_TOKEN" not in env:
1502
- dotenv = Path(".env")
1503
- if dotenv.exists():
1504
- try:
1505
- content = dotenv.read_text()
1506
- for line in content.splitlines():
1507
- if line.strip() and not line.startswith("#") and "=" in line:
1508
- key, val = line.split("=", 1)
1509
- key = key.strip()
1510
- val = val.strip().strip("'").strip('"')
1511
- if key == "PyPI_API_TOKEN":
1512
- env["UV_PUBLISH_TOKEN"] = val
1513
- print("Loaded credentials from .env")
1514
- except Exception:
1515
- pass
1516
-
1517
- try:
1518
- subprocess.run(["uv", "publish"], env=env, check=True)
1519
- print("\nāœ… Successfully published!")
1520
- except subprocess.CalledProcessError:
1521
- print("\nāŒ Publish failed.", file=sys.stderr)
1522
- sys.exit(1)
1523
-
1524
-
1525
- def cmd_feature_list(args):
1526
- """List features by status."""
1527
- from htmlgraph.sdk import SDK
1528
- from htmlgraph.converter import node_to_dict
1529
- import json
1530
-
1531
- # Use SDK for feature queries
1532
- sdk = SDK(directory=args.graph_dir)
1533
-
1534
- # Query features with SDK
1535
- if args.status:
1536
- nodes = sdk.features.where(status=args.status)
1537
- else:
1538
- nodes = sdk.features.all()
1539
-
1540
- # Sort by priority then updated
1541
- from datetime import timezone
1542
- priority_order = {"critical": 0, "high": 1, "medium": 2, "low": 3}
1543
-
1544
- def sort_key(n):
1545
- # Ensure timezone-aware datetime for comparison
1546
- updated = n.updated
1547
- if updated.tzinfo is None:
1548
- updated = updated.replace(tzinfo=timezone.utc)
1549
- return (priority_order.get(n.priority, 99), updated)
1550
-
1551
- nodes.sort(key=sort_key, reverse=True)
1552
-
1553
- if args.format == "json":
1554
- print(json.dumps([node_to_dict(n) for n in nodes], indent=2, default=str))
1555
- else:
1556
- if not nodes:
1557
- print(f"No features found with status '{args.status}'." if args.status else "No features found.")
1558
- return
1559
-
1560
- print(f"{'ID':<25} {'Status':<12} {'Priority':<10} {'Title'}")
1561
- print("=" * 80)
1562
- for node in nodes:
1563
- title = node.title[:35] + "..." if len(node.title) > 38 else node.title
1564
- print(f"{node.id:<25} {node.status:<12} {node.priority:<10} {title}")
1565
-
1566
-
1567
- # =============================================================================
1568
- # Track Management Commands (Conductor-Style Planning)
1569
- # =============================================================================
1570
-
1571
- def cmd_feature_step_complete(args):
1572
- """Mark one or more feature steps as complete via API."""
1573
- import json
1574
- import http.client
1575
-
1576
- # Parse step indices (support both space-separated and comma-separated)
1577
- step_indices = []
1578
- for step_arg in args.steps:
1579
- if ',' in step_arg:
1580
- # Comma-separated: "0,1,2"
1581
- step_indices.extend(int(s.strip()) for s in step_arg.split(',') if s.strip())
1582
- else:
1583
- # Space-separated: "0" "1" "2"
1584
- step_indices.append(int(step_arg))
1585
-
1586
- # Remove duplicates and sort
1587
- step_indices = sorted(set(step_indices))
1588
-
1589
- if not step_indices:
1590
- print("Error: No step indices provided", file=sys.stderr)
1591
- sys.exit(1)
1592
-
1593
- # Make API requests for each step
1594
- success_count = 0
1595
- error_count = 0
1596
- results = []
1597
-
1598
- for step_index in step_indices:
1599
- try:
1600
- conn = http.client.HTTPConnection(args.host, args.port, timeout=5)
1601
- body = json.dumps({"complete_step": step_index})
1602
- headers = {"Content-Type": "application/json"}
1603
-
1604
- conn.request("PATCH", f"/api/{args.collection}/{args.id}", body, headers)
1605
- response = conn.getresponse()
1606
- response_data = response.read().decode()
1607
-
1608
- if response.status == 200:
1609
- success_count += 1
1610
- results.append({"step": step_index, "status": "success"})
1611
- if args.format != "json":
1612
- print(f"āœ“ Marked step {step_index} complete")
1613
- else:
1614
- error_count += 1
1615
- results.append({"step": step_index, "status": "error", "message": response_data})
1616
- if args.format != "json":
1617
- print(f"āœ— Failed to mark step {step_index} complete: {response_data}", file=sys.stderr)
1618
-
1619
- conn.close()
1620
- except Exception as e:
1621
- error_count += 1
1622
- results.append({"step": step_index, "status": "error", "message": str(e)})
1623
- if args.format != "json":
1624
- print(f"āœ— Error marking step {step_index} complete: {e}", file=sys.stderr)
1625
-
1626
- # Output results
1627
- if args.format == "json":
1628
- output = {
1629
- "feature_id": args.id,
1630
- "total_steps": len(step_indices),
1631
- "success": success_count,
1632
- "errors": error_count,
1633
- "results": results
1634
- }
1635
- print(json.dumps(output, indent=2))
1636
- else:
1637
- print(f"\nCompleted {success_count}/{len(step_indices)} steps for feature '{args.id}'")
1638
- if error_count > 0:
1639
- sys.exit(1)
1640
-
1641
-
1642
-
1643
- def cmd_feature_delete(args):
1644
- """Delete a feature."""
1645
- from htmlgraph import SDK
1646
- import json
1647
- import sys
1648
-
1649
- sdk = SDK(agent=getattr(args, "agent", "cli"), directory=args.graph_dir)
1650
-
1651
- # Get the feature first to show confirmation
1652
- collection = getattr(sdk, args.collection, None)
1653
- if not collection:
1654
- print(f"Error: Collection '{args.collection}' not found", file=sys.stderr)
1655
- sys.exit(1)
1656
-
1657
- feature = collection.get(args.id)
1658
- if not feature:
1659
- print(f"Error: {args.collection.rstrip('s').capitalize()} '{args.id}' not found", file=sys.stderr)
1660
- sys.exit(1)
1661
-
1662
- # Confirmation prompt (unless --yes flag)
1663
- if not args.yes:
1664
- print(f"Delete {args.collection.rstrip('s')} '{args.id}'?")
1665
- print(f" Title: {feature.title}")
1666
- print(f" Status: {feature.status}")
1667
- print(f"\nThis cannot be undone. Continue? [y/N] ", end="")
1668
-
1669
- response = input().strip().lower()
1670
- if response not in ('y', 'yes'):
1671
- print("Cancelled")
1672
- sys.exit(0)
1673
-
1674
- # Delete
1675
- try:
1676
- success = collection.delete(args.id)
1677
- if success:
1678
- if args.format == "json":
1679
- data = {
1680
- "id": args.id,
1681
- "title": feature.title,
1682
- "deleted": True
1683
- }
1684
- print(json.dumps(data, indent=2))
1685
- else:
1686
- print(f"Deleted {args.collection.rstrip('s')}: {args.id}")
1687
- print(f" Title: {feature.title}")
1688
- else:
1689
- print(f"Error: Failed to delete {args.collection.rstrip('s')} '{args.id}'", file=sys.stderr)
1690
- sys.exit(1)
1691
- except Exception as e:
1692
- print(f"Error: {e}", file=sys.stderr)
1693
- sys.exit(1)
1694
-
1695
-
1696
- def cmd_track_new(args):
1697
- """Create a new track."""
1698
- from htmlgraph.track_manager import TrackManager
1699
- import json
1700
-
1701
- manager = TrackManager(args.graph_dir)
1702
-
1703
- try:
1704
- track = manager.create_track(
1705
- title=args.title,
1706
- description=args.description or "",
1707
- priority=args.priority,
1708
- )
1709
- except ValueError as e:
1710
- print(f"Error: {e}", file=sys.stderr)
1711
- sys.exit(1)
1712
-
1713
- if args.format == "json":
1714
- data = {
1715
- "id": track.id,
1716
- "title": track.title,
1717
- "status": track.status,
1718
- "priority": track.priority,
1719
- "path": f"{args.graph_dir}/tracks/{track.id}/"
1720
- }
1721
- print(json.dumps(data, indent=2))
1722
- else:
1723
- print(f"Created track: {track.id}")
1724
- print(f" Title: {track.title}")
1725
- print(f" Status: {track.status}")
1726
- print(f" Priority: {track.priority}")
1727
- print(f" Path: {args.graph_dir}/tracks/{track.id}/")
1728
- print(f"\nNext steps:")
1729
- print(f" - Create spec: htmlgraph track spec {track.id} 'Spec Title'")
1730
- print(f" - Create plan: htmlgraph track plan {track.id} 'Plan Title'")
1731
-
1732
-
1733
- def cmd_track_list(args):
1734
- """List all tracks."""
1735
- from htmlgraph.track_manager import TrackManager
1736
- import json
1737
-
1738
- manager = TrackManager(args.graph_dir)
1739
- track_ids = manager.list_tracks()
1740
-
1741
- if args.format == "json":
1742
- print(json.dumps({"tracks": track_ids}, indent=2))
1743
- else:
1744
- if not track_ids:
1745
- print("No tracks found.")
1746
- print(f"\nCreate a track with: htmlgraph track new 'Track Title'")
1747
- return
1748
-
1749
- print(f"Tracks in {args.graph_dir}/tracks/:")
1750
- print("=" * 60)
1751
- for track_id in track_ids:
1752
- # Check for both consolidated (single file) and directory-based formats
1753
- track_file = Path(args.graph_dir) / "tracks" / f"{track_id}.html"
1754
- track_dir = Path(args.graph_dir) / "tracks" / track_id
1755
-
1756
- if track_file.exists():
1757
- # Consolidated format - spec and plan are in the same file
1758
- content = track_file.read_text(encoding="utf-8")
1759
- has_spec = 'data-section="overview"' in content or 'data-section="requirements"' in content
1760
- has_plan = 'data-section="plan"' in content
1761
- format_indicator = " (consolidated)"
1762
- else:
1763
- # Directory format
1764
- has_spec = (track_dir / "spec.html").exists()
1765
- has_plan = (track_dir / "plan.html").exists()
1766
- format_indicator = ""
1767
-
1768
- components = []
1769
- if has_spec:
1770
- components.append("spec")
1771
- if has_plan:
1772
- components.append("plan")
1773
-
1774
- components_str = f" [{', '.join(components)}]" if components else " [empty]"
1775
- print(f" {track_id}{components_str}{format_indicator}")
1776
-
1777
-
1778
- def cmd_track_spec(args):
1779
- """Create a spec for a track."""
1780
- from htmlgraph.track_manager import TrackManager
1781
- import json
1782
-
1783
- manager = TrackManager(args.graph_dir)
1784
-
1785
- # Check if track uses consolidated format
1786
- if manager.is_consolidated(args.track_id):
1787
- track_file = manager.tracks_dir / f"{args.track_id}.html"
1788
- print(f"Track '{args.track_id}' uses consolidated single-file format.")
1789
- print(f"Spec is embedded in: {track_file}")
1790
- print(f"\nTo create a track with separate spec/plan files, use:")
1791
- print(f" sdk.tracks.builder().separate_files().title('...').create()")
1792
- return
1793
-
1794
- try:
1795
- spec = manager.create_spec(
1796
- track_id=args.track_id,
1797
- title=args.title,
1798
- overview=args.overview or "",
1799
- context=args.context or "",
1800
- author=args.author,
1801
- )
1802
- except ValueError as e:
1803
- print(f"Error: {e}", file=sys.stderr)
1804
- sys.exit(1)
1805
- except FileNotFoundError as e:
1806
- print(f"Error: {e}", file=sys.stderr)
1807
- sys.exit(1)
1808
-
1809
- if args.format == "json":
1810
- data = {
1811
- "id": spec.id,
1812
- "title": spec.title,
1813
- "track_id": spec.track_id,
1814
- "status": spec.status,
1815
- "path": f"{args.graph_dir}/tracks/{args.track_id}/spec.html"
1816
- }
1817
- print(json.dumps(data, indent=2))
1818
- else:
1819
- print(f"Created spec: {spec.id}")
1820
- print(f" Title: {spec.title}")
1821
- print(f" Track: {spec.track_id}")
1822
- print(f" Status: {spec.status}")
1823
- print(f" Path: {args.graph_dir}/tracks/{args.track_id}/spec.html")
1824
- print(f"\nView spec: open {args.graph_dir}/tracks/{args.track_id}/spec.html")
1825
-
1826
-
1827
- def cmd_track_plan(args):
1828
- """Create a plan for a track."""
1829
- from htmlgraph.track_manager import TrackManager
1830
- import json
1831
-
1832
- manager = TrackManager(args.graph_dir)
1833
-
1834
- # Check if track uses consolidated format
1835
- if manager.is_consolidated(args.track_id):
1836
- track_file = manager.tracks_dir / f"{args.track_id}.html"
1837
- print(f"Track '{args.track_id}' uses consolidated single-file format.")
1838
- print(f"Plan is embedded in: {track_file}")
1839
- print(f"\nTo create a track with separate spec/plan files, use:")
1840
- print(f" sdk.tracks.builder().separate_files().title('...').create()")
1841
- return
1842
-
1843
- try:
1844
- plan = manager.create_plan(
1845
- track_id=args.track_id,
1846
- title=args.title,
1847
- )
1848
- except ValueError as e:
1849
- print(f"Error: {e}", file=sys.stderr)
1850
- sys.exit(1)
1851
- except FileNotFoundError as e:
1852
- print(f"Error: {e}", file=sys.stderr)
1853
- sys.exit(1)
1854
-
1855
- if args.format == "json":
1856
- data = {
1857
- "id": plan.id,
1858
- "title": plan.title,
1859
- "track_id": plan.track_id,
1860
- "status": plan.status,
1861
- "path": f"{args.graph_dir}/tracks/{args.track_id}/plan.html"
1862
- }
1863
- print(json.dumps(data, indent=2))
1864
- else:
1865
- print(f"Created plan: {plan.id}")
1866
- print(f" Title: {plan.title}")
1867
- print(f" Track: {plan.track_id}")
1868
- print(f" Status: {plan.status}")
1869
- print(f" Path: {args.graph_dir}/tracks/{args.track_id}/plan.html")
1870
- print(f"\nView plan: open {args.graph_dir}/tracks/{args.track_id}/plan.html")
1871
-
1872
-
1873
- def cmd_track_delete(args):
1874
- """Delete a track."""
1875
- from htmlgraph.track_manager import TrackManager
1876
- import json
1877
-
1878
- manager = TrackManager(args.graph_dir)
1879
-
1880
- try:
1881
- manager.delete_track(args.track_id)
1882
- except ValueError as e:
1883
- print(f"Error: {e}", file=sys.stderr)
1884
- sys.exit(1)
1885
-
1886
- if args.format == "json":
1887
- data = {
1888
- "deleted": True,
1889
- "track_id": args.track_id
1890
- }
1891
- print(json.dumps(data, indent=2))
1892
- else:
1893
- print(f"āœ“ Deleted track: {args.track_id}")
1894
- print(f" Removed: {args.graph_dir}/tracks/{args.track_id}/")
1895
-
1896
-
1897
- def create_default_index(path: Path):
1898
- """
1899
- Create a default index.html for new projects.
1900
-
1901
- The dashboard UI evolves quickly; to keep new projects consistent with the
1902
- current dashboard, prefer a packaged HTML template over a hardcoded string.
1903
- """
1904
- template = Path(__file__).parent / "dashboard.html"
1905
- try:
1906
- if template.exists():
1907
- path.write_text(template.read_text(encoding="utf-8"), encoding="utf-8")
1908
- return
1909
- except Exception:
1910
- pass
1911
-
1912
- # Fallback (rare): minimal landing page.
1913
- path.write_text(
1914
- "<!doctype html><html><head><meta charset='utf-8'><title>HtmlGraph</title></head>"
1915
- "<body><h1>HtmlGraph</h1><p>Run <code>htmlgraph serve</code> and open "
1916
- "<code>http://localhost:8080</code>.</p></body></html>",
1917
- encoding="utf-8",
1918
- )
1919
-
1920
-
1921
- def main():
1922
- parser = argparse.ArgumentParser(
1923
- description="HtmlGraph - HTML is All You Need",
1924
- formatter_class=argparse.RawDescriptionHelpFormatter,
1925
- epilog="""
1926
- Examples:
1927
- htmlgraph init # Initialize .htmlgraph in current dir
1928
- htmlgraph serve # Start server on port 8080
1929
- htmlgraph status # Show graph status
1930
- htmlgraph query "[data-status='todo']" # Query nodes
1931
-
1932
- Session Management:
1933
- htmlgraph session start # Start a new session (auto-ID)
1934
- htmlgraph session start --id my-session --title "Bug fixes"
1935
- htmlgraph session end my-session # End a session
1936
- htmlgraph session list # List all sessions
1937
- htmlgraph activity Edit "Edit: src/app.py:45-60" --files src/app.py
1938
-
1939
- Feature Management:
1940
- htmlgraph feature list # List all features
1941
- htmlgraph feature start feat-001 # Start working on a feature
1942
- htmlgraph feature primary feat-001 # Set primary feature
1943
- htmlgraph feature claim feat-001 # Claim feature for current agent
1944
- htmlgraph feature release feat-001 # Release claim
1945
- htmlgraph feature auto-release # Release all claims for agent
1946
- htmlgraph feature step-complete feat-001 0 1 2 # Mark steps complete
1947
- htmlgraph feature complete feat-001 # Mark feature as done
1948
-
1949
- Track Management (Conductor-Style Planning):
1950
- htmlgraph track new "User Authentication" # Create a new track
1951
- htmlgraph track list # List all tracks
1952
- htmlgraph track spec track-001-auth "Auth Specification" # Create spec
1953
- htmlgraph track plan track-001-auth "Auth Implementation Plan" # Create plan
1954
-
1955
- Analytics:
1956
- htmlgraph analytics # Project-wide work type analytics
1957
- htmlgraph analytics --recent 10 # Analyze last 10 sessions
1958
- htmlgraph analytics --session-id session-123 # Detailed session metrics
1959
-
1960
- curl Examples:
1961
- curl localhost:8080/api/status
1962
- curl localhost:8080/api/features
1963
- curl -X POST localhost:8080/api/features -d '{"title": "New feature"}'
1964
- curl -X PATCH localhost:8080/api/features/feat-001 -d '{"status": "done"}'
1965
- """
1966
- )
1967
-
1968
- subparsers = parser.add_subparsers(dest="command", help="Command to run")
1969
-
1970
- # serve
1971
- serve_parser = subparsers.add_parser("serve", help="Start the HtmlGraph server")
1972
- serve_parser.add_argument("--port", "-p", type=int, default=8080, help="Port (default: 8080)")
1973
- serve_parser.add_argument("--host", default="0.0.0.0", help="Host to bind to (default: 0.0.0.0)")
1974
- serve_parser.add_argument("--graph-dir", "-g", default=".htmlgraph", help="Graph directory")
1975
- serve_parser.add_argument("--static-dir", "-s", default=".", help="Static files directory")
1976
- serve_parser.add_argument("--no-watch", action="store_true", help="Disable file watching (auto-reload disabled)")
1977
-
1978
- # init
1979
- init_parser = subparsers.add_parser("init", help="Initialize .htmlgraph directory")
1980
- init_parser.add_argument("dir", nargs="?", default=".", help="Directory to initialize")
1981
- init_parser.add_argument("--install-hooks", action="store_true", help="Install Git hooks for event logging")
1982
- init_parser.add_argument("--no-index", action="store_true", help="Do not create the analytics cache (index.sqlite)")
1983
- init_parser.add_argument("--no-update-gitignore", action="store_true", help="Do not update/create .gitignore for HtmlGraph cache files")
1984
- init_parser.add_argument("--no-events-keep", action="store_true", help="Do not create .htmlgraph/events/.gitkeep")
1985
-
1986
- # status
1987
- status_parser = subparsers.add_parser("status", help="Show graph status")
1988
- status_parser.add_argument("--graph-dir", "-g", default=".htmlgraph", help="Graph directory")
1989
-
1990
- # query
1991
- query_parser = subparsers.add_parser("query", help="Query nodes with CSS selector")
1992
- query_parser.add_argument("selector", help="CSS selector (e.g. [data-status='todo'])")
1993
- query_parser.add_argument("--graph-dir", "-g", default=".htmlgraph", help="Graph directory")
1994
- query_parser.add_argument("--format", "-f", choices=["text", "json"], default="text", help="Output format")
1995
-
1996
- # =========================================================================
1997
- # Session Management
1998
- # =========================================================================
1999
-
2000
- # session (with subcommands)
2001
- session_parser = subparsers.add_parser("session", help="Session management")
2002
- session_subparsers = session_parser.add_subparsers(dest="session_command", help="Session command")
2003
-
2004
- # session start
2005
- session_start = session_subparsers.add_parser("start", help="Start a new session")
2006
- session_start.add_argument("--id", help="Session ID (auto-generated if not provided)")
2007
- session_start.add_argument("--agent", default="claude-code", help="Agent name")
2008
- session_start.add_argument("--title", help="Session title")
2009
- session_start.add_argument("--graph-dir", "-g", default=".htmlgraph", help="Graph directory")
2010
- session_start.add_argument("--format", "-f", choices=["text", "json"], default="text", help="Output format")
2011
-
2012
- # session end
2013
- session_end = session_subparsers.add_parser("end", help="End a session")
2014
- session_end.add_argument("id", help="Session ID to end")
2015
- session_end.add_argument("--notes", help="Handoff notes for the next session")
2016
- session_end.add_argument("--recommend", help="Recommended next steps")
2017
- session_end.add_argument("--blocker", action="append", default=[], help="Blocker to record (repeatable)")
2018
- session_end.add_argument("--graph-dir", "-g", default=".htmlgraph", help="Graph directory")
2019
- session_end.add_argument("--format", "-f", choices=["text", "json"], default="text", help="Output format")
2020
-
2021
- # session handoff
2022
- session_handoff = session_subparsers.add_parser("handoff", help="Set or show session handoff context")
2023
- session_handoff.add_argument("--session-id", help="Session ID (defaults to active session)")
2024
- session_handoff.add_argument("--agent", help="Agent filter (used for --show when no session provided)")
2025
- session_handoff.add_argument("--notes", help="Handoff notes for the next session")
2026
- session_handoff.add_argument("--recommend", help="Recommended next steps")
2027
- session_handoff.add_argument("--blocker", action="append", default=[], help="Blocker to record (repeatable)")
2028
- session_handoff.add_argument("--show", action="store_true", help="Show handoff context instead of setting it")
2029
- session_handoff.add_argument("--graph-dir", "-g", default=".htmlgraph", help="Graph directory")
2030
- session_handoff.add_argument("--format", "-f", choices=["text", "json"], default="text", help="Output format")
2031
-
2032
- # session list
2033
- session_list = session_subparsers.add_parser("list", help="List all sessions")
2034
- session_list.add_argument("--graph-dir", "-g", default=".htmlgraph", help="Graph directory")
2035
- session_list.add_argument("--format", "-f", choices=["text", "json"], default="text", help="Output format")
2036
-
2037
- # session status-report (and resume alias)
2038
- session_report = session_subparsers.add_parser("status-report", help="Print comprehensive session status report")
2039
- session_report.add_argument("--graph-dir", "-g", default=".htmlgraph", help="Graph directory")
2040
-
2041
- session_resume = session_subparsers.add_parser("resume", help="Alias for status-report (Resume session context)")
2042
- session_resume.add_argument("--graph-dir", "-g", default=".htmlgraph", help="Graph directory")
2043
-
2044
- # session dedupe
2045
- session_dedupe = session_subparsers.add_parser(
2046
- "dedupe",
2047
- help="Move SessionStart-only sessions into a subfolder",
2048
- )
2049
- session_dedupe.add_argument("--graph-dir", "-g", default=".htmlgraph", help="Graph directory")
2050
- session_dedupe.add_argument("--max-events", type=int, default=1, help="Max events to consider orphaned")
2051
- session_dedupe.add_argument("--move-dir", default="_orphans", help="Subfolder name under sessions/")
2052
- session_dedupe.add_argument("--dry-run", action="store_true", help="Show what would happen without moving files")
2053
- session_dedupe.add_argument("--no-stale-active", action="store_true", help="Do not mark extra active sessions as stale")
2054
-
2055
- # session link
2056
- session_link = session_subparsers.add_parser(
2057
- "link",
2058
- help="Link a feature to a session retroactively"
2059
- )
2060
- session_link.add_argument("session_id", help="Session ID")
2061
- session_link.add_argument("feature_id", help="Feature ID to link")
2062
- session_link.add_argument("--collection", "-c", default="features", help="Feature collection")
2063
- session_link.add_argument("--graph-dir", "-g", default=".htmlgraph", help="Graph directory")
2064
- session_link.add_argument("--bidirectional", "-b", action="store_true", help="Also add session to feature's implemented-in edges")
2065
- session_link.add_argument("--format", "-f", choices=["text", "json"], default="text", help="Output format")
2066
-
2067
- # session validate-attribution
2068
- session_validate = session_subparsers.add_parser(
2069
- "validate-attribution",
2070
- help="Validate feature attribution and tracking"
2071
- )
2072
- session_validate.add_argument("feature_id", help="Feature ID to validate")
2073
- session_validate.add_argument("--collection", "-c", default="features", help="Feature collection")
2074
- session_validate.add_argument("--graph-dir", "-g", default=".htmlgraph", help="Graph directory")
2075
- session_validate.add_argument("--format", "-f", choices=["text", "json"], default="text", help="Output format")
2076
-
2077
- # activity (legacy: was "track")
2078
- activity_parser = subparsers.add_parser("activity", help="Track an activity (legacy: use 'htmlgraph track' for new features)")
2079
- activity_parser.add_argument("tool", help="Tool name (Edit, Bash, Read, etc.)")
2080
- activity_parser.add_argument("summary", help="Activity summary")
2081
- activity_parser.add_argument("--session", help="Session ID (uses active session if not provided)")
2082
- activity_parser.add_argument("--files", nargs="*", help="Files involved")
2083
- activity_parser.add_argument("--failed", action="store_true", help="Mark as failed")
2084
- activity_parser.add_argument("--graph-dir", "-g", default=".htmlgraph", help="Graph directory")
2085
- activity_parser.add_argument("--format", "-f", choices=["text", "json"], default="text", help="Output format")
2086
-
2087
- # =========================================================================
2088
- # Work Management (Smart Routing)
2089
- # =========================================================================
2090
-
2091
- # work (with subcommands)
2092
- work_parser = subparsers.add_parser("work", help="Work management with smart routing")
2093
- work_subparsers = work_parser.add_subparsers(dest="work_command", help="Work command")
2094
-
2095
- # work next
2096
- work_next = work_subparsers.add_parser("next", help="Get next best task using smart routing")
2097
- work_next.add_argument(
2098
- "--agent",
2099
- default=os.environ.get("HTMLGRAPH_AGENT") or "claude",
2100
- help="Agent ID (default: $HTMLGRAPH_AGENT or 'claude')",
2101
- )
2102
- work_next.add_argument("--graph-dir", "-g", default=".htmlgraph", help="Graph directory")
2103
- work_next.add_argument("--auto-claim", action="store_true", help="Automatically claim the task")
2104
- work_next.add_argument("--min-score", type=float, default=20.0, help="Minimum routing score (default: 20.0)")
2105
- work_next.add_argument("--format", "-f", choices=["text", "json"], default="text", help="Output format")
2106
-
2107
- # work queue
2108
- work_queue = work_subparsers.add_parser("queue", help="Get prioritized work queue")
2109
- work_queue.add_argument(
2110
- "--agent",
2111
- default=os.environ.get("HTMLGRAPH_AGENT") or "claude",
2112
- help="Agent ID (default: $HTMLGRAPH_AGENT or 'claude')",
2113
- )
2114
- work_queue.add_argument("--graph-dir", "-g", default=".htmlgraph", help="Graph directory")
2115
- work_queue.add_argument("--limit", "-l", type=int, default=10, help="Maximum tasks to show (default: 10)")
2116
- work_queue.add_argument("--min-score", type=float, default=20.0, help="Minimum routing score (default: 20.0)")
2117
- work_queue.add_argument("--format", "-f", choices=["text", "json"], default="text", help="Output format")
2118
-
2119
- # agent (with subcommands)
2120
- agent_parser = subparsers.add_parser("agent", help="Agent management")
2121
- agent_subparsers = agent_parser.add_subparsers(dest="agent_command", help="Agent command")
2122
-
2123
- # agent list
2124
- agent_list = agent_subparsers.add_parser("list", help="List all registered agents")
2125
- agent_list.add_argument("--graph-dir", "-g", default=".htmlgraph", help="Graph directory")
2126
- agent_list.add_argument("--active-only", action="store_true", help="Only show active agents")
2127
- agent_list.add_argument("--format", "-f", choices=["text", "json"], default="text", help="Output format")
2128
-
2129
- # =========================================================================
2130
- # Feature Management
2131
- # =========================================================================
2132
-
2133
- # feature (with subcommands)
2134
- feature_parser = subparsers.add_parser("feature", help="Feature management")
2135
- feature_subparsers = feature_parser.add_subparsers(dest="feature_command", help="Feature command")
2136
-
2137
- # feature create
2138
- feature_create = feature_subparsers.add_parser("create", help="Create a new feature")
2139
- feature_create.add_argument("title", help="Feature title")
2140
- feature_create.add_argument("--collection", "-c", default="features", help="Collection (features, bugs)")
2141
- feature_create.add_argument("--description", "-d", help="Description")
2142
- feature_create.add_argument("--priority", "-p", default="medium", choices=["low", "medium", "high", "critical"], help="Priority")
2143
- feature_create.add_argument("--steps", nargs="*", help="Implementation steps")
2144
- feature_create.add_argument(
2145
- "--agent",
2146
- default=os.environ.get("HTMLGRAPH_AGENT") or "cli",
2147
- help="Agent name for attribution (default: $HTMLGRAPH_AGENT or 'cli')",
2148
- )
2149
- feature_create.add_argument("--graph-dir", "-g", default=".htmlgraph", help="Graph directory")
2150
- feature_create.add_argument("--format", "-f", choices=["text", "json"], default="text", help="Output format")
2151
-
2152
- # feature start
2153
- feature_start = feature_subparsers.add_parser("start", help="Start working on a feature")
2154
- feature_start.add_argument("id", help="Feature ID")
2155
- feature_start.add_argument("--collection", "-c", default="features", help="Collection (features, bugs)")
2156
- feature_start.add_argument(
2157
- "--agent",
2158
- default=os.environ.get("HTMLGRAPH_AGENT") or "cli",
2159
- help="Agent name for attribution (default: $HTMLGRAPH_AGENT or 'cli')",
2160
- )
2161
- feature_start.add_argument("--graph-dir", "-g", default=".htmlgraph", help="Graph directory")
2162
- feature_start.add_argument("--format", "-f", choices=["text", "json"], default="text", help="Output format")
2163
-
2164
- # feature complete
2165
- feature_complete = feature_subparsers.add_parser("complete", help="Mark feature as complete")
2166
- feature_complete.add_argument("id", help="Feature ID")
2167
- feature_complete.add_argument("--collection", "-c", default="features", help="Collection (features, bugs)")
2168
- feature_complete.add_argument(
2169
- "--agent",
2170
- default=os.environ.get("HTMLGRAPH_AGENT") or "cli",
2171
- help="Agent name for attribution (default: $HTMLGRAPH_AGENT or 'cli')",
2172
- )
2173
- feature_complete.add_argument("--graph-dir", "-g", default=".htmlgraph", help="Graph directory")
2174
- feature_complete.add_argument("--format", "-f", choices=["text", "json"], default="text", help="Output format")
2175
-
2176
- # feature primary
2177
- feature_primary = feature_subparsers.add_parser("primary", help="Set primary feature")
2178
- feature_primary.add_argument("id", help="Feature ID")
2179
- feature_primary.add_argument("--collection", "-c", default="features", help="Collection (features, bugs)")
2180
- feature_primary.add_argument(
2181
- "--agent",
2182
- default=os.environ.get("HTMLGRAPH_AGENT") or "cli",
2183
- help="Agent name for attribution (default: $HTMLGRAPH_AGENT or 'cli')",
2184
- )
2185
- feature_primary.add_argument("--graph-dir", "-g", default=".htmlgraph", help="Graph directory")
2186
- feature_primary.add_argument("--format", "-f", choices=["text", "json"], default="text", help="Output format")
2187
-
2188
- # feature claim
2189
- feature_claim = feature_subparsers.add_parser("claim", help="Claim a feature")
2190
- feature_claim.add_argument("id", help="Feature ID")
2191
- feature_claim.add_argument("--collection", "-c", default="features", help="Collection (features, bugs)")
2192
- feature_claim.add_argument(
2193
- "--agent",
2194
- default=os.environ.get("HTMLGRAPH_AGENT") or "cli",
2195
- help="Agent name for attribution (default: $HTMLGRAPH_AGENT or 'cli')",
2196
- )
2197
- feature_claim.add_argument("--graph-dir", "-g", default=".htmlgraph", help="Graph directory")
2198
- feature_claim.add_argument("--format", "-f", choices=["text", "json"], default="text", help="Output format")
2199
-
2200
- # feature release
2201
- feature_release = feature_subparsers.add_parser("release", help="Release a feature claim")
2202
- feature_release.add_argument("id", help="Feature ID")
2203
- feature_release.add_argument("--collection", "-c", default="features", help="Collection (features, bugs)")
2204
- feature_release.add_argument(
2205
- "--agent",
2206
- default=os.environ.get("HTMLGRAPH_AGENT") or "cli",
2207
- help="Agent name for attribution (default: $HTMLGRAPH_AGENT or 'cli')",
2208
- )
2209
- feature_release.add_argument("--graph-dir", "-g", default=".htmlgraph", help="Graph directory")
2210
- feature_release.add_argument("--format", "-f", choices=["text", "json"], default="text", help="Output format")
2211
-
2212
- # feature auto-release
2213
- feature_auto_release = feature_subparsers.add_parser("auto-release", help="Release all features claimed by agent")
2214
- feature_auto_release.add_argument(
2215
- "--agent",
2216
- default=os.environ.get("HTMLGRAPH_AGENT") or "cli",
2217
- help="Agent name for attribution (default: $HTMLGRAPH_AGENT or 'cli')",
2218
- )
2219
- feature_auto_release.add_argument("--graph-dir", "-g", default=".htmlgraph", help="Graph directory")
2220
- feature_auto_release.add_argument("--format", "-f", choices=["text", "json"], default="text", help="Output format")
2221
-
2222
- # feature list
2223
- feature_list = feature_subparsers.add_parser("list", help="List features")
2224
- feature_list.add_argument("--status", "-s", help="Filter by status")
2225
- feature_list.add_argument("--collection", "-c", default="features", help="Collection (features, bugs)")
2226
- feature_list.add_argument("--graph-dir", "-g", default=".htmlgraph", help="Graph directory")
2227
- feature_list.add_argument("--format", "-f", choices=["text", "json"], default="text", help="Output format")
2228
-
2229
- # feature step-complete
2230
- feature_step_complete = feature_subparsers.add_parser("step-complete", help="Mark feature step(s) as complete")
2231
- feature_step_complete.add_argument("id", help="Feature ID")
2232
- feature_step_complete.add_argument("steps", nargs="+", help="Step index(es) to mark complete (0-based, supports: 0 1 2 or 0,1,2)")
2233
- feature_step_complete.add_argument("--collection", "-c", default="features", help="Collection (features, bugs)")
2234
- feature_step_complete.add_argument(
2235
- "--agent",
2236
- default=os.environ.get("HTMLGRAPH_AGENT") or "cli",
2237
- help="Agent name for attribution (default: $HTMLGRAPH_AGENT or 'cli')",
2238
- )
2239
- feature_step_complete.add_argument("--graph-dir", "-g", default=".htmlgraph", help="Graph directory")
2240
- feature_step_complete.add_argument("--format", "-f", choices=["text", "json"], default="text", help="Output format")
2241
- feature_step_complete.add_argument("--host", default="localhost", help="API host (default: localhost)")
2242
- feature_step_complete.add_argument("--port", type=int, default=8080, help="API port (default: 8080)")
2243
-
2244
- # feature delete
2245
- feature_delete = feature_subparsers.add_parser("delete", help="Delete a feature")
2246
- feature_delete.add_argument("id", help="Feature ID to delete")
2247
- feature_delete.add_argument("--collection", "-c", default="features", help="Collection (features, bugs)")
2248
- feature_delete.add_argument("--yes", "-y", action="store_true", help="Skip confirmation prompt")
2249
- feature_delete.add_argument("--graph-dir", "-g", default=".htmlgraph", help="Graph directory")
2250
- feature_delete.add_argument("--format", "-f", choices=["text", "json"], default="text", help="Output format")
2251
-
2252
- # =========================================================================
2253
- # Track Management (Conductor-Style Planning)
2254
- # =========================================================================
2255
-
2256
- # track (with subcommands)
2257
- track_parser = subparsers.add_parser("track", help="Track management (Conductor-style planning)")
2258
- track_subparsers = track_parser.add_subparsers(dest="track_command", help="Track command")
2259
-
2260
- # track new
2261
- track_new = track_subparsers.add_parser("new", help="Create a new track")
2262
- track_new.add_argument("title", help="Track title")
2263
- track_new.add_argument("--description", "-d", help="Track description")
2264
- track_new.add_argument("--priority", "-p", default="medium", choices=["low", "medium", "high", "critical"], help="Priority")
2265
- track_new.add_argument("--graph-dir", "-g", default=".htmlgraph", help="Graph directory")
2266
- track_new.add_argument("--format", "-f", choices=["text", "json"], default="text", help="Output format")
2267
-
2268
- # track list
2269
- track_list = track_subparsers.add_parser("list", help="List all tracks")
2270
- track_list.add_argument("--graph-dir", "-g", default=".htmlgraph", help="Graph directory")
2271
- track_list.add_argument("--format", "-f", choices=["text", "json"], default="text", help="Output format")
2272
-
2273
- # track spec
2274
- track_spec = track_subparsers.add_parser("spec", help="Create a spec for a track")
2275
- track_spec.add_argument("track_id", help="Track ID")
2276
- track_spec.add_argument("title", help="Spec title")
2277
- track_spec.add_argument("--overview", "-o", help="Spec overview")
2278
- track_spec.add_argument("--context", "-c", help="Context/rationale")
2279
- track_spec.add_argument("--author", "-a", default="claude-code", help="Spec author")
2280
- track_spec.add_argument("--graph-dir", "-g", default=".htmlgraph", help="Graph directory")
2281
- track_spec.add_argument("--format", "-f", choices=["text", "json"], default="text", help="Output format")
2282
-
2283
- # track plan
2284
- track_plan = track_subparsers.add_parser("plan", help="Create a plan for a track")
2285
- track_plan.add_argument("track_id", help="Track ID")
2286
- track_plan.add_argument("title", help="Plan title")
2287
- track_plan.add_argument("--graph-dir", "-g", default=".htmlgraph", help="Graph directory")
2288
- track_plan.add_argument("--format", "-f", choices=["text", "json"], default="text", help="Output format")
2289
-
2290
- # track delete
2291
- track_delete = track_subparsers.add_parser("delete", help="Delete a track")
2292
- track_delete.add_argument("track_id", help="Track ID to delete")
2293
- track_delete.add_argument("--graph-dir", "-g", default=".htmlgraph", help="Graph directory")
2294
- track_delete.add_argument("--format", "-f", choices=["text", "json"], default="text", help="Output format")
2295
-
2296
- # =========================================================================
2297
- # Analytics
2298
- # =========================================================================
2299
-
2300
- # analytics
2301
- analytics_parser = subparsers.add_parser("analytics", help="Work type analytics and project health metrics")
2302
- analytics_parser.add_argument("--session-id", "-s", help="Analyze specific session ID")
2303
- analytics_parser.add_argument("--recent", "-r", type=int, help="Analyze N recent sessions")
2304
- analytics_parser.add_argument("--agent", default="cli", help="Agent name for SDK initialization")
2305
- analytics_parser.add_argument("--graph-dir", "-g", default=".htmlgraph", help="Graph directory")
2306
-
2307
- # =========================================================================
2308
- # Events & Analytics Index
2309
- # =========================================================================
2310
-
2311
- events_parser = subparsers.add_parser("events", help="Event log utilities")
2312
- events_subparsers = events_parser.add_subparsers(dest="events_command", help="Events command")
2313
-
2314
- events_export = events_subparsers.add_parser(
2315
- "export-sessions",
2316
- help="Export session HTML activity logs to JSONL under .htmlgraph/events/",
2317
- )
2318
- events_export.add_argument("--graph-dir", "-g", default=".htmlgraph", help="Graph directory")
2319
- events_export.add_argument("--overwrite", action="store_true", help="Overwrite existing JSONL files")
2320
- events_export.add_argument("--include-subdirs", action="store_true", help="Include subdirectories like sessions/_orphans/")
2321
-
2322
- index_parser = subparsers.add_parser("index", help="Analytics index commands")
2323
- index_subparsers = index_parser.add_subparsers(dest="index_command", help="Index command")
2324
-
2325
- index_rebuild = index_subparsers.add_parser(
2326
- "rebuild",
2327
- help="Rebuild .htmlgraph/index.sqlite from .htmlgraph/events/*.jsonl",
2328
- )
2329
- index_rebuild.add_argument("--graph-dir", "-g", default=".htmlgraph", help="Graph directory")
2330
-
2331
- # watch
2332
- watch_parser = subparsers.add_parser("watch", help="Watch file changes and log events")
2333
- watch_parser.add_argument("--root", "-r", default=".", help="Root directory to watch")
2334
- watch_parser.add_argument("--graph-dir", "-g", default=".htmlgraph", help="Graph directory")
2335
- watch_parser.add_argument("--session-id", help="Session ID (defaults to deduped active session)")
2336
- watch_parser.add_argument("--agent", default="codex", help="Agent name for the watcher")
2337
- watch_parser.add_argument("--interval", type=float, default=2.0, help="Polling interval seconds")
2338
- watch_parser.add_argument("--batch-seconds", type=float, default=5.0, help="Batch window seconds")
2339
-
2340
- # git-event
2341
- git_event_parser = subparsers.add_parser("git-event", help="Log Git events (commit, checkout, merge, push)")
2342
- git_event_parser.add_argument("event_type", choices=["commit", "checkout", "merge", "push"], help="Type of Git event")
2343
- git_event_parser.add_argument(
2344
- "args",
2345
- nargs="*",
2346
- help="Event-specific args (checkout: old new flag; merge: squash_flag; push: remote_name remote_url)",
2347
- )
2348
-
2349
- # mcp
2350
- mcp_parser = subparsers.add_parser("mcp", help="Minimal MCP server (stdio)")
2351
- mcp_subparsers = mcp_parser.add_subparsers(dest="mcp_command", help="MCP command")
2352
- mcp_serve = mcp_subparsers.add_parser("serve", help="Serve MCP over stdio")
2353
- mcp_serve.add_argument("--graph-dir", "-g", default=".htmlgraph", help="Graph directory")
2354
- mcp_serve.add_argument("--agent", default="mcp", help="Agent name for session attribution")
2355
-
2356
- # setup
2357
- setup_parser = subparsers.add_parser("setup", help="Set up HtmlGraph for AI CLI platforms")
2358
- setup_subparsers = setup_parser.add_subparsers(dest="setup_command", help="Platform to set up")
2359
-
2360
- setup_claude = setup_subparsers.add_parser("claude", help="Set up for Claude Code")
2361
- setup_claude.add_argument("--auto-install", action="store_true", help="Automatically install when possible")
2362
-
2363
- setup_codex = setup_subparsers.add_parser("codex", help="Set up for Codex CLI")
2364
- setup_codex.add_argument("--auto-install", action="store_true", help="Automatically install when possible")
2365
-
2366
- setup_gemini = setup_subparsers.add_parser("gemini", help="Set up for Gemini CLI")
2367
- setup_gemini.add_argument("--auto-install", action="store_true", help="Automatically install when possible")
2368
-
2369
- setup_all_parser = setup_subparsers.add_parser("all", help="Set up for all supported platforms")
2370
- setup_all_parser.add_argument("--auto-install", action="store_true", help="Automatically install when possible")
2371
-
2372
- # publish
2373
- publish_parser = subparsers.add_parser("publish", help="Build and publish package to PyPI")
2374
- publish_parser.add_argument("--dry-run", action="store_true", help="Build only, do not publish")
2375
-
2376
- # sync-docs
2377
- sync_docs_parser = subparsers.add_parser("sync-docs", help="Synchronize AI agent memory files across platforms")
2378
- sync_docs_parser.add_argument("--check", action="store_true", help="Check if files are synchronized (no changes)")
2379
- sync_docs_parser.add_argument("--generate", metavar="PLATFORM", help="Generate a platform-specific file (gemini, claude, codex)")
2380
- sync_docs_parser.add_argument("--project-root", type=str, help="Project root directory (default: current directory)")
2381
- sync_docs_parser.add_argument("--force", action="store_true", help="Overwrite existing files when generating")
2382
-
2383
- # deploy
2384
- deploy_parser = subparsers.add_parser("deploy", help="Flexible deployment system for packaging and publishing")
2385
- deploy_subparsers = deploy_parser.add_subparsers(dest="deploy_command", help="Deploy command")
2386
-
2387
- # deploy init
2388
- deploy_init = deploy_subparsers.add_parser("init", help="Initialize deployment configuration")
2389
- deploy_init.add_argument("--output", "-o", help="Output file path (default: htmlgraph-deploy.toml)")
2390
- deploy_init.add_argument("--force", action="store_true", help="Overwrite existing configuration")
2391
-
2392
- # deploy run
2393
- deploy_run = deploy_subparsers.add_parser("run", help="Run deployment process")
2394
- deploy_run.add_argument("--config", "-c", help="Configuration file (default: htmlgraph-deploy.toml)")
2395
- deploy_run.add_argument("--dry-run", action="store_true", help="Show what would happen without executing")
2396
- deploy_run.add_argument("--docs-only", action="store_true", help="Only commit and push to git")
2397
- deploy_run.add_argument("--build-only", action="store_true", help="Only build package")
2398
- deploy_run.add_argument("--skip-pypi", action="store_true", help="Skip PyPI publishing")
2399
- deploy_run.add_argument("--skip-plugins", action="store_true", help="Skip plugin updates")
2400
-
2401
- # install-gemini-extension
2402
- install_gemini_parser = subparsers.add_parser(
2403
- "install-gemini-extension",
2404
- help="Install the Gemini CLI extension from the bundled package"
2405
- )
2406
-
2407
- args = parser.parse_args()
2408
-
2409
- if args.command == "serve":
2410
- cmd_serve(args)
2411
- elif args.command == "init":
2412
- cmd_init(args)
2413
- elif args.command == "status":
2414
- cmd_status(args)
2415
- elif args.command == "query":
2416
- cmd_query(args)
2417
- elif args.command == "session":
2418
- if args.session_command == "start":
2419
- cmd_session_start(args)
2420
- elif args.session_command == "end":
2421
- cmd_session_end(args)
2422
- elif args.session_command == "list":
2423
- cmd_session_list(args)
2424
- elif args.session_command == "status-report" or args.session_command == "resume":
2425
- cmd_session_status_report(args)
2426
- elif args.session_command == "dedupe":
2427
- cmd_session_dedupe(args)
2428
- elif args.session_command == "link":
2429
- cmd_session_link(args)
2430
- elif args.session_command == "validate-attribution":
2431
- cmd_session_validate_attribution(args)
2432
- elif args.session_command == "handoff":
2433
- cmd_session_handoff(args)
2434
- else:
2435
- session_parser.print_help()
2436
- sys.exit(1)
2437
- elif args.command == "activity":
2438
- # Legacy activity tracking command
2439
- cmd_track(args)
2440
- elif args.command == "track":
2441
- # New track management commands
2442
- if args.track_command == "new":
2443
- cmd_track_new(args)
2444
- elif args.track_command == "list":
2445
- cmd_track_list(args)
2446
- elif args.track_command == "spec":
2447
- cmd_track_spec(args)
2448
- elif args.track_command == "plan":
2449
- cmd_track_plan(args)
2450
- elif args.track_command == "delete":
2451
- cmd_track_delete(args)
2452
- else:
2453
- track_parser.print_help()
2454
- sys.exit(1)
2455
- elif args.command == "work":
2456
- # Work management with smart routing
2457
- if args.work_command == "next":
2458
- cmd_work_next(args)
2459
- elif args.work_command == "queue":
2460
- cmd_work_queue(args)
2461
- else:
2462
- work_parser.print_help()
2463
- sys.exit(1)
2464
- elif args.command == "agent":
2465
- # Agent management
2466
- if args.agent_command == "list":
2467
- cmd_agent_list(args)
2468
- else:
2469
- agent_parser.print_help()
2470
- sys.exit(1)
2471
- elif args.command == "feature":
2472
- if args.feature_command == "create":
2473
- cmd_feature_create(args)
2474
- elif args.feature_command == "start":
2475
- cmd_feature_start(args)
2476
- elif args.feature_command == "complete":
2477
- cmd_feature_complete(args)
2478
- elif args.feature_command == "primary":
2479
- cmd_feature_primary(args)
2480
- elif args.feature_command == "claim":
2481
- cmd_feature_claim(args)
2482
- elif args.feature_command == "release":
2483
- cmd_feature_release(args)
2484
- elif args.feature_command == "auto-release":
2485
- cmd_feature_auto_release(args)
2486
- elif args.feature_command == "list":
2487
- cmd_feature_list(args)
2488
- elif args.feature_command == "step-complete":
2489
- cmd_feature_step_complete(args)
2490
- elif args.feature_command == "delete":
2491
- cmd_feature_delete(args)
2492
- else:
2493
- feature_parser.print_help()
2494
- sys.exit(1)
2495
- elif args.command == "analytics":
2496
- from htmlgraph.cli_analytics import cmd_analytics
2497
- cmd_analytics(args)
2498
- elif args.command == "events":
2499
- if args.events_command == "export-sessions":
2500
- cmd_events_export(args)
2501
- else:
2502
- events_parser.print_help()
2503
- sys.exit(1)
2504
- elif args.command == "index":
2505
- if args.index_command == "rebuild":
2506
- cmd_index_rebuild(args)
2507
- else:
2508
- index_parser.print_help()
2509
- sys.exit(1)
2510
- elif args.command == "watch":
2511
- cmd_watch(args)
2512
- elif args.command == "git-event":
2513
- cmd_git_event(args)
2514
- elif args.command == "mcp":
2515
- if args.mcp_command == "serve":
2516
- cmd_mcp_serve(args)
2517
- else:
2518
- mcp_parser.print_help()
2519
- sys.exit(1)
2520
- elif args.command == "setup":
2521
- from htmlgraph.setup import setup_claude, setup_codex, setup_gemini, setup_all
2522
-
2523
- if args.setup_command == "claude":
2524
- setup_claude(args)
2525
- elif args.setup_command == "codex":
2526
- setup_codex(args)
2527
- elif args.setup_command == "gemini":
2528
- setup_gemini(args)
2529
- elif args.setup_command == "all":
2530
- setup_all(args)
2531
- else:
2532
- setup_parser.print_help()
2533
- sys.exit(1)
2534
- elif args.command == "publish":
2535
- cmd_publish(args)
2536
- elif args.command == "sync-docs":
2537
- cmd_sync_docs(args)
2538
- elif args.command == "deploy":
2539
- if args.deploy_command == "init":
2540
- cmd_deploy_init(args)
2541
- elif args.deploy_command == "run":
2542
- cmd_deploy_run(args)
2543
- else:
2544
- deploy_parser.print_help()
2545
- sys.exit(1)
2546
- elif args.command == "install-gemini-extension":
2547
- cmd_install_gemini_extension(args)
2548
- else:
2549
- parser.print_help()
2550
- sys.exit(1)
2551
-
2552
-
2553
- # =============================================================================
2554
- # Deployment Commands
2555
- # =============================================================================
2556
-
2557
- def cmd_deploy_init(args):
2558
- """Initialize deployment configuration."""
2559
- from htmlgraph.deploy import create_deployment_config_template
2560
-
2561
- output_path = Path(args.output or "htmlgraph-deploy.toml")
2562
-
2563
- if output_path.exists() and not args.force:
2564
- print(f"Error: {output_path} already exists. Use --force to overwrite.", file=sys.stderr)
2565
- sys.exit(1)
2566
-
2567
- create_deployment_config_template(output_path)
2568
-
2569
-
2570
- def cmd_deploy_run(args):
2571
- """Run deployment process."""
2572
- from htmlgraph.deploy import DeploymentConfig, Deployer
2573
-
2574
- # Load configuration
2575
- config_path = Path(args.config or "htmlgraph-deploy.toml")
2576
-
2577
- if not config_path.exists():
2578
- print(f"Error: Configuration file not found: {config_path}", file=sys.stderr)
2579
- print("Run 'htmlgraph deploy init' to create a template configuration.", file=sys.stderr)
2580
- sys.exit(1)
2581
-
2582
- try:
2583
- config = DeploymentConfig.from_toml(config_path)
2584
- except Exception as e:
2585
- print(f"Error loading configuration: {e}", file=sys.stderr)
2586
- sys.exit(1)
2587
-
2588
- # Handle shortcut flags
2589
- skip_steps = []
2590
- only_steps = None
2591
-
2592
- if args.docs_only:
2593
- only_steps = ['git-push']
2594
- elif args.build_only:
2595
- only_steps = ['build']
2596
- elif args.skip_pypi:
2597
- skip_steps.append('pypi-publish')
2598
- elif args.skip_plugins:
2599
- skip_steps.append('update-plugins')
2600
-
2601
- # Create deployer
2602
- deployer = Deployer(
2603
- config=config,
2604
- dry_run=args.dry_run,
2605
- skip_steps=skip_steps,
2606
- only_steps=only_steps
2607
- )
2608
-
2609
- # Run deployment
2610
- deployer.deploy()
2611
-
2612
-
2613
- # =============================================================================
2614
- # Documentation Sync Command
2615
- # =============================================================================
2616
-
2617
- def cmd_sync_docs(args):
2618
- """Synchronize AI agent memory files across platforms."""
2619
- from htmlgraph.sync_docs import check_all_files, sync_all_files, generate_platform_file
2620
-
2621
- project_root = Path(args.project_root or os.getcwd()).resolve()
2622
-
2623
- if args.check:
2624
- # Check mode
2625
- print("šŸ” Checking memory files...")
2626
- results = check_all_files(project_root)
2627
-
2628
- print("\nStatus:")
2629
- all_good = True
2630
- for filename, status in results.items():
2631
- if filename == "AGENTS.md":
2632
- if status:
2633
- print(f" āœ… {filename} exists")
2634
- else:
2635
- print(f" āŒ {filename} MISSING (required)")
2636
- all_good = False
2637
- else:
2638
- if status:
2639
- print(f" āœ… {filename} references AGENTS.md")
2640
- else:
2641
- print(f" āš ļø {filename} missing reference")
2642
- all_good = False
2643
-
2644
- if all_good:
2645
- print("\nāœ… All files are properly synchronized!")
2646
- return 0
2647
- else:
2648
- print("\nāš ļø Some files need attention")
2649
- return 1
2650
-
2651
- elif args.generate:
2652
- # Generate mode
2653
- platform = args.generate.lower()
2654
- print(f"šŸ“ Generating {platform.upper()} memory file...")
2655
-
2656
- try:
2657
- content = generate_platform_file(platform, project_root)
2658
- from htmlgraph.sync_docs import PLATFORM_TEMPLATES
2659
- template = PLATFORM_TEMPLATES[platform]
2660
- filepath = project_root / template["filename"]
2661
-
2662
- if filepath.exists() and not args.force:
2663
- print(f"āš ļø {filepath.name} already exists. Use --force to overwrite.")
2664
- return 1
2665
-
2666
- filepath.write_text(content)
2667
- print(f"āœ… Created: {filepath}")
2668
- print(f"\nThe file references AGENTS.md for core documentation.")
2669
- return 0
2670
-
2671
- except ValueError as e:
2672
- print(f"āŒ Error: {e}")
2673
- return 1
2674
-
2675
- else:
2676
- # Sync mode (default)
2677
- print("šŸ”„ Synchronizing memory files...")
2678
- changes = sync_all_files(project_root)
2679
-
2680
- print("\nResults:")
2681
- for change in changes:
2682
- print(f" {change}")
2683
-
2684
- return 1 if any("āš ļø" in c or "āŒ" in c for c in changes) else 0
2685
-
2686
-
2687
- if __name__ == "__main__":
2688
- main()