@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
package/scripts/uninstall.py
CHANGED
|
@@ -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,
|
|
1312
|
-
"leftover v4.16.x recovery data while preserving user-owned
|
|
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
|
|
1333
|
-
|
|
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
|
-
|
|
1336
|
-
|
|
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}
|
|
1474
|
+
print(f" WARN could not clean {label} {surface}: {error}")
|
|
1350
1475
|
else:
|
|
1351
|
-
print(f" Cleaned: {label}
|
|
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(
|
|
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
|
|
1443
|
-
#
|
|
1444
|
-
#
|
|
1445
|
-
|
|
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
|
package/scripts/validate.py
CHANGED
|
@@ -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":
|
|
79
|
-
"
|
|
80
|
-
|
|
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": ("
|
|
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():
|