@softspark/ai-toolkit 4.2.1 → 4.2.2
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 +16 -0
- package/README.md +7 -8
- package/app/.claude-plugin/plugin.json +1 -1
- package/manifest.json +1 -1
- package/package.json +1 -1
- package/scripts/inject_hook_cli.py +39 -3
- package/scripts/merge-hooks.py +23 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,22 @@ Versioning follows [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## v4.2.2 - Legacy hook duplicate cleanup (2026-05-12)
|
|
11
|
+
|
|
12
|
+
Patch release. Cleans up legacy untagged hook duplicates found during post-release verification of v4.2.1.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- **Legacy notification hook cleanup** - `scripts/merge-hooks.py` now removes old direct `osascript` notification hooks from pre-script installs while preserving user-owned notification hooks.
|
|
17
|
+
- **External hook duplicate cleanup** - `scripts/inject_hook_cli.py` now removes legacy untagged entries matching the same event, matcher, and handler payload as the external hook being re-injected.
|
|
18
|
+
|
|
19
|
+
### Verification
|
|
20
|
+
|
|
21
|
+
- `bats tests/test_merge_hooks_statusline.bats tests/test_inject_hook.bats`: 42/42 passing.
|
|
22
|
+
- Local `~/.claude/settings.json`: 0 behavior duplicates, 0 exact duplicates, 0 untagged hook groups after cleanup.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
10
26
|
## v4.2.1 - Claude Code hook enforcement and README cleanup (2026-05-12)
|
|
11
27
|
|
|
12
28
|
Patch release. Tightens Claude Code runtime enforcement, fixes duplicated hook cleanup during install/update, and removes the README hero image for release.
|
package/README.md
CHANGED
|
@@ -6,16 +6,15 @@
|
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
[](app/skills/)
|
|
8
8
|
[](app/agents/)
|
|
9
|
-
[](tests/)
|
|
10
10
|
|
|
11
|
-
## What's New in v4.2.
|
|
11
|
+
## What's New in v4.2.2
|
|
12
12
|
|
|
13
|
-
Patch release focused on
|
|
13
|
+
Patch release focused on cleaning legacy hook duplicates found during post-release verification.
|
|
14
14
|
|
|
15
|
-
- **
|
|
16
|
-
- **
|
|
17
|
-
- **
|
|
18
|
-
- **README cleanup**: removed the hero image block for a lighter release README.
|
|
15
|
+
- **Legacy notification cleanup**: `merge-hooks.py` now removes old direct `osascript` notification hooks from pre-script installs.
|
|
16
|
+
- **External hook cleanup**: `inject-hook` now removes legacy untagged duplicates that match the re-injected external hook payload.
|
|
17
|
+
- **Regression coverage**: added tests for both cleanup paths so repeated updates keep hook settings idempotent.
|
|
19
18
|
|
|
20
19
|
See [CHANGELOG.md](CHANGELOG.md) for full history.
|
|
21
20
|
|
|
@@ -148,7 +147,7 @@ ai-toolkit/
|
|
|
148
147
|
│ └── ARCHITECTURE.md # Full system design
|
|
149
148
|
├── kb/ # Reference docs, procedures, plans
|
|
150
149
|
├── scripts/ # Validation, install, evaluation scripts
|
|
151
|
-
├── tests/ # Bats test suite (
|
|
150
|
+
├── tests/ # Bats test suite (1051 tests)
|
|
152
151
|
└── CHANGELOG.md
|
|
153
152
|
```
|
|
154
153
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-toolkit",
|
|
3
3
|
"description": "Professional-grade Claude Code toolkit with persona presets, skill security auditor, expanded lifecycle hooks, experimental opt-in plugin packs, benchmark harvesting, and multi-tool support.",
|
|
4
|
-
"version": "4.2.
|
|
4
|
+
"version": "4.2.2",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "SoftSpark",
|
|
7
7
|
"url": "https://github.com/softspark"
|
package/manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softspark/ai-toolkit",
|
|
3
|
-
"version": "4.2.
|
|
3
|
+
"version": "4.2.2",
|
|
4
4
|
"description": "AI coding toolkit: 107 skills, 44 agents, 12-editor write-through (Claude, Cursor, Windsurf, Copilot, Gemini, Cline, Roo, Aider, Augment, Antigravity, Codex, opencode), machine-enforced safety constitution, SARIF audit, signed npm provenance.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|
|
@@ -120,20 +120,56 @@ def _entry_source(entry: dict) -> str | None:
|
|
|
120
120
|
return None
|
|
121
121
|
|
|
122
122
|
|
|
123
|
-
def
|
|
123
|
+
def _entry_signature(entry: dict) -> tuple:
|
|
124
|
+
"""Return behavior-defining hook fields without source tags."""
|
|
125
|
+
handlers = []
|
|
126
|
+
for hook in entry.get("hooks", []):
|
|
127
|
+
if not isinstance(hook, dict):
|
|
128
|
+
handlers.append(hook)
|
|
129
|
+
continue
|
|
130
|
+
handlers.append(tuple(sorted(
|
|
131
|
+
(key, value)
|
|
132
|
+
for key, value in hook.items()
|
|
133
|
+
if key != "_source"
|
|
134
|
+
)))
|
|
135
|
+
return (entry.get("matcher", ""), tuple(handlers))
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def strip_source(hooks: dict, source: str, replacement_hooks: dict | None = None) -> dict:
|
|
124
139
|
"""Remove all entries whose ``_source`` matches *source*.
|
|
125
140
|
|
|
126
141
|
Args:
|
|
127
142
|
hooks: Existing hooks dict (event-name -> list of entries).
|
|
128
143
|
source: Source tag to strip.
|
|
144
|
+
replacement_hooks: Optional hook entries being re-injected. When
|
|
145
|
+
provided, untagged legacy entries with the same event/matcher/
|
|
146
|
+
handler payload are stripped as belonging to the same source.
|
|
129
147
|
|
|
130
148
|
Returns:
|
|
131
149
|
New hooks dict with matching entries removed. Empty event lists
|
|
132
150
|
are omitted.
|
|
133
151
|
"""
|
|
152
|
+
legacy_signatures: dict[str, set[tuple]] = {}
|
|
153
|
+
if replacement_hooks:
|
|
154
|
+
for event, entries in replacement_hooks.items():
|
|
155
|
+
legacy_signatures[event] = {
|
|
156
|
+
_entry_signature(entry)
|
|
157
|
+
for entry in entries
|
|
158
|
+
if isinstance(entry, dict)
|
|
159
|
+
}
|
|
160
|
+
|
|
134
161
|
result: dict = {}
|
|
135
162
|
for event, entries in hooks.items():
|
|
136
|
-
|
|
163
|
+
signatures = legacy_signatures.get(event, set())
|
|
164
|
+
filtered = [
|
|
165
|
+
e for e in entries
|
|
166
|
+
if _entry_source(e) != source
|
|
167
|
+
and not (
|
|
168
|
+
isinstance(e, dict)
|
|
169
|
+
and _entry_source(e) is None
|
|
170
|
+
and _entry_signature(e) in signatures
|
|
171
|
+
)
|
|
172
|
+
]
|
|
137
173
|
if filtered:
|
|
138
174
|
result[event] = filtered
|
|
139
175
|
return result
|
|
@@ -174,7 +210,7 @@ def merge_hooks(new_hooks: dict, existing_hooks: dict, source: str) -> dict:
|
|
|
174
210
|
Returns:
|
|
175
211
|
Merged hooks dict.
|
|
176
212
|
"""
|
|
177
|
-
merged = strip_source(existing_hooks, source)
|
|
213
|
+
merged = strip_source(existing_hooks, source, new_hooks)
|
|
178
214
|
for event, entries in new_hooks.items():
|
|
179
215
|
if event not in merged:
|
|
180
216
|
merged[event] = []
|
package/scripts/merge-hooks.py
CHANGED
|
@@ -27,6 +27,23 @@ import os
|
|
|
27
27
|
import sys
|
|
28
28
|
|
|
29
29
|
SOURCE_TAG = "ai-toolkit"
|
|
30
|
+
LEGACY_TOOLKIT_HOOKS = {
|
|
31
|
+
"Notification": [
|
|
32
|
+
{
|
|
33
|
+
"matcher": "",
|
|
34
|
+
"hooks": [
|
|
35
|
+
{
|
|
36
|
+
"type": "command",
|
|
37
|
+
"command": (
|
|
38
|
+
"osascript -e 'display notification "
|
|
39
|
+
'"Claude Code needs your attention" with title "Claude Code"'
|
|
40
|
+
"'"
|
|
41
|
+
),
|
|
42
|
+
}
|
|
43
|
+
],
|
|
44
|
+
}
|
|
45
|
+
],
|
|
46
|
+
}
|
|
30
47
|
|
|
31
48
|
|
|
32
49
|
def load_json(path: str) -> dict:
|
|
@@ -95,6 +112,12 @@ def strip_toolkit(hooks: dict, toolkit_hooks: dict | None = None) -> dict:
|
|
|
95
112
|
for entry in entries
|
|
96
113
|
if isinstance(entry, dict)
|
|
97
114
|
}
|
|
115
|
+
for event, entries in LEGACY_TOOLKIT_HOOKS.items():
|
|
116
|
+
legacy_signatures.setdefault(event, set()).update(
|
|
117
|
+
_entry_signature(entry)
|
|
118
|
+
for entry in entries
|
|
119
|
+
if isinstance(entry, dict)
|
|
120
|
+
)
|
|
98
121
|
|
|
99
122
|
result = {}
|
|
100
123
|
for event, entries in hooks.items():
|