@softspark/ai-toolkit 4.16.1 → 4.18.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 +84 -0
- package/README.md +13 -19
- package/app/.claude-plugin/plugin.json +1 -1
- package/app/hooks/session-end.sh +1 -13
- package/app/hooks.json +0 -10
- package/app/plugins/README.md +16 -4
- package/app/plugins/rtk-pack/README.md +123 -0
- package/app/plugins/rtk-pack/hooks/rewrite.sh +79 -0
- package/app/plugins/rtk-pack/plugin.json +60 -0
- package/app/plugins/rtk-pack/scripts/init.py +252 -0
- package/app/plugins/rtk-pack/scripts/status.py +105 -0
- package/benchmarks/ecosystem-doctor-snapshot.json +14 -15
- package/bin/ai-toolkit.js +15 -2
- package/kb/history/completed/output-filter-retirement-20260726.md +128 -0
- package/kb/history/completed/rtk-pack-integration-20260726.md +710 -0
- package/kb/procedures/maintenance-sop.md +1 -1
- package/kb/procedures/release-preparation-sop.md +8 -3
- package/kb/procedures/rtk-upstream-sync-sop.md +279 -0
- package/kb/reference/architecture-overview.md +2 -3
- package/kb/reference/cli-reference.md +4 -14
- package/kb/reference/enterprise-config-guide.md +1 -21
- package/kb/reference/hooks-catalog.md +3 -60
- package/kb/reference/plugin-pack-conventions.md +17 -2
- package/kb/reference/supported-tools-registry.md +0 -4
- package/llms-full.txt +1171 -402
- package/llms.txt +3 -1
- package/manifest.json +147 -36
- package/package.json +1 -2
- package/scripts/audit_skills.py +21 -0
- package/scripts/claude_app.py +2 -21
- package/scripts/config_cli.py +4 -0
- package/scripts/config_merger.py +0 -17
- package/scripts/config_validator.py +11 -138
- package/scripts/doctor.py +3 -20
- package/scripts/install.py +2 -1
- package/scripts/install_steps/ai_tools.py +28 -99
- package/scripts/install_steps/hooks.py +26 -24
- package/scripts/merge-hooks.py +33 -2
- package/scripts/output_filter_retirement.py +395 -0
- package/scripts/plugin.py +136 -16
- package/scripts/schemas/ai-toolkit-config.schema.json +0 -60
- package/scripts/uninstall.py +13 -27
- package/scripts/verify_rtk_binary.py +335 -0
- package/app/hooks/filter-tool-output.sh +0 -76
- package/app/output-filter-policy.json +0 -15
- package/benchmarks/output-filter/README.md +0 -11
- package/benchmarks/output-filter/scenarios.json +0 -25
- package/kb/reference/tool-output-filter.md +0 -288
- package/scripts/benchmark_output_filter.py +0 -343
- package/scripts/output_filter_cli.py +0 -347
- package/scripts/output_filter_hook.py +0 -23
- package/scripts/tool_output_filter/__init__.py +0 -33
- package/scripts/tool_output_filter/contracts.py +0 -173
- package/scripts/tool_output_filter/engine.py +0 -260
- package/scripts/tool_output_filter/hook_runtime.py +0 -369
- package/scripts/tool_output_filter/input.py +0 -56
- package/scripts/tool_output_filter/invariants.py +0 -40
- package/scripts/tool_output_filter/policy.py +0 -153
- package/scripts/tool_output_filter/profiles/__init__.py +0 -68
- package/scripts/tool_output_filter/profiles/repeat_lines.py +0 -71
- package/scripts/tool_output_filter/profiles/tap_success.py +0 -154
- package/scripts/tool_output_filter/recovery.py +0 -846
- package/scripts/tool_output_filter/telemetry.py +0 -13
package/scripts/doctor.py
CHANGED
|
@@ -75,7 +75,6 @@ EXPECTED_HOOKS = [
|
|
|
75
75
|
"subagent-start.sh",
|
|
76
76
|
"subagent-stop.sh",
|
|
77
77
|
"session-end.sh",
|
|
78
|
-
"filter-tool-output.sh",
|
|
79
78
|
]
|
|
80
79
|
|
|
81
80
|
PLANNED_ASSETS = [
|
|
@@ -442,25 +441,9 @@ def check_generated_artifacts(dr: DiagResult, fix_mode: bool) -> None:
|
|
|
442
441
|
else:
|
|
443
442
|
dr.warn(f"{artifact} missing (run: ai-toolkit generate-all)")
|
|
444
443
|
|
|
445
|
-
#
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
agents_dir = toolkit_dir / "app" / "agents"
|
|
449
|
-
actual_agents = sum(1 for f in agents_dir.glob("*.md") if f.is_file()) if agents_dir.is_dir() else 0
|
|
450
|
-
content = agents_md.read_text(encoding="utf-8")
|
|
451
|
-
mentioned_agents = len(re.findall(r"^### `", content, re.MULTILINE))
|
|
452
|
-
if actual_agents != mentioned_agents:
|
|
453
|
-
if fix_mode:
|
|
454
|
-
result = subprocess.run(
|
|
455
|
-
["python3", str(toolkit_dir / "scripts" / "generate_agents_md.py")],
|
|
456
|
-
capture_output=True,
|
|
457
|
-
text=True,
|
|
458
|
-
)
|
|
459
|
-
if result.returncode == 0:
|
|
460
|
-
agents_md.write_text(result.stdout, encoding="utf-8")
|
|
461
|
-
dr.fixed(f"regenerated stale AGENTS.md ({mentioned_agents} -> {actual_agents} entries)")
|
|
462
|
-
else:
|
|
463
|
-
dr.warn(f"AGENTS.md may be stale: {mentioned_agents} entries vs {actual_agents} agent files")
|
|
444
|
+
# AGENTS.md no longer embeds the agent catalog (agents are discovered from
|
|
445
|
+
# their native directories), so there is no per-entry staleness to check —
|
|
446
|
+
# presence is covered by the generated-artifacts loop above.
|
|
464
447
|
|
|
465
448
|
print()
|
|
466
449
|
|
package/scripts/install.py
CHANGED
|
@@ -51,7 +51,7 @@ from emission import agent_count as count_agents, skill_count as count_skills
|
|
|
51
51
|
|
|
52
52
|
# Step modules
|
|
53
53
|
from install_steps.symlinks import install_agents, install_skills, clean_legacy_commands
|
|
54
|
-
from install_steps.hooks import install_hooks
|
|
54
|
+
from install_steps.hooks import cleanup_retired_output_filter, install_hooks
|
|
55
55
|
from install_steps.markers import install_marker_files, inject_rules, refresh_url_hooks, refresh_url_mcp
|
|
56
56
|
from install_steps.ai_tools import install_ai_tools, install_local_project, run_script
|
|
57
57
|
from install_steps.install_state import (
|
|
@@ -435,6 +435,7 @@ def install_claude_code(target_dir: Path, hooks_scripts_dir: Path,
|
|
|
435
435
|
install_agents(claude_dir, only, skip, dry_run)
|
|
436
436
|
install_skills(claude_dir, only, skip, dry_run)
|
|
437
437
|
clean_legacy_commands(claude_dir, dry_run)
|
|
438
|
+
cleanup_retired_output_filter(hooks_scripts_dir, dry_run)
|
|
438
439
|
install_hooks(claude_dir, hooks_scripts_dir, only, skip, dry_run)
|
|
439
440
|
install_marker_files(claude_dir, only, skip, dry_run)
|
|
440
441
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"""Install global and project-local AI tool configs."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
-
import json
|
|
5
4
|
import os
|
|
6
5
|
import shutil
|
|
7
6
|
import subprocess
|
|
@@ -15,7 +14,10 @@ from codex_skill_adapter import (
|
|
|
15
14
|
unmanaged_codex_skill_names,
|
|
16
15
|
)
|
|
17
16
|
from mcp_editors import sync_project_mcp_to_editors
|
|
18
|
-
from
|
|
17
|
+
from output_filter_retirement import (
|
|
18
|
+
PROJECT_POLICY_NAME,
|
|
19
|
+
managed_project_policy,
|
|
20
|
+
)
|
|
19
21
|
from injection import (
|
|
20
22
|
collapse_blank_runs as _collapse_blank_runs,
|
|
21
23
|
strip_all_sections as _strip_all_sections,
|
|
@@ -23,11 +25,6 @@ from injection import (
|
|
|
23
25
|
)
|
|
24
26
|
|
|
25
27
|
|
|
26
|
-
OUTPUT_FILTER_POLICY_NAME = "ai-toolkit-output-filter.json"
|
|
27
|
-
OUTPUT_FILTER_OWNER_NAME = ".ai-toolkit-output-filter.owner"
|
|
28
|
-
OUTPUT_FILTER_OWNER_MARKER = b"ai-toolkit-output-filter-policy-v1\n"
|
|
29
|
-
|
|
30
|
-
|
|
31
28
|
def install_ai_tools(target_dir: Path, rules_dir: Path,
|
|
32
29
|
dry_run: bool,
|
|
33
30
|
editors: list[str] | None = None,
|
|
@@ -399,6 +396,24 @@ def _cleanup_retired_windsurf_surfaces(cwd: Path) -> None:
|
|
|
399
396
|
print(" Migrated: removed undocumented .devin/skills toolkit pointer")
|
|
400
397
|
|
|
401
398
|
|
|
399
|
+
def _cleanup_retired_output_filter_policy(cwd: Path) -> None:
|
|
400
|
+
"""Remove the per-project policy the v4.16.x tool-output filter wrote.
|
|
401
|
+
|
|
402
|
+
Only the owner-marked pair is reclaimed; a policy file the user wrote by
|
|
403
|
+
hand has no marker and is left untouched.
|
|
404
|
+
"""
|
|
405
|
+
managed = managed_project_policy(cwd / ".claude")
|
|
406
|
+
if not managed:
|
|
407
|
+
return
|
|
408
|
+
for path in managed:
|
|
409
|
+
try:
|
|
410
|
+
path.unlink()
|
|
411
|
+
except OSError as error:
|
|
412
|
+
print(f" Warning: kept .claude/{path.name} ({error})")
|
|
413
|
+
return
|
|
414
|
+
print(f" Migrated: removed retired .claude/{PROJECT_POLICY_NAME}")
|
|
415
|
+
|
|
416
|
+
|
|
402
417
|
def _install_cline_global(target_dir: Path, rules_dir: Path) -> None:
|
|
403
418
|
"""Install Cline global rules in the documented ~/.cline directory."""
|
|
404
419
|
from generate_cline_rules import generate as gen_cline_rules
|
|
@@ -757,10 +772,11 @@ def install_local_project(rules_dir: Path, dry_run: bool, reset: bool,
|
|
|
757
772
|
print(f" Would inject language rules: {', '.join(language_modules)}")
|
|
758
773
|
if merged_config:
|
|
759
774
|
print(" Would apply merged config from extends")
|
|
760
|
-
if
|
|
761
|
-
print(
|
|
762
|
-
|
|
763
|
-
|
|
775
|
+
if managed_project_policy(cwd / ".claude"):
|
|
776
|
+
print(
|
|
777
|
+
" Would migrate: remove retired .claude/"
|
|
778
|
+
f"{PROJECT_POLICY_NAME}"
|
|
779
|
+
)
|
|
764
780
|
return
|
|
765
781
|
|
|
766
782
|
(cwd / ".claude").mkdir(parents=True, exist_ok=True)
|
|
@@ -770,12 +786,7 @@ def install_local_project(rules_dir: Path, dry_run: bool, reset: bool,
|
|
|
770
786
|
|
|
771
787
|
_create_local_claude_md(cwd, reset)
|
|
772
788
|
_create_local_settings(cwd, reset)
|
|
773
|
-
|
|
774
|
-
merged_config.get("toolOutputFilter")
|
|
775
|
-
if merged_config is not None
|
|
776
|
-
else None
|
|
777
|
-
)
|
|
778
|
-
_sync_local_output_filter_policy(cwd, configured_policy)
|
|
789
|
+
_cleanup_retired_output_filter_policy(cwd)
|
|
779
790
|
|
|
780
791
|
legacy_local_hooks = cwd / ".claude" / "hooks.json"
|
|
781
792
|
if legacy_local_hooks.is_file():
|
|
@@ -893,88 +904,6 @@ def _apply_extends_config(cwd: Path, merged: dict) -> None:
|
|
|
893
904
|
print(" Saved: .softspark-toolkit-extends.json (resolution metadata)")
|
|
894
905
|
|
|
895
906
|
|
|
896
|
-
def _sync_local_output_filter_policy(
|
|
897
|
-
cwd: Path,
|
|
898
|
-
configured: dict | None,
|
|
899
|
-
) -> None:
|
|
900
|
-
"""Atomically synchronize the toolkit-owned per-project output policy."""
|
|
901
|
-
policy_path = cwd / ".claude" / OUTPUT_FILTER_POLICY_NAME
|
|
902
|
-
owner_path = cwd / ".claude" / OUTPUT_FILTER_OWNER_NAME
|
|
903
|
-
policy_destination = SecureDestination(
|
|
904
|
-
policy_path, cwd, "project output-filter policy",
|
|
905
|
-
)
|
|
906
|
-
owner_destination = SecureDestination(
|
|
907
|
-
owner_path, cwd, "project output-filter owner marker",
|
|
908
|
-
)
|
|
909
|
-
destinations = [policy_destination, owner_destination]
|
|
910
|
-
|
|
911
|
-
try:
|
|
912
|
-
policy_content = (
|
|
913
|
-
_materialize_output_filter_policy(configured)
|
|
914
|
-
if configured is not None
|
|
915
|
-
else None
|
|
916
|
-
)
|
|
917
|
-
except (
|
|
918
|
-
AttributeError,
|
|
919
|
-
json.JSONDecodeError,
|
|
920
|
-
OSError,
|
|
921
|
-
RuntimeError,
|
|
922
|
-
TypeError,
|
|
923
|
-
ValueError,
|
|
924
|
-
) as error:
|
|
925
|
-
print(f" Warning: project output-filter policy not changed: {error}")
|
|
926
|
-
return
|
|
927
|
-
|
|
928
|
-
def mutate(transaction) -> None:
|
|
929
|
-
owner = transaction.initial_content(owner_destination)
|
|
930
|
-
existing_policy = transaction.initial_content(policy_destination)
|
|
931
|
-
if owner is None and existing_policy is not None:
|
|
932
|
-
print(" Kept: user-owned .claude/ai-toolkit-output-filter.json")
|
|
933
|
-
return
|
|
934
|
-
if owner not in (None, OUTPUT_FILTER_OWNER_MARKER):
|
|
935
|
-
print(" Kept: untrusted .claude output-filter ownership marker")
|
|
936
|
-
return
|
|
937
|
-
|
|
938
|
-
if policy_content is None:
|
|
939
|
-
if owner == OUTPUT_FILTER_OWNER_MARKER:
|
|
940
|
-
transaction.unlink(policy_destination)
|
|
941
|
-
transaction.unlink(owner_destination)
|
|
942
|
-
print(" Removed: managed project output-filter policy")
|
|
943
|
-
return
|
|
944
|
-
|
|
945
|
-
transaction.atomic_write(policy_destination, policy_content, 0o600)
|
|
946
|
-
if owner is None:
|
|
947
|
-
transaction.atomic_write(
|
|
948
|
-
owner_destination,
|
|
949
|
-
OUTPUT_FILTER_OWNER_MARKER,
|
|
950
|
-
0o600,
|
|
951
|
-
)
|
|
952
|
-
print(" Wrote: .claude/ai-toolkit-output-filter.json")
|
|
953
|
-
|
|
954
|
-
try:
|
|
955
|
-
run_secure_transaction(destinations, mutate)
|
|
956
|
-
except RuntimeError as error:
|
|
957
|
-
print(f" Warning: project output-filter policy not changed: {error}")
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
def _materialize_output_filter_policy(configured: dict) -> bytes:
|
|
961
|
-
"""Merge a partial project policy over canonical safe defaults."""
|
|
962
|
-
default_path = app_dir / "output-filter-policy.json"
|
|
963
|
-
with open(default_path, encoding="utf-8") as handle:
|
|
964
|
-
defaults = json.load(handle)
|
|
965
|
-
policy = dict(defaults)
|
|
966
|
-
policy.update(configured)
|
|
967
|
-
recovery = dict(defaults.get("recovery", {}))
|
|
968
|
-
recovery.update(configured.get("recovery", {}))
|
|
969
|
-
policy["recovery"] = recovery
|
|
970
|
-
|
|
971
|
-
from config_validator import validate_project_config
|
|
972
|
-
errors = validate_project_config({"toolOutputFilter": policy})
|
|
973
|
-
if errors:
|
|
974
|
-
raise RuntimeError("invalid output-filter policy: " + "; ".join(errors))
|
|
975
|
-
return (json.dumps(policy, indent=2, sort_keys=True) + "\n").encode()
|
|
976
|
-
|
|
977
|
-
|
|
978
907
|
def _inject_language_rules(cwd: Path, language_modules: list[str] | None) -> None:
|
|
979
908
|
"""Install Claude language-rule entrypoints for a project.
|
|
980
909
|
|
|
@@ -7,6 +7,32 @@ import subprocess
|
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
|
|
9
9
|
from _common import app_dir, should_install, toolkit_dir
|
|
10
|
+
from output_filter_retirement import (
|
|
11
|
+
cleanup_global_artifacts,
|
|
12
|
+
find_global_artifacts,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def cleanup_retired_output_filter(hooks_scripts_dir: Path, dry_run: bool) -> None:
|
|
17
|
+
"""Reclaim the v4.16.x tool-output filter files left on disk by an upgrade.
|
|
18
|
+
|
|
19
|
+
Runs outside ``should_install`` because the feature is gone: an install
|
|
20
|
+
that skips the hooks component still must not leave an orphaned hook
|
|
21
|
+
script, a global policy, or private recovery data behind. Silent when the
|
|
22
|
+
machine never ran v4.16.x.
|
|
23
|
+
"""
|
|
24
|
+
toolkit_data_dir = hooks_scripts_dir.parent
|
|
25
|
+
|
|
26
|
+
if dry_run:
|
|
27
|
+
for label in find_global_artifacts(toolkit_data_dir):
|
|
28
|
+
print(f" Would migrate: remove retired output-filter {label}")
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
removed, warnings = cleanup_global_artifacts(toolkit_data_dir)
|
|
32
|
+
for label in removed:
|
|
33
|
+
print(f" Migrated: removed retired output-filter {label}")
|
|
34
|
+
for warning in warnings:
|
|
35
|
+
print(f" Warning: output-filter retirement {warning}")
|
|
10
36
|
|
|
11
37
|
|
|
12
38
|
def install_hooks(claude_dir: Path, hooks_scripts_dir: Path,
|
|
@@ -61,13 +87,6 @@ def _copy_hook_scripts(claude_dir: Path, hooks_scripts_dir: Path) -> None:
|
|
|
61
87
|
for runtime_file in sorted(hooks_src.glob("*.json")):
|
|
62
88
|
shutil.copy2(runtime_file, hooks_scripts_dir / runtime_file.name)
|
|
63
89
|
copied += 1
|
|
64
|
-
output_filter_policy = app_dir / "output-filter-policy.json"
|
|
65
|
-
policy_destination = hooks_scripts_dir / output_filter_policy.name
|
|
66
|
-
# The global policy is user configuration: seed it once, never overwrite,
|
|
67
|
-
# so `ai-toolkit update` cannot silently reset an enabled mode to off.
|
|
68
|
-
if output_filter_policy.is_file() and not policy_destination.exists():
|
|
69
|
-
shutil.copy2(output_filter_policy, policy_destination)
|
|
70
|
-
copied += 1
|
|
71
90
|
print(f" Copied: {copied} hook scripts to ~/.softspark/ai-toolkit/hooks/")
|
|
72
91
|
legacy_hooks = claude_dir / "hooks"
|
|
73
92
|
if legacy_hooks.is_symlink():
|
|
@@ -78,8 +97,6 @@ def _copy_hook_scripts(claude_dir: Path, hooks_scripts_dir: Path) -> None:
|
|
|
78
97
|
# Python helpers that hooks invoke at runtime. Kept narrow on purpose — only
|
|
79
98
|
# scripts that a deployed hook actually executes belong here.
|
|
80
99
|
HOOK_RUNTIME_SCRIPTS: tuple[str, ...] = (
|
|
81
|
-
"output_filter_cli.py",
|
|
82
|
-
"output_filter_hook.py",
|
|
83
100
|
"session_state.py",
|
|
84
101
|
"session_token_stats.py",
|
|
85
102
|
"test_cohesion.py",
|
|
@@ -107,21 +124,6 @@ def _copy_hook_runtime_scripts(scripts_dst: Path) -> None:
|
|
|
107
124
|
shutil.copy2(src, dst)
|
|
108
125
|
dst.chmod(dst.stat().st_mode | 0o111)
|
|
109
126
|
copied += 1
|
|
110
|
-
output_filter_src = scripts_src / "tool_output_filter"
|
|
111
|
-
if output_filter_src.is_dir():
|
|
112
|
-
output_filter_dst = scripts_dst / output_filter_src.name
|
|
113
|
-
# Prune first: dirs_exist_ok alone never removes modules deleted in a
|
|
114
|
-
# newer release, and a stale .py at sys.path[0] would shadow the
|
|
115
|
-
# shipped implementation under `python3 -S`.
|
|
116
|
-
if output_filter_dst.is_dir() and not output_filter_dst.is_symlink():
|
|
117
|
-
shutil.rmtree(output_filter_dst)
|
|
118
|
-
shutil.copytree(
|
|
119
|
-
output_filter_src,
|
|
120
|
-
output_filter_dst,
|
|
121
|
-
dirs_exist_ok=True,
|
|
122
|
-
ignore=shutil.ignore_patterns("__pycache__", "*.pyc"),
|
|
123
|
-
)
|
|
124
|
-
copied += 1
|
|
125
127
|
if copied:
|
|
126
128
|
print(f" Copied: {copied} hook runtime assets to ~/.softspark/ai-toolkit/scripts/")
|
|
127
129
|
|
package/scripts/merge-hooks.py
CHANGED
|
@@ -54,6 +54,15 @@ LEGACY_TOOLKIT_HOOKS = {
|
|
|
54
54
|
],
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
# Hook scripts older releases installed and current releases no longer ship.
|
|
58
|
+
# Claude Code rewrites settings.json without the "_source" tag, so a retired
|
|
59
|
+
# entry cannot be reclaimed by tag, and it cannot be reclaimed by signature
|
|
60
|
+
# either because the matching entry is gone from the toolkit hooks.json. The
|
|
61
|
+
# script path under the toolkit-owned hooks directory is the durable marker.
|
|
62
|
+
RETIRED_HOOK_SCRIPTS = (
|
|
63
|
+
"ai-toolkit/hooks/filter-tool-output.sh",
|
|
64
|
+
)
|
|
65
|
+
|
|
57
66
|
|
|
58
67
|
def load_json(path: str) -> dict:
|
|
59
68
|
"""Load and parse a JSON file.
|
|
@@ -91,6 +100,25 @@ def _is_toolkit_entry(entry: dict) -> bool:
|
|
|
91
100
|
return False
|
|
92
101
|
|
|
93
102
|
|
|
103
|
+
def _is_retired_toolkit_entry(entry: dict) -> bool:
|
|
104
|
+
"""Check if every handler in an entry runs a retired toolkit hook script.
|
|
105
|
+
|
|
106
|
+
Requiring *all* handlers to match keeps a user entry that merely shares an
|
|
107
|
+
event with a retired hook, and keeps entries that chain their own command
|
|
108
|
+
after the toolkit one.
|
|
109
|
+
"""
|
|
110
|
+
handlers = entry.get("hooks", [])
|
|
111
|
+
if not handlers:
|
|
112
|
+
return False
|
|
113
|
+
for hook in handlers:
|
|
114
|
+
if not isinstance(hook, dict):
|
|
115
|
+
return False
|
|
116
|
+
command = hook.get("command", "")
|
|
117
|
+
if not any(script in command for script in RETIRED_HOOK_SCRIPTS):
|
|
118
|
+
return False
|
|
119
|
+
return True
|
|
120
|
+
|
|
121
|
+
|
|
94
122
|
def _entry_signature(entry: dict) -> tuple:
|
|
95
123
|
"""Return the behavior-defining parts of a hook entry.
|
|
96
124
|
|
|
@@ -112,7 +140,7 @@ def _entry_signature(entry: dict) -> tuple:
|
|
|
112
140
|
|
|
113
141
|
|
|
114
142
|
def strip_toolkit(hooks: dict, toolkit_hooks: dict | None = None) -> dict:
|
|
115
|
-
"""Remove entries tagged
|
|
143
|
+
"""Remove ai-toolkit entries: tagged, legacy-signature, or retired script."""
|
|
116
144
|
legacy_signatures: dict[str, set[tuple]] = {}
|
|
117
145
|
if toolkit_hooks:
|
|
118
146
|
for event, entries in toolkit_hooks.items():
|
|
@@ -137,7 +165,10 @@ def strip_toolkit(hooks: dict, toolkit_hooks: dict | None = None) -> dict:
|
|
|
137
165
|
if not _is_toolkit_entry(e)
|
|
138
166
|
and not (
|
|
139
167
|
isinstance(e, dict)
|
|
140
|
-
and
|
|
168
|
+
and (
|
|
169
|
+
_entry_signature(e) in signatures
|
|
170
|
+
or _is_retired_toolkit_entry(e)
|
|
171
|
+
)
|
|
141
172
|
)
|
|
142
173
|
]
|
|
143
174
|
if filtered:
|