Twitter スキル は何をしますか?
Chrome Computer Use モードを使って X(Twitter)にテキストと画像を投稿します。
スクリプトディレクトリ
重要: すべてのスクリプトはこのスキルの 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ツイートのFree枠超過、レート制限)や
subscription.yamlにAPI認証情報が設定されていない場合は、自動的に Chrome Computer Use モード / CLI ブリッジ にフォールバックしてブラウザ操作をシミュレートします。
前提条件
- API モードの場合: ローカルの
subscription.yamlファイル(gitignore対象)に 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(Twitter)にログイン済みで、必要に応じて 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秒待つ - 公開の安全性: 現在の会話でユーザーから明示的な最終確認を得るまでは、
Publish、Post、またはそれに類するボタンを決してクリックしないでください。 - ユーザーが確認したら、
Postボタンをクリックして公開する - 公開後、作成モーダルを閉じて 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() { /* コード */ })();
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 コンポーネントがクラッシュし、Post ボタンが永久的に無効になります。 - 空のフォーカスされたエディターには、単に
document.execCommand('insertText', false, text)を直接使用してください。
- 空の作成ボックスに対して
-
React 状態更新のトリガー: テキスト挿入後、バブリング
inputイベントを発火させて React に通知します:el.dispatchEvent(new Event('input', { bubbles: true })); -
正しい Post ボタンの特定: ボタンの 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 + ')';
}
}
// フォールバック: 最初のメニュー項目をクリック(ドロップダウンでは常に "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 がキー、ツイートのテキストコンテンツが値となります。
- オプションの日付フィルター: