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
1,360 changes: 0 additions & 1,360 deletions cmd/agents.go

This file was deleted.

741 changes: 741 additions & 0 deletions cmd/auth_connections.go

Large diffs are not rendered by default.

27 changes: 22 additions & 5 deletions cmd/browsers.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,9 @@ func getAvailableViewports() []string {
"1920x1080@25",
"1920x1200@25",
"1440x900@25",
"1280x800@60",
"1024x768@60",
"1200x800@60",
"1280x800@60",
}
}

Expand Down Expand Up @@ -222,6 +222,7 @@ type BrowsersCmd struct {
type BrowsersListInput struct {
Output string
IncludeDeleted bool
Status string
Limit int
Offset int
}
Expand All @@ -232,7 +233,19 @@ func (b BrowsersCmd) List(ctx context.Context, in BrowsersListInput) error {
}

params := kernel.BrowserListParams{}
if in.IncludeDeleted {
// Use new Status parameter if provided, otherwise fall back to deprecated IncludeDeleted
if in.Status != "" {
switch in.Status {
case "active":
params.Status = kernel.BrowserListParamsStatusActive
case "deleted":
params.Status = kernel.BrowserListParamsStatusDeleted
case "all":
params.Status = kernel.BrowserListParamsStatusAll
default:
return fmt.Errorf("invalid --status value: %s (must be 'active', 'deleted', or 'all')", in.Status)
}
} else if in.IncludeDeleted {
params.IncludeDeleted = kernel.Opt(true)
}
if in.Limit > 0 {
Expand Down Expand Up @@ -263,7 +276,8 @@ func (b BrowsersCmd) List(ctx context.Context, in BrowsersListInput) error {

// Prepare table data
headers := []string{"Browser ID", "Created At", "Persistent ID", "Profile", "CDP WS URL", "Live View URL"}
if in.IncludeDeleted {
showDeletedAt := in.IncludeDeleted || in.Status == "deleted" || in.Status == "all"
if showDeletedAt {
headers = append(headers, "Deleted At")
}
tableData := pterm.TableData{headers}
Expand All @@ -290,7 +304,7 @@ func (b BrowsersCmd) List(ctx context.Context, in BrowsersListInput) error {
truncateURL(browser.BrowserLiveViewURL, 50),
}

if in.IncludeDeleted {
if showDeletedAt {
deletedAt := "-"
if !browser.DeletedAt.IsZero() {
deletedAt = util.FormatLocal(browser.DeletedAt)
Expand Down Expand Up @@ -2053,7 +2067,8 @@ Note: Profiles can only be loaded into sessions that don't already have a profil
func init() {
// list flags
browsersListCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response")
browsersListCmd.Flags().Bool("include-deleted", false, "Include soft-deleted browser sessions in the results")
browsersListCmd.Flags().Bool("include-deleted", false, "DEPRECATED: Use --status instead. Include soft-deleted browser sessions in the results")
browsersListCmd.Flags().String("status", "", "Filter by status: 'active' (default), 'deleted', or 'all'")
browsersListCmd.Flags().Int("limit", 0, "Maximum number of results to return (default 20, max 100)")
browsersListCmd.Flags().Int("offset", 0, "Number of results to skip (for pagination)")

Expand Down Expand Up @@ -2322,11 +2337,13 @@ func runBrowsersList(cmd *cobra.Command, args []string) error {
b := BrowsersCmd{browsers: &svc}
out, _ := cmd.Flags().GetString("output")
includeDeleted, _ := cmd.Flags().GetBool("include-deleted")
status, _ := cmd.Flags().GetString("status")
limit, _ := cmd.Flags().GetInt("limit")
offset, _ := cmd.Flags().GetInt("offset")
return b.List(cmd.Context(), BrowsersListInput{
Output: out,
IncludeDeleted: includeDeleted,
Status: status,
Limit: limit,
Offset: offset,
})
Expand Down
2 changes: 1 addition & 1 deletion cmd/browsers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1151,8 +1151,8 @@ func TestGetAvailableViewports_ReturnsExpectedOptions(t *testing.T) {
assert.Contains(t, viewports, "1920x1080@25")
assert.Contains(t, viewports, "1920x1200@25")
assert.Contains(t, viewports, "1440x900@25")
assert.Contains(t, viewports, "1200x800@60")
assert.Contains(t, viewports, "1280x800@60")
assert.Contains(t, viewports, "1200x800@60")
assert.Contains(t, viewports, "1024x768@60")
}

Expand Down
75 changes: 75 additions & 0 deletions cmd/credential_providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type CredentialProvidersService interface {
List(ctx context.Context, opts ...option.RequestOption) (res *[]kernel.CredentialProvider, err error)
Delete(ctx context.Context, id string, opts ...option.RequestOption) (err error)
Test(ctx context.Context, id string, opts ...option.RequestOption) (res *kernel.CredentialProviderTestResult, err error)
ListItems(ctx context.Context, id string, opts ...option.RequestOption) (res *kernel.CredentialProviderListItemsResponse, err error)
}

// CredentialProvidersCmd handles credential provider operations independent of cobra.
Expand Down Expand Up @@ -62,6 +63,11 @@ type CredentialProvidersTestInput struct {
Output string
}

type CredentialProvidersListItemsInput struct {
ID string
Output string
}

func (c CredentialProvidersCmd) List(ctx context.Context, in CredentialProvidersListInput) error {
if in.Output != "" && in.Output != "json" {
return fmt.Errorf("unsupported --output value: use 'json'")
Expand Down Expand Up @@ -281,6 +287,51 @@ func (c CredentialProvidersCmd) Test(ctx context.Context, in CredentialProviders
return nil
}

func (c CredentialProvidersCmd) ListItems(ctx context.Context, in CredentialProvidersListItemsInput) error {
if in.Output != "" && in.Output != "json" {
return fmt.Errorf("unsupported --output value: use 'json'")
}

if in.Output != "json" {
pterm.Info.Printf("Listing items for credential provider '%s'...\n", in.ID)
}

result, err := c.providers.ListItems(ctx, in.ID)
if err != nil {
return util.CleanedUpSdkError{Err: err}
}

if in.Output == "json" {
if len(result.Items) == 0 {
fmt.Println("[]")
return nil
}
return util.PrintPrettyJSONSlice(result.Items)
}

if len(result.Items) == 0 {
pterm.Info.Println("No items found")
return nil
}

tableData := pterm.TableData{{"Path", "Title", "Vault", "URLs"}}
for _, item := range result.Items {
urls := ""
if len(item.URLs) > 0 {
urls = strings.Join(item.URLs, ", ")
}
tableData = append(tableData, []string{
item.Path,
item.Title,
item.VaultName,
urls,
})
}

PrintTableNoPad(tableData, true)
return nil
}

// --- Cobra wiring ---

var credentialProvidersCmd = &cobra.Command{
Expand Down Expand Up @@ -345,13 +396,22 @@ var credentialProvidersTestCmd = &cobra.Command{
RunE: runCredentialProvidersTest,
}

var credentialProvidersListItemsCmd = &cobra.Command{
Use: "list-items <id>",
Short: "List items from a credential provider",
Long: `List all credential items available from the specified external credential provider.`,
Args: cobra.ExactArgs(1),
RunE: runCredentialProvidersListItems,
}

func init() {
credentialProvidersCmd.AddCommand(credentialProvidersListCmd)
credentialProvidersCmd.AddCommand(credentialProvidersGetCmd)
credentialProvidersCmd.AddCommand(credentialProvidersCreateCmd)
credentialProvidersCmd.AddCommand(credentialProvidersUpdateCmd)
credentialProvidersCmd.AddCommand(credentialProvidersDeleteCmd)
credentialProvidersCmd.AddCommand(credentialProvidersTestCmd)
credentialProvidersCmd.AddCommand(credentialProvidersListItemsCmd)

// List flags
credentialProvidersListCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response")
Expand Down Expand Up @@ -379,6 +439,9 @@ func init() {

// Test flags
credentialProvidersTestCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response")

// ListItems flags
credentialProvidersListItemsCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response")
}

func runCredentialProvidersList(cmd *cobra.Command, args []string) error {
Expand Down Expand Up @@ -464,3 +527,15 @@ func runCredentialProvidersTest(cmd *cobra.Command, args []string) error {
Output: output,
})
}

func runCredentialProvidersListItems(cmd *cobra.Command, args []string) error {
client := getKernelClient(cmd)
output, _ := cmd.Flags().GetString("output")

svc := client.CredentialProviders
c := CredentialProvidersCmd{providers: &svc}
return c.ListItems(cmd.Context(), CredentialProvidersListItemsInput{
ID: args[0],
Output: output,
})
}
99 changes: 86 additions & 13 deletions cmd/invoke.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,20 @@ var invocationHistoryCmd = &cobra.Command{
RunE: runInvocationHistory,
}

var invocationBrowsersCmd = &cobra.Command{
Use: "browsers <invocation_id>",
Short: "List browser sessions for an invocation",
Long: "List all active browser sessions created within a specific invocation.",
Args: cobra.ExactArgs(1),
RunE: runInvocationBrowsers,
}

func init() {
invokeCmd.Flags().StringP("version", "v", "latest", "Specify a version of the app to invoke (optional, defaults to 'latest')")
invokeCmd.Flags().StringP("payload", "p", "", "JSON payload for the invocation (optional)")
invokeCmd.Flags().StringP("payload-file", "f", "", "Path to a JSON file containing the payload (use '-' for stdin)")
invokeCmd.Flags().BoolP("sync", "s", false, "Invoke synchronously (default false). A synchronous invocation will open a long-lived HTTP POST to the Kernel API to wait for the invocation to complete. This will time out after 60 seconds, so only use this option if you expect your invocation to complete in less than 60 seconds. The default is to invoke asynchronously, in which case the CLI will open an SSE connection to the Kernel API after submitting the invocation and wait for the invocation to complete.")
invokeCmd.Flags().Int64("async-timeout", 0, "Timeout in seconds for async invocations (min 10, max 3600). Only applies when async mode is used.")
invokeCmd.Flags().StringP("output", "o", "", "Output format: json for JSONL streaming output")
invokeCmd.MarkFlagsMutuallyExclusive("payload", "payload-file")

Expand All @@ -48,6 +57,9 @@ func init() {
invocationHistoryCmd.Flags().String("version", "", "Filter by invocation version")
invocationHistoryCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response")
invokeCmd.AddCommand(invocationHistoryCmd)

invocationBrowsersCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response")
invokeCmd.AddCommand(invocationBrowsersCmd)
}

func runInvoke(cmd *cobra.Command, args []string) error {
Expand All @@ -70,12 +82,16 @@ func runInvoke(cmd *cobra.Command, args []string) error {
return fmt.Errorf("version cannot be an empty string")
}
isSync, _ := cmd.Flags().GetBool("sync")
asyncTimeout, _ := cmd.Flags().GetInt64("async-timeout")
params := kernel.InvocationNewParams{
AppName: appName,
ActionName: actionName,
Version: version,
Async: kernel.Opt(!isSync),
}
if asyncTimeout > 0 {
params.AsyncTimeoutSeconds = kernel.Opt(asyncTimeout)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing validation for async-timeout documented bounds

Low Severity

The --async-timeout flag help text states "(min 10, max 3600)" but the code only checks if asyncTimeout > 0 before passing the value to AsyncTimeoutSeconds. This allows values like 5 (below minimum) or 10000 (above maximum) to be sent to the API. Users relying on the documented constraints would expect invalid values to be rejected by the CLI with a helpful error.

Additional Locations (1)

Fix in Cursor Fix in Web


payloadStr, hasPayload, err := getPayload(cmd)
if err != nil {
Expand Down Expand Up @@ -179,21 +195,21 @@ func runInvoke(cmd *cobra.Command, args []string) error {
if err == nil {
fmt.Println(string(bs))
}
// Check for terminal states
if ev.Event == "invocation_state" {
stateEv := ev.AsInvocationState()
status := stateEv.Invocation.Status
if status == string(kernel.InvocationGetResponseStatusSucceeded) {
return nil
// Check for terminal states
if ev.Event == "invocation_state" {
stateEv := ev.AsInvocationState()
status := stateEv.Invocation.Status
if status == string(kernel.InvocationGetResponseStatusSucceeded) {
return nil
}
if status == string(kernel.InvocationGetResponseStatusFailed) {
return fmt.Errorf("invocation failed")
}
}
if status == string(kernel.InvocationGetResponseStatusFailed) {
return fmt.Errorf("invocation failed")
if ev.Event == "error" {
errEv := ev.AsError()
return fmt.Errorf("%s: %s", errEv.Error.Code, errEv.Error.Message)
}
}
if ev.Event == "error" {
errEv := ev.AsError()
return fmt.Errorf("%s: %s", errEv.Error.Code, errEv.Error.Message)
}
continue
}

Expand Down Expand Up @@ -428,3 +444,60 @@ func runInvocationHistory(cmd *cobra.Command, args []string) error {
}
return nil
}

func runInvocationBrowsers(cmd *cobra.Command, args []string) error {
client := getKernelClient(cmd)
invocationID := args[0]
output, _ := cmd.Flags().GetString("output")

if output != "" && output != "json" {
return fmt.Errorf("unsupported --output value: use 'json'")
}

resp, err := client.Invocations.ListBrowsers(cmd.Context(), invocationID)
if err != nil {
return util.CleanedUpSdkError{Err: err}
}

if resp == nil {
pterm.Info.Printf("No active browsers found for invocation %s\n", invocationID)
return nil
}

if output == "json" {
if len(resp.Browsers) == 0 {
fmt.Println("[]")
return nil
}
return util.PrintPrettyJSONSlice(resp.Browsers)
}

if len(resp.Browsers) == 0 {
pterm.Info.Printf("No active browsers found for invocation %s\n", invocationID)
return nil
}

table := pterm.TableData{{"Session ID", "Created At", "Headless", "Stealth", "Timeout", "CDP WS URL", "Live View URL"}}

for _, browser := range resp.Browsers {
created := util.FormatLocal(browser.CreatedAt)
liveView := browser.BrowserLiveViewURL
if liveView == "" {
liveView = "-"
}

table = append(table, []string{
browser.SessionID,
created,
fmt.Sprintf("%v", browser.Headless),
fmt.Sprintf("%v", browser.Stealth),
fmt.Sprintf("%d", browser.TimeoutSeconds),
truncateURL(browser.CdpWsURL, 40),
truncateURL(liveView, 40),
})
}

pterm.Info.Printf("Browsers for invocation %s:\n", invocationID)
pterm.DefaultTable.WithHasHeader().WithData(table).Render()
return nil
}
6 changes: 4 additions & 2 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,11 @@ func isAuthExempt(cmd *cobra.Command) bool {

// Check if the top-level command is in the exempt list
switch topLevel.Name() {
case "login", "logout", "auth", "help", "completion", "create", "mcp", "upgrade":
case "login", "logout", "help", "completion", "create", "mcp", "upgrade":
return true
case "auth":
// Only exempt the auth command itself (status display), not its subcommands
return cmd == topLevel
}

return false
Expand Down Expand Up @@ -141,7 +144,6 @@ func init() {
rootCmd.AddCommand(extensionsCmd)
rootCmd.AddCommand(credentialsCmd)
rootCmd.AddCommand(credentialProvidersCmd)
rootCmd.AddCommand(agentsCmd)
rootCmd.AddCommand(createCmd)
rootCmd.AddCommand(mcp.MCPCmd)
rootCmd.AddCommand(upgradeCmd)
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ require (
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1
github.com/golang-jwt/jwt/v5 v5.2.2
github.com/joho/godotenv v1.5.1
github.com/kernel/kernel-go-sdk v0.28.0
github.com/kernel/kernel-go-sdk v0.33.0
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
github.com/pquerna/otp v1.5.0
github.com/pterm/pterm v0.12.80
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/kernel/kernel-go-sdk v0.28.0 h1:cvaCWP25UIB5w6oOdQ5J+rVboNGq3VaWYhtmshlPrhg=
github.com/kernel/kernel-go-sdk v0.28.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ=
github.com/kernel/kernel-go-sdk v0.33.0 h1:kfk2bwrw3mbR4IW3JMnOj6Tecxor44YjM8YV153xDTY=
github.com/kernel/kernel-go-sdk v0.33.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
Expand Down