From 912a4162259fbf1cd0171d2e140caa851ce17a0b Mon Sep 17 00:00:00 2001 From: bobo Date: Wed, 1 Jul 2026 12:20:59 +0800 Subject: [PATCH 01/34] Add X MCP installer for twitter digest --- twitter-digest/DATA_COLLECTION.md | 8 +- twitter-digest/README.md | 32 ++- twitter-digest/SKILL.md | 43 ++- twitter-digest/scripts/install_xmcp.sh | 303 +++++++++++++++++++++ twitter-digest/scripts/run_daily_digest.py | 41 +-- 5 files changed, 378 insertions(+), 49 deletions(-) create mode 100755 twitter-digest/scripts/install_xmcp.sh diff --git a/twitter-digest/DATA_COLLECTION.md b/twitter-digest/DATA_COLLECTION.md index 5bd6e37..5fe9663 100644 --- a/twitter-digest/DATA_COLLECTION.md +++ b/twitter-digest/DATA_COLLECTION.md @@ -87,8 +87,8 @@ scripts/run_daily_digest.py 选择逻辑: ```text ---source api -> 强制 API ---source browser -> 强制浏览器 +--source api -> 强制 API,只采公开数据,不启动浏览器 +--source browser -> 强制浏览器,采公开网页和可见 X Chat / DM --source auto -> 有 X_BEARER_TOKEN/TWITTER_BEARER_TOKEN 或已保存 OAuth2 user token 用 API,否则浏览器;API 不可用时回退浏览器 ``` @@ -98,7 +98,7 @@ scripts/run_daily_digest.py ```text scripts/api_x_digest.py -> 官方 API 抓取:home timeline、mentions、profile;API DM 仅作为 TODO/调试 -scripts/browser_x_digest.py -> 浏览器抓取:X Chat / 加密 DM / API 拿不到的网页内容 +scripts/browser_x_digest.py -> 浏览器抓取:公开网页 + X Chat / 加密 DM ``` 普通日报: @@ -217,7 +217,7 @@ API DM TODO / 调试规则: - 只把最后一条不是用户发出的会话正文放进 `dm_threads`,用于判断是否需要回复。 - 最后一条是用户发出的会话只计数,不展开正文。 - 如果 `/2/dm_events` 返回权限、tier、认证或限流错误,写入 `api_dm_todo` data gap,不把失败当作“无私信”。 -- 如果 `/2/dm_events` 返回 0 条,或 API 返回的事件不能确认是否需要回复,也写入 `api_dm_todo` TODO List;完整 DM 以 `scripts/browser_x_digest.py` 或 `run_daily_digest.py` 的浏览器 DM 页为准。 +- API 来源不启动浏览器,也不补采浏览器 DM;如果需要完整 DM,必须显式使用 `--source browser`。 ## 稳定性策略 diff --git a/twitter-digest/README.md b/twitter-digest/README.md index 9b5808a..2fb0aee 100644 --- a/twitter-digest/README.md +++ b/twitter-digest/README.md @@ -66,6 +66,36 @@ On first run, a dedicated browser profile opens. Log in to X once in that browse ## Data Collection Sources +Preferred path when available: + +```text +X MCP registered as xapi -> agent calls MCP tools directly -> agent writes the Chinese digest +``` + +The MCP path does not use a local collector script and does not write `digest-context.*`. The agent should call tools such as `get_users_me`, `get_users_timeline`, `get_users_mentions`, and `get_users_posts` directly. If X MCP does not expose DM/X Chat tools, report DM as not collected through MCP rather than claiming there are no private messages. + +To install and register X MCP for Codex / Claude Code: + +```bash +twitter-digest/scripts/install_xmcp.sh +``` + +When launched by a Codex or Claude Code agent on macOS, the installer opens a real Terminal window for OAuth2 Client ID / Secret input and browser authorization. Secrets should be entered there, not pasted into chat. + +By default this only installs and registers X MCP. To also install the `twitter-digest` skill in the same flow: + +```bash +X_MCP_INSTALL_SKILL=1 twitter-digest/scripts/install_xmcp.sh +``` + +Useful install options: + +```bash +X_MCP_SKILL_CLIENT=codex # auto | codex | claude +X_MCP_SKILL_INSTALL_MODE=symlink # copy | symlink +X_MCP_SKIP_BROWSER_CHECK=1 # skip browser runtime check for skill install +``` + There are three collection entry points: ```bash @@ -91,7 +121,7 @@ python3 ~/.claude/skills/twitter-digest/scripts/run_daily_digest.py --source bro X_BEARER_TOKEN=... python3 ~/.claude/skills/twitter-digest/scripts/run_daily_digest.py --source api --handle ``` -API mode is for stable public-data collection, including the official home timeline endpoint when the configured token has user-context timeline access. Normal daily runs use API for public data and the browser collector for X Chat / DM content. API DM lookup is marked TODO because XChat / encrypted DMs may not appear in `/2/dm_events`; do not use API DM to conclude there are no private messages. App-only API keys are not enough for user-context data. +API mode is for stable public-data collection, including the official home timeline endpoint when the configured token has user-context timeline access. API mode never starts a browser and never collects X Chat / DM content. Use browser mode when visible X Chat / DM is required. API DM lookup is marked TODO because XChat / encrypted DMs may not appear in `/2/dm_events`; do not use API DM to conclude there are no private messages. App-only API keys are not enough for user-context data. ## Configure API In Chat diff --git a/twitter-digest/SKILL.md b/twitter-digest/SKILL.md index dba750d..c234f31 100644 --- a/twitter-digest/SKILL.md +++ b/twitter-digest/SKILL.md @@ -1,13 +1,13 @@ --- name: twitter-digest -description: Use when the user wants Claude Code or another agent to analyze their own X/Twitter mentions, home timeline, visible direct messages, reply opportunities, and daily social-media summaries through API or local logged-in browser collection. +description: Use when the user wants Claude Code or another agent to analyze their own X/Twitter mentions, home timeline, visible direct messages, reply opportunities, and daily social-media summaries through X MCP, API, or local logged-in browser collection. --- # X/Twitter Digest ## Overview -Use this skill to produce a concise Chinese daily digest from the user's own X/Twitter account. The recommended entry point is `scripts/run_daily_digest.py`, which selects API collection when API credentials are configured and otherwise falls back to local browser collection with a persistent dedicated Chromium profile. +Use this skill to produce a concise Chinese daily digest from the user's own X/Twitter account. The preferred path is X MCP when the agent exposes an `xapi` / X MCP server. In that case, call MCP tools directly and write the digest from those tool results; do not run a local MCP collector script. If X MCP is not available, use `scripts/run_daily_digest.py`, which selects API collection when API credentials are configured and otherwise falls back to local browser collection with a persistent dedicated Chromium profile. After installation, configuration and daily runs should use the installed skill copy, not a temporary clone/source checkout. Installed locations are `~/.claude/skills/twitter-digest` for Claude Code and `~/.codex/skills/twitter-digest` for Codex. If `run_daily_digest.py` or `configure_api.py` is accidentally run from a source checkout while an installed copy exists, the script automatically re-runs the installed copy so `.state` is written to the installed skill directory. @@ -34,6 +34,39 @@ Load `references/x-twitter-digest.md` when you need implementation details, brow ## Data Collection +### X MCP Source + +When X MCP tools are available in the current agent session, use them directly as the collection source: + +- `get_users_me`: resolve the authenticated account. +- `get_users_timeline`: collect home timeline / followed-account activity. +- `get_users_mentions`: collect direct @ mentions. +- `get_users_posts`: collect the user's own recent posts. +- `search_posts_all`: optional keyword or mention search when supported by the current auth mode. +- `get_trends_by_woeid` / news tools: optional hotspot context when available. + +Do not run `run_daily_digest.py` just to use MCP. MCP is an agent tool source, not a local file-producing collector. If an MCP endpoint returns an auth/tier error, include it as a data gap. If the current X MCP tool list does not include DM/X Chat tools, say DM was not collected through MCP and do not claim there are no private messages. + +If X MCP is installed but Codex or Claude Code cannot see tools, the user likely needs MCP registration plus a new agent session. The installer helper is: + +```bash +twitter-digest/scripts/install_xmcp.sh +``` + +It installs `@xdevplatform/xurl`, runs OAuth2 authorization, and registers the MCP server as `xapi` for Codex and, when available, Claude Code. + +When launched by a Codex or Claude Code agent on macOS, the installer opens a real Terminal window for OAuth2 Client ID / Secret input and browser authorization. Do not ask the user to paste OAuth credentials into chat. + +By default, `install_xmcp.sh` installs only X MCP. If the user wants one command to also install this skill, set: + +```bash +X_MCP_INSTALL_SKILL=1 twitter-digest/scripts/install_xmcp.sh +``` + +Optional controls are `X_MCP_SKILL_CLIENT=auto|codex|claude`, `X_MCP_SKILL_INSTALL_MODE=copy|symlink`, and `X_MCP_SKIP_BROWSER_CHECK=1`. + +### Script Sources + There are three collection scripts: ```bash @@ -48,7 +81,7 @@ For chat usage, run the wrapper: RUN_DAILY_DIGEST ``` -`run_daily_digest.py --source auto` uses saved OAuth2 user-context credentials, `X_BEARER_TOKEN`, or `TWITTER_BEARER_TOKEN` for public data when present; otherwise it uses the browser collector. During the current X API limitation, normal daily runs use API for public data and browser collection for all DM/X Chat content. Treat API DM lookup as TODO / waiting for X to fix XChat-encrypted DM coverage; do not use API DM to decide whether the user has private messages. +`run_daily_digest.py --source auto` uses saved OAuth2 user-context credentials, `X_BEARER_TOKEN`, or `TWITTER_BEARER_TOKEN` for public data when present; otherwise it uses the browser collector. The script sources are isolated: API source never starts a browser and browser source owns visible X Chat / DM collection. Treat API DM lookup as TODO / waiting for X to fix XChat-encrypted DM coverage; do not use API DM to decide whether the user has private messages. If the user asks to configure API access, trigger the OAuth/user-token setup from chat: @@ -84,7 +117,7 @@ All normal flows should be triggered from chat by the agent: - 清除 X API 配置: run `CONFIGURE_API --clear`. - 调试浏览器: run `RUN_DAILY_DIGEST --source browser --headed`. -Force a source: +Force a script source: ```bash RUN_DAILY_DIGEST --source browser @@ -93,7 +126,7 @@ X_BEARER_TOKEN=... RUN_DAILY_DIGEST --source api --handle The first run opens a dedicated browser profile at `twitter-digest/.state/chrome-profile`. The user logs in to X once in that browser. Later runs default to headless collection and reuse the saved local browser session. If the saved login is unavailable, the script automatically opens a visible browser window for manual login. The skill has two collector scripts: `scripts/api_x_digest.py` for official API public data, and `scripts/browser_x_digest.py` for browser-visible X Chat / encrypted DM content. API-visible DM events remain TODO-only until X fixes or documents reliable XChat coverage. -DM reading is enabled by default and only reads visible local browser content. To skip DMs for a run: +DM reading is enabled by default only for browser-source runs and only reads visible local browser content. API-source runs do not start a browser, even when `--include-dms` is passed. To skip DMs for a browser run: ```bash RUN_DAILY_DIGEST --no-dms diff --git a/twitter-digest/scripts/install_xmcp.sh b/twitter-digest/scripts/install_xmcp.sh new file mode 100755 index 0000000..30bbf81 --- /dev/null +++ b/twitter-digest/scripts/install_xmcp.sh @@ -0,0 +1,303 @@ +#!/usr/bin/env bash +set -euo pipefail + +PACKAGE="${XMCP_PACKAGE:-@xdevplatform/xurl}" +VERSION="${XMCP_VERSION:-latest}" +INSTALL_SPEC="${PACKAGE}@${VERSION}" +APP_NAME="${X_MCP_APP_NAME:-xmcp}" +REDIRECT_URI="${X_MCP_REDIRECT_URI:-http://localhost:8080/callback}" +SERVER_NAME="${X_MCP_SERVER_NAME:-xapi}" +REGISTER_CODEX="${X_MCP_REGISTER_CODEX:-1}" +REGISTER_CLAUDE="${X_MCP_REGISTER_CLAUDE:-auto}" +CODEX_CONFIG="${CODEX_CONFIG:-$HOME/.codex/config.toml}" +OPEN_TERMINAL="${X_MCP_OPEN_TERMINAL:-auto}" +INSTALL_SKILL="${X_MCP_INSTALL_SKILL:-0}" +SKILL_CLIENT="${X_MCP_SKILL_CLIENT:-auto}" +SKILL_INSTALL_MODE="${X_MCP_SKILL_INSTALL_MODE:-copy}" +SKIP_BROWSER_CHECK="${X_MCP_SKIP_BROWSER_CHECK:-0}" +ALLOW_CLAUDE_COMMANDS="${X_MCP_ALLOW_CLAUDE_COMMANDS:-0}" +ALLOW_CLAUDE_STATE_READ="${X_MCP_ALLOW_CLAUDE_STATE_READ:-0}" + +info() { + printf '==> %s\n' "$1" +} + +fail() { + printf 'Error: %s\n' "$1" >&2 + exit 1 +} + +command_exists() { + command -v "$1" >/dev/null 2>&1 +} + +shell_quote() { + local value="$1" + printf "'%s'" "$(printf '%s' "$value" | sed "s/'/'\\\\''/g")" +} + +applescript_quote() { + local value="$1" + printf '%s' "$value" | sed 's/\\/\\\\/g; s/"/\\"/g' +} + +toml_quote() { + local value="$1" + printf '"%s"' "$(printf '%s' "$value" | sed 's/\\/\\\\/g; s/"/\\"/g')" +} + +script_path() { + local source="${BASH_SOURCE[0]}" + local dir="" + while [ -L "$source" ]; do + dir="$(cd -P "$(dirname "$source")" >/dev/null 2>&1 && pwd)" + source="$(readlink "$source")" + case "$source" in + /*) ;; + *) source="$dir/$source" ;; + esac + done + dir="$(cd -P "$(dirname "$source")" >/dev/null 2>&1 && pwd)" + printf '%s/%s' "$dir" "$(basename "$source")" +} + +running_under_agent() { + if [ -n "${CODEX_THREAD_ID:-}" ] || [ -n "${CODEX_CI:-}" ] || [ "${__CFBundleIdentifier:-}" = "com.openai.codex" ]; then + return 0 + fi + if env | grep -Eq '^(CLAUDE|ANTHROPIC)'; then + return 0 + fi + return 1 +} + +should_open_terminal() { + if [ "${X_MCP_TERMINAL_CHILD:-}" = "1" ]; then + return 1 + fi + case "$OPEN_TERMINAL" in + 1|true|yes) return 0 ;; + 0|false|no) return 1 ;; + esac + if [ "$(uname -s)" != "Darwin" ]; then + return 1 + fi + if running_under_agent; then + return 0 + fi + if [ ! -t 0 ]; then + return 0 + fi + return 1 +} + +open_in_terminal_and_exit() { + if ! command_exists osascript; then + fail "Cannot open macOS Terminal because osascript is unavailable." + fi + local path + path="$(script_path)" + local cwd + cwd="$(pwd)" + local command_text + command_text="cd $(shell_quote "$cwd") && X_MCP_TERMINAL_CHILD=1 /bin/bash $(shell_quote "$path"); printf '\\n'; read -r -n 1 -s -p 'Press any key to close this window...'" + osascript >/dev/null <&2 + else + printf '%s: ' "$name" >&2 + fi + IFS= read -r value + printf '%s' "${value:-$default_value}" +} + +prompt_secret() { + local name="$1" + local value="" + printf '%s: ' "$name" >&2 + if [ -t 0 ]; then + stty -echo + IFS= read -r value + stty echo + printf '\n' >&2 + else + IFS= read -r value + fi + printf '%s' "$value" +} + +register_codex() { + local config_path="$1" + local server_name="$2" + local app_name="$3" + local marker="[mcp_servers.${server_name}]" + + mkdir -p "$(dirname "$config_path")" + touch "$config_path" + + if grep -Fqx "$marker" "$config_path"; then + info "Codex MCP server '${server_name}' already exists in ${config_path}; leaving it unchanged." + return + fi + + { + printf '\n[mcp_servers.%s]\n' "$server_name" + printf 'command = "xurl"\n' + printf 'args = [%s, %s, %s, %s]\n' \ + "$(toml_quote '--app')" \ + "$(toml_quote "$app_name")" \ + "$(toml_quote 'mcp')" \ + "$(toml_quote 'https://api.x.com/mcp')" + } >> "$config_path" + + info "Registered Codex MCP server '${server_name}' in ${config_path}" +} + +register_claude() { + local server_name="$1" + local app_name="$2" + + if ! command_exists claude; then + info "Claude Code CLI not found; skipped Claude MCP registration." + return + fi + + if claude mcp get "$server_name" >/dev/null 2>&1; then + info "Claude MCP server '${server_name}' already exists; leaving it unchanged." + return + fi + + info "Registering Claude Code MCP server '${server_name}'" + claude mcp add "$server_name" -- xurl --app "$app_name" mcp https://api.x.com/mcp +} + +install_twitter_skill() { + local path + path="$(script_path)" + local root + root="$(cd "$(dirname "$path")/.." >/dev/null 2>&1 && pwd)" + local install_py="$root/scripts/install.py" + local cmd=(python3 "$install_py" --client "$SKILL_CLIENT") + + if [ "$SKILL_INSTALL_MODE" = "symlink" ]; then + cmd+=(--symlink) + else + cmd+=(--copy) + fi + if [ "$SKIP_BROWSER_CHECK" = "1" ] || [ "$SKIP_BROWSER_CHECK" = "true" ]; then + cmd+=(--skip-browser-check) + fi + if [ "$ALLOW_CLAUDE_COMMANDS" = "1" ] || [ "$ALLOW_CLAUDE_COMMANDS" = "true" ]; then + cmd+=(--allow-claude-commands) + fi + if [ "$ALLOW_CLAUDE_STATE_READ" = "1" ] || [ "$ALLOW_CLAUDE_STATE_READ" = "true" ]; then + cmd+=(--allow-claude-state-read) + fi + + info "Installing twitter-digest skill" + "${cmd[@]}" +} + +if should_open_terminal; then + open_in_terminal_and_exit +fi + +if ! command_exists node; then + fail "Node.js is required before installing X MCP. Install Node.js, then rerun this script." +fi + +if ! command_exists npm; then + fail "npm is required before installing X MCP. Install npm, then rerun this script." +fi + +info "Installing ${INSTALL_SPEC}" +npm install -g "${INSTALL_SPEC}" + +if ! command_exists xurl; then + fail "xurl was installed by npm, but the xurl command is not on PATH." +fi + +info "Installed $(xurl --version 2>/dev/null || printf 'xurl')" + +CLIENT_ID="${X_MCP_CLIENT_ID:-${X_OAUTH_CLIENT_ID:-}}" +CLIENT_SECRET="${X_MCP_CLIENT_SECRET:-${X_OAUTH_CLIENT_SECRET:-}}" + +if [ -z "$CLIENT_ID" ]; then + if [ ! -t 0 ]; then + fail "X_MCP_CLIENT_ID is required when stdin is not interactive." + fi + CLIENT_ID="$(prompt 'X OAuth Client ID')" +fi + +if [ -z "$CLIENT_ID" ]; then + fail "X OAuth Client ID is required." +fi + +if [ -z "${X_MCP_CLIENT_SECRET+x}" ] && [ -z "${X_OAUTH_CLIENT_SECRET+x}" ] && [ -t 0 ]; then + CLIENT_SECRET="$(prompt_secret 'X OAuth Client Secret (leave empty for public PKCE apps)')" +fi + +if [ -t 0 ]; then + APP_NAME="$(prompt 'xurl app name' "$APP_NAME")" + REDIRECT_URI="$(prompt 'OAuth callback URL' "$REDIRECT_URI")" +fi + +info "Registering xurl OAuth app '${APP_NAME}'" +app_cmd=(xurl auth apps add "$APP_NAME" --client-id "$CLIENT_ID" --redirect-uri "$REDIRECT_URI") +if [ -n "$CLIENT_SECRET" ]; then + app_cmd+=(--client-secret "$CLIENT_SECRET") +fi +"${app_cmd[@]}" + +info "Opening X OAuth authorization flow" +xurl auth oauth2 --app "$APP_NAME" + +info "Setting '${APP_NAME}' as the default xurl auth app" +xurl auth default "$APP_NAME" + +if [ "$REGISTER_CODEX" = "1" ] || [ "$REGISTER_CODEX" = "true" ]; then + register_codex "$CODEX_CONFIG" "$SERVER_NAME" "$APP_NAME" +else + info "Skipped Codex MCP registration." +fi + +if [ "$REGISTER_CLAUDE" = "1" ] || [ "$REGISTER_CLAUDE" = "true" ]; then + register_claude "$SERVER_NAME" "$APP_NAME" +elif [ "$REGISTER_CLAUDE" = "auto" ]; then + if command_exists claude; then + register_claude "$SERVER_NAME" "$APP_NAME" + else + info "Claude Code CLI not found; skipped Claude MCP registration." + fi +else + info "Skipped Claude Code MCP registration." +fi + +if [ "$INSTALL_SKILL" = "1" ] || [ "$INSTALL_SKILL" = "true" ]; then + install_twitter_skill +else + info "Skipped twitter-digest skill install. Set X_MCP_INSTALL_SKILL=1 to install it." +fi + +info "Done" +printf '\nX MCP command for MCP clients:\n' +printf ' xurl mcp https://api.x.com/mcp\n' +printf '\nCodex config example:\n' +printf '[mcp_servers.xapi]\n' +printf 'command = "xurl"\n' +printf 'args = ["--app", "%s", "mcp", "https://api.x.com/mcp"]\n' "$APP_NAME" +printf '\nOptional one-command skill install:\n' +printf ' X_MCP_INSTALL_SKILL=1 /bin/bash twitter-digest/scripts/install_xmcp.sh\n' diff --git a/twitter-digest/scripts/run_daily_digest.py b/twitter-digest/scripts/run_daily_digest.py index 875355f..78deac0 100644 --- a/twitter-digest/scripts/run_daily_digest.py +++ b/twitter-digest/scripts/run_daily_digest.py @@ -8,7 +8,6 @@ import os import subprocess import sys -import tempfile from pathlib import Path from api_config_store import API_CONFIG_PATH, load_api_config, refresh_oauth_token_if_needed @@ -163,34 +162,6 @@ def run_browser_command(cmd: list[str]) -> None: subprocess.run(cmd, check=True) -def merge_api_public_with_browser_dm(api_out: Path, browser_out: Path, final_out: Path) -> None: - api_data = json.loads((api_out / "digest-input.json").read_text(encoding="utf-8")) - browser_data = json.loads((browser_out / "digest-input.json").read_text(encoding="utf-8")) - browser_messages = [page for page in browser_data.get("pages", []) if isinstance(page, dict) and page.get("kind") == "messages"] - if not browser_messages: - browser_messages = [ - { - "kind": "messages", - "url": "https://x.com/messages", - "items": [], - "dm_status": "no_visible_threads", - "dm_note": "Browser DM collection completed but did not produce a messages page.", - "dm_threads": [], - "dm_visible_thread_count": 0, - "dm_replied_thread_count": 0, - "dm_unreplied_thread_count": 0, - "dm_captured_message_count": 0, - } - ] - merged_pages = [page for page in api_data.get("pages", []) if not (isinstance(page, dict) and page.get("kind") == "messages")] - merged_pages.extend(browser_messages) - api_data["pages"] = merged_pages - api_data["source"] = "api+browser_dm" - api_data["dm_source"] = "browser" - final_out.mkdir(parents=True, exist_ok=True) - (final_out / "digest-input.json").write_text(json.dumps(api_data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") - - def main() -> None: rerun_from_installed_if_needed(__file__) args = parse_args() @@ -238,19 +209,11 @@ def main() -> None: cmd = browser_command(args, args.out, handle, include_dms) child_env = None if source == "api" and include_dms: - print("API source selected for public data. Browser collection will be used for X Chat/DM.", flush=True) + print("API source selected. Browser DM collection is not run for API source; use --source browser when DMs are required.", flush=True) print(f"Collecting X digest data via {source} source.", flush=True) try: if source == "api": - if include_dms: - with tempfile.TemporaryDirectory(prefix="x-digest-api-") as api_tmp, tempfile.TemporaryDirectory(prefix="x-digest-browser-dm-") as browser_tmp: - run_api_command(api_command(args, api_tmp, api_base, user_id, handle), child_env) - print("Collecting X Chat/DM via browser source.", flush=True) - run_browser_command(browser_command(args, browser_tmp, handle, include_dms=True, dm_only=True)) - merge_api_public_with_browser_dm(Path(api_tmp), Path(browser_tmp), Path(args.out)) - source = "api+browser_dm" - else: - run_api_command(cmd, child_env) + run_api_command(cmd, child_env) else: run_browser_command(cmd) except subprocess.CalledProcessError as exc: From 9622384b735ce555f5c25909d471b0d3be6c82c6 Mon Sep 17 00:00:00 2001 From: bobo Date: Wed, 1 Jul 2026 13:08:53 +0800 Subject: [PATCH 02/34] Split X MCP installer into separate skill --- twitter-digest/README.md | 18 +--- twitter-digest/SKILL.md | 20 +---- twitter-mcp/SKILL.md | 88 +++++++++++++++++++ twitter-mcp/agents/openai.yaml | 4 + .../scripts/install_xmcp.sh | 45 +--------- 5 files changed, 98 insertions(+), 77 deletions(-) create mode 100644 twitter-mcp/SKILL.md create mode 100644 twitter-mcp/agents/openai.yaml rename {twitter-digest => twitter-mcp}/scripts/install_xmcp.sh (81%) diff --git a/twitter-digest/README.md b/twitter-digest/README.md index 2fb0aee..a681881 100644 --- a/twitter-digest/README.md +++ b/twitter-digest/README.md @@ -74,28 +74,14 @@ X MCP registered as xapi -> agent calls MCP tools directly -> agent writes the C The MCP path does not use a local collector script and does not write `digest-context.*`. The agent should call tools such as `get_users_me`, `get_users_timeline`, `get_users_mentions`, and `get_users_posts` directly. If X MCP does not expose DM/X Chat tools, report DM as not collected through MCP rather than claiming there are no private messages. -To install and register X MCP for Codex / Claude Code: +To install and register X MCP for Codex / Claude Code, use the separate `twitter-mcp` skill: ```bash -twitter-digest/scripts/install_xmcp.sh +/bin/bash twitter-mcp/scripts/install_xmcp.sh ``` When launched by a Codex or Claude Code agent on macOS, the installer opens a real Terminal window for OAuth2 Client ID / Secret input and browser authorization. Secrets should be entered there, not pasted into chat. -By default this only installs and registers X MCP. To also install the `twitter-digest` skill in the same flow: - -```bash -X_MCP_INSTALL_SKILL=1 twitter-digest/scripts/install_xmcp.sh -``` - -Useful install options: - -```bash -X_MCP_SKILL_CLIENT=codex # auto | codex | claude -X_MCP_SKILL_INSTALL_MODE=symlink # copy | symlink -X_MCP_SKIP_BROWSER_CHECK=1 # skip browser runtime check for skill install -``` - There are three collection entry points: ```bash diff --git a/twitter-digest/SKILL.md b/twitter-digest/SKILL.md index c234f31..56a0d53 100644 --- a/twitter-digest/SKILL.md +++ b/twitter-digest/SKILL.md @@ -1,6 +1,6 @@ --- name: twitter-digest -description: Use when the user wants Claude Code or another agent to analyze their own X/Twitter mentions, home timeline, visible direct messages, reply opportunities, and daily social-media summaries through X MCP, API, or local logged-in browser collection. +description: Use when the user wants Claude Code or another agent to analyze their own X/Twitter mentions, home timeline, visible direct messages, reply opportunities, and daily social-media summaries through available X MCP tools, API, or local logged-in browser collection. Do not use for installing or registering X MCP; use twitter-mcp for that. --- # X/Twitter Digest @@ -47,23 +47,7 @@ When X MCP tools are available in the current agent session, use them directly a Do not run `run_daily_digest.py` just to use MCP. MCP is an agent tool source, not a local file-producing collector. If an MCP endpoint returns an auth/tier error, include it as a data gap. If the current X MCP tool list does not include DM/X Chat tools, say DM was not collected through MCP and do not claim there are no private messages. -If X MCP is installed but Codex or Claude Code cannot see tools, the user likely needs MCP registration plus a new agent session. The installer helper is: - -```bash -twitter-digest/scripts/install_xmcp.sh -``` - -It installs `@xdevplatform/xurl`, runs OAuth2 authorization, and registers the MCP server as `xapi` for Codex and, when available, Claude Code. - -When launched by a Codex or Claude Code agent on macOS, the installer opens a real Terminal window for OAuth2 Client ID / Secret input and browser authorization. Do not ask the user to paste OAuth credentials into chat. - -By default, `install_xmcp.sh` installs only X MCP. If the user wants one command to also install this skill, set: - -```bash -X_MCP_INSTALL_SKILL=1 twitter-digest/scripts/install_xmcp.sh -``` - -Optional controls are `X_MCP_SKILL_CLIENT=auto|codex|claude`, `X_MCP_SKILL_INSTALL_MODE=copy|symlink`, and `X_MCP_SKIP_BROWSER_CHECK=1`. +If X MCP is installed but Codex or Claude Code cannot see tools, the user likely needs MCP registration plus a new agent session. Use the separate `twitter-mcp` skill for X MCP installation, authorization, registration, and troubleshooting. Do not install or register MCP from this skill. ### Script Sources diff --git a/twitter-mcp/SKILL.md b/twitter-mcp/SKILL.md new file mode 100644 index 0000000..2472ec6 --- /dev/null +++ b/twitter-mcp/SKILL.md @@ -0,0 +1,88 @@ +--- +name: twitter-mcp +description: Use when the user wants to install, authorize, register, verify, or troubleshoot the official X/Twitter MCP server for Codex, Claude Code, or another MCP-capable agent. +--- + +# X/Twitter MCP + +## Overview + +Use this skill to install and register the official X MCP server through `@xdevplatform/xurl`. This skill only handles MCP setup and troubleshooting. It does not collect timeline data, generate digests, read DMs, or install `twitter-digest`. + +After successful setup, the MCP server is registered as `xapi` by default. The user usually needs to start a new Codex or Claude Code session before newly registered MCP tools become visible. + +## Install And Register + +From the repository `skills/` directory: + +```bash +/bin/bash twitter-mcp/scripts/install_xmcp.sh +``` + +The installer: + +- Requires Node.js and npm. +- Installs `@xdevplatform/xurl` globally with npm. +- Opens the X OAuth2 authorization flow. +- Registers an MCP server named `xapi` for Codex by editing `~/.codex/config.toml`. +- Registers the same MCP server for Claude Code when the `claude` CLI is available. + +When launched by Codex or Claude Code on macOS, the installer opens a real Terminal window for OAuth2 Client ID / Secret input and browser authorization. Do not ask the user to paste OAuth credentials into chat. + +## Configuration + +Environment controls: + +```bash +XMCP_PACKAGE=@xdevplatform/xurl +XMCP_VERSION=latest +X_MCP_APP_NAME=xmcp +X_MCP_REDIRECT_URI=http://localhost:8080/callback +X_MCP_SERVER_NAME=xapi +X_MCP_REGISTER_CODEX=1 +X_MCP_REGISTER_CLAUDE=auto +CODEX_CONFIG=~/.codex/config.toml +X_MCP_OPEN_TERMINAL=auto +X_MCP_CLIENT_ID= +X_MCP_CLIENT_SECRET= +``` + +Use `X_MCP_CLIENT_ID` and `X_MCP_CLIENT_SECRET` only when the user explicitly provides them through a secure environment. Prefer the interactive Terminal prompts for secrets. + +## Manual MCP Command + +For MCP clients that need the command directly: + +```bash +xurl --app xmcp mcp https://api.x.com/mcp +``` + +Codex config shape: + +```toml +[mcp_servers.xapi] +command = "xurl" +args = ["--app", "xmcp", "mcp", "https://api.x.com/mcp"] +``` + +Claude Code command shape: + +```bash +claude mcp add xapi -- xurl --app xmcp mcp https://api.x.com/mcp +``` + +## Verification + +After installation: + +1. Start a new Codex or Claude Code session. +2. Check whether X MCP tools are visible under the `xapi` server. +3. If tools are missing, verify `xurl` is on `PATH`, the MCP config contains `xapi`, and the OAuth app name matches the configured `--app` value. + +If an X MCP endpoint returns an auth, subscription, tier, or scope error, report the exact failing capability as a setup or account limitation. Do not infer that the requested X data does not exist. + +## Boundaries + +- Use `twitter-mcp` for installing, authorizing, registering, and troubleshooting X MCP. +- Use a separate data or digest skill, such as `twitter-digest`, for analyzing X/Twitter content after MCP tools are visible. +- Do not install or modify unrelated skills from this skill. diff --git a/twitter-mcp/agents/openai.yaml b/twitter-mcp/agents/openai.yaml new file mode 100644 index 0000000..cd4786b --- /dev/null +++ b/twitter-mcp/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "X/Twitter MCP" + short_description: "Install and register the official X MCP server" + default_prompt: "Use $twitter-mcp to install and register X MCP for Codex or Claude Code." diff --git a/twitter-digest/scripts/install_xmcp.sh b/twitter-mcp/scripts/install_xmcp.sh similarity index 81% rename from twitter-digest/scripts/install_xmcp.sh rename to twitter-mcp/scripts/install_xmcp.sh index 30bbf81..e82e756 100755 --- a/twitter-digest/scripts/install_xmcp.sh +++ b/twitter-mcp/scripts/install_xmcp.sh @@ -11,12 +11,6 @@ REGISTER_CODEX="${X_MCP_REGISTER_CODEX:-1}" REGISTER_CLAUDE="${X_MCP_REGISTER_CLAUDE:-auto}" CODEX_CONFIG="${CODEX_CONFIG:-$HOME/.codex/config.toml}" OPEN_TERMINAL="${X_MCP_OPEN_TERMINAL:-auto}" -INSTALL_SKILL="${X_MCP_INSTALL_SKILL:-0}" -SKILL_CLIENT="${X_MCP_SKILL_CLIENT:-auto}" -SKILL_INSTALL_MODE="${X_MCP_SKILL_INSTALL_MODE:-copy}" -SKIP_BROWSER_CHECK="${X_MCP_SKIP_BROWSER_CHECK:-0}" -ALLOW_CLAUDE_COMMANDS="${X_MCP_ALLOW_CLAUDE_COMMANDS:-0}" -ALLOW_CLAUDE_STATE_READ="${X_MCP_ALLOW_CLAUDE_STATE_READ:-0}" info() { printf '==> %s\n' "$1" @@ -184,33 +178,6 @@ register_claude() { claude mcp add "$server_name" -- xurl --app "$app_name" mcp https://api.x.com/mcp } -install_twitter_skill() { - local path - path="$(script_path)" - local root - root="$(cd "$(dirname "$path")/.." >/dev/null 2>&1 && pwd)" - local install_py="$root/scripts/install.py" - local cmd=(python3 "$install_py" --client "$SKILL_CLIENT") - - if [ "$SKILL_INSTALL_MODE" = "symlink" ]; then - cmd+=(--symlink) - else - cmd+=(--copy) - fi - if [ "$SKIP_BROWSER_CHECK" = "1" ] || [ "$SKIP_BROWSER_CHECK" = "true" ]; then - cmd+=(--skip-browser-check) - fi - if [ "$ALLOW_CLAUDE_COMMANDS" = "1" ] || [ "$ALLOW_CLAUDE_COMMANDS" = "true" ]; then - cmd+=(--allow-claude-commands) - fi - if [ "$ALLOW_CLAUDE_STATE_READ" = "1" ] || [ "$ALLOW_CLAUDE_STATE_READ" = "true" ]; then - cmd+=(--allow-claude-state-read) - fi - - info "Installing twitter-digest skill" - "${cmd[@]}" -} - if should_open_terminal; then open_in_terminal_and_exit fi @@ -286,18 +253,10 @@ else info "Skipped Claude Code MCP registration." fi -if [ "$INSTALL_SKILL" = "1" ] || [ "$INSTALL_SKILL" = "true" ]; then - install_twitter_skill -else - info "Skipped twitter-digest skill install. Set X_MCP_INSTALL_SKILL=1 to install it." -fi - info "Done" printf '\nX MCP command for MCP clients:\n' -printf ' xurl mcp https://api.x.com/mcp\n' +printf ' xurl --app %s mcp https://api.x.com/mcp\n' "$APP_NAME" printf '\nCodex config example:\n' -printf '[mcp_servers.xapi]\n' +printf '[mcp_servers.%s]\n' "$SERVER_NAME" printf 'command = "xurl"\n' printf 'args = ["--app", "%s", "mcp", "https://api.x.com/mcp"]\n' "$APP_NAME" -printf '\nOptional one-command skill install:\n' -printf ' X_MCP_INSTALL_SKILL=1 /bin/bash twitter-digest/scripts/install_xmcp.sh\n' From 4d9787b3a8b90465b6f8b9720e38aaf5fc197c08 Mon Sep 17 00:00:00 2001 From: bobo Date: Wed, 1 Jul 2026 13:11:21 +0800 Subject: [PATCH 03/34] Remove MCP references from twitter digest --- twitter-digest/FUNCTION_RULES_FLOW.md | 1 - twitter-digest/README.md | 16 ---------------- twitter-digest/SKILL.md | 21 ++------------------- 3 files changed, 2 insertions(+), 36 deletions(-) diff --git a/twitter-digest/FUNCTION_RULES_FLOW.md b/twitter-digest/FUNCTION_RULES_FLOW.md index d7f6a49..004db7a 100644 --- a/twitter-digest/FUNCTION_RULES_FLOW.md +++ b/twitter-digest/FUNCTION_RULES_FLOW.md @@ -67,7 +67,6 @@ twitter-digest/.state/run/digest-context.md - 默认入口 `run_daily_digest.py --source auto`:有 OAuth2 user-context API 配置时优先用 API 抓公开数据;没有 API 配置时用浏览器抓取。 - DM / X Chat 以本地浏览器抓取为准;API DM 现阶段仅保留为 TODO/调试,不用于判断是否有私信。 - API 不可用、权限不足、tier 不支持或限流时,记录数据缺口并回退浏览器路径。 -- 不使用 MCP。 - 不要求用户复制 cookie 或 token。 - 默认 headless 运行。 - 第一次没有登录态时,会自动打开可见浏览器让用户登录。 diff --git a/twitter-digest/README.md b/twitter-digest/README.md index a681881..1fa842d 100644 --- a/twitter-digest/README.md +++ b/twitter-digest/README.md @@ -66,22 +66,6 @@ On first run, a dedicated browser profile opens. Log in to X once in that browse ## Data Collection Sources -Preferred path when available: - -```text -X MCP registered as xapi -> agent calls MCP tools directly -> agent writes the Chinese digest -``` - -The MCP path does not use a local collector script and does not write `digest-context.*`. The agent should call tools such as `get_users_me`, `get_users_timeline`, `get_users_mentions`, and `get_users_posts` directly. If X MCP does not expose DM/X Chat tools, report DM as not collected through MCP rather than claiming there are no private messages. - -To install and register X MCP for Codex / Claude Code, use the separate `twitter-mcp` skill: - -```bash -/bin/bash twitter-mcp/scripts/install_xmcp.sh -``` - -When launched by a Codex or Claude Code agent on macOS, the installer opens a real Terminal window for OAuth2 Client ID / Secret input and browser authorization. Secrets should be entered there, not pasted into chat. - There are three collection entry points: ```bash diff --git a/twitter-digest/SKILL.md b/twitter-digest/SKILL.md index 56a0d53..c6853af 100644 --- a/twitter-digest/SKILL.md +++ b/twitter-digest/SKILL.md @@ -1,13 +1,13 @@ --- name: twitter-digest -description: Use when the user wants Claude Code or another agent to analyze their own X/Twitter mentions, home timeline, visible direct messages, reply opportunities, and daily social-media summaries through available X MCP tools, API, or local logged-in browser collection. Do not use for installing or registering X MCP; use twitter-mcp for that. +description: Use when the user wants Claude Code or another agent to analyze their own X/Twitter mentions, home timeline, visible direct messages, reply opportunities, and daily social-media summaries through API or local logged-in browser collection. --- # X/Twitter Digest ## Overview -Use this skill to produce a concise Chinese daily digest from the user's own X/Twitter account. The preferred path is X MCP when the agent exposes an `xapi` / X MCP server. In that case, call MCP tools directly and write the digest from those tool results; do not run a local MCP collector script. If X MCP is not available, use `scripts/run_daily_digest.py`, which selects API collection when API credentials are configured and otherwise falls back to local browser collection with a persistent dedicated Chromium profile. +Use this skill to produce a concise Chinese daily digest from the user's own X/Twitter account. Use `scripts/run_daily_digest.py`, which selects API collection when API credentials are configured and otherwise falls back to local browser collection with a persistent dedicated Chromium profile. After installation, configuration and daily runs should use the installed skill copy, not a temporary clone/source checkout. Installed locations are `~/.claude/skills/twitter-digest` for Claude Code and `~/.codex/skills/twitter-digest` for Codex. If `run_daily_digest.py` or `configure_api.py` is accidentally run from a source checkout while an installed copy exists, the script automatically re-runs the installed copy so `.state` is written to the installed skill directory. @@ -34,23 +34,6 @@ Load `references/x-twitter-digest.md` when you need implementation details, brow ## Data Collection -### X MCP Source - -When X MCP tools are available in the current agent session, use them directly as the collection source: - -- `get_users_me`: resolve the authenticated account. -- `get_users_timeline`: collect home timeline / followed-account activity. -- `get_users_mentions`: collect direct @ mentions. -- `get_users_posts`: collect the user's own recent posts. -- `search_posts_all`: optional keyword or mention search when supported by the current auth mode. -- `get_trends_by_woeid` / news tools: optional hotspot context when available. - -Do not run `run_daily_digest.py` just to use MCP. MCP is an agent tool source, not a local file-producing collector. If an MCP endpoint returns an auth/tier error, include it as a data gap. If the current X MCP tool list does not include DM/X Chat tools, say DM was not collected through MCP and do not claim there are no private messages. - -If X MCP is installed but Codex or Claude Code cannot see tools, the user likely needs MCP registration plus a new agent session. Use the separate `twitter-mcp` skill for X MCP installation, authorization, registration, and troubleshooting. Do not install or register MCP from this skill. - -### Script Sources - There are three collection scripts: ```bash From 3eed4dbd5ba2774e1ac12a3ced51994424ae638e Mon Sep 17 00:00:00 2001 From: bobo Date: Wed, 1 Jul 2026 13:15:53 +0800 Subject: [PATCH 04/34] Prevent browser fallback when API is configured --- twitter-digest/DATA_COLLECTION.md | 11 +++++----- twitter-digest/FUNCTION_RULES_FLOW.md | 4 ++-- twitter-digest/README.md | 5 +++-- twitter-digest/RUNBOOK.md | 5 +++-- twitter-digest/SKILL.md | 4 ++-- twitter-digest/scripts/run_daily_digest.py | 24 ++++++++++++---------- 6 files changed, 29 insertions(+), 24 deletions(-) diff --git a/twitter-digest/DATA_COLLECTION.md b/twitter-digest/DATA_COLLECTION.md index 5fe9663..f3db5d6 100644 --- a/twitter-digest/DATA_COLLECTION.md +++ b/twitter-digest/DATA_COLLECTION.md @@ -32,7 +32,7 @@ scripts/browser_x_digest.py - 无 API 配置的普通用户。 - 需要读取 X Chat / DM 内容的场景。 -- API 权限不足时的 fallback。 +- 用户显式选择浏览器模式,或完全没有 API 配置时的默认来源。 ### 2. API 抓取脚本 @@ -79,7 +79,8 @@ scripts/run_daily_digest.py - 统一入口。 - 提供 `--configure-api`,由 Agent 在对话里触发 OAuth 配置。 - 默认 `--source auto`。 -- 如果检测到环境变量 token,或 `.state/api_config.json` 里保存的 OAuth2 user-context bearer token,走 API 抓取。 +- 如果检测到环境变量 token,或 `.state/api_config.json` 里保存的 OAuth2 user-context 配置,走 API 抓取。 +- API 已配置时不启动浏览器,不自动回退浏览器;API 不可用时失败或写入 data gap。 - 如果没有 API token,走浏览器抓取。 - 如果 OAuth2 access token 快过期且保存了 refresh token,自动刷新后再抓取。 - 抓取完成后调用 `digest_context.py` 生成 `digest-context.*`。 @@ -89,7 +90,7 @@ scripts/run_daily_digest.py ```text --source api -> 强制 API,只采公开数据,不启动浏览器 --source browser -> 强制浏览器,采公开网页和可见 X Chat / DM ---source auto -> 有 X_BEARER_TOKEN/TWITTER_BEARER_TOKEN 或已保存 OAuth2 user token 用 API,否则浏览器;API 不可用时回退浏览器 +--source auto -> 有 X_BEARER_TOKEN/TWITTER_BEARER_TOKEN 或已保存 OAuth2 user token 用 API,否则浏览器;API 已配置时不回退浏览器 ``` ## 对话触发流程 @@ -159,7 +160,7 @@ Agent:再次运行 `python3 ~/.claude/skills/twitter-digest/scripts/run_daily_ ```text 用户:清除 X API 配置 Agent:运行 `python3 ~/.claude/skills/twitter-digest/scripts/configure_api.py --clear` -后续:`python3 ~/.claude/skills/twitter-digest/scripts/run_daily_digest.py --source auto` 自动回到浏览器抓取 +后续:`python3 ~/.claude/skills/twitter-digest/scripts/run_daily_digest.py --source auto` 因没有 API 配置而使用浏览器抓取 ``` ## 标准输出结构 @@ -236,7 +237,7 @@ API DM TODO / 调试规则: - 每个页面独立采集,某个 endpoint 失败不会阻塞其他页面。 - API 错误写入页面级 `collection_error`。 - Home timeline 会先尝试官方 timeline endpoint,只有 endpoint 返回权限/额度/可用性错误时才进入 data gap。 -- 没有 API token 时不报错,由上层自动 fallback 到浏览器。 +- 没有 API token 时不报错;上层仅在完全没有 API 配置时使用浏览器。 ### 上层摘要 diff --git a/twitter-digest/FUNCTION_RULES_FLOW.md b/twitter-digest/FUNCTION_RULES_FLOW.md index 004db7a..884a904 100644 --- a/twitter-digest/FUNCTION_RULES_FLOW.md +++ b/twitter-digest/FUNCTION_RULES_FLOW.md @@ -64,9 +64,9 @@ twitter-digest/.state/run/digest-context.md ## 3. 运行规则 -- 默认入口 `run_daily_digest.py --source auto`:有 OAuth2 user-context API 配置时优先用 API 抓公开数据;没有 API 配置时用浏览器抓取。 +- 默认入口 `run_daily_digest.py --source auto`:有 OAuth2 user-context API 配置时只用 API 抓公开数据,不打开浏览器;没有 API 配置时用浏览器抓取。 - DM / X Chat 以本地浏览器抓取为准;API DM 现阶段仅保留为 TODO/调试,不用于判断是否有私信。 -- API 不可用、权限不足、tier 不支持或限流时,记录数据缺口并回退浏览器路径。 +- API 不可用、权限不足、tier 不支持或限流时,记录数据缺口或失败,不回退浏览器路径。 - 不要求用户复制 cookie 或 token。 - 默认 headless 运行。 - 第一次没有登录态时,会自动打开可见浏览器让用户登录。 diff --git a/twitter-digest/README.md b/twitter-digest/README.md index 1fa842d..daf6240 100644 --- a/twitter-digest/README.md +++ b/twitter-digest/README.md @@ -82,7 +82,8 @@ python3 ~/.claude/skills/twitter-digest/scripts/run_daily_digest.py `run_daily_digest.py` defaults to `--source auto`: - If `X_BEARER_TOKEN` or `TWITTER_BEARER_TOKEN` is configured, it uses the API collector. -- Otherwise it falls back to the browser collector. +- If saved API credentials exist, it uses the API collector and does not fall back to browser on API errors. +- If no API credentials exist, it uses the browser collector. Force a source: @@ -139,7 +140,7 @@ On macOS prompts appear as system dialogs; non-GUI terminals fall back to hidden twitter-digest/.state/api_config.json ``` -The file is created with owner-only permissions where supported. Later runs of `run_daily_digest.py --source auto` read this saved config and use API automatically. To clear it: +The file is created with owner-only permissions where supported. Later runs of `run_daily_digest.py --source auto` read this saved config and use API automatically without opening the browser. To clear it: ```bash python3 ~/.claude/skills/twitter-digest/scripts/configure_api.py --clear diff --git a/twitter-digest/RUNBOOK.md b/twitter-digest/RUNBOOK.md index 08b4311..180cd71 100644 --- a/twitter-digest/RUNBOOK.md +++ b/twitter-digest/RUNBOOK.md @@ -4,7 +4,7 @@ `twitter-digest` 读取用户自己的 X/Twitter 数据并生成中文日报。抓数据层支持 API 和本地已登录浏览器两种来源。 -默认入口是 `scripts/run_daily_digest.py`。如果已经通过对话内 OAuth 授权保存了 user-context token,或环境里配置了 X API token,它优先用 API 抓公开数据;如果没有 API 配置,它自动回退到浏览器抓取。读取 X Chat / DM 内容时仍使用本地浏览器,因为普通 API 配置通常没有私信读取能力。 +默认入口是 `scripts/run_daily_digest.py`。如果已经通过对话内 OAuth 授权保存了 user-context token,或环境里配置了 X API token,它只用 API 抓公开数据,不打开浏览器;如果没有 API 配置,它使用浏览器抓取。读取 X Chat / DM 内容必须显式使用浏览器模式。 核心链路: @@ -30,6 +30,7 @@ scripts/api_x_digest.py scripts/run_daily_digest.py 上层入口。默认 --source auto: - 检测到已保存 OAuth token 或 X_BEARER_TOKEN / TWITTER_BEARER_TOKEN 时走 API。 + - API 已配置时不回退浏览器;API 失败就报错或写 data gap。 - 没有 API 配置时走浏览器。 - 保存了 refresh token 时自动刷新过期 access token。 ``` @@ -46,7 +47,7 @@ python3 twitter-digest/scripts/run_daily_digest.py --source browser X_BEARER_TOKEN=... python3 twitter-digest/scripts/run_daily_digest.py --source api --handle ``` -API 模式重点用于更稳定地抓公开数据。X Chat / DM 内容仍以浏览器模式为准;如果 API 模式无法读取 DM,会在 `digest-context` 的 Data Gaps 中标注。 +API 模式重点用于更稳定地抓公开数据。API 模式不会打开浏览器。X Chat / DM 内容仍以浏览器模式为准;如果 API 模式无法读取 DM,会在 `digest-context` 的 Data Gaps 中标注。 ## 对话内 API 授权 diff --git a/twitter-digest/SKILL.md b/twitter-digest/SKILL.md index c6853af..b54d42e 100644 --- a/twitter-digest/SKILL.md +++ b/twitter-digest/SKILL.md @@ -7,7 +7,7 @@ description: Use when the user wants Claude Code or another agent to analyze the ## Overview -Use this skill to produce a concise Chinese daily digest from the user's own X/Twitter account. Use `scripts/run_daily_digest.py`, which selects API collection when API credentials are configured and otherwise falls back to local browser collection with a persistent dedicated Chromium profile. +Use this skill to produce a concise Chinese daily digest from the user's own X/Twitter account. Use `scripts/run_daily_digest.py`, which selects API collection when API credentials are configured and otherwise uses local browser collection with a persistent dedicated Chromium profile. Once API credentials are configured, `--source auto` must not open the browser; API failures should fail or report data gaps until the user fixes or clears the API configuration. After installation, configuration and daily runs should use the installed skill copy, not a temporary clone/source checkout. Installed locations are `~/.claude/skills/twitter-digest` for Claude Code and `~/.codex/skills/twitter-digest` for Codex. If `run_daily_digest.py` or `configure_api.py` is accidentally run from a source checkout while an installed copy exists, the script automatically re-runs the installed copy so `.state` is written to the installed skill directory. @@ -48,7 +48,7 @@ For chat usage, run the wrapper: RUN_DAILY_DIGEST ``` -`run_daily_digest.py --source auto` uses saved OAuth2 user-context credentials, `X_BEARER_TOKEN`, or `TWITTER_BEARER_TOKEN` for public data when present; otherwise it uses the browser collector. The script sources are isolated: API source never starts a browser and browser source owns visible X Chat / DM collection. Treat API DM lookup as TODO / waiting for X to fix XChat-encrypted DM coverage; do not use API DM to decide whether the user has private messages. +`run_daily_digest.py --source auto` uses saved OAuth2 user-context credentials, `X_BEARER_TOKEN`, or `TWITTER_BEARER_TOKEN` for public data when present; otherwise it uses the browser collector. When API credentials are present, `auto` does not fall back to browser on API errors. The script sources are isolated: API source never starts a browser and browser source owns visible X Chat / DM collection. Treat API DM lookup as TODO / waiting for X to fix XChat-encrypted DM coverage; do not use API DM to decide whether the user has private messages. If the user asks to configure API access, trigger the OAuth/user-token setup from chat: diff --git a/twitter-digest/scripts/run_daily_digest.py b/twitter-digest/scripts/run_daily_digest.py index 78deac0..195fb13 100644 --- a/twitter-digest/scripts/run_daily_digest.py +++ b/twitter-digest/scripts/run_daily_digest.py @@ -100,6 +100,10 @@ def api_configured(bearer_token: str) -> bool: return bool(bearer_token) +def has_saved_api_credentials(config: dict) -> bool: + return any(str(config.get(key) or "") for key in ("bearer_token", "refresh_token", "client_id")) + + def summarize_child_error(error: subprocess.CalledProcessError) -> str: text = "\n".join(part for part in [error.stdout or "", error.stderr or ""] if part) return summarize_collector_error(text, returncode=error.returncode) @@ -181,7 +185,9 @@ def main() -> None: print(json.dumps({"config": str(CONFIG_PATH), "saved": bool(args.save_default)}, ensure_ascii=False, indent=2)) return config = load_config() - api_config = refresh_oauth_token_if_needed(load_api_config()) + raw_api_config = load_api_config() + saved_api_configured = has_saved_api_credentials(raw_api_config) + api_config = refresh_oauth_token_if_needed(raw_api_config) explicit_bearer_token = bool(args.bearer_token) refresh_error = str(api_config.get("refresh_error") or "") if refresh_error and not explicit_bearer_token: @@ -194,9 +200,9 @@ def main() -> None: api_base = args.api_base or str(api_config.get("api_base") or "https://api.x.com/2") user_id = args.user_id or str(api_config.get("user_id") or "") handle = (args.handle or api_config.get("handle") or config.get("handle") or "").lstrip("@") - source = args.source if args.source != "auto" else ("api" if api_configured(bearer_token) else "browser") - if args.source == "api" and refresh_error and not explicit_bearer_token: - raise SystemExit("Saved X OAuth token refresh failed. Re-run --configure-api or pass X_BEARER_TOKEN to use API source.") + source = args.source if args.source != "auto" else ("api" if (explicit_bearer_token or saved_api_configured or api_configured(bearer_token)) else "browser") + if source == "api" and refresh_error and not explicit_bearer_token: + raise SystemExit("Saved X OAuth token refresh failed. Re-run --configure-api or pass X_BEARER_TOKEN to use API source. Browser collection will not be opened while API is configured.") include_dms = not args.no_dms if args.include_dms: include_dms = True @@ -218,13 +224,9 @@ def main() -> None: run_browser_command(cmd) except subprocess.CalledProcessError as exc: summary = summarize_child_error(exc) - if args.source == "auto" and source == "api": - print(f"API collection unavailable ({summary}). Falling back to browser collection.", flush=True) - source = "browser" - run_browser_command(browser_command(args, args.out, handle, include_dms)) - else: - print(f"API collection failed: {summary}", file=sys.stderr, flush=True) - raise SystemExit(exc.returncode) from exc + source_label = "API" if source == "api" else source.capitalize() + print(f"{source_label} collection failed: {summary}", file=sys.stderr, flush=True) + raise SystemExit(exc.returncode) from exc out_dir = Path(args.out) build_current_context_from_file( input_path=out_dir / "digest-input.json", From 383ddfe539018e53d8437dc642acf7b62d3bf9aa Mon Sep 17 00:00:00 2001 From: bobo Date: Wed, 1 Jul 2026 13:55:01 +0800 Subject: [PATCH 05/34] Document Codex one-line install commands --- twitter-digest/README.md | 6 ++++++ twitter-mcp/SKILL.md | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/twitter-digest/README.md b/twitter-digest/README.md index daf6240..8229d01 100644 --- a/twitter-digest/README.md +++ b/twitter-digest/README.md @@ -14,6 +14,12 @@ python3 twitter-digest/scripts/install.py To ask Codex to install this skill for itself, paste this into Codex: +```bash +TMPDIR="$(mktemp -d)" && git clone --depth 1 --branch twitter-digest-xmcp-installer https://github.com/BofAI/skills.git "$TMPDIR/skills" && python3 "$TMPDIR/skills/twitter-digest/scripts/install.py" --client codex +``` + +Or use the natural-language prompt: + ```text 请帮我安装这个 Codex skill: diff --git a/twitter-mcp/SKILL.md b/twitter-mcp/SKILL.md index 2472ec6..1ec75cd 100644 --- a/twitter-mcp/SKILL.md +++ b/twitter-mcp/SKILL.md @@ -19,6 +19,12 @@ From the repository `skills/` directory: /bin/bash twitter-mcp/scripts/install_xmcp.sh ``` +For a one-line Codex install from this branch: + +```bash +TMPDIR="$(mktemp -d)" && git clone --depth 1 --branch twitter-digest-xmcp-installer https://github.com/BofAI/skills.git "$TMPDIR/skills" && /bin/bash "$TMPDIR/skills/twitter-mcp/scripts/install_xmcp.sh" +``` + The installer: - Requires Node.js and npm. From fdd41c984efe9d45619531116c8db2ce9c8a5798 Mon Sep 17 00:00:00 2001 From: bobo Date: Wed, 1 Jul 2026 13:58:23 +0800 Subject: [PATCH 06/34] Document Claude one-line install commands --- twitter-digest/README.md | 6 ++++++ twitter-mcp/SKILL.md | 8 +++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/twitter-digest/README.md b/twitter-digest/README.md index 8229d01..9fb737d 100644 --- a/twitter-digest/README.md +++ b/twitter-digest/README.md @@ -32,6 +32,12 @@ git clone git@github.com:BofAI/skills.git bofai-skills \ To ask Claude Code to install this skill for itself, paste this into Claude Code: +```bash +TMPDIR="$(mktemp -d)" && git clone --depth 1 --branch twitter-digest-xmcp-installer https://github.com/BofAI/skills.git "$TMPDIR/skills" && python3 "$TMPDIR/skills/twitter-digest/scripts/install.py" --client claude --allow-claude-commands --allow-claude-state-read +``` + +Or use the natural-language prompt: + ```text 请帮我安装这个 Claude Code skill: diff --git a/twitter-mcp/SKILL.md b/twitter-mcp/SKILL.md index 1ec75cd..2342c52 100644 --- a/twitter-mcp/SKILL.md +++ b/twitter-mcp/SKILL.md @@ -22,7 +22,13 @@ From the repository `skills/` directory: For a one-line Codex install from this branch: ```bash -TMPDIR="$(mktemp -d)" && git clone --depth 1 --branch twitter-digest-xmcp-installer https://github.com/BofAI/skills.git "$TMPDIR/skills" && /bin/bash "$TMPDIR/skills/twitter-mcp/scripts/install_xmcp.sh" +TMPDIR="$(mktemp -d)" && git clone --depth 1 --branch twitter-digest-xmcp-installer https://github.com/BofAI/skills.git "$TMPDIR/skills" && X_MCP_REGISTER_CODEX=1 X_MCP_REGISTER_CLAUDE=0 /bin/bash "$TMPDIR/skills/twitter-mcp/scripts/install_xmcp.sh" +``` + +For a one-line Claude Code install from this branch: + +```bash +TMPDIR="$(mktemp -d)" && git clone --depth 1 --branch twitter-digest-xmcp-installer https://github.com/BofAI/skills.git "$TMPDIR/skills" && X_MCP_REGISTER_CODEX=0 X_MCP_REGISTER_CLAUDE=1 /bin/bash "$TMPDIR/skills/twitter-mcp/scripts/install_xmcp.sh" ``` The installer: From 93e58840ac04d37934224d89f4caec551dae7371 Mon Sep 17 00:00:00 2001 From: bobo Date: Wed, 1 Jul 2026 14:01:19 +0800 Subject: [PATCH 07/34] Pin one-line install commands to beta tag --- twitter-digest/README.md | 4 ++-- twitter-mcp/SKILL.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/twitter-digest/README.md b/twitter-digest/README.md index 9fb737d..c691e5b 100644 --- a/twitter-digest/README.md +++ b/twitter-digest/README.md @@ -15,7 +15,7 @@ python3 twitter-digest/scripts/install.py To ask Codex to install this skill for itself, paste this into Codex: ```bash -TMPDIR="$(mktemp -d)" && git clone --depth 1 --branch twitter-digest-xmcp-installer https://github.com/BofAI/skills.git "$TMPDIR/skills" && python3 "$TMPDIR/skills/twitter-digest/scripts/install.py" --client codex +TMPDIR="$(mktemp -d)" && git clone --depth 1 --branch v1.5.11-beta.2 https://github.com/BofAI/skills.git "$TMPDIR/skills" && python3 "$TMPDIR/skills/twitter-digest/scripts/install.py" --client codex ``` Or use the natural-language prompt: @@ -33,7 +33,7 @@ git clone git@github.com:BofAI/skills.git bofai-skills \ To ask Claude Code to install this skill for itself, paste this into Claude Code: ```bash -TMPDIR="$(mktemp -d)" && git clone --depth 1 --branch twitter-digest-xmcp-installer https://github.com/BofAI/skills.git "$TMPDIR/skills" && python3 "$TMPDIR/skills/twitter-digest/scripts/install.py" --client claude --allow-claude-commands --allow-claude-state-read +TMPDIR="$(mktemp -d)" && git clone --depth 1 --branch v1.5.11-beta.2 https://github.com/BofAI/skills.git "$TMPDIR/skills" && python3 "$TMPDIR/skills/twitter-digest/scripts/install.py" --client claude --allow-claude-commands --allow-claude-state-read ``` Or use the natural-language prompt: diff --git a/twitter-mcp/SKILL.md b/twitter-mcp/SKILL.md index 2342c52..897c9f0 100644 --- a/twitter-mcp/SKILL.md +++ b/twitter-mcp/SKILL.md @@ -22,13 +22,13 @@ From the repository `skills/` directory: For a one-line Codex install from this branch: ```bash -TMPDIR="$(mktemp -d)" && git clone --depth 1 --branch twitter-digest-xmcp-installer https://github.com/BofAI/skills.git "$TMPDIR/skills" && X_MCP_REGISTER_CODEX=1 X_MCP_REGISTER_CLAUDE=0 /bin/bash "$TMPDIR/skills/twitter-mcp/scripts/install_xmcp.sh" +TMPDIR="$(mktemp -d)" && git clone --depth 1 --branch v1.5.11-beta.2 https://github.com/BofAI/skills.git "$TMPDIR/skills" && X_MCP_REGISTER_CODEX=1 X_MCP_REGISTER_CLAUDE=0 /bin/bash "$TMPDIR/skills/twitter-mcp/scripts/install_xmcp.sh" ``` For a one-line Claude Code install from this branch: ```bash -TMPDIR="$(mktemp -d)" && git clone --depth 1 --branch twitter-digest-xmcp-installer https://github.com/BofAI/skills.git "$TMPDIR/skills" && X_MCP_REGISTER_CODEX=0 X_MCP_REGISTER_CLAUDE=1 /bin/bash "$TMPDIR/skills/twitter-mcp/scripts/install_xmcp.sh" +TMPDIR="$(mktemp -d)" && git clone --depth 1 --branch v1.5.11-beta.2 https://github.com/BofAI/skills.git "$TMPDIR/skills" && X_MCP_REGISTER_CODEX=0 X_MCP_REGISTER_CLAUDE=1 /bin/bash "$TMPDIR/skills/twitter-mcp/scripts/install_xmcp.sh" ``` The installer: From c49dea3cba1c52c3b085d15fca1ee8869c14213a Mon Sep 17 00:00:00 2001 From: bobo Date: Wed, 1 Jul 2026 14:04:47 +0800 Subject: [PATCH 08/34] Add curl installer for Twitter MCP --- twitter-digest/README.md | 4 ++-- twitter-mcp/SKILL.md | 8 ++++---- twitter-mcp/install.sh | 27 +++++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 6 deletions(-) create mode 100755 twitter-mcp/install.sh diff --git a/twitter-digest/README.md b/twitter-digest/README.md index c691e5b..a6543ef 100644 --- a/twitter-digest/README.md +++ b/twitter-digest/README.md @@ -15,7 +15,7 @@ python3 twitter-digest/scripts/install.py To ask Codex to install this skill for itself, paste this into Codex: ```bash -TMPDIR="$(mktemp -d)" && git clone --depth 1 --branch v1.5.11-beta.2 https://github.com/BofAI/skills.git "$TMPDIR/skills" && python3 "$TMPDIR/skills/twitter-digest/scripts/install.py" --client codex +TMPDIR="$(mktemp -d)" && git clone --depth 1 --branch v1.5.11-beta.3 https://github.com/BofAI/skills.git "$TMPDIR/skills" && python3 "$TMPDIR/skills/twitter-digest/scripts/install.py" --client codex ``` Or use the natural-language prompt: @@ -33,7 +33,7 @@ git clone git@github.com:BofAI/skills.git bofai-skills \ To ask Claude Code to install this skill for itself, paste this into Claude Code: ```bash -TMPDIR="$(mktemp -d)" && git clone --depth 1 --branch v1.5.11-beta.2 https://github.com/BofAI/skills.git "$TMPDIR/skills" && python3 "$TMPDIR/skills/twitter-digest/scripts/install.py" --client claude --allow-claude-commands --allow-claude-state-read +TMPDIR="$(mktemp -d)" && git clone --depth 1 --branch v1.5.11-beta.3 https://github.com/BofAI/skills.git "$TMPDIR/skills" && python3 "$TMPDIR/skills/twitter-digest/scripts/install.py" --client claude --allow-claude-commands --allow-claude-state-read ``` Or use the natural-language prompt: diff --git a/twitter-mcp/SKILL.md b/twitter-mcp/SKILL.md index 897c9f0..c285a39 100644 --- a/twitter-mcp/SKILL.md +++ b/twitter-mcp/SKILL.md @@ -19,16 +19,16 @@ From the repository `skills/` directory: /bin/bash twitter-mcp/scripts/install_xmcp.sh ``` -For a one-line Codex install from this branch: +For a one-line Codex install from this beta tag: ```bash -TMPDIR="$(mktemp -d)" && git clone --depth 1 --branch v1.5.11-beta.2 https://github.com/BofAI/skills.git "$TMPDIR/skills" && X_MCP_REGISTER_CODEX=1 X_MCP_REGISTER_CLAUDE=0 /bin/bash "$TMPDIR/skills/twitter-mcp/scripts/install_xmcp.sh" +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.3/twitter-mcp/install.sh | X_MCP_REGISTER_CODEX=1 X_MCP_REGISTER_CLAUDE=0 sh ``` -For a one-line Claude Code install from this branch: +For a one-line Claude Code install from this beta tag: ```bash -TMPDIR="$(mktemp -d)" && git clone --depth 1 --branch v1.5.11-beta.2 https://github.com/BofAI/skills.git "$TMPDIR/skills" && X_MCP_REGISTER_CODEX=0 X_MCP_REGISTER_CLAUDE=1 /bin/bash "$TMPDIR/skills/twitter-mcp/scripts/install_xmcp.sh" +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.3/twitter-mcp/install.sh | X_MCP_REGISTER_CODEX=0 X_MCP_REGISTER_CLAUDE=1 sh ``` The installer: diff --git a/twitter-mcp/install.sh b/twitter-mcp/install.sh new file mode 100755 index 0000000..350b1ae --- /dev/null +++ b/twitter-mcp/install.sh @@ -0,0 +1,27 @@ +#!/bin/sh +set -eu + +TAG="${X_MCP_INSTALL_TAG:-v1.5.11-beta.3}" +BASE_URL="${X_MCP_INSTALL_BASE_URL:-https://raw.githubusercontent.com/BofAI/skills/${TAG}/twitter-mcp}" + +if command -v mktemp >/dev/null 2>&1; then + WORKDIR="$(mktemp -d 2>/dev/null || mktemp -d -t twitter-mcp)" +else + WORKDIR="${TMPDIR:-/tmp}/twitter-mcp-install.$$" + mkdir -p "$WORKDIR" +fi + +INSTALLER="$WORKDIR/install_xmcp.sh" +INSTALLER_URL="${BASE_URL%/}/scripts/install_xmcp.sh" + +if command -v curl >/dev/null 2>&1; then + curl -fsSL "$INSTALLER_URL" -o "$INSTALLER" +elif command -v wget >/dev/null 2>&1; then + wget -qO "$INSTALLER" "$INSTALLER_URL" +else + printf 'Error: curl or wget is required to download %s\n' "$INSTALLER_URL" >&2 + exit 1 +fi + +chmod 700 "$INSTALLER" +exec /bin/bash "$INSTALLER" "$@" From 5e4b7b4bfc306dde0e076cc1884e57f1279571b6 Mon Sep 17 00:00:00 2001 From: bobo Date: Wed, 1 Jul 2026 15:01:12 +0800 Subject: [PATCH 09/34] Document digest source isolation --- twitter-digest/DATA_COLLECTION.md | 7 +++++++ twitter-digest/README.md | 13 ++++++++++--- twitter-digest/RUNBOOK.md | 7 +++++++ twitter-digest/SKILL.md | 11 +++++++++-- twitter-mcp/SKILL.md | 4 ++-- twitter-mcp/install.sh | 2 +- 6 files changed, 36 insertions(+), 8 deletions(-) diff --git a/twitter-digest/DATA_COLLECTION.md b/twitter-digest/DATA_COLLECTION.md index f3db5d6..e65f602 100644 --- a/twitter-digest/DATA_COLLECTION.md +++ b/twitter-digest/DATA_COLLECTION.md @@ -93,6 +93,13 @@ scripts/run_daily_digest.py --source auto -> 有 X_BEARER_TOKEN/TWITTER_BEARER_TOKEN 或已保存 OAuth2 user token 用 API,否则浏览器;API 已配置时不回退浏览器 ``` +隔离规则: + +- API 来源只运行 `api_x_digest.py`,不启动浏览器、不读取浏览器 profile、不用浏览器补采 API 缺口。 +- 浏览器来源只运行 `browser_x_digest.py`,不读取 API token、不合并 API collector 输出。 +- `--source auto` 每次只选择一个来源,不合并 API 和浏览器两边的数据。 +- API 输出里的“需要浏览器确认 DM”只能作为 data gap 提示,不表示本次已经读取了浏览器 DM。 + ## 对话触发流程 底层只保留两个抓取脚本: diff --git a/twitter-digest/README.md b/twitter-digest/README.md index a6543ef..80bd25c 100644 --- a/twitter-digest/README.md +++ b/twitter-digest/README.md @@ -15,7 +15,7 @@ python3 twitter-digest/scripts/install.py To ask Codex to install this skill for itself, paste this into Codex: ```bash -TMPDIR="$(mktemp -d)" && git clone --depth 1 --branch v1.5.11-beta.3 https://github.com/BofAI/skills.git "$TMPDIR/skills" && python3 "$TMPDIR/skills/twitter-digest/scripts/install.py" --client codex +TMPDIR="$(mktemp -d)" && git clone --depth 1 --branch v1.5.11-beta.4 https://github.com/BofAI/skills.git "$TMPDIR/skills" && python3 "$TMPDIR/skills/twitter-digest/scripts/install.py" --client codex ``` Or use the natural-language prompt: @@ -33,7 +33,7 @@ git clone git@github.com:BofAI/skills.git bofai-skills \ To ask Claude Code to install this skill for itself, paste this into Claude Code: ```bash -TMPDIR="$(mktemp -d)" && git clone --depth 1 --branch v1.5.11-beta.3 https://github.com/BofAI/skills.git "$TMPDIR/skills" && python3 "$TMPDIR/skills/twitter-digest/scripts/install.py" --client claude --allow-claude-commands --allow-claude-state-read +TMPDIR="$(mktemp -d)" && git clone --depth 1 --branch v1.5.11-beta.4 https://github.com/BofAI/skills.git "$TMPDIR/skills" && python3 "$TMPDIR/skills/twitter-digest/scripts/install.py" --client claude --allow-claude-commands --allow-claude-state-read ``` Or use the natural-language prompt: @@ -97,6 +97,13 @@ python3 ~/.claude/skills/twitter-digest/scripts/run_daily_digest.py - If saved API credentials exist, it uses the API collector and does not fall back to browser on API errors. - If no API credentials exist, it uses the browser collector. +Source isolation: + +- API mode only runs `api_x_digest.py`; it never opens a browser or reads the browser profile. +- Browser mode only runs `browser_x_digest.py`; it does not use API tokens or API collector output. +- `--source auto` picks one source for the run and does not merge API and browser data. +- API DM data gaps are notes only; they do not mean browser DM data was collected. + Force a source: ```bash @@ -104,7 +111,7 @@ python3 ~/.claude/skills/twitter-digest/scripts/run_daily_digest.py --source bro X_BEARER_TOKEN=... python3 ~/.claude/skills/twitter-digest/scripts/run_daily_digest.py --source api --handle ``` -API mode is for stable public-data collection, including the official home timeline endpoint when the configured token has user-context timeline access. API mode never starts a browser and never collects X Chat / DM content. Use browser mode when visible X Chat / DM is required. API DM lookup is marked TODO because XChat / encrypted DMs may not appear in `/2/dm_events`; do not use API DM to conclude there are no private messages. App-only API keys are not enough for user-context data. +API mode is for stable public-data collection, including the official home timeline endpoint when the configured token has user-context timeline access. API mode never starts a browser, never reads the browser profile, and never collects X Chat / DM content from the browser. Use browser mode when visible X Chat / DM is required. API DM lookup is marked TODO because XChat / encrypted DMs may not appear in `/2/dm_events`; do not use API DM to conclude there are no private messages. App-only API keys are not enough for user-context data. ## Configure API In Chat diff --git a/twitter-digest/RUNBOOK.md b/twitter-digest/RUNBOOK.md index 180cd71..23c07ec 100644 --- a/twitter-digest/RUNBOOK.md +++ b/twitter-digest/RUNBOOK.md @@ -49,6 +49,13 @@ X_BEARER_TOKEN=... python3 twitter-digest/scripts/run_daily_digest.py --source a API 模式重点用于更稳定地抓公开数据。API 模式不会打开浏览器。X Chat / DM 内容仍以浏览器模式为准;如果 API 模式无法读取 DM,会在 `digest-context` 的 Data Gaps 中标注。 +来源隔离规则: + +- API 模式只运行 `api_x_digest.py`,不启动浏览器、不读取浏览器 profile、不用浏览器补采 API 缺口。 +- 浏览器模式只运行 `browser_x_digest.py`,不读取 API token、不合并 API collector 输出。 +- `--source auto` 每次只选择一个来源,不合并 API 和浏览器两边的数据。 +- API 模式输出的 DM 浏览器确认提示只是 data gap,不代表浏览器数据已被采集。 + ## 对话内 API 授权 用户不需要自己 export 环境变量。用户在对话里说“配置 X API”时,Agent 运行: diff --git a/twitter-digest/SKILL.md b/twitter-digest/SKILL.md index b54d42e..0f2db3b 100644 --- a/twitter-digest/SKILL.md +++ b/twitter-digest/SKILL.md @@ -48,7 +48,14 @@ For chat usage, run the wrapper: RUN_DAILY_DIGEST ``` -`run_daily_digest.py --source auto` uses saved OAuth2 user-context credentials, `X_BEARER_TOKEN`, or `TWITTER_BEARER_TOKEN` for public data when present; otherwise it uses the browser collector. When API credentials are present, `auto` does not fall back to browser on API errors. The script sources are isolated: API source never starts a browser and browser source owns visible X Chat / DM collection. Treat API DM lookup as TODO / waiting for X to fix XChat-encrypted DM coverage; do not use API DM to decide whether the user has private messages. +`run_daily_digest.py --source auto` uses saved OAuth2 user-context credentials, `X_BEARER_TOKEN`, or `TWITTER_BEARER_TOKEN` for public data when present; otherwise it uses the browser collector. When API credentials are present, `auto` does not fall back to browser on API errors. Treat API DM lookup as TODO / waiting for X to fix XChat-encrypted DM coverage; do not use API DM to decide whether the user has private messages. + +Source isolation is strict: + +- API source runs only `api_x_digest.py`. It never starts a browser, never opens X pages, never reads the browser profile, and never supplements missing API data with browser data. +- Browser source runs only `browser_x_digest.py`. It uses the dedicated browser profile and does not use saved API tokens or API collector output. +- `--source auto` picks exactly one source for the run: API when API credentials exist, otherwise browser. It does not merge both sources. +- If API output says DM needs browser confirmation, treat that as a data gap note only. It does not mean browser data was collected. If the user asks to configure API access, trigger the OAuth/user-token setup from chat: @@ -91,7 +98,7 @@ RUN_DAILY_DIGEST --source browser X_BEARER_TOKEN=... RUN_DAILY_DIGEST --source api --handle ``` -The first run opens a dedicated browser profile at `twitter-digest/.state/chrome-profile`. The user logs in to X once in that browser. Later runs default to headless collection and reuse the saved local browser session. If the saved login is unavailable, the script automatically opens a visible browser window for manual login. The skill has two collector scripts: `scripts/api_x_digest.py` for official API public data, and `scripts/browser_x_digest.py` for browser-visible X Chat / encrypted DM content. API-visible DM events remain TODO-only until X fixes or documents reliable XChat coverage. +Browser-source runs use a dedicated browser profile at `twitter-digest/.state/chrome-profile`. The user logs in to X once in that browser. Later browser runs default to headless collection and reuse the saved local browser session. If the saved login is unavailable during a browser-source run, the script automatically opens a visible browser window for manual login. API-source runs do not touch this profile. The skill has two collector scripts: `scripts/api_x_digest.py` for official API public data, and `scripts/browser_x_digest.py` for browser-visible X Chat / encrypted DM content. API-visible DM events remain TODO-only until X fixes or documents reliable XChat coverage. DM reading is enabled by default only for browser-source runs and only reads visible local browser content. API-source runs do not start a browser, even when `--include-dms` is passed. To skip DMs for a browser run: diff --git a/twitter-mcp/SKILL.md b/twitter-mcp/SKILL.md index c285a39..73f782e 100644 --- a/twitter-mcp/SKILL.md +++ b/twitter-mcp/SKILL.md @@ -22,13 +22,13 @@ From the repository `skills/` directory: For a one-line Codex install from this beta tag: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.3/twitter-mcp/install.sh | X_MCP_REGISTER_CODEX=1 X_MCP_REGISTER_CLAUDE=0 sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.4/twitter-mcp/install.sh | X_MCP_REGISTER_CODEX=1 X_MCP_REGISTER_CLAUDE=0 sh ``` For a one-line Claude Code install from this beta tag: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.3/twitter-mcp/install.sh | X_MCP_REGISTER_CODEX=0 X_MCP_REGISTER_CLAUDE=1 sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.4/twitter-mcp/install.sh | X_MCP_REGISTER_CODEX=0 X_MCP_REGISTER_CLAUDE=1 sh ``` The installer: diff --git a/twitter-mcp/install.sh b/twitter-mcp/install.sh index 350b1ae..6ac5f30 100755 --- a/twitter-mcp/install.sh +++ b/twitter-mcp/install.sh @@ -1,7 +1,7 @@ #!/bin/sh set -eu -TAG="${X_MCP_INSTALL_TAG:-v1.5.11-beta.3}" +TAG="${X_MCP_INSTALL_TAG:-v1.5.11-beta.4}" BASE_URL="${X_MCP_INSTALL_BASE_URL:-https://raw.githubusercontent.com/BofAI/skills/${TAG}/twitter-mcp}" if command -v mktemp >/dev/null 2>&1; then From f35ae11363fbf7bea0c2bb0af856e810500e519e Mon Sep 17 00:00:00 2001 From: bobo Date: Wed, 1 Jul 2026 15:09:52 +0800 Subject: [PATCH 10/34] Install Twitter MCP skill during bootstrap --- twitter-digest/README.md | 4 +- twitter-mcp/SKILL.md | 5 ++- twitter-mcp/install.sh | 92 ++++++++++++++++++++++++++++++++++++---- 3 files changed, 89 insertions(+), 12 deletions(-) diff --git a/twitter-digest/README.md b/twitter-digest/README.md index 80bd25c..ca1a2b2 100644 --- a/twitter-digest/README.md +++ b/twitter-digest/README.md @@ -15,7 +15,7 @@ python3 twitter-digest/scripts/install.py To ask Codex to install this skill for itself, paste this into Codex: ```bash -TMPDIR="$(mktemp -d)" && git clone --depth 1 --branch v1.5.11-beta.4 https://github.com/BofAI/skills.git "$TMPDIR/skills" && python3 "$TMPDIR/skills/twitter-digest/scripts/install.py" --client codex +TMPDIR="$(mktemp -d)" && git clone --depth 1 --branch v1.5.11-beta.5 https://github.com/BofAI/skills.git "$TMPDIR/skills" && python3 "$TMPDIR/skills/twitter-digest/scripts/install.py" --client codex ``` Or use the natural-language prompt: @@ -33,7 +33,7 @@ git clone git@github.com:BofAI/skills.git bofai-skills \ To ask Claude Code to install this skill for itself, paste this into Claude Code: ```bash -TMPDIR="$(mktemp -d)" && git clone --depth 1 --branch v1.5.11-beta.4 https://github.com/BofAI/skills.git "$TMPDIR/skills" && python3 "$TMPDIR/skills/twitter-digest/scripts/install.py" --client claude --allow-claude-commands --allow-claude-state-read +TMPDIR="$(mktemp -d)" && git clone --depth 1 --branch v1.5.11-beta.5 https://github.com/BofAI/skills.git "$TMPDIR/skills" && python3 "$TMPDIR/skills/twitter-digest/scripts/install.py" --client claude --allow-claude-commands --allow-claude-state-read ``` Or use the natural-language prompt: diff --git a/twitter-mcp/SKILL.md b/twitter-mcp/SKILL.md index 73f782e..62ef6c5 100644 --- a/twitter-mcp/SKILL.md +++ b/twitter-mcp/SKILL.md @@ -22,17 +22,18 @@ From the repository `skills/` directory: For a one-line Codex install from this beta tag: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.4/twitter-mcp/install.sh | X_MCP_REGISTER_CODEX=1 X_MCP_REGISTER_CLAUDE=0 sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.5/twitter-mcp/install.sh | X_MCP_REGISTER_CODEX=1 X_MCP_REGISTER_CLAUDE=0 sh ``` For a one-line Claude Code install from this beta tag: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.4/twitter-mcp/install.sh | X_MCP_REGISTER_CODEX=0 X_MCP_REGISTER_CLAUDE=1 sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.5/twitter-mcp/install.sh | X_MCP_REGISTER_CODEX=0 X_MCP_REGISTER_CLAUDE=1 sh ``` The installer: +- Installs this `twitter-mcp` skill into the selected local skills directory. - Requires Node.js and npm. - Installs `@xdevplatform/xurl` globally with npm. - Opens the X OAuth2 authorization flow. diff --git a/twitter-mcp/install.sh b/twitter-mcp/install.sh index 6ac5f30..2312ce1 100755 --- a/twitter-mcp/install.sh +++ b/twitter-mcp/install.sh @@ -1,8 +1,14 @@ #!/bin/sh set -eu -TAG="${X_MCP_INSTALL_TAG:-v1.5.11-beta.4}" +TAG="${X_MCP_INSTALL_TAG:-v1.5.11-beta.5}" BASE_URL="${X_MCP_INSTALL_BASE_URL:-https://raw.githubusercontent.com/BofAI/skills/${TAG}/twitter-mcp}" +REGISTER_CODEX="${X_MCP_REGISTER_CODEX:-1}" +REGISTER_CLAUDE="${X_MCP_REGISTER_CLAUDE:-auto}" + +info() { + printf '==> %s\n' "$1" +} if command -v mktemp >/dev/null 2>&1; then WORKDIR="$(mktemp -d 2>/dev/null || mktemp -d -t twitter-mcp)" @@ -14,14 +20,84 @@ fi INSTALLER="$WORKDIR/install_xmcp.sh" INSTALLER_URL="${BASE_URL%/}/scripts/install_xmcp.sh" -if command -v curl >/dev/null 2>&1; then - curl -fsSL "$INSTALLER_URL" -o "$INSTALLER" -elif command -v wget >/dev/null 2>&1; then - wget -qO "$INSTALLER" "$INSTALLER_URL" -else - printf 'Error: curl or wget is required to download %s\n' "$INSTALLER_URL" >&2 - exit 1 +download_file() { + url="$1" + output="$2" + if command -v curl >/dev/null 2>&1; then + curl -fsSL "$url" -o "$output" + elif command -v wget >/dev/null 2>&1; then + wget -qO "$output" "$url" + else + printf 'Error: curl or wget is required to download %s\n' "$url" >&2 + exit 1 + fi +} + +truthy() { + case "${1:-}" in + 1|true|yes) return 0 ;; + *) return 1 ;; + esac +} + +should_install_claude_skill() { + if truthy "$REGISTER_CLAUDE"; then + return 0 + fi + if [ "$REGISTER_CLAUDE" = "auto" ] && command -v claude >/dev/null 2>&1; then + return 0 + fi + return 1 +} + +backup_existing_skill() { + target="$1" + skills_dir="$2" + if [ ! -e "$target" ] && [ ! -L "$target" ]; then + return 0 + fi + backup_dir="$skills_dir/.backups" + stamp="$(date +%Y%m%d-%H%M%S)" + backup="$backup_dir/twitter-mcp-$stamp" + suffix=1 + while [ -e "$backup" ] || [ -L "$backup" ]; do + suffix=$((suffix + 1)) + backup="$backup_dir/twitter-mcp-$stamp-$suffix" + done + mkdir -p "$backup_dir" + mv "$target" "$backup" + if [ -f "$backup/SKILL.md" ]; then + mv "$backup/SKILL.md" "$backup/SKILL.md.disabled" + fi + info "Existing twitter-mcp skill moved to $backup" +} + +install_skill_copy() { + skills_dir="$1" + target="$skills_dir/twitter-mcp" + staging="$WORKDIR/twitter-mcp-skill" + rm -rf "$staging" + mkdir -p "$staging/scripts" "$staging/agents" + download_file "${BASE_URL%/}/SKILL.md" "$staging/SKILL.md" + download_file "${BASE_URL%/}/install.sh" "$staging/install.sh" + download_file "${BASE_URL%/}/agents/openai.yaml" "$staging/agents/openai.yaml" + download_file "${BASE_URL%/}/scripts/install_xmcp.sh" "$staging/scripts/install_xmcp.sh" + chmod 700 "$staging/install.sh" "$staging/scripts/install_xmcp.sh" + mkdir -p "$skills_dir" + backup_existing_skill "$target" "$skills_dir" + mv "$staging" "$target" + info "Installed twitter-mcp skill to $target" +} + +if truthy "$REGISTER_CODEX"; then + install_skill_copy "$HOME/.codex/skills" fi +if should_install_claude_skill; then + install_skill_copy "$HOME/.claude/skills" +fi + +download_file "$INSTALLER_URL" "$INSTALLER" + chmod 700 "$INSTALLER" exec /bin/bash "$INSTALLER" "$@" From 2dc63c24c5d44835d0aff93b03cd7976a1aaed14 Mon Sep 17 00:00:00 2001 From: bobo Date: Wed, 1 Jul 2026 15:32:48 +0800 Subject: [PATCH 11/34] Fix Terminal wait prompt for zsh --- twitter-digest/README.md | 4 ++-- twitter-mcp/SKILL.md | 4 ++-- twitter-mcp/install.sh | 2 +- twitter-mcp/scripts/install_xmcp.sh | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/twitter-digest/README.md b/twitter-digest/README.md index ca1a2b2..886c1cb 100644 --- a/twitter-digest/README.md +++ b/twitter-digest/README.md @@ -15,7 +15,7 @@ python3 twitter-digest/scripts/install.py To ask Codex to install this skill for itself, paste this into Codex: ```bash -TMPDIR="$(mktemp -d)" && git clone --depth 1 --branch v1.5.11-beta.5 https://github.com/BofAI/skills.git "$TMPDIR/skills" && python3 "$TMPDIR/skills/twitter-digest/scripts/install.py" --client codex +TMPDIR="$(mktemp -d)" && git clone --depth 1 --branch v1.5.11-beta.6 https://github.com/BofAI/skills.git "$TMPDIR/skills" && python3 "$TMPDIR/skills/twitter-digest/scripts/install.py" --client codex ``` Or use the natural-language prompt: @@ -33,7 +33,7 @@ git clone git@github.com:BofAI/skills.git bofai-skills \ To ask Claude Code to install this skill for itself, paste this into Claude Code: ```bash -TMPDIR="$(mktemp -d)" && git clone --depth 1 --branch v1.5.11-beta.5 https://github.com/BofAI/skills.git "$TMPDIR/skills" && python3 "$TMPDIR/skills/twitter-digest/scripts/install.py" --client claude --allow-claude-commands --allow-claude-state-read +TMPDIR="$(mktemp -d)" && git clone --depth 1 --branch v1.5.11-beta.6 https://github.com/BofAI/skills.git "$TMPDIR/skills" && python3 "$TMPDIR/skills/twitter-digest/scripts/install.py" --client claude --allow-claude-commands --allow-claude-state-read ``` Or use the natural-language prompt: diff --git a/twitter-mcp/SKILL.md b/twitter-mcp/SKILL.md index 62ef6c5..9f242c2 100644 --- a/twitter-mcp/SKILL.md +++ b/twitter-mcp/SKILL.md @@ -22,13 +22,13 @@ From the repository `skills/` directory: For a one-line Codex install from this beta tag: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.5/twitter-mcp/install.sh | X_MCP_REGISTER_CODEX=1 X_MCP_REGISTER_CLAUDE=0 sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.6/twitter-mcp/install.sh | X_MCP_REGISTER_CODEX=1 X_MCP_REGISTER_CLAUDE=0 sh ``` For a one-line Claude Code install from this beta tag: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.5/twitter-mcp/install.sh | X_MCP_REGISTER_CODEX=0 X_MCP_REGISTER_CLAUDE=1 sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.6/twitter-mcp/install.sh | X_MCP_REGISTER_CODEX=0 X_MCP_REGISTER_CLAUDE=1 sh ``` The installer: diff --git a/twitter-mcp/install.sh b/twitter-mcp/install.sh index 2312ce1..b14f4c8 100755 --- a/twitter-mcp/install.sh +++ b/twitter-mcp/install.sh @@ -1,7 +1,7 @@ #!/bin/sh set -eu -TAG="${X_MCP_INSTALL_TAG:-v1.5.11-beta.5}" +TAG="${X_MCP_INSTALL_TAG:-v1.5.11-beta.6}" BASE_URL="${X_MCP_INSTALL_BASE_URL:-https://raw.githubusercontent.com/BofAI/skills/${TAG}/twitter-mcp}" REGISTER_CODEX="${X_MCP_REGISTER_CODEX:-1}" REGISTER_CLAUDE="${X_MCP_REGISTER_CLAUDE:-auto}" diff --git a/twitter-mcp/scripts/install_xmcp.sh b/twitter-mcp/scripts/install_xmcp.sh index e82e756..7c8fe0c 100755 --- a/twitter-mcp/scripts/install_xmcp.sh +++ b/twitter-mcp/scripts/install_xmcp.sh @@ -94,7 +94,7 @@ open_in_terminal_and_exit() { local cwd cwd="$(pwd)" local command_text - command_text="cd $(shell_quote "$cwd") && X_MCP_TERMINAL_CHILD=1 /bin/bash $(shell_quote "$path"); printf '\\n'; read -r -n 1 -s -p 'Press any key to close this window...'" + command_text="cd $(shell_quote "$cwd") && X_MCP_TERMINAL_CHILD=1 /bin/bash $(shell_quote "$path"); printf '\\nPress Enter to close this window...'; IFS= read -r _" osascript >/dev/null < Date: Wed, 1 Jul 2026 15:35:11 +0800 Subject: [PATCH 12/34] Improve Twitter digest Chinese trigger metadata --- twitter-digest/README.md | 4 ++-- twitter-digest/SKILL.md | 2 +- twitter-digest/agents/openai.yaml | 4 ++-- twitter-mcp/SKILL.md | 4 ++-- twitter-mcp/install.sh | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/twitter-digest/README.md b/twitter-digest/README.md index 886c1cb..0a6e3a9 100644 --- a/twitter-digest/README.md +++ b/twitter-digest/README.md @@ -15,7 +15,7 @@ python3 twitter-digest/scripts/install.py To ask Codex to install this skill for itself, paste this into Codex: ```bash -TMPDIR="$(mktemp -d)" && git clone --depth 1 --branch v1.5.11-beta.6 https://github.com/BofAI/skills.git "$TMPDIR/skills" && python3 "$TMPDIR/skills/twitter-digest/scripts/install.py" --client codex +TMPDIR="$(mktemp -d)" && git clone --depth 1 --branch v1.5.11-beta.7 https://github.com/BofAI/skills.git "$TMPDIR/skills" && python3 "$TMPDIR/skills/twitter-digest/scripts/install.py" --client codex ``` Or use the natural-language prompt: @@ -33,7 +33,7 @@ git clone git@github.com:BofAI/skills.git bofai-skills \ To ask Claude Code to install this skill for itself, paste this into Claude Code: ```bash -TMPDIR="$(mktemp -d)" && git clone --depth 1 --branch v1.5.11-beta.6 https://github.com/BofAI/skills.git "$TMPDIR/skills" && python3 "$TMPDIR/skills/twitter-digest/scripts/install.py" --client claude --allow-claude-commands --allow-claude-state-read +TMPDIR="$(mktemp -d)" && git clone --depth 1 --branch v1.5.11-beta.7 https://github.com/BofAI/skills.git "$TMPDIR/skills" && python3 "$TMPDIR/skills/twitter-digest/scripts/install.py" --client claude --allow-claude-commands --allow-claude-state-read ``` Or use the natural-language prompt: diff --git a/twitter-digest/SKILL.md b/twitter-digest/SKILL.md index 0f2db3b..5f0edb6 100644 --- a/twitter-digest/SKILL.md +++ b/twitter-digest/SKILL.md @@ -1,6 +1,6 @@ --- name: twitter-digest -description: Use when the user wants Claude Code or another agent to analyze their own X/Twitter mentions, home timeline, visible direct messages, reply opportunities, and daily social-media summaries through API or local logged-in browser collection. +description: Use when the user asks to generate an X/Twitter daily digest or says phrases such as "生成X日报", "生成 x 日报", "X日报", "推特日报", "Twitter digest", or wants an agent to analyze their own X/Twitter mentions, home timeline, visible direct messages, reply opportunities, and daily social-media summaries through API or local logged-in browser collection. --- # X/Twitter Digest diff --git a/twitter-digest/agents/openai.yaml b/twitter-digest/agents/openai.yaml index 116d4ec..1a0d6fb 100644 --- a/twitter-digest/agents/openai.yaml +++ b/twitter-digest/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "Twitter Digest" - short_description: "Analyze X mentions, trends, DMs, daily summaries, and local memory" - default_prompt: "Use $twitter-digest to summarize my X mentions, timeline hotspots, important DMs, and what changed since prior runs." + short_description: "Generate X/Twitter daily digests from own account" + default_prompt: "Use $twitter-digest to generate my X daily digest, including mentions, timeline hotspots, important DMs, and action items." diff --git a/twitter-mcp/SKILL.md b/twitter-mcp/SKILL.md index 9f242c2..3db0bad 100644 --- a/twitter-mcp/SKILL.md +++ b/twitter-mcp/SKILL.md @@ -22,13 +22,13 @@ From the repository `skills/` directory: For a one-line Codex install from this beta tag: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.6/twitter-mcp/install.sh | X_MCP_REGISTER_CODEX=1 X_MCP_REGISTER_CLAUDE=0 sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.7/twitter-mcp/install.sh | X_MCP_REGISTER_CODEX=1 X_MCP_REGISTER_CLAUDE=0 sh ``` For a one-line Claude Code install from this beta tag: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.6/twitter-mcp/install.sh | X_MCP_REGISTER_CODEX=0 X_MCP_REGISTER_CLAUDE=1 sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.7/twitter-mcp/install.sh | X_MCP_REGISTER_CODEX=0 X_MCP_REGISTER_CLAUDE=1 sh ``` The installer: diff --git a/twitter-mcp/install.sh b/twitter-mcp/install.sh index b14f4c8..15b494d 100755 --- a/twitter-mcp/install.sh +++ b/twitter-mcp/install.sh @@ -1,7 +1,7 @@ #!/bin/sh set -eu -TAG="${X_MCP_INSTALL_TAG:-v1.5.11-beta.6}" +TAG="${X_MCP_INSTALL_TAG:-v1.5.11-beta.7}" BASE_URL="${X_MCP_INSTALL_BASE_URL:-https://raw.githubusercontent.com/BofAI/skills/${TAG}/twitter-mcp}" REGISTER_CODEX="${X_MCP_REGISTER_CODEX:-1}" REGISTER_CLAUDE="${X_MCP_REGISTER_CLAUDE:-auto}" From dd5c7998a0dd0cd165e0e876f5cb35dcc4534bd0 Mon Sep 17 00:00:00 2001 From: bobo Date: Wed, 1 Jul 2026 15:43:57 +0800 Subject: [PATCH 13/34] Add MCP-sourced Twitter digest workflow --- twitter-digest/README.md | 4 ++-- twitter-mcp/SKILL.md | 41 +++++++++++++++++++++++++++++----- twitter-mcp/agents/openai.yaml | 4 ++-- twitter-mcp/install.sh | 2 +- 4 files changed, 40 insertions(+), 11 deletions(-) diff --git a/twitter-digest/README.md b/twitter-digest/README.md index 0a6e3a9..3e5c675 100644 --- a/twitter-digest/README.md +++ b/twitter-digest/README.md @@ -15,7 +15,7 @@ python3 twitter-digest/scripts/install.py To ask Codex to install this skill for itself, paste this into Codex: ```bash -TMPDIR="$(mktemp -d)" && git clone --depth 1 --branch v1.5.11-beta.7 https://github.com/BofAI/skills.git "$TMPDIR/skills" && python3 "$TMPDIR/skills/twitter-digest/scripts/install.py" --client codex +TMPDIR="$(mktemp -d)" && git clone --depth 1 --branch v1.5.11-beta.8 https://github.com/BofAI/skills.git "$TMPDIR/skills" && python3 "$TMPDIR/skills/twitter-digest/scripts/install.py" --client codex ``` Or use the natural-language prompt: @@ -33,7 +33,7 @@ git clone git@github.com:BofAI/skills.git bofai-skills \ To ask Claude Code to install this skill for itself, paste this into Claude Code: ```bash -TMPDIR="$(mktemp -d)" && git clone --depth 1 --branch v1.5.11-beta.7 https://github.com/BofAI/skills.git "$TMPDIR/skills" && python3 "$TMPDIR/skills/twitter-digest/scripts/install.py" --client claude --allow-claude-commands --allow-claude-state-read +TMPDIR="$(mktemp -d)" && git clone --depth 1 --branch v1.5.11-beta.8 https://github.com/BofAI/skills.git "$TMPDIR/skills" && python3 "$TMPDIR/skills/twitter-digest/scripts/install.py" --client claude --allow-claude-commands --allow-claude-state-read ``` Or use the natural-language prompt: diff --git a/twitter-mcp/SKILL.md b/twitter-mcp/SKILL.md index 3db0bad..bd78450 100644 --- a/twitter-mcp/SKILL.md +++ b/twitter-mcp/SKILL.md @@ -1,16 +1,45 @@ --- name: twitter-mcp -description: Use when the user wants to install, authorize, register, verify, or troubleshoot the official X/Twitter MCP server for Codex, Claude Code, or another MCP-capable agent. +description: Use when the user wants to install, authorize, register, verify, or troubleshoot the official X/Twitter MCP server for Codex, Claude Code, or another MCP-capable agent. Also use when the user asks "生成X日报", "X日报", "推特日报", or "Twitter digest" and wants the digest generated from X MCP tools rather than local API/browser collectors. --- # X/Twitter MCP ## Overview -Use this skill to install and register the official X MCP server through `@xdevplatform/xurl`. This skill only handles MCP setup and troubleshooting. It does not collect timeline data, generate digests, read DMs, or install `twitter-digest`. +Use this skill to install and register the official X MCP server through `@xdevplatform/xurl`, and to generate X/Twitter daily digests from X MCP tool results when those tools are visible in the current agent session. This skill does not use local API collectors, local browser collectors, or `twitter-digest` scripts. After successful setup, the MCP server is registered as `xapi` by default. The user usually needs to start a new Codex or Claude Code session before newly registered MCP tools become visible. +If the user asks to generate an X/Twitter daily digest, says `生成X日报`, `X日报`, `推特日报`, or asks for a Twitter digest, use available `xapi` / X MCP tools directly and write the digest from those tool results. Do not create a generic report template. Do not run `twitter-digest` scripts or local browser/API collectors from this skill. + +## MCP Digest Workflow + +When X MCP tools are available, collect data directly through MCP: + +- Resolve the authenticated account with `get_users_me` or the closest available current-user tool. +- Collect home timeline / followed-account activity with `get_users_timeline` or the closest available timeline tool. +- Collect direct mentions with `get_users_mentions`. +- Collect the user's own recent posts with `get_users_posts`. +- Use search tools such as `search_posts_all` only for explicit keywords, extra mention searches, or user-requested topics. +- Use trends or news tools only when available and relevant. +- Collect DM/X Chat data only if the current X MCP tool list exposes a DM/chat capability. If it does not, clearly report that DMs were not collected through MCP; do not claim there are no private messages. + +If X MCP tools are not visible in the current session: + +- Say that the MCP server may be installed but the current agent session cannot see its tools yet. +- Tell the user to open a new Codex or Claude Code session after installation. +- If tools are still missing, troubleshoot registration with this skill. + +Write the final response in Chinese by default for `X日报` requests. Use this structure: + +- 今日概览: 3-6 bullets with the highest-signal changes. +- 需要处理: direct asks, risks, reply opportunities, urgent DMs if collected. +- 时间线热点: grouped by topic with why it matters. +- 我的账号动态: notable own posts or engagement. +- 数据缺口: missing MCP tools, auth/tier errors, unavailable DMs, or rate limits. +- 建议动作: concise reply/follow-up suggestions. Do not post or send anything without explicit approval. + ## Install And Register From the repository `skills/` directory: @@ -22,13 +51,13 @@ From the repository `skills/` directory: For a one-line Codex install from this beta tag: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.7/twitter-mcp/install.sh | X_MCP_REGISTER_CODEX=1 X_MCP_REGISTER_CLAUDE=0 sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.8/twitter-mcp/install.sh | X_MCP_REGISTER_CODEX=1 X_MCP_REGISTER_CLAUDE=0 sh ``` For a one-line Claude Code install from this beta tag: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.7/twitter-mcp/install.sh | X_MCP_REGISTER_CODEX=0 X_MCP_REGISTER_CLAUDE=1 sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.8/twitter-mcp/install.sh | X_MCP_REGISTER_CODEX=0 X_MCP_REGISTER_CLAUDE=1 sh ``` The installer: @@ -96,6 +125,6 @@ If an X MCP endpoint returns an auth, subscription, tier, or scope error, report ## Boundaries -- Use `twitter-mcp` for installing, authorizing, registering, and troubleshooting X MCP. -- Use a separate data or digest skill, such as `twitter-digest`, for analyzing X/Twitter content after MCP tools are visible. +- Use `twitter-mcp` for installing, authorizing, registering, troubleshooting X MCP, and generating MCP-sourced X/Twitter digests. +- Do not use local API tokens, local browser sessions, cookies, screenshots, or `twitter-digest` run outputs from this skill. - Do not install or modify unrelated skills from this skill. diff --git a/twitter-mcp/agents/openai.yaml b/twitter-mcp/agents/openai.yaml index cd4786b..4580e72 100644 --- a/twitter-mcp/agents/openai.yaml +++ b/twitter-mcp/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "X/Twitter MCP" - short_description: "Install and register the official X MCP server" - default_prompt: "Use $twitter-mcp to install and register X MCP for Codex or Claude Code." + short_description: "Install X MCP and generate MCP-based digests" + default_prompt: "Use $twitter-mcp to generate my X daily digest from X MCP tools, or install and register X MCP if tools are missing." diff --git a/twitter-mcp/install.sh b/twitter-mcp/install.sh index 15b494d..dd9a913 100755 --- a/twitter-mcp/install.sh +++ b/twitter-mcp/install.sh @@ -1,7 +1,7 @@ #!/bin/sh set -eu -TAG="${X_MCP_INSTALL_TAG:-v1.5.11-beta.7}" +TAG="${X_MCP_INSTALL_TAG:-v1.5.11-beta.8}" BASE_URL="${X_MCP_INSTALL_BASE_URL:-https://raw.githubusercontent.com/BofAI/skills/${TAG}/twitter-mcp}" REGISTER_CODEX="${X_MCP_REGISTER_CODEX:-1}" REGISTER_CLAUDE="${X_MCP_REGISTER_CLAUDE:-auto}" From 3218b85b41b28bf9097d27583db6d7a44cd57bb0 Mon Sep 17 00:00:00 2001 From: bobo Date: Wed, 1 Jul 2026 15:55:08 +0800 Subject: [PATCH 14/34] Use absolute xurl path for X MCP registration --- twitter-digest/README.md | 4 +- twitter-mcp/SKILL.md | 10 ++--- twitter-mcp/install.sh | 2 +- twitter-mcp/scripts/install_xmcp.sh | 67 +++++++++++++++++++++-------- 4 files changed, 57 insertions(+), 26 deletions(-) diff --git a/twitter-digest/README.md b/twitter-digest/README.md index 3e5c675..1ae3c9b 100644 --- a/twitter-digest/README.md +++ b/twitter-digest/README.md @@ -15,7 +15,7 @@ python3 twitter-digest/scripts/install.py To ask Codex to install this skill for itself, paste this into Codex: ```bash -TMPDIR="$(mktemp -d)" && git clone --depth 1 --branch v1.5.11-beta.8 https://github.com/BofAI/skills.git "$TMPDIR/skills" && python3 "$TMPDIR/skills/twitter-digest/scripts/install.py" --client codex +TMPDIR="$(mktemp -d)" && git clone --depth 1 --branch v1.5.11-beta.9 https://github.com/BofAI/skills.git "$TMPDIR/skills" && python3 "$TMPDIR/skills/twitter-digest/scripts/install.py" --client codex ``` Or use the natural-language prompt: @@ -33,7 +33,7 @@ git clone git@github.com:BofAI/skills.git bofai-skills \ To ask Claude Code to install this skill for itself, paste this into Claude Code: ```bash -TMPDIR="$(mktemp -d)" && git clone --depth 1 --branch v1.5.11-beta.8 https://github.com/BofAI/skills.git "$TMPDIR/skills" && python3 "$TMPDIR/skills/twitter-digest/scripts/install.py" --client claude --allow-claude-commands --allow-claude-state-read +TMPDIR="$(mktemp -d)" && git clone --depth 1 --branch v1.5.11-beta.9 https://github.com/BofAI/skills.git "$TMPDIR/skills" && python3 "$TMPDIR/skills/twitter-digest/scripts/install.py" --client claude --allow-claude-commands --allow-claude-state-read ``` Or use the natural-language prompt: diff --git a/twitter-mcp/SKILL.md b/twitter-mcp/SKILL.md index bd78450..51964ef 100644 --- a/twitter-mcp/SKILL.md +++ b/twitter-mcp/SKILL.md @@ -51,13 +51,13 @@ From the repository `skills/` directory: For a one-line Codex install from this beta tag: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.8/twitter-mcp/install.sh | X_MCP_REGISTER_CODEX=1 X_MCP_REGISTER_CLAUDE=0 sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.9/twitter-mcp/install.sh | X_MCP_REGISTER_CODEX=1 X_MCP_REGISTER_CLAUDE=0 sh ``` For a one-line Claude Code install from this beta tag: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.8/twitter-mcp/install.sh | X_MCP_REGISTER_CODEX=0 X_MCP_REGISTER_CLAUDE=1 sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.9/twitter-mcp/install.sh | X_MCP_REGISTER_CODEX=0 X_MCP_REGISTER_CLAUDE=1 sh ``` The installer: @@ -66,7 +66,7 @@ The installer: - Requires Node.js and npm. - Installs `@xdevplatform/xurl` globally with npm. - Opens the X OAuth2 authorization flow. -- Registers an MCP server named `xapi` for Codex by editing `~/.codex/config.toml`. +- Registers an MCP server named `xapi` for Codex by editing `~/.codex/config.toml` with the absolute `xurl` command path. - Registers the same MCP server for Claude Code when the `claude` CLI is available. When launched by Codex or Claude Code on macOS, the installer opens a real Terminal window for OAuth2 Client ID / Secret input and browser authorization. Do not ask the user to paste OAuth credentials into chat. @@ -103,7 +103,7 @@ Codex config shape: ```toml [mcp_servers.xapi] -command = "xurl" +command = "/absolute/path/to/xurl" args = ["--app", "xmcp", "mcp", "https://api.x.com/mcp"] ``` @@ -119,7 +119,7 @@ After installation: 1. Start a new Codex or Claude Code session. 2. Check whether X MCP tools are visible under the `xapi` server. -3. If tools are missing, verify `xurl` is on `PATH`, the MCP config contains `xapi`, and the OAuth app name matches the configured `--app` value. +3. If tools are missing, verify the MCP config contains `xapi`, `command` points to an executable `xurl` path, and the OAuth app name matches the configured `--app` value. If an X MCP endpoint returns an auth, subscription, tier, or scope error, report the exact failing capability as a setup or account limitation. Do not infer that the requested X data does not exist. diff --git a/twitter-mcp/install.sh b/twitter-mcp/install.sh index dd9a913..96befbe 100755 --- a/twitter-mcp/install.sh +++ b/twitter-mcp/install.sh @@ -1,7 +1,7 @@ #!/bin/sh set -eu -TAG="${X_MCP_INSTALL_TAG:-v1.5.11-beta.8}" +TAG="${X_MCP_INSTALL_TAG:-v1.5.11-beta.9}" BASE_URL="${X_MCP_INSTALL_BASE_URL:-https://raw.githubusercontent.com/BofAI/skills/${TAG}/twitter-mcp}" REGISTER_CODEX="${X_MCP_REGISTER_CODEX:-1}" REGISTER_CLAUDE="${X_MCP_REGISTER_CLAUDE:-auto}" diff --git a/twitter-mcp/scripts/install_xmcp.sh b/twitter-mcp/scripts/install_xmcp.sh index 7c8fe0c..9893de6 100755 --- a/twitter-mcp/scripts/install_xmcp.sh +++ b/twitter-mcp/scripts/install_xmcp.sh @@ -11,6 +11,7 @@ REGISTER_CODEX="${X_MCP_REGISTER_CODEX:-1}" REGISTER_CLAUDE="${X_MCP_REGISTER_CLAUDE:-auto}" CODEX_CONFIG="${CODEX_CONFIG:-$HOME/.codex/config.toml}" OPEN_TERMINAL="${X_MCP_OPEN_TERMINAL:-auto}" +XURL_COMMAND="${X_MCP_XURL_COMMAND:-}" info() { printf '==> %s\n' "$1" @@ -40,6 +41,19 @@ toml_quote() { printf '"%s"' "$(printf '%s' "$value" | sed 's/\\/\\\\/g; s/"/\\"/g')" } +resolve_command_path() { + local command_name="$1" + local resolved="" + resolved="$(command -v "$command_name" 2>/dev/null || true)" + if [ -z "$resolved" ]; then + return 1 + fi + case "$resolved" in + /*) printf '%s' "$resolved" ;; + *) printf '%s/%s' "$(cd -P "$(dirname "$resolved")" >/dev/null 2>&1 && pwd)" "$(basename "$resolved")" ;; + esac +} + script_path() { local source="${BASH_SOURCE[0]}" local dir="" @@ -137,19 +151,31 @@ register_codex() { local config_path="$1" local server_name="$2" local app_name="$3" + local xurl_command="$4" local marker="[mcp_servers.${server_name}]" + local subtable_prefix="[mcp_servers.${server_name}." + local tmp_config="" mkdir -p "$(dirname "$config_path")" touch "$config_path" - - if grep -Fqx "$marker" "$config_path"; then - info "Codex MCP server '${server_name}' already exists in ${config_path}; leaving it unchanged." - return - fi + tmp_config="${config_path}.tmp.$$" + + awk -v marker="$marker" -v subtable_prefix="$subtable_prefix" ' + /^\[/ { + if ($0 == marker || index($0, subtable_prefix) == 1) { + skip = 1 + next + } + skip = 0 + } + skip { next } + { print } + ' "$config_path" > "$tmp_config" + mv "$tmp_config" "$config_path" { printf '\n[mcp_servers.%s]\n' "$server_name" - printf 'command = "xurl"\n' + printf 'command = %s\n' "$(toml_quote "$xurl_command")" printf 'args = [%s, %s, %s, %s]\n' \ "$(toml_quote '--app')" \ "$(toml_quote "$app_name")" \ @@ -157,12 +183,13 @@ register_codex() { "$(toml_quote 'https://api.x.com/mcp')" } >> "$config_path" - info "Registered Codex MCP server '${server_name}' in ${config_path}" + info "Registered Codex MCP server '${server_name}' in ${config_path} with ${xurl_command}" } register_claude() { local server_name="$1" local app_name="$2" + local xurl_command="$3" if ! command_exists claude; then info "Claude Code CLI not found; skipped Claude MCP registration." @@ -175,7 +202,7 @@ register_claude() { fi info "Registering Claude Code MCP server '${server_name}'" - claude mcp add "$server_name" -- xurl --app "$app_name" mcp https://api.x.com/mcp + claude mcp add "$server_name" -- "$xurl_command" --app "$app_name" mcp https://api.x.com/mcp } if should_open_terminal; then @@ -193,11 +220,15 @@ fi info "Installing ${INSTALL_SPEC}" npm install -g "${INSTALL_SPEC}" -if ! command_exists xurl; then +if [ -z "$XURL_COMMAND" ]; then + XURL_COMMAND="$(resolve_command_path xurl || true)" +fi + +if [ -z "$XURL_COMMAND" ] || [ ! -x "$XURL_COMMAND" ]; then fail "xurl was installed by npm, but the xurl command is not on PATH." fi -info "Installed $(xurl --version 2>/dev/null || printf 'xurl')" +info "Installed $("$XURL_COMMAND" --version 2>/dev/null || printf 'xurl') at ${XURL_COMMAND}" CLIENT_ID="${X_MCP_CLIENT_ID:-${X_OAUTH_CLIENT_ID:-}}" CLIENT_SECRET="${X_MCP_CLIENT_SECRET:-${X_OAUTH_CLIENT_SECRET:-}}" @@ -223,29 +254,29 @@ if [ -t 0 ]; then fi info "Registering xurl OAuth app '${APP_NAME}'" -app_cmd=(xurl auth apps add "$APP_NAME" --client-id "$CLIENT_ID" --redirect-uri "$REDIRECT_URI") +app_cmd=("$XURL_COMMAND" auth apps add "$APP_NAME" --client-id "$CLIENT_ID" --redirect-uri "$REDIRECT_URI") if [ -n "$CLIENT_SECRET" ]; then app_cmd+=(--client-secret "$CLIENT_SECRET") fi "${app_cmd[@]}" info "Opening X OAuth authorization flow" -xurl auth oauth2 --app "$APP_NAME" +"$XURL_COMMAND" auth oauth2 --app "$APP_NAME" info "Setting '${APP_NAME}' as the default xurl auth app" -xurl auth default "$APP_NAME" +"$XURL_COMMAND" auth default "$APP_NAME" if [ "$REGISTER_CODEX" = "1" ] || [ "$REGISTER_CODEX" = "true" ]; then - register_codex "$CODEX_CONFIG" "$SERVER_NAME" "$APP_NAME" + register_codex "$CODEX_CONFIG" "$SERVER_NAME" "$APP_NAME" "$XURL_COMMAND" else info "Skipped Codex MCP registration." fi if [ "$REGISTER_CLAUDE" = "1" ] || [ "$REGISTER_CLAUDE" = "true" ]; then - register_claude "$SERVER_NAME" "$APP_NAME" + register_claude "$SERVER_NAME" "$APP_NAME" "$XURL_COMMAND" elif [ "$REGISTER_CLAUDE" = "auto" ]; then if command_exists claude; then - register_claude "$SERVER_NAME" "$APP_NAME" + register_claude "$SERVER_NAME" "$APP_NAME" "$XURL_COMMAND" else info "Claude Code CLI not found; skipped Claude MCP registration." fi @@ -255,8 +286,8 @@ fi info "Done" printf '\nX MCP command for MCP clients:\n' -printf ' xurl --app %s mcp https://api.x.com/mcp\n' "$APP_NAME" +printf ' %s --app %s mcp https://api.x.com/mcp\n' "$XURL_COMMAND" "$APP_NAME" printf '\nCodex config example:\n' printf '[mcp_servers.%s]\n' "$SERVER_NAME" -printf 'command = "xurl"\n' +printf 'command = "%s"\n' "$XURL_COMMAND" printf 'args = ["--app", "%s", "mcp", "https://api.x.com/mcp"]\n' "$APP_NAME" From 3a2165d5ba6c75f7c5e569a52daa3a35afecce2e Mon Sep 17 00:00:00 2001 From: bobo Date: Wed, 1 Jul 2026 16:12:40 +0800 Subject: [PATCH 15/34] Use search fallbacks for X MCP digest data --- twitter-digest/README.md | 4 ++-- twitter-mcp/SKILL.md | 10 +++++----- twitter-mcp/install.sh | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/twitter-digest/README.md b/twitter-digest/README.md index 1ae3c9b..eb96eca 100644 --- a/twitter-digest/README.md +++ b/twitter-digest/README.md @@ -15,7 +15,7 @@ python3 twitter-digest/scripts/install.py To ask Codex to install this skill for itself, paste this into Codex: ```bash -TMPDIR="$(mktemp -d)" && git clone --depth 1 --branch v1.5.11-beta.9 https://github.com/BofAI/skills.git "$TMPDIR/skills" && python3 "$TMPDIR/skills/twitter-digest/scripts/install.py" --client codex +TMPDIR="$(mktemp -d)" && git clone --depth 1 --branch v1.5.11-beta.10 https://github.com/BofAI/skills.git "$TMPDIR/skills" && python3 "$TMPDIR/skills/twitter-digest/scripts/install.py" --client codex ``` Or use the natural-language prompt: @@ -33,7 +33,7 @@ git clone git@github.com:BofAI/skills.git bofai-skills \ To ask Claude Code to install this skill for itself, paste this into Claude Code: ```bash -TMPDIR="$(mktemp -d)" && git clone --depth 1 --branch v1.5.11-beta.9 https://github.com/BofAI/skills.git "$TMPDIR/skills" && python3 "$TMPDIR/skills/twitter-digest/scripts/install.py" --client claude --allow-claude-commands --allow-claude-state-read +TMPDIR="$(mktemp -d)" && git clone --depth 1 --branch v1.5.11-beta.10 https://github.com/BofAI/skills.git "$TMPDIR/skills" && python3 "$TMPDIR/skills/twitter-digest/scripts/install.py" --client claude --allow-claude-commands --allow-claude-state-read ``` Or use the natural-language prompt: diff --git a/twitter-mcp/SKILL.md b/twitter-mcp/SKILL.md index 51964ef..7a5f0ab 100644 --- a/twitter-mcp/SKILL.md +++ b/twitter-mcp/SKILL.md @@ -19,9 +19,9 @@ When X MCP tools are available, collect data directly through MCP: - Resolve the authenticated account with `get_users_me` or the closest available current-user tool. - Collect home timeline / followed-account activity with `get_users_timeline` or the closest available timeline tool. -- Collect direct mentions with `get_users_mentions`. -- Collect the user's own recent posts with `get_users_posts`. -- Use search tools such as `search_posts_all` only for explicit keywords, extra mention searches, or user-requested topics. +- Collect direct mentions with `get_users_mentions` when available. If no mentions tool is exposed, use available search tools with mention queries for the authenticated handle, such as `@handle`, `to:handle`, or equivalent X search syntax. +- Collect the user's own recent posts with `get_users_posts` when available. If no own-posts tool is exposed, use available search tools with `from:handle` for the authenticated handle. +- Use search tools such as `search_posts_all` for explicit keywords, extra mention searches, user-requested topics, and fallbacks when dedicated mentions or own-post tools are not exposed. - Use trends or news tools only when available and relevant. - Collect DM/X Chat data only if the current X MCP tool list exposes a DM/chat capability. If it does not, clearly report that DMs were not collected through MCP; do not claim there are no private messages. @@ -51,13 +51,13 @@ From the repository `skills/` directory: For a one-line Codex install from this beta tag: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.9/twitter-mcp/install.sh | X_MCP_REGISTER_CODEX=1 X_MCP_REGISTER_CLAUDE=0 sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.10/twitter-mcp/install.sh | X_MCP_REGISTER_CODEX=1 X_MCP_REGISTER_CLAUDE=0 sh ``` For a one-line Claude Code install from this beta tag: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.9/twitter-mcp/install.sh | X_MCP_REGISTER_CODEX=0 X_MCP_REGISTER_CLAUDE=1 sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.10/twitter-mcp/install.sh | X_MCP_REGISTER_CODEX=0 X_MCP_REGISTER_CLAUDE=1 sh ``` The installer: diff --git a/twitter-mcp/install.sh b/twitter-mcp/install.sh index 96befbe..0850eca 100755 --- a/twitter-mcp/install.sh +++ b/twitter-mcp/install.sh @@ -1,7 +1,7 @@ #!/bin/sh set -eu -TAG="${X_MCP_INSTALL_TAG:-v1.5.11-beta.9}" +TAG="${X_MCP_INSTALL_TAG:-v1.5.11-beta.10}" BASE_URL="${X_MCP_INSTALL_BASE_URL:-https://raw.githubusercontent.com/BofAI/skills/${TAG}/twitter-mcp}" REGISTER_CODEX="${X_MCP_REGISTER_CODEX:-1}" REGISTER_CLAUDE="${X_MCP_REGISTER_CLAUDE:-auto}" From 0cc27044fd1284cc915b8889a38be9bd18d79371 Mon Sep 17 00:00:00 2001 From: bobo Date: Wed, 1 Jul 2026 16:27:25 +0800 Subject: [PATCH 16/34] Use xurl CLI as Twitter digest data source --- twitter-digest/README.md | 4 +- twitter-mcp/SKILL.md | 63 +++---- twitter-mcp/agents/openai.yaml | 6 +- twitter-mcp/install.sh | 9 +- twitter-mcp/scripts/install_xmcp.sh | 10 +- twitter-mcp/scripts/xurl_daily_digest.py | 202 +++++++++++++++++++++++ 6 files changed, 253 insertions(+), 41 deletions(-) create mode 100644 twitter-mcp/scripts/xurl_daily_digest.py diff --git a/twitter-digest/README.md b/twitter-digest/README.md index eb96eca..3d7dd26 100644 --- a/twitter-digest/README.md +++ b/twitter-digest/README.md @@ -15,7 +15,7 @@ python3 twitter-digest/scripts/install.py To ask Codex to install this skill for itself, paste this into Codex: ```bash -TMPDIR="$(mktemp -d)" && git clone --depth 1 --branch v1.5.11-beta.10 https://github.com/BofAI/skills.git "$TMPDIR/skills" && python3 "$TMPDIR/skills/twitter-digest/scripts/install.py" --client codex +TMPDIR="$(mktemp -d)" && git clone --depth 1 --branch v1.5.11-beta.11 https://github.com/BofAI/skills.git "$TMPDIR/skills" && python3 "$TMPDIR/skills/twitter-digest/scripts/install.py" --client codex ``` Or use the natural-language prompt: @@ -33,7 +33,7 @@ git clone git@github.com:BofAI/skills.git bofai-skills \ To ask Claude Code to install this skill for itself, paste this into Claude Code: ```bash -TMPDIR="$(mktemp -d)" && git clone --depth 1 --branch v1.5.11-beta.10 https://github.com/BofAI/skills.git "$TMPDIR/skills" && python3 "$TMPDIR/skills/twitter-digest/scripts/install.py" --client claude --allow-claude-commands --allow-claude-state-read +TMPDIR="$(mktemp -d)" && git clone --depth 1 --branch v1.5.11-beta.11 https://github.com/BofAI/skills.git "$TMPDIR/skills" && python3 "$TMPDIR/skills/twitter-digest/scripts/install.py" --client claude --allow-claude-commands --allow-claude-state-read ``` Or use the natural-language prompt: diff --git a/twitter-mcp/SKILL.md b/twitter-mcp/SKILL.md index 7a5f0ab..404c05b 100644 --- a/twitter-mcp/SKILL.md +++ b/twitter-mcp/SKILL.md @@ -1,35 +1,41 @@ --- name: twitter-mcp -description: Use when the user wants to install, authorize, register, verify, or troubleshoot the official X/Twitter MCP server for Codex, Claude Code, or another MCP-capable agent. Also use when the user asks "生成X日报", "X日报", "推特日报", or "Twitter digest" and wants the digest generated from X MCP tools rather than local API/browser collectors. +description: Use when the user wants to install or authorize xurl for X/Twitter, generate "生成X日报", "X日报", "推特日报", or "Twitter digest" from the local xurl CLI, or optionally register/troubleshoot the hosted X MCP bridge. --- -# X/Twitter MCP +# X/Twitter xurl Digest ## Overview -Use this skill to install and register the official X MCP server through `@xdevplatform/xurl`, and to generate X/Twitter daily digests from X MCP tool results when those tools are visible in the current agent session. This skill does not use local API collectors, local browser collectors, or `twitter-digest` scripts. +Use this skill to install and authorize `@xdevplatform/xurl`, and to generate X/Twitter daily digests from the local `xurl` CLI. The primary digest data source is `xurl` CLI output, not the hosted X MCP tool list. This skill does not use local API collectors, local browser collectors, or `twitter-digest` scripts. -After successful setup, the MCP server is registered as `xapi` by default. The user usually needs to start a new Codex or Claude Code session before newly registered MCP tools become visible. +The hosted X MCP bridge can still be registered as `xapi` when explicitly requested, but daily digest generation should not depend on MCP tools being visible. `xurl` CLI exposes direct digest-relevant commands such as `whoami`, `timeline`, `mentions`, `posts`, `dms`, and `search`. -If the user asks to generate an X/Twitter daily digest, says `生成X日报`, `X日报`, `推特日报`, or asks for a Twitter digest, use available `xapi` / X MCP tools directly and write the digest from those tool results. Do not create a generic report template. Do not run `twitter-digest` scripts or local browser/API collectors from this skill. +If the user asks to generate an X/Twitter daily digest, says `生成X日报`, `X日报`, `推特日报`, or asks for a Twitter digest, run the installed `scripts/xurl_daily_digest.py` helper and write the digest from its generated context. Do not create a generic report template. Do not run `twitter-digest` scripts or local browser/API collectors from this skill. -## MCP Digest Workflow +## xurl Digest Workflow -When X MCP tools are available, collect data directly through MCP: +For daily digest requests, run: -- Resolve the authenticated account with `get_users_me` or the closest available current-user tool. -- Collect home timeline / followed-account activity with `get_users_timeline` or the closest available timeline tool. -- Collect direct mentions with `get_users_mentions` when available. If no mentions tool is exposed, use available search tools with mention queries for the authenticated handle, such as `@handle`, `to:handle`, or equivalent X search syntax. -- Collect the user's own recent posts with `get_users_posts` when available. If no own-posts tool is exposed, use available search tools with `from:handle` for the authenticated handle. -- Use search tools such as `search_posts_all` for explicit keywords, extra mention searches, user-requested topics, and fallbacks when dedicated mentions or own-post tools are not exposed. -- Use trends or news tools only when available and relevant. -- Collect DM/X Chat data only if the current X MCP tool list exposes a DM/chat capability. If it does not, clearly report that DMs were not collected through MCP; do not claim there are no private messages. +```bash +python3 ~/.codex/skills/twitter-mcp/scripts/xurl_daily_digest.py --max-results 100 +``` + +Use the matching installed path for the current client: + +- Codex: `~/.codex/skills/twitter-mcp/scripts/xurl_daily_digest.py` +- Claude Code: `~/.claude/skills/twitter-mcp/scripts/xurl_daily_digest.py` + +The helper collects: -If X MCP tools are not visible in the current session: +- `xurl whoami` +- `xurl timeline -n 100` +- `xurl mentions -n 100` +- `xurl posts -n 100` when the handle can be detected or is provided with `--handle` +- `xurl search "from:"`, `xurl search "@"`, and `xurl search "to:"` +- `xurl dms -n 100` unless `--no-include-dms` is passed -- Say that the MCP server may be installed but the current agent session cannot see its tools yet. -- Tell the user to open a new Codex or Claude Code session after installation. -- If tools are still missing, troubleshoot registration with this skill. +After it runs, read `twitter-mcp/.state/xurl-run/digest-context.md` and write the digest from that context only. If a specific `xurl` command fails, report that command under data gaps and continue with the successful command outputs. If `xurl` is missing or unauthenticated, help the user install or authorize it with this skill. Write the final response in Chinese by default for `X日报` requests. Use this structure: @@ -37,7 +43,7 @@ Write the final response in Chinese by default for `X日报` requests. Use this - 需要处理: direct asks, risks, reply opportunities, urgent DMs if collected. - 时间线热点: grouped by topic with why it matters. - 我的账号动态: notable own posts or engagement. -- 数据缺口: missing MCP tools, auth/tier errors, unavailable DMs, or rate limits. +- 数据缺口: failed `xurl` commands, auth/tier errors, unavailable DMs, or rate limits. - 建议动作: concise reply/follow-up suggestions. Do not post or send anything without explicit approval. ## Install And Register @@ -51,13 +57,13 @@ From the repository `skills/` directory: For a one-line Codex install from this beta tag: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.10/twitter-mcp/install.sh | X_MCP_REGISTER_CODEX=1 X_MCP_REGISTER_CLAUDE=0 sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.11/twitter-mcp/install.sh | X_MCP_REGISTER_CODEX=1 X_MCP_REGISTER_CLAUDE=0 sh ``` For a one-line Claude Code install from this beta tag: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.10/twitter-mcp/install.sh | X_MCP_REGISTER_CODEX=0 X_MCP_REGISTER_CLAUDE=1 sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.11/twitter-mcp/install.sh | X_MCP_REGISTER_CODEX=0 X_MCP_REGISTER_CLAUDE=1 sh ``` The installer: @@ -66,8 +72,7 @@ The installer: - Requires Node.js and npm. - Installs `@xdevplatform/xurl` globally with npm. - Opens the X OAuth2 authorization flow. -- Registers an MCP server named `xapi` for Codex by editing `~/.codex/config.toml` with the absolute `xurl` command path. -- Registers the same MCP server for Claude Code when the `claude` CLI is available. +- Does not register the hosted X MCP bridge by default. To also register MCP, set `X_MCP_REGISTER_CODEX_MCP=1` or `X_MCP_REGISTER_CLAUDE_MCP=1`. When launched by Codex or Claude Code on macOS, the installer opens a real Terminal window for OAuth2 Client ID / Secret input and browser authorization. Do not ask the user to paste OAuth credentials into chat. @@ -83,6 +88,8 @@ X_MCP_REDIRECT_URI=http://localhost:8080/callback X_MCP_SERVER_NAME=xapi X_MCP_REGISTER_CODEX=1 X_MCP_REGISTER_CLAUDE=auto +X_MCP_REGISTER_CODEX_MCP=0 +X_MCP_REGISTER_CLAUDE_MCP=0 CODEX_CONFIG=~/.codex/config.toml X_MCP_OPEN_TERMINAL=auto X_MCP_CLIENT_ID= @@ -93,7 +100,7 @@ Use `X_MCP_CLIENT_ID` and `X_MCP_CLIENT_SECRET` only when the user explicitly pr ## Manual MCP Command -For MCP clients that need the command directly: +For optional hosted MCP clients that need the bridge command directly: ```bash xurl --app xmcp mcp https://api.x.com/mcp @@ -117,14 +124,14 @@ claude mcp add xapi -- xurl --app xmcp mcp https://api.x.com/mcp After installation: -1. Start a new Codex or Claude Code session. -2. Check whether X MCP tools are visible under the `xapi` server. -3. If tools are missing, verify the MCP config contains `xapi`, `command` points to an executable `xurl` path, and the OAuth app name matches the configured `--app` value. +1. Run `xurl whoami` to verify the local CLI is authorized. +2. Run `python3 ~/.codex/skills/twitter-mcp/scripts/xurl_daily_digest.py --max-results 10` to verify digest collection. +3. If optional MCP registration was enabled, start a new Codex or Claude Code session and check whether X MCP tools are visible under the `xapi` server. If an X MCP endpoint returns an auth, subscription, tier, or scope error, report the exact failing capability as a setup or account limitation. Do not infer that the requested X data does not exist. ## Boundaries -- Use `twitter-mcp` for installing, authorizing, registering, troubleshooting X MCP, and generating MCP-sourced X/Twitter digests. +- Use `twitter-mcp` for installing and authorizing `xurl`, generating `xurl` CLI-sourced X/Twitter digests, and optionally registering/troubleshooting the hosted X MCP bridge. - Do not use local API tokens, local browser sessions, cookies, screenshots, or `twitter-digest` run outputs from this skill. - Do not install or modify unrelated skills from this skill. diff --git a/twitter-mcp/agents/openai.yaml b/twitter-mcp/agents/openai.yaml index 4580e72..2edab43 100644 --- a/twitter-mcp/agents/openai.yaml +++ b/twitter-mcp/agents/openai.yaml @@ -1,4 +1,4 @@ interface: - display_name: "X/Twitter MCP" - short_description: "Install X MCP and generate MCP-based digests" - default_prompt: "Use $twitter-mcp to generate my X daily digest from X MCP tools, or install and register X MCP if tools are missing." + display_name: "X/Twitter xurl" + short_description: "Install xurl and generate X digests" + default_prompt: "Use $twitter-mcp to generate my X daily digest from the local xurl CLI." diff --git a/twitter-mcp/install.sh b/twitter-mcp/install.sh index 0850eca..8459387 100755 --- a/twitter-mcp/install.sh +++ b/twitter-mcp/install.sh @@ -1,10 +1,12 @@ #!/bin/sh set -eu -TAG="${X_MCP_INSTALL_TAG:-v1.5.11-beta.10}" +TAG="${X_MCP_INSTALL_TAG:-v1.5.11-beta.11}" BASE_URL="${X_MCP_INSTALL_BASE_URL:-https://raw.githubusercontent.com/BofAI/skills/${TAG}/twitter-mcp}" REGISTER_CODEX="${X_MCP_REGISTER_CODEX:-1}" REGISTER_CLAUDE="${X_MCP_REGISTER_CLAUDE:-auto}" +REGISTER_CODEX_MCP="${X_MCP_REGISTER_CODEX_MCP:-0}" +REGISTER_CLAUDE_MCP="${X_MCP_REGISTER_CLAUDE_MCP:-0}" info() { printf '==> %s\n' "$1" @@ -82,7 +84,8 @@ install_skill_copy() { download_file "${BASE_URL%/}/install.sh" "$staging/install.sh" download_file "${BASE_URL%/}/agents/openai.yaml" "$staging/agents/openai.yaml" download_file "${BASE_URL%/}/scripts/install_xmcp.sh" "$staging/scripts/install_xmcp.sh" - chmod 700 "$staging/install.sh" "$staging/scripts/install_xmcp.sh" + download_file "${BASE_URL%/}/scripts/xurl_daily_digest.py" "$staging/scripts/xurl_daily_digest.py" + chmod 700 "$staging/install.sh" "$staging/scripts/install_xmcp.sh" "$staging/scripts/xurl_daily_digest.py" mkdir -p "$skills_dir" backup_existing_skill "$target" "$skills_dir" mv "$staging" "$target" @@ -100,4 +103,4 @@ fi download_file "$INSTALLER_URL" "$INSTALLER" chmod 700 "$INSTALLER" -exec /bin/bash "$INSTALLER" "$@" +X_MCP_REGISTER_CODEX="$REGISTER_CODEX_MCP" X_MCP_REGISTER_CLAUDE="$REGISTER_CLAUDE_MCP" exec /bin/bash "$INSTALLER" "$@" diff --git a/twitter-mcp/scripts/install_xmcp.sh b/twitter-mcp/scripts/install_xmcp.sh index 9893de6..09fa610 100755 --- a/twitter-mcp/scripts/install_xmcp.sh +++ b/twitter-mcp/scripts/install_xmcp.sh @@ -7,8 +7,8 @@ INSTALL_SPEC="${PACKAGE}@${VERSION}" APP_NAME="${X_MCP_APP_NAME:-xmcp}" REDIRECT_URI="${X_MCP_REDIRECT_URI:-http://localhost:8080/callback}" SERVER_NAME="${X_MCP_SERVER_NAME:-xapi}" -REGISTER_CODEX="${X_MCP_REGISTER_CODEX:-1}" -REGISTER_CLAUDE="${X_MCP_REGISTER_CLAUDE:-auto}" +REGISTER_CODEX="${X_MCP_REGISTER_CODEX:-0}" +REGISTER_CLAUDE="${X_MCP_REGISTER_CLAUDE:-0}" CODEX_CONFIG="${CODEX_CONFIG:-$HOME/.codex/config.toml}" OPEN_TERMINAL="${X_MCP_OPEN_TERMINAL:-auto}" XURL_COMMAND="${X_MCP_XURL_COMMAND:-}" @@ -115,7 +115,7 @@ tell application "Terminal" do script "$(applescript_quote "$command_text")" end tell OSA - info "Opened Terminal for X MCP OAuth2 setup. Continue there." + info "Opened Terminal for xurl OAuth2 setup. Continue there." exit 0 } @@ -285,9 +285,9 @@ else fi info "Done" -printf '\nX MCP command for MCP clients:\n' +printf '\nOptional hosted X MCP bridge command:\n' printf ' %s --app %s mcp https://api.x.com/mcp\n' "$XURL_COMMAND" "$APP_NAME" -printf '\nCodex config example:\n' +printf '\nOptional Codex MCP config example:\n' printf '[mcp_servers.%s]\n' "$SERVER_NAME" printf 'command = "%s"\n' "$XURL_COMMAND" printf 'args = ["--app", "%s", "mcp", "https://api.x.com/mcp"]\n' "$APP_NAME" diff --git a/twitter-mcp/scripts/xurl_daily_digest.py b/twitter-mcp/scripts/xurl_daily_digest.py new file mode 100644 index 0000000..d3042db --- /dev/null +++ b/twitter-mcp/scripts/xurl_daily_digest.py @@ -0,0 +1,202 @@ +#!/usr/bin/env python3 +"""Collect X daily digest inputs through the local xurl CLI.""" + +from __future__ import annotations + +import argparse +import json +import os +import re +import shutil +import subprocess +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +SKILL_DIR = Path(__file__).resolve().parents[1] +DEFAULT_OUT_DIR = SKILL_DIR / ".state" / "xurl-run" + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Collect X digest context through xurl CLI.") + parser.add_argument("--app", default=os.environ.get("X_MCP_APP_NAME") or "", help="xurl app name. Defaults to xurl's configured default app.") + parser.add_argument("--handle", default="", help="Authenticated account handle, without @. Auto-detected from xurl whoami when possible.") + parser.add_argument("--out", default=str(DEFAULT_OUT_DIR), help="Output directory for digest context files.") + parser.add_argument("--max-results", type=int, default=100, help="Maximum items requested per xurl read command.") + parser.add_argument("--xurl-command", default=os.environ.get("XURL_COMMAND") or "xurl", help="xurl executable path or command name.") + parser.add_argument("--include-dms", action=argparse.BooleanOptionalAction, default=True, help="Collect recent DMs through xurl dms.") + parser.add_argument("--timeout", type=int, default=90, help="Timeout per xurl command in seconds.") + return parser.parse_args() + + +def secure_dir(path: Path) -> None: + path.mkdir(parents=True, exist_ok=True) + try: + path.chmod(0o700) + except OSError: + pass + + +def write_private(path: Path, content: str) -> None: + path.write_text(content, encoding="utf-8") + try: + path.chmod(0o600) + except OSError: + pass + + +def command_display(argv: list[str]) -> str: + return " ".join(json.dumps(part) if any(ch.isspace() for ch in part) else part for part in argv) + + +def run_xurl(xurl: str, app: str, args: list[str], timeout: int) -> dict[str, Any]: + argv = [xurl] + if app: + argv.extend(["--app", app]) + argv.extend(args) + try: + completed = subprocess.run(argv, check=False, capture_output=True, text=True, timeout=timeout) + return { + "name": args[0] if args else "xurl", + "argv": argv, + "display": command_display(argv), + "returncode": completed.returncode, + "stdout": completed.stdout, + "stderr": completed.stderr, + } + except FileNotFoundError: + return { + "name": args[0] if args else "xurl", + "argv": argv, + "display": command_display(argv), + "returncode": 127, + "stdout": "", + "stderr": f"xurl command not found: {xurl}\n", + } + except subprocess.TimeoutExpired as exc: + return { + "name": args[0] if args else "xurl", + "argv": argv, + "display": command_display(argv), + "returncode": 124, + "stdout": exc.stdout or "", + "stderr": (exc.stderr or "") + f"\nTimed out after {timeout} seconds.\n", + } + + +def extract_handle(whoami_output: str) -> str: + try: + data = json.loads(whoami_output) + node: Any = data + if isinstance(node, dict) and isinstance(node.get("data"), dict): + node = node["data"] + username = node.get("username") if isinstance(node, dict) else "" + if isinstance(username, str) and username: + return username.lstrip("@") + except json.JSONDecodeError: + pass + patterns = [ + r'"username"\s*:\s*"([A-Za-z0-9_]{1,15})"', + r"(?:username|handle)\s*[:=]\s*@?([A-Za-z0-9_]{1,15})", + r"@([A-Za-z0-9_]{1,15})", + ] + for pattern in patterns: + match = re.search(pattern, whoami_output, flags=re.IGNORECASE) + if match: + return match.group(1).lstrip("@") + return "" + + +def markdown_section(result: dict[str, Any]) -> str: + status = "ok" if result["returncode"] == 0 else f"exit {result['returncode']}" + chunks = [ + f"## {result['name']} ({status})", + "", + f"Command: `{result['display']}`", + "", + "### stdout", + "", + "```text", + result["stdout"].rstrip(), + "```", + ] + if result["stderr"].strip(): + chunks.extend(["", "### stderr", "", "```text", result["stderr"].rstrip(), "```"]) + return "\n".join(chunks) + + +def main() -> int: + args = parse_args() + xurl = shutil.which(args.xurl_command) or args.xurl_command + out_dir = Path(args.out).expanduser().resolve() + secure_dir(out_dir) + + max_results = max(1, min(args.max_results, 100)) + collected: list[dict[str, Any]] = [] + + whoami = run_xurl(xurl, args.app, ["whoami"], args.timeout) + collected.append(whoami) + handle = args.handle.lstrip("@") or extract_handle(whoami.get("stdout") or "") + + read_commands: list[tuple[str, list[str]]] = [ + ("timeline", ["timeline", "-n", str(max_results)]), + ("mentions", ["mentions", "-n", str(max(5, max_results))]), + ] + if handle: + read_commands.extend( + [ + ("posts", ["posts", handle, "-n", str(max(5, max_results))]), + ("search_from", ["search", f"from:{handle}", "-n", str(max(10, max_results))]), + ("search_mentions", ["search", f"@{handle}", "-n", str(max(10, max_results))]), + ("search_to", ["search", f"to:{handle}", "-n", str(max(10, max_results))]), + ] + ) + if args.include_dms: + read_commands.append(("dms", ["dms", "-n", str(max_results)])) + + for name, command_args in read_commands: + result = run_xurl(xurl, args.app, command_args, args.timeout) + result["name"] = name + collected.append(result) + + payload = { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source": "xurl-cli", + "app": args.app or None, + "handle": handle or None, + "xurl": xurl, + "commands": collected, + } + write_private(out_dir / "digest-input.json", json.dumps(payload, ensure_ascii=False, indent=2) + "\n") + + lines = [ + "# X Daily Digest Context", + "", + f"- generated_at: `{payload['generated_at']}`", + "- source: `xurl-cli`", + f"- app: `{args.app or 'xurl default'}`", + f"- handle: `{handle or 'unknown'}`", + "", + "Use this file as the source for the Chinese X daily digest. Treat DM sections as private.", + "", + ] + lines.extend(markdown_section(result) for result in collected) + write_private(out_dir / "digest-context.md", "\n\n".join(lines) + "\n") + + failures = [result for result in collected if result["returncode"] != 0] + print(json.dumps({ + "source": "xurl-cli", + "out_dir": str(out_dir), + "context": str(out_dir / "digest-context.md"), + "json": str(out_dir / "digest-input.json"), + "handle": handle or None, + "commands": len(collected), + "failures": [{"name": item["name"], "returncode": item["returncode"]} for item in failures], + }, ensure_ascii=False, indent=2)) + return 0 if len(failures) < len(collected) else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) From 163e2db021d6d6398ced77ecedfd682c59db421f Mon Sep 17 00:00:00 2001 From: bobo Date: Wed, 1 Jul 2026 16:30:21 +0800 Subject: [PATCH 17/34] Guide xurl digest collection from skill --- twitter-digest/README.md | 4 +- twitter-mcp/SKILL.md | 41 +++-- twitter-mcp/install.sh | 5 +- twitter-mcp/scripts/install_xmcp.sh | 4 +- twitter-mcp/scripts/xurl_daily_digest.py | 202 ----------------------- 5 files changed, 29 insertions(+), 227 deletions(-) delete mode 100644 twitter-mcp/scripts/xurl_daily_digest.py diff --git a/twitter-digest/README.md b/twitter-digest/README.md index 3d7dd26..4b83604 100644 --- a/twitter-digest/README.md +++ b/twitter-digest/README.md @@ -15,7 +15,7 @@ python3 twitter-digest/scripts/install.py To ask Codex to install this skill for itself, paste this into Codex: ```bash -TMPDIR="$(mktemp -d)" && git clone --depth 1 --branch v1.5.11-beta.11 https://github.com/BofAI/skills.git "$TMPDIR/skills" && python3 "$TMPDIR/skills/twitter-digest/scripts/install.py" --client codex +TMPDIR="$(mktemp -d)" && git clone --depth 1 --branch v1.5.11-beta.12 https://github.com/BofAI/skills.git "$TMPDIR/skills" && python3 "$TMPDIR/skills/twitter-digest/scripts/install.py" --client codex ``` Or use the natural-language prompt: @@ -33,7 +33,7 @@ git clone git@github.com:BofAI/skills.git bofai-skills \ To ask Claude Code to install this skill for itself, paste this into Claude Code: ```bash -TMPDIR="$(mktemp -d)" && git clone --depth 1 --branch v1.5.11-beta.11 https://github.com/BofAI/skills.git "$TMPDIR/skills" && python3 "$TMPDIR/skills/twitter-digest/scripts/install.py" --client claude --allow-claude-commands --allow-claude-state-read +TMPDIR="$(mktemp -d)" && git clone --depth 1 --branch v1.5.11-beta.12 https://github.com/BofAI/skills.git "$TMPDIR/skills" && python3 "$TMPDIR/skills/twitter-digest/scripts/install.py" --client claude --allow-claude-commands --allow-claude-state-read ``` Or use the natural-language prompt: diff --git a/twitter-mcp/SKILL.md b/twitter-mcp/SKILL.md index 404c05b..597a01d 100644 --- a/twitter-mcp/SKILL.md +++ b/twitter-mcp/SKILL.md @@ -11,31 +11,36 @@ Use this skill to install and authorize `@xdevplatform/xurl`, and to generate X/ The hosted X MCP bridge can still be registered as `xapi` when explicitly requested, but daily digest generation should not depend on MCP tools being visible. `xurl` CLI exposes direct digest-relevant commands such as `whoami`, `timeline`, `mentions`, `posts`, `dms`, and `search`. -If the user asks to generate an X/Twitter daily digest, says `生成X日报`, `X日报`, `推特日报`, or asks for a Twitter digest, run the installed `scripts/xurl_daily_digest.py` helper and write the digest from its generated context. Do not create a generic report template. Do not run `twitter-digest` scripts or local browser/API collectors from this skill. +If the user asks to generate an X/Twitter daily digest, says `生成X日报`, `X日报`, `推特日报`, or asks for a Twitter digest, use this skill to guide direct `xurl` CLI collection. Do not create a generic report template. Do not run digest helper scripts, `twitter-digest` scripts, or local browser/API collectors from this skill. ## xurl Digest Workflow -For daily digest requests, run: +For daily digest requests, run `xurl` commands directly from the agent shell and use their outputs as the digest source. Do not create or run wrapper/helper scripts for digest collection. ```bash -python3 ~/.codex/skills/twitter-mcp/scripts/xurl_daily_digest.py --max-results 100 +xurl whoami +xurl timeline -n 100 +xurl mentions -n 100 ``` -Use the matching installed path for the current client: +Detect the authenticated handle from `xurl whoami`. Then run: -- Codex: `~/.codex/skills/twitter-mcp/scripts/xurl_daily_digest.py` -- Claude Code: `~/.claude/skills/twitter-mcp/scripts/xurl_daily_digest.py` - -The helper collects: +```bash +xurl posts -n 100 +xurl search "from:" -n 100 +xurl search "@" -n 100 +xurl search "to:" -n 100 +xurl dms -n 100 +``` -- `xurl whoami` -- `xurl timeline -n 100` -- `xurl mentions -n 100` -- `xurl posts -n 100` when the handle can be detected or is provided with `--handle` -- `xurl search "from:"`, `xurl search "@"`, and `xurl search "to:"` -- `xurl dms -n 100` unless `--no-include-dms` is passed +Rules for collection: -After it runs, read `twitter-mcp/.state/xurl-run/digest-context.md` and write the digest from that context only. If a specific `xurl` command fails, report that command under data gaps and continue with the successful command outputs. If `xurl` is missing or unauthenticated, help the user install or authorize it with this skill. +- If `xurl whoami` does not reveal a handle, ask the user for the handle or skip handle-dependent commands and report the gap. +- If a specific `xurl` command fails, report that command under data gaps and continue with successful command outputs. +- If `xurl dms` fails because the app lacks scopes, tier access, or DM API access, report it as a data gap. Do not claim there are no DMs. +- Treat `xurl dms` output as private. Do not quote sensitive DM content unless needed for the user's requested action. +- Do not post, reply, like, repost, bookmark, follow, or send DMs without explicit approval after showing a draft/action summary. +- If `xurl` is missing or unauthenticated, help the user install or authorize it with this skill before generating the digest. Write the final response in Chinese by default for `X日报` requests. Use this structure: @@ -57,13 +62,13 @@ From the repository `skills/` directory: For a one-line Codex install from this beta tag: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.11/twitter-mcp/install.sh | X_MCP_REGISTER_CODEX=1 X_MCP_REGISTER_CLAUDE=0 sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.12/twitter-mcp/install.sh | X_MCP_REGISTER_CODEX=1 X_MCP_REGISTER_CLAUDE=0 sh ``` For a one-line Claude Code install from this beta tag: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.11/twitter-mcp/install.sh | X_MCP_REGISTER_CODEX=0 X_MCP_REGISTER_CLAUDE=1 sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.12/twitter-mcp/install.sh | X_MCP_REGISTER_CODEX=0 X_MCP_REGISTER_CLAUDE=1 sh ``` The installer: @@ -125,7 +130,7 @@ claude mcp add xapi -- xurl --app xmcp mcp https://api.x.com/mcp After installation: 1. Run `xurl whoami` to verify the local CLI is authorized. -2. Run `python3 ~/.codex/skills/twitter-mcp/scripts/xurl_daily_digest.py --max-results 10` to verify digest collection. +2. Run `xurl timeline -n 10` and `xurl mentions -n 10` to verify digest collection. 3. If optional MCP registration was enabled, start a new Codex or Claude Code session and check whether X MCP tools are visible under the `xapi` server. If an X MCP endpoint returns an auth, subscription, tier, or scope error, report the exact failing capability as a setup or account limitation. Do not infer that the requested X data does not exist. diff --git a/twitter-mcp/install.sh b/twitter-mcp/install.sh index 8459387..410bbb8 100755 --- a/twitter-mcp/install.sh +++ b/twitter-mcp/install.sh @@ -1,7 +1,7 @@ #!/bin/sh set -eu -TAG="${X_MCP_INSTALL_TAG:-v1.5.11-beta.11}" +TAG="${X_MCP_INSTALL_TAG:-v1.5.11-beta.12}" BASE_URL="${X_MCP_INSTALL_BASE_URL:-https://raw.githubusercontent.com/BofAI/skills/${TAG}/twitter-mcp}" REGISTER_CODEX="${X_MCP_REGISTER_CODEX:-1}" REGISTER_CLAUDE="${X_MCP_REGISTER_CLAUDE:-auto}" @@ -84,8 +84,7 @@ install_skill_copy() { download_file "${BASE_URL%/}/install.sh" "$staging/install.sh" download_file "${BASE_URL%/}/agents/openai.yaml" "$staging/agents/openai.yaml" download_file "${BASE_URL%/}/scripts/install_xmcp.sh" "$staging/scripts/install_xmcp.sh" - download_file "${BASE_URL%/}/scripts/xurl_daily_digest.py" "$staging/scripts/xurl_daily_digest.py" - chmod 700 "$staging/install.sh" "$staging/scripts/install_xmcp.sh" "$staging/scripts/xurl_daily_digest.py" + chmod 700 "$staging/install.sh" "$staging/scripts/install_xmcp.sh" mkdir -p "$skills_dir" backup_existing_skill "$target" "$skills_dir" mv "$staging" "$target" diff --git a/twitter-mcp/scripts/install_xmcp.sh b/twitter-mcp/scripts/install_xmcp.sh index 09fa610..3c5e92a 100755 --- a/twitter-mcp/scripts/install_xmcp.sh +++ b/twitter-mcp/scripts/install_xmcp.sh @@ -210,11 +210,11 @@ if should_open_terminal; then fi if ! command_exists node; then - fail "Node.js is required before installing X MCP. Install Node.js, then rerun this script." + fail "Node.js is required before installing xurl. Install Node.js, then rerun this script." fi if ! command_exists npm; then - fail "npm is required before installing X MCP. Install npm, then rerun this script." + fail "npm is required before installing xurl. Install npm, then rerun this script." fi info "Installing ${INSTALL_SPEC}" diff --git a/twitter-mcp/scripts/xurl_daily_digest.py b/twitter-mcp/scripts/xurl_daily_digest.py deleted file mode 100644 index d3042db..0000000 --- a/twitter-mcp/scripts/xurl_daily_digest.py +++ /dev/null @@ -1,202 +0,0 @@ -#!/usr/bin/env python3 -"""Collect X daily digest inputs through the local xurl CLI.""" - -from __future__ import annotations - -import argparse -import json -import os -import re -import shutil -import subprocess -import sys -from datetime import datetime, timezone -from pathlib import Path -from typing import Any - - -SKILL_DIR = Path(__file__).resolve().parents[1] -DEFAULT_OUT_DIR = SKILL_DIR / ".state" / "xurl-run" - - -def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser(description="Collect X digest context through xurl CLI.") - parser.add_argument("--app", default=os.environ.get("X_MCP_APP_NAME") or "", help="xurl app name. Defaults to xurl's configured default app.") - parser.add_argument("--handle", default="", help="Authenticated account handle, without @. Auto-detected from xurl whoami when possible.") - parser.add_argument("--out", default=str(DEFAULT_OUT_DIR), help="Output directory for digest context files.") - parser.add_argument("--max-results", type=int, default=100, help="Maximum items requested per xurl read command.") - parser.add_argument("--xurl-command", default=os.environ.get("XURL_COMMAND") or "xurl", help="xurl executable path or command name.") - parser.add_argument("--include-dms", action=argparse.BooleanOptionalAction, default=True, help="Collect recent DMs through xurl dms.") - parser.add_argument("--timeout", type=int, default=90, help="Timeout per xurl command in seconds.") - return parser.parse_args() - - -def secure_dir(path: Path) -> None: - path.mkdir(parents=True, exist_ok=True) - try: - path.chmod(0o700) - except OSError: - pass - - -def write_private(path: Path, content: str) -> None: - path.write_text(content, encoding="utf-8") - try: - path.chmod(0o600) - except OSError: - pass - - -def command_display(argv: list[str]) -> str: - return " ".join(json.dumps(part) if any(ch.isspace() for ch in part) else part for part in argv) - - -def run_xurl(xurl: str, app: str, args: list[str], timeout: int) -> dict[str, Any]: - argv = [xurl] - if app: - argv.extend(["--app", app]) - argv.extend(args) - try: - completed = subprocess.run(argv, check=False, capture_output=True, text=True, timeout=timeout) - return { - "name": args[0] if args else "xurl", - "argv": argv, - "display": command_display(argv), - "returncode": completed.returncode, - "stdout": completed.stdout, - "stderr": completed.stderr, - } - except FileNotFoundError: - return { - "name": args[0] if args else "xurl", - "argv": argv, - "display": command_display(argv), - "returncode": 127, - "stdout": "", - "stderr": f"xurl command not found: {xurl}\n", - } - except subprocess.TimeoutExpired as exc: - return { - "name": args[0] if args else "xurl", - "argv": argv, - "display": command_display(argv), - "returncode": 124, - "stdout": exc.stdout or "", - "stderr": (exc.stderr or "") + f"\nTimed out after {timeout} seconds.\n", - } - - -def extract_handle(whoami_output: str) -> str: - try: - data = json.loads(whoami_output) - node: Any = data - if isinstance(node, dict) and isinstance(node.get("data"), dict): - node = node["data"] - username = node.get("username") if isinstance(node, dict) else "" - if isinstance(username, str) and username: - return username.lstrip("@") - except json.JSONDecodeError: - pass - patterns = [ - r'"username"\s*:\s*"([A-Za-z0-9_]{1,15})"', - r"(?:username|handle)\s*[:=]\s*@?([A-Za-z0-9_]{1,15})", - r"@([A-Za-z0-9_]{1,15})", - ] - for pattern in patterns: - match = re.search(pattern, whoami_output, flags=re.IGNORECASE) - if match: - return match.group(1).lstrip("@") - return "" - - -def markdown_section(result: dict[str, Any]) -> str: - status = "ok" if result["returncode"] == 0 else f"exit {result['returncode']}" - chunks = [ - f"## {result['name']} ({status})", - "", - f"Command: `{result['display']}`", - "", - "### stdout", - "", - "```text", - result["stdout"].rstrip(), - "```", - ] - if result["stderr"].strip(): - chunks.extend(["", "### stderr", "", "```text", result["stderr"].rstrip(), "```"]) - return "\n".join(chunks) - - -def main() -> int: - args = parse_args() - xurl = shutil.which(args.xurl_command) or args.xurl_command - out_dir = Path(args.out).expanduser().resolve() - secure_dir(out_dir) - - max_results = max(1, min(args.max_results, 100)) - collected: list[dict[str, Any]] = [] - - whoami = run_xurl(xurl, args.app, ["whoami"], args.timeout) - collected.append(whoami) - handle = args.handle.lstrip("@") or extract_handle(whoami.get("stdout") or "") - - read_commands: list[tuple[str, list[str]]] = [ - ("timeline", ["timeline", "-n", str(max_results)]), - ("mentions", ["mentions", "-n", str(max(5, max_results))]), - ] - if handle: - read_commands.extend( - [ - ("posts", ["posts", handle, "-n", str(max(5, max_results))]), - ("search_from", ["search", f"from:{handle}", "-n", str(max(10, max_results))]), - ("search_mentions", ["search", f"@{handle}", "-n", str(max(10, max_results))]), - ("search_to", ["search", f"to:{handle}", "-n", str(max(10, max_results))]), - ] - ) - if args.include_dms: - read_commands.append(("dms", ["dms", "-n", str(max_results)])) - - for name, command_args in read_commands: - result = run_xurl(xurl, args.app, command_args, args.timeout) - result["name"] = name - collected.append(result) - - payload = { - "generated_at": datetime.now(timezone.utc).isoformat(), - "source": "xurl-cli", - "app": args.app or None, - "handle": handle or None, - "xurl": xurl, - "commands": collected, - } - write_private(out_dir / "digest-input.json", json.dumps(payload, ensure_ascii=False, indent=2) + "\n") - - lines = [ - "# X Daily Digest Context", - "", - f"- generated_at: `{payload['generated_at']}`", - "- source: `xurl-cli`", - f"- app: `{args.app or 'xurl default'}`", - f"- handle: `{handle or 'unknown'}`", - "", - "Use this file as the source for the Chinese X daily digest. Treat DM sections as private.", - "", - ] - lines.extend(markdown_section(result) for result in collected) - write_private(out_dir / "digest-context.md", "\n\n".join(lines) + "\n") - - failures = [result for result in collected if result["returncode"] != 0] - print(json.dumps({ - "source": "xurl-cli", - "out_dir": str(out_dir), - "context": str(out_dir / "digest-context.md"), - "json": str(out_dir / "digest-input.json"), - "handle": handle or None, - "commands": len(collected), - "failures": [{"name": item["name"], "returncode": item["returncode"]} for item in failures], - }, ensure_ascii=False, indent=2)) - return 0 if len(failures) < len(collected) else 1 - - -if __name__ == "__main__": - raise SystemExit(main()) From dbbe288c1fd5e490950faee9f2898f31dbbfc652 Mon Sep 17 00:00:00 2001 From: bobo Date: Wed, 1 Jul 2026 16:43:53 +0800 Subject: [PATCH 18/34] Skip DM collection in xurl digest skill --- twitter-digest/README.md | 4 ++-- twitter-mcp/SKILL.md | 15 +++++++-------- twitter-mcp/install.sh | 2 +- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/twitter-digest/README.md b/twitter-digest/README.md index 4b83604..e06396a 100644 --- a/twitter-digest/README.md +++ b/twitter-digest/README.md @@ -15,7 +15,7 @@ python3 twitter-digest/scripts/install.py To ask Codex to install this skill for itself, paste this into Codex: ```bash -TMPDIR="$(mktemp -d)" && git clone --depth 1 --branch v1.5.11-beta.12 https://github.com/BofAI/skills.git "$TMPDIR/skills" && python3 "$TMPDIR/skills/twitter-digest/scripts/install.py" --client codex +TMPDIR="$(mktemp -d)" && git clone --depth 1 --branch v1.5.11-beta.13 https://github.com/BofAI/skills.git "$TMPDIR/skills" && python3 "$TMPDIR/skills/twitter-digest/scripts/install.py" --client codex ``` Or use the natural-language prompt: @@ -33,7 +33,7 @@ git clone git@github.com:BofAI/skills.git bofai-skills \ To ask Claude Code to install this skill for itself, paste this into Claude Code: ```bash -TMPDIR="$(mktemp -d)" && git clone --depth 1 --branch v1.5.11-beta.12 https://github.com/BofAI/skills.git "$TMPDIR/skills" && python3 "$TMPDIR/skills/twitter-digest/scripts/install.py" --client claude --allow-claude-commands --allow-claude-state-read +TMPDIR="$(mktemp -d)" && git clone --depth 1 --branch v1.5.11-beta.13 https://github.com/BofAI/skills.git "$TMPDIR/skills" && python3 "$TMPDIR/skills/twitter-digest/scripts/install.py" --client claude --allow-claude-commands --allow-claude-state-read ``` Or use the natural-language prompt: diff --git a/twitter-mcp/SKILL.md b/twitter-mcp/SKILL.md index 597a01d..00cd8a8 100644 --- a/twitter-mcp/SKILL.md +++ b/twitter-mcp/SKILL.md @@ -9,7 +9,7 @@ description: Use when the user wants to install or authorize xurl for X/Twitter, Use this skill to install and authorize `@xdevplatform/xurl`, and to generate X/Twitter daily digests from the local `xurl` CLI. The primary digest data source is `xurl` CLI output, not the hosted X MCP tool list. This skill does not use local API collectors, local browser collectors, or `twitter-digest` scripts. -The hosted X MCP bridge can still be registered as `xapi` when explicitly requested, but daily digest generation should not depend on MCP tools being visible. `xurl` CLI exposes direct digest-relevant commands such as `whoami`, `timeline`, `mentions`, `posts`, `dms`, and `search`. +The hosted X MCP bridge can still be registered as `xapi` when explicitly requested, but daily digest generation should not depend on MCP tools being visible. `xurl` CLI exposes direct digest-relevant commands such as `whoami`, `timeline`, `mentions`, `posts`, and `search`. If the user asks to generate an X/Twitter daily digest, says `生成X日报`, `X日报`, `推特日报`, or asks for a Twitter digest, use this skill to guide direct `xurl` CLI collection. Do not create a generic report template. Do not run digest helper scripts, `twitter-digest` scripts, or local browser/API collectors from this skill. @@ -30,25 +30,24 @@ xurl posts -n 100 xurl search "from:" -n 100 xurl search "@" -n 100 xurl search "to:" -n 100 -xurl dms -n 100 ``` Rules for collection: - If `xurl whoami` does not reveal a handle, ask the user for the handle or skip handle-dependent commands and report the gap. - If a specific `xurl` command fails, report that command under data gaps and continue with successful command outputs. -- If `xurl dms` fails because the app lacks scopes, tier access, or DM API access, report it as a data gap. Do not claim there are no DMs. -- Treat `xurl dms` output as private. Do not quote sensitive DM content unless needed for the user's requested action. +- Do not run the DM command during normal daily digest collection. Current DM/API coverage is not reliable for this workflow and should not appear in the daily digest. +- If the user explicitly asks for private messages, explain that this skill does not collect them by default because the current API path is unavailable or unreliable; do not claim there are no private messages. - Do not post, reply, like, repost, bookmark, follow, or send DMs without explicit approval after showing a draft/action summary. - If `xurl` is missing or unauthenticated, help the user install or authorize it with this skill before generating the digest. Write the final response in Chinese by default for `X日报` requests. Use this structure: - 今日概览: 3-6 bullets with the highest-signal changes. -- 需要处理: direct asks, risks, reply opportunities, urgent DMs if collected. +- 需要处理: direct asks, risks, and reply opportunities from public timeline, mentions, and searches. - 时间线热点: grouped by topic with why it matters. - 我的账号动态: notable own posts or engagement. -- 数据缺口: failed `xurl` commands, auth/tier errors, unavailable DMs, or rate limits. +- 数据缺口: failed `xurl` commands, auth/tier errors, or rate limits. - 建议动作: concise reply/follow-up suggestions. Do not post or send anything without explicit approval. ## Install And Register @@ -62,13 +61,13 @@ From the repository `skills/` directory: For a one-line Codex install from this beta tag: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.12/twitter-mcp/install.sh | X_MCP_REGISTER_CODEX=1 X_MCP_REGISTER_CLAUDE=0 sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.13/twitter-mcp/install.sh | X_MCP_REGISTER_CODEX=1 X_MCP_REGISTER_CLAUDE=0 sh ``` For a one-line Claude Code install from this beta tag: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.12/twitter-mcp/install.sh | X_MCP_REGISTER_CODEX=0 X_MCP_REGISTER_CLAUDE=1 sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.13/twitter-mcp/install.sh | X_MCP_REGISTER_CODEX=0 X_MCP_REGISTER_CLAUDE=1 sh ``` The installer: diff --git a/twitter-mcp/install.sh b/twitter-mcp/install.sh index 410bbb8..adb39cc 100755 --- a/twitter-mcp/install.sh +++ b/twitter-mcp/install.sh @@ -1,7 +1,7 @@ #!/bin/sh set -eu -TAG="${X_MCP_INSTALL_TAG:-v1.5.11-beta.12}" +TAG="${X_MCP_INSTALL_TAG:-v1.5.11-beta.13}" BASE_URL="${X_MCP_INSTALL_BASE_URL:-https://raw.githubusercontent.com/BofAI/skills/${TAG}/twitter-mcp}" REGISTER_CODEX="${X_MCP_REGISTER_CODEX:-1}" REGISTER_CLAUDE="${X_MCP_REGISTER_CLAUDE:-auto}" From a1b215254ea91ab610cd30d2cde9a80e8bd7ceb1 Mon Sep 17 00:00:00 2001 From: bobo Date: Wed, 1 Jul 2026 16:46:36 +0800 Subject: [PATCH 19/34] Add curl bootstrap for Twitter digest install --- twitter-digest/README.md | 4 +-- twitter-digest/install.sh | 63 +++++++++++++++++++++++++++++++++++++++ twitter-mcp/SKILL.md | 4 +-- twitter-mcp/install.sh | 2 +- 4 files changed, 68 insertions(+), 5 deletions(-) create mode 100755 twitter-digest/install.sh diff --git a/twitter-digest/README.md b/twitter-digest/README.md index e06396a..18bc5b7 100644 --- a/twitter-digest/README.md +++ b/twitter-digest/README.md @@ -15,7 +15,7 @@ python3 twitter-digest/scripts/install.py To ask Codex to install this skill for itself, paste this into Codex: ```bash -TMPDIR="$(mktemp -d)" && git clone --depth 1 --branch v1.5.11-beta.13 https://github.com/BofAI/skills.git "$TMPDIR/skills" && python3 "$TMPDIR/skills/twitter-digest/scripts/install.py" --client codex +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.14/twitter-digest/install.sh | env TWITTER_DIGEST_INSTALL_CLIENT=codex sh ``` Or use the natural-language prompt: @@ -33,7 +33,7 @@ git clone git@github.com:BofAI/skills.git bofai-skills \ To ask Claude Code to install this skill for itself, paste this into Claude Code: ```bash -TMPDIR="$(mktemp -d)" && git clone --depth 1 --branch v1.5.11-beta.13 https://github.com/BofAI/skills.git "$TMPDIR/skills" && python3 "$TMPDIR/skills/twitter-digest/scripts/install.py" --client claude --allow-claude-commands --allow-claude-state-read +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.14/twitter-digest/install.sh | env TWITTER_DIGEST_INSTALL_CLIENT=claude TWITTER_DIGEST_ALLOW_CLAUDE_COMMANDS=1 TWITTER_DIGEST_ALLOW_CLAUDE_STATE_READ=1 sh ``` Or use the natural-language prompt: diff --git a/twitter-digest/install.sh b/twitter-digest/install.sh new file mode 100755 index 0000000..8f08e48 --- /dev/null +++ b/twitter-digest/install.sh @@ -0,0 +1,63 @@ +#!/bin/sh +set -eu + +TAG="${TWITTER_DIGEST_INSTALL_TAG:-v1.5.11-beta.14}" +REPO="${TWITTER_DIGEST_INSTALL_REPO:-https://github.com/BofAI/skills.git}" +CLIENT="${TWITTER_DIGEST_INSTALL_CLIENT:-auto}" +ALLOW_CLAUDE_COMMANDS="${TWITTER_DIGEST_ALLOW_CLAUDE_COMMANDS:-0}" +ALLOW_CLAUDE_STATE_READ="${TWITTER_DIGEST_ALLOW_CLAUDE_STATE_READ:-0}" +SKIP_BROWSER_CHECK="${TWITTER_DIGEST_SKIP_BROWSER_CHECK:-0}" + +info() { + printf '==> %s\n' "$1" +} + +truthy() { + case "${1:-}" in + 1|true|yes) return 0 ;; + *) return 1 ;; + esac +} + +if ! command -v git >/dev/null 2>&1; then + printf 'Error: git is required to install twitter-digest.\n' >&2 + exit 1 +fi + +if ! command -v python3 >/dev/null 2>&1; then + printf 'Error: python3 is required to install twitter-digest.\n' >&2 + exit 1 +fi + +if command -v mktemp >/dev/null 2>&1; then + WORKDIR="$(mktemp -d 2>/dev/null || mktemp -d -t twitter-digest)" +else + WORKDIR="${TMPDIR:-/tmp}/twitter-digest-install.$$" + mkdir -p "$WORKDIR" +fi + +CLONE_DIR="$WORKDIR/skills" +info "Cloning ${REPO} at ${TAG}" +git clone --depth 1 --branch "$TAG" "$REPO" "$CLONE_DIR" + +args="" +case "$CLIENT" in + auto|codex|claude) args="$args --client $CLIENT" ;; + *) printf 'Error: TWITTER_DIGEST_INSTALL_CLIENT must be auto, codex, or claude.\n' >&2; exit 1 ;; +esac + +if truthy "$ALLOW_CLAUDE_COMMANDS"; then + args="$args --allow-claude-commands" +fi + +if truthy "$ALLOW_CLAUDE_STATE_READ"; then + args="$args --allow-claude-state-read" +fi + +if truthy "$SKIP_BROWSER_CHECK"; then + args="$args --skip-browser-check" +fi + +info "Running twitter-digest installer" +# shellcheck disable=SC2086 +exec python3 "$CLONE_DIR/twitter-digest/scripts/install.py" $args "$@" diff --git a/twitter-mcp/SKILL.md b/twitter-mcp/SKILL.md index 00cd8a8..2f50925 100644 --- a/twitter-mcp/SKILL.md +++ b/twitter-mcp/SKILL.md @@ -61,13 +61,13 @@ From the repository `skills/` directory: For a one-line Codex install from this beta tag: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.13/twitter-mcp/install.sh | X_MCP_REGISTER_CODEX=1 X_MCP_REGISTER_CLAUDE=0 sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.14/twitter-mcp/install.sh | X_MCP_REGISTER_CODEX=1 X_MCP_REGISTER_CLAUDE=0 sh ``` For a one-line Claude Code install from this beta tag: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.13/twitter-mcp/install.sh | X_MCP_REGISTER_CODEX=0 X_MCP_REGISTER_CLAUDE=1 sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.14/twitter-mcp/install.sh | X_MCP_REGISTER_CODEX=0 X_MCP_REGISTER_CLAUDE=1 sh ``` The installer: diff --git a/twitter-mcp/install.sh b/twitter-mcp/install.sh index adb39cc..e999e6b 100755 --- a/twitter-mcp/install.sh +++ b/twitter-mcp/install.sh @@ -1,7 +1,7 @@ #!/bin/sh set -eu -TAG="${X_MCP_INSTALL_TAG:-v1.5.11-beta.13}" +TAG="${X_MCP_INSTALL_TAG:-v1.5.11-beta.14}" BASE_URL="${X_MCP_INSTALL_BASE_URL:-https://raw.githubusercontent.com/BofAI/skills/${TAG}/twitter-mcp}" REGISTER_CODEX="${X_MCP_REGISTER_CODEX:-1}" REGISTER_CLAUDE="${X_MCP_REGISTER_CLAUDE:-auto}" From b58fb637456bf9a871566304cc59590235a6cf47 Mon Sep 17 00:00:00 2001 From: bobo Date: Wed, 1 Jul 2026 16:58:30 +0800 Subject: [PATCH 20/34] Avoid Terminal launch before xurl prerequisites --- twitter-digest/README.md | 4 ++-- twitter-digest/install.sh | 2 +- twitter-mcp/SKILL.md | 4 ++-- twitter-mcp/install.sh | 2 +- twitter-mcp/scripts/install_xmcp.sh | 12 +++++------- 5 files changed, 11 insertions(+), 13 deletions(-) diff --git a/twitter-digest/README.md b/twitter-digest/README.md index 18bc5b7..daddea6 100644 --- a/twitter-digest/README.md +++ b/twitter-digest/README.md @@ -15,7 +15,7 @@ python3 twitter-digest/scripts/install.py To ask Codex to install this skill for itself, paste this into Codex: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.14/twitter-digest/install.sh | env TWITTER_DIGEST_INSTALL_CLIENT=codex sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.16/twitter-digest/install.sh | env TWITTER_DIGEST_INSTALL_CLIENT=codex sh ``` Or use the natural-language prompt: @@ -33,7 +33,7 @@ git clone git@github.com:BofAI/skills.git bofai-skills \ To ask Claude Code to install this skill for itself, paste this into Claude Code: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.14/twitter-digest/install.sh | env TWITTER_DIGEST_INSTALL_CLIENT=claude TWITTER_DIGEST_ALLOW_CLAUDE_COMMANDS=1 TWITTER_DIGEST_ALLOW_CLAUDE_STATE_READ=1 sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.16/twitter-digest/install.sh | env TWITTER_DIGEST_INSTALL_CLIENT=claude TWITTER_DIGEST_ALLOW_CLAUDE_COMMANDS=1 TWITTER_DIGEST_ALLOW_CLAUDE_STATE_READ=1 sh ``` Or use the natural-language prompt: diff --git a/twitter-digest/install.sh b/twitter-digest/install.sh index 8f08e48..827b7d2 100755 --- a/twitter-digest/install.sh +++ b/twitter-digest/install.sh @@ -1,7 +1,7 @@ #!/bin/sh set -eu -TAG="${TWITTER_DIGEST_INSTALL_TAG:-v1.5.11-beta.14}" +TAG="${TWITTER_DIGEST_INSTALL_TAG:-v1.5.11-beta.16}" REPO="${TWITTER_DIGEST_INSTALL_REPO:-https://github.com/BofAI/skills.git}" CLIENT="${TWITTER_DIGEST_INSTALL_CLIENT:-auto}" ALLOW_CLAUDE_COMMANDS="${TWITTER_DIGEST_ALLOW_CLAUDE_COMMANDS:-0}" diff --git a/twitter-mcp/SKILL.md b/twitter-mcp/SKILL.md index 2f50925..6e7df15 100644 --- a/twitter-mcp/SKILL.md +++ b/twitter-mcp/SKILL.md @@ -61,13 +61,13 @@ From the repository `skills/` directory: For a one-line Codex install from this beta tag: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.14/twitter-mcp/install.sh | X_MCP_REGISTER_CODEX=1 X_MCP_REGISTER_CLAUDE=0 sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.16/twitter-mcp/install.sh | X_MCP_REGISTER_CODEX=1 X_MCP_REGISTER_CLAUDE=0 sh ``` For a one-line Claude Code install from this beta tag: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.14/twitter-mcp/install.sh | X_MCP_REGISTER_CODEX=0 X_MCP_REGISTER_CLAUDE=1 sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.16/twitter-mcp/install.sh | X_MCP_REGISTER_CODEX=0 X_MCP_REGISTER_CLAUDE=1 sh ``` The installer: diff --git a/twitter-mcp/install.sh b/twitter-mcp/install.sh index e999e6b..ad587be 100755 --- a/twitter-mcp/install.sh +++ b/twitter-mcp/install.sh @@ -1,7 +1,7 @@ #!/bin/sh set -eu -TAG="${X_MCP_INSTALL_TAG:-v1.5.11-beta.14}" +TAG="${X_MCP_INSTALL_TAG:-v1.5.11-beta.16}" BASE_URL="${X_MCP_INSTALL_BASE_URL:-https://raw.githubusercontent.com/BofAI/skills/${TAG}/twitter-mcp}" REGISTER_CODEX="${X_MCP_REGISTER_CODEX:-1}" REGISTER_CLAUDE="${X_MCP_REGISTER_CLAUDE:-auto}" diff --git a/twitter-mcp/scripts/install_xmcp.sh b/twitter-mcp/scripts/install_xmcp.sh index 3c5e92a..bb5ff8f 100755 --- a/twitter-mcp/scripts/install_xmcp.sh +++ b/twitter-mcp/scripts/install_xmcp.sh @@ -105,10 +105,8 @@ open_in_terminal_and_exit() { fi local path path="$(script_path)" - local cwd - cwd="$(pwd)" local command_text - command_text="cd $(shell_quote "$cwd") && X_MCP_TERMINAL_CHILD=1 /bin/bash $(shell_quote "$path"); printf '\\nPress Enter to close this window...'; IFS= read -r _" + command_text="cd ~ && X_MCP_TERMINAL_CHILD=1 /bin/bash $(shell_quote "$path"); printf '\\nPress Enter to close this window...'; IFS= read -r _" osascript >/dev/null < Date: Wed, 1 Jul 2026 17:03:54 +0800 Subject: [PATCH 21/34] Install Node with Homebrew for xurl setup --- twitter-digest/README.md | 4 +-- twitter-digest/install.sh | 2 +- twitter-mcp/SKILL.md | 6 ++-- twitter-mcp/install.sh | 2 +- twitter-mcp/scripts/install_xmcp.sh | 56 +++++++++++++++++++++++++---- 5 files changed, 56 insertions(+), 14 deletions(-) diff --git a/twitter-digest/README.md b/twitter-digest/README.md index daddea6..227bf4d 100644 --- a/twitter-digest/README.md +++ b/twitter-digest/README.md @@ -15,7 +15,7 @@ python3 twitter-digest/scripts/install.py To ask Codex to install this skill for itself, paste this into Codex: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.16/twitter-digest/install.sh | env TWITTER_DIGEST_INSTALL_CLIENT=codex sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.17/twitter-digest/install.sh | env TWITTER_DIGEST_INSTALL_CLIENT=codex sh ``` Or use the natural-language prompt: @@ -33,7 +33,7 @@ git clone git@github.com:BofAI/skills.git bofai-skills \ To ask Claude Code to install this skill for itself, paste this into Claude Code: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.16/twitter-digest/install.sh | env TWITTER_DIGEST_INSTALL_CLIENT=claude TWITTER_DIGEST_ALLOW_CLAUDE_COMMANDS=1 TWITTER_DIGEST_ALLOW_CLAUDE_STATE_READ=1 sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.17/twitter-digest/install.sh | env TWITTER_DIGEST_INSTALL_CLIENT=claude TWITTER_DIGEST_ALLOW_CLAUDE_COMMANDS=1 TWITTER_DIGEST_ALLOW_CLAUDE_STATE_READ=1 sh ``` Or use the natural-language prompt: diff --git a/twitter-digest/install.sh b/twitter-digest/install.sh index 827b7d2..d6fe5ca 100755 --- a/twitter-digest/install.sh +++ b/twitter-digest/install.sh @@ -1,7 +1,7 @@ #!/bin/sh set -eu -TAG="${TWITTER_DIGEST_INSTALL_TAG:-v1.5.11-beta.16}" +TAG="${TWITTER_DIGEST_INSTALL_TAG:-v1.5.11-beta.17}" REPO="${TWITTER_DIGEST_INSTALL_REPO:-https://github.com/BofAI/skills.git}" CLIENT="${TWITTER_DIGEST_INSTALL_CLIENT:-auto}" ALLOW_CLAUDE_COMMANDS="${TWITTER_DIGEST_ALLOW_CLAUDE_COMMANDS:-0}" diff --git a/twitter-mcp/SKILL.md b/twitter-mcp/SKILL.md index 6e7df15..00b8f70 100644 --- a/twitter-mcp/SKILL.md +++ b/twitter-mcp/SKILL.md @@ -61,19 +61,19 @@ From the repository `skills/` directory: For a one-line Codex install from this beta tag: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.16/twitter-mcp/install.sh | X_MCP_REGISTER_CODEX=1 X_MCP_REGISTER_CLAUDE=0 sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.17/twitter-mcp/install.sh | X_MCP_REGISTER_CODEX=1 X_MCP_REGISTER_CLAUDE=0 sh ``` For a one-line Claude Code install from this beta tag: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.16/twitter-mcp/install.sh | X_MCP_REGISTER_CODEX=0 X_MCP_REGISTER_CLAUDE=1 sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.17/twitter-mcp/install.sh | X_MCP_REGISTER_CODEX=0 X_MCP_REGISTER_CLAUDE=1 sh ``` The installer: - Installs this `twitter-mcp` skill into the selected local skills directory. -- Requires Node.js and npm. +- Requires Node.js and npm; when they are missing and Homebrew is available, the installer installs Node.js automatically with `brew install node`. - Installs `@xdevplatform/xurl` globally with npm. - Opens the X OAuth2 authorization flow. - Does not register the hosted X MCP bridge by default. To also register MCP, set `X_MCP_REGISTER_CODEX_MCP=1` or `X_MCP_REGISTER_CLAUDE_MCP=1`. diff --git a/twitter-mcp/install.sh b/twitter-mcp/install.sh index ad587be..8ab0988 100755 --- a/twitter-mcp/install.sh +++ b/twitter-mcp/install.sh @@ -1,7 +1,7 @@ #!/bin/sh set -eu -TAG="${X_MCP_INSTALL_TAG:-v1.5.11-beta.16}" +TAG="${X_MCP_INSTALL_TAG:-v1.5.11-beta.17}" BASE_URL="${X_MCP_INSTALL_BASE_URL:-https://raw.githubusercontent.com/BofAI/skills/${TAG}/twitter-mcp}" REGISTER_CODEX="${X_MCP_REGISTER_CODEX:-1}" REGISTER_CLAUDE="${X_MCP_REGISTER_CLAUDE:-auto}" diff --git a/twitter-mcp/scripts/install_xmcp.sh b/twitter-mcp/scripts/install_xmcp.sh index bb5ff8f..f4765eb 100755 --- a/twitter-mcp/scripts/install_xmcp.sh +++ b/twitter-mcp/scripts/install_xmcp.sh @@ -12,6 +12,7 @@ REGISTER_CLAUDE="${X_MCP_REGISTER_CLAUDE:-0}" CODEX_CONFIG="${CODEX_CONFIG:-$HOME/.codex/config.toml}" OPEN_TERMINAL="${X_MCP_OPEN_TERMINAL:-auto}" XURL_COMMAND="${X_MCP_XURL_COMMAND:-}" +INSTALL_NODE="${X_MCP_INSTALL_NODE:-auto}" info() { printf '==> %s\n' "$1" @@ -26,6 +27,22 @@ command_exists() { command -v "$1" >/dev/null 2>&1 } +find_brew() { + local brew_path="" + brew_path="$(command -v brew 2>/dev/null || true)" + if [ -n "$brew_path" ]; then + printf '%s' "$brew_path" + return 0 + fi + for candidate in /opt/homebrew/bin/brew /usr/local/bin/brew; do + if [ -x "$candidate" ]; then + printf '%s' "$candidate" + return 0 + fi + done + return 1 +} + shell_quote() { local value="$1" printf "'%s'" "$(printf '%s' "$value" | sed "s/'/'\\\\''/g")" @@ -145,6 +162,37 @@ prompt_secret() { printf '%s' "$value" } +ensure_node_runtime() { + if command_exists node && command_exists npm; then + return 0 + fi + + case "$INSTALL_NODE" in + 0|false|no) + fail "Node.js and npm are required before installing xurl. Install Node.js, then rerun this script." + ;; + auto|1|true|yes) ;; + *) + fail "X_MCP_INSTALL_NODE must be auto, 1, true, yes, 0, false, or no." + ;; + esac + + local brew_path="" + brew_path="$(find_brew || true)" + if [ -n "$brew_path" ]; then + info "Node.js/npm not found. Installing Node.js with Homebrew." + "$brew_path" install node + export PATH="${brew_path%/*}:$PATH" + fi + + if command_exists node && command_exists npm; then + info "Node.js is available: $(node --version 2>/dev/null || printf 'node')" + return 0 + fi + + fail "Node.js and npm are required before installing xurl. Install Node.js from https://nodejs.org/ or run 'brew install node', then rerun this script." +} + register_codex() { local config_path="$1" local server_name="$2" @@ -203,13 +251,7 @@ register_claude() { claude mcp add "$server_name" -- "$xurl_command" --app "$app_name" mcp https://api.x.com/mcp } -if ! command_exists node; then - fail "Node.js is required before installing xurl. Install Node.js, then rerun this script." -fi - -if ! command_exists npm; then - fail "npm is required before installing xurl. Install npm, then rerun this script." -fi +ensure_node_runtime if should_open_terminal; then open_in_terminal_and_exit From a5f9db2cb8cc74fba6c1e4b67a83ebc706fd1a81 Mon Sep 17 00:00:00 2001 From: bobo Date: Wed, 1 Jul 2026 17:09:07 +0800 Subject: [PATCH 22/34] Revert automatic Node installation --- twitter-digest/README.md | 4 +-- twitter-digest/install.sh | 2 +- twitter-mcp/SKILL.md | 6 ++-- twitter-mcp/install.sh | 2 +- twitter-mcp/scripts/install_xmcp.sh | 56 ++++------------------------- 5 files changed, 14 insertions(+), 56 deletions(-) diff --git a/twitter-digest/README.md b/twitter-digest/README.md index 227bf4d..0e352c3 100644 --- a/twitter-digest/README.md +++ b/twitter-digest/README.md @@ -15,7 +15,7 @@ python3 twitter-digest/scripts/install.py To ask Codex to install this skill for itself, paste this into Codex: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.17/twitter-digest/install.sh | env TWITTER_DIGEST_INSTALL_CLIENT=codex sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.18/twitter-digest/install.sh | env TWITTER_DIGEST_INSTALL_CLIENT=codex sh ``` Or use the natural-language prompt: @@ -33,7 +33,7 @@ git clone git@github.com:BofAI/skills.git bofai-skills \ To ask Claude Code to install this skill for itself, paste this into Claude Code: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.17/twitter-digest/install.sh | env TWITTER_DIGEST_INSTALL_CLIENT=claude TWITTER_DIGEST_ALLOW_CLAUDE_COMMANDS=1 TWITTER_DIGEST_ALLOW_CLAUDE_STATE_READ=1 sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.18/twitter-digest/install.sh | env TWITTER_DIGEST_INSTALL_CLIENT=claude TWITTER_DIGEST_ALLOW_CLAUDE_COMMANDS=1 TWITTER_DIGEST_ALLOW_CLAUDE_STATE_READ=1 sh ``` Or use the natural-language prompt: diff --git a/twitter-digest/install.sh b/twitter-digest/install.sh index d6fe5ca..8916b75 100755 --- a/twitter-digest/install.sh +++ b/twitter-digest/install.sh @@ -1,7 +1,7 @@ #!/bin/sh set -eu -TAG="${TWITTER_DIGEST_INSTALL_TAG:-v1.5.11-beta.17}" +TAG="${TWITTER_DIGEST_INSTALL_TAG:-v1.5.11-beta.18}" REPO="${TWITTER_DIGEST_INSTALL_REPO:-https://github.com/BofAI/skills.git}" CLIENT="${TWITTER_DIGEST_INSTALL_CLIENT:-auto}" ALLOW_CLAUDE_COMMANDS="${TWITTER_DIGEST_ALLOW_CLAUDE_COMMANDS:-0}" diff --git a/twitter-mcp/SKILL.md b/twitter-mcp/SKILL.md index 00b8f70..c0efa41 100644 --- a/twitter-mcp/SKILL.md +++ b/twitter-mcp/SKILL.md @@ -61,19 +61,19 @@ From the repository `skills/` directory: For a one-line Codex install from this beta tag: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.17/twitter-mcp/install.sh | X_MCP_REGISTER_CODEX=1 X_MCP_REGISTER_CLAUDE=0 sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.18/twitter-mcp/install.sh | X_MCP_REGISTER_CODEX=1 X_MCP_REGISTER_CLAUDE=0 sh ``` For a one-line Claude Code install from this beta tag: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.17/twitter-mcp/install.sh | X_MCP_REGISTER_CODEX=0 X_MCP_REGISTER_CLAUDE=1 sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.18/twitter-mcp/install.sh | X_MCP_REGISTER_CODEX=0 X_MCP_REGISTER_CLAUDE=1 sh ``` The installer: - Installs this `twitter-mcp` skill into the selected local skills directory. -- Requires Node.js and npm; when they are missing and Homebrew is available, the installer installs Node.js automatically with `brew install node`. +- Requires Node.js and npm. - Installs `@xdevplatform/xurl` globally with npm. - Opens the X OAuth2 authorization flow. - Does not register the hosted X MCP bridge by default. To also register MCP, set `X_MCP_REGISTER_CODEX_MCP=1` or `X_MCP_REGISTER_CLAUDE_MCP=1`. diff --git a/twitter-mcp/install.sh b/twitter-mcp/install.sh index 8ab0988..f7a548d 100755 --- a/twitter-mcp/install.sh +++ b/twitter-mcp/install.sh @@ -1,7 +1,7 @@ #!/bin/sh set -eu -TAG="${X_MCP_INSTALL_TAG:-v1.5.11-beta.17}" +TAG="${X_MCP_INSTALL_TAG:-v1.5.11-beta.18}" BASE_URL="${X_MCP_INSTALL_BASE_URL:-https://raw.githubusercontent.com/BofAI/skills/${TAG}/twitter-mcp}" REGISTER_CODEX="${X_MCP_REGISTER_CODEX:-1}" REGISTER_CLAUDE="${X_MCP_REGISTER_CLAUDE:-auto}" diff --git a/twitter-mcp/scripts/install_xmcp.sh b/twitter-mcp/scripts/install_xmcp.sh index f4765eb..bb5ff8f 100755 --- a/twitter-mcp/scripts/install_xmcp.sh +++ b/twitter-mcp/scripts/install_xmcp.sh @@ -12,7 +12,6 @@ REGISTER_CLAUDE="${X_MCP_REGISTER_CLAUDE:-0}" CODEX_CONFIG="${CODEX_CONFIG:-$HOME/.codex/config.toml}" OPEN_TERMINAL="${X_MCP_OPEN_TERMINAL:-auto}" XURL_COMMAND="${X_MCP_XURL_COMMAND:-}" -INSTALL_NODE="${X_MCP_INSTALL_NODE:-auto}" info() { printf '==> %s\n' "$1" @@ -27,22 +26,6 @@ command_exists() { command -v "$1" >/dev/null 2>&1 } -find_brew() { - local brew_path="" - brew_path="$(command -v brew 2>/dev/null || true)" - if [ -n "$brew_path" ]; then - printf '%s' "$brew_path" - return 0 - fi - for candidate in /opt/homebrew/bin/brew /usr/local/bin/brew; do - if [ -x "$candidate" ]; then - printf '%s' "$candidate" - return 0 - fi - done - return 1 -} - shell_quote() { local value="$1" printf "'%s'" "$(printf '%s' "$value" | sed "s/'/'\\\\''/g")" @@ -162,37 +145,6 @@ prompt_secret() { printf '%s' "$value" } -ensure_node_runtime() { - if command_exists node && command_exists npm; then - return 0 - fi - - case "$INSTALL_NODE" in - 0|false|no) - fail "Node.js and npm are required before installing xurl. Install Node.js, then rerun this script." - ;; - auto|1|true|yes) ;; - *) - fail "X_MCP_INSTALL_NODE must be auto, 1, true, yes, 0, false, or no." - ;; - esac - - local brew_path="" - brew_path="$(find_brew || true)" - if [ -n "$brew_path" ]; then - info "Node.js/npm not found. Installing Node.js with Homebrew." - "$brew_path" install node - export PATH="${brew_path%/*}:$PATH" - fi - - if command_exists node && command_exists npm; then - info "Node.js is available: $(node --version 2>/dev/null || printf 'node')" - return 0 - fi - - fail "Node.js and npm are required before installing xurl. Install Node.js from https://nodejs.org/ or run 'brew install node', then rerun this script." -} - register_codex() { local config_path="$1" local server_name="$2" @@ -251,7 +203,13 @@ register_claude() { claude mcp add "$server_name" -- "$xurl_command" --app "$app_name" mcp https://api.x.com/mcp } -ensure_node_runtime +if ! command_exists node; then + fail "Node.js is required before installing xurl. Install Node.js, then rerun this script." +fi + +if ! command_exists npm; then + fail "npm is required before installing xurl. Install npm, then rerun this script." +fi if should_open_terminal; then open_in_terminal_and_exit From 1cefc72967261bccc9d8d3e43534e706a5f78eaf Mon Sep 17 00:00:00 2001 From: bobo Date: Wed, 1 Jul 2026 18:35:42 +0800 Subject: [PATCH 23/34] Enforce local 24 hour window for xurl digest --- twitter-digest/README.md | 4 ++-- twitter-digest/install.sh | 2 +- twitter-mcp/SKILL.md | 17 ++++++++++++++--- twitter-mcp/install.sh | 2 +- 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/twitter-digest/README.md b/twitter-digest/README.md index 0e352c3..3f98431 100644 --- a/twitter-digest/README.md +++ b/twitter-digest/README.md @@ -15,7 +15,7 @@ python3 twitter-digest/scripts/install.py To ask Codex to install this skill for itself, paste this into Codex: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.18/twitter-digest/install.sh | env TWITTER_DIGEST_INSTALL_CLIENT=codex sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.19/twitter-digest/install.sh | env TWITTER_DIGEST_INSTALL_CLIENT=codex sh ``` Or use the natural-language prompt: @@ -33,7 +33,7 @@ git clone git@github.com:BofAI/skills.git bofai-skills \ To ask Claude Code to install this skill for itself, paste this into Claude Code: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.18/twitter-digest/install.sh | env TWITTER_DIGEST_INSTALL_CLIENT=claude TWITTER_DIGEST_ALLOW_CLAUDE_COMMANDS=1 TWITTER_DIGEST_ALLOW_CLAUDE_STATE_READ=1 sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.19/twitter-digest/install.sh | env TWITTER_DIGEST_INSTALL_CLIENT=claude TWITTER_DIGEST_ALLOW_CLAUDE_COMMANDS=1 TWITTER_DIGEST_ALLOW_CLAUDE_STATE_READ=1 sh ``` Or use the natural-language prompt: diff --git a/twitter-digest/install.sh b/twitter-digest/install.sh index 8916b75..f034e08 100755 --- a/twitter-digest/install.sh +++ b/twitter-digest/install.sh @@ -1,7 +1,7 @@ #!/bin/sh set -eu -TAG="${TWITTER_DIGEST_INSTALL_TAG:-v1.5.11-beta.18}" +TAG="${TWITTER_DIGEST_INSTALL_TAG:-v1.5.11-beta.19}" REPO="${TWITTER_DIGEST_INSTALL_REPO:-https://github.com/BofAI/skills.git}" CLIENT="${TWITTER_DIGEST_INSTALL_CLIENT:-auto}" ALLOW_CLAUDE_COMMANDS="${TWITTER_DIGEST_ALLOW_CLAUDE_COMMANDS:-0}" diff --git a/twitter-mcp/SKILL.md b/twitter-mcp/SKILL.md index c0efa41..acdf07d 100644 --- a/twitter-mcp/SKILL.md +++ b/twitter-mcp/SKILL.md @@ -17,6 +17,14 @@ If the user asks to generate an X/Twitter daily digest, says `生成X日报`, `X For daily digest requests, run `xurl` commands directly from the agent shell and use their outputs as the digest source. Do not create or run wrapper/helper scripts for digest collection. +Before collecting X data, determine the user's current local timezone and the exact 24-hour window: + +```bash +date '+%Y-%m-%d %H:%M:%S %Z %z' +``` + +Use that local time as `now`, compute `cutoff = now - 24 hours`, and include only X items whose timestamp is within `[cutoff, now]` in the user's local timezone. Convert UTC `created_at` timestamps from X into the user's local timezone before filtering or grouping. Do not use calendar-day-only filtering unless the user explicitly asks for "today" by date instead of "past 24 hours". + ```bash xurl whoami xurl timeline -n 100 @@ -35,6 +43,9 @@ xurl search "to:" -n 100 Rules for collection: - If `xurl whoami` does not reveal a handle, ask the user for the handle or skip handle-dependent commands and report the gap. +- Keep only posts, mentions, searches, and timeline items from the last 24 hours in the user's current local timezone. +- If an item has no parseable timestamp, do not use it for time-bound facts; report it under data gaps as time-unverified. +- When using `xurl search`, search date operators may be used only as a coarse prefilter; still post-filter each returned item to the exact 24-hour window. - If a specific `xurl` command fails, report that command under data gaps and continue with successful command outputs. - Do not run the DM command during normal daily digest collection. Current DM/API coverage is not reliable for this workflow and should not appear in the daily digest. - If the user explicitly asks for private messages, explain that this skill does not collect them by default because the current API path is unavailable or unreliable; do not claim there are no private messages. @@ -47,7 +58,7 @@ Write the final response in Chinese by default for `X日报` requests. Use this - 需要处理: direct asks, risks, and reply opportunities from public timeline, mentions, and searches. - 时间线热点: grouped by topic with why it matters. - 我的账号动态: notable own posts or engagement. -- 数据缺口: failed `xurl` commands, auth/tier errors, or rate limits. +- 数据缺口: failed `xurl` commands, auth/tier errors, rate limits, or items excluded because timestamps were missing/unparseable. - 建议动作: concise reply/follow-up suggestions. Do not post or send anything without explicit approval. ## Install And Register @@ -61,13 +72,13 @@ From the repository `skills/` directory: For a one-line Codex install from this beta tag: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.18/twitter-mcp/install.sh | X_MCP_REGISTER_CODEX=1 X_MCP_REGISTER_CLAUDE=0 sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.19/twitter-mcp/install.sh | env X_MCP_REGISTER_CODEX=1 X_MCP_REGISTER_CLAUDE=0 sh ``` For a one-line Claude Code install from this beta tag: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.18/twitter-mcp/install.sh | X_MCP_REGISTER_CODEX=0 X_MCP_REGISTER_CLAUDE=1 sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.19/twitter-mcp/install.sh | env X_MCP_REGISTER_CODEX=0 X_MCP_REGISTER_CLAUDE=1 sh ``` The installer: diff --git a/twitter-mcp/install.sh b/twitter-mcp/install.sh index f7a548d..df24e27 100755 --- a/twitter-mcp/install.sh +++ b/twitter-mcp/install.sh @@ -1,7 +1,7 @@ #!/bin/sh set -eu -TAG="${X_MCP_INSTALL_TAG:-v1.5.11-beta.18}" +TAG="${X_MCP_INSTALL_TAG:-v1.5.11-beta.19}" BASE_URL="${X_MCP_INSTALL_BASE_URL:-https://raw.githubusercontent.com/BofAI/skills/${TAG}/twitter-mcp}" REGISTER_CODEX="${X_MCP_REGISTER_CODEX:-1}" REGISTER_CLAUDE="${X_MCP_REGISTER_CLAUDE:-auto}" From 719cdc7def7ca7bc81829cd0459536e70b1f9d14 Mon Sep 17 00:00:00 2001 From: bobo Date: Wed, 1 Jul 2026 18:37:05 +0800 Subject: [PATCH 24/34] Tighten xurl mention filtering and reply checks --- twitter-digest/README.md | 4 ++-- twitter-digest/install.sh | 2 +- twitter-mcp/SKILL.md | 12 +++++++----- twitter-mcp/install.sh | 2 +- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/twitter-digest/README.md b/twitter-digest/README.md index 3f98431..fc224ce 100644 --- a/twitter-digest/README.md +++ b/twitter-digest/README.md @@ -15,7 +15,7 @@ python3 twitter-digest/scripts/install.py To ask Codex to install this skill for itself, paste this into Codex: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.19/twitter-digest/install.sh | env TWITTER_DIGEST_INSTALL_CLIENT=codex sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.20/twitter-digest/install.sh | env TWITTER_DIGEST_INSTALL_CLIENT=codex sh ``` Or use the natural-language prompt: @@ -33,7 +33,7 @@ git clone git@github.com:BofAI/skills.git bofai-skills \ To ask Claude Code to install this skill for itself, paste this into Claude Code: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.19/twitter-digest/install.sh | env TWITTER_DIGEST_INSTALL_CLIENT=claude TWITTER_DIGEST_ALLOW_CLAUDE_COMMANDS=1 TWITTER_DIGEST_ALLOW_CLAUDE_STATE_READ=1 sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.20/twitter-digest/install.sh | env TWITTER_DIGEST_INSTALL_CLIENT=claude TWITTER_DIGEST_ALLOW_CLAUDE_COMMANDS=1 TWITTER_DIGEST_ALLOW_CLAUDE_STATE_READ=1 sh ``` Or use the natural-language prompt: diff --git a/twitter-digest/install.sh b/twitter-digest/install.sh index f034e08..a495a29 100755 --- a/twitter-digest/install.sh +++ b/twitter-digest/install.sh @@ -1,7 +1,7 @@ #!/bin/sh set -eu -TAG="${TWITTER_DIGEST_INSTALL_TAG:-v1.5.11-beta.19}" +TAG="${TWITTER_DIGEST_INSTALL_TAG:-v1.5.11-beta.20}" REPO="${TWITTER_DIGEST_INSTALL_REPO:-https://github.com/BofAI/skills.git}" CLIENT="${TWITTER_DIGEST_INSTALL_CLIENT:-auto}" ALLOW_CLAUDE_COMMANDS="${TWITTER_DIGEST_ALLOW_CLAUDE_COMMANDS:-0}" diff --git a/twitter-mcp/SKILL.md b/twitter-mcp/SKILL.md index acdf07d..f85d70f 100644 --- a/twitter-mcp/SKILL.md +++ b/twitter-mcp/SKILL.md @@ -23,7 +23,7 @@ Before collecting X data, determine the user's current local timezone and the ex date '+%Y-%m-%d %H:%M:%S %Z %z' ``` -Use that local time as `now`, compute `cutoff = now - 24 hours`, and include only X items whose timestamp is within `[cutoff, now]` in the user's local timezone. Convert UTC `created_at` timestamps from X into the user's local timezone before filtering or grouping. Do not use calendar-day-only filtering unless the user explicitly asks for "today" by date instead of "past 24 hours". +Use that local time as `now`, compute `cutoff = now - 24 hours`, and include only X items whose timestamp is within `[cutoff, now]` in the user's local timezone. Convert UTC `created_at` timestamps from X into the user's local timezone before filtering or grouping. Do not use calendar-day-only filtering unless the user explicitly asks for "today" by date instead of "past 24 hours". Do not rely on `xurl` result ordering, labels such as "recent", or search date operators as proof that an item is inside the window; every item used in the digest must pass this timestamp check. ```bash xurl whoami @@ -43,9 +43,11 @@ xurl search "to:" -n 100 Rules for collection: - If `xurl whoami` does not reveal a handle, ask the user for the handle or skip handle-dependent commands and report the gap. -- Keep only posts, mentions, searches, and timeline items from the last 24 hours in the user's current local timezone. +- Keep only posts, mentions, searches, and timeline items from the last 24 hours in the user's current local timezone. This rule is strict for `xurl mentions`: mentions older than `cutoff` must be discarded before analysis and must not appear in `需要处理`. - If an item has no parseable timestamp, do not use it for time-bound facts; report it under data gaps as time-unverified. - When using `xurl search`, search date operators may be used only as a coarse prefilter; still post-filter each returned item to the exact 24-hour window. +- Before adding any mention or direct ask to `需要处理` as "needs reply", verify whether the authenticated account has already replied after that mention's timestamp. Use `xurl posts ` and `xurl search "from: to:" -n 100` or equivalent `xurl` output to check for replies in the same conversation/thread or to the same author. If a reply from the authenticated account already exists after the mention timestamp, mark it as already handled or omit it from `需要处理`; do not ask the user to reply again. +- If the reply status cannot be verified from available `xurl` output, label it as `回复状态未确认` instead of claiming the user still needs to reply. - If a specific `xurl` command fails, report that command under data gaps and continue with successful command outputs. - Do not run the DM command during normal daily digest collection. Current DM/API coverage is not reliable for this workflow and should not appear in the daily digest. - If the user explicitly asks for private messages, explain that this skill does not collect them by default because the current API path is unavailable or unreliable; do not claim there are no private messages. @@ -55,7 +57,7 @@ Rules for collection: Write the final response in Chinese by default for `X日报` requests. Use this structure: - 今日概览: 3-6 bullets with the highest-signal changes. -- 需要处理: direct asks, risks, and reply opportunities from public timeline, mentions, and searches. +- 需要处理: unresolved direct asks, risks, and reply opportunities from public timeline, mentions, and searches. Exclude out-of-window mentions and already replied-to mentions. - 时间线热点: grouped by topic with why it matters. - 我的账号动态: notable own posts or engagement. - 数据缺口: failed `xurl` commands, auth/tier errors, rate limits, or items excluded because timestamps were missing/unparseable. @@ -72,13 +74,13 @@ From the repository `skills/` directory: For a one-line Codex install from this beta tag: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.19/twitter-mcp/install.sh | env X_MCP_REGISTER_CODEX=1 X_MCP_REGISTER_CLAUDE=0 sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.20/twitter-mcp/install.sh | env X_MCP_REGISTER_CODEX=1 X_MCP_REGISTER_CLAUDE=0 sh ``` For a one-line Claude Code install from this beta tag: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.19/twitter-mcp/install.sh | env X_MCP_REGISTER_CODEX=0 X_MCP_REGISTER_CLAUDE=1 sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.20/twitter-mcp/install.sh | env X_MCP_REGISTER_CODEX=0 X_MCP_REGISTER_CLAUDE=1 sh ``` The installer: diff --git a/twitter-mcp/install.sh b/twitter-mcp/install.sh index df24e27..a330bb5 100755 --- a/twitter-mcp/install.sh +++ b/twitter-mcp/install.sh @@ -1,7 +1,7 @@ #!/bin/sh set -eu -TAG="${X_MCP_INSTALL_TAG:-v1.5.11-beta.19}" +TAG="${X_MCP_INSTALL_TAG:-v1.5.11-beta.20}" BASE_URL="${X_MCP_INSTALL_BASE_URL:-https://raw.githubusercontent.com/BofAI/skills/${TAG}/twitter-mcp}" REGISTER_CODEX="${X_MCP_REGISTER_CODEX:-1}" REGISTER_CLAUDE="${X_MCP_REGISTER_CLAUDE:-auto}" From 1910585a2ec8ca98b9c3eea24698801cd1e15ee8 Mon Sep 17 00:00:00 2001 From: bobo Date: Wed, 1 Jul 2026 18:41:32 +0800 Subject: [PATCH 25/34] Apply strict mention window rules to twitter digest --- twitter-digest/README.md | 4 +- twitter-digest/SKILL.md | 11 ++++- twitter-digest/install.sh | 2 +- twitter-digest/references/x-twitter-digest.md | 11 ++++- twitter-digest/scripts/digest_context.py | 41 ++++++++++++++++++- twitter-mcp/SKILL.md | 4 +- twitter-mcp/install.sh | 2 +- 7 files changed, 66 insertions(+), 9 deletions(-) diff --git a/twitter-digest/README.md b/twitter-digest/README.md index fc224ce..b788efd 100644 --- a/twitter-digest/README.md +++ b/twitter-digest/README.md @@ -15,7 +15,7 @@ python3 twitter-digest/scripts/install.py To ask Codex to install this skill for itself, paste this into Codex: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.20/twitter-digest/install.sh | env TWITTER_DIGEST_INSTALL_CLIENT=codex sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.21/twitter-digest/install.sh | env TWITTER_DIGEST_INSTALL_CLIENT=codex sh ``` Or use the natural-language prompt: @@ -33,7 +33,7 @@ git clone git@github.com:BofAI/skills.git bofai-skills \ To ask Claude Code to install this skill for itself, paste this into Claude Code: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.20/twitter-digest/install.sh | env TWITTER_DIGEST_INSTALL_CLIENT=claude TWITTER_DIGEST_ALLOW_CLAUDE_COMMANDS=1 TWITTER_DIGEST_ALLOW_CLAUDE_STATE_READ=1 sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.21/twitter-digest/install.sh | env TWITTER_DIGEST_INSTALL_CLIENT=claude TWITTER_DIGEST_ALLOW_CLAUDE_COMMANDS=1 TWITTER_DIGEST_ALLOW_CLAUDE_STATE_READ=1 sh ``` Or use the natural-language prompt: diff --git a/twitter-digest/SKILL.md b/twitter-digest/SKILL.md index 5f0edb6..2b800a7 100644 --- a/twitter-digest/SKILL.md +++ b/twitter-digest/SKILL.md @@ -114,7 +114,13 @@ Default scope: - Today's visible DM conversations, with only conversations whose latest preview is not from the user opened for content. - Optional keyword searches only when the user explicitly passes `--keywords`. -Public timeline/profile/mentions pages use the same daily-window loading model as DMs. API public collection keeps up to 300 items; browser public collection scrolls each public page up to 40 rounds, keeps up to 100 public items, waits for DOM growth after each scroll, and only allows early stop after at least 5 scroll rounds when loaded post timestamps show content beyond the 24-hour digest window (`--scrolls 40`, `--min-public-scrolls 5`, `--max-public-items 100`, `--public-window-hours 24`). +Public timeline/profile/mentions pages use the same daily-window loading model as DMs. API public collection keeps up to 300 items; browser public collection scrolls each public page up to 40 rounds, keeps up to 100 public items, waits for DOM growth after each scroll, and only allows early stop after at least 5 scroll rounds when loaded post timestamps show content beyond the 24-hour digest window (`--scrolls 40`, `--min-public-scrolls 5`, `--max-public-items 100`, `--public-window-hours 24`). The generated digest context applies a second strict filter: public timeline/profile/mention items must be inside `[now - 24 hours, now]` in the user's current local timezone. Items with missing or unparseable timestamps are excluded from final-summary facts and reported as `time-unverified` data gaps. + +Mention handling is strict: + +- Do not include mentions older than the local 24-hour window in `该处理`, `谁 @ 了你`, or reply drafts. +- Do not present an already-replied mention as needing reply. Use own-profile/API own posts, browser-visible reply context, or context metadata when available to decide whether the authenticated account already replied after the mention timestamp. +- If reply status cannot be verified from the current run's data, label the item as `回复状态未确认` instead of claiming the user still needs to reply. Read the installed `digest-context.md` with the file Read tool when writing the Chinese digest. Its `Final Summary Facts` section is the content source for the final summary. If more focused context is needed, use the file Read tool on the split current-run files: `digest-context-timeline.md` for home/profile/timeline items, `digest-context-mentions.md` for @ mentions, and `digest-context-dm.md` for visible DM conversations. Use `digest-input.md` only when debugging collection issues, not during normal summarization. Do not add content from older runs. Do not write ad-hoc `python3 -c`, shell, `cat`, `head`, `tail`, `grep`, or temporary scripts to inspect context structure during normal summarization. If counts or non-content structure must be checked, run the built-in `scripts/inspect_digest.py`, which does not print DM bodies. @@ -209,6 +215,8 @@ Group mentions by reason to care: - Opportunity: partnership, hiring, customer lead, investor/media attention, community praise. - Noise: spam, generic tags, low-context reposts. +Before putting a mention into `✅ 该处理` or `🔴 值得回 / 需要处理`, confirm it is within the local 24-hour window and not already replied to. If it is already replied to, either omit it from action items or mark it as already handled. If the current run cannot verify reply status, mark `回复状态未确认` and avoid phrasing it as definitely needing a reply. + Classify DMs as: - `urgent`: time-sensitive, business-critical, safety/security, money, reputation, or deadline. @@ -248,6 +256,7 @@ For private messages, summarize minimally. Quote only the short phrase needed to - 🔴 值得回 / 需要处理 - 🟡 一般互动 - ⚪ 噪音折叠统计 +- 只包含本地 24 小时窗口内的 mentions;已回复过的 mentions 不再作为待回复提醒 **◆ 私信(DM)** - 会话统计:今日可见会话 N 个,最后我发出 N 个,等我回复 N 个 diff --git a/twitter-digest/install.sh b/twitter-digest/install.sh index a495a29..3fa0e03 100755 --- a/twitter-digest/install.sh +++ b/twitter-digest/install.sh @@ -1,7 +1,7 @@ #!/bin/sh set -eu -TAG="${TWITTER_DIGEST_INSTALL_TAG:-v1.5.11-beta.20}" +TAG="${TWITTER_DIGEST_INSTALL_TAG:-v1.5.11-beta.21}" REPO="${TWITTER_DIGEST_INSTALL_REPO:-https://github.com/BofAI/skills.git}" CLIENT="${TWITTER_DIGEST_INSTALL_CLIENT:-auto}" ALLOW_CLAUDE_COMMANDS="${TWITTER_DIGEST_ALLOW_CLAUDE_COMMANDS:-0}" diff --git a/twitter-digest/references/x-twitter-digest.md b/twitter-digest/references/x-twitter-digest.md index 16cdd00..6c8963a 100644 --- a/twitter-digest/references/x-twitter-digest.md +++ b/twitter-digest/references/x-twitter-digest.md @@ -133,12 +133,21 @@ Privacy rule: do not write long-term memory or daily archives. Raw DM text may e Date rule: - Run dates use the user's local timezone, not UTC. +- Public timeline/profile/mentions facts use a strict `[now - 24 hours, now]` window in the user's current local timezone. +- Public items with missing or unparseable timestamps are excluded from final-summary facts and must be reported as `time-unverified` data gaps. ## Hotspot Detection Cluster home timeline posts by repeated topics, hashtags, URLs, named entities, accounts, and semantic similarity. Optional keyword-search posts may be included only when the user explicitly passes `--keywords`. -Public timeline, profile, and mentions collection follows the same bounded-window model as DM collection: load enough context for a 24-hour digest, cap the amount passed to the model, and stop early when timestamps show older content. Treat public item counts as browser-loaded items in this run, not exhaustive X history. +Public timeline, profile, and mentions collection follows the same bounded-window model as DM collection: load enough context for a 24-hour digest, cap the amount passed to the model, and stop early when timestamps show older content. Treat public item counts as browser-loaded items in this run, not exhaustive X history. The generated digest context applies a second strict local-time 24-hour filter before writing final-summary public facts; do not reintroduce older raw-capture items while summarizing. + +Mention handling is strict: + +- Mentions older than the local 24-hour window must not appear in action items, reply drafts, or the `谁 @ 了你` section. +- Before labeling a mention as needing reply, verify from current-run data whether the authenticated account already replied after the mention timestamp. +- Already-replied mentions should be omitted from action items or marked as already handled. +- If reply status cannot be verified, label the mention as `回复状态未确认` instead of claiming the user still needs to reply. A hotspot needs at least one of: diff --git a/twitter-digest/scripts/digest_context.py b/twitter-digest/scripts/digest_context.py index 1296958..4651ecb 100644 --- a/twitter-digest/scripts/digest_context.py +++ b/twitter-digest/scripts/digest_context.py @@ -4,6 +4,7 @@ from __future__ import annotations import argparse +import datetime as dt import json import re from pathlib import Path @@ -82,6 +83,8 @@ def summarize_current_run(data: dict[str, Any]) -> dict[str, Any]: def build_digest_facts(data: dict[str, Any], summary: dict[str, Any]) -> dict[str, Any]: + now = dt.datetime.now().astimezone() + cutoff = now - dt.timedelta(hours=24) facts: dict[str, Any] = { "schema_version": 1, "run": { @@ -89,6 +92,9 @@ def build_digest_facts(data: dict[str, Any], summary: dict[str, Any]) -> dict[st "date": summary.get("date"), "source": summary.get("source"), "timezone": local_timezone_name(), + "window_start": cutoff.isoformat(), + "window_end": now.isoformat(), + "window_hours": 24, }, "account": { "handle": summary.get("handle") or clean_handle(data.get("handle")), @@ -104,9 +110,11 @@ def build_digest_facts(data: dict[str, Any], summary: dict[str, Any]) -> dict[st "Only threads whose latest preview is not from the user are opened for content.", "Do not treat embedded post authors as DM senders.", "Count low-value waiting-reply DMs but do not expand spam, phishing, generic promotions, or repeated junk.", + "Public timeline/profile/mention items must be inside the local-time 24-hour window.", + "Do not present already-handled mentions as needing reply; if reply status is unclear, label it as unverified.", ], }, - "public": {"counts": summary.get("post_counts") or {}, "items": []}, + "public": {"counts": {}, "loaded_counts": summary.get("post_counts") or {}, "items": []}, "dms": { "status": summary.get("dm_status"), "counts": summary.get("dm_counts") or {}, @@ -201,6 +209,21 @@ def build_digest_facts(data: dict[str, Any], summary: dict[str, Any]) -> dict[st for item in page.get("items") or []: if not isinstance(item, dict): continue + item_time = parse_item_time(item.get("time")) + if item_time is None: + facts["data_gaps"].append( + { + "source": kind, + "status": "time-unverified", + "detail": f"Excluded public item without parseable timestamp: {compact_text(item.get('url') or item.get('text'))[:180]}", + } + ) + continue + local_item_time = item_time.astimezone() + if local_item_time < cutoff or local_item_time > now: + continue + public_counts = facts["public"]["counts"].setdefault(kind, {"total": 0}) + public_counts["total"] = int(public_counts.get("total") or 0) + 1 facts["public"]["items"].append( { "kind": kind, @@ -233,6 +256,19 @@ def build_digest_facts(data: dict[str, Any], summary: dict[str, Any]) -> dict[st return facts +def parse_item_time(value: Any) -> dt.datetime | None: + text = str(value or "").strip() + if not text: + return None + try: + parsed = dt.datetime.fromisoformat(text.replace("Z", "+00:00")) + except ValueError: + return None + if parsed.tzinfo is None: + parsed = parsed.replace(tzinfo=dt.datetime.now().astimezone().tzinfo) + return parsed + + def assess_dm_thread(thread: dict[str, Any]) -> dict[str, Any]: text = compact_text(thread.get("text") or thread.get("label")).lower() if not text: @@ -589,6 +625,9 @@ def render_digest_facts(facts: dict[str, Any]) -> str: f"- generated_at: `{(facts.get('run') or {}).get('generated_at')}`", f"- source: `{(facts.get('run') or {}).get('source') or 'browser'}`", f"- timezone: `{(facts.get('run') or {}).get('timezone')}`", + f"- window_start: `{(facts.get('run') or {}).get('window_start')}`", + f"- window_end: `{(facts.get('run') or {}).get('window_end')}`", + f"- window_hours: `{(facts.get('run') or {}).get('window_hours')}`", f"- account: `@{(facts.get('account') or {}).get('handle') or ''}`", ] lines.extend(["", *render_dm_facts_section(facts).splitlines()]) diff --git a/twitter-mcp/SKILL.md b/twitter-mcp/SKILL.md index f85d70f..cff39cb 100644 --- a/twitter-mcp/SKILL.md +++ b/twitter-mcp/SKILL.md @@ -74,13 +74,13 @@ From the repository `skills/` directory: For a one-line Codex install from this beta tag: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.20/twitter-mcp/install.sh | env X_MCP_REGISTER_CODEX=1 X_MCP_REGISTER_CLAUDE=0 sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.21/twitter-mcp/install.sh | env X_MCP_REGISTER_CODEX=1 X_MCP_REGISTER_CLAUDE=0 sh ``` For a one-line Claude Code install from this beta tag: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.20/twitter-mcp/install.sh | env X_MCP_REGISTER_CODEX=0 X_MCP_REGISTER_CLAUDE=1 sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.21/twitter-mcp/install.sh | env X_MCP_REGISTER_CODEX=0 X_MCP_REGISTER_CLAUDE=1 sh ``` The installer: diff --git a/twitter-mcp/install.sh b/twitter-mcp/install.sh index a330bb5..7b443d9 100755 --- a/twitter-mcp/install.sh +++ b/twitter-mcp/install.sh @@ -1,7 +1,7 @@ #!/bin/sh set -eu -TAG="${X_MCP_INSTALL_TAG:-v1.5.11-beta.20}" +TAG="${X_MCP_INSTALL_TAG:-v1.5.11-beta.21}" BASE_URL="${X_MCP_INSTALL_BASE_URL:-https://raw.githubusercontent.com/BofAI/skills/${TAG}/twitter-mcp}" REGISTER_CODEX="${X_MCP_REGISTER_CODEX:-1}" REGISTER_CLAUDE="${X_MCP_REGISTER_CLAUDE:-auto}" From c9483afee1ecfac7933fabc30bdbe5e4ef35ad36 Mon Sep 17 00:00:00 2001 From: bobo Date: Wed, 1 Jul 2026 19:51:23 +0800 Subject: [PATCH 26/34] Run twitter mcp installer in Terminal from agents --- twitter-digest/README.md | 4 +- twitter-digest/install.sh | 2 +- twitter-mcp/SKILL.md | 8 +-- twitter-mcp/install.sh | 90 ++++++++++++++++++++++++++--- twitter-mcp/scripts/install_xmcp.sh | 10 ++-- 5 files changed, 94 insertions(+), 20 deletions(-) diff --git a/twitter-digest/README.md b/twitter-digest/README.md index b788efd..9510bad 100644 --- a/twitter-digest/README.md +++ b/twitter-digest/README.md @@ -15,7 +15,7 @@ python3 twitter-digest/scripts/install.py To ask Codex to install this skill for itself, paste this into Codex: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.21/twitter-digest/install.sh | env TWITTER_DIGEST_INSTALL_CLIENT=codex sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.22/twitter-digest/install.sh | env TWITTER_DIGEST_INSTALL_CLIENT=codex sh ``` Or use the natural-language prompt: @@ -33,7 +33,7 @@ git clone git@github.com:BofAI/skills.git bofai-skills \ To ask Claude Code to install this skill for itself, paste this into Claude Code: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.21/twitter-digest/install.sh | env TWITTER_DIGEST_INSTALL_CLIENT=claude TWITTER_DIGEST_ALLOW_CLAUDE_COMMANDS=1 TWITTER_DIGEST_ALLOW_CLAUDE_STATE_READ=1 sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.22/twitter-digest/install.sh | env TWITTER_DIGEST_INSTALL_CLIENT=claude TWITTER_DIGEST_ALLOW_CLAUDE_COMMANDS=1 TWITTER_DIGEST_ALLOW_CLAUDE_STATE_READ=1 sh ``` Or use the natural-language prompt: diff --git a/twitter-digest/install.sh b/twitter-digest/install.sh index 3fa0e03..03ef5b2 100755 --- a/twitter-digest/install.sh +++ b/twitter-digest/install.sh @@ -1,7 +1,7 @@ #!/bin/sh set -eu -TAG="${TWITTER_DIGEST_INSTALL_TAG:-v1.5.11-beta.21}" +TAG="${TWITTER_DIGEST_INSTALL_TAG:-v1.5.11-beta.22}" REPO="${TWITTER_DIGEST_INSTALL_REPO:-https://github.com/BofAI/skills.git}" CLIENT="${TWITTER_DIGEST_INSTALL_CLIENT:-auto}" ALLOW_CLAUDE_COMMANDS="${TWITTER_DIGEST_ALLOW_CLAUDE_COMMANDS:-0}" diff --git a/twitter-mcp/SKILL.md b/twitter-mcp/SKILL.md index cff39cb..47a0165 100644 --- a/twitter-mcp/SKILL.md +++ b/twitter-mcp/SKILL.md @@ -74,24 +74,24 @@ From the repository `skills/` directory: For a one-line Codex install from this beta tag: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.21/twitter-mcp/install.sh | env X_MCP_REGISTER_CODEX=1 X_MCP_REGISTER_CLAUDE=0 sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.22/twitter-mcp/install.sh | env X_MCP_REGISTER_CODEX=1 X_MCP_REGISTER_CLAUDE=0 sh ``` For a one-line Claude Code install from this beta tag: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.21/twitter-mcp/install.sh | env X_MCP_REGISTER_CODEX=0 X_MCP_REGISTER_CLAUDE=1 sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.22/twitter-mcp/install.sh | env X_MCP_REGISTER_CODEX=0 X_MCP_REGISTER_CLAUDE=1 sh ``` The installer: - Installs this `twitter-mcp` skill into the selected local skills directory. -- Requires Node.js and npm. +- Requires Node.js 18+ and npm. Node.js 20 LTS+ is recommended. - Installs `@xdevplatform/xurl` globally with npm. - Opens the X OAuth2 authorization flow. - Does not register the hosted X MCP bridge by default. To also register MCP, set `X_MCP_REGISTER_CODEX_MCP=1` or `X_MCP_REGISTER_CLAUDE_MCP=1`. -When launched by Codex or Claude Code on macOS, the installer opens a real Terminal window for OAuth2 Client ID / Secret input and browser authorization. Do not ask the user to paste OAuth credentials into chat. +When launched by Codex, Claude Code, or another non-interactive agent on macOS, the one-line installer immediately opens a real Terminal window and re-runs the full installation there. Node/npm checks, `xurl` installation, OAuth2 Client ID / Secret input, and browser authorization all happen in Terminal, not inside the agent permission sandbox. Do not ask the user to paste OAuth credentials into chat. ## Configuration diff --git a/twitter-mcp/install.sh b/twitter-mcp/install.sh index 7b443d9..cf25318 100755 --- a/twitter-mcp/install.sh +++ b/twitter-mcp/install.sh @@ -1,17 +1,98 @@ #!/bin/sh set -eu -TAG="${X_MCP_INSTALL_TAG:-v1.5.11-beta.21}" +TAG="${X_MCP_INSTALL_TAG:-v1.5.11-beta.22}" BASE_URL="${X_MCP_INSTALL_BASE_URL:-https://raw.githubusercontent.com/BofAI/skills/${TAG}/twitter-mcp}" REGISTER_CODEX="${X_MCP_REGISTER_CODEX:-1}" REGISTER_CLAUDE="${X_MCP_REGISTER_CLAUDE:-auto}" REGISTER_CODEX_MCP="${X_MCP_REGISTER_CODEX_MCP:-0}" REGISTER_CLAUDE_MCP="${X_MCP_REGISTER_CLAUDE_MCP:-0}" +OPEN_TERMINAL="${X_MCP_OPEN_TERMINAL:-auto}" info() { printf '==> %s\n' "$1" } +fail() { + printf 'Error: %s\n' "$1" >&2 + exit 1 +} + +command_exists() { + command -v "$1" >/dev/null 2>&1 +} + +shell_quote() { + value="$1" + printf "'%s'" "$(printf '%s' "$value" | sed "s/'/'\\\\''/g")" +} + +applescript_quote() { + value="$1" + printf '%s' "$value" | sed 's/\\/\\\\/g; s/"/\\"/g' +} + +truthy() { + case "${1:-}" in + 1|true|yes) return 0 ;; + *) return 1 ;; + esac +} + +running_under_agent() { + if [ -n "${CODEX_THREAD_ID:-}" ] || [ -n "${CODEX_CI:-}" ] || [ "${__CFBundleIdentifier:-}" = "com.openai.codex" ]; then + return 0 + fi + if env | grep -Eq '^(CLAUDE|ANTHROPIC)'; then + return 0 + fi + return 1 +} + +should_open_terminal() { + if [ "${X_MCP_TERMINAL_CHILD:-}" = "1" ]; then + return 1 + fi + case "$OPEN_TERMINAL" in + 1|true|yes) return 0 ;; + 0|false|no) return 1 ;; + esac + if [ "$(uname -s)" != "Darwin" ]; then + return 1 + fi + if running_under_agent; then + return 0 + fi + if [ ! -t 0 ]; then + return 0 + fi + return 1 +} + +open_self_in_terminal_and_exit() { + command_exists osascript || fail "Cannot open macOS Terminal because osascript is unavailable." + command_exists curl || fail "curl is required to open the installer in Terminal." + + installer_url="${BASE_URL%/}/install.sh" + args_text="" + for arg in "$@"; do + args_text="${args_text} $(shell_quote "$arg")" + done + command_text="cd ~ && TMPDIR=\"\$(mktemp -d)\" && INSTALL_SH=\"\$TMPDIR/twitter-mcp-install.sh\" && curl -fsSL $(shell_quote "$installer_url") -o \"\$INSTALL_SH\" && chmod 700 \"\$INSTALL_SH\" && env X_MCP_TERMINAL_CHILD=1 X_MCP_OPEN_TERMINAL=0 X_MCP_INSTALL_TAG=$(shell_quote "$TAG") X_MCP_INSTALL_BASE_URL=$(shell_quote "$BASE_URL") X_MCP_REGISTER_CODEX=$(shell_quote "$REGISTER_CODEX") X_MCP_REGISTER_CLAUDE=$(shell_quote "$REGISTER_CLAUDE") X_MCP_REGISTER_CODEX_MCP=$(shell_quote "$REGISTER_CODEX_MCP") X_MCP_REGISTER_CLAUDE_MCP=$(shell_quote "$REGISTER_CLAUDE_MCP") /bin/sh \"\$INSTALL_SH\"${args_text}; printf '\\nPress Enter to close this window...'; IFS= read -r _" + osascript >/dev/null </dev/null 2>&1; then WORKDIR="$(mktemp -d 2>/dev/null || mktemp -d -t twitter-mcp)" else @@ -35,13 +116,6 @@ download_file() { fi } -truthy() { - case "${1:-}" in - 1|true|yes) return 0 ;; - *) return 1 ;; - esac -} - should_install_claude_skill() { if truthy "$REGISTER_CLAUDE"; then return 0 diff --git a/twitter-mcp/scripts/install_xmcp.sh b/twitter-mcp/scripts/install_xmcp.sh index bb5ff8f..f2f4e36 100755 --- a/twitter-mcp/scripts/install_xmcp.sh +++ b/twitter-mcp/scripts/install_xmcp.sh @@ -203,18 +203,18 @@ register_claude() { claude mcp add "$server_name" -- "$xurl_command" --app "$app_name" mcp https://api.x.com/mcp } +if should_open_terminal; then + open_in_terminal_and_exit +fi + if ! command_exists node; then - fail "Node.js is required before installing xurl. Install Node.js, then rerun this script." + fail "Node.js is required before installing xurl. Install Node.js 18 or newer, then rerun this script." fi if ! command_exists npm; then fail "npm is required before installing xurl. Install npm, then rerun this script." fi -if should_open_terminal; then - open_in_terminal_and_exit -fi - info "Installing ${INSTALL_SPEC}" npm install -g "${INSTALL_SPEC}" From 88666dac62652e4e85de323579efca5bfea77c66 Mon Sep 17 00:00:00 2001 From: bobo Date: Wed, 1 Jul 2026 19:58:48 +0800 Subject: [PATCH 27/34] Run digest installer in Terminal from agents --- twitter-digest/README.md | 6 ++-- twitter-digest/SKILL.md | 2 ++ twitter-digest/install.sh | 76 ++++++++++++++++++++++++++++++++++++++- twitter-mcp/SKILL.md | 4 +-- twitter-mcp/install.sh | 2 +- 5 files changed, 84 insertions(+), 6 deletions(-) diff --git a/twitter-digest/README.md b/twitter-digest/README.md index 9510bad..e112bcb 100644 --- a/twitter-digest/README.md +++ b/twitter-digest/README.md @@ -15,9 +15,11 @@ python3 twitter-digest/scripts/install.py To ask Codex to install this skill for itself, paste this into Codex: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.22/twitter-digest/install.sh | env TWITTER_DIGEST_INSTALL_CLIENT=codex sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.23/twitter-digest/install.sh | env TWITTER_DIGEST_INSTALL_CLIENT=codex sh ``` +When this one-line installer is launched from Codex, Claude Code, or another non-interactive agent on macOS, it opens a real Terminal window and re-runs the full installation there. This avoids agent permission/inspect prompts during `git clone`, Python checks, browser checks, and installer writes. Set `TWITTER_DIGEST_OPEN_TERMINAL=0` only when intentionally running in an already interactive Terminal or CI. + Or use the natural-language prompt: ```text @@ -33,7 +35,7 @@ git clone git@github.com:BofAI/skills.git bofai-skills \ To ask Claude Code to install this skill for itself, paste this into Claude Code: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.22/twitter-digest/install.sh | env TWITTER_DIGEST_INSTALL_CLIENT=claude TWITTER_DIGEST_ALLOW_CLAUDE_COMMANDS=1 TWITTER_DIGEST_ALLOW_CLAUDE_STATE_READ=1 sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.23/twitter-digest/install.sh | env TWITTER_DIGEST_INSTALL_CLIENT=claude TWITTER_DIGEST_ALLOW_CLAUDE_COMMANDS=1 TWITTER_DIGEST_ALLOW_CLAUDE_STATE_READ=1 sh ``` Or use the natural-language prompt: diff --git a/twitter-digest/SKILL.md b/twitter-digest/SKILL.md index 2b800a7..fc53064 100644 --- a/twitter-digest/SKILL.md +++ b/twitter-digest/SKILL.md @@ -132,6 +132,8 @@ From the repository `skills/` directory: python3 twitter-digest/scripts/install.py ``` +For one-line installs through `twitter-digest/install.sh`, Codex, Claude Code, and other non-interactive macOS agents open a real Terminal window and re-run the full installation there. This keeps `git clone`, Python/browser prerequisite checks, and skill-directory writes out of the agent permission sandbox. To force direct in-process installation from an already interactive terminal or CI, set `TWITTER_DIGEST_OPEN_TERMINAL=0`. + Default install targets the current agent client: Codex installs to `~/.codex/skills/twitter-digest`, Claude Code installs to `~/.claude/skills/twitter-digest`. Use `--client codex`, `--client claude`, or `--skills-dir` to override. Local development can use `--symlink`. For Claude Code, the skill cannot silently grant itself Bash permission or file access outside the project. On first use, approve the visible `run_daily_digest.py` command and choose "don't ask again" if appropriate. For a global opt-in during install, run: diff --git a/twitter-digest/install.sh b/twitter-digest/install.sh index 03ef5b2..e8ce702 100755 --- a/twitter-digest/install.sh +++ b/twitter-digest/install.sh @@ -1,17 +1,37 @@ #!/bin/sh set -eu -TAG="${TWITTER_DIGEST_INSTALL_TAG:-v1.5.11-beta.22}" +TAG="${TWITTER_DIGEST_INSTALL_TAG:-v1.5.11-beta.23}" REPO="${TWITTER_DIGEST_INSTALL_REPO:-https://github.com/BofAI/skills.git}" CLIENT="${TWITTER_DIGEST_INSTALL_CLIENT:-auto}" ALLOW_CLAUDE_COMMANDS="${TWITTER_DIGEST_ALLOW_CLAUDE_COMMANDS:-0}" ALLOW_CLAUDE_STATE_READ="${TWITTER_DIGEST_ALLOW_CLAUDE_STATE_READ:-0}" SKIP_BROWSER_CHECK="${TWITTER_DIGEST_SKIP_BROWSER_CHECK:-0}" +OPEN_TERMINAL="${TWITTER_DIGEST_OPEN_TERMINAL:-auto}" info() { printf '==> %s\n' "$1" } +fail() { + printf 'Error: %s\n' "$1" >&2 + exit 1 +} + +command_exists() { + command -v "$1" >/dev/null 2>&1 +} + +shell_quote() { + value="$1" + printf "'%s'" "$(printf '%s' "$value" | sed "s/'/'\\\\''/g")" +} + +applescript_quote() { + value="$1" + printf '%s' "$value" | sed 's/\\/\\\\/g; s/"/\\"/g' +} + truthy() { case "${1:-}" in 1|true|yes) return 0 ;; @@ -19,6 +39,60 @@ truthy() { esac } +running_under_agent() { + if [ -n "${CODEX_THREAD_ID:-}" ] || [ -n "${CODEX_CI:-}" ] || [ "${__CFBundleIdentifier:-}" = "com.openai.codex" ]; then + return 0 + fi + if env | grep -Eq '^(CLAUDE|ANTHROPIC)'; then + return 0 + fi + return 1 +} + +should_open_terminal() { + if [ "${TWITTER_DIGEST_TERMINAL_CHILD:-}" = "1" ]; then + return 1 + fi + case "$OPEN_TERMINAL" in + 1|true|yes) return 0 ;; + 0|false|no) return 1 ;; + esac + if [ "$(uname -s)" != "Darwin" ]; then + return 1 + fi + if running_under_agent; then + return 0 + fi + if [ ! -t 0 ]; then + return 0 + fi + return 1 +} + +open_self_in_terminal_and_exit() { + command_exists osascript || fail "Cannot open macOS Terminal because osascript is unavailable." + command_exists curl || fail "curl is required to open the installer in Terminal." + + installer_url="https://raw.githubusercontent.com/BofAI/skills/${TAG}/twitter-digest/install.sh" + args_text="" + for arg in "$@"; do + args_text="${args_text} $(shell_quote "$arg")" + done + command_text="cd ~ && TMPDIR=\"\$(mktemp -d)\" && INSTALL_SH=\"\$TMPDIR/twitter-digest-install.sh\" && curl -fsSL $(shell_quote "$installer_url") -o \"\$INSTALL_SH\" && chmod 700 \"\$INSTALL_SH\" && env TWITTER_DIGEST_TERMINAL_CHILD=1 TWITTER_DIGEST_OPEN_TERMINAL=0 TWITTER_DIGEST_INSTALL_TAG=$(shell_quote "$TAG") TWITTER_DIGEST_INSTALL_REPO=$(shell_quote "$REPO") TWITTER_DIGEST_INSTALL_CLIENT=$(shell_quote "$CLIENT") TWITTER_DIGEST_ALLOW_CLAUDE_COMMANDS=$(shell_quote "$ALLOW_CLAUDE_COMMANDS") TWITTER_DIGEST_ALLOW_CLAUDE_STATE_READ=$(shell_quote "$ALLOW_CLAUDE_STATE_READ") TWITTER_DIGEST_SKIP_BROWSER_CHECK=$(shell_quote "$SKIP_BROWSER_CHECK") /bin/sh \"\$INSTALL_SH\"${args_text}; printf '\\nPress Enter to close this window...'; IFS= read -r _" + osascript >/dev/null </dev/null 2>&1; then printf 'Error: git is required to install twitter-digest.\n' >&2 exit 1 diff --git a/twitter-mcp/SKILL.md b/twitter-mcp/SKILL.md index 47a0165..d5fd488 100644 --- a/twitter-mcp/SKILL.md +++ b/twitter-mcp/SKILL.md @@ -74,13 +74,13 @@ From the repository `skills/` directory: For a one-line Codex install from this beta tag: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.22/twitter-mcp/install.sh | env X_MCP_REGISTER_CODEX=1 X_MCP_REGISTER_CLAUDE=0 sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.23/twitter-mcp/install.sh | env X_MCP_REGISTER_CODEX=1 X_MCP_REGISTER_CLAUDE=0 sh ``` For a one-line Claude Code install from this beta tag: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.22/twitter-mcp/install.sh | env X_MCP_REGISTER_CODEX=0 X_MCP_REGISTER_CLAUDE=1 sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.23/twitter-mcp/install.sh | env X_MCP_REGISTER_CODEX=0 X_MCP_REGISTER_CLAUDE=1 sh ``` The installer: diff --git a/twitter-mcp/install.sh b/twitter-mcp/install.sh index cf25318..701b46b 100755 --- a/twitter-mcp/install.sh +++ b/twitter-mcp/install.sh @@ -1,7 +1,7 @@ #!/bin/sh set -eu -TAG="${X_MCP_INSTALL_TAG:-v1.5.11-beta.22}" +TAG="${X_MCP_INSTALL_TAG:-v1.5.11-beta.23}" BASE_URL="${X_MCP_INSTALL_BASE_URL:-https://raw.githubusercontent.com/BofAI/skills/${TAG}/twitter-mcp}" REGISTER_CODEX="${X_MCP_REGISTER_CODEX:-1}" REGISTER_CLAUDE="${X_MCP_REGISTER_CLAUDE:-auto}" From 9fde9e8c995a1c3c02e70837c0831403dc90b15e Mon Sep 17 00:00:00 2001 From: bobo Date: Wed, 1 Jul 2026 20:02:36 +0800 Subject: [PATCH 28/34] Mark handled mentions before digest summaries --- twitter-digest/README.md | 4 +- twitter-digest/SKILL.md | 4 +- twitter-digest/install.sh | 2 +- twitter-digest/references/x-twitter-digest.md | 2 + twitter-digest/scripts/api_x_digest.py | 8 ++ twitter-digest/scripts/digest_context.py | 103 +++++++++++++++--- .../scripts/dom_scripts/extract_articles.js | 17 ++- twitter-mcp/SKILL.md | 7 +- twitter-mcp/install.sh | 2 +- 9 files changed, 125 insertions(+), 24 deletions(-) diff --git a/twitter-digest/README.md b/twitter-digest/README.md index e112bcb..bddb0e1 100644 --- a/twitter-digest/README.md +++ b/twitter-digest/README.md @@ -15,7 +15,7 @@ python3 twitter-digest/scripts/install.py To ask Codex to install this skill for itself, paste this into Codex: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.23/twitter-digest/install.sh | env TWITTER_DIGEST_INSTALL_CLIENT=codex sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.24/twitter-digest/install.sh | env TWITTER_DIGEST_INSTALL_CLIENT=codex sh ``` When this one-line installer is launched from Codex, Claude Code, or another non-interactive agent on macOS, it opens a real Terminal window and re-runs the full installation there. This avoids agent permission/inspect prompts during `git clone`, Python checks, browser checks, and installer writes. Set `TWITTER_DIGEST_OPEN_TERMINAL=0` only when intentionally running in an already interactive Terminal or CI. @@ -35,7 +35,7 @@ git clone git@github.com:BofAI/skills.git bofai-skills \ To ask Claude Code to install this skill for itself, paste this into Claude Code: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.23/twitter-digest/install.sh | env TWITTER_DIGEST_INSTALL_CLIENT=claude TWITTER_DIGEST_ALLOW_CLAUDE_COMMANDS=1 TWITTER_DIGEST_ALLOW_CLAUDE_STATE_READ=1 sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.24/twitter-digest/install.sh | env TWITTER_DIGEST_INSTALL_CLIENT=claude TWITTER_DIGEST_ALLOW_CLAUDE_COMMANDS=1 TWITTER_DIGEST_ALLOW_CLAUDE_STATE_READ=1 sh ``` Or use the natural-language prompt: diff --git a/twitter-digest/SKILL.md b/twitter-digest/SKILL.md index fc53064..74f5d45 100644 --- a/twitter-digest/SKILL.md +++ b/twitter-digest/SKILL.md @@ -120,6 +120,8 @@ Mention handling is strict: - Do not include mentions older than the local 24-hour window in `该处理`, `谁 @ 了你`, or reply drafts. - Do not present an already-replied mention as needing reply. Use own-profile/API own posts, browser-visible reply context, or context metadata when available to decide whether the authenticated account already replied after the mention timestamp. +- When `digest-context.md` marks a mention with `reply_state=already_replied` or `action_state=handled`, it must not appear as a pending reply opportunity. It may be omitted or summarized as already handled. +- When `digest-context.md` marks `reply_state=reply_unverified`, do not say the user "needs to reply"; write `回复状态未确认` and suggest review only if the content is important. - If reply status cannot be verified from the current run's data, label the item as `回复状态未确认` instead of claiming the user still needs to reply. Read the installed `digest-context.md` with the file Read tool when writing the Chinese digest. Its `Final Summary Facts` section is the content source for the final summary. If more focused context is needed, use the file Read tool on the split current-run files: `digest-context-timeline.md` for home/profile/timeline items, `digest-context-mentions.md` for @ mentions, and `digest-context-dm.md` for visible DM conversations. Use `digest-input.md` only when debugging collection issues, not during normal summarization. Do not add content from older runs. Do not write ad-hoc `python3 -c`, shell, `cat`, `head`, `tail`, `grep`, or temporary scripts to inspect context structure during normal summarization. If counts or non-content structure must be checked, run the built-in `scripts/inspect_digest.py`, which does not print DM bodies. @@ -217,7 +219,7 @@ Group mentions by reason to care: - Opportunity: partnership, hiring, customer lead, investor/media attention, community praise. - Noise: spam, generic tags, low-context reposts. -Before putting a mention into `✅ 该处理` or `🔴 值得回 / 需要处理`, confirm it is within the local 24-hour window and not already replied to. If it is already replied to, either omit it from action items or mark it as already handled. If the current run cannot verify reply status, mark `回复状态未确认` and avoid phrasing it as definitely needing a reply. +Before putting a mention into `✅ 该处理` or `🔴 值得回 / 需要处理`, confirm it is within the local 24-hour window and not already replied to. If it is already replied to, either omit it from action items or mark it as already handled. If the current run cannot verify reply status, mark `回复状态未确认` and avoid phrasing it as definitely needing a reply. For mentions, treat `reply_state` / `action_state` in `digest-context.md` as authoritative over the text of the mention. Classify DMs as: diff --git a/twitter-digest/install.sh b/twitter-digest/install.sh index e8ce702..36278a2 100755 --- a/twitter-digest/install.sh +++ b/twitter-digest/install.sh @@ -1,7 +1,7 @@ #!/bin/sh set -eu -TAG="${TWITTER_DIGEST_INSTALL_TAG:-v1.5.11-beta.23}" +TAG="${TWITTER_DIGEST_INSTALL_TAG:-v1.5.11-beta.24}" REPO="${TWITTER_DIGEST_INSTALL_REPO:-https://github.com/BofAI/skills.git}" CLIENT="${TWITTER_DIGEST_INSTALL_CLIENT:-auto}" ALLOW_CLAUDE_COMMANDS="${TWITTER_DIGEST_ALLOW_CLAUDE_COMMANDS:-0}" diff --git a/twitter-digest/references/x-twitter-digest.md b/twitter-digest/references/x-twitter-digest.md index 6c8963a..bd5824c 100644 --- a/twitter-digest/references/x-twitter-digest.md +++ b/twitter-digest/references/x-twitter-digest.md @@ -147,6 +147,8 @@ Mention handling is strict: - Mentions older than the local 24-hour window must not appear in action items, reply drafts, or the `谁 @ 了你` section. - Before labeling a mention as needing reply, verify from current-run data whether the authenticated account already replied after the mention timestamp. - Already-replied mentions should be omitted from action items or marked as already handled. +- If `digest-context.md` includes `reply_state=already_replied` or `action_state=handled`, the mention is not a pending reply. Do not include it in `✅ 该处理` as a reply task. +- If `digest-context.md` includes `reply_state=reply_unverified`, report `回复状态未确认`; do not claim the user definitely needs to reply. - If reply status cannot be verified, label the mention as `回复状态未确认` instead of claiming the user still needs to reply. A hotspot needs at least one of: diff --git a/twitter-digest/scripts/api_x_digest.py b/twitter-digest/scripts/api_x_digest.py index 904a8bb..2ce415e 100644 --- a/twitter-digest/scripts/api_x_digest.py +++ b/twitter-digest/scripts/api_x_digest.py @@ -206,6 +206,9 @@ def normalize_tweets(raw: list[dict[str, Any]], includes: dict[str, Any], source out.append( { "api_source": source_kind, + "id": tweet_id, + "conversation_id": str(tweet.get("conversation_id") or ""), + "author_username": username, "text": text, "metrics": metrics, "url": url, @@ -215,6 +218,11 @@ def normalize_tweets(raw: list[dict[str, Any]], includes: dict[str, Any], source "cards": cards, "time": tweet.get("created_at"), "authorUrl": f"https://x.com/{username}" if username else "", + "referenced_tweets": [ + {"id": str(ref.get("id") or ""), "type": str(ref.get("type") or "")} + for ref in (tweet.get("referenced_tweets") or []) + if isinstance(ref, dict) + ], } ) return out diff --git a/twitter-digest/scripts/digest_context.py b/twitter-digest/scripts/digest_context.py index 4651ecb..4a5112f 100644 --- a/twitter-digest/scripts/digest_context.py +++ b/twitter-digest/scripts/digest_context.py @@ -85,6 +85,7 @@ def summarize_current_run(data: dict[str, Any]) -> dict[str, Any]: def build_digest_facts(data: dict[str, Any], summary: dict[str, Any]) -> dict[str, Any]: now = dt.datetime.now().astimezone() cutoff = now - dt.timedelta(hours=24) + public_items: list[dict[str, Any]] = [] facts: dict[str, Any] = { "schema_version": 1, "run": { @@ -224,19 +225,8 @@ def build_digest_facts(data: dict[str, Any], summary: dict[str, Any]) -> dict[st continue public_counts = facts["public"]["counts"].setdefault(kind, {"total": 0}) public_counts["total"] = int(public_counts.get("total") or 0) + 1 - facts["public"]["items"].append( - { - "kind": kind, - "time": format_local_time(item.get("time")), - "raw_time": item.get("time") or "", - "url": item.get("url") or "", - "author_url": item.get("authorUrl") or "", - "text_excerpt": compact_text(item.get("text"))[:700], - "external_links": normalize_context_assets(item.get("externalLinks")), - "media": normalize_context_assets(item.get("media")), - "cards": normalize_context_assets(item.get("cards")), - } - ) + public_items.append(normalize_public_item(kind, item)) + facts["public"]["items"] = annotate_public_reply_states(public_items, str((facts.get("account") or {}).get("handle") or "")) if (summary.get("dm_status") or "") in {"blocked_by_x_chat_passcode", "visible_threads_unopened", "no_visible_threads", "dm_page_loading_timeout", "api_dm_unavailable", "api_dm_error", "api_dm_todo"}: facts["data_gaps"].append( { @@ -256,6 +246,83 @@ def build_digest_facts(data: dict[str, Any], summary: dict[str, Any]) -> dict[st return facts +def normalize_public_item(kind: str, item: dict[str, Any]) -> dict[str, Any]: + author_username = clean_handle(item.get("author_username") or item.get("authorUsername") or author_from_url(item.get("authorUrl"))) + references = item.get("referenced_tweets") if isinstance(item.get("referenced_tweets"), list) else [] + return { + "kind": kind, + "id": compact_text(item.get("id")), + "conversation_id": compact_text(item.get("conversation_id")), + "author_username": author_username, + "time": format_local_time(item.get("time")), + "raw_time": item.get("time") or "", + "url": item.get("url") or "", + "author_url": item.get("authorUrl") or "", + "text_excerpt": compact_text(item.get("text"))[:700], + "referenced_tweets": [ + {"id": compact_text(ref.get("id")), "type": compact_text(ref.get("type"))} + for ref in references + if isinstance(ref, dict) + ], + "external_links": normalize_context_assets(item.get("externalLinks")), + "media": normalize_context_assets(item.get("media")), + "cards": normalize_context_assets(item.get("cards")), + } + + +def annotate_public_reply_states(items: list[dict[str, Any]], handle: str) -> list[dict[str, Any]]: + clean_self = clean_handle(handle).lower() + own_items = [item for item in items if is_own_public_item(item, clean_self)] + for item in items: + if "mention" not in str(item.get("kind") or "").lower(): + continue + evidence = find_reply_evidence(item, own_items) + if evidence: + item["reply_state"] = "already_replied" + item["action_state"] = "handled" + item["reply_evidence"] = evidence + else: + item["reply_state"] = "reply_unverified" + item["action_state"] = "review_without_claiming_needs_reply" + return items + + +def is_own_public_item(item: dict[str, Any], clean_self: str) -> bool: + kind = str(item.get("kind") or "").lower() + author = clean_handle(item.get("author_username")).lower() + return kind == "own_profile" or (bool(clean_self) and author == clean_self) + + +def find_reply_evidence(mention: dict[str, Any], own_items: list[dict[str, Any]]) -> str: + mention_time = parse_item_time(mention.get("raw_time")) + mention_id = compact_text(mention.get("id")) + conversation_id = compact_text(mention.get("conversation_id")) + mention_author = clean_handle(mention.get("author_username")).lower() + for own in own_items: + own_time = parse_item_time(own.get("raw_time")) + if mention_time and own_time and own_time <= mention_time: + continue + if mention_id and referenced_ids(own) and mention_id in referenced_ids(own): + return f"own reply references mention tweet {mention_id}" + own_conversation_id = compact_text(own.get("conversation_id")) + if conversation_id and own_conversation_id and own_conversation_id == conversation_id and compact_text(own.get("id")) != mention_id: + return f"own post appears later in same conversation {conversation_id}" + if mention_author and f"@{mention_author}" in str(own.get("text_excerpt") or "").lower(): + return f"own post mentions @{mention_author} after the mention" + return "" + + +def referenced_ids(item: dict[str, Any]) -> set[str]: + refs = item.get("referenced_tweets") if isinstance(item.get("referenced_tweets"), list) else [] + return {compact_text(ref.get("id")) for ref in refs if isinstance(ref, dict) and ref.get("id")} + + +def author_from_url(value: Any) -> str: + text = str(value or "").strip() + match = re.search(r"https?://(?:www\.)?(?:x|twitter)\.com/([^/?#]+)", text) + return match.group(1) if match else "" + + def parse_item_time(value: Any) -> dt.datetime | None: text = str(value or "").strip() if not text: @@ -573,7 +640,10 @@ def render_public_slice_section(facts: dict[str, Any], slice_name: str) -> str: lines.append("| none | 0 |") lines.extend(["", "## Public Items", ""]) for item in items[:300]: - lines.append(f"- `{item.get('kind')}` `{item.get('time')}` {item.get('url') or '[no url]'} - {item.get('text_excerpt')}") + reply_state = f" reply_state=`{item.get('reply_state')}` action_state=`{item.get('action_state')}`" if item.get("reply_state") else "" + evidence = f" reply_evidence={item.get('reply_evidence')}" if item.get("reply_evidence") else "" + author = f" @{item.get('author_username')}" if item.get("author_username") else "" + lines.append(f"- `{item.get('kind')}` `{item.get('time')}`{author}{reply_state}{evidence} {item.get('url') or '[no url]'} - {item.get('text_excerpt')}") for asset in item.get("media") or []: alt = f" alt={asset.get('alt')}" if asset.get("alt") else "" poster = f" poster={asset.get('poster')}" if asset.get("poster") else "" @@ -644,7 +714,10 @@ def render_digest_facts(facts: dict[str, Any]) -> str: lines.extend(["", "## Public Items", ""]) for item in ((facts.get("public") or {}).get("items") or [])[:300]: - lines.append(f"- `{item.get('kind')}` `{item.get('time')}` {item.get('url') or '[no url]'} - {item.get('text_excerpt')}") + reply_state = f" reply_state=`{item.get('reply_state')}` action_state=`{item.get('action_state')}`" if item.get("reply_state") else "" + evidence = f" reply_evidence={item.get('reply_evidence')}" if item.get("reply_evidence") else "" + author = f" @{item.get('author_username')}" if item.get("author_username") else "" + lines.append(f"- `{item.get('kind')}` `{item.get('time')}`{author}{reply_state}{evidence} {item.get('url') or '[no url]'} - {item.get('text_excerpt')}") for asset in item.get("media") or []: alt = f" alt={asset.get('alt')}" if asset.get("alt") else "" poster = f" poster={asset.get('poster')}" if asset.get("poster") else "" diff --git a/twitter-digest/scripts/dom_scripts/extract_articles.js b/twitter-digest/scripts/dom_scripts/extract_articles.js index bfd0ea9..d1818fa 100644 --- a/twitter-digest/scripts/dom_scripts/extract_articles.js +++ b/twitter-digest/scripts/dom_scripts/extract_articles.js @@ -68,6 +68,13 @@ const text = (article.innerText || '').trim(); const links = Array.from(article.querySelectorAll('a[href]')).map(a => a.href).filter(Boolean); const status = links.map(statusUrl).find(Boolean) || null; + let tweetId = null; + if (status) { + try { + const match = new URL(status).pathname.match(/\/status\/(\d+)/); + tweetId = match ? match[1] : null; + } catch {} + } const times = Array.from(article.querySelectorAll('time')).map(t => t.getAttribute('datetime')).filter(Boolean); const authorLinks = links.filter(h => { try { @@ -75,7 +82,14 @@ return /^\/[^/]+$/.test(p) && !p.includes('/i/'); } catch { return false; } }); + let authorUsername = null; + if (authorLinks[0]) { + try { + authorUsername = new URL(authorLinks[0]).pathname.split('/').filter(Boolean)[0] || null; + } catch {} + } return { + id: tweetId, text, url: status, links, @@ -83,7 +97,8 @@ media: mediaInfo(article), cards: cardInfo(article), time: times[0] || null, - authorUrl: authorLinks[0] || null + authorUrl: authorLinks[0] || null, + authorUsername }; }).filter(item => item.text); })() diff --git a/twitter-mcp/SKILL.md b/twitter-mcp/SKILL.md index d5fd488..92aab31 100644 --- a/twitter-mcp/SKILL.md +++ b/twitter-mcp/SKILL.md @@ -47,6 +47,7 @@ Rules for collection: - If an item has no parseable timestamp, do not use it for time-bound facts; report it under data gaps as time-unverified. - When using `xurl search`, search date operators may be used only as a coarse prefilter; still post-filter each returned item to the exact 24-hour window. - Before adding any mention or direct ask to `需要处理` as "needs reply", verify whether the authenticated account has already replied after that mention's timestamp. Use `xurl posts ` and `xurl search "from: to:" -n 100` or equivalent `xurl` output to check for replies in the same conversation/thread or to the same author. If a reply from the authenticated account already exists after the mention timestamp, mark it as already handled or omit it from `需要处理`; do not ask the user to reply again. +- For every mention considered for `需要处理`, write down the reply verification result before summarizing: `already_replied`, `reply_unverified`, or `not_replied_found`. Only `not_replied_found` may be phrased as a pending reply. `already_replied` must be omitted from pending actions or shown as already handled. `reply_unverified` must be labeled `回复状态未确认`. - If the reply status cannot be verified from available `xurl` output, label it as `回复状态未确认` instead of claiming the user still needs to reply. - If a specific `xurl` command fails, report that command under data gaps and continue with successful command outputs. - Do not run the DM command during normal daily digest collection. Current DM/API coverage is not reliable for this workflow and should not appear in the daily digest. @@ -57,7 +58,7 @@ Rules for collection: Write the final response in Chinese by default for `X日报` requests. Use this structure: - 今日概览: 3-6 bullets with the highest-signal changes. -- 需要处理: unresolved direct asks, risks, and reply opportunities from public timeline, mentions, and searches. Exclude out-of-window mentions and already replied-to mentions. +- 需要处理: unresolved direct asks, risks, and reply opportunities from public timeline, mentions, and searches. Exclude out-of-window mentions and already replied-to mentions. For mention rows, include the reply verification state; do not call a mention a reply task unless verification found no later reply from the authenticated account. - 时间线热点: grouped by topic with why it matters. - 我的账号动态: notable own posts or engagement. - 数据缺口: failed `xurl` commands, auth/tier errors, rate limits, or items excluded because timestamps were missing/unparseable. @@ -74,13 +75,13 @@ From the repository `skills/` directory: For a one-line Codex install from this beta tag: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.23/twitter-mcp/install.sh | env X_MCP_REGISTER_CODEX=1 X_MCP_REGISTER_CLAUDE=0 sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.24/twitter-mcp/install.sh | env X_MCP_REGISTER_CODEX=1 X_MCP_REGISTER_CLAUDE=0 sh ``` For a one-line Claude Code install from this beta tag: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.23/twitter-mcp/install.sh | env X_MCP_REGISTER_CODEX=0 X_MCP_REGISTER_CLAUDE=1 sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.24/twitter-mcp/install.sh | env X_MCP_REGISTER_CODEX=0 X_MCP_REGISTER_CLAUDE=1 sh ``` The installer: diff --git a/twitter-mcp/install.sh b/twitter-mcp/install.sh index 701b46b..8483b57 100755 --- a/twitter-mcp/install.sh +++ b/twitter-mcp/install.sh @@ -1,7 +1,7 @@ #!/bin/sh set -eu -TAG="${X_MCP_INSTALL_TAG:-v1.5.11-beta.23}" +TAG="${X_MCP_INSTALL_TAG:-v1.5.11-beta.24}" BASE_URL="${X_MCP_INSTALL_BASE_URL:-https://raw.githubusercontent.com/BofAI/skills/${TAG}/twitter-mcp}" REGISTER_CODEX="${X_MCP_REGISTER_CODEX:-1}" REGISTER_CLAUDE="${X_MCP_REGISTER_CLAUDE:-auto}" From b3a445b4105eaa9c13fec3fb4deed5ad775fecaa Mon Sep 17 00:00:00 2001 From: bobo Date: Wed, 1 Jul 2026 20:21:09 +0800 Subject: [PATCH 29/34] Require complete mention search before digest output --- twitter-digest/README.md | 4 +-- twitter-digest/SKILL.md | 3 ++ twitter-digest/install.sh | 2 +- twitter-digest/references/x-twitter-digest.md | 2 ++ twitter-digest/scripts/digest_context.py | 36 +++++++++++++++++++ twitter-mcp/SKILL.md | 8 +++-- twitter-mcp/install.sh | 2 +- 7 files changed, 51 insertions(+), 6 deletions(-) diff --git a/twitter-digest/README.md b/twitter-digest/README.md index bddb0e1..fced213 100644 --- a/twitter-digest/README.md +++ b/twitter-digest/README.md @@ -15,7 +15,7 @@ python3 twitter-digest/scripts/install.py To ask Codex to install this skill for itself, paste this into Codex: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.24/twitter-digest/install.sh | env TWITTER_DIGEST_INSTALL_CLIENT=codex sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.25/twitter-digest/install.sh | env TWITTER_DIGEST_INSTALL_CLIENT=codex sh ``` When this one-line installer is launched from Codex, Claude Code, or another non-interactive agent on macOS, it opens a real Terminal window and re-runs the full installation there. This avoids agent permission/inspect prompts during `git clone`, Python checks, browser checks, and installer writes. Set `TWITTER_DIGEST_OPEN_TERMINAL=0` only when intentionally running in an already interactive Terminal or CI. @@ -35,7 +35,7 @@ git clone git@github.com:BofAI/skills.git bofai-skills \ To ask Claude Code to install this skill for itself, paste this into Claude Code: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.24/twitter-digest/install.sh | env TWITTER_DIGEST_INSTALL_CLIENT=claude TWITTER_DIGEST_ALLOW_CLAUDE_COMMANDS=1 TWITTER_DIGEST_ALLOW_CLAUDE_STATE_READ=1 sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.25/twitter-digest/install.sh | env TWITTER_DIGEST_INSTALL_CLIENT=claude TWITTER_DIGEST_ALLOW_CLAUDE_COMMANDS=1 TWITTER_DIGEST_ALLOW_CLAUDE_STATE_READ=1 sh ``` Or use the natural-language prompt: diff --git a/twitter-digest/SKILL.md b/twitter-digest/SKILL.md index 74f5d45..b4852e2 100644 --- a/twitter-digest/SKILL.md +++ b/twitter-digest/SKILL.md @@ -118,6 +118,7 @@ Public timeline/profile/mentions pages use the same daily-window loading model a Mention handling is strict: +- Both mention sources must be considered when available: direct mention/notification collection and handle search (`@handle` / equivalent search page or API recent search). Do not conclude "no current mentions" from only one source unless the other source was attempted and failed or is unavailable. - Do not include mentions older than the local 24-hour window in `该处理`, `谁 @ 了你`, or reply drafts. - Do not present an already-replied mention as needing reply. Use own-profile/API own posts, browser-visible reply context, or context metadata when available to decide whether the authenticated account already replied after the mention timestamp. - When `digest-context.md` marks a mention with `reply_state=already_replied` or `action_state=handled`, it must not appear as a pending reply opportunity. It may be omitted or summarized as already handled. @@ -126,6 +127,8 @@ Mention handling is strict: Read the installed `digest-context.md` with the file Read tool when writing the Chinese digest. Its `Final Summary Facts` section is the content source for the final summary. If more focused context is needed, use the file Read tool on the split current-run files: `digest-context-timeline.md` for home/profile/timeline items, `digest-context-mentions.md` for @ mentions, and `digest-context-dm.md` for visible DM conversations. Use `digest-input.md` only when debugging collection issues, not during normal summarization. Do not add content from older runs. Do not write ad-hoc `python3 -c`, shell, `cat`, `head`, `tail`, `grep`, or temporary scripts to inspect context structure during normal summarization. If counts or non-content structure must be checked, run the built-in `scripts/inspect_digest.py`, which does not print DM bodies. +If `digest-context.md` or `digest-context-mentions.md` shows missing mention sources, collection errors for `mentions_search` / `mentions_notifications`, or only stale mention data, report that as a data gap. Do not turn stale mentions into action items. + ## Install From the repository `skills/` directory: diff --git a/twitter-digest/install.sh b/twitter-digest/install.sh index 36278a2..cd4c93f 100755 --- a/twitter-digest/install.sh +++ b/twitter-digest/install.sh @@ -1,7 +1,7 @@ #!/bin/sh set -eu -TAG="${TWITTER_DIGEST_INSTALL_TAG:-v1.5.11-beta.24}" +TAG="${TWITTER_DIGEST_INSTALL_TAG:-v1.5.11-beta.25}" REPO="${TWITTER_DIGEST_INSTALL_REPO:-https://github.com/BofAI/skills.git}" CLIENT="${TWITTER_DIGEST_INSTALL_CLIENT:-auto}" ALLOW_CLAUDE_COMMANDS="${TWITTER_DIGEST_ALLOW_CLAUDE_COMMANDS:-0}" diff --git a/twitter-digest/references/x-twitter-digest.md b/twitter-digest/references/x-twitter-digest.md index bd5824c..ece8e7c 100644 --- a/twitter-digest/references/x-twitter-digest.md +++ b/twitter-digest/references/x-twitter-digest.md @@ -144,6 +144,8 @@ Public timeline, profile, and mentions collection follows the same bounded-windo Mention handling is strict: +- Direct mention/notification collection and handle search (`@handle` / equivalent search page or API recent search) are complementary. Use both sources when available before concluding there are no current mentions or no reply opportunities. +- If one mention source is missing, failed, unavailable, or only returned stale data, report the source status as a data gap and do not turn stale mentions into action items. - Mentions older than the local 24-hour window must not appear in action items, reply drafts, or the `谁 @ 了你` section. - Before labeling a mention as needing reply, verify from current-run data whether the authenticated account already replied after the mention timestamp. - Already-replied mentions should be omitted from action items or marked as already handled. diff --git a/twitter-digest/scripts/digest_context.py b/twitter-digest/scripts/digest_context.py index 4a5112f..0a9d96c 100644 --- a/twitter-digest/scripts/digest_context.py +++ b/twitter-digest/scripts/digest_context.py @@ -86,6 +86,8 @@ def build_digest_facts(data: dict[str, Any], summary: dict[str, Any]) -> dict[st now = dt.datetime.now().astimezone() cutoff = now - dt.timedelta(hours=24) public_items: list[dict[str, Any]] = [] + loaded_public_kinds: set[str] = set() + kept_public_counts: dict[str, int] = {} facts: dict[str, Any] = { "schema_version": 1, "run": { @@ -129,6 +131,8 @@ def build_digest_facts(data: dict[str, Any], summary: dict[str, Any]) -> dict[st if not isinstance(page, dict): continue kind = str(page.get("kind") or "unknown") + if kind != "messages": + loaded_public_kinds.add(kind) if page.get("collection_error"): facts["data_gaps"].append( { @@ -225,8 +229,10 @@ def build_digest_facts(data: dict[str, Any], summary: dict[str, Any]) -> dict[st continue public_counts = facts["public"]["counts"].setdefault(kind, {"total": 0}) public_counts["total"] = int(public_counts.get("total") or 0) + 1 + kept_public_counts[kind] = kept_public_counts.get(kind, 0) + 1 public_items.append(normalize_public_item(kind, item)) facts["public"]["items"] = annotate_public_reply_states(public_items, str((facts.get("account") or {}).get("handle") or "")) + add_public_source_gaps(facts, loaded_public_kinds, kept_public_counts, summary) if (summary.get("dm_status") or "") in {"blocked_by_x_chat_passcode", "visible_threads_unopened", "no_visible_threads", "dm_page_loading_timeout", "api_dm_unavailable", "api_dm_error", "api_dm_todo"}: facts["data_gaps"].append( { @@ -246,6 +252,36 @@ def build_digest_facts(data: dict[str, Any], summary: dict[str, Any]) -> dict[st return facts +def add_public_source_gaps( + facts: dict[str, Any], + loaded_public_kinds: set[str], + kept_public_counts: dict[str, int], + summary: dict[str, Any], +) -> None: + post_counts = summary.get("post_counts") if isinstance(summary.get("post_counts"), dict) else {} + required_mentions = ["mentions_search", "mentions_notifications"] + for kind in required_mentions: + if kind not in loaded_public_kinds: + facts["data_gaps"].append( + { + "source": kind, + "status": "missing_required_mention_source", + "detail": f"{kind} was not present in this run; do not conclude mention state from the other source alone.", + } + ) + continue + loaded_total = int((post_counts.get(kind) or {}).get("total") or 0) if isinstance(post_counts.get(kind), dict) else 0 + kept_total = int(kept_public_counts.get(kind, 0)) + if loaded_total > 0 and kept_total == 0: + facts["data_gaps"].append( + { + "source": kind, + "status": "no_in_window_items", + "detail": f"{kind} loaded {loaded_total} item(s), but none survived the local 24-hour filter.", + } + ) + + def normalize_public_item(kind: str, item: dict[str, Any]) -> dict[str, Any]: author_username = clean_handle(item.get("author_username") or item.get("authorUsername") or author_from_url(item.get("authorUrl"))) references = item.get("referenced_tweets") if isinstance(item.get("referenced_tweets"), list) else [] diff --git a/twitter-mcp/SKILL.md b/twitter-mcp/SKILL.md index 92aab31..f70033c 100644 --- a/twitter-mcp/SKILL.md +++ b/twitter-mcp/SKILL.md @@ -40,10 +40,14 @@ xurl search "@" -n 100 xurl search "to:" -n 100 ``` +These commands are mandatory for daily digest collection. Do not write a final digest if any of them has not been attempted. If you notice during drafting that `from:`, `@`, or `to:` search was skipped, stop drafting, run the missing command(s), then rebuild the digest from the combined outputs. + Rules for collection: - If `xurl whoami` does not reveal a handle, ask the user for the handle or skip handle-dependent commands and report the gap. - Keep only posts, mentions, searches, and timeline items from the last 24 hours in the user's current local timezone. This rule is strict for `xurl mentions`: mentions older than `cutoff` must be discarded before analysis and must not appear in `需要处理`. +- `xurl mentions` is not enough to conclude current mention state. If `xurl mentions` returns only old items or no in-window items, the `@` and `to:` searches are still required before saying there are no current mentions or no reply tasks. +- Do not put "未跑关键词搜索" in the final data gaps for mandatory `from:/@/to:` searches. Missing mandatory searches are a collection error to fix by running the commands before the final answer. Only report a mandatory search as a data gap if it was attempted and failed with an error, auth limit, tier limit, or rate limit. - If an item has no parseable timestamp, do not use it for time-bound facts; report it under data gaps as time-unverified. - When using `xurl search`, search date operators may be used only as a coarse prefilter; still post-filter each returned item to the exact 24-hour window. - Before adding any mention or direct ask to `需要处理` as "needs reply", verify whether the authenticated account has already replied after that mention's timestamp. Use `xurl posts ` and `xurl search "from: to:" -n 100` or equivalent `xurl` output to check for replies in the same conversation/thread or to the same author. If a reply from the authenticated account already exists after the mention timestamp, mark it as already handled or omit it from `需要处理`; do not ask the user to reply again. @@ -75,13 +79,13 @@ From the repository `skills/` directory: For a one-line Codex install from this beta tag: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.24/twitter-mcp/install.sh | env X_MCP_REGISTER_CODEX=1 X_MCP_REGISTER_CLAUDE=0 sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.25/twitter-mcp/install.sh | env X_MCP_REGISTER_CODEX=1 X_MCP_REGISTER_CLAUDE=0 sh ``` For a one-line Claude Code install from this beta tag: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.24/twitter-mcp/install.sh | env X_MCP_REGISTER_CODEX=0 X_MCP_REGISTER_CLAUDE=1 sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.25/twitter-mcp/install.sh | env X_MCP_REGISTER_CODEX=0 X_MCP_REGISTER_CLAUDE=1 sh ``` The installer: diff --git a/twitter-mcp/install.sh b/twitter-mcp/install.sh index 8483b57..3565d2a 100755 --- a/twitter-mcp/install.sh +++ b/twitter-mcp/install.sh @@ -1,7 +1,7 @@ #!/bin/sh set -eu -TAG="${X_MCP_INSTALL_TAG:-v1.5.11-beta.24}" +TAG="${X_MCP_INSTALL_TAG:-v1.5.11-beta.25}" BASE_URL="${X_MCP_INSTALL_BASE_URL:-https://raw.githubusercontent.com/BofAI/skills/${TAG}/twitter-mcp}" REGISTER_CODEX="${X_MCP_REGISTER_CODEX:-1}" REGISTER_CLAUDE="${X_MCP_REGISTER_CLAUDE:-auto}" From 535da213aa474bf26272e026f060c6b489918442 Mon Sep 17 00:00:00 2001 From: bobo Date: Wed, 1 Jul 2026 21:04:25 +0800 Subject: [PATCH 30/34] Default twitter digest to browser source --- twitter-digest/DATA_COLLECTION.md | 29 +++++++++---------- twitter-digest/FUNCTION_RULES_FLOW.md | 5 ++-- twitter-digest/README.md | 20 ++++++------- twitter-digest/RUNBOOK.md | 18 ++++++------ twitter-digest/SKILL.md | 16 +++++----- twitter-digest/install.sh | 2 +- twitter-digest/references/x-twitter-digest.md | 4 +-- twitter-digest/scripts/configure_api.py | 2 +- twitter-digest/scripts/run_daily_digest.py | 6 ++-- twitter-mcp/SKILL.md | 4 +-- twitter-mcp/install.sh | 2 +- 11 files changed, 55 insertions(+), 53 deletions(-) diff --git a/twitter-digest/DATA_COLLECTION.md b/twitter-digest/DATA_COLLECTION.md index e65f602..2aebdd6 100644 --- a/twitter-digest/DATA_COLLECTION.md +++ b/twitter-digest/DATA_COLLECTION.md @@ -32,7 +32,7 @@ scripts/browser_x_digest.py - 无 API 配置的普通用户。 - 需要读取 X Chat / DM 内容的场景。 -- 用户显式选择浏览器模式,或完全没有 API 配置时的默认来源。 +- 普通日报的默认来源;即使本地已有 API 配置,未显式选择 API 时也继续使用浏览器。 ### 2. API 抓取脚本 @@ -55,7 +55,7 @@ scripts/api_x_digest.py 适合: -- API 已配置、需要更稳定公开数据的场景。 +- 用户显式选择 API,且需要更稳定公开数据的场景。 - 定时任务中减少浏览器页面变化影响。 限制: @@ -78,10 +78,10 @@ scripts/run_daily_digest.py - 统一入口。 - 提供 `--configure-api`,由 Agent 在对话里触发 OAuth 配置。 -- 默认 `--source auto`。 -- 如果检测到环境变量 token,或 `.state/api_config.json` 里保存的 OAuth2 user-context 配置,走 API 抓取。 -- API 已配置时不启动浏览器,不自动回退浏览器;API 不可用时失败或写入 data gap。 -- 如果没有 API token,走浏览器抓取。 +- 默认使用浏览器抓取。 +- 只有用户主动要求配置/使用 X API,或命令显式传入 `--source api` / `--source auto`,才走 API 抓取。 +- API source 已显式选择时不启动浏览器,不自动回退浏览器;API 不可用时失败或写入 data gap。 +- 普通 `run_daily_digest.py` 即使检测到环境变量 token 或 `.state/api_config.json`,也继续使用浏览器。 - 如果 OAuth2 access token 快过期且保存了 refresh token,自动刷新后再抓取。 - 抓取完成后调用 `digest_context.py` 生成 `digest-context.*`。 @@ -90,14 +90,14 @@ scripts/run_daily_digest.py ```text --source api -> 强制 API,只采公开数据,不启动浏览器 --source browser -> 强制浏览器,采公开网页和可见 X Chat / DM ---source auto -> 有 X_BEARER_TOKEN/TWITTER_BEARER_TOKEN 或已保存 OAuth2 user token 用 API,否则浏览器;API 已配置时不回退浏览器 +--source auto -> 显式选择自动模式;有 X_BEARER_TOKEN/TWITTER_BEARER_TOKEN 或已保存 OAuth2 user token 用 API,否则浏览器;API 已配置时不回退浏览器 ``` 隔离规则: - API 来源只运行 `api_x_digest.py`,不启动浏览器、不读取浏览器 profile、不用浏览器补采 API 缺口。 - 浏览器来源只运行 `browser_x_digest.py`,不读取 API token、不合并 API collector 输出。 -- `--source auto` 每次只选择一个来源,不合并 API 和浏览器两边的数据。 +- 默认浏览器来源和 `--source auto` 每次都只选择一个来源,不合并 API 和浏览器两边的数据。 - API 输出里的“需要浏览器确认 DM”只能作为 data gap 提示,不表示本次已经读取了浏览器 DM。 ## 对话触发流程 @@ -126,7 +126,7 @@ Agent:运行 `python3 ~/.claude/skills/twitter-digest/scripts/run_daily_digest 脚本:弹出隐藏输入框 用户:粘贴 user access token 脚本:保存 token 到 .state/api_config.json -后续:`python3 ~/.claude/skills/twitter-digest/scripts/run_daily_digest.py --source auto` 自动走 API +后续普通日报仍运行 `python3 ~/.claude/skills/twitter-digest/scripts/run_daily_digest.py` 并默认走浏览器;要用 API 时显式运行 `--source api` 或 `--source auto` ``` 配置 API,OAuth2 授权: @@ -139,7 +139,7 @@ Agent:运行 `python3 ~/.claude/skills/twitter-digest/scripts/run_daily_digest 脚本:打开 X 授权页 用户:在浏览器里授权 app 脚本:通过本地 callback 换取 access token / refresh token 并保存 -后续:`python3 ~/.claude/skills/twitter-digest/scripts/run_daily_digest.py --source auto` 自动走 API +后续普通日报仍运行 `python3 ~/.claude/skills/twitter-digest/scripts/run_daily_digest.py` 并默认走浏览器;要用 API 时显式运行 `--source api` 或 `--source auto` ``` 后续运行: @@ -147,16 +147,15 @@ Agent:运行 `python3 ~/.claude/skills/twitter-digest/scripts/run_daily_digest ```text 用户:生成 X 日报 Agent:运行 scripts/run_daily_digest.py -脚本:读取 .state/api_config.json -脚本:OAuth2 如需 refresh 则自动 refresh -脚本:采集 API 数据并生成 digest-context.md +脚本:默认使用浏览器采集并生成 digest-context.md +脚本:只有显式 `--source api` / `--source auto` 时才读取 `.state/api_config.json` 使用 API Agent:读取 digest-context.md 写中文日报 ``` 如果凭据失效: ```text -脚本:把 API endpoint 错误写入 data gap +脚本:如果显式使用 API,则把 API endpoint 错误写入 data gap Agent:告知用户需要重新配置/授权 用户:在对话里说“重新配置 X API” Agent:再次运行 `python3 ~/.claude/skills/twitter-digest/scripts/run_daily_digest.py --configure-api` @@ -167,7 +166,7 @@ Agent:再次运行 `python3 ~/.claude/skills/twitter-digest/scripts/run_daily_ ```text 用户:清除 X API 配置 Agent:运行 `python3 ~/.claude/skills/twitter-digest/scripts/configure_api.py --clear` -后续:`python3 ~/.claude/skills/twitter-digest/scripts/run_daily_digest.py --source auto` 因没有 API 配置而使用浏览器抓取 +后续:`python3 ~/.claude/skills/twitter-digest/scripts/run_daily_digest.py` 默认使用浏览器抓取 ``` ## 标准输出结构 diff --git a/twitter-digest/FUNCTION_RULES_FLOW.md b/twitter-digest/FUNCTION_RULES_FLOW.md index 884a904..6184db0 100644 --- a/twitter-digest/FUNCTION_RULES_FLOW.md +++ b/twitter-digest/FUNCTION_RULES_FLOW.md @@ -8,7 +8,7 @@ twitter-digest/RUNBOOK.md ## 1. 功能 -`twitter-digest` 通过 X API 或本地已登录浏览器读取用户自己的 X/Twitter 数据,并生成中文日报。 +`twitter-digest` 默认通过本地已登录浏览器读取用户自己的 X/Twitter 数据,并生成中文日报。只有用户主动要求配置/使用 X API,或命令显式传入 `--source api` / `--source auto` 时,才使用 X API。 默认采集内容: @@ -64,7 +64,8 @@ twitter-digest/.state/run/digest-context.md ## 3. 运行规则 -- 默认入口 `run_daily_digest.py --source auto`:有 OAuth2 user-context API 配置时只用 API 抓公开数据,不打开浏览器;没有 API 配置时用浏览器抓取。 +- 默认入口 `run_daily_digest.py`:使用浏览器抓取。即使已保存 OAuth2 user-context API 配置或环境里有 `X_BEARER_TOKEN` / `TWITTER_BEARER_TOKEN`,普通日报也不会自动切到 API。 +- 只有用户主动要求 API 或命令显式 `--source api` / `--source auto` 时才使用 API;API 模式只抓公开数据,不打开浏览器。 - DM / X Chat 以本地浏览器抓取为准;API DM 现阶段仅保留为 TODO/调试,不用于判断是否有私信。 - API 不可用、权限不足、tier 不支持或限流时,记录数据缺口或失败,不回退浏览器路径。 - 不要求用户复制 cookie 或 token。 diff --git a/twitter-digest/README.md b/twitter-digest/README.md index fced213..0d15b28 100644 --- a/twitter-digest/README.md +++ b/twitter-digest/README.md @@ -1,6 +1,6 @@ # X/Twitter Digest -Skill for generating a Chinese daily digest from a user's own X/Twitter account through API collection when configured, otherwise through a saved local browser session. +Skill for generating a Chinese daily digest from a user's own X/Twitter account through a saved local browser session by default, with optional X API collection only when explicitly configured and selected. ## Quick Install @@ -15,7 +15,7 @@ python3 twitter-digest/scripts/install.py To ask Codex to install this skill for itself, paste this into Codex: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.25/twitter-digest/install.sh | env TWITTER_DIGEST_INSTALL_CLIENT=codex sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.26/twitter-digest/install.sh | env TWITTER_DIGEST_INSTALL_CLIENT=codex sh ``` When this one-line installer is launched from Codex, Claude Code, or another non-interactive agent on macOS, it opens a real Terminal window and re-runs the full installation there. This avoids agent permission/inspect prompts during `git clone`, Python checks, browser checks, and installer writes. Set `TWITTER_DIGEST_OPEN_TERMINAL=0` only when intentionally running in an already interactive Terminal or CI. @@ -35,7 +35,7 @@ git clone git@github.com:BofAI/skills.git bofai-skills \ To ask Claude Code to install this skill for itself, paste this into Claude Code: ```bash -curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.25/twitter-digest/install.sh | env TWITTER_DIGEST_INSTALL_CLIENT=claude TWITTER_DIGEST_ALLOW_CLAUDE_COMMANDS=1 TWITTER_DIGEST_ALLOW_CLAUDE_STATE_READ=1 sh +curl -fsSL https://raw.githubusercontent.com/BofAI/skills/v1.5.11-beta.26/twitter-digest/install.sh | env TWITTER_DIGEST_INSTALL_CLIENT=claude TWITTER_DIGEST_ALLOW_CLAUDE_COMMANDS=1 TWITTER_DIGEST_ALLOW_CLAUDE_STATE_READ=1 sh ``` Or use the natural-language prompt: @@ -93,17 +93,17 @@ X_BEARER_TOKEN=... python3 twitter-digest/scripts/api_x_digest.py --handle