Initial commit: knox knowledge index
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/david/knox/internal/db"
|
||||
"github.com/david/knox/internal/ingest"
|
||||
)
|
||||
|
||||
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 ""
|
||||
}
|
||||
Reference in New Issue
Block a user