Skip to content
Closed
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
3 changes: 3 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
run:
timeout: 5m

linters:
enable:
- gosec
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ require (
github.com/AlecAivazis/survey/v2 v2.3.7
github.com/BurntSushi/toml v1.4.0
github.com/MakeNowJust/heredoc v1.0.0
github.com/algolia/algoliasearch-client-go/v4 v4.43.1
github.com/algolia/algoliasearch-client-go/v4 v4.48.0
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869
github.com/briandowns/spinner v1.23.2
github.com/cli/go-internal v0.0.0-20241025142207-6c48bcd5ce24
Expand Down
8 changes: 2 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,8 @@ github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
github.com/algolia/algoliasearch-client-go/v4 v4.37.2 h1:Iqwb/mx9mVKKWKo8qildTAK36RTLIhFNuufqkhBmiHM=
github.com/algolia/algoliasearch-client-go/v4 v4.37.2/go.mod h1:2bHeze2/5+jvT8IYVq8j2NDLr/4R6erGxgud7ESuXww=
github.com/algolia/algoliasearch-client-go/v4 v4.38.0 h1:ufPpfevxC7DN9vTt9niVc7MvV6inrq8zHpIiTvp2lXI=
github.com/algolia/algoliasearch-client-go/v4 v4.38.0/go.mod h1:2bHeze2/5+jvT8IYVq8j2NDLr/4R6erGxgud7ESuXww=
github.com/algolia/algoliasearch-client-go/v4 v4.43.1 h1:XKS6RIGaGdlBhTdDdg/eZPts3QE+ASVAWNsSaC0Ouko=
github.com/algolia/algoliasearch-client-go/v4 v4.43.1/go.mod h1:2bHeze2/5+jvT8IYVq8j2NDLr/4R6erGxgud7ESuXww=
github.com/algolia/algoliasearch-client-go/v4 v4.48.0 h1:wphSQqb1ezO1s+UesFq0Z54zKDrxd5dYnhajg5DKcNs=
github.com/algolia/algoliasearch-client-go/v4 v4.48.0/go.mod h1:2bHeze2/5+jvT8IYVq8j2NDLr/4R6erGxgud7ESuXww=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
Expand Down
32 changes: 32 additions & 0 deletions pkg/cmd/agents/agents.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package agents

import (
"github.com/spf13/cobra"

"github.com/algolia/cli/pkg/cmd/agents/create"
"github.com/algolia/cli/pkg/cmd/agents/delete"
"github.com/algolia/cli/pkg/cmd/agents/get"
"github.com/algolia/cli/pkg/cmd/agents/list"
"github.com/algolia/cli/pkg/cmd/agents/publish"
"github.com/algolia/cli/pkg/cmd/agents/unpublish"
"github.com/algolia/cli/pkg/cmd/agents/update"
"github.com/algolia/cli/pkg/cmdutil"
)

// NewAgentsCmd returns the agents command group.
func NewAgentsCmd(f *cmdutil.Factory) *cobra.Command {
cmd := &cobra.Command{
Use: "agents",
Short: "Manage Agent Studio agents",
Long: "List, create, update, delete, publish, and unpublish Agent Studio agents.",
}

cmd.AddCommand(list.NewListCmd(f))
cmd.AddCommand(get.NewGetCmd(f))
cmd.AddCommand(create.NewCreateCmd(f))
cmd.AddCommand(update.NewUpdateCmd(f))
cmd.AddCommand(delete.NewDeleteCmd(f))
cmd.AddCommand(publish.NewPublishCmd(f))
cmd.AddCommand(unpublish.NewUnpublishCmd(f))
return cmd
}
124 changes: 124 additions & 0 deletions pkg/cmd/agents/create/create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package create

import (
"encoding/json"
"fmt"

"github.com/MakeNowJust/heredoc"
agentStudio "github.com/algolia/algoliasearch-client-go/v4/algolia/agent-studio"
"github.com/spf13/cobra"

"github.com/algolia/cli/pkg/cmdutil"
"github.com/algolia/cli/pkg/config"
"github.com/algolia/cli/pkg/iostreams"
)

// CreateOptions holds the dependencies and flags for the create command.
type CreateOptions struct {
Config config.IConfig
IO *iostreams.IOStreams
AgentStudioClient func() (*agentStudio.APIClient, error)
PrintFlags *cmdutil.PrintFlags

JSON string
Name string
Instructions string
}

// NewCreateCmd returns the `agents create` command.
func NewCreateCmd(f *cmdutil.Factory) *cobra.Command {
opts := &CreateOptions{
IO: f.IOStreams,
Config: f.Config,
AgentStudioClient: f.AgentStudioClient,
PrintFlags: cmdutil.NewPrintFlags().WithDefaultOutput("json"),
}

cmd := &cobra.Command{
Use: "create",
Short: "Create an Agent Studio agent",
Annotations: map[string]string{
"acls": "editSettings",
},
Example: heredoc.Doc(`
# Create a draft from flags (not publishable until it has a provider and model)
$ algolia agentstudio agents create --name "support-bot" --instructions "You are a helpful assistant."

# Create from a JSON object
$ algolia agentstudio agents create --json '{"name":"support-bot","instructions":"You are a helpful assistant."}'

# Create with a provider and model so the agent can be published
$ algolia agentstudio agents create --json '{"name":"support-bot","instructions":"You are a helpful assistant.","providerId":"22222222-2222-4222-8222-222222222222","model":"gpt-4o"}'
`),
RunE: func(cmd *cobra.Command, args []string) error {
if opts.JSON != "" {
if opts.Name != "" || opts.Instructions != "" {
return cmdutil.FlagErrorf("--json cannot be combined with --name or --instructions")
}
} else if opts.Name == "" || opts.Instructions == "" {
return cmdutil.FlagErrorf("`--json` or both `--name` and `--instructions` are required")
}

return runCreateCmd(opts)
},
}

cmd.Flags().StringVar(&opts.JSON, "json", "", "Agent JSON object (include providerId and model to publish later)")
cmd.Flags().StringVar(&opts.Name, "name", "", "Agent name")
cmd.Flags().StringVar(&opts.Instructions, "instructions", "", "Agent prompt")

opts.PrintFlags.AddFlags(cmd)
return cmd
}

func buildAgent(opts *CreateOptions) (*agentStudio.AgentConfigCreate, error) {
if opts.JSON == "" {
return agentStudio.NewEmptyAgentConfigCreate().
SetName(opts.Name).
SetInstructions(opts.Instructions), nil
}

var body agentStudio.AgentConfigCreate
if err := json.Unmarshal([]byte(opts.JSON), &body); err != nil {
return nil, fmt.Errorf("parsing agent JSON: %w", err)
}
if body.Name == "" || body.Instructions == "" {
return nil, cmdutil.FlagErrorf("agent JSON requires `name` and `instructions`")
}

return &body, nil
}

func runCreateCmd(opts *CreateOptions) error {
body, err := buildAgent(opts)
if err != nil {
return err
}

client, err := opts.AgentStudioClient()
if err != nil {
return err
}

p, err := opts.PrintFlags.ToPrinter()
if err != nil {
return err
}

opts.IO.StartProgressIndicatorWithLabel("Creating agent")

res, err := client.CreateAgent(client.NewApiCreateAgentRequest(body))
if err != nil {
opts.IO.StopProgressIndicator()
return err
}

opts.IO.StopProgressIndicator()

if opts.IO.IsStdoutTTY() {
cs := opts.IO.ColorScheme()
fmt.Fprintf(opts.IO.Out, "%s Created agent %s\n", cs.SuccessIcon(), res.Id)
}

return p.Print(opts.IO, res)
}
108 changes: 108 additions & 0 deletions pkg/cmd/agents/create/create_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package create_test

import (
"io"
"net/http"
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/algolia/cli/pkg/cmd/agents/create"
"github.com/algolia/cli/pkg/httpmock"
"github.com/algolia/cli/test"
)

const createdAgentBody = `{
"id": "agt_1",
"name": "cli-test-agent",
"description": null,
"status": "draft",
"providerId": null,
"instructions": "You are a helpful assistant.",
"config": {},
"createdAt": "2026-01-01T00:00:00Z",
"updatedAt": "2026-01-01T00:00:00Z",
"lastUsedAt": null
}`

func TestCreateAgent_FromFlags(t *testing.T) {
r := &httpmock.Registry{}
var gotBody []byte
r.Register(httpmock.REST("POST", "agent-studio/1/agents"), func(req *http.Request) (*http.Response, error) {
gotBody, _ = io.ReadAll(req.Body)
return httpmock.StringResponse(createdAgentBody)(req)
})

f, out := test.NewFactory(false, r, nil, "")
cmd := create.NewCreateCmd(f)
_, err := test.Execute(cmd, `--name cli-test-agent --instructions "You are a helpful assistant."`, out)
require.NoError(t, err)

assert.JSONEq(t, createdAgentBody, strings.TrimSpace(out.String()))
assert.JSONEq(t, `{"name":"cli-test-agent","instructions":"You are a helpful assistant."}`, string(gotBody))
r.Verify(t)
}

func TestCreateAgent_FromJSON(t *testing.T) {
r := &httpmock.Registry{}
var gotBody []byte
r.Register(httpmock.REST("POST", "agent-studio/1/agents"), func(req *http.Request) (*http.Response, error) {
gotBody, _ = io.ReadAll(req.Body)
return httpmock.StringResponse(createdAgentBody)(req)
})

f, out := test.NewFactory(false, r, nil, "")
cmd := create.NewCreateCmd(f)
_, err := test.Execute(
cmd,
`--json '{"name":"cli-test-agent","instructions":"You are a helpful assistant.","model":"gpt-4o"}'`,
out,
)
require.NoError(t, err)

assert.JSONEq(t, createdAgentBody, strings.TrimSpace(out.String()))
assert.JSONEq(
t,
`{"name":"cli-test-agent","instructions":"You are a helpful assistant.","model":"gpt-4o"}`,
string(gotBody),
)
r.Verify(t)
}

func TestCreateAgent_MissingInput(t *testing.T) {
r := &httpmock.Registry{}
f, out := test.NewFactory(false, r, nil, "")
cmd := create.NewCreateCmd(f)
_, err := test.Execute(cmd, "", out)
require.Error(t, err)
assert.Contains(t, err.Error(), "`--json` or both `--name` and `--instructions` are required")
}

func TestCreateAgent_JSONAndFlagsConflict(t *testing.T) {
r := &httpmock.Registry{}
f, out := test.NewFactory(false, r, nil, "")
cmd := create.NewCreateCmd(f)
_, err := test.Execute(cmd, `--json '{"name":"x","instructions":"y"}' --name x --instructions y`, out)
require.Error(t, err)
assert.Contains(t, err.Error(), "--json cannot be combined")
}

func TestCreateAgent_InvalidJSON(t *testing.T) {
r := &httpmock.Registry{}
f, out := test.NewFactory(false, r, nil, "")
cmd := create.NewCreateCmd(f)
_, err := test.Execute(cmd, "--json not-json", out)
require.Error(t, err)
assert.Contains(t, err.Error(), "parsing agent JSON")
}

func TestCreateAgent_JSONMissingRequiredFields(t *testing.T) {
r := &httpmock.Registry{}
f, out := test.NewFactory(false, r, nil, "")
cmd := create.NewCreateCmd(f)
_, err := test.Execute(cmd, `--json '{"name":"only-name"}'`, out)
require.Error(t, err)
assert.Contains(t, err.Error(), "requires `name` and `instructions`")
}
98 changes: 98 additions & 0 deletions pkg/cmd/agents/delete/delete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package delete

import (
"fmt"

"github.com/MakeNowJust/heredoc"
agentStudio "github.com/algolia/algoliasearch-client-go/v4/algolia/agent-studio"
"github.com/spf13/cobra"

"github.com/algolia/cli/pkg/cmdutil"
"github.com/algolia/cli/pkg/config"
"github.com/algolia/cli/pkg/iostreams"
"github.com/algolia/cli/pkg/prompt"
"github.com/algolia/cli/pkg/validators"
)

// DeleteOptions holds the dependencies and flags for the delete command.
type DeleteOptions struct {
Config config.IConfig
IO *iostreams.IOStreams
AgentStudioClient func() (*agentStudio.APIClient, error)
AgentID string
DoConfirm bool
}

// NewDeleteCmd returns the `agents delete` command.
func NewDeleteCmd(f *cmdutil.Factory) *cobra.Command {
opts := &DeleteOptions{
IO: f.IOStreams,
Config: f.Config,
AgentStudioClient: f.AgentStudioClient,
}

cmd := &cobra.Command{
Use: "delete <agent-id>",
Short: "Delete an Agent Studio agent",
Args: validators.ExactArgsWithMsg(1, "agentstudio agents delete requires a <agent-id> argument."),
Annotations: map[string]string{
"acls": "editSettings",
},
Example: heredoc.Doc(`
# Delete an agent (with confirmation prompt)
$ algolia agentstudio agents delete 11111111-1111-4111-8111-111111111111

# Delete without confirmation
$ algolia agentstudio agents delete 11111111-1111-4111-8111-111111111111 --confirm
`),
RunE: func(cmd *cobra.Command, args []string) error {
opts.AgentID = args[0]

if !opts.DoConfirm {
if !opts.IO.CanPrompt() {
return cmdutil.FlagErrorf("--confirm required when non-interactive shell is detected")
}
var confirmed bool
err := prompt.Confirm(
fmt.Sprintf("Delete agent %q?", opts.AgentID),
&confirmed,
)
if err != nil {
return fmt.Errorf("failed to prompt: %w", err)
}
if !confirmed {
return nil
}
}

return runDeleteCmd(opts)
},
}

cmd.Flags().BoolVarP(&opts.DoConfirm, "confirm", "y", false, "Skip confirmation prompt")
return cmd
}

func runDeleteCmd(opts *DeleteOptions) error {
client, err := opts.AgentStudioClient()
if err != nil {
return err
}

opts.IO.StartProgressIndicatorWithLabel("Deleting agent")

err = client.DeleteAgent(client.NewApiDeleteAgentRequest(opts.AgentID))
if err != nil {
opts.IO.StopProgressIndicator()
return err
}

opts.IO.StopProgressIndicator()

if opts.IO.IsStdoutTTY() {
cs := opts.IO.ColorScheme()
fmt.Fprintf(opts.IO.Out, "%s Deleted agent %s\n", cs.SuccessIcon(), opts.AgentID)
}

return nil
}
Loading
Loading