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配置

Informations de source

Dépôt
mguozhen/paperclip-remote-agent-skill
Dernière activité de la source
11 avril 2026 à 21:12
Langue détectée de SKILL.md
anglais
Étoiles
1
Forks
0

Options d'installation

Le prompt qui vérifie d'abord la source est sélectionné par défaut. Vous pouvez passer à une commande directe ou télécharger une copie locale.

Vérifiez les fichiers source

Lisez SKILL.md et les fichiers associés affichés par SkillsMP avant de décider de l'installer.

Affichage de SKILL.md

SKILL.md
Instructions source · Aperçu en lecture seule
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 ==="
Voir sur GitHub
Ce SKILL.md est tres volumineux, SkillsMP affiche donc ici seulement la premiere section. Voir sur GitHub