Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 68 additions & 14 deletions aiwb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@

set -o pipefail

# ============================================================================
# CWD SAFETY (Termux/Android)
# ============================================================================
# When spawned from a bot or after an Android app switch the original CWD may
# no longer exist, causing pwd/getcwd to fail. Fix it early.
if ! pwd >/dev/null 2>&1; then
cd "${HOME}" 2>/dev/null || cd / 2>/dev/null || true
fi

# ============================================================================
# INTERRUPT HANDLING
# ============================================================================
Expand Down Expand Up @@ -33,8 +42,8 @@ cleanup_on_interrupt() {
# Also try pkill as backup for any orphaned curl processes
pkill -P $$ curl 2>/dev/null || true

# Clean up temp files
rm -f /tmp/aiwb_curl_* 2>/dev/null || true
# Clean up temp files (respect $TMPDIR for Termux)
rm -f "${TMPDIR:-/tmp}"/aiwb_curl_* 2>/dev/null || true

echo "Goodbye!"
exit 130 # Standard exit code for SIGINT
Expand Down Expand Up @@ -3169,10 +3178,31 @@ cmd_doctor() {
success "Health check complete"
}

# Helper: copy the latest output file to the --output destination.
# Called both on success and on failure/timeout so partial output is preserved.
_headless_copy_output() {
local output="$1"
[[ -z "$output" ]] && return 0

local workspace output_dir latest
workspace="$(get_workspace)"
output_dir="$workspace/outputs"
latest=$(find "$output_dir" -name "*.md" ! -name "*.feedback.md" -type f 2>/dev/null | sort -r | head -1)
if [[ -n "$latest" ]]; then
cp "$latest" "$output"
msg "Output copied to: $output"
return 0
else
err "headless: no output file found to copy"
return 1
fi
}

# Headless (non-interactive) command handler for bot/CI use
cmd_headless() {
local mode="" prompt="" instruct="" model="" provider="" model_name=""
local verifier="" check_instruct="" context="" output=""
local timeout_secs="${AIWB_HEADLESS_TIMEOUT}"

# Parse flags
while [[ $# -gt 0 ]]; do
Expand All @@ -3187,6 +3217,7 @@ cmd_headless() {
--check-instruct) check_instruct="$2"; shift 2 ;;
--context) context="$2"; shift 2 ;;
--output) output="$2"; shift 2 ;;
--timeout) timeout_secs="$2"; shift 2 ;;
*) err "Unknown headless flag: $1"; return 1 ;;
esac
done
Expand Down Expand Up @@ -3250,24 +3281,47 @@ cmd_headless() {
fi
fi

# --- Start watchdog timer for headless timeout ---
local _watchdog_pid=""
if [[ -n "$timeout_secs" ]] && [[ "$timeout_secs" -gt 0 ]] 2>/dev/null; then
debug "headless: pipeline timeout set to ${timeout_secs}s"
(
sleep "$timeout_secs" 2>/dev/null
# Time's up — try to save whatever output exists, then signal parent
warn "headless: pipeline timed out after ${timeout_secs}s"
kill -USR1 $$ 2>/dev/null || true
) &
_watchdog_pid=$!
# Trap USR1: on timeout, copy partial output then exit
trap '
warn "headless: timed out — saving partial output"
_headless_copy_output "'"$output"'" 2>/dev/null || true
kill '"$_watchdog_pid"' 2>/dev/null || true
exit 124
' USR1
fi

# Run
init_mode "$mode"
mode_run
local run_exit=0
mode_run || run_exit=$?

# Copy output to --output path if requested
# Cancel watchdog if still running
if [[ -n "$_watchdog_pid" ]]; then
kill "$_watchdog_pid" 2>/dev/null || true
wait "$_watchdog_pid" 2>/dev/null || true
fi

# Copy output to --output path if requested.
# Always attempt this — even if mode_run failed (e.g. verifier timed out),
# the generator output may already be saved. Partial success > no output.
if [[ -n "$output" ]]; then
local workspace output_dir latest
workspace="$(get_workspace)"
output_dir="$workspace/outputs"
latest=$(find "$output_dir" -name "*.md" ! -name "*.feedback.md" -type f 2>/dev/null | sort -r | head -1)
if [[ -n "$latest" ]]; then
cp "$latest" "$output"
msg "Output copied to: $output"
else
err "headless: no output file found to copy"
return 1
if ! _headless_copy_output "$output"; then
[[ $run_exit -eq 0 ]] && run_exit=1
fi
fi

return $run_exit
}

# GitHub command handler
Expand Down
10 changes: 10 additions & 0 deletions aiwb_headless
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,19 @@
# --check-instruct path to check instruction file
# --context path to context file or directory
# --output copy output to this file path
# --timeout pipeline timeout in seconds (default: $AIWB_HEADLESS_TIMEOUT or 300)
#
# Environment variables:
# TMPDIR temp directory (Termux: set to writable path, e.g. $HOME/.clide/tmp)
# AIWB_HEADLESS_TIMEOUT default timeout for the full headless pipeline (seconds)

export AIWB_HEADLESS=1

# Ensure CWD exists (Termux may have a stale CWD after app switches)
if ! pwd >/dev/null 2>&1; then
cd "${HOME}" 2>/dev/null || cd / 2>/dev/null || true
fi

# Resolve real path to aiwb sibling script
SCRIPT_DIR="$(cd "$(dirname "$(realpath "${BASH_SOURCE[0]}")")" && pwd)"

Expand Down
4 changes: 2 additions & 2 deletions clear-old-context.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ if [[ -f "$HISTORY_FILE" ]]; then
rm -f "$HISTORY_FILE"
fi

# Also clear any temp context files
find /tmp -name "aiwb_git_audit_*" -delete 2>/dev/null
# Also clear any temp context files (respect $TMPDIR for Termux)
find "${TMPDIR:-/tmp}" -name "aiwb_git_audit_*" -delete 2>/dev/null

echo "✅ Old context cleared!"
echo ""
Expand Down
60 changes: 30 additions & 30 deletions lib/api.sh
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ call_gemini() {

# Handle large prompts by using a temp file instead of command-line args
local request_body
local prompt_file=$(mktemp)
local prompt_file=$(aiwb_mktemp)
echo -n "$prompt" > "$prompt_file"

request_body=$(jq -n \
Expand All @@ -318,9 +318,9 @@ call_gemini() {

local response
local curl_error curl_output request_file
curl_error=$(mktemp -t aiwb_curl_err_XXXXXX)
curl_output=$(mktemp -t aiwb_curl_out_XXXXXX)
request_file=$(mktemp -t aiwb_curl_req_XXXXXX)
curl_error=$(aiwb_mktemp -t aiwb_curl_err_XXXXXX)
curl_output=$(aiwb_mktemp -t aiwb_curl_out_XXXXXX)
request_file=$(aiwb_mktemp -t aiwb_curl_req_XXXXXX)

# Write request body to file to avoid ARG_MAX limits
echo "$request_body" > "$request_file"
Expand Down Expand Up @@ -489,9 +489,9 @@ call_gemini_vision() {

local response
local curl_error curl_output request_file
curl_error=$(mktemp -t aiwb_curl_err_XXXXXX)
curl_output=$(mktemp -t aiwb_curl_out_XXXXXX)
request_file=$(mktemp -t aiwb_curl_req_XXXXXX)
curl_error=$(aiwb_mktemp -t aiwb_curl_err_XXXXXX)
curl_output=$(aiwb_mktemp -t aiwb_curl_out_XXXXXX)
request_file=$(aiwb_mktemp -t aiwb_curl_req_XXXXXX)

# Write request body to file to avoid ARG_MAX limits
echo "$request_body" > "$request_file"
Expand Down Expand Up @@ -613,7 +613,7 @@ call_claude() {
local url="https://api.anthropic.com/v1/messages"

# Handle large prompts by using a temp file
local prompt_file=$(mktemp -t aiwb_prompt_XXXXXX)
local prompt_file=$(aiwb_mktemp -t aiwb_prompt_XXXXXX)
echo -n "$prompt" > "$prompt_file"

local request_body
Expand All @@ -632,9 +632,9 @@ call_claude() {

local response
local curl_error curl_output request_file
curl_error=$(mktemp -t aiwb_curl_err_XXXXXX)
curl_output=$(mktemp -t aiwb_curl_out_XXXXXX)
request_file=$(mktemp -t aiwb_curl_req_XXXXXX)
curl_error=$(aiwb_mktemp -t aiwb_curl_err_XXXXXX)
curl_output=$(aiwb_mktemp -t aiwb_curl_out_XXXXXX)
request_file=$(aiwb_mktemp -t aiwb_curl_req_XXXXXX)

# Write request body to file to avoid ARG_MAX limits
echo "$request_body" > "$request_file"
Expand Down Expand Up @@ -770,9 +770,9 @@ call_claude_vision() {

local response
local curl_error curl_output request_file
curl_error=$(mktemp -t aiwb_curl_err_XXXXXX)
curl_output=$(mktemp -t aiwb_curl_out_XXXXXX)
request_file=$(mktemp -t aiwb_curl_req_XXXXXX)
curl_error=$(aiwb_mktemp -t aiwb_curl_err_XXXXXX)
curl_output=$(aiwb_mktemp -t aiwb_curl_out_XXXXXX)
request_file=$(aiwb_mktemp -t aiwb_curl_req_XXXXXX)

# Write request body to file to avoid ARG_MAX limits
echo "$request_body" > "$request_file"
Expand Down Expand Up @@ -868,7 +868,7 @@ call_openai() {
local url="https://api.openai.com/v1/chat/completions"

# Handle large prompts by using a temp file
local prompt_file=$(mktemp -t aiwb_prompt_XXXXXX)
local prompt_file=$(aiwb_mktemp -t aiwb_prompt_XXXXXX)
echo -n "$prompt" > "$prompt_file"

local request_body
Expand All @@ -887,9 +887,9 @@ call_openai() {

local response
local curl_error curl_output request_file
curl_error=$(mktemp -t aiwb_curl_err_XXXXXX)
curl_output=$(mktemp -t aiwb_curl_out_XXXXXX)
request_file=$(mktemp -t aiwb_curl_req_XXXXXX)
curl_error=$(aiwb_mktemp -t aiwb_curl_err_XXXXXX)
curl_output=$(aiwb_mktemp -t aiwb_curl_out_XXXXXX)
request_file=$(aiwb_mktemp -t aiwb_curl_req_XXXXXX)

# Write request body to file to avoid ARG_MAX limits
echo "$request_body" > "$request_file"
Expand Down Expand Up @@ -1011,7 +1011,7 @@ call_groq() {
local url="https://api.groq.com/openai/v1/chat/completions"

# Handle large prompts by using a temp file
local prompt_file=$(mktemp -t aiwb_prompt_XXXXXX)
local prompt_file=$(aiwb_mktemp -t aiwb_prompt_XXXXXX)
echo -n "$prompt" > "$prompt_file"

local request_body
Expand All @@ -1030,9 +1030,9 @@ call_groq() {

local response
local curl_error curl_output request_file
curl_error=$(mktemp -t aiwb_curl_err_XXXXXX)
curl_output=$(mktemp -t aiwb_curl_out_XXXXXX)
request_file=$(mktemp -t aiwb_curl_req_XXXXXX)
curl_error=$(aiwb_mktemp -t aiwb_curl_err_XXXXXX)
curl_output=$(aiwb_mktemp -t aiwb_curl_out_XXXXXX)
request_file=$(aiwb_mktemp -t aiwb_curl_req_XXXXXX)

# Write request body to file to avoid ARG_MAX limits
echo "$request_body" > "$request_file"
Expand Down Expand Up @@ -1147,7 +1147,7 @@ call_xai() {
local url="https://api.x.ai/v1/chat/completions"

# Handle large prompts by using a temp file
local prompt_file=$(mktemp -t aiwb_prompt_XXXXXX)
local prompt_file=$(aiwb_mktemp -t aiwb_prompt_XXXXXX)
echo -n "$prompt" > "$prompt_file"

local request_body
Expand All @@ -1166,9 +1166,9 @@ call_xai() {

local response
local curl_error curl_output request_file
curl_error=$(mktemp -t aiwb_curl_err_XXXXXX)
curl_output=$(mktemp -t aiwb_curl_out_XXXXXX)
request_file=$(mktemp -t aiwb_curl_req_XXXXXX)
curl_error=$(aiwb_mktemp -t aiwb_curl_err_XXXXXX)
curl_output=$(aiwb_mktemp -t aiwb_curl_out_XXXXXX)
request_file=$(aiwb_mktemp -t aiwb_curl_req_XXXXXX)

# Write request body to file to avoid ARG_MAX limits
echo "$request_body" > "$request_file"
Expand Down Expand Up @@ -1283,7 +1283,7 @@ call_ollama() {
local url="$endpoint/api/generate"

# Handle large prompts by using a temp file
local prompt_file=$(mktemp -t aiwb_prompt_XXXXXX)
local prompt_file=$(aiwb_mktemp -t aiwb_prompt_XXXXXX)
echo -n "$prompt" > "$prompt_file"

local request_body
Expand All @@ -1299,9 +1299,9 @@ call_ollama() {

local response
local curl_error curl_output request_file
curl_error=$(mktemp -t aiwb_curl_err_XXXXXX)
curl_output=$(mktemp -t aiwb_curl_out_XXXXXX)
request_file=$(mktemp -t aiwb_curl_req_XXXXXX)
curl_error=$(aiwb_mktemp -t aiwb_curl_err_XXXXXX)
curl_output=$(aiwb_mktemp -t aiwb_curl_out_XXXXXX)
request_file=$(aiwb_mktemp -t aiwb_curl_req_XXXXXX)

# Write request body to file to avoid ARG_MAX limits
echo "$request_body" > "$request_file"
Expand Down
45 changes: 44 additions & 1 deletion lib/common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,32 @@ spinner() {
printf "\r"
}

# ============================================================================
# TEMP FILE UTILITIES (Termux-safe: respects $TMPDIR)
# ============================================================================

# Termux-safe mktemp wrapper. On Termux /tmp is read-only, so callers
# must honour $TMPDIR (which Clide sets to $HOME/.clide/tmp).
# Usage mirrors mktemp:
# aiwb_mktemp -> creates a temp file
# aiwb_mktemp -d -> creates a temp directory
# aiwb_mktemp -t NAME -> creates a temp file with template NAME.XXXXXX
aiwb_mktemp() {
# Ensure TMPDIR directory exists (Clide may set it before the dir is created)
if [[ -n "${TMPDIR:-}" ]] && [[ ! -d "$TMPDIR" ]]; then
mkdir -p "$TMPDIR" 2>/dev/null || true
fi
# mktemp honours TMPDIR automatically on all platforms
mktemp "$@"
}

# Get the platform-appropriate temp directory path
get_tmp_dir() {
local tmp="${TMPDIR:-/tmp}"
[[ ! -d "$tmp" ]] && mkdir -p "$tmp" 2>/dev/null || true
echo "$tmp"
}

# ============================================================================
# PATH UTILITIES
# ============================================================================
Expand Down Expand Up @@ -298,6 +324,23 @@ get_workspace() {
fi
}

# Get the current working directory safely.
# On Termux, the CWD may have been deleted (e.g. after an app switch).
# Falls back to the workspace directory or $HOME.
safe_cwd() {
local cwd
cwd="$(pwd 2>/dev/null)" && [[ -d "$cwd" ]] && { echo "$cwd"; return 0; }
# CWD is gone — try reasonable fallbacks
local fallback="${AIWB_WORKSPACE:-${HOME}}"
if [[ -d "$fallback" ]]; then
cd "$fallback" 2>/dev/null || true
echo "$fallback"
else
cd "$HOME" 2>/dev/null || true
echo "$HOME"
fi
}

# Ensure directory exists
ensure_dir() {
local dir="$1"
Expand Down Expand Up @@ -331,7 +374,7 @@ json_set() {
local value="$3"

local tmp
tmp="$(mktemp)"
tmp="$(aiwb_mktemp)"
jq --arg k "$key" --arg v "$value" '.[$k] = $v' "$file" > "$tmp" && mv "$tmp" "$file"
}

Expand Down
Loading
Loading