116 lines
2.8 KiB
Go
116 lines
2.8 KiB
Go
package cmd
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"strings"
|
|
|
|
"github.com/david/knox/internal/db"
|
|
"github.com/david/knox/internal/ingest"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
func NewBrowserCmd(kdb *db.KnoxDB) *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "browser",
|
|
Short: "Ingest browser history from Chromium and Min into the knowledge index",
|
|
Long: `Reads browser history databases (Chromium and Min), fingerprints URLs,
|
|
and stores them as observations. Tracks what you've searched for and read.
|
|
|
|
Run periodically to capture browsing exhaust.`,
|
|
RunE: func(c *cobra.Command, args []string) error {
|
|
ing := ingest.NewBrowserHistoryIngester()
|
|
log.Printf("[knox] ingesting browser history...")
|
|
|
|
results, err := ing.IngestAll()
|
|
if err != nil {
|
|
return fmt.Errorf("browser ingest: %w", err)
|
|
}
|
|
|
|
if len(results) == 0 {
|
|
fmt.Println("No browser history found (Chromium/Min not detected or empty).")
|
|
return nil
|
|
}
|
|
|
|
var newCount, dupCount int
|
|
for _, result := range results {
|
|
_, isNew, err := kdb.RecordObservation(db.ObservationRecord{
|
|
Fingerprint: result.Fingerprint,
|
|
SourceID: result.SourceID,
|
|
SourcePath: result.SourcePath,
|
|
ContentType: result.ContentType,
|
|
Title: result.Title,
|
|
Summary: result.Summary,
|
|
CreatedAt: result.CreatedAt,
|
|
Confidence: result.Confidence,
|
|
IngesterVersion: result.IngesterVersion,
|
|
Trigger: "browser_ingest",
|
|
Provenance: ingest.ProvenanceJSON(result.Provenance),
|
|
})
|
|
if err != nil {
|
|
log.Printf("[knox] db error: %v", err)
|
|
continue
|
|
}
|
|
if isNew {
|
|
newCount++
|
|
} else {
|
|
dupCount++
|
|
}
|
|
}
|
|
|
|
fmt.Printf("Browser history: %d new, %d already seen\n", newCount, dupCount)
|
|
|
|
// Show top domains
|
|
domains := make(map[string]int)
|
|
for _, r := range results {
|
|
domains[extractDomain(r.Summary)]++
|
|
}
|
|
fmt.Println("\nTop domains:")
|
|
for _, d := range topDomains(domains, 10) {
|
|
fmt.Printf(" %-40s %d\n", d.Name, d.Count)
|
|
}
|
|
|
|
return nil
|
|
},
|
|
}
|
|
return cmd
|
|
}
|
|
|
|
func topDomains(domains map[string]int, n int) []struct {
|
|
Name string
|
|
Count int
|
|
} {
|
|
var sorted []struct {
|
|
Name string
|
|
Count int
|
|
}
|
|
for name, count := range domains {
|
|
sorted = append(sorted, struct {
|
|
Name string
|
|
Count int
|
|
}{name, count})
|
|
}
|
|
// Simple bubble sort for small n
|
|
for i := 0; i < len(sorted)-1; i++ {
|
|
for j := 0; j < len(sorted)-1-i; j++ {
|
|
if sorted[j].Count < sorted[j+1].Count {
|
|
sorted[j], sorted[j+1] = sorted[j+1], sorted[j]
|
|
}
|
|
}
|
|
}
|
|
if len(sorted) > n {
|
|
sorted = sorted[:n]
|
|
}
|
|
return sorted
|
|
}
|
|
|
|
func extractDomain(url string) string {
|
|
url = strings.TrimPrefix(url, "https://")
|
|
url = strings.TrimPrefix(url, "http://")
|
|
parts := strings.Split(url, "/")
|
|
if len(parts) > 0 {
|
|
return parts[0]
|
|
}
|
|
return ""
|
|
}
|