use std::fs;
use std::path::PathBuf;
use vision_squeezer::{
OutputFormat, ProcessConfig, ProcessMode, VisionModel, encode_to_bytes, process,
token_savings_table,
};
fn print_usage() {
eprintln!("Usage: vision-squeezer <image> [options]");
eprintln!(" vision-squeezer stats (show cumulative savings)");
eprintln!(" vision-squeezer /vision-stats (alias for stats)");
eprintln!(" vision-squeezer setup-hook (print shell integration script)");
eprintln!("\nOptions:");
eprintln!(" --mode ocr|standard|auto (default: auto)");
eprintln!(" --format jpeg|webp (default: jpeg)");
eprintln!(" --quality 1-100 (default: 75)");
eprintln!(" --tile-size N (default: 512)");
eprintln!(" --no-crop");
eprintln!(" --bg-tolerance N (default: 15)");
eprintln!(" --model claude|gpt4o|gpt5|gemini model-aware resizing");
eprintln!(" --max-tiles N (limit maximum token tiles)");
eprintln!(" --output, -o <path> (custom output path)");
eprintln!(" --ops 'JSON' (Think in Code: list of atomic operations)");
eprintln!(
" ex: --ops '[{{\"op\":\"crop\",\"x\":0,\"y\":0,\"width\":100,\"height\":100}},{{\"op\":\"grayscale\"}}]'"
);
}
fn main() {
let args: Vec<String> = std::env::args().collect();
let _ = vision_squeezer::Persistence::init_db();
if matches!(
args.get(1).map(|s| s.as_str()),
Some("stats") | Some("/vision-stats")
) {
print_stats();
return;
}
if args.get(1).map(|s| s.as_str()) == Some("setup-hook") {
print_hook_script();
return;
}
if args.len() < 2 {
print_usage();
return;
}
let path = PathBuf::from(&args[1]);
let input_bytes = fs::metadata(&path).map(|m| m.len()).unwrap_or(0);
let img = image::open(&path).expect("failed to open image");
let (orig_w, orig_h) = (img.width(), img.height());
let mut cfg = ProcessConfig::builder();
let mut mode = ProcessMode::Auto;
let mut fmt = OutputFormat::Jpeg;
let mut custom_output: Option<PathBuf> = None;
let mut ops: Vec<vision_squeezer::ImageOp> = Vec::new();
let mut i = 2usize;
while i < args.len() {
match args[i].as_str() {
"--output" | "-o" => {
i += 1;
if let Some(p) = args.get(i) {
custom_output = Some(PathBuf::from(p));
}
}
"--mode" => {
i += 1;
match args.get(i).map(|s| s.as_str()) {
Some("ocr") => mode = ProcessMode::Ocr,
Some("standard") => mode = ProcessMode::Standard,
_ => mode = ProcessMode::Auto,
}
}
"--format" => {
i += 1;
if args.get(i).map(|s| s.as_str()) == Some("webp") {
fmt = OutputFormat::WebP;
}
}
"--quality" => {
i += 1;
if let Some(q) = args.get(i).and_then(|s| s.parse().ok()) {
cfg = cfg.quality(q);
}
}
"--tile-size" => {
i += 1;
if let Some(t) = args.get(i).and_then(|s| s.parse().ok()) {
cfg = cfg.tile_size(t);
}
}
"--max-tiles" => {
i += 1;
if let Some(m) = args.get(i).and_then(|s| s.parse().ok()) {
cfg = cfg.max_tiles(m);
}
}
"--no-crop" => {
cfg = cfg.crop(false);
}
"--bg-tolerance" => {
i += 1;
if let Some(t) = args.get(i).and_then(|s| s.parse().ok()) {
cfg = cfg.bg_tolerance(t);
}
}
"--model" => {
i += 1;
let m = match args.get(i).map(|s| s.as_str()) {
Some("gpt4o") | Some("gpt-4o") => Some(VisionModel::Gpt4o),
Some("gpt5") | Some("gpt-5") | Some("gpt5.5") => Some(VisionModel::Gpt5),
Some("gemini") => Some(VisionModel::Gemini15),
_ => Some(VisionModel::Claude),
};
if let Some(model) = m {
cfg = cfg.target_model(model);
}
}
"--ops" => {
i += 1;
if let Some(s) = args.get(i) {
let parsed: Vec<vision_squeezer::ImageOp> =
serde_json::from_str(s).expect("failed to parse --ops JSON");
ops.extend(parsed);
}
}
_ => {}
}
i += 1;
}
let cfg = cfg.output_format(fmt).build();
println!(
"Input: {}×{} ({:.1} MB)",
orig_w,
orig_h,
input_bytes as f64 / 1_048_576.0
);
let img = if !ops.is_empty() {
println!("Sandbox: Applying {} operations...", ops.len());
vision_squeezer::process_with_operations(img, ops)
} else {
img
};
let mut result = process(img, mode, input_bytes, &cfg);
let ext = match cfg.output_format {
OutputFormat::WebP => "webp",
OutputFormat::Jpeg => "jpg",
};
let out_path = custom_output.unwrap_or_else(|| path.with_extension(format!("optimized.{ext}")));
let bytes = encode_to_bytes(&result.image, &cfg).expect("encode failed");
let output_bytes = bytes.len() as u64;
fs::write(&out_path, &bytes).expect("write failed");
result.report.bytes_after = Some(output_bytes);
println!(
"Output: {}×{} ({:.1} MB, {} q{})",
result.width,
result.height,
output_bytes as f64 / 1_048_576.0,
ext.to_uppercase(),
cfg.quality,
);
if let Some(pct) = result.report.size_reduction_pct() {
println!("File: {:.1}% smaller", pct);
}
println!();
println!("── Token Estimates ─────────────────────────────────────────");
let table = token_savings_table(orig_w, orig_h, result.width, result.height);
table.print();
println!("────────────────────────────────────────────────────────────");
println!("→ {}", out_path.display());
let target_model_name = match cfg.target_model {
Some(VisionModel::Claude) => "Claude",
Some(VisionModel::Gpt4o) => "GPT-4o",
Some(VisionModel::Gpt5) => "GPT-5",
Some(VisionModel::Gemini15) => "Gemini",
None => "Agnostic",
};
let m = cfg.target_model.unwrap_or(VisionModel::Claude);
let orig_tokens = vision_squeezer::estimate_tokens(orig_w, orig_h, m).tokens;
let opt_tokens = vision_squeezer::estimate_tokens(result.width, result.height, m).tokens;
let _ = vision_squeezer::Persistence::log_optimization(
target_model_name,
orig_tokens,
opt_tokens,
input_bytes,
output_bytes,
&format!("{:?}", mode),
);
}
fn print_hook_script() {
println!(
r#"
# VisionSqueezer Shell Hook
# Add this to your .zshrc or .bashrc:
# eval "$(vision-squeezer setup-hook)"
# The 'squeeze' command: optimizes an image and returns the new path
squeeze() {{
if [ -z "$1" ]; then
echo "Usage: squeeze <file> [options]"
return 1
fi
local input="$1"
local output="${{input%.*}}.squeezed.${{input##*.}}"
vision-squeezer "$input" --output "$output" "${{@:2}}" > /dev/null
if [ -f "$output" ]; then
echo "$output"
else
echo "Error: Optimization failed"
return 1
fi
}}
# Aliases for quick analytics
alias vision-stats='vision-squeezer stats'
alias /vision-stats='vision-squeezer stats'
# Install /vision-stats Claude Code skill (zero-overhead stats — no MCP round-trip)
_vs_install_skill() {{
local skill_dir="$HOME/.claude/skills/vision-stats"
local skill_file="$skill_dir/SKILL.md"
local bin
bin="$(command -v vision-squeezer 2>/dev/null || echo 'vision-squeezer')"
if [ ! -f "$skill_file" ]; then
mkdir -p "$skill_dir"
cat > "$skill_file" << 'SKILL_EOF'
---
name: vision-stats
description: >
Show VisionSqueezer cumulative token & byte savings analytics. Zero MCP
overhead — reads directly from local stats.db via CLI binary. Use when user
says "vision-stats", "squeeze stats", "token savings", "how much saved",
"vision-squeezer stats", "optimization history", or "/vision-stats".
allowed-tools: Bash
---
# vision-stats — VisionSqueezer Analytics Skill
Zero-overhead stats. Calls `vision-squeezer stats` directly — no MCP round-trip.
## Trigger
`/vision-stats` or any of: "vision stats", "squeeze stats", "show savings", "how much have I saved", "optimization stats"
## Action
Run this binary resolution chain, stop at first success:
```bash
vision-squeezer stats 2>/dev/null \
|| ~/.cargo/bin/vision-squeezer stats 2>/dev/null \
|| "$(dirname "$(command -v vision-squeezer-mcp 2>/dev/null)")/vision-squeezer" stats 2>/dev/null \
|| find "$HOME/.cargo/bin" "$HOME/Desktop" "$HOME/Projects" -maxdepth 6 -name "vision-squeezer" -not -path "*/deps/*" -not -path "*/debug/*" 2>/dev/null | head -1 | xargs -I{{}} {{}} stats 2>/dev/null \
|| echo "vision-squeezer not found. Install: cargo install --git https://github.com/eralpozcan/vision-squeezer"
```
Print output verbatim. No wrapping, no commentary, no interpretation.
## Error handling
Binary not found → tell user to run `cargo install --path .` from project root or `eval "$(vision-squeezer setup-hook)"` after install.
## Notes
- Stats persist in local stats.db on the user's machine
- MCP tool `get_savings_stats` does the same but costs ~150 tokens overhead — use this skill instead
SKILL_EOF
echo "[vision-squeezer] /vision-stats skill installed → $skill_file"
fi
}}
_vs_install_skill
unset -f _vs_install_skill
# Install /vision-doctor Claude Code skill (version check + update guidance)
_vs_install_doctor_skill() {{
local skill_dir="$HOME/.claude/skills/vision-doctor"
local skill_file="$skill_dir/SKILL.md"
if [ ! -f "$skill_file" ]; then
mkdir -p "$skill_dir"
cat > "$skill_file" << 'SKILL_EOF'
---
name: vision-doctor
description: >
Check VisionSqueezer installation health and version status. Detects installed
version, compares against latest npm release, and shows update command if outdated.
Use when user says "vision-doctor", "check vision-squeezer version", "update vision-squeezer",
"is vision-squeezer up to date", "upgrade vision-squeezer", or "/vision-doctor".
allowed-tools: Bash
---
# vision-doctor — VisionSqueezer Health Check Skill
Checks binary installation, current version, and latest available version.
## Trigger
`/vision-doctor` or any of: "vision doctor", "check vision-squeezer", "update vision-squeezer",
"is vision-squeezer up to date", "upgrade vision-squeezer", "vision-squeezer version"
## Action
Run the following shell script and display the formatted checklist result:
```bash
BIN=$(command -v vision-squeezer 2>/dev/null || echo ~/.cargo/bin/vision-squeezer)
if [ -x "$BIN" ]; then
INSTALLED=$("$BIN" --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
else
INSTALLED=""
fi
LATEST=$(npm view vision-squeezer version 2>/dev/null)
echo "BIN=$BIN"
echo "INSTALLED=$INSTALLED"
echo "LATEST=$LATEST"
```
## Output format
Display as a markdown checklist based on the values:
```
## VisionSqueezer Doctor
- [x/] Binary found: <path or "not found">
- [x/] Installed version: <version or "unknown">
- [x/] Latest version (npm): <version or "unavailable">
- [x/] Status: Up to date / Update available / Not installed
```
Use `[x]` for OK/pass, `[ ]` for missing/fail.
### If update available (`INSTALLED` != `LATEST` and both non-empty):
Show update commands:
```
## Update available: v<INSTALLED> → v<LATEST>
Via cargo:
cargo install vision-squeezer
Via npm global:
npm install -g vision-squeezer
Via npx: no action needed — always pulls latest automatically.
```
### If not installed:
```
## VisionSqueezer not found
Install via Claude Code (one-liner):
claude mcp add vision-squeezer -- npx -y vision-squeezer
Or via cargo:
cargo install vision-squeezer
```
## Notes
- `npx -y vision-squeezer` users are always on latest — no update needed
- cargo install users must run `cargo install vision-squeezer` to upgrade
- npm global users run `npm install -g vision-squeezer`
SKILL_EOF
echo "[vision-squeezer] /vision-doctor skill installed → $skill_file"
fi
}}
_vs_install_doctor_skill
unset -f _vs_install_doctor_skill
"#
);
}
fn print_stats() {
match vision_squeezer::Persistence::get_stats() {
Ok(stats) => {
println!("\x1b[1m── VisionSqueezer Analytics ────────────────────────────────\x1b[0m");
println!("Total Optimizations: {}", stats.total_optimizations);
println!(
"Total Tokens Saved: \x1b[32m{}\x1b[0m",
stats.total_token_savings()
);
println!(
"Total Bytes Saved: \x1b[32m{:.2} MB\x1b[0m",
stats.total_byte_savings() as f64 / 1_048_576.0
);
println!(
"Estimated USD Saved: \x1b[35m${:.2}\x1b[0m",
stats.estimated_usd_saved()
);
println!("────────────────────────────────────────────────────────────");
if !stats.history.is_empty() {
println!("\x1b[2mLast 5 operations:\x1b[0m");
for (i, op) in stats.history.iter().take(5).enumerate() {
let date = op.timestamp.split('T').next().unwrap_or("");
println!(
"{}. {} | {:8} | {} → {} tokens",
i + 1,
date,
op.model,
op.original_tokens,
op.optimized_tokens
);
}
}
}
Err(e) => eprintln!("Error retrieving stats: {}", e),
}
}