@softspark/ai-toolkit 4.24.0 → 4.25.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 +66 -0
- package/README.md +35 -15
- package/app/.claude-plugin/plugin.json +1 -1
- package/app/skills/hook-creator/SKILL.md +18 -4
- package/app/surface.json +4 -0
- package/benchmarks/ecosystem-doctor-snapshot.json +67 -19
- package/bin/ai-toolkit.js +27 -3
- package/kb/reference/architecture-overview.md +13 -5
- package/kb/reference/claude-ecosystem-expansion-foundations.md +30 -5
- package/kb/reference/cli-reference.md +27 -2
- package/kb/reference/codex-cli-compatibility.md +101 -11
- package/kb/reference/global-install-model.md +10 -8
- package/kb/reference/hooks-catalog.md +41 -5
- package/kb/reference/mcp-editor-compatibility.md +3 -3
- package/kb/reference/mcp-templates.md +3 -3
- package/kb/reference/opencode-compatibility.md +53 -5
- package/kb/reference/supported-tools-registry.md +31 -26
- package/llms-full.txt +312 -73
- package/manifest.json +1 -1
- package/package.json +6 -2
- package/scripts/antigravity_plugin.py +570 -0
- package/scripts/codex_plugin.py +764 -0
- package/scripts/ecosystem_tools.json +92 -17
- package/scripts/generate_antigravity.py +16 -14
- package/scripts/generate_antigravity_agents.py +255 -0
- package/scripts/generate_antigravity_hooks.py +344 -0
- package/scripts/generate_cline_hooks.py +391 -0
- package/scripts/generate_cline_rules.py +210 -43
- package/scripts/generate_cline_skills.py +65 -2
- package/scripts/generate_codex_hooks.py +70 -8
- package/scripts/generate_gemini_agents.py +197 -0
- package/scripts/generate_gemini_hooks.py +24 -4
- package/scripts/generate_opencode_skills.py +544 -0
- package/scripts/inject_hook_cli.py +4 -26
- package/scripts/install.py +11 -10
- package/scripts/install_steps/ai_tools.py +209 -34
- package/scripts/mcp_editors.py +9 -1
- package/scripts/plugin.py +21 -0
- package/scripts/plugin_schema.py +8 -7
- package/scripts/secure_fs.py +35 -0
- package/scripts/uninstall.py +162 -18
- package/scripts/validate.py +42 -12
|
@@ -88,17 +88,29 @@ def install_ai_tools(target_dir: Path, rules_dir: Path,
|
|
|
88
88
|
if add_hooks:
|
|
89
89
|
print(" Would generate: ~/.gemini/settings.json (hooks)")
|
|
90
90
|
if add_native_surfaces:
|
|
91
|
-
print(
|
|
91
|
+
print(
|
|
92
|
+
" Would generate: ~/.gemini/commands/, ~/.gemini/skills/, "
|
|
93
|
+
"~/.gemini/agents/"
|
|
94
|
+
)
|
|
92
95
|
else:
|
|
93
96
|
inject_with_rules("generate-gemini.sh", gemini_file, rules_dir)
|
|
94
|
-
# Hooks (~/.gemini/settings.json), commands
|
|
95
|
-
# and
|
|
96
|
-
#
|
|
97
|
+
# Hooks (~/.gemini/settings.json), commands, the skills pointer,
|
|
98
|
+
# and native agents are documented user-tier surfaces. Install
|
|
99
|
+
# them globally with the same profile gates as the local install.
|
|
97
100
|
if add_hooks:
|
|
98
101
|
_try_generator("generate_gemini_hooks", target_dir)
|
|
102
|
+
else:
|
|
103
|
+
from generate_gemini_hooks import cleanup as cleanup_gemini_hooks
|
|
104
|
+
|
|
105
|
+
cleanup_gemini_hooks(target_dir)
|
|
99
106
|
if add_native_surfaces:
|
|
100
107
|
_try_generator("generate_gemini_commands", target_dir)
|
|
101
108
|
_try_generator("generate_gemini_skills", target_dir)
|
|
109
|
+
_try_generator("generate_gemini_agents", target_dir)
|
|
110
|
+
else:
|
|
111
|
+
from generate_gemini_agents import cleanup as cleanup_gemini_agents
|
|
112
|
+
|
|
113
|
+
cleanup_gemini_agents(target_dir)
|
|
102
114
|
installed.append("gemini")
|
|
103
115
|
|
|
104
116
|
if "augment" in eds:
|
|
@@ -120,13 +132,21 @@ def install_ai_tools(target_dir: Path, rules_dir: Path,
|
|
|
120
132
|
|
|
121
133
|
if "cline" in eds:
|
|
122
134
|
if dry_run:
|
|
123
|
-
print(
|
|
135
|
+
print(
|
|
136
|
+
" Would generate: ~/.cline/rules/ai-toolkit-*.md + "
|
|
137
|
+
"~/Documents/Cline/Rules/ai-toolkit-*.md"
|
|
138
|
+
)
|
|
139
|
+
if add_hooks:
|
|
140
|
+
print(
|
|
141
|
+
" Would generate: ~/.cline/hooks/<Event> + "
|
|
142
|
+
"~/Documents/Cline/Hooks/<Event>"
|
|
143
|
+
)
|
|
124
144
|
if _claude_skills_discoverable(target_dir):
|
|
125
145
|
print(" Would reuse: ~/.claude/skills/ (Cline-compatible discovery)")
|
|
126
146
|
else:
|
|
127
147
|
print(" Would generate: ~/.cline/skills/ai-toolkit-skill-catalogue/SKILL.md")
|
|
128
148
|
else:
|
|
129
|
-
_install_cline_global(target_dir, rules_dir)
|
|
149
|
+
_install_cline_global(target_dir, rules_dir, add_hooks=add_hooks)
|
|
130
150
|
installed.append("cline")
|
|
131
151
|
|
|
132
152
|
if "roo" in eds:
|
|
@@ -163,8 +183,14 @@ def install_ai_tools(target_dir: Path, rules_dir: Path,
|
|
|
163
183
|
if dry_run:
|
|
164
184
|
print(" Would inject: ~/.config/opencode/{AGENTS.md, agents/, "
|
|
165
185
|
"commands/, plugins/ai-toolkit-hooks.js, opencode.json}")
|
|
186
|
+
if add_native_surfaces:
|
|
187
|
+
print(" Would generate: ~/.config/opencode/skills/ (profile=full)")
|
|
166
188
|
else:
|
|
167
|
-
_install_opencode_global(
|
|
189
|
+
_install_opencode_global(
|
|
190
|
+
target_dir,
|
|
191
|
+
rules_dir,
|
|
192
|
+
add_native_surfaces=add_native_surfaces,
|
|
193
|
+
)
|
|
168
194
|
installed.append("opencode")
|
|
169
195
|
|
|
170
196
|
if "cursor" in eds:
|
|
@@ -211,20 +237,51 @@ def install_ai_tools(target_dir: Path, rules_dir: Path,
|
|
|
211
237
|
installed.append("copilot")
|
|
212
238
|
|
|
213
239
|
if "antigravity" in eds:
|
|
214
|
-
# Antigravity
|
|
215
|
-
#
|
|
216
|
-
# products) and ~/.gemini/antigravity-cli/skills (CLI-private).
|
|
240
|
+
# Antigravity rules stay project-local. CLI and IDE/shared product each
|
|
241
|
+
# own a canonical skill root; full additionally installs subagents.
|
|
217
242
|
if dry_run:
|
|
218
|
-
print(
|
|
243
|
+
print(
|
|
244
|
+
" Would generate: ~/.gemini/antigravity-cli/skills/ "
|
|
245
|
+
"(current CLI skills)"
|
|
246
|
+
)
|
|
247
|
+
print(
|
|
248
|
+
" Would generate: ~/.gemini/config/skills/ "
|
|
249
|
+
"(current IDE/shared skills)"
|
|
250
|
+
)
|
|
251
|
+
if add_hooks:
|
|
252
|
+
print(" Would generate: ~/.gemini/config/hooks.json + hooks runtime")
|
|
253
|
+
if add_native_surfaces:
|
|
254
|
+
print(" Would generate: ~/.gemini/config/agents/ (profile=full)")
|
|
219
255
|
else:
|
|
220
256
|
from generate_antigravity import generate_global as gen_antigravity_global
|
|
221
257
|
gen_antigravity_global(target_dir)
|
|
258
|
+
if add_hooks:
|
|
259
|
+
_try_generator(
|
|
260
|
+
"generate_antigravity_hooks",
|
|
261
|
+
target_dir,
|
|
262
|
+
global_install=True,
|
|
263
|
+
)
|
|
264
|
+
else:
|
|
265
|
+
from generate_antigravity_hooks import cleanup as cleanup_hooks
|
|
266
|
+
|
|
267
|
+
cleanup_hooks(target_dir, global_install=True)
|
|
268
|
+
antigravity_config = target_dir / ".gemini" / "config"
|
|
269
|
+
if add_native_surfaces:
|
|
270
|
+
_try_generator(
|
|
271
|
+
"generate_antigravity_agents",
|
|
272
|
+
target_dir,
|
|
273
|
+
config_root=antigravity_config,
|
|
274
|
+
)
|
|
275
|
+
else:
|
|
276
|
+
from generate_antigravity_agents import cleanup as cleanup_agents
|
|
277
|
+
|
|
278
|
+
cleanup_agents(target_dir, config_root=antigravity_config)
|
|
222
279
|
installed.append("antigravity")
|
|
223
280
|
|
|
224
281
|
print()
|
|
225
282
|
print(f" Available: {', '.join(GLOBAL_CAPABLE_EDITORS)}")
|
|
226
283
|
print(" Note: Cursor and Antigravity RULES stay project-local (no mergeable "
|
|
227
|
-
"global file surface);
|
|
284
|
+
"global file surface); Antigravity full also installs native agents. "
|
|
228
285
|
"Use 'ai-toolkit install --local' for full per-project setup.")
|
|
229
286
|
|
|
230
287
|
return installed
|
|
@@ -418,21 +475,46 @@ def _cleanup_retired_output_filter_policy(cwd: Path) -> None:
|
|
|
418
475
|
print(f" Migrated: removed retired .claude/{PROJECT_POLICY_NAME}")
|
|
419
476
|
|
|
420
477
|
|
|
421
|
-
def _install_cline_global(
|
|
422
|
-
|
|
478
|
+
def _install_cline_global(
|
|
479
|
+
target_dir: Path,
|
|
480
|
+
rules_dir: Path,
|
|
481
|
+
*,
|
|
482
|
+
add_hooks: bool,
|
|
483
|
+
) -> None:
|
|
484
|
+
"""Install native Cline CLI surfaces plus extension compatibility."""
|
|
423
485
|
from generate_cline_rules import generate as gen_cline_rules
|
|
424
486
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
487
|
+
for rules_root, label in (
|
|
488
|
+
(target_dir / ".cline" / "rules", "~/.cline/rules"),
|
|
489
|
+
(
|
|
490
|
+
target_dir / "Documents" / "Cline" / "Rules",
|
|
491
|
+
"~/Documents/Cline/Rules",
|
|
492
|
+
),
|
|
493
|
+
):
|
|
494
|
+
gen_cline_rules(
|
|
495
|
+
target_dir,
|
|
496
|
+
rules_dir=rules_dir,
|
|
497
|
+
output_root=rules_root,
|
|
498
|
+
emit_workflows=False,
|
|
499
|
+
managed_scopes=("standard", "custom"),
|
|
500
|
+
)
|
|
501
|
+
print(f" Created: {label}/ai-toolkit-*.md")
|
|
502
|
+
|
|
503
|
+
from generate_cline_hooks import (
|
|
504
|
+
cleanup as cleanup_cline_hooks,
|
|
505
|
+
generate_extension_global,
|
|
506
|
+
generate_global,
|
|
434
507
|
)
|
|
435
|
-
|
|
508
|
+
|
|
509
|
+
extension_hooks = target_dir / "Documents" / "Cline" / "Hooks"
|
|
510
|
+
if add_hooks:
|
|
511
|
+
generate_global(target_dir)
|
|
512
|
+
generate_extension_global(target_dir)
|
|
513
|
+
print(" Created: ~/.cline/hooks/<Event>")
|
|
514
|
+
print(" Created: ~/Documents/Cline/Hooks/<Event>")
|
|
515
|
+
else:
|
|
516
|
+
cleanup_cline_hooks(target_dir)
|
|
517
|
+
cleanup_cline_hooks(target_dir, hooks_root=extension_hooks)
|
|
436
518
|
|
|
437
519
|
from generate_cline_skills import generate as gen_cline_skills
|
|
438
520
|
gen_cline_skills(target_dir, emit_skill_pointer=not _claude_skills_discoverable(target_dir))
|
|
@@ -494,13 +576,19 @@ def _aider_default_model() -> str:
|
|
|
494
576
|
return DEFAULT_CLAUDE_MODELS["sonnet"]
|
|
495
577
|
|
|
496
578
|
|
|
497
|
-
def _install_opencode_global(
|
|
579
|
+
def _install_opencode_global(
|
|
580
|
+
target_dir: Path,
|
|
581
|
+
rules_dir: Path,
|
|
582
|
+
*,
|
|
583
|
+
add_native_surfaces: bool,
|
|
584
|
+
) -> None:
|
|
498
585
|
"""Install opencode at the global level (~/.config/opencode/).
|
|
499
586
|
|
|
500
587
|
Creates:
|
|
501
588
|
- ~/.config/opencode/AGENTS.md (marker injection with rules)
|
|
502
589
|
- ~/.config/opencode/agents/ai-toolkit-*.md (subagents)
|
|
503
590
|
- ~/.config/opencode/commands/ai-toolkit-*.md (slash commands)
|
|
591
|
+
- ~/.config/opencode/skills/*/SKILL.md (native skills, profile=full)
|
|
504
592
|
- ~/.config/opencode/plugins/ai-toolkit-hooks.js (hook bridge)
|
|
505
593
|
- ~/.config/opencode/opencode.json (MCP merge, preserves user keys)
|
|
506
594
|
"""
|
|
@@ -527,6 +615,32 @@ def _install_opencode_global(target_dir: Path, rules_dir: Path) -> None:
|
|
|
527
615
|
msg += ")"
|
|
528
616
|
print(msg)
|
|
529
617
|
|
|
618
|
+
if add_native_surfaces:
|
|
619
|
+
from generate_opencode_skills import generate as gen_opencode_skills
|
|
620
|
+
|
|
621
|
+
written, removed, preserved = gen_opencode_skills(
|
|
622
|
+
target_dir,
|
|
623
|
+
config_root=opencode_home,
|
|
624
|
+
)
|
|
625
|
+
msg = f" Created: ~/.config/opencode/skills/ ({written} skills"
|
|
626
|
+
if removed:
|
|
627
|
+
msg += f", {removed} stale removed"
|
|
628
|
+
if preserved:
|
|
629
|
+
msg += f", {preserved} user-owned preserved"
|
|
630
|
+
print(msg + ")")
|
|
631
|
+
else:
|
|
632
|
+
from generate_opencode_skills import cleanup as cleanup_opencode_skills
|
|
633
|
+
|
|
634
|
+
removed, _ = cleanup_opencode_skills(
|
|
635
|
+
target_dir,
|
|
636
|
+
config_root=opencode_home,
|
|
637
|
+
)
|
|
638
|
+
if removed:
|
|
639
|
+
print(
|
|
640
|
+
" Removed: ~/.config/opencode/skills/ "
|
|
641
|
+
f"({removed} managed skills)"
|
|
642
|
+
)
|
|
643
|
+
|
|
530
644
|
from generate_opencode_plugin import generate as gen_opencode_plugin
|
|
531
645
|
gen_opencode_plugin(target_dir, config_root=opencode_home)
|
|
532
646
|
print(" Created: ~/.config/opencode/plugins/ai-toolkit-hooks.js")
|
|
@@ -660,6 +774,9 @@ _EDITOR_MARKERS: dict[str, str] = {
|
|
|
660
774
|
# .windsurf/ markers stay to detect legacy installs.
|
|
661
775
|
".devin/rules": "windsurf",
|
|
662
776
|
".clinerules": "cline",
|
|
777
|
+
".cline/rules": "cline",
|
|
778
|
+
".cline/hooks": "cline",
|
|
779
|
+
".cline/agents": "cline",
|
|
663
780
|
".roomodes": "roo",
|
|
664
781
|
".roo/rules": "roo",
|
|
665
782
|
".aider.conf.yml": "aider",
|
|
@@ -692,8 +809,8 @@ def _detect_editors(cwd: Path) -> list[str]:
|
|
|
692
809
|
if not p.exists():
|
|
693
810
|
continue
|
|
694
811
|
if marker == ".agents/skills" and _is_pointer_only_skills_dir(p):
|
|
695
|
-
# The Antigravity
|
|
696
|
-
# only
|
|
812
|
+
# The canonical workspace Antigravity pointer also lives in
|
|
813
|
+
# .agents/skills/; only materialized skills indicate Codex.
|
|
697
814
|
continue
|
|
698
815
|
found.add(editor)
|
|
699
816
|
return sorted(found)
|
|
@@ -1040,6 +1157,7 @@ def _install_local_dry_run(reset: bool, editors: list[str] | None = None,
|
|
|
1040
1157
|
|
|
1041
1158
|
add_copilot_dir = profile in {"standard", "strict", "full"}
|
|
1042
1159
|
add_gemini_hooks = profile in {"standard", "strict", "full"}
|
|
1160
|
+
add_cline_hooks = profile in {"standard", "strict", "full"}
|
|
1043
1161
|
add_native_surfaces = profile == "full"
|
|
1044
1162
|
|
|
1045
1163
|
# Editor-specific dry-run messages
|
|
@@ -1047,7 +1165,7 @@ def _install_local_dry_run(reset: bool, editors: list[str] | None = None,
|
|
|
1047
1165
|
"copilot": " Would inject: .github/copilot-instructions.md + AGENTS.md",
|
|
1048
1166
|
"cursor": " Would generate: .cursorrules + .cursor/rules/*.mdc",
|
|
1049
1167
|
"windsurf": " Would generate: .windsurfrules + .devin/rules/*.md + .windsurf/rules/*.md",
|
|
1050
|
-
"cline": " Would generate: .clinerules/*.md",
|
|
1168
|
+
"cline": " Would generate: .cline/rules/*.md + .clinerules/*.md",
|
|
1051
1169
|
"roo": " Would generate: .roomodes + .roo/rules/*.md",
|
|
1052
1170
|
"aider": " Would generate: .aider.conf.yml + CONVENTIONS.md",
|
|
1053
1171
|
"augment": " Would generate: .augment/rules/ai-toolkit-*.md",
|
|
@@ -1079,6 +1197,13 @@ def _install_local_dry_run(reset: bool, editors: list[str] | None = None,
|
|
|
1079
1197
|
".github/hooks/ (profile >= standard)")
|
|
1080
1198
|
if "gemini" in eds and add_gemini_hooks:
|
|
1081
1199
|
print(" Would generate: .gemini/settings.json hooks (profile >= standard)")
|
|
1200
|
+
if "antigravity" in eds and add_gemini_hooks:
|
|
1201
|
+
print(" Would generate: .agents/hooks.json + hooks runtime (profile >= standard)")
|
|
1202
|
+
if "cline" in eds and add_cline_hooks:
|
|
1203
|
+
print(
|
|
1204
|
+
" Would generate: .cline/hooks/<Event> + "
|
|
1205
|
+
".clinerules/hooks/<Event> (profile >= standard)"
|
|
1206
|
+
)
|
|
1082
1207
|
if add_native_surfaces:
|
|
1083
1208
|
if "cursor" in eds:
|
|
1084
1209
|
print(" Would generate: .cursor/hooks.json + .cursor/agents/ + .cursor/skills/ (profile=full)")
|
|
@@ -1090,7 +1215,14 @@ def _install_local_dry_run(reset: bool, editors: list[str] | None = None,
|
|
|
1090
1215
|
print(" Would generate: .augment/agents/ + .augment/commands/ + "
|
|
1091
1216
|
"$HOME/.augment/settings.json + .augment/skills/ (profile=full)")
|
|
1092
1217
|
if "gemini" in eds:
|
|
1093
|
-
print(
|
|
1218
|
+
print(
|
|
1219
|
+
" Would generate: .gemini/commands/ + .gemini/skills/ + "
|
|
1220
|
+
".gemini/agents/ (profile=full)"
|
|
1221
|
+
)
|
|
1222
|
+
if "antigravity" in eds:
|
|
1223
|
+
print(" Would generate: .agents/agents/ (profile=full)")
|
|
1224
|
+
if "opencode" in eds:
|
|
1225
|
+
print(" Would generate: .opencode/skills/ (profile=full)")
|
|
1094
1226
|
if "codex" in eds:
|
|
1095
1227
|
print(" Would generate: .codex/agents/ native agents")
|
|
1096
1228
|
print(" Would generate: .agents/skills/ Codex skills")
|
|
@@ -1290,6 +1422,7 @@ def _create_local_ai_tool_configs(cwd: Path, rules_dir: Path,
|
|
|
1290
1422
|
_profile = profile if profile in {"minimal", "standard", "strict", "full"} else "standard"
|
|
1291
1423
|
add_copilot_dir = _profile in {"standard", "strict", "full"}
|
|
1292
1424
|
add_gemini_hooks = _profile in {"standard", "strict", "full"}
|
|
1425
|
+
add_cline_hooks = _profile in {"standard", "strict", "full"}
|
|
1293
1426
|
add_native_surfaces = _profile == "full"
|
|
1294
1427
|
# Skip the editor skill-catalogue pointer when real skills are already
|
|
1295
1428
|
# discoverable at .claude/skills (project or ~/.claude/skills global):
|
|
@@ -1364,11 +1497,6 @@ def _create_local_ai_tool_configs(cwd: Path, rules_dir: Path,
|
|
|
1364
1497
|
_try_generator("generate_windsurf_skills", cwd)
|
|
1365
1498
|
|
|
1366
1499
|
if "cline" in eds:
|
|
1367
|
-
# Migrate: remove legacy .clinerules single file (replaced by directory)
|
|
1368
|
-
legacy_clinerules = cwd / ".clinerules"
|
|
1369
|
-
if legacy_clinerules.is_file():
|
|
1370
|
-
legacy_clinerules.unlink()
|
|
1371
|
-
print(" Migrated: .clinerules file → .clinerules/ directory")
|
|
1372
1500
|
from generate_cline_rules import generate as gen_cline_rules
|
|
1373
1501
|
gen_cline_rules(
|
|
1374
1502
|
cwd,
|
|
@@ -1376,8 +1504,18 @@ def _create_local_ai_tool_configs(cwd: Path, rules_dir: Path,
|
|
|
1376
1504
|
rules_dir=rules_dir,
|
|
1377
1505
|
managed_scopes=("standard", "lang", "custom"),
|
|
1378
1506
|
)
|
|
1507
|
+
if add_cline_hooks:
|
|
1508
|
+
_try_generator("generate_cline_hooks", cwd)
|
|
1509
|
+
else:
|
|
1510
|
+
from generate_cline_hooks import cleanup as cleanup_cline_hooks
|
|
1511
|
+
|
|
1512
|
+
cleanup_cline_hooks(cwd)
|
|
1379
1513
|
if add_native_surfaces:
|
|
1380
1514
|
_try_generator("generate_cline_skills", cwd, emit_skill_pointer=emit_pointer)
|
|
1515
|
+
else:
|
|
1516
|
+
from generate_cline_skills import cleanup as cleanup_cline_skills
|
|
1517
|
+
|
|
1518
|
+
cleanup_cline_skills(cwd)
|
|
1381
1519
|
|
|
1382
1520
|
if "roo" in eds:
|
|
1383
1521
|
roo_output = run_script("generate-roo-modes.sh", capture=True)
|
|
@@ -1409,6 +1547,18 @@ def _create_local_ai_tool_configs(cwd: Path, rules_dir: Path,
|
|
|
1409
1547
|
from generate_antigravity import generate as gen_antigravity
|
|
1410
1548
|
gen_antigravity(cwd, language_modules=language_modules,
|
|
1411
1549
|
rules_dir=rules_dir)
|
|
1550
|
+
if add_gemini_hooks:
|
|
1551
|
+
_try_generator("generate_antigravity_hooks", cwd)
|
|
1552
|
+
else:
|
|
1553
|
+
from generate_antigravity_hooks import cleanup as cleanup_hooks
|
|
1554
|
+
|
|
1555
|
+
cleanup_hooks(cwd)
|
|
1556
|
+
if add_native_surfaces:
|
|
1557
|
+
_try_generator("generate_antigravity_agents", cwd)
|
|
1558
|
+
else:
|
|
1559
|
+
from generate_antigravity_agents import cleanup as cleanup_agents
|
|
1560
|
+
|
|
1561
|
+
cleanup_agents(cwd)
|
|
1412
1562
|
|
|
1413
1563
|
if "codex" in eds:
|
|
1414
1564
|
# AGENTS.md — marker injection; universal coding rules are inlined into
|
|
@@ -1442,9 +1592,18 @@ def _create_local_ai_tool_configs(cwd: Path, rules_dir: Path,
|
|
|
1442
1592
|
)
|
|
1443
1593
|
if add_gemini_hooks:
|
|
1444
1594
|
_try_generator("generate_gemini_hooks", cwd)
|
|
1595
|
+
else:
|
|
1596
|
+
from generate_gemini_hooks import cleanup as cleanup_gemini_hooks
|
|
1597
|
+
|
|
1598
|
+
cleanup_gemini_hooks(cwd)
|
|
1445
1599
|
if add_native_surfaces:
|
|
1446
1600
|
_try_generator("generate_gemini_commands", cwd)
|
|
1447
1601
|
_try_generator("generate_gemini_skills", cwd)
|
|
1602
|
+
_try_generator("generate_gemini_agents", cwd)
|
|
1603
|
+
else:
|
|
1604
|
+
from generate_gemini_agents import cleanup as cleanup_gemini_agents
|
|
1605
|
+
|
|
1606
|
+
cleanup_gemini_agents(cwd)
|
|
1448
1607
|
|
|
1449
1608
|
if "opencode" in eds:
|
|
1450
1609
|
# AGENTS.md — shared with Codex via marker injection (opencode reads same file)
|
|
@@ -1470,6 +1629,22 @@ def _create_local_ai_tool_configs(cwd: Path, rules_dir: Path,
|
|
|
1470
1629
|
msg += f", {removed} stale removed"
|
|
1471
1630
|
msg += ")"
|
|
1472
1631
|
print(msg)
|
|
1632
|
+
if add_native_surfaces:
|
|
1633
|
+
from generate_opencode_skills import generate as gen_opencode_skills
|
|
1634
|
+
|
|
1635
|
+
written, removed, preserved = gen_opencode_skills(cwd)
|
|
1636
|
+
msg = f" Created: .opencode/skills/ ({written} skills"
|
|
1637
|
+
if removed:
|
|
1638
|
+
msg += f", {removed} stale removed"
|
|
1639
|
+
if preserved:
|
|
1640
|
+
msg += f", {preserved} user-owned preserved"
|
|
1641
|
+
print(msg + ")")
|
|
1642
|
+
else:
|
|
1643
|
+
from generate_opencode_skills import cleanup as cleanup_opencode_skills
|
|
1644
|
+
|
|
1645
|
+
removed, _ = cleanup_opencode_skills(cwd)
|
|
1646
|
+
if removed:
|
|
1647
|
+
print(f" Removed: .opencode/skills/ ({removed} managed skills)")
|
|
1473
1648
|
# .opencode/plugins/ai-toolkit-hooks.js — lifecycle hook bridge
|
|
1474
1649
|
from generate_opencode_plugin import generate as gen_opencode_plugin
|
|
1475
1650
|
gen_opencode_plugin(cwd)
|
package/scripts/mcp_editors.py
CHANGED
|
@@ -520,12 +520,13 @@ def _normalize_server(editor: str, server: dict) -> dict:
|
|
|
520
520
|
|
|
521
521
|
|
|
522
522
|
def _normalize_antigravity_server(server: dict) -> dict:
|
|
523
|
-
"""Validate Antigravity's
|
|
523
|
+
"""Validate and emit Antigravity's current ``serverUrl`` MCP schema."""
|
|
524
524
|
if not isinstance(server, dict):
|
|
525
525
|
raise ValueError("Antigravity MCP server configuration must be an object")
|
|
526
526
|
|
|
527
527
|
data = copy.deepcopy(server)
|
|
528
528
|
data.pop("_source", None)
|
|
529
|
+
data.pop("transport", None)
|
|
529
530
|
if "httpUrl" in data:
|
|
530
531
|
raise ValueError("Antigravity MCP does not support the legacy 'httpUrl' field")
|
|
531
532
|
|
|
@@ -538,6 +539,13 @@ def _normalize_antigravity_server(server: dict) -> dict:
|
|
|
538
539
|
transport_key = transport_keys[0]
|
|
539
540
|
_require_antigravity_string(data, transport_key)
|
|
540
541
|
|
|
542
|
+
# Current Antigravity documentation emits only serverUrl. Accept the old
|
|
543
|
+
# portable `url` input so existing .mcp.json templates keep working, but
|
|
544
|
+
# never persist that legacy spelling in native Antigravity configuration.
|
|
545
|
+
if transport_key == "url":
|
|
546
|
+
data["serverUrl"] = data.pop("url")
|
|
547
|
+
transport_key = "serverUrl"
|
|
548
|
+
|
|
541
549
|
if transport_key == "command":
|
|
542
550
|
_validate_antigravity_stdio(data)
|
|
543
551
|
else:
|
package/scripts/plugin.py
CHANGED
|
@@ -603,10 +603,30 @@ def _ensure_claude_settings() -> Path:
|
|
|
603
603
|
return settings_path
|
|
604
604
|
|
|
605
605
|
|
|
606
|
+
def _clear_dangling_link(path: Path, label: str) -> bool:
|
|
607
|
+
"""Drop a symlink whose target no longer exists, so install can relink it.
|
|
608
|
+
|
|
609
|
+
A toolkit upgrade replaces app/ wholesale, which leaves every ~/.claude link
|
|
610
|
+
into the old package pointing at nothing. The install guard treated such a
|
|
611
|
+
link as installed (`exists() or is_symlink()`), reported OK and repaired
|
|
612
|
+
nothing — the user was left with a dead link and a green log. Only dangling
|
|
613
|
+
links are removed: a live link or a real file is user content and stays.
|
|
614
|
+
"""
|
|
615
|
+
if path.is_symlink() and not path.exists():
|
|
616
|
+
try:
|
|
617
|
+
path.unlink()
|
|
618
|
+
except OSError:
|
|
619
|
+
return False
|
|
620
|
+
print(f" Dangling {label} link removed: {path.name}")
|
|
621
|
+
return True
|
|
622
|
+
return False
|
|
623
|
+
|
|
624
|
+
|
|
606
625
|
def _install_claude_skills(pack: dict, pack_dir: Path, installed_items: list[str]) -> None:
|
|
607
626
|
for skill in pack.get("includes", {}).get("skills", []):
|
|
608
627
|
skill_dir = CLAUDE_DIR / "skills" / skill
|
|
609
628
|
source_dir = _resolve_skill_source(pack_dir, skill)
|
|
629
|
+
_clear_dangling_link(skill_dir, "skill")
|
|
610
630
|
if skill_dir.exists() or skill_dir.is_symlink():
|
|
611
631
|
print(f" OK skill: {skill}")
|
|
612
632
|
elif source_dir:
|
|
@@ -622,6 +642,7 @@ def _install_claude_agents(pack: dict, pack_dir: Path, installed_items: list[str
|
|
|
622
642
|
for agent in pack.get("includes", {}).get("agents", []):
|
|
623
643
|
agent_file = CLAUDE_DIR / "agents" / f"{agent}.md"
|
|
624
644
|
source_file = _resolve_agent_source(pack_dir, agent)
|
|
645
|
+
_clear_dangling_link(agent_file, "agent")
|
|
625
646
|
if agent_file.exists() or agent_file.is_symlink():
|
|
626
647
|
print(f" OK agent: {agent}")
|
|
627
648
|
elif source_file is not None:
|
package/scripts/plugin_schema.py
CHANGED
|
@@ -40,13 +40,14 @@ VALID_TYPES = frozenset({
|
|
|
40
40
|
|
|
41
41
|
# Valid hook event names (must match validate.py VALID_HOOK_EVENTS)
|
|
42
42
|
VALID_HOOK_EVENTS = frozenset({
|
|
43
|
-
"SessionStart", "
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
43
|
+
"SessionStart", "SessionEnd", "UserPromptSubmit", "Notification",
|
|
44
|
+
"MessageDisplay", "PreToolUse", "PostToolUse", "PostToolUseFailure",
|
|
45
|
+
"PostToolBatch", "Stop", "StopFailure", "UserPromptExpansion",
|
|
46
|
+
"SubagentStart", "SubagentStop", "PreCompact", "PostCompact",
|
|
47
|
+
"PermissionRequest", "PermissionDenied", "Elicitation", "ElicitationResult",
|
|
48
|
+
"TaskCreated", "TaskCompleted", "TeammateIdle", "WorktreeCreate",
|
|
49
|
+
"WorktreeRemove", "CwdChanged", "FileChanged", "ConfigChange",
|
|
50
|
+
"DirectoryAdded", "Setup", "InstructionsLoaded",
|
|
50
51
|
})
|
|
51
52
|
|
|
52
53
|
|
package/scripts/secure_fs.py
CHANGED
|
@@ -438,6 +438,41 @@ class SecureTransaction:
|
|
|
438
438
|
def unlink(self, destination: SecureDestination) -> None:
|
|
439
439
|
self._unlink(destination, enforce_snapshot=True)
|
|
440
440
|
|
|
441
|
+
def rmdir_empty(
|
|
442
|
+
self,
|
|
443
|
+
anchor: SecureDestination,
|
|
444
|
+
relative: Path,
|
|
445
|
+
) -> bool:
|
|
446
|
+
"""Remove an empty directory below a pinned parent without following links.
|
|
447
|
+
|
|
448
|
+
This is a post-commit pruning helper. It deliberately returns ``False``
|
|
449
|
+
for missing, non-empty, or unsafe paths instead of widening a completed
|
|
450
|
+
file transaction's rollback surface.
|
|
451
|
+
"""
|
|
452
|
+
if relative.is_absolute() or not relative.parts or ".." in relative.parts:
|
|
453
|
+
raise RuntimeError(f"Invalid relative directory path: {relative}")
|
|
454
|
+
pinned = self._get(anchor)
|
|
455
|
+
if not pinned.materialized:
|
|
456
|
+
raise RuntimeError(
|
|
457
|
+
f"Anchor parent was not materialized: {pinned.destination.path}"
|
|
458
|
+
)
|
|
459
|
+
directory_fd = os.dup(pinned.parent_fd)
|
|
460
|
+
try:
|
|
461
|
+
for part in relative.parts[:-1]:
|
|
462
|
+
next_fd = os.open(part, _DIRECTORY_FLAGS, dir_fd=directory_fd)
|
|
463
|
+
os.close(directory_fd)
|
|
464
|
+
directory_fd = next_fd
|
|
465
|
+
os.rmdir(relative.parts[-1], dir_fd=directory_fd)
|
|
466
|
+
os.fsync(directory_fd)
|
|
467
|
+
return True
|
|
468
|
+
except OSError:
|
|
469
|
+
return False
|
|
470
|
+
finally:
|
|
471
|
+
try:
|
|
472
|
+
os.close(directory_fd)
|
|
473
|
+
except OSError:
|
|
474
|
+
pass
|
|
475
|
+
|
|
441
476
|
def _unlink(
|
|
442
477
|
self,
|
|
443
478
|
destination: SecureDestination,
|