feat: add cursor activity attribution#20
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds a dedicated codetok cursor activity subcommand that reads Cursor's local SQLite tracking database (~/.cursor/ai-tracking/ai-code-tracking.db) and reports composer and tab line-based activity metrics, intentionally kept separate from the existing token-based reporting in daily and session.
Changes:
- New
cursor/activity.gowith activity model, SQLite reader, andService.Activitymethod; newcursor activityCobra subcommand incmd/cursor.gowith--jsonand--db-pathflags - Unit, command, and e2e tests verifying activity reading, command output, and regression ensuring activity data doesn't pollute token reports
- Documentation updates in both READMEs, design/plan docs, and OpenSpec task tracking
Reviewed changes
Copilot reviewed 11 out of 12 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| cursor/activity.go | New activity reader, model types, default DB path resolution, and Service.Activity |
| cmd/cursor.go | New cursor activity subcommand with JSON and table output |
| cursor/activity_test.go | Unit tests for SQLite reader with fixture DBs |
| cmd/cursor_test.go | Command-level tests for activity subcommand |
| e2e/e2e_test.go | E2e tests for activity JSON output and daily/session non-pollution |
| go.mod / go.sum | Added modernc.org/sqlite and transitive dependencies |
| README.md / README_zh.md | Documented cursor activity command and updated API boundary docs |
| docs/plans/*.md | Design and implementation plan documents |
| openspec/changes/cursor-usage-support/tasks.md | Marked completed tasks |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if os.IsNotExist(err) || os.IsPermission(err) { | ||
| return result, nil | ||
| } | ||
| return result, nil |
There was a problem hiding this comment.
Updated Read to keep only os.IsNotExist and os.IsPermission on the no-data path. Unexpected os.Stat failures now return an error with the DB path, and TestReadActivity_UnexpectedStatErrorReturnsError locks that in.
| if strings.Contains(err.Error(), "no such table") { | ||
| return result, nil | ||
| } | ||
| return result, nil |
There was a problem hiding this comment.
Changed the reader to surface unexpected open/query failures with context while still treating no such table as no data. Coverage is in TestReadActivity_OpenDatabaseErrorReturnsError and TestReadActivity_UnexpectedQueryErrorReturnsError.
| func writeCursorActivityDB(t *testing.T, rows []activityFixtureRow) string { | ||
| t.Helper() | ||
|
|
||
| dbPath := filepath.Join(t.TempDir(), "ai-code-tracking.db") | ||
| db, err := sql.Open("sqlite", dbPath) | ||
| if err != nil { | ||
| t.Fatalf("sql.Open returned error: %v", err) | ||
| } | ||
| defer db.Close() | ||
|
|
||
| _, err = db.Exec(` | ||
| CREATE TABLE scored_commits ( | ||
| commitHash TEXT NOT NULL, | ||
| branchName TEXT NOT NULL, | ||
| scoredAt INTEGER NOT NULL, | ||
| linesAdded INTEGER, | ||
| linesDeleted INTEGER, | ||
| tabLinesAdded INTEGER, | ||
| tabLinesDeleted INTEGER, | ||
| composerLinesAdded INTEGER, | ||
| composerLinesDeleted INTEGER, | ||
| humanLinesAdded INTEGER, | ||
| humanLinesDeleted INTEGER, | ||
| blankLinesAdded INTEGER, | ||
| blankLinesDeleted INTEGER, | ||
| commitMessage TEXT, | ||
| commitDate TEXT, | ||
| v1AiPercentage TEXT, | ||
| v2AiPercentage TEXT, | ||
| PRIMARY KEY (commitHash, branchName) | ||
| ); | ||
| `) | ||
| if err != nil { | ||
| t.Fatalf("creating scored_commits table: %v", err) | ||
| } | ||
|
|
||
| for i, row := range rows { | ||
| _, err := db.Exec(` | ||
| INSERT INTO scored_commits ( | ||
| commitHash, branchName, scoredAt, linesAdded, linesDeleted, | ||
| tabLinesAdded, tabLinesDeleted, composerLinesAdded, composerLinesDeleted | ||
| ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) | ||
| `, | ||
| "commit-"+string(rune('a'+i)), | ||
| "main", | ||
| 1000+i, | ||
| row.composerAdded+row.tabAdded, | ||
| row.composerDeleted+row.tabDeleted, | ||
| row.tabAdded, | ||
| row.tabDeleted, | ||
| row.composerAdded, | ||
| row.composerDeleted, | ||
| ) | ||
| if err != nil { | ||
| t.Fatalf("inserting fixture row %d: %v", i, err) | ||
| } | ||
| } | ||
|
|
||
| return dbPath |
There was a problem hiding this comment.
Extracted the shared SQLite fixture builder into internal/testutil/cursor_activity_db.go and switched both cursor/activity_test.go and e2e/e2e_test.go to use it, so the activity schema and insert logic now live in one place.
|
Conflict resolution update:
|
Summary
~/.cursor/ai-tracking/ai-code-tracking.dbcodetok cursor activitywith table and JSON output for separatecomposerandtabattribution metricsprovider.TokenUsage,daily, andsessionChanges
cursor/activity.go: new activity model, default DB path resolution, SQLite reader, andService.Activitycmd/cursor.go: newcursor activitysubcommand with--jsonand--db-pathcursor/activity_test.go,cmd/cursor_test.go,e2e/e2e_test.go: reader, command, and regression coverageREADME.md,README_zh.md: document the separate local activity view and the explicit remote Cursor command boundaryopenspec/changes/cursor-usage-support/tasks.md: mark4.1-4.4,5.1, and5.4completeValidation
make fmtmake vetmake testmake lint(skipped by Makefile becausegolangci-lintis not installed)make build./bin/codetok cursor activity --json./bin/codetok daily --json --all --cursor-dir $(pwd)/e2e/testdata/cursor --claude-dir <empty> --codex-dir <empty> --kimi-dir <empty>Risks / Notes
openspec-verify-changeoropenspec-archive-changebecause the broadercursor-usage-supporttask list still appears to have unresolved items outside this ticket's scoped work