Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
83 changes: 83 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,40 @@ Create an API key from the [Kernel dashboard](https://dashboard.onkernel.com).
- `--timeout <seconds>` - Maximum execution time in seconds (defaults server-side)
- If `[code]` is omitted, code is read from stdin

### Claude Extension

The `kernel claude` commands provide a complete workflow for using the Claude for Chrome extension in Kernel browsers:

- `kernel claude extract` - Extract Claude extension from local Chrome
- `-o, --output <path>` - Output path for the bundle zip file (default: claude-bundle.zip)
- `--chrome-profile <name>` - Chrome profile name to extract from (default: Default)
- `--no-auth` - Skip authentication storage (extension will require login)
- `--list-profiles` - List available Chrome profiles and exit

- `kernel claude launch` - Create a browser with Claude extension loaded
- `-b, --bundle <path>` - Path to the Claude bundle zip file (required)
- `-t, --timeout <seconds>` - Session timeout in seconds (default: 600)
- `-s, --stealth` - Launch browser in stealth mode
- `-H, --headless` - Launch browser in headless mode
- `--url <url>` - Initial URL to navigate to (default: https://claude.ai)
- `--chat` - Start interactive chat after launch
- `--viewport <WxH[@R]>` - Browser viewport size (e.g., 1920x1080@25)

- `kernel claude load <browser-id>` - Load Claude extension into existing browser
- `-b, --bundle <path>` - Path to the Claude bundle zip file (required)

- `kernel claude status <browser-id>` - Check Claude extension status
- `-o, --output json` - Output format: json for raw response

- `kernel claude send <browser-id> [message]` - Send a message to Claude
- `-f, --file <path>` - Read message from file
- `--timeout <seconds>` - Response timeout in seconds (default: 120)
- `--json` - Output response as JSON
- `--raw` - Output raw response without formatting

- `kernel claude chat <browser-id>` - Interactive chat with Claude
- `--no-tui` - Disable interactive mode (line-by-line I/O)

### Extension Management

- `kernel extensions list` - List all uploaded extensions
Expand Down Expand Up @@ -528,6 +562,55 @@ return { opsPerSec, ops, durationMs };
TS
```

### Claude Extension

```bash
# Step 1: Extract the Claude extension from your local Chrome (run on your machine)
kernel claude extract -o claude-bundle.zip

# List available Chrome profiles
kernel claude extract --list-profiles

# Extract from a specific Chrome profile
kernel claude extract --chrome-profile "Profile 1" -o claude-bundle.zip

# Extract without authentication (will require login)
kernel claude extract --no-auth -o claude-bundle.zip

# Step 2: Launch a browser with Claude pre-loaded
kernel claude launch -b claude-bundle.zip

# Launch with longer timeout (1 hour)
kernel claude launch -b claude-bundle.zip -t 3600

# Launch in stealth mode
kernel claude launch -b claude-bundle.zip --stealth

# Launch and immediately start interactive chat
kernel claude launch -b claude-bundle.zip --chat

# Load Claude into an existing browser
kernel claude load abc123xyz -b claude-bundle.zip

# Check Claude extension status
kernel claude status abc123xyz

# Send a single message (great for scripting)
kernel claude send abc123xyz "What is 2+2?"

# Pipe a message from stdin
echo "Explain this code" | kernel claude send abc123xyz

# Read message from a file
kernel claude send abc123xyz -f prompt.txt

# Get response as JSON
kernel claude send abc123xyz "Hello" --json

# Start interactive chat session
kernel claude chat abc123xyz
```

### Extension management

```bash
Expand Down
226 changes: 226 additions & 0 deletions cmd/claude/chat.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
package claude

import (
"bufio"
"context"
"encoding/json"
"fmt"
"os"
"strings"

"github.com/onkernel/cli/internal/claude"
"github.com/onkernel/cli/pkg/util"
"github.com/onkernel/kernel-go-sdk"
"github.com/pterm/pterm"
"github.com/spf13/cobra"
)

var chatCmd = &cobra.Command{
Use: "chat <browser-id>",
Short: "Interactive chat with Claude",
Long: `Start an interactive chat session with Claude in a Kernel browser.

This provides a simple command-line interface for having a conversation
with Claude. Type your messages and receive responses directly in the terminal.

CLI commands:
/quit, /exit - Exit the chat session
/clear - Clear the terminal

All other slash commands (like /hn-summary) are passed to Claude.`,
Example: ` # Start chat with existing browser
kernel claude chat abc123xyz

# Launch new browser and start chatting
kernel claude launch -b claude-bundle.zip --chat`,
Args: cobra.ExactArgs(1),
RunE: runChat,
}

func init() {
chatCmd.Flags().Bool("no-tui", false, "Disable interactive mode (line-by-line I/O)")
}

func runChat(cmd *cobra.Command, args []string) error {
browserID := args[0]
// noTUI, _ := cmd.Flags().GetBool("no-tui")
// For now, both modes use the same implementation

ctx := cmd.Context()
client := util.GetKernelClient(cmd)

return runChatWithBrowser(ctx, client, browserID)
}

func runChatWithBrowser(ctx context.Context, client kernel.Client, browserID string) error {
// Verify the browser exists
pterm.Info.Printf("Connecting to browser: %s\n", browserID)

browser, err := client.Browsers.Get(ctx, browserID)
if err != nil {
return fmt.Errorf("failed to get browser: %w", err)
}

// Open the side panel by clicking the extension icon
if err := claude.OpenSidePanel(ctx, client, browser.SessionID); err != nil {
return fmt.Errorf("failed to open side panel: %w", err)
}

// Check Claude status first
pterm.Info.Println("Checking Claude extension status...")
statusResult, err := client.Browsers.Playwright.Execute(ctx, browser.SessionID, kernel.BrowserPlaywrightExecuteParams{
Code: claude.CheckStatusScript,
TimeoutSec: kernel.Opt(int64(30)),
})
if err != nil {
return fmt.Errorf("failed to check status: %w", err)
}

if statusResult.Result != nil {
var status struct {
ExtensionLoaded bool `json:"extensionLoaded"`
Authenticated bool `json:"authenticated"`
Error string `json:"error"`
}
resultBytes, _ := json.Marshal(statusResult.Result)
_ = json.Unmarshal(resultBytes, &status)

if !status.ExtensionLoaded {
return fmt.Errorf("Claude extension is not loaded. Load it first with: kernel claude load %s -b claude-bundle.zip", browserID)
}
if !status.Authenticated {
pterm.Warning.Println("Claude extension is not authenticated.")
pterm.Info.Printf("Please log in via the live view: %s\n", browser.BrowserLiveViewURL)
return fmt.Errorf("authentication required")
}
}

// Display chat header
pterm.Println()
pterm.DefaultHeader.WithBackgroundStyle(pterm.NewStyle(pterm.BgBlue)).
WithTextStyle(pterm.NewStyle(pterm.FgWhite)).
Println("Claude Chat")
pterm.Println()
pterm.Info.Printf("Browser: %s\n", browserID)
pterm.Info.Printf("Live View: %s\n", browser.BrowserLiveViewURL)
pterm.Println()
pterm.Info.Println("Type your message and press Enter. Use /quit to exit, /clear to clear screen.")
pterm.Println()

// Start the chat loop
scanner := bufio.NewScanner(os.Stdin)
messageCount := 0

for {
// Show prompt
pterm.Print(pterm.Cyan("You: "))

if !scanner.Scan() {
break
}

input := strings.TrimSpace(scanner.Text())
if input == "" {
continue
}

// Handle special commands
if strings.HasPrefix(input, "/") {
handled, shouldExit := handleChatCommand(ctx, client, browserID, input)
if shouldExit {
pterm.Info.Println("Goodbye!")
return nil
}
if handled {
continue
}
}

// Send the message
messageCount++
spinner, _ := pterm.DefaultSpinner.Start("Claude is thinking...")

response, err := sendChatMessage(ctx, client, browser.SessionID, input)
spinner.Stop()

if err != nil {
pterm.Error.Printf("Error: %v\n", err)
pterm.Println()
continue
}

// Display the response
pterm.Println()
pterm.Print(pterm.Green("Claude: "))
fmt.Println(response)
pterm.Println()
}

return nil
}

func handleChatCommand(ctx context.Context, client kernel.Client, browserID, input string) (handled bool, shouldExit bool) {
parts := strings.Fields(input)
if len(parts) == 0 {
return false, false
}

cmd := strings.ToLower(parts[0])

switch cmd {
case "/quit", "/exit", "/q":
return true, true

case "/clear":
// Clear terminal (works on most terminals)
fmt.Print("\033[H\033[2J")
pterm.Info.Println("Terminal cleared.")
return true, false

default:
// Pass all other slash commands through to Claude
// (Claude for Chrome has its own slash commands like /hn-summary)
return false, false
}
}

func sendChatMessage(ctx context.Context, client kernel.Client, browserID, message string) (string, error) {
// Build the script with the message
script := fmt.Sprintf(`
process.env.CLAUDE_MESSAGE = %s;
process.env.CLAUDE_TIMEOUT_MS = '300000';

%s
`, jsonMarshalString(message), claude.SendMessageScript)

result, err := client.Browsers.Playwright.Execute(ctx, browserID, kernel.BrowserPlaywrightExecuteParams{
Code: script,
TimeoutSec: kernel.Opt(int64(330)), // 5.5 minutes
})
if err != nil {
return "", fmt.Errorf("failed to send message: %w", err)
}

if !result.Success {
if result.Error != "" {
return "", fmt.Errorf("%s", result.Error)
}
return "", fmt.Errorf("send failed")
}

// Parse the result
var response struct {
Response string `json:"response"`
Warning string `json:"warning"`
}
if result.Result != nil {
resultBytes, _ := json.Marshal(result.Result)
_ = json.Unmarshal(resultBytes, &response)
}

if response.Response == "" {
return "", fmt.Errorf("empty response")
}

return response.Response, nil
}
51 changes: 51 additions & 0 deletions cmd/claude/claude.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Package claude provides commands for interacting with the Claude for Chrome extension
// in Kernel browsers.
package claude

import (
"github.com/spf13/cobra"
)

// ClaudeCmd is the parent command for Claude extension operations
var ClaudeCmd = &cobra.Command{
Use: "claude",
Short: "Interact with Claude for Chrome extension in Kernel browsers",
Long: `Commands for using the Claude for Chrome extension in Kernel browsers.

This command group provides a complete workflow for:
- Extracting the Claude extension from your local Chrome installation
- Launching Kernel browsers with the extension pre-loaded
- Sending messages and interacting with Claude programmatically

Example workflow:
# Extract extension from local Chrome (run on your machine)
kernel claude extract -o claude-bundle.zip

# Transfer to server if needed
scp claude-bundle.zip server:~/

# Launch a browser with Claude
kernel claude launch -b claude-bundle.zip

# Send a message
kernel claude send <browser-id> "Hello Claude!"

# Start interactive chat
kernel claude chat <browser-id>

For more info: https://docs.onkernel.com/claude`,
Run: func(cmd *cobra.Command, args []string) {
// Show help if called without subcommands
_ = cmd.Help()
},
}

func init() {
// Register subcommands
ClaudeCmd.AddCommand(extractCmd)
ClaudeCmd.AddCommand(launchCmd)
ClaudeCmd.AddCommand(loadCmd)
ClaudeCmd.AddCommand(statusCmd)
ClaudeCmd.AddCommand(sendCmd)
ClaudeCmd.AddCommand(chatCmd)
}
Loading