mirror of
https://github.com/Dvorinka/Devour.git
synced 2026-06-03 20:13:03 +00:00
182 lines
4.7 KiB
Go
182 lines
4.7 KiB
Go
package cmd
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/url"
|
|
"os"
|
|
"os/exec"
|
|
"sort"
|
|
"strings"
|
|
"unicode"
|
|
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
var (
|
|
autoDryRun bool
|
|
autoJSON bool
|
|
autoLang string
|
|
)
|
|
|
|
var autoCmd = &cobra.Command{
|
|
Use: "auto <intent>",
|
|
Short: "Route natural-language intent to the best Devour command",
|
|
Long: `Auto-classify intent and run the best matching command (get/scrape/ask/quality).
|
|
|
|
Examples:
|
|
devour auto "how to parse json in go"
|
|
devour auto "https://pkg.go.dev/net/http"
|
|
devour auto "check code quality" --dry-run
|
|
devour auto "what is useEffect" --lang react`,
|
|
Args: cobra.MinimumNArgs(1),
|
|
RunE: runAuto,
|
|
}
|
|
|
|
func init() {
|
|
autoCmd.Flags().BoolVar(&autoDryRun, "dry-run", false, "print selected command without executing")
|
|
autoCmd.Flags().BoolVar(&autoJSON, "json", false, "output route decision as JSON")
|
|
autoCmd.Flags().StringVar(&autoLang, "lang", "", "optional language override for ask/get routes")
|
|
}
|
|
|
|
type autoDecision struct {
|
|
Intent string `json:"intent"`
|
|
Route string `json:"route"`
|
|
Reason string `json:"reason"`
|
|
Command []string `json:"command"`
|
|
}
|
|
|
|
func runAuto(cmd *cobra.Command, args []string) error {
|
|
intent := strings.TrimSpace(strings.Join(args, " "))
|
|
if intent == "" {
|
|
return fmt.Errorf("intent is required")
|
|
}
|
|
|
|
decision, err := classifyIntent(intent, strings.TrimSpace(autoLang))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if autoJSON {
|
|
enc := json.NewEncoder(cmd.OutOrStdout())
|
|
enc.SetIndent("", " ")
|
|
return enc.Encode(decision)
|
|
}
|
|
|
|
fmt.Printf("Route: %s\n", decision.Route)
|
|
fmt.Printf("Reason: %s\n", decision.Reason)
|
|
fmt.Printf("Command: devour %s\n", strings.Join(decision.Command, " "))
|
|
|
|
if autoDryRun {
|
|
return nil
|
|
}
|
|
|
|
exe, err := os.Executable()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
run := exec.Command(exe, decision.Command...)
|
|
run.Stdout = cmd.OutOrStdout()
|
|
run.Stderr = cmd.ErrOrStderr()
|
|
return run.Run()
|
|
}
|
|
|
|
func classifyIntent(intent, langOverride string) (*autoDecision, error) {
|
|
lower := strings.ToLower(intent)
|
|
trimmed := strings.TrimSpace(intent)
|
|
|
|
if u, err := url.Parse(trimmed); err == nil && (u.Scheme == "http" || u.Scheme == "https") {
|
|
route := []string{"scrape", trimmed}
|
|
return &autoDecision{Intent: intent, Route: "scrape", Reason: "detected URL input", Command: route}, nil
|
|
}
|
|
|
|
if strings.Contains(lower, "quality") || strings.Contains(lower, "technical debt") || strings.Contains(lower, "lint") || strings.Contains(lower, "code smell") {
|
|
route := []string{"quality", "status"}
|
|
if strings.Contains(lower, "scan") {
|
|
route = []string{"quality", "scan", "."}
|
|
}
|
|
return &autoDecision{Intent: intent, Route: "quality", Reason: "detected quality-analysis intent", Command: route}, nil
|
|
}
|
|
|
|
language := strings.TrimSpace(langOverride)
|
|
if language == "" {
|
|
language = inferLanguageFromText(lower)
|
|
}
|
|
if language != "" {
|
|
if canonical, ok := normalizeLanguage(language); ok {
|
|
language = canonical
|
|
} else {
|
|
language = ""
|
|
}
|
|
}
|
|
|
|
if strings.Contains(lower, "?") || strings.Contains(lower, "how") || strings.Contains(lower, "why") || strings.Contains(lower, "what") {
|
|
if language == "" {
|
|
language = "go"
|
|
}
|
|
route := []string{"ask", "--lang", language, intent, "--format", "text"}
|
|
return &autoDecision{Intent: intent, Route: "ask", Reason: "question-style intent", Command: route}, nil
|
|
}
|
|
|
|
if language == "" {
|
|
language = "go"
|
|
}
|
|
keyword := inferKeyword(intent)
|
|
if canonical, ok := normalizeLanguage(keyword); ok && canonical == language {
|
|
keyword = "overview"
|
|
}
|
|
route := []string{"get", language, keyword}
|
|
return &autoDecision{Intent: intent, Route: "get", Reason: "default docs retrieval route", Command: route}, nil
|
|
}
|
|
|
|
func inferLanguageFromText(text string) string {
|
|
text = strings.ToLower(text)
|
|
if strings.Contains(text, "c#") {
|
|
return "csharp"
|
|
}
|
|
if strings.Contains(text, "next.js") {
|
|
return "nextjs"
|
|
}
|
|
|
|
tokens := strings.FieldsFunc(text, func(r rune) bool {
|
|
return !(unicode.IsLetter(r) || unicode.IsDigit(r))
|
|
})
|
|
tokenSet := make(map[string]bool, len(tokens))
|
|
for _, tok := range tokens {
|
|
if tok != "" {
|
|
tokenSet[tok] = true
|
|
}
|
|
}
|
|
|
|
aliases := make([]string, 0, len(languageAliases()))
|
|
for alias := range languageAliases() {
|
|
aliases = append(aliases, alias)
|
|
}
|
|
sort.Slice(aliases, func(i, j int) bool {
|
|
return len(aliases[i]) > len(aliases[j])
|
|
})
|
|
|
|
for _, alias := range aliases {
|
|
if tokenSet[alias] {
|
|
return alias
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func inferKeyword(intent string) string {
|
|
words := strings.Fields(strings.ToLower(intent))
|
|
stop := map[string]bool{
|
|
"get": true, "docs": true, "documentation": true, "about": true, "for": true, "on": true,
|
|
"the": true, "a": true, "an": true, "show": true, "me": true, "please": true,
|
|
}
|
|
for _, w := range words {
|
|
w = strings.Trim(w, ",.!?;:")
|
|
if w == "" || stop[w] || len(w) < 2 {
|
|
continue
|
|
}
|
|
return w
|
|
}
|
|
return "overview"
|
|
}
|