196 lines
4.2 KiB
Bash
Executable File
196 lines
4.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
: <<'DOC'
|
|
ask — AI terminal assistant (macOS)
|
|
|
|
Setup API key (Keychain):
|
|
security add-generic-password -a "$USER" -s TK_AI_API_TOKEN -w "YOUR_API_KEY"
|
|
|
|
Usage:
|
|
ask "question"
|
|
ask -i # interactive mode
|
|
ask -v # verbose (debug raw response)
|
|
DOC
|
|
|
|
# ===== CONFIG =====
|
|
SERVER="https://ai.acbpro.com/v1/chat/completions"
|
|
MODEL="gpt-5.3-codex"
|
|
KEY_NAME="TK_AI_API_TOKEN"
|
|
|
|
VERBOSE=0
|
|
INTERACTIVE=0
|
|
|
|
# ===== COLORS =====
|
|
GREEN="\033[1;32m"
|
|
CYAN="\033[1;36m"
|
|
YELLOW="\033[1;33m"
|
|
RED="\033[1;31m"
|
|
RESET="\033[0m"
|
|
|
|
# ===== PARSE FLAGS =====
|
|
while [[ "$1" == -* ]]; do
|
|
case "$1" in
|
|
-v) VERBOSE=1 ;;
|
|
-i) INTERACTIVE=1 ;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
# ===== LOAD TOKEN =====
|
|
API_TOKEN=$(security find-generic-password -a "$USER" -s "$KEY_NAME" -w 2>/dev/null | tr -d '\n')
|
|
|
|
if [ -z "$API_TOKEN" ]; then
|
|
echo "❌ API token not found in Keychain ($KEY_NAME)"
|
|
exit 1
|
|
fi
|
|
|
|
# ===== SYSTEM PROMPT =====
|
|
SYSTEM_PROMPT="You are a terminal assistant for macOS (zsh/bash).
|
|
|
|
- Default: return ONLY the command, no explanation.
|
|
- If user asks for explanation, then explain.
|
|
- Commands must be safe and runnable.
|
|
- Prefer standard macOS/Linux tools.
|
|
- No markdown, no code block.
|
|
"
|
|
|
|
# ===== TERMINAL STATUS =====
|
|
supports_status_line() {
|
|
[ -t 2 ] && [ "${TERM:-}" != "dumb" ]
|
|
}
|
|
|
|
render_status_line() {
|
|
supports_status_line || return 0
|
|
printf '\r\033[2K%b' "$1" >&2
|
|
}
|
|
|
|
clear_status_line() {
|
|
supports_status_line || return 0
|
|
printf '\r\033[2K' >&2
|
|
}
|
|
|
|
print_response() {
|
|
if [ -t 1 ] && [ "${TERM:-}" != "dumb" ]; then
|
|
echo -e "${CYAN}🤖 $1${RESET}"
|
|
else
|
|
printf '%s\n' "$1"
|
|
fi
|
|
}
|
|
|
|
# ===== FUNCTION CALL API =====
|
|
call_api() {
|
|
local Q="$1"
|
|
local CWD GIT_BRANCH LAST_CMD JSON
|
|
local START_TIME TMP_FILE CURL_PID
|
|
local SPINNER i NOW ELAPSED CHAR COLOR
|
|
local RESPONSE CONTENT CURL_STATUS
|
|
|
|
CWD="$(pwd)"
|
|
GIT_BRANCH="$(git branch --show-current 2>/dev/null)"
|
|
LAST_CMD="$(fc -ln -1 2>/dev/null)"
|
|
|
|
JSON=$(jq -n \
|
|
--arg model "$MODEL" \
|
|
--arg sys "$SYSTEM_PROMPT" \
|
|
--arg q "$Q" \
|
|
--arg cwd "$CWD" \
|
|
--arg branch "$GIT_BRANCH" \
|
|
--arg last "$LAST_CMD" \
|
|
'{
|
|
model: $model,
|
|
messages: [
|
|
{role: "system", content: $sys},
|
|
{role: "user", content:
|
|
("Context:
|
|
" +
|
|
"cwd: " + $cwd + "
|
|
" +
|
|
"git_branch: " + $branch + "
|
|
" +
|
|
"last_command: " + $last + "
|
|
|
|
" +
|
|
"Request:
|
|
" + $q)
|
|
}
|
|
]
|
|
}'
|
|
)
|
|
|
|
# ===== CALL (async) =====
|
|
START_TIME=$(date +%s)
|
|
TMP_FILE=$(mktemp)
|
|
|
|
curl -s "$SERVER" \
|
|
-H "Authorization: Bearer $API_TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
-d "$JSON" > "$TMP_FILE" &
|
|
|
|
CURL_PID=$!
|
|
trap 'clear_status_line; [ -n "${CURL_PID:-}" ] && kill "${CURL_PID}" 2>/dev/null; [ -n "${TMP_FILE:-}" ] && rm -f "${TMP_FILE}"; exit 130' INT TERM
|
|
|
|
# ===== SPINNER + TIMER =====
|
|
SPINNER='|/-\'
|
|
i=0
|
|
if supports_status_line; then
|
|
while kill -0 "$CURL_PID" 2>/dev/null; do
|
|
NOW=$(date +%s)
|
|
ELAPSED=$((NOW - START_TIME))
|
|
|
|
CHAR=${SPINNER:i%${#SPINNER}:1}
|
|
|
|
if [ "$ELAPSED" -lt 10 ]; then COLOR="$GREEN";
|
|
elif [ "$ELAPSED" -lt 30 ]; then COLOR="$YELLOW";
|
|
else COLOR="$RED"; fi
|
|
|
|
render_status_line "${COLOR}⏳ calling ${CHAR} ${ELAPSED}s${RESET}"
|
|
|
|
i=$((i+1))
|
|
sleep 0.1
|
|
done
|
|
fi
|
|
|
|
wait "$CURL_PID"
|
|
CURL_STATUS=$?
|
|
clear_status_line
|
|
trap - INT TERM
|
|
|
|
RESPONSE=$(<"$TMP_FILE")
|
|
rm -f "$TMP_FILE"
|
|
|
|
if [ "$CURL_STATUS" -ne 0 ]; then
|
|
echo "❌ request failed (curl exit $CURL_STATUS)" >&2
|
|
[ -n "$RESPONSE" ] && echo "$RESPONSE" >&2
|
|
return "$CURL_STATUS"
|
|
fi
|
|
|
|
if [ "$VERBOSE" -eq 1 ]; then
|
|
echo -e "${YELLOW}🔧 RAW RESPONSE:${RESET}" >&2
|
|
echo "$RESPONSE" >&2
|
|
fi
|
|
|
|
CONTENT=$(echo "$RESPONSE" | jq -r '.choices[0].message.content // .response // .text // .')
|
|
|
|
print_response "$CONTENT"
|
|
}
|
|
|
|
# ===== INTERACTIVE MODE =====
|
|
if [ "$INTERACTIVE" -eq 1 ]; then
|
|
echo -e "${GREEN}💬 Interactive mode (Ctrl+C để thoát)${RESET}"
|
|
while true; do
|
|
printf "${GREEN}❓ ask> ${RESET}"
|
|
read Q
|
|
[ -z "$Q" ] && continue
|
|
call_api "$Q"
|
|
done
|
|
exit 0
|
|
fi
|
|
|
|
# ===== NORMAL MODE =====
|
|
if [ $# -eq 0 ]; then
|
|
echo "Usage: ask [-v] [-i] <question>"
|
|
exit 1
|
|
fi
|
|
|
|
call_api "$*"
|