commit 79549492695bf47ecbc04a090a3b4b8aee3deff0 Author: Luxferre Date: Sun Oct 19 11:55:24 2025 +0300 first commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..31edfe8 --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +# sh-goodies: a useful collection of single-file, POSIX-compatible shell scripts + +## List + +* [DaemonTamer V2](./dt): local service manager to daemonize any command +* [Outport](./outport): expose reverse tunnels via SSH +* [shee.sh](./shee.sh): simple LLM chat application for OpenAI-compatible endpoints +* [ShellFlow](./flow.sh): simple workflow orchestration script +* [LShar](./lshar.sh): create self-extracting archives in the form of shell scripts +* [ShellBeat](./shellbeat.sh): a [bytebeat](https://stellartux.github.io/websynth/guide.html) audio formula player in pure shell (also depends on xxd) +* [GHMD](./ghmd.sh): a command suite to set up your own self-hosted Git repositories diff --git a/dt b/dt new file mode 100755 index 0000000..17fca71 --- /dev/null +++ b/dt @@ -0,0 +1,106 @@ +#!/bin/sh +# DaemonTamer: simple and portable local service manager in pure sh +# that allows to quickly daemonize any command and manage it as a service +# Depends on POSIX versions of sh, nohup, cut and grep +# the unreg option also depends on mktemp (not POSIX but present everywhere) +# Run sh dt help to see the parameters +# Created by Luxferre in 2025, released into public domain with no warranties + +err() { + echo $* + exit 1 +} + +msgexit() { + echo $* + exit 0 +} + +find_service() { + sline="$(grep -E "^$1\s+" $CONF)" + [ -z "$sline" ] && err "No such service found in the config!" + echo "$sline" +} + +CONF="$HOME/.dt.conf" +TMPDIR="$(dirname $(mktemp -u))" +srv_name="$2" +[ "$1" != "help" ] && [ -z "$srv_name" ] && err "Service name is required!" +pidfile="$TMPDIR/$srv_name.pid" +case $1 in +help) + echo 'DaemonTamer v2' + echo '--------------' + echo "Usage: $0 {reg|unreg|start|stop|restart} SERVICE_NAME [REG_PARAMS]" + echo 'Parameters for the reg subcommand:' + echo "$0 reg 'COMMAND' [WORKDIR] [STDOUT_LOG_FILE] [STDERR_LOG_FILE] [PRESTART] [POSTSTART] [PRESTOP] [POSTSTOP]" + echo '(the COMMAND must be specified in single quotes and without nohup)' + echo + echo 'Created by Luxferre in 2025, released into public domain with no warranties' + ;; +reg) + srv_cmd="$3" + [ -z "$srv_cmd" ] && err "Service runner command is required!" + srv_wdir="$4" + srv_outlog="$5" + srv_errlog="$6" + srv_prestart="$7" + srv_poststart="$8" + srv_prestop="$9" + srv_poststop="$10" + [ -z "$srv_wdir" ] && srv_wdir='-' + [ -z "$srv_outlog" ] && srv_outlog='/dev/null' + [ -z "$srv_errlog" ] && srv_errlog='/dev/null' + [ -z "$srv_prestart" ] && srv_prestart='-' + [ -z "$srv_poststart" ] && srv_poststart='-' + [ -z "$srv_prestop" ] && srv_prestop='-' + [ -z "$srv_poststop" ] && srv_poststop='-' + printf "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n" "$srv_name" "$srv_cmd" \ + "$srv_wdir" "$srv_outlog" "$srv_errlog" "$srv_prestart" "$srv_poststart" \ + "$srv_prestop" "$srv_poststop" >> $CONF + echo "Service $srv_name registered" + ;; +unreg) + srvline="$(find_service "$srv_name")" + TCONF="$(mktemp)" + grep -v "$srv_name" $CONF > $TCONF && mv $TCONF $CONF + echo "Service $srv_name unregistered" + ;; +start) + [ -f "$pidfile" ] && msgexit "Service $srv is already running"; + srvline="$(find_service "$srv_name")" + srv_cmd="$(echo "$srvline" | cut -f2)" + srv_wdir="$(echo "$srvline" | cut -f3)" + srv_outlog="$(echo "$srvline" | cut -f4)" + srv_errlog="$(echo "$srvline" | cut -f5)" + srv_prestart="$(echo "$srvline" | cut -f6)" + srv_poststart="$(echo "$srvline" | cut -f7)" + echo "Starting service $srv_name" + [ "$srv_wdir" != "-" ] && cd "$srv_wdir" + [ "$srv_prestart" != "-" ] && $SHELL -c "$srv_prestart" + nohup $srv_cmd >$srv_outlog 2>$srv_errlog & echo $! > $pidfile + [ "$srv_poststart" != "-" ] && $SHELL -c "$srv_poststart" + echo "Service $srv_name started" + ;; +stop) + [ ! -f "$pidfile" ] && msgexit "Service $srv is not running"; + srvline="$(find_service "$srv_name")" + srv_prestop="$(echo "$srvline" | cut -f8)" + srv_poststop="$(echo "$srvline" | cut -f9)" + echo "Stopping service $srv_name" + [ "$srv_prestop" != "-" ] && $SHELL -c "$srv_prestop" + kill $(cat $pidfile) + rm -f $pidfile + [ "$srv_poststop" != "-" ] && $SHELL -c "$srv_poststop" + echo "Service $srv_name stopped" + ;; +restart) + srvline="$(find_service "$srv_name")" + $SHELL $0 stop $srv_name + $SHELL $0 start $srv_name + ;; +*) + echo 'Invalid operation! Pick start, stop, restart, reg, unreg or help' + ;; +esac + diff --git a/flow.sh b/flow.sh new file mode 100755 index 0000000..548abc9 --- /dev/null +++ b/flow.sh @@ -0,0 +1,60 @@ +#!/bin/sh +# +# ShellFlow: the simplest workflow orchestrator in under +# 20 SLOC of POSIX shell, using Unix pipelines +# +# Usage: echo some_input | /path/to/flow.sh /path/to/workflow +# +# Exposes the following variables to workflows and tasks: +# - SFLIB - resolved path to this file +# - WFDIR - directory of the workflow +# +# Also exposes the debugout function to output to stderr +# +# Typical workflow script structure: ./task1 | ./task2 | ./task3 ... +# (the workflow directory becomes the current working directory) +# +# Data format: workflow name, tab character, all other input +# +# Typical task script structure (if shell-based): +# #!/bin/sh +# . $SFLIB # source the library part of the script +# do_task() { # accepts workflow name into $1 and all input into $2 +# # your actions here +# } +# run-task +# +# You can call different workflows from inside tasks by referring to +# this very script using the $SFLIB environment variable or by calling +# the sfrun function +# +# Created by Luxferre in 2025, released into public domain + +# ShellFlow task function library + +# print to stderr +debugout() { printf '%s\n' "$*" >&2; } + +# format the message and print to stdout +flowout() { printf '%s\t%s' "$1" "$2"; } + +# use this in all task definitions +run-task() { + input="$(cat)" + wf="$(printf '%s' "$input" | cut -f 1)" + body="$(printf '%s' "$input" | cut -f 2-)" + flowout "$wf" "$(do_task "$wf" "$body")" +} + +# ShellFlow entry point + +sfrun() { + SFLIB="$(readlink -f "$0")" + wfbase="$(basename "$1")" + WFDIR="$(readlink -f $(dirname -- "$1"))" + cd "$WFDIR" + debugout "Starting workflow $wfbase..." + flowout $wfbase "$(cat)" | SFLIB="$SFLIB" WFDIR="$WFDIR" $SHELL $wfbase | cut -f 2- +} + +[ "$(basename "$0")" == "flow.sh" ] && sfrun $* diff --git a/ghmd.sh b/ghmd.sh new file mode 100755 index 0000000..d2ef700 --- /dev/null +++ b/ghmd.sh @@ -0,0 +1,157 @@ +#!/bin/sh +# GHMD is a shell script for setting up any of the following things: +# 1) a Git user on a remote instance (initial setup) +# 2) a private Git repo on the remote instance +# 3) a public Git repo using git:// protocol on the remote instance (if it uses systemd) +# 4) Git daemon (serving git:// protocol) on the remote instance +# 5) access via the Git daemon to individual repositories +# 6) repository description and clone URL for some services like stagit +# Usage: ghmd.sh [newsrv | newrepo | gitd-sd | gitd-publish | gitd-unpublish | set-repo-desc | set-repo-url | set-repo-owner] user@host [name] ("[desc|url]") +# regardless of the actions, the user must have root permissions +# prerequisite: git package must be already installed on the server +# Created by Luxferre in 2024, released into public domain + +GITUSR="git" +PARAM="$1" +USERHOST="$2" +THOST="${USERHOST##*@}" +SSHCMD="ssh $USERHOST" # instance ssh cmd + +help() { +cat < /home/${GITUSR}/.ssh/authorized_keys; chown -R ${GITUSR} /home/${GITUSR}/.ssh" + echo "Now, you'll need to enter the newly created ${GITUSR} user password:" + ssh-copy-id ${GITUSR}@${THOST} + $SSHCMD "chsh $GITUSR -s $(which git-shell)" + echo "Git user set up on ${THOST}. Now use $0 newrepo ${GITUSR}@${THOST} [name] to set up new Git repos" + ;; + "newrepo") # set up a new Git repo + [[ -z "$USERHOST" ]] && echo "Error: no user and hostname!" && exit 1 + REPONAME="$3" + [[ -z "$REPONAME" ]] && echo "Error: no repository name!" && exit 1 + REPODIR="${REPONAME}.git" + FULLREPODIR="/home/${GITUSR}/$REPODIR" + $SSHCMD "mkdir -p $FULLREPODIR;cd $FULLREPODIR;git init --bare --shared;chown -R ${GITUSR}:${GITUSR} ." + echo "Empty repository created at ${USERHOST}:${REPODIR}" + ;; + "set-repo-desc") # set the repo description for some services + [[ -z "$USERHOST" ]] && echo "Error: no user and hostname!" && exit 1 + REPONAME="$3" + [[ -z "$REPONAME" ]] && echo "Error: no repository name!" && exit 1 + VALUE="$4" # it may be empty + REPODIR="${REPONAME}.git" + FULLREPODIR="/home/${GITUSR}/$REPODIR" + $SSHCMD "cd $FULLREPODIR;echo \"$VALUE\" > description;chown -R ${GITUSR}:${GITUSR} description" + echo "Description set for ${REPODIR}" + ;; + "set-repo-url") # set the repo clone URL for some services + [[ -z "$USERHOST" ]] && echo "Error: no user and hostname!" && exit 1 + REPONAME="$3" + [[ -z "$REPONAME" ]] && echo "Error: no repository name!" && exit 1 + VALUE="$4" # it may be empty + REPODIR="${REPONAME}.git" + FULLREPODIR="/home/${GITUSR}/$REPODIR" + $SSHCMD "cd $FULLREPODIR;echo \"$VALUE\" > url;chown -R ${GITUSR}:${GITUSR} url" + echo "Clone URL set for ${REPODIR}" + ;; + "set-repo-owner") # set the repo owner name for some services + [[ -z "$USERHOST" ]] && echo "Error: no user and hostname!" && exit 1 + REPONAME="$3" + [[ -z "$REPONAME" ]] && echo "Error: no repository name!" && exit 1 + VALUE="$4" # it may be empty + REPODIR="${REPONAME}.git" + FULLREPODIR="/home/${GITUSR}/$REPODIR" + $SSHCMD "cd $FULLREPODIR;echo \"$VALUE\" > owner;chown -R ${GITUSR}:${GITUSR} owner" + echo "Owner name set for ${REPODIR}" + ;; + "gitd-sd") # set up the Git daemon using a systemd unit + [[ -z "$USERHOST" ]] && echo "Error: no user and hostname!" && exit 1 + TMPUNIT="/tmp/git-daemon.service" +cat << EOF > $TMPUNIT +[Unit] +Description=Start Git Daemon + +[Service] +ExecStart=/usr/bin/git daemon --reuseaddr --base-path=/home/${GITUSR} /home/${GITUSR} +Restart=always +RestartSec=500ms +StandardOutput=syslog +StandardError=syslog +SyslogIdentifier=git-daemon +User=$GITUSR +Group=$GITUSR + +[Install] +WantedBy=multi-user.target +EOF + scp $TMPUNIT ${USERHOST}:/etc/systemd/system/git-daemon.service + $SSHCMD "systemctl enable git-daemon; systemctl start git-daemon" + echo "Git daemon installed on ${THOST} via systemd" + rm -f $TMPUNIT + ;; + "gitd-publish") # make a repository public via the Git daemon + [[ -z "$USERHOST" ]] && echo "Error: no user and hostname!" && exit 1 + REPONAME="$3" + [[ -z "$REPONAME" ]] && echo "Error: no repository name!" && exit 1 + REPODIR="${REPONAME}.git" + ACCFILE="/home/${GITUSR}/${REPODIR}/git-daemon-export-ok" + $SSHCMD "touch $ACCFILE; chown $GITUSR $ACCFILE" + echo "Repository git://${THOST}/$REPODIR made public" + ;; + "gitd-unpublish") # revoke repository publishing via the Git daemon + [[ -z "$USERHOST" ]] && echo "Error: no user and hostname!" && exit 1 + REPONAME="$3" + [[ -z "$REPONAME" ]] && echo "Error: no repository name!" && exit 1 + REPODIR="${REPONAME}.git" + ACCFILE="/home/${GITUSR}/${REPODIR}/git-daemon-export-ok" + $SSHCMD "rm -f $ACCFILE" + echo "Repository git://${THOST}/$REPODIR made private" + ;; + *) + help +esac + diff --git a/lshar.sh b/lshar.sh new file mode 100644 index 0000000..2b178aa --- /dev/null +++ b/lshar.sh @@ -0,0 +1,70 @@ +#!/bin/sh +# lshar.sh - Luxferre's shell archiver +# Outputs to stdout only (for GNU and BSD shar compatibility) +# No external deps for .shars (sh commands required: mkdir, printf) +# Archive creation also depends on sort (Busybox, Toybox or GNU Coreutils) +# Works with any text or binary files and directory structures +# Created by Luxferre in 2023, released into public domain + +# a portable, compact, no-deps serializer of any file into printf sequence +f2p() { + c=0 # init counter + l="$2" # get chunk length + [ -z "$l" ] && l=4096 # 4096-byte chunks by default (shouldn't be a problem) + cprefix="printf '%b' '" # pattern to prepend to every chunk + csuffix="' >> '$1'\n" # pattern to append to every chunk + printf '%b' "$cprefix" # always start with the prefix + IFS='' # separator + while read -r -s -d '' -n1 b; do # convert every byte + if [ "$c" == "$l" ]; then # roll over and output suffix+prefix + c=0 # reinit counter + printf '%b' "$csuffix" # end the chunk + printf '%b' "$cprefix" # begin a new chunk + else + c="$(( c + 1 ))" # increment c + fi + bcode="$(printf '%d' "'$b")" # get the ASCII code of the byte + if [ "$bcode" -gt 32 ] && [ "$bcode" -lt 127 ] && + [ "$bcode" -ne 39 ] && [ "$bcode" -ne 92 ] + then # printable and not a single quote or a backslash + printf '%b' "$b" # codes from 34 to 126 are safe to print + else # subject to escaping, print the hex representation + printf '\\x%02x' "$bcode" + fi + done <"$1" + printf '%b' "$csuffix" # always end with the suffix +} + +# main code +LF=$'\n' # cache the linefeed character +flist='' # pre-build the file list from args +dlist='' # pre-build the directory list from args +for i; do # iterate over args + bname="${i##*/}" # get the basename + if [ -d "$i" ]; then # it's a directory, we skip . and .. + [ "$bname" == '.' ] || [ "$bname" == '..' ] && continue + dlist="${dlist}${i%/}${LF}" # append the directory + else # it's a file + dname="${i%%$bname}" # get the basedir + dlist="${dlist}${dname%/}${LF}" # append the basedir + flist="${flist}${i}${LF}" # append the file + fi +done +flist="$(printf '%s' "$flist" | sort -u)" # remove file duplicates +dlist="$(printf '%s' "$dlist" | sort -u)" # remove directory duplicates +printf '%s\n' '#!/bin/sh' # output the shebang, just in case +IFS="$LF" # update the separator +for indir in $dlist; do # create the directory structure first + [ -z "$indir" ] && continue # skip empty names + printf '# dir: %s\n' "$indir" + printf "printf 'c - %%s\\\n' '%s'\n" "$indir" # announce creation + printf "mkdir -p '%s' > /dev/null 2>&1\n" "$indir" # create the dir +done +for infile in $flist; do # iterate over the file list afterwards + [ -z "$infile" ] && continue # skip empty names + printf '# file: %s\n' "$infile" + printf "printf 'x - %%s\\\n' '%s'\n" "$infile" # announce unrolling + printf "printf '' > '%s'\n" "$infile" # clear the file + f2p "$infile" # run the serializer + IFS="$LF" # update the separator +done diff --git a/outport b/outport new file mode 100755 index 0000000..c94c626 --- /dev/null +++ b/outport @@ -0,0 +1,31 @@ +#!/bin/sh +# Outport: a simple script to expose reverse tunnels via SSH +# Usage: outport.sh {start|stop} user@host [port1] [port2] ... +# Created by Luxferre in 2025, released into public domain +SOCKET_PATH=/var/run/sshcontrol.sock +ACTION="$1" +TARGET="$2" +shift +shift +PORT_OPTS="" +case "$ACTION" in + start) + while :; do + [ -z "$1" ] && break + PORT_OPTS="$PORT_OPTS -R :$1:localhost:$1" + shift + done + MISC_OPTS="-o ServerAliveInterval=60 -o ServerAliveCountMax=15" + ssh -f -N -M -S $SOCKET_PATH $PORT_OPTS $MISC_OPTS $TARGET + echo "Tunnel started" + ;; + stop) + ssh -S $SOCKET_PATH -O cancel $TARGET + ssh -S $SOCKET_PATH -O exit $TARGET + echo "Tunnel stopped" + ;; + *) + echo "Invalid action (specify start or stop)" + ;; +esac + diff --git a/shee.sh b/shee.sh new file mode 100755 index 0000000..665cc8d --- /dev/null +++ b/shee.sh @@ -0,0 +1,162 @@ +#!/bin/sh +# shee.sh: a shell-based OpenAI-compatible local chat +# Depends on curl, jq, file and mktemp, otherwise fully POSIX-compatible +# (for history/line editing, run it via rlwrap) +# Configurable in-chat and with environment variables +# Created by Luxferre in 2025, released into public domain + +# all configuration variables are here + +[ -z "$SHEESH_API_ROOT" ] && SHEESH_API_ROOT='https://text.pollinations.ai/openai' +[ -z "$SHEESH_MODEL" ] && SHEESH_MODEL='openai-fast' +[ -z "$SHEESH_TEMP" ] && SHEESH_TEMP='0.6' +[ -z "$SHEESH_SYSPROMPT" ] && SHEESH_SYSPROMPT='You are a helpful assistant.' + +histfile="$(mktemp -t furrfu.XXXXXX)" +datafile="$(mktemp -t furrfu.XXXXXX)" +addhistory() { + role="$1"; shift + jq -nc --slurpfile h $histfile --arg r "$role" --arg a "$*" '$h[0]+[{"role":$r,"content":$a}]' > ${histfile}.new + mv ${histfile}.new $histfile +} + +prompt() { printf "\e[1;32m>> \e[0m"; } +clearhist() { + printf '[]' > $histfile + addhistory system "$SHEESH_SYSPROMPT" +} +startswith() { case $1 in "$2"*) true;; *) false;; esac; } +setsys() { + SHEESH_SYSPROMPT="$*" + clearhist +} +setsysfile() { setsys "$(<"$1")"; } +addfile() { + tmp="$(mktemp -t furrfufile.XXXXXXX)" + fname="$(basename $1)" + if startswith "$1" "http*://"; then + [ -z "$fname" || "$fname" == "/" ] && $fname="${1//\//_}" + curl -sSLN $1 > $tmp + else + cp "$1" $tmp + fi + if file -b --mime-encoding $tmp | grep binary; then + uuencode -m $tmp $fname > ${tmp}.new + mv ${tmp}.new $tmp + else + echo "Contents of ${fname}:" >> ${tmp}.new + cat $tmp >> ${tmp}.new + mv ${tmp}.new $tmp + fi + jq -nc --slurpfile h $histfile --rawfile a "$tmp" '$h[0]+[{"role":"user","content":$a}]' > ${histfile}.new + mv ${histfile}.new $histfile + rm $tmp +} + +listmodels() { + echo 'Available models:' + curl -sSL ${SHEESH_API_ROOT}/models \ + ${SHEESH_UA:+-H "User-Agent: $SHEESH_UA"} \ + ${SHEESH_REFERER:+-H "Referer: $SHEESH_REFERER"} \ + ${SHEESH_ORIGIN:+-H "Origin: $SHEESH_ORIGIN"} \ + ${OPENAI_API_KEY:+-H "Authorization: Bearer $OPENAI_API_KEY"} \ + ${SHEESH_COOKIES:+-H "Cookie: $SHEESH_COOKIES"} \ + | jq -r '.data[].id' | while read -r modelname; do + printf '\e[0;36m* %s\e[0m\n' "$modelname" + done +} + +help() { + printf 'Current settings:\n\n\e[2;37mEndpoint: %s\n' "$SHEESH_API_ROOT" + printf 'Model: %s\n' "$SHEESH_MODEL" + printf 'Temperature: %.2f\n' "$SHEESH_TEMP" + printf 'System prompt: "%s"\e[0m\n\n' "$SHEESH_SYSPROMPT" + echo 'Available commands:' + echo + echo '* /models: list available models' + echo '* /model: change the current model' + echo '* /add: add a file to the context' + echo '* /temp: change the current temperature' + echo '* /endpoint: change the current API endpoint' + echo '* /clear: clear the session context' + echo '* /sys [prompt]: set a system prompt and clear the context' + echo '* /sysload [file]: load the system prompt from a file and clear the context' + echo '* /bye: exit the chat immediately' + echo + echo 'Available environment variables for configuration:' + echo + echo '* OPENAI_API_KEY: API key string (default empty)' + echo '* SHEESH_API_ROOT: API endpoint (default https://text.pollinations.ai/openai)' + echo '* SHEESH_MODEL: model ID (default openai-fast)' + echo '* SHEESH_TEMP: model temperature (default 0.6)' + echo '* SHEESH_SYSPROMPT: system prompt (default "You are a helpful assistant.") ' + echo '* SHEESH_UA: user agent override (default empty)' + echo '* SHEESH_REFERER: Referer header value (default empty)' + echo '* SHEESH_ORIGIN: Origin header value (default empty)' + echo '* SHEESH_COOKIES: Cookie header value (default empty)' + echo +} + +trap cleanup SIGINT SIGABRT SIGHUP SIGQUIT +cleanup() { + rm $histfile $datafile + printf '\nBye!\n' + exit 0 +} + +hdrline='----------------------' +title='SHEE.SH v2 by Luxferre' +printf '\e[1;36m%s\n%s\n%s\e[0m\n' "$hdrline" "$title" "$hdrline" +printf '\e[2;37mEndpoint: %s\n' "$SHEESH_API_ROOT" +printf 'Model: %s\n' "$SHEESH_MODEL" +printf 'Temperature: %.2f\n' "$SHEESH_TEMP" +printf '\e[1;36m%s\e[0m\n' "$hdrline" + +setsys "$SHEESH_SYSPROMPT" +while prompt && read -r line; do + [ -z "$line" ] && continue + [ "$line" == "/bye" ] && break + [ "$line" == "/clear" ] && clearhist && echo "Session cleared" && continue + [ "$line" == "/help" ] && help && continue + [ "$line" == "/models" ] && listmodels && continue + startswith "$line" '/sys ' && setsys "${line#\/sys }" && echo "System prompt set and session cleared" && continue + startswith "$line" '/sysload ' && setsysfile "${line#\/sysload }" && echo "System prompt loaded and session cleared" && continue + startswith "$line" '/temp ' && SHEESH_TEMP="${line#\/temp }" && echo "Temperature set to $SHEESH_TEMP" && continue + startswith "$line" '/model ' && SHEESH_MODEL="${line#\/model }" && echo "Model set to $SHEESH_MODEL" && continue + startswith "$line" '/endpoint ' && SHEESH_API_ROOT="${line#\/endpoint }" && echo "API endpoint set to $SHEESH_API_ROOT" && continue + if startswith "$line" '/add '; then + filelist="${line#\/add }" + for f in $filelist; do + addfile "$f" + printf "File %s added to the context\n" "$f" + done + continue + fi + # commands end, normal message processing start + addhistory user "$line" + jq -nc --arg m "$SHEESH_MODEL" --arg t "$SHEESH_TEMP" --slurpfile msgs $histfile \ + '{model:$m,stream:true,messages:$msgs[0],temperature:($t|tonumber)}' > $datafile + curl -sSLN "${SHEESH_API_ROOT}/chat/completions" \ + -H 'Content-Type: application/json' \ + ${SHEESH_UA:+-H "User-Agent: $SHEESH_UA"} \ + ${SHEESH_REFERER:+-H "Referer: $SHEESH_REFERER"} \ + ${SHEESH_ORIGIN:+-H "Origin: $SHEESH_ORIGIN"} \ + ${OPENAI_API_KEY:+-H "Authorization: Bearer $OPENAI_API_KEY"} \ + ${SHEESH_COOKIES:+-H "Cookie: $SHEESH_COOKIES"} \ + -H 'Accept: text/event-stream' --data-binary "@$datafile" 2>/dev/null | \ + while IFS= read -r chunk; do + [ -z "$chunk" ] && continue + data="${chunk#data: }" + [ "$data" == "[DONE]" ] && break + delta='' + startswith "$data" '{' && delta="$(printf '%s' "$data" | jq -rj '.choices[0].delta.content // empty' 2>/dev/null;printf x)" + delta="${delta%?}" + [ -n "$delta" ] && printf "\e[0;33m%s\e[0m" "$delta" + tailbuf="$tailbuf$delta" + done + echo + addhistory assistant "$tailbuf" + tailbuf="" + line='' +done +cleanup diff --git a/shellbeat.sh b/shellbeat.sh new file mode 100755 index 0000000..e893d04 --- /dev/null +++ b/shellbeat.sh @@ -0,0 +1,23 @@ +#!/bin/sh +# ShellBeat: a Busybox-compatible Bytebeat player with purely shell arithmetic syntax +# depends on xxd only +# By default, SoX is also required, or change the PLAYCMD variable to your engine +# (see some commented examples, some of them only accept 8Khz sample rate) +# Usage: shellbeat.sh 'formula'[ samplerate=8000] +# Example test on "42 melody": sh shellbeat.sh 't * (42 & (t >> 10))' +# Created in 2023 by Luxferre, released into public domain +FORMULA="$1" +SAMPLERATE="$2" +[ -z "$FORMULA" ] && printf 'No formula!\n' && exit +[ -z "$SAMPLERATE" ] && SAMPLERATE=8000 + +PLAYCMD="play -q -tu8 -r${SAMPLERATE} -c1 -" # most universal option with SoX play command +# or else uncomment one of these if you don't have SoX: +#PLAYCMD="tee /dev/dsp" # if you have bare OSS (only 8KHz input) +#PLAYCMD="aplay -q -f U8 -r ${SAMPLERATE}" # if you have ALSA +#PLAYCMD="pacat --raw --format=u8 --rate=${SAMPLERATE} --channels=1" # if you have PulseAudio +#PLAYCMD="pw-play --format=u8 --rate=${SAMPLERATE} --channels=1 -" # if you have PipeWire + +printf 'ShellBeat by Luxferre\nPlaying formula: %s\nSample rate: %d Hz\n' "$FORMULA" "$SAMPLERATE" +(t=0; while true; do printf '%02X\n' "$((255&(${FORMULA})))"; t=$((t+1)); done) | xxd -r -p | $PLAYCMD > /dev/null +