]> begriffs open source - ai-unix/blob - ai-common
Simpler prompt for fixer
[ai-unix] / ai-common
1 #!/bin/bash
2
3 # AI-Unix Common Library
4 # Beautiful, composable functions following McConnell's principles
5
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
14
15 # Signal handling
16 setup_signal_handling() {
17     trap 'exit $EXIT_INTERRUPTED' SIGINT SIGTERM
18 }
19
20 # Pure validation functions (no side effects, single responsibility)
21 # ================================================================
22
23 validate_non_empty_string() {
24     local value="$1"
25     [[ -n "$value" && -n "$(echo "$value" | tr -d '[:space:]')" ]]
26 }
27
28 validate_file_exists() {
29     local file="$1"
30     [[ -n "$file" && -f "$file" && -r "$file" ]]
31 }
32
33 validate_command_available() {
34     local command="$1"
35     command -v "$command" >/dev/null 2>&1
36 }
37
38 validate_json_format() {
39     local text="$1"
40     echo "$text" | grep -q "^{.*}$"
41 }
42
43 validate_boolean_response() {
44     local text="$1"
45     echo "$text" | grep -iq "^true$\|^false$"
46 }
47
48 # Pure data processing functions (deterministic, no side effects)
49 # ==============================================================
50
51 read_from_stdin() {
52     cat
53 }
54
55 read_from_file() {
56     local file="$1"
57     validate_file_exists "$file" && cat "$file"
58 }
59
60 read_from_files() {
61     local files=("$@")
62     local file
63     for file in "${files[@]}"; do
64         validate_file_exists "$file" || return 1
65     done
66     cat "${files[@]}"
67 }
68
69 normalize_whitespace() {
70     local text="$1"
71     echo "$text" | tr -d '[:space:]'
72 }
73
74 extract_number() {
75     local text="$1"
76     echo "$text" | grep -o '[0-9]\+' | head -1
77 }
78
79 extract_boolean() {
80     local text="$1"
81     echo "$text" | tr '[:lower:]' '[:upper:]' | grep -o 'TRUE\|FALSE' | head -1
82 }
83
84 filter_explanatory_text() {
85     local text="$1"
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'
87 }
88
89 # Error message generators (pure functions)
90 # =========================================
91
92 error_file_not_found() {
93     local file="$1"
94     echo "Error: File '$file' not found or not readable"
95 }
96
97 error_missing_argument() {
98     local arg_name="$1"
99     echo "Error: $arg_name is required"
100 }
101
102 error_invalid_option() {
103     local option="$1"
104     echo "Invalid option: -$option"
105 }
106
107 error_command_not_found() {
108     local command="$1"
109     printf "Error: '%s' command not found. Please install Claude CLI.\nVisit: https://claude.ai/cli for installation instructions." "$command"
110 }
111
112 error_llm_no_response() {
113     echo "Error: No response received from LLM"
114 }
115
116 error_llm_api_error() {
117     echo "Error: Claude API error. Check your connection and API key."
118 }
119
120 error_llm_command_failed() {
121     echo "Error: Claude command failed"
122 }
123
124 error_invalid_json() {
125     echo "Error: Invalid JSON format in LLM response"
126 }
127
128 error_invalid_boolean() {
129     echo "Error: Invalid boolean response from LLM - expected TRUE or FALSE"
130 }
131
132 error_invalid_count() {
133     echo "Error: Invalid count format in LLM response"
134 }
135
136 # I/O functions (controlled side effects)
137 # =======================================
138
139 print_error() {
140     local message="$1"
141     echo "$message" >&2
142 }
143
144 print_usage_and_exit() {
145     local usage_func="$1"
146     local exit_code="${2:-$EXIT_USAGE_ERROR}"
147     "$usage_func" >&2
148     exit "$exit_code"
149 }
150
151 # Input processing (returns data via stdout, status via return code)
152 process_input_sources() {
153     local files=("$@")
154     local input
155     
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]}")
161     else
162         input=$(read_from_files "${files[@]}")
163     fi
164     
165     # Early return on read failure
166     [[ $? -ne 0 ]] && return $EXIT_USAGE_ERROR
167     
168     # Early return on empty input
169     validate_non_empty_string "$input" || return $EXIT_NO_MATCH
170     
171     echo "$input"
172     return 0
173 }
174
175 # LLM interaction (prompt and context as separate arguments)
176 execute_llm_request() {
177     local prompt="$1"
178     local context="$2"
179     
180     if ! validate_non_empty_string "$prompt"; then
181         return $EXIT_USAGE_ERROR
182     fi
183     
184     local result exit_code temp_dir
185     
186     # Create temporary directory and execute Claude in it to isolate from project context
187     temp_dir=$(mktemp -d) || return $EXIT_USAGE_ERROR
188     
189     result=$(
190         cd "$temp_dir" || exit 1
191         if [ -n "$context" ]; then
192             echo "$context" | claude -p "$prompt" --disallowedTools "*" 2>/dev/null
193         else
194             claude -p "$prompt" --disallowedTools "*" --max-turns 1 2>/dev/null
195         fi
196     )
197     exit_code=$?
198     
199     # Clean up temporary directory (only if it was successfully created)
200     [[ -n "$temp_dir" && -d "$temp_dir" ]] && rmdir "$temp_dir"
201     
202     case $exit_code in
203         0)
204             if validate_non_empty_string "$result"; then
205                 echo "$result"
206                 return 0
207             else
208                 return $EXIT_NO_MATCH
209             fi
210             ;;
211         2) return $EXIT_API_ERROR ;;
212         *) return $EXIT_LLM_ERROR ;;
213     esac
214 }
215
216 # Standardized error handling for LLM operations
217 # ===============================================
218
219 handle_llm_error() {
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)" ;;
225     esac
226     exit "$error_code"
227 }
228
229 # High-level validation with error reporting
230 # ==========================================
231
232 ensure_dependencies() {
233     if ! validate_command_available "claude"; then
234         print_error "$(error_command_not_found "claude")"
235         exit $EXIT_COMMAND_NOT_FOUND
236     fi
237 }
238
239 ensure_files_exist() {
240     local files=("$@")
241     local file
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
246         fi
247     done
248 }
249
250 ensure_argument_provided() {
251     local arg_name="$1"
252     local arg_value="$2"
253     local usage_func="$3"
254     
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
259     fi
260 }
261
262 # Standard option handlers
263 # =======================
264
265 handle_invalid_option() {
266     local option="$1"
267     print_error "$(error_invalid_option "$option")"
268     exit $EXIT_USAGE_ERROR
269 }
270
271 handle_help_option() {
272     local usage_func="$1"
273     "$usage_func"
274     exit $EXIT_SUCCESS
275 }
276
277 # LLM response processors (specialized for each utility type)
278 # ==========================================================
279
280 process_categorization_response() {
281     local response="$1"
282     if echo "$response" | grep -q ":"; then
283         echo "$response"
284         return 0
285     else
286         print_error "Error: Invalid response format from LLM - expected 'category: content'"
287         return $EXIT_LLM_ERROR
288     fi
289 }
290
291 process_json_response() {
292     local response="$1"
293     if validate_json_format "$response"; then
294         echo "$response"
295         return 0
296     else
297         print_error "$(error_invalid_json)"
298         return $EXIT_LLM_ERROR
299     fi
300 }
301
302 process_boolean_response() {
303     local response="$1"
304     local normalized
305     
306     normalized=$(extract_boolean "$response")
307     
308     if [[ -z "$normalized" ]]; then
309         print_error "$(error_invalid_boolean)"
310         return $EXIT_LLM_ERROR
311     fi
312     
313     echo "$normalized"
314     return 0
315 }
316
317 process_count_response() {
318     local response="$1"
319     local count
320     
321     count=$(extract_number "$response")
322     
323     if [[ -n "$count" && "$count" -ge 0 ]]; then
324         echo "$count"
325         return 0
326     else
327         print_error "$(error_invalid_count)"
328         return $EXIT_LLM_ERROR
329     fi
330 }
331
332 process_filtered_response() {
333     local response="$1"
334     local filtered
335     
336     filtered=$(filter_explanatory_text "$response")
337     
338     if validate_non_empty_string "$filtered"; then
339         echo "$filtered"
340         return 0
341     else
342         return $EXIT_NO_MATCH
343     fi
344 }
345
346 # Initialization
347 setup_signal_handling