Compare commits

..

2 Commits

Author SHA1 Message Date
Your Name a4761df3f6 feat: telegram listener text-prefix map — <word> <text> routes to a mapped app
gates / consistency-and-conventions (push) Successful in 2m3s
Generalizes the Telegram listener with a configurable text-prefix map
(telegram_prefixes.env): any 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".

Routing order per message: text-prefix map → built-in Gemini ai bridge →
/command map → Unknown command. A mapped word shadows the Gemini bridge.

The prefix verb is reworked: bare = list map + bridge word; 'prefix
<word> <cmd...>' = map; 'prefix <word>' = show; 'prefix -r <word>' = remove.
The Gemini trigger word itself is now set via 'pos config telegram'
(TELEGRAM_AI_PREFIX).

Also extracted run_and_reply() to share the /command-map (60s) and
prefix (120s) execution semantics; fixed a latent set -e abort on
invalid templates in prefix_map_set's check_syntax call.

Verified: 27/27 routing-harness assertions, full CLI verb suite,
dispatch smoke, pos config telegram render, bash -n, make gen && make check,
make lint 0 FAIL / 0 WARN, shellcheck -S style (0 new findings).
2026-09-04 08:10:59 -04:00
Your Name e6fa0a4ee9 feat: configurable AI-bridge trigger word for telegram listener
gates / consistency-and-conventions (push) Successful in 1m50s
The listener's "ai " bridge prefix was hard-coded. Messages starting
with <prefix> + space (case-insensitive, literal match) are now
forwarded to Gemini; default stays "ai".

- TELEGRAM_AI_PREFIX in telegram.env (default ai), hot-reloaded per
  message like the command map — no daemon restart needed
- New 'prefix' verb: pos communication telegram listener prefix [word]
  (validated [A-Za-z0-9][A-Za-z0-9_-]*; writes telegram.env chmod 600)
- Field added to the telegram # POS_CONFIG: scope (sender header) so
  'pos config telegram' edits it too
- --status shows the current prefix; usage + POS_SUBCMDS: prefix
  (completions regenerated)
- Matching via scoped nocasematch + quoted-literal =~ prefix;
  ai_bridge_prefix() precedence: env file > env var > default ai

Verified: routing harness (default/custom/case-insensitive/reset/
fallback/unknown-command) green, CLI verb tests, dispatch smoke,
pos config render, gates 0 FAIL 0 WARN.
2026-08-27 11:04:37 -04:00
8 changed files with 296 additions and 41 deletions
+2
View File
@@ -136,3 +136,5 @@ summary (newest last).
- **2026-08-23** — Menu Phase 2 + firewall style-migration (decision "a" activated: P1 landed, `lib/menu-lib.sh` exists). `pos docker vbox` (157→261): 6-item menu hub over the inline case verbs via a quoted self-invocation `menu_self` (verbs never re-enter the menu → no recursion); `enter` hands over the terminal and returns to the loop; rm/create behind VM-naming y/N. `pos network download` (950→1104): 13-item top-verb map onto existing cmd_* fns — add URL (`menu_ask_value`, optional `--tmux`), gid-pick → info/pause/resume/remove/restart (remove names name+gid before delete), typed-confirm purge, watch handover, daemon start/stop (stop confirmed); non-fatal RPC liveness gate (`-m 3`) keeps queue views alive on a dead daemon; deliberately NOT added to INTERACTIVE_CMDS — menu-lib's tty-guarded reads make membership unnecessary and keep tee-logging for all scripted verbs (survey E-002; Reviewer traced the lint pass as honest through `uses_stdin`). `pos system firewall` (308→325) migrated to repo-standard mechanics ONLY: menu heredoc render → stderr `{ … } >&2` (body byte-preserved), all **38** interactive reads → `/dev/tty` via tool-local `tty_read()` (EOF/no-tty → pointer + rc1, never hangs), `prompt_ipver` de-command-substituted so EOF exits gracefully; root gate / per-cmd confirm / typed RESET / pager / notify / every ufw invocation untouched. Both new tools register `# POS_SUBCMDS:` += `menu`; POS.md rows updated; GEN regen'd. Chain: Builder T4 (`reportAgents/2026-08-23-builder-t4-p2-menus-vbox-download.md`; correctly caught an Orchestrator brief error claiming download was in INTERACTIVE_CMDS) + T5 (`-t5-firewall-menu-migration.md`; pty parity captures vs pre-edit baseline) → Reviewer ACCEPT-WITH-NITS over both (`-reviewer-phase2-menu.md`, transcribed by Orchestrator; recursion/injection analysis, 13/13 mapping proof, four T5 intents verified hunk-by-hunk). Verified: bash -n ×3 + gates green after each pass; final trio re-run by Orchestrator post-T5 — `make check` OK · `make lint` 0 FAIL / 0 WARN (76s under box load ~7; the earlier apparent lint hang was shared-box CPU contention, no code issue). Remaining notes for later sessions: errexit kills whole menu when a backing verb hard-fails (repo-wide pattern, all six menus); `confirm()` EOF hits set-u unbound `yn` (pre-existing common.sh); vbox create EOF at dir prompt degrades to default while name/image prompts abort (cosmetic). - **2026-08-23** — Menu Phase 2 + firewall style-migration (decision "a" activated: P1 landed, `lib/menu-lib.sh` exists). `pos docker vbox` (157→261): 6-item menu hub over the inline case verbs via a quoted self-invocation `menu_self` (verbs never re-enter the menu → no recursion); `enter` hands over the terminal and returns to the loop; rm/create behind VM-naming y/N. `pos network download` (950→1104): 13-item top-verb map onto existing cmd_* fns — add URL (`menu_ask_value`, optional `--tmux`), gid-pick → info/pause/resume/remove/restart (remove names name+gid before delete), typed-confirm purge, watch handover, daemon start/stop (stop confirmed); non-fatal RPC liveness gate (`-m 3`) keeps queue views alive on a dead daemon; deliberately NOT added to INTERACTIVE_CMDS — menu-lib's tty-guarded reads make membership unnecessary and keep tee-logging for all scripted verbs (survey E-002; Reviewer traced the lint pass as honest through `uses_stdin`). `pos system firewall` (308→325) migrated to repo-standard mechanics ONLY: menu heredoc render → stderr `{ … } >&2` (body byte-preserved), all **38** interactive reads → `/dev/tty` via tool-local `tty_read()` (EOF/no-tty → pointer + rc1, never hangs), `prompt_ipver` de-command-substituted so EOF exits gracefully; root gate / per-cmd confirm / typed RESET / pager / notify / every ufw invocation untouched. Both new tools register `# POS_SUBCMDS:` += `menu`; POS.md rows updated; GEN regen'd. Chain: Builder T4 (`reportAgents/2026-08-23-builder-t4-p2-menus-vbox-download.md`; correctly caught an Orchestrator brief error claiming download was in INTERACTIVE_CMDS) + T5 (`-t5-firewall-menu-migration.md`; pty parity captures vs pre-edit baseline) → Reviewer ACCEPT-WITH-NITS over both (`-reviewer-phase2-menu.md`, transcribed by Orchestrator; recursion/injection analysis, 13/13 mapping proof, four T5 intents verified hunk-by-hunk). Verified: bash -n ×3 + gates green after each pass; final trio re-run by Orchestrator post-T5 — `make check` OK · `make lint` 0 FAIL / 0 WARN (76s under box load ~7; the earlier apparent lint hang was shared-box CPU contention, no code issue). Remaining notes for later sessions: errexit kills whole menu when a backing verb hard-fails (repo-wide pattern, all six menus); `confirm()` EOF hits set-u unbound `yn` (pre-existing common.sh); vbox create EOF at dir prompt degrades to default while name/image prompts abort (cosmetic).
- **2026-08-26** — `pos ai alias` activation rework (Option B) + `pos config` listing readability, per the 2026-08-26 Architect/Designer specs (`AgentsReport/{architect,designer}/2026-08-26-*.md`). **Alias activation:** the stale sourced-snapshot mechanism is gone — every `pos ai alias` invocation runs `_alias_sync()` (two-way reconciliation: render-diff-install of one executable wrapper per ENV record at `~/.local/bin/<name>` chmod 755 via mktemp+mv with a `bash -n` pre-commit guard; marker-guarded deletion of owned wrappers missing from ENV; legacy `ai-aliases.sh` generation stopped and generator-marker-guarded auto-removal with an `unalias <names>` remediation hint; loud PATH guidance when `~/.local/bin` is off PATH). Edits are live on next invocation with no shell reload (kills the reported stale-gemini-alias bug class); create refuses foreign-file and PATH-binary collisions; `show` gains the wrapper path; `pos-system-uninstall` sweeps the wrappers by their line-2 marker in discovery+removal. Dup-table menu bug fixed with a single `_alias_table` renderer (menu option 4 returns to the loop whose pre-render already shows fresh state). **Config readability** (`lib/config-ui.sh`, fully generic): new optional `# POS_CONFIG:` field types — `@Caption` / `@[KEY=v1|v2] Caption` group captions (condition evaluated per render via `cfg_value`; inactive groups dimmed with a textual reason, never hidden → numbering stable; empty-alt segment = unset-as-default) and `*providers=<tag>` adapter filtering (zero match warns once + suppresses its caption); uniform typography tier for ALL scopes (bold title/keys, CYAN rule, dim numbers/placeholders/examples/captions, hanging-indent wrap clamped 60120 cols, whole render block → stderr per menu-lib house pattern, honest prompt `Number to edit [r=refresh, q=quit]:`); masking/edit flow byte-compatible, no per-scope branches. `bin/pos-ai` line-6 header adopted to the caption/tag syntax (single-line change). Verified: stub-PATH harness (`HOME=/tmp/…`, `CONFIG_DIR` seam, argv-capturing `pos` shim) covering %q quoting round-trips (quotes/backticks/`$()`/%/unicode), staleness kill-test, orphan retraction, collision-refusal matrix, legacy migration (marker + foreign), PATH-absent warning, non-tty guard, idempotent double-sync; rendered-output diffs vs Designer mockups for `ai` AND old-format `system`; gates `make gen && make check && make lint` 0 FAIL / 0 WARN. - **2026-08-26** — `pos ai alias` activation rework (Option B) + `pos config` listing readability, per the 2026-08-26 Architect/Designer specs (`AgentsReport/{architect,designer}/2026-08-26-*.md`). **Alias activation:** the stale sourced-snapshot mechanism is gone — every `pos ai alias` invocation runs `_alias_sync()` (two-way reconciliation: render-diff-install of one executable wrapper per ENV record at `~/.local/bin/<name>` chmod 755 via mktemp+mv with a `bash -n` pre-commit guard; marker-guarded deletion of owned wrappers missing from ENV; legacy `ai-aliases.sh` generation stopped and generator-marker-guarded auto-removal with an `unalias <names>` remediation hint; loud PATH guidance when `~/.local/bin` is off PATH). Edits are live on next invocation with no shell reload (kills the reported stale-gemini-alias bug class); create refuses foreign-file and PATH-binary collisions; `show` gains the wrapper path; `pos-system-uninstall` sweeps the wrappers by their line-2 marker in discovery+removal. Dup-table menu bug fixed with a single `_alias_table` renderer (menu option 4 returns to the loop whose pre-render already shows fresh state). **Config readability** (`lib/config-ui.sh`, fully generic): new optional `# POS_CONFIG:` field types — `@Caption` / `@[KEY=v1|v2] Caption` group captions (condition evaluated per render via `cfg_value`; inactive groups dimmed with a textual reason, never hidden → numbering stable; empty-alt segment = unset-as-default) and `*providers=<tag>` adapter filtering (zero match warns once + suppresses its caption); uniform typography tier for ALL scopes (bold title/keys, CYAN rule, dim numbers/placeholders/examples/captions, hanging-indent wrap clamped 60120 cols, whole render block → stderr per menu-lib house pattern, honest prompt `Number to edit [r=refresh, q=quit]:`); masking/edit flow byte-compatible, no per-scope branches. `bin/pos-ai` line-6 header adopted to the caption/tag syntax (single-line change). Verified: stub-PATH harness (`HOME=/tmp/…`, `CONFIG_DIR` seam, argv-capturing `pos` shim) covering %q quoting round-trips (quotes/backticks/`$()`/%/unicode), staleness kill-test, orphan retraction, collision-refusal matrix, legacy migration (marker + foreign), PATH-absent warning, non-tty guard, idempotent double-sync; rendered-output diffs vs Designer mockups for `ai` AND old-format `system`; gates `make gen && make check && make lint` 0 FAIL / 0 WARN.
- **2026-08-27** — Critical fix: paste injection + multiline paste in `pos ai alias`'s Insert Prompt (root cause: `menu_ask_value` → plain line-oriented `read -rp`; a multi-line Ctrl+V paste floods the tty queue, `read` consumes only line one and the rest execute as commands later or get eaten by the next prompt — user-verified `$(whoami)`/`; ls`/`sudo apt update` behavior). New `menu_read_value()` in `lib/menu-lib.sh` (169→362): raw-mode (`stty -icanon -echo -isig min 1 time 0`) bracketed-paste-aware value reader — `\e[?2004h/l` markers, text inside `[200~…[201~` inserted LITERALLY (embedded newlines/CR are data), Enter submits only outside a paste, Backspace/DEL/Left/Right/Home/End/Delete/Ctrl-U edit, Ctrl-D-on-empty + Ctrl-C/Z/\ cancel (terminal restored first); bytes read chunk-wise via `dd bs=4096|od -tx1|tr` — NOT bash's `read` builtin, which self-interrupts on an ETX byte from a tty even with ISIG disabled (SIGINTs the whole script on Ctrl-C); confirmed `read -erp` (readline) atomically consumes a paste but returns only its first line, so a custom reader was required. `bin/pos-ai-alias` (712→760): `_alias_prompt_encode/_decode` (backslash→`\\`, newline→`\n`; literal `[ = ]` comparisons — bash `case` patterns don't match a single backslash), `_alias_prompt_truncate` newline-safe + max-length arg; load/save encode/decode the prompt field; edit wizard shows a truncated display default but Enter restores the FULL original prompt (fixes pre-existing silent truncation of >80-char prompts), empty-original Enter continues. Verified: pty harnesses (`/tmp/pty_{menulib,cancel,e2e_alias}.py`, `/tmp/roundtrip_test.sh`) — bracketed multiline paste captured verbatim incl. `C:\temp\note`/`$(whoami)`/`; ls`/`echo test`/`sudo apt update`, nothing executed, clean exit; single-line paste; Ctrl-D and Ctrl-C both cancel cleanly (CANCELLED→DONE, terminal restored); full create→list→show→edit E2E with decode round-trip and Enter-keeps-full; `bash -n` ×2, `make gen && make check`, `make lint` 0 FAIL / 0 WARN. - **2026-08-27** — Critical fix: paste injection + multiline paste in `pos ai alias`'s Insert Prompt (root cause: `menu_ask_value` → plain line-oriented `read -rp`; a multi-line Ctrl+V paste floods the tty queue, `read` consumes only line one and the rest execute as commands later or get eaten by the next prompt — user-verified `$(whoami)`/`; ls`/`sudo apt update` behavior). New `menu_read_value()` in `lib/menu-lib.sh` (169→362): raw-mode (`stty -icanon -echo -isig min 1 time 0`) bracketed-paste-aware value reader — `\e[?2004h/l` markers, text inside `[200~…[201~` inserted LITERALLY (embedded newlines/CR are data), Enter submits only outside a paste, Backspace/DEL/Left/Right/Home/End/Delete/Ctrl-U edit, Ctrl-D-on-empty + Ctrl-C/Z/\ cancel (terminal restored first); bytes read chunk-wise via `dd bs=4096|od -tx1|tr` — NOT bash's `read` builtin, which self-interrupts on an ETX byte from a tty even with ISIG disabled (SIGINTs the whole script on Ctrl-C); confirmed `read -erp` (readline) atomically consumes a paste but returns only its first line, so a custom reader was required. `bin/pos-ai-alias` (712→760): `_alias_prompt_encode/_decode` (backslash→`\\`, newline→`\n`; literal `[ = ]` comparisons — bash `case` patterns don't match a single backslash), `_alias_prompt_truncate` newline-safe + max-length arg; load/save encode/decode the prompt field; edit wizard shows a truncated display default but Enter restores the FULL original prompt (fixes pre-existing silent truncation of >80-char prompts), empty-original Enter continues. Verified: pty harnesses (`/tmp/pty_{menulib,cancel,e2e_alias}.py`, `/tmp/roundtrip_test.sh`) — bracketed multiline paste captured verbatim incl. `C:\temp\note`/`$(whoami)`/`; ls`/`echo test`/`sudo apt update`, nothing executed, clean exit; single-line paste; Ctrl-D and Ctrl-C both cancel cleanly (CANCELLED→DONE, terminal restored); full create→list→show→edit E2E with decode round-trip and Enter-keeps-full; `bash -n` ×2, `make gen && make check`, `make lint` 0 FAIL / 0 WARN.
- **2026-08-27** — Configurable AI-bridge trigger word for the Telegram listener: the hard-coded `ai ` prefix in `pos-communication-telegram-listener` became `TELEGRAM_AI_PREFIX` (default `ai`) — messages starting with `<prefix> ` (case-insensitive, literal match) are forwarded to Gemini. New `prefix` verb: `pos communication telegram listener prefix` shows the current word, `prefix <word>` sets it (validated `[A-Za-z0-9][A-Za-z0-9_-]*`, writes `TELEGRAM_AI_PREFIX` to `telegram.env` chmod 600); also editable via `pos config telegram` (field added to the sender's `# POS_CONFIG:` telegram scope — registry-driven, no code in config-ui). Matching is per-message hot-reloaded (like the command map — no daemon restart), via scoped `shopt -s nocasematch` + quoted-literal `=~` prefix (bash `case` patterns can't do literal-then-whitespace + case-insensitivity in one test); `ai_bridge_prefix()` precedence: telegram.env > env from load_config > default `ai`. `--status` shows the current prefix; usage + `# POS_SUBCMDS: prefix` added (completions regenerate). Preserved edge: bare `ai` (no trailing space) never matched the old regex, so it still falls through to "Unknown command". Docs: POS.md listener rows/paragraph, howto/ai.md Telegram section + troubleshooting (also corrected a stale claim that AI errors reply with a `pos config ai` hint — code replies `AI error: …` only). Verified: function-level routing harness (`/tmp/ai_prefix_routing_test.sh` — extraction of the real listener functions + PATH stub `pos`): default `ai`/`AI` routes, bare-prefix and unknown-command fallthrough, `ai /reset` and custom-`bot` `/reset` reset the session, custom `bot`/`BOT` routes and old `ai` no longer routes, per-message hot-reload after removing the var; CLI verb tests (show/set/invalid rc 1/leading-digit/`--status`); dispatch smoke `pos communication telegram listener prefix` + flat form; `pos config telegram` render shows the field; `bash -n` ×2, `make gen && make check`, `make lint` 0 FAIL / 0 WARN.
- **2026-08-29** — Generic text-prefix map for the Telegram listener (user's clarification superseding the scalar `TELEGRAM_AI_PREFIX` setter): `bin/pos-communication-telegram-listener` (623→782) now routes any non-command message `<word> <text>` to a mapped command with `<text>` appended as ONE quoted argument — `opencode=opencode` turns "opencode check cpu" into `opencode "check cpu"`. New map file `telegram_prefixes.env` (chmod 600, re-read per message, `@quiet` values, 120s cap, empty→`OK`, `exit <rc>` reply, syntax-checked on save, first-file-match wins, case-insensitive, word must be space-delimited so bare `<word>` still falls through). Routing order: text-prefix map → built-in Gemini `ai` bridge → `/command` map → Unknown (a mapped `ai` shadows the bridge). `prefix` verb reworked: bare = list map + bridge word; `prefix <word> <command...>` = map (validated `[A-Za-z0-9][A-Za-z0-9_-]*`, `bash -n` via check_syntax); `prefix <word>` = show one; `prefix -r <word>` = remove; the AI-bridge word itself is now set ONLY via `pos config telegram` (`TELEGRAM_AI_PREFIX`, default `ai``--status` + bare `prefix` still display it). `run_and_reply()` extracted to share `/command`-map (60s) and prefix (120s) execution semantics; dispatch passes `"${@:2}"`. Docs: POS.md listener rows/paragraph, howto/communication.md bullet, howto/ai.md (prefix-map + shadowing), usage(), `# POS:` header, AGENT_Context regen. Verified: routing harness `/tmp/prefix_map_routing_test.sh` 27/27 (ai-bridge regression incl. `/reset`, opencode remainder=ONE arg, case-insensitivity, bare/trailing-space fallthrough, shadowing, no partial-prefix false match, exit/OK/@quiet/env-expansion, /command-map regression via run_and_reply); CLI verb suite (set/show/remove/missing rc 1/invalid word rc 1/invalid cmd rc 1 — fixed latent `set -e` cmdsubst abort on syntax errors); dispatch smoke nested + flat + `--status`; `pos config telegram` render; `bash -n`, `make gen && make check`, `make lint` 0 FAIL / 0 WARN.
+4 -4
View File
@@ -67,7 +67,7 @@ Linux_post_install/
│ ├── pos-communication-matrix-listener # Matrix listener: map /command → bash, run them on room messages │ ├── pos-communication-matrix-listener # Matrix listener: map /command → bash, run them on room messages
│ ├── pos-communication-matrix-sender # Send messages to a Matrix room via the client-server API (send, test, login) │ ├── pos-communication-matrix-sender # Send messages to a Matrix room via the client-server API (send, test, login)
│ ├── pos-communication-scrcpy # Mirror/control an Android device via scrcpy+adb (mirror, devices, record, tcpip, connect, push, pull, screenshot, info) │ ├── pos-communication-scrcpy # Mirror/control an Android device via scrcpy+adb (mirror, devices, record, tcpip, connect, push, pull, screenshot, info)
│ ├── pos-communication-telegram-listener # Telegram bot listener: map /command → bash, run them on chat messages │ ├── pos-communication-telegram-listener # Telegram bot listener: map /command → bash and <prefix> → app, run them on chat messages
│ ├── pos-communication-telegram-sender # Send Telegram messages/files/links/stickers via Bot API (send, test) │ ├── pos-communication-telegram-sender # Send Telegram messages/files/links/stickers via Bot API (send, test)
│ ├── pos-docker-compose # Docker Compose service manager (ls/up/down/restart/logs/update/config) │ ├── pos-docker-compose # Docker Compose service manager (ls/up/down/restart/logs/update/config)
│ ├── pos-docker-health # One-glance container health dashboard (exits 1 if unhealthy) │ ├── pos-docker-health # One-glance container health dashboard (exits 1 if unhealthy)
@@ -281,7 +281,7 @@ All non-interactive `pos` commands log output to `~/.local/share/linux_post_inst
| communication | matrix-listener | `pos-communication-matrix-listener` | Matrix listener: map /command → bash, run them on room messages | | | | communication | matrix-listener | `pos-communication-matrix-listener` | Matrix listener: map /command → bash, run them on room messages | | |
| communication | matrix-sender | `pos-communication-matrix-sender` | Send messages to a Matrix room via the client-server API (send, test, login) | | | | communication | matrix-sender | `pos-communication-matrix-sender` | Send messages to a Matrix room via the client-server API (send, test, login) | | |
| communication | scrcpy | `pos-communication-scrcpy` | Mirror/control an Android device via scrcpy+adb (mirror, devices, record, tcpip, connect, push, pull, screenshot, info) | | | | communication | scrcpy | `pos-communication-scrcpy` | Mirror/control an Android device via scrcpy+adb (mirror, devices, record, tcpip, connect, push, pull, screenshot, info) | | |
| communication | telegram-listener | `pos-communication-telegram-listener` | Telegram bot listener: map /command → bash, run them on chat messages | | | | communication | telegram-listener | `pos-communication-telegram-listener` | Telegram bot listener: map /command → bash and <prefix> → app, run them on chat messages | | |
| communication | telegram-sender | `pos-communication-telegram-sender` | Send Telegram messages/files/links/stickers via Bot API (send, test) | | | | communication | telegram-sender | `pos-communication-telegram-sender` | Send Telegram messages/files/links/stickers via Bot API (send, test) | | |
| docker | compose | `pos-docker-compose` | Docker Compose service manager (ls/up/down/restart/logs/update/config) | | | | docker | compose | `pos-docker-compose` | Docker Compose service manager (ls/up/down/restart/logs/update/config) | | |
| docker | health | `pos-docker-health` | One-glance container health dashboard (exits 1 if unhealthy) | | | | docker | health | `pos-docker-health` | One-glance container health dashboard (exits 1 if unhealthy) | | |
@@ -609,7 +609,7 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`
| `bin/pos-communication-matrix-listener` | 568 | Matrix listener: map /command → bash, run them on room messages | | `bin/pos-communication-matrix-listener` | 568 | Matrix listener: map /command → bash, run them on room messages |
| `bin/pos-communication-matrix-sender` | 224 | Send messages to a Matrix room via the client-server API (send, test, login) | | `bin/pos-communication-matrix-sender` | 224 | Send messages to a Matrix room via the client-server API (send, test, login) |
| `bin/pos-communication-scrcpy` | 254 | Mirror/control an Android device via scrcpy+adb (mirror, devices, record, tcpip, connect, push, pull, screenshot, info) | | `bin/pos-communication-scrcpy` | 254 | Mirror/control an Android device via scrcpy+adb (mirror, devices, record, tcpip, connect, push, pull, screenshot, info) |
| `bin/pos-communication-telegram-listener` | 566 | Telegram bot listener: map /command → bash, run them on chat messages | | `bin/pos-communication-telegram-listener` | 782 | Telegram bot listener: map /command → bash and <prefix> → app, run them on chat messages |
| `bin/pos-communication-telegram-sender` | 221 | 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-docker-compose` | 487 | Docker Compose service manager (ls/up/down/restart/logs/update/config) | | `bin/pos-docker-compose` | 487 | Docker Compose service manager (ls/up/down/restart/logs/update/config) |
| `bin/pos-docker-health` | 107 | One-glance container health dashboard (exits 1 if unhealthy) | | `bin/pos-docker-health` | 107 | One-glance container health dashboard (exits 1 if unhealthy) |
@@ -644,7 +644,7 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`
| `bin/pos-ai` | 692 | AI assistant: ask, chat, sessions, capture, models, providers | | `bin/pos-ai` | 692 | AI assistant: ask, chat, sessions, capture, models, providers |
| `bin/pos-config` | 80 | Interactive editor for the tools' runtime config (reads # POS_CONFIG: registry) | | `bin/pos-config` | 80 | Interactive editor for the tools' runtime config (reads # POS_CONFIG: registry) |
| `bin/pos-tree` | 118 | Show the pos CLI command tree: categories, commands, and subcommands | | `bin/pos-tree` | 118 | Show the pos CLI command tree: categories, commands, and subcommands |
| `completions/pos.bash` | 307 | Dynamic bash completion | | `completions/pos.bash` | 308 | Dynamic bash completion |
<!-- GEN:END filetable --> <!-- GEN:END filetable -->
| `apps/install.sh` | 171 | App install/uninstall picker/orchestrator | | `apps/install.sh` | 171 | App install/uninstall picker/orchestrator |
+9 -3
View File
@@ -307,7 +307,7 @@ Subcommands that need input prompt interactively when args are omitted. Bare inv
| Command | File | Purpose | Configuration | | 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 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 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`). 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 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 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 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` |
@@ -335,12 +335,18 @@ The bot token is a secret — it is stored only in `~/.config/linux_post_install
|---------|----------| |---------|----------|
| `pos communication telegram listener` | Interactive editor for the `/command` → bash map (`a`dd / `e`dit / `r`emove / `t`est / `q`uit); test-runs run `bash -n` first and may execute the command live | | `pos communication telegram listener` | Interactive editor for the `/command` → bash map (`a`dd / `e`dit / `r`emove / `t`est / `q`uit); 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 --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 --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 --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 --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 --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. 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)`. 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".
**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 telegram``TELEGRAM_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. 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.
+21 -6
View File
@@ -150,6 +150,19 @@ you: ai what is Nvidia
bot: NVIDIA is a company best known for GPUs... bot: NVIDIA is a company best known for GPUs...
``` ```
The trigger word is configurable via `pos config telegram`
`TELEGRAM_AI_PREFIX` (default `ai`); it takes effect immediately, so with
prefix `bot` you'd message `bot what is Nvidia`. `pos communication telegram
listener prefix` shows the current value.
The listener also has a generic **text-prefix map** (`telegram_prefixes.env`,
managed with `pos communication telegram listener prefix <word> <command...>`)
that runs any app with the rest of the message as one argument — e.g.
`prefix opencode opencode` turns the message `opencode check cpu` into
`opencode "check cpu"`. Routing order per non-command message: text-prefix
map → AI bridge → `/command` map, so mapping a word in the prefix map
shadows the Gemini bridge for that word.
The bridge lives in the Telegram listener's `handle_message` (it calls The bridge lives in the Telegram listener's `handle_message` (it calls
`pos ai ask`); only the owner chat is served, so your key stays private. `pos ai ask`); only the owner chat is served, so your key stays private.
Set a different model per message: Set a different model per message:
@@ -162,13 +175,14 @@ you: ai --model gemini-2.5-flash explain a Raft consensus log
Each chat has its own persistent session (`telegram-<chat id>` — independent Each chat has its own persistent session (`telegram-<chat id>` — independent
of your terminal's `default` session), so the model of your terminal's `default` session), so the model
remembers the conversation; `ai /reset` clears it. The listener passes a system remembers the conversation; `<prefix> /reset` clears it (with the default
prefix that's `ai /reset`). The listener passes a system
prompt telling the model it is answering in a Telegram chat — so it uses emojis prompt telling the model it is answering in a Telegram chat — so it uses emojis
and stays lively — and strips markdown (`**x**`, backticks, `#`, links…) from and stays lively — and strips markdown (`**x**`, backticks, `#`, links…) from
the reply before sending it, since messages go out as plain text. the reply before sending it, since messages go out as plain text.
Replying to a message before `ai …` makes that message part of the prompt, so Replying to a message before `<prefix> …` makes that message part of the
the model can answer about it: prompt, so the model can answer about it:
``` ```
you: /status → bot: (system health output…) you: /status → bot: (system health output…)
@@ -237,9 +251,10 @@ truncated). To disable: `unset __POS_CAPTURE_ACTIVE`.
model's context window; check `pos ai models`. model's context window; check `pos ai models`.
- `API error 429` → rate limit (free tier); wait and retry, or use a different - `API error 429` → rate limit (free tier); wait and retry, or use a different
model. model.
- Nothing in Telegram for `ai …` → the listener daemon must be running - Nothing in Telegram for `<prefix> …` (default `ai`) → the listener daemon
(`pos communication telegram listener --status`); the bot token and owner must be running (`pos communication telegram listener --status`); the bot
chat id must match `pos config telegram`. token and owner chat id must match `pos config telegram`. Check the current
trigger word with `pos communication telegram listener prefix`.
--- ---
+15
View File
@@ -97,6 +97,21 @@ pos communication telegram listener --disable # remove it
- **Runs as you:** mapped commands execute as your user with a 60s timeout, - **Runs as you:** mapped commands execute as your user with a 60s timeout,
stdout + stderr are replied to the chat (truncated ~3800 chars; empty → `OK`). stdout + stderr are replied to the chat (truncated ~3800 chars; empty → `OK`).
`sudo` inside a command needs a NOPASSWD rule. `sudo` inside a command needs a NOPASSWD rule.
- **Text-prefix map (apps):** `~/.config/linux_post_install/telegram_prefixes.env`
(chmod 600), one `<word>=command` per line — a non-command message
`<word> <text>` runs the app with `<text>` appended as ONE quoted argument,
e.g. `opencode=opencode` turns "opencode check cpu" into `opencode "check cpu"`.
First match wins (file order), case-insensitive, word must be space-delimited.
Manage it with `pos communication telegram listener prefix <word> <command...>`
(bare `prefix` lists, `prefix <word>` shows one, `prefix -r <word>` removes):
```bash
pos communication telegram listener prefix opencode opencode
# then message: opencode check cpu → runs opencode "check cpu"
pos communication telegram listener prefix ai "pos ai gemini ask --session telegram-\$TELEGRAM_CHAT_ID"
# overrides the built-in Gemini bridge for the word 'ai'
```
Routing order per non-command message: text-prefix map → AI bridge →
`/command` map → "Unknown command".
- **`@quiet` prefix:** a map value starting with `@quiet ` runs the command but - **`@quiet` prefix:** a map value starting with `@quiet ` runs the command but
does NOT reply — for commands that already send their own notification, so does NOT reply — for commands that already send their own notification, so
you don't get it twice. `pos system backup` self-notifies, so you don't get it twice. `pos system backup` self-notifies, so
+243 -27
View File
@@ -1,17 +1,22 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
# POS: communication telegram-listener — Telegram bot listener: map /command → bash, run them on chat messages # POS: communication telegram-listener — Telegram bot listener: map /command → bash and <prefix> → app, run them on chat messages
# POS_FLAGS: --enable --disable --status --sync-commands --run # POS_FLAGS: --enable --disable --status --sync-commands --run
# POS_SUBCMDS: prefix
CONFIG_DIR="${CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/linux_post_install}" CONFIG_DIR="${CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/linux_post_install}"
CONFIG_FILE="$CONFIG_DIR/telegram.env" CONFIG_FILE="$CONFIG_DIR/telegram.env"
MAP_FILE="$CONFIG_DIR/telegram_commands.env" MAP_FILE="$CONFIG_DIR/telegram_commands.env"
PREFIX_FILE="$CONFIG_DIR/telegram_prefixes.env"
API="https://api.telegram.org" API="https://api.telegram.org"
SERVICE="pos-telegram-listener.service" SERVICE="pos-telegram-listener.service"
USER_SYSTEMD_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user" USER_SYSTEMD_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
# System prompt for the "ai " bridge: replies are posted straight into the # System prompt for the "<prefix> " AI bridge (default prefix: "ai"): replies
# chat, so ask for concise, emoji-friendly Telegram-style answers. # are posted straight into the chat, so ask for concise, emoji-friendly
# Telegram-style answers. The trigger word is configurable via
# TELEGRAM_AI_PREFIX in telegram.env ('pos config telegram', default 'ai');
# the text-prefix map checked before it can override any word.
AI_SYSTEM="You are a friendly assistant chatting in a Telegram chat. Keep replies concise, use emojis and light formatting to make them lively, and never claim to send messages yourself." AI_SYSTEM="You are a friendly assistant chatting in a Telegram chat. Keep replies concise, use emojis and light formatting to make them lively, and never claim to send messages yourself."
err() { echo "ERROR: $*" >&2; exit 1; } err() { echo "ERROR: $*" >&2; exit 1; }
@@ -32,10 +37,25 @@ Commands:
--sync-commands --sync-commands
Push the mapped /commands to the bot's "/" menu (setMyCommands) Push the mapped /commands to the bot's "/" menu (setMyCommands)
--run Run the polling loop in the foreground (used by the service) --run Run the polling loop in the foreground (used by the service)
prefix [word [command...]]
Manage the text-prefix map (telegram_prefixes.env): any
non-command message '<prefix> <text>' runs the mapped command
with <text> as ONE argument. Bare: list; <word>: show one;
<word> <command...>: map (e.g. 'prefix opencode opencode' →
"opencode check cpu" runs 'opencode "check cpu"');
-r <word>: remove. Built-in Gemini bridge word (default 'ai')
is set via 'pos config telegram' (TELEGRAM_AI_PREFIX).
Config: $CONFIG_FILE (TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID — edit with 'pos config telegram') Config: $CONFIG_FILE (TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID,
TELEGRAM_AI_PREFIX — edit with 'pos config telegram')
Map: $MAP_FILE — '/cmd=bash command' per line (optional Map: $MAP_FILE — '/cmd=bash command' per line (optional
'/cmd::short description=bash command' shown in the bot menu) '/cmd::short description=bash command' shown in the bot menu)
Prefix map: $PREFIX_FILE — '<word>=command' per line: a non-command
message starting with '<word> <text>' runs the command with <text>
appended as ONE quoted argument. First match wins (file order),
case-insensitive; a mapped word shadows the built-in Gemini 'ai'
bridge. Routing order: text-prefix map → AI bridge → /command map →
'Unknown command'. Manage it with the 'prefix' verb.
Prefix a map value with '@quiet ' to run the command without replying — Prefix a map value with '@quiet ' to run the command without replying —
for commands that already send their own notification, so you don't get it for commands that already send their own notification, so you don't get it
@@ -241,6 +261,105 @@ strip_quiet() {
fi fi
} }
# ── text-prefix map (PREFIX_FILE) ───────────────────────────────
# Lines: <word>=command. Unlike the /command map (exact match on the whole
# message), a matching <word> at the START of a non-command message routes
# the REST of the message to the mapped command as ONE quoted argument:
# 'opencode=opencode' + message "opencode check cpu" → 'opencode "check cpu"'
# The map is re-read per message (edits apply without restarting), matching
# is case-insensitive, the FIRST matching line wins (file order), a bare
# <word> with no trailing space does NOT match, and a mapped word shadows the
# built-in Gemini bridge in handle_message.
prefix_map_find() {
[ -f "$PREFIX_FILE" ] || return 1
local text="$1" line word cmd rem
shopt -s nocasematch
while IFS= read -r line; do
case "$line" in \#*|'') continue ;; esac
word="${line%%=*}"
cmd="${line#*=}"
word="${word# }"
cmd="${cmd# }"
[[ -n "$word" && -n "$cmd" ]] || continue
if [[ "$text" =~ ^"$word"[[:space:]](.*)$ ]]; then
rem="${BASH_REMATCH[1]}"
[ -n "$rem" ] || continue
shopt -u nocasematch
printf '%s\x1f%s\n' "$cmd" "$rem"
return 0
fi
done < "$PREFIX_FILE"
shopt -u nocasematch
return 1
}
prefix_map_set() {
local word="$1" cmd="$2"
if ! [[ "$word" =~ ^[A-Za-z0-9][A-Za-z0-9_-]*$ ]]; then
err "invalid prefix '$word' — use one word of letters, digits, '-' or '_' (no spaces)"
fi
local errs
errs="$(check_syntax "$cmd" 2>&1)" || err "invalid command for '$word': $errs"
mkdir -p "$CONFIG_DIR"
touch "$PREFIX_FILE"
chmod 600 "$PREFIX_FILE"
local tmp
tmp="$(mktemp)"
awk -v k="$word" 'index($0, k "=") != 1 { print }' "$PREFIX_FILE" > "$tmp"
printf '%s=%s\n' "$word" "$cmd" >> "$tmp"
mv "$tmp" "$PREFIX_FILE"
chmod 600 "$PREFIX_FILE"
}
prefix_map_del() {
[ -f "$PREFIX_FILE" ] || return 1
local tmp
tmp="$(mktemp)"
awk -v k="$1" 'index($0, k "=") != 1 { print }' "$PREFIX_FILE" > "$tmp"
if cmp -s "$tmp" "$PREFIX_FILE"; then
rm -f "$tmp"
return 1
fi
mv "$tmp" "$PREFIX_FILE"
chmod 600 "$PREFIX_FILE"
return 0
}
prefix_map_show() {
local word="$1" line key
[ -f "$PREFIX_FILE" ] || return 1
while IFS= read -r line; do
case "$line" in \#*|'') continue ;; esac
key="${line%%=*}"
if [ "$key" = "$word" ]; then
printf '%s -> %s\n' "$key" "${line#*=}"
return 0
fi
done < "$PREFIX_FILE"
return 1
}
# Run a mapped command line and reply with its output: empty output → "OK",
# non-zero exit → "exit <rc>" + output; quiet=1 suppresses the reply (for
# '@quiet ' entries that self-notify). Used by the /command map (60s cap)
# and the text-prefix bridge (120s cap for app calls).
run_and_reply() {
local cmdline="$1" msg_id="$2" tmo="${3:-120}" quiet="${4:-0}" output rc
if output="$(timeout "$tmo" bash -c "$cmdline" 2>&1)"; then
rc=0
else
rc=$?
fi
[ "$quiet" -eq 1 ] && return
[ -n "$output" ] || output="OK"
if [ "$rc" -ne 0 ]; then
reply "$(printf 'exit %s\n%s' "$rc" "$output")" "$msg_id" "$rc"
else
reply "$output" "$msg_id"
fi
}
ui_run_command() { ui_run_command() {
local value="$1" output rc local value="$1" output rc
value="$(strip_quiet "$value")" value="$(strip_quiet "$value")"
@@ -417,12 +536,95 @@ status() {
fi fi
echo "config: $CONFIG_FILE" echo "config: $CONFIG_FILE"
echo "map file: $MAP_FILE" echo "map file: $MAP_FILE"
echo "prefix map: $PREFIX_FILE"
load_config
echo "ai prefix: ${TELEGRAM_AI_PREFIX:-ai}"
load_map load_map
echo "commands: $MAP_N mapped" echo "commands: $MAP_N mapped"
local i local i
for ((i=1; i<=MAP_N; i++)); do for ((i=1; i<=MAP_N; i++)); do
printf ' %-16s -> %s%s\n' "${MAP_CMDS[$i]}" "${MAP_VALS[$i]}" "${MAP_DESCS[$i]:+ (${MAP_DESCS[$i]})}" printf ' %-16s -> %s%s\n' "${MAP_CMDS[$i]}" "${MAP_VALS[$i]}" "${MAP_DESCS[$i]:+ (${MAP_DESCS[$i]})}"
done done
if [ -f "$PREFIX_FILE" ] && [ -s "$PREFIX_FILE" ]; then
local pline pword pcmd pn=0
echo "prefixes: ('<prefix> <text>' routes to the mapped app)"
while IFS= read -r pline; do
case "$pline" in \#*|'') continue ;; esac
pword="${pline%%=*}" pcmd="${pline#*=}"
pn=$((pn + 1))
printf ' %-16s -> %s\n' "$pword" "$pcmd"
done < "$PREFIX_FILE"
[ "$pn" -gt 0 ] || echo " (none)"
else
echo "prefixes: (none)"
fi
}
# ── text-prefix map + built-in Gemini bridge word (prefix verb) ──
# 'prefix' manages PREFIX_FILE (word → command). The built-in Gemini bridge
# word (TELEGRAM_AI_PREFIX, default 'ai') is set via 'pos config telegram';
# it is only reached when no text-prefix entry matches first.
prefix_cmd() {
local arg="${1:-}" rest="${*:2}"
if [ -z "$arg" ]; then
load_config
echo "Text-prefix map ($PREFIX_FILE): messages like '<prefix> <text>' run the"
echo "mapped command with <text> passed as ONE argument. First match wins"
echo "(file order), case-insensitive; a mapped word shadows the built-in"
echo "Gemini bridge. A bare <prefix> with no trailing space does not match."
echo
if [ -f "$PREFIX_FILE" ] && [ -s "$PREFIX_FILE" ]; then
local line word cmd
while IFS= read -r line; do
case "$line" in \#*|'') continue ;; esac
word="${line%%=*}" cmd="${line#*=}"
printf ' %-16s -> %s\n' "$word" "$cmd"
done < "$PREFIX_FILE"
else
echo " (none)"
fi
echo
echo "Built-in Gemini bridge word: ${TELEGRAM_AI_PREFIX:-ai} (set via 'pos config telegram')"
echo
echo "Usage:"
echo " prefix list this map + the Gemini bridge word"
echo " prefix <word> show one mapping"
echo " prefix <word> <command...> map <word> to a command"
echo " prefix -r <word> remove a mapping"
echo "Examples:"
echo " pos communication telegram listener prefix opencode opencode"
echo " # then sending 'opencode check cpu' runs: opencode \"check cpu\""
return 0
fi
case "$arg" in
-r|--remove)
[ -n "$rest" ] || err "usage: prefix -r <word>"
if prefix_map_del "$rest"; then
log "removed prefix mapping '$rest'"
else
warn "no prefix mapping for '$rest'"
fi
return 0 ;;
esac
if [ -z "$rest" ]; then
prefix_map_show "$arg" && return 0
warn "no prefix mapping for '$arg'"
echo "Map one with: prefix <word> <command...> (e.g. prefix opencode opencode)"
echo "The built-in Gemini bridge word is set via 'pos config telegram' (TELEGRAM_AI_PREFIX)."
return 1
fi
prefix_map_set "$arg" "$rest"
log "prefix '$arg' -> '$rest' — sending '<$arg> <text>' runs: $rest \"<text>\" (takes effect immediately, no restart)"
}
# Current AI-bridge trigger word. Like the command map, re-read per message so
# 'prefix <word>' edits apply without restarting the daemon. Precedence:
# telegram.env > env var from load_config (--run) > default 'ai'.
ai_bridge_prefix() {
local v=""
[ -f "$CONFIG_FILE" ] && v="$(grep -E '^TELEGRAM_AI_PREFIX=' "$CONFIG_FILE" | tail -1 | sed 's/^[^=]*=//; s/^["'\'']//; s/["'\'']$//')" || true
[ -n "$v" ] || v="${TELEGRAM_AI_PREFIX:-ai}"
printf '%s' "$v"
} }
# ── polling daemon ────────────────────────────────────────────── # ── polling daemon ──────────────────────────────────────────────
@@ -454,20 +656,45 @@ strip_markdown() {
} }
handle_message() { handle_message() {
local text="$1" msg_id="$2" reply_text="${3:-}" value output rc quiet=0 local text="$1" msg_id="$2" reply_text="${3:-}" value quiet=0
case "$text" in case "$text" in
/help|/start) /help|/start)
reply "Mapped commands: $(map_cmds_list)" "$msg_id" reply "Mapped commands: $(map_cmds_list)" "$msg_id"
return ;; return ;;
esac esac
# AI bridge: non-command text starting with "ai " (case-insensitive) is # Text-prefix bridge: '<prefix> <text>' runs the mapped command with
# forwarded to Gemini; the model's answer is replied verbatim. Each chat # <text> passed as ONE quoted argument (telegram_prefixes.env), e.g.
# gets its own persistent memory session ("telegram-<chat_id>"); the exact # 'opencode=opencode' → sending "opencode check cpu" runs
# prompt "ai /reset" clears it. Future non-command intents (e.g. reminders) # 'opencode "check cpu"'. Re-read per message, first match wins (file
# slot in as more case arms here. # order), case-insensitive; a mapped word shadows the built-in Gemini
if [[ "$text" != /* && "$text" =~ ^[Aa][Ii][[:space:]](.*)$ ]]; then # bridge below. A bare <prefix> with no trailing space does not match.
local pv cmd t qtext
if pv="$(prefix_map_find "$text")"; then
cmd="${pv%%$'\x1f'*}"
t="${pv#*$'\x1f'}"
if [ "${cmd#"$QUIET_PREFIX "}" != "$cmd" ]; then
quiet=1
cmd="${cmd#"$QUIET_PREFIX "}"
fi
qtext="$(printf '%q' "$t")"
log "prefix: $text"
run_and_reply "$cmd $qtext" "$msg_id" 120 "$quiet"
return
fi
# AI bridge: non-command text starting with "<prefix> " (default "ai",
# case-insensitive, configurable via TELEGRAM_AI_PREFIX in 'pos config
# telegram') is forwarded to Gemini; the model's answer is replied
# verbatim. Each chat gets its own persistent memory session
# ("telegram-<chat_id>"); the exact prompt "<prefix> /reset" clears it.
# Future non-command intents (e.g. reminders) slot in as more case arms
# here.
local prefix
prefix="$(ai_bridge_prefix)"
shopt -s nocasematch
if [[ "$text" != /* && "$text" =~ ^"$prefix"[[:space:]](.*)$ ]]; then
shopt -u nocasematch
local prompt="${BASH_REMATCH[1]}" answer session local prompt="${BASH_REMATCH[1]}" answer session
[ -n "$prompt" ] || { reply "Usage: ai <prompt> — e.g. 'ai what is Nvidia'" "$msg_id"; return; } [ -n "$prompt" ] || { reply "Usage: $prefix <prompt> — e.g. '$prefix what is Nvidia'" "$msg_id"; return; }
session="telegram-${TELEGRAM_CHAT_ID}" session="telegram-${TELEGRAM_CHAT_ID}"
if [[ "$prompt" =~ ^/?reset[[:space:]]*$ ]]; then if [[ "$prompt" =~ ^/?reset[[:space:]]*$ ]]; then
if pos ai gemini sessions reset "$session" >/dev/null 2>&1; then if pos ai gemini sessions reset "$session" >/dev/null 2>&1; then
@@ -477,7 +704,7 @@ handle_message() {
fi fi
return return
fi fi
log "ai: $prompt" log "$prefix: $prompt"
if [ -n "$reply_text" ]; then if [ -n "$reply_text" ]; then
prompt="[Reply context — the message you are replying to]\n${reply_text}\n\n${prompt}" prompt="[Reply context — the message you are replying to]\n${reply_text}\n\n${prompt}"
fi fi
@@ -489,6 +716,7 @@ handle_message() {
fi fi
return return
fi fi
shopt -u nocasematch
value="$(map_get "$text")" value="$(map_get "$text")"
if [ -z "$value" ]; then if [ -z "$value" ]; then
reply "Unknown command: $text (send /help)" "$msg_id" reply "Unknown command: $text (send /help)" "$msg_id"
@@ -499,20 +727,7 @@ handle_message() {
value="${value#"$QUIET_PREFIX "}" value="${value#"$QUIET_PREFIX "}"
fi fi
log "exec: $text" log "exec: $text"
if output="$(timeout 60 bash -c "$value" 2>&1)"; then run_and_reply "$value" "$msg_id" 60 "$quiet"
rc=0
else
rc=$?
fi
[ "$quiet" -eq 1 ] && return
if [ -z "$output" ]; then
output="OK"
fi
if [ "$rc" -ne 0 ]; then
reply "$(printf 'exit %s\n%s' "$rc" "$output")" "$msg_id" "$rc"
else
reply "$output" "$msg_id"
fi
} }
run_daemon() { run_daemon() {
@@ -561,6 +776,7 @@ case "${1:-}" in
--status) status ;; --status) status ;;
--sync-commands) sync_bot_commands ;; --sync-commands) sync_bot_commands ;;
--run) run_daemon ;; --run) run_daemon ;;
prefix) prefix_cmd "${@:2}" ;;
"") ui ;; "") ui ;;
*) err "Unknown option '$1' (see --help)" ;; *) err "Unknown option '$1' (see --help)" ;;
esac esac
+1 -1
View File
@@ -3,7 +3,7 @@ set -euo pipefail
# POS: communication telegram-sender — Send Telegram messages/files/links/stickers via Bot API (send, test) # POS: communication telegram-sender — Send Telegram messages/files/links/stickers via Bot API (send, test)
# POS_FLAGS: --type --caption --parse-mode --no-preview --token --chat-id --markdown # POS_FLAGS: --type --caption --parse-mode --no-preview --token --chat-id --markdown
# POS_SUBCMDS: send test # POS_SUBCMDS: send test
# POS_CONFIG: telegram | telegram.env | TELEGRAM_BOT_TOKEN=secret:Bot token from @BotFather | TELEGRAM_CHAT_ID=digits:Numeric chat id from @userinfobot # POS_CONFIG: telegram | telegram.env | TELEGRAM_BOT_TOKEN=secret:Bot token from @BotFather | TELEGRAM_CHAT_ID=digits:Numeric chat id from @userinfobot | TELEGRAM_AI_PREFIX=:AI-bridge trigger word in the telegram listener (default ai)::ai
CONFIG_DIR="${CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/linux_post_install}" CONFIG_DIR="${CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/linux_post_install}"
CONFIG_FILE="$CONFIG_DIR/telegram.env" CONFIG_FILE="$CONFIG_DIR/telegram.env"
+1
View File
@@ -30,6 +30,7 @@ _pos_subcmds[ai-gemini]="ask chat models sessions capture"
_pos_subcmds[ai-openrouter]="ask chat sessions capture" _pos_subcmds[ai-openrouter]="ask chat sessions capture"
_pos_subcmds[communication-matrix-sender]="send test login" _pos_subcmds[communication-matrix-sender]="send test login"
_pos_subcmds[communication-scrcpy]="devices record tcpip connect push pull screenshot info" _pos_subcmds[communication-scrcpy]="devices record tcpip connect push pull screenshot info"
_pos_subcmds[communication-telegram-listener]="prefix"
_pos_subcmds[communication-telegram-sender]="send test" _pos_subcmds[communication-telegram-sender]="send test"
_pos_subcmds[docker-compose]="ls installed up down restart logs update config menu" _pos_subcmds[docker-compose]="ls installed up down restart logs update config menu"
_pos_subcmds[docker-vbox]="create enter stop start rm ls menu" _pos_subcmds[docker-vbox]="create enter stop start rm ls menu"