@softspark/ai-toolkit 4.28.0 → 4.29.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 +85 -0
- package/README.md +15 -16
- package/app/.claude-plugin/plugin.json +1 -1
- package/app/hooks/guard-destructive.sh +28 -10
- package/kb/reference/hooks-catalog.md +7 -0
- package/llms-full.txt +7 -0
- package/manifest.json +1 -1
- package/package.json +1 -1
- package/scripts/doctor.py +80 -4
- package/scripts/install.py +12 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,91 @@ Versioning follows [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## v4.29.1 — The guard stops blocking the safe branch delete (2026-08-21)
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **`guard-destructive.sh` no longer folds `-d` onto `-D`.** Every pattern was
|
|
15
|
+
matched by a single `grep -qEi`, so the `git\s+branch\s+-D\b` pattern also
|
|
16
|
+
caught `git branch -d`. Those are different operations: `-D` discards an
|
|
17
|
+
unmerged branch, `-d` refuses to. The safe one is what
|
|
18
|
+
`.claude/rules/ai-toolkit-git-workflow.md` tells users to run after every
|
|
19
|
+
merge, and the guard blocked it — a false positive on the toolkit's own
|
|
20
|
+
advice, in the toolkit's own safety hook.
|
|
21
|
+
- **Patterns are matched in two passes, split by whether case carries meaning.**
|
|
22
|
+
Command names and flags go through a case-sensitive `grep -qE`; POSIX flags
|
|
23
|
+
that differ only in case are different flags, and folding them together can
|
|
24
|
+
only lose information. SQL keywords and the Windows `format` command go
|
|
25
|
+
through a case-insensitive `grep -qEi`, because their casing genuinely varies
|
|
26
|
+
in the wild — `drop table`, `Truncate`, and `delete from` all still block.
|
|
27
|
+
The `-i` was presumably there for the SQL patterns all along; it was the
|
|
28
|
+
flag patterns that paid for it.
|
|
29
|
+
|
|
30
|
+
- **A cross-file race in the test suite.** `doctor --fix regenerates missing
|
|
31
|
+
llms-full.txt` deleted the artifact from the shared checkout and asserted on
|
|
32
|
+
doctor's `FIXED` line. `npm test` runs `bats --jobs 4
|
|
33
|
+
--no-parallelize-within-files`, so files run concurrently, and
|
|
34
|
+
`test_doctor_plugin_double_load.bats` also runs `doctor --fix` — which
|
|
35
|
+
regenerates that artifact. When it won, the assertion saw a present file, no
|
|
36
|
+
`FIXED` line, and a red suite; the test also left a `llms-full.txt.test-bak`
|
|
37
|
+
in the repo, because the restoring `mv` never ran. v4.29.0 added a second
|
|
38
|
+
`doctor --fix` call to the plugin test file, which is what made a latent race
|
|
39
|
+
start firing. The test now runs doctor against a private copy of the toolkit,
|
|
40
|
+
so it mutates no shared state at all.
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
|
|
44
|
+
- `kb/reference/hooks-catalog.md` documents the two-pass matching and lists the
|
|
45
|
+
safe branch delete among the guard's exemptions.
|
|
46
|
+
- Test count: 1666 → 1671. The new cases pin both directions: the safe delete
|
|
47
|
+
and an uppercase non-flag pass, lowercase and mixed-case SQL block.
|
|
48
|
+
|
|
49
|
+
### Known gap (not addressed here)
|
|
50
|
+
|
|
51
|
+
- The guard matches `git branch -D` but not its long form, `git branch --delete
|
|
52
|
+
--force`. That gap predates this fix and closing it is a coverage change
|
|
53
|
+
rather than a case-sensitivity one, so it is left for a separate decision.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## v4.29.0 — Stale plugin uploads stop hiding (2026-08-21)
|
|
58
|
+
|
|
59
|
+
### Added
|
|
60
|
+
|
|
61
|
+
- **`doctor` reports Claude app plugin version drift.** Check 11 now compares the
|
|
62
|
+
`version` recorded for every `ai-toolkit@…` entry in
|
|
63
|
+
`~/.claude/plugins/installed_plugins.json` against the installed toolkit's
|
|
64
|
+
`package.json` and warns when they differ. The upload is a point-in-time ZIP:
|
|
65
|
+
once the toolkit moves on, Chat and Cowork keep running the skills, agents,
|
|
66
|
+
hooks, and rules from whenever the export was made, with no signal anywhere
|
|
67
|
+
that they are behind. `--fix` deliberately cannot clear this warning — the
|
|
68
|
+
export can be regenerated but the upload itself happens by hand in the app's
|
|
69
|
+
Customize > Plugins panel, so the check names the two steps instead of
|
|
70
|
+
pretending to do them.
|
|
71
|
+
|
|
72
|
+
### Changed
|
|
73
|
+
|
|
74
|
+
- **Version drift is checked before the enabled/disabled branch.** A plugin
|
|
75
|
+
disabled for Claude Code still feeds Chat and Cowork, which have no other
|
|
76
|
+
channel for toolkit content. The old flow returned `OK: registered but
|
|
77
|
+
disabled` and never looked at versions, so the state a user reaches by running
|
|
78
|
+
`doctor --fix` was also the state where a stale upload passed silently. That
|
|
79
|
+
was the wrong place to stop looking.
|
|
80
|
+
- **`ai-toolkit install` re-asserts the plugin as disabled for Claude Code.**
|
|
81
|
+
Uploading the ZIP re-enables the plugin every time, which put the double-load
|
|
82
|
+
fix back on whoever remembered to run `doctor --fix` afterwards. Global
|
|
83
|
+
installs and updates now flip enabled toolkit plugins to `false` in
|
|
84
|
+
`~/.claude/settings.json` and print each key they touched. `--local` and
|
|
85
|
+
`--dry-run` are untouched, and non-toolkit plugins are never modified. The
|
|
86
|
+
disable logic moved into `disable_toolkit_plugins_for_claude_code()` in
|
|
87
|
+
`scripts/doctor.py`, shared by both call sites.
|
|
88
|
+
- Test count: 1659 → 1666.
|
|
89
|
+
- Skill body budget threshold unchanged. The largest body (`medplum-rules`, 16760
|
|
90
|
+
bytes) sits 1240 bytes under the 18000-byte warn line, short of the 2000-byte
|
|
91
|
+
margin the ratchet requires, so the threshold stays where it is this release.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
10
95
|
## v4.28.0 — MCP servers reach Chat and Cowork (2026-08-21)
|
|
11
96
|
|
|
12
97
|
### Added
|
package/README.md
CHANGED
|
@@ -6,22 +6,21 @@
|
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
[](app/skills/)
|
|
8
8
|
[](app/agents/)
|
|
9
|
-
[](tests/)
|
|
10
|
+
|
|
11
|
+
## What's New in v4.29.1
|
|
12
|
+
|
|
13
|
+
**v4.29.1** stops the destructive-command guard from blocking a safe branch delete:
|
|
14
|
+
|
|
15
|
+
- `guard-destructive.sh` matched every pattern with one case-insensitive grep, so
|
|
16
|
+
the force-delete pattern also caught the lowercase safe delete -- the variant
|
|
17
|
+
that refuses to drop an unmerged branch, and exactly the post-merge cleanup the
|
|
18
|
+
toolkit's own git-workflow rules prescribe.
|
|
19
|
+
- Patterns are now matched in two passes. Command names and flags are matched
|
|
20
|
+
case-sensitively, because short flags that differ only in case are different
|
|
21
|
+
flags. SQL keywords and the Windows `format` command stay case-insensitive, so
|
|
22
|
+
lowercase SQL still blocks.
|
|
23
|
+
- Force delete still blocks. Test count: 1666 -> 1671.
|
|
25
24
|
|
|
26
25
|
## Table of Contents
|
|
27
26
|
|
|
@@ -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.
|
|
6
|
+
"version": "4.29.1",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "SoftSpark",
|
|
9
9
|
"url": "https://github.com/softspark"
|
|
@@ -40,7 +40,15 @@ fi
|
|
|
40
40
|
# --force or -f left behind still blocks.
|
|
41
41
|
SAFE_STRIPPED=$(printf '%s' "$NORMALIZED" | sed -E 's/--force-with-lease(=[^ ]*)?//g; s/--force-if-includes//g')
|
|
42
42
|
|
|
43
|
-
# Destructive patterns — word-boundary aware where possible
|
|
43
|
+
# Destructive patterns — word-boundary aware where possible.
|
|
44
|
+
#
|
|
45
|
+
# Two buckets, matched by two greps, because case sensitivity is not a uniform
|
|
46
|
+
# property of these patterns. POSIX command names and flags are case-SIGNIFICANT:
|
|
47
|
+
# `-d` and `-D` are different flags, and folding them together made the guard
|
|
48
|
+
# block `git branch -d` — the safe delete that refuses unmerged branches — as if
|
|
49
|
+
# it were `-D`. SQL keywords carry no such distinction and are written in every
|
|
50
|
+
# casing in the wild, so they need folding. One case-insensitive grep over both
|
|
51
|
+
# sets cannot express that, and the flag bucket is the one that loses.
|
|
44
52
|
DESTRUCTIVE_PATTERNS=(
|
|
45
53
|
# rm variants (short flags, long flags, separated flags, sudo, xargs/find piped)
|
|
46
54
|
'rm\s+(-[rRf]{2,}|-r\s+-f|-f\s+-r)'
|
|
@@ -54,18 +62,13 @@ DESTRUCTIVE_PATTERNS=(
|
|
|
54
62
|
'find\s+.*-delete'
|
|
55
63
|
'find\s+.*-exec\s+rm\b'
|
|
56
64
|
|
|
57
|
-
# SQL destructive operations
|
|
58
|
-
'DROP\s+(TABLE|DATABASE|SCHEMA|INDEX)'
|
|
59
|
-
'TRUNCATE\s+'
|
|
60
|
-
'DELETE\s+FROM\s+\S+\s*(;|$|WHERE\s+1)'
|
|
61
|
-
|
|
62
65
|
# Disk/filesystem destructive
|
|
63
|
-
'format\s+/'
|
|
64
66
|
'dd\s+if='
|
|
65
67
|
'mkfs\b'
|
|
66
68
|
'shred\b'
|
|
67
69
|
|
|
68
|
-
# Git destructive operations
|
|
70
|
+
# Git destructive operations. `-D` only: `git branch -d` refuses to delete an
|
|
71
|
+
# unmerged branch and is the documented post-merge cleanup, not a hazard.
|
|
69
72
|
'git\s+push\s+(--force|-f)\b'
|
|
70
73
|
'git\s+push\s+.*--force'
|
|
71
74
|
'git\s+reset\s+--hard'
|
|
@@ -88,10 +91,25 @@ DESTRUCTIVE_PATTERNS=(
|
|
|
88
91
|
'>\s*/dev/sd[a-z]'
|
|
89
92
|
)
|
|
90
93
|
|
|
91
|
-
#
|
|
94
|
+
# Case-insensitive bucket: patterns whose real-world casing genuinely varies.
|
|
95
|
+
# SQL is written DROP TABLE, drop table, and Drop Table with equal frequency;
|
|
96
|
+
# `format` is a Windows command and Windows command names are case-insensitive.
|
|
97
|
+
DESTRUCTIVE_PATTERNS_NOCASE=(
|
|
98
|
+
# SQL destructive operations
|
|
99
|
+
'DROP\s+(TABLE|DATABASE|SCHEMA|INDEX)'
|
|
100
|
+
'TRUNCATE\s+'
|
|
101
|
+
'DELETE\s+FROM\s+\S+\s*(;|$|WHERE\s+1)'
|
|
102
|
+
|
|
103
|
+
# Disk/filesystem destructive
|
|
104
|
+
'format\s+/'
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# Build combined regexes
|
|
92
108
|
REGEX=$(IFS='|'; echo "${DESTRUCTIVE_PATTERNS[*]}")
|
|
109
|
+
REGEX_NOCASE=$(IFS='|'; echo "${DESTRUCTIVE_PATTERNS_NOCASE[*]}")
|
|
93
110
|
|
|
94
|
-
if echo "$SAFE_STRIPPED" | grep -
|
|
111
|
+
if echo "$SAFE_STRIPPED" | grep -qE "($REGEX)" \
|
|
112
|
+
|| echo "$SAFE_STRIPPED" | grep -qEi "($REGEX_NOCASE)"; then
|
|
95
113
|
echo "WARNING: Potentially destructive command detected. Please verify." >&2
|
|
96
114
|
exit 2
|
|
97
115
|
fi
|
|
@@ -95,8 +95,15 @@ for debugging; `AI_TOOLKIT_HOOK_QUIET=1` keeps it silent explicitly.
|
|
|
95
95
|
- `git push --force`
|
|
96
96
|
- `chmod -R 777`
|
|
97
97
|
|
|
98
|
+
**Case sensitivity:** patterns are matched in two passes. Command names and flags
|
|
99
|
+
are matched case-**sensitively**, because `-d` and `-D` are different flags —
|
|
100
|
+
`git branch -D` blocks, `git branch -d` does not. SQL keywords and the Windows
|
|
101
|
+
`format` command are matched case-**insensitively**, so `drop table` blocks the
|
|
102
|
+
same as `DROP TABLE`.
|
|
103
|
+
|
|
98
104
|
**Exemptions (avoid false positives):**
|
|
99
105
|
- `git push --force-with-lease` / `--force-if-includes` — the safe force-push variants are allowed.
|
|
106
|
+
- `git branch -d` — the safe delete refuses unmerged branches and is the post-merge cleanup the git-workflow rules prescribe. `git branch -D` still blocks.
|
|
100
107
|
- A single, non-chained `echo`/`printf`/`git commit`/`git tag` carrying a destructive token as *data* (e.g. a commit message mentioning `DROP TABLE`) is allowed. Chained commands (`&&`, `;`, `|`) are still inspected in full.
|
|
101
108
|
|
|
102
109
|
### PreToolUse (file ops) — `guard-path.sh`
|
package/llms-full.txt
CHANGED
|
@@ -12244,8 +12244,15 @@ for debugging; `AI_TOOLKIT_HOOK_QUIET=1` keeps it silent explicitly.
|
|
|
12244
12244
|
- `git push --force`
|
|
12245
12245
|
- `chmod -R 777`
|
|
12246
12246
|
|
|
12247
|
+
**Case sensitivity:** patterns are matched in two passes. Command names and flags
|
|
12248
|
+
are matched case-**sensitively**, because `-d` and `-D` are different flags —
|
|
12249
|
+
`git branch -D` blocks, `git branch -d` does not. SQL keywords and the Windows
|
|
12250
|
+
`format` command are matched case-**insensitively**, so `drop table` blocks the
|
|
12251
|
+
same as `DROP TABLE`.
|
|
12252
|
+
|
|
12247
12253
|
**Exemptions (avoid false positives):**
|
|
12248
12254
|
- `git push --force-with-lease` / `--force-if-includes` — the safe force-push variants are allowed.
|
|
12255
|
+
- `git branch -d` — the safe delete refuses unmerged branches and is the post-merge cleanup the git-workflow rules prescribe. `git branch -D` still blocks.
|
|
12249
12256
|
- A single, non-chained `echo`/`printf`/`git commit`/`git tag` carrying a destructive token as *data* (e.g. a commit message mentioning `DROP TABLE`) is allowed. Chained commands (`&&`, `;`, `|`) are still inspected in full.
|
|
12250
12257
|
|
|
12251
12258
|
### PreToolUse (file ops) — `guard-path.sh`
|
package/manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softspark/ai-toolkit",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.29.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/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
|
-
|
|
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
|
|
824
|
+
for key in disabled:
|
|
749
825
|
dr.fixed(f"disabled {key} for Claude Code (global install stays authoritative)")
|
|
750
826
|
|
|
751
827
|
|
package/scripts/install.py
CHANGED
|
@@ -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
|
|