Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a4761df3f6 | |||
| e6fa0a4ee9 |
@@ -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 60–120 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 60–120 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.
|
||||||
|
|||||||
@@ -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
@@ -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 (1–32 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 (1–32 chars) — commands like `/Status` or `/my-cmd` are skipped from the menu with a warning but still resolve when typed.
|
||||||
|
|
||||||
|
|||||||
+21
-6
@@ -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`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user