diff --git a/cmd/browsers.go b/cmd/browsers.go index 37f3943..8d93a03 100644 --- a/cmd/browsers.go +++ b/cmd/browsers.go @@ -92,9 +92,11 @@ type BrowserPlaywrightService interface { // BrowserComputerService defines the subset we use for OS-level mouse & screen. type BrowserComputerService interface { + Batch(ctx context.Context, id string, body kernel.BrowserComputerBatchParams, opts ...option.RequestOption) (err error) CaptureScreenshot(ctx context.Context, id string, body kernel.BrowserComputerCaptureScreenshotParams, opts ...option.RequestOption) (res *http.Response, err error) ClickMouse(ctx context.Context, id string, body kernel.BrowserComputerClickMouseParams, opts ...option.RequestOption) (err error) DragMouse(ctx context.Context, id string, body kernel.BrowserComputerDragMouseParams, opts ...option.RequestOption) (err error) + GetMousePosition(ctx context.Context, id string, opts ...option.RequestOption) (res *kernel.BrowserComputerGetMousePositionResponse, err error) MoveMouse(ctx context.Context, id string, body kernel.BrowserComputerMoveMouseParams, opts ...option.RequestOption) (err error) PressKey(ctx context.Context, id string, body kernel.BrowserComputerPressKeyParams, opts ...option.RequestOption) (err error) Scroll(ctx context.Context, id string, body kernel.BrowserComputerScrollParams, opts ...option.RequestOption) (err error) @@ -172,6 +174,7 @@ type BrowsersCreateInput struct { TimeoutSeconds int Stealth BoolFlag Headless BoolFlag + GPU BoolFlag Kiosk BoolFlag ProfileID string ProfileName string @@ -340,6 +343,9 @@ func (b BrowsersCmd) Create(ctx context.Context, in BrowsersCreateInput) error { if in.Headless.Set { params.Headless = kernel.Opt(in.Headless.Value) } + if in.GPU.Set { + params.GPU = kernel.Opt(in.GPU.Value) + } if in.Kiosk.Set { params.KioskMode = kernel.Opt(in.Kiosk.Value) } @@ -527,6 +533,7 @@ func (b BrowsersCmd) Get(ctx context.Context, in BrowsersGetInput) error { tableData = append(tableData, []string{"Timeout (seconds)", fmt.Sprintf("%d", browser.TimeoutSeconds)}) tableData = append(tableData, []string{"Headless", fmt.Sprintf("%t", browser.Headless)}) tableData = append(tableData, []string{"Stealth", fmt.Sprintf("%t", browser.Stealth)}) + tableData = append(tableData, []string{"GPU", fmt.Sprintf("%t", browser.GPU)}) tableData = append(tableData, []string{"Kiosk Mode", fmt.Sprintf("%t", browser.KioskMode)}) if browser.Viewport.Width > 0 && browser.Viewport.Height > 0 { viewportStr := fmt.Sprintf("%dx%d", browser.Viewport.Width, browser.Viewport.Height) @@ -740,6 +747,16 @@ type BrowsersComputerSetCursorInput struct { Hidden bool } +type BrowsersComputerGetMousePositionInput struct { + Identifier string + Output string +} + +type BrowsersComputerBatchInput struct { + Identifier string + ActionsJSON string +} + func (b BrowsersCmd) ComputerClickMouse(ctx context.Context, in BrowsersComputerClickMouseInput) error { if b.computer == nil { pterm.Error.Println("computer service not available") @@ -956,6 +973,49 @@ func (b BrowsersCmd) ComputerSetCursor(ctx context.Context, in BrowsersComputerS return nil } +func (b BrowsersCmd) ComputerGetMousePosition(ctx context.Context, in BrowsersComputerGetMousePositionInput) error { + if b.computer == nil { + pterm.Error.Println("computer service not available") + return nil + } + br, err := b.browsers.Get(ctx, in.Identifier, kernel.BrowserGetParams{}) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + res, err := b.computer.GetMousePosition(ctx, br.SessionID) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + if in.Output == "json" { + enc := json.NewEncoder(os.Stdout) + enc.SetIndent("", " ") + return enc.Encode(res) + } + fmt.Printf("x: %d\ny: %d\n", res.X, res.Y) + return nil +} + +func (b BrowsersCmd) ComputerBatch(ctx context.Context, in BrowsersComputerBatchInput) error { + if b.computer == nil { + pterm.Error.Println("computer service not available") + return nil + } + br, err := b.browsers.Get(ctx, in.Identifier, kernel.BrowserGetParams{}) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + var body kernel.BrowserComputerBatchParams + if err := json.Unmarshal([]byte(in.ActionsJSON), &body); err != nil { + pterm.Error.Printf("Invalid JSON: %v\n", err) + return nil + } + if err := b.computer.Batch(ctx, br.SessionID, body); err != nil { + return util.CleanedUpSdkError{Err: err} + } + pterm.Success.Println("Batch actions executed") + return nil +} + // Replays type BrowsersReplaysListInput struct { Identifier string @@ -2300,7 +2360,16 @@ func init() { computerSetCursor.Flags().String("hidden", "", "Whether to hide the cursor: true or false") _ = computerSetCursor.MarkFlagRequired("hidden") - computerRoot.AddCommand(computerClick, computerMove, computerScreenshot, computerType, computerPressKey, computerScroll, computerDrag, computerSetCursor) + // computer get-mouse-position + computerGetMousePosition := &cobra.Command{Use: "get-mouse-position ", Short: "Get current mouse cursor position", Args: cobra.ExactArgs(1), RunE: runBrowsersComputerGetMousePosition} + computerGetMousePosition.Flags().StringP("output", "o", "", "Output format: json for raw API response") + + // computer batch + computerBatch := &cobra.Command{Use: "batch ", Short: "Execute a batch of computer actions from JSON", Args: cobra.ExactArgs(1), RunE: runBrowsersComputerBatch} + computerBatch.Flags().String("actions", "", "JSON object with actions array (e.g., {\"actions\":[{\"type\":\"click_mouse\",...}]})") + _ = computerBatch.MarkFlagRequired("actions") + + computerRoot.AddCommand(computerClick, computerMove, computerScreenshot, computerType, computerPressKey, computerScroll, computerDrag, computerSetCursor, computerGetMousePosition, computerBatch) browsersCmd.AddCommand(computerRoot) // playwright @@ -2316,6 +2385,7 @@ func init() { _ = browsersCreateCmd.Flags().MarkDeprecated("persistent-id", "use --timeout (up to 72 hours) and profiles instead") browsersCreateCmd.Flags().BoolP("stealth", "s", false, "Launch browser in stealth mode to avoid detection") browsersCreateCmd.Flags().BoolP("headless", "H", false, "Launch browser without GUI access") + browsersCreateCmd.Flags().Bool("gpu", false, "Launch browser with hardware-accelerated GPU rendering") browsersCreateCmd.Flags().Bool("kiosk", false, "Launch browser in kiosk mode") browsersCreateCmd.Flags().IntP("timeout", "t", 60, "Timeout in seconds for the browser session") browsersCreateCmd.Flags().String("profile-id", "", "Profile ID to load into the browser session (mutually exclusive with --profile-name)") @@ -2359,6 +2429,7 @@ func runBrowsersCreate(cmd *cobra.Command, args []string) error { } stealthVal, _ := cmd.Flags().GetBool("stealth") headlessVal, _ := cmd.Flags().GetBool("headless") + gpuVal, _ := cmd.Flags().GetBool("gpu") kioskVal, _ := cmd.Flags().GetBool("kiosk") timeout, _ := cmd.Flags().GetInt("timeout") profileID, _ := cmd.Flags().GetString("profile-id") @@ -2470,6 +2541,7 @@ func runBrowsersCreate(cmd *cobra.Command, args []string) error { TimeoutSeconds: timeout, Stealth: BoolFlag{Set: cmd.Flags().Changed("stealth"), Value: stealthVal}, Headless: BoolFlag{Set: cmd.Flags().Changed("headless"), Value: headlessVal}, + GPU: BoolFlag{Set: cmd.Flags().Changed("gpu"), Value: gpuVal}, Kiosk: BoolFlag{Set: cmd.Flags().Changed("kiosk"), Value: kioskVal}, ProfileID: profileID, ProfileName: profileName, @@ -3001,6 +3073,24 @@ func runBrowsersComputerSetCursor(cmd *cobra.Command, args []string) error { return b.ComputerSetCursor(cmd.Context(), BrowsersComputerSetCursorInput{Identifier: args[0], Hidden: hidden}) } +func runBrowsersComputerGetMousePosition(cmd *cobra.Command, args []string) error { + client := getKernelClient(cmd) + svc := client.Browsers + output, _ := cmd.Flags().GetString("output") + + b := BrowsersCmd{browsers: &svc, computer: &svc.Computer} + return b.ComputerGetMousePosition(cmd.Context(), BrowsersComputerGetMousePositionInput{Identifier: args[0], Output: output}) +} + +func runBrowsersComputerBatch(cmd *cobra.Command, args []string) error { + client := getKernelClient(cmd) + svc := client.Browsers + actionsJSON, _ := cmd.Flags().GetString("actions") + + b := BrowsersCmd{browsers: &svc, computer: &svc.Computer} + return b.ComputerBatch(cmd.Context(), BrowsersComputerBatchInput{Identifier: args[0], ActionsJSON: actionsJSON}) +} + func truncateURL(url string, maxLen int) string { if len(url) <= maxLen { return url diff --git a/cmd/browsers_test.go b/cmd/browsers_test.go index 696e29a..0a1b13a 100644 --- a/cmd/browsers_test.go +++ b/cmd/browsers_test.go @@ -671,7 +671,9 @@ func makeStream[T any](vals []T) *ssestream.Stream[T] { // --- Fake for Computer --- type FakeComputerService struct { + BatchFunc func(ctx context.Context, id string, body kernel.BrowserComputerBatchParams, opts ...option.RequestOption) error ClickMouseFunc func(ctx context.Context, id string, body kernel.BrowserComputerClickMouseParams, opts ...option.RequestOption) error + GetMousePositionFunc func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.BrowserComputerGetMousePositionResponse, error) MoveMouseFunc func(ctx context.Context, id string, body kernel.BrowserComputerMoveMouseParams, opts ...option.RequestOption) error CaptureScreenshotFunc func(ctx context.Context, id string, body kernel.BrowserComputerCaptureScreenshotParams, opts ...option.RequestOption) (*http.Response, error) PressKeyFunc func(ctx context.Context, id string, body kernel.BrowserComputerPressKeyParams, opts ...option.RequestOption) error @@ -681,12 +683,24 @@ type FakeComputerService struct { SetCursorVisibilityFunc func(ctx context.Context, id string, body kernel.BrowserComputerSetCursorVisibilityParams, opts ...option.RequestOption) (*kernel.BrowserComputerSetCursorVisibilityResponse, error) } +func (f *FakeComputerService) Batch(ctx context.Context, id string, body kernel.BrowserComputerBatchParams, opts ...option.RequestOption) error { + if f.BatchFunc != nil { + return f.BatchFunc(ctx, id, body, opts...) + } + return nil +} func (f *FakeComputerService) ClickMouse(ctx context.Context, id string, body kernel.BrowserComputerClickMouseParams, opts ...option.RequestOption) error { if f.ClickMouseFunc != nil { return f.ClickMouseFunc(ctx, id, body, opts...) } return nil } +func (f *FakeComputerService) GetMousePosition(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.BrowserComputerGetMousePositionResponse, error) { + if f.GetMousePositionFunc != nil { + return f.GetMousePositionFunc(ctx, id, opts...) + } + return &kernel.BrowserComputerGetMousePositionResponse{X: 100, Y: 200}, nil +} func (f *FakeComputerService) MoveMouse(ctx context.Context, id string, body kernel.BrowserComputerMoveMouseParams, opts ...option.RequestOption) error { if f.MoveMouseFunc != nil { return f.MoveMouseFunc(ctx, id, body, opts...) diff --git a/go.mod b/go.mod index 436fdb3..1e7285d 100644 --- a/go.mod +++ b/go.mod @@ -9,14 +9,13 @@ require ( github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1 github.com/golang-jwt/jwt/v5 v5.2.2 github.com/joho/godotenv v1.5.1 - github.com/kernel/kernel-go-sdk v0.33.0 + github.com/kernel/kernel-go-sdk v0.35.0 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c - github.com/pquerna/otp v1.5.0 github.com/pterm/pterm v0.12.80 github.com/samber/lo v1.51.0 github.com/spf13/cobra v1.9.1 github.com/spf13/pflag v1.0.6 - github.com/stretchr/testify v1.11.0 + github.com/stretchr/testify v1.11.1 github.com/zalando/go-keyring v0.2.6 golang.org/x/crypto v0.47.0 golang.org/x/oauth2 v0.30.0 @@ -27,7 +26,6 @@ require ( atomicgo.dev/cursor v0.2.0 // indirect atomicgo.dev/keyboard v0.2.9 // indirect atomicgo.dev/schedule v0.1.0 // indirect - github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect github.com/charmbracelet/colorprofile v0.3.0 // indirect github.com/charmbracelet/x/ansi v0.8.0 // indirect github.com/charmbracelet/x/cellbuf v0.0.13 // indirect diff --git a/go.sum b/go.sum index d254c19..b50a470 100644 --- a/go.sum +++ b/go.sum @@ -22,8 +22,6 @@ github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lpr github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk= github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= -github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= -github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boyter/gocodewalker v1.4.0 h1:fVmFeQxKpj5tlpjPcyTtJ96btgaHYd9yn6m+T/66et4= github.com/boyter/gocodewalker v1.4.0/go.mod h1:hXG8xzR1uURS+99P5/3xh3uWHjaV2XfoMMmvPyhrCDg= github.com/charmbracelet/colorprofile v0.3.0 h1:KtLh9uuu1RCt+Hml4s6Hz+kB1PfV3wi++1h5ia65yKQ= @@ -66,8 +64,8 @@ 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.33.0 h1:kfk2bwrw3mbR4IW3JMnOj6Tecxor44YjM8YV153xDTY= -github.com/kernel/kernel-go-sdk v0.33.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.35.0 h1:zQcDPxq7N1njnNVoFmxvi3XMKoqemOVlnkVYuYPqAE0= +github.com/kernel/kernel-go-sdk v0.35.0/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= @@ -99,8 +97,6 @@ github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmd github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs= -github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI= github.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg= github.com/pterm/pterm v0.12.30/go.mod h1:MOqLIyMOgmTDz9yorcYbcw+HsgoZo3BQfg2wtl3HEFE= @@ -125,12 +121,11 @@ github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An 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= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8= -github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=