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 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 blended answer.
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>` — blend across all models, get one answer
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-blend** — plans and PRs reviewed automatically before you approve
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-blend hooks | ✓ | ✓ | ⚠ experimental | Rule-based |
30
+ | Auto-review hooks | ✓ | ✓ | ⚠ experimental | Rule-based |
31
31
  | `/smoothie` | ✓ | ✓ | — | — |
32
- | `smoothie blend` CLI | ✓ | ✓ | ✓ | ✓ |
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-blend
39
- smoothie blend "<prompt>" # run a blend
40
- smoothie blend --deep "..." # deep blend with full context
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-blend hook
53
- \-- MCP Server → smoothie_blend(prompt)
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 blended answer
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-blend-hook.sh — PreToolUse hook for ExitPlanMode
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-blend is enabled in config
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.auto_blend === true ? 'true' : 'false');
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/blend-cli.js" 2>/dev/stderr)
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-blend results — multiple models reviewed this plan:
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-blend completed',
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
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
3
- CONFIG="$SCRIPT_DIR/config.json"
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 "$SCRIPT_DIR/dist/select-models.js" "$@"
49
+ node "$INSTALL_DIR/dist/select-models.js" "$@"
9
50
  ;;
10
- blend)
51
+ review)
11
52
  shift
12
- node "$SCRIPT_DIR/dist/blend-cli.js" "$@"
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.auto_blend = true;
61
+ c.auto_review = true;
21
62
  fs.writeFileSync('$CONFIG', JSON.stringify(c, null, 2));
22
63
  "
23
- echo " ✓ Auto-blend enabled"
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.auto_blend = false;
70
+ c.auto_review = false;
30
71
  fs.writeFileSync('$CONFIG', JSON.stringify(c, null, 2));
31
72
  "
32
- echo " ✓ Auto-blend disabled"
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.auto_blend ? 'on' : 'off');
79
+ console.log(c.auto_review ? 'on' : 'off');
39
80
  } catch(e) { console.log('off'); }
40
81
  ")
41
- echo " Auto-blend is $ENABLED"
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="$SCRIPT_DIR/.smoothie-history.jsonl"
88
+ HISTORY="$SMOOTHIE_HOME/.smoothie-history.jsonl"
48
89
  if [ ! -f "$HISTORY" ]; then
49
- echo " No history yet. Run a blend first."
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: params });
102
- const req = https.request({
103
- hostname: 'api.smoothiecode.com',
104
- path: '/api/share',
105
- method: 'POST',
106
- headers: { 'Content-Type': 'application/json', 'Content-Length': postData.length },
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
- const shortUrl = JSON.parse(body).url;
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
- console.log(' https://smoothiecode.com/stats?' + params);
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 blends:');
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 "$SCRIPT_DIR/.last-blend.json" ]; then
171
+ if [ -f "$SMOOTHIE_HOME/.last-review.json" ]; then
147
172
  PARAMS=$(node -e "
148
- const r = JSON.parse(require('fs').readFileSync('$SCRIPT_DIR/.last-blend.json','utf8'));
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) + '&times=' + encodeURIComponent(times) + '&tokens=' + encodeURIComponent(tokens) + '&type=plan&suggestions=0');
153
178
  ")
154
- # Try to create short URL
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 blend found. Run a blend first."
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 blend"
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 blends this week yet.');
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 ' + r.total_blends + ' blends' + tw);
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
- help|--help|-h|"")
249
- echo ""
250
- echo " 🧃 Smoothie — multi-model review for Claude Code"
251
- echo ""
252
- echo " smoothie models Pick models interactively"
253
- echo " smoothie models list Show current models"
254
- echo " smoothie models add <id> Add by OpenRouter model ID"
255
- echo " smoothie models remove <id> Remove a model"
256
- echo ""
257
- echo " smoothie auto Show auto-blend status"
258
- echo " smoothie auto on Enable auto-blend for plans"
259
- echo " smoothie auto off Disable auto-blend"
260
- echo ""
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(PROJECT_ROOT, '.last-blend.json'), JSON.stringify({ results }, null, 2));
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(PROJECT_ROOT, '.smoothie-history.jsonl'), JSON.stringify(entry) + '\n');
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('smoothie_blend', {
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 blending...\n\n');
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(PROJECT_ROOT, '.last-blend.json'), JSON.stringify({ results }, null, 2));
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' : 'blend',
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(PROJECT_ROOT, '.smoothie-history.jsonl'), JSON.stringify(entry) + '\n');
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 blendId = `${cfg.github}-${Date.now()}`;
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
- blend_id: blendId,
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();
@@ -234,7 +234,7 @@ async function cmdPick(apiKey, configPath) {
234
234
  finalSelection.push(p);
235
235
  }
236
236
  }
237
- // Preserve existing config fields (like auto_blend)
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)+'&times='+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 blend.${N}"
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 — blend will use OpenRouter models only${N}"
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-blend ──────────────────────────────────────────────
159
+ # ─── Step 6: Auto-review ──────────────────────────────────────────────
160
160
  step "Configuring hooks"
161
161
 
162
- chmod +x "$SCRIPT_DIR/plan-hook.sh" "$SCRIPT_DIR/auto-blend-hook.sh" "$SCRIPT_DIR/pr-blend-hook.sh"
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-blend${N} reviews every plan with all models before"
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-blend? [y/N]: " AUTO_BLEND
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.auto_blend = true;
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-blend on"
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 `smoothie_blend` with the user's prompt. The MCP server queries all
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 `smoothie_blend` with a prompt asking models to review the diff for:
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-blend-hook.sh",
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-blend-hook'));
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-blend-hook.sh",
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 = "Blend this problem across multiple AI models. Gemini judges."
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 smoothie_blend with the problem text. Wait for results.
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 smoothie_blend asking models to review the diff for bugs, security, performance.
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-blend hook for Gemini CLI
391
- chmod +x "$SCRIPT_DIR/gemini-blend-hook.sh"
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-blend-hook.sh',
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-blend hook registered (BeforeTool/exit_plan_mode)"
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-blend (only if auto-blend is enabled)
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 `smoothie_blend` MCP tool with a description of what you're about to do.
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 blend for trivial changes (typos, single-line fixes, config tweaks).
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-blend)"
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 smoothie_blend via MCP${N}"
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-blend${N} ${G}on${N} (via Cursor Rule)"
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} blend in Claude Code"
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-blend${N} ${G}on${N} for all plans"
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": "1.2.0",
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-blend-hook.sh",
13
+ "auto-review-hook.sh",
14
+ "gemini-review-hook.sh",
14
15
  "plan-hook.sh",
15
- "pr-blend-hook.sh",
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 blend this plan before approving."
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-blend-hook.sh — PreToolUse hook for Bash commands
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-blend is enabled in config
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.auto_blend === true ? 'true' : 'false');
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/blend-cli.js" 2>/dev/stderr)
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=""