diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 091cfb1..f7014c3 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.10.0" + ".": "0.11.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index b791078..6ac19ba 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 41 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-a7c1df5070fe59642d7a1f168aa902a468227752bfc930cbf38930f7c205dbb6.yml -openapi_spec_hash: eab65e39aef4f0a0952b82adeecf6b5b -config_hash: 5de78bc29ac060562575cb54bb26826c +configured_endpoints: 46 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-e98d46c55826cdf541a9ee0df04ce92806ac6d4d92957ae79f897270b7d85b23.yml +openapi_spec_hash: 8a1af54fc0a4417165b8a52e6354b685 +config_hash: 043ddc54629c6d8b889123770cb4769f diff --git a/CHANGELOG.md b/CHANGELOG.md index dcc69a6..b890e4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## 0.11.0 (2025-09-04) + +Full Changelog: [v0.10.0...v0.11.0](https://github.com/onkernel/kernel-go-sdk/compare/v0.10.0...v0.11.0) + +### Features + +* **api:** adding support for browser profiles ([481cdb3](https://github.com/onkernel/kernel-go-sdk/commit/481cdb3500744c9e4ec050e340a920302d8fea19)) + + +### Bug Fixes + +* close body before retrying ([a6a2e40](https://github.com/onkernel/kernel-go-sdk/commit/a6a2e4054c629d6ee85997ed81a1b14e70e594dc)) + + +### Chores + +* **internal:** codegen related update ([a7030ab](https://github.com/onkernel/kernel-go-sdk/commit/a7030abb99c06c675f60a4f2afde43d376d9981f)) + ## 0.10.0 (2025-08-27) Full Changelog: [v0.9.1...v0.10.0](https://github.com/onkernel/kernel-go-sdk/compare/v0.9.1...v0.10.0) diff --git a/README.md b/README.md index 63938fd..0b271ea 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,11 @@ # Kernel Go API Library + + Go Reference + + The Kernel Go library provides convenient access to the [Kernel REST API](https://docs.onkernel.com) from applications written in Go. @@ -24,7 +28,7 @@ Or to pin the version: ```sh -go get -u 'github.com/onkernel/kernel-go-sdk@v0.10.0' +go get -u 'github.com/onkernel/kernel-go-sdk@v0.11.0' ``` diff --git a/api.md b/api.md index 702f8c4..0efea8f 100644 --- a/api.md +++ b/api.md @@ -61,6 +61,7 @@ Params Types: Response Types: - kernel.BrowserPersistence +- kernel.Profile - kernel.BrowserNewResponse - kernel.BrowserGetResponse - kernel.BrowserListResponse @@ -147,3 +148,13 @@ Methods: Methods: - client.Browsers.Logs.Stream(ctx context.Context, id string, query kernel.BrowserLogStreamParams) (shared.LogEvent, error) + +# Profiles + +Methods: + +- client.Profiles.New(ctx context.Context, body kernel.ProfileNewParams) (kernel.Profile, error) +- client.Profiles.Get(ctx context.Context, idOrName string) (kernel.Profile, error) +- client.Profiles.List(ctx context.Context) ([]kernel.Profile, error) +- client.Profiles.Delete(ctx context.Context, idOrName string) error +- client.Profiles.Download(ctx context.Context, idOrName string) (http.Response, error) diff --git a/browser.go b/browser.go index f0194a3..9e85646 100644 --- a/browser.go +++ b/browser.go @@ -140,6 +140,36 @@ func (r *BrowserPersistenceParam) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } +// Browser profile metadata. +type Profile struct { + // Unique identifier for the profile + ID string `json:"id,required"` + // Timestamp when the profile was created + CreatedAt time.Time `json:"created_at,required" format:"date-time"` + // Timestamp when the profile was last used + LastUsedAt time.Time `json:"last_used_at" format:"date-time"` + // Optional, easier-to-reference name for the profile + Name string `json:"name,nullable"` + // Timestamp when the profile was last updated + UpdatedAt time.Time `json:"updated_at" format:"date-time"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + ID respjson.Field + CreatedAt respjson.Field + LastUsedAt respjson.Field + Name respjson.Field + UpdatedAt respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r Profile) RawJSON() string { return r.JSON.raw } +func (r *Profile) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + type BrowserNewResponse struct { // Websocket URL for Chrome DevTools Protocol connections to the browser session CdpWsURL string `json:"cdp_ws_url,required"` @@ -158,6 +188,8 @@ type BrowserNewResponse struct { BrowserLiveViewURL string `json:"browser_live_view_url"` // Optional persistence configuration for the browser session. Persistence BrowserPersistence `json:"persistence"` + // Browser profile metadata. + Profile Profile `json:"profile"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { CdpWsURL respjson.Field @@ -168,6 +200,7 @@ type BrowserNewResponse struct { TimeoutSeconds respjson.Field BrowserLiveViewURL respjson.Field Persistence respjson.Field + Profile respjson.Field ExtraFields map[string]respjson.Field raw string } `json:"-"` @@ -197,6 +230,8 @@ type BrowserGetResponse struct { BrowserLiveViewURL string `json:"browser_live_view_url"` // Optional persistence configuration for the browser session. Persistence BrowserPersistence `json:"persistence"` + // Browser profile metadata. + Profile Profile `json:"profile"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { CdpWsURL respjson.Field @@ -207,6 +242,7 @@ type BrowserGetResponse struct { TimeoutSeconds respjson.Field BrowserLiveViewURL respjson.Field Persistence respjson.Field + Profile respjson.Field ExtraFields map[string]respjson.Field raw string } `json:"-"` @@ -236,6 +272,8 @@ type BrowserListResponse struct { BrowserLiveViewURL string `json:"browser_live_view_url"` // Optional persistence configuration for the browser session. Persistence BrowserPersistence `json:"persistence"` + // Browser profile metadata. + Profile Profile `json:"profile"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { CdpWsURL respjson.Field @@ -246,6 +284,7 @@ type BrowserListResponse struct { TimeoutSeconds respjson.Field BrowserLiveViewURL respjson.Field Persistence respjson.Field + Profile respjson.Field ExtraFields map[string]respjson.Field raw string } `json:"-"` @@ -272,6 +311,10 @@ type BrowserNewParams struct { TimeoutSeconds param.Opt[int64] `json:"timeout_seconds,omitzero"` // Optional persistence configuration for the browser session. Persistence BrowserPersistenceParam `json:"persistence,omitzero"` + // Profile selection for the browser session. Provide either id or name. If + // specified, the matching profile will be loaded into the browser session. + // Profiles must be created beforehand. + Profile BrowserNewParamsProfile `json:"profile,omitzero"` paramObj } @@ -283,6 +326,29 @@ func (r *BrowserNewParams) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } +// Profile selection for the browser session. Provide either id or name. If +// specified, the matching profile will be loaded into the browser session. +// Profiles must be created beforehand. +type BrowserNewParamsProfile struct { + // Profile ID to load for this browser session + ID param.Opt[string] `json:"id,omitzero"` + // Profile name to load for this browser session (instead of id). Must be 1-255 + // characters, using letters, numbers, dots, underscores, or hyphens. + Name param.Opt[string] `json:"name,omitzero"` + // If true, save changes made during the session back to the profile when the + // session ends. + SaveChanges param.Opt[bool] `json:"save_changes,omitzero"` + paramObj +} + +func (r BrowserNewParamsProfile) MarshalJSON() (data []byte, err error) { + type shadow BrowserNewParamsProfile + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *BrowserNewParamsProfile) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + type BrowserDeleteParams struct { // Persistent browser identifier PersistentID string `query:"persistent_id,required" json:"-"` diff --git a/browser_test.go b/browser_test.go index d7d9898..66fb631 100644 --- a/browser_test.go +++ b/browser_test.go @@ -32,6 +32,11 @@ func TestBrowserNewWithOptionalParams(t *testing.T) { Persistence: kernel.BrowserPersistenceParam{ ID: "my-awesome-browser-for-user-1234", }, + Profile: kernel.BrowserNewParamsProfile{ + ID: kernel.String("id"), + Name: kernel.String("name"), + SaveChanges: kernel.Bool(true), + }, Stealth: kernel.Bool(true), TimeoutSeconds: kernel.Int(0), }) diff --git a/client.go b/client.go index 526823e..4d9b871 100644 --- a/client.go +++ b/client.go @@ -20,6 +20,7 @@ type Client struct { Apps AppService Invocations InvocationService Browsers BrowserService + Profiles ProfileService } // DefaultClientOptions read from the environment (KERNEL_API_KEY, @@ -48,6 +49,7 @@ func NewClient(opts ...option.RequestOption) (r Client) { r.Apps = NewAppService(opts...) r.Invocations = NewInvocationService(opts...) r.Browsers = NewBrowserService(opts...) + r.Profiles = NewProfileService(opts...) return } diff --git a/internal/requestconfig/requestconfig.go b/internal/requestconfig/requestconfig.go index e1e3825..7a93e18 100644 --- a/internal/requestconfig/requestconfig.go +++ b/internal/requestconfig/requestconfig.go @@ -461,6 +461,11 @@ func (cfg *RequestConfig) Execute() (err error) { break } + // Close the response body before retrying to prevent connection leaks + if res != nil && res.Body != nil { + res.Body.Close() + } + time.Sleep(retryDelay(res, retryCount)) } diff --git a/internal/version.go b/internal/version.go index 3a404f6..153db11 100644 --- a/internal/version.go +++ b/internal/version.go @@ -2,4 +2,4 @@ package internal -const PackageVersion = "0.10.0" // x-release-please-version +const PackageVersion = "0.11.0" // x-release-please-version diff --git a/profile.go b/profile.go new file mode 100644 index 0000000..603641f --- /dev/null +++ b/profile.go @@ -0,0 +1,104 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package kernel + +import ( + "context" + "errors" + "fmt" + "net/http" + + "github.com/onkernel/kernel-go-sdk/internal/apijson" + "github.com/onkernel/kernel-go-sdk/internal/requestconfig" + "github.com/onkernel/kernel-go-sdk/option" + "github.com/onkernel/kernel-go-sdk/packages/param" +) + +// ProfileService contains methods and other services that help with interacting +// with the kernel API. +// +// Note, unlike clients, this service does not read variables from the environment +// automatically. You should not instantiate this service directly, and instead use +// the [NewProfileService] method instead. +type ProfileService struct { + Options []option.RequestOption +} + +// NewProfileService generates a new service that applies the given options to each +// request. These options are applied after the parent client's options (if there +// is one), and before any request-specific options. +func NewProfileService(opts ...option.RequestOption) (r ProfileService) { + r = ProfileService{} + r.Options = opts + return +} + +// Create a browser profile that can be used to load state into future browser +// sessions. +func (r *ProfileService) New(ctx context.Context, body ProfileNewParams, opts ...option.RequestOption) (res *Profile, err error) { + opts = append(r.Options[:], opts...) + path := "profiles" + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) + return +} + +// Retrieve details for a single profile by its ID or name. +func (r *ProfileService) Get(ctx context.Context, idOrName string, opts ...option.RequestOption) (res *Profile, err error) { + opts = append(r.Options[:], opts...) + if idOrName == "" { + err = errors.New("missing required id_or_name parameter") + return + } + path := fmt.Sprintf("profiles/%s", idOrName) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) + return +} + +// List profiles with optional filtering and pagination. +func (r *ProfileService) List(ctx context.Context, opts ...option.RequestOption) (res *[]Profile, err error) { + opts = append(r.Options[:], opts...) + path := "profiles" + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) + return +} + +// Delete a profile by its ID or by its name. +func (r *ProfileService) Delete(ctx context.Context, idOrName string, opts ...option.RequestOption) (err error) { + opts = append(r.Options[:], opts...) + opts = append([]option.RequestOption{option.WithHeader("Accept", "")}, opts...) + if idOrName == "" { + err = errors.New("missing required id_or_name parameter") + return + } + path := fmt.Sprintf("profiles/%s", idOrName) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodDelete, path, nil, nil, opts...) + return +} + +// Download the profile. Profiles are JSON files containing the pieces of state +// that we save. +func (r *ProfileService) Download(ctx context.Context, idOrName string, opts ...option.RequestOption) (res *http.Response, err error) { + opts = append(r.Options[:], opts...) + opts = append([]option.RequestOption{option.WithHeader("Accept", "application/octet-stream")}, opts...) + if idOrName == "" { + err = errors.New("missing required id_or_name parameter") + return + } + path := fmt.Sprintf("profiles/%s/download", idOrName) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) + return +} + +type ProfileNewParams struct { + // Optional name of the profile. Must be unique within the organization. + Name param.Opt[string] `json:"name,omitzero"` + paramObj +} + +func (r ProfileNewParams) MarshalJSON() (data []byte, err error) { + type shadow ProfileNewParams + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *ProfileNewParams) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} diff --git a/profile_test.go b/profile_test.go new file mode 100644 index 0000000..a39e93f --- /dev/null +++ b/profile_test.go @@ -0,0 +1,146 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package kernel_test + +import ( + "bytes" + "context" + "errors" + "io" + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/onkernel/kernel-go-sdk" + "github.com/onkernel/kernel-go-sdk/internal/testutil" + "github.com/onkernel/kernel-go-sdk/option" +) + +func TestProfileNewWithOptionalParams(t *testing.T) { + t.Skip("Prism tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := kernel.NewClient( + option.WithBaseURL(baseURL), + option.WithAPIKey("My API Key"), + ) + _, err := client.Profiles.New(context.TODO(), kernel.ProfileNewParams{ + Name: kernel.String("name"), + }) + if err != nil { + var apierr *kernel.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestProfileGet(t *testing.T) { + t.Skip("Prism tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := kernel.NewClient( + option.WithBaseURL(baseURL), + option.WithAPIKey("My API Key"), + ) + _, err := client.Profiles.Get(context.TODO(), "id_or_name") + if err != nil { + var apierr *kernel.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestProfileList(t *testing.T) { + t.Skip("Prism tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := kernel.NewClient( + option.WithBaseURL(baseURL), + option.WithAPIKey("My API Key"), + ) + _, err := client.Profiles.List(context.TODO()) + if err != nil { + var apierr *kernel.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestProfileDelete(t *testing.T) { + t.Skip("Prism tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := kernel.NewClient( + option.WithBaseURL(baseURL), + option.WithAPIKey("My API Key"), + ) + err := client.Profiles.Delete(context.TODO(), "id_or_name") + if err != nil { + var apierr *kernel.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestProfileDownload(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + w.Write([]byte("abc")) + })) + defer server.Close() + baseURL := server.URL + client := kernel.NewClient( + option.WithBaseURL(baseURL), + option.WithAPIKey("My API Key"), + ) + resp, err := client.Profiles.Download(context.TODO(), "id_or_name") + if err != nil { + var apierr *kernel.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } + defer resp.Body.Close() + + b, err := io.ReadAll(resp.Body) + if err != nil { + var apierr *kernel.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } + if !bytes.Equal(b, []byte("abc")) { + t.Fatalf("return value not %s: %s", "abc", b) + } +}