diff --git a/README.md b/README.md index 9fdb4fb..b5a134e 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ anyapi run reddit.search --input '{"query":"anyapi","limit":5}' - `anyapi login` - sign in to an AnyAPI account immediately with the OAuth 2.0 device flow. The CLI prints a verification URL and user code, opens the complete URL when possible, and waits without binding a localhost callback. - `anyapi login --api-key aa_live_...` - manual-key compatibility path: store an existing dashboard key locally without starting OAuth. - `anyapi search ` - search the public catalog and print SKU, name, and USD price terms. + `--category ` and `--platform ` scope the search, and either one works without a query. - `anyapi list [--category ]` - list catalog APIs. - `anyapi describe ` - print the authenticated API definition, including opaque schemas and gateway-published USD pricing, lane order, and failover metadata. - `anyapi run [--input ''] [-i file] [--idempotency-key ] [--jq ] [--fields a,b] [--max-items N] [--summary] [-o path] [--json]` - run an API. Always saves the full result; shape flags trim only the stdout view. diff --git a/__tests__/bundled-skills.test.ts b/__tests__/bundled-skills.test.ts index 04b2171..0f46495 100644 --- a/__tests__/bundled-skills.test.ts +++ b/__tests__/bundled-skills.test.ts @@ -12,6 +12,7 @@ describe('bundled agent skills', () => { it('document the current discovery, execution, and onboarding commands', () => { const discover = readSkill('anyapi-discover'); expect(discover).toContain('anyapi search '); + expect(discover).toContain('anyapi search --category --platform '); expect(discover).toContain('anyapi list --category '); expect(discover).toContain('anyapi describe '); expect(discover).toContain('dedicated ranked discovery search'); diff --git a/__tests__/discovery.test.ts b/__tests__/discovery.test.ts index 3a0241c..efb2bb5 100644 --- a/__tests__/discovery.test.ts +++ b/__tests__/discovery.test.ts @@ -162,6 +162,30 @@ describe('customer-safe discovery reader', () => { expectCustomerSafe(response); }); + it('scopes a search without a query and omits q rather than sending it empty', async () => { + let requested = ''; + const client = clientFor({ results: [], total: 0, ranking: 'semantic' }, (url) => { requested = url; }); + + await client.search({ platform: 'reddit' }); + + let url = new URL(requested); + expect(url.pathname).toBe('/catalog/search'); + expect(Object.fromEntries(url.searchParams)).toEqual({ platform: 'reddit' }); + + await client.search({ category: 'social', limit: 5 }); + + url = new URL(requested); + expect(Object.fromEntries(url.searchParams)).toEqual({ category: 'social', limit: '5' }); + }); + + it('rejects a search naming no query, category, or platform without calling the gateway', async () => { + let called = false; + const client = clientFor({ results: [] }, () => { called = true; }); + + await expect(client.search({})).rejects.toThrow('Search needs a query, --category, or --platform.'); + expect(called).toBe(false); + }); + it('reads authenticated detail responses and preserves schemas as opaque JSON', async () => { let authorization = ''; const body = { diff --git a/skills/anyapi-discover/SKILL.md b/skills/anyapi-discover/SKILL.md index 29543d7..ad6358a 100644 --- a/skills/anyapi-discover/SKILL.md +++ b/skills/anyapi-discover/SKILL.md @@ -15,6 +15,7 @@ Use this before running an unknown task. Search or list first, then describe the ```sh anyapi search "reddit posts" +anyapi search --platform reddit anyapi list --category social anyapi describe reddit.search ``` @@ -32,6 +33,9 @@ Search and list are public. Describe is authenticated because it returns the ful ## Key options - `anyapi search ` uses the dedicated ranked discovery search. +- `anyapi search --category --platform ` scopes the search. Any + non-empty combination of the query, `--category` and `--platform` works, so a + scope with no query lists every API on that platform or in that category. - `anyapi list --category ` narrows by category. - `anyapi describe ` prints input schema, output schema, and USD pricing. - Discovery pricing is always nested under `pricing`; ranked search reports diff --git a/src/api.ts b/src/api.ts index e1ad269..ad6a5d9 100644 --- a/src/api.ts +++ b/src/api.ts @@ -128,11 +128,20 @@ export class AnyApiClient { return readCatalogResponse(body); } - async search(options: { query: string; category?: string; platform?: string; limit?: number }): Promise { + // search accepts any non-empty combination of query, category and platform, + // matching what the gateway's ranked search allows. An absent query is omitted + // rather than sent empty, and a request naming none of the three is rejected + // here instead of earning a gateway 400. + async search(options: { query?: string; category?: string; platform?: string; limit?: number }): Promise { + if (!options.query && !options.category && !options.platform) { + throw new CliError('Search needs a query, --category, or --platform.'); + } const url = new URL(this.catalogUrl); url.pathname = `${url.pathname.replace(/\/$/, '')}/search`; url.search = ''; - url.searchParams.set('q', options.query); + if (options.query) { + url.searchParams.set('q', options.query); + } if (options.category) { url.searchParams.set('category', options.category); } diff --git a/src/commands.ts b/src/commands.ts index 58b5c97..fc8de68 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -78,9 +78,13 @@ export async function loginCommand( writeLine(ctx.stdout, 'AnyAPI key saved to ~/.anyapi/config.json.'); } -export async function searchCommand(ctx: CommandContext, query: string): Promise { +export async function searchCommand( + ctx: CommandContext, + query: string | undefined, + options: { category?: string; platform?: string } = {}, +): Promise { const client = new AnyApiClient({ fetchImpl: ctx.fetchImpl }); - const results = await client.search({ query }); + const results = await client.search({ query, category: options.category, platform: options.platform }); writeCatalogTable(ctx, results.results); } diff --git a/src/index.ts b/src/index.ts index 2363005..2cfb4c0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -58,8 +58,10 @@ program program .command("search") .description("Search the public AnyAPI catalog.") - .argument("", "Search query.") - .action((query) => run(() => searchCommand(ctx, query))); + .argument("[query]", "Search query. Optional when --category or --platform is given.") + .option("--category ", "Filter by category.") + .option("--platform ", "Filter by platform.") + .action((query, options) => run(() => searchCommand(ctx, query, options))); program .command("list")