[agent/codex] A specs/ folder has been injected into this workspace with R... #12
31 changed files with 1500 additions and 605 deletions
|
|
@ -3,21 +3,22 @@ package ai
|
|||
import (
|
||||
"bufio"
|
||||
"cmp"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"dappco.re/go/core"
|
||||
coreio "dappco.re/go/core/io"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
)
|
||||
|
||||
// metricsMu protects concurrent file writes in Record.
|
||||
var metricsMu sync.Mutex
|
||||
|
||||
// Event represents a recorded AI/security metric event.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// _ = Record(Event{Type: "security.scan", Repo: "host-uk/core"})
|
||||
type Event struct {
|
||||
Type string `json:"type"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
|
|
@ -29,20 +30,30 @@ type Event struct {
|
|||
|
||||
// metricsDir returns the base directory for metrics storage.
|
||||
func metricsDir() (string, error) {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", coreerr.E("ai.metricsDir", "get home directory", err)
|
||||
home := core.Env("CORE_HOME")
|
||||
if home == "" {
|
||||
home = core.Env("HOME")
|
||||
}
|
||||
return filepath.Join(home, ".core", "ai", "metrics"), nil
|
||||
if home == "" {
|
||||
home = core.Env("DIR_HOME")
|
||||
}
|
||||
if home == "" {
|
||||
return "", core.E("ai.metricsDir", "home directory unavailable", nil)
|
||||
}
|
||||
return core.Path(home, ".core", "ai", "metrics"), nil
|
||||
}
|
||||
|
||||
// metricsFilePath returns the JSONL file path for the given date.
|
||||
func metricsFilePath(dir string, t time.Time) string {
|
||||
return filepath.Join(dir, t.Format("2006-01-02")+".jsonl")
|
||||
return core.Path(dir, core.Concat(t.Format("2006-01-02"), ".jsonl"))
|
||||
}
|
||||
|
||||
// Record appends an event to the daily JSONL file at
|
||||
// ~/.core/ai/metrics/YYYY-MM-DD.jsonl.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// err := Record(Event{Type: "metrics.read", AgentID: "agent-1"})
|
||||
func Record(event Event) (err error) {
|
||||
if event.Timestamp.IsZero() {
|
||||
event.Timestamp = time.Now()
|
||||
|
|
@ -56,35 +67,39 @@ func Record(event Event) (err error) {
|
|||
return err
|
||||
}
|
||||
|
||||
if err := coreio.Local.EnsureDir(dir); err != nil {
|
||||
return coreerr.E("ai.Record", "create metrics directory", err)
|
||||
if err := coreio.EnsureDir(coreio.Local, dir); err != nil {
|
||||
return core.E("ai.Record", "create metrics directory", err)
|
||||
}
|
||||
|
||||
path := metricsFilePath(dir, event.Timestamp)
|
||||
|
||||
f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
|
||||
f, err := coreio.Local.Append(path)
|
||||
if err != nil {
|
||||
return coreerr.E("ai.Record", "open metrics file", err)
|
||||
return core.E("ai.Record", "open metrics file", err)
|
||||
}
|
||||
defer func() {
|
||||
if cerr := f.Close(); cerr != nil && err == nil {
|
||||
err = coreerr.E("ai.Record", "close metrics file", cerr)
|
||||
err = core.E("ai.Record", "close metrics file", cerr)
|
||||
}
|
||||
}()
|
||||
|
||||
data, err := json.Marshal(event)
|
||||
if err != nil {
|
||||
return coreerr.E("ai.Record", "marshal event", err)
|
||||
data := core.JSONMarshal(event)
|
||||
if !data.OK {
|
||||
return core.E("ai.Record", "marshal event", data.Value.(error))
|
||||
}
|
||||
|
||||
if _, err := f.Write(append(data, '\n')); err != nil {
|
||||
return coreerr.E("ai.Record", "write event", err)
|
||||
if _, err := f.Write(append(data.Value.([]byte), '\n')); err != nil {
|
||||
return core.E("ai.Record", "write event", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadEvents reads events from JSONL files within the given time range.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// events, err := ReadEvents(time.Now().Add(-24 * time.Hour))
|
||||
func ReadEvents(since time.Time) ([]Event, error) {
|
||||
dir, err := metricsDir()
|
||||
if err != nil {
|
||||
|
|
@ -110,20 +125,21 @@ func ReadEvents(since time.Time) ([]Event, error) {
|
|||
|
||||
// readMetricsFile reads events from a single JSONL file, returning only those at or after since.
|
||||
func readMetricsFile(path string, since time.Time) ([]Event, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, coreerr.E("ai.readMetricsFile", "open metrics file", err)
|
||||
if !coreio.Local.Exists(path) {
|
||||
return nil, nil
|
||||
}
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
f, err := coreio.Local.Open(path)
|
||||
if err != nil {
|
||||
return nil, core.E("ai.readMetricsFile", "open metrics file", err)
|
||||
}
|
||||
defer core.CloseStream(f)
|
||||
|
||||
var events []Event
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
var ev Event
|
||||
if err := json.Unmarshal(scanner.Bytes(), &ev); err != nil {
|
||||
if result := core.JSONUnmarshal(scanner.Bytes(), &ev); !result.OK {
|
||||
continue // skip malformed lines
|
||||
}
|
||||
if !ev.Timestamp.Before(since) {
|
||||
|
|
@ -131,12 +147,16 @@ func readMetricsFile(path string, since time.Time) ([]Event, error) {
|
|||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, coreerr.E("ai.readMetricsFile", "read metrics file", err)
|
||||
return nil, core.E("ai.readMetricsFile", "read metrics file", err)
|
||||
}
|
||||
return events, nil
|
||||
}
|
||||
|
||||
// Summary aggregates events into counts by type, repo, and agent.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// summary := Summary(events)
|
||||
func Summary(events []Event) map[string]any {
|
||||
byType := make(map[string]int)
|
||||
byRepo := make(map[string]int)
|
||||
|
|
@ -172,7 +192,10 @@ func sortedMap(m map[string]int) []map[string]any {
|
|||
}
|
||||
|
||||
slices.SortFunc(entries, func(a, b entry) int {
|
||||
return cmp.Compare(b.count, a.count)
|
||||
if diff := cmp.Compare(b.count, a.count); diff != 0 {
|
||||
return diff
|
||||
}
|
||||
return cmp.Compare(a.key, b.key)
|
||||
})
|
||||
|
||||
result := make([]map[string]any, len(entries))
|
||||
|
|
|
|||
|
|
@ -1,48 +1,22 @@
|
|||
package ai
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
coreio "dappco.re/go/core/io"
|
||||
"dappco.re/go/core"
|
||||
)
|
||||
|
||||
// --- Helpers ---
|
||||
|
||||
// setupBenchMetricsDir overrides the metrics directory to a temp dir for benchmarks.
|
||||
// Returns a cleanup function to restore the original.
|
||||
func setupBenchMetricsDir(b *testing.B) string {
|
||||
b.Helper()
|
||||
dir := b.TempDir()
|
||||
// Override HOME so metricsDir() resolves to our temp dir
|
||||
origHome := os.Getenv("HOME")
|
||||
tmpHome := b.TempDir()
|
||||
// Create the metrics path under the fake HOME
|
||||
metricsPath := filepath.Join(tmpHome, ".core", "ai", "metrics")
|
||||
if err := coreio.Local.EnsureDir(metricsPath); err != nil {
|
||||
b.Fatalf("Failed to create metrics dir: %v", err)
|
||||
}
|
||||
os.Setenv("HOME", tmpHome)
|
||||
b.Cleanup(func() {
|
||||
os.Setenv("HOME", origHome)
|
||||
})
|
||||
_ = dir
|
||||
return metricsPath
|
||||
}
|
||||
|
||||
// seedEvents writes n events to the metrics directory for the current day.
|
||||
func seedEvents(b *testing.B, n int) {
|
||||
b.Helper()
|
||||
now := time.Now()
|
||||
for i := range n {
|
||||
ev := Event{
|
||||
Type: fmt.Sprintf("type-%d", i%10),
|
||||
Type: core.Sprintf("type-%d", i%10),
|
||||
Timestamp: now.Add(-time.Duration(i) * time.Millisecond),
|
||||
AgentID: fmt.Sprintf("agent-%d", i%5),
|
||||
Repo: fmt.Sprintf("repo-%d", i%3),
|
||||
AgentID: core.Sprintf("agent-%d", i%5),
|
||||
Repo: core.Sprintf("repo-%d", i%3),
|
||||
Data: map[string]any{"i": i, "tool": "bench_tool"},
|
||||
}
|
||||
if err := Record(ev); err != nil {
|
||||
|
|
@ -55,7 +29,7 @@ func seedEvents(b *testing.B, n int) {
|
|||
|
||||
// BenchmarkMetricsRecord benchmarks writing individual metric events.
|
||||
func BenchmarkMetricsRecord(b *testing.B) {
|
||||
setupBenchMetricsDir(b)
|
||||
withTempHome(b)
|
||||
|
||||
now := time.Now()
|
||||
b.ResetTimer()
|
||||
|
|
@ -76,7 +50,7 @@ func BenchmarkMetricsRecord(b *testing.B) {
|
|||
|
||||
// BenchmarkMetricsRecord_Parallel benchmarks concurrent metric recording.
|
||||
func BenchmarkMetricsRecord_Parallel(b *testing.B) {
|
||||
setupBenchMetricsDir(b)
|
||||
withTempHome(b)
|
||||
|
||||
now := time.Now()
|
||||
b.ResetTimer()
|
||||
|
|
@ -101,7 +75,7 @@ func BenchmarkMetricsRecord_Parallel(b *testing.B) {
|
|||
|
||||
// BenchmarkMetricsQuery_10K benchmarks querying 10K events.
|
||||
func BenchmarkMetricsQuery_10K(b *testing.B) {
|
||||
setupBenchMetricsDir(b)
|
||||
withTempHome(b)
|
||||
seedEvents(b, 10_000)
|
||||
|
||||
since := time.Now().Add(-24 * time.Hour)
|
||||
|
|
@ -120,7 +94,7 @@ func BenchmarkMetricsQuery_10K(b *testing.B) {
|
|||
|
||||
// BenchmarkMetricsQuery_50K benchmarks querying 50K events.
|
||||
func BenchmarkMetricsQuery_50K(b *testing.B) {
|
||||
setupBenchMetricsDir(b)
|
||||
withTempHome(b)
|
||||
seedEvents(b, 50_000)
|
||||
|
||||
since := time.Now().Add(-24 * time.Hour)
|
||||
|
|
@ -139,7 +113,7 @@ func BenchmarkMetricsQuery_50K(b *testing.B) {
|
|||
|
||||
// BenchmarkMetricsSummary_10K benchmarks summarising 10K events.
|
||||
func BenchmarkMetricsSummary_10K(b *testing.B) {
|
||||
setupBenchMetricsDir(b)
|
||||
withTempHome(b)
|
||||
seedEvents(b, 10_000)
|
||||
|
||||
since := time.Now().Add(-24 * time.Hour)
|
||||
|
|
@ -159,14 +133,14 @@ func BenchmarkMetricsSummary_10K(b *testing.B) {
|
|||
|
||||
// BenchmarkMetricsRecordAndQuery benchmarks the full write-then-read cycle at 10K scale.
|
||||
func BenchmarkMetricsRecordAndQuery(b *testing.B) {
|
||||
setupBenchMetricsDir(b)
|
||||
withTempHome(b)
|
||||
|
||||
now := time.Now()
|
||||
|
||||
// Write 10K events
|
||||
for i := range 10_000 {
|
||||
ev := Event{
|
||||
Type: fmt.Sprintf("type-%d", i%10),
|
||||
Type: core.Sprintf("type-%d", i%10),
|
||||
Timestamp: now,
|
||||
AgentID: "bench",
|
||||
Repo: "bench-repo",
|
||||
|
|
@ -190,19 +164,9 @@ func BenchmarkMetricsRecordAndQuery(b *testing.B) {
|
|||
|
||||
// --- Unit tests for metrics at scale ---
|
||||
|
||||
// TestMetricsRecordAndRead_10K_Good writes 10K events and reads them back.
|
||||
func TestMetricsRecordAndRead_10K_Good(t *testing.T) {
|
||||
// Override HOME to temp dir
|
||||
origHome := os.Getenv("HOME")
|
||||
tmpHome := t.TempDir()
|
||||
metricsPath := filepath.Join(tmpHome, ".core", "ai", "metrics")
|
||||
if err := coreio.Local.EnsureDir(metricsPath); err != nil {
|
||||
t.Fatalf("Failed to create metrics dir: %v", err)
|
||||
}
|
||||
os.Setenv("HOME", tmpHome)
|
||||
t.Cleanup(func() {
|
||||
os.Setenv("HOME", origHome)
|
||||
})
|
||||
// TestMetricsBench_MetricsRecordAndRead_Good writes 10K events and reads them back.
|
||||
func TestMetricsBench_MetricsRecordAndRead_Good(t *testing.T) {
|
||||
withTempHome(t)
|
||||
|
||||
now := time.Now()
|
||||
const n = 10_000
|
||||
|
|
@ -210,10 +174,10 @@ func TestMetricsRecordAndRead_10K_Good(t *testing.T) {
|
|||
// Write events
|
||||
for i := range n {
|
||||
ev := Event{
|
||||
Type: fmt.Sprintf("type-%d", i%10),
|
||||
Type: core.Sprintf("type-%d", i%10),
|
||||
Timestamp: now.Add(-time.Duration(i) * time.Millisecond),
|
||||
AgentID: fmt.Sprintf("agent-%d", i%5),
|
||||
Repo: fmt.Sprintf("repo-%d", i%3),
|
||||
AgentID: core.Sprintf("agent-%d", i%5),
|
||||
Repo: core.Sprintf("repo-%d", i%3),
|
||||
Data: map[string]any{"index": i},
|
||||
}
|
||||
if err := Record(ev); err != nil {
|
||||
|
|
|
|||
|
|
@ -1,213 +1,198 @@
|
|||
package ai
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"dappco.re/go/core"
|
||||
coreio "dappco.re/go/core/io"
|
||||
)
|
||||
|
||||
// withTempHome overrides HOME to a temp dir for the duration of the test.
|
||||
func withTempHome(t *testing.T) {
|
||||
t.Helper()
|
||||
origHome := os.Getenv("HOME")
|
||||
tmpHome := t.TempDir()
|
||||
metricsPath := filepath.Join(tmpHome, ".core", "ai", "metrics")
|
||||
if err := coreio.Local.EnsureDir(metricsPath); err != nil {
|
||||
t.Fatalf("create metrics dir: %v", err)
|
||||
func withTempHome(tb testing.TB) {
|
||||
tb.Helper()
|
||||
tmpHome := tb.TempDir()
|
||||
metricsPath := core.Path(tmpHome, ".core", "ai", "metrics")
|
||||
if err := coreio.EnsureDir(coreio.Local, metricsPath); err != nil {
|
||||
tb.Fatalf("create metrics dir: %v", err)
|
||||
}
|
||||
os.Setenv("HOME", tmpHome)
|
||||
t.Cleanup(func() { os.Setenv("HOME", origHome) })
|
||||
tb.Setenv("HOME", tmpHome)
|
||||
tb.Setenv("CORE_HOME", tmpHome)
|
||||
}
|
||||
|
||||
// --- Record ---
|
||||
func TestMetrics_Record_Good(t *testing.T) {
|
||||
t.Run("PersistsEvent", func(t *testing.T) {
|
||||
withTempHome(t)
|
||||
|
||||
func TestRecord_Good(t *testing.T) {
|
||||
withTempHome(t)
|
||||
ev := Event{
|
||||
Type: "test_event",
|
||||
AgentID: "agent-1",
|
||||
Repo: "repo-1",
|
||||
Data: map[string]any{"key": "value"},
|
||||
}
|
||||
if err := Record(ev); err != nil {
|
||||
t.Fatalf("Record: %v", err)
|
||||
}
|
||||
|
||||
ev := Event{
|
||||
Type: "test_event",
|
||||
AgentID: "agent-1",
|
||||
Repo: "repo-1",
|
||||
Data: map[string]any{"key": "value"},
|
||||
}
|
||||
if err := Record(ev); err != nil {
|
||||
t.Fatalf("Record: %v", err)
|
||||
}
|
||||
events, err := ReadEvents(time.Now().Add(-time.Hour))
|
||||
if err != nil {
|
||||
t.Fatalf("ReadEvents: %v", err)
|
||||
}
|
||||
if len(events) != 1 {
|
||||
t.Fatalf("expected 1 event, got %d", len(events))
|
||||
}
|
||||
if events[0].Type != "test_event" {
|
||||
t.Errorf("expected type test_event, got %s", events[0].Type)
|
||||
}
|
||||
if events[0].Timestamp.IsZero() {
|
||||
t.Error("expected auto-set timestamp, got zero")
|
||||
}
|
||||
})
|
||||
|
||||
events, err := ReadEvents(time.Now().Add(-time.Hour))
|
||||
if err != nil {
|
||||
t.Fatalf("ReadEvents: %v", err)
|
||||
}
|
||||
if len(events) != 1 {
|
||||
t.Fatalf("expected 1 event, got %d", len(events))
|
||||
}
|
||||
if events[0].Type != "test_event" {
|
||||
t.Errorf("expected type test_event, got %s", events[0].Type)
|
||||
}
|
||||
if events[0].Timestamp.IsZero() {
|
||||
t.Error("expected auto-set timestamp, got zero")
|
||||
}
|
||||
t.Run("AutoTimestamp", func(t *testing.T) {
|
||||
withTempHome(t)
|
||||
|
||||
before := time.Now()
|
||||
ev := Event{Type: "auto_ts"}
|
||||
if err := Record(ev); err != nil {
|
||||
t.Fatalf("Record: %v", err)
|
||||
}
|
||||
after := time.Now()
|
||||
|
||||
events, err := ReadEvents(before)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadEvents: %v", err)
|
||||
}
|
||||
if len(events) != 1 {
|
||||
t.Fatalf("expected 1 event, got %d", len(events))
|
||||
}
|
||||
ts := events[0].Timestamp
|
||||
if ts.Before(before) || ts.After(after) {
|
||||
t.Errorf("timestamp %v not in range [%v, %v]", ts, before, after)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("PresetTimestamp", func(t *testing.T) {
|
||||
withTempHome(t)
|
||||
|
||||
fixed := time.Date(2026, 3, 15, 10, 0, 0, 0, time.UTC)
|
||||
ev := Event{Type: "preset_ts", Timestamp: fixed}
|
||||
if err := Record(ev); err != nil {
|
||||
t.Fatalf("Record: %v", err)
|
||||
}
|
||||
|
||||
events, err := ReadEvents(fixed.Add(-time.Hour))
|
||||
if err != nil {
|
||||
t.Fatalf("ReadEvents: %v", err)
|
||||
}
|
||||
if len(events) != 1 {
|
||||
t.Fatalf("expected 1 event, got %d", len(events))
|
||||
}
|
||||
if !events[0].Timestamp.Equal(fixed) {
|
||||
t.Errorf("expected timestamp %v, got %v", fixed, events[0].Timestamp)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRecord_Good_AutoTimestamp(t *testing.T) {
|
||||
withTempHome(t)
|
||||
func TestMetrics_ReadEvents_Good(t *testing.T) {
|
||||
t.Run("Empty", func(t *testing.T) {
|
||||
withTempHome(t)
|
||||
|
||||
before := time.Now()
|
||||
ev := Event{Type: "auto_ts"}
|
||||
if err := Record(ev); err != nil {
|
||||
t.Fatalf("Record: %v", err)
|
||||
}
|
||||
after := time.Now()
|
||||
events, err := ReadEvents(time.Now().Add(-24 * time.Hour))
|
||||
if err != nil {
|
||||
t.Fatalf("ReadEvents: %v", err)
|
||||
}
|
||||
if len(events) != 0 {
|
||||
t.Errorf("expected 0 events, got %d", len(events))
|
||||
}
|
||||
})
|
||||
|
||||
events, err := ReadEvents(before)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadEvents: %v", err)
|
||||
}
|
||||
if len(events) != 1 {
|
||||
t.Fatalf("expected 1 event, got %d", len(events))
|
||||
}
|
||||
ts := events[0].Timestamp
|
||||
if ts.Before(before) || ts.After(after) {
|
||||
t.Errorf("timestamp %v not in range [%v, %v]", ts, before, after)
|
||||
}
|
||||
t.Run("MultiDay", func(t *testing.T) {
|
||||
withTempHome(t)
|
||||
|
||||
now := time.Now()
|
||||
yesterday := now.AddDate(0, 0, -1)
|
||||
|
||||
if err := Record(Event{Type: "yesterday", Timestamp: yesterday}); err != nil {
|
||||
t.Fatalf("Record yesterday: %v", err)
|
||||
}
|
||||
if err := Record(Event{Type: "today", Timestamp: now}); err != nil {
|
||||
t.Fatalf("Record today: %v", err)
|
||||
}
|
||||
|
||||
events, err := ReadEvents(now.AddDate(0, 0, -2))
|
||||
if err != nil {
|
||||
t.Fatalf("ReadEvents: %v", err)
|
||||
}
|
||||
if len(events) != 2 {
|
||||
t.Fatalf("expected 2 events, got %d", len(events))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("FiltersBySince", func(t *testing.T) {
|
||||
withTempHome(t)
|
||||
|
||||
now := time.Now()
|
||||
if err := Record(Event{Type: "old", Timestamp: now.Add(-2 * time.Hour)}); err != nil {
|
||||
t.Fatalf("Record old: %v", err)
|
||||
}
|
||||
if err := Record(Event{Type: "recent", Timestamp: now}); err != nil {
|
||||
t.Fatalf("Record recent: %v", err)
|
||||
}
|
||||
|
||||
events, err := ReadEvents(now.Add(-time.Hour))
|
||||
if err != nil {
|
||||
t.Fatalf("ReadEvents: %v", err)
|
||||
}
|
||||
if len(events) != 1 {
|
||||
t.Fatalf("expected 1 event, got %d", len(events))
|
||||
}
|
||||
if events[0].Type != "recent" {
|
||||
t.Errorf("expected type recent, got %s", events[0].Type)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRecord_Good_PresetTimestamp(t *testing.T) {
|
||||
withTempHome(t)
|
||||
func TestMetrics_ReadMetricsFile_Good(t *testing.T) {
|
||||
t.Run("MalformedLines", func(t *testing.T) {
|
||||
withTempHome(t)
|
||||
|
||||
fixed := time.Date(2026, 3, 15, 10, 0, 0, 0, time.UTC)
|
||||
ev := Event{Type: "preset_ts", Timestamp: fixed}
|
||||
if err := Record(ev); err != nil {
|
||||
t.Fatalf("Record: %v", err)
|
||||
}
|
||||
dir, err := metricsDir()
|
||||
if err != nil {
|
||||
t.Fatalf("metricsDir: %v", err)
|
||||
}
|
||||
|
||||
events, err := ReadEvents(fixed.Add(-time.Hour))
|
||||
if err != nil {
|
||||
t.Fatalf("ReadEvents: %v", err)
|
||||
}
|
||||
if len(events) != 1 {
|
||||
t.Fatalf("expected 1 event, got %d", len(events))
|
||||
}
|
||||
if !events[0].Timestamp.Equal(fixed) {
|
||||
t.Errorf("expected timestamp %v, got %v", fixed, events[0].Timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
// --- ReadEvents ---
|
||||
|
||||
func TestReadEvents_Good_Empty(t *testing.T) {
|
||||
withTempHome(t)
|
||||
|
||||
events, err := ReadEvents(time.Now().Add(-24 * time.Hour))
|
||||
if err != nil {
|
||||
t.Fatalf("ReadEvents: %v", err)
|
||||
}
|
||||
if len(events) != 0 {
|
||||
t.Errorf("expected 0 events, got %d", len(events))
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadEvents_Good_MultiDay(t *testing.T) {
|
||||
withTempHome(t)
|
||||
|
||||
now := time.Now()
|
||||
yesterday := now.AddDate(0, 0, -1)
|
||||
|
||||
// Write event for yesterday
|
||||
ev1 := Event{Type: "yesterday", Timestamp: yesterday}
|
||||
if err := Record(ev1); err != nil {
|
||||
t.Fatalf("Record yesterday: %v", err)
|
||||
}
|
||||
|
||||
// Write event for today
|
||||
ev2 := Event{Type: "today", Timestamp: now}
|
||||
if err := Record(ev2); err != nil {
|
||||
t.Fatalf("Record today: %v", err)
|
||||
}
|
||||
|
||||
// Read from 2 days ago — should get both
|
||||
events, err := ReadEvents(now.AddDate(0, 0, -2))
|
||||
if err != nil {
|
||||
t.Fatalf("ReadEvents: %v", err)
|
||||
}
|
||||
if len(events) != 2 {
|
||||
t.Fatalf("expected 2 events, got %d", len(events))
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadEvents_Good_FiltersBySince(t *testing.T) {
|
||||
withTempHome(t)
|
||||
|
||||
now := time.Now()
|
||||
// Write an old event and a recent one
|
||||
old := Event{Type: "old", Timestamp: now.Add(-2 * time.Hour)}
|
||||
if err := Record(old); err != nil {
|
||||
t.Fatalf("Record old: %v", err)
|
||||
}
|
||||
recent := Event{Type: "recent", Timestamp: now}
|
||||
if err := Record(recent); err != nil {
|
||||
t.Fatalf("Record recent: %v", err)
|
||||
}
|
||||
|
||||
// Read only events from the last hour
|
||||
events, err := ReadEvents(now.Add(-time.Hour))
|
||||
if err != nil {
|
||||
t.Fatalf("ReadEvents: %v", err)
|
||||
}
|
||||
if len(events) != 1 {
|
||||
t.Fatalf("expected 1 event, got %d", len(events))
|
||||
}
|
||||
if events[0].Type != "recent" {
|
||||
t.Errorf("expected type recent, got %s", events[0].Type)
|
||||
}
|
||||
}
|
||||
|
||||
// --- readMetricsFile ---
|
||||
|
||||
func TestReadMetricsFile_Good_MalformedLines(t *testing.T) {
|
||||
withTempHome(t)
|
||||
|
||||
dir, err := metricsDir()
|
||||
if err != nil {
|
||||
t.Fatalf("metricsDir: %v", err)
|
||||
}
|
||||
|
||||
// Write a file with a mix of valid and invalid lines
|
||||
path := metricsFilePath(dir, time.Now())
|
||||
content := `{"type":"valid","timestamp":"2026-03-15T10:00:00Z"}
|
||||
path := metricsFilePath(dir, time.Now())
|
||||
content := `{"type":"valid","timestamp":"2026-03-15T10:00:00Z"}
|
||||
not-json
|
||||
{"type":"also_valid","timestamp":"2026-03-15T11:00:00Z"}
|
||||
{broken
|
||||
`
|
||||
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
|
||||
t.Fatalf("write test file: %v", err)
|
||||
}
|
||||
if err := coreio.Write(coreio.Local, path, content); err != nil {
|
||||
t.Fatalf("write test file: %v", err)
|
||||
}
|
||||
|
||||
events, err := readMetricsFile(path, time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC))
|
||||
if err != nil {
|
||||
t.Fatalf("readMetricsFile: %v", err)
|
||||
}
|
||||
if len(events) != 2 {
|
||||
t.Errorf("expected 2 valid events (skipping malformed), got %d", len(events))
|
||||
}
|
||||
events, err := readMetricsFile(path, time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC))
|
||||
if err != nil {
|
||||
t.Fatalf("readMetricsFile: %v", err)
|
||||
}
|
||||
if len(events) != 2 {
|
||||
t.Errorf("expected 2 valid events (skipping malformed), got %d", len(events))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("NonExistent", func(t *testing.T) {
|
||||
events, err := readMetricsFile(core.Path(t.TempDir(), "nonexistent-metrics-file.jsonl"), time.Time{})
|
||||
if err != nil {
|
||||
t.Fatalf("expected nil error for missing file, got: %v", err)
|
||||
}
|
||||
if len(events) != 0 {
|
||||
t.Errorf("expected 0 events, got %d", len(events))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestReadMetricsFile_Good_NonExistent(t *testing.T) {
|
||||
events, err := readMetricsFile("/tmp/nonexistent-metrics-file.jsonl", time.Time{})
|
||||
if err != nil {
|
||||
t.Fatalf("expected nil error for missing file, got: %v", err)
|
||||
}
|
||||
if len(events) != 0 {
|
||||
t.Errorf("expected 0 events, got %d", len(events))
|
||||
}
|
||||
}
|
||||
|
||||
// --- metricsFilePath ---
|
||||
|
||||
func TestMetricsFilePath_Good(t *testing.T) {
|
||||
func TestMetrics_MetricsFilePath_Good(t *testing.T) {
|
||||
ts := time.Date(2026, 3, 17, 14, 30, 0, 0, time.UTC)
|
||||
got := metricsFilePath("/base", ts)
|
||||
want := "/base/2026-03-17.jsonl"
|
||||
|
|
@ -216,63 +201,78 @@ func TestMetricsFilePath_Good(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// --- Summary ---
|
||||
func TestMetrics_Summary_Good(t *testing.T) {
|
||||
t.Run("Empty", func(t *testing.T) {
|
||||
s := Summary(nil)
|
||||
total, ok := s["total"].(int)
|
||||
if !ok || total != 0 {
|
||||
t.Errorf("expected total 0, got %v", s["total"])
|
||||
}
|
||||
})
|
||||
|
||||
func TestSummary_Good_Empty(t *testing.T) {
|
||||
s := Summary(nil)
|
||||
total, ok := s["total"].(int)
|
||||
if !ok || total != 0 {
|
||||
t.Errorf("expected total 0, got %v", s["total"])
|
||||
}
|
||||
t.Run("Aggregates", func(t *testing.T) {
|
||||
events := []Event{
|
||||
{Type: "build", Repo: "core-php", AgentID: "agent-1"},
|
||||
{Type: "build", Repo: "core-php", AgentID: "agent-2"},
|
||||
{Type: "test", Repo: "core-api", AgentID: "agent-1"},
|
||||
}
|
||||
|
||||
s := Summary(events)
|
||||
|
||||
total, _ := s["total"].(int)
|
||||
if total != 3 {
|
||||
t.Errorf("expected total 3, got %d", total)
|
||||
}
|
||||
|
||||
byType, _ := s["by_type"].([]map[string]any)
|
||||
if len(byType) != 2 {
|
||||
t.Fatalf("expected 2 types, got %d", len(byType))
|
||||
}
|
||||
if byType[0]["key"] != "build" || byType[0]["count"] != 2 {
|
||||
t.Errorf("expected build:2 first, got %v:%v", byType[0]["key"], byType[0]["count"])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSummary_Good(t *testing.T) {
|
||||
events := []Event{
|
||||
{Type: "build", Repo: "core-php", AgentID: "agent-1"},
|
||||
{Type: "build", Repo: "core-php", AgentID: "agent-2"},
|
||||
{Type: "test", Repo: "core-api", AgentID: "agent-1"},
|
||||
}
|
||||
func TestMetrics_SortedMap_Good(t *testing.T) {
|
||||
t.Run("Empty", func(t *testing.T) {
|
||||
result := sortedMap(map[string]int{})
|
||||
if len(result) != 0 {
|
||||
t.Errorf("expected empty slice, got %d entries", len(result))
|
||||
}
|
||||
})
|
||||
|
||||
s := Summary(events)
|
||||
t.Run("Ordering", func(t *testing.T) {
|
||||
m := map[string]int{"a": 1, "b": 3, "c": 2}
|
||||
result := sortedMap(m)
|
||||
if len(result) != 3 {
|
||||
t.Fatalf("expected 3 entries, got %d", len(result))
|
||||
}
|
||||
if result[0]["key"] != "b" {
|
||||
t.Errorf("expected first key 'b', got %v", result[0]["key"])
|
||||
}
|
||||
if result[1]["key"] != "c" {
|
||||
t.Errorf("expected second key 'c', got %v", result[1]["key"])
|
||||
}
|
||||
if result[2]["key"] != "a" {
|
||||
t.Errorf("expected third key 'a', got %v", result[2]["key"])
|
||||
}
|
||||
})
|
||||
|
||||
total, _ := s["total"].(int)
|
||||
if total != 3 {
|
||||
t.Errorf("expected total 3, got %d", total)
|
||||
}
|
||||
|
||||
byType, _ := s["by_type"].([]map[string]any)
|
||||
if len(byType) != 2 {
|
||||
t.Fatalf("expected 2 types, got %d", len(byType))
|
||||
}
|
||||
// Sorted by count descending — "build" (2) first
|
||||
if byType[0]["key"] != "build" || byType[0]["count"] != 2 {
|
||||
t.Errorf("expected build:2 first, got %v:%v", byType[0]["key"], byType[0]["count"])
|
||||
}
|
||||
}
|
||||
|
||||
// --- sortedMap ---
|
||||
|
||||
func TestSortedMap_Good_Empty(t *testing.T) {
|
||||
result := sortedMap(map[string]int{})
|
||||
if len(result) != 0 {
|
||||
t.Errorf("expected empty slice, got %d entries", len(result))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSortedMap_Good_Ordering(t *testing.T) {
|
||||
m := map[string]int{"a": 1, "b": 3, "c": 2}
|
||||
result := sortedMap(m)
|
||||
if len(result) != 3 {
|
||||
t.Fatalf("expected 3 entries, got %d", len(result))
|
||||
}
|
||||
// Descending by count
|
||||
if result[0]["key"] != "b" {
|
||||
t.Errorf("expected first key 'b', got %v", result[0]["key"])
|
||||
}
|
||||
if result[1]["key"] != "c" {
|
||||
t.Errorf("expected second key 'c', got %v", result[1]["key"])
|
||||
}
|
||||
if result[2]["key"] != "a" {
|
||||
t.Errorf("expected third key 'a', got %v", result[2]["key"])
|
||||
}
|
||||
t.Run("TieBreaksByKey", func(t *testing.T) {
|
||||
m := map[string]int{"b": 1, "c": 1, "a": 1}
|
||||
result := sortedMap(m)
|
||||
if len(result) != 3 {
|
||||
t.Fatalf("expected 3 entries, got %d", len(result))
|
||||
}
|
||||
if result[0]["key"] != "a" {
|
||||
t.Errorf("expected first key 'a', got %v", result[0]["key"])
|
||||
}
|
||||
if result[1]["key"] != "b" {
|
||||
t.Errorf("expected second key 'b', got %v", result[1]["key"])
|
||||
}
|
||||
if result[2]["key"] != "c" {
|
||||
t.Errorf("expected third key 'c', got %v", result[2]["key"])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
18
ai/rag.go
18
ai/rag.go
|
|
@ -4,12 +4,16 @@ import (
|
|||
"context"
|
||||
"time"
|
||||
|
||||
coreerr "dappco.re/go/core/log"
|
||||
"dappco.re/go/core"
|
||||
"forge.lthn.ai/core/go-rag"
|
||||
)
|
||||
|
||||
// TaskInfo carries the minimal task data needed for RAG queries,
|
||||
// avoiding a direct dependency on pkg/agentic (which imports pkg/ai).
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// task := TaskInfo{Title: "Fix metrics", Description: "Need storage docs"}
|
||||
type TaskInfo struct {
|
||||
Title string
|
||||
Description string
|
||||
|
|
@ -18,8 +22,12 @@ type TaskInfo struct {
|
|||
// QueryRAGForTask queries Qdrant for documentation relevant to a task.
|
||||
// It builds a query from the task title and description, queries with
|
||||
// sensible defaults, and returns formatted context.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// context, err := QueryRAGForTask(TaskInfo{Title: "Deploy", Description: "Need release steps"})
|
||||
func QueryRAGForTask(task TaskInfo) (string, error) {
|
||||
query := task.Title + " " + task.Description
|
||||
query := core.Concat(task.Title, " ", task.Description)
|
||||
|
||||
// Truncate to 500 runes to keep the embedding focused.
|
||||
runes := []rune(query)
|
||||
|
|
@ -30,14 +38,14 @@ func QueryRAGForTask(task TaskInfo) (string, error) {
|
|||
qdrantCfg := rag.DefaultQdrantConfig()
|
||||
qdrantClient, err := rag.NewQdrantClient(qdrantCfg)
|
||||
if err != nil {
|
||||
return "", coreerr.E("ai.QueryRAGForTask", "rag qdrant client", err)
|
||||
return "", core.E("ai.QueryRAGForTask", "rag qdrant client", err)
|
||||
}
|
||||
defer func() { _ = qdrantClient.Close() }()
|
||||
|
||||
ollamaCfg := rag.DefaultOllamaConfig()
|
||||
ollamaClient, err := rag.NewOllamaClient(ollamaCfg)
|
||||
if err != nil {
|
||||
return "", coreerr.E("ai.QueryRAGForTask", "rag ollama client", err)
|
||||
return "", core.E("ai.QueryRAGForTask", "rag ollama client", err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
|
|
@ -51,7 +59,7 @@ func QueryRAGForTask(task TaskInfo) (string, error) {
|
|||
|
||||
results, err := rag.Query(ctx, qdrantClient, ollamaClient, query, queryCfg)
|
||||
if err != nil {
|
||||
return "", coreerr.E("ai.QueryRAGForTask", "rag query", err)
|
||||
return "", core.E("ai.QueryRAGForTask", "rag query", err)
|
||||
}
|
||||
|
||||
return rag.FormatResultsContext(results), nil
|
||||
|
|
|
|||
|
|
@ -10,19 +10,14 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
coreerr "dappco.re/go/core/log"
|
||||
"dappco.re/go/core"
|
||||
)
|
||||
|
||||
var ollamaURL = flag.String("ollama", "http://localhost:11434", "Ollama base URL")
|
||||
|
|
@ -91,16 +86,16 @@ var queries = []struct {
|
|||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
fmt.Println("OpenBrain Embedding Model Benchmark")
|
||||
fmt.Println(strings.Repeat("=", 60))
|
||||
core.Println("OpenBrain Embedding Model Benchmark")
|
||||
core.Println(repeat("=", 60))
|
||||
|
||||
for _, model := range models {
|
||||
fmt.Printf("\n## Model: %s\n", model)
|
||||
fmt.Println(strings.Repeat("-", 40))
|
||||
core.Println(core.Sprintf("\n## Model: %s", model))
|
||||
core.Println(repeat("-", 40))
|
||||
|
||||
// Check model is available
|
||||
if !modelAvailable(model) {
|
||||
fmt.Printf(" SKIPPED — model not pulled (run: ollama pull %s)\n", model)
|
||||
core.Println(core.Sprintf(" SKIPPED - model not pulled (run: ollama pull %s)", model))
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
@ -114,20 +109,20 @@ func main() {
|
|||
}
|
||||
}
|
||||
|
||||
fmt.Printf(" Embedding %d memories...\n", len(allMemories))
|
||||
core.Println(core.Sprintf(" Embedding %d memories...", len(allMemories)))
|
||||
start := time.Now()
|
||||
memVectors := make([][]float64, len(allMemories))
|
||||
for i, mem := range allMemories {
|
||||
vec, err := embed(model, mem)
|
||||
if err != nil {
|
||||
fmt.Printf(" ERROR embedding memory %d: %v\n", i, err)
|
||||
core.Println(core.Sprintf(" ERROR embedding memory %d: %v", i, err))
|
||||
break
|
||||
}
|
||||
memVectors[i] = vec
|
||||
}
|
||||
embedTime := time.Since(start)
|
||||
fmt.Printf(" Embedded in %v (%.0fms/memory)\n", embedTime, float64(embedTime.Milliseconds())/float64(len(allMemories)))
|
||||
fmt.Printf(" Vector dimension: %d\n", len(memVectors[0]))
|
||||
core.Println(core.Sprintf(" Embedded in %v (%.0fms/memory)", embedTime, float64(embedTime.Milliseconds())/float64(len(allMemories))))
|
||||
core.Println(core.Sprintf(" Vector dimension: %d", len(memVectors[0])))
|
||||
|
||||
// 2. Intra-group vs inter-group similarity
|
||||
var intraSims, interSims []float64
|
||||
|
|
@ -146,18 +141,18 @@ func main() {
|
|||
interAvg := avg(interSims)
|
||||
separation := intraAvg - interAvg
|
||||
|
||||
fmt.Printf("\n Cluster separation:\n")
|
||||
fmt.Printf(" Intra-group similarity (same topic): %.4f\n", intraAvg)
|
||||
fmt.Printf(" Inter-group similarity (diff topic): %.4f\n", interAvg)
|
||||
fmt.Printf(" Separation gap: %.4f %s\n", separation, qualityLabel(separation))
|
||||
core.Println("\n Cluster separation:")
|
||||
core.Println(core.Sprintf(" Intra-group similarity (same topic): %.4f", intraAvg))
|
||||
core.Println(core.Sprintf(" Inter-group similarity (diff topic): %.4f", interAvg))
|
||||
core.Println(core.Sprintf(" Separation gap: %.4f %s", separation, qualityLabel(separation)))
|
||||
|
||||
// 3. Query recall accuracy
|
||||
fmt.Printf("\n Query recall (top-1 accuracy):\n")
|
||||
core.Println("\n Query recall (top-1 accuracy):")
|
||||
correct := 0
|
||||
for _, q := range queries {
|
||||
qVec, err := embed(model, q.query)
|
||||
if err != nil {
|
||||
fmt.Printf(" ERROR: %v\n", err)
|
||||
core.Println(core.Sprintf(" ERROR: %v", err))
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
@ -181,11 +176,11 @@ func main() {
|
|||
if !hit {
|
||||
marker = "✗"
|
||||
}
|
||||
fmt.Printf(" %s %.4f %q → %s (want: %s)\n", marker, bestSim, truncate(q.query, 40), matchTopic, q.targetTopic)
|
||||
core.Println(core.Sprintf(" %s %.4f %q -> %s (want: %s)", marker, bestSim, truncate(q.query, 40), matchTopic, q.targetTopic))
|
||||
}
|
||||
|
||||
accuracy := float64(correct) / float64(len(queries)) * 100
|
||||
fmt.Printf("\n Top-1 accuracy: %.0f%% (%d/%d)\n", accuracy, correct, len(queries))
|
||||
core.Println(core.Sprintf("\n Top-1 accuracy: %.0f%% (%d/%d)", accuracy, correct, len(queries)))
|
||||
|
||||
// 4. Top-3 recall
|
||||
correct3 := 0
|
||||
|
|
@ -210,11 +205,11 @@ func main() {
|
|||
}
|
||||
}
|
||||
accuracy3 := float64(correct3) / float64(len(queries)) * 100
|
||||
fmt.Printf(" Top-3 accuracy: %.0f%% (%d/%d)\n", accuracy3, correct3, len(queries))
|
||||
core.Println(core.Sprintf(" Top-3 accuracy: %.0f%% (%d/%d)", accuracy3, correct3, len(queries)))
|
||||
}
|
||||
|
||||
fmt.Println("\n" + strings.Repeat("=", 60))
|
||||
fmt.Println("Done.")
|
||||
core.Println(core.Concat("\n", repeat("=", 60)))
|
||||
core.Println("Done.")
|
||||
}
|
||||
|
||||
// -- Ollama helpers --
|
||||
|
|
@ -236,40 +231,57 @@ type embedResponse struct {
|
|||
}
|
||||
|
||||
func embed(model, text string) ([]float64, error) {
|
||||
body, _ := json.Marshal(embedRequest{Model: model, Prompt: text})
|
||||
resp, err := httpClient.Post(*ollamaURL+"/api/embeddings", "application/json", bytes.NewReader(body))
|
||||
body := core.JSONMarshal(embedRequest{Model: model, Prompt: text})
|
||||
if !body.OK {
|
||||
return nil, core.E("embed", "marshal request", body.Value.(error))
|
||||
}
|
||||
|
||||
resp, err := httpClient.Post(core.Concat(*ollamaURL, "/api/embeddings"), "application/json", core.NewReader(string(body.Value.([]byte))))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, coreerr.E("embed", fmt.Sprintf("HTTP %d", resp.StatusCode), nil)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
core.CloseStream(resp.Body)
|
||||
return nil, core.E("embed", core.Sprintf("HTTP %d", resp.StatusCode), nil)
|
||||
}
|
||||
|
||||
payload := core.ReadAll(resp.Body)
|
||||
if !payload.OK {
|
||||
return nil, core.E("embed", "read response body", payload.Value.(error))
|
||||
}
|
||||
|
||||
var result embedResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return nil, err
|
||||
if decoded := core.JSONUnmarshalString(payload.Value.(string), &result); !decoded.OK {
|
||||
return nil, core.E("embed", "decode response", decoded.Value.(error))
|
||||
}
|
||||
if len(result.Embedding) == 0 {
|
||||
return nil, coreerr.E("embed", "empty embedding", nil)
|
||||
return nil, core.E("embed", "empty embedding", nil)
|
||||
}
|
||||
return result.Embedding, nil
|
||||
}
|
||||
|
||||
func modelAvailable(model string) bool {
|
||||
resp, err := httpClient.Get(*ollamaURL + "/api/tags")
|
||||
resp, err := httpClient.Get(core.Concat(*ollamaURL, "/api/tags"))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
payload := core.ReadAll(resp.Body)
|
||||
if !payload.OK {
|
||||
return false
|
||||
}
|
||||
|
||||
var result struct {
|
||||
Models []struct {
|
||||
Name string `json:"name"`
|
||||
} `json:"models"`
|
||||
}
|
||||
json.NewDecoder(resp.Body).Decode(&result)
|
||||
if decoded := core.JSONUnmarshalString(payload.Value.(string), &result); !decoded.OK {
|
||||
return false
|
||||
}
|
||||
for _, m := range result.Models {
|
||||
// Match "nomic-embed-text:latest" against "nomic-embed-text"
|
||||
if m.Name == model || strings.HasPrefix(m.Name, model+":") {
|
||||
if m.Name == model || core.HasPrefix(m.Name, core.Concat(model, ":")) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
@ -316,14 +328,17 @@ func qualityLabel(gap float64) string {
|
|||
}
|
||||
}
|
||||
|
||||
func repeat(s string, n int) string {
|
||||
b := core.NewBuilder()
|
||||
for range n {
|
||||
b.WriteString(s)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func truncate(s string, n int) string {
|
||||
if len(s) <= n {
|
||||
return s
|
||||
}
|
||||
return s[:n-3] + "..."
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Ensure stderr doesn't buffer
|
||||
os.Stderr.Sync()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,10 +5,11 @@ package lab
|
|||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"forge.lthn.ai/core/cli/pkg/cli"
|
||||
|
|
@ -43,6 +44,10 @@ func init() {
|
|||
}
|
||||
|
||||
// AddLabCommands registers the 'lab' command and subcommands.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// AddLabCommands(root)
|
||||
func AddLabCommands(root *cli.Command) {
|
||||
labCmd.AddCommand(serveCmd)
|
||||
root.AddCommand(labCmd)
|
||||
|
|
@ -53,7 +58,7 @@ func runServe(cmd *cli.Command, args []string) error {
|
|||
cfg.Addr = labBind
|
||||
|
||||
store := lab.NewStore()
|
||||
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
|
||||
logger := slog.New(slog.NewJSONHandler(io.Discard, nil))
|
||||
|
||||
// Setup collectors.
|
||||
reg := collector.NewRegistry(logger)
|
||||
|
|
@ -79,7 +84,7 @@ func runServe(cmd *cli.Command, args []string) error {
|
|||
time.Duration(cfg.InfluxInterval)*time.Second)
|
||||
}
|
||||
|
||||
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT)
|
||||
defer cancel()
|
||||
reg.Start(ctx)
|
||||
defer reg.Stop()
|
||||
|
|
|
|||
|
|
@ -2,14 +2,13 @@
|
|||
package metrics
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"forge.lthn.ai/core/cli/pkg/cli"
|
||||
"dappco.re/go/core"
|
||||
"dappco.re/go/core/ai/ai"
|
||||
"dappco.re/go/core/i18n"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
"forge.lthn.ai/core/cli/pkg/cli"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
@ -32,6 +31,10 @@ func initMetricsFlags() {
|
|||
}
|
||||
|
||||
// AddMetricsCommand adds the 'metrics' command to the parent.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// AddMetricsCommand(root)
|
||||
func AddMetricsCommand(parent *cli.Command) {
|
||||
initMetricsFlags()
|
||||
parent.AddCommand(metricsCmd)
|
||||
|
|
@ -51,11 +54,11 @@ func runMetrics() error {
|
|||
|
||||
if metricsJSON {
|
||||
summary := ai.Summary(events)
|
||||
output, err := json.MarshalIndent(summary, "", " ")
|
||||
if err != nil {
|
||||
return cli.Wrap(err, "marshal JSON output")
|
||||
output := core.JSONMarshal(summary)
|
||||
if !output.OK {
|
||||
return cli.Wrap(output.Value.(error), "marshal JSON output")
|
||||
}
|
||||
cli.Text(string(output))
|
||||
cli.Text(string(output.Value.([]byte)))
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -104,19 +107,19 @@ func runMetrics() error {
|
|||
// parseDuration parses a human-friendly duration like "7d", "24h", "30d".
|
||||
func parseDuration(s string) (time.Duration, error) {
|
||||
if len(s) < 2 {
|
||||
return 0, coreerr.E("metrics.parseDuration", fmt.Sprintf("invalid duration: %s", s), nil)
|
||||
return 0, core.E("metrics.parseDuration", core.Concat("invalid duration: ", s), nil)
|
||||
}
|
||||
|
||||
unit := s[len(s)-1]
|
||||
value := s[:len(s)-1]
|
||||
|
||||
var n int
|
||||
if _, err := fmt.Sscanf(value, "%d", &n); err != nil {
|
||||
return 0, coreerr.E("metrics.parseDuration", fmt.Sprintf("invalid duration: %s", s), nil)
|
||||
n, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
return 0, core.E("metrics.parseDuration", core.Concat("invalid duration: ", s), err)
|
||||
}
|
||||
|
||||
if n <= 0 {
|
||||
return 0, coreerr.E("metrics.parseDuration", fmt.Sprintf("duration must be positive: %s", s), nil)
|
||||
return 0, core.E("metrics.parseDuration", core.Concat("duration must be positive: ", s), nil)
|
||||
}
|
||||
|
||||
switch unit {
|
||||
|
|
@ -127,6 +130,6 @@ func parseDuration(s string) (time.Duration, error) {
|
|||
case 'm':
|
||||
return time.Duration(n) * time.Minute, nil
|
||||
default:
|
||||
return 0, coreerr.E("metrics.parseDuration", fmt.Sprintf("unknown unit %c in duration: %s", unit, s), nil)
|
||||
return 0, core.E("metrics.parseDuration", core.Concat("unknown unit ", string(unit), " in duration: ", s), nil)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
func TestParseDuration_Good(t *testing.T) {
|
||||
func TestCmd_ParseDuration_Good(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
want time.Duration
|
||||
|
|
@ -29,7 +29,7 @@ func TestParseDuration_Good(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestParseDuration_Bad(t *testing.T) {
|
||||
func TestCmd_ParseDuration_Bad(t *testing.T) {
|
||||
bad := []string{
|
||||
"", // too short
|
||||
"d", // too short
|
||||
|
|
|
|||
|
|
@ -4,4 +4,8 @@ package rag
|
|||
import ragcmd "forge.lthn.ai/core/go-rag/cmd/rag"
|
||||
|
||||
// AddRAGSubcommands registers RAG commands as subcommands of parent.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// AddRAGSubcommands(root)
|
||||
var AddRAGSubcommands = ragcmd.AddRAGSubcommands
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
package security
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"forge.lthn.ai/core/cli/pkg/cli"
|
||||
"dappco.re/go/core"
|
||||
"dappco.re/go/core/i18n"
|
||||
"forge.lthn.ai/core/cli/pkg/cli"
|
||||
)
|
||||
|
||||
func addAlertsCommand(parent *cli.Command) {
|
||||
|
|
@ -28,6 +26,10 @@ func addAlertsCommand(parent *cli.Command) {
|
|||
}
|
||||
|
||||
// AlertOutput represents a unified alert for output.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// var alert AlertOutput
|
||||
type AlertOutput struct {
|
||||
Repo string `json:"repo"`
|
||||
Severity string `json:"severity"`
|
||||
|
|
@ -63,11 +65,15 @@ func runAlerts() error {
|
|||
summary := &AlertSummary{}
|
||||
|
||||
for _, repo := range repoList {
|
||||
repoFullName := fmt.Sprintf("%s/%s", reg.Org, repo.Name)
|
||||
repoFullName := core.Concat(reg.Org, "/", repo.Name)
|
||||
fetchErrors := 0
|
||||
|
||||
// Fetch Dependabot alerts
|
||||
depAlerts, err := fetchDependabotAlerts(repoFullName)
|
||||
if err == nil {
|
||||
if err != nil {
|
||||
cli.Print("%s %s: %v\n", cli.WarningStyle.Render(">>"), repoFullName, err)
|
||||
fetchErrors++
|
||||
} else {
|
||||
for _, alert := range depAlerts {
|
||||
if alert.State != "open" {
|
||||
continue
|
||||
|
|
@ -91,7 +97,10 @@ func runAlerts() error {
|
|||
|
||||
// Fetch code scanning alerts
|
||||
codeAlerts, err := fetchCodeScanningAlerts(repoFullName)
|
||||
if err == nil {
|
||||
if err != nil {
|
||||
cli.Print("%s %s: %v\n", cli.WarningStyle.Render(">>"), repoFullName, err)
|
||||
fetchErrors++
|
||||
} else {
|
||||
for _, alert := range codeAlerts {
|
||||
if alert.State != "open" {
|
||||
continue
|
||||
|
|
@ -101,7 +110,7 @@ func runAlerts() error {
|
|||
continue
|
||||
}
|
||||
summary.Add(severity)
|
||||
location := fmt.Sprintf("%s:%d", alert.MostRecentInstance.Location.Path, alert.MostRecentInstance.Location.StartLine)
|
||||
location := core.Sprintf("%s:%d", alert.MostRecentInstance.Location.Path, alert.MostRecentInstance.Location.StartLine)
|
||||
allAlerts = append(allAlerts, AlertOutput{
|
||||
Repo: repo.Name,
|
||||
Severity: severity,
|
||||
|
|
@ -115,7 +124,10 @@ func runAlerts() error {
|
|||
|
||||
// Fetch secret scanning alerts
|
||||
secretAlerts, err := fetchSecretScanningAlerts(repoFullName)
|
||||
if err == nil {
|
||||
if err != nil {
|
||||
cli.Print("%s %s: %v\n", cli.WarningStyle.Render(">>"), repoFullName, err)
|
||||
fetchErrors++
|
||||
} else {
|
||||
for _, alert := range secretAlerts {
|
||||
if alert.State != "open" {
|
||||
continue
|
||||
|
|
@ -127,21 +139,20 @@ func runAlerts() error {
|
|||
allAlerts = append(allAlerts, AlertOutput{
|
||||
Repo: repo.Name,
|
||||
Severity: "high",
|
||||
ID: fmt.Sprintf("secret-%d", alert.Number),
|
||||
ID: core.Sprintf("secret-%d", alert.Number),
|
||||
Type: "secret-scanning",
|
||||
Message: alert.SecretType,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if fetchErrors == 3 {
|
||||
cli.Print("%s %s: all alert sources failed\n", cli.WarningStyle.Render(">>"), repoFullName)
|
||||
}
|
||||
}
|
||||
|
||||
if securityJSON {
|
||||
output, err := json.MarshalIndent(allAlerts, "", " ")
|
||||
if err != nil {
|
||||
return cli.Wrap(err, "marshal JSON output")
|
||||
}
|
||||
cli.Text(string(output))
|
||||
return nil
|
||||
return printJSON(allAlerts)
|
||||
}
|
||||
|
||||
// Print summary
|
||||
|
|
@ -163,12 +174,12 @@ func runAlerts() error {
|
|||
location = alert.Location
|
||||
}
|
||||
if alert.Version != "" {
|
||||
location = fmt.Sprintf("%s %s", location, cli.DimStyle.Render(alert.Version))
|
||||
location = core.Sprintf("%s %s", location, cli.DimStyle.Render(alert.Version))
|
||||
}
|
||||
|
||||
cli.Print("%-20s %s %-16s %-40s %s\n",
|
||||
cli.ValueStyle.Render(alert.Repo),
|
||||
sevStyle.Render(fmt.Sprintf("%-8s", alert.Severity)),
|
||||
sevStyle.Render(core.Sprintf("%-8s", alert.Severity)),
|
||||
alert.ID,
|
||||
location,
|
||||
cli.DimStyle.Render(alert.Type),
|
||||
|
|
@ -188,10 +199,14 @@ func runAlertsForTarget(target string) error {
|
|||
|
||||
var allAlerts []AlertOutput
|
||||
summary := &AlertSummary{}
|
||||
fetchErrors := 0
|
||||
|
||||
// Fetch Dependabot alerts
|
||||
depAlerts, err := fetchDependabotAlerts(fullName)
|
||||
if err == nil {
|
||||
if err != nil {
|
||||
cli.Print("%s %s: %v\n", cli.WarningStyle.Render(">>"), fullName, err)
|
||||
fetchErrors++
|
||||
} else {
|
||||
for _, alert := range depAlerts {
|
||||
if alert.State != "open" {
|
||||
continue
|
||||
|
|
@ -215,7 +230,10 @@ func runAlertsForTarget(target string) error {
|
|||
|
||||
// Fetch code scanning alerts
|
||||
codeAlerts, err := fetchCodeScanningAlerts(fullName)
|
||||
if err == nil {
|
||||
if err != nil {
|
||||
cli.Print("%s %s: %v\n", cli.WarningStyle.Render(">>"), fullName, err)
|
||||
fetchErrors++
|
||||
} else {
|
||||
for _, alert := range codeAlerts {
|
||||
if alert.State != "open" {
|
||||
continue
|
||||
|
|
@ -225,7 +243,7 @@ func runAlertsForTarget(target string) error {
|
|||
continue
|
||||
}
|
||||
summary.Add(severity)
|
||||
location := fmt.Sprintf("%s:%d", alert.MostRecentInstance.Location.Path, alert.MostRecentInstance.Location.StartLine)
|
||||
location := core.Sprintf("%s:%d", alert.MostRecentInstance.Location.Path, alert.MostRecentInstance.Location.StartLine)
|
||||
allAlerts = append(allAlerts, AlertOutput{
|
||||
Repo: repo.Name,
|
||||
Severity: severity,
|
||||
|
|
@ -239,7 +257,10 @@ func runAlertsForTarget(target string) error {
|
|||
|
||||
// Fetch secret scanning alerts
|
||||
secretAlerts, err := fetchSecretScanningAlerts(fullName)
|
||||
if err == nil {
|
||||
if err != nil {
|
||||
cli.Print("%s %s: %v\n", cli.WarningStyle.Render(">>"), fullName, err)
|
||||
fetchErrors++
|
||||
} else {
|
||||
for _, alert := range secretAlerts {
|
||||
if alert.State != "open" {
|
||||
continue
|
||||
|
|
@ -251,20 +272,19 @@ func runAlertsForTarget(target string) error {
|
|||
allAlerts = append(allAlerts, AlertOutput{
|
||||
Repo: repo.Name,
|
||||
Severity: "high",
|
||||
ID: fmt.Sprintf("secret-%d", alert.Number),
|
||||
ID: core.Sprintf("secret-%d", alert.Number),
|
||||
Type: "secret-scanning",
|
||||
Message: alert.SecretType,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if fetchErrors == 3 {
|
||||
return cli.Err("failed to fetch alerts for %s", fullName)
|
||||
}
|
||||
|
||||
if securityJSON {
|
||||
output, err := json.MarshalIndent(allAlerts, "", " ")
|
||||
if err != nil {
|
||||
return cli.Wrap(err, "marshal JSON output")
|
||||
}
|
||||
cli.Text(string(output))
|
||||
return nil
|
||||
return printJSON(allAlerts)
|
||||
}
|
||||
|
||||
cli.Blank()
|
||||
|
|
@ -282,11 +302,11 @@ func runAlertsForTarget(target string) error {
|
|||
location = alert.Location
|
||||
}
|
||||
if alert.Version != "" {
|
||||
location = fmt.Sprintf("%s %s", location, cli.DimStyle.Render(alert.Version))
|
||||
location = core.Sprintf("%s %s", location, cli.DimStyle.Render(alert.Version))
|
||||
}
|
||||
cli.Print("%-20s %s %-16s %-40s %s\n",
|
||||
cli.ValueStyle.Render(alert.Repo),
|
||||
sevStyle.Render(fmt.Sprintf("%-8s", alert.Severity)),
|
||||
sevStyle.Render(core.Sprintf("%-8s", alert.Severity)),
|
||||
alert.ID,
|
||||
location,
|
||||
cli.DimStyle.Render(alert.Type),
|
||||
|
|
@ -298,43 +318,43 @@ func runAlertsForTarget(target string) error {
|
|||
}
|
||||
|
||||
func fetchDependabotAlerts(repoFullName string) ([]DependabotAlert, error) {
|
||||
endpoint := fmt.Sprintf("repos/%s/dependabot/alerts?state=open", repoFullName)
|
||||
endpoint := core.Concat("repos/", repoFullName, "/dependabot/alerts?state=open")
|
||||
output, err := runGHAPI(endpoint)
|
||||
if err != nil {
|
||||
return nil, cli.Wrap(err, fmt.Sprintf("fetch dependabot alerts for %s", repoFullName))
|
||||
return nil, cli.Wrap(err, core.Sprintf("fetch dependabot alerts for %s", repoFullName))
|
||||
}
|
||||
|
||||
var alerts []DependabotAlert
|
||||
if err := json.Unmarshal(output, &alerts); err != nil {
|
||||
return nil, cli.Wrap(err, fmt.Sprintf("parse dependabot alerts for %s", repoFullName))
|
||||
if decoded := core.JSONUnmarshal(output, &alerts); !decoded.OK {
|
||||
return nil, cli.Wrap(decoded.Value.(error), core.Sprintf("parse dependabot alerts for %s", repoFullName))
|
||||
}
|
||||
return alerts, nil
|
||||
}
|
||||
|
||||
func fetchCodeScanningAlerts(repoFullName string) ([]CodeScanningAlert, error) {
|
||||
endpoint := fmt.Sprintf("repos/%s/code-scanning/alerts?state=open", repoFullName)
|
||||
endpoint := core.Concat("repos/", repoFullName, "/code-scanning/alerts?state=open")
|
||||
output, err := runGHAPI(endpoint)
|
||||
if err != nil {
|
||||
return nil, cli.Wrap(err, fmt.Sprintf("fetch code-scanning alerts for %s", repoFullName))
|
||||
return nil, cli.Wrap(err, core.Sprintf("fetch code-scanning alerts for %s", repoFullName))
|
||||
}
|
||||
|
||||
var alerts []CodeScanningAlert
|
||||
if err := json.Unmarshal(output, &alerts); err != nil {
|
||||
return nil, cli.Wrap(err, fmt.Sprintf("parse code-scanning alerts for %s", repoFullName))
|
||||
if decoded := core.JSONUnmarshal(output, &alerts); !decoded.OK {
|
||||
return nil, cli.Wrap(decoded.Value.(error), core.Sprintf("parse code-scanning alerts for %s", repoFullName))
|
||||
}
|
||||
return alerts, nil
|
||||
}
|
||||
|
||||
func fetchSecretScanningAlerts(repoFullName string) ([]SecretScanningAlert, error) {
|
||||
endpoint := fmt.Sprintf("repos/%s/secret-scanning/alerts?state=open", repoFullName)
|
||||
endpoint := core.Concat("repos/", repoFullName, "/secret-scanning/alerts?state=open")
|
||||
output, err := runGHAPI(endpoint)
|
||||
if err != nil {
|
||||
return nil, cli.Wrap(err, fmt.Sprintf("fetch secret-scanning alerts for %s", repoFullName))
|
||||
return nil, cli.Wrap(err, core.Sprintf("fetch secret-scanning alerts for %s", repoFullName))
|
||||
}
|
||||
|
||||
var alerts []SecretScanningAlert
|
||||
if err := json.Unmarshal(output, &alerts); err != nil {
|
||||
return nil, cli.Wrap(err, fmt.Sprintf("parse secret-scanning alerts for %s", repoFullName))
|
||||
if decoded := core.JSONUnmarshal(output, &alerts); !decoded.OK {
|
||||
return nil, cli.Wrap(decoded.Value.(error), core.Sprintf("parse secret-scanning alerts for %s", repoFullName))
|
||||
}
|
||||
return alerts, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
package security
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"forge.lthn.ai/core/cli/pkg/cli"
|
||||
"dappco.re/go/core"
|
||||
"dappco.re/go/core/i18n"
|
||||
"forge.lthn.ai/core/cli/pkg/cli"
|
||||
)
|
||||
|
||||
func addDepsCommand(parent *cli.Command) {
|
||||
|
|
@ -28,6 +26,10 @@ func addDepsCommand(parent *cli.Command) {
|
|||
}
|
||||
|
||||
// DepAlert represents a dependency vulnerability for output.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// var alert DepAlert
|
||||
type DepAlert struct {
|
||||
Repo string `json:"repo"`
|
||||
Severity string `json:"severity"`
|
||||
|
|
@ -64,7 +66,7 @@ func runDeps() error {
|
|||
summary := &AlertSummary{}
|
||||
|
||||
for _, repo := range repoList {
|
||||
repoFullName := fmt.Sprintf("%s/%s", reg.Org, repo.Name)
|
||||
repoFullName := core.Concat(reg.Org, "/", repo.Name)
|
||||
|
||||
alerts, err := fetchDependabotAlerts(repoFullName)
|
||||
if err != nil {
|
||||
|
|
@ -100,12 +102,7 @@ func runDeps() error {
|
|||
}
|
||||
|
||||
if securityJSON {
|
||||
output, err := json.MarshalIndent(allAlerts, "", " ")
|
||||
if err != nil {
|
||||
return cli.Wrap(err, "marshal JSON output")
|
||||
}
|
||||
cli.Text(string(output))
|
||||
return nil
|
||||
return printJSON(allAlerts)
|
||||
}
|
||||
|
||||
// Print summary
|
||||
|
|
@ -124,12 +121,12 @@ func runDeps() error {
|
|||
// Format upgrade suggestion
|
||||
upgrade := alert.Vulnerable
|
||||
if alert.PatchedVersion != "" {
|
||||
upgrade = fmt.Sprintf("%s -> %s", alert.Vulnerable, cli.SuccessStyle.Render(alert.PatchedVersion))
|
||||
upgrade = core.Sprintf("%s -> %s", alert.Vulnerable, cli.SuccessStyle.Render(alert.PatchedVersion))
|
||||
}
|
||||
|
||||
cli.Print("%-16s %s %-16s %-30s %s\n",
|
||||
cli.ValueStyle.Render(alert.Repo),
|
||||
sevStyle.Render(fmt.Sprintf("%-8s", alert.Severity)),
|
||||
sevStyle.Render(core.Sprintf("%-8s", alert.Severity)),
|
||||
alert.CVE,
|
||||
alert.Package,
|
||||
upgrade,
|
||||
|
|
@ -178,12 +175,7 @@ func runDepsForTarget(target string) error {
|
|||
}
|
||||
|
||||
if securityJSON {
|
||||
output, err := json.MarshalIndent(allAlerts, "", " ")
|
||||
if err != nil {
|
||||
return cli.Wrap(err, "marshal JSON output")
|
||||
}
|
||||
cli.Text(string(output))
|
||||
return nil
|
||||
return printJSON(allAlerts)
|
||||
}
|
||||
|
||||
cli.Blank()
|
||||
|
|
@ -194,11 +186,11 @@ func runDepsForTarget(target string) error {
|
|||
sevStyle := severityStyle(alert.Severity)
|
||||
upgrade := alert.Vulnerable
|
||||
if alert.PatchedVersion != "" {
|
||||
upgrade = fmt.Sprintf("%s -> %s", alert.Vulnerable, cli.SuccessStyle.Render(alert.PatchedVersion))
|
||||
upgrade = core.Sprintf("%s -> %s", alert.Vulnerable, cli.SuccessStyle.Render(alert.PatchedVersion))
|
||||
}
|
||||
cli.Print("%-16s %s %-16s %-30s %s\n",
|
||||
cli.ValueStyle.Render(alert.Repo),
|
||||
sevStyle.Render(fmt.Sprintf("%-8s", alert.Severity)),
|
||||
sevStyle.Render(core.Sprintf("%-8s", alert.Severity)),
|
||||
alert.CVE,
|
||||
alert.Package,
|
||||
upgrade,
|
||||
|
|
|
|||
|
|
@ -1,15 +1,12 @@
|
|||
package security
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"forge.lthn.ai/core/cli/pkg/cli"
|
||||
"dappco.re/go/core"
|
||||
"dappco.re/go/core/ai/ai"
|
||||
"dappco.re/go/core/i18n"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
"forge.lthn.ai/core/cli/pkg/cli"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
@ -67,9 +64,9 @@ func runJobs() error {
|
|||
}
|
||||
|
||||
func createJobForTarget(target string) error {
|
||||
parts := strings.SplitN(target, "/", 2)
|
||||
parts := core.SplitN(target, "/", 2)
|
||||
if len(parts) != 2 {
|
||||
return coreerr.E("security.createJobForTarget", "invalid target format: use owner/repo", nil)
|
||||
return core.E("security.createJobForTarget", "invalid target format: use owner/repo", nil)
|
||||
}
|
||||
|
||||
// Gather findings
|
||||
|
|
@ -93,8 +90,8 @@ func createJobForTarget(target string) error {
|
|||
severity = "medium"
|
||||
}
|
||||
summary.Add(severity)
|
||||
findings = append(findings, fmt.Sprintf("- [%s] %s: %s (%s:%d)",
|
||||
strings.ToUpper(severity), alert.Tool.Name, alert.Rule.Description,
|
||||
findings = append(findings, core.Sprintf("- [%s] %s: %s (%s:%d)",
|
||||
core.Upper(severity), alert.Tool.Name, alert.Rule.Description,
|
||||
alert.MostRecentInstance.Location.Path, alert.MostRecentInstance.Location.StartLine))
|
||||
}
|
||||
}
|
||||
|
|
@ -111,8 +108,8 @@ func createJobForTarget(target string) error {
|
|||
continue
|
||||
}
|
||||
summary.Add(alert.Advisory.Severity)
|
||||
findings = append(findings, fmt.Sprintf("- [%s] %s: %s (%s)",
|
||||
strings.ToUpper(alert.Advisory.Severity), alert.Dependency.Package.Name,
|
||||
findings = append(findings, core.Sprintf("- [%s] %s: %s (%s)",
|
||||
core.Upper(alert.Advisory.Severity), alert.Dependency.Package.Name,
|
||||
alert.Advisory.Summary, alert.Advisory.CVEID))
|
||||
}
|
||||
}
|
||||
|
|
@ -129,12 +126,12 @@ func createJobForTarget(target string) error {
|
|||
continue
|
||||
}
|
||||
summary.Add("high")
|
||||
findings = append(findings, fmt.Sprintf("- [HIGH] Secret: %s (#%d)", alert.SecretType, alert.Number))
|
||||
findings = append(findings, core.Sprintf("- [HIGH] Secret: %s (#%d)", alert.SecretType, alert.Number))
|
||||
}
|
||||
}
|
||||
|
||||
if fetchErrors == 3 {
|
||||
return coreerr.E("security.createJobForTarget", fmt.Sprintf("failed to fetch any alerts for %s", target), nil)
|
||||
return core.E("security.createJobForTarget", core.Sprintf("failed to fetch any alerts for %s", target), nil)
|
||||
}
|
||||
|
||||
if summary.Total == 0 {
|
||||
|
|
@ -143,13 +140,13 @@ func createJobForTarget(target string) error {
|
|||
}
|
||||
|
||||
// Build issue body
|
||||
title := fmt.Sprintf("Security scan: %s", target)
|
||||
title := core.Sprintf("Security scan: %s", target)
|
||||
body := buildJobIssueBody(target, summary, findings)
|
||||
|
||||
for i := range jobsCopies {
|
||||
issueTitle := title
|
||||
if jobsCopies > 1 {
|
||||
issueTitle = fmt.Sprintf("%s (#%d)", title, i+1)
|
||||
issueTitle = core.Sprintf("%s (#%d)", title, i+1)
|
||||
}
|
||||
|
||||
if jobsDryRun {
|
||||
|
|
@ -161,20 +158,16 @@ func createJobForTarget(target string) error {
|
|||
continue
|
||||
}
|
||||
|
||||
// Create issue via gh CLI
|
||||
cmd := exec.Command("gh", "issue", "create",
|
||||
"--repo", jobsIssueRepo,
|
||||
"--title", issueTitle,
|
||||
"--body", body,
|
||||
"--label", "type:security-scan,repo:"+target,
|
||||
issueURL, err := createGitHubIssue(
|
||||
jobsIssueRepo,
|
||||
issueTitle,
|
||||
body,
|
||||
[]string{"type:security-scan", core.Concat("repo:", target)},
|
||||
)
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return cli.Wrap(err, fmt.Sprintf("create issue for %s: %s", target, string(output)))
|
||||
return cli.Wrap(err, core.Sprintf("create issue for %s", target))
|
||||
}
|
||||
|
||||
issueURL := strings.TrimSpace(string(output))
|
||||
cli.Print("%s %s: %s\n", cli.SuccessStyle.Render(">>"), issueTitle, issueURL)
|
||||
|
||||
// Record metrics
|
||||
|
|
@ -196,10 +189,10 @@ func createJobForTarget(target string) error {
|
|||
}
|
||||
|
||||
func buildJobIssueBody(target string, summary *AlertSummary, findings []string) string {
|
||||
var sb strings.Builder
|
||||
sb := core.NewBuilder()
|
||||
|
||||
fmt.Fprintf(&sb, "## Security Scan: %s\n\n", target)
|
||||
fmt.Fprintf(&sb, "**Summary:** %s\n\n", summary.String())
|
||||
sb.WriteString(core.Sprintf("## Security Scan: %s\n\n", target))
|
||||
sb.WriteString(core.Sprintf("**Summary:** %s\n\n", summary.PlainString()))
|
||||
|
||||
sb.WriteString("### Findings\n\n")
|
||||
if len(findings) > 50 {
|
||||
|
|
@ -207,7 +200,7 @@ func buildJobIssueBody(target string, summary *AlertSummary, findings []string)
|
|||
for _, f := range findings[:50] {
|
||||
sb.WriteString(f + "\n")
|
||||
}
|
||||
fmt.Fprintf(&sb, "\n... and %d more\n", len(findings)-50)
|
||||
sb.WriteString(core.Sprintf("\n... and %d more\n", len(findings)-50))
|
||||
} else {
|
||||
for _, f := range findings {
|
||||
sb.WriteString(f + "\n")
|
||||
|
|
@ -222,7 +215,7 @@ func buildJobIssueBody(target string, summary *AlertSummary, findings []string)
|
|||
|
||||
sb.WriteString("\n### Instructions\n\n")
|
||||
sb.WriteString("1. Claim this issue by assigning yourself\n")
|
||||
fmt.Fprintf(&sb, "2. Run `core security alerts --target %s` for the latest findings\n", target)
|
||||
sb.WriteString(core.Sprintf("2. Run `core security alerts --target %s` for the latest findings\n", target))
|
||||
sb.WriteString("3. Work through the checklist above\n")
|
||||
sb.WriteString("4. Close this issue when all findings are addressed\n")
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
package security
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"dappco.re/go/core"
|
||||
"dappco.re/go/core/ai/ai"
|
||||
"forge.lthn.ai/core/cli/pkg/cli"
|
||||
"dappco.re/go/core/i18n"
|
||||
"forge.lthn.ai/core/cli/pkg/cli"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
@ -35,6 +34,10 @@ func addScanCommand(parent *cli.Command) {
|
|||
}
|
||||
|
||||
// ScanAlert represents a code scanning alert for output.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// var alert ScanAlert
|
||||
type ScanAlert struct {
|
||||
Repo string `json:"repo"`
|
||||
Severity string `json:"severity"`
|
||||
|
|
@ -70,7 +73,7 @@ func runScan() error {
|
|||
summary := &AlertSummary{}
|
||||
|
||||
for _, repo := range repoList {
|
||||
repoFullName := fmt.Sprintf("%s/%s", reg.Org, repo.Name)
|
||||
repoFullName := core.Concat(reg.Org, "/", repo.Name)
|
||||
|
||||
alerts, err := fetchCodeScanningAlerts(repoFullName)
|
||||
if err != nil {
|
||||
|
|
@ -127,12 +130,7 @@ func runScan() error {
|
|||
})
|
||||
|
||||
if securityJSON {
|
||||
output, err := json.MarshalIndent(allAlerts, "", " ")
|
||||
if err != nil {
|
||||
return cli.Wrap(err, "marshal JSON output")
|
||||
}
|
||||
cli.Text(string(output))
|
||||
return nil
|
||||
return printJSON(allAlerts)
|
||||
}
|
||||
|
||||
// Print summary
|
||||
|
|
@ -148,11 +146,11 @@ func runScan() error {
|
|||
for _, alert := range allAlerts {
|
||||
sevStyle := severityStyle(alert.Severity)
|
||||
|
||||
location := fmt.Sprintf("%s:%d", alert.Path, alert.Line)
|
||||
location := core.Sprintf("%s:%d", alert.Path, alert.Line)
|
||||
|
||||
cli.Print("%-16s %s %-20s %-40s %s\n",
|
||||
cli.ValueStyle.Render(alert.Repo),
|
||||
sevStyle.Render(fmt.Sprintf("%-8s", alert.Severity)),
|
||||
sevStyle.Render(core.Sprintf("%-8s", alert.Severity)),
|
||||
alert.RuleID,
|
||||
location,
|
||||
cli.DimStyle.Render(alert.Tool),
|
||||
|
|
@ -221,12 +219,7 @@ func runScanForTarget(target string) error {
|
|||
})
|
||||
|
||||
if securityJSON {
|
||||
output, err := json.MarshalIndent(allAlerts, "", " ")
|
||||
if err != nil {
|
||||
return cli.Wrap(err, "marshal JSON output")
|
||||
}
|
||||
cli.Text(string(output))
|
||||
return nil
|
||||
return printJSON(allAlerts)
|
||||
}
|
||||
|
||||
cli.Blank()
|
||||
|
|
@ -239,10 +232,10 @@ func runScanForTarget(target string) error {
|
|||
|
||||
for _, alert := range allAlerts {
|
||||
sevStyle := severityStyle(alert.Severity)
|
||||
location := fmt.Sprintf("%s:%d", alert.Path, alert.Line)
|
||||
location := core.Sprintf("%s:%d", alert.Path, alert.Line)
|
||||
cli.Print("%-16s %s %-20s %-40s %s\n",
|
||||
cli.ValueStyle.Render(alert.Repo),
|
||||
sevStyle.Render(fmt.Sprintf("%-8s", alert.Severity)),
|
||||
sevStyle.Render(core.Sprintf("%-8s", alert.Severity)),
|
||||
alert.RuleID,
|
||||
location,
|
||||
cli.DimStyle.Render(alert.Tool),
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
package security
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"forge.lthn.ai/core/cli/pkg/cli"
|
||||
"dappco.re/go/core"
|
||||
"dappco.re/go/core/i18n"
|
||||
"forge.lthn.ai/core/cli/pkg/cli"
|
||||
)
|
||||
|
||||
func addSecretsCommand(parent *cli.Command) {
|
||||
|
|
@ -27,6 +25,10 @@ func addSecretsCommand(parent *cli.Command) {
|
|||
}
|
||||
|
||||
// SecretAlert represents a secret scanning alert for output.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// var alert SecretAlert
|
||||
type SecretAlert struct {
|
||||
Repo string `json:"repo"`
|
||||
Number int `json:"number"`
|
||||
|
|
@ -60,10 +62,11 @@ func runSecrets() error {
|
|||
openCount := 0
|
||||
|
||||
for _, repo := range repoList {
|
||||
repoFullName := fmt.Sprintf("%s/%s", reg.Org, repo.Name)
|
||||
repoFullName := core.Concat(reg.Org, "/", repo.Name)
|
||||
|
||||
alerts, err := fetchSecretScanningAlerts(repoFullName)
|
||||
if err != nil {
|
||||
cli.Print("%s %s: %v\n", cli.WarningStyle.Render(">>"), repoFullName, err)
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
@ -86,18 +89,13 @@ func runSecrets() error {
|
|||
}
|
||||
|
||||
if securityJSON {
|
||||
output, err := json.MarshalIndent(allAlerts, "", " ")
|
||||
if err != nil {
|
||||
return cli.Wrap(err, "marshal JSON output")
|
||||
}
|
||||
cli.Text(string(output))
|
||||
return nil
|
||||
return printJSON(allAlerts)
|
||||
}
|
||||
|
||||
// Print summary
|
||||
cli.Blank()
|
||||
if openCount > 0 {
|
||||
cli.Print("%s %s\n", cli.DimStyle.Render("Secrets:"), cli.ErrorStyle.Render(fmt.Sprintf("%d open", openCount)))
|
||||
cli.Print("%s %s\n", cli.DimStyle.Render("Secrets:"), cli.ErrorStyle.Render(core.Sprintf("%d open", openCount)))
|
||||
} else {
|
||||
cli.Print("%s %s\n", cli.DimStyle.Render("Secrets:"), cli.SuccessStyle.Render("No exposed secrets"))
|
||||
}
|
||||
|
|
@ -157,19 +155,14 @@ func runSecretsForTarget(target string) error {
|
|||
}
|
||||
|
||||
if securityJSON {
|
||||
output, err := json.MarshalIndent(allAlerts, "", " ")
|
||||
if err != nil {
|
||||
return cli.Wrap(err, "marshal JSON output")
|
||||
}
|
||||
cli.Text(string(output))
|
||||
return nil
|
||||
return printJSON(allAlerts)
|
||||
}
|
||||
|
||||
cli.Blank()
|
||||
if openCount > 0 {
|
||||
cli.Print("%s %s\n", cli.DimStyle.Render("Secrets ("+fullName+"):"), cli.ErrorStyle.Render(fmt.Sprintf("%d open", openCount)))
|
||||
cli.Print("%s %s\n", cli.DimStyle.Render(core.Concat("Secrets (", fullName, "):")), cli.ErrorStyle.Render(core.Sprintf("%d open", openCount)))
|
||||
} else {
|
||||
cli.Print("%s %s\n", cli.DimStyle.Render("Secrets ("+fullName+"):"), cli.SuccessStyle.Render("No exposed secrets"))
|
||||
cli.Print("%s %s\n", cli.DimStyle.Render(core.Concat("Secrets (", fullName, "):")), cli.SuccessStyle.Render("No exposed secrets"))
|
||||
}
|
||||
cli.Blank()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,12 @@
|
|||
package security
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"forge.lthn.ai/core/cli/pkg/cli"
|
||||
"dappco.re/go/core"
|
||||
"dappco.re/go/core/i18n"
|
||||
"dappco.re/go/core/io"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
"forge.lthn.ai/core/cli/pkg/cli"
|
||||
"forge.lthn.ai/core/go-scm/repos"
|
||||
)
|
||||
|
||||
|
|
@ -23,6 +20,10 @@ var (
|
|||
)
|
||||
|
||||
// AddSecurityCommands adds the 'security' command to the root.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// AddSecurityCommands(root)
|
||||
func AddSecurityCommands(root *cli.Command) {
|
||||
secCmd := &cli.Command{
|
||||
Use: "security",
|
||||
|
|
@ -40,6 +41,10 @@ func AddSecurityCommands(root *cli.Command) {
|
|||
}
|
||||
|
||||
// DependabotAlert represents a Dependabot vulnerability alert.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// var alert DependabotAlert
|
||||
type DependabotAlert struct {
|
||||
Number int `json:"number"`
|
||||
State string `json:"state"`
|
||||
|
|
@ -69,6 +74,10 @@ type DependabotAlert struct {
|
|||
}
|
||||
|
||||
// CodeScanningAlert represents a code scanning alert.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// var alert CodeScanningAlert
|
||||
type CodeScanningAlert struct {
|
||||
Number int `json:"number"`
|
||||
State string `json:"state"`
|
||||
|
|
@ -96,6 +105,10 @@ type CodeScanningAlert struct {
|
|||
}
|
||||
|
||||
// SecretScanningAlert represents a secret scanning alert.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// var alert SecretScanningAlert
|
||||
type SecretScanningAlert struct {
|
||||
Number int `json:"number"`
|
||||
State string `json:"state"`
|
||||
|
|
@ -126,37 +139,9 @@ func loadRegistry(registryPath string) (*repos.Registry, error) {
|
|||
return reg, nil
|
||||
}
|
||||
|
||||
// checkGH verifies gh CLI is available.
|
||||
func checkGH() error {
|
||||
if _, err := exec.LookPath("gh"); err != nil {
|
||||
return coreerr.E("security.checkGH", i18n.T("error.gh_not_found"), nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// runGHAPI runs a gh api command and returns the output.
|
||||
func runGHAPI(endpoint string) ([]byte, error) {
|
||||
cmd := exec.Command("gh", "api", endpoint, "--paginate")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
stderr := string(exitErr.Stderr)
|
||||
// Handle common errors gracefully
|
||||
if strings.Contains(stderr, "404") || strings.Contains(stderr, "Not Found") {
|
||||
return []byte("[]"), nil // Return empty array for not found
|
||||
}
|
||||
if strings.Contains(stderr, "403") {
|
||||
return nil, coreerr.E("security.runGHAPI", "access denied (check token permissions)", nil)
|
||||
}
|
||||
}
|
||||
return nil, cli.Wrap(err, "run gh api")
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
// severityStyle returns the appropriate style for a severity level.
|
||||
func severityStyle(severity string) *cli.AnsiStyle {
|
||||
switch strings.ToLower(severity) {
|
||||
switch core.Lower(severity) {
|
||||
case "critical":
|
||||
return cli.ErrorStyle
|
||||
case "high":
|
||||
|
|
@ -174,9 +159,9 @@ func filterBySeverity(severity, filter string) bool {
|
|||
return true
|
||||
}
|
||||
|
||||
sev := strings.ToLower(severity)
|
||||
return slices.ContainsFunc(slices.Collect(strings.SplitSeq(strings.ToLower(filter), ",")), func(s string) bool {
|
||||
return strings.TrimSpace(s) == sev
|
||||
sev := core.Lower(severity)
|
||||
return slices.ContainsFunc(core.Split(core.Lower(filter), ","), func(s string) bool {
|
||||
return core.Trim(s) == sev
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -193,7 +178,7 @@ func getReposToCheck(reg *repos.Registry, repoFilter string) []*repos.Repo {
|
|||
|
||||
// buildTargetRepo creates a synthetic Repo entry for an external target (e.g. "wailsapp/wails").
|
||||
func buildTargetRepo(target string) (*repos.Repo, string) {
|
||||
parts := strings.SplitN(target, "/", 2)
|
||||
parts := core.SplitN(target, "/", 2)
|
||||
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
|
||||
return nil, ""
|
||||
}
|
||||
|
|
@ -201,6 +186,10 @@ func buildTargetRepo(target string) (*repos.Repo, string) {
|
|||
}
|
||||
|
||||
// AlertSummary holds aggregated alert counts.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// summary := &AlertSummary{}
|
||||
type AlertSummary struct {
|
||||
Critical int
|
||||
High int
|
||||
|
|
@ -211,9 +200,13 @@ type AlertSummary struct {
|
|||
}
|
||||
|
||||
// Add increments summary counters for the provided severity.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// summary.Add("high")
|
||||
func (s *AlertSummary) Add(severity string) {
|
||||
s.Total++
|
||||
switch strings.ToLower(severity) {
|
||||
switch core.Lower(severity) {
|
||||
case "critical":
|
||||
s.Critical++
|
||||
case "high":
|
||||
|
|
@ -228,25 +221,51 @@ func (s *AlertSummary) Add(severity string) {
|
|||
}
|
||||
|
||||
// String renders a human-readable summary of alert counts.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// text := summary.String()
|
||||
func (s *AlertSummary) String() string {
|
||||
parts := []string{}
|
||||
if s.Critical > 0 {
|
||||
parts = append(parts, cli.ErrorStyle.Render(fmt.Sprintf("%d critical", s.Critical)))
|
||||
}
|
||||
if s.High > 0 {
|
||||
parts = append(parts, cli.WarningStyle.Render(fmt.Sprintf("%d high", s.High)))
|
||||
}
|
||||
if s.Medium > 0 {
|
||||
parts = append(parts, cli.ValueStyle.Render(fmt.Sprintf("%d medium", s.Medium)))
|
||||
}
|
||||
if s.Low > 0 {
|
||||
parts = append(parts, cli.DimStyle.Render(fmt.Sprintf("%d low", s.Low)))
|
||||
}
|
||||
if s.Unknown > 0 {
|
||||
parts = append(parts, cli.DimStyle.Render(fmt.Sprintf("%d unknown", s.Unknown)))
|
||||
}
|
||||
if len(parts) == 0 {
|
||||
return cli.SuccessStyle.Render("No alerts")
|
||||
}
|
||||
return strings.Join(parts, " | ")
|
||||
return s.renderSummary(func(level, text string) string {
|
||||
switch level {
|
||||
case "critical":
|
||||
return cli.ErrorStyle.Render(text)
|
||||
case "high":
|
||||
return cli.WarningStyle.Render(text)
|
||||
case "medium":
|
||||
return cli.ValueStyle.Render(text)
|
||||
case "none":
|
||||
return cli.SuccessStyle.Render(text)
|
||||
default:
|
||||
return cli.DimStyle.Render(text)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// PlainString renders an unstyled summary suitable for logs and issue bodies.
|
||||
func (s *AlertSummary) PlainString() string {
|
||||
return s.renderSummary(func(_ string, text string) string {
|
||||
return text
|
||||
})
|
||||
}
|
||||
|
||||
func (s *AlertSummary) renderSummary(render func(level, text string) string) string {
|
||||
parts := []string{}
|
||||
|
||||
appendPart := func(level string, count int) {
|
||||
if count > 0 {
|
||||
parts = append(parts, render(level, core.Sprintf("%d %s", count, level)))
|
||||
}
|
||||
}
|
||||
|
||||
appendPart("critical", s.Critical)
|
||||
appendPart("high", s.High)
|
||||
appendPart("medium", s.Medium)
|
||||
appendPart("low", s.Low)
|
||||
appendPart("unknown", s.Unknown)
|
||||
|
||||
if len(parts) == 0 {
|
||||
return render("none", "No alerts")
|
||||
}
|
||||
return core.Join(" | ", parts...)
|
||||
}
|
||||
|
|
|
|||
48
cmd/security/cmd_security_test.go
Normal file
48
cmd/security/cmd_security_test.go
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
package security
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAlertSummary_PlainString_Good(t *testing.T) {
|
||||
t.Run("RendersWithoutANSI", func(t *testing.T) {
|
||||
summary := &AlertSummary{
|
||||
Critical: 1,
|
||||
High: 2,
|
||||
Medium: 3,
|
||||
Low: 4,
|
||||
Unknown: 5,
|
||||
}
|
||||
|
||||
got := summary.PlainString()
|
||||
want := "1 critical | 2 high | 3 medium | 4 low | 5 unknown"
|
||||
if got != want {
|
||||
t.Fatalf("PlainString() = %q, want %q", got, want)
|
||||
}
|
||||
if strings.Contains(got, "\x1b[") {
|
||||
t.Fatalf("PlainString() unexpectedly contains ANSI escapes: %q", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("RendersEmpty", func(t *testing.T) {
|
||||
summary := &AlertSummary{}
|
||||
if got := summary.PlainString(); got != "No alerts" {
|
||||
t.Fatalf("PlainString() = %q, want %q", got, "No alerts")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSecurity_BuildJobIssueBody_Good(t *testing.T) {
|
||||
summary := &AlertSummary{Critical: 1, High: 2}
|
||||
body := buildJobIssueBody("owner/repo", summary, []string{"- [HIGH] Finding"})
|
||||
|
||||
if strings.Contains(body, "\x1b[") {
|
||||
t.Fatalf("issue body unexpectedly contains ANSI escapes: %q", body)
|
||||
}
|
||||
if !strings.Contains(body, "**Summary:** 1 critical | 2 high") {
|
||||
t.Fatalf("issue body missing plain summary: %q", body)
|
||||
}
|
||||
}
|
||||
176
cmd/security/github.go
Normal file
176
cmd/security/github.go
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
package security
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"dappco.re/go/core"
|
||||
"forge.lthn.ai/core/cli/pkg/cli"
|
||||
)
|
||||
|
||||
const githubAPIBase = "https://api.github.com"
|
||||
|
||||
func printJSON(value any) error {
|
||||
output := core.JSONMarshal(value)
|
||||
if !output.OK {
|
||||
return cli.Wrap(output.Value.(error), "marshal JSON output")
|
||||
}
|
||||
|
||||
cli.Text(string(output.Value.([]byte)))
|
||||
return nil
|
||||
}
|
||||
|
||||
func githubToken() string {
|
||||
if token := core.Env("GH_TOKEN"); token != "" {
|
||||
return token
|
||||
}
|
||||
return core.Env("GITHUB_TOKEN")
|
||||
}
|
||||
|
||||
// checkGH verifies GitHub API authentication is configured.
|
||||
func checkGH() error {
|
||||
if githubToken() == "" {
|
||||
return core.E("security.checkGH", "missing GitHub token (set GH_TOKEN or GITHUB_TOKEN)", nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// runGHAPI fetches all paginated GitHub API results for an array endpoint.
|
||||
func runGHAPI(endpoint string) ([]byte, error) {
|
||||
pages, err := runGHAPIPages(endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []byte(combineGitHubPages(pages)), nil
|
||||
}
|
||||
|
||||
func runGHAPIPages(endpoint string) ([]string, error) {
|
||||
nextURL := core.Concat(githubAPIBase, "/", endpoint)
|
||||
pages := make([]string, 0, 1)
|
||||
|
||||
for nextURL != "" {
|
||||
status, body, headers, err := executeGitHubRequest(http.MethodGet, nextURL, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch status {
|
||||
case http.StatusOK:
|
||||
case http.StatusNotFound:
|
||||
return nil, nil
|
||||
case http.StatusForbidden:
|
||||
return nil, core.E("security.runGHAPI", "access denied (check token permissions)", nil)
|
||||
default:
|
||||
msg := core.Sprintf("GitHub API GET %d", status)
|
||||
if trimmed := core.Trim(body); trimmed != "" {
|
||||
msg = core.Concat(msg, ": ", trimmed)
|
||||
}
|
||||
return nil, core.E("security.runGHAPI", msg, nil)
|
||||
}
|
||||
|
||||
pages = append(pages, body)
|
||||
nextURL = nextGitHubPage(headers.Get("Link"))
|
||||
}
|
||||
|
||||
return pages, nil
|
||||
}
|
||||
|
||||
func createGitHubIssue(repo, title, body string, labels []string) (string, error) {
|
||||
payload := core.JSONMarshal(map[string]any{
|
||||
"title": title,
|
||||
"body": body,
|
||||
"labels": labels,
|
||||
})
|
||||
if !payload.OK {
|
||||
return "", core.E("security.createGitHubIssue", "marshal issue request", payload.Value.(error))
|
||||
}
|
||||
|
||||
status, responseBody, _, err := executeGitHubRequest(
|
||||
http.MethodPost,
|
||||
core.Concat(githubAPIBase, "/repos/", repo, "/issues"),
|
||||
string(payload.Value.([]byte)),
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if status < http.StatusOK || status >= http.StatusMultipleChoices {
|
||||
msg := core.Sprintf("GitHub issue create %d", status)
|
||||
if trimmed := core.Trim(responseBody); trimmed != "" {
|
||||
msg = core.Concat(msg, ": ", trimmed)
|
||||
}
|
||||
return "", core.E("security.createGitHubIssue", msg, nil)
|
||||
}
|
||||
|
||||
var response struct {
|
||||
HTMLURL string `json:"html_url"`
|
||||
}
|
||||
if decoded := core.JSONUnmarshalString(responseBody, &response); !decoded.OK {
|
||||
return "", core.E("security.createGitHubIssue", "decode issue response", decoded.Value.(error))
|
||||
}
|
||||
if response.HTMLURL == "" {
|
||||
return "", core.E("security.createGitHubIssue", "issue response missing html_url", nil)
|
||||
}
|
||||
|
||||
return response.HTMLURL, nil
|
||||
}
|
||||
|
||||
func executeGitHubRequest(method, targetURL, body string) (int, string, http.Header, error) {
|
||||
req, err := http.NewRequest(method, targetURL, core.NewReader(body))
|
||||
if err != nil {
|
||||
return 0, "", nil, core.E("security.executeGitHubRequest", "build request", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/vnd.github+json")
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("User-Agent", "core-ai-security")
|
||||
req.Header.Set("X-GitHub-Api-Version", "2022-11-28")
|
||||
|
||||
if token := githubToken(); token != "" {
|
||||
req.Header.Set("Authorization", core.Concat("Bearer ", token))
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return 0, "", nil, core.E("security.executeGitHubRequest", "send request", err)
|
||||
}
|
||||
|
||||
payload := core.ReadAll(resp.Body)
|
||||
if !payload.OK {
|
||||
return 0, "", nil, core.E("security.executeGitHubRequest", "read response body", payload.Value.(error))
|
||||
}
|
||||
|
||||
return resp.StatusCode, payload.Value.(string), resp.Header, nil
|
||||
}
|
||||
|
||||
func combineGitHubPages(pages []string) string {
|
||||
parts := make([]string, 0, len(pages))
|
||||
for _, page := range pages {
|
||||
trimmed := core.Trim(page)
|
||||
if trimmed == "" || trimmed == "[]" {
|
||||
continue
|
||||
}
|
||||
trimmed = core.TrimPrefix(trimmed, "[")
|
||||
trimmed = core.TrimSuffix(trimmed, "]")
|
||||
trimmed = core.Trim(trimmed)
|
||||
if trimmed != "" {
|
||||
parts = append(parts, trimmed)
|
||||
}
|
||||
}
|
||||
if len(parts) == 0 {
|
||||
return "[]"
|
||||
}
|
||||
return core.Concat("[", core.Join(",", parts...), "]")
|
||||
}
|
||||
|
||||
func nextGitHubPage(linkHeader string) string {
|
||||
for _, part := range core.Split(linkHeader, ",") {
|
||||
if !core.Contains(part, `rel="next"`) {
|
||||
continue
|
||||
}
|
||||
urlPart := core.Trim(core.SplitN(part, ";", 2)[0])
|
||||
urlPart = core.TrimPrefix(urlPart, "<")
|
||||
return core.TrimSuffix(urlPart, ">")
|
||||
}
|
||||
return ""
|
||||
}
|
||||
28
go.mod
28
go.mod
|
|
@ -3,16 +3,16 @@ module dappco.re/go/core/ai
|
|||
go 1.26.0
|
||||
|
||||
require (
|
||||
dappco.re/go/core/i18n v0.1.7
|
||||
dappco.re/go/core/io v0.1.7
|
||||
dappco.re/go/core/log v0.0.4
|
||||
dappco.re/go/core v0.8.0-alpha.1
|
||||
dappco.re/go/core/i18n v0.2.0
|
||||
dappco.re/go/core/io v0.2.0
|
||||
forge.lthn.ai/core/cli v0.3.7
|
||||
forge.lthn.ai/core/go-rag v0.1.11
|
||||
forge.lthn.ai/core/go-scm v0.2.0
|
||||
)
|
||||
|
||||
require (
|
||||
dappco.re/go/core v0.4.7 // indirect
|
||||
dappco.re/go/core/log v0.1.0 // indirect
|
||||
forge.lthn.ai/core/go v0.3.3 // indirect
|
||||
forge.lthn.ai/core/go-i18n v0.1.7 // indirect
|
||||
forge.lthn.ai/core/go-inference v0.1.7 // indirect
|
||||
|
|
@ -57,23 +57,3 @@ require (
|
|||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
replace (
|
||||
dappco.re/go/core => ../go
|
||||
dappco.re/go/core/i18n => ../go-i18n
|
||||
dappco.re/go/core/io => ../go-io
|
||||
dappco.re/go/core/log => ../go-log
|
||||
dappco.re/go/core/webview => ../go-webview
|
||||
dappco.re/go/core/ws => ../go-ws
|
||||
forge.lthn.ai/core/api => ../api
|
||||
forge.lthn.ai/core/cli => ../cli
|
||||
forge.lthn.ai/core/go-crypt => ../go-crypt
|
||||
forge.lthn.ai/core/go-inference => ../go-inference
|
||||
forge.lthn.ai/core/go-ml => ../go-ml
|
||||
forge.lthn.ai/core/go-mlx => ../go-mlx
|
||||
forge.lthn.ai/core/go-process => ../go-process
|
||||
forge.lthn.ai/core/go-rag => ../go-rag
|
||||
forge.lthn.ai/core/go-scm => ../go-scm
|
||||
forge.lthn.ai/core/mcp => ../mcp
|
||||
forge.lthn.ai/lthn/lem => ../../lthn/LEM
|
||||
)
|
||||
|
|
|
|||
180
go.sum
180
go.sum
|
|
@ -1,17 +1,67 @@
|
|||
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
|
||||
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
||||
code.gitea.io/sdk/gitea v0.23.2/go.mod h1:yyF5+GhljqvA30sRDreoyHILruNiy4ASufugzYg0VHM=
|
||||
codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2 v2.2.0/go.mod h1:ZglEEDj+qkxYUb+SQIeqGtFxQrbaMYqIOgahNKb7uxs=
|
||||
dappco.re/go/core v0.8.0-alpha.1 h1:gj7+Scv+L63Z7wMxbJYHhaRFkHJo2u4MMPuUSv/Dhtk=
|
||||
dappco.re/go/core v0.8.0-alpha.1/go.mod h1:f2/tBZ3+3IqDrg2F5F598llv0nmb/4gJVCFzM5geE4A=
|
||||
dappco.re/go/core/i18n v0.2.0 h1:NHzk6RCU93/qVRA3f2jvMr9P1R6FYheR/sHL+TnvKbI=
|
||||
dappco.re/go/core/i18n v0.2.0/go.mod h1:9eSVJXr3OpIGWQvDynfhqcp27xnLMwlYLgsByU+p7ok=
|
||||
dappco.re/go/core/io v0.2.0 h1:zuudgIiTsQQ5ipVt97saWdGLROovbEB/zdVyy9/l+I4=
|
||||
dappco.re/go/core/io v0.2.0/go.mod h1:1QnQV6X9LNgFKfm8SkOtR9LLaj3bDcsOIeJOOyjbL5E=
|
||||
dappco.re/go/core/log v0.1.0 h1:pa71Vq2TD2aoEUQWFKwNcaJ3GBY8HbaNGqtE688Unyc=
|
||||
dappco.re/go/core/log v0.1.0/go.mod h1:Nkqb8gsXhZAO8VLpx7B8i1iAmohhzqA20b9Zr8VUcJs=
|
||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
forge.lthn.ai/Snider/Borg v0.3.1/go.mod h1:Z7DJD0yHXsxSyM7Mjl6/g4gH1NBsIz44Bf5AFlV76Wg=
|
||||
forge.lthn.ai/core/api v0.1.0/go.mod h1:c86Lk9AmaS0xbiRCEG/+du8s9KyYNHnp8RED35gR/Fo=
|
||||
forge.lthn.ai/core/cli v0.3.7 h1:1GrbaGg0wDGHr6+klSbbGyN/9sSbHvFbdySJznymhwg=
|
||||
forge.lthn.ai/core/cli v0.3.7/go.mod h1:DBUppJkA9P45ZFGgI2B8VXw1rAZxamHoI/KG7fRvTNs=
|
||||
forge.lthn.ai/core/config v0.1.0/go.mod h1:8HYA29drAWlX+bO4VI1JhmKUgGU66E2Xge8D3tKd3Dg=
|
||||
forge.lthn.ai/core/go v0.3.3 h1:kYYZ2nRYy0/Be3cyuLJspRjLqTMxpckVyhb/7Sw2gd0=
|
||||
forge.lthn.ai/core/go v0.3.3/go.mod h1:Cp4ac25pghvO2iqOu59t1GyngTKVOzKB5/VPdhRi9CQ=
|
||||
forge.lthn.ai/core/go-crypt v0.1.6/go.mod h1:4VZAGqxlbadhSB66sJkdj54/HSJ+bSxVgwWK5kMMYDo=
|
||||
forge.lthn.ai/core/go-i18n v0.1.7 h1:aHkAoc3W8fw3RPNvw/UszQbjyFWXHszzbZgty3SwyAA=
|
||||
forge.lthn.ai/core/go-i18n v0.1.7/go.mod h1:0VDjwtY99NSj2iqwrI09h5GUsJeM9s48MLkr+/Dn4G8=
|
||||
forge.lthn.ai/core/go-inference v0.1.7 h1:9Dy6v03jX5ZRH3n5iTzlYyGtucuBIgSe+S7GWvBzx9Q=
|
||||
forge.lthn.ai/core/go-inference v0.1.7/go.mod h1:jfWz+IJX55wAH98+ic6FEqqGB6/P31CHlg7VY7pxREw=
|
||||
forge.lthn.ai/core/go-io v0.1.7 h1:Tdb6sqh+zz1lsGJaNX9RFWM6MJ/RhSAyxfulLXrJsbk=
|
||||
forge.lthn.ai/core/go-io v0.1.7/go.mod h1:8lRLFk4Dnp5cR/Cyzh9WclD5566TbpdRgwcH7UZLWn4=
|
||||
forge.lthn.ai/core/go-log v0.0.4 h1:KTuCEPgFmuM8KJfnyQ8vPOU1Jg654W74h8IJvfQMfv0=
|
||||
forge.lthn.ai/core/go-log v0.0.4/go.mod h1:r14MXKOD3LF/sI8XUJQhRk/SZHBE7jAFVuCfgkXoZPw=
|
||||
forge.lthn.ai/core/go-rag v0.1.11 h1:KXTOtnOdrx8YKmvnj0EOi2EI/+cKjE8w2PpJCQIrSd8=
|
||||
forge.lthn.ai/core/go-rag v0.1.11/go.mod h1:vIlOKVD1SdqqjkJ2XQyXPuKPtiajz/STPLCaDpqOzk8=
|
||||
forge.lthn.ai/core/go-scm v0.2.0 h1:TvDyCzw0HWzXjmqe6uPc46nPaRzc7MPGswmwZt0CmXo=
|
||||
forge.lthn.ai/core/go-scm v0.2.0/go.mod h1:Q/PV2FbqDlWnAOsXAd1pgSiHOlRCPW4HcPmOt8Z9H+E=
|
||||
forge.lthn.ai/core/go-ws v0.1.0/go.mod h1:wBQLXDUod6FqESh1CM4OnAjyP3cmWg8Vd5M43RIdTwA=
|
||||
github.com/42wim/httpsig v1.2.3/go.mod h1:nZq9OlYKDrUBhptd77IHx4/sZZD+IxTBADvAPI9G/EM=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/ProtonMail/go-crypto v1.4.0/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo=
|
||||
github.com/TheTitanrain/w32 v0.0.0-20180517000239-4f5cfb03fabf/go.mod h1:peYoMncQljjNS6tZwI9WVyQB3qZS6u79/N3mBOcnd3I=
|
||||
github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo=
|
||||
github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40/go.mod h1:Q7yQnSMnLvcXlZ8RV+jwz/6y1rQTqbX6C82SndT52Zs=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.4/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.7/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20/go.mod h1:oydPDJKcfMhgfcgBUZaG+toBbwy8yPWubJXBVERtI4o=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20/go.mod h1:YJ898MhD067hSHA6xYCx5ts/jEd8BSOLtQDL3iZsvbc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.21/go.mod h1:UUxgWxofmOdAMuqEsSppbDtGKLfR04HGsD0HXzvhI1k=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.12/go.mod h1:v2pNpJbRNl4vEUWEh5ytQok0zACAKfdmKS51Hotc3pQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20/go.mod h1:V4X406Y666khGa8ghKmphma/7C0DAtEQYhkq9z4vpbk=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.20/go.mod h1:4TLZCmVJDM3FOu5P5TJP0zOlu9zWgDWU7aUxWbr+rcw=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.1/go.mod h1:qXVal5H0ChqXP63t6jze5LmFalc7+ZE7wOdLtZ0LCP0=
|
||||
github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
|
||||
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
|
||||
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
|
||||
github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
|
||||
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
||||
|
|
@ -24,90 +74,198 @@ github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF
|
|||
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
|
||||
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
||||
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
||||
github.com/chewxy/hm v1.0.0/go.mod h1:qg9YI4q6Fkj/whwHR1D+bOGeF7SniIP40VweVepLjg0=
|
||||
github.com/chewxy/math32 v1.11.0/go.mod h1:dOB2rcuFrCn6UHrze36WSLVPKtzPMRAQvBvUwkSsLqs=
|
||||
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
|
||||
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
|
||||
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI=
|
||||
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
|
||||
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
|
||||
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||
github.com/d4l3k/go-bfloat16 v0.0.0-20211005043715-690c3bdd05f1/go.mod h1:uw2gLcxEuYUlAd/EXyjc/v55nd3+47YAgWbSXVxPrNI=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/emirpasic/gods/v2 v2.0.0-alpha/go.mod h1:W0y4M2dtBB9U5z3YlghmpuUhiaZT2h6yoeE+C1sCp6A=
|
||||
github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98=
|
||||
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||
github.com/gin-contrib/cors v1.7.2/go.mod h1:SUJVARKgQ40dmrzgXEVxj2m7Ig1v1qIboQkPDTQ9t2E=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
||||
github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/flatbuffers v24.3.25+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/ledongthuc/pdf v0.0.0-20250511090121-5959a4027728/go.mod h1:1fEHWurg7pvf5SG6XNE5Q8UZmOwex51Mkx3SLhrW5B4=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mailru/easyjson v0.9.2 h1:dX8U45hQsZpxd80nLvDGihsQ/OxlvTkVUXH2r/8cb2M=
|
||||
github.com/mailru/easyjson v0.9.2/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||
github.com/mattn/go-pointer v0.0.1/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc=
|
||||
github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w=
|
||||
github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU=
|
||||
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
|
||||
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
|
||||
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
|
||||
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
|
||||
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/nlpodyssey/gopickle v0.3.0/go.mod h1:f070HJ/yR+eLi5WmM1OXJEGaTpuJEUiib19olXgYha0=
|
||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||
github.com/ollama/ollama v0.18.1 h1:7K6anW64C2keASpToYfuOa00LuP8aCmofLKcT2c1mlY=
|
||||
github.com/ollama/ollama v0.18.1/go.mod h1:tCX4IMV8DHjl3zY0THxuEkpWDZSOchJpzTuLACpMwFw=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||
github.com/pdevine/tensor v0.0.0-20240510204454-f88f4562727c/go.mod h1:PSojXDXF7TbgQiD6kkd98IHOS0QqTyUEaWRiS8+BLu8=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/qdrant/go-client v1.17.1 h1:7QmPwDddrHL3hC4NfycwtQlraVKRLcRi++BX6TTm+3g=
|
||||
github.com/qdrant/go-client v1.17.1/go.mod h1:n1h6GhkdAzcohoXt/5Z19I2yxbCkMA6Jejob3S6NZT8=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
|
||||
github.com/shirou/gopsutil/v4 v4.26.1/go.mod h1:medLI9/UNAb0dOI9Q3/7yWSqKkj00u+1tgY8nvv41pc=
|
||||
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
|
||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
||||
github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/testcontainers/testcontainers-go v0.40.0/go.mod h1:FSXV5KQtX2HAMlm7U3APNyLkkap35zNLxukw9oBi/MY=
|
||||
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
|
||||
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
|
||||
github.com/tkrajina/go-reflector v0.5.5/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
|
||||
github.com/tkrajina/typescriptify-golang-structs v0.2.0/go.mod h1:sjU00nti/PMEOZb07KljFlR+lJ+RotsC0GBQMv9EKls=
|
||||
github.com/tree-sitter/go-tree-sitter v0.25.0/go.mod h1:r77ig7BikoZhHrrsjAnv8RqGti5rtSyvDHPzgTPsUuU=
|
||||
github.com/tree-sitter/tree-sitter-cpp v0.23.4/go.mod h1:doqNW64BriC7WBCQ1klf0KmJpdEvfxyXtoEybnBo6v8=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
github.com/xtgo/set v1.0.0/go.mod h1:d3NHzGzSa0NmB2NhFyECA+QdRp29oEn2xbT+TpeFoM8=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.39.0/go.mod h1:t/OGqzHBa5v6RHZwrDBJ2OirWc+4q/w2fTbLZwAKjTk=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=
|
||||
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
|
||||
go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
|
||||
go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
|
||||
go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
|
||||
go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo=
|
||||
go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0/go.mod h1:khvBS2IggMFNwZK/6lEeHg/W57h/IX6J4URh57fuI40=
|
||||
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
|
||||
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc=
|
||||
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
|
||||
go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
|
||||
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
|
||||
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20231121144256-b99613f794b6/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
|
||||
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA=
|
||||
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ=
|
||||
golang.org/x/image v0.22.0/go.mod h1:9hPFhljd4zZ1GNSIZJ49sqbp45GKK9t6w+iXvGqZUz4=
|
||||
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
||||
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
|
|
@ -116,8 +274,12 @@ golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
|
|||
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
|
||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260316180232-0b37fe3546d5 h1:aJmi6DVGGIStN9Mobk/tZOOQUBbj0BPjZjjnOdoZKts=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260316180232-0b37fe3546d5/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
|
||||
|
|
@ -129,3 +291,9 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN
|
|||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorgonia.org/vecf32 v0.9.0/go.mod h1:NCc+5D2oxddRL11hd+pCB1PEyXWOyiQxfZ/1wwhOXCA=
|
||||
gorgonia.org/vecf64 v0.9.0/go.mod h1:hp7IOWCnRiVQKON73kkC/AUMtEXyf9kGlVrtPQ9ccVA=
|
||||
modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||
modernc.org/sqlite v1.47.0/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig=
|
||||
|
|
|
|||
57
specs/ai.md
Normal file
57
specs/ai.md
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
# ai
|
||||
|
||||
**Import:** `dappco.re/go/core/ai/ai`
|
||||
**Files:** 3
|
||||
|
||||
Package ai provides the unified AI package for the core CLI.
|
||||
|
||||
It composes functionality from pkg/rag (vector search) and pkg/agentic
|
||||
(task management) into a single public API surface. New AI features
|
||||
should be added here; existing packages remain importable but pkg/ai
|
||||
is the canonical entry point.
|
||||
|
||||
Sub-packages composed:
|
||||
- pkg/rag: Qdrant vector database + Ollama embeddings
|
||||
- pkg/agentic: Task queue client and context building
|
||||
|
||||
## Types
|
||||
|
||||
### Event
|
||||
- **File:** metrics.go
|
||||
- **Purpose:** Event represents a recorded AI/security metric event.
|
||||
- **Fields:**
|
||||
- `Type string` — Type.
|
||||
- `Timestamp time.Time` — Timestamp.
|
||||
- `AgentID string` — Agent ID.
|
||||
- `Repo string` — Repo.
|
||||
- `Duration time.Duration` — Duration.
|
||||
- `Data map[string]any` — Data.
|
||||
|
||||
### TaskInfo
|
||||
- **File:** rag.go
|
||||
- **Purpose:** TaskInfo carries the minimal task data needed for RAG queries, avoiding a direct dependency on pkg/agentic (which imports pkg/ai).
|
||||
- **Fields:**
|
||||
- `Title string` — Title.
|
||||
- `Description string` — Description.
|
||||
|
||||
## Functions
|
||||
|
||||
### QueryRAGForTask
|
||||
- **File:** rag.go
|
||||
- **Signature:** `func QueryRAGForTask(task TaskInfo) (string, error)`
|
||||
- **Purpose:** QueryRAGForTask queries Qdrant for documentation relevant to a task.
|
||||
|
||||
### ReadEvents
|
||||
- **File:** metrics.go
|
||||
- **Signature:** `func ReadEvents(since time.Time) ([]Event, error)`
|
||||
- **Purpose:** ReadEvents reads events from JSONL files within the given time range.
|
||||
|
||||
### Record
|
||||
- **File:** metrics.go
|
||||
- **Signature:** `func Record(event Event) (err error)`
|
||||
- **Purpose:** Record appends an event to the daily JSONL file at ~/.core/ai/metrics/YYYY-MM-DD.jsonl.
|
||||
|
||||
### Summary
|
||||
- **File:** metrics.go
|
||||
- **Signature:** `func Summary(events []Event) map[string]any`
|
||||
- **Purpose:** Summary aggregates events into counts by type, repo, and agent.
|
||||
55
specs/ai/RFC.md
Normal file
55
specs/ai/RFC.md
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
# ai
|
||||
|
||||
**Import:** `dappco.re/go/core/ai/ai`
|
||||
**Files:** 3
|
||||
|
||||
Package ai provides the unified AI package for the core CLI.
|
||||
|
||||
It composes functionality from pkg/rag (vector search) and pkg/agentic
|
||||
(task management) into a single public API surface. New AI features
|
||||
should be added here; existing packages remain importable but pkg/ai
|
||||
is the canonical entry point.
|
||||
|
||||
## Types
|
||||
|
||||
### Event
|
||||
- **Kind:** `struct`
|
||||
- **File:** `metrics.go`
|
||||
- **Purpose:** Event represents a recorded AI/security metric event.
|
||||
- **Fields:**
|
||||
- `Type string`
|
||||
- `Timestamp time.Time`
|
||||
- `AgentID string`
|
||||
- `Repo string`
|
||||
- `Duration time.Duration`
|
||||
- `Data map[string]any`
|
||||
|
||||
### TaskInfo
|
||||
- **Kind:** `struct`
|
||||
- **File:** `rag.go`
|
||||
- **Purpose:** TaskInfo carries the minimal task data needed for RAG queries, avoiding a direct dependency on pkg/agentic (which imports pkg/ai).
|
||||
- **Fields:**
|
||||
- `Title string`
|
||||
- `Description string`
|
||||
|
||||
## Functions
|
||||
|
||||
### QueryRAGForTask
|
||||
- **File:** `rag.go`
|
||||
- **Signature:** `func QueryRAGForTask(task TaskInfo) (string, error)`
|
||||
- **Purpose:** QueryRAGForTask concatenates the task title and description, truncates the query to 500 runes, queries the `hostuk-docs` collection with `Limit: 3` and `Threshold: 0.5`, and returns `rag.FormatResultsContext(results)`.
|
||||
|
||||
### ReadEvents
|
||||
- **File:** `metrics.go`
|
||||
- **Signature:** `func ReadEvents(since time.Time) ([]Event, error)`
|
||||
- **Purpose:** ReadEvents reads daily JSONL metric files from the day containing `since` through `time.Now()`, returning only events whose `Timestamp` is not before `since`.
|
||||
|
||||
### Record
|
||||
- **File:** `metrics.go`
|
||||
- **Signature:** `func Record(event Event) (err error)`
|
||||
- **Purpose:** Record fills a zero timestamp with `time.Now()`, ensures the metrics directory exists, and appends one JSON line to `~/.core/ai/metrics/YYYY-MM-DD.jsonl`.
|
||||
|
||||
### Summary
|
||||
- **File:** `metrics.go`
|
||||
- **Signature:** `func Summary(events []Event) map[string]any`
|
||||
- **Purpose:** Summary returns aggregate counts under the keys `total`, `by_type`, `by_repo`, and `by_agent`; grouped entries are sorted by count descending and then key ascending.
|
||||
20
specs/cmd/embed-bench.md
Normal file
20
specs/cmd/embed-bench.md
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# main
|
||||
|
||||
**Import:** `dappco.re/go/core/ai/cmd/embed-bench`
|
||||
**Files:** 1
|
||||
|
||||
embed-bench compares embedding models for OpenBrain by testing how well
|
||||
they separate semantically related vs unrelated agent memory pairs.
|
||||
|
||||
Usage:
|
||||
|
||||
go run ./cmd/embed-bench
|
||||
go run ./cmd/embed-bench -ollama http://localhost:11434
|
||||
|
||||
## Types
|
||||
|
||||
None.
|
||||
|
||||
## Functions
|
||||
|
||||
None.
|
||||
15
specs/cmd/lab.md
Normal file
15
specs/cmd/lab.md
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# lab
|
||||
|
||||
**Import:** `dappco.re/go/core/ai/cmd/lab`
|
||||
**Files:** 1
|
||||
|
||||
## Types
|
||||
|
||||
None.
|
||||
|
||||
## Functions
|
||||
|
||||
### AddLabCommands
|
||||
- **File:** cmd_lab.go
|
||||
- **Signature:** `func AddLabCommands(root *cli.Command)`
|
||||
- **Purpose:** AddLabCommands registers the 'lab' command and subcommands.
|
||||
17
specs/cmd/metrics.md
Normal file
17
specs/cmd/metrics.md
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# metrics
|
||||
|
||||
**Import:** `dappco.re/go/core/ai/cmd/metrics`
|
||||
**Files:** 1
|
||||
|
||||
Package metrics implements the metrics viewing command.
|
||||
|
||||
## Types
|
||||
|
||||
None.
|
||||
|
||||
## Functions
|
||||
|
||||
### AddMetricsCommand
|
||||
- **File:** cmd.go
|
||||
- **Signature:** `func AddMetricsCommand(parent *cli.Command)`
|
||||
- **Purpose:** AddMetricsCommand adds the 'metrics' command to the parent.
|
||||
21
specs/cmd/rag.md
Normal file
21
specs/cmd/rag.md
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# rag
|
||||
|
||||
**Import:** `dappco.re/go/core/ai/cmd/rag`
|
||||
**Files:** 1
|
||||
|
||||
Package rag re-exports go-rag's CLI commands for use in the core CLI.
|
||||
|
||||
## Variables
|
||||
|
||||
### AddRAGSubcommands
|
||||
- **File:** cmd.go
|
||||
- **Signature:** `var AddRAGSubcommands = ragcmd.AddRAGSubcommands`
|
||||
- **Purpose:** AddRAGSubcommands registers RAG commands as subcommands of parent.
|
||||
|
||||
## Types
|
||||
|
||||
None.
|
||||
|
||||
## Functions
|
||||
|
||||
None.
|
||||
111
specs/cmd/security.md
Normal file
111
specs/cmd/security.md
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
# security
|
||||
|
||||
**Import:** `dappco.re/go/core/ai/cmd/security`
|
||||
**Files:** 8
|
||||
|
||||
## Types
|
||||
|
||||
### AlertOutput
|
||||
- **File:** cmd_alerts.go
|
||||
- **Purpose:** AlertOutput represents a unified alert for output.
|
||||
- **Fields:**
|
||||
- `Repo string` — Repo.
|
||||
- `Severity string` — Severity.
|
||||
- `ID string` — ID.
|
||||
- `Package string` — Package.
|
||||
- `Version string` — Version.
|
||||
- `Location string` — Location.
|
||||
- `Type string` — Type.
|
||||
- `Message string` — Message.
|
||||
|
||||
### AlertSummary
|
||||
- **File:** cmd_security.go
|
||||
- **Purpose:** AlertSummary holds aggregated alert counts.
|
||||
- **Fields:**
|
||||
- `Critical int` — Critical.
|
||||
- `High int` — High.
|
||||
- `Medium int` — Medium.
|
||||
- `Low int` — Low.
|
||||
- `Unknown int` — Unknown.
|
||||
- `Total int` — Total.
|
||||
- **Methods:**
|
||||
- `func (s *AlertSummary) Add(severity string)` (cmd_security.go) — Add increments summary counters for the provided severity.
|
||||
- `func (s *AlertSummary) PlainString() string` (cmd_security.go) — PlainString renders an unstyled summary suitable for logs and issue bodies.
|
||||
- `func (s *AlertSummary) String() string` (cmd_security.go) — String renders a human-readable summary of alert counts.
|
||||
|
||||
### CodeScanningAlert
|
||||
- **File:** cmd_security.go
|
||||
- **Purpose:** CodeScanningAlert represents a code scanning alert.
|
||||
- **Fields:**
|
||||
- `Number int` — Number.
|
||||
- `State string` — State.
|
||||
- `DismissedReason string` — Dismissed Reason.
|
||||
- `Rule struct{...}` — Rule.
|
||||
- `Tool struct{...}` — Tool.
|
||||
- `MostRecentInstance struct{...}` — Most Recent Instance.
|
||||
|
||||
### DepAlert
|
||||
- **File:** cmd_deps.go
|
||||
- **Purpose:** DepAlert represents a dependency vulnerability for output.
|
||||
- **Fields:**
|
||||
- `Repo string` — Repo.
|
||||
- `Severity string` — Severity.
|
||||
- `CVE string` — CVE.
|
||||
- `Package string` — Package.
|
||||
- `Ecosystem string` — Ecosystem.
|
||||
- `Vulnerable string` — Vulnerable.
|
||||
- `PatchedVersion string` — Patched Version.
|
||||
- `Manifest string` — Manifest.
|
||||
- `Summary string` — Summary.
|
||||
|
||||
### DependabotAlert
|
||||
- **File:** cmd_security.go
|
||||
- **Purpose:** DependabotAlert represents a Dependabot vulnerability alert.
|
||||
- **Fields:**
|
||||
- `Number int` — Number.
|
||||
- `State string` — State.
|
||||
- `Advisory struct{...}` — Advisory.
|
||||
- `Dependency struct{...}` — Dependency.
|
||||
- `SecurityVulnerability struct{...}` — Security Vulnerability.
|
||||
|
||||
### ScanAlert
|
||||
- **File:** cmd_scan.go
|
||||
- **Purpose:** ScanAlert represents a code scanning alert for output.
|
||||
- **Fields:**
|
||||
- `Repo string` — Repo.
|
||||
- `Severity string` — Severity.
|
||||
- `RuleID string` — Rule ID.
|
||||
- `Tool string` — Tool.
|
||||
- `Path string` — Path.
|
||||
- `Line int` — Line.
|
||||
- `Description string` — Description.
|
||||
- `Message string` — Message.
|
||||
|
||||
### SecretAlert
|
||||
- **File:** cmd_secrets.go
|
||||
- **Purpose:** SecretAlert represents a secret scanning alert for output.
|
||||
- **Fields:**
|
||||
- `Repo string` — Repo.
|
||||
- `Number int` — Number.
|
||||
- `SecretType string` — Secret Type.
|
||||
- `State string` — State.
|
||||
- `Resolution string` — Resolution.
|
||||
- `PushProtection bool` — Push Protection.
|
||||
|
||||
### SecretScanningAlert
|
||||
- **File:** cmd_security.go
|
||||
- **Purpose:** SecretScanningAlert represents a secret scanning alert.
|
||||
- **Fields:**
|
||||
- `Number int` — Number.
|
||||
- `State string` — State.
|
||||
- `SecretType string` — Secret Type.
|
||||
- `Secret string` — Secret.
|
||||
- `PushProtection bool` — Push Protection.
|
||||
- `Resolution string` — Resolution.
|
||||
|
||||
## Functions
|
||||
|
||||
### AddSecurityCommands
|
||||
- **File:** cmd_security.go
|
||||
- **Signature:** `func AddSecurityCommands(root *cli.Command)`
|
||||
- **Purpose:** AddSecurityCommands adds the 'security' command to the root.
|
||||
18
specs/embed-bench/RFC.md
Normal file
18
specs/embed-bench/RFC.md
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# main
|
||||
|
||||
**Import:** `dappco.re/go/core/ai/cmd/embed-bench`
|
||||
**Files:** 1
|
||||
|
||||
Package main benchmarks Ollama embedding models against grouped memory
|
||||
and query examples.
|
||||
|
||||
It compares how well candidate embedding models separate semantically
|
||||
related agent-memory pairs from unrelated ones.
|
||||
|
||||
## Types
|
||||
|
||||
None.
|
||||
|
||||
## Functions
|
||||
|
||||
None.
|
||||
17
specs/lab/RFC.md
Normal file
17
specs/lab/RFC.md
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# lab
|
||||
|
||||
**Import:** `dappco.re/go/core/ai/cmd/lab`
|
||||
**Files:** 1
|
||||
|
||||
`cmd_lab.go` is guarded by `//go:build ignore`.
|
||||
|
||||
## Types
|
||||
|
||||
None.
|
||||
|
||||
## Functions
|
||||
|
||||
### AddLabCommands
|
||||
- **File:** `cmd_lab.go`
|
||||
- **Signature:** `func AddLabCommands(root *cli.Command)`
|
||||
- **Purpose:** AddLabCommands adds the `lab` root command and registers its `serve` subcommand on `root`.
|
||||
17
specs/metrics/RFC.md
Normal file
17
specs/metrics/RFC.md
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# metrics
|
||||
|
||||
**Import:** `dappco.re/go/core/ai/cmd/metrics`
|
||||
**Files:** 1
|
||||
|
||||
Package metrics implements the metrics viewing command.
|
||||
|
||||
## Types
|
||||
|
||||
None.
|
||||
|
||||
## Functions
|
||||
|
||||
### AddMetricsCommand
|
||||
- **File:** `cmd.go`
|
||||
- **Signature:** `func AddMetricsCommand(parent *cli.Command)`
|
||||
- **Purpose:** AddMetricsCommand initializes the `metrics` command flags and adds the command to `parent`.
|
||||
21
specs/rag/RFC.md
Normal file
21
specs/rag/RFC.md
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# rag
|
||||
|
||||
**Import:** `dappco.re/go/core/ai/cmd/rag`
|
||||
**Files:** 1
|
||||
|
||||
Package rag re-exports go-rag's CLI commands for use in the core CLI.
|
||||
|
||||
## Variables
|
||||
|
||||
### AddRAGSubcommands
|
||||
- **File:** `cmd.go`
|
||||
- **Declaration:** `var AddRAGSubcommands = ragcmd.AddRAGSubcommands`
|
||||
- **Purpose:** AddRAGSubcommands re-exports `ragcmd.AddRAGSubcommands`.
|
||||
|
||||
## Types
|
||||
|
||||
None.
|
||||
|
||||
## Functions
|
||||
|
||||
None.
|
||||
122
specs/security/RFC.md
Normal file
122
specs/security/RFC.md
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
# security
|
||||
|
||||
**Import:** `dappco.re/go/core/ai/cmd/security`
|
||||
**Files:** 8
|
||||
|
||||
The package exports the `security` command registration entry point and
|
||||
the alert/output structs used by the security subcommands.
|
||||
|
||||
## Types
|
||||
|
||||
### AlertOutput
|
||||
- **Kind:** `struct`
|
||||
- **File:** `cmd_alerts.go`
|
||||
- **Purpose:** AlertOutput represents a unified alert for output.
|
||||
- **Fields:**
|
||||
- `Repo string`
|
||||
- `Severity string`
|
||||
- `ID string`
|
||||
- `Package string`
|
||||
- `Version string`
|
||||
- `Location string`
|
||||
- `Type string`
|
||||
- `Message string`
|
||||
|
||||
### AlertSummary
|
||||
- **Kind:** `struct`
|
||||
- **File:** `cmd_security.go`
|
||||
- **Purpose:** AlertSummary holds aggregated alert counts.
|
||||
- **Fields:**
|
||||
- `Critical int`
|
||||
- `High int`
|
||||
- `Medium int`
|
||||
- `Low int`
|
||||
- `Unknown int`
|
||||
- `Total int`
|
||||
- **Methods:**
|
||||
- `func (s *AlertSummary) Add(severity string)` - increments the summary counters for the provided severity.
|
||||
- `func (s *AlertSummary) String() string` - renders a styled summary string.
|
||||
- `func (s *AlertSummary) PlainString() string` - renders an unstyled summary string for logs and issue bodies.
|
||||
|
||||
### CodeScanningAlert
|
||||
- **Kind:** `struct`
|
||||
- **File:** `cmd_security.go`
|
||||
- **Purpose:** CodeScanningAlert represents a code scanning alert.
|
||||
- **Fields:**
|
||||
- `Number int`
|
||||
- `State string`
|
||||
- `DismissedReason string`
|
||||
- `Rule struct{ ID string; Severity string; Description string; Tags []string }`
|
||||
- `Tool struct{ Name string; Version string }`
|
||||
- `MostRecentInstance struct{ Location struct{ Path string; StartLine int; EndLine int }; Message struct{ Text string } }`
|
||||
|
||||
### DepAlert
|
||||
- **Kind:** `struct`
|
||||
- **File:** `cmd_deps.go`
|
||||
- **Purpose:** DepAlert represents a dependency vulnerability for output.
|
||||
- **Fields:**
|
||||
- `Repo string`
|
||||
- `Severity string`
|
||||
- `CVE string`
|
||||
- `Package string`
|
||||
- `Ecosystem string`
|
||||
- `Vulnerable string`
|
||||
- `PatchedVersion string`
|
||||
- `Manifest string`
|
||||
- `Summary string`
|
||||
|
||||
### DependabotAlert
|
||||
- **Kind:** `struct`
|
||||
- **File:** `cmd_security.go`
|
||||
- **Purpose:** DependabotAlert represents a Dependabot vulnerability alert.
|
||||
- **Fields:**
|
||||
- `Number int`
|
||||
- `State string`
|
||||
- `Advisory struct{ Severity string; CVEID string; Summary string; Description string }`
|
||||
- `Dependency struct{ Package struct{ Name string; Ecosystem string }; ManifestPath string }`
|
||||
- `SecurityVulnerability struct{ Package struct{ Name string; Ecosystem string }; FirstPatchedVersion struct{ Identifier string }; VulnerableVersionRange string }`
|
||||
|
||||
### ScanAlert
|
||||
- **Kind:** `struct`
|
||||
- **File:** `cmd_scan.go`
|
||||
- **Purpose:** ScanAlert represents a code scanning alert for output.
|
||||
- **Fields:**
|
||||
- `Repo string`
|
||||
- `Severity string`
|
||||
- `RuleID string`
|
||||
- `Tool string`
|
||||
- `Path string`
|
||||
- `Line int`
|
||||
- `Description string`
|
||||
- `Message string`
|
||||
|
||||
### SecretAlert
|
||||
- **Kind:** `struct`
|
||||
- **File:** `cmd_secrets.go`
|
||||
- **Purpose:** SecretAlert represents a secret scanning alert for output.
|
||||
- **Fields:**
|
||||
- `Repo string`
|
||||
- `Number int`
|
||||
- `SecretType string`
|
||||
- `State string`
|
||||
- `Resolution string`
|
||||
- `PushProtection bool`
|
||||
|
||||
### SecretScanningAlert
|
||||
- **Kind:** `struct`
|
||||
- **File:** `cmd_security.go`
|
||||
- **Purpose:** SecretScanningAlert represents a secret scanning alert.
|
||||
- **Fields:**
|
||||
- `Number int`
|
||||
- `State string`
|
||||
- `SecretType string`
|
||||
- `Secret string`
|
||||
- `PushProtection bool`
|
||||
- `Resolution string`
|
||||
|
||||
## Functions
|
||||
|
||||
### AddSecurityCommands
|
||||
- **File:** `cmd_security.go`
|
||||
- **Signature:** `func AddSecurityCommands(root *cli.Command)`
|
||||
- **Purpose:** AddSecurityCommands creates the `security` command and registers the `alerts`, `deps`, `scan`, `secrets`, and `jobs` subcommands on `root`.
|
||||
Loading…
Add table
Reference in a new issue