Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
xurl
.xurl_test
.DS_Store# Added by goreleaser init:
.DS_Store

# Added by goreleaser init:
dist/
33 changes: 30 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,29 @@

All user-visible bugs and enhancements should be recorded here.

## Unreleased

Last updated: 2026-05-14 11:38:34 PDT
## v1.2.0 - 2026-06-29

### Fixed

- [2026-06-29] `install.sh` now uses `id -u` instead of the bash-only `$EUID` to detect root, so `curl ... | sh` (POSIX/dash) installs to `/usr/local/bin` as root instead of silently falling back to `~/.local/bin`. (#68)
- [2026-06-29] npm install on Windows works again: `install.js` extracts the `.zip` with PowerShell's `Expand-Archive` instead of the Unix `unzip` command. (#56)
- [2026-06-29] `whoami` (and `user`) now request `verified_type` and `subscription_type`, so Premium/blue accounts are reported correctly instead of `verified: false`. (#41)
- [2026-06-29] OAuth2 token exchange and refresh now send client credentials with the correct auth style — HTTP Basic header for confidential clients (those with a secret), `client_id` in the body for public clients — instead of relying on autodetection, which could fail against X with `unauthorized_client: Missing valid authorization header`.
- [2026-06-29] `mcp` bridge no longer launches a browser at startup: it still refreshes an existing token silently, but when none is available it fails fast with instructions (`xurl auth oauth2 [--app NAME] [--headless]`) instead of opening a browser mid-startup (which could hang an MCP client's handshake) and printing to the JSON-RPC stdout channel. OAuth2 diagnostics now go to stderr.
- [2026-06-29] `mcp` bridge no longer lets a strict client hang: a request that cannot be answered — transport failure, a failed token refresh/retry after a 401, or a response with an empty/non-JSON body — now gets a synthesized JSON-RPC error keyed to its id. Notifications (e.g. `notifications/cancelled`) are no longer head-of-line blocked behind an in-flight streaming response, large but valid JSON error bodies are forwarded whole instead of being truncated, the standalone server->client stream stops probing a non-event-stream `200` and only resets its reconnect backoff after a healthy stream, and stdin memory stays bounded when an oversized line is dropped.
- [2026-06-25] `mcp` bridge hardening: serialized token-store access (fixes a fatal data race when a token expires mid-session), strict newline-delimited-JSON stdout (SSE/JSON responses are validated and compacted, non-JSON keep-alives dropped), a forced token refresh on HTTP 401, cancelable stdin so SIGINT/SIGTERM shuts the bridge down, resilience to oversized input lines, a server->client stream that resets its backoff/supports stateless servers/retries 408 & 429, and a best-effort session `DELETE` on shutdown.
- [2026-06-25] `media upload --wait` now also waits for animated GIFs (auto-detected as `tweet_gif`), and a media type that cannot be detected — or is recognized but unsupported (e.g. `application/pdf`) — now fails with a clear message instead of guessing `tweet_image` and getting an opaque API error.
- [2026-06-25] `timeline` `--max-results` minimum corrected to 1 (matches the reverse-chronological endpoint).
- [2026-06-25] The raw-request "No URL provided" usage message now prints to stderr.
- [2026-06-25] `media upload --wait` now actually waits for processing and no longer always sends the trace header — the `waitForProcessing` and `trace` arguments were passed in the wrong order.
- [2026-06-25] Raw API requests now surface the real transport/auth error instead of printing `null` when a request fails before getting an HTTP response (e.g. DNS or connection failures).
- [2026-06-25] Requests with no usable credentials, or an invalid `--auth` value, now fail with a clear authentication error instead of silently sending an unauthenticated request.
- [2026-06-25] `xurl dm` now JSON-encodes message text correctly; quotes, backslashes, and newlines no longer produce a malformed request body.
- [2026-06-25] OAuth2 expiry is stored correctly; a token returned without an expiry now refreshes on next use instead of being treated as never-expiring.
- [2026-06-25] `--max-results` is clamped to each endpoint's accepted range for timeline, mentions, bookmarks, likes, following, followers, dms, and posts.
- [2026-06-25] `fetchUsername` now uses a 10s HTTP timeout, and PKCE verifier generation now handles RNG errors instead of ignoring them.
- [2026-06-25] `webhook start` help now references the correct `-P` pretty-print flag and serves on an isolated `ServeMux`.
- [2026-06-25] `.gitignore` now correctly ignores `.DS_Store` (a missing newline had merged it with a comment).
- [2026-04-19 23:08:51 CEST] OAuth2 callback listeners now bind to the host and port derived from the effective redirect URI instead of always listening on `127.0.0.1:8080`. For `localhost`, `xurl` now listens on both `127.0.0.1` and `::1`, which fixes browser-dependent loopback resolution failures while still supporting non-default callback paths.
- [2026-04-19 23:08:51 CEST] The OAuth2 listener now starts listening before the browser opens, which removes a race where the browser could reach the callback URL before the local server was ready.
- [2026-04-19 23:08:51 CEST] OAuth2 token refresh no longer depends on `/2/users/me` succeeding. If username discovery fails, `xurl` keeps the refreshed token instead of failing the request.
Expand All @@ -16,6 +33,16 @@ Last updated: 2026-05-14 11:38:34 PDT

### Enhanced

- [2026-06-29] Added `xurl auth oauth2 --headless` for authenticating on remote/headless machines where the localhost OAuth callback is unreachable: xurl prints the authorization URL, you open it on any device and approve, then paste the resulting redirect URL (or just the `code`) back at the prompt. No callback listener or local browser is required. (Closes the headless half of #62 / #40.)
- [2026-06-25] OAuth2 tokens now refresh ~30s before expiry (clock-skew leeway) so a token handed to a caller does not expire in-flight; a new forced-refresh path backs the `mcp` bridge's 401 recovery.
- [2026-06-25] `xurl token`'s missing-token error now names the requested user, and `token`/`mcp` errors omit ANSI color when stderr is not a terminal (cleaner piped/logged output). The auto-generated `help`/`completion` commands now appear under the Management group.
- [2026-06-25] Added `xurl token`: prints a valid (refreshed, persisted) OAuth2 access token for the active app to stdout without opening a browser, so it can be scripted. Respects `--app` and `-u/--username`.
- [2026-06-25] Added `xurl mcp [URL]`: a stdio↔Streamable-HTTP MCP bridge for the hosted X API MCP server (default `https://api.x.com/mcp`). It injects `Authorization: Bearer <token>`, maintains the MCP session id, handles plain-JSON and SSE responses, refreshes the token in-process, and triggers the browser login on first run if needed. Usable from any MCP client via `npx -y @xdevplatform/xurl mcp`.
- [2026-06-29] The app-only token command is now `xurl auth app-only [TOKEN]` (named for the auth mode, not the "bearer" token scheme that OAuth2 user tokens also use), taking the token as an argument or from stdin via `-`. It removes the old `app` vs `apps` confusion and the redundant `auth bearer --bearer-token`. Back-compat: `auth app` and `auth bearer` remain aliases and `--bearer-token` is still accepted.
- [2026-06-25] `xurl --help` now groups subcommands into "Posting & Engagement", "Users & Social Graph", "Reading & Lists", and "Management" sections instead of one flat list.
- [2026-06-25] Added `xurl posts USERNAME` to list a user's recent posts.
- [2026-06-25] `xurl --version` is now supported in addition to `xurl version`.
- [2026-06-25] Raw requests now default to `POST` when `-d` is supplied (curl-like), and `media upload` auto-detects the media type and category from the file extension when they are not provided.
- [2026-05-14 11:38:34 PDT] Documentation and the bundled `xurl` skill now recommend authenticating registered apps with `xurl auth oauth2 --app APP_NAME` and explain that omitting `--app` saves the token to the current default app.
- [2026-04-19 23:08:51 CEST] OAuth2 tokens can now be retained without a discovered username label when X’s `/2/users/me` lookup is unavailable. Status output makes that state visible as `(unknown user)` instead of silently dropping the token.
- [2026-04-19 23:08:51 CEST] Repo documentation now describes the effective redirect URI as the source of callback host, port, and path, calls out explicit username authentication as the safer fallback when username discovery is unreliable, and documents the new stored `redirect_uri` behavior.
Expand Down
79 changes: 70 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,14 @@ xurl auth oauth2 --app my-app

If you omit `--app`, the token is saved to the current default app. You can also run `xurl auth default my-app` first and then use `xurl auth oauth2`.

**Headless / remote machines.** The default flow opens a browser and waits for a callback on `localhost`, which isn't reachable from a remote server. On those hosts use `--headless`:

```bash
xurl auth oauth2 --app my-app --headless
```

xurl prints the authorization URL; open it on any device with a browser, approve, then paste the resulting redirect URL (or just the `code` value from the address bar) back into the prompt. No callback listener is needed — the page failing to load is expected; the code is in the URL.

If X returns a `client-forbidden` / `client-not-enrolled` error even though auth completed successfully, check the app’s package and environment in the X developer console. On current X platform setup, the working fix was:

1. Go to `Apps` -> `Manage apps`
Expand All @@ -104,10 +112,13 @@ xurl auth oauth2 --app my-app YOUR_USERNAME

That keeps the OAuth2 token associated with the expected username and also gives shortcut commands a fallback when `/2/users/me` is unavailable.

#### App authentication (bearer token):
#### App-only authentication (Bearer Token):
```bash
xurl auth app --bearer-token BEARER_TOKEN
xurl auth app-only BEARER_TOKEN
cat token.txt | xurl auth app-only - # read from stdin (keeps it out of shell history)
```
This stores X's app-only Bearer Token (from the developer portal), used at request time with `--auth app`. It's named for the auth *mode* (app-only) rather than the token *scheme* (bearer), since OAuth2 user tokens are also sent as `Authorization: Bearer`.
> Back-compat: `xurl auth app` and `xurl auth bearer` still work as aliases, and `--bearer-token TOKEN` is still accepted.

#### OAuth 1.0a authentication:
```bash
Expand Down Expand Up @@ -249,6 +260,53 @@ You can also force streaming mode for any endpoint using the `--stream` or `-s`
xurl -s /2/users/me
```

### Printing an Access Token

`xurl token` prints a valid OAuth2 access token for the active app to stdout (a single line, no decoration). If the stored token has expired it is refreshed and persisted first. This command never opens a browser, so it is safe to use in scripts:

```bash
xurl token # token for the default app/user
xurl token --app my-app # token for a specific app
xurl token -u alice # token for a specific OAuth2 user
TOKEN=$(xurl token) && curl -H "Authorization: Bearer $TOKEN" https://api.x.com/2/users/me
```

If no token is available (and none can be refreshed), it exits non-zero with a hint to run `xurl auth oauth2`.

### MCP Server (`xurl mcp`)

`xurl mcp` turns xurl into a [Model Context Protocol](https://modelcontextprotocol.io) bridge for the hosted X API MCP server. It reads newline-delimited JSON-RPC from stdin, relays each message to a remote Streamable HTTP MCP endpoint with an `Authorization: Bearer <token>` header, and writes the server's responses (plain JSON or `text/event-stream`) back to stdout as newline-delimited JSON. The MCP session id is maintained automatically and the token is refreshed in-process as it expires.

Because X's OAuth requires your own app (there is no dynamic client registration), xurl holds the app identity and mints/refreshes the token. **Authenticate once before starting the bridge** — `xurl mcp` will refresh an expired token automatically but never opens a browser itself (its stdio is the MCP channel), so it fails fast with instructions if no token exists:

```bash
xurl auth oauth2 --app my-app # local machine with a browser
xurl auth oauth2 --app my-app --headless # remote/headless machine
```

Use it directly from any MCP client (Claude Desktop, Cursor, etc.) with a standard MCP server config — no separate install step is needed thanks to the npm launcher:

```json
{
"mcpServers": {
"xapi": {
"command": "npx",
"args": ["-y", "@xdevplatform/xurl", "mcp", "https://api.x.com/mcp"],
"env": { "CLIENT_ID": "...", "CLIENT_SECRET": "..." }
}
}
}
```

The `<url>` positional is optional and defaults to `https://api.x.com/mcp`. `--app` is honored, so you can point a client at a specific registered app:

```bash
xurl --app my-app mcp # bridge the default endpoint using my-app
xurl mcp https://api.x.com/mcp # explicit endpoint
```

All diagnostics are written to stderr so stdout stays a clean JSON-RPC channel.

### Temporary Webhook Setup

`xurl` can help you quickly set up a temporary webhook URL to receive events from the X API. This is useful for development and testing.
Expand Down Expand Up @@ -280,12 +338,13 @@ xurl -s /2/users/me

The tool supports uploading media files to the X API using the chunked upload process.

Upload a media file:
Upload a media file (the media type and category are auto-detected from the file extension):
```bash
xurl media upload path/to/file.mp4
xurl media upload path/to/photo.jpg
```

With custom media type and category:
Override the auto-detected media type and category when needed:
```bash
xurl media upload --media-type image/jpeg --category tweet_image path/to/image.jpg
```
Expand All @@ -302,21 +361,23 @@ xurl media status --wait MEDIA_ID

#### Direct Media Upload

You can also use the main command with the `-F` flag for direct media uploads:
Most users should just use `xurl media upload` above. If you need to drive the
chunked upload manually, use the `-F` flag with the path-style endpoints that
`xurl media upload` itself uses:

1. First, initialize the upload:
```bash
xurl -X POST '/2/media/upload?command=INIT&total_bytes=FILE_SIZE&media_type=video/mp4&media_category=tweet_video'
xurl -X POST /2/media/upload/initialize -d '{"total_bytes": FILE_SIZE, "media_type": "video/mp4", "media_category": "tweet_video"}'
```

2. Then, append the media chunks:
2. Then, append the media chunks (repeat with an increasing `segment_index`):
```bash
xurl -X POST -F path/to/file.mp4 '/2/media/upload?command=APPEND&media_id=MEDIA_ID&segment_index=0'
xurl -X POST -F path/to/file.mp4 /2/media/upload/MEDIA_ID/append
```

3. Finally, finalize the upload:
```bash
xurl -X POST '/2/media/upload?command=FINALIZE&media_id=MEDIA_ID'
xurl -X POST /2/media/upload/MEDIA_ID/finalize
```

4. Check the status:
Expand Down
11 changes: 11 additions & 0 deletions SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ Before using any command you must be authenticated. Run `xurl auth status` to ch
- Do not recommend or execute auth commands with inline secrets in agent/LLM sessions.
- Warn that using CLI secret options in agent sessions can leak credentials (prompt/context, logs, shell history).
- Never use `--verbose` / `-v` in agent/LLM sessions; it can expose sensitive headers/tokens in output.
- Never run `xurl token` in agent/LLM sessions: it prints a live OAuth2 access token to stdout, which is a credential and must not enter the LLM context.
- `xurl mcp` is for configuring an MCP client (it bridges stdio↔HTTP and injects the bearer token); it is not something to invoke directly from an agent/LLM session.
- Sensitive flags that must never be used in agent commands: `--bearer-token`, `--consumer-key`, `--consumer-secret`, `--access-token`, `--token-secret`, `--client-id`, `--client-secret`.
- To verify whether at least one app with credentials is already registered, run: `xurl auth status`.

Expand All @@ -37,6 +39,8 @@ xurl auth oauth2 --app APP_NAME

You can also run `xurl auth default APP_NAME` first and then use `xurl auth oauth2`.

On a remote/headless machine (no reachable browser callback), add `--headless`: `xurl auth oauth2 --app APP_NAME --headless` prints the authorization URL and reads the pasted redirect URL (or code) back, so no localhost callback is needed.

For multiple pre-configured apps, switch between them:
```bash
xurl auth default prod-app # set default app
Expand Down Expand Up @@ -66,6 +70,7 @@ Tokens are persisted to `~/.xurl` in YAML format. Each app has its own isolated
| Search posts | `xurl search "QUERY" -n 10` |
| Who am I | `xurl whoami` |
| Look up a user | `xurl user @handle` |
| List a user's posts | `xurl posts @handle -n 10` |
| Home timeline | `xurl timeline -n 20` |
| Mentions | `xurl mentions -n 10` |
| Like | `xurl like POST_ID` |
Expand Down Expand Up @@ -157,6 +162,10 @@ xurl whoami
# Look up any user
xurl user elonmusk
xurl user @XDevelopers

# List a user's recent posts (by @username)
xurl posts elonmusk
xurl posts @XDevelopers -n 25
```

### Timelines & Mentions
Expand Down Expand Up @@ -404,3 +413,5 @@ xurl --app staging /2/users/me # one-off request against staging
- **Multiple accounts:** You can authenticate multiple OAuth 2.0 accounts per app and switch between them with `--username` / `-u` or set a default with `xurl auth default APP USER`.
- **Default user:** When no `-u` flag is given, xurl uses the default user for the active app (set via `xurl auth default`). If no default user is set, it uses the first available token.
- **Token storage:** `~/.xurl` is YAML. Each app stores its own credentials and tokens. Never read or send this file to LLM context.
- **Access tokens:** `xurl token` prints a valid (refreshed) OAuth2 access token for the active app to stdout, refreshing and persisting it if expired. It never opens a browser. The output is a secret — use it only in the user's own scripts, never in agent/LLM sessions.
- **MCP bridge:** `xurl mcp [URL]` bridges a stdio MCP client to a remote Streamable HTTP MCP server (default `https://api.x.com/mcp`), injecting `Authorization: Bearer <token>` and refreshing the token automatically. Authenticate once first (`xurl auth oauth2 --app APP_NAME`, or `--headless` on a remote host) — the bridge refreshes an existing token but never opens a browser itself, so it fails fast with guidance if none exists. Configure it in an MCP client via the npm launcher: `{"command":"npx","args":["-y","@xdevplatform/xurl","mcp","https://api.x.com/mcp"],"env":{"CLIENT_ID":"...","CLIENT_SECRET":"..."}}`.
Loading
Loading