diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e1b322..7207dd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## 0.1.0-canary.4 + +- Use the vllnt logo as the Claude plugin icon: `assets/vllnt-logo.png` is wrapped by the generator into `.claude-plugin/icon.svg`. + - Remove distribution version numbers from the README so routine version bumps need no README edits; `VERSION` and this changelog remain the version records. - Record the `v0.1.0-canary.3` GitHub prerelease. diff --git a/README.md b/README.md index 81ea789..d89b144 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Then start a new chat and invoke a namespaced skill such as `/vstack:plan-work`. | `upstream.lock.json` | Trusted upstream repository and immutable commit | | `VERSION` | Shared distribution version | | `scripts/sync.py` | Standard-library generator and read-only drift check | -| `assets/claude-icon.svg` | Claude plugin icon, copied into the Claude package as `.claude-plugin/icon.svg` | +| `assets/vllnt-logo.png` | vllnt logo; the generator wraps it as the Claude package's `.claude-plugin/icon.svg` | | `plugins/claude/` | Generated Claude Code native plugin | | `plugins/codex/` | Generated portable Agent Plugin for Codex | | `plugins/cursor/` | Generated Cursor native plugin and principles rule | diff --git a/VERSION b/VERSION index 6cf7a46..cc9a41d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.1.0-canary.3 +0.1.0-canary.4 diff --git a/assets/claude-icon.svg b/assets/claude-icon.svg deleted file mode 100644 index 1f4882c..0000000 --- a/assets/claude-icon.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/vllnt-logo.png b/assets/vllnt-logo.png new file mode 100644 index 0000000..10b7149 Binary files /dev/null and b/assets/vllnt-logo.png differ diff --git a/plugins/claude/.claude-plugin/icon.svg b/plugins/claude/.claude-plugin/icon.svg index 1f4882c..e112555 100644 --- a/plugins/claude/.claude-plugin/icon.svg +++ b/plugins/claude/.claude-plugin/icon.svg @@ -1,10 +1 @@ - - - - - - - - - - + diff --git a/plugins/claude/.claude-plugin/plugin.json b/plugins/claude/.claude-plugin/plugin.json index 204a497..8564a6a 100644 --- a/plugins/claude/.claude-plugin/plugin.json +++ b/plugins/claude/.claude-plugin/plugin.json @@ -13,5 +13,5 @@ "license": "MIT", "name": "vstack", "repository": "https://github.com/vllnt/stack", - "version": "0.1.0-canary.3" + "version": "0.1.0-canary.4" } diff --git a/plugins/claude/SOURCE.json b/plugins/claude/SOURCE.json index 7cf958e..29fb7a8 100644 --- a/plugins/claude/SOURCE.json +++ b/plugins/claude/SOURCE.json @@ -1,7 +1,7 @@ { "files": { - ".claude-plugin/icon.svg": "dc3b420724330eed1296c63c5c6d3290fa031d0eceb39180a2258f6992c76108", - ".claude-plugin/plugin.json": "ff44d6e276f70392c84dcfa3cf86f33ce5001ab864781dae8adbadbc90102689", + ".claude-plugin/icon.svg": "0b4b639f6df841b4904ec0cd9f6d38131c6373ce4a52a1e7fe6452a37598d7f1", + ".claude-plugin/plugin.json": "4808262e4a7a3e327124bfd2b6e23590f33502b981cfcc4e893be02674ab4a53", "LICENSE": "24ba061a4603738ddd5105ef4abc198eeded5ac7efce23b360561816f4dd6559", "skills/build-landing-page/README.md": "46a280eadac8962180cf3f0c42eb0e845a3b0156a644569182d932881e1d51f8", "skills/build-landing-page/SKILL.md": "b08e4963d6149f3647b28a0d9dccdc8793812abab95a1d081d93699e142de0f0", diff --git a/plugins/codex/SOURCE.json b/plugins/codex/SOURCE.json index ce064ad..f10d7d2 100644 --- a/plugins/codex/SOURCE.json +++ b/plugins/codex/SOURCE.json @@ -1,7 +1,7 @@ { "files": { "LICENSE": "24ba061a4603738ddd5105ef4abc198eeded5ac7efce23b360561816f4dd6559", - "plugin.json": "aea0fdbea75ce7f02be70116bf560bcffc31258b6def32b79d302f2aee307dd4", + "plugin.json": "789b793f660535ffddadde5268e5d776ea9a76db6b4809014eb71537d877f235", "skills/build-landing-page/README.md": "46a280eadac8962180cf3f0c42eb0e845a3b0156a644569182d932881e1d51f8", "skills/build-landing-page/SKILL.md": "b08e4963d6149f3647b28a0d9dccdc8793812abab95a1d081d93699e142de0f0", "skills/build-landing-page/references/copywriting-formulas.md": "6d19f6166e1badcfdda0af4f8b303160e94ee6e6149321157e12849654cc013e", diff --git a/plugins/codex/plugin.json b/plugins/codex/plugin.json index 9569b5b..ab43f83 100644 --- a/plugins/codex/plugin.json +++ b/plugins/codex/plugin.json @@ -6,5 +6,5 @@ "description": "Evidence-led engineering workflows to plan, build, review, and ship software with AI agents. Part of the vllnt universe: open, sovereign tools for freedom by design (vllnt.com).", "license": "MIT", "name": "vstack", - "version": "0.1.0-canary.3" + "version": "0.1.0-canary.4" } diff --git a/plugins/cursor/.cursor-plugin/plugin.json b/plugins/cursor/.cursor-plugin/plugin.json index d93bf60..8dcbac1 100644 --- a/plugins/cursor/.cursor-plugin/plugin.json +++ b/plugins/cursor/.cursor-plugin/plugin.json @@ -5,5 +5,5 @@ "description": "Evidence-led engineering workflows to plan, build, review, and ship software with AI agents. Part of the vllnt universe: open, sovereign tools for freedom by design (vllnt.com).", "license": "MIT", "name": "vstack", - "version": "0.1.0-canary.3" + "version": "0.1.0-canary.4" } diff --git a/plugins/cursor/SOURCE.json b/plugins/cursor/SOURCE.json index b2c3d24..2c247bb 100644 --- a/plugins/cursor/SOURCE.json +++ b/plugins/cursor/SOURCE.json @@ -1,6 +1,6 @@ { "files": { - ".cursor-plugin/plugin.json": "3fbf8954a01a9f6a73fec13119d07eaa08e465fb3ac59c27cf711685cdd860f3", + ".cursor-plugin/plugin.json": "b4325fd2a33221ef271b6b92d621c9c62052b6bd6f468b567d47401fe35ee2ea", "LICENSE": "24ba061a4603738ddd5105ef4abc198eeded5ac7efce23b360561816f4dd6559", "rules/vstack-principles.mdc": "5234bc0506e0a328d6163cd83785d2f37dd1fc23ef84287d71e03b92459e064a", "skills/build-landing-page/README.md": "46a280eadac8962180cf3f0c42eb0e845a3b0156a644569182d932881e1d51f8", diff --git a/scripts/sync.py b/scripts/sync.py index 6320550..dd6dac1 100644 --- a/scripts/sync.py +++ b/scripts/sync.py @@ -3,6 +3,7 @@ from __future__ import annotations import argparse +import base64 import gzip import hashlib import io @@ -12,6 +13,7 @@ import re import selectors import shutil +import struct import subprocess import tarfile import tempfile @@ -24,7 +26,7 @@ HOMEPAGE = "https://vllnt.com" DESCRIPTION = ("Evidence-led engineering workflows to plan, build, review, and ship software with AI agents. " "Part of the vllnt universe: open, sovereign tools for freedom by design (vllnt.com).") -CLAUDE_ICON = ROOT / "assets" / "claude-icon.svg" +CLAUDE_ICON = ROOT / "assets" / "vllnt-logo.png" HOSTS = ("claude", "codex", "cursor") PRINCIPLES = tuple(f"vllnt-{name}-principles" for name in ("thinking", "orchestration", "collaboration")) MAX_ARCHIVE = 32 * 1024 * 1024 @@ -217,6 +219,19 @@ def payload(files: dict[str, bytes]) -> tuple[dict[str, bytes], bytes]: return skills, rule.encode() +def icon_svg(png: bytes) -> bytes: + """Wrap a square PNG in the SVG file Claude's plugin directory looks for.""" + if png[:8] != b"\x89PNG\r\n\x1a\n": + raise Invalid("Claude icon must be a PNG") + width, height = struct.unpack(">II", png[16:24]) + if width != height or width < 128: + raise Invalid("Claude icon must be square and at least 128px") + data = base64.b64encode(png).decode() + return (f'\n').encode() + + def build(files: dict[str, bytes], lock: dict, version: str) -> dict[str, bytes]: if not re.fullmatch(r"\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?", version): raise Invalid("VERSION must contain a semantic version") @@ -235,7 +250,7 @@ def build(files: dict[str, bytes], lock: dict, version: str) -> dict[str, bytes] "cursor": ".cursor-plugin/plugin.json"}[host] package = {**skills, "LICENSE": files["LICENSE"], manifest_path: encoded(manifest)} if host == "claude": - package[".claude-plugin/icon.svg"] = CLAUDE_ICON.read_bytes() + package[".claude-plugin/icon.svg"] = icon_svg(CLAUDE_ICON.read_bytes()) if host == "cursor": package["rules/vstack-principles.mdc"] = rule package["SOURCE.json"] = encoded({"upstream": lock, "files": { diff --git a/tests/test_sync.py b/tests/test_sync.py index 081f8b3..0f36d2c 100644 --- a/tests/test_sync.py +++ b/tests/test_sync.py @@ -4,6 +4,7 @@ import io import json from pathlib import Path +import struct import subprocess import tarfile import tempfile @@ -54,6 +55,7 @@ def test_deterministic_standalone_packages_and_provenance(self): self.assertEqual("rules/vstack-principles.mdc" in package, host == "cursor") self.assertEqual(package["LICENSE"], files["LICENSE"]) self.assertEqual(".claude-plugin/icon.svg" in package, host == "claude") + self.assertIn(b"data:image/png;base64,", first["plugins/claude/.claude-plugin/icon.svg"]) claude = json.loads(first[".claude-plugin/marketplace.json"]) codex = json.loads(first[".agents/plugins/marketplace.json"]) cursor = json.loads(first[".cursor-plugin/marketplace.json"]) @@ -70,6 +72,13 @@ def test_deterministic_standalone_packages_and_provenance(self): self.assertNotIn("repository", manifests["codex"]) self.assertNotIn("repository", manifests["cursor"]) + def test_claude_icon_requires_square_png(self): + png = lambda w, h: b"\x89PNG\r\n\x1a\n" + b"\0\0\0\rIHDR" + struct.pack(">II", w, h) + self.assertIn(b'viewBox="0 0 256 256"', sync.icon_svg(png(256, 256))) + for bad in (png(256, 128), png(64, 64), b"" + bytes(32)): + with self.assertRaises(sync.Invalid): + sync.icon_svg(bad) + def test_added_and_removed_workflows(self): files = source() files["workflows/another-task/SKILL.md"] = skill("another-task")