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 +82 -0
- package/auto-blend-hook.sh +94 -0
- package/banner-v2.svg +307 -0
- package/bin/smoothie +67 -0
- package/config.json +17 -0
- package/dist/blend-cli.d.ts +12 -0
- package/dist/blend-cli.js +185 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +286 -0
- package/dist/select-models.d.ts +2 -0
- package/dist/select-models.js +277 -0
- package/docs/banner.svg +307 -0
- package/docs/favicon.svg +17 -0
- package/docs/index.html +306 -0
- package/icon.svg +17 -0
- package/install.sh +377 -0
- package/package.json +20 -0
- package/plan-hook.sh +38 -0
- package/pr-blend-hook.sh +95 -0
- package/src/blend-cli.ts +219 -0
- package/src/index.ts +367 -0
- package/src/select-models.ts +318 -0
- package/tsconfig.json +14 -0
package/docs/index.html
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Smoothie — Multi-model review for Claude Code</title>
|
|
7
|
+
<link rel="icon" type="image/svg+xml" href="favicon.svg">
|
|
8
|
+
<meta name="description" content="Query multiple AI models in parallel, get one blended answer. Plugin for Claude Code, Codex CLI, and Gemini CLI.">
|
|
9
|
+
<meta property="og:title" content="Smoothie">
|
|
10
|
+
<meta property="og:description" content="Multi-model review for Claude Code, Codex CLI, and Gemini CLI">
|
|
11
|
+
<meta property="og:image" content="https://hotairbag.github.io/smoothie/banner.svg">
|
|
12
|
+
<style>
|
|
13
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
14
|
+
|
|
15
|
+
body {
|
|
16
|
+
background: #0d1117;
|
|
17
|
+
color: #f0f6fc;
|
|
18
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
19
|
+
min-height: 100vh;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.page {
|
|
23
|
+
max-width: 880px;
|
|
24
|
+
margin: 0 auto;
|
|
25
|
+
padding: 0 24px;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/* Nav */
|
|
29
|
+
nav {
|
|
30
|
+
display: flex;
|
|
31
|
+
justify-content: flex-end;
|
|
32
|
+
padding: 20px 0;
|
|
33
|
+
gap: 12px;
|
|
34
|
+
}
|
|
35
|
+
.gh-star {
|
|
36
|
+
display: inline-flex;
|
|
37
|
+
align-items: center;
|
|
38
|
+
gap: 6px;
|
|
39
|
+
background: #21262d;
|
|
40
|
+
border: 1px solid #30363d;
|
|
41
|
+
border-radius: 6px;
|
|
42
|
+
padding: 6px 14px;
|
|
43
|
+
color: #f0f6fc;
|
|
44
|
+
font-size: 13px;
|
|
45
|
+
font-weight: 500;
|
|
46
|
+
text-decoration: none;
|
|
47
|
+
transition: border-color 0.2s, background 0.2s;
|
|
48
|
+
}
|
|
49
|
+
.gh-star:hover { background: #30363d; border-color: #484f58; }
|
|
50
|
+
.gh-star svg { width: 16px; height: 16px; fill: #e3b341; }
|
|
51
|
+
|
|
52
|
+
/* Banner */
|
|
53
|
+
.banner {
|
|
54
|
+
margin: 24px 0 0;
|
|
55
|
+
border-radius: 16px;
|
|
56
|
+
border: 1px solid #21262d;
|
|
57
|
+
overflow: hidden;
|
|
58
|
+
background: #0d1117;
|
|
59
|
+
}
|
|
60
|
+
.banner img { width: 100%; display: block; }
|
|
61
|
+
|
|
62
|
+
/* Hero */
|
|
63
|
+
.hero {
|
|
64
|
+
text-align: center;
|
|
65
|
+
padding: 56px 0 40px;
|
|
66
|
+
}
|
|
67
|
+
.hero p {
|
|
68
|
+
font-size: 18px;
|
|
69
|
+
font-weight: 400;
|
|
70
|
+
color: #8b949e;
|
|
71
|
+
line-height: 1.7;
|
|
72
|
+
max-width: 560px;
|
|
73
|
+
margin: 0 auto;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* Install */
|
|
77
|
+
.install {
|
|
78
|
+
text-align: center;
|
|
79
|
+
margin-bottom: 56px;
|
|
80
|
+
}
|
|
81
|
+
.install-box {
|
|
82
|
+
display: inline-flex;
|
|
83
|
+
align-items: center;
|
|
84
|
+
gap: 12px;
|
|
85
|
+
background: #161b22;
|
|
86
|
+
border: 1px solid #30363d;
|
|
87
|
+
border-radius: 12px;
|
|
88
|
+
padding: 18px 28px;
|
|
89
|
+
cursor: pointer;
|
|
90
|
+
transition: border-color 0.2s, box-shadow 0.2s;
|
|
91
|
+
position: relative;
|
|
92
|
+
}
|
|
93
|
+
.install-box:hover {
|
|
94
|
+
border-color: #f78166;
|
|
95
|
+
box-shadow: 0 0 20px rgba(247, 129, 102, 0.08);
|
|
96
|
+
}
|
|
97
|
+
.install-cmd {
|
|
98
|
+
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
|
|
99
|
+
font-size: 16px;
|
|
100
|
+
color: #f0f6fc;
|
|
101
|
+
}
|
|
102
|
+
.install-cmd .kw { color: #f78166; }
|
|
103
|
+
.install-copy {
|
|
104
|
+
background: #21262d;
|
|
105
|
+
border: 1px solid #30363d;
|
|
106
|
+
border-radius: 6px;
|
|
107
|
+
padding: 4px 10px;
|
|
108
|
+
font-size: 11px;
|
|
109
|
+
color: #8b949e;
|
|
110
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
111
|
+
transition: color 0.2s, background 0.2s;
|
|
112
|
+
}
|
|
113
|
+
.install-box:hover .install-copy { color: #f0f6fc; background: #30363d; }
|
|
114
|
+
.install-box.copied .install-copy { color: #3fb950; }
|
|
115
|
+
.install-alt {
|
|
116
|
+
margin-top: 12px;
|
|
117
|
+
font-size: 12px;
|
|
118
|
+
color: #484f58;
|
|
119
|
+
}
|
|
120
|
+
.install-alt a { color: #8b949e; text-decoration: none; }
|
|
121
|
+
.install-alt a:hover { color: #f0f6fc; }
|
|
122
|
+
|
|
123
|
+
/* Divider */
|
|
124
|
+
.divider {
|
|
125
|
+
height: 1px;
|
|
126
|
+
background: #21262d;
|
|
127
|
+
margin: 0 0 56px;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/* Features */
|
|
131
|
+
.features {
|
|
132
|
+
display: grid;
|
|
133
|
+
grid-template-columns: repeat(3, 1fr);
|
|
134
|
+
gap: 16px;
|
|
135
|
+
margin-bottom: 56px;
|
|
136
|
+
}
|
|
137
|
+
@media (max-width: 640px) {
|
|
138
|
+
.features { grid-template-columns: 1fr; }
|
|
139
|
+
}
|
|
140
|
+
.feature {
|
|
141
|
+
background: #161b22;
|
|
142
|
+
border: 1px solid #21262d;
|
|
143
|
+
border-radius: 12px;
|
|
144
|
+
padding: 28px 24px;
|
|
145
|
+
transition: border-color 0.2s;
|
|
146
|
+
}
|
|
147
|
+
.feature:hover { border-color: #30363d; }
|
|
148
|
+
.feature h3 {
|
|
149
|
+
font-size: 15px;
|
|
150
|
+
font-weight: 600;
|
|
151
|
+
margin-bottom: 10px;
|
|
152
|
+
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
153
|
+
}
|
|
154
|
+
.feature p {
|
|
155
|
+
font-size: 13px;
|
|
156
|
+
color: #8b949e;
|
|
157
|
+
line-height: 1.6;
|
|
158
|
+
}
|
|
159
|
+
.accent { color: #f78166; }
|
|
160
|
+
.green { color: #3fb950; }
|
|
161
|
+
|
|
162
|
+
/* Usage */
|
|
163
|
+
.usage {
|
|
164
|
+
margin-bottom: 64px;
|
|
165
|
+
}
|
|
166
|
+
.usage h2 {
|
|
167
|
+
font-size: 13px;
|
|
168
|
+
font-weight: 600;
|
|
169
|
+
color: #8b949e;
|
|
170
|
+
letter-spacing: 1.5px;
|
|
171
|
+
text-transform: uppercase;
|
|
172
|
+
margin-bottom: 20px;
|
|
173
|
+
}
|
|
174
|
+
.usage-grid {
|
|
175
|
+
display: grid;
|
|
176
|
+
grid-template-columns: 1fr 1fr;
|
|
177
|
+
gap: 10px;
|
|
178
|
+
}
|
|
179
|
+
@media (max-width: 640px) {
|
|
180
|
+
.usage-grid { grid-template-columns: 1fr; }
|
|
181
|
+
}
|
|
182
|
+
.usage-item {
|
|
183
|
+
background: #161b22;
|
|
184
|
+
border: 1px solid #21262d;
|
|
185
|
+
border-radius: 8px;
|
|
186
|
+
padding: 16px 20px;
|
|
187
|
+
display: flex;
|
|
188
|
+
justify-content: space-between;
|
|
189
|
+
align-items: center;
|
|
190
|
+
}
|
|
191
|
+
.usage-item code {
|
|
192
|
+
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
193
|
+
font-size: 13px;
|
|
194
|
+
color: #f0f6fc;
|
|
195
|
+
}
|
|
196
|
+
.usage-item .desc {
|
|
197
|
+
font-size: 12px;
|
|
198
|
+
color: #484f58;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/* Footer */
|
|
202
|
+
footer {
|
|
203
|
+
padding: 40px 0;
|
|
204
|
+
text-align: center;
|
|
205
|
+
border-top: 1px solid #21262d;
|
|
206
|
+
}
|
|
207
|
+
footer a {
|
|
208
|
+
display: inline-flex;
|
|
209
|
+
align-items: center;
|
|
210
|
+
gap: 6px;
|
|
211
|
+
color: #484f58;
|
|
212
|
+
text-decoration: none;
|
|
213
|
+
font-size: 13px;
|
|
214
|
+
transition: color 0.2s;
|
|
215
|
+
}
|
|
216
|
+
footer a:hover { color: #8b949e; }
|
|
217
|
+
footer svg { width: 18px; height: 18px; fill: currentColor; }
|
|
218
|
+
</style>
|
|
219
|
+
</head>
|
|
220
|
+
<body>
|
|
221
|
+
<div class="page">
|
|
222
|
+
<nav>
|
|
223
|
+
<a class="gh-star" href="https://github.com/hotairbag/smoothie">
|
|
224
|
+
<svg viewBox="0 0 16 16"><path d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25z"/></svg>
|
|
225
|
+
Star on GitHub
|
|
226
|
+
</a>
|
|
227
|
+
</nav>
|
|
228
|
+
|
|
229
|
+
<div class="banner">
|
|
230
|
+
<img src="banner.svg" alt="Smoothie">
|
|
231
|
+
</div>
|
|
232
|
+
|
|
233
|
+
<div class="hero">
|
|
234
|
+
<p>Query multiple AI models in parallel. Get one blended answer. Works with Claude Code, Codex CLI, and Gemini CLI.</p>
|
|
235
|
+
</div>
|
|
236
|
+
|
|
237
|
+
<div class="install">
|
|
238
|
+
<div class="install-box" onclick="copyInstall(this)">
|
|
239
|
+
<span class="install-cmd"><span class="kw">npx</span> smoothie-code</span>
|
|
240
|
+
<span class="install-copy">copy</span>
|
|
241
|
+
</div>
|
|
242
|
+
<div class="install-alt">
|
|
243
|
+
or <a href="https://github.com/hotairbag/smoothie">clone from GitHub</a>
|
|
244
|
+
</div>
|
|
245
|
+
</div>
|
|
246
|
+
|
|
247
|
+
<div class="divider"></div>
|
|
248
|
+
|
|
249
|
+
<div class="features">
|
|
250
|
+
<div class="feature">
|
|
251
|
+
<h3><span class="accent">/smoothie</span></h3>
|
|
252
|
+
<p>Slash command sends your problem to all models. The host AI judges responses and gives you one clean answer.</p>
|
|
253
|
+
</div>
|
|
254
|
+
<div class="feature">
|
|
255
|
+
<h3><span class="green">auto-blend</span></h3>
|
|
256
|
+
<p>Plans and PRs automatically reviewed by multiple models before you approve. Zero typing required.</p>
|
|
257
|
+
</div>
|
|
258
|
+
<div class="feature">
|
|
259
|
+
<h3>--deep</h3>
|
|
260
|
+
<p>Full context mode. Sends project files, git diff, and docs to all reviewers. Shows cost estimate first.</p>
|
|
261
|
+
</div>
|
|
262
|
+
</div>
|
|
263
|
+
|
|
264
|
+
<div class="usage">
|
|
265
|
+
<h2>Commands</h2>
|
|
266
|
+
<div class="usage-grid">
|
|
267
|
+
<div class="usage-item">
|
|
268
|
+
<code>/smoothie <span class="accent">fix the auth bug</span></code>
|
|
269
|
+
<span class="desc">blend</span>
|
|
270
|
+
</div>
|
|
271
|
+
<div class="usage-item">
|
|
272
|
+
<code>/smoothie-pr</code>
|
|
273
|
+
<span class="desc">review PR</span>
|
|
274
|
+
</div>
|
|
275
|
+
<div class="usage-item">
|
|
276
|
+
<code>smoothie models</code>
|
|
277
|
+
<span class="desc">pick models</span>
|
|
278
|
+
</div>
|
|
279
|
+
<div class="usage-item">
|
|
280
|
+
<code>smoothie auto <span class="green">on</span></code>
|
|
281
|
+
<span class="desc">auto-blend</span>
|
|
282
|
+
</div>
|
|
283
|
+
</div>
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
286
|
+
|
|
287
|
+
<footer>
|
|
288
|
+
<a href="https://github.com/hotairbag/smoothie">
|
|
289
|
+
<svg viewBox="0 0 16 16"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg>
|
|
290
|
+
hotairbag/smoothie
|
|
291
|
+
</a>
|
|
292
|
+
</footer>
|
|
293
|
+
|
|
294
|
+
<script>
|
|
295
|
+
function copyInstall(el) {
|
|
296
|
+
navigator.clipboard.writeText('npx smoothie-code');
|
|
297
|
+
el.classList.add('copied');
|
|
298
|
+
el.querySelector('.install-copy').textContent = 'copied!';
|
|
299
|
+
setTimeout(() => {
|
|
300
|
+
el.classList.remove('copied');
|
|
301
|
+
el.querySelector('.install-copy').textContent = 'copy';
|
|
302
|
+
}, 2000);
|
|
303
|
+
}
|
|
304
|
+
</script>
|
|
305
|
+
</body>
|
|
306
|
+
</html>
|
package/icon.svg
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
|
|
2
|
+
<rect width="64" height="64" rx="14" fill="#0d1117"/>
|
|
3
|
+
<g transform="translate(12, 8)" stroke="#f0f6fc" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" fill="none">
|
|
4
|
+
<!-- Cup body -->
|
|
5
|
+
<path d="M 6 22 L 10 48 L 30 48 L 34 22"/>
|
|
6
|
+
<!-- Lid -->
|
|
7
|
+
<path d="M 2 18 L 38 18 C 38 18 38 22 20 22 C 2 22 2 18 2 18"/>
|
|
8
|
+
<!-- Dome -->
|
|
9
|
+
<path d="M 8 18 C 8 10 14 4 20 4 C 26 4 32 10 32 18"/>
|
|
10
|
+
<!-- Straw -->
|
|
11
|
+
<line x1="24" y1="4" x2="34" y2="-6" stroke-width="2.5"/>
|
|
12
|
+
<line x1="34" y1="-6" x2="38" y2="-6" stroke-width="2.5"/>
|
|
13
|
+
<!-- Drips -->
|
|
14
|
+
<path d="M 13 22 C 13 26 17 26 17 22" opacity="0.5" stroke-width="2"/>
|
|
15
|
+
<path d="M 23 22 C 23 26 27 26 27 22" opacity="0.5" stroke-width="2"/>
|
|
16
|
+
</g>
|
|
17
|
+
</svg>
|
package/install.sh
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -e
|
|
3
|
+
|
|
4
|
+
# Colors
|
|
5
|
+
G='\033[0;32m' # green
|
|
6
|
+
Y='\033[1;33m' # yellow
|
|
7
|
+
R='\033[0;31m' # red
|
|
8
|
+
C='\033[0;36m' # cyan
|
|
9
|
+
D='\033[0;90m' # dim
|
|
10
|
+
B='\033[1m' # bold
|
|
11
|
+
N='\033[0m' # reset
|
|
12
|
+
|
|
13
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
14
|
+
TOTAL_STEPS=7
|
|
15
|
+
STEP=0
|
|
16
|
+
|
|
17
|
+
step() {
|
|
18
|
+
STEP=$((STEP + 1))
|
|
19
|
+
echo ""
|
|
20
|
+
echo -e " ${D}[$STEP/$TOTAL_STEPS]${N} ${B}$1${N}"
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
spin() {
|
|
24
|
+
local pid=$1 msg=$2
|
|
25
|
+
local frames=("⠋" "⠙" "⠹" "⠸" "⠼" "⠴" "⠦" "⠧" "⠇" "⠏")
|
|
26
|
+
local i=0
|
|
27
|
+
while kill -0 "$pid" 2>/dev/null; do
|
|
28
|
+
printf "\r ${D}${frames[$i]} $msg${N} "
|
|
29
|
+
i=$(( (i + 1) % ${#frames[@]} ))
|
|
30
|
+
sleep 0.1
|
|
31
|
+
done
|
|
32
|
+
wait "$pid" 2>/dev/null
|
|
33
|
+
printf "\r ${G}✓${N} $msg \n"
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
clear
|
|
37
|
+
echo ""
|
|
38
|
+
echo -e " ${C}${B}"
|
|
39
|
+
echo ' ____ _ _ _'
|
|
40
|
+
echo ' / ___| _ __ ___ ___ ___ | |_| |__ (_) ___'
|
|
41
|
+
echo ' \___ \| `_ ` _ \ / _ \ / _ \| __| `_ \| |/ _ \'
|
|
42
|
+
echo ' ___) | | | | | | (_) | (_) | |_| | | | | __/'
|
|
43
|
+
echo ' |____/|_| |_| |_|\___/ \___/ \__|_| |_|_|\___|'
|
|
44
|
+
echo -e "${N}"
|
|
45
|
+
echo -e " ${D}multi-model review for Claude Code${N}"
|
|
46
|
+
echo ""
|
|
47
|
+
echo -e " ${D}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${N}"
|
|
48
|
+
|
|
49
|
+
# ─── Step 1: Dependencies ────────────────────────────────────────────
|
|
50
|
+
step "Installing dependencies"
|
|
51
|
+
|
|
52
|
+
if ! command -v node &>/dev/null; then
|
|
53
|
+
echo -e " ${R}✗ Node.js not found. Install from https://nodejs.org${N}"
|
|
54
|
+
exit 1
|
|
55
|
+
fi
|
|
56
|
+
echo -e " ${G}✓${N} Node $(node --version)"
|
|
57
|
+
|
|
58
|
+
cd "$SCRIPT_DIR"
|
|
59
|
+
npm install --silent 2>/dev/null &
|
|
60
|
+
spin $! "npm install"
|
|
61
|
+
|
|
62
|
+
npm run build 2>&1 >/dev/null &
|
|
63
|
+
spin $! "Compiling TypeScript"
|
|
64
|
+
|
|
65
|
+
npm link --silent 2>/dev/null &
|
|
66
|
+
spin $! "Linking smoothie CLI"
|
|
67
|
+
|
|
68
|
+
# ─── Step 2: Detect platform ─────────────────────────────────────────
|
|
69
|
+
step "Detecting platform"
|
|
70
|
+
|
|
71
|
+
HAS_CLAUDE=$(command -v claude &>/dev/null && echo "yes" || echo "no")
|
|
72
|
+
HAS_CODEX=$(command -v codex &>/dev/null && echo "yes" || echo "no")
|
|
73
|
+
HAS_GEMINI=$(command -v gemini &>/dev/null && echo "yes" || echo "no")
|
|
74
|
+
|
|
75
|
+
DETECTED=()
|
|
76
|
+
[ "$HAS_CLAUDE" = "yes" ] && DETECTED+=("claude:Claude Code")
|
|
77
|
+
[ "$HAS_CODEX" = "yes" ] && DETECTED+=("codex:Codex CLI")
|
|
78
|
+
[ "$HAS_GEMINI" = "yes" ] && DETECTED+=("gemini:Gemini CLI")
|
|
79
|
+
|
|
80
|
+
if [ ${#DETECTED[@]} -eq 0 ]; then
|
|
81
|
+
echo -e " ${Y}No AI CLI detected. Defaulting to Claude Code.${N}"
|
|
82
|
+
PLATFORM="claude"
|
|
83
|
+
elif [ ${#DETECTED[@]} -eq 1 ]; then
|
|
84
|
+
PLATFORM="${DETECTED[0]%%:*}"
|
|
85
|
+
echo -e " ${G}✓${N} ${DETECTED[0]##*:}"
|
|
86
|
+
else
|
|
87
|
+
echo ""
|
|
88
|
+
i=1
|
|
89
|
+
for entry in "${DETECTED[@]}"; do
|
|
90
|
+
echo -e " ${B}$i.${N} ${entry##*:}"
|
|
91
|
+
i=$((i+1))
|
|
92
|
+
done
|
|
93
|
+
echo ""
|
|
94
|
+
read -p " Install for which platform? [1]: " CHOICE
|
|
95
|
+
CHOICE=${CHOICE:-1}
|
|
96
|
+
PLATFORM="${DETECTED[$((CHOICE-1))]%%:*}"
|
|
97
|
+
echo -e " ${G}✓${N} ${PLATFORM}"
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
# ─── Step 3: Codex CLI (optional, not shown for codex platform) ──────
|
|
101
|
+
if [ "$PLATFORM" != "codex" ]; then
|
|
102
|
+
step "Setting up Codex ${D}(optional)${N}"
|
|
103
|
+
|
|
104
|
+
echo ""
|
|
105
|
+
echo -e " ${D}Codex adds OpenAI's coding model to the blend.${N}"
|
|
106
|
+
echo -e " ${D}Requires a ChatGPT account. Skip if you only want OpenRouter.${N}"
|
|
107
|
+
echo ""
|
|
108
|
+
read -p " Set up Codex? [Y/n]: " SETUP_CODEX
|
|
109
|
+
|
|
110
|
+
if [[ "$SETUP_CODEX" =~ ^[Nn]$ ]]; then
|
|
111
|
+
echo -e " ${D}Skipped — blend will use OpenRouter models only${N}"
|
|
112
|
+
else
|
|
113
|
+
if ! command -v codex &>/dev/null; then
|
|
114
|
+
npm install -g @openai/codex 2>/dev/null &
|
|
115
|
+
spin $! "Installing Codex CLI"
|
|
116
|
+
else
|
|
117
|
+
echo -e " ${G}✓${N} Codex CLI found"
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
if codex auth status 2>/dev/null | grep -q "Logged in"; then
|
|
121
|
+
echo -e " ${G}✓${N} Already authenticated"
|
|
122
|
+
else
|
|
123
|
+
echo ""
|
|
124
|
+
echo -e " ${D}Opens browser → sign in with ChatGPT account${N}"
|
|
125
|
+
read -p " Press Enter when ready → " _
|
|
126
|
+
codex auth login >/dev/null 2>&1
|
|
127
|
+
echo -e " ${G}✓${N} Codex authenticated"
|
|
128
|
+
fi
|
|
129
|
+
fi
|
|
130
|
+
fi
|
|
131
|
+
|
|
132
|
+
# ─── Step 4: OpenRouter ──────────────────────────────────────────────
|
|
133
|
+
step "Connecting OpenRouter"
|
|
134
|
+
|
|
135
|
+
echo ""
|
|
136
|
+
echo -e " ${D}One API key for Gemini, Grok, DeepSeek + 200 more${N}"
|
|
137
|
+
echo -e " ${D}Get yours free →${N} ${C}https://openrouter.ai/keys${N}"
|
|
138
|
+
echo ""
|
|
139
|
+
read -s -p " API key (hidden): " OPENROUTER_KEY
|
|
140
|
+
echo ""
|
|
141
|
+
|
|
142
|
+
if [ -z "$OPENROUTER_KEY" ]; then
|
|
143
|
+
echo -e " ${Y}Skipped${N} ${D}— add later to .env${N}"
|
|
144
|
+
echo "OPENROUTER_API_KEY=" > "$SCRIPT_DIR/.env"
|
|
145
|
+
else
|
|
146
|
+
echo "OPENROUTER_API_KEY=$OPENROUTER_KEY" > "$SCRIPT_DIR/.env"
|
|
147
|
+
echo -e " ${G}✓${N} Key saved"
|
|
148
|
+
fi
|
|
149
|
+
echo "SMOOTHIE_PLATFORM=$PLATFORM" >> "$SCRIPT_DIR/.env"
|
|
150
|
+
|
|
151
|
+
# ─── Step 5: Pick models ─────────────────────────────────────────────
|
|
152
|
+
step "Choosing models"
|
|
153
|
+
|
|
154
|
+
echo ""
|
|
155
|
+
node "$SCRIPT_DIR/dist/select-models.js" "$OPENROUTER_KEY" "$SCRIPT_DIR/config.json"
|
|
156
|
+
|
|
157
|
+
# ─── Step 6: Auto-blend ──────────────────────────────────────────────
|
|
158
|
+
step "Configuring hooks"
|
|
159
|
+
|
|
160
|
+
chmod +x "$SCRIPT_DIR/plan-hook.sh" "$SCRIPT_DIR/auto-blend-hook.sh" "$SCRIPT_DIR/pr-blend-hook.sh"
|
|
161
|
+
|
|
162
|
+
echo ""
|
|
163
|
+
echo -e " ${B}Auto-blend${N} reviews every plan with all models before"
|
|
164
|
+
echo -e " you approve. Adds 30-90s per plan."
|
|
165
|
+
echo ""
|
|
166
|
+
read -p " Enable auto-blend? [y/N]: " AUTO_BLEND
|
|
167
|
+
if [[ "$AUTO_BLEND" =~ ^[Yy]$ ]]; then
|
|
168
|
+
node -e "
|
|
169
|
+
const fs = require('fs');
|
|
170
|
+
const c = JSON.parse(fs.readFileSync('$SCRIPT_DIR/config.json','utf8'));
|
|
171
|
+
c.auto_blend = true;
|
|
172
|
+
fs.writeFileSync('$SCRIPT_DIR/config.json', JSON.stringify(c, null, 2));
|
|
173
|
+
"
|
|
174
|
+
echo -e " ${G}✓${N} Auto-blend on"
|
|
175
|
+
else
|
|
176
|
+
echo -e " ${D}Skipped — toggle in config.json anytime${N}"
|
|
177
|
+
fi
|
|
178
|
+
|
|
179
|
+
# ─── Step 7: Wire up ─────────────────────────────────────────────────
|
|
180
|
+
step "Wiring up"
|
|
181
|
+
|
|
182
|
+
if [ "$PLATFORM" = "claude" ]; then
|
|
183
|
+
# Find .claude dir
|
|
184
|
+
CLAUDE_DIR=""
|
|
185
|
+
SEARCH_DIR="$PWD"
|
|
186
|
+
while [ "$SEARCH_DIR" != "/" ]; do
|
|
187
|
+
if [ -d "$SEARCH_DIR/.claude" ]; then
|
|
188
|
+
CLAUDE_DIR="$SEARCH_DIR/.claude"
|
|
189
|
+
break
|
|
190
|
+
fi
|
|
191
|
+
SEARCH_DIR="$(dirname "$SEARCH_DIR")"
|
|
192
|
+
done
|
|
193
|
+
if [ -z "$CLAUDE_DIR" ]; then
|
|
194
|
+
CLAUDE_DIR="$HOME/.claude"
|
|
195
|
+
mkdir -p "$CLAUDE_DIR"
|
|
196
|
+
fi
|
|
197
|
+
|
|
198
|
+
# Slash command
|
|
199
|
+
mkdir -p "$CLAUDE_DIR/commands"
|
|
200
|
+
cat > "$CLAUDE_DIR/commands/smoothie.md" << 'EOF'
|
|
201
|
+
You are running Smoothie — a multi-model review session.
|
|
202
|
+
|
|
203
|
+
The user has provided this context/problem:
|
|
204
|
+
$ARGUMENTS
|
|
205
|
+
|
|
206
|
+
**Step 1 — Blend**
|
|
207
|
+
Call `smoothie_blend` with the user's prompt. The MCP server queries all
|
|
208
|
+
models in parallel and shows live progress in the terminal. Wait for it to return.
|
|
209
|
+
|
|
210
|
+
**Step 2 — Judge and respond**
|
|
211
|
+
You now have responses from all models. You are the final decision-maker.
|
|
212
|
+
Do NOT show the user raw model outputs.
|
|
213
|
+
|
|
214
|
+
- If reviewing a **problem**: respond with the answer or fix. Mention in one
|
|
215
|
+
sentence if there was a meaningful conflict worth flagging.
|
|
216
|
+
- If reviewing a **plan**: respond with a revised plan with improvements already
|
|
217
|
+
incorporated. End with a "What changed" section (2–3 bullets).
|
|
218
|
+
|
|
219
|
+
Use your full codebase context to filter out irrelevant suggestions. Be decisive.
|
|
220
|
+
EOF
|
|
221
|
+
echo -e " ${G}✓${N} Slash command /smoothie"
|
|
222
|
+
|
|
223
|
+
cat > "$CLAUDE_DIR/commands/smoothie-pr.md" << 'EOF'
|
|
224
|
+
You are running Smoothie PR Review — a multi-model code review.
|
|
225
|
+
|
|
226
|
+
$ARGUMENTS
|
|
227
|
+
|
|
228
|
+
**Step 1 — Get the diff**
|
|
229
|
+
Run `git diff main...HEAD` to get the full branch diff.
|
|
230
|
+
|
|
231
|
+
**Step 2 — Blend**
|
|
232
|
+
Call `smoothie_blend` with a prompt asking models to review the diff for:
|
|
233
|
+
- Bugs, logic errors, edge cases
|
|
234
|
+
- Security vulnerabilities
|
|
235
|
+
- Performance issues
|
|
236
|
+
- Code style / best practices
|
|
237
|
+
|
|
238
|
+
**Step 3 — Respond**
|
|
239
|
+
Summarize the review. List concrete issues found (if any) with file:line references.
|
|
240
|
+
If everything looks good, say so briefly. Be direct.
|
|
241
|
+
EOF
|
|
242
|
+
echo -e " ${G}✓${N} Slash command /smoothie-pr"
|
|
243
|
+
|
|
244
|
+
# Merge settings.json
|
|
245
|
+
SETTINGS_FILE="$CLAUDE_DIR/settings.json"
|
|
246
|
+
EXISTING="{}"
|
|
247
|
+
[ -f "$SETTINGS_FILE" ] && EXISTING=$(cat "$SETTINGS_FILE")
|
|
248
|
+
|
|
249
|
+
node - << NODEJS
|
|
250
|
+
const fs = require('fs');
|
|
251
|
+
let s;
|
|
252
|
+
try { s = JSON.parse(\`$EXISTING\`); } catch(e) { s = {}; }
|
|
253
|
+
|
|
254
|
+
s.mcpServers = s.mcpServers || {};
|
|
255
|
+
s.mcpServers.smoothie = {
|
|
256
|
+
command: "node",
|
|
257
|
+
args: ["$SCRIPT_DIR/dist/index.js"],
|
|
258
|
+
env: { OPENROUTER_API_KEY: "$OPENROUTER_KEY", SMOOTHIE_PLATFORM: "$PLATFORM" }
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
s.hooks = s.hooks || {};
|
|
262
|
+
|
|
263
|
+
s.hooks.PreToolUse = s.hooks.PreToolUse || [];
|
|
264
|
+
const preExists = s.hooks.PreToolUse.some(h => h.matcher === 'ExitPlanMode');
|
|
265
|
+
if (!preExists) {
|
|
266
|
+
s.hooks.PreToolUse.push({
|
|
267
|
+
matcher: "ExitPlanMode",
|
|
268
|
+
hooks: [{
|
|
269
|
+
type: "command",
|
|
270
|
+
command: "bash $SCRIPT_DIR/auto-blend-hook.sh",
|
|
271
|
+
timeout: 120
|
|
272
|
+
}]
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Add PR review hook for Bash commands
|
|
277
|
+
const bashHookExists = s.hooks.PreToolUse.some(h => h.matcher === 'Bash' && h.hooks?.[0]?.command?.includes('pr-blend-hook'));
|
|
278
|
+
if (!bashHookExists) {
|
|
279
|
+
s.hooks.PreToolUse.push({
|
|
280
|
+
matcher: "Bash",
|
|
281
|
+
hooks: [{
|
|
282
|
+
type: "command",
|
|
283
|
+
command: "bash $SCRIPT_DIR/pr-blend-hook.sh",
|
|
284
|
+
timeout: 120
|
|
285
|
+
}]
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
s.hooks.Stop = s.hooks.Stop || [];
|
|
290
|
+
const stopExists = s.hooks.Stop.some(h => h.hooks?.[0]?.command?.includes('plan-hook.sh'));
|
|
291
|
+
if (!stopExists) {
|
|
292
|
+
s.hooks.Stop.push({ hooks: [{ type: "command", command: "bash $SCRIPT_DIR/plan-hook.sh" }] });
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
fs.writeFileSync('$SETTINGS_FILE', JSON.stringify(s, null, 2));
|
|
296
|
+
NODEJS
|
|
297
|
+
echo -e " ${G}✓${N} MCP server registered"
|
|
298
|
+
echo -e " ${G}✓${N} Hooks configured"
|
|
299
|
+
fi
|
|
300
|
+
|
|
301
|
+
if [ "$PLATFORM" = "gemini" ]; then
|
|
302
|
+
mkdir -p "$HOME/.gemini/commands"
|
|
303
|
+
|
|
304
|
+
# Gemini MCP server
|
|
305
|
+
GEMINI_SETTINGS="$HOME/.gemini/settings.json"
|
|
306
|
+
GEMINI_EXISTING="{}"
|
|
307
|
+
[ -f "$GEMINI_SETTINGS" ] && GEMINI_EXISTING=$(cat "$GEMINI_SETTINGS")
|
|
308
|
+
|
|
309
|
+
node -e "
|
|
310
|
+
const fs = require('fs');
|
|
311
|
+
let s;
|
|
312
|
+
try { s = JSON.parse(\`$GEMINI_EXISTING\`); } catch(e) { s = {}; }
|
|
313
|
+
s.mcpServers = s.mcpServers || {};
|
|
314
|
+
s.mcpServers.smoothie = {
|
|
315
|
+
command: 'node',
|
|
316
|
+
args: ['$SCRIPT_DIR/dist/index.js'],
|
|
317
|
+
env: { OPENROUTER_API_KEY: '$OPENROUTER_KEY', SMOOTHIE_PLATFORM: 'gemini' }
|
|
318
|
+
};
|
|
319
|
+
fs.writeFileSync('$GEMINI_SETTINGS', JSON.stringify(s, null, 2));
|
|
320
|
+
"
|
|
321
|
+
echo -e " ${G}✓${N} Gemini MCP server registered"
|
|
322
|
+
|
|
323
|
+
# Gemini slash commands (.toml)
|
|
324
|
+
cat > "$HOME/.gemini/commands/smoothie.toml" << 'TOML'
|
|
325
|
+
description = "Blend this problem across multiple AI models. Gemini judges."
|
|
326
|
+
|
|
327
|
+
prompt = """
|
|
328
|
+
You are running Smoothie — a multi-model review session.
|
|
329
|
+
|
|
330
|
+
{{args}}
|
|
331
|
+
|
|
332
|
+
Step 1 — Call smoothie_blend with the problem text. Wait for results.
|
|
333
|
+
|
|
334
|
+
Step 2 — You have responses from all models. Do NOT show raw outputs.
|
|
335
|
+
- If reviewing a problem: give the answer. Mention conflicts in one sentence.
|
|
336
|
+
- If reviewing a plan: return a revised plan. End with "What changed" bullets.
|
|
337
|
+
|
|
338
|
+
Be decisive. Use your full codebase context.
|
|
339
|
+
"""
|
|
340
|
+
TOML
|
|
341
|
+
echo -e " ${G}✓${N} Slash command /smoothie (Gemini)"
|
|
342
|
+
|
|
343
|
+
cat > "$HOME/.gemini/commands/smoothie-pr.toml" << 'TOML'
|
|
344
|
+
description = "Multi-model PR review before creating a pull request."
|
|
345
|
+
|
|
346
|
+
prompt = """
|
|
347
|
+
You are running Smoothie PR Review.
|
|
348
|
+
|
|
349
|
+
{{args}}
|
|
350
|
+
|
|
351
|
+
Step 1 — Run git diff main...HEAD to get the branch diff.
|
|
352
|
+
Step 2 — Call smoothie_blend asking models to review the diff for bugs, security, performance.
|
|
353
|
+
Step 3 — Summarize findings with file:line references. Be direct.
|
|
354
|
+
"""
|
|
355
|
+
TOML
|
|
356
|
+
echo -e " ${G}✓${N} Slash command /smoothie-pr (Gemini)"
|
|
357
|
+
fi
|
|
358
|
+
|
|
359
|
+
# ─── Done ─────────────────────────────────────────────────────────────
|
|
360
|
+
MODELS=$(node -e "
|
|
361
|
+
const d = JSON.parse(require('fs').readFileSync('$SCRIPT_DIR/config.json','utf8'));
|
|
362
|
+
console.log(['Codex', ...d.openrouter_models.map(m=>m.label)].join(' · '));
|
|
363
|
+
")
|
|
364
|
+
|
|
365
|
+
echo ""
|
|
366
|
+
echo -e " ${D}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${N}"
|
|
367
|
+
echo ""
|
|
368
|
+
echo -e " ${G}${B}Done!${N} Restart Claude Code, then:"
|
|
369
|
+
echo ""
|
|
370
|
+
echo -e " ${C}/smoothie${N} ${D}<your problem>${N} blend in Claude Code"
|
|
371
|
+
if [[ "$AUTO_BLEND" =~ ^[Yy]$ ]]; then
|
|
372
|
+
echo -e " ${C}auto-blend${N} ${G}on${N} for all plans"
|
|
373
|
+
fi
|
|
374
|
+
echo -e " ${C}smoothie models${N} manage models"
|
|
375
|
+
echo ""
|
|
376
|
+
echo -e " ${D}$MODELS${N}"
|
|
377
|
+
echo ""
|