Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 29 additions & 7 deletions bin/coderrr.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,10 @@ const Agent = require('../src/agent');
const configManager = require('../src/configManager');
const { getProviderChoices, getModelChoices, getProvider, validateApiKey } = require('../src/providers');
const { tryExtractJSON } = require('../src/utils');

const { displayRecipeList } = require('../src/recipeUI');
const recipeManager = require('../src/recipeManager');
const { displayInsights } = require('../src/insightsUI');

program
.command('insights')
.description('Display local usage statistics and task history')
.action(() => {
displayInsights();
});
// Optional: Load .env from user's home directory (for advanced users who want custom backend)
const homeConfigPath = path.join(os.homedir(), '.coderrr', '.env');
if (fs.existsSync(homeConfigPath)) {
Expand All @@ -41,6 +36,33 @@ program
.description('AI Coding Agent CLI - Your personal coding assistant')
.version('1.0.0');

// Recipe command - manage and run custom coding recipes
program
.command('recipe [name]')
.description('Manage and run custom coding recipes')
.option('-l, --list', 'List all available recipes')
.action((name, options) => {
if (options.list || !name) {
displayRecipeList();
} else {
const recipe = recipeManager.getRecipe(name);
if (recipe) {
console.log(`Running recipe: ${recipe.name}...`);
// Logic to pass tasks to the agent would go here
} else {
console.log(`Recipe "${name}" not found.`);
}
}
});

// Insights command - display local usage statistics
program
.command('insights')
.description('Display local usage statistics and task history')
.action(() => {
displayInsights();
});

// Config command - configure provider and API key
program
.command('config')
Expand Down
18 changes: 18 additions & 0 deletions docs/recipes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Recipe System

Recipes are pre-defined sets of tasks that Coderrr can execute.

## Usage
List available recipes:
`coderrr recipe --list`

Run a recipe:
`coderrr recipe <name>`
Comment on lines +6 to +10
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The command syntax shown in the documentation uses backticks for inline code formatting, but the commands are split across two lines without proper markdown formatting. Line 7 shows coderrr recipe --list as inline code, which is correct. However, lines 9-10 split the command explanation across two lines in a way that might be confusing. Consider reformatting to match line 6-7's pattern, using backticks around the command on a single line for consistency.

Suggested change
List available recipes:
`coderrr recipe --list`
Run a recipe:
`coderrr recipe <name>`
List available recipes: `coderrr recipe --list`
Run a recipe: `coderrr recipe <name>`

Copilot uses AI. Check for mistakes.

## Creating your own
Save a `.json` file in `~/.coderrr/recipes/`:
```json
{
"name": "Quick Express",
"tasks": ["Initialize npm", "Install express", "Create app.js"]
}
Comment on lines +13 to +18
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation doesn't mention that the JSON filename (e.g., "quick-express.json") serves as the recipe ID used to run it. Based on the implementation in recipeManager.js (line 29) and bin/coderrr.js (line 48), users would run coderrr recipe quick-express (using the filename without .json extension), not coderrr recipe "Quick Express" (using the "name" field). Consider adding a note explaining that the filename becomes the recipe ID, such as: "Note: The filename (without .json) will be the ID used to run the recipe, e.g., save as quick-express.json and run with coderrr recipe quick-express."

Copilot uses AI. Check for mistakes.
Comment on lines +14 to +18
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation file is missing a closing brace for the JSON code block. The JSON example that starts on line 14 with three backticks and "json" is never closed with three closing backticks. This will cause the markdown to render incorrectly. Add a line with three backticks (```) after line 18 to properly close the code block.

Copilot uses AI. Check for mistakes.
42 changes: 42 additions & 0 deletions src/recipeManager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
const fs = require('fs');
const path = require('path');
const os = require('os');

const RECIPES_DIR = path.join(os.homedir(), '.coderrr', 'recipes');

class RecipeManager {
constructor() {
this.ensureDirectory();
}

ensureDirectory() {
if (!fs.existsSync(RECIPES_DIR)) {
fs.mkdirSync(RECIPES_DIR, { recursive: true });
// Add a default "Hello World" recipe
const defaultRecipe = {
name: "ping",
description: "A simple health check for the recipe system",
tasks: ["Create a file named ALIVE.md with the text 'Coderrr is here'"]
};
fs.writeFileSync(path.join(RECIPES_DIR, 'ping.json'), JSON.stringify(defaultRecipe, null, 2));
}
}

listRecipes() {
const files = fs.readdirSync(RECIPES_DIR).filter(f => f.endsWith('.json'));
return files.map(f => {
const content = JSON.parse(fs.readFileSync(path.join(RECIPES_DIR, f), 'utf8'));
return { id: f.replace('.json', ''), ...content };
});
Comment on lines +27 to +30
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The listRecipes() method will throw an error if any JSON file in the recipes directory is malformed. Unlike other managers in the codebase (e.g., configManager.js lines 31-40 and insights.js lines 40-42), there's no try-catch error handling. If a user creates an invalid JSON file, this will crash the application. Consider wrapping the file reading and parsing in a try-catch block and either skipping invalid recipes or returning an error structure for each failed recipe.

Suggested change
return files.map(f => {
const content = JSON.parse(fs.readFileSync(path.join(RECIPES_DIR, f), 'utf8'));
return { id: f.replace('.json', ''), ...content };
});
return files.reduce((recipes, f) => {
try {
const content = JSON.parse(fs.readFileSync(path.join(RECIPES_DIR, f), 'utf8'));
recipes.push({ id: f.replace('.json', ''), ...content });
} catch (error) {
// Skip malformed recipe files to avoid crashing the application
}
return recipes;
}, []);

Copilot uses AI. Check for mistakes.
}

getRecipe(name) {
const filePath = path.join(RECIPES_DIR, `${name}.json`);
if (fs.existsSync(filePath)) {
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
}
return null;
Comment on lines +35 to +38
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The getRecipe() method will throw an error if the recipe file contains malformed JSON. Similar to listRecipes(), there's no error handling. Following the error handling pattern seen in configManager.js (lines 31-40), consider wrapping the file read and JSON parse operations in a try-catch block and returning null on error, which is already the expected return value when a file doesn't exist.

Suggested change
if (fs.existsSync(filePath)) {
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
}
return null;
if (!fs.existsSync(filePath)) {
return null;
}
try {
const content = fs.readFileSync(filePath, 'utf8');
return JSON.parse(content);
} catch (error) {
// If the recipe file is malformed or unreadable, treat it as missing
return null;
}

Copilot uses AI. Check for mistakes.
}
}

module.exports = new RecipeManager();
19 changes: 19 additions & 0 deletions src/recipeUI.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
const chalk = require('chalk');
const recipeManager = require('./recipeManager');

function displayRecipeList() {
const recipes = recipeManager.listRecipes();
console.log('\n' + chalk.magenta.bold('📜 AVAILABLE CODERRR RECIPES'));
console.log(chalk.gray('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));

if (recipes.length === 0) {
console.log(chalk.yellow('No recipes found in ~/.coderrr/recipes'));
} else {
recipes.forEach(r => {
console.log(`${chalk.cyan.bold(r.id)}: ${chalk.white(r.description || 'No description')}`);
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The UI displays r.description || 'No description', but the recipeValidator.js doesn't require the "description" field. This creates an inconsistency where custom recipes without descriptions will show "No description", but the validator won't flag this as an issue. Consider either making "description" optional in the validator with a default value, or adding a recommendation for including descriptions in the documentation.

Suggested change
console.log(`${chalk.cyan.bold(r.id)}: ${chalk.white(r.description || 'No description')}`);
const hasDescription = typeof r.description === 'string' && r.description.trim().length > 0;
if (hasDescription) {
console.log(`${chalk.cyan.bold(r.id)}: ${chalk.white(r.description)}`);
} else {
console.log(chalk.cyan.bold(r.id));
}

Copilot uses AI. Check for mistakes.
});
}
console.log(chalk.gray('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'));
}

module.exports = { displayRecipeList };
16 changes: 16 additions & 0 deletions src/utils/recipeValidator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* Validates the structure of a custom recipe
*/
const validateRecipe = (recipe) => {
const errors = [];
if (!recipe.name) errors.push("Missing 'name' field");
if (!Array.isArray(recipe.tasks) || recipe.tasks.length === 0) {
errors.push("'tasks' must be a non-empty array");
}
Comment on lines +2 to +9
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The validator checks for required fields "name" and "tasks", but the default recipe created in recipeManager.js also includes a "description" field. Additionally, according to the PR description and CLI implementation (bin/coderrr.js line 48-54), recipes are retrieved and executed by their ID (filename without .json), not by their "name" field. Consider either: (1) adding an "id" field requirement to the validator, or (2) clarifying that "name" serves as the display name while the filename serves as the ID. The validator is currently unused in the codebase, which means custom recipes won't be validated when loaded.

Suggested change
* Validates the structure of a custom recipe
*/
const validateRecipe = (recipe) => {
const errors = [];
if (!recipe.name) errors.push("Missing 'name' field");
if (!Array.isArray(recipe.tasks) || recipe.tasks.length === 0) {
errors.push("'tasks' must be a non-empty array");
}
* Validates the structure of a custom recipe.
*
* Notes:
* - `name` is the human-readable display name for the recipe.
* - The recipe "ID" is derived from the recipe filename (without `.json`),
* not from any field within the recipe object.
* - `description` is optional, but when present it should be a string.
*/
const validateRecipe = (recipe) => {
const errors = [];
// Basic shape check
if (!recipe || typeof recipe !== 'object') {
errors.push("Recipe must be an object");
} else {
// Validate display name
if (typeof recipe.name !== 'string' || recipe.name.trim().length === 0) {
errors.push("Missing or invalid 'name' field (expected non-empty string)");
}
// Validate tasks array
if (!Array.isArray(recipe.tasks) || recipe.tasks.length === 0) {
errors.push("'tasks' must be a non-empty array");
}
// Validate optional description
if (Object.prototype.hasOwnProperty.call(recipe, 'description') &&
typeof recipe.description !== 'string') {
errors.push("If provided, 'description' must be a string");
}
}

Copilot uses AI. Check for mistakes.
return {
valid: errors.length === 0,
errors
};
};

module.exports = { validateRecipe };
10 changes: 10 additions & 0 deletions test/recipes.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const recipeManager = require('../src/recipeManager');

describe('Recipe System', () => {
test('should find the default ping recipe', () => {
const recipes = recipeManager.listRecipes();
const ping = recipes.find(r => r.id === 'ping');
expect(ping).toBeDefined();
expect(ping.name).toBe('ping');
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test expects the "ping" recipe to have a "name" field set to "ping", but according to the RecipeManager implementation (line 17 in recipeManager.js), the recipe object has a "name" field set to "ping". However, the test is checking ping.name which comes from the object returned by listRecipes(). The listRecipes() method spreads the content object after setting the "id" property (line 29: { id: f.replace('.json', ''), ...content }), so the recipe should have both "id" and "name" fields. This test should pass, but it's testing for the "name" field when the "id" field is what was used to find the recipe. Consider testing ping.id instead since that's the field used in the find condition, or test both fields for completeness.

Copilot uses AI. Check for mistakes.
});
});
Comment on lines +3 to +10
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test coverage for the recipe system is minimal. The test only verifies that the default "ping" recipe exists and has a name field. Following the pattern in insights.test.js which tests the full module functionality, consider adding tests for: (1) listing recipes when the directory is empty or doesn't exist, (2) listing multiple recipes, (3) retrieving a specific recipe with getRecipe(), (4) handling malformed JSON files gracefully, and (5) verifying the recipe validator functionality. This would provide more comprehensive coverage of the new feature.

Copilot uses AI. Check for mistakes.
Loading