import { AsyncLocalStorage } from 'node:async_hooks'; import { Octokit } from '@octokit/rest'; import { BaseFormatConverter, Root, AdapterPostableMessage, Logger, Adapter, ChatInstance, Thread, UserInfo, WebhookOptions, Message, Author, RawMessage, StreamChunk, StreamOptions, EmojiValue, FetchOptions, FetchResult, ThreadInfo, ListThreadsOptions, ListThreadsResult, ChannelInfo, FormattedContent, MessageSubject } from 'chat'; /** * GitHub-specific format conversion using AST-based parsing. * * GitHub uses GitHub Flavored Markdown (GFM) which is very close to standard markdown. * This converter primarily passes through standard markdown, with special handling for: * - @mentions (user references) * - #refs (issue/PR references) * - SHA references (commit links) */ declare class GitHubFormatConverter extends BaseFormatConverter { /** * GitHub uses standard GFM, so we can use remark-stringify directly. * We just need to ensure @mentions are preserved. */ fromAst(ast: Root): string; /** * Parse GitHub markdown into an AST. * GitHub uses standard GFM, so we use the standard parser. */ toAst(markdown: string): Root; /** * Override renderPostable to handle @mentions in plain strings. * GitHub @mentions are already in the correct format (@username). */ renderPostable(message: AdapterPostableMessage): string; } /** * Type definitions for the GitHub adapter. */ /** * Custom webhook verifier used in place of the GitHub webhook secret. * * Receives the incoming request and its raw body. Return a truthy value * to accept the request; throw or return a falsy value to reject it * (the adapter responds `401`). Used for verifying Vercel Connect * trigger-forwarded webhooks via the Vercel OIDC token Connect attaches. */ type GitHubWebhookVerifier = (request: Request, body: string) => Promise | unknown; /** * Base configuration options shared by all auth methods. */ interface GitHubAdapterBaseConfig { /** Override the GitHub API base URL (e.g. "https://github.example.com/api/v3" for GitHub Enterprise). Defaults to GITHUB_API_URL env var. */ apiUrl?: string; /** * Bot's GitHub user ID (numeric). Used for self-message detection. * Defaults to the `GITHUB_BOT_USER_ID` env var. If still unset, the adapter * tries to auto-detect it (and, in Vercel Connect mode, learns it from the * first comment it posts). Set it explicitly in Connect mode — auto-detection * can't run with only an installation token, and without it the adapter may * reply to its own comments (especially across serverless instances). */ botUserId?: number; /** Logger instance for error reporting. Defaults to ConsoleLogger. */ logger?: Logger; /** * Bot username (e.g., "my-bot" or "my-bot[bot]" for GitHub Apps). * Used for @-mention detection. * Defaults to GITHUB_BOT_USERNAME env var or "github-bot". */ userName?: string; /** * Webhook secret for HMAC-SHA256 verification. * Set this in your GitHub webhook settings. * Defaults to GITHUB_WEBHOOK_SECRET env var. */ webhookSecret?: string; /** * Custom webhook verifier used in place of `webhookSecret`. When set, it * takes precedence over `webhookSecret` and the `GITHUB_WEBHOOK_SECRET` * env var. Useful when webhooks arrive via Vercel Connect trigger * forwarding (verified with a Vercel OIDC token rather than GitHub's * webhook secret). */ webhookVerifier?: GitHubWebhookVerifier; } /** * Configuration using a Personal Access Token (PAT). * Simpler setup, suitable for personal bots or testing. */ interface GitHubAdapterPATConfig extends GitHubAdapterBaseConfig { appId?: never; installationId?: never; installationToken?: never; privateKey?: never; /** Personal Access Token with appropriate scopes (repo, write:discussion) */ token: string; } /** * Configuration using a GitHub App with a fixed installation. * Use this when your bot is only installed on a single org/repo. */ interface GitHubAdapterAppConfig extends GitHubAdapterBaseConfig { /** GitHub App ID */ appId: string; /** Installation ID for the app (for single-tenant apps) */ installationId: number; installationToken?: never; /** GitHub App private key (PEM format) */ privateKey: string; token?: never; } /** * Configuration using a GitHub App for multi-tenant (public) apps. * The installation ID is automatically extracted from each webhook payload. * Use this when your bot can be installed by anyone. */ interface GitHubAdapterMultiTenantAppConfig extends GitHubAdapterBaseConfig { /** GitHub App ID */ appId: string; /** Omit installationId to enable multi-tenant mode */ installationId?: never; installationToken?: never; /** GitHub App private key (PEM format) */ privateKey: string; token?: never; } /** * Configuration backed by Vercel Connect. * * Supplies installation access tokens directly (skipping the GitHub App * JWT exchange) and verifies inbound webhooks with a custom * `webhookVerifier` instead of a webhook secret. Pair with * `connectGitHubAdapter()` from `@vercel/connect/chat`. * * @see https://vercel.com/docs/connect */ interface GitHubAdapterConnectConfig extends GitHubAdapterBaseConfig { appId?: never; installationId?: never; /** * Installation access token, or a resolver invoked per API call. * The adapter sends it directly as the bearer credential and skips the * GitHub App private-key JWT exchange. The function form composes with * Vercel Connect's short-lived tokens (`getToken`). */ installationToken: string | (() => string | Promise); privateKey?: never; token?: never; /** Connect verifies webhooks via OIDC, so a webhook secret is not used. */ webhookSecret?: never; /** * Required. Connect-forwarded webhooks are verified with a Vercel OIDC * token rather than GitHub's webhook secret. */ webhookVerifier: GitHubWebhookVerifier; } /** * Configuration with no auth fields - will auto-detect from env vars. */ interface GitHubAdapterAutoConfig extends GitHubAdapterBaseConfig { appId?: never; installationId?: never; installationToken?: never; privateKey?: never; token?: never; } /** * GitHub adapter configuration - PAT, single-tenant App, multi-tenant App, * or Vercel Connect. */ type GitHubAdapterConfig = GitHubAdapterPATConfig | GitHubAdapterAppConfig | GitHubAdapterMultiTenantAppConfig | GitHubAdapterConnectConfig | GitHubAdapterAutoConfig; /** * Decoded thread ID for GitHub. * * Thread types: * - PR-level: Comments in the "Conversation" tab (issue_comment API) * - Review comment: Line-specific comments in "Files changed" tab (pull request review comment API) * - Issue-level: Comments on GitHub issues (issue_comment API) */ interface GitHubThreadId { /** Repository owner (user or organization) */ owner: string; /** * Issue or pull request number. * GitHub uses a shared number space, so this works for both PRs and issues. */ prNumber: number; /** Repository name */ repo: string; /** * Root review comment ID for line-specific threads. * If present, this is a review comment thread. * If absent, this is a PR-level or issue-level comment thread. * Only valid when type is "pr" or omitted. */ reviewCommentId?: number; /** * Thread context type. * - "pr": PR conversation tab or review comment thread (default) * - "issue": GitHub issue comment thread * * Omitting this field is equivalent to "pr" for backward compatibility. */ type?: "pr" | "issue"; } /** * GitHub user object (simplified). */ interface GitHubUser { avatar_url?: string; id: number; login: string; type: "User" | "Bot" | "Organization"; } /** * GitHub repository object (simplified). */ interface GitHubRepository { full_name: string; id: number; name: string; owner: GitHubUser; } /** * GitHub pull request object (simplified). */ interface GitHubPullRequest { body: string | null; html_url: string; id: number; number: number; state: "open" | "closed"; title: string; user: GitHubUser; } /** * GitHub issue comment (PR-level comment in Conversation tab). */ interface GitHubIssueComment { body: string; created_at: string; html_url: string; id: number; /** Reactions summary */ reactions?: { url: string; total_count: number; "+1": number; "-1": number; laugh: number; hooray: number; confused: number; heart: number; rocket: number; eyes: number; }; updated_at: string; user: GitHubUser; } /** * GitHub pull request review comment (line-specific comment in Files Changed tab). */ interface GitHubReviewComment { body: string; /** The commit SHA the comment is associated with */ commit_id: string; created_at: string; /** The diff hunk the comment applies to */ diff_hunk: string; html_url: string; id: number; /** * The ID of the comment this is a reply to. * If present, this is a reply in an existing thread. * If absent, this is the root of a new thread. */ in_reply_to_id?: number; /** Line number in the diff */ line?: number; /** The original commit SHA (for outdated comments) */ original_commit_id: string; /** Original line number */ original_line?: number; /** Path to the file being commented on */ path: string; /** Reactions summary */ reactions?: GitHubIssueComment["reactions"]; /** Side of the diff (LEFT or RIGHT) */ side?: "LEFT" | "RIGHT"; /** Start line for multi-line comments */ start_line?: number | null; /** Start side for multi-line comments */ start_side?: "LEFT" | "RIGHT" | null; updated_at: string; user: GitHubUser; } /** * GitHub App installation info included in webhooks. */ interface GitHubInstallation { id: number; node_id?: string; } /** * Webhook payload for issue_comment events. */ interface IssueCommentWebhookPayload { action: "created" | "edited" | "deleted"; comment: GitHubIssueComment; /** Present when webhook is from a GitHub App */ installation?: GitHubInstallation; issue: { number: number; title: string; pull_request?: { url: string; }; }; repository: GitHubRepository; sender: GitHubUser; } /** * Webhook payload for pull_request_review_comment events. */ interface PullRequestReviewCommentWebhookPayload { action: "created" | "edited" | "deleted"; comment: GitHubReviewComment; /** Present when webhook is from a GitHub App */ installation?: GitHubInstallation; pull_request: GitHubPullRequest; repository: GitHubRepository; sender: GitHubUser; } /** * Platform-specific raw message type for GitHub. * Can be either an issue comment or a review comment. */ type GitHubRawMessage = { type: "issue_comment"; comment: GitHubIssueComment; repository: GitHubRepository; prNumber: number; /** * Whether this comment is on a PR or a plain issue. * Defaults to "pr" when omitted for backward compatibility. */ threadType?: "pr" | "issue"; } | { type: "review_comment"; comment: GitHubReviewComment; repository: GitHubRepository; prNumber: number; }; /** * Reaction content types supported by GitHub. */ type GitHubReactionContent = "+1" | "-1" | "laugh" | "confused" | "heart" | "hooray" | "rocket" | "eyes"; /** * GitHub adapter for chat SDK. * * Supports PR-level comments (Conversation tab), review comment threads * (Files Changed tab - line-specific), and issue comments. * * @example Single-tenant (your own org) * ```typescript * import { Chat } from "chat"; * import { GitHubAdapter } from "@chat-adapter/github"; * import { MemoryState } from "@chat-adapter/state-memory"; * * const chat = new Chat({ * userName: "my-bot", * adapters: { * github: new GitHubAdapter({ * token: process.env.GITHUB_TOKEN!, * webhookSecret: process.env.GITHUB_WEBHOOK_SECRET!, * userName: "my-bot", * logger: console, * }), * }, * state: new MemoryState(), * logger: "info", * }); * ``` * * @example Multi-tenant (public app anyone can install) * ```typescript * const chat = new Chat({ * userName: "my-bot[bot]", * adapters: { * github: new GitHubAdapter({ * appId: process.env.GITHUB_APP_ID!, * privateKey: process.env.GITHUB_PRIVATE_KEY!, * // No installationId - automatically extracted from webhooks * webhookSecret: process.env.GITHUB_WEBHOOK_SECRET!, * userName: "my-bot[bot]", * logger: console, * }), * }, * state: new MemoryState(), * logger: "info", * }); * ``` */ declare class GitHubAdapter implements Adapter { readonly name = "github"; readonly userName: string; protected readonly requestContext: AsyncLocalStorage<{ installationId?: number; }>; /** * The underlying [Octokit](https://github.com/octokit/octokit.js) REST * client, authenticated with the credentials this adapter was configured * with. Use this for any GitHub API call that isn't covered by the unified * Chat SDK surface. * * Resolution rules: * - **PAT mode** and **single-tenant GitHub App mode** (with a fixed * `installationId`): always returns the same client instance. * - **Multi-tenant GitHub App mode**: returns the client for the current * webhook request's installation, resolved from `AsyncLocalStorage`. * Calling this getter outside a webhook handler throws, since there is * no installation to authenticate as. * * @throws {ValidationError} In multi-tenant mode when called outside a * webhook handler (no installation ID is available). * * @example * ```ts * const github = bot.getAdapter("github").octokit; * const { data: pulls } = await github.rest.pulls.list({ * owner: "vercel", * repo: "chat", * state: "open", * }); * ``` */ get octokit(): Octokit; /** * @deprecated Use {@link GitHubAdapter.octokit | `octokit`} instead. This * alias is preserved for backwards compatibility and will be removed in a * future major release. */ get client(): Octokit; protected readonly defaultOctokit: Octokit | null; protected readonly appCredentials: { appId: string; privateKey: string; } | null; protected readonly fixedInstallationId: number | null; private readonly installationClients; protected readonly apiUrl?: string; protected readonly installationTokenProvider: (() => Promise) | null; protected readonly webhookSecret?: string; protected readonly webhookVerifier?: GitHubWebhookVerifier; protected chat: ChatInstance | null; protected readonly logger: Logger; protected _botUserId: number | null; protected readonly formatConverter: GitHubFormatConverter; /** Bot user ID (numeric) used for self-message detection */ get botUserId(): string | undefined; /** Whether this adapter is in multi-tenant mode (no fixed installation ID) */ get isMultiTenant(): boolean; constructor(config?: GitHubAdapterConfig); /** * Get or create an Octokit instance for a specific installation. * For single-tenant mode, returns the single instance. * For multi-tenant mode, creates/caches instances per installation. */ protected getOctokit(installationId?: number): Octokit; /** * Fetch the bot's user ID from GitHub. Used for self-message detection. * Best-effort: errors are swallowed so they don't block webhook processing. * The returned promise resolves once detection completes (or fails). */ protected detectBotUserId(octokit: Octokit): Promise; /** * Learn the bot's own numeric user id from a comment it just created. The * create-comment response's `user` is the authenticated author (the bot), so * this lets self-message detection (`isMe`) start working even in Vercel * Connect mode, where the id can't be auto-detected from an installation * token — preventing the adapter from replying to its own comments in a loop. */ protected captureBotUserId(user: { id?: number; login?: string; } | null | undefined): void; initialize(chat: ChatInstance): Promise; /** * Get the state key for storing installation ID for a repository. */ protected getInstallationKey(owner: string, repo: string): string; /** * Store the installation ID for a repository (for multi-tenant mode). */ protected storeInstallationId(owner: string, repo: string, installationId: number): Promise; /** * Get the installation ID for a repository (for multi-tenant mode). */ protected getStoredInstallationId(owner: string, repo: string): Promise; /** * Get the GitHub App installation ID associated with a thread. * * Returns the fixed installation ID in single-tenant app mode, the cached * repository installation in multi-tenant mode, or undefined in PAT mode. */ getInstallationId(thread: Thread | string): Promise; getUser(userId: string): Promise; /** * Handle incoming webhook from GitHub. */ handleWebhook(request: Request, options?: WebhookOptions): Promise; /** * Verify GitHub webhook signature using HMAC-SHA256. */ protected verifySignature(body: string, signature: string | null): boolean; /** * Handle issue_comment webhook (PR-level comments in Conversation tab). */ protected handleIssueComment(payload: IssueCommentWebhookPayload, _installationId: number | undefined, options?: WebhookOptions): void; /** * Handle pull_request_review_comment webhook (line-specific comments). */ protected handleReviewComment(payload: PullRequestReviewCommentWebhookPayload, _installationId: number | undefined, options?: WebhookOptions): void; /** * Parse an issue comment into a normalized Message. */ protected parseIssueComment(comment: GitHubIssueComment, repository: { owner: GitHubUser; name: string; }, prNumber: number, threadId: string, threadType?: "pr" | "issue"): Message; /** * Parse a review comment into a normalized Message. */ protected parseReviewComment(comment: GitHubReviewComment, repository: { owner: GitHubUser; name: string; }, prNumber: number, threadId: string): Message; /** * Parse a GitHub user into an Author. */ protected parseAuthor(user: GitHubUser): Author; /** * Get the Octokit client for a specific thread. * In multi-tenant mode, looks up the installation ID from state. */ protected getOctokitForThread(owner: string, repo: string): Promise; /** * Post a message to a thread. */ postMessage(threadId: string, message: AdapterPostableMessage): Promise>; /** * Edit an existing message. */ editMessage(threadId: string, messageId: string, message: AdapterPostableMessage): Promise>; /** * Stream a message by accumulating text and posting once. * * The default fallback posts a placeholder then edits it every 500ms. * GitHub rejects these edits with 422 because `body` is empty before * the first token arrives, and rapid edits risk secondary rate limits. */ stream(threadId: string, textStream: AsyncIterable, _options?: StreamOptions): Promise>; /** * Delete a message. */ deleteMessage(threadId: string, messageId: string): Promise; /** * Add a reaction to a message. */ addReaction(threadId: string, messageId: string, emoji: EmojiValue | string): Promise; /** * Remove a reaction from a message. */ removeReaction(threadId: string, messageId: string, emoji: EmojiValue | string): Promise; /** * Convert SDK emoji to GitHub reaction content. */ protected emojiToGitHubReaction(emoji: EmojiValue | string): GitHubReactionContent; /** * Show typing indicator (no-op for GitHub). */ startTyping(_threadId: string, _status?: string): Promise; /** * Fetch messages from a thread. */ fetchMessages(threadId: string, options?: FetchOptions): Promise>; /** * Fetch thread metadata. */ fetchThread(threadId: string): Promise; /** * Encode platform data into a thread ID string. * * Thread ID formats: * - PR-level: `github:{owner}/{repo}:{prNumber}` * - Issue-level: `github:{owner}/{repo}:issue:{issueNumber}` * - Review comment: `github:{owner}/{repo}:{prNumber}:rc:{reviewCommentId}` */ encodeThreadId(platformData: GitHubThreadId): string; /** * Decode thread ID string back to platform data. */ decodeThreadId(threadId: string): GitHubThreadId; /** * Derive channel ID from a GitHub thread ID. * github:{owner}/{repo}:{prNumber}... -> github:{owner}/{repo} */ channelIdFromThreadId(threadId: string): string; /** * List threads (PRs) in a GitHub repository. * Each open PR is treated as a thread. * Note: Issue threads are not listed here — they are created reactively via webhooks. */ listThreads(channelId: string, options?: ListThreadsOptions): Promise>; /** * Fetch GitHub repository info as channel metadata. */ fetchChannelInfo(channelId: string): Promise; /** * Parse a raw message into normalized format. */ parseMessage(raw: GitHubRawMessage): Message; /** * Render formatted content to GitHub markdown. */ renderFormatted(content: FormattedContent): string; fetchSubject(raw: GitHubRawMessage): Promise; } /** * Create a new GitHub adapter instance. * * @example * ```typescript * const chat = new Chat({ * adapters: { * github: createGitHubAdapter({ * token: process.env.GITHUB_TOKEN!, * webhookSecret: process.env.GITHUB_WEBHOOK_SECRET!, * userName: "my-bot", * logger: console, * }), * }, * }); * ``` */ declare function createGitHubAdapter(config?: GitHubAdapterConfig): GitHubAdapter; export { GitHubAdapter, type GitHubAdapterAppConfig, type GitHubAdapterConfig, type GitHubAdapterConnectConfig, type GitHubAdapterMultiTenantAppConfig, type GitHubAdapterPATConfig, type GitHubRawMessage, type GitHubThreadId, type GitHubWebhookVerifier, createGitHubAdapter };