viruagent-cli 0.3.2 → 0.3.3

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.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "AI-agent-optimized CLI for blog publishing (Tistory, Naver)",
5
5
  "private": false,
6
6
  "type": "commonjs",
@@ -1945,13 +1945,292 @@ const generateSelfSignedCert = (domain) => {
1945
1945
  return { keyPath, certPath, tempDir };
1946
1946
  };
1947
1947
 
1948
+ const CDP_DEBUG_PORT = 9222;
1949
+
1950
+ const tryConnectCDP = async (port) => {
1951
+ const http = require('http');
1952
+ return new Promise((resolve) => {
1953
+ http.get(`http://127.0.0.1:${port}/json/version`, { timeout: 2000 }, (res) => {
1954
+ let data = '';
1955
+ res.on('data', c => data += c);
1956
+ res.on('end', () => {
1957
+ try {
1958
+ const info = JSON.parse(data);
1959
+ resolve(info.webSocketDebuggerUrl || null);
1960
+ } catch { resolve(null); }
1961
+ });
1962
+ }).on('error', () => resolve(null));
1963
+ });
1964
+ };
1965
+
1966
+ const findChromeDebugPort = async () => {
1967
+ // 1. 고정 포트 9222 시도
1968
+ const ws = await tryConnectCDP(CDP_DEBUG_PORT);
1969
+ if (ws) return { port: CDP_DEBUG_PORT, wsUrl: ws };
1970
+
1971
+ // 2. DevToolsActivePort 파일 확인
1972
+ const dtpPath = path.join(
1973
+ process.env.LOCALAPPDATA || '',
1974
+ 'Google', 'Chrome', 'User Data', 'DevToolsActivePort'
1975
+ );
1976
+ try {
1977
+ const content = fs.readFileSync(dtpPath, 'utf-8').trim();
1978
+ const port = parseInt(content.split('\n')[0], 10);
1979
+ if (port > 0) {
1980
+ const ws2 = await tryConnectCDP(port);
1981
+ if (ws2) return { port, wsUrl: ws2 };
1982
+ }
1983
+ } catch {}
1984
+
1985
+ return null;
1986
+ };
1987
+
1988
+ const enableChromeDebugPort = () => {
1989
+ // Chrome 바로가기에 --remote-debugging-port 추가 (한 번만 실행)
1990
+ if (process.platform !== 'win32') return false;
1991
+
1992
+ const flag = `--remote-debugging-port=${CDP_DEBUG_PORT}`;
1993
+ const shortcutPaths = [];
1994
+
1995
+ // 바탕화면, 시작 메뉴, 작업표시줄 바로가기 검색
1996
+ const locations = [
1997
+ path.join(os.homedir(), 'Desktop'),
1998
+ path.join(os.homedir(), 'AppData', 'Roaming', 'Microsoft', 'Windows', 'Start Menu', 'Programs'),
1999
+ path.join(os.homedir(), 'AppData', 'Roaming', 'Microsoft', 'Internet Explorer', 'Quick Launch', 'User Pinned', 'TaskBar'),
2000
+ 'C:\\ProgramData\\Microsoft\\Windows\\Start Menu\\Programs',
2001
+ ];
2002
+ for (const loc of locations) {
2003
+ try {
2004
+ const files = fs.readdirSync(loc);
2005
+ for (const f of files) {
2006
+ if (/chrome/i.test(f) && f.endsWith('.lnk')) {
2007
+ shortcutPaths.push(path.join(loc, f));
2008
+ }
2009
+ }
2010
+ } catch {}
2011
+ }
2012
+ // Google Chrome 폴더 내부도 탐색
2013
+ for (const loc of locations) {
2014
+ try {
2015
+ const chromeDir = path.join(loc, 'Google Chrome');
2016
+ if (fs.existsSync(chromeDir)) {
2017
+ const files = fs.readdirSync(chromeDir);
2018
+ for (const f of files) {
2019
+ if (/chrome/i.test(f) && f.endsWith('.lnk')) {
2020
+ shortcutPaths.push(path.join(chromeDir, f));
2021
+ }
2022
+ }
2023
+ }
2024
+ } catch {}
2025
+ }
2026
+
2027
+ let modified = 0;
2028
+ for (const lnkPath of shortcutPaths) {
2029
+ try {
2030
+ const psScript = `
2031
+ $shell = New-Object -ComObject WScript.Shell
2032
+ $sc = $shell.CreateShortcut('${lnkPath.replace(/'/g, "''")}')
2033
+ if ($sc.Arguments -notmatch 'remote-debugging-port') {
2034
+ $sc.Arguments = ($sc.Arguments + ' ${flag}').Trim()
2035
+ $sc.Save()
2036
+ Write-Output 'MODIFIED'
2037
+ } else {
2038
+ Write-Output 'ALREADY'
2039
+ }`;
2040
+ const result = execSync(`powershell -Command "${psScript.replace(/"/g, '\\"')}"`, {
2041
+ timeout: 5000,
2042
+ encoding: 'utf-8',
2043
+ }).trim();
2044
+ if (result === 'MODIFIED') modified++;
2045
+ } catch {}
2046
+ }
2047
+ return modified > 0;
2048
+ };
2049
+
2050
+ const extractCookiesFromCDP = async (port, targetSessionPath) => {
2051
+ const http = require('http');
2052
+ const WebSocket = require('ws');
2053
+
2054
+ // 1. 브라우저 레벨 CDP에 연결하여 tistory 탭 생성/탐색
2055
+ const browserWsUrl = await tryConnectCDP(port);
2056
+ if (!browserWsUrl) throw new Error('Chrome CDP 연결 실패');
2057
+
2058
+ // 2. 기존 tistory 탭 찾거나 새로 생성
2059
+ const targetsJson = await new Promise((resolve, reject) => {
2060
+ http.get(`http://127.0.0.1:${port}/json/list`, { timeout: 3000 }, (res) => {
2061
+ let data = '';
2062
+ res.on('data', c => data += c);
2063
+ res.on('end', () => resolve(data));
2064
+ }).on('error', reject);
2065
+ });
2066
+ const targets = JSON.parse(targetsJson);
2067
+ let pageTarget = targets.find(t => t.type === 'page' && t.url && t.url.includes('tistory'));
2068
+
2069
+ if (!pageTarget) {
2070
+ // tistory 탭이 없으면 브라우저 CDP로 새 탭 생성
2071
+ const bws = new WebSocket(browserWsUrl);
2072
+ const newTargetId = await new Promise((resolve, reject) => {
2073
+ const timeout = setTimeout(() => reject(new Error('탭 생성 시간 초과')), 10000);
2074
+ bws.on('open', () => {
2075
+ bws.send(JSON.stringify({ id: 1, method: 'Target.createTarget', params: { url: 'https://www.tistory.com/' } }));
2076
+ });
2077
+ bws.on('message', (msg) => {
2078
+ const resp = JSON.parse(msg.toString());
2079
+ if (resp.id === 1) {
2080
+ clearTimeout(timeout);
2081
+ resolve(resp.result?.targetId);
2082
+ bws.close();
2083
+ }
2084
+ });
2085
+ bws.on('error', (e) => { clearTimeout(timeout); reject(e); });
2086
+ });
2087
+ // 새 탭의 WebSocket URL 조회
2088
+ await new Promise(r => setTimeout(r, 3000));
2089
+ const newTargetsJson = await new Promise((resolve, reject) => {
2090
+ http.get(`http://127.0.0.1:${port}/json/list`, { timeout: 3000 }, (res) => {
2091
+ let data = '';
2092
+ res.on('data', c => data += c);
2093
+ res.on('end', () => resolve(data));
2094
+ }).on('error', reject);
2095
+ });
2096
+ const newTargets = JSON.parse(newTargetsJson);
2097
+ pageTarget = newTargets.find(t => t.id === newTargetId) || newTargets.find(t => t.type === 'page' && t.url && t.url.includes('tistory'));
2098
+ }
2099
+
2100
+ if (!pageTarget || !pageTarget.webSocketDebuggerUrl) {
2101
+ throw new Error('tistory 페이지 타겟을 찾을 수 없습니다.');
2102
+ }
2103
+
2104
+ // 3. 페이지 레벨 CDP에서 Network.enable → Network.getAllCookies
2105
+ const ws = new WebSocket(pageTarget.webSocketDebuggerUrl);
2106
+ const cookies = await new Promise((resolve, reject) => {
2107
+ const timeout = setTimeout(() => reject(new Error('CDP 쿠키 추출 시간 초과')), 15000);
2108
+ let msgId = 1;
2109
+ ws.on('open', () => {
2110
+ ws.send(JSON.stringify({ id: msgId++, method: 'Network.enable' }));
2111
+ });
2112
+ ws.on('message', (msg) => {
2113
+ const resp = JSON.parse(msg.toString());
2114
+ if (resp.id === 1) {
2115
+ // Network enabled → getAllCookies
2116
+ ws.send(JSON.stringify({ id: msgId++, method: 'Network.getAllCookies' }));
2117
+ }
2118
+ if (resp.id === 2) {
2119
+ clearTimeout(timeout);
2120
+ resolve(resp.result?.cookies || []);
2121
+ ws.close();
2122
+ }
2123
+ });
2124
+ ws.on('error', (e) => { clearTimeout(timeout); reject(e); });
2125
+ });
2126
+
2127
+ const tistoryCookies = cookies.filter(c => String(c.domain).includes('tistory'));
2128
+ const tssession = tistoryCookies.find(c => c.name === 'TSSESSION');
2129
+ if (!tssession || !tssession.value) {
2130
+ throw new Error('Chrome에 티스토리 로그인 세션이 없습니다. Chrome에서 먼저 티스토리에 로그인해 주세요.');
2131
+ }
2132
+
2133
+ const payload = {
2134
+ cookies: tistoryCookies.map(c => ({
2135
+ name: c.name, value: c.value, domain: c.domain,
2136
+ path: c.path || '/', expires: c.expires > 0 ? c.expires : -1,
2137
+ httpOnly: !!c.httpOnly, secure: !!c.secure,
2138
+ sameSite: c.sameSite || 'None',
2139
+ })),
2140
+ updatedAt: new Date().toISOString(),
2141
+ };
2142
+ await fs.promises.mkdir(path.dirname(targetSessionPath), { recursive: true });
2143
+ await fs.promises.writeFile(targetSessionPath, JSON.stringify(payload, null, 2), 'utf-8');
2144
+ return { cookieCount: tistoryCookies.length };
2145
+ };
2146
+
2147
+ const getOrCreateJunctionPath = (chromeRoot) => {
2148
+ // Chrome 145+: 기본 user-data-dir에서는 --remote-debugging-port가 작동하지 않음
2149
+ // Junction point로 같은 디렉토리를 다른 경로로 가리켜서 우회
2150
+ if (process.platform !== 'win32') return chromeRoot;
2151
+
2152
+ const junctionPath = path.join(path.dirname(chromeRoot), 'ChromeDebug');
2153
+ if (!fs.existsSync(junctionPath)) {
2154
+ try {
2155
+ execSync(`cmd /c "mklink /J "${junctionPath}" "${chromeRoot}""`, {
2156
+ timeout: 5000, stdio: 'pipe',
2157
+ });
2158
+ } catch {
2159
+ // Junction 생성 실패 시 원본 경로 사용 (디버그 포트 작동 안 할 수 있음)
2160
+ return chromeRoot;
2161
+ }
2162
+ }
2163
+ return junctionPath;
2164
+ };
2165
+
2166
+ const extractCookiesViaCDP = async (targetSessionPath, chromeRoot, profileName) => {
2167
+ // Chrome 실행 중: CDP(Chrome DevTools Protocol)로 쿠키 추출
2168
+ // 1단계: 이미 디버그 포트가 열려있으면 바로 연결 (크롬 종료 없음)
2169
+ // 2단계: 없으면 한 번만 재시작 + 바로가기 수정 (이후 재시작 불필요)
2170
+ const { spawn } = require('child_process');
2171
+
2172
+ // 1. 이미 디버그 포트가 열려있는지 확인
2173
+ const existing = await findChromeDebugPort();
2174
+ if (existing) {
2175
+ console.log(`[chrome-cdp] 기존 Chrome 디버그 포트(${existing.port}) 감지 — 크롬 종료 없이 쿠키 추출`);
2176
+ return await extractCookiesFromCDP(existing.port, targetSessionPath);
2177
+ }
2178
+
2179
+ // 2. 디버그 포트 없음 → Chrome 바로가기에 디버그 포트 추가 (이후 재시작 불필요)
2180
+ console.log('[chrome-cdp] Chrome 디버그 포트 미감지 — 바로가기에 --remote-debugging-port 추가 중...');
2181
+ const shortcutModified = enableChromeDebugPort();
2182
+ if (shortcutModified) {
2183
+ console.log('[chrome-cdp] Chrome 바로가기 수정 완료 — 다음부터는 크롬 종료 없이 쿠키 추출 가능');
2184
+ }
2185
+
2186
+ // 3. Chrome을 graceful하게 종료하고 디버그 포트로 재시작 (최초 1회만)
2187
+ const chromePath = findWindowsChromePath();
2188
+ if (!chromePath) throw new Error('Chrome 실행 파일을 찾을 수 없습니다.');
2189
+
2190
+ console.log('[chrome-cdp] Chrome을 디버그 포트와 함께 재시작합니다 (탭 자동 복원)...');
2191
+ try {
2192
+ if (process.platform === 'win32') {
2193
+ execSync('cmd /c "taskkill /IM chrome.exe"', { stdio: 'ignore', timeout: 10000 });
2194
+ }
2195
+ } catch {}
2196
+ await new Promise(r => setTimeout(r, 2000));
2197
+ if (isChromeRunning()) {
2198
+ try { execSync('cmd /c "taskkill /F /IM chrome.exe"', { stdio: 'ignore', timeout: 5000 }); } catch {}
2199
+ await new Promise(r => setTimeout(r, 1000));
2200
+ }
2201
+
2202
+ // 4. Junction 경로로 디버그 포트 + 세션 복원 재시작
2203
+ // Chrome 145+는 기본 user-data-dir에서 디버그 포트를 거부하므로 junction으로 우회
2204
+ const junctionRoot = getOrCreateJunctionPath(chromeRoot);
2205
+ const chromeProc = spawn(chromePath, [
2206
+ `--remote-debugging-port=${CDP_DEBUG_PORT}`,
2207
+ '--remote-allow-origins=*',
2208
+ '--restore-last-session',
2209
+ `--user-data-dir=${junctionRoot}`,
2210
+ `--profile-directory=${profileName}`,
2211
+ ], { detached: true, stdio: 'ignore' });
2212
+ chromeProc.unref();
2213
+
2214
+ // 5. CDP 연결 대기
2215
+ let connected = null;
2216
+ const maxWait = 15000;
2217
+ const start = Date.now();
2218
+ while (Date.now() - start < maxWait) {
2219
+ await new Promise(r => setTimeout(r, 500));
2220
+ connected = await findChromeDebugPort();
2221
+ if (connected) break;
2222
+ }
2223
+ if (!connected) throw new Error('Chrome 디버그 포트 연결 시간 초과');
2224
+
2225
+ // 6. 쿠키 추출 (Chrome은 계속 실행 상태 유지 — 종료하지 않음)
2226
+ return await extractCookiesFromCDP(connected.port, targetSessionPath);
2227
+ };
2228
+
1948
2229
  const importSessionViaChromeDirectLaunch = async (targetSessionPath, chromeRoot, profileName) => {
1949
2230
  // Windows Chrome 145+: v20 App Bound Encryption으로 외부에서 쿠키 복호화 불가
1950
- // HTTPS 서버 + DNS 리다이렉션으로 Chrome이 보내는 Cookie 헤더에서 세션 추출
2231
+ // Chrome 실행 중이면 CDP 방식으로 추출 (잠시 재시작, 자동 복원)
1951
2232
  if (isChromeRunning()) {
1952
- throw new Error(
1953
- 'Chrome이 실행 중입니다. --from-chrome 세션 추출을 위해 Chrome을 종료한 후 다시 시도해 주세요.'
1954
- );
2233
+ return await extractCookiesViaCDP(targetSessionPath, chromeRoot, profileName);
1955
2234
  }
1956
2235
 
1957
2236
  const chromePath = findWindowsChromePath();
@@ -2005,7 +2284,7 @@ const importSessionViaChromeDirectLaunch = async (targetSessionPath, chromeRoot,
2005
2284
  });
2006
2285
  });
2007
2286
 
2008
- // 4. Chrome 실행 (기본 프로필, DNS 리다이렉션, 인증서 오류 무시)
2287
+ // 4. Chrome 실행 (Chrome이 꺼진 상태에서만 실행됨 - DNS 리다이렉션, 인증서 오류 무시)
2009
2288
  chromeProc = spawn(chromePath, [
2010
2289
  '--no-first-run',
2011
2290
  '--no-default-browser-check',