- deps guards before -h|--help in docker-health/ps, network-scan, usb-server, media-mp3/mp4 (--dry-run pre-scan kept); system-firewall gains usage()/--help; autostart/usb-automount get flags.sh + template - install.sh: normalize N-M range syntax in --steps - bin/pos: INTERACTIVE_CMDS += docker-compose docker-vbox network-hotspot - common.sh: canonical XDG-aware CONFIG_DIR + DIM color var; notify.sh stderr fallback; ent_plugin_* registry renames (runtime plugin API kept) - docker-compose SCALE_DIR/CONFIG_ENV env seams; ffmpeg in PACKAGES; scrcpy.sh exec bit - docs: health is console-only (--send/--markdown removed), POS.md file refs for config/tree/entertainment, DEV.md no-guard exception, docmap/ filetable regenerated (make gen), hand-maintained line rows bumped - add scripts/lint-conventions.sh gate + Makefile lint target; record all VERIFIED outcomes in MAINTENANCE.md; AGENT_TODO Done entry (2026-08-14) - gates: make gen/check/lint all green (0 FAIL, 0 WARN); bash -n sweep clean; restricted-PATH dep tests + step-matrix dry-runs verified
46 KiB
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-gemini
Purpose: chat with Google Gemini via the REST API (generativelanguage.googleapis.com). One tool, three subcommands: ask (one-shot, scriptable), chat (interactive multi-turn REPL), and models (list generateContent-capable ids).
| Command | Behavior |
|---|---|
pos ai gemini ask "<prompt>" |
One-shot; POSTs generateContent and prints only the answer text to stdout (pipe/script/Telegram-friendly). The prompt may also be piped in via stdin when no argument is given |
pos ai gemini chat |
Interactive REPL with multi-turn history (the contents[] array is appended per turn); q/quit/exit or Ctrl+C quit, /reset clears the history, empty input re-prompts |
pos ai gemini models |
Lists models that support generateContent and flags the configured default |
pos ai gemini --model <id> … |
Overrides the model for one invocation |
pos ai gemini 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_GEMINI_API_KEY |
yes | — | API key from aistudio.google.com (secret — masked in pos config ai) |
AI_GEMINI_MODEL |
no | gemini-2.5-flash |
Model id used by ask/chat/models |
Precedence: --model flag > AI_GEMINI_MODEL env > config file > gemini-2.5-flash. 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).
Messaging bridges: the Telegram and Matrix listeners forward non-command messages starting with ai (case-insensitive) to pos ai gemini 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>).
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 nmapwhen possible (root, passwordless sudo, or an interactive terminal with--full). --retries Ntunes 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 |
--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 '</dev/tcp/8.8.8.8/53'). 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 compose … |
bin/pos-docker-compose |
ScaleTail service manager | See Docker Compose / ScaleTail below |
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:
- If not yet deployed, creates
$SERVICES_BASE/<service>/withconfig/anddata/, copies the template'scompose.yaml. - If no
.envexists, copies the template's.env(or writes a default) and fills in your global config values (TS_AUTHKEY,TZ,DNS_SERVER). - If
TS_AUTHKEYis still empty, prompts for it. - Offers to edit
.envbefore starting (default yes on first deploy). - Runs
docker compose up -d.
pos docker compose down/restart/logs <service> — stop, restart, or tail logs of a deployment.
pos docker compose update — git 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).
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>] |
Creates a container from ubuntu:22.04 (or the given image), bind-mounting ~/<name> (or --dir, or . for cwd) as the working directory; 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) |
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/ |
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). Uses sudo tar; needs gnupg (in preinstall.sh PACKAGES). Artifact <name>_<date>.tar.gz.gpg in the current directory, chmod 600. Once the archive verifies, connected USB storage is offered (auto-detected removable mounts via lsblk, 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 |
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 |
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.
| 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 |
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 |
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 |
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 run them from chat; interactive editor for the map |
Same telegram.env (the bot is the owner, TELEGRAM_CHAT_ID). Map lives in ~/.config/linux_post_install/telegram_commands.env (/cmd=bash command lines, 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; watches MATRIX_ROOM_ID or all joined rooms). 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 photo → sendPhoto, video → sendVideo, audio → sendAudio, voice → sendVoice, animation → sendAnimation, 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 |
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) |
The map file is re-read for every message — edits apply without a restart. The listener only reacts to the owner chat (TELEGRAM_CHAT_ID); anyone else's message is ignored. /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 and the answer is replied verbatim; an AI failure replies the error plus a pos config ai hint. Commands run as your user via timeout 60 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 (1–32 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); a MATRIX_ROOM_ID restricts it to one room, otherwise every joined room is watched. / 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}), runningpos entertainment send <plugin>as your user on that schedule (OnCalendar+Persistent=true).pos entertainment enablealso triessudo loginctl enable-linger $USERonce 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. viarunuser/sudo -u), run theenable/disablecommands as that user. - The unit template (
TimeoutStopSec=5s,Persistent=true, network-online deps) is shared with the system scheduler vialib/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).
| 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 |