* refactor(skills): shipped-set slim — 15 skills to optional, github six-way merge, pdf absorbs OCR+nano-pdf, channel-gated teams pipeline
Maintainer-directed shipped-skills curation (skills index 1,900 -> ~1,400
tok/call on desktop; every session pays the index, so this is a per-call
diet on all installs):
- optional-skills moves (installable via skills hub, history preserved):
creative comfyui/ascii-art/excalidraw/pretext/sketch/touchdesigner-mcp;
ALL of mlops (huggingface-hub, llama-cpp, serving-llms-vllm,
weights-and-biases, evaluating-llms-harness — subcategory structure
kept); research-paper-writing (55 supporting files, 17.3K-tok load);
openhue; blogwatcher (first taught the cronjob monitor-field watch
pattern + web_extract instead of pre-cron manual workflows)
- DELETED session-librarian (Aug-12 'inspired by Perplexity Computer'
port, never maintainer-intended; session_search covers discovery)
- github: six skills (auth, issues, pr-workflow, issue-to-pr,
code-review, repo-management) merged into ONE software-development/
github skill — routing body + complete per-workflow references;
benbarclay authorship credited; codebase-inspection rides along;
discipline pins from test_github_issue_to_pr_skill.py preserved
against the reference body in the new test_github_skill.py
- pdf absorbs ocr-and-documents + nano-pdf as references/ + scripts
(extract_pymupdf, extract_marker converted to the argparse house
standard its contract test enforces)
- NEW session_platforms frontmatter gate (metadata.hermes): hides a
skill from the index on gateway channels it is not for; fail-open on
unknown platform; teams-meeting-pipeline gated to [teams, cron]
- blocked-page-recovery: research -> new web category; trigger-first
description ('Use when a fetch fails: 403/429, paywall, WAF, bot
wall.') so the model actually reaches for it on blocked fetches
- docs regenerated via generate-skill-docs.py (195 pages); related_skills
swept repo-wide; tests: 1672 passed (2 openclaw failures pre-existing
on clean main, Windows-local)
* chore: ignore .skills_prompt_snapshot.json (local index cache, accidentally committed)
134 lines
3.7 KiB
Python
134 lines
3.7 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Upload an .excalidraw file to excalidraw.com and print a shareable URL.
|
|
|
|
No account required. The diagram is encrypted client-side (AES-GCM) before
|
|
upload -- the encryption key is embedded in the URL fragment, so the server
|
|
never sees plaintext.
|
|
|
|
Requirements:
|
|
pip install cryptography
|
|
|
|
Usage:
|
|
python upload.py <path-to-file.excalidraw>
|
|
|
|
Example:
|
|
python upload.py ~/diagrams/architecture.excalidraw
|
|
# prints: https://excalidraw.com/#json=abc123,encryptionKeyHere
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import struct
|
|
import sys
|
|
import zlib
|
|
import base64
|
|
import urllib.request
|
|
|
|
try:
|
|
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
except ImportError:
|
|
print("Error: 'cryptography' package is required for upload.")
|
|
print("Install it with: pip install cryptography")
|
|
sys.exit(1)
|
|
|
|
# Excalidraw public upload endpoint (no auth needed)
|
|
UPLOAD_URL = "https://json.excalidraw.com/api/v2/post/"
|
|
|
|
|
|
def concat_buffers(*buffers: bytes) -> bytes:
|
|
"""
|
|
Build the Excalidraw v2 concat-buffers binary format.
|
|
|
|
Layout: [version=1 (4B big-endian)] then for each buffer:
|
|
[length (4B big-endian)] [data bytes]
|
|
"""
|
|
parts = [struct.pack(">I", 1)] # version = 1
|
|
for buf in buffers:
|
|
parts.append(struct.pack(">I", len(buf)))
|
|
parts.append(buf)
|
|
return b"".join(parts)
|
|
|
|
|
|
def upload(excalidraw_json: str) -> str:
|
|
"""
|
|
Encrypt and upload Excalidraw JSON to excalidraw.com.
|
|
|
|
Args:
|
|
excalidraw_json: The full .excalidraw file content as a string.
|
|
|
|
Returns:
|
|
Shareable URL string.
|
|
"""
|
|
# 1. Inner payload: concat_buffers(file_metadata, data)
|
|
file_metadata = json.dumps({}).encode("utf-8")
|
|
data_bytes = excalidraw_json.encode("utf-8")
|
|
inner_payload = concat_buffers(file_metadata, data_bytes)
|
|
|
|
# 2. Compress with zlib
|
|
compressed = zlib.compress(inner_payload)
|
|
|
|
# 3. AES-GCM 128-bit encrypt
|
|
raw_key = os.urandom(16) # 128-bit key
|
|
iv = os.urandom(12) # 12-byte nonce
|
|
aesgcm = AESGCM(raw_key)
|
|
encrypted = aesgcm.encrypt(iv, compressed, None)
|
|
|
|
# 4. Encoding metadata
|
|
encoding_meta = json.dumps({
|
|
"version": 2,
|
|
"compression": "pako@1",
|
|
"encryption": "AES-GCM",
|
|
}).encode("utf-8")
|
|
|
|
# 5. Outer payload: concat_buffers(encoding_meta, iv, encrypted)
|
|
payload = concat_buffers(encoding_meta, iv, encrypted)
|
|
|
|
# 6. Upload
|
|
req = urllib.request.Request(UPLOAD_URL, data=payload, method="POST")
|
|
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
if resp.status != 200:
|
|
raise RuntimeError(f"Upload failed with HTTP {resp.status}")
|
|
result = json.loads(resp.read().decode("utf-8"))
|
|
|
|
file_id = result.get("id")
|
|
if not file_id:
|
|
raise RuntimeError(f"Upload returned no file ID. Response: {result}")
|
|
|
|
# 7. Key as base64url (JWK 'k' format, no padding)
|
|
key_b64 = base64.urlsafe_b64encode(raw_key).rstrip(b"=").decode("ascii")
|
|
|
|
return f"https://excalidraw.com/#json={file_id},{key_b64}"
|
|
|
|
|
|
def main():
|
|
if len(sys.argv) < 2:
|
|
print("Usage: python upload.py <path-to-file.excalidraw>")
|
|
sys.exit(1)
|
|
|
|
file_path = sys.argv[1]
|
|
|
|
if not os.path.isfile(file_path):
|
|
print(f"Error: File not found: {file_path}")
|
|
sys.exit(1)
|
|
|
|
with open(file_path, "r", encoding="utf-8") as f:
|
|
content = f.read()
|
|
|
|
# Basic validation: should be valid JSON with an "elements" key
|
|
try:
|
|
doc = json.loads(content)
|
|
except json.JSONDecodeError as e:
|
|
print(f"Error: File is not valid JSON: {e}")
|
|
sys.exit(1)
|
|
|
|
if "elements" not in doc:
|
|
print("Warning: File does not contain an 'elements' key. Uploading anyway.")
|
|
|
|
url = upload(content)
|
|
print(url)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|