smoothie-code 1.1.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -57
- package/auto-review-hook.sh +138 -0
- package/bin/smoothie +221 -16
- package/config.json +3 -1
- package/dist/blend-cli.js +69 -10
- package/dist/index.js +98 -14
- package/dist/review-cli.d.ts +12 -0
- package/dist/review-cli.js +244 -0
- package/dist/select-models.js +3 -1
- package/gemini-review-hook.sh +149 -0
- package/install.sh +142 -23
- package/package.json +12 -1
- package/plan-hook.sh +1 -1
- package/{pr-blend-hook.sh → pr-review-hook.sh} +43 -9
- package/auto-blend-hook.sh +0 -103
- package/banner-v2.svg +0 -307
- package/docs/banner.svg +0 -307
- package/docs/favicon.png +0 -0
- package/docs/favicon.svg +0 -17
- package/docs/index.html +0 -319
- package/icon.png +0 -0
- package/icon.svg +0 -17
- package/src/blend-cli.ts +0 -219
- package/src/index.ts +0 -367
- package/src/select-models.ts +0 -318
- package/tsconfig.json +0 -14
package/README.md
CHANGED
|
@@ -1,82 +1,64 @@
|
|
|
1
1
|
# Smoothie
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
<img src="banner-v2.svg" alt="Smoothie — multi-model review for Claude Code" width="100%">
|
|
5
|
-
</p>
|
|
3
|
+
Multi-model code review for AI coding agents. Query Codex, Gemini, Grok, DeepSeek and more in parallel — get one reviewed answer.
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
**Two model tracks:**
|
|
10
|
-
- **Codex** — Codex CLI, authenticated via ChatGPT account OAuth
|
|
11
|
-
- **OpenRouter** — single API key, models selected at install time from a live ranked list
|
|
5
|
+
**[Website](https://smoothiecode.com)** · **[Docs](https://smoothiecode.com/docs)** · **[Leaderboard](https://smoothiecode.com/leaderboard)** · **[@smoothie_code](https://x.com/smoothie_code)**
|
|
12
6
|
|
|
13
7
|
## Install
|
|
14
8
|
|
|
15
9
|
```bash
|
|
16
|
-
|
|
10
|
+
npx smoothie-code
|
|
17
11
|
```
|
|
18
12
|
|
|
19
|
-
|
|
13
|
+
Works with **Claude Code**, **Gemini CLI**, **Codex CLI**, and **Cursor**.
|
|
20
14
|
|
|
21
|
-
|
|
15
|
+
## Features
|
|
22
16
|
|
|
23
|
-
|
|
17
|
+
- `/smoothie <problem>` — review across all models, get one answer
|
|
18
|
+
- `/smoothie-pr` — multi-model PR review
|
|
19
|
+
- `/smoothie --deep` — full context mode with cost estimate
|
|
20
|
+
- **Auto-review** — plans and PRs reviewed automatically before you approve
|
|
21
|
+
- **Leaderboard** — weekly token rankings at [smoothiecode.com/leaderboard](https://smoothiecode.com/leaderboard)
|
|
22
|
+
- **Stats & sharing** — `smoothie stats`, `smoothie share`
|
|
24
23
|
|
|
25
|
-
|
|
26
|
-
```
|
|
27
|
-
/smoothie <your problem or question>
|
|
28
|
-
```
|
|
24
|
+
## Platform support
|
|
29
25
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
26
|
+
| Feature | Claude Code | Gemini CLI | Codex CLI | Cursor |
|
|
27
|
+
|---------|------------|-----------|-----------|--------|
|
|
28
|
+
| MCP server | ✓ | ✓ | ✓ (STDIO) | ✓ |
|
|
29
|
+
| Slash commands | ✓ | ✓ | — | — |
|
|
30
|
+
| Auto-review hooks | ✓ | ✓ | ⚠ experimental | Rule-based |
|
|
31
|
+
| `/smoothie` | ✓ | ✓ | — | — |
|
|
32
|
+
| `smoothie review` CLI | ✓ | ✓ | ✓ | ✓ |
|
|
37
33
|
|
|
38
|
-
|
|
34
|
+
## CLI
|
|
39
35
|
|
|
40
|
-
### Manage models
|
|
41
36
|
```bash
|
|
42
|
-
smoothie models #
|
|
43
|
-
smoothie
|
|
44
|
-
smoothie
|
|
45
|
-
smoothie
|
|
37
|
+
smoothie models # pick models
|
|
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
|
+
smoothie stats # usage stats
|
|
42
|
+
smoothie share # share last report
|
|
43
|
+
smoothie leaderboard # view rankings
|
|
44
|
+
smoothie help # all commands
|
|
46
45
|
```
|
|
47
|
-
No restart needed — config is read fresh on each blend.
|
|
48
46
|
|
|
49
47
|
## How it works
|
|
50
48
|
|
|
51
49
|
```
|
|
52
|
-
Claude
|
|
50
|
+
Your IDE (Claude/Gemini/Cursor)
|
|
53
51
|
|
|
|
54
|
-
|-- /smoothie
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|-- Streams live progress to terminal
|
|
60
|
-
\-- Returns all responses to Claude
|
|
52
|
+
|-- /smoothie or auto-review hook
|
|
53
|
+
\-- MCP Server → smoothie_review(prompt)
|
|
54
|
+
|-- Queries all models in parallel
|
|
55
|
+
|-- Returns responses to the judge AI
|
|
56
|
+
\-- Judge gives you one reviewed answer
|
|
61
57
|
```
|
|
62
58
|
|
|
63
|
-
|
|
64
|
-
```
|
|
65
|
-
Claude presents plan → ExitPlanMode hook fires → Smoothie blend runs
|
|
66
|
-
→ Results injected as context → Claude revises plan → You approve
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
Claude acts as judge. Raw model outputs are never shown. Claude absorbs everything and hands you one result.
|
|
70
|
-
|
|
71
|
-
## File overview
|
|
59
|
+
## Links
|
|
72
60
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
| `src/select-models.ts` | Interactive model picker (OpenRouter API) |
|
|
78
|
-
| `auto-blend-hook.sh` | PreToolUse hook — auto-blends plans |
|
|
79
|
-
| `plan-hook.sh` | Stop hook — plan mode hint (fallback) |
|
|
80
|
-
| `install.sh` | One-command installer |
|
|
81
|
-
| `config.json` | Model selection + auto_blend flag |
|
|
82
|
-
| `.env` | API keys (gitignored) |
|
|
61
|
+
- [Documentation](https://smoothiecode.com/docs)
|
|
62
|
+
- [Leaderboard](https://smoothiecode.com/leaderboard)
|
|
63
|
+
- [npm](https://www.npmjs.com/package/smoothie-code)
|
|
64
|
+
- [OpenRouter Usage](https://openrouter.ai/apps?url=https%3A%2F%2Fsmoothiecode.com)
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
#
|
|
3
|
+
# auto-review-hook.sh — PreToolUse hook for ExitPlanMode
|
|
4
|
+
#
|
|
5
|
+
# Intercepts plan approval, runs Smoothie blend on the plan,
|
|
6
|
+
# and injects results back so Claude revises before you see it.
|
|
7
|
+
#
|
|
8
|
+
|
|
9
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
10
|
+
|
|
11
|
+
# Read hook input from stdin
|
|
12
|
+
INPUT=$(cat)
|
|
13
|
+
|
|
14
|
+
# Check if auto-review is enabled in config
|
|
15
|
+
if [ -f "$SCRIPT_DIR/config.json" ]; then
|
|
16
|
+
AUTO_ENABLED=$(node -e "
|
|
17
|
+
try {
|
|
18
|
+
const c = JSON.parse(require('fs').readFileSync('$SCRIPT_DIR/config.json','utf8'));
|
|
19
|
+
console.log(c.auto_review === true ? 'true' : 'false');
|
|
20
|
+
} catch(e) { console.log('false'); }
|
|
21
|
+
" 2>/dev/null)
|
|
22
|
+
|
|
23
|
+
if [ "$AUTO_ENABLED" != "true" ]; then
|
|
24
|
+
# Auto-blend disabled — allow ExitPlanMode without intervention
|
|
25
|
+
echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow"}}'
|
|
26
|
+
exit 0
|
|
27
|
+
fi
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
# Extract transcript path
|
|
31
|
+
TRANSCRIPT_PATH=$(echo "$INPUT" | python3 -c "
|
|
32
|
+
import sys, json
|
|
33
|
+
try:
|
|
34
|
+
d = json.load(sys.stdin)
|
|
35
|
+
print(d.get('transcript_path', ''))
|
|
36
|
+
except:
|
|
37
|
+
print('')
|
|
38
|
+
" 2>/dev/null)
|
|
39
|
+
|
|
40
|
+
if [ -z "$TRANSCRIPT_PATH" ] || [ ! -f "$TRANSCRIPT_PATH" ]; then
|
|
41
|
+
echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow"}}'
|
|
42
|
+
exit 0
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
# Extract the plan from the last ~4000 chars of the transcript
|
|
46
|
+
PLAN_CONTEXT=$(tail -c 4000 "$TRANSCRIPT_PATH" 2>/dev/null)
|
|
47
|
+
|
|
48
|
+
if [ -z "$PLAN_CONTEXT" ]; then
|
|
49
|
+
echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow"}}'
|
|
50
|
+
exit 0
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
# Build the review prompt
|
|
54
|
+
REVIEW_PROMPT="You are reviewing a plan that Claude Code generated. Analyze it for:
|
|
55
|
+
- Missing steps or edge cases
|
|
56
|
+
- Better approaches or optimizations
|
|
57
|
+
- Potential bugs or issues
|
|
58
|
+
- Security concerns
|
|
59
|
+
|
|
60
|
+
Here is the plan context (from the conversation transcript):
|
|
61
|
+
|
|
62
|
+
$PLAN_CONTEXT
|
|
63
|
+
|
|
64
|
+
Provide concise, actionable feedback. Focus only on things that should change."
|
|
65
|
+
|
|
66
|
+
# Run the blend (progress shows on stderr, results on stdout)
|
|
67
|
+
BLEND_RESULTS=$(echo "$REVIEW_PROMPT" | node "$SCRIPT_DIR/dist/review-cli.js" 2>/dev/stderr)
|
|
68
|
+
|
|
69
|
+
# Generate share link (metadata only, no raw content)
|
|
70
|
+
SHARE_URL=""
|
|
71
|
+
SHARE_PARAMS=$(echo "$BLEND_RESULTS" | node -e "
|
|
72
|
+
const fs=require('fs');
|
|
73
|
+
let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{
|
|
74
|
+
try {
|
|
75
|
+
const r=JSON.parse(d);
|
|
76
|
+
const models=r.results.map(m=>m.model).join(',');
|
|
77
|
+
const times=r.results.map(m=>m.elapsed_s||0).join(',');
|
|
78
|
+
const tokens=r.results.map(m=>(m.tokens&&m.tokens.total)||0).join(',');
|
|
79
|
+
const responded=r.results.filter(m=>!m.response.startsWith('Error:')&&m.response!=='No response content'&&m.response!=='(empty response)').length;
|
|
80
|
+
let github='',judge='Claude Code';
|
|
81
|
+
try{const c=JSON.parse(fs.readFileSync('$SCRIPT_DIR/config.json','utf8'));github=c.github||'';const p=process.env.SMOOTHIE_PLATFORM||'claude';judge={claude:'Claude Code',gemini:'Gemini CLI',codex:'Codex CLI',cursor:'Cursor'}[p]||'Claude Code';}catch{}
|
|
82
|
+
let params='models='+encodeURIComponent(models)+'×='+encodeURIComponent(times)+'&tokens='+encodeURIComponent(tokens)+'&type=plan&suggestions='+responded+'&judge='+encodeURIComponent(judge);
|
|
83
|
+
if(github)params+='&user='+encodeURIComponent(github);
|
|
84
|
+
console.log(params);
|
|
85
|
+
} catch { console.log(''); }
|
|
86
|
+
});
|
|
87
|
+
" 2>/dev/null)
|
|
88
|
+
|
|
89
|
+
if [ -n "$SHARE_PARAMS" ]; then
|
|
90
|
+
SHARE_URL=$(curl -s --max-time 5 --connect-timeout 3 -X POST "https://api.smoothiecode.com/api/share" \
|
|
91
|
+
-H "Content-Type: application/json" \
|
|
92
|
+
-d "{\"type\":\"report\",\"data\":\"$SHARE_PARAMS\"}" 2>/dev/null | node -e "
|
|
93
|
+
let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{
|
|
94
|
+
try{console.log(JSON.parse(d).url)}catch{console.log('')}
|
|
95
|
+
});
|
|
96
|
+
" 2>/dev/null) || true
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
if [ -z "$BLEND_RESULTS" ]; then
|
|
100
|
+
echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow"}}'
|
|
101
|
+
exit 0
|
|
102
|
+
fi
|
|
103
|
+
|
|
104
|
+
# Build the additionalContext string
|
|
105
|
+
CONTEXT="🧃 Smoothie auto-review results — multiple models reviewed this plan:
|
|
106
|
+
|
|
107
|
+
$BLEND_RESULTS
|
|
108
|
+
|
|
109
|
+
IMPORTANT — You must do TWO things:
|
|
110
|
+
|
|
111
|
+
1. FIRST, show a Smoothie report table. Use elapsed_s and tokens from each result:
|
|
112
|
+
|
|
113
|
+
🧃 Smoothie reviewed this plan (N models, Xs total):
|
|
114
|
+
|
|
115
|
+
| Model | Time | Tokens | Verdict | Feedback |
|
|
116
|
+
|-------|------|--------|---------|----------|
|
|
117
|
+
| ModelName | 3.1s | 1,847 | useful | one-line summary |
|
|
118
|
+
| ModelName | 8.2s | 2,104 | generic | one-line summary |
|
|
119
|
+
(skip errored/empty models. Verdict = useful, generic, or irrelevant)
|
|
120
|
+
$([ -n "$SHARE_URL" ] && echo "
|
|
121
|
+
Share this report: $SHARE_URL")
|
|
122
|
+
|
|
123
|
+
2. THEN proceed with implementation as planned, incorporating valid Smoothie suggestions as you go. Do NOT stop to revise — start coding immediately."
|
|
124
|
+
|
|
125
|
+
# Return: allow ExitPlanMode but inject blend results
|
|
126
|
+
node -e "
|
|
127
|
+
const ctx = $(echo "$CONTEXT" | python3 -c "import sys,json; print(json.dumps(sys.stdin.read()))" 2>/dev/null);
|
|
128
|
+
console.log(JSON.stringify({
|
|
129
|
+
hookSpecificOutput: {
|
|
130
|
+
hookEventName: 'PreToolUse',
|
|
131
|
+
permissionDecision: 'allow',
|
|
132
|
+
permissionDecisionReason: 'Smoothie auto-review completed',
|
|
133
|
+
additionalContext: ctx
|
|
134
|
+
}
|
|
135
|
+
}));
|
|
136
|
+
"
|
|
137
|
+
|
|
138
|
+
exit 0
|
package/bin/smoothie
CHANGED
|
@@ -1,15 +1,27 @@
|
|
|
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"
|
|
4
16
|
|
|
5
17
|
case "$1" in
|
|
6
18
|
models)
|
|
7
19
|
shift
|
|
8
|
-
node "$
|
|
20
|
+
node "$INSTALL_DIR/dist/select-models.js" "$@"
|
|
9
21
|
;;
|
|
10
|
-
|
|
22
|
+
review)
|
|
11
23
|
shift
|
|
12
|
-
node "$
|
|
24
|
+
node "$INSTALL_DIR/dist/review-cli.js" "$@"
|
|
13
25
|
;;
|
|
14
26
|
auto)
|
|
15
27
|
case "$2" in
|
|
@@ -17,32 +29,216 @@ case "$1" in
|
|
|
17
29
|
node -e "
|
|
18
30
|
const fs = require('fs');
|
|
19
31
|
const c = JSON.parse(fs.readFileSync('$CONFIG','utf8'));
|
|
20
|
-
c.
|
|
32
|
+
c.auto_review = true;
|
|
21
33
|
fs.writeFileSync('$CONFIG', JSON.stringify(c, null, 2));
|
|
22
34
|
"
|
|
23
|
-
echo " ✓ Auto-
|
|
35
|
+
echo " ✓ Auto-review enabled"
|
|
24
36
|
;;
|
|
25
37
|
off)
|
|
26
38
|
node -e "
|
|
27
39
|
const fs = require('fs');
|
|
28
40
|
const c = JSON.parse(fs.readFileSync('$CONFIG','utf8'));
|
|
29
|
-
c.
|
|
41
|
+
c.auto_review = false;
|
|
30
42
|
fs.writeFileSync('$CONFIG', JSON.stringify(c, null, 2));
|
|
31
43
|
"
|
|
32
|
-
echo " ✓ Auto-
|
|
44
|
+
echo " ✓ Auto-review disabled"
|
|
33
45
|
;;
|
|
34
46
|
*)
|
|
35
47
|
ENABLED=$(node -e "
|
|
36
48
|
try {
|
|
37
49
|
const c = JSON.parse(require('fs').readFileSync('$CONFIG','utf8'));
|
|
38
|
-
console.log(c.
|
|
50
|
+
console.log(c.auto_review ? 'on' : 'off');
|
|
39
51
|
} catch(e) { console.log('off'); }
|
|
40
52
|
")
|
|
41
|
-
echo " Auto-
|
|
53
|
+
echo " Auto-review is $ENABLED"
|
|
42
54
|
echo " Usage: smoothie auto on|off"
|
|
43
55
|
;;
|
|
44
56
|
esac
|
|
45
57
|
;;
|
|
58
|
+
stats)
|
|
59
|
+
HISTORY="$SMOOTHIE_HOME/.smoothie-history.jsonl"
|
|
60
|
+
if [ ! -f "$HISTORY" ]; then
|
|
61
|
+
echo " No history yet. Run a review first."
|
|
62
|
+
exit 0
|
|
63
|
+
fi
|
|
64
|
+
node -e "
|
|
65
|
+
const fs = require('fs');
|
|
66
|
+
const lines = fs.readFileSync('$HISTORY', 'utf8').trim().split('\n').map(l => JSON.parse(l));
|
|
67
|
+
|
|
68
|
+
const totalBlends = lines.length;
|
|
69
|
+
const totalTokens = lines.reduce((s, e) => s + e.models.reduce((s2, m) => s2 + (m.tokens?.total || 0), 0), 0);
|
|
70
|
+
const totalTime = lines.reduce((s, e) => s + Math.max(...e.models.map(m => m.elapsed_s || 0)), 0);
|
|
71
|
+
const errors = lines.reduce((s, e) => s + e.models.filter(m => m.error).length, 0);
|
|
72
|
+
const successes = lines.reduce((s, e) => s + e.models.filter(m => !m.error).length, 0);
|
|
73
|
+
|
|
74
|
+
const modelStats = {};
|
|
75
|
+
for (const entry of lines) {
|
|
76
|
+
for (const m of entry.models) {
|
|
77
|
+
if (!modelStats[m.model]) modelStats[m.model] = { calls: 0, tokens: 0, errors: 0, totalTime: 0 };
|
|
78
|
+
modelStats[m.model].calls++;
|
|
79
|
+
modelStats[m.model].tokens += m.tokens?.total || 0;
|
|
80
|
+
modelStats[m.model].totalTime += m.elapsed_s || 0;
|
|
81
|
+
if (m.error) modelStats[m.model].errors++;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const sorted = Object.entries(modelStats).sort((a, b) => b[1].calls - a[1].calls);
|
|
86
|
+
|
|
87
|
+
if ('$2' === '--share') {
|
|
88
|
+
const rate = (successes / (successes + errors) * 100).toFixed(0);
|
|
89
|
+
const names = sorted.map(s => s[0]).join(',');
|
|
90
|
+
const mt = sorted.map(s => s[1].tokens).join(',');
|
|
91
|
+
const mc = sorted.map(s => s[1].calls).join(',');
|
|
92
|
+
const ma = sorted.map(s => (s[1].totalTime / s[1].calls).toFixed(1)).join(',');
|
|
93
|
+
|
|
94
|
+
const https = require('https');
|
|
95
|
+
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) });
|
|
96
|
+
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);
|
|
97
|
+
|
|
98
|
+
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 => {
|
|
99
|
+
let d = '';
|
|
100
|
+
res.on('data', c => d += c);
|
|
101
|
+
res.on('end', () => {
|
|
102
|
+
try { console.log(' ' + JSON.parse(d).url); }
|
|
103
|
+
catch { console.log(' ' + longUrl); }
|
|
104
|
+
console.log('');
|
|
105
|
+
console.log(' Open in browser to save as image or copy tweet');
|
|
106
|
+
console.log('');
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
req.on('error', () => { console.log(' ' + longUrl); console.log(''); });
|
|
110
|
+
req.on('timeout', () => { req.destroy(); console.log(' ' + longUrl); console.log(''); });
|
|
111
|
+
req.write(postData);
|
|
112
|
+
req.end();
|
|
113
|
+
} else {
|
|
114
|
+
console.log('');
|
|
115
|
+
console.log(' 🧃 Smoothie Stats');
|
|
116
|
+
console.log(' ─────────────────────────────────────────');
|
|
117
|
+
console.log(' Reviews: ' + totalBlends + ' Tokens: ' + totalTokens.toLocaleString() + ' Time: ' + totalTime.toFixed(0) + 's');
|
|
118
|
+
console.log(' Success rate: ' + successes + '/' + (successes + errors) + ' (' + (successes / (successes + errors) * 100).toFixed(0) + '%)');
|
|
119
|
+
console.log('');
|
|
120
|
+
console.log(' Model Calls Tokens Avg time Errors');
|
|
121
|
+
console.log(' ─────────────────────────────────────────────────────────────────');
|
|
122
|
+
for (const [name, s] of sorted) {
|
|
123
|
+
const avg = (s.totalTime / s.calls).toFixed(1);
|
|
124
|
+
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));
|
|
125
|
+
}
|
|
126
|
+
console.log('');
|
|
127
|
+
|
|
128
|
+
const recent = lines.slice(-5).reverse();
|
|
129
|
+
console.log(' Recent reviews:');
|
|
130
|
+
for (const e of recent) {
|
|
131
|
+
const models = e.models.filter(m => !m.error).map(m => m.model).join(', ');
|
|
132
|
+
const time = Math.max(...e.models.map(m => m.elapsed_s || 0)).toFixed(1);
|
|
133
|
+
const date = new Date(e.ts).toLocaleDateString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
|
|
134
|
+
console.log(' ' + date + ' ' + e.type.padEnd(6) + time + 's ' + models);
|
|
135
|
+
}
|
|
136
|
+
console.log('');
|
|
137
|
+
}
|
|
138
|
+
"
|
|
139
|
+
;;
|
|
140
|
+
share)
|
|
141
|
+
shift
|
|
142
|
+
if [ -f "$SMOOTHIE_HOME/.last-review.json" ]; then
|
|
143
|
+
PARAMS=$(node -e "
|
|
144
|
+
const r = JSON.parse(require('fs').readFileSync('$SMOOTHIE_HOME/.last-review.json','utf8'));
|
|
145
|
+
const models = r.results.map(m => m.model).join(',');
|
|
146
|
+
const times = r.results.map(m => m.elapsed_s || 0).join(',');
|
|
147
|
+
const tokens = r.results.map(m => m.tokens?.total || 0).join(',');
|
|
148
|
+
console.log('models=' + encodeURIComponent(models) + '×=' + encodeURIComponent(times) + '&tokens=' + encodeURIComponent(tokens) + '&type=plan&suggestions=0');
|
|
149
|
+
")
|
|
150
|
+
SHORT=$(curl -s --max-time 5 --connect-timeout 3 -X POST "https://api.smoothiecode.com/api/share" \
|
|
151
|
+
-H "Content-Type: application/json" \
|
|
152
|
+
-d "{\"type\":\"report\",\"data\":\"$PARAMS\"}" 2>/dev/null | node -e "
|
|
153
|
+
let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{
|
|
154
|
+
try{console.log(JSON.parse(d).url)}catch{console.log('')}
|
|
155
|
+
})
|
|
156
|
+
" 2>/dev/null)
|
|
157
|
+
if [ -n "$SHORT" ]; then
|
|
158
|
+
echo " $SHORT"
|
|
159
|
+
else
|
|
160
|
+
echo " https://smoothiecode.com/report?$PARAMS"
|
|
161
|
+
fi
|
|
162
|
+
echo ""
|
|
163
|
+
echo " Open in browser or copy to share"
|
|
164
|
+
else
|
|
165
|
+
echo " No recent review found. Run a review first."
|
|
166
|
+
fi
|
|
167
|
+
;;
|
|
168
|
+
leaderboard)
|
|
169
|
+
LEADERBOARD_API="https://api.smoothiecode.com"
|
|
170
|
+
case "$2" in
|
|
171
|
+
join)
|
|
172
|
+
GITHUB=$(git config user.name 2>/dev/null || echo "")
|
|
173
|
+
if [ -z "$GITHUB" ]; then
|
|
174
|
+
read -p " GitHub username: " GITHUB
|
|
175
|
+
else
|
|
176
|
+
read -p " GitHub username [$GITHUB]: " INPUT_GH
|
|
177
|
+
GITHUB="${INPUT_GH:-$GITHUB}"
|
|
178
|
+
fi
|
|
179
|
+
node -e "
|
|
180
|
+
const fs = require('fs');
|
|
181
|
+
const c = JSON.parse(fs.readFileSync('$CONFIG','utf8'));
|
|
182
|
+
c.leaderboard = true;
|
|
183
|
+
c.github = '$GITHUB';
|
|
184
|
+
fs.writeFileSync('$CONFIG', JSON.stringify(c, null, 2));
|
|
185
|
+
"
|
|
186
|
+
echo " ✓ Joined as $GITHUB"
|
|
187
|
+
echo " Your stats will appear on the leaderboard after your next review"
|
|
188
|
+
;;
|
|
189
|
+
leave)
|
|
190
|
+
node -e "
|
|
191
|
+
const fs = require('fs');
|
|
192
|
+
const c = JSON.parse(fs.readFileSync('$CONFIG','utf8'));
|
|
193
|
+
const github = c.github || '';
|
|
194
|
+
c.leaderboard = false;
|
|
195
|
+
fs.writeFileSync('$CONFIG', JSON.stringify(c, null, 2));
|
|
196
|
+
console.log(github);
|
|
197
|
+
" | xargs -I{} curl -s -X DELETE "$LEADERBOARD_API/api/user/{}" > /dev/null 2>&1
|
|
198
|
+
echo " ✓ Left the leaderboard (remote data deleted)"
|
|
199
|
+
;;
|
|
200
|
+
profile)
|
|
201
|
+
read -p " Twitter handle (e.g. @hotairbag): " TWITTER
|
|
202
|
+
GITHUB=$(node -e "const c=JSON.parse(require('fs').readFileSync('$CONFIG','utf8'));console.log(c.github||'')")
|
|
203
|
+
if [ -z "$GITHUB" ]; then
|
|
204
|
+
echo " Join the leaderboard first: smoothie leaderboard join"
|
|
205
|
+
else
|
|
206
|
+
curl -s -X POST "$LEADERBOARD_API/api/profile" \
|
|
207
|
+
-H "Content-Type: application/json" \
|
|
208
|
+
-d "{\"github\":\"$GITHUB\",\"twitter\":\"$TWITTER\"}" > /dev/null 2>&1
|
|
209
|
+
echo " ✓ Twitter set to $TWITTER"
|
|
210
|
+
fi
|
|
211
|
+
;;
|
|
212
|
+
*)
|
|
213
|
+
RESULT=$(curl -s "$LEADERBOARD_API/api/leaderboard" 2>/dev/null)
|
|
214
|
+
if [ -z "$RESULT" ]; then
|
|
215
|
+
echo " Could not reach leaderboard. Check your connection."
|
|
216
|
+
else
|
|
217
|
+
node -e "
|
|
218
|
+
const d = JSON.parse(\`$RESULT\`);
|
|
219
|
+
if (!d.rankings || d.rankings.length === 0) {
|
|
220
|
+
console.log(' No reviews this week yet.');
|
|
221
|
+
process.exit(0);
|
|
222
|
+
}
|
|
223
|
+
console.log('');
|
|
224
|
+
console.log(' 🧃 Smoothie Leaderboard — ' + d.week);
|
|
225
|
+
console.log(' ──────────────────────────────────────────');
|
|
226
|
+
const medals = ['🥇','🥈','🥉'];
|
|
227
|
+
for (let i = 0; i < d.rankings.length && i < 20; i++) {
|
|
228
|
+
const r = d.rankings[i];
|
|
229
|
+
const medal = medals[i] || ' ';
|
|
230
|
+
const tokens = r.total_tokens >= 1000 ? (r.total_tokens/1000).toFixed(1)+'k' : r.total_tokens;
|
|
231
|
+
const tw = r.twitter ? ' @'+r.twitter : '';
|
|
232
|
+
console.log(' ' + medal + ' #' + (i+1) + ' ' + r.github.padEnd(20) + tokens.padStart(8) + ' tokens' + tw);
|
|
233
|
+
}
|
|
234
|
+
console.log('');
|
|
235
|
+
console.log(' https://smoothiecode.com/leaderboard');
|
|
236
|
+
console.log('');
|
|
237
|
+
"
|
|
238
|
+
fi
|
|
239
|
+
;;
|
|
240
|
+
esac
|
|
241
|
+
;;
|
|
46
242
|
help|--help|-h|"")
|
|
47
243
|
echo ""
|
|
48
244
|
echo " 🧃 Smoothie — multi-model review for Claude Code"
|
|
@@ -52,12 +248,21 @@ case "$1" in
|
|
|
52
248
|
echo " smoothie models add <id> Add by OpenRouter model ID"
|
|
53
249
|
echo " smoothie models remove <id> Remove a model"
|
|
54
250
|
echo ""
|
|
55
|
-
echo " smoothie auto Show auto-
|
|
56
|
-
echo " smoothie auto on Enable auto-
|
|
57
|
-
echo " smoothie auto off Disable auto-
|
|
251
|
+
echo " smoothie auto Show auto-review status"
|
|
252
|
+
echo " smoothie auto on Enable auto-review for plans"
|
|
253
|
+
echo " smoothie auto off Disable auto-review"
|
|
254
|
+
echo ""
|
|
255
|
+
echo " smoothie review \"<prompt>\" Run a review from terminal"
|
|
256
|
+
echo " smoothie review --deep \"<prompt>\" Deep review with full context"
|
|
257
|
+
echo ""
|
|
258
|
+
echo " smoothie stats Usage stats and history"
|
|
259
|
+
echo " smoothie stats --share Shareable stats page"
|
|
260
|
+
echo " smoothie share Share last review report"
|
|
58
261
|
echo ""
|
|
59
|
-
echo " smoothie
|
|
60
|
-
echo " smoothie
|
|
262
|
+
echo " smoothie leaderboard View weekly rankings"
|
|
263
|
+
echo " smoothie leaderboard join Join the leaderboard"
|
|
264
|
+
echo " smoothie leaderboard leave Leave and delete data"
|
|
265
|
+
echo " smoothie leaderboard profile Set Twitter handle"
|
|
61
266
|
echo ""
|
|
62
267
|
;;
|
|
63
268
|
*)
|