118 lines
2.7 KiB
Go
118 lines
2.7 KiB
Go
// ©AngelaMos | 2026
|
|
// tui.go
|
|
|
|
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"github.com/CarterPerez-dev/nadezhda/internal/ai"
|
|
"github.com/CarterPerez-dev/nadezhda/internal/rank"
|
|
"github.com/CarterPerez-dev/nadezhda/internal/store"
|
|
"github.com/CarterPerez-dev/nadezhda/internal/tui"
|
|
)
|
|
|
|
var tuiSince string
|
|
|
|
var tuiCmd = &cobra.Command{
|
|
Use: "tui",
|
|
Short: "Browse aggregated news in an interactive terminal UI",
|
|
RunE: runTUI,
|
|
}
|
|
|
|
func init() {
|
|
tuiCmd.Flags().StringVar(&tuiSince, "since", "", "only clusters active within this window (e.g. 24h, 168h)")
|
|
rootCmd.AddCommand(tuiCmd)
|
|
}
|
|
|
|
func runTUI(cmd *cobra.Command, args []string) error {
|
|
cfg, err := loadConfig()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
now := time.Now()
|
|
var since int64
|
|
if tuiSince != "" {
|
|
d, err := time.ParseDuration(tuiSince)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid --since %q: %w", tuiSince, err)
|
|
}
|
|
since = now.Add(-d).Unix()
|
|
}
|
|
|
|
st, err := store.Open(cfg.DBPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer st.Close()
|
|
|
|
loader := func() (tui.Data, error) {
|
|
clusters, err := st.DigestClusters(since)
|
|
if err != nil {
|
|
return tui.Data{}, err
|
|
}
|
|
scored := rank.Rank(clusters, cfg.Rank, cfg.Watchlist, now)
|
|
detail := make(map[string]store.CVE)
|
|
for _, s := range scored {
|
|
for _, v := range s.Cluster.CVEs {
|
|
if _, ok := detail[v.ID]; ok {
|
|
continue
|
|
}
|
|
full, err := st.GetCVE(v.ID)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
detail[v.ID] = full
|
|
}
|
|
}
|
|
notes := map[int64]ai.IdeationResult{}
|
|
if persisted, err := st.LatestAINotes(); err == nil {
|
|
for cid, n := range persisted {
|
|
var angles []string
|
|
_ = json.Unmarshal([]byte(n.AnglesJSON), &angles)
|
|
notes[cid] = ai.IdeationResult{Summary: n.Summary, Why: n.Why, Angles: angles, Format: n.Format}
|
|
}
|
|
}
|
|
return tui.Data{Scored: scored, CVEDetail: detail, Notes: notes}, nil
|
|
}
|
|
|
|
var ideator tui.Ideator
|
|
if cfg.AI.Enabled {
|
|
provider, err := ai.Factory(cfg.AI)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ctx := cmd.Context()
|
|
ideator = func(c store.DigestCluster) (ai.IdeationResult, error) {
|
|
res, err := provider.Generate(ctx, ai.RequestFromCluster(c))
|
|
if err != nil {
|
|
return ai.IdeationResult{}, err
|
|
}
|
|
angles, err := json.Marshal(res.Angles)
|
|
if err != nil {
|
|
return ai.IdeationResult{}, err
|
|
}
|
|
note := store.AINote{
|
|
ClusterID: c.ClusterID,
|
|
Provider: provider.Name(),
|
|
Summary: res.Summary,
|
|
Why: res.Why,
|
|
AnglesJSON: string(angles),
|
|
Format: res.Format,
|
|
CreatedAt: time.Now().Unix(),
|
|
}
|
|
if err := st.InsertAINote(note); err != nil {
|
|
return ai.IdeationResult{}, fmt.Errorf("save note: %w", err)
|
|
}
|
|
return res, nil
|
|
}
|
|
}
|
|
|
|
return tui.Run(loader, ideator)
|
|
}
|