300b742ac8
gates / consistency-and-conventions (push) Successful in 1m29s
Add an optional5th 'trusted' field to aliases (name|provider|session|prompt|trusted). Trusted aliases pass --trust to pos ai, which makes _prompt_run_command auto-execute the agent's detected commands without the Y/n confirmation (command still printed for audit). - bin/pos-ai: new --trust global flag; _prompt_run_command takes trusted arg and skips the prompt when set; POS_FLAGS + usage updated - bin/pos-ai-alias: _ALIAS_TRUSTED array, 5-field env format (backward compat: missing field defaults to untrusted), Trust column in table, trust row in show, trust step (5/5) in create wizard with security warning, trust toggle (4/4) with diff tag in edit wizard, wrapper scripts get --trust when alias is trusted - completions/pos.bash + gen docs updated Gates: make gen && make check && make lint = 0 FAIL, 0 WARN
713 lines
27 KiB
Bash
Executable File
713 lines
27 KiB
Bash
Executable File
#!/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=()
|
|
_ALIAS_TRUSTED=()
|
|
[ -f "$ENV_FILE" ] || return 0
|
|
# NOTE: loop vars use _l* prefix to avoid dynamic-scope collision with
|
|
# callers that declare 'local name' (bash read clobbers the nearest
|
|
# matching variable up the call chain).
|
|
local _ln _lp _ls _lp2 _lr
|
|
while IFS='|' read -r _ln _lp _ls _lp2 _lr; do
|
|
[[ "$_ln" =~ ^[[:space:]]*# ]] && continue
|
|
[[ -z "${_ln// /}" ]] && continue
|
|
_ln="${_ln## }"; _ln="${_ln%% }"
|
|
[[ "$_ln" =~ ^[a-zA-Z][a-zA-Z0-9_-]*$ ]] || continue
|
|
_lp="${_lp## }"; _lp="${_lp%% }"
|
|
_ls="${_ls## }"; _ls="${_ls%% }"
|
|
_ALIAS_NAMES+=("$_ln")
|
|
_ALIAS_PROVIDERS+=("$_lp")
|
|
_ALIAS_SESSIONS+=("$_ls")
|
|
_ALIAS_PROMPTS+=("$_lp2")
|
|
_ALIAS_TRUSTED+=("${_lr:-0}")
|
|
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|trusted"
|
|
printf '%s\n' "#"
|
|
local i
|
|
for ((i = 0; i < ${#_ALIAS_NAMES[@]}; i++)); do
|
|
printf '%s|%s|%s|%s|%s\n' "${_ALIAS_NAMES[$i]}" "${_ALIAS_PROVIDERS[$i]}" \
|
|
"${_ALIAS_SESSIONS[$i]}" "${_ALIAS_PROMPTS[$i]}" "${_ALIAS_TRUSTED[$i]:-0}"
|
|
done
|
|
} >"$ENV_FILE"
|
|
chmod 600 "$ENV_FILE"
|
|
}
|
|
|
|
# Build the pos-ai command for an alias with the prompt safely quoted as
|
|
# ONE shell word (printf %q) — shared by regen (stored form) and show
|
|
# (copy-pasteable display form). Empty prompt → no --system fragment.
|
|
_alias_quote_cmd() {
|
|
local provider="$1" session="$2" prompt="${3:-}" trusted="${4:-0}" q_prompt
|
|
printf -v q_prompt '%q' "$prompt"
|
|
printf 'pos ai %s ask --session %s' "$provider" "$session"
|
|
[ "$trusted" -eq 1 ] && printf ' --trust'
|
|
[ -n "$prompt" ] && printf ' --system %s' "$q_prompt"
|
|
return 0
|
|
}
|
|
|
|
# ── Activation artifacts (Option B) ────────────────────────────
|
|
# ENV stays the single source of truth; each alias is materialized as an
|
|
# executable wrapper script at ~/.local/bin/<name>. Every invocation re-reads
|
|
# current bytes, so a stale snapshot (the old sourced-alias failure mode) is
|
|
# impossible by construction. No shell sourcing of any kind.
|
|
|
|
_wrapper_path() {
|
|
printf '%s/.local/bin/%s' "$HOME" "$1"
|
|
}
|
|
|
|
# Ownership test: line 2 must carry our generator marker. Files failing this
|
|
# test are NEVER overwritten or deleted.
|
|
_alias_owned() {
|
|
[ -f "$1" ] && sed -n '2p' "$1" 2>/dev/null | grep -q 'Managed by pos ai alias'
|
|
}
|
|
|
|
# Render one wrapper to stdout (args: name provider session prompt [trusted]).
|
|
# The exec line reuses _alias_quote_cmd's double-%q mechanics so the prompt
|
|
# lands as exactly ONE shell word; "$@" passes user args through.
|
|
_wrapper_render() {
|
|
local name="$1" provider="$2" session="$3" prompt="${4:-}" trusted="${5:-0}"
|
|
cat <<WRAPPER_EOF
|
|
#!/usr/bin/env bash
|
|
# Managed by pos ai alias — regenerated automatically; hand-edits are overwritten.
|
|
# Alias: ${name} | provider: ${provider} | session: ${session}
|
|
set -euo pipefail
|
|
exec $(_alias_quote_cmd "$provider" "$session" "$prompt" "$trusted") "\$@"
|
|
WRAPPER_EOF
|
|
}
|
|
|
|
# Atomically install/refresh one wrapper. Skips the write when the rendered
|
|
# content already matches (stable mtimes → sync idempotence is observable).
|
|
# Pre-commit validation: bash -n on the rendered file; failure keeps previous.
|
|
_wrapper_install() { # name provider session prompt [trusted]
|
|
local path="$(_wrapper_path "$1")" tmp
|
|
tmp="$(mktemp "${HOME}/.local/bin/.pos-alias.XXXXXX")"
|
|
_wrapper_render "$1" "$2" "$3" "$4" "${5:-0}" >"$tmp"
|
|
if cmp -s "$tmp" "$path"; then
|
|
rm -f "$tmp"
|
|
return 0
|
|
fi
|
|
if ! bash -n "$tmp" 2>/dev/null; then
|
|
warn "Wrapper for '$1' failed syntax check — keeping previous version" >&2
|
|
rm -f "$tmp"
|
|
return 1
|
|
fi
|
|
mv "$tmp" "$path"
|
|
chmod 755 "$path"
|
|
}
|
|
|
|
# rc 0 iff ~/.local/bin is on PATH.
|
|
_alias_check_path() {
|
|
case ":$PATH:" in
|
|
*":$HOME/.local/bin:"*) return 0 ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
# Legacy ~/.config/.../ai-aliases.sh retirement: activation moved to wrapper
|
|
# scripts, and a stale sourced alias would shadow them (interactive bash gives
|
|
# aliases precedence over PATH lookups). Marker-guarded auto-remove only —
|
|
# foreign files are warned about and left untouched.
|
|
_alias_retire_legacy_sh() {
|
|
[ -f "$SH_FILE" ] || return 0
|
|
if ! head -n 3 "$SH_FILE" | grep -q 'Auto-generated by pos ai alias'; then
|
|
warn "$SH_FILE was not generated by pos ai alias — left untouched; review manually"
|
|
return 0
|
|
fi
|
|
local stale
|
|
stale="$(sed -n 's/^alias \([A-Za-z_][A-Za-z0-9_-]*\)=.*/\1/p' "$SH_FILE" | tr '\n' ' ')"
|
|
stale="${stale% }"
|
|
rm -f "$SH_FILE"
|
|
{
|
|
echo "[!] Alias activation moved to executable scripts in ~/.local/bin/ — legacy file removed: $SH_FILE"
|
|
[ -n "$stale" ] && echo " Stale sourced aliases shadow the new scripts until cleaned — run: unalias $stale"
|
|
echo " (or simply start a new shell)"
|
|
} >&2
|
|
return 0
|
|
}
|
|
|
|
# Two-way reconciliation on EVERY invocation:
|
|
# forward: each ENV entry → render-diff-install (first-run migration,
|
|
# create/edit/remove consistency, silent heal of hand-edited wrappers)
|
|
# reverse: owned wrappers whose name is not in ENV → deleted (covers remove,
|
|
# manual ENV edits, and the empty-set case)
|
|
# plus: legacy .sh retirement; PATH guidance when owned wrappers exist but
|
|
# ~/.local/bin is absent from PATH (wrappers are written regardless).
|
|
_alias_sync() {
|
|
_alias_load
|
|
local bin_dir="${HOME}/.local/bin" i name f base match any=0
|
|
mkdir -p "$bin_dir"
|
|
for ((i = 0; i < ${#_ALIAS_NAMES[@]}; i++)); do
|
|
_wrapper_install "${_ALIAS_NAMES[$i]}" "${_ALIAS_PROVIDERS[$i]}" \
|
|
"${_ALIAS_SESSIONS[$i]}" "${_ALIAS_PROMPTS[$i]}" "${_ALIAS_TRUSTED[$i]:-0}" || :
|
|
done
|
|
for f in "$bin_dir"/*; do
|
|
[ -f "$f" ] || continue
|
|
_alias_owned "$f" || continue
|
|
base="${f##*/}"
|
|
match=0
|
|
for name in ${_ALIAS_NAMES[@]+"${_ALIAS_NAMES[@]}"}; do
|
|
[ "$base" = "$name" ] && { match=1; break; }
|
|
done
|
|
[ "$match" -eq 1 ] || rm -f "$f"
|
|
done
|
|
_alias_retire_legacy_sh
|
|
if ! _alias_check_path; then
|
|
for f in "$bin_dir"/*; do
|
|
[ -f "$f" ] && _alias_owned "$f" && { any=1; break; }
|
|
done
|
|
if [ "$any" -eq 1 ]; then
|
|
warn "~/.local/bin is not on your PATH — alias scripts will not resolve by name."
|
|
warn " Fix now: export PATH=\"\$HOME/.local/bin:\$PATH\""
|
|
warn " Persist it: echo 'export PATH=\"\$HOME/.local/bin:\$PATH\"' >> ~/.profile"
|
|
fi
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
_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 ─────────────────────────────────────
|
|
|
|
# SINGLE alias-table renderer — used by `list` (stdout) and the menu
|
|
# pre-render (inside its stderr display block). One source of truth for the
|
|
# grid so the two contexts can never drift or duplicate each other.
|
|
_alias_table() {
|
|
local count=${#_ALIAS_NAMES[@]} i
|
|
[ "$count" -eq 0 ] && return 0
|
|
printf ' %-12s %-12s %-12s %-5s %s\n' "Name" "Provider" "Session" "Trust" "Prompt"
|
|
printf ' %-12s %-12s %-12s %-5s %s\n' "------------" "------------" "------------" "-----" \
|
|
"------------------------------------------"
|
|
for ((i = 0; i < count; i++)); do
|
|
local _tmark="—"
|
|
[ "${_ALIAS_TRUSTED[$i]:-0}" = "1" ] && _tmark="yes"
|
|
printf ' %-12s %-12s %-12s %-5s %s\n' "${_ALIAS_NAMES[$i]}" "${_ALIAS_PROVIDERS[$i]}" \
|
|
"${_ALIAS_SESSIONS[$i]}" "$_tmark" "$(_alias_prompt_truncate "${_ALIAS_PROMPTS[$i]}")"
|
|
done
|
|
}
|
|
|
|
_alias_list() {
|
|
printf 'Aliases (%d):\n' "${#_ALIAS_NAMES[@]}"
|
|
_alias_table
|
|
}
|
|
|
|
_alias_show() {
|
|
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]}"
|
|
local trusted="${_ALIAS_TRUSTED[$idx]:-0}"
|
|
[ -z "$session" ] && session="$name"
|
|
printf ' %-12s %s\n' "Alias:" "$name"
|
|
printf ' %-12s %s\n' "Provider:" "$provider"
|
|
printf ' %-12s %s\n' "Session:" "$session"
|
|
if _alias_check_path; then
|
|
printf ' %-12s %s\n' "Wrapper:" "$(_wrapper_path "$name")"
|
|
else
|
|
printf ' %-12s %s\n' "Wrapper:" "(not installed — ~/.local/bin not on PATH)"
|
|
fi
|
|
printf ' %-12s %s\n' "Prompt:" "${prompt:-$(printf '%s' "(default)")}"
|
|
printf ' %-12s %s\n' "Trusted:" "$([ "$trusted" = "1" ] && echo "yes (auto-executes commands)" || echo "no (prompts before running)")"
|
|
# Show the resolved command (same quoting mechanism as the generated
|
|
# wrapper — what users copy from here pastes into a shell verbatim)
|
|
printf ' %-12s %s\n' "Command:" "$(_alias_quote_cmd "$provider" "$session" "$prompt" "$trusted")"
|
|
}
|
|
|
|
# ── 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
|
|
_alias_table
|
|
printf ' %d alias(es)\n' "${#_ALIAS_NAMES[@]}"
|
|
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) : ;; # List aliases — the loop's pre-render above IS the current
|
|
# table (single renderer, redrawn fresh every iteration);
|
|
# option 4 returns to the loop for a fresh render instead
|
|
# of printing a second copy (dup-table bug fix).
|
|
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
|
|
# Collision refusals (never clobber foreign files or real binaries):
|
|
# 1. wrapper exists WITH our marker → fine, sync regenerates it
|
|
# 2. file exists WITHOUT marker → refuse
|
|
# 3. name resolves to another binary on PATH → refuse, naming it
|
|
local wpath
|
|
wpath="$(_wrapper_path "$name")"
|
|
if [ -e "$wpath" ]; then
|
|
_alias_owned "$wpath" || err "File '~/.local/bin/$name' already exists and was not created by pos ai alias — pick another name"
|
|
elif command -v "$name" >/dev/null 2>&1; then
|
|
err "'$name' already exists on PATH as $(command -v "$name") — pick another name"
|
|
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 5 "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 5 "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
|
|
|
|
# Step 5: Trust level
|
|
local trusted="0"
|
|
while true; do
|
|
step 5 5 "Trust Level" >&2
|
|
{
|
|
echo " TRUSTED aliases auto-execute commands from the agent"
|
|
echo " WITHOUT asking for confirmation."
|
|
echo ""
|
|
echo " Only enable this for aliases you fully trust with"
|
|
echo " unrestricted shell access on this machine."
|
|
} >&2
|
|
local trust_ans
|
|
trust_ans="$(menu_ask_value "Trust this alias? (y/N)" "N")" || return 0
|
|
case "${trust_ans,,}" in
|
|
y|yes) trusted="1"; break ;;
|
|
n|no|"") trusted="0"; break ;;
|
|
*) warn "Please answer y or n" >&2 ;;
|
|
esac
|
|
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>}"
|
|
printf ' Trusted: %s\n' "$([ "$trusted" = "1" ] && echo "yes (auto-execute)" || echo "no (confirm before run)")"
|
|
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_TRUSTED+=("$trusted")
|
|
_alias_save
|
|
_alias_sync
|
|
log "Alias '$name' created." >&2
|
|
log "Available immediately: $(_wrapper_path "$name")" >&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"
|
|
printf ' Trusted: %s\n' "$([ "${_ALIAS_TRUSTED[$idx]:-0}" = "1" ] && echo "yes" || echo "no")"
|
|
echo >&2
|
|
} >&2
|
|
|
|
local new_provider="${_ALIAS_PROVIDERS[$idx]}"
|
|
local new_session="${_ALIAS_SESSIONS[$idx]}"
|
|
local new_prompt="${_ALIAS_PROMPTS[$idx]}"
|
|
local new_trusted="${_ALIAS_TRUSTED[$idx]:-0}"
|
|
local changed=0
|
|
|
|
# Edit provider
|
|
step 1 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 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 4 "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 4 "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
|
|
|
|
# Edit trust
|
|
step 4 4 "Trust Level" >&2
|
|
local cur_trust_label="no"
|
|
[ "$new_trusted" = "1" ] && cur_trust_label="yes"
|
|
local trust_ans
|
|
trust_ans="$(menu_ask_value "Trust this alias? (y/N)" "$cur_trust_label")" || return 0
|
|
case "${trust_ans,,}" in
|
|
y|yes) new_trusted="1" ;;
|
|
n|no|"") new_trusted="$new_trusted" ;;
|
|
*) warn "Please answer y or n" >&2 ;;
|
|
esac
|
|
[ "$new_trusted" != "${_ALIAS_TRUSTED[$idx]:-0}" ] && 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 tag_t
|
|
[ "$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)"
|
|
[ "$new_trusted" = "${_ALIAS_TRUSTED[$idx]:-0}" ] && tag_t="(unchanged)" || tag_t="(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"
|
|
printf ' Trusted: %-12s %s\n' "$([ "$new_trusted" = "1" ] && echo "yes" || echo "no")" "$tag_t"
|
|
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_TRUSTED[$idx]="$new_trusted"
|
|
_alias_save
|
|
_alias_sync
|
|
log "Alias '$name' updated — the change is live on next invocation." >&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_sync
|
|
log "Alias '$name' removed — script deleted from $(_wrapper_path "$name")." >&2
|
|
log "If the name still autocompletes stale in this shell, run: hash -r" >&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,
|
|
optional system prompt, and a trust level.
|
|
|
|
Trusted aliases auto-execute the agent's commands without asking for
|
|
confirmation. Only enable for aliases you fully trust with shell access.
|
|
|
|
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
|
|
|
|
Activation: every alias is materialized as an executable script at
|
|
~/.local/bin/<name>, synced automatically on every invocation — no shell
|
|
sourcing required. Changes are live on the next invocation, and the
|
|
scripts work identically in interactive shells, scripts, cron, and
|
|
non-login ssh sessions.
|
|
|
|
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 ──────────────────────────────────────────────
|
|
# Every subcommand syncs first: artifacts always equal ENV truth before any
|
|
# subcommand logic runs (migration, healing, retraction — all automatic).
|
|
|
|
case "${1:-}" in
|
|
-h|--help) usage ;;
|
|
create) shift; _alias_sync; _alias_create "${1:-}" ;;
|
|
edit) shift; _alias_sync; _alias_edit "${1:-}" ;;
|
|
remove) shift; _alias_sync; _alias_remove "${1:-}" ;;
|
|
list) _alias_sync; _alias_list ;;
|
|
show)
|
|
[ -n "${2:-}" ] || err "Usage: pos ai alias show <name>"
|
|
_alias_sync
|
|
_alias_show "$2"
|
|
;;
|
|
"") _alias_sync; _alias_menu ;;
|
|
*) err "Unknown subcommand '$1' (use -h for help)" ;;
|
|
esac
|