@softspark/ai-toolkit 4.10.1 → 4.12.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/CHANGELOG.md +38 -0
- package/README.md +12 -13
- package/app/.claude-plugin/plugin.json +1 -1
- package/benchmarks/ecosystem-doctor-snapshot.json +18 -12
- package/bin/ai-toolkit.js +1 -1
- package/kb/procedures/maintenance-sop.md +5 -3
- package/kb/reference/architecture-overview.md +5 -4
- package/kb/reference/codex-cli-compatibility.md +15 -10
- package/kb/reference/extension-api.md +5 -5
- package/kb/reference/global-install-model.md +31 -11
- package/kb/reference/integrations.md +16 -17
- package/kb/reference/language-rules.md +35 -14
- package/kb/reference/supported-tools-registry.md +34 -25
- package/llms-full.txt +146 -89
- package/manifest.json +1 -1
- package/package.json +1 -1
- package/scripts/ecosystem_tools.json +17 -6
- package/scripts/generate_antigravity.py +15 -0
- package/scripts/generate_codex_hooks.py +8 -5
- package/scripts/generate_copilot.py +10 -3
- package/scripts/generate_language_rules_skills.py +4 -2
- package/scripts/inject_hook_cli.py +16 -2
- package/scripts/install.py +5 -5
- package/scripts/install_steps/ai_tools.py +205 -36
- package/scripts/install_steps/install_state.py +9 -2
- package/scripts/install_steps/markers.py +132 -14
- package/scripts/plugin.py +31 -8
- package/scripts/remove_rule.py +15 -4
|
@@ -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/plugin.py
CHANGED
|
@@ -23,6 +23,7 @@ Actions:
|
|
|
23
23
|
from __future__ import annotations
|
|
24
24
|
|
|
25
25
|
import json
|
|
26
|
+
import re
|
|
26
27
|
import shutil
|
|
27
28
|
import sqlite3 as sqlite
|
|
28
29
|
import subprocess
|
|
@@ -31,6 +32,7 @@ from pathlib import Path
|
|
|
31
32
|
|
|
32
33
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
33
34
|
from _common import app_dir, inject_section, inject_rule, remove_rule_section
|
|
35
|
+
from injection import strip_section, trim_trailing_blanks
|
|
34
36
|
from codex_skill_adapter import cleanup_codex_skills, sync_codex_skill
|
|
35
37
|
from generate_codex_hooks import generate as generate_codex_hooks
|
|
36
38
|
from install_steps.ai_tools import inject_with_rules
|
|
@@ -536,8 +538,9 @@ def _install_codex_extra_skills(pack: dict, pack_dir: Path) -> None:
|
|
|
536
538
|
def _install_codex_base() -> None:
|
|
537
539
|
_ensure_core_hook_scripts()
|
|
538
540
|
# Universal coding rules are inlined into AGENTS.md by generate_codex.py;
|
|
539
|
-
# Codex does not read a .agents/rules/ directory.
|
|
540
|
-
|
|
541
|
+
# Codex does not read a .agents/rules/ directory. Global instructions must
|
|
542
|
+
# live at $CODEX_HOME/AGENTS.md (default ~/.codex/AGENTS.md), not ~/AGENTS.md.
|
|
543
|
+
inject_with_rules("generate_codex.py", CODEX_ROOT / ".codex" / "AGENTS.md", RULES_DIR)
|
|
541
544
|
hooks_path = CODEX_ROOT / ".codex" / "hooks.json"
|
|
542
545
|
existing = _load_json(hooks_path, {"hooks": {}})
|
|
543
546
|
plugin_entries: dict[str, list[dict]] = {}
|
|
@@ -632,21 +635,41 @@ def _strip_codex_hooks(name: str) -> None:
|
|
|
632
635
|
|
|
633
636
|
|
|
634
637
|
def _install_codex_rules(name: str, rule_specs: list[dict]) -> None:
|
|
635
|
-
|
|
636
|
-
|
|
638
|
+
# Codex reads instructions only from AGENTS.md, never from a .agents/rules/
|
|
639
|
+
# directory, so pack rules are marker-injected into ~/.codex/AGENTS.md (the
|
|
640
|
+
# documented global instruction file) instead of written as dead files.
|
|
641
|
+
agents_md = CODEX_ROOT / ".codex" / "AGENTS.md"
|
|
637
642
|
for spec in rule_specs:
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
print(f"
|
|
643
|
+
section = f"plugin-{name}-{spec['name']}"
|
|
644
|
+
inject_section(spec["source"], agents_md, section)
|
|
645
|
+
print(f" Injected Codex rule: {spec['name']} -> ~/.codex/AGENTS.md")
|
|
646
|
+
_clean_legacy_codex_rule_files(name)
|
|
641
647
|
|
|
642
648
|
|
|
643
649
|
def _remove_codex_rules(name: str) -> None:
|
|
650
|
+
agents_md = CODEX_ROOT / ".codex" / "AGENTS.md"
|
|
651
|
+
if agents_md.is_file():
|
|
652
|
+
content = agents_md.read_text(encoding="utf-8")
|
|
653
|
+
changed = False
|
|
654
|
+
for match in re.findall(r"<!-- TOOLKIT:(plugin-" + re.escape(name) + r"-[^ ]+) START -->", content):
|
|
655
|
+
content = strip_section(content, match)
|
|
656
|
+
changed = True
|
|
657
|
+
if changed:
|
|
658
|
+
agents_md.write_text(trim_trailing_blanks(content) + "\n", encoding="utf-8")
|
|
659
|
+
print(f" Removed Codex rules for {name} from ~/.codex/AGENTS.md")
|
|
660
|
+
_clean_legacy_codex_rule_files(name)
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
def _clean_legacy_codex_rule_files(name: str) -> None:
|
|
664
|
+
"""Remove dead ~/.agents/rules/plugin-<name>-*.md files written by earlier
|
|
665
|
+
versions. Codex never read them; Antigravity's .agents/rules/ is a separate,
|
|
666
|
+
project-local surface, so this only touches our own plugin-prefixed files."""
|
|
644
667
|
rules_dir = CODEX_ROOT / ".agents" / "rules"
|
|
645
668
|
if not rules_dir.is_dir():
|
|
646
669
|
return
|
|
647
670
|
for rule_file in sorted(rules_dir.glob(f"plugin-{name}-*.md")):
|
|
648
671
|
rule_file.unlink()
|
|
649
|
-
print(f" Removed Codex rule: {rule_file.name}")
|
|
672
|
+
print(f" Removed legacy Codex rule file: {rule_file.name}")
|
|
650
673
|
|
|
651
674
|
|
|
652
675
|
def install_pack_codex(name: str, pack: dict, pack_dir: Path) -> bool:
|
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'}")
|