sf-agent-framework
Version:
AI Agent Orchestration Framework for Salesforce Development - Two-phase architecture with 70% context reduction
599 lines (535 loc) ⢠20.9 kB
JavaScript
const { Command } = require('commander');
const path = require('path');
const { execSync } = require('child_process');
const { version } = require('../package.json');
const program = new Command();
program
.name('sf-agent')
.description('SF-Agent Framework CLI for building and managing agents')
.version(version);
program
.command('install')
.description('Install SF-Agent Framework agents and tools interactively')
.option('-f, --full', 'Install complete SF-Agent Framework')
.option('-d, --directory <path>', 'Installation directory')
.option(
'-i, --ide <ide...>',
'Configure for specific IDE(s) - can specify multiple (cursor, claude-code, windsurf, trae, roo, cline, gemini, github-copilot, other)'
)
.action(async (options) => {
try {
// Use child_process to run the installer - this is more reliable for npm packages
const installerPath = path.join(__dirname, 'installer', 'bin', 'sf-agent.js');
// Build the command
let cmd = `node "${installerPath}" install`;
if (options.full) cmd += ' --full';
if (options.directory) cmd += ` --directory "${options.directory}"`;
if (options.ide && options.ide.length > 0) {
cmd += ` --ide ${options.ide.join(' ')}`;
}
// Execute the installer
execSync(cmd, { stdio: 'inherit' });
} catch (error) {
console.error('Installation failed:', error.message);
process.exit(1);
}
});
// Story-based context engineering commands
program
.command('shard <document>')
.description('Shard planning document into story-sized pieces')
.option('-o, --output <dir>', 'Output directory', 'docs/sharded')
.option('-v, --verbose', 'Verbose output')
.action(async (document, options) => {
try {
const sharderPath = path.join(__dirname, 'document-sharder.js');
const cmd = `node "${sharderPath}" "${document}" -o "${options.output}"${options.verbose ? ' -v' : ''}`;
execSync(cmd, { stdio: 'inherit' });
} catch (error) {
console.error('Sharding failed:', error.message);
process.exit(1);
}
});
program
.command('story <action> [storyId]')
.description('Manage story queue and context')
.option('-d, --dir <directory>', 'Story directory', 'docs/stories')
.action(async (action, storyId, options) => {
try {
const queuePath = path.join(__dirname, 'story-queue-manager.js');
let cmd = `node "${queuePath}" ${action}`;
if (storyId) cmd += ` ${storyId}`;
if (options.dir) cmd += ` -d "${options.dir}"`;
execSync(cmd, { stdio: 'inherit' });
} catch (error) {
console.error('Story management failed:', error.message);
process.exit(1);
}
});
program
.command('validate-story <storyId>')
.description('Validate story has complete context for development')
.option('--fix', 'Apply auto-fix suggestions')
.option('--block', 'Block story if invalid')
.action(async (storyId, options) => {
console.log(`Validating story ${storyId}...`);
// Story validation logic would go here
console.log('Story validation complete');
});
program
.command('phase <phase>')
.description('Switch between planning and development phases')
.action(async (phase) => {
if (!['planning', 'development'].includes(phase)) {
console.error('Phase must be either "planning" or "development"');
process.exit(1);
}
console.log(`Switching to ${phase} phase...`);
console.log(`Context limit: ${phase === 'planning' ? '128k' : '32k'} tokens`);
console.log(`Agent mode: ${phase === 'planning' ? 'Rich' : 'Lean'}`);
});
program
.command('workflow [workflowId]')
.description('Start an interactive workflow session')
.option('-d, --dir <directory>', 'Workflow directory', 'sf-core/workflows')
.option(
'-o, --output <directory>',
'Output directory for session files',
'docs/workflow-sessions'
)
.option('-v, --verbose', 'Verbose output')
.action(async (workflowId, options) => {
try {
const InteractiveWorkflowPlanner = require('./lib/interactive-workflow-planner');
const planner = new InteractiveWorkflowPlanner(process.cwd());
if (!workflowId) {
// List available workflows
console.log('\nš Available Workflows:\n');
const workflows = await planner.listWorkflows();
if (workflows.length === 0) {
console.log('No workflows found. Creating default interactive workflow...');
workflowId = 'interactive-project-setup';
} else {
workflows.forEach((w) => console.log(` - ${w}`));
console.log('\nRun: sf-agent workflow <workflow-name> to start\n');
return;
}
}
// Start the interactive workflow
const result = await planner.startInteractiveWorkflow(workflowId);
// Display summary
console.log('\nā
Workflow Completed!\n');
console.log('Summary:', JSON.stringify(result, null, 2));
// Save session results if output directory specified
if (options.output) {
const fs = require('fs').promises;
const outputPath = path.join(options.output, `${workflowId}-${Date.now()}.json`);
await fs.mkdir(options.output, { recursive: true });
await fs.writeFile(outputPath, JSON.stringify(result, null, 2));
console.log(`\nSession saved to: ${outputPath}`);
}
} catch (error) {
console.error('Workflow execution failed:', error.message);
process.exit(1);
}
});
program
.command('handoff <action> [handoffId]')
.description('Manage agent handoffs (create, accept, complete, status)')
.option('--from <agent>', 'Source agent (for create)')
.option('--to <agent>', 'Target agent (for create)')
.option('--agent <agent>', 'Agent ID (for accept)')
.option('--artifacts <paths...>', 'Artifact paths')
.option('--notes <text>', 'Notes')
.option('--next <agent>', 'Next agent in chain (for complete)')
.action(async (action, handoffId, options) => {
try {
const AgentCollaborationProtocol = require('./lib/agent-collaboration-protocol');
const protocol = new AgentCollaborationProtocol(process.cwd());
await protocol.initialize();
switch (action) {
case 'create':
if (!options.from || !options.to) {
console.error('Error: --from and --to are required for create');
process.exit(1);
}
const handoff = await protocol.createHandoff({
from: options.from,
to: options.to,
artifacts: options.artifacts || [],
context: { notes: options.notes },
});
console.log(`\nā
Handoff created: ${handoff.id}`);
console.log(`From: ${handoff.from} ā To: ${handoff.to}`);
break;
case 'accept':
if (!handoffId || !options.agent) {
console.error('Error: handoffId and --agent are required for accept');
process.exit(1);
}
const accepted = await protocol.acceptHandoff(handoffId, options.agent);
console.log(`\nā
Handoff ${handoffId} accepted by ${options.agent}`);
console.log(`Artifacts loaded: ${accepted.artifacts.length}`);
break;
case 'complete':
if (!handoffId || !options.agent) {
console.error('Error: handoffId and --agent are required for complete');
process.exit(1);
}
const deliverables =
options.artifacts?.map((a) => ({ name: a, content: `Deliverable: ${a}` })) || [];
const completed = await protocol.completeHandoff(handoffId, options.agent, deliverables);
console.log(`\nā
Handoff ${handoffId} completed`);
console.log(`Deliverables: ${deliverables.length}`);
break;
case 'status':
if (handoffId) {
const status = protocol.getHandoffStatus(handoffId);
console.log(`\nHandoff ${handoffId}:`);
console.log(JSON.stringify(status, null, 2));
} else {
const handoffs = protocol.listActiveHandoffs(options.agent);
console.log(`\nActive Handoffs${options.agent ? ` for ${options.agent}` : ''}:`);
handoffs.forEach((h) => {
console.log(` - ${h.id}: ${h.from} ā ${h.to} (${h.status})`);
});
}
break;
case 'metrics':
const metrics = protocol.getCollaborationMetrics();
console.log('\nš Collaboration Metrics:');
console.log(JSON.stringify(metrics, null, 2));
break;
default:
console.error(`Unknown action: ${action}`);
console.log('Available actions: create, accept, complete, status, metrics');
process.exit(1);
}
} catch (error) {
console.error('Handoff operation failed:', error.message);
process.exit(1);
}
});
program
.command('update')
.description('Update existing SF-Agent installation')
.option('--force', 'Force update, overwriting modified files')
.option('--dry-run', 'Show what would be updated without making changes')
.action(async (options) => {
try {
// Use child_process to run the installer - this is more reliable for npm packages
const installerPath = path.join(__dirname, 'installer', 'bin', 'sf-agent.js');
// Build the command
let cmd = `node "${installerPath}" update`;
if (options.force) cmd += ' --force';
if (options.dryRun) cmd += ' --dry-run';
// Execute the installer
execSync(cmd, { stdio: 'inherit' });
} catch (error) {
console.error('Update failed:', error.message);
process.exit(1);
}
});
program
.command('build')
.description('Build web bundles for agents and teams')
.option('-a, --agents-only', 'Build only agent bundles')
.option('-t, --teams-only', 'Build only team bundles')
.option('--no-clean', 'Skip cleaning output directories')
.action(async (options) => {
const WebBuilder = require('./builders/web-builder');
const builder = new WebBuilder({
rootDir: process.cwd(),
});
try {
if (options.clean) {
console.log('Cleaning output directories...');
await builder.cleanOutputDirs();
}
if (!options.teamsOnly) {
console.log('Building agent bundles...');
await builder.buildAgents();
}
if (!options.agentsOnly) {
console.log('Building team bundles...');
await builder.buildTeams();
}
console.log('Build completed successfully!');
} catch (error) {
console.error('Build failed:', error.message);
process.exit(1);
}
});
program
.command('list:agents')
.description('List all available agents')
.action(async () => {
const WebBuilder = require('./builders/web-builder');
const builder = new WebBuilder({ rootDir: process.cwd() });
const agents = await builder.resolver.listAgents();
console.log('Available agents:');
agents.forEach((agent) => console.log(` - ${agent}`));
});
program
.command('validate')
.description('Validate agent and team configurations')
.action(async () => {
const WebBuilder = require('./builders/web-builder');
const builder = new WebBuilder({ rootDir: process.cwd() });
try {
// Validate by attempting to build all agents and teams
const agents = await builder.resolver.listAgents();
const teams = await builder.resolver.listTeams();
console.log('Validating agents...');
for (const agent of agents) {
await builder.resolver.resolveAgentDependencies(agent);
console.log(` ā ${agent}`);
}
console.log('\nValidating teams...');
for (const team of teams) {
await builder.resolver.resolveTeamDependencies(team);
console.log(` ā ${team}`);
}
console.log('\nAll configurations are valid!');
} catch (error) {
console.error('Validation failed:', error.message);
process.exit(1);
}
});
program
.command('upgrade')
.description('Upgrade a SF-Agent-Method V3 project to V4')
.option('-p, --project <path>', 'Path to V3 project (defaults to current directory)')
.option('--dry-run', 'Show what would be changed without making changes')
.option('--no-backup', 'Skip creating backup (not recommended)')
.action(async (options) => {
const V3ToV4Upgrader = require('./upgraders/v3-to-v4-upgrader');
const upgrader = new V3ToV4Upgrader();
await upgrader.upgrade({
projectPath: options.project,
dryRun: options.dryRun,
backup: options.backup,
});
});
program
.command('mcp:setup')
.description('Set up MCP (Model Context Protocol) for your IDE')
.option(
'-i, --ide <ide>',
'Specific IDE to configure (cursor, claude-code, windsurf, github-copilot, vscode)'
)
.option('-a, --all', 'Configure MCP for all supported IDEs')
.option('-u, --universal', 'Create universal MCP configuration file')
.action(async (options) => {
const IdeSetup = require('./installer/lib/ide-setup');
const projectDir = process.cwd();
try {
if (options.universal || options.all) {
console.log('Creating universal MCP configuration...');
await IdeSetup.createUniversalMCPConfig(projectDir);
}
if (options.all) {
console.log('\nConfiguring MCP for all supported IDEs...');
const ides = ['cursor', 'claude-code', 'windsurf', 'github-copilot', 'vscode'];
for (const ide of ides) {
console.log(`\nConfiguring ${ide}...`);
switch (ide) {
case 'cursor':
await IdeSetup.setupCursorMCP(projectDir);
break;
case 'claude-code':
await IdeSetup.setupClaudeCodeMCP(projectDir);
break;
case 'windsurf':
await IdeSetup.setupWindsurfMCP(projectDir);
break;
case 'github-copilot':
await IdeSetup.setupGitHubCopilotMCP(projectDir);
break;
case 'vscode':
await IdeSetup.setupVSCodeMCP(projectDir);
break;
}
}
} else if (options.ide) {
console.log(`Configuring MCP for ${options.ide}...`);
switch (options.ide) {
case 'cursor':
await IdeSetup.setupCursorMCP(projectDir);
break;
case 'claude-code':
await IdeSetup.setupClaudeCodeMCP(projectDir);
break;
case 'windsurf':
await IdeSetup.setupWindsurfMCP(projectDir);
break;
case 'github-copilot':
await IdeSetup.setupGitHubCopilotMCP(projectDir);
break;
case 'vscode':
await IdeSetup.setupVSCodeMCP(projectDir);
break;
default:
console.error(`Unsupported IDE: ${options.ide}`);
console.log('Supported IDEs: cursor, claude-code, windsurf, github-copilot, vscode');
process.exit(1);
}
} else {
console.log('Please specify an IDE with --ide or use --all for all IDEs');
console.log('Example: npm run mcp:setup -- --ide cursor');
console.log('Example: npm run mcp:setup -- --all');
}
console.log('\nā MCP setup complete!');
console.log('See sf-core/mcp/MCP-IDE-GUIDE.md for usage instructions');
} catch (error) {
console.error('MCP setup failed:', error.message);
process.exit(1);
}
});
program
.command('mcp:list')
.description('List all available MCP servers in the project')
.action(async () => {
const IdeSetup = require('./installer/lib/ide-setup');
const projectDir = process.cwd();
const servers = await IdeSetup.discoverMCPServers(projectDir);
if (servers.length === 0) {
console.log('No MCP servers found in the project.');
console.log('To add MCP servers, create .js files in:');
console.log(' - .sf-core/mcp/servers/');
return;
}
console.log('Available MCP servers:');
servers.forEach((server) => {
console.log(`\n ${server.packName}/${server.serverName}`);
console.log(` Path: ${server.relativePath}`);
});
console.log(`\nTotal: ${servers.length} MCP servers found`);
});
program
.command('metrics:record <eventName> [data]')
.description('Record a metric event')
.action((eventName, data) => {
const { recordEvent } = require('./metrics-tracker');
let parsedData = null;
if (data) {
try {
parsedData = JSON.parse(data);
} catch (error) {
console.error('Invalid JSON data:', error.message);
process.exit(1);
}
}
recordEvent(eventName, parsedData);
});
program
.command('metrics:report')
.description('Generate a metrics report')
.action(() => {
const { generateReport } = require('./metrics-tracker');
generateReport();
});
program
.command('visualize:workflow <file>')
.description('Generate a Mermaid diagram for a workflow YAML file')
.action((file) => {
const visualizeWorkflow = require('./workflow-visualizer');
visualizeWorkflow(file);
});
program
.command('create-agent')
.description('Create a new agent interactively')
.action(() => {
try {
execSync('node tools/agent-creator.js', { stdio: 'inherit' });
} catch (error) {
console.error('Agent creation failed:', error.message);
process.exit(1);
}
});
program
.command('flatten')
.description('Flatten the codebase into a single XML file')
.action(() => {
try {
console.log('Flattening codebase...');
execSync('node tools/codebase-flattener.js', { stdio: 'inherit' });
console.log('Codebase flattened successfully!');
} catch (error) {
console.error('Codebase flattening failed:', error.message);
process.exit(1);
}
});
// Memory system commands
program
.command('memory <action>')
.description('Manage context memory system (init, store, retrieve, stats, patterns)')
.option('--query <text>', 'Query for retrieval')
.option('--type <type>', 'Context type')
.option('--agent <agent>', 'Agent name')
.option('--content <text>', 'Content to store')
.option('--limit <number>', 'Limit results', '10')
.action(async (action, options) => {
try {
const ContextMemorySystem = require('./lib/context-memory-system');
const memory = new ContextMemorySystem(process.cwd());
switch (action) {
case 'init':
await memory.initialize();
console.log('ā
Memory system initialized');
console.log(`Session ID: ${memory.currentSession.id}`);
break;
case 'store':
await memory.initialize();
const contextId = await memory.storeContext({
type: options.type || 'general',
agent: options.agent || 'user',
content: options.content || 'Test context',
metadata: {},
});
console.log(`ā
Context stored: ${contextId}`);
break;
case 'retrieve':
await memory.initialize();
const contexts = await memory.retrieveContext(options.query || '', {
limit: parseInt(options.limit),
});
console.log(`\nš Retrieved ${contexts.length} contexts:`);
contexts.forEach((c, i) => {
console.log(`\n${i + 1}. ${c.id} (${c.type})`);
console.log(` Agent: ${c.agent}`);
console.log(` Importance: ${c.importance}`);
});
break;
case 'stats':
await memory.initialize();
const stats = memory.getMemoryStats();
console.log('\nš Memory Statistics:');
console.log(JSON.stringify(stats, null, 2));
break;
case 'patterns':
await memory.initialize();
const patterns = memory.getLearnedPatterns(options.type);
console.log(`\nš Learned Patterns${options.type ? ` (${options.type})` : ''}:`);
patterns.forEach((p, i) => {
console.log(`\n${i + 1}. ${p.pattern.type}`);
console.log(` Frequency: ${p.frequency}`);
console.log(` First seen: ${p.first_seen}`);
});
break;
case 'end':
await memory.initialize();
const session = await memory.endSession();
console.log(`ā
Session ended: ${session.id}`);
break;
default:
console.error(`Unknown action: ${action}`);
console.log('Available actions: init, store, retrieve, stats, patterns, end');
}
} catch (error) {
console.error('Memory operation failed:', error.message);
process.exit(1);
}
});
program.parse(process.argv);