naver-blog-writer-skill

star 8

이 스킬은 ACP/OpenClaw 환경에서 네이버 블로그 발행을 자동화합니다.

modbender By modbender schedule Updated 3/6/2026

name: Naver Blog Writer Skill description: 이 스킬은 ACP/OpenClaw 환경에서 네이버 블로그 발행을 자동화합니다.

Naver Blog Writer Skill

이 스킬은 ACP/OpenClaw 환경에서 네이버 블로그 발행을 자동화합니다.

Important

  • ClawHub 배포본에는 보통 SKILL.md만 포함됩니다.
  • 따라서 tools/preflight, tools/publish 같은 로컬 파일 경로를 전제로 실행하지 마세요.
  • 항상 아래 Inline Runner 절차를 사용하세요.

Billing Prerequisites

  • 이 스킬은 ACP 유료 오퍼링(naver-blog-writer)입니다.
  • 결제/사용 승인 후에만 온보딩 증빙이 생성됩니다.
  • 온보딩 증빙(둘 중 하나 필요):
    • SETUP_URL
    • PROOF_TOKEN + SETUP_ISSUE_URL
  • 결제 컨텍스트가 없으면 다음 형식으로 실패를 반환하세요:
{
  "error": "RUNNER_NOT_READY",
  "setup_url": null,
  "next_action": "PURCHASE_AND_GET_SETUP_CREDENTIALS",
  "hint": "Purchase/authorize this ACP offering first, then provide SETUP_URL or PROOF_TOKEN + SETUP_ISSUE_URL.",
  "missing": ["ACP_PAYMENT_CONTEXT", "SETUP_URL_OR_PROOF_TOKEN_AND_SETUP_ISSUE_URL"]
}

Runtime Config

  • OPENCLAW_OFFERING_ID (default: naver-blog-writer)
  • X_LOCAL_TOKEN (optional; 없으면 ~/.config/naver-thin-runner/config.json에서 자동 로드)
  • LOCAL_DAEMON_PORT (default: 19090)
  • DEVICE_FINGERPRINT (default: hostname-platform-arch)
  • OPENCLAW_OFFERING_EXECUTE_URL (+ optional OPENCLAW_CORE_API_KEY)
  • fallback: CONTROL_PLANE_URL + ACP_ADMIN_API_KEY

Inline Runner (Always Use This)

아래 스크립트를 실행해 게시하세요.
입력: TITLE, BODY, optional TAGS (comma separated).

bash -s -- --title "제목" --body "본문" --tags "tag1,tag2" <<'BASH'
set -euo pipefail

TITLE=''
BODY=''
TAGS=''
while [[ $# -gt 0 ]]; do
  case "$1" in
    --title) TITLE="$2"; shift 2 ;;
    --body) BODY="$2"; shift 2 ;;
    --tags) TAGS="$2"; shift 2 ;;
    *) echo "unknown arg: $1" >&2; exit 1 ;;
  esac
done

if [[ -z "$TITLE" || -z "$BODY" ]]; then
  echo '{"error":"INVALID_INPUT","next_action":"PROVIDE_TITLE_AND_BODY","hint":"title and body are required"}'
  exit 1
fi

SETUP_URL="${SETUP_URL:-}"
PROOF_TOKEN="${PROOF_TOKEN:-}"
SETUP_ISSUE_URL="${SETUP_ISSUE_URL:-}"
DEVICE_FINGERPRINT="${DEVICE_FINGERPRINT:-$(hostname)-$(uname -s)-$(uname -m)}"
LOCAL_DAEMON_PORT="${LOCAL_DAEMON_PORT:-19090}"
X_LOCAL_TOKEN="${X_LOCAL_TOKEN:-${THIN_RUNNER_LOCAL_TOKEN:-}}"
OFFERING_ID="${OPENCLAW_OFFERING_ID:-naver-blog-writer}"
OFFERING_EXECUTE_URL="${OPENCLAW_OFFERING_EXECUTE_URL:-}"
OFFERING_API_KEY="${OPENCLAW_CORE_API_KEY:-}"
OFFERING_API_KEY_HEADER="${OPENCLAW_CORE_API_KEY_HEADER:-x-api-key}"
CONTROL_PLANE_URL="${CONTROL_PLANE_URL:-${ACP_RUNNER_SERVER_URL:-}}"
ADMIN_KEY_RAW="${ACP_ADMIN_API_KEY:-${ACP_CONTROL_PLANE_API_KEYS:-}}"
ADMIN_KEY="${ADMIN_KEY_RAW%%,*}"

json_error() {
  local code="$1"; local action="$2"; local hint="$3"; local setup="${4:-}"
  node - "$code" "$action" "$hint" "$setup" <<'NODE'
const code = process.argv[2];
const action = process.argv[3];
const hint = process.argv[4];
const setup = process.argv[5] || '';
console.log(JSON.stringify({
  error: code,
  setup_url: setup || null,
  next_action: action,
  hint,
}, null, 2));
NODE
}

read_local_token() {
  node - <<'NODE'
const fs = require('node:fs');
const p = `${process.env.HOME}/.config/naver-thin-runner/config.json`;
try {
  const j = JSON.parse(fs.readFileSync(p, 'utf8'));
  process.stdout.write(typeof j.localToken === 'string' ? j.localToken.trim() : '');
} catch {
  process.stdout.write('');
}
NODE
}

runner_cmd() {
  if npx @y80163442/naver-thin-runner --help >/dev/null 2>&1; then
    npx @y80163442/naver-thin-runner "$@"
  else
    echo '{"error":"RUNNER_CLI_NOT_FOUND","next_action":"INSTALL_THIN_RUNNER","hint":"Install @y80163442/naver-thin-runner first"}'
    exit 1
  fi
}

issue_setup_url() {
  if [[ -n "$SETUP_URL" ]]; then
    printf '%s' "$SETUP_URL"; return 0
  fi
  if [[ -z "$PROOF_TOKEN" || -z "$SETUP_ISSUE_URL" ]]; then
    printf ''; return 0
  fi
  local payload
  payload="$(node - "$PROOF_TOKEN" "$DEVICE_FINGERPRINT" <<'NODE'
const proof = process.argv[2];
const fp = process.argv[3];
process.stdout.write(JSON.stringify({ proof_token: proof, device_fingerprint: fp }));
NODE
)"
  curl -sS -X POST -H 'content-type: application/json' -d "$payload" "$SETUP_ISSUE_URL" \
    | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{const j=JSON.parse(d);process.stdout.write(j.setup_url||'')}catch{process.stdout.write('')}});"
}

if [[ -z "$X_LOCAL_TOKEN" ]]; then
  X_LOCAL_TOKEN="$(read_local_token)"
fi

identity_tmp="$(mktemp)"
seal_tmp="$(mktemp)"
exec_tmp="$(mktemp)"
trap 'rm -f "$identity_tmp" "$seal_tmp" "$exec_tmp"' EXIT

call_identity() {
  curl -sS --max-time 5 -o "$identity_tmp" -w '%{http_code}' \
    -H "x-local-token: ${X_LOCAL_TOKEN}" \
    "http://127.0.0.1:${LOCAL_DAEMON_PORT}/v1/local/identity" || true
}

http_code="$(call_identity)"
if [[ "$http_code" != "200" ]]; then
  # try daemon boot once
  runner_cmd daemon start --port "$LOCAL_DAEMON_PORT" >/tmp/naver-thin-runner-daemon.log 2>&1 &
  sleep 2
  http_code="$(call_identity)"
fi

if [[ "$http_code" != "200" ]]; then
  setup_url="$(issue_setup_url)"
  if [[ -z "$setup_url" && ( -z "$PROOF_TOKEN" || -z "$SETUP_ISSUE_URL" ) ]]; then
    json_error \
      "RUNNER_NOT_READY" \
      "PURCHASE_AND_GET_SETUP_CREDENTIALS" \
      "Purchase/authorize ACP offering and provide SETUP_URL or PROOF_TOKEN + SETUP_ISSUE_URL." \
      ""
    exit 2
  fi

  if [[ -n "$setup_url" ]]; then
    runner_cmd setup --setup-url "$setup_url" --auto-service both
  else
    runner_cmd setup --proof-token "$PROOF_TOKEN" --setup-issue-url "$SETUP_ISSUE_URL" --device-fingerprint "$DEVICE_FINGERPRINT" --auto-service both
  fi

  # first-time login (human action in browser)
  runner_cmd login
  X_LOCAL_TOKEN="$(read_local_token)"
  runner_cmd daemon start --port "$LOCAL_DAEMON_PORT" >/tmp/naver-thin-runner-daemon.log 2>&1 &
  sleep 2
  http_code="$(call_identity)"
fi

if [[ "$http_code" != "200" ]]; then
  json_error "RUNNER_NOT_READY" "CHECK_LOCAL_DAEMON" "Local daemon identity failed after setup/login retry." ""
  exit 2
fi

seal_payload="$(node - "$TITLE" "$BODY" "$TAGS" <<'NODE'
const title = process.argv[2] || '';
const body = process.argv[3] || '';
const tags = (process.argv[4] || '').split(',').map(v => v.trim()).filter(Boolean);
process.stdout.write(JSON.stringify({ title, body_markdown: body, tags, publish_at: null }));
NODE
)"

seal_code="$({
  curl -sS -o "$seal_tmp" -w '%{http_code}' \
    -X POST \
    -H 'content-type: application/json' \
    -H "x-local-token: ${X_LOCAL_TOKEN}" \
    -d "$seal_payload" \
    "http://127.0.0.1:${LOCAL_DAEMON_PORT}/v1/local/seal-job"
} || true)"

if [[ "$seal_code" != "200" ]]; then
  cat "$seal_tmp"; exit 1
fi

requirement_payload="$(node - "$(cat "$identity_tmp")" "$(cat "$seal_tmp")" <<'NODE'
const identity = JSON.parse(process.argv[2]);
const sealed = JSON.parse(process.argv[3]);
process.stdout.write(JSON.stringify({
  runner_attestation: identity.runner_attestation,
  sealed_payload: sealed.sealed_payload,
  payload_digest: sealed.payload_digest,
  idempotency_key: sealed.idempotency_key,
}));
NODE
)"

if [[ -n "$OFFERING_EXECUTE_URL" ]]; then
  execute_body="$(node - "$OFFERING_ID" "$requirement_payload" <<'NODE'
const offeringId = process.argv[2];
const requirements = JSON.parse(process.argv[3]);
process.stdout.write(JSON.stringify({ offering_id: offeringId, requirements }));
NODE
)"
  if [[ -n "$OFFERING_API_KEY" ]]; then
    curl -sS -X POST -H 'content-type: application/json' -H "${OFFERING_API_KEY_HEADER}: ${OFFERING_API_KEY}" -d "$execute_body" "$OFFERING_EXECUTE_URL"
  else
    curl -sS -X POST -H 'content-type: application/json' -d "$execute_body" "$OFFERING_EXECUTE_URL"
  fi
  exit 0
fi

if [[ -z "$CONTROL_PLANE_URL" || -z "$ADMIN_KEY" ]]; then
  json_error "EXECUTE_ENDPOINT_NOT_CONFIGURED" "CONFIGURE_EXECUTE_ENDPOINT" "Set OPENCLAW_OFFERING_EXECUTE_URL or CONTROL_PLANE_URL + ACP_ADMIN_API_KEY." ""
  exit 1
fi

curl -sS -X POST \
  -H 'content-type: application/json' \
  -H "x-api-key: ${ADMIN_KEY}" \
  -d "$requirement_payload" \
  "${CONTROL_PLANE_URL%/}/v2/jobs/dispatch-and-wait"
BASH

Agent Behavior Rules

  • RUNNER_NOT_READY면 위 Inline Runner를 통해 auto-setup/login을 먼저 시도하세요.
  • 자동 복구 후에도 실패하면 원문 JSON(error, next_action, hint)을 사용자에게 그대로 전달하세요.
  • 사람 개입이 필요한 단계는 기본적으로 네이버 로그인 1회뿐입니다.
Install via CLI
npx skills add https://github.com/modbender/skill-library-mcp --skill naver-blog-writer-skill
Repository Details
star Stars 8
call_split Forks 2
navigation Branch main
article Path SKILL.md
More from Creator