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
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,12 @@ Create an API key from the [Kernel dashboard](https://dashboard.onkernel.com).
- `--button <button>` - Mouse button: left, middle, right (default: left)
- `--hold-key <key>` - Modifier keys to hold (repeatable)

### Browser Playwright

- `kernel browsers playwright execute <id or persistent id> [code]` - Execute Playwright/TypeScript code against the browser
- `--timeout <seconds>` - Maximum execution time in seconds (defaults server-side)
- If `[code]` is omitted, code is read from stdin

### Extension Management

- `kernel extensions list` - List all uploaded extensions
Expand Down Expand Up @@ -395,6 +401,37 @@ kernel browsers computer type my-browser --text "Hello, World!"

# Type text with a 100ms delay between keystrokes
kernel browsers computer type my-browser --text "Slow typing..." --delay 100

```

### Playwright execution

```bash
# Execute inline Playwright (TypeScript) code
kernel browsers playwright execute my-browser 'await page.goto("https://example.com"); const title = await page.title(); return title;'

# Or pipe code from stdin
cat <<'TS' | kernel browsers playwright execute my-browser
await page.goto("https://example.com");
const title = await page.title();
return { title };
TS

# With a timeout in seconds
kernel browsers playwright execute my-browser --timeout 30 'await (await context.newPage()).goto("https://example.com")'

# Mini CDP connection load test (10s)
cat <<'TS' | kernel browsers playwright execute my-browser
const start = Date.now();
let ops = 0;
while (Date.now() - start < 10_000) {
await page.evaluate("new Date();");
ops++;
}
const durationMs = Date.now() - start;
const opsPerSec = ops / (durationMs / 1000);
return { opsPerSec, ops, durationMs };
TS
```

### Extension management
Expand Down
104 changes: 98 additions & 6 deletions cmd/browsers.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ type BrowserLogService interface {
StreamStreaming(ctx context.Context, id string, query kernel.BrowserLogStreamParams, opts ...option.RequestOption) (stream *ssestream.Stream[shared.LogEvent])
}

// BrowserPlaywrightService defines the subset we use for Playwright execution.
type BrowserPlaywrightService interface {
Execute(ctx context.Context, id string, body kernel.BrowserPlaywrightExecuteParams, opts ...option.RequestOption) (res *kernel.BrowserPlaywrightExecuteResponse, err error)
}

// BrowserComputerService defines the subset we use for OS-level mouse & screen.
type BrowserComputerService interface {
CaptureScreenshot(ctx context.Context, id string, body kernel.BrowserComputerCaptureScreenshotParams, opts ...option.RequestOption) (res *http.Response, err error)
Expand Down Expand Up @@ -166,12 +171,13 @@ type BrowsersViewInput struct {

// BrowsersCmd is a cobra-independent command handler for browsers operations.
type BrowsersCmd struct {
browsers BrowsersService
replays BrowserReplaysService
fs BrowserFSService
process BrowserProcessService
logs BrowserLogService
computer BrowserComputerService
browsers BrowsersService
replays BrowserReplaysService
fs BrowserFSService
process BrowserProcessService
logs BrowserLogService
computer BrowserComputerService
playwright BrowserPlaywrightService
}

type BrowsersListInput struct {
Expand Down Expand Up @@ -940,6 +946,59 @@ type BrowsersProcessStdoutStreamInput struct {
ProcessID string
}

// Playwright
type BrowsersPlaywrightExecuteInput struct {
Identifier string
Code string
Timeout int64
}

func (b BrowsersCmd) PlaywrightExecute(ctx context.Context, in BrowsersPlaywrightExecuteInput) error {
if b.playwright == nil {
pterm.Error.Println("playwright service not available")
return nil
}
br, err := b.resolveBrowserByIdentifier(ctx, in.Identifier)
if err != nil {
return util.CleanedUpSdkError{Err: err}
}
if br == nil {
pterm.Error.Printf("Browser '%s' not found\n", in.Identifier)
return nil
}
params := kernel.BrowserPlaywrightExecuteParams{Code: in.Code}
if in.Timeout > 0 {
params.TimeoutSec = kernel.Opt(in.Timeout)
}
res, err := b.playwright.Execute(ctx, br.SessionID, params)
if err != nil {
return util.CleanedUpSdkError{Err: err}
}

rows := pterm.TableData{{"Property", "Value"}, {"Success", fmt.Sprintf("%t", res.Success)}}
PrintTableNoPad(rows, true)

if res.Stdout != "" {
pterm.Info.Println("stdout:")
fmt.Println(res.Stdout)
}
if res.Stderr != "" {
pterm.Info.Println("stderr:")
fmt.Fprintln(os.Stderr, res.Stderr)
}
if res.Result != nil {
bs, err := json.MarshalIndent(res.Result, "", " ")
if err == nil {
pterm.Info.Println("result:")
fmt.Println(string(bs))
}
}
if !res.Success && res.Error != "" {
pterm.Error.Printf("error: %s\n", res.Error)
}
return nil
}

func (b BrowsersCmd) ProcessExec(ctx context.Context, in BrowsersProcessExecInput) error {
if b.process == nil {
pterm.Error.Println("process service not available")
Expand Down Expand Up @@ -1898,6 +1957,13 @@ func init() {
computerRoot.AddCommand(computerClick, computerMove, computerScreenshot, computerType, computerPressKey, computerScroll, computerDrag)
browsersCmd.AddCommand(computerRoot)

// playwright
playwrightRoot := &cobra.Command{Use: "playwright", Short: "Playwright operations"}
playwrightExecute := &cobra.Command{Use: "execute <id|persistent-id> [code]", Short: "Execute Playwright/TypeScript code against the browser", Args: cobra.MinimumNArgs(1), RunE: runBrowsersPlaywrightExecute}
playwrightExecute.Flags().Int64("timeout", 0, "Maximum execution time in seconds (default per server)")
playwrightRoot.AddCommand(playwrightExecute)
browsersCmd.AddCommand(playwrightRoot)

// Add flags for create command
browsersCreateCmd.Flags().StringP("persistent-id", "p", "", "Unique identifier for browser session persistence")
browsersCreateCmd.Flags().BoolP("stealth", "s", false, "Launch browser in stealth mode to avoid detection")
Expand Down Expand Up @@ -2120,6 +2186,32 @@ func runBrowsersProcessStdoutStream(cmd *cobra.Command, args []string) error {
return b.ProcessStdoutStream(cmd.Context(), BrowsersProcessStdoutStreamInput{Identifier: args[0], ProcessID: args[1]})
}

func runBrowsersPlaywrightExecute(cmd *cobra.Command, args []string) error {
client := getKernelClient(cmd)
svc := client.Browsers

var code string
if len(args) >= 2 {
code = strings.Join(args[1:], " ")
} else {
// Read code from stdin
stat, _ := os.Stdin.Stat()
if (stat.Mode() & os.ModeCharDevice) != 0 {
pterm.Error.Println("no code provided. Provide code as an argument or pipe via stdin")
return nil
}
data, err := io.ReadAll(os.Stdin)
if err != nil {
pterm.Error.Printf("failed to read stdin: %v\n", err)
return nil
}
code = string(data)
}
timeout, _ := cmd.Flags().GetInt64("timeout")
b := BrowsersCmd{browsers: &svc, playwright: &svc.Playwright}
return b.PlaywrightExecute(cmd.Context(), BrowsersPlaywrightExecuteInput{Identifier: args[0], Code: strings.TrimSpace(code), Timeout: timeout})
}

func runBrowsersFSNewDirectory(cmd *cobra.Command, args []string) error {
client := getKernelClient(cmd)
svc := client.Browsers
Expand Down