Skip to content

Commit 188bbf9

Browse files
bmsaadatclaude
andcommitted
browsers + acquire: support session name and tags
Surface the new Kernel API name/tags fields in the CLI, modeled on the profiles command and the hypeman CLI tag convention. browsers: - create: --name and repeatable --tag KEY=VALUE (parsed with a hypeman-style parser that warns on malformed pairs); forwarded to BrowserNewParams. - get/view/update/delete now accept <id-or-name> (SDK resolves either); name and tags are shown in the get detail table, the JSON output, and a new Name column in list. - list: --tag KEY=VALUE filter (deepObject, ANDed) and --query now also matches name. - create --pool-id/--pool-name: --name/--tag now apply to the acquired lease. - Note: name/tags are creation-time only. get/list and JSON responses echo them, but update cannot change them (the SDK BrowserUpdateParams has no name/tags), so update offers no such flags. browser-pools acquire: --name and --tag apply per-lease (cleared on release), forwarded to BrowserPoolAcquireParams and shown in the acquired-session table. The per-lease acquire params (name/tags/timeout) are built by a single shared buildAcquireParams helper used by both `browser-pools acquire` and the `browsers create --pool-*` path, so the two cannot silently diverge. Adds parseKeyValueSpecs/tagsFromFlag/formatTags helpers and tests for create, list, get (incl. JSON output), acquire, pool-list limit/offset forwarding, the buildAcquireParams forwarding contract, the parser, and the malformed-tag warning path. README is updated for the new browser and acquire flags. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent f7bdb1e commit 188bbf9

5 files changed

Lines changed: 430 additions & 37 deletions

File tree

README.md

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -205,25 +205,29 @@ Commands with JSON output support:
205205
### Browser Management
206206

207207
- `kernel browsers list` - List running browsers
208+
- `--query <q>` - Search by name, session ID, profile ID, proxy ID, or pool name
209+
- `--tag <KEY=VALUE>` - Filter by tag, repeatable; a session must match every pair
208210
- `--output json`, `-o json` - Output raw JSON array
209211
- `kernel browsers create` - Create a new browser session
210212
- `-s, --stealth` - Launch browser in stealth mode to avoid detection
211213
- `-H, --headless` - Launch browser without GUI access
212214
- `--kiosk` - Launch browser in kiosk mode
213215
- `--start-url <url>` - Initial page to open on launch
214-
- `--pool-id <id>` - Acquire a browser from the specified pool (mutually exclusive with --pool-name; ignores other session flags)
216+
- `--name <name>` - Optional unique name for the session (set at creation; used to find it later by name)
217+
- `--tag <KEY=VALUE>` - Set a tag on the session, repeatable; up to 50 pairs
218+
- `--pool-id <id>` - Acquire a browser from the specified pool (mutually exclusive with --pool-name; ignores other session flags). `--name`/`--tag` still apply to the acquired session.
215219
- `--pool-name <name>` - Acquire a browser from the pool name (mutually exclusive with --pool-id; ignores other session flags)
216220
- `--telemetry=all` - Enable telemetry for all categories
217221
- `--telemetry=off` - Disable telemetry
218222
- `--telemetry=<list>` - Per-category config, e.g. `--telemetry=network=on,page=off`
219223
- `--output json`, `-o json` - Output raw JSON object
220224
- _Note: When a pool is specified, omit other session configuration flags—pool settings determine profile, proxy, viewport, etc._
221-
- `kernel browsers delete <id>` - Delete a browser
222-
- `kernel browsers view <id>` - Get live view URL for a browser
225+
- `kernel browsers delete <id-or-name>` - Delete a browser by ID or name
226+
- `kernel browsers view <id-or-name>` - Get live view URL for a browser by ID or name
223227
- `--output json`, `-o json` - Output JSON with liveViewUrl
224-
- `kernel browsers get <id>` - Get detailed browser session info
228+
- `kernel browsers get <id-or-name>` - Get detailed browser session info by ID or name
225229
- `--output json`, `-o json` - Output raw JSON object
226-
- `kernel browsers update <id>` - Update a running browser session
230+
- `kernel browsers update <id-or-name>` - Update a running browser session by ID or name
227231
- `--telemetry=all` - Enable telemetry for all categories
228232
- `--telemetry=off` - Disable telemetry
229233
- `--telemetry=<list>` - Per-category config, e.g. `--telemetry=network=on,page=off`
@@ -264,6 +268,8 @@ Commands with JSON output support:
264268
- `--force` - Force delete even if browsers are leased
265269
- `kernel browser-pools acquire <id-or-name>` - Acquire a browser from the pool
266270
- `--timeout <seconds>` - Acquire timeout before returning 204
271+
- `--name <name>` - Optional name for the acquired session (applies to this lease; cleared on release)
272+
- `--tag <KEY=VALUE>` - Set a tag on the acquired session, repeatable; applies to this lease
267273
- `--output json`, `-o json` - Output raw JSON object
268274
- `kernel browser-pools release <id-or-name>` - Release a browser back to the pool
269275
- `--session-id <id>` - Browser session ID to release (required)

cmd/browser_pools.go

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -354,18 +354,34 @@ func (c BrowserPoolsCmd) Delete(ctx context.Context, in BrowserPoolsDeleteInput)
354354
type BrowserPoolsAcquireInput struct {
355355
IDOrName string
356356
TimeoutSeconds int64
357+
Name string
358+
Tags map[string]string
357359
Output string
358360
}
359361

362+
// buildAcquireParams builds the SDK params for acquiring a browser from a pool.
363+
// Shared by `browser-pools acquire` and the `browsers create --pool-id/--pool-name`
364+
// path so the per-lease name/tags forwarding cannot silently diverge between them.
365+
func buildAcquireParams(name string, tags map[string]string, timeoutSeconds int64) kernel.BrowserPoolAcquireParams {
366+
params := kernel.BrowserPoolAcquireParams{}
367+
if timeoutSeconds > 0 {
368+
params.AcquireTimeoutSeconds = kernel.Int(timeoutSeconds)
369+
}
370+
if name != "" {
371+
params.Name = kernel.Opt(name)
372+
}
373+
if len(tags) > 0 {
374+
params.Tags = kernel.Tags(tags)
375+
}
376+
return params
377+
}
378+
360379
func (c BrowserPoolsCmd) Acquire(ctx context.Context, in BrowserPoolsAcquireInput) error {
361380
if err := validateJSONOutput(in.Output); err != nil {
362381
return err
363382
}
364383

365-
params := kernel.BrowserPoolAcquireParams{}
366-
if in.TimeoutSeconds > 0 {
367-
params.AcquireTimeoutSeconds = kernel.Int(in.TimeoutSeconds)
368-
}
384+
params := buildAcquireParams(in.Name, in.Tags, in.TimeoutSeconds)
369385
resp, err := c.client.Acquire(ctx, in.IDOrName, params)
370386
if err != nil {
371387
return util.CleanedUpSdkError{Err: err}
@@ -386,12 +402,20 @@ func (c BrowserPoolsCmd) Acquire(ctx context.Context, in BrowserPoolsAcquireInpu
386402
tableData := pterm.TableData{
387403
{"Property", "Value"},
388404
{"Session ID", resp.SessionID},
389-
{"CDP WebSocket URL", resp.CdpWsURL},
390-
{"Live View URL", resp.BrowserLiveViewURL},
391405
}
406+
if resp.Name != "" {
407+
tableData = append(tableData, []string{"Name", resp.Name})
408+
}
409+
tableData = append(tableData,
410+
[]string{"CDP WebSocket URL", resp.CdpWsURL},
411+
[]string{"Live View URL", resp.BrowserLiveViewURL},
412+
)
392413
if resp.StartURL != "" {
393414
tableData = append(tableData, []string{"Start URL", resp.StartURL})
394415
}
416+
if len(resp.Tags) > 0 {
417+
tableData = append(tableData, []string{"Tags", formatTags(resp.Tags)})
418+
}
395419
PrintTableNoPad(tableData, true)
396420
return nil
397421
}
@@ -541,6 +565,8 @@ func init() {
541565
browserPoolsDeleteCmd.Flags().Bool("force", false, "Force delete even if browsers are leased")
542566

543567
browserPoolsAcquireCmd.Flags().Int64("timeout", 0, "Acquire timeout in seconds")
568+
browserPoolsAcquireCmd.Flags().String("name", "", "Optional name for the acquired session (applies to this lease; cleared on release)")
569+
browserPoolsAcquireCmd.Flags().StringArray("tag", nil, "Set a tag KEY=VALUE on the acquired session (repeatable; applies to this lease)")
544570
addJSONOutputFlag(browserPoolsAcquireCmd)
545571

546572
browserPoolsReleaseCmd.Flags().String("session-id", "", "Browser session ID to release")
@@ -676,9 +702,17 @@ func runBrowserPoolsDelete(cmd *cobra.Command, args []string) error {
676702
func runBrowserPoolsAcquire(cmd *cobra.Command, args []string) error {
677703
client := getKernelClient(cmd)
678704
timeout, _ := cmd.Flags().GetInt64("timeout")
705+
name, _ := cmd.Flags().GetString("name")
706+
tags := tagsFromFlag(cmd, "tag")
679707
output, _ := cmd.Flags().GetString("output")
680708
c := BrowserPoolsCmd{client: &client.BrowserPools}
681-
return c.Acquire(cmd.Context(), BrowserPoolsAcquireInput{IDOrName: args[0], TimeoutSeconds: timeout, Output: output})
709+
return c.Acquire(cmd.Context(), BrowserPoolsAcquireInput{
710+
IDOrName: args[0],
711+
TimeoutSeconds: timeout,
712+
Name: name,
713+
Tags: tags,
714+
Output: output,
715+
})
682716
}
683717

684718
func runBrowserPoolsRelease(cmd *cobra.Command, args []string) error {

cmd/browser_pools_test.go

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
package cmd
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/kernel/kernel-go-sdk"
8+
"github.com/kernel/kernel-go-sdk/option"
9+
"github.com/kernel/kernel-go-sdk/packages/pagination"
10+
"github.com/stretchr/testify/assert"
11+
)
12+
13+
// FakeBrowserPoolsService is a configurable fake implementing BrowserPoolsService.
14+
type FakeBrowserPoolsService struct {
15+
AcquireFunc func(ctx context.Context, id string, body kernel.BrowserPoolAcquireParams, opts ...option.RequestOption) (*kernel.BrowserPoolAcquireResponse, error)
16+
ListFunc func(ctx context.Context, query kernel.BrowserPoolListParams, opts ...option.RequestOption) (*pagination.OffsetPagination[kernel.BrowserPool], error)
17+
}
18+
19+
func (f *FakeBrowserPoolsService) List(ctx context.Context, query kernel.BrowserPoolListParams, opts ...option.RequestOption) (*pagination.OffsetPagination[kernel.BrowserPool], error) {
20+
if f.ListFunc != nil {
21+
return f.ListFunc(ctx, query, opts...)
22+
}
23+
return &pagination.OffsetPagination[kernel.BrowserPool]{Items: []kernel.BrowserPool{}}, nil
24+
}
25+
26+
func (f *FakeBrowserPoolsService) New(ctx context.Context, body kernel.BrowserPoolNewParams, opts ...option.RequestOption) (*kernel.BrowserPool, error) {
27+
return &kernel.BrowserPool{}, nil
28+
}
29+
30+
func (f *FakeBrowserPoolsService) Get(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.BrowserPool, error) {
31+
return &kernel.BrowserPool{}, nil
32+
}
33+
34+
func (f *FakeBrowserPoolsService) Update(ctx context.Context, id string, body kernel.BrowserPoolUpdateParams, opts ...option.RequestOption) (*kernel.BrowserPool, error) {
35+
return &kernel.BrowserPool{}, nil
36+
}
37+
38+
func (f *FakeBrowserPoolsService) Delete(ctx context.Context, id string, body kernel.BrowserPoolDeleteParams, opts ...option.RequestOption) error {
39+
return nil
40+
}
41+
42+
func (f *FakeBrowserPoolsService) Acquire(ctx context.Context, id string, body kernel.BrowserPoolAcquireParams, opts ...option.RequestOption) (*kernel.BrowserPoolAcquireResponse, error) {
43+
if f.AcquireFunc != nil {
44+
return f.AcquireFunc(ctx, id, body, opts...)
45+
}
46+
return &kernel.BrowserPoolAcquireResponse{}, nil
47+
}
48+
49+
func (f *FakeBrowserPoolsService) Release(ctx context.Context, id string, body kernel.BrowserPoolReleaseParams, opts ...option.RequestOption) error {
50+
return nil
51+
}
52+
53+
func (f *FakeBrowserPoolsService) Flush(ctx context.Context, id string, opts ...option.RequestOption) error {
54+
return nil
55+
}
56+
57+
func TestBrowserPoolsAcquire_WithNameAndTags(t *testing.T) {
58+
setupStdoutCapture(t)
59+
60+
var capturedID string
61+
var captured kernel.BrowserPoolAcquireParams
62+
fake := &FakeBrowserPoolsService{
63+
AcquireFunc: func(ctx context.Context, id string, body kernel.BrowserPoolAcquireParams, opts ...option.RequestOption) (*kernel.BrowserPoolAcquireResponse, error) {
64+
capturedID = id
65+
captured = body
66+
return &kernel.BrowserPoolAcquireResponse{
67+
SessionID: "sess-acq",
68+
CdpWsURL: "ws://cdp-acq",
69+
Name: "lease-name",
70+
Tags: kernel.Tags{"env": "prod"},
71+
}, nil
72+
},
73+
}
74+
75+
c := BrowserPoolsCmd{client: fake}
76+
err := c.Acquire(context.Background(), BrowserPoolsAcquireInput{
77+
IDOrName: "my-pool",
78+
Name: "lease-name",
79+
Tags: map[string]string{"env": "prod"},
80+
})
81+
assert.NoError(t, err)
82+
83+
// Pool lookup is by id or name; name + tags are forwarded per-lease.
84+
assert.Equal(t, "my-pool", capturedID)
85+
assert.True(t, captured.Name.Valid())
86+
assert.Equal(t, "lease-name", captured.Name.Value)
87+
assert.Equal(t, "prod", captured.Tags["env"])
88+
89+
// And surfaced in the acquired-session table.
90+
out := outBuf.String()
91+
assert.Contains(t, out, "lease-name")
92+
assert.Contains(t, out, "Tags")
93+
assert.Contains(t, out, "env=prod")
94+
}
95+
96+
func TestBrowserPoolsList_ForwardsLimitOffset(t *testing.T) {
97+
setupStdoutCapture(t)
98+
99+
var captured kernel.BrowserPoolListParams
100+
fake := &FakeBrowserPoolsService{
101+
ListFunc: func(ctx context.Context, query kernel.BrowserPoolListParams, opts ...option.RequestOption) (*pagination.OffsetPagination[kernel.BrowserPool], error) {
102+
captured = query
103+
return &pagination.OffsetPagination[kernel.BrowserPool]{Items: []kernel.BrowserPool{}}, nil
104+
},
105+
}
106+
107+
c := BrowserPoolsCmd{client: fake}
108+
err := c.List(context.Background(), BrowserPoolsListInput{Limit: 4, Offset: 8})
109+
110+
assert.NoError(t, err)
111+
assert.Equal(t, int64(4), captured.Limit.Value)
112+
assert.Equal(t, int64(8), captured.Offset.Value)
113+
}
114+
115+
// TestBuildAcquireParams covers the shared name/tags/timeout forwarding used by
116+
// both `browser-pools acquire` and the `browsers create --pool-id` lease path.
117+
func TestBuildAcquireParams(t *testing.T) {
118+
p := buildAcquireParams("lease", map[string]string{"env": "prod"}, 30)
119+
assert.True(t, p.Name.Valid())
120+
assert.Equal(t, "lease", p.Name.Value)
121+
assert.Equal(t, "prod", p.Tags["env"])
122+
assert.True(t, p.AcquireTimeoutSeconds.Valid())
123+
assert.Equal(t, int64(30), p.AcquireTimeoutSeconds.Value)
124+
125+
// Unset inputs produce an empty params struct (nothing forwarded).
126+
empty := buildAcquireParams("", nil, 0)
127+
assert.False(t, empty.Name.Valid())
128+
assert.Len(t, empty.Tags, 0)
129+
assert.False(t, empty.AcquireTimeoutSeconds.Valid())
130+
}

0 commit comments

Comments
 (0)