npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@aiconnect/s3cli

v1.5.1

Published

Simple CLI for S3 operations using AWS SDK. Compatible with Supabase Storage, AWS S3, MinIO and other S3-compatible providers.

Readme

s3cli

Agent-native CLI for S3 operations using AWS SDK. Compatible with Supabase Storage, AWS S3, MinIO and other S3-compatible providers.

Every command emits a stable, structured JSON envelope when invoked from a pipe or subprocess, and a human-friendly output when used in a TTY — so the same binary is consumed by humans in a terminal and by LLM agents over stdout.

Installation

npm install
npm run build

For development, you can use npm run start -- <command> instead of npm link + s3cli.

Configuration

The quickest way to get started is with the interactive init command:

s3cli init

This creates $XDG_CONFIG_HOME/s3cli/.env (defaults to ~/.config/s3cli/.env) with your credentials, with permissions set to 0600.

Config resolution order

  1. --env-file <path> — custom .env file passed via CLI flag (highest priority).
  2. --profile <slug> — named profile loaded from $XDG_CONFIG_HOME/s3cli/<slug>.env (with legacy fallback at ~/.s3cli/<slug>.env). See Profiles below.
  3. $XDG_CONFIG_HOME/s3cli/.env — XDG-compliant location (default ~/.config/s3cli/.env).
  4. ~/.s3cli/.env — legacy path; loading from here emits a deprecation warning on stderr.
  5. Shell environment variables — already exported in the shell (lowest priority, not overwritten by dotenv).

--env-file and --profile are mutually exclusive — combining them exits with USAGE_ERROR.

If no config is found, s3cli will tell you to run s3cli init.

Profiles

Use --profile <slug> to switch between named configurations (e.g., production, staging, MinIO local) without editing files or repeating --env-file:

s3cli ls --profile staging
s3cli upload local.txt remote.txt --profile production

Each profile is a separate .env file with the same schema as the default config, named <slug>.env and stored in the same directory:

# Create a "staging" profile by copying the default config:
cp ~/.config/s3cli/.env ~/.config/s3cli/staging.env
$EDITOR ~/.config/s3cli/staging.env  # adjust endpoint, bucket, credentials

Slug rules: must match [a-zA-Z0-9._-]+ (letters, digits, dot, underscore, hyphen). Path separators, spaces, and empty strings are rejected with USAGE_ERROR. If the profile file does not exist at either the XDG path or the legacy ~/.s3cli/<slug>.env, the command exits with USAGE_ERROR rather than falling back to the default .env.

Migrating from the legacy path

If you still have config at ~/.s3cli/.env, migrate it with:

s3cli init --migrate-config

This copies ~/.s3cli/.env to ~/.config/s3cli/.env (or $XDG_CONFIG_HOME/s3cli/.env when set) and leaves the legacy file in place. Failures (no legacy file, or XDG file already present) exit with code 30.

Breaking change (v1.3.0): Local .env files in the current directory are no longer loaded automatically. If you relied on this behavior, use --env-file ./.env explicitly or move your config to ~/.config/s3cli/.env.

Manual setup

You can also create the .env file manually:

S3_ENDPOINT=https://your-project.supabase.co/storage/v1/s3
S3_ACCESS_KEY=your-access-key
S3_SECRET_KEY=your-secret-key
S3_USE_SSL=true
S3_BUCKET=your-bucket
S3_REGION=eu-central-1
S3_PATH_STYLE=true
S3CLI_DEFAULT_EXPIRY=3600

To inspect the active configuration (with secrets masked):

s3cli config show

Output format

All commands accept a global --format <json|human> flag. The default is TTY-aware: human when stdout is a terminal, json when it's piped, redirected, or captured by a subprocess. Pass --format explicitly to override.

  • stdout — exclusive for the structured response (JSON envelope or one-line human message).
  • stderr — operational logs, progress lines, and deprecation warnings.

This means s3cli put a.txt remote/a.txt --format json | jq . is always safe.

Success envelope

{
  "_apiVersion": "1.0",
  "ok": true,
  "command": "put",
  "result": {
    "bucket": "my-bucket",
    "key": "remote/a.txt",
    "etag": "\"abc123\"",
    "size": 1024,
    "contentType": "application/json",
    "versionId": null
  }
}

Error envelope

{
  "_apiVersion": "1.0",
  "ok": false,
  "command": "put",
  "errorCode": "PRECONDITION_FAILED",
  "message": "If-Match precondition did not match current ETag.",
  "remediation": "Call 's3cli head <remote>' to get the current ETag and retry.",
  "context": {
    "remote": "skills/onboarding.json",
    "expectedETag": "\"abc123\""
  }
}

Exit codes

| Code | Name | When | |------|------|------| | 0 | SUCCESS | Command completed | | 2 | USAGE_ERROR | Invalid arguments or local file missing | | 30 | PRECONDITION_FAILED | Conditional write failed (--if-match/--if-none-match) or precondition for init --migrate-config not met | | 40 | IO_ERROR | Generic S3 / network / config error | | 50 | NOT_FOUND | Object or source key does not exist |

Remote argument syntax

Most commands accept the remote target either as a plain key or as a full s3://<bucket>/<key> URI:

# Equivalent — uses the configured S3_BUCKET (or --bucket flag)
s3cli get path/file.txt
s3cli --bucket my-bucket get path/file.txt

# URI form — explicit bucket, ignores S3_BUCKET
s3cli get s3://my-bucket/path/file.txt

# Cross-bucket copy with URIs on both sides
s3cli cp s3://source-bucket/x.txt s3://dest-bucket/y.txt

Commands that accept URIs: get, put, rm, head, url, copy/cp, upload. (ls does not — it operates on a prefix and uses the configured bucket.)

Rules:

  • The URI bucket has precedence over --bucket and S3_BUCKET. If --bucket is also passed with a different value, the command exits with USAGE_ERROR.
  • S3_BUCKET is optional in the config file when you always provide a URI or --bucket. Commands that need a bucket and cannot resolve one from any source exit with USAGE_ERROR.
  • Other schemes (https://, s3a://, …) are not parsed — they are treated as literal keys.
  • Percent-encoded characters in the key are preserved literally (e.g. s3://bucket/a%20b.txt ⇒ key a%20b.txt).

Usage

List files (ls)

# List all files (default: first 100)
s3cli ls

# Filter by prefix
s3cli ls folder/

# Local pagination
s3cli ls --limit 50 --offset 50

# No limit (all files)
s3cli ls --limit 0

# Directory-style listing with delimiter
s3cli ls skills/ --delimiter "/"

# Server-side pagination via S3 continuation token
s3cli ls --continuation-token "<token-from-previous-page>"

In --format json, the response includes objects, total, isTruncated, and (when applicable) commonPrefixes and nextContinuationToken.

Upload (upload) — simple uploads

s3cli upload file.txt backup/file.txt

upload auto-detects Content-Type by extension and supports public-read ACL:

# Override the Content-Type
s3cli upload file.txt backup/file.txt --content-type text/plain

# Public-read ACL
s3cli upload file.txt backup/file.txt --public

Put (put) — uploads with conditional writes & metadata

put is the agent-oriented upload primitive. Use it when you need conditional writes or custom metadata.

# Basic upload (equivalent to `upload`, but no --public)
s3cli put file.txt backup/file.txt

# Override Content-Type
s3cli put file.txt backup/file.txt --content-type text/plain

# Conditional update — only overwrite if the remote ETag still matches
s3cli put file.txt backup/file.txt --if-match '"abc123"'

# Conditional create — only upload if the key does NOT exist
s3cli put file.txt backup/file.txt --if-none-match

# Custom metadata (key=value, comma-separated)
s3cli put file.txt backup/file.txt --metadata 'publishedBy=agent-001,version=2'

--if-match and --if-none-match are mutually exclusive. On precondition failure, put exits with code 30 and an errorCode of PRECONDITION_FAILED or KEY_ALREADY_EXISTS.

Reading content from stdin or another file (--body)

When --body is set, <source> becomes the S3 key and the upload content comes from the path passed to --body (or from stdin if --body -):

# Upload from stdin (e.g. piping JSON output from another command)
echo '{"hello":"world"}' | s3cli put skills/hello.json --body -

# Upload from an arbitrary file (key derived from the S3 path, not from --body)
s3cli put assets/app.js --body /tmp/built-app.js

Notes:

  • <remote> positional is rejected when --body is used — the key is <source>. Combining both exits with USAGE_ERROR (code 2).
  • Content-Type is auto-detected from the S3 key extension (not from the --body path). Override with --content-type when needed.
  • --body - buffers stdin in memory up to 50 MB. For larger payloads, write to a temp file and pass it via --body <path>. The limit is overridable with S3CLI_STDIN_MAX_BYTES=<bytes>; exceeding it exits with USAGE_ERROR.

Head (head) — metadata + ETag without download

s3cli head backup/file.txt

Returns object metadata, ETag, size, content-type, last-modified, custom metadata, and version ID. Missing keys are not errors — they return ok: true with result.exists: false, which is the natural shape for "check then act" agent flows.

Copy (copy) — server-side copy

# Copy within the configured bucket
s3cli copy current/SKILL.md previous/SKILL.md

# Copy from a different source bucket
s3cli copy src.txt dst.txt --source-bucket other-bucket

# Replace metadata during copy
s3cli copy a.txt b.txt --metadata-directive REPLACE --metadata 'owner=team-x'

--metadata requires --metadata-directive REPLACE. A missing source key exits with code 50 (NOT_FOUND).

Download (get)

# Download keeping original name
s3cli get backup/file.txt

# Download with new name
s3cli get backup/file.txt restored-file.txt

# Conditional download — fails with code 30 if the remote ETag drifted
s3cli get backup/file.txt --if-match '"abc123"'

Delete (rm)

s3cli rm backup/file.txt

By default, deleting a missing key is a noop (success with result.action: "noop"), which keeps agent retries idempotent. To treat a missing key as an error instead:

s3cli rm backup/file.txt --not-found-error

Generate signed URL (url)

# URL valid for 1 hour (default)
s3cli url backup/file.txt

# URL valid for 24 hours (86400 seconds)
s3cli url backup/file.txt 86400

# Only the URL (no message, ideal for pipes)
s3cli url backup/file.txt --quiet

# Automatically shortened URL (requires S3CLI_SHORTENER_CMD)
s3cli url backup/file.txt --shorten

# Combined: short and clean URL
s3cli url backup/file.txt -qs

# Unsigned URL (for public buckets / Supabase Storage)
s3cli url backup/file.txt --unsigned

In --format json, the response includes url, expiresInSeconds, and expiresAt (ISO 8601). Unsigned URLs return unsigned: true instead of an expiry.

URL Shortener

To use the --shorten flag, configure the S3CLI_SHORTENER_CMD environment variable:

# In .env file
S3CLI_SHORTENER_CMD=shortener

# Or export directly
export S3CLI_SHORTENER_CMD=shortener

The command should accept a URL as argument and output the shortened URL to stdout. Example with shortener:

shortener "https://example.com/long/url"
# Output: https://short.io/abc123

s3cli url file.txt --shorten
# Output: https://short.io/xyz789

Help

Each command supports two help formats:

# Classic Unix-style help (default in TTY)
s3cli put --help

# Markdown-formatted help inside a JSON envelope (agent-friendly)
s3cli put --help --format json

Commands

| Command | Description | |---------|-------------| | init [--migrate-config] | Interactive setup; --migrate-config moves ~/.s3cli/.env to the XDG path | | config show | Print active configuration (secrets masked) | | ls [prefix] | List files (--limit, --offset, --delimiter, --continuation-token) | | upload <local> [remote] | Simple upload (--content-type, --public for public-read ACL) | | put <source> [remote] | Upload with conditional writes / metadata (--if-match, --if-none-match, --metadata, --content-type). With --body <path\|->, <source> becomes the S3 key (--body - reads stdin, max 50MB) | | head <remote> | Metadata + ETag without downloading | | copy <src> <dst> | Server-side copy (--source-bucket, --metadata-directive, --metadata) | | get <remote> [local] | Download a file (--if-match) | | rm <remote> | Delete a file (--not-found-error) | | url <remote> [expiry] | Generate a signed URL (--unsigned, --quiet, --shorten) |

upload and put coexist: use upload for the simple "send this file" path (with --public ACL support), and put whenever you need conditional writes or custom metadata.

Global flags

| Flag | Description | |------|-------------| | --bucket <name> | Override the default bucket from config | | --env-file <path> | Load configuration from a custom .env file | | --profile <slug> | Load configuration from named profile (<slug>.env in config dir); mutually exclusive with --env-file | | --format <json\|human> | Output format (default: human in TTY, json when piped) |

Global Installation

To use s3cli as a global command:

npm run build
npm link

Then:

s3cli ls
s3cli put file.txt remote/file.txt --if-none-match
s3cli url remote/file.txt 3600

License

MIT