Initial commit
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
#!/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 "$*"
|
||||
Reference in New Issue
Block a user