viruagent-cli 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.ko.md CHANGED
@@ -24,13 +24,14 @@
24
24
 
25
25
  ## 지원 플랫폼
26
26
 
27
- | 플랫폼 | 로그인 | 주요 기능 | 가이드 |
28
- |--------|--------|----------|--------|
29
- | **Tistory** | Playwright (카카오) | 글 발행, 임시저장, 카테고리, 이미지 업로드 | [가이드](docs/ko/guide-tistory.md) |
30
- | **Naver Blog** | Playwright (네이버) | 글 발행, 카테고리, SE Editor, 이미지 업로드 | [가이드](docs/ko/guide-naver.md) |
31
- | **Naver Cafe** | HTTP (브라우저 불필요) | 카페 가입 (캡차 자동해결), 글쓰기, 게시판 조회 | [가이드](docs/ko/guide-naver.md) |
32
- | **Instagram** | HTTP (브라우저 불필요) | 좋아요, 댓글, 팔로우, 포스팅, 프로필, 피드 | [가이드](docs/ko/guide-instagram.md) |
33
- | **X (Twitter)** | HTTP (쿠키 인증) | 트윗, 좋아요, 리트윗, 팔로우, 검색, 타임라인, 미디어 업로드 | [가이드](docs/ko/guide-x.md) |
27
+ | 플랫폼 | 주요 기능 | 가이드 |
28
+ |--------|----------|--------|
29
+ | **Tistory** | 글 발행, 임시저장, 카테고리, 이미지 업로드 | [가이드](docs/ko/guide-tistory.md) |
30
+ | **Naver Blog** | 글 발행, 카테고리, SE Editor, 이미지 업로드 | [가이드](docs/ko/guide-naver.md) |
31
+ | **Naver Cafe** | 카페 가입 (모바일 5회 캡차 면제), 글쓰기, 게시판 조회, 이미지 업로드 (슬라이드/콜라주) | [가이드](docs/ko/guide-naver-cafe.md) |
32
+ | **Instagram** | 좋아요, 댓글, 팔로우, 포스팅, 프로필, 피드 | [가이드](docs/ko/guide-instagram.md) |
33
+ | **X (Twitter)** | 트윗, 좋아요, 리트윗, 팔로우, 검색, 타임라인, 미디어 업로드 | [가이드](docs/ko/guide-x.md) |
34
+ | **Threads** | 글쓰기, 답글, 좋아요, 팔로우, 이미지 업로드, 검색, 피드 | [가이드](docs/ko/guide-threads.md) |
34
35
 
35
36
  ## 동작 방식
36
37
 
@@ -119,6 +120,13 @@ npx viruagent-cli login --provider x --auth-token <토큰> --ct0 <ct0>
119
120
  >
120
121
  > 전체 API 레퍼런스, GraphQL 동기화, rate limit 규칙은 [X 가이드](docs/ko/guide-x.md)를 참고하세요.
121
122
 
123
+ ### Threads
124
+
125
+ ```bash
126
+ npx viruagent-cli login --provider threads --username <인스타 ID> --password <비밀번호>
127
+ ```
128
+ > Instagram 계정으로 로그인합니다. 쓰레드는 별도 계정이 없습니다.
129
+
122
130
  ## 사용법
123
131
 
124
132
  | 이렇게 말하면 | 에이전트가 알아서 |
@@ -135,15 +143,19 @@ npx viruagent-cli login --provider x --auth-token <토큰> --ct0 <ct0>
135
143
  | "X에서 AI 도구 검색해줘" | search → 결과 반환 |
136
144
  | "X에서 IT 개발자 좋아요하고 팔로우해줘" | search → like + follow (딜레이 자동 적용) |
137
145
  | "내 X 타임라인 보여줘" | getFeed → 최신 트윗 표시 |
138
- | "이 네이버 카페 가입해줘" | cafe-id → cafe-join (캡차 자동해결) |
146
+ | "이 네이버 카페 가입해줘" | cafe-id → cafe-join (모바일 5회 캡차 면제) |
139
147
  | "네이버 카페에 글 써줘" | cafe-list → cafe-write |
148
+ | "쓰레드에 글 올려줘" | login → publish (텍스트 또는 이미지) |
149
+ | "이 쓰레드에 답글 달아줘" | comment (rate limit 자동 적용) |
140
150
 
141
151
  ## 플랫폼별 가이드
142
152
 
143
153
  - **[Tistory 가이드](docs/ko/guide-tistory.md)** — 블로그 발행, 이미지 업로드, 카테고리
144
154
  - **[Naver Blog 가이드](docs/ko/guide-naver.md)** — SE Editor, 블로그 발행, 이미지 업로드
155
+ - **[Naver Cafe 가이드](docs/ko/guide-naver-cafe.md)** — 카페 가입 (모바일 5회 캡차 면제), 글쓰기, 슬라이드/콜라주
145
156
  - **[Instagram 가이드](docs/ko/guide-instagram.md)** — 18개 API 메서드, rate limit 규칙, AI 댓글
146
157
  - **[X (Twitter) 가이드](docs/ko/guide-x.md)** — GraphQL API, queryId 동적 동기화, rate limit 규칙
158
+ - **[Threads 가이드](docs/ko/guide-threads.md)** — Barcelona API, IGT:2 토큰 인증, rate limit 규칙
147
159
 
148
160
  ## 지원 환경
149
161
 
@@ -165,7 +177,8 @@ npx viruagent-cli login --provider x --auth-token <토큰> --ct0 <ct0>
165
177
  | Rate Limiting | 유저별 영속 카운터 + 랜덤 딜레이 |
166
178
  | 이미지 검색 | DuckDuckGo, Wikimedia Commons |
167
179
  | 네이버 에디터 | SE Editor 컴포넌트 모델 + RabbitWrite API |
168
- | 네이버 카페 API | 순수 HTTP (가입, 글쓰기, 게시판 조회, 2Captcha 자동해결) |
180
+ | 네이버 카페 API | 순수 HTTP (가입, 글쓰기, 게시판 조회, 사용자 캡차 입력) |
181
+ | Threads API | Barcelona (Instagram Private API), IGT:2 토큰 인증 |
169
182
  | 출력 형식 | JSON envelope (`{ ok, data }` / `{ ok, error, hint }`) |
170
183
 
171
184
  ## Contributing
package/README.md CHANGED
@@ -24,14 +24,15 @@ Designed not for humans, but for **AI agents**.
24
24
 
25
25
  ## Supported Platforms
26
26
 
27
- | Platform | Login | Features | Guide |
28
- |----------|-------|----------|-------|
29
- | **Tistory** | Playwright (Kakao) | Publish, Draft, Categories, Image Upload | [Guide](docs/en/guide-tistory.md) |
30
- | **Naver Blog** | Playwright (Naver) | Publish, Categories, SE Editor, Image Upload | [Guide](docs/en/guide-naver.md) |
31
- | **Naver Cafe** | HTTP (No Browser) | Cafe Join (auto-captcha), Write Post, Board List | [Guide](docs/en/guide-naver.md) |
32
- | **Instagram** | HTTP (No Browser) | Like, Comment, Follow, Post, Profile, Feed, Rate Limit | [Guide](docs/en/guide-instagram.md) |
33
- | **X (Twitter)** | HTTP (Cookie Auth) | Tweet, Like, Retweet, Follow, Search, Timeline, Media Upload | [Guide](docs/en/guide-x.md) |
34
- | **Reddit** | OAuth2 / Cookie | Post, Comment, Upvote, Search, Subscribe, Subreddit | [Guide](docs/en/guide-reddit.md) |
27
+ | Platform | Features | Guide |
28
+ |----------|----------|-------|
29
+ | **Tistory** | Publish, Draft, Categories, Image Upload | [Guide](docs/en/guide-tistory.md) |
30
+ | **Naver Blog** | Publish, Categories, SE Editor, Image Upload | [Guide](docs/en/guide-naver.md) |
31
+ | **Naver Cafe** | Cafe Join (captcha-free for 5 joins), Write Post, Board List, Image Upload (slide/collage) | [Guide](docs/en/guide-naver-cafe.md) |
32
+ | **Instagram** | Like, Comment, Follow, Post, Profile, Feed, Rate Limit | [Guide](docs/en/guide-instagram.md) |
33
+ | **X (Twitter)** | Tweet, Like, Retweet, Follow, Search, Timeline, Media Upload | [Guide](docs/en/guide-x.md) |
34
+ | **Reddit** | Post, Comment, Upvote, Search, Subscribe, Subreddit | [Guide](docs/en/guide-reddit.md) |
35
+ | **Threads** | Post, Reply, Like, Follow, Image Upload, Search, Feed | [Guide](docs/en/guide-threads.md) |
35
36
 
36
37
  ## How It Works
37
38
 
@@ -120,6 +121,13 @@ npx viruagent-cli login --provider x --auth-token <token> --ct0 <ct0>
120
121
  >
121
122
  > See the [X Guide](docs/en/guide-x.md) for full API reference, GraphQL sync, and rate limit rules.
122
123
 
124
+ ### Threads
125
+
126
+ ```bash
127
+ npx viruagent-cli login --provider threads --username <instagram_id> --password <password>
128
+ ```
129
+ > Threads uses your Instagram account. No separate login needed.
130
+
123
131
  ## Usage
124
132
 
125
133
  | Say this | Agent handles |
@@ -136,15 +144,19 @@ npx viruagent-cli login --provider x --auth-token <token> --ct0 <ct0>
136
144
  | "Search X for AI tools" | search → return results |
137
145
  | "Like and follow IT devs on X" | search → like + follow (with delays) |
138
146
  | "Show my X timeline" | getFeed → show latest tweets |
139
- | "Join this Naver cafe" | cafe-id → cafe-join (auto-captcha) |
147
+ | "Join this Naver cafe" | cafe-id → cafe-join (captcha-free for 5 joins) |
140
148
  | "Write a post on Naver cafe" | cafe-list → cafe-write |
149
+ | "Post on Threads" | login → publish (text or image) |
150
+ | "Reply to a thread" | comment (with rate limit) |
141
151
 
142
152
  ## Platform Guides
143
153
 
144
154
  - **[Tistory Guide](docs/en/guide-tistory.md)** — Blog publishing, image upload, categories
145
155
  - **[Naver Blog Guide](docs/en/guide-naver.md)** — SE Editor, blog publishing, image upload
156
+ - **[Naver Cafe Guide](docs/en/guide-naver-cafe.md)** — Cafe join (captcha-free for 5 joins), write post, slide/collage images
146
157
  - **[Instagram Guide](docs/en/guide-instagram.md)** — 18 API methods, rate limits, AI commenting
147
158
  - **[X (Twitter) Guide](docs/en/guide-x.md)** — GraphQL API, dynamic queryId sync, rate limits
159
+ - **[Threads Guide](docs/en/guide-threads.md)** — Barcelona API, IGT:2 token auth, rate limits
148
160
 
149
161
  ## Supported Environments
150
162
 
@@ -166,7 +178,8 @@ npx viruagent-cli login --provider x --auth-token <token> --ct0 <ct0>
166
178
  | Rate Limiting | Per-user persistent counters with random delays |
167
179
  | Image Search | DuckDuckGo, Wikimedia Commons |
168
180
  | Naver Editor | SE Editor component model + RabbitWrite API |
169
- | Naver Cafe API | Pure HTTP (join, write, board list, 2Captcha auto-solve) |
181
+ | Naver Cafe API | Pure HTTP (join, write, board list, manual captcha) |
182
+ | Threads API | Barcelona (Instagram Private API), IGT:2 token auth |
170
183
  | Output Format | JSON envelope (`{ ok, data }` / `{ ok, error, hint }`) |
171
184
 
172
185
  ## Contributing
package/bin/index.js CHANGED
@@ -18,7 +18,7 @@ program
18
18
 
19
19
  // Global options
20
20
  const addProviderOption = (cmd) =>
21
- cmd.option('--provider <name>', 'Provider name (tistory, naver, insta, x, reddit)', 'tistory');
21
+ cmd.option('--provider <name>', 'Provider name (tistory, naver, insta, x, reddit, threads)', 'tistory');
22
22
 
23
23
  const addDryRunOption = (cmd) =>
24
24
  cmd.option('--dry-run', 'Validate params without executing', false);
@@ -290,7 +290,8 @@ addProviderOption(cafeJoinCmd);
290
290
  cafeJoinCmd
291
291
  .option('--cafe-url <url>', 'Cafe URL or slug')
292
292
  .option('--nickname <nick>', 'Nickname to use (default: auto)')
293
- .option('--captcha-api-key <key>', '2Captcha API key for auto-solve')
293
+ .option('--captcha-value <text>', 'Captcha answer text (from AI agent reading the image)')
294
+ .option('--captcha-key <key>', 'Captcha key (returned by previous captcha_required response)')
294
295
  .option('--answers <answers>', 'Comma-separated answers for join questions')
295
296
  .action((opts) => execute('cafe-join', opts));
296
297
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "viruagent-cli",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "AI-agent-optimized CLI for blog/SNS publishing and engagement (Tistory, Naver, Instagram, X/Twitter, Reddit)",
5
5
  "private": false,
6
6
  "type": "commonjs",
@@ -23,7 +23,7 @@ Naver 블로그 퍼블리싱을 위한 viruagent-cli 가이드. 항상 `--provid
23
23
  | list-categories | va-naver-categories | 카테고리 조회 |
24
24
  | list-posts, read-post | va-naver-posts | 글 목록/읽기 |
25
25
  | cafe-id | va-naver-cafe-id | 카페 ID 추출 |
26
- | cafe-join | va-naver-cafe-join | 카페 가입 (캡차 자동해결) |
26
+ | cafe-join | va-naver-cafe-join | 카페 가입 (모바일 5회 캡차 면제) |
27
27
  | cafe-list | va-naver-cafe-list | 카페 게시판 목록 |
28
28
  | cafe-write | va-naver-cafe-write | 카페 글쓰기 (슬라이드/콜라주) |
29
29
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: va-naver-cafe-join
3
- description: "Naver: 카페 가입 (캡차 자동 해결, 질문 자동 답변)"
3
+ description: "Naver: 카페 가입 (모바일 5회 캡차 면제, 이후 사용자 입력)"
4
4
  metadata:
5
5
  category: "command"
6
6
  provider: "naver"
@@ -10,7 +10,7 @@ metadata:
10
10
 
11
11
  # va-naver-cafe-join — 카페 가입
12
12
 
13
- 네이버 카페에 가입한다. 캡차가 있으면 2Captcha API로 자동 해결한다.
13
+ 네이버 카페에 가입한다. 모바일 헤더 사용으로 처음 5회까지 캡차 없이 가입 가능. 캡차 발생 시 사용자에게 입력을 요청한다.
14
14
 
15
15
  ## 실행
16
16
 
@@ -18,7 +18,8 @@ metadata:
18
18
  npx viruagent-cli cafe-join --provider naver \
19
19
  --cafe-url <url_or_slug> \
20
20
  [--nickname <닉네임>] \
21
- [--captcha-api-key <2captcha_key>] \
21
+ [--captcha-value <텍스트>] \
22
+ [--captcha-key <키>] \
22
23
  [--answers "답1,답2"]
23
24
  ```
24
25
 
@@ -28,7 +29,8 @@ npx viruagent-cli cafe-join --provider naver \
28
29
  |--------|------|------|--------|
29
30
  | `--cafe-url` | O | 카페 URL 또는 슬러그 | - |
30
31
  | `--nickname` | - | 사용할 닉네임 | 자동 생성 |
31
- | `--captcha-api-key` | - | 2Captcha API (캡차 자동 해결) | - |
32
+ | `--captcha-value` | - | 캡차 이미지 텍스트 (사용자 입력) | - |
33
+ | `--captcha-key` | - | 캡차 세션 키 (captcha_required 응답에서 제공) | - |
32
34
  | `--answers` | - | 가입 질문 답변 (쉼표 구분) | 모두 "네" |
33
35
 
34
36
  ### 가입 유형
@@ -40,15 +42,18 @@ npx viruagent-cli cafe-join --provider naver \
40
42
 
41
43
  ### 캡차 처리
42
44
 
43
- - `--captcha-api-key` 미제공 시: 캡차 필요한 카페는 `captcha_required` 반환
44
- - `--captcha-api-key` 제공 시: 2Captcha로 자동 해결 (최대 3회 재시도)
45
+ - 모바일 버전(`x-cafe-product: mweb`) 가입은 **처음 5회까지 캡차가 발생하지 않음**
46
+ - 캡차 발생 시: `captcha_required` 상태와 `captchaImageUrl` 반환
47
+ - 사용자가 이미지 URL을 브라우저에서 열어 텍스트를 확인
48
+ - `--captcha-value <텍스트> --captcha-key <키>`와 함께 재실행
49
+ - 틀린 경우 `captcha_invalid`와 새 이미지 URL 제공 → 반복
45
50
 
46
51
  ## 에러 처리
47
52
 
48
53
  | 에러 | 조치 |
49
54
  |------|------|
50
55
  | `ALREADY_JOINED` | 이미 가입된 카페 |
51
- | `CAPTCHA_REQUIRED` | `--captcha-api-key` 옵션 추가 |
56
+ | `CAPTCHA_REQUIRED` | `captchaImageUrl` 확인 후 `--captcha-value`/`--captcha-key`와 재실행 |
52
57
  | `NOT_LOGGED_IN` | `login --provider naver` 먼저 실행 |
53
58
 
54
59
  ## See Also
@@ -13,6 +13,8 @@ triggers:
13
13
  - 좋아요
14
14
  - 댓글
15
15
  - 팔로우
16
+ - 쓰레드
17
+ - threads
16
18
  metadata:
17
19
  category: "router"
18
20
  requires:
@@ -32,6 +34,7 @@ viruagent-cli를 사용하는 블로그/SNS 자동화 에이전트입니다.
32
34
  | 네이버, naver | `va-naver/SKILL.md` |
33
35
  | 카페, cafe, 카페 가입, 카페 글쓰기 | `va-naver-cafe-join/SKILL.md` 또는 `va-naver-cafe-write/SKILL.md` |
34
36
  | 인스타, instagram, 좋아요, 댓글, 팔로우 | `va-insta/SKILL.md` |
37
+ | 쓰레드, threads | `va-threads/SKILL.md` |
35
38
  | 블로그 써줘 (플랫폼 미지정) | 사용자에게 플랫폼 질문 |
36
39
  | 블로거 역할 | `persona-blogger/SKILL.md` |
37
40
  | 인플루언서 관리 | `persona-influencer-manager/SKILL.md` |
@@ -64,7 +67,7 @@ SKILLS_DIR: <viruagent-cli 설치 경로>/skills/
64
67
  | va-naver-categories | `va-naver-categories/SKILL.md` | 카테고리 조회 |
65
68
  | va-naver-posts | `va-naver-posts/SKILL.md` | 글 목록/읽기 |
66
69
  | va-naver-cafe-id | `va-naver-cafe-id/SKILL.md` | 카페 ID 추출 |
67
- | va-naver-cafe-join | `va-naver-cafe-join/SKILL.md` | 카페 가입 (캡차 자동해결) |
70
+ | va-naver-cafe-join | `va-naver-cafe-join/SKILL.md` | 카페 가입 (모바일 5회 캡차 면제) |
68
71
  | va-naver-cafe-list | `va-naver-cafe-list/SKILL.md` | 카페 게시판 목록 |
69
72
  | va-naver-cafe-write | `va-naver-cafe-write/SKILL.md` | 카페 글쓰기 (슬라이드/콜라주) |
70
73
  | va-insta | `va-insta/SKILL.md` | Instagram 개요 + 레이트리밋 |
@@ -75,6 +78,8 @@ SKILLS_DIR: <viruagent-cli 설치 경로>/skills/
75
78
  | va-insta-follow | `va-insta-follow/SKILL.md` | 팔로우/언팔 |
76
79
  | va-insta-dm | `va-insta-dm/SKILL.md` | DM |
77
80
  | va-insta-feed | `va-insta-feed/SKILL.md` | 피드/프로필/분석 |
81
+ | va-threads | `va-threads/SKILL.md` | Threads 개요 + 레이트리밋 |
82
+ | va-threads-publish | `va-threads-publish/SKILL.md` | Threads 글쓰기 |
78
83
 
79
84
  ### 페르소나 스킬
80
85
 
@@ -0,0 +1,67 @@
1
+ ---
2
+ name: va-threads
3
+ version: 1.0.0
4
+ description: "Threads 자동화: 명령 개요 및 레이트리밋 규칙"
5
+ metadata:
6
+ category: "overview"
7
+ provider: "threads"
8
+ requires:
9
+ bins: ["viruagent-cli"]
10
+ ---
11
+
12
+ # va-threads — Threads 자동화 개요
13
+
14
+ Threads 자동화를 위한 viruagent-cli 가이드. 항상 `--provider threads` 사용.
15
+
16
+ ## 명령 목록
17
+
18
+ | 명령 | 스킬 | 설명 |
19
+ |------|------|------|
20
+ | login | va-threads | 로그인 (Instagram 계정) |
21
+ | publish | va-threads-publish | 글쓰기 (텍스트/이미지) |
22
+ | like | va-threads | 좋아요 |
23
+ | comment | va-threads | 답글 |
24
+ | follow | va-threads | 팔로우 |
25
+ | search | va-threads | 검색 |
26
+ | get-profile | va-threads | 프로필 조회 |
27
+ | get-feed | va-threads | 피드 조회 |
28
+
29
+ ## Rate Limit Safety (신규 계정 기준)
30
+
31
+ | 액션 | 딜레이 | 시간당 | 일일 |
32
+ |------|--------|--------|------|
33
+ | Post | 2~5min | 5 | 25 |
34
+ | Like | 20~40s | 15 | 500 |
35
+ | Reply | 5~7min | 5 | 100 |
36
+ | Follow | 1~2min | 15 | 250 |
37
+
38
+ 모든 딜레이는 랜덤화되어 자동 적용. 카운터는 세션별 userId로 영속화.
39
+
40
+ ## 레이트리밋 확인
41
+
42
+ ```bash
43
+ npx viruagent-cli rate-limit-status --provider threads
44
+ ```
45
+
46
+ 대량 작업 전 반드시 확인.
47
+
48
+ ## 중요 사항
49
+
50
+ - Instagram 계정을 공유하므로 Instagram rate limit에 영향을 줄 수 있음
51
+ - Barcelona User-Agent + Bloks API 사용
52
+ - IGT:2 토큰이 주요 인증 수단
53
+ - 세션 + 카운터: `~/.viruagent-cli/sessions/threads-session.json`
54
+
55
+ ## 환경변수
56
+
57
+ ```
58
+ THREADS_USERNAME=
59
+ THREADS_PASSWORD=
60
+ # 또는
61
+ INSTA_USERNAME=
62
+ INSTA_PASSWORD=
63
+ ```
64
+
65
+ ## See Also
66
+
67
+ va-shared, va-threads-publish
@@ -0,0 +1,76 @@
1
+ ---
2
+ name: va-threads-publish
3
+ version: 1.0.0
4
+ description: "Threads 글쓰기: 텍스트/이미지 포스팅"
5
+ metadata:
6
+ category: "publish"
7
+ provider: "threads"
8
+ requires:
9
+ bins: ["viruagent-cli"]
10
+ ---
11
+
12
+ # va-threads-publish — Threads 글쓰기
13
+
14
+ Threads에 텍스트/이미지를 발행하는 가이드.
15
+
16
+ ## 명령어
17
+
18
+ ```bash
19
+ # 텍스트만
20
+ npx viruagent-cli publish --provider threads \
21
+ --content "<텍스트>"
22
+
23
+ # 이미지 첨부
24
+ npx viruagent-cli publish --provider threads \
25
+ --content "<텍스트>" \
26
+ --image-urls "<이미지URL>"
27
+ ```
28
+
29
+ ## 글쓰기 규칙 (Threads 최적화)
30
+
31
+ ### 분량
32
+ - **최적 길이**: 100~300자 (짧고 임팩트 있게)
33
+ - Threads는 짧은 텍스트 중심 — 블로그처럼 길게 쓰지 않음
34
+
35
+ ### 첫 줄이 전부다
36
+ - **숫자**: "3가지만 알면 됩니다"
37
+ - **반전**: "잘못 알고 있었습니다"
38
+ - **질문**: "이거 해보셨나요?"
39
+ - **공감**: "저만 이런 거 아니죠?"
40
+
41
+ ### 해시태그
42
+ - Threads는 해시태그 효과가 Instagram보다 약함
43
+ - 최대 5개, 본문 하단에 배치
44
+ - 검색 기능이 제한적이므로 키워드를 본문에 자연스럽게 녹이기
45
+
46
+ ### 이미지
47
+ - 이미지 1장 첨부 가능
48
+ - `--image-urls`에 직접 URL 지정
49
+ - 고해상도 (1080x1080 이상) 권장
50
+
51
+ ## 발행 예시
52
+
53
+ ```bash
54
+ # 짧은 의견
55
+ npx viruagent-cli publish --provider threads \
56
+ --content "AI가 코드 리뷰해주는 시대.
57
+
58
+ 그런데 아직도 혼자 다 보고 있다면
59
+ 도구를 바꿀 때입니다."
60
+
61
+ # 이미지 포함
62
+ npx viruagent-cli publish --provider threads \
63
+ --content "오늘의 작업 환경. 카페에서 코딩하는 게 제일 잘 됨." \
64
+ --image-urls "https://example.com/workspace.jpg"
65
+ ```
66
+
67
+ ## 주의사항
68
+
69
+ - 글 최대 500자 (초과 시 잘림)
70
+ - 발행 간격: 최소 2~5분 (rate limit)
71
+ - 로그인 안 된 경우: `login --provider threads` 먼저 실행
72
+ - Instagram 계정 공유이므로 Instagram challenge 발생 가능
73
+
74
+ ## See Also
75
+
76
+ va-threads, va-shared
@@ -198,39 +198,6 @@ const createCafeApiClient = ({ sessionPath }) => {
198
198
  return buffer.toString('base64');
199
199
  };
200
200
 
201
- const solveCaptchaWith2Captcha = async (imageBase64, apiKey) => {
202
- const submitRes = await fetch('https://2captcha.com/in.php', {
203
- method: 'POST',
204
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
205
- body: new URLSearchParams({
206
- key: apiKey,
207
- method: 'base64',
208
- body: imageBase64,
209
- json: '1',
210
- }),
211
- });
212
- const submitData = await submitRes.json();
213
- if (submitData.status !== 1) {
214
- throw createError('CAPTCHA_SUBMIT_FAILED', `2Captcha submit failed: ${submitData.request}`);
215
- }
216
-
217
- const captchaId = submitData.request;
218
-
219
- // Poll for result (max 120s)
220
- for (let i = 0; i < 24; i++) {
221
- await new Promise((r) => setTimeout(r, 5000));
222
- const pollRes = await fetch(
223
- `https://2captcha.com/res.php?key=${apiKey}&action=get&id=${captchaId}&json=1`,
224
- );
225
- const pollData = await pollRes.json();
226
- if (pollData.status === 1) return pollData.request;
227
- if (pollData.request !== 'CAPCHA_NOT_READY') {
228
- throw createError('CAPTCHA_POLL_FAILED', `2Captcha poll failed: ${pollData.request}`);
229
- }
230
- }
231
- throw createError('CAPTCHA_TIMEOUT', '2Captcha timeout (120s)');
232
- };
233
-
234
201
  const submitJoin = async (cafeId, { alimCode, clubTempId, applyPayload }) => {
235
202
  const cookieStr = getCookieStr();
236
203
  const queryParams = new URLSearchParams({
@@ -582,7 +549,6 @@ const createCafeApiClient = ({ sessionPath }) => {
582
549
  checkNickname,
583
550
  validateCaptcha,
584
551
  downloadCaptchaImage,
585
- solveCaptchaWith2Captcha,
586
552
  submitJoin,
587
553
  getBoardList,
588
554
  getEditorInfo,
@@ -241,7 +241,7 @@ const createNaverProvider = ({ sessionPath, account }) => {
241
241
  });
242
242
  },
243
243
 
244
- async cafeJoin({ cafeUrl, nickname, captchaApiKey, answers } = {}) {
244
+ async cafeJoin({ cafeUrl, nickname, captchaValue, captchaKey: inputCaptchaKey, answers } = {}) {
245
245
  return withProviderSession(async () => {
246
246
  if (!cafeUrl) {
247
247
  const err = new Error('cafeUrl is required');
@@ -262,60 +262,41 @@ const createNaverProvider = ({ sessionPath, account }) => {
262
262
  finalNickname = `user${Math.floor(Math.random() * 9000 + 1000)}`;
263
263
  }
264
264
 
265
- // 4. Handle captcha
266
- let captchaKey = form.captchaKey;
267
- let captchaValue = '';
265
+ // 4. Handle captcha — prompt user for manual input
266
+ let captchaKey = inputCaptchaKey || form.captchaKey;
267
+ let resolvedCaptchaValue = captchaValue || '';
268
268
 
269
- if (form.needCaptcha) {
270
- if (!captchaApiKey) {
271
- return {
272
- provider: 'naver',
273
- mode: 'cafe-join',
274
- status: 'captcha_required',
275
- cafeId: id,
276
- slug,
277
- cafeName: form.cafeName,
278
- captchaImageUrl: form.captchaImageUrl,
279
- captchaKey: form.captchaKey,
280
- message: 'Captcha is required. Provide --captcha-api-key for auto-solve or solve manually.',
281
- };
282
- }
283
-
284
- // Auto-solve with 2Captcha (max 3 attempts)
285
- let solved = false;
286
- let captchaImageUrl = form.captchaImageUrl;
287
-
288
- for (let attempt = 0; attempt < 3; attempt++) {
289
- const imgBase64 = await cafeApi.downloadCaptchaImage(captchaImageUrl);
290
- captchaValue = await cafeApi.solveCaptchaWith2Captcha(imgBase64, captchaApiKey);
291
- const validateResult = await cafeApi.validateCaptcha(captchaKey, captchaValue);
292
-
293
- if (validateResult.valid) {
294
- solved = true;
295
- break;
296
- }
297
-
298
- // Update captcha for retry
299
- if (validateResult.captchaKey) {
300
- captchaKey = validateResult.captchaKey;
301
- captchaImageUrl = validateResult.captchaImageUrl;
302
- } else {
303
- const newForm = await cafeApi.getJoinForm(id);
304
- captchaKey = newForm.captchaKey;
305
- captchaImageUrl = newForm.captchaImageUrl;
306
- }
307
- captchaValue = '';
308
- }
269
+ if (form.needCaptcha && !resolvedCaptchaValue) {
270
+ return {
271
+ provider: 'naver',
272
+ mode: 'cafe-join',
273
+ status: 'captcha_required',
274
+ cafeId: id,
275
+ slug,
276
+ cafeName: form.cafeName,
277
+ captchaKey: form.captchaKey,
278
+ captchaImageUrl: form.captchaImageUrl,
279
+ nickname: finalNickname,
280
+ message: 'Captcha required. Open the captchaImageUrl in a browser, read the text, and re-run with --captcha-value <text> --captcha-key <key>',
281
+ };
282
+ }
309
283
 
310
- if (!solved) {
284
+ // Validate captcha if provided
285
+ if (form.needCaptcha && resolvedCaptchaValue) {
286
+ const validateResult = await cafeApi.validateCaptcha(captchaKey, resolvedCaptchaValue);
287
+ if (!validateResult.valid) {
288
+ const newForm = await cafeApi.getJoinForm(id);
311
289
  return {
312
290
  provider: 'naver',
313
291
  mode: 'cafe-join',
314
- status: 'captcha_failed',
292
+ status: 'captcha_invalid',
315
293
  cafeId: id,
316
294
  slug,
317
295
  cafeName: form.cafeName,
318
- message: 'Captcha solve failed after 3 attempts.',
296
+ captchaKey: newForm.captchaKey,
297
+ captchaImageUrl: newForm.captchaImageUrl,
298
+ nickname: finalNickname,
299
+ message: 'Captcha answer was wrong. Open the new captchaImageUrl, read the text, and retry with --captcha-value <text> --captcha-key <key>',
319
300
  };
320
301
  }
321
302
  }