Communitygithub.com

트위터 스킬

Chrome Computer Use 모드와 X API를 통해 X(트위터)에 텍스트와 이미지를 게시하고, 트윗을 삭제하거나 내보내는 등의 작업을 수행합니다.

트위터 스킬란 무엇인가요?

이 스킬은 AI 에이전트가 X(트위터) 플랫폼에서 텍스트/이미지 게시, 트윗 삭제, 트윗 내보내기 등의 작업을 수행할 수 있게 해줍니다. **하이브리드 실행 모델**을 채택하여, 공식 X API를 우선 사용(빠르고 안정적)하고, API 호출 실패 시(예: 무료 티어 월 1,500개 트윗 제한 초과, 속도 제한) Chrome Computer Use 모드나 CLI 브리지로 자동 대체(fallback)합니다. Claude Code, Cursor, Codex 등 다양한 AI 에이전트 플랫폼에서 동작하도록 설계되었으며, `scripts/` 디렉토리 아래에 클립보드 복사, X API 호출, 트윗 내보내기용 스크립트가 포함되어 있습니다. API 모드 사용을 위해선 `subscription.yaml`에 X API OAuth 1.0a 사용자 컨텍스트 토큰을 설정해야 하며, 브라우저 폴백 모드에서는 Chrome이 설치되고 X에 로그인되어 있어야 합니다. 또한 JavaScript/DOM 자동화에 대한 상세 가이드라인을 제공하여 Draft.js 에디터와 안전하게 상호작용하고, React 상태 업데이트를 올바르게 트리거하는 방법을 설명합니다.

지원 대상✓Claude Code~Codex CLI~Cursor✓Antigravity
npx skills add hankunpeng/hkp-skills

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

트위터 스킬은(는) 무엇을 하나요?

Chrome Computer Use 모드를 통해 X(트위터)에 텍스트와 이미지를 게시합니다.

스크립트 디렉토리

중요: 모든 스크립트는 이 스킬의 scripts/ 하위 디렉토리에 있습니다.

에이전트 실행 지침:

  1. 이 SKILL.md 파일의 디렉토리 경로를 {baseDir}으로 결정합니다.
  2. 스크립트 경로:
    • 클립보드 도우미: {baseDir}/scripts/copy-to-clipboard.ts
    • 트윗 내보내기/스크래퍼: {baseDir}/scripts/export-tweets.ts
    • 공식 X API 도우미: {baseDir}/scripts/x-api.ts
  3. ${BUN_X} 런타임 확인: bun이 설치되어 있으면 → bun; npx를 사용할 수 있으면 → npx -y bun; 그렇지 않으면 bun 설치를 제안합니다.
  4. 이 문서의 모든 {baseDir}와 ${BUN_X}를 실제 값으로 바꿉니다.

실행 모드

이 스킬은 하이브리드 실행 모델을 따릅니다:

  1. API 우선 (텍스트 게시 권장): 공식 X API 스크립트(x-api.ts)를 사용하여 트윗을 게시합니다. 빠르고 안정적이며 브라우저 리소스를 사용하지 않습니다.
  2. 브라우저 폴백: 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 접근성 권한이 부여되어 있어야 합니다.

일반 게시 워크플로우 (텍스트 및 이미지)

게시를 실행할 때:

  1. get_app_state(또는 이에 상응하는 도구)를 호출하여 Google Chrome의 상태를 확인합니다.
  2. Google Chrome을 열거나 https://x.com/compose/post로 이동합니다.
  3. 트윗 작성 입력 상자를 찾습니다.
  4. Computer Use 키보드 입력을 사용하여 작성기에 게시 텍스트를 입력합니다.
  5. 첨부할 이미지가 있는 경우(최대 4개): 각 이미지에 대해: a. 클립보드 도우미 스크립트를 실행하여 이미지를 클립보드에 복사합니다:
    ${BUN_X} {baseDir}/scripts/copy-to-clipboard.ts image /absolute/path/to/image.png
    
    b. 붙여넣기 단축키(macOS는 super+v, Windows/Linux는 control+v)를 사용하여 작성기에 이미지를 붙여넣습니다. c. X가 미디어 업로드를 완료할 때까지 2-3초 기다립니다.
  6. 게시 안전: 현재 대화에서 사용자의 명시적인 최종 확인 없이 게시, 포스트 또는 동등한 버튼을 절대 클릭하지 마십시오.
  7. 사용자가 확인하면 게시 버튼을 클릭하여 게시합니다.
  8. 게시 후 작성기 모달을 닫아 UI가 작성 대화 상자에 계속 멈춰 있지 않도록 합니다. 닫기 버튼 또는 Escape 키를 사용합니다:
    • DOM 선택자: [data-testid="app-bar-close"] 또는 [aria-label="Close"]
    • 대체: Escape 키다운 이벤트를 디스패치합니다.
  9. 자동 피드 새로고침 (선택 사항): 사용자가 자신의 프로필(예: 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 편집기와 상호작용하십시오:

  1. 다중 편집기 감지: X 페이지에는 여러 개의 작성 텍스트 영역이 포함되는 경우가 많습니다(예: 홈 피드의 인라인 작성기와 활성 모달 대화상자). 항상 모든 편집기를 쿼리합니다:

    var els = document.querySelectorAll('[data-testid="tweetTextarea_0"]');
    var el = els.length > 1 ? els[els.length - 1] : els[0];
    

    항상 활성 모달 작성기(보통 목록의 마지막 요소)를 대상으로 합니다.

  2. 바인딩 선택 및 포커스: 텍스트를 삽입하기 전에 반드시 요소를 클릭하여 Draft.js 선택 바인딩을 트리거한 다음 포커스를 설정합니다:

    el.click();
    el.focus();
    
  3. 편집기 구조 유지:

    • 빈 작성기에서 el.innerHTML = '' 또는 document.execCommand('delete')를 사용하지 마십시오. DOM 노드를 지우면 Draft.js의 내부 래퍼 구조(예: public-DraftStyleDefault-block 스팬)가 파괴되어 React 구성 요소가 충돌하고 게시 버튼이 영구적으로 비활성화됩니다.
    • 빈 포커스 편집기에 직접 document.execCommand('insertText', false, text)를 사용하기만 하면 됩니다.
  4. React 상태 업데이트 트리거: 텍스트 삽입 후, 버블링된 input 이벤트를 디스패치하여 React에 알립니다:

    el.dispatchEvent(new Event('input', { bubbles: true }));
    
  5. 올바른 게시 버튼 찾기: 버튼 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();
    

트윗 삭제 워크플로우

삭제를 실행할 때:

  1. Google Chrome을 열거나 사용자의 프로필 페이지(https://x.com/[username]) 또는 직접 트윗 URL(https://x.com/[username]/status/[tweetId])로 이동합니다.
  2. 삭제할 텍스트가 포함된 대상 트윗 <article> 컨테이너를 검색합니다.
  3. 트윗의 옵션 메뉴 버튼을 클릭합니다:
    • DOM 선택자: [data-testid="caret"]
  4. 1-2초 기다린 다음 "삭제" 메뉴 항목을 클릭합니다:
    • DOM 선택자: 텍스트에 "Delete" 또는 "删除"이 포함된 [role="menuitem"] 요소.
  5. 1-2초 기다린 다음 대화 상자 시트에서 확인 삭제 버튼을 클릭합니다:
    • DOM 선택자: [data-testid="confirmationSheetConfirm"] (또는 텍스트가 "Delete" 또는 "删除"인 대화 상자 버튼으로 대체).

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'

트윗 내보내기 워크플로우

프로필 페이지에서 모든 트윗 또는 필터링된 트윗을 내보내기/저장하려면:

  1. 내보내기 스크립트를 실행합니다:
    ${BUN_X} {baseDir}/scripts/export-tweets.ts [startDate] [endDate]
    
    • 선택적 날짜 필터: startDate(예: 2026-06-01) 및 endDate(예: 2026-06-30)를 전달하여 날짜 범위별로 출력을 필터링할 수 있습니다. 생략하면 스크랩된 모든 트윗이 내보내집니다.
    • 출력 파일: 트윗은 /Users/alex/twitter/twitter.yaml에 저장되며, 트윗 URL이 키이고 트윗 텍스트 내용이 값입니다.

관련 스킬