claude-code-templates
Version:
CLI tool to setup Claude Code configurations with framework-specific commands, automation hooks and MCP Servers for your projects
646 lines (541 loc) • 22.2 kB
JavaScript
const fs = require('fs-extra');
const path = require('path');
const os = require('os');
const chalk = require('chalk');
const ora = require('ora');
// Global agents directory
const GLOBAL_AGENTS_DIR = path.join(os.homedir(), '.claude-code-templates');
const AGENTS_DIR = path.join(GLOBAL_AGENTS_DIR, 'agents');
const LOCAL_BIN_DIR = path.join(GLOBAL_AGENTS_DIR, 'bin');
// Try to use system bin directory for immediate availability
const SYSTEM_BIN_DIR = '/usr/local/bin';
const isSystemWritable = () => {
try {
const testFile = path.join(SYSTEM_BIN_DIR, '.test-write');
require('fs').writeFileSync(testFile, 'test', 'utf8');
require('fs').unlinkSync(testFile);
return true;
} catch (error) {
return false;
}
};
// Choose the best bin directory
const BIN_DIR = isSystemWritable() ? SYSTEM_BIN_DIR : LOCAL_BIN_DIR;
/**
* Create a global agent that can be executed from anywhere
*/
async function createGlobalAgent(agentName, options = {}) {
console.log(chalk.blue(`🤖 Creating global agent: ${agentName}`));
try {
// Ensure directories exist
await fs.ensureDir(AGENTS_DIR);
await fs.ensureDir(LOCAL_BIN_DIR); // Always ensure local bin exists for backups
if (BIN_DIR === SYSTEM_BIN_DIR) {
console.log(chalk.green('🌍 Installing to system directory (immediately available)'));
} else {
console.log(chalk.yellow('⚠️ Installing to user directory (requires PATH setup)'));
await fs.ensureDir(BIN_DIR);
}
// Download agent from GitHub
const spinner = ora('Downloading agent from GitHub...').start();
let githubUrl;
if (agentName.includes('/')) {
// Category/agent format
githubUrl = `https://raw.githubusercontent.com/davila7/claude-code-templates/main/cli-tool/components/agents/${agentName}.md`;
} else {
// Direct agent format - try to find it in any category
githubUrl = await findAgentUrl(agentName);
if (!githubUrl) {
spinner.fail(`Agent "${agentName}" not found`);
await showAvailableAgents();
return;
}
}
const response = await fetch(githubUrl);
if (!response.ok) {
spinner.fail(`Failed to download agent: HTTP ${response.status}`);
if (response.status === 404) {
await showAvailableAgents();
}
return;
}
const agentContent = await response.text();
spinner.succeed('Agent downloaded successfully');
// Extract agent name for file/executable naming
const executableName = agentName.includes('/') ?
agentName.split('/')[1] : agentName;
// Save agent content
const agentFile = path.join(AGENTS_DIR, `${executableName}.md`);
await fs.writeFile(agentFile, agentContent, 'utf8');
// Generate executable script
await generateExecutableScript(executableName, agentFile);
console.log(chalk.green(`✅ Global agent '${executableName}' created successfully!`));
console.log(chalk.cyan('📦 Usage:'));
console.log(chalk.white(` ${executableName} "your prompt here"`));
if (BIN_DIR === SYSTEM_BIN_DIR) {
console.log(chalk.green('🎉 Ready to use immediately! No setup required.'));
console.log(chalk.gray('💡 Works in scripts, npm tasks, CI/CD, etc.'));
} else {
// Add to PATH (first time setup) only for user directory
await addToPath();
console.log(chalk.yellow('🔄 Restart your terminal or run:'));
console.log(chalk.gray(' source ~/.bashrc # for bash'));
console.log(chalk.gray(' source ~/.zshrc # for zsh'));
}
} catch (error) {
console.log(chalk.red(`❌ Error creating global agent: ${error.message}`));
}
}
/**
* List installed global agents
*/
async function listGlobalAgents(options = {}) {
console.log(chalk.blue('📋 Installed Global Agents:'));
try {
// Check both system and local bin directories
let systemAgents = [];
if (await fs.pathExists(SYSTEM_BIN_DIR)) {
const systemFiles = await fs.readdir(SYSTEM_BIN_DIR);
for (const file of systemFiles) {
if (!file.startsWith('.') && await fs.pathExists(path.join(AGENTS_DIR, `${file}.md`))) {
systemAgents.push(file);
}
}
}
const localAgents = await fs.pathExists(LOCAL_BIN_DIR) ?
(await fs.readdir(LOCAL_BIN_DIR)).filter(file => !file.startsWith('.')) : [];
const allAgents = [...new Set([...systemAgents, ...localAgents])];
if (allAgents.length === 0) {
console.log(chalk.yellow('⚠️ No global agents installed yet.'));
console.log(chalk.gray('💡 Create one with: npx claude-code-templates@latest --create-agent <agent-name>'));
return;
}
console.log(chalk.green(`\n✅ Found ${allAgents.length} global agent(s):\n`));
for (const agent of allAgents) {
// Check which directory has the agent
const systemPath = path.join(SYSTEM_BIN_DIR, agent);
const localPath = path.join(LOCAL_BIN_DIR, agent);
let agentPath, location;
if (await fs.pathExists(systemPath)) {
agentPath = systemPath;
location = '🌍 system';
} else {
agentPath = localPath;
location = '👤 user';
}
const stats = await fs.stat(agentPath);
const isExecutable = (stats.mode & parseInt('111', 8)) !== 0;
console.log(chalk.cyan(` ${isExecutable ? '✅' : '❌'} ${agent} (${location})`));
console.log(chalk.gray(` Usage: ${agent} "your prompt"`));
console.log(chalk.gray(` Created: ${stats.birthtime.toLocaleDateString()}`));
console.log('');
}
console.log(chalk.blue('🌟 Global Usage:'));
console.log(chalk.gray(' • Run from any directory: <agent-name> "prompt"'));
console.log(chalk.gray(' • List agents: npx claude-code-templates@latest --list-agents'));
console.log(chalk.gray(' • Remove agent: npx claude-code-templates@latest --remove-agent <name>'));
} catch (error) {
console.log(chalk.red(`❌ Error listing agents: ${error.message}`));
}
}
/**
* Remove a global agent
*/
async function removeGlobalAgent(agentName, options = {}) {
console.log(chalk.blue(`🗑️ Removing global agent: ${agentName}`));
try {
const systemExecutablePath = path.join(SYSTEM_BIN_DIR, agentName);
const localExecutablePath = path.join(LOCAL_BIN_DIR, agentName);
const agentPath = path.join(AGENTS_DIR, `${agentName}.md`);
let removed = false;
// Remove from system directory
if (await fs.pathExists(systemExecutablePath)) {
await fs.remove(systemExecutablePath);
console.log(chalk.green(`✅ Removed system executable: ${agentName}`));
removed = true;
}
// Remove from local directory
if (await fs.pathExists(localExecutablePath)) {
await fs.remove(localExecutablePath);
console.log(chalk.green(`✅ Removed local executable: ${agentName}`));
removed = true;
}
// Remove agent file
if (await fs.pathExists(agentPath)) {
await fs.remove(agentPath);
console.log(chalk.green(`✅ Removed agent file: ${agentName}.md`));
removed = true;
}
if (!removed) {
console.log(chalk.yellow(`⚠️ Agent '${agentName}' not found.`));
console.log(chalk.gray('💡 List available agents with: --list-agents'));
return;
}
console.log(chalk.green(`🎉 Global agent '${agentName}' removed successfully!`));
} catch (error) {
console.log(chalk.red(`❌ Error removing agent: ${error.message}`));
}
}
/**
* Update a global agent
*/
async function updateGlobalAgent(agentName, options = {}) {
console.log(chalk.blue(`🔄 Updating global agent: ${agentName}`));
try {
const executablePath = path.join(BIN_DIR, agentName);
if (!await fs.pathExists(executablePath)) {
console.log(chalk.yellow(`⚠️ Agent '${agentName}' not found.`));
console.log(chalk.gray('💡 Create it with: --create-agent <agent-name>'));
return;
}
// Re-download and recreate
console.log(chalk.gray('🔄 Re-downloading latest version...'));
await createGlobalAgent(agentName, { ...options, update: true });
} catch (error) {
console.log(chalk.red(`❌ Error updating agent: ${error.message}`));
}
}
/**
* Generate executable script for an agent
*/
async function generateExecutableScript(agentName, agentFile) {
const scriptContent = `#!/usr/bin/env node
const { execSync } = require('child_process');
const path = require('path');
const fs = require('fs');
// Check if Claude CLI is available
function checkClaudeCLI() {
try {
execSync('claude --version', { stdio: 'ignore' });
return true;
} catch (error) {
return false;
}
}
// Read agent system prompt
const agentPath = '${agentFile}';
if (!fs.existsSync(agentPath)) {
console.error('❌ Agent file not found:', agentPath);
process.exit(1);
}
const rawSystemPrompt = fs.readFileSync(agentPath, 'utf8');
// Remove YAML front matter if present to get clean system prompt
const systemPrompt = rawSystemPrompt.replace(/^---[\\s\\S]*?---\\n/, '').trim();
// Parse arguments and detect context
const args = process.argv.slice(2);
let userInput = '';
let explicitFiles = [];
let explicitDirs = [];
let autoDetect = true;
// Parse command line arguments
let verbose = false;
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg === '--file' && i + 1 < args.length) {
explicitFiles.push(args[++i]);
autoDetect = false; // Disable auto-detect when explicit files provided
} else if (arg === '--dir' && i + 1 < args.length) {
explicitDirs.push(args[++i]);
autoDetect = false;
} else if (arg === '--no-auto') {
autoDetect = false;
} else if (arg === '--verbose' || arg === '-v') {
verbose = true;
} else if (arg === '--help' || arg === '-h') {
console.log('Usage: ${agentName} [options] "your prompt"');
console.log('');
console.log('Context Options:');
console.log(' [default] Auto-detect project files (smart context)');
console.log(' --file <path> Include specific file');
console.log(' --dir <path> Include specific directory');
console.log(' --no-auto Disable auto-detection');
console.log(' --verbose, -v Enable verbose debugging output');
console.log('');
console.log('Examples:');
console.log(' ${agentName} "review for security issues" # Auto-detect');
console.log(' ${agentName} --file auth.js "check this file" # Specific file');
console.log(' ${agentName} --no-auto "general advice" # No context');
process.exit(0);
} else if (!arg.startsWith('--')) {
userInput += arg + ' ';
}
}
userInput = userInput.trim();
if (!userInput) {
console.error('❌ Please provide a prompt');
console.error('Usage: ${agentName} [options] "your prompt"');
process.exit(1);
}
// Auto-detect project context if enabled
let contextPrompt = '';
if (autoDetect && explicitFiles.length === 0 && explicitDirs.length === 0) {
const fs = require('fs');
const path = require('path');
const cwd = process.cwd();
// Detect project type and relevant files
let projectType = 'unknown';
let relevantFiles = [];
// Check for common project indicators
if (fs.existsSync('package.json')) {
projectType = 'javascript/node';
const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'));
// Add framework detection
if (packageJson.dependencies?.react || packageJson.devDependencies?.react) {
projectType = 'react';
} else if (packageJson.dependencies?.vue || packageJson.devDependencies?.vue) {
projectType = 'vue';
} else if (packageJson.dependencies?.next || packageJson.devDependencies?.next) {
projectType = 'nextjs';
}
// Common files to include for JS projects
const jsFiles = ['src/', 'lib/', 'components/', 'pages/', 'api/', 'routes/'];
jsFiles.forEach(dir => {
if (fs.existsSync(dir)) relevantFiles.push(dir);
});
} else if (fs.existsSync('requirements.txt') || fs.existsSync('pyproject.toml')) {
projectType = 'python';
relevantFiles = ['*.py', 'src/', 'app/', 'api/'];
} else if (fs.existsSync('Cargo.toml')) {
projectType = 'rust';
relevantFiles = ['src/', 'Cargo.toml'];
} else if (fs.existsSync('go.mod')) {
projectType = 'go';
relevantFiles = ['*.go', 'cmd/', 'internal/', 'pkg/'];
}
// Build context prompt
if (projectType !== 'unknown') {
contextPrompt = \`
📁 PROJECT CONTEXT:
- Project type: \${projectType}
- Working directory: \${path.basename(cwd)}
- Auto-detected relevant files/folders: \${relevantFiles.join(', ')}
Please analyze the \${userInput} in the context of this \${projectType} project. You have access to read any files in the current directory using the Read tool.\`;
}
}
// Check Claude CLI availability
if (!checkClaudeCLI()) {
console.error('❌ Claude CLI not found in PATH');
console.error('💡 Install Claude CLI: https://claude.ai/code');
console.error('💡 Or install via npm: npm install -g @anthropic-ai/claude-code');
process.exit(1);
}
// Escape quotes in system prompt for shell execution
const escapedSystemPrompt = systemPrompt.replace(/"/g, '\\\\"').replace(/\`/g, '\\\\\`');
// Build final prompt with context
const finalPrompt = userInput + contextPrompt;
const escapedFinalPrompt = finalPrompt.replace(/"/g, '\\\\"').replace(/\`/g, '\\\\\`');
// Build Claude command with SDK - use --system-prompt instead of --append-system-prompt for better control
const claudeCmd = \`claude -p "\${escapedFinalPrompt}" --system-prompt "\${escapedSystemPrompt}"\${verbose ? ' --verbose' : ''}\`;
// Debug output if verbose
if (verbose) {
console.log('\\n🔍 DEBUG MODE - Command Details:');
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
console.log('📝 User Input:', userInput);
console.log('📁 Project Context:', contextPrompt ? 'Auto-detected' : 'None');
console.log('🎯 Final Prompt Length:', finalPrompt.length, 'characters');
console.log('🤖 System Prompt Preview:', systemPrompt.substring(0, 150) + '...');
console.log('⚡ Claude Command:', claudeCmd);
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n');
}
// Show loading indicator
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
let currentFrame = 0;
const agentDisplayName = '${agentName}'.replace(/-/g, ' ').replace(/\\b\\w/g, l => l.toUpperCase());
console.error(\`\\n🤖 \${agentDisplayName} is thinking...\`);
const loader = setInterval(() => {
process.stderr.write(\`\\r\${frames[currentFrame]} Claude Code is working... \`);
currentFrame = (currentFrame + 1) % frames.length;
}, 100);
try {
// Execute Claude with the agent's system prompt
execSync(claudeCmd, {
stdio: ['inherit', 'inherit', 'pipe'],
cwd: process.cwd()
});
// Clear loader
clearInterval(loader);
process.stderr.write('\\r✅ Response ready!\\n\\n');
} catch (error) {
clearInterval(loader);
process.stderr.write('\\r');
console.error('❌ Error executing Claude:', error.message);
process.exit(1);
}
`;
const scriptPath = path.join(BIN_DIR, agentName);
await fs.writeFile(scriptPath, scriptContent, 'utf8');
// Make executable (Unix/Linux/macOS)
if (process.platform !== 'win32') {
await fs.chmod(scriptPath, 0o755);
}
}
/**
* Add global agents bin directory to PATH
*/
async function addToPath() {
const shell = process.env.SHELL || '';
const isWindows = process.platform === 'win32';
if (isWindows) {
// Windows PATH management
console.log(chalk.yellow('🪟 Windows detected:'));
console.log(chalk.gray(`Add this to your PATH: ${BIN_DIR}`));
console.log(chalk.gray('Or run this in PowerShell as Administrator:'));
console.log(chalk.white(`[Environment]::SetEnvironmentVariable("Path", $env:Path + ";${BIN_DIR}", "User")`));
return;
}
// Unix-like systems
const pathExport = `export PATH="${BIN_DIR}:$PATH"`;
// Determine shell config files to update
const configFiles = [];
if (shell.includes('bash') || !shell) {
configFiles.push(path.join(os.homedir(), '.bashrc'));
configFiles.push(path.join(os.homedir(), '.bash_profile'));
}
if (shell.includes('zsh')) {
configFiles.push(path.join(os.homedir(), '.zshrc'));
}
if (shell.includes('fish')) {
const fishConfigDir = path.join(os.homedir(), '.config', 'fish');
await fs.ensureDir(fishConfigDir);
configFiles.push(path.join(fishConfigDir, 'config.fish'));
}
// Add default files if shell not detected
if (configFiles.length === 0) {
configFiles.push(path.join(os.homedir(), '.bashrc'));
configFiles.push(path.join(os.homedir(), '.zshrc'));
}
// Check if PATH is already added
let alreadyInPath = false;
for (const configFile of configFiles) {
if (await fs.pathExists(configFile)) {
const content = await fs.readFile(configFile, 'utf8');
if (content.includes(BIN_DIR)) {
alreadyInPath = true;
break;
}
}
}
if (alreadyInPath) {
console.log(chalk.green('✅ PATH already configured'));
return;
}
// Add to PATH in config files
console.log(chalk.blue('🔧 Adding to PATH...'));
for (const configFile of configFiles) {
try {
let content = '';
if (await fs.pathExists(configFile)) {
content = await fs.readFile(configFile, 'utf8');
}
// Add PATH export if not already present
if (!content.includes(BIN_DIR)) {
const newContent = content + `\n# Claude Code Templates - Global Agents\n${pathExport}\n`;
await fs.writeFile(configFile, newContent, 'utf8');
console.log(chalk.green(`✅ Updated ${path.basename(configFile)}`));
}
} catch (error) {
console.log(chalk.yellow(`⚠️ Could not update ${configFile}: ${error.message}`));
}
}
}
/**
* Find agent URL by searching in all categories
*/
async function findAgentUrl(agentName) {
try {
// First try root level
const rootUrl = `https://raw.githubusercontent.com/davila7/claude-code-templates/main/cli-tool/components/agents/${agentName}.md`;
const rootResponse = await fetch(rootUrl);
if (rootResponse.ok) {
return rootUrl;
}
// Search in categories
const categoriesResponse = await fetch('https://api.github.com/repos/davila7/claude-code-templates/contents/cli-tool/components/agents');
if (!categoriesResponse.ok) {
return null;
}
const contents = await categoriesResponse.json();
for (const item of contents) {
if (item.type === 'dir') {
const categoryUrl = `https://raw.githubusercontent.com/davila7/claude-code-templates/main/cli-tool/components/agents/${item.name}/${agentName}.md`;
try {
const categoryResponse = await fetch(categoryUrl);
if (categoryResponse.ok) {
return categoryUrl;
}
} catch (error) {
// Continue searching
}
}
}
return null;
} catch (error) {
return null;
}
}
/**
* Show available agents for user selection
*/
async function showAvailableAgents() {
console.log(chalk.yellow('\n📋 Available Agents:'));
console.log(chalk.gray('Use format: category/agent-name or just agent-name\n'));
try {
const response = await fetch('https://api.github.com/repos/davila7/claude-code-templates/contents/cli-tool/components/agents');
if (!response.ok) {
console.log(chalk.red('❌ Could not fetch available agents from GitHub'));
return;
}
const contents = await response.json();
const agents = [];
for (const item of contents) {
if (item.type === 'file' && item.name.endsWith('.md')) {
agents.push({ name: item.name.replace('.md', ''), category: 'root' });
} else if (item.type === 'dir') {
try {
const categoryResponse = await fetch(`https://api.github.com/repos/davila7/claude-code-templates/contents/cli-tool/components/agents/${item.name}`);
if (categoryResponse.ok) {
const categoryContents = await categoryResponse.json();
for (const categoryItem of categoryContents) {
if (categoryItem.type === 'file' && categoryItem.name.endsWith('.md')) {
agents.push({
name: categoryItem.name.replace('.md', ''),
category: item.name,
path: `${item.name}/${categoryItem.name.replace('.md', '')}`
});
}
}
}
} catch (error) {
// Skip category on error
}
}
}
// Group by category
const grouped = agents.reduce((acc, agent) => {
const category = agent.category === 'root' ? '🤖 General' : `📁 ${agent.category}`;
if (!acc[category]) acc[category] = [];
acc[category].push(agent);
return acc;
}, {});
Object.entries(grouped).forEach(([category, categoryAgents]) => {
console.log(chalk.cyan(category));
categoryAgents.forEach(agent => {
const displayName = agent.path || agent.name;
console.log(chalk.gray(` • ${displayName}`));
});
console.log('');
});
console.log(chalk.blue('Examples:'));
console.log(chalk.gray(' npx claude-code-templates@latest --create-agent api-security-audit'));
console.log(chalk.gray(' npx claude-code-templates@latest --create-agent deep-research-team/academic-researcher'));
} catch (error) {
console.log(chalk.red('❌ Error fetching agents:', error.message));
}
}
module.exports = {
createGlobalAgent,
listGlobalAgents,
removeGlobalAgent,
updateGlobalAgent,
showAvailableAgents
};