@softspark/ai-toolkit 4.17.0 → 4.19.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/llms.txt CHANGED
@@ -21,12 +21,15 @@
21
21
  - [Plan: Offline-First SLM Profile — Lightweight Mode for Local Models](kb/history/completed/offline-slm-profile-plan-20260411.md)
22
22
  - [Retirement: Native Tool-Output Filter](kb/history/completed/output-filter-retirement-20260726.md)
23
23
  - [Plan: Output & Token Discipline](kb/history/completed/output-token-discipline-plan-20260504.md)
24
+ - [rtk Pack Integration](kb/history/completed/rtk-pack-integration-20260726.md)
25
+ - [Retirement: rtk-pack](kb/history/completed/rtk-pack-retirement-20260727.md)
24
26
  - [How-To Guides](kb/howto/README.md)
25
27
  - [Plan: Cloud Security Pack — Multi-Cloud Audit](kb/planning/cloud-security-pack-plan.md)
26
28
  - [Plan: Drop Cascade hooks after 2026-07-01 sunset](kb/planning/drop-cascade-hooks-after-sunset.md)
27
29
  - [PRD: MCP Context Trim v4.0](kb/planning/mcp-context-trim-v4-prd.md)
28
30
  - [SOP: Ecosystem Sync](kb/procedures/ecosystem-sync-sop.md)
29
31
  - [SOP: AI Toolkit Maintenance](kb/procedures/maintenance-sop.md)
32
+ - [SOP: Post-Release Testing](kb/procedures/post-release-testing-sop.md)
30
33
  - [SOP: Release Preparation](kb/procedures/release-preparation-sop.md)
31
34
  - [SOP: Release Verification](kb/procedures/release-verification-sop.md)
32
35
  - [Agents Catalog](kb/reference/agents-catalog.md)
package/manifest.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "4.17.0",
2
+ "version": "4.19.0",
3
3
  "components": {
4
4
  "agents": {
5
5
  "description": "44 specialized agents (orchestrator, backend, frontend, security, devops, etc.)",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softspark/ai-toolkit",
3
- "version": "4.17.0",
3
+ "version": "4.19.0",
4
4
  "description": "AI coding toolkit: 108 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",
@@ -454,6 +454,27 @@ def audit(toolkit_root: Path) -> list[Finding]:
454
454
  scan_secrets(agent_md, findings)
455
455
  scan_unicode(agent_md, findings)
456
456
 
457
+ # Scan plugin packs. Pack code ships and executes exactly like skill code,
458
+ # and a pack may carry an install script that runs at install time, so
459
+ # leaving app/plugins out of the HIGH gate exempted the highest-risk code
460
+ # in the repo from the check that exists to catch it.
461
+ plugins = app / "plugins"
462
+ if plugins.is_dir():
463
+ for pack_dir in sorted(plugins.iterdir()):
464
+ if not pack_dir.is_dir():
465
+ continue
466
+ for py in sorted(pack_dir.rglob("*.py")):
467
+ scan_file_patterns(py, PYTHON_HIGH, "HIGH", findings)
468
+ scan_file_patterns(py, PYTHON_WARN, "WARN", findings)
469
+ scan_secrets(py, findings)
470
+ for sh in sorted(pack_dir.rglob("*.sh")):
471
+ scan_file_patterns(sh, BASH_HIGH, "HIGH", findings)
472
+ scan_file_patterns(sh, BASH_WARN, "WARN", findings)
473
+ scan_secrets(sh, findings)
474
+ for md in sorted(pack_dir.rglob("*.md")):
475
+ scan_secrets(md, findings)
476
+ scan_unicode(md, findings)
477
+
457
478
  # Unicode safety across the rest of the shipped prompt surface.
458
479
  for extra in ("rules", "personas", "mcp-templates"):
459
480
  extra_dir = app / extra
@@ -490,3 +490,47 @@ def main() -> None:
490
490
 
491
491
  if __name__ == "__main__":
492
492
  main()
493
+
494
+
495
+ def _cleanup_config_path(target_dir: Path) -> Path | None:
496
+ return Path(target_dir).expanduser() / ".cursor" / "hooks.json"
497
+
498
+
499
+ def _cleanup_write(path: Path, document: dict) -> None:
500
+ path.write_text(json.dumps(document, indent=2, sort_keys=True) + "\n", encoding="utf-8")
501
+
502
+
503
+ def cleanup(target_dir: Path) -> None:
504
+ """Strip this toolkit's hook entries for an uninstall or profile downgrade.
505
+
506
+ Only entries tagged with SOURCE_TAG are removed; user and plugin-pack
507
+ entries are left in place. The file is deleted only when nothing survives,
508
+ so an uninstall does not take a user's own configuration with it.
509
+ """
510
+ config = _cleanup_config_path(target_dir)
511
+ if config is None or not config.is_file() or config.is_symlink():
512
+ return
513
+ try:
514
+ with open(config, encoding="utf-8") as handle:
515
+ document = json.load(handle)
516
+ except (OSError, json.JSONDecodeError):
517
+ return
518
+ if not isinstance(document, dict):
519
+ return
520
+
521
+ hooks = document.get("hooks")
522
+ if not isinstance(hooks, dict):
523
+ return
524
+ survivors = strip_toolkit_hooks(hooks)
525
+ if survivors == hooks:
526
+ return
527
+
528
+ if survivors:
529
+ document["hooks"] = survivors
530
+ else:
531
+ document.pop("hooks", None)
532
+
533
+ if document:
534
+ _cleanup_write(config, document)
535
+ else:
536
+ config.unlink()
@@ -179,3 +179,47 @@ def main() -> None:
179
179
 
180
180
  if __name__ == "__main__":
181
181
  main()
182
+
183
+
184
+ def _cleanup_config_path(target_dir: Path) -> Path | None:
185
+ return Path(target_dir).expanduser() / ".gemini" / "settings.json"
186
+
187
+
188
+ def _cleanup_write(path: Path, document: dict) -> None:
189
+ _write_settings_atomic(path, document)
190
+
191
+
192
+ def cleanup(target_dir: Path) -> None:
193
+ """Strip this toolkit's hook entries for an uninstall or profile downgrade.
194
+
195
+ Only entries tagged with SOURCE_TAG are removed; user and plugin-pack
196
+ entries are left in place. The file is deleted only when nothing survives,
197
+ so an uninstall does not take a user's own configuration with it.
198
+ """
199
+ config = _cleanup_config_path(target_dir)
200
+ if config is None or not config.is_file() or config.is_symlink():
201
+ return
202
+ try:
203
+ with open(config, encoding="utf-8") as handle:
204
+ document = json.load(handle)
205
+ except (OSError, json.JSONDecodeError):
206
+ return
207
+ if not isinstance(document, dict):
208
+ return
209
+
210
+ hooks = document.get("hooks")
211
+ if not isinstance(hooks, dict):
212
+ return
213
+ survivors = strip_toolkit_hooks(hooks)
214
+ if survivors == hooks:
215
+ return
216
+
217
+ if survivors:
218
+ document["hooks"] = survivors
219
+ else:
220
+ document.pop("hooks", None)
221
+
222
+ if document:
223
+ _cleanup_write(config, document)
224
+ else:
225
+ config.unlink()
@@ -286,7 +286,20 @@ def parse_args(argv: list[str]) -> dict:
286
286
  print(f"Unknown option: {arg}")
287
287
  sys.exit(1)
288
288
  else:
289
- cfg["target_dir"] = Path(arg)
289
+ # A bare word that is not an existing directory is almost never a
290
+ # deliberate install target: `ai-toolkit install <pack>` reads like
291
+ # `plugin install <pack>` and silently writes a full toolkit tree
292
+ # into ./<pack>. Require an existing directory or an explicit path
293
+ # separator, so creating a new top-level directory has to be asked
294
+ # for rather than typed by accident.
295
+ candidate = Path(arg)
296
+ looks_like_path = "/" in arg or "\\" in arg or arg.startswith("~") or arg == "."
297
+ if not candidate.is_dir() and not looks_like_path:
298
+ print(f"Refusing to install into a new directory named '{arg}'.")
299
+ print(f" If you meant a plugin pack: ai-toolkit plugin install {arg}")
300
+ print(f" If you really meant a path: ai-toolkit install ./{arg}")
301
+ sys.exit(1)
302
+ cfg["target_dir"] = candidate
290
303
  i += 1
291
304
  return cfg
292
305