viruagent-cli 0.3.6 → 0.4.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.
@@ -13,7 +13,6 @@ const {
13
13
  const { normalizeThumbnailForPublish } = require('./imageNormalization');
14
14
  const { enrichContentWithUploadedImages, resolveMandatoryThumbnail } = require('./imageEnrichment');
15
15
  const { createWithProviderSession } = require('./session');
16
- const { importSessionFromChrome } = require('./chromeImport');
17
16
  const { createAskForAuthentication } = require('./auth');
18
17
 
19
18
  const createTistoryProvider = ({ sessionPath }) => {
@@ -70,25 +69,7 @@ const createTistoryProvider = ({ sessionPath }) => {
70
69
  username,
71
70
  password,
72
71
  twoFactorCode,
73
- fromChrome,
74
- profile,
75
72
  } = {}) {
76
- if (fromChrome) {
77
- await importSessionFromChrome(sessionPath, profile || 'Default');
78
- tistoryApi.resetState();
79
- const blogName = await tistoryApi.initBlog();
80
- const result = {
81
- provider: 'tistory',
82
- loggedIn: true,
83
- blogName,
84
- blogUrl: `https://${blogName}.tistory.com`,
85
- sessionPath,
86
- source: 'chrome-import',
87
- };
88
- saveProviderMeta('tistory', { loggedIn: true, blogName, blogUrl: result.blogUrl, sessionPath });
89
- return result;
90
- }
91
-
92
73
  const creds = readCredentialsFromEnv();
93
74
  const resolved = {
94
75
  headless,
package/src/runner.js CHANGED
@@ -46,19 +46,27 @@ const runCommand = async (command, opts = {}) => {
46
46
  }
47
47
 
48
48
  if (command === 'install-skill') {
49
- const skillSrc = path.resolve(__dirname, '..', 'skills', 'viruagent.md');
50
- if (!fs.existsSync(skillSrc)) {
51
- throw createError('FILE_NOT_FOUND', 'Skill file not found in package');
52
- }
49
+ const skillsDir = path.resolve(__dirname, '..', 'skills');
50
+ const skillFiles = ['viruagent.md', 'viruagent-tistory.md', 'viruagent-naver.md', 'viruagent-insta.md'];
53
51
 
54
- // Detect target: Claude Code (~/.claude/commands/) or custom
55
52
  const targetDir = opts.target
56
53
  || path.join(os.homedir(), '.claude', 'commands');
57
54
  fs.mkdirSync(targetDir, { recursive: true });
58
55
 
59
- const dest = path.join(targetDir, 'viruagent.md');
60
- fs.copyFileSync(skillSrc, dest);
61
- return { installed: true, path: dest };
56
+ const installed = [];
57
+ for (const file of skillFiles) {
58
+ const src = path.join(skillsDir, file);
59
+ if (!fs.existsSync(src)) continue;
60
+ const dest = path.join(targetDir, file);
61
+ fs.copyFileSync(src, dest);
62
+ installed.push(dest);
63
+ }
64
+
65
+ if (installed.length === 0) {
66
+ throw createError('FILE_NOT_FOUND', 'Skill files not found in package');
67
+ }
68
+
69
+ return { installed: true, paths: installed, count: installed.length };
62
70
  }
63
71
 
64
72
  const manager = createProviderManager();
@@ -97,8 +105,6 @@ const runCommand = async (command, opts = {}) => {
97
105
  username: opts.username || undefined,
98
106
  password: opts.password || undefined,
99
107
  twoFactorCode: opts.twoFactorCode || undefined,
100
- fromChrome: Boolean(opts.fromChrome),
101
- profile: opts.profile || undefined,
102
108
  })
103
109
  )();
104
110
 
@@ -151,7 +157,10 @@ const runCommand = async (command, opts = {}) => {
151
157
 
152
158
  case 'list-posts':
153
159
  return withProvider(() =>
154
- provider.listPosts({ limit: parseIntOrNull(opts.limit) || 20 })
160
+ provider.listPosts({
161
+ username: opts.username || undefined,
162
+ limit: parseIntOrNull(opts.limit) || 20,
163
+ })
155
164
  )();
156
165
 
157
166
  case 'read-post': {
@@ -169,6 +178,68 @@ const runCommand = async (command, opts = {}) => {
169
178
  case 'logout':
170
179
  return withProvider(() => provider.logout())();
171
180
 
181
+ // ── Instagram 전용 (다른 프로바이더에도 메서드가 있으면 동작) ──
182
+
183
+ case 'get-profile':
184
+ if (!opts.username) {
185
+ throw createError('MISSING_PARAM', 'get-profile requires --username');
186
+ }
187
+ return withProvider(() => provider.getProfile({ username: opts.username }))();
188
+
189
+ case 'get-feed':
190
+ return withProvider(() => provider.getFeed())();
191
+
192
+ case 'like':
193
+ if (!opts.postId) {
194
+ throw createError('MISSING_PARAM', 'like requires --post-id');
195
+ }
196
+ return withProvider(() => provider.like({ postId: opts.postId }))();
197
+
198
+ case 'unlike':
199
+ if (!opts.postId) {
200
+ throw createError('MISSING_PARAM', 'unlike requires --post-id');
201
+ }
202
+ return withProvider(() => provider.unlike({ postId: opts.postId }))();
203
+
204
+ case 'comment':
205
+ if (!opts.postId || !opts.text) {
206
+ throw createError('MISSING_PARAM', 'comment requires --post-id and --text');
207
+ }
208
+ return withProvider(() => provider.comment({ postId: opts.postId, text: opts.text }))();
209
+
210
+ case 'follow':
211
+ if (!opts.username) {
212
+ throw createError('MISSING_PARAM', 'follow requires --username');
213
+ }
214
+ return withProvider(() => provider.follow({ username: opts.username }))();
215
+
216
+ case 'unfollow':
217
+ if (!opts.username) {
218
+ throw createError('MISSING_PARAM', 'unfollow requires --username');
219
+ }
220
+ return withProvider(() => provider.unfollow({ username: opts.username }))();
221
+
222
+ case 'like-comment':
223
+ if (!opts.commentId) {
224
+ throw createError('MISSING_PARAM', 'like-comment requires --comment-id');
225
+ }
226
+ return withProvider(() => provider.likeComment({ commentId: opts.commentId }))();
227
+
228
+ case 'unlike-comment':
229
+ if (!opts.commentId) {
230
+ throw createError('MISSING_PARAM', 'unlike-comment requires --comment-id');
231
+ }
232
+ return withProvider(() => provider.unlikeComment({ commentId: opts.commentId }))();
233
+
234
+ case 'analyze-post':
235
+ if (!opts.postId) {
236
+ throw createError('MISSING_PARAM', 'analyze-post requires --post-id');
237
+ }
238
+ return withProvider(() => provider.analyzePost({ postId: opts.postId }))();
239
+
240
+ case 'rate-limit-status':
241
+ return withProvider(() => Promise.resolve(provider.rateLimitStatus()))();
242
+
172
243
  default:
173
244
  throw createError('UNKNOWN_COMMAND', `Unknown command: ${command}`, 'viruagent-cli --spec');
174
245
  }
@@ -2,13 +2,15 @@ const path = require('path');
2
2
  const { getSessionPath } = require('../storage/sessionStore');
3
3
  const createTistoryProvider = require('../providers/tistory');
4
4
  const createNaverProvider = require('../providers/naver');
5
+ const createInstaProvider = require('../providers/insta');
5
6
 
6
7
  const providerFactory = {
7
8
  tistory: createTistoryProvider,
8
9
  naver: createNaverProvider,
10
+ insta: createInstaProvider,
9
11
  };
10
12
 
11
- const providers = ['tistory', 'naver'];
13
+ const providers = ['tistory', 'naver', 'insta'];
12
14
 
13
15
  const createProviderManager = () => {
14
16
  const cache = new Map();
@@ -32,9 +34,10 @@ const createProviderManager = () => {
32
34
  return cache.get(normalized);
33
35
  };
34
36
 
37
+ const providerNames = { tistory: 'Tistory', naver: 'Naver Blog', insta: 'Instagram' };
35
38
  const getAvailableProviders = () => providers.map((provider) => ({
36
39
  id: provider,
37
- name: provider === 'tistory' ? 'Tistory' : 'Naver Blog',
40
+ name: providerNames[provider] || provider,
38
41
  }));
39
42
 
40
43
  return { getProvider, getAvailableProviders };
@@ -1,195 +0,0 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const os = require('os');
4
- const crypto = require('crypto');
5
- const { execSync, execFileSync } = require('child_process');
6
-
7
- const NAVER_SESSION_COOKIES = ['NID_AUT', 'NID_SES'];
8
-
9
- const decryptChromeCookieMac = (encryptedValue, derivedKey) => {
10
- if (!encryptedValue || encryptedValue.length < 4) return '';
11
- const prefix = encryptedValue.slice(0, 3).toString('ascii');
12
- if (prefix !== 'v10') return encryptedValue.toString('utf-8');
13
-
14
- const encrypted = encryptedValue.slice(3);
15
- const iv = Buffer.alloc(16, 0x20);
16
- const decipher = crypto.createDecipheriv('aes-128-cbc', derivedKey, iv);
17
- decipher.setAutoPadding(true);
18
- try {
19
- const dec = Buffer.concat([decipher.update(encrypted), decipher.final()]);
20
- let start = dec.length;
21
- for (let i = dec.length - 1; i >= 0; i--) {
22
- if (dec[i] >= 0x20 && dec[i] <= 0x7e) { start = i; }
23
- else { break; }
24
- }
25
- return start < dec.length ? dec.slice(start).toString('utf-8') : '';
26
- } catch {
27
- return '';
28
- }
29
- };
30
-
31
- const getWindowsChromeMasterKey = (chromeRoot) => {
32
- const localStatePath = path.join(chromeRoot, 'Local State');
33
- if (!fs.existsSync(localStatePath)) {
34
- throw new Error('Chrome Local State 파일을 찾을 수 없습니다.');
35
- }
36
- const localState = JSON.parse(fs.readFileSync(localStatePath, 'utf-8'));
37
- const encryptedKeyB64 = localState.os_crypt && localState.os_crypt.encrypted_key;
38
- if (!encryptedKeyB64) {
39
- throw new Error('Chrome Local State에서 암호화 키를 찾을 수 없습니다.');
40
- }
41
- const encryptedKeyWithPrefix = Buffer.from(encryptedKeyB64, 'base64');
42
- const encryptedKey = encryptedKeyWithPrefix.slice(5);
43
- const encHex = encryptedKey.toString('hex');
44
-
45
- const psScript = `
46
- Add-Type -AssemblyName System.Security
47
- $encBytes = [byte[]]::new(${encryptedKey.length})
48
- $hex = '${encHex}'
49
- for ($i = 0; $i -lt $encBytes.Length; $i++) {
50
- $encBytes[$i] = [Convert]::ToByte($hex.Substring($i * 2, 2), 16)
51
- }
52
- $decBytes = [System.Security.Cryptography.ProtectedData]::Unprotect($encBytes, $null, [System.Security.Cryptography.DataProtectionScope]::CurrentUser)
53
- $decHex = -join ($decBytes | ForEach-Object { $_.ToString('x2') })
54
- Write-Output $decHex
55
- `.trim().replace(/\n/g, '; ');
56
-
57
- try {
58
- const decHex = execSync(
59
- `powershell -NoProfile -Command "${psScript}"`,
60
- { encoding: 'utf-8', timeout: 10000 }
61
- ).trim();
62
- return Buffer.from(decHex, 'hex');
63
- } catch {
64
- throw new Error('Chrome 암호화 키를 DPAPI로 복호화할 수 없습니다.');
65
- }
66
- };
67
-
68
- const decryptChromeCookieWindows = (encryptedValue, masterKey) => {
69
- if (!encryptedValue || encryptedValue.length < 4) return '';
70
- const prefix = encryptedValue.slice(0, 3).toString('ascii');
71
- if (prefix !== 'v10' && prefix !== 'v20') return encryptedValue.toString('utf-8');
72
-
73
- const nonce = encryptedValue.slice(3, 3 + 12);
74
- const authTag = encryptedValue.slice(encryptedValue.length - 16);
75
- const ciphertext = encryptedValue.slice(3 + 12, encryptedValue.length - 16);
76
-
77
- try {
78
- const decipher = crypto.createDecipheriv('aes-256-gcm', masterKey, nonce);
79
- decipher.setAuthTag(authTag);
80
- const dec = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
81
- return dec.toString('utf-8');
82
- } catch {
83
- return '';
84
- }
85
- };
86
-
87
- const decryptChromeCookie = (encryptedValue, key) => {
88
- if (process.platform === 'win32') {
89
- return decryptChromeCookieWindows(encryptedValue, key);
90
- }
91
- return decryptChromeCookieMac(encryptedValue, key);
92
- };
93
-
94
- const extractChromeCookies = (cookiesDb, derivedKey, domainPattern) => {
95
- const tempDb = path.join(os.tmpdir(), `viruagent-naver-cookies-${Date.now()}.db`);
96
-
97
- const backupCmd = process.platform === 'win32'
98
- ? `.backup "${tempDb}"`
99
- : `.backup '${tempDb.replace(/'/g, "''")}'`;
100
- try {
101
- execFileSync('sqlite3', [cookiesDb, backupCmd], { stdio: 'ignore', timeout: 10000 });
102
- } catch {
103
- let copied = false;
104
- try {
105
- fs.copyFileSync(cookiesDb, tempDb);
106
- copied = true;
107
- } catch {}
108
- if (!copied) {
109
- throw new Error('Chrome 쿠키 DB 복사에 실패했습니다. Chrome이 실행 중이면 종료 후 다시 시도해 주세요.');
110
- }
111
- }
112
-
113
- for (const suffix of ['-wal', '-shm', '-journal']) {
114
- try { fs.unlinkSync(tempDb + suffix); } catch {}
115
- }
116
-
117
- try {
118
- const query = `SELECT host_key, name, value, hex(encrypted_value), path, expires_utc, is_secure, is_httponly, samesite FROM cookies WHERE host_key LIKE '${domainPattern}'`;
119
- const rows = execFileSync('sqlite3', ['-separator', '||', tempDb, query], {
120
- encoding: 'utf-8',
121
- timeout: 5000,
122
- }).trim();
123
- if (!rows) return [];
124
-
125
- const chromeEpochOffset = 11644473600;
126
- const sameSiteMap = { '-1': 'None', '0': 'None', '1': 'Lax', '2': 'Strict' };
127
- return rows.split('\n').map((row) => {
128
- const [domain, name, plainValue, encHex, cookiePath, expiresUtc, isSecure, isHttpOnly, sameSite] = row.split('||');
129
- let value = plainValue || '';
130
- if (!value && encHex) {
131
- value = decryptChromeCookie(Buffer.from(encHex, 'hex'), derivedKey);
132
- }
133
- if (value && !/^[\x20-\x7E]*$/.test(value)) value = '';
134
- const expires = expiresUtc === '0' ? -1 : Math.floor(Number(expiresUtc) / 1000000) - chromeEpochOffset;
135
- return { name, value, domain, path: cookiePath || '/', expires, httpOnly: isHttpOnly === '1', secure: isSecure === '1', sameSite: sameSiteMap[sameSite] || 'None' };
136
- }).filter((c) => c.value);
137
- } finally {
138
- try { fs.unlinkSync(tempDb); } catch {}
139
- }
140
- };
141
-
142
- const importSessionFromChrome = async (targetSessionPath, profileName = 'Default') => {
143
- let chromeRoot;
144
- if (process.platform === 'win32') {
145
- chromeRoot = path.join(process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'), 'Google', 'Chrome', 'User Data');
146
- } else {
147
- chromeRoot = path.join(os.homedir(), 'Library', 'Application Support', 'Google', 'Chrome');
148
- }
149
- if (!fs.existsSync(chromeRoot)) {
150
- throw new Error('Chrome이 설치되어 있지 않습니다.');
151
- }
152
-
153
- const profileDir = path.join(chromeRoot, profileName);
154
- let cookiesDb = path.join(profileDir, 'Network', 'Cookies');
155
- if (!fs.existsSync(cookiesDb)) {
156
- cookiesDb = path.join(profileDir, 'Cookies');
157
- }
158
- if (!fs.existsSync(cookiesDb)) {
159
- throw new Error(`Chrome 프로필 "${profileName}"에 쿠키 DB가 없습니다.`);
160
- }
161
-
162
- let derivedKey;
163
- if (process.platform === 'win32') {
164
- derivedKey = getWindowsChromeMasterKey(chromeRoot);
165
- } else {
166
- let keychainPassword;
167
- try {
168
- keychainPassword = execSync(
169
- 'security find-generic-password -s "Chrome Safe Storage" -w',
170
- { encoding: 'utf-8', timeout: 5000 }
171
- ).trim();
172
- } catch {
173
- throw new Error('Chrome Safe Storage 키를 Keychain에서 읽을 수 없습니다. macOS 권한을 확인해 주세요.');
174
- }
175
- derivedKey = crypto.pbkdf2Sync(keychainPassword, 'saltysalt', 1003, 16, 'sha1');
176
- }
177
-
178
- const naverCookies = extractChromeCookies(cookiesDb, derivedKey, '%naver.com');
179
-
180
- const hasAuth = naverCookies.some((c) => c.name === 'NID_AUT' && c.value) &&
181
- naverCookies.some((c) => c.name === 'NID_SES' && c.value);
182
-
183
- if (!hasAuth) {
184
- throw new Error('Chrome에 네이버 로그인 세션이 없습니다. Chrome에서 먼저 네이버에 로그인해 주세요.');
185
- }
186
-
187
- const payload = { cookies: naverCookies, updatedAt: new Date().toISOString() };
188
- await fs.promises.mkdir(path.dirname(targetSessionPath), { recursive: true });
189
- await fs.promises.writeFile(targetSessionPath, JSON.stringify(payload, null, 2), 'utf-8');
190
- return { cookieCount: naverCookies.length };
191
- };
192
-
193
- module.exports = {
194
- importSessionFromChrome,
195
- };