# viruagent-cli 심층 분석 보고서

> 작성: 개발4팀 (비슈누 팀장) | 분석일: 2026-03-31
> 대상: https://github.com/greekr4/viruagent-cli v0.9.2

---

## 1. 프로젝트 개요

viruagent-cli는 AI 에이전트(Claude Code)가 SNS 자동화를 수행할 수 있도록 설계된 Node.js CLI 도구다.
7개 플랫폼(네이버 블로그, 네이버 카페, 티스토리, 인스타그램, Threads, X/Twitter, Reddit)을 지원한다.

- 총 코드량: ~12,979줄 (JS 40개 파일)
- 프로덕션 의존성: 3개 (`commander`, `playwright`, `x-client-transaction-id`)
- 테스트: 0개 (테스트 파일, 린터 설정 전무)
- 라이선스: MIT
- 출력 형식: 모든 결과가 구조화된 JSON

---

## 2. 플랫폼별 API/자동화 방식 상세

### 2.1 네이버 블로그 — SE 에디터 + RabbitWrite 내부 API

**인증**: Playwright CDP로 `nid.naver.com` 로그인. `navigator.webdriver = undefined` 등 봇 탐지 우회 스크립트 주입. `NID_AUT` + `NID_SES` httpOnly 쿠키를 CDP `Network.getAllCookies`로 추출.

**발행 파이프라인**:
1. `GET PostWriteFormSeOptions.naver` → `Se-Authorization` 토큰 획득
2. `GET platform.editor.naver.com/api/blogpc001/v1/service_config` → editorId 획득
3. HTML → SE 컴포넌트 변환: `POST upconvert.editor.naver.com/blog/html/components` (1차), 로컬 HTML 파서 (폴백)
4. SE 2.9.0 문서 모델 구성 (documentTitle + text/quotation/image 컴포넌트)
5. `POST blog.naver.com/RabbitWrite.naver` — form-encoded 발행

**이미지 업로드**:
1. `GET platform.editor.naver.com/.../photo-uploader/session-key` → 세션키 획득
2. `POST blog.upphoto.naver.com/{sessionKey}/simpleUpload/0` → XML 응답 파싱 (url, width, height)
3. 이미지 소스: URL 직접 지정 또는 키워드 기반 검색 (DuckDuckGo/Wikimedia)

**카테고리**: `GET PostWriteFormManagerOptions.naver` → `categoryFormViewList[]` 파싱
**초안**: 별도 API 없음. `openType=0`(비공개)으로 publish 재사용

### 2.2 네이버 카페 — 모바일 API + SE 에디터

**Base URL**: `https://apis.naver.com/cafe-web/cafe-mobile` (모바일 API)

**카페 ID 추출**: `m.cafe.naver.com/ca-fe/{slug}` HTML에서 5단계 regex 추출 (`g_sClubId`, `clubId`, `cafeId`, `clubid=`, `cafes/`)

**가입 흐름**:
1. `CafeApplyView.json` → 가입 폼 + 캡차 필요 여부
2. 캡차 필요 시 `CaptchaValidate.json`으로 검증 (캡차 값은 외부에서 제공 필요)
3. `CafeApply.json` → 가입 완료

**글 작성**: `POST apis.cafe.naver.com/editor/v2.0/cafes/{cafeId}/menus/{menuId}/articles`
- SE 2.9.0 문서 JSON (`contentJson` 필드)
- 슬라이드(`layout: 'slide'`) / 콜라주(`layout: 'collage'`, `widthPercentage: 50`) 레이아웃 지원

**캡차 우회**: 자동 우회가 아님. `captcha_required` 상태 반환 시 사용자가 수동으로 captchaValue 제공 필요.

### 2.3 티스토리 — Playwright 로그인 + fetch API

**인증**: Playwright CDP로 Kakao OAuth 로그인 자동화. `TSSESSION` httpOnly 쿠키 추출.
- Kakao 계정 연속 확인, OTP 2FA, Kakao 앱 2FA, manual 모드(5분 대기) 지원

**API 호출** (로그인 후 모두 fetch 기반):
- 발행: `POST {blogName}.tistory.com/manage/post.json` (JSON body)
- 초안: `POST {blogName}.tistory.com/manage/drafts`
- 이미지 업로드: `POST {blogName}.tistory.com/manage/post/attach.json` (FormData)
- 카테고리: `/manage/newpost` HTML에서 `window.Config` 추출 → `vm.runInNewContext()` 실행 → 카테고리 트리 평탄화

**Rate Limit**: 일일 발행 15회 하드캡. 세션 파일에 영속적 카운터 저장.

### 2.4 인스타그램 — HTTP fetch 비공개 Web API

**인증**: `POST instagram.com/api/v1/web/accounts/login/ajax/`
- 비밀번호 형식: `#PWD_INSTAGRAM_BROWSER:0:{timestamp}:{plainPassword}` (실질적 암호화 없음)
- `IG_APP_ID = '936619743392459'` 하드코딩
- 챌린지 자동 해소: `POST /api/v1/challenge/web/action/` (choice=0)
- 2FA 미지원

**18개 API 엔드포인트**:
- 피드: `POST /api/v1/feed/timeline/`
- 좋아요: `POST /api/v1/web/likes/{mediaId}/like/`
- 댓글: `POST /api/v1/web/comments/{mediaId}/add/`
- 팔로우: `POST /api/v1/friendships/create/{userId}/`
- 이미지 업로드: `POST /rupload_igphoto/{uploadName}`
- 게시 설정: `POST /api/v1/media/configure/`
- DM: `POST i.instagram.com/api/v1/direct_v2/threads/broadcast/text/` (별도 모바일 UA + AppID `567067343352427`)

**smartComment.js**: AI 댓글 생성기가 아님. 게시물 데이터(캡션, 썸네일 base64, 인게이지먼트 수치)를 수집하여 Claude Vision이 분석 후 댓글 생성.

**Rate Limit (userId별 영속 카운터)**:
- 좋아요: 20-40초 딜레이, 시간당 15, 일 500
- 댓글: 5-7분 딜레이, 시간당 5, 일 100
- 팔로우: 60-120초 딜레이, 시간당 15, 일 250
- DM: 2-5분 딜레이, 시간당 5, 일 30
- 게시: 60-120초 딜레이, 시간당 3, 일 25

### 2.5 Threads — Barcelona (Instagram 모바일) API + IGT:2 토큰

**Base URL**: `https://i.instagram.com` (Instagram 모바일 서버 재사용)
**User-Agent**: `Barcelona 289.0.0.77.109 Android` (Threads 앱 위장)
**App ID**: `238260118697367`

**인증**: `POST i.instagram.com/api/v1/bloks/apps/com.bloks.www.bloks.caa.login.async.send_login_request/`
- 비밀번호: `#PWD_INSTAGRAM:0:{timestamp}:{plainPassword}`
- 토큰 추출: 응답 헤더 `ig-set-authorization` → 바디 regex 폴백 → `Bearer IGT:2:{token}`
- userId: 토큰 base64 디코딩 → `ds_user_id` 필드

**주요 API**:
- 텍스트 게시: `POST /api/v1/media/configure_text_only_post/` (바디: `signed_body=SIGNATURE.{JSON}`)
- 좋아요: `POST /api/v1/media/{postId}_{userId}/like/` (복합 ID)
- 답글 = 게시물 (별도 comment API 없음)
- 이미지 업로드: Instagram과 동일한 `rupload_igphoto` 엔드포인트 재사용

**환경변수**: `THREADS_USERNAME` → `INSTA_USERNAME` 폴백 (Instagram 계정 공유)

### 2.6 X(Twitter) — 내부 GraphQL API + 동적 queryId

**인증**: 프로그래밍 방식 로그인 없음. 수동으로 브라우저에서 `auth_token` + `ct0` 쿠키 추출 필요.
- 공개 Bearer Token 하드코딩: `AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs=...`
- 환경변수: `X_AUTH_TOKEN`, `X_CT0`

**동적 queryId 추출 메커니즘** (`graphqlSync.js`):
1. `GET x.com` HTML → `main.[hash].js` 파일명 추출
2. `GET abs.twimg.com/.../main.[hash].js` 다운로드
3. 정규식으로 166개 GraphQL 오퍼레이션 파싱 (queryId, featureSwitches, fieldToggles)
4. 2단계 캐시: 메모리 Map + `~/.viruagent-cli/x-graphql-cache.json` (1시간 TTL)
5. GraphQL "Could not resolve" 에러 시 캐시 무효화 + 재동기화

**API 호출**:
- GraphQL: `https://x.com/i/api/graphql/{queryId}/{operationName}` (읽기: GET, 뮤테이션: POST)
- 팔로우/언팔로우: REST v1.1 `POST /i/api/1.1/friendships/create.json`
- 미디어 업로드: INIT → APPEND → FINALIZE 3단계 청크 (`upload.x.com/i/media/upload.json`)
- URL 2000자 초과 시 GET → POST 자동 전환
- `x-client-transaction-id`: npm 패키지로 요청별 동적 생성

**Rate Limit**: 트윗 시간당 10/일 50, 좋아요 시간당 15/일 200, 팔로우 시간당 10/일 100. 226 에러(자동화 감지) 시 12-48시간 쿨다운 필수.

### 2.7 Reddit — OAuth2 공식 API

**3가지 인증 방식**:
1. **OAuth2 Password Grant** (권장): `POST reddit.com/api/v1/access_token` (Basic Auth)
2. **Cookie Login** (레거시): `POST old.reddit.com/api/login` → `reddit_session` + `modhash`
3. **Browser Login**: Playwright persistent context (CDP 차단되므로 chromeManager 미사용)

**authMode별 라우팅**: OAuth → `oauth.reddit.com` + Bearer, Browser(token_v2) → `oauth.reddit.com` + Bearer, Cookie → `old.reddit.com` + Cookie + modhash

**주요 API**: `submit`, `comment`, `vote`, `subscribe`, `search`, `del` 등 — 모두 공식 엔드포인트

**Rate Limit**: 게시 10분 딜레이/시간당 2/일 10, 댓글 2분 딜레이/시간당 6/일 50, 투표 10초 딜레이/시간당 30/일 500

---

## 3. 스킬 시스템 분석

### 3.1 스킬 구조

모든 스킬은 `skills/{prefix}-{name}/SKILL.md` 패턴. YAML frontmatter로 메타데이터 정의.

**3가지 prefix 체계**:
- `va-` (원자 스킬): 단일 플랫폼의 단일 기능. 선행 조건, 실행 명령어, 파라미터 테이블, 에러 처리, 예시 포함.
- `persona-` (역할): 특정 역할의 행동 양식 + 복수 va- 스킬 조합 정의 (blogger, influencer-manager, sns-marketer)
- `recipe-` (워크플로우): 여러 va- 스킬의 순서적 조합. `metadata.requires.skills`로 의존 명시 (blog-publish, cross-post, daily-engagement, grow-followers)

### 3.2 라우팅 메커니즘

`va-shared/SKILL.md`가 라우터 스킬. Claude Code `/viruagent` 슬래시 커맨드로 설치.
- 트리거 키워드 감지 → 적절한 서브 스킬 파일 경로 지시
- `SKILLS_DIR` 플레이스홀더를 설치 시 실제 경로로 치환
- 지연 로딩: 라우터 먼저 로드, 서브 스킬은 On-Demand 로드

### 3.3 개발 파이프라인 (.claude/ 디렉토리)

viruagent-orchestrator 스킬이 AdVooster(Electron 마케팅 도구) → viruagent-cli 포팅 파이프라인을 조율:
```
advooster-analyzer → provider-builder → skill-writer → qa-verifier
```

5개 에이전트 역할:
- `web-reverser`: JS 번들 역공학 + API 리버스 엔지니어링
- `advooster-analyzer`: Python 코드에서 API/인증/로직 추출
- `provider-builder`: 기존 패턴 준수하여 새 프로바이더 생성
- `skill-writer`: 새 기능용 SKILL.md 생성
- `qa-verifier`: CLI 명령 실행 테스트 + 코드 구조 검증

### 3.4 우리 스킬 시스템과의 비교

| 항목 | viruagent-cli | 우리 시스템 |
|------|--------------|------------|
| 스킬 파일 위치 | `skills/` + `.claude/skills/` | `/home/jay/.claude/skills/` |
| 스킬 포맷 | YAML frontmatter + markdown | markdown only |
| 분류 체계 | va-/persona-/recipe- prefix | 기능별 분류 |
| 라우팅 | 라우터 스킬이 키워드 매칭 | 시스템 프롬프트에서 직접 매칭 |
| 설치 | CLI `install-skill` 명령 | 수동 파일 배치 |
| 버전 관리 | package.json 버전 연동 | 없음 |

---

## 4. 세션/인증 관리

### 4.1 파일 기반 세션 저장소

```
~/.viruagent-cli/
├── providers.json              # 메타데이터 (loggedIn, lastValidatedAt)
├── sessions/
│   ├── {provider}-session.json        # 기본 계정
│   └── {provider}-{account}-session.json  # 멀티 계정
└── x-graphql-cache.json        # X queryId 캐시
```

### 4.2 플랫폼별 세션 포맷

- **Naver/Tistory**: `{ cookies: [...], updatedAt }` (httpOnly 쿠키 포함)
- **Instagram**: `{ cookies: [...], rateLimits: { [userId]: {...} }, updatedAt }` (필수: sessionid, csrftoken, ds_user_id)
- **Threads**: `{ token: "IGT:2:...", userId, deviceId, rateLimits, updatedAt }`
- **X**: `{ cookies: [...], updatedAt }` (필수: auth_token, ct0)
- **Reddit OAuth**: `{ authMode: "oauth", accessToken, expiresAt, username, rateLimits, updatedAt }`
- **Reddit Browser**: `{ authMode: "browser", cookies: [...], username, rateLimits, updatedAt }`

### 4.3 세션 만료/갱신

모든 플랫폼에서 `createXxxWithProviderSession()` 래퍼가 에러 메시지 패턴 매칭(`session expired`, `401`, `403` 등) → 자동 재로그인 시도.
Reddit OAuth는 `expiresAt - 60000` 선제 갱신.

---

## 5. 속도 제한 및 안전장치

### 5.1 구현 패턴

`withDelay(type, fn)` 함수가 공통 패턴:
1. 카운터 한도 확인 (시간당/일일)
2. 최소 딜레이 미충족 시 랜덤 지연 (`min + random * (max - min)`)
3. 액션 실행
4. 카운터 증가 + 세션 파일 영속 저장

### 5.2 플랫폼별 차이

- **Instagram/Threads/X/Reddit**: 자체 withDelay + 카운터 완비
- **Tistory**: 일일 발행 15회 하드캡만 (딜레이 없음)
- **Naver**: 코드 레벨 rate limit 전무 (서버 429/403 대응만)

### 5.3 밴 방지 전략

- 봇 탐지 우회: `navigator.webdriver = undefined`, 가짜 plugins/chrome 객체 주입 (Naver, Tistory)
- 랜덤 지연: 최소-최대 범위 내 균등 분포
- Reddit: CDP 차단 대응으로 `persistent context` + `--disable-blink-features=AutomationControlled`
- X: 226 에러(자동화 감지) 시 수동 쿨다운 안내

---

## 6. 이미지 처리

### 6.1 이미지 검색 파이프라인

우선순위 순서:
1. **DuckDuckGo**: vqd 토큰 스크래핑 → `/i.js` API (4개 URL 시도) → 이미지 URL 추출
2. **Wikimedia Commons**: `commons.wikimedia.org/w/api.php` 공식 API (gsrsearch, namespace=6)
3. **LoremFlickr**: `loremflickr.com/1200/800/{keyword}`
4. **Picsum**: `picsum.photos/seed/{md5}/1200/800`
5. **Placeholder**: `placehold.co`, `via.placeholder.com`, `dummyimage.com`

각 단계에서 6개 후보 이미지가 채워지면 중단.

### 6.2 저작권 처리 부재

코드베이스 전체에서 `copyright`, `license`, `attribution`, `저작권`, `라이선스` 키워드 0건.
- DuckDuckGo 결과의 저작권 보호 이미지 무단 사용 가능
- Wikimedia CC BY-SA 이미지의 저작자 표시 의무 미이행
- 상업적 블로그 운영 시 저작권법 위반 리스크

---

## 7. 코드 품질 분석

### 7.1 긍정 요소

- CommonJS 일관 사용, 팩토리 함수 패턴 6개 프로바이더에 통일
- 에러 객체 구조화 (`{ ok, error, message, hint }`)
- 모든 출력 JSON 구조화 — AI 통합에 적합
- `--spec` 자가 문서화, `--dry-run` 테스트 지원
- 프로덕션 의존성 3개로 매우 가벼움

### 7.2 부정 요소

- 테스트 전무 (0개 파일, CI에 테스트 단계 없음)
- 린터/포매터 없음 (ESLint, Prettier 미설정)
- TypeScript 미사용 (덕 타이핑 의존)
- 빈 catch 블록 다수 (에러 무시)
- 매직 넘버/하드코딩 상수 다수

### 7.3 주요 버그 3건

1. **imageUploadLimit 무시**: `tistory/imageEnrichment.js`에서 내부 상수가 사용자 옵션을 덮어쓸 수 있음
2. **getPost() 비효율**: `tistoryApiClient.js`에서 전체 목록 조회 후 클라이언트 측 필터링
3. **Naver content 유실**: `naverApiClient.js:313`에서 HTML 문자열이 전달되면 빈 배열 처리

### 7.4 아키텍처 결함

- **크로스 프로바이더 의존**: `naver/imageUpload.js`가 `tistory/imageSources.js`를 직접 import (모듈 경계 위반)
- **인터페이스 계약 부재**: 프로바이더 메서드 계약이 명시적으로 정의되지 않음

---

## 8. 보안 분석

### Critical (3건)
- **C-1**: 세션 파일 평문 저장 (파일 권한 0o666, 암호화 없음)
- **C-2**: X Bearer Token 소스코드 + npm 공개 배포
- **C-3**: Instagram/Threads 앱 ID 하드코딩 + 공개 배포

### High (4건)
- **H-1**: httpOnly 쿠키 CDP 강제 추출 (Kakao SSO 토큰 포함)
- **H-2**: CLI 인수 비밀번호가 프로세스 메모리/ps/cmdline에 노출
- **H-3**: Instagram 비밀번호 실질적 암호화 없이 형식만 모방
- **H-4**: `vm.runInNewContext()`로 서버 응답 HTML 내 JS 코드 직접 실행

### Medium (3건)
- **M-1**: X GraphQL 캐시 파일 무결성 검증 없음
- **M-2**: 타임아웃 처리 불완전 (abort 후 response.json() 에러 불균일)
- **M-3**: 임시 파일 TOCTOU 경쟁 조건

---

## 9. ToS 위반 리스크

| 플랫폼 | 위험도 | 핵심 위반 |
|--------|--------|----------|
| Instagram | **상** | 비공개 Web API, 앱 ID 스푸핑, 자동화 금지 조항 |
| Threads | **상** | Android 앱 API 스푸핑, 비공개 Bloks API, Meta ToS |
| X(Twitter) | **상** | 내부 GraphQL, main.js 파싱 queryId 추출, Bearer Token |
| Tistory | **중** | Kakao OAuth CDP 자동화, 내부 관리 API |
| Naver | **중** | SE 에디터 내부 API 리버스 엔지니어링, 봇 탐지 우회 |
| Reddit | **하** | OAuth2 공식 API 사용 (rate limit 준수 필요) |

**법적 리스크**: Meta는 자동화 도구에 대한 소송 전례 있음. X는 2023년부터 법적 조치 강화. 계정 영구 정지, IP 차단, 법적 조치 가능.

---

## 10. CLI 아키텍처

### 10.1 명령 흐름

```
bin/index.js (commander) → src/runner.js (switch 디스패치) → providerManager → provider/index.js → apiClient/auth/session
```

### 10.2 전체 커맨드 (40개+)

```
status, login, logout, publish, save-draft,
list-categories, list-posts, read-post,
get-profile, get-feed, like, unlike, comment,
follow, unfollow, like-comment, unlike-comment,
send-dm, list-messages, list-comments,
analyze-post, resolve-challenge, rate-limit-status,
search, retweet, unretweet, delete, delete-post,
subscribe, unsubscribe, sync-operations,
cafe-id, cafe-join, cafe-list, cafe-write,
list-providers, install-skill
```

### 10.3 AI 통합 설계

- `--spec`: 전체 커맨드 JSON 스키마 자가 문서화
- `--dry-run`: write 커맨드 실행 없이 파라미터 확인
- `--account`: 멀티 계정 지원
- 모든 출력: `{ ok: true/false, provider, ... }` JSON 구조
