@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/githubQuick 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 tokenclient.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 daysclient.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 reviewrepo.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 filescollectDirectories(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, mergeabilityPullRequestFile: Modified file metadata (added,removed,modified, etc.)PullRequestCommit: Commit details with author and messagePullRequestReview: Review object with state (APPROVED,CHANGES_REQUESTED, etc.)PullRequestComment: Comment with author and timestamp
Repository Types
Repository: GitHub repository metadataContributionRepo: Minimal repo info for contribution trackingTreeEntry: Git tree entry with path, type, and SHA
Watcher Types
WatchOptions: Configuration forPRWatcherWatchThrottle: Task-oriented rate-limiter interface (compatible with@hardlydifficult/throttle)ClassifyPR: Function to compute custom PR statusDiscoverRepos: Function to discover repositories dynamicallyPREvent: Base event for PR actionsPRStatusEvent: Event with user-defined statusStatusChangedEvent: Status change with previous valueCommentEvent,ReviewEvent,CheckRunEvent: Activity-specific eventsPRUpdatedEvent: Draft/status/label changesPollCompleteEvent: Emitted at end of each poll cyclePushEvent: HEAD SHA change for watched repos
Git API Types
CommitAuthor: Name and emailCommitFile: Path and contentCommitOptions: Parameters forrepo.commitCommitResult: Result of a file commitOpenPullRequestOptions: Parameters forrepo.openPRCreatedPR: Result of PR creationPullRequestSnapshot: Result ofprClient.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
