{"repo":"NousResearch/hermes-agent","public_id":"378","display_label":"Add here.now static web publishing skill","summary":"These members all concern adding a here.now skill to enable instant static web publishing of agent-created content. The work appears across a feature request and at least two implementation or proposal pull requests.","subsystem":"skills","counts":{"members":3,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":1},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":18170,"merged_at":"2026-05-01T02:48:15+00:00"},"resolution":{"state":"resolved","by":"pr-18170"},"freshness":{"state":"stale","last_verified_at":"2026-05-01T02:48:15+00:00","is_stale":true},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.6666666666666666},"fix_edges":[{"issue_number":378,"pr_number":18170,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-05-01T02:48:15+00:00"}],"members":[{"kind":"issue","number":378,"title":"Feature: here.now Skill — Instant Static Web Publishing for Agent-Created Content","url":null,"state":"closed"},{"kind":"pr","number":14803,"title":"Add built-in here.now skill","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"f7dfd4ae3","number":0},{"kind":"commit","value":"21cc9c8d3","number":0},{"kind":"pr","value":"#18170","number":18170}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":18170,"title":"feat(skills): add here.now as an optional skill","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["optional-skills/productivity/here-now/SKILL.md","optional-skills/productivity/here-now/scripts/drive.sh","optional-skills/productivity/here-now/scripts/publish.sh","scripts/release.py"],"fix_hunks":[{"file":"optional-skills/productivity/here-now/SKILL.md","hunks":["@@ -0,0 +1,217 @@\n+---\n+name: here.now\n+description: Publish static sites to {slug}.here.now and store private files in cloud Drives for agent-to-agent handoff.\n+version: 1.15.3\n+author: here.now\n+license: MIT\n+prerequisites:\n+  commands: [curl, file, jq]\n+platforms: [macos, linux]\n+metadata:\n+  hermes:\n+    tags: [here.now, herenow, publish, deploy, hosting, static-site, web, share, URL, drive, storage]\n+    homepage: https://here.now\n+    requires_toolsets: [terminal]\n+---\n+\n+# here.now\n+\n+here.now lets agents publish websites and store private files in cloud Drives.\n+\n+Use here.now for two jobs:\n+\n+- **Sites**: publish websites and files at `{slug}.here.now`.\n+- **Drives**: store private agent files in cloud folders.\n+\n+## Current docs\n+\n+**Before answering questions about here.now capabilities, features, or workflows, read the current docs:**\n+\n+→ **https://here.now/docs**\n+\n+Read the docs:\n+\n+- at the first here.now-related interaction in a conversation\n+- any time the user asks how to do something\n+- any time the user asks what is possible, supported, or recommended\n+- before telling the user a feature is unsupported\n+\n+Topics that require current docs (do not rely on local skill text alone):\n+\n+- Drives and Drive sharing\n+- custom domains\n+- payments and payment gating\n+- forking\n+- proxy routes and service variables\n+- handles and links\n+- limits and quotas\n+- SPA routing\n+- error handling and remediation\n+- feature availability\n+\n+**If docs and live API behavior disagree, trust the live API behavior.**\n+\n+If the docs fetch fails or times out, continue with the local skill and live API/script output. Prefer live API behavior for active operations.\n+\n+## Requirements\n+\n+- Required binaries: `curl`, `file`, `jq`\n+- Optional environment variable: `$HERENOW_API_KEY`\n+- Optional Drive token variable: `$HERENOW_DRIVE_TOKEN`\n+- Optional credentials file: `~/.herenow/credentials`\n+- Skill helper paths:\n+  - `${HERMES_SKILL_DIR}/scripts/publish.sh` for publishing sites\n+  - `${HERMES_SKILL_DIR}/scripts/drive.sh` for private Drive storage\n+\n+## Create a site\n+\n+```bash\n+PUBLISH=\"${HERMES_SKILL_DIR}/scripts/publish.sh\"\n+bash \"$PUBLISH\" {file-or-dir} --client hermes\n+```\n+\n+Outputs the live URL (e.g. `https://bright-canvas-a7k2.here.now/`).\n+\n+Under the hood this is a three-step flow: create/update -> upload files -> finalize. A site is not live until finalize succeeds.\n+\n+Without an API key this creates an **anonymous site** that expires in 24 hours.\n+With a saved API key, the site is permanent.\n+\n+**File structure:** For HTML sites, place `index.html` at the root of the directory you publish, not inside a subdirectory. The directory's contents become the site root. For example, publish `my-site/` where `my-site/index.html` exists — don't publish a parent folder that contains `my-site/`.\n+\n+You can also publish raw files without any HTML. Single files get a rich auto-viewer (images, PDF, video, audio). Multiple files get an auto-generated directory listing with folder navigation and an image gallery.\n+\n+## Update an existing site\n+\n+```bash\n+PUBLISH=\"${HERMES_SKILL_DIR}/scripts/publish.sh\"\n+bash \"$PUBLISH\" {file-or-dir} --slug {slug} --client hermes\n+```\n+\n+The script auto-loads the `claimToken` from `.herenow/state.json` when updating anonymous sites. Pass `--claim-token {token}` to override.\n+\n+Authenticated updates require a saved API key.\n+\n+## Use a Drive\n+\n+Use a Drive when the user wants private cloud storage for agent files: documents, context, memory, plans, assets, media, research, code, and anything else that should persist without being published as a website.\n+\n+Every signed-in account has a default Drive named `My Drive`.\n+\n+```bash\n+DRIVE=\"${HERMES_SKILL_DIR}/scripts/drive.sh\"\n+bash \"$DRIVE\" default\n+bash \"$DRIVE\" ls \"My Drive\"\n+bash \"$DRIVE\" put \"My Drive\" notes/today.md --from ./notes/today.md\n+bash \"$DRIVE\" cat \"My Drive\" notes/today.md\n+bash \"$DRIVE\" share \"My Drive\" --perms write --prefix notes/ --ttl 7d\n+```\n+\n+Use scoped Drive tokens for agent-to-agent handoff. If you receive a `herenow_drive` share block, use its `token` as `Authorization: Bearer <token>` against `api_base`, respect `pathPrefix` when present, and preserve ETags on writes. A `pathPrefix` of `null` means full-Drive access. If the skill is available, prefer `drive.sh`; otherwise call the listed API operations directly.\n+\n+## API key storage\n+\n+The publish script reads the API key from these sources (first match wins):\n+\n+1. `--api-key {key}` flag (CI/scripting only — avoid in interactive use)\n+2. `$HERENOW_API_KEY` environment variable\n+3. `~/.herenow/credentials` file (recommended for agents)\n+\n+To store a key, write it to the credentials file:\n+\n+```bash\n+mkdir -p ~/.herenow && echo \"{API_KEY}\" > ~/.herenow/credentials && chmod 600 ~/.herenow/credentials\n+```\n+\n+**IMPORTANT**: After receiving an API key, save it immediately — run the command above yourself. Do not ask the user to run it manually. Avoid passing the key via CLI flags (e.g. `--api-key`) in interactive sessions; the credentials file is the preferred storage method.\n+\n+Never commit credentials or local state files (`~/.herenow/credentials`, `.herenow/state.json`) to source control.\n+\n+## Getting an API key\n+\n+To upgrade from anonymous (24h) to permanent sites:\n+\n+1. Ask the user for their email address.\n+2. Request a one-time sign-in code:\n+\n+```bash\n+curl -sS https://here.now/api/auth/agent/request-code \\\n+  -H \"content-type: application/json\" \\\n+  -d '{\"email\": \"[redacted:email]\"}'\n+```\n+\n+3. Tell the user: \"Check your inbox for a sign-in code from here.now and paste it here.\"\n+4. Verify the code and get the API key:\n+\n+```bash\n+curl -sS https://here.now/api/auth/agent/verify-code \\\n+  -H \"content-type: application/json\" \\\n+  -d '{\"email\":\"[redacted:email]\",\"code\":\"ABCD-2345\"}'\n+```\n+\n+5. Save the returned `apiKey` yourself (do not ask the user to do this):\n+\n+```bash\n+mkdir -p ~/.herenow && echo \"{API_KEY}\" > ~/.herenow/credentials && chmod 600 ~/.herenow/credentials\n+```\n+\n+## State file\n+\n+After every site create/update, the script writes to `.herenow/state.json` in the working directory:\n+\n+```json\n+{\n+  \"publishes\": {\n+    \"bright-canvas-a7k2\": {\n+      \"siteUrl\": \"https://bright-canvas-a7k2.here.now/\",\n+      \"claimToken\": \"[redacted:secret-assignment]\",\n+      \"claimUrl\": \"https://here.now/claim?slug=bright-canvas-a7k2&token=[redacted:secret-assignment]\",\n+      \"expiresAt\": \"2026-02-18T01:00:00.000Z\"\n+    }\n+  }\n+}\n+```\n+\n+Before creating or updating sites, you may check this file to find prior slugs.\n+Treat `.herenow/state.json` as internal cache only.\n+Never present this local file path as a URL, and never use it as source of truth for auth mode, expiry, or claim URL.\n+\n+## What to tell the user\n+\n+For published sites:\n+\n+- Always share the `siteUrl` from the current script run.\n+- Read and follow `publish_result.*` lines from script stderr to determine auth mode.\n+- When `publish_result.auth_mode=authenticated`: tell the user the site is **permanent** and saved to their account. No claim URL is needed.\n+- When `publish_result.auth_mode=anonymous`: tell the user the site **expires in 24 hours**. Share the claim URL (if `publish_result.claim_url` is non-empty and starts with `https://`) so they can keep it permanently. Warn that claim tokens are only returned once and cannot be recovered.\n+- Never tell the user to inspect `.herenow/state.json` for claim URLs or auth status.\n+\n+For Drives:\n+\n+- Do not describe Drive files as public URLs.\n+- Tell the user Drive contents are private unless shared with a scoped token.\n+- When sharing access with another agent, prefer a scoped token with a narrow `pathPrefix` and short TTL.\n+\n+## publish.sh options\n+\n+| Flag                   | Description                                  |\n+| ---------------------- | -------------------------------------------- |\n+| `--slug {slug}`        | Update an existing site instead of creating |\n+| `--claim-token [redacted:secret-assignment] Override claim token for anonymous updates    |\n+| `--title {text}`       | Viewer title (non-HTML sites)             |\n+| `--description {text}` | Viewer description                            |\n+| `--ttl {seconds}`      | Set expiry (authenticated only)               |\n+| `--client {name}`      | Agent name for attribution (e.g. `hermes`)    |\n+| `--base-url {url}`     | API base URL (default: `https://here.now`)    |\n+| `--allow-nonherenow-base-url` | Allow sending auth to non-default `--base-url` |\n+| `--api-key {key}`      | API key override (prefer credentials file)    |\n+| `--spa`                | Enable SPA routing (serve index.html for unknown paths) |\n+| `--forkable`           | Allow others to fork this site                           |\n+\n+## Beyond publish.sh\n+\n+For Drive operations, use `drive.sh` or the Drive API. For broader account and site management — delete, metadata, passwords, payments, domains, handles, links, variables, proxy routes, forking, duplication, and more — see the current docs:\n+\n+→ **https://here.now/docs**\n+\n+Full docs: https://here.now/docs"]},{"file":"optional-skills/productivity/here-now/scripts/drive.sh","hunks":["@@ -0,0 +1,406 @@\n+#!/usr/bin/env bash\n+set -euo pipefail\n+\n+BASE_URL=\"https://here.now\"\n+CREDENTIALS_FILE=\"[redacted:secret-assignment]\"\n+API_KEY=\"[redacted:secret-assignment]\"\n+DRIVE_TOKEN=\"[redacted:secret-assignment]\"\n+ALLOW_NON_HERENOW_BASE_URL=0\n+MAX_FILE_BYTES=$((500 * 1024 * 1024))\n+\n+usage() {\n+  cat <<'USAGE'\n+Usage: drive.sh [global options] <command> [args]\n+\n+Global options:\n+  --api-key <key>        Account API key (or $HERENOW_API_KEY / ~/.herenow/credentials)\n+  --token [redacted:secret-assignment] Drive token (or $HERENOW_DRIVE_TOKEN)\n+  --base-url <url>       API base (default: https://here.now)\n+  --allow-nonherenow-base-url\n+\n+Commands:\n+  create [name] [--default]\n+  default\n+  ls\n+  ls <drive> [prefix]\n+  cat <drive> <path>\n+  put <drive> <path> --from <local-file>\n+  import <drive> <prefix> --from <local-folder> [--dry-run]\n+  export <drive> <prefix> --to <local-folder> [--dry-run]\n+  rm <drive> <path> [--recursive --confirm <path>]\n+  share <drive> --perms read|write [--prefix notes/] [--ttl 30d] [--label text] [--manage-tokens]\n+  tokens <drive>\n+  revoke <drive> <tokenId>\n+  delete <drive> --confirm \"<drive name>\"\n+USAGE\n+  exit 1\n+}\n+\n+die() { echo \"error: $1\" >&2; exit 1; }\n+\n+SCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\n+SKILL_DIR=\"$(cd \"${SCRIPT_DIR}/..\" && pwd)\"\n+BUNDLED_JQ=\"${SKILL_DIR}/bin/jq\"\n+\n+if [[ -x \"$BUNDLED_JQ\" ]]; then\n+  JQ_BIN=\"$BUNDLED_JQ\"\n+elif command -v jq >/dev/null 2>&1; then\n+  JQ_BIN=\"$(command -v jq)\"\n+else\n+  die \"requires jq\"\n+fi\n+\n+for cmd in curl file; do\n+  command -v \"$cmd\" >/dev/null 2>&1 || die \"requires $cmd\"\n+done\n+\n+while [[ $# -gt 0 ]]; do\n+  case \"$1\" in\n+    --api-key) API_KEY=\"$2\"; shift 2 ;;\n+    --token) DRIVE_TOKEN=\"$2\"; shift 2 ;;\n+    --base-url) BASE_URL=\"$2\"; shift 2 ;;\n+    --allow-nonherenow-base-url) ALLOW_NON_HERENOW_BASE_URL=1; shift ;;\n+    --help|-h) usage ;;\n+    --*) die \"unknown global option: $1\" ;;\n+    *) break ;;\n+  esac\n+done\n+\n+CMD=\"${1:-}\"\n+[[ -n \"$CMD\" ]] || usage\n+shift || true\n+\n+if [[ -z \"$API_KEY\" && -z \"$DRIVE_TOKEN\" && -f \"$CREDENTIALS_FILE\" ]]; then\n+  API_KEY=$(tr -d '[:space:]' < \"$CREDENTIALS_FILE\")\n+fi\n+\n+BASE_URL=\"${BASE_URL%/}\"\n+if [[ \"$BASE_URL\" != \"https://here.now\" && \"$ALLOW_NON_HERENOW_BASE_URL\" -ne 1 ]]; then\n+  if [[ -n \"$API_KEY\" || -n \"$DRIVE_TOKEN\" ]]; then\n+    die \"refusing to send credentials to non-default base URL; pass --allow-nonherenow-base-url to override\"\n+  fi\n+fi\n+\n+auth_header=()\n+if [[ -n \"$DRIVE_TOKEN\" ]]; then\n+  auth_header=(-H \"authorization: Bearer $DRIVE_TOKEN\")\n+elif [[ -n \"$API_KEY\" ]]; then\n+  auth_header=(-H \"authorization: Bearer $API_KEY\")\n+else\n+  die \"missing credentials; set HERENOW_API_KEY, HERENOW_DRIVE_TOKEN, or ~/.herenow/credentials\"\n+fi\n+\n+compute_sha256() {\n+  local f=\"$1\"\n+  if command -v sha256sum >/dev/null 2>&1; then\n+    sha256sum \"$f\" | cut -d' ' -f1\n+  else\n+    shasum -a 256 \"$f\" | cut -d' ' -f1\n+  fi\n+}\n+\n+guess_content_type() {\n+  local f=\"$1\"\n+  case \"${f##*.}\" in\n+    html|htm) echo \"text/html; charset=utf-8\" ;;\n+    css) echo \"text/css; charset=utf-8\" ;;\n+    js|mjs) echo \"text/javascript; charset=utf-8\" ;;\n+    json) echo \"application/json; charset=utf-8\" ;;\n+    md|txt) echo \"text/plain; charset=utf-8\" ;;\n+    svg) echo \"image/svg+xml\" ;;\n+    png) echo \"image/png\" ;;\n+    jpg|jpeg) echo \"image/jpeg\" ;;\n+    gif) echo \"image/gif\" ;;\n+    webp) echo \"image/webp\" ;;\n+    pdf) echo \"application/pdf\" ;;\n+    *) file --brief --mime-type \"$f\" 2>/dev/null || echo \"application/octet-stream\" ;;\n+  esac\n+}\n+\n+api_json() {\n+  local method=\"$1\"; shift\n+  local url=\"$1\"; shift\n+  local body=\"${1:-}\"\n+  local tmp\n+  tmp=$(mktemp)\n+  local code\n+  if [[ -n \"$body\" ]]; then\n+    code=$(curl -sS -o \"$tmp\" -w \"%{http_code}\" -X \"$method\" \"$url\" \"${auth_header[@]}\" -H \"content-type: application/json\" -d \"$body\")\n+  else\n+    code=$(curl -sS -o \"$tmp\" -w \"%{http_code}\" -X \"$method\" \"$url\" \"${auth_header[@]}\")\n+  fi\n+  if [[ \"$code\" -lt 200 || \"$code\" -ge 300 ]]; then\n+    local err\n+    err=$(\"$JQ_BIN\" -r '.error // empty' \"$tmp\" 2>/dev/null || true)\n+    [[ -n \"$err\" ]] || err=\"$(cat \"$tmp\")\"\n+    rm -f \"$tmp\"\n+    die \"HTTP $code: $err\"\n+  fi\n+  cat \"$tmp\"\n+  rm -f \"$tmp\"\n+}\n+\n+urlenc() {\n+  \"$JQ_BIN\" -nr --arg v \"$1\" '$v|@uri'\n+}\n+\n+urlenc_path() {\n+  local path=\"$1\"\n+  local out=\"\"\n+  local part\n+  IFS='/' read -r -a parts <<< \"$path\"\n+  for part in \"${parts[@]}\"; do\n+    [[ -n \"$out\" ]] && out=\"$out/\"\n+    out=\"$out$(urlenc \"$part\")\"\n+  done\n+  echo \"$out\"\n+}\n+\n+resolve_drive() {\n+  local name=\"$1\"\n+  if [[ \"$name\" == drv_* ]]; then\n+    echo \"$name\"\n+    return\n+  fi\n+  if [[ -n \"$DRIVE_TOKEN\" ]]; then\n+    die \"drive tokens must reference drives by drv_ id; use account credentials to resolve drive names\"\n+  fi\n+  if [[ \"$name\" == \"default\" || \"$name\" == \"my-drive\" || \"$name\" == \"My Drive\" ]]; then\n+    api_json GET \"$BASE_URL/api/v1/drives/default\" | \"$JQ_BIN\" -r '.drive.id'\n+    return\n+  fi\n+  local rows count\n+  rows=$(api_json GET \"$BASE_URL/api/v1/drives\" | \"$JQ_BIN\" --arg n \"$name\" '[.drives[] | select(.name == $n)]')\n+  count=$(echo \"$rows\" | \"$JQ_BIN\" 'length')\n+  [[ \"$count\" -eq 1 ]] || die \"drive name '$name' matched $count drives; use a drv_ id\"\n+  echo \"$rows\" | \"$JQ_BIN\" -r '.[0].id'\n+}\n+\n+drive_head() {\n+  local id=\"$1\"\n+  api_json GET \"$BASE_URL/api/v1/drives/$id\" | \"$JQ_BIN\" -r '.drive.headVersionId // .headVersionId // empty'\n+}\n+\n+file_meta() {\n+  local id=\"$1\"\n+  local path=\"$2\"\n+  local prefix\n+  prefix=$(urlenc \"$path\")\n+  api_json GET \"$BASE_URL/api/v1/drives/$id/files?prefix=$prefix&limit=200\" | \"$JQ_BIN\" -c --arg p \"$path\" '.files[]? | select(.path == $p)' | head -n 1\n+}\n+\n+put_file() {\n+  local drive=\"$1\"; shift\n+  local path=\"$1\"; shift\n+  local local_file=\"\"\n+  while [[ $# -gt 0 ]]; do\n+    case \"$1\" in\n+      --from) local_file=\"$2\"; shift 2 ;;\n+      *) die \"unexpected put argument: $1\" ;;\n+    esac\n+  done\n+  [[ -f \"$local_file\" ]] || die \"--from must be a file\"\n+  local id sz ct sha meta body upload upload_url upload_id http_code\n+  id=$(resolve_drive \"$drive\")\n+  sz=$(wc -c < \"$local_file\" | tr -d ' ')\n+  [[ \"$sz\" -le \"$MAX_FILE_BYTES\" ]] || die \"$path exceeds the $MAX_FILE_BYTES byte Drive file limit\"\n+  ct=$(guess_content_type \"$local_file\")\n+  sha=$(compute_sha256 \"$local_file\")\n+  meta=$(file_meta \"$id\" \"$path\" || true)\n+  body=$(\"$JQ_BIN\" -n --arg p \"$path\" --argjson s \"$sz\" --arg c \"$ct\" --arg sha \"$sha\" \\\n+    '{path:$p,size:$s,contentType:$c,sha256:$sha}')\n+  if [[ -n \"$meta\" ]]; then\n+    etag=$(echo \"$meta\" | \"$JQ_BIN\" -r '.etag')\n+    body=$(echo \"$body\" | \"$JQ_BIN\" --arg e \"$etag\" '.ifMatch = $e')\n+  else\n+    body=$(echo \"$body\" | \"$JQ_BIN\" '.ifNoneMatch = \"*\"')\n+  fi\n+  upload=$(api_json POST \"$BASE_URL/api/v1/drives/$id/files/uploads\" \"$body\")\n+  upload_url=$(echo \"$upload\" | \"$JQ_BIN\" -r '.uploadUrl')\n+  upload_id=$(echo \"$upload\" | \"$JQ_BIN\" -r '.uploadId')\n+  http_code=$(curl -sS -o /dev/null -w \"%{http_code}\" -X PUT \"$upload_url\" -H \"Content-Type: $ct\" --data-binary \"@$local_file\")\n+  [[ \"$http_code\" -ge 200 && \"$http_code\" -lt 300 ]] || die \"upload failed for $path (HTTP $http_code)\"\n+  api_json POST \"$BASE_URL/api/v1/drives/$id/files/finalize\" \"$(\"$JQ_BIN\" -n --arg u \"$upload_id\" '{uploadId:$u}')\" | \"$JQ_BIN\" .\n+}\n+\n+case \"$CMD\" in\n+  create)\n+    name=\"\"\n+    is_default=\"false\"\n+    while [[ $# -gt 0 ]]; do\n+      case \"$1\" in\n+        --default) is_default=\"true\"; shift ;;\n+        *) [[ -z \"$name\" ]] && name=\"$1\" || die \"unexpected argument: $1\"; shift ;;\n+      esac\n+    done\n+    body=$(\"$JQ_BIN\" -n --arg n \"$name\" --argjson d \"$is_default\" '{isDefault:$d} + (if $n == \"\" then {} else {name:$n} end)')\n+    api_json POST \"$BASE_URL/api/v1/drives\" \"$body\" | \"$JQ_BIN\" .\n+    ;;\n+  default)\n+    api_json GET \"$BASE_URL/api/v1/drives/default\" | \"$JQ_BIN\" .\n+    ;;\n+  ls)\n+    if [[ $# -eq 0 ]]; then\n+      [[ -z \"$DRIVE_TOKEN\" ]] || die \"drive tokens cannot list drives; pass a drv_ id\"\n+      api_json GET \"$BASE_URL/api/v1/drives\" | \"$JQ_BIN\" .\n+    else\n+      id=$(resolve_drive \"$1\")\n+      prefix=\"${2:-}\"\n+      api_json GET \"$BASE_URL/api/v1/drives/$id/files?prefix=$(urlenc \"$prefix\")\" | \"$JQ_BIN\" .\n+    fi\n+    ;;\n+  cat)\n+    [[ $# -eq 2 ]] || die \"usage: drive.sh cat <drive> <path>\"\n+    id=$(resolve_drive \"$1\")\n+    curl -fsS \"$BASE_URL/api/v1/drives/$id/files/$(urlenc_path \"$2\")\" \"${auth_header[@]}\"\n+    ;;\n+  put)\n+    [[ $# -ge 2 ]] || die \"usage: drive.sh put <drive> <path> --from <local-file>\"\n+    put_file \"$@\"\n+    ;;\n+  import)\n+    [[ $# -ge 2 ]] || die \"usage: drive.sh import <drive> <prefix> --from <local-folder> [--dry-run]\"\n+    drive=\"$1\"; prefix=\"${2%/}\"; shift 2\n+    from=\"\"; dry=0\n+    while [[ $# -gt 0 ]]; do\n+      case \"$1\" in\n+        --from) from=\"$2\"; shift 2 ;;\n+        --dry-run) dry=1; shift ;;\n+        *) die \"unexpected import argument: $1\" ;;\n+      esac\n+    done\n+    [[ -d \"$from\" ]] || die \"--from must be a folder\"\n+    uploaded=0\n+    skipped=0\n+    failed=0\n+    planned=0\n+    while IFS= read -r -d '' f; do\n+      rel=\"${f#$from/}\"\n+      [[ \"$rel\" == .git/* || \"$rel\" == node_modules/* || \"$rel\" == \".DS_Store\" || \"$rel\" == */.DS_Store ]] && continue\n+      planned=$((planned + 1))\n+      sz=$(wc -c < \"$f\" | tr -d ' ')\n+      if [[ \"$sz\" -gt \"$MAX_FILE_BYTES\" ]]; then\n+        echo \"skip oversized $f ($sz bytes > $MAX_FILE_BYTES)\" >&2\n+        skipped=$((skipped + 1))\n+        continue\n+      fi\n+      dest=\"$rel\"\n+      [[ -n \"$prefix\" ]] && dest=\"$prefix/$rel\"\n+      if [[ \"$dry\" -eq 1 ]]; then\n+        echo \"upload $f -> $dest\"\n+        skipped=$((skipped + 1))\n+      else\n+        if (put_file \"$drive\" \"$dest\" --from \"$f\" >/dev/null); then\n+          uploaded=$((uploaded + 1))\n+        else\n+          failed=$((failed + 1))\n+        fi\n+      fi\n+    done < <(find \"$from\" -type f -print0 | sort -z)\n+    echo \"planned=$planned uploaded=$uploaded skipped=$skipped failed=$failed\"\n+    [[ \"$failed\" -eq 0 ]] || exit 1\n+    ;;\n+  export)\n+    [[ $# -ge 2 ]] || die \"usage: drive.sh export <drive> <prefix> --to <local-folder> [--dry-run]\"\n+    id=$(resolve_drive \"$1\"); prefix=\"${2%/}\"; shift 2\n+    to=\"\"; dry=0\n+    while [[ $# -gt 0 ]]; do\n+      case \"$1\" in\n+        --to) to=\"$2\"; shift 2 ;;\n+        --dry-run) dry=1; shift ;;\n+        *) die \"unexpected export argument: $1\" ;;\n+      esac\n+    done\n+    [[ -n \"$to\" ]] || die \"--to is required\"\n+    cursor=\"\"\n+    total=0\n+    while true; do\n+      url=\"$BASE_URL/api/v1/drives/$id/files?prefix=$(urlenc \"$prefix\")&limit=200\"\n+      [[ -n \"$cursor\" ]] && url=\"$url&cursor=$(urlenc \"$cursor\")\"\n+      files=$(api_json GET \"$url\")\n+      while IFS= read -r p; do\n+        [[ -n \"$p\" ]] || continue\n+        rel=\"$p\"\n+        [[ -n \"$prefix\" ]] && rel=\"${p#$prefix/}\"\n+        out=\"$to/$rel\"\n+        if [[ \"$dry\" -eq 1 ]]; then\n+          echo \"download $p -> $out\"\n+        else\n+          mkdir -p \"$(dirname \"$out\")\"\n+          curl -fsS \"$BASE_URL/api/v1/drives/$id/files/$(urlenc_path \"$p\")\" \"${auth_header[@]}\" -o \"$out\"\n+        fi\n+        total=$((total + 1))\n+      done < <(echo \"$files\" | \"$JQ_BIN\" -r '.files[].path')\n+      cursor=$(echo \"$files\" | \"$JQ_BIN\" -r '.nextCursor // empty')\n+      [[ -n \"$cursor\" ]] || break\n+    done\n+    echo \"files=$total\"\n+    ;;\n+  rm)\n+    [[ $# -ge 2 ]] || die \"usage: drive.sh rm <drive> <path> [--recursive --confirm <path>]\"\n+    id=$(resolve_drive \"$1\"); path=\"$2\"; shift 2\n+    recursive=0; confirm=\"\"\n+    while [[ $# -gt 0 ]]; do\n+      case \"$1\" in\n+        --recursive) recursive=1; shift ;;\n+        --confirm) confirm=\"$2\"; shift 2 ;;\n+        *) die \"unexpected rm argument: $1\" ;;\n+      esac\n+    done\n+    if [[ \"$recursive\" -eq 1 ]]; then\n+      [[ \"$confirm\" == \"$path\" ]] || die \"recursive delete requires --confirm '$path'\"\n+      head=$(drive_head \"$id\")\n+      api_json DELETE \"$BASE_URL/api/v1/drives/$id/files/$(urlenc_path \"$path\")?recursive=true&baseVersionId=$(urlenc \"$head\")\" | \"$JQ_BIN\" .\n+    else\n+      meta=$(file_meta \"$id\" \"$path\")\n+      etag=$(echo \"$meta\" | \"$JQ_BIN\" -r '.etag')\n+      curl -fsS -X DELETE \"$BASE_URL/api/v1/drives/$id/files/$(urlenc_path \"$path\")\" \"${auth_header[@]}\" -H \"If-Match: $etag\" | \"$JQ_BIN\" .\n+    fi\n+    ;;\n+  share)\n+    [[ $# -ge 1 ]] || die \"usage: drive.sh share <drive> --perms read|write [--prefix notes/] [--ttl 30d] [--label text] [--manage-tokens]\"\n+    id=$(resolve_drive \"$1\"); shift\n+    perms=\"write\"; prefix=\"\"; ttl=\"\"; label=\"\"; manage_tokens=\"false\"\n+    while [[ $# -gt 0 ]]; do\n+      case \"$1\" in\n+        --perms) perms=\"$2\"; shift 2 ;;\n+        --prefix) prefix=\"$2\"; shift 2 ;;\n+        --ttl) ttl=\"$2\"; shift 2 ;;\n+        --label) label=\"$2\"; shift 2 ;;\n+        --manage-tokens) manage_tokens=\"true\"; shift ;;\n+        *) die \"unexpected share argument: $1\" ;;\n+      esac\n+    done\n+    body=$(\"$JQ_BIN\" -n --arg p \"$perms\" --arg pp \"$prefix\" --arg ttl \"$ttl\" --arg label \"$label\" --argjson mt \"$manage_tokens\" \\\n+      '{perms:$p} + (if $mt then {manageTokens:true} else {} end) + (if $ttl == \"\" then {} else {ttl:$ttl} end) + (if $pp == \"\" then {} else {pathPrefix:$pp} end) + (if $label == \"\" then {} else {label:$label} end)')\n+    api_json POST \"$BASE_URL/api/v1/drives/$id/tokens\" \"$body\" | \"$JQ_BIN\" -r '.shareBlock'\n+    ;;\n+  tokens)\n+    [[ $# -eq 1 ]] || die \"usage: drive.sh tokens <drive>\"\n+    id=$(resolve_drive \"$1\")\n+    api_json GET \"$BASE_URL/api/v1/drives/$id/tokens\" | \"$JQ_BIN\" .\n+    ;;\n+  revoke)\n+    [[ $# -eq 2 ]] || die \"usage: drive.sh revoke <drive> <tokenId>\"\n+    id=$(resolve_drive \"$1\")\n+    api_json DELETE \"$BASE_URL/api/v1/drives/$id/tokens/$2\" | \"$JQ_BIN\" .\n+    ;;\n+  delete)\n+    [[ $# -ge 1 ]] || die \"usage: drive.sh delete <drive> --confirm <drive name>\"\n+    id=$(resolve_drive \"$1\"); shift\n+    confirm=\"\"\n+    while [[ $# -gt 0 ]]; do\n+      case \"$1\" in\n+        --confirm) confirm=\"$2\"; shift 2 ;;\n+        *) die \"unexpected delete argument: $1\" ;;\n+      esac\n+    done\n+    drive=$(api_json GET \"$BASE_URL/api/v1/drives/$id\")\n+    name=$(echo \"$drive\" | \"$JQ_BIN\" -r '.drive.name')\n+    [[ \"$confirm\" == \"$name\" ]] || die \"delete requires --confirm '$name'\"\n+    api_json DELETE \"$BASE_URL/api/v1/drives/$id\" | \"$JQ_BIN\" .\n+    ;;\n+  *)\n+    die \"unknown command: $CMD\"\n+    ;;\n+esac"]},{"file":"optional-skills/productivity/here-now/scripts/publish.sh","hunks":["@@ -0,0 +1,445 @@\n+#!/usr/bin/env bash\n+set -euo pipefail\n+\n+BASE_URL=\"https://here.now\"\n+CREDENTIALS_FILE=\"[redacted:secret-assignment]\"\n+API_KEY=\"[redacted:secret-assignment]\"\n+API_KEY_SOURCE=\"none\"\n+if [[ -n \"${HERENOW_API_KEY:-}\" ]]; then\n+  API_KEY_SOURCE=\"env\"\n+fi\n+ALLOW_NON_HERENOW_BASE_URL=0\n+SLUG=\"\"\n+CLAIM_TOKEN=\"\"\n+TITLE=\"\"\n+DESCRIPTION=\"\"\n+TTL=\"\"\n+CLIENT=\"\"\n+TARGET=\"\"\n+FORKABLE=\"\"\n+SPA_MODE=\"\"\n+FROM_DRIVE=\"\"\n+DRIVE_VERSION=\"\"\n+\n+usage() {\n+  cat <<'USAGE'\n+Usage: publish.sh <file-or-dir> [options]\n+\n+Options:\n+  --api-key <key>         API key (or set $HERENOW_API_KEY)\n+  --slug <slug>           Update existing publish\n+  --claim-token [redacted:secret-assignment]   Claim token for anonymous updates\n+  --title <text>          Viewer title\n+  --description <text>    Viewer description\n+  --ttl <seconds>         Expiry (authenticated only)\n+  --client <name>         Agent name for attribution (e.g. cursor, claude-code)\n+  --forkable              Allow others to fork this site\n+  --spa                   Enable SPA routing\n+  --from-drive <drv_...>  Publish a Drive snapshot instead of local files\n+  --version <dv_...>      Drive version for --from-drive (default: current head)\n+  --base-url <url>        API base (default: https://here.now)\n+  --allow-nonherenow-base-url\n+                         Allow auth requests to non-default API base URL\n+USAGE\n+  exit 1\n+}\n+\n+die() { echo \"error: $1\" >&2; exit 1; }\n+\n+SCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\n+SKILL_DIR=\"$(cd \"${SCRIPT_DIR}/..\" && pwd)\"\n+BUNDLED_JQ=\"${SKILL_DIR}/bin/jq\"\n+\n+if [[ -x \"$BUNDLED_JQ\" ]]; then\n+  JQ_BIN=\"$BUNDLED_JQ\"\n+elif command -v jq >/dev/null 2>&1; then\n+  JQ_BIN=\"$(command -v jq)\"\n+else\n+  die \"requires jq\"\n+fi\n+\n+for cmd in curl file; do\n+  command -v \"$cmd\" >/dev/null 2>&1 || die \"requires $cmd\"\n+done\n+\n+while [[ $# -gt 0 ]]; do\n+  case \"$1\" in\n+    --api-key)      API_KEY=\"$2\"; API_KEY_SOURCE=\"flag\"; shift 2 ;;\n+    --slug)         SLUG=\"$2\"; shift 2 ;;\n+    --claim-token)  CLAIM_TOKEN=\"$2\"; shift 2 ;;\n+    --title)        TITLE=\"$2\"; shift 2 ;;\n+    --description)  DESCRIPTION=\"$2\"; shift 2 ;;\n+    --ttl)          TTL=\"$2\"; shift 2 ;;\n+    --client)       CLIENT=\"$2\"; shift 2 ;;\n+    --base-url)     BASE_URL=\"$2\"; shift 2 ;;\n+    --allow-nonherenow-base-url) ALLOW_NON_HERENOW_BASE_URL=1; shift ;;\n+    --forkable)     FORKABLE=\"true\"; shift ;;\n+    --spa)          SPA_MODE=\"true\"; shift ;;\n+    --from-drive)   FROM_DRIVE=\"$2\"; shift 2 ;;\n+    --version)      DRIVE_VERSION=\"$2\"; shift 2 ;;\n+    --help|-h)      usage ;;\n+    -*)             die \"unknown option: $1\" ;;\n+    *)              [[ -z \"$TARGET\" ]] && TARGET=\"$1\" || die \"unexpected argument: $1\"; shift ;;\n+  esac\n+done\n+\n+if [[ -n \"$FROM_DRIVE\" ]]; then\n+  [[ -z \"$TARGET\" ]] || die \"--from-drive does not accept a local file-or-dir argument\"\n+else\n+  [[ -n \"$TARGET\" ]] || usage\n+  [[ -e \"$TARGET\" ]] || die \"path does not exist: $TARGET\"\n+fi\n+\n+# Load API key from credentials file if not provided via flag or env\n+if [[ -z \"$API_KEY\" && -f \"$CREDENTIALS_FILE\" ]]; then\n+  API_KEY=$(cat \"$CREDENTIALS_FILE\" | tr -d '[:space:]')\n+  [[ -n \"$API_KEY\" ]] && API_KEY_SOURCE=\"[redacted:secret-assignment]\"\n+fi\n+\n+BASE_URL=\"${BASE_URL%/}\"\n+STATE_DIR=\".herenow\"\n+STATE_FILE=\"$STATE_DIR/state.json\"\n+\n+# Safety guard: avoid accidentally sending bearer auth to arbitrary endpoints.\n+if [[ -n \"$API_KEY\" && \"$BASE_URL\" != \"https://here.now\" && \"$ALLOW_NON_HERENOW_BASE_URL\" -ne 1 ]]; then\n+  die \"refusing to send API key to non-default base URL; pass --allow-nonherenow-base-url to override\"\n+fi\n+\n+# Auto-load claim token from state file for anonymous updates\n+if [[ -n \"$SLUG\" && -z \"$CLAIM_TOKEN\" && -z \"$API_KEY\" && -f \"$STATE_FILE\" ]]; then\n+  CLAIM_TOKEN=$(\"$JQ_BIN\" -r --arg s \"$SLUG\" '.publishes[$s].claimToken // empty' \"$STATE_FILE\" 2>/dev/null || true)\n+fi\n+\n+if [[ -n \"$FROM_DRIVE\" ]]; then\n+  [[ -n \"$API_KEY\" ]] || die \"--from-drive requires an account API key\"\n+  BODY=$(\"$JQ_BIN\" -n --arg d \"$FROM_DRIVE\" '{driveId:$d}')\n+  [[ -n \"$DRIVE_VERSION\" ]] && BODY=$(echo \"$BODY\" | \"$JQ_BIN\" --arg v \"$DRIVE_VERSION\" '.versionId = $v')\n+  [[ -n \"$SLUG\" ]] && BODY=$(echo \"$BODY\" | \"$JQ_BIN\" --arg s \"$SLUG\" '.slug = $s')\n+  if [[ -n \"$TITLE\" || -n \"$DESCRIPTION\" ]]; then\n+    viewer=\"{}\"\n+    [[ -n \"$TITLE\" ]] && viewer=$(echo \"$viewer\" | \"$JQ_BIN\" --arg t \"$TITLE\" '.title = $t')\n+    [[ -n \"$DESCRIPTION\" ]] && viewer=$(echo \"$viewer\" | \"$JQ_BIN\" --arg d \"$DESCRIPTION\" '.description = $d')\n+    BODY=$(echo \"$BODY\" | \"$JQ_BIN\" --argjson v \"$viewer\" '.viewer = $v')\n+  fi\n+  [[ \"$FORKABLE\" == \"true\" ]] && BODY=$(echo \"$BODY\" | \"$JQ_BIN\" '.forkable = true')\n+  [[ \"$SPA_MODE\" == \"true\" ]] && BODY=$(echo \"$BODY\" | \"$JQ_BIN\" '.spaMode = true')\n+  CLIENT_HEADER_VALUE=\"here-now-publish-sh\"\n+  if [[ -n \"$CLIENT\" ]]; then\n+    normalized_client=$(echo \"$CLIENT\" | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9._-' '-')\n+    normalized_client=\"${normalized_client#-}\"\n+    normalized_client=\"${normalized_client%-}\"\n+    if [[ -n \"$normalized_client\" ]]; then\n+      CLIENT_HEADER_VALUE=\"${normalized_client}/publish-sh\"\n+    fi\n+  fi\n+\n+  echo \"publishing from Drive...\" >&2\n+  RESPONSE=$(curl -sS -X POST \"$BASE_URL/api/v1/publish/from-drive\" \\\n+    -H \"authorization: Bearer $API_KEY\" \\\n+    -H \"x-herenow-client: $CLIENT_HEADER_VALUE\" \\\n+    -H \"content-type: application/json\" \\\n+    -d \"$BODY\")\n+  if echo \"$RESPONSE\" | \"$JQ_BIN\" -e '.error' >/dev/null 2>&1; then\n+    err=$(echo \"$RESPONSE\" | \"$JQ_BIN\" -r '.error')\n+    die \"$err\"\n+  fi\n+  SITE_URL=$(echo \"$RESPONSE\" | \"$JQ_BIN\" -r '.siteUrl')\n+  OUT_SLUG=$(echo \"$RESPONSE\" | \"$JQ_BIN\" -r '.slug')\n+  CURRENT_VERSION=$(echo \"$RESPONSE\" | \"$JQ_BIN\" -r '.currentVersionId')\n+  DRIVE_VERSION_OUT=$(echo \"$RESPONSE\" | \"$JQ_BIN\" -r '.driveVersionId')\n+  echo \"$SITE_URL\"\n+  echo \"\" >&2\n+  echo \"publish_result.site_url=$SITE_URL\" >&2\n+  echo \"publish_result.slug=$OUT_SLUG\" >&2\n+  echo \"publish_result.action=from_drive\" >&2\n+  echo \"publish_result.auth_mode=authenticated\" >&2\n+  echo \"publish_result.api_key_source=[redacted:secret-assignment]\" >&2\n+  echo \"publish_result.persistence=permanent\" >&2\n+  echo \"publish_result.drive_id=$FROM_DRIVE\" >&2\n+  echo \"publish_result.drive_version_id=$DRIVE_VERSION_OUT\" >&2\n+  echo \"publish_result.current_version_id=$CURRENT_VERSION\" >&2\n+  exit 0\n+fi\n+\n+compute_sha256() {\n+  local f=\"$1\"\n+  if command -v sha256sum >/dev/null 2>&1; then\n+    sha256sum \"$f\" | cut -d' ' -f1\n+  else\n+    shasum -a 256 \"$f\" | cut -d' ' -f1\n+  fi\n+}\n+\n+guess_content_type() {\n+  local f=\"$1\"\n+  case \"${f##*.}\" in\n+    html|htm) echo \"text/html; charset=utf-8\" ;;\n+    css)      echo \"text/css; charset=utf-8\" ;;\n+    js|mjs)   echo \"text/javascript; charset=utf-8\" ;;\n+    json)     echo \"application/json; charset=utf-8\" ;;\n+    md|txt)   echo \"text/plain; charset=utf-8\" ;;\n+    svg)      echo \"image/svg+xml\" ;;\n+    png)      echo \"image/png\" ;;\n+    jpg|jpeg) echo \"image/jpeg\" ;;\n+    gif)      echo \"image/gif\" ;;\n+    webp)     echo \"image/webp\" ;;\n+    pdf)      echo \"application/pdf\" ;;\n+    mp4)      echo \"video/mp4\" ;;\n+    mov)      echo \"video/quicktime\" ;;\n+    mp3)      echo \"audio/mpeg\" ;;\n+    wav)      echo \"audio/wav\" ;;\n+    xml)      echo \"application/xml\" ;;\n+    woff2)    echo \"font/woff2\" ;;\n+    woff)     echo \"font/woff\" ;;\n+    ttf)      echo \"font/ttf\" ;;\n+    ico)      echo \"image/x-icon\" ;;\n+    *)\n+      local detected\n+      detected=$(file --brief --mime-type \"$f\" 2>/dev/null || echo \"application/octet-stream\")\n+      echo \"$detected\"\n+      ;;\n+  esac\n+}\n+\n+# Build file manifest as JSON array\n+FILES_JSON=\"[]\"\n+\n+if [[ -f \"$TARGET\" ]]; then\n+  sz=$(wc -c < \"$TARGET\" | tr -d ' ')\n+  ct=$(guess_content_type \"$TARGET\")\n+  bn=$(basename \"$TARGET\")\n+  h=$(compute_sha256 \"$TARGET\")\n+  FILES_JSON=$(\"$JQ_BIN\" -n --arg p \"$bn\" --argjson s \"$sz\" --arg c \"$ct\" --arg h \"$h\" \\\n+    '[{\"path\":$p,\"size\":$s,\"contentType\":$c,\"hash\":$h}]')\n+  FILE_MAP=$(\"$JQ_BIN\" -n --arg p \"$bn\" --arg a \"$(cd \"$(dirname \"$TARGET\")\" && pwd)/$(basename \"$TARGET\")\" \\\n+    '{($p):$a}')\n+elif [[ -d \"$TARGET\" ]]; then\n+  FILE_MAP=\"{}\"\n+  while IFS= read -r -d '' f; do\n+    rel=\"${f#$TARGET/}\"\n+    [[ \"$rel\" == \".DS_Store\" ]] && continue\n+    [[ \"$(basename \"$rel\")\" == \".DS_Store\" ]] && continue\n+    [[ \"$rel\" == \".herenow/fork-meta.json\" ]] && continue\n+    sz=$(wc -c < \"$f\" | tr -d ' ')\n+    ct=$(guess_content_type \"$f\")\n+    h=$(compute_sha256 \"$f\")\n+    abs=$(cd \"$(dirname \"$f\")\" && pwd)/$(basename \"$f\")\n+    FILES_JSON=$(echo \"$FILES_JSON\" | \"$JQ_BIN\" --arg p \"$rel\" --argjson s \"$sz\" --arg c \"$ct\" --arg h \"$h\" \\\n+      '. + [{\"path\":$p,\"size\":$s,\"contentType\":$c,\"hash\":$h}]')\n+    FILE_MAP=$(echo \"$FILE_MAP\" | \"$JQ_BIN\" --arg p \"$rel\" --arg a \"$abs\" '. + {($p):$a}')\n+  done < <(find \"$TARGET\" -type f -print0 | sort -z)\n+else\n+  die \"not a file or directory: $TARGET\"\n+fi\n+\n+file_count=$(echo \"$FILES_JSON\" | \"$JQ_BIN\" 'length')\n+[[ \"$file_count\" -gt 0 ]] || die \"no files found\"\n+\n+# Read fork-meta.json defaults if present and no explicit flags given\n+FORK_META=\"\"\n+if [[ -d \"$TARGET\" ]]; then\n+  FORK_META_PATH=\"$TARGET/.herenow/fork-meta.json\"\n+  if [[ -f \"$FORK_META_PATH\" ]]; then\n+    FORK_META=$(cat \"$FORK_META_PATH\")\n+    if [[ -z \"$FORKABLE\" ]]; then\n+      FORKABLE=$(\"$JQ_BIN\" -r '.forkable // empty' <<< \"$FORK_META\" 2>/dev/null || true)\n+    fi\n+  fi\n+fi\n+\n+# Build request body\n+BODY=$(echo \"$FILES_JSON\" | \"$JQ_BIN\" '{files: .}')\n+\n+if [[ -n \"$TTL\" ]]; then\n+  BODY=$(echo \"$BODY\" | \"$JQ_BIN\" --argjson t \"$TTL\" '.ttlSeconds = $t')\n+fi\n+\n+if [[ -n \"$TITLE\" || -n \"$DESCRIPTION\" ]]; then\n+  viewer=\"{}\"\n+  [[ -n \"$TITLE\" ]] && viewer=$(echo \"$viewer\" | \"$JQ_BIN\" --arg t \"$TITLE\" '.title = $t')\n+  [[ -n \"$DESCRIPTION\" ]] && viewer=$(echo \"$viewer\" | \"$JQ_BIN\" --arg d \"$DESCRIPTION\" '.description = $d')\n+  BODY=$(echo \"$BODY\" | \"$JQ_BIN\" --argjson v \"$viewer\" '.viewer = $v')\n+fi\n+\n+if [[ -n \"$CLAIM_TOKEN\" && -n \"$SLUG\" && -z \"$API_KEY\" ]]; then\n+  BODY=$(echo \"$BODY\" | \"$JQ_BIN\" --arg ct \"$CLAIM_TOKEN\" '.claimToken = $ct')\n+fi\n+\n+if [[ \"$FORKABLE\" == \"true\" ]]; then\n+  BODY=$(echo \"$BODY\" | \"$JQ_BIN\" '.forkable = true')\n+fi\n+\n+if [[ \"$SPA_MODE\" == \"true\" ]]; then\n+  BODY=$(echo \"$BODY\" | \"$JQ_BIN\" '.spaMode = true')\n+fi\n+\n+# Determine endpoint and method\n+if [[ -n \"$SLUG\" ]]; then\n+  URL=\"$BASE_URL/api/v1/publish/$SLUG\"\n+  METHOD=\"PUT\"\n+else\n+  URL=\"$BASE_URL/api/v1/publish\"\n+  METHOD=\"POST\"\n+fi\n+\n+# Build auth header\n+AUTH_ARGS=()\n+if [[ -n \"$API_KEY\" ]]; then\n+  AUTH_ARGS=(-H \"authorization: Bearer $API_KEY\")\n+fi\n+\n+AUTH_MODE=\"anonymous\"\n+if [[ -n \"$API_KEY\" ]]; then\n+  AUTH_MODE=\"authenticated\"\n+fi\n+\n+CLIENT_HEADER_VALUE=\"here-now-publish-sh\"\n+if [[ -n \"$CLIENT\" ]]; then\n+  normalized_client=$(echo \"$CLIENT\" | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9._-' '-')\n+  normalized_client=\"${normalized_client#-}\"\n+  normalized_client=\"${normalized_client%-}\"\n+  if [[ -n \"$normalized_client\" ]]; then\n+    CLIENT_HEADER_VALUE=\"${normalized_client}/publish-sh\"\n+  fi\n+fi\n+CLIENT_ARGS=(-H \"x-herenow-client: $CLIENT_HEADER_VALUE\")\n+\n+# Step 1: Create/update publish\n+echo \"creating publish ($file_count files)...\" >&2\n+RESPONSE=$(curl -sS -X \"$METHOD\" \"$URL\" \\\n+  \"${AUTH_ARGS[@]+\"${AUTH_ARGS[@]}\"}\" \\\n+  \"${CLIENT_ARGS[@]+\"${CLIENT_ARGS[@]}\"}\" \\\n+  -H \"content-type: application/json\" \\\n+  -d \"$BODY\")\n+\n+# Check for errors\n+if echo \"$RESPONSE\" | \"$JQ_BIN\" -e '.error' >/dev/null 2>&1; then\n+  err=$(echo \"$RESPONSE\" | \"$JQ_BIN\" -r '.error')\n+  details=$(echo \"$RESPONSE\" | \"$JQ_BIN\" -r '.details // empty')\n+  die \"$err${details:+ ($details)}\"\n+fi\n+\n+OUT_SLUG=$(echo \"$RESPONSE\" | \"$JQ_BIN\" -r '.slug')\n+VERSION_ID=$(echo \"$RESPONSE\" | \"$JQ_BIN\" -r '.upload.versionId')\n+FINALIZE_URL=$(echo \"$RESPONSE\" | \"$JQ_BIN\" -r '.upload.finalizeUrl')\n+SITE_URL=$(echo \"$RESPONSE\" | \"$JQ_BIN\" -r '.siteUrl')\n+UPLOAD_COUNT=$(echo \"$RESPONSE\" | \"$JQ_BIN\" '.upload.uploads | length')\n+SKIPPED_COUNT=$(echo \"$RESPONSE\" | \"$JQ_BIN\" '.upload.skipped // [] | length')\n+\n+[[ \"$OUT_SLUG\" != \"null\" ]] || die \"unexpected response: $RESPONSE\"\n+\n+# Step 2: Upload files (skipped files are unchanged from previous version)\n+if [[ \"$SKIPPED_COUNT\" -gt 0 ]]; then\n+  echo \"uploading $UPLOAD_COUNT files ($SKIPPED_COUNT unchanged, skipped)...\" >&2\n+else\n+  echo \"uploading $UPLOAD_COUNT files...\" >&2\n+fi\n+upload_errors=0\n+\n+for i in $(seq 0 $((UPLOAD_COUNT - 1))); do\n+  upload_path=$(echo \"$RESPONSE\" | \"$JQ_BIN\" -r \".upload.uploads[$i].path\")\n+  upload_url=$(echo \"$RESPONSE\" | \"$JQ_BIN\" -r \".upload.uploads[$i].url\")\n+  upload_ct=$(echo \"$RESPONSE\" | \"$JQ_BIN\" -r \".upload.uploads[$i].headers[\\\"Content-Type\\\"] // empty\")\n+\n+  if [[ -f \"$TARGET\" && ! -d \"$TARGET\" ]]; then\n+    local_file=\"$TARGET\"\n+  else\n+    local_file=$(echo \"$FILE_MAP\" | \"$JQ_BIN\" -r --arg p \"$upload_path\" '.[$p]')\n+  fi\n+\n+  if [[ ! -f \"$local_file\" ]]; then\n+    echo \"warning: missing local file for $upload_path\" >&2\n+    upload_errors=$((upload_errors + 1))\n+    continue\n+  fi\n+\n+  ct_args=()\n+  [[ -n \"$upload_ct\" ]] && ct_args=(-H \"Content-Type: $upload_ct\")\n+\n+  http_code=$(curl -sS -o /dev/null -w \"%{http_code}\" -X PUT \"$upload_url\" \\\n+    \"${ct_args[@]+\"${ct_args[@]}\"}\" \\\n+    --data-binary \"@$local_file\")\n+\n+  if [[ \"$http_code\" -lt 200 || \"$http_code\" -ge 300 ]]; then\n+    echo \"warning: upload failed for $upload_path (HTTP $http_code)\" >&2\n+    upload_errors=$((upload_errors + 1))\n+  fi\n+done\n+\n+[[ \"$upload_errors\" -eq 0 ]] || die \"$upload_errors file(s) failed to upload\"\n+\n+# Step 3: Finalize\n+echo \"finalizing...\" >&2\n+FIN_RESPONSE=$(curl -sS -X POST \"$FINALIZE_URL\" \\\n+  \"${AUTH_ARGS[@]+\"${AUTH_ARGS[@]}\"}\" \\\n+  \"${CLIENT_ARGS[@]+\"${CLIENT_ARGS[@]}\"}\" \\\n+  -H \"content-type: application/json\" \\\n+  -d \"{\\\"versionId\\\":\\\"$VERSION_ID\\\"}\")\n+\n+if echo \"$FIN_RESPONSE\" | \"$JQ_BIN\" -e '.error' >/dev/null 2>&1; then\n+  err=$(echo \"$FIN_RESPONSE\" | \"$JQ_BIN\" -r '.error')\n+  die \"finalize failed: $err\"\n+fi\n+\n+# Save state\n+mkdir -p \"$STATE_DIR\"\n+if [[ -f \"$STATE_FILE\" ]]; then\n+  STATE=$(cat \"$STATE_FILE\")\n+else\n+  STATE='{\"publishes\":{}}'\n+fi\n+\n+entry=$(\"$JQ_BIN\" -n --arg s \"$SITE_URL\" '{siteUrl: $s}')\n+\n+RESPONSE_CLAIM_TOKEN=[redacted:secret-assignment] \"$RESPONSE\" | \"$JQ_BIN\" -r '.claimToken // empty')\n+RESPONSE_CLAIM_URL=$(echo \"$RESPONSE\" | \"$JQ_BIN\" -r '.claimUrl // empty')\n+RESPONSE_EXPIRES=$(echo \"$RESPONSE\" | \"$JQ_BIN\" -r '.expiresAt // empty')\n+\n+[[ -n \"$RESPONSE_CLAIM_TOKEN\" ]] && entry=$(echo \"$entry\" | \"$JQ_BIN\" --arg v \"$RESPONSE_CLAIM_TOKEN\" '.claimToken = $v')\n+[[ -n \"$RESPONSE_CLAIM_URL\" ]] && entry=$(echo \"$entry\" | \"$JQ_BIN\" --arg v \"$RESPONSE_CLAIM_URL\" '.claimUrl = $v')\n+[[ -n \"$RESPONSE_EXPIRES\" ]] && entry=$(echo \"$entry\" | \"$JQ_BIN\" --arg v \"$RESPONSE_EXPIRES\" '.expiresAt = $v')\n+\n+STATE=$(echo \"$STATE\" | \"$JQ_BIN\" --arg slug \"$OUT_SLUG\" --argjson e \"$entry\" '.publishes[$slug] = $e')\n+echo \"$STATE\" | \"$JQ_BIN\" '.' > \"$STATE_FILE\"\n+\n+# Output\n+echo \"$SITE_URL\"\n+\n+PERSISTENCE=\"permanent\"\n+if [[ \"$AUTH_MODE\" == \"anonymous\" ]]; then\n+  PERSISTENCE=\"expires_24h\"\n+elif [[ -n \"$RESPONSE_EXPIRES\" ]]; then\n+  PERSISTENCE=\"expires_at\"\n+fi\n+\n+SAFE_CLAIM_URL=\"\"\n+if [[ -n \"$RESPONSE_CLAIM_URL\" && \"$RESPONSE_CLAIM_URL\" == https://* ]]; then\n+  SAFE_CLAIM_URL=\"$RESPONSE_CLAIM_URL\"\n+fi\n+\n+ACTION=\"create\"\n+if [[ -n \"$SLUG\" ]]; then\n+  ACTION=\"update\"\n+fi\n+\n+echo \"\" >&2\n+echo \"publish_result.site_url=$SITE_URL\" >&2\n+echo \"publish_result.slug=$OUT_SLUG\" >&2\n+echo \"publish_result.action=$ACTION\" >&2\n+echo \"publish_result.auth_mode=$AUTH_MODE\" >&2\n+echo \"publish_result.api_key_source=[redacted:secret-assignment]\" >&2\n+echo \"publish_result.persistence=$PERSISTENCE\" >&2\n+echo \"publish_result.expires_at=$RESPONSE_EXPIRES\" >&2\n+echo \"publish_result.claim_url=$SAFE_CLAIM_URL\" >&2\n+\n+if [[ \"$AUTH_MODE\" == \"authenticated\" ]]; then\n+  echo \"authenticated publish (permanent, saved to your account)\" >&2\n+else\n+  echo \"anonymous publish (expires in 24h)\" >&2\n+  if [[ -n \"$SAFE_CLAIM_URL\" ]]; then\n+    echo \"claim URL: $SAFE_CLAIM_URL\" >&2\n+  fi\n+  if [[ -n \"$RESPONSE_CLAIM_TOKEN\" ]]; then\n+    echo \"claim token saved to $STATE_FILE\" >&2\n+  fi\n+fi"]},{"file":"scripts/release.py","hunks":["@@ -45,6 +45,7 @@\n     \"[redacted:email]\": \"leoneparise\",\n     \"[redacted:email]\": \"teknium1\",\n     \"127238744+teknium1@users.noreply.github.com\": \"teknium1\",\n+    \"[redacted:email]\": \"adamludwin\",\n     \"2093036+exiao@users.noreply.github.com\": \"exiao\",\n     \"[redacted:email]\": \"rylena\",\n     \"14046872+tmimmanuel@users.noreply.github.com\": \"tmimmanuel\",\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}