Files
Linux_post_install/DOC/POS.md
T
Your Name 0b5043a9f3
gates / consistency-and-conventions (push) Successful in 26s
fix: llama-server start breakage — version detection, flag-validation race, model dir resolution, user-bus pre-flight
User report after the llamacpp app install: 'installed llama.cpp unknown',
valid flags rejected (randomly per run), 'Model not found' for the HF
downloader's own layout, and a systemd user-bus failure over SSH. Detective
(real b10822 binary, FACT) found four independent causes:

- version: llama-server --version prints to STDERR; detect_llama_version's
  2>/dev/null swallowed it -> always 'unknown'. Now captures 2>&1 + accepts
  semver/build tokens (incl. build 1.2.3 edge)
- validation: printf|grep -q under pipefail -> SIGPIPE rc=141 race randomly
  rejected flags present in the 59 KB --help. Now pipe-less grep (no race);
  20x determinism regression test
- model resolution: resolve_model accepted files only, but the HF downloader
  creates <models>/<repo>/file.gguf dirs. Now expands a dir with exactly one
  *.gguf (never silently picks; multi-gguf lists + errs)
- port: llama.cpp default 8080 vs tool/adapter 8088; validation reliability
  means --port is now always pinned in the unit
- user bus: headless/SSH sessions lack XDG_RUNTIME_DIR -> ensure_user_bus in
  lib/common.sh pre-flights all three systemctl --user tools with remediation
  text; pos ai server --no-unit direct-run escape hatch (pidfile) for boxes
  with no bus
- find_llamacpp narrowed to llama-server/llama-server-cuda (bare 'server'
  fallback hazard); installer post-install sanity (version+help execute,
  symlink targets resolve)

Architect decisions DQ1-DQ6 recorded. Tester: 4 new regression files
(version-from-stderr, 25x flag-validation determinism, model dir expansion,
bus pre-flight + E2E) + 3 fixture updates; suite 16 files / 269 checks.

Verified: make gen idempotent; make check OK; make lint 0 FAIL, 0 WARN;
make test 269/269 (~49s); bash -n clean; git diff --check clean.
2026-09-06 09:25:52 -04:00

70 KiB
Raw Blame History

pos CLI Reference

pos is the unified command-line interface installed to /usr/local/bin/. Every tool is a small script in bin/ with a pos-<category>-<command> name. This document explains the dispatcher and every command.


The dispatcher — bin/pos

Purpose: turn pos <category> <command> [args] into a call to the matching pos-* script.

How it works

pos scans its own directory for executable pos-* files and tries variable-length argument matching, longest first. For pos docker compose up jellyfin:

tries pos-docker-compose-up-jellyfin   (not found)
tries pos-docker-compose-up            (not found)
tries pos-docker-compose               (found) → runs with args "up jellyfin"

pos help <full command> runs that tool's --help (e.g. pos help communication telegram, pos help docker compose — the words are joined with dashes). pos <category> and pos <category> --help list that category's subcommands (derived from bin/pos-<category>-* filenames, no script execution). Running pos with no args prints the built-in usage text (which doubles as the category cheat-sheet).


Logging behavior

Every non-interactive pos invocation logs to ~/.local/share/linux_post_install/logs/:

  • Per-command files: YYYYMMDD_HHMMSS_pos_<args>.log (full stdout + stderr).
  • pos.log: one line per invocation — command, log file, exit code.
  • Interactive commands (pos system firewall, pos media mp4, pos system backup) only log the invocation, not their output.

Commands

Category-less tools (config, tree) live outside any category and are documented in their own ### sections below.

ai

File: bin/pos-ai (provider-agnostic main tool), bin/pos-ai-gemini / bin/pos-ai-openrouter / bin/pos-ai-llamacpp (backward-compat forwarders → pos ai --provider <name>), bin/pos-ai-hf (Hugging Face model downloader), bin/pos-ai-server (llama.cpp inference server manager) Provider adapters: lib/ai-providers/gemini.sh, lib/ai-providers/openrouter.sh, lib/ai-providers/llamacpp.sh Purpose: AI assistant with pluggable providers. Six subcommands: ask (scriptable, persistent session), capture (run a command and save its output for --last), chat (interactive multi-turn REPL), models (list available models), providers (list providers and config status), and sessions (list/clear sessions). Providers handle API-specific logic; the main tool handles sessions, rendering, machine context, and all shared logic.

Command Behavior
pos ai ask "<prompt>" Sends the prompt to the active provider (default: gemini) and prints the answer text to stdout. The prompt may also be piped in via stdin when no argument is given. Runs in the persistent default session (~/.local/share/linux_post_install/ai/default.json, capped at 40 turns; --session <name> picks another). Terse by default: a built-in system instruction asks for commands-first minimal prose and to diagnose pasted errors/output with the fix first (--system "<text>" replaces it wholesale, --full skips it; AI_SYSTEM_PROMPT env/config provides a custom default). With --last, the output of the most recent logged pos command or captured output (tail, max 4096 chars) is appended to the question. On a tty the answer is rendered as markdown (glow if installed, else a built-in renderer); non-tty stdout gets the raw markdown bytes unchanged
pos ai --provider openrouter ask "<prompt>" Same, but uses OpenRouter instead of the default Gemini provider
pos ai capture <cmd..> Run a command, capture its stdout+stderr to screen and to ~/.local/share/linux_post_install/last_cmd_output for --last. Each capture overwrites the previous one. Returns the command's exit code
pos ai chat Interactive REPL with multi-turn history (the messages[] array is appended per turn and persisted to the session file — default unless --session); replies are rendered like ask on a tty; q/quit/exit or Ctrl+C quit, /reset clears the history, empty input re-prompts
pos ai sessions Lists session files with turn counts; sessions reset <name> clears one (e.g. reset default)
pos ai models Lists available models for the active provider and flags the configured default
pos ai providers Lists available providers, their config status, and the active provider
pos ai --model <id> … Overrides the model for one invocation
pos ai --provider <name> … Selects the provider for one invocation (gemini|openrouter|llamacpp)
pos ai alias Interactive alias manager (bin/pos-ai-alias): menu loop (create / edit / remove / list) that shows the alias table (Name/Provider/Session/Prompt, prompts truncated) between picks
pos ai alias create [name] Interactive 4-step wizard: alias name (leading letter, then letters/digits/-/_; unique across aliases), provider pick (from installed lib/ai-providers/*.sh adapters), session name (defaults to the alias name), optional system prompt (must not contain |; warns above 500 chars); confirm defaults to yes, then the alias is saved
pos ai alias edit [name] Edits an existing alias (pick from list or pass the name): provider/session/prompt are re-prompted pre-filled with the current values — Enter keeps the current value; a per-field changed/unchanged summary is confirmed (default yes) before saving; nothing is written if nothing changed
pos ai alias remove [name] Removes an alias (pick from list or pass the name); the confirmation defaults to no and removal cannot be undone
pos ai alias list Non-interactive: prints all aliases as a Name/Provider/Session/Prompt table (prompts truncated at 42 chars)
pos ai alias show <name> Prints one alias's details including the wrapper path and the resolved command: pos ai <provider> ask --session <session>[ --system '<prompt>']

Alias storage & activation: records live in ~/.config/linux_post_install/ai-aliases.env — one name\|provider\|session\|system_prompt line per alias, chmod 600, managed by the tool (do not hand-edit); an empty session falls back to the alias name. Activation needs no shell sourcing: every pos ai alias invocation syncs the ENV file (the single source of truth) against executable wrapper scripts at ~/.local/bin/<name> (chmod 755) — missing or changed wrappers are atomically rewritten, wrappers pos owns but ENV no longer lists are deleted, and hand-edited wrappers are healed. A wrapper re-reads its bytes on every run, so an edit is live on the next invocation (no reload), and the scripts work identically in interactive shells, scripts, cron, and non-login ssh sessions (~/.local/bin must stay on PATH — a loud warning with a copy-paste fix appears when it isn't). Create refuses name collisions: a foreign file at ~/.local/bin/<name> and names resolving to another binary on PATH are never overwritten. The legacy generated ~/.config/linux_post_install/ai-aliases.sh is no longer written; on the next invocation pos removes it automatically (marker-guarded — a foreign-content file is left untouched with a warning) and prints an unalias <names> remediation hint for already-running shells (or simply start a new shell).

Backward compatibility: pos ai gemini, pos ai openrouter, and pos ai llamacpp still work as shorthands for pos ai --provider gemini, pos ai --provider openrouter, and pos ai --provider llamacpp.

pos ai with no subcommand prints usage (never blocks on stdin). ask/chat time out after 60s per request; on a non-2xx response the API's error.message is shown and the tool exits non-zero.

Configuration (~/.config/linux_post_install/ai.env, edit with pos config ai):

Key Required Default Purpose
AI_PROVIDER no gemini Active provider (gemini|openrouter|llamacpp)
AI_API_KEY yes API key for the active provider (secret — masked in pos config ai)
AI_MODEL no per provider Model id used by ask/chat/models
AI_SYSTEM_PROMPT no built-in terse prompt Custom system prompt (overrides built-in; empty to reset)
AI_GEMINI_API_KEY fallback Legacy: Gemini API key (used when AI_API_KEY is empty)
AI_GEMINI_MODEL fallback gemini-2.5-flash Legacy: Gemini model id (used when AI_MODEL is empty)
OPENROUTER_API_KEY fallback Legacy: OpenRouter API key (used when AI_API_KEY is empty)
OPENROUTER_MODEL fallback openrouter/auto Legacy: OpenRouter model id (used when AI_MODEL is empty)

Model precedence: --model flag > AI_MODEL env > provider-specific fallback (AI_GEMINI_MODEL/OPENROUTER_MODEL) > provider default. API key precedence: AI_API_KEY env > provider-specific fallback (AI_GEMINI_API_KEY/OPENROUTER_API_KEY) > error. postinstall.sh copies the repo's config/ai.env template to ~/.config/linux_post_install/ai.env on install (no clobber). Dependencies: curl + jq (both in preinstall.sh PACKAGES). Sessions are stored in OpenAI messages format universally; old Gemini-format sessions (contents[]) are auto-migrated on load.

Messaging bridges: the Telegram and Matrix listeners forward non-command messages starting with ai (case-insensitive) to pos ai ask and reply with the model's answer — see communication → listener. The Telegram bridge uses one session per chat (telegram-<chat id>), the Matrix bridge one per room (matrix-<room>).

Command execution posture: when an ask/chat answer contains a sh/shell fenced code block, pos ai offers to run it and the default is deny. On an interactive terminal it prompts Run this command? [y/N] — only an explicit y/Y runs it; anything else (including Enter) declines and adds the command to shell history. Without an interactive tty (pipes, scripts, cron, chat bridges) commands are never executed — the block is neither printed nor run. --trust auto-executes without the confirmation prompt, but only on an interactive terminal (no effect in a non-tty/bridge context). --no-command-execution disables execution entirely: the detected command is neither printed nor run and no prompt appears — this is the structural guard the chat bridges rely on so a future refactor cannot auto-execute. When both --trust and --no-command-execution are passed, the last one on the command line wins.

pos ai hf — Hugging Face model downloader:

Command Behavior
pos ai hf search <query> Search Hugging Face models by query (sorted by downloads); prints model ID, download count
pos ai hf download <repo-id> [filename] Download a file or entire repo from Hugging Face. Creates <namespace>-<model-name>/ under HF_DOWNLOAD_DIR (default ~/.local/share/linux_post_install/ai/models/). Options: --branch <rev> (specific branch/revision; alias --revision, when both are given the later one wins), --gguf (only .gguf weight files; lists recursively and excludes mmproj/imatrix/vision/MTP artifacts), --quant <dir> (with --gguf: pick one quant directory when a repo groups weights into several, e.g. --gguf --quant Q8_0), --include <pattern> / --exclude <pattern> (glob filters applied after the gguf/filename filter, in the order gguf → include → exclude, e.g. --include "*.gguf" --exclude "*Q4_*"), --list (list remote repository files without downloading — shows exactly what download would fetch), --output <dir> (override download dir). A filename may be a full path (Q8_0/model.gguf) or a bare name (model.gguf) — bare names matching files in multiple directories error and ask for the full path. Progress bars to stderr; summary with path and size to stdout. Writes .hf-meta JSON (repo-id, branch, files, timestamp) for list and remove
pos ai hf list List all downloaded models with size and date
pos ai hf remove <repo-id> Remove a downloaded model directory and show freed space
pos ai hf cache [status|clear] status shows the cache directory, model count and total on-disk size of all downloaded models; clear lists the downloaded models, asks for confirmation (destructive default n) and removes them, printing the freed space

Auth: HF_TOKEN in ~/.config/linux_post_install/ai.env (same scope as pos ai; edit via pos config ai). Even for public repos, a token increases rate limits from 500/5min to 1000/5min. Resume: curl -C - resumes interrupted downloads. Rate limit handling: on HTTP 429, sleeps Retry-After or 60s, retries once.

pos ai server — llama.cpp local inference server manager:

Command Behavior
pos ai server start [model] Generate and start a systemd user service running llama-server. Model resolution: explicit arg > LLAMACPP_MODEL config > interactive pick (TTY only). Auto-detects GPU (CUDA via nvidia-smi); sets --n-gpu-layers accordingly. Writes unit to ~/.config/systemd/user/pos-ai-server.service, runs daemon-reload && enable --now. Warns about linger if needed
pos ai server stop Stop and disable the systemd user service, remove the unit file
pos ai server status Show service state, loaded model (from /v1/models), port, host, GPU, context, threads, autostart, endpoint, and health (from /health)
pos ai server models List .gguf files found in HF_DOWNLOAD_DIR with sizes
pos ai server logs [lines] Show recent server logs via journalctl --user -u pos-ai-server (default 50 lines)

Flags: --port <port> (default 8088), --host <addr> (default 127.0.0.1), --model <path> (overrides arg/config), --ctx <size> (context window, default 4096), --gpu <layers> (-1=auto, 0=CPU, N=explicit, default -1), --threads <n> (default nproc), --gpu-layers/--n-gpu-layers <n> (GPU layers override), --gpu-threads <n>, --tensor-split <n>, --batch-size <n>, --ubatch-size <n>, --temperature <n>, --top-k <n>, --top-p <n>, --repetition-penalty <n>, --mmap, --mlock, --kv-cache <size>, --ctx-size <n>, --metrics, --health, --slots <n>, --no-unit (run the server directly under nohup with a pidfile in $XDG_RUNTIME_DIR//tmp instead of installing a systemd unit — for headless/SSH boxes whose user systemd bus is unreachable; stop/status still work via the pidfile). Without --no-unit, start pre-flights the user systemd bus (ensure_user_bus) and aborts with remediation (export XDG_RUNTIME_DIR=…, sudo loginctl enable-linger …) before writing any unit. Every flag that enters ExecStart is validated against the installed llama.cpp's --help (word-boundary match, version-aware). Flag validation is split by intent: flags the user explicitly requested (via CLI, LLAMACPP_* config, or exported env) that are unsupported cause a hard error naming the flag + detected version; always-emitted default flags (--port, --host, --n-gpu-layers, --ctx-size, --threads) that the user did not request and that are unsupported are omitted from the unit with a single warning (never a hard error, never silently passing an unsupported flag). If --help cannot be read the tool warns and proceeds (all flags accepted). The generated unit and --dry-run contain only flags that passed validation. Config keys in ai.env: LLAMACPP_PORT, LLAMACPP_HOST, LLAMACPP_MODEL, LLAMACPP_CTX_SIZE, LLAMACPP_GPU_LAYERS, LLAMACPP_THREADS. Requires curl + jq and a llama-server binary on PATH (install via bash apps/install.sh llamacpp).

network

Command File Purpose Configuration
pos network ip bin/pos-network-ip Show interfaces, default route, public IP + location None. Public IP via https://ifconfig.me; location via ip-api.com (5s timeouts)
pos network checkport <ip:port> bin/pos-network-checkport Check if a TCP port is open None. Uses /dev/tcp with a 2s timeout; exit 0/1 via OPEN/CLOSED
pos network scan <cidr> [--full] [--retries N] bin/pos-network-scan Two-phase nmap scan See below
pos network hotspot [cmd] bin/pos-network-hotspot Wi-Fi hotspot via create_ap (CLI) or wihotspot-gui (GUI) Uses the precompiled binaries from x64_bin/; see below
pos network download <cmd> bin/pos-network-download aria2 RPC daemon + queue control (add/torrent/metalink, watch, limits) aria2c/jq/curl; daemon = systemd user service; secret in ~/.config/linux_post_install/download.env; see below

pos network scan in detail:

  • Phase 1 — fast host discovery (nmap -sn -T5), prints the live host list.
  • Phase 2 (only with --full) — service/version scan (-sV -sC), plus OS detection and NSE scripts if run with privileges; shows ports, OS, SSH host keys, HTTP titles, NetBIOS/SMB info.
  • Accepts a bare IP (treated as /32) or a CIDR.
  • Auto-raises to sudo nmap when possible (root, passwordless sudo, or an interactive terminal with --full).
  • --retries N tunes discovery retries (default 1).

pos network hotspot in detail:

Backed by the precompiled binaries shipped in x64_bin/ (see SCRIPTS.md → x64_bin/). Needs root for the CLI commands (uses sudo):

Command Behavior
pos network hotspot Launches the wihotspot-gui (GTK3 GUI)
pos network hotspot start <wifi-iface> [<internet-iface>] <ssid> [<passphrase>] Asks whether to run in the background; y starts create_ap --daemon (logs to /var/log/linux_post_install_hotspot.log), n runs in the foreground (blocks until Ctrl+C)
pos network hotspot start --foreground <wifi-iface> [<internet-iface>] <ssid> [<passphrase>] Skips the prompt, runs in the foreground
pos network hotspot stop [<id>] Stops the running access point via create_ap --stop; <id> is an interface name or PID, auto-detected if omitted
pos network hotspot status Runs create_ap --list-running

pos network download in detail:

Runs a persistent aria2c JSON-RPC daemon (localhost:6800) as a systemd user service (pos-aria2.service, enabled with systemctl --user enable --now; prints a linger warning on headless boxes). start installs the unit and generates a random RPC_SECRET into ~/.config/linux_post_install/download.env (chmod 600); the secret is also respected as the RPC_SECRET env var. Unit flags: --continue=true --max-connection-per-server=16 --split=16 --seed-time=0 --dir=$HOME/Downloads.

Command Behavior
pos network download start / stop Install+enable the systemd user service / stop and remove it
pos network download status Daemon health + global transfer stats (getGlobalStat)
pos network download add <url>... Enqueue HTTP/FTP downloads (auto-starts the daemon); options --dir, --out, --split, --tmux
`pos network download torrent <file magnet>...`
`pos network download metalink <file url>...`
pos network download list Table of active / waiting / finished downloads (GID, status, %, dl/up speeds, name)
pos network download info <gid> Full tellStatus dump (status, progress, speeds, ETA, error)
pos network download files <gid> / peers <gid> Files of a download / peers of a torrent
pos network download pause|resume [gid|all] Pause/resume one or all (default all)
pos network download remove [gid|all] Remove one or all; --force = forceRemove (kills immediately)
pos network download purge Clear finished/error history
pos network download move <gid> <pos> Reorder the waiting queue (changePosition)
pos network download limit [gid] <speed> Speed limit, global or per-download (--upload = upload speed; 0 = unlimited); accepts 2M/512K
pos network download set <k=v>... Set global aria2 options (--gid <gid> = per-download)
pos network download watch [gid] Live table, 2 s refresh; with a GID it exits when that download completes
pos network download restart <gid> Re-queue a finished/errored download from history: torrents re-add via magnet (info-hash + trackers), HTTP via their original URLs — --continue=true resumes partial files, a complete file re-verifies instantly. Options --dir, --seed, --split, --tmux
pos network download retry <gid|all> Smart retry of errored downloads: waits out internet outages (poll --interval, give up after --max-wait), then re-queues and re-verifies. Sources failing with aria2 error 3 are marked permanent in ~/.config/linux_post_install/download.retry (id = url:<uri> / bt:<infohash>) and skipped by retry all — manual restart overrides. --once (healer timer mode) skips the wait and exits 0 even on failure; --quiet silences output. Options --dir, --seed, --split, --tmux
pos network download replace <gid> <url> Give a dead download a fresh URL: re-queues with the same dir + file name (so --continue=true resumes the partial), forgets the old dead source from download.retry, and verifies the new link — a dead replacement is diagnosed and marked permanent. status lists downloads needing this. Single-file HTTP/FTP only (torrents: restart); options --dir, --split, --tmux

pos network download menu — bare invocation on a terminal (or the explicit menu subcommand) opens an interactive hub over the top verbs: daemon status, overview (status + queue snapshot), list, add URL (asked via a prompt, with an optional --tmux handover), gid-pick → info / pause / resume / remove / restart (remove behind an explicit y/N confirm naming the download), purge (typed-purge confirm), live watch (Ctrl-C leaves the menu), and daemon start/stop (stop behind a y/N confirm). Queue views are gated on a non-fatal RPC liveness probe — with the daemon down you get a graceful hint and stay in the menu instead of an error exit. Arguments stay scriptable; without a terminal the menu fails closed with a pointer to these subcommands.

--tmux: after enqueueing, add/torrent/metalink open a detached tmux session dl-<name> running watch <gid> (name from --out or the URL basename, sanitized and truncated to 40 chars; -2 suffix on collision). The session closes itself when the download finishes — attach with tmux attach -t dl-<name>.

Outage resilience: watch <gid> auto-restarts its download when the network comes back (it polls NET_PROBE, default timeout 3 bash -c 'exec 3<>/dev/tcp/$1/$2' _ 8.8.8.8 53 — host/port are positional args, never interpolated into the probe's shell source; the NET_PROBE env override is operator-controlled). For unattended machines the retry healer timer (pos-aria2-retry.timer, systemd user scope) runs retry all --once --quiet every 2 min; it arms automatically whenever a download starts (add/torrent/metalink/restart) and disables itself when no active, waiting, or errored downloads remain. When a source is genuinely gone (aria2 error 3, e.g. a 404), the download is marked permanent — status prints a needs fresh link: <name> (<gid>) line and replace <gid> <url> resumes it with a new URL. Both are dry-run aware.

docker

Command File Purpose Configuration
pos docker ps bin/pos-docker-ps Enhanced container list: name, image, health, uptime, IPs, ports, ID, plus a healthy/unhealthy summary None. Requires Docker + Python 3
pos docker health bin/pos-docker-health One-glance health dashboard; exits 1 if any container is unhealthy None. Checks all containers including stopped ones
pos docker stack [-a] bin/pos-docker-stack Containers grouped by compose stack (project); -a includes stopped; non-compose containers under Standalone None. Requires Docker
pos docker compose … bin/pos-docker-compose ScaleTail service manager See Docker Compose / ScaleTail below

pos docker stack [-a|--all] — containers grouped by their Docker Compose project (stack). Each stack is a section (project name, sorted) whose lines show container name, status, and port mappings (- when none — e.g. ScaleTail sidecar containers). Containers with no compose project land in a Standalone section at the end. Default shows running containers; -a|--all includes stopped/exited (like docker ps -a). Status is colored when output goes to a terminal: Up* green, Exited*/Dead*/Created* red, Paused*/Restarting* yellow. Ends with a summary line (Stacks: N containers: N standalone: N); exit 0 even when no containers exist.

Docker Compose / ScaleTail

pos docker compose ls — list available ScaleTail service templates.

pos docker compose installed — list deployed services under $SERVICES_BASE.

pos docker compose up <service> — deploy a service:

  1. If not yet deployed, creates $SERVICES_BASE/<service>/ with config/ and data/, copies the template's compose.yaml.
  2. If no .env exists, copies the template's .env (or writes a default) and fills in your global config values (TS_AUTHKEY, TZ, DNS_SERVER).
  3. If TS_AUTHKEY is still empty, prompts for it.
  4. Offers to edit .env before starting (default yes on first deploy).
  5. Runs docker compose up -d.

pos docker compose down/restart/logs <service> — stop, restart, or tail logs of a deployment.

pos docker compose updategit pull the ScaleTail templates, then refresh the compose.yaml of every deployed service. Per-service .env files are never touched.

pos docker compose config [show] — show the global config file and SERVICES_BASE.

pos docker compose config set KEY=VALUE — set/update a global default in ~/.config/linux_post_install/compose.env.

pos docker compose config edit — open the global config in $EDITOR (creates a default file first).

pos docker compose menu — bare invocation on a terminal (or the explicit menu subcommand) opens an interactive hub wrapping these commands: list templates / deployed stacks (+ status), up (pick a template — first-deploy .env/TS_AUTHKEY prompts included), down/restart (each behind an explicit y/N confirm naming the stack), follow logs (-f; Ctrl-C returns to the menu), update (y/N confirm naming $SERVICES_BASE; .env never touched), and global config show/edit. Arguments stay scriptable; without a terminal the menu fails closed with a pointer to these subcommands.

Configuration (three layers, most specific wins):

Layer File Notes
Template defaults /usr/local/share/linux_post_install/scale-tail/services/<name>/.env Read-only
Global config ~/.config/linux_post_install/compose.env Edited via config set / config edit
Per-service $SERVICES_BASE/<service>/.env Created on first up, never overwritten

Global config keys:

Key Required Default Purpose
TS_AUTHKEY yes Tailscale auth key for the sidecar
TZ no Europe/Amsterdam Service timezone
DNS_SERVER no 9.9.9.9 DNS server
SERVICES_BASE no /srv Deployment root

Docker vbox

File: bin/pos-docker-vbox Purpose: manage disposable Docker containers as lightweight "VMs". Each container gets a bind-mounted host directory so files persist after the container is removed. Containers carry the label linux_post_install.vbox=true.

Command Behavior
pos docker vbox create <name> [image] [--dir <path>]… [--device </dev/node>]… [--gpu] [--port H:C]… [--cpus N] [--memory SIZE] [--network MODE] Creates a container from ubuntu:22.04 (or the given image), bind-mounting ~/<name> (or the first --dir; repeatable for extra same-path mounts) as the working directory; optional flags add GPU (--gpus all), device passthrough, port publishes and cpu/memory limits; prompts to enter immediately
pos docker vbox enter <name> Shell into the container (auto-starts it if stopped); detects the working dir from the container mounts
pos docker vbox start/stop/rm <name> Start, stop, or force-remove the container
pos docker vbox ls List vbox containers only (label filter)

pos docker vbox menu — bare invocation on a terminal (or the explicit menu subcommand) opens an interactive hub wrapping these verbs: list, create (categorized flow: name → category hub with live basket counts — image quick-picks, GPU/Nvidia with automatic toolkit/device-node detection, host devices, dir mounts, ports, CPU/RAM → review screen rendering the exact docker create plan before anything runs; 'n' returns to the hub with edits preserved), enter (hands the terminal to the container shell — exit returns to the menu), start/stop (pick a VM), and remove (y/N confirm naming the VM; rm -f removes the container, the host folder is kept). Arguments stay scriptable; without a terminal the menu fails closed with a pointer to these subcommands.

The standalone vbox command still works and forwards to pos docker vbox (see Legacy wrappers).

media

Command File Purpose Configuration
pos media mp3 <url> bin/pos-media-mp3 Download audio as MP3 via yt-dlp, with thumbnail + metadata Output to ~/Music/%(title)s.%(ext)s, --audio-quality 0
pos media mp4 <url> bin/pos-media-mp4 Download video via yt-dlp with interactive format selection Lists formats (yt-dlp -F), asks for a format ID, saves to ~/Videos/
pos media grab <url> bin/pos-media-grab Auto-download a URL as audio or video (classify + route to mp3/mp4) Domain-based classification (YouTube Music/SoundCloud/Bandcamp → audio; YouTube/Vimeo/Twitch → video); --audio/--video force the mode; --best default for video (non-interactive); prints a clean summary (🎵/🎬 title, path, size). Config: GRAB_DEFAULT (pos config grab, default video) for unknown domains
pos media sync [--mp3|--mp4] bin/pos-media-sync Incremental Music → USB sync (add/update only — never deletes) Copies mp3/mp4 from $HOME/Music (or --source <dir>) into <usb>/Music/, preserving the tree; missing or changed (size/mtime) files are copied, identical ones skipped. Same USB detection as pos system backup (lsblk TRAN + lsusb/by-id, mount offer for unmounted sticks, multi-stick picker). --mp3/--mp4 filter by extension, neither = both; --dry-run previews. Config: MEDIA_SYNC_SOURCE, MEDIA_SYNC_DEST, shared USB_MOUNT_BASE/USB_BYID from ~/.config/linux_post_install/system.env. Result notified via lib/notify.sh. Bare invocation on a terminal (or the menu subcommand) opens an interactive menu wrapping these actions (sync now mp3+mp4, dry-run preview, mp3-only, mp4-only, change source folder); flags stay scriptable
pos media ytsync [add|sync|list|remove] bin/pos-media-ytsync Incremental YouTube channel/playlist sync — first run asks for a URL (bare invocation = interactive menu; empty state goes straight to the prompt), repeat runs fetch only new videos One yt-dlp call per new video (bestvideo*+bestaudio/best → MP4, metadata/chapters/thumbnail embedded, --no-overwrites, --windows-filenames --trim-filenames 120); per-source --download-archive (~/.local/share/linux_post_install/ytsync/archive/<slug>.txt) makes runs crash-safe and idempotent; registry tracks slug/type/url/subdir. Verbs never prompt (scheduler/timer safe); non-tty interactive entry prints a guard line and exits 0. --dry-run probes + plans with zero writes. Notify digest only when new>0 or failed>0 via lib/notify.sh. Config: YTSYNC_VIDEOS_DIR, YTSYNC_EXTRA_ARGS via pos config ytsync; automate with pos system schedule (COMMAND=pos media ytsync sync, NOTIFY=never)

A watch link carrying both ?v= and &list= downloads only that single video (--no-playlist) — nobody accidentally backfills a 500-video playlist from a watch link; youtu.be/<id> short links count as watch links too. A pure playlist link becomes a tracked playlist source with numbered <NNN> - <title>.mp4 files. pos media ytsync remove <name> stops tracking but keeps the downloaded files AND the archive — re-adding the same source later resumes incrementally instead of re-downloading. Members-only/age-gated videos are reported as "N videos require sign-in — skipped" (escape hatch: YTSYNC_EXTRA_ARGS="--cookies …" in ytsync.env). Research details: tools-docs/ytsync.md.

system

Command File Purpose Configuration
sudo pos system firewall bin/pos-system-firewall Interactive UFW ("UFW POWER") menu: add/delete rules, status, enable/disable/reset, default policies Must run as root. Every command is previewed and confirmed before execution; supports --dry-run; keeps a history of executed commands. Executed mutating changes are announced via lib/notify.sh
pos system backup <folder-path> bin/pos-system-backup Create a gpg-encrypted (AES-256) tar.gz snapshot of a folder and verify it Prompts twice for a password (never stored; the passphrase is fed to gpg on an internal fd — --passphrase-fd, never via argv, so it cannot leak through ps). Uses sudo tar; needs gnupg (in preinstall.sh PACKAGES) only when encrypting. Artifact <name>_<date>.tar.gz[.gpg] in the current directory, chmod 600; --no-encrypt (or BACKUP_ENCRYPT=0) keeps a plain .tar.gz with no password prompt (headless/cron safe). Once the archive verifies, connected USB storage is offered (detected via lsblk TRAN with lsusb/by-id cross-check; unmounted sticks get a mount offer first — see DOC/howto/system.md; or pinned with BACKUP_USB_ROOT): the copy lands in <usb>/backups/ and is proven 100% by sha256 before it is announced. Success/failure are announced via lib/notify.sh. Bare invocation on a terminal (or the menu subcommand) opens an interactive menu wrapping these modes (encrypted backup with typed folder, encrypted backup picked from the service roots, unencrypted variant) — each runs only after an explicit y/N confirm naming the folder; arguments stay scriptable
pos system backup --service bin/pos-system-backup Lists folders under /srv and ~/srv, lets you pick one, then runs the same backup Roots via BACKUP_SERVICE_ROOTS (space-separated, default /srv $HOME/srv) or ~/.config/linux_post_install/system.env
pos system health bin/pos-system-health Host health dashboard: disk per mount, RAM/swap, failed systemd units, backup age, fail2ban, docker containers. Exits 1 if any check FAILs Console-only reporter — health itself never sends notifications; forward the output with a wrapper (e.g. the Telegram/Matrix listener map /status=pos system health) or schedule it via pos system schedule with a NOTIFY policy. HEALTH_BACKUP_MAX_AGE_DAYS (default 2) and BACKUP_SERVICE_ROOTS come from ~/.config/linux_post_install/system.env; --help shows the effective values
pos system schedule <cmd> bin/pos-system-schedule Scheduled jobs — run a command on a timer, notify (or stay silent): run [name|all], list, config, enable [name|all], disable [name|all], status, migrate. Each job is a file in ~/.config/linux_post_install/schedule.d/<name>.env with INTERVAL (5m…59m, 1h…23h, hourly, daily, weekly, OnCalendar=…), NOTIFY policy, optional MSG, RULE (threshold only), and COMMAND = the literal rest of the line (pipes/quotes/sudo fine). Policies: always (full output every run), onchange (send when output differs from the last run; first run always sends), onerror (non-zero exit or empty output), threshold (first numeric output vs RULE, alert on false→true + one recovery — the old event-trigger behavior), never (side-effect jobs, no notify) One systemd user timer pair per job (pos-schedule-<name>.timer + oneshot .service, Persistent=true), reconciled on enable/disable; the legacy single pos-event-trigger timer is auto-removed. migrate converts a pre-existing event.env rule set into schedule.d/rule-N.env threshold jobs. config is an interactive editor (add/edit/remove/enable/disable, validates interval + threshold); alerts via lib/notify.sh; --dry-run previews runs/writes/sends; jobs are arbitrary shell commands (chmod 600, same trust model as the Telegram map); starter jobs in config/schedule.d/ auto-installed no-clobber by postinstall. Bare invocation on a terminal (or the menu subcommand) opens an interactive hub over these verbs (list, timer status, run-now, enable, disable, config editor) — a menu run-now asks y/N first and goes through the same run <name> path the systemd timers use
pos system uninstall bin/pos-system-uninstall Safe, interactive uninstaller for the pos toolkit — scans and removes binaries, services, shell integration, config, and data in three tiers Tier 1 (always): binaries in /usr/local/bin/ (pos, pos-*, libs, ai-providers, entertainment plugins, prebuilt, features), systemd services (disable+remove) including runtime-created ~/.config/systemd/user/pos-* user units, ScaleTail templates + feature-flag store under /usr/local/share/linux_post_install/, shell integration in ~/.bashrc (PATH, completion, pos-ai-hook source), completion file. Tier 2 (--config): ~/.config/linux_post_install/ (.env files, schedule.d/, authorized_keys, rclone.conf). Tier 3 (--data): ~/.local/share/linux_post_install/ (ai sessions, logs, captured output). Flags: --yes (skip prompts, tier 1 only), --config (include tier 2), --data (include tier 3). Combine all three for nuclear removal. Git repo is never removed

A scheduled job is the recommended way to run the health dashboard on a timer: a daily job with COMMAND=pos system health and NOTIFY=always sends the dashboard output as the alert — no separate systemd unit needed (the old pos-health.{service,timer} units are gone; a legacy install may still have them failed/leftover — disable and remove them).

ssh

Command File Purpose Configuration
pos ssh load-keys bin/pos-ssh-load-keys Load all ~/.ssh/id_* private keys into the ssh-agent Uses SSH_AUTH_SOCK (default /run/ssh-agent/socket, provided by ssh-agent.service); skips .pub, known_hosts, authorized_keys, config; validates keys before adding

share

Share files and devices over the network (USB over network, NFS, SMB/Samba).

File: bin/pos-share-usb-server Purpose: control the USB Redirector server (usbsrv) — share local USB devices over the network and manage connected clients. Requires usbsrv (manual install from incentivespro.com — not in PACKAGES).

Command Behavior
pos share usb server --ls List host USB devices and connected clients
pos share usb server --ls-shared List shared or in-use devices only
pos share usb server --share [dev-id] [client-id] Share a device and connect it to a client; interactive picker when IDs are omitted (-share + -connect-to CLIENT-DEV)
pos share usb server --unshare [dev-id] Stop sharing a device
pos share usb server --auto-share on|off Toggle automatic sharing of new devices
pos share usb server --callback [addr:port] Create a callback connection to a client
pos share usb server --close-callback [target|all] Close a client callback
pos share usb server --auto-connect on|off [client] Toggle remote auto-connect for a client
pos share usb server --disconnect [dev-id|all] Disconnect a device from its clients
pos share usb server --nickname [dev-id] [nick] Set a device nickname (empty nick removes it)
pos share usb server --timeout [dev-id] [sec] Set device inactivity timeout (0 disables)
pos share usb server --port [num] Set the TCP port (restart server to apply)
pos share usb server --info / --version Show server info / version

Subcommands that need input prompt interactively when args are omitted. Bare invocation (pos share usb server, no args) opens an interactive menu wrapping all of the above — device/client pickers parse the server listing, and when the listing can't be read it is shown raw with manual ID entry as fallback.

Command File Purpose Configuration
pos share nfs server <cmd> bin/pos-share-nfs-server Manage the NFS kernel server: status, share <path> [client], unshare <path>, list, reload, enable, disable Requires nfs-kernel-server (added to preinstall.sh PACKAGES). Exports live in /etc/exports; share is idempotent (replaces any existing line for the path) and runs exportfs -ra. Default client *(rw,sync,no_subtree_check) — the tool warns you to restrict it; help prints Tailscale CGNAT (100.64.0.0/10), WireGuard (10.10.0.0/24) and LAN examples. Mutating commands announce via lib/notify.sh. Bare invocation opens an interactive menu (share/unshare/list/reload/enable/disable) — the share flow picks a folder from mounted candidates, names it, and offers client-spec presets (open/WireGuard/LAN/single-IP); inactive-service and UFW conflicts are surfaced as optional fixes
pos share nfs client <cmd> bin/pos-share-nfs-client Mount and manage NFS shares: mount <server:export> <local-dir>, unmount <local-dir>, list, persist <server:export> <local-dir>, unpersist <local-dir> Requires nfs-common (added to preinstall.sh PACKAGES). persist writes a systemd .mount unit (systemd-escape --path --suffix=mount) with After=network-online.target / Wants=network-online.target — mounts only once all interfaces are up, no fstab edits to break boot — then daemon-reload + enable --now. unpersist stops/disables/removes the unit. mount/persist announce via lib/notify.sh. Bare invocation opens an interactive menu (mount/persist/unmount/unpersist/list) with mountpoint candidates + manual entry; unmount/persist removals are idempotent (already-absent targets are reported, not errors)
pos share smb server <cmd> bin/pos-share-smb-server Manage the Samba server: status, `share [name] [--read-only --guest
pos share smb client <cmd> bin/pos-share-smb-client Mount and manage SMB/CIFS shares: mount <//server/share> <local-dir> [user], unmount <local-dir>, list, persist <//server/share> <local-dir> [user], unpersist <local-dir> Requires cifs-utils (added to preinstall.sh PACKAGES). With a user you are prompted for the Samba password — one-shot mounts use a throwaway chmod-600 credentials file, persist keeps one at /etc/samba/credentials/<name> (chmod 600). persist writes systemd .mount and .automount units (systemd-escape --path --suffix=mount) with _netdev — the automount defers the actual mount until first access, never blocks boot — then daemon-reload + enable --now the automount. unpersist stops/disables/removes both units + credentials. list shows active mounts (findmnt -t cifs) and persistent units (as automount shares aren't mounted until first access, they'd otherwise be invisible). mount/persist announce via lib/notify.sh. Bare invocation opens an interactive menu (enumerate/mount/persist/unmount/unpersist/list): it can enumerate Disk shares via smbclient (empty user = guest try, auth retry on denial), pick a share + mountpoint from candidates (manual entry fallback), and reuses the authenticated account for the mount

communication

Command File Purpose Configuration
pos communication telegram sender send "text" bin/pos-communication-telegram-sender Send a message, link, or media file (auto-detects the type) to a Telegram chat via the Bot API Token + chat ID from ~/.config/linux_post_install/telegram.env (TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID, chmod 600). Precedence: --token/--chat-id flags > env > config file
pos communication telegram listener bin/pos-communication-telegram-listener Telegram bot listener: map /command → bash commands and <prefix> → apps, run them from chat; interactive editor for the map Same telegram.env (the bot is the owner, TELEGRAM_CHAT_ID; commands also require TELEGRAM_OWNER_ID — see the detail block). Map lives in ~/.config/linux_post_install/telegram_commands.env (/cmd=bash command lines); text-prefix app map in telegram_prefixes.env (<word>=command lines) — both chmod 600
pos communication matrix sender send "text" bin/pos-communication-matrix-sender Send a text message (plain or --markdown) to a Matrix room via the client-server API; also login (password → access token) and test Homeserver + room from ~/.config/linux_post_install/matrix.env (MATRIX_HOMESERVER, MATRIX_ACCESS_TOKEN, MATRIX_USER_ID, MATRIX_ROOM_ID, chmod 600, secrets masked by pos config matrix). Precedence: --room flag > env > config file
pos communication matrix listener bin/pos-communication-matrix-listener Matrix listener: map /command → bash commands and run them from room messages; interactive editor for the map Same matrix.env (reacts to MATRIX_USER_ID's own messages only, in the room set by MATRIX_ROOM_ID — unset = fail-closed, no commands run). Map lives in ~/.config/linux_post_install/matrix_commands.env (/cmd=bash command lines, chmod 600)
pos communication scrcpy [cmd] bin/pos-communication-scrcpy Mirror/control an Android device via scrcpy+adb: devices, record, tcpip, connect, push, pull, screenshot, info (bare = mirror) scrcpy.env (SCRCPY_SERIAL, SCRCPY_MAX_SIZE, SCRCPY_MAX_FPS, SCRCPY_BIT_RATE, SCRCPY_FULLSCREEN, SCRCPY_RECORD_DIR, SCRCPY_PUSH_TARGET, SCRCPY_EXTRA_FLAGS) via pos config scrcpy

pos communication telegram sender in detail:

Command Behavior
pos communication telegram sender send "text" POSTs sendMessage to the Bot API (60s timeout); prints [+] message sent to chat <id> or fails with a nonzero exit
pos communication telegram sender send <value> Auto-detects the type when --type is omitted: existing file → file (except .webp → sticker, .gif → animation, images → photo, video/audio/voice extensions → their type), value starting with http:///https:///www.link, otherwise message
pos communication telegram sender send <path> --type file [--caption "…"] Uploads a file as a sendDocument via multipart (document=@path); --caption adds a caption. Path must exist and be readable
pos communication telegram sender send <path> [--caption "…"] Media uploads via their Bot API endpoint: --type photosendPhoto, videosendVideo, audiosendAudio, voicesendVoice, animationsendAnimation, sticker (.webp) → sendSticker (captions not supported for stickers)
pos communication telegram sender send "url" --type link [--no-preview] Sends a link as a message (URLs auto-linkify); --no-preview adds disable_web_page_preview=true
pos communication telegram sender send "text" --parse-mode <mode> Send with Telegram formatting; <mode> is plain (default), markdown, or html (passed as parse_mode to the API — also applies to captions). Markdown/HTML use raw Telegram syntax — unescaped characters may be rejected by the API (400)
pos communication telegram sender send … --token <t> --chat-id <id> One-shot override of token/chat ID
pos communication telegram sender test Sends a canned test message using the current config

send option validation: --caption is only valid with media types (file/photo/video/audio/voice/animation), --no-preview only with --type message/link, and --type only accepts message|file|link|sticker|photo|video|audio|voice|animation. An explicit --type always overrides auto-detection.

The bot token is a secret — it is stored only in ~/.config/linux_post_install/telegram.env and never in the repo. Edit TELEGRAM_BOT_TOKEN / TELEGRAM_CHAT_ID interactively with pos config telegram (masked input + display). Requires network access to api.telegram.org.

pos communication telegram listener in detail:

Command Behavior
pos communication telegram listener Interactive editor for the /command → bash map (add / edit / remove / test / quit); test-runs run bash -n first and may execute the command live
pos communication telegram listener --status Shows service state (running/autostart), config + map file paths, and the mapped commands
pos communication telegram listener --enable Installs + starts a systemd user service (pos-telegram-listener.service); the daemon polls getUpdates and runs mapped commands + text-prefix apps
pos communication telegram listener --disable Stops, disables, and removes the service
pos communication telegram listener --sync-commands Push the mapped /commands to the bot's / menu (setMyCommands) — also run automatically after every map edit, on --enable, and at daemon start
pos communication telegram listener --run Run the polling loop in the foreground (what the service executes)
pos communication telegram listener prefix List the text-prefix map (telegram_prefixes.env: <word>=command lines) plus the built-in Gemini bridge word
pos communication telegram listener prefix <word> Show one mapping, or map <word> to a command with prefix <word> <command...> — non-command messages <word> <text> run the command with <text> appended as ONE quoted argument (e.g. prefix opencode opencode → "opencode check cpu" runs opencode "check cpu"). prefix -r <word> removes. Mapped words shadow the Gemini bridge; the bridge word itself (TELEGRAM_AI_PREFIX, default ai) is set via pos config telegram

The map file is re-read for every message — edits apply without a restart. A command runs only when the message is sent to the owner chat (TELEGRAM_CHAT_ID) BY the owner account (TELEGRAM_OWNER_ID) — both must match, so a forwarded message or an impersonator can't trigger commands; with TELEGRAM_OWNER_ID unset the daemon starts but refuses every command (fail-closed). /help lists mapped commands; an unmapped command replies "Unknown command".

Text-prefix map (telegram_prefixes.env, managed via the prefix verb): for apps, not bash snippets — a non-command message <word> <text> runs the mapped command with <text> appended as ONE quoted argument, e.g. opencode=opencode turns "opencode check cpu" into opencode "check cpu". First match wins (file order), matching is case-insensitive and the word must be space-delimited (bare <word> with no trailing space replies Unknown command). Values are syntax-checked on save; @quiet values suppress the reply; the 120s timeout + empty→OK + exit <rc> reply mirror the command map. Routing order on every non-command message: text-prefix map → AI bridge → /command map → "Unknown command".

AI bridge: non-command text starting with <word> — default ai , configurable via pos config telegramTELEGRAM_AI_PREFIX (a text-prefix entry with the same word shadows it) — is forwarded to Gemini via pos ai gemini ask (case-insensitive, e.g. ai what is Nvidia or BOT what is Nvidia with prefix bot) and the answer is replied verbatim; an AI failure replies the error. Commands run as your user via timeout bash -c "…" (stdout + stderr are replied, truncated to ~3800 chars; empty output → OK), so sudo inside them needs a NOPASSWD rule. A map value prefixed with @quiet runs the command but does NOT reply — for commands that already send their own notification (e.g. pos system backup self-notifies, so /backup=@quiet pos system backup $HOME/Documents avoids a double message). --enable warns if linger is off — the service stops when you log out unless you run sudo loginctl enable-linger $(whoami).

Map entries may carry an optional description shown in the bot's / menu: /cmd::short description=bash command (the description falls back to the bash command, truncated to ~40 chars, when omitted). After every add/edit/remove the command list is pushed to the bot via setMyCommands, so the menu stays in sync; an empty map clears the menu. Telegram only registers lowercase [a-z0-9_] names (132 chars) — commands like /Status or /my-cmd are skipped from the menu with a warning but still resolve when typed.

pos communication matrix sender in detail:

Command Behavior
pos communication matrix sender send "text" PUTs an m.room.message (m.text) to the homeserver's client-server API v3 (60s timeout); prints [+] m.text sent to room <room> or fails with a nonzero exit. Room id/alias is URL-encoded automatically; a unique transaction id (<timestamp>ns) is generated per message
pos communication matrix sender send "text" --markdown Sends with format: org.matrix.custom.html — a best-effort markdown → HTML conversion (**bold**, __bold__, *em*, _em_, `code`, fences``` ``, ~~strike~~, [link](url), headers, list items). Deliberately simple; it never fails the send
pos communication matrix sender send "text" --room <id|alias> One-shot override of the room for this send only (e.g. --room '#ops:example.org')
pos communication matrix sender login --user <@id> Prompts (masked) for the account password, POSTs m.login.password to /login, and saves the returned access_token + user_id to matrix.env
pos communication matrix sender test Sends a canned test message (Test message from pos <timestamp>) using the current config

The access token is a secret — it is stored only in ~/.config/linux_post_install/matrix.env and never in the repo. pos config matrix edits MATRIX_HOMESERVER, MATRIX_ACCESS_TOKEN (masked), MATRIX_USER_ID, MATRIX_ROOM_ID. Requires network access to your homeserver. The sender implements the lib/notify.sh sender contract, so matrix can be added to NOTIFY_PLATFORM for multi-platform alerting.

pos communication matrix listener in detail:

Command Behavior
pos communication matrix listener Interactive editor for the /command → bash map (add / edit / remove / test / quit); test-runs run bash -n first and may execute the command live
pos communication matrix listener --status Shows service state (running/autostart), config + map file paths, and the mapped commands
pos communication matrix listener --enable Installs + starts a systemd user service (pos-matrix-listener.service); the daemon long-polls /sync and runs mapped commands
pos communication matrix listener --disable Stops, disables, and removes the service
pos communication matrix listener --run Run the polling loop in the foreground (what the service executes)

The daemon long-polls /sync (30s timeout, per-sync since token, compact filter that drops presence/account_data/device noise and only requests m.room.message timeline events). It reacts only to messages from MATRIX_USER_ID (your own account — resolved via /account/whoami if unset), and only in the room set by MATRIX_ROOM_ID — without it the daemon starts but runs no commands (fail-closed), so a bot account in many rooms can't be tricked. / and ! prefixes both resolve (!status = /status). /help lists mapped commands; an unmapped command replies "Unknown command". Non-command text starting with ai (case-insensitive, e.g. ai what is Nvidia) is forwarded to Gemini via pos ai gemini ask with a per-room session (matrix-<room>; ai /reset clears it) and the answer is replied verbatim with markdown stripped. Replies are sent as m.text threaded with m.in_reply_to on your message. Commands run as your user via timeout 60 bash -c "…" (stdout + stderr are replied, truncated to ~3800 chars; empty output → OK; non-zero exit is prefixed with exit <rc>), so sudo inside them needs a NOPASSWD rule. A map value prefixed with @quiet runs the command but does NOT reply — for commands that already send their own notification (e.g. pos system backup self-notifies, so /backup=@quiet pos system backup $HOME/Documents avoids a double message). Map lines may carry a /cmd::description=… description. --enable warns if linger is off — the service stops when you log out unless you run sudo loginctl enable-linger $(whoami).

pos communication scrcpy in detail:

Command Behavior
pos communication scrcpy Mirror the device: opens the scrcpy window (needs a display — over ssh use ssh -X). Built from scrcpy.env defaults plus any pass-through scrcpy flags (pos communication scrcpy --turn-screen-off --stay-awake)
pos communication scrcpy --new-display[=…] Mirror to a new virtual display on the phone (no need to mirror the real screen): --new-display (default size/dpi), --new-display=1920x1080, --new-display=1920x1080/420 or --new-display=/240. Also settable persistently via SCRCPY_NEW_DISPLAY
pos communication scrcpy record [file] [--headless] Record a session to an mp4 — default $SCRCPY_RECORD_DIR/<device>_<date>.mp4; --headless adds --no-playback (no window — headless-server friendly)
pos communication scrcpy tcpip [port] adb tcpip <port> (default 5555) — switch the USB device to wireless adb, prints the reconnect command with the detected device IP
pos communication scrcpy connect <ip[:port]> adb connect then mirror over WiFi (-s <ip:port>)
pos communication scrcpy push <local> [remote] adb push — default destination $SCRCPY_PUSH_TARGET (/sdcard/Download, scrcpy's own default)
pos communication scrcpy pull <remote> [local] adb pull — default local dir is the current directory
pos communication scrcpy screenshot [file] adb exec-out screencap -p → a PNG, default $SCRCPY_RECORD_DIR/<device>_<date>.png
pos communication scrcpy info Device model, Android version, SDK, serial (adb shell getprop)

A device must have USB debugging enabled (Developer options) and the phone's "allow USB debugging" dialog accepted on first connect. devices, record --headless, tcpip, connect, push/pull, screenshot, info work without a display; the bare mirror needs one.

Configuration (~/.config/linux_post_install/scrcpy.env, edit with pos config scrcpy):

Key Required Default Purpose
SCRCPY_SERIAL no Default device serial/ip:port (from devices) — passed as -s to adb/scrcpy
SCRCPY_MAX_SIZE no Limit video size, e.g. 1920 (scrcpy --max-size)
SCRCPY_MAX_FPS no Limit frame rate, e.g. 60 (scrcpy --max-fps)
SCRCPY_BIT_RATE no Video bit rate, e.g. 8M (scrcpy --video-bit-rate)
SCRCPY_FULLSCREEN no false true adds --fullscreen
SCRCPY_NEW_DISPLAY no New virtual display on the phone (--new-display): true (default size/dpi), 1920x1080, 1920x1080/420 or /240
SCRCPY_AUDIO no true Forward device audio to the desktop (scrcpy default). false adds --no-audio
SCRCPY_RECORD_DIR no ~/Videos/scrcpy Output dir for record/screenshot defaults
SCRCPY_PUSH_TARGET no /sdcard/Download Default adb push destination
SCRCPY_EXTRA_FLAGS no Extra scrcpy flags appended to every mirror

Requires scrcpy + adb. adb is in preinstall.sh PACKAGES; scrcpy is not — apt rarely ships it on Debian/Ubuntu without contrib/universe (and it's older there anyway), so it installs via the optional app apps/media/scrcpy.sh (GitHub latest release, bundles adb); run it via ./install.sh --apps or directly. The tool errors with that hint when scrcpy is missing.

entertainment

File: bin/pos-entertainment-send (management subcommands: bin/pos-entertainment-config, bin/pos-entertainment-enable, bin/pos-entertainment-disable, bin/pos-entertainment-status) Purpose: run a public-API plugin and send its output via notify_send — the platform follows NOTIFY_PLATFORM (default Telegram, silent-fail when none configured). Plugins are standalone scripts in entertainment/ that fetch a public API and print the message to stdout — that stdout is what gets sent.

Command Behavior
pos entertainment send List available plugins + usage
pos entertainment send <plugin> [--print] [--markdown] [args…] Run the plugin, send its output via notify_send (silent)
pos entertainment send <plugin> --print Print the output locally; do not send
pos entertainment send <plugin> --markdown Send with Markdown parse_mode (via the notify senders)
pos entertainment config Show the config file (~/.config/linux_post_install/entertainment.env)
pos entertainment config get KEY Print one key's current value ((not set) if absent)
pos entertainment config set KEY=VALUE… Set keys (any UPPER_SNAKE key; warns if no installed plugin uses it) and re-sync the schedule
pos entertainment config unset KEY Remove a key from the config file
pos entertainment config ls Declared keys with their current values, aligned
pos entertainment config edit Interactive editor for the scope (via pos config UI)
pos entertainment enable <plugin> [interval] Add plugin to ENABLED + schedule it as a systemd user timer
pos entertainment disable <plugin> Remove plugin from ENABLED + remove its scheduled job
pos entertainment status Enabled plugins (each with interval + last run), installed-but-not-enabled plugins, scheduler + timers

Plugin lookup order: $ENTERTAINMENT_DIR → repo entertainment//usr/local/bin/ (installed by install.sh Phase 2, beside the runner). A plugin name matches the file name with or without the .sh suffix.

Plugins:

Plugin Source API Config
weather Open-Meteo (no API key) ~/.config/linux_post_install/entertainment.env: WEATHER_LAT, WEATHER_LON (required), WEATHER_CITY (optional label)
joke icanhazdadjoke.com (no API key) None
gold goldprice.dev (no API key, anonymous free tier) None

Config auto-install: postinstall.sh copies the repo's config/entertainment.env (a commented template showing each key's syntax) to ~/.config/linux_post_install/entertainment.env on install — but only if you haven't already created your own (no clobber), and prints the template so you can fill in your location. Fill in WEATHER_LAT/WEATHER_LON (and optionally WEATHER_CITY) to enable the weather plugin.

Adding a plugin: drop an executable script in entertainment/ (e.g. myfeed.sh) with a # POS_PLUGIN: <name> marker — the runner lists and validates plugins by this marker, so non-plugin .sh files in the shared /usr/local/bin are ignored. The plugin must be non-interactive and print the message to stdout; errors go to stderr (exit nonzero). Source lib/entertainment-plugin-lib.sh for the standard helpers — plugin_load_config (reads entertainment.env, env precedence), plugin_have <cmd>, plugin_require KEY <desc>, plugin_http_json <url> [--key <jq>] [-H <header>] (curl, 2 retries, timeout) — it never writes to stdout, so the message stays clean. No registration needed. Dependencies beyond curl/jq (both in preinstall.sh PACKAGES) should be guarded with plugin_have.

Declaring config keys (pattern): document every key the plugin reads with one # POS_KEYS: line right after # POS_PLUGIN:KEY, a description, and (required)/(optional):

# POS_PLUGIN: myfeed
# POS_KEYS: MYFEED_URL <feed url> (required)
# POS_KEYS: MYFEED_TAG <filter tag> (optional)

The plugin itself still reads the keys as plain env vars (${MYFEED_URL:-}). The declaration is what pos entertainment config uses to print its Keys section and to decide whether config set warns about an undeclared key — add the line whenever a plugin gets a new config key.

Automation (auto-trigger): enable plugins on a schedule via the ENABLED key in the config — a comma-separated list of plugin, interval pairs:

ENABLED="weather, 5m gold, 1h joke, daily"

pos entertainment enable <plugin> [interval] appends/updates one entry and re-syncs; pos entertainment disable <plugin> removes it; pos entertainment config set ENABLED="…" replaces the whole list. Scheduling uses systemd user timers (requires a reachable user systemd manager):

  • One user timer per enabled plugin (~/.config/systemd/user/pos-entertainment-<plugin>.{service,timer}), running pos entertainment send <plugin> as your user on that schedule (OnCalendar + Persistent=true). pos entertainment enable also tries sudo loginctl enable-linger $USER once so timers fire without login.

The job runs as you, so it reads your $HOME configs (weather location, notify platform) natively — no Environment=HOME= hacks.

Intervals: 5m 10m 15m 30m 45m hourly 2h 6h 12h daily weekly, or a raw OnCalendar=… spec. Default when omitted: daily.

pos entertainment status shows the enabled plugins (each with interval + last run), the installed-but-not-enabled plugins, the scheduler, and each plugin's next fire time (systemctl --user list-timers). Last run is recorded by pos entertainment send on every non---print run (~/.local/share/linux_post_install/entertainment/last/<plugin>); a run that fails while fired by a timer also notifies the configured platforms.

Notes:

  • The runner is headless/timer-friendly — no TTY prompts, exit 0 on success / 1 on failure.
  • Scheduling is per-user for the user who runs enable; if you manage a different machine's user (e.g. via runuser/sudo -u), run the enable/disable commands as that user.
  • The unit template (TimeoutStopSec=5s, Persistent=true, network-online deps) is shared with the system scheduler via lib/user-timers-lib.sh.

flags

Feature-flag management CLIs (see SCRIPTS.md → lib/flags.sh):

Command Purpose
flag-reader List all flags + status (set: <name> / unset: <name>)
flag-reader <name> Check one flag; exit 0 if set, 1 if not
flag-reader --raw <name> Print only the stored value (script-friendly)
flag-set <name> [value] Set a flag, optionally with a value (requires sudo)
flag-clear <name> Unset a flag (requires sudo)

config

File: bin/pos-config

pos config is the interactive editor for the tools' runtime config (see DEV.md and §10 of AGENT_Context). Every tool exposes its configuration by declaring a # POS_CONFIG: header; pos config reads those at runtime — it knows nothing about the variables themselves. Values live in ~/.config/linux_post_install/<scope>.env (chmod 600).

Headers may also declare group captions: @Caption starts a visual group, and @[KEY=v1|v2] Caption makes the group conditional — while KEY's current value matches none of the listed alternatives, the group stays visible but dimmed with a textual reason (— inactive while KEY=…), so row numbering never changes mid-session. Wildcards can be tagged: *providers=<tag> pulls keys from a single AI provider adapter instead of all of them. The listing renders uniformly for every scope (bold title/keys, dim numbers/examples/placeholders, word-wrapped descriptions); at the prompt type a number to edit, r to refresh, or q to quit.

Command Purpose
pos config Scope picker (on a TTY), otherwise the scope list
pos config <scope> Edit that scope's variables (masked secrets, validation, - to clear)
pos config <scope> set KEY=VALUE Set a value non-interactively (each tool's config set form)

tree

File: bin/pos-tree

pos tree prints the pos command tree — every category, command, and subcommand the dispatcher can reach, annotated with each tool's # POS: description. Data is derived live from the bin/pos-* filenames and their # POS_SUBCMDS: headers, so it always matches what pos can actually run.

Command Purpose
pos tree Full command tree
pos tree --depth N Limit nesting depth (1 = root only)

Legacy wrappers

Thin 2-line scripts that exec pos … "$@". All of them still work:

Wrapper Forwards to
wr-ip pos network ip
wr-checkport pos network checkport
wr-scan-ping pos network scan
wr-docker pos docker
wr-compose pos docker compose
wr-ufw pos system firewall
mp3 pos media mp3
mp4 pos media mp4
vbox pos docker vbox
ssh-load-all pos ssh load-keys