viruagent-cli 0.4.1 → 0.5.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/bin/index.js CHANGED
@@ -212,6 +212,12 @@ analyzePostCmd
212
212
  .option('--post-id <shortcode>', 'Post shortcode')
213
213
  .action((opts) => execute('analyze-post', opts));
214
214
 
215
+ const resolveChallengeCmd = program
216
+ .command('resolve-challenge')
217
+ .description('Resolve Instagram challenge (auto-verify identity)');
218
+ addProviderOption(resolveChallengeCmd);
219
+ resolveChallengeCmd.action((opts) => execute('resolve-challenge', opts));
220
+
215
221
  const rateLimitCmd = program
216
222
  .command('rate-limit-status')
217
223
  .description('Show current rate limit usage');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "viruagent-cli",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "AI-agent-optimized CLI for blog publishing (Tistory, Naver) and Instagram automation",
5
5
  "private": false,
6
6
  "type": "commonjs",
@@ -8,40 +8,40 @@ const randomDelay = (minSec, maxSec) => {
8
8
  };
9
9
 
10
10
  // ──────────────────────────────────────────────────────────────
11
- // Instagram 안전 액션 규칙 (2026 기준, 리서치 기반)
11
+ // Instagram Safe Action Rules (2026, research-based)
12
12
  //
13
- // [계정 나이별 시간당 한도]
14
- // 신규 (0~20) | 성숙 (20일+)
15
- // 좋아요 15/h | 60/h
16
- // 댓글 5/h | 20/h
17
- // 팔로우 15/h | 60/h
18
- // 언팔로우 10/h | 30/h
13
+ // [Hourly limits by account age]
14
+ // New (0~20 days) | Mature (20+ days)
15
+ // Like 15/h | 60/h
16
+ // Comment 5/h | 20/h
17
+ // Follow 15/h | 60/h
18
+ // Unfollow 10/h | 30/h
19
19
  // DM 5/h | 50/h
20
- // 게시물 3/h | 10/h
20
+ // Publish 3/h | 10/h
21
21
  //
22
- // [일일 한도]
23
- // 좋아요 500/일 댓글 100/일 팔로우 250/일
24
- // 언팔로우 200/일 DM 30/일 게시물 25/일
22
+ // [Daily limits]
23
+ // Like 500/day Comment 100/day Follow 250/day
24
+ // Unfollow 200/day DM 30/day Publish 25/day
25
25
  //
26
- // [최소 액션 간격 (신규 계정)]
27
- // 좋아요: 20~40초 | 댓글: 300~420초(5~7분) | 팔로우: 60~120초
28
- // 언팔로우: 60~120초 | DM: 120~300초 | 게시물: 60~120초
26
+ // [Minimum action intervals (new accounts)]
27
+ // Like: 20~40s | Comment: 300~420s (5~7min) | Follow: 60~120s
28
+ // Unfollow: 60~120s | DM: 120~300s | Publish: 60~120s
29
29
  //
30
- // [주의사항]
31
- // - 시간당 액션 15개 이하 (신규) / 40 이하 (성숙)
32
- // - 균일 간격은 감지랜덤 딜레이 필수
33
- // - 동일 유저에게 반복 액션 금지
34
- // - challenge 발생 브라우저에서 본인 인증 필요
35
- // - challenge 24~48시간 대기 권장
30
+ // [Notes]
31
+ // - Max 15 total actions/hour (new) / 40 (mature)
32
+ // - Uniform intervals trigger bot detection random delay required
33
+ // - Repeated actions on the same user are prohibited
34
+ // - Challenge requires manual verification in the browser
35
+ // - Wait 24~48 hours after a challenge is recommended
36
36
  // ──────────────────────────────────────────────────────────────
37
37
 
38
38
  const DELAY = {
39
- like: [20, 40], // 20~40초
40
- comment: [300, 420], // 5~7분
41
- follow: [60, 120], // 1~2분
42
- unfollow: [60, 120], // 1~2분
43
- dm: [120, 300], // 2~5분
44
- publish: [60, 120], // 1~2분
39
+ like: [20, 40], // 20~40s
40
+ comment: [300, 420], // 5~7min
41
+ follow: [60, 120], // 1~2min
42
+ unfollow: [60, 120], // 1~2min
43
+ dm: [120, 300], // 2~5min
44
+ publish: [60, 120], // 1~2min
45
45
  };
46
46
 
47
47
  const HOURLY_LIMIT = {
@@ -69,7 +69,7 @@ const createInstaApiClient = ({ sessionPath }) => {
69
69
  let cachedUserId = null;
70
70
  let countersCache = null;
71
71
 
72
- // ── 세션 파일 기반 Rate Limit 카운터 ──
72
+ // ── Session file-based Rate Limit counters ──
73
73
 
74
74
  const loadCounters = () => {
75
75
  if (countersCache) return countersCache;
@@ -90,7 +90,7 @@ const createInstaApiClient = ({ sessionPath }) => {
90
90
  if (!userId || !countersCache) return;
91
91
  saveRateLimits(sessionPath, userId, countersCache);
92
92
  } catch {
93
- // 저장 실패해도 동작에 영향 없음
93
+ // Save failure does not affect operation
94
94
  }
95
95
  };
96
96
 
@@ -112,10 +112,10 @@ const createInstaApiClient = ({ sessionPath }) => {
112
112
  const dailyMax = DAILY_LIMIT[type];
113
113
  if (hourlyMax && c.hourly >= hourlyMax) {
114
114
  const waitMin = Math.ceil((3600000 - (Date.now() - c.hourStart)) / 60000);
115
- throw new Error(`hourly_limit: ${type} 시간당 한도 ${hourlyMax} 초과. ${waitMin} 후 재시도하세요.`);
115
+ throw new Error(`hourly_limit: ${type} exceeded hourly limit of ${hourlyMax}. Retry in ${waitMin} minutes.`);
116
116
  }
117
117
  if (dailyMax && c.daily >= dailyMax) {
118
- throw new Error(`daily_limit: ${type} 일일 한도 ${dailyMax} 초과. 내일 다시 시도하세요.`);
118
+ throw new Error(`daily_limit: ${type} exceeded daily limit of ${dailyMax}. Try again tomorrow.`);
119
119
  }
120
120
  };
121
121
 
@@ -130,11 +130,11 @@ const createInstaApiClient = ({ sessionPath }) => {
130
130
  if (cachedCookies) return cachedCookies;
131
131
  const cookies = loadInstaSession(sessionPath);
132
132
  if (!cookies) {
133
- throw new Error('세션 파일이 없습니다. 먼저 로그인해 주세요.');
133
+ throw new Error('No session file found. Please log in first.');
134
134
  }
135
135
  const sessionid = cookies.find((c) => c.name === 'sessionid');
136
136
  if (!sessionid?.value) {
137
- throw new Error('세션에 유효한 쿠키가 없습니다. 다시 로그인해 주세요.');
137
+ throw new Error('No valid cookies in session. Please log in again.');
138
138
  }
139
139
  cachedCookies = cookies;
140
140
  return cookies;
@@ -172,17 +172,44 @@ const createInstaApiClient = ({ sessionPath }) => {
172
172
  if (res.status === 302 || res.status === 301) {
173
173
  const location = res.headers.get('location') || '';
174
174
  if (location.includes('/accounts/login')) {
175
- throw new Error('세션이 만료되었습니다. 다시 로그인해 주세요.');
175
+ throw new Error('Session expired. Please log in again.');
176
+ }
177
+ if (location.includes('/challenge')) {
178
+ // Attempt automatic challenge resolution
179
+ const resolved = await resolveChallenge();
180
+ if (resolved) {
181
+ // Retry original request after resolution
182
+ return fetch(url, { ...options, headers, redirect: 'manual' });
183
+ }
184
+ throw new Error('challenge_required: Identity verification required. Please complete it manually in the browser.');
185
+ }
186
+ throw new Error(`Redirect occurred: ${res.status} → ${location}`);
187
+ }
188
+
189
+ // Handle challenge_required JSON response
190
+ if (res.status === 400 && !options.allowError) {
191
+ const cloned = res.clone();
192
+ try {
193
+ const data = await cloned.json();
194
+ if (data.message === 'challenge_required') {
195
+ const resolved = await resolveChallenge();
196
+ if (resolved) {
197
+ return fetch(url, { ...options, headers, redirect: 'manual' });
198
+ }
199
+ throw new Error('challenge_required: Identity verification required.');
200
+ }
201
+ } catch (e) {
202
+ if (e.message.includes('challenge_required')) throw e;
203
+ // Ignore JSON parse failure and continue original flow
176
204
  }
177
- throw new Error(`리다이렉트 발생: ${res.status} → ${location}`);
178
205
  }
179
206
 
180
207
  if (res.status === 401 || res.status === 403) {
181
- throw new Error(`인증 오류 (${res.status}). 다시 로그인해 주세요.`);
208
+ throw new Error(`Authentication error (${res.status}). Please log in again.`);
182
209
  }
183
210
 
184
211
  if (!res.ok && !options.allowError) {
185
- throw new Error(`Instagram API 오류: ${res.status} ${res.statusText}`);
212
+ throw new Error(`Instagram API error: ${res.status} ${res.statusText}`);
186
213
  }
187
214
 
188
215
  return res;
@@ -194,7 +221,7 @@ const createInstaApiClient = ({ sessionPath }) => {
194
221
  );
195
222
  const data = await res.json();
196
223
  const user = data?.data?.user;
197
- if (!user) throw new Error(`프로필을 찾을 없습니다: ${username}`);
224
+ if (!user) throw new Error(`Profile not found: ${username}`);
198
225
  return {
199
226
  id: user.id,
200
227
  username: user.username,
@@ -295,7 +322,7 @@ const createInstaApiClient = ({ sessionPath }) => {
295
322
  has_threaded_comments: true,
296
323
  });
297
324
  const media = data?.data?.xdt_shortcode_media || data?.data?.shortcode_media;
298
- if (!media) throw new Error(`게시물을 찾을 없습니다: ${shortcode}`);
325
+ if (!media) throw new Error(`Post not found: ${shortcode}`);
299
326
  return {
300
327
  id: media.id,
301
328
  code: media.shortcode,
@@ -316,6 +343,35 @@ const createInstaApiClient = ({ sessionPath }) => {
316
343
  };
317
344
  };
318
345
 
346
+ // ── Automatic Challenge Resolution ──
347
+
348
+ const resolveChallenge = async () => {
349
+ try {
350
+ const cookies = getCookies();
351
+ const res = await fetch('https://www.instagram.com/api/v1/challenge/web/action/', {
352
+ method: 'POST',
353
+ headers: {
354
+ 'User-Agent': USER_AGENT,
355
+ 'X-IG-App-ID': IG_APP_ID,
356
+ 'X-CSRFToken': getCsrfToken(),
357
+ 'X-Requested-With': 'XMLHttpRequest',
358
+ 'X-Instagram-AJAX': '1',
359
+ Referer: 'https://www.instagram.com/challenge/',
360
+ Origin: 'https://www.instagram.com',
361
+ 'Content-Type': 'application/x-www-form-urlencoded',
362
+ Cookie: cookiesToHeader(cookies),
363
+ },
364
+ body: 'choice=0',
365
+ redirect: 'manual',
366
+ });
367
+ if (!res.ok) return false;
368
+ const data = await res.json();
369
+ return data.status === 'ok';
370
+ } catch {
371
+ return false;
372
+ }
373
+ };
374
+
319
375
  const parseLikeResponse = async (res) => {
320
376
  if (res.ok) return res.json();
321
377
  if (res.status === 400) {
@@ -323,17 +379,17 @@ const createInstaApiClient = ({ sessionPath }) => {
323
379
  if (data.spam) {
324
380
  throw new Error(`rate_limit: ${data.feedback_title || 'Try Again Later'}`);
325
381
  }
326
- // 이미 좋아요/취소 상태
382
+ // Already liked/unliked state
327
383
  return { status: 'already', message: data.message };
328
384
  }
329
- throw new Error(`Instagram API 오류: ${res.status}`);
385
+ throw new Error(`Instagram API error: ${res.status}`);
330
386
  };
331
387
 
332
388
  const withDelay = async (type, fn) => {
333
- // 한도 체크
389
+ // Check limits
334
390
  checkLimit(type);
335
391
 
336
- // 랜덤 딜레이
392
+ // Random delay
337
393
  const [min, max] = DELAY[type] || [20, 40];
338
394
  const elapsed = (Date.now() - lastActionTime) / 1000;
339
395
  if (lastActionTime > 0 && elapsed < min) {
@@ -519,7 +575,7 @@ const createInstaApiClient = ({ sessionPath }) => {
519
575
  );
520
576
  const data = await res.json();
521
577
  if (data.status !== 'ok') {
522
- throw new Error(`이미지 업로드 실패: ${data.message || 'unknown'}`);
578
+ throw new Error(`Image upload failed: ${data.message || 'unknown'}`);
523
579
  }
524
580
  return uploadId;
525
581
  };
@@ -545,7 +601,7 @@ const createInstaApiClient = ({ sessionPath }) => {
545
601
  });
546
602
  const data = await res.json();
547
603
  if (data.status !== 'ok') {
548
- throw new Error(`게시물 생성 실패: ${data.message || 'unknown'}`);
604
+ throw new Error(`Post creation failed: ${data.message || 'unknown'}`);
549
605
  }
550
606
  return {
551
607
  id: data.media?.pk,
@@ -561,10 +617,10 @@ const createInstaApiClient = ({ sessionPath }) => {
561
617
  imageBuffer = fs.readFileSync(imagePath);
562
618
  } else if (imageUrl) {
563
619
  const res = await fetch(imageUrl);
564
- if (!res.ok) throw new Error(`이미지 다운로드 실패: ${res.status}`);
620
+ if (!res.ok) throw new Error(`Image download failed: ${res.status}`);
565
621
  imageBuffer = Buffer.from(await res.arrayBuffer());
566
622
  } else {
567
- throw new Error('imageUrl 또는 imagePath 필요합니다.');
623
+ throw new Error('Either imageUrl or imagePath is required.');
568
624
  }
569
625
 
570
626
  const uploadId = await uploadPhoto(imageBuffer);
@@ -611,6 +667,7 @@ const createInstaApiClient = ({ sessionPath }) => {
611
667
  configurePost,
612
668
  publishPost,
613
669
  deletePost,
670
+ resolveChallenge,
614
671
  resetState,
615
672
  getRateLimitStatus: () => {
616
673
  const status = {};
@@ -619,7 +676,7 @@ const createInstaApiClient = ({ sessionPath }) => {
619
676
  status[type] = {
620
677
  hourly: `${c.hourly}/${HOURLY_LIMIT[type]}`,
621
678
  daily: `${c.daily}/${DAILY_LIMIT[type]}`,
622
- delay: `${DELAY[type]?.[0]}~${DELAY[type]?.[1]}초`,
679
+ delay: `${DELAY[type]?.[0]}~${DELAY[type]?.[1]}s`,
623
680
  };
624
681
  }
625
682
  return status;
@@ -50,12 +50,12 @@ const createAskForAuthentication = ({ sessionPath }) => async ({
50
50
 
51
51
  if (!resolvedUsername || !resolvedPassword) {
52
52
  throw new Error(
53
- '인스타그램 로그인에 username/password 필요합니다. ' +
54
- '환경변수 INSTA_USERNAME / INSTA_PASSWORD 설정해 주세요.',
53
+ 'Instagram login requires username/password. ' +
54
+ 'Please set the INSTA_USERNAME / INSTA_PASSWORD environment variables.',
55
55
  );
56
56
  }
57
57
 
58
- // Step 1: GET login page -> csrftoken + mid 쿠키 획득
58
+ // Step 1: GET login page -> obtain csrftoken + mid cookies
59
59
  const initRes = await fetch('https://www.instagram.com/accounts/login/', {
60
60
  headers: { 'User-Agent': USER_AGENT },
61
61
  redirect: 'manual',
@@ -64,7 +64,7 @@ const createAskForAuthentication = ({ sessionPath }) => async ({
64
64
 
65
65
  const csrfCookie = cookies.find((c) => c.name === 'csrftoken');
66
66
  if (!csrfCookie) {
67
- throw new Error('Instagram 초기 페이지에서 csrftoken 가져올 없습니다.');
67
+ throw new Error('Failed to retrieve csrftoken from the Instagram login page.');
68
68
  }
69
69
  const csrfToken = csrfCookie.value;
70
70
  const cookieHeader = cookies.map((c) => `${c.name}=${c.value}`).join('; ');
@@ -99,24 +99,75 @@ const createAskForAuthentication = ({ sessionPath }) => async ({
99
99
 
100
100
  const loginData = await loginRes.json();
101
101
 
102
- if (loginData.checkpoint_url) {
103
- throw new Error(
104
- '2단계 인증(checkpoint)이 필요합니다. 브라우저에서 먼저 인증을 완료해 주세요.',
105
- );
102
+ if (loginData.checkpoint_url || loginData.message === 'challenge_required') {
103
+ // Attempt automatic challenge resolution (choice=0 = "This was me")
104
+ const challengeRes = await fetch('https://www.instagram.com/api/v1/challenge/web/action/', {
105
+ method: 'POST',
106
+ headers: {
107
+ 'User-Agent': USER_AGENT,
108
+ 'X-CSRFToken': csrfToken,
109
+ 'X-Requested-With': 'XMLHttpRequest',
110
+ 'X-Instagram-AJAX': '1',
111
+ 'X-IG-App-ID': IG_APP_ID,
112
+ Referer: 'https://www.instagram.com/challenge/',
113
+ Origin: 'https://www.instagram.com',
114
+ 'Content-Type': 'application/x-www-form-urlencoded',
115
+ Cookie: cookieHeader,
116
+ },
117
+ body: 'choice=0',
118
+ redirect: 'manual',
119
+ });
120
+ const challengeData = await challengeRes.json().catch(() => ({}));
121
+ if (challengeData.status !== 'ok') {
122
+ throw new Error(
123
+ 'challenge_required: Automatic resolution failed. Please complete identity verification in the browser. ' +
124
+ (loginData.checkpoint_url || ''),
125
+ );
126
+ }
127
+ // Re-login after challenge resolution
128
+ const retryRes = await fetch('https://www.instagram.com/api/v1/web/accounts/login/ajax/', {
129
+ method: 'POST',
130
+ headers: {
131
+ 'User-Agent': USER_AGENT,
132
+ 'X-CSRFToken': csrfToken,
133
+ 'X-Requested-With': 'XMLHttpRequest',
134
+ 'X-IG-App-ID': IG_APP_ID,
135
+ 'Content-Type': 'application/x-www-form-urlencoded',
136
+ Referer: 'https://www.instagram.com/accounts/login/',
137
+ Origin: 'https://www.instagram.com',
138
+ Cookie: cookieHeader,
139
+ },
140
+ body: body.toString(),
141
+ redirect: 'manual',
142
+ });
143
+ const retryCookies = parseCookiesFromHeaders(retryRes.headers);
144
+ cookies = mergeCookies(cookies, retryCookies);
145
+ const retryData = await retryRes.json().catch(() => ({}));
146
+ if (retryData.authenticated) {
147
+ saveInstaSession(sessionPath, cookies);
148
+ return {
149
+ provider: 'insta',
150
+ loggedIn: true,
151
+ userId: retryData.userId || null,
152
+ username: resolvedUsername,
153
+ sessionPath,
154
+ challengeResolved: true,
155
+ };
156
+ }
106
157
  }
107
158
 
108
159
  if (loginData.two_factor_required) {
109
160
  throw new Error(
110
- '2단계 인증(2FA) 필요합니다. 브라우저에서 먼저 인증을 완료해 주세요.',
161
+ 'Two-factor authentication (2FA) is required. Please complete verification in the browser first.',
111
162
  );
112
163
  }
113
164
 
114
165
  if (!loginData.authenticated) {
115
166
  const reason = loginData.message || loginData.status || 'unknown';
116
- throw new Error(`인스타그램 로그인 실패: ${reason}`);
167
+ throw new Error(`Instagram login failed: ${reason}`);
117
168
  }
118
169
 
119
- // 세션 저장
170
+ // Save session
120
171
  saveInstaSession(sessionPath, cookies);
121
172
 
122
173
  return {
@@ -52,8 +52,8 @@ const createInstaProvider = ({ sessionPath }) => {
52
52
 
53
53
  if (!resolved.username || !resolved.password) {
54
54
  throw new Error(
55
- '인스타그램 로그인에 username/password 필요합니다. ' +
56
- '환경변수 INSTA_USERNAME / INSTA_PASSWORD 설정해 주세요.',
55
+ 'Instagram login requires username/password. ' +
56
+ 'Please set the INSTA_USERNAME / INSTA_PASSWORD environment variables.',
57
57
  );
58
58
  }
59
59
 
@@ -73,7 +73,7 @@ const createInstaProvider = ({ sessionPath }) => {
73
73
  async getProfile({ username } = {}) {
74
74
  return withProviderSession(async () => {
75
75
  if (!username) {
76
- throw new Error('username 필요합니다.');
76
+ throw new Error('username is required.');
77
77
  }
78
78
  const profile = await instaApi.getProfile(username);
79
79
  return {
@@ -99,7 +99,7 @@ const createInstaProvider = ({ sessionPath }) => {
99
99
  async listPosts({ username, limit = 12 } = {}) {
100
100
  return withProviderSession(async () => {
101
101
  if (!username) {
102
- throw new Error('username 필요합니다.');
102
+ throw new Error('username is required.');
103
103
  }
104
104
  const posts = await instaApi.getUserPosts(username, limit);
105
105
  return {
@@ -120,7 +120,7 @@ const createInstaProvider = ({ sessionPath }) => {
120
120
  provider: 'insta',
121
121
  mode: 'post',
122
122
  status: 'invalid_post_id',
123
- message: 'postId(shortcode) 필요합니다.',
123
+ message: 'postId (shortcode) is required.',
124
124
  };
125
125
  }
126
126
  const post = await instaApi.getPostDetail(shortcode);
@@ -134,7 +134,7 @@ const createInstaProvider = ({ sessionPath }) => {
134
134
 
135
135
  async follow({ username } = {}) {
136
136
  return withProviderSession(async () => {
137
- if (!username) throw new Error('username 필요합니다.');
137
+ if (!username) throw new Error('username is required.');
138
138
  const profile = await instaApi.getProfile(username);
139
139
  const result = await instaApi.followUser(profile.id);
140
140
  return {
@@ -151,7 +151,7 @@ const createInstaProvider = ({ sessionPath }) => {
151
151
 
152
152
  async unfollow({ username } = {}) {
153
153
  return withProviderSession(async () => {
154
- if (!username) throw new Error('username 필요합니다.');
154
+ if (!username) throw new Error('username is required.');
155
155
  const profile = await instaApi.getProfile(username);
156
156
  const result = await instaApi.unfollowUser(profile.id);
157
157
  return {
@@ -168,7 +168,7 @@ const createInstaProvider = ({ sessionPath }) => {
168
168
  async like({ postId } = {}) {
169
169
  return withProviderSession(async () => {
170
170
  const shortcode = String(postId || '').trim();
171
- if (!shortcode) throw new Error('postId(shortcode) 필요합니다.');
171
+ if (!shortcode) throw new Error('postId (shortcode) is required.');
172
172
  const mediaId = await instaApi.getMediaIdFromShortcode(shortcode);
173
173
  const result = await instaApi.likePost(mediaId);
174
174
  return { provider: 'insta', mode: 'like', postId: shortcode, status: result.status };
@@ -178,7 +178,7 @@ const createInstaProvider = ({ sessionPath }) => {
178
178
  async unlike({ postId } = {}) {
179
179
  return withProviderSession(async () => {
180
180
  const shortcode = String(postId || '').trim();
181
- if (!shortcode) throw new Error('postId(shortcode) 필요합니다.');
181
+ if (!shortcode) throw new Error('postId (shortcode) is required.');
182
182
  const mediaId = await instaApi.getMediaIdFromShortcode(shortcode);
183
183
  const result = await instaApi.unlikePost(mediaId);
184
184
  return { provider: 'insta', mode: 'unlike', postId: shortcode, status: result.status };
@@ -187,7 +187,7 @@ const createInstaProvider = ({ sessionPath }) => {
187
187
 
188
188
  async likeComment({ commentId } = {}) {
189
189
  return withProviderSession(async () => {
190
- if (!commentId) throw new Error('commentId 필요합니다.');
190
+ if (!commentId) throw new Error('commentId is required.');
191
191
  const result = await instaApi.likeComment(commentId);
192
192
  return { provider: 'insta', mode: 'likeComment', commentId, status: result.status };
193
193
  });
@@ -195,7 +195,7 @@ const createInstaProvider = ({ sessionPath }) => {
195
195
 
196
196
  async unlikeComment({ commentId } = {}) {
197
197
  return withProviderSession(async () => {
198
- if (!commentId) throw new Error('commentId 필요합니다.');
198
+ if (!commentId) throw new Error('commentId is required.');
199
199
  const result = await instaApi.unlikeComment(commentId);
200
200
  return { provider: 'insta', mode: 'unlikeComment', commentId, status: result.status };
201
201
  });
@@ -206,10 +206,10 @@ const createInstaProvider = ({ sessionPath }) => {
206
206
  const shortcode = String(postId || '').trim();
207
207
  const commentText = String(text || '').trim();
208
208
  if (!shortcode) {
209
- throw new Error('postId(shortcode) 필요합니다.');
209
+ throw new Error('postId (shortcode) is required.');
210
210
  }
211
211
  if (!commentText) {
212
- throw new Error('댓글 내용(text)이 필요합니다.');
212
+ throw new Error('Comment text is required.');
213
213
  }
214
214
  const mediaId = await instaApi.getMediaIdFromShortcode(shortcode);
215
215
  const result = await instaApi.addComment(mediaId, commentText);
@@ -228,7 +228,7 @@ const createInstaProvider = ({ sessionPath }) => {
228
228
  async publish({ imageUrl, imagePath, caption = '' } = {}) {
229
229
  return withProviderSession(async () => {
230
230
  if (!imageUrl && !imagePath) {
231
- throw new Error('imageUrl 또는 imagePath 필요합니다.');
231
+ throw new Error('Either imageUrl or imagePath is required.');
232
232
  }
233
233
  const result = await instaApi.publishPost({ imageUrl, imagePath, caption });
234
234
  return {
@@ -243,7 +243,7 @@ const createInstaProvider = ({ sessionPath }) => {
243
243
  return withProviderSession(async () => {
244
244
  const shortcode = String(postId || '').trim();
245
245
  if (!shortcode) {
246
- throw new Error('postId(shortcode) 필요합니다.');
246
+ throw new Error('postId (shortcode) is required.');
247
247
  }
248
248
  const analysis = await smart.analyzePost({ shortcode });
249
249
  return {
@@ -258,7 +258,7 @@ const createInstaProvider = ({ sessionPath }) => {
258
258
  return withProviderSession(async () => {
259
259
  const shortcode = String(postId || '').trim();
260
260
  if (!shortcode) {
261
- throw new Error('postId(shortcode) 필요합니다.');
261
+ throw new Error('postId (shortcode) is required.');
262
262
  }
263
263
  const mediaId = await instaApi.getMediaIdFromShortcode(shortcode);
264
264
  const result = await instaApi.deletePost(mediaId);
@@ -271,6 +271,16 @@ const createInstaProvider = ({ sessionPath }) => {
271
271
  });
272
272
  },
273
273
 
274
+ async resolveChallenge() {
275
+ const resolved = await instaApi.resolveChallenge();
276
+ return {
277
+ provider: 'insta',
278
+ mode: 'resolveChallenge',
279
+ resolved,
280
+ message: resolved ? 'Challenge resolved successfully.' : 'Challenge resolution failed. Please handle it manually in the browser.',
281
+ };
282
+ },
283
+
274
284
  rateLimitStatus() {
275
285
  return {
276
286
  provider: 'insta',
@@ -33,7 +33,7 @@ const loadInstaSession = (sessionPath) => {
33
33
  return Array.isArray(raw?.cookies) ? raw.cookies : null;
34
34
  };
35
35
 
36
- // ── Rate Limit 영속화 (userId) ──
36
+ // ── Rate Limit persistence (per userId) ──
37
37
 
38
38
  const loadRateLimits = (sessionPath, userId) => {
39
39
  const raw = readSessionFile(sessionPath);
@@ -92,7 +92,7 @@ const createInstaWithProviderSession = (askForAuthentication) => async (fn) => {
92
92
  });
93
93
 
94
94
  if (!loginResult.loggedIn) {
95
- throw new Error(loginResult.message || '세션 갱신 로그인 상태가 확인되지 않았습니다.');
95
+ throw new Error(loginResult.message || 'Login status could not be confirmed after session refresh.');
96
96
  }
97
97
 
98
98
  return fn();
@@ -1,6 +1,6 @@
1
1
  const createSmartComment = (instaApi) => {
2
2
  const analyzePost = async ({ shortcode }) => {
3
- // 1. 게시물 상세
3
+ // 1. Post detail
4
4
  const post = await instaApi.getPostDetail(shortcode);
5
5
  const caption = post.caption || '';
6
6
  const isVideo = post.isVideo;
@@ -8,15 +8,15 @@ const createSmartComment = (instaApi) => {
8
8
  const thumbnailUrl = post.imageUrl;
9
9
  const ownerUsername = post.owner?.username || '';
10
10
 
11
- // 2. 작성자 프로필
11
+ // 2. Owner profile
12
12
  let ownerProfile = null;
13
13
  try {
14
14
  ownerProfile = await instaApi.getProfile(ownerUsername);
15
15
  } catch {
16
- // 비공개 실패 무시
16
+ // Ignore failures (e.g., private account)
17
17
  }
18
18
 
19
- // 3. 썸네일 이미지 base64 (Claude Code Vision)
19
+ // 3. Thumbnail image base64 (for Claude Code Vision)
20
20
  let thumbnailBase64 = null;
21
21
  let thumbnailMediaType = 'image/jpeg';
22
22
  if (thumbnailUrl) {
@@ -29,15 +29,15 @@ const createSmartComment = (instaApi) => {
29
29
  if (ct) thumbnailMediaType = ct;
30
30
  }
31
31
  } catch {
32
- // 실패해도 캡션만으로 진행
32
+ // Proceed with caption only on failure
33
33
  }
34
34
  }
35
35
 
36
36
  const contentType = isVideo
37
- ? '영상(릴스)'
37
+ ? 'video (reel)'
38
38
  : mediaType?.includes('Sidecar')
39
- ? '캐러셀(다중 이미지)'
40
- : '사진';
39
+ ? 'carousel (multiple images)'
40
+ : 'photo';
41
41
 
42
42
  return {
43
43
  shortcode,
@@ -10,10 +10,10 @@ const readInstaCredentials = () => {
10
10
  const parseInstaSessionError = (error) => {
11
11
  const message = String(error?.message || '').toLowerCase();
12
12
  return [
13
- '세션 파일이 없습니다',
14
- '세션에 유효한 쿠키',
15
- '세션이 만료',
16
- '로그인이 필요합니다',
13
+ 'no session file found',
14
+ 'no valid cookies in session',
15
+ 'session expired',
16
+ 'login required',
17
17
  'login_required',
18
18
  'checkpoint_required',
19
19
  '401',
@@ -22,7 +22,7 @@ const parseInstaSessionError = (error) => {
22
22
  ].some((token) => message.includes(token.toLowerCase()));
23
23
  };
24
24
 
25
- const buildLoginErrorMessage = (error) => String(error?.message || '세션 검증에 실패했습니다.');
25
+ const buildLoginErrorMessage = (error) => String(error?.message || 'Session validation failed.');
26
26
 
27
27
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
28
28