@ace-sdk/cli
Version:
ACE CLI - Command-line tool for intelligent pattern learning and playbook management
145 lines ⢠6.67 kB
JavaScript
/**
* 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