#!/bin/bash # Piped SDK - Simple shell script client for executing pipelines # Uses curl to make HTTP requests # # Usage: piped.sh [pipeline...] # # With no arguments and an interactive terminal (TTY): enters REPL mode. # With no arguments and stdin not a TTY: prints usage and exits. # With arguments: runs the pipeline once (single-shot). # # Environment Variables: # PIPED_API_KEY - Your API key (required) # PIPED_SERVER_URL - Server URL for the API (default: https://piped.sh) # PIPED_REPL_PROMPT - REPL prompt string (default: piped> ) # PIPED_SDK_DIR - Directory containing piped.js (optional; default: same dir as this script) # PIPED_USE_BASH - Set to 1 to force pure-bash and skip Node/Ruby/Python # # Arguments (single-shot): # --mcp Write .mcp.json in the current directory (only if it does not exist) # for MCP server configuration. Uses PIPED_API_KEY when set. # pipeline - The pipeline string to execute (e.g., "cat | grep hello | wc -l") # All arguments are concatenated into a single pipeline string # # REPL commands: exit, quit # # Input: Input data is read from stdin if available (single-shot) # # Examples: # export PIPED_API_KEY="pk_your_api_key_here" # ./piped.sh "cat | grep hello" # echo "hello world" | ./piped.sh "cat | grep hello" # ./piped.sh # no args + TTY → REPL # ./piped.sh --mcp # write .mcp.json in current dir (if missing) # # Tries in order: piped.js (Bun then Node), piped.rb (Ruby), piped.py (Python), then pure bash below. # Those scripts can also be run stand-alone (e.g. node piped.js --mcp, ruby piped.rb --mcp, python3 piped.py --mcp). # Set PIPED_USE_BASH=1 to force the pure-bash implementation and skip the runtimes above. # Set PIPED_SDK_DIR to a directory containing piped.js (and optionally piped.py, piped.rb) # when piped.sh is installed alone (e.g. in ~/.local/bin); otherwise runtimes are looked up # next to this script. # Resolve script path so we find piped.js when run by name from PATH (e.g. "piped" → $0 is "piped", not path) _script="${BASH_SOURCE[0]:-$0}" if [[ "$_script" != */* ]]; then _script="$(command -v "$_script" 2>/dev/null)" || true fi SCRIPT_DIR="$(cd "$(dirname "$_script")" && pwd)" if [ -n "${PIPED_SDK_DIR:-}" ]; then SDK_DIR="$(cd "$PIPED_SDK_DIR" && pwd)" else SDK_DIR="$SCRIPT_DIR" fi # Quote/join helpers used when delegating to runtimes so "alert \"hello world\"" stays one arg _quote_arg() { local arg="$1" if [[ "$arg" =~ [[:space:]] ]] || [[ "$arg" =~ [\"\'\\\$] ]] || [[ "$arg" =~ [\(\)\{\}\[\]\;\&\|\<\>] ]]; then local escaped="${arg//\\/\\\\}" escaped="${escaped//\"/\\\"}" printf "\"%s\"" "$escaped" else printf "%s" "$arg" fi } _join_args() { local first=1 for arg in "$@"; do if [ $first -eq 1 ]; then first=0; else printf " "; fi _quote_arg "$arg" done } if [ -z "${PIPED_USE_BASH:-}" ]; then if [ $# -ge 1 ]; then if [ -f "$SDK_DIR/piped.js" ]; then if command -v bun >/dev/null 2>&1; then exec bun "$SDK_DIR/piped.js" "$@"; fi if command -v node >/dev/null 2>&1; then exec node "$SDK_DIR/piped.js" "$@"; fi fi if [ -f "$SDK_DIR/piped.rb" ]; then if command -v ruby >/dev/null 2>&1; then exec ruby "$SDK_DIR/piped.rb" "$@"; fi fi if [ -f "$SDK_DIR/piped.py" ]; then if command -v python3 >/dev/null 2>&1; then exec python3 "$SDK_DIR/piped.py" "$@"; fi fi else # No args: delegate to runtime for REPL (or usage) if [ -f "$SDK_DIR/piped.js" ]; then if command -v bun >/dev/null 2>&1; then exec bun "$SDK_DIR/piped.js"; fi if command -v node >/dev/null 2>&1; then exec node "$SDK_DIR/piped.js"; fi fi if [ -f "$SDK_DIR/piped.rb" ]; then if command -v ruby >/dev/null 2>&1; then exec ruby "$SDK_DIR/piped.rb"; fi fi if [ -f "$SDK_DIR/piped.py" ]; then if command -v python3 >/dev/null 2>&1; then exec python3 "$SDK_DIR/piped.py"; fi fi fi fi set -euo pipefail SERVER_URL="${PIPED_SERVER_URL:-https://piped.sh}" SERVER_URL="${SERVER_URL%/}" API_KEY="${PIPED_API_KEY:-}" # --- --mcp: write .mcp.json in current directory (only if missing) --- if [ $# -ge 1 ] && [ "$1" = "--mcp" ]; then MCP_FILE=".mcp.json" if [ -f "$MCP_FILE" ]; then printf '.mcp.json already exists\n' >&2 exit 0 fi API_KEY_FOR_MCP="${PIPED_API_KEY:-pk_your_api_key_here}" cat << EOF > "$MCP_FILE" { "mcpServers": { "piped": { "type": "http", "url": "${SERVER_URL}/mcp", "headers": { "X-API-Key": "${API_KEY_FOR_MCP}" } } } } EOF printf 'Wrote %s\n' "$MCP_FILE" >&2 exit 0 fi # Get current time in ISO 8601 format for X-User-Time header _get_user_time() { if command -v date >/dev/null 2>&1; then date +"%Y-%m-%dT%H:%M:%S%z" | sed 's/\([0-9][0-9]\)$/:\1/' elif command -v python3 >/dev/null 2>&1; then python3 -c "from datetime import datetime; print(datetime.now().astimezone().isoformat())" elif command -v node >/dev/null 2>&1; then node -e "const d = new Date(); const pad = n => String(n).padStart(2, '0'); const offset = -d.getTimezoneOffset(); const h = pad(Math.floor(Math.abs(offset)/60)); const m = pad(Math.abs(offset)%60); console.log(\`\${d.getFullYear()}-\${pad(d.getMonth()+1)}-\${pad(d.getDate())}T\${pad(d.getHours())}:\${pad(d.getMinutes())}:\${pad(d.getSeconds())}\${offset>=0?'+':'-'}\${h}:\${m}\`)" else date +"%Y-%m-%dT%H:%M:%S" 2>/dev/null || echo "" fi } # Encode pipeline for URL query (no stdin consumed) _encode_pipeline() { local PIPELINE="$1" if command -v jq >/dev/null 2>&1; then printf '%s' "$PIPELINE" | jq -sRr @uri elif command -v python3 >/dev/null 2>&1; then printf '%s' "$PIPELINE" | python3 -c "import sys, urllib.parse; print(urllib.parse.quote(sys.stdin.read()), end='')" elif command -v node >/dev/null 2>&1; then printf '%s' "$PIPELINE" | node -e "process.stdout.write(encodeURIComponent(require('fs').readFileSync(0,'utf-8')))" else # Encode % first to avoid double-encoding, then encode all RFC 3986 reserved + unsafe chars printf '%s' "$PIPELINE" | sed \ -e 's/%/%25/g' \ -e 's/ /%20/g' \ -e 's/!/%21/g' \ -e "s/'/%27/g" \ -e 's/(/%28/g' \ -e 's/)/%29/g' \ -e 's/\*/%2A/g' \ -e 's/+/%2B/g' \ -e 's/,/%2C/g' \ -e 's/:/%3A/g' \ -e 's/;/%3B/g' \ -e 's/=/%3D/g' \ -e 's/?/%3F/g' \ -e 's/@/%40/g' \ -e 's/"/%22/g' \ -e 's/#/%23/g' \ -e 's/\$/%24/g' \ -e 's/&/%26/g' \ -e 's/|/%7C/g' \ -e 's/\[/%5B/g' \ -e 's/\]/%5D/g' \ -e 's/{/%7B/g' \ -e 's/}/%7D/g' \ -e 's/\\/%5C/g' \ -e 's/\^/%5E/g' \ -e 's/`/%60/g' \ -e 's//%3E/g' fi } # Extract error message from JSON response body _parse_error() { local BODY="$1" HTTP_CODE="$2" local MSG="" # Try jq first, then grep with flexible whitespace around colon if command -v jq >/dev/null 2>&1; then MSG=$(printf '%s' "$BODY" | jq -r '.error // empty' 2>/dev/null) || true fi if [ -z "$MSG" ]; then MSG=$(printf '%s' "$BODY" | grep -oE '"error"\s*:\s*"[^"]*"' | head -1 | sed 's/.*:.*"\(.*\)"/\1/' 2>/dev/null) || true fi if [ -z "$MSG" ]; then MSG="Request failed with status $HTTP_CODE" fi printf '%s' "$MSG" } # Run a pipeline: usage run_pipeline "" "" # When input is empty, sends pipeline as body. When input is non-empty, sends pipeline in query and input as body via stdin. # On success prints body to stdout and returns 0; on failure prints error to stderr and returns 1. run_pipeline() { local PIPELINE="$1" local INPUT="${2:-}" local USER_TIME USER_TIME=$(_get_user_time) local RESPONSE HTTP_CODE BODY ENCODED_PIPELINE if [ -z "$API_KEY" ]; then printf 'Error: PIPED_API_KEY environment variable is required\n' >&2 return 1 fi if [ -n "$INPUT" ]; then ENCODED_PIPELINE=$(_encode_pipeline "$PIPELINE") CURL_ARGS=(-s -w "\n%{http_code}" -X POST -H "Content-Type: application/octet-stream" -H "X-API-Key: $API_KEY") [ -n "$USER_TIME" ] && CURL_ARGS+=(-H "X-User-Time: $USER_TIME") CURL_ARGS+=(--data-binary @- "${SERVER_URL}/?pipeline=${ENCODED_PIPELINE}") RESPONSE=$(printf '%s' "$INPUT" | curl "${CURL_ARGS[@]}") else CURL_ARGS=(-s -w "\n%{http_code}" -X POST -H "Content-Type: text/plain" -H "X-API-Key: $API_KEY") [ -n "$USER_TIME" ] && CURL_ARGS+=(-H "X-User-Time: $USER_TIME") CURL_ARGS+=(--data-binary "$PIPELINE" "${SERVER_URL}/") RESPONSE=$(curl "${CURL_ARGS[@]}") fi # Extract HTTP code from last line, body from everything before it HTTP_CODE="${RESPONSE##*$'\n'}" BODY="${RESPONSE%$'\n'"$HTTP_CODE"}" if [[ "$HTTP_CODE" =~ ^[0-9]+$ ]] && [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then printf '%s\n' "$BODY" return 0 else printf 'Error: %s\n' "$(_parse_error "$BODY" "$HTTP_CODE")" >&2 return 1 fi } # Run pipeline with raw stdin as body (no UTF-8; server detects type from magic bytes). # Stdin is consumed by curl. Call only when stdin is not a TTY. run_pipeline_raw_stdin() { local PIPELINE="$1" local USER_TIME USER_TIME=$(_get_user_time) local ENCODED_PIPELINE RESPONSE BODY HTTP_CODE if [ -z "$API_KEY" ]; then printf 'Error: PIPED_API_KEY environment variable is required\n' >&2 return 1 fi ENCODED_PIPELINE=$(_encode_pipeline "$PIPELINE") CURL_ARGS=(-s -w "\n%{http_code}" -X POST -H "Content-Type: application/octet-stream" -H "X-API-Key: $API_KEY") [ -n "$USER_TIME" ] && CURL_ARGS+=(-H "X-User-Time: $USER_TIME") RESPONSE=$(curl "${CURL_ARGS[@]}" --data-binary @- "${SERVER_URL}/?pipeline=${ENCODED_PIPELINE}") # Extract HTTP code from last line, body from everything before it HTTP_CODE="${RESPONSE##*$'\n'}" BODY="${RESPONSE%$'\n'"$HTTP_CODE"}" if [[ "$HTTP_CODE" =~ ^[0-9]+$ ]] && [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then printf '%s\n' "$BODY" return 0 else printf 'Error: %s\n' "$(_parse_error "$BODY" "$HTTP_CODE")" >&2 return 1 fi } # --- Single-shot: arguments provided (_quote_arg / _join_args defined at top) --- if [ $# -ge 1 ]; then if [ $# -eq 1 ]; then PIPELINE="$1" else PIPELINE=$(_join_args "$@") fi if [ -z "$API_KEY" ]; then printf 'Error: PIPED_API_KEY environment variable is required\n' >&2 printf 'Set it with: export PIPED_API_KEY="pk_your_api_key_here"\n' >&2 exit 1 fi if [ ! -t 0 ]; then # Stdin piped: send raw body with pipeline in query (server detects type from magic bytes) run_pipeline_raw_stdin "$PIPELINE" && exit 0 || exit 1 fi if run_pipeline "$PIPELINE" ""; then exit 0 else exit 1 fi fi # --- No arguments: REPL if TTY, else usage + exit 1 --- if [ ! -t 0 ]; then printf 'Error: pipeline is required\n' >&2 printf 'Usage: %s \n' "$0" >&2 printf 'With no arguments and a TTY, %s enters REPL mode.\n' "$0" >&2 if [ -z "${PIPED_API_KEY:-}" ]; then printf 'Note: Set PIPED_API_KEY environment variable with your API key\n' >&2 fi exit 1 fi # --- REPL mode --- PROMPT="${PIPED_REPL_PROMPT:-piped> }" while true; do printf '%s' "$PROMPT" if ! IFS= read -r line; then break fi # Normalize: trim, strip leading /, trim again line="${line#"${line%%[![:space:]]*}"}" line="${line%"${line##*[![:space:]]}"}" line="${line#/}" line="${line#"${line%%[![:space:]]*}"}" line="${line%"${line##*[![:space:]]}"}" case "$line" in exit|quit) exit 0 ;; "") ;; *) if [ -z "$API_KEY" ]; then printf 'Error: PIPED_API_KEY environment variable is required\n' >&2 continue fi run_pipeline "$line" "" || true ;; esac done exit 0