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.
- htmlgraph/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/.htmlgraph/agents.json +72 -0
- htmlgraph/.htmlgraph/htmlgraph.db +0 -0
- htmlgraph/__init__.py +173 -17
- htmlgraph/__init__.pyi +123 -0
- htmlgraph/agent_detection.py +127 -0
- htmlgraph/agent_registry.py +45 -30
- htmlgraph/agents.py +160 -107
- htmlgraph/analytics/__init__.py +9 -2
- htmlgraph/analytics/cli.py +190 -51
- htmlgraph/analytics/cost_analyzer.py +391 -0
- htmlgraph/analytics/cost_monitor.py +664 -0
- htmlgraph/analytics/cost_reporter.py +675 -0
- htmlgraph/analytics/cross_session.py +617 -0
- htmlgraph/analytics/dependency.py +192 -100
- htmlgraph/analytics/pattern_learning.py +771 -0
- htmlgraph/analytics/session_graph.py +707 -0
- htmlgraph/analytics/strategic/__init__.py +80 -0
- htmlgraph/analytics/strategic/cost_optimizer.py +611 -0
- htmlgraph/analytics/strategic/pattern_detector.py +876 -0
- htmlgraph/analytics/strategic/preference_manager.py +709 -0
- htmlgraph/analytics/strategic/suggestion_engine.py +747 -0
- htmlgraph/analytics/work_type.py +190 -14
- htmlgraph/analytics_index.py +135 -51
- htmlgraph/api/__init__.py +3 -0
- htmlgraph/api/cost_alerts_websocket.py +416 -0
- htmlgraph/api/main.py +2498 -0
- htmlgraph/api/static/htmx.min.js +1 -0
- htmlgraph/api/static/style-redesign.css +1344 -0
- htmlgraph/api/static/style.css +1079 -0
- htmlgraph/api/templates/dashboard-redesign.html +1366 -0
- htmlgraph/api/templates/dashboard.html +794 -0
- htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
- htmlgraph/api/templates/partials/activity-feed.html +1100 -0
- htmlgraph/api/templates/partials/agents-redesign.html +317 -0
- htmlgraph/api/templates/partials/agents.html +317 -0
- htmlgraph/api/templates/partials/event-traces.html +373 -0
- htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
- htmlgraph/api/templates/partials/features.html +578 -0
- htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
- htmlgraph/api/templates/partials/metrics.html +346 -0
- htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
- htmlgraph/api/templates/partials/orchestration.html +198 -0
- htmlgraph/api/templates/partials/spawners.html +375 -0
- htmlgraph/api/templates/partials/work-items.html +613 -0
- htmlgraph/api/websocket.py +538 -0
- htmlgraph/archive/__init__.py +24 -0
- htmlgraph/archive/bloom.py +234 -0
- htmlgraph/archive/fts.py +297 -0
- htmlgraph/archive/manager.py +583 -0
- htmlgraph/archive/search.py +244 -0
- htmlgraph/atomic_ops.py +560 -0
- htmlgraph/attribute_index.py +208 -0
- htmlgraph/bounded_paths.py +539 -0
- htmlgraph/builders/__init__.py +14 -0
- htmlgraph/builders/base.py +118 -29
- htmlgraph/builders/bug.py +150 -0
- htmlgraph/builders/chore.py +119 -0
- htmlgraph/builders/epic.py +150 -0
- htmlgraph/builders/feature.py +31 -6
- htmlgraph/builders/insight.py +195 -0
- htmlgraph/builders/metric.py +217 -0
- htmlgraph/builders/pattern.py +202 -0
- htmlgraph/builders/phase.py +162 -0
- htmlgraph/builders/spike.py +52 -19
- htmlgraph/builders/track.py +148 -72
- htmlgraph/cigs/__init__.py +81 -0
- htmlgraph/cigs/autonomy.py +385 -0
- htmlgraph/cigs/cost.py +475 -0
- htmlgraph/cigs/messages_basic.py +472 -0
- htmlgraph/cigs/messaging.py +365 -0
- htmlgraph/cigs/models.py +771 -0
- htmlgraph/cigs/pattern_storage.py +427 -0
- htmlgraph/cigs/patterns.py +503 -0
- htmlgraph/cigs/posttool_analyzer.py +234 -0
- htmlgraph/cigs/reporter.py +818 -0
- htmlgraph/cigs/tracker.py +317 -0
- htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/cli/.htmlgraph/agents.json +72 -0
- htmlgraph/cli/.htmlgraph/htmlgraph.db +0 -0
- htmlgraph/cli/__init__.py +42 -0
- htmlgraph/cli/__main__.py +6 -0
- htmlgraph/cli/analytics.py +1424 -0
- htmlgraph/cli/base.py +685 -0
- htmlgraph/cli/constants.py +206 -0
- htmlgraph/cli/core.py +954 -0
- htmlgraph/cli/main.py +147 -0
- htmlgraph/cli/models.py +475 -0
- htmlgraph/cli/templates/__init__.py +1 -0
- htmlgraph/cli/templates/cost_dashboard.py +399 -0
- htmlgraph/cli/work/__init__.py +239 -0
- htmlgraph/cli/work/browse.py +115 -0
- htmlgraph/cli/work/features.py +568 -0
- htmlgraph/cli/work/orchestration.py +676 -0
- htmlgraph/cli/work/report.py +728 -0
- htmlgraph/cli/work/sessions.py +466 -0
- htmlgraph/cli/work/snapshot.py +559 -0
- htmlgraph/cli/work/tracks.py +486 -0
- htmlgraph/cli_commands/__init__.py +1 -0
- htmlgraph/cli_commands/feature.py +195 -0
- htmlgraph/cli_framework.py +115 -0
- htmlgraph/collections/__init__.py +18 -0
- htmlgraph/collections/base.py +415 -98
- htmlgraph/collections/bug.py +53 -0
- htmlgraph/collections/chore.py +53 -0
- htmlgraph/collections/epic.py +53 -0
- htmlgraph/collections/feature.py +12 -26
- htmlgraph/collections/insight.py +100 -0
- htmlgraph/collections/metric.py +92 -0
- htmlgraph/collections/pattern.py +97 -0
- htmlgraph/collections/phase.py +53 -0
- htmlgraph/collections/session.py +194 -0
- htmlgraph/collections/spike.py +56 -16
- htmlgraph/collections/task_delegation.py +241 -0
- htmlgraph/collections/todo.py +511 -0
- htmlgraph/collections/traces.py +487 -0
- htmlgraph/config/cost_models.json +56 -0
- htmlgraph/config.py +190 -0
- htmlgraph/context_analytics.py +344 -0
- htmlgraph/converter.py +216 -28
- htmlgraph/cost_analysis/__init__.py +5 -0
- htmlgraph/cost_analysis/analyzer.py +438 -0
- htmlgraph/dashboard.html +2406 -307
- htmlgraph/dashboard.html.backup +6592 -0
- htmlgraph/dashboard.html.bak +7181 -0
- htmlgraph/dashboard.html.bak2 +7231 -0
- htmlgraph/dashboard.html.bak3 +7232 -0
- htmlgraph/db/__init__.py +38 -0
- htmlgraph/db/queries.py +790 -0
- htmlgraph/db/schema.py +1788 -0
- htmlgraph/decorators.py +317 -0
- htmlgraph/dependency_models.py +19 -2
- htmlgraph/deploy.py +142 -125
- htmlgraph/deployment_models.py +474 -0
- htmlgraph/docs/API_REFERENCE.md +841 -0
- htmlgraph/docs/HTTP_API.md +750 -0
- htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
- htmlgraph/docs/ORCHESTRATION_PATTERNS.md +717 -0
- htmlgraph/docs/README.md +532 -0
- htmlgraph/docs/__init__.py +77 -0
- htmlgraph/docs/docs_version.py +55 -0
- htmlgraph/docs/metadata.py +93 -0
- htmlgraph/docs/migrations.py +232 -0
- htmlgraph/docs/template_engine.py +143 -0
- htmlgraph/docs/templates/_sections/cli_reference.md.j2 +52 -0
- htmlgraph/docs/templates/_sections/core_concepts.md.j2 +29 -0
- htmlgraph/docs/templates/_sections/sdk_basics.md.j2 +69 -0
- htmlgraph/docs/templates/base_agents.md.j2 +78 -0
- htmlgraph/docs/templates/example_user_override.md.j2 +47 -0
- htmlgraph/docs/version_check.py +163 -0
- htmlgraph/edge_index.py +182 -27
- htmlgraph/error_handler.py +544 -0
- htmlgraph/event_log.py +100 -52
- htmlgraph/event_migration.py +13 -4
- htmlgraph/exceptions.py +49 -0
- htmlgraph/file_watcher.py +101 -28
- htmlgraph/find_api.py +75 -63
- htmlgraph/git_events.py +145 -63
- htmlgraph/graph.py +1122 -106
- htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/hooks/.htmlgraph/agents.json +72 -0
- htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
- htmlgraph/hooks/__init__.py +45 -0
- htmlgraph/hooks/bootstrap.py +169 -0
- htmlgraph/hooks/cigs_pretool_enforcer.py +354 -0
- htmlgraph/hooks/concurrent_sessions.py +208 -0
- htmlgraph/hooks/context.py +350 -0
- htmlgraph/hooks/drift_handler.py +525 -0
- htmlgraph/hooks/event_tracker.py +1314 -0
- htmlgraph/hooks/git_commands.py +175 -0
- htmlgraph/hooks/hooks-config.example.json +12 -0
- htmlgraph/hooks/installer.py +343 -0
- htmlgraph/hooks/orchestrator.py +674 -0
- htmlgraph/hooks/orchestrator_reflector.py +223 -0
- htmlgraph/hooks/post-checkout.sh +28 -0
- htmlgraph/hooks/post-commit.sh +24 -0
- htmlgraph/hooks/post-merge.sh +26 -0
- htmlgraph/hooks/post_tool_use_failure.py +273 -0
- htmlgraph/hooks/post_tool_use_handler.py +257 -0
- htmlgraph/hooks/posttooluse.py +408 -0
- htmlgraph/hooks/pre-commit.sh +94 -0
- htmlgraph/hooks/pre-push.sh +28 -0
- htmlgraph/hooks/pretooluse.py +819 -0
- htmlgraph/hooks/prompt_analyzer.py +637 -0
- htmlgraph/hooks/session_handler.py +668 -0
- htmlgraph/hooks/session_summary.py +395 -0
- htmlgraph/hooks/state_manager.py +504 -0
- htmlgraph/hooks/subagent_detection.py +202 -0
- htmlgraph/hooks/subagent_stop.py +369 -0
- htmlgraph/hooks/task_enforcer.py +255 -0
- htmlgraph/hooks/task_validator.py +177 -0
- htmlgraph/hooks/validator.py +628 -0
- htmlgraph/ids.py +41 -27
- htmlgraph/index.d.ts +286 -0
- htmlgraph/learning.py +767 -0
- htmlgraph/mcp_server.py +69 -23
- htmlgraph/models.py +1586 -87
- htmlgraph/operations/README.md +62 -0
- htmlgraph/operations/__init__.py +79 -0
- htmlgraph/operations/analytics.py +339 -0
- htmlgraph/operations/bootstrap.py +289 -0
- htmlgraph/operations/events.py +244 -0
- htmlgraph/operations/fastapi_server.py +231 -0
- htmlgraph/operations/hooks.py +350 -0
- htmlgraph/operations/initialization.py +597 -0
- htmlgraph/operations/initialization.py.backup +228 -0
- htmlgraph/operations/server.py +303 -0
- htmlgraph/orchestration/__init__.py +58 -0
- htmlgraph/orchestration/claude_launcher.py +179 -0
- htmlgraph/orchestration/command_builder.py +72 -0
- htmlgraph/orchestration/headless_spawner.py +281 -0
- htmlgraph/orchestration/live_events.py +377 -0
- htmlgraph/orchestration/model_selection.py +327 -0
- htmlgraph/orchestration/plugin_manager.py +140 -0
- htmlgraph/orchestration/prompts.py +137 -0
- htmlgraph/orchestration/spawner_event_tracker.py +383 -0
- htmlgraph/orchestration/spawners/__init__.py +16 -0
- htmlgraph/orchestration/spawners/base.py +194 -0
- htmlgraph/orchestration/spawners/claude.py +173 -0
- htmlgraph/orchestration/spawners/codex.py +435 -0
- htmlgraph/orchestration/spawners/copilot.py +294 -0
- htmlgraph/orchestration/spawners/gemini.py +471 -0
- htmlgraph/orchestration/subprocess_runner.py +36 -0
- htmlgraph/orchestration/task_coordination.py +343 -0
- htmlgraph/orchestration.md +563 -0
- htmlgraph/orchestrator-system-prompt-optimized.txt +863 -0
- htmlgraph/orchestrator.py +669 -0
- htmlgraph/orchestrator_config.py +357 -0
- htmlgraph/orchestrator_mode.py +328 -0
- htmlgraph/orchestrator_validator.py +133 -0
- htmlgraph/parallel.py +646 -0
- htmlgraph/parser.py +160 -35
- htmlgraph/path_query.py +608 -0
- htmlgraph/pattern_matcher.py +636 -0
- htmlgraph/planning.py +147 -52
- htmlgraph/pydantic_models.py +476 -0
- htmlgraph/quality_gates.py +350 -0
- htmlgraph/query_builder.py +109 -72
- htmlgraph/query_composer.py +509 -0
- htmlgraph/reflection.py +443 -0
- htmlgraph/refs.py +344 -0
- htmlgraph/repo_hash.py +512 -0
- htmlgraph/repositories/__init__.py +292 -0
- htmlgraph/repositories/analytics_repository.py +455 -0
- htmlgraph/repositories/analytics_repository_standard.py +628 -0
- htmlgraph/repositories/feature_repository.py +581 -0
- htmlgraph/repositories/feature_repository_htmlfile.py +668 -0
- htmlgraph/repositories/feature_repository_memory.py +607 -0
- htmlgraph/repositories/feature_repository_sqlite.py +858 -0
- htmlgraph/repositories/filter_service.py +620 -0
- htmlgraph/repositories/filter_service_standard.py +445 -0
- htmlgraph/repositories/shared_cache.py +621 -0
- htmlgraph/repositories/shared_cache_memory.py +395 -0
- htmlgraph/repositories/track_repository.py +552 -0
- htmlgraph/repositories/track_repository_htmlfile.py +619 -0
- htmlgraph/repositories/track_repository_memory.py +508 -0
- htmlgraph/repositories/track_repository_sqlite.py +711 -0
- htmlgraph/routing.py +8 -19
- htmlgraph/scripts/deploy.py +1 -2
- htmlgraph/sdk/__init__.py +398 -0
- htmlgraph/sdk/__init__.pyi +14 -0
- htmlgraph/sdk/analytics/__init__.py +19 -0
- htmlgraph/sdk/analytics/engine.py +155 -0
- htmlgraph/sdk/analytics/helpers.py +178 -0
- htmlgraph/sdk/analytics/registry.py +109 -0
- htmlgraph/sdk/base.py +484 -0
- htmlgraph/sdk/constants.py +216 -0
- htmlgraph/sdk/core.pyi +308 -0
- htmlgraph/sdk/discovery.py +120 -0
- htmlgraph/sdk/help/__init__.py +12 -0
- htmlgraph/sdk/help/mixin.py +699 -0
- htmlgraph/sdk/mixins/__init__.py +15 -0
- htmlgraph/sdk/mixins/attribution.py +113 -0
- htmlgraph/sdk/mixins/mixin.py +410 -0
- htmlgraph/sdk/operations/__init__.py +12 -0
- htmlgraph/sdk/operations/mixin.py +427 -0
- htmlgraph/sdk/orchestration/__init__.py +17 -0
- htmlgraph/sdk/orchestration/coordinator.py +203 -0
- htmlgraph/sdk/orchestration/spawner.py +204 -0
- htmlgraph/sdk/planning/__init__.py +19 -0
- htmlgraph/sdk/planning/bottlenecks.py +93 -0
- htmlgraph/sdk/planning/mixin.py +211 -0
- htmlgraph/sdk/planning/parallel.py +186 -0
- htmlgraph/sdk/planning/queue.py +210 -0
- htmlgraph/sdk/planning/recommendations.py +87 -0
- htmlgraph/sdk/planning/smart_planning.py +319 -0
- htmlgraph/sdk/session/__init__.py +19 -0
- htmlgraph/sdk/session/continuity.py +57 -0
- htmlgraph/sdk/session/handoff.py +110 -0
- htmlgraph/sdk/session/info.py +309 -0
- htmlgraph/sdk/session/manager.py +103 -0
- htmlgraph/sdk/strategic/__init__.py +26 -0
- htmlgraph/sdk/strategic/mixin.py +563 -0
- htmlgraph/server.py +685 -180
- htmlgraph/services/__init__.py +10 -0
- htmlgraph/services/claiming.py +199 -0
- htmlgraph/session_hooks.py +300 -0
- htmlgraph/session_manager.py +1392 -175
- htmlgraph/session_registry.py +587 -0
- htmlgraph/session_state.py +436 -0
- htmlgraph/session_warning.py +201 -0
- htmlgraph/sessions/__init__.py +23 -0
- htmlgraph/sessions/handoff.py +756 -0
- htmlgraph/setup.py +34 -17
- htmlgraph/spike_index.py +143 -0
- htmlgraph/sync_docs.py +12 -15
- htmlgraph/system_prompts.py +450 -0
- htmlgraph/templates/AGENTS.md.template +366 -0
- htmlgraph/templates/CLAUDE.md.template +97 -0
- htmlgraph/templates/GEMINI.md.template +87 -0
- htmlgraph/templates/orchestration-view.html +350 -0
- htmlgraph/track_builder.py +146 -15
- htmlgraph/track_manager.py +69 -21
- htmlgraph/transcript.py +890 -0
- htmlgraph/transcript_analytics.py +699 -0
- htmlgraph/types.py +323 -0
- htmlgraph/validation.py +115 -0
- htmlgraph/watch.py +8 -5
- htmlgraph/work_type_utils.py +3 -2
- {htmlgraph-0.9.3.data ā htmlgraph-0.27.5.data}/data/htmlgraph/dashboard.html +2406 -307
- htmlgraph-0.27.5.data/data/htmlgraph/templates/AGENTS.md.template +366 -0
- htmlgraph-0.27.5.data/data/htmlgraph/templates/CLAUDE.md.template +97 -0
- htmlgraph-0.27.5.data/data/htmlgraph/templates/GEMINI.md.template +87 -0
- {htmlgraph-0.9.3.dist-info ā htmlgraph-0.27.5.dist-info}/METADATA +97 -64
- htmlgraph-0.27.5.dist-info/RECORD +337 -0
- {htmlgraph-0.9.3.dist-info ā htmlgraph-0.27.5.dist-info}/entry_points.txt +1 -1
- htmlgraph/cli.py +0 -2688
- htmlgraph/sdk.py +0 -709
- htmlgraph-0.9.3.dist-info/RECORD +0 -61
- {htmlgraph-0.9.3.data ā htmlgraph-0.27.5.data}/data/htmlgraph/styles.css +0 -0
- {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()
|