viruagent-cli 0.4.2 → 0.5.1

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.
@@ -57,8 +57,8 @@ const persistTistorySession = async (context, targetSessionPath) => {
57
57
  };
58
58
 
59
59
  /**
60
- * withProviderSession 팩토리.
61
- * askForAuthentication 외부에서 주입받아 스코프 버그를 해결한다.
60
+ * withProviderSession factory.
61
+ * Receives askForAuthentication via dependency injection to avoid scope issues.
62
62
  */
63
63
  const createWithProviderSession = (askForAuthentication) => async (fn) => {
64
64
  const credentials = readCredentialsFromEnv();
@@ -94,7 +94,7 @@ const createWithProviderSession = (askForAuthentication) => async (fn) => {
94
94
  });
95
95
 
96
96
  if (!loginResult.loggedIn) {
97
- throw new Error(loginResult.message || '세션 갱신 로그인 상태가 확인되지 않았습니다.');
97
+ throw new Error(loginResult.message || 'Login status could not be confirmed after session refresh.');
98
98
  }
99
99
 
100
100
  return fn();
@@ -11,10 +11,10 @@ const imageTrace = (message, data) => {
11
11
  return;
12
12
  }
13
13
  if (data === undefined) {
14
- console.log(`[이미지 추적] ${message}`);
14
+ console.log(`[Image Trace] ${message}`);
15
15
  return;
16
16
  }
17
- console.log(`[이미지 추적] ${message}`, data);
17
+ console.log(`[Image Trace] ${message}`, data);
18
18
  };
19
19
 
20
20
  const MAX_IMAGE_UPLOAD_COUNT = 1;
@@ -57,17 +57,17 @@ const normalizeTagList = (value = '') => {
57
57
  const parseSessionError = (error) => {
58
58
  const message = String(error?.message || '').toLowerCase();
59
59
  return [
60
- '세션이 만료',
61
- '세션에 유효한 쿠키',
62
- '세션 파일이 없습니다',
63
- '블로그 정보 조회 실패: 401',
64
- '블로그 정보 조회 실패: 403',
65
- '세션이 만료되었습니다',
66
- '다시 로그인',
60
+ 'session expired',
61
+ 'valid cookie in session',
62
+ 'session file not found',
63
+ 'blog info fetch failed: 401',
64
+ 'blog info fetch failed: 403',
65
+ 'session has expired',
66
+ 'please log in again',
67
67
  ].some((token) => message.includes(token.toLowerCase()));
68
68
  };
69
69
 
70
- const buildLoginErrorMessage = (error) => String(error?.message || '세션 검증에 실패했습니다.');
70
+ const buildLoginErrorMessage = (error) => String(error?.message || 'Session validation failed.');
71
71
 
72
72
  const promptCategorySelection = async (categories = []) => {
73
73
  if (!process.stdin || !process.stdin.isTTY) {
@@ -79,9 +79,9 @@ const promptCategorySelection = async (categories = []) => {
79
79
 
80
80
  const candidates = categories.map((category, index) => `${index + 1}. ${category.name} (${category.id})`);
81
81
  const lines = [
82
- '발행할 카테고리를 선택해 주세요.',
82
+ 'Please select a category for publishing.',
83
83
  ...candidates,
84
- `입력: 번호(1-${categories.length}) 또는 카테고리 ID (엔터 입력 건너뛰기)`,
84
+ `Enter: number (1-${categories.length}) or category ID (press Enter to skip)`,
85
85
  ];
86
86
  const prompt = `${lines.join('\n')}\n> `;
87
87
 
@@ -126,7 +126,7 @@ const promptCategorySelection = async (categories = []) => {
126
126
  return;
127
127
  }
128
128
 
129
- console.log('잘못된 입력입니다. 번호 또는 카테고리 ID를 다시 입력해 주세요.');
129
+ console.log('Invalid input. Please enter a number or category ID again.');
130
130
  ask(retryCount + 1);
131
131
  });
132
132
  };
@@ -137,7 +137,7 @@ const promptCategorySelection = async (categories = []) => {
137
137
 
138
138
  const isPublishLimitError = (error) => {
139
139
  const message = String(error?.message || '');
140
- return /발행 실패:\s*403/.test(message) || /\b403\b/.test(message);
140
+ return /publish failed:\s*403/i.test(message) || /\b403\b/.test(message);
141
141
  };
142
142
 
143
143
  const isProvidedCategory = (value) => {
package/src/runner.js CHANGED
@@ -178,7 +178,7 @@ const runCommand = async (command, opts = {}) => {
178
178
  case 'logout':
179
179
  return withProvider(() => provider.logout())();
180
180
 
181
- // ── Instagram 전용 (다른 프로바이더에도 메서드가 있으면 동작) ──
181
+ // ── Instagram-specific (works with other providers if the method exists) ──
182
182
 
183
183
  case 'get-profile':
184
184
  if (!opts.username) {
@@ -231,6 +231,27 @@ const runCommand = async (command, opts = {}) => {
231
231
  }
232
232
  return withProvider(() => provider.unlikeComment({ commentId: opts.commentId }))();
233
233
 
234
+ case 'send-dm':
235
+ if (!opts.username && !opts.threadId) {
236
+ throw createError('MISSING_PARAM', 'send-dm requires --username or --thread-id');
237
+ }
238
+ if (!opts.text) {
239
+ throw createError('MISSING_PARAM', 'send-dm requires --text');
240
+ }
241
+ return withProvider(() => provider.sendDm({ username: opts.username, threadId: opts.threadId, text: opts.text }))();
242
+
243
+ case 'list-messages':
244
+ if (!opts.threadId) {
245
+ throw createError('MISSING_PARAM', 'list-messages requires --thread-id');
246
+ }
247
+ return withProvider(() => provider.listMessages({ threadId: opts.threadId }))();
248
+
249
+ case 'list-comments':
250
+ if (!opts.postId) {
251
+ throw createError('MISSING_PARAM', 'list-comments requires --post-id');
252
+ }
253
+ return withProvider(() => provider.listComments({ postId: opts.postId }))();
254
+
234
255
  case 'analyze-post':
235
256
  if (!opts.postId) {
236
257
  throw createError('MISSING_PARAM', 'analyze-post requires --post-id');
@@ -29,19 +29,19 @@ const normalizeCookies = (session) => {
29
29
  const readSessionCookies = (sessionPath) => {
30
30
  const resolvedPath = path.resolve(sessionPath);
31
31
  if (!fs.existsSync(resolvedPath)) {
32
- throw new Error(`세션 파일이 없습니다. ${resolvedPath} 로그인 정보를 먼저 저장하세요.`);
32
+ throw new Error(`Session file not found. Please save login credentials to ${resolvedPath} first.`);
33
33
  }
34
34
 
35
35
  let raw;
36
36
  try {
37
37
  raw = JSON.parse(fs.readFileSync(resolvedPath, 'utf-8'));
38
38
  } catch (error) {
39
- throw new Error(`세션 파일 파싱 실패: ${error.message}`);
39
+ throw new Error(`Failed to parse session file: ${error.message}`);
40
40
  }
41
41
 
42
42
  const cookies = normalizeCookies(raw);
43
43
  if (!cookies.length) {
44
- throw new Error('세션에 유효한 쿠키가 없습니다. 다시 로그인해 주세요.');
44
+ throw new Error('No valid cookies found in session. Please log in again.');
45
45
  }
46
46
 
47
47
  return cookies.join('; ');
@@ -88,7 +88,7 @@ const createNaverApiClient = ({ sessionPath }) => {
88
88
  detail = await response.text();
89
89
  detail = detail ? `: ${detail.slice(0, 200)}` : '';
90
90
  } catch {}
91
- throw new Error(`요청 실패: ${response.status} ${response.statusText}${detail}`);
91
+ throw new Error(`Request failed: ${response.status} ${response.statusText}${detail}`);
92
92
  }
93
93
  return response.json();
94
94
  } finally {
@@ -105,7 +105,7 @@ const createNaverApiClient = ({ sessionPath }) => {
105
105
  ...options,
106
106
  });
107
107
  if (!response.ok) {
108
- throw new Error(`요청 실패: ${response.status} ${response.statusText}`);
108
+ throw new Error(`Request failed: ${response.status} ${response.statusText}`);
109
109
  }
110
110
  return response.text();
111
111
  } finally {
@@ -122,11 +122,11 @@ const createNaverApiClient = ({ sessionPath }) => {
122
122
 
123
123
  const match = html.match(/blogId\s*=\s*'([^']+)'/);
124
124
  if (!match) {
125
- // JSON 응답인 경우도 체크
125
+ // Also check if response is a login page
126
126
  if (html.includes('로그인') || html.includes('login')) {
127
- throw new Error('세션이 만료되었습니다. 다시 로그인해 주세요.');
127
+ throw new Error('Session expired. Please log in again.');
128
128
  }
129
- throw new Error('MyBlog 응답에서 blogId 찾을 없습니다.');
129
+ throw new Error('Could not find blogId in MyBlog response.');
130
130
  }
131
131
 
132
132
  blogId = match[1];
@@ -144,7 +144,7 @@ const createNaverApiClient = ({ sessionPath }) => {
144
144
  }
145
145
  );
146
146
  const token = json?.result?.token;
147
- if (!token) throw new Error('Se-Authorization 토큰을 가져올 수 없습니다.');
147
+ if (!token) throw new Error('Failed to retrieve Se-Authorization token.');
148
148
  return token;
149
149
  };
150
150
 
@@ -185,7 +185,7 @@ const createNaverApiClient = ({ sessionPath }) => {
185
185
  }
186
186
  );
187
187
  const editorId = configJson?.editorInfo?.id;
188
- if (!editorId) throw new Error('에디터 ID를 가져올 없습니다.');
188
+ if (!editorId) throw new Error('Failed to retrieve editor ID.');
189
189
 
190
190
  const managerJson = await requestJson(
191
191
  `${BLOG_HOST}/PostWriteFormManagerOptions.naver?blogId=${encodeURIComponent(id)}&categoryNo=${encodeURIComponent(categoryNo)}`,
@@ -216,7 +216,7 @@ const createNaverApiClient = ({ sessionPath }) => {
216
216
  const uploadImage = async (imageBuffer, filename, token) => {
217
217
  const id = blogId || await initBlog();
218
218
  const sessionKey = await getUploadSessionKey(token);
219
- if (!sessionKey) throw new Error('이미지 업로드 세션 키를 가져올 없습니다.');
219
+ if (!sessionKey) throw new Error('Failed to retrieve image upload session key.');
220
220
 
221
221
  const uploadUrl = `https://blog.upphoto.naver.com/${sessionKey}/simpleUpload/0?userId=${encodeURIComponent(id)}&extractExif=true&extractAnimatedCnt=true&autorotate=true&extractDominantColor=false&denyAnimatedImage=false&skipXcamFiltering=false`;
222
222
 
@@ -238,12 +238,12 @@ const createNaverApiClient = ({ sessionPath }) => {
238
238
  });
239
239
 
240
240
  if (!response.ok) {
241
- throw new Error(`이미지 업로드 실패: ${response.status}`);
241
+ throw new Error(`Image upload failed: ${response.status}`);
242
242
  }
243
243
 
244
244
  const xml = await response.text();
245
245
  if (!xml.includes('<url>')) {
246
- throw new Error('이미지 업로드 응답에 URL이 없습니다.');
246
+ throw new Error('Image upload response does not contain a URL.');
247
247
  }
248
248
 
249
249
  const extractTag = (tag) => {
@@ -309,7 +309,7 @@ const createNaverApiClient = ({ sessionPath }) => {
309
309
  : await getDefaultCategoryNo();
310
310
  const { editorId, editorSource, token } = await getEditorInfo(resolvedCategoryNo);
311
311
 
312
- // content 이미 컴포넌트 배열이면 그대로 사용, 아니면 배열
312
+ // Use content as-is if already a component array, otherwise empty array
313
313
  const contentComponents = Array.isArray(content) ? content : [];
314
314
 
315
315
  const titleComponent = {
@@ -396,12 +396,12 @@ const createNaverApiClient = ({ sessionPath }) => {
396
396
  });
397
397
 
398
398
  if (!response.ok) {
399
- throw new Error(`글 발행 실패: ${response.status}`);
399
+ throw new Error(`Post publish failed: ${response.status}`);
400
400
  }
401
401
 
402
402
  const json = await response.json();
403
403
  if (!json.isSuccess) {
404
- throw new Error(`글 발행 실패: ${JSON.stringify(json).slice(0, 200)}`);
404
+ throw new Error(`Post publish failed: ${JSON.stringify(json).slice(0, 200)}`);
405
405
  }
406
406
 
407
407
  const redirectUrl = json.result?.redirectUrl || '';
@@ -432,7 +432,7 @@ const createNaverApiClient = ({ sessionPath }) => {
432
432
 
433
433
  let json;
434
434
  try {
435
- // 네이버 응답에 잘못된 이스케이프(\') 포함될 수 있어 정리
435
+ // Naver response may contain invalid escape sequences (\'), so sanitize
436
436
  const sanitized = text.replace(/\\'/g, "'");
437
437
  json = JSON.parse(sanitized);
438
438
  } catch {
@@ -18,7 +18,7 @@ const createProviderManager = () => {
18
18
  const getProvider = (provider = 'tistory') => {
19
19
  const normalized = String(provider || 'tistory').toLowerCase();
20
20
  if (!providerFactory[normalized]) {
21
- throw new Error(`지원하지 않는 provider입니다: ${provider}. 가능한 값: ${providers.join(', ')}`);
21
+ throw new Error(`Unsupported provider: ${provider}. Available options: ${providers.join(', ')}`);
22
22
  }
23
23
 
24
24
  if (!cache.has(normalized)) {
@@ -31,19 +31,19 @@ const normalizeCookies = (session) => {
31
31
  const readSessionCookies = (sessionPath) => {
32
32
  const resolvedPath = path.resolve(sessionPath);
33
33
  if (!fs.existsSync(resolvedPath)) {
34
- throw new Error(`세션 파일이 없습니다. ${resolvedPath} 로그인 정보를 먼저 저장하세요.`);
34
+ throw new Error(`Session file not found. Please save login credentials to ${resolvedPath} first.`);
35
35
  }
36
36
 
37
37
  let raw;
38
38
  try {
39
39
  raw = JSON.parse(fs.readFileSync(resolvedPath, 'utf-8'));
40
40
  } catch (error) {
41
- throw new Error(`세션 파일 파싱 실패: ${error.message}`);
41
+ throw new Error(`Failed to parse session file: ${error.message}`);
42
42
  }
43
43
 
44
44
  const cookies = normalizeCookies(raw);
45
45
  if (!cookies.length) {
46
- throw new Error('세션에 유효한 쿠키가 없습니다. 다시 로그인해 주세요.');
46
+ throw new Error('No valid cookies found in session. Please log in again.');
47
47
  }
48
48
 
49
49
  return cookies.join('; ');
@@ -70,7 +70,7 @@ const createTistoryApiClient = ({ sessionPath }) => {
70
70
 
71
71
  const getBase = () => {
72
72
  if (!blogName) {
73
- throw new Error('블로그 이름이 초기화되지 않았습니다. initBlog() 먼저 호출하세요.');
73
+ throw new Error('Blog name not initialized. Call initBlog() first.');
74
74
  }
75
75
  return `https://${blogName}.tistory.com/manage`;
76
76
  };
@@ -131,7 +131,7 @@ const createTistoryApiClient = ({ sessionPath }) => {
131
131
  } catch {
132
132
  detail = '';
133
133
  }
134
- throw new Error(`요청 실패: ${response.status} ${response.statusText}${detail}`);
134
+ throw new Error(`Request failed: ${response.status} ${response.statusText}${detail}`);
135
135
  }
136
136
  return response.json();
137
137
  } finally {
@@ -148,7 +148,7 @@ const createTistoryApiClient = ({ sessionPath }) => {
148
148
  ...options,
149
149
  });
150
150
  if (!response.ok) {
151
- throw new Error(`요청 실패: ${response.status} ${response.statusText}`);
151
+ throw new Error(`Request failed: ${response.status} ${response.statusText}`);
152
152
  }
153
153
  return response.text();
154
154
  } finally {
@@ -181,18 +181,18 @@ const createTistoryApiClient = ({ sessionPath }) => {
181
181
  redirect: 'follow',
182
182
  });
183
183
  if (!response.ok) {
184
- throw new Error(`블로그 정보 조회 실패: ${response.status}`);
184
+ throw new Error(`Failed to fetch blog info: ${response.status}`);
185
185
  }
186
186
 
187
187
  const contentType = response.headers.get('content-type') || '';
188
188
  if (!contentType.includes('application/json')) {
189
- throw new Error('세션이 만료되었습니다. /auth/login으로 다시 로그인하세요.');
189
+ throw new Error('Session expired. Please log in again via /auth/login.');
190
190
  }
191
191
 
192
192
  const json = await response.json();
193
193
  const defaultBlog = (json?.data || []).find((blog) => blog?.defaultBlog) || (json?.data || [])[0];
194
194
  if (!defaultBlog) {
195
- throw new Error('블로그를 찾을 수 없습니다.');
195
+ throw new Error('Blog not found.');
196
196
  }
197
197
 
198
198
  blogName = defaultBlog.name;
@@ -255,14 +255,14 @@ const createTistoryApiClient = ({ sessionPath }) => {
255
255
 
256
256
  const match = html.match(/window\.Config\s*=\s*(\{[\s\S]*?\})\s*(?:\n|;)/);
257
257
  if (!match) {
258
- throw new Error('카테고리 파싱 실패');
258
+ throw new Error('Failed to parse categories');
259
259
  }
260
260
 
261
261
  const sandbox = {};
262
262
  vm.runInNewContext(`var result = ${match[1]};`, sandbox);
263
263
  const rootCategories = sandbox?.result?.blog?.categories;
264
264
  if (!Array.isArray(rootCategories)) {
265
- throw new Error('카테고리 파싱 실패');
265
+ throw new Error('Failed to parse categories');
266
266
  }
267
267
 
268
268
  return flattenCategories(rootCategories, {});
@@ -287,12 +287,12 @@ const createTistoryApiClient = ({ sessionPath }) => {
287
287
 
288
288
  if (!response.ok) {
289
289
  const text = await response.text().catch(() => '');
290
- throw new Error(`이미지 업로드 실패: ${response.status} ${text ? `: ${text.slice(0, 500)}` : ''}`);
290
+ throw new Error(`Image upload failed: ${response.status} ${text ? `: ${text.slice(0, 500)}` : ''}`);
291
291
  }
292
292
 
293
293
  const uploaded = await response.json();
294
294
  if (!uploaded?.url) {
295
- throw new Error('이미지 업로드 응답에 URL이 없습니다.');
295
+ throw new Error('Image upload response does not contain a URL.');
296
296
  }
297
297
  return uploaded;
298
298
  };