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
htmlgraph/cli/core.py ADDED
@@ -0,0 +1,856 @@
1
+ """HtmlGraph CLI - Infrastructure commands.
2
+
3
+ Commands for core infrastructure operations:
4
+ - serve: Start FastAPI server
5
+ - serve-api: Start API dashboard
6
+ - init: Initialize .htmlgraph directory
7
+ - status: Show graph status
8
+ - query: CSS selector query
9
+ - debug: Debug mode
10
+ - install-hooks: Install Git hooks
11
+ - Other utilities
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import argparse
17
+ import sys
18
+ from typing import TYPE_CHECKING
19
+
20
+ from htmlgraph.cli.base import BaseCommand, CommandError, CommandResult
21
+ from htmlgraph.cli.constants import (
22
+ COLLECTIONS,
23
+ DEFAULT_DATABASE_NAME,
24
+ DEFAULT_GRAPH_DIR,
25
+ DEFAULT_SERVER_HOST,
26
+ DEFAULT_SERVER_PORT,
27
+ get_error_message,
28
+ )
29
+
30
+ if TYPE_CHECKING:
31
+ from argparse import _SubParsersAction
32
+
33
+
34
+ def register_commands(subparsers: _SubParsersAction) -> None:
35
+ """Register infrastructure commands with the argument parser.
36
+
37
+ Args:
38
+ subparsers: Subparser action from ArgumentParser.add_subparsers()
39
+ """
40
+ # serve
41
+ serve_parser = subparsers.add_parser("serve", help="Start the HtmlGraph server")
42
+ serve_parser.add_argument(
43
+ "--port",
44
+ "-p",
45
+ type=int,
46
+ default=DEFAULT_SERVER_PORT,
47
+ help="Port (default: 8080)",
48
+ )
49
+ serve_parser.add_argument(
50
+ "--host", default=DEFAULT_SERVER_HOST, help="Host to bind to (default: 0.0.0.0)"
51
+ )
52
+ serve_parser.add_argument(
53
+ "--graph-dir", "-g", default=DEFAULT_GRAPH_DIR, help="Graph directory"
54
+ )
55
+ serve_parser.add_argument(
56
+ "--static-dir", "-s", default=".", help="Static files directory"
57
+ )
58
+ serve_parser.add_argument(
59
+ "--no-watch",
60
+ action="store_true",
61
+ help="Disable file watching (auto-reload disabled)",
62
+ )
63
+ serve_parser.add_argument(
64
+ "--auto-port",
65
+ action="store_true",
66
+ help="Automatically find an available port if default is occupied",
67
+ )
68
+ serve_parser.set_defaults(func=ServeCommand.from_args)
69
+
70
+ # serve-api
71
+ serve_api_parser = subparsers.add_parser(
72
+ "serve-api",
73
+ help="Start the FastAPI-based observability dashboard",
74
+ )
75
+ serve_api_parser.add_argument(
76
+ "--port", "-p", type=int, default=8000, help="Port (default: 8000)"
77
+ )
78
+ serve_api_parser.add_argument(
79
+ "--host", default="127.0.0.1", help="Host to bind to (default: 127.0.0.1)"
80
+ )
81
+ serve_api_parser.add_argument(
82
+ "--db", default=None, help="Path to SQLite database file"
83
+ )
84
+ serve_api_parser.add_argument(
85
+ "--auto-port",
86
+ action="store_true",
87
+ help="Automatically find an available port if default is occupied",
88
+ )
89
+ serve_api_parser.add_argument(
90
+ "--reload",
91
+ action="store_true",
92
+ help="Enable auto-reload on file changes (development mode)",
93
+ )
94
+ serve_api_parser.set_defaults(func=ServeApiCommand.from_args)
95
+
96
+ # init
97
+ init_parser = subparsers.add_parser("init", help="Initialize .htmlgraph directory")
98
+ init_parser.add_argument(
99
+ "dir", nargs="?", default=".", help="Directory to initialize"
100
+ )
101
+ init_parser.add_argument(
102
+ "--install-hooks",
103
+ action="store_true",
104
+ help="Install Git hooks for event logging",
105
+ )
106
+ init_parser.add_argument(
107
+ "--interactive", "-i", action="store_true", help="Interactive setup wizard"
108
+ )
109
+ init_parser.add_argument(
110
+ "--no-index",
111
+ action="store_true",
112
+ help="Do not create the analytics cache (index.sqlite)",
113
+ )
114
+ init_parser.add_argument(
115
+ "--no-update-gitignore",
116
+ action="store_true",
117
+ help="Do not update/create .gitignore for HtmlGraph cache files",
118
+ )
119
+ init_parser.add_argument(
120
+ "--no-events-keep",
121
+ action="store_true",
122
+ help="Do not create .htmlgraph/events/.gitkeep",
123
+ )
124
+ init_parser.set_defaults(func=InitCommand.from_args)
125
+
126
+ # status
127
+ status_parser = subparsers.add_parser("status", help="Show graph status")
128
+ status_parser.add_argument(
129
+ "--graph-dir", "-g", default=DEFAULT_GRAPH_DIR, help="Graph directory"
130
+ )
131
+ status_parser.set_defaults(func=StatusCommand.from_args)
132
+
133
+ # debug
134
+ debug_parser = subparsers.add_parser(
135
+ "debug", help="Show debugging resources and system diagnostics"
136
+ )
137
+ debug_parser.add_argument(
138
+ "--graph-dir", "-g", default=DEFAULT_GRAPH_DIR, help="Graph directory"
139
+ )
140
+ debug_parser.set_defaults(func=DebugCommand.from_args)
141
+
142
+ # query
143
+ query_parser = subparsers.add_parser("query", help="Query nodes with CSS selector")
144
+ query_parser.add_argument(
145
+ "selector", help="CSS selector (e.g. [data-status='todo'])"
146
+ )
147
+ query_parser.add_argument(
148
+ "--graph-dir", "-g", default=DEFAULT_GRAPH_DIR, help="Graph directory"
149
+ )
150
+ query_parser.set_defaults(func=QueryCommand.from_args)
151
+
152
+ # install-hooks
153
+ install_hooks_parser = subparsers.add_parser(
154
+ "install-hooks", help="Install Git hooks for event logging"
155
+ )
156
+ install_hooks_parser.add_argument(
157
+ "--graph-dir", "-g", default=DEFAULT_GRAPH_DIR, help="Graph directory"
158
+ )
159
+ install_hooks_parser.add_argument(
160
+ "--force",
161
+ action="store_true",
162
+ help="Force installation, overwriting existing hooks",
163
+ )
164
+ install_hooks_parser.add_argument(
165
+ "--dry-run",
166
+ action="store_true",
167
+ help="Show what would be installed without making changes",
168
+ )
169
+ install_hooks_parser.set_defaults(func=InstallHooksCommand.from_args)
170
+
171
+
172
+ # ============================================================================
173
+ # Command Implementations
174
+ # ============================================================================
175
+
176
+
177
+ class ServeCommand(BaseCommand):
178
+ """Start the HtmlGraph server."""
179
+
180
+ def __init__(
181
+ self,
182
+ *,
183
+ port: int,
184
+ host: str,
185
+ static_dir: str,
186
+ no_watch: bool,
187
+ auto_port: bool,
188
+ ) -> None:
189
+ super().__init__()
190
+ self.port = port
191
+ self.host = host
192
+ self.static_dir = static_dir
193
+ self.no_watch = no_watch
194
+ self.auto_port = auto_port
195
+
196
+ @classmethod
197
+ def from_args(cls, args: argparse.Namespace) -> ServeCommand:
198
+ return cls(
199
+ port=args.port,
200
+ host=args.host,
201
+ static_dir=args.static_dir,
202
+ no_watch=args.no_watch,
203
+ auto_port=args.auto_port,
204
+ )
205
+
206
+ def execute(self) -> CommandResult:
207
+ """Start the FastAPI server."""
208
+ import asyncio
209
+ from pathlib import Path
210
+
211
+ from rich.console import Console
212
+ from rich.panel import Panel
213
+
214
+ from htmlgraph.operations.fastapi_server import (
215
+ run_fastapi_server,
216
+ start_fastapi_server,
217
+ )
218
+
219
+ console = Console()
220
+
221
+ try:
222
+ # Default to database in graph dir if not specified
223
+ db_path = str(
224
+ Path(self.graph_dir or DEFAULT_GRAPH_DIR) / DEFAULT_DATABASE_NAME
225
+ )
226
+
227
+ result = start_fastapi_server(
228
+ port=self.port,
229
+ host=self.host,
230
+ db_path=db_path,
231
+ auto_port=self.auto_port,
232
+ reload=False, # Not supported for cmd_serve
233
+ )
234
+
235
+ # Display server info using Rich
236
+ console.print()
237
+ console.print(
238
+ Panel.fit(
239
+ f"[bold blue]{result.handle.url}[/bold blue]",
240
+ title="[bold cyan]HtmlGraph Server (FastAPI)[/bold cyan]",
241
+ border_style="cyan",
242
+ )
243
+ )
244
+
245
+ console.print(
246
+ f"[dim]Graph directory:[/dim] {self.graph_dir or DEFAULT_GRAPH_DIR}"
247
+ )
248
+ console.print(f"[dim]Database:[/dim] {result.config_used['db_path']}")
249
+
250
+ # Show warnings if any
251
+ if result.warnings:
252
+ console.print()
253
+ for warning in result.warnings:
254
+ console.print(f"[yellow]⚠️ {warning}[/yellow]")
255
+
256
+ # Show available features
257
+ console.print()
258
+ console.print("[cyan]Features:[/cyan]")
259
+ console.print(" • Real-time agent activity feed (HTMX)")
260
+ console.print(" • Orchestration chains visualization")
261
+ console.print(" • Feature tracker with Kanban view")
262
+ console.print(" • Session metrics & performance analytics")
263
+
264
+ console.print()
265
+ console.print("[cyan]Press Ctrl+C to stop.[/cyan]")
266
+ console.print()
267
+
268
+ # Run server (blocking)
269
+ asyncio.run(run_fastapi_server(result.handle))
270
+
271
+ except KeyboardInterrupt:
272
+ console.print("\n[yellow]Shutting down...[/yellow]")
273
+ except Exception as e:
274
+ from htmlgraph.cli.base import save_traceback
275
+
276
+ log_file = save_traceback(
277
+ e, context={"command": "serve", "port": self.port}
278
+ )
279
+ console.print(f"\n[red]Error:[/red] {e}")
280
+ console.print(f"[dim]Full traceback saved to:[/dim] {log_file}")
281
+ sys.exit(1)
282
+
283
+ return CommandResult(text="Server stopped")
284
+
285
+
286
+ class ServeApiCommand(BaseCommand):
287
+ """Start the FastAPI-based dashboard."""
288
+
289
+ def __init__(
290
+ self,
291
+ *,
292
+ port: int,
293
+ host: str,
294
+ db: str | None,
295
+ auto_port: bool,
296
+ reload: bool,
297
+ ) -> None:
298
+ super().__init__()
299
+ self.port = port
300
+ self.host = host
301
+ self.db = db
302
+ self.auto_port = auto_port
303
+ self.reload = reload
304
+
305
+ @classmethod
306
+ def from_args(cls, args: argparse.Namespace) -> ServeApiCommand:
307
+ return cls(
308
+ port=args.port,
309
+ host=args.host,
310
+ db=args.db,
311
+ auto_port=args.auto_port,
312
+ reload=args.reload,
313
+ )
314
+
315
+ def execute(self) -> CommandResult:
316
+ """Start the FastAPI dashboard server."""
317
+ import asyncio
318
+
319
+ from rich.console import Console
320
+ from rich.panel import Panel
321
+
322
+ from htmlgraph.operations.fastapi_server import (
323
+ run_fastapi_server,
324
+ start_fastapi_server,
325
+ )
326
+
327
+ console = Console()
328
+
329
+ try:
330
+ result = start_fastapi_server(
331
+ port=self.port,
332
+ host=self.host,
333
+ db_path=self.db,
334
+ auto_port=self.auto_port,
335
+ reload=self.reload,
336
+ )
337
+
338
+ # Display server info using Rich
339
+ console.print()
340
+ console.print(
341
+ Panel.fit(
342
+ f"[bold blue]{result.handle.url}[/bold blue]",
343
+ title="[bold cyan]HtmlGraph FastAPI Dashboard[/bold cyan]",
344
+ border_style="green",
345
+ )
346
+ )
347
+
348
+ console.print("[bold green]✓[/bold green] Started observability dashboard")
349
+ console.print(f"[dim]Database:[/dim] {result.config_used['db_path']}")
350
+
351
+ # Show warnings if any
352
+ if result.warnings:
353
+ console.print()
354
+ for warning in result.warnings:
355
+ console.print(f"[yellow]⚠️ {warning}[/yellow]")
356
+
357
+ # Show available features
358
+ console.print()
359
+ console.print("[cyan]Features:[/cyan]")
360
+ console.print(" • Real-time agent activity feed")
361
+ console.print(" • Orchestration chains visualization")
362
+ console.print(" • Feature tracker with Kanban view")
363
+ console.print(" • Session metrics & performance analytics")
364
+ console.print(" • WebSocket live event streaming")
365
+
366
+ console.print()
367
+ console.print("[cyan]Press Ctrl+C to stop.[/cyan]")
368
+ console.print()
369
+
370
+ # Run server (blocking)
371
+ asyncio.run(run_fastapi_server(result.handle))
372
+
373
+ except KeyboardInterrupt:
374
+ console.print("\n[yellow]Shutting down...[/yellow]")
375
+ except Exception as e:
376
+ from htmlgraph.cli.base import save_traceback
377
+
378
+ log_file = save_traceback(e, context={"command": "serve-api"})
379
+ console.print(f"\n[red]Error:[/red] {e}")
380
+ console.print(f"[dim]Full traceback saved to:[/dim] {log_file}")
381
+ sys.exit(1)
382
+
383
+ return CommandResult(text="Dashboard stopped")
384
+
385
+
386
+ class InitCommand(BaseCommand):
387
+ """Initialize .htmlgraph directory."""
388
+
389
+ def __init__(
390
+ self,
391
+ *,
392
+ dir: str,
393
+ install_hooks: bool,
394
+ interactive: bool,
395
+ no_index: bool,
396
+ no_update_gitignore: bool,
397
+ no_events_keep: bool,
398
+ ) -> None:
399
+ super().__init__()
400
+ self.dir = dir
401
+ self.install_hooks = install_hooks
402
+ self.interactive = interactive
403
+ self.no_index = no_index
404
+ self.no_update_gitignore = no_update_gitignore
405
+ self.no_events_keep = no_events_keep
406
+
407
+ @classmethod
408
+ def from_args(cls, args: argparse.Namespace) -> InitCommand:
409
+ return cls(
410
+ dir=args.dir,
411
+ install_hooks=args.install_hooks,
412
+ interactive=args.interactive,
413
+ no_index=args.no_index,
414
+ no_update_gitignore=args.no_update_gitignore,
415
+ no_events_keep=args.no_events_keep,
416
+ )
417
+
418
+ def execute(self) -> CommandResult:
419
+ """Initialize the .htmlgraph directory."""
420
+ from htmlgraph.cli.base import TextOutputBuilder
421
+ from htmlgraph.cli.models import InitConfig
422
+ from htmlgraph.operations.initialization import initialize_htmlgraph
423
+
424
+ # Create config from command parameters
425
+ config = InitConfig(
426
+ dir=self.dir,
427
+ install_hooks=self.install_hooks,
428
+ interactive=self.interactive,
429
+ no_index=self.no_index,
430
+ no_update_gitignore=self.no_update_gitignore,
431
+ no_events_keep=self.no_events_keep,
432
+ )
433
+
434
+ # Initialize using new module
435
+ result = initialize_htmlgraph(config)
436
+
437
+ # Return result
438
+ if result.success:
439
+ output = TextOutputBuilder()
440
+ output.add_success("Initialized .htmlgraph directory")
441
+ output.add_field("Location", result.graph_dir)
442
+
443
+ # Show what was created
444
+ if result.directories_created:
445
+ output.add_info(
446
+ f"Created {len(result.directories_created)} directories"
447
+ )
448
+ if result.files_created:
449
+ output.add_info(f"Created/updated {len(result.files_created)} files")
450
+ if result.hooks_installed:
451
+ output.add_info("Git hooks installed")
452
+
453
+ # Show any warnings
454
+ for warning in result.warnings:
455
+ output.add_warning(warning)
456
+
457
+ return CommandResult(text=output.build(), json_data=result.dict())
458
+ else:
459
+ # Build error message from all errors
460
+ error_msg = (
461
+ "\n".join(result.errors) if result.errors else "Initialization failed"
462
+ )
463
+ raise CommandError(error_msg)
464
+
465
+
466
+ class StatusCommand(BaseCommand):
467
+ """Show graph status."""
468
+
469
+ @classmethod
470
+ def from_args(cls, args: argparse.Namespace) -> StatusCommand:
471
+ return cls()
472
+
473
+ def execute(self) -> CommandResult:
474
+ """Show the current graph status."""
475
+ from collections import Counter
476
+
477
+ from rich.console import Console
478
+ from rich.progress import Progress, SpinnerColumn, TextColumn
479
+
480
+ console = Console()
481
+
482
+ # Initialize SDK
483
+ with console.status("[blue]Initializing SDK...", spinner="dots"):
484
+ sdk = self.get_sdk()
485
+
486
+ total = 0
487
+ by_status: Counter[str] = Counter()
488
+ by_collection: dict[str, int] = {}
489
+
490
+ # Scan all collections
491
+ with Progress(
492
+ SpinnerColumn(),
493
+ TextColumn("[progress.description]{task.description}"),
494
+ console=console,
495
+ transient=True,
496
+ ) as progress:
497
+ task = progress.add_task("Scanning collections...", total=len(COLLECTIONS))
498
+
499
+ for coll_name in COLLECTIONS:
500
+ progress.update(task, description=f"Scanning {coll_name}...")
501
+ try:
502
+ coll = getattr(sdk, coll_name)
503
+ nodes = coll.all()
504
+ count = len(nodes)
505
+
506
+ if count > 0:
507
+ by_collection[coll_name] = count
508
+ total += count
509
+
510
+ # Count by status
511
+ for node in nodes:
512
+ status = getattr(node, "status", "unknown")
513
+ by_status[status] += 1
514
+
515
+ except Exception:
516
+ # Collection might not exist yet
517
+ pass
518
+
519
+ progress.update(task, advance=1)
520
+
521
+ # Build status table
522
+ from htmlgraph.cli.base import TableBuilder
523
+
524
+ builder = TableBuilder.create_list_table(f"HtmlGraph Status: {self.graph_dir}")
525
+ builder.add_column("Collection", style="cyan")
526
+ builder.add_numeric_column("Count", style="green")
527
+
528
+ for coll_name in sorted(by_collection.keys()):
529
+ builder.add_row(coll_name, str(by_collection[coll_name]))
530
+
531
+ builder.add_separator()
532
+ builder.add_row("[bold]Total", f"[bold]{total}")
533
+ table = builder.table
534
+
535
+ # Display results
536
+ console.print()
537
+ console.print(table)
538
+
539
+ # Show status breakdown
540
+ if by_status:
541
+ console.print()
542
+ console.print("[cyan]By Status:[/cyan]")
543
+ for status, count in sorted(by_status.items()):
544
+ console.print(f" {status}: {count}")
545
+
546
+ return CommandResult(
547
+ data={
548
+ "total_nodes": total,
549
+ "by_collection": dict(sorted(by_collection.items())),
550
+ "by_status": dict(sorted(by_status.items())),
551
+ },
552
+ text=f"Total nodes: {total}",
553
+ )
554
+
555
+
556
+ class DebugCommand(BaseCommand):
557
+ """Show debugging resources."""
558
+
559
+ @classmethod
560
+ def from_args(cls, args: argparse.Namespace) -> DebugCommand:
561
+ return cls()
562
+
563
+ def execute(self) -> CommandResult:
564
+ """Show debugging resources and diagnostics."""
565
+ import os
566
+ import sys
567
+ from pathlib import Path
568
+
569
+ from rich.console import Console
570
+ from rich.panel import Panel
571
+
572
+ console = Console()
573
+
574
+ # Header
575
+ console.print()
576
+ console.print(
577
+ Panel.fit(
578
+ "[bold cyan]HtmlGraph Debugging Resources[/bold cyan]",
579
+ border_style="cyan",
580
+ )
581
+ )
582
+
583
+ # Documentation section
584
+ console.print("\n[bold yellow]Documentation:[/bold yellow]")
585
+ console.print(" • DEBUGGING.md - Complete debugging guide")
586
+ console.print(" • AGENTS.md - SDK and agent documentation")
587
+ console.print(" • CLAUDE.md - Project workflow")
588
+
589
+ # Debugging Agents section
590
+ console.print("\n[bold yellow]Debugging Agents:[/bold yellow]")
591
+ agents_dir = Path("packages/claude-plugin/agents")
592
+ if agents_dir.exists():
593
+ console.print(f" • {agents_dir}/researcher.md")
594
+ console.print(f" • {agents_dir}/debugger.md")
595
+ console.print(f" • {agents_dir}/test-runner.md")
596
+ else:
597
+ console.print(
598
+ " • researcher.md - Research documentation before implementing"
599
+ )
600
+ console.print(" • debugger.md - Systematic error analysis")
601
+ console.print(" • test-runner.md - Quality gates and validation")
602
+
603
+ # Diagnostic Commands section
604
+ from htmlgraph.cli.base import TableBuilder
605
+
606
+ console.print("\n[bold yellow]Diagnostic Commands:[/bold yellow]")
607
+ cmd_builder = TableBuilder.create_compact_table()
608
+ cmd_builder.add_column("Command", style="cyan")
609
+ cmd_builder.add_column("Description", style="dim")
610
+ cmd_builder.add_row("htmlgraph status", "Show current graph state")
611
+ cmd_builder.add_row("htmlgraph feature list", "List all features")
612
+ cmd_builder.add_row("htmlgraph session list", "List all sessions")
613
+ cmd_builder.add_row("htmlgraph analytics", "Project analytics")
614
+ console.print(cmd_builder.table)
615
+
616
+ # Current Status section
617
+ console.print("\n[bold yellow]Current Status:[/bold yellow]")
618
+ graph_path = Path(self.graph_dir or DEFAULT_GRAPH_DIR)
619
+
620
+ status_builder = TableBuilder.create_compact_table()
621
+ status_builder.add_column("Item", style="dim")
622
+ status_builder.add_column("Value")
623
+
624
+ status_builder.add_row("Graph directory:", str(graph_path))
625
+
626
+ if graph_path.exists():
627
+ status_builder.add_row("Status:", "[green]✓ Initialized[/green]")
628
+
629
+ # Try to get quick stats
630
+ try:
631
+ sdk = self.get_sdk()
632
+
633
+ # Count features
634
+ features = sdk.features.all()
635
+ status_builder.add_row("Features:", str(len(features)))
636
+
637
+ # Count sessions
638
+ sessions = sdk.sessions.all()
639
+ status_builder.add_row("Sessions:", str(len(sessions)))
640
+
641
+ # Count other collections
642
+ for coll_name in [
643
+ "bugs",
644
+ "chores",
645
+ "spikes",
646
+ "epics",
647
+ "phases",
648
+ "tracks",
649
+ ]:
650
+ try:
651
+ coll = getattr(sdk, coll_name)
652
+ nodes = coll.all()
653
+ if len(nodes) > 0:
654
+ status_builder.add_row(
655
+ f"{coll_name.capitalize()}:", str(len(nodes))
656
+ )
657
+ except Exception:
658
+ pass
659
+
660
+ except Exception as e:
661
+ status_builder.add_row(
662
+ "Warning:", f"[yellow]Could not load graph data: {e}[/yellow]"
663
+ )
664
+ else:
665
+ status_builder.add_row("Status:", "[yellow]⚠️ Not initialized[/yellow]")
666
+ status_builder.add_row(
667
+ "", "[dim]Run 'htmlgraph init' to create .htmlgraph directory[/dim]"
668
+ )
669
+
670
+ console.print(status_builder.table)
671
+
672
+ # Environment Info section
673
+ console.print("\n[bold yellow]Environment:[/bold yellow]")
674
+ env_builder = TableBuilder.create_compact_table()
675
+ env_builder.add_column("Item", style="dim")
676
+ env_builder.add_column("Value")
677
+ env_builder.add_row("Python:", sys.version.split()[0])
678
+ env_builder.add_row("Working dir:", os.getcwd())
679
+ console.print(env_builder.table)
680
+
681
+ # Project Files section
682
+ console.print("\n[bold yellow]Project Files:[/bold yellow]")
683
+ files_builder = TableBuilder.create_compact_table()
684
+ files_builder.add_column("Status", justify="center")
685
+ files_builder.add_column("File")
686
+ for filename in ["pyproject.toml", "package.json", ".git", "README.md"]:
687
+ exists = "[green]✓[/green]" if Path(filename).exists() else "[red]✗[/red]"
688
+ files_builder.add_row(exists, filename)
689
+ console.print(files_builder.table)
690
+
691
+ # Footer
692
+ console.print()
693
+ console.print(
694
+ "[dim]For more help: https://github.com/Shakes-tzd/htmlgraph[/dim]"
695
+ )
696
+ console.print()
697
+
698
+ return CommandResult(text="Debug info displayed")
699
+
700
+
701
+ class QueryCommand(BaseCommand):
702
+ """Query nodes with CSS selector."""
703
+
704
+ def __init__(self, *, selector: str) -> None:
705
+ super().__init__()
706
+ self.selector = selector
707
+
708
+ @classmethod
709
+ def from_args(cls, args: argparse.Namespace) -> QueryCommand:
710
+ return cls(selector=args.selector)
711
+
712
+ def execute(self) -> CommandResult:
713
+ """Execute CSS selector query."""
714
+ from pathlib import Path
715
+ from typing import Any
716
+
717
+ from rich.console import Console
718
+ from rich.table import Table
719
+
720
+ from htmlgraph.converter import node_to_dict
721
+ from htmlgraph.graph import HtmlGraph
722
+
723
+ console = Console()
724
+
725
+ graph_dir = Path(self.graph_dir or DEFAULT_GRAPH_DIR)
726
+ if not graph_dir.exists():
727
+ raise CommandError(
728
+ get_error_message("missing_graph_dir", path=str(graph_dir))
729
+ )
730
+
731
+ # Query across all collections
732
+ results: list[dict[str, Any]] = []
733
+
734
+ with console.status(
735
+ f"[blue]Querying with selector '{self.selector}'...", spinner="dots"
736
+ ):
737
+ for collection_dir in graph_dir.iterdir():
738
+ if collection_dir.is_dir() and not collection_dir.name.startswith("."):
739
+ graph = HtmlGraph(collection_dir, auto_load=True)
740
+ for node in graph.query(self.selector):
741
+ data = node_to_dict(node)
742
+ data["_collection"] = collection_dir.name
743
+ results.append(data)
744
+
745
+ # Display results in table
746
+ if results:
747
+ table = Table(
748
+ title=f"Query Results: {self.selector}",
749
+ show_header=True,
750
+ header_style="bold cyan",
751
+ )
752
+ table.add_column("Collection", style="dim")
753
+ table.add_column("ID", style="cyan")
754
+ table.add_column("Title", style="white")
755
+ table.add_column("Status", style="blue")
756
+ table.add_column("Priority", style="yellow")
757
+
758
+ for result in results:
759
+ table.add_row(
760
+ result.get("_collection", "?"),
761
+ result.get("id", "?"),
762
+ result.get("title", "?"),
763
+ result.get("status", "?"),
764
+ result.get("priority", "?"),
765
+ )
766
+
767
+ console.print()
768
+ console.print(table)
769
+ console.print(f"\n[green]Found {len(results)} results[/green]")
770
+ else:
771
+ console.print(f"\n[yellow]No results found for '{self.selector}'[/yellow]")
772
+
773
+ return CommandResult(data=results, text=f"Found {len(results)} results")
774
+
775
+
776
+ class InstallHooksCommand(BaseCommand):
777
+ """Install Git hooks for event logging."""
778
+
779
+ def __init__(self, *, force: bool = False, dry_run: bool = False) -> None:
780
+ super().__init__()
781
+ self.force = force
782
+ self.dry_run = dry_run
783
+
784
+ @classmethod
785
+ def from_args(cls, args: argparse.Namespace) -> InstallHooksCommand:
786
+ return cls(
787
+ force=getattr(args, "force", False),
788
+ dry_run=getattr(args, "dry_run", False),
789
+ )
790
+
791
+ def execute(self) -> CommandResult:
792
+ """Install Git hooks."""
793
+ from pathlib import Path
794
+
795
+ from rich.console import Console
796
+
797
+ from htmlgraph.hooks.installer import HookConfig, HookInstaller
798
+
799
+ console = Console()
800
+
801
+ graph_dir = Path(self.graph_dir or DEFAULT_GRAPH_DIR).resolve()
802
+
803
+ # Validate environment
804
+ if not (graph_dir.parent / ".git").exists():
805
+ raise CommandError("Not a git repository (no .git directory found)")
806
+
807
+ if not graph_dir.exists():
808
+ raise CommandError(f"Graph directory not found: {graph_dir}")
809
+
810
+ # Create hook config and installer
811
+ config_path = graph_dir / "hooks-config.json"
812
+ config = HookConfig(config_path)
813
+ installer = HookInstaller(graph_dir.parent, config)
814
+
815
+ # Validate environment
816
+ is_valid, error_msg = installer.validate_environment()
817
+ if not is_valid:
818
+ raise CommandError(error_msg)
819
+
820
+ # Install hooks
821
+ with console.status("[blue]Installing Git hooks...", spinner="dots"):
822
+ results = installer.install_all_hooks(
823
+ dry_run=self.dry_run, force=self.force
824
+ )
825
+
826
+ # Build output
827
+ from htmlgraph.cli.base import TextOutputBuilder
828
+
829
+ output = TextOutputBuilder()
830
+
831
+ if self.dry_run:
832
+ output.add_info("DRY RUN - No changes made")
833
+
834
+ # Count results
835
+ success_count = sum(1 for success, _ in results.values() if success)
836
+ total = len(results)
837
+
838
+ output.add_success(f"Installed {success_count}/{total} hooks")
839
+
840
+ # Show individual results
841
+ for hook_name, (success, message) in sorted(results.items()):
842
+ status = "[green]✓[/green]" if success else "[yellow]✗[/yellow]"
843
+ output.add_line(f"{status} {hook_name}: {message}")
844
+
845
+ return CommandResult(
846
+ text=output.build(),
847
+ json_data={
848
+ "dry_run": self.dry_run,
849
+ "installed": success_count,
850
+ "total": total,
851
+ "results": {
852
+ name: {"success": success, "message": msg}
853
+ for name, (success, msg) in results.items()
854
+ },
855
+ },
856
+ )