banner_notice.py 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  1. #!/usr/bin/env python3
  2. """Show the Claude Security banner as a display-only systemMessage.
  3. Always exits 0 with either the banner or no output.
  4. """
  5. import contextlib
  6. import json
  7. import os
  8. import sys
  9. from typing import cast
  10. PLUGIN_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
  11. LAUNCH_NOTICE = "Launching Claude Security..."
  12. BOX_INNER = 53
  13. MIN_PYTHON = (3, 9)
  14. def plugin_version() -> str:
  15. """The plugin's version from plugin.json, or "unknown". Never raises."""
  16. try:
  17. path = os.path.join(PLUGIN_ROOT, ".claude-plugin", "plugin.json")
  18. with open(path, encoding="utf-8") as handle:
  19. loaded = cast("object", json.load(handle))
  20. except Exception:
  21. return "unknown"
  22. if not isinstance(loaded, dict):
  23. return "unknown"
  24. version = cast("dict[str, object]", loaded).get("version")
  25. return version if isinstance(version, str) and version else "unknown"
  26. def box_line(text: str) -> str:
  27. """One boxed body line, centered so the right border always aligns."""
  28. if len(text) > BOX_INNER:
  29. text = text[:BOX_INNER]
  30. return " │" + text.center(BOX_INNER) + "│"
  31. def bottom_border(version: str) -> str:
  32. """The box's bottom edge with the version set into it, right-aligned."""
  33. tag = f" v{version} "
  34. fill = BOX_INNER - len(tag) - 3
  35. if fill < 1:
  36. return " └" + "─" * BOX_INNER + "┘"
  37. return " └" + "─" * fill + tag + "─" * 3 + "┘"
  38. def banner() -> str:
  39. lines = [
  40. "",
  41. " ██████╗██╗ █████╗ ██╗ ██╗██████╗ ███████╗",
  42. " ██╔════╝██║ ██╔══██╗██║ ██║██╔══██╗██╔════╝",
  43. " ██║ ██║ ███████║██║ ██║██║ ██║█████╗",
  44. " ██║ ██║ ██╔══██║██║ ██║██║ ██║██╔══╝",
  45. " ╚██████╗███████╗██║ ██║╚██████╔╝██████╔╝███████╗",
  46. " ╚═════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝",
  47. " ──────── S · E · C · U · R · I · T · Y ────────",
  48. " ┌" + "─" * BOX_INNER + "┐",
  49. box_line("Find and fix vulnerabilities in source code"),
  50. bottom_border(plugin_version()),
  51. "",
  52. ]
  53. return "\n".join(lines)
  54. def emit(message: str) -> None:
  55. """Write one systemMessage. Never raises; a failed write is just no banner."""
  56. try:
  57. sys.stdout.write(json.dumps({"systemMessage": message}))
  58. sys.stdout.flush()
  59. except Exception:
  60. # Also silence the interpreter's exit-time flush of the buffered message.
  61. with contextlib.suppress(Exception):
  62. os.dup2(os.open(os.devnull, os.O_WRONLY), sys.stdout.fileno())
  63. def main() -> int:
  64. if sys.version_info < MIN_PYTHON:
  65. need = f"{MIN_PYTHON[0]}.{MIN_PYTHON[1]}"
  66. have = ".".join(str(part) for part in sys.version_info[:3])
  67. emit(
  68. f"\n\u26a0\ufe0f Claude Security needs python3 {need} or newer, but this "
  69. f"python3 is {have}. Scanning and fixing will fail until a newer "
  70. "python3 is first on PATH.\n"
  71. )
  72. return 0
  73. try:
  74. message = "\n" + LAUNCH_NOTICE + "\n\n" + banner()
  75. except Exception:
  76. message = "\n" + LAUNCH_NOTICE + "\n"
  77. emit(message)
  78. return 0
  79. if __name__ == "__main__":
  80. sys.exit(main())