Compare commits

..

8 Commits

Author SHA1 Message Date
Your Name 4fd3c37c40 update docs
gates / consistency-and-conventions (push) Failing after 16s
2026-08-26 07:28:03 -04:00
Your Name 5d7407e30f chore: update docmap + filetable for registry.sh addition 2026-08-26 06:23:35 -04:00
Your Name 6566c8343d chore: re-gen docs for pos-ai-alias addition 2026-08-26 06:02:10 -04:00
Your Name 9f289ba31b feat: pos ai alias — manage AI agent aliases
gates / consistency-and-conventions (push) Failing after 22s
- Create/edit/remove named aliases (provider + session + system prompt)
- Aliases stored in ai-aliases.env, generated ai-aliases.sh sourced by bashrc
- Interactive menu using lib/menu-lib.sh primitives
- Provider auto-discovered from lib/ai-providers/

Fix: _alias_find() return 1 crashed under set -e; changed to return 0
since -1 sentinel is the not-found signal, not the exit code.
2026-08-26 06:00:01 -04:00
Your Name a5c19e842d revert: remove e(dit) option from AI command prompt
gates / consistency-and-conventions (push) Successful in 1m40s
Keep only Y/n (run or skip). The edit feature was unreliable across
different terminal contexts (tee pipes, SSH, CLI). May revisit later.
2026-08-26 04:58:08 -04:00
Your Name d84a35efce fix: read -e -i stores into variable directly, not stdout
gates / consistency-and-conventions (push) Successful in 2m34s
edited="\$(read ...)" was always empty because read writes to a variable
name, not stdout. Changed to: read -e -p "Command: " -i "\$flat" edited
which stores directly into \$edited.
2026-08-26 04:42:18 -04:00
Your Name 9564880ebf fix: AI command edit - flatten multi-line for readline
gates / consistency-and-conventions (push) Successful in 1m47s
read -e -i only handles single-line text. Multi-line commands (docker
install etc) broke it. Now flattens newlines to spaces before pre-filling
the readline buffer. User sees a single editable line.
2026-08-26 04:37:52 -04:00
Your Name d0299d3f98 feat: AI command edit via clipboard + xdotool fallback
gates / consistency-and-conventions (push) Successful in 2m14s
- _inject_command tries: xclip/wl-copy (clipboard) -> xdotool (typing) -> tmux -> history
- Clipboard is primary: user pastes with Ctrl+Shift+V
- preinstall.sh: add xdotool and xclip to PACKAGES
2026-08-26 04:13:36 -04:00
7 changed files with 579 additions and 43 deletions
+21 -17
View File
@@ -10,19 +10,19 @@
<!-- GEN:START docmap -->
| ## 1. Project Overview | 2843 |
| ## 2. Directory Structure | 44199 |
| ## 3. Installation Flow | 200253 |
| ## 4. The `pos` CLI System | 254330 |
| ## 5. Shared Library — `lib/common.sh` | 331362 |
| ## 6. Docker Compose / ScaleTail | 363405 |
| ## 7. Optional Apps (`apps/`) | 406435 |
| ## 8. Entertainment Module | 436449 |
| ## 9. Systemd Services | 450461 |
| ## 10. Configuration Files | 462488 |
| ## 11. Coding Conventions | 489521 |
| ## 12. Development Workflow | 522574 |
| ## 13. Key File Quick Reference | 575644 |
| ## 14. Common Tasks for Agents | 645678 |
| ## 2. Directory Structure | 44200 |
| ## 3. Installation Flow | 201254 |
| ## 4. The `pos` CLI System | 255332 |
| ## 5. Shared Library — `lib/common.sh` | 333364 |
| ## 6. Docker Compose / ScaleTail | 365407 |
| ## 7. Optional Apps (`apps/`) | 408437 |
| ## 8. Entertainment Module | 438451 |
| ## 9. Systemd Services | 452463 |
| ## 10. Configuration Files | 464490 |
| ## 11. Coding Conventions | 491523 |
| ## 12. Development Workflow | 524576 |
| ## 13. Key File Quick Reference | 577648 |
| ## 14. Common Tasks for Agents | 649682 |
<!-- GEN:END docmap -->
## 1. Project Overview
@@ -61,6 +61,7 @@ Linux_post_install/
├── bin/ # CLI tools — installed to /usr/local/bin/
│ ├── pos # Main dispatcher — smart arg matching to pos-* scripts
<!-- GEN:START tree -->
│ ├── pos-ai-alias # manage AI agent aliases
│ ├── pos-ai-gemini # Forward to pos ai --provider gemini (backward compat)
│ ├── pos-ai-openrouter # Forward to pos ai --provider openrouter (backward compat)
│ ├── pos-communication-matrix-listener # Matrix listener: map /command → bash, run them on room messages
@@ -207,7 +208,7 @@ User runs: ./install.sh [--apps|--full|--feature|--dry-run|--skip <phase>|--step
├─ Phase 2: install.sh (requires root)
│ └─ Copies bin/* → /usr/local/bin/ (chmod 755)
│ └─ Copies lib/*.sh (common, flags, notify, entertainment-lib,
│ └─ Copies lib/*.sh (common, flags, notify, registry, entertainment-lib,
│ scheduler-lib, config-ui, user-timers-lib, entertainment-plugin-lib,
│ usb-lib, share-lib, menu-lib) → /usr/local/bin/ (chmod 644)
│ └─ Copies x64_bin/* → /usr/local/bin/ on x86_64 (arm64_bin/ on aarch64)
@@ -270,6 +271,7 @@ All non-interactive `pos` commands log output to `~/.local/share/linux_post_inst
| Category | Command | Script | Description |
|----------|---------|--------|-------------|
<!-- GEN:START dispatch -->
| ai | alias | `pos-ai-alias` | manage AI agent aliases |
| ai | gemini | `pos-ai-gemini` | Forward to pos ai --provider gemini (backward compat) |
| ai | openrouter | `pos-ai-openrouter` | Forward to pos ai --provider openrouter (backward compat) |
| communication | matrix-listener | `pos-communication-matrix-listener` | Matrix listener: map /command → bash, run them on room messages |
@@ -589,6 +591,7 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`
| `lib/usb-lib.sh` | 205 | Shared USB-storage detection + pick flow (detect/mount-offer/`usb_pick_root`; EFI system partitions excluded; picker shows size/label/fs) — used by `pos system backup` + `pos media sync` |
| `lib/share-lib.sh` | 318 | Domain layer for the share suite (usbsrv/smbclient record parsers, folder+mountpoint candidates, remote listings, service/firewall advisories; EOF-safe) + compat shims to `lib/menu-lib.sh` — used by all five `pos share *` tools |
| `lib/menu-lib.sh` | 169 | Category-neutral interactive menu primitives (`menu_guard` tty guard, `menu_run` looping boxed menu, `menu_pick` type-to-filter picker, `menu_ask_value` prompt-with-default; stderr render, fail-closed on non-tty/EOF) — sourced by `share-lib.sh`, open to any category |
| `lib/registry.sh` | 199 | Shared query API for POS tool metadata headers (`# POS_*:`) — `reg_scan`/`reg_list`/`reg_lookup`/`reg_each`/config scope helpers; used by `pos-tree` and `gen-docs.sh` |
| `bin/flag-reader` | 58 | Inspect flags (list/status/`--raw`) |
| `bin/flag-set` | 21 | Set a flag (optionally with a value) |
| `bin/flag-clear` | 21 | Unset a flag |
@@ -596,6 +599,7 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`
| `features/usb-automount.sh` | 138 | USB automount feature (udev rule + flag-gated service) |
<!-- GEN:START filetable -->
| `bin/pos` | 295 | CLI dispatcher with smart arg matching + logging + category help |
| `bin/pos-ai-alias` | 542 | manage AI agent aliases |
| `bin/pos-ai-gemini` | 7 | Forward to pos ai --provider gemini (backward compat) |
| `bin/pos-ai-openrouter` | 7 | Forward to pos ai --provider openrouter (backward compat) |
| `bin/pos-communication-matrix-listener` | 568 | Matrix listener: map /command → bash, run them on room messages |
@@ -633,10 +637,10 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`
| `bin/pos-system-health` | 209 | Host health dashboard (disk, RAM, services, backup age, fail2ban, docker); exit 1 if any FAIL |
| `bin/pos-system-schedule` | 151 | Scheduled jobs: run a command on a timer; notify on threshold/change/error/always or silently |
| `bin/pos-system-uninstall` | 415 | Remove pos toolkit binaries, services, shell integration, config, and data |
| `bin/pos-ai` | 702 | AI assistant: ask, chat, sessions, capture, models, providers |
| `bin/pos-ai` | 680 | 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-tree` | 112 | Show the pos CLI command tree: categories, commands, and subcommands |
| `completions/pos.bash` | 306 | Dynamic bash completion |
| `bin/pos-tree` | 118 | Show the pos CLI command tree: categories, commands, and subcommands |
| `completions/pos.bash` | 307 | Dynamic bash completion |
<!-- GEN:END filetable -->
| `apps/install.sh` | 171 | App install/uninstall picker/orchestrator |
+4 -2
View File
@@ -31,7 +31,7 @@ Each phase is independent and runs only if the corresponding script exists.
|-----------|---------|-------------|
| `bin/` | Daily-use CLI tools and wrappers | `/usr/local/bin/` |
| `apps/<category>/` | Optional desktop app installers | run on demand |
| `lib/` | Shared libraries: `common.sh` (helpers), `flags.sh` (feature flags), `notify.sh` (multi-platform alerting), `entertainment-lib.sh` (entertainment scheduling + last-run state), `entertainment-plugin-lib.sh` (message-safe plugin helpers), `scheduler-lib.sh` (system scheduler), `user-timers-lib.sh` (shared systemd user timer machinery), `config-ui.sh` (interactive config UI), `menu-lib.sh` (category-neutral menu primitives: guard/looping menu/filter picker/prompt), `share-lib.sh` (share-suite domain probes/listings + compat shims to menu-lib) | sourced at build time |
| `lib/` | Shared libraries: `common.sh` (helpers), `flags.sh` (feature flags), `notify.sh` (multi-platform alerting), `registry.sh` (shared query API for POS tool metadata headers), `entertainment-lib.sh` (entertainment scheduling + last-run state), `entertainment-plugin-lib.sh` (message-safe plugin helpers), `scheduler-lib.sh` (system scheduler), `user-timers-lib.sh` (shared systemd user timer machinery), `config-ui.sh` (interactive config UI), `menu-lib.sh` (category-neutral menu primitives: guard/looping menu/filter picker/prompt), `share-lib.sh` (share-suite domain probes/listings + compat shims to menu-lib) | sourced at build time |
| `config/` | Gitignored user config files | `~/.config/<app>/` (via postinstall) |
| `entertainment/` | Public-API plugins for the entertainment module | `/usr/local/bin` (via install.sh Phase 2) |
| `compose/` | ScaleTail templates (git submodule) | `/usr/local/share/linux_post_install/scale-tail` |
@@ -128,8 +128,10 @@ esac
# POS: <category> <command> — one-line description rendered by `make gen`
# POS_FLAGS: --flag1 --flag2 # ONLY for flag-style tools
# POS_SUBCMDS: sub1 sub2 # ONLY for multi-command tools
# POS_DEPS: binary1 binary2 # Optional: runtime deps (space-separated binary names)
# POS_EXAMPLES: pos <tool> <args> | Description # Optional: usage examples
```
The description feeds the dispatch table, bin tree and file table in `DOC/AGENT_Context_Project.md`; `POS_FLAGS` feeds flag completion and `POS_SUBCMDS` feeds subcommand completion in `completions/pos.bash` (both update via `make gen`). `make gen` only reads the text after the first `` — the `<category> <command>` words before it are convention-only (for nested tools, keep the full path there, e.g. `# POS: communication telegram-listener — …`).
The description feeds the dispatch table, bin tree and file table in `DOC/AGENT_Context_Project.md`; `POS_FLAGS` feeds flag completion and `POS_SUBCMDS` feeds subcommand completion in `completions/pos.bash` (both update via `make gen`). `POS_DEPS` lists runtime binary names that `command -v` would check — use when the tool requires specific binaries beyond what `preinstall.sh` installs. `POS_EXAMPLES` provides curated usage examples (one per line, pipe-delimited `command | description`) shown in `pos tree` and future help views. Both are optional and degrade gracefully when absent. `make gen` only reads the text after the first `` — the `<category> <command>` words before it are convention-only (for nested tools, keep the full path there, e.g. `# POS: communication telegram-listener — …`).
- **Category-less vs categorized:** most tools are `bin/pos-<category>-<command>`. Use category-less `bin/pos-<cmd>` (e.g. `pos-config`, `pos-tree`) only for dispatcher/dev-level commands that fit no category — they dispatch and document like any tool but show with an empty category in the generated tables.
- Nested tools (e.g. `bin/pos-communication-telegram-listener`) are auto-detected from filenames: the trailing segment (`listener`) is offered as a subcommand of the parent tool (`communication-telegram`) in `pos <category> --help` and tab-completion, instead of appearing as a flat sibling (`telegram-listener`). The flat dash-form (`pos communication telegram-listener`) still dispatches.
- Optionally add an EXAMPLES line in `bin/pos` `usage()` to showcase the tool in `pos --help`.
+8
View File
@@ -70,6 +70,14 @@ Category-less tools (`config`, `tree`) live outside any category and are documen
| `pos ai providers` | Lists available providers, their config status, and the active provider |
| `pos ai --model <id> …` | Overrides the model for one invocation |
| `pos ai --provider <name> …` | Selects the provider for one invocation (gemini\|openrouter) |
| `pos ai alias` | Interactive alias manager (`bin/pos-ai-alias`): menu loop (create / edit / remove / list) that shows the alias table (Name/Provider/Session/Prompt, prompts truncated) between picks |
| `pos ai alias create [name]` | Interactive 4-step wizard: alias name (leading letter, then letters/digits/-/_; unique across aliases), provider pick (from installed `lib/ai-providers/*.sh` adapters), session name (defaults to the alias name), optional system prompt (must not contain `\|`; warns above 500 chars); confirm defaults to yes, then the alias is saved |
| `pos ai alias edit [name]` | Edits an existing alias (pick from list or pass the name): provider/session/prompt are re-prompted pre-filled with the current values — Enter keeps the current value; a per-field changed/unchanged summary is confirmed (default yes) before saving; nothing is written if nothing changed |
| `pos ai alias remove [name]` | Removes an alias (pick from list or pass the name); the confirmation defaults to **no** and removal cannot be undone |
| `pos ai alias list` | Non-interactive: prints all aliases as a Name/Provider/Session/Prompt table (prompts truncated at 42 chars) |
| `pos ai alias show <name>` | Prints one alias's details including the resolved command: `pos ai <provider> ask --session <session>[ --system '<prompt>']` |
Alias storage: records live in `~/.config/linux_post_install/ai-aliases.env` — one `name\|provider\|session\|system_prompt` line per alias, chmod 600, managed by the tool (do not hand-edit). Every create/edit/remove also rewrites `~/.config/linux_post_install/ai-aliases.sh` (chmod 644), which defines one shell alias per record — `alias <name>='pos ai <provider> ask --session <session>[ --system '<prompt>']'`. An empty session falls back to the alias name, prompts are single-quote-escaped, and the generated file is syntax-checked before it replaces the previous version. Source this file from your shell rc (e.g. `.bashrc`) to activate the aliases in new shells.
Backward compatibility: `pos ai gemini` and `pos ai openrouter` still work as shorthands for `pos ai --provider gemini` and `pos ai --provider openrouter`.
+1 -23
View File
@@ -376,7 +376,7 @@ _prompt_run_command() {
[ -w /dev/tty ] || return 0
printf '\n%s\n' "Command detected:" >&2
printf ' %s\n\n' "$cmd" >&2
printf 'Run this command? [Y/n/e(dit)] ' >&2
printf 'Run this command? [Y/n] ' >&2
local choice
IFS= read -r choice </dev/tty || choice=""
case "${choice,,}" in
@@ -385,9 +385,6 @@ _prompt_run_command() {
history -s "$cmd" 2>/dev/null || true
printf '%s\n' "Command added to history — press ↑ to recall, edit, and run." >&2
;;
e|E)
_inject_command "$cmd"
;;
*)
# Y or Enter: execute
printf '%s\n' "$cmd"
@@ -396,25 +393,6 @@ _prompt_run_command() {
esac
}
# Simulate keyboard input to paste command onto the active terminal line.
# Tries: xdotool (X11/Wayland) → tmux send-keys → history fallback.
_inject_command() {
local cmd="$1"
if command -v xdotool >/dev/null 2>&1 && [ -n "${DISPLAY:-}${WAYLAND_DISPLAY:-}" ]; then
# xdotool: simulate typing into the focused terminal
printf '%s\n' "Typing command — edit if needed, press Enter to run." >&2
printf '%s' "$cmd" | xdotool type --clearmodifiers --file -
elif [ -n "${TMUX:-}" ]; then
# tmux: send keys directly into the pane
printf '%s\n' "Typing command — edit if needed, press Enter to run." >&2
tmux send-keys "$cmd"
else
# Fallback: add to history, user presses ↑ to recall
history -s "$cmd" 2>/dev/null || true
printf '%s\n' "Command added to history — press ↑ to recall, edit, and run." >&2
fi
}
# ── Machine context appended to the built-in default prompt ─────
mc_clean() {
sed -e 's/\x1b\[[0-9;]*[A-Za-z]//g' \
+542
View File
@@ -0,0 +1,542 @@
#!/usr/bin/env bash
set -euo pipefail
# POS: ai alias — manage AI agent aliases
# POS_SUBCMDS: create edit remove list show
source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh"
source "$(dirname "$0")/../lib/menu-lib.sh" 2>/dev/null || source "$(dirname "$0")/menu-lib.sh"
# ── Paths & constants ──────────────────────────────────────────
ENV_FILE="${CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/linux_post_install}/ai-aliases.env"
SH_FILE="${CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/linux_post_install}/ai-aliases.sh"
# Provider discovery — same pattern as bin/pos-ai (lines 17-18)
PROVIDER_DIR="$(dirname "$0")/../lib/ai-providers"
[ -d "$PROVIDER_DIR" ] || PROVIDER_DIR="$(dirname "$0")/ai-providers"
# ── Core helpers ───────────────────────────────────────────────
_alias_load() {
_ALIAS_NAMES=(); _ALIAS_PROVIDERS=(); _ALIAS_SESSIONS=(); _ALIAS_PROMPTS=()
[ -f "$ENV_FILE" ] || return 0
while IFS='|' read -r name provider session prompt _rest; do
[[ "$name" =~ ^[[:space:]]*# ]] && continue
[[ -z "${name// /}" ]] && continue
name="${name## }"; name="${name%% }"
[[ "$name" =~ ^[a-zA-Z][a-zA-Z0-9_-]*$ ]] || continue
provider="${provider## }"; provider="${provider%% }"
session="${session## }"; session="${session%% }"
_ALIAS_NAMES+=("$name")
_ALIAS_PROVIDERS+=("$provider")
_ALIAS_SESSIONS+=("$session")
_ALIAS_PROMPTS+=("$prompt")
done < <(grep -v '^[[:space:]]*#' "$ENV_FILE" | grep -v '^[[:space:]]*$' || true)
}
_alias_save() {
mkdir -p "$(dirname "$ENV_FILE")"
{
printf '%s\n' "# AI aliases — managed by pos ai alias (do not hand-edit)"
printf '%s\n' "# Format: alias_name|provider|session_name|system_prompt"
printf '%s\n' "#"
local i
for ((i = 0; i < ${#_ALIAS_NAMES[@]}; i++)); do
printf '%s|%s|%s|%s\n' "${_ALIAS_NAMES[$i]}" "${_ALIAS_PROVIDERS[$i]}" \
"${_ALIAS_SESSIONS[$i]}" "${_ALIAS_PROMPTS[$i]}"
done
} >"$ENV_FILE"
chmod 600 "$ENV_FILE"
}
_alias_regen() {
local tmp
tmp="$(mktemp)"
printf '%s\n' "#!/usr/bin/env bash" >"$tmp"
printf '%s\n\n' "# Auto-generated by pos ai alias — do not hand-edit." >>"$tmp"
if [ -f "$ENV_FILE" ]; then
while IFS='|' read -r name provider session prompt _rest; do
[[ "$name" =~ ^[[:space:]]*# ]] && continue
[[ -z "${name// /}" ]] && continue
name="${name## }"; name="${name%% }"
[[ "$name" =~ ^[a-zA-Z][a-zA-Z0-9_-]*$ ]] || continue
provider="${provider## }"; provider="${provider%% }"
session="${session## }"; session="${session%% }"
[ -z "$session" ] && session="$name"
local escaped_prompt="${prompt//\'/\'\\\'\'}"
printf "alias %s='pos ai %s ask --session %s" "$name" "$provider" "$session" >>"$tmp"
if [ -n "$prompt" ]; then
printf " --system '%s'" "$escaped_prompt" >>"$tmp"
fi
printf "'\n" >>"$tmp"
done < <(grep -v '^[[:space:]]*#' "$ENV_FILE" | grep -v '^[[:space:]]*$' || true)
fi
if ! bash -n "$tmp" 2>/dev/null; then
warn "Generated alias file has syntax errors — keeping previous version"
rm -f "$tmp"
return 1
fi
mv "$tmp" "$SH_FILE"
chmod 644 "$SH_FILE"
}
_alias_provider_pick() {
local providers=()
for f in "$PROVIDER_DIR"/*.sh; do
[ -f "$f" ] || continue
providers+=("$(basename "$f" .sh)")
done
if [ ${#providers[@]} -eq 0 ]; then
err "No AI providers installed — run 'pos ai' setup first"
fi
menu_pick "Pick provider" "${providers[@]}"
}
_alias_find() {
local name="$1" i
for ((i = 0; i < ${#_ALIAS_NAMES[@]}; i++)); do
if [ "${_ALIAS_NAMES[$i]}" = "$name" ]; then
echo "$i"
return 0
fi
done
echo "-1"
return 0
}
_alias_name_valid() {
[[ "$1" =~ ^[a-zA-Z][a-zA-Z0-9_-]*$ ]]
}
_alias_prompt_truncate() {
local p="$1"
if [ ${#p} -gt 42 ]; then
printf '%s…' "${p:0:42}"
else
printf '%s' "$p"
fi
}
# ── Non-interactive output ─────────────────────────────────────
_alias_list() {
_alias_load
local count=${#_ALIAS_NAMES[@]}
printf 'Aliases (%d):\n' "$count"
[ "$count" -eq 0 ] && return 0
printf ' %-12s %-12s %-12s %s\n' "Name" "Provider" "Session" "Prompt"
printf ' %-12s %-12s %-12s %s\n' "------------" "------------" "------------" \
"------------------------------------------"
local i
for ((i = 0; i < count; i++)); do
printf ' %-12s %-12s %-12s %s\n' "${_ALIAS_NAMES[$i]}" "${_ALIAS_PROVIDERS[$i]}" \
"${_ALIAS_SESSIONS[$i]}" "$(_alias_prompt_truncate "${_ALIAS_PROMPTS[$i]}")"
done
}
_alias_show() {
_alias_load
local idx
idx="$(_alias_find "$1")"
[ "$idx" = "-1" ] && err "Alias '$1' not found"
local name="${_ALIAS_NAMES[$idx]}" provider="${_ALIAS_PROVIDERS[$idx]}"
local session="${_ALIAS_SESSIONS[$idx]}" prompt="${_ALIAS_PROMPTS[$idx]}"
[ -z "$session" ] && session="$name"
printf ' %-12s %s\n' "Alias:" "$name"
printf ' %-12s %s\n' "Provider:" "$provider"
printf ' %-12s %s\n' "Session:" "$session"
printf ' %-12s %s\n' "Prompt:" "${prompt:-$(printf '%s' "(default)")}"
# Show the resolved command
local cmd="pos ai $provider ask --session $session"
if [ -n "$prompt" ]; then
local escaped="${prompt//\'/\'\\\'\'}"
cmd="$cmd --system '$escaped'"
fi
printf ' %-12s %s\n' "Command:" "$cmd"
}
# ── Interactive: main menu ─────────────────────────────────────
_alias_menu() {
menu_guard || return 1
while true; do
{
_alias_load
if [ ${#_ALIAS_NAMES[@]} -eq 0 ]; then
echo "${YELLOW}[!] No aliases defined yet — create one with option 1.${RESET}"
else
local count=${#_ALIAS_NAMES[@]} i
printf ' %-12s %-12s %-12s %s\n' "Name" "Provider" "Session" "Prompt"
printf ' %-12s %-12s %-12s %s\n' "------------" "------------" "------------" \
"------------------------------------------"
for ((i = 0; i < count; i++)); do
local p="${_ALIAS_PROMPTS[$i]}"
if [ ${#p} -gt 42 ]; then
p="${p:0:42}…"
fi
printf ' %-12s %-12s %-12s %s\n' "${_ALIAS_NAMES[$i]}" "${_ALIAS_PROVIDERS[$i]}" \
"${_ALIAS_SESSIONS[$i]}" "$p"
done
printf ' %-12s %-12s %-12s %s\n' "------------" "------------" "------------" \
"------------------------------------------"
printf ' %d alias(es)\n' "$count"
fi
echo >&2
} >&2
local choice
choice="$(menu_run "AI Agent Aliases" "Create new alias" "Edit existing alias" \
"Remove alias" "List aliases")" || return 0
case "$choice" in
1) _alias_create ;;
2) _alias_edit ;;
3) _alias_remove ;;
4) _alias_list ;;
esac
done
}
# ── Interactive: create ────────────────────────────────────────
_alias_create() {
local preset_name="${1:-}"
section "Create AI Agent Alias" >&2
# Step 1: Alias name
local name="$preset_name"
while true; do
if [ -z "$name" ]; then
step 1 4 "Alias Name" >&2
name="$(menu_ask_value "Alias name" "")" || return 0
fi
[ -z "$name" ] && { warn "Alias name cannot be empty" >&2; name=""; continue; }
if ! _alias_name_valid "$name"; then
warn "Invalid name '$name' — use letters, digits, hyphens, underscores (start with a letter)" >&2
name=""; continue
fi
_alias_load
local existing
existing="$(_alias_find "$name")"
if [ "$existing" != "-1" ]; then
warn "Alias '$name' already exists — use 'pos ai alias edit $name' instead" >&2
[ -n "$preset_name" ] && return 1
name=""; continue
fi
break
done
# Step 2: Provider
step 2 4 "Provider" >&2
local pidx
pidx="$(_alias_provider_pick)" || return 0
local providers=()
for f in "$PROVIDER_DIR"/*.sh; do
[ -f "$f" ] || continue
providers+=("$(basename "$f" .sh)")
done
local provider="${providers[$((pidx - 1))]}"
# Step 3: Session name
local session=""
while true; do
step 3 4 "Session Name" >&2
session="$(menu_ask_value "Session name" "$name")" || return 0
if [ -n "$session" ] && ! _alias_name_valid "$session"; then
warn "Invalid session '$session' — use letters, digits, hyphens, underscores" >&2
session=""; continue
fi
break
done
[ -z "$session" ] && session="$name"
# Step 4: System prompt
local prompt=""
while true; do
step 4 4 "System Prompt" >&2
prompt="$(menu_ask_value "System prompt (empty = use built-in)" "")" || return 0
if [[ "$prompt" == *'|'* ]]; then
warn "System prompt must not contain '|' characters" >&2
prompt=""; continue
fi
if [ ${#prompt} -gt 500 ]; then
warn "Prompt is ${#prompt} chars — consider keeping it concise" >&2
fi
break
done
# Confirmation
{
echo "────────────────────────────────────────────"
printf ' Create alias '\''%s'\''?\n' "$name"
printf ' Provider: %s\n' "$provider"
printf ' Session: %s\n' "$session"
local dp="$prompt"
[ ${#dp} -gt 50 ] && dp="${dp:0:50}…"
printf ' Prompt: %s\n' "${dp:-<built-in>}"
echo "────────────────────────────────────────────"
} >&2
if ! confirm "Create alias '$name'?" y; then
log "Aborted." >&2
return 0
fi
_alias_load
_ALIAS_NAMES+=("$name")
_ALIAS_PROVIDERS+=("$provider")
_ALIAS_SESSIONS+=("$session")
_ALIAS_PROMPTS+=("$prompt")
_alias_save
_alias_regen
log "Alias '$name' created. Reload shell: source ~/.bashrc" >&2
}
# ── Interactive: edit ──────────────────────────────────────────
_alias_edit() {
local preset_name="${1:-}"
_alias_load
if [ ${#_ALIAS_NAMES[@]} -eq 0 ]; then
warn "No aliases to edit — create one first" >&2
return 0
fi
local name="$preset_name"
if [ -z "$name" ]; then
section "Edit AI Agent Alias" >&2
local display_items=() i
for ((i = 0; i < ${#_ALIAS_NAMES[@]}; i++)); do
local p="${_ALIAS_PROMPTS[$i]}"
if [ ${#p} -gt 30 ]; then
p="${p:0:30}…"
fi
display_items+=("${_ALIAS_NAMES[$i]} [${_ALIAS_PROVIDERS[$i]}] ${p}")
done
local picked
picked="$(menu_pick "Pick alias to edit" "${display_items[@]}")" || return 0
name="${_ALIAS_NAMES[$((picked - 1))]}"
fi
local idx
idx="$(_alias_find "$name")"
if [ "$idx" = "-1" ]; then
err "Alias '$name' not found"
fi
# Show current values
{
echo " Current values for '$name':"
printf ' Provider: %s\n' "${_ALIAS_PROVIDERS[$idx]}"
printf ' Session: %s\n' "${_ALIAS_SESSIONS[$idx]}"
local cp="${_ALIAS_PROMPTS[$idx]}"
[ -z "$cp" ] && cp="(default)"
printf ' Prompt: %s\n' "$cp"
echo >&2
} >&2
local new_provider="${_ALIAS_PROVIDERS[$idx]}"
local new_session="${_ALIAS_SESSIONS[$idx]}"
local new_prompt="${_ALIAS_PROMPTS[$idx]}"
local changed=0
# Edit provider
step 1 3 "Provider" >&2
local pidx
pidx="$(_alias_provider_pick)" || return 0
local providers=()
for f in "$PROVIDER_DIR"/*.sh; do
[ -f "$f" ] || continue
providers+=("$(basename "$f" .sh)")
done
local picked_provider="${providers[$((pidx - 1))]}"
if [ "$picked_provider" != "$new_provider" ]; then
new_provider="$picked_provider"
changed=1
fi
# Edit session
local tmp_session=""
while true; do
step 2 3 "Session Name" >&2
tmp_session="$(menu_ask_value "Session name" "$new_session")" || return 0
if [ -n "$tmp_session" ] && ! _alias_name_valid "$tmp_session"; then
warn "Invalid session '$tmp_session' — use letters, digits, hyphens, underscores" >&2
tmp_session=""; continue
fi
break
done
[ -n "$tmp_session" ] && new_session="$tmp_session"
[ "$new_session" != "${_ALIAS_SESSIONS[$idx]}" ] && changed=1
# Edit prompt
local tmp_prompt=""
while true; do
step 3 3 "System Prompt" >&2
local default_prompt="${_ALIAS_PROMPTS[$idx]}"
[ ${#default_prompt} -gt 80 ] && default_prompt="${default_prompt:0:80}…"
[ -z "$default_prompt" ] && default_prompt=""
tmp_prompt="$(menu_ask_value "System prompt" "$default_prompt")" || return 0
if [[ "$tmp_prompt" == *'|'* ]]; then
warn "System prompt must not contain '|' characters" >&2
tmp_prompt=""; continue
fi
if [ ${#tmp_prompt} -gt 500 ]; then
warn "Prompt is ${#tmp_prompt} chars — consider keeping it concise" >&2
fi
break
done
# Keep full current if user pressed Enter (tmp_prompt = default_prompt value)
if [ -n "$tmp_prompt" ]; then
new_prompt="$tmp_prompt"
fi
[ "$new_prompt" != "${_ALIAS_PROMPTS[$idx]}" ] && changed=1
# No changes?
if [ "$changed" -eq 0 ]; then
log "No changes — nothing to save." >&2
return 0
fi
# Show diff summary
{
echo "────────────────────────────────────────────"
printf ' Save changes to '\''%s'\''?\n' "$name"
local tag_p tag_s tag_pr
[ "$new_provider" = "${_ALIAS_PROVIDERS[$idx]}" ] && tag_p="(unchanged)" || tag_p="(changed)"
[ "$new_session" = "${_ALIAS_SESSIONS[$idx]}" ] && tag_s="(unchanged)" || tag_s="(changed)"
[ "$new_prompt" = "${_ALIAS_PROMPTS[$idx]}" ] && tag_pr="(unchanged)" || tag_pr="(changed)"
printf ' Provider: %-12s %s\n' "$new_provider" "$tag_p"
printf ' Session: %-12s %s\n' "$new_session" "$tag_s"
local dp="$new_prompt"
[ -z "$dp" ] && dp="<built-in>"
printf ' Prompt: %s %s\n' "${dp:0:40}" "$tag_pr"
echo "────────────────────────────────────────────"
} >&2
if ! confirm "Save changes to '$name'?" y; then
log "Discarded." >&2
return 0
fi
_alias_load
_ALIAS_PROVIDERS[$idx]="$new_provider"
_ALIAS_SESSIONS[$idx]="$new_session"
_ALIAS_PROMPTS[$idx]="$new_prompt"
_alias_save
_alias_regen
log "Alias '$name' updated." >&2
}
# ── Interactive: remove ────────────────────────────────────────
_alias_remove() {
local preset_name="${1:-}"
_alias_load
if [ ${#_ALIAS_NAMES[@]} -eq 0 ]; then
warn "No aliases to remove" >&2
return 0
fi
local name="$preset_name"
if [ -z "$name" ]; then
section "Remove AI Agent Alias" >&2
local display_items=() i
for ((i = 0; i < ${#_ALIAS_NAMES[@]}; i++)); do
local p="${_ALIAS_PROMPTS[$i]}"
if [ ${#p} -gt 30 ]; then
p="${p:0:30}…"
fi
display_items+=("${_ALIAS_NAMES[$i]} [${_ALIAS_PROVIDERS[$i]}] ${p}")
done
local picked
picked="$(menu_pick "Pick alias to remove" "${display_items[@]}")" || return 0
name="${_ALIAS_NAMES[$((picked - 1))]}"
fi
local idx
idx="$(_alias_find "$name")"
if [ "$idx" = "-1" ]; then
err "Alias '$name' not found"
fi
# Show alias detail
{
echo " Alias: $name"
printf ' Provider: %s\n' "${_ALIAS_PROVIDERS[$idx]}"
printf ' Session: %s\n' "${_ALIAS_SESSIONS[$idx]}"
printf ' Prompt: %s\n' "${_ALIAS_PROMPTS[$idx]:-<built-in>}"
echo >&2
} >&2
if ! confirm "Remove alias '$name'? This cannot be undone." n; then
log "Cancelled." >&2
return 0
fi
_alias_load
local new_names=() new_providers=() new_sessions=() new_prompts=() i
for ((i = 0; i < ${#_ALIAS_NAMES[@]}; i++)); do
if [ "${_ALIAS_NAMES[$i]}" != "$name" ]; then
new_names+=("${_ALIAS_NAMES[$i]}")
new_providers+=("${_ALIAS_PROVIDERS[$i]}")
new_sessions+=("${_ALIAS_SESSIONS[$i]}")
new_prompts+=("${_ALIAS_PROMPTS[$i]}")
fi
done
_ALIAS_NAMES=("${new_names[@]+"${new_names[@]}"}")
_ALIAS_PROVIDERS=("${new_providers[@]+"${new_providers[@]}"}")
_ALIAS_SESSIONS=("${new_sessions[@]+"${new_sessions[@]}"}")
_ALIAS_PROMPTS=("${new_prompts[@]+"${new_prompts[@]}"}")
_alias_save
_alias_regen
log "Alias '$name' removed." >&2
}
# ── show <name> ───────────────────────────────────────────────
# (defined above as _alias_show)
# ── Usage ──────────────────────────────────────────────────────
usage() {
cat <<'EOF'
Usage: pos ai alias [subcommand] [args]
Manage named AI agent aliases — create, edit, remove, list, and show
configured aliases. Each alias maps a name to a provider, session, and
optional system prompt.
Subcommands:
(no args) Interactive menu
create [name] Create a new alias (interactive prompts for each field)
edit [name] Edit an existing alias (interactive, Enter = keep)
remove [name] Remove an alias (interactive, default = no)
list List all aliases (non-interactive, machine-readable)
show <name> Show one alias's details
Options:
-h|--help Show this help.
Examples:
pos ai alias # interactive menu
pos ai alias list # show all aliases
pos ai alias create # interactive create
pos ai alias create mybot # create 'mybot' alias
pos ai alias edit mybot # edit the 'mybot' alias
pos ai alias remove mybot # remove 'mybot' (with confirm)
pos ai alias show mybot # show alias details
EOF
exit 0
}
# ── Main dispatch ──────────────────────────────────────────────
case "${1:-}" in
-h|--help) usage ;;
create) shift; _alias_create "${1:-}" ;;
edit) shift; _alias_edit "${1:-}" ;;
remove) shift; _alias_remove "${1:-}" ;;
list) _alias_list ;;
show)
[ -n "${2:-}" ] || err "Usage: pos ai alias show <name>"
_alias_show "$2"
;;
"") _alias_menu ;;
*) err "Unknown subcommand '$1' (use -h for help)" ;;
esac
+2 -1
View File
@@ -25,6 +25,7 @@ _pos_flags[tree]="--depth"
# GEN:END posflags
# GEN:START possubcmds
declare -A _pos_subcmds
_pos_subcmds[ai-alias]="create edit remove list show"
_pos_subcmds[ai-gemini]="ask chat models sessions capture"
_pos_subcmds[ai-openrouter]="ask chat sessions capture"
_pos_subcmds[communication-matrix-sender]="send test login"
@@ -41,7 +42,7 @@ _pos_subcmds[share-smb-client]="mount unmount list persist unpersist menu"
_pos_subcmds[share-smb-server]="status share unshare list adduser deluser reload enable disable menu"
_pos_subcmds[system-backup]="menu"
_pos_subcmds[system-schedule]="run list config enable disable status migrate menu"
_pos_subcmds[ai]="ask chat sessions capture models providers gemini openrouter"
_pos_subcmds[ai]="ask chat sessions capture models providers alias gemini openrouter"
# GEN:END possubcmds
# GEN:START posconfigscopes
declare -a _pos_config_scopes=(ai compose entertainment matrix notify scrcpy system telegram ytsync)
+1
View File
@@ -39,6 +39,7 @@ PACKAGES=(
python3 python3-pip rclone
ffmpeg
libqrencode4 libgtk-3-0 adb
xdotool xclip
)
spawn "apt update" sudo apt update