Skip to content

Commit 1b43e7c

Browse files
committed
feat: v1 cli
1 parent 0264371 commit 1b43e7c

16 files changed

Lines changed: 1563 additions & 580 deletions

File tree

cmd/ask.go

Lines changed: 82 additions & 195 deletions
Original file line numberDiff line numberDiff line change
@@ -1,207 +1,94 @@
11
package cmd
22

33
import (
4-
"bytes"
5-
"encoding/json"
6-
"fmt"
7-
"net/http"
8-
"os"
9-
"os/user"
10-
"regexp"
11-
"strings"
12-
"time"
13-
14-
"github.com/atotto/clipboard"
15-
"github.com/spf13/cobra"
16-
"github.com/fatih/color"
4+
"context"
5+
"fmt"
6+
"strings"
7+
"time"
8+
9+
"docsgpt-cli/internal/api"
10+
"docsgpt-cli/internal/config"
11+
ctxenrich "docsgpt-cli/internal/context"
12+
"docsgpt-cli/internal/display"
13+
"docsgpt-cli/internal/tools"
14+
15+
"github.com/fatih/color"
16+
"github.com/spf13/cobra"
1717
)
1818

1919
var askCmd = &cobra.Command{
20-
Use: "ask",
21-
Short: "Ask a question to DocsGPT",
22-
Long: `Ask a question to DocsGPT, and instantly find answers about anything.
20+
Use: "ask",
21+
Short: "Ask a question to DocsGPT",
22+
Long: `Ask a question to DocsGPT, and instantly find answers about anything.
2323
2424
Example usage:
2525
docsgpt-cli ask "How do I open a file in Python?"
2626
2727
This command will provide a contextual answer and, if applicable, copy a relevant code snippet to your clipboard.`,
28-
Run: func(cmd *cobra.Command, args []string) {
29-
keys := loadKeys()
30-
keyName, key := getDefaultKey(keys)
31-
if key.Key == "" {
32-
return
33-
}
34-
35-
questionWithContext := getQuestionWithContext(args)
36-
37-
done := make(chan bool)
38-
go spinner(done) // Start the spinner in a separate goroutine
39-
40-
green := color.New(color.FgGreen).SprintFunc()
41-
fmt.Printf(green("Key: %s\n"), keyName) // Print the name of the selected key
42-
43-
answer, err := requestDocsgpt(questionWithContext, key.Key)
44-
done <- true // Stop the spinner
45-
if err != nil {
46-
printError(err.Error())
47-
return
48-
}
49-
50-
fmt.Printf(green(" ❯ "))
51-
fmt.Println(answer)
52-
53-
command := extractCommand(answer)
54-
if command != "" {
55-
copyToClipboard(command)
56-
}
57-
},
58-
}
59-
60-
func getQuestionWithContext(args []string) string {
61-
settings := loadSettings()
62-
question := strings.Join(args, " ")
63-
64-
var context string
65-
66-
if settings.SendCurrentDirectory {
67-
currentPath, _ := os.Getwd()
68-
context += fmt.Sprintf("CURRENT_DIRECTORY: %s\n", currentPath)
69-
}
70-
71-
if settings.SendDirectoryContents {
72-
currentPath, _ := os.Getwd()
73-
files, _ := os.ReadDir(currentPath)
74-
75-
var fileNames []string
76-
for _, file := range files {
77-
fileNames = append(fileNames, file.Name())
78-
}
79-
directoryContents := strings.Join(fileNames, ", ")
80-
context += fmt.Sprintf("DIRECTORY_CONTENTS: %s\n", directoryContents)
81-
}
82-
83-
if settings.SendLastCommands {
84-
lastCommands := getLastCommands(settings.NumberOfLastCommands)
85-
context += fmt.Sprintf("LAST_COMMANDS:\n%s\n", lastCommands)
86-
}
87-
88-
return fmt.Sprintf("QUESTION: %s\n\n%s", question, context)
89-
}
90-
91-
func getLastCommands(n int) string {
92-
shell := os.Getenv("SHELL")
93-
var historyFile string
94-
95-
usr, _ := user.Current()
96-
homeDir := usr.HomeDir
97-
98-
if strings.Contains(shell, "zsh") {
99-
historyFile = fmt.Sprintf("%s/.zsh_history", homeDir)
100-
} else if strings.Contains(shell, "bash") {
101-
historyFile = fmt.Sprintf("%s/.bash_history", homeDir)
102-
} else if strings.Contains(shell, "fish") {
103-
historyFile = fmt.Sprintf("%s/.local/share/fish/fish_history", homeDir)
104-
} else {
105-
return "Unknown shell"
106-
}
107-
108-
data, err := os.ReadFile(historyFile)
109-
if err != nil {
110-
return "Could not read history"
111-
}
112-
113-
lines := strings.Split(string(data), "\n")
114-
var commands []string
115-
116-
// Process zsh and bash history to strip out timestamps and other metadata
117-
if strings.Contains(shell, "zsh") || strings.Contains(shell, "bash") {
118-
for _, line := range lines {
119-
line = strings.TrimSpace(line) // Remove leading/trailing whitespace
120-
if strings.HasPrefix(line, ":") {
121-
// For zsh history lines that look like `: 1623390275:0;command`
122-
parts := strings.SplitN(line, ";", 2)
123-
if len(parts) == 2 {
124-
commands = append(commands, parts[1])
125-
}
126-
} else if line != "" {
127-
// Include lines without timestamps (typical in .bash_history)
128-
commands = append(commands, line)
129-
}
130-
}
131-
} else if strings.Contains(shell, "fish") {
132-
for _, line := range lines {
133-
if strings.HasPrefix(line, "- cmd: ") {
134-
commands = append(commands, strings.TrimPrefix(line, "- cmd: "))
135-
}
136-
}
137-
} else {
138-
return "Unsupported shell"
139-
}
140-
141-
// Ensure we're only taking the last n commands
142-
if len(commands) > n {
143-
return strings.Join(commands[len(commands)-n:], "\n")
144-
}
145-
return strings.Join(commands, "\n")
146-
}
147-
148-
func requestDocsgpt(question, apiKey string) (string, error) {
149-
payload := map[string]string{"question": question, "api_key": apiKey}
150-
jsonPayload, _ := json.Marshal(payload)
151-
152-
resp, err := http.Post("https://gptcloud.arc53.com/api/answer", "application/json", bytes.NewBuffer(jsonPayload))
153-
if err != nil {
154-
return "", err
155-
}
156-
defer resp.Body.Close()
157-
158-
var result map[string]interface{}
159-
_ = json.NewDecoder(resp.Body).Decode(&result)
160-
161-
if answer, found := result["answer"].(string); found {
162-
return answer, nil
163-
}
164-
return "", fmt.Errorf("no answer in response")
165-
}
166-
167-
func extractCommand(answer string) string {
168-
re := regexp.MustCompile("(?s)```bash(.*?)```")
169-
match := re.FindStringSubmatch(answer)
170-
if len(match) > 1 {
171-
return match[1]
172-
}
173-
174-
re = regexp.MustCompile("(?s)```sh(.*?)```")
175-
match = re.FindStringSubmatch(answer)
176-
if len(match) > 1 {
177-
return match[1]
178-
}
179-
180-
return ""
181-
}
182-
183-
func copyToClipboard(command string) {
184-
trimmedCommand := strings.TrimSpace(command)
185-
err := clipboard.WriteAll(trimmedCommand)
186-
if err != nil {
187-
printError("Failed to copy to clipboard: " + err.Error())
188-
} else {
189-
green := color.New(color.FgGreen).SprintFunc()
190-
fmt.Printf("%s %s\n", green("Command copied to clipboard:"), green(trimmedCommand))
191-
}
192-
}
193-
194-
func spinner(done chan bool) {
195-
for {
196-
select {
197-
case <-done:
198-
fmt.Print("\r") // Clear the spinner
199-
return
200-
default:
201-
for _, r := range `-\|/` {
202-
fmt.Printf("\r[%c] Loading...", r)
203-
time.Sleep(100 * time.Millisecond)
204-
}
205-
}
206-
}
28+
RunE: func(cmd *cobra.Command, args []string) error {
29+
if len(args) == 0 {
30+
return fmt.Errorf("please provide a question")
31+
}
32+
33+
cfg, err := config.Load()
34+
if err != nil {
35+
return err
36+
}
37+
38+
keyName, apiKey, err := cfg.ResolveKey(globalKey)
39+
if err != nil {
40+
return err
41+
}
42+
43+
baseURL := cfg.ResolveURL(globalURL)
44+
client := api.NewClient(baseURL, apiKey)
45+
46+
question := strings.Join(args, " ")
47+
includeContext := !globalNoContext
48+
fullQuestion := ctxenrich.BuildQuestion(question, cfg.Settings, includeContext)
49+
50+
messages := []api.Message{
51+
{Role: "user", Content: fullQuestion},
52+
}
53+
54+
green := color.New(color.FgGreen).SprintFunc()
55+
fmt.Printf(green("Key: %s\n"), keyName)
56+
fmt.Printf(green(" ❯ "))
57+
58+
ctx := context.Background()
59+
toolDefs := tools.ToolDefinitions()
60+
timeout := time.Duration(globalTimeout) * time.Second
61+
62+
onDelta := func(delta api.Delta, finishReason string) {
63+
display.StreamDelta(delta)
64+
}
65+
66+
onToolCall := func(tc api.ToolCall) string {
67+
return handleToolCall(tc, timeout)
68+
}
69+
70+
updatedHistory, err := client.RunWithTools(
71+
ctx, messages, toolDefs, !globalNoStream, onDelta, onToolCall,
72+
)
73+
if err != nil {
74+
return err
75+
}
76+
fmt.Println()
77+
78+
// Find the last assistant message for clipboard
79+
var answer string
80+
for i := len(updatedHistory) - 1; i >= 0; i-- {
81+
if updatedHistory[i].Role == "assistant" {
82+
answer = updatedHistory[i].Content
83+
break
84+
}
85+
}
86+
87+
command := extractCommand(answer)
88+
if command != "" {
89+
copyToClipboard(command)
90+
}
91+
92+
return nil
93+
},
20794
}

0 commit comments

Comments
 (0)