@ace-sdk/cli
Version:
ACE CLI - Command-line tool for intelligent pattern learning and playbook management
531 lines • 22 kB
JavaScript
/**
* ACE CLI - Standalone command-line tool for pattern learning and playbook management
*/
import { Command } from 'commander';
import tabtab from 'tabtab';
import { configCommand, configShowCommand, configSetCommand, configResetCommand, configValidateCommand } from './commands/config.js';
import { doctorCommand } from './commands/doctor.js';
import { patternsCommand } from './commands/patterns.js';
import { searchCommand } from './commands/search.js';
import { topCommand } from './commands/top.js';
import { learnCommand } from './commands/learn.js';
import { bootstrapCommand } from './commands/bootstrap.js';
import { statusCommand } from './commands/status.js';
import { usageCommand } from './commands/usage.js';
import { domainsCommand } from './commands/domains.js';
import { clearCommand } from './commands/clear.js';
import { cacheCommand, cacheRecallCommand } from './commands/cache.js';
import { deltaCommand } from './commands/delta.js';
import { exportCommand } from './commands/export.js';
import { importCommand } from './commands/import.js';
import { recordStartCommand, recordStopCommand, recordListCommand, recordExportCommand, recordDeleteCommand, recordStatusCommand } from './commands/record.js';
import { pluginListCommand, pluginTrustCommand, pluginUntrustCommand, pluginEnableCommand, pluginDisableCommand } from './commands/plugin.js';
import { projectsListCommand } from './commands/projects.js';
import { tuneCommand, tuneShowCommand, tuneResetCommand } from './commands/tune.js';
import { loginCommand } from './commands/login.js';
import { logoutCommand } from './commands/logout.js';
import { whoamiCommand } from './commands/whoami.js';
import { orgsCommand } from './commands/orgs.js';
import { switchOrgCommand } from './commands/switch-org.js';
import { refreshCommand } from './commands/refresh.js';
import { tokenCommand } from './commands/token.js';
import { devicesListCommand, devicesRenameCommand, devicesRemoveCommand, devicesLimitCommand } from './commands/devices.js';
import { checkCliUpdate } from './utils/version-checker.js';
import { readFileSync, existsSync } from 'fs';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
// Read version from package.json (avoid hardcoding)
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const packageJson = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
const CLI_VERSION = packageJson.version;
// Load .claude/settings.json if present in current directory
// This allows per-directory ACE configuration
function loadLocalSettings() {
const settingsPath = join(process.cwd(), '.claude', 'settings.json');
if (existsSync(settingsPath)) {
try {
const settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
// Handle both flat structure and nested env structure
const envVars = settings.env || settings;
// Set environment variables from settings (only if not already set)
// This allows .claude/settings.json to provide defaults that can be overridden by actual env vars
if (envVars.ACE_SERVER_URL && !process.env.ACE_SERVER_URL) {
process.env.ACE_SERVER_URL = envVars.ACE_SERVER_URL;
}
if (envVars.ACE_API_TOKEN && !process.env.ACE_API_TOKEN) {
process.env.ACE_API_TOKEN = envVars.ACE_API_TOKEN;
}
if (envVars.ACE_ORG_ID && !process.env.ACE_ORG_ID) {
process.env.ACE_ORG_ID = envVars.ACE_ORG_ID;
}
if (envVars.ACE_PROJECT_ID && !process.env.ACE_PROJECT_ID) {
process.env.ACE_PROJECT_ID = envVars.ACE_PROJECT_ID;
}
// v2.1.0: Verbosity setting
if (envVars.ACE_VERBOSITY && !process.env.ACE_VERBOSITY) {
process.env.ACE_VERBOSITY = envVars.ACE_VERBOSITY;
}
}
catch (error) {
// Silently ignore errors - if settings file is malformed, just skip it
}
}
}
// Load local settings before anything else
loadLocalSettings();
const program = new Command();
// Handle tab completion
if (process.env.COMP_LINE) {
const env = tabtab.parseEnv(process.env);
if (env.complete) {
// Main command completions
if (!env.prev || env.prev === 'ace-cli' || env.prev === 'ce-ace') {
tabtab.log([
'login', 'logout', 'whoami', 'orgs', 'switch-org', 'refresh', 'token',
'devices', 'status', 'patterns', 'search', 'top', 'learn',
'bootstrap', 'cache', 'delta', 'export', 'import',
'record', 'plugin', 'config', 'doctor',
'projects', 'clear', 'completion', 'tune', 'domains'
]);
}
// Subcommand completions
else if (env.prev === 'record') {
tabtab.log(['start', 'stop', 'list', 'export', 'delete', 'status']);
}
else if (env.prev === 'plugin') {
tabtab.log(['list', 'trust', 'untrust', 'enable', 'disable']);
}
else if (env.prev === 'config') {
tabtab.log(['show', 'set', 'reset', 'validate']);
}
else if (env.prev === 'tune') {
tabtab.log(['show', 'reset']);
}
else if (env.prev === 'devices') {
tabtab.log(['list', 'rename', 'remove', 'limit']);
}
else if (env.prev === 'projects') {
tabtab.log(['list']);
}
else if (env.prev === 'cache') {
tabtab.log(['clear']);
}
else if (env.prev === 'delta') {
tabtab.log(['add', 'update', 'remove']);
}
else if (env.prev === 'completion') {
tabtab.log(['install', 'uninstall']);
}
// Flag completions for specific commands
else if (env.prev === 'search') {
tabtab.log(['--stdin', '--threshold', '--section', '--json', '--verbose', '--quiet']);
}
else if (env.prev === 'patterns') {
tabtab.log(['--section', '--min-helpful', '--json', '--verbose', '--quiet']);
}
else if (env.prev === 'bootstrap') {
tabtab.log(['--mode', '--thoroughness', '--commit-limit', '--days-back', '--max-files', '--repo-path', '--merge']);
}
process.exit(0);
}
}
// Check for CLI updates in background (non-blocking, cached for 24h)
checkCliUpdate().catch(() => {
// Silently ignore errors - update checks shouldn't break the CLI
});
// Make global options accessible
export let globalOptions = {
json: false,
verbose: false,
quiet: false,
trace: false,
org: undefined,
project: undefined,
};
program
.name('ace-cli')
.description('ACE CLI - Intelligent pattern learning and playbook management')
.version(CLI_VERSION)
.option('--json', 'Output structured JSON (for automation)')
.option('--verbose', 'Show detailed output')
.option('--quiet', 'Suppress non-essential output')
.option('--trace', 'Log raw HTTP requests and responses (debug mode)')
.option('--org <org_id>', 'Organization ID (overrides env and .claude/settings.json)')
.option('--project <project_id>', 'Project ID (overrides env and .claude/settings.json)')
.hook('preAction', (thisCommand) => {
const opts = thisCommand.optsWithGlobals();
globalOptions = {
json: opts.json || false,
verbose: opts.verbose || false,
quiet: opts.quiet || false,
trace: opts.trace || false,
org: opts.org,
project: opts.project,
};
});
// Authentication commands (v3.10.0+)
program
.command('login')
.description('Authenticate with ACE server (opens browser)')
.option('--no-browser', 'Skip opening browser (for SSH/headless mode)')
.action(loginCommand);
program
.command('logout')
.description('Clear authentication credentials')
.action(logoutCommand);
program
.command('whoami')
.description('Display current user info')
.option('--server', 'Fetch real-time token status from server (validates sliding window)')
.action(whoamiCommand);
program
.command('orgs')
.description('List your organizations')
.action(orgsCommand);
program
.command('switch-org <nameOrId>')
.description('Switch default organization')
.action(switchOrgCommand);
program
.command('refresh')
.description('Refresh organizations from server')
.action(refreshCommand);
program
.command('token')
.description('Display current API token')
.option('--reveal', 'Show full token (unmasked)')
.action(tokenCommand);
// Device management commands (v3.6.0+)
const devices = program
.command('devices')
.description('Manage your devices');
devices
.command('list')
.description('List all authorized devices')
.action(devicesListCommand);
devices
.command('rename <id> <name>')
.description('Rename a device')
.action(devicesRenameCommand);
devices
.command('remove <id>')
.description('Remove a device (revokes all sessions)')
.option('--yes', 'Skip confirmation prompt')
.action(devicesRemoveCommand);
devices
.command('limit')
.description('Show device limit')
.action(devicesLimitCommand);
// Configuration commands
const config = program
.command('config')
.description('Configuration wizard (interactive or non-interactive)')
.option('--server-url <url>', 'ACE server URL')
.option('--api-token <token>', 'API token')
.option('--org-id <org_id>', 'Organization ID (optional)')
.option('--project-id <project_id>', 'Project ID')
.action(configCommand);
config
.command('show')
.description('Display current configuration')
.action(configShowCommand);
config
.command('set <key> <value>')
.description('Set a configuration value')
.action(configSetCommand);
config
.command('reset')
.description('Reset configuration to defaults')
.option('--yes', 'Skip confirmation prompt')
.action(configResetCommand);
config
.command('validate')
.description('Validate token and display organization/project info')
.option('--server-url <url>', 'ACE server URL')
.option('--api-token <token>', 'API token')
.action((_options, command) => {
// Commander.js subcommands need optsWithGlobals() to get options
// This merges parent and subcommand options, with kebab→camelCase conversion
const opts = command.optsWithGlobals();
configValidateCommand({
serverUrl: opts.serverUrl,
apiToken: opts.apiToken
});
});
// Project discovery (v3.8.0: uses new /api/v1/auth/projects endpoint)
const projects = program
.command('projects')
.description('List accessible projects across organizations');
projects
.command('list')
.description('List all projects you have access to')
.option('--org <org_id>', 'Filter to specific organization')
.action(projectsListCommand);
// Default action for `ace-cli projects` (without subcommand)
projects.action(projectsListCommand);
// Diagnostics
program
.command('doctor')
.description('Run diagnostics on ACE configuration and connectivity')
.action(doctorCommand);
// Tune thresholds and settings (actual server fields - 11 total)
const tune = program
.command('tune')
.description('Tune ACE server configuration (interactive or flags)')
.option('--scope <scope>', 'Configuration scope (project)', 'project')
.option('--constitution-threshold <value>', 'Retrieval similarity threshold (0.0-1.0)', parseFloat)
.option('--search-top-k <value>', 'Max search results (1-100)', parseInt)
.option('--dedup-similarity-threshold <value>', 'Deduplication similarity threshold (0.0-1.0)', parseFloat)
.option('--dedup-enabled <boolean>', 'Enable/disable deduplication', (v) => v === 'true')
.option('--pruning-threshold <value>', 'Pruning threshold to keep patterns (0.0-1.0)', parseFloat)
.option('--max-playbook-tokens <value>', 'Maximum playbook tokens (null for unlimited)', parseInt)
.option('--token-budget-enforcement <boolean>', 'Enable/disable token budget enforcement', (v) => v === 'true')
.option('--max-batch-size <value>', 'Maximum patterns per batch (1-100)', parseInt)
.option('--auto-learning-enabled <boolean>', 'Enable/disable automatic learning', (v) => v === 'true')
.option('--reflector-enabled <boolean>', 'Enable/disable Reflector agent', (v) => v === 'true')
.option('--curator-enabled <boolean>', 'Enable/disable Curator agent', (v) => v === 'true')
.action(tuneCommand);
tune
.command('show')
.description('Show current effective configuration')
.action(tuneShowCommand);
tune
.command('reset')
.description('Reset configuration to defaults')
.option('--scope <scope>', 'Scope to reset (project)', 'project')
.action(tuneResetCommand);
// Playbook access
program
.command('patterns')
.description('View playbook patterns')
.option('--section <section>', 'Filter by section (strategies|snippets|troubleshooting|apis)')
.option('--min-helpful <score>', 'Minimum helpful score', parseFloat)
.action(patternsCommand);
program
.command('search [query]')
.description('Search for patterns using semantic search')
.option('--stdin', 'Read query from stdin')
.option('--threshold <value>', 'Similarity threshold (0.0-1.0) - overrides server default', parseFloat)
.option('--section <section>', 'Filter by section - overrides server default')
.option('--top-k <number>', 'Maximum patterns to return (1-100)', parseInt)
.option('--pin-session <sessionId>', 'Pin results to session for later recall')
.option('--allowed-domains <domains>', 'Whitelist: Only return patterns from these domains (comma-separated)')
.option('--blocked-domains <domains>', 'Blacklist: Exclude patterns from these domains (comma-separated)')
.action(searchCommand);
program
.command('top')
.description('Show top-rated patterns')
.option('--section <section>', 'Filter by section')
.option('--limit <number>', 'Number of patterns to show (server adaptive default if omitted)', parseInt)
.option('--min-helpful <score>', 'Minimum helpful score', parseFloat)
.action(topCommand);
// Learning / Knowledge updates
program
.command('learn')
.description('Submit learning event to update playbook')
.option('--transcript <file>', 'Path to transcript file')
.option('--stdin', 'Read transcript from stdin')
.option('--task <description>', 'Task description')
.option('--success', 'Task was successful')
.option('--failure', 'Task failed')
.option('--output <text>', 'Output/lessons learned')
.option('--no-stream', 'Disable SSE streaming (use sync POST)')
.option('--timeout <ms>', 'Stream timeout in milliseconds', parseInt, 60000)
.option('--verbosity <mode>', 'Output detail level (compact|detailed)', 'compact')
// Git context options (v2.2.0+ AI-Trail)
.option('--git-context', 'Auto-detect git context from cwd (default: true)')
.option('--no-git-context', 'Disable git context detection')
.option('--git-commit <hash>', 'Explicit commit hash (overrides auto-detection)')
.option('--git-branch <name>', 'Explicit branch name (overrides auto-detection)')
.action(learnCommand);
// Bootstrap / Data ingestion
program
.command('bootstrap')
.description('Initialize playbook from codebase')
.option('--mode <mode>', 'Analysis mode (hybrid|both|local-files|git-history|docs-only)', 'hybrid')
.option('--thoroughness <level>', 'Analysis depth (light|medium|deep)', 'medium')
.option('--commit-limit <number>', 'Max commits to analyze', parseInt)
.option('--days-back <number>', 'Days of history to analyze', parseInt)
.option('--max-files <number>', 'Max files to analyze', parseInt)
.option('--repo-path <path>', 'Path to repository', process.cwd())
.option('--merge', 'Merge with existing playbook', true)
.action(bootstrapCommand);
// Maintenance
program
.command('status')
.description('Show playbook statistics')
.action(statusCommand);
program
.command('usage')
.description('Show time-windowed API usage history')
.option('--window <window>', 'Time window: 1h, 6h, 12h, 1d, 7d, 14d, 30d (default: 1d)')
.option('--project <project_id>', 'Filter to specific project')
.action(usageCommand);
program
.command('domains')
.description('List available pattern domains for search filtering')
.option('--min-patterns <count>', 'Minimum patterns to show domain', (v) => parseInt(v, 10), 1)
.action(domainsCommand);
program
.command('clear')
.description('Clear the playbook')
.option('--yes', 'Skip confirmation prompt')
.action(clearCommand);
const cache = program
.command('cache')
.description('Manage local cache');
cache
.command('clear')
.description('Clear local cache')
.option('--type <type>', 'Cache type (ram|sqlite|all)', 'all')
.action(cacheCommand);
cache
.command('recall')
.description('Recall patterns from a pinned session')
.requiredOption('--session <sessionId>', 'Session ID to recall')
.action(cacheRecallCommand);
program
.command('delta <operation>')
.description('Apply incremental pattern updates (add|update|remove)')
.option('--bullets <json>', 'Bullets as JSON string')
.option('--file <path>', 'Read bullets from JSON file')
.option('--stdin', 'Read bullets from stdin')
.action(deltaCommand);
program
.command('export')
.description('Export playbook to JSON')
.option('--output <file>', 'Output file (default: stdout)')
.action(exportCommand);
program
.command('import [file]')
.description('Import playbook from JSON file or stdin')
.option('--file <path>', 'Import from file')
.option('--stdin', 'Read playbook from stdin')
.action(importCommand);
// Session recording
const record = program
.command('record')
.description('Manage session recordings');
record
.command('start')
.description('Start recording a session')
.option('--session <name>', 'Session name/description')
.action(recordStartCommand);
record
.command('stop')
.description('Stop the current recording session')
.action(recordStopCommand);
record
.command('list')
.description('List all recorded sessions')
.action(recordListCommand);
record
.command('export <sessionId> <output>')
.description('Export a session to a file')
.action(recordExportCommand);
record
.command('delete <sessionId>')
.description('Delete a session recording')
.option('--yes', 'Skip confirmation prompt')
.action(recordDeleteCommand);
record
.command('status')
.description('Show current recording status')
.action(recordStatusCommand);
// Plugin management
const plugin = program
.command('plugin')
.description('Manage plugins');
plugin
.command('list')
.description('List all available plugins')
.action(pluginListCommand);
plugin
.command('trust <name>')
.description('Trust a plugin (required before enabling)')
.option('--yes', 'Skip confirmation prompt')
.action(pluginTrustCommand);
plugin
.command('untrust <name>')
.description('Untrust and disable a plugin')
.action(pluginUntrustCommand);
plugin
.command('enable <name>')
.description('Enable a trusted plugin')
.action(pluginEnableCommand);
plugin
.command('disable <name>')
.description('Disable a plugin')
.action(pluginDisableCommand);
// Shell completion
const completion = program
.command('completion')
.description('Manage shell completion (bash/zsh/fish)');
completion
.command('install')
.description('Install shell completion for ace-cli')
.action(async () => {
try {
await tabtab.install({
name: 'ace-cli',
completer: 'ace-cli'
});
console.log('Shell completion installed successfully!');
console.log('Please restart your shell or run: source ~/.bashrc (or ~/.zshrc)');
}
catch (error) {
console.error('Failed to install completion:', error instanceof Error ? error.message : String(error));
process.exit(1);
}
});
completion
.command('uninstall')
.description('Uninstall shell completion for ace-cli')
.action(async () => {
try {
await tabtab.uninstall({
name: 'ace-cli'
});
console.log('Shell completion uninstalled successfully!');
}
catch (error) {
console.error('Failed to uninstall completion:', error instanceof Error ? error.message : String(error));
process.exit(1);
}
});
// Only run CLI when executed directly (not when imported for testing)
const isMainModule = process.argv[1] && (process.argv[1].endsWith('/cli.js') ||
process.argv[1].endsWith('/ce-ace') ||
process.argv[1].endsWith('/ace-cli') ||
process.argv[1].includes('cli/dist/cli.js'));
if (isMainModule) {
// Handle uncaught errors
process.on('unhandledRejection', (reason, _promise) => {
if (!globalOptions.json) {
console.error('Unhandled rejection:', reason);
}
else {
console.error(JSON.stringify({ error: String(reason), type: 'unhandled_rejection' }));
}
process.exit(1);
});
process.on('uncaughtException', (error) => {
if (!globalOptions.json) {
console.error('Uncaught exception:', error);
}
else {
console.error(JSON.stringify({ error: error.message, stack: error.stack, type: 'uncaught_exception' }));
}
process.exit(1);
});
// Parse arguments
program.parse(process.argv);
// Show help if no command provided
if (!process.argv.slice(2).length) {
program.outputHelp();
process.exit(0);
}
}
//# sourceMappingURL=cli.js.map