@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.
@@ -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) -> list[str]:
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: ~/AGENTS.md, ~/.agents/, ~/.codex/hooks.json")
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, Copilot, and Antigravity rule installs are project-local; use 'ai-toolkit install --local' for those.")
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 (~/ layer).
204
+ """Install Codex at the global level (~/.codex layer).
126
205
 
127
206
  Creates:
128
- - ~/AGENTS.md (marker injection; universal coding rules inlined here —
129
- Codex reads instructions only from AGENTS.md, not .agents/rules/)
130
- - ~/.agents/skills/* (skill symlinks)
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
- # Cursor intentionally stays out of this list: its documented global rules
135
- # surface is the Settings UI, not a stable file path we can merge safely.
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
- inject_with_rules("generate_codex.py", CODEX_ROOT / "AGENTS.md", RULES_DIR)
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
- rules_dir = CODEX_ROOT / ".agents" / "rules"
636
- rules_dir.mkdir(parents=True, exist_ok=True)
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
- dest = rules_dir / f"plugin-{name}-{spec['name']}.md"
639
- shutil.copy2(spec["source"], dest)
640
- print(f" Installed Codex rule: {dest.name}")
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: