npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@t-rents/stars

v1.0.4

Published

Search and discover over your GitHub starred repos. Sync /user/starred, expand via topic search, annotate locally with tags/notes/hidden. Pluggable storage, zero runtime deps, fully typed.

Downloads

51

Readme

@t-rents/stars

npm version bundle size types dependencies license

A local search and discovery layer over your GitHub starred repos.

If you've starred more than a few hundred repos, GitHub's own stars page collapses — slow, no tags, no notes, no way to find that vector-db thing you starred two years ago, and no way to expand the set without manually clicking through topic pages. This is the storage and search engine for fixing that: sync your stars, expand the set with topic searches, annotate with tags / notes / a hidden flag — and every annotation survives every resync. Works in any TypeScript runtime, including Chrome MV3 extensions.

Quick start

import { StarsStore, GitHubClient, FileAdapter } from "@t-rents/stars";

const gh = new GitHubClient(process.env.GH_TOKEN!);
const store = new StarsStore(new FileAdapter("./stars.json"), gh);

await store.sync();                                              // pull /user/starred
await store.discover(["vector-database", "rag"], { limit: 50 }); // expand via topic search

await store.addTag("pgvector/pgvector", "favorite");
await store.setNote("pgvector/pgvector", "the one i actually use in prod");

const { items } = await store.search("vector db", { tags: ["favorite"] });

Install

npm install @t-rents/stars
# or
bun add @t-rents/stars

Why not just...

  • GitHub's own stars page — fine for a few dozen stars; collapses at scale. No tags, no notes, no topic-based set expansion, no fuzzy search across description + topics + language.
  • gh api /user/starred — gives you the JSON. Doesn't give you a query layer, annotations, sync diff, ETag-conditional refresh, or a way to merge topic-discovered repos into the same searchable space.
  • astralapp and similar star managers — mirror your existing stars and stop there. This treats topic-discovery as a first-class source, so repos you haven't starred yet sit alongside the ones you have, in one searchable space.

Storage adapters

new FileAdapter("/path.json")    // node, CLI, seeding from existing JSON
new ChromeStorageAdapter()       // chrome.storage.local — extensions (10MB quota MV3)
new MemoryAdapter()              // tests, transient state

StorageAdapter is a 3-method interface (load, save, clear) — implement to plug in IndexedDB, localStorage, OPFS, or anything else.

API

Reads

store.data()                                    // StoreData
store.get(fullName)                             // RepoRecord | null
store.search(query, opts?)                      // SearchResult<RepoRecord>
store.searchProjected(query, opts?)             // SearchResult<RepoProjection> (slim shape)
store.searchAll(query, opts?)                   // AsyncGenerator<SearchResult>
store.topics(minCount?)                         // TopicFacet[]
store.languages()                               // LanguageFacet[]
store.stats()                                   // Stats
store.export()                                  // StoreData (deep clone)

Writes (serialized through internal queue)

store.sync({ signal?, force? })                 // SyncResult & { changes: SyncChanges }
store.discover(topics, { limit?, sort?, signal? })
store.discoverMany(topicSets, opts?)            // sequential batch

store.addTag(fullName, tag)
store.removeTag(fullName, tag)
store.setTags(fullName, tags[])
store.setNote(fullName, note | null)
store.hide(fullName) / store.unhide(fullName)
store.remove(fullName)                          // hard delete

store.importRecords(records, { overwrite? })    // seed from an existing record array
store.clear()                                   // wipe storage

Events

store.on("synced",     ({ result, changes }) => {})
store.on("discovered", ({ topics, result }) => {})
store.on("imported",   ({ count }) => {})
store.on("updated",    ({ full_name, kind, record }) => {}) // kind: tags|note|hide|unhide|remove
store.on("cleared",    () => {})
store.on("error",      ({ error, op }) => {})
// each `on` returns an unsubscribe fn

Search options

{
  source?:        "github" | "topic"
  language?:      string
  minStars?:      number
  topics?:        string[]                  // require ALL (intersection)
  anyTopic?:      string[]                  // require ANY (union)
  tags?:          string[]                  // require ALL local tags
  includeHidden?: boolean                   // default false
  perPage?:       25 | 50 | 100             // default 50; throws on other values
  page?:          number                    // 1-indexed
  sortBy?:        "stars" | "updated" | "pushed" | "starred_at" | "name" | "created"
  sortDir?:       "asc" | "desc"            // default desc
}

Sync semantics

  • A repo found on /user/starred overwrites the local record but preserves tags, note, hidden, and local_added_at.
  • A topic-discovered repo that turns out to be starred is upgraded (source: "topic" → "github") with discovered_via retained as audit trail.
  • Repos that disappear from /user/starred (you unstarred them) are pruned by default. Disable with new StarsStore(adapter, gh, { pruneUnstarred: false }). Topic-discovered repos are never pruned.
  • Sends If-None-Match (page-1 ETag) on subsequent syncs; returns not_modified: true on 304 with no work done.

Errors

GitHubApiError    // any non-2xx, non-304 response
AuthError         // 401 — bad token
RateLimitError    // 403/429 with rate-limit headers; has resetAt, remaining, limit, resource
AbortError        // signal aborted
StorageError      // adapter failures
MigrationError    // bad schema version or migrator failure

isAbortError(e) covers both AbortError and DOMException("AbortError").

Schema versioning

The persisted store has a version field. New installs start at the current schema. A migration registry runs on load, so future schema changes can roll forward without breaking existing stores. Throws MigrationError if it encounters a store from a newer schema than the library knows.

Browser extension manifest

If you're using this in a Chrome MV3 extension:

{
  "manifest_version": 3,
  "permissions": ["storage"],
  "host_permissions": ["https://api.github.com/*"]
}

Contributing

See CONTRIBUTING.md.

License

MIT