@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.
Files changed (42) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.md +35 -15
  3. package/app/.claude-plugin/plugin.json +1 -1
  4. package/app/skills/hook-creator/SKILL.md +18 -4
  5. package/app/surface.json +4 -0
  6. package/benchmarks/ecosystem-doctor-snapshot.json +67 -19
  7. package/bin/ai-toolkit.js +27 -3
  8. package/kb/reference/architecture-overview.md +13 -5
  9. package/kb/reference/claude-ecosystem-expansion-foundations.md +30 -5
  10. package/kb/reference/cli-reference.md +27 -2
  11. package/kb/reference/codex-cli-compatibility.md +101 -11
  12. package/kb/reference/global-install-model.md +10 -8
  13. package/kb/reference/hooks-catalog.md +41 -5
  14. package/kb/reference/mcp-editor-compatibility.md +3 -3
  15. package/kb/reference/mcp-templates.md +3 -3
  16. package/kb/reference/opencode-compatibility.md +53 -5
  17. package/kb/reference/supported-tools-registry.md +31 -26
  18. package/llms-full.txt +312 -73
  19. package/manifest.json +1 -1
  20. package/package.json +6 -2
  21. package/scripts/antigravity_plugin.py +570 -0
  22. package/scripts/codex_plugin.py +764 -0
  23. package/scripts/ecosystem_tools.json +92 -17
  24. package/scripts/generate_antigravity.py +16 -14
  25. package/scripts/generate_antigravity_agents.py +255 -0
  26. package/scripts/generate_antigravity_hooks.py +344 -0
  27. package/scripts/generate_cline_hooks.py +391 -0
  28. package/scripts/generate_cline_rules.py +210 -43
  29. package/scripts/generate_cline_skills.py +65 -2
  30. package/scripts/generate_codex_hooks.py +70 -8
  31. package/scripts/generate_gemini_agents.py +197 -0
  32. package/scripts/generate_gemini_hooks.py +24 -4
  33. package/scripts/generate_opencode_skills.py +544 -0
  34. package/scripts/inject_hook_cli.py +4 -26
  35. package/scripts/install.py +11 -10
  36. package/scripts/install_steps/ai_tools.py +209 -34
  37. package/scripts/mcp_editors.py +9 -1
  38. package/scripts/plugin.py +21 -0
  39. package/scripts/plugin_schema.py +8 -7
  40. package/scripts/secure_fs.py +35 -0
  41. package/scripts/uninstall.py +162 -18
  42. 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(" Would generate: ~/.gemini/commands/, ~/.gemini/skills/")
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 (~/.gemini/commands/),
95
- # and the skills pointer (~/.gemini/skills/) are documented user-tier
96
- # surfaces install them globally, gated like the local install.
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(" Would generate: ~/Documents/Cline/Rules/ai-toolkit-*.md")
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(target_dir, rules_dir)
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 RULES stay project-local, but the skill pointer has a
215
- # documented global surface: ~/.gemini/config/skills (all Antigravity
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(" Would generate: ~/.gemini/config/skills/, ~/.gemini/antigravity-cli/skills/ (skill pointer)")
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); their global installs cover hooks/skills only. "
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(target_dir: Path, rules_dir: Path) -> None:
422
- """Install Cline global rules in the documented ~/.cline directory."""
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
- # Cline reads GLOBAL rules from ~/Documents/Cline/Rules/ (docs.cline.bot);
426
- # ~/.cline/rules/ is not a Cline-read path.
427
- rules_root = target_dir / "Documents" / "Cline" / "Rules"
428
- gen_cline_rules(
429
- target_dir,
430
- rules_dir=rules_dir,
431
- output_root=rules_root,
432
- emit_workflows=False,
433
- managed_scopes=("standard", "custom"),
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
- print(" Created: ~/Documents/Cline/Rules/ai-toolkit-*.md")
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(target_dir: Path, rules_dir: Path) -> None:
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 CLI pointer skill also lives in .agents/skills/;
696
- # only real (materialized) skills indicate a Codex install.
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(" Would generate: .gemini/commands/ + .gemini/skills/ (profile=full)")
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)
@@ -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 native MCP schema without rewriting transports."""
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:
@@ -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", "Notification", "PreToolUse", "PostToolUse", "Stop",
44
- "PreCompact", "SubagentStop", "UserPromptSubmit", "TaskCompleted",
45
- "TeammateIdle", "SubagentStart", "SessionEnd", "PermissionRequest", "Setup",
46
- "InstructionsLoaded", "ConfigChange", "PostToolUseFailure", "PostToolBatch",
47
- "UserPromptExpansion", "PostCompact", "StopFailure", "CwdChanged",
48
- "FileChanged", "PermissionDenied", "Elicitation", "ElicitationResult",
49
- "WorktreeCreate", "WorktreeRemove", "TaskCreated",
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
 
@@ -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,