From afeb7114ad9cd0dc8a760606a0c86e2c25a8d069 Mon Sep 17 00:00:00 2001 From: ehl-jf Date: Mon, 18 May 2026 11:30:09 +0200 Subject: [PATCH] JGC-477 - Remove the `how` AI command --- docs/general/ai/help.go | 7 - general/ai/cli.go | 226 ----------------------------- go.mod | 2 +- main.go | 9 -- utils/cliutils/persistence.go | 28 ---- utils/cliutils/persistence_test.go | 18 --- 6 files changed, 1 insertion(+), 289 deletions(-) delete mode 100644 docs/general/ai/help.go delete mode 100644 general/ai/cli.go diff --git a/docs/general/ai/help.go b/docs/general/ai/help.go deleted file mode 100644 index 3ae224eb0..000000000 --- a/docs/general/ai/help.go +++ /dev/null @@ -1,7 +0,0 @@ -package ai - -var Usage = []string{"how"} - -func GetDescription() string { - return "An AI-powered interface that converts natural language inputs into AI-generated JFrog CLI commands." -} diff --git a/general/ai/cli.go b/general/ai/cli.go deleted file mode 100644 index 41bb02388..000000000 --- a/general/ai/cli.go +++ /dev/null @@ -1,226 +0,0 @@ -package ai - -import ( - "bufio" - "bytes" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "os" - "strings" - - "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" - "github.com/jfrog/jfrog-cli/utils/cliutils" - "github.com/jfrog/jfrog-client-go/artifactory/services/utils" - "github.com/jfrog/jfrog-client-go/http/httpclient" - clientutils "github.com/jfrog/jfrog-client-go/utils" - "github.com/jfrog/jfrog-client-go/utils/errorutils" - "github.com/jfrog/jfrog-client-go/utils/log" - "github.com/manifoldco/promptui" - "github.com/urfave/cli" -) - -type ApiCommand string - -const ( - cliAiAppApiUrl = "https://cli-ai-app.jfrog.io/api/" - askRateLimitHeader = "X-JFrog-CLI-AI" - // The latest version of the terms and conditions for using the AI interface. (https://docs.jfrog-applications.jfrog.io/jfrog-applications/jfrog-cli/cli-ai/terms) - aiTermsRevision = 1 -) - -type ApiType string - -const ( - ask ApiType = "ask" - feedback ApiType = "feedback" -) - -func HowCmd(c *cli.Context) error { - if show, err := cliutils.ShowCmdHelpIfNeeded(c, c.Args()); show || err != nil { - return err - } - if c.NArg() > 0 { - return cliutils.WrongNumberOfArgumentsHandler(c) - } - log.Output(coreutils.PrintLink("This AI-powered interface converts natural language inputs into AI-generated JFrog CLI commands.\n" + - "For more information about this interface, see https://docs.jfrog-applications.jfrog.io/jfrog-applications/jfrog-cli/cli-ai\n" + - "Try it out by typing a question, such as: 'How can I upload all .zip files from user/mylibs/ to the libs-local repository in Artifactory?'\n" + - "Note: JFrog AI Assistant is in beta and currently supports primarily Artifactory and Xray commands.\n")) - - // Ask the user to agree to the terms and conditions. If the user does not agree, the command will not proceed. - // Ask this only once per JFrog CLI installation, unless the terms are updated. - if agreed, err := handleAiTermsAgreement(); err != nil { - return err - } else if !agreed { - // If the user does not agree to the terms, the command will not proceed. - return reportTermsDisagreement() - } - - for { - var question string - scanner := bufio.NewScanner(os.Stdin) - fmt.Print("🐸 Your request:\n ") - for { - // Ask the user for a question - scanner.Scan() - question = strings.TrimSpace(scanner.Text()) - if question != "" { - // If the user entered a question, break the loop - break - } - } - fmt.Print("\nšŸ¤– Generated command:\n") - llmAnswer, err := askQuestion(question) - if err != nil { - return err - } - validResponse := strings.HasPrefix(llmAnswer, "jf") - // Print the generated command within a styled table frame. - if validResponse { - coreutils.PrintMessageInsideFrame(coreutils.PrintBoldTitle(llmAnswer), " ") - } else { - log.Output(" " + coreutils.PrintYellow(llmAnswer)) - } - - // If the response is a valid JFrog CLI command, ask the user for feedback. - if validResponse { - log.Output() - if err = handleResponseFeedback(); err != nil { - return err - } - } - - log.Output("\n" + coreutils.PrintComment("-------------------") + "\n") - } -} - -type questionBody struct { - Question string `json:"question"` -} - -func askQuestion(question string) (response string, err error) { - return sendRestAPI(ask, questionBody{Question: question}) -} - -type feedbackBody struct { - IsGoodResponse *bool `json:"is_good_response,omitempty"` - IsAgreedTerms *bool `json:"is_agreed_terms,omitempty"` -} - -func handleResponseFeedback() (err error) { - isGoodResponse, err := getUserFeedback() - if err != nil { - return - } - _, err = sendRestAPI(feedback, feedbackBody{IsGoodResponse: &isGoodResponse}) - return -} - -func reportTermsDisagreement() (err error) { - _, err = sendRestAPI(feedback, feedbackBody{IsAgreedTerms: clientutils.Pointer(false)}) - return -} - -func getUserFeedback() (bool, error) { - // Customize the template to place the options on the same line as the question - templates := &promptui.SelectTemplates{ - Label: "{{ . }}", - Active: " šŸ‘‰ {{ . | cyan }}", - Inactive: " {{ . }}", - Selected: "šŸ™ Thanks for your feedback!", - } - - prompt := promptui.Select{ - Label: "⭐ Rate this response:", - Items: []string{"šŸ‘ Good response!", "šŸ‘Ž Could be better..."}, - Templates: templates, - HideHelp: true, - } - selected, _, err := prompt.Run() - if err != nil { - return false, err - } - return selected == 0, nil -} - -func sendRestAPI(apiType ApiType, content interface{}) (response string, err error) { - contentBytes, err := json.Marshal(content) - if errorutils.CheckError(err) != nil { - return - } - client, err := httpclient.ClientBuilder().Build() - if errorutils.CheckError(err) != nil { - return - } - req, err := http.NewRequest(http.MethodPost, cliAiAppApiUrl+string(apiType), bytes.NewBuffer(contentBytes)) - if errorutils.CheckError(err) != nil { - return - } - req.Header.Set("Content-Type", "application/json") - if apiType == ask { - req.Header.Set(askRateLimitHeader, "true") - } - log.Debug(fmt.Sprintf("Sending HTTP %s request to: %s", req.Method, req.URL)) - resp, err := client.GetClient().Do(req) //#nosec G704 -- URL is constructed from a hardcoded AI service endpoint - if err != nil { - err = errorutils.CheckErrorf("CLI-AI server is not available. Please check your network or try again later.") - return - } - if resp == nil { - err = errorutils.CheckErrorf("received empty response from server") - return - } - if err = errorutils.CheckResponseStatus(resp, http.StatusOK); err != nil { - switch resp.StatusCode { - case http.StatusInternalServerError: - err = errorutils.CheckErrorf("JFrog CLI-AI model endpoint is not available. Please try again later.") - case http.StatusNotAcceptable: - err = errorutils.CheckErrorf("The system is currently handling multiple requests from other users\n" + - "Please try submitting your question again in a few minutes. Thank you for your patience!") - default: - err = errorutils.CheckErrorf("JFrog CLI-AI server is not available. Please check your network or try again later:\n%s", err.Error()) - } - return - } - - if apiType == feedback { - // If the API is feedback, no response is expected - return - } - - defer func() { - if resp.Body != nil { - err = errors.Join(err, errorutils.CheckError(resp.Body.Close())) - } - }() - var body []byte - // Limit size of response body to 10MB - body, err = io.ReadAll(io.LimitReader(resp.Body, 10*utils.SizeMiB)) - if errorutils.CheckError(err) != nil { - return - } - response = strings.TrimSpace(string(body)) - return -} - -func handleAiTermsAgreement() (bool, error) { - latestTermsVer, err := cliutils.GetLatestAiTermsRevision() - if err != nil { - return false, err - } - if latestTermsVer == nil || *latestTermsVer < aiTermsRevision { - if !coreutils.AskYesNo("By using this interface, you agree to the terms of JFrog's AI Addendum on behalf of your organization as an active JFrog customer.\n"+ - "Review these terms at "+coreutils.PrintLink("https://docs.jfrog-applications.jfrog.io/jfrog-applications/jfrog-cli/cli-ai/terms")+ - "\nDo you agree?", false) { - return false, nil - } - if err = cliutils.SetLatestAiTermsRevision(aiTermsRevision); err != nil { - return false, err - } - log.Output() - } - return true, nil -} diff --git a/go.mod b/go.mod index 0dcc507ce..9abcc17b2 100644 --- a/go.mod +++ b/go.mod @@ -26,7 +26,6 @@ require ( github.com/jfrog/jfrog-cli-security v1.29.0 github.com/jfrog/jfrog-client-go v1.55.1-0.20260508101905-a17af78a38d7 github.com/jszwec/csvutil v1.10.0 - github.com/manifoldco/promptui v0.9.0 github.com/spf13/viper v1.21.0 github.com/stretchr/testify v1.11.1 github.com/testcontainers/testcontainers-go v0.40.0 @@ -144,6 +143,7 @@ require ( github.com/ktrysmt/go-bitbucket v0.9.88 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/magiconair/properties v1.8.10 // indirect + github.com/manifoldco/promptui v0.9.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.21 // indirect diff --git a/main.go b/main.go index 2aa107b0c..4dfadf02e 100644 --- a/main.go +++ b/main.go @@ -10,7 +10,6 @@ import ( "strings" statsDocs "github.com/jfrog/jfrog-cli/docs/general/stats" - "github.com/jfrog/jfrog-cli/general/ai" services "github.com/jfrog/jfrog-cli/stats" "github.com/agnivade/levenshtein" @@ -29,7 +28,6 @@ import ( "github.com/jfrog/jfrog-cli/completion" "github.com/jfrog/jfrog-cli/config" "github.com/jfrog/jfrog-cli/docs/common" - aiDocs "github.com/jfrog/jfrog-cli/docs/general/ai" apiDocs "github.com/jfrog/jfrog-cli/docs/general/api" loginDocs "github.com/jfrog/jfrog-cli/docs/general/login" oidcDocs "github.com/jfrog/jfrog-cli/docs/general/oidc" @@ -303,13 +301,6 @@ func getCommands() ([]cli.Command, error) { Category: otherCategory, Action: login.LoginCmd, }, - { - Name: "how", - Usage: aiDocs.GetDescription(), - HelpName: corecommon.CreateUsage("how", aiDocs.GetDescription(), aiDocs.Usage), - BashComplete: corecommon.CreateBashCompletionFunc(), - Action: ai.HowCmd, - }, { Name: "access-token-create", Aliases: []string{"atc"}, diff --git a/utils/cliutils/persistence.go b/utils/cliutils/persistence.go index 235fdafb7..f87f1e675 100644 --- a/utils/cliutils/persistence.go +++ b/utils/cliutils/persistence.go @@ -16,7 +16,6 @@ const persistenceFileName = "persistence.json" // PersistenceFile holds various indicators that need to be persisted between CLI runs type PersistenceFile struct { LatestCliVersionCheckTime *int64 `json:"latestCliVersionCheckTime,omitempty"` - LatestAiTermsRevision *int `json:"latestAiTermsRevision,omitempty"` } var ( @@ -63,33 +62,6 @@ func getLatestCliVersionCheckTime() (*int64, error) { return info.LatestCliVersionCheckTime, nil } -// SetLatestAiTermsRevision updates the AI terms version in the persistence file -func SetLatestAiTermsRevision(version int) error { - fileLock.Lock() - defer fileLock.Unlock() - - info, err := getPersistenceInfo() - if err != nil { - return err - } - - info.LatestAiTermsRevision = &version - return setPersistenceInfo(info) -} - -// GetLatestAiTermsRevision retrieves the AI terms version from the persistence file -func GetLatestAiTermsRevision() (*int, error) { - fileLock.Lock() - defer fileLock.Unlock() - - info, err := getPersistenceInfo() - if err != nil { - return nil, err - } - - return info.LatestAiTermsRevision, nil -} - // getPersistenceInfo reads the persistence file, creates it if it doesn't exist, and returns the persisted info func getPersistenceInfo() (*PersistenceFile, error) { if err := getPersistenceFilePath(); err != nil { diff --git a/utils/cliutils/persistence_test.go b/utils/cliutils/persistence_test.go index cdf5826e7..1ff0871b3 100644 --- a/utils/cliutils/persistence_test.go +++ b/utils/cliutils/persistence_test.go @@ -27,24 +27,6 @@ func TestSetAndGetLatestVersionCheckTime(t *testing.T) { assert.Equal(t, timestamp, *storedTimestamp, "Stored timestamp does not match the set timestamp") } -// TestSetAndGetAiTermsVersion tests setting and getting the LatestAiTermsRevision -func TestSetAndGetAiTermsVersion(t *testing.T) { - // Setup temporary directory - persistenceFilePath = filepath.Join(t.TempDir(), persistenceFileName) - - // Set the AI terms version - version := 42 - err := SetLatestAiTermsRevision(version) - assert.NoError(t, err, "Failed to set LatestAiTermsRevision") - - // Get the AI terms version - storedVersion, err := GetLatestAiTermsRevision() - assert.NoError(t, err, "Failed to get LatestAiTermsRevision") - - // Assert equality - assert.Equal(t, version, *storedVersion, "Stored AI terms version does not match the set version") -} - // TestPersistenceFileCreation tests if the persistence file is created when it doesn't exist func TestPersistenceFileCreation(t *testing.T) { // Setup temporary directory