@softspark/ai-toolkit 4.10.0 → 4.11.0
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.
- package/AGENTS.md +135 -0
- package/CHANGELOG.md +28 -0
- package/README.md +13 -12
- package/app/.claude-plugin/plugin.json +1 -1
- package/app/hooks/loop-guard.sh +0 -1
- package/benchmarks/ecosystem-doctor-snapshot.json +26 -19
- package/bin/ai-toolkit.js +1 -1
- package/kb/procedures/maintenance-sop.md +5 -3
- package/kb/reference/architecture-overview.md +7 -6
- package/kb/reference/codex-cli-compatibility.md +10 -4
- package/kb/reference/extension-api.md +5 -5
- package/kb/reference/global-install-model.md +18 -3
- package/kb/reference/integrations.md +16 -17
- package/kb/reference/language-rules.md +35 -14
- package/kb/reference/supported-tools-registry.md +8 -8
- package/llms-full.txt +104 -60
- package/manifest.json +1 -1
- package/package.json +3 -1
- package/scripts/ecosystem_tools.json +6 -2
- package/scripts/generate_copilot.py +6 -0
- package/scripts/generate_language_rules_skills.py +4 -2
- package/scripts/install.py +3 -3
- package/scripts/install_steps/ai_tools.py +102 -29
- package/scripts/install_steps/markers.py +132 -14
- package/scripts/remove_rule.py +15 -4
|
@@ -352,6 +352,39 @@ def inject_with_rules(
|
|
|
352
352
|
print(f" Updated: {target_file}")
|
|
353
353
|
|
|
354
354
|
|
|
355
|
+
def _inject_text_section(target_file: Path, section: str, text: str) -> None:
|
|
356
|
+
"""Inject generated text into one marker section without touching others."""
|
|
357
|
+
target_file.parent.mkdir(parents=True, exist_ok=True)
|
|
358
|
+
existing = target_file.read_text(encoding="utf-8") if target_file.is_file() else ""
|
|
359
|
+
existing = _trim_trailing_blanks(_strip_section(existing, section))
|
|
360
|
+
|
|
361
|
+
parts: list[str] = []
|
|
362
|
+
if existing.strip():
|
|
363
|
+
parts.append(existing)
|
|
364
|
+
parts.append("")
|
|
365
|
+
parts.extend([
|
|
366
|
+
f"<!-- TOOLKIT:{section} START -->",
|
|
367
|
+
"<!-- Auto-injected by ai-toolkit. Re-run to update. -->",
|
|
368
|
+
"",
|
|
369
|
+
text.rstrip("\n"),
|
|
370
|
+
"",
|
|
371
|
+
f"<!-- TOOLKIT:{section} END -->",
|
|
372
|
+
])
|
|
373
|
+
|
|
374
|
+
output = _collapse_blank_runs("\n".join(parts) + "\n").lstrip("\n")
|
|
375
|
+
target_file.write_text(output, encoding="utf-8")
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _install_copilot_agents_md(cwd: Path) -> None:
|
|
379
|
+
"""Emit root AGENTS.md for GitHub Copilot without clobbering other tools."""
|
|
380
|
+
generated = run_script("generate_agents_md.py", capture=True)
|
|
381
|
+
if not generated.strip():
|
|
382
|
+
print(" ERROR: generate_agents_md.py produced no output")
|
|
383
|
+
return
|
|
384
|
+
_inject_text_section(cwd / "AGENTS.md", "copilot-agents", generated)
|
|
385
|
+
print(" Updated: AGENTS.md (Copilot agent instructions)")
|
|
386
|
+
|
|
387
|
+
|
|
355
388
|
def run_script(script_name: str, *args: str, capture: bool = False) -> str:
|
|
356
389
|
"""Run a script from the scripts/ directory (prefers .py over .sh)."""
|
|
357
390
|
scripts_dir = toolkit_dir / "scripts"
|
|
@@ -628,17 +661,16 @@ def _apply_extends_config(cwd: Path, merged: dict) -> None:
|
|
|
628
661
|
|
|
629
662
|
|
|
630
663
|
def _inject_language_rules(cwd: Path, language_modules: list[str] | None) -> None:
|
|
631
|
-
"""
|
|
664
|
+
"""Install Claude language-rule entrypoints for a project.
|
|
632
665
|
|
|
633
666
|
Per-language rules (``app/rules/<lang>/``) are NOT injected here -- they
|
|
634
667
|
ship as ``<lang>-rules`` knowledge skills under ``app/skills/`` and load
|
|
635
|
-
contextually via the Agent Skills progressive-disclosure mechanism.
|
|
636
|
-
keeps ``CLAUDE.md`` small while ensuring language-specific guidance still
|
|
637
|
-
reaches Claude when relevant.
|
|
668
|
+
contextually via the Agent Skills progressive-disclosure mechanism.
|
|
638
669
|
|
|
639
|
-
Common rules are
|
|
640
|
-
|
|
641
|
-
|
|
670
|
+
Common rules are written as Claude Code path-scoped rules under
|
|
671
|
+
``.claude/rules/``. Current Claude Code guidance targets under 200 lines
|
|
672
|
+
per ``CLAUDE.md`` file; path-scoped rules keep startup context smaller
|
|
673
|
+
while still loading the rule bodies when project files are opened.
|
|
642
674
|
"""
|
|
643
675
|
if not language_modules:
|
|
644
676
|
return
|
|
@@ -648,6 +680,8 @@ def _inject_language_rules(cwd: Path, language_modules: list[str] | None) -> Non
|
|
|
648
680
|
if not common_dir.is_dir():
|
|
649
681
|
return
|
|
650
682
|
|
|
683
|
+
rule_files = _sync_claude_common_rules(cwd, common_dir)
|
|
684
|
+
|
|
651
685
|
# Detect requested per-language modules so we can name the linked skills
|
|
652
686
|
# in the marker block. The modules themselves are not inlined.
|
|
653
687
|
langs: list[str] = []
|
|
@@ -657,38 +691,30 @@ def _inject_language_rules(cwd: Path, language_modules: list[str] | None) -> Non
|
|
|
657
691
|
if name != "common":
|
|
658
692
|
langs.append(name)
|
|
659
693
|
|
|
660
|
-
# Inline full content of every common rule file, stripping YAML
|
|
661
|
-
# frontmatter so the resulting block reads as plain Markdown.
|
|
662
|
-
inlined: list[str] = []
|
|
663
|
-
for f in sorted(common_dir.glob("*.md")):
|
|
664
|
-
body = f.read_text(encoding="utf-8")
|
|
665
|
-
if body.startswith("---"):
|
|
666
|
-
end = body.find("\n---", 3)
|
|
667
|
-
if end != -1:
|
|
668
|
-
body = body[end + 4:].lstrip("\n")
|
|
669
|
-
inlined.append(body.rstrip())
|
|
670
|
-
|
|
671
694
|
lines: list[str] = ["# Language Rules", ""]
|
|
672
695
|
lines.append(
|
|
673
|
-
"Common
|
|
674
|
-
"
|
|
675
|
-
"
|
|
676
|
-
|
|
677
|
-
|
|
696
|
+
"Common ai-toolkit rules live in `.claude/rules/ai-toolkit-*.md` "
|
|
697
|
+
"with Claude Code `paths` frontmatter so they load when project files "
|
|
698
|
+
"are opened instead of expanding this CLAUDE.md at session startup."
|
|
699
|
+
)
|
|
700
|
+
if rule_files:
|
|
701
|
+
lines.append("")
|
|
702
|
+
lines.append("Common rule files: " + ", ".join(f"`{p}`" for p in rule_files) + ".")
|
|
703
|
+
lines.append("")
|
|
704
|
+
lines.append(
|
|
705
|
+
"Language-specific rules live in `<lang>-rules` knowledge skills "
|
|
706
|
+
"(e.g. `python-rules`, `typescript-rules`) and load automatically "
|
|
707
|
+
"when their triggers match -- you do not need to Read them manually."
|
|
678
708
|
)
|
|
679
709
|
if langs:
|
|
680
710
|
skill_names = ", ".join(f"`{l}-rules`" for l in langs)
|
|
681
711
|
lines.append("")
|
|
682
712
|
lines.append(f"Detected languages: {skill_names}.")
|
|
683
|
-
lines.append("")
|
|
684
|
-
lines.append("---")
|
|
685
|
-
lines.append("")
|
|
686
|
-
lines.extend(inlined)
|
|
687
713
|
|
|
688
714
|
# Write to temp file, then inject as a single named section so reruns are
|
|
689
715
|
# idempotent (existing block is replaced, not duplicated).
|
|
690
716
|
import tempfile
|
|
691
|
-
combined = "\n
|
|
717
|
+
combined = "\n".join(lines).rstrip() + "\n"
|
|
692
718
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".md", delete=False,
|
|
693
719
|
encoding="utf-8") as tmp:
|
|
694
720
|
tmp.write(combined)
|
|
@@ -705,6 +731,51 @@ def _inject_language_rules(cwd: Path, language_modules: list[str] | None) -> Non
|
|
|
705
731
|
tmp_path.unlink(missing_ok=True)
|
|
706
732
|
|
|
707
733
|
|
|
734
|
+
def _strip_rule_frontmatter(text: str) -> str:
|
|
735
|
+
if text.startswith("---"):
|
|
736
|
+
end = text.find("\n---", 3)
|
|
737
|
+
if end != -1:
|
|
738
|
+
return text[end + 4:].lstrip("\n")
|
|
739
|
+
return text
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
def _sync_claude_common_rules(cwd: Path, common_dir: Path) -> list[str]:
|
|
743
|
+
"""Write common ai-toolkit rules as Claude Code path-scoped rules.
|
|
744
|
+
|
|
745
|
+
Only ``ai-toolkit-*.md`` files are managed. User-authored files in
|
|
746
|
+
``.claude/rules/`` are preserved.
|
|
747
|
+
"""
|
|
748
|
+
rules_dir = cwd / ".claude" / "rules"
|
|
749
|
+
rules_dir.mkdir(parents=True, exist_ok=True)
|
|
750
|
+
|
|
751
|
+
source_files = sorted(common_dir.glob("*.md"))
|
|
752
|
+
expected = {f"ai-toolkit-{src.stem}.md" for src in source_files}
|
|
753
|
+
for stale in sorted(rules_dir.glob("ai-toolkit-*.md")):
|
|
754
|
+
if stale.name not in expected:
|
|
755
|
+
stale.unlink()
|
|
756
|
+
|
|
757
|
+
written: list[str] = []
|
|
758
|
+
for src in source_files:
|
|
759
|
+
body = _strip_rule_frontmatter(src.read_text(encoding="utf-8")).rstrip()
|
|
760
|
+
rel = Path(".claude") / "rules" / f"ai-toolkit-{src.stem}.md"
|
|
761
|
+
target = cwd / rel
|
|
762
|
+
target.write_text(
|
|
763
|
+
"\n".join([
|
|
764
|
+
"---",
|
|
765
|
+
"paths:",
|
|
766
|
+
' - "**/*"',
|
|
767
|
+
"---",
|
|
768
|
+
"",
|
|
769
|
+
body,
|
|
770
|
+
"",
|
|
771
|
+
]),
|
|
772
|
+
encoding="utf-8",
|
|
773
|
+
)
|
|
774
|
+
written.append(rel.as_posix())
|
|
775
|
+
|
|
776
|
+
return written
|
|
777
|
+
|
|
778
|
+
|
|
708
779
|
def _install_local_dry_run(reset: bool, editors: list[str] | None = None,
|
|
709
780
|
profile: str = "standard",
|
|
710
781
|
codex_skills: bool = False) -> None:
|
|
@@ -717,6 +788,7 @@ def _install_local_dry_run(reset: bool, editors: list[str] | None = None,
|
|
|
717
788
|
print(" Would create: CLAUDE.md (if missing)")
|
|
718
789
|
print(" Would create: .claude/settings.local.json (if missing)")
|
|
719
790
|
print(" Would inject: .claude/constitution.md")
|
|
791
|
+
print(" Would generate: .claude/rules/ai-toolkit-*.md")
|
|
720
792
|
|
|
721
793
|
add_copilot_dir = profile in {"standard", "strict", "full"}
|
|
722
794
|
add_gemini_hooks = profile in {"standard", "strict", "full"}
|
|
@@ -724,7 +796,7 @@ def _install_local_dry_run(reset: bool, editors: list[str] | None = None,
|
|
|
724
796
|
|
|
725
797
|
# Editor-specific dry-run messages
|
|
726
798
|
_EDITOR_DRY_RUN = {
|
|
727
|
-
"copilot": " Would inject: .github/copilot-instructions.md",
|
|
799
|
+
"copilot": " Would inject: .github/copilot-instructions.md + AGENTS.md",
|
|
728
800
|
"cursor": " Would generate: .cursorrules + .cursor/rules/*.mdc",
|
|
729
801
|
"windsurf": " Would generate: .windsurfrules + .devin/rules/*.md + .windsurf/rules/*.md",
|
|
730
802
|
"cline": " Would generate: .clinerules/*.md",
|
|
@@ -949,6 +1021,7 @@ def _create_local_ai_tool_configs(cwd: Path, rules_dir: Path,
|
|
|
949
1021
|
cwd / ".github" / "copilot-instructions.md",
|
|
950
1022
|
rules_dir,
|
|
951
1023
|
)
|
|
1024
|
+
_install_copilot_agents_md(cwd)
|
|
952
1025
|
# `standard` and above: emit path-specific instructions + prompt files
|
|
953
1026
|
# (directory mode). `minimal` stays backwards-compatible with v2.
|
|
954
1027
|
if add_copilot_dir:
|
|
@@ -1,9 +1,21 @@
|
|
|
1
|
-
"""Marker file injection and rule
|
|
1
|
+
"""Marker file injection and rule installation."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
+
import re
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
|
|
6
|
-
from _common import
|
|
7
|
+
from _common import (
|
|
8
|
+
app_dir,
|
|
9
|
+
inject_section,
|
|
10
|
+
remove_rule_section,
|
|
11
|
+
should_install,
|
|
12
|
+
_collapse_blank_runs,
|
|
13
|
+
_strip_section,
|
|
14
|
+
_trim_trailing_blanks,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
GLOBAL_RULES_SECTION = "global-rules"
|
|
7
19
|
|
|
8
20
|
|
|
9
21
|
def install_marker_files(claude_dir: Path, only: str, skip: str,
|
|
@@ -34,7 +46,7 @@ def install_marker_files(claude_dir: Path, only: str, skip: str,
|
|
|
34
46
|
def inject_rules(claude_dir: Path, target_dir: Path, rules_dir: Path,
|
|
35
47
|
only: str, skip: str, dry_run: bool,
|
|
36
48
|
refresh_urls: bool = False) -> None:
|
|
37
|
-
"""
|
|
49
|
+
"""Install Claude Code user-level rules.
|
|
38
50
|
|
|
39
51
|
When refresh_urls is True, re-fetches URL-sourced rules before injection.
|
|
40
52
|
Only the global install path should set this to True (once per update).
|
|
@@ -53,26 +65,131 @@ def inject_rules(claude_dir: Path, target_dir: Path, rules_dir: Path,
|
|
|
53
65
|
claude_md.touch()
|
|
54
66
|
print(" Created: ~/.claude/CLAUDE.md")
|
|
55
67
|
|
|
56
|
-
|
|
57
|
-
|
|
68
|
+
install_toolkit_rules = should_install("rules", only, skip)
|
|
69
|
+
if not install_toolkit_rules:
|
|
70
|
+
print(" Skipped: toolkit rule files")
|
|
58
71
|
|
|
59
|
-
|
|
72
|
+
expected: set[str] = set()
|
|
73
|
+
rules_synced: list[str] = []
|
|
60
74
|
|
|
61
75
|
if rules_dir.is_dir():
|
|
62
76
|
for rule_file in sorted(rules_dir.glob("*.md")):
|
|
63
77
|
rule_name = rule_file.stem
|
|
64
|
-
|
|
65
|
-
|
|
78
|
+
output_name = f"ai-toolkit-registered-{_safe_rule_name(rule_name)}"
|
|
79
|
+
_write_claude_rule_file(claude_dir, rule_file, output_name)
|
|
80
|
+
_remove_legacy_rule_marker(target_dir, rule_name)
|
|
81
|
+
expected.add(output_name)
|
|
82
|
+
rules_synced.append(rule_name)
|
|
66
83
|
|
|
67
|
-
if
|
|
84
|
+
if install_toolkit_rules:
|
|
68
85
|
rules_src = app_dir / "rules"
|
|
69
86
|
if rules_src.is_dir():
|
|
70
87
|
for source_file in sorted(rules_src.glob("*.md")):
|
|
71
88
|
rule_name = source_file.stem
|
|
72
|
-
|
|
73
|
-
|
|
89
|
+
output_name = f"ai-toolkit-{_safe_rule_name(rule_name)}"
|
|
90
|
+
_write_claude_rule_file(claude_dir, source_file, output_name)
|
|
91
|
+
_remove_legacy_rule_marker(target_dir, rule_name)
|
|
92
|
+
expected.add(output_name)
|
|
93
|
+
rules_synced.append(rule_name)
|
|
94
|
+
|
|
95
|
+
if not install_toolkit_rules and not rules_synced:
|
|
96
|
+
return
|
|
74
97
|
|
|
75
|
-
|
|
98
|
+
removed = _cleanup_managed_claude_rules(
|
|
99
|
+
claude_dir,
|
|
100
|
+
expected,
|
|
101
|
+
cleanup_toolkit_rules=install_toolkit_rules,
|
|
102
|
+
)
|
|
103
|
+
_inject_global_rules_index(claude_md, sorted(expected), rules_synced)
|
|
104
|
+
|
|
105
|
+
print(f" Rules synced: {' '.join(rules_synced)}")
|
|
106
|
+
if removed:
|
|
107
|
+
print(f" Cleaned: {removed} stale .claude/rules/ai-toolkit-*.md file(s)")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _safe_rule_name(name: str) -> str:
|
|
111
|
+
"""Return a Claude-safe filename/marker stem."""
|
|
112
|
+
return re.sub(r"[^a-zA-Z0-9_-]", "", name)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _write_claude_rule_file(
|
|
116
|
+
claude_dir: Path,
|
|
117
|
+
source_file: Path,
|
|
118
|
+
output_name: str,
|
|
119
|
+
) -> None:
|
|
120
|
+
"""Write a managed user-level rule under ``~/.claude/rules``."""
|
|
121
|
+
rules_root = claude_dir / "rules"
|
|
122
|
+
rules_root.mkdir(parents=True, exist_ok=True)
|
|
123
|
+
dst = rules_root / f"{output_name}.md"
|
|
124
|
+
content = source_file.read_text(encoding="utf-8").rstrip() + "\n"
|
|
125
|
+
dst.write_text(content, encoding="utf-8")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _remove_legacy_rule_marker(target_dir: Path, rule_name: str) -> None:
|
|
129
|
+
"""Remove old CLAUDE.md marker sections for rules now stored as files."""
|
|
130
|
+
remove_rule_section(_safe_rule_name(rule_name), target_dir)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _cleanup_managed_claude_rules(
|
|
134
|
+
claude_dir: Path,
|
|
135
|
+
expected: set[str],
|
|
136
|
+
*,
|
|
137
|
+
cleanup_toolkit_rules: bool,
|
|
138
|
+
) -> int:
|
|
139
|
+
"""Remove stale ai-toolkit-managed user-level rule files only."""
|
|
140
|
+
rules_root = claude_dir / "rules"
|
|
141
|
+
if not rules_root.is_dir():
|
|
142
|
+
return 0
|
|
143
|
+
|
|
144
|
+
removed = 0
|
|
145
|
+
for path in sorted(rules_root.glob("ai-toolkit-*.md")):
|
|
146
|
+
if path.stem in expected:
|
|
147
|
+
continue
|
|
148
|
+
if not cleanup_toolkit_rules and not path.stem.startswith("ai-toolkit-registered-"):
|
|
149
|
+
continue
|
|
150
|
+
path.unlink()
|
|
151
|
+
removed += 1
|
|
152
|
+
return removed
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _inject_global_rules_index(
|
|
156
|
+
claude_md: Path,
|
|
157
|
+
managed_rule_names: list[str],
|
|
158
|
+
display_names: list[str],
|
|
159
|
+
) -> None:
|
|
160
|
+
"""Keep CLAUDE.md as a compact pointer to user-level rule files."""
|
|
161
|
+
existing = claude_md.read_text(encoding="utf-8") if claude_md.is_file() else ""
|
|
162
|
+
existing = _trim_trailing_blanks(_strip_section(existing, GLOBAL_RULES_SECTION))
|
|
163
|
+
|
|
164
|
+
lines = [
|
|
165
|
+
"# Global ai-toolkit Rules",
|
|
166
|
+
"",
|
|
167
|
+
"ai-toolkit rules live in `~/.claude/rules/ai-toolkit-*.md` as Claude Code user-level rules.",
|
|
168
|
+
"They are intentionally not inlined into this `CLAUDE.md`; use `/memory` to inspect loaded rule files.",
|
|
169
|
+
]
|
|
170
|
+
if display_names:
|
|
171
|
+
names = ", ".join(f"`{name}`" for name in display_names)
|
|
172
|
+
lines.extend(["", f"Rules: {names}"])
|
|
173
|
+
if managed_rule_names:
|
|
174
|
+
files = ", ".join(f"`~/.claude/rules/{name}.md`" for name in managed_rule_names)
|
|
175
|
+
lines.extend(["", f"Files: {files}"])
|
|
176
|
+
|
|
177
|
+
parts: list[str] = []
|
|
178
|
+
if existing.strip():
|
|
179
|
+
parts.append(existing)
|
|
180
|
+
parts.append("")
|
|
181
|
+
parts.extend([
|
|
182
|
+
f"<!-- TOOLKIT:{GLOBAL_RULES_SECTION} START -->",
|
|
183
|
+
"<!-- Auto-injected by ai-toolkit. Re-run to update. -->",
|
|
184
|
+
"",
|
|
185
|
+
"\n".join(lines),
|
|
186
|
+
"",
|
|
187
|
+
f"<!-- TOOLKIT:{GLOBAL_RULES_SECTION} END -->",
|
|
188
|
+
])
|
|
189
|
+
|
|
190
|
+
output = _collapse_blank_runs("\n".join(parts) + "\n").lstrip("\n")
|
|
191
|
+
claude_md.parent.mkdir(parents=True, exist_ok=True)
|
|
192
|
+
claude_md.write_text(output, encoding="utf-8")
|
|
76
193
|
|
|
77
194
|
|
|
78
195
|
def _refresh_url_rules(rules_dir: Path) -> None:
|
|
@@ -186,9 +303,10 @@ def _inject_rules_dry_run(rules_dir: Path) -> None:
|
|
|
186
303
|
rule_names = " ".join(
|
|
187
304
|
f.stem for f in sorted(rules_src.glob("*.md"))
|
|
188
305
|
) if rules_src.is_dir() else ""
|
|
189
|
-
print(f" Would
|
|
306
|
+
print(f" Would generate: ~/.claude/rules/ai-toolkit-*.md ({rule_names})")
|
|
190
307
|
if rules_dir.is_dir():
|
|
191
308
|
registered = list(rules_dir.glob("*.md"))
|
|
192
309
|
if registered:
|
|
193
310
|
reg_names = " ".join(f.stem for f in sorted(registered))
|
|
194
|
-
print(f" Would
|
|
311
|
+
print(f" Would generate: ~/.claude/rules/ai-toolkit-registered-*.md ({reg_names})")
|
|
312
|
+
print(" Would update: ~/.claude/CLAUDE.md global rules index")
|
package/scripts/remove_rule.py
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
"""remove-rule -- Unregister a rule (opposite of add-rule).
|
|
3
3
|
|
|
4
4
|
Removes the rule file from ~/.softspark/ai-toolkit/rules/ (so it is no longer
|
|
5
|
-
re-applied on future 'ai-toolkit install' runs)
|
|
6
|
-
|
|
5
|
+
re-applied on future 'ai-toolkit install' runs), removes the generated Claude
|
|
6
|
+
Code user-level rule file, and strips the legacy injected block from
|
|
7
|
+
CLAUDE.md.
|
|
7
8
|
|
|
8
9
|
Usage:
|
|
9
10
|
remove_rule.py <rule-name> [target-dir]
|
|
@@ -16,13 +17,14 @@ from __future__ import annotations
|
|
|
16
17
|
|
|
17
18
|
import sys
|
|
18
19
|
from pathlib import Path
|
|
20
|
+
import re
|
|
19
21
|
|
|
20
22
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
21
23
|
from _common import remove_rule_section
|
|
22
24
|
|
|
23
25
|
|
|
24
26
|
def main() -> None:
|
|
25
|
-
"""Unregister a rule and
|
|
27
|
+
"""Unregister a rule and remove generated Claude rule artifacts."""
|
|
26
28
|
if len(sys.argv) < 2:
|
|
27
29
|
print("Usage: remove_rule.py <rule-name> [target-dir]", file=sys.stderr)
|
|
28
30
|
sys.exit(1)
|
|
@@ -48,7 +50,16 @@ def main() -> None:
|
|
|
48
50
|
if unregister_source(rules_dir, rule_name):
|
|
49
51
|
print(f"Removed URL source for '{rule_name}'")
|
|
50
52
|
|
|
51
|
-
|
|
53
|
+
safe_rule_name = re.sub(r"[^a-zA-Z0-9_-]", "", rule_name)
|
|
54
|
+
|
|
55
|
+
# 2. Remove generated Claude Code user-level rule file
|
|
56
|
+
generated_rule = target_dir / ".claude" / "rules" / f"ai-toolkit-registered-{safe_rule_name}.md"
|
|
57
|
+
if generated_rule.is_file():
|
|
58
|
+
generated_rule.unlink()
|
|
59
|
+
print(f"Removed generated Claude rule: {generated_rule}")
|
|
60
|
+
removed += 1
|
|
61
|
+
|
|
62
|
+
# 3. Strip legacy injected block from .claude/CLAUDE.md
|
|
52
63
|
found = remove_rule_section(rule_name, target_dir)
|
|
53
64
|
if found:
|
|
54
65
|
print(f"Removed rule '{rule_name}' from {target_dir / '.claude' / 'CLAUDE.md'}")
|