Back to skills
SkillHub ClubResearch & OpsFull StackBackend

mealie

Interact with Mealie recipe manager (recipes, shopping lists, meal plans). Self-hosted recipe and meal planning API client.

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
3,111
Hot score
99
Updated
March 20, 2026
Overall rating
C0.0
Composite score
0.0
Best-practice grade
B80.4

Install command

npx @skill-hub/cli install openclaw-skills-mealie-api

Repository

openclaw/skills

Skill path: skills/angusthefuzz/mealie-api

Interact with Mealie recipe manager (recipes, shopping lists, meal plans). Self-hosted recipe and meal planning API client.

Open repository

Best for

Primary workflow: Research & Ops.

Technical facets: Full Stack, Backend.

Target audience: everyone.

License: Unknown.

Original source

Catalog source: SkillHub Club.

Repository owner: openclaw.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install mealie into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://github.com/openclaw/skills before adding mealie to shared team environments
  • Use mealie for development workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 0.

Sub-skills: 0.

Aggregator: No.

Original source / Raw SKILL.md

---
name: mealie
description: Interact with Mealie recipe manager (recipes, shopping lists, meal plans). Self-hosted recipe and meal planning API client.
metadata:
  openclaw:
    emoji: 🍳
    requires:
      bins: [node]
      env: [MEALIE_URL, MEALIE_API_TOKEN]
---

# Mealie Skill

API client for [Mealie](https://mealie.io), a self-hosted recipe manager and meal planner. Manage recipes, shopping lists, and meal plans.

## Environment Variables

Set these in your agent's `.env` (`~/.openclaw/.env`) or create a skill-level `.env` at `~/.openclaw/skills/mealie/.env`:

- `MEALIE_URL` β€” Your Mealie instance URL (e.g., `https://recipes.example.com`)
- `MEALIE_API_TOKEN` β€” Your API token (create at `/user/profile/api-tokens` in Mealie)

The script only reads `MEALIE_URL` and `MEALIE_API_TOKEN` from `.env` files β€” other variables are ignored.

## Getting an API Token

1. Log into your Mealie instance
2. Go to User Profile β†’ API Tokens
3. Create a new token with a descriptive name
4. Copy the token to your `.env`

## Commands

### Recipes

```bash
node ~/.openclaw/skills/mealie/scripts/mealie.js recipes              # List all recipes
node ~/.openclaw/skills/mealie/scripts/mealie.js recipe <slug>        # Get recipe details
node ~/.openclaw/skills/mealie/scripts/mealie.js search "query"       # Search recipes
node ~/.openclaw/skills/mealie.js create-recipe <url>                 # Import recipe from URL
node ~/.openclaw/skills/mealie.js delete-recipe <slug>                # Delete recipe
```

### Shopping Lists

```bash
node ~/.openclaw/skills/mealie/scripts/mealie.js lists                # List shopping lists
node ~/.openclaw/skills/mealie.js list <id>                           # Show list items
node ~/.openclaw/skills/mealie.js add-item <listId> "item" [qty]      # Add item
node ~/.openclaw/skills/mealie.js check-item <listId> <itemId>        # Mark checked
node ~/.openclaw/skills/mealie.js uncheck-item <listId> <itemId>      # Mark unchecked
node ~/.openclaw/skills/mealie.js delete-item <listId> <itemId>       # Delete item
```

### Meal Plans

```bash
node ~/.openclaw/skills/mealie/scripts/mealie.js mealplan [days]      # Show meal plan (default 7 days)
node ~/.openclaw/skills/mealie.js add-meal <date> <recipeSlug> [meal] # Add meal to plan
node ~/.openclaw/skills/mealie.js delete-meal <planId>                # Remove meal from plan
```

### Other

```bash
node ~/.openclaw/skills/mealie.js stats                               # Show statistics
node ~/.openclaw/skills/mealie.js tags                                # List all tags
node ~/.openclaw/skills/mealie.js categories                          # List all categories
```

## Examples

```bash
# List all recipes
node ~/.openclaw/skills/mealie/scripts/mealie.js recipes

# Search for pasta recipes
node ~/.openclaw/skills/mealie/scripts/mealie.js search "pasta"

# Get a specific recipe
node ~/.openclaw/skills/mealie/scripts/mealie.js recipe spaghetti-carbonara

# Add milk to shopping list
node ~/.openclaw/skills/mealie/scripts/mealie.js add-item abc123 "Milk" "1 gallon"

# Show this week's meal plan
node ~/.openclaw/skills/mealie/scripts/mealie.js mealplan 7

# Add a recipe to Tuesday's dinner
node ~/.openclaw/skills/mealie/scripts/mealie.js add-meal 2026-02-17 chicken-tacos dinner
```

## API Details

- Uses Bearer token authentication
- All endpoints are under `/api/`
- Pagination is supported on list endpoints (use `--page` and `--per-page` flags)
- Recipe slugs are URL-friendly identifiers (e.g., `spaghetti-carbonara`)

Based on [Mealie API docs](https://docs.mealie.io).


---

## Skill Companion Files

> Additional files collected from the skill directory layout.

### _meta.json

```json
{
  "owner": "angusthefuzz",
  "slug": "mealie-api",
  "displayName": "Mealie Recipe Manager",
  "latest": {
    "version": "1.0.0",
    "publishedAt": 1771169570146,
    "commit": "https://github.com/openclaw/skills/commit/b07da93ec6147f031fda608765700fae891d0710"
  },
  "history": []
}

```

### scripts/mealie.js

```javascript
#!/usr/bin/env node
/**
 * Mealie CLI - Interact with Mealie recipe manager
 * 
 * Recipes:
 *   node mealie.js recipes                     - List all recipes
 *   node mealie.js recipe <slug>               - Get recipe details
 *   node mealie.js search "query"              - Search recipes
 *   node mealie.js create-recipe <url>         - Import recipe from URL
 *   node mealie.js delete-recipe <slug>        - Delete recipe
 * 
 * Shopping Lists:
 *   node mealie.js lists                       - List shopping lists
 *   node mealie.js list <id>                   - Show list items
 *   node mealie.js add-item <listId> "item"    - Add item to list
 *   node mealie.js check-item <listId> <itemId> - Mark checked
 *   node mealie.js delete-item <listId> <itemId> - Delete item
 * 
 * Meal Plans:
 *   node mealie.js mealplan [days]             - Show meal plan (default 7)
 *   node mealie.js add-meal <date> <slug> [meal] - Add meal
 * 
 * Requires: MEALIE_URL and MEALIE_API_TOKEN environment variables
 */

const fs = require('fs');
const path = require('path');
const https = require('http'); // May be http or https depending on URL

// Load Mealie credentials from environment
function loadEnv() {
  if (process.env.MEALIE_URL && process.env.MEALIE_API_TOKEN) {
    return;
  }
  
  // Try skill-level .env first
  const skillEnvPath = path.join(__dirname, '..', '.env');
  if (fs.existsSync(skillEnvPath)) {
    const content = fs.readFileSync(skillEnvPath, 'utf8');
    content.split('\n').forEach(line => {
      const match = line.match(/^(MEALIE_URL|MEALIE_API_TOKEN)=(.*)$/);
      if (match && !process.env[match[1]]) {
        process.env[match[1]] = match[2].trim().replace(/^["']|["']$/g, '');
      }
    });
  }
  
  // Fallback: agent-level .env (only MEALIE_* vars)
  const agentEnvPath = path.join(__dirname, '..', '..', '..', '.env');
  if (fs.existsSync(agentEnvPath)) {
    const content = fs.readFileSync(agentEnvPath, 'utf8');
    content.split('\n').forEach(line => {
      const match = line.match(/^(MEALIE_URL|MEALIE_API_TOKEN)=(.*)$/);
      if (match && !process.env[match[1]]) {
        process.env[match[1]] = match[2].trim().replace(/^["']|["']$/g, '');
      }
    });
  }
}

loadEnv();

const MEALIE_URL = process.env.MEALIE_URL;
const API_TOKEN = process.env.MEALIE_API_TOKEN;

if (!MEALIE_URL || !API_TOKEN) {
  console.error('Error: MEALIE_URL and MEALIE_API_TOKEN must be set');
  console.error('Set them in ~/.openclaw/.env or ~/.openclaw/skills/mealie/.env');
  process.exit(1);
}

// Parse URL
const urlObj = new URL(MEALIE_URL);
const isHttps = urlObj.protocol === 'https:';
const httpModule = isHttps ? require('https') : require('http');

// API request helper
async function api(method, endpoint, body = null) {
  return new Promise((resolve, reject) => {
    const path = endpoint.startsWith('/api') ? endpoint : `/api${endpoint}`;
    const options = {
      hostname: urlObj.hostname,
      port: urlObj.port || (isHttps ? 443 : 80),
      path: path,
      method: method,
      headers: {
        'Authorization': `Bearer ${API_TOKEN}`,
        'Content-Type': 'application/json'
      }
    };
    
    const req = httpModule.request(options, (res) => {
      let data = '';
      res.on('data', chunk => data += chunk);
      res.on('end', () => {
        try {
          const parsed = data ? JSON.parse(data) : {};
          if (res.statusCode >= 400) {
            reject(new Error(`HTTP ${res.statusCode}: ${data}`));
          } else {
            resolve(parsed);
          }
        } catch (e) {
          reject(new Error(`Parse error: ${data}`));
        }
      });
    });
    
    req.on('error', reject);
    if (body) req.write(JSON.stringify(body));
    req.end();
  });
}

// Format recipe list
function formatRecipes(recipes) {
  const items = recipes.items || recipes.data || recipes;
  if (!items.length) {
    console.log('No recipes found');
    return;
  }
  console.log(`\n🍳 Recipes (${recipes.total || items.length})\n`);
  items.forEach(r => {
    console.log(`   ${r.name || r.slug}`);
    console.log(`   Slug: ${r.slug}`);
    console.log('');
  });
}

// Format single recipe
function formatRecipe(recipe) {
  console.log(`\n🍳 ${recipe.name}\n`);
  console.log(`   Slug: ${recipe.slug}`);
  console.log(`   Serves: ${recipe.recipeYield || 'N/A'}`);
  console.log(`   Prep: ${recipe.prepTime || 'N/A'} | Cook: ${recipe.performTime || 'N/A'}`);
  
  if (recipe.recipeIngredient?.length) {
    console.log('\n   Ingredients:');
    recipe.recipeIngredient.forEach(ing => {
      const text = ing.display || ing.note || (typeof ing === 'string' ? ing : JSON.stringify(ing));
      console.log(`     β€’ ${text}`);
    });
  }
  
  if (recipe.recipeInstructions?.length) {
    console.log('\n   Instructions:');
    recipe.recipeInstructions.forEach((step, i) => {
      const text = step.text || step;
      console.log(`     ${i + 1}. ${text.slice(0, 80)}${text.length > 80 ? '...' : ''}`);
    });
  }
  console.log('');
}

// Format shopping lists
function formatLists(lists) {
  const items = lists.items || lists.data || lists;
  if (!items.length) {
    console.log('No shopping lists found');
    return;
  }
  console.log(`\nπŸ›’ Shopping Lists\n`);
  items.forEach(list => {
    const checked = list.listItems?.filter(i => i.checked).length || 0;
    const total = list.listItems?.length || 0;
    console.log(`   ${list.name}`);
    console.log(`   ID: ${list.id} | ${checked}/${total} items checked`);
    console.log('');
  });
}

// Format single list
function formatList(list) {
  console.log(`\nπŸ›’ ${list.name}\n`);
  if (!list.listItems?.length) {
    console.log('   (empty)');
    return;
  }
  list.listItems.forEach(item => {
    const check = item.checked ? 'β˜‘' : '☐';
    console.log(`   ${check} ${item.display || item.note || item.food?.name || 'Item'}`);
    if (item.quantity) console.log(`      Qty: ${item.quantity} ${item.unit?.name || ''}`);
  });
  console.log('');
}

// Format meal plan
function formatMealplan(plans) {
  const items = plans.items || plans.data || plans;
  if (!items.length) {
    console.log('No meals planned');
    return;
  }
  
  // Group by date
  const byDate = {};
  items.forEach(meal => {
    const date = meal.date;
    if (!byDate[date]) byDate[date] = [];
    byDate[date].push(meal);
  });
  
  console.log(`\nπŸ“… Meal Plan\n`);
  
  Object.keys(byDate).sort().forEach(date => {
    const d = new Date(date);
    const weekday = d.toLocaleDateString('en-US', { weekday: 'short' });
    const day = d.getDate();
    console.log(`   ${day.toString().padStart(2, ' ')} ${weekday}`);
    byDate[date].forEach(meal => {
      const recipe = meal.recipe?.name || meal.recipe?.slug || 'No recipe';
      const mealType = meal.entryType || '';
      console.log(`       ${mealType}: ${recipe}`);
    });
  });
  console.log('');
}

// Commands
async function main() {
  const [cmd, ...args] = process.argv.slice(2);
  
  try {
    switch (cmd) {
      case 'recipes':
        const recipes = await api('GET', '/recipes?perPage=100');
        formatRecipes(recipes);
        break;
        
      case 'recipe':
        if (!args[0]) {
          console.error('Usage: mealie.js recipe <slug>');
          process.exit(1);
        }
        const recipe = await api('GET', `/recipes/${args[0]}`);
        formatRecipe(recipe);
        break;
        
      case 'search':
        if (!args[0]) {
          console.error('Usage: mealie.js search "query"');
          process.exit(1);
        }
        const results = await api('GET', `/recipes?search=${encodeURIComponent(args[0])}`);
        formatRecipes(results);
        break;
        
      case 'create-recipe':
        if (!args[0]) {
          console.error('Usage: mealie.js create-recipe <url>');
          process.exit(1);
        }
        const created = await api('POST', '/recipes/create/url', { url: args[0] });
        console.log(`βœ“ Created recipe: ${created.slug}`);
        break;
        
      case 'delete-recipe':
        if (!args[0]) {
          console.error('Usage: mealie.js delete-recipe <slug>');
          process.exit(1);
        }
        await api('DELETE', `/recipes/${args[0]}`);
        console.log(`βœ“ Deleted recipe: ${args[0]}`);
        break;
        
      case 'lists':
        const lists = await api('GET', '/households/shopping/lists');
        formatLists(lists);
        break;
        
      case 'list':
        if (!args[0]) {
          console.error('Usage: mealie.js list <id>');
          process.exit(1);
        }
        const list = await api('GET', `/households/shopping/lists/${args[0]}`);
        formatList(list);
        break;
        
      case 'add-item':
        if (!args[0] || !args[1]) {
          console.error('Usage: mealie.js add-item <listId> "item" [quantity]');
          process.exit(1);
        }
        const newItem = await api('POST', '/households/shopping/items', {
          shoppingListId: args[0],
          note: args[1],
          quantity: args[2] ? parseFloat(args[2]) : 1
        });
        console.log(`βœ“ Added item to list`);
        break;
        
      case 'check-item':
        if (!args[0] || !args[1]) {
          console.error('Usage: mealie.js check-item <listId> <itemId>');
          process.exit(1);
        }
        await api('PUT', `/households/shopping/items/${args[1]}`, { checked: true });
        console.log(`βœ“ Item checked`);
        break;
        
      case 'uncheck-item':
        if (!args[0] || !args[1]) {
          console.error('Usage: mealie.js uncheck-item <listId> <itemId>');
          process.exit(1);
        }
        await api('PUT', `/households/shopping/items/${args[1]}`, { checked: false });
        console.log(`βœ“ Item unchecked`);
        break;
        
      case 'delete-item':
        if (!args[0] || !args[1]) {
          console.error('Usage: mealie.js delete-item <listId> <itemId>');
          process.exit(1);
        }
        await api('DELETE', `/households/shopping/items/${args[1]}`);
        console.log(`βœ“ Item deleted`);
        break;
        
      case 'mealplan':
        const days = args[0] ? parseInt(args[0]) : 7;
        const today = new Date().toISOString().split('T')[0];
        const endDate = new Date(Date.now() + days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
        const plans = await api('GET', `/households/mealplans?start_date=${today}&end_date=${endDate}`);
        formatMealplan(plans);
        break;
        
      case 'add-meal':
        if (!args[0] || !args[1]) {
          console.error('Usage: mealie.js add-meal <date> <recipeSlug> [mealType]');
          process.exit(1);
        }
        const meal = await api('POST', '/households/mealplans', {
          date: args[0],
          recipeSlug: args[1],
          entryType: args[2] || 'dinner'
        });
        console.log(`βœ“ Added meal for ${args[0]}`);
        break;
        
      case 'stats':
        const stats = await api('GET', '/households/statistics');
        console.log('\nπŸ“Š Statistics\n');
        console.log(`   Total recipes: ${stats.totalRecipes || 'N/A'}`);
        console.log(`   Total users: ${stats.totalUsers || 'N/A'}`);
        console.log(`   Total categories: ${stats.totalCategories || 'N/A'}`);
        console.log(`   Total tags: ${stats.totalTags || 'N/A'}`);
        console.log('');
        break;
        
      case 'tags':
        const tags = await api('GET', '/organizers/tags');
        console.log('\n🏷️ Tags\n');
        (tags.items || tags.data || tags).forEach(t => console.log(`   ${t.name}`));
        console.log('');
        break;
        
      case 'categories':
        const cats = await api('GET', '/organizers/categories');
        console.log('\nπŸ“ Categories\n');
        (cats.items || cats.data || cats).forEach(c => console.log(`   ${c.name}`));
        console.log('');
        break;
        
      default:
        console.log('Usage: mealie.js <command> [args]');
        console.log('\nRecipes: recipes, recipe, search, create-recipe, delete-recipe');
        console.log('Lists: lists, list, add-item, check-item, uncheck-item, delete-item');
        console.log('Meal Plans: mealplan, add-meal');
        console.log('Other: stats, tags, categories');
    }
  } catch (err) {
    console.error(`Error: ${err.message}`);
    process.exit(1);
  }
}

main();

```

mealie | SkillHub