Files
Devour/cmd/auto.go
Tomas Dvorak 898a3c303f update
2026-02-24 10:33:59 +01:00

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"
}