htmlgraph 0.24.1__py3-none-any.whl ā 0.25.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- htmlgraph/__init__.py +20 -1
- htmlgraph/agent_detection.py +26 -10
- htmlgraph/analytics/cross_session.py +4 -3
- htmlgraph/analytics/work_type.py +52 -16
- htmlgraph/analytics_index.py +51 -19
- htmlgraph/api/__init__.py +3 -0
- htmlgraph/api/main.py +2115 -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 +812 -0
- htmlgraph/api/templates/dashboard.html +783 -0
- htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
- htmlgraph/api/templates/partials/activity-feed.html +570 -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 +509 -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 +163 -0
- htmlgraph/api/templates/partials/spawners.html +375 -0
- htmlgraph/atomic_ops.py +560 -0
- htmlgraph/builders/base.py +55 -1
- htmlgraph/builders/bug.py +17 -2
- htmlgraph/builders/chore.py +17 -2
- htmlgraph/builders/epic.py +17 -2
- htmlgraph/builders/feature.py +25 -2
- htmlgraph/builders/phase.py +17 -2
- htmlgraph/builders/spike.py +27 -2
- htmlgraph/builders/track.py +14 -0
- htmlgraph/cigs/__init__.py +4 -0
- htmlgraph/cigs/reporter.py +818 -0
- htmlgraph/cli.py +1427 -401
- htmlgraph/cli_commands/__init__.py +1 -0
- htmlgraph/cli_commands/feature.py +195 -0
- htmlgraph/cli_framework.py +115 -0
- htmlgraph/collections/__init__.py +2 -0
- htmlgraph/collections/base.py +21 -0
- htmlgraph/collections/session.py +189 -0
- htmlgraph/collections/spike.py +7 -1
- htmlgraph/collections/task_delegation.py +236 -0
- htmlgraph/collections/traces.py +482 -0
- htmlgraph/config.py +113 -0
- htmlgraph/converter.py +41 -0
- htmlgraph/cost_analysis/__init__.py +5 -0
- htmlgraph/cost_analysis/analyzer.py +438 -0
- htmlgraph/dashboard.html +3315 -492
- htmlgraph-0.24.1.data/data/htmlgraph/dashboard.html ā htmlgraph/dashboard.html.backup +2246 -248
- 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 +1334 -0
- htmlgraph/deploy.py +26 -27
- 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 +710 -0
- htmlgraph/docs/README.md +533 -0
- htmlgraph/docs/version_check.py +3 -1
- htmlgraph/error_handler.py +544 -0
- htmlgraph/event_log.py +2 -0
- htmlgraph/hooks/__init__.py +8 -0
- htmlgraph/hooks/bootstrap.py +169 -0
- htmlgraph/hooks/context.py +271 -0
- htmlgraph/hooks/drift_handler.py +521 -0
- htmlgraph/hooks/event_tracker.py +405 -15
- htmlgraph/hooks/post_tool_use_handler.py +257 -0
- htmlgraph/hooks/pretooluse.py +476 -6
- htmlgraph/hooks/prompt_analyzer.py +648 -0
- htmlgraph/hooks/session_handler.py +583 -0
- htmlgraph/hooks/state_manager.py +501 -0
- htmlgraph/hooks/subagent_stop.py +309 -0
- htmlgraph/hooks/task_enforcer.py +39 -0
- htmlgraph/models.py +111 -15
- htmlgraph/operations/fastapi_server.py +230 -0
- htmlgraph/orchestration/headless_spawner.py +22 -14
- htmlgraph/pydantic_models.py +476 -0
- htmlgraph/quality_gates.py +350 -0
- htmlgraph/repo_hash.py +511 -0
- htmlgraph/sdk.py +348 -10
- htmlgraph/server.py +194 -0
- htmlgraph/session_hooks.py +300 -0
- htmlgraph/session_manager.py +131 -1
- htmlgraph/session_registry.py +587 -0
- htmlgraph/session_state.py +436 -0
- htmlgraph/system_prompts.py +449 -0
- htmlgraph/templates/orchestration-view.html +350 -0
- htmlgraph/track_builder.py +19 -0
- htmlgraph/validation.py +115 -0
- htmlgraph-0.25.0.data/data/htmlgraph/dashboard.html +7417 -0
- {htmlgraph-0.24.1.dist-info ā htmlgraph-0.25.0.dist-info}/METADATA +91 -64
- {htmlgraph-0.24.1.dist-info ā htmlgraph-0.25.0.dist-info}/RECORD +103 -42
- {htmlgraph-0.24.1.data ā htmlgraph-0.25.0.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.24.1.data ā htmlgraph-0.25.0.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
- {htmlgraph-0.24.1.data ā htmlgraph-0.25.0.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
- {htmlgraph-0.24.1.data ā htmlgraph-0.25.0.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
- {htmlgraph-0.24.1.dist-info ā htmlgraph-0.25.0.dist-info}/WHEEL +0 -0
- {htmlgraph-0.24.1.dist-info ā htmlgraph-0.25.0.dist-info}/entry_points.txt +0 -0
htmlgraph/cli.py
CHANGED
|
@@ -41,6 +41,13 @@ Analytics:
|
|
|
41
41
|
htmlgraph analytics # Project-wide analytics
|
|
42
42
|
htmlgraph analytics --session-id SESSION_ID # Single session analysis
|
|
43
43
|
htmlgraph analytics --recent N # Analyze recent N sessions
|
|
44
|
+
|
|
45
|
+
Cost Attribution:
|
|
46
|
+
htmlgraph cigs cost-dashboard # Display cost summary in console
|
|
47
|
+
htmlgraph cigs cost-dashboard --save # Save to .htmlgraph/cost-dashboard.html
|
|
48
|
+
htmlgraph cigs cost-dashboard --open # Open in browser after generation
|
|
49
|
+
htmlgraph cigs cost-dashboard --json # Output JSON instead of HTML
|
|
50
|
+
htmlgraph cigs cost-dashboard --output PATH # Custom output path
|
|
44
51
|
"""
|
|
45
52
|
|
|
46
53
|
import argparse
|
|
@@ -51,6 +58,20 @@ from datetime import datetime
|
|
|
51
58
|
from pathlib import Path
|
|
52
59
|
from typing import Any
|
|
53
60
|
|
|
61
|
+
from rich import box
|
|
62
|
+
from rich.console import Console
|
|
63
|
+
from rich.panel import Panel
|
|
64
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
65
|
+
from rich.prompt import Confirm, Prompt
|
|
66
|
+
from rich.table import Table
|
|
67
|
+
from rich.traceback import install as install_traceback
|
|
68
|
+
|
|
69
|
+
# Install Rich traceback globally for better error display
|
|
70
|
+
install_traceback(show_locals=True)
|
|
71
|
+
|
|
72
|
+
# Global Rich Console for beautiful CLI output
|
|
73
|
+
console = Console()
|
|
74
|
+
|
|
54
75
|
|
|
55
76
|
def create_json_response(
|
|
56
77
|
command: str,
|
|
@@ -84,14 +105,15 @@ def cmd_install_gemini_extension(args: argparse.Namespace) -> None:
|
|
|
84
105
|
extension_dir = package_dir / "extensions" / "gemini"
|
|
85
106
|
|
|
86
107
|
if not extension_dir.exists():
|
|
87
|
-
print(
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
108
|
+
console.print(
|
|
109
|
+
f"[red]Error: Gemini extension not found at {extension_dir}[/red]"
|
|
110
|
+
)
|
|
111
|
+
console.print(
|
|
112
|
+
"[red]The extension may not be bundled with this version of htmlgraph.[/red]"
|
|
91
113
|
)
|
|
92
114
|
sys.exit(1)
|
|
93
115
|
|
|
94
|
-
print(f"Installing Gemini extension from: {extension_dir}")
|
|
116
|
+
console.print(f"[cyan]Installing Gemini extension from:[/cyan] {extension_dir}")
|
|
95
117
|
|
|
96
118
|
# Run gemini extensions install with the bundled path
|
|
97
119
|
try:
|
|
@@ -101,35 +123,125 @@ def cmd_install_gemini_extension(args: argparse.Namespace) -> None:
|
|
|
101
123
|
text=True,
|
|
102
124
|
check=True,
|
|
103
125
|
)
|
|
104
|
-
print(result.stdout)
|
|
105
|
-
print("\nā
Gemini extension installed successfully!")
|
|
106
|
-
print("\nTo verify installation:")
|
|
107
|
-
print(" gemini extensions list")
|
|
126
|
+
console.print(result.stdout)
|
|
127
|
+
console.print("\n[green]ā
Gemini extension installed successfully![/green]")
|
|
128
|
+
console.print("\nTo verify installation:")
|
|
129
|
+
console.print(" gemini extensions list", style="dim")
|
|
108
130
|
except subprocess.CalledProcessError as e:
|
|
109
|
-
print(f"Error installing extension: {e.stderr}",
|
|
131
|
+
console.print(f"[red]Error installing extension: {e.stderr}[/red]", style="red")
|
|
110
132
|
sys.exit(1)
|
|
111
133
|
except FileNotFoundError:
|
|
112
|
-
print("Error: 'gemini' command not found."
|
|
113
|
-
print("Please install Gemini CLI first:"
|
|
114
|
-
print(" npm install -g @google/gemini-cli",
|
|
134
|
+
console.print("[red]Error: 'gemini' command not found.[/red]")
|
|
135
|
+
console.print("Please install Gemini CLI first:")
|
|
136
|
+
console.print(" npm install -g @google/gemini-cli", style="dim")
|
|
115
137
|
sys.exit(1)
|
|
116
138
|
|
|
117
139
|
|
|
118
140
|
def cmd_serve(args: argparse.Namespace) -> None:
|
|
119
|
-
"""Start the HtmlGraph server."""
|
|
120
|
-
|
|
141
|
+
"""Start the HtmlGraph server (FastAPI-based)."""
|
|
142
|
+
import asyncio
|
|
121
143
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
static_dir=args.static_dir,
|
|
126
|
-
host=args.host,
|
|
127
|
-
watch=not args.no_watch,
|
|
128
|
-
auto_port=args.auto_port,
|
|
144
|
+
from htmlgraph.operations.fastapi_server import (
|
|
145
|
+
run_fastapi_server,
|
|
146
|
+
start_fastapi_server,
|
|
129
147
|
)
|
|
130
148
|
|
|
131
|
-
|
|
132
|
-
|
|
149
|
+
try:
|
|
150
|
+
# Default to database in graph dir if not specified
|
|
151
|
+
db_path = getattr(args, "db", None)
|
|
152
|
+
if not db_path:
|
|
153
|
+
db_path = str(Path(args.graph_dir) / "index.sqlite")
|
|
154
|
+
|
|
155
|
+
result = start_fastapi_server(
|
|
156
|
+
port=args.port,
|
|
157
|
+
host=args.host,
|
|
158
|
+
db_path=db_path,
|
|
159
|
+
auto_port=args.auto_port,
|
|
160
|
+
reload=getattr(args, "reload", False),
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# Print server info
|
|
164
|
+
console.print("\n[bold cyan]HtmlGraph Server (FastAPI)[/bold cyan]")
|
|
165
|
+
console.print(f"URL: [bold blue]{result.handle.url}[/bold blue]")
|
|
166
|
+
console.print(f"Graph directory: {args.graph_dir}")
|
|
167
|
+
console.print(f"Database: {result.config_used['db_path']}")
|
|
168
|
+
|
|
169
|
+
if result.warnings:
|
|
170
|
+
for warning in result.warnings:
|
|
171
|
+
console.print(f"[yellow]Warning: {warning}[/yellow]")
|
|
172
|
+
|
|
173
|
+
from htmlgraph.server import HtmlGraphAPIHandler
|
|
174
|
+
|
|
175
|
+
console.print(f"Collections: {', '.join(HtmlGraphAPIHandler.COLLECTIONS)}")
|
|
176
|
+
console.print("\n[cyan]Features:[/cyan]")
|
|
177
|
+
console.print(" ⢠Real-time agent activity feed (HTMX)")
|
|
178
|
+
console.print(" ⢠Orchestration chains visualization")
|
|
179
|
+
console.print(" ⢠Feature tracker with Kanban view")
|
|
180
|
+
console.print(" ⢠Session metrics & performance analytics")
|
|
181
|
+
console.print("\n[cyan]Press Ctrl+C to stop.[/cyan]\n")
|
|
182
|
+
|
|
183
|
+
# Run server
|
|
184
|
+
asyncio.run(run_fastapi_server(result.handle))
|
|
185
|
+
|
|
186
|
+
except KeyboardInterrupt:
|
|
187
|
+
console.print("\n[yellow]Shutting down...[/yellow]")
|
|
188
|
+
except Exception as e:
|
|
189
|
+
console.print(f"\n[red]Error:[/red] {e}")
|
|
190
|
+
import traceback
|
|
191
|
+
|
|
192
|
+
if getattr(args, "verbose", False):
|
|
193
|
+
traceback.print_exc()
|
|
194
|
+
sys.exit(1)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def cmd_serve_api(args: argparse.Namespace) -> None:
|
|
198
|
+
"""Start the FastAPI-based observability dashboard (Phase 3)."""
|
|
199
|
+
import asyncio
|
|
200
|
+
|
|
201
|
+
from htmlgraph.operations.fastapi_server import (
|
|
202
|
+
run_fastapi_server,
|
|
203
|
+
start_fastapi_server,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
try:
|
|
207
|
+
result = start_fastapi_server(
|
|
208
|
+
port=args.port,
|
|
209
|
+
host=args.host,
|
|
210
|
+
db_path=args.db,
|
|
211
|
+
auto_port=args.auto_port,
|
|
212
|
+
reload=args.reload,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# Print server info
|
|
216
|
+
console.print("\n[bold cyan]HtmlGraph FastAPI Dashboard[/bold cyan]")
|
|
217
|
+
console.print("[bold green]ā[/bold green] Started observability dashboard")
|
|
218
|
+
console.print(f"URL: [bold blue]{result.handle.url}[/bold blue]")
|
|
219
|
+
console.print(f"Database: {result.config_used['db_path']}")
|
|
220
|
+
|
|
221
|
+
if result.warnings:
|
|
222
|
+
for warning in result.warnings:
|
|
223
|
+
console.print(f"[yellow]Warning: {warning}[/yellow]")
|
|
224
|
+
|
|
225
|
+
console.print("\n[cyan]Features:[/cyan]")
|
|
226
|
+
console.print(" ⢠Real-time agent activity feed")
|
|
227
|
+
console.print(" ⢠Orchestration chains visualization")
|
|
228
|
+
console.print(" ⢠Feature tracker with Kanban view")
|
|
229
|
+
console.print(" ⢠Session metrics & performance analytics")
|
|
230
|
+
console.print(" ⢠WebSocket live event streaming")
|
|
231
|
+
console.print("\n[cyan]Press Ctrl+C to stop.[/cyan]\n")
|
|
232
|
+
|
|
233
|
+
# Run server
|
|
234
|
+
asyncio.run(run_fastapi_server(result.handle))
|
|
235
|
+
|
|
236
|
+
except KeyboardInterrupt:
|
|
237
|
+
console.print("\n[yellow]Shutting down...[/yellow]")
|
|
238
|
+
except Exception as e:
|
|
239
|
+
console.print(f"\n[red]Error:[/red] {e}")
|
|
240
|
+
import traceback
|
|
241
|
+
|
|
242
|
+
if getattr(args, "verbose", False):
|
|
243
|
+
traceback.print_exc()
|
|
244
|
+
sys.exit(1)
|
|
133
245
|
|
|
134
246
|
|
|
135
247
|
def cmd_init(args: argparse.Namespace) -> None:
|
|
@@ -142,28 +254,26 @@ def cmd_init(args: argparse.Namespace) -> None:
|
|
|
142
254
|
|
|
143
255
|
# Interactive setup wizard
|
|
144
256
|
if args.interactive:
|
|
145
|
-
print("=== HtmlGraph Interactive Setup
|
|
257
|
+
console.print("\n[bold cyan]=== HtmlGraph Interactive Setup ===[/bold cyan]\n")
|
|
146
258
|
|
|
147
259
|
# Get project name
|
|
148
260
|
default_name = Path(args.dir).resolve().name
|
|
149
|
-
project_name =
|
|
261
|
+
project_name = Prompt.ask("Project name", default=default_name)
|
|
150
262
|
|
|
151
263
|
# Get agent name
|
|
152
|
-
agent_name =
|
|
264
|
+
agent_name = Prompt.ask("Your agent name", default="claude")
|
|
153
265
|
|
|
154
266
|
# Ask about git hooks
|
|
155
|
-
|
|
156
|
-
|
|
267
|
+
args.install_hooks = Confirm.ask(
|
|
268
|
+
"Install git hooks for automatic tracking?", default=True
|
|
157
269
|
)
|
|
158
|
-
args.install_hooks = install_hooks_response != "n"
|
|
159
270
|
|
|
160
271
|
# Ask about documentation generation
|
|
161
|
-
|
|
162
|
-
|
|
272
|
+
generate_docs = Confirm.ask(
|
|
273
|
+
"Generate AGENTS.md, CLAUDE.md, GEMINI.md?", default=True
|
|
163
274
|
)
|
|
164
|
-
generate_docs = gen_docs_response != "n"
|
|
165
275
|
|
|
166
|
-
print()
|
|
276
|
+
console.print()
|
|
167
277
|
else:
|
|
168
278
|
# Non-interactive defaults
|
|
169
279
|
project_name = Path(args.dir).resolve().name
|
|
@@ -548,7 +658,7 @@ exit 0
|
|
|
548
658
|
content = render_template(agents_template, replacements)
|
|
549
659
|
if content:
|
|
550
660
|
agents_dest.write_text(content, encoding="utf-8")
|
|
551
|
-
print(f"ā Generated: {agents_dest}")
|
|
661
|
+
console.print(f"[green]ā Generated:[/green] {agents_dest}")
|
|
552
662
|
|
|
553
663
|
# Generate CLAUDE.md
|
|
554
664
|
claude_template = templates_dir / "CLAUDE.md.template"
|
|
@@ -557,7 +667,7 @@ exit 0
|
|
|
557
667
|
content = render_template(claude_template, replacements)
|
|
558
668
|
if content:
|
|
559
669
|
claude_dest.write_text(content, encoding="utf-8")
|
|
560
|
-
print(f"ā Generated: {claude_dest}")
|
|
670
|
+
console.print(f"[green]ā Generated:[/green] {claude_dest}")
|
|
561
671
|
|
|
562
672
|
# Generate GEMINI.md
|
|
563
673
|
gemini_template = templates_dir / "GEMINI.md.template"
|
|
@@ -566,15 +676,17 @@ exit 0
|
|
|
566
676
|
content = render_template(gemini_template, replacements)
|
|
567
677
|
if content:
|
|
568
678
|
gemini_dest.write_text(content, encoding="utf-8")
|
|
569
|
-
print(f"ā Generated: {gemini_dest}")
|
|
679
|
+
console.print(f"[green]ā Generated:[/green] {gemini_dest}")
|
|
570
680
|
|
|
571
681
|
def install_hooks_step() -> None:
|
|
572
682
|
if not args.install_hooks:
|
|
573
683
|
return
|
|
574
684
|
git_dir = Path(args.dir) / ".git"
|
|
575
685
|
if not git_dir.exists():
|
|
576
|
-
print(
|
|
577
|
-
|
|
686
|
+
console.print(
|
|
687
|
+
"\n[yellow]ā ļø Warning: No .git directory found. Git hooks not installed.[/yellow]"
|
|
688
|
+
)
|
|
689
|
+
console.print("[dim]Initialize git first: git init[/dim]")
|
|
578
690
|
return
|
|
579
691
|
|
|
580
692
|
def install_hook(
|
|
@@ -607,11 +719,11 @@ exit 0
|
|
|
607
719
|
git_hook_path = git_dir / "hooks" / hook_name
|
|
608
720
|
|
|
609
721
|
if git_hook_path.exists():
|
|
610
|
-
print(f"\nā ļø Existing {hook_name} hook found")
|
|
722
|
+
console.print(f"\n[yellow]ā ļø Existing {hook_name} hook found[/yellow]")
|
|
611
723
|
backup_path = git_hook_path.with_suffix(".existing")
|
|
612
724
|
if not backup_path.exists():
|
|
613
725
|
shutil.copy(git_hook_path, backup_path)
|
|
614
|
-
print(f"
|
|
726
|
+
console.print(f"[dim]Backed up to: {backup_path}[/dim]")
|
|
615
727
|
|
|
616
728
|
chain_content = f'''#!/bin/bash
|
|
617
729
|
# Chained hook - runs existing hook then HtmlGraph hook
|
|
@@ -626,18 +738,18 @@ fi
|
|
|
626
738
|
'''
|
|
627
739
|
git_hook_path.write_text(chain_content)
|
|
628
740
|
git_hook_path.chmod(0o755)
|
|
629
|
-
print(f"
|
|
741
|
+
console.print(f"[dim]Installed chained hook at: {git_hook_path}[/dim]")
|
|
630
742
|
return
|
|
631
743
|
|
|
632
744
|
try:
|
|
633
745
|
git_hook_path.symlink_to(hook_dest.resolve())
|
|
634
|
-
print("\nā Git hooks installed")
|
|
635
|
-
print(f"
|
|
746
|
+
console.print("\n[green]ā Git hooks installed[/green]")
|
|
747
|
+
console.print(f"[dim]{hook_name}: {git_hook_path} -> {hook_dest}[/dim]")
|
|
636
748
|
except OSError:
|
|
637
749
|
shutil.copy(hook_dest, git_hook_path)
|
|
638
750
|
git_hook_path.chmod(0o755)
|
|
639
|
-
print("\nā Git hooks installed")
|
|
640
|
-
print(f"
|
|
751
|
+
console.print("\n[green]ā Git hooks installed[/green]")
|
|
752
|
+
console.print(f"[dim]{hook_name}: {git_hook_path}[/dim]")
|
|
641
753
|
|
|
642
754
|
install_hook("pre-commit", hook_files["pre-commit"], pre_commit)
|
|
643
755
|
install_hook("post-commit", hook_files["post-commit"], post_commit)
|
|
@@ -645,7 +757,9 @@ fi
|
|
|
645
757
|
install_hook("post-merge", hook_files["post-merge"], post_merge)
|
|
646
758
|
install_hook("pre-push", hook_files["pre-push"], pre_push)
|
|
647
759
|
|
|
648
|
-
print(
|
|
760
|
+
console.print(
|
|
761
|
+
"\n[cyan]Git events will now be logged to HtmlGraph automatically.[/cyan]"
|
|
762
|
+
)
|
|
649
763
|
|
|
650
764
|
steps: list[tuple[str, Any]] = [
|
|
651
765
|
("Create .htmlgraph directories", create_graph_dirs),
|
|
@@ -678,14 +792,19 @@ fi
|
|
|
678
792
|
|
|
679
793
|
run_steps(steps)
|
|
680
794
|
|
|
681
|
-
print(
|
|
682
|
-
print(f"
|
|
683
|
-
print(
|
|
795
|
+
console.print()
|
|
796
|
+
console.print(f"[green]ā Initialized HtmlGraph in[/green] {graph_dir}")
|
|
797
|
+
console.print(
|
|
798
|
+
f"[cyan]Collections:[/cyan] {', '.join(HtmlGraphAPIHandler.COLLECTIONS)}"
|
|
799
|
+
)
|
|
800
|
+
console.print()
|
|
801
|
+
console.print("[bold cyan]Start server with:[/bold cyan]")
|
|
802
|
+
console.print(" htmlgraph serve", style="dim")
|
|
684
803
|
if not args.no_index:
|
|
685
|
-
print(
|
|
686
|
-
f"Analytics cache: {graph_dir / 'index.sqlite'} (rebuildable; typically gitignored)"
|
|
804
|
+
console.print(
|
|
805
|
+
f"[dim]Analytics cache: {graph_dir / 'index.sqlite'} (rebuildable; typically gitignored)[/dim]"
|
|
687
806
|
)
|
|
688
|
-
print(f"Events: {events_dir}/ (append-only JSONL)")
|
|
807
|
+
console.print(f"[dim]Events: {events_dir}/ (append-only JSONL)[/dim]")
|
|
689
808
|
|
|
690
809
|
|
|
691
810
|
def cmd_install_hooks(args: argparse.Namespace) -> None:
|
|
@@ -704,22 +823,24 @@ def cmd_install_hooks(args: argparse.Namespace) -> None:
|
|
|
704
823
|
# Handle configuration changes
|
|
705
824
|
if args.enable:
|
|
706
825
|
if args.enable not in AVAILABLE_HOOKS:
|
|
707
|
-
print(f"Error: Unknown hook '{args.enable}'")
|
|
708
|
-
print(f"Available hooks: {', '.join(AVAILABLE_HOOKS)}")
|
|
826
|
+
console.print(f"[red]Error: Unknown hook '{args.enable}'[/red]")
|
|
827
|
+
console.print(f"[cyan]Available hooks:[/cyan] {', '.join(AVAILABLE_HOOKS)}")
|
|
709
828
|
return
|
|
710
829
|
config.enable_hook(args.enable)
|
|
711
830
|
config.save()
|
|
712
|
-
print(f"ā Enabled hook '{args.enable}' in configuration")
|
|
831
|
+
console.print(f"[green]ā Enabled hook '{args.enable}' in configuration[/green]")
|
|
713
832
|
return
|
|
714
833
|
|
|
715
834
|
if args.disable:
|
|
716
835
|
if args.disable not in AVAILABLE_HOOKS:
|
|
717
|
-
print(f"Error: Unknown hook '{args.disable}'")
|
|
718
|
-
print(f"Available hooks: {', '.join(AVAILABLE_HOOKS)}")
|
|
836
|
+
console.print(f"[red]Error: Unknown hook '{args.disable}'[/red]")
|
|
837
|
+
console.print(f"[cyan]Available hooks:[/cyan] {', '.join(AVAILABLE_HOOKS)}")
|
|
719
838
|
return
|
|
720
839
|
config.disable_hook(args.disable)
|
|
721
840
|
config.save()
|
|
722
|
-
print(
|
|
841
|
+
console.print(
|
|
842
|
+
f"[green]ā Disabled hook '{args.disable}' in configuration[/green]"
|
|
843
|
+
)
|
|
723
844
|
return
|
|
724
845
|
|
|
725
846
|
# Override symlink preference if --use-copy is set
|
|
@@ -732,84 +853,116 @@ def cmd_install_hooks(args: argparse.Namespace) -> None:
|
|
|
732
853
|
# Validate environment
|
|
733
854
|
is_valid, error_msg = installer.validate_environment()
|
|
734
855
|
if not is_valid:
|
|
735
|
-
print(f"ā {error_msg}")
|
|
856
|
+
console.print(f"[red]ā {error_msg}[/red]")
|
|
736
857
|
return
|
|
737
858
|
|
|
738
859
|
# List hooks status
|
|
739
860
|
if args.list:
|
|
740
|
-
print(
|
|
741
|
-
|
|
861
|
+
console.print()
|
|
862
|
+
table = Table(title="Git Hooks Installation Status", border_style="cyan")
|
|
863
|
+
table.add_column("Hook", style="cyan", no_wrap=True)
|
|
864
|
+
table.add_column("Enabled", justify="center", style="green")
|
|
865
|
+
table.add_column("Installed", justify="center", style="green")
|
|
866
|
+
table.add_column("Type", style="dim")
|
|
867
|
+
table.add_column("Status", style="dim")
|
|
742
868
|
|
|
743
869
|
status = installer.list_hooks()
|
|
744
870
|
for hook_name, info in status.items():
|
|
745
871
|
status_icon = "ā" if info["installed"] else "ā"
|
|
746
872
|
enabled_icon = "š¢" if info["enabled"] else "š“"
|
|
873
|
+
type_str = "Symlink" if info["is_symlink"] else "Copied"
|
|
874
|
+
our_hook = (
|
|
875
|
+
"ā"
|
|
876
|
+
if info.get("our_hook", False)
|
|
877
|
+
else "ā"
|
|
878
|
+
if info["is_symlink"]
|
|
879
|
+
else ""
|
|
880
|
+
)
|
|
881
|
+
|
|
882
|
+
status_str = (
|
|
883
|
+
f"{type_str} {our_hook}".strip() if type_str == "Symlink" else type_str
|
|
884
|
+
)
|
|
885
|
+
|
|
886
|
+
table.add_row(hook_name, enabled_icon, status_icon, type_str, status_str)
|
|
747
887
|
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
elif info["installed"]:
|
|
758
|
-
print(" Type: Copied file")
|
|
759
|
-
|
|
760
|
-
print("\n" + "=" * 60)
|
|
761
|
-
print(f"\nConfiguration: {config_path}")
|
|
762
|
-
print("Use 'htmlgraph install-hooks --enable <hook>' to enable")
|
|
763
|
-
print("Use 'htmlgraph install-hooks --disable <hook>' to disable")
|
|
888
|
+
console.print(table)
|
|
889
|
+
console.print()
|
|
890
|
+
console.print(f"[dim]Configuration: {config_path}[/dim]")
|
|
891
|
+
console.print(
|
|
892
|
+
"[dim]Use 'htmlgraph install-hooks --enable <hook>' to enable[/dim]"
|
|
893
|
+
)
|
|
894
|
+
console.print(
|
|
895
|
+
"[dim]Use 'htmlgraph install-hooks --disable <hook>' to disable[/dim]"
|
|
896
|
+
)
|
|
764
897
|
return
|
|
765
898
|
|
|
766
899
|
# Uninstall a hook
|
|
767
900
|
if args.uninstall:
|
|
768
901
|
if args.uninstall not in AVAILABLE_HOOKS:
|
|
769
|
-
print(f"Error: Unknown hook '{args.uninstall}'")
|
|
770
|
-
print(f"Available hooks: {', '.join(AVAILABLE_HOOKS)}")
|
|
902
|
+
console.print(f"[red]Error: Unknown hook '{args.uninstall}'[/red]")
|
|
903
|
+
console.print(f"[cyan]Available hooks:[/cyan] {', '.join(AVAILABLE_HOOKS)}")
|
|
771
904
|
return
|
|
772
905
|
|
|
773
906
|
success, message = installer.uninstall_hook(args.uninstall)
|
|
774
907
|
if success:
|
|
775
|
-
print(f"ā {message}")
|
|
908
|
+
console.print(f"[green]ā {message}[/green]")
|
|
776
909
|
else:
|
|
777
|
-
print(f"ā {message}")
|
|
910
|
+
console.print(f"[red]ā {message}[/red]")
|
|
778
911
|
return
|
|
779
912
|
|
|
780
913
|
# Install hooks
|
|
781
|
-
print("\nš§ Installing Git hooks for HtmlGraph\n")
|
|
782
|
-
print(f"Project: {project_dir}")
|
|
783
|
-
print(f"Configuration: {config_path}")
|
|
914
|
+
console.print("\n[cyan]š§ Installing Git hooks for HtmlGraph[/cyan]\n")
|
|
915
|
+
console.print(f"[dim]Project: {project_dir}[/dim]")
|
|
916
|
+
console.print(f"[dim]Configuration: {config_path}[/dim]")
|
|
784
917
|
|
|
785
918
|
if args.dry_run:
|
|
786
|
-
print("\n[DRY RUN MODE - No changes will be made]\n")
|
|
919
|
+
console.print("\n[yellow][DRY RUN MODE - No changes will be made][/yellow]\n")
|
|
787
920
|
|
|
788
921
|
results = installer.install_all_hooks(force=args.force, dry_run=args.dry_run)
|
|
789
922
|
|
|
790
|
-
# Display results
|
|
923
|
+
# Display results in a table
|
|
924
|
+
result_table = Table(title="Installation Results", border_style="cyan")
|
|
925
|
+
result_table.add_column("Hook", style="cyan", no_wrap=True)
|
|
926
|
+
result_table.add_column("Status", justify="center")
|
|
927
|
+
result_table.add_column("Message", style="dim")
|
|
928
|
+
|
|
791
929
|
success_count = 0
|
|
792
930
|
failure_count = 0
|
|
793
931
|
|
|
794
932
|
for hook_name, (success, message) in results.items():
|
|
795
933
|
if success:
|
|
796
934
|
success_count += 1
|
|
797
|
-
|
|
935
|
+
result_table.add_row(hook_name, "[green]ā Success[/green]", message)
|
|
798
936
|
else:
|
|
799
937
|
failure_count += 1
|
|
800
|
-
|
|
938
|
+
result_table.add_row(hook_name, "[red]ā Failed[/red]", message)
|
|
801
939
|
|
|
802
|
-
print(
|
|
803
|
-
print(
|
|
940
|
+
console.print(result_table)
|
|
941
|
+
console.print()
|
|
942
|
+
console.print(
|
|
943
|
+
f"[cyan]Summary:[/cyan] {success_count} installed, {failure_count} failed"
|
|
944
|
+
)
|
|
804
945
|
|
|
805
946
|
if not args.dry_run:
|
|
806
|
-
print(
|
|
807
|
-
print("
|
|
808
|
-
print(
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
print(
|
|
812
|
-
print("
|
|
947
|
+
console.print()
|
|
948
|
+
console.print(f"[green]Configuration saved to: {config_path}[/green]")
|
|
949
|
+
console.print(
|
|
950
|
+
"[cyan]Git events will now be logged to HtmlGraph automatically.[/cyan]"
|
|
951
|
+
)
|
|
952
|
+
console.print()
|
|
953
|
+
console.print("[bold cyan]Management commands:[/bold cyan]")
|
|
954
|
+
console.print(
|
|
955
|
+
"[dim] htmlgraph install-hooks --list # Show status[/dim]"
|
|
956
|
+
)
|
|
957
|
+
console.print(
|
|
958
|
+
"[dim] htmlgraph install-hooks --uninstall <hook> # Remove hook[/dim]"
|
|
959
|
+
)
|
|
960
|
+
console.print(
|
|
961
|
+
"[dim] htmlgraph install-hooks --enable <hook> # Enable hook[/dim]"
|
|
962
|
+
)
|
|
963
|
+
console.print(
|
|
964
|
+
"[dim] htmlgraph install-hooks --disable <hook> # Disable hook[/dim]"
|
|
965
|
+
)
|
|
813
966
|
|
|
814
967
|
|
|
815
968
|
def cmd_status(args: argparse.Namespace) -> None:
|
|
@@ -819,8 +972,9 @@ def cmd_status(args: argparse.Namespace) -> None:
|
|
|
819
972
|
|
|
820
973
|
from htmlgraph.sdk import SDK
|
|
821
974
|
|
|
822
|
-
# Use SDK to query all collections
|
|
823
|
-
|
|
975
|
+
# Use SDK to query all collections with status spinner
|
|
976
|
+
with console.status("[blue]Initializing SDK...", spinner="dots"):
|
|
977
|
+
sdk = SDK(directory=args.graph_dir)
|
|
824
978
|
|
|
825
979
|
total = 0
|
|
826
980
|
by_status: Counter[str] = Counter()
|
|
@@ -839,22 +993,34 @@ def cmd_status(args: argparse.Namespace) -> None:
|
|
|
839
993
|
"agents",
|
|
840
994
|
]
|
|
841
995
|
|
|
842
|
-
for
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
996
|
+
# Use progress bar for scanning collections
|
|
997
|
+
with Progress(
|
|
998
|
+
SpinnerColumn(),
|
|
999
|
+
TextColumn("[progress.description]{task.description}"),
|
|
1000
|
+
console=console,
|
|
1001
|
+
transient=True,
|
|
1002
|
+
) as progress:
|
|
1003
|
+
task = progress.add_task("Scanning collections...", total=len(collections))
|
|
1004
|
+
|
|
1005
|
+
for coll_name in collections:
|
|
1006
|
+
progress.update(task, description=f"Scanning {coll_name}...")
|
|
1007
|
+
coll = getattr(sdk, coll_name)
|
|
1008
|
+
try:
|
|
1009
|
+
nodes = coll.all()
|
|
1010
|
+
count = len(nodes)
|
|
1011
|
+
if count > 0:
|
|
1012
|
+
by_collection[coll_name] = count
|
|
1013
|
+
total += count
|
|
1014
|
+
|
|
1015
|
+
# Count by status
|
|
1016
|
+
for node in nodes:
|
|
1017
|
+
status = getattr(node, "status", "unknown")
|
|
1018
|
+
by_status[status] += 1
|
|
1019
|
+
except Exception:
|
|
1020
|
+
# Collection might not exist yet
|
|
1021
|
+
pass
|
|
1022
|
+
|
|
1023
|
+
progress.update(task, advance=1)
|
|
858
1024
|
|
|
859
1025
|
# Output based on format flag
|
|
860
1026
|
if args.format == "json":
|
|
@@ -995,17 +1161,21 @@ def cmd_query(args: argparse.Namespace) -> None:
|
|
|
995
1161
|
|
|
996
1162
|
graph_dir = Path(args.graph_dir)
|
|
997
1163
|
if not graph_dir.exists():
|
|
998
|
-
print(f"Error: {graph_dir} not found."
|
|
1164
|
+
console.print(f"[red]Error: {graph_dir} not found.[/red]")
|
|
999
1165
|
sys.exit(1)
|
|
1000
1166
|
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1167
|
+
# Query with status spinner
|
|
1168
|
+
with console.status(
|
|
1169
|
+
f"[blue]Querying with selector '{args.selector}'...", spinner="dots"
|
|
1170
|
+
):
|
|
1171
|
+
results = []
|
|
1172
|
+
for collection_dir in graph_dir.iterdir():
|
|
1173
|
+
if collection_dir.is_dir() and not collection_dir.name.startswith("."):
|
|
1174
|
+
graph = HtmlGraph(collection_dir, auto_load=True)
|
|
1175
|
+
for node in graph.query(args.selector):
|
|
1176
|
+
data = node_to_dict(node)
|
|
1177
|
+
data["_collection"] = collection_dir.name
|
|
1178
|
+
results.append(data)
|
|
1009
1179
|
|
|
1010
1180
|
if args.format == "json":
|
|
1011
1181
|
print(json.dumps(results, indent=2, default=str))
|
|
@@ -1027,10 +1197,31 @@ def cmd_session_start(args: argparse.Namespace) -> None:
|
|
|
1027
1197
|
"""Start a new session."""
|
|
1028
1198
|
import json
|
|
1029
1199
|
|
|
1200
|
+
from pydantic import ValidationError
|
|
1201
|
+
|
|
1202
|
+
from htmlgraph.pydantic_models import SessionStartInput
|
|
1030
1203
|
from htmlgraph.sdk import SDK
|
|
1204
|
+
from htmlgraph.validation import display_validation_error
|
|
1031
1205
|
|
|
1032
|
-
|
|
1033
|
-
|
|
1206
|
+
try:
|
|
1207
|
+
input_data = SessionStartInput(
|
|
1208
|
+
session_id=args.id,
|
|
1209
|
+
title=args.title,
|
|
1210
|
+
agent=args.agent,
|
|
1211
|
+
)
|
|
1212
|
+
except ValidationError as e:
|
|
1213
|
+
display_validation_error(e)
|
|
1214
|
+
sys.exit(1)
|
|
1215
|
+
|
|
1216
|
+
with console.status(
|
|
1217
|
+
"[blue]Initializing SDK and starting session...", spinner="dots"
|
|
1218
|
+
):
|
|
1219
|
+
sdk = SDK(directory=args.graph_dir, agent=input_data.agent)
|
|
1220
|
+
session = sdk.start_session(
|
|
1221
|
+
session_id=input_data.session_id,
|
|
1222
|
+
title=input_data.title,
|
|
1223
|
+
agent=input_data.agent,
|
|
1224
|
+
)
|
|
1034
1225
|
|
|
1035
1226
|
if args.format == "json":
|
|
1036
1227
|
from htmlgraph.converter import session_to_dict
|
|
@@ -1048,19 +1239,33 @@ def cmd_session_end(args: argparse.Namespace) -> None:
|
|
|
1048
1239
|
"""End a session."""
|
|
1049
1240
|
import json
|
|
1050
1241
|
|
|
1242
|
+
from pydantic import ValidationError
|
|
1243
|
+
|
|
1244
|
+
from htmlgraph.pydantic_models import SessionEndInput
|
|
1051
1245
|
from htmlgraph.sdk import SDK
|
|
1246
|
+
from htmlgraph.validation import display_validation_error
|
|
1247
|
+
|
|
1248
|
+
try:
|
|
1249
|
+
input_data = SessionEndInput(
|
|
1250
|
+
session_id=args.id,
|
|
1251
|
+
notes=args.notes,
|
|
1252
|
+
recommend=args.recommend,
|
|
1253
|
+
blocker=args.blocker if args.blocker else None,
|
|
1254
|
+
)
|
|
1255
|
+
except ValidationError as e:
|
|
1256
|
+
display_validation_error(e)
|
|
1257
|
+
sys.exit(1)
|
|
1052
1258
|
|
|
1053
1259
|
sdk = SDK(directory=args.graph_dir)
|
|
1054
|
-
blockers = args.blocker if args.blocker else None
|
|
1055
1260
|
session = sdk.end_session(
|
|
1056
|
-
|
|
1057
|
-
handoff_notes=
|
|
1058
|
-
recommended_next=
|
|
1059
|
-
blockers=
|
|
1261
|
+
input_data.session_id,
|
|
1262
|
+
handoff_notes=input_data.notes,
|
|
1263
|
+
recommended_next=input_data.recommend,
|
|
1264
|
+
blockers=input_data.blocker,
|
|
1060
1265
|
)
|
|
1061
1266
|
|
|
1062
1267
|
if session is None:
|
|
1063
|
-
print(f"Error: Session '{
|
|
1268
|
+
console.print(f"[red]Error: Session '{input_data.session_id}' not found.[/red]")
|
|
1064
1269
|
sys.exit(1)
|
|
1065
1270
|
|
|
1066
1271
|
if args.format == "json":
|
|
@@ -1079,18 +1284,30 @@ def cmd_session_handoff(args: argparse.Namespace) -> None:
|
|
|
1079
1284
|
"""Set or show session handoff context."""
|
|
1080
1285
|
import json
|
|
1081
1286
|
|
|
1287
|
+
from pydantic import ValidationError
|
|
1288
|
+
|
|
1289
|
+
from htmlgraph.pydantic_models import SessionHandoffInput
|
|
1082
1290
|
from htmlgraph.sdk import SDK
|
|
1291
|
+
from htmlgraph.validation import display_validation_error
|
|
1083
1292
|
|
|
1084
|
-
|
|
1293
|
+
try:
|
|
1294
|
+
input_data = SessionHandoffInput(
|
|
1295
|
+
session_id=args.session_id,
|
|
1296
|
+
notes=args.notes,
|
|
1297
|
+
recommend=args.recommend,
|
|
1298
|
+
blocker=args.blocker if args.blocker else None,
|
|
1299
|
+
show=args.show,
|
|
1300
|
+
)
|
|
1301
|
+
except ValidationError as e:
|
|
1302
|
+
display_validation_error(e)
|
|
1303
|
+
sys.exit(1)
|
|
1085
1304
|
|
|
1086
|
-
|
|
1087
|
-
# For showing, we might still need direct manager access or add more methods to SDK
|
|
1088
|
-
# But for now, let's keep using SessionManager logic via SDK property if needed
|
|
1089
|
-
# or implement show logic here using SDK collections
|
|
1305
|
+
sdk = SDK(directory=args.graph_dir, agent=args.agent)
|
|
1090
1306
|
|
|
1307
|
+
if input_data.show:
|
|
1091
1308
|
# Use session_manager.get_session() to get Session objects (not Node)
|
|
1092
|
-
if
|
|
1093
|
-
session = sdk.session_manager.get_session(
|
|
1309
|
+
if input_data.session_id:
|
|
1310
|
+
session = sdk.session_manager.get_session(input_data.session_id)
|
|
1094
1311
|
else:
|
|
1095
1312
|
# Need "last ended session" - SDK doesn't expose this yet.
|
|
1096
1313
|
# Fallback to session_manager logic exposed on SDK
|
|
@@ -1118,7 +1335,7 @@ def cmd_session_handoff(args: argparse.Namespace) -> None:
|
|
|
1118
1335
|
return
|
|
1119
1336
|
|
|
1120
1337
|
# Setting handoff
|
|
1121
|
-
if not (
|
|
1338
|
+
if not (input_data.notes or input_data.recommend or input_data.blocker):
|
|
1122
1339
|
print(
|
|
1123
1340
|
"Error: Provide --notes, --recommend, or --blocker (or use --show).",
|
|
1124
1341
|
file=sys.stderr,
|
|
@@ -1126,15 +1343,17 @@ def cmd_session_handoff(args: argparse.Namespace) -> None:
|
|
|
1126
1343
|
sys.exit(1)
|
|
1127
1344
|
|
|
1128
1345
|
handoff_result = sdk.set_session_handoff(
|
|
1129
|
-
session_id=
|
|
1130
|
-
handoff_notes=
|
|
1131
|
-
recommended_next=
|
|
1132
|
-
blockers=
|
|
1346
|
+
session_id=input_data.session_id, # Optional, defaults to active
|
|
1347
|
+
handoff_notes=input_data.notes,
|
|
1348
|
+
recommended_next=input_data.recommend,
|
|
1349
|
+
blockers=input_data.blocker,
|
|
1133
1350
|
)
|
|
1134
1351
|
|
|
1135
1352
|
if handoff_result is None:
|
|
1136
|
-
if
|
|
1137
|
-
print(
|
|
1353
|
+
if input_data.session_id:
|
|
1354
|
+
print(
|
|
1355
|
+
f"Error: Session '{input_data.session_id}' not found.", file=sys.stderr
|
|
1356
|
+
)
|
|
1138
1357
|
else:
|
|
1139
1358
|
print(
|
|
1140
1359
|
"Error: No active session found. Provide --session-id.",
|
|
@@ -1160,17 +1379,20 @@ def cmd_session_list(args: argparse.Namespace) -> None:
|
|
|
1160
1379
|
return
|
|
1161
1380
|
|
|
1162
1381
|
converter = SessionConverter(sessions_dir)
|
|
1163
|
-
sessions = converter.load_all()
|
|
1164
1382
|
|
|
1165
|
-
#
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1383
|
+
# Load sessions with status spinner
|
|
1384
|
+
with console.status("[blue]Loading sessions...", spinner="dots"):
|
|
1385
|
+
sessions = converter.load_all()
|
|
1386
|
+
|
|
1387
|
+
# Sort by started_at descending (handle mixed tz-aware/naive datetimes)
|
|
1388
|
+
def sort_key(s: Any) -> Any:
|
|
1389
|
+
ts = s.started_at
|
|
1390
|
+
# Make naive datetimes comparable by assuming UTC
|
|
1391
|
+
if ts.tzinfo is None:
|
|
1392
|
+
return ts.replace(tzinfo=None)
|
|
1393
|
+
return ts.replace(tzinfo=None) # Compare as naive for sorting
|
|
1172
1394
|
|
|
1173
|
-
|
|
1395
|
+
sessions.sort(key=sort_key, reverse=True)
|
|
1174
1396
|
|
|
1175
1397
|
if args.format == "json":
|
|
1176
1398
|
from htmlgraph.converter import session_to_dict
|
|
@@ -1178,17 +1400,34 @@ def cmd_session_list(args: argparse.Namespace) -> None:
|
|
|
1178
1400
|
print(json.dumps([session_to_dict(s) for s in sessions], indent=2))
|
|
1179
1401
|
else:
|
|
1180
1402
|
if not sessions:
|
|
1181
|
-
print("No sessions found.")
|
|
1403
|
+
console.print("[yellow]No sessions found.[/yellow]")
|
|
1182
1404
|
return
|
|
1183
1405
|
|
|
1184
|
-
|
|
1185
|
-
|
|
1406
|
+
# Create Rich table
|
|
1407
|
+
table = Table(
|
|
1408
|
+
title="Sessions",
|
|
1409
|
+
show_header=True,
|
|
1410
|
+
header_style="bold magenta",
|
|
1411
|
+
box=box.ROUNDED,
|
|
1412
|
+
)
|
|
1413
|
+
table.add_column("ID", style="cyan", no_wrap=False, max_width=30)
|
|
1414
|
+
table.add_column("Status", style="green", width=10)
|
|
1415
|
+
table.add_column("Agent", style="blue", width=15)
|
|
1416
|
+
table.add_column("Events", justify="right", style="yellow", width=8)
|
|
1417
|
+
table.add_column("Started", style="white")
|
|
1418
|
+
|
|
1186
1419
|
for session in sessions:
|
|
1187
1420
|
started = session.started_at.strftime("%Y-%m-%d %H:%M")
|
|
1188
|
-
|
|
1189
|
-
|
|
1421
|
+
table.add_row(
|
|
1422
|
+
session.id,
|
|
1423
|
+
session.status,
|
|
1424
|
+
session.agent,
|
|
1425
|
+
str(session.event_count),
|
|
1426
|
+
started,
|
|
1190
1427
|
)
|
|
1191
1428
|
|
|
1429
|
+
console.print(table)
|
|
1430
|
+
|
|
1192
1431
|
|
|
1193
1432
|
def cmd_session_start_info(args: argparse.Namespace) -> None:
|
|
1194
1433
|
"""Get comprehensive session start information (optimized for AI agents)."""
|
|
@@ -1210,16 +1449,18 @@ def cmd_session_start_info(args: argparse.Namespace) -> None:
|
|
|
1210
1449
|
else:
|
|
1211
1450
|
# Human-readable format
|
|
1212
1451
|
status: dict = info["status"] # type: ignore
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
print("=" * 80)
|
|
1216
|
-
|
|
1217
|
-
# Project status
|
|
1218
|
-
print(f"\nProject: {status.get('project_name', 'HtmlGraph')}")
|
|
1219
|
-
print(f"Total features: {status.get('total_features', 0)}")
|
|
1220
|
-
print(f"In progress: {status.get('wip_count', 0)}")
|
|
1452
|
+
|
|
1453
|
+
# Project status panel
|
|
1221
1454
|
by_status = status.get("by_status", {})
|
|
1222
|
-
|
|
1455
|
+
project_info = (
|
|
1456
|
+
f"Project: {status.get('project_name', 'HtmlGraph')}\n"
|
|
1457
|
+
f"Total features: {status.get('total_features', 0)}\n"
|
|
1458
|
+
f"In progress: {status.get('wip_count', 0)}\n"
|
|
1459
|
+
f"Completed: {by_status.get('done', 0)}"
|
|
1460
|
+
)
|
|
1461
|
+
console.print(
|
|
1462
|
+
Panel(project_info, title="SESSION START INFO", border_style="cyan")
|
|
1463
|
+
)
|
|
1223
1464
|
|
|
1224
1465
|
# Active work item (validation status)
|
|
1225
1466
|
active_work = info.get("active_work")
|
|
@@ -1382,13 +1623,17 @@ def cmd_session_dedupe(args: argparse.Namespace) -> None:
|
|
|
1382
1623
|
"""Move low-signal session files out of the main sessions directory."""
|
|
1383
1624
|
from htmlgraph import SDK
|
|
1384
1625
|
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1626
|
+
with console.status("[blue]Initializing SDK...", spinner="dots"):
|
|
1627
|
+
sdk = SDK(directory=args.graph_dir)
|
|
1628
|
+
|
|
1629
|
+
operation = "Analyzing" if args.dry_run else "Deduplicating"
|
|
1630
|
+
with console.status(f"[blue]{operation} sessions...", spinner="dots"):
|
|
1631
|
+
result = sdk.dedupe_sessions(
|
|
1632
|
+
max_events=args.max_events,
|
|
1633
|
+
move_dir_name=args.move_dir,
|
|
1634
|
+
dry_run=args.dry_run,
|
|
1635
|
+
stale_extra_active=not args.no_stale_active,
|
|
1636
|
+
)
|
|
1392
1637
|
|
|
1393
1638
|
print(f"Scanned: {result['scanned']}")
|
|
1394
1639
|
print(f"Moved: {result['moved']}")
|
|
@@ -1424,15 +1669,14 @@ def cmd_session_link(args: argparse.Namespace) -> None:
|
|
|
1424
1669
|
session_graph = HtmlGraph(sessions_dir)
|
|
1425
1670
|
session = session_graph.get(args.session_id)
|
|
1426
1671
|
if not session:
|
|
1427
|
-
print(f"Error: Failed to load session '{args.session_id}'"
|
|
1672
|
+
console.print(f"[red]Error: Failed to load session '{args.session_id}'[/red]")
|
|
1428
1673
|
sys.exit(1)
|
|
1429
1674
|
|
|
1430
1675
|
# Load feature
|
|
1431
1676
|
feature_file = feature_dir / f"{args.feature_id}.html"
|
|
1432
1677
|
if not feature_file.exists():
|
|
1433
|
-
print(
|
|
1434
|
-
f"Error: Feature '{args.feature_id}' not found at {feature_file}"
|
|
1435
|
-
file=sys.stderr,
|
|
1678
|
+
console.print(
|
|
1679
|
+
f"[red]Error: Feature '{args.feature_id}' not found at {feature_file}[/red]"
|
|
1436
1680
|
)
|
|
1437
1681
|
sys.exit(1)
|
|
1438
1682
|
|
|
@@ -1620,6 +1864,70 @@ def cmd_session_validate_attribution(args: argparse.Namespace) -> None:
|
|
|
1620
1864
|
)
|
|
1621
1865
|
|
|
1622
1866
|
|
|
1867
|
+
def cmd_session_debug(args: argparse.Namespace) -> None:
|
|
1868
|
+
"""Show full error traceback and debugging information for a session."""
|
|
1869
|
+
from htmlgraph.session_manager import SessionManager
|
|
1870
|
+
|
|
1871
|
+
manager = SessionManager(args.graph_dir)
|
|
1872
|
+
|
|
1873
|
+
try:
|
|
1874
|
+
session = manager.get_session(args.session_id)
|
|
1875
|
+
except Exception:
|
|
1876
|
+
session = None
|
|
1877
|
+
|
|
1878
|
+
if not session:
|
|
1879
|
+
console.print(f"[red]ā Session not found:[/red] {args.session_id}")
|
|
1880
|
+
sys.exit(1)
|
|
1881
|
+
|
|
1882
|
+
# Display session header
|
|
1883
|
+
console.print("\n[bold cyan]Session Debug Information[/bold cyan]")
|
|
1884
|
+
console.print(f"[dim]Session ID:[/dim] {session.id}")
|
|
1885
|
+
console.print(f"[dim]Agent:[/dim] {session.agent}")
|
|
1886
|
+
console.print(f"[dim]Status:[/dim] {session.status}")
|
|
1887
|
+
console.print(
|
|
1888
|
+
f"[dim]Started:[/dim] {session.started_at.strftime('%Y-%m-%d %H:%M:%S')}"
|
|
1889
|
+
)
|
|
1890
|
+
|
|
1891
|
+
# Check for errors
|
|
1892
|
+
errors = session.error_log
|
|
1893
|
+
if not errors:
|
|
1894
|
+
console.print("\n[green]ā No errors in session[/green]")
|
|
1895
|
+
return
|
|
1896
|
+
|
|
1897
|
+
console.print(f"\n[bold yellow]Errors ({len(errors)})[/bold yellow]")
|
|
1898
|
+
|
|
1899
|
+
for i, error in enumerate(errors, 1):
|
|
1900
|
+
# Error header
|
|
1901
|
+
console.print(
|
|
1902
|
+
f"\n[bold]Error {i}[/bold] [{error.timestamp.strftime('%H:%M:%S')}]"
|
|
1903
|
+
)
|
|
1904
|
+
console.print(f"[red]{error.error_type}[/red]: {error.message}")
|
|
1905
|
+
|
|
1906
|
+
# Tool information
|
|
1907
|
+
if error.tool:
|
|
1908
|
+
console.print(f"[dim]Tool:[/dim] {error.tool}")
|
|
1909
|
+
|
|
1910
|
+
# Context information
|
|
1911
|
+
if error.context:
|
|
1912
|
+
console.print(f"[dim]Context:[/dim] {error.context}")
|
|
1913
|
+
|
|
1914
|
+
# Full traceback
|
|
1915
|
+
if error.traceback:
|
|
1916
|
+
console.print("\n[dim]Traceback:[/dim]")
|
|
1917
|
+
console.print(f"[dim]{error.traceback}[/dim]")
|
|
1918
|
+
|
|
1919
|
+
console.print("[dim]ā" * 80 + "[/dim]")
|
|
1920
|
+
|
|
1921
|
+
# Summary
|
|
1922
|
+
console.print("\n[bold cyan]Summary[/bold cyan]")
|
|
1923
|
+
console.print(f"[dim]Total errors:[/dim] {len(errors)}")
|
|
1924
|
+
error_types: dict[str, int] = {}
|
|
1925
|
+
for error in errors:
|
|
1926
|
+
error_types[error.error_type] = error_types.get(error.error_type, 0) + 1
|
|
1927
|
+
for error_type, count in sorted(error_types.items()):
|
|
1928
|
+
console.print(f"[dim] - {error_type}:[/dim] {count}")
|
|
1929
|
+
|
|
1930
|
+
|
|
1623
1931
|
# =========================================================================
|
|
1624
1932
|
# Transcript Commands
|
|
1625
1933
|
# =========================================================================
|
|
@@ -1974,27 +2282,33 @@ def cmd_transcript_patterns(args: argparse.Namespace) -> None:
|
|
|
1974
2282
|
)
|
|
1975
2283
|
)
|
|
1976
2284
|
else:
|
|
1977
|
-
print("Workflow Patterns Detected")
|
|
1978
|
-
print("=" * 50)
|
|
1979
|
-
|
|
1980
2285
|
optimal = [p for p in patterns if p.category == "optimal"]
|
|
1981
2286
|
anti = [p for p in patterns if p.category == "anti-pattern"]
|
|
1982
2287
|
neutral = [p for p in patterns if p.category == "neutral"][:10]
|
|
1983
2288
|
|
|
2289
|
+
content = ""
|
|
1984
2290
|
if optimal:
|
|
1985
|
-
|
|
2291
|
+
content += "ā
Optimal Patterns:\n"
|
|
1986
2292
|
for p in optimal:
|
|
1987
|
-
|
|
2293
|
+
content += f" {' ā '.join(p.sequence)} ({p.count}x)\n"
|
|
1988
2294
|
|
|
1989
2295
|
if anti:
|
|
1990
|
-
|
|
2296
|
+
content += "\nā ļø Anti-Patterns:\n"
|
|
1991
2297
|
for p in anti:
|
|
1992
|
-
|
|
2298
|
+
content += f" {' ā '.join(p.sequence)} ({p.count}x)\n"
|
|
1993
2299
|
|
|
1994
2300
|
if neutral:
|
|
1995
|
-
|
|
2301
|
+
content += "\nš Common Patterns:\n"
|
|
1996
2302
|
for p in neutral:
|
|
1997
|
-
|
|
2303
|
+
content += f" {' ā '.join(p.sequence)} ({p.count}x)\n"
|
|
2304
|
+
|
|
2305
|
+
console.print(
|
|
2306
|
+
Panel(
|
|
2307
|
+
content.strip(),
|
|
2308
|
+
title="Workflow Patterns Detected",
|
|
2309
|
+
border_style="green",
|
|
2310
|
+
)
|
|
2311
|
+
)
|
|
1998
2312
|
|
|
1999
2313
|
|
|
2000
2314
|
def cmd_transcript_transitions(args: argparse.Namespace) -> None:
|
|
@@ -2009,11 +2323,6 @@ def cmd_transcript_transitions(args: argparse.Namespace) -> None:
|
|
|
2009
2323
|
if args.format == "json":
|
|
2010
2324
|
print(json.dumps(transitions, indent=2))
|
|
2011
2325
|
else:
|
|
2012
|
-
print("Tool Transition Matrix")
|
|
2013
|
-
print("=" * 50)
|
|
2014
|
-
print("(from_tool ā to_tool: count)")
|
|
2015
|
-
print()
|
|
2016
|
-
|
|
2017
2326
|
# Flatten and sort
|
|
2018
2327
|
flat = []
|
|
2019
2328
|
for from_tool, tos in transitions.items():
|
|
@@ -2022,9 +2331,23 @@ def cmd_transcript_transitions(args: argparse.Namespace) -> None:
|
|
|
2022
2331
|
|
|
2023
2332
|
flat.sort(key=lambda x: -x[2])
|
|
2024
2333
|
|
|
2334
|
+
# Create Rich table
|
|
2335
|
+
table = Table(
|
|
2336
|
+
title="Tool Transition Matrix",
|
|
2337
|
+
show_header=True,
|
|
2338
|
+
header_style="bold magenta",
|
|
2339
|
+
box=box.ROUNDED,
|
|
2340
|
+
)
|
|
2341
|
+
table.add_column("From Tool", style="cyan", width=12)
|
|
2342
|
+
table.add_column("To Tool", style="blue", width=12)
|
|
2343
|
+
table.add_column("Count", justify="right", style="yellow", width=6)
|
|
2344
|
+
table.add_column("Visual", style="green")
|
|
2345
|
+
|
|
2025
2346
|
for from_t, to_t, count in flat[:20]:
|
|
2026
2347
|
bar = "ā" * min(count, 20)
|
|
2027
|
-
|
|
2348
|
+
table.add_row(from_t, to_t, str(count), bar)
|
|
2349
|
+
|
|
2350
|
+
console.print(table)
|
|
2028
2351
|
|
|
2029
2352
|
|
|
2030
2353
|
def cmd_transcript_recommendations(args: argparse.Namespace) -> None:
|
|
@@ -2041,10 +2364,10 @@ def cmd_transcript_recommendations(args: argparse.Namespace) -> None:
|
|
|
2041
2364
|
if args.format == "json":
|
|
2042
2365
|
print(json.dumps({"recommendations": recommendations}, indent=2))
|
|
2043
2366
|
else:
|
|
2044
|
-
|
|
2045
|
-
print(
|
|
2046
|
-
|
|
2047
|
-
|
|
2367
|
+
content = "\n".join([f" ⢠{rec}" for rec in recommendations])
|
|
2368
|
+
console.print(
|
|
2369
|
+
Panel(content, title="Workflow Recommendations", border_style="yellow")
|
|
2370
|
+
)
|
|
2048
2371
|
|
|
2049
2372
|
|
|
2050
2373
|
def cmd_transcript_insights(args: argparse.Namespace) -> None:
|
|
@@ -2071,24 +2394,43 @@ def cmd_transcript_insights(args: argparse.Namespace) -> None:
|
|
|
2071
2394
|
)
|
|
2072
2395
|
)
|
|
2073
2396
|
else:
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2397
|
+
# Summary panel
|
|
2398
|
+
summary = (
|
|
2399
|
+
f"Sessions Analyzed: {insights.total_sessions}\n"
|
|
2400
|
+
f"Total User Messages: {insights.total_user_messages}\n"
|
|
2401
|
+
f"Total Tool Calls: {insights.total_tool_calls}\n"
|
|
2402
|
+
f"Avg Session Health: {insights.avg_session_health:.0%}"
|
|
2403
|
+
)
|
|
2404
|
+
console.print(
|
|
2405
|
+
Panel(summary, title="š Transcript Insights", border_style="cyan")
|
|
2406
|
+
)
|
|
2081
2407
|
|
|
2408
|
+
# Top tools table
|
|
2082
2409
|
if insights.tool_frequency:
|
|
2083
|
-
|
|
2410
|
+
table = Table(
|
|
2411
|
+
title="š§ Top Tools",
|
|
2412
|
+
show_header=True,
|
|
2413
|
+
header_style="bold magenta",
|
|
2414
|
+
box=box.ROUNDED,
|
|
2415
|
+
)
|
|
2416
|
+
table.add_column("Tool", style="cyan", width=15)
|
|
2417
|
+
table.add_column("Count", justify="right", style="yellow", width=6)
|
|
2418
|
+
table.add_column("Visual", style="green")
|
|
2419
|
+
|
|
2084
2420
|
for tool, count in list(insights.tool_frequency.items())[:8]:
|
|
2085
2421
|
bar = "ā" * min(count // 5, 15)
|
|
2086
|
-
|
|
2422
|
+
table.add_row(tool, str(count), bar)
|
|
2087
2423
|
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2424
|
+
console.print(table)
|
|
2425
|
+
|
|
2426
|
+
# Recommendations panel
|
|
2427
|
+
if insights.recommendations:
|
|
2428
|
+
rec_content = "\n".join(
|
|
2429
|
+
[f" {rec}" for rec in insights.recommendations[:5]]
|
|
2430
|
+
)
|
|
2431
|
+
console.print(
|
|
2432
|
+
Panel(rec_content, title="š” Recommendations", border_style="yellow")
|
|
2433
|
+
)
|
|
2092
2434
|
|
|
2093
2435
|
|
|
2094
2436
|
def cmd_transcript_export(args: argparse.Namespace) -> None:
|
|
@@ -2210,18 +2552,34 @@ def cmd_track(args: argparse.Namespace) -> None:
|
|
|
2210
2552
|
"""Track an activity in the current session."""
|
|
2211
2553
|
import json
|
|
2212
2554
|
|
|
2555
|
+
from pydantic import ValidationError
|
|
2556
|
+
|
|
2213
2557
|
from htmlgraph import SDK
|
|
2558
|
+
from htmlgraph.pydantic_models import ActivityTrackInput
|
|
2559
|
+
from htmlgraph.validation import display_validation_error
|
|
2560
|
+
|
|
2561
|
+
try:
|
|
2562
|
+
input_data = ActivityTrackInput(
|
|
2563
|
+
tool=args.tool,
|
|
2564
|
+
summary=args.summary,
|
|
2565
|
+
files=args.files,
|
|
2566
|
+
session=args.session,
|
|
2567
|
+
failed=args.failed,
|
|
2568
|
+
)
|
|
2569
|
+
except ValidationError as e:
|
|
2570
|
+
display_validation_error(e)
|
|
2571
|
+
sys.exit(1)
|
|
2214
2572
|
|
|
2215
2573
|
agent = os.environ.get("HTMLGRAPH_AGENT")
|
|
2216
2574
|
sdk = SDK(directory=args.graph_dir, agent=agent)
|
|
2217
2575
|
|
|
2218
2576
|
try:
|
|
2219
2577
|
entry = sdk.track_activity(
|
|
2220
|
-
tool=
|
|
2221
|
-
summary=
|
|
2222
|
-
file_paths=
|
|
2223
|
-
success=not
|
|
2224
|
-
session_id=
|
|
2578
|
+
tool=input_data.tool,
|
|
2579
|
+
summary=input_data.summary,
|
|
2580
|
+
file_paths=input_data.files,
|
|
2581
|
+
success=not input_data.failed,
|
|
2582
|
+
session_id=input_data.session, # None if not specified, SDK will find active session
|
|
2225
2583
|
)
|
|
2226
2584
|
except ValueError as e:
|
|
2227
2585
|
print(f"Error: {e}", file=sys.stderr)
|
|
@@ -2664,160 +3022,121 @@ def cmd_agent_list(args: argparse.Namespace) -> None:
|
|
|
2664
3022
|
|
|
2665
3023
|
def cmd_feature_create(args: argparse.Namespace) -> None:
|
|
2666
3024
|
"""Create a new feature."""
|
|
2667
|
-
import
|
|
2668
|
-
|
|
2669
|
-
from htmlgraph.sdk import SDK
|
|
3025
|
+
from pydantic import ValidationError
|
|
2670
3026
|
|
|
2671
|
-
|
|
2672
|
-
|
|
3027
|
+
from htmlgraph.cli_commands.feature import FeatureCreateCommand
|
|
3028
|
+
from htmlgraph.pydantic_models import FeatureCreateInput
|
|
3029
|
+
from htmlgraph.validation import display_validation_error
|
|
2673
3030
|
|
|
2674
3031
|
try:
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
priority=args.priority,
|
|
2685
|
-
)
|
|
2686
|
-
if args.steps:
|
|
2687
|
-
builder.add_steps(args.steps)
|
|
2688
|
-
node = builder.save()
|
|
2689
|
-
else:
|
|
2690
|
-
# Fallback to SessionManager directly for non-feature collections
|
|
2691
|
-
# (or extend SDK to support create on all collections)
|
|
2692
|
-
# For consistency with old CLI, we use SessionManager here if not features.
|
|
2693
|
-
# But wait, SDK initializes SessionManager.
|
|
2694
|
-
|
|
2695
|
-
# Creating bugs/chores via SDK isn't fully fluent yet.
|
|
2696
|
-
# Let's use the low-level SessionManager.create_feature logic for now via SDK's session_manager
|
|
2697
|
-
# IF we want to strictly use SDK. But SDK.session_manager IS exposed now.
|
|
2698
|
-
node = sdk.session_manager.create_feature(
|
|
2699
|
-
title=args.title,
|
|
2700
|
-
collection=args.collection,
|
|
2701
|
-
description=args.description or "",
|
|
2702
|
-
priority=args.priority,
|
|
2703
|
-
steps=args.steps,
|
|
2704
|
-
agent=args.agent,
|
|
2705
|
-
)
|
|
2706
|
-
|
|
2707
|
-
except ValueError as e:
|
|
2708
|
-
print(f"Error: {e}", file=sys.stderr)
|
|
3032
|
+
input_data = FeatureCreateInput(
|
|
3033
|
+
title=args.title,
|
|
3034
|
+
description=args.description,
|
|
3035
|
+
priority=args.priority,
|
|
3036
|
+
steps=args.steps,
|
|
3037
|
+
collection=args.collection,
|
|
3038
|
+
)
|
|
3039
|
+
except ValidationError as e:
|
|
3040
|
+
display_validation_error(e)
|
|
2709
3041
|
sys.exit(1)
|
|
2710
3042
|
|
|
2711
|
-
|
|
2712
|
-
|
|
3043
|
+
# Convert steps count to list of step names (e.g., 3 -> ["Step 1", "Step 2", "Step 3"])
|
|
3044
|
+
step_names = None
|
|
3045
|
+
if input_data.steps:
|
|
3046
|
+
step_names = [f"Step {i + 1}" for i in range(input_data.steps)]
|
|
2713
3047
|
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
3048
|
+
command = FeatureCreateCommand(
|
|
3049
|
+
collection=input_data.collection,
|
|
3050
|
+
title=input_data.title,
|
|
3051
|
+
description=input_data.description or "",
|
|
3052
|
+
priority=input_data.priority,
|
|
3053
|
+
steps=step_names,
|
|
3054
|
+
track_id=args.track,
|
|
3055
|
+
)
|
|
3056
|
+
command.run(graph_dir=args.graph_dir, agent=args.agent, output_format=args.format)
|
|
2720
3057
|
|
|
2721
3058
|
|
|
2722
3059
|
def cmd_feature_start(args: argparse.Namespace) -> None:
|
|
2723
3060
|
"""Start working on a feature."""
|
|
2724
|
-
import
|
|
3061
|
+
from pydantic import ValidationError
|
|
2725
3062
|
|
|
2726
|
-
from htmlgraph.
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
collection = getattr(sdk, args.collection, None)
|
|
2730
|
-
|
|
2731
|
-
if not collection:
|
|
2732
|
-
print(
|
|
2733
|
-
f"Error: Collection '{args.collection}' not found in SDK.", file=sys.stderr
|
|
2734
|
-
)
|
|
2735
|
-
sys.exit(1)
|
|
3063
|
+
from htmlgraph.cli_commands.feature import FeatureStartCommand
|
|
3064
|
+
from htmlgraph.pydantic_models import FeatureStartInput
|
|
3065
|
+
from htmlgraph.validation import display_validation_error
|
|
2736
3066
|
|
|
2737
3067
|
try:
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
sys.exit(1)
|
|
2742
|
-
|
|
2743
|
-
if node is None:
|
|
2744
|
-
print(
|
|
2745
|
-
f"Error: Feature '{args.id}' not found in {args.collection}.",
|
|
2746
|
-
file=sys.stderr,
|
|
3068
|
+
input_data = FeatureStartInput(
|
|
3069
|
+
feature_id=args.id,
|
|
3070
|
+
collection=args.collection,
|
|
2747
3071
|
)
|
|
3072
|
+
except ValidationError as e:
|
|
3073
|
+
display_validation_error(e)
|
|
2748
3074
|
sys.exit(1)
|
|
2749
3075
|
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
print(f"Started: {node.id}")
|
|
2756
|
-
print(f" Title: {node.title}")
|
|
2757
|
-
print(f" Status: {node.status}")
|
|
2758
|
-
|
|
2759
|
-
# Show WIP status
|
|
2760
|
-
status = sdk.session_manager.get_status()
|
|
2761
|
-
print(f" WIP: {status['wip_count']}/{status['wip_limit']}")
|
|
3076
|
+
command = FeatureStartCommand(
|
|
3077
|
+
collection=input_data.collection,
|
|
3078
|
+
feature_id=input_data.feature_id,
|
|
3079
|
+
)
|
|
3080
|
+
command.run(graph_dir=args.graph_dir, agent=args.agent, output_format=args.format)
|
|
2762
3081
|
|
|
2763
3082
|
|
|
2764
3083
|
def cmd_feature_complete(args: argparse.Namespace) -> None:
|
|
2765
3084
|
"""Mark a feature as complete."""
|
|
2766
|
-
import
|
|
3085
|
+
from pydantic import ValidationError
|
|
2767
3086
|
|
|
2768
|
-
from htmlgraph.
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
collection = getattr(sdk, args.collection, None)
|
|
2772
|
-
|
|
2773
|
-
if not collection:
|
|
2774
|
-
print(
|
|
2775
|
-
f"Error: Collection '{args.collection}' not found in SDK.", file=sys.stderr
|
|
2776
|
-
)
|
|
2777
|
-
sys.exit(1)
|
|
3087
|
+
from htmlgraph.cli_commands.feature import FeatureCompleteCommand
|
|
3088
|
+
from htmlgraph.pydantic_models import FeatureCompleteInput
|
|
3089
|
+
from htmlgraph.validation import display_validation_error
|
|
2778
3090
|
|
|
2779
3091
|
try:
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
sys.exit(1)
|
|
2784
|
-
|
|
2785
|
-
if node is None:
|
|
2786
|
-
print(
|
|
2787
|
-
f"Error: Feature '{args.id}' not found in {args.collection}.",
|
|
2788
|
-
file=sys.stderr,
|
|
3092
|
+
input_data = FeatureCompleteInput(
|
|
3093
|
+
feature_id=args.id,
|
|
3094
|
+
collection=args.collection,
|
|
2789
3095
|
)
|
|
3096
|
+
except ValidationError as e:
|
|
3097
|
+
display_validation_error(e)
|
|
2790
3098
|
sys.exit(1)
|
|
2791
3099
|
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
print(f"Completed: {node.id}")
|
|
2798
|
-
print(f" Title: {node.title}")
|
|
3100
|
+
command = FeatureCompleteCommand(
|
|
3101
|
+
collection=input_data.collection,
|
|
3102
|
+
feature_id=input_data.feature_id,
|
|
3103
|
+
)
|
|
3104
|
+
command.run(graph_dir=args.graph_dir, agent=args.agent, output_format=args.format)
|
|
2799
3105
|
|
|
2800
3106
|
|
|
2801
3107
|
def cmd_feature_primary(args: argparse.Namespace) -> None:
|
|
2802
3108
|
"""Set the primary feature for attribution."""
|
|
2803
3109
|
import json
|
|
2804
3110
|
|
|
3111
|
+
from pydantic import ValidationError
|
|
3112
|
+
|
|
3113
|
+
from htmlgraph.pydantic_models import FeaturePrimaryInput
|
|
2805
3114
|
from htmlgraph.sdk import SDK
|
|
3115
|
+
from htmlgraph.validation import display_validation_error
|
|
3116
|
+
|
|
3117
|
+
try:
|
|
3118
|
+
input_data = FeaturePrimaryInput(
|
|
3119
|
+
feature_id=args.id,
|
|
3120
|
+
collection=args.collection,
|
|
3121
|
+
)
|
|
3122
|
+
except ValidationError as e:
|
|
3123
|
+
display_validation_error(e)
|
|
3124
|
+
sys.exit(1)
|
|
2806
3125
|
|
|
2807
3126
|
sdk = SDK(directory=args.graph_dir, agent=args.agent)
|
|
2808
3127
|
|
|
2809
3128
|
# Only FeatureCollection has set_primary currently
|
|
2810
|
-
if
|
|
2811
|
-
node = sdk.features.set_primary(
|
|
3129
|
+
if input_data.collection == "features":
|
|
3130
|
+
node = sdk.features.set_primary(input_data.feature_id)
|
|
2812
3131
|
else:
|
|
2813
3132
|
# Fallback to direct session manager for other collections
|
|
2814
3133
|
node = sdk.session_manager.set_primary_feature(
|
|
2815
|
-
|
|
3134
|
+
input_data.feature_id, collection=input_data.collection, agent=args.agent
|
|
2816
3135
|
)
|
|
2817
3136
|
|
|
2818
3137
|
if node is None:
|
|
2819
3138
|
print(
|
|
2820
|
-
f"Error: Feature '{
|
|
3139
|
+
f"Error: Feature '{input_data.feature_id}' not found in {input_data.collection}.",
|
|
2821
3140
|
file=sys.stderr,
|
|
2822
3141
|
)
|
|
2823
3142
|
sys.exit(1)
|
|
@@ -2835,26 +3154,40 @@ def cmd_feature_claim(args: argparse.Namespace) -> None:
|
|
|
2835
3154
|
"""Claim a feature."""
|
|
2836
3155
|
import json
|
|
2837
3156
|
|
|
3157
|
+
from pydantic import ValidationError
|
|
3158
|
+
|
|
3159
|
+
from htmlgraph.pydantic_models import FeatureClaimInput
|
|
2838
3160
|
from htmlgraph.sdk import SDK
|
|
3161
|
+
from htmlgraph.validation import display_validation_error
|
|
3162
|
+
|
|
3163
|
+
try:
|
|
3164
|
+
input_data = FeatureClaimInput(
|
|
3165
|
+
feature_id=args.id,
|
|
3166
|
+
collection=args.collection,
|
|
3167
|
+
)
|
|
3168
|
+
except ValidationError as e:
|
|
3169
|
+
display_validation_error(e)
|
|
3170
|
+
sys.exit(1)
|
|
2839
3171
|
|
|
2840
3172
|
sdk = SDK(directory=args.graph_dir, agent=args.agent)
|
|
2841
|
-
collection = getattr(sdk,
|
|
3173
|
+
collection = getattr(sdk, input_data.collection, None)
|
|
2842
3174
|
|
|
2843
3175
|
if not collection:
|
|
2844
3176
|
print(
|
|
2845
|
-
f"Error: Collection '{
|
|
3177
|
+
f"Error: Collection '{input_data.collection}' not found in SDK.",
|
|
3178
|
+
file=sys.stderr,
|
|
2846
3179
|
)
|
|
2847
3180
|
sys.exit(1)
|
|
2848
3181
|
|
|
2849
3182
|
try:
|
|
2850
|
-
node = collection.claim(
|
|
3183
|
+
node = collection.claim(input_data.feature_id)
|
|
2851
3184
|
except ValueError as e:
|
|
2852
3185
|
print(f"Error: {e}", file=sys.stderr)
|
|
2853
3186
|
sys.exit(1)
|
|
2854
3187
|
|
|
2855
3188
|
if node is None:
|
|
2856
3189
|
print(
|
|
2857
|
-
f"Error: Feature '{
|
|
3190
|
+
f"Error: Feature '{input_data.feature_id}' not found in {input_data.collection}.",
|
|
2858
3191
|
file=sys.stderr,
|
|
2859
3192
|
)
|
|
2860
3193
|
sys.exit(1)
|
|
@@ -2873,26 +3206,40 @@ def cmd_feature_release(args: argparse.Namespace) -> None:
|
|
|
2873
3206
|
"""Release a feature."""
|
|
2874
3207
|
import json
|
|
2875
3208
|
|
|
3209
|
+
from pydantic import ValidationError
|
|
3210
|
+
|
|
3211
|
+
from htmlgraph.pydantic_models import FeatureReleaseInput
|
|
2876
3212
|
from htmlgraph.sdk import SDK
|
|
3213
|
+
from htmlgraph.validation import display_validation_error
|
|
3214
|
+
|
|
3215
|
+
try:
|
|
3216
|
+
input_data = FeatureReleaseInput(
|
|
3217
|
+
feature_id=args.id,
|
|
3218
|
+
collection=args.collection,
|
|
3219
|
+
)
|
|
3220
|
+
except ValidationError as e:
|
|
3221
|
+
display_validation_error(e)
|
|
3222
|
+
sys.exit(1)
|
|
2877
3223
|
|
|
2878
3224
|
sdk = SDK(directory=args.graph_dir, agent=args.agent)
|
|
2879
|
-
collection = getattr(sdk,
|
|
3225
|
+
collection = getattr(sdk, input_data.collection, None)
|
|
2880
3226
|
|
|
2881
3227
|
if not collection:
|
|
2882
3228
|
print(
|
|
2883
|
-
f"Error: Collection '{
|
|
3229
|
+
f"Error: Collection '{input_data.collection}' not found in SDK.",
|
|
3230
|
+
file=sys.stderr,
|
|
2884
3231
|
)
|
|
2885
3232
|
sys.exit(1)
|
|
2886
3233
|
|
|
2887
3234
|
try:
|
|
2888
|
-
node = collection.release(
|
|
3235
|
+
node = collection.release(input_data.feature_id)
|
|
2889
3236
|
except ValueError as e:
|
|
2890
3237
|
print(f"Error: {e}", file=sys.stderr)
|
|
2891
3238
|
sys.exit(1)
|
|
2892
3239
|
|
|
2893
3240
|
if node is None:
|
|
2894
3241
|
print(
|
|
2895
|
-
f"Error: Feature '{
|
|
3242
|
+
f"Error: Feature '{input_data.feature_id}' not found in {input_data.collection}.",
|
|
2896
3243
|
file=sys.stderr,
|
|
2897
3244
|
)
|
|
2898
3245
|
sys.exit(1)
|
|
@@ -3193,11 +3540,10 @@ def cmd_cigs_reset_violations(args: argparse.Namespace) -> None:
|
|
|
3193
3540
|
|
|
3194
3541
|
# Confirm reset
|
|
3195
3542
|
if not args.yes:
|
|
3196
|
-
print(f"Current violations: {summary.total_violations}")
|
|
3197
|
-
print(f"Total waste: {summary.total_waste_tokens} tokens")
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
print("Reset cancelled")
|
|
3543
|
+
console.print(f"Current violations: {summary.total_violations}")
|
|
3544
|
+
console.print(f"Total waste: {summary.total_waste_tokens} tokens")
|
|
3545
|
+
if not Confirm.ask("\nReset violations for current session?", default=False):
|
|
3546
|
+
console.print("Reset cancelled")
|
|
3201
3547
|
return
|
|
3202
3548
|
|
|
3203
3549
|
# Clear violations file
|
|
@@ -3208,6 +3554,552 @@ def cmd_cigs_reset_violations(args: argparse.Namespace) -> None:
|
|
|
3208
3554
|
print("Starting fresh for this session")
|
|
3209
3555
|
|
|
3210
3556
|
|
|
3557
|
+
def cmd_cigs_cost_dashboard(args: argparse.Namespace) -> None:
|
|
3558
|
+
"""Generate cost attribution dashboard from HtmlGraph events."""
|
|
3559
|
+
import webbrowser
|
|
3560
|
+
from pathlib import Path
|
|
3561
|
+
|
|
3562
|
+
from htmlgraph.cigs.cost import CostCalculator
|
|
3563
|
+
from htmlgraph.operations.events import query_events
|
|
3564
|
+
|
|
3565
|
+
graph_dir = Path(args.graph_dir or ".htmlgraph")
|
|
3566
|
+
|
|
3567
|
+
# Parse options
|
|
3568
|
+
save = getattr(args, "save", False)
|
|
3569
|
+
open_browser = getattr(args, "open", False)
|
|
3570
|
+
output_json = getattr(args, "json", False)
|
|
3571
|
+
output_path = getattr(args, "output", None)
|
|
3572
|
+
|
|
3573
|
+
# Display progress
|
|
3574
|
+
with console.status("[blue]Analyzing HtmlGraph events...[/blue]", spinner="dots"):
|
|
3575
|
+
try:
|
|
3576
|
+
# Query all events
|
|
3577
|
+
result = query_events(graph_dir=graph_dir, limit=None)
|
|
3578
|
+
events = result.events if hasattr(result, "events") else []
|
|
3579
|
+
|
|
3580
|
+
if not events:
|
|
3581
|
+
console.print(
|
|
3582
|
+
"[yellow]No events found. Run some work to generate analytics![/yellow]"
|
|
3583
|
+
)
|
|
3584
|
+
return
|
|
3585
|
+
|
|
3586
|
+
# Calculate costs from events
|
|
3587
|
+
cost_calc = CostCalculator()
|
|
3588
|
+
cost_summary = _analyze_event_costs(events, cost_calc)
|
|
3589
|
+
|
|
3590
|
+
except Exception as e:
|
|
3591
|
+
console.print(f"[red]Error analyzing events: {e}[/red]")
|
|
3592
|
+
return
|
|
3593
|
+
|
|
3594
|
+
# Generate output
|
|
3595
|
+
if output_json:
|
|
3596
|
+
_output_cost_json(cost_summary, output_path)
|
|
3597
|
+
else:
|
|
3598
|
+
html_content = _generate_cost_dashboard_html(cost_summary)
|
|
3599
|
+
|
|
3600
|
+
if save or output_path:
|
|
3601
|
+
output_file = (
|
|
3602
|
+
Path(output_path) if output_path else graph_dir / "cost-dashboard.html"
|
|
3603
|
+
)
|
|
3604
|
+
output_file.write_text(html_content)
|
|
3605
|
+
console.print(f"[green]ā Dashboard saved to: {output_file}[/green]")
|
|
3606
|
+
|
|
3607
|
+
if open_browser:
|
|
3608
|
+
webbrowser.open(f"file://{output_file.absolute()}")
|
|
3609
|
+
console.print("[blue]Opening dashboard in browser...[/blue]")
|
|
3610
|
+
else:
|
|
3611
|
+
# Display summary to console
|
|
3612
|
+
_display_cost_summary(cost_summary)
|
|
3613
|
+
|
|
3614
|
+
# Print recommendations
|
|
3615
|
+
_print_cost_recommendations(cost_summary)
|
|
3616
|
+
|
|
3617
|
+
|
|
3618
|
+
def _analyze_event_costs(events: list[dict], cost_calc: object) -> dict:
|
|
3619
|
+
"""Analyze events and calculate cost attribution."""
|
|
3620
|
+
cost_summary: dict[str, Any] = {
|
|
3621
|
+
"total_cost_tokens": 0,
|
|
3622
|
+
"total_events": len(events),
|
|
3623
|
+
"tool_costs": {},
|
|
3624
|
+
"session_costs": {},
|
|
3625
|
+
"delegation_count": 0,
|
|
3626
|
+
"direct_execution_count": 0,
|
|
3627
|
+
"cost_by_category": {},
|
|
3628
|
+
}
|
|
3629
|
+
|
|
3630
|
+
for event in events:
|
|
3631
|
+
try:
|
|
3632
|
+
tool = event.get("tool", "unknown")
|
|
3633
|
+
session_id = event.get("session_id", "unknown")
|
|
3634
|
+
cost = (
|
|
3635
|
+
event.get("predicted_tokens", 0)
|
|
3636
|
+
or event.get("actual_tokens", 0)
|
|
3637
|
+
or 2000
|
|
3638
|
+
)
|
|
3639
|
+
|
|
3640
|
+
# Track by tool
|
|
3641
|
+
if tool not in cost_summary["tool_costs"]:
|
|
3642
|
+
cost_summary["tool_costs"][tool] = {"count": 0, "total_tokens": 0}
|
|
3643
|
+
cost_summary["tool_costs"][tool]["count"] += 1
|
|
3644
|
+
cost_summary["tool_costs"][tool]["total_tokens"] += cost
|
|
3645
|
+
|
|
3646
|
+
# Track by session
|
|
3647
|
+
if session_id not in cost_summary["session_costs"]:
|
|
3648
|
+
cost_summary["session_costs"][session_id] = {
|
|
3649
|
+
"count": 0,
|
|
3650
|
+
"total_tokens": 0,
|
|
3651
|
+
}
|
|
3652
|
+
cost_summary["session_costs"][session_id]["count"] += 1
|
|
3653
|
+
cost_summary["session_costs"][session_id]["total_tokens"] += cost
|
|
3654
|
+
|
|
3655
|
+
# Track delegation vs direct
|
|
3656
|
+
if tool in ["Task", "spawn_gemini", "spawn_codex", "spawn_copilot"]:
|
|
3657
|
+
cost_summary["delegation_count"] += 1
|
|
3658
|
+
category = "delegation"
|
|
3659
|
+
else:
|
|
3660
|
+
cost_summary["direct_execution_count"] += 1
|
|
3661
|
+
category = "direct"
|
|
3662
|
+
|
|
3663
|
+
if category not in cost_summary["cost_by_category"]:
|
|
3664
|
+
cost_summary["cost_by_category"][category] = {
|
|
3665
|
+
"count": 0,
|
|
3666
|
+
"total_tokens": 0,
|
|
3667
|
+
}
|
|
3668
|
+
cost_summary["cost_by_category"][category]["count"] += 1
|
|
3669
|
+
cost_summary["cost_by_category"][category]["total_tokens"] += cost
|
|
3670
|
+
|
|
3671
|
+
cost_summary["total_cost_tokens"] += cost
|
|
3672
|
+
except Exception:
|
|
3673
|
+
continue
|
|
3674
|
+
|
|
3675
|
+
return cost_summary
|
|
3676
|
+
|
|
3677
|
+
|
|
3678
|
+
def _generate_cost_dashboard_html(cost_summary: dict) -> str:
|
|
3679
|
+
"""Generate HTML dashboard for cost attribution."""
|
|
3680
|
+
from datetime import datetime
|
|
3681
|
+
|
|
3682
|
+
# Calculate metrics
|
|
3683
|
+
total_cost = cost_summary["total_cost_tokens"]
|
|
3684
|
+
total_events = cost_summary["total_events"]
|
|
3685
|
+
avg_cost = total_cost / total_events if total_events > 0 else 0
|
|
3686
|
+
delegation_pct = (
|
|
3687
|
+
cost_summary["delegation_count"] / total_events * 100 if total_events > 0 else 0
|
|
3688
|
+
)
|
|
3689
|
+
|
|
3690
|
+
# Estimate cost in dollars (assuming $0.001 per 1K tokens for simplicity)
|
|
3691
|
+
cost_usd = total_cost / 1_000_000 * 5 # Rough estimate
|
|
3692
|
+
|
|
3693
|
+
# Sort tools by cost
|
|
3694
|
+
sorted_tools = sorted(
|
|
3695
|
+
cost_summary["tool_costs"].items(),
|
|
3696
|
+
key=lambda x: x[1]["total_tokens"],
|
|
3697
|
+
reverse=True,
|
|
3698
|
+
)
|
|
3699
|
+
|
|
3700
|
+
# Sort sessions by cost
|
|
3701
|
+
sorted_sessions = sorted(
|
|
3702
|
+
cost_summary["session_costs"].items(),
|
|
3703
|
+
key=lambda x: x[1]["total_tokens"],
|
|
3704
|
+
reverse=True,
|
|
3705
|
+
)
|
|
3706
|
+
|
|
3707
|
+
# Build tool cost rows
|
|
3708
|
+
tool_rows = "".join(
|
|
3709
|
+
f"""
|
|
3710
|
+
<tr>
|
|
3711
|
+
<td class="cell">{tool}</td>
|
|
3712
|
+
<td class="cell number">{data["count"]}</td>
|
|
3713
|
+
<td class="cell number">{data["total_tokens"]:,}</td>
|
|
3714
|
+
<td class="cell number">{data["total_tokens"] / total_cost * 100:.1f}%</td>
|
|
3715
|
+
</tr>
|
|
3716
|
+
"""
|
|
3717
|
+
for tool, data in sorted_tools[:20]
|
|
3718
|
+
)
|
|
3719
|
+
|
|
3720
|
+
# Build session cost rows
|
|
3721
|
+
session_rows = "".join(
|
|
3722
|
+
f"""
|
|
3723
|
+
<tr>
|
|
3724
|
+
<td class="cell">{session[:12]}...</td>
|
|
3725
|
+
<td class="cell number">{data["count"]}</td>
|
|
3726
|
+
<td class="cell number">{data["total_tokens"]:,}</td>
|
|
3727
|
+
<td class="cell number">{data["total_tokens"] / total_cost * 100:.1f}%</td>
|
|
3728
|
+
</tr>
|
|
3729
|
+
"""
|
|
3730
|
+
for session, data in sorted_sessions[:20]
|
|
3731
|
+
)
|
|
3732
|
+
|
|
3733
|
+
html = f"""<!DOCTYPE html>
|
|
3734
|
+
<html lang="en">
|
|
3735
|
+
<head>
|
|
3736
|
+
<meta charset="UTF-8">
|
|
3737
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
3738
|
+
<title>HtmlGraph Cost Dashboard</title>
|
|
3739
|
+
<style>
|
|
3740
|
+
* {{
|
|
3741
|
+
margin: 0;
|
|
3742
|
+
padding: 0;
|
|
3743
|
+
box-sizing: border-box;
|
|
3744
|
+
}}
|
|
3745
|
+
|
|
3746
|
+
body {{
|
|
3747
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
3748
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
3749
|
+
min-height: 100vh;
|
|
3750
|
+
padding: 40px 20px;
|
|
3751
|
+
}}
|
|
3752
|
+
|
|
3753
|
+
.container {{
|
|
3754
|
+
max-width: 1400px;
|
|
3755
|
+
margin: 0 auto;
|
|
3756
|
+
}}
|
|
3757
|
+
|
|
3758
|
+
header {{
|
|
3759
|
+
background: white;
|
|
3760
|
+
border-radius: 12px;
|
|
3761
|
+
padding: 30px;
|
|
3762
|
+
margin-bottom: 30px;
|
|
3763
|
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
3764
|
+
}}
|
|
3765
|
+
|
|
3766
|
+
h1 {{
|
|
3767
|
+
color: #667eea;
|
|
3768
|
+
margin-bottom: 10px;
|
|
3769
|
+
font-size: 28px;
|
|
3770
|
+
}}
|
|
3771
|
+
|
|
3772
|
+
.timestamp {{
|
|
3773
|
+
color: #999;
|
|
3774
|
+
font-size: 12px;
|
|
3775
|
+
}}
|
|
3776
|
+
|
|
3777
|
+
.metrics {{
|
|
3778
|
+
display: grid;
|
|
3779
|
+
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
3780
|
+
gap: 20px;
|
|
3781
|
+
margin-top: 20px;
|
|
3782
|
+
}}
|
|
3783
|
+
|
|
3784
|
+
.metric {{
|
|
3785
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
3786
|
+
color: white;
|
|
3787
|
+
padding: 20px;
|
|
3788
|
+
border-radius: 8px;
|
|
3789
|
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
3790
|
+
}}
|
|
3791
|
+
|
|
3792
|
+
.metric-label {{
|
|
3793
|
+
font-size: 12px;
|
|
3794
|
+
opacity: 0.9;
|
|
3795
|
+
margin-bottom: 8px;
|
|
3796
|
+
text-transform: uppercase;
|
|
3797
|
+
letter-spacing: 1px;
|
|
3798
|
+
}}
|
|
3799
|
+
|
|
3800
|
+
.metric-value {{
|
|
3801
|
+
font-size: 32px;
|
|
3802
|
+
font-weight: bold;
|
|
3803
|
+
}}
|
|
3804
|
+
|
|
3805
|
+
.metric-unit {{
|
|
3806
|
+
font-size: 14px;
|
|
3807
|
+
opacity: 0.8;
|
|
3808
|
+
margin-left: 8px;
|
|
3809
|
+
}}
|
|
3810
|
+
|
|
3811
|
+
.metric.warning {{
|
|
3812
|
+
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
|
3813
|
+
}}
|
|
3814
|
+
|
|
3815
|
+
.metric.success {{
|
|
3816
|
+
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
|
3817
|
+
}}
|
|
3818
|
+
|
|
3819
|
+
section {{
|
|
3820
|
+
background: white;
|
|
3821
|
+
border-radius: 12px;
|
|
3822
|
+
padding: 30px;
|
|
3823
|
+
margin-bottom: 30px;
|
|
3824
|
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
3825
|
+
}}
|
|
3826
|
+
|
|
3827
|
+
h2 {{
|
|
3828
|
+
color: #333;
|
|
3829
|
+
margin-bottom: 20px;
|
|
3830
|
+
font-size: 20px;
|
|
3831
|
+
border-bottom: 2px solid #667eea;
|
|
3832
|
+
padding-bottom: 10px;
|
|
3833
|
+
}}
|
|
3834
|
+
|
|
3835
|
+
table {{
|
|
3836
|
+
width: 100%;
|
|
3837
|
+
border-collapse: collapse;
|
|
3838
|
+
}}
|
|
3839
|
+
|
|
3840
|
+
th {{
|
|
3841
|
+
background: #f5f5f5;
|
|
3842
|
+
padding: 12px;
|
|
3843
|
+
text-align: left;
|
|
3844
|
+
font-weight: 600;
|
|
3845
|
+
color: #333;
|
|
3846
|
+
border-bottom: 2px solid #ddd;
|
|
3847
|
+
}}
|
|
3848
|
+
|
|
3849
|
+
td {{
|
|
3850
|
+
padding: 12px;
|
|
3851
|
+
border-bottom: 1px solid #eee;
|
|
3852
|
+
}}
|
|
3853
|
+
|
|
3854
|
+
td.cell {{
|
|
3855
|
+
color: #333;
|
|
3856
|
+
}}
|
|
3857
|
+
|
|
3858
|
+
td.number {{
|
|
3859
|
+
text-align: right;
|
|
3860
|
+
font-family: 'Monaco', 'Courier New', monospace;
|
|
3861
|
+
color: #667eea;
|
|
3862
|
+
font-weight: 500;
|
|
3863
|
+
}}
|
|
3864
|
+
|
|
3865
|
+
tr:hover {{
|
|
3866
|
+
background: #f9f9f9;
|
|
3867
|
+
}}
|
|
3868
|
+
|
|
3869
|
+
.insights {{
|
|
3870
|
+
display: grid;
|
|
3871
|
+
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
3872
|
+
gap: 20px;
|
|
3873
|
+
margin-top: 20px;
|
|
3874
|
+
}}
|
|
3875
|
+
|
|
3876
|
+
.insight {{
|
|
3877
|
+
background: #f0f4ff;
|
|
3878
|
+
border-left: 4px solid #667eea;
|
|
3879
|
+
padding: 16px;
|
|
3880
|
+
border-radius: 4px;
|
|
3881
|
+
}}
|
|
3882
|
+
|
|
3883
|
+
.insight-title {{
|
|
3884
|
+
font-weight: 600;
|
|
3885
|
+
color: #333;
|
|
3886
|
+
margin-bottom: 8px;
|
|
3887
|
+
}}
|
|
3888
|
+
|
|
3889
|
+
.insight-text {{
|
|
3890
|
+
color: #666;
|
|
3891
|
+
font-size: 14px;
|
|
3892
|
+
line-height: 1.6;
|
|
3893
|
+
}}
|
|
3894
|
+
|
|
3895
|
+
.footer {{
|
|
3896
|
+
text-align: center;
|
|
3897
|
+
color: #999;
|
|
3898
|
+
font-size: 12px;
|
|
3899
|
+
margin-top: 40px;
|
|
3900
|
+
}}
|
|
3901
|
+
</style>
|
|
3902
|
+
</head>
|
|
3903
|
+
<body>
|
|
3904
|
+
<div class="container">
|
|
3905
|
+
<header>
|
|
3906
|
+
<h1>š° HtmlGraph Cost Dashboard</h1>
|
|
3907
|
+
<p class="timestamp">Generated: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}</p>
|
|
3908
|
+
<div class="metrics">
|
|
3909
|
+
<div class="metric">
|
|
3910
|
+
<div class="metric-label">Total Cost</div>
|
|
3911
|
+
<div class="metric-value">{total_cost:,}<span class="metric-unit">tokens</span></div>
|
|
3912
|
+
</div>
|
|
3913
|
+
<div class="metric success">
|
|
3914
|
+
<div class="metric-label">Estimated USD</div>
|
|
3915
|
+
<div class="metric-value">${cost_usd:.2f}</div>
|
|
3916
|
+
</div>
|
|
3917
|
+
<div class="metric">
|
|
3918
|
+
<div class="metric-label">Average Cost</div>
|
|
3919
|
+
<div class="metric-value">{avg_cost:,.0f}<span class="metric-unit">tokens</span></div>
|
|
3920
|
+
</div>
|
|
3921
|
+
<div class="metric success">
|
|
3922
|
+
<div class="metric-label">Delegation Rate</div>
|
|
3923
|
+
<div class="metric-value">{delegation_pct:.1f}%</div>
|
|
3924
|
+
</div>
|
|
3925
|
+
</div>
|
|
3926
|
+
</header>
|
|
3927
|
+
|
|
3928
|
+
<section>
|
|
3929
|
+
<h2>š Cost by Tool</h2>
|
|
3930
|
+
<table>
|
|
3931
|
+
<thead>
|
|
3932
|
+
<tr>
|
|
3933
|
+
<th>Tool</th>
|
|
3934
|
+
<th>Count</th>
|
|
3935
|
+
<th>Total Tokens</th>
|
|
3936
|
+
<th>% of Total</th>
|
|
3937
|
+
</tr>
|
|
3938
|
+
</thead>
|
|
3939
|
+
<tbody>
|
|
3940
|
+
{tool_rows}
|
|
3941
|
+
</tbody>
|
|
3942
|
+
</table>
|
|
3943
|
+
</section>
|
|
3944
|
+
|
|
3945
|
+
<section>
|
|
3946
|
+
<h2>š Cost by Session</h2>
|
|
3947
|
+
<table>
|
|
3948
|
+
<thead>
|
|
3949
|
+
<tr>
|
|
3950
|
+
<th>Session ID</th>
|
|
3951
|
+
<th>Count</th>
|
|
3952
|
+
<th>Total Tokens</th>
|
|
3953
|
+
<th>% of Total</th>
|
|
3954
|
+
</tr>
|
|
3955
|
+
</thead>
|
|
3956
|
+
<tbody>
|
|
3957
|
+
{session_rows}
|
|
3958
|
+
</tbody>
|
|
3959
|
+
</table>
|
|
3960
|
+
</section>
|
|
3961
|
+
|
|
3962
|
+
<section>
|
|
3963
|
+
<h2>š” Insights & Recommendations</h2>
|
|
3964
|
+
<div class="insights">
|
|
3965
|
+
<div class="insight">
|
|
3966
|
+
<div class="insight-title">ā Delegation Usage</div>
|
|
3967
|
+
<div class="insight-text">
|
|
3968
|
+
You're delegating {delegation_pct:.1f}% of operations.
|
|
3969
|
+
{"Continue delegation for cost efficiency!" if delegation_pct > 50 else "Consider delegating more operations to reduce costs."}
|
|
3970
|
+
</div>
|
|
3971
|
+
</div>
|
|
3972
|
+
<div class="insight">
|
|
3973
|
+
<div class="insight-title">šÆ Top Cost Driver</div>
|
|
3974
|
+
<div class="insight-text">
|
|
3975
|
+
{sorted_tools[0][0] if sorted_tools else "N/A"} accounts for {sorted_tools[0][1]["total_tokens"] / total_cost * 100:.1f}% of total cost.
|
|
3976
|
+
Review if this tool usage is optimal.
|
|
3977
|
+
</div>
|
|
3978
|
+
</div>
|
|
3979
|
+
<div class="insight">
|
|
3980
|
+
<div class="insight-title">š Parallelization Opportunity</div>
|
|
3981
|
+
<div class="insight-text">
|
|
3982
|
+
Parallel Task() calls can reduce overall execution time by ~40%.
|
|
3983
|
+
Look for independent operations that can run simultaneously.
|
|
3984
|
+
</div>
|
|
3985
|
+
</div>
|
|
3986
|
+
</div>
|
|
3987
|
+
</section>
|
|
3988
|
+
|
|
3989
|
+
<div class="footer">
|
|
3990
|
+
<p>HtmlGraph Cost Attribution Dashboard | Real-time cost tracking and optimization</p>
|
|
3991
|
+
</div>
|
|
3992
|
+
</div>
|
|
3993
|
+
</body>
|
|
3994
|
+
</html>"""
|
|
3995
|
+
|
|
3996
|
+
return html
|
|
3997
|
+
|
|
3998
|
+
|
|
3999
|
+
def _output_cost_json(cost_summary: dict, output_path: str | None) -> None:
|
|
4000
|
+
"""Output cost data as JSON."""
|
|
4001
|
+
import json
|
|
4002
|
+
|
|
4003
|
+
output_file = Path(output_path) if output_path else Path("cost-summary.json")
|
|
4004
|
+
output_file.write_text(json.dumps(cost_summary, indent=2))
|
|
4005
|
+
console.print(f"[green]ā JSON output saved to: {output_file}[/green]")
|
|
4006
|
+
|
|
4007
|
+
|
|
4008
|
+
def _display_cost_summary(cost_summary: dict) -> None:
|
|
4009
|
+
"""Display cost summary in console."""
|
|
4010
|
+
console.print("\n[bold cyan]Cost Dashboard Summary[/bold cyan]\n")
|
|
4011
|
+
|
|
4012
|
+
# Create summary table
|
|
4013
|
+
summary_table = Table(
|
|
4014
|
+
show_header=True, header_style="bold magenta", box=box.ROUNDED
|
|
4015
|
+
)
|
|
4016
|
+
summary_table.add_column("Metric", style="cyan")
|
|
4017
|
+
summary_table.add_column("Value", style="green")
|
|
4018
|
+
|
|
4019
|
+
total_tokens = cost_summary["total_cost_tokens"]
|
|
4020
|
+
total_events = cost_summary["total_events"]
|
|
4021
|
+
avg_tokens = total_tokens / total_events if total_events > 0 else 0
|
|
4022
|
+
delegation_pct = (
|
|
4023
|
+
cost_summary["delegation_count"] / total_events * 100 if total_events > 0 else 0
|
|
4024
|
+
)
|
|
4025
|
+
cost_usd = total_tokens / 1_000_000 * 5
|
|
4026
|
+
|
|
4027
|
+
summary_table.add_row("Total Events", str(total_events))
|
|
4028
|
+
summary_table.add_row("Total Cost", f"{total_tokens:,} tokens")
|
|
4029
|
+
summary_table.add_row("Average Cost", f"{avg_tokens:,.0f} tokens/event")
|
|
4030
|
+
summary_table.add_row("Estimated USD", f"${cost_usd:.2f}")
|
|
4031
|
+
summary_table.add_row("Delegation Count", str(cost_summary["delegation_count"]))
|
|
4032
|
+
summary_table.add_row("Delegation Rate", f"{delegation_pct:.1f}%")
|
|
4033
|
+
summary_table.add_row(
|
|
4034
|
+
"Direct Executions", str(cost_summary["direct_execution_count"])
|
|
4035
|
+
)
|
|
4036
|
+
|
|
4037
|
+
console.print(summary_table)
|
|
4038
|
+
|
|
4039
|
+
# Top tools
|
|
4040
|
+
if cost_summary["tool_costs"]:
|
|
4041
|
+
console.print("\n[bold cyan]Top Cost Drivers (by Tool)[/bold cyan]\n")
|
|
4042
|
+
tools_table = Table(
|
|
4043
|
+
show_header=True, header_style="bold magenta", box=box.ROUNDED
|
|
4044
|
+
)
|
|
4045
|
+
tools_table.add_column("Tool", style="cyan")
|
|
4046
|
+
tools_table.add_column("Count", justify="right", style="green")
|
|
4047
|
+
tools_table.add_column("Tokens", justify="right", style="yellow")
|
|
4048
|
+
tools_table.add_column("% Total", justify="right", style="magenta")
|
|
4049
|
+
|
|
4050
|
+
for tool, data in sorted(
|
|
4051
|
+
cost_summary["tool_costs"].items(),
|
|
4052
|
+
key=lambda x: x[1]["total_tokens"],
|
|
4053
|
+
reverse=True,
|
|
4054
|
+
)[:10]:
|
|
4055
|
+
pct = data["total_tokens"] / total_tokens * 100
|
|
4056
|
+
tools_table.add_row(
|
|
4057
|
+
tool, str(data["count"]), f"{data['total_tokens']:,}", f"{pct:.1f}%"
|
|
4058
|
+
)
|
|
4059
|
+
|
|
4060
|
+
console.print(tools_table)
|
|
4061
|
+
|
|
4062
|
+
|
|
4063
|
+
def _print_cost_recommendations(cost_summary: dict) -> None:
|
|
4064
|
+
"""Print recommendations for cost optimization."""
|
|
4065
|
+
console.print("\n[bold cyan]Recommendations[/bold cyan]\n")
|
|
4066
|
+
|
|
4067
|
+
total_events = cost_summary["total_events"]
|
|
4068
|
+
delegation_pct = (
|
|
4069
|
+
cost_summary["delegation_count"] / total_events * 100 if total_events > 0 else 0
|
|
4070
|
+
)
|
|
4071
|
+
|
|
4072
|
+
recommendations = []
|
|
4073
|
+
|
|
4074
|
+
if delegation_pct < 50:
|
|
4075
|
+
recommendations.append(
|
|
4076
|
+
"[yellow]ā Increase delegation usage[/yellow] - Consider using Task() and spawn_* for more operations"
|
|
4077
|
+
)
|
|
4078
|
+
|
|
4079
|
+
if cost_summary["tool_costs"]:
|
|
4080
|
+
top_tool = max(
|
|
4081
|
+
cost_summary["tool_costs"].items(), key=lambda x: x[1]["total_tokens"]
|
|
4082
|
+
)
|
|
4083
|
+
if top_tool[1]["total_tokens"] / cost_summary["total_cost_tokens"] > 0.4:
|
|
4084
|
+
recommendations.append(
|
|
4085
|
+
f"[yellow]ā Review {top_tool[0]} usage[/yellow] - It accounts for {top_tool[1]['total_tokens'] / cost_summary['total_cost_tokens'] * 100:.1f}% of total cost"
|
|
4086
|
+
)
|
|
4087
|
+
|
|
4088
|
+
if total_events > 100:
|
|
4089
|
+
recommendations.append(
|
|
4090
|
+
"[green]ā Good event volume[/green] - Sufficient data for optimization analysis"
|
|
4091
|
+
)
|
|
4092
|
+
|
|
4093
|
+
recommendations.append(
|
|
4094
|
+
"[blue]š” Tip: Use parallel Task() calls to reduce execution time by ~40%[/blue]"
|
|
4095
|
+
)
|
|
4096
|
+
|
|
4097
|
+
for rec in recommendations:
|
|
4098
|
+
console.print(f" {rec}")
|
|
4099
|
+
|
|
4100
|
+
console.print()
|
|
4101
|
+
|
|
4102
|
+
|
|
3211
4103
|
def cmd_publish(args: argparse.Namespace) -> None:
|
|
3212
4104
|
"""Build and publish the package to PyPI (Interoperable)."""
|
|
3213
4105
|
import shutil
|
|
@@ -3228,14 +4120,14 @@ def cmd_publish(args: argparse.Namespace) -> None:
|
|
|
3228
4120
|
shutil.rmtree(dist_dir)
|
|
3229
4121
|
|
|
3230
4122
|
# 2. Build
|
|
3231
|
-
print("Building package with uv...")
|
|
4123
|
+
console.print("Building package with uv...", style="blue")
|
|
3232
4124
|
try:
|
|
3233
4125
|
subprocess.run(["uv", "build"], check=True)
|
|
3234
4126
|
except subprocess.CalledProcessError:
|
|
3235
|
-
print("Error: Build failed."
|
|
4127
|
+
console.print("[red]Error: Build failed.[/red]")
|
|
3236
4128
|
sys.exit(1)
|
|
3237
4129
|
except FileNotFoundError:
|
|
3238
|
-
print("Error: 'uv' command not found."
|
|
4130
|
+
console.print("[red]Error: 'uv' command not found.[/red]")
|
|
3239
4131
|
sys.exit(1)
|
|
3240
4132
|
|
|
3241
4133
|
# 3. Publish
|
|
@@ -3316,39 +4208,61 @@ def cmd_feature_list(args: argparse.Namespace) -> None:
|
|
|
3316
4208
|
else:
|
|
3317
4209
|
if not nodes:
|
|
3318
4210
|
if not args.quiet:
|
|
3319
|
-
print(
|
|
3320
|
-
f"No features found with status '{args.status}'."
|
|
4211
|
+
console.print(
|
|
4212
|
+
f"[yellow]No features found with status '{args.status}'.[/yellow]"
|
|
3321
4213
|
if args.status
|
|
3322
|
-
else "No features found."
|
|
4214
|
+
else "[yellow]No features found.[/yellow]"
|
|
3323
4215
|
)
|
|
3324
4216
|
return
|
|
3325
4217
|
|
|
3326
|
-
#
|
|
4218
|
+
# Create Rich table (skip if quiet)
|
|
3327
4219
|
if not args.quiet:
|
|
3328
|
-
|
|
3329
|
-
|
|
4220
|
+
table = Table(
|
|
4221
|
+
title="Features",
|
|
4222
|
+
show_header=True,
|
|
4223
|
+
header_style="bold magenta",
|
|
4224
|
+
box=box.ROUNDED,
|
|
4225
|
+
)
|
|
4226
|
+
table.add_column("ID", style="cyan", no_wrap=True, width=25)
|
|
4227
|
+
table.add_column("Status", style="green", width=12)
|
|
4228
|
+
table.add_column("Priority", style="yellow", width=10)
|
|
4229
|
+
table.add_column("Title", style="white")
|
|
3330
4230
|
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
4231
|
+
# List features
|
|
4232
|
+
for node in nodes:
|
|
4233
|
+
title = node.title[:35] + "..." if len(node.title) > 38 else node.title
|
|
4234
|
+
table.add_row(node.id, node.status, node.priority or "-", title)
|
|
3335
4235
|
|
|
3336
|
-
|
|
4236
|
+
console.print(table)
|
|
4237
|
+
else:
|
|
4238
|
+
# Quiet mode - simple output without table
|
|
4239
|
+
for node in nodes:
|
|
4240
|
+
print(f"{node.id}\t{node.status}\t{node.priority}\t{node.title}")
|
|
4241
|
+
|
|
4242
|
+
# Verbose output with Rich.Panel
|
|
3337
4243
|
if args.verbose >= 1:
|
|
3338
|
-
|
|
3339
|
-
print(f"Total features: {len(nodes)}")
|
|
3340
|
-
print(f"Graph directory: {args.graph_dir}")
|
|
4244
|
+
details = f"Total features: {len(nodes)}\nGraph directory: {args.graph_dir}"
|
|
3341
4245
|
if args.status:
|
|
3342
|
-
|
|
4246
|
+
details += f"\nFiltered by status: {args.status}"
|
|
4247
|
+
|
|
4248
|
+
console.print(Panel(details, title="Verbose Details", border_style="cyan"))
|
|
3343
4249
|
|
|
3344
4250
|
if args.verbose >= 2:
|
|
3345
|
-
print("\nFeature breakdown by status:")
|
|
3346
4251
|
from collections import Counter
|
|
3347
4252
|
|
|
3348
4253
|
status_counts = Counter(n.status for n in sdk.features.all())
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
4254
|
+
breakdown = "\n".join(
|
|
4255
|
+
[
|
|
4256
|
+
f" {'ā' if status == args.status else ' '} {status}: {count}"
|
|
4257
|
+
for status, count in sorted(status_counts.items())
|
|
4258
|
+
]
|
|
4259
|
+
)
|
|
4260
|
+
|
|
4261
|
+
console.print(
|
|
4262
|
+
Panel(
|
|
4263
|
+
breakdown, title="Feature Breakdown by Status", border_style="blue"
|
|
4264
|
+
)
|
|
4265
|
+
)
|
|
3352
4266
|
|
|
3353
4267
|
|
|
3354
4268
|
# =============================================================================
|
|
@@ -3377,7 +4291,7 @@ def cmd_feature_step_complete(args: argparse.Namespace) -> None:
|
|
|
3377
4291
|
step_indices = sorted(set(step_indices))
|
|
3378
4292
|
|
|
3379
4293
|
if not step_indices:
|
|
3380
|
-
print("Error: No step indices provided"
|
|
4294
|
+
console.print("[red]Error: No step indices provided[/red]")
|
|
3381
4295
|
sys.exit(1)
|
|
3382
4296
|
|
|
3383
4297
|
# Make API requests for each step
|
|
@@ -3463,14 +4377,13 @@ def cmd_feature_delete(args: argparse.Namespace) -> None:
|
|
|
3463
4377
|
|
|
3464
4378
|
# Confirmation prompt (unless --yes flag)
|
|
3465
4379
|
if not args.yes:
|
|
3466
|
-
print(f"Delete {args.collection.rstrip('s')} '{args.id}'?")
|
|
3467
|
-
print(f" Title: {feature.title}")
|
|
3468
|
-
print(f" Status: {feature.status}")
|
|
3469
|
-
print("\
|
|
4380
|
+
console.print(f"Delete {args.collection.rstrip('s')} '{args.id}'?")
|
|
4381
|
+
console.print(f" Title: {feature.title}")
|
|
4382
|
+
console.print(f" Status: {feature.status}")
|
|
4383
|
+
console.print("\n[bold red]This cannot be undone.[/bold red]")
|
|
3470
4384
|
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
print("Cancelled")
|
|
4385
|
+
if not Confirm.ask("Continue?", default=False):
|
|
4386
|
+
console.print("Cancelled")
|
|
3474
4387
|
sys.exit(0)
|
|
3475
4388
|
|
|
3476
4389
|
# Delete
|
|
@@ -3545,12 +4458,23 @@ def cmd_track_list(args: argparse.Namespace) -> None:
|
|
|
3545
4458
|
print(json.dumps({"tracks": track_ids}, indent=2))
|
|
3546
4459
|
else:
|
|
3547
4460
|
if not track_ids:
|
|
3548
|
-
print("No tracks found.")
|
|
3549
|
-
print(
|
|
4461
|
+
console.print("[yellow]No tracks found.[/yellow]")
|
|
4462
|
+
console.print(
|
|
4463
|
+
"\n[dim]Create a track with: htmlgraph track new 'Track Title'[/dim]"
|
|
4464
|
+
)
|
|
3550
4465
|
return
|
|
3551
4466
|
|
|
3552
|
-
|
|
3553
|
-
|
|
4467
|
+
# Create Rich table
|
|
4468
|
+
table = Table(
|
|
4469
|
+
title=f"Tracks in {args.graph_dir}/tracks/",
|
|
4470
|
+
show_header=True,
|
|
4471
|
+
header_style="bold magenta",
|
|
4472
|
+
box=box.ROUNDED,
|
|
4473
|
+
)
|
|
4474
|
+
table.add_column("Track ID", style="cyan", no_wrap=True)
|
|
4475
|
+
table.add_column("Components", style="green")
|
|
4476
|
+
table.add_column("Format", style="blue")
|
|
4477
|
+
|
|
3554
4478
|
for track_id in track_ids:
|
|
3555
4479
|
# Check for both consolidated (single file) and directory-based formats
|
|
3556
4480
|
track_file = Path(args.graph_dir) / "tracks" / f"{track_id}.html"
|
|
@@ -3564,12 +4488,12 @@ def cmd_track_list(args: argparse.Namespace) -> None:
|
|
|
3564
4488
|
or 'data-section="requirements"' in content
|
|
3565
4489
|
)
|
|
3566
4490
|
has_plan = 'data-section="plan"' in content
|
|
3567
|
-
|
|
4491
|
+
format_type = "consolidated"
|
|
3568
4492
|
else:
|
|
3569
4493
|
# Directory format
|
|
3570
4494
|
has_spec = (track_dir / "spec.html").exists()
|
|
3571
4495
|
has_plan = (track_dir / "plan.html").exists()
|
|
3572
|
-
|
|
4496
|
+
format_type = "directory"
|
|
3573
4497
|
|
|
3574
4498
|
components = []
|
|
3575
4499
|
if has_spec:
|
|
@@ -3577,8 +4501,11 @@ def cmd_track_list(args: argparse.Namespace) -> None:
|
|
|
3577
4501
|
if has_plan:
|
|
3578
4502
|
components.append("plan")
|
|
3579
4503
|
|
|
3580
|
-
components_str =
|
|
3581
|
-
|
|
4504
|
+
components_str = ", ".join(components) if components else "empty"
|
|
4505
|
+
|
|
4506
|
+
table.add_row(track_id, components_str, format_type)
|
|
4507
|
+
|
|
4508
|
+
console.print(table)
|
|
3582
4509
|
|
|
3583
4510
|
|
|
3584
4511
|
def cmd_track_spec(args: argparse.Namespace) -> None:
|
|
@@ -4221,6 +5148,32 @@ For more help: https://github.com/Shakes-tzd/htmlgraph
|
|
|
4221
5148
|
help="Automatically find an available port if default is occupied",
|
|
4222
5149
|
)
|
|
4223
5150
|
|
|
5151
|
+
# serve-api (FastAPI-based dashboard with real-time observability)
|
|
5152
|
+
serve_api_parser = subparsers.add_parser(
|
|
5153
|
+
"serve-api",
|
|
5154
|
+
help="Start the FastAPI-based observability dashboard (Phase 3)",
|
|
5155
|
+
)
|
|
5156
|
+
serve_api_parser.add_argument(
|
|
5157
|
+
"--port", "-p", type=int, default=8000, help="Port (default: 8000)"
|
|
5158
|
+
)
|
|
5159
|
+
serve_api_parser.add_argument(
|
|
5160
|
+
"--host", default="127.0.0.1", help="Host to bind to (default: 127.0.0.1)"
|
|
5161
|
+
)
|
|
5162
|
+
serve_api_parser.add_argument(
|
|
5163
|
+
"--db", default=None, help="Path to SQLite database file"
|
|
5164
|
+
)
|
|
5165
|
+
serve_api_parser.add_argument(
|
|
5166
|
+
"--auto-port",
|
|
5167
|
+
action="store_true",
|
|
5168
|
+
help="Automatically find an available port if default is occupied",
|
|
5169
|
+
)
|
|
5170
|
+
serve_api_parser.add_argument(
|
|
5171
|
+
"--reload",
|
|
5172
|
+
action="store_true",
|
|
5173
|
+
help="Enable auto-reload on file changes (development mode)",
|
|
5174
|
+
)
|
|
5175
|
+
serve_api_parser.set_defaults(func=cmd_serve_api)
|
|
5176
|
+
|
|
4224
5177
|
# init
|
|
4225
5178
|
init_parser = subparsers.add_parser("init", help="Initialize .htmlgraph directory")
|
|
4226
5179
|
init_parser.add_argument(
|
|
@@ -4486,6 +5439,17 @@ For more help: https://github.com/Shakes-tzd/htmlgraph
|
|
|
4486
5439
|
session_validate.add_argument(
|
|
4487
5440
|
"--format", "-f", choices=["text", "json"], default="text", help="Output format"
|
|
4488
5441
|
)
|
|
5442
|
+
session_validate.set_defaults(func=cmd_session_validate_attribution)
|
|
5443
|
+
|
|
5444
|
+
# session debug
|
|
5445
|
+
session_debug = session_subparsers.add_parser(
|
|
5446
|
+
"debug", help="Show error tracebacks and debug information for a session"
|
|
5447
|
+
)
|
|
5448
|
+
session_debug.add_argument("session_id", help="Session ID to debug")
|
|
5449
|
+
session_debug.add_argument(
|
|
5450
|
+
"--graph-dir", "-g", default=".htmlgraph", help="Graph directory"
|
|
5451
|
+
)
|
|
5452
|
+
session_debug.set_defaults(func=cmd_session_debug)
|
|
4489
5453
|
|
|
4490
5454
|
# activity (legacy: was "track")
|
|
4491
5455
|
activity_parser = subparsers.add_parser(
|
|
@@ -4824,6 +5788,11 @@ For more help: https://github.com/Shakes-tzd/htmlgraph
|
|
|
4824
5788
|
help="Priority",
|
|
4825
5789
|
)
|
|
4826
5790
|
feature_create.add_argument("--steps", nargs="*", help="Implementation steps")
|
|
5791
|
+
feature_create.add_argument(
|
|
5792
|
+
"--track",
|
|
5793
|
+
"-t",
|
|
5794
|
+
help="Track ID to link feature to (required for features collection)",
|
|
5795
|
+
)
|
|
4827
5796
|
feature_create.add_argument(
|
|
4828
5797
|
"--agent",
|
|
4829
5798
|
default=os.environ.get("HTMLGRAPH_AGENT") or "cli",
|
|
@@ -5565,6 +6534,27 @@ For more help: https://github.com/Shakes-tzd/htmlgraph
|
|
|
5565
6534
|
"--graph-dir", "-g", default=".htmlgraph", help="Graph directory"
|
|
5566
6535
|
)
|
|
5567
6536
|
|
|
6537
|
+
# cigs cost dashboard
|
|
6538
|
+
cigs_cost_dashboard = cigs_subparsers.add_parser(
|
|
6539
|
+
"cost-dashboard", help="Generate cost attribution dashboard"
|
|
6540
|
+
)
|
|
6541
|
+
cigs_cost_dashboard.add_argument(
|
|
6542
|
+
"--graph-dir", "-g", default=".htmlgraph", help="Graph directory"
|
|
6543
|
+
)
|
|
6544
|
+
cigs_cost_dashboard.add_argument(
|
|
6545
|
+
"--save",
|
|
6546
|
+
"-s",
|
|
6547
|
+
action="store_true",
|
|
6548
|
+
help="Save to .htmlgraph/cost-dashboard.html",
|
|
6549
|
+
)
|
|
6550
|
+
cigs_cost_dashboard.add_argument(
|
|
6551
|
+
"--open", "-o", action="store_true", help="Open in browser after generation"
|
|
6552
|
+
)
|
|
6553
|
+
cigs_cost_dashboard.add_argument(
|
|
6554
|
+
"--json", action="store_true", help="Output JSON instead of HTML"
|
|
6555
|
+
)
|
|
6556
|
+
cigs_cost_dashboard.add_argument("--output", type=str, help="Custom output path")
|
|
6557
|
+
|
|
5568
6558
|
# install-gemini-extension
|
|
5569
6559
|
subparsers.add_parser(
|
|
5570
6560
|
"install-gemini-extension",
|
|
@@ -5598,6 +6588,8 @@ For more help: https://github.com/Shakes-tzd/htmlgraph
|
|
|
5598
6588
|
|
|
5599
6589
|
if args.command == "serve":
|
|
5600
6590
|
cmd_serve(args)
|
|
6591
|
+
elif args.command == "serve-api":
|
|
6592
|
+
cmd_serve_api(args)
|
|
5601
6593
|
elif args.command == "init":
|
|
5602
6594
|
cmd_init(args)
|
|
5603
6595
|
elif args.command == "install-hooks":
|
|
@@ -5840,6 +6832,8 @@ For more help: https://github.com/Shakes-tzd/htmlgraph
|
|
|
5840
6832
|
cmd_cigs_patterns(args)
|
|
5841
6833
|
elif args.cigs_command == "reset-violations":
|
|
5842
6834
|
cmd_cigs_reset_violations(args)
|
|
6835
|
+
elif args.cigs_command == "cost-dashboard":
|
|
6836
|
+
cmd_cigs_cost_dashboard(args)
|
|
5843
6837
|
else:
|
|
5844
6838
|
cigs_parser.print_help()
|
|
5845
6839
|
sys.exit(1)
|
|
@@ -6016,14 +7010,17 @@ def cmd_archive_create(args: argparse.Namespace) -> None:
|
|
|
6016
7010
|
print(f"Error: Directory not found: {htmlgraph_dir}", file=sys.stderr)
|
|
6017
7011
|
sys.exit(1)
|
|
6018
7012
|
|
|
6019
|
-
manager =
|
|
7013
|
+
with console.status("[blue]Initializing archive manager...", spinner="dots"):
|
|
7014
|
+
manager = ArchiveManager(htmlgraph_dir)
|
|
6020
7015
|
|
|
6021
|
-
# Run archive operation
|
|
6022
|
-
|
|
6023
|
-
|
|
6024
|
-
|
|
6025
|
-
|
|
6026
|
-
|
|
7016
|
+
# Run archive operation with status spinner
|
|
7017
|
+
operation = "Previewing" if args.dry_run else "Archiving"
|
|
7018
|
+
with console.status(f"[blue]{operation} entities...", spinner="dots"):
|
|
7019
|
+
result = manager.archive_entities(
|
|
7020
|
+
older_than_days=args.older_than,
|
|
7021
|
+
period=args.period,
|
|
7022
|
+
dry_run=args.dry_run,
|
|
7023
|
+
)
|
|
6027
7024
|
|
|
6028
7025
|
if result["dry_run"]:
|
|
6029
7026
|
print("\nš DRY RUN - Preview (no changes made)\n")
|
|
@@ -6055,26 +7052,55 @@ def cmd_archive_search(args: argparse.Namespace) -> None:
|
|
|
6055
7052
|
print(f"Error: Directory not found: {htmlgraph_dir}", file=sys.stderr)
|
|
6056
7053
|
sys.exit(1)
|
|
6057
7054
|
|
|
6058
|
-
manager =
|
|
7055
|
+
with console.status("[blue]Initializing archive manager...", spinner="dots"):
|
|
7056
|
+
manager = ArchiveManager(htmlgraph_dir)
|
|
6059
7057
|
|
|
6060
|
-
# Search archives
|
|
6061
|
-
|
|
7058
|
+
# Search archives with status spinner
|
|
7059
|
+
with console.status(
|
|
7060
|
+
f"[blue]Searching archives for '{args.query}'...", spinner="dots"
|
|
7061
|
+
):
|
|
7062
|
+
results = manager.search(args.query, limit=args.limit)
|
|
6062
7063
|
|
|
6063
7064
|
if args.format == "json":
|
|
6064
7065
|
print(json.dumps({"query": args.query, "results": results}, indent=2))
|
|
6065
7066
|
else:
|
|
6066
|
-
print(f"\nš Search results for: '{args.query}'\n")
|
|
6067
|
-
print(f"Found {len(results)} result(s):\n")
|
|
6068
|
-
|
|
6069
|
-
for
|
|
6070
|
-
|
|
6071
|
-
|
|
6072
|
-
|
|
6073
|
-
|
|
6074
|
-
|
|
6075
|
-
|
|
6076
|
-
|
|
6077
|
-
|
|
7067
|
+
console.print(f"\nš Search results for: '{args.query}'\n")
|
|
7068
|
+
console.print(f"Found {len(results)} result(s):\n")
|
|
7069
|
+
|
|
7070
|
+
# Use progress bar for large result sets
|
|
7071
|
+
if len(results) > 10:
|
|
7072
|
+
with Progress(
|
|
7073
|
+
SpinnerColumn(),
|
|
7074
|
+
TextColumn("[progress.description]{task.description}"),
|
|
7075
|
+
console=console,
|
|
7076
|
+
transient=True,
|
|
7077
|
+
) as progress:
|
|
7078
|
+
task = progress.add_task("Displaying results...", total=len(results))
|
|
7079
|
+
for i, result in enumerate(results, 1):
|
|
7080
|
+
console.print(
|
|
7081
|
+
f"{i}. {result['entity_id']} ({result['entity_type']})"
|
|
7082
|
+
)
|
|
7083
|
+
console.print(f" Archive: {result['archive_file']}")
|
|
7084
|
+
console.print(f" Status: {result['status']}")
|
|
7085
|
+
console.print(f" Title: {result['title_snippet']}")
|
|
7086
|
+
if result["description_snippet"]:
|
|
7087
|
+
console.print(
|
|
7088
|
+
f" Description: {result['description_snippet']}"
|
|
7089
|
+
)
|
|
7090
|
+
console.print(f" Relevance: {result['rank']:.2f}")
|
|
7091
|
+
console.print()
|
|
7092
|
+
progress.update(task, advance=1)
|
|
7093
|
+
else:
|
|
7094
|
+
# No progress bar for small result sets
|
|
7095
|
+
for i, result in enumerate(results, 1):
|
|
7096
|
+
console.print(f"{i}. {result['entity_id']} ({result['entity_type']})")
|
|
7097
|
+
console.print(f" Archive: {result['archive_file']}")
|
|
7098
|
+
console.print(f" Status: {result['status']}")
|
|
7099
|
+
console.print(f" Title: {result['title_snippet']}")
|
|
7100
|
+
if result["description_snippet"]:
|
|
7101
|
+
console.print(f" Description: {result['description_snippet']}")
|
|
7102
|
+
console.print(f" Relevance: {result['rank']:.2f}")
|
|
7103
|
+
console.print()
|
|
6078
7104
|
|
|
6079
7105
|
manager.close()
|
|
6080
7106
|
|