트위터 스킬은(는) 무엇을 하나요?
Chrome Computer Use 모드를 통해 X(트위터)에 텍스트와 이미지를 게시합니다.
스크립트 디렉토리
중요: 모든 스크립트는 이 스킬의 scripts/ 하위 디렉토리에 있습니다.
에이전트 실행 지침:
- 이 SKILL.md 파일의 디렉토리 경로를
{baseDir}으로 결정합니다. - 스크립트 경로:
- 클립보드 도우미:
{baseDir}/scripts/copy-to-clipboard.ts - 트윗 내보내기/스크래퍼:
{baseDir}/scripts/export-tweets.ts - 공식 X API 도우미:
{baseDir}/scripts/x-api.ts
- 클립보드 도우미:
${BUN_X}런타임 확인:bun이 설치되어 있으면 →bun;npx를 사용할 수 있으면 →npx -y bun; 그렇지 않으면 bun 설치를 제안합니다.- 이 문서의 모든
{baseDir}와${BUN_X}를 실제 값으로 바꿉니다.
실행 모드
이 스킬은 하이브리드 실행 모델을 따릅니다:
- API 우선 (텍스트 게시 권장): 공식 X API 스크립트(
x-api.ts)를 사용하여 트윗을 게시합니다. 빠르고 안정적이며 브라우저 리소스를 사용하지 않습니다. - 브라우저 폴백: API 호출이 실패한 경우(예: 무료 티어 월 1500개 트윗 제한 초과, 속도 제한) 또는
subscription.yaml에 API 자격 증명이 구성되지 않은 경우, 자동으로 Chrome Computer Use 모드 / CLI 브리지로 대체하여 브라우저 동작을 시뮬레이션합니다.
사전 요구 사항
- API 모드: 로컬
subscription.yaml파일(git-ignored)에 X API 자격 증명(OAuth 1.0a 사용자 컨텍스트 토큰)을 구성합니다.x_api: api_key: "YOUR_API_KEY" api_key_secret: "YOUR_API_KEY_SECRET" access_token: "YOUR_ACCESS_TOKEN" access_token_secret: "YOUR_ACCESS_TOKEN_SECRET" - 브라우저 폴백: Google Chrome이 설치되어 있고, Chrome에서 X(트위터)에 로그인되어 있으며, 필요한 경우 macOS 접근성 권한이 부여되어 있어야 합니다.
일반 게시 워크플로우 (텍스트 및 이미지)
게시를 실행할 때:
get_app_state(또는 이에 상응하는 도구)를 호출하여Google Chrome의 상태를 확인합니다.- Google Chrome을 열거나
https://x.com/compose/post로 이동합니다. - 트윗 작성 입력 상자를 찾습니다.
- Computer Use 키보드 입력을 사용하여 작성기에 게시 텍스트를 입력합니다.
- 첨부할 이미지가 있는 경우(최대 4개):
각 이미지에 대해:
a. 클립보드 도우미 스크립트를 실행하여 이미지를 클립보드에 복사합니다:
b. 붙여넣기 단축키(macOS는${BUN_X} {baseDir}/scripts/copy-to-clipboard.ts image /absolute/path/to/image.pngsuper+v, Windows/Linux는control+v)를 사용하여 작성기에 이미지를 붙여넣습니다. c. X가 미디어 업로드를 완료할 때까지 2-3초 기다립니다. - 게시 안전: 현재 대화에서 사용자의 명시적인 최종 확인 없이
게시,포스트또는 동등한 버튼을 절대 클릭하지 마십시오. - 사용자가 확인하면
게시버튼을 클릭하여 게시합니다. - 게시 후 작성기 모달을 닫아 UI가 작성 대화 상자에 계속 멈춰 있지 않도록 합니다. 닫기 버튼 또는 Escape 키를 사용합니다:
- DOM 선택자:
[data-testid="app-bar-close"]또는[aria-label="Close"] - 대체:
Escape키다운 이벤트를 디스패치합니다.
- DOM 선택자:
- 자동 피드 새로고침 (선택 사항): 사용자가 자신의 프로필(예:
x.com/[username])이나 홈 피드(x.com/home)에 다른 탭을 열어 둔 경우, 새 트윗이 즉시 보이도록 해당 탭을 새로고침합니다.
var closeBtn = document.querySelector('[data-testid="app-bar-close"], [aria-label="Close"]');
if (closeBtn) { closeBtn.click(); }
else { document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', code: 'Escape', keyCode: 27, which: 27, bubbles: true })); }
CLI 브리지 (Computer Use 도구 없음)
환경에 Computer Use 키보드/마우스 도구가 없는 경우, 플랫폼별 방법을 사용하여 Chrome을 열고 페이지에 JavaScript를 삽입합니다.
macOS (AppleScript)
작성 페이지 열기:
open -a "Google Chrome" "https://x.com/compose/post"
Chrome에서 JavaScript 실행 — 먼저 JS를 임시 파일에 작성(셸 이스케이프 문제 방지)한 다음 AppleScript를 통해 실행합니다:
cat > /tmp/tweet.js << 'EOF'
(function() { /* your code */ })();
EOF
osascript -e '
tell application "Google Chrome"
activate
set js to read "/tmp/tweet.js"
set result to execute front window''''s active tab javascript js
return result
end tell'
패턴: 항상 JS 페이로드를 먼저 임시 파일에 작성하십시오. 인라인 osascript -e에 JS를 포함하여 실행하지 마십시오. 따옴표/이스케이프 충돌로 인해 구문 분석 오류가 발생합니다.
게시 후 작성기 닫기 (그렇지 않으면 모달이 계속 열려 있음):
var closeBtn = document.querySelector('[data-testid="app-bar-close"], [aria-label="Close"]');
if (closeBtn) { closeBtn.click(); }
else { document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', code: 'Escape', keyCode: 27, which: 27, bubbles: true })); }
새 게시물을 즉시 반영하도록 프로필/홈 탭 새로고침 (선택 사항):
osascript -e '
tell application "Google Chrome"
tell window 1
set tabList to every tab whose URL contains "x.com/hankunpeng" or URL contains "x.com/home"
repeat with t in tabList
reload t
end repeat
end tell
end tell'
Linux
google-chrome "https://x.com/compose/post"
# chrome-remote-interface 또는 유사한 CDP 클라이언트를 사용하여 JS 실행
Windows
Start-Process "chrome" "https://x.com/compose/post"
# CDP를 --remote-debugging-port 또는 PowerShell 자동화를 통해 사용
JavaScript / DOM 자동화 가이드라인 (폴백)
직접적인 Computer Use 키보드/마우스 이벤트를 사용할 수 없거나 브라우저 스크립팅/CDP 모드로 실행 중인 경우, 다음 가이드라인에 따라 X의 React/Draft.js 편집기와 상호작용하십시오:
-
다중 편집기 감지: X 페이지에는 여러 개의 작성 텍스트 영역이 포함되는 경우가 많습니다(예: 홈 피드의 인라인 작성기와 활성 모달 대화상자). 항상 모든 편집기를 쿼리합니다:
var els = document.querySelectorAll('[data-testid="tweetTextarea_0"]'); var el = els.length > 1 ? els[els.length - 1] : els[0];항상 활성 모달 작성기(보통 목록의 마지막 요소)를 대상으로 합니다.
-
바인딩 선택 및 포커스: 텍스트를 삽입하기 전에 반드시 요소를 클릭하여 Draft.js 선택 바인딩을 트리거한 다음 포커스를 설정합니다:
el.click(); el.focus(); -
편집기 구조 유지:
- 빈 작성기에서
el.innerHTML = ''또는document.execCommand('delete')를 사용하지 마십시오. DOM 노드를 지우면 Draft.js의 내부 래퍼 구조(예:public-DraftStyleDefault-block스팬)가 파괴되어 React 구성 요소가 충돌하고 게시 버튼이 영구적으로 비활성화됩니다. - 빈 포커스 편집기에 직접
document.execCommand('insertText', false, text)를 사용하기만 하면 됩니다.
- 빈 작성기에서
-
React 상태 업데이트 트리거: 텍스트 삽입 후, 버블링된
input이벤트를 디스패치하여 React에 알립니다:el.dispatchEvent(new Event('input', { bubbles: true })); -
올바른 게시 버튼 찾기: 버튼 testid(
tweetButtonInline및tweetButton)는 컨텍스트에 따라 서로 바뀔 수 있습니다. 항상 표시되고 활성화된 버튼을 스캔합니다:var btns = document.querySelectorAll('[data-testid="tweetButtonInline"], [data-testid="tweetButton"]'); var activeBtn = Array.from(btns).find(function(btn) { var isVisible = btn.offsetWidth > 0 && btn.offsetHeight > 0; var isDisabled = btn.disabled || btn.getAttribute('aria-disabled') === 'true'; return isVisible && !isDisabled; }); if (activeBtn) activeBtn.click();
트윗 삭제 워크플로우
삭제를 실행할 때:
- Google Chrome을 열거나 사용자의 프로필 페이지(
https://x.com/[username]) 또는 직접 트윗 URL(https://x.com/[username]/status/[tweetId])로 이동합니다. - 삭제할 텍스트가 포함된 대상 트윗
<article>컨테이너를 검색합니다. - 트윗의 옵션 메뉴 버튼을 클릭합니다:
- DOM 선택자:
[data-testid="caret"]
- DOM 선택자:
- 1-2초 기다린 다음 "삭제" 메뉴 항목을 클릭합니다:
- DOM 선택자: 텍스트에 "Delete" 또는 "删除"이 포함된
[role="menuitem"]요소.
- DOM 선택자: 텍스트에 "Delete" 또는 "删除"이 포함된
- 1-2초 기다린 다음 대화 상자 시트에서 확인 삭제 버튼을 클릭합니다:
- DOM 선택자:
[data-testid="confirmationSheetConfirm"](또는 텍스트가 "Delete" 또는 "删除"인 대화 상자 버튼으로 대체).
- DOM 선택자:
CLI 브리지 예제 (macOS)
게시와 동일한 임시 파일 + AppleScript 패턴을 사용합니다. TWEET_TEXT_HERE를 대상 트윗 내용으로 바꿉니다.
1단계 — 트윗 찾고 케어트 클릭:
cat > /tmp/del-1.js << 'EOF'
(function() {
var articles = document.querySelectorAll('article');
var target = null;
var needle = 'TWEET_TEXT_HERE';
for (var i = 0; i < articles.length; i++) {
var textEl = articles[i].querySelector('[data-testid="tweetText"]');
if (textEl && textEl.textContent.trim().indexOf(needle) !== -1) {
target = articles[i];
break;
}
}
if (!target) return 'ERROR: tweet not found';
var caret = target.querySelector('[data-testid="caret"]');
if (!caret) return 'ERROR: caret not found';
caret.click();
return 'OK: caret clicked';
})();
EOF
osascript -e '
tell application "Google Chrome"
set js to read "/tmp/del-1.js"
set result to execute front window''''s active tab javascript js
return result
end tell'
2단계 — 삭제 메뉴 항목 클릭 (1단계 후 1-2초 대기):
cat > /tmp/del-2.js << 'EOF'
(function() {
var items = document.querySelectorAll('[role="menuitem"]');
for (var i = 0; i < items.length; i++) {
var txt = items[i].textContent.trim();
if (txt === 'Delete' || txt === '删除') {
// 메뉴 항목 자체를 클릭하거나, 중첩된 클릭 가능한 요소가 포함된 경우
var clickTarget = items[i].querySelector('[role="menuitem"]') || items[i];
clickTarget.click();
return 'OK: delete clicked (index ' + i + ')';
}
}
// 대체: 첫 번째 menuitem 클릭 (드롭다운에서 항상 "Delete")
var first = document.querySelectorAll('[role="menuitem"]')[0];
if (first) { first.click(); return 'OK: first menuitem clicked'; }
return 'ERROR: no menuitems';
})();
EOF
osascript -e '
tell application "Google Chrome"
set js to read "/tmp/del-2.js"
set result to execute front window''''s active tab javascript js
return result
end tell'
3단계 — 삭제 확인 (2단계 후 1-2초 대기):
cat > /tmp/del-3.js << 'EOF'
(function() {
var confirmBtn = document.querySelector('[data-testid="confirmationSheetConfirm"]');
if (!confirmBtn) {
var buttons = document.querySelectorAll('[role="button"], button');
for (var i = 0; i < buttons.length; i++) {
var txt = buttons[i].textContent.trim();
if (txt === 'Delete' || txt === '删除') {
confirmBtn = buttons[i];
break;
}
}
}
if (!confirmBtn) return 'ERROR: confirm button not found';
confirmBtn.click();
return 'OK: confirm clicked';
})();
EOF
osascript -e '
tell application "Google Chrome"
set js to read "/tmp/del-3.js"
set result to execute front window''''s active tab javascript js
return result
end tell'
트윗 내보내기 워크플로우
프로필 페이지에서 모든 트윗 또는 필터링된 트윗을 내보내기/저장하려면:
- 내보내기 스크립트를 실행합니다:
${BUN_X} {baseDir}/scripts/export-tweets.ts [startDate] [endDate]- 선택적 날짜 필터:
startDate(예:2026-06-01) 및endDate(예:2026-06-30)를 전달하여 날짜 범위별로 출력을 필터링할 수 있습니다. 생략하면 스크랩된 모든 트윗이 내보내집니다. - 출력 파일: 트윗은
/Users/alex/twitter/twitter.yaml에 저장되며, 트윗 URL이 키이고 트윗 텍스트 내용이 값입니다.
- 선택적 날짜 필터: