From 54ba9dd50484704b12ea92c0ace576693c12fbc1 Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Thu, 9 Apr 2026 17:01:54 -0400 Subject: [PATCH 1/4] feat: add `kernel browsers curl` command Adds a CLI command that makes HTTP requests through a browser session's Chrome network stack, inheriting TLS fingerprint, cookies, proxy config, and headers. Mirrors real curl UX with -X, -H, -d flags. Two modes: - Structured (default): POST /browsers/{id}/curl with JSON response - Raw (--raw or -o): GET /browsers/{id}/curl/raw for streaming Co-Authored-By: Claude Opus 4.6 --- cmd/browsers.go | 286 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 286 insertions(+) diff --git a/cmd/browsers.go b/cmd/browsers.go index d799667..5d063af 100644 --- a/cmd/browsers.go +++ b/cmd/browsers.go @@ -9,12 +9,14 @@ import ( "io" "math/big" "net/http" + neturl "net/url" "os" "path/filepath" "regexp" "strconv" "strings" + "github.com/kernel/cli/pkg/auth" "github.com/kernel/cli/pkg/util" "github.com/kernel/kernel-go-sdk" "github.com/kernel/kernel-go-sdk/option" @@ -225,6 +227,7 @@ type BrowsersCmd struct { logs BrowserLogService computer BrowserComputerService playwright BrowserPlaywrightService + client *kernel.Client } type BrowsersListInput struct { @@ -2518,6 +2521,29 @@ func init() { browsersCreateCmd.Flags().String("pool-id", "", "Browser pool ID to acquire from (mutually exclusive with --pool-name)") browsersCreateCmd.Flags().String("pool-name", "", "Browser pool name to acquire from (mutually exclusive with --pool-id)") + // curl + curlCmd := &cobra.Command{ + Use: "curl ", + Short: "Make HTTP requests through a browser session", + Long: `Execute HTTP requests through Chrome's network stack, inheriting the +browser's TLS fingerprint, cookies, proxy configuration, and headers. +Works like curl but requests go through the browser session.`, + Args: cobra.ExactArgs(2), + RunE: runBrowsersCurl, + } + curlCmd.Flags().StringP("request", "X", "", "HTTP method (default: GET)") + curlCmd.Flags().StringArrayP("header", "H", nil, "HTTP header (repeatable, \"Key: Value\" format)") + curlCmd.Flags().StringP("data", "d", "", "Request body") + curlCmd.Flags().String("data-file", "", "Read request body from file") + curlCmd.Flags().Int("timeout", 30000, "Request timeout in milliseconds") + curlCmd.Flags().String("encoding", "", "Response encoding: utf8 or base64") + curlCmd.Flags().StringP("output", "o", "", "Write response body to file (uses streaming mode)") + curlCmd.Flags().Bool("raw", false, "Use streaming mode (no JSON wrapper)") + curlCmd.Flags().BoolP("include", "i", false, "Include response headers in output") + curlCmd.Flags().BoolP("silent", "s", false, "Suppress progress output") + curlCmd.Flags().Bool("json", false, "Output full JSON response") + browsersCmd.AddCommand(curlCmd) + // no flags for view; it takes a single positional argument } @@ -3255,6 +3281,266 @@ func runBrowsersComputerWriteClipboard(cmd *cobra.Command, args []string) error return b.ComputerWriteClipboard(cmd.Context(), BrowsersComputerWriteClipboardInput{Identifier: args[0], Text: text}) } +// Curl + +type BrowsersCurlInput struct { + Identifier string + URL string + Method string + Headers []string + Data string + DataFile string + TimeoutMs int + Encoding string + OutputFile string + Raw bool + Include bool + Silent bool + JSON bool +} + +// browserCurlRequest is the JSON body for POST /browsers/{id}/curl. +type browserCurlRequest struct { + URL string `json:"url"` + Method string `json:"method,omitempty"` + Headers map[string][]string `json:"headers,omitempty"` + Body string `json:"body,omitempty"` + TimeoutMs int `json:"timeout_ms,omitempty"` + ResponseEncoding string `json:"response_encoding,omitempty"` +} + +// browserCurlResponse is the JSON response from POST /browsers/{id}/curl. +type browserCurlResponse struct { + Status int `json:"status"` + Headers map[string][]string `json:"headers"` + Body string `json:"body"` + DurationMs float64 `json:"duration_ms"` +} + +func parseCurlHeaders(raw []string) map[string][]string { + if len(raw) == 0 { + return nil + } + headers := make(map[string][]string) + for _, h := range raw { + k, v, ok := strings.Cut(h, ":") + if !ok { + continue + } + key := strings.TrimSpace(k) + val := strings.TrimSpace(v) + headers[key] = append(headers[key], val) + } + return headers +} + +func (b BrowsersCmd) Curl(ctx context.Context, in BrowsersCurlInput) error { + if in.Raw || in.OutputFile != "" { + return b.curlRaw(ctx, in) + } + + // Read body from file if specified + body := in.Data + if in.DataFile != "" { + data, err := os.ReadFile(in.DataFile) + if err != nil { + return fmt.Errorf("reading data file: %w", err) + } + body = string(data) + } + + reqBody := browserCurlRequest{ + URL: in.URL, + } + if in.Method != "" { + reqBody.Method = in.Method + } + if in.TimeoutMs != 0 { + reqBody.TimeoutMs = in.TimeoutMs + } + if in.Encoding != "" { + reqBody.ResponseEncoding = in.Encoding + } + if body != "" { + reqBody.Body = body + } + reqBody.Headers = parseCurlHeaders(in.Headers) + + client := b.client + path := fmt.Sprintf("/browsers/%s/curl", in.Identifier) + + var result browserCurlResponse + err := client.Post(ctx, path, reqBody, &result) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + + if in.JSON { + enc := json.NewEncoder(os.Stdout) + enc.SetIndent("", " ") + return enc.Encode(result) + } + + if in.Include { + fmt.Fprintf(os.Stdout, "HTTP %d\n", result.Status) + for k, vals := range result.Headers { + for _, v := range vals { + fmt.Fprintf(os.Stdout, "%s: %s\n", k, v) + } + } + fmt.Fprintln(os.Stdout) + } + + fmt.Fprint(os.Stdout, result.Body) + return nil +} + +func (b BrowsersCmd) curlRaw(ctx context.Context, in BrowsersCurlInput) error { + // Build the full URL for /curl/raw + baseURL := util.GetBaseURL() + method := in.Method + if method == "" { + method = "GET" + } + + params := neturl.Values{} + params.Set("url", in.URL) + params.Set("method", method) + params.Set("timeout_ms", fmt.Sprintf("%d", in.TimeoutMs)) + + // Add custom headers as query params + for _, h := range in.Headers { + k, v, ok := strings.Cut(h, ":") + if !ok { + continue + } + params.Add("header", strings.TrimSpace(k)+": "+strings.TrimSpace(v)) + } + + rawURL := fmt.Sprintf("%s/browsers/%s/curl/raw?%s", + strings.TrimRight(baseURL, "/"), + in.Identifier, + params.Encode(), + ) + + // Read body from file if specified + body := in.Data + if in.DataFile != "" { + data, err := os.ReadFile(in.DataFile) + if err != nil { + return fmt.Errorf("reading data file: %w", err) + } + body = string(data) + } + + var bodyReader io.Reader + if body != "" { + bodyReader = strings.NewReader(body) + } + + req, err := http.NewRequestWithContext(ctx, method, rawURL, bodyReader) + if err != nil { + return fmt.Errorf("creating request: %w", err) + } + + // Get auth token from the SDK client's options + token := b.getAuthToken() + if token != "" { + req.Header.Set("Authorization", "Bearer "+token) + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return fmt.Errorf("request failed: %w", err) + } + defer resp.Body.Close() + + // Check for API-level errors (auth failures, session not found, etc.) + if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden { + respBody, _ := io.ReadAll(resp.Body) + return fmt.Errorf("authentication error (%d): %s", resp.StatusCode, strings.TrimSpace(string(respBody))) + } + if resp.StatusCode == http.StatusNotFound { + respBody, _ := io.ReadAll(resp.Body) + return fmt.Errorf("not found (%d): %s", resp.StatusCode, strings.TrimSpace(string(respBody))) + } + + if in.OutputFile != "" { + f, err := os.Create(in.OutputFile) + if err != nil { + return fmt.Errorf("creating output file: %w", err) + } + defer f.Close() + _, err = io.Copy(f, resp.Body) + if err != nil { + return fmt.Errorf("writing output file: %w", err) + } + if !in.Silent { + pterm.Success.Printf("Saved response to %s\n", in.OutputFile) + } + return nil + } + + if in.Include { + fmt.Fprintf(os.Stdout, "HTTP %d\n", resp.StatusCode) + for k, vals := range resp.Header { + for _, v := range vals { + fmt.Fprintf(os.Stdout, "%s: %s\n", k, v) + } + } + fmt.Fprintln(os.Stdout) + } + + _, err = io.Copy(os.Stdout, resp.Body) + return err +} + +// getAuthToken retrieves the bearer token for raw HTTP requests. +func (b BrowsersCmd) getAuthToken() string { + if apiKey := os.Getenv("KERNEL_API_KEY"); apiKey != "" { + return apiKey + } + tokens, err := auth.LoadTokens() + if err != nil { + return "" + } + return tokens.AccessToken +} + +func runBrowsersCurl(cmd *cobra.Command, args []string) error { + client := getKernelClient(cmd) + svc := client.Browsers + + method, _ := cmd.Flags().GetString("request") + headers, _ := cmd.Flags().GetStringArray("header") + data, _ := cmd.Flags().GetString("data") + dataFile, _ := cmd.Flags().GetString("data-file") + timeout, _ := cmd.Flags().GetInt("timeout") + encoding, _ := cmd.Flags().GetString("encoding") + outputFile, _ := cmd.Flags().GetString("output") + raw, _ := cmd.Flags().GetBool("raw") + include, _ := cmd.Flags().GetBool("include") + silent, _ := cmd.Flags().GetBool("silent") + jsonOutput, _ := cmd.Flags().GetBool("json") + + b := BrowsersCmd{browsers: &svc, client: &client} + return b.Curl(cmd.Context(), BrowsersCurlInput{ + Identifier: args[0], + URL: args[1], + Method: method, + Headers: headers, + Data: data, + DataFile: dataFile, + TimeoutMs: timeout, + Encoding: encoding, + OutputFile: outputFile, + Raw: raw, + Include: include, + Silent: silent, + JSON: jsonOutput, + }) +} + func truncateURL(url string, maxLen int) string { if len(url) <= maxLen { return url From 466ed67148e416aa9bfcaa7d692a6e733d4b9437 Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Thu, 9 Apr 2026 17:08:31 -0400 Subject: [PATCH 2/4] fix: use map[string]string for curl request headers The API expects single-value headers (map[string]string), not multi-value (map[string][]string). The previous type would serialize as {"Content-Type": ["application/json"]} causing a runtime unmarshal error on the server. Co-Authored-By: Claude Opus 4.6 --- cmd/browsers.go | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/cmd/browsers.go b/cmd/browsers.go index 5d063af..31957df 100644 --- a/cmd/browsers.go +++ b/cmd/browsers.go @@ -3301,12 +3301,12 @@ type BrowsersCurlInput struct { // browserCurlRequest is the JSON body for POST /browsers/{id}/curl. type browserCurlRequest struct { - URL string `json:"url"` - Method string `json:"method,omitempty"` - Headers map[string][]string `json:"headers,omitempty"` - Body string `json:"body,omitempty"` - TimeoutMs int `json:"timeout_ms,omitempty"` - ResponseEncoding string `json:"response_encoding,omitempty"` + URL string `json:"url"` + Method string `json:"method,omitempty"` + Headers map[string]string `json:"headers,omitempty"` + Body string `json:"body,omitempty"` + TimeoutMs int `json:"timeout_ms,omitempty"` + ResponseEncoding string `json:"response_encoding,omitempty"` } // browserCurlResponse is the JSON response from POST /browsers/{id}/curl. @@ -3317,19 +3317,17 @@ type browserCurlResponse struct { DurationMs float64 `json:"duration_ms"` } -func parseCurlHeaders(raw []string) map[string][]string { +func parseCurlHeaders(raw []string) map[string]string { if len(raw) == 0 { return nil } - headers := make(map[string][]string) + headers := make(map[string]string) for _, h := range raw { k, v, ok := strings.Cut(h, ":") if !ok { continue } - key := strings.TrimSpace(k) - val := strings.TrimSpace(v) - headers[key] = append(headers[key], val) + headers[strings.TrimSpace(k)] = strings.TrimSpace(v) } return headers } From 7f8ceced55378c41c71cc748028c763a592a6ed5 Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Thu, 9 Apr 2026 22:14:19 -0400 Subject: [PATCH 3/4] refactor: use SDK Curl method instead of raw HTTP for structured browser curl Replace the hand-rolled POST /browsers/{id}/curl with the generated BrowserCurlParams/BrowserCurlResponse types from the Stainless preview SDK. The raw/streaming mode (curlRaw) remains custom HTTP as before. Also updates ProxyService.Check interface to match the new SDK signature (added ProxyCheckParams parameter). Uses a temporary replace directive pointing kernel-go-sdk at the stainless-sdks/kernel-go preview module. Co-Authored-By: Claude Opus 4.6 --- cmd/browsers.go | 39 +++++++++----------------------------- cmd/browsers_test.go | 8 ++++++++ cmd/proxies/check.go | 2 +- cmd/proxies/check_test.go | 2 +- cmd/proxies/common_test.go | 6 +++--- cmd/proxies/types.go | 2 +- go.mod | 2 ++ go.sum | 4 ++-- 8 files changed, 27 insertions(+), 38 deletions(-) diff --git a/cmd/browsers.go b/cmd/browsers.go index 31957df..5ee5bfa 100644 --- a/cmd/browsers.go +++ b/cmd/browsers.go @@ -37,6 +37,7 @@ type BrowsersService interface { Update(ctx context.Context, id string, body kernel.BrowserUpdateParams, opts ...option.RequestOption) (res *kernel.BrowserUpdateResponse, err error) Delete(ctx context.Context, body kernel.BrowserDeleteParams, opts ...option.RequestOption) (err error) DeleteByID(ctx context.Context, id string, opts ...option.RequestOption) (err error) + Curl(ctx context.Context, id string, body kernel.BrowserCurlParams, opts ...option.RequestOption) (res *kernel.BrowserCurlResponse, err error) LoadExtensions(ctx context.Context, id string, body kernel.BrowserLoadExtensionsParams, opts ...option.RequestOption) (err error) } @@ -3299,24 +3300,6 @@ type BrowsersCurlInput struct { JSON bool } -// browserCurlRequest is the JSON body for POST /browsers/{id}/curl. -type browserCurlRequest struct { - URL string `json:"url"` - Method string `json:"method,omitempty"` - Headers map[string]string `json:"headers,omitempty"` - Body string `json:"body,omitempty"` - TimeoutMs int `json:"timeout_ms,omitempty"` - ResponseEncoding string `json:"response_encoding,omitempty"` -} - -// browserCurlResponse is the JSON response from POST /browsers/{id}/curl. -type browserCurlResponse struct { - Status int `json:"status"` - Headers map[string][]string `json:"headers"` - Body string `json:"body"` - DurationMs float64 `json:"duration_ms"` -} - func parseCurlHeaders(raw []string) map[string]string { if len(raw) == 0 { return nil @@ -3347,28 +3330,24 @@ func (b BrowsersCmd) Curl(ctx context.Context, in BrowsersCurlInput) error { body = string(data) } - reqBody := browserCurlRequest{ - URL: in.URL, + params := kernel.BrowserCurlParams{ + URL: in.URL, + Headers: parseCurlHeaders(in.Headers), } if in.Method != "" { - reqBody.Method = in.Method + params.Method = kernel.BrowserCurlParamsMethod(in.Method) } if in.TimeoutMs != 0 { - reqBody.TimeoutMs = in.TimeoutMs + params.TimeoutMs = kernel.Opt(int64(in.TimeoutMs)) } if in.Encoding != "" { - reqBody.ResponseEncoding = in.Encoding + params.ResponseEncoding = kernel.BrowserCurlParamsResponseEncoding(in.Encoding) } if body != "" { - reqBody.Body = body + params.Body = kernel.Opt(body) } - reqBody.Headers = parseCurlHeaders(in.Headers) - - client := b.client - path := fmt.Sprintf("/browsers/%s/curl", in.Identifier) - var result browserCurlResponse - err := client.Post(ctx, path, reqBody, &result) + result, err := b.browsers.Curl(ctx, in.Identifier, params) if err != nil { return util.CleanedUpSdkError{Err: err} } diff --git a/cmd/browsers_test.go b/cmd/browsers_test.go index 2bb2c71..4195b3c 100644 --- a/cmd/browsers_test.go +++ b/cmd/browsers_test.go @@ -60,6 +60,7 @@ type FakeBrowsersService struct { UpdateFunc func(ctx context.Context, id string, body kernel.BrowserUpdateParams, opts ...option.RequestOption) (*kernel.BrowserUpdateResponse, error) DeleteFunc func(ctx context.Context, body kernel.BrowserDeleteParams, opts ...option.RequestOption) error DeleteByIDFunc func(ctx context.Context, id string, opts ...option.RequestOption) error + CurlFunc func(ctx context.Context, id string, body kernel.BrowserCurlParams, opts ...option.RequestOption) (*kernel.BrowserCurlResponse, error) LoadExtensionsFunc func(ctx context.Context, id string, body kernel.BrowserLoadExtensionsParams, opts ...option.RequestOption) error } @@ -105,6 +106,13 @@ func (f *FakeBrowsersService) DeleteByID(ctx context.Context, id string, opts .. return nil } +func (f *FakeBrowsersService) Curl(ctx context.Context, id string, body kernel.BrowserCurlParams, opts ...option.RequestOption) (*kernel.BrowserCurlResponse, error) { + if f.CurlFunc != nil { + return f.CurlFunc(ctx, id, body, opts...) + } + return &kernel.BrowserCurlResponse{}, nil +} + func (f *FakeBrowsersService) LoadExtensions(ctx context.Context, id string, body kernel.BrowserLoadExtensionsParams, opts ...option.RequestOption) error { if f.LoadExtensionsFunc != nil { return f.LoadExtensionsFunc(ctx, id, body, opts...) diff --git a/cmd/proxies/check.go b/cmd/proxies/check.go index 5f47fae..a820ff4 100644 --- a/cmd/proxies/check.go +++ b/cmd/proxies/check.go @@ -20,7 +20,7 @@ func (p ProxyCmd) Check(ctx context.Context, in ProxyCheckInput) error { pterm.Info.Printf("Running health check on proxy %s...\n", in.ID) } - proxy, err := p.proxies.Check(ctx, in.ID) + proxy, err := p.proxies.Check(ctx, in.ID, kernel.ProxyCheckParams{}) if err != nil { return util.CleanedUpSdkError{Err: err} } diff --git a/cmd/proxies/check_test.go b/cmd/proxies/check_test.go index 8f24ecb..c130206 100644 --- a/cmd/proxies/check_test.go +++ b/cmd/proxies/check_test.go @@ -13,7 +13,7 @@ func TestProxyCheck_ShowsBypassHosts(t *testing.T) { buf := captureOutput(t) fake := &FakeProxyService{ - CheckFunc: func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.ProxyCheckResponse, error) { + CheckFunc: func(ctx context.Context, id string, body kernel.ProxyCheckParams, opts ...option.RequestOption) (*kernel.ProxyCheckResponse, error) { return &kernel.ProxyCheckResponse{ ID: id, Name: "Proxy 1", diff --git a/cmd/proxies/common_test.go b/cmd/proxies/common_test.go index 48f13cf..df49b76 100644 --- a/cmd/proxies/common_test.go +++ b/cmd/proxies/common_test.go @@ -41,7 +41,7 @@ type FakeProxyService struct { GetFunc func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.ProxyGetResponse, error) NewFunc func(ctx context.Context, body kernel.ProxyNewParams, opts ...option.RequestOption) (*kernel.ProxyNewResponse, error) DeleteFunc func(ctx context.Context, id string, opts ...option.RequestOption) error - CheckFunc func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.ProxyCheckResponse, error) + CheckFunc func(ctx context.Context, id string, body kernel.ProxyCheckParams, opts ...option.RequestOption) (*kernel.ProxyCheckResponse, error) } func (f *FakeProxyService) List(ctx context.Context, opts ...option.RequestOption) (*[]kernel.ProxyListResponse, error) { @@ -73,9 +73,9 @@ func (f *FakeProxyService) Delete(ctx context.Context, id string, opts ...option return nil } -func (f *FakeProxyService) Check(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.ProxyCheckResponse, error) { +func (f *FakeProxyService) Check(ctx context.Context, id string, body kernel.ProxyCheckParams, opts ...option.RequestOption) (*kernel.ProxyCheckResponse, error) { if f.CheckFunc != nil { - return f.CheckFunc(ctx, id, opts...) + return f.CheckFunc(ctx, id, body, opts...) } return &kernel.ProxyCheckResponse{ID: id, Type: kernel.ProxyCheckResponseTypeDatacenter}, nil } diff --git a/cmd/proxies/types.go b/cmd/proxies/types.go index c8e7a38..bf55d9f 100644 --- a/cmd/proxies/types.go +++ b/cmd/proxies/types.go @@ -13,7 +13,7 @@ type ProxyService interface { Get(ctx context.Context, id string, opts ...option.RequestOption) (res *kernel.ProxyGetResponse, err error) New(ctx context.Context, body kernel.ProxyNewParams, opts ...option.RequestOption) (res *kernel.ProxyNewResponse, err error) Delete(ctx context.Context, id string, opts ...option.RequestOption) (err error) - Check(ctx context.Context, id string, opts ...option.RequestOption) (res *kernel.ProxyCheckResponse, err error) + Check(ctx context.Context, id string, body kernel.ProxyCheckParams, opts ...option.RequestOption) (res *kernel.ProxyCheckResponse, err error) } // ProxyCmd handles proxy operations independent of cobra. diff --git a/go.mod b/go.mod index bbc599b..a90f587 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,8 @@ require ( golang.org/x/sync v0.19.0 ) +replace github.com/kernel/kernel-go-sdk => github.com/stainless-sdks/kernel-go v0.0.0-20260410014529-98c58b154bb9 + require ( al.essio.dev/pkg/shellescape v1.5.1 // indirect atomicgo.dev/cursor v0.2.0 // indirect diff --git a/go.sum b/go.sum index 2c777ed..0eec95f 100644 --- a/go.sum +++ b/go.sum @@ -64,8 +64,6 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/kernel/kernel-go-sdk v0.44.1-0.20260323174449-5e56fc5d99a6 h1:RBlGCN3IagI0b+XrWsb5FOUV/18tniuL6oHFAb7MMHE= -github.com/kernel/kernel-go-sdk v0.44.1-0.20260323174449-5e56fc5d99a6/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= @@ -118,6 +116,8 @@ github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stainless-sdks/kernel-go v0.0.0-20260410014529-98c58b154bb9 h1:6swlSdr5UYmQbuM3HWM9+1FDMjVHeBqE+ZPUvkDr73I= +github.com/stainless-sdks/kernel-go v0.0.0-20260410014529-98c58b154bb9/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= From d11219192b52735ad197e706c18db89703576f82 Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Thu, 9 Apr 2026 22:24:02 -0400 Subject: [PATCH 4/4] fix: address bugbot review comments for browser curl - Pass encoding param to /curl/raw endpoint (was silently ignored) - Print response headers to stdout when -i and -o are combined - Remove unused client field from BrowsersCmd Co-Authored-By: Claude Opus 4.6 --- cmd/browsers.go | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/cmd/browsers.go b/cmd/browsers.go index 5ee5bfa..0b25ddd 100644 --- a/cmd/browsers.go +++ b/cmd/browsers.go @@ -228,7 +228,6 @@ type BrowsersCmd struct { logs BrowserLogService computer BrowserComputerService playwright BrowserPlaywrightService - client *kernel.Client } type BrowsersListInput struct { @@ -3384,6 +3383,9 @@ func (b BrowsersCmd) curlRaw(ctx context.Context, in BrowsersCurlInput) error { params.Set("url", in.URL) params.Set("method", method) params.Set("timeout_ms", fmt.Sprintf("%d", in.TimeoutMs)) + if in.Encoding != "" { + params.Set("response_encoding", in.Encoding) + } // Add custom headers as query params for _, h := range in.Headers { @@ -3442,6 +3444,16 @@ func (b BrowsersCmd) curlRaw(ctx context.Context, in BrowsersCurlInput) error { return fmt.Errorf("not found (%d): %s", resp.StatusCode, strings.TrimSpace(string(respBody))) } + if in.Include { + fmt.Fprintf(os.Stdout, "HTTP %d\n", resp.StatusCode) + for k, vals := range resp.Header { + for _, v := range vals { + fmt.Fprintf(os.Stdout, "%s: %s\n", k, v) + } + } + fmt.Fprintln(os.Stdout) + } + if in.OutputFile != "" { f, err := os.Create(in.OutputFile) if err != nil { @@ -3458,16 +3470,6 @@ func (b BrowsersCmd) curlRaw(ctx context.Context, in BrowsersCurlInput) error { return nil } - if in.Include { - fmt.Fprintf(os.Stdout, "HTTP %d\n", resp.StatusCode) - for k, vals := range resp.Header { - for _, v := range vals { - fmt.Fprintf(os.Stdout, "%s: %s\n", k, v) - } - } - fmt.Fprintln(os.Stdout) - } - _, err = io.Copy(os.Stdout, resp.Body) return err } @@ -3500,7 +3502,7 @@ func runBrowsersCurl(cmd *cobra.Command, args []string) error { silent, _ := cmd.Flags().GetBool("silent") jsonOutput, _ := cmd.Flags().GetBool("json") - b := BrowsersCmd{browsers: &svc, client: &client} + b := BrowsersCmd{browsers: &svc} return b.Curl(cmd.Context(), BrowsersCurlInput{ Identifier: args[0], URL: args[1],