aux4/curl

HTTP client with OAuth2 authentication and NDJSON streaming support. Similar to curl but designed for OAuth2 workflows: authenticate with any OAuth2 provider, make authenticated requests, upload files and download binary responses, stream NDJSON input where each JSON line triggers a separate HTTP request, and collect results as NDJSON output.

Installation

aux4 aux4 pkger install aux4/curl

Quick Start

# Simple GET request
aux4 curl request https://api.example.com/users

# POST with a body
aux4 curl request --method POST --body '{"name":"Alice"}' --header "Content-Type: application/json" https://api.example.com/users

# OAuth2 login
aux4 curl oauth login myprovider --clientId abc123 --clientSecret secret --authUrl https://provider.com/oauth --tokenUrl https://provider.com/token --scopes read,write

# Authenticated request (auto-injects Bearer token)
aux4 curl auth-request --provider myprovider https://api.provider.com/v1/me

# Stream NDJSON: each line becomes a request, results streamed as NDJSON
cat users.ndjson | aux4 curl stream https://api.example.com/process

Commands

aux4 curl request

Make an HTTP request. Supports all standard HTTP methods, custom headers, request bodies from a string or a file, file uploads, and binary downloads.

aux4 curl request [--method <METHOD>] [--header <Header: Value>] [--body <data>] [--showHeaders <true|false>] [--upload <path>] [--uploadField <name>] [--output <path>] [--status <true|false>] [--maxTime <seconds>] <url>

| Flag | Description | Default | |------|-------------|---------| | --method | HTTP method (GET, POST, PUT, DELETE, PATCH, etc.) | GET | | --header | Request header in Name: Value format (repeatable) | | | --body | Request body as a string, or a JSON object of extra form fields when uploading | | | --bodyFile | Send the contents of this file as the raw request body | | | --showHeaders | Include response headers in output | false | | --upload | File to send as multipart/form-data (repeatable, field=path to name the part) | | | --uploadField | Default form field name for uploaded files | file | | --output | Write the response body to this file instead of stdout | | | --status | Print only the numeric HTTP status code and exit 0 (transport failure prints nothing and exits 1) | false | | --maxTime | Request timeout in seconds (decimals allowed); 0 means no timeout | 0 | | url | Request URL (positional argument) | required |

Piping data into aux4 curl request does not set the request body — aux4 does not forward stdin to it, and forwarding it would make bodyless requests such as a plain GET hang. Use --bodyFile to send a file's contents (the way to send binary payloads), or --body for a string; --bodyFile wins if both are given. For one request per line of piped input, use aux4 curl stream.

Uploading files

--upload sends a file as multipart/form-data — set automatically, with a generated boundary, so no Content-Type is needed. The content type of each part is detected from the file extension, and the method defaults to POST. Repeat --upload to send several files; by default they share the field name from --uploadField, and writing an entry as field=path names that part explicitly. When --body is given alongside a form-data upload it must be a JSON object, and its entries are sent as additional text fields in the same form.

multipart/related (for Google Drive/Gmail-style uploads): pass --header "Content-Type: multipart/related" and the body is built differently — --body becomes a single JSON metadata part (application/json, sent verbatim rather than split into fields), followed by one media part per --upload. Each media part's Content-Type comes from the file extension, or write the entry as mime/type=path to set it explicitly (e.g. --upload "text/markdown=article.md"). The generated boundary is appended and Content-Length is set. This is the shape Drive's uploadType=multipart expects for converting a file into a Doc, Sheet or Slides deck.

# Import Markdown as a Google Doc (Drive converts on upload)
aux4 curl auth-request --provider google --method POST \
  --header "Content-Type: multipart/related" \
  --url "https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart" \
  --body '{"name":"My Doc","mimeType":"application/vnd.google-apps.document"}' \
  --upload "text/markdown=./article.md"

Downloading binary responses

--output writes the response body to a file rather than stdout, which is required for images, archives, and other binary payloads. If the server returns an error status, the response is written to stderr and no file is created, so a failed download never leaves a truncated or error-filled file behind.

Health checks and timeouts

--status prints only the numeric HTTP status code and nothing else — no body, headers, or output file. It exits 0 for any status class, so 200, 404 and 503 all succeed and you can capture a "down" code in a pipeline or a monitor. --status overrides --showHeaders and --output. A transport failure — DNS, connection refused, TLS, or a --maxTime timeout — prints nothing to stdout, writes the error to stderr, and exits 1, distinguishing an unreachable host from a real status code.

--maxTime bounds the request with a timeout in seconds (decimals allowed, e.g. 2.5). 0 or omitting it means no timeout (the default). Exceeding it fails like any transport error: an error on stderr and exit 1 (with --status, empty stdout + exit 1). This is the primitive aux4/uptime uses for command-based health probes.

# Print just the status code
aux4 curl request --status true https://example.com
# -> 200

# Fail fast if the host is slow or hung
aux4 curl request --maxTime 2.5 https://example.com

# Bounded health probe
aux4 curl request --status true --maxTime 2 https://example.com
# -> 200

Note: --status is a boolean flag — pass it as --status true, or write it bare after the URL (aux4 curl request https://example.com --status). A bare --status before the URL would consume the URL as its value (the same rule that applies to --showHeaders).

Examples

# GET request
aux4 curl request https://api.example.com/users

# POST with inline body
aux4 curl request --method POST --body '{"name":"Alice"}' --header "Content-Type: application/json" https://api.example.com/users

# Show response headers
aux4 curl request --showHeaders true https://api.example.com/health

# Upload a file, with an extra text field in the same form
aux4 curl request --method POST --upload ./logo.png --uploadField media --body '{"media_category":"tweet_image"}' https://api.example.com/media

# Upload several files, each under its own field name
aux4 curl request --method POST --upload "avatar=./avatar.png" --upload "banner=./banner.png" https://api.example.com/profile

# Download a binary response to a file
aux4 curl request --output ./report.pdf https://api.example.com/reports/1

# Send a file's contents as the raw body
aux4 curl request --method PUT --bodyFile ./payload.json --header "Content-Type: application/json" https://api.example.com/resource/1

aux4 curl stream

Read NDJSON (newline-delimited JSON) from stdin. For each JSON line, make an HTTP request to the given URL with that line as the body. Output results as NDJSON, one JSON object per line.

aux4 curl stream [--method <METHOD>] [--header <Header: Value>] [--concurrency <N>] [--maxTime <seconds>] <url>

| Flag | Description | Default | |------|-------------|---------| | --method | HTTP method | POST | | --header | Request header in Name: Value format (repeatable) | | | --concurrency | Number of concurrent requests | 1 | | --maxTime | Per-request timeout in seconds (decimals allowed); 0 means no timeout | 0 | | url | Request URL (positional argument) | required |

Each output line is a JSON object with:

  • status — HTTP status code (or 0 on connection error or timeout)
  • body — Response body (parsed as JSON if valid, otherwise a string)
  • input — The original input JSON that triggered this request

--maxTime sets a per-request timeout. A timed-out line is treated as a transport error — it emits {"error":"…timeout…","input":…,"status":0} and the stream still exits 0, so one slow upstream never aborts the batch.

Examples

# Process each user record
cat users.ndjson | aux4 curl stream https://api.example.com/process

# With concurrency
cat records.ndjson | aux4 curl stream --concurrency 5 https://api.example.com/batch

# Custom method and headers
cat updates.ndjson | aux4 curl stream --method PUT --header "Authorization: Bearer token123" https://api.example.com/update

# Chain with jq for filtering
cat items.ndjson | aux4 curl stream https://api.example.com/enrich | jq 'select(.status == 200) | .body'

# Bound each request with a timeout (slow lines become status:0 error objects)
cat records.ndjson | aux4 curl stream --maxTime 5 https://api.example.com/batch

OAuth2 Authentication

The oauth command group manages OAuth2 tokens using the authorization code flow with PKCE (Proof Key for Code Exchange). PKCE is automatically enabled for all providers, ensuring compatibility with providers like X (Twitter) that require it. Tokens are stored locally in .oauth/<provider>.json by default. Add .oauth/ to your .gitignore.

> Token storage. Tokens are written as a plaintext JSON file with 0600 permissions (owner read/write only) in a 0700 directory — the same posture as the gcloud, aws and gh CLIs. The file contains the access token, the long-lived refresh token, and the client id/secret, so it should be treated as a credential: keep it out of version control and off shared machines. It is not encrypted at rest; anyone who can read the file as your user can use the tokens.

aux4 curl oauth login

Authenticate with an OAuth2 provider using the authorization code flow with PKCE. Opens a local callback server and prints a URL to authorize in the browser.

aux4 curl oauth login <provider> --clientId <id> --clientSecret <secret> --authUrl <url> --tokenUrl <url> --scopes <scopes> [--callbackPort <port>] [--tokenFile <path>]

| Flag | Description | Default | |------|-------------|---------| | provider | Provider name (positional argument) | required | | --clientId | OAuth client ID | required | | --clientSecret | OAuth client secret | required | | --authUrl | Authorization endpoint URL | required | | --tokenUrl | Token exchange endpoint URL | required | | --scopes | Comma-separated scopes | required | | --callbackPort | Local callback server port | 9876 | | --tokenFile | Custom token file path | .oauth/<provider>.json |

Example

aux4 curl oauth login pinterest \
  --clientId abc123 \
  --clientSecret mysecret \
  --authUrl https://www.pinterest.com/oauth/ \
  --tokenUrl https://api.pinterest.com/v5/oauth/token \
  --scopes boards:read,pins:read,pins:write

aux4 curl oauth token

Print a valid access token to stdout. Automatically refreshes if expired.

aux4 curl oauth token <provider> [--tokenFile <path>]

Example

# Get token
aux4 curl oauth token pinterest

# Use in a script
TOKEN=$(aux4 curl oauth token pinterest)
curl -H "Authorization: Bearer $TOKEN" https://api.pinterest.com/v5/user_account

aux4 curl oauth status

Show token status, scopes, expiry, and whether a refresh token is available.

aux4 curl oauth status <provider> [--tokenFile <path>]

Example

aux4 curl oauth status pinterest
Provider:      pinterest
Status:        valid
Scopes:        boards:read,pins:read,pins:write
Expires at:    2026-04-28T12:30:00Z
Refresh token: yes
Token file:    .oauth/pinterest.json

aux4 curl oauth logout

Remove the stored token file for a provider. Does not revoke the token on the provider side.

aux4 curl oauth logout <provider> [--tokenFile <path>]

Authenticated Requests

aux4 curl auth-request

Same as request but automatically injects the Authorization: Bearer <token> header. Reads the stored token for the provider, refreshes if expired, and adds the header before making the request.

aux4 curl auth-request --provider <name> [--tokenFile <path>] [--method <METHOD>] [--header <Header: Value>] [--body <data>] [--showHeaders <true|false>] [--upload <path>] [--uploadField <name>] [--output <path>] <url>

| Flag | Description | Default | |------|-------------|---------| | --provider | OAuth provider name | required | | --tokenFile | Custom token file path | .oauth/<provider>.json | | --method | HTTP method | GET | | --header | Request header (repeatable) | | | --body | Request body, or a JSON object of extra form fields when uploading | | | --bodyFile | Send the contents of this file as the raw request body | | | --showHeaders | Include response headers | false | | --upload | File to send as multipart/form-data (repeatable, field=path to name the part) | | | --uploadField | Default form field name for uploaded files | file | | --output | Write the response body to this file instead of stdout | | | url | Request URL (positional argument) | required |

File uploads and binary downloads work exactly as in request — see Uploading files and Downloading binary responses.

Examples

# GET with auth
aux4 curl auth-request --provider pinterest https://api.pinterest.com/v5/user_account

# POST with auth
aux4 curl auth-request --provider pinterest --method POST \
  --body '{"title":"My Pin","board_id":"123"}' \
  --header "Content-Type: application/json" \
  https://api.pinterest.com/v5/pins

aux4 curl auth-stream

Same as stream but automatically injects the Authorization: Bearer <token> header for each request. Reads NDJSON from stdin, makes one authenticated HTTP request per line, outputs NDJSON results.

echo '<ndjson>' | aux4 curl auth-stream --provider <name> [--tokenFile <path>] [--method <METHOD>] [--header <Header: Value>] [--concurrency <n>] <url>

| Flag | Description | Default | |------|-------------|---------| | --provider | OAuth provider name | required | | --tokenFile | Custom token file path | .oauth/<provider>.json | | --method | HTTP method | POST | | --header | Request header (repeatable) | | | --concurrency | Number of concurrent requests | 1 | | url | Request URL (positional argument) | required |

Example

echo '{"title":"Pin 1","board_id":"123"}
{"title":"Pin 2","board_id":"123"}' | aux4 curl auth-stream --provider pinterest \
  --header "Content-Type: application/json" \
  https://api.pinterest.com/v5/pins

Token File

By default, tokens are stored in .oauth/<provider>.json in the current directory. Override with --tokenFile on any command:

aux4 curl oauth login myprovider --tokenFile /custom/path/token.json ...
aux4 curl auth-request --provider myprovider --tokenFile /custom/path/token.json https://api.example.com/data