viruagent-cli 0.4.2 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "viruagent-cli",
3
- "version": "0.4.2",
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,21 +172,21 @@ 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
176
  }
177
177
  if (location.includes('/challenge')) {
178
- // challenge 자동 해결 시도
178
+ // Attempt automatic challenge resolution
179
179
  const resolved = await resolveChallenge();
180
180
  if (resolved) {
181
- // 해결 원래 요청 재시도
181
+ // Retry original request after resolution
182
182
  return fetch(url, { ...options, headers, redirect: 'manual' });
183
183
  }
184
- throw new Error('challenge_required: 본인 인증이 필요합니다. 브라우저에서 수동으로 처리해 주세요.');
184
+ throw new Error('challenge_required: Identity verification required. Please complete it manually in the browser.');
185
185
  }
186
- throw new Error(`리다이렉트 발생: ${res.status} → ${location}`);
186
+ throw new Error(`Redirect occurred: ${res.status} → ${location}`);
187
187
  }
188
188
 
189
- // challenge_required JSON 응답 처리
189
+ // Handle challenge_required JSON response
190
190
  if (res.status === 400 && !options.allowError) {
191
191
  const cloned = res.clone();
192
192
  try {
@@ -196,20 +196,20 @@ const createInstaApiClient = ({ sessionPath }) => {
196
196
  if (resolved) {
197
197
  return fetch(url, { ...options, headers, redirect: 'manual' });
198
198
  }
199
- throw new Error('challenge_required: 본인 인증이 필요합니다.');
199
+ throw new Error('challenge_required: Identity verification required.');
200
200
  }
201
201
  } catch (e) {
202
202
  if (e.message.includes('challenge_required')) throw e;
203
- // JSON 파싱 실패는 무시하고 원래 흐름
203
+ // Ignore JSON parse failure and continue original flow
204
204
  }
205
205
  }
206
206
 
207
207
  if (res.status === 401 || res.status === 403) {
208
- throw new Error(`인증 오류 (${res.status}). 다시 로그인해 주세요.`);
208
+ throw new Error(`Authentication error (${res.status}). Please log in again.`);
209
209
  }
210
210
 
211
211
  if (!res.ok && !options.allowError) {
212
- throw new Error(`Instagram API 오류: ${res.status} ${res.statusText}`);
212
+ throw new Error(`Instagram API error: ${res.status} ${res.statusText}`);
213
213
  }
214
214
 
215
215
  return res;
@@ -221,7 +221,7 @@ const createInstaApiClient = ({ sessionPath }) => {
221
221
  );
222
222
  const data = await res.json();
223
223
  const user = data?.data?.user;
224
- if (!user) throw new Error(`프로필을 찾을 없습니다: ${username}`);
224
+ if (!user) throw new Error(`Profile not found: ${username}`);
225
225
  return {
226
226
  id: user.id,
227
227
  username: user.username,
@@ -322,7 +322,7 @@ const createInstaApiClient = ({ sessionPath }) => {
322
322
  has_threaded_comments: true,
323
323
  });
324
324
  const media = data?.data?.xdt_shortcode_media || data?.data?.shortcode_media;
325
- if (!media) throw new Error(`게시물을 찾을 없습니다: ${shortcode}`);
325
+ if (!media) throw new Error(`Post not found: ${shortcode}`);
326
326
  return {
327
327
  id: media.id,
328
328
  code: media.shortcode,
@@ -343,7 +343,7 @@ const createInstaApiClient = ({ sessionPath }) => {
343
343
  };
344
344
  };
345
345
 
346
- // ── Challenge 자동 해결 ──
346
+ // ── Automatic Challenge Resolution ──
347
347
 
348
348
  const resolveChallenge = async () => {
349
349
  try {
@@ -379,17 +379,17 @@ const createInstaApiClient = ({ sessionPath }) => {
379
379
  if (data.spam) {
380
380
  throw new Error(`rate_limit: ${data.feedback_title || 'Try Again Later'}`);
381
381
  }
382
- // 이미 좋아요/취소 상태
382
+ // Already liked/unliked state
383
383
  return { status: 'already', message: data.message };
384
384
  }
385
- throw new Error(`Instagram API 오류: ${res.status}`);
385
+ throw new Error(`Instagram API error: ${res.status}`);
386
386
  };
387
387
 
388
388
  const withDelay = async (type, fn) => {
389
- // 한도 체크
389
+ // Check limits
390
390
  checkLimit(type);
391
391
 
392
- // 랜덤 딜레이
392
+ // Random delay
393
393
  const [min, max] = DELAY[type] || [20, 40];
394
394
  const elapsed = (Date.now() - lastActionTime) / 1000;
395
395
  if (lastActionTime > 0 && elapsed < min) {
@@ -575,7 +575,7 @@ const createInstaApiClient = ({ sessionPath }) => {
575
575
  );
576
576
  const data = await res.json();
577
577
  if (data.status !== 'ok') {
578
- throw new Error(`이미지 업로드 실패: ${data.message || 'unknown'}`);
578
+ throw new Error(`Image upload failed: ${data.message || 'unknown'}`);
579
579
  }
580
580
  return uploadId;
581
581
  };
@@ -601,7 +601,7 @@ const createInstaApiClient = ({ sessionPath }) => {
601
601
  });
602
602
  const data = await res.json();
603
603
  if (data.status !== 'ok') {
604
- throw new Error(`게시물 생성 실패: ${data.message || 'unknown'}`);
604
+ throw new Error(`Post creation failed: ${data.message || 'unknown'}`);
605
605
  }
606
606
  return {
607
607
  id: data.media?.pk,
@@ -617,10 +617,10 @@ const createInstaApiClient = ({ sessionPath }) => {
617
617
  imageBuffer = fs.readFileSync(imagePath);
618
618
  } else if (imageUrl) {
619
619
  const res = await fetch(imageUrl);
620
- if (!res.ok) throw new Error(`이미지 다운로드 실패: ${res.status}`);
620
+ if (!res.ok) throw new Error(`Image download failed: ${res.status}`);
621
621
  imageBuffer = Buffer.from(await res.arrayBuffer());
622
622
  } else {
623
- throw new Error('imageUrl 또는 imagePath 필요합니다.');
623
+ throw new Error('Either imageUrl or imagePath is required.');
624
624
  }
625
625
 
626
626
  const uploadId = await uploadPhoto(imageBuffer);
@@ -676,7 +676,7 @@ const createInstaApiClient = ({ sessionPath }) => {
676
676
  status[type] = {
677
677
  hourly: `${c.hourly}/${HOURLY_LIMIT[type]}`,
678
678
  daily: `${c.daily}/${DAILY_LIMIT[type]}`,
679
- delay: `${DELAY[type]?.[0]}~${DELAY[type]?.[1]}초`,
679
+ delay: `${DELAY[type]?.[0]}~${DELAY[type]?.[1]}s`,
680
680
  };
681
681
  }
682
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('; ');
@@ -100,7 +100,7 @@ const createAskForAuthentication = ({ sessionPath }) => async ({
100
100
  const loginData = await loginRes.json();
101
101
 
102
102
  if (loginData.checkpoint_url || loginData.message === 'challenge_required') {
103
- // challenge 자동 해결 시도 (choice=0 = 본인입니다)
103
+ // Attempt automatic challenge resolution (choice=0 = "This was me")
104
104
  const challengeRes = await fetch('https://www.instagram.com/api/v1/challenge/web/action/', {
105
105
  method: 'POST',
106
106
  headers: {
@@ -120,11 +120,11 @@ const createAskForAuthentication = ({ sessionPath }) => async ({
120
120
  const challengeData = await challengeRes.json().catch(() => ({}));
121
121
  if (challengeData.status !== 'ok') {
122
122
  throw new Error(
123
- 'challenge_required: 자동 해결 실패. 브라우저에서 본인 인증을 완료해 주세요. ' +
123
+ 'challenge_required: Automatic resolution failed. Please complete identity verification in the browser. ' +
124
124
  (loginData.checkpoint_url || ''),
125
125
  );
126
126
  }
127
- // challenge 해결 재로그인
127
+ // Re-login after challenge resolution
128
128
  const retryRes = await fetch('https://www.instagram.com/api/v1/web/accounts/login/ajax/', {
129
129
  method: 'POST',
130
130
  headers: {
@@ -158,16 +158,16 @@ const createAskForAuthentication = ({ sessionPath }) => async ({
158
158
 
159
159
  if (loginData.two_factor_required) {
160
160
  throw new Error(
161
- '2단계 인증(2FA) 필요합니다. 브라우저에서 먼저 인증을 완료해 주세요.',
161
+ 'Two-factor authentication (2FA) is required. Please complete verification in the browser first.',
162
162
  );
163
163
  }
164
164
 
165
165
  if (!loginData.authenticated) {
166
166
  const reason = loginData.message || loginData.status || 'unknown';
167
- throw new Error(`인스타그램 로그인 실패: ${reason}`);
167
+ throw new Error(`Instagram login failed: ${reason}`);
168
168
  }
169
169
 
170
- // 세션 저장
170
+ // Save session
171
171
  saveInstaSession(sessionPath, cookies);
172
172
 
173
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);
@@ -277,7 +277,7 @@ const createInstaProvider = ({ sessionPath }) => {
277
277
  provider: 'insta',
278
278
  mode: 'resolveChallenge',
279
279
  resolved,
280
- message: resolved ? 'Challenge 해결 완료' : 'Challenge 해결 실패. 브라우저에서 수동으로 처리해 주세요.',
280
+ message: resolved ? 'Challenge resolved successfully.' : 'Challenge resolution failed. Please handle it manually in the browser.',
281
281
  };
282
282
  },
283
283
 
@@ -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