Files
wehub-resource-sync f99010fae1
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Windows) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:36 +08:00

198 lines
5.5 KiB
Go

package parser
import (
"database/sql"
"fmt"
"os"
"path/filepath"
"sort"
"time"
_ "github.com/mattn/go-sqlite3"
)
type CursorAttribution struct {
ScoredCommits int64 `json:"scored_commits"`
LinesAdded int64 `json:"lines_added"`
LinesDeleted int64 `json:"lines_deleted"`
TabLinesAdded int64 `json:"tab_lines_added"`
TabLinesDeleted int64 `json:"tab_lines_deleted"`
ComposerLinesAdded int64 `json:"composer_lines_added"`
ComposerLinesDeleted int64 `json:"composer_lines_deleted"`
HumanLinesAdded int64 `json:"human_lines_added"`
HumanLinesDeleted int64 `json:"human_lines_deleted"`
BlankLinesAdded int64 `json:"blank_lines_added"`
BlankLinesDeleted int64 `json:"blank_lines_deleted"`
AIAuthoredPct float64 `json:"ai_authored_pct"`
ConversationCounts []CursorConversationCount `json:"conversation_counts,omitempty"`
}
type CursorAttributionStatus string
const (
CursorAttributionAvailable CursorAttributionStatus = "available"
CursorAttributionEmpty CursorAttributionStatus = "empty"
CursorAttributionUnavailable CursorAttributionStatus = "unavailable"
)
type CursorConversationCount struct {
Model string `json:"model"`
Mode string `json:"mode"`
Count int64 `json:"count"`
}
func LoadCursorAttribution(
from, to time.Time,
) (*CursorAttribution, CursorAttributionStatus, error) {
dbPath := cursorAttributionDBPath()
if dbPath == "" {
return nil, CursorAttributionUnavailable, nil
}
if _, err := os.Stat(dbPath); err != nil {
if os.IsNotExist(err) {
return nil, CursorAttributionUnavailable, nil
}
return nil, "", fmt.Errorf("stat cursor attribution db: %w", err)
}
conn, err := openCursorAttributionDB(dbPath)
if err != nil {
return nil, "", err
}
defer conn.Close()
attr := &CursorAttribution{}
if err := conn.QueryRow(
`SELECT
COUNT(*),
COALESCE(SUM(linesAdded), 0),
COALESCE(SUM(linesDeleted), 0),
COALESCE(SUM(tabLinesAdded), 0),
COALESCE(SUM(tabLinesDeleted), 0),
COALESCE(SUM(composerLinesAdded), 0),
COALESCE(SUM(composerLinesDeleted), 0),
COALESCE(SUM(humanLinesAdded), 0),
COALESCE(SUM(humanLinesDeleted), 0),
COALESCE(SUM(blankLinesAdded), 0),
COALESCE(SUM(blankLinesDeleted), 0)
FROM scored_commits
WHERE scoredAt >= ? AND scoredAt < ?`,
timeToMillis(from), timeToMillis(to),
).Scan(
&attr.ScoredCommits,
&attr.LinesAdded,
&attr.LinesDeleted,
&attr.TabLinesAdded,
&attr.TabLinesDeleted,
&attr.ComposerLinesAdded,
&attr.ComposerLinesDeleted,
&attr.HumanLinesAdded,
&attr.HumanLinesDeleted,
&attr.BlankLinesAdded,
&attr.BlankLinesDeleted,
); err != nil {
return nil, "", fmt.Errorf("querying scored_commits: %w", err)
}
rows, err := conn.Query(
`SELECT
COALESCE(model, ''),
COALESCE(mode, ''),
COUNT(*)
FROM conversation_summaries
WHERE updatedAt >= ? AND updatedAt < ?
GROUP BY COALESCE(model, ''), COALESCE(mode, '')
ORDER BY COUNT(*) DESC, COALESCE(model, ''), COALESCE(mode, '')`,
timeToMillis(from), timeToMillis(to),
)
if err != nil {
return nil, "", fmt.Errorf(
"querying conversation_summaries: %w", err,
)
}
defer rows.Close()
for rows.Next() {
var entry CursorConversationCount
if err := rows.Scan(
&entry.Model, &entry.Mode, &entry.Count,
); err != nil {
return nil, "", fmt.Errorf(
"scanning conversation_summaries: %w", err,
)
}
attr.ConversationCounts = append(
attr.ConversationCounts, entry,
)
}
if err := rows.Err(); err != nil {
return nil, "", fmt.Errorf(
"iterating conversation_summaries: %w", err,
)
}
sortCursorConversationCounts(attr.ConversationCounts)
if attr.ScoredCommits == 0 &&
attr.LinesAdded == 0 &&
attr.LinesDeleted == 0 &&
len(attr.ConversationCounts) == 0 {
return nil, CursorAttributionEmpty, nil
}
aiLines := attr.TabLinesAdded + attr.ComposerLinesAdded
denom := attr.LinesAdded
if denom > 0 {
attr.AIAuthoredPct = float64(aiLines) / float64(denom)
}
return attr, CursorAttributionAvailable, nil
}
func cursorAttributionDBPath() string {
if path := os.Getenv("AGENTSVIEW_CURSOR_ATTRIBUTION_DB"); path != "" {
return path
}
home, err := os.UserHomeDir()
if err != nil || home == "" {
return ""
}
return filepath.Join(home, ".cursor", "ai-tracking", "ai-code-tracking.db")
}
func openCursorAttributionDB(path string) (*sql.DB, error) {
// The file: prefix is required for go-sqlite3 to honor mode=ro;
// without it the query string is dropped and the live Cursor db
// would be opened read-write.
conn, err := sql.Open(
"sqlite3",
"file:"+sqliteURIPath(path)+"?mode=ro&_busy_timeout=3000",
)
if err != nil {
return nil, fmt.Errorf("opening cursor attribution db: %w", err)
}
conn.SetMaxOpenConns(1)
if err := conn.Ping(); err != nil {
conn.Close()
return nil, fmt.Errorf("opening cursor attribution db: %w", err)
}
return conn, nil
}
func timeToMillis(t time.Time) int64 {
return t.UTC().UnixNano() / int64(time.Millisecond)
}
func sortCursorConversationCounts(
counts []CursorConversationCount,
) {
sort.Slice(counts, func(i, j int) bool {
if counts[i].Count != counts[j].Count {
return counts[i].Count > counts[j].Count
}
if counts[i].Model != counts[j].Model {
return counts[i].Model < counts[j].Model
}
return counts[i].Mode < counts[j].Mode
})
}