diff --git a/package.json b/package.json index 5b091f3f5b..b07f137ce0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firecrawl-cli", - "version": "1.23.4-alexandria-beta.19", + "version": "1.23.4-alexandria-beta.20", "publishConfig": { "tag": "alexandria" }, diff --git a/src/__tests__/alexandria-beta.test.ts b/src/__tests__/alexandria-beta.test.ts index d974de728c..5fa7915aea 100644 --- a/src/__tests__/alexandria-beta.test.ts +++ b/src/__tests__/alexandria-beta.test.ts @@ -418,6 +418,27 @@ it('preserves mixed search results, tools and billing metadata', async () => { ); }); +it('searches Alexandria semantically with the positional shortcut', async () => { + response = { success: true, data: { tools: [] } }; + const result = await cli(['search', 'alexandria', 'hello world', '--json']); + expect(result.code).toBe(0); + expect(requests).toHaveLength(1); + expect(requests[0].body).toMatchObject({ + query: 'hello world', + sources: [{ type: 'alexandria' }], + domainTools: false, + }); + const invalid = await cli([ + 'search', + 'alexandria', + 'hello world', + '--sources', + 'web', + ]); + expect(invalid.code).not.toBe(0); + expect(requests).toHaveLength(1); +}); + it('sends provider calls to Scrape with a stable retry ID and preserves the receipt', async () => { const args = [ 'scrape', diff --git a/src/index.ts b/src/index.ts index 2ff2bdb722..393197ddce 100644 --- a/src/index.ts +++ b/src/index.ts @@ -920,7 +920,8 @@ Max upload size: 50 MB function createSearchCommand(): Command { const searchCmd = new Command('search') .description('Search the web and discover relevant Alexandria tools') - .argument('', 'Search query') + .argument('', 'Search query, or alexandria for semantic tool search') + .argument('[tool-query]', 'Query for search alexandria') .option( '--limit ', 'Maximum number of results (default: 5, max: 100)', @@ -986,9 +987,30 @@ function createSearchCommand(): Command { // false // ) .option('--json', 'Output as compact JSON', false) - .action(async (query, options) => { + .action(async (query, toolQuery, options) => { + const alexandriaOnly = toolQuery !== undefined; + if (alexandriaOnly && query !== 'alexandria') { + throw new Error( + 'Quote your search query, or use search alexandria "query".' + ); + } + if (alexandriaOnly && !toolQuery.trim()) { + throw new Error('Provide a non-empty Alexandria search query.'); + } + if ( + alexandriaOnly && + options.sources && + options.sources.trim().toLowerCase() !== 'alexandria' + ) { + throw new Error( + 'search alexandria requires --sources alexandria; omit --sources or use regular search.' + ); + } + if (alexandriaOnly) query = toolQuery; // Parse sources - let sources: SearchSource[] = ['web', 'alexandria']; + let sources: SearchSource[] = alexandriaOnly + ? ['alexandria'] + : ['web', 'alexandria']; if (options.sources) { sources = options.sources .split(',') @@ -1035,7 +1057,9 @@ function createSearchCommand(): Command { const searchOptions = { query, - domainTools: options.domainTools ?? sources.includes('alexandria'), + domainTools: + options.domainTools ?? + (!alexandriaOnly && sources.includes('alexandria')), limit: options.limit, sources, categories, @@ -1066,6 +1090,10 @@ function createSearchCommand(): Command { '--no-domain-tools', 'Disable domain matching; source selection still controls semantic tools' ); + searchCmd.addHelpText( + 'after', + '\nSemantic tool search: firecrawl search alexandria "find company contacts"\n' + ); return searchCmd; }