@softspark/ai-toolkit 4.11.0 → 4.13.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 +54 -0
- package/README.md +33 -15
- package/app/.claude-plugin/plugin.json +11 -19
- package/app/ARCHITECTURE.md +6 -6
- package/app/claude-app/global-instructions.md +10 -0
- package/app/claude-app/hooks/hooks.json +284 -0
- package/app/claude-app/skills/ai-toolkit-rules/SKILL.md +359 -0
- package/app/hooks/config-desync-guard.sh +63 -23
- package/app/plugins/README.md +4 -1
- package/benchmarks/ecosystem-doctor-snapshot.json +91 -25
- package/bin/ai-toolkit.js +21 -3
- package/kb/planning/drop-cascade-hooks-after-sunset.md +13 -8
- package/kb/procedures/ecosystem-sync-sop.md +5 -5
- package/kb/procedures/maintenance-sop.md +42 -4
- package/kb/procedures/release-preparation-sop.md +5 -1
- package/kb/procedures/release-verification-sop.md +25 -9
- package/kb/reference/architecture-overview.md +7 -2
- package/kb/reference/claude-ecosystem-expansion-foundations.md +12 -3
- package/kb/reference/cli-reference.md +4 -2
- package/kb/reference/codex-cli-compatibility.md +15 -10
- package/kb/reference/global-install-model.md +37 -11
- package/kb/reference/hooks-catalog.md +2 -3
- package/kb/reference/plugin-pack-conventions.md +5 -5
- package/kb/reference/skills-catalog.md +2 -0
- package/kb/reference/supported-tools-registry.md +59 -33
- package/kb/reference/unique-features.md +3 -2
- package/llms-full.txt +236 -98
- package/manifest.json +8 -8
- package/package.json +4 -3
- package/scripts/claude_app.py +347 -0
- package/scripts/doctor.py +85 -4
- package/scripts/ecosystem_tools.json +45 -9
- 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_devin_hooks.py +3 -4
- package/scripts/generate_windsurf_skills.py +5 -6
- package/scripts/inject_hook_cli.py +16 -2
- package/scripts/install.py +25 -23
- package/scripts/install_steps/ai_tools.py +234 -14
- package/scripts/install_steps/install_state.py +11 -2
- package/scripts/plugin.py +31 -8
- package/scripts/update_projects.py +12 -1
- package/scripts/validate.py +34 -2
- package/AGENTS.md +0 -790
- package/scripts/generate_windsurf_hooks.py +0 -152
|
@@ -99,6 +99,21 @@ def _write_skill_pointer(target_dir: Path) -> None:
|
|
|
99
99
|
# Main
|
|
100
100
|
# ---------------------------------------------------------------------------
|
|
101
101
|
|
|
102
|
+
def generate_global(target_dir: Path) -> None:
|
|
103
|
+
"""Write the skill pointer to Antigravity's documented HOME-scoped skill
|
|
104
|
+
dirs. ``~/.gemini/config/skills/`` is shared across all Antigravity products
|
|
105
|
+
(IDE, editor, CLI); ``~/.gemini/antigravity-cli/skills/`` is CLI-private.
|
|
106
|
+
Antigravity RULES have no documented global file surface, so only the skill
|
|
107
|
+
pointer is emitted globally (rules stay project-local).
|
|
108
|
+
"""
|
|
109
|
+
content = _pointer_skill_md()
|
|
110
|
+
for rel in (".gemini/config/skills", ".gemini/antigravity-cli/skills"):
|
|
111
|
+
skill_dir = target_dir / rel / POINTER_SKILL_NAME
|
|
112
|
+
skill_dir.mkdir(parents=True, exist_ok=True)
|
|
113
|
+
(skill_dir / "SKILL.md").write_text(content, encoding="utf-8")
|
|
114
|
+
print(f" Generated: {rel}/{POINTER_SKILL_NAME}/SKILL.md")
|
|
115
|
+
|
|
116
|
+
|
|
102
117
|
def generate(target_dir: Path, *,
|
|
103
118
|
language_modules: list[str] | None = None,
|
|
104
119
|
rules_dir: Path | None = None,
|
|
@@ -8,11 +8,14 @@ Codex exposes 10 lifecycle events (PascalCase in config.toml / hooks.json):
|
|
|
8
8
|
``PreToolUse``, ``PostToolUse``, ``PermissionRequest``, ``PreCompact``,
|
|
9
9
|
``PostCompact``, ``SessionStart``, ``UserPromptSubmit``, ``SubagentStart``,
|
|
10
10
|
``SubagentStop``, ``Stop``. PreToolUse/PostToolUse only support the ``Bash``
|
|
11
|
-
matcher. We wire
|
|
12
|
-
hook scripts, mirroring the Claude Code mapping in ``app/hooks.json``.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
matcher. We wire 9 of the 10 events in ``CODEX_HOOKS`` below to the shared
|
|
12
|
+
toolkit hook scripts, mirroring the Claude Code mapping in ``app/hooks.json``.
|
|
13
|
+
``PostCompact`` is intentionally unwired (its only hook was the removed
|
|
14
|
+
environment-snapshot probe).
|
|
15
|
+
|
|
16
|
+
Handler types in Codex: ``command`` (what we emit). ``prompt`` and ``agent``
|
|
17
|
+
are parsed by Codex but not yet executed, so hand-authored handlers of those
|
|
18
|
+
types are inert. Reference: codex-rs/config/src/hook_config.rs.
|
|
16
19
|
|
|
17
20
|
Usage:
|
|
18
21
|
python3 scripts/generate_codex_hooks.py [target-dir]
|
|
@@ -228,17 +228,24 @@ def generate(target_dir: Path, *,
|
|
|
228
228
|
language_modules: list[str] | None = None,
|
|
229
229
|
rules_dir: Path | None = None,
|
|
230
230
|
emit_prompts: bool = True,
|
|
231
|
-
emit_instructions: bool = True
|
|
231
|
+
emit_instructions: bool = True,
|
|
232
|
+
config_root: Path | None = None) -> None:
|
|
232
233
|
"""Write Copilot path-specific instructions and prompt files.
|
|
233
234
|
|
|
235
|
+
By default writes to ``<target_dir>/.github/`` (project-local). Pass
|
|
236
|
+
``config_root=~/.copilot`` for the Copilot CLI user-level global layout,
|
|
237
|
+
where instructions land in ``~/.copilot/instructions/*.instructions.md``.
|
|
238
|
+
|
|
234
239
|
``.github/copilot-instructions.md`` is intentionally not written here —
|
|
235
240
|
the legacy ``main()`` entry point still emits it to stdout so existing
|
|
236
241
|
scripts (including ``ai-toolkit install``) keep working unchanged.
|
|
237
242
|
"""
|
|
238
243
|
github_dir = target_dir / ".github"
|
|
244
|
+
instr_root = config_root if config_root is not None else github_dir
|
|
245
|
+
instr_label = "~/.copilot/instructions" if config_root is not None else ".github/instructions"
|
|
239
246
|
|
|
240
247
|
if emit_instructions:
|
|
241
|
-
instr_dir =
|
|
248
|
+
instr_dir = instr_root / "instructions"
|
|
242
249
|
instr_dir.mkdir(parents=True, exist_ok=True)
|
|
243
250
|
|
|
244
251
|
instruction_files: dict[str, callable] = dict(_make_instruction_files())
|
|
@@ -269,7 +276,7 @@ def generate(target_dir: Path, *,
|
|
|
269
276
|
|
|
270
277
|
for name, content_fn in instruction_files.items():
|
|
271
278
|
(instr_dir / name).write_text(content_fn(), encoding="utf-8")
|
|
272
|
-
print(f" Generated:
|
|
279
|
+
print(f" Generated: {instr_label}/{name}")
|
|
273
280
|
|
|
274
281
|
if emit_prompts:
|
|
275
282
|
prompt_dir = github_dir / "prompts"
|
|
@@ -3,10 +3,9 @@
|
|
|
3
3
|
|
|
4
4
|
Devin CLI uses a hook format **compatible with Claude Code hooks**
|
|
5
5
|
(docs.devin.ai/cli/extensibility/hooks/overview). This generator is the
|
|
6
|
-
replacement for the
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
regenerated onto this new file.
|
|
6
|
+
replacement for the removed Cascade ``.windsurf/hooks.json`` surface, which
|
|
7
|
+
stopped working on 2026-07-01. Devin Local and the standalone Devin CLI do not
|
|
8
|
+
read that legacy file as a fallback, so hooks are emitted here.
|
|
10
9
|
|
|
11
10
|
Output file: ``<target>/.devin/hooks.v1.json``. Per the Devin docs the
|
|
12
11
|
standalone ``hooks.v1.json`` file's entire contents ARE the hooks object —
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""Generate a Devin
|
|
2
|
+
"""Generate a Devin/Windsurf skill pointer.
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
and ``.
|
|
6
|
-
|
|
7
|
-
global install path).
|
|
4
|
+
Current Devin docs list ``.windsurf/skills/`` as a supported cross-tool skill
|
|
5
|
+
path and do not list ``.devin/skills/``. Pass an explicit ``skill_root`` for a
|
|
6
|
+
single alternate location (used by the legacy HOME-scoped Windsurf install).
|
|
8
7
|
"""
|
|
9
8
|
from __future__ import annotations
|
|
10
9
|
|
|
@@ -14,7 +13,7 @@ from pathlib import Path
|
|
|
14
13
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
15
14
|
from skill_pointer import POINTER_SKILL_NAME, write_pointer_skill
|
|
16
15
|
|
|
17
|
-
DEFAULT_SKILL_ROOTS: tuple[str, ...] = (".
|
|
16
|
+
DEFAULT_SKILL_ROOTS: tuple[str, ...] = (".windsurf/skills",)
|
|
18
17
|
|
|
19
18
|
|
|
20
19
|
def generate(target_dir: Path, *, emit_skill_pointer: bool = True,
|
|
@@ -49,8 +49,22 @@ sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
|
49
49
|
# Protected source tag -- this CLI must never touch ai-toolkit's own entries.
|
|
50
50
|
PROTECTED_SOURCE = "ai-toolkit"
|
|
51
51
|
|
|
52
|
-
# Codex CLI
|
|
53
|
-
|
|
52
|
+
# Codex CLI's HookEventName enum defines 10 events; we propagate the 9 that
|
|
53
|
+
# generate_codex_hooks.py also wires (all except PostCompact, whose only hook
|
|
54
|
+
# was the removed environment-snapshot probe). Keeping this in sync with the
|
|
55
|
+
# generator prevents injected custom hooks from silently losing events Codex
|
|
56
|
+
# supports.
|
|
57
|
+
CODEX_EVENTS = {
|
|
58
|
+
"SessionStart",
|
|
59
|
+
"PreToolUse",
|
|
60
|
+
"PostToolUse",
|
|
61
|
+
"PermissionRequest",
|
|
62
|
+
"UserPromptSubmit",
|
|
63
|
+
"SubagentStart",
|
|
64
|
+
"SubagentStop",
|
|
65
|
+
"PreCompact",
|
|
66
|
+
"Stop",
|
|
67
|
+
}
|
|
54
68
|
|
|
55
69
|
|
|
56
70
|
# ---------------------------------------------------------------------------
|
package/scripts/install.py
CHANGED
|
@@ -18,7 +18,7 @@ Other tools (global config locations):
|
|
|
18
18
|
- Roo Code: ~/.roo/rules/
|
|
19
19
|
- Aider: ~/.aider.conf.yml (created only if absent)
|
|
20
20
|
- Augment: ~/.augment/rules/ai-toolkit.md
|
|
21
|
-
- Codex:
|
|
21
|
+
- Codex: ~/.codex/AGENTS.md, ~/.agents/skills/, ~/.codex/hooks.json
|
|
22
22
|
- opencode: ~/.config/opencode/
|
|
23
23
|
|
|
24
24
|
Registered rules (~/.softspark/ai-toolkit/rules/*.md) are also synced into
|
|
@@ -750,40 +750,42 @@ def main() -> None:
|
|
|
750
750
|
global_eds = get_global_editors() or None
|
|
751
751
|
|
|
752
752
|
installed_eds = install_ai_tools(target_dir, rules_dir, dry_run,
|
|
753
|
-
editors=global_eds)
|
|
753
|
+
editors=global_eds, profile=profile)
|
|
754
754
|
install_persona(target_dir, persona, dry_run)
|
|
755
755
|
install_strict_git_hooks(profile, local, dry_run)
|
|
756
756
|
|
|
757
757
|
# Record install state (skip for dry-run)
|
|
758
758
|
if not dry_run:
|
|
759
|
-
auto_detected = None
|
|
760
|
-
if auto_detect:
|
|
761
|
-
auto_detected = detect_languages(project_dir, toolkit_dir)
|
|
762
|
-
|
|
763
|
-
# Determine what modules to record
|
|
764
|
-
if resolved_modules is not None:
|
|
765
|
-
record_modules = resolved_modules
|
|
766
|
-
else:
|
|
767
|
-
# Legacy mode: infer modules from profile/only
|
|
768
|
-
record_modules = _infer_modules_from_legacy(profile, only)
|
|
769
|
-
|
|
770
759
|
# Extract extends metadata if available
|
|
771
760
|
extends_info = None
|
|
772
761
|
merged = cfg.get("_merged_config")
|
|
773
762
|
if merged and merged.get("_extends_meta"):
|
|
774
763
|
extends_info = merged["_extends_meta"]
|
|
775
764
|
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
765
|
+
# state.json describes the global install and drives future `update`
|
|
766
|
+
# invocations. Project-local installs belong in projects.json and must
|
|
767
|
+
# never replace the global profile/modules with the last project's
|
|
768
|
+
# auto-detected language set.
|
|
769
|
+
if not local:
|
|
770
|
+
auto_detected = None
|
|
771
|
+
if auto_detect:
|
|
772
|
+
auto_detected = detect_languages(project_dir, toolkit_dir)
|
|
773
|
+
|
|
774
|
+
if resolved_modules is not None:
|
|
775
|
+
record_modules = resolved_modules
|
|
776
|
+
else:
|
|
777
|
+
record_modules = _infer_modules_from_legacy(profile, only)
|
|
778
|
+
|
|
779
|
+
record_install(
|
|
780
|
+
version=_get_toolkit_version(),
|
|
781
|
+
modules=record_modules,
|
|
782
|
+
profile=profile or "standard",
|
|
783
|
+
auto_detected=auto_detected,
|
|
784
|
+
extends_info=extends_info,
|
|
785
|
+
)
|
|
783
786
|
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
record_global_editors(installed_eds)
|
|
787
|
+
if installed_eds:
|
|
788
|
+
record_global_editors(installed_eds)
|
|
787
789
|
|
|
788
790
|
# Register project in global registry (for `ai-toolkit update` propagation)
|
|
789
791
|
# Skipped when called from update_projects.py (--skip-register) to avoid
|
|
@@ -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,22 +74,46 @@ 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:
|
|
81
111
|
if dry_run:
|
|
82
112
|
print(" Would generate: ~/Documents/Cline/Rules/ai-toolkit-*.md")
|
|
83
|
-
|
|
113
|
+
if _claude_skills_discoverable(target_dir):
|
|
114
|
+
print(" Would reuse: ~/.claude/skills/ (Cline-compatible discovery)")
|
|
115
|
+
else:
|
|
116
|
+
print(" Would generate: ~/.cline/skills/ai-toolkit-skill-catalogue/SKILL.md")
|
|
84
117
|
else:
|
|
85
118
|
_install_cline_global(target_dir, rules_dir)
|
|
86
119
|
installed.append("cline")
|
|
@@ -88,8 +121,15 @@ def install_ai_tools(target_dir: Path, rules_dir: Path,
|
|
|
88
121
|
if "roo" in eds:
|
|
89
122
|
if dry_run:
|
|
90
123
|
print(" Would generate: ~/.roo/rules/ai-toolkit-*.md")
|
|
124
|
+
if "codex" not in eds:
|
|
125
|
+
print(" Would generate: ~/.agents/skills/* (Roo native skill discovery)")
|
|
91
126
|
else:
|
|
92
127
|
_install_roo_global(target_dir, rules_dir)
|
|
128
|
+
# Roo/Zoo Code natively discover skills from ~/.agents/skills. Reuse
|
|
129
|
+
# the shared installer, but skip when codex is also selected — its
|
|
130
|
+
# branch populates the same directory — to avoid a duplicate pass.
|
|
131
|
+
if "codex" not in eds:
|
|
132
|
+
_install_codex_skills(target_dir)
|
|
93
133
|
installed.append("roo")
|
|
94
134
|
|
|
95
135
|
if "aider" in eds:
|
|
@@ -101,7 +141,7 @@ def install_ai_tools(target_dir: Path, rules_dir: Path,
|
|
|
101
141
|
|
|
102
142
|
if "codex" in eds:
|
|
103
143
|
if dry_run:
|
|
104
|
-
print(" Would inject:
|
|
144
|
+
print(" Would inject: ~/.codex/AGENTS.md, ~/.agents/skills/, ~/.codex/hooks.json")
|
|
105
145
|
else:
|
|
106
146
|
_install_codex_global(target_dir, rules_dir)
|
|
107
147
|
installed.append("codex")
|
|
@@ -114,28 +154,85 @@ def install_ai_tools(target_dir: Path, rules_dir: Path,
|
|
|
114
154
|
_install_opencode_global(target_dir, rules_dir)
|
|
115
155
|
installed.append("opencode")
|
|
116
156
|
|
|
157
|
+
if "cursor" in eds:
|
|
158
|
+
# Cursor RULES have no mergeable global file surface (Settings UI only),
|
|
159
|
+
# but ~/.cursor/hooks.json is a documented user-level hooks scope — so a
|
|
160
|
+
# global Cursor install activates the safety/quality hooks everywhere.
|
|
161
|
+
if dry_run:
|
|
162
|
+
if add_hooks:
|
|
163
|
+
print(" Would generate: ~/.cursor/hooks.json (global hooks)")
|
|
164
|
+
elif add_hooks:
|
|
165
|
+
_try_generator("generate_cursor_hooks", target_dir)
|
|
166
|
+
installed.append("cursor")
|
|
167
|
+
|
|
168
|
+
if "copilot" in eds:
|
|
169
|
+
# Copilot CLI reads user-level instructions from ~/.copilot/. RULES have
|
|
170
|
+
# a documented global surface here even though .github/ stays repo-only.
|
|
171
|
+
copilot_root = target_dir / ".copilot"
|
|
172
|
+
if dry_run:
|
|
173
|
+
print(" Would inject: ~/.copilot/copilot-instructions.md")
|
|
174
|
+
print(" Would generate: ~/.copilot/instructions/ai-toolkit-*.instructions.md")
|
|
175
|
+
else:
|
|
176
|
+
inject_with_rules(
|
|
177
|
+
"generate_copilot.py",
|
|
178
|
+
copilot_root / "copilot-instructions.md",
|
|
179
|
+
rules_dir,
|
|
180
|
+
)
|
|
181
|
+
_try_generator("generate_copilot", target_dir,
|
|
182
|
+
rules_dir=rules_dir, config_root=copilot_root,
|
|
183
|
+
emit_prompts=False)
|
|
184
|
+
installed.append("copilot")
|
|
185
|
+
|
|
186
|
+
if "antigravity" in eds:
|
|
187
|
+
# Antigravity RULES stay project-local, but the skill pointer has a
|
|
188
|
+
# documented global surface: ~/.gemini/config/skills (all Antigravity
|
|
189
|
+
# products) and ~/.gemini/antigravity-cli/skills (CLI-private).
|
|
190
|
+
if dry_run:
|
|
191
|
+
print(" Would generate: ~/.gemini/config/skills/, ~/.gemini/antigravity-cli/skills/ (skill pointer)")
|
|
192
|
+
else:
|
|
193
|
+
from generate_antigravity import generate_global as gen_antigravity_global
|
|
194
|
+
gen_antigravity_global(target_dir)
|
|
195
|
+
installed.append("antigravity")
|
|
196
|
+
|
|
117
197
|
print()
|
|
118
198
|
print(f" Available: {', '.join(GLOBAL_CAPABLE_EDITORS)}")
|
|
119
|
-
print(" Note: Cursor
|
|
199
|
+
print(" Note: Cursor and Antigravity RULES stay project-local (no mergeable "
|
|
200
|
+
"global file surface); their global installs cover hooks/skills only. "
|
|
201
|
+
"Use 'ai-toolkit install --local' for full per-project setup.")
|
|
120
202
|
|
|
121
203
|
return installed
|
|
122
204
|
|
|
123
205
|
|
|
124
206
|
def _install_codex_global(target_dir: Path, rules_dir: Path) -> None:
|
|
125
|
-
"""Install Codex at the global level (
|
|
207
|
+
"""Install Codex at the global level (~/.codex layer).
|
|
126
208
|
|
|
127
209
|
Creates:
|
|
128
|
-
-
|
|
129
|
-
Codex reads instructions
|
|
130
|
-
|
|
210
|
+
- ~/.codex/AGENTS.md (marker injection; universal coding rules inlined
|
|
211
|
+
here). Codex reads GLOBAL instructions from ``$CODEX_HOME/AGENTS.md``
|
|
212
|
+
(default ~/.codex/AGENTS.md), NOT ~/AGENTS.md — a home-root AGENTS.md
|
|
213
|
+
is only loaded in the degenerate case where a session's cwd is $HOME.
|
|
214
|
+
- ~/.agents/skills/* (skill symlinks; a documented Codex skill dir)
|
|
131
215
|
- ~/.codex/hooks.json (lifecycle hooks)
|
|
132
216
|
"""
|
|
133
217
|
inject_with_rules(
|
|
134
218
|
"generate_codex.py",
|
|
135
|
-
target_dir / "AGENTS.md",
|
|
219
|
+
target_dir / ".codex" / "AGENTS.md",
|
|
136
220
|
rules_dir,
|
|
137
221
|
)
|
|
138
222
|
|
|
223
|
+
# Migration: earlier versions wrote global instructions to ~/AGENTS.md,
|
|
224
|
+
# which Codex never loads as global instructions. Strip that stale toolkit
|
|
225
|
+
# section so upgraders are not left with dead, unread content.
|
|
226
|
+
if _strip_toolkit_sections(target_dir / "AGENTS.md"):
|
|
227
|
+
print(" Migrated: removed stale ai-toolkit section from ~/AGENTS.md")
|
|
228
|
+
|
|
229
|
+
override = target_dir / ".codex" / "AGENTS.override.md"
|
|
230
|
+
if override.is_file() and override.read_text(encoding="utf-8").strip():
|
|
231
|
+
print(
|
|
232
|
+
" Warning: ~/.codex/AGENTS.override.md exists and takes precedence "
|
|
233
|
+
"over ~/.codex/AGENTS.md — toolkit rules will be masked."
|
|
234
|
+
)
|
|
235
|
+
|
|
139
236
|
from generate_codex_hooks import generate as gen_codex_hooks
|
|
140
237
|
gen_codex_hooks(target_dir)
|
|
141
238
|
print(" Created: ~/.codex/hooks.json")
|
|
@@ -166,6 +263,13 @@ def _install_windsurf_global(target_dir: Path, rules_dir: Path) -> None:
|
|
|
166
263
|
windsurf_file = target_dir / ".codeium" / "windsurf" / "memories" / "global_rules.md"
|
|
167
264
|
inject_with_rules("generate-windsurf.sh", windsurf_file, rules_dir)
|
|
168
265
|
|
|
266
|
+
# Devin CLI reads global rules from ~/.config/devin/AGENTS.md, NOT the
|
|
267
|
+
# Desktop ~/.codeium/windsurf/memories/global_rules.md (that path is absent
|
|
268
|
+
# from read_config_from.windsurf). Cover editor-only Devin CLI installs
|
|
269
|
+
# where no global Claude install (~/.claude/CLAUDE.md compat read) exists.
|
|
270
|
+
devin_agents = target_dir / ".config" / "devin" / "AGENTS.md"
|
|
271
|
+
inject_with_rules("generate-windsurf.sh", devin_agents, rules_dir)
|
|
272
|
+
|
|
169
273
|
from generate_windsurf_skills import generate as gen_windsurf_skills
|
|
170
274
|
# Windsurf is excluded from the .claude/skills conditional: its native scan
|
|
171
275
|
# of .claude/skills is gated behind a Devin "Claude Code config reading"
|
|
@@ -176,6 +280,80 @@ def _install_windsurf_global(target_dir: Path, rules_dir: Path) -> None:
|
|
|
176
280
|
)
|
|
177
281
|
|
|
178
282
|
|
|
283
|
+
def _cleanup_retired_windsurf_surfaces(cwd: Path) -> None:
|
|
284
|
+
"""Remove only ai-toolkit content from retired/undocumented Devin paths."""
|
|
285
|
+
import json
|
|
286
|
+
|
|
287
|
+
legacy_hooks = cwd / ".windsurf" / "hooks.json"
|
|
288
|
+
if legacy_hooks.is_file():
|
|
289
|
+
try:
|
|
290
|
+
document = json.loads(legacy_hooks.read_text(encoding="utf-8"))
|
|
291
|
+
except (OSError, json.JSONDecodeError):
|
|
292
|
+
print(" Warning: kept invalid .windsurf/hooks.json for manual review")
|
|
293
|
+
else:
|
|
294
|
+
hooks = document.get("hooks") if isinstance(document, dict) else None
|
|
295
|
+
changed = False
|
|
296
|
+
if isinstance(hooks, dict):
|
|
297
|
+
kept_hooks: dict = {}
|
|
298
|
+
for event, entries in hooks.items():
|
|
299
|
+
if not isinstance(entries, list):
|
|
300
|
+
kept_hooks[event] = entries
|
|
301
|
+
continue
|
|
302
|
+
survivors = [
|
|
303
|
+
entry
|
|
304
|
+
for entry in entries
|
|
305
|
+
if not (
|
|
306
|
+
isinstance(entry, dict)
|
|
307
|
+
and (
|
|
308
|
+
entry.get("_source") == "ai-toolkit"
|
|
309
|
+
or any(
|
|
310
|
+
isinstance(handler, dict)
|
|
311
|
+
and handler.get("_source") == "ai-toolkit"
|
|
312
|
+
for handler in entry.get("hooks", [])
|
|
313
|
+
)
|
|
314
|
+
)
|
|
315
|
+
)
|
|
316
|
+
]
|
|
317
|
+
if len(survivors) != len(entries):
|
|
318
|
+
changed = True
|
|
319
|
+
if survivors:
|
|
320
|
+
kept_hooks[event] = survivors
|
|
321
|
+
if changed:
|
|
322
|
+
if kept_hooks:
|
|
323
|
+
document["hooks"] = kept_hooks
|
|
324
|
+
else:
|
|
325
|
+
document.pop("hooks", None)
|
|
326
|
+
if document:
|
|
327
|
+
legacy_hooks.write_text(
|
|
328
|
+
json.dumps(
|
|
329
|
+
document,
|
|
330
|
+
indent=4,
|
|
331
|
+
ensure_ascii=False,
|
|
332
|
+
sort_keys=True,
|
|
333
|
+
) + "\n",
|
|
334
|
+
encoding="utf-8",
|
|
335
|
+
)
|
|
336
|
+
else:
|
|
337
|
+
legacy_hooks.unlink()
|
|
338
|
+
print(" Migrated: removed retired ai-toolkit Cascade hooks")
|
|
339
|
+
|
|
340
|
+
retired_pointer = (
|
|
341
|
+
cwd / ".devin" / "skills" / "ai-toolkit-skill-catalogue"
|
|
342
|
+
)
|
|
343
|
+
skill_file = retired_pointer / "SKILL.md"
|
|
344
|
+
if skill_file.is_file() and "name: ai-toolkit-skill-catalogue" in (
|
|
345
|
+
skill_file.read_text(encoding="utf-8")
|
|
346
|
+
):
|
|
347
|
+
if retired_pointer.is_symlink():
|
|
348
|
+
retired_pointer.unlink()
|
|
349
|
+
else:
|
|
350
|
+
shutil.rmtree(retired_pointer)
|
|
351
|
+
for parent in (retired_pointer.parent, retired_pointer.parent.parent):
|
|
352
|
+
if parent.is_dir() and not any(parent.iterdir()):
|
|
353
|
+
parent.rmdir()
|
|
354
|
+
print(" Migrated: removed undocumented .devin/skills toolkit pointer")
|
|
355
|
+
|
|
356
|
+
|
|
179
357
|
def _install_cline_global(target_dir: Path, rules_dir: Path) -> None:
|
|
180
358
|
"""Install Cline global rules in the documented ~/.cline directory."""
|
|
181
359
|
from generate_cline_rules import generate as gen_cline_rules
|
|
@@ -352,6 +530,35 @@ def inject_with_rules(
|
|
|
352
530
|
print(f" Updated: {target_file}")
|
|
353
531
|
|
|
354
532
|
|
|
533
|
+
def _strip_toolkit_sections(target_file: Path) -> bool:
|
|
534
|
+
"""Remove ai-toolkit ``<!-- TOOLKIT:* -->`` marker sections from an existing
|
|
535
|
+
file, preserving any non-toolkit content. Returns True if the file changed.
|
|
536
|
+
|
|
537
|
+
Used to clean a stale ~/AGENTS.md after Codex global instructions moved to
|
|
538
|
+
~/.codex/AGENTS.md. If stripping leaves the file empty (it only ever held
|
|
539
|
+
toolkit content), the file is removed.
|
|
540
|
+
"""
|
|
541
|
+
if not target_file.is_file():
|
|
542
|
+
return False
|
|
543
|
+
import re
|
|
544
|
+
|
|
545
|
+
original = target_file.read_text(encoding="utf-8")
|
|
546
|
+
stripped = re.sub(
|
|
547
|
+
r"<!-- TOOLKIT:[^ ]+ START -->.*?<!-- TOOLKIT:[^ ]+ END -->\n?",
|
|
548
|
+
"",
|
|
549
|
+
original,
|
|
550
|
+
flags=re.DOTALL,
|
|
551
|
+
)
|
|
552
|
+
if stripped == original:
|
|
553
|
+
return False
|
|
554
|
+
stripped = stripped.lstrip("\n")
|
|
555
|
+
if stripped.strip():
|
|
556
|
+
target_file.write_text(stripped, encoding="utf-8")
|
|
557
|
+
else:
|
|
558
|
+
target_file.unlink()
|
|
559
|
+
return True
|
|
560
|
+
|
|
561
|
+
|
|
355
562
|
def _inject_text_section(target_file: Path, section: str, text: str) -> None:
|
|
356
563
|
"""Inject generated text into one marker section without touching others."""
|
|
357
564
|
target_file.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -811,6 +1018,18 @@ def _install_local_dry_run(reset: bool, editors: list[str] | None = None,
|
|
|
811
1018
|
if ed in eds:
|
|
812
1019
|
print(msg)
|
|
813
1020
|
|
|
1021
|
+
if "windsurf" in eds:
|
|
1022
|
+
if (Path.cwd() / ".windsurf" / "hooks.json").is_file():
|
|
1023
|
+
print(" Would migrate: remove retired ai-toolkit Cascade hooks")
|
|
1024
|
+
if (
|
|
1025
|
+
Path.cwd()
|
|
1026
|
+
/ ".devin"
|
|
1027
|
+
/ "skills"
|
|
1028
|
+
/ "ai-toolkit-skill-catalogue"
|
|
1029
|
+
/ "SKILL.md"
|
|
1030
|
+
).is_file():
|
|
1031
|
+
print(" Would migrate: remove undocumented .devin/skills toolkit pointer")
|
|
1032
|
+
|
|
814
1033
|
# Profile-driven extras (matrix in kb/reference/global-install-model.md)
|
|
815
1034
|
if "copilot" in eds and add_copilot_dir:
|
|
816
1035
|
print(" Would generate: .github/instructions/ + .github/prompts/ (profile >= standard)")
|
|
@@ -820,7 +1039,7 @@ def _install_local_dry_run(reset: bool, editors: list[str] | None = None,
|
|
|
820
1039
|
if "cursor" in eds:
|
|
821
1040
|
print(" Would generate: .cursor/hooks.json + .cursor/agents/ + .cursor/skills/ (profile=full)")
|
|
822
1041
|
if "windsurf" in eds:
|
|
823
|
-
print(" Would generate: .devin/hooks.v1.json + .windsurf/
|
|
1042
|
+
print(" Would generate: .devin/hooks.v1.json + .windsurf/skills/ (profile=full)")
|
|
824
1043
|
if "cline" in eds:
|
|
825
1044
|
print(" Would generate: .cline/skills/ (profile=full)")
|
|
826
1045
|
if "augment" in eds:
|
|
@@ -1044,6 +1263,7 @@ def _create_local_ai_tool_configs(cwd: Path, rules_dir: Path,
|
|
|
1044
1263
|
_try_generator("generate_cursor_skills", cwd, emit_skill_pointer=emit_pointer)
|
|
1045
1264
|
|
|
1046
1265
|
if "windsurf" in eds:
|
|
1266
|
+
_cleanup_retired_windsurf_surfaces(cwd)
|
|
1047
1267
|
inject_with_rules(
|
|
1048
1268
|
"generate-windsurf.sh",
|
|
1049
1269
|
cwd / ".windsurfrules",
|
|
@@ -1053,11 +1273,11 @@ def _create_local_ai_tool_configs(cwd: Path, rules_dir: Path,
|
|
|
1053
1273
|
gen_windsurf_rules(cwd, language_modules=language_modules,
|
|
1054
1274
|
rules_dir=rules_dir)
|
|
1055
1275
|
if add_native_surfaces:
|
|
1056
|
-
# .windsurf/hooks.json
|
|
1057
|
-
# .devin/hooks.v1.json
|
|
1058
|
-
_try_generator("generate_windsurf_hooks", cwd)
|
|
1276
|
+
# Cascade's .windsurf/hooks.json surface ended on 2026-07-01.
|
|
1277
|
+
# Devin CLI uses the Claude-compatible .devin/hooks.v1.json format.
|
|
1059
1278
|
_try_generator("generate_devin_hooks", cwd)
|
|
1060
|
-
#
|
|
1279
|
+
# Current Devin docs list .windsurf/skills as a supported path;
|
|
1280
|
+
# .devin/skills is not documented.
|
|
1061
1281
|
_try_generator("generate_windsurf_skills", cwd)
|
|
1062
1282
|
|
|
1063
1283
|
if "cline" in eds:
|
|
@@ -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",
|
|
@@ -190,6 +197,8 @@ def record_install(
|
|
|
190
197
|
state["profile"] = profile
|
|
191
198
|
if auto_detected is not None:
|
|
192
199
|
state["auto_detected_languages"] = sorted(auto_detected)
|
|
200
|
+
else:
|
|
201
|
+
state.pop("auto_detected_languages", None)
|
|
193
202
|
|
|
194
203
|
if extends_info is not None:
|
|
195
204
|
state["extends"] = {
|