htmlgraph 0.22.0__py3-none-any.whl → 0.23.1__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 +1 -1
- htmlgraph/agent_detection.py +41 -2
- htmlgraph/analytics/cli.py +86 -20
- htmlgraph/cli.py +280 -87
- htmlgraph/collections/base.py +68 -4
- htmlgraph/git_events.py +61 -7
- htmlgraph/operations/README.md +62 -0
- htmlgraph/operations/__init__.py +61 -0
- htmlgraph/operations/analytics.py +338 -0
- htmlgraph/operations/events.py +243 -0
- htmlgraph/operations/hooks.py +349 -0
- htmlgraph/operations/server.py +302 -0
- htmlgraph/orchestration/__init__.py +39 -0
- htmlgraph/orchestration/headless_spawner.py +566 -0
- htmlgraph/orchestration/model_selection.py +323 -0
- htmlgraph/orchestrator-system-prompt-optimized.txt +92 -0
- htmlgraph/parser.py +56 -1
- htmlgraph/sdk.py +529 -7
- htmlgraph/server.py +153 -60
- {htmlgraph-0.22.0.dist-info → htmlgraph-0.23.1.dist-info}/METADATA +3 -1
- {htmlgraph-0.22.0.dist-info → htmlgraph-0.23.1.dist-info}/RECORD +29 -19
- /htmlgraph/{orchestration.py → orchestration/task_coordination.py} +0 -0
- {htmlgraph-0.22.0.data → htmlgraph-0.23.1.data}/data/htmlgraph/dashboard.html +0 -0
- {htmlgraph-0.22.0.data → htmlgraph-0.23.1.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.22.0.data → htmlgraph-0.23.1.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
- {htmlgraph-0.22.0.data → htmlgraph-0.23.1.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
- {htmlgraph-0.22.0.data → htmlgraph-0.23.1.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
- {htmlgraph-0.22.0.dist-info → htmlgraph-0.23.1.dist-info}/WHEEL +0 -0
- {htmlgraph-0.22.0.dist-info → htmlgraph-0.23.1.dist-info}/entry_points.txt +0 -0
htmlgraph/cli.py
CHANGED
|
@@ -8,6 +8,11 @@ Usage:
|
|
|
8
8
|
htmlgraph status [--dir DIR]
|
|
9
9
|
htmlgraph query SELECTOR [--dir DIR]
|
|
10
10
|
|
|
11
|
+
Claude Code Integration:
|
|
12
|
+
htmlgraph claude # Start Claude Code
|
|
13
|
+
htmlgraph claude --init # Start with orchestrator system prompt (recommended)
|
|
14
|
+
htmlgraph claude --continue # Resume last Claude Code session
|
|
15
|
+
|
|
11
16
|
Session Management:
|
|
12
17
|
htmlgraph session start [--id ID] [--agent AGENT]
|
|
13
18
|
htmlgraph session end ID [--notes NOTES] [--recommend NEXT] [--blocker BLOCKER]
|
|
@@ -111,9 +116,9 @@ def cmd_install_gemini_extension(args: argparse.Namespace) -> None:
|
|
|
111
116
|
|
|
112
117
|
def cmd_serve(args: argparse.Namespace) -> None:
|
|
113
118
|
"""Start the HtmlGraph server."""
|
|
114
|
-
from htmlgraph.
|
|
119
|
+
from htmlgraph.operations import start_server
|
|
115
120
|
|
|
116
|
-
|
|
121
|
+
start_server(
|
|
117
122
|
port=args.port,
|
|
118
123
|
graph_dir=args.graph_dir,
|
|
119
124
|
static_dir=args.static_dir,
|
|
@@ -122,10 +127,14 @@ def cmd_serve(args: argparse.Namespace) -> None:
|
|
|
122
127
|
auto_port=args.auto_port,
|
|
123
128
|
)
|
|
124
129
|
|
|
130
|
+
# The start_server operation already handles all output and blocks
|
|
131
|
+
# No additional CLI formatting needed
|
|
132
|
+
|
|
125
133
|
|
|
126
134
|
def cmd_init(args: argparse.Namespace) -> None:
|
|
127
135
|
"""Initialize a new .htmlgraph directory."""
|
|
128
136
|
import shutil
|
|
137
|
+
from contextlib import nullcontext
|
|
129
138
|
|
|
130
139
|
from htmlgraph.analytics_index import AnalyticsIndex
|
|
131
140
|
from htmlgraph.server import HtmlGraphAPIHandler
|
|
@@ -160,38 +169,33 @@ def cmd_init(args: argparse.Namespace) -> None:
|
|
|
160
169
|
agent_name = "claude"
|
|
161
170
|
generate_docs = True # Always generate in non-interactive mode
|
|
162
171
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
for collection in HtmlGraphAPIHandler.COLLECTIONS:
|
|
167
|
-
(graph_dir / collection).mkdir(exist_ok=True)
|
|
168
|
-
|
|
169
|
-
# Event stream directory (Git-friendly source of truth)
|
|
170
|
-
events_dir = graph_dir / "events"
|
|
171
|
-
events_dir.mkdir(exist_ok=True)
|
|
172
|
-
if not args.no_events_keep:
|
|
173
|
-
keep = events_dir / ".gitkeep"
|
|
174
|
-
if not keep.exists():
|
|
175
|
-
keep.write_text("", encoding="utf-8")
|
|
176
|
-
|
|
177
|
-
# Copy stylesheet
|
|
178
|
-
styles_src = Path(__file__).parent / "styles.css"
|
|
179
|
-
styles_dest = graph_dir / "styles.css"
|
|
180
|
-
if styles_src.exists() and not styles_dest.exists():
|
|
181
|
-
styles_dest.write_text(styles_src.read_text())
|
|
182
|
-
|
|
183
|
-
# Create default index.html if not exists
|
|
184
|
-
index_path = Path(args.dir) / "index.html"
|
|
185
|
-
if not index_path.exists():
|
|
186
|
-
create_default_index(index_path)
|
|
187
|
-
|
|
188
|
-
# Create analytics cache DB (rebuildable; typically gitignored)
|
|
189
|
-
if not args.no_index:
|
|
172
|
+
def init_progress() -> tuple[Any | None, Any | None]:
|
|
173
|
+
if args.quiet or getattr(args, "format", "text") != "text":
|
|
174
|
+
return None, None
|
|
190
175
|
try:
|
|
191
|
-
|
|
176
|
+
from rich.console import Console
|
|
177
|
+
from rich.progress import (
|
|
178
|
+
BarColumn,
|
|
179
|
+
Progress,
|
|
180
|
+
SpinnerColumn,
|
|
181
|
+
TextColumn,
|
|
182
|
+
TimeElapsedColumn,
|
|
183
|
+
)
|
|
192
184
|
except Exception:
|
|
193
|
-
|
|
194
|
-
|
|
185
|
+
return None, None
|
|
186
|
+
console = Console()
|
|
187
|
+
progress = Progress(
|
|
188
|
+
SpinnerColumn(),
|
|
189
|
+
TextColumn("{task.description}"),
|
|
190
|
+
BarColumn(),
|
|
191
|
+
TimeElapsedColumn(),
|
|
192
|
+
console=console,
|
|
193
|
+
transient=True,
|
|
194
|
+
)
|
|
195
|
+
return progress, console
|
|
196
|
+
|
|
197
|
+
graph_dir = Path(args.dir) / ".htmlgraph"
|
|
198
|
+
events_dir = graph_dir / "events"
|
|
195
199
|
|
|
196
200
|
def ensure_gitignore_entries(project_dir: Path, lines: list[str]) -> None:
|
|
197
201
|
if args.no_update_gitignore:
|
|
@@ -218,19 +222,12 @@ def cmd_init(args: argparse.Namespace) -> None:
|
|
|
218
222
|
# Don't fail init on .gitignore issues.
|
|
219
223
|
pass
|
|
220
224
|
|
|
221
|
-
|
|
222
|
-
Path(args.dir),
|
|
223
|
-
[
|
|
224
|
-
".htmlgraph/index.sqlite",
|
|
225
|
-
".htmlgraph/index.sqlite-wal",
|
|
226
|
-
".htmlgraph/index.sqlite-shm",
|
|
227
|
-
".htmlgraph/git-hook-errors.log",
|
|
228
|
-
],
|
|
229
|
-
)
|
|
225
|
+
progress, progress_console = init_progress()
|
|
230
226
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
227
|
+
def status_context(message: str) -> Any:
|
|
228
|
+
if progress_console is None:
|
|
229
|
+
return nullcontext()
|
|
230
|
+
return progress_console.status(message)
|
|
234
231
|
|
|
235
232
|
# Hook templates (used when htmlgraph is installed without this repo layout).
|
|
236
233
|
post_commit = """#!/bin/bash
|
|
@@ -443,26 +440,77 @@ fi
|
|
|
443
440
|
exit 0
|
|
444
441
|
"""
|
|
445
442
|
|
|
446
|
-
|
|
447
|
-
hook_dest = hooks_dir / f"{hook_name}.sh"
|
|
448
|
-
if not hook_dest.exists():
|
|
449
|
-
hook_dest.write_text(hook_content)
|
|
450
|
-
try:
|
|
451
|
-
hook_dest.chmod(0o755)
|
|
452
|
-
except Exception:
|
|
453
|
-
pass
|
|
454
|
-
return hook_dest
|
|
455
|
-
|
|
456
|
-
hook_files = {
|
|
457
|
-
"pre-commit": ensure_hook_file("pre-commit", pre_commit),
|
|
458
|
-
"post-commit": ensure_hook_file("post-commit", post_commit),
|
|
459
|
-
"post-checkout": ensure_hook_file("post-checkout", post_checkout),
|
|
460
|
-
"post-merge": ensure_hook_file("post-merge", post_merge),
|
|
461
|
-
"pre-push": ensure_hook_file("pre-push", pre_push),
|
|
462
|
-
}
|
|
443
|
+
hook_files: dict[str, Path] = {}
|
|
463
444
|
|
|
464
|
-
|
|
465
|
-
|
|
445
|
+
def create_graph_dirs() -> None:
|
|
446
|
+
graph_dir.mkdir(parents=True, exist_ok=True)
|
|
447
|
+
for collection in HtmlGraphAPIHandler.COLLECTIONS:
|
|
448
|
+
(graph_dir / collection).mkdir(exist_ok=True)
|
|
449
|
+
|
|
450
|
+
def create_events_dir() -> None:
|
|
451
|
+
events_dir.mkdir(exist_ok=True)
|
|
452
|
+
if not args.no_events_keep:
|
|
453
|
+
keep = events_dir / ".gitkeep"
|
|
454
|
+
if not keep.exists():
|
|
455
|
+
keep.write_text("", encoding="utf-8")
|
|
456
|
+
|
|
457
|
+
def copy_assets() -> None:
|
|
458
|
+
styles_src = Path(__file__).parent / "styles.css"
|
|
459
|
+
styles_dest = graph_dir / "styles.css"
|
|
460
|
+
if styles_src.exists() and not styles_dest.exists():
|
|
461
|
+
styles_dest.write_text(styles_src.read_text())
|
|
462
|
+
|
|
463
|
+
index_path = Path(args.dir) / "index.html"
|
|
464
|
+
if not index_path.exists():
|
|
465
|
+
create_default_index(index_path)
|
|
466
|
+
|
|
467
|
+
def init_analytics_cache() -> None:
|
|
468
|
+
if args.no_index:
|
|
469
|
+
return
|
|
470
|
+
with status_context("Initializing analytics cache..."):
|
|
471
|
+
try:
|
|
472
|
+
AnalyticsIndex(graph_dir / "index.sqlite").ensure_schema()
|
|
473
|
+
except Exception:
|
|
474
|
+
# Never fail init because of analytics cache.
|
|
475
|
+
pass
|
|
476
|
+
|
|
477
|
+
def update_gitignore() -> None:
|
|
478
|
+
ensure_gitignore_entries(
|
|
479
|
+
Path(args.dir),
|
|
480
|
+
[
|
|
481
|
+
".htmlgraph/index.sqlite",
|
|
482
|
+
".htmlgraph/index.sqlite-wal",
|
|
483
|
+
".htmlgraph/index.sqlite-shm",
|
|
484
|
+
".htmlgraph/git-hook-errors.log",
|
|
485
|
+
],
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
def create_hook_templates() -> None:
|
|
489
|
+
nonlocal hook_files
|
|
490
|
+
hooks_dir = graph_dir / "hooks"
|
|
491
|
+
hooks_dir.mkdir(exist_ok=True)
|
|
492
|
+
|
|
493
|
+
def ensure_hook_file(hook_name: str, hook_content: str) -> Path:
|
|
494
|
+
hook_dest = hooks_dir / f"{hook_name}.sh"
|
|
495
|
+
if not hook_dest.exists():
|
|
496
|
+
hook_dest.write_text(hook_content)
|
|
497
|
+
try:
|
|
498
|
+
hook_dest.chmod(0o755)
|
|
499
|
+
except Exception:
|
|
500
|
+
pass
|
|
501
|
+
return hook_dest
|
|
502
|
+
|
|
503
|
+
hook_files = {
|
|
504
|
+
"pre-commit": ensure_hook_file("pre-commit", pre_commit),
|
|
505
|
+
"post-commit": ensure_hook_file("post-commit", post_commit),
|
|
506
|
+
"post-checkout": ensure_hook_file("post-checkout", post_checkout),
|
|
507
|
+
"post-merge": ensure_hook_file("post-merge", post_merge),
|
|
508
|
+
"pre-push": ensure_hook_file("pre-push", pre_push),
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
def generate_docs_step() -> None:
|
|
512
|
+
if not generate_docs:
|
|
513
|
+
return
|
|
466
514
|
|
|
467
515
|
def render_template(
|
|
468
516
|
template_path: Path, replacements: dict[str, str]
|
|
@@ -483,7 +531,7 @@ exit 0
|
|
|
483
531
|
from htmlgraph import __version__
|
|
484
532
|
|
|
485
533
|
version = __version__
|
|
486
|
-
except:
|
|
534
|
+
except Exception:
|
|
487
535
|
version = "unknown"
|
|
488
536
|
|
|
489
537
|
replacements = {
|
|
@@ -519,17 +567,9 @@ exit 0
|
|
|
519
567
|
gemini_dest.write_text(content, encoding="utf-8")
|
|
520
568
|
print(f"✓ Generated: {gemini_dest}")
|
|
521
569
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
if not args.no_index:
|
|
526
|
-
print(
|
|
527
|
-
f"Analytics cache: {graph_dir / 'index.sqlite'} (rebuildable; typically gitignored)"
|
|
528
|
-
)
|
|
529
|
-
print(f"Events: {events_dir}/ (append-only JSONL)")
|
|
530
|
-
|
|
531
|
-
# Install Git hooks if requested
|
|
532
|
-
if args.install_hooks:
|
|
570
|
+
def install_hooks_step() -> None:
|
|
571
|
+
if not args.install_hooks:
|
|
572
|
+
return
|
|
533
573
|
git_dir = Path(args.dir) / ".git"
|
|
534
574
|
if not git_dir.exists():
|
|
535
575
|
print("\n⚠️ Warning: No .git directory found. Git hooks not installed.")
|
|
@@ -606,6 +646,46 @@ fi
|
|
|
606
646
|
|
|
607
647
|
print("\nGit events will now be logged to HtmlGraph automatically.")
|
|
608
648
|
|
|
649
|
+
steps: list[tuple[str, Any]] = [
|
|
650
|
+
("Create .htmlgraph directories", create_graph_dirs),
|
|
651
|
+
("Create event log directory", create_events_dir),
|
|
652
|
+
("Update .gitignore", update_gitignore),
|
|
653
|
+
("Prepare git hook templates", create_hook_templates),
|
|
654
|
+
("Copy default assets", copy_assets),
|
|
655
|
+
]
|
|
656
|
+
if not args.no_index:
|
|
657
|
+
steps.append(("Initialize analytics cache", init_analytics_cache))
|
|
658
|
+
if generate_docs:
|
|
659
|
+
steps.append(("Generate documentation", generate_docs_step))
|
|
660
|
+
if args.install_hooks:
|
|
661
|
+
steps.append(("Install git hooks", install_hooks_step))
|
|
662
|
+
|
|
663
|
+
def run_steps(step_list: list[tuple[str, Any]]) -> None:
|
|
664
|
+
if progress is None:
|
|
665
|
+
for _, fn in step_list:
|
|
666
|
+
fn()
|
|
667
|
+
return
|
|
668
|
+
|
|
669
|
+
with progress:
|
|
670
|
+
task_id = progress.add_task(
|
|
671
|
+
"Initializing HtmlGraph...", total=len(step_list)
|
|
672
|
+
)
|
|
673
|
+
for description, fn in step_list:
|
|
674
|
+
progress.update(task_id, description=description)
|
|
675
|
+
fn()
|
|
676
|
+
progress.advance(task_id)
|
|
677
|
+
|
|
678
|
+
run_steps(steps)
|
|
679
|
+
|
|
680
|
+
print(f"\nInitialized HtmlGraph in {graph_dir}")
|
|
681
|
+
print(f"Collections: {', '.join(HtmlGraphAPIHandler.COLLECTIONS)}")
|
|
682
|
+
print("\nStart server with: htmlgraph serve")
|
|
683
|
+
if not args.no_index:
|
|
684
|
+
print(
|
|
685
|
+
f"Analytics cache: {graph_dir / 'index.sqlite'} (rebuildable; typically gitignored)"
|
|
686
|
+
)
|
|
687
|
+
print(f"Events: {events_dir}/ (append-only JSONL)")
|
|
688
|
+
|
|
609
689
|
|
|
610
690
|
def cmd_install_hooks(args: argparse.Namespace) -> None:
|
|
611
691
|
"""Install Git hooks for automatic tracking."""
|
|
@@ -2358,22 +2438,15 @@ def cmd_events_export(args: argparse.Namespace) -> None:
|
|
|
2358
2438
|
|
|
2359
2439
|
def cmd_index_rebuild(args: argparse.Namespace) -> None:
|
|
2360
2440
|
"""Rebuild the SQLite analytics index from JSONL event logs."""
|
|
2361
|
-
from htmlgraph.
|
|
2362
|
-
from htmlgraph.event_log import JsonlEventLog
|
|
2441
|
+
from htmlgraph.operations import rebuild_index
|
|
2363
2442
|
|
|
2364
2443
|
graph_dir = Path(args.graph_dir)
|
|
2365
|
-
events_dir = graph_dir / "events"
|
|
2366
|
-
db_path = graph_dir / "index.sqlite"
|
|
2367
|
-
|
|
2368
|
-
log = JsonlEventLog(events_dir)
|
|
2369
|
-
index = AnalyticsIndex(db_path)
|
|
2370
2444
|
|
|
2371
|
-
|
|
2372
|
-
result = index.rebuild_from_events(events)
|
|
2445
|
+
result = rebuild_index(graph_dir=graph_dir)
|
|
2373
2446
|
|
|
2374
|
-
print(f"DB: {db_path}")
|
|
2375
|
-
print(f"Inserted: {result
|
|
2376
|
-
print(f"Skipped: {result
|
|
2447
|
+
print(f"DB: {result.db_path}")
|
|
2448
|
+
print(f"Inserted: {result.inserted}")
|
|
2449
|
+
print(f"Skipped: {result.skipped}")
|
|
2377
2450
|
|
|
2378
2451
|
|
|
2379
2452
|
def cmd_watch(args: argparse.Namespace) -> None:
|
|
@@ -3512,6 +3585,106 @@ def create_default_index(path: Path) -> None:
|
|
|
3512
3585
|
)
|
|
3513
3586
|
|
|
3514
3587
|
|
|
3588
|
+
def cmd_claude(args: argparse.Namespace) -> None:
|
|
3589
|
+
"""Start Claude Code with orchestrator prompt."""
|
|
3590
|
+
import textwrap
|
|
3591
|
+
|
|
3592
|
+
try:
|
|
3593
|
+
if args.init:
|
|
3594
|
+
# Load optimized orchestrator system prompt
|
|
3595
|
+
prompt_file = Path(__file__).parent / "orchestrator_system_prompt.txt"
|
|
3596
|
+
|
|
3597
|
+
if prompt_file.exists():
|
|
3598
|
+
system_prompt = prompt_file.read_text(encoding="utf-8")
|
|
3599
|
+
else:
|
|
3600
|
+
# Fallback: provide minimal orchestrator guidance
|
|
3601
|
+
system_prompt = textwrap.dedent(
|
|
3602
|
+
"""
|
|
3603
|
+
You are an AI orchestrator for HtmlGraph project development.
|
|
3604
|
+
|
|
3605
|
+
CRITICAL DIRECTIVES:
|
|
3606
|
+
1. DELEGATE to subagents - do not implement directly
|
|
3607
|
+
2. CREATE work items before delegating (features, bugs, spikes)
|
|
3608
|
+
3. USE SDK for tracking - all work must be tracked in .htmlgraph/
|
|
3609
|
+
4. RESPECT dependencies - check blockers before starting
|
|
3610
|
+
|
|
3611
|
+
Key Rules:
|
|
3612
|
+
- Implementation work → delegate to general-purpose subagent
|
|
3613
|
+
- Research/exploration → delegate to explorer subagent
|
|
3614
|
+
- Testing/validation → delegate to test-runner subagent
|
|
3615
|
+
- Complex analysis → delegate to appropriate specialist
|
|
3616
|
+
|
|
3617
|
+
Always use:
|
|
3618
|
+
from htmlgraph import SDK
|
|
3619
|
+
sdk = SDK(agent='orchestrator')
|
|
3620
|
+
|
|
3621
|
+
See CLAUDE.md for complete orchestrator directives.
|
|
3622
|
+
"""
|
|
3623
|
+
)
|
|
3624
|
+
|
|
3625
|
+
if args.quiet or args.format == "json":
|
|
3626
|
+
# Non-interactive: directly launch Claude with system prompt
|
|
3627
|
+
cmd = ["claude", "--append-system-prompt", system_prompt]
|
|
3628
|
+
else:
|
|
3629
|
+
# Interactive: show summary first
|
|
3630
|
+
print("=" * 60)
|
|
3631
|
+
print("🤖 HtmlGraph Orchestrator Mode")
|
|
3632
|
+
print("=" * 60)
|
|
3633
|
+
print("\nStarting Claude Code with orchestrator system prompt...")
|
|
3634
|
+
print("Key directives:")
|
|
3635
|
+
print(" ✓ Delegate implementation to subagents")
|
|
3636
|
+
print(" ✓ Create work items before delegating")
|
|
3637
|
+
print(" ✓ Track all work in .htmlgraph/")
|
|
3638
|
+
print(" ✓ Respect dependency chains")
|
|
3639
|
+
print()
|
|
3640
|
+
|
|
3641
|
+
cmd = ["claude", "--append-system-prompt", system_prompt]
|
|
3642
|
+
|
|
3643
|
+
try:
|
|
3644
|
+
subprocess.run(cmd, check=False)
|
|
3645
|
+
except FileNotFoundError:
|
|
3646
|
+
print("Error: 'claude' command not found.", file=sys.stderr)
|
|
3647
|
+
print(
|
|
3648
|
+
"Please install Claude Code CLI: https://code.claude.com",
|
|
3649
|
+
file=sys.stderr,
|
|
3650
|
+
)
|
|
3651
|
+
sys.exit(1)
|
|
3652
|
+
|
|
3653
|
+
elif args.continue_session:
|
|
3654
|
+
# Resume last Claude Code session
|
|
3655
|
+
if args.quiet or args.format == "json":
|
|
3656
|
+
cmd = ["claude", "--resume"]
|
|
3657
|
+
else:
|
|
3658
|
+
print("Resuming last Claude Code session...")
|
|
3659
|
+
cmd = ["claude", "--resume"]
|
|
3660
|
+
|
|
3661
|
+
try:
|
|
3662
|
+
subprocess.run(cmd, check=False)
|
|
3663
|
+
except FileNotFoundError:
|
|
3664
|
+
print("Error: 'claude' command not found.", file=sys.stderr)
|
|
3665
|
+
print(
|
|
3666
|
+
"Please install Claude Code CLI: https://code.claude.com",
|
|
3667
|
+
file=sys.stderr,
|
|
3668
|
+
)
|
|
3669
|
+
sys.exit(1)
|
|
3670
|
+
|
|
3671
|
+
else:
|
|
3672
|
+
# Default: start normal Claude Code session
|
|
3673
|
+
try:
|
|
3674
|
+
subprocess.run(["claude"], check=False)
|
|
3675
|
+
except FileNotFoundError:
|
|
3676
|
+
print("Error: 'claude' command not found.", file=sys.stderr)
|
|
3677
|
+
print(
|
|
3678
|
+
"Please install Claude Code CLI: https://code.claude.com",
|
|
3679
|
+
file=sys.stderr,
|
|
3680
|
+
)
|
|
3681
|
+
sys.exit(1)
|
|
3682
|
+
|
|
3683
|
+
except Exception as e:
|
|
3684
|
+
print(f"Error: Failed to start Claude Code: {e}", file=sys.stderr)
|
|
3685
|
+
sys.exit(1)
|
|
3686
|
+
|
|
3687
|
+
|
|
3515
3688
|
def main() -> None:
|
|
3516
3689
|
parser = argparse.ArgumentParser(
|
|
3517
3690
|
description="HtmlGraph - HTML is All You Need",
|
|
@@ -4917,6 +5090,24 @@ For more help: https://github.com/Shakes-tzd/htmlgraph
|
|
|
4917
5090
|
help="Install the Gemini CLI extension from the bundled package",
|
|
4918
5091
|
)
|
|
4919
5092
|
|
|
5093
|
+
# claude - Start Claude Code with orchestrator support
|
|
5094
|
+
claude_parser = subparsers.add_parser(
|
|
5095
|
+
"claude", help="Start Claude Code with HtmlGraph integration"
|
|
5096
|
+
)
|
|
5097
|
+
claude_group = claude_parser.add_mutually_exclusive_group()
|
|
5098
|
+
claude_group.add_argument(
|
|
5099
|
+
"--init",
|
|
5100
|
+
action="store_true",
|
|
5101
|
+
help="Start with orchestrator system prompt (recommended)",
|
|
5102
|
+
)
|
|
5103
|
+
claude_group.add_argument(
|
|
5104
|
+
"--continue",
|
|
5105
|
+
dest="continue_session",
|
|
5106
|
+
action="store_true",
|
|
5107
|
+
help="Resume last Claude Code session",
|
|
5108
|
+
)
|
|
5109
|
+
claude_parser.set_defaults(func=cmd_claude)
|
|
5110
|
+
|
|
4920
5111
|
args = parser.parse_args()
|
|
4921
5112
|
|
|
4922
5113
|
if args.command == "serve":
|
|
@@ -5154,6 +5345,8 @@ For more help: https://github.com/Shakes-tzd/htmlgraph
|
|
|
5154
5345
|
sys.exit(1)
|
|
5155
5346
|
elif args.command == "install-gemini-extension":
|
|
5156
5347
|
cmd_install_gemini_extension(args)
|
|
5348
|
+
elif args.command == "claude":
|
|
5349
|
+
cmd_claude(args)
|
|
5157
5350
|
else:
|
|
5158
5351
|
parser.print_help()
|
|
5159
5352
|
sys.exit(1)
|
htmlgraph/collections/base.py
CHANGED
|
@@ -91,6 +91,48 @@ class BaseCollection(Generic[CollectionT]):
|
|
|
91
91
|
|
|
92
92
|
return self._graph
|
|
93
93
|
|
|
94
|
+
def __getattribute__(self, name: str) -> Any:
|
|
95
|
+
"""Override to provide helpful error messages for missing attributes."""
|
|
96
|
+
try:
|
|
97
|
+
return object.__getattribute__(self, name)
|
|
98
|
+
except AttributeError as e:
|
|
99
|
+
# Get available methods
|
|
100
|
+
available = [m for m in dir(self) if not m.startswith("_")]
|
|
101
|
+
|
|
102
|
+
# Common mistakes mapping
|
|
103
|
+
common_mistakes = {
|
|
104
|
+
"mark_complete": "mark_done",
|
|
105
|
+
"complete": "Use complete(node_id) for single item or mark_done([ids]) for batch",
|
|
106
|
+
"finish": "mark_done",
|
|
107
|
+
"end": "mark_done",
|
|
108
|
+
"update_status": "edit() context manager or batch_update()",
|
|
109
|
+
"mark_as_done": "mark_done",
|
|
110
|
+
"set_done": "mark_done",
|
|
111
|
+
"complete_all": "mark_done",
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
suggestions = []
|
|
115
|
+
if name in common_mistakes:
|
|
116
|
+
suggestions.append(f"Did you mean: {common_mistakes[name]}")
|
|
117
|
+
|
|
118
|
+
# Find similar method names
|
|
119
|
+
similar = [
|
|
120
|
+
m
|
|
121
|
+
for m in available
|
|
122
|
+
if name.lower() in m.lower() or m.lower() in name.lower()
|
|
123
|
+
]
|
|
124
|
+
if similar:
|
|
125
|
+
suggestions.append(f"Similar methods: {', '.join(similar[:5])}")
|
|
126
|
+
|
|
127
|
+
# Build helpful error message
|
|
128
|
+
error_msg = f"'{type(self).__name__}' has no attribute '{name}'."
|
|
129
|
+
if suggestions:
|
|
130
|
+
error_msg += "\n\n" + "\n".join(suggestions)
|
|
131
|
+
error_msg += f"\n\nAvailable methods: {', '.join(available[:15])}"
|
|
132
|
+
error_msg += "\n\nTip: Use sdk.help() to see all available operations."
|
|
133
|
+
|
|
134
|
+
raise AttributeError(error_msg) from e
|
|
135
|
+
|
|
94
136
|
def __dir__(self) -> list[str]:
|
|
95
137
|
"""Return attributes with most useful ones first for discoverability."""
|
|
96
138
|
priority = [
|
|
@@ -418,7 +460,7 @@ class BaseCollection(Generic[CollectionT]):
|
|
|
418
460
|
|
|
419
461
|
return count
|
|
420
462
|
|
|
421
|
-
def mark_done(self, node_ids: list[str]) ->
|
|
463
|
+
def mark_done(self, node_ids: list[str]) -> dict[str, Any]:
|
|
422
464
|
"""
|
|
423
465
|
Batch mark nodes as done.
|
|
424
466
|
|
|
@@ -426,12 +468,34 @@ class BaseCollection(Generic[CollectionT]):
|
|
|
426
468
|
node_ids: List of node IDs to mark as done
|
|
427
469
|
|
|
428
470
|
Returns:
|
|
429
|
-
|
|
471
|
+
Dict with 'success_count', 'failed_ids', and 'warnings'
|
|
430
472
|
|
|
431
473
|
Example:
|
|
432
|
-
>>> sdk.features.mark_done(["feat-001", "feat-002"])
|
|
474
|
+
>>> result = sdk.features.mark_done(["feat-001", "feat-002"])
|
|
475
|
+
>>> print(f"Completed {result['success_count']} of {len(node_ids)}")
|
|
476
|
+
>>> if result['failed_ids']:
|
|
477
|
+
... print(f"Failed: {result['failed_ids']}")
|
|
433
478
|
"""
|
|
434
|
-
|
|
479
|
+
graph = self._ensure_graph()
|
|
480
|
+
results: dict[str, Any] = {"success_count": 0, "failed_ids": [], "warnings": []}
|
|
481
|
+
|
|
482
|
+
for node_id in node_ids:
|
|
483
|
+
try:
|
|
484
|
+
node = graph.get(node_id)
|
|
485
|
+
if not node:
|
|
486
|
+
results["failed_ids"].append(node_id)
|
|
487
|
+
results["warnings"].append(f"Node {node_id} not found")
|
|
488
|
+
continue
|
|
489
|
+
|
|
490
|
+
node.status = "done"
|
|
491
|
+
node.updated = datetime.now()
|
|
492
|
+
graph.update(node)
|
|
493
|
+
results["success_count"] += 1
|
|
494
|
+
except Exception as e:
|
|
495
|
+
results["failed_ids"].append(node_id)
|
|
496
|
+
results["warnings"].append(f"Failed to mark {node_id}: {str(e)}")
|
|
497
|
+
|
|
498
|
+
return results
|
|
435
499
|
|
|
436
500
|
def assign(self, node_ids: list[str], agent: str) -> int:
|
|
437
501
|
"""
|
htmlgraph/git_events.py
CHANGED
|
@@ -168,9 +168,19 @@ def parse_feature_refs(message: str) -> list[str]:
|
|
|
168
168
|
Parse feature IDs from commit message.
|
|
169
169
|
|
|
170
170
|
Looks for patterns like:
|
|
171
|
-
- Implements:
|
|
171
|
+
- Implements: feat-xyz
|
|
172
172
|
- Fixes: bug-abc
|
|
173
|
-
-
|
|
173
|
+
- [feat-123abc]
|
|
174
|
+
- feat-xyz
|
|
175
|
+
|
|
176
|
+
Supports HtmlGraph ID formats:
|
|
177
|
+
- feat-XXXXXXXX (features)
|
|
178
|
+
- feature-XXXXXXXX (legacy features)
|
|
179
|
+
- bug-XXXXXXXX (bugs)
|
|
180
|
+
- spk-XXXXXXXX (spikes)
|
|
181
|
+
- spike-XXXXXXXX (legacy spikes)
|
|
182
|
+
- chr-XXXXXXXX (chores)
|
|
183
|
+
- trk-XXXXXXXX (tracks)
|
|
174
184
|
|
|
175
185
|
Args:
|
|
176
186
|
message: Commit message
|
|
@@ -180,14 +190,21 @@ def parse_feature_refs(message: str) -> list[str]:
|
|
|
180
190
|
"""
|
|
181
191
|
features = []
|
|
182
192
|
|
|
183
|
-
#
|
|
184
|
-
|
|
193
|
+
# All HtmlGraph ID prefixes (current + legacy)
|
|
194
|
+
id_prefixes = r"(?:feat|feature|bug|spk|spike|chr|chore|trk|track|todo)"
|
|
195
|
+
|
|
196
|
+
# Pattern 1: Explicit tags (Implements: feat-xyz)
|
|
197
|
+
pattern1 = rf"(?:Implements|Fixes|Closes|Refs):\s*({id_prefixes}-[\w-]+)"
|
|
185
198
|
features.extend(re.findall(pattern1, message, re.IGNORECASE))
|
|
186
199
|
|
|
187
|
-
# Pattern:
|
|
188
|
-
pattern2 =
|
|
200
|
+
# Pattern 2: Square brackets [feat-xyz] (common in commit messages)
|
|
201
|
+
pattern2 = rf"\[({id_prefixes}-[\w-]+)\]"
|
|
189
202
|
features.extend(re.findall(pattern2, message, re.IGNORECASE))
|
|
190
203
|
|
|
204
|
+
# Pattern 3: Anywhere in message as word boundary
|
|
205
|
+
pattern3 = rf"\b({id_prefixes}-[\w-]+)\b"
|
|
206
|
+
features.extend(re.findall(pattern3, message, re.IGNORECASE))
|
|
207
|
+
|
|
191
208
|
# Remove duplicates while preserving order
|
|
192
209
|
seen = set()
|
|
193
210
|
unique_features = []
|
|
@@ -288,7 +305,44 @@ def _determine_context(graph_dir: Path, commit_message: str | None = None) -> di
|
|
|
288
305
|
if f and f not in all_features:
|
|
289
306
|
all_features.append(f)
|
|
290
307
|
|
|
291
|
-
session
|
|
308
|
+
# Try to find the right session based on feature IDs in commit message
|
|
309
|
+
# This handles multi-agent scenarios where multiple sessions are active
|
|
310
|
+
session = None
|
|
311
|
+
if message_features:
|
|
312
|
+
# If commit mentions specific features, find the session working on them
|
|
313
|
+
manager = _get_session_manager(graph_dir)
|
|
314
|
+
if manager:
|
|
315
|
+
try:
|
|
316
|
+
# Try to find a session that has any of the message features as active
|
|
317
|
+
for feature_id in message_features:
|
|
318
|
+
# Get the feature to check which agent is working on it
|
|
319
|
+
try:
|
|
320
|
+
# Try features graph first
|
|
321
|
+
feature = manager.features_graph.get(feature_id)
|
|
322
|
+
if not feature:
|
|
323
|
+
# Try bugs graph
|
|
324
|
+
feature = manager.bugs_graph.get(feature_id)
|
|
325
|
+
|
|
326
|
+
if (
|
|
327
|
+
feature
|
|
328
|
+
and hasattr(feature, "agent_assigned")
|
|
329
|
+
and feature.agent_assigned
|
|
330
|
+
):
|
|
331
|
+
# Find active session for this agent
|
|
332
|
+
session = manager.get_active_session(
|
|
333
|
+
agent=feature.agent_assigned
|
|
334
|
+
)
|
|
335
|
+
if session:
|
|
336
|
+
break
|
|
337
|
+
except Exception:
|
|
338
|
+
pass
|
|
339
|
+
except Exception:
|
|
340
|
+
pass
|
|
341
|
+
|
|
342
|
+
# Fallback to any active session if we couldn't match by feature
|
|
343
|
+
if not session:
|
|
344
|
+
session = get_active_session(graph_dir)
|
|
345
|
+
|
|
292
346
|
if session:
|
|
293
347
|
return {
|
|
294
348
|
"session_id": session.id,
|