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.
- package/bin/index.js +26 -0
- package/package.json +1 -1
- package/src/providers/insta/apiClient.js +92 -54
- package/src/providers/insta/auth.js +10 -10
- package/src/providers/insta/index.js +215 -17
- package/src/providers/insta/session.js +2 -2
- package/src/providers/insta/smartComment.js +8 -8
- package/src/providers/insta/utils.js +5 -5
- package/src/providers/naver/auth.js +19 -19
- package/src/providers/naver/editorConvert.js +16 -16
- package/src/providers/naver/imageUpload.js +7 -7
- package/src/providers/naver/index.js +9 -9
- package/src/providers/naver/session.js +2 -2
- package/src/providers/naver/utils.js +8 -8
- package/src/providers/tistory/auth.js +12 -12
- package/src/providers/tistory/fetchLayer.js +5 -5
- package/src/providers/tistory/imageEnrichment.js +19 -19
- package/src/providers/tistory/imageNormalization.js +1 -1
- package/src/providers/tistory/imageSources.js +5 -5
- package/src/providers/tistory/index.js +15 -15
- package/src/providers/tistory/session.js +3 -3
- package/src/providers/tistory/utils.js +14 -14
- package/src/runner.js +22 -1
- package/src/services/naverApiClient.js +17 -17
- package/src/services/providerManager.js +1 -1
- package/src/services/tistoryApiClient.js +13 -13
|
@@ -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(`[
|
|
14
|
+
console.log(`[Image Trace] ${message}`);
|
|
15
15
|
return;
|
|
16
16
|
}
|
|
17
|
-
console.log(`[
|
|
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
|
-
'
|
|
64
|
-
'
|
|
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
|
-
|
|
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('
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
//
|
|
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('
|
|
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('
|
|
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(
|
|
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('
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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('
|
|
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(
|
|
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(
|
|
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(
|
|
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('
|
|
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(
|
|
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('
|
|
295
|
+
throw new Error('Image upload response does not contain a URL.');
|
|
296
296
|
}
|
|
297
297
|
return uploaded;
|
|
298
298
|
};
|