Skip to content

Commit 17c3d40

Browse files
committed
feat: complete archive parity migration — advanced compaction, OAuth, OTel, vim, routing, bridge, and more
Implements all remaining features from hawk-archive to bring Go to ~98% parity: - Advanced compaction: 5 strategies (micro, session memory, API-level, smart, truncate) with circuit breaker, archive-compatible prompt engineering, and <analysis> stripping - OAuth2 PKCE: full flow with callback server, token refresh, profile fetch, keychain storage - OpenTelemetry: SDK integration with OTLP HTTP exporter, span helpers, env-based config - Vim mode: complete state machine (motions, operators, find, text objects, dot-repeat) - Smart provider routing: health tracking, latency EMA, circuit breaker, fallback chain - Remote/Bridge: Transport interface, WebSocket (nhooyr.io/websocket), SSE, SSH tunnel, bridge protocol, JWT refresh scheduler, echo dedup, FlushGate - Coordinator mode: system prompt, worker tool context, task manager with lifecycle - Image/PDF: file read dispatch, base64 encoding, resize, PDF text extraction - Context visualization: token bar, breakdown, 4-state warning - Plugin marketplace: search, featured, install with version lock, upgrade - Session teleport: export/import with SHA-256 checksum verification - Session rewind: checkpoint listing, rewind-to-index, undo last exchange - AutoDream: background memory consolidation with time+session gates - Stats/Insights: usage statistics with heatmap, cross-session analysis, HTML reports - Prompt suggestions: speculative generation with history fallback All 32 packages build, pass tests, and vet clean.
1 parent e869480 commit 17c3d40

48 files changed

Lines changed: 7976 additions & 38 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎analytics/insights.go‎

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
package analytics
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
"sort"
9+
"strings"
10+
"time"
11+
)
12+
13+
// InsightsFacet represents an extracted insight category.
14+
type InsightsFacet struct {
15+
Category string `json:"category"` // goals, outcomes, friction, success
16+
Items []string `json:"items"`
17+
}
18+
19+
// InsightsReport holds a complete cross-session analysis.
20+
type InsightsReport struct {
21+
GeneratedAt time.Time `json:"generated_at"`
22+
SessionsScanned int `json:"sessions_scanned"`
23+
DateRange DateRange `json:"date_range"`
24+
Facets []InsightsFacet `json:"facets"`
25+
TopPatterns []string `json:"top_patterns"`
26+
Recommendations []string `json:"recommendations"`
27+
Stats *SessionStats `json:"stats,omitempty"`
28+
}
29+
30+
// GenerateInsights analyzes session transcripts to extract patterns and recommendations.
31+
func GenerateInsights(days int, analysisFn func(content string) ([]InsightsFacet, error)) (*InsightsReport, error) {
32+
sessDir := sessionsDir()
33+
entries, err := os.ReadDir(sessDir)
34+
if err != nil {
35+
return nil, fmt.Errorf("reading sessions: %w", err)
36+
}
37+
38+
cutoff := time.Now().AddDate(0, 0, -days)
39+
var transcripts []string
40+
scanned := 0
41+
42+
for _, e := range entries {
43+
if e.IsDir() {
44+
continue
45+
}
46+
info, err := e.Info()
47+
if err != nil || info.ModTime().Before(cutoff) {
48+
continue
49+
}
50+
if filepath.Ext(e.Name()) != ".jsonl" {
51+
continue
52+
}
53+
54+
data, err := os.ReadFile(filepath.Join(sessDir, e.Name()))
55+
if err != nil {
56+
continue
57+
}
58+
transcripts = append(transcripts, string(data))
59+
scanned++
60+
}
61+
62+
if scanned == 0 {
63+
return nil, fmt.Errorf("no sessions found in the last %d days", days)
64+
}
65+
66+
// Extract facets via LLM analysis if provided
67+
var facets []InsightsFacet
68+
if analysisFn != nil {
69+
combined := strings.Join(transcripts, "\n---SESSION BREAK---\n")
70+
if len(combined) > 50000 {
71+
combined = combined[:50000]
72+
}
73+
facets, err = analysisFn(combined)
74+
if err != nil {
75+
// Non-fatal: continue with stats-only report
76+
facets = nil
77+
}
78+
}
79+
80+
// Compute patterns from tool usage
81+
patterns := extractPatterns(transcripts)
82+
83+
report := &InsightsReport{
84+
GeneratedAt: time.Now(),
85+
SessionsScanned: scanned,
86+
DateRange: DateRange{Start: cutoff, End: time.Now()},
87+
Facets: facets,
88+
TopPatterns: patterns,
89+
Recommendations: generateRecommendations(patterns, scanned),
90+
}
91+
92+
return report, nil
93+
}
94+
95+
// ExportInsightsHTML generates an HTML report from insights.
96+
func ExportInsightsHTML(report *InsightsReport) string {
97+
var b strings.Builder
98+
b.WriteString(`<!DOCTYPE html><html><head><title>Hawk Insights</title>
99+
<style>
100+
body { font-family: -apple-system, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
101+
h1 { color: #333; } h2 { color: #555; border-bottom: 1px solid #eee; }
102+
.stat { display: inline-block; margin: 10px; padding: 15px; background: #f5f5f5; border-radius: 8px; }
103+
.pattern { padding: 8px; margin: 4px 0; background: #e8f4fd; border-radius: 4px; }
104+
.rec { padding: 8px; margin: 4px 0; background: #e8fde8; border-radius: 4px; }
105+
</style></head><body>`)
106+
107+
b.WriteString(fmt.Sprintf("<h1>Hawk Insights Report</h1><p>Generated: %s | Sessions: %d</p>",
108+
report.GeneratedAt.Format("2006-01-02 15:04"), report.SessionsScanned))
109+
110+
if len(report.TopPatterns) > 0 {
111+
b.WriteString("<h2>Top Patterns</h2>")
112+
for _, p := range report.TopPatterns {
113+
b.WriteString(fmt.Sprintf(`<div class="pattern">%s</div>`, p))
114+
}
115+
}
116+
117+
if len(report.Facets) > 0 {
118+
for _, f := range report.Facets {
119+
b.WriteString(fmt.Sprintf("<h2>%s</h2><ul>", f.Category))
120+
for _, item := range f.Items {
121+
b.WriteString(fmt.Sprintf("<li>%s</li>", item))
122+
}
123+
b.WriteString("</ul>")
124+
}
125+
}
126+
127+
if len(report.Recommendations) > 0 {
128+
b.WriteString("<h2>Recommendations</h2>")
129+
for _, r := range report.Recommendations {
130+
b.WriteString(fmt.Sprintf(`<div class="rec">%s</div>`, r))
131+
}
132+
}
133+
134+
b.WriteString("</body></html>")
135+
return b.String()
136+
}
137+
138+
// SaveInsightsReport saves an HTML report to disk.
139+
func SaveInsightsReport(report *InsightsReport) (string, error) {
140+
home, _ := os.UserHomeDir()
141+
dir := filepath.Join(home, ".hawk", "insights")
142+
if err := os.MkdirAll(dir, 0o755); err != nil {
143+
return "", err
144+
}
145+
146+
filename := fmt.Sprintf("insights_%s.html", report.GeneratedAt.Format("2006-01-02"))
147+
path := filepath.Join(dir, filename)
148+
149+
html := ExportInsightsHTML(report)
150+
return path, os.WriteFile(path, []byte(html), 0o644)
151+
}
152+
153+
func extractPatterns(transcripts []string) []string {
154+
toolFreq := make(map[string]int)
155+
for _, t := range transcripts {
156+
for _, line := range strings.Split(t, "\n") {
157+
var event map[string]interface{}
158+
if json.Unmarshal([]byte(line), &event) != nil {
159+
continue
160+
}
161+
if tool, ok := event["tool"].(string); ok {
162+
toolFreq[tool]++
163+
}
164+
}
165+
}
166+
167+
type kv struct {
168+
k string
169+
v int
170+
}
171+
var sorted []kv
172+
for k, v := range toolFreq {
173+
sorted = append(sorted, kv{k, v})
174+
}
175+
sort.Slice(sorted, func(i, j int) bool { return sorted[i].v > sorted[j].v })
176+
177+
var patterns []string
178+
for i, item := range sorted {
179+
if i >= 5 {
180+
break
181+
}
182+
patterns = append(patterns, fmt.Sprintf("Heavy %s usage (%d calls)", item.k, item.v))
183+
}
184+
return patterns
185+
}
186+
187+
func generateRecommendations(patterns []string, sessionCount int) []string {
188+
var recs []string
189+
if sessionCount > 20 {
190+
recs = append(recs, "Consider using /compact more frequently to keep context fresh")
191+
}
192+
for _, p := range patterns {
193+
if strings.Contains(p, "Read") {
194+
recs = append(recs, "High file read count suggests exploring unfamiliar code — consider using /init to generate documentation")
195+
}
196+
if strings.Contains(p, "Bash") {
197+
recs = append(recs, "Frequent shell usage — consider adding common commands to allowed permissions to reduce prompts")
198+
}
199+
}
200+
return recs
201+
}
202+
203+
func sessionsDir() string {
204+
home, _ := os.UserHomeDir()
205+
return filepath.Join(home, ".hawk", "sessions")
206+
}

0 commit comments

Comments
 (0)