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

@hardlydifficult/github

v1.0.99

Published

Typed GitHub API client with chainable PR operations and a polling watcher for real-time activity.

Readme

@hardlydifficult/github

Typed GitHub API client with chainable PR operations and a polling watcher for real-time activity.

Installation

npm install @hardlydifficult/github

Quick Start

import { github } from "@hardlydifficult/github";

const client = github({ token: "ghp_..." });
const repo = client.repo("owner", "repo");
const pr = repo.pr(123);
const watcher = client.watch({ repos: ["owner/repo"] });

// Listen for new PRs
watcher.onNewPR(({ pr, repo }) => {
  console.log(`New PR: ${pr.title} in ${repo.owner}/${repo.name}`);
});

// Start polling every 30 seconds
await watcher.start();

GitHub API Client

The GitHubClient provides top-level access to repositories, PR watching, and user contribution queries.

github({ token? })

Creates a new client instance from a personal access token. Falls back to GH_PAT, then GITHUB_TOKEN, when no token is provided. GitHubClient is still available, but the factory keeps call sites shorter.

import { github } from "@hardlydifficult/github";

const client = github(); // reads GH_PAT, then GITHUB_TOKEN
const client2 = github({ token: "ghp_..." }); // explicit token

client.repo(owner, repo)

Returns a RepoClient for repository-specific operations.

const repo = client.repo("owner", "repo");
const sameRepo = client.repo("hardlydifficult", "typescript");

client.watch(options)

Returns a PRWatcher instance configured with the specified options.

const watcher = client.watch({
  // `repos` accepts either "owner/repo" or GitHub URLs.
  repos: [
    "hardlydifficult/typescript",
    "https://github.com/hardlydifficult/repo-processor"
  ],
  myPRs: true,
  intervalMs: 60_000
});

client.ownerRepositories(owner)

Fetches all public repositories for an owner (user or organization).

const repos = await client.ownerRepositories("hardlydifficult");
// => [{ owner: "hardlydifficult", name: "typescript", fullName: "hardlydifficult/typescript" }, ...]

client.contributedRepositories(days)

Returns repositories where the authenticated user made PR commits in the last N days.

const repos = await client.contributedRepositories(30);
// => Repos where user contributed in past 30 days

client.myOpenPRs()

Returns all open PRs authored by the authenticated user.

const results = await client.myOpenPRs();
// => [{ pr: PullRequest, repo: { owner, name } }, ...]

RepoClient — Repository Operations

RepoClient provides methods to inspect and modify a specific repository.

repo.pr(number)

Returns a PRClient for interacting with a specific pull request.

const prClient = repo.pr(42);
await prClient.ready(); // Mark PR #42 as ready for review

repo.openPRs()

Fetches all open pull requests in the repository.

const prs = await repo.openPRs();

repo.get()

Fetches repository metadata.

const repoInfo = await repo.get();
// => { id, name, full_name, owner, html_url, default_branch, description, ... }

repo.tree(ref)

Retrieves the file tree for a given commit SHA (default: HEAD).

const { entries, rootSha } = await repo.tree();
// entries: [{ path: "src/index.ts", type: "blob", sha: "...", size: 1234 }, ...]

repo.read(filePath, ref)

Returns the content of a file as a string.

const content = await repo.read("README.md", "main");

repo.context({ files, maxChars, ref? })

Fetches the file tree and specific key files for AI context gathering.

const context = await repo.context({
  files: ["src/index.ts", "tsconfig.json"],
  maxChars: 10_000,
});
// => { filePaths: [...], keyFiles: [{ path, content }, ...] }

repo.defaultBranchSha()

Returns the HEAD commit SHA of the repository's default branch.

const sha = await repo.defaultBranchSha();

repo.branchSha(branch)

Returns the SHA of a branch, or null if the branch does not exist.

const sha = await repo.branchSha("feature/branch");

repo.mergeBranch(base, head)

Merges one branch into another. Returns the merge commit SHA on success, or null if already up-to-date.

const mergeSha = await repo.mergeBranch("main", "feature/branch");

repo.createBranch(branch, sha)

Creates a new branch pointing to the given SHA.

await repo.createBranch("feature/branch", "abc123");

repo.updateBranch(branch, sha)

Updates an existing branch ref to point to a new SHA.

await repo.updateBranch("feature/branch", "def456");

repo.deleteBranch(branch)

Deletes a branch reference.

await repo.deleteBranch("feature/branch");

repo.openPR(options)

Creates a new pull request.

const pr = await repo.openPR({
  head: "feature/branch",
  title: "Add new feature",
  body: "This PR adds...",
});
// => { number: 42, url: "https://github.com/owner/repo/pull/42" }

repo.commit(options)

Creates a commit with file changes and updates or creates the target branch. The parent SHA is chosen automatically from the branch head or default branch.

const result = await repo.commit({
  branch: "feature/branch",
  files: [{ path: "README.md", content: "Hello" }],
  message: "Update README",
  author: { name: "Alice", email: "[email protected]" }
});
// => { commitSha: "def456", branchCreated: true }

PRClient — Pull Request Operations

PRClient provides high-level methods for working with a specific pull request.

prClient.get()

Fetches the full pull request details.

const pr = await prClient.get();

prClient.diff()

Fetches the PR diff in text format.

const diff = await prClient.diff();

prClient.files()

Lists files modified in the PR.

const files = await prClient.files();
// => [{ sha, filename, status, additions, deletions, changes, ... }, ...]

prClient.commits()

Lists commits in the PR.

const commits = await prClient.commits();

prClient.reviews()

Lists review objects on the PR.

const reviews = await prClient.reviews();

prClient.comments()

Lists comments on the PR.

const comments = await prClient.comments();

prClient.checks()

Lists check runs associated with the PR's head SHA.

const checkRuns = await prClient.checks();

prClient.comment(body)

Adds a comment to the PR.

await prClient.comment(" LGTM!");

prClient.timeline()

Fetches comments, reviews, and commits in parallel and merges them into a chronologically sorted timeline.

const timeline = await prClient.timeline();
// => TimelineEntry[]

prClient.snapshot()

Loads the PR details plus comments, reviews, checks, and a prebuilt timeline in one call.

const snapshot = await prClient.snapshot();
// => { pr, repo, comments, reviews, checks, timeline }

prClient.formatTimeline(entries)

Formats a timeline as human-readable markdown.

const formatted = formatTimeline(timeline);
// => "[2024-01-15 10:30] 💬 @alice (comment): Looks good\n[...]"

prClient.squash(title)

Squash-merges the PR with a custom commit title.

await prClient.squash("Merge feature/branch");

prClient.ready()

Marks the PR as ready for review (unset draft).

await prClient.ready();

prClient.enableAutoMerge()

Enables squash auto-merge.

await prClient.enableAutoMerge();

PRWatcher — Real-Time Event Polling

PRWatcher polls GitHub at regular intervals and emits events for PR activity.

For new integrations, prefer watcher.onEvent(...) with a single switch statement. Existing onX methods remain fully supported for compatibility.

Migration guidance: prefer onEvent for new code; onX remains supported.

watcher.onEvent(callback)

Fires for every watcher event as a discriminated union: { type, payload }.

watcher.onEvent((event) => {
  switch (event.type) {
    case "new_pr":
      console.log(`New PR: #${event.payload.pr.number}`);
      break;
    case "comment":
      console.log(`Comment on #${event.payload.pr.number}: ${event.payload.comment.body}`);
      break;
    case "review":
      console.log(`Review on #${event.payload.pr.number}: ${event.payload.review.state}`);
      break;
    case "check_run":
      console.log(`Check run ${event.payload.checkRun.name}: ${event.payload.checkRun.status}`);
      break;
    case "merged":
    case "closed":
      console.log(`PR #${event.payload.pr.number} is now ${event.type}`);
      break;
    case "pr_updated":
      console.log(`PR #${event.payload.pr.number} metadata changed`);
      break;
    case "status_changed":
      console.log(`PR #${event.payload.pr.number} status: ${event.payload.previousStatus} -> ${event.payload.status}`);
      break;
    case "push":
      console.log(`Push on ${event.payload.repo.owner}/${event.payload.repo.name}@${event.payload.branch}`);
      break;
    case "poll_complete":
      console.log(`Tracking ${event.payload.prs.length} PRs`);
      break;
  }
});

watcher.onNewPR(callback)

Fires once when a PR is first seen.

watcher.onNewPR(({ pr, repo }) => {
  console.log(`New PR: #${pr.number}`);
});

watcher.onComment(callback)

Fires when a new comment is added to a PR.

watcher.onComment(({ comment, pr, repo }) => {
  console.log(`New comment: ${comment.body}`);
});

watcher.onReview(callback)

Fires when a new review is submitted.

watcher.onReview(({ review, pr, repo }) => {
  console.log(`New review: ${review.state}`);
});

watcher.onCheckRun(callback)

Fires when a check run’s status or conclusion changes.

watcher.onCheckRun(({ checkRun, pr, repo }) => {
  console.log(`Check run: ${checkRun.name} -> ${checkRun.status}`);
});

watcher.onMerged(callback)

Fires when a PR is merged.

watcher.onMerged(({ pr, repo }) => {
  console.log(`PR #${pr.number} merged!`);
});

watcher.onClosed(callback)

Fires when a PR is closed (without merging).

watcher.onClosed(({ pr, repo }) => {
  console.log(`PR #${pr.number} closed`);
});

watcher.onPRUpdated(callback)

Fires when draft status, mergeable state, or labels change.

watcher.onPRUpdated(({ pr, repo, changes }) => {
  if (changes.draft) {
    console.log(`PR changed from draft ${changes.draft.from} to ${changes.draft.to}`);
  }
});

watcher.onStatusChanged(callback)

Fires when a user-defined status changes (requires classifyPR option).

const watcher = client.watch({
  repos: ["owner/repo"],
  classifyPR: async ({ pr, repo }) => {
    const checks = await client.repo(repo.owner, repo.name).pr(pr.number).checks();
    return checks.every((check) => check.conclusion === "success")
      ? "green"
      : "red";
  }
});

watcher.onStatusChanged(({ pr, status, previousStatus }) => {
  console.log(`PR status changed: ${previousStatus} -> ${status}`);
});

watcher.onPush(callback)

Fires when the default branch HEAD changes (push event detection).

watcher.onPush(({ repo, branch, sha, previousSha }) => {
  console.log(`New push to ${repo.owner}/${repo.name}/${branch}`);
});

watcher.start()

Starts polling and returns an array of current PR statuses.

const currentStatuses = await watcher.start();
// => PRStatusEvent[]

watcher.stop()

Stops polling.

watcher.stop();

watcher.getWatchedPRs()

Returns all currently watched PRs.

const prs = watcher.getWatchedPRs();

watcher.addRepo(repo)

Adds a repository to watch.

watcher.addRepo("owner/new-repo");

watcher.removeRepo(repo)

Removes a repository from watching.

watcher.removeRepo("owner/outdated-repo");

Advanced Features

Custom Classification

const watcher = client.watch({
  repos: ["owner/repo"],
  classifyPR: async ({ pr, repo }, activity) => {
    if (pr.draft) return "draft";
    if (activity.comments.length > 5) return "needs_review";
    if (activity.checkRuns.some(r => r.status === "pending")) return "ci_running";
    return "approved";
  },
});

watcher.onStatusChanged(({ previousStatus, status, pr }) => {
  console.log(`Status changed for #${pr.number}: ${previousStatus} → ${status}`);
});

Dynamic Repository Discovery

const watcher = client.watch({
  repos: ["owner/main-repo"],
  discoverRepos: async () => {
    const repositories = await client.ownerRepositories("owner");
    return repositories
      .map((repo) => repo.fullName)
      .filter((fullName) => fullName.endsWith("-ts"));
  },
});

Throttling Integration

const throttle = {
  async run<T>(task: () => Promise<T>, weight: number) {
    // Rate-limit the work, then run it
    await new Promise(resolve => setTimeout(resolve, weight * 100));
    return task();
  },
};

const watcher = client.watch({
  repos: ["owner/repo"],
  throttle,
});

Stale PR Cleanup

const watcher = client.watch({
  repos: ["owner/repo"],
  stalePRThresholdMs: 7 * 24 * 60 * 60 * 1000, // 7 days
});

URL Parsing Utilities

parseGitHubFileUrl(url)

Parses a GitHub file URL to extract owner, repo, branch, and path.

const info = parseGitHubFileUrl("https://github.com/owner/repo/blob/main/src/index.ts");
// => { owner: "owner", repo: "repo", branch: "main", filePath: "src/index.ts" }

parseGitHubDirectoryUrl(url)

Parses a GitHub directory URL to extract owner, repo, branch, and directory path.

const info = parseGitHubDirectoryUrl("https://github.com/owner/repo/tree/main/src");
// => { owner: "owner", repo: "repo", branch: "main", dirPath: "src" }

Tree Diff Utilities

diffTree(blobs, manifest)

Compares the current git tree against a manifest of previously processed blob SHAs.

const { changedFiles, removedFiles, staleDirs } = diffTree(entries, manifest);
// changedFiles: new/modified files
// removedFiles: deleted paths
// staleDirs: directories containing changed/removed files

collectDirectories(filePaths)

Collects all ancestor directory paths from a list of file paths.

const dirs = collectDirectories(["src/index.ts", "lib/utils.ts"]);
// => ["", "src", "lib"]

groupByDirectory(filePaths)

Groups file paths by their immediate parent directory.

const groups = groupByDirectory(["src/index.ts", "lib/utils.ts"]);
// => Map([ "src" => ["index.ts"], "lib" => ["utils.ts"] ])

Timeline Utilities

buildTimeline(comments, reviews, commits)

Merges PR timeline entries into a chronologically sorted array.

const timeline = buildTimeline(comments, reviews, commits);
// => TimelineEntry[]

formatTimeline(entries)

Formats a timeline as readable markdown text.

const formatted = formatTimeline(timeline);
// "[2024-01-15 10:30] 💬 @alice (comment): Looks good\n[...]"

TimelineEntry

interface TimelineEntry {
  kind: "comment" | "review" | "commit";
  timestamp: string;
  author: string;
  body: string;
  reviewState?: string;
  commitSha?: string;
}

Types

All exported types are included below:

PR-related Types

  • PullRequest: Full PR object with titles, states, labels, reviewers, mergeability
  • PullRequestFile: Modified file metadata (added, removed, modified, etc.)
  • PullRequestCommit: Commit details with author and message
  • PullRequestReview: Review object with state (APPROVED, CHANGES_REQUESTED, etc.)
  • PullRequestComment: Comment with author and timestamp

Repository Types

  • Repository: GitHub repository metadata
  • ContributionRepo: Minimal repo info for contribution tracking
  • TreeEntry: Git tree entry with path, type, and SHA

Watcher Types

  • WatchOptions: Configuration for PRWatcher
  • WatchThrottle: Task-oriented rate-limiter interface (compatible with @hardlydifficult/throttle)
  • ClassifyPR: Function to compute custom PR status
  • DiscoverRepos: Function to discover repositories dynamically
  • PREvent: Base event for PR actions
  • PRStatusEvent: Event with user-defined status
  • StatusChangedEvent: Status change with previous value
  • CommentEvent, ReviewEvent, CheckRunEvent: Activity-specific events
  • PRUpdatedEvent: Draft/status/label changes
  • PollCompleteEvent: Emitted at end of each poll cycle
  • PushEvent: HEAD SHA change for watched repos

Git API Types

  • CommitAuthor: Name and email
  • CommitFile: Path and content
  • CommitOptions: Parameters for repo.commit
  • CommitResult: Result of a file commit
  • OpenPullRequestOptions: Parameters for repo.openPR
  • CreatedPR: Result of PR creation
  • PullRequestSnapshot: Result of prClient.snapshot()

Setup

You must provide a GitHub personal access token via the token parameter, GH_PAT, or GITHUB_TOKEN. The token requires repo scope for full read/write access.

Appendix

| Operation | Rate Limiting | Notes | |------|---|-----| | ownerRepositories | Uses repos.listForOrg or repos.listForUser | Falls back to user if org is not found | | fetchWatchedPRs | 1 call per repo + 1 for myPRs if enabled | De-duplicates PRs | | fetchPRActivitySelective | Caches comments/reviews; checks check runs only when PR changed | Reduces API calls by up to 2/3 | | branchHeadTracker | Zero-cost if default branch was harvested from PR data | otherwise 1 call per repo | | classifyPR | Executed per PR per poll cycle | User responsibility to manage internal rate limits |

License

MIT