Skip to main content

paperclip-remote-agent

One-click setup for Paperclip remote agent. Installs listener, starts tunnel, registers with server. Triggers: paperclip remote setup, setup remote agent, remote agent setup, 远程agent配置

Source facts

Repository
mguozhen/paperclip-remote-agent-skill
Last source activity
April 11, 2026 at 21:12
Detected SKILL.md language
English
Stars
1
Forks
0

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
paperclip-remote-agent
description
One-click setup for Paperclip remote agent. Installs listener, starts tunnel, registers with server. Triggers: paperclip remote setup, setup remote agent, remote agent setup, 远程agent配置
allowed-tools
Bash, Read, Write, Glob, Grep
# Paperclip Remote Agent Setup You are setting up this machine as a remote Paperclip agent. This is a ONE-SHOT fully automated setup — run all steps without asking for confirmation unless a critical error occurs. ## Server Configuration The server URL may change (Quick Tunnel). Use this discovery logic: ```bash # Try fixed domain first, then fallback for url in "https://paperclip.solvea.io" "https://paperclip.solveaagent.com"; do if curl -sf --connect-timeout 5 "$url/api/health" > /dev/null 2>&1; then PAPERCLIP_API_URL="$url" break fi done ``` If neither works, ask the user: **"服务端 URL 是什么?问一下 Wayne。"** Other constants: ``` PAPERCLIP_API_KEY=local-trusted LISTENER_PORT=7700 ``` ## Agent Registry Ask the user ONE question: **"你是哪个 Agent?(vocaisage / reddit2 / reddit-mguozhen / Niko / X-SolveaCX)"** | Name | Agent ID | |------|----------| | vocaisage | `ea0afa73-5a0c-44cc-ad21-5a9f57ab5621` | | reddit2 | `49c283a0-cc1f-4e81-8fe4-f9509edeed58` | | reddit-mguozhen | `6e6ad935-e7a5-4642-864a-cc2aee81b3cf` | | Niko | `22f4130e-f51f-47c4-ac69-ee49b612d389` | | X-SolveaCX | `1519cbcd-bde6-497d-989d-c1c92fc6ddc7` | If the name doesn't match, ask the user to confirm. Set `PAPERCLIP_AGENT_ID` accordingly. After getting the agent name, run ALL remaining steps automatically. ## Step 1: Preflight Checks ```bash echo "=== Preflight Checks ===" # Node.js echo -n "Node.js: " node --version 2>/dev/null || { echo "MISSING — install from https://nodejs.org"; exit 1; } # Claude Code — search common paths CLAUDE_PATH=$(which claude 2>/dev/null || echo "") if [ -z "$CLAUDE_PATH" ]; then for p in ~/.local/bin/claude /usr/local/bin/claude /opt/homebrew/bin/claude; do if [ -x "$p" ]; then CLAUDE_PATH="$p"; break; fi done # Also try nvm paths if [ -z "$CLAUDE_PATH" ]; then for p in ~/.nvm/versions/node/*/bin/claude; do if [ -x "$p" ]; then CLAUDE_PATH="$p"; break; fi done fi fi if [ -z "$CLAUDE_PATH" ]; then echo "Claude: MISSING" echo "Fix: npm install -g @anthropic-ai/claude-code && claude login" exit 1 fi echo "Claude: $CLAUDE_PATH" # cloudflared echo -n "cloudflared: " which cloudflared 2>/dev/null || { echo "MISSING" echo "Fix (macOS): brew install cloudflare/cloudflare/cloudflared" echo "Fix (Linux): https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/" exit 1 } echo "=== All checks passed ===" ``` Save `CLAUDE_PATH` for later steps. ## Step 2: Discover Server URL ```bash echo "=== Discovering server URL ===" PAPERCLIP_API_URL="" for url in "https://paperclip.solvea.io" "https://paperclip.solveaagent.com"; do echo -n "Trying $url ... " if curl -sf --connect-timeout 5 "$url/api/health" > /dev/null 2>&1; then PAPERCLIP_API_URL="$url" echo "OK" break else echo "unreachable" fi done if [ -z "$PAPERCLIP_API_URL" ]; then echo "ERROR: Cannot reach server. Ask Wayne for the current URL." exit 1 fi echo "Server: $PAPERCLIP_API_URL" ``` If both fail, ask the user for the URL. Do NOT proceed without a working server URL. ## Step 3: Create Workspace & Listener ```bash mkdir -p ~/paperclip-remote ``` Write the file `~/paperclip-remote/listener.mjs` using the Write tool with this EXACT content: ```javascript #!/usr/bin/env node import { createServer } from "node:http"; import { spawn } from "node:child_process"; import { resolve } from "node:path"; import { homedir } from "node:os"; import { existsSync, mkdirSync } from "node:fs"; const PORT = parseInt(process.env.PORT || "7700", 10); const LOCAL_API_URL = process.env.PAPERCLIP_API_URL || ""; const MAX_STDOUT = 4 * 1024 * 1024; function expandHome(p) { if (!p) return resolve(homedir(), "paperclip-remote"); return p.startsWith("~/") ? resolve(homedir(), p.slice(2)) : p; } function ts() { return new Date().toISOString(); } let activeRuns = 0; const server = createServer(async (req, res) => { if (req.method === "GET" && req.url === "/health") { res.writeHead(200, { "content-type": "application/json" }); res.end(JSON.stringify({ status: "ok", activeRuns })); return; } if (req.method === "POST" && req.url?.startsWith("/execute")) { let body = ""; for await (const chunk of req) body += chunk; let payload; try { payload = JSON.parse(body); } catch { res.writeHead(400); res.end('{"error":"Invalid JSON"}'); return; } const { runId = "unknown", prompt = "", command = "claude", args = [], env: remoteEnv = {}, cwd = "~/paperclip-remote", timeoutSec = 600 } = payload; const resolvedCwd = expandHome(cwd); if (!existsSync(resolvedCwd)) { try { mkdirSync(resolvedCwd, { recursive: true }); } catch (e) { res.writeHead(500); res.end(JSON.stringify({ error: e.message })); return; } } // remoteEnv (from /execute payload) wins — the server-side adapter // dictates env per call, so we MUST NOT let LOCAL_API_URL clobber it. const mergedEnv = { ...process.env, ...(LOCAL_API_URL ? { PAPERCLIP_API_URL: LOCAL_API_URL } : {}), ...remoteEnv }; console.log(`[${ts()}] > run=${runId} cmd=${command} cwd=${resolvedCwd}`); activeRuns++; const result = await new Promise((done) => { let stdout = "", stderr = "", timedOut = false, stdoutBytes = 0; const child = spawn(command, args, { cwd: resolvedCwd, env: mergedEnv, stdio: ["pipe","pipe","pipe"] }); const timer = timeoutSec > 0 ? setTimeout(() => { timedOut = true; child.kill("SIGTERM"); setTimeout(() => { try { child.kill("SIGKILL"); } catch {} }, 15000); }, timeoutSec * 1000) : null; child.stdout.on("data", (c) => { const s = c.toString(); stdoutBytes += s.length; if (stdoutBytes <= MAX_STDOUT) stdout += s; }); child.stderr.on("data", (c) => { stderr += c.toString(); if (stderr.length > 512*1024) stderr = stderr.slice(-512*1024); }); if (prompt) { child.stdin.write(prompt); } child.stdin.end(); child.on("close", (exitCode, signal) => { if (timer) clearTimeout(timer); done({ exitCode, signal, stdout, stderr, timedOut }); }); child.on("error", (err) => { if (timer) clearTimeout(timer); done({ exitCode: 1, signal: null, stdout, stderr: stderr + "\n" + err.message, timedOut: false }); }); }); activeRuns--; // Strip ANSI escape codes to produce clean JSON const ansiRe = /\x1B\[[0-9;]*[A-Za-z]|\x1B\].*?\x07|\x1B[()][A-B012]|\x1B\[[\?]?[0-9;]*[hl]/g; result.stdout = result.stdout.replace(ansiRe, ""); result.stderr = result.stderr.replace(ansiRe, ""); console.log(`[${ts()}] < run=${runId} exit=${result.exitCode} timedOut=${result.timedOut}`); res.writeHead(200, { "content-type": "application/json" }); res.end(JSON.stringify(result)); return; } res.writeHead(404); res.end("Not found\n"); }); server.listen(PORT, () => { console.log(`Paperclip Remote Listener on port ${PORT}`); console.log(`API URL: ${LOCAL_API_URL || "(from server)"}`); console.log(`Waiting for heartbeat requests...`); }); ``` ## Step 4: Start Listener ```bash cd ~/paperclip-remote # Kill any existing listener pkill -f "listener.mjs" 2>/dev/null || true sleep 1 # Start listener (pass server URL so remote claude can call back) PAPERCLIP_API_URL="$PAPERCLIP_API_URL" nohup node listener.mjs > listener.log 2>&1 & echo $! > listener.pid sleep 2 # Verify curl -s http://localhost:7700/health || { echo "ERROR: Listener failed"; cat listener.log; exit 1; } echo " Listener OK" ``` ## Step 5: Start Cloudflare Tunnel Uses a Cloudflare **Quick Tunnel** — zero login, zero per-machine account setup. Tradeoff: the public URL rotates on every cloudflared restart, and a single HTTP request is capped at ~100s at the Cloudflare edge. The skill re-registers with the Paperclip server every time it runs, so URL rotation is handled automatically. ```bash cd ~/paperclip-remote # Kill any existing tunnel pkill -f "cloudflared.*7700" 2>/dev/null || true sleep 1 # Start tunnel nohup cloudflared tunnel --url http://localhost:7700 > tunnel.log 2>&1 & echo $! > tunnel.pid # Wait for URL (retry up to 60 seconds) TUNNEL_URL="" for i in $(seq 1 12); do sleep 5 TUNNEL_URL=$(grep -o 'https://[a-z0-9-]*\.trycloudflare\.com' tunnel.log | head -1) if [ -n "$TUNNEL_URL" ]; then break; fi echo "Waiting for tunnel... ($i/12)" done if [ -z "$TUNNEL_URL" ]; then echo "ERROR: Failed to get tunnel URL" cat tunnel.log exit 1 fi echo "Tunnel: $TUNNEL_URL" ``` ## Step 6: Register with Server ```bash RESULT=$(curl -s -X PATCH "$PAPERCLIP_API_URL/api/agents/$PAPERCLIP_AGENT_ID" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer local-trusted" \ -d "{ \"adapterType\": \"claude_remote\", \"adapterConfig\": { \"url\": \"$TUNNEL_URL\", \"apiUrl\": \"$PAPERCLIP_API_URL\", \"command\": \"$CLAUDE_PATH\", \"cwd\": \"$HOME/paperclip-remote\", \"dangerouslySkipPermissions\": true }, \"runtimeConfig\": { \"heartbeat\": {\"enabled\": true, \"intervalSec\": 60, \"wakeOnDemand\": true} } }") # Check result echo "$RESULT" | python3 -c " import sys,json try: d=json.load(sys.stdin) if 'adapterType' in d and d['adapterType'] == 'claude_remote': print('Registration OK') else: print('ERROR:', json.dumps(d)[:200]) except: print('ERROR: Invalid response') " ``` ## Step 7: Verify End-to-End ```bash echo "" echo "=== Verification ==="
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub