File size: 16,928 Bytes
be8c7bb
7c90063
be8c7bb
 
 
7c90063
be8c7bb
 
 
 
 
 
 
7c90063
 
 
 
 
 
08569ac
 
 
 
be8c7bb
6e26197
be8c7bb
7c90063
 
c1db737
 
 
7c90063
c1db737
7c90063
c1db737
 
 
 
7c90063
 
 
67a5cb1
5ee080c
 
 
 
67a5cb1
7c90063
b8b0f98
7c90063
 
 
 
b8b0f98
 
 
 
 
 
 
 
 
7c90063
 
 
 
 
 
 
 
 
 
be8c7bb
6ad52e8
5ee080c
6ad52e8
5ee080c
6ad52e8
 
 
 
7c90063
 
 
 
 
 
 
 
 
 
 
 
 
 
0c60c15
 
7c90063
0c60c15
be8c7bb
0c60c15
be8c7bb
0c60c15
 
7c90063
0c60c15
 
 
7c90063
 
0c60c15
be8c7bb
7c90063
0c60c15
 
 
6ad52e8
 
 
c979202
6ad52e8
 
 
 
 
 
 
 
7c90063
be8c7bb
6ad52e8
c979202
6ad52e8
c979202
 
6ad52e8
 
7c90063
be8c7bb
7c90063
6ad52e8
7c90063
 
be8c7bb
6ad52e8
b17ba14
 
 
 
 
 
 
 
6ad52e8
 
c979202
6ad52e8
 
 
 
 
be8c7bb
7c90063
be8c7bb
 
7c90063
eb3a3f8
7c90063
 
be8c7bb
 
eb3a3f8
 
 
 
 
 
 
9161677
7c42a6c
 
9161677
7c42a6c
9161677
7c90063
be8c7bb
08ebfa3
 
7c42a6c
08ebfa3
 
4e5f895
 
 
 
 
7c42a6c
7c90063
 
be8c7bb
 
 
 
 
 
 
 
7c90063
be8c7bb
 
7c90063
be8c7bb
 
6b586fa
4b5ca71
 
 
6b586fa
 
be8c7bb
 
7c90063
be8c7bb
 
 
 
 
 
 
7a3506c
7c90063
7a3506c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7c90063
be8c7bb
7c90063
6ad52e8
 
 
 
 
 
 
 
 
 
 
 
 
7c90063
be8c7bb
 
6ad52e8
be8c7bb
 
 
 
67a5cb1
fb0a5f1
 
 
 
67a5cb1
 
fb0a5f1
67a5cb1
fb0a5f1
 
 
227dcc0
fb0a5f1
 
 
67a5cb1
 
 
 
 
9946cfb
 
 
 
 
 
7c90063
9946cfb
 
a2c720b
 
7c90063
28c31b5
a2c720b
28c31b5
7c90063
28c31b5
a2c720b
 
 
 
 
28c31b5
9946cfb
6d97584
0502cce
 
7c90063
 
 
 
 
 
 
 
a2c720b
0502cce
7c90063
a2c720b
f635d84
748bded
dcedf5f
7c90063
dcedf5f
 
7c90063
 
44ce938
 
7c90063
 
 
a2c720b
b9ccb74
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
#!/bin/bash
set -euo pipefail

umask 0077

# ── Config ────────────────────────────────────────────────────────────────────
export DATABASE_URL="${DATABASE_URL:-postgres://postgres:paperclip@localhost:5432/paperclip}"
export PORT="${PORT:-3100}"
export SERVE_UI="${SERVE_UI:-true}"
export NODE_ENV="${NODE_ENV:-production}"
export HOST="${HOST:-0.0.0.0}"
export PAPERCLIP_HOME="${PAPERCLIP_HOME:-/paperclip}"
export PAPERCLIP_DEPLOYMENT_MODE="${PAPERCLIP_DEPLOYMENT_MODE:-authenticated}"
export PAPERCLIP_DEPLOYMENT_EXPOSURE="${PAPERCLIP_DEPLOYMENT_EXPOSURE:-private}"
export PAPERCLIP_INSTANCE_ID="${PAPERCLIP_INSTANCE_ID:-default}"
export PAPERCLIP_CONFIG="${PAPERCLIP_CONFIG:-${PAPERCLIP_HOME}/instances/default/config.json}"
export PAPERCLIP_TELEMETRY_DISABLED="${PAPERCLIP_TELEMETRY_DISABLED:-1}"
export DO_NOT_TRACK="${DO_NOT_TRACK:-1}"
export OPENCODE_ALLOW_ALL_MODELS="${OPENCODE_ALLOW_ALL_MODELS:-true}"
# Sanitize: strip non-digits, clamp minimum to 60s to prevent spin loops.
SYNC_INTERVAL=$(printf '%s' "${SYNC_INTERVAL:-3600}" | tr -dc '0-9')
{ [ -z "${SYNC_INTERVAL}" ] || [ "${SYNC_INTERVAL}" -lt 60 ]; } && SYNC_INTERVAL=3600
export SYNC_INTERVAL
export SYNC_MAX_FILE_BYTES="${SYNC_MAX_FILE_BYTES:-52428800}"
export BACKUP_DATASET_NAME="${BACKUP_DATASET_NAME:-huggingclip-backup}"

# Derive public URL from HF Space host
if [ -z "${PAPERCLIP_PUBLIC_URL:-}" ] && [ -n "${SPACE_HOST:-}" ]; then
    export PAPERCLIP_PUBLIC_URL="https://${SPACE_HOST}"
fi

# Allowed hostnames
_ALLOWED="localhost,127.0.0.1,0.0.0.0"
if [ -n "${SPACE_HOST:-}" ]; then
    _ALLOWED="${_ALLOWED},${SPACE_HOST}"
fi
export PAPERCLIP_ALLOWED_HOSTNAMES="${PAPERCLIP_ALLOWED_HOSTNAMES:-${_ALLOWED}}"

# LLM API keys
export GEMINI_API_KEY="${GEMINI_API_KEY:-}"
export OPENAI_API_KEY="${OPENAI_API_KEY:-}"
# Anthropic/Claude Code β€” set one or neither:
#   CLAUDE_CODE_OAUTH_TOKEN : long-lived OAuth token (sk-ant-oat01-..., 1 year)
#                             Generate at: claude.ai/settings β†’ "Claude Code" β†’ "Create token"
#   ANTHROPIC_API_KEY       : API key mode (sk-ant-api03-..., pay-per-use)
export CLAUDE_CODE_OAUTH_TOKEN="${CLAUDE_CODE_OAUTH_TOKEN:-}"
export ANTHROPIC_API_KEY="${ANTHROPIC_API_KEY:-}"

mkdir -p "${PAPERCLIP_HOME}"

# Auth secrets (generate + persist so they survive restarts)
AUTH_SECRET_FILE="${PAPERCLIP_HOME}/.auth-secret"
if [ -z "${BETTER_AUTH_SECRET:-}" ]; then
    if [ -f "${AUTH_SECRET_FILE}" ]; then
        export BETTER_AUTH_SECRET=$(cat "${AUTH_SECRET_FILE}")
    else
        export BETTER_AUTH_SECRET=$(openssl rand -base64 32)
        echo "${BETTER_AUTH_SECRET}" > "${AUTH_SECRET_FILE}"
        chmod 600 "${AUTH_SECRET_FILE}"
    fi
fi

JWT_SECRET_FILE="${PAPERCLIP_HOME}/.jwt-secret"
if [ -z "${PAPERCLIP_AGENT_JWT_SECRET:-}" ]; then
    if [ -f "${JWT_SECRET_FILE}" ]; then
        export PAPERCLIP_AGENT_JWT_SECRET=$(cat "${JWT_SECRET_FILE}")
    else
        export PAPERCLIP_AGENT_JWT_SECRET=$(openssl rand -base64 32)
        echo "${PAPERCLIP_AGENT_JWT_SECRET}" > "${JWT_SECRET_FILE}"
        chmod 600 "${JWT_SECRET_FILE}"
    fi
fi

# ── Validate LLM providers ───────────────────────────────────────────────────
if [ -z "${GEMINI_API_KEY:-}" ] && [ -z "${ANTHROPIC_API_KEY:-}" ] && [ -z "${CLAUDE_CODE_OAUTH_TOKEN:-}" ] && [ -z "${OPENAI_API_KEY:-}" ]; then
    echo "⚠️  WARNING: No LLM provider configured"
    echo "   Set at least one of: GEMINI_API_KEY, CLAUDE_CODE_OAUTH_TOKEN, ANTHROPIC_API_KEY, OPENAI_API_KEY"
    echo "   Agents will fail to run without an LLM provider"
    echo ""
fi

# ── Banner ────────────────────────────────────────────────────────────────────
echo ""
echo "  ╔════════════════════════════════════╗"
echo "  β•‘          HuggingClip               β•‘"
echo "  β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•"
echo ""
echo "Public host  : ${SPACE_HOST:-not detected}"
echo "Public URL   : ${PAPERCLIP_PUBLIC_URL:-http://localhost:${PORT}}"
echo "App port     : ${PORT}"
echo "Deploy mode  : ${PAPERCLIP_DEPLOYMENT_MODE}"
echo "Sync every   : ${SYNC_INTERVAL}s"
echo ""

# ── PostgreSQL ────────────────────────────────────────────────────────────────
PG_VERSION=$(ls /usr/lib/postgresql/ 2>/dev/null | sort -V | tail -1)
if [ -z "$PG_VERSION" ]; then
    echo "ERROR: PostgreSQL not found"
    exit 1
fi
PG_DATA="/var/lib/postgresql/${PG_VERSION}/main"

if [ ! -f "${PG_DATA}/PG_VERSION" ]; then
    echo "Initializing PostgreSQL cluster..."
    pg_createcluster "${PG_VERSION}" main --locale=C.UTF-8 >/dev/null 2>&1
fi

if ! pg_ctlcluster "${PG_VERSION}" main status 2>/dev/null | grep -q "online"; then
    echo "Starting PostgreSQL..."
    pg_ctlcluster "${PG_VERSION}" main start >/dev/null 2>&1
fi

until pg_isready -h localhost -U postgres >/dev/null 2>&1; do
    sleep 1
done

# Generate random DB password on first run (don't hardcode 'paperclip')
DB_PASSWORD_FILE="${PAPERCLIP_HOME}/.db-password"
if [ ! -f "${DB_PASSWORD_FILE}" ]; then
    DB_PASSWORD=$(openssl rand -hex 24)
    echo "$DB_PASSWORD" > "${DB_PASSWORD_FILE}"
    chmod 600 "${DB_PASSWORD_FILE}"
else
    DB_PASSWORD=$(cat "${DB_PASSWORD_FILE}")
fi
export PGPASSWORD="${DB_PASSWORD}"

su - postgres -c "psql -c \"ALTER USER postgres WITH PASSWORD '${DB_PASSWORD}';\"" >/dev/null 2>&1 || true
su - postgres -c "psql -tc \"SELECT 1 FROM pg_database WHERE datname = 'paperclip'\" | grep -q 1 || psql -c \"CREATE DATABASE paperclip OWNER postgres;\"" >/dev/null 2>&1 || true

# Update DATABASE_URL with generated password (if not explicitly set)
# URL-encode the password to handle special chars (e.g. / + = from old base64 passwords)
if [[ "$DATABASE_URL" == *"postgres:paperclip"* ]]; then
    DB_PASSWORD_ENCODED=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1], safe=''))" "${DB_PASSWORD}")
    export DATABASE_URL="postgres://postgres:${DB_PASSWORD_ENCODED}@localhost:5432/paperclip"
fi

echo "PostgreSQL ready (v${PG_VERSION})"

# ── Restore from HF Dataset ───────────────────────────────────────────────────
SYNC_STATUS_FILE="/tmp/sync-status.json"
if [ -n "${HF_TOKEN:-}" ]; then
    echo "Restoring persisted data from HF Dataset..."
    python3 /app/paperclip-sync.py restore 2>&1 || true

    # Re-stamp .db-password with current session's password so restore can't
    # overwrite it with an old base64 value that breaks DATABASE_URL next restart
    echo "${DB_PASSWORD}" > "${DB_PASSWORD_FILE}"
    chmod 600 "${DB_PASSWORD_FILE}"

    # Update PostgreSQL password to match (restore may have re-created the DB)
    su - postgres -c "psql -c \"ALTER USER postgres WITH PASSWORD '${DB_PASSWORD}';\"" >/dev/null 2>&1 || true

    # Check if last sync failed
    if [ -f "${SYNC_STATUS_FILE}" ]; then
        LAST_ERROR=$(python3 -c "import json; f=open('${SYNC_STATUS_FILE}'); d=json.load(f); print(d.get('last_error') or '')" 2>/dev/null || true)
        if [ -n "$LAST_ERROR" ]; then
            echo "⚠️  WARNING: Last backup sync failed: $LAST_ERROR"
            echo "   Data may not be persisted to HF Dataset"
        fi
    fi
else
    echo "HF_TOKEN not set β€” running without backup persistence"
fi

# ── Cloudflare Proxy ──────────────────────────────────────────────────────────
if [ -n "${CLOUDFLARE_WORKERS_TOKEN:-}" ]; then
    echo "Setting up Cloudflare proxy..."
    python3 /app/cloudflare-proxy-setup.py 2>&1 || echo "Cloudflare setup failed, continuing without proxy"
fi

# Source CF proxy env if the setup script wrote it (provides CLOUDFLARE_PROXY_URL + SECRET)
_CF_ENV="/tmp/huggingclaw-cloudflare-proxy.env"
if [ -f "${_CF_ENV}" ]; then
    # shellcheck source=/dev/null
    . "${_CF_ENV}"
fi

# ── Cloudflare proxy flag (applied inline to Paperclip only, not exported globally)
# Only enable if proxy is actually configured. Otherwise agent CLIs (claude, gemini,
# codex) inherit it via subprocess env and break their HTTP requests.
_CF_NODE_OPTS=""
if [ -n "${CLOUDFLARE_PROXY_URL:-}" ] && [ -f /app/cloudflare-proxy.js ]; then
    _CF_NODE_OPTS="--require /app/cloudflare-proxy.js"
fi

# ── Gemini CLI environment ───────────────────────────────────────────────────
# Disable sandbox (would try to start Docker inside Docker)
export GEMINI_SANDBOX=false
# Trust the workspace β€” paperclip user runs from /app/paperclip (root-owned).
export GEMINI_CLI_TRUST_WORKSPACE=true
# Kill-switch for relaunch.ts::relaunchAppInChildProcess() β€” the spawn inside
# fails when Paperclip pipes gemini's stdio (IPC channel setup fails).
# With this set, relaunchAppInChildProcess() returns early and gemini runs
# as the main process without spawning a child.
export GEMINI_CLI_NO_RELAUNCH=1

# ── Background sync loop ──────────────────────────────────────────────────────
if [ -n "${HF_TOKEN:-}" ]; then
    (
        while true; do
            sleep "$SYNC_INTERVAL"
            python3 /app/paperclip-sync.py sync 2>&1 || true
        done
    ) &
    SYNC_PID=$!
else
    SYNC_PID=""
fi

# ── Health server ─────────────────────────────────────────────────────────────
node /app/health-server.js &
HEALTH_PID=$!

if [ -n "${CLOUDFLARE_WORKERS_TOKEN:-}" ]; then
  echo "Setting up Cloudflare KeepAlive monitor..."
  python3 /app/cloudflare-keepalive-setup.py || true
fi

sleep 2

# ── Paperclip instance config ─────────────────────────────────────────────────
cd /app/paperclip

if [ ! -d "node_modules" ]; then
    echo "Installing Paperclip dependencies..."
    pnpm install 2>&1 | tail -5 || npm install 2>&1 | tail -5
fi

if [ ! -f "${PAPERCLIP_CONFIG}" ]; then
    echo "Creating instance config (first boot)..."
    mkdir -p "$(dirname "${PAPERCLIP_CONFIG}")"
    python3 <<'PYEOF'
import json, os

home = os.environ.get("PAPERCLIP_HOME", "/paperclip")
port = int(os.environ.get("PORT", "3100"))
public_url = os.environ.get("PAPERCLIP_PUBLIC_URL", f"http://localhost:{port}")

config = {
    "$meta": {"version": 1, "updatedAt": "2024-01-01T00:00:00Z", "source": "onboard"},
    "llm": {"provider": "claude", "apiKey": ""},
    "database": {
        "mode": "postgres",
        "connectionString": os.environ.get("DATABASE_URL", "postgres://postgres:paperclip@localhost:5432/paperclip")
    },
    "logging": {"mode": "file", "logDir": f"{home}/instances/default/logs"},
    "server": {
        "deploymentMode": os.environ.get("PAPERCLIP_DEPLOYMENT_MODE", "authenticated"),
        "exposure": os.environ.get("PAPERCLIP_DEPLOYMENT_EXPOSURE", "private"),
        "host": "0.0.0.0",
        "port": port,
        "allowedHostnames": [],
        "serveUi": True
    },
    "auth": {
        "baseUrlMode": "explicit",
        "publicBaseUrl": public_url,
        "disableSignUp": False
    },
    "storage": {
        "provider": "local_disk",
        "localDisk": {"baseDir": f"{home}/instances/default/data/storage"}
    },
    "secrets": {
        "provider": "local_encrypted",
        "strictMode": False,
        "localEncrypted": {"keyFilePath": f"{home}/instances/default/secrets/master.key"}
    },
    "telemetry": {"enabled": False}
}

config_path = os.environ.get("PAPERCLIP_CONFIG", f"{home}/instances/default/config.json")
os.makedirs(os.path.dirname(config_path), exist_ok=True)
with open(config_path, "w") as f:
    json.dump(config, f, indent=2)
print(f"  Config written to {config_path}")
PYEOF
fi

# ── Graceful shutdown ─────────────────────────────────────────────────────────
cleanup() {
    echo "Shutting down β€” syncing data..."

    # Stop services
    [ -n "${HEALTH_PID:-}" ]    && kill "$HEALTH_PID"    2>/dev/null || true
    [ -n "${PAPERCLIP_PID:-}" ] && kill "$PAPERCLIP_PID" 2>/dev/null || true

    # Kill background sync loop, then wait for it to exit before running final sync
    # (avoids concurrent writes: kill stops the loop, wait confirms it's done)
    if [ -n "${SYNC_PID:-}" ]; then
        kill "$SYNC_PID" 2>/dev/null || true
        wait "$SYNC_PID" 2>/dev/null || true
    fi

    # Run final backup sync
    if [ -n "${HF_TOKEN:-}" ]; then
        python3 /app/paperclip-sync.py sync 2>&1 || true
    fi

    exit 0
}
trap cleanup SIGTERM SIGINT

# ── Codex API key config ─────────────────────────────────────────────────────
# forced_login_method="api" alone isn't enough β€” codex reads the key from its
# credentials store, not from OPENAI_API_KEY env var (which Paperclip may not
# pass to subprocesses). Workaround: custom provider with experimental_bearer_token
# baked in. Can't use [model_providers.openai] β€” reserved built-in ID.
if [ -n "${OPENAI_API_KEY:-}" ]; then
    mkdir -p /home/paperclip/.codex
    cat > /home/paperclip/.codex/config.toml <<TOMLEOF
forced_login_method = "api"
model_provider = "openai-hf"

[model_providers.openai-hf]
name = "OpenAI"
base_url = "https://api.openai.com/v1"
experimental_bearer_token = "${OPENAI_API_KEY}"
requires_openai_auth = false
TOMLEOF
    chmod 600 /home/paperclip/.codex/config.toml
    chown -R paperclip:paperclip /home/paperclip/.codex
fi

# ── Ensure paperclip user owns runtime dirs ──────────────────────────────────
chown -R paperclip:paperclip /app /paperclip 2>/dev/null || true

# ── Launch Paperclip as non-root ──────────────────────────────────────────────
# Agent CLIs (claude, gemini, codex) refuse --dangerously-skip-permissions as root.
# Run Paperclip as 'paperclip' user so all spawned subprocesses are non-root.
echo "Starting Paperclip..."
HOME=/home/paperclip NODE_OPTIONS="${_CF_NODE_OPTS}" runuser -u paperclip -- \
    node --import ./server/node_modules/tsx/dist/loader.mjs server/dist/index.js &
PAPERCLIP_PID=$!

# Wait for API ready (max 90s)
PAPERCLIP_READY=false
for i in $(seq 1 45); do
    if curl -sf http://127.0.0.1:3100/api/health >/dev/null 2>&1; then
        echo "Paperclip ready (${i}s)"
        PAPERCLIP_READY=true
        break
    fi
    sleep 2
done

if [ "$PAPERCLIP_READY" = true ]; then
    BOOTSTRAP_OUTPUT=$(HOME=/home/paperclip runuser -u paperclip -- pnpm paperclipai auth bootstrap-ceo 2>&1 || true)
    INVITE_URL=$(echo "$BOOTSTRAP_OUTPUT" | grep "Invite URL:" 2>/dev/null | sed 's/\x1B\[[0-9;]*[a-zA-Z]//g' | grep -o 'https\?://[^ ]*' | head -1 || true)
    if [ -n "$INVITE_URL" ]; then
        echo "$INVITE_URL" > /tmp/invite-url.txt
        echo ""
        echo "  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”"
        echo "  β”‚  ADMIN SETUP β€” open this URL in your browser:       β”‚"
        echo "  β”‚                                                     β”‚"
        echo "  β”‚  ${INVITE_URL}"
        echo "  β”‚                                                     β”‚"
        echo "  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜"
        echo ""
    else
        rm -f /tmp/invite-url.txt
        echo "Admin account already configured"
    fi


else
    echo "Warning: Paperclip did not become ready in 90s"
fi

echo "HuggingClip is ready!"
echo ""
echo "  Health dashboard : http://localhost:7861/"
echo "  Paperclip UI     : http://localhost:7861/app/"
echo "  API              : http://localhost:7861/api/"
echo ""

wait $PAPERCLIP_PID