feat(auth): add TWITTER_BROWSER env var to specify browser preference
Add TWITTER_BROWSER environment variable to allow users to control which browser's cookies are prioritized during extraction. Example: TWITTER_BROWSER=chrome twitter feed Supported values: arc, chrome, edge, firefox, brave. The specified browser is moved to the front of the extraction order. Also adds AGENTS.md developer guide for AI coding assistants. Co-authored-by: Agassi <413855+agassiyzh@users.noreply.github.com>
This commit is contained in:
70
AGENTS.md
Normal file
70
AGENTS.md
Normal file
@@ -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
|
||||||
1
SKILL.md
1
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.
|
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`.
|
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
|
```bash
|
||||||
twitter whoami
|
twitter whoami
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ def test_extract_via_subprocess_script_includes_arc(monkeypatch) -> None:
|
|||||||
cookies, diagnostics = auth._extract_via_subprocess()
|
cookies, diagnostics = auth._extract_via_subprocess()
|
||||||
|
|
||||||
assert cookies is None
|
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:
|
def test_extract_via_subprocess_retries_uv_when_current_env_has_no_output(monkeypatch) -> None:
|
||||||
|
|||||||
@@ -193,6 +193,20 @@ _CHROMIUM_BASE_DIRS: Dict[str, str] = {
|
|||||||
"brave": os.path.join("BraveSoftware", "Brave-Browser"),
|
"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]:
|
def _iter_chrome_cookie_files(browser_name: str) -> List[str]:
|
||||||
"""Return cookie file paths for all Chrome profiles.
|
"""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")
|
logger.debug("browser_cookie3 not installed, skipping in-process extraction")
|
||||||
return None, ["browser-cookie3 not installed"]
|
return None, ["browser-cookie3 not installed"]
|
||||||
|
|
||||||
browsers = [
|
browser_fns = {
|
||||||
("arc", browser_cookie3.arc),
|
"arc": browser_cookie3.arc,
|
||||||
("chrome", browser_cookie3.chrome),
|
"chrome": browser_cookie3.chrome,
|
||||||
("edge", browser_cookie3.edge),
|
"edge": browser_cookie3.edge,
|
||||||
("firefox", browser_cookie3.firefox),
|
"firefox": browser_cookie3.firefox,
|
||||||
("brave", browser_cookie3.brave),
|
"brave": browser_cookie3.brave,
|
||||||
]
|
}
|
||||||
attempts: List[str] = []
|
attempts: List[str] = []
|
||||||
diagnostics: 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:
|
if name in _CHROMIUM_BASE_DIRS:
|
||||||
# Chromium-based: iterate all profiles
|
# Chromium-based: iterate all profiles
|
||||||
cookie_files = _iter_chrome_cookie_files(name)
|
cookie_files = _iter_chrome_cookie_files(name)
|
||||||
@@ -398,16 +413,23 @@ def extract_from_jar(jar, name, profile=""):
|
|||||||
return result
|
return result
|
||||||
return None
|
return None
|
||||||
|
|
||||||
browsers = [
|
DEFAULT_ORDER = ["arc", "chrome", "edge", "firefox", "brave"]
|
||||||
("arc", browser_cookie3.arc),
|
env_browser = os.environ.get("TWITTER_BROWSER", "").strip().lower()
|
||||||
("chrome", browser_cookie3.chrome),
|
if env_browser in {"arc", "chrome", "edge", "firefox", "brave"}:
|
||||||
("edge", browser_cookie3.edge),
|
browser_order = [env_browser] + [b for b in DEFAULT_ORDER if b != env_browser]
|
||||||
("firefox", browser_cookie3.firefox),
|
else:
|
||||||
("brave", browser_cookie3.brave),
|
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 = []
|
attempts = []
|
||||||
|
|
||||||
for name, fn in browsers:
|
for name in browser_order:
|
||||||
|
fn = browser_fns[name]
|
||||||
if name in CHROMIUM_BASE_DIRS:
|
if name in CHROMIUM_BASE_DIRS:
|
||||||
cookie_files = iter_cookie_files(name)
|
cookie_files = iter_cookie_files(name)
|
||||||
if not cookie_files:
|
if not cookie_files:
|
||||||
|
|||||||
Reference in New Issue
Block a user