Initial commit
This commit is contained in:
12
.claude-plugin/plugin.json
Normal file
12
.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "multi-ai-consult",
|
||||||
|
"description": "Consult Gemini, Codex, Qwen, and OpenCode AIs in parallel and synthesize responses",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": {
|
||||||
|
"name": "Walter Levan",
|
||||||
|
"github": "wjlevan"
|
||||||
|
},
|
||||||
|
"commands": [
|
||||||
|
"./commands"
|
||||||
|
]
|
||||||
|
}
|
||||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# multi-ai-consult
|
||||||
|
|
||||||
|
Consult Gemini, Codex, Qwen, and OpenCode AIs in parallel and synthesize responses
|
||||||
588
commands/consult.md
Normal file
588
commands/consult.md
Normal file
@@ -0,0 +1,588 @@
|
|||||||
|
---
|
||||||
|
description: Consult Gemini, Codex, Qwen, and OpenCode AIs in parallel and synthesize responses
|
||||||
|
---
|
||||||
|
|
||||||
|
# Multi-AI Consultation
|
||||||
|
|
||||||
|
Consult multiple AIs in parallel, then synthesize their responses into a unified answer.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Models and timeouts can be overridden via environment variables:
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|----------|---------|-------------|
|
||||||
|
| `CONSULT_GEMINI_MODEL` | `gemini-3-pro-preview` | Gemini model |
|
||||||
|
| `CONSULT_CODEX_MODEL` | `gpt-5.1-codex-max` | Codex model |
|
||||||
|
| `CONSULT_QWEN_MODEL` | (CLI default) | Qwen model |
|
||||||
|
| `CONSULT_OPENCODE_MODEL` | `anthropic/claude-opus-4-5` | OpenCode model |
|
||||||
|
| `CONSULT_TIMEOUT` | `120` | Timeout in seconds per CLI |
|
||||||
|
| `CONSULT_MAX_RESPONSE_CHARS` | `20000` | Max chars per response (~5K tokens) |
|
||||||
|
|
||||||
|
## Arguments
|
||||||
|
|
||||||
|
Parse these from the query if present:
|
||||||
|
- `--only=gemini,codex` - Consult only specified AIs (comma-separated)
|
||||||
|
- `--exclude=opencode` - Exclude specified AIs (comma-separated)
|
||||||
|
- `--verbose` - Show individual AI responses before synthesis
|
||||||
|
- `--dry-run` - Show prepared prompt without executing
|
||||||
|
|
||||||
|
## Instructions
|
||||||
|
|
||||||
|
### Step 1: Verify Dependencies
|
||||||
|
|
||||||
|
Check required CLIs and tools exist:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Platform detection and timeout command selection
|
||||||
|
# CRITICAL: macOS timeout behaves differently and may not inherit PATH
|
||||||
|
if [[ "$(uname)" == "Darwin" ]]; then
|
||||||
|
# macOS: prefer gtimeout from GNU coreutils (brew install coreutils)
|
||||||
|
if command -v gtimeout >/dev/null 2>&1; then
|
||||||
|
TIMEOUT_CMD="$(command -v gtimeout)"
|
||||||
|
elif command -v timeout >/dev/null 2>&1; then
|
||||||
|
TIMEOUT_CMD="$(command -v timeout)"
|
||||||
|
echo "Warning: Using macOS timeout - consider 'brew install coreutils' for gtimeout"
|
||||||
|
else
|
||||||
|
echo "Error: No timeout command found. Install with: brew install coreutils"
|
||||||
|
exit 11
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Linux: use standard timeout
|
||||||
|
TIMEOUT_CMD="$(command -v timeout)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Platform: $(uname), using timeout: $TIMEOUT_CMD"
|
||||||
|
|
||||||
|
# Check jq for JSON parsing
|
||||||
|
command -v jq >/dev/null 2>&1 || { echo "Warning: jq not found, JSON parsing may fail"; }
|
||||||
|
|
||||||
|
# Resolve FULL PATHS to CLI binaries
|
||||||
|
# CRITICAL: timeout subprocess may not inherit shell PATH
|
||||||
|
AVAILABLE_CLIS=""
|
||||||
|
GEMINI_BIN=""
|
||||||
|
CODEX_BIN=""
|
||||||
|
QWEN_BIN=""
|
||||||
|
OPENCODE_BIN=""
|
||||||
|
|
||||||
|
if command -v gemini >/dev/null 2>&1; then
|
||||||
|
GEMINI_BIN="$(command -v gemini)"
|
||||||
|
AVAILABLE_CLIS="$AVAILABLE_CLIS gemini"
|
||||||
|
echo " Gemini: $GEMINI_BIN"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v codex >/dev/null 2>&1; then
|
||||||
|
CODEX_BIN="$(command -v codex)"
|
||||||
|
AVAILABLE_CLIS="$AVAILABLE_CLIS codex"
|
||||||
|
echo " Codex: $CODEX_BIN"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v qwen >/dev/null 2>&1; then
|
||||||
|
QWEN_BIN="$(command -v qwen)"
|
||||||
|
AVAILABLE_CLIS="$AVAILABLE_CLIS qwen"
|
||||||
|
echo " Qwen: $QWEN_BIN"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v opencode >/dev/null 2>&1; then
|
||||||
|
OPENCODE_BIN="$(command -v opencode)"
|
||||||
|
AVAILABLE_CLIS="$AVAILABLE_CLIS opencode"
|
||||||
|
echo " OpenCode: $OPENCODE_BIN"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
If no AI CLIs are available, stop and provide installation guidance. Otherwise, proceed with available CLIs (graceful degradation).
|
||||||
|
|
||||||
|
### CLI Command Reference
|
||||||
|
|
||||||
|
| CLI | Command | Key Flags |
|
||||||
|
|-----|---------|-----------|
|
||||||
|
| Gemini | `gemini -m "$MODEL" -o json` | `-m`: model, `-o json`: JSON output |
|
||||||
|
| Codex | `codex exec --json -m "$MODEL" --skip-git-repo-check` | `--json`: NDJSON output |
|
||||||
|
| Qwen | `qwen -p "" -o json` | `-p ""`: stdin prompt, `-o json`: JSON output |
|
||||||
|
| OpenCode | `opencode run -m "$MODEL" --format json` | `--format json`: NDJSON events |
|
||||||
|
|
||||||
|
### Step 2: Prepare the Prompt
|
||||||
|
|
||||||
|
Construct a clear prompt:
|
||||||
|
|
||||||
|
1. **Reword** the user's query for clarity
|
||||||
|
2. **Include file contents** if files are being discussed (read them first)
|
||||||
|
3. **Add context** about what kind of response is expected
|
||||||
|
4. **Validate input** - ensure no sensitive data (API keys, passwords) is included
|
||||||
|
|
||||||
|
Store in a variable using proper quoting:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PROMPT="Your prepared prompt here"
|
||||||
|
|
||||||
|
# For dry-run mode, display and exit
|
||||||
|
if [ "$DRY_RUN" = "true" ]; then
|
||||||
|
echo "=== DRY RUN - Prompt ==="
|
||||||
|
printf '%s\n' "$PROMPT"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Execute All in Parallel
|
||||||
|
|
||||||
|
Create temp files and run CLIs with timeout. **Important:** Do NOT wrap commands in subshells `( )` - this breaks job control.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Configuration with defaults
|
||||||
|
TIMEOUT="${CONSULT_TIMEOUT:-120}"
|
||||||
|
MAX_RESPONSE_CHARS="${CONSULT_MAX_RESPONSE_CHARS:-20000}"
|
||||||
|
GEMINI_MODEL="${CONSULT_GEMINI_MODEL:-gemini-3-pro-preview}"
|
||||||
|
CODEX_MODEL="${CONSULT_CODEX_MODEL:-gpt-5.1-codex-max}"
|
||||||
|
OPENCODE_MODEL="${CONSULT_OPENCODE_MODEL:-anthropic/claude-opus-4-5}"
|
||||||
|
|
||||||
|
# Create temp files (use mktemp for safety)
|
||||||
|
GEMINI_OUT=$(mktemp /tmp/consult_gemini.XXXXXX)
|
||||||
|
GEMINI_ERR=$(mktemp /tmp/consult_gemini_err.XXXXXX)
|
||||||
|
CODEX_OUT=$(mktemp /tmp/consult_codex.XXXXXX)
|
||||||
|
CODEX_ERR=$(mktemp /tmp/consult_codex_err.XXXXXX)
|
||||||
|
QWEN_OUT=$(mktemp /tmp/consult_qwen.XXXXXX)
|
||||||
|
QWEN_ERR=$(mktemp /tmp/consult_qwen_err.XXXXXX)
|
||||||
|
OPENCODE_OUT=$(mktemp /tmp/consult_opencode.XXXXXX)
|
||||||
|
OPENCODE_ERR=$(mktemp /tmp/consult_opencode_err.XXXXXX)
|
||||||
|
|
||||||
|
# Cleanup trap - runs on exit, interrupt, or termination
|
||||||
|
cleanup() {
|
||||||
|
rm -f "$GEMINI_OUT" "$GEMINI_ERR" "$CODEX_OUT" "$CODEX_ERR" \
|
||||||
|
"$QWEN_OUT" "$QWEN_ERR" "$OPENCODE_OUT" "$OPENCODE_ERR"
|
||||||
|
# Kill any remaining background jobs
|
||||||
|
jobs -p | xargs -r kill 2>/dev/null
|
||||||
|
}
|
||||||
|
trap cleanup EXIT INT TERM
|
||||||
|
|
||||||
|
echo "Starting parallel consultation..."
|
||||||
|
echo " Timeout: ${TIMEOUT}s per CLI"
|
||||||
|
echo " CLIs:$AVAILABLE_CLIS"
|
||||||
|
|
||||||
|
# Run in parallel using FULL PATHS and $TIMEOUT_CMD
|
||||||
|
# CRITICAL: Use full paths resolved in Step 1 to avoid PATH inheritance issues
|
||||||
|
# Use printf for safe prompt handling (handles special chars, -n, etc.)
|
||||||
|
|
||||||
|
[ -n "$GEMINI_BIN" ] && {
|
||||||
|
printf '%s' "$PROMPT" | "$TIMEOUT_CMD" "$TIMEOUT" "$GEMINI_BIN" -m "$GEMINI_MODEL" -o json > "$GEMINI_OUT" 2>"$GEMINI_ERR" &
|
||||||
|
GEMINI_PID=$!
|
||||||
|
}
|
||||||
|
|
||||||
|
[ -n "$CODEX_BIN" ] && {
|
||||||
|
printf '%s' "$PROMPT" | "$TIMEOUT_CMD" "$TIMEOUT" "$CODEX_BIN" exec --json -m "$CODEX_MODEL" --skip-git-repo-check > "$CODEX_OUT" 2>"$CODEX_ERR" &
|
||||||
|
CODEX_PID=$!
|
||||||
|
}
|
||||||
|
|
||||||
|
[ -n "$QWEN_BIN" ] && {
|
||||||
|
printf '%s' "$PROMPT" | "$TIMEOUT_CMD" "$TIMEOUT" "$QWEN_BIN" -p "" -o json > "$QWEN_OUT" 2>"$QWEN_ERR" &
|
||||||
|
QWEN_PID=$!
|
||||||
|
}
|
||||||
|
|
||||||
|
[ -n "$OPENCODE_BIN" ] && {
|
||||||
|
printf '%s' "$PROMPT" | "$TIMEOUT_CMD" "$TIMEOUT" "$OPENCODE_BIN" run -m "$OPENCODE_MODEL" --format json > "$OPENCODE_OUT" 2>"$OPENCODE_ERR" &
|
||||||
|
OPENCODE_PID=$!
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "All CLIs launched. Waiting for responses..."
|
||||||
|
|
||||||
|
# Wait for all and capture exit codes
|
||||||
|
[ -n "$GEMINI_PID" ] && { wait $GEMINI_PID 2>/dev/null; GEMINI_EXIT=$?; } || GEMINI_EXIT=127
|
||||||
|
[ -n "$CODEX_PID" ] && { wait $CODEX_PID 2>/dev/null; CODEX_EXIT=$?; } || CODEX_EXIT=127
|
||||||
|
[ -n "$QWEN_PID" ] && { wait $QWEN_PID 2>/dev/null; QWEN_EXIT=$?; } || QWEN_EXIT=127
|
||||||
|
[ -n "$OPENCODE_PID" ] && { wait $OPENCODE_PID 2>/dev/null; OPENCODE_EXIT=$?; } || OPENCODE_EXIT=127
|
||||||
|
|
||||||
|
echo "All CLIs completed. Processing responses..."
|
||||||
|
echo "Exit codes: Gemini=$GEMINI_EXIT Codex=$CODEX_EXIT Qwen=$QWEN_EXIT OpenCode=$OPENCODE_EXIT"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3.5: Progress Monitoring (Optional)
|
||||||
|
|
||||||
|
For long consultations, provide periodic status updates to show activity during the 2+ minutes of parallel execution:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Optional: Monitor progress every 15 seconds
|
||||||
|
monitor_progress() {
|
||||||
|
local start_time=$(date +%s)
|
||||||
|
while true; do
|
||||||
|
sleep 15
|
||||||
|
local elapsed=$(( $(date +%s) - start_time ))
|
||||||
|
|
||||||
|
local status=""
|
||||||
|
[ -s "$GEMINI_OUT" ] && status="${status} Gemini:$(wc -c < "$GEMINI_OUT" | tr -d ' ')b"
|
||||||
|
[ -s "$CODEX_OUT" ] && status="${status} Codex:$(wc -c < "$CODEX_OUT" | tr -d ' ')b"
|
||||||
|
[ -s "$QWEN_OUT" ] && status="${status} Qwen:$(wc -c < "$QWEN_OUT" | tr -d ' ')b"
|
||||||
|
[ -s "$OPENCODE_OUT" ] && status="${status} OpenCode:$(wc -c < "$OPENCODE_OUT" | tr -d ' ')b"
|
||||||
|
|
||||||
|
echo "[${elapsed}s] Progress:${status:-" waiting..."}"
|
||||||
|
|
||||||
|
# Check if all background jobs completed
|
||||||
|
if ! jobs -r 2>/dev/null | grep -q .; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# Start monitor in background before waiting
|
||||||
|
monitor_progress &
|
||||||
|
MONITOR_PID=$!
|
||||||
|
|
||||||
|
# ... (wait commands from Step 3) ...
|
||||||
|
|
||||||
|
# Stop monitor after all CLIs complete
|
||||||
|
kill $MONITOR_PID 2>/dev/null
|
||||||
|
```
|
||||||
|
|
||||||
|
**Simpler alternative:** Just use the status messages already added:
|
||||||
|
- "Starting parallel consultation..." (before launch)
|
||||||
|
- "All CLIs launched. Waiting for responses..." (after launch)
|
||||||
|
- "All CLIs completed. Processing responses..." (after wait)
|
||||||
|
|
||||||
|
### Step 4: Classify Errors and Retry with Backoff
|
||||||
|
|
||||||
|
Classify error types and retry failures with exponential backoff (NOT parallel):
|
||||||
|
|
||||||
|
| Exit Code | Meaning | Action |
|
||||||
|
|-----------|---------|--------|
|
||||||
|
| 0 | Success | Continue |
|
||||||
|
| 124 | Timeout (GNU) | Retry with +60s timeout |
|
||||||
|
| 127 | Command not found | Check PATH resolution - likely a setup issue |
|
||||||
|
| 1-2 | General error | Check stderr AND stdout, retry once |
|
||||||
|
| Other | Unknown | Check stderr AND stdout, retry once |
|
||||||
|
|
||||||
|
Check **BOTH stderr AND stdout** for error patterns (some CLIs embed errors in JSON):
|
||||||
|
|
||||||
|
| Location | Pattern | Error Type | Retry? |
|
||||||
|
|----------|---------|------------|--------|
|
||||||
|
| stderr | `401`, `403`, `invalid.*key`, `unauthorized` | Auth failure | No - fix credentials |
|
||||||
|
| stdout JSON | `"error":`, `"FatalToolExecutionError"` | API/tool error | No - check CLI setup |
|
||||||
|
| stdout JSON | `"type":"error"` | Structured error event | Check message, maybe retry |
|
||||||
|
| stderr | `429`, `rate.*limit`, `quota` | Rate limit | Yes - with backoff |
|
||||||
|
| stderr | `timeout`, `timed out` | Timeout | Yes - increase timeout |
|
||||||
|
| stderr | `connection`, `network`, `ENOTFOUND` | Network error | Yes - with backoff |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Enhanced error classification function
|
||||||
|
classify_error() {
|
||||||
|
local exit_code="$1" err_file="$2" out_file="$3"
|
||||||
|
|
||||||
|
# Exit 127 = command not found (PATH issue from Step 1)
|
||||||
|
if [ "$exit_code" -eq 127 ]; then
|
||||||
|
echo "PATH_ERROR"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Exit 124 = GNU timeout expired
|
||||||
|
if [ "$exit_code" -eq 124 ]; then
|
||||||
|
echo "TIMEOUT"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check stderr for auth errors
|
||||||
|
if grep -qiE '401|403|invalid.*key|unauthorized' "$err_file" 2>/dev/null; then
|
||||||
|
echo "AUTH_ERROR"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check stdout JSON for embedded errors (Qwen, Gemini tool errors)
|
||||||
|
if grep -qiE '"error":|FatalToolExecutionError|"type":"error"' "$out_file" 2>/dev/null; then
|
||||||
|
echo "API_ERROR"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Rate limit
|
||||||
|
if grep -qiE '429|rate.*limit|quota' "$err_file" 2>/dev/null; then
|
||||||
|
echo "RATE_LIMIT"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Network errors
|
||||||
|
if grep -qiE 'connection|network|ENOTFOUND|ETIMEDOUT' "$err_file" 2>/dev/null; then
|
||||||
|
echo "NETWORK_ERROR"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "UNKNOWN_ERROR"
|
||||||
|
}
|
||||||
|
|
||||||
|
retry_with_backoff() {
|
||||||
|
local cli="$1" exit_code="$2" err_file="$3" out_file="$4"
|
||||||
|
local error_type=$(classify_error "$exit_code" "$err_file" "$out_file")
|
||||||
|
|
||||||
|
echo " $cli: $error_type (exit $exit_code)"
|
||||||
|
|
||||||
|
# Don't retry auth or API errors
|
||||||
|
case "$error_type" in
|
||||||
|
AUTH_ERROR|API_ERROR|PATH_ERROR)
|
||||||
|
echo " $cli: Not retrying $error_type"
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Backoff delay: 2 seconds for first retry
|
||||||
|
echo " $cli: Retrying in 2s..."
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# Retry the specific CLI using full paths
|
||||||
|
local extra_timeout=60
|
||||||
|
[ "$error_type" = "TIMEOUT" ] && extra_timeout=90
|
||||||
|
|
||||||
|
case "$cli" in
|
||||||
|
gemini)
|
||||||
|
printf '%s' "$PROMPT" | "$TIMEOUT_CMD" "$((TIMEOUT + extra_timeout))" "$GEMINI_BIN" -m "$GEMINI_MODEL" -o json > "$out_file" 2>"$err_file"
|
||||||
|
;;
|
||||||
|
codex)
|
||||||
|
printf '%s' "$PROMPT" | "$TIMEOUT_CMD" "$((TIMEOUT + extra_timeout))" "$CODEX_BIN" exec --json -m "$CODEX_MODEL" --skip-git-repo-check > "$out_file" 2>"$err_file"
|
||||||
|
;;
|
||||||
|
qwen)
|
||||||
|
printf '%s' "$PROMPT" | "$TIMEOUT_CMD" "$((TIMEOUT + extra_timeout))" "$QWEN_BIN" -p "" -o json > "$out_file" 2>"$err_file"
|
||||||
|
;;
|
||||||
|
opencode)
|
||||||
|
printf '%s' "$PROMPT" | "$TIMEOUT_CMD" "$((TIMEOUT + extra_timeout))" "$OPENCODE_BIN" run -m "$OPENCODE_MODEL" --format json > "$out_file" 2>"$err_file"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# Retry failed CLIs sequentially with backoff
|
||||||
|
[ $GEMINI_EXIT -ne 0 ] && [ -n "$GEMINI_BIN" ] && retry_with_backoff gemini $GEMINI_EXIT "$GEMINI_ERR" "$GEMINI_OUT" && GEMINI_EXIT=$?
|
||||||
|
[ $CODEX_EXIT -ne 0 ] && [ -n "$CODEX_BIN" ] && retry_with_backoff codex $CODEX_EXIT "$CODEX_ERR" "$CODEX_OUT" && CODEX_EXIT=$?
|
||||||
|
[ $QWEN_EXIT -ne 0 ] && [ -n "$QWEN_BIN" ] && retry_with_backoff qwen $QWEN_EXIT "$QWEN_ERR" "$QWEN_OUT" && QWEN_EXIT=$?
|
||||||
|
[ $OPENCODE_EXIT -ne 0 ] && [ -n "$OPENCODE_BIN" ] && retry_with_backoff opencode $OPENCODE_EXIT "$OPENCODE_ERR" "$OPENCODE_OUT" && OPENCODE_EXIT=$?
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 5: Parse Results with jq
|
||||||
|
|
||||||
|
Extract text responses using proper jq filters. Stderr is separate, so JSON parsing won't break.
|
||||||
|
|
||||||
|
**IMPORTANT:** AI responses can be very large (100K+ chars). Truncate to `MAX_RESPONSE_CHARS` to avoid token limit issues when synthesizing.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Response truncation function - prevents token overflow
|
||||||
|
truncate_response() {
|
||||||
|
local response="$1"
|
||||||
|
local max_chars="${2:-$MAX_RESPONSE_CHARS}"
|
||||||
|
local char_count="${#response}"
|
||||||
|
|
||||||
|
if [ "$char_count" -gt "$max_chars" ]; then
|
||||||
|
# Keep first 70% and last 20% for context
|
||||||
|
local head_chars=$((max_chars * 70 / 100))
|
||||||
|
local tail_chars=$((max_chars * 20 / 100))
|
||||||
|
|
||||||
|
local head_part="${response:0:$head_chars}"
|
||||||
|
local tail_part="${response: -$tail_chars}"
|
||||||
|
|
||||||
|
echo "${head_part}
|
||||||
|
|
||||||
|
... [TRUNCATED: ${char_count} chars total, showing first ${head_chars} and last ${tail_chars}] ...
|
||||||
|
|
||||||
|
${tail_part}"
|
||||||
|
else
|
||||||
|
echo "$response"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Gemini - check for errors first, then extract response
|
||||||
|
if grep -q '"error"' "$GEMINI_OUT" 2>/dev/null; then
|
||||||
|
GEMINI_RESPONSE=""
|
||||||
|
else
|
||||||
|
GEMINI_RESPONSE=$(jq -r '.response // .candidates[0].content.parts[0].text // empty' "$GEMINI_OUT" 2>/dev/null)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Codex - NDJSON format with multiple item types
|
||||||
|
# NOTE: Codex outputs reasoning traces, NOT agent_message. Try multiple extraction patterns.
|
||||||
|
# Pattern 1: Look for message items with content array containing output_text
|
||||||
|
CODEX_RESPONSE=$(grep '"item.completed"' "$CODEX_OUT" 2>/dev/null | \
|
||||||
|
jq -rs '
|
||||||
|
[.[] | select(.item.type == "message" and .item.content)] |
|
||||||
|
last |
|
||||||
|
.item.content |
|
||||||
|
if type == "array" then
|
||||||
|
[.[] | select(.type == "output_text") | .text] | join("")
|
||||||
|
else
|
||||||
|
.
|
||||||
|
end
|
||||||
|
' 2>/dev/null)
|
||||||
|
|
||||||
|
# Pattern 2: Fall back to reasoning summaries if no message found
|
||||||
|
if [ -z "$CODEX_RESPONSE" ]; then
|
||||||
|
CODEX_RESPONSE=$(grep '"item.completed"' "$CODEX_OUT" 2>/dev/null | \
|
||||||
|
jq -rs '[.[] | select(.item.type == "reasoning") | .item.text // .item.summary // empty] | join("\n\n")' 2>/dev/null)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Qwen - filter out error lines, then parse standard JSON
|
||||||
|
QWEN_RESPONSE=$(grep -v 'FatalToolExecutionError' "$QWEN_OUT" 2>/dev/null | \
|
||||||
|
jq -rs 'last | .response // empty' 2>/dev/null)
|
||||||
|
|
||||||
|
# OpenCode - NDJSON, concatenate all text parts
|
||||||
|
OPENCODE_RESPONSE=$(grep '"type":"text"' "$OPENCODE_OUT" 2>/dev/null | \
|
||||||
|
jq -rs '[.[].part.text // .[].text // empty] | join("")' 2>/dev/null)
|
||||||
|
|
||||||
|
# Fallback to raw output if jq parsing fails (truncated for safety)
|
||||||
|
[ -z "$GEMINI_RESPONSE" ] && [ -s "$GEMINI_OUT" ] && GEMINI_RESPONSE=$(head -c "$MAX_RESPONSE_CHARS" "$GEMINI_OUT")
|
||||||
|
[ -z "$CODEX_RESPONSE" ] && [ -s "$CODEX_OUT" ] && CODEX_RESPONSE=$(head -c "$MAX_RESPONSE_CHARS" "$CODEX_OUT")
|
||||||
|
[ -z "$QWEN_RESPONSE" ] && [ -s "$QWEN_OUT" ] && QWEN_RESPONSE=$(head -c "$MAX_RESPONSE_CHARS" "$QWEN_OUT")
|
||||||
|
[ -z "$OPENCODE_RESPONSE" ] && [ -s "$OPENCODE_OUT" ] && OPENCODE_RESPONSE=$(head -c "$MAX_RESPONSE_CHARS" "$OPENCODE_OUT")
|
||||||
|
|
||||||
|
# Apply truncation to prevent token overflow during synthesis
|
||||||
|
GEMINI_RESPONSE=$(truncate_response "$GEMINI_RESPONSE")
|
||||||
|
CODEX_RESPONSE=$(truncate_response "$CODEX_RESPONSE")
|
||||||
|
QWEN_RESPONSE=$(truncate_response "$QWEN_RESPONSE")
|
||||||
|
OPENCODE_RESPONSE=$(truncate_response "$OPENCODE_RESPONSE")
|
||||||
|
|
||||||
|
# CRITICAL: Write to SEPARATE files to avoid 25K token limit
|
||||||
|
# Claude can read each file individually during synthesis
|
||||||
|
echo "$GEMINI_RESPONSE" > /tmp/consult_gemini_response.txt
|
||||||
|
echo "$CODEX_RESPONSE" > /tmp/consult_codex_response.txt
|
||||||
|
echo "$QWEN_RESPONSE" > /tmp/consult_qwen_response.txt
|
||||||
|
echo "$OPENCODE_RESPONSE" > /tmp/consult_opencode_response.txt
|
||||||
|
|
||||||
|
# Report file sizes for verification
|
||||||
|
echo "Response sizes (chars):"
|
||||||
|
echo " Gemini: ${#GEMINI_RESPONSE}"
|
||||||
|
echo " Codex: ${#CODEX_RESPONSE}"
|
||||||
|
echo " Qwen: ${#QWEN_RESPONSE}"
|
||||||
|
echo " OpenCode: ${#OPENCODE_RESPONSE}"
|
||||||
|
echo " TOTAL: $((${#GEMINI_RESPONSE} + ${#CODEX_RESPONSE} + ${#QWEN_RESPONSE} + ${#OPENCODE_RESPONSE}))"
|
||||||
|
```
|
||||||
|
|
||||||
|
**IMPORTANT - Token Limit Prevention:**
|
||||||
|
|
||||||
|
To avoid the 25K token limit when reading responses:
|
||||||
|
|
||||||
|
1. **NEVER write a combined file** - Don't concatenate all responses into one file
|
||||||
|
2. **Read each response file separately** during synthesis:
|
||||||
|
- `Read(/tmp/consult_gemini_response.txt)`
|
||||||
|
- `Read(/tmp/consult_codex_response.txt)`
|
||||||
|
- `Read(/tmp/consult_qwen_response.txt)`
|
||||||
|
- `Read(/tmp/consult_opencode_response.txt)`
|
||||||
|
3. **Max safe combined size**: ~80K chars (4 × 20K) ≈ 20K tokens, well under 25K limit
|
||||||
|
4. **If responses are still too large**: Reduce `CONSULT_MAX_RESPONSE_CHARS` to 15000
|
||||||
|
|
||||||
|
### Step 6: Report Status
|
||||||
|
|
||||||
|
Show which AIs responded:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo "---"
|
||||||
|
echo "Consultation Results:"
|
||||||
|
[ -n "$GEMINI_RESPONSE" ] && echo " Gemini: ✓" || echo " Gemini: ✗ ($(head -1 "$GEMINI_ERR" 2>/dev/null || echo 'no response'))"
|
||||||
|
[ -n "$CODEX_RESPONSE" ] && echo " Codex: ✓" || echo " Codex: ✗ ($(head -1 "$CODEX_ERR" 2>/dev/null || echo 'no response'))"
|
||||||
|
[ -n "$QWEN_RESPONSE" ] && echo " Qwen: ✓" || echo " Qwen: ✗ ($(head -1 "$QWEN_ERR" 2>/dev/null || echo 'no response'))"
|
||||||
|
[ -n "$OPENCODE_RESPONSE" ] && echo " OpenCode: ✓" || echo " OpenCode: ✗ ($(head -1 "$OPENCODE_ERR" 2>/dev/null || echo 'no response'))"
|
||||||
|
|
||||||
|
# Count successful responses
|
||||||
|
SUCCESS_COUNT=0
|
||||||
|
[ -n "$GEMINI_RESPONSE" ] && SUCCESS_COUNT=$((SUCCESS_COUNT + 1))
|
||||||
|
[ -n "$CODEX_RESPONSE" ] && SUCCESS_COUNT=$((SUCCESS_COUNT + 1))
|
||||||
|
[ -n "$QWEN_RESPONSE" ] && SUCCESS_COUNT=$((SUCCESS_COUNT + 1))
|
||||||
|
[ -n "$OPENCODE_RESPONSE" ] && SUCCESS_COUNT=$((SUCCESS_COUNT + 1))
|
||||||
|
|
||||||
|
echo " Consensus basis: $SUCCESS_COUNT/4 AIs responded"
|
||||||
|
echo "---"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 7: Synthesize Response
|
||||||
|
|
||||||
|
If `--verbose` was specified, show individual responses first.
|
||||||
|
|
||||||
|
#### Tiered Synthesis Strategy
|
||||||
|
|
||||||
|
Choose synthesis approach based on **total response size**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Calculate total response size
|
||||||
|
TOTAL_CHARS=$((${#GEMINI_RESPONSE} + ${#CODEX_RESPONSE} + ${#QWEN_RESPONSE} + ${#OPENCODE_RESPONSE}))
|
||||||
|
echo "Total response size: $TOTAL_CHARS chars"
|
||||||
|
```
|
||||||
|
|
||||||
|
| Tier | Total Size | Strategy |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| **Small** | < 30K chars | Read all responses directly, synthesize in one pass |
|
||||||
|
| **Medium** | 30K-80K chars | Summarize each response first, then synthesize summaries |
|
||||||
|
| **Large** | > 80K chars | Process incrementally: read → summarize → integrate one at a time |
|
||||||
|
|
||||||
|
**Tier 1 (Small):** Direct synthesis - read all and combine.
|
||||||
|
|
||||||
|
**Tier 2 (Medium):** First summarize each response:
|
||||||
|
|
||||||
|
```
|
||||||
|
For each AI response, extract:
|
||||||
|
1. Main recommendation/answer (1-2 sentences)
|
||||||
|
2. Key reasoning/evidence (2-3 bullet points)
|
||||||
|
3. Notable caveats or alternatives
|
||||||
|
4. Unique insights not mentioned by others
|
||||||
|
```
|
||||||
|
|
||||||
|
**Tier 3 (Large):** Incremental synthesis:
|
||||||
|
1. Read + summarize Gemini response
|
||||||
|
2. Read Codex, compare to Gemini summary, update synthesis
|
||||||
|
3. Read Qwen, integrate new points
|
||||||
|
4. Read OpenCode, finalize synthesis
|
||||||
|
|
||||||
|
#### Synthesis Guidelines
|
||||||
|
|
||||||
|
Combine all perspectives into a **single unified response**:
|
||||||
|
|
||||||
|
- Do NOT show separate "According to Gemini/Codex/Qwen/OpenCode" sections
|
||||||
|
- Integrate the best insights from all responding AIs into one cohesive answer
|
||||||
|
- If they agree, present the consensus with high confidence
|
||||||
|
- If they disagree, synthesize a balanced view incorporating all perspectives
|
||||||
|
- Present as your own synthesized analysis
|
||||||
|
- **Deduplicate**: AI responses often overlap - don't repeat the same point multiple times
|
||||||
|
|
||||||
|
**Confidence indicator** (optional footer):
|
||||||
|
- **High** (4/4 or 3/3 agree on core points)
|
||||||
|
- **Medium** (3/4 agree, or 2/2 with partial overlap)
|
||||||
|
- **Low** (significant disagreement or only 1-2 responses)
|
||||||
|
|
||||||
|
### Step 8: Cleanup
|
||||||
|
|
||||||
|
Cleanup happens automatically via the trap, but you can also explicitly call:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cleanup
|
||||||
|
```
|
||||||
|
|
||||||
|
## Exit Codes
|
||||||
|
|
||||||
|
Use distinct exit codes for scripting:
|
||||||
|
|
||||||
|
| Code | Meaning |
|
||||||
|
|------|---------|
|
||||||
|
| 0 | Success - at least 2 AIs responded |
|
||||||
|
| 1 | Partial success - only 1 AI responded |
|
||||||
|
| 2 | All AIs failed - auth errors |
|
||||||
|
| 3 | All AIs failed - rate limit |
|
||||||
|
| 4 | All AIs failed - timeout |
|
||||||
|
| 5 | All AIs failed - other errors |
|
||||||
|
| 10 | No AI CLIs installed |
|
||||||
|
| 11 | Missing required dependency (jq) |
|
||||||
|
|
||||||
|
## Error Handling Summary
|
||||||
|
|
||||||
|
### CLI Installation
|
||||||
|
If a CLI is missing, continue with others. Provide installation guidance only when asked:
|
||||||
|
- **Gemini**: `npm install -g @anthropic-ai/gemini-cli` or `pip install google-generativeai`
|
||||||
|
- **Codex**: `npm install -g @openai/codex`
|
||||||
|
- **Qwen**: `npm install -g @anthropic-ai/qwen-cli`
|
||||||
|
- **OpenCode**: See https://opencode.ai/docs/installation
|
||||||
|
|
||||||
|
### Graceful Degradation
|
||||||
|
- **3/4 respond**: Full synthesis, note one was unavailable
|
||||||
|
- **2/4 respond**: Synthesis with reduced confidence
|
||||||
|
- **1/4 responds**: Report single response, offer Claude-only alternative
|
||||||
|
- **0/4 respond**: Report all errors, provide Claude-only response
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
User: `/consult How should I structure my React state management?`
|
||||||
|
|
||||||
|
1. **Verify**: Check available CLIs and jq
|
||||||
|
2. **Prepare**: "What are best practices for structuring React state management? Consider scalability, maintainability, and performance."
|
||||||
|
3. **Execute**: Run available CLIs in parallel with 120s timeout
|
||||||
|
4. **Retry**: Any failures get one retry with 2s backoff
|
||||||
|
5. **Parse**: Extract responses using jq:
|
||||||
|
- Gemini: `.response`
|
||||||
|
- Codex: NDJSON `.item.text` from `agent_message`
|
||||||
|
- Qwen: `.response`
|
||||||
|
- OpenCode: NDJSON `.part.text`
|
||||||
|
6. **Report**: "Gemini ✓ | Codex ✓ | Qwen ✓ | OpenCode ✓ (4/4)"
|
||||||
|
7. **Synthesize**: "For React state management, consider a layered approach: local state for UI concerns, Context for shared app state, and a dedicated store (Redux/Zustand) for complex global state. Key principles include..."
|
||||||
45
plugin.lock.json
Normal file
45
plugin.lock.json
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"$schema": "internal://schemas/plugin.lock.v1.json",
|
||||||
|
"pluginId": "gh:wlevan3/claude-plugins:multi-ai-consult",
|
||||||
|
"normalized": {
|
||||||
|
"repo": null,
|
||||||
|
"ref": "refs/tags/v20251128.0",
|
||||||
|
"commit": "20b05a091e399d4630a3fc1cadba09569b3a9703",
|
||||||
|
"treeHash": "be9d199d617753156f100dc2c6beb200aee3e0df3f2a277ecb716e0a9c2807e6",
|
||||||
|
"generatedAt": "2025-11-28T10:29:04.285270Z",
|
||||||
|
"toolVersion": "publish_plugins.py@0.2.0"
|
||||||
|
},
|
||||||
|
"origin": {
|
||||||
|
"remote": "git@github.com:zhongweili/42plugin-data.git",
|
||||||
|
"branch": "master",
|
||||||
|
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
|
||||||
|
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
|
||||||
|
},
|
||||||
|
"manifest": {
|
||||||
|
"name": "multi-ai-consult",
|
||||||
|
"description": "Consult Gemini, Codex, Qwen, and OpenCode AIs in parallel and synthesize responses",
|
||||||
|
"version": "1.0.0"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"path": "README.md",
|
||||||
|
"sha256": "d7baa9e23dfc7e0a6be2813e36a34d18a412d648db1b98476701c0ed4eecfb3b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": ".claude-plugin/plugin.json",
|
||||||
|
"sha256": "17d7d32a9d3e76675bf89081b6ad3c3e0b570d5181eb0bac132e8c02ce749947"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "commands/consult.md",
|
||||||
|
"sha256": "915c85a560ee470232647ac4a8ca2bb737dafe3d9928e1bcf427769b07224b41"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dirSha256": "be9d199d617753156f100dc2c6beb200aee3e0df3f2a277ecb716e0a9c2807e6"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"scannedAt": null,
|
||||||
|
"scannerVersion": null,
|
||||||
|
"flags": []
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user