Best use case
Skill: recipe-scaler is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
## Overview
Teams using Skill: recipe-scaler should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/recipe-scaler/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How Skill: recipe-scaler Compares
| Feature / Agent | Skill: recipe-scaler | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
## Overview
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# Skill: recipe-scaler
## Overview
Full-stack recipe storage and scaling application. Users store recipes with ingredient quantities and serving counts. A scaling endpoint multiplies all quantities by a factor and applies smart unit conversion (e.g., 0.5 cup -> 8 tbsp). The frontend has a live-updating serving slider. Backend: Node.js/Express/SQLite. Frontend: React 18/Vite/TypeScript.
## Core Scaling Algorithm
```typescript
// apps/api/src/lib/recipe-scaler.ts
import { parseFraction } from './fraction-parser';
import { smartUnit, convertUnit } from './unit-converter';
import { formatQuantity } from './fraction-format';
interface Ingredient {
id: number;
quantity: number | null; // NULL = "to taste"
unit: string | null;
name: string;
preparation: string | null;
isOptional: boolean;
}
interface ScaledIngredient {
id: number;
name: string;
originalQuantity: number | null;
originalUnit: string | null;
scaledQuantity: number | null;
scaledUnit: string | null;
display: string;
preparation: string | null;
isOptional: boolean;
}
export function scaleIngredients(
ingredients: Ingredient[],
scaleFactor: number,
unitSystem: 'us' | 'metric' = 'us'
): ScaledIngredient[] {
return ingredients.map(ing => {
if (ing.quantity === null) {
return {
id: ing.id,
name: ing.name,
originalQuantity: null,
originalUnit: ing.unit,
scaledQuantity: null,
scaledUnit: ing.unit,
display: 'to taste',
preparation: ing.preparation,
isOptional: ing.isOptional,
};
}
const rawScaled = ing.quantity * scaleFactor;
// Apply smart unit conversion and system conversion
const { qty, unit } = smartUnit(rawScaled, ing.unit ?? '', unitSystem);
return {
id: ing.id,
name: ing.name,
originalQuantity: ing.quantity,
originalUnit: ing.unit,
scaledQuantity: qty,
scaledUnit: unit,
display: formatDisplay(qty, unit),
preparation: ing.preparation,
isOptional: ing.isOptional,
};
});
}
function formatDisplay(qty: number, unit: string | null): string {
const qtyStr = formatQuantity(qty);
if (!unit) return qtyStr;
// Pluralize unit for qty > 1
const unitStr = qty > 1 && !unit.endsWith('s') && !['g','kg','ml','l','oz','lb'].includes(unit)
? `${unit}s`
: unit;
return `${qtyStr} ${unitStr}`;
}
```
## Scale API Route
```typescript
// GET /api/recipes/:id/scale?servings=N&units=us|metric
router.get('/:id/scale', requireAuth, (req, res) => {
const recipe = db.prepare('SELECT * FROM recipes WHERE id = ? AND user_id = ?')
.get(req.params.id, req.session.userId);
if (!recipe) return res.status(404).json({ error: 'Not found' });
const targetServings = Math.max(1, Math.min(100, parseInt(req.query.servings as string) || recipe.servings));
const scaleFactor = targetServings / recipe.servings;
const unitSystem = req.query.units === 'metric' ? 'metric' : 'us';
const ingredients = db.prepare(
'SELECT * FROM ingredients WHERE recipe_id = ? ORDER BY sort_order'
).all(req.params.id);
const scaled = scaleIngredients(ingredients, scaleFactor, unitSystem);
res.json({
recipeId: recipe.id,
originalServings: recipe.servings,
targetServings,
scaleFactor,
ingredients: scaled,
});
});
```
## Frontend useScaler Hook
```typescript
// apps/web/src/hooks/useScaler.ts
import { useState, useCallback } from 'react';
import { useDebounce } from './useDebounce';
import { api } from '../lib/api';
interface ScaledIngredient {
id: number;
name: string;
scaledQuantity: number | null;
scaledUnit: string | null;
display: string;
preparation: string | null;
isOptional: boolean;
}
interface ScaleResult {
scaleFactor: number;
ingredients: ScaledIngredient[];
}
export function useScaler(recipeId: number, originalServings: number) {
const [servings, setServings] = useState(originalServings);
const [unitSystem, setUnitSystem] = useState<'us' | 'metric'>('us');
const [result, setResult] = useState<ScaleResult | null>(null);
const [loading, setLoading] = useState(false);
const debouncedServings = useDebounce(servings, 200);
const fetchScaled = useCallback(async (s: number, u: 'us' | 'metric') => {
setLoading(true);
try {
const data = await api.get(`/recipes/${recipeId}/scale?servings=${s}&units=${u}`);
setResult(data);
} finally {
setLoading(false);
}
}, [recipeId]);
// Fetch on debounced change
useState(() => {
fetchScaled(debouncedServings, unitSystem);
});
const reset = () => {
setServings(originalServings);
fetchScaled(originalServings, unitSystem);
};
return {
servings,
setServings,
unitSystem,
setUnitSystem,
result,
loading,
reset,
scaleFactor: result?.scaleFactor ?? 1,
};
}
```
## Fraction Format (frontend display)
```typescript
// apps/web/src/lib/fraction-format.ts
const FRACTIONS: [number, string][] = [
[1/8, '1/8'],
[1/6, '1/6'],
[1/4, '1/4'],
[1/3, '1/3'],
[3/8, '3/8'],
[1/2, '1/2'],
[5/8, '5/8'],
[2/3, '2/3'],
[3/4, '3/4'],
[5/6, '5/6'],
[7/8, '7/8'],
];
export function formatQuantity(value: number): string {
if (value === 0) return '0';
const whole = Math.floor(value);
const decimal = value - whole;
if (decimal < 0.01) return String(whole);
// Find closest fraction
let best: [number, string] | null = null;
let bestDiff = Infinity;
for (const [frac, str] of FRACTIONS) {
const diff = Math.abs(decimal - frac);
if (diff < bestDiff) {
bestDiff = diff;
best = [frac, str];
}
}
if (best && bestDiff < 0.04) {
const fracStr = best[1];
return whole > 0 ? `${whole} ${fracStr}` : fracStr;
}
// Fall back to 1 decimal place
return value.toFixed(1).replace(/\.0$/, '');
}
```
## Smart Unit Selection
```typescript
// apps/api/src/lib/unit-converter.ts
type UnitSystem = 'us' | 'metric';
interface UnitResult {
qty: number;
unit: string;
}
// Volume conversion in milliliters
const ML: Record<string, number> = {
tsp: 4.929,
tbsp: 14.787,
'fl oz': 29.574,
cup: 236.588,
pint: 473.176,
quart: 946.353,
liter: 1000,
ml: 1,
};
// Weight conversion in grams
const G: Record<string, number> = {
g: 1,
kg: 1000,
oz: 28.3495,
lb: 453.592,
};
export function convertUnit(qty: number, from: string, to: string): number {
if (from === to) return qty;
// Volume conversion
if (ML[from] && ML[to]) {
return qty * ML[from] / ML[to];
}
// Weight conversion
if (G[from] && G[to]) {
return qty * G[from] / G[to];
}
throw new Error(`Cannot convert ${from} to ${to}`);
}
export function smartUnit(qty: number, unit: string, system: UnitSystem = 'us'): UnitResult {
if (!unit || !ML[unit] && !G[unit]) {
// No unit (count items): round to nearest whole
return { qty: Math.round(qty * 4) / 4, unit: unit };
}
if (ML[unit]) {
return smartVolumeUnit(qty, unit, system);
}
if (G[unit]) {
return smartWeightUnit(qty, unit, system);
}
return { qty, unit };
}
function smartVolumeUnit(qty: number, unit: string, system: UnitSystem): UnitResult {
const totalMl = qty * ML[unit];
if (system === 'metric') {
if (totalMl < 5) return round({ qty: totalMl / ML['ml'], unit: 'ml' });
if (totalMl < 50) return round({ qty: totalMl / ML['tsp'], unit: 'tsp' });
if (totalMl < 200) return round({ qty: totalMl / ML['tbsp'], unit: 'tbsp' });
if (totalMl < 900) return round({ qty: totalMl / ML['cup'], unit: 'cup' });
return round({ qty: totalMl / ML['liter'], unit: 'L' });
}
// US customary
if (totalMl < 2.5) return round({ qty: totalMl / ML['tsp'], unit: 'tsp' });
if (totalMl < 14) return round({ qty: totalMl / ML['tsp'], unit: 'tsp' });
if (totalMl < 59) return round({ qty: totalMl / ML['tbsp'], unit: 'tbsp' });
if (totalMl < 400) return round({ qty: totalMl / ML['cup'], unit: 'cup' });
if (totalMl < 800) return round({ qty: totalMl / ML['pint'], unit: 'pint' });
return round({ qty: totalMl / ML['quart'], unit: 'quart' });
}
function smartWeightUnit(qty: number, unit: string, system: UnitSystem): UnitResult {
const totalG = qty * G[unit];
if (system === 'metric') {
if (totalG < 1000) return round({ qty: totalG, unit: 'g' });
return round({ qty: totalG / 1000, unit: 'kg' });
}
if (totalG < 28.35) return round({ qty: totalG, unit: 'g' });
if (totalG < 453.6) return round({ qty: totalG / G['oz'], unit: 'oz' });
return round({ qty: totalG / G['lb'], unit: 'lb' });
}
function round(r: UnitResult): UnitResult {
// Round to 3 significant figures
return { qty: parseFloat(r.qty.toPrecision(3)), unit: r.unit };
}
```
## pnpm Commands
```bash
pnpm install # install all workspace deps
pnpm dev # api (3000) + web (5173) concurrently
pnpm build # tsc + vite build
pnpm test # vitest
pnpm db:migrate # run SQL migrations
pnpm db:seed # seed sample recipes
```Related Skills
Skill: Uptime Monitoring
## Overview
Skill: Status Page
## Overview
Skill: unit-conversion
## Overview
reading-list
Operate the reading-list API to save, manage, tag, search, and export articles.
email-digest
Configure, test, and troubleshoot the reading-list daily email digest delivered via nodemailer.
websocket-realtime
Use the WebSocket connection in poll-builder to receive live vote updates. Use when you need to stream real-time poll results, monitor a poll for new votes, or build a live dashboard. Triggers include "live results", "real-time updates", "stream votes", "watch poll", or "WebSocket".
poll-builder
Self-hosted poll creation tool with real-time results. Use when you need to create a poll, check vote counts, close a poll, export results, or get the shareable link for a poll. Triggers include "create poll", "vote", "poll results", "survey", "collect votes", "share poll", or any task involving polling or voting.
Skill: personal-finance
## Overview
Skill: csv-import
## Overview
Skill: Syntax Highlighting
## Purpose
Skill: Pastebin Core
## Purpose
Skill: Cost Reporting
## Overview