feat: sync --gaps, quoted tweets, bookmarkedAt, update checker#35
Merged
Conversation
Remove redundant `incremental &&` guards from sync stop conditions. `reachedLatestStored` is already self-gating (only true when `newestKnownId` is set, which requires incremental mode). The stale page limit should apply universally to prevent full syncs from fetching hundreds of empty pages. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Rename --full to --rebuild for full re-crawl - Add --gaps mode: backfills missing quoted tweets via syndication API - Extract quoted tweet content from GraphQL response (all sync modes) - Extract bookmarkedAt timestamp from entry sortIndex snowflake - Add QuotedTweetSnapshot type, schema v4 migration - Add daily npm update checker (non-blocking, cached) - Error if --rebuild and --gaps used together - Extract CHROME_UA constant to deduplicate user-agent string Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove dead updateBookmarkedAt() — bookmarkedAt comes from sortIndex during sync, not from gap-fill - Export compareVersions for testability - Add 6 tests for version comparison edge cases (double-digit segments, etc.) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Version number (e.g. v1.2.1) now displayed in the logo box - WHATS_NEW map shows changelog highlights once on first run after updating to a new version (tracked via .last-version file) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Prefer tweet.note_tweet.note_tweet_results.result.text over legacy.full_text which truncates at 280 chars. Applies to all sync modes. Existing truncated articles fixed on next --rebuild. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
legacy.full_text caps at ~304 chars, so X Articles and long-form note tweets were silently truncated. --gaps now fetches all bookmarks with text >= 275 chars from the syndication API and expands any where the full text is longer. Combined with quoted tweet backfill in a single deduplicated fetch pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Apply quoted tweet and text expansion results as each fetch completes instead of batching fetch-then-apply. Progress bar now shows accurate counts during the run, not just at the end. - Checkpoint JSONL every 100 fetches so a crash doesn't lose all progress on large gap-fill runs. - Build lookup indexes (recordsByQuotedId, recordsByTweetId) to avoid re-scanning all records for each fetch. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Running ft sync --rebuild now shows a warning explaining it will re-crawl all bookmarks, prints the exact cp command to back up first, and asks for confirmation. Use --yes to skip the prompt. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…TS5 errors - Atomic file writes: JSONL, JSON, and SQLite now write to .tmp then rename, preventing corruption on crash/interrupt - Data directory created with mode 0o700 (owner-only access) - buildIndex transaction wrapped in try/catch with ROLLBACK on error - FTS5 query parse errors caught and re-thrown with a user-friendly message instead of raw SQLite errors Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Spinner now runs on an 80ms interval independent of network fetches. Data callbacks update the state, the timer handles rendering. Applies to both sync and gaps progress bars. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Ctrl+C during sync or gaps now stops the spinner cleanly and prints "Your data is safe — progress has been saved. Run the same command again to pick up where you left off." instead of a raw stack trace. Handler is scoped to spinner lifetime and cleaned up on normal completion. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Failed tweets now tracked with reason (deleted, private, rate limited, empty) and written to gaps-failures.json. Summary grouped by reason shown in CLI output with path to full log for inspection. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
4 tasks
This was referenced Apr 7, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Sync modes
--full→--rebuild: Renamed for clarity. Full re-crawl with confirmation prompt, backup instructions, and--yesflag to skip.--gaps: Scans existing bookmarks and backfills missing data via X's syndication API (no auth needed):gaps-failures.jsonwith per-tweet diagnostics grouped by reasonincremental &&guards so--rebuildstops after 3 empty pages instead of paging to 500.Data extraction (all sync modes)
convertTweetToRecord()now readsquoted_status_result.result. Captures text, author, media, URL.note_tweet.note_tweet_results.result.textover truncatedlegacy.full_text(capped at ~304 chars).bookmarkedAttimestamp: Decoded from timeline entrysortIndexsnowflake. Previously alwaysnull.UX
Security
.tmpthenrename, preventing corruption on crash.~/.ft-bookmarks/created with mode0o700(owner-only).buildIndexwrapped withROLLBACKon error.Housekeeping
quoted_tweet_jsoncolumn)CHROME_UAconstant extractedTest plan
ft synccaptures quoted tweets, full article text, and bookmarkedAtft sync --gapsbackfills quoted tweets and expands truncated articlesft sync --gapswritesgaps-failures.jsonwith diagnostics for failed tweetsft sync --rebuildshows confirmation prompt with backup commandft sync --rebuild --yesskips confirmationft sync --rebuild --gapserrors cleanlynpm run buildpassesnpm test— 73 pass (5 pre-existing fixture failures unchanged)🤖 Generated with Claude Code