ai-consultants 2.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/.claude/commands/ai-consultants:consult.md +94 -0
  2. package/.claude/commands/ai-consultants:debate.md +103 -0
  3. package/.claude/commands/ai-consultants:help.md +30 -0
  4. package/CLAUDE.md +839 -0
  5. package/LICENSE +21 -0
  6. package/README.md +758 -0
  7. package/SKILL.md +156 -0
  8. package/bin/ai-consultants +96 -0
  9. package/docs/COST_RATES.md +172 -0
  10. package/docs/JSON_SCHEMA.md +305 -0
  11. package/docs/SETUP.md +879 -0
  12. package/docs/SMART_ROUTING.md +219 -0
  13. package/docs/cost_rates.json +143 -0
  14. package/package.json +47 -0
  15. package/references/configuration.md +141 -0
  16. package/references/details.md +60 -0
  17. package/scripts/build_context.sh +562 -0
  18. package/scripts/classify_question.sh +148 -0
  19. package/scripts/config.sh +753 -0
  20. package/scripts/configure.sh +1093 -0
  21. package/scripts/consult_all.sh +934 -0
  22. package/scripts/debate_round.sh +421 -0
  23. package/scripts/doctor.sh +642 -0
  24. package/scripts/followup.sh +197 -0
  25. package/scripts/install.sh +319 -0
  26. package/scripts/lib/api.sh +640 -0
  27. package/scripts/lib/api_query.sh +228 -0
  28. package/scripts/lib/cache.sh +338 -0
  29. package/scripts/lib/chunking.sh +880 -0
  30. package/scripts/lib/code_optimizer.sh +671 -0
  31. package/scripts/lib/common.sh +1088 -0
  32. package/scripts/lib/costs.sh +674 -0
  33. package/scripts/lib/personas.sh +372 -0
  34. package/scripts/lib/progress.sh +251 -0
  35. package/scripts/lib/reflection.sh +447 -0
  36. package/scripts/lib/routing.sh +566 -0
  37. package/scripts/lib/schema.json +165 -0
  38. package/scripts/lib/session.sh +327 -0
  39. package/scripts/lib/symbol_map.sh +445 -0
  40. package/scripts/lib/voting.sh +495 -0
  41. package/scripts/peer_review.sh +460 -0
  42. package/scripts/preflight_check.sh +452 -0
  43. package/scripts/query_aider.sh +83 -0
  44. package/scripts/query_amp.sh +67 -0
  45. package/scripts/query_claude.sh +99 -0
  46. package/scripts/query_codex.sh +92 -0
  47. package/scripts/query_cursor.sh +72 -0
  48. package/scripts/query_deepseek.sh +17 -0
  49. package/scripts/query_gemini.sh +85 -0
  50. package/scripts/query_glm.sh +17 -0
  51. package/scripts/query_grok.sh +17 -0
  52. package/scripts/query_kilo.sh +140 -0
  53. package/scripts/query_kimi.sh +67 -0
  54. package/scripts/query_minimax.sh +17 -0
  55. package/scripts/query_mistral.sh +97 -0
  56. package/scripts/query_ollama.sh +328 -0
  57. package/scripts/query_qwen3.sh +93 -0
  58. package/scripts/setup_wizard.sh +367 -0
  59. package/scripts/skillport-install.sh +196 -0
  60. package/scripts/synthesize.sh +332 -0
  61. package/templates/consultation_report.md +323 -0
  62. package/templates/synthesis_prompt.md +120 -0
@@ -0,0 +1,228 @@
1
+ #!/bin/bash
2
+ # api_query.sh - API mode query execution for AI Consultants v2.6
3
+ #
4
+ # This module provides a unified API query function that handles the
5
+ # differences between API formats (OpenAI, Anthropic, Google AI).
6
+ #
7
+ # Used by query_*.sh scripts when they are in API mode instead of CLI mode.
8
+
9
+ # Guard against double-sourcing
10
+ if [[ -n "${_API_QUERY_SH_SOURCED:-}" ]]; then
11
+ return 0 2>/dev/null || exit 0
12
+ fi
13
+ _API_QUERY_SH_SOURCED=1
14
+
15
+ # Load dependencies
16
+ SCRIPT_DIR_API_QUERY="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
17
+ source "$SCRIPT_DIR_API_QUERY/common.sh"
18
+ source "$SCRIPT_DIR_API_QUERY/api.sh"
19
+
20
+ # =============================================================================
21
+ # API QUERY EXECUTION
22
+ # =============================================================================
23
+
24
+ # Execute an API query for a consultant in API mode
25
+ #
26
+ # Usage: run_api_mode_query <consultant_name> <model> <query> <output_file> <timeout_sec>
27
+ #
28
+ # This function:
29
+ # 1. Determines the correct API format based on consultant name
30
+ # 2. Builds the appropriate request body
31
+ # 3. Executes the API call with retry logic
32
+ # 4. Parses the response
33
+ # 5. Returns the raw text response
34
+ #
35
+ # Returns:
36
+ # 0 on success (response written to output_file)
37
+ # 1 on auth failure
38
+ # 2 on rate limit
39
+ # 3 on server error
40
+ # 124 on timeout
41
+ run_api_mode_query() {
42
+ local consultant_name="$1"
43
+ local model="$2"
44
+ local query="$3"
45
+ local output_file="$4"
46
+ local timeout_seconds="${5:-180}"
47
+
48
+ # Get API configuration
49
+ local api_format
50
+ api_format=$(get_api_format "$consultant_name")
51
+ local api_key_var
52
+ api_key_var=$(get_api_key_var "$consultant_name")
53
+ local api_url
54
+ api_url=$(get_api_url "$consultant_name")
55
+
56
+ # Validate API key (except for Google AI where key is in URL)
57
+ if [[ "$api_format" != "google_ai" && -z "${!api_key_var:-}" ]]; then
58
+ log_error "[$consultant_name] API key not set: $api_key_var"
59
+ return 1
60
+ fi
61
+
62
+ # Build request body based on format
63
+ local request_body
64
+ local final_api_url="$api_url"
65
+ local auth_style="bearer"
66
+
67
+ case "$api_format" in
68
+ google_ai)
69
+ request_body=$(build_google_ai_request "$query")
70
+ # Google AI appends model to URL; use x-goog-api-key header for security
71
+ final_api_url="${api_url}/${model}:generateContent"
72
+ auth_style="google_ai"
73
+ ;;
74
+ anthropic)
75
+ request_body=$(build_anthropic_request "$query" "$model")
76
+ auth_style="anthropic"
77
+ ;;
78
+ qwen)
79
+ request_body=$(build_qwen_request "$query" "$model")
80
+ ;;
81
+ *) # openai format
82
+ request_body=$(build_openai_request "$query" "$model")
83
+ ;;
84
+ esac
85
+
86
+ log_debug "[$consultant_name] API format: $api_format"
87
+ log_debug "[$consultant_name] API URL: $final_api_url"
88
+
89
+ # Create temp file for raw API response
90
+ local temp_response
91
+ temp_response=$(mktemp)
92
+
93
+ # Execute API call using unified run_api_query with auth_style
94
+ run_api_query "$consultant_name" "$temp_response" "$timeout_seconds" "$final_api_url" "$api_key_var" "$request_body" "$auth_style"
95
+ local exit_code=$?
96
+
97
+ if [[ $exit_code -ne 0 ]]; then
98
+ rm -f "$temp_response"
99
+ return $exit_code
100
+ fi
101
+
102
+ # Parse response based on format
103
+ local raw_response
104
+ raw_response=$(cat "$temp_response")
105
+
106
+ local parsed_content
107
+ case "$api_format" in
108
+ google_ai)
109
+ parsed_content=$(parse_google_ai_response "$raw_response")
110
+ ;;
111
+ anthropic)
112
+ parsed_content=$(parse_anthropic_response "$raw_response")
113
+ ;;
114
+ qwen)
115
+ parsed_content=$(parse_qwen_response "$raw_response")
116
+ ;;
117
+ *) # openai format
118
+ parsed_content=$(parse_openai_response "$raw_response")
119
+ ;;
120
+ esac
121
+
122
+ if [[ -z "$parsed_content" ]]; then
123
+ log_error "[$consultant_name] Failed to parse API response"
124
+ rm -f "$temp_response"
125
+ return 1
126
+ fi
127
+
128
+ # Write parsed content to output
129
+ echo "$parsed_content" > "$output_file"
130
+
131
+ # Extract and log token usage
132
+ local tokens
133
+ tokens=$(extract_token_usage "$raw_response" "$api_format")
134
+ log_debug "[$consultant_name] Tokens used: $tokens"
135
+
136
+ rm -f "$temp_response"
137
+ return 0
138
+ }
139
+
140
+ # =============================================================================
141
+ # GENERIC API CONSULTANT FUNCTION (v2.6)
142
+ # =============================================================================
143
+
144
+ # Run a consultation using API mode for any API-based consultant
145
+ # This function is called by consult_all.sh for custom API agents and
146
+ # as a fallback when no dedicated query script exists.
147
+ #
148
+ # Usage: run_api_consultant <consultant_name> <query> <context_file> <output_file>
149
+ #
150
+ # Parameters:
151
+ # consultant_name - Name of the consultant (e.g., "Qwen3", "DeepSeek")
152
+ # query - The query text (can be empty if context_file provides all content)
153
+ # context_file - Path to context file (can be empty)
154
+ # output_file - Path to write the JSON response
155
+ #
156
+ # Returns:
157
+ # 0 on success
158
+ # 1 on failure
159
+ run_api_consultant() {
160
+ local consultant_name="$1"
161
+ local query="$2"
162
+ local context_file="$3"
163
+ local output_file="$4"
164
+
165
+ local consultant_upper
166
+ consultant_upper=$(to_upper "$consultant_name")
167
+
168
+ # Get configuration for this consultant
169
+ local model_var="${consultant_upper}_MODEL"
170
+ local timeout_var="${consultant_upper}_TIMEOUT_SECONDS"
171
+
172
+ local model="${!model_var:-}"
173
+ local timeout_seconds="${!timeout_var:-180}"
174
+
175
+ # Build full query from query + context
176
+ local full_query
177
+ full_query=$(build_full_query "$query" "$context_file")
178
+
179
+ if [[ -z "$full_query" ]]; then
180
+ log_error "[$consultant_name] No query to send"
181
+ return 1
182
+ fi
183
+
184
+ local start_time
185
+ start_time=$(get_timestamp_ms)
186
+
187
+ # Create temp file for API response
188
+ local temp_response
189
+ temp_response=$(mktemp)
190
+
191
+ # Use run_api_mode_query which handles all the API format/auth logic
192
+ run_api_mode_query "$consultant_name" "$model" "$full_query" "$temp_response" "$timeout_seconds"
193
+ local exit_code=$?
194
+
195
+ local end_time
196
+ end_time=$(get_timestamp_ms)
197
+ local latency=$((end_time - start_time))
198
+
199
+ if [[ $exit_code -ne 0 ]]; then
200
+ build_error_response "$consultant_name" "${model:-unknown}" "API Consultant" \
201
+ "API query failed (exit code: $exit_code)" "$latency" > "$output_file"
202
+ rm -f "$temp_response"
203
+ return 1
204
+ fi
205
+
206
+ # Read parsed content from temp file (run_api_mode_query already parsed it)
207
+ local parsed_content
208
+ parsed_content=$(cat "$temp_response")
209
+ rm -f "$temp_response"
210
+
211
+ if [[ -z "$parsed_content" ]]; then
212
+ build_error_response "$consultant_name" "${model:-unknown}" "API Consultant" \
213
+ "Empty response from API" "$latency" > "$output_file"
214
+ return 1
215
+ fi
216
+
217
+ # Try to parse as structured JSON response
218
+ if echo "$parsed_content" | jq -e '.response' >/dev/null 2>&1; then
219
+ build_structured_response "$consultant_name" "${model:-unknown}" "API Consultant" \
220
+ "$parsed_content" "$latency" > "$output_file"
221
+ else
222
+ build_fallback_response "$consultant_name" "${model:-unknown}" "API Consultant" \
223
+ "$parsed_content" "$latency" > "$output_file"
224
+ fi
225
+
226
+ log_success "[$consultant_name] Response generated"
227
+ return 0
228
+ }
@@ -0,0 +1,338 @@
1
+ #!/bin/bash
2
+ # cache.sh - Semantic caching for AI Consultants
3
+ #
4
+ # Caches consultation responses based on semantic fingerprints
5
+ # to avoid redundant API calls for similar queries.
6
+
7
+ set -euo pipefail
8
+
9
+ # =============================================================================
10
+ # CONFIGURATION
11
+ # =============================================================================
12
+
13
+ CACHE_DIR="${CACHE_DIR:-/tmp/ai_consultants_cache}"
14
+ CACHE_TTL_HOURS="${CACHE_TTL_HOURS:-24}"
15
+
16
+ # =============================================================================
17
+ # CACHE INITIALIZATION
18
+ # =============================================================================
19
+
20
+ # Initialize cache directory
21
+ # Usage: init_cache
22
+ init_cache() {
23
+ if [[ ! -d "$CACHE_DIR" ]]; then
24
+ mkdir -p "$CACHE_DIR"
25
+ chmod 700 "$CACHE_DIR"
26
+ fi
27
+ }
28
+
29
+ # =============================================================================
30
+ # FINGERPRINT GENERATION
31
+ # =============================================================================
32
+
33
+ # Normalize query for consistent hashing
34
+ # Usage: _normalize_query <query>
35
+ _normalize_query() {
36
+ local query="$1"
37
+ # Lowercase, remove extra whitespace, trim
38
+ echo "$query" | tr '[:upper:]' '[:lower:]' | sed 's/[[:space:]]\+/ /g' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//'
39
+ }
40
+
41
+ # Calculate checksum for a file
42
+ # Usage: _file_checksum <file_path>
43
+ _file_checksum() {
44
+ local file="$1"
45
+ if [[ -f "$file" ]]; then
46
+ if command -v md5sum &>/dev/null; then
47
+ md5sum "$file" 2>/dev/null | cut -d' ' -f1
48
+ elif command -v md5 &>/dev/null; then
49
+ md5 -q "$file" 2>/dev/null
50
+ else
51
+ # Fallback: use file size and mtime
52
+ stat -f "%z%m" "$file" 2>/dev/null || stat -c "%s%Y" "$file" 2>/dev/null || echo "0"
53
+ fi
54
+ else
55
+ echo ""
56
+ fi
57
+ }
58
+
59
+ # Generate semantic fingerprint for a query + context
60
+ # Usage: generate_fingerprint <query> <category> [context_file]
61
+ generate_fingerprint() {
62
+ local query="$1"
63
+ local category="${2:-GENERAL}"
64
+ local context_file="${3:-}"
65
+
66
+ # Normalize query
67
+ local normalized_query
68
+ normalized_query=$(_normalize_query "$query")
69
+
70
+ # Build fingerprint components
71
+ local fingerprint_input="${normalized_query}|${category}"
72
+
73
+ # Add context checksum if provided
74
+ if [[ -n "$context_file" && -f "$context_file" ]]; then
75
+ local context_checksum
76
+ context_checksum=$(_file_checksum "$context_file")
77
+ fingerprint_input+="|${context_checksum}"
78
+ fi
79
+
80
+ # Generate hash
81
+ if command -v sha256sum &>/dev/null; then
82
+ echo -n "$fingerprint_input" | sha256sum | cut -d' ' -f1
83
+ elif command -v shasum &>/dev/null; then
84
+ echo -n "$fingerprint_input" | shasum -a 256 | cut -d' ' -f1
85
+ else
86
+ # Fallback to md5
87
+ if command -v md5sum &>/dev/null; then
88
+ echo -n "$fingerprint_input" | md5sum | cut -d' ' -f1
89
+ elif command -v md5 &>/dev/null; then
90
+ echo -n "$fingerprint_input" | md5
91
+ else
92
+ # Last resort: simple hash
93
+ echo -n "$fingerprint_input" | cksum | cut -d' ' -f1
94
+ fi
95
+ fi
96
+ }
97
+
98
+ # =============================================================================
99
+ # CACHE OPERATIONS
100
+ # =============================================================================
101
+
102
+ # Check if semantic caching is enabled
103
+ # Usage: is_cache_enabled
104
+ is_cache_enabled() {
105
+ [[ "${ENABLE_SEMANTIC_CACHE:-true}" == "true" ]]
106
+ }
107
+
108
+ # Get cache file path for a fingerprint
109
+ # Usage: _get_cache_path <fingerprint> <consultant>
110
+ _get_cache_path() {
111
+ local fingerprint="$1"
112
+ local consultant="${2:-all}"
113
+ echo "${CACHE_DIR}/${consultant}_${fingerprint}.json"
114
+ }
115
+
116
+ # Get cache age in hours
117
+ # Usage: _get_cache_age_hours <cache_file>
118
+ _get_cache_age_hours() {
119
+ local cache_file="$1"
120
+
121
+ if [[ ! -f "$cache_file" ]]; then
122
+ echo "999999" # Very old
123
+ return
124
+ fi
125
+
126
+ local now
127
+ local file_time
128
+ local age_seconds
129
+
130
+ now=$(date +%s)
131
+
132
+ # Get file modification time
133
+ if stat -f %m "$cache_file" &>/dev/null; then
134
+ # macOS
135
+ file_time=$(stat -f %m "$cache_file")
136
+ elif stat -c %Y "$cache_file" &>/dev/null; then
137
+ # Linux
138
+ file_time=$(stat -c %Y "$cache_file")
139
+ else
140
+ echo "999999"
141
+ return
142
+ fi
143
+
144
+ age_seconds=$((now - file_time))
145
+ echo $((age_seconds / 3600))
146
+ }
147
+
148
+ # Check cache for a response
149
+ # Usage: check_cache <query> <category> <consultant> [context_file]
150
+ # Returns: cached response JSON if found and valid, empty otherwise
151
+ check_cache() {
152
+ local query="$1"
153
+ local category="${2:-GENERAL}"
154
+ local consultant="${3:-all}"
155
+ local context_file="${4:-}"
156
+
157
+ # Check if caching is enabled
158
+ if ! is_cache_enabled; then
159
+ return 1
160
+ fi
161
+
162
+ init_cache
163
+
164
+ local fingerprint
165
+ fingerprint=$(generate_fingerprint "$query" "$category" "$context_file")
166
+
167
+ local cache_file
168
+ cache_file=$(_get_cache_path "$fingerprint" "$consultant")
169
+
170
+ # Check if cache file exists
171
+ if [[ ! -f "$cache_file" ]]; then
172
+ return 1
173
+ fi
174
+
175
+ # Check TTL
176
+ local age_hours
177
+ age_hours=$(_get_cache_age_hours "$cache_file")
178
+ local ttl="${CACHE_TTL_HOURS:-24}"
179
+
180
+ if [[ $age_hours -ge $ttl ]]; then
181
+ # Cache expired
182
+ rm -f "$cache_file" 2>/dev/null || true
183
+ return 1
184
+ fi
185
+
186
+ # Return cached content
187
+ cat "$cache_file"
188
+ return 0
189
+ }
190
+
191
+ # Store response in cache
192
+ # Usage: store_cache <query> <category> <consultant> <response_json> [context_file]
193
+ store_cache() {
194
+ local query="$1"
195
+ local category="${2:-GENERAL}"
196
+ local consultant="${3:-all}"
197
+ local response="$4"
198
+ local context_file="${5:-}"
199
+
200
+ # Check if caching is enabled
201
+ if ! is_cache_enabled; then
202
+ return 0
203
+ fi
204
+
205
+ init_cache
206
+
207
+ local fingerprint
208
+ fingerprint=$(generate_fingerprint "$query" "$category" "$context_file")
209
+
210
+ local cache_file
211
+ cache_file=$(_get_cache_path "$fingerprint" "$consultant")
212
+
213
+ # Store response with metadata
214
+ local timestamp
215
+ timestamp=$(date -Iseconds)
216
+
217
+ # Add cache metadata to response
218
+ local cached_response
219
+ cached_response=$(echo "$response" | jq --arg ts "$timestamp" --arg fp "$fingerprint" \
220
+ '. + {cache_metadata: {cached_at: $ts, fingerprint: $fp, from_cache: false}}' 2>/dev/null || echo "$response")
221
+
222
+ # Atomic write: write to temp file first, then move to final location
223
+ # This prevents race conditions with parallel consultants
224
+ local temp_file
225
+ temp_file=$(mktemp "${CACHE_DIR}/cache_tmp.XXXXXX")
226
+ echo "$cached_response" > "$temp_file"
227
+ chmod 600 "$temp_file"
228
+ mv "$temp_file" "$cache_file"
229
+ }
230
+
231
+ # Invalidate cache for a specific query
232
+ # Usage: invalidate_cache <query> <category> [consultant] [context_file]
233
+ invalidate_cache() {
234
+ local query="$1"
235
+ local category="${2:-GENERAL}"
236
+ local consultant="${3:-}"
237
+ local context_file="${4:-}"
238
+
239
+ init_cache
240
+
241
+ local fingerprint
242
+ fingerprint=$(generate_fingerprint "$query" "$category" "$context_file")
243
+
244
+ if [[ -n "$consultant" ]]; then
245
+ # Invalidate specific consultant cache
246
+ local cache_file
247
+ cache_file=$(_get_cache_path "$fingerprint" "$consultant")
248
+ rm -f "$cache_file" 2>/dev/null || true
249
+ else
250
+ # Invalidate all caches for this fingerprint
251
+ rm -f "${CACHE_DIR}"/*_"${fingerprint}".json 2>/dev/null || true
252
+ fi
253
+ }
254
+
255
+ # Clear all expired cache entries
256
+ # Usage: cleanup_cache
257
+ cleanup_cache() {
258
+ init_cache
259
+
260
+ local ttl="${CACHE_TTL_HOURS:-24}"
261
+ local count=0
262
+
263
+ for cache_file in "$CACHE_DIR"/*.json; do
264
+ if [[ -f "$cache_file" ]]; then
265
+ local age_hours
266
+ age_hours=$(_get_cache_age_hours "$cache_file")
267
+
268
+ if [[ $age_hours -ge $ttl ]]; then
269
+ rm -f "$cache_file" 2>/dev/null || true
270
+ ((count++)) || true
271
+ fi
272
+ fi
273
+ done
274
+
275
+ echo "$count"
276
+ }
277
+
278
+ # Clear all cache entries
279
+ # Usage: clear_cache
280
+ clear_cache() {
281
+ init_cache
282
+ rm -f "$CACHE_DIR"/*.json 2>/dev/null || true
283
+ }
284
+
285
+ # =============================================================================
286
+ # CACHE STATISTICS
287
+ # =============================================================================
288
+
289
+ # Get cache statistics
290
+ # Usage: get_cache_stats
291
+ get_cache_stats() {
292
+ init_cache
293
+
294
+ local total_files=0
295
+ local total_size=0
296
+ local expired_files=0
297
+ local ttl="${CACHE_TTL_HOURS:-24}"
298
+
299
+ for cache_file in "$CACHE_DIR"/*.json; do
300
+ if [[ -f "$cache_file" ]]; then
301
+ ((total_files++)) || true
302
+
303
+ local file_size
304
+ if stat -f %z "$cache_file" &>/dev/null; then
305
+ file_size=$(stat -f %z "$cache_file")
306
+ else
307
+ file_size=$(stat -c %s "$cache_file" 2>/dev/null || echo 0)
308
+ fi
309
+ total_size=$((total_size + file_size))
310
+
311
+ local age_hours
312
+ age_hours=$(_get_cache_age_hours "$cache_file")
313
+ if [[ $age_hours -ge $ttl ]]; then
314
+ ((expired_files++)) || true
315
+ fi
316
+ fi
317
+ done
318
+
319
+ local size_kb=$((total_size / 1024))
320
+
321
+ cat << EOF
322
+ {
323
+ "total_entries": $total_files,
324
+ "expired_entries": $expired_files,
325
+ "total_size_kb": $size_kb,
326
+ "cache_dir": "$CACHE_DIR",
327
+ "ttl_hours": $ttl,
328
+ "enabled": $(is_cache_enabled && echo "true" || echo "false")
329
+ }
330
+ EOF
331
+ }
332
+
333
+ # Mark response as from cache (for logging/metrics)
334
+ # Usage: echo "$json" | mark_from_cache OR mark_from_cache "$json"
335
+ mark_from_cache() {
336
+ local response="${1:-$(cat)}"
337
+ echo "$response" | jq '.cache_metadata.from_cache = true' 2>/dev/null || echo "$response"
338
+ }