mirror of
https://github.com/Dvorinka/Devour.git
synced 2026-06-03 20:13:03 +00:00
update
This commit is contained in:
+181
@@ -0,0 +1,181 @@
|
||||
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"
|
||||
}
|
||||
Reference in New Issue
Block a user