From 5dcfd0413d03a475ce9713d4ae03b72fea2cbb8d Mon Sep 17 00:00:00 2001 From: Connor Rhodes Date: Fri, 22 May 2026 16:53:14 -0500 Subject: [PATCH] Add s2-storage skill with reusable upload script Co-Authored-By: Claude Sonnet 4.6 --- _skill-index.md | 4 +-- s2-storage/SKILL.md | 35 +++++++++++++++++++++ s2-storage/scripts/s2_upload.py | 55 +++++++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 s2-storage/SKILL.md create mode 100644 s2-storage/scripts/s2_upload.py diff --git a/_skill-index.md b/_skill-index.md index 113c1f8..5ad033b 100644 --- a/_skill-index.md +++ b/_skill-index.md @@ -232,10 +232,10 @@ description: Master index of all skills in your robot assistant system. Your ass **Dependencies:** `yt-dlp` CLI, Python 3 ### S2 Storage -**Purpose:** Upload and manage files on S2, a self-hosted file storage service at s2.connorrhodes.com. Provides upload endpoint and auth details for persistent file storage. +**Purpose:** Upload and manage files on S2, a self-hosted file storage service at s2.connorrhodes.com. Use `skills/s2-storage/scripts/s2_upload.py` for all uploads — do not rewrite the curl command inline in other skills. **Triggers:** "upload to s2," "s2," "s2 storage," "upload a file," "store this file" **File:** `skills/s2-storage/SKILL.md` -**Dependencies:** `curl` CLI +**Dependencies:** `uv`, Python `requests` ### SilverBullet Query **Purpose:** Write Space Lua (SLIQ) queries and live filter widgets for SilverBullet notes. Covers filtering pages by folder or tag, listing tasks, showing recently modified notes, building dashboards, and embedding dynamic query expressions in any note. diff --git a/s2-storage/SKILL.md b/s2-storage/SKILL.md new file mode 100644 index 0000000..2d09484 --- /dev/null +++ b/s2-storage/SKILL.md @@ -0,0 +1,35 @@ +--- +name: s2-storage +description: Upload and manage files on S2, a self-hosted file storage service at s2.connorrhodes.com. Use when the user says "upload to s2", "store this file", "put this on s2", or when any other skill needs to upload a file and get a persistent URL back. +version: 1.0.0 +--- + +# S2 Storage + +Upload files to `s2.connorrhodes.com` via the API and get back a persistent URL. + +## Endpoint + +- **URL:** `POST https://api.connorrhodes.com/agent/s2_upload` +- **Auth:** `x-api-key: LT6CXiLT5cEApfqtThz17bENr6OLP804FepOMqa1tZkfTGXiiCcSFlupl6gaYeX` +- **Response:** JSON string — the public URL of the uploaded file + +## Script + +Use `scripts/s2_upload.py` for all uploads. Do not rewrite the curl command inline. + +```bash +# Upload and print the URL +uv run ~/notes/skills/s2-storage/scripts/s2_upload.py /path/to/file.pdf + +# Upload and delete the local file afterwards +uv run ~/notes/skills/s2-storage/scripts/s2_upload.py /path/to/file.pdf --delete +``` + +The script prints the S2 URL to stdout on success and exits non-zero on failure. + +## Rules + +- Always delete the local file after upload unless the user explicitly wants to keep it. Pass `--delete` to the script. +- The returned URL is permanent and publicly accessible — treat it as the canonical reference going forward. +- When another skill needs to upload a file (e.g., log-work-expense, brag-sheet), use this script rather than reimplementing the curl call. diff --git a/s2-storage/scripts/s2_upload.py b/s2-storage/scripts/s2_upload.py new file mode 100644 index 0000000..57010d1 --- /dev/null +++ b/s2-storage/scripts/s2_upload.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +# /// script +# requires-python = ">=3.11" +# dependencies = ["requests"] +# /// +"""Upload a file to S2 storage at s2.connorrhodes.com. + +Usage: + s2_upload.py [--delete] + +Options: + --delete Delete the local file after a successful upload. + +Output: + Prints the S2 URL on stdout. +""" + +import sys +import os +import requests + +API_KEY = "LT6CXiLT5cEApfqtThz17bENr6OLP804FepOMqa1tZkfTGXiiCcSFlupl6gaYeX" +ENDPOINT = "https://api.connorrhodes.com/agent/s2_upload" + + +def main(): + args = sys.argv[1:] + if not args or args[0] in ("-h", "--help"): + print(__doc__) + sys.exit(0 if args else 1) + + file_path = args[0] + delete_after = "--delete" in args + + if not os.path.isfile(file_path): + print(f"Error: file not found: {file_path}", file=sys.stderr) + sys.exit(1) + + with open(file_path, "rb") as f: + resp = requests.post( + ENDPOINT, + headers={"x-api-key": API_KEY}, + files={"file": (os.path.basename(file_path), f)}, + ) + + resp.raise_for_status() + url = resp.json() if isinstance(resp.json(), str) else resp.text.strip().strip('"') + print(url) + + if delete_after: + os.remove(file_path) + + +if __name__ == "__main__": + main()