@softspark/ai-toolkit 4.17.0 → 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 +52 -0
- package/README.md +10 -10
- package/app/.claude-plugin/plugin.json +1 -1
- 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/bin/ai-toolkit.js +15 -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/cli-reference.md +1 -1
- package/kb/reference/plugin-pack-conventions.md +17 -2
- package/llms-full.txt +1028 -7
- package/llms.txt +2 -0
- package/manifest.json +1 -1
- package/package.json +1 -1
- package/scripts/audit_skills.py +21 -0
- package/scripts/plugin.py +136 -16
- package/scripts/verify_rtk_binary.py +335 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Report rtk-pack health: binary, digest, and hook wiring.
|
|
3
|
+
|
|
4
|
+
Invoked by `ai-toolkit plugin status` and `ai-toolkit doctor` through the
|
|
5
|
+
generic scripts/status.py hook in scripts/plugin.py.
|
|
6
|
+
|
|
7
|
+
The point of this script is to distinguish "installed" from "working". The pack
|
|
8
|
+
can be recorded as installed, have its hook wired, and still do nothing at all
|
|
9
|
+
because the binary never downloaded. That is the failure mode this project has
|
|
10
|
+
already shipped once, so it gets its own line rather than being inferred.
|
|
11
|
+
|
|
12
|
+
Stdlib only. Never raises; exit code is always 0.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import hashlib
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import subprocess
|
|
20
|
+
import sys
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
PACK_NAME = "rtk-pack"
|
|
24
|
+
TOOLKIT_DATA_DIR = Path(
|
|
25
|
+
os.environ.get("AI_TOOLKIT_DATA_DIR", Path.home() / ".softspark" / "ai-toolkit")
|
|
26
|
+
)
|
|
27
|
+
PACK_STATE_DIR = TOOLKIT_DATA_DIR / "plugin-scripts" / PACK_NAME
|
|
28
|
+
VERSION_FILE = PACK_STATE_DIR / "version.json"
|
|
29
|
+
HOOK_FILE = TOOLKIT_DATA_DIR / "hooks" / f"plugin-{PACK_NAME}-rewrite.sh"
|
|
30
|
+
CLAUDE_SETTINGS = Path.home() / ".claude" / "settings.json"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def find_binary() -> Path | None:
|
|
34
|
+
for candidate in (PACK_STATE_DIR / "bin" / "rtk", PACK_STATE_DIR / "bin" / "rtk.exe"):
|
|
35
|
+
if candidate.is_file():
|
|
36
|
+
return candidate
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def digest_of(path: Path) -> str:
|
|
41
|
+
h = hashlib.sha256()
|
|
42
|
+
with path.open("rb") as handle:
|
|
43
|
+
for chunk in iter(lambda: handle.read(1 << 16), b""):
|
|
44
|
+
h.update(chunk)
|
|
45
|
+
return h.hexdigest()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def hook_registered() -> bool:
|
|
49
|
+
"""True when settings.json still points at this pack's hook."""
|
|
50
|
+
try:
|
|
51
|
+
settings = json.loads(CLAUDE_SETTINGS.read_text(encoding="utf-8"))
|
|
52
|
+
except (OSError, json.JSONDecodeError):
|
|
53
|
+
return False
|
|
54
|
+
return f"plugin-{PACK_NAME}-rewrite.sh" in json.dumps(settings.get("hooks", {}))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def main() -> int:
|
|
58
|
+
binary = find_binary()
|
|
59
|
+
if binary is None:
|
|
60
|
+
print("rtk binary: MISSING — the hook is inert and every command runs unchanged")
|
|
61
|
+
print(" fix: ai-toolkit plugin install rtk-pack")
|
|
62
|
+
return 0
|
|
63
|
+
|
|
64
|
+
record: dict = {}
|
|
65
|
+
try:
|
|
66
|
+
record = json.loads(VERSION_FILE.read_text(encoding="utf-8"))
|
|
67
|
+
except (OSError, json.JSONDecodeError):
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
upstream = record.get("upstream_version", "unknown")
|
|
71
|
+
print(f"rtk binary: {binary} (upstream {upstream})")
|
|
72
|
+
|
|
73
|
+
# The recorded digest is of the archive, not the extracted binary, so it
|
|
74
|
+
# cannot be recomputed here. Report the binary's own digest and whether the
|
|
75
|
+
# install record still exists, and say which is which rather than implying
|
|
76
|
+
# a verification that is not happening.
|
|
77
|
+
if record:
|
|
78
|
+
print(f" install record: {record.get('asset', '?')} sha256 {record.get('sha256', '?')[:16]}…")
|
|
79
|
+
else:
|
|
80
|
+
print(" install record: MISSING — reinstall to restore provenance")
|
|
81
|
+
print(f" binary sha256: {digest_of(binary)[:16]}…")
|
|
82
|
+
|
|
83
|
+
if not os.access(binary, os.X_OK):
|
|
84
|
+
print(" executable: NO — the hook cannot run it")
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
proc = subprocess.run([str(binary), "--version"], capture_output=True, text=True, timeout=15)
|
|
88
|
+
reported = (proc.stdout.strip() or proc.stderr.strip()).splitlines()[0] if proc.returncode == 0 else ""
|
|
89
|
+
except (OSError, subprocess.SubprocessError, IndexError):
|
|
90
|
+
reported = ""
|
|
91
|
+
if reported:
|
|
92
|
+
print(f" runs: {reported}")
|
|
93
|
+
expected = str(upstream).lstrip("v")
|
|
94
|
+
if expected and expected not in reported:
|
|
95
|
+
print(f" WARNING: binary reports {reported}, manifest pins {upstream}")
|
|
96
|
+
else:
|
|
97
|
+
print(" runs: NO — binary present but will not start")
|
|
98
|
+
|
|
99
|
+
print(f" hook script: {'present' if HOOK_FILE.is_file() else 'MISSING'}")
|
|
100
|
+
print(f" hook registered in settings.json: {'yes' if hook_registered() else 'no'}")
|
|
101
|
+
return 0
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
if __name__ == "__main__":
|
|
105
|
+
sys.exit(main())
|
package/bin/ai-toolkit.js
CHANGED
|
@@ -574,6 +574,21 @@ function handleUpdate(args) {
|
|
|
574
574
|
// User-provided args override state-derived args
|
|
575
575
|
run(scriptPath('install.py'), [...stateArgs, ...args]);
|
|
576
576
|
|
|
577
|
+
// Installed plugin packs live under ~/.softspark/ai-toolkit and are global,
|
|
578
|
+
// so --local (project-local config only) leaves them alone. The wiring is
|
|
579
|
+
// generic: plugin.py decides per pack whether anything changed and stays
|
|
580
|
+
// silent when nothing has, so this adds no noise for the common case.
|
|
581
|
+
if (!isLocal) {
|
|
582
|
+
const pluginArgs = ['update', '--editor', 'all', '--all'];
|
|
583
|
+
if (isDryRun) pluginArgs.push('--dry-run');
|
|
584
|
+
try {
|
|
585
|
+
run(scriptPath('plugin.py'), pluginArgs);
|
|
586
|
+
} catch (_err) {
|
|
587
|
+
// A pack update never fails the core update that already succeeded.
|
|
588
|
+
console.warn('WARN: plugin pack update skipped');
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
577
592
|
// After global update (not --local), propagate to all registered projects
|
|
578
593
|
if (!isLocal && !isDryRun) {
|
|
579
594
|
const registryPath = path.join(process.env.HOME, '.softspark', 'ai-toolkit', 'projects.json');
|