Skip to content

Commit 1c90d53

Browse files
committed
Add 'kernel claude' command group for Claude extension support
Implement a complete workflow for using the Claude for Chrome extension in Kernel browsers: Commands: - kernel claude extract: Extract extension + auth from local Chrome - kernel claude launch: Create browser with Claude pre-loaded - kernel claude load: Load extension into existing browser - kernel claude status: Check extension/auth status - kernel claude send: Send single message (scriptable) - kernel claude chat: Interactive TUI chat session Features: - Cross-platform Chrome path detection (macOS, Linux, Windows) - Bundle format with extension and auth storage - Embedded Playwright scripts for browser automation - Scriptable send command with stdin/file/JSON support - Interactive chat with /help, /status, /clear commands
1 parent b1a3793 commit 1c90d53

19 files changed

Lines changed: 2350 additions & 0 deletions

README.md

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,40 @@ Create an API key from the [Kernel dashboard](https://dashboard.onkernel.com).
339339
- `--timeout <seconds>` - Maximum execution time in seconds (defaults server-side)
340340
- If `[code]` is omitted, code is read from stdin
341341

342+
### Claude Extension
343+
344+
The `kernel claude` commands provide a complete workflow for using the Claude for Chrome extension in Kernel browsers:
345+
346+
- `kernel claude extract` - Extract Claude extension from local Chrome
347+
- `-o, --output <path>` - Output path for the bundle zip file (default: claude-bundle.zip)
348+
- `--chrome-profile <name>` - Chrome profile name to extract from (default: Default)
349+
- `--no-auth` - Skip authentication storage (extension will require login)
350+
- `--list-profiles` - List available Chrome profiles and exit
351+
352+
- `kernel claude launch` - Create a browser with Claude extension loaded
353+
- `-b, --bundle <path>` - Path to the Claude bundle zip file (required)
354+
- `-t, --timeout <seconds>` - Session timeout in seconds (default: 600)
355+
- `-s, --stealth` - Launch browser in stealth mode
356+
- `-H, --headless` - Launch browser in headless mode
357+
- `--url <url>` - Initial URL to navigate to (default: https://claude.ai)
358+
- `--chat` - Start interactive chat after launch
359+
- `--viewport <WxH[@R]>` - Browser viewport size (e.g., 1920x1080@25)
360+
361+
- `kernel claude load <browser-id>` - Load Claude extension into existing browser
362+
- `-b, --bundle <path>` - Path to the Claude bundle zip file (required)
363+
364+
- `kernel claude status <browser-id>` - Check Claude extension status
365+
- `-o, --output json` - Output format: json for raw response
366+
367+
- `kernel claude send <browser-id> [message]` - Send a message to Claude
368+
- `-f, --file <path>` - Read message from file
369+
- `--timeout <seconds>` - Response timeout in seconds (default: 120)
370+
- `--json` - Output response as JSON
371+
- `--raw` - Output raw response without formatting
372+
373+
- `kernel claude chat <browser-id>` - Interactive chat with Claude
374+
- `--no-tui` - Disable interactive mode (line-by-line I/O)
375+
342376
### Extension Management
343377

344378
- `kernel extensions list` - List all uploaded extensions
@@ -528,6 +562,55 @@ return { opsPerSec, ops, durationMs };
528562
TS
529563
```
530564

565+
### Claude Extension
566+
567+
```bash
568+
# Step 1: Extract the Claude extension from your local Chrome (run on your machine)
569+
kernel claude extract -o claude-bundle.zip
570+
571+
# List available Chrome profiles
572+
kernel claude extract --list-profiles
573+
574+
# Extract from a specific Chrome profile
575+
kernel claude extract --chrome-profile "Profile 1" -o claude-bundle.zip
576+
577+
# Extract without authentication (will require login)
578+
kernel claude extract --no-auth -o claude-bundle.zip
579+
580+
# Step 2: Launch a browser with Claude pre-loaded
581+
kernel claude launch -b claude-bundle.zip
582+
583+
# Launch with longer timeout (1 hour)
584+
kernel claude launch -b claude-bundle.zip -t 3600
585+
586+
# Launch in stealth mode
587+
kernel claude launch -b claude-bundle.zip --stealth
588+
589+
# Launch and immediately start interactive chat
590+
kernel claude launch -b claude-bundle.zip --chat
591+
592+
# Load Claude into an existing browser
593+
kernel claude load abc123xyz -b claude-bundle.zip
594+
595+
# Check Claude extension status
596+
kernel claude status abc123xyz
597+
598+
# Send a single message (great for scripting)
599+
kernel claude send abc123xyz "What is 2+2?"
600+
601+
# Pipe a message from stdin
602+
echo "Explain this code" | kernel claude send abc123xyz
603+
604+
# Read message from a file
605+
kernel claude send abc123xyz -f prompt.txt
606+
607+
# Get response as JSON
608+
kernel claude send abc123xyz "Hello" --json
609+
610+
# Start interactive chat session
611+
kernel claude chat abc123xyz
612+
```
613+
531614
### Extension management
532615

533616
```bash

cmd/claude/chat.go

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
package claude
2+
3+
import (
4+
"bufio"
5+
"context"
6+
"encoding/json"
7+
"fmt"
8+
"os"
9+
"strings"
10+
11+
"github.com/onkernel/cli/internal/claude"
12+
"github.com/onkernel/cli/pkg/util"
13+
"github.com/onkernel/kernel-go-sdk"
14+
"github.com/pterm/pterm"
15+
"github.com/spf13/cobra"
16+
)
17+
18+
var chatCmd = &cobra.Command{
19+
Use: "chat <browser-id>",
20+
Short: "Interactive chat with Claude",
21+
Long: `Start an interactive chat session with Claude in a Kernel browser.
22+
23+
This provides a simple command-line interface for having a conversation
24+
with Claude. Type your messages and receive responses directly in the terminal.
25+
26+
Special commands:
27+
/quit, /exit - Exit the chat session
28+
/clear - Clear the terminal
29+
/status - Check extension status
30+
/help - Show available commands`,
31+
Example: ` # Start chat with existing browser
32+
kernel claude chat abc123xyz
33+
34+
# Launch new browser and start chatting
35+
kernel claude launch -b claude-bundle.zip --chat`,
36+
Args: cobra.ExactArgs(1),
37+
RunE: runChat,
38+
}
39+
40+
func init() {
41+
chatCmd.Flags().Bool("no-tui", false, "Disable interactive mode (line-by-line I/O)")
42+
}
43+
44+
func runChat(cmd *cobra.Command, args []string) error {
45+
browserID := args[0]
46+
// noTUI, _ := cmd.Flags().GetBool("no-tui")
47+
// For now, both modes use the same implementation
48+
49+
ctx := cmd.Context()
50+
client := util.GetKernelClient(cmd)
51+
52+
return runChatWithBrowser(ctx, client, browserID)
53+
}
54+
55+
func runChatWithBrowser(ctx context.Context, client kernel.Client, browserID string) error {
56+
// Verify the browser exists
57+
pterm.Info.Printf("Connecting to browser: %s\n", browserID)
58+
59+
browser, err := client.Browsers.Get(ctx, browserID)
60+
if err != nil {
61+
return fmt.Errorf("failed to get browser: %w", err)
62+
}
63+
64+
// Check Claude status first
65+
pterm.Info.Println("Checking Claude extension status...")
66+
statusResult, err := client.Browsers.Playwright.Execute(ctx, browser.SessionID, kernel.BrowserPlaywrightExecuteParams{
67+
Code: claude.CheckStatusScript,
68+
TimeoutSec: kernel.Opt(int64(30)),
69+
})
70+
if err != nil {
71+
return fmt.Errorf("failed to check status: %w", err)
72+
}
73+
74+
if statusResult.Result != nil {
75+
var status struct {
76+
ExtensionLoaded bool `json:"extensionLoaded"`
77+
Authenticated bool `json:"authenticated"`
78+
Error string `json:"error"`
79+
}
80+
resultBytes, _ := json.Marshal(statusResult.Result)
81+
_ = json.Unmarshal(resultBytes, &status)
82+
83+
if !status.ExtensionLoaded {
84+
return fmt.Errorf("Claude extension is not loaded. Load it first with: kernel claude load %s -b claude-bundle.zip", browserID)
85+
}
86+
if !status.Authenticated {
87+
pterm.Warning.Println("Claude extension is not authenticated.")
88+
pterm.Info.Printf("Please log in via the live view: %s\n", browser.BrowserLiveViewURL)
89+
return fmt.Errorf("authentication required")
90+
}
91+
}
92+
93+
// Display chat header
94+
pterm.Println()
95+
pterm.DefaultHeader.WithBackgroundStyle(pterm.NewStyle(pterm.BgBlue)).
96+
WithTextStyle(pterm.NewStyle(pterm.FgWhite)).
97+
Println("Claude Chat")
98+
pterm.Println()
99+
pterm.Info.Printf("Browser: %s\n", browserID)
100+
pterm.Info.Printf("Live View: %s\n", browser.BrowserLiveViewURL)
101+
pterm.Println()
102+
pterm.Info.Println("Type your message and press Enter. Use /help for commands, /quit to exit.")
103+
pterm.Println()
104+
105+
// Start the chat loop
106+
scanner := bufio.NewScanner(os.Stdin)
107+
messageCount := 0
108+
109+
for {
110+
// Show prompt
111+
pterm.Print(pterm.Cyan("You: "))
112+
113+
if !scanner.Scan() {
114+
break
115+
}
116+
117+
input := strings.TrimSpace(scanner.Text())
118+
if input == "" {
119+
continue
120+
}
121+
122+
// Handle special commands
123+
if strings.HasPrefix(input, "/") {
124+
handled, shouldExit := handleChatCommand(ctx, client, browserID, input)
125+
if shouldExit {
126+
pterm.Info.Println("Goodbye!")
127+
return nil
128+
}
129+
if handled {
130+
continue
131+
}
132+
}
133+
134+
// Send the message
135+
messageCount++
136+
spinner, _ := pterm.DefaultSpinner.Start("Claude is thinking...")
137+
138+
response, err := sendChatMessage(ctx, client, browser.SessionID, input)
139+
spinner.Stop()
140+
141+
if err != nil {
142+
pterm.Error.Printf("Error: %v\n", err)
143+
pterm.Println()
144+
continue
145+
}
146+
147+
// Display the response
148+
pterm.Println()
149+
pterm.Print(pterm.Green("Claude: "))
150+
fmt.Println(response)
151+
pterm.Println()
152+
}
153+
154+
return nil
155+
}
156+
157+
func handleChatCommand(ctx context.Context, client kernel.Client, browserID, input string) (handled bool, shouldExit bool) {
158+
parts := strings.Fields(input)
159+
if len(parts) == 0 {
160+
return false, false
161+
}
162+
163+
cmd := strings.ToLower(parts[0])
164+
165+
switch cmd {
166+
case "/quit", "/exit", "/q":
167+
return true, true
168+
169+
case "/clear":
170+
// Clear terminal (works on most terminals)
171+
fmt.Print("\033[H\033[2J")
172+
pterm.Info.Println("Terminal cleared.")
173+
return true, false
174+
175+
case "/status":
176+
pterm.Info.Println("Checking status...")
177+
result, err := client.Browsers.Playwright.Execute(ctx, browserID, kernel.BrowserPlaywrightExecuteParams{
178+
Code: claude.CheckStatusScript,
179+
TimeoutSec: kernel.Opt(int64(30)),
180+
})
181+
if err != nil {
182+
pterm.Error.Printf("Status check failed: %v\n", err)
183+
return true, false
184+
}
185+
186+
var status struct {
187+
ExtensionLoaded bool `json:"extensionLoaded"`
188+
Authenticated bool `json:"authenticated"`
189+
HasConversation bool `json:"hasConversation"`
190+
Error string `json:"error"`
191+
}
192+
if result.Result != nil {
193+
resultBytes, _ := json.Marshal(result.Result)
194+
_ = json.Unmarshal(resultBytes, &status)
195+
}
196+
197+
pterm.Info.Printf("Extension: %v, Auth: %v, Conversation: %v\n",
198+
status.ExtensionLoaded, status.Authenticated, status.HasConversation)
199+
if status.Error != "" {
200+
pterm.Warning.Printf("Error: %s\n", status.Error)
201+
}
202+
return true, false
203+
204+
case "/help", "/?":
205+
pterm.Println()
206+
pterm.Info.Println("Available commands:")
207+
pterm.Println(" /quit, /exit - Exit the chat session")
208+
pterm.Println(" /clear - Clear the terminal")
209+
pterm.Println(" /status - Check extension status")
210+
pterm.Println(" /help - Show this help message")
211+
pterm.Println()
212+
return true, false
213+
214+
default:
215+
pterm.Warning.Printf("Unknown command: %s (use /help for available commands)\n", cmd)
216+
return true, false
217+
}
218+
}
219+
220+
func sendChatMessage(ctx context.Context, client kernel.Client, browserID, message string) (string, error) {
221+
// Build the script with the message
222+
script := fmt.Sprintf(`
223+
process.env.CLAUDE_MESSAGE = %s;
224+
process.env.CLAUDE_TIMEOUT_MS = '300000';
225+
226+
%s
227+
`, jsonMarshalString(message), claude.SendMessageScript)
228+
229+
result, err := client.Browsers.Playwright.Execute(ctx, browserID, kernel.BrowserPlaywrightExecuteParams{
230+
Code: script,
231+
TimeoutSec: kernel.Opt(int64(330)), // 5.5 minutes
232+
})
233+
if err != nil {
234+
return "", fmt.Errorf("failed to send message: %w", err)
235+
}
236+
237+
if !result.Success {
238+
if result.Error != "" {
239+
return "", fmt.Errorf("%s", result.Error)
240+
}
241+
return "", fmt.Errorf("send failed")
242+
}
243+
244+
// Parse the result
245+
var response struct {
246+
Response string `json:"response"`
247+
Warning string `json:"warning"`
248+
}
249+
if result.Result != nil {
250+
resultBytes, _ := json.Marshal(result.Result)
251+
_ = json.Unmarshal(resultBytes, &response)
252+
}
253+
254+
if response.Response == "" {
255+
return "", fmt.Errorf("empty response")
256+
}
257+
258+
return response.Response, nil
259+
}

0 commit comments

Comments
 (0)