NousResearch/hermes-agent

Add here.now static web publishing skill

Cluster 378 · skills. Data as of

Nothing open on GitHub. Fix merged in #18170 on 1 May 2026. Check it covers your case.

The short answer

0open issues
0open PRs
3items grouped
Known?
Yes. 3 related items grouped here, 1 issue already closed.
Fix event
A fix was merged to main in PR #18170. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this resolved, deciding PR #18170. That is separate from the fix event.
Still open
Nothing open on GitHub.

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.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #18170 feat(skills): add here.now as an optional skill

    merged to main

    files: 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

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

1 PR closed without merging

From the close comments: merged via another change 1

  • #14803 Add built-in here.now skill · merged via another change → f7dfd4a, 21cc9c8, #18170
Closed issues · 1
  • Closed#378Feature: here.now Skill — Instant Static Web Publishing for Agent-Created Content
How this cluster was grouped
  • Link evidence (structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
  • Pairwise link density 0.67: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. 1.00 means every pair is linked.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Subsystem
skills
Merged PRs
#18170
Closed issues
1
Attempts
1 PR closed without merging
Fix link checked
1 May 2026. This confirmation is old and may be out of date (a revert or reopen would not have invalidated it here); re-verify.

For your agent

The same facts, typed.