LLAMACPP_* keys lived under a generic General group. Added a conditional
'@[AI_PROVIDER=llamacpp] llamacpp' caption (mirrors OpenRouter), so the
provider gets its own section; General now only holds AI_SYSTEM_PROMPT
plus the HF keys that arrive from pos-ai-hf. AI_PROVIDER description and
usage text mention llamacpp.
Also: inactive config groups no longer dim the key name — keys stay bold
so an inactive provider block is not one uniform grey wall; only values
and descriptions dim, and the caption still carries the inactive reason.
Service manager (start/stop/status/models/logs) with systemd user
service generation, GPU auto-detection, model selection from pos ai hf
downloads. Provider adapter integrates with pos ai ask as --provider
llamacpp. Config extends existing ai scope with LLAMACPP_* keys.
87 test cases / 0 failed. make gen/check/lint 0 FAIL / 0 WARN.
menu_ask_value used line-oriented read -rp: a multiline Ctrl+V paste
flooded the tty queue, read consumed only the first line, and the rest
executed as commands later (or were eaten by a later prompt).
- lib/menu-lib.sh: new menu_read_value() raw-mode bracketed-paste
reader (stty -icanon -echo -isig, \e[?2004h/l, literal newlines inside
[200~..[201~, Enter submits outside paste, edit keys, cancel on
Ctrl-D-empty/Ctrl-C/Z/\, terminal restored via trap). Bytes via
dd|od|tr chunks, not bash read: read self-interrupts on ETX from a
tty even with ISIG disabled.
- bin/pos-ai-alias: prompt encode/decode (backslash, newline) with
load/save wiring; newline-safe truncate; edit wizard Enter keeps the
full original prompt (no more silent >80-char truncation).
Verified via pty harnesses: multiline + single-line paste captured
verbatim with nothing executed, Ctrl-D/Ctrl-C cancel cleanly, full
create/list/show/edit E2E, round-trips byte-exact. Gates: make gen &&
make check, make lint 0 FAIL 0 WARN.
usb_detect offered a Ventoy stick's 32M VTOYEFI ESP as a sync/backup
target: with the data partition unmounted it was the only mounted
candidate, and cp died mid-copy with 'No space left on device'.
Detection now reads FSTYPE/PARTTYPENAME and drops EFI system
partitions from both the mounted list and the mount-offer list;
USB_MOUNTED entries carry mp|label|size|model|fs and usb_pick_root
shows that in the single-stick confirm and the multi-stick/partition
picker (USB_ROOT stays a bare mountpoint). pos-media-sync pre-flights
the exact payload size vs df free space (err, or warn under --dry-run)
before any copy.
The single-timer threshold monitor generalizes into a scheduler: each job is a
chmod-600 file in schedule.d/<name>.env (INTERVAL 5m..59m/1h..23h/hourly/daily/
weekly/OnCalendar=..., NOTIFY policy, optional MSG, RULE for threshold,
COMMAND = literal rest of line) with its own systemd user timer pair
(pos-schedule-<name>.timer + oneshot .service, Persistent, reconciled on
enable/disable — orphan units + the legacy pos-event-trigger timer
auto-removed). Policies: always (full output every run), onchange (diff vs
last run, first run sends), onerror (non-zero exit or empty output),
threshold (old event-trigger behavior: first numeric vs RULE, alert on
false→true + recovery, per-job firing state), never (silent side-effect jobs).
run [name|all], list, config (interactive add/edit/remove/enable/disable with
validation), enable/disable [name|all], status, migrate (converts legacy
event.env rules → rule-N.env threshold jobs, verbatim LHS as COMMAND, adopts
the legacy timer's OnCalendar or 5m, removes the old timer). Per-run logs +
state in ~/.local/share/linux_post_install/schedule/{logs,state}/.
config/event.env + event-rules.template → config/schedule.d/ starter jobs
(nvme-health, cpu-temp, disk-root, silent log-cleanup); postinstall installs
them no-clobber into an empty schedule.d/ (legacy event.env users get a
migrate hint instead). bin/pos EXAMPLES + INTERACTIVE_CMDS
(system-schedule config) updated; install.sh ships the renamed tool+lib.
eventer_status counted RULES but never populated it (eventer_read_rules
was only called by run/config), so status always said 'rules: 0' even
with rules present. Call eventer_read_rules before counting.
Each line of event.env is an independent rule: ["msg" if ] <check> <op> <thr>.
Check runs on every pass; first numeric output compared float-safe; op is the
rightmost 'op threshold' pair so checks with their own >/< parse fine. Alerts
once on false->true + one recovery message on true->false (no repeats while
the condition holds); per-rule state keyed by rule-line hash in
~/.local/share/linux_post_install/eventer/state/.
Subcommands: run (timer entrypoint), config (interactive add/remove/edit with
check-validation), list (rules + live values), enable [interval] (systemd
user timer pos-event-trigger.timer; 5m..weekly or OnCalendar; graceful without
a user manager, loginctl enable-linger attempt), disable, status. --dry-run
honors the DEV.md convention. Alerts via lib/notify.sh (Telegram default).
New: bin/pos-system-event-trigger, lib/eventer-lib.sh, config/event.env
template (no-clobber via postinstall), install.sh lib install, INTERACTIVE_CMDS
entry. Docs: POS.md system row, HOWTO.md index, howto/event-trigger.md.
make gen && make check green; functional tests cover trigger/recovery/no-repeat,
float+unit parsing, editor add/remove/edit + validation + dry-run, timer
enable/disable/status, dispatcher routing.
cfg_read_secret is invoked via $() inside _cfg_edit_one, so the echo that
advanced the cursor after hidden input landed on the capture pipe instead of
the terminal — every secret value stored with pos config on a real TTY got a
leading newline (e.g. AI_GEMINI_API_KEY="\n<key>"), which neither cfg_value
nor the ai/telegram load_config parsers could read back: the menu showed
'(not set)' and pos ai gemini kept demanding a key.
Fix: send that newline to stderr (>&2). Defense in depth: cfg_write and
write_config_key now strip CR and truncate multi-line pastes, and the readers
(cfg_value, ai + telegram load_config) strip CR. Reproduced and verified on a
pseudo-TTY; piped-stdin tests never caught it because non-TTY input skips the
stty/echo path.
- Drop sender 'config'/'config set' subcommand — redundant with 'pos config
telegram' (same POS_CONFIG registry: masked token input/display, chat-id
validation, chmod 600); sender/listener error hints now point there.
- Listener: load_map now uses a \x1f delimiter, so mapped /commands
containing '|' are no longer truncated at the first pipe.
- entertainment send: forward extra plugin args (previously dropped); pass
'--' before the message so leading '-' plugin output isn't parsed as an
option.
- write_config_key/cfg_write: replace unescaped sed writes with grep-v+append
so values with &, |, \ round-trip safely.
- sync_systemd: daemon-reload after removing timer units.
- config-ui: 'digits' validation accepts negative group/supergroup chat ids.
Under 'set -u', 'local field=$1 key flags desc rest example' leaves the
unassigned locals UNSET rather than empty, so any POS_CONFIG key without
an '::example' hint (e.g. the whole system scope) hit 'example: unbound
variable' and aborted the scope listing. Initialize all locals explicitly.
POS_CONFIG fields gain an optional 'desc::example' value-format hint shown
in the editor ('e.g. weather,5m joke,10m' under ENABLED). Harden
entertainment-lib's plugin_marker/plugin_keys grep|sed pipelines with
'|| true': under set -o pipefail a grep miss exited 1 and set -e aborted
config_keys mid-scan, so installed layouts (/usr/local/bin mixes libs and
plugins) silently lost all plugin keys in pos config.
Add bin/pos-config + lib/config-ui.sh: reads the '# POS_CONFIG:' headers
across tools into a unified runtime config (~/.config/linux_post_install/
<scope>.env, chmod 600). Secret masking with show/hide, digits:/num:/url:
validation, '-' clears, blank keeps; '*plugins' expands entertainment
plugin vars from entertainment-lib.sh. pos config with no scope opens a
picker (scope list when not a TTY); completion offers scopes from the
registry at runtime.
gen-docs.sh now handles category-less tools (pos-config) instead of
emitting pos-config-config; fix set -e/pipefail abort that truncated the
header registry mid-read. install.sh ships config-ui.sh.
The --send flag duplicated the send subcommand — completion suggested both.
Now 'pos communication telegram <TAB>' completes to just 'sender' and
'listener'. send is the single canonical action.
- git mv bin/pos-communication-telegram bin/pos-communication-telegram-sender
- remove --send branch + POS_FLAGS entry; add --markdown to completion
- lib/notify.sh: notify_sender_name() maps platform telegram -> telegram-sender
(notify_send otherwise looks for bin/pos-communication-telegram)
- pos-system-health --send 'sent:' check + pos-entertainment-send use the new
name and the send subcommand
- completion: keys with nested tools but no direct tool complete to the group
suffixes (sender/listener); --type/--parse-mode value completion moved to
the new word positions
- docs updated (POS.md, AGENT_Context, DEV.md, SCRIPTS.md, notify.env,
HOWTO.md, postinstall.sh); removed phantom webhook/log/broadcast subcommands
from howto/communication.md
- spawn() now respects DRY_RUN, install.sh exports it to child phases, and
postinstall.sh wraps every user-home mutation in run() — '--dry-run' no
longer runs apt/install/clone or edits dotfiles for real
- gen-docs.sh chmods regenerated files to 644 (mktemp mv left them at 0600)
- make check now syntax-checks apps/, entertainment/, features/, templates/
- .gitignore protects config/authorized_keys + config/rclone.conf; drop the
tracked empty authorized_keys and the stray 6 MB session file
- pos-system-health --send prints 'sent:' only when a platform sender exists,
otherwise warns on stderr (notify_send is silent-fail)
- standardize sourced libs (no shebang); refresh AGENT_Context/DEV/APPS/SCRIPTS
doc drift: notify.sh in lib lists, pos-health systemd units, tsui, scripts/,
INTERACTIVE_CMDS list, entertainment scheduler (systemd timers only)
- lib/notify.sh: route notify_send to every platform in NOTIFY_PLATFORM
(notify.env, default telegram; comma-separated = send to all). New
platforms need only a bin/pos-communication-<p> sender implementing
'send <value> [--markdown]' (Matrix/Synapse ready)
- pos-communication-telegram: add --markdown as alias for --parse-mode
markdown to match the sender contract
- lib/common.sh: load_system_env() — shared ~/.config/linux_post_install/
system.env for pos-system-* tools (env exported > file > default)
- pos-system-health/backup: load system.env and show effective dynamic
values (NOTIFY_PLATFORM, HEALTH_BACKUP_MAX_AGE_DAYS, BACKUP_SERVICE_ROOTS)
in --help
- config/system.env + config/notify.env templates copied by postinstall
- systemd/pos-health.service: EnvironmentFile for both configs so the
daily digest honors them
- Move autostart.sh from bin/ to features/ so re-installs never reset it
- Add lib/flags.sh (flag_set/clear/is_set/value/list/status) + flag-reader/set/clear CLIs
- install.sh --feature copies features/* to /usr/local/bin with overwrite prompt, sets flags
- postinstall.sh enables autostart.service only when the autostart flag is set
- Document features & flags in README, DEV.md, AGENT_Context_Project.md