diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..b2622e6 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,70 @@ +# AGENTS.md — Agent Developer Guide for twitter-cli + +This file provides context for AI agents working in this repository. + +## Project Overview + +- **Project**: twitter-cli — A CLI for Twitter/X (read timelines, bookmarks, search, post, reply, etc.) +- **Language**: Python 3.10+ +- **Package Manager**: uv (recommended) / pip +- **Repository**: https://github.com/jackwener/twitter-cli + +## Build, Lint, and Test Commands + +```bash +# Install all dependencies (including dev) +uv sync --extra dev + +# Run ruff linter +uv run ruff check . + +# Run mypy type checker +uv run mypy twitter_cli + +# Run all tests (excludes smoke tests by default) +uv run pytest -q + +# Run a single test +uv run pytest tests/test_cli.py::test_feed_command -v + +# Run tests matching pattern +uv run pytest -k "test_parse" -v +``` + +## Code Style + +- **Line length**: 100 characters +- **Python version**: 3.10+ +- Use `from __future__ import annotations` at top of all .py files +- **Functions/variables**: `snake_case`, **Classes**: `PascalCase`, **Constants**: `UPPER_SNAKE_CASE` +- Private functions: prefix with `_` +- Use `@dataclass` for data models (in `models.py`) +- Use Click framework for CLI commands +- Custom exceptions in `exceptions.py`, base: `TwitterError(RuntimeError)` + +## Project Structure + +``` +twitter_cli/ +├── cli.py # Click CLI entry point +├── client.py # Twitter API client (HTTP) +├── auth.py # Cookie extraction & auth +├── graphql.py # GraphQL query IDs +├── parser.py # Tweet/User parsing +├── models.py # Dataclass models +├── formatter.py # Rich table formatting +├── serialization.py # YAML/JSON output +├── output.py # Structured output helpers +├── config.py # Config loading +├── filter.py # Tweet ranking/scoring +├── constants.py # Constants +├── exceptions.py # Custom exceptions +├── cache.py # Tweet caching +├── search.py # Search utilities +└── timeutil.py # Time utilities +``` + +## CI + +- GitHub Actions: Python 3.10, 3.11, 3.12 +- CI validates: ruff check + mypy + pytest diff --git a/SKILL.md b/SKILL.md index fc30a8d..512f7b9 100644 --- a/SKILL.md +++ b/SKILL.md @@ -49,6 +49,7 @@ If `AUTH_NEEDED`, proceed to guide the user: Ensure user is logged into x.com in one of: Arc, Chrome, Edge, Firefox, Brave. twitter-cli auto-extracts cookies. All Chrome profiles are scanned automatically. To specify a profile: `TWITTER_CHROME_PROFILE="Profile 2" twitter feed`. +To prioritize a specific browser: `TWITTER_BROWSER=chrome twitter feed` (supported: arc, chrome, edge, firefox, brave). ```bash twitter whoami diff --git a/tests/test_auth.py b/tests/test_auth.py index c111676..0ef95a8 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -135,7 +135,7 @@ def test_extract_via_subprocess_script_includes_arc(monkeypatch) -> None: cookies, diagnostics = auth._extract_via_subprocess() assert cookies is None - assert '("arc", browser_cookie3.arc)' in seen["script"] + assert '"arc": browser_cookie3.arc' in seen["script"] def test_extract_via_subprocess_retries_uv_when_current_env_has_no_output(monkeypatch) -> None: diff --git a/twitter_cli/auth.py b/twitter_cli/auth.py index f5f0f5c..f9d8f61 100644 --- a/twitter_cli/auth.py +++ b/twitter_cli/auth.py @@ -193,6 +193,20 @@ _CHROMIUM_BASE_DIRS: Dict[str, str] = { "brave": os.path.join("BraveSoftware", "Brave-Browser"), } +# Default browser order for cookie extraction +_DEFAULT_BROWSER_ORDER = ["arc", "chrome", "edge", "firefox", "brave"] + + +def _get_browser_order() -> List[str]: + """Return browser extraction order, respecting TWITTER_BROWSER env var.""" + env = os.environ.get("TWITTER_BROWSER", "").strip().lower() + if not env: + return _DEFAULT_BROWSER_ORDER + if env not in {"arc", "chrome", "edge", "firefox", "brave"}: + logger.warning("TWITTER_BROWSER='%s' is invalid, using default order", env) + return _DEFAULT_BROWSER_ORDER + return [env] + [b for b in _DEFAULT_BROWSER_ORDER if b != env] + def _iter_chrome_cookie_files(browser_name: str) -> List[str]: """Return cookie file paths for all Chrome profiles. @@ -262,17 +276,18 @@ def _extract_in_process() -> Tuple[Optional[Dict[str, str]], List[str]]: logger.debug("browser_cookie3 not installed, skipping in-process extraction") return None, ["browser-cookie3 not installed"] - browsers = [ - ("arc", browser_cookie3.arc), - ("chrome", browser_cookie3.chrome), - ("edge", browser_cookie3.edge), - ("firefox", browser_cookie3.firefox), - ("brave", browser_cookie3.brave), - ] + browser_fns = { + "arc": browser_cookie3.arc, + "chrome": browser_cookie3.chrome, + "edge": browser_cookie3.edge, + "firefox": browser_cookie3.firefox, + "brave": browser_cookie3.brave, + } attempts: List[str] = [] diagnostics: List[str] = [] - for name, fn in browsers: + for name in _get_browser_order(): + fn = browser_fns[name] if name in _CHROMIUM_BASE_DIRS: # Chromium-based: iterate all profiles cookie_files = _iter_chrome_cookie_files(name) @@ -398,16 +413,23 @@ def extract_from_jar(jar, name, profile=""): return result return None -browsers = [ - ("arc", browser_cookie3.arc), - ("chrome", browser_cookie3.chrome), - ("edge", browser_cookie3.edge), - ("firefox", browser_cookie3.firefox), - ("brave", browser_cookie3.brave), -] +DEFAULT_ORDER = ["arc", "chrome", "edge", "firefox", "brave"] +env_browser = os.environ.get("TWITTER_BROWSER", "").strip().lower() +if env_browser in {"arc", "chrome", "edge", "firefox", "brave"}: + browser_order = [env_browser] + [b for b in DEFAULT_ORDER if b != env_browser] +else: + browser_order = DEFAULT_ORDER +browser_fns = { + "arc": browser_cookie3.arc, + "chrome": browser_cookie3.chrome, + "edge": browser_cookie3.edge, + "firefox": browser_cookie3.firefox, + "brave": browser_cookie3.brave, +} attempts = [] -for name, fn in browsers: +for name in browser_order: + fn = browser_fns[name] if name in CHROMIUM_BASE_DIRS: cookie_files = iter_cookie_files(name) if not cookie_files: