├── .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 | 
18 |
19 |
20 |
21 | You should also use `sc-add` which can pipe anything into the context. Here's an example:
22 | 
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 | 
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 | 
34 |
35 | 
36 |
37 | 
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 |
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 |
--------------------------------------------------------------------------------