Skip to content

Commit 6c922e1

Browse files
sjmiller609kernel-internal[bot]cursoragentmasnwilliams
authored
Get mouse position, GPU mode browsers (#115)
<!-- CURSOR_SUMMARY --> > [!NOTE] > **Medium Risk** > Moderate risk due to upgrading `kernel-go-sdk` and wiring new request fields/CLI commands that call newly added browser computer APIs and JSON parsing. > > **Overview** > Adds a new `--gpu` flag to `browsers create`, passes it through to the Kernel API, and surfaces the GPU setting in `browsers get` output. > > Extends `browsers computer` with `get-mouse-position` (supports `--output json`) and `batch` (executes a JSON-described action list), including new SDK interface methods and test fakes to cover these calls. > > Bumps dependencies, notably `github.com/kernel/kernel-go-sdk` to `v0.35.0` (plus minor `testify` update). > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit b9dd123. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: kernel-internal[bot] <260533166+kernel-internal[bot]@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Mason Williams <masnwilliams@users.noreply.github.com> Co-authored-by: Mason Williams <mason@onkernel.com>
1 parent 928f53a commit 6c922e1

4 files changed

Lines changed: 111 additions & 14 deletions

File tree

cmd/browsers.go

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,11 @@ type BrowserPlaywrightService interface {
9292

9393
// BrowserComputerService defines the subset we use for OS-level mouse & screen.
9494
type BrowserComputerService interface {
95+
Batch(ctx context.Context, id string, body kernel.BrowserComputerBatchParams, opts ...option.RequestOption) (err error)
9596
CaptureScreenshot(ctx context.Context, id string, body kernel.BrowserComputerCaptureScreenshotParams, opts ...option.RequestOption) (res *http.Response, err error)
9697
ClickMouse(ctx context.Context, id string, body kernel.BrowserComputerClickMouseParams, opts ...option.RequestOption) (err error)
9798
DragMouse(ctx context.Context, id string, body kernel.BrowserComputerDragMouseParams, opts ...option.RequestOption) (err error)
99+
GetMousePosition(ctx context.Context, id string, opts ...option.RequestOption) (res *kernel.BrowserComputerGetMousePositionResponse, err error)
98100
MoveMouse(ctx context.Context, id string, body kernel.BrowserComputerMoveMouseParams, opts ...option.RequestOption) (err error)
99101
PressKey(ctx context.Context, id string, body kernel.BrowserComputerPressKeyParams, opts ...option.RequestOption) (err error)
100102
Scroll(ctx context.Context, id string, body kernel.BrowserComputerScrollParams, opts ...option.RequestOption) (err error)
@@ -172,6 +174,7 @@ type BrowsersCreateInput struct {
172174
TimeoutSeconds int
173175
Stealth BoolFlag
174176
Headless BoolFlag
177+
GPU BoolFlag
175178
Kiosk BoolFlag
176179
ProfileID string
177180
ProfileName string
@@ -340,6 +343,9 @@ func (b BrowsersCmd) Create(ctx context.Context, in BrowsersCreateInput) error {
340343
if in.Headless.Set {
341344
params.Headless = kernel.Opt(in.Headless.Value)
342345
}
346+
if in.GPU.Set {
347+
params.GPU = kernel.Opt(in.GPU.Value)
348+
}
343349
if in.Kiosk.Set {
344350
params.KioskMode = kernel.Opt(in.Kiosk.Value)
345351
}
@@ -527,6 +533,7 @@ func (b BrowsersCmd) Get(ctx context.Context, in BrowsersGetInput) error {
527533
tableData = append(tableData, []string{"Timeout (seconds)", fmt.Sprintf("%d", browser.TimeoutSeconds)})
528534
tableData = append(tableData, []string{"Headless", fmt.Sprintf("%t", browser.Headless)})
529535
tableData = append(tableData, []string{"Stealth", fmt.Sprintf("%t", browser.Stealth)})
536+
tableData = append(tableData, []string{"GPU", fmt.Sprintf("%t", browser.GPU)})
530537
tableData = append(tableData, []string{"Kiosk Mode", fmt.Sprintf("%t", browser.KioskMode)})
531538
if browser.Viewport.Width > 0 && browser.Viewport.Height > 0 {
532539
viewportStr := fmt.Sprintf("%dx%d", browser.Viewport.Width, browser.Viewport.Height)
@@ -740,6 +747,16 @@ type BrowsersComputerSetCursorInput struct {
740747
Hidden bool
741748
}
742749

750+
type BrowsersComputerGetMousePositionInput struct {
751+
Identifier string
752+
Output string
753+
}
754+
755+
type BrowsersComputerBatchInput struct {
756+
Identifier string
757+
ActionsJSON string
758+
}
759+
743760
func (b BrowsersCmd) ComputerClickMouse(ctx context.Context, in BrowsersComputerClickMouseInput) error {
744761
if b.computer == nil {
745762
pterm.Error.Println("computer service not available")
@@ -956,6 +973,49 @@ func (b BrowsersCmd) ComputerSetCursor(ctx context.Context, in BrowsersComputerS
956973
return nil
957974
}
958975

976+
func (b BrowsersCmd) ComputerGetMousePosition(ctx context.Context, in BrowsersComputerGetMousePositionInput) error {
977+
if b.computer == nil {
978+
pterm.Error.Println("computer service not available")
979+
return nil
980+
}
981+
br, err := b.browsers.Get(ctx, in.Identifier, kernel.BrowserGetParams{})
982+
if err != nil {
983+
return util.CleanedUpSdkError{Err: err}
984+
}
985+
res, err := b.computer.GetMousePosition(ctx, br.SessionID)
986+
if err != nil {
987+
return util.CleanedUpSdkError{Err: err}
988+
}
989+
if in.Output == "json" {
990+
enc := json.NewEncoder(os.Stdout)
991+
enc.SetIndent("", " ")
992+
return enc.Encode(res)
993+
}
994+
fmt.Printf("x: %d\ny: %d\n", res.X, res.Y)
995+
return nil
996+
}
997+
998+
func (b BrowsersCmd) ComputerBatch(ctx context.Context, in BrowsersComputerBatchInput) error {
999+
if b.computer == nil {
1000+
pterm.Error.Println("computer service not available")
1001+
return nil
1002+
}
1003+
br, err := b.browsers.Get(ctx, in.Identifier, kernel.BrowserGetParams{})
1004+
if err != nil {
1005+
return util.CleanedUpSdkError{Err: err}
1006+
}
1007+
var body kernel.BrowserComputerBatchParams
1008+
if err := json.Unmarshal([]byte(in.ActionsJSON), &body); err != nil {
1009+
pterm.Error.Printf("Invalid JSON: %v\n", err)
1010+
return nil
1011+
}
1012+
if err := b.computer.Batch(ctx, br.SessionID, body); err != nil {
1013+
return util.CleanedUpSdkError{Err: err}
1014+
}
1015+
pterm.Success.Println("Batch actions executed")
1016+
return nil
1017+
}
1018+
9591019
// Replays
9601020
type BrowsersReplaysListInput struct {
9611021
Identifier string
@@ -2300,7 +2360,16 @@ func init() {
23002360
computerSetCursor.Flags().String("hidden", "", "Whether to hide the cursor: true or false")
23012361
_ = computerSetCursor.MarkFlagRequired("hidden")
23022362

2303-
computerRoot.AddCommand(computerClick, computerMove, computerScreenshot, computerType, computerPressKey, computerScroll, computerDrag, computerSetCursor)
2363+
// computer get-mouse-position
2364+
computerGetMousePosition := &cobra.Command{Use: "get-mouse-position <id>", Short: "Get current mouse cursor position", Args: cobra.ExactArgs(1), RunE: runBrowsersComputerGetMousePosition}
2365+
computerGetMousePosition.Flags().StringP("output", "o", "", "Output format: json for raw API response")
2366+
2367+
// computer batch
2368+
computerBatch := &cobra.Command{Use: "batch <id>", Short: "Execute a batch of computer actions from JSON", Args: cobra.ExactArgs(1), RunE: runBrowsersComputerBatch}
2369+
computerBatch.Flags().String("actions", "", "JSON object with actions array (e.g., {\"actions\":[{\"type\":\"click_mouse\",...}]})")
2370+
_ = computerBatch.MarkFlagRequired("actions")
2371+
2372+
computerRoot.AddCommand(computerClick, computerMove, computerScreenshot, computerType, computerPressKey, computerScroll, computerDrag, computerSetCursor, computerGetMousePosition, computerBatch)
23042373
browsersCmd.AddCommand(computerRoot)
23052374

23062375
// playwright
@@ -2316,6 +2385,7 @@ func init() {
23162385
_ = browsersCreateCmd.Flags().MarkDeprecated("persistent-id", "use --timeout (up to 72 hours) and profiles instead")
23172386
browsersCreateCmd.Flags().BoolP("stealth", "s", false, "Launch browser in stealth mode to avoid detection")
23182387
browsersCreateCmd.Flags().BoolP("headless", "H", false, "Launch browser without GUI access")
2388+
browsersCreateCmd.Flags().Bool("gpu", false, "Launch browser with hardware-accelerated GPU rendering")
23192389
browsersCreateCmd.Flags().Bool("kiosk", false, "Launch browser in kiosk mode")
23202390
browsersCreateCmd.Flags().IntP("timeout", "t", 60, "Timeout in seconds for the browser session")
23212391
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 {
23592429
}
23602430
stealthVal, _ := cmd.Flags().GetBool("stealth")
23612431
headlessVal, _ := cmd.Flags().GetBool("headless")
2432+
gpuVal, _ := cmd.Flags().GetBool("gpu")
23622433
kioskVal, _ := cmd.Flags().GetBool("kiosk")
23632434
timeout, _ := cmd.Flags().GetInt("timeout")
23642435
profileID, _ := cmd.Flags().GetString("profile-id")
@@ -2470,6 +2541,7 @@ func runBrowsersCreate(cmd *cobra.Command, args []string) error {
24702541
TimeoutSeconds: timeout,
24712542
Stealth: BoolFlag{Set: cmd.Flags().Changed("stealth"), Value: stealthVal},
24722543
Headless: BoolFlag{Set: cmd.Flags().Changed("headless"), Value: headlessVal},
2544+
GPU: BoolFlag{Set: cmd.Flags().Changed("gpu"), Value: gpuVal},
24732545
Kiosk: BoolFlag{Set: cmd.Flags().Changed("kiosk"), Value: kioskVal},
24742546
ProfileID: profileID,
24752547
ProfileName: profileName,
@@ -3001,6 +3073,24 @@ func runBrowsersComputerSetCursor(cmd *cobra.Command, args []string) error {
30013073
return b.ComputerSetCursor(cmd.Context(), BrowsersComputerSetCursorInput{Identifier: args[0], Hidden: hidden})
30023074
}
30033075

3076+
func runBrowsersComputerGetMousePosition(cmd *cobra.Command, args []string) error {
3077+
client := getKernelClient(cmd)
3078+
svc := client.Browsers
3079+
output, _ := cmd.Flags().GetString("output")
3080+
3081+
b := BrowsersCmd{browsers: &svc, computer: &svc.Computer}
3082+
return b.ComputerGetMousePosition(cmd.Context(), BrowsersComputerGetMousePositionInput{Identifier: args[0], Output: output})
3083+
}
3084+
3085+
func runBrowsersComputerBatch(cmd *cobra.Command, args []string) error {
3086+
client := getKernelClient(cmd)
3087+
svc := client.Browsers
3088+
actionsJSON, _ := cmd.Flags().GetString("actions")
3089+
3090+
b := BrowsersCmd{browsers: &svc, computer: &svc.Computer}
3091+
return b.ComputerBatch(cmd.Context(), BrowsersComputerBatchInput{Identifier: args[0], ActionsJSON: actionsJSON})
3092+
}
3093+
30043094
func truncateURL(url string, maxLen int) string {
30053095
if len(url) <= maxLen {
30063096
return url

cmd/browsers_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -671,7 +671,9 @@ func makeStream[T any](vals []T) *ssestream.Stream[T] {
671671
// --- Fake for Computer ---
672672

673673
type FakeComputerService struct {
674+
BatchFunc func(ctx context.Context, id string, body kernel.BrowserComputerBatchParams, opts ...option.RequestOption) error
674675
ClickMouseFunc func(ctx context.Context, id string, body kernel.BrowserComputerClickMouseParams, opts ...option.RequestOption) error
676+
GetMousePositionFunc func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.BrowserComputerGetMousePositionResponse, error)
675677
MoveMouseFunc func(ctx context.Context, id string, body kernel.BrowserComputerMoveMouseParams, opts ...option.RequestOption) error
676678
CaptureScreenshotFunc func(ctx context.Context, id string, body kernel.BrowserComputerCaptureScreenshotParams, opts ...option.RequestOption) (*http.Response, error)
677679
PressKeyFunc func(ctx context.Context, id string, body kernel.BrowserComputerPressKeyParams, opts ...option.RequestOption) error
@@ -681,12 +683,24 @@ type FakeComputerService struct {
681683
SetCursorVisibilityFunc func(ctx context.Context, id string, body kernel.BrowserComputerSetCursorVisibilityParams, opts ...option.RequestOption) (*kernel.BrowserComputerSetCursorVisibilityResponse, error)
682684
}
683685

686+
func (f *FakeComputerService) Batch(ctx context.Context, id string, body kernel.BrowserComputerBatchParams, opts ...option.RequestOption) error {
687+
if f.BatchFunc != nil {
688+
return f.BatchFunc(ctx, id, body, opts...)
689+
}
690+
return nil
691+
}
684692
func (f *FakeComputerService) ClickMouse(ctx context.Context, id string, body kernel.BrowserComputerClickMouseParams, opts ...option.RequestOption) error {
685693
if f.ClickMouseFunc != nil {
686694
return f.ClickMouseFunc(ctx, id, body, opts...)
687695
}
688696
return nil
689697
}
698+
func (f *FakeComputerService) GetMousePosition(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.BrowserComputerGetMousePositionResponse, error) {
699+
if f.GetMousePositionFunc != nil {
700+
return f.GetMousePositionFunc(ctx, id, opts...)
701+
}
702+
return &kernel.BrowserComputerGetMousePositionResponse{X: 100, Y: 200}, nil
703+
}
690704
func (f *FakeComputerService) MoveMouse(ctx context.Context, id string, body kernel.BrowserComputerMoveMouseParams, opts ...option.RequestOption) error {
691705
if f.MoveMouseFunc != nil {
692706
return f.MoveMouseFunc(ctx, id, body, opts...)

go.mod

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,13 @@ require (
99
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1
1010
github.com/golang-jwt/jwt/v5 v5.2.2
1111
github.com/joho/godotenv v1.5.1
12-
github.com/kernel/kernel-go-sdk v0.33.0
12+
github.com/kernel/kernel-go-sdk v0.35.0
1313
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
14-
github.com/pquerna/otp v1.5.0
1514
github.com/pterm/pterm v0.12.80
1615
github.com/samber/lo v1.51.0
1716
github.com/spf13/cobra v1.9.1
1817
github.com/spf13/pflag v1.0.6
19-
github.com/stretchr/testify v1.11.0
18+
github.com/stretchr/testify v1.11.1
2019
github.com/zalando/go-keyring v0.2.6
2120
golang.org/x/crypto v0.47.0
2221
golang.org/x/oauth2 v0.30.0
@@ -27,7 +26,6 @@ require (
2726
atomicgo.dev/cursor v0.2.0 // indirect
2827
atomicgo.dev/keyboard v0.2.9 // indirect
2928
atomicgo.dev/schedule v0.1.0 // indirect
30-
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
3129
github.com/charmbracelet/colorprofile v0.3.0 // indirect
3230
github.com/charmbracelet/x/ansi v0.8.0 // indirect
3331
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect

go.sum

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@ github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lpr
2222
github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk=
2323
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
2424
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
25-
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=
26-
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
2725
github.com/boyter/gocodewalker v1.4.0 h1:fVmFeQxKpj5tlpjPcyTtJ96btgaHYd9yn6m+T/66et4=
2826
github.com/boyter/gocodewalker v1.4.0/go.mod h1:hXG8xzR1uURS+99P5/3xh3uWHjaV2XfoMMmvPyhrCDg=
2927
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
6664
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
6765
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
6866
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
69-
github.com/kernel/kernel-go-sdk v0.33.0 h1:kfk2bwrw3mbR4IW3JMnOj6Tecxor44YjM8YV153xDTY=
70-
github.com/kernel/kernel-go-sdk v0.33.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ=
67+
github.com/kernel/kernel-go-sdk v0.35.0 h1:zQcDPxq7N1njnNVoFmxvi3XMKoqemOVlnkVYuYPqAE0=
68+
github.com/kernel/kernel-go-sdk v0.35.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ=
7169
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
7270
github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
7371
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
9997
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
10098
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
10199
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
102-
github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
103-
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
104100
github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI=
105101
github.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg=
106102
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
125121
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
126122
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
127123
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
128-
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
129124
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
130125
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
131126
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
132-
github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=
133-
github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
127+
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
128+
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
134129
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
135130
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
136131
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=

0 commit comments

Comments
 (0)