Practical LLM Applications on the Linux Command Line
Small shell scripts are often sufficient to turn a general-purpose LLM into a repeatable Linux tool.
The examples on this page cover direct local prompting, provider-independent prompting, OCR, plant
recognition, transcript summarization and an MCP-based Outlook bridge that makes a Windows Outlook instance available to an LLM over an SSH-accessible WSL2 environment. The deliberately minimal myllmcalllocal talks
directly to the Ollama command line and lets the user choose an installed model interactively. The more
automated scripts move model choice into ~/models.txt and can delegate inference either to
OpenCode with a provider such as OpenRouter, or directly to Ollama. OpenCode Zen and local Ollama models
are natural alternatives without changing the basic workflow design.
The scripts follow the usual Unix division of labour. Bash handles arguments, files, naming, loops,
subprocesses and deterministic text processing. The LLM is called only for the semantic part of the
task: understanding an image, transcribing a scan, identifying a plant or compressing a transcript.
This keeps the surrounding workflow transparent and easy to automate.
Bash script
arguments, files, pipes
model choice→
interactive or ~/models.txt
simple local call or automated configuration
inference→
Ollama or OpenCode
local or provider-backed LLM
Generic LLM calls
myllmcalllocal is the minimal direct Ollama variant for a task file and interactive model
selection. myopencodecall turns opencode run into a provider-independent shell
command for a prompt plus an optional text or image file.
OCR and vision
myollamaocr performs local OCR through Ollama, while myopenrouterocr and
myopenrouterimagerecognition use OpenCode with a vision-capable provider model.
Transcript processing
myyoutubesubs downloads and cleans YouTube subtitles; myyoutubesubs_local
extracts subtitles from local MP4 files. Both can optionally ask an LLM for a condensed version.
1
myllmcalllocal: The Simplest Direct Local LLM Call
A minimal Bash wrapper around the local ollama command.
myllmcalllocal demonstrates the shortest path from a text file to a locally installed language
model. It accepts exactly one task file, verifies that the ollama command exists, obtains the
locally available model names from ollama list, and presents them through Bash's built-in
select menu. The selected model then receives the complete file content on standard input.
The model response is written to result.txt.
Interface
Usage: myllmcalllocal [-h] <task>.txt
The task itself is simply a text file. There is no provider configuration, API key, model option or
OpenCode dependency. The only runtime prerequisite is a working local Ollama installation with at
least one downloaded model.
Design characteristic
Model selection is intentionally interactive rather than configuration-driven. This makes the script
well suited for manual experiments and illustrates the local inference mechanism without an additional
abstraction layer. For unattended or repeatable workflows, the later scripts replace this interaction
with ~/models.txt.
Essential excerpt · discover and select a local model
models=$(ollama list | awk -F' ' '{print $1}')
options=($models)
PS3="Please select a model: "
select model in "${options[@]}"; do
if [[ -n $model ]]; then
break
fi
echo "Invalid option. Please select a valid model."
done
Essential excerpt · invoke Ollama through a Unix pipe
cat "$1" | ollama run "$model" > result.txt
Local workflow
task.txt→myllmcalllocal→ollama list→interactive model choice→ollama run→result.txt
2
Central Model Selection with ~/models.txt
Model choice is configuration, not a command-line option.
Each script derives its own name from $0 and searches ~/models.txt for a line
beginning with that name followed by a colon. If no script-specific entry exists, the line
default: is used. The user can therefore switch models globally or per application without
modifying the scripts and without adding a model-selection option to every command line.
If neither exists, terminate with an explicit error.
For OpenCode calls, model identifiers use OpenCode's provider/model notation. The local
myollamaocr script is the deliberate exception because it talks directly to Ollama and
therefore uses Ollama's own model name such as glm-ocr.
A small wrapper around opencode run for shell scripts and one-shot commands.
myopencodecall is the generic member of the set. The remaining command-line arguments form
the prompt. With -f, one text or image file is attached. The model itself is deliberately not
selectable on the command line; it is resolved through ~/models.txt and then passed to
OpenCode with -m internally.
Essential excerpt · constructing the OpenCode invocation
load_model
prompt="$*"
args=(run -m "$MODEL" "$prompt")
if [ -n "$file" ]; then
args+=(-f "$file")
fi
opencode "${args[@]}"
myopencodecall "Explain the difference between fork and exec."
myopencodecall -f notes.txt "Summarize the attached file in five bullet points."
myopencodecall -f scan.png "Describe this image."
4
OCR: Local Ollama or Cloud Vision Model
Two scripts implement the same application with different inference paths.
myollamaocr · local OCR
This script bypasses OpenCode and calls the local Ollama generate API directly. The image is Base64
encoded, inserted into a JSON request with jq, and sent to
http://127.0.0.1:<OLLAMA_PORT>/api/generate. The recognized text is written beside the image as
a .txt file.
Here OpenCode supplies the attachment handling and provider abstraction. The script contributes the
task-specific OCR prompt, the input file and the configured model. With the supplied configuration,
that model is obtained through OpenRouter.
opencode run \
-m "$MODEL" \
'Perform OCR on this scanned document.
Transcribe all visible text accurately,
preserve line and paragraph structure where possible,
and output only the transcription.' \
-f "$file" \
> "$out"
Two OCR paths
image→myollamaocr→Ollama localhost→local vision model
image→myopenrouterocr→OpenCode→OpenRouter / alternative provider
5
myopenrouterimagerecognition: Plant Recognition
Vision models can be wrapped just as easily as text models.
This script asks a vision-capable model to identify a plant from an image. It returns the most likely
German common name and, where reliable, the scientific name. In single-file mode it creates a matching
.txt file. Batch mode scans the current directory for JPEG images and additionally produces
recognition.csv.
Essential excerpt · task-specific vision prompt
opencode run \
-m "$MODEL" \
'Identify the plant shown in this image.
Give the most likely German common name first,
followed by the scientific name in parentheses
if you can determine it reliably.
If the identification is uncertain, say so and
give the most likely alternatives.
Output only the identification.' \
-f "$file" \
> "$out"
Plant-recognition workflow
JPG / JPEG→script or batch loop→vision model→TXT+recognition.csv
The supplied models.txt assigns the vision-capable
openrouter/qwen/qwen3-vl-8b-instruct specifically to this script, independently of the
default text model. This is exactly the kind of specialization for which the per-script configuration
was introduced.
6
Transcript Processing and Optional LLM Summaries
The deterministic part stays in ordinary command-line tools; OpenCode is added only at the end.
myyoutubesubs · online source
yt-dlp downloads automatic subtitles for a single YouTube URL or a URL list. An AWK
stage removes WebVTT headers, timestamps, tags, entities, empty lines and immediately repeated lines.
The result becomes a normal text file. With -s, OpenCode summarizes that file.
yt-dlp \
--write-auto-subs \
--skip-download \
"$URL"
# ... VTT cleanup with awk ...
opencode run -m "$MODEL" -f "$file" \
"Summarize the content of the attached file
concisely and completely. Output only the summary." \
> "$tmp"
fold -s -w 80 "$tmp" > "$condensed"
myyoutubesubs_local · local MP4 source
The local counterpart processes all MP4 files in the current directory. ffprobe locates
an embedded subtitle stream, preferring the stream marked as default. ffmpeg extracts it
as SRT, with WebVTT as fallback. The same cleanup and optional OpenCode summarization then follow.
YouTube / MP4→yt-dlp or ffprobe/ffmpeg→AWK cleanup→TXT→OpenCode summary→*_condensed.txt
7
Remote Outlook Automation through MCP, WSL2 and Win32OLE
Natural-language access to a local Windows Outlook installation, including read and write operations, through an SSH-accessible WSL2 host.
This application turns a normally local, GUI-bound Outlook installation into an LLM-accessible service without replacing Outlook's native data access.
The LLM client may run directly in WSL2 or on a separate machine and reach outlookmcp.py, a Python MCP server built with MCPServer,
through an SSH-carried MCP stdio stream.
Each decorated MCP tool delegates to a common _call() function, which serializes one operation as a JSON line and sends it over TCP to a Ruby
bridge executed by Windows Ruby. The bridge uses WIN32OLE to attach to the running Outlook.Application COM object
and performs the requested operation directly against the configured mailbox and calendar. Because WSL2 can start Windows executables through interop,
the Ruby bridge can be started, stopped and checked entirely from an SSH session into WSL2; Windows Explorer is not required.
LLM client / OpenCode
local or remote natural-language request
stdio / MCP→
outlookmcp.py
WSL2 MCP adapter
TCP / JSON-lines→
outlookservermcp.rb
Windows Ruby · WIN32OLE / COM
COM / MAPI→
Outlook.exe
mailbox and calendar
Remote access path
OpenCode on host A→SSH stdio transport→outlookmcp.py on WSL2 host B→local TCP/JSON bridge→Outlook COM
Remote-local MCP over SSH
The MCP client does not have to run on the Outlook machine. A client such as OpenCode can run on a separate host A and start the stdio MCP server
remotely on host B through SSH. OpenCode still treats the process as a local MCP command: its stdin and stdout are connected to ssh, while
SSH transparently carries the MCP stdio stream to outlookmcp.py on the remote WSL2 system. The Outlook bridge remains local to host B and
does not need to be exposed or forwarded to host A.
+---------------------------+
| Host A |
| |
| OpenCode |
| | |
| | MCP over stdio |
| v |
| ssh remoteuser@wsl-host |
+-------------|-------------+
|
| encrypted SSH connection
| stdin / stdout
v
+-------------|--------------------------------------+
| Host B / WSL2 |
| |
| outlookmcp.py |
| | |
| | local TCP / JSON-lines |
| v |
| Windows-side Ruby bridge |
| outlookservermcp.rb |
| | |
| | Win32OLE / COM |
| v |
| Outlook.exe |
| mailbox + calendar |
+--------------------------------------------------+
No Outlook bridge port is forwarded to host A.
SSH transports only the MCP server's stdio stream.
OpenCode configuration on host A
The remote MCP server is configured as a normal local command. The command executed by OpenCode is simply ssh; the remaining arguments
select the remote host, the Python interpreter from the remote virtual environment and the MCP server script.
A manual smoke test is equivalent to running
ssh remoteuser@wsl-host /home/remoteuser/app/outlook/.venv/bin/python3 /home/remoteuser/app/outlook/outlookmcp.py.
If the command remains attached without producing ordinary output, the stdio MCP server is waiting for protocol messages as expected.
Mail: read and search
The MCP server lists folders and messages, retrieves complete message bodies, filters unread mail and offers both simple single-field searches and a
combined search whose sender, recipient and subject criteria are AND-ed. The combined search is date-bounded and defaults to the last 30 days when no
range is supplied. Outlook items are returned with their EntryID, so subsequent requests address stable Outlook objects rather than transient
list positions. All attachments of a selected mail can be saved to the Windows download directory, with collision-safe file naming.
Mail: write and organize
New messages can be created as drafts, replied to, replied to all, forwarded and finally sent. Messages can also be moved or deleted. Recipient names
are resolved first through a small local contacts.csv and then through Outlook's Global Address Book; unresolved names are returned to the LLM
instead of silently producing an incomplete draft. A sender can be added idempotently to the local contacts file directly from a selected message.
Calendar and invitations
Calendar entries can be listed and retrieved, and new non-meeting appointments can be created as one-off or recurring entries. Creation supports
all-day or timed appointments, busy status, recurrence intervals and daily, weekly, monthly or yearly recurrence. Solo appointments can be moved or
deleted; a single occurrence of the user's own recurring series can also be moved independently. Meeting invitations can be accepted, tentatively
accepted or declined through Outlook's native meeting-response mechanism.
HOST = "<WINDOWS_WSL_INTERFACE_IP>"
PORT = <OUTLOOK_BRIDGE_PORT>
MAILBOX = "<MAILBOX_ADDRESS>"
$server = TCPServer.new(HOST, PORT)
loop do
client = $server.accept
while (line = client.gets)
request = JSON.parse(line.strip)
response = handle(request)
client.puts(JSON.generate(response))
end
ensure
client.close rescue nil
end
Essential excerpt · attach to Outlook through Win32OLE
begin
$outlook = WIN32OLE.connect('Outlook.Application')
rescue
$outlook = WIN32OLE.new('Outlook.Application')
end
WIN32OLE.const_load($outlook, OutlookConst)
$ns = $outlook.GetNamespace("MAPI")
$top = $ns.Folders.Item(MAILBOX)
Essential excerpt · stable addressing and safe draft-before-send workflow
# Read an existing item by Outlook EntryID
mail = $ns.GetItemFromID(req["entry_id"], $top.StoreID)
# Create and save a draft first
mail = $outlook.CreateItem(OutlookConst::OlMailItem)
mail.Subject = req["subject"].to_s
mail.Body = req["body"].to_s
mail.Save
# Sending is a separate operation on that draft
mail = $ns.GetItemFromID(req["entry_id"], $top.StoreID)
mail.Send
Essential excerpt · SSH-friendly bridge control from WSL2
Outlook itself must be running in an interactive Windows user session and logged in to the target mailbox. The bridge code may live on the WSL filesystem:
Windows Ruby can execute it through WSL interop and the corresponding UNC mapping. The WSL-side control script keeps a PID file and log, supports
start, stop, restart and status, and verifies the TCP listener. The virtual WSL interface address can change
after Windows or WSL updates, so both ends should use configuration variables rather than embedding an address in application logic.
8
OpenCode, OpenRouter, OpenCode Zen and Ollama
The scripts separate application logic from the concrete model provider.
OpenRouter
In the supplied setup, most OpenCode-based scripts use model IDs beginning with
openrouter/. OpenCode handles the file attachment and request; OpenRouter acts as the
hosted model gateway. A single OpenCode installation can therefore use different text and vision
models without provider-specific shell code.
OpenCode Zen
OpenCode Zen is an alternative provider integrated into OpenCode. It exposes a curated set of models
tested for OpenCode and is configured like other providers. After authentication, a Zen model can be
selected with an OpenCode model ID such as opencode/<model>. For these scripts, changing
the corresponding entry in ~/models.txt is sufficient; the application code remains the same.
Ollama
Ollama is the local alternative. It can be used directly, as in myollamaocr, or exposed
as an OpenCode provider. In the latter case the existing OpenCode-based scripts can keep their
opencode run workflow while inference occurs on localhost, provided the local model has
the required text or vision capabilities.
Provider abstraction through OpenCode
application script→model ID from ~/models.txt→OpenCode→OpenRouter/OpenCode Zen/Ollama
Why keep OpenCode between Bash and the provider?
The shell scripts need only one stable interface: opencode run -m MODEL PROMPT -f FILE.
Provider authentication, model discovery and provider-specific communication remain OpenCode's job.
This is especially useful for multimodal calls because the Bash code does not need to construct provider-specific
JSON payloads or Base64 attachments. The direct Ollama OCR script demonstrates the opposite design when full local
control and a minimal HTTP dependency are preferred.
Switching provider without changing the script
# Hosted gateway
myopenrouterocr:openrouter/qwen/qwen3-vl-8b-instruct
# Alternative: OpenCode Zen
myopenrouterocr:opencode/<vision-model>
# Alternative: locally configured Ollama provider in OpenCode
myopenrouterocr:ollama/<vision-model>
Direct Comparison of the Eight Applications
Script
Purpose
Deterministic tools
LLM path
Output
myllmcalllocal
simple local task-file execution
Bash, awk, select, pipe
direct Ollama CLI, interactively selected model
result.txt
myopencodecall
generic prompt + optional file
Bash argument handling
OpenCode → configured provider
stdout
myollamaocr
local document OCR
base64, jq, curl
direct Ollama API
basename.txt
myopenrouterocr
cloud/provider document OCR
Bash file handling
OpenCode → OpenRouter by current config
basename.txt
myopenrouterimagerecognition
plant identification
Bash loop + CSV output
OpenCode → vision provider
TXT + optional CSV
myyoutubesubs
YouTube subtitle extraction + summary
yt-dlp, awk, fold
optional OpenCode summary
TXT / condensed TXT
myyoutubesubs_local
embedded MP4 subtitles + summary
ffprobe, ffmpeg, awk, fold
optional OpenCode summary
TXT / condensed TXT
Outlook MCP bridge
remote natural-language mail and calendar automation
SSH, WSL2, TCP/JSON-lines, Ruby WIN32OLE/COM
LLM client → MCP adapter → Outlook bridge
Outlook reads/writes + LLM-generated summaries/drafts
Common pattern:
keep deterministic processing in shell utilities and put only the semantic step behind an LLM call. The
minimal myllmcalllocal variant chooses a local Ollama model interactively; the automated scripts
keep model selection in ~/models.txt. OpenCode provides a uniform bridge to hosted or local
providers, while direct Ollama access remains useful where a completely local, narrowly controlled call is
preferable. The scripts therefore remain small while the model backend can evolve independently. The Outlook MCP example extends the same Unix-oriented philosophy in the other direction: instead of sending a file to a model, it exposes a stateful desktop application as a set of deterministic tools that the model can combine through natural language.
Official References
OpenCode models and provider configuration: opencode.ai/docs/models and opencode.ai/docs/providers
OpenCode Zen: opencode.ai/docs/zen
OpenRouter provider and model gateway: openrouter.ai