Skip to content

feat: add sync-likes, sync-timeline, and sync-feed commands#43

Open
mmenestret wants to merge 3 commits into
afar1:mainfrom
mmenestret:feat/sync-likes-timeline-feed
Open

feat: add sync-likes, sync-timeline, and sync-feed commands#43
mmenestret wants to merge 3 commits into
afar1:mainfrom
mmenestret:feat/sync-likes-timeline-feed

Conversation

@mmenestret
Copy link
Copy Markdown

@mmenestret mmenestret commented Apr 7, 2026

Summary

  • Adds ft sync-likes <user>, ft sync-timeline <user>, and ft sync-feed commands to sync liked tweets, user timeline, and Following feed via GraphQL
  • All synced data merges into the existing SQLite FTS5 index with a new source column (schema v5), making it queryable via search, list, and stats
  • Adds --source filter (bookmarks, likes, timeline, feed) to search, list, and stats commands

Details

New commands

Command Endpoint Auth
ft sync-likes <user> GraphQL Likes Session cookies
ft sync-timeline <user> GraphQL UserTweets Session cookies
ft sync-feed GraphQL HomeLatestTimeline Session cookies

All three commands support the same browser/cookie options as ft sync (--browser, --cookies, --chrome-profile-directory, etc.).

Architecture

  • New src/graphql-user-sync.ts module reuses shared helpers from graphql-bookmarks.ts (convertTweetToRecord, mergeRecords, buildHeaders, parseSnowflake, snowflakeToIso)
  • Feed sync is session-scoped (no userId needed); likes/timeline resolve userId via UserByScreenName GraphQL query
  • CLI commands factored via registerUserSyncCommand helper to avoid boilerplate duplication
  • Data stored in separate JSONL files (likes.jsonl, timeline.jsonl, feed.jsonl)

Index integration

  • buildIndex() reads all 4 JSONL files and tags each record with its source
  • Schema v5 migration adds source TEXT DEFAULT 'bookmarks' column (backward-compatible)
  • --source filter validated against allowed values with clear error message

Known limitations

  • ft classify only classifies bookmarks (not likes/timeline/feed) — intentional scope
  • likedAt is stored in JSONL but not in the SQLite schema — could be added in a future iteration
  • ft categories and ft domains don't support --source yet

Test plan

  • 19 new tests for parseUserTimelineResponse (user timeline + feed response shapes, cursors, conversation modules, ingestedVia, likedAt)
  • All 117 tests pass (98 existing + 19 new)
  • TypeScript compiles cleanly with --noEmit
  • Manual testing: synced 1500+ likes, 688 tweets, verified --source filtering on search/list/stats
  • --source typo produces clear validation error
  • No personal data in code (grep verified)
  • No breaking changes to existing commands

Note

Medium Risk
Adds new GraphQL sync surfaces and a DB schema migration (schema_version 5 with new source column), which could impact indexing/query correctness and requires careful handling of incremental merges and migrations.

Overview
Adds new CLI commands ft sync-likes <user>, ft sync-timeline <user>, and ft sync-feed that sync additional X timelines via a shared GraphQL engine (graphql-user-sync.ts) and store them in separate JSONL caches.

Updates indexing and query paths to merge all caches into the SQLite FTS index by introducing a source field (schema v5 + migration + index), and adds --source filtering to search, list, and stats. Documentation is updated and new tests cover parsing/ingestion behavior for the new GraphQL response shapes (including cursor handling and likedAt).

Reviewed by Cursor Bugbot for commit 5984733. Bugbot is set up for automated code reviews on this repo. Configure here.

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 5984733. Configure here.

Comment thread src/bookmarks-db.ts

const src = options?.source;
const sourceFilter = src ? 'WHERE source = ?' : '';
const sourceAnd = src ? 'AND source = ?' : '';
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing ensureMigrations call in getStats

Medium Severity

getStats now queries the source column (added in schema v5) when options.source is provided, but unlike searchBookmarks, listBookmarks, and other DB-reading functions, it never calls ensureMigrations(db). On a pre-v5 database, running ft stats --source likes will crash because the source column doesn't exist yet.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 5984733. Configure here.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 64163a2 — added ensureMigrations(db) call in getStats.

Comment thread src/cli.ts
console.log(`\n \u2713 ${result.added} new ${cfg.label} synced (${result.totalBookmarks} total)`);
console.log(` ${friendlyStopReason(result.stopReason)}`);
console.log(` \u2713 Data: ${dataDir()}\n`);
}));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New sync commands don't rebuild search index

Medium Severity

The sync-likes, sync-timeline, and sync-feed commands only write to JSONL cache files but never call buildIndex afterward. Unlike ft sync (which calls rebuildIndex after syncing), the new commands leave data unsearchable. The README Quick Start shows ft sync-likes then ft search as a workflow, but this won't return results without a manual ft index step.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 5984733. Configure here.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 64163a2 — added await rebuildIndex(result.added) after sync in registerUserSyncCommand, matching existing ft sync behavior.

Comment thread src/bookmarks-db.ts Outdated
for (const record of newRecords) {
insertRecord(db, record);
for (const { record, source } of newEntries) {
insertRecord(db, record, source);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cross-source duplicates get wrong source tag in index

Low Severity

buildIndex collects records from all four JSONL files without deduplicating by record.id. When the same tweet exists in multiple sources (e.g., bookmarked AND liked), both entries pass the newEntries filter and INSERT OR REPLACE causes the last source in the array to win. A bookmarked tweet that was also liked would be tagged as likes, making it invisible to --source bookmarks queries.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 5984733. Configure here.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a bug. buildIndex uses an existingIds set (line 346-354) to skip already-indexed records — there is no INSERT OR REPLACE on duplicates. The first source encountered wins, and subsequent sources are filtered out by !existingIds.has(record.id). The source order in the sources array is deterministic (bookmarks first), so bookmarks always take priority.

@cursor
Copy link
Copy Markdown

cursor Bot commented Apr 7, 2026

You have used all of your free Bugbot PR reviews.

To receive reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

@mefrem
Copy link
Copy Markdown

mefrem commented Apr 10, 2026

Was going to vibe up the same thing; thanks for making this @mmenestret !!

Add three new sync commands that extend ft beyond bookmarks:

- `ft sync-likes <user>`: sync liked tweets via GraphQL (Likes endpoint)
- `ft sync-timeline <user>`: sync user's own tweets (UserTweets endpoint)
- `ft sync-feed`: sync Following/chronological feed (HomeLatestTimeline endpoint)

All synced data is stored in separate JSONL files (likes.jsonl, timeline.jsonl,
feed.jsonl) and merged into the unified SQLite FTS5 index via `ft index`.
A new `source` column (schema v5) enables filtering across all query commands:

  ft search "AI" --source likes
  ft list --source feed
  ft stats --source likes

Implementation details:
- New `src/graphql-user-sync.ts` module handles all three endpoints,
  reusing shared helpers (convertTweetToRecord, mergeRecords, buildHeaders,
  parseSnowflake, snowflakeToIso) from graphql-bookmarks.ts
- Feed sync is session-scoped (no userId needed), likes/timeline resolve
  userId via UserByScreenName GraphQL query
- CLI commands factored via registerUserSyncCommand to avoid boilerplate
- --source validated against allowed values (bookmarks, likes, timeline, feed)
- 19 new tests covering response parsing for both user timeline and feed
  response shapes, cursor extraction, conversation modules, ingestedVia
  assignment, and likedAt conversion
…c commands

getStats now calls ensureMigrations(db) to avoid crashing on pre-v5
databases when --source is used.

sync-likes, sync-timeline, and sync-feed now rebuild the search index
after syncing, matching the existing sync command behavior.
@mmenestret mmenestret force-pushed the feat/sync-likes-timeline-feed branch from 64163a2 to 39a2d2a Compare April 13, 2026 14:57
@cursor
Copy link
Copy Markdown

cursor Bot commented Apr 13, 2026

You have used all of your free Bugbot PR reviews.

To receive reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants