smoothie-code 1.0.0

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.
package/README.md ADDED
@@ -0,0 +1,82 @@
1
+ # Smoothie
2
+
3
+ <p align="center">
4
+ <img src="banner-v2.svg" alt="Smoothie — multi-model review for Claude Code" width="100%">
5
+ </p>
6
+
7
+ Multi-model review plugin for Claude Code. Sends your problem or plan to multiple AI models simultaneously, then Claude judges all responses and serves you one blended result.
8
+
9
+ **Two model tracks:**
10
+ - **Codex** — Codex CLI, authenticated via ChatGPT account OAuth
11
+ - **OpenRouter** — single API key, models selected at install time from a live ranked list
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ git clone https://github.com/hotairbag/smoothie && cd smoothie && bash install.sh
17
+ ```
18
+
19
+ The installer walks you through everything: dependencies, Codex auth, OpenRouter key, and model selection.
20
+
21
+ Restart Claude Code after install.
22
+
23
+ ## Usage
24
+
25
+ ### Slash command
26
+ ```
27
+ /smoothie <your problem or question>
28
+ ```
29
+
30
+ ### Auto-blend (plans)
31
+ When enabled, every plan is automatically reviewed by all models before you see it. Claude revises the plan with their feedback, then presents the improved version for approval. Zero effort.
32
+
33
+ Enable during install, or toggle anytime in `config.json`:
34
+ ```json
35
+ { "auto_blend": true }
36
+ ```
37
+
38
+ Adds 30-90s to plan approval while models respond.
39
+
40
+ ### Manage models
41
+ ```bash
42
+ smoothie models # re-pick from top models
43
+ smoothie models add openai/gpt-5.4
44
+ smoothie models remove openai/gpt-5.4
45
+ smoothie models list
46
+ ```
47
+ No restart needed — config is read fresh on each blend.
48
+
49
+ ## How it works
50
+
51
+ ```
52
+ Claude Code
53
+ |
54
+ |-- /smoothie <context> <- manual slash command
55
+ |-- PreToolUse hook <- auto-blend on ExitPlanMode
56
+ \-- MCP Server
57
+ \-- smoothie_blend(prompt)
58
+ |-- Queries all models in parallel
59
+ |-- Streams live progress to terminal
60
+ \-- Returns all responses to Claude
61
+ ```
62
+
63
+ **Auto-blend flow:**
64
+ ```
65
+ Claude presents plan → ExitPlanMode hook fires → Smoothie blend runs
66
+ → Results injected as context → Claude revises plan → You approve
67
+ ```
68
+
69
+ Claude acts as judge. Raw model outputs are never shown. Claude absorbs everything and hands you one result.
70
+
71
+ ## File overview
72
+
73
+ | File | Purpose |
74
+ |---|---|
75
+ | `src/index.ts` | MCP server exposing `smoothie_blend` tool |
76
+ | `src/blend-cli.ts` | Standalone blend runner (used by hooks) |
77
+ | `src/select-models.ts` | Interactive model picker (OpenRouter API) |
78
+ | `auto-blend-hook.sh` | PreToolUse hook — auto-blends plans |
79
+ | `plan-hook.sh` | Stop hook — plan mode hint (fallback) |
80
+ | `install.sh` | One-command installer |
81
+ | `config.json` | Model selection + auto_blend flag |
82
+ | `.env` | API keys (gitignored) |
@@ -0,0 +1,94 @@
1
+ #!/bin/bash
2
+ #
3
+ # auto-blend-hook.sh — PreToolUse hook for ExitPlanMode
4
+ #
5
+ # Intercepts plan approval, runs Smoothie blend on the plan,
6
+ # and injects results back so Claude revises before you see it.
7
+ #
8
+
9
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
10
+
11
+ # Read hook input from stdin
12
+ INPUT=$(cat)
13
+
14
+ # Check if auto-blend is enabled in config
15
+ if [ -f "$SCRIPT_DIR/config.json" ]; then
16
+ AUTO_ENABLED=$(node -e "
17
+ try {
18
+ const c = JSON.parse(require('fs').readFileSync('$SCRIPT_DIR/config.json','utf8'));
19
+ console.log(c.auto_blend === true ? 'true' : 'false');
20
+ } catch(e) { console.log('false'); }
21
+ " 2>/dev/null)
22
+
23
+ if [ "$AUTO_ENABLED" != "true" ]; then
24
+ # Auto-blend disabled — allow ExitPlanMode without intervention
25
+ echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow"}}'
26
+ exit 0
27
+ fi
28
+ fi
29
+
30
+ # Extract transcript path
31
+ TRANSCRIPT_PATH=$(echo "$INPUT" | python3 -c "
32
+ import sys, json
33
+ try:
34
+ d = json.load(sys.stdin)
35
+ print(d.get('transcript_path', ''))
36
+ except:
37
+ print('')
38
+ " 2>/dev/null)
39
+
40
+ if [ -z "$TRANSCRIPT_PATH" ] || [ ! -f "$TRANSCRIPT_PATH" ]; then
41
+ echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow"}}'
42
+ exit 0
43
+ fi
44
+
45
+ # Extract the plan from the last ~4000 chars of the transcript
46
+ PLAN_CONTEXT=$(tail -c 4000 "$TRANSCRIPT_PATH" 2>/dev/null)
47
+
48
+ if [ -z "$PLAN_CONTEXT" ]; then
49
+ echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow"}}'
50
+ exit 0
51
+ fi
52
+
53
+ # Build the review prompt
54
+ REVIEW_PROMPT="You are reviewing a plan that Claude Code generated. Analyze it for:
55
+ - Missing steps or edge cases
56
+ - Better approaches or optimizations
57
+ - Potential bugs or issues
58
+ - Security concerns
59
+
60
+ Here is the plan context (from the conversation transcript):
61
+
62
+ $PLAN_CONTEXT
63
+
64
+ Provide concise, actionable feedback. Focus only on things that should change."
65
+
66
+ # Run the blend (progress shows on stderr, results on stdout)
67
+ BLEND_RESULTS=$(echo "$REVIEW_PROMPT" | node "$SCRIPT_DIR/dist/blend-cli.js" 2>/dev/stderr)
68
+
69
+ if [ -z "$BLEND_RESULTS" ]; then
70
+ echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow"}}'
71
+ exit 0
72
+ fi
73
+
74
+ # Build the additionalContext string
75
+ CONTEXT="🧃 Smoothie auto-blend results — multiple models reviewed this plan:
76
+
77
+ $BLEND_RESULTS
78
+
79
+ Revise the plan above based on this feedback. Incorporate valid suggestions, discard irrelevant ones. Present the improved plan to the user."
80
+
81
+ # Return: allow ExitPlanMode but inject blend results
82
+ node -e "
83
+ const ctx = $(echo "$CONTEXT" | python3 -c "import sys,json; print(json.dumps(sys.stdin.read()))" 2>/dev/null);
84
+ console.log(JSON.stringify({
85
+ hookSpecificOutput: {
86
+ hookEventName: 'PreToolUse',
87
+ permissionDecision: 'allow',
88
+ permissionDecisionReason: 'Smoothie auto-blend completed',
89
+ additionalContext: ctx
90
+ }
91
+ }));
92
+ "
93
+
94
+ exit 0
package/banner-v2.svg ADDED
@@ -0,0 +1,307 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 440" fill="none">
2
+ <style>
3
+ /* === PERSISTENT: fade in once, stay forever === */
4
+ @keyframes shineOnce {
5
+ 0% { opacity: 0; filter: blur(10px) brightness(2); }
6
+ 40% { opacity: 1; filter: blur(3px) brightness(1.3); }
7
+ 100% { opacity: 1; filter: blur(0) brightness(1); }
8
+ }
9
+ @keyframes fadeOnce {
10
+ from { opacity: 0; transform: translateY(8px); }
11
+ to { opacity: 1; transform: translateY(0); }
12
+ }
13
+ .persist-title { opacity: 0; animation: shineOnce 1.2s ease 0.1s forwards; }
14
+ .persist-sub { opacity: 0; animation: fadeOnce 0.7s ease 0.4s forwards; }
15
+ .persist-term { opacity: 0; animation: fadeOnce 0.5s ease 0.7s forwards; }
16
+ .persist-dollar{ opacity: 0; animation: fadeOnce 0.3s ease 1s forwards; }
17
+
18
+ /* === LOOPING: 14s cycle === */
19
+ /* fade=85-89%, pause=89-100%(1.54s)+0-1%(0.14s)=1.68s total dead */
20
+
21
+ @keyframes pathLoop {
22
+ 0%, 14% { opacity: 0; }
23
+ 17% { opacity: 1; }
24
+ 85% { opacity: 1; }
25
+ 89% { opacity: 0; }
26
+ 100% { opacity: 0; }
27
+ }
28
+ @keyframes mA { 0%,22% { opacity:0; transform:translateY(8px); } 25% { opacity:1; transform:translateY(0); } 85% { opacity:1; } 89% { opacity:0; } 100% { opacity:0; } }
29
+ @keyframes mB { 0%,24% { opacity:0; transform:translateY(8px); } 27% { opacity:1; transform:translateY(0); } 85% { opacity:1; } 89% { opacity:0; } 100% { opacity:0; } }
30
+ @keyframes mC { 0%,26% { opacity:0; transform:translateY(8px); } 29% { opacity:1; transform:translateY(0); } 85% { opacity:1; } 89% { opacity:0; } 100% { opacity:0; } }
31
+ @keyframes mD { 0%,28% { opacity:0; transform:translateY(8px); } 31% { opacity:1; transform:translateY(0); } 85% { opacity:1; } 89% { opacity:0; } 100% { opacity:0; } }
32
+
33
+ @keyframes spin { to { transform: rotate(360deg); } }
34
+ @keyframes svA { 0%,22% { opacity:0; } 23% { opacity:1; } 36% { opacity:1; } 37% { opacity:0; } 100% { opacity:0; } }
35
+ @keyframes svB { 0%,24% { opacity:0; } 25% { opacity:1; } 39% { opacity:1; } 40% { opacity:0; } 100% { opacity:0; } }
36
+ @keyframes svC { 0%,26% { opacity:0; } 27% { opacity:1; } 42% { opacity:1; } 43% { opacity:0; } 100% { opacity:0; } }
37
+ @keyframes svD { 0%,28% { opacity:0; } 29% { opacity:1; } 45% { opacity:1; } 46% { opacity:0; } 100% { opacity:0; } }
38
+
39
+ @keyframes ckA { 0%,36% { opacity:0; transform:scale(0); } 38% { opacity:1; transform:scale(1.15); } 39% { transform:scale(1); } 85% { opacity:1; } 89% { opacity:0; } 100% { opacity:0; } }
40
+ @keyframes ckB { 0%,39% { opacity:0; transform:scale(0); } 41% { opacity:1; transform:scale(1.15); } 42% { transform:scale(1); } 85% { opacity:1; } 89% { opacity:0; } 100% { opacity:0; } }
41
+ @keyframes ckC { 0%,42% { opacity:0; transform:scale(0); } 44% { opacity:1; transform:scale(1.15); } 45% { transform:scale(1); } 85% { opacity:1; } 89% { opacity:0; } 100% { opacity:0; } }
42
+ @keyframes ckD { 0%,45% { opacity:0; transform:scale(0); } 47% { opacity:1; transform:scale(1.15); } 48% { transform:scale(1); } 85% { opacity:1; } 89% { opacity:0; } 100% { opacity:0; } }
43
+
44
+ @keyframes dA { 0%,36% { opacity:0; } 37% { opacity:1; } 85% { opacity:1; } 89% { opacity:0; } 100% { opacity:0; } }
45
+ @keyframes dB { 0%,39% { opacity:0; } 40% { opacity:1; } 85% { opacity:1; } 89% { opacity:0; } 100% { opacity:0; } }
46
+ @keyframes dC { 0%,42% { opacity:0; } 43% { opacity:1; } 85% { opacity:1; } 89% { opacity:0; } 100% { opacity:0; } }
47
+ @keyframes dD { 0%,45% { opacity:0; } 46% { opacity:1; } 85% { opacity:1; } 89% { opacity:0; } 100% { opacity:0; } }
48
+
49
+ /* Output: appear at 53%, hold until 85%, fade 89% → 4.5s visible */
50
+ @keyframes outShine {
51
+ 0%, 53% { opacity:0; filter: blur(10px) brightness(2); }
52
+ 57% { opacity:1; filter: blur(3px) brightness(1.3); }
53
+ 59% { opacity:1; filter: blur(0) brightness(1); }
54
+ 85% { opacity:1; }
55
+ 89% { opacity:0; }
56
+ 100% { opacity:0; }
57
+ }
58
+ @keyframes olA { 0%,57% { opacity:0; transform:translateY(8px); } 60% { opacity:1; transform:translateY(0); } 85% { opacity:1; } 89% { opacity:0; } 100% { opacity:0; } }
59
+ @keyframes olB { 0%,59% { opacity:0; transform:translateY(8px); } 62% { opacity:1; transform:translateY(0); } 85% { opacity:1; } 89% { opacity:0; } 100% { opacity:0; } }
60
+ @keyframes olC { 0%,61% { opacity:0; transform:translateY(8px); } 64% { opacity:1; transform:translateY(0); } 85% { opacity:1; } 89% { opacity:0; } 100% { opacity:0; } }
61
+ @keyframes labelFade {
62
+ 0%,14% { opacity:0; } 17% { opacity:0.7; } 85% { opacity:0.7; } 89% { opacity:0; } 100% { opacity:0; }
63
+ }
64
+ @keyframes judgeLabelFade {
65
+ 0%,50% { opacity:0; } 53% { opacity:0.7; } 85% { opacity:0.7; } 89% { opacity:0; } 100% { opacity:0; }
66
+ }
67
+ @keyframes gentleFloat {
68
+ 0%, 100% { transform: translateY(0); }
69
+ 50% { transform: translateY(-4px); }
70
+ }
71
+
72
+ /* Class assignments */
73
+ .paths { animation: pathLoop 14s ease infinite; }
74
+ .label-par { animation: labelFade 14s ease infinite; }
75
+ .label-judge { animation: judgeLabelFade 14s ease infinite; }
76
+ .mA { animation: mA 14s ease infinite; }
77
+ .mB { animation: mB 14s ease infinite; }
78
+ .mC { animation: mC 14s ease infinite; }
79
+ .mD { animation: mD 14s ease infinite; }
80
+ .spA { transform-origin: 448px 165px; animation: spin 0.7s linear infinite, svA 14s ease infinite; }
81
+ .spB { transform-origin: 448px 225px; animation: spin 0.7s linear infinite, svB 14s ease infinite; }
82
+ .spC { transform-origin: 448px 285px; animation: spin 0.7s linear infinite, svC 14s ease infinite; }
83
+ .spD { transform-origin: 448px 345px; animation: spin 0.7s linear infinite, svD 14s ease infinite; }
84
+ .ckA { transform-origin: 448px 165px; animation: ckA 14s ease infinite; }
85
+ .ckB { transform-origin: 448px 225px; animation: ckB 14s ease infinite; }
86
+ .ckC { transform-origin: 448px 285px; animation: ckC 14s ease infinite; }
87
+ .ckD { transform-origin: 448px 345px; animation: ckD 14s ease infinite; }
88
+ .dnA { animation: dA 14s ease infinite; }
89
+ .dnB { animation: dB 14s ease infinite; }
90
+ .dnC { animation: dC 14s ease infinite; }
91
+ .dnD { animation: dD 14s ease infinite; }
92
+ .output-card { animation: outShine 14s ease infinite; }
93
+ .output-float { animation: gentleFloat 3s ease-in-out infinite; }
94
+ .olA { animation: olA 14s ease infinite; }
95
+ .olB { animation: olB 14s ease infinite; }
96
+ .olC { animation: olC 14s ease infinite; }
97
+ </style>
98
+
99
+ <defs>
100
+ <linearGradient id="og" x1="0" y1="0" x2="1" y2="0">
101
+ <stop offset="0%" stop-color="#f78166" stop-opacity="0.2"/>
102
+ <stop offset="100%" stop-color="#f78166"/>
103
+ </linearGradient>
104
+ <filter id="sh" x="-8%" y="-8%" width="116%" height="120%">
105
+ <feDropShadow dx="0" dy="2" stdDeviation="8" flood-color="#000" flood-opacity="0.35"/>
106
+ </filter>
107
+
108
+ <!-- Typewriter clip: grows to reveal text, shrinks on loop reset -->
109
+ <clipPath id="type-clip">
110
+ <rect x="72" y="250" width="0" height="30">
111
+ <animate attributeName="width" values="0;0;80;80;0;0" keyTimes="0;0.01;0.10;0.82;0.85;1" dur="14s" repeatCount="indefinite"/>
112
+ </rect>
113
+ </clipPath>
114
+ </defs>
115
+
116
+ <!-- ======= BACKGROUND (always visible) ======= -->
117
+ <rect width="1200" height="440" fill="#0d1117"/>
118
+ <pattern id="dots" width="24" height="24" patternUnits="userSpaceOnUse">
119
+ <circle cx="12" cy="12" r="0.5" fill="#f0f6fc" opacity="0.06"/>
120
+ </pattern>
121
+ <rect width="1200" height="440" fill="url(#dots)"/>
122
+ <line x1="100" y1="105" x2="1100" y2="105" stroke="#21262d" stroke-width="0.5"/>
123
+ <line x1="100" y1="418" x2="1100" y2="418" stroke="#21262d" stroke-width="0.5"/>
124
+
125
+ <!-- ======= PERSISTENT: Title + logo (fade in once, stay) ======= -->
126
+ <g class="persist-title">
127
+ <!-- Smoothie cup icon -->
128
+ <g transform="translate(480, 24)" fill="none" stroke="#f0f6fc" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
129
+ <!-- Cup body (trapezoid) -->
130
+ <path d="M 4 14 L 7 34 L 25 34 L 28 14"/>
131
+ <!-- Lid -->
132
+ <path d="M 1 11 L 31 11 C 31 11 31 14 16 14 C 1 14 1 11 1 11"/>
133
+ <!-- Dome top -->
134
+ <path d="M 5 11 C 5 5 11 1 16 1 C 21 1 27 5 27 11"/>
135
+ <!-- Straw -->
136
+ <line x1="20" y1="1" x2="27" y2="-8"/>
137
+ <line x1="27" y1="-8" x2="30" y2="-8"/>
138
+ <!-- Drip details -->
139
+ <path d="M 9 14 C 9 17 12 17 12 14" opacity="0.5"/>
140
+ <path d="M 18 14 C 18 17 21 17 21 14" opacity="0.5"/>
141
+ </g>
142
+ <text x="620" y="55" text-anchor="middle" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="40" font-weight="700" fill="#f0f6fc" letter-spacing="-0.5">Smoothie</text>
143
+ </g>
144
+ <text class="persist-sub" x="600" y="85" text-anchor="middle" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="14" fill="#7d8590" letter-spacing="0.5">multi-model review for Claude Code</text>
145
+
146
+ <!-- ======= PERSISTENT: Terminal box (fade in once, stay) ======= -->
147
+ <g class="persist-term">
148
+ <rect x="42" y="225" width="195" height="65" rx="10" fill="#161b22" stroke="#30363d" stroke-width="1" filter="url(#sh)"/>
149
+ <circle cx="60" cy="240" r="4" fill="#f85149" fill-opacity="0.7"/>
150
+ <circle cx="73" cy="240" r="4" fill="#d29922" fill-opacity="0.7"/>
151
+ <circle cx="86" cy="240" r="4" fill="#3fb950" fill-opacity="0.7"/>
152
+ </g>
153
+ <g class="persist-dollar">
154
+ <text x="58" y="272" font-family="'SF Mono', 'Fira Code', monospace" font-size="13" fill="#7d8590">$</text>
155
+ </g>
156
+
157
+ <!-- ======= LOOPING: Typewriter "/smoothie" ======= -->
158
+ <g clip-path="url(#type-clip)">
159
+ <text x="72" y="272" font-family="'SF Mono', 'Fira Code', monospace" font-size="13" fill="#f0f6fc">/smoothie</text>
160
+ </g>
161
+
162
+ <!-- Cursor: follows typing, blinks after -->
163
+ <rect y="260" width="7" height="14" rx="1" fill="#f78166">
164
+ <animate attributeName="x" values="72;72;148;148;148;148;72;72" keyTimes="0;0.01;0.10;0.11;0.13;0.82;0.85;1" dur="14s" repeatCount="indefinite"/>
165
+ <animate attributeName="opacity" values="0;1;1;0;1;0;1;1;0;0" keyTimes="0;0.01;0.10;0.105;0.11;0.115;0.13;0.82;0.85;1" dur="14s" repeatCount="indefinite"/>
166
+ </rect>
167
+
168
+ <!-- ======= LOOPING: Branching paths ======= -->
169
+ <g class="paths" stroke="#30363d" stroke-width="1.5" fill="none" stroke-dasharray="4 4">
170
+ <path d="M 250 258 L 320 258 L 320 165 L 430 165"/>
171
+ <path d="M 250 258 L 320 258 L 320 225 L 430 225"/>
172
+ <path d="M 250 258 L 320 258 L 320 285 L 430 285"/>
173
+ <path d="M 250 258 L 320 258 L 320 345 L 430 345"/>
174
+ <path d="M 680 165 L 790 165 L 790 258 L 860 258"/>
175
+ <path d="M 680 225 L 790 225 L 790 258 L 860 258"/>
176
+ <path d="M 680 285 L 790 285 L 790 258 L 860 258"/>
177
+ <path d="M 680 345 L 790 345 L 790 258 L 860 258"/>
178
+ </g>
179
+ <g class="paths">
180
+ <rect x="318" y="256" width="4" height="4" rx="1" fill="#484f58"/>
181
+ <rect x="788" y="256" width="4" height="4" rx="1" fill="#484f58"/>
182
+ </g>
183
+ <text class="label-par" x="285" y="240" text-anchor="middle" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="10" fill="#f78166">parallel query</text>
184
+ <text class="label-judge" x="825" y="240" text-anchor="middle" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="10" fill="#3fb950">Claude judges</text>
185
+
186
+ <!-- ======= LOOPING: Orange data packets (after typing, 14-19%) ======= -->
187
+ <circle r="3.5" fill="#f78166" opacity="0">
188
+ <animate attributeName="opacity" values="0;0;1;1;0;0" keyTimes="0;0.14;0.145;0.23;0.24;1" dur="14s" repeatCount="indefinite"/>
189
+ <animateMotion dur="14s" repeatCount="indefinite" keyPoints="0;0;1;1" keyTimes="0;0.14;0.24;1" calcMode="linear" path="M 250 258 L 320 258 L 320 165 L 430 165"/>
190
+ </circle>
191
+ <circle r="3.5" fill="#f78166" opacity="0">
192
+ <animate attributeName="opacity" values="0;0;1;1;0;0" keyTimes="0;0.14;0.145;0.23;0.24;1" dur="14s" repeatCount="indefinite"/>
193
+ <animateMotion dur="14s" repeatCount="indefinite" keyPoints="0;0;1;1" keyTimes="0;0.14;0.24;1" calcMode="linear" path="M 250 258 L 320 258 L 320 225 L 430 225"/>
194
+ </circle>
195
+ <circle r="3.5" fill="#f78166" opacity="0">
196
+ <animate attributeName="opacity" values="0;0;1;1;0;0" keyTimes="0;0.14;0.145;0.23;0.24;1" dur="14s" repeatCount="indefinite"/>
197
+ <animateMotion dur="14s" repeatCount="indefinite" keyPoints="0;0;1;1" keyTimes="0;0.14;0.24;1" calcMode="linear" path="M 250 258 L 320 258 L 320 285 L 430 285"/>
198
+ </circle>
199
+ <circle r="3.5" fill="#f78166" opacity="0">
200
+ <animate attributeName="opacity" values="0;0;1;1;0;0" keyTimes="0;0.14;0.145;0.23;0.24;1" dur="14s" repeatCount="indefinite"/>
201
+ <animateMotion dur="14s" repeatCount="indefinite" keyPoints="0;0;1;1" keyTimes="0;0.14;0.24;1" calcMode="linear" path="M 250 258 L 320 258 L 320 345 L 430 345"/>
202
+ </circle>
203
+
204
+ <!-- ======= LOOPING: Model cards ======= -->
205
+
206
+ <!-- Opus -->
207
+ <g class="mA">
208
+ <rect x="430" y="143" width="250" height="44" rx="8" fill="#161b22" stroke="#30363d" stroke-width="1"/>
209
+ <text x="472" y="170" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="13.5" fill="#f0f6fc">Opus 4.6</text>
210
+ <rect x="440" y="178" width="230" height="3" rx="1.5" fill="#21262d"/>
211
+ </g>
212
+ <g class="spA"><circle cx="448" cy="165" r="5" fill="none" stroke="#f78166" stroke-width="1.5" stroke-dasharray="20 12" stroke-linecap="round"/></g>
213
+ <g class="ckA">
214
+ <circle cx="448" cy="165" r="6" fill="#3fb950" fill-opacity="0.15"/>
215
+ <path d="M 443 165 L 446.5 168.5 L 453 162" stroke="#3fb950" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
216
+ </g>
217
+ <text class="dnA" x="670" y="170" text-anchor="end" font-family="'SF Mono', monospace" font-size="10" fill="#3fb950">2.1s</text>
218
+
219
+ <!-- GPT Codex -->
220
+ <g class="mB">
221
+ <rect x="430" y="203" width="250" height="44" rx="8" fill="#161b22" stroke="#30363d" stroke-width="1"/>
222
+ <text x="472" y="230" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="13.5" fill="#f0f6fc">GPT 5.4 Codex</text>
223
+ <rect x="440" y="238" width="230" height="3" rx="1.5" fill="#21262d"/>
224
+ </g>
225
+ <g class="spB"><circle cx="448" cy="225" r="5" fill="none" stroke="#f78166" stroke-width="1.5" stroke-dasharray="20 12" stroke-linecap="round"/></g>
226
+ <g class="ckB">
227
+ <circle cx="448" cy="225" r="6" fill="#3fb950" fill-opacity="0.15"/>
228
+ <path d="M 443 225 L 446.5 228.5 L 453 222" stroke="#3fb950" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
229
+ </g>
230
+ <text class="dnB" x="670" y="230" text-anchor="end" font-family="'SF Mono', monospace" font-size="10" fill="#3fb950">3.8s</text>
231
+
232
+ <!-- Gemini -->
233
+ <g class="mC">
234
+ <rect x="430" y="263" width="250" height="44" rx="8" fill="#161b22" stroke="#30363d" stroke-width="1"/>
235
+ <text x="472" y="290" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="13.5" fill="#f0f6fc">Gemini 3.1 Pro</text>
236
+ <rect x="440" y="298" width="230" height="3" rx="1.5" fill="#21262d"/>
237
+ </g>
238
+ <g class="spC"><circle cx="448" cy="285" r="5" fill="none" stroke="#f78166" stroke-width="1.5" stroke-dasharray="20 12" stroke-linecap="round"/></g>
239
+ <g class="ckC">
240
+ <circle cx="448" cy="285" r="6" fill="#3fb950" fill-opacity="0.15"/>
241
+ <path d="M 443 285 L 446.5 288.5 L 453 282" stroke="#3fb950" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
242
+ </g>
243
+ <text class="dnC" x="670" y="290" text-anchor="end" font-family="'SF Mono', monospace" font-size="10" fill="#3fb950">4.2s</text>
244
+
245
+ <!-- Grok -->
246
+ <g class="mD">
247
+ <rect x="430" y="323" width="250" height="44" rx="8" fill="#161b22" stroke="#30363d" stroke-width="1"/>
248
+ <text x="472" y="350" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="13.5" fill="#f0f6fc">Grok 4.20</text>
249
+ <rect x="440" y="358" width="230" height="3" rx="1.5" fill="#21262d"/>
250
+ </g>
251
+ <g class="spD"><circle cx="448" cy="345" r="5" fill="none" stroke="#f78166" stroke-width="1.5" stroke-dasharray="20 12" stroke-linecap="round"/></g>
252
+ <g class="ckD">
253
+ <circle cx="448" cy="345" r="6" fill="#3fb950" fill-opacity="0.15"/>
254
+ <path d="M 443 345 L 446.5 348.5 L 453 342" stroke="#3fb950" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
255
+ </g>
256
+ <text class="dnD" x="670" y="350" text-anchor="end" font-family="'SF Mono', monospace" font-size="10" fill="#3fb950">5.1s</text>
257
+
258
+ <!-- Per-model progress bars -->
259
+ <rect x="440" y="178" width="0" height="3" rx="1.5" fill="url(#og)">
260
+ <animate attributeName="width" values="0;0;230;230;0;0" keyTimes="0;0.23;0.36;0.37;0.38;1" dur="14s" repeatCount="indefinite"/>
261
+ </rect>
262
+ <rect x="440" y="238" width="0" height="3" rx="1.5" fill="url(#og)">
263
+ <animate attributeName="width" values="0;0;230;230;0;0" keyTimes="0;0.25;0.39;0.40;0.41;1" dur="14s" repeatCount="indefinite"/>
264
+ </rect>
265
+ <rect x="440" y="298" width="0" height="3" rx="1.5" fill="url(#og)">
266
+ <animate attributeName="width" values="0;0;230;230;0;0" keyTimes="0;0.27;0.42;0.43;0.44;1" dur="14s" repeatCount="indefinite"/>
267
+ </rect>
268
+ <rect x="440" y="358" width="0" height="3" rx="1.5" fill="url(#og)">
269
+ <animate attributeName="width" values="0;0;230;230;0;0" keyTimes="0;0.29;0.45;0.46;0.47;1" dur="14s" repeatCount="indefinite"/>
270
+ </rect>
271
+
272
+ <!-- ======= LOOPING: Green dots (staggered return, collect at junction) ======= -->
273
+ <!-- Opus: leaves at 38%, arrives junction at 42%, holds, goes to output at 51% -->
274
+ <circle r="3.5" fill="#3fb950" opacity="0">
275
+ <animate attributeName="opacity" values="0;0;1;1;0;0" keyTimes="0;0.38;0.385;0.52;0.53;1" dur="14s" repeatCount="indefinite"/>
276
+ <animateMotion dur="14s" repeatCount="indefinite" keyPoints="0;0;0.6;0.6;1;1" keyTimes="0;0.38;0.42;0.48;0.53;1" calcMode="linear" path="M 680 165 L 790 165 L 790 258 L 860 258"/>
277
+ </circle>
278
+ <!-- GPT: leaves at 41%, arrives junction at 44%, holds, goes to output at 51% -->
279
+ <circle r="3.5" fill="#3fb950" opacity="0">
280
+ <animate attributeName="opacity" values="0;0;1;1;0;0" keyTimes="0;0.41;0.415;0.52;0.53;1" dur="14s" repeatCount="indefinite"/>
281
+ <animateMotion dur="14s" repeatCount="indefinite" keyPoints="0;0;0.6;0.6;1;1" keyTimes="0;0.41;0.44;0.48;0.53;1" calcMode="linear" path="M 680 225 L 790 225 L 790 258 L 860 258"/>
282
+ </circle>
283
+ <!-- Gemini: leaves at 44%, arrives junction at 47%, holds, goes to output at 51% -->
284
+ <circle r="3.5" fill="#3fb950" opacity="0">
285
+ <animate attributeName="opacity" values="0;0;1;1;0;0" keyTimes="0;0.44;0.445;0.52;0.53;1" dur="14s" repeatCount="indefinite"/>
286
+ <animateMotion dur="14s" repeatCount="indefinite" keyPoints="0;0;0.6;0.6;1;1" keyTimes="0;0.44;0.47;0.49;0.53;1" calcMode="linear" path="M 680 285 L 790 285 L 790 258 L 860 258"/>
287
+ </circle>
288
+ <!-- Grok: leaves at 47%, arrives junction at 49%, holds briefly, goes to output at 51% -->
289
+ <circle r="3.5" fill="#3fb950" opacity="0">
290
+ <animate attributeName="opacity" values="0;0;1;1;0;0" keyTimes="0;0.47;0.475;0.52;0.53;1" dur="14s" repeatCount="indefinite"/>
291
+ <animateMotion dur="14s" repeatCount="indefinite" keyPoints="0;0;0.6;0.6;1;1" keyTimes="0;0.47;0.49;0.50;0.53;1" calcMode="linear" path="M 680 345 L 790 345 L 790 258 L 860 258"/>
292
+ </circle>
293
+
294
+ <!-- ======= LOOPING: Output card ======= -->
295
+ <g class="output-card">
296
+ <g class="output-float">
297
+ <rect x="870" y="210" width="210" height="100" rx="10" fill="#161b22" stroke="#3fb950" stroke-opacity="0.3" stroke-width="1" filter="url(#sh)"/>
298
+ <text x="975" y="236" text-anchor="middle" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="10" font-weight="600" fill="#3fb950" letter-spacing="2.5" opacity="0.8">OUTPUT</text>
299
+ <line x1="893" y1="246" x2="1057" y2="246" stroke="#21262d" stroke-width="1"/>
300
+ </g>
301
+ </g>
302
+ <g class="output-float">
303
+ <g class="olA"><text x="893" y="266" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="12" fill="#8b949e">Judged by Claude</text></g>
304
+ <g class="olB"><text x="893" y="284" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="12" fill="#8b949e">Conflicts resolved</text></g>
305
+ <g class="olC"><text x="893" y="302" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="12.5" fill="#3fb950" font-weight="600">One clean answer</text></g>
306
+ </g>
307
+ </svg>
package/bin/smoothie ADDED
@@ -0,0 +1,67 @@
1
+ #!/bin/bash
2
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
3
+ CONFIG="$SCRIPT_DIR/config.json"
4
+
5
+ case "$1" in
6
+ models)
7
+ shift
8
+ node "$SCRIPT_DIR/dist/select-models.js" "$@"
9
+ ;;
10
+ blend)
11
+ shift
12
+ node "$SCRIPT_DIR/dist/blend-cli.js" "$@"
13
+ ;;
14
+ auto)
15
+ case "$2" in
16
+ on)
17
+ node -e "
18
+ const fs = require('fs');
19
+ const c = JSON.parse(fs.readFileSync('$CONFIG','utf8'));
20
+ c.auto_blend = true;
21
+ fs.writeFileSync('$CONFIG', JSON.stringify(c, null, 2));
22
+ "
23
+ echo " ✓ Auto-blend enabled"
24
+ ;;
25
+ off)
26
+ node -e "
27
+ const fs = require('fs');
28
+ const c = JSON.parse(fs.readFileSync('$CONFIG','utf8'));
29
+ c.auto_blend = false;
30
+ fs.writeFileSync('$CONFIG', JSON.stringify(c, null, 2));
31
+ "
32
+ echo " ✓ Auto-blend disabled"
33
+ ;;
34
+ *)
35
+ ENABLED=$(node -e "
36
+ try {
37
+ const c = JSON.parse(require('fs').readFileSync('$CONFIG','utf8'));
38
+ console.log(c.auto_blend ? 'on' : 'off');
39
+ } catch(e) { console.log('off'); }
40
+ ")
41
+ echo " Auto-blend is $ENABLED"
42
+ echo " Usage: smoothie auto on|off"
43
+ ;;
44
+ esac
45
+ ;;
46
+ help|--help|-h|"")
47
+ echo ""
48
+ echo " 🧃 Smoothie — multi-model review for Claude Code"
49
+ echo ""
50
+ echo " smoothie models Pick models interactively"
51
+ echo " smoothie models list Show current models"
52
+ echo " smoothie models add <id> Add by OpenRouter model ID"
53
+ echo " smoothie models remove <id> Remove a model"
54
+ echo ""
55
+ echo " smoothie auto Show auto-blend status"
56
+ echo " smoothie auto on Enable auto-blend for plans"
57
+ echo " smoothie auto off Disable auto-blend"
58
+ echo ""
59
+ echo " smoothie blend \"<prompt>\" Run a blend from terminal"
60
+ echo " smoothie blend --deep \"<prompt>\" Deep blend with full context"
61
+ echo ""
62
+ ;;
63
+ *)
64
+ echo " Unknown command: $1"
65
+ echo " Run 'smoothie help' for usage"
66
+ ;;
67
+ esac
package/config.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "openrouter_models": [
3
+ {
4
+ "id": "google/gemma-4-26b-a4b-it",
5
+ "label": "Google: Gemma 4 26B A4B "
6
+ },
7
+ {
8
+ "id": "qwen/qwen3.6-plus:free",
9
+ "label": "Qwen: Qwen3.6 Plus (free)"
10
+ },
11
+ {
12
+ "id": "z-ai/glm-5v-turbo",
13
+ "label": "Z.ai: GLM 5V Turbo"
14
+ }
15
+ ],
16
+ "auto_blend": true
17
+ }
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * blend-cli.ts — Standalone blend runner for hooks.
4
+ *
5
+ * Usage:
6
+ * node dist/blend-cli.js "Review this plan: ..."
7
+ * echo "plan text" | node dist/blend-cli.js
8
+ *
9
+ * Queries Codex + OpenRouter models in parallel, prints JSON results to stdout.
10
+ * Progress goes to stderr so it doesn't interfere with hook JSON output.
11
+ */
12
+ export {};