X Home
Erbauliches Wörterbücher - online Lateinische Verbklassen Griechische Grammatik Griechische Verbklassen dynamisch Griechische Verbformen (breit) Griechische Verbformen (schmal) Griechischer Wortschatz Griechisch-Konverter
AI: MCP-Server AI: Local/Cloud LLMs AI: AI-Applications Toolbox CSV-Verarbeitung Scanner Toolbox Lexer Eigener Nameserver Cloud-Telefonanlage mit Asterisk Fernsteuerung von Outlook per ssh Interaktive HTML-Tabelle Bilderverwaltung im Browser CSV => Excel (formatiert) Befehlsreferenzen
Horae vulnerant ... Terminal 2.0 ...



Bash · models.txt · OpenCode · OpenRouter · OpenCode Zen · Ollama · OCR · Vision · Summaries · MCP · Outlook

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.

Bash OpenCode OpenRouter OpenCode Zen Ollama models.txt MCP Outlook

Overview: LLMs as Small Unix Tools

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.

Example configuration

default:openrouter/deepseek/deepseek-v4-flash-0731
myopencodecall:openrouter/deepseek/deepseek-v4-flash-0731
myopenrouterimagerecognition:openrouter/qwen/qwen3-vl-8b-instruct
myopenrouterocr:openrouter/qwen/qwen3-vl-8b-instruct
myyoutubesubs_local:openrouter/deepseek/deepseek-v4-flash-0731
myyoutubesubs:openrouter/deepseek/deepseek-v4-flash-0731
myollamaocr:glm-ocr

Lookup policy

  1. Require $HOME/models.txt.
  2. Look for <script-name>:<model>.
  3. If absent, look for default:<model>.
  4. 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.

Essential excerpt · common load_model()

MODELS_FILE="$HOME/models.txt"
MODEL=""

load_model()
{
    local script_name=${0##*/}

    [ -f "$MODELS_FILE" ] || {
        echo "Fehler: Modelldatei '$MODELS_FILE' nicht gefunden." >&2
        exit 1
    }

    MODEL=$(awk -v key="$script_name" '
        index($0, key ":") == 1 {
            print substr($0, length(key) + 2)
            exit
        }' "$MODELS_FILE")

    [ -n "$MODEL" ] ||
        MODEL=$(awk '
            index($0, "default:") == 1 {
                print substr($0, 9)
                exit
            }' "$MODELS_FILE")

    [ -n "$MODEL" ] || exit 1
}
3

myopencodecall: Generic Prompt and File Interface

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[@]}"

Generic call workflow

prompt + optional file→ myopencodecall→ ~/models.txt→ opencode run→ configured provider
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.

base64 -w 0 "$INPUT" > "$IMAGE64"

jq -n \
    --rawfile image "$IMAGE64" \
    --arg model "$MODEL" \
    '{
        model: $model,
        prompt: "Text Recognition:",
        images: [$image],
        stream: false
    }' > "$REQUEST"

curl --data-binary "@$REQUEST" \
     "$OLLAMA_URL"

myopenrouterocr · OCR through OpenCode

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.

ffprobe -v error \
  -select_streams s \
  -show_entries stream=index:stream_disposition=default \
  -of csv=p=0 -- "$file"

ffmpeg -v error -y -i "$file" \
  -map "0:${stream_index}" -c:s srt "$srt"

opencode run -m "$MODEL" \
  "Summarize the content ... without translation." \
  -f "$inputfile" \
  > "$tmp"

Transcript-to-summary workflow

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.

{
  "mcp": {
    "outlook": {
      "type": "local",
      "command": [
        "ssh",
        "remoteuser@wsl-host",
        "/home/remoteuser/app/outlook/.venv/bin/python3",
        "/home/remoteuser/app/outlook/outlookmcp.py"
      ],
      "enabled": true
    }
  }
}

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.

MCP mail tools

outlook_info, list_folders, list_mails, get_mail, search_mails, search, move_mail, delete_mail, save_sender and save_attachments.

MCP composition tools

create_mail, forward_mail, reply, reply_all and send_mail. Composition operations create drafts; sending is deliberately exposed as a separate tool.

MCP calendar tools

list_calendar, get_calendar, reply_to_invitation, create_calendar_entry, move_calendar_entry and delete_calendar_entry.

Essential excerpt · MCP adapter and transport

from mcp.server.mcpserver import MCPServer

HOST = os.environ.get("OUTLOOK_MCP_HOST", "<WINDOWS_WSL_INTERFACE_IP>")
PORT = int(os.environ.get("OUTLOOK_MCP_PORT", "<OUTLOOK_BRIDGE_PORT>"))

mcp = MCPServer("outlook")

def _call(cmd: str, **params):
    request = {"cmd": cmd, **params}
    with socket.create_connection((HOST, PORT), timeout=30) as sock:
        sock.sendall((json.dumps(request) + "\n").encode("utf-8"))
        # receive one JSON-line response ...
    # validate {"ok": ...} and return response["data"]

Essential excerpt · MCP tools map directly to bridge commands

@mcp.tool()
def get_mail(entry_id: str) -> dict:
    return _call("get_mail", entry_id=entry_id)

@mcp.tool()
def create_mail(to: list[str], subject: str, body: str,
                cc: list[str] | None = None) -> dict:
    return _call("create_mail", to=to, cc=cc or [],
                 subject=subject, body=body)

@mcp.tool()
def send_mail(entry_id: str) -> dict:
    return _call("send_mail", entry_id=entry_id)

Essential excerpt · combined natural-language-friendly search

@mcp.tool()
def search(folder="inbox", sender=None, recipient=None,
           subject=None, date_from=None, date_to=None, limit=30):
    return _call("search", folder=folder,
                 sender=sender, recipient=recipient, subject=subject,
                 date_from=date_from, date_to=date_to, limit=limit)

Essential excerpt · stateless JSON-lines bridge

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

RUBY_EXE="/mnt/c/Program Files/Ruby34-x64/bin/ruby.exe"
HOST="<WINDOWS_WSL_INTERFACE_IP>"
PORT="<OUTLOOK_BRIDGE_PORT>"

nohup "$RUBY_EXE" outlookservermcp.rb > "$LOGFILE" 2>&1 &
echo $! > "$PIDFILE"
disown

timeout 3 bash -c "echo > /dev/tcp/$HOST/$PORT"

Example requests on the bridge protocol

{"cmd":"list_mails","folder":"inbox","limit":10,"unread_only":true}
{"cmd":"search","folder":"inbox","subject":"<SEARCH_TERM>","date_from":"<YYYY-MM-DD>","date_to":"<YYYY-MM-DD>"}
{"cmd":"get_mail","entry_id":"<OUTLOOK_ENTRY_ID>"}
{"cmd":"create_mail","to":["<RECIPIENT>"],"subject":"<SUBJECT>","body":"<BODY>"}
{"cmd":"send_mail","entry_id":"<OUTLOOK_ENTRY_ID>"}
{"cmd":"list_calendar","from":"<YYYY-MM-DD>","to":"<YYYY-MM-DD>"}

Operational characteristics

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
  • Ollama local runtime and API: docs.ollama.com
  • yt-dlp: github.com/yt-dlp/yt-dlp
  • FFmpeg / ffprobe: ffmpeg.org

Legal notice