ai-agent-rules 0.11.0__py3-none-any.whl → 0.15.8__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.
Potentially problematic release.
This version of ai-agent-rules might be problematic. Click here for more details.
- {ai_agent_rules-0.11.0.dist-info → ai_agent_rules-0.15.8.dist-info}/METADATA +91 -6
- {ai_agent_rules-0.11.0.dist-info → ai_agent_rules-0.15.8.dist-info}/RECORD +27 -16
- ai_rules/agents/claude.py +3 -1
- ai_rules/agents/cursor.py +70 -0
- ai_rules/agents/goose.py +4 -1
- ai_rules/bootstrap/__init__.py +8 -4
- ai_rules/bootstrap/installer.py +95 -23
- ai_rules/bootstrap/updater.py +183 -43
- ai_rules/cli.py +360 -42
- ai_rules/config/AGENTS.md +5 -4
- ai_rules/config/claude/CLAUDE.md +1 -0
- ai_rules/config/claude/commands/agents-md.md +422 -0
- ai_rules/config/claude/settings.json +7 -4
- ai_rules/config/cursor/keybindings.json +14 -0
- ai_rules/config/cursor/settings.json +81 -0
- ai_rules/config/goose/.goosehints +1 -0
- ai_rules/config/profiles/default.yaml +6 -0
- ai_rules/config/profiles/work.yaml +11 -0
- ai_rules/config.py +55 -46
- ai_rules/mcp.py +2 -3
- ai_rules/profiles.py +187 -0
- ai_rules/state.py +47 -0
- ai_rules/utils.py +35 -0
- {ai_agent_rules-0.11.0.dist-info → ai_agent_rules-0.15.8.dist-info}/WHEEL +0 -0
- {ai_agent_rules-0.11.0.dist-info → ai_agent_rules-0.15.8.dist-info}/entry_points.txt +0 -0
- {ai_agent_rules-0.11.0.dist-info → ai_agent_rules-0.15.8.dist-info}/licenses/LICENSE +0 -0
- {ai_agent_rules-0.11.0.dist-info → ai_agent_rules-0.15.8.dist-info}/top_level.txt +0 -0
ai_rules/cli.py
CHANGED
|
@@ -22,6 +22,7 @@ from rich.table import Table
|
|
|
22
22
|
|
|
23
23
|
from ai_rules.agents.base import Agent
|
|
24
24
|
from ai_rules.agents.claude import ClaudeAgent
|
|
25
|
+
from ai_rules.agents.cursor import CursorAgent
|
|
25
26
|
from ai_rules.agents.goose import GooseAgent
|
|
26
27
|
from ai_rules.agents.shared import SharedAgent
|
|
27
28
|
from ai_rules.config import (
|
|
@@ -115,6 +116,7 @@ def get_agents(config_dir: Path, config: Config) -> list[Agent]:
|
|
|
115
116
|
"""
|
|
116
117
|
return [
|
|
117
118
|
ClaudeAgent(config_dir, config),
|
|
119
|
+
CursorAgent(config_dir, config),
|
|
118
120
|
GooseAgent(config_dir, config),
|
|
119
121
|
SharedAgent(config_dir, config),
|
|
120
122
|
]
|
|
@@ -132,6 +134,18 @@ def complete_agents(
|
|
|
132
134
|
return [CompletionItem(aid) for aid in agent_ids if aid.startswith(incomplete)]
|
|
133
135
|
|
|
134
136
|
|
|
137
|
+
def complete_profiles(
|
|
138
|
+
ctx: click.Context, param: click.Parameter, incomplete: str
|
|
139
|
+
) -> list[CompletionItem]:
|
|
140
|
+
"""Dynamically complete profile names for --profile option."""
|
|
141
|
+
from ai_rules.profiles import ProfileLoader
|
|
142
|
+
|
|
143
|
+
loader = ProfileLoader()
|
|
144
|
+
profiles = loader.list_profiles()
|
|
145
|
+
|
|
146
|
+
return [CompletionItem(p) for p in profiles if p.startswith(incomplete)]
|
|
147
|
+
|
|
148
|
+
|
|
135
149
|
def detect_old_config_symlinks() -> list[tuple[Path, Path]]:
|
|
136
150
|
"""Detect symlinks pointing to old config/ location.
|
|
137
151
|
|
|
@@ -438,6 +452,20 @@ def version_callback(ctx: click.Context, param: click.Parameter, value: bool) ->
|
|
|
438
452
|
|
|
439
453
|
console.print(f"ai-rules, version {__version__}")
|
|
440
454
|
|
|
455
|
+
try:
|
|
456
|
+
from ai_rules.bootstrap import get_tool_version, is_command_available
|
|
457
|
+
|
|
458
|
+
if is_command_available("claude-statusline"):
|
|
459
|
+
statusline_version = get_tool_version("claude-code-statusline")
|
|
460
|
+
if statusline_version:
|
|
461
|
+
console.print(f"statusline, version {statusline_version}")
|
|
462
|
+
else:
|
|
463
|
+
console.print(
|
|
464
|
+
"statusline, version [dim](installed, version unknown)[/dim]"
|
|
465
|
+
)
|
|
466
|
+
except Exception as e:
|
|
467
|
+
logger.debug(f"Failed to get statusline version: {e}")
|
|
468
|
+
|
|
441
469
|
try:
|
|
442
470
|
from ai_rules.bootstrap import check_tool_updates, get_tool_by_id
|
|
443
471
|
|
|
@@ -592,17 +620,26 @@ def install_user_symlinks(
|
|
|
592
620
|
|
|
593
621
|
|
|
594
622
|
@main.command()
|
|
623
|
+
@click.option("--github", is_flag=True, help="Install from GitHub instead of PyPI")
|
|
595
624
|
@click.option("--force", is_flag=True, help="Skip confirmation prompts")
|
|
596
625
|
@click.option("--dry-run", is_flag=True, help="Show what would be done")
|
|
597
626
|
@click.option("--skip-symlinks", is_flag=True, help="Skip symlink installation step")
|
|
598
627
|
@click.option("--skip-completions", is_flag=True, help="Skip shell completion setup")
|
|
628
|
+
@click.option(
|
|
629
|
+
"--profile",
|
|
630
|
+
default=None,
|
|
631
|
+
shell_complete=complete_profiles,
|
|
632
|
+
help="Profile to use (default: 'default')",
|
|
633
|
+
)
|
|
599
634
|
@click.pass_context
|
|
600
635
|
def setup(
|
|
601
636
|
ctx: click.Context,
|
|
637
|
+
github: bool,
|
|
602
638
|
force: bool,
|
|
603
639
|
dry_run: bool,
|
|
604
640
|
skip_symlinks: bool,
|
|
605
641
|
skip_completions: bool,
|
|
642
|
+
profile: str | None,
|
|
606
643
|
) -> None:
|
|
607
644
|
"""One-time setup: install symlinks and make ai-rules available system-wide.
|
|
608
645
|
|
|
@@ -616,43 +653,96 @@ def setup(
|
|
|
616
653
|
get_tool_config_dir,
|
|
617
654
|
install_tool,
|
|
618
655
|
)
|
|
656
|
+
from ai_rules.bootstrap.updater import (
|
|
657
|
+
check_tool_updates,
|
|
658
|
+
get_tool_by_id,
|
|
659
|
+
perform_tool_upgrade,
|
|
660
|
+
)
|
|
619
661
|
|
|
620
|
-
|
|
662
|
+
console.print("[bold cyan]Step 1/3: Install ai-rules system-wide[/bold cyan]")
|
|
663
|
+
console.print("This allows you to run 'ai-rules' from any directory.\n")
|
|
664
|
+
|
|
665
|
+
statusline_result, statusline_message = ensure_statusline_installed(
|
|
666
|
+
dry_run=dry_run, from_github=github
|
|
667
|
+
)
|
|
621
668
|
if statusline_result == "installed":
|
|
622
|
-
|
|
669
|
+
if dry_run and statusline_message:
|
|
670
|
+
console.print(f"[dim]{statusline_message}[/dim]")
|
|
671
|
+
else:
|
|
672
|
+
console.print("[green]✓[/green] Installed claude-statusline")
|
|
673
|
+
elif statusline_result == "upgraded":
|
|
674
|
+
console.print(
|
|
675
|
+
f"[green]✓[/green] Upgraded claude-statusline ({statusline_message})"
|
|
676
|
+
)
|
|
677
|
+
elif statusline_result == "upgrade_available":
|
|
678
|
+
console.print(f"[dim]{statusline_message}[/dim]")
|
|
623
679
|
elif statusline_result == "failed":
|
|
624
680
|
console.print(
|
|
625
|
-
"[yellow]⚠[/yellow] Failed to install claude-statusline (continuing anyway)
|
|
681
|
+
"[yellow]⚠[/yellow] Failed to install claude-statusline (continuing anyway)"
|
|
626
682
|
)
|
|
627
683
|
|
|
628
|
-
|
|
629
|
-
|
|
684
|
+
ai_rules_tool = get_tool_by_id("ai-rules")
|
|
685
|
+
tool_install_success = False
|
|
630
686
|
|
|
631
|
-
if
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
687
|
+
if ai_rules_tool and ai_rules_tool.is_installed():
|
|
688
|
+
try:
|
|
689
|
+
update_info = check_tool_updates(ai_rules_tool, timeout=10)
|
|
690
|
+
if update_info and update_info.has_update:
|
|
691
|
+
if dry_run:
|
|
692
|
+
console.print(
|
|
693
|
+
f"[dim]Would upgrade ai-rules {update_info.current_version} → {update_info.latest_version}[/dim]"
|
|
694
|
+
)
|
|
695
|
+
tool_install_success = True
|
|
696
|
+
else:
|
|
697
|
+
if not force and not Confirm.ask(
|
|
698
|
+
f"Upgrade ai-rules {update_info.current_version} → {update_info.latest_version}?",
|
|
699
|
+
default=True,
|
|
700
|
+
):
|
|
701
|
+
console.print("[yellow]Skipped ai-rules upgrade[/yellow]")
|
|
702
|
+
tool_install_success = True
|
|
703
|
+
else:
|
|
704
|
+
success, msg, _ = perform_tool_upgrade(ai_rules_tool)
|
|
705
|
+
if success:
|
|
706
|
+
console.print(
|
|
707
|
+
f"[green]✓[/green] Upgraded ai-rules ({update_info.current_version} → {update_info.latest_version})"
|
|
708
|
+
)
|
|
709
|
+
tool_install_success = True
|
|
710
|
+
else:
|
|
711
|
+
console.print(
|
|
712
|
+
"[red]Error:[/red] Failed to upgrade ai-rules"
|
|
713
|
+
)
|
|
714
|
+
else:
|
|
715
|
+
tool_install_success = True
|
|
716
|
+
except Exception:
|
|
717
|
+
pass
|
|
718
|
+
|
|
719
|
+
if not tool_install_success:
|
|
720
|
+
if not force and not dry_run:
|
|
721
|
+
if not Confirm.ask("Install ai-rules permanently?", default=True):
|
|
722
|
+
console.print(
|
|
723
|
+
"\n[yellow]Skipped.[/yellow] You can still run via: uvx ai-rules <command>"
|
|
724
|
+
)
|
|
725
|
+
return
|
|
726
|
+
|
|
727
|
+
try:
|
|
728
|
+
success, message = install_tool(
|
|
729
|
+
"ai-agent-rules", from_github=github, force=force, dry_run=dry_run
|
|
635
730
|
)
|
|
636
|
-
return
|
|
637
731
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
console.print("\n[
|
|
651
|
-
console.print(" uv tool install ai-agent-rules")
|
|
732
|
+
if dry_run:
|
|
733
|
+
console.print(f"[dim]{message}[/dim]")
|
|
734
|
+
tool_install_success = True
|
|
735
|
+
elif success:
|
|
736
|
+
console.print("[green]✓[/green] Tool installed successfully")
|
|
737
|
+
tool_install_success = True
|
|
738
|
+
else:
|
|
739
|
+
console.print(f"\n[red]Error:[/red] {message}")
|
|
740
|
+
console.print("\n[yellow]Manual installation:[/yellow]")
|
|
741
|
+
console.print(" uv tool install ai-agent-rules")
|
|
742
|
+
return
|
|
743
|
+
except Exception as e:
|
|
744
|
+
console.print(f"\n[red]Error:[/red] {e}")
|
|
652
745
|
return
|
|
653
|
-
except Exception as e:
|
|
654
|
-
console.print(f"\n[red]Error:[/red] {e}")
|
|
655
|
-
return
|
|
656
746
|
|
|
657
747
|
if not skip_symlinks:
|
|
658
748
|
console.print(
|
|
@@ -685,21 +775,31 @@ def setup(
|
|
|
685
775
|
rebuild_cache=False,
|
|
686
776
|
agents=None,
|
|
687
777
|
skip_completions=True,
|
|
778
|
+
profile=profile,
|
|
688
779
|
config_dir_override=config_dir_override,
|
|
689
780
|
)
|
|
690
781
|
|
|
691
782
|
if not skip_completions:
|
|
692
783
|
from ai_rules.completions import (
|
|
693
784
|
detect_shell,
|
|
785
|
+
find_config_file,
|
|
694
786
|
get_supported_shells,
|
|
695
787
|
install_completion,
|
|
788
|
+
is_completion_installed,
|
|
696
789
|
)
|
|
697
790
|
|
|
698
791
|
console.print("\n[bold cyan]Step 3/3: Shell completion setup[/bold cyan]\n")
|
|
699
792
|
|
|
700
793
|
shell = detect_shell()
|
|
701
794
|
if shell:
|
|
702
|
-
|
|
795
|
+
config_path = find_config_file(shell)
|
|
796
|
+
if config_path and is_completion_installed(config_path):
|
|
797
|
+
console.print(f"[green]✓[/green] {shell} completion already installed")
|
|
798
|
+
elif (
|
|
799
|
+
force
|
|
800
|
+
or dry_run
|
|
801
|
+
or Confirm.ask(f"Install {shell} tab completion?", default=True)
|
|
802
|
+
):
|
|
703
803
|
success, msg = install_completion(shell, dry_run=dry_run)
|
|
704
804
|
if success:
|
|
705
805
|
console.print(f"[green]✓[/green] {msg}")
|
|
@@ -711,8 +811,11 @@ def setup(
|
|
|
711
811
|
f"[dim]Shell completion not available for your shell (only {supported} supported)[/dim]"
|
|
712
812
|
)
|
|
713
813
|
|
|
714
|
-
|
|
715
|
-
|
|
814
|
+
if dry_run:
|
|
815
|
+
console.print("\n[dim]Dry run complete - no changes were made.[/dim]")
|
|
816
|
+
else:
|
|
817
|
+
console.print("\n[green]✓ Setup complete![/green]")
|
|
818
|
+
console.print("You can now run [bold]ai-rules[/bold] from anywhere.")
|
|
716
819
|
|
|
717
820
|
|
|
718
821
|
@main.command()
|
|
@@ -733,6 +836,12 @@ def setup(
|
|
|
733
836
|
is_flag=True,
|
|
734
837
|
help="Skip shell completion installation",
|
|
735
838
|
)
|
|
839
|
+
@click.option(
|
|
840
|
+
"--profile",
|
|
841
|
+
default=None,
|
|
842
|
+
shell_complete=complete_profiles,
|
|
843
|
+
help="Profile to use (default: 'default' for backward compatibility)",
|
|
844
|
+
)
|
|
736
845
|
@click.option(
|
|
737
846
|
"--config-dir",
|
|
738
847
|
"config_dir_override",
|
|
@@ -745,14 +854,18 @@ def install(
|
|
|
745
854
|
rebuild_cache: bool,
|
|
746
855
|
agents: str | None,
|
|
747
856
|
skip_completions: bool,
|
|
857
|
+
profile: str | None,
|
|
748
858
|
config_dir_override: str | None = None,
|
|
749
859
|
) -> None:
|
|
750
860
|
"""Install AI agent configs via symlinks."""
|
|
751
861
|
from ai_rules.bootstrap import ensure_statusline_installed
|
|
752
862
|
|
|
753
|
-
statusline_result = ensure_statusline_installed(dry_run=dry_run)
|
|
863
|
+
statusline_result, statusline_message = ensure_statusline_installed(dry_run=dry_run)
|
|
754
864
|
if statusline_result == "installed":
|
|
755
|
-
|
|
865
|
+
if dry_run and statusline_message:
|
|
866
|
+
console.print(f"[dim]{statusline_message}[/dim]\n")
|
|
867
|
+
else:
|
|
868
|
+
console.print("[green]✓[/green] Installed claude-statusline\n")
|
|
756
869
|
elif statusline_result == "failed":
|
|
757
870
|
console.print(
|
|
758
871
|
"[yellow]⚠[/yellow] Failed to install claude-statusline (continuing anyway)\n"
|
|
@@ -766,7 +879,20 @@ def install(
|
|
|
766
879
|
else:
|
|
767
880
|
config_dir = get_config_dir()
|
|
768
881
|
|
|
769
|
-
|
|
882
|
+
from ai_rules.profiles import ProfileNotFoundError
|
|
883
|
+
from ai_rules.state import set_active_profile
|
|
884
|
+
|
|
885
|
+
try:
|
|
886
|
+
config = Config.load(profile=profile)
|
|
887
|
+
except ProfileNotFoundError as e:
|
|
888
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
889
|
+
sys.exit(1)
|
|
890
|
+
|
|
891
|
+
if not dry_run:
|
|
892
|
+
set_active_profile(profile or "default")
|
|
893
|
+
|
|
894
|
+
if profile and profile != "default":
|
|
895
|
+
console.print(f"[dim]Using profile: {profile}[/dim]\n")
|
|
770
896
|
|
|
771
897
|
if rebuild_cache and not dry_run:
|
|
772
898
|
import shutil
|
|
@@ -783,6 +909,12 @@ def install(
|
|
|
783
909
|
"claude", claude_settings, force_rebuild=rebuild_cache
|
|
784
910
|
)
|
|
785
911
|
|
|
912
|
+
cursor_settings = config_dir / "cursor" / "settings.json"
|
|
913
|
+
if cursor_settings.exists():
|
|
914
|
+
config.build_merged_settings(
|
|
915
|
+
"cursor", cursor_settings, force_rebuild=rebuild_cache
|
|
916
|
+
)
|
|
917
|
+
|
|
786
918
|
goose_settings = config_dir / "goose" / "config.yaml"
|
|
787
919
|
if goose_settings.exists():
|
|
788
920
|
config.build_merged_settings(
|
|
@@ -985,6 +1117,8 @@ def _display_symlink_status(
|
|
|
985
1117
|
)
|
|
986
1118
|
def status(agents: str | None) -> None:
|
|
987
1119
|
"""Check status of AI agent symlinks."""
|
|
1120
|
+
from ai_rules.state import get_active_profile
|
|
1121
|
+
|
|
988
1122
|
config_dir = get_config_dir()
|
|
989
1123
|
config = Config.load()
|
|
990
1124
|
all_agents = get_agents(config_dir, config)
|
|
@@ -992,6 +1126,10 @@ def status(agents: str | None) -> None:
|
|
|
992
1126
|
|
|
993
1127
|
console.print("[bold]AI Rules Status[/bold]\n")
|
|
994
1128
|
|
|
1129
|
+
active_profile = get_active_profile()
|
|
1130
|
+
if active_profile:
|
|
1131
|
+
console.print(f"[dim]Profile: {active_profile}[/dim]\n")
|
|
1132
|
+
|
|
995
1133
|
all_correct = True
|
|
996
1134
|
|
|
997
1135
|
console.print("[bold cyan]User-Level Configuration[/bold cyan]\n")
|
|
@@ -1299,7 +1437,7 @@ def upgrade(check: bool, force: bool, skip_install: bool, only: str | None) -> N
|
|
|
1299
1437
|
from ai_rules.bootstrap import (
|
|
1300
1438
|
UPDATABLE_TOOLS,
|
|
1301
1439
|
check_tool_updates,
|
|
1302
|
-
|
|
1440
|
+
perform_tool_upgrade,
|
|
1303
1441
|
)
|
|
1304
1442
|
|
|
1305
1443
|
tools = [t for t in UPDATABLE_TOOLS if only is None or t.tool_id == only]
|
|
@@ -1348,13 +1486,12 @@ def upgrade(check: bool, force: bool, skip_install: bool, only: str | None) -> N
|
|
|
1348
1486
|
console.print("[green]✓[/green] All tools are up to date!")
|
|
1349
1487
|
return
|
|
1350
1488
|
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
)
|
|
1489
|
+
for tool, update_info in tool_updates:
|
|
1490
|
+
if update_info.has_update:
|
|
1491
|
+
console.print(
|
|
1492
|
+
f"[cyan]Update available for {tool.display_name}:[/cyan] "
|
|
1493
|
+
f"{update_info.current_version} → {update_info.latest_version}"
|
|
1494
|
+
)
|
|
1358
1495
|
|
|
1359
1496
|
if check:
|
|
1360
1497
|
if tool_updates:
|
|
@@ -1374,7 +1511,7 @@ def upgrade(check: bool, force: bool, skip_install: bool, only: str | None) -> N
|
|
|
1374
1511
|
for tool, update_info in tool_updates:
|
|
1375
1512
|
with console.status(f"Upgrading {tool.display_name}..."):
|
|
1376
1513
|
try:
|
|
1377
|
-
success, msg, was_upgraded =
|
|
1514
|
+
success, msg, was_upgraded = perform_tool_upgrade(tool)
|
|
1378
1515
|
except Exception as e:
|
|
1379
1516
|
console.print(
|
|
1380
1517
|
f"\n[red]Error:[/red] {tool.display_name} upgrade failed: {e}"
|
|
@@ -1453,6 +1590,59 @@ def upgrade(check: bool, force: bool, skip_install: bool, only: str | None) -> N
|
|
|
1453
1590
|
console.print("[dim]Restart your terminal if the command doesn't work[/dim]")
|
|
1454
1591
|
|
|
1455
1592
|
|
|
1593
|
+
@main.command()
|
|
1594
|
+
def info() -> None:
|
|
1595
|
+
"""Show installation method and version info for ai-rules tools.
|
|
1596
|
+
|
|
1597
|
+
Displays how each tool was installed (PyPI, GitHub, or local development)
|
|
1598
|
+
along with current versions and update availability.
|
|
1599
|
+
"""
|
|
1600
|
+
from rich.table import Table
|
|
1601
|
+
|
|
1602
|
+
from ai_rules.bootstrap import (
|
|
1603
|
+
UPDATABLE_TOOLS,
|
|
1604
|
+
check_tool_updates,
|
|
1605
|
+
get_tool_source,
|
|
1606
|
+
)
|
|
1607
|
+
|
|
1608
|
+
table = Table(title="AI Rules Installation Info", show_header=True)
|
|
1609
|
+
table.add_column("Tool", style="cyan")
|
|
1610
|
+
table.add_column("Source", style="bold")
|
|
1611
|
+
table.add_column("Version")
|
|
1612
|
+
table.add_column("Update")
|
|
1613
|
+
|
|
1614
|
+
has_updates = False
|
|
1615
|
+
|
|
1616
|
+
for tool in UPDATABLE_TOOLS:
|
|
1617
|
+
tool_name = tool.display_name
|
|
1618
|
+
|
|
1619
|
+
if not tool.is_installed():
|
|
1620
|
+
table.add_row(tool_name, "-", "-", "[dim](not installed)[/dim]")
|
|
1621
|
+
continue
|
|
1622
|
+
|
|
1623
|
+
source = get_tool_source(tool.package_name)
|
|
1624
|
+
source_display = source.name.lower() if source else "[dim]unknown[/dim]"
|
|
1625
|
+
|
|
1626
|
+
version = tool.get_version()
|
|
1627
|
+
version_display = version if version else "[dim]unknown[/dim]"
|
|
1628
|
+
|
|
1629
|
+
update_display = "-"
|
|
1630
|
+
try:
|
|
1631
|
+
update_info = check_tool_updates(tool, timeout=5)
|
|
1632
|
+
if update_info and update_info.has_update:
|
|
1633
|
+
update_display = f"[cyan]{update_info.latest_version} available[/cyan]"
|
|
1634
|
+
has_updates = True
|
|
1635
|
+
except Exception:
|
|
1636
|
+
update_display = "[dim](check failed)[/dim]"
|
|
1637
|
+
|
|
1638
|
+
table.add_row(tool_name, source_display, version_display, update_display)
|
|
1639
|
+
|
|
1640
|
+
console.print(table)
|
|
1641
|
+
|
|
1642
|
+
if has_updates:
|
|
1643
|
+
console.print("\n[dim]Run 'ai-rules upgrade' to install updates.[/dim]")
|
|
1644
|
+
|
|
1645
|
+
|
|
1456
1646
|
@main.command()
|
|
1457
1647
|
@click.option(
|
|
1458
1648
|
"--agents",
|
|
@@ -2181,6 +2371,134 @@ def config_init() -> None:
|
|
|
2181
2371
|
console.print("[dim]Configuration not saved[/dim]")
|
|
2182
2372
|
|
|
2183
2373
|
|
|
2374
|
+
@main.group()
|
|
2375
|
+
def profile() -> None:
|
|
2376
|
+
"""Manage configuration profiles."""
|
|
2377
|
+
pass
|
|
2378
|
+
|
|
2379
|
+
|
|
2380
|
+
@profile.command("list")
|
|
2381
|
+
def profile_list() -> None:
|
|
2382
|
+
"""List available profiles."""
|
|
2383
|
+
from rich.table import Table
|
|
2384
|
+
|
|
2385
|
+
from ai_rules.profiles import ProfileLoader
|
|
2386
|
+
|
|
2387
|
+
loader = ProfileLoader()
|
|
2388
|
+
profiles = loader.list_profiles()
|
|
2389
|
+
|
|
2390
|
+
table = Table(title="Available Profiles", show_header=True)
|
|
2391
|
+
table.add_column("Name", style="cyan")
|
|
2392
|
+
table.add_column("Description")
|
|
2393
|
+
table.add_column("Extends")
|
|
2394
|
+
|
|
2395
|
+
for name in profiles:
|
|
2396
|
+
try:
|
|
2397
|
+
info = loader.get_profile_info(name)
|
|
2398
|
+
desc = info.get("description", "")
|
|
2399
|
+
extends = info.get("extends") or "-"
|
|
2400
|
+
table.add_row(name, desc, extends)
|
|
2401
|
+
except Exception:
|
|
2402
|
+
table.add_row(name, "[dim]Error loading[/dim]", "-")
|
|
2403
|
+
|
|
2404
|
+
console.print(table)
|
|
2405
|
+
|
|
2406
|
+
|
|
2407
|
+
@profile.command("show")
|
|
2408
|
+
@click.argument("name")
|
|
2409
|
+
@click.option(
|
|
2410
|
+
"--resolved", is_flag=True, help="Show resolved profile with inheritance applied"
|
|
2411
|
+
)
|
|
2412
|
+
def profile_show(name: str, resolved: bool) -> None:
|
|
2413
|
+
"""Show profile details."""
|
|
2414
|
+
from ai_rules.profiles import (
|
|
2415
|
+
CircularInheritanceError,
|
|
2416
|
+
ProfileLoader,
|
|
2417
|
+
ProfileNotFoundError,
|
|
2418
|
+
)
|
|
2419
|
+
|
|
2420
|
+
loader = ProfileLoader()
|
|
2421
|
+
|
|
2422
|
+
try:
|
|
2423
|
+
if resolved:
|
|
2424
|
+
profile = loader.load_profile(name)
|
|
2425
|
+
console.print(f"[bold]Profile: {profile.name}[/bold] (resolved)")
|
|
2426
|
+
console.print(f"[dim]Description:[/dim] {profile.description}")
|
|
2427
|
+
if profile.extends:
|
|
2428
|
+
console.print(f"[dim]Extends:[/dim] {profile.extends}")
|
|
2429
|
+
|
|
2430
|
+
if profile.settings_overrides:
|
|
2431
|
+
console.print("\n[bold]Settings Overrides:[/bold]")
|
|
2432
|
+
for agent, overrides in sorted(profile.settings_overrides.items()):
|
|
2433
|
+
console.print(f" [cyan]{agent}:[/cyan]")
|
|
2434
|
+
for key, value in sorted(overrides.items()):
|
|
2435
|
+
console.print(f" {key}: {value}")
|
|
2436
|
+
|
|
2437
|
+
if profile.exclude_symlinks:
|
|
2438
|
+
console.print("\n[bold]Exclude Symlinks:[/bold]")
|
|
2439
|
+
for pattern in sorted(profile.exclude_symlinks):
|
|
2440
|
+
console.print(f" - {pattern}")
|
|
2441
|
+
|
|
2442
|
+
if profile.mcp_overrides:
|
|
2443
|
+
console.print("\n[bold]MCP Overrides:[/bold]")
|
|
2444
|
+
for mcp, overrides in sorted(profile.mcp_overrides.items()):
|
|
2445
|
+
console.print(f" [cyan]{mcp}:[/cyan]")
|
|
2446
|
+
for key, value in sorted(overrides.items()):
|
|
2447
|
+
console.print(f" {key}: {value}")
|
|
2448
|
+
else:
|
|
2449
|
+
import yaml
|
|
2450
|
+
|
|
2451
|
+
info = loader.get_profile_info(name)
|
|
2452
|
+
console.print(f"[bold]Profile: {info.get('name', name)}[/bold]")
|
|
2453
|
+
console.print(yaml.dump(info, default_flow_style=False, sort_keys=False))
|
|
2454
|
+
|
|
2455
|
+
except ProfileNotFoundError as e:
|
|
2456
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
2457
|
+
sys.exit(1)
|
|
2458
|
+
except CircularInheritanceError as e:
|
|
2459
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
2460
|
+
sys.exit(1)
|
|
2461
|
+
|
|
2462
|
+
|
|
2463
|
+
@profile.command("current")
|
|
2464
|
+
def profile_current() -> None:
|
|
2465
|
+
"""Show currently active profile."""
|
|
2466
|
+
from ai_rules.state import get_active_profile
|
|
2467
|
+
|
|
2468
|
+
active = get_active_profile()
|
|
2469
|
+
if active:
|
|
2470
|
+
console.print(f"Active profile: [cyan]{active}[/cyan]")
|
|
2471
|
+
else:
|
|
2472
|
+
console.print("[dim]No profile set (using default)[/dim]")
|
|
2473
|
+
|
|
2474
|
+
|
|
2475
|
+
@profile.command("switch")
|
|
2476
|
+
@click.argument("name", shell_complete=complete_profiles)
|
|
2477
|
+
@click.pass_context
|
|
2478
|
+
def profile_switch(ctx: click.Context, name: str) -> None:
|
|
2479
|
+
"""Switch to a different profile."""
|
|
2480
|
+
from ai_rules.profiles import ProfileLoader, ProfileNotFoundError
|
|
2481
|
+
|
|
2482
|
+
loader = ProfileLoader()
|
|
2483
|
+
try:
|
|
2484
|
+
loader.load_profile(name)
|
|
2485
|
+
except ProfileNotFoundError as e:
|
|
2486
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
2487
|
+
sys.exit(1)
|
|
2488
|
+
|
|
2489
|
+
console.print(f"Switching to profile: [cyan]{name}[/cyan]")
|
|
2490
|
+
ctx.invoke(
|
|
2491
|
+
install,
|
|
2492
|
+
profile=name,
|
|
2493
|
+
rebuild_cache=True,
|
|
2494
|
+
force=True,
|
|
2495
|
+
skip_completions=True,
|
|
2496
|
+
agents=None,
|
|
2497
|
+
dry_run=False,
|
|
2498
|
+
config_dir_override=None,
|
|
2499
|
+
)
|
|
2500
|
+
|
|
2501
|
+
|
|
2184
2502
|
@main.group()
|
|
2185
2503
|
def completions() -> None:
|
|
2186
2504
|
"""Manage shell tab completion."""
|
ai_rules/config/AGENTS.md
CHANGED
|
@@ -187,15 +187,16 @@ def test_register_user_calls_hash_password():
|
|
|
187
187
|
## Priority 3: Style & Formatting Guidelines
|
|
188
188
|
|
|
189
189
|
### Code Comment Standards
|
|
190
|
-
**Rule:** ONLY add comments explaining WHY, NOT WHAT.
|
|
190
|
+
**Rule:** ONLY add comments explaining WHY, NOT WHAT. Never add code comments that simply restate what the code does.
|
|
191
191
|
|
|
192
|
-
**Prohibited (WHAT):**
|
|
192
|
+
**Prohibited - Comments that restate code behavior (WHAT):**
|
|
193
193
|
```python
|
|
194
194
|
counter += 1 # Increment counter by 1
|
|
195
195
|
for user in users: # Loop through users
|
|
196
|
+
result = calculate_total(items) # Calculate the total from items
|
|
196
197
|
```
|
|
197
198
|
|
|
198
|
-
**Required (WHY):**
|
|
199
|
+
**Required - Comments that explain reasoning or context (WHY):**
|
|
199
200
|
```python
|
|
200
201
|
# Use exponential backoff to avoid Stripe API rate limiting
|
|
201
202
|
delay = 2 ** retry_count
|
|
@@ -207,7 +208,7 @@ results.sort(key=lambda x: x.timestamp, reverse=True)
|
|
|
207
208
|
sanitized = validate_and_sanitize(user_input)
|
|
208
209
|
```
|
|
209
210
|
|
|
210
|
-
**Why:** Code should be self-explanatory via naming. Comments
|
|
211
|
+
**Why:** Code should be self-explanatory via naming. Comments that restate what code does become outdated and add noise. Comments explaining WHY preserve critical context (business rules, security requirements, performance considerations) not visible in the code itself.
|
|
211
212
|
|
|
212
213
|
### Whitespace Standards
|
|
213
214
|
**Rule:** Remove ALL trailing whitespace | Ensure blank lines have NO whitespace | Preserve existing newlines | All files end with single newline
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@~/AGENTS.md
|