@chamvi/github
GitHub App webhooks and tools for chamvi agents. The public API of packages/github/src/index.ts.
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
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).
/*
* 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
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).
/*
* 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.
/*
* 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.
/*
* 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;
};