Command line interface (CLI)
The official Gram CLI (@gram-lang/cli) is the primary tool for validating, compiling, and managing your recipe collections locally. It acts as the bridge between your .gram files and the rest of your technical stack (SSG, Next.js, mobile apps, etc.).
Installation
Section titled “Installation”The CLI runs on both Node.js (>=20.6.0, automatically loading .env files via process.loadEnvFile) and Bun — no runtime-specific APIs are required, so any of the two works equally well.
v1.2.0 When a newer version is published on npm, gram prints a short notice after any command finishes (skipped in CI and non-interactive runs). Run gram upgrade to check and install it on demand, or silence the passive notice with gram config set updateCheck false — or, for a single run, the GRAM_NO_UPDATE_CHECK environment variable.
Global options
Section titled “Global options”--verbose/--debug: works with any command, prints a full stack trace on error instead of just the terse message.--stock <specifiers>: treats specified imported module paths as already on hand (skips their timeline cooking steps and collapses them into a single purchasable item on shopping lists). Accepts comma-separated paths (@bases/shortcrust.gram,./sauces/bechamel.gram).
Core commands
Section titled “Core commands”Project management & developer workflow
Section titled “Project management & developer workflow”gram init
Section titled “gram init”Scaffolds a new Gram environment in the current directory.
- Creates a
.gram/directory. - Generates a plain, uncommented
config.yaml. - Interactively configures your preferred recipe language (currently
enorfr). - Interactively configures your preferred AI provider and model.
- Generates/updates a
.envfile for your AI API keys. - Generates a heavily commented starter
ingredients.yamlingredient database at.gram/ingredients.yaml. - Generates a
.gitignoreto prevent committing sensitive keys.
gram upgrade v1.2.0
Section titled “gram upgrade ”Checks npm for a newer @gram-lang/cli release and offers to install it.
- Always performs a fresh lookup against the npm registry — independent of the passive background check described above.
- Prints a comparison of the installed and latest versions, then asks for confirmation before running
npm install -g @gram-lang/cli@latest. - Does nothing (beyond reporting) when already up to date.
gram check [pattern]
Section titled “gram check [pattern]”Validates your .gram files for syntax errors, structural integrity, and undefined ingredients.
- Runs the OhmJS parser to catch syntax errors.
- Runs the Kitchen compiler to catch structural errors (e.g., cyclic dependencies).
- Connects to your
ingredients.yamlto warn about ingredients not documented in your database. - Warnings (nutritional/estimation gaps, incomplete-but-valid annotations) don’t fail the command by default — only structural errors (undefined references, scope conflicts) do. Pass
--strictto treat every warning as an error too (useful in CI). - Options:
--db <path>,--skip-db,--strict.
gram build [pattern]
Section titled “gram build [pattern]”Compiles your .gram recipes into the final, minified JSON format.
- By default, outputs pure JSON directly to
stdoutfor easy piping. - Computes nutritional data and physical mass standardization automatically via the database.
--scale <factor>bakes the scaling into the JSON output. Reference mode (e.g.flour=300g) isn’t available here by design:buildcan batch multiple files at once, and a reproducible batch output shouldn’t depend on reading one specific file’s shopping list first. Usegram view/gram scaleto find the factor you want, then pass that numeric factor tobuild.- When a glob or several files are given, a file imported (via
@use) by another file in the same batch is excluded by default, so its ingredients aren’t counted both on their own and again composed into the importer — pass--include-modulesto build it anyway. - Options:
--output/-o <dir>,--pretty,--scale <factor>,--db <path>,--skip-db,--include-modules.
gram view <file>
Section titled “gram view <file>”Displays a recipe directly in the terminal in a beautifully styled ASCII box.
- Supports automatic paging for long recipes.
- Displays calculated nutrition, timings, and ingredient checklists.
- With
--scale, all ingredient quantities (shopping list and in-step references) are adjusted. - Options:
--scale <factor|ref>,--no-pager,--skip-db,--db,--bakers-math,--bakers-reference <id>,--bakers-math-only,--nutrition <basis>. --nutrition <auto|total|per-portion|per-100g>v1.1.0 picks which nutrition basis is shown (defaultauto: per portion when the recipe declaresportions:, otherwise the whole recipe).per-100gis reported per 100 g of the raw assembled mixture — see Nutritional Estimation.
gram import <source>
Section titled “gram import <source>”Imports a recipe from a web page URL, a local JSON-LD file, or a YouTube video / Short, and converts it into a .gram file using AI.
Supported Sources:
- Web page URLs: automatically fetches the page and extracts structured
application/ld+jsonrecipe metadata (Schema.orgRecipe). - Local JSON / JSON-LD files: parses exported JSON-LD schemas directly from disk.
- YouTube videos & Shorts v1.1.0: analyzes cooking videos directly using Gemini multimodal video understanding (without requiring third-party transcript scrapers).
Import Workflow & Guardrails:
-
Translates and formats the recipe into valid Gram syntax, respecting the
languageconfig. -
Requires AI to be configured (see Configure AI Provider).
-
Before writing to
--output, the converted recipe is shown for review and confirmation. In a non-interactive run the review is skipped with a warning — pass--yes/-yto skip it deliberately. -
Nothing is emitted when the import came back broken. Two checks run on the result and refuse it outright:
- Lost content — every
@ingredientwritten into the file is matched against what the compiler actually registered. A mismatch means text was swallowed, most often by a//comment dropped mid-sentence, which hides the rest of its line. Such a file compiles with no warning at all and is simply missing ingredients. - Unfixed errors — the AI gets two attempts to repair error-severity problems. If any survive, they are reported rather than written out.
Pass
--forceto write the file anyway, or retry with a stronger--model. - Lost content — every
-
source:andauthor:are taken from the source data (or YouTube metadata), or removed when it has none — never left for the model to invent. -
Ingredients that came back without a quantity are counted and listed by line. Not an error —
@salt{}is idiomatic for “to taste” — but it is the clearest measure of how finished the recipe is, and it points you at the lines to edit. An ingredient given an amount once and merely mentioned again later is not counted. -
When an ingredient database is present, the analyzer also reports what it could not resolve (unknown ingredients, quantities with no usable mass). These are gaps in your database, not import failures, so they never block.
YouTube Video Import Specifics: v1.1.0
-
Google / Gemini only: Gemini is the one provider that natively processes YouTube URLs. With any other provider the import stops rather than sending it something that isn’t a video.
-
Cost scales with length, at roughly 100 input tokens per second of video: a Short is a few thousand tokens, ten minutes is around 60,000, twenty-six minutes around 158,000. Videos over 20 minutes are refused by default; raise the ceiling with
--max-duration <minutes>. -
Cost estimate needs
YOUTUBE_API_KEY: a plain YouTube Data API key, read from the environment only, never fromconfig.yaml. Without it the import still runs, but it cannot determine the length upfront or enforce the limit, and says so. -
Video quantity expectations: a video states far fewer explicit quantities than a written recipe. The prompt tells the model to leave
{}empty rather than guess, and the checks above then refuse anything where content went missing. Expect gaps to fill in by hand — that is the honest outcome, not a failure. -
title:,author:(channel name), andsource:(URL) come directly from YouTube metadata and are used verbatim. -
Options:
--output/-o <file>,--yes/-y,--force,--max-duration <minutes>v1.1.0, plus the shared AI options.
gram shop [pattern]
Section titled “gram shop [pattern]”Generates an aggregated shopping list across multiple recipes.
- Aggregates quantities intelligently via density (volume → grams when density is known).
- Groups ingredients by their
categoryfield (culinary family: Produce, Dairy, Grains, etc.). - Alias grouping: if two recipes use different names for the same ingredient (e.g.
butterandbeurre), they are merged under the canonical key via the database aliases. - Ingredients without a quantity (e.g.
@salt{}) are listed alongside their main entry rather than in a separate section. - Supports small units:
pinch,dash,drop(and French variants) are handled without aggregation errors. --scale <factor>applies a numeric multiplier to all recipes (factor only — ref mode not available for multi-file).- When a glob or several files are given, a file imported (via
@use) by another file in the same batch is excluded by default, so its ingredients aren’t counted both on their own and again composed into the importer — pass--include-modulesto list it anyway. - Options:
--format terminal|md|json,--output/-o <file>,--scale <factor>,--db,--skip-db,--include-modules.
gram cook <file>
Section titled “gram cook <file>”Launches an interactive step-by-step cooking guide directly in the terminal.
Flow:
-
Mise en place : full aggregated ingredient list for the recipe (Space to start)
-
Section intro : ingredients for the upcoming section (aggregated, shown before each section)
-
Cooking steps : one step at a time in a two-column layout: ingredients on the left, instructions on the right
-
End screen : total time spent
Ingredients are aggregated per section: repeated uses of the same ingredient in a section are grouped into one entry with quantities joined (e.g. 200g butter + 50g butter → shown as 200g + 50g butter, not arithmetically summed). The ingredient panel shows all necessary quantities for that section at a glance.
Timers — timers annotated in the recipe (~label{30min}) appear in the step view:
- Press
Tto start a timer; if multiple are available, a picker appears - Timers run in the background — they remain visible as you advance through steps
- A terminal bell sounds and the timer’s display switches to a static “done” state when it finishes
Q/Esctrigger the quit-confirmation flow rather than dismissing a finished timer
Keyboard shortcuts:
| Key | Action |
|---|---|
Space / Enter | Next step |
B | Previous step |
T | Start a timer |
Q / Esc | Quit (asks for confirmation if a timer is running) |
- Options:
--scale <factor|ref>,--skip-db,--db.
gram diff <file> [file-b]
Section titled “gram diff <file> [file-b]”Shows a semantic diff of a recipe — comparing ingredients, timings, sections, temperatures, timers, and frontmatter rather than raw text.
The diff covers seven axes:
-
Ingredients — added/removed/changed quantities, with
percentChangewhen units match. -
Timings —
totalTime,cookTime,activeTime,preparationTimein minutes. -
Sections — added/removed sections and step-count changes.
-
Frontmatter — changes to
portions,description, and other metadata fields. Atitlechange is tracked separately and shown as its own line above the frontmatter block. -
Preparations — changed preparation modes per ingredient (e.g. “diced” → “sliced”).
-
Temperatures & Timers — added/removed/changed temperature targets and timer durations per section.
-
Modules — an
@useimport added, removed, re-bound to a different name, or rescaled to a different factor. -
Operates on compiled objects (Kitchen output), not raw text — syntactic reformatting produces no diff.
-
Git mode requires the file to be tracked. Gracefully errors if
gitis unavailable. -
A section spliced in from an
@useimport never counts toward the Sections/Preparations/Temperatures/Timers axes — otherwise adding one import would look like every other section moved. Its own import is what the Modules axis reports instead.gram diffdoesn’t resolve@useon its own, though: it compiles each side standalone, so a change inside an imported base isn’t visible here — diff the base file directly for that. -
Options:
--ref <git-ref>.
gram scale <file>
Section titled “gram scale <file>”Displays a before/after comparison of ingredient quantities at a given scale.
- Displays a comparison table: original quantities (dimmed) vs scaled (green).
- Quantities that cannot be scaled (text values like “1 pinch”) are listed separately.
- Warns on extreme factors (below ×0.1 or above ×20) and notes that cooking times are not adjusted.
- Reference mode (
id=value) computes the factor from the ingredient’s current quantity. The ID must match the ingredient key in the recipe. Units in the same family convert automatically (e.g.flour=1kgagainst a recipe written in500g); crossing mass↔volume (e.g.water=150gagainst a recipe inml) also works whenever a density is available — fromgram db enrich, or adensities: ["water:1.0"]override in the recipe’s own frontmatter. - Not every ingredient can be a reference target: fixed (
@=) ingredients, relative quantities (70% @&flour), ingredients only used inside a sub-recipe (their composite parent’s own total is a valid target instead), ingredients inside an alternative group, and ingredients split across incompatible units are all rejected with a specific error explaining why (and what to scale by instead). See Deep Dive: Scaling for the full list. - Options:
--scale <factor|ref>,--skip-db,--db.
gram watch [dir]
Section titled “gram watch [dir]”Watches a directory for .gram file changes and re-runs gram check automatically on every save.
- Displays a timestamped result line per change:
[12:34:01] ✓ brioche.gramor✗ brioche.gram — 1 error. - Errors are shown inline below the filename — the watcher never stops on error.
- 150ms debounce prevents redundant runs when editors write files in multiple chunks.
- Saving a base recipe also re-checks every file under the watched directory that
@uses it, directly or transitively — not just the file that changed on disk. A→ rechecking N dependent file(s)line names how many. - Options:
--build,--output/-o <dir>,--skip-db,--db.
gram suggest
Section titled “gram suggest”Finds recipes in your project that use a given set of ingredients.
- Scans all
.gramfiles in parallel — uses only the parser (no full pipeline compilation), so it is fast even on large collections. - Alias-aware matching: if a database is configured, ingredient names are resolved through the alias index. Searching for
"beurre"will match recipes containing@butter. - Scores each recipe by match percentage (
matched / total with-terms). Use--min-matchto filter low-score results. --withoutimmediately excludes any recipe that contains one of those ingredients.- Options:
--with/-w <csv>,--without <csv>,--top/-n <n>(default 10),--min-match <0–100>(default 1),--pattern <glob>,--db,--skip-db,--json.
gram print <file>
Section titled “gram print <file>”Generates a print-ready HTML and opens it in the default browser.
- The HTML is written to a temporary file (
gram_print_<timestamp>.htmlin the OS temp directory, e.g./tmpon Linux/macOS) and opened with the OS default browser (openon macOS,xdg-openon Linux,cmd /c starton Windows). - Identical output to
gram export --format html— suitable for browser print dialog (Ctrl+P/Cmd+P) to produce an A4 PDF. --no-step-qty— hides ingredient quantities in step text (useful when cooking from the ingredient list in the margin).- Options:
--no-open,--scale <factor|ref>,--no-step-qty,--skip-db,--db,--bakers-math,--bakers-reference <id>,--bakers-math-only,--nutrition <basis>. --nutrition <auto|total|per-portion|per-100g>v1.1.0 picks which nutrition basis is shown (defaultauto: per portion when the recipe declaresportions:, otherwise the whole recipe).per-100gis reported per 100 g of the raw assembled mixture — see Nutritional Estimation.
gram export <file>
Section titled “gram export <file>”Exports a recipe to Markdown or print-ready HTML.
--format md— standard Markdown with a shopping list, equipment section, and numbered steps.--format html— standalone A4-ready HTML document with embedded CSS for printing, with inline Lucide SVG icons for timers and temperatures (no external dependency there). It does load Courier Prime (body) and Inter (labels) from Google Fonts via a CSS@import, so it is not fully offline-capable — a network connection is needed the first time fonts are fetched.- Default output path: same directory as the input file, extension replaced (
.gram→.mdor.html). --no-step-qty— hides ingredient quantities in step text. HTML format only — has no effect with--format md. The section mise en place (ingredient list before each section) always shows full quantities.- Options:
--format md|html,--output <path>,--scale <factor|ref>,--no-step-qty,--skip-db,--db,--bakers-math,--bakers-reference <id>,--bakers-math-only,--nutrition <basis>. --nutrition <auto|total|per-portion|per-100g>v1.1.0 picks which nutrition basis is shown (defaultauto: per portion when the recipe declaresportions:, otherwise the whole recipe).per-100gis reported per 100 g of the raw assembled mixture — see Nutritional Estimation.
gram format [pattern]
Section titled “gram format [pattern]”Auto-formats .gram files applying 13 canonical rules in-place via @gram-lang/format.
13 Canonical rules applied:
- Frontmatter formatting — delimiter normalization and whitespace cleanup
- Section header titles — single space after
## - Step indexing — normalized step prefixes (
1.) - Action blocks — normalized action prefixes (
[Mix]) - Ingredient tokens —
@ingredient{qty}spacing and bracket syntax - Cookware tokens —
#cookware{qty}spacing and bracket syntax - Timer tokens —
~timer{duration}and passive~_timer{duration}spacing - Temperature tokens —
^temp{value}formatting - Intermediate declarations & references —
->&doughand&doughspacing - Composite syntax —
<@parentcomposite ingredient syntax - Trailing decimal zeros —
{500.0g}→{500g},{1.50g}→{1.5g} - Comment formatting — clean spacing after comment sigils (
//) - Whitespace cleanup & single trailing EOF newline
Output per file: ✔ brioche.gram 2 IDs lowercased · 1 trailing zero removed
The formatter is idempotent — running it twice produces no further changes.
- Options:
--check.
Configuration
Section titled “Configuration”Commands nested under gram config to read and write project (or global) configuration.
gram config list
Section titled “gram config list”Displays all configuration values from both the local project config and the global config.
gram config get <key>
Section titled “gram config get <key>”Prints a single config value to stdout.
gram config set <key> <value>
Section titled “gram config set <key> <value>”Sets a configuration value.
- Values are written to
.gram/config.yamlby default; use--globalfor~/.config/gram/config.yaml. - Sensitive keys (
ai.apiKey) are always written to the project.envfile. The env var name is derived from the configured provider (e.g.google→GEMINI_API_KEY). - Numbers and booleans are coerced automatically (
"2"→2,"true"→true).
gram config unset <key>
Section titled “gram config unset <key>”Removes a configuration value.
Database management
Section titled “Database management”Commands nested under gram db to manage your ingredients.yaml.
-
Section titled “gram db sync [pattern]”gram db sync [pattern]Scans your recipes to find undocumented ingredients and adds them to your database.
- Interactive fuzzy matching (Levenshtein) helps you avoid duplicates for plurals or typos.
- Options:
--dry-run/-n(preview without writing),--db <path>.
-
Section titled “gram db lint”gram db lintUses AI to detect and resolve semantic duplicates and plurals in your database.
- Detects cross-language duplicates (e.g.
sucre/sugar) and plural forms (e.g.eggs→egg). - For each duplicate, lets you choose which key to keep — the removed key is automatically added as an alias.
- Displays a nutrition diff (only differing fields) when both entries have conflicting nutrition data, so you can make an informed choice.
- Options:
--report/-r(show issues without applying fixes),--db <path>, plus the shared AI options.
- Detects cross-language duplicates (e.g.
-
v1.1.0
Section titled “gram db enrich”gram db enrichUses AI to propose missing data for your database, then walks you through an interactive review before writing anything.
- Proposes
density,unit_weight,nutrition,category, andtagsfields in batches (unit_weightis filled alongsidedensity).categoryis a culinary family (e.g. Vegetables, Dairy, Grains) — distinct from free-formtags. nutritioncovers every nutrient the database accepts, including the fat subtypes (sat_fat,mono_fat,poly_fat) andalcohol.category/tagsare written automatically;density/unit_weight/nutritionare reviewed one entry at a time (accept, edit, or skip) since they’re product-specific — only you know what’s actually in your kitchen.- Values accepted as-is are tagged
# [LLM]iningredients.yaml, marking them as AI estimates that haven’t been human-reviewed. - Safely re-runnable: only proposes fields that are still missing.
- Options:
--ingredient <slug>(enrich a single entry),--field density|nutrition|tags|category|all(defaultall),--yes/-y(skip the review, accept everything — for scripting),--report/-r(preview what needs review without writing),--db <path>, plus the shared AI options.
- Proposes
gram db validate
Section titled “gram db validate”Validates the integrity of your ingredients.yaml.
- Checks for schema errors, duplicated aliases, and incoherent values (e.g., density > 2.5).
- A malformed entry doesn’t stop the rest from being checked: every entry is validated independently, and the report lists every issue found across the whole file in one pass.
- Options:
--strict(exit 1 on warnings, useful in CI).
gram db search [query]
Section titled “gram db search [query]”Searches and displays ingredient entries in full detail.
Every field is displayed for each match: name, aliases, tags, category, and full nutrition (calories, protein, carbs, fat, saturated/monounsaturated/polyunsaturated fat, sugar, fiber, sodium, alcohol per 100g) and physical data (density, yield, unit weight).
- Options:
--tag <tag>,--category/-c <category>,--missing nutrition|physical|aliases,--exact,--count,--json,--db <path>.
gram db merge <source.yaml>
Section titled “gram db merge <source.yaml>”Merges an external ingredient database into your local one.
The merge is alias-aware: if your database has butter with alias beurre, and the source has a beurre entry, they are recognized as the same ingredient.
- Options:
--prefer local|remote(defaultlocal),--dry-run,--only-new,--db <path>.
Configuration file details
Section titled “Configuration file details”The CLI merges configuration from ~/.config/gram/config.yaml (global) and .gram/config.yaml (project).
Choosing the AI model
Section titled “Choosing the AI model”Every AI-backed command — gram import, gram db lint and gram db enrich — accepts the same three options and prints the model it is about to call, so an unexpected choice is visible before it costs you anything:
--model <name>— use a different model for this run. The provider is unchanged.--provider <google|openai|anthropic|ollama>— use a different provider for this run. The provider is never guessed from the model name.--pick-model— pick provider and model from a menu. Requires a terminal; in a script, use--provider/--modelinstead.
Neither flag writes anything to your config. To change the default for good, use gram config set ai.model <name> or re-run gram init.
When --provider (or --pick-model) selects a provider your config did not name, the model, apiKey and baseUrl written under ai: are not carried over — they belong to the provider they were written for. You will be asked for the new provider’s own environment variable instead. This holds whether your ai: block names a different provider or names none at all: a block with no provider: belongs to whichever provider Gram would have auto-detected, not to one you picked by hand.
Cascading AI configuration
Section titled “Cascading AI configuration”Gram uses a cascading fallback hierarchy for sensitive credentials like AI API keys:
- Environment Variables: The variable belonging to the provider in use —
GEMINI_API_KEY,OPENAI_API_KEYorANTHROPIC_API_KEY(from the system or a.envfile) — takes absolute precedence. This is the recommended way to store secrets locally and in CI/CD environments. config.yamlFallback: If that variable is missing, Gram falls back to theai.apiKeyfield in yourconfig.yaml— but only when it was written for the provider being called.
A key is never reused across providers. If you have configured provider: openai and only GEMINI_API_KEY is exported, Gram stops and asks for OPENAI_API_KEY rather than sending your Google key to OpenAI. The same rule applies across config layers: a global ai: block is ignored entirely when your project selects a different provider.
config.yaml settings
Section titled “config.yaml settings”Here is the complete reference of all available settings in config.yaml: