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>`

## 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 +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 JSON example code block is not closed with a matching fence, which will cause the Markdown renderer to treat the rest of the document as part of the code block. Please add a closing ``` line after the JSON snippet so the usage text renders correctly.

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
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 comment says this creates a default "Hello World" recipe, but the actual recipe is named "ping" and has a different description. Please update the comment to match the behavior so future readers are not misled.

Suggested change
// Add a default "Hello World" recipe
// Add a default "ping" health check recipe

Copilot uses AI. Check for mistakes.
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 };
});
}

getRecipe(name) {
const filePath = path.join(RECIPES_DIR, `${name}.json`);
if (fs.existsSync(filePath)) {
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
Comment on lines +27 to +36
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.

JSON.parse is called on each recipe file without any error handling, so a single malformed or partially-written JSON file in the recipes directory will throw and break listRecipes() entirely. Consider wrapping this read/parse in a try/catch and either skipping invalid recipes or surfacing a clear validation error, potentially reusing the recipe validator utility.

Suggested change
return files.map(f => {
const content = JSON.parse(fs.readFileSync(path.join(RECIPES_DIR, f), 'utf8'));
return { id: f.replace('.json', ''), ...content };
});
}
getRecipe(name) {
const filePath = path.join(RECIPES_DIR, `${name}.json`);
if (fs.existsSync(filePath)) {
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
const recipes = [];
for (const f of files) {
const filePath = path.join(RECIPES_DIR, f);
try {
const raw = fs.readFileSync(filePath, 'utf8');
const content = JSON.parse(raw);
recipes.push({ id: f.replace('.json', ''), ...content });
} catch (error) {
console.error(`Failed to read or parse recipe file "${f}": ${error.message}`);
}
}
return recipes;
}
getRecipe(name) {
const filePath = path.join(RECIPES_DIR, `${name}.json`);
if (fs.existsSync(filePath)) {
try {
const raw = fs.readFileSync(filePath, 'utf8');
return JSON.parse(raw);
} catch (error) {
throw new Error(`Failed to read or parse recipe "${name}": ${error.message}`);
}

Copilot uses AI. Check for mistakes.
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.

getRecipe also calls JSON.parse without guarding against invalid JSON, which means a corrupt recipe file will cause the CLI command to throw instead of returning null or a structured error. It would be safer to catch parse errors here and either return null or propagate a user-friendly error, ideally using the shared recipe validation logic.

Suggested change
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
const content = fs.readFileSync(filePath, 'utf8');
try {
return JSON.parse(content);
} catch (error) {
// If the recipe file contains invalid JSON, treat it as unavailable
console.error(`Failed to parse recipe "${name}" from ${filePath}: ${error.message}`);
return null;
}

Copilot uses AI. Check for mistakes.
}
return null;
}
}

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')}`);
});
}
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");
}
return {
valid: errors.length === 0,
errors
};
};

module.exports = { validateRecipe };
Comment on lines +4 to +16
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.

This validateRecipe helper is not imported or used anywhere, so recipes are never actually validated before use even though the PR description says custom recipes are validated. Either wire this into RecipeManager/the CLI flow (e.g., when loading or running recipes) or remove it until it is integrated to avoid dead code and confusion.

Copilot uses AI. Check for mistakes.
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');
Comment on lines +3 to +8
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.

This test uses the real RecipeManager singleton, which writes to and reads from ~/.coderrr/recipes and assumes a default ping recipe exists. That couples test results to the developer's home directory state (e.g., if they delete or change their recipes the test will fail), so consider isolating it by mocking os.homedir/fs or allowing RecipeManager to be pointed at a temporary directory.

Copilot uses AI. Check for mistakes.
});
});
Loading