├── .gitignore ├── LICENSE.MIT ├── README.md ├── Snoopers ├── install.sh ├── shell-hook.zsh ├── shellwrap.py └── wtf ├── Talkers ├── installer.sh ├── sc-add ├── sc-picker ├── sidechat └── simple-parse.py ├── Xtractors ├── kb-capture.py └── llm-magic ├── assets ├── llmehelp.jpg ├── llmehelp.png ├── sidechat-logo.svg ├── sidechat_100.png └── sidechat_200.png └── one-shot └── talker /.gitignore: -------------------------------------------------------------------------------- 1 | .aider* 2 | -------------------------------------------------------------------------------- /LICENSE.MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Chris McKenzie 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |
4 | A tmux-based ai assistant 5 |

6 | 7 | Simple installer: 8 | 9 | curl day50.dev/sidechat | sh 10 | 11 | An llm intervention in your little terminal with suppport for adding screenshots, command output, cycling pane focus, turning off and on pane capturing, adding external context and more, all sitting agnostically on top of tmux so there's no substantive workflow change needed. You can just beckon your trusty friend at your leisure. 12 | 13 | 14 | [demo.webm](https://github.com/user-attachments/assets/9e8dd99a-510b-4708-9ab5-58b75edf5945) 15 | 16 | ## There's an agentic mode. We call it DUI. Enable it with `/dui`. 17 | ![2025-05-15_18-50](https://github.com/user-attachments/assets/d1da6063-b450-49f8-863d-fcf0c32647fc) 18 | 19 | 20 | 21 | You should also use `sc-add` which can pipe anything into the context. Here's an example: 22 | ![out](https://github.com/user-attachments/assets/62318080-9d67-41de-921b-976ad61e1122) 23 | 24 | 25 | Once you're in there's a few slash commands. Use `/help` to get the current list. 26 | 27 | Multiline is available with backslash `\`, just like it is at the shell 28 | 29 | ![2025-05-01_13-38](https://github.com/user-attachments/assets/e57ea643-cb63-4727-9901-e15109b81adb) 30 | 31 | 32 | Here's some screenshots of how it seamlessly works with [Streamdown's](https://github.com/day50-dev/Streamdown) built in `Savebrace` feature and how it helps workflow. 33 | ![2025-04-26_18-45](https://github.com/user-attachments/assets/a81cbcea-cb15-46d9-92ac-5430238b2b85) 34 | 35 | ![2025-04-26_18-49](https://github.com/user-attachments/assets/c8b98e30-cd09-47bc-b751-02a929a82703) 36 | 37 | ![2025-04-26_18-49_1](https://github.com/user-attachments/assets/c752f94f-b780-4a8b-b597-1ce62b2bdb78) 38 | 39 | Also you don't need `tmux`! Often you'll be doing things and then realize you want the talk party and you're not in tmux. 40 | 41 | That's fine! If you use DAY50's [streamdown](https://github.com/day50-dev/Streamdown), `sc-picker` works like it does inside tmux. You can also `sc-add` by id. It's not great but you're not locked in. That's the point! 42 | 43 | **Note**: This used to be the home of many tools. They've been moved to [llm-incubator](https://github.com/day50-dev/llm-incubator) 44 | -------------------------------------------------------------------------------- /Snoopers/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [[ ! "$(getent passwd $(whoami) | cut -d: -f7)" =~ zsh ]]; then 3 | echo "oh brother, we are kind of a zsh house here ... let's see what we can do." 4 | fi 5 | cat << END 6 | You can do this. Just do: 7 | 8 | cat shell-hook.zsh wtf >> .zshrc 9 | 10 | The zsh part of shell-hook is how it binds. Totally fixable for all the others! 11 | END 12 | -------------------------------------------------------------------------------- /Snoopers/shell-hook.zsh: -------------------------------------------------------------------------------- 1 | shell_hook() { 2 | local QUESTION="$BUFFER" 3 | local SHELL=$(ps -p $$ -o command= | awk '{print $1}') 4 | local PROMPT=" 5 | You are an experienced Linux engineer with expertise in all Linux 6 | commands and their 7 | functionality across different Linux systems. 8 | 9 | Given a task, generate a single command or a pipeline 10 | of commands that accomplish the task efficiently. 11 | This command is to be executed in the current shell, $SHELL. 12 | For complex tasks or those requiring multiple 13 | steps, provide a pipeline of commands. 14 | Ensure all commands are safe and prefer modern ways. For instance, 15 | homectl instead of adduser, ip instead of ifconfig, systemctl, journalctl, etc. 16 | Make sure that the command flags used are supported by the binaries 17 | usually available in the current system or shell. 18 | If a command is not compatible with the 19 | system or shell, provide a suitable alternative. 20 | 21 | The system information is: $(uname -a) (generated using: uname -a). 22 | The user is $USER. Use sudo when necessary 23 | 24 | Create a command to accomplish the following task: $QUESTION 25 | 26 | If there is text enclosed in paranthesis, that's what ought to be changed 27 | 28 | Output only the command as a single line of plain text, with no 29 | quotes, formatting, or additional commentary. Do not use markdown or any 30 | other formatting. Do not include the command into a code block. 31 | Don't include the shell itself (bash, zsh, etc.) in the command. 32 | " 33 | local model='' 34 | 35 | if [[ -r "$HOME/.config/io.datasette.llm/default_model.txt" ]]; then 36 | model=$(< $HOME/.config/io.datasette.llm/default_model.txt) 37 | fi 38 | 39 | BUFFER="$QUESTION ... $model" 40 | zle -R 41 | local response=$(llm "$PROMPT") 42 | local COMMAND=$(echo "$response" | sed 's/```//g' | tr -d '\n') 43 | echo "$(date %s) {$QUESTION | $response}" >> /tmp/shell-hook 44 | if [[ -n "$COMMAND" ]] ; then 45 | BUFFER="$COMMAND" 46 | CURSOR=${#BUFFER} 47 | else 48 | BUFFER="$QUESTION ... no results" 49 | fi 50 | } 51 | 52 | zle -N shell_hook 53 | 54 | bindkey '^Xx' shell_hook 55 | 56 | -------------------------------------------------------------------------------- /Snoopers/shellwrap.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | import pty 4 | import select 5 | import sys 6 | import argparse 7 | import tty 8 | import termios 9 | import fcntl 10 | import re 11 | import subprocess 12 | from io import StringIO 13 | 14 | CLIENT = None 15 | ESCAPE = b'\x18' # ctrl+x 16 | PATH_INPUT = None 17 | PATH_OUTPUT = None 18 | ANSIESCAPE = r'\033(?:\[[0-9;?]*[a-zA-Z]|][0-9]*;;.*?\\|\\)' 19 | strip_ansi = lambda x: re.sub(ANSIESCAPE, "", x) 20 | 21 | parser = argparse.ArgumentParser(description='shell wrap, a transparent shell wrapper') 22 | parser.add_argument('--method', choices=['litellm', 'simonw', 'vllm'], default='litellm', help='Method to use for LLM interaction') 23 | parser.add_argument('--exec', '-e', dest='exec_command', help='Command to execute') 24 | args = parser.parse_args() 25 | 26 | if args.method == 'litellm': 27 | import openai # openai v1.0.0+ 28 | CLIENT = openai.OpenAI(api_key="anything",base_url="http://0.0.0.0:4000") # litellm 29 | 30 | def clean_input(raw_input): 31 | fake_stdin = StringIO(raw_input.decode('utf-8')) 32 | sys.stdin = fake_stdin 33 | processed_input = input() 34 | sys.stdin = sys.__stdin__ 35 | return processed_input 36 | 37 | def activate(qstr): 38 | with open(PATH_INPUT, "rb") as f: 39 | f.seek(0, os.SEEK_END) 40 | size = f.tell() 41 | f.seek(max(0, size - 500), os.SEEK_SET) 42 | recent_input = f.read() 43 | 44 | processed_input = clean_input(recent_input) 45 | 46 | with open(PATH_OUTPUT, "rb") as f: 47 | f.seek(0, os.SEEK_END) 48 | size = f.tell() 49 | f.seek(max(0, size - 500), os.SEEK_SET) 50 | recent_output = strip_ansi(f.read().decode('utf-8')) 51 | 52 | request = f"""You are an experienced fullstack software engineer with expertise in all Linux commands and their functionality 53 | Given a task, along with a sequence of previous inputs and screen scrape, generate a single line of commands that accomplish the task efficiently. 54 | This command is to be executed in the current program which can be determined by the screen scrape 55 | The screen scrape is 56 | ---- 57 | {recent_output} 58 | ---- 59 | The recent input is {processed_input} 60 | ---- 61 | Take special care and look at the most recent part of the screen scrape. Pay attention to 62 | things like the prompt style, welcome banners, and be sensitive if the person is say at 63 | a python prompt, ruby prompt, gdb, or perhaps inside a program such as vim. 64 | 65 | Create a command to accomplish the following task: {qstr.decode('utf-8')} 66 | 67 | If there is text enclosed in paranthesis, that's what ought to be changed 68 | 69 | Output only the command as a single line of plain text, with no 70 | quotes, formatting, or additional commentary. Do not use markdown or any 71 | other formatting. Do not include the command into a code block. 72 | Don't include the program itself (bash, zsh, etc.) in the command. 73 | """ 74 | 75 | if args.method == 'litellm': 76 | response = CLIENT.chat.completions.create(model="gpt-3.5-turbo", messages = [ 77 | { 78 | "role": "system", 79 | "content": SYSTEMPROMPT 80 | }, 81 | { 82 | "role": "user", 83 | "content": request 84 | } 85 | ]) 86 | print(response.output_text) 87 | 88 | elif args.method == 'simonw': 89 | command = f'llm -x "{request}"' 90 | process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 91 | output, error = process.communicate() 92 | with open("/tmp/screen-query/log", "ab") as f: 93 | f.write(request.encode('utf-8') + b'\n(' + output + b')\n') 94 | f.flush() 95 | 96 | return output 97 | 98 | elif args.method == 'vllm': 99 | print("vllm method selected") 100 | 101 | 102 | def set_pty_size(fd, target_fd): 103 | s = fcntl.ioctl(target_fd, termios.TIOCGWINSZ, b"\x00" * 8) 104 | fcntl.ioctl(fd, termios.TIOCSWINSZ, s) 105 | 106 | def main(): 107 | global PATH_INPUT, PATH_OUTPUT 108 | temp_dir = "/tmp/screen-query" 109 | os.makedirs(temp_dir, exist_ok=True) 110 | PATH_INPUT = os.path.join(temp_dir, "input") 111 | PATH_OUTPUT = os.path.join(temp_dir, "output") 112 | print(f"Input path: {PATH_INPUT}") 113 | print(f"Output path: {PATH_OUTPUT}") 114 | is_escaped = False 115 | ansi_pos = None 116 | qstr = b'' 117 | 118 | orig_attrs = termios.tcgetattr(sys.stdin.fileno()) 119 | try: 120 | tty.setraw(sys.stdin.fileno()) # raw mode to send Ctrl-C, etc. 121 | pid, fd = pty.fork() 122 | 123 | if pid == 0: 124 | print(args.exec_command) 125 | exec_command_list = args.exec_command.split() 126 | os.execvp(exec_command_list[0], exec_command_list) 127 | else: 128 | set_pty_size(fd, sys.stdin.fileno()) 129 | 130 | while True: 131 | r, _, _ = select.select([fd, sys.stdin], [], []) 132 | 133 | if sys.stdin in r: 134 | user_input = os.read(sys.stdin.fileno(), 1024) 135 | if not user_input: 136 | break 137 | 138 | if is_escaped: 139 | qstr += user_input 140 | 141 | if re.search(r'[\r\n]', user_input.decode('utf-8')): 142 | is_escaped = False 143 | sys.stdout.write('◀\x1b[8m') 144 | sys.stdout.flush() 145 | command = activate(qstr) 146 | os.write(sys.stdout.fileno(), '\x1b[u'.encode()) 147 | os.write(fd, command) 148 | sys.stdout.flush() 149 | qstr = b'' 150 | continue 151 | 152 | if ESCAPE in user_input: 153 | is_escaped = True 154 | #AI: save the ansi position here 155 | ansi_pos = '\x1b[s' 156 | os.write(sys.stdout.fileno(), ansi_pos.encode()) 157 | 158 | sys.stdout.write('\x1b[7m▶') 159 | sys.stdout.flush() 160 | continue 161 | 162 | with open(PATH_INPUT, "ab") as f: 163 | f.write(user_input) 164 | f.flush() 165 | 166 | if is_escaped: 167 | os.write(sys.stdout.fileno(), user_input) 168 | else: 169 | os.write(fd, user_input) 170 | 171 | if fd in r: 172 | output = os.read(fd, 1024) 173 | if not output: 174 | break 175 | 176 | with open(PATH_OUTPUT, "ab") as f: 177 | f.write(output) 178 | f.flush() 179 | 180 | os.write(sys.stdout.fileno(), output) 181 | #os.write(sys.stdout.fileno(), b''.join(chunks)) 182 | 183 | except (OSError, KeyboardInterrupt): 184 | termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, orig_attrs) 185 | print("\n\rExiting...") 186 | sys.exit(130) 187 | 188 | finally: 189 | termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, orig_attrs) 190 | 191 | if __name__ == "__main__": 192 | main() 193 | -------------------------------------------------------------------------------- /Snoopers/wtf: -------------------------------------------------------------------------------- 1 | function wtf { 2 | local -a files 3 | while IFS= read -r i; do 4 | files+=("$i") 5 | done 6 | 7 | local width=$(( ( $(tput cols) * 4 ) / 5)) 8 | local head=$(( width / 2 )) 9 | local question="$1" 10 | local each_question 11 | local first= 12 | local lines=50 13 | 14 | [[ -n "$question" ]] && each_question="Lastly, $question ONLY If this file is relevant to the question, tell me in bold after the summary. If not relevant, do not refer to this question and do not answer this question." 15 | 16 | for i in "${files[@]}"; do 17 | if [[ -f "$i" && "$(mimetype "$i" | grep text | wc -l)" -gt 0 ]]; then 18 | date=$(git log -1 --format="%ad" --date=short -- "$i" 2> /dev/null ) 19 | desc=$( head -$lines $i | iconv -f UTF-8 -t ASCII//TRANSLIT | llm $first "I've included the first $lines lines of '$i'. Briefly summarize it. Do not be conversational. Do not include code. Your response will be a command outpuat. DO NOT include the file name. Again, DO NOT INCLUDE THE FILE NAME. Make sure your output does not have code. $each_question") 20 | printf "%-${head}s $date\n" $i 21 | echo "$desc" | sd 22 | echo 23 | [[ -z "$first" ]] && first="-c" 24 | fi 25 | done 26 | 27 | printf '\xe2\x80\x95%.0s' $(seq 1 $(tput cols)) 28 | [[ -n "$question" ]] && last_question="Finally, list the top 4 scripts or code files that are relevant to the question '$question'." 29 | llm -c "Finally, summarize and cluster files based on their descriptions into an outline form (${files[*]}). Cluster them together and break it into categories. Put the most important things first. Don't be conversational. $last_question" | sd 30 | } 31 | 32 | -------------------------------------------------------------------------------- /Talkers/installer.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 3 | PIP=$(command -v pipx || command -v pip || command -v pip3 ) 4 | pybin= 5 | if [[ -z "$PIP" ]]; then 6 | echo "Woops, we need python and either pip or pipx." 7 | exit 1 8 | fi 9 | if ! command -v unzip > /dev/null; then 10 | echo "Woops, unzip needs to be installed." 11 | fi 12 | 13 | if [[ $PIP =~ /pipx$ ]]; then 14 | PIP="$PIP install" 15 | pybin=$(pipx environment | grep PIPX_BIN_DIR=/ | cut -d = -f 2) 16 | else 17 | PIP="$PIP install --user" 18 | if [[ $(uname) == "Linux" || "$(pip3 --version | grep homebrew | wc -l)" != 0 ]]; then 19 | PIP="$PIP --break-system-packages " 20 | fi 21 | fi 22 | 23 | if [[ $(uname) == "Linux" ]]; then 24 | insdir="$HOME/.local/bin" 25 | sd="$insdir/sd" 26 | else 27 | insdir="$HOME/Library/bin" 28 | if [[ -z "$pybin" ]]; then 29 | pybin=$(python3 -msite --user-base)"/bin" 30 | fi 31 | sd="$pybin/sd" 32 | fi 33 | 34 | set -eEuo pipefail 35 | trap 'echo "Error on line $LINENO"; read -rp "Press enter to exit..."; exit 1' ERR 36 | echo -e "\n INSTALLING\n" 37 | 38 | [[ -d "$insdir" ]] || mkdir -p "$insdir" 39 | 40 | touch ~/.tmux.conf 41 | if ! grep -q "bind h run-shell" ~/.tmux.conf; then 42 | cat << ENDL >> ~/.tmux.conf 43 | bind h run-shell "tmux split-window -h '$insdir/sidechat #{pane_id}'" 44 | bind j display-popup -E "$insdir/sc-picker" 45 | ENDL 46 | if pgrep -u $UID tmux > /dev/null; then 47 | tmux source-file "$HOME"/.tmux.conf 48 | fi 49 | fi 50 | 51 | for cmd in simple-parse.py sc-add sc-picker sidechat; do 52 | rm -f "$insdir"/$cmd 53 | echo " ✅ $cmd" 54 | cp -p "$DIR"/$cmd "$insdir" 55 | done 56 | 57 | for pkg in llm streamdown; do 58 | echo " ✅ $pkg" 59 | $PIP $pkg &> /dev/null 60 | done 61 | 62 | if [[ ! -d ~/.fzf ]]; then 63 | git clone --quiet --depth 1 https://github.com/junegunn/fzf.git ~/.fzf 64 | ~/.fzf/install --no-update-rc --no-completion --no-key-bindings >& /dev/null 65 | for i in ~/.fzf/bin/*; do 66 | cmd=$(basename $i) 67 | rm -f "$insdir"/$cmd 68 | ln -s "$i" "$insdir"/$cmd 69 | done 70 | echo " ✅ fzf" 71 | fi 72 | 73 | if ! command -v sqlite3 &> /dev/null; then 74 | command -v wget > /dev/null && wget -q https://www.sqlite.org/2025/sqlite-tools-linux-x64-3490200.zip -O /tmp/sqlite-tools-linux-x64-3490200.zip || curl -s https://www.sqlite.org/2025/sqlite-tools-linux-x64-3490200.zip > /tmp/sqlite-tools-linux-x64-3490200.zip 75 | cd $insdir && unzip -o -q /tmp/sqlite-tools-linux-x64-3490200.zip 76 | fi 77 | echo " ✅ sqlite3" 78 | 79 | msg="You're ready to go!" 80 | 81 | if [[ $(uname) == "Darwin" ]]; then 82 | sed -i "" "s^#@@INJECTPATH^PATH=\$PATH:$insdir:$pybin^g" $insdir/sidechat 83 | fi 84 | 85 | if ! echo $PATH | grep "$insdir:$pybin" > /dev/null; then 86 | if [[ $(uname) == "Linux" ]]; then 87 | shell=$(getent passwd $(whoami) | awk -F / '{print $NF}') 88 | else 89 | shell=$(basename $SHELL) 90 | fi 91 | msg="**Important!**" 92 | if [[ $shell == "bash" ]]; then 93 | echo "export PATH=\$PATH:$insdir:$pybin" >> $HOME/.bashrc 94 | msg="$msg Run \`source ~/.bashrc\`" 95 | elif [[ $shell == "zsh" ]]; then 96 | echo "export PATH=\$PATH:$insdir:$pybin" >> $HOME/.zshrc 97 | msg="$msg Run \`source ~/.zshrc\`" 98 | elif [[ $shell == "fish" ]]; then 99 | config_dir="${XDG_CONFIG_HOME:-$HOME/.config}" 100 | mkdir -p "$config_dir/fish" 101 | echo "fish_add_path $insdir" >> "$config_dir/fish"/config.fish 102 | msg="$msg Run \`source ~/.config/fish/config.fish\`" 103 | else 104 | msg="$msg Add $insdir to your path" 105 | fi 106 | msg="$msg or restart your shell." 107 | fi 108 | { 109 | cat <&2 19 | ;; 20 | esac 21 | done 22 | shift $((OPTIND -1)) 23 | if [[ -z "$convo_id" ]]; then 24 | echo "No conversation ID, no context to add" 25 | exit 1 26 | fi 27 | 28 | if [ -n "$*" ]; then 29 | prompt="$*" 30 | else 31 | prompt=$convo_id 32 | fi 33 | 34 | echo -e "\nAdding to #$convo_id ..." 35 | { echo "The user is running a command and sending it to you. Here's the last 5 lines of tmux capture-pane, delimited by ----8<----" 36 | tmux capture-pane -t "${pane_id}" -p | grep -v '^$' | tail -5 37 | echo "----8<----" 38 | echo "Here is the output of the command" 39 | echo "----8<----" 40 | test -t 0 || cat; 41 | [[ -r "$1" ]] && cat "$1" 42 | } | llm "Briefly respond in less than 1 paragraph" --cid $convo_id | sd 43 | 44 | -------------------------------------------------------------------------------- /Talkers/sc-picker: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | tmp=$(python3 -c "import tempfile;print(tempfile.gettempdir())") 3 | if [[ -n "$1" ]] ; then 4 | perl -0777 -ne 'print join("\0", (split(/\0/, $_))[-'$1'])' /$tmp/sd/$UID/savebrace 5 | exit 6 | fi 7 | 8 | selected=$(perl -0777 -ne 'print join("\0", (split(/\0/, $_))[-40..-1])' /$tmp/sd/$UID/savebrace | fzf --read0 --ansi --multi --no-sort --tac --highlight-line --gap) 9 | if [ -n "$selected" ]; then 10 | tmux send-keys -t "$TMUX_PANE" "$selected" 11 | fi 12 | -------------------------------------------------------------------------------- /Talkers/sidechat: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # there's a few commands and /help documents them 3 | 4 | #set -eEuo pipefail 5 | #trap 'echo "Error on line $LINENO"; read -rp "Press enter to exit..."; exit 1' ERR 6 | 7 | # This is an OS X install-time hack 8 | #@@INJECTPATH 9 | 10 | tmp=$(python3 -c "import tempfile;print(tempfile.gettempdir())") 11 | MDIR=$tmp/sidechat 12 | [[ -d $MDIR ]] || (mkdir $MDIR && chmod 0777 $MDIR) 13 | MDIR=$MDIR/$UID 14 | [[ -d $MDIR ]] || mkdir $MDIR 15 | 16 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 17 | HISTFILE="$MDIR/history" 18 | HISTSIZE=1000 19 | INJECT= 20 | MDREAD=sd 21 | [[ -r "$MDIR/DUI" ]] || echo OFF > "$MDIR/DUI" 22 | DUI=$(< "$MDIR/DUI" ) 23 | DUIPROMPT='(Note: You are now in Do-Ur-Inspection mode: the contents of "command" code blocks with a syntax type AGENT will run. For example, ```AGENT\nls``` will run ls. Only run what is necessary. Remember, if you mention it N times, N things will run, so use it carefully. If you want the user to run something, do not use the special syntax. That is only for you. Your current path is '$PWD')' 24 | CAPTURE="ON" 25 | USECLIP= 26 | 27 | [[ -r $HOME/.local/bin/sd ]] && MDREAD=$HOME/.local/bin/sd 28 | [[ -f "$HISTFILE" ]] || touch "$HISTFILE" 29 | history -r "$HISTFILE" 30 | trap 'history -w "$HISTFILE"' EXIT 31 | if [[ $(uname) == "Linux" ]]; then 32 | config=".config" 33 | else 34 | config="Library/Application Support" 35 | fi 36 | 37 | if [[ -r "$HOME/$config/io.datasette.llm/default_model.txt" ]]; then 38 | model=$(cat "$HOME/$config/io.datasette.llm/default_model.txt") 39 | else 40 | model=$(llm models default) 41 | fi 42 | 43 | [[ -n "$TMUX" ]] && my_id=$(tmux display-message -p '#{pane_id}') || my_id= 44 | 45 | help() { 46 | if [[ "$#" -gt 0 ]]; then 47 | { 48 | echo "### Parameters" 49 | echo " * **System Prompt**:" 50 | echo "$SYSTEM" | sed 's/^/> /g' 51 | echo " * **Model**: $model" 52 | echo " * **CID**: ${conv_id:-*(Not available until first message)*}" 53 | echo " * **TMP**: $MDIR" 54 | echo -e "\n### Commands" 55 | 56 | } | $MDREAD 57 | system_prompt 58 | fi 59 | cat <). 75 | They are using a markdown formatter. Only insert newlines into code braces for syntax. 76 | They can do the following: $(help) 77 | END 78 | [[ "$DUI" == "ON" ]] && echo "$DUIPROMPT" 79 | ) 80 | } 81 | 82 | system_prompt 83 | pane_id=${1-} 84 | conv= 85 | forcecapture= 86 | debug= 87 | [[ -r "$MDIR/${pane_id}.convo" ]] && conv_id=$(cat "$MDIR/${pane_id}.convo") || conv_id= 88 | 89 | touch "$MDIR/${pane_id}.convo" 90 | touch "$MDIR/${pane_id}.old" 91 | 92 | flash() { 93 | if [[ -n "$TMUX" && -n "$pane_id" ]]; then 94 | tmux select-pane -t $pane_id -P 'bg=colour95'; sleep 0.03 95 | tmux select-pane -t $pane_id -P 'bg=color129'; sleep 0.03 96 | tmux select-pane -t $pane_id -P 'bg=default' 97 | tmux select-pane -t $my_id 98 | fi 99 | set_prompt 100 | } 101 | 102 | getmodel() { 103 | if [[ -r "$HOME/$config/io.datasette.llm/default_model.txt" ]]; then 104 | model=$(cat "$HOME/$config/io.datasette.llm/default_model.txt") 105 | else 106 | model=$(llm models default) 107 | fi 108 | model=$(echo "$model" | cut -d '/' -f 3-) 109 | } 110 | 111 | getmodel 112 | 113 | set_prompt() { 114 | local camera="— " 115 | local dui="— " 116 | local clipboard='' 117 | [[ -n "$TMUX" && $CAPTURE == "ON" ]] && camera="📷" 118 | [[ $DUI == "ON" ]] && dui="🚗" 119 | [[ -n "$USECLIP" ]] && clipboard='📋' 120 | prompt="${camera} ${dui}${clipboard} > " 121 | } 122 | 123 | choose() { 124 | [[ -z "$TMUX" ]] && return 125 | tmux display-panes -d 0 "run-shell 'echo %% > $MDIR/pane-id'" 126 | if [[ $? == "1" ]] ; then 127 | echo "Exiting due to possible infinite loop." 128 | exit 1 129 | fi 130 | if [[ -e $MDIR/pane-id ]]; then 131 | pane_id=$(cat $MDIR/pane-id) 132 | echo "Using $pane_id" 133 | flash 134 | rm $MDIR/pane-id 135 | tmux select-pane -t $my_id 136 | touch $MDIR/${pane_id}.{convo,old} 137 | INJECT=" (Note: User changed focus to a new tmux pane)" 138 | else 139 | echo "Cancelled" 140 | return 1 141 | fi 142 | return 0 143 | } 144 | 145 | last() { 146 | amount=${1:-40} 147 | if [[ -z "$conv_id" ]]; then 148 | if [[ -r "$MDIR/last.conv" ]]; then 149 | conv_id=$( cat "$MDIR/last.convo" ) 150 | else 151 | conv_id=$(sqlite3 "$HOME/$config/io.datasette.llm/logs.db" "select conversation_id from responses order by datetime_utc desc limit 1") 152 | fi 153 | fi 154 | { echo "# [ $conv_id ] "; llm logs list --cid $conv_id | tail -$amount; } | $MDREAD -c <(echo -e "[style]\nMargin = 4") 155 | conv="--cid $conv_id" 156 | } 157 | 158 | mindwipe() { 159 | echo "Memory has been wiped." 160 | system_prompt 161 | conv= 162 | conv_id= 163 | getmodel 164 | forcecapture=1 165 | } 166 | 167 | process_cmd() { 168 | infile="$1" 169 | n=$2 170 | local base="${infile}.${n}" 171 | { 172 | local llm_in="${base}-IN" 173 | local llm_parse="${base}-OUT" 174 | #echo $llm_in $llm_parse 175 | err=0 176 | 177 | for cmdfile in $(cat "$base" | "$DIR/simple-parse.py" -r '```AGENT\n(.*)```' -b "$llm_parse"); do 178 | #echo "## $cmdfile" 179 | if [[ $DUI == "ON" ]]; then 180 | cmd=$( < "$cmdfile" ) 181 | cat "$cmdfile" | grep -v '^$' | tee -a "$llm_in" | sed 's/^/### /g' 182 | echo '```bash' 183 | { echo "set -eEuo pipefail"; cat "$cmdfile"; } > "${cmdfile}.safe" 184 | { 185 | bash "${cmdfile}.safe" | tee -a $llm_in | head -30 186 | [[ $? -ne 0 ]] && err=$? 187 | } 188 | echo '```' 189 | else 190 | echo "~~ $match ~~ " 191 | fi 192 | done 193 | 194 | if [[ $err == 0 && $DUI == "ON" && -s "$llm_in" ]]; then 195 | (( n++ )) 196 | cat "$llm_in" | llm "You ran Do-Ur-Inspection commands" $conv | tee "${infile}.${n}" 197 | fi 198 | } | $MDREAD -c <(echo -e "[style]\nMargin = 4") 199 | } 200 | 201 | oneshot() { 202 | echo "$*" >> "$MDIR/input.txt" 203 | llm "$*" -s "$SYSTEM" $conv >> "$MDIR/output.txt" & 204 | } 205 | 206 | capture() { 207 | if [[ $CAPTURE == "ON" ]]; then 208 | echo "Capturing off. Shades are drawn!" 209 | CAPTURE=OFF 210 | INJECT="$INJECT (Note: User turned capture off)" 211 | else 212 | while ! tmux list-panes -t "$pane_id" &>/dev/null; do 213 | echo "Woops, the pane disappeared. Choose another!" 214 | choose 215 | done 216 | 217 | flash 218 | echo "Capturing on. I see you!" 219 | INJECT="$INJECT (Note: User turned capture on)" 220 | CAPTURE=ON 221 | fi 222 | } 223 | 224 | clip_nvim() { 225 | nvim --headless --cmd "echo getreg('+')" --cmd "qa" 2>&1 226 | } 227 | 228 | clip() { 229 | USECLIP= 230 | local xclip=$(command -v xclip) 231 | local nvim=$(command -v nvim) 232 | local pbpaste=$(command -v pbpaste) 233 | local emacs=$(command -v emacsclient) 234 | 235 | 236 | if [[ -n "$xclip" ]]; then 237 | for i in p c s; do 238 | xclip -o -${i} > "$MDIR/clip.$i" 239 | done 240 | fi 241 | 242 | tmux show-buffer > "$MDIR/clip.tmux" 2> /dev/null 243 | 244 | if [[ -n "$pbpaste" ]]; then 245 | pbpaste > "$MDIR/clip.pbpaste" 246 | fi 247 | 248 | if [[ -n "$nvim" ]]; then 249 | clip_nvim > "$MDIR/clip.nvim" 250 | fi 251 | 252 | if [[ -n "$emacs" ]]; then 253 | emacsclient --eval '(current-kill 0)' > "$MDIR/clip.emacs" 2>/dev/null 254 | fi 255 | 256 | { 257 | echo "Select the text you want to send and I'll find out what clipboard it is, hopefully..." 258 | echo -n 'Supported: `tmux` ' 259 | [[ -n "$nvim" ]] && echo -n '`nvim` ' || echo -n '~~`nvim`~~ ' 260 | [[ -n "$pbpaste" ]] && echo -n '`pbpaste` ' || echo -n '~~`pbpaste`~~ ' 261 | [[ -n "$xclip" ]] && echo -n '`xsel` ' || echo -n '~~`xsel`~~ ' 262 | [[ -n "$emacs" ]] && echo -n '`emacs` ' || echo -n '~~`emacs`~~ ' 263 | echo 264 | echo 265 | } | $MDREAD 266 | 267 | while [[ 0 ]]; do 268 | if [[ -n "$xclip" ]]; then 269 | for i in p c s; do 270 | if ! diff <(xclip -o -${i}) "$MDIR/clip.$i" > /dev/null; then 271 | USECLIP="xclip -o -${i}" 272 | break 2 273 | fi 274 | done 275 | fi 276 | 277 | if [[ -n "$emacs" ]]; then 278 | if ! diff <(emacsclient --eval '(current-kill 0)' 2>/dev/null) "$MDIR/clip.emacs" > /dev/null; then 279 | USECLIP="emacsclient --eval '(current-kill 0)' 2>/dev/null" 280 | break 281 | fi 282 | fi 283 | 284 | if [[ -n "$pbpaste" ]]; then 285 | if ! diff <(pbpaste) "$MDIR/clip.pbpaste" > /dev/null; then 286 | USECLIP=pbpaste 287 | break 288 | fi 289 | fi 290 | 291 | if [[ -n "$nvim" ]]; then 292 | if ! diff <(clip_nvim) "$MDIR/clip.nvim" > /dev/null; then 293 | USECLIP=clip_nvim 294 | break 295 | fi 296 | fi 297 | if ! diff <(tmux show-buffer 2>/dev/null) "$MDIR/clip.tmux" > /dev/null; then 298 | USECLIP="tmux show-buffer" 299 | break 300 | fi 301 | sleep 0.3 302 | done 303 | 304 | if [[ -n "$USECLIP" ]]; then 305 | { 306 | echo 307 | echo "Found!" 308 | echo "### Next prompt will use \`$USECLIP\`" 309 | } | $MDREAD 310 | else 311 | echo "Aborted" 312 | fi 313 | } 314 | 315 | dui() { 316 | if [[ "$DUI" == "ON" ]]; then 317 | DUI=OFF 318 | echo "Sobering up."; 319 | oneshot "(Note: Do-Ur-Inspection is now off. Do not talk about it. It no longer exists. Piping into sc-add is still available)" 320 | else 321 | DUI=ON 322 | echo "Fuck it, let's roll!" 323 | oneshot "$DUIPROMPT" 324 | fi 325 | echo $DUI > "$MDIR/DUI" 326 | } 327 | 328 | set_prompt 329 | ata="" 330 | if [[ -n "$conv_id" ]]; then 331 | last 5 332 | else 333 | echo "Conversation Start: Type /help for options" 334 | fi 335 | flash 336 | 337 | while [ 0 ]; do 338 | echo 339 | input="" 340 | pline="$prompt" 341 | while IFS= read -rep "$pline" line || exit; do 342 | if [[ "$line" == *\\ ]]; then 343 | input+="${line%\\}" 344 | input+=$'\n' 345 | else 346 | input+="$line" 347 | break 348 | fi 349 | pline=" | " 350 | done 351 | [[ -z "$input" ]] && continue 352 | echo 353 | 354 | _uuid=$(date +%s.%N) 355 | 356 | history -s "$input" # Save to history 357 | 358 | if [[ $input =~ ^/ ]]; then 359 | if [[ $input == '/debug' ]]; then 360 | if [[ -n "$debug" ]]; then 361 | echo "Debug Off" 362 | debug=; set +x; 363 | else 364 | echo "Debug On" 365 | debug=1; set -x; 366 | fi 367 | elif [[ $input =~ ^/sshot ]]; then 368 | sshot=$(mktemp --suffix=.png -p "$MDIR") 369 | import $sshot 370 | ata="-a $sshot" 371 | echo "Next prompt will use your screenshot" 372 | elif [[ $input =~ (clip|capture|last|dui|mindwipe|choose|flash) ]]; then 373 | ${input:1} 374 | elif [[ $input == "/model" ]]; then 375 | chosen=$(llm models list | fzf-tmux | cut -d ':' -f 2- | sed 's/^\s//g' ) 376 | if [[ -n "$chosen" ]]; then 377 | echo "Setting to $chosen" 378 | llm models default $chosen 379 | mindwipe 380 | fi 381 | else 382 | help 1 383 | fi 384 | set_prompt 385 | continue 386 | else 387 | text="$input" 388 | fi 389 | 390 | # synchronize input & output 391 | { echo -e "\n\n"; echo "$conv_id:${_uuid}"; } | tee -a "$MDIR/input.txt" | cat >> "$MDIR/output.txt" 392 | echo "($text)" >> "$MDIR/input.txt" 393 | 394 | if [[ -z "$USECLIP" && $CAPTURE == "ON" ]]; then 395 | while ! tmux capture-pane -t "${pane_id}" -p > "$MDIR/${pane_id}.new"; do 396 | echo "Choose another pane" 397 | choose 398 | if [[ $? == 1 ]]; then 399 | echo "Ok fine, no capture, that's fine." 400 | capture 401 | set_prompt 402 | break 403 | fi 404 | done 405 | fi 406 | 407 | n=0 408 | in="$MDIR/${conv_id}_${_uuid}" 409 | { 410 | if [[ -n "$USECLIP" ]]; then 411 | $USECLIP 412 | elif [[ "$CAPTURE" == "ON" ]]; then 413 | _pid=$(tmux display -pt "$pane_id" '#{pane_pid}') 414 | { 415 | if [[ "$(uname)" == "Linux" ]]; then 416 | echo "[process hierarchy: $(ps -o comm= --ppid $_pid -p $_pid)]" 417 | fi 418 | 419 | if [[ -n "$forcecapture" ]]; then 420 | cat "$MDIR/${pane_id}.new" 421 | else 422 | diff "$MDIR/${pane_id}.new" "$MDIR/${pane_id}.old" > /dev/null \ 423 | && echo \ || { echo "[capture-pane -p:"; cat "$MDIR/${pane_id}.new"; echo "]"; } 424 | cp "$MDIR/${pane_id}.new" "$MDIR/${pane_id}.old" 425 | fi 426 | } | grep -Ev "^$" \ 427 | | tee -a "$MDIR/input.txt" 428 | fi 429 | 430 | } | llm "${text}${INJECT}" -s "$SYSTEM" $ata $conv \ 431 | | tee "${in}.${n}" | tee "$MDIR/output.txt" \ 432 | | $MDREAD -c <(echo -e "[style]\nMargin = 4") 433 | 434 | [[ -n "$USECLIP" ]] && USECLIP= 435 | [[ -n "$forcecapture" ]] && forcecapture= 436 | set_prompt 437 | 438 | if [[ $DUI == "ON" ]]; then 439 | while true; do 440 | process_cmd "$in" $n 441 | (( n++ )) 442 | [[ -s "${in}.${n}" ]] || break 443 | done 444 | fi 445 | 446 | echo "${text}${INJECT}" >> "$MDIR/input.txt" 447 | 448 | INJECT= 449 | 450 | if [[ -z "$conv" ]]; then 451 | conv_id=$(sqlite3 "$HOME/$config/io.datasette.llm/logs.db" "select conversation_id from responses order by datetime_utc desc limit 1") 452 | echo $conv_id > "$MDIR/${pane_id}.convo" 453 | echo $conv_id > "$MDIR/last.convo" 454 | conv="--cid $conv_id" 455 | INJECT=" (Note: cid is $conv_id - there is no need to mention this)" 456 | fi 457 | ata="" 458 | done 459 | -------------------------------------------------------------------------------- /Talkers/simple-parse.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import argparse 3 | import re 4 | import sys 5 | import os 6 | 7 | def split_on_regex(regex, base_path): 8 | """ 9 | Splits the input from stdin based on the provided regex and writes each match 10 | to a separate file in the specified base path. 11 | 12 | Args: 13 | regex: The PCRE regex to use for splitting. 14 | base_path: The base path where the split files will be created. 15 | 16 | Returns: 17 | A list of file paths that were created. 18 | """ 19 | 20 | file_paths = [] 21 | counter = 0 22 | input_data = sys.stdin.read() 23 | 24 | matches = re.findall(regex, input_data, re.DOTALL | re.I) 25 | 26 | for match in matches: 27 | file_path = os.path.join(base_path, f"out.{counter}") #Construct file path 28 | try : 29 | with open(file_path, "w") as outfile: 30 | outfile.write(match) # Use match, not the entire input 31 | file_paths.append(file_path) 32 | except OSError as e: 33 | print(f"Error creating or writing to file {file_path}: {e}", file=sys.stderr) 34 | return [] # Handle error gracefully, return empty list 35 | 36 | counter += 1 37 | 38 | return file_paths 39 | 40 | 41 | if __name__ == "__main__": 42 | parser = argparse.ArgumentParser( 43 | description="Splits input from stdin based on a regex and writes each match to a file." 44 | ) 45 | parser.add_argument( 46 | "-r", "--regex", required=True, help="The PCRE regex to use for splitting." 47 | ) 48 | parser.add_argument( 49 | "-b", "--base_path", required=True, help="The base path where the split files will be created." 50 | ) 51 | 52 | args = parser.parse_args() 53 | 54 | # Create the base path if it doesn't exist. Important for proper functioning 55 | if not os.path.exists(args.base_path): 56 | try: 57 | os.makedirs(args.base_path) 58 | except OSError as e: 59 | print(f"Error creating base path {args.base_path}: {e}", file=sys.stderr) 60 | sys.exit(1) 61 | 62 | file_paths = split_on_regex(args.regex, args.base_path) 63 | 64 | for path in file_paths: 65 | print(path) 66 | -------------------------------------------------------------------------------- /Xtractors/kb-capture.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S uv run --script 2 | # /// script 3 | # requires-python = ">=3.8" 4 | # dependencies = [ 5 | # "python-xlib", 6 | # "ipdb" 7 | # ] 8 | # /// 9 | 10 | 11 | 12 | 13 | from Xlib import X, XK, display 14 | from Xlib.ext import record 15 | from Xlib.protocol import rq 16 | 17 | local_display = display.Display() 18 | record_display = display.Display() 19 | 20 | captured_string = "" 21 | 22 | def lookup_keycode(keycode): 23 | keysym = local_display.keycode_to_keysym(keycode, 0) 24 | return XK.keysym_to_string(keysym) if keysym else None 25 | 26 | def callback(data): 27 | global captured_string 28 | if not data or data.category != record.FromServer or data.client_swapped or not len(data.data): 29 | return 30 | 31 | if data.data.startswith(b'\x02') or data.data.startswith(b'\x03'): 32 | event = data.data[0] 33 | if event == X.KeyPress or data.data.startswith(b'\x02'): 34 | keycode = data.data[1] 35 | state = data.data[2] 36 | shift_pressed = state & 1 37 | keysym = local_display.keycode_to_keysym(keycode, 1 if shift_pressed else 0) 38 | key = XK.keysym_to_string(keysym) if keysym else None 39 | 40 | if key: 41 | if key == ";" or key == ":": 42 | print(captured_string) 43 | exit(0) 44 | elif key == "BackSpace": 45 | captured_string = captured_string[:-1] 46 | else: 47 | captured_string += key 48 | 49 | if not record_display.has_extension("RECORD"): 50 | print("X server does not support the RECORD extension") 51 | exit(1) 52 | 53 | # Fix the record context creation 54 | ctx = record_display.record_create_context( 55 | 0, 56 | [record.AllClients], 57 | [{ 58 | 'core_requests': (0, 0), 59 | 'core_replies': (0, 0), 60 | 'ext_requests': (0, 0, 0, 0), 61 | 'ext_replies': (0, 0, 0, 0), 62 | 'delivered_events': (0, 0), 63 | 'device_events': (X.KeyPress, X.KeyRelease), # Captures key events 64 | 'errors': (0, 0), 65 | 'client_started': False, 66 | 'client_died': False, 67 | }] 68 | ) 69 | 70 | record_display.record_enable_context(ctx, callback) 71 | 72 | -------------------------------------------------------------------------------- /Xtractors/llm-magic: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 6 | echo "Capturing!" | dzen2 -ta c -y 0 -x 0 -p 20 -m -fg "#FEAEAE" -bg "#900000" & 7 | pid1="$!" 8 | question="$($DIR/kb-capture.py)" 9 | kill $pid1 10 | 11 | len="${#question}" 12 | echo "$question" | dzen2 -ta c -y 0 -x 0 -p 10 -m -fg "#FEAEAE" -bg "#410141" & 13 | pid1="$!" 14 | 15 | if [[ "$len" -lt 10 ]]; then 16 | kill $pid1 17 | echo "Aborted" | dzen2 -ta c -y 0 -x 0 -p 1 -m -bg "#FEAEAE" -fg "#110111" & 18 | exit 19 | fi 20 | 21 | res=$(import -depth 3 -window root png:- | llm -xs "Answer this question as briefly as possible. Give only the code or command needed, no other explanations. The image provided is only to establish context of what the user is doing to help guide you with the needed code or command. For instance, they may be on a specific website trying to invoke an advanced search or an in editor" "Answer only with the code or command needed: $question" --at - image/png) 22 | if [[ -z "$res" ]]; then 23 | res=$(llm -xs "Answer this question as briefly as possible. Give only the code or command needed, no other explanations." "Answer only with the code or command needed: $question" ) 24 | fi 25 | kill $pid1 26 | echo "$question | $res" >> /tmp/llm-magic 27 | res=$(echo "$res" | sed -E 's/(^`|`$)//g') 28 | echo "$res" | dzen2 -ta c -y 0 -x 0 -p 1 -m -bg "#FEAEAE" -fg "#110111" 29 | 30 | for i in $(seq 0 "${#question}"); do 31 | xdotool key BackSpace 32 | done 33 | 34 | xdotool type "$res" 35 | -------------------------------------------------------------------------------- /assets/llmehelp.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/day50-dev/sidechat/a1340a08e5d94b424e1f14c4f3533e2782cea5d7/assets/llmehelp.jpg -------------------------------------------------------------------------------- /assets/llmehelp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/day50-dev/sidechat/a1340a08e5d94b424e1f14c4f3533e2782cea5d7/assets/llmehelp.png -------------------------------------------------------------------------------- /assets/sidechat-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 14 | 34 | 39 | 44 | 45 | 47 | 54 | 57 | 58 | 65 | 68 | 69 | 79 | 81 | 85 | 89 | 90 | 100 | 102 | 106 | 110 | 111 | 118 | 121 | 122 | 132 | 139 | 142 | 143 | 151 | 158 | 161 | 162 | 169 | 172 | 173 | 180 | 183 | 184 | 191 | 194 | 195 | 202 | 205 | 206 | 207 | 210 | 214 | 218 | 222 | 230 | 238 | 239 | sidechat 250 | 251 | -------------------------------------------------------------------------------- /assets/sidechat_100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/day50-dev/sidechat/a1340a08e5d94b424e1f14c4f3533e2782cea5d7/assets/sidechat_100.png -------------------------------------------------------------------------------- /assets/sidechat_200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/day50-dev/sidechat/a1340a08e5d94b424e1f14c4f3533e2782cea5d7/assets/sidechat_200.png -------------------------------------------------------------------------------- /one-shot/talker: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cat << ENDL 3 | 4 | ░▒▓███████▓▒░ ░▒▓██████▓▒░░▒▓█▓▒░░▒▓█▓▒░ ░▒▓████████▓▒░▒▓████████▓▒░ 5 | ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ 6 | ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ 7 | ░▒▓█▓▒░░▒▓█▓▒░▒▓████████▓▒░░▒▓██████▓▒░ ░▒▓███████▓▒░░▒▓█▓▒░░▒▓█▓▒░ 8 | ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ 9 | ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ 10 | ░▒▓███████▓▒░░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓███████▓▒░░▒▓████████▓▒░ 11 | 12 | 13 | Keeping AI Productive 50 Days In And Beyond 14 | 15 | ENDL 16 | insdir=$(mktemp -u) 17 | git clone --quiet --depth=1 https://github.com/kristopolous/llmehelp $insdir 18 | $insdir/Talkers/installer.sh 19 | --------------------------------------------------------------------------------