smoothie-code 1.2.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -11
- package/{auto-blend-hook.sh → auto-review-hook.sh} +6 -6
- package/bin/smoothie +99 -88
- package/dist/blend-cli.js +9 -3
- package/dist/index.js +44 -9
- package/dist/review-cli.d.ts +12 -0
- package/dist/review-cli.js +244 -0
- package/dist/select-models.js +1 -1
- package/gemini-review-hook.sh +149 -0
- package/install.sh +36 -28
- package/package.json +4 -3
- package/plan-hook.sh +1 -1
- package/{pr-blend-hook.sh → pr-review-hook.sh} +4 -4
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Smoothie
|
|
2
2
|
|
|
3
|
-
Multi-model code review for AI coding agents. Query Codex, Gemini, Grok, DeepSeek and more in parallel — get one
|
|
3
|
+
Multi-model code review for AI coding agents. Query Codex, Gemini, Grok, DeepSeek and more in parallel — get one reviewed answer.
|
|
4
4
|
|
|
5
5
|
**[Website](https://smoothiecode.com)** · **[Docs](https://smoothiecode.com/docs)** · **[Leaderboard](https://smoothiecode.com/leaderboard)** · **[@smoothie_code](https://x.com/smoothie_code)**
|
|
6
6
|
|
|
@@ -14,10 +14,10 @@ Works with **Claude Code**, **Gemini CLI**, **Codex CLI**, and **Cursor**.
|
|
|
14
14
|
|
|
15
15
|
## Features
|
|
16
16
|
|
|
17
|
-
- `/smoothie <problem>` —
|
|
17
|
+
- `/smoothie <problem>` — review across all models, get one answer
|
|
18
18
|
- `/smoothie-pr` — multi-model PR review
|
|
19
19
|
- `/smoothie --deep` — full context mode with cost estimate
|
|
20
|
-
- **Auto-
|
|
20
|
+
- **Auto-review** — plans and PRs reviewed automatically before you approve
|
|
21
21
|
- **Leaderboard** — weekly token rankings at [smoothiecode.com/leaderboard](https://smoothiecode.com/leaderboard)
|
|
22
22
|
- **Stats & sharing** — `smoothie stats`, `smoothie share`
|
|
23
23
|
|
|
@@ -27,17 +27,17 @@ Works with **Claude Code**, **Gemini CLI**, **Codex CLI**, and **Cursor**.
|
|
|
27
27
|
|---------|------------|-----------|-----------|--------|
|
|
28
28
|
| MCP server | ✓ | ✓ | ✓ (STDIO) | ✓ |
|
|
29
29
|
| Slash commands | ✓ | ✓ | — | — |
|
|
30
|
-
| Auto-
|
|
30
|
+
| Auto-review hooks | ✓ | ✓ | ⚠ experimental | Rule-based |
|
|
31
31
|
| `/smoothie` | ✓ | ✓ | — | — |
|
|
32
|
-
| `smoothie
|
|
32
|
+
| `smoothie review` CLI | ✓ | ✓ | ✓ | ✓ |
|
|
33
33
|
|
|
34
34
|
## CLI
|
|
35
35
|
|
|
36
36
|
```bash
|
|
37
37
|
smoothie models # pick models
|
|
38
|
-
smoothie auto on|off # toggle auto-
|
|
39
|
-
smoothie
|
|
40
|
-
smoothie
|
|
38
|
+
smoothie auto on|off # toggle auto-review
|
|
39
|
+
smoothie review "<prompt>" # run a review
|
|
40
|
+
smoothie review --deep "..." # deep review with full context
|
|
41
41
|
smoothie stats # usage stats
|
|
42
42
|
smoothie share # share last report
|
|
43
43
|
smoothie leaderboard # view rankings
|
|
@@ -49,11 +49,11 @@ smoothie help # all commands
|
|
|
49
49
|
```
|
|
50
50
|
Your IDE (Claude/Gemini/Cursor)
|
|
51
51
|
|
|
|
52
|
-
|-- /smoothie or auto-
|
|
53
|
-
\-- MCP Server →
|
|
52
|
+
|-- /smoothie or auto-review hook
|
|
53
|
+
\-- MCP Server → smoothie_review(prompt)
|
|
54
54
|
|-- Queries all models in parallel
|
|
55
55
|
|-- Returns responses to the judge AI
|
|
56
|
-
\-- Judge gives you one
|
|
56
|
+
\-- Judge gives you one reviewed answer
|
|
57
57
|
```
|
|
58
58
|
|
|
59
59
|
## Links
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
#
|
|
3
|
-
# auto-
|
|
3
|
+
# auto-review-hook.sh — PreToolUse hook for ExitPlanMode
|
|
4
4
|
#
|
|
5
5
|
# Intercepts plan approval, runs Smoothie blend on the plan,
|
|
6
6
|
# and injects results back so Claude revises before you see it.
|
|
@@ -11,12 +11,12 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
11
11
|
# Read hook input from stdin
|
|
12
12
|
INPUT=$(cat)
|
|
13
13
|
|
|
14
|
-
# Check if auto-
|
|
14
|
+
# Check if auto-review is enabled in config
|
|
15
15
|
if [ -f "$SCRIPT_DIR/config.json" ]; then
|
|
16
16
|
AUTO_ENABLED=$(node -e "
|
|
17
17
|
try {
|
|
18
18
|
const c = JSON.parse(require('fs').readFileSync('$SCRIPT_DIR/config.json','utf8'));
|
|
19
|
-
console.log(c.
|
|
19
|
+
console.log(c.auto_review === true ? 'true' : 'false');
|
|
20
20
|
} catch(e) { console.log('false'); }
|
|
21
21
|
" 2>/dev/null)
|
|
22
22
|
|
|
@@ -64,7 +64,7 @@ $PLAN_CONTEXT
|
|
|
64
64
|
Provide concise, actionable feedback. Focus only on things that should change."
|
|
65
65
|
|
|
66
66
|
# Run the blend (progress shows on stderr, results on stdout)
|
|
67
|
-
BLEND_RESULTS=$(echo "$REVIEW_PROMPT" | node "$SCRIPT_DIR/dist/
|
|
67
|
+
BLEND_RESULTS=$(echo "$REVIEW_PROMPT" | node "$SCRIPT_DIR/dist/review-cli.js" 2>/dev/stderr)
|
|
68
68
|
|
|
69
69
|
# Generate share link (metadata only, no raw content)
|
|
70
70
|
SHARE_URL=""
|
|
@@ -102,7 +102,7 @@ if [ -z "$BLEND_RESULTS" ]; then
|
|
|
102
102
|
fi
|
|
103
103
|
|
|
104
104
|
# Build the additionalContext string
|
|
105
|
-
CONTEXT="🧃 Smoothie auto-
|
|
105
|
+
CONTEXT="🧃 Smoothie auto-review results — multiple models reviewed this plan:
|
|
106
106
|
|
|
107
107
|
$BLEND_RESULTS
|
|
108
108
|
|
|
@@ -129,7 +129,7 @@ node -e "
|
|
|
129
129
|
hookSpecificOutput: {
|
|
130
130
|
hookEventName: 'PreToolUse',
|
|
131
131
|
permissionDecision: 'allow',
|
|
132
|
-
permissionDecisionReason: 'Smoothie auto-
|
|
132
|
+
permissionDecisionReason: 'Smoothie auto-review completed',
|
|
133
133
|
additionalContext: ctx
|
|
134
134
|
}
|
|
135
135
|
}));
|
package/bin/smoothie
CHANGED
|
@@ -1,15 +1,56 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
|
|
3
|
+
# ~/.smoothie/ is the single source of truth for all config + data
|
|
4
|
+
SMOOTHIE_HOME="$HOME/.smoothie"
|
|
5
|
+
CONFIG="$SMOOTHIE_HOME/config.json"
|
|
6
|
+
|
|
7
|
+
# Install dir (where dist/, hooks live) — stored during install
|
|
8
|
+
INSTALL_DIR="$(cat "$SMOOTHIE_HOME/.install-path" 2>/dev/null)"
|
|
9
|
+
if [ -z "$INSTALL_DIR" ] || [ ! -d "$INSTALL_DIR" ]; then
|
|
10
|
+
# Fallback: resolve from script location (works for git clone installs)
|
|
11
|
+
INSTALL_DIR="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}" 2>/dev/null || echo "${BASH_SOURCE[0]}")")/.." && pwd)"
|
|
12
|
+
fi
|
|
13
|
+
|
|
14
|
+
# Ensure ~/.smoothie exists
|
|
15
|
+
mkdir -p "$SMOOTHIE_HOME"
|
|
16
|
+
|
|
17
|
+
show_help() {
|
|
18
|
+
echo ""
|
|
19
|
+
echo " 🧃 Smoothie — multi-model code review"
|
|
20
|
+
echo ""
|
|
21
|
+
echo " smoothie install Run the installer"
|
|
22
|
+
echo ""
|
|
23
|
+
echo " smoothie models Pick models interactively"
|
|
24
|
+
echo " smoothie models list Show current models"
|
|
25
|
+
echo " smoothie models add <id> Add by OpenRouter model ID"
|
|
26
|
+
echo " smoothie models remove <id> Remove a model"
|
|
27
|
+
echo ""
|
|
28
|
+
echo " smoothie auto Show auto-review status"
|
|
29
|
+
echo " smoothie auto on Enable auto-review for plans"
|
|
30
|
+
echo " smoothie auto off Disable auto-review"
|
|
31
|
+
echo ""
|
|
32
|
+
echo " smoothie review \"<prompt>\" Run a review from terminal"
|
|
33
|
+
echo " smoothie review --deep \"<prompt>\" Deep review with full context"
|
|
34
|
+
echo ""
|
|
35
|
+
echo " smoothie stats Usage stats and history"
|
|
36
|
+
echo " smoothie stats --share Shareable stats page"
|
|
37
|
+
echo " smoothie share Share last review report"
|
|
38
|
+
echo ""
|
|
39
|
+
echo " smoothie leaderboard View weekly rankings"
|
|
40
|
+
echo " smoothie leaderboard join Join the leaderboard"
|
|
41
|
+
echo " smoothie leaderboard leave Leave and delete data"
|
|
42
|
+
echo " smoothie leaderboard profile Set Twitter handle"
|
|
43
|
+
echo ""
|
|
44
|
+
}
|
|
4
45
|
|
|
5
46
|
case "$1" in
|
|
6
47
|
models)
|
|
7
48
|
shift
|
|
8
|
-
node "$
|
|
49
|
+
node "$INSTALL_DIR/dist/select-models.js" "$@"
|
|
9
50
|
;;
|
|
10
|
-
|
|
51
|
+
review)
|
|
11
52
|
shift
|
|
12
|
-
node "$
|
|
53
|
+
node "$INSTALL_DIR/dist/review-cli.js" "$@"
|
|
13
54
|
;;
|
|
14
55
|
auto)
|
|
15
56
|
case "$2" in
|
|
@@ -17,36 +58,36 @@ case "$1" in
|
|
|
17
58
|
node -e "
|
|
18
59
|
const fs = require('fs');
|
|
19
60
|
const c = JSON.parse(fs.readFileSync('$CONFIG','utf8'));
|
|
20
|
-
c.
|
|
61
|
+
c.auto_review = true;
|
|
21
62
|
fs.writeFileSync('$CONFIG', JSON.stringify(c, null, 2));
|
|
22
63
|
"
|
|
23
|
-
echo " ✓ Auto-
|
|
64
|
+
echo " ✓ Auto-review enabled"
|
|
24
65
|
;;
|
|
25
66
|
off)
|
|
26
67
|
node -e "
|
|
27
68
|
const fs = require('fs');
|
|
28
69
|
const c = JSON.parse(fs.readFileSync('$CONFIG','utf8'));
|
|
29
|
-
c.
|
|
70
|
+
c.auto_review = false;
|
|
30
71
|
fs.writeFileSync('$CONFIG', JSON.stringify(c, null, 2));
|
|
31
72
|
"
|
|
32
|
-
echo " ✓ Auto-
|
|
73
|
+
echo " ✓ Auto-review disabled"
|
|
33
74
|
;;
|
|
34
75
|
*)
|
|
35
76
|
ENABLED=$(node -e "
|
|
36
77
|
try {
|
|
37
78
|
const c = JSON.parse(require('fs').readFileSync('$CONFIG','utf8'));
|
|
38
|
-
console.log(c.
|
|
79
|
+
console.log(c.auto_review ? 'on' : 'off');
|
|
39
80
|
} catch(e) { console.log('off'); }
|
|
40
81
|
")
|
|
41
|
-
echo " Auto-
|
|
82
|
+
echo " Auto-review is $ENABLED"
|
|
42
83
|
echo " Usage: smoothie auto on|off"
|
|
43
84
|
;;
|
|
44
85
|
esac
|
|
45
86
|
;;
|
|
46
87
|
stats)
|
|
47
|
-
HISTORY="$
|
|
88
|
+
HISTORY="$SMOOTHIE_HOME/.smoothie-history.jsonl"
|
|
48
89
|
if [ ! -f "$HISTORY" ]; then
|
|
49
|
-
echo " No history yet. Run a
|
|
90
|
+
echo " No history yet. Run a review first."
|
|
50
91
|
exit 0
|
|
51
92
|
fi
|
|
52
93
|
node -e "
|
|
@@ -59,7 +100,6 @@ case "$1" in
|
|
|
59
100
|
const errors = lines.reduce((s, e) => s + e.models.filter(m => m.error).length, 0);
|
|
60
101
|
const successes = lines.reduce((s, e) => s + e.models.filter(m => !m.error).length, 0);
|
|
61
102
|
|
|
62
|
-
// Model usage counts
|
|
63
103
|
const modelStats = {};
|
|
64
104
|
for (const entry of lines) {
|
|
65
105
|
for (const m of entry.models) {
|
|
@@ -71,66 +111,51 @@ case "$1" in
|
|
|
71
111
|
}
|
|
72
112
|
}
|
|
73
113
|
|
|
74
|
-
// Sort by calls
|
|
75
114
|
const sorted = Object.entries(modelStats).sort((a, b) => b[1].calls - a[1].calls);
|
|
76
115
|
|
|
77
|
-
console.log('');
|
|
78
|
-
console.log(' 🧃 Smoothie Stats');
|
|
79
|
-
console.log(' ─────────────────────────────────────────');
|
|
80
|
-
console.log(' Blends: ' + totalBlends + ' Tokens: ' + totalTokens.toLocaleString() + ' Time: ' + totalTime.toFixed(0) + 's');
|
|
81
|
-
console.log(' Success rate: ' + successes + '/' + (successes + errors) + ' (' + (successes / (successes + errors) * 100).toFixed(0) + '%)');
|
|
82
|
-
console.log('');
|
|
83
|
-
console.log(' Model Calls Tokens Avg time Errors');
|
|
84
|
-
console.log(' ─────────────────────────────────────────────────────────────────');
|
|
85
|
-
for (const [name, s] of sorted) {
|
|
86
|
-
const avg = (s.totalTime / s.calls).toFixed(1);
|
|
87
|
-
console.log(' ' + name.padEnd(30) + String(s.calls).padStart(5) + String(s.tokens.toLocaleString()).padStart(8) + (avg + 's').padStart(10) + String(s.errors).padStart(8));
|
|
88
|
-
}
|
|
89
|
-
console.log('');
|
|
90
|
-
|
|
91
116
|
if ('$2' === '--share') {
|
|
92
117
|
const rate = (successes / (successes + errors) * 100).toFixed(0);
|
|
93
118
|
const names = sorted.map(s => s[0]).join(',');
|
|
94
119
|
const mt = sorted.map(s => s[1].tokens).join(',');
|
|
95
120
|
const mc = sorted.map(s => s[1].calls).join(',');
|
|
96
121
|
const ma = sorted.map(s => (s[1].totalTime / s[1].calls).toFixed(1)).join(',');
|
|
97
|
-
const params = 'blends=' + totalBlends + '&tokens=' + totalTokens + '&time=' + totalTime.toFixed(0) + '&rate=' + rate + '&names=' + encodeURIComponent(names) + '&mt=' + encodeURIComponent(mt) + '&mc=' + encodeURIComponent(mc) + '&ma=' + encodeURIComponent(ma);
|
|
98
122
|
|
|
99
|
-
// Try to create short URL via API
|
|
100
123
|
const https = require('https');
|
|
101
|
-
const postData = JSON.stringify({ type: 'stats', data:
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
timeout: 5000,
|
|
108
|
-
}, (res) => {
|
|
109
|
-
let body = '';
|
|
110
|
-
res.on('data', c => body += c);
|
|
124
|
+
const postData = JSON.stringify({ type: 'stats', data: 'blends=' + totalBlends + '&tokens=' + totalTokens + '&time=' + totalTime.toFixed(0) + '&rate=' + rate + '&names=' + encodeURIComponent(names) + '&mt=' + encodeURIComponent(mt) + '&mc=' + encodeURIComponent(mc) + '&ma=' + encodeURIComponent(ma) });
|
|
125
|
+
const longUrl = 'https://smoothiecode.com/stats?blends=' + totalBlends + '&tokens=' + totalTokens + '&time=' + totalTime.toFixed(0) + '&rate=' + rate + '&names=' + encodeURIComponent(names) + '&mt=' + encodeURIComponent(mt) + '&mc=' + encodeURIComponent(mc) + '&ma=' + encodeURIComponent(ma);
|
|
126
|
+
|
|
127
|
+
const req = https.request('https://api.smoothiecode.com/api/share', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(postData) }, timeout: 5000 }, res => {
|
|
128
|
+
let d = '';
|
|
129
|
+
res.on('data', c => d += c);
|
|
111
130
|
res.on('end', () => {
|
|
112
|
-
try {
|
|
113
|
-
|
|
114
|
-
if (shortUrl) { console.log(' ' + shortUrl); }
|
|
115
|
-
else { console.log(' https://smoothiecode.com/stats?' + params); }
|
|
116
|
-
} catch { console.log(' https://smoothiecode.com/stats?' + params); }
|
|
131
|
+
try { console.log(' ' + JSON.parse(d).url); }
|
|
132
|
+
catch { console.log(' ' + longUrl); }
|
|
117
133
|
console.log('');
|
|
118
134
|
console.log(' Open in browser to save as image or copy tweet');
|
|
119
135
|
console.log('');
|
|
120
136
|
});
|
|
121
137
|
});
|
|
122
|
-
req.on('error', () => {
|
|
123
|
-
|
|
124
|
-
console.log('');
|
|
125
|
-
console.log(' Open in browser to save as image or copy tweet');
|
|
126
|
-
console.log('');
|
|
127
|
-
});
|
|
128
|
-
req.on('timeout', () => { req.destroy(); });
|
|
138
|
+
req.on('error', () => { console.log(' ' + longUrl); console.log(''); });
|
|
139
|
+
req.on('timeout', () => { req.destroy(); console.log(' ' + longUrl); console.log(''); });
|
|
129
140
|
req.write(postData);
|
|
130
141
|
req.end();
|
|
131
142
|
} else {
|
|
143
|
+
console.log('');
|
|
144
|
+
console.log(' 🧃 Smoothie Stats');
|
|
145
|
+
console.log(' ─────────────────────────────────────────');
|
|
146
|
+
console.log(' Reviews: ' + totalBlends + ' Tokens: ' + totalTokens.toLocaleString() + ' Time: ' + totalTime.toFixed(0) + 's');
|
|
147
|
+
console.log(' Success rate: ' + successes + '/' + (successes + errors) + ' (' + (successes / (successes + errors) * 100).toFixed(0) + '%)');
|
|
148
|
+
console.log('');
|
|
149
|
+
console.log(' Model Calls Tokens Avg time Errors');
|
|
150
|
+
console.log(' ─────────────────────────────────────────────────────────────────');
|
|
151
|
+
for (const [name, s] of sorted) {
|
|
152
|
+
const avg = (s.totalTime / s.calls).toFixed(1);
|
|
153
|
+
console.log(' ' + name.padEnd(30) + String(s.calls).padStart(5) + String(s.tokens.toLocaleString()).padStart(8) + (avg + 's').padStart(10) + String(s.errors).padStart(8));
|
|
154
|
+
}
|
|
155
|
+
console.log('');
|
|
156
|
+
|
|
132
157
|
const recent = lines.slice(-5).reverse();
|
|
133
|
-
console.log(' Recent
|
|
158
|
+
console.log(' Recent reviews:');
|
|
134
159
|
for (const e of recent) {
|
|
135
160
|
const models = e.models.filter(m => !m.error).map(m => m.model).join(', ');
|
|
136
161
|
const time = Math.max(...e.models.map(m => m.elapsed_s || 0)).toFixed(1);
|
|
@@ -143,16 +168,15 @@ case "$1" in
|
|
|
143
168
|
;;
|
|
144
169
|
share)
|
|
145
170
|
shift
|
|
146
|
-
if [ -f "$
|
|
171
|
+
if [ -f "$SMOOTHIE_HOME/.last-review.json" ]; then
|
|
147
172
|
PARAMS=$(node -e "
|
|
148
|
-
const r = JSON.parse(require('fs').readFileSync('$
|
|
173
|
+
const r = JSON.parse(require('fs').readFileSync('$SMOOTHIE_HOME/.last-review.json','utf8'));
|
|
149
174
|
const models = r.results.map(m => m.model).join(',');
|
|
150
175
|
const times = r.results.map(m => m.elapsed_s || 0).join(',');
|
|
151
176
|
const tokens = r.results.map(m => m.tokens?.total || 0).join(',');
|
|
152
177
|
console.log('models=' + encodeURIComponent(models) + '×=' + encodeURIComponent(times) + '&tokens=' + encodeURIComponent(tokens) + '&type=plan&suggestions=0');
|
|
153
178
|
")
|
|
154
|
-
|
|
155
|
-
SHORT=$(curl -s -X POST "https://api.smoothiecode.com/api/share" \
|
|
179
|
+
SHORT=$(curl -s --max-time 5 --connect-timeout 3 -X POST "https://api.smoothiecode.com/api/share" \
|
|
156
180
|
-H "Content-Type: application/json" \
|
|
157
181
|
-d "{\"type\":\"report\",\"data\":\"$PARAMS\"}" 2>/dev/null | node -e "
|
|
158
182
|
let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{
|
|
@@ -167,7 +191,7 @@ case "$1" in
|
|
|
167
191
|
echo ""
|
|
168
192
|
echo " Open in browser or copy to share"
|
|
169
193
|
else
|
|
170
|
-
echo " No recent
|
|
194
|
+
echo " No recent review found. Run a review first."
|
|
171
195
|
fi
|
|
172
196
|
;;
|
|
173
197
|
leaderboard)
|
|
@@ -189,7 +213,7 @@ case "$1" in
|
|
|
189
213
|
fs.writeFileSync('$CONFIG', JSON.stringify(c, null, 2));
|
|
190
214
|
"
|
|
191
215
|
echo " ✓ Joined as $GITHUB"
|
|
192
|
-
echo " Your stats will appear on the leaderboard after your next
|
|
216
|
+
echo " Your stats will appear on the leaderboard after your next review"
|
|
193
217
|
;;
|
|
194
218
|
leave)
|
|
195
219
|
node -e "
|
|
@@ -215,7 +239,6 @@ case "$1" in
|
|
|
215
239
|
fi
|
|
216
240
|
;;
|
|
217
241
|
*)
|
|
218
|
-
# Show leaderboard
|
|
219
242
|
RESULT=$(curl -s "$LEADERBOARD_API/api/leaderboard" 2>/dev/null)
|
|
220
243
|
if [ -z "$RESULT" ]; then
|
|
221
244
|
echo " Could not reach leaderboard. Check your connection."
|
|
@@ -223,7 +246,7 @@ case "$1" in
|
|
|
223
246
|
node -e "
|
|
224
247
|
const d = JSON.parse(\`$RESULT\`);
|
|
225
248
|
if (!d.rankings || d.rankings.length === 0) {
|
|
226
|
-
console.log(' No
|
|
249
|
+
console.log(' No reviews this week yet.');
|
|
227
250
|
process.exit(0);
|
|
228
251
|
}
|
|
229
252
|
console.log('');
|
|
@@ -235,7 +258,7 @@ case "$1" in
|
|
|
235
258
|
const medal = medals[i] || ' ';
|
|
236
259
|
const tokens = r.total_tokens >= 1000 ? (r.total_tokens/1000).toFixed(1)+'k' : r.total_tokens;
|
|
237
260
|
const tw = r.twitter ? ' @'+r.twitter : '';
|
|
238
|
-
console.log(' ' + medal + ' #' + (i+1) + ' ' + r.github.padEnd(20) + tokens.padStart(8) + ' tokens
|
|
261
|
+
console.log(' ' + medal + ' #' + (i+1) + ' ' + r.github.padEnd(20) + tokens.padStart(8) + ' tokens' + tw);
|
|
239
262
|
}
|
|
240
263
|
console.log('');
|
|
241
264
|
console.log(' https://smoothiecode.com/leaderboard');
|
|
@@ -245,31 +268,19 @@ case "$1" in
|
|
|
245
268
|
;;
|
|
246
269
|
esac
|
|
247
270
|
;;
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
echo " smoothie blend \"<prompt>\" Run a blend from terminal"
|
|
262
|
-
echo " smoothie blend --deep \"<prompt>\" Deep blend with full context"
|
|
263
|
-
echo ""
|
|
264
|
-
echo " smoothie leaderboard View weekly rankings"
|
|
265
|
-
echo " smoothie leaderboard join Join the leaderboard"
|
|
266
|
-
echo " smoothie leaderboard leave Leave and delete data"
|
|
267
|
-
echo " smoothie leaderboard profile Set Twitter handle"
|
|
268
|
-
echo ""
|
|
269
|
-
echo " smoothie stats Usage stats and history"
|
|
270
|
-
echo " smoothie stats --share Shareable stats page"
|
|
271
|
-
echo " smoothie share Share last blend report"
|
|
272
|
-
echo ""
|
|
271
|
+
install)
|
|
272
|
+
bash "$INSTALL_DIR/install.sh"
|
|
273
|
+
;;
|
|
274
|
+
help|--help|-h)
|
|
275
|
+
show_help
|
|
276
|
+
;;
|
|
277
|
+
"")
|
|
278
|
+
# No args: install if first time, help if already set up
|
|
279
|
+
if [ ! -f "$CONFIG" ]; then
|
|
280
|
+
bash "$INSTALL_DIR/install.sh"
|
|
281
|
+
else
|
|
282
|
+
show_help
|
|
283
|
+
fi
|
|
273
284
|
;;
|
|
274
285
|
*)
|
|
275
286
|
echo " Unknown command: $1"
|
package/dist/blend-cli.js
CHANGED
|
@@ -9,15 +9,21 @@
|
|
|
9
9
|
* Queries Codex + OpenRouter models in parallel, prints JSON results to stdout.
|
|
10
10
|
* Progress goes to stderr so it doesn't interfere with hook JSON output.
|
|
11
11
|
*/
|
|
12
|
-
import { readFileSync, writeFileSync } from 'fs';
|
|
12
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
13
13
|
import { fileURLToPath } from 'url';
|
|
14
14
|
import { dirname, join } from 'path';
|
|
15
|
+
import { homedir } from 'os';
|
|
15
16
|
import { execFile as execFileCb } from 'child_process';
|
|
16
17
|
import { promisify } from 'util';
|
|
17
18
|
import { createInterface } from 'readline';
|
|
18
19
|
const execFile = promisify(execFileCb);
|
|
19
20
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
20
21
|
const PROJECT_ROOT = join(__dirname, '..');
|
|
22
|
+
const SMOOTHIE_HOME = join(homedir(), '.smoothie');
|
|
23
|
+
try {
|
|
24
|
+
mkdirSync(SMOOTHIE_HOME, { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
catch { }
|
|
21
27
|
// ---------------------------------------------------------------------------
|
|
22
28
|
// .env loader
|
|
23
29
|
// ---------------------------------------------------------------------------
|
|
@@ -194,13 +200,13 @@ async function main() {
|
|
|
194
200
|
// Save for share command + append to history
|
|
195
201
|
try {
|
|
196
202
|
const { appendFileSync } = await import('fs');
|
|
197
|
-
writeFileSync(join(
|
|
203
|
+
writeFileSync(join(SMOOTHIE_HOME, '.last-blend.json'), JSON.stringify({ results }, null, 2));
|
|
198
204
|
const entry = {
|
|
199
205
|
ts: new Date().toISOString(),
|
|
200
206
|
type: deep ? 'deep' : 'blend',
|
|
201
207
|
models: results.map(r => ({ model: r.model, elapsed_s: r.elapsed_s, tokens: r.tokens, error: r.response.startsWith('Error:') })),
|
|
202
208
|
};
|
|
203
|
-
appendFileSync(join(
|
|
209
|
+
appendFileSync(join(SMOOTHIE_HOME, '.smoothie-history.jsonl'), JSON.stringify(entry) + '\n');
|
|
204
210
|
}
|
|
205
211
|
catch { }
|
|
206
212
|
// Submit to leaderboard if opted in
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { readFileSync } from 'fs';
|
|
1
|
+
import { readFileSync, mkdirSync } from 'fs';
|
|
2
2
|
import { fileURLToPath } from 'url';
|
|
3
3
|
import { dirname, join } from 'path';
|
|
4
|
+
import { homedir } from 'os';
|
|
4
5
|
import { execFile as execFileCb, execFileSync } from 'child_process';
|
|
5
6
|
import { promisify } from 'util';
|
|
6
7
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
@@ -9,6 +10,17 @@ import { z } from 'zod';
|
|
|
9
10
|
const execFile = promisify(execFileCb);
|
|
10
11
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
12
|
const PROJECT_ROOT = join(__dirname, '..');
|
|
13
|
+
const SMOOTHIE_HOME = join(homedir(), '.smoothie');
|
|
14
|
+
try {
|
|
15
|
+
mkdirSync(SMOOTHIE_HOME, { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
catch { }
|
|
18
|
+
// Read version from package.json
|
|
19
|
+
let CURRENT_VERSION = '0.0.0';
|
|
20
|
+
try {
|
|
21
|
+
CURRENT_VERSION = JSON.parse(readFileSync(join(PROJECT_ROOT, 'package.json'), 'utf8')).version;
|
|
22
|
+
}
|
|
23
|
+
catch { }
|
|
12
24
|
// ---------------------------------------------------------------------------
|
|
13
25
|
// .env loader (no dotenv dependency)
|
|
14
26
|
// ---------------------------------------------------------------------------
|
|
@@ -164,7 +176,7 @@ function buildDeepContext(prompt) {
|
|
|
164
176
|
// MCP Server
|
|
165
177
|
// ---------------------------------------------------------------------------
|
|
166
178
|
const server = new McpServer({ name: 'smoothie', version: '1.0.0' });
|
|
167
|
-
server.tool('
|
|
179
|
+
server.tool('smoothie_review', {
|
|
168
180
|
prompt: z.string().describe('The prompt to send to all models'),
|
|
169
181
|
deep: z.boolean().optional().describe('Full context mode with project files and git diff'),
|
|
170
182
|
}, async ({ prompt, deep }) => {
|
|
@@ -199,7 +211,7 @@ server.tool('smoothie_blend', {
|
|
|
199
211
|
});
|
|
200
212
|
}
|
|
201
213
|
// Print initial progress
|
|
202
|
-
process.stderr.write('\n\u{1F9C3} Smoothie
|
|
214
|
+
process.stderr.write('\n\u{1F9C3} Smoothie reviewing...\n\n');
|
|
203
215
|
for (const { label } of models) {
|
|
204
216
|
process.stderr.write(` \u23F3 ${label.padEnd(26)} waiting...\n`);
|
|
205
217
|
}
|
|
@@ -228,13 +240,13 @@ server.tool('smoothie_blend', {
|
|
|
228
240
|
// Save for share command + append to history
|
|
229
241
|
try {
|
|
230
242
|
const { writeFileSync, appendFileSync } = await import('fs');
|
|
231
|
-
writeFileSync(join(
|
|
243
|
+
writeFileSync(join(SMOOTHIE_HOME, '.last-review.json'), JSON.stringify({ results }, null, 2));
|
|
232
244
|
const entry = {
|
|
233
245
|
ts: new Date().toISOString(),
|
|
234
|
-
type: deep ? 'deep' : '
|
|
246
|
+
type: deep ? 'deep' : 'review',
|
|
235
247
|
models: results.map(r => ({ model: r.model, elapsed_s: r.elapsed_s, tokens: r.tokens, error: r.response.startsWith('Error:') })),
|
|
236
248
|
};
|
|
237
|
-
appendFileSync(join(
|
|
249
|
+
appendFileSync(join(SMOOTHIE_HOME, '.smoothie-history.jsonl'), JSON.stringify(entry) + '\n');
|
|
238
250
|
}
|
|
239
251
|
catch { }
|
|
240
252
|
// Submit to leaderboard if opted in
|
|
@@ -247,13 +259,13 @@ server.tool('smoothie_blend', {
|
|
|
247
259
|
const weekNum = Math.ceil((days + jan1.getDay() + 1) / 7);
|
|
248
260
|
const week = `${now.getFullYear()}-W${String(weekNum).padStart(2, '0')}`;
|
|
249
261
|
const totalTokens = results.reduce((s, r) => s + (r.tokens?.total || 0), 0);
|
|
250
|
-
const
|
|
262
|
+
const reviewId = `${cfg.github}-${Date.now()}`;
|
|
251
263
|
await fetch('https://api.smoothiecode.com/api/submit', {
|
|
252
264
|
method: 'POST',
|
|
253
265
|
headers: { 'Content-Type': 'application/json' },
|
|
254
266
|
body: JSON.stringify({
|
|
255
267
|
github: cfg.github,
|
|
256
|
-
|
|
268
|
+
review_id: reviewId,
|
|
257
269
|
tokens: totalTokens,
|
|
258
270
|
blends: 1,
|
|
259
271
|
models: results.map(r => ({ model: r.model, tokens: r.tokens?.total || 0 })),
|
|
@@ -264,8 +276,31 @@ server.tool('smoothie_blend', {
|
|
|
264
276
|
}
|
|
265
277
|
}
|
|
266
278
|
catch { }
|
|
279
|
+
// Weekly update check (non-blocking)
|
|
280
|
+
let updateNote = '';
|
|
281
|
+
try {
|
|
282
|
+
const checkFile = join(SMOOTHIE_HOME, '.update-check');
|
|
283
|
+
let shouldCheck = true;
|
|
284
|
+
try {
|
|
285
|
+
const last = readFileSync(checkFile, 'utf8').trim();
|
|
286
|
+
if (Date.now() - parseInt(last) < 7 * 24 * 60 * 60 * 1000)
|
|
287
|
+
shouldCheck = false;
|
|
288
|
+
}
|
|
289
|
+
catch { }
|
|
290
|
+
if (shouldCheck) {
|
|
291
|
+
const { writeFileSync: wfs } = await import('fs');
|
|
292
|
+
wfs(checkFile, String(Date.now()));
|
|
293
|
+
const res = await fetch('https://registry.npmjs.org/smoothie-code/latest', { signal: AbortSignal.timeout(3000) });
|
|
294
|
+
const { version: latest } = (await res.json());
|
|
295
|
+
const current = CURRENT_VERSION;
|
|
296
|
+
if (latest && latest !== current) {
|
|
297
|
+
updateNote = `\n\n⬆ Smoothie update available: ${current} → ${latest} — run: npx smoothie-code`;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
catch { }
|
|
267
302
|
return {
|
|
268
|
-
content: [{ type: 'text', text: JSON.stringify({ results }, null, 2) }],
|
|
303
|
+
content: [{ type: 'text', text: JSON.stringify({ results }, null, 2) + updateNote }],
|
|
269
304
|
};
|
|
270
305
|
});
|
|
271
306
|
server.tool('smoothie_estimate', {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* review-cli.ts — Standalone blend runner for hooks.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* node dist/review-cli.js "Review this plan: ..."
|
|
7
|
+
* echo "plan text" | node dist/review-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 {};
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* review-cli.ts — Standalone blend runner for hooks.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* node dist/review-cli.js "Review this plan: ..."
|
|
7
|
+
* echo "plan text" | node dist/review-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
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
13
|
+
import { fileURLToPath } from 'url';
|
|
14
|
+
import { dirname, join } from 'path';
|
|
15
|
+
import { homedir } from 'os';
|
|
16
|
+
import { execFile as execFileCb } from 'child_process';
|
|
17
|
+
import { promisify } from 'util';
|
|
18
|
+
import { createInterface } from 'readline';
|
|
19
|
+
const execFile = promisify(execFileCb);
|
|
20
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
const PROJECT_ROOT = join(__dirname, '..');
|
|
22
|
+
const SMOOTHIE_HOME = join(homedir(), '.smoothie');
|
|
23
|
+
try {
|
|
24
|
+
mkdirSync(SMOOTHIE_HOME, { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
catch { }
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// .env loader
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
function loadEnv() {
|
|
31
|
+
try {
|
|
32
|
+
const env = readFileSync(join(PROJECT_ROOT, '.env'), 'utf8');
|
|
33
|
+
for (const line of env.split('\n')) {
|
|
34
|
+
const [key, ...val] = line.split('=');
|
|
35
|
+
if (key && val.length)
|
|
36
|
+
process.env[key.trim()] = val.join('=').trim();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// no .env
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
loadEnv();
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Model queries (same as index.ts)
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
async function queryCodex(prompt) {
|
|
48
|
+
try {
|
|
49
|
+
const tmpFile = join(PROJECT_ROOT, `.codex-out-${Date.now()}.txt`);
|
|
50
|
+
await execFile('codex', ['exec', '--full-auto', '-o', tmpFile, prompt], {
|
|
51
|
+
timeout: 0,
|
|
52
|
+
});
|
|
53
|
+
let response;
|
|
54
|
+
try {
|
|
55
|
+
response = readFileSync(tmpFile, 'utf8').trim();
|
|
56
|
+
const { unlinkSync } = await import('fs');
|
|
57
|
+
unlinkSync(tmpFile);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
response = '';
|
|
61
|
+
}
|
|
62
|
+
const estimatedTokens = Math.ceil(prompt.length / 3) + Math.ceil(response.length / 3);
|
|
63
|
+
return {
|
|
64
|
+
model: 'Codex',
|
|
65
|
+
response: response || '(empty response)',
|
|
66
|
+
tokens: { prompt: Math.ceil(prompt.length / 3), completion: Math.ceil(response.length / 3), total: estimatedTokens },
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
71
|
+
return { model: 'Codex', response: `Error: ${message}` };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async function queryOpenRouter(prompt, modelId, modelLabel) {
|
|
75
|
+
try {
|
|
76
|
+
const controller = new AbortController();
|
|
77
|
+
const timer = setTimeout(() => controller.abort(), 60_000);
|
|
78
|
+
const res = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
|
79
|
+
method: 'POST',
|
|
80
|
+
headers: {
|
|
81
|
+
'Authorization': `Bearer ${process.env.OPENROUTER_API_KEY}`,
|
|
82
|
+
'HTTP-Referer': 'https://smoothiecode.com',
|
|
83
|
+
'X-Title': 'Smoothie',
|
|
84
|
+
'Content-Type': 'application/json',
|
|
85
|
+
},
|
|
86
|
+
body: JSON.stringify({
|
|
87
|
+
model: modelId,
|
|
88
|
+
messages: [{ role: 'user', content: prompt }],
|
|
89
|
+
}),
|
|
90
|
+
signal: controller.signal,
|
|
91
|
+
});
|
|
92
|
+
clearTimeout(timer);
|
|
93
|
+
const data = (await res.json());
|
|
94
|
+
const text = data.choices?.[0]?.message?.content ?? 'No response content';
|
|
95
|
+
const usage = data.usage;
|
|
96
|
+
return {
|
|
97
|
+
model: modelLabel,
|
|
98
|
+
response: text,
|
|
99
|
+
tokens: usage ? { prompt: usage.prompt_tokens || 0, completion: usage.completion_tokens || 0, total: usage.total_tokens || 0 } : undefined,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
104
|
+
return { model: modelLabel, response: `Error: ${message}` };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// Read prompt from arg or stdin
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
async function getPrompt() {
|
|
111
|
+
if (process.argv[2])
|
|
112
|
+
return process.argv[2];
|
|
113
|
+
// Read from stdin
|
|
114
|
+
const rl = createInterface({ input: process.stdin });
|
|
115
|
+
const lines = [];
|
|
116
|
+
for await (const line of rl) {
|
|
117
|
+
lines.push(line);
|
|
118
|
+
}
|
|
119
|
+
return lines.join('\n');
|
|
120
|
+
}
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
// Main
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
async function main() {
|
|
125
|
+
const args = process.argv.slice(2);
|
|
126
|
+
const deep = args.includes('--deep');
|
|
127
|
+
const filteredArgs = args.filter(a => a !== '--deep');
|
|
128
|
+
// Temporarily override argv for getPrompt
|
|
129
|
+
process.argv = [process.argv[0], process.argv[1], ...filteredArgs];
|
|
130
|
+
const prompt = await getPrompt();
|
|
131
|
+
if (!prompt.trim()) {
|
|
132
|
+
process.stderr.write('review-cli: no prompt provided\n');
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
let finalPrompt = prompt;
|
|
136
|
+
if (deep) {
|
|
137
|
+
// Read context file
|
|
138
|
+
for (const name of ['GEMINI.md', 'CLAUDE.md', 'AGENTS.md']) {
|
|
139
|
+
try {
|
|
140
|
+
const content = readFileSync(join(process.cwd(), name), 'utf8');
|
|
141
|
+
if (content.trim()) {
|
|
142
|
+
finalPrompt = `## Context File\n${content}\n\n## Prompt\n${prompt}`;
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
// file not found, try next
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// Add git diff
|
|
151
|
+
try {
|
|
152
|
+
const { execFileSync } = await import('child_process');
|
|
153
|
+
const diff = execFileSync('git', ['diff', 'HEAD~3'], { encoding: 'utf8', maxBuffer: 100 * 1024, timeout: 10000 });
|
|
154
|
+
if (diff)
|
|
155
|
+
finalPrompt += `\n\n## Recent Git Diff\n${diff.slice(0, 40000)}`;
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
// no git diff available
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
let config;
|
|
162
|
+
try {
|
|
163
|
+
config = JSON.parse(readFileSync(join(PROJECT_ROOT, 'config.json'), 'utf8'));
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
config = { openrouter_models: [] };
|
|
167
|
+
}
|
|
168
|
+
const models = [
|
|
169
|
+
{ fn: () => queryCodex(finalPrompt), label: 'Codex' },
|
|
170
|
+
...config.openrouter_models.map((m) => ({
|
|
171
|
+
fn: () => queryOpenRouter(finalPrompt, m.id, m.label),
|
|
172
|
+
label: m.label,
|
|
173
|
+
})),
|
|
174
|
+
];
|
|
175
|
+
process.stderr.write('\n🧃 Smoothie reviewing...\n\n');
|
|
176
|
+
for (const { label } of models) {
|
|
177
|
+
process.stderr.write(` ⏳ ${label.padEnd(26)} waiting...\n`);
|
|
178
|
+
}
|
|
179
|
+
process.stderr.write('\n');
|
|
180
|
+
const startTimes = {};
|
|
181
|
+
const promises = models.map(({ fn, label }) => {
|
|
182
|
+
startTimes[label] = Date.now();
|
|
183
|
+
return fn()
|
|
184
|
+
.then((result) => {
|
|
185
|
+
const elapsed = ((Date.now() - startTimes[label]) / 1000);
|
|
186
|
+
process.stderr.write(` ✓ ${label.padEnd(26)} done (${elapsed.toFixed(1)}s)\n`);
|
|
187
|
+
return { ...result, elapsed_s: parseFloat(elapsed.toFixed(1)) };
|
|
188
|
+
})
|
|
189
|
+
.catch((err) => {
|
|
190
|
+
const elapsed = ((Date.now() - startTimes[label]) / 1000);
|
|
191
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
192
|
+
process.stderr.write(` ✗ ${label.padEnd(26)} failed (${elapsed.toFixed(1)}s)\n`);
|
|
193
|
+
return { model: label, response: `Error: ${message}`, elapsed_s: parseFloat(elapsed.toFixed(1)) };
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
const results = await Promise.all(promises);
|
|
197
|
+
process.stderr.write('\n ◆ All done.\n\n');
|
|
198
|
+
// Output JSON to stdout (for hook consumption)
|
|
199
|
+
process.stdout.write(JSON.stringify({ results }, null, 2));
|
|
200
|
+
// Save for share command + append to history
|
|
201
|
+
try {
|
|
202
|
+
const { appendFileSync } = await import('fs');
|
|
203
|
+
writeFileSync(join(SMOOTHIE_HOME, '.last-review.json'), JSON.stringify({ results }, null, 2));
|
|
204
|
+
const entry = {
|
|
205
|
+
ts: new Date().toISOString(),
|
|
206
|
+
type: deep ? 'deep' : 'review',
|
|
207
|
+
models: results.map(r => ({ model: r.model, elapsed_s: r.elapsed_s, tokens: r.tokens, error: r.response.startsWith('Error:') })),
|
|
208
|
+
};
|
|
209
|
+
appendFileSync(join(SMOOTHIE_HOME, '.smoothie-history.jsonl'), JSON.stringify(entry) + '\n');
|
|
210
|
+
}
|
|
211
|
+
catch { }
|
|
212
|
+
// Submit to leaderboard if opted in
|
|
213
|
+
try {
|
|
214
|
+
const cfg = JSON.parse(readFileSync(join(PROJECT_ROOT, 'config.json'), 'utf8'));
|
|
215
|
+
if (cfg.leaderboard && cfg.github) {
|
|
216
|
+
const now = new Date();
|
|
217
|
+
const jan1 = new Date(now.getFullYear(), 0, 1);
|
|
218
|
+
const days = Math.floor((now.getTime() - jan1.getTime()) / 86400000);
|
|
219
|
+
const weekNum = Math.ceil((days + jan1.getDay() + 1) / 7);
|
|
220
|
+
const week = `${now.getFullYear()}-W${String(weekNum).padStart(2, '0')}`;
|
|
221
|
+
const totalTokens = results.reduce((s, r) => s + (r.tokens?.total || 0), 0);
|
|
222
|
+
const reviewId = `${cfg.github}-${Date.now()}`;
|
|
223
|
+
await fetch('https://api.smoothiecode.com/api/submit', {
|
|
224
|
+
method: 'POST',
|
|
225
|
+
headers: { 'Content-Type': 'application/json' },
|
|
226
|
+
body: JSON.stringify({
|
|
227
|
+
github: cfg.github,
|
|
228
|
+
review_id: reviewId,
|
|
229
|
+
tokens: totalTokens,
|
|
230
|
+
blends: 1,
|
|
231
|
+
models: results.map(r => ({ model: r.model, tokens: r.tokens?.total || 0 })),
|
|
232
|
+
week,
|
|
233
|
+
}),
|
|
234
|
+
signal: AbortSignal.timeout(5000),
|
|
235
|
+
}).catch(() => { });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
catch { }
|
|
239
|
+
const totalTime = Math.max(...results.map(r => r.elapsed_s || 0));
|
|
240
|
+
const totalTokens = results.reduce((sum, r) => sum + (r.tokens?.total || 0), 0);
|
|
241
|
+
const responded = results.filter(r => !r.response.startsWith('Error:')).length;
|
|
242
|
+
process.stderr.write(` ${responded}/${results.length} models · ${totalTime.toFixed(1)}s · ${totalTokens} tokens\n\n`);
|
|
243
|
+
}
|
|
244
|
+
main();
|
package/dist/select-models.js
CHANGED
|
@@ -234,7 +234,7 @@ async function cmdPick(apiKey, configPath) {
|
|
|
234
234
|
finalSelection.push(p);
|
|
235
235
|
}
|
|
236
236
|
}
|
|
237
|
-
// Preserve existing config fields (like
|
|
237
|
+
// Preserve existing config fields (like auto_review)
|
|
238
238
|
const existing = loadConfig(configPath);
|
|
239
239
|
existing.openrouter_models = finalSelection;
|
|
240
240
|
saveConfig(configPath, existing);
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
#
|
|
3
|
+
# gemini-review-hook.sh — BeforeTool hook for exit_plan_mode (Gemini CLI)
|
|
4
|
+
#
|
|
5
|
+
# Same as auto-review-hook.sh but adapted for Gemini CLI:
|
|
6
|
+
# - BeforeTool instead of PreToolUse
|
|
7
|
+
# - exit_plan_mode instead of ExitPlanMode
|
|
8
|
+
# - Reads plan from tool_input.plan_path instead of tailing transcript
|
|
9
|
+
#
|
|
10
|
+
# Caveat: only fires when the agent calls exit_plan_mode itself,
|
|
11
|
+
# not when user manually toggles via /plan or Shift+Tab.
|
|
12
|
+
#
|
|
13
|
+
|
|
14
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
15
|
+
|
|
16
|
+
# Read hook input from stdin
|
|
17
|
+
INPUT=$(cat)
|
|
18
|
+
|
|
19
|
+
# Check if auto-review is enabled in config
|
|
20
|
+
if [ -f "$SCRIPT_DIR/config.json" ]; then
|
|
21
|
+
AUTO_ENABLED=$(node -e "
|
|
22
|
+
try {
|
|
23
|
+
const c = JSON.parse(require('fs').readFileSync('$SCRIPT_DIR/config.json','utf8'));
|
|
24
|
+
console.log(c.auto_review === true ? 'true' : 'false');
|
|
25
|
+
} catch(e) { console.log('false'); }
|
|
26
|
+
" 2>/dev/null)
|
|
27
|
+
|
|
28
|
+
if [ "$AUTO_ENABLED" != "true" ]; then
|
|
29
|
+
echo '{"hookSpecificOutput":{"hookEventName":"BeforeTool","permissionDecision":"allow"}}'
|
|
30
|
+
exit 0
|
|
31
|
+
fi
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
# Try to read plan from tool_input.plan_path (Gemini provides this)
|
|
35
|
+
PLAN_PATH=$(echo "$INPUT" | python3 -c "
|
|
36
|
+
import sys, json
|
|
37
|
+
try:
|
|
38
|
+
d = json.load(sys.stdin)
|
|
39
|
+
print(d.get('tool_input', {}).get('plan_path', ''))
|
|
40
|
+
except:
|
|
41
|
+
print('')
|
|
42
|
+
" 2>/dev/null)
|
|
43
|
+
|
|
44
|
+
PLAN_CONTEXT=""
|
|
45
|
+
if [ -n "$PLAN_PATH" ] && [ -f "$PLAN_PATH" ]; then
|
|
46
|
+
PLAN_CONTEXT=$(cat "$PLAN_PATH" 2>/dev/null | head -c 16000)
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
# Fallback: read from transcript if plan_path not available
|
|
50
|
+
if [ -z "$PLAN_CONTEXT" ]; then
|
|
51
|
+
TRANSCRIPT_PATH=$(echo "$INPUT" | python3 -c "
|
|
52
|
+
import sys, json
|
|
53
|
+
try:
|
|
54
|
+
d = json.load(sys.stdin)
|
|
55
|
+
print(d.get('transcript_path', ''))
|
|
56
|
+
except:
|
|
57
|
+
print('')
|
|
58
|
+
" 2>/dev/null)
|
|
59
|
+
|
|
60
|
+
if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
|
|
61
|
+
PLAN_CONTEXT=$(tail -c 4000 "$TRANSCRIPT_PATH" 2>/dev/null)
|
|
62
|
+
fi
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
if [ -z "$PLAN_CONTEXT" ]; then
|
|
66
|
+
echo '{"hookSpecificOutput":{"hookEventName":"BeforeTool","permissionDecision":"allow"}}'
|
|
67
|
+
exit 0
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
# Build the review prompt
|
|
71
|
+
REVIEW_PROMPT="You are reviewing a plan that Gemini CLI generated. Analyze it for:
|
|
72
|
+
- Missing steps or edge cases
|
|
73
|
+
- Better approaches or optimizations
|
|
74
|
+
- Potential bugs or issues
|
|
75
|
+
- Security concerns
|
|
76
|
+
|
|
77
|
+
Here is the plan:
|
|
78
|
+
|
|
79
|
+
$PLAN_CONTEXT
|
|
80
|
+
|
|
81
|
+
Provide concise, actionable feedback. Focus only on things that should change."
|
|
82
|
+
|
|
83
|
+
# Run the blend (progress shows on stderr, results on stdout)
|
|
84
|
+
BLEND_RESULTS=$(echo "$REVIEW_PROMPT" | node "$SCRIPT_DIR/dist/review-cli.js" 2>/dev/stderr)
|
|
85
|
+
|
|
86
|
+
# Generate share link (metadata only, no raw content)
|
|
87
|
+
SHARE_URL=""
|
|
88
|
+
SHARE_PARAMS=$(echo "$BLEND_RESULTS" | node -e "
|
|
89
|
+
let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{
|
|
90
|
+
try {
|
|
91
|
+
const r=JSON.parse(d);
|
|
92
|
+
const models=r.results.map(m=>m.model).join(',');
|
|
93
|
+
const times=r.results.map(m=>m.elapsed_s||0).join(',');
|
|
94
|
+
const tokens=r.results.map(m=>(m.tokens&&m.tokens.total)||0).join(',');
|
|
95
|
+
console.log('models='+encodeURIComponent(models)+'×='+encodeURIComponent(times)+'&tokens='+encodeURIComponent(tokens)+'&type=plan');
|
|
96
|
+
} catch { console.log(''); }
|
|
97
|
+
});
|
|
98
|
+
" 2>/dev/null)
|
|
99
|
+
|
|
100
|
+
if [ -n "$SHARE_PARAMS" ]; then
|
|
101
|
+
SHARE_URL=$(curl -s --max-time 5 --connect-timeout 3 -X POST "https://api.smoothiecode.com/api/share" \
|
|
102
|
+
-H "Content-Type: application/json" \
|
|
103
|
+
-d "{\"type\":\"report\",\"data\":\"$SHARE_PARAMS\"}" 2>/dev/null | node -e "
|
|
104
|
+
let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{
|
|
105
|
+
try{console.log(JSON.parse(d).url)}catch{console.log('')}
|
|
106
|
+
});
|
|
107
|
+
" 2>/dev/null) || true
|
|
108
|
+
fi
|
|
109
|
+
|
|
110
|
+
if [ -z "$BLEND_RESULTS" ]; then
|
|
111
|
+
echo '{"hookSpecificOutput":{"hookEventName":"BeforeTool","permissionDecision":"allow"}}'
|
|
112
|
+
exit 0
|
|
113
|
+
fi
|
|
114
|
+
|
|
115
|
+
# Build the additionalContext string
|
|
116
|
+
CONTEXT="🧃 Smoothie auto-review results — multiple models reviewed this plan:
|
|
117
|
+
|
|
118
|
+
$BLEND_RESULTS
|
|
119
|
+
|
|
120
|
+
IMPORTANT — You must do TWO things:
|
|
121
|
+
|
|
122
|
+
1. FIRST, show a Smoothie report table. Use elapsed_s and tokens from each result:
|
|
123
|
+
|
|
124
|
+
🧃 Smoothie reviewed this plan (N models, Xs total):
|
|
125
|
+
|
|
126
|
+
| Model | Time | Tokens | Verdict | Feedback |
|
|
127
|
+
|-------|------|--------|---------|----------|
|
|
128
|
+
| ModelName | 3.1s | 1,847 | useful | one-line summary |
|
|
129
|
+
| ModelName | 8.2s | 2,104 | generic | one-line summary |
|
|
130
|
+
(skip errored/empty models. Verdict = useful, generic, or irrelevant)
|
|
131
|
+
$([ -n "$SHARE_URL" ] && echo "
|
|
132
|
+
Share this report: $SHARE_URL")
|
|
133
|
+
|
|
134
|
+
2. THEN proceed with implementation as planned, incorporating valid Smoothie suggestions as you go. Do NOT stop to revise — start coding immediately."
|
|
135
|
+
|
|
136
|
+
# Return: allow exit_plan_mode but inject blend results
|
|
137
|
+
node -e "
|
|
138
|
+
const ctx = $(echo "$CONTEXT" | python3 -c "import sys,json; print(json.dumps(sys.stdin.read()))" 2>/dev/null);
|
|
139
|
+
console.log(JSON.stringify({
|
|
140
|
+
hookSpecificOutput: {
|
|
141
|
+
hookEventName: 'BeforeTool',
|
|
142
|
+
permissionDecision: 'allow',
|
|
143
|
+
permissionDecisionReason: 'Smoothie auto-review completed',
|
|
144
|
+
additionalContext: ctx
|
|
145
|
+
}
|
|
146
|
+
}));
|
|
147
|
+
"
|
|
148
|
+
|
|
149
|
+
exit 0
|
package/install.sh
CHANGED
|
@@ -104,13 +104,13 @@ if [ "$PLATFORM" != "codex" ]; then
|
|
|
104
104
|
step "Setting up Codex ${D}(optional)${N}"
|
|
105
105
|
|
|
106
106
|
echo ""
|
|
107
|
-
echo -e " ${D}Codex adds OpenAI's coding model to the
|
|
107
|
+
echo -e " ${D}Codex adds OpenAI's coding model to the review.${N}"
|
|
108
108
|
echo -e " ${D}Requires a ChatGPT account. Skip if you only want OpenRouter.${N}"
|
|
109
109
|
echo ""
|
|
110
110
|
read -p " Set up Codex? [Y/n]: " SETUP_CODEX
|
|
111
111
|
|
|
112
112
|
if [[ "$SETUP_CODEX" =~ ^[Nn]$ ]]; then
|
|
113
|
-
echo -e " ${D}Skipped —
|
|
113
|
+
echo -e " ${D}Skipped — review will use OpenRouter models only${N}"
|
|
114
114
|
else
|
|
115
115
|
if ! command -v codex &>/dev/null; then
|
|
116
116
|
npm install -g @openai/codex 2>/dev/null &
|
|
@@ -156,24 +156,24 @@ step "Choosing models"
|
|
|
156
156
|
echo ""
|
|
157
157
|
node "$SCRIPT_DIR/dist/select-models.js" "$OPENROUTER_KEY" "$SCRIPT_DIR/config.json"
|
|
158
158
|
|
|
159
|
-
# ─── Step 6: Auto-
|
|
159
|
+
# ─── Step 6: Auto-review ──────────────────────────────────────────────
|
|
160
160
|
step "Configuring hooks"
|
|
161
161
|
|
|
162
|
-
chmod +x "$SCRIPT_DIR/plan-hook.sh" "$SCRIPT_DIR/auto-
|
|
162
|
+
chmod +x "$SCRIPT_DIR/plan-hook.sh" "$SCRIPT_DIR/auto-review-hook.sh" "$SCRIPT_DIR/pr-review-hook.sh"
|
|
163
163
|
|
|
164
164
|
echo ""
|
|
165
|
-
echo -e " ${B}Auto-
|
|
165
|
+
echo -e " ${B}Auto-review${N} reviews every plan with all models before"
|
|
166
166
|
echo -e " you approve. Adds 30-90s per plan."
|
|
167
167
|
echo ""
|
|
168
|
-
read -p " Enable auto-
|
|
168
|
+
read -p " Enable auto-review? [y/N]: " AUTO_BLEND
|
|
169
169
|
if [[ "$AUTO_BLEND" =~ ^[Yy]$ ]]; then
|
|
170
170
|
node -e "
|
|
171
171
|
const fs = require('fs');
|
|
172
172
|
const c = JSON.parse(fs.readFileSync('$SCRIPT_DIR/config.json','utf8'));
|
|
173
|
-
c.
|
|
173
|
+
c.auto_review = true;
|
|
174
174
|
fs.writeFileSync('$SCRIPT_DIR/config.json', JSON.stringify(c, null, 2));
|
|
175
175
|
"
|
|
176
|
-
echo -e " ${G}✓${N} Auto-
|
|
176
|
+
echo -e " ${G}✓${N} Auto-review on"
|
|
177
177
|
else
|
|
178
178
|
echo -e " ${D}Skipped — toggle in config.json anytime${N}"
|
|
179
179
|
fi
|
|
@@ -236,7 +236,7 @@ The user has provided this context/problem:
|
|
|
236
236
|
$ARGUMENTS
|
|
237
237
|
|
|
238
238
|
**Step 1 — Blend**
|
|
239
|
-
Call `
|
|
239
|
+
Call `smoothie_review` with the user's prompt. The MCP server queries all
|
|
240
240
|
models in parallel and shows live progress in the terminal. Wait for it to return.
|
|
241
241
|
|
|
242
242
|
**Step 2 — Judge and respond**
|
|
@@ -261,7 +261,7 @@ $ARGUMENTS
|
|
|
261
261
|
Run `git diff main...HEAD` to get the full branch diff.
|
|
262
262
|
|
|
263
263
|
**Step 2 — Blend**
|
|
264
|
-
Call `
|
|
264
|
+
Call `smoothie_review` with a prompt asking models to review the diff for:
|
|
265
265
|
- Bugs, logic errors, edge cases
|
|
266
266
|
- Security vulnerabilities
|
|
267
267
|
- Performance issues
|
|
@@ -299,20 +299,20 @@ if (!preExists) {
|
|
|
299
299
|
matcher: "ExitPlanMode",
|
|
300
300
|
hooks: [{
|
|
301
301
|
type: "command",
|
|
302
|
-
command: "bash $SCRIPT_DIR/auto-
|
|
302
|
+
command: "bash $SCRIPT_DIR/auto-review-hook.sh",
|
|
303
303
|
timeout: 600
|
|
304
304
|
}]
|
|
305
305
|
});
|
|
306
306
|
}
|
|
307
307
|
|
|
308
308
|
// Add PR review hook for Bash commands
|
|
309
|
-
const bashHookExists = s.hooks.PreToolUse.some(h => h.matcher === 'Bash' && h.hooks?.[0]?.command?.includes('pr-
|
|
309
|
+
const bashHookExists = s.hooks.PreToolUse.some(h => h.matcher === 'Bash' && h.hooks?.[0]?.command?.includes('pr-review-hook'));
|
|
310
310
|
if (!bashHookExists) {
|
|
311
311
|
s.hooks.PreToolUse.push({
|
|
312
312
|
matcher: "Bash",
|
|
313
313
|
hooks: [{
|
|
314
314
|
type: "command",
|
|
315
|
-
command: "bash $SCRIPT_DIR/pr-
|
|
315
|
+
command: "bash $SCRIPT_DIR/pr-review-hook.sh",
|
|
316
316
|
timeout: 600
|
|
317
317
|
}]
|
|
318
318
|
});
|
|
@@ -354,14 +354,14 @@ if [ "$PLATFORM" = "gemini" ]; then
|
|
|
354
354
|
|
|
355
355
|
# Gemini slash commands (.toml)
|
|
356
356
|
cat > "$HOME/.gemini/commands/smoothie.toml" << 'TOML'
|
|
357
|
-
description = "
|
|
357
|
+
description = "Review this problem across multiple AI models. Gemini judges."
|
|
358
358
|
|
|
359
359
|
prompt = """
|
|
360
360
|
You are running Smoothie — a multi-model review session.
|
|
361
361
|
|
|
362
362
|
{{args}}
|
|
363
363
|
|
|
364
|
-
Step 1 — Call
|
|
364
|
+
Step 1 — Call smoothie_review with the problem text. Wait for results.
|
|
365
365
|
|
|
366
366
|
Step 2 — You have responses from all models. Do NOT show raw outputs.
|
|
367
367
|
- If reviewing a problem: give the answer. Mention conflicts in one sentence.
|
|
@@ -381,14 +381,14 @@ You are running Smoothie PR Review.
|
|
|
381
381
|
{{args}}
|
|
382
382
|
|
|
383
383
|
Step 1 — Run git diff main...HEAD to get the branch diff.
|
|
384
|
-
Step 2 — Call
|
|
384
|
+
Step 2 — Call smoothie_review asking models to review the diff for bugs, security, performance.
|
|
385
385
|
Step 3 — Summarize findings with file:line references. Be direct.
|
|
386
386
|
"""
|
|
387
387
|
TOML
|
|
388
388
|
echo -e " ${G}✓${N} Slash command /smoothie-pr (Gemini)"
|
|
389
389
|
|
|
390
|
-
# Register auto-
|
|
391
|
-
chmod +x "$SCRIPT_DIR/gemini-
|
|
390
|
+
# Register auto-review hook for Gemini CLI
|
|
391
|
+
chmod +x "$SCRIPT_DIR/gemini-review-hook.sh"
|
|
392
392
|
GEMINI_SETTINGS="$HOME/.gemini/settings.json"
|
|
393
393
|
GEMINI_EXISTING="{}"
|
|
394
394
|
[ -f "$GEMINI_SETTINGS" ] && GEMINI_EXISTING=$(cat "$GEMINI_SETTINGS")
|
|
@@ -405,14 +405,14 @@ TOML
|
|
|
405
405
|
matcher: 'exit_plan_mode',
|
|
406
406
|
hooks: [{
|
|
407
407
|
type: 'command',
|
|
408
|
-
command: 'bash $SCRIPT_DIR/gemini-
|
|
408
|
+
command: 'bash $SCRIPT_DIR/gemini-review-hook.sh',
|
|
409
409
|
timeout: 600
|
|
410
410
|
}]
|
|
411
411
|
});
|
|
412
412
|
}
|
|
413
413
|
fs.writeFileSync('$GEMINI_SETTINGS', JSON.stringify(s, null, 2));
|
|
414
414
|
"
|
|
415
|
-
echo -e " ${G}✓${N} Auto-
|
|
415
|
+
echo -e " ${G}✓${N} Auto-review hook registered (BeforeTool/exit_plan_mode)"
|
|
416
416
|
fi
|
|
417
417
|
|
|
418
418
|
if [ "$PLATFORM" = "cursor" ]; then
|
|
@@ -435,7 +435,7 @@ if [ "$PLATFORM" = "cursor" ]; then
|
|
|
435
435
|
"
|
|
436
436
|
echo -e " ${G}✓${N} Cursor MCP server registered"
|
|
437
437
|
|
|
438
|
-
# Create Cursor Rule for auto-
|
|
438
|
+
# Create Cursor Rule for auto-review (only if auto-review is enabled)
|
|
439
439
|
if [[ "$AUTO_BLEND" =~ ^[Yy]$ ]]; then
|
|
440
440
|
mkdir -p "$HOME/.cursor/rules"
|
|
441
441
|
cat > "$HOME/.cursor/rules/smoothie.mdc" << 'CURSORRULE'
|
|
@@ -445,19 +445,27 @@ alwaysApply: true
|
|
|
445
445
|
---
|
|
446
446
|
|
|
447
447
|
Before proposing or implementing any plan that involves significant code changes,
|
|
448
|
-
call the `
|
|
448
|
+
call the `smoothie_review` MCP tool with a description of what you're about to do.
|
|
449
449
|
|
|
450
450
|
Wait for results, then:
|
|
451
451
|
1. Show a summary table: Model | Time | Tokens | Verdict | Key feedback
|
|
452
452
|
2. Incorporate valid suggestions into your plan
|
|
453
453
|
3. Proceed with implementation
|
|
454
454
|
|
|
455
|
-
Skip the
|
|
455
|
+
Skip the review for trivial changes (typos, single-line fixes, config tweaks).
|
|
456
456
|
CURSORRULE
|
|
457
|
-
echo -e " ${G}✓${N} Cursor Rule created (auto-
|
|
457
|
+
echo -e " ${G}✓${N} Cursor Rule created (auto-review)"
|
|
458
458
|
fi
|
|
459
459
|
fi
|
|
460
460
|
|
|
461
|
+
# ─── Setup ~/.smoothie/ (single source of truth for CLI) ─────────────
|
|
462
|
+
mkdir -p "$HOME/.smoothie"
|
|
463
|
+
chmod 700 "$HOME/.smoothie"
|
|
464
|
+
cp "$SCRIPT_DIR/config.json" "$HOME/.smoothie/config.json" 2>/dev/null
|
|
465
|
+
cp "$SCRIPT_DIR/.env" "$HOME/.smoothie/.env" 2>/dev/null
|
|
466
|
+
echo "$SCRIPT_DIR" > "$HOME/.smoothie/.install-path"
|
|
467
|
+
echo -e " ${G}✓${N} Config saved to ~/.smoothie/"
|
|
468
|
+
|
|
461
469
|
# ─── Done ─────────────────────────────────────────────────────────────
|
|
462
470
|
MODELS=$(node -e "
|
|
463
471
|
const d = JSON.parse(require('fs').readFileSync('$SCRIPT_DIR/config.json','utf8'));
|
|
@@ -470,16 +478,16 @@ echo ""
|
|
|
470
478
|
if [ "$PLATFORM" = "cursor" ]; then
|
|
471
479
|
echo -e " ${G}${B}Done!${N} Restart Cursor, then:"
|
|
472
480
|
echo ""
|
|
473
|
-
echo -e " ${D}Ask Cursor to plan something — it calls
|
|
481
|
+
echo -e " ${D}Ask Cursor to plan something — it calls smoothie_review via MCP${N}"
|
|
474
482
|
if [[ "$AUTO_BLEND" =~ ^[Yy]$ ]]; then
|
|
475
|
-
echo -e " ${C}auto-
|
|
483
|
+
echo -e " ${C}auto-review${N} ${G}on${N} (via Cursor Rule)"
|
|
476
484
|
fi
|
|
477
485
|
else
|
|
478
486
|
echo -e " ${G}${B}Done!${N} Restart Claude Code, then:"
|
|
479
487
|
echo ""
|
|
480
|
-
echo -e " ${C}/smoothie${N} ${D}<your problem>${N}
|
|
488
|
+
echo -e " ${C}/smoothie${N} ${D}<your problem>${N} review in Claude Code"
|
|
481
489
|
if [[ "$AUTO_BLEND" =~ ^[Yy]$ ]]; then
|
|
482
|
-
echo -e " ${C}auto-
|
|
490
|
+
echo -e " ${C}auto-review${N} ${G}on${N} for all plans"
|
|
483
491
|
fi
|
|
484
492
|
fi
|
|
485
493
|
echo -e " ${C}smoothie models${N} manage models"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "smoothie-code",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -10,9 +10,10 @@
|
|
|
10
10
|
"dist/",
|
|
11
11
|
"bin/",
|
|
12
12
|
"install.sh",
|
|
13
|
-
"auto-
|
|
13
|
+
"auto-review-hook.sh",
|
|
14
|
+
"gemini-review-hook.sh",
|
|
14
15
|
"plan-hook.sh",
|
|
15
|
-
"pr-
|
|
16
|
+
"pr-review-hook.sh",
|
|
16
17
|
"config.json",
|
|
17
18
|
"README.md"
|
|
18
19
|
],
|
package/plan-hook.sh
CHANGED
|
@@ -32,7 +32,7 @@ IS_PLAN=$(tail -c 3000 "$TRANSCRIPT" | grep -c "Would you like to proceed\|## Pl
|
|
|
32
32
|
|
|
33
33
|
if [ "$IS_PLAN" -gt 0 ]; then
|
|
34
34
|
echo ""
|
|
35
|
-
echo "🧃 Smoothie: type 'smoothie' in option 5 to
|
|
35
|
+
echo "🧃 Smoothie: type 'smoothie' in option 5 to review this plan before approving."
|
|
36
36
|
fi
|
|
37
37
|
|
|
38
38
|
exit 0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
#
|
|
3
|
-
# pr-
|
|
3
|
+
# pr-review-hook.sh — PreToolUse hook for Bash commands
|
|
4
4
|
#
|
|
5
5
|
# Intercepts `gh pr create` commands, runs Smoothie blend on the
|
|
6
6
|
# branch diff, and injects review results so Claude can revise
|
|
@@ -12,11 +12,11 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
12
12
|
# Read hook input from stdin
|
|
13
13
|
INPUT=$(cat)
|
|
14
14
|
|
|
15
|
-
# Check if auto-
|
|
15
|
+
# Check if auto-review is enabled in config
|
|
16
16
|
AUTO_ENABLED=$(node -e "
|
|
17
17
|
try {
|
|
18
18
|
const c = JSON.parse(require('fs').readFileSync('$SCRIPT_DIR/config.json','utf8'));
|
|
19
|
-
console.log(c.
|
|
19
|
+
console.log(c.auto_review === true ? 'true' : 'false');
|
|
20
20
|
} catch(e) { console.log('false'); }
|
|
21
21
|
" 2>/dev/null)
|
|
22
22
|
|
|
@@ -65,7 +65,7 @@ $DIFF
|
|
|
65
65
|
|
|
66
66
|
Provide concise, actionable feedback."
|
|
67
67
|
|
|
68
|
-
BLEND_RESULTS=$(echo "$REVIEW_PROMPT" | node "$SCRIPT_DIR/dist/
|
|
68
|
+
BLEND_RESULTS=$(echo "$REVIEW_PROMPT" | node "$SCRIPT_DIR/dist/review-cli.js" 2>/dev/stderr)
|
|
69
69
|
|
|
70
70
|
# Generate share link (metadata only, no raw content)
|
|
71
71
|
SHARE_URL=""
|