Skip to content

@chamvi/github ​

GitHub App webhooks and tools for chamvi agents. The public API of packages/github/src/index.ts.

bash
npm install @chamvi/github @chamvi/http @chamvi/core ai --save

@chamvi/core, @chamvi/http and ai are peer dependencies. See GitHub App for setup and behaviour.

createGithubApp ​

ts
function createGithubApp(options: GithubAppOptions): IGithubApp;

const DEFAULT_GITHUB_API_URL: 'https://api.github.com';
const DEFAULT_WEBHOOK_PATH: '/webhooks/github';

Throws InvalidAgentDefinitionError for an empty appId, a key that is no RSA private key, an empty webhook secret, an installationId that is no positive integer, a baseUrl that is no absolute https URL without query (empty means https://api.github.com) and a slug that is not the App's lower-case slug (^[a-z0-9]+(-[a-z0-9]+)*$, as in the App's URL; a miscased one would silently disable the self guard).

ts
/*
 * Copyright (c) 2026.
 * Author Peter Placzek (tada5hi)
 * For the full copyright and license information,
 * view the LICENSE file that was distributed with this source code.
 */

import type { ToolSet } from 'ai';
import type { IClock } from '@chamvi/core';
import type { HttpRoute } from '@chamvi/http';
import type { GithubWebhookRouteOptions } from '../webhook/types.ts';
import type { PaginatedOctokit } from './helpers.ts';

export type GithubAppOptions = {
    appId: string | number;
    /** PEM (PKCS#1 or PKCS#8); literal "\n" sequences become newlines. */
    privateKey: string;
    /** Several during a secret rotation; a delivery verifies under any. */
    webhookSecret: string | readonly string[];
    /** For runs not started by a delivery (cron, http, mcp). */
    installationId?: number;
    /** The App's slug; skips `GET /app` for the bot login. */
    slug?: string;
    /** Default DEFAULT_GITHUB_API_URL ('https://api.github.com'). */
    baseUrl?: string;
    clock?: IClock;
    fetch?: typeof globalThis.fetch;
};

/** An Octokit (`@octokit/core` with `paginate`). */
export type GithubOctokit = InstanceType<typeof PaginatedOctokit>;

export interface IGithubApp {
    readonly appId: string;
    readonly installationId?: number;
    /** An Octokit authenticated as the installation (cached token). */
    octokit(installationId: number): Promise<GithubOctokit>;
    /** `<slug>[bot]`, from `slug` or one `GET /app`; cached, single-flight. */
    botLogin(): Promise<string>;
    /** `sha256=<hex>` of the raw body under any webhook secret, compared in constant time. */
    verifySignature(body: Uint8Array, signature: string | null): boolean;
    /** The webhook route for `createHttpApp({ routes })`: `POST /webhooks/github` unless `path` is given. */
    webhookRoute(options?: GithubWebhookRouteOptions): HttpRoute;
    /** The read-only tools (`github_get_issue`, `github_search_issues`, ...), acting as the run's installation. */
    readTools(): ToolSet;
    /**
     * The write tools (`github_add_labels`, `github_create_comment`,
     * `github_close_issue`), effect tools applied at most once per run.
     */
    writeTools(): ToolSet;
}

githubWebhook ​

ts
function githubWebhook(options: GithubWebhookTriggerOptions): TriggerDefinition<GithubEvent>;

Throws InvalidAgentDefinitionError without events or for an event that is not name or name.action (lower case letters, digits and underscores, e.g. projects_v2_item.created).

ts
/*
 * Copyright (c) 2026.
 * Author Peter Placzek (tada5hi)
 * For the full copyright and license information,
 * view the LICENSE file that was distributed with this source code.
 */

/** The parts of a GitHub webhook body chamvi reads; the rest passes through. */
export type GithubPayload = {
    action?: string;
    installation?: { id: number };
    repository?: { full_name: string; [key: string]: unknown };
    sender?: {
        login: string; 
        type: string; 
        [key: string]: unknown 
    };
    issue?: { number: number; [key: string]: unknown };
    pull_request?: { number: number; [key: string]: unknown };
    [key: string]: unknown;
};

/** The envelope payload of a `source: 'github'` run. */
export type GithubEvent = {
    /** X-GitHub-Event, e.g. 'issues'; not covered by the signature, so never a key on its own. */
    name: string;
    action?: string;
    /** X-GitHub-Delivery; not covered by the signature. */
    delivery: string;
    installationId?: number;
    payload: GithubPayload;
};

export type GithubWebhookTriggerOptions = {
    /** 'issues' (every action) or 'issues.opened'; at least one. */
    events: readonly string[];
    /** Default `github:<events joined by ','>`. */
    id?: string;
    /** Runs after the event match; a delivery it refuses starts no run of this trigger. */
    filter?: (event: GithubEvent) => boolean;
    /**
     * Default: the delivery id (applied by the route). The signature does not
     * cover that header, so a replayed body under a new id is a new run;
     * derive the key from the content on an exposed endpoint.
     */
    idempotencyKey?: (event: GithubEvent) => string | undefined;
    /** Default: `<full_name>#<issue|pull_request number>`, else `<full_name>`. */
    concurrencyKey?: (event: GithubEvent) => string | undefined;
};

Webhook route ​

gh.webhookRoute(options?) returns an HttpRoute for createHttpApp({ routes }); it answers 202 with a GithubWebhookResult.

ts
/*
 * Copyright (c) 2026.
 * Author Peter Placzek (tada5hi)
 * For the full copyright and license information,
 * view the LICENSE file that was distributed with this source code.
 */

export type GithubWebhookRouteOptions = {
    /** Default DEFAULT_WEBHOOK_PATH ('/webhooks/github'), under the http app's `basePath`. */
    path?: string;
};

/** The `202` answer to a delivery: the run each matching agent got (deduplicated on a redelivery). */
export type GithubWebhookResult = {
    delivery: string;
    runs: Array<{
        agent: string;
        runId: string;
        deduplicated: boolean;
    }>;
};

Tools ​

gh.readTools() and gh.writeTools() return plain AI SDK ToolSets; the results are these shapes.

ts
/*
 * Copyright (c) 2026.
 * Author Peter Placzek (tada5hi)
 * For the full copyright and license information,
 * view the LICENSE file that was distributed with this source code.
 */

/** The input of a tool on a repository. */
export type RepoInput = { repo: string };

/** The input of a tool on an issue or pull request. */
export type NumberInput = RepoInput & { number: number };

/** `github_get_issue`. */
export type GithubIssue = {
    number: number;
    title: string;
    body: string | null;
    state: string;
    labels: string[];
    author: string | null;
    url: string;
    createdAt: string;
    comments: number;
    isPullRequest: boolean;
};

/** `github_search_issues`. */
export type GithubIssueSearch = {
    total: number;
    items: Array<{
        repo: string;
        number: number;
        title: string;
        state: string;
        url: string;
    }>;
};

/** One entry of `github_list_labels`. */
export type GithubLabel = {
    name: string;
    description: string | null;
};

/** `github_get_pull_request`; `base` and `head` are branch names. */
export type GithubPullRequest = {
    number: number;
    title: string;
    body: string | null;
    state: string;
    draft: boolean;
    merged: boolean;
    base: string;
    head: string;
    author: string | null;
    url: string;
    changedFiles: number;
};

/** `github_get_file` for a file: UTF-8 text, cut at FILE_MAX_CHARS characters. */
export type GithubFile = {
    path: string;
    size: number;
    content: string;
    truncated: boolean;
};

/** `github_get_file` for a directory. */
export type GithubDirectory = {
    path: string;
    entries: Array<{
        name: string;
        type: string;
        path: string;
    }>;
};

/** `github_get_latest_release`; `{ none: true }` when the repository has no release. */
export type GithubRelease = {
    tag: string;
    name: string | null;
    publishedAt: string | null;
    url: string;
    body: string | null;
} | { none: true };

/** `github_create_comment`: the posted (or found) comment. */
export type GithubComment = {
    id: number;
    url: string;
};

/** `github_add_labels`: every label the issue carries afterwards. */
export type GithubIssueLabels = {
    labels: string[];
};

/** `github_close_issue`. */
export type GithubClosedIssue = {
    number: number;
    state: 'closed';
    stateReason: string | null;
};