talksh is a minimal, Unix-friendly command-line tool for running LLM prompts on data. It reads from stdin, calls an LLM, and prints the result to stdout.
It's designed to be a simple, powerful building block in your shell, letting you compose it with other tools like cat, grep, jq, pv, etc.
For example, to count the number of posts about AI in your blog:
cat blog-posts.jsonl | jq '.title' | \
talksh map --prompt 'Is this post about AI? Answer with exactly YES or NO, nothing else.\n\n{{}}' | \
grep 'YES' | wc -lThere are two main ways to install talksh.
Every push builds fresh binaries. Grab the latest run:
- Visit https://github.com/piojanu/talksh/actions
- Click the most recent âś… run on
main(or the tag you care about) - Download the
talksh-<os>-<arch>artifact you need - Rename and move the
talkshbinary to a directory in your$PATH.
For non-root users (Recommended):
A common user-specific location is $HOME/.local/bin.
# Ensure the directory exists
mkdir -p $HOME/.local/bin
# Move the binary
mv talksh-<os>-<arch> $HOME/.local/bin/talksh(Note: Make sure $HOME/.local/bin is in your $PATH by adding export PATH="$HOME/.local/bin:$PATH" to your ~/.bashrc or ~/.zshrc if it isn't already.)
For root users (System-wide):
You can make talksh available to all users by placing it in /usr/local/bin.
sudo mv talksh-<os>-<arch> /usr/local/bin/talkshIf you have the Go toolchain installed, you can install talksh with a single command:
go install github.com/piojanu/talksh@latestThis will compile and place the talksh binary in your $GOPATH/bin directory. Make sure this directory is in your system's $PATH.
talksh is configured via a file or environment variables, loaded in this order of priority:
- Default Config File:
$HOME/.talksh.yaml - Custom Config File: (Specified with
--config /path/to/config.yaml) - Environment Variables
By default, talksh looks for a config file at $HOME/.talksh.yaml.
Create this file with the following structure:
# $HOME/.talksh.yaml
api:
base_url: "https://api.openai.com/v1"
key: "sk-your-key-here"
model: "gpt-4o"You can also specify a custom config file path using the global --config flag:
cat ... | talksh --config /path/to/my-config.yaml map ...You can override any config file setting with environment variables. talksh automatically maps . in the keys to _ (e.g., api.key becomes API_KEY).
API_KEYoverridesapi.keyAPI_BASE_URLoverridesapi.base_urlAPI_MODELoverridesapi.model
Example:
# This key will be used for this session, ignoring the config file's key
export API_KEY="sk-other-key-for-this-session"
cat ... | talksh map ...talksh has two simple commands that represent the "map" and "reduce" patterns.
A 1-to-1 operation. It runs your prompt for each line of input it receives.
- 100 lines of input = 100 LLM calls.
- Use for: Transforming, extracting, or analyzing many small items (like files, log lines, or JSON objects).
An N-to-1 operation. It reads the entire input as a single block of text and runs your prompt once.
- 100 lines of input = 1 LLM call.
- Use for: Summarizing, synthesizing, or getting a single answer about a large amount of text.
Your prompt tells talksh what to do with the content you pipe in. Inside your prompt, you must use the {{}} placeholder to mark where the input content should go.
- For
talksh map,{{}}is replaced by each line, one at a time. - For
talksh reduce,{{}}is replaced by the entire text block at once.
You can provide a prompt in two ways:
- As a string:
--prompt "Summarize this: {{}}" - From a file:
--prompt-file ./my-prompts/summarize.txt
Here are common use cases, starting with the basics.
Pipe a line of text into talksh map. It will replace {{}} with "Hello World" and send it to the LLM.
echo "Hello World" | talksh map --prompt 'Say "{{}}"'Pipe multiple lines of text into talksh map. It will replace {{}} with each line of text and send it to the LLM one by one.
printf 'foo\nbar\n' | talksh map --prompt 'Say "{{}}"'If your prompt doesn't need any input text, you can use echo "" to pipe in a single empty line to trigger the command. This is great for asking one-off questions.
echo "" | talksh map --prompt "How do I use 'xargs' to cat the content of files listed in a text file?"Or put this function in your .bash_aliases.
# Ask talksh anything
function ata() { echo "" | talksh reduce --prompt "$1" }
$ ata 'How do I kill the whole slurm array?'To summarize all your Markdown files into one answer, cat them all together and pipe them into talksh reduce. {{}} will be replaced by the combined text of all files.
cat *.md | talksh reduce --prompt "Summarize all the following text into 5 bullet points: {{}}"This is a primary use case. talksh map is perfect for JSONL (JSON Lines) files, where each line is a separate JSON object. talksh map sees each line (object) as one item.
cat data.jsonl | talksh map --prompt "Extract the 'user' and 'timestamp' from this JSON: {{}}"What if you have one giant, pretty-printed JSON file (data.json) that is an array?
[
{
"user": "alice",
"post": "Hello world"
},
{
"user": "bob",
"post": "Hi there"
}
]Use jq to "explode" the array and "compact" (with -c) each object onto a single line. talksh map can then process them one by one.
# jq -c '.[]' turns the file above into a stream of single-line JSON objects:
# {"user":"alice","post":"Hello world"}
# {"user":"bob","post":"Hi there"}
cat data.json | jq -c '.[]' | talksh map --prompt "Who wrote this post? {{}}"talksh only accepts one prompt at a time. To run multiple prompts over the same data, use a shell for loop.
Let's say you have a directory ~/my-prompts/ with:
01-summarize.txt(contains "Summarize this: {{}}")02-extract-topics.txt(contains "Extract 3 main topics from this: {{}}")
You can run them all against your files like this:
# This loop runs once for each prompt file
for p in ~/my-prompts/*.txt; do
echo "--- Running prompt: $p ---" >&2
cat *.md | talksh reduce --prompt-file "$p"
done(Note: >&2 prints the "Running prompt" message to stderr, so it doesn't get mixed up with your real output.)
This is the most powerful use case. You can pipe the output of talksh map directly into talksh reduce for a two-stage analysis.
Let's say you have logs.jsonl full of server logs.
Step 1. map: First, "map" over every log line. Ask the LLM to identify a specific error. If the error isn't found, it should output a consistent string like NO_PROBLEM.
Step 2. reduce: Second, "reduce" the results from the map step. Pipe the output of map into reduce and ask a final question about the collected results.
# Define the prompts
MAP_PROMPT="Does this log line contain a 'Database Connection Error'? If yes, describe the error. If no, just say 'NO_PROBLEM'. Here is the log: {{}}"
REDUCE_PROMPT="You will receive a list of error descriptions. Count the total number of errors (ignore lines that say 'NO_PROBLEM') and provide 3 representative examples. Here is the list: {{}}"
# Run the pipeline
cat logs.jsonl | talksh map --prompt "$MAP_PROMPT" | talksh reduce --prompt "$REDUCE_PROMPT"This command first runs talksh map hundreds of times (once per log line), and then runs talksh reduce once on the collected output of the first stage.
LLM calls are slow, especially for map commands. You can track progress by piping your data through pv (Pipe Viewer), a standard Unix tool (brew install pv or apt-get install pv).
pv's behavior changes depending on whether it can guess the total size of the stream.
If you pipe a single file into pv, it can read the file's size and show you a full percentage-based progress bar. This is the simplest case.
# pv knows the total size of logs.jsonl and shows a real-time %
cat logs.jsonl | pv | talksh map --prompt "Is this an error log? {{}}"Terminal Output:
[=======> ] 35% 1.2MiB/s
This is the most common case for pipelines. If pv receives a stream of unknown size (like the output from jq or cat *), it can't show a percentage.
Instead, you can use the -l (line count) flag. This shows a live-updating counter, which is perfect for tracking talksh map.
# pv -l counts the lines coming out of jq
cat data.json | jq -c '.[]' | pv -l | talksh map --prompt "Processing... {{}}"This also applies to piping multiple files:
# pv -l counts the total lines from all .md files
cat *.md | pv -l | talksh reduce --prompt "Summarize this... {{}}"Terminal Output:
[ 145 ]
(This shows 145 lines have been processed so far)
What if you have a complex stream (like from jq or cat *) but you really want a percentage bar?
You must use a two-step "temp file" trick. This forces the stream to be fully processed first, so pv can read it as a single file (like in Method 1).
Step 1. Run your slow jq process and save the result.
cat data.json | jq -c '.[]' > /tmp/processed.jsonlStep 2. Run talksh on that file with pv.
Now pv can see the file's total size and show a percentage.
cat /tmp/processed.jsonl | pv | talksh map --prompt "Processing... {{}}"Terminal Output:
[=======> ] 35% 1.2MiB/s