Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <query>` - search the public catalog and print SKU, name, and USD price terms.
`--category <cat>` and `--platform <platform>` scope the search, and either one works without a query.
- `anyapi list [--category <cat>]` - list catalog APIs.
- `anyapi describe <sku>` - print the authenticated API definition, including opaque schemas and gateway-published USD pricing, lane order, and failover metadata.
- `anyapi run <sku> [--input '<json>'] [-i file] [--idempotency-key <key>] [--jq <expr>] [--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.
Expand Down
1 change: 1 addition & 0 deletions __tests__/bundled-skills.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <query>');
expect(discover).toContain('anyapi search --category <cat> --platform <platform>');
expect(discover).toContain('anyapi list --category <cat>');
expect(discover).toContain('anyapi describe <sku>');
expect(discover).toContain('dedicated ranked discovery search');
Expand Down
24 changes: 24 additions & 0 deletions __tests__/discovery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
4 changes: 4 additions & 0 deletions skills/anyapi-discover/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand All @@ -32,6 +33,9 @@ Search and list are public. Describe is authenticated because it returns the ful
## Key options

- `anyapi search <query>` uses the dedicated ranked discovery search.
- `anyapi search --category <cat> --platform <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 <cat>` narrows by category.
- `anyapi describe <sku>` prints input schema, output schema, and USD pricing.
- Discovery pricing is always nested under `pricing`; ranked search reports
Expand Down
13 changes: 11 additions & 2 deletions src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,11 +128,20 @@ export class AnyApiClient {
return readCatalogResponse(body);
}

async search(options: { query: string; category?: string; platform?: string; limit?: number }): Promise<SearchResponse> {
// 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<SearchResponse> {
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);
}
Expand Down
8 changes: 6 additions & 2 deletions src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
export async function searchCommand(
ctx: CommandContext,
query: string | undefined,
options: { category?: string; platform?: string } = {},
): Promise<void> {
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);
}

Expand Down
6 changes: 4 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,10 @@ program
program
.command("search")
.description("Search the public AnyAPI catalog.")
.argument("<query>", "Search query.")
.action((query) => run(() => searchCommand(ctx, query)));
.argument("[query]", "Search query. Optional when --category or --platform is given.")
.option("--category <category>", "Filter by category.")
.option("--platform <platform>", "Filter by platform.")
.action((query, options) => run(() => searchCommand(ctx, query, options)));

program
.command("list")
Expand Down
Loading