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
2 changes: 2 additions & 0 deletions pkg/cmd/agentstudio/agentstudio.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

"github.com/algolia/cli/pkg/cmd/agents"
"github.com/algolia/cli/pkg/cmd/conversations"
"github.com/algolia/cli/pkg/cmd/providers"
"github.com/algolia/cli/pkg/cmdutil"
)

Expand All @@ -18,5 +19,6 @@ func NewAgentStudioCmd(f *cmdutil.Factory) *cobra.Command {

cmd.AddCommand(agents.NewAgentsCmd(f))
cmd.AddCommand(conversations.NewConversationsCmd(f))
cmd.AddCommand(providers.NewProvidersCmd(f))
return cmd
}
110 changes: 110 additions & 0 deletions pkg/cmd/providers/create/create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
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
}

// NewCreateCmd returns the `providers 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 LLM provider",
Long: "Creates a provider from a JSON object. Anthropic keys must be scoped to a single workspace; Agent Studio does not send anthropic-workspace-id.",
Annotations: map[string]string{
"acls": "editSettings",
},
Example: heredoc.Doc(`
# Create an Anthropic provider
$ algolia agentstudio providers create --json '{"name":"anthropic-prod","providerName":"anthropic","input":{"apiKey":"<api-key>"}}'

# Create an OpenAI provider
$ algolia agentstudio providers create --json '{"name":"openai-prod","providerName":"openai","input":{"apiKey":"<api-key>"}}'
`),
RunE: func(cmd *cobra.Command, args []string) error {
if opts.JSON == "" {
return cmdutil.FlagErrorf("`--json` is required")
}
return runCreateCmd(opts)
},
}

cmd.Flags().
StringVar(&opts.JSON, "json", "", "Provider JSON object (name, providerName, input)")

opts.PrintFlags.AddFlags(cmd)
return cmd
}

func buildProvider(opts *CreateOptions) (*agentStudio.ProviderAuthenticationCreate, error) {
var body agentStudio.ProviderAuthenticationCreate
if err := json.Unmarshal([]byte(opts.JSON), &body); err != nil {
return nil, fmt.Errorf("parsing provider JSON: %w", err)
}
if body.Name == "" || !body.ProviderName.IsValid() {
return nil, cmdutil.FlagErrorf("provider JSON requires `name` and a valid `providerName`")
}
if body.Input.GetActualInstance() == nil {
return nil, cmdutil.FlagErrorf("provider JSON requires `input`")
}

return &body, nil
}

func runCreateCmd(opts *CreateOptions) error {
body, err := buildProvider(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 provider")

res, err := client.CreateProvider(client.NewApiCreateProviderRequest(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 provider %s\n", cs.SuccessIcon(), res.Id)
}

return p.Print(opts.IO, res)
}
97 changes: 97 additions & 0 deletions pkg/cmd/providers/create/create_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
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/providers/create"
"github.com/algolia/cli/pkg/httpmock"
"github.com/algolia/cli/test"
)

const createdProviderBody = `{
"id": "22222222-2222-4222-8222-222222222222",
"name": "anthropic-prod",
"providerName": "anthropic",
"input": {"apiKey": "sk-test"},
"createdAt": "2026-01-01T00:00:00Z",
"updatedAt": "2026-01-01T00:00:00Z"
}`

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

f, out := test.NewFactory(false, r, nil, "")
cmd := create.NewCreateCmd(f)
_, err := test.Execute(
cmd,
`--json '{"name":"anthropic-prod","providerName":"anthropic","input":{"apiKey":"sk-test"}}'`,
out,
)
require.NoError(t, err)

assert.JSONEq(t, createdProviderBody, strings.TrimSpace(out.String()))
assert.JSONEq(
t,
`{"name":"anthropic-prod","providerName":"anthropic","input":{"apiKey":"sk-test"}}`,
string(gotBody),
)
r.Verify(t)
}

func TestCreateProvider_MissingJSON(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` is required")
}

func TestCreateProvider_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 provider JSON")
}

func TestCreateProvider_MissingName(t *testing.T) {
r := &httpmock.Registry{}
f, out := test.NewFactory(false, r, nil, "")
cmd := create.NewCreateCmd(f)
_, err := test.Execute(
cmd,
`--json '{"providerName":"anthropic","input":{"apiKey":"sk-test"}}'`,
out,
)
require.Error(t, err)
assert.Contains(t, err.Error(), "requires `name` and a valid `providerName`")
}

func TestCreateProvider_MissingInput(t *testing.T) {
r := &httpmock.Registry{}
f, out := test.NewFactory(false, r, nil, "")
cmd := create.NewCreateCmd(f)
_, err := test.Execute(
cmd,
`--json '{"name":"anthropic-prod","providerName":"anthropic"}'`,
out,
)
require.Error(t, err)
assert.Contains(t, err.Error(), "requires `input`")
}
103 changes: 103 additions & 0 deletions pkg/cmd/providers/delete/delete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
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)
ProviderID string
DoConfirm bool
}

// NewDeleteCmd returns the `providers 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 <provider-id>",
Short: "Delete an Agent Studio provider",
Args: validators.ExactArgsWithMsg(
1,
"agentstudio providers delete requires a <provider-id> argument.",
),
Annotations: map[string]string{
"acls": "editSettings",
},
Example: heredoc.Doc(`
# Delete a provider (with confirmation prompt)
$ algolia agentstudio providers delete 22222222-2222-4222-8222-222222222222

# Delete without confirmation
$ algolia agentstudio providers delete 22222222-2222-4222-8222-222222222222 --confirm
`),
RunE: func(cmd *cobra.Command, args []string) error {
opts.ProviderID = 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 provider %q?", opts.ProviderID),
&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 provider")

err = client.DeleteProvider(client.NewApiDeleteProviderRequest(opts.ProviderID))
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 provider %s\n", cs.SuccessIcon(), opts.ProviderID)
}

return nil
}
52 changes: 52 additions & 0 deletions pkg/cmd/providers/delete/delete_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package delete_test

import (
"testing"

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

"github.com/algolia/cli/pkg/cmd/providers/delete"
"github.com/algolia/cli/pkg/httpmock"
"github.com/algolia/cli/test"
)

const (
providerID = "22222222-2222-4222-8222-222222222222"
deletePath = "agent-studio/1/providers/" + providerID
)

func TestDeleteProvider(t *testing.T) {
r := &httpmock.Registry{}
r.Register(httpmock.REST("DELETE", deletePath), httpmock.StringResponse(""))

f, out := test.NewFactory(false, r, nil, "")
cmd := delete.NewDeleteCmd(f)
_, err := test.Execute(cmd, providerID+" --confirm", out)
require.NoError(t, err)
assert.Empty(t, out.String())
r.Verify(t)
}

func TestDeleteProvider_RequiresConfirmation(t *testing.T) {
r := &httpmock.Registry{}
f, out := test.NewFactory(false, r, nil, "")
cmd := delete.NewDeleteCmd(f)
_, err := test.Execute(cmd, providerID, out)
require.Error(t, err)
assert.Contains(t, err.Error(), "--confirm required")
assert.Empty(t, r.Requests)
}

func TestDeleteProvider_MissingArg(t *testing.T) {
r := &httpmock.Registry{}
f, out := test.NewFactory(false, r, nil, "")
cmd := delete.NewDeleteCmd(f)
_, err := test.Execute(cmd, "--confirm", out)
require.Error(t, err)
assert.Contains(
t,
err.Error(),
"agentstudio providers delete requires a <provider-id> argument",
)
}
Loading
Loading