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
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ Commands with JSON output support:
- `--fill-rate <n>` - Percentage of the pool to fill per minute
- `--timeout <seconds>` - Idle timeout for browsers acquired from the pool
- `--stealth`, `--headless`, `--kiosk` - Default pool configuration
- `--profile-id`, `--profile-name`, `--save-changes`, `--proxy-id`, `--start-url`, `--extension`, `--viewport` - Same semantics as `kernel browsers create`
- `--profile-id`, `--profile-name`, `--proxy-id`, `--start-url`, `--extension`, `--viewport` - Same semantics as `kernel browsers create`
- `--chrome-policy <json>` / `--chrome-policy-file <path>` - Custom Chrome enterprise policy applied to every browser in the pool, as a JSON object or from a file (`-` for stdin). Same semantics as `kernel browsers create`.
- `--output json`, `-o json` - Output raw JSON object
- `kernel browser-pools get <id-or-name>` - Get pool details
Expand Down Expand Up @@ -317,8 +317,14 @@ Per-category updates are partial — only categories you name are changed; other
- `--categories <list>` - Filter by event category (`console`, `network`, `page`, `interaction`, `control`, `connection`, `system`, `screenshot`, `captcha`, `monitor`)
- `--types <list>` - Filter by event type (e.g. `network_response`, `console_error`)
- `--seq <n>` - Resume after sequence number N (Last-Event-ID); replays events with `seq > N`. Omit to stream from now.
- `--replay all` - Replay buffered events on connect, starting from the oldest retained event (mutually exclusive with `--seq`)
- `-o, --output json` - Output newline-delimited JSON envelopes
- Default output: tab-separated `<time>\t[<category>]\t<type>`, e.g. `15:04:05 [network] network_response`
- `kernel browsers telemetry events <id>` - Read historical telemetry events (paged)
- `--limit <n>` - Maximum number of events per page (default 20)
- `--offset <cursor>` - Pagination cursor: pass the `X-Next-Offset` from a previous response
- `--since <ts|dur>` / `--until <ts|dur>` - Time window (RFC-3339 timestamp or duration like `5m`). `--since` is ignored when `--offset` is set; `--until` still bounds the page
- `-o, --output json` - Output `{ "events": [...], "next_offset": "..." }` (omit `next_offset` when there is no next page)

### Browser Process Control

Expand Down Expand Up @@ -444,6 +450,8 @@ Per-category updates are partial — only categories you name are changed; other

- `kernel extensions list` - List all uploaded extensions
- `--output json`, `-o json` - Output raw JSON array
- `kernel extensions get <id-or-name>` - Show extension metadata (id, name, created, size, last used)
- `--output json`, `-o json` - Output raw JSON object
- `kernel extensions upload <directory>` - Upload an unpacked browser extension directory
- `--name <name>` - Optional unique extension name
- `--output json`, `-o json` - Output raw JSON object
Expand Down
4 changes: 2 additions & 2 deletions cmd/api_keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (

type APIKeysService interface {
New(ctx context.Context, body kernel.APIKeyNewParams, opts ...option.RequestOption) (*kernel.CreatedAPIKey, error)
Get(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.APIKey, error)
Get(ctx context.Context, id string, query kernel.APIKeyGetParams, opts ...option.RequestOption) (*kernel.APIKey, error)
Update(ctx context.Context, id string, body kernel.APIKeyUpdateParams, opts ...option.RequestOption) (*kernel.APIKey, error)
List(ctx context.Context, query kernel.APIKeyListParams, opts ...option.RequestOption) (*pagination.OffsetPagination[kernel.APIKey], error)
Delete(ctx context.Context, id string, opts ...option.RequestOption) error
Expand Down Expand Up @@ -145,7 +145,7 @@ func (c APIKeysCmd) Get(ctx context.Context, in APIKeysGetInput) error {
return err
}

key, err := c.apiKeys.Get(ctx, in.ID)
key, err := c.apiKeys.Get(ctx, in.ID, kernel.APIKeyGetParams{})
if err != nil {
return util.CleanedUpSdkError{Err: err}
}
Expand Down
8 changes: 4 additions & 4 deletions cmd/api_keys_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import (

type FakeAPIKeysService struct {
NewFunc func(ctx context.Context, body kernel.APIKeyNewParams, opts ...option.RequestOption) (*kernel.CreatedAPIKey, error)
GetFunc func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.APIKey, error)
GetFunc func(ctx context.Context, id string, query kernel.APIKeyGetParams, opts ...option.RequestOption) (*kernel.APIKey, error)
UpdateFunc func(ctx context.Context, id string, body kernel.APIKeyUpdateParams, opts ...option.RequestOption) (*kernel.APIKey, error)
ListFunc func(ctx context.Context, query kernel.APIKeyListParams, opts ...option.RequestOption) (*pagination.OffsetPagination[kernel.APIKey], error)
DeleteFunc func(ctx context.Context, id string, opts ...option.RequestOption) error
Expand All @@ -29,9 +29,9 @@ func (f *FakeAPIKeysService) New(ctx context.Context, body kernel.APIKeyNewParam
return createdAPIKeyFromJSON(`{"id":"key_123","name":"default","key":"sk_test","masked_key":"sk_...test","created_at":"2026-05-27T12:00:00Z","created_by":{"id":"user_123","email":"dev@example.com","name":"Dev"},"expires_at":null,"project_id":null,"project_name":null}`), nil
}

func (f *FakeAPIKeysService) Get(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.APIKey, error) {
func (f *FakeAPIKeysService) Get(ctx context.Context, id string, query kernel.APIKeyGetParams, opts ...option.RequestOption) (*kernel.APIKey, error) {
if f.GetFunc != nil {
return f.GetFunc(ctx, id, opts...)
return f.GetFunc(ctx, id, query, opts...)
}
return apiKeyFromJSON(`{"id":"` + id + `","name":"default","masked_key":"sk_...test","created_at":"2026-05-27T12:00:00Z","created_by":{"id":"user_123","email":"dev@example.com","name":"Dev"},"expires_at":null,"project_id":null,"project_name":null}`), nil
}
Expand Down Expand Up @@ -125,7 +125,7 @@ func TestAPIKeysRejectInvalidOutputBeforeCallingAPI(t *testing.T) {
t.Fatal("New should not be called")
return nil, nil
},
GetFunc: func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.APIKey, error) {
GetFunc: func(ctx context.Context, id string, query kernel.APIKeyGetParams, opts ...option.RequestOption) (*kernel.APIKey, error) {
t.Fatal("Get should not be called")
return nil, nil
},
Expand Down
200 changes: 94 additions & 106 deletions cmd/browser_pools.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,23 +91,22 @@ func (c BrowserPoolsCmd) List(ctx context.Context, in BrowserPoolsListInput) err
}

type BrowserPoolsCreateInput struct {
Name string
Size int64
FillRate int64
TimeoutSeconds int64
Stealth BoolFlag
Headless BoolFlag
Kiosk BoolFlag
ProfileID string
ProfileName string
ProfileSaveChanges BoolFlag
ProxyID string
StartURL string
Extensions []string
Viewport string
ChromePolicy string
ChromePolicyFile string
Output string
Name string
Size int64
FillRate int64
TimeoutSeconds int64
Stealth BoolFlag
Headless BoolFlag
Kiosk BoolFlag
ProfileID string
ProfileName string
ProxyID string
StartURL string
Extensions []string
Viewport string
ChromePolicy string
ChromePolicyFile string
Output string
}

func (c BrowserPoolsCmd) Create(ctx context.Context, in BrowserPoolsCreateInput) error {
Expand Down Expand Up @@ -141,13 +140,17 @@ func (c BrowserPoolsCmd) Create(ctx context.Context, in BrowserPoolsCreateInput)
params.KioskMode = kernel.Bool(in.Kiosk.Value)
}

profile, err := buildProfileParam(in.ProfileID, in.ProfileName, in.ProfileSaveChanges)
profileID, profileName, profileSet, err := resolvePoolProfile(in.ProfileID, in.ProfileName)
if err != nil {
pterm.Error.Println(err.Error())
return nil
}
if profile != nil {
params.Profile = *profile
if profileSet {
if profileID != "" {
params.Profile.ID = kernel.String(profileID)
} else {
params.Profile.Name = kernel.String(profileName)
}
}

if in.ProxyID != "" {
Expand Down Expand Up @@ -239,26 +242,25 @@ func (c BrowserPoolsCmd) Get(ctx context.Context, in BrowserPoolsGetInput) error
}

type BrowserPoolsUpdateInput struct {
IDOrName string
Name string
Size int64
FillRate int64
TimeoutSeconds int64
Stealth BoolFlag
Headless BoolFlag
Kiosk BoolFlag
ProfileID string
ProfileName string
ProfileSaveChanges BoolFlag
ProxyID string
StartURL string
ClearStartURL bool
Extensions []string
Viewport string
ChromePolicy string
ChromePolicyFile string
DiscardAllIdle BoolFlag
Output string
IDOrName string
Name string
Size int64
FillRate int64
TimeoutSeconds int64
Stealth BoolFlag
Headless BoolFlag
Kiosk BoolFlag
ProfileID string
ProfileName string
ProxyID string
StartURL string
ClearStartURL bool
Extensions []string
Viewport string
ChromePolicy string
ChromePolicyFile string
DiscardAllIdle BoolFlag
Output string
}

func (c BrowserPoolsCmd) Update(ctx context.Context, in BrowserPoolsUpdateInput) error {
Expand Down Expand Up @@ -299,13 +301,17 @@ func (c BrowserPoolsCmd) Update(ctx context.Context, in BrowserPoolsUpdateInput)
params.DiscardAllIdle = kernel.Bool(in.DiscardAllIdle.Value)
}

profile, err := buildProfileParam(in.ProfileID, in.ProfileName, in.ProfileSaveChanges)
profileID, profileName, profileSet, err := resolvePoolProfile(in.ProfileID, in.ProfileName)
if err != nil {
pterm.Error.Println(err.Error())
return nil
}
if profile != nil {
params.Profile = *profile
if profileSet {
if profileID != "" {
params.Profile.ID = kernel.String(profileID)
} else {
params.Profile.Name = kernel.String(profileName)
}
}

if in.ProxyID != "" {
Expand Down Expand Up @@ -561,7 +567,6 @@ func init() {
browserPoolsCreateCmd.Flags().Bool("kiosk", false, "Enable kiosk mode")
browserPoolsCreateCmd.Flags().String("profile-id", "", "Profile ID")
browserPoolsCreateCmd.Flags().String("profile-name", "", "Profile name")
browserPoolsCreateCmd.Flags().Bool("save-changes", false, "Save changes to profile")
browserPoolsCreateCmd.Flags().String("proxy-id", "", "Proxy ID")
browserPoolsCreateCmd.Flags().String("start-url", "", "Initial page to open for new browsers")
browserPoolsCreateCmd.Flags().StringSlice("extension", []string{}, "Extension IDs or names")
Expand All @@ -581,7 +586,6 @@ func init() {
browserPoolsUpdateCmd.Flags().Bool("kiosk", false, "Enable kiosk mode")
browserPoolsUpdateCmd.Flags().String("profile-id", "", "Profile ID")
browserPoolsUpdateCmd.Flags().String("profile-name", "", "Profile name")
browserPoolsUpdateCmd.Flags().Bool("save-changes", false, "Save changes to profile")
browserPoolsUpdateCmd.Flags().String("proxy-id", "", "Proxy ID")
browserPoolsUpdateCmd.Flags().String("start-url", "", "Initial page to open for new browsers")
browserPoolsUpdateCmd.Flags().Bool("clear-start-url", false, "Clear the pool start URL")
Expand Down Expand Up @@ -641,7 +645,6 @@ func runBrowserPoolsCreate(cmd *cobra.Command, args []string) error {
kiosk, _ := cmd.Flags().GetBool("kiosk")
profileID, _ := cmd.Flags().GetString("profile-id")
profileName, _ := cmd.Flags().GetString("profile-name")
saveChanges, _ := cmd.Flags().GetBool("save-changes")
proxyID, _ := cmd.Flags().GetString("proxy-id")
startURL, _ := cmd.Flags().GetString("start-url")
extensions, _ := cmd.Flags().GetStringSlice("extension")
Expand All @@ -651,23 +654,22 @@ func runBrowserPoolsCreate(cmd *cobra.Command, args []string) error {
output, _ := cmd.Flags().GetString("output")

in := BrowserPoolsCreateInput{
Name: name,
Size: size,
FillRate: fillRate,
TimeoutSeconds: timeout,
Stealth: BoolFlag{Set: cmd.Flags().Changed("stealth"), Value: stealth},
Headless: BoolFlag{Set: cmd.Flags().Changed("headless"), Value: headless},
Kiosk: BoolFlag{Set: cmd.Flags().Changed("kiosk"), Value: kiosk},
ProfileID: profileID,
ProfileName: profileName,
ProfileSaveChanges: BoolFlag{Set: cmd.Flags().Changed("save-changes"), Value: saveChanges},
ProxyID: proxyID,
StartURL: startURL,
Extensions: extensions,
Viewport: viewport,
ChromePolicy: chromePolicy,
ChromePolicyFile: chromePolicyFile,
Output: output,
Name: name,
Size: size,
FillRate: fillRate,
TimeoutSeconds: timeout,
Stealth: BoolFlag{Set: cmd.Flags().Changed("stealth"), Value: stealth},
Headless: BoolFlag{Set: cmd.Flags().Changed("headless"), Value: headless},
Kiosk: BoolFlag{Set: cmd.Flags().Changed("kiosk"), Value: kiosk},
ProfileID: profileID,
ProfileName: profileName,
ProxyID: proxyID,
StartURL: startURL,
Extensions: extensions,
Viewport: viewport,
ChromePolicy: chromePolicy,
ChromePolicyFile: chromePolicyFile,
Output: output,
}

c := BrowserPoolsCmd{client: &client.BrowserPools}
Expand All @@ -693,7 +695,6 @@ func runBrowserPoolsUpdate(cmd *cobra.Command, args []string) error {
kiosk, _ := cmd.Flags().GetBool("kiosk")
profileID, _ := cmd.Flags().GetString("profile-id")
profileName, _ := cmd.Flags().GetString("profile-name")
saveChanges, _ := cmd.Flags().GetBool("save-changes")
proxyID, _ := cmd.Flags().GetString("proxy-id")
startURL, _ := cmd.Flags().GetString("start-url")
clearStartURL, _ := cmd.Flags().GetBool("clear-start-url")
Expand All @@ -705,26 +706,25 @@ func runBrowserPoolsUpdate(cmd *cobra.Command, args []string) error {
output, _ := cmd.Flags().GetString("output")

in := BrowserPoolsUpdateInput{
IDOrName: args[0],
Name: name,
Size: size,
FillRate: fillRate,
TimeoutSeconds: timeout,
Stealth: BoolFlag{Set: cmd.Flags().Changed("stealth"), Value: stealth},
Headless: BoolFlag{Set: cmd.Flags().Changed("headless"), Value: headless},
Kiosk: BoolFlag{Set: cmd.Flags().Changed("kiosk"), Value: kiosk},
ProfileID: profileID,
ProfileName: profileName,
ProfileSaveChanges: BoolFlag{Set: cmd.Flags().Changed("save-changes"), Value: saveChanges},
ProxyID: proxyID,
StartURL: startURL,
ClearStartURL: clearStartURL,
Extensions: extensions,
Viewport: viewport,
ChromePolicy: chromePolicy,
ChromePolicyFile: chromePolicyFile,
DiscardAllIdle: BoolFlag{Set: cmd.Flags().Changed("discard-all-idle"), Value: discardIdle},
Output: output,
IDOrName: args[0],
Name: name,
Size: size,
FillRate: fillRate,
TimeoutSeconds: timeout,
Stealth: BoolFlag{Set: cmd.Flags().Changed("stealth"), Value: stealth},
Headless: BoolFlag{Set: cmd.Flags().Changed("headless"), Value: headless},
Kiosk: BoolFlag{Set: cmd.Flags().Changed("kiosk"), Value: kiosk},
ProfileID: profileID,
ProfileName: profileName,
ProxyID: proxyID,
StartURL: startURL,
ClearStartURL: clearStartURL,
Extensions: extensions,
Viewport: viewport,
ChromePolicy: chromePolicy,
ChromePolicyFile: chromePolicyFile,
DiscardAllIdle: BoolFlag{Set: cmd.Flags().Changed("discard-all-idle"), Value: discardIdle},
Output: output,
}

c := BrowserPoolsCmd{client: &client.BrowserPools}
Expand Down Expand Up @@ -772,23 +772,18 @@ func runBrowserPoolsFlush(cmd *cobra.Command, args []string) error {
return c.Flush(cmd.Context(), BrowserPoolsFlushInput{IDOrName: args[0]})
}

func buildProfileParam(profileID, profileName string, saveChanges BoolFlag) (*kernel.BrowserProfileParam, error) {
// resolvePoolProfile validates and resolves a pool profile selection. Browser
// pools have their own profile type with no save_changes; this helper works for
// both create and update param types by returning the resolved id/name plus
// whether a profile was selected at all.
func resolvePoolProfile(profileID, profileName string) (id, name string, set bool, err error) {
if profileID != "" && profileName != "" {
return nil, fmt.Errorf("must specify at most one of --profile-id or --profile-name")
return "", "", false, fmt.Errorf("must specify at most one of --profile-id or --profile-name")
}
if profileID == "" && profileName == "" {
return nil, nil
return "", "", false, nil
}

profile := kernel.BrowserProfileParam{
SaveChanges: kernel.Bool(saveChanges.Value),
}
if profileID != "" {
profile.ID = kernel.String(profileID)
} else if profileName != "" {
profile.Name = kernel.String(profileName)
}
return &profile, nil
return profileID, profileName, true, nil
}

func validateStartURLFlag(startURL string) error {
Expand Down Expand Up @@ -847,15 +842,8 @@ func formatFillRate(rate int64) string {
return "-"
}

func formatProfile(profile kernel.BrowserProfile) string {
name := util.FirstOrDash(profile.Name, profile.ID)
if name == "-" {
return "-"
}
if profile.SaveChanges {
return fmt.Sprintf("%s (save changes: true)", name)
}
return fmt.Sprintf("%s (save changes: false)", name)
func formatProfile(profile kernel.BrowserPoolBrowserPoolConfigProfile) string {
return util.FirstOrDash(profile.Name, profile.ID)
}

func formatExtensions(extensions []kernel.BrowserExtension) string {
Expand Down
10 changes: 10 additions & 0 deletions cmd/browsers.go
Original file line number Diff line number Diff line change
Expand Up @@ -2748,7 +2748,17 @@ followed automatically by Chromium.`,
telemetryStream.Flags().StringSlice("types", []string{}, "Filter by event type (e.g. network_response,console_error)")
telemetryStream.Flags().Int64("seq", -1, "Resume after sequence number N (Last-Event-ID); replays events with seq > N. Default -1 streams from now")
telemetryStream.Flags().StringP("output", "o", "", "Output format: json for newline-delimited JSON envelopes")
telemetryStream.Flags().String("replay", "", "Replay buffered events on connect: --replay=all starts from the oldest retained event")
telemetryStream.MarkFlagsMutuallyExclusive("seq", "replay")
telemetryRoot.AddCommand(telemetryStream)

telemetryEvents := &cobra.Command{Use: "events <id>", Short: "Read historical telemetry events (paged)", Args: cobra.ExactArgs(1), RunE: runBrowsersTelemetryEvents}
telemetryEvents.Flags().Int64("limit", 0, "Maximum number of events per page (default 20)")
telemetryEvents.Flags().Int64("offset", 0, "Pagination cursor: pass the X-Next-Offset from a previous response")
telemetryEvents.Flags().String("since", "", "Window start: RFC-3339 timestamp or a duration like 5m (default 5m). Ignored when --offset is set")
telemetryEvents.Flags().String("until", "", "Window end (exclusive): RFC-3339 timestamp or a duration like 5m")
addJSONOutputFlag(telemetryEvents)
telemetryRoot.AddCommand(telemetryEvents)
browsersCmd.AddCommand(telemetryRoot)

// no flags for view; it takes a single positional argument
Expand Down
Loading
Loading