3 # AI-Unix Common Library
4 # Beautiful, composable functions following McConnell's principles
6 # Exit codes (standardized across all tools)
7 readonly EXIT_SUCCESS=0
8 readonly EXIT_NO_MATCH=1
9 readonly EXIT_USAGE_ERROR=2
10 readonly EXIT_LLM_ERROR=3
11 readonly EXIT_API_ERROR=4
12 readonly EXIT_COMMAND_NOT_FOUND=127
13 readonly EXIT_INTERRUPTED=130
16 setup_signal_handling() {
17 trap 'exit $EXIT_INTERRUPTED' SIGINT SIGTERM
20 # Pure validation functions (no side effects, single responsibility)
21 # ================================================================
23 validate_non_empty_string() {
25 [[ -n "$value" && -n "$(echo "$value" | tr -d '[:space:]')" ]]
28 validate_file_exists() {
30 [[ -n "$file" && -f "$file" && -r "$file" ]]
33 validate_command_available() {
35 command -v "$command" >/dev/null 2>&1
38 validate_json_format() {
40 echo "$text" | grep -q "^{.*}$"
43 validate_boolean_response() {
45 echo "$text" | grep -iq "^true$\|^false$"
48 # Pure data processing functions (deterministic, no side effects)
49 # ==============================================================
57 validate_file_exists "$file" && cat "$file"
63 for file in "${files[@]}"; do
64 validate_file_exists "$file" || return 1
69 normalize_whitespace() {
71 echo "$text" | tr -d '[:space:]'
76 echo "$text" | grep -o '[0-9]\+' | head -1
81 echo "$text" | tr '[:lower:]' '[:upper:]' | grep -o 'TRUE\|FALSE' | head -1
84 filter_explanatory_text() {
86 echo "$text" | grep -v "^Looking at\|^I'll\|^Here are\|^The following\|^Based on\|^Analyzing\|^To find\|^These are\|^Let me\|^I need to" | sed '/^[[:space:]]*$/d'
89 # Error message generators (pure functions)
90 # =========================================
92 error_file_not_found() {
94 echo "Error: File '$file' not found or not readable"
97 error_missing_argument() {
99 echo "Error: $arg_name is required"
102 error_invalid_option() {
104 echo "Invalid option: -$option"
107 error_command_not_found() {
109 printf "Error: '%s' command not found. Please install Claude CLI.\nVisit: https://claude.ai/cli for installation instructions." "$command"
112 error_llm_no_response() {
113 echo "Error: No response received from LLM"
116 error_llm_api_error() {
117 echo "Error: Claude API error. Check your connection and API key."
120 error_llm_command_failed() {
121 echo "Error: Claude command failed"
124 error_invalid_json() {
125 echo "Error: Invalid JSON format in LLM response"
128 error_invalid_boolean() {
129 echo "Error: Invalid boolean response from LLM - expected TRUE or FALSE"
132 error_invalid_count() {
133 echo "Error: Invalid count format in LLM response"
136 # I/O functions (controlled side effects)
137 # =======================================
144 print_usage_and_exit() {
145 local usage_func="$1"
146 local exit_code="${2:-$EXIT_USAGE_ERROR}"
151 # Input processing (returns data via stdout, status via return code)
152 process_input_sources() {
156 # Determine input source and read data
157 if [[ ${#files[@]} -eq 0 ]]; then
158 input=$(read_from_stdin)
159 elif [[ ${#files[@]} -eq 1 ]]; then
160 input=$(read_from_file "${files[0]}")
162 input=$(read_from_files "${files[@]}")
165 # Early return on read failure
166 [[ $? -ne 0 ]] && return $EXIT_USAGE_ERROR
168 # Early return on empty input
169 validate_non_empty_string "$input" || return $EXIT_NO_MATCH
175 # LLM interaction (prompt and context as separate arguments)
176 execute_llm_request() {
180 if ! validate_non_empty_string "$prompt"; then
181 return $EXIT_USAGE_ERROR
184 local result exit_code temp_dir
186 # Create temporary directory and execute Claude in it to isolate from project context
187 temp_dir=$(mktemp -d) || return $EXIT_USAGE_ERROR
190 cd "$temp_dir" || exit 1
191 if [ -n "$context" ]; then
192 echo "$context" | claude -p "$prompt" --disallowedTools "*" 2>/dev/null
194 claude -p "$prompt" --disallowedTools "*" --max-turns 1 2>/dev/null
199 # Clean up temporary directory (only if it was successfully created)
200 [[ -n "$temp_dir" && -d "$temp_dir" ]] && rmdir "$temp_dir"
204 if validate_non_empty_string "$result"; then
208 return $EXIT_NO_MATCH
211 2) return $EXIT_API_ERROR ;;
212 *) return $EXIT_LLM_ERROR ;;
216 # Standardized error handling for LLM operations
217 # ===============================================
220 local error_code="$1"
221 case "$error_code" in
222 "$EXIT_NO_MATCH") print_error "$(error_llm_no_response)" ;;
223 "$EXIT_API_ERROR") print_error "$(error_llm_api_error)" ;;
224 *) print_error "$(error_llm_command_failed)" ;;
229 # High-level validation with error reporting
230 # ==========================================
232 ensure_dependencies() {
233 if ! validate_command_available "claude"; then
234 print_error "$(error_command_not_found "claude")"
235 exit $EXIT_COMMAND_NOT_FOUND
239 ensure_files_exist() {
242 for file in "${files[@]}"; do
243 if ! validate_file_exists "$file"; then
244 print_error "$(error_file_not_found "$file")"
245 exit $EXIT_USAGE_ERROR
250 ensure_argument_provided() {
253 local usage_func="$3"
255 if ! validate_non_empty_string "$arg_value"; then
256 print_error "$(error_missing_argument "$arg_name")"
257 [[ -n "$usage_func" ]] && print_usage_and_exit "$usage_func"
258 exit $EXIT_USAGE_ERROR
262 # Standard option handlers
263 # =======================
265 handle_invalid_option() {
267 print_error "$(error_invalid_option "$option")"
268 exit $EXIT_USAGE_ERROR
271 handle_help_option() {
272 local usage_func="$1"
277 # LLM response processors (specialized for each utility type)
278 # ==========================================================
280 process_categorization_response() {
282 if echo "$response" | grep -q ":"; then
286 print_error "Error: Invalid response format from LLM - expected 'category: content'"
287 return $EXIT_LLM_ERROR
291 process_json_response() {
293 if validate_json_format "$response"; then
297 print_error "$(error_invalid_json)"
298 return $EXIT_LLM_ERROR
302 process_boolean_response() {
306 normalized=$(extract_boolean "$response")
308 if [[ -z "$normalized" ]]; then
309 print_error "$(error_invalid_boolean)"
310 return $EXIT_LLM_ERROR
317 process_count_response() {
321 count=$(extract_number "$response")
323 if [[ -n "$count" && "$count" -ge 0 ]]; then
327 print_error "$(error_invalid_count)"
328 return $EXIT_LLM_ERROR
332 process_filtered_response() {
336 filtered=$(filter_explanatory_text "$response")
338 if validate_non_empty_string "$filtered"; then
342 return $EXIT_NO_MATCH
347 setup_signal_handling