htmlgraph 0.26.5__py3-none-any.whl → 0.26.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. htmlgraph/.htmlgraph/.session-warning-state.json +1 -1
  2. htmlgraph/__init__.py +1 -1
  3. htmlgraph/api/main.py +50 -10
  4. htmlgraph/api/templates/dashboard-redesign.html +608 -54
  5. htmlgraph/api/templates/partials/activity-feed.html +21 -0
  6. htmlgraph/api/templates/partials/features.html +81 -12
  7. htmlgraph/api/templates/partials/orchestration.html +35 -0
  8. htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
  9. htmlgraph/cli/.htmlgraph/agents.json +72 -0
  10. htmlgraph/cli/__init__.py +42 -0
  11. htmlgraph/cli/__main__.py +6 -0
  12. htmlgraph/cli/analytics.py +939 -0
  13. htmlgraph/cli/base.py +660 -0
  14. htmlgraph/cli/constants.py +206 -0
  15. htmlgraph/cli/core.py +856 -0
  16. htmlgraph/cli/main.py +143 -0
  17. htmlgraph/cli/models.py +462 -0
  18. htmlgraph/cli/templates/__init__.py +1 -0
  19. htmlgraph/cli/templates/cost_dashboard.py +398 -0
  20. htmlgraph/cli/work/__init__.py +159 -0
  21. htmlgraph/cli/work/features.py +567 -0
  22. htmlgraph/cli/work/orchestration.py +675 -0
  23. htmlgraph/cli/work/sessions.py +465 -0
  24. htmlgraph/cli/work/tracks.py +485 -0
  25. htmlgraph/dashboard.html +6414 -634
  26. htmlgraph/db/schema.py +8 -3
  27. htmlgraph/docs/ORCHESTRATION_PATTERNS.md +20 -13
  28. htmlgraph/docs/README.md +2 -3
  29. htmlgraph/hooks/event_tracker.py +355 -26
  30. htmlgraph/hooks/git_commands.py +175 -0
  31. htmlgraph/hooks/orchestrator.py +137 -71
  32. htmlgraph/hooks/orchestrator_reflector.py +23 -0
  33. htmlgraph/hooks/pretooluse.py +29 -6
  34. htmlgraph/hooks/session_handler.py +28 -0
  35. htmlgraph/hooks/session_summary.py +391 -0
  36. htmlgraph/hooks/subagent_detection.py +202 -0
  37. htmlgraph/hooks/subagent_stop.py +71 -12
  38. htmlgraph/hooks/validator.py +192 -79
  39. htmlgraph/operations/__init__.py +18 -0
  40. htmlgraph/operations/initialization.py +596 -0
  41. htmlgraph/operations/initialization.py.backup +228 -0
  42. htmlgraph/orchestration/__init__.py +16 -1
  43. htmlgraph/orchestration/claude_launcher.py +185 -0
  44. htmlgraph/orchestration/command_builder.py +71 -0
  45. htmlgraph/orchestration/headless_spawner.py +72 -1332
  46. htmlgraph/orchestration/plugin_manager.py +136 -0
  47. htmlgraph/orchestration/prompts.py +137 -0
  48. htmlgraph/orchestration/spawners/__init__.py +16 -0
  49. htmlgraph/orchestration/spawners/base.py +194 -0
  50. htmlgraph/orchestration/spawners/claude.py +170 -0
  51. htmlgraph/orchestration/spawners/codex.py +442 -0
  52. htmlgraph/orchestration/spawners/copilot.py +299 -0
  53. htmlgraph/orchestration/spawners/gemini.py +478 -0
  54. htmlgraph/orchestration/subprocess_runner.py +33 -0
  55. htmlgraph/orchestration.md +563 -0
  56. htmlgraph/orchestrator-system-prompt-optimized.txt +620 -55
  57. htmlgraph/orchestrator_config.py +357 -0
  58. htmlgraph/orchestrator_mode.py +45 -12
  59. htmlgraph/transcript.py +16 -4
  60. htmlgraph-0.26.7.data/data/htmlgraph/dashboard.html +6592 -0
  61. {htmlgraph-0.26.5.dist-info → htmlgraph-0.26.7.dist-info}/METADATA +1 -1
  62. {htmlgraph-0.26.5.dist-info → htmlgraph-0.26.7.dist-info}/RECORD +68 -34
  63. {htmlgraph-0.26.5.dist-info → htmlgraph-0.26.7.dist-info}/entry_points.txt +1 -1
  64. htmlgraph/cli.py +0 -7256
  65. htmlgraph-0.26.5.data/data/htmlgraph/dashboard.html +0 -812
  66. {htmlgraph-0.26.5.data → htmlgraph-0.26.7.data}/data/htmlgraph/styles.css +0 -0
  67. {htmlgraph-0.26.5.data → htmlgraph-0.26.7.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
  68. {htmlgraph-0.26.5.data → htmlgraph-0.26.7.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
  69. {htmlgraph-0.26.5.data → htmlgraph-0.26.7.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
  70. {htmlgraph-0.26.5.dist-info → htmlgraph-0.26.7.dist-info}/WHEEL +0 -0
@@ -0,0 +1,939 @@
1
+ """HtmlGraph CLI - Analytics and reporting commands.
2
+
3
+ Commands for analytics and reporting:
4
+ - analytics: Project-wide analytics
5
+ - cigs: Cost dashboard and attribution
6
+ - transcripts: Transcript management
7
+ - sync-docs: Documentation synchronization
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+ import json
14
+ import webbrowser
15
+ from pathlib import Path
16
+ from typing import TYPE_CHECKING
17
+
18
+ from pydantic import BaseModel, Field
19
+ from rich import box
20
+ from rich.console import Console
21
+ from rich.panel import Panel
22
+ from rich.table import Table
23
+
24
+ from htmlgraph.cli.base import BaseCommand, CommandError, CommandResult
25
+ from htmlgraph.cli.constants import DEFAULT_GRAPH_DIR
26
+
27
+ if TYPE_CHECKING:
28
+ from argparse import _SubParsersAction
29
+
30
+ console = Console()
31
+
32
+
33
+ # ============================================================================
34
+ # Command Registration
35
+ # ============================================================================
36
+
37
+
38
+ def register_commands(subparsers: _SubParsersAction) -> None:
39
+ """Register analytics and reporting commands with the argument parser.
40
+
41
+ Args:
42
+ subparsers: Subparser action from ArgumentParser.add_subparsers()
43
+ """
44
+ # Analytics command
45
+ analytics_parser = subparsers.add_parser(
46
+ "analytics", help="Project-wide analytics and insights"
47
+ )
48
+ analytics_parser.add_argument(
49
+ "--graph-dir", "-g", default=DEFAULT_GRAPH_DIR, help="Graph directory"
50
+ )
51
+ analytics_parser.add_argument("--session-id", help="Analyze specific session")
52
+ analytics_parser.add_argument(
53
+ "--recent", type=int, metavar="N", help="Analyze recent N sessions"
54
+ )
55
+ analytics_parser.add_argument(
56
+ "--agent", default="cli", help="Agent name for SDK initialization"
57
+ )
58
+ analytics_parser.add_argument(
59
+ "--quiet", "-q", action="store_true", help="Suppress progress indicators"
60
+ )
61
+ analytics_parser.set_defaults(func=AnalyticsCommand.from_args)
62
+
63
+ # CIGS commands
64
+ _register_cigs_commands(subparsers)
65
+
66
+ # Transcript commands
67
+ _register_transcript_commands(subparsers)
68
+
69
+ # Sync docs command
70
+ _register_sync_docs_command(subparsers)
71
+
72
+
73
+ def _register_cigs_commands(subparsers: _SubParsersAction) -> None:
74
+ """Register CIGS (Cost Intelligence & Governance System) commands."""
75
+ cigs_parser = subparsers.add_parser("cigs", help="Cost dashboard and attribution")
76
+ cigs_subparsers = cigs_parser.add_subparsers(
77
+ dest="cigs_command", help="CIGS command"
78
+ )
79
+
80
+ # cigs cost-dashboard
81
+ cost_dashboard = cigs_subparsers.add_parser(
82
+ "cost-dashboard", help="Display cost summary dashboard"
83
+ )
84
+ cost_dashboard.add_argument(
85
+ "--graph-dir", "-g", default=DEFAULT_GRAPH_DIR, help="Graph directory"
86
+ )
87
+ cost_dashboard.add_argument(
88
+ "--save", action="store_true", help="Save to .htmlgraph/cost-dashboard.html"
89
+ )
90
+ cost_dashboard.add_argument(
91
+ "--open", action="store_true", help="Open in browser after generation"
92
+ )
93
+ cost_dashboard.add_argument(
94
+ "--json", action="store_true", help="Output JSON instead of HTML"
95
+ )
96
+ cost_dashboard.add_argument("--output", help="Custom output path")
97
+ cost_dashboard.set_defaults(func=CostDashboardCommand.from_args)
98
+
99
+ # cigs status
100
+ cigs_status = cigs_subparsers.add_parser("status", help="Show CIGS status")
101
+ cigs_status.add_argument(
102
+ "--graph-dir", "-g", default=DEFAULT_GRAPH_DIR, help="Graph directory"
103
+ )
104
+ cigs_status.set_defaults(func=CigsStatusCommand.from_args)
105
+
106
+ # cigs summary
107
+ cigs_summary = cigs_subparsers.add_parser("summary", help="Show cost summary")
108
+ cigs_summary.add_argument(
109
+ "--graph-dir", "-g", default=DEFAULT_GRAPH_DIR, help="Graph directory"
110
+ )
111
+ cigs_summary.add_argument("--session-id", help="Specific session ID")
112
+ cigs_summary.set_defaults(func=CigsSummaryCommand.from_args)
113
+
114
+
115
+ def _register_transcript_commands(subparsers: _SubParsersAction) -> None:
116
+ """Register transcript management commands."""
117
+ transcript_parser = subparsers.add_parser(
118
+ "transcript", help="Transcript management"
119
+ )
120
+ transcript_subparsers = transcript_parser.add_subparsers(
121
+ dest="transcript_command", help="Transcript command"
122
+ )
123
+
124
+ # transcript list
125
+ transcript_list = transcript_subparsers.add_parser("list", help="List transcripts")
126
+ transcript_list.add_argument(
127
+ "--graph-dir", "-g", default=DEFAULT_GRAPH_DIR, help="Graph directory"
128
+ )
129
+ transcript_list.add_argument("--format", choices=["text", "json"], default="text")
130
+ transcript_list.add_argument("--limit", type=int, default=20)
131
+ transcript_list.add_argument("--project", help="Filter by project path")
132
+ transcript_list.set_defaults(func=TranscriptListCommand.from_args)
133
+
134
+ # transcript import
135
+ transcript_import = transcript_subparsers.add_parser(
136
+ "import", help="Import transcript"
137
+ )
138
+ transcript_import.add_argument("session_id", help="Transcript session ID to import")
139
+ transcript_import.add_argument(
140
+ "--graph-dir", "-g", default=DEFAULT_GRAPH_DIR, help="Graph directory"
141
+ )
142
+ transcript_import.add_argument("--to-session", help="Target HtmlGraph session ID")
143
+ transcript_import.add_argument("--agent", default="claude-code", help="Agent name")
144
+ transcript_import.add_argument(
145
+ "--overwrite", action="store_true", help="Overwrite existing events"
146
+ )
147
+ transcript_import.add_argument("--link-feature", help="Link to feature ID")
148
+ transcript_import.add_argument("--format", choices=["text", "json"], default="text")
149
+ transcript_import.set_defaults(func=TranscriptImportCommand.from_args)
150
+
151
+
152
+ def _register_sync_docs_command(subparsers: _SubParsersAction) -> None:
153
+ """Register documentation synchronization command."""
154
+ sync_docs = subparsers.add_parser(
155
+ "sync-docs", help="Synchronize AI agent memory files"
156
+ )
157
+ sync_docs.add_argument(
158
+ "--project-root", help="Project root directory (default: current directory)"
159
+ )
160
+ sync_docs.add_argument(
161
+ "--check", action="store_true", help="Check synchronization status"
162
+ )
163
+ sync_docs.add_argument(
164
+ "--generate",
165
+ choices=["claude", "gemini"],
166
+ help="Generate specific platform file",
167
+ )
168
+ sync_docs.add_argument(
169
+ "--force", action="store_true", help="Force overwrite existing files"
170
+ )
171
+ sync_docs.set_defaults(func=SyncDocsCommand.from_args)
172
+
173
+
174
+ # ============================================================================
175
+ # Pydantic Models for Cost Analytics
176
+ # ============================================================================
177
+
178
+
179
+ class ToolCostData(BaseModel):
180
+ """Cost data for a specific tool."""
181
+
182
+ count: int = Field(ge=0)
183
+ total_tokens: int = Field(ge=0)
184
+
185
+
186
+ class CategoryCostData(BaseModel):
187
+ """Cost data for a category (delegation/direct)."""
188
+
189
+ count: int = Field(ge=0)
190
+ total_tokens: int = Field(ge=0)
191
+
192
+
193
+ class CostSummary(BaseModel):
194
+ """Complete cost analysis summary."""
195
+
196
+ total_cost_tokens: int = Field(ge=0)
197
+ total_events: int = Field(ge=0)
198
+ tool_costs: dict[str, ToolCostData] = Field(default_factory=dict)
199
+ session_costs: dict[str, ToolCostData] = Field(default_factory=dict)
200
+ delegation_count: int = Field(ge=0)
201
+ direct_execution_count: int = Field(ge=0)
202
+ cost_by_category: dict[str, CategoryCostData] = Field(default_factory=dict)
203
+
204
+ @property
205
+ def avg_cost_per_event(self) -> float:
206
+ """Average token cost per event."""
207
+ return (
208
+ self.total_cost_tokens / self.total_events if self.total_events > 0 else 0
209
+ )
210
+
211
+ @property
212
+ def delegation_percentage(self) -> float:
213
+ """Percentage of events that were delegated."""
214
+ return (
215
+ self.delegation_count / self.total_events * 100
216
+ if self.total_events > 0
217
+ else 0
218
+ )
219
+
220
+ @property
221
+ def estimated_cost_usd(self) -> float:
222
+ """Estimated cost in USD (rough approximation)."""
223
+ return self.total_cost_tokens / 1_000_000 * 5
224
+
225
+
226
+ # ============================================================================
227
+ # Command Implementations
228
+ # ============================================================================
229
+
230
+
231
+ class AnalyticsCommand(BaseCommand):
232
+ """Project-wide analytics and insights."""
233
+
234
+ def __init__(
235
+ self, *, session_id: str | None, recent: int | None, agent: str, quiet: bool
236
+ ) -> None:
237
+ super().__init__()
238
+ self.session_id = session_id
239
+ self.recent = recent
240
+ self.agent = agent
241
+ self.quiet = quiet
242
+
243
+ @classmethod
244
+ def from_args(cls, args: argparse.Namespace) -> AnalyticsCommand:
245
+ return cls(
246
+ session_id=getattr(args, "session_id", None),
247
+ recent=getattr(args, "recent", None),
248
+ agent=getattr(args, "agent", "cli"),
249
+ quiet=getattr(args, "quiet", False),
250
+ )
251
+
252
+ def execute(self) -> CommandResult:
253
+ """Execute analytics analysis using analytics/cli.py implementation."""
254
+ from htmlgraph.analytics.cli import cmd_analytics
255
+
256
+ args = argparse.Namespace(
257
+ graph_dir=self.graph_dir,
258
+ session_id=self.session_id,
259
+ recent=self.recent,
260
+ agent=self.agent,
261
+ quiet=self.quiet,
262
+ )
263
+ exit_code = cmd_analytics(args)
264
+ if exit_code != 0:
265
+ raise CommandError("Analytics command failed", exit_code=exit_code)
266
+ return CommandResult(text="Analytics complete")
267
+
268
+
269
+ class CostDashboardCommand(BaseCommand):
270
+ """Display cost summary dashboard."""
271
+
272
+ def __init__(
273
+ self,
274
+ *,
275
+ save: bool,
276
+ open_browser: bool,
277
+ json_output: bool,
278
+ output_path: str | None,
279
+ ) -> None:
280
+ super().__init__()
281
+ self.save = save
282
+ self.open_browser = open_browser
283
+ self.json_output = json_output
284
+ self.output_path = output_path
285
+
286
+ @classmethod
287
+ def from_args(cls, args: argparse.Namespace) -> CostDashboardCommand:
288
+ return cls(
289
+ save=args.save,
290
+ open_browser=getattr(args, "open", False),
291
+ json_output=getattr(args, "json", False),
292
+ output_path=getattr(args, "output", None),
293
+ )
294
+
295
+ def execute(self) -> CommandResult:
296
+ """Generate and display cost dashboard."""
297
+ if not self.graph_dir:
298
+ raise CommandError("Graph directory not specified")
299
+ graph_dir = Path(self.graph_dir)
300
+
301
+ # Get events from database
302
+ with console.status(
303
+ "[blue]Analyzing HtmlGraph events...[/blue]", spinner="dots"
304
+ ):
305
+ try:
306
+ from htmlgraph.operations.events import query_events
307
+
308
+ result = query_events(graph_dir=graph_dir, limit=None)
309
+ events = result.events if hasattr(result, "events") else []
310
+
311
+ if not events:
312
+ console.print(
313
+ "[yellow]No events found. Run some work to generate analytics![/yellow]"
314
+ )
315
+ return CommandResult(text="No events to analyze")
316
+
317
+ # Calculate costs
318
+ cost_summary = self._analyze_event_costs(events)
319
+
320
+ except Exception as e:
321
+ console.print(f"[red]Error analyzing events: {e}[/red]")
322
+ raise CommandError(f"Failed to analyze events: {e}")
323
+
324
+ # Generate output
325
+ if self.json_output:
326
+ self._output_json(cost_summary)
327
+ else:
328
+ if self.save or self.output_path:
329
+ html_file = self._save_html_dashboard(cost_summary, graph_dir)
330
+ if self.open_browser:
331
+ webbrowser.open(f"file://{html_file.absolute()}")
332
+ console.print("[blue]Opening dashboard in browser...[/blue]")
333
+ else:
334
+ self._display_console_summary(cost_summary)
335
+
336
+ # Print recommendations
337
+ self._print_recommendations(cost_summary)
338
+
339
+ return CommandResult(text="Cost dashboard generated")
340
+
341
+ def _analyze_event_costs(self, events: list[dict]) -> CostSummary:
342
+ """Analyze events and calculate cost attribution."""
343
+ summary = CostSummary(
344
+ total_events=len(events),
345
+ total_cost_tokens=0,
346
+ delegation_count=0,
347
+ direct_execution_count=0,
348
+ )
349
+
350
+ for event in events:
351
+ try:
352
+ tool = event.get("tool", "unknown")
353
+ session_id = event.get("session_id", "unknown")
354
+ cost = (
355
+ event.get("predicted_tokens", 0)
356
+ or event.get("actual_tokens", 0)
357
+ or 2000
358
+ )
359
+
360
+ # Track by tool
361
+ if tool not in summary.tool_costs:
362
+ summary.tool_costs[tool] = ToolCostData(count=0, total_tokens=0)
363
+ summary.tool_costs[tool].count += 1
364
+ summary.tool_costs[tool].total_tokens += cost
365
+
366
+ # Track by session
367
+ if session_id not in summary.session_costs:
368
+ summary.session_costs[session_id] = ToolCostData(
369
+ count=0, total_tokens=0
370
+ )
371
+ summary.session_costs[session_id].count += 1
372
+ summary.session_costs[session_id].total_tokens += cost
373
+
374
+ # Track delegation vs direct
375
+ delegation_tools = [
376
+ "Task",
377
+ "spawn_gemini",
378
+ "spawn_codex",
379
+ "spawn_copilot",
380
+ ]
381
+ if tool in delegation_tools:
382
+ summary.delegation_count += 1
383
+ category = "delegation"
384
+ else:
385
+ summary.direct_execution_count += 1
386
+ category = "direct"
387
+
388
+ if category not in summary.cost_by_category:
389
+ summary.cost_by_category[category] = CategoryCostData(
390
+ count=0, total_tokens=0
391
+ )
392
+ summary.cost_by_category[category].count += 1
393
+ summary.cost_by_category[category].total_tokens += cost
394
+
395
+ summary.total_cost_tokens += cost
396
+
397
+ except Exception:
398
+ continue
399
+
400
+ return summary
401
+
402
+ def _output_json(self, summary: CostSummary) -> None:
403
+ """Output cost data as JSON."""
404
+ output_file = (
405
+ Path(self.output_path) if self.output_path else Path("cost-summary.json")
406
+ )
407
+ output_file.write_text(summary.model_dump_json(indent=2))
408
+ console.print(f"[green]✓ JSON output saved to: {output_file}[/green]")
409
+
410
+ def _save_html_dashboard(self, summary: CostSummary, graph_dir: Path) -> Path:
411
+ """Save HTML dashboard to file."""
412
+ from htmlgraph.cli.templates.cost_dashboard import generate_html
413
+
414
+ html_content = generate_html(summary)
415
+ output_file = (
416
+ Path(self.output_path)
417
+ if self.output_path
418
+ else graph_dir / "cost-dashboard.html"
419
+ )
420
+ output_file.write_text(html_content)
421
+ console.print(f"[green]✓ Dashboard saved to: {output_file}[/green]")
422
+ return output_file
423
+
424
+ def _display_console_summary(self, summary: CostSummary) -> None:
425
+ """Display cost summary in console."""
426
+ from htmlgraph.cli.base import TableBuilder
427
+
428
+ console.print("\n[bold cyan]Cost Dashboard Summary[/bold cyan]\n")
429
+
430
+ # Summary table
431
+ summary_builder = TableBuilder.create_list_table(title=None)
432
+ summary_builder.add_column("Metric", style="cyan")
433
+ summary_builder.add_column("Value", style="green")
434
+
435
+ summary_builder.add_row("Total Events", str(summary.total_events))
436
+ summary_builder.add_row("Total Cost", f"{summary.total_cost_tokens:,} tokens")
437
+ summary_builder.add_row(
438
+ "Average Cost", f"{summary.avg_cost_per_event:,.0f} tokens/event"
439
+ )
440
+ summary_builder.add_row("Estimated USD", f"${summary.estimated_cost_usd:.2f}")
441
+ summary_builder.add_row("Delegation Count", str(summary.delegation_count))
442
+ summary_builder.add_row(
443
+ "Delegation Rate", f"{summary.delegation_percentage:.1f}%"
444
+ )
445
+ summary_builder.add_row(
446
+ "Direct Executions", str(summary.direct_execution_count)
447
+ )
448
+
449
+ console.print(summary_builder.table)
450
+
451
+ # Top tools table
452
+ if summary.tool_costs:
453
+ console.print("\n[bold cyan]Top Cost Drivers (by Tool)[/bold cyan]\n")
454
+ tools_builder = TableBuilder.create_list_table(title=None)
455
+ tools_builder.add_column("Tool", style="cyan")
456
+ tools_builder.add_numeric_column("Count", style="green")
457
+ tools_builder.add_numeric_column("Tokens", style="yellow")
458
+ tools_builder.add_numeric_column("% Total", style="magenta")
459
+
460
+ sorted_tools = sorted(
461
+ summary.tool_costs.items(),
462
+ key=lambda x: x[1].total_tokens,
463
+ reverse=True,
464
+ )
465
+ for tool, data in sorted_tools[:10]:
466
+ pct = data.total_tokens / summary.total_cost_tokens * 100
467
+ tools_builder.add_row(
468
+ tool, str(data.count), f"{data.total_tokens:,}", f"{pct:.1f}%"
469
+ )
470
+
471
+ console.print(tools_builder.table)
472
+
473
+ def _print_recommendations(self, summary: CostSummary) -> None:
474
+ """Print cost optimization recommendations."""
475
+ console.print("\n[bold cyan]Recommendations[/bold cyan]\n")
476
+
477
+ recommendations = []
478
+
479
+ if summary.delegation_percentage < 50:
480
+ recommendations.append(
481
+ "[yellow]→ Increase delegation usage[/yellow] - Consider using Task() and spawn_* for more operations"
482
+ )
483
+
484
+ if summary.tool_costs:
485
+ top_tool, top_data = max(
486
+ summary.tool_costs.items(), key=lambda x: x[1].total_tokens
487
+ )
488
+ top_pct = top_data.total_tokens / summary.total_cost_tokens * 100
489
+ if top_pct > 40:
490
+ recommendations.append(
491
+ f"[yellow]→ Review {top_tool} usage[/yellow] - It accounts for {top_pct:.1f}% of total cost"
492
+ )
493
+
494
+ if summary.total_events > 100:
495
+ recommendations.append(
496
+ "[green]✓ Good event volume[/green] - Sufficient data for optimization analysis"
497
+ )
498
+
499
+ recommendations.append(
500
+ "[blue]💡 Tip: Use parallel Task() calls to reduce execution time by ~40%[/blue]"
501
+ )
502
+
503
+ for rec in recommendations:
504
+ console.print(f" {rec}")
505
+
506
+ console.print()
507
+
508
+
509
+ class CigsStatusCommand(BaseCommand):
510
+ """Show CIGS status."""
511
+
512
+ @classmethod
513
+ def from_args(cls, args: argparse.Namespace) -> CigsStatusCommand:
514
+ return cls()
515
+
516
+ def execute(self) -> CommandResult:
517
+ """Show CIGS status."""
518
+ from htmlgraph.cigs.autonomy import AutonomyRecommender
519
+ from htmlgraph.cigs.pattern_storage import PatternStorage
520
+ from htmlgraph.cigs.tracker import ViolationTracker
521
+
522
+ if not self.graph_dir:
523
+ raise CommandError("Graph directory not specified")
524
+ graph_dir = Path(self.graph_dir)
525
+
526
+ # Get violation tracker
527
+ tracker = ViolationTracker(graph_dir)
528
+ summary = tracker.get_session_violations()
529
+
530
+ # Get pattern storage
531
+ pattern_storage = PatternStorage(graph_dir)
532
+ patterns = pattern_storage.get_anti_patterns()
533
+
534
+ # Get autonomy recommendation
535
+ recommender = AutonomyRecommender()
536
+ autonomy = recommender.recommend(summary, patterns)
537
+
538
+ # Display with Rich
539
+ status_table = Table(title="CIGS Status", box=box.ROUNDED)
540
+ status_table.add_column("Metric", style="cyan")
541
+ status_table.add_column("Value", style="green")
542
+
543
+ status_table.add_row("Session", summary.session_id)
544
+ status_table.add_row("Violations", f"{summary.total_violations}/3")
545
+ status_table.add_row("Compliance Rate", f"{summary.compliance_rate:.1%}")
546
+ status_table.add_row("Total Waste", f"{summary.total_waste_tokens} tokens")
547
+ status_table.add_row(
548
+ "Circuit Breaker",
549
+ "🚨 TRIGGERED" if summary.circuit_breaker_triggered else "Not triggered",
550
+ )
551
+
552
+ console.print(status_table)
553
+
554
+ if summary.violations_by_type:
555
+ console.print("\n[bold]Violation Breakdown:[/bold]")
556
+ for vtype, count in summary.violations_by_type.items():
557
+ console.print(f" • {vtype}: {count}")
558
+
559
+ console.print(f"\n[bold]Autonomy Level:[/bold] {autonomy.level.upper()}")
560
+ console.print(
561
+ f"[bold]Messaging Intensity:[/bold] {autonomy.messaging_intensity}"
562
+ )
563
+ console.print(f"[bold]Enforcement Mode:[/bold] {autonomy.enforcement_mode}")
564
+
565
+ if patterns:
566
+ console.print(f"\n[bold]Anti-Patterns Detected:[/bold] {len(patterns)}")
567
+ for pattern in patterns[:3]:
568
+ console.print(f" • {pattern.name} ({pattern.occurrence_count}x)")
569
+
570
+ return CommandResult(text="CIGS status displayed")
571
+
572
+
573
+ class CigsSummaryCommand(BaseCommand):
574
+ """Show cost summary."""
575
+
576
+ def __init__(self, *, session_id: str | None) -> None:
577
+ super().__init__()
578
+ self.session_id = session_id
579
+
580
+ @classmethod
581
+ def from_args(cls, args: argparse.Namespace) -> CigsSummaryCommand:
582
+ return cls(session_id=getattr(args, "session_id", None))
583
+
584
+ def execute(self) -> CommandResult:
585
+ """Show cost summary."""
586
+ from htmlgraph.cigs.tracker import ViolationTracker
587
+
588
+ if not self.graph_dir:
589
+ raise CommandError("Graph directory not specified")
590
+ graph_dir = Path(self.graph_dir)
591
+ tracker = ViolationTracker(graph_dir)
592
+
593
+ # Get session ID
594
+ session_id = self.session_id or tracker._session_id
595
+
596
+ if not session_id:
597
+ console.print(
598
+ "[yellow]⚠️ No active session. Specify --session-id to view past sessions.[/yellow]"
599
+ )
600
+ return CommandResult(text="No active session")
601
+
602
+ summary = tracker.get_session_violations(session_id)
603
+
604
+ # Display summary
605
+ panel = Panel(
606
+ f"[cyan]Session ID:[/cyan] {summary.session_id}\n"
607
+ f"[cyan]Total Violations:[/cyan] {summary.total_violations}\n"
608
+ f"[cyan]Compliance Rate:[/cyan] {summary.compliance_rate:.1%}\n"
609
+ f"[cyan]Total Waste:[/cyan] {summary.total_waste_tokens} tokens\n"
610
+ f"[cyan]Circuit Breaker:[/cyan] {'🚨 TRIGGERED' if summary.circuit_breaker_triggered else 'Not triggered'}",
611
+ title="CIGS Session Summary",
612
+ border_style="cyan",
613
+ )
614
+ console.print(panel)
615
+
616
+ if summary.violations_by_type:
617
+ console.print("\n[bold]Violation Breakdown:[/bold]")
618
+ for vtype, count in summary.violations_by_type.items():
619
+ console.print(f" • {vtype}: {count}")
620
+
621
+ if summary.violations:
622
+ console.print(
623
+ f"\n[bold]Recent Violations ({len(summary.violations)}):[/bold]"
624
+ )
625
+ for v in summary.violations[-5:]:
626
+ console.print(
627
+ f" • {v.tool} - {v.violation_type} - {v.waste_tokens} tokens wasted"
628
+ )
629
+ console.print(f" Should have: {v.should_have_delegated_to}")
630
+
631
+ return CommandResult(text="Cost summary displayed")
632
+
633
+
634
+ class TranscriptListCommand(BaseCommand):
635
+ """List transcripts."""
636
+
637
+ def __init__(self, *, format: str, limit: int, project: str | None) -> None:
638
+ super().__init__()
639
+ self.format = format
640
+ self.limit = limit
641
+ self.project = project
642
+
643
+ @classmethod
644
+ def from_args(cls, args: argparse.Namespace) -> TranscriptListCommand:
645
+ return cls(
646
+ format=getattr(args, "format", "text"),
647
+ limit=getattr(args, "limit", 20),
648
+ project=getattr(args, "project", None),
649
+ )
650
+
651
+ def execute(self) -> CommandResult:
652
+ """List all transcripts."""
653
+ from htmlgraph.transcript import TranscriptReader
654
+
655
+ reader = TranscriptReader()
656
+ sessions = reader.list_sessions(project_path=self.project, limit=self.limit)
657
+
658
+ if not sessions:
659
+ if self.format == "json":
660
+ console.print_json(json.dumps({"sessions": [], "count": 0}))
661
+ else:
662
+ console.print("[yellow]No Claude Code transcripts found.[/yellow]")
663
+ console.print(f"[dim]Looked in: {reader.claude_dir}[/dim]")
664
+ return CommandResult(text="No transcripts found")
665
+
666
+ if self.format == "json":
667
+ data = {
668
+ "sessions": [
669
+ {
670
+ "session_id": s.session_id,
671
+ "path": str(s.path),
672
+ "cwd": s.cwd,
673
+ "git_branch": s.git_branch,
674
+ "started_at": s.started_at.isoformat()
675
+ if s.started_at
676
+ else None,
677
+ "user_messages": s.user_message_count,
678
+ "tool_calls": s.tool_call_count,
679
+ "duration_seconds": s.duration_seconds,
680
+ }
681
+ for s in sessions
682
+ ],
683
+ "count": len(sessions),
684
+ }
685
+ console.print_json(json.dumps(data))
686
+ else:
687
+ # Display with Rich table
688
+ table = Table(
689
+ title=f"Claude Code Transcripts ({len(sessions)} found)",
690
+ box=box.ROUNDED,
691
+ )
692
+ table.add_column("Session ID", style="cyan", no_wrap=False, max_width=20)
693
+ table.add_column("Started", style="dim")
694
+ table.add_column("Duration", justify="right")
695
+ table.add_column("Messages", justify="right")
696
+ table.add_column("Branch", style="blue")
697
+
698
+ for s in sessions:
699
+ started = (
700
+ s.started_at.strftime("%Y-%m-%d %H:%M")
701
+ if s.started_at
702
+ else "unknown"
703
+ )
704
+ duration = (
705
+ f"{int(s.duration_seconds / 60)}m" if s.duration_seconds else "?"
706
+ )
707
+ branch = s.git_branch or "no branch"
708
+
709
+ table.add_row(
710
+ s.session_id[:20] + "...",
711
+ started,
712
+ duration,
713
+ str(s.user_message_count),
714
+ branch,
715
+ )
716
+
717
+ console.print(table)
718
+
719
+ return CommandResult(text=f"Listed {len(sessions)} transcripts")
720
+
721
+
722
+ class TranscriptImportCommand(BaseCommand):
723
+ """Import transcript."""
724
+
725
+ def __init__(
726
+ self,
727
+ *,
728
+ session_id: str,
729
+ to_session: str | None,
730
+ agent: str,
731
+ overwrite: bool,
732
+ link_feature: str | None,
733
+ format: str,
734
+ ) -> None:
735
+ super().__init__()
736
+ self.session_id = session_id
737
+ self.to_session = to_session
738
+ self.agent = agent
739
+ self.overwrite = overwrite
740
+ self.link_feature = link_feature
741
+ self.format = format
742
+
743
+ @classmethod
744
+ def from_args(cls, args: argparse.Namespace) -> TranscriptImportCommand:
745
+ return cls(
746
+ session_id=args.session_id,
747
+ to_session=getattr(args, "to_session", None),
748
+ agent=getattr(args, "agent", "claude-code"),
749
+ overwrite=getattr(args, "overwrite", False),
750
+ link_feature=getattr(args, "link_feature", None),
751
+ format=getattr(args, "format", "text"),
752
+ )
753
+
754
+ def execute(self) -> CommandResult:
755
+ """Import a transcript file."""
756
+ from htmlgraph.session_manager import SessionManager
757
+ from htmlgraph.transcript import TranscriptReader
758
+
759
+ if not self.graph_dir:
760
+ raise CommandError("Graph directory not specified")
761
+
762
+ reader = TranscriptReader()
763
+ manager = SessionManager(self.graph_dir)
764
+
765
+ # Find the transcript
766
+ transcript = reader.read_session(self.session_id)
767
+ if not transcript:
768
+ console.print(f"[red]Error: Transcript not found: {self.session_id}[/red]")
769
+ return CommandResult(text="Transcript not found", exit_code=1)
770
+
771
+ # Find or create HtmlGraph session
772
+ htmlgraph_session_id = self.to_session
773
+ if not htmlgraph_session_id:
774
+ # Check if already linked
775
+ existing = manager.find_session_by_transcript(self.session_id)
776
+ if existing:
777
+ htmlgraph_session_id = existing.id
778
+ console.print(
779
+ f"[blue]Found existing linked session: {htmlgraph_session_id}[/blue]"
780
+ )
781
+ else:
782
+ # Create new session
783
+ new_session = manager.start_session(
784
+ agent=self.agent,
785
+ title=f"Imported: {transcript.session_id[:12]}",
786
+ )
787
+ htmlgraph_session_id = new_session.id
788
+ console.print(
789
+ f"[green]Created new session: {htmlgraph_session_id}[/green]"
790
+ )
791
+
792
+ # Import events
793
+ result = manager.import_transcript_events(
794
+ session_id=htmlgraph_session_id,
795
+ transcript_session=transcript,
796
+ overwrite=self.overwrite,
797
+ )
798
+
799
+ # Link to feature if specified
800
+ if self.link_feature:
801
+ session = manager.get_session(htmlgraph_session_id)
802
+ if session and self.link_feature not in session.worked_on:
803
+ session.worked_on.append(self.link_feature)
804
+ manager.session_converter.save(session)
805
+ result["linked_feature"] = self.link_feature
806
+
807
+ # Display results
808
+ if self.format == "json":
809
+ console.print_json(json.dumps(result))
810
+ else:
811
+ console.print(
812
+ f"[green]✅ Imported transcript {self.session_id[:12]}:[/green]"
813
+ )
814
+ console.print(f" → HtmlGraph session: {htmlgraph_session_id}")
815
+ console.print(f" → Events imported: {result.get('imported', 0)}")
816
+ console.print(f" → Events skipped: {result.get('skipped', 0)}")
817
+ if result.get("linked_feature"):
818
+ console.print(f" → Linked to feature: {result['linked_feature']}")
819
+
820
+ return CommandResult(text=f"Imported transcript: {self.session_id}")
821
+
822
+
823
+ class SyncDocsCommand(BaseCommand):
824
+ """Synchronize AI agent memory files."""
825
+
826
+ def __init__(
827
+ self,
828
+ *,
829
+ project_root: str | None,
830
+ check: bool,
831
+ generate: str | None,
832
+ force: bool,
833
+ ) -> None:
834
+ super().__init__()
835
+ self.project_root = project_root
836
+ self.check = check
837
+ self.generate = generate
838
+ self.force = force
839
+
840
+ @classmethod
841
+ def from_args(cls, args: argparse.Namespace) -> SyncDocsCommand:
842
+ return cls(
843
+ project_root=getattr(args, "project_root", None),
844
+ check=getattr(args, "check", False),
845
+ generate=getattr(args, "generate", None),
846
+ force=getattr(args, "force", False),
847
+ )
848
+
849
+ def execute(self) -> CommandResult:
850
+ """Synchronize AI agent memory files across platforms."""
851
+ import os
852
+
853
+ from htmlgraph.sync_docs import (
854
+ PLATFORM_TEMPLATES,
855
+ check_all_files,
856
+ generate_platform_file,
857
+ sync_all_files,
858
+ )
859
+
860
+ project_root = Path(self.project_root or os.getcwd()).resolve()
861
+
862
+ if self.check:
863
+ # Check mode
864
+ console.print("[blue]🔍 Checking memory files...[/blue]")
865
+ results = check_all_files(project_root)
866
+
867
+ table = Table(title="Memory File Status", box=box.ROUNDED)
868
+ table.add_column("File", style="cyan")
869
+ table.add_column("Status", style="green")
870
+
871
+ all_good = True
872
+ for filename, status in results.items():
873
+ if filename == "AGENTS.md":
874
+ if status:
875
+ table.add_row(filename, "✅ exists")
876
+ else:
877
+ table.add_row(filename, "❌ MISSING (required)")
878
+ all_good = False
879
+ else:
880
+ if status:
881
+ table.add_row(filename, "✅ references AGENTS.md")
882
+ else:
883
+ table.add_row(filename, "⚠️ missing reference")
884
+ all_good = False
885
+
886
+ console.print(table)
887
+
888
+ if all_good:
889
+ console.print(
890
+ "\n[green]✅ All files are properly synchronized![/green]"
891
+ )
892
+ return CommandResult(text="All files synchronized", exit_code=0)
893
+ else:
894
+ console.print("\n[yellow]⚠️ Some files need attention[/yellow]")
895
+ return CommandResult(text="Files need attention", exit_code=1)
896
+
897
+ elif self.generate:
898
+ # Generate mode
899
+ platform = self.generate.lower()
900
+ console.print(
901
+ f"[blue]📝 Generating {platform.upper()} memory file...[/blue]"
902
+ )
903
+
904
+ try:
905
+ content = generate_platform_file(platform, project_root)
906
+ template = PLATFORM_TEMPLATES[platform]
907
+ filepath = project_root / template["filename"]
908
+
909
+ if filepath.exists() and not self.force:
910
+ console.print(
911
+ f"[yellow]⚠️ {filepath.name} already exists. Use --force to overwrite.[/yellow]"
912
+ )
913
+ raise CommandError("File already exists")
914
+
915
+ filepath.write_text(content)
916
+ console.print(f"[green]✅ Created: {filepath}[/green]")
917
+ console.print(
918
+ "\n[dim]The file references AGENTS.md for core documentation.[/dim]"
919
+ )
920
+ return CommandResult(text=f"Generated {platform} file")
921
+
922
+ except ValueError as e:
923
+ console.print(f"[red]❌ Error: {e}[/red]")
924
+ return CommandResult(text=str(e), exit_code=1)
925
+
926
+ else:
927
+ # Sync mode (default)
928
+ console.print("[blue]🔄 Synchronizing memory files...[/blue]")
929
+ changes = sync_all_files(project_root)
930
+
931
+ console.print("\n[bold]Results:[/bold]")
932
+ for change in changes:
933
+ console.print(f" {change}")
934
+
935
+ has_errors = any("⚠️" in c or "❌" in c for c in changes)
936
+ return CommandResult(
937
+ text="Synchronization complete",
938
+ exit_code=1 if has_errors else 0,
939
+ )