@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.
@@ -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');