diff --git a/bin/pos-ai-alias b/bin/pos-ai-alias new file mode 100755 index 0000000..f30611f --- /dev/null +++ b/bin/pos-ai-alias @@ -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:-}" + 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="" + 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]:-}" + 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 ─────────────────────────────────────────────── + +# (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 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 " + _alias_show "$2" + ;; + "") _alias_menu ;; + *) err "Unknown subcommand '$1' (use -h for help)" ;; +esac