@softspark/ai-toolkit 4.24.0 → 4.24.1

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,33 @@ Versioning follows [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ---
9
9
 
10
+ ## v4.24.1 — a dead link reported itself as installed (2026-08-19)
11
+
12
+ ### Fixed
13
+
14
+ - **`plugin install` treated a dangling symlink as an installed asset.** The guard
15
+ was `if path.exists() or path.is_symlink()`, and a link whose target is gone
16
+ satisfies the second half — so the installer printed `OK agent` / `OK skill`,
17
+ changed nothing, and left the user with a dead link and a green log. This is the
18
+ exact state a toolkit upgrade produces: `npm install -g @softspark/ai-toolkit`
19
+ replaces `app/` wholesale, so every `~/.claude` link into the old package points
20
+ at nothing, and re-running install could not repair it.
21
+
22
+ A dangling link is now dropped before the guard runs and relinked to the
23
+ resolved source, printing `Dangling agent link removed: <name>`. **Live links and
24
+ real files are untouched** — those are user content, and the merge-friendly
25
+ install model keeps them.
26
+
27
+ Surfaced while migrating a pack from `app/plugins/` to the v4.24.0 user root:
28
+ the upgrade broke both links and `plugin install` insisted everything was fine.
29
+
30
+ ### Added
31
+
32
+ - **Two tests.** One builds the post-upgrade state (links pointing into a path
33
+ that no longer exists) and asserts install removes and relinks both; the other
34
+ pins the negative — a real file on the agent name and a live skill link survive
35
+ a reinstall untouched.
36
+
10
37
  ## v4.24.0 — external packs stop dying on upgrade (2026-08-18)
11
38
 
12
39
  ### Added
package/README.md CHANGED
@@ -6,18 +6,17 @@
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-1522%20passing-success)](tests/)
10
-
11
- ## What's New in v4.24.0
12
-
13
- **v4.24.0** — a plugin pack maintained in its own repository had nowhere safe to
14
- live: the only directory the toolkit scanned was `app/plugins/`, inside the npm
15
- package, so every `npm install -g` upgrade deleted it. Packs now also load from
16
- `~/.softspark/ai-toolkit/plugins/`, which the user owns and upgrades never touch.
17
- The entry can be a symlink to an npm-installed package, so updating an external
18
- pack is one `npm install -g` with nothing to re-link. Core packs still win a name
19
- collision. Ships with v4.23.1's fix for pack-shipped agents, which the same
20
- external-pack workflow depends on.
9
+ [![Tests](https://img.shields.io/badge/tests-1524%20passing-success)](tests/)
10
+
11
+ ## What's New in v4.24.1
12
+
13
+ **v4.24.1** — `plugin install` accepted a dead symlink as proof of installation.
14
+ The guard asked `exists() or is_symlink()`, and a link with no target still
15
+ answers yes to the second half, so the installer logged `OK` and repaired
16
+ nothing precisely the state a toolkit upgrade leaves, since replacing `app/`
17
+ breaks every link into it. Dangling links are now dropped and relinked; real
18
+ files and live links stay untouched. Pairs with v4.24.0's user-level pack root,
19
+ which is what stops the breakage happening in the first place.
21
20
 
22
21
  See [CHANGELOG.md](CHANGELOG.md) for full history.
23
22
 
@@ -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.24.0",
6
+ "version": "4.24.1",
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.24.0",
2
+ "version": "4.24.1",
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.24.0",
3
+ "version": "4.24.1",
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/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: