@softspark/ai-toolkit 4.28.0 → 4.29.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 CHANGED
@@ -7,6 +7,44 @@ Versioning follows [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ---
9
9
 
10
+ ## v4.29.0 — Stale plugin uploads stop hiding (2026-08-21)
11
+
12
+ ### Added
13
+
14
+ - **`doctor` reports Claude app plugin version drift.** Check 11 now compares the
15
+ `version` recorded for every `ai-toolkit@…` entry in
16
+ `~/.claude/plugins/installed_plugins.json` against the installed toolkit's
17
+ `package.json` and warns when they differ. The upload is a point-in-time ZIP:
18
+ once the toolkit moves on, Chat and Cowork keep running the skills, agents,
19
+ hooks, and rules from whenever the export was made, with no signal anywhere
20
+ that they are behind. `--fix` deliberately cannot clear this warning — the
21
+ export can be regenerated but the upload itself happens by hand in the app's
22
+ Customize > Plugins panel, so the check names the two steps instead of
23
+ pretending to do them.
24
+
25
+ ### Changed
26
+
27
+ - **Version drift is checked before the enabled/disabled branch.** A plugin
28
+ disabled for Claude Code still feeds Chat and Cowork, which have no other
29
+ channel for toolkit content. The old flow returned `OK: registered but
30
+ disabled` and never looked at versions, so the state a user reaches by running
31
+ `doctor --fix` was also the state where a stale upload passed silently. That
32
+ was the wrong place to stop looking.
33
+ - **`ai-toolkit install` re-asserts the plugin as disabled for Claude Code.**
34
+ Uploading the ZIP re-enables the plugin every time, which put the double-load
35
+ fix back on whoever remembered to run `doctor --fix` afterwards. Global
36
+ installs and updates now flip enabled toolkit plugins to `false` in
37
+ `~/.claude/settings.json` and print each key they touched. `--local` and
38
+ `--dry-run` are untouched, and non-toolkit plugins are never modified. The
39
+ disable logic moved into `disable_toolkit_plugins_for_claude_code()` in
40
+ `scripts/doctor.py`, shared by both call sites.
41
+ - Test count: 1659 → 1666.
42
+ - Skill body budget threshold unchanged. The largest body (`medplum-rules`, 16760
43
+ bytes) sits 1240 bytes under the 18000-byte warn line, short of the 2000-byte
44
+ margin the ratchet requires, so the threshold stays where it is this release.
45
+
46
+ ---
47
+
10
48
  ## v4.28.0 — MCP servers reach Chat and Cowork (2026-08-21)
11
49
 
12
50
  ### Added
package/README.md CHANGED
@@ -6,22 +6,24 @@
6
6
  [![License: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE)
7
7
  [![Skills](https://img.shields.io/badge/skills-109-brightgreen)](app/skills/)
8
8
  [![Agents](https://img.shields.io/badge/agents-44-blue)](app/agents/)
9
- [![Tests](https://img.shields.io/badge/tests-1659%20passing-success)](tests/)
10
-
11
- ## What's New in v4.28.0
12
-
13
- **v4.28.0** gives Claude Chat and Cowork their MCP servers:
14
-
15
- - The Claude app parses `claude_desktop_config.json` on startup and reports bad
16
- entries in its own warning dialog, but ai-toolkit never wrote to it. A user
17
- with seven servers configured for Claude Code still had an empty `mcpServers`
18
- block in Cowork.
19
- - `ai-toolkit mcp install --editor claude-app --scope global <template>` now
20
- writes it, on macOS, Windows, and Linux, honoring `CLAUDE_USER_DATA_DIR` the
21
- way the app does. `--editor claude` remains Claude Code -- different runtime,
22
- different file.
23
- - The app accepts stdio entries only, so HTTP/SSE templates are bridged as
24
- `npx -y mcp-remote <url>` rather than written verbatim and silently skipped.
9
+ [![Tests](https://img.shields.io/badge/tests-1666%20passing-success)](tests/)
10
+
11
+ ## What's New in v4.29.0
12
+
13
+ **v4.29.0** stops a stale Claude app upload from passing as healthy:
14
+
15
+ - The plugin ZIP is a point-in-time snapshot. Once the toolkit moves on, Chat and
16
+ Cowork keep running the skills, agents, hooks, and rules from whenever you
17
+ exported it, with nothing anywhere saying so.
18
+ - `ai-toolkit doctor` now compares the version recorded in
19
+ `~/.claude/plugins/installed_plugins.json` against the installed toolkit and
20
+ warns when they differ -- including when the plugin is disabled for Claude
21
+ Code, since Chat and Cowork still load it.
22
+ - `--fix` cannot clear that warning. It can regenerate the export; the upload
23
+ itself is manual, so the check names both steps instead of pretending.
24
+ - `ai-toolkit install` now re-asserts the plugin as disabled for Claude Code.
25
+ Uploading the ZIP re-enables it every time, which had left the double-load fix
26
+ to whoever remembered to re-run `doctor --fix`.
25
27
 
26
28
  ## Table of Contents
27
29
 
@@ -3,7 +3,7 @@
3
3
  "name": "ai-toolkit",
4
4
  "displayName": "AI Toolkit",
5
5
  "description": "Professional-grade engineering skills, agents, rules, and lifecycle guardrails for Claude Code, Claude Chat, and Cowork.",
6
- "version": "4.28.0",
6
+ "version": "4.29.0",
7
7
  "author": {
8
8
  "name": "SoftSpark",
9
9
  "url": "https://github.com/softspark"
package/manifest.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "4.28.0",
2
+ "version": "4.29.0",
3
3
  "components": {
4
4
  "agents": {
5
5
  "description": "44 specialized agents (orchestrator, backend, frontend, security, devops, etc.)",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softspark/ai-toolkit",
3
- "version": "4.28.0",
3
+ "version": "4.29.0",
4
4
  "description": "AI coding toolkit: 109 skills, 44 agents, 12 developer-tool integrations, recoverable native tool-output filtering, Claude Chat/Cowork export, safety constitution, SARIF audit, and signed npm provenance.",
5
5
  "keywords": [
6
6
  "claude",
package/scripts/doctor.py CHANGED
@@ -683,6 +683,68 @@ def _registered_toolkit_plugins(registry: dict) -> list[str]:
683
683
  return [key for key in plugins if str(key).split("@", 1)[0] == "ai-toolkit"]
684
684
 
685
685
 
686
+ def _registered_plugin_versions(registry: dict, keys: list[str]) -> dict[str, str]:
687
+ """Map plugin key to the version recorded in the Claude Code registry."""
688
+ plugins = registry.get("plugins", {})
689
+ versions: dict[str, str] = {}
690
+ for key in keys:
691
+ entries = plugins.get(key)
692
+ if not isinstance(entries, list):
693
+ continue
694
+ for entry in entries:
695
+ if isinstance(entry, dict) and isinstance(entry.get("version"), str):
696
+ versions[key] = entry["version"]
697
+ break
698
+ return versions
699
+
700
+
701
+ def _toolkit_package_version() -> str | None:
702
+ """Version of the installed toolkit package, or None when unreadable."""
703
+ try:
704
+ data = json.loads((toolkit_dir / "package.json").read_text(encoding="utf-8"))
705
+ except (OSError, json.JSONDecodeError):
706
+ return None
707
+ version = data.get("version")
708
+ return version if isinstance(version, str) else None
709
+
710
+
711
+ def disable_toolkit_plugins_for_claude_code() -> list[str]:
712
+ """Disable every enabled ai-toolkit plugin in ``~/.claude/settings.json``.
713
+
714
+ Returns the keys that were flipped. Uploading the Claude app plugin re-enables
715
+ it for Claude Code every time, so this is re-asserted rather than fixed once.
716
+ Raises OSError when settings.json cannot be written.
717
+ """
718
+ if not PLUGIN_REGISTRY.is_file():
719
+ return []
720
+ try:
721
+ registry = json.loads(PLUGIN_REGISTRY.read_text(encoding="utf-8"))
722
+ except (OSError, json.JSONDecodeError):
723
+ return []
724
+
725
+ keys = _registered_toolkit_plugins(registry)
726
+ if not keys:
727
+ return []
728
+
729
+ settings_json = CLAUDE_DIR / "settings.json"
730
+ try:
731
+ settings = json.loads(settings_json.read_text(encoding="utf-8"))
732
+ except (OSError, json.JSONDecodeError):
733
+ return []
734
+ enabled = settings.get("enabledPlugins", {})
735
+ if not isinstance(enabled, dict):
736
+ enabled = {}
737
+
738
+ active = [key for key in keys if enabled.get(key, True)]
739
+ if not active:
740
+ return []
741
+
742
+ enabled.update({key: False for key in active})
743
+ settings["enabledPlugins"] = enabled
744
+ settings_json.write_text(json.dumps(settings, indent=2) + "\n", encoding="utf-8")
745
+ return active
746
+
747
+
686
748
  def check_plugin_double_load(dr: DiagResult, fix_mode: bool) -> None:
687
749
  """Warn when the Claude app plugin and the global install both feed Claude Code.
688
750
 
@@ -709,6 +771,22 @@ def check_plugin_double_load(dr: DiagResult, fix_mode: bool) -> None:
709
771
  dr.ok("no ai-toolkit plugin registered in Claude Code")
710
772
  return
711
773
 
774
+ # Version drift is independent of whether the plugin is enabled here: a
775
+ # plugin disabled for Claude Code still feeds Chat and Cowork, and those
776
+ # runtimes have no other channel for skills, agents, hooks, or rules. Report
777
+ # it before the enabled/disabled branch, which returns early on the healthy
778
+ # path and would otherwise let a stale upload pass unnoticed.
779
+ toolkit_version = _toolkit_package_version()
780
+ if toolkit_version:
781
+ for key, plugin_version in _registered_plugin_versions(registry, keys).items():
782
+ if plugin_version != toolkit_version:
783
+ dr.warn(
784
+ f"{key} is v{plugin_version} but the toolkit is v{toolkit_version}: "
785
+ "Chat and Cowork keep running the older skills, agents, hooks, and "
786
+ "rules until you re-run `ai-toolkit claude-app export` and upload "
787
+ "the ZIP again (--fix cannot do this; the upload is manual)"
788
+ )
789
+
712
790
  settings_json = CLAUDE_DIR / "settings.json"
713
791
  try:
714
792
  settings = json.loads(settings_json.read_text(encoding="utf-8"))
@@ -738,14 +816,12 @@ def check_plugin_double_load(dr: DiagResult, fix_mode: bool) -> None:
738
816
  if not fix_mode:
739
817
  return
740
818
 
741
- enabled.update({key: False for key in active})
742
- settings["enabledPlugins"] = enabled
743
819
  try:
744
- settings_json.write_text(json.dumps(settings, indent=2) + "\n", encoding="utf-8")
820
+ disabled = disable_toolkit_plugins_for_claude_code()
745
821
  except OSError as exc:
746
822
  dr.fail(f"could not disable plugin in settings.json: {exc}")
747
823
  return
748
- for key in active:
824
+ for key in disabled:
749
825
  dr.fixed(f"disabled {key} for Claude Code (global install stays authoritative)")
750
826
 
751
827
 
@@ -831,6 +831,18 @@ def main() -> None:
831
831
  if is_new:
832
832
  print(f" Registered project in {TOOLKIT_DATA_DIR / 'projects.json'}")
833
833
 
834
+ # A global install is authoritative for Claude Code, so an uploaded Claude
835
+ # app plugin must not feed it in parallel. Uploading the ZIP re-enables the
836
+ # plugin every time, so this is re-asserted on every install/update rather
837
+ # than left to whoever remembers to run `doctor --fix` afterwards.
838
+ if not local and not dry_run:
839
+ from doctor import disable_toolkit_plugins_for_claude_code
840
+ try:
841
+ for key in disable_toolkit_plugins_for_claude_code():
842
+ print(f" Disabled Claude app plugin for Claude Code: {key}")
843
+ except OSError as error:
844
+ print(f" WARNING: could not disable Claude app plugin: {error}")
845
+
834
846
  print_summary(local=local)
835
847
 
836
848