@softspark/ai-toolkit 4.11.0 → 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/AGENTS.md +0 -135
- package/CHANGELOG.md +24 -0
- package/README.md +9 -9
- package/app/.claude-plugin/plugin.json +1 -1
- package/benchmarks/ecosystem-doctor-snapshot.json +11 -11
- package/kb/reference/codex-cli-compatibility.md +15 -10
- package/kb/reference/global-install-model.md +13 -8
- package/kb/reference/supported-tools-registry.md +32 -23
- package/llms-full.txt +60 -41
- package/manifest.json +1 -1
- package/package.json +1 -1
- package/scripts/ecosystem_tools.json +12 -4
- package/scripts/generate_antigravity.py +15 -0
- package/scripts/generate_codex_hooks.py +8 -5
- package/scripts/generate_copilot.py +10 -3
- package/scripts/inject_hook_cli.py +16 -2
- package/scripts/install.py +2 -2
- package/scripts/install_steps/ai_tools.py +138 -8
- package/scripts/install_steps/install_state.py +9 -2
- package/scripts/plugin.py +31 -8
|
@@ -20,12 +20,16 @@ from injection import (
|
|
|
20
20
|
|
|
21
21
|
def install_ai_tools(target_dir: Path, rules_dir: Path,
|
|
22
22
|
dry_run: bool,
|
|
23
|
-
editors: list[str] | None = None
|
|
23
|
+
editors: list[str] | None = None,
|
|
24
|
+
profile: str = "standard") -> list[str]:
|
|
24
25
|
"""Install global editor configs.
|
|
25
26
|
|
|
26
27
|
Args:
|
|
27
28
|
editors: Explicit list of editors to install globally. If None,
|
|
28
29
|
uses DEFAULT_GLOBAL_EDITORS (empty = Claude only).
|
|
30
|
+
profile: Install profile. Gates each editor's optional native surfaces
|
|
31
|
+
the same way the local install does — hooks at
|
|
32
|
+
standard/strict/full, sub-agents/commands/skills at full.
|
|
29
33
|
|
|
30
34
|
Returns:
|
|
31
35
|
List of editors that were actually installed (for state tracking).
|
|
@@ -43,6 +47,10 @@ def install_ai_tools(target_dir: Path, rules_dir: Path,
|
|
|
43
47
|
if not eds:
|
|
44
48
|
return []
|
|
45
49
|
|
|
50
|
+
_profile = profile if profile in {"minimal", "standard", "strict", "full"} else "standard"
|
|
51
|
+
add_hooks = _profile in {"standard", "strict", "full"}
|
|
52
|
+
add_native_surfaces = _profile == "full"
|
|
53
|
+
|
|
46
54
|
print()
|
|
47
55
|
print("## Other AI Tools (global)")
|
|
48
56
|
print()
|
|
@@ -57,6 +65,7 @@ def install_ai_tools(target_dir: Path, rules_dir: Path,
|
|
|
57
65
|
if dry_run:
|
|
58
66
|
print(" Would inject: ~/.codeium/windsurf/memories/global_rules.md")
|
|
59
67
|
print(" Would generate: ~/.codeium/windsurf/skills/ai-toolkit-skill-catalogue/SKILL.md")
|
|
68
|
+
print(" Would inject: ~/.config/devin/AGENTS.md (Devin CLI global rules)")
|
|
60
69
|
else:
|
|
61
70
|
_install_windsurf_global(target_dir, rules_dir)
|
|
62
71
|
installed.append("windsurf")
|
|
@@ -65,16 +74,37 @@ def install_ai_tools(target_dir: Path, rules_dir: Path,
|
|
|
65
74
|
gemini_file = target_dir / ".gemini" / "GEMINI.md"
|
|
66
75
|
if dry_run:
|
|
67
76
|
print(" Would inject: ~/.gemini/GEMINI.md")
|
|
77
|
+
if add_hooks:
|
|
78
|
+
print(" Would generate: ~/.gemini/settings.json (hooks)")
|
|
79
|
+
if add_native_surfaces:
|
|
80
|
+
print(" Would generate: ~/.gemini/commands/, ~/.gemini/skills/")
|
|
68
81
|
else:
|
|
69
82
|
inject_with_rules("generate-gemini.sh", gemini_file, rules_dir)
|
|
83
|
+
# Hooks (~/.gemini/settings.json), commands (~/.gemini/commands/),
|
|
84
|
+
# and the skills pointer (~/.gemini/skills/) are documented user-tier
|
|
85
|
+
# surfaces — install them globally, gated like the local install.
|
|
86
|
+
if add_hooks:
|
|
87
|
+
_try_generator("generate_gemini_hooks", target_dir)
|
|
88
|
+
if add_native_surfaces:
|
|
89
|
+
_try_generator("generate_gemini_commands", target_dir)
|
|
90
|
+
_try_generator("generate_gemini_skills", target_dir)
|
|
70
91
|
installed.append("gemini")
|
|
71
92
|
|
|
72
93
|
if "augment" in eds:
|
|
73
94
|
augment_file = target_dir / ".augment" / "rules" / "ai-toolkit.md"
|
|
74
95
|
if dry_run:
|
|
75
96
|
print(" Would inject: ~/.augment/rules/ai-toolkit.md")
|
|
97
|
+
if add_native_surfaces:
|
|
98
|
+
print(" Would generate: ~/.augment/agents/, ~/.augment/commands/, ~/.augment/settings.json (hooks)")
|
|
76
99
|
else:
|
|
77
100
|
inject_with_rules("generate-augment.sh", augment_file, rules_dir)
|
|
101
|
+
# Sub-agents, commands, and hooks all have documented user-tier
|
|
102
|
+
# surfaces under ~/.augment — a global-only Augment user otherwise
|
|
103
|
+
# gets no hooks/agents/commands. Gate them like the local install.
|
|
104
|
+
if add_native_surfaces:
|
|
105
|
+
_try_generator("generate_augment_agents", target_dir)
|
|
106
|
+
_try_generator("generate_augment_commands", target_dir)
|
|
107
|
+
_try_generator("generate_augment_hooks", target_dir)
|
|
78
108
|
installed.append("augment")
|
|
79
109
|
|
|
80
110
|
if "cline" in eds:
|
|
@@ -88,8 +118,15 @@ def install_ai_tools(target_dir: Path, rules_dir: Path,
|
|
|
88
118
|
if "roo" in eds:
|
|
89
119
|
if dry_run:
|
|
90
120
|
print(" Would generate: ~/.roo/rules/ai-toolkit-*.md")
|
|
121
|
+
if "codex" not in eds:
|
|
122
|
+
print(" Would generate: ~/.agents/skills/* (Roo native skill discovery)")
|
|
91
123
|
else:
|
|
92
124
|
_install_roo_global(target_dir, rules_dir)
|
|
125
|
+
# Roo/Zoo Code natively discover skills from ~/.agents/skills. Reuse
|
|
126
|
+
# the shared installer, but skip when codex is also selected — its
|
|
127
|
+
# branch populates the same directory — to avoid a duplicate pass.
|
|
128
|
+
if "codex" not in eds:
|
|
129
|
+
_install_codex_skills(target_dir)
|
|
93
130
|
installed.append("roo")
|
|
94
131
|
|
|
95
132
|
if "aider" in eds:
|
|
@@ -101,7 +138,7 @@ def install_ai_tools(target_dir: Path, rules_dir: Path,
|
|
|
101
138
|
|
|
102
139
|
if "codex" in eds:
|
|
103
140
|
if dry_run:
|
|
104
|
-
print(" Would inject:
|
|
141
|
+
print(" Would inject: ~/.codex/AGENTS.md, ~/.agents/skills/, ~/.codex/hooks.json")
|
|
105
142
|
else:
|
|
106
143
|
_install_codex_global(target_dir, rules_dir)
|
|
107
144
|
installed.append("codex")
|
|
@@ -114,28 +151,85 @@ def install_ai_tools(target_dir: Path, rules_dir: Path,
|
|
|
114
151
|
_install_opencode_global(target_dir, rules_dir)
|
|
115
152
|
installed.append("opencode")
|
|
116
153
|
|
|
154
|
+
if "cursor" in eds:
|
|
155
|
+
# Cursor RULES have no mergeable global file surface (Settings UI only),
|
|
156
|
+
# but ~/.cursor/hooks.json is a documented user-level hooks scope — so a
|
|
157
|
+
# global Cursor install activates the safety/quality hooks everywhere.
|
|
158
|
+
if dry_run:
|
|
159
|
+
if add_hooks:
|
|
160
|
+
print(" Would generate: ~/.cursor/hooks.json (global hooks)")
|
|
161
|
+
elif add_hooks:
|
|
162
|
+
_try_generator("generate_cursor_hooks", target_dir)
|
|
163
|
+
installed.append("cursor")
|
|
164
|
+
|
|
165
|
+
if "copilot" in eds:
|
|
166
|
+
# Copilot CLI reads user-level instructions from ~/.copilot/. RULES have
|
|
167
|
+
# a documented global surface here even though .github/ stays repo-only.
|
|
168
|
+
copilot_root = target_dir / ".copilot"
|
|
169
|
+
if dry_run:
|
|
170
|
+
print(" Would inject: ~/.copilot/copilot-instructions.md")
|
|
171
|
+
print(" Would generate: ~/.copilot/instructions/ai-toolkit-*.instructions.md")
|
|
172
|
+
else:
|
|
173
|
+
inject_with_rules(
|
|
174
|
+
"generate_copilot.py",
|
|
175
|
+
copilot_root / "copilot-instructions.md",
|
|
176
|
+
rules_dir,
|
|
177
|
+
)
|
|
178
|
+
_try_generator("generate_copilot", target_dir,
|
|
179
|
+
rules_dir=rules_dir, config_root=copilot_root,
|
|
180
|
+
emit_prompts=False)
|
|
181
|
+
installed.append("copilot")
|
|
182
|
+
|
|
183
|
+
if "antigravity" in eds:
|
|
184
|
+
# Antigravity RULES stay project-local, but the skill pointer has a
|
|
185
|
+
# documented global surface: ~/.gemini/config/skills (all Antigravity
|
|
186
|
+
# products) and ~/.gemini/antigravity-cli/skills (CLI-private).
|
|
187
|
+
if dry_run:
|
|
188
|
+
print(" Would generate: ~/.gemini/config/skills/, ~/.gemini/antigravity-cli/skills/ (skill pointer)")
|
|
189
|
+
else:
|
|
190
|
+
from generate_antigravity import generate_global as gen_antigravity_global
|
|
191
|
+
gen_antigravity_global(target_dir)
|
|
192
|
+
installed.append("antigravity")
|
|
193
|
+
|
|
117
194
|
print()
|
|
118
195
|
print(f" Available: {', '.join(GLOBAL_CAPABLE_EDITORS)}")
|
|
119
|
-
print(" Note: Cursor
|
|
196
|
+
print(" Note: Cursor and Antigravity RULES stay project-local (no mergeable "
|
|
197
|
+
"global file surface); their global installs cover hooks/skills only. "
|
|
198
|
+
"Use 'ai-toolkit install --local' for full per-project setup.")
|
|
120
199
|
|
|
121
200
|
return installed
|
|
122
201
|
|
|
123
202
|
|
|
124
203
|
def _install_codex_global(target_dir: Path, rules_dir: Path) -> None:
|
|
125
|
-
"""Install Codex at the global level (
|
|
204
|
+
"""Install Codex at the global level (~/.codex layer).
|
|
126
205
|
|
|
127
206
|
Creates:
|
|
128
|
-
-
|
|
129
|
-
Codex reads instructions
|
|
130
|
-
|
|
207
|
+
- ~/.codex/AGENTS.md (marker injection; universal coding rules inlined
|
|
208
|
+
here). Codex reads GLOBAL instructions from ``$CODEX_HOME/AGENTS.md``
|
|
209
|
+
(default ~/.codex/AGENTS.md), NOT ~/AGENTS.md — a home-root AGENTS.md
|
|
210
|
+
is only loaded in the degenerate case where a session's cwd is $HOME.
|
|
211
|
+
- ~/.agents/skills/* (skill symlinks; a documented Codex skill dir)
|
|
131
212
|
- ~/.codex/hooks.json (lifecycle hooks)
|
|
132
213
|
"""
|
|
133
214
|
inject_with_rules(
|
|
134
215
|
"generate_codex.py",
|
|
135
|
-
target_dir / "AGENTS.md",
|
|
216
|
+
target_dir / ".codex" / "AGENTS.md",
|
|
136
217
|
rules_dir,
|
|
137
218
|
)
|
|
138
219
|
|
|
220
|
+
# Migration: earlier versions wrote global instructions to ~/AGENTS.md,
|
|
221
|
+
# which Codex never loads as global instructions. Strip that stale toolkit
|
|
222
|
+
# section so upgraders are not left with dead, unread content.
|
|
223
|
+
if _strip_toolkit_sections(target_dir / "AGENTS.md"):
|
|
224
|
+
print(" Migrated: removed stale ai-toolkit section from ~/AGENTS.md")
|
|
225
|
+
|
|
226
|
+
override = target_dir / ".codex" / "AGENTS.override.md"
|
|
227
|
+
if override.is_file() and override.read_text(encoding="utf-8").strip():
|
|
228
|
+
print(
|
|
229
|
+
" Warning: ~/.codex/AGENTS.override.md exists and takes precedence "
|
|
230
|
+
"over ~/.codex/AGENTS.md — toolkit rules will be masked."
|
|
231
|
+
)
|
|
232
|
+
|
|
139
233
|
from generate_codex_hooks import generate as gen_codex_hooks
|
|
140
234
|
gen_codex_hooks(target_dir)
|
|
141
235
|
print(" Created: ~/.codex/hooks.json")
|
|
@@ -166,6 +260,13 @@ def _install_windsurf_global(target_dir: Path, rules_dir: Path) -> None:
|
|
|
166
260
|
windsurf_file = target_dir / ".codeium" / "windsurf" / "memories" / "global_rules.md"
|
|
167
261
|
inject_with_rules("generate-windsurf.sh", windsurf_file, rules_dir)
|
|
168
262
|
|
|
263
|
+
# Devin CLI reads global rules from ~/.config/devin/AGENTS.md, NOT the
|
|
264
|
+
# Desktop ~/.codeium/windsurf/memories/global_rules.md (that path is absent
|
|
265
|
+
# from read_config_from.windsurf). Cover editor-only Devin CLI installs
|
|
266
|
+
# where no global Claude install (~/.claude/CLAUDE.md compat read) exists.
|
|
267
|
+
devin_agents = target_dir / ".config" / "devin" / "AGENTS.md"
|
|
268
|
+
inject_with_rules("generate-windsurf.sh", devin_agents, rules_dir)
|
|
269
|
+
|
|
169
270
|
from generate_windsurf_skills import generate as gen_windsurf_skills
|
|
170
271
|
# Windsurf is excluded from the .claude/skills conditional: its native scan
|
|
171
272
|
# of .claude/skills is gated behind a Devin "Claude Code config reading"
|
|
@@ -352,6 +453,35 @@ def inject_with_rules(
|
|
|
352
453
|
print(f" Updated: {target_file}")
|
|
353
454
|
|
|
354
455
|
|
|
456
|
+
def _strip_toolkit_sections(target_file: Path) -> bool:
|
|
457
|
+
"""Remove ai-toolkit ``<!-- TOOLKIT:* -->`` marker sections from an existing
|
|
458
|
+
file, preserving any non-toolkit content. Returns True if the file changed.
|
|
459
|
+
|
|
460
|
+
Used to clean a stale ~/AGENTS.md after Codex global instructions moved to
|
|
461
|
+
~/.codex/AGENTS.md. If stripping leaves the file empty (it only ever held
|
|
462
|
+
toolkit content), the file is removed.
|
|
463
|
+
"""
|
|
464
|
+
if not target_file.is_file():
|
|
465
|
+
return False
|
|
466
|
+
import re
|
|
467
|
+
|
|
468
|
+
original = target_file.read_text(encoding="utf-8")
|
|
469
|
+
stripped = re.sub(
|
|
470
|
+
r"<!-- TOOLKIT:[^ ]+ START -->.*?<!-- TOOLKIT:[^ ]+ END -->\n?",
|
|
471
|
+
"",
|
|
472
|
+
original,
|
|
473
|
+
flags=re.DOTALL,
|
|
474
|
+
)
|
|
475
|
+
if stripped == original:
|
|
476
|
+
return False
|
|
477
|
+
stripped = stripped.lstrip("\n")
|
|
478
|
+
if stripped.strip():
|
|
479
|
+
target_file.write_text(stripped, encoding="utf-8")
|
|
480
|
+
else:
|
|
481
|
+
target_file.unlink()
|
|
482
|
+
return True
|
|
483
|
+
|
|
484
|
+
|
|
355
485
|
def _inject_text_section(target_file: Path, section: str, text: str) -> None:
|
|
356
486
|
"""Inject generated text into one marker section without touching others."""
|
|
357
487
|
target_file.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -131,13 +131,20 @@ DEFAULT_GLOBAL_EDITORS: list[str] = []
|
|
|
131
131
|
|
|
132
132
|
# All editors that support global install (opt-in via --editors).
|
|
133
133
|
#
|
|
134
|
-
#
|
|
135
|
-
#
|
|
134
|
+
# Scope varies by editor's documented global file surfaces:
|
|
135
|
+
# - cursor: HOOKS only (~/.cursor/hooks.json). Cursor RULES stay project-local
|
|
136
|
+
# — their only global surface is the Settings UI, not a mergeable file.
|
|
137
|
+
# - copilot: user-level instructions (~/.copilot/, read by Copilot CLI).
|
|
138
|
+
# - antigravity: global skill pointer (~/.gemini/config/skills,
|
|
139
|
+
# ~/.gemini/antigravity-cli/skills). Rules stay project-local.
|
|
136
140
|
GLOBAL_CAPABLE_EDITORS = [
|
|
137
141
|
"aider",
|
|
142
|
+
"antigravity",
|
|
138
143
|
"augment",
|
|
139
144
|
"cline",
|
|
140
145
|
"codex",
|
|
146
|
+
"copilot",
|
|
147
|
+
"cursor",
|
|
141
148
|
"gemini",
|
|
142
149
|
"opencode",
|
|
143
150
|
"roo",
|
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:
|