smoothie-code 1.1.0 → 2.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 +39 -57
- package/auto-review-hook.sh +138 -0
- package/bin/smoothie +221 -16
- package/config.json +3 -1
- package/dist/blend-cli.js +69 -10
- package/dist/index.js +98 -14
- package/dist/review-cli.d.ts +12 -0
- package/dist/review-cli.js +244 -0
- package/dist/select-models.js +3 -1
- package/gemini-review-hook.sh +149 -0
- package/install.sh +142 -23
- package/package.json +12 -1
- package/plan-hook.sh +1 -1
- package/{pr-blend-hook.sh → pr-review-hook.sh} +43 -9
- package/auto-blend-hook.sh +0 -103
- package/banner-v2.svg +0 -307
- package/docs/banner.svg +0 -307
- package/docs/favicon.png +0 -0
- package/docs/favicon.svg +0 -17
- package/docs/index.html +0 -319
- package/icon.png +0 -0
- package/icon.svg +0 -17
- package/src/blend-cli.ts +0 -219
- package/src/index.ts +0 -367
- package/src/select-models.ts +0 -318
- package/tsconfig.json +0 -14
package/docs/favicon.svg
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
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, 10)" stroke="#f0f6fc" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" fill="none">
|
|
4
|
-
<!-- Straw (inside viewBox now) -->
|
|
5
|
-
<line x1="24" y1="8" x2="32" y2="2"/>
|
|
6
|
-
<line x1="32" y1="2" x2="36" y2="2"/>
|
|
7
|
-
<!-- Dome -->
|
|
8
|
-
<path d="M 8 20 C 8 12 14 6 20 6 C 26 6 32 12 32 20"/>
|
|
9
|
-
<!-- Lid -->
|
|
10
|
-
<path d="M 2 20 L 38 20 C 38 20 38 24 20 24 C 2 24 2 20 2 20"/>
|
|
11
|
-
<!-- Cup body -->
|
|
12
|
-
<path d="M 6 24 L 10 46 L 30 46 L 34 24"/>
|
|
13
|
-
<!-- Drips -->
|
|
14
|
-
<path d="M 13 24 C 13 28 17 28 17 24" opacity="0.4" stroke-width="1.8"/>
|
|
15
|
-
<path d="M 23 24 C 23 28 27 28 27 24" opacity="0.4" stroke-width="1.8"/>
|
|
16
|
-
</g>
|
|
17
|
-
</svg>
|
package/docs/index.html
DELETED
|
@@ -1,319 +0,0 @@
|
|
|
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/png" href="favicon.png">
|
|
8
|
-
<link rel="icon" type="image/svg+xml" href="favicon.svg">
|
|
9
|
-
<meta name="description" content="Query multiple AI models in parallel, get one blended answer. Plugin for Claude Code, Codex CLI, and Gemini CLI.">
|
|
10
|
-
<meta property="og:title" content="Smoothie">
|
|
11
|
-
<meta property="og:description" content="Multi-model review for Claude Code, Codex CLI, and Gemini CLI">
|
|
12
|
-
<meta property="og:image" content="https://hotairbag.github.io/smoothie/banner.svg">
|
|
13
|
-
<style>
|
|
14
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
15
|
-
|
|
16
|
-
body {
|
|
17
|
-
background: #0d1117;
|
|
18
|
-
color: #f0f6fc;
|
|
19
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
20
|
-
min-height: 100vh;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
.page {
|
|
24
|
-
max-width: 880px;
|
|
25
|
-
margin: 0 auto;
|
|
26
|
-
padding: 0 24px;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/* Nav */
|
|
30
|
-
nav {
|
|
31
|
-
display: flex;
|
|
32
|
-
justify-content: flex-end;
|
|
33
|
-
padding: 20px 0;
|
|
34
|
-
gap: 12px;
|
|
35
|
-
}
|
|
36
|
-
.gh-star {
|
|
37
|
-
display: inline-flex;
|
|
38
|
-
align-items: center;
|
|
39
|
-
gap: 6px;
|
|
40
|
-
background: #21262d;
|
|
41
|
-
border: 1px solid #30363d;
|
|
42
|
-
border-radius: 6px;
|
|
43
|
-
padding: 6px 14px;
|
|
44
|
-
color: #f0f6fc;
|
|
45
|
-
font-size: 13px;
|
|
46
|
-
font-weight: 500;
|
|
47
|
-
text-decoration: none;
|
|
48
|
-
transition: border-color 0.2s, background 0.2s;
|
|
49
|
-
}
|
|
50
|
-
.gh-star:hover { background: #30363d; border-color: #484f58; }
|
|
51
|
-
.gh-star svg { width: 16px; height: 16px; fill: #e3b341; }
|
|
52
|
-
|
|
53
|
-
/* Banner */
|
|
54
|
-
.banner {
|
|
55
|
-
margin: 24px 0 0;
|
|
56
|
-
border-radius: 16px;
|
|
57
|
-
border: 1px solid #21262d;
|
|
58
|
-
overflow: hidden;
|
|
59
|
-
background: #0d1117;
|
|
60
|
-
}
|
|
61
|
-
.banner img { width: 100%; display: block; }
|
|
62
|
-
|
|
63
|
-
/* Hero */
|
|
64
|
-
.hero {
|
|
65
|
-
text-align: center;
|
|
66
|
-
padding: 56px 0 40px;
|
|
67
|
-
}
|
|
68
|
-
.hero p {
|
|
69
|
-
font-size: 18px;
|
|
70
|
-
font-weight: 400;
|
|
71
|
-
color: #8b949e;
|
|
72
|
-
line-height: 1.7;
|
|
73
|
-
max-width: 560px;
|
|
74
|
-
margin: 0 auto;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/* Install */
|
|
78
|
-
.install {
|
|
79
|
-
text-align: center;
|
|
80
|
-
margin-bottom: 56px;
|
|
81
|
-
}
|
|
82
|
-
.install-box {
|
|
83
|
-
display: inline-flex;
|
|
84
|
-
align-items: center;
|
|
85
|
-
gap: 12px;
|
|
86
|
-
background: #161b22;
|
|
87
|
-
border: 1px solid #30363d;
|
|
88
|
-
border-radius: 12px;
|
|
89
|
-
padding: 18px 28px;
|
|
90
|
-
cursor: pointer;
|
|
91
|
-
transition: border-color 0.2s, box-shadow 0.2s;
|
|
92
|
-
position: relative;
|
|
93
|
-
}
|
|
94
|
-
.install-box:hover {
|
|
95
|
-
border-color: #f78166;
|
|
96
|
-
box-shadow: 0 0 20px rgba(247, 129, 102, 0.08);
|
|
97
|
-
}
|
|
98
|
-
.install-cmd {
|
|
99
|
-
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
|
|
100
|
-
font-size: 16px;
|
|
101
|
-
color: #f0f6fc;
|
|
102
|
-
}
|
|
103
|
-
.install-cmd .kw { color: #f78166; }
|
|
104
|
-
.install-copy {
|
|
105
|
-
background: #21262d;
|
|
106
|
-
border: 1px solid #30363d;
|
|
107
|
-
border-radius: 6px;
|
|
108
|
-
padding: 4px 10px;
|
|
109
|
-
font-size: 11px;
|
|
110
|
-
color: #8b949e;
|
|
111
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
112
|
-
transition: color 0.2s, background 0.2s;
|
|
113
|
-
}
|
|
114
|
-
.install-box:hover .install-copy { color: #f0f6fc; background: #30363d; }
|
|
115
|
-
.install-box.copied .install-copy { color: #3fb950; }
|
|
116
|
-
.install-alt {
|
|
117
|
-
margin-top: 12px;
|
|
118
|
-
font-size: 12px;
|
|
119
|
-
color: #484f58;
|
|
120
|
-
}
|
|
121
|
-
.install-alt a { color: #8b949e; text-decoration: none; }
|
|
122
|
-
.install-alt a:hover { color: #f0f6fc; }
|
|
123
|
-
|
|
124
|
-
/* Divider */
|
|
125
|
-
.divider {
|
|
126
|
-
height: 1px;
|
|
127
|
-
background: #21262d;
|
|
128
|
-
margin: 0 0 56px;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/* Features */
|
|
132
|
-
.features {
|
|
133
|
-
display: grid;
|
|
134
|
-
grid-template-columns: repeat(3, 1fr);
|
|
135
|
-
gap: 16px;
|
|
136
|
-
margin-bottom: 56px;
|
|
137
|
-
}
|
|
138
|
-
@media (max-width: 640px) {
|
|
139
|
-
.features { grid-template-columns: 1fr; }
|
|
140
|
-
}
|
|
141
|
-
.feature {
|
|
142
|
-
background: #161b22;
|
|
143
|
-
border: 1px solid #21262d;
|
|
144
|
-
border-radius: 12px;
|
|
145
|
-
padding: 28px 24px;
|
|
146
|
-
transition: border-color 0.2s;
|
|
147
|
-
}
|
|
148
|
-
.feature:hover { border-color: #30363d; }
|
|
149
|
-
.feature h3 {
|
|
150
|
-
font-size: 15px;
|
|
151
|
-
font-weight: 600;
|
|
152
|
-
margin-bottom: 10px;
|
|
153
|
-
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
154
|
-
}
|
|
155
|
-
.feature p {
|
|
156
|
-
font-size: 13px;
|
|
157
|
-
color: #8b949e;
|
|
158
|
-
line-height: 1.6;
|
|
159
|
-
}
|
|
160
|
-
.accent { color: #f78166; }
|
|
161
|
-
.green { color: #3fb950; }
|
|
162
|
-
|
|
163
|
-
/* Usage */
|
|
164
|
-
.usage {
|
|
165
|
-
margin-bottom: 64px;
|
|
166
|
-
}
|
|
167
|
-
.usage h2 {
|
|
168
|
-
font-size: 13px;
|
|
169
|
-
font-weight: 600;
|
|
170
|
-
color: #8b949e;
|
|
171
|
-
letter-spacing: 1.5px;
|
|
172
|
-
text-transform: uppercase;
|
|
173
|
-
margin-bottom: 20px;
|
|
174
|
-
}
|
|
175
|
-
.usage-grid {
|
|
176
|
-
display: grid;
|
|
177
|
-
grid-template-columns: 1fr 1fr;
|
|
178
|
-
gap: 10px;
|
|
179
|
-
}
|
|
180
|
-
@media (max-width: 640px) {
|
|
181
|
-
.usage-grid { grid-template-columns: 1fr; }
|
|
182
|
-
}
|
|
183
|
-
.usage-item {
|
|
184
|
-
background: #161b22;
|
|
185
|
-
border: 1px solid #21262d;
|
|
186
|
-
border-radius: 8px;
|
|
187
|
-
padding: 16px 20px;
|
|
188
|
-
display: flex;
|
|
189
|
-
justify-content: space-between;
|
|
190
|
-
align-items: center;
|
|
191
|
-
}
|
|
192
|
-
.usage-item code {
|
|
193
|
-
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
194
|
-
font-size: 13px;
|
|
195
|
-
color: #f0f6fc;
|
|
196
|
-
}
|
|
197
|
-
.usage-item .desc {
|
|
198
|
-
font-size: 12px;
|
|
199
|
-
color: #484f58;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/* Footer */
|
|
203
|
-
footer {
|
|
204
|
-
padding: 40px 0;
|
|
205
|
-
text-align: center;
|
|
206
|
-
border-top: 1px solid #21262d;
|
|
207
|
-
}
|
|
208
|
-
footer {
|
|
209
|
-
display: flex;
|
|
210
|
-
justify-content: center;
|
|
211
|
-
gap: 24px;
|
|
212
|
-
}
|
|
213
|
-
footer a {
|
|
214
|
-
display: inline-flex;
|
|
215
|
-
align-items: center;
|
|
216
|
-
gap: 6px;
|
|
217
|
-
color: #484f58;
|
|
218
|
-
text-decoration: none;
|
|
219
|
-
font-size: 13px;
|
|
220
|
-
transition: color 0.2s;
|
|
221
|
-
}
|
|
222
|
-
footer a:hover { color: #8b949e; }
|
|
223
|
-
footer svg { width: 18px; height: 18px; fill: currentColor; }
|
|
224
|
-
</style>
|
|
225
|
-
</head>
|
|
226
|
-
<body>
|
|
227
|
-
<div class="page">
|
|
228
|
-
<nav>
|
|
229
|
-
<a class="gh-star" href="https://github.com/hotairbag/smoothie">
|
|
230
|
-
<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>
|
|
231
|
-
Star on GitHub
|
|
232
|
-
</a>
|
|
233
|
-
</nav>
|
|
234
|
-
|
|
235
|
-
<div class="banner">
|
|
236
|
-
<img src="banner.svg" alt="Smoothie">
|
|
237
|
-
</div>
|
|
238
|
-
|
|
239
|
-
<div class="hero">
|
|
240
|
-
<p>Query multiple AI models in parallel. Get one blended answer. Works with Claude Code, Codex CLI, and Gemini CLI.</p>
|
|
241
|
-
</div>
|
|
242
|
-
|
|
243
|
-
<div class="install">
|
|
244
|
-
<div class="install-box" onclick="copyInstall(this)">
|
|
245
|
-
<span class="install-cmd"><span class="kw">npx</span> smoothie-code</span>
|
|
246
|
-
<span class="install-copy">copy</span>
|
|
247
|
-
</div>
|
|
248
|
-
<div class="install-alt">
|
|
249
|
-
or <a href="https://github.com/hotairbag/smoothie">clone from GitHub</a>
|
|
250
|
-
</div>
|
|
251
|
-
</div>
|
|
252
|
-
|
|
253
|
-
<div class="divider"></div>
|
|
254
|
-
|
|
255
|
-
<div class="features">
|
|
256
|
-
<div class="feature">
|
|
257
|
-
<h3><span class="accent">/smoothie</span></h3>
|
|
258
|
-
<p>Slash command sends your problem to all models. The host AI judges responses and gives you one clean answer.</p>
|
|
259
|
-
</div>
|
|
260
|
-
<div class="feature">
|
|
261
|
-
<h3><span class="green">auto-blend</span></h3>
|
|
262
|
-
<p>Plans and PRs automatically reviewed by multiple models before you approve. Zero typing required.</p>
|
|
263
|
-
</div>
|
|
264
|
-
<div class="feature">
|
|
265
|
-
<h3>--deep</h3>
|
|
266
|
-
<p>Full context mode. Sends project files, git diff, and docs to all reviewers. Shows cost estimate first.</p>
|
|
267
|
-
</div>
|
|
268
|
-
</div>
|
|
269
|
-
|
|
270
|
-
<div class="usage">
|
|
271
|
-
<h2>Commands</h2>
|
|
272
|
-
<div class="usage-grid">
|
|
273
|
-
<div class="usage-item">
|
|
274
|
-
<code>/smoothie <span class="accent">fix the auth bug</span></code>
|
|
275
|
-
<span class="desc">blend</span>
|
|
276
|
-
</div>
|
|
277
|
-
<div class="usage-item">
|
|
278
|
-
<code>/smoothie-pr</code>
|
|
279
|
-
<span class="desc">review PR</span>
|
|
280
|
-
</div>
|
|
281
|
-
<div class="usage-item">
|
|
282
|
-
<code>smoothie models</code>
|
|
283
|
-
<span class="desc">pick models</span>
|
|
284
|
-
</div>
|
|
285
|
-
<div class="usage-item">
|
|
286
|
-
<code>smoothie auto <span class="green">on</span></code>
|
|
287
|
-
<span class="desc">auto-blend</span>
|
|
288
|
-
</div>
|
|
289
|
-
</div>
|
|
290
|
-
</div>
|
|
291
|
-
</div>
|
|
292
|
-
|
|
293
|
-
<footer>
|
|
294
|
-
<a href="https://github.com/hotairbag/smoothie">
|
|
295
|
-
<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>
|
|
296
|
-
GitHub
|
|
297
|
-
</a>
|
|
298
|
-
<a href="https://openrouter.ai/apps?url=https%3A%2F%2Fhotairbag.github.io%2Fsmoothie">
|
|
299
|
-
<svg viewBox="0 0 16 16"><path d="M8 1a7 7 0 100 14A7 7 0 008 1zm0 1.5a5.5 5.5 0 11-.001 11.001A5.5 5.5 0 018 2.5zm0 2a.75.75 0 00-.75.75v3.5a.75.75 0 001.5 0v-3.5A.75.75 0 008 4.5zm0 6a.75.75 0 100 1.5.75.75 0 000-1.5z"/></svg>
|
|
300
|
-
OpenRouter Usage
|
|
301
|
-
</a>
|
|
302
|
-
<a href="https://www.npmjs.com/package/smoothie-code">
|
|
303
|
-
npm
|
|
304
|
-
</a>
|
|
305
|
-
</footer>
|
|
306
|
-
|
|
307
|
-
<script>
|
|
308
|
-
function copyInstall(el) {
|
|
309
|
-
navigator.clipboard.writeText('npx smoothie-code');
|
|
310
|
-
el.classList.add('copied');
|
|
311
|
-
el.querySelector('.install-copy').textContent = 'copied!';
|
|
312
|
-
setTimeout(() => {
|
|
313
|
-
el.classList.remove('copied');
|
|
314
|
-
el.querySelector('.install-copy').textContent = 'copy';
|
|
315
|
-
}, 2000);
|
|
316
|
-
}
|
|
317
|
-
</script>
|
|
318
|
-
</body>
|
|
319
|
-
</html>
|
package/icon.png
DELETED
|
Binary file
|
package/icon.svg
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
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, 10)" stroke="#f0f6fc" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" fill="none">
|
|
4
|
-
<!-- Straw (inside viewBox now) -->
|
|
5
|
-
<line x1="24" y1="8" x2="32" y2="2"/>
|
|
6
|
-
<line x1="32" y1="2" x2="36" y2="2"/>
|
|
7
|
-
<!-- Dome -->
|
|
8
|
-
<path d="M 8 20 C 8 12 14 6 20 6 C 26 6 32 12 32 20"/>
|
|
9
|
-
<!-- Lid -->
|
|
10
|
-
<path d="M 2 20 L 38 20 C 38 20 38 24 20 24 C 2 24 2 20 2 20"/>
|
|
11
|
-
<!-- Cup body -->
|
|
12
|
-
<path d="M 6 24 L 10 46 L 30 46 L 34 24"/>
|
|
13
|
-
<!-- Drips -->
|
|
14
|
-
<path d="M 13 24 C 13 28 17 28 17 24" opacity="0.4" stroke-width="1.8"/>
|
|
15
|
-
<path d="M 23 24 C 23 28 27 28 27 24" opacity="0.4" stroke-width="1.8"/>
|
|
16
|
-
</g>
|
|
17
|
-
</svg>
|
package/src/blend-cli.ts
DELETED
|
@@ -1,219 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* blend-cli.ts — Standalone blend runner for hooks.
|
|
5
|
-
*
|
|
6
|
-
* Usage:
|
|
7
|
-
* node dist/blend-cli.js "Review this plan: ..."
|
|
8
|
-
* echo "plan text" | node dist/blend-cli.js
|
|
9
|
-
*
|
|
10
|
-
* Queries Codex + OpenRouter models in parallel, prints JSON results to stdout.
|
|
11
|
-
* Progress goes to stderr so it doesn't interfere with hook JSON output.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { readFileSync } from 'fs';
|
|
15
|
-
import { fileURLToPath } from 'url';
|
|
16
|
-
import { dirname, join } from 'path';
|
|
17
|
-
import { execFile as execFileCb } from 'child_process';
|
|
18
|
-
import { promisify } from 'util';
|
|
19
|
-
import { createInterface } from 'readline';
|
|
20
|
-
|
|
21
|
-
const execFile = promisify(execFileCb);
|
|
22
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
23
|
-
const PROJECT_ROOT = join(__dirname, '..');
|
|
24
|
-
|
|
25
|
-
// ---------------------------------------------------------------------------
|
|
26
|
-
// Types
|
|
27
|
-
// ---------------------------------------------------------------------------
|
|
28
|
-
|
|
29
|
-
interface Config {
|
|
30
|
-
openrouter_models: Array<{ id: string; label: string }>;
|
|
31
|
-
auto_blend?: boolean;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
interface ModelResult {
|
|
35
|
-
model: string;
|
|
36
|
-
response: string;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
interface OpenRouterResponse {
|
|
40
|
-
choices?: Array<{ message: { content: string } }>;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// ---------------------------------------------------------------------------
|
|
44
|
-
// .env loader
|
|
45
|
-
// ---------------------------------------------------------------------------
|
|
46
|
-
function loadEnv(): void {
|
|
47
|
-
try {
|
|
48
|
-
const env = readFileSync(join(PROJECT_ROOT, '.env'), 'utf8');
|
|
49
|
-
for (const line of env.split('\n')) {
|
|
50
|
-
const [key, ...val] = line.split('=');
|
|
51
|
-
if (key && val.length) process.env[key.trim()] = val.join('=').trim();
|
|
52
|
-
}
|
|
53
|
-
} catch {
|
|
54
|
-
// no .env
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
loadEnv();
|
|
58
|
-
|
|
59
|
-
// ---------------------------------------------------------------------------
|
|
60
|
-
// Model queries (same as index.ts)
|
|
61
|
-
// ---------------------------------------------------------------------------
|
|
62
|
-
|
|
63
|
-
async function queryCodex(prompt: string): Promise<ModelResult> {
|
|
64
|
-
try {
|
|
65
|
-
const tmpFile = join(PROJECT_ROOT, `.codex-out-${Date.now()}.txt`);
|
|
66
|
-
await execFile('codex', ['exec', '--full-auto', '-o', tmpFile, prompt], {
|
|
67
|
-
timeout: 0,
|
|
68
|
-
});
|
|
69
|
-
let response: string;
|
|
70
|
-
try {
|
|
71
|
-
response = readFileSync(tmpFile, 'utf8').trim();
|
|
72
|
-
const { unlinkSync } = await import('fs');
|
|
73
|
-
unlinkSync(tmpFile);
|
|
74
|
-
} catch {
|
|
75
|
-
response = '';
|
|
76
|
-
}
|
|
77
|
-
return { model: 'Codex', response: response || '(empty response)' };
|
|
78
|
-
} catch (err: unknown) {
|
|
79
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
80
|
-
return { model: 'Codex', response: `Error: ${message}` };
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
async function queryOpenRouter(
|
|
85
|
-
prompt: string,
|
|
86
|
-
modelId: string,
|
|
87
|
-
modelLabel: string,
|
|
88
|
-
): Promise<ModelResult> {
|
|
89
|
-
try {
|
|
90
|
-
const controller = new AbortController();
|
|
91
|
-
const timer = setTimeout(() => controller.abort(), 60_000);
|
|
92
|
-
const res = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
|
93
|
-
method: 'POST',
|
|
94
|
-
headers: {
|
|
95
|
-
'Authorization': `Bearer ${process.env.OPENROUTER_API_KEY}`,
|
|
96
|
-
'HTTP-Referer': 'https://hotairbag.github.io/smoothie',
|
|
97
|
-
'X-Title': 'Smoothie',
|
|
98
|
-
'Content-Type': 'application/json',
|
|
99
|
-
},
|
|
100
|
-
body: JSON.stringify({
|
|
101
|
-
model: modelId,
|
|
102
|
-
messages: [{ role: 'user', content: prompt }],
|
|
103
|
-
}),
|
|
104
|
-
signal: controller.signal,
|
|
105
|
-
});
|
|
106
|
-
clearTimeout(timer);
|
|
107
|
-
const data = (await res.json()) as OpenRouterResponse;
|
|
108
|
-
const text = data.choices?.[0]?.message?.content ?? 'No response content';
|
|
109
|
-
return { model: modelLabel, response: text };
|
|
110
|
-
} catch (err: unknown) {
|
|
111
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
112
|
-
return { model: modelLabel, response: `Error: ${message}` };
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// ---------------------------------------------------------------------------
|
|
117
|
-
// Read prompt from arg or stdin
|
|
118
|
-
// ---------------------------------------------------------------------------
|
|
119
|
-
|
|
120
|
-
async function getPrompt(): Promise<string> {
|
|
121
|
-
if (process.argv[2]) return process.argv[2];
|
|
122
|
-
|
|
123
|
-
// Read from stdin
|
|
124
|
-
const rl = createInterface({ input: process.stdin });
|
|
125
|
-
const lines: string[] = [];
|
|
126
|
-
for await (const line of rl) {
|
|
127
|
-
lines.push(line);
|
|
128
|
-
}
|
|
129
|
-
return lines.join('\n');
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// ---------------------------------------------------------------------------
|
|
133
|
-
// Main
|
|
134
|
-
// ---------------------------------------------------------------------------
|
|
135
|
-
|
|
136
|
-
async function main(): Promise<void> {
|
|
137
|
-
const args = process.argv.slice(2);
|
|
138
|
-
const deep = args.includes('--deep');
|
|
139
|
-
const filteredArgs = args.filter(a => a !== '--deep');
|
|
140
|
-
// Temporarily override argv for getPrompt
|
|
141
|
-
process.argv = [process.argv[0], process.argv[1], ...filteredArgs];
|
|
142
|
-
const prompt = await getPrompt();
|
|
143
|
-
if (!prompt.trim()) {
|
|
144
|
-
process.stderr.write('blend-cli: no prompt provided\n');
|
|
145
|
-
process.exit(1);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
let finalPrompt = prompt;
|
|
149
|
-
if (deep) {
|
|
150
|
-
// Read context file
|
|
151
|
-
for (const name of ['GEMINI.md', 'CLAUDE.md', 'AGENTS.md']) {
|
|
152
|
-
try {
|
|
153
|
-
const content = readFileSync(join(process.cwd(), name), 'utf8');
|
|
154
|
-
if (content.trim()) {
|
|
155
|
-
finalPrompt = `## Context File\n${content}\n\n## Prompt\n${prompt}`;
|
|
156
|
-
break;
|
|
157
|
-
}
|
|
158
|
-
} catch {
|
|
159
|
-
// file not found, try next
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
// Add git diff
|
|
163
|
-
try {
|
|
164
|
-
const { execFileSync } = await import('child_process');
|
|
165
|
-
const diff = execFileSync('git', ['diff', 'HEAD~3'], { encoding: 'utf8', maxBuffer: 100 * 1024, timeout: 10000 });
|
|
166
|
-
if (diff) finalPrompt += `\n\n## Recent Git Diff\n${diff.slice(0, 40000)}`;
|
|
167
|
-
} catch {
|
|
168
|
-
// no git diff available
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
let config: Config;
|
|
173
|
-
try {
|
|
174
|
-
config = JSON.parse(
|
|
175
|
-
readFileSync(join(PROJECT_ROOT, 'config.json'), 'utf8'),
|
|
176
|
-
) as Config;
|
|
177
|
-
} catch {
|
|
178
|
-
config = { openrouter_models: [] };
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
const models: Array<{ fn: () => Promise<ModelResult>; label: string }> = [
|
|
182
|
-
{ fn: () => queryCodex(finalPrompt), label: 'Codex' },
|
|
183
|
-
...config.openrouter_models.map((m) => ({
|
|
184
|
-
fn: () => queryOpenRouter(finalPrompt, m.id, m.label),
|
|
185
|
-
label: m.label,
|
|
186
|
-
})),
|
|
187
|
-
];
|
|
188
|
-
|
|
189
|
-
process.stderr.write('\n🧃 Smoothie blending...\n\n');
|
|
190
|
-
for (const { label } of models) {
|
|
191
|
-
process.stderr.write(` ⏳ ${label.padEnd(26)} waiting...\n`);
|
|
192
|
-
}
|
|
193
|
-
process.stderr.write('\n');
|
|
194
|
-
|
|
195
|
-
const startTimes: Record<string, number> = {};
|
|
196
|
-
const promises = models.map(({ fn, label }) => {
|
|
197
|
-
startTimes[label] = Date.now();
|
|
198
|
-
return fn()
|
|
199
|
-
.then((result) => {
|
|
200
|
-
const elapsed = ((Date.now() - startTimes[label]) / 1000).toFixed(1);
|
|
201
|
-
process.stderr.write(` ✓ ${label.padEnd(26)} done (${elapsed}s)\n`);
|
|
202
|
-
return result;
|
|
203
|
-
})
|
|
204
|
-
.catch((err: unknown) => {
|
|
205
|
-
const elapsed = ((Date.now() - startTimes[label]) / 1000).toFixed(1);
|
|
206
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
207
|
-
process.stderr.write(` ✗ ${label.padEnd(26)} failed (${elapsed}s)\n`);
|
|
208
|
-
return { model: label, response: `Error: ${message}` };
|
|
209
|
-
});
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
const results = await Promise.all(promises);
|
|
213
|
-
process.stderr.write('\n ◆ All done.\n\n');
|
|
214
|
-
|
|
215
|
-
// Output JSON to stdout (for hook consumption)
|
|
216
|
-
process.stdout.write(JSON.stringify({ results }, null, 2));
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
main();
|