feat: AI suggestion (#34)
## Feat * feat(ai): add AI suggestion engine, context gathering, and ghost text overlay (`v0.3.0`) ## Security & Perf * fix(ai): restrict background `--help` execution to a hardcoded command allowlist to prevent RCE * fix(root): defer context cancellation in goroutine to prevent resource leaks * perf(ai): implement LRU eviction and maximum size limit for `ProviderCache` * perf(ai): truncate command context output (`CommandContextProvider.Gather`) to 1000 characters to save tokens ## Bug Fixes * fix(ai): add mutex synchronization and thread-safe snapshotting to `AIEngine.RegisterProvider` and `GatherDynamicContext` * fix(ai): return a copy of fresh `Suggestion` in `AIEngine.Suggest` to prevent cache mutation * fix(ai): use length-prefixed encoding in `EnvSnapshot.Hash` to prevent delimiter collisions * fix(ai): skip variable assignment lines containing `=` when extracting Makefile targets * fix(runner): check `scanner.Err()` and return `nil` on scan errors in justfile generator * test(ci): rename `git commit` to `git checkout` in overlay test to resolve CI `typos` false positive
This commit is contained in:
+68
-4
@@ -2,11 +2,13 @@ package root
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/versenilvis/iris/spec"
|
||||
"github.com/versenilvis/iris/ai"
|
||||
"github.com/versenilvis/iris/config"
|
||||
"github.com/versenilvis/iris/integration"
|
||||
"github.com/versenilvis/iris/logger"
|
||||
"github.com/versenilvis/iris/spec"
|
||||
)
|
||||
|
||||
// MergeResults collects and dedupes suggestions for a query and mode
|
||||
@@ -38,6 +40,12 @@ func MergeResults(query string, mode string) []spec.Suggestion {
|
||||
if normalizedCmd == normalizedQuery {
|
||||
return
|
||||
}
|
||||
if s.Source == "" {
|
||||
s.Source = "spec"
|
||||
if s.Confidence == 0 {
|
||||
s.Confidence = 50
|
||||
}
|
||||
}
|
||||
if !seen[s.Cmd] {
|
||||
seen[s.Cmd] = true
|
||||
deduped = append(deduped, s)
|
||||
@@ -48,9 +56,11 @@ func MergeResults(query string, mode string) []spec.Suggestion {
|
||||
// history mode: history first, then spec/alias
|
||||
for _, h := range histResults {
|
||||
addSuggestion(spec.Suggestion{
|
||||
Cmd: h.Cmd,
|
||||
Desc: "history",
|
||||
Icon: "history",
|
||||
Cmd: h.Cmd,
|
||||
Desc: "history",
|
||||
Icon: "history",
|
||||
Source: "history",
|
||||
Confidence: 70,
|
||||
})
|
||||
}
|
||||
for _, s := range cmdResults {
|
||||
@@ -63,8 +73,62 @@ func MergeResults(query string, mode string) []spec.Suggestion {
|
||||
}
|
||||
}
|
||||
|
||||
if aiSugg := GetCurrentAISuggestion(); aiSugg != nil {
|
||||
normalizedCmd := strings.TrimSpace(aiSugg.Cmd)
|
||||
if normalizedCmd != "" && normalizedCmd != normalizedQuery && strings.HasPrefix(strings.ToLower(normalizedCmd), strings.ToLower(normalizedQuery)) {
|
||||
if !seen[aiSugg.Cmd] {
|
||||
seen[aiSugg.Cmd] = true
|
||||
if len(deduped) == 0 || aiSugg.Confidence > deduped[0].Confidence {
|
||||
deduped = append([]spec.Suggestion{*aiSugg}, deduped...)
|
||||
} else {
|
||||
deduped = append(deduped, *aiSugg)
|
||||
}
|
||||
} else if len(deduped) > 0 && aiSugg.Confidence > deduped[0].Confidence {
|
||||
for i, item := range deduped {
|
||||
if item.Cmd == aiSugg.Cmd {
|
||||
deduped = append(deduped[:i], deduped[i+1:]...)
|
||||
deduped = append([]spec.Suggestion{*aiSugg}, deduped...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(deduped) > maxSugg {
|
||||
deduped = deduped[:maxSugg]
|
||||
}
|
||||
return deduped
|
||||
}
|
||||
|
||||
var (
|
||||
aiEngine *ai.AIEngine
|
||||
aiEngineOnce sync.Once
|
||||
)
|
||||
|
||||
func GetAIEngine() *ai.AIEngine {
|
||||
aiEngineOnce.Do(func() {
|
||||
aiEngine = ai.NewAIEngine(nil)
|
||||
for _, p := range ai.DefaultProviders {
|
||||
aiEngine.RegisterProvider(p)
|
||||
}
|
||||
})
|
||||
return aiEngine
|
||||
}
|
||||
|
||||
var (
|
||||
currentAISugg *spec.Suggestion
|
||||
aiSuggMu sync.RWMutex
|
||||
)
|
||||
|
||||
func SetCurrentAISuggestion(sugg *spec.Suggestion) {
|
||||
aiSuggMu.Lock()
|
||||
defer aiSuggMu.Unlock()
|
||||
currentAISugg = sugg
|
||||
}
|
||||
|
||||
func GetCurrentAISuggestion() *spec.Suggestion {
|
||||
aiSuggMu.RLock()
|
||||
defer aiSuggMu.RUnlock()
|
||||
return currentAISugg
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user