@softspark/ai-toolkit 4.12.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 +30 -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/global-install-model.md +24 -3
- 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 +30 -13
- package/kb/reference/unique-features.md +3 -2
- package/llms-full.txt +179 -60
- 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 +33 -5
- package/scripts/generate_devin_hooks.py +3 -4
- package/scripts/generate_windsurf_skills.py +5 -6
- package/scripts/install.py +23 -21
- package/scripts/install_steps/ai_tools.py +96 -6
- package/scripts/install_steps/install_state.py +2 -0
- package/scripts/update_projects.py +12 -1
- package/scripts/validate.py +34 -2
- package/AGENTS.md +0 -655
- package/scripts/generate_windsurf_hooks.py +0 -152
package/scripts/install.py
CHANGED
|
@@ -756,34 +756,36 @@ def main() -> None:
|
|
|
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
|
|
@@ -110,7 +110,10 @@ def install_ai_tools(target_dir: Path, rules_dir: Path,
|
|
|
110
110
|
if "cline" in eds:
|
|
111
111
|
if dry_run:
|
|
112
112
|
print(" Would generate: ~/Documents/Cline/Rules/ai-toolkit-*.md")
|
|
113
|
-
|
|
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")
|
|
114
117
|
else:
|
|
115
118
|
_install_cline_global(target_dir, rules_dir)
|
|
116
119
|
installed.append("cline")
|
|
@@ -277,6 +280,80 @@ def _install_windsurf_global(target_dir: Path, rules_dir: Path) -> None:
|
|
|
277
280
|
)
|
|
278
281
|
|
|
279
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
|
+
|
|
280
357
|
def _install_cline_global(target_dir: Path, rules_dir: Path) -> None:
|
|
281
358
|
"""Install Cline global rules in the documented ~/.cline directory."""
|
|
282
359
|
from generate_cline_rules import generate as gen_cline_rules
|
|
@@ -941,6 +1018,18 @@ def _install_local_dry_run(reset: bool, editors: list[str] | None = None,
|
|
|
941
1018
|
if ed in eds:
|
|
942
1019
|
print(msg)
|
|
943
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
|
+
|
|
944
1033
|
# Profile-driven extras (matrix in kb/reference/global-install-model.md)
|
|
945
1034
|
if "copilot" in eds and add_copilot_dir:
|
|
946
1035
|
print(" Would generate: .github/instructions/ + .github/prompts/ (profile >= standard)")
|
|
@@ -950,7 +1039,7 @@ def _install_local_dry_run(reset: bool, editors: list[str] | None = None,
|
|
|
950
1039
|
if "cursor" in eds:
|
|
951
1040
|
print(" Would generate: .cursor/hooks.json + .cursor/agents/ + .cursor/skills/ (profile=full)")
|
|
952
1041
|
if "windsurf" in eds:
|
|
953
|
-
print(" Would generate: .devin/hooks.v1.json + .windsurf/
|
|
1042
|
+
print(" Would generate: .devin/hooks.v1.json + .windsurf/skills/ (profile=full)")
|
|
954
1043
|
if "cline" in eds:
|
|
955
1044
|
print(" Would generate: .cline/skills/ (profile=full)")
|
|
956
1045
|
if "augment" in eds:
|
|
@@ -1174,6 +1263,7 @@ def _create_local_ai_tool_configs(cwd: Path, rules_dir: Path,
|
|
|
1174
1263
|
_try_generator("generate_cursor_skills", cwd, emit_skill_pointer=emit_pointer)
|
|
1175
1264
|
|
|
1176
1265
|
if "windsurf" in eds:
|
|
1266
|
+
_cleanup_retired_windsurf_surfaces(cwd)
|
|
1177
1267
|
inject_with_rules(
|
|
1178
1268
|
"generate-windsurf.sh",
|
|
1179
1269
|
cwd / ".windsurfrules",
|
|
@@ -1183,11 +1273,11 @@ def _create_local_ai_tool_configs(cwd: Path, rules_dir: Path,
|
|
|
1183
1273
|
gen_windsurf_rules(cwd, language_modules=language_modules,
|
|
1184
1274
|
rules_dir=rules_dir)
|
|
1185
1275
|
if add_native_surfaces:
|
|
1186
|
-
# .windsurf/hooks.json
|
|
1187
|
-
# .devin/hooks.v1.json
|
|
1188
|
-
_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.
|
|
1189
1278
|
_try_generator("generate_devin_hooks", cwd)
|
|
1190
|
-
#
|
|
1279
|
+
# Current Devin docs list .windsurf/skills as a supported path;
|
|
1280
|
+
# .devin/skills is not documented.
|
|
1191
1281
|
_try_generator("generate_windsurf_skills", cwd)
|
|
1192
1282
|
|
|
1193
1283
|
if "cline" in eds:
|
|
@@ -197,6 +197,8 @@ def record_install(
|
|
|
197
197
|
state["profile"] = profile
|
|
198
198
|
if auto_detected is not None:
|
|
199
199
|
state["auto_detected_languages"] = sorted(auto_detected)
|
|
200
|
+
else:
|
|
201
|
+
state.pop("auto_detected_languages", None)
|
|
200
202
|
|
|
201
203
|
if extends_info is not None:
|
|
202
204
|
state["extends"] = {
|
|
@@ -28,8 +28,19 @@ def _update_project(project: dict[str, Any], install_script: str, extra_args: li
|
|
|
28
28
|
# Pass saved editors from registry so update re-installs the same editors
|
|
29
29
|
cmd_args = ["python3", install_script, "--local"] + extra_args
|
|
30
30
|
project_editors = project.get("editors", [])
|
|
31
|
-
|
|
31
|
+
has_editor_override = any(
|
|
32
|
+
arg == "--editors" or arg.startswith("--editors=")
|
|
33
|
+
for arg in extra_args
|
|
34
|
+
)
|
|
35
|
+
if project_editors and not has_editor_override:
|
|
32
36
|
cmd_args.extend(["--editors", ",".join(project_editors)])
|
|
37
|
+
has_profile_override = any(
|
|
38
|
+
arg == "--profile" or arg.startswith("--profile=")
|
|
39
|
+
for arg in extra_args
|
|
40
|
+
)
|
|
41
|
+
project_profile = project.get("profile", "")
|
|
42
|
+
if project_profile and not has_profile_override:
|
|
43
|
+
cmd_args.extend(["--profile", project_profile])
|
|
33
44
|
|
|
34
45
|
try:
|
|
35
46
|
proc = subprocess.run(
|
package/scripts/validate.py
CHANGED
|
@@ -124,6 +124,10 @@ LANGUAGE_RULE_CATEGORIES = frozenset({
|
|
|
124
124
|
|
|
125
125
|
PLANNED_ASSETS = [
|
|
126
126
|
"app/.claude-plugin/plugin.json",
|
|
127
|
+
"app/claude-app/hooks/hooks.json",
|
|
128
|
+
"app/claude-app/skills/ai-toolkit-rules/SKILL.md",
|
|
129
|
+
"app/claude-app/global-instructions.md",
|
|
130
|
+
"scripts/claude_app.py",
|
|
127
131
|
"scripts/doctor.py",
|
|
128
132
|
"scripts/benchmark_ecosystem.py",
|
|
129
133
|
"scripts/harvest_ecosystem.py",
|
|
@@ -561,9 +565,37 @@ def validate_planned_assets(tk_dir: Path, vr: ValidationResult) -> None:
|
|
|
561
565
|
assert d["name"]
|
|
562
566
|
assert d["version"]
|
|
563
567
|
assert d["description"]
|
|
564
|
-
|
|
568
|
+
assert isinstance(d.get("repository"), str)
|
|
569
|
+
ignored = {"npm", "install", "marketplace", "claude-code"} & set(d)
|
|
570
|
+
assert not ignored
|
|
571
|
+
for path_field in ("hooks", "skills"):
|
|
572
|
+
relative = str(d[path_field]).removeprefix("./")
|
|
573
|
+
assert (tk_dir / "app" / relative).exists()
|
|
574
|
+
print(" OK: plugin manifest matches the official schema surface")
|
|
565
575
|
except (json.JSONDecodeError, KeyError, AssertionError):
|
|
566
|
-
vr.error("Invalid plugin manifest JSON or
|
|
576
|
+
vr.error("Invalid plugin manifest JSON, official fields, or component paths")
|
|
577
|
+
|
|
578
|
+
try:
|
|
579
|
+
from claude_app import (
|
|
580
|
+
GLOBAL_INSTRUCTIONS_PATH,
|
|
581
|
+
PLUGIN_HOOKS_PATH,
|
|
582
|
+
PLUGIN_RULES_SKILL_PATH,
|
|
583
|
+
render_global_instructions,
|
|
584
|
+
render_plugin_hooks,
|
|
585
|
+
render_rules_skill,
|
|
586
|
+
)
|
|
587
|
+
generated = (
|
|
588
|
+
(PLUGIN_HOOKS_PATH, render_plugin_hooks()),
|
|
589
|
+
(PLUGIN_RULES_SKILL_PATH, render_rules_skill()),
|
|
590
|
+
(GLOBAL_INSTRUCTIONS_PATH, render_global_instructions()),
|
|
591
|
+
)
|
|
592
|
+
stale = [path for path, expected in generated if path.read_text(encoding="utf-8") != expected]
|
|
593
|
+
if stale:
|
|
594
|
+
vr.error("Stale Claude app assets; run: python3 scripts/claude_app.py sync")
|
|
595
|
+
else:
|
|
596
|
+
print(" OK: Claude app plugin assets match canonical rules/hooks")
|
|
597
|
+
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
598
|
+
vr.error(f"Invalid Claude app generated assets: {exc}")
|
|
567
599
|
|
|
568
600
|
# Validate benchmark dashboard JSON
|
|
569
601
|
benchmark_dashboard = tk_dir / "benchmarks" / "ecosystem-dashboard.json"
|