@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
@@ -1249,6 +1249,9 @@ def _transaction_specs(
1249
1249
  claude: Path,
1250
1250
  codex: list[CodexSurface],
1251
1251
  copilot: list[CopilotSurface],
1252
+ *,
1253
+ target: Path,
1254
+ scope: str,
1252
1255
  ) -> list[tuple[Path, bool, Path]]:
1253
1256
  specs: dict[Path, tuple[bool, Path]] = {}
1254
1257
 
@@ -1299,6 +1302,11 @@ def _transaction_specs(
1299
1302
  (root / "hooks", True),
1300
1303
  ):
1301
1304
  add(path, recursive, trusted_root)
1305
+ for config_root in _opencode_config_roots(target, scope):
1306
+ root = config_root if config_root is not None else target / ".opencode"
1307
+ add(root / "skills", True, target)
1308
+ for root in _cline_transaction_roots(target, scope):
1309
+ add(root, True, target)
1302
1310
  return [
1303
1311
  (path, recursive, trusted_root)
1304
1312
  for path, (recursive, trusted_root) in specs.items()
@@ -1308,8 +1316,9 @@ def _transaction_specs(
1308
1316
  def _parse_args(argv: list[str]) -> argparse.Namespace:
1309
1317
  parser = argparse.ArgumentParser(
1310
1318
  description=(
1311
- "Remove only ai-toolkit-managed Claude, Codex, Copilot, and "
1312
- "leftover v4.16.x recovery data while preserving user-owned content."
1319
+ "Remove only ai-toolkit-managed Claude, Codex, Copilot, Cline, OpenCode, "
1320
+ "and leftover v4.16.x recovery data while preserving user-owned "
1321
+ "content."
1313
1322
  ),
1314
1323
  epilog=(
1315
1324
  "Global Codex and Copilot locations honor CODEX_HOME and "
@@ -1329,15 +1338,131 @@ def _parse_args(argv: list[str]) -> argparse.Namespace:
1329
1338
  return args
1330
1339
 
1331
1340
 
1332
- def _remove_editor_hook_configs(target: Path) -> None:
1333
- """Strip toolkit hook entries from editor configs that have no other cleanup.
1341
+ def _opencode_config_roots(target: Path, scope: str) -> list[Path | None]:
1342
+ roots: list[Path | None] = []
1343
+ if scope in {"local", "both"}:
1344
+ roots.append(None)
1345
+ if scope in {"global", "both"}:
1346
+ roots.append(target / ".config" / "opencode")
1347
+ return roots
1348
+
1349
+
1350
+ def _cline_transaction_roots(target: Path, scope: str) -> list[Path]:
1351
+ roots = {target / ".cline" / "skills"}
1352
+ if scope in {"local", "both"}:
1353
+ roots.update(
1354
+ {
1355
+ target / ".cline" / "rules",
1356
+ target / ".cline" / "hooks",
1357
+ target / ".clinerules",
1358
+ }
1359
+ )
1360
+ if scope in {"global", "both"}:
1361
+ roots.update(
1362
+ {
1363
+ target / ".cline" / "rules",
1364
+ target / ".cline" / "hooks",
1365
+ target / "Documents" / "Cline" / "Rules",
1366
+ target / "Documents" / "Cline" / "Hooks",
1367
+ }
1368
+ )
1369
+ return sorted(roots)
1370
+
1371
+
1372
+ def _discover_opencode_skills(
1373
+ target: Path,
1374
+ scope: str,
1375
+ ) -> list[tuple[str, str]]:
1376
+ from generate_opencode_skills import discover
1377
+
1378
+ found: list[tuple[str, str]] = []
1379
+ for config_root in _opencode_config_roots(target, scope):
1380
+ count = discover(target, config_root=config_root)
1381
+ if count:
1382
+ location = config_root or target / ".opencode"
1383
+ found.append(
1384
+ (f"Managed: {location / 'skills'} ({count} OpenCode skills)",
1385
+ "opencode-skills")
1386
+ )
1387
+ return found
1388
+
1389
+
1390
+ def _discover_cline_surfaces(target: Path, scope: str) -> list[tuple[str, str]]:
1391
+ from generate_cline_hooks import discover as discover_cline_hooks
1392
+ from generate_cline_rules import managed_files as managed_cline_rules
1393
+ from generate_cline_skills import discover as discover_cline_skills
1394
+
1395
+ count = 0
1396
+ if scope in {"local", "both"}:
1397
+ count += len(managed_cline_rules(target))
1398
+ count += discover_cline_hooks(target)
1399
+ if scope in {"global", "both"}:
1400
+ global_rules = (
1401
+ target / ".cline" / "rules",
1402
+ target / "Documents" / "Cline" / "Rules",
1403
+ )
1404
+ count += len(
1405
+ managed_cline_rules(
1406
+ target,
1407
+ output_roots=global_rules,
1408
+ include_workflows=False,
1409
+ )
1410
+ )
1411
+ if scope == "global":
1412
+ count += discover_cline_hooks(
1413
+ target,
1414
+ hooks_root=target / ".cline" / "hooks",
1415
+ )
1416
+ count += discover_cline_hooks(
1417
+ target,
1418
+ hooks_root=target / "Documents" / "Cline" / "Hooks",
1419
+ )
1420
+ count += discover_cline_skills(target)
1421
+ if not count:
1422
+ return []
1423
+ return [(f"Managed: Cline native surfaces ({count} artifacts)", "cline")]
1424
+
1425
+
1426
+ def _cleanup_cline_surfaces(target: Path, scope: str) -> None:
1427
+ from generate_cline_hooks import cleanup as cleanup_cline_hooks
1428
+ from generate_cline_rules import cleanup as cleanup_cline_rules
1429
+ from generate_cline_skills import cleanup as cleanup_cline_skills
1334
1430
 
1335
- Best effort by design: a missing generator or an unreadable config must not
1336
- abort an uninstall that has already removed the primary surfaces.
1431
+ if scope in {"local", "both"}:
1432
+ cleanup_cline_rules(target)
1433
+ cleanup_cline_hooks(target)
1434
+ if scope in {"global", "both"}:
1435
+ global_rule_roots = [target / "Documents" / "Cline" / "Rules"]
1436
+ if scope == "global":
1437
+ global_rule_roots.insert(0, target / ".cline" / "rules")
1438
+ cleanup_cline_rules(
1439
+ target,
1440
+ output_roots=tuple(global_rule_roots),
1441
+ include_workflows=False,
1442
+ )
1443
+ if scope == "global":
1444
+ cleanup_cline_hooks(
1445
+ target,
1446
+ hooks_root=target / ".cline" / "hooks",
1447
+ )
1448
+ cleanup_cline_hooks(
1449
+ target,
1450
+ hooks_root=target / "Documents" / "Cline" / "Hooks",
1451
+ )
1452
+ cleanup_cline_skills(target)
1453
+
1454
+
1455
+ def _remove_editor_managed_surfaces(target: Path, scope: str) -> None:
1456
+ """Strip managed editor surfaces that have no primary uninstall path.
1457
+
1458
+ Cursor, Gemini, and OpenCode compatibility cleanup remains best effort.
1459
+ Cline cleanup is transactional and fail-closed so the outer uninstall can
1460
+ roll every managed Cline root back after a partial failure.
1337
1461
  """
1338
- for module_name, label in (
1339
- ("generate_cursor_hooks", "Cursor"),
1340
- ("generate_gemini_hooks", "Gemini"),
1462
+ for module_name, label, surface in (
1463
+ ("generate_cursor_hooks", "Cursor", "hook entries"),
1464
+ ("generate_gemini_hooks", "Gemini", "hook entries"),
1465
+ ("generate_gemini_agents", "Gemini", "agents"),
1341
1466
  ):
1342
1467
  try:
1343
1468
  module = importlib.import_module(module_name)
@@ -1346,9 +1471,22 @@ def _remove_editor_hook_configs(target: Path) -> None:
1346
1471
  continue
1347
1472
  cleanup(target)
1348
1473
  except (ImportError, OSError, RuntimeError, ValueError) as error:
1349
- print(f" WARN could not clean {label} hooks: {error}")
1474
+ print(f" WARN could not clean {label} {surface}: {error}")
1350
1475
  else:
1351
- print(f" Cleaned: {label} hook entries")
1476
+ print(f" Cleaned: {label} {surface}")
1477
+
1478
+ from generate_opencode_skills import cleanup as cleanup_opencode_skills
1479
+
1480
+ for config_root in _opencode_config_roots(target, scope):
1481
+ try:
1482
+ cleanup_opencode_skills(target, config_root=config_root)
1483
+ except (OSError, RuntimeError, ValueError) as error:
1484
+ print(f" WARN could not clean OpenCode skills: {error}")
1485
+ else:
1486
+ print(" Cleaned: OpenCode skills")
1487
+
1488
+ _cleanup_cline_surfaces(target, scope)
1489
+ print(" Cleaned: Cline native surfaces")
1352
1490
 
1353
1491
 
1354
1492
  def main(argv: list[str] | None = None) -> None:
@@ -1380,6 +1518,8 @@ def main(argv: list[str] | None = None) -> None:
1380
1518
  components.extend(_discover_codex(surface))
1381
1519
  for surface in copilot:
1382
1520
  components.extend(_discover_copilot(surface))
1521
+ components.extend(_discover_opencode_skills(target, scope))
1522
+ components.extend(_discover_cline_surfaces(target, scope))
1383
1523
  recovery_root = (
1384
1524
  target / ".softspark" / "ai-toolkit" / "sessions"
1385
1525
  if scope in {"global", "both"}
@@ -1429,7 +1569,13 @@ def main(argv: list[str] | None = None) -> None:
1429
1569
  transaction: _UninstallTransaction | None = None
1430
1570
  try:
1431
1571
  transaction = _UninstallTransaction(
1432
- _transaction_specs(claude, codex, copilot)
1572
+ _transaction_specs(
1573
+ claude,
1574
+ codex,
1575
+ copilot,
1576
+ target=target,
1577
+ scope=scope,
1578
+ )
1433
1579
  )
1434
1580
  _preflight(target, claude, codex, copilot)
1435
1581
  remove_components(claude, target)
@@ -1439,12 +1585,10 @@ def main(argv: list[str] | None = None) -> None:
1439
1585
  for surface in copilot:
1440
1586
  _preflight(target, claude, [], [surface])
1441
1587
  _remove_copilot(surface)
1442
- # Cursor and Gemini hook configs were never cleaned: uninstall knew
1443
- # only Codex and Copilot, so `.cursor/hooks.json` and the hooks block in
1444
- # `.gemini/settings.json` survived an uninstall. Both cleanups strip
1445
- # only entries tagged `ai-toolkit`, leaving user-authored and
1446
- # plugin-pack entries and any unrelated settings in place.
1447
- _remove_editor_hook_configs(target)
1588
+ # Cursor and Gemini configs have no primary uninstall branch. These
1589
+ # cleanups strip only managed hook entries and Gemini agent files,
1590
+ # leaving user-authored, plugin-owned, and unrelated settings intact.
1591
+ _remove_editor_managed_surfaces(target, scope)
1448
1592
  if recovery_root is not None and recovery_components:
1449
1593
  # The recovery API preflights its complete tree before the first
1450
1594
  # unlink. A later I/O fault can still leave recovery partially
@@ -21,6 +21,7 @@ from __future__ import annotations
21
21
  import json
22
22
  import re
23
23
  import sys
24
+ import tempfile
24
25
  from pathlib import Path
25
26
 
26
27
  sys.path.insert(0, str(Path(__file__).resolve().parent))
@@ -74,25 +75,35 @@ VALID_HOOK_TYPES = frozenset({
74
75
  "mcp_tool",
75
76
  })
76
77
 
78
+ PROMPT_AGENT_HOOK_EVENTS = frozenset({
79
+ "PermissionDenied",
80
+ "PermissionRequest",
81
+ "PostToolBatch",
82
+ "PostToolUse",
83
+ "PostToolUseFailure",
84
+ "PreToolUse",
85
+ "Stop",
86
+ "SubagentStop",
87
+ "TaskCompleted",
88
+ "TaskCreated",
89
+ "TeammateIdle",
90
+ "UserPromptExpansion",
91
+ "UserPromptSubmit",
92
+ })
93
+
94
+ HTTP_HOOK_EVENTS = VALID_HOOK_EVENTS - {"SessionStart", "Setup"}
95
+
77
96
  HOOK_TYPE_EVENTS = {
78
- "agent": frozenset({"Stop", "SubagentStop"}),
79
- "prompt": frozenset({
80
- "PreToolUse",
81
- "PostToolUse",
82
- "PostToolUseFailure",
83
- "PostToolBatch",
84
- "UserPromptSubmit",
85
- "UserPromptExpansion",
86
- "Stop",
87
- "SubagentStop",
88
- }),
97
+ "agent": PROMPT_AGENT_HOOK_EVENTS,
98
+ "http": HTTP_HOOK_EVENTS,
99
+ "prompt": PROMPT_AGENT_HOOK_EVENTS,
89
100
  }
90
101
 
91
102
  HOOK_REQUIRED_FIELDS = {
92
103
  "command": ("command",),
93
104
  "http": ("url",),
94
105
  "prompt": ("prompt",),
95
- "agent": ("agent",),
106
+ "agent": ("prompt",),
96
107
  "mcp_tool": ("server", "tool", "arguments"),
97
108
  }
98
109
 
@@ -736,6 +747,25 @@ def validate_planned_assets(tk_dir: Path, vr: ValidationResult) -> None:
736
747
  except (OSError, ValueError, json.JSONDecodeError) as exc:
737
748
  vr.error(f"Invalid Claude app generated assets: {exc}")
738
749
 
750
+ # Validate a clean native Codex plugin stage without touching user state.
751
+ if tk_dir.resolve() == default_toolkit_dir.resolve():
752
+ try:
753
+ from codex_plugin import PLUGIN_NAME, stage_plugin, validate_staged_plugin
754
+
755
+ with tempfile.TemporaryDirectory(
756
+ prefix="ai-toolkit-codex-plugin-validation-"
757
+ ) as temporary:
758
+ staged = Path(temporary) / PLUGIN_NAME
759
+ stage_plugin(staged)
760
+ errors = validate_staged_plugin(staged)
761
+ if errors:
762
+ for error in errors:
763
+ vr.error(f"Invalid native Codex plugin: {error}")
764
+ else:
765
+ print(" OK: native Codex plugin stage is schema-valid and self-contained")
766
+ except (OSError, ValueError, json.JSONDecodeError) as exc:
767
+ vr.error(f"Invalid native Codex plugin generated assets: {exc}")
768
+
739
769
  # Validate benchmark dashboard JSON
740
770
  benchmark_dashboard = tk_dir / "benchmarks" / "ecosystem-dashboard.json"
741
771
  if benchmark_dashboard.is_file():