diff --git a/AGENT_TODO.md b/AGENT_TODO.md index 201c187..ed4baba 100644 --- a/AGENT_TODO.md +++ b/AGENT_TODO.md @@ -16,6 +16,8 @@ summary (newest last). ## Done +- **2026-08-09** — Fixed `pos config` secret-value corruption: `cfg_read_secret`'s cursor-advance `echo` went to stdout and, since the function is called via `$(...)`, a leading `\n` ended up inside every secret value → the env file got `AI_GEMINI_API_KEY="\n"`, unreadable by `cfg_value`/`load_config` (menu showed `(not set)`, `pos ai gemini` demanded a key). The newline now goes to the terminal (`echo >&2`). Defense in depth: `cfg_write`/`write_config_key` strip CR and truncate multi-line pastes (warn), `cfg_value` and the ai/telegram `load_config`s strip CR on read. Verified on a real PTY (piped tests couldn't reproduce — non-TTY stdin skips the echo path). + - **2026-08-09** — `ai` category — `pos ai gemini` (ask/chat/models) via Google Gemini REST API. `ask` prints only the answer (pipe/script/Telegram-friendly), `chat` is a multi-turn REPL (q/quit/Ctrl+C, `/reset`, empty input re-prompts), `models` lists generateContent-capable ids and flags the default; `--model` override; default `gemini-2.5-flash`. Config scope `ai` (`AI_GEMINI_API_KEY` secret + `AI_GEMINI_MODEL`) in `~/.config/linux_post_install/ai.env`, edited via `pos config ai`; `config/ai.env` template installed no-clobber by postinstall; `ai-gemini` added to `INTERACTIVE_CMDS`. Telegram listener now answers non-command messages starting with `ai ` via `pos ai gemini ask` (owner chat only; error replies carry the `pos config ai` hint) — future intents (reminders) slot in as more case arms in `handle_message`. Docs: POS.md `ai` section + listener bridge, howto/ai.md, HOWTO/README index rows, `bin/pos` usage example. - **2026-08-09** — Entertainment plugins `gold` + `weather` now emit emoji-visualized Telegram messages. Gold: headline is USD/**gram** (XAU/oz ÷ 31.1034768), ounce as reference, bid/ask, cleaned timestamp (`+00:00`/fractional seconds stripped). Weather: per-WMO-code emoji (☀️/🌙 day-night aware for clear sky), °C + feels-like, humidity, wind with unit spacing. Both verified live; emojis are safe in the default plain send mode. diff --git a/DOC/AGENT_Context_Project.md b/DOC/AGENT_Context_Project.md index 88a3a4b..f0a944c 100644 --- a/DOC/AGENT_Context_Project.md +++ b/DOC/AGENT_Context_Project.md @@ -560,9 +560,9 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:` | `features/autostart.sh` | 14 | Boot-time feature (moved from `bin/`, flag-gated service) | | `bin/pos` | 277 | CLI dispatcher with smart arg matching + logging + category help | -| `bin/pos-ai-gemini` | 197 | Chat with Google Gemini (ask, chat, models) | +| `bin/pos-ai-gemini` | 198 | Chat with Google Gemini (ask, chat, models) | | `bin/pos-communication-telegram-listener` | 526 | Telegram bot listener: map /command → bash, run them on chat messages | -| `bin/pos-communication-telegram-sender` | 220 | Send Telegram messages/files/links/stickers via Bot API (send, test) | +| `bin/pos-communication-telegram-sender` | 221 | Send Telegram messages/files/links/stickers via Bot API (send, test) | | `bin/pos-config` | 80 | Interactive editor for the tools' runtime config (reads # POS_CONFIG: registry) | | `bin/pos-docker-compose` | 366 | Docker Compose service manager (ls/up/down/restart/logs/update/config) | | `bin/pos-docker-health` | 110 | One-glance container health dashboard (exits 1 if unhealthy) | diff --git a/bin/pos-ai-gemini b/bin/pos-ai-gemini index f079f50..d8ec724 100755 --- a/bin/pos-ai-gemini +++ b/bin/pos-ai-gemini @@ -52,6 +52,7 @@ load_config() { \#*) continue ;; esac v="${v%\"}"; v="${v#\"}"; v="${v%\'}"; v="${v#\'}" + v="${v//$'\r'/}" if [ -z "${!k:-}" ]; then export "$k"="$v" fi diff --git a/bin/pos-communication-telegram-sender b/bin/pos-communication-telegram-sender index e76be6c..83d1284 100755 --- a/bin/pos-communication-telegram-sender +++ b/bin/pos-communication-telegram-sender @@ -67,6 +67,7 @@ load_config() { \#*) continue ;; esac v="${v%\"}"; v="${v#\"}"; v="${v%\'}"; v="${v#\'}" + v="${v//$'\r'/}" if [ -z "${!k:-}" ]; then export "$k"="$v" fi diff --git a/lib/config-ui.sh b/lib/config-ui.sh index a69ef57..2f77eb6 100644 --- a/lib/config-ui.sh +++ b/lib/config-ui.sh @@ -171,6 +171,7 @@ cfg_value() { local file="$1" key="$2" v [ -f "$file" ] || return 0 v="$(sed -n "s|^${key}=||p" "$file" | tail -1)" + v="${v//$'\r'/}" v="${v%\"}"; v="${v#\"}"; v="${v%\'}"; v="${v#\'}" printf '%s' "$v" } @@ -189,6 +190,11 @@ cfg_write() { chmod 600 "$file" return 0 fi + val="${val//$'\r'/}" + if [[ "$val" == *$'\n'* ]]; then + val="${val%%$'\n'*}" + warn "multi-line paste — using first line only" + fi tmp="$(mktemp)" grep -v "^${key}=" "$file" 2>/dev/null >"$tmp" || true printf '%s="%s"\n' "$key" "$val" >>"$tmp" @@ -230,6 +236,9 @@ cfg_validate() { # Read a value with echo off (secret flags). Falls back to plain read when # stdin is not a TTY (e.g. piped menu input). +# The cursor-advancing newline after the hidden input MUST go to the terminal +# (>&2), not stdout — cfg_read_secret is called via $(), so anything on stdout +# is captured into the value; a stray leading newline corrupted ai.env. cfg_read_secret() { local val echo_off=0 if [ -t 0 ]; then @@ -238,7 +247,7 @@ cfg_read_secret() { read -r val || true if [ "$echo_off" -eq 1 ]; then stty echo 2>/dev/null || true - echo + echo >&2 fi printf '%s' "$val" } diff --git a/lib/entertainment-lib.sh b/lib/entertainment-lib.sh index 68a7c76..acbb2ec 100644 --- a/lib/entertainment-lib.sh +++ b/lib/entertainment-lib.sh @@ -25,6 +25,8 @@ config_value() { write_config_key() { local key="$1" val="$2" tmp + val="${val//$'\r'/}" + val="${val%%$'\n'*}" mkdir -p "$CONFIG_DIR" tmp="$(mktemp)" grep -v "^${key}=" "$CONFIG_FILE" 2>/dev/null >"$tmp" || true