From 00dc5a1a29a82bd6660c9b5369d2beaa956d80b4 Mon Sep 17 00:00:00 2001 From: chruffins <23645059+chruffins@users.noreply.github.com> Date: Tue, 22 Sep 2026 16:03:36 +0000 Subject: [PATCH 1/5] Add Search API CLI commands --- README.md | 41 ++++++++++++ cmd/root.go | 1 + cmd/search.go | 154 +++++++++++++++++++++++++++++++++++++++++++++ cmd/search_test.go | 142 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 338 insertions(+) create mode 100644 cmd/search.go create mode 100644 cmd/search_test.go diff --git a/README.md b/README.md index 46483a8c..fd8061d0 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,47 @@ Commands with JSON output support: - **Browser Sub-commands**: `replays list/start`, `process exec/spawn`, `fs file-info/list-files`, `webmcp list` (`webmcp invoke` always prints JSON output) - **Browser NDJSON streaming**: `telemetry stream` +### Search + +Search commands always return the full API response as JSON, including results, +warnings, provider attempts, usage, and expiry. They use the normal API key or +OAuth authentication and global `--project` scope. Your organization must have +Search API access enabled. + +```bash +# Automatic routing (server defaults to 10 results) +kernel search 'browser automation benchmarks' + +# Pin one provider; discover available slugs and capabilities first +kernel search providers +kernel search providers --slug exa +kernel search 'browser automation benchmarks' --provider exa --max-results 5 + +# Retrieve a retained result without another provider call or search charge +kernel search get srch_123 + +# Ordered fallback with portable filters and provider-native options +kernel search --request '{"query":"browser automation","strategy":{"type":"fallback","providers":[{"provider":"exa"},{"provider":"brave"}],"fallback_on":["error","timeout","empty"]},"include_domains":["example.com"],"strict_params":true}' + +# Complete request from a file or stdin, with an optional replay key +kernel search --request-file request.json --idempotency-key search-001 +cat request.json | kernel search --request-file - +``` + +- `--max-results` accepts 1–100; the API may clamp it to the provider cap. +- `--request` and `--request-file` accept the complete Search API JSON object, + including `content`, `include_raw`, date/locale filters, and typed strategies. + They cannot be combined with a positional query, `--provider`, or `--max-results`. + Provider-specific and advanced request validation is performed by the API. +- Create requests are not automatically retried, to avoid duplicate billable + searches after an ambiguous failure. Reuse an `--idempotency-key` only with the + same request when replaying it manually. +- Retained searches return 404 when missing, expired, or inaccessible. Deferred + content retrieval is not exposed because it is reserved but unavailable in the + current API contract. +- To search for a literal query equal to a subcommand name (`get` or `providers`), + use `--request '{"query":"providers"}'`. + ### Authentication - `kernel login [--force]` - Login via OAuth 2.0 diff --git a/cmd/root.go b/cmd/root.go index 5b34a49c..3dd54b48 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -174,6 +174,7 @@ func init() { rootCmd.AddCommand(mcp.MCPCmd) rootCmd.AddCommand(upgradeCmd) rootCmd.AddCommand(statusCmd) + rootCmd.AddCommand(newSearchCommand()) rootCmd.PersistentPostRunE = func(cmd *cobra.Command, args []string) error { // running synchronously so we never slow the command diff --git a/cmd/search.go b/cmd/search.go new file mode 100644 index 00000000..84427fe4 --- /dev/null +++ b/cmd/search.go @@ -0,0 +1,154 @@ +package cmd + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "os" + "strings" + "unicode/utf8" + + "github.com/kernel/cli/pkg/util" + "github.com/kernel/kernel-go-sdk/option" + "github.com/spf13/cobra" +) + +func newSearchCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "search [query]", + Short: "Search the web and return the full result as JSON", + Long: "Search the web using automatic routing or a pinned provider. Returns the full JSON resource, including warnings, attempts, usage, and expiry. Use --request or --request-file for the complete Search API request, including fallback strategies, portable filters, content, and provider-native options. Requires Search API access for your organization.", + Example: " kernel search 'browser automation' --provider exa --max-results 5\n kernel search --request-file request.json\n kernel search get srch_123\n kernel search providers --slug exa", + Args: cobra.MaximumNArgs(1), + RunE: runSearch, + } + cmd.Flags().String("provider", "", "Pin a provider slug (default: automatic routing)") + cmd.Flags().Int("max-results", 10, "Requested result count (1–100; subject to provider cap)") + cmd.Flags().String("request", "", "Complete Search API request as a JSON object; cannot be combined with query flags") + cmd.Flags().String("request-file", "", "Complete Search API request file (use '-' for stdin)") + cmd.Flags().String("idempotency-key", "", "Idempotency key for safely replaying the same request") + cmd.MarkFlagsMutuallyExclusive("request", "request-file") + for _, input := range []string{"request", "request-file"} { + cmd.MarkFlagsMutuallyExclusive(input, "provider") + cmd.MarkFlagsMutuallyExclusive(input, "max-results") + } + get := &cobra.Command{Use: "get ", Short: "Retrieve a retained search as JSON without running it again", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { + if strings.TrimSpace(args[0]) == "" { + return fmt.Errorf("search ID must not be empty") + } + return executeSearchRequest(cmd, http.MethodGet, "search/"+url.PathEscape(args[0]), nil) + }} + providers := &cobra.Command{Use: "providers", Short: "List configured providers and their capabilities as JSON", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { + path := "search/providers" + if cmd.Flags().Changed("slug") { + slug, _ := cmd.Flags().GetString("slug") + path += "?" + url.Values{"slug": {slug}}.Encode() + } + return executeSearchRequest(cmd, http.MethodGet, path, nil) + }} + providers.Flags().String("slug", "", "Filter by provider slug") + cmd.AddCommand(get, providers) + return cmd +} + +func runSearch(cmd *cobra.Command, args []string) error { + var body json.RawMessage + if cmd.Flags().Changed("request") || cmd.Flags().Changed("request-file") { + if len(args) != 0 { + return fmt.Errorf("query cannot be combined with --request or --request-file") + } + input, _ := cmd.Flags().GetString("request") + if cmd.Flags().Changed("request-file") { + path, _ := cmd.Flags().GetString("request-file") + var data []byte + var err error + if path == "-" { + data, err = io.ReadAll(cmd.InOrStdin()) + } else { + data, err = os.ReadFile(path) + } + if err != nil { + return fmt.Errorf("read search request: %w", err) + } + input = string(data) + } + var fields map[string]json.RawMessage + if err := json.Unmarshal([]byte(input), &fields); err != nil { + return fmt.Errorf("search request must be a JSON object: %w", err) + } + if fields == nil { + return fmt.Errorf("search request must be a JSON object") + } + var query string + if err := json.Unmarshal(fields["query"], &query); err != nil { + return fmt.Errorf("search request must contain a string query") + } + if err := validateSearchQuery(query); err != nil { + return err + } + body = json.RawMessage(input) + } else { + if len(args) != 1 { + return fmt.Errorf("provide a query or --request/--request-file") + } + if err := validateSearchQuery(args[0]); err != nil { + return err + } + request := map[string]any{"query": args[0]} + if cmd.Flags().Changed("max-results") { + count, _ := cmd.Flags().GetInt("max-results") + if count < 1 || count > 100 { + return fmt.Errorf("--max-results must be between 1 and 100") + } + request["max_results"] = count + } + if cmd.Flags().Changed("provider") { + provider, _ := cmd.Flags().GetString("provider") + if strings.TrimSpace(provider) == "" { + return fmt.Errorf("--provider must not be empty") + } + request["strategy"] = map[string]any{"type": "pinned", "provider": map[string]string{"provider": provider}} + } + var err error + body, err = json.Marshal(request) + if err != nil { + return err + } + } + return executeSearchRequest(cmd, http.MethodPost, "search", body) +} + +func validateSearchQuery(query string) error { + if strings.TrimSpace(query) == "" || utf8.RuneCountInString(query) > 2048 { + return fmt.Errorf("query must contain 1–2048 characters and not be blank") + } + return nil +} + +func executeSearchRequest(cmd *cobra.Command, method, path string, body json.RawMessage) error { + client := getKernelClient(cmd) + var response json.RawMessage + var opts []option.RequestOption + if method == http.MethodPost { + // Avoid duplicate billable searches after an ambiguous failure. + opts = append(opts, option.WithMaxRetries(0)) + if cmd.Flags().Changed("idempotency-key") { + key, _ := cmd.Flags().GetString("idempotency-key") + if strings.TrimSpace(key) == "" { + return fmt.Errorf("--idempotency-key must not be empty") + } + opts = append(opts, option.WithHeader("Idempotency-Key", key)) + } + } + if err := client.Execute(cmd.Context(), method, path, body, &response, opts...); err != nil { + return util.CleanedUpSdkError{Err: err} + } + data, err := json.MarshalIndent(response, "", " ") + if err != nil { + return err + } + fmt.Println(string(data)) + return nil +} diff --git a/cmd/search_test.go b/cmd/search_test.go new file mode 100644 index 00000000..06fa6b73 --- /dev/null +++ b/cmd/search_test.go @@ -0,0 +1,142 @@ +package cmd + +import ( + "context" + "fmt" + "io" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/kernel/cli/pkg/util" + kernel "github.com/kernel/kernel-go-sdk" + "github.com/kernel/kernel-go-sdk/option" + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func executeSearchCommand(t *testing.T, handler http.HandlerFunc, stdin string, args ...string) (string, error) { + t.Helper() + server := httptest.NewServer(handler) + t.Cleanup(server.Close) + client := kernel.NewClient(option.WithBaseURL(server.URL), option.WithAPIKey("test"), option.WithProject("project-test")) + root := &cobra.Command{Use: "kernel", SilenceErrors: true, SilenceUsage: true} + root.SetContext(context.WithValue(context.Background(), util.KernelClientKey, client)) + root.SetIn(strings.NewReader(stdin)) + root.AddCommand(newSearchCommand()) + root.SetArgs(append([]string{"search"}, args...)) + var err error + stdout := captureStdout(t, func() { err = root.Execute() }) + return stdout, err +} + +func TestSearchCreate(t *testing.T) { + const advanced = `{"query":"test","strategy":{"type":"fallback","providers":[{"provider":"exa","options":{"type":"auto"}},{"provider":"brave"}],"fallback_on":["error","timeout","empty"]},"content":true,"strict_params":true,"include_raw":true,"include_domains":["example.com"]}` + path := filepath.Join(t.TempDir(), "request.json") + require.NoError(t, os.WriteFile(path, []byte(advanced), 0600)) + for _, tc := range []struct { + name string + args []string + stdin, want string + }{ + {"auto", []string{"test"}, "", `{"query":"test"}`}, + {"pinned", []string{"test", "--provider", "exa", "--max-results", "5", "--idempotency-key", "search-001"}, "", `{"query":"test","max_results":5,"strategy":{"type":"pinned","provider":{"provider":"exa"}}}`}, + {"inline", []string{"--request", advanced}, "", advanced}, + {"stdin", []string{"--request-file", "-"}, advanced, advanced}, + {"file", []string{"--request-file", path}, "", advanced}, + } { + t.Run(tc.name, func(t *testing.T) { + calls := 0 + const response = `{"id":"srch_test","results":[],"warnings":[{"code":"billing_unavailable"}],"attempts":[],"future_field":9007199254740993}` + stdout, err := executeSearchCommand(t, func(w http.ResponseWriter, r *http.Request) { + calls++ + assert.Equal(t, http.MethodPost, r.Method) + assert.Equal(t, "/search", r.URL.Path) + assert.Equal(t, "Bearer test", r.Header.Get("Authorization")) + assert.Equal(t, "project-test", r.Header.Get("X-Kernel-Project")) + if tc.name == "pinned" { + assert.Equal(t, "search-001", r.Header.Get("Idempotency-Key")) + } + data, err := io.ReadAll(r.Body) + require.NoError(t, err) + assert.JSONEq(t, tc.want, string(data)) + w.Header().Set("Content-Type", "application/json") + fmt.Fprint(w, response) + }, tc.stdin, tc.args...) + require.NoError(t, err) + assert.Equal(t, 1, calls) + assert.JSONEq(t, response, stdout) + assert.Contains(t, stdout, "9007199254740993") + }) + } +} + +func TestSearchReadCommands(t *testing.T) { + for _, tc := range []struct { + args []string + path, query, response string + }{ + {[]string{"get", "srch_test"}, "/search/srch_test", "", `{"id":"srch_test","results":[]}`}, + {[]string{"providers"}, "/search/providers", "", `[]`}, + {[]string{"providers", "--slug", "exa&other=1"}, "/search/providers", "slug=exa%26other%3D1", `[]`}, + } { + t.Run(strings.Join(tc.args, " "), func(t *testing.T) { + stdout, err := executeSearchCommand(t, func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodGet, r.Method) + assert.Equal(t, tc.path, r.URL.Path) + assert.Equal(t, tc.query, r.URL.RawQuery) + w.Header().Set("Content-Type", "application/json") + fmt.Fprint(w, tc.response) + }, "", tc.args...) + require.NoError(t, err) + assert.JSONEq(t, tc.response, stdout) + }) + } +} + +func TestSearchInvalidInput(t *testing.T) { + for _, args := range [][]string{ + {}, {" "}, {strings.Repeat("x", 2049)}, {"a", "b"}, {"test", "--provider", ""}, + {"test", "--max-results", "0"}, {"test", "--max-results", "101"}, {"test", "--idempotency-key", ""}, + {"--request", "null"}, {"--request", "[]"}, {"--request", "{"}, {"--request", `{}`}, {"--request", `{"query":1}`}, + {"test", "--request", `{"query":"test"}`}, {"--request", `{}`, "--provider", "exa"}, + {"--request", `{}`, "--max-results", "5"}, {"--request", `{}`, "--request-file", "-"}, + {"--request-file", "/nonexistent/search-request.json"}, {"get"}, {"providers", "extra"}, + } { + t.Run(strings.Join(args, " "), func(t *testing.T) { + _, err := executeSearchCommand(t, func(w http.ResponseWriter, r *http.Request) { t.Error("unexpected API call") }, "", args...) + require.Error(t, err) + }) + } +} + +func TestSearchErrorsDoNotRetryCreate(t *testing.T) { + for _, status := range []int{400, 401, 403, 429, 500} { + t.Run(fmt.Sprint(status), func(t *testing.T) { + calls := 0 + stdout, err := executeSearchCommand(t, func(w http.ResponseWriter, r *http.Request) { + calls++ + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + fmt.Fprint(w, `{"message":"search failed"}`) + }, "", "test") + require.Error(t, err) + assert.Empty(t, stdout) + assert.Equal(t, 1, calls) + }) + } +} + +func TestSearchWiring(t *testing.T) { + for _, path := range [][]string{{"search"}, {"search", "get"}, {"search", "providers"}} { + cmd, remaining, err := rootCmd.Find(path) + require.NoError(t, err) + require.Empty(t, remaining) + assert.NotNil(t, cmd.RunE) + assert.False(t, isAuthExempt(cmd)) + } +} From 4b24c0f4452a92f916a05112e22781ac10894ad4 Mon Sep 17 00:00:00 2001 From: chruffins <23645059+chruffins@users.noreply.github.com> Date: Tue, 22 Sep 2026 18:12:30 +0000 Subject: [PATCH 2/5] Fix search request transport handling --- cmd/search.go | 10 +++++++--- cmd/search_test.go | 3 +++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/cmd/search.go b/cmd/search.go index 84427fe4..580a7c74 100644 --- a/cmd/search.go +++ b/cmd/search.go @@ -142,13 +142,17 @@ func executeSearchRequest(cmd *cobra.Command, method, path string, body json.Raw opts = append(opts, option.WithHeader("Idempotency-Key", key)) } } - if err := client.Execute(cmd.Context(), method, path, body, &response, opts...); err != nil { + var requestBody any + if method == http.MethodPost { + requestBody = body + } + if err := client.Execute(cmd.Context(), method, path, requestBody, &response, opts...); err != nil { return util.CleanedUpSdkError{Err: err} } data, err := json.MarshalIndent(response, "", " ") if err != nil { return err } - fmt.Println(string(data)) - return nil + _, err = fmt.Fprintln(cmd.OutOrStdout(), string(data)) + return err } diff --git a/cmd/search_test.go b/cmd/search_test.go index 06fa6b73..6fa3542b 100644 --- a/cmd/search_test.go +++ b/cmd/search_test.go @@ -89,6 +89,9 @@ func TestSearchReadCommands(t *testing.T) { assert.Equal(t, http.MethodGet, r.Method) assert.Equal(t, tc.path, r.URL.Path) assert.Equal(t, tc.query, r.URL.RawQuery) + body, err := io.ReadAll(r.Body) + require.NoError(t, err) + assert.Empty(t, body) w.Header().Set("Content-Type", "application/json") fmt.Fprint(w, tc.response) }, "", tc.args...) From f9a809510470e3f2ea0d30ed4e3c9db94730afa5 Mon Sep 17 00:00:00 2001 From: chruffins <23645059+chruffins@users.noreply.github.com> Date: Tue, 22 Sep 2026 18:13:38 +0000 Subject: [PATCH 3/5] Remove unsupported search idempotency option --- README.md | 8 ++++---- cmd/search.go | 8 -------- cmd/search_test.go | 7 ++----- 3 files changed, 6 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index fd8061d0..590b192d 100644 --- a/README.md +++ b/README.md @@ -159,8 +159,8 @@ kernel search get srch_123 # Ordered fallback with portable filters and provider-native options kernel search --request '{"query":"browser automation","strategy":{"type":"fallback","providers":[{"provider":"exa"},{"provider":"brave"}],"fallback_on":["error","timeout","empty"]},"include_domains":["example.com"],"strict_params":true}' -# Complete request from a file or stdin, with an optional replay key -kernel search --request-file request.json --idempotency-key search-001 +# Complete request from a file or stdin +kernel search --request-file request.json cat request.json | kernel search --request-file - ``` @@ -170,8 +170,8 @@ cat request.json | kernel search --request-file - They cannot be combined with a positional query, `--provider`, or `--max-results`. Provider-specific and advanced request validation is performed by the API. - Create requests are not automatically retried, to avoid duplicate billable - searches after an ambiguous failure. Reuse an `--idempotency-key` only with the - same request when replaying it manually. + searches after an ambiguous failure. If a request fails ambiguously, use + `search get` only when the API returned a retained search ID. - Retained searches return 404 when missing, expired, or inaccessible. Deferred content retrieval is not exposed because it is reserved but unavailable in the current API contract. diff --git a/cmd/search.go b/cmd/search.go index 580a7c74..654a2673 100644 --- a/cmd/search.go +++ b/cmd/search.go @@ -28,7 +28,6 @@ func newSearchCommand() *cobra.Command { cmd.Flags().Int("max-results", 10, "Requested result count (1–100; subject to provider cap)") cmd.Flags().String("request", "", "Complete Search API request as a JSON object; cannot be combined with query flags") cmd.Flags().String("request-file", "", "Complete Search API request file (use '-' for stdin)") - cmd.Flags().String("idempotency-key", "", "Idempotency key for safely replaying the same request") cmd.MarkFlagsMutuallyExclusive("request", "request-file") for _, input := range []string{"request", "request-file"} { cmd.MarkFlagsMutuallyExclusive(input, "provider") @@ -134,13 +133,6 @@ func executeSearchRequest(cmd *cobra.Command, method, path string, body json.Raw if method == http.MethodPost { // Avoid duplicate billable searches after an ambiguous failure. opts = append(opts, option.WithMaxRetries(0)) - if cmd.Flags().Changed("idempotency-key") { - key, _ := cmd.Flags().GetString("idempotency-key") - if strings.TrimSpace(key) == "" { - return fmt.Errorf("--idempotency-key must not be empty") - } - opts = append(opts, option.WithHeader("Idempotency-Key", key)) - } } var requestBody any if method == http.MethodPost { diff --git a/cmd/search_test.go b/cmd/search_test.go index 6fa3542b..47486c2a 100644 --- a/cmd/search_test.go +++ b/cmd/search_test.go @@ -44,7 +44,7 @@ func TestSearchCreate(t *testing.T) { stdin, want string }{ {"auto", []string{"test"}, "", `{"query":"test"}`}, - {"pinned", []string{"test", "--provider", "exa", "--max-results", "5", "--idempotency-key", "search-001"}, "", `{"query":"test","max_results":5,"strategy":{"type":"pinned","provider":{"provider":"exa"}}}`}, + {"pinned", []string{"test", "--provider", "exa", "--max-results", "5"}, "", `{"query":"test","max_results":5,"strategy":{"type":"pinned","provider":{"provider":"exa"}}}`}, {"inline", []string{"--request", advanced}, "", advanced}, {"stdin", []string{"--request-file", "-"}, advanced, advanced}, {"file", []string{"--request-file", path}, "", advanced}, @@ -58,9 +58,6 @@ func TestSearchCreate(t *testing.T) { assert.Equal(t, "/search", r.URL.Path) assert.Equal(t, "Bearer test", r.Header.Get("Authorization")) assert.Equal(t, "project-test", r.Header.Get("X-Kernel-Project")) - if tc.name == "pinned" { - assert.Equal(t, "search-001", r.Header.Get("Idempotency-Key")) - } data, err := io.ReadAll(r.Body) require.NoError(t, err) assert.JSONEq(t, tc.want, string(data)) @@ -104,7 +101,7 @@ func TestSearchReadCommands(t *testing.T) { func TestSearchInvalidInput(t *testing.T) { for _, args := range [][]string{ {}, {" "}, {strings.Repeat("x", 2049)}, {"a", "b"}, {"test", "--provider", ""}, - {"test", "--max-results", "0"}, {"test", "--max-results", "101"}, {"test", "--idempotency-key", ""}, + {"test", "--max-results", "0"}, {"test", "--max-results", "101"}, {"--request", "null"}, {"--request", "[]"}, {"--request", "{"}, {"--request", `{}`}, {"--request", `{"query":1}`}, {"test", "--request", `{"query":"test"}`}, {"--request", `{}`, "--provider", "exa"}, {"--request", `{}`, "--max-results", "5"}, {"--request", `{}`, "--request-file", "-"}, From aa818c3aa3efd8fe980b946d3cde8acd8a903186 Mon Sep 17 00:00:00 2001 From: chruffins <23645059+chruffins@users.noreply.github.com> Date: Tue, 22 Sep 2026 19:58:09 +0000 Subject: [PATCH 4/5] Align search CLI examples --- README.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 590b192d..83aa2337 100644 --- a/README.md +++ b/README.md @@ -145,21 +145,23 @@ OAuth authentication and global `--project` scope. Your organization must have Search API access enabled. ```bash -# Automatic routing (server defaults to 10 results) -kernel search 'browser automation benchmarks' - -# Pin one provider; discover available slugs and capabilities first +# Discover providers and their capabilities kernel search providers kernel search providers --slug exa -kernel search 'browser automation benchmarks' --provider exa --max-results 5 + +# Automatic routing +kernel search "Playwright browser automation" --max-results 5 + +# Pin a provider +kernel search "Playwright browser automation" --provider exa --max-results 5 # Retrieve a retained result without another provider call or search charge -kernel search get srch_123 +kernel search get srch_01jsearchresult -# Ordered fallback with portable filters and provider-native options +# Use an ordered fallback strategy or other advanced request fields kernel search --request '{"query":"browser automation","strategy":{"type":"fallback","providers":[{"provider":"exa"},{"provider":"brave"}],"fallback_on":["error","timeout","empty"]},"include_domains":["example.com"],"strict_params":true}' -# Complete request from a file or stdin +# Load a complete request from a file or stdin kernel search --request-file request.json cat request.json | kernel search --request-file - ``` From 494ba73ed53c422f52069396a210f729ccc5926c Mon Sep 17 00:00:00 2001 From: chruffins <23645059+chruffins@users.noreply.github.com> Date: Tue, 22 Sep 2026 20:06:23 +0000 Subject: [PATCH 5/5] Use provider-neutral search examples --- README.md | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 83aa2337..bd5bf257 100644 --- a/README.md +++ b/README.md @@ -145,21 +145,17 @@ OAuth authentication and global `--project` scope. Your organization must have Search API access enabled. ```bash -# Discover providers and their capabilities +# Discover currently configured providers and capabilities kernel search providers -kernel search providers --slug exa # Automatic routing kernel search "Playwright browser automation" --max-results 5 -# Pin a provider -kernel search "Playwright browser automation" --provider exa --max-results 5 - # Retrieve a retained result without another provider call or search charge kernel search get srch_01jsearchresult -# Use an ordered fallback strategy or other advanced request fields -kernel search --request '{"query":"browser automation","strategy":{"type":"fallback","providers":[{"provider":"exa"},{"provider":"brave"}],"fallback_on":["error","timeout","empty"]},"include_domains":["example.com"],"strict_params":true}' +# Use portable filters or other advanced request fields +kernel search --request '{"query":"browser automation","include_domains":["example.com"],"strict_params":true}' # Load a complete request from a file or stdin kernel search --request-file request.json