{
  "name": "clientshare",
  "description": "Self-hosted static-site publishing and private file drives, built to be driven by agents.",
  "url": "https://share.benediktglass.com",
  "documentation": "https://share.benediktglass.com/llms.txt",
  "openapi": "https://share.benediktglass.com/openapi.json",
  "auth": {
    "type": "bearer",
    "instructions": "Create an API key in the cockpit at https://share.benediktglass.com/keys, or a scoped drive token at https://share.benediktglass.com/drive."
  },
  "capabilities": {
    "publish": "POST /_api/publish with a gzipped tar of a folder (optional ?ttl= for auto-expiry)",
    "manage": "REST under /_api/sites, /_api/trash, /_api/vars, /_api/kunden, /_api/keys, /_api/analytics, /_api/shortlinks (Bearer key, JSON) — list/create/update/delete sites, access, data, versions, analytics, short links. Archive a site (organisational, stays live): PATCH /_api/sites/<k>/<s> { archived:true|false }; list with ?archived=exclude|include|only (default exclude). Redirect mode for moved sites (ALL requests → HTTP 308): PATCH /_api/sites/<k>/<s> { redirect:\"https://target/\" }, remove with redirect:\"\".",
    "clients": "a client = a subdomain slug + a display name (slug IS <kunde>.<domain>). GET /_api/kunden[?q=<name>] to list/resolve (match by slug OR label — \"Danny Mittenzwey\" resolves to slug \"dm\"); POST /_api/kunden { kunde, label?, confirm? } to create; PUT /_api/kunden/<k> { label } to rename. Moving a site (PATCH /_api/sites/<k>/<s> { toKunde, toKundeLabel?, toSlug? }) to a not-yet-existing client creates it on the fly. BEFORE creating for a named client, resolve it first: creating/moving to a client whose name matches an existing one is rejected 409 kunde_exists_similar (with candidate) — reuse it, or pass confirm:true to force a new one.",
    "coldStart": "no key yet? POST /_api/auth/request-code then /_api/auth/verify-code (email code, allowlisted) → first key; then mint scoped keys and drive tokens over the API",
    "access": "per-site protection via PUT /_api/sites/<k>/<s>/access { mode: open|password|restricted, password?, emails? }. Password = in-app forward_auth gate (nice /_unlock page + signed cookie + rate-limit), NOT basic_auth. Access links open a protected site WITHOUT a password while it stays non-public: POST /_api/kunden/<k>/access-link { slug?, ttlDays? } → { url: /_access/link/<token> } (with slug = that one site, without = all of the client's protected sites; opens password- AND email-restricted sites); revocable per-site links are minted in the cockpit (Zugänge). Owner opens any protected site instantly via /_access/self/<k>/<s>.",
    "credentialIntake": "HOW TO GIVE AN AGENT A PASSWORD OR A CLIENT API KEY (secret handover, credential intake, entering credentials from a phone). In: client-level (default) POST /_api/kunden/<kunde>/intake or site-level POST /_api/sites/<kunde>/<slug>/intake → a write-only, login-free link the holder opens and types the secret into; values are stored AES-256-GCM encrypted. Out, option A (agent never sees it): reference ${VAR} in a proxy route, the server injects it into the upstream. Out, option B (agent must use the raw value): POST /_api/secrets/reveal { name, kunde?, slug?, burn?, reason? } — OWNER-LEVEL KEY ONLY (client-scoped key → 403), no public route, no browser path; resolves along the cascade site > kunde > account and returns { value, source, revealedAt, previousReveals }. Every reveal is audited: GET /_api/secrets/reveals?kunde=&name=&limit= (metadata only, never the value). burn:true deletes the value after handing it out (one-time handover; not the default because the same variable may still feed a live proxy route). FOLLOW-UP (\"I asked a client for a credential days ago, did they enter it?\", \"which credentials do I already have for client X?\", \"revoke an intake link\"): GET /_api/kunden/<k>/intake or /_api/sites/<k>/<s>/intake — filledAt=null means still open, and the AGE (now minus createdAt) is the trigger to chase it; DELETE …/intake/<id> revokes. A lost link cannot be re-read (the plaintext token exists only in the mint response, then it is stored hashed) — mint a new one for the same names and revoke the old. GET /_api/kunden/<k>/vars lists the credential NAMES already on file. Same view for the owner in the login-gated cockpit at /zugangsdaten (names + metadata only, never a value).",
    "variables": "cascade site > kunde > account for ${VAR} in proxy routes; /_api/vars, /_api/kunden/<k>/vars, /_api/sites/<k>/<s>/vars (values write-only)",
    "siteData": "per published site: GET|POST ./_data/<collection> (cursor pagination), GET|PATCH|DELETE ./_data/<collection>/<id>; per-action ACL (read/insert/update/delete each public|owner) via PUT /_api/sites/<k>/<s>/data/<collection> {access:{...}} enables a moderated wall (public read+insert, owner-only update+delete); owner bypasses all with Authorization: Bearer <key>",
    "proxy": "per published site: ./_proxy/<path> (server-side secret injection)",
    "drive": "multiple named drives via /_api/drives; scoped drvtok_ via /_api/drive/tokens; GET|PUT|DELETE|PATCH(move/batch) /_api/drive/files/<path>; GET|PUT|DELETE /_drive/<path>",
    "support": "POST /_api/support { message, subject?, email? }",
    "health": "GET /_api/health (owner key) → { ok, checks } for external monitors (e.g. Uptime Kuma); 200 when healthy, 503 when an invariant is violated (e.g. a deprecated legacy basic_auth gate was found)",
    "analytics": "log-based + cookie-free, included (not paywalled): GET /_api/sites/<k>/<s>/analytics?range= and account rollup GET /_api/analytics?range= → views, visitors (daily-unique salted IP+UA hash, no raw IP stored), crawlers (counted separately), 404s, daily series, top paths/sites, referrers, countries (offline GeoIP), devices, browsers, OS",
    "dashboards": "per-client curated dashboard + shared task board: PATCH /_api/sites/<k>/<s> {showOnDashboard}, /_api/kunden/<k>/dashboard(/links), /_api/kunden/<k>/tasks; public client view at /dash/<token> (client-visible fields only)",
    "search": "GET /_api/search?q=<term> — full-text search over published site CONTENT (scans .html/.md/.txt/.json in the shared data volume), not just slug/name like /_api/sites?q=. Returns [{kunde,slug,path,url,snippet,score}]. Owner key = all clients, scoped key = its namespace only.",
    "commandSurface": "a visual, mobile-first CEO command center for the owner (KOMMANDOZENTRALE-VISION.md): top shows \"wo stehen wir\" status of running work, below the decisions that need the CEO grouped by area (Organisation/Verkauf/Marketing/Vater-Stickerei/ClientShare/System/Privat), each with an Eisenhower marker (urgent/important) + a stage badge (richtung-offen|laeuft-autonom|freigabe-noetig = give direction / runs autonomously / approve before shipping) + clickable suggestions + a note field. Only real owner-only decisions are cards (escalation). PUT /_api/kommando sets the feed; a tap/note posts to PUBLIC token-gated POST /_api/kommando/eingang; the owner/orchestrator read choices via GET /_api/kommando. Login-free surface at /kommando/<token>.",
    "verifyUi": "HOW TO PROVE A COCKPIT PAGE BEHIND THE LOGIN REALLY RENDERS (page verification, \"does /zugangsdaten work\", \"check the UI\", \"test the login\"). Cockpit pages are session-gated, so curl on them returns 303 → /login, which proves nothing (an invented URL returns the same). Never log in to a live instance and never handle a human's password. Instead run the repository's end-to-end suite against a LOCAL dev server with a throwaway password: cd cockpit && cp .env.test.example .env.test && npm run test:e2e. It boots its own dev server, performs the REAL login (fills and submits the form), stores the session as Playwright storageState and asserts the rendered content of each page — at a 390px viewport as well as 1280px, including a no-horizontal-overflow assert (the cockpit is used mostly from a phone). New page? add a line to cockpit/tests/e2e/cockpit.spec.ts. Docs: /docs/tests. For checking DATA rather than rendering, use /_api with a Bearer key — no browser needed.",
    "compat": "here.now-style /api/v1/publishes and /api/v1/publish[/:slug] aliases (default kunde from key scope)"
  },
  "skill": "https://share.benediktglass.com/publish.sh"
}