UNPKG

@ace-sdk/cli

Version:

ACE CLI - Command-line tool for intelligent pattern learning and playbook management

145 lines • 6.67 kB
/** * Semantic search command */ import { readFileSync } from 'fs'; import { globalOptions } from '../cli.js'; import { ACEServerClient } from '../services/server-client.js'; import { SessionStorage } from '../services/session-storage.js'; import { formatSearchResults } from '../formatters/search-formatter.js'; import { Logger } from '../services/logger.js'; import chalk from 'chalk'; /** * Search for patterns using semantic search */ export async function searchCommand(query, options) { const logger = new Logger(globalOptions); let finalQuery = query; // Read from stdin if requested if (options.stdin) { try { const stdinData = readFileSync(0, 'utf8'); // fd 0 is stdin finalQuery = stdinData.trim(); } catch (error) { if (logger.isJson()) { logger.error('Failed to read from stdin'); } else { logger.error(chalk.red('Error: Failed to read from stdin')); } process.exit(1); } } if (!finalQuery) { if (logger.isJson()) { logger.error('Query is required'); } else { logger.error(chalk.red('Error: Query is required')); } process.exit(1); } const spinner = logger.spinner('Searching playbook...'); try { // Create context using 4-tier precedence (flags > env > .claude/settings.json > error) const context = await import('../types/config.js').then(m => m.createContext({ org: globalOptions.org, project: globalOptions.project })); // Create client with resolved context const client = new ACEServerClient(context, logger); // Fetch server-derived runtime settings const serverConfig = await client.getConfig(); const runtimeSettings = serverConfig?.runtime_settings || context.runtimeSettings; // Use CLI option if provided, otherwise use server's constitution_threshold // NOTE: constitution_threshold is the actual field returned by the server for search similarity // Server ALWAYS returns this field (confirmed by server team) - if missing, server is broken const threshold = options.threshold ?? serverConfig?.constitution_threshold; if (threshold === undefined) { logger.error(chalk.red('Error: Server did not provide constitution_threshold')); logger.error(chalk.dim('This indicates a server configuration issue. Please contact support.')); process.exit(1); } const section = options.section ?? runtimeSettings.patternDefaultSection; // Parse and validate top_k parameter - use server default if not provided let topK = undefined; if (options.topK !== undefined) { topK = typeof options.topK === 'number' ? options.topK : parseInt(String(options.topK), 10); if (isNaN(topK) || topK < 1 || topK > 100) { logger.error(chalk.red('Error: --top-k must be between 1 and 100')); process.exit(1); } } else { // Use server's search_top_k as default when flag not provided topK = serverConfig?.search_top_k; } // Validate mutual exclusivity of domain filters if (options.allowedDomains && options.blockedDomains) { logger.error(chalk.red('Error: Cannot use both --allowed-domains and --blocked-domains simultaneously')); process.exit(1); } // Parse comma-separated domain strings to arrays const allowedDomains = options.allowedDomains ? options.allowedDomains.split(',').map(d => d.trim()).filter(d => d.length > 0) : undefined; const blockedDomains = options.blockedDomains ? options.blockedDomains.split(',').map(d => d.trim()).filter(d => d.length > 0) : undefined; logger.debug(`Using search threshold: ${threshold} ${options.threshold ? '(CLI override)' : '(server-derived)'}`); logger.debug(`Context source: flags=${!!globalOptions.org}, env=${!!process.env.ACE_ORG_ID}, project=${!!context.orgId}`); if (topK !== undefined) { logger.debug(`Using top_k: ${topK} ${options.topK ? '(CLI override)' : '(server-derived)'}`); } if (allowedDomains) { logger.debug(`Domain filter: allowed_domains=[${allowedDomains.join(', ')}]`); } if (blockedDomains) { logger.debug(`Domain filter: blocked_domains=[${blockedDomains.join(', ')}]`); } const result = await client.searchPatterns({ query: finalQuery, threshold, section, top_k: topK, include_metadata: true, allowed_domains: allowedDomains, blocked_domains: blockedDomains }); // Pin patterns to session if requested if (options.pinSession && result.similar_patterns && result.similar_patterns.length > 0) { const sessionStorage = new SessionStorage(logger); await sessionStorage.initialize(); await sessionStorage.pinSession(options.pinSession, finalQuery, result.similar_patterns, threshold, topK || result.top_k || 25); sessionStorage.close(); logger.debug(`📌 Patterns pinned to session: ${options.pinSession}`); } spinner?.succeed(`Found ${result.similar_patterns?.length || 0} patterns`); if (logger.isJson()) { logger.output(result); } else { if (!result.similar_patterns || result.similar_patterns.length === 0) { logger.info(chalk.yellow('\n⚠️ No matching patterns found\n')); logger.info(chalk.dim(`Try lowering the threshold (current: ${threshold})\n`)); return; } formatSearchResults(result.similar_patterns, { query: finalQuery, verbose: logger.isVerbose() }); if (result.metadata && logger.isVerbose()) { logger.debug(chalk.dim('\nMetadata:')); logger.debug(chalk.dim(` ${JSON.stringify(result.metadata, null, 2)}`)); } } } catch (error) { spinner?.fail('Search failed'); if (logger.isJson()) { logger.error(error instanceof Error ? error.message : String(error)); } else { logger.error(chalk.red(`\nError: ${error instanceof Error ? error.message : String(error)}\n`)); } process.exit(1); } } //# sourceMappingURL=search.js.map