From 365e41f91739c5de0186bb3e92539a4879387766 Mon Sep 17 00:00:00 2001 From: Hugo Richard Date: Fri, 28 Aug 2026 19:51:31 +0100 Subject: [PATCH] refactor(sdk): derive tool registries from a single GITHUB_TOOL_CATALOG --- .changeset/tool-catalog-single-source.md | 5 + .github/CONTRIBUTING.md | 8 +- AGENTS.md | 3 +- packages/github-tools/src/connect/scopes.ts | 129 +--- packages/github-tools/src/core/catalog.ts | 712 ++++++++++++++++++ packages/github-tools/src/core/tool-names.ts | 186 +---- packages/github-tools/src/core/write-tools.ts | 102 +-- packages/github-tools/src/eve/build.test.ts | 5 +- packages/github-tools/src/eve/registry.ts | 596 +-------------- packages/github-tools/src/index.test.ts | 13 + 10 files changed, 793 insertions(+), 966 deletions(-) create mode 100644 .changeset/tool-catalog-single-source.md create mode 100644 packages/github-tools/src/core/catalog.ts create mode 100644 packages/github-tools/src/index.test.ts diff --git a/.changeset/tool-catalog-single-source.md b/.changeset/tool-catalog-single-source.md new file mode 100644 index 0000000..09e09a3 --- /dev/null +++ b/.changeset/tool-catalog-single-source.md @@ -0,0 +1,5 @@ +--- +"@github-tools/sdk": patch +--- + +Internal refactor: introduce `GITHUB_TOOL_CATALOG` as the single source of truth for tool metadata. `GITHUB_TOOL_NAMES`, `GITHUB_WRITE_TOOLS`, `TOOL_CONNECT_SCOPES`, and the eve tool registry are now derived from it instead of being maintained as parallel hand-written registries. No public API changes. diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 8f4fe72..676b97b 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -50,12 +50,10 @@ Every tool splits into a **core** function (pure logic) and a **tool factory** ( 1. **Core logic** — add `{name}InputSchema` (zod, `.describe()` on every field), `{name}Description`, and `{name}Core({ token, ...args })` to `packages/github-tools/src/core/{domain}.ts`. Shape the return — never return the raw Octokit response. 2. **Tool factory** — add the `"use step"` wrapper and the exported factory to `packages/github-tools/src/tools/{domain}.ts`. Read tools take `(token)`; write tools also take `({ needsApproval = true }: ToolOptions = {})`. 3. **Register** (new domain? add a re-export in `packages/github-tools/src/core/index.ts` too): - - `packages/github-tools/src/core/tool-names.ts` — add to `GITHUB_TOOL_NAMES`, with a one-line JSDoc (note "Requires approval by default" for write tools) - - `packages/github-tools/src/core/write-tools.ts` — write tools only: add to `GITHUB_WRITE_TOOLS` + - `packages/github-tools/src/core/catalog.ts` — add one `GITHUB_TOOL_CATALOG` entry (JSDoc, `description`, `inputSchema`, `get core()`, `write: true` for write tools, `connectScopes`). `GITHUB_TOOL_NAMES`, `GITHUB_WRITE_TOOLS`, `TOOL_CONNECT_SCOPES`, and the eve registry are all derived from it — no separate registration + - `packages/github-tools/src/index.ts` — add to `allTools` in `createGithubTools()` (compile-enforced by `satisfies AllGithubTools`), re-export the factory at the bottom (guarded by `src/index.test.ts`) - `packages/github-tools/src/core/presets.ts` — add to every preset it belongs in (update each preset's JSDoc tool list too) - - `packages/github-tools/src/index.ts` — add to `allTools` in `createGithubTools()`, re-export the factory at the bottom - - `packages/github-tools/src/eve/registry.ts` — add an entry so the tool is reachable from `defineDynamic` (direct eve import) and the eve extension - - `packages/github-tools/src/connect/scopes.ts` — add any new Vercel Connect scope the tool needs to `PRESET_CONNECT_SCOPES` for every preset that includes it, and to `TOOL_CONNECT_SCOPES` for the tool itself (used when `include` / `exclude` derive scopes) + - `packages/github-tools/src/connect/scopes.ts` — only if a preset now needs a scope family it did not have: update `PRESET_CONNECT_SCOPES` - `packages/github-tools/src/agents.ts` — mention the tool in `PRESET_INSTRUCTIONS` for presets where it changes the agent's behavior 4. **Chat app metadata** — add a `GITHUB_TOOL_META` entry in `apps/chat/shared/utils/tools/github.ts` 5. **Documentation**: diff --git a/AGENTS.md b/AGENTS.md index aa12bda..f5d4296 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -80,7 +80,8 @@ export const myTool = (token: GithubTokenInput, { needsApproval = true }: ToolOp - `src/client.ts` — `createOctokit(token)` wrapper - `src/types.ts` — `ToolOptions`, `CommitToolOptions`, `ToolOverrides`, `GithubTool` - `src/tools/` — domain files (the `ai` SDK wrapper layer): `repository.ts`, `pull-requests.ts`, `issues.ts`, `reactions.ts`, `discussions.ts`, `notifications.ts`, `commits.ts`, `gists.ts`, `workflows.ts`, `search.ts`, `checks.ts`, `releases.ts`, `bundles.ts` -- `src/core/` — matching domain files (pure logic: schema, description, `*Core` function) plus `tool-names.ts` (`GITHUB_TOOL_NAMES`/`GithubToolName`), `write-tools.ts` (`GITHUB_WRITE_TOOLS`/`GithubWriteToolName`), `presets.ts` (`PRESET_TOOLS`), `token.ts` (`resolveGithubToken`), `approval.ts` (`resolveAiSdkApproval`) +- `src/core/catalog.ts` — **single source of truth for tools**: one `GITHUB_TOOL_CATALOG` entry per tool (description, schema, core, `write` flag, Connect scopes). `GITHUB_TOOL_NAMES`, `GITHUB_WRITE_TOOLS`, `TOOL_CONNECT_SCOPES`, and the eve tool registry are derived from it. Registering a tool = catalog entry + `"use step"` factory + `allTools` in `index.ts` (the last is compile-enforced via `satisfies AllGithubTools`) +- `src/core/` — matching domain files (pure logic: schema, description, `*Core` function) plus `tool-names.ts`/`write-tools.ts` (derived shims), `presets.ts` (`PRESET_TOOLS`), `token.ts` (`resolveGithubToken`), `approval.ts` (`resolveAiSdkApproval`) ### Dual-Mode Agents diff --git a/packages/github-tools/src/connect/scopes.ts b/packages/github-tools/src/connect/scopes.ts index eb3b806..f03b0ca 100644 --- a/packages/github-tools/src/connect/scopes.ts +++ b/packages/github-tools/src/connect/scopes.ts @@ -1,4 +1,5 @@ import { resolvePresetTools, type GithubToolPreset } from '../core/presets' +import { GITHUB_TOOL_CATALOG } from '../core/catalog' import { ALL_GITHUB_TOOL_NAMES, type GithubToolName } from '../core/tool-names' /** @@ -137,126 +138,18 @@ const SCOPE_ORDER = [ 'administration:write', ] as const -const CONTENTS_READ = ['contents:read', 'metadata:read'] as const -const CONTENTS_WRITE = ['contents:read', 'contents:write', 'metadata:read'] as const -const PR_READ = ['contents:read', 'metadata:read', 'pull_requests:read'] as const -const PR_WRITE = ['contents:read', 'metadata:read', 'pull_requests:read', 'pull_requests:write'] as const -/** Read-only PR context (details, files, reviews) plus optional CI checks. */ -const PR_CONTEXT = ['contents:read', 'metadata:read', 'pull_requests:read', 'checks:read', 'statuses:read'] as const -const ISSUES_READ = ['contents:read', 'metadata:read', 'issues:read'] as const -const ISSUES_WRITE = ['contents:read', 'metadata:read', 'issues:read', 'issues:write'] as const -const DISCUSSIONS_READ = ['contents:read', 'metadata:read', 'discussions:read'] as const -const DISCUSSIONS_WRITE = ['contents:read', 'metadata:read', 'discussions:read', 'discussions:write'] as const -const ACTIONS_READ = ['contents:read', 'metadata:read', 'actions:read'] as const -const ACTIONS_WRITE = ['contents:read', 'metadata:read', 'actions:read', 'actions:write'] as const -const CHECKS = ['contents:read', 'metadata:read', 'checks:read', 'statuses:read'] as const -const CI_CONTEXT = ['contents:read', 'metadata:read', 'actions:read', 'checks:read', 'statuses:read'] as const -const ADMIN = ['metadata:read', 'administration:read', 'administration:write'] as const -const SEARCH_REPOS = ['metadata:read'] as const -const SEARCH_ISSUES = ['metadata:read', 'issues:read', 'pull_requests:read'] as const -const UNSCOPED = [] as const - /** - * Per-tool Connect scopes. Empty arrays are intentional for gist and - * notification tools (installation tokens cannot satisfy those APIs). + * Per-tool Connect scopes, derived from `GITHUB_TOOL_CATALOG`. Empty arrays are + * intentional for gist and notification tools (installation tokens cannot + * satisfy those APIs). */ -export const TOOL_CONNECT_SCOPES = { - getRepository: CONTENTS_READ, - listBranches: CONTENTS_READ, - getFileContent: CONTENTS_READ, - getRepositoryTree: CONTENTS_READ, - createBranch: CONTENTS_WRITE, - deleteBranch: CONTENTS_WRITE, - forkRepository: CONTENTS_READ, - createRepository: ADMIN, - createOrUpdateFile: CONTENTS_WRITE, - - listPullRequests: PR_READ, - getPullRequest: PR_READ, - createPullRequest: PR_WRITE, - mergePullRequest: PR_WRITE, - updatePullRequest: PR_WRITE, - addPullRequestComment: PR_WRITE, - updatePullRequestComment: PR_WRITE, - deletePullRequestComment: PR_WRITE, - listPullRequestFiles: PR_READ, - listPullRequestReviews: PR_READ, - listPullRequestReviewThreads: PR_READ, - createPullRequestReview: PR_WRITE, - replyToReviewComment: PR_WRITE, - resolveReviewThread: PR_WRITE, - requestReviewers: PR_WRITE, - getPullRequestContext: PR_CONTEXT, - - listIssues: ISSUES_READ, - getIssue: ISSUES_READ, - getIssueContext: ISSUES_READ, - listIssueComments: ISSUES_READ, - createIssue: ISSUES_WRITE, - addIssueComment: ISSUES_WRITE, - updateIssueComment: ISSUES_WRITE, - deleteIssueComment: ISSUES_WRITE, - closeIssue: ISSUES_WRITE, - updateIssue: ISSUES_WRITE, - listLabels: ISSUES_READ, - addLabels: ISSUES_WRITE, - removeLabel: ISSUES_WRITE, - createLabel: ISSUES_WRITE, - updateLabel: ISSUES_WRITE, - deleteLabel: ISSUES_WRITE, - addAssignees: ISSUES_WRITE, - removeAssignees: ISSUES_WRITE, - - searchCode: CONTENTS_READ, - searchRepositories: SEARCH_REPOS, - searchIssues: SEARCH_ISSUES, - - listCommits: CONTENTS_READ, - getCommit: CONTENTS_READ, - getBlame: CONTENTS_READ, - compareCommits: CONTENTS_READ, - - listGists: UNSCOPED, - getGist: UNSCOPED, - listGistComments: UNSCOPED, - createGist: UNSCOPED, - updateGist: UNSCOPED, - deleteGist: UNSCOPED, - createGistComment: UNSCOPED, - - listWorkflows: ACTIONS_READ, - listWorkflowRuns: ACTIONS_READ, - getWorkflowRun: ACTIONS_READ, - listWorkflowJobs: ACTIONS_READ, - getWorkflowJobLogs: ACTIONS_READ, - triggerWorkflow: ACTIONS_WRITE, - cancelWorkflowRun: ACTIONS_WRITE, - rerunWorkflowRun: ACTIONS_WRITE, - - listCheckRuns: CHECKS, - getCombinedStatus: CHECKS, - getCiFailureContext: CI_CONTEXT, - - listDiscussions: DISCUSSIONS_READ, - getDiscussion: DISCUSSIONS_READ, - addDiscussionComment: DISCUSSIONS_WRITE, - - listNotifications: UNSCOPED, - markNotificationRead: UNSCOPED, - - listIssueReactions: ISSUES_READ, - addIssueReaction: ISSUES_WRITE, - listCommentReactions: ISSUES_READ, - addCommentReaction: ISSUES_WRITE, - - listReleases: CONTENTS_READ, - getLatestRelease: CONTENTS_READ, - getRelease: CONTENTS_READ, - getReleaseContext: CONTENTS_READ, - createRelease: CONTENTS_WRITE, - updateRelease: CONTENTS_WRITE, - deleteRelease: CONTENTS_WRITE, -} as const satisfies Record +export const TOOL_CONNECT_SCOPES = ALL_GITHUB_TOOL_NAMES.reduce( + (map, name) => { + map[name] = GITHUB_TOOL_CATALOG[name].connectScopes + return map + }, + {} as Record, +) function orderScopes(scopes: Set): string[] { return SCOPE_ORDER.filter(scope => scopes.has(scope)) diff --git a/packages/github-tools/src/core/catalog.ts b/packages/github-tools/src/core/catalog.ts new file mode 100644 index 0000000..8c95bc8 --- /dev/null +++ b/packages/github-tools/src/core/catalog.ts @@ -0,0 +1,712 @@ +import type { z } from 'zod' +import * as bundles from './bundles' +import * as checks from './checks' +import * as commits from './commits' +import * as discussions from './discussions' +import * as gists from './gists' +import * as issues from './issues' +import * as notifications from './notifications' +import * as pullRequests from './pull-requests' +import * as reactions from './reactions' +import * as releases from './releases' +import * as repository from './repository' +import * as search from './search' +import * as workflows from './workflows' + +/** + * Vercel Connect scope sets, mirroring GitHub App permissions. + * Release tools fall under `contents`, reaction tools under `issues`. + * Gist and notification tools are intentionally unscoped: those APIs only + * accept user access tokens, never the installation tokens Connect mints. + */ +export const CONTENTS_READ = ['contents:read', 'metadata:read'] as const +export const CONTENTS_WRITE = ['contents:read', 'contents:write', 'metadata:read'] as const +export const PR_READ = ['contents:read', 'metadata:read', 'pull_requests:read'] as const +export const PR_WRITE = ['contents:read', 'metadata:read', 'pull_requests:read', 'pull_requests:write'] as const +/** Read-only PR context (details, files, reviews) plus optional CI checks. */ +export const PR_CONTEXT = ['contents:read', 'metadata:read', 'pull_requests:read', 'checks:read', 'statuses:read'] as const +export const ISSUES_READ = ['contents:read', 'metadata:read', 'issues:read'] as const +export const ISSUES_WRITE = ['contents:read', 'metadata:read', 'issues:read', 'issues:write'] as const +export const DISCUSSIONS_READ = ['contents:read', 'metadata:read', 'discussions:read'] as const +export const DISCUSSIONS_WRITE = ['contents:read', 'metadata:read', 'discussions:read', 'discussions:write'] as const +export const ACTIONS_READ = ['contents:read', 'metadata:read', 'actions:read'] as const +export const ACTIONS_WRITE = ['contents:read', 'metadata:read', 'actions:read', 'actions:write'] as const +export const CHECKS = ['contents:read', 'metadata:read', 'checks:read', 'statuses:read'] as const +export const CI_CONTEXT = ['contents:read', 'metadata:read', 'actions:read', 'checks:read', 'statuses:read'] as const +export const ADMIN = ['metadata:read', 'administration:read', 'administration:write'] as const +export const SEARCH_REPOS = ['metadata:read'] as const +export const SEARCH_ISSUES = ['metadata:read', 'issues:read', 'pull_requests:read'] as const +export const UNSCOPED = [] as const + +/** + * One tool in the catalog. `core` argument types vary per tool, so the field + * is typed contravariantly (`never`) — callers cast at the single dispatch + * boundary (`withToken` in the eve registry). `core` is a getter so the ESM + * binding stays live (module spies in tests, hot reload). + */ +type GithubToolDescriptor = { + description: string + inputSchema: z.ZodType + core: (args: never) => Promise + /** Present on write tools — drives `GITHUB_WRITE_TOOLS` and approval defaults. */ + write?: true + connectScopes: readonly string[] +} + +/** + * Single source of truth for every GitHub tool. + * + * `GITHUB_TOOL_NAMES`, `GITHUB_WRITE_TOOLS`, `TOOL_CONNECT_SCOPES`, and the + * eve tool registry are all derived from this catalog. Adding a tool here (plus + * its `"use step"` factory in `src/tools/` and `allTools` in `src/index.ts`, + * both enforced at compile time) is the only registration needed. + */ +export const GITHUB_TOOL_CATALOG = { + /** Get information about a GitHub repository including description, stars, forks, language, and default branch. */ + getRepository: { + description: repository.getRepositoryDescription, + inputSchema: repository.getRepositoryInputSchema, + get core() { return repository.getRepositoryCore }, + connectScopes: CONTENTS_READ, + }, + /** List branches in a GitHub repository. */ + listBranches: { + description: repository.listBranchesDescription, + inputSchema: repository.listBranchesInputSchema, + get core() { return repository.listBranchesCore }, + connectScopes: CONTENTS_READ, + }, + /** Get the content of a file from a GitHub repository. Prefer startLine/endLine or maxLines for large files. */ + getFileContent: { + description: repository.getFileContentDescription, + inputSchema: repository.getFileContentInputSchema, + get core() { return repository.getFileContentCore }, + connectScopes: CONTENTS_READ, + }, + /** List the file and directory structure of a repository at a given ref. */ + getRepositoryTree: { + description: repository.getRepositoryTreeDescription, + inputSchema: repository.getRepositoryTreeInputSchema, + get core() { return repository.getRepositoryTreeCore }, + connectScopes: CONTENTS_READ, + }, + /** Create a new branch in a GitHub repository from an existing branch or commit SHA. Requires approval by default. */ + createBranch: { + description: repository.createBranchDescription, + inputSchema: repository.createBranchInputSchema, + get core() { return repository.createBranchCore }, + write: true, + connectScopes: CONTENTS_WRITE, + }, + /** Delete a branch from a GitHub repository permanently. Requires approval by default. */ + deleteBranch: { + description: repository.deleteBranchDescription, + inputSchema: repository.deleteBranchInputSchema, + get core() { return repository.deleteBranchCore }, + write: true, + connectScopes: CONTENTS_WRITE, + }, + /** Fork a GitHub repository to the authenticated user account or a specified organization. Requires approval by default. */ + forkRepository: { + description: repository.forkRepositoryDescription, + inputSchema: repository.forkRepositoryInputSchema, + get core() { return repository.forkRepositoryCore }, + write: true, + connectScopes: CONTENTS_READ, + }, + /** Create a new GitHub repository for the authenticated user or a specified organization. Requires approval by default. */ + createRepository: { + description: repository.createRepositoryDescription, + inputSchema: repository.createRepositoryInputSchema, + get core() { return repository.createRepositoryCore }, + write: true, + connectScopes: ADMIN, + }, + /** Create or update a file in a GitHub repository. Provide the SHA when updating an existing file. Requires approval by default. */ + createOrUpdateFile: { + description: repository.createOrUpdateFileDescription, + inputSchema: repository.createOrUpdateFileInputSchema, + get core() { return repository.createOrUpdateFileCore }, + write: true, + connectScopes: CONTENTS_WRITE, + }, + /** List pull requests for a GitHub repository. */ + listPullRequests: { + description: pullRequests.listPullRequestsDescription, + inputSchema: pullRequests.listPullRequestsInputSchema, + get core() { return pullRequests.listPullRequestsCore }, + connectScopes: PR_READ, + }, + /** Get detailed information about a specific pull request. Body truncated by default (detail: summary). */ + getPullRequest: { + description: pullRequests.getPullRequestDescription, + inputSchema: pullRequests.getPullRequestInputSchema, + get core() { return pullRequests.getPullRequestCore }, + connectScopes: PR_READ, + }, + /** Create a new pull request in a GitHub repository. Requires approval by default. */ + createPullRequest: { + description: pullRequests.createPullRequestDescription, + inputSchema: pullRequests.createPullRequestInputSchema, + get core() { return pullRequests.createPullRequestCore }, + write: true, + connectScopes: PR_WRITE, + }, + /** Merge a pull request. Requires approval by default. */ + mergePullRequest: { + description: pullRequests.mergePullRequestDescription, + inputSchema: pullRequests.mergePullRequestInputSchema, + get core() { return pullRequests.mergePullRequestCore }, + write: true, + connectScopes: PR_WRITE, + }, + /** Update a pull request — title, body, state, base branch, or draft status. Requires approval by default. */ + updatePullRequest: { + description: pullRequests.updatePullRequestDescription, + inputSchema: pullRequests.updatePullRequestInputSchema, + get core() { return pullRequests.updatePullRequestCore }, + write: true, + connectScopes: PR_WRITE, + }, + /** Add a comment to a pull request. Requires approval by default. */ + addPullRequestComment: { + description: pullRequests.addPullRequestCommentDescription, + inputSchema: pullRequests.addPullRequestCommentInputSchema, + get core() { return pullRequests.addPullRequestCommentCore }, + write: true, + connectScopes: PR_WRITE, + }, + /** Update the body of a comment on a pull request. Requires approval by default. */ + updatePullRequestComment: { + description: pullRequests.updatePullRequestCommentDescription, + inputSchema: pullRequests.updatePullRequestCommentInputSchema, + get core() { return pullRequests.updatePullRequestCommentCore }, + write: true, + connectScopes: PR_WRITE, + }, + /** Delete a comment from a pull request permanently. Requires approval by default. */ + deletePullRequestComment: { + description: pullRequests.deletePullRequestCommentDescription, + inputSchema: pullRequests.deletePullRequestCommentInputSchema, + get core() { return pullRequests.deletePullRequestCommentCore }, + write: true, + connectScopes: PR_WRITE, + }, + /** List files changed in a pull request with status and stats. Patches omitted by default — set includePatch true for diffs. */ + listPullRequestFiles: { + description: pullRequests.listPullRequestFilesDescription, + inputSchema: pullRequests.listPullRequestFilesInputSchema, + get core() { return pullRequests.listPullRequestFilesCore }, + connectScopes: PR_READ, + }, + /** List reviews on a pull request (approvals, change requests, and comments). */ + listPullRequestReviews: { + description: pullRequests.listPullRequestReviewsDescription, + inputSchema: pullRequests.listPullRequestReviewsInputSchema, + get core() { return pullRequests.listPullRequestReviewsCore }, + connectScopes: PR_READ, + }, + /** Submit a pull request review — approve, request changes, or comment with optional inline comments on specific lines. Requires approval by default. */ + createPullRequestReview: { + description: pullRequests.createPullRequestReviewDescription, + inputSchema: pullRequests.createPullRequestReviewInputSchema, + get core() { return pullRequests.createPullRequestReviewCore }, + write: true, + connectScopes: PR_WRITE, + }, + /** List review threads on a pull request with comments, resolution state, and the IDs needed to reply or resolve. Unresolved only by default. */ + listPullRequestReviewThreads: { + description: pullRequests.listPullRequestReviewThreadsDescription, + inputSchema: pullRequests.listPullRequestReviewThreadsInputSchema, + get core() { return pullRequests.listPullRequestReviewThreadsCore }, + connectScopes: PR_READ, + }, + /** Reply to a pull request review comment in its review thread. Requires approval by default. */ + replyToReviewComment: { + description: pullRequests.replyToReviewCommentDescription, + inputSchema: pullRequests.replyToReviewCommentInputSchema, + get core() { return pullRequests.replyToReviewCommentCore }, + write: true, + connectScopes: PR_WRITE, + }, + /** Mark a pull request review thread as resolved. Requires approval by default. */ + resolveReviewThread: { + description: pullRequests.resolveReviewThreadDescription, + inputSchema: pullRequests.resolveReviewThreadInputSchema, + get core() { return pullRequests.resolveReviewThreadCore }, + write: true, + connectScopes: PR_WRITE, + }, + /** Request reviews from users or teams on a pull request. Requires approval by default. */ + requestReviewers: { + description: pullRequests.requestReviewersDescription, + inputSchema: pullRequests.requestReviewersInputSchema, + get core() { return pullRequests.requestReviewersCore }, + write: true, + connectScopes: PR_WRITE, + }, + /** Fetch pull request details plus files, reviews, and optional CI checks in one call. */ + getPullRequestContext: { + description: bundles.getPullRequestContextDescription, + inputSchema: bundles.getPullRequestContextInputSchema, + get core() { return bundles.getPullRequestContextCore }, + connectScopes: PR_CONTEXT, + }, + /** Fetch an issue plus available label names and recent comments in one call. */ + getIssueContext: { + description: bundles.getIssueContextDescription, + inputSchema: bundles.getIssueContextInputSchema, + get core() { return bundles.getIssueContextCore }, + connectScopes: ISSUES_READ, + }, + /** List issues for a GitHub repository (excludes pull requests). */ + listIssues: { + description: issues.listIssuesDescription, + inputSchema: issues.listIssuesInputSchema, + get core() { return issues.listIssuesCore }, + connectScopes: ISSUES_READ, + }, + /** Get detailed information about a specific issue. Body truncated by default (detail: summary). */ + getIssue: { + description: issues.getIssueDescription, + inputSchema: issues.getIssueInputSchema, + get core() { return issues.getIssueCore }, + connectScopes: ISSUES_READ, + }, + /** List comments on a GitHub issue. Bodies are truncated by default (detail: summary). Prefer getIssueContext for the first page when triaging. */ + listIssueComments: { + description: issues.listIssueCommentsDescription, + inputSchema: issues.listIssueCommentsInputSchema, + get core() { return issues.listIssueCommentsCore }, + connectScopes: ISSUES_READ, + }, + /** Create a new issue in a GitHub repository. Requires approval by default. */ + createIssue: { + description: issues.createIssueDescription, + inputSchema: issues.createIssueInputSchema, + get core() { return issues.createIssueCore }, + write: true, + connectScopes: ISSUES_WRITE, + }, + /** Add a comment to a GitHub issue. Requires approval by default. */ + addIssueComment: { + description: issues.addIssueCommentDescription, + inputSchema: issues.addIssueCommentInputSchema, + get core() { return issues.addIssueCommentCore }, + write: true, + connectScopes: ISSUES_WRITE, + }, + /** Update the body of a comment on a GitHub issue. Requires approval by default. */ + updateIssueComment: { + description: issues.updateIssueCommentDescription, + inputSchema: issues.updateIssueCommentInputSchema, + get core() { return issues.updateIssueCommentCore }, + write: true, + connectScopes: ISSUES_WRITE, + }, + /** Delete a comment from a GitHub issue permanently. Requires approval by default. */ + deleteIssueComment: { + description: issues.deleteIssueCommentDescription, + inputSchema: issues.deleteIssueCommentInputSchema, + get core() { return issues.deleteIssueCommentCore }, + write: true, + connectScopes: ISSUES_WRITE, + }, + /** Close an open GitHub issue. Requires approval by default. */ + closeIssue: { + description: issues.closeIssueDescription, + inputSchema: issues.closeIssueInputSchema, + get core() { return issues.closeIssueCore }, + write: true, + connectScopes: ISSUES_WRITE, + }, + /** Update a GitHub issue — title, body, state, labels, milestone, or assignees. Requires approval by default. */ + updateIssue: { + description: issues.updateIssueDescription, + inputSchema: issues.updateIssueInputSchema, + get core() { return issues.updateIssueCore }, + write: true, + connectScopes: ISSUES_WRITE, + }, + /** List labels available in a GitHub repository. */ + listLabels: { + description: issues.listLabelsDescription, + inputSchema: issues.listLabelsInputSchema, + get core() { return issues.listLabelsCore }, + connectScopes: ISSUES_READ, + }, + /** Add labels to an issue or pull request. Requires approval by default. */ + addLabels: { + description: issues.addLabelsDescription, + inputSchema: issues.addLabelsInputSchema, + get core() { return issues.addLabelsCore }, + write: true, + connectScopes: ISSUES_WRITE, + }, + /** Remove a label from an issue or pull request. Requires approval by default. */ + removeLabel: { + description: issues.removeLabelDescription, + inputSchema: issues.removeLabelInputSchema, + get core() { return issues.removeLabelCore }, + write: true, + connectScopes: ISSUES_WRITE, + }, + /** Create a label in a GitHub repository. Requires approval by default. */ + createLabel: { + description: issues.createLabelDescription, + inputSchema: issues.createLabelInputSchema, + get core() { return issues.createLabelCore }, + write: true, + connectScopes: ISSUES_WRITE, + }, + /** Update a label in a GitHub repository — name, color, or description. Requires approval by default. */ + updateLabel: { + description: issues.updateLabelDescription, + inputSchema: issues.updateLabelInputSchema, + get core() { return issues.updateLabelCore }, + write: true, + connectScopes: ISSUES_WRITE, + }, + /** Delete a label from a GitHub repository permanently. Requires approval by default. */ + deleteLabel: { + description: issues.deleteLabelDescription, + inputSchema: issues.deleteLabelInputSchema, + get core() { return issues.deleteLabelCore }, + write: true, + connectScopes: ISSUES_WRITE, + }, + /** Assign users to an issue or pull request. Requires approval by default. */ + addAssignees: { + description: issues.addAssigneesDescription, + inputSchema: issues.addAssigneesInputSchema, + get core() { return issues.addAssigneesCore }, + write: true, + connectScopes: ISSUES_WRITE, + }, + /** Remove assignees from an issue or pull request. Requires approval by default. */ + removeAssignees: { + description: issues.removeAssigneesDescription, + inputSchema: issues.removeAssigneesInputSchema, + get core() { return issues.removeAssigneesCore }, + write: true, + connectScopes: ISSUES_WRITE, + }, + /** List reactions on an issue or pull request conversation, with per-emoji counts. */ + listIssueReactions: { + description: reactions.listIssueReactionsDescription, + inputSchema: reactions.listIssueReactionsInputSchema, + get core() { return reactions.listIssueReactionsCore }, + connectScopes: ISSUES_READ, + }, + /** React to an issue or pull request with an emoji. Requires approval by default. */ + addIssueReaction: { + description: reactions.addIssueReactionDescription, + inputSchema: reactions.addIssueReactionInputSchema, + get core() { return reactions.addIssueReactionCore }, + write: true, + connectScopes: ISSUES_WRITE, + }, + /** List reactions on an issue or pull request comment, with per-emoji counts. */ + listCommentReactions: { + description: reactions.listCommentReactionsDescription, + inputSchema: reactions.listCommentReactionsInputSchema, + get core() { return reactions.listCommentReactionsCore }, + connectScopes: ISSUES_READ, + }, + /** React to an issue or pull request comment with an emoji. Requires approval by default. */ + addCommentReaction: { + description: reactions.addCommentReactionDescription, + inputSchema: reactions.addCommentReactionInputSchema, + get core() { return reactions.addCommentReactionCore }, + write: true, + connectScopes: ISSUES_WRITE, + }, + /** List discussions in a GitHub repository, most recently updated first, optionally filtered by category. */ + listDiscussions: { + description: discussions.listDiscussionsDescription, + inputSchema: discussions.listDiscussionsInputSchema, + get core() { return discussions.listDiscussionsCore }, + connectScopes: DISCUSSIONS_READ, + }, + /** Get a GitHub discussion by number. Body truncated by default (detail: summary). */ + getDiscussion: { + description: discussions.getDiscussionDescription, + inputSchema: discussions.getDiscussionInputSchema, + get core() { return discussions.getDiscussionCore }, + connectScopes: DISCUSSIONS_READ, + }, + /** Add a comment to a GitHub discussion. Requires approval by default. */ + addDiscussionComment: { + description: discussions.addDiscussionCommentDescription, + inputSchema: discussions.addDiscussionCommentInputSchema, + get core() { return discussions.addDiscussionCommentCore }, + write: true, + connectScopes: DISCUSSIONS_WRITE, + }, + /** List notification threads for the authenticated user. Requires a token with notifications access. */ + listNotifications: { + description: notifications.listNotificationsDescription, + inputSchema: notifications.listNotificationsInputSchema, + get core() { return notifications.listNotificationsCore }, + connectScopes: UNSCOPED, + }, + /** Mark a single notification thread as read. Requires approval by default. */ + markNotificationRead: { + description: notifications.markNotificationReadDescription, + inputSchema: notifications.markNotificationReadInputSchema, + get core() { return notifications.markNotificationReadCore }, + write: true, + connectScopes: UNSCOPED, + }, + /** Search for code in GitHub repositories. Use qualifiers like "repo:owner/name" to scope the search. Results include matching text snippets when GitHub returns them. */ + searchCode: { + description: search.searchCodeDescription, + inputSchema: search.searchCodeInputSchema, + get core() { return search.searchCodeCore }, + connectScopes: CONTENTS_READ, + }, + /** Search for GitHub repositories by keyword, topic, language, or other qualifiers. */ + searchRepositories: { + description: search.searchRepositoriesDescription, + inputSchema: search.searchRepositoriesInputSchema, + get core() { return search.searchRepositoriesCore }, + connectScopes: SEARCH_REPOS, + }, + /** Search for issues and pull requests across GitHub using search qualifiers like "repo:owner/name is:open". */ + searchIssues: { + description: search.searchIssuesDescription, + inputSchema: search.searchIssuesInputSchema, + get core() { return search.searchIssuesCore }, + connectScopes: SEARCH_ISSUES, + }, + /** List commits for a GitHub repository. Filter by file path to see commits that touched a file. For line-by-line attribution at a given ref, use getBlame instead. */ + listCommits: { + description: commits.listCommitsDescription, + inputSchema: commits.listCommitsInputSchema, + get core() { return commits.listCommitsCore }, + connectScopes: CONTENTS_READ, + }, + /** Get detailed information about a specific commit, including the list of files changed. Patches omitted by default. */ + getCommit: { + description: commits.getCommitDescription, + inputSchema: commits.getCommitInputSchema, + get core() { return commits.getCommitCore }, + connectScopes: CONTENTS_READ, + }, + /** Line-level git blame for a file at a commit-like ref (branch, tag, or SHA). Returns contiguous ranges mapping lines to the commits that last modified them. */ + getBlame: { + description: commits.getBlameDescription, + inputSchema: commits.getBlameInputSchema, + get core() { return commits.getBlameCore }, + connectScopes: CONTENTS_READ, + }, + /** Compare two branches, tags, or commits — ahead/behind counts, commits in between, and differing files. Patches omitted by default. */ + compareCommits: { + description: commits.compareCommitsDescription, + inputSchema: commits.compareCommitsInputSchema, + get core() { return commits.compareCommitsCore }, + connectScopes: CONTENTS_READ, + }, + /** List gists for the authenticated user or a specific user. */ + listGists: { + description: gists.listGistsDescription, + inputSchema: gists.listGistsInputSchema, + get core() { return gists.listGistsCore }, + connectScopes: UNSCOPED, + }, + /** Get a gist by ID, including file contents. */ + getGist: { + description: gists.getGistDescription, + inputSchema: gists.getGistInputSchema, + get core() { return gists.getGistCore }, + connectScopes: UNSCOPED, + }, + /** List comments on a gist. */ + listGistComments: { + description: gists.listGistCommentsDescription, + inputSchema: gists.listGistCommentsInputSchema, + get core() { return gists.listGistCommentsCore }, + connectScopes: UNSCOPED, + }, + /** Create a new gist with one or more files. Requires approval by default. */ + createGist: { + description: gists.createGistDescription, + inputSchema: gists.createGistInputSchema, + get core() { return gists.createGistCore }, + write: true, + connectScopes: UNSCOPED, + }, + /** Update an existing gist — edit description, update files, or remove files. Requires approval by default. */ + updateGist: { + description: gists.updateGistDescription, + inputSchema: gists.updateGistInputSchema, + get core() { return gists.updateGistCore }, + write: true, + connectScopes: UNSCOPED, + }, + /** Delete a gist permanently. Requires approval by default. */ + deleteGist: { + description: gists.deleteGistDescription, + inputSchema: gists.deleteGistInputSchema, + get core() { return gists.deleteGistCore }, + write: true, + connectScopes: UNSCOPED, + }, + /** Add a comment to a gist. Requires approval by default. */ + createGistComment: { + description: gists.createGistCommentDescription, + inputSchema: gists.createGistCommentInputSchema, + get core() { return gists.createGistCommentCore }, + write: true, + connectScopes: UNSCOPED, + }, + /** List GitHub Actions workflows in a repository. */ + listWorkflows: { + description: workflows.listWorkflowsDescription, + inputSchema: workflows.listWorkflowsInputSchema, + get core() { return workflows.listWorkflowsCore }, + connectScopes: ACTIONS_READ, + }, + /** List workflow runs for a repository, optionally filtered by workflow, branch, status, or event. */ + listWorkflowRuns: { + description: workflows.listWorkflowRunsDescription, + inputSchema: workflows.listWorkflowRunsInputSchema, + get core() { return workflows.listWorkflowRunsCore }, + connectScopes: ACTIONS_READ, + }, + /** Get details of a specific workflow run including status, timing, and trigger info. */ + getWorkflowRun: { + description: workflows.getWorkflowRunDescription, + inputSchema: workflows.getWorkflowRunInputSchema, + get core() { return workflows.getWorkflowRunCore }, + connectScopes: ACTIONS_READ, + }, + /** List jobs for a workflow run, including step-level status and timing. */ + listWorkflowJobs: { + description: workflows.listWorkflowJobsDescription, + inputSchema: workflows.listWorkflowJobsInputSchema, + get core() { return workflows.listWorkflowJobsCore }, + connectScopes: ACTIONS_READ, + }, + /** Get the log output of a workflow job to diagnose failures. Returns the tail (default 200 lines) with timestamps stripped. */ + getWorkflowJobLogs: { + description: workflows.getWorkflowJobLogsDescription, + inputSchema: workflows.getWorkflowJobLogsInputSchema, + get core() { return workflows.getWorkflowJobLogsCore }, + connectScopes: ACTIONS_READ, + }, + /** Trigger a workflow via workflow_dispatch event. Requires approval by default. */ + triggerWorkflow: { + description: workflows.triggerWorkflowDescription, + inputSchema: workflows.triggerWorkflowInputSchema, + get core() { return workflows.triggerWorkflowCore }, + write: true, + connectScopes: ACTIONS_WRITE, + }, + /** Cancel an in-progress workflow run. Requires approval by default. */ + cancelWorkflowRun: { + description: workflows.cancelWorkflowRunDescription, + inputSchema: workflows.cancelWorkflowRunInputSchema, + get core() { return workflows.cancelWorkflowRunCore }, + write: true, + connectScopes: ACTIONS_WRITE, + }, + /** Re-run a workflow run, optionally only the failed jobs. Requires approval by default. */ + rerunWorkflowRun: { + description: workflows.rerunWorkflowRunDescription, + inputSchema: workflows.rerunWorkflowRunInputSchema, + get core() { return workflows.rerunWorkflowRunCore }, + write: true, + connectScopes: ACTIONS_WRITE, + }, + /** List check runs (Checks API — GitHub Actions and other CI providers) for a commit, branch, or tag. */ + listCheckRuns: { + description: checks.listCheckRunsDescription, + inputSchema: checks.listCheckRunsInputSchema, + get core() { return checks.listCheckRunsCore }, + connectScopes: CHECKS, + }, + /** Get the combined commit status (Statuses API — legacy CI integrations) for a commit, branch, or tag. */ + getCombinedStatus: { + description: checks.getCombinedStatusDescription, + inputSchema: checks.getCombinedStatusInputSchema, + get core() { return checks.getCombinedStatusCore }, + connectScopes: CHECKS, + }, + /** Diagnose CI failures for a ref — combined status, failing checks, and failed workflow jobs in one call. */ + getCiFailureContext: { + description: bundles.getCiFailureContextDescription, + inputSchema: bundles.getCiFailureContextInputSchema, + get core() { return bundles.getCiFailureContextCore }, + connectScopes: CI_CONTEXT, + }, + /** List releases for a GitHub repository, newest first (includes drafts and prereleases). */ + listReleases: { + description: releases.listReleasesDescription, + inputSchema: releases.listReleasesInputSchema, + get core() { return releases.listReleasesCore }, + connectScopes: CONTENTS_READ, + }, + /** Get the latest published release for a GitHub repository (excludes drafts and prereleases). Body truncated by default. */ + getLatestRelease: { + description: releases.getLatestReleaseDescription, + inputSchema: releases.getLatestReleaseInputSchema, + get core() { return releases.getLatestReleaseCore }, + connectScopes: CONTENTS_READ, + }, + /** Get a specific release by ID, including its assets. Body truncated by default. */ + getRelease: { + description: releases.getReleaseDescription, + inputSchema: releases.getReleaseInputSchema, + get core() { return releases.getReleaseCore }, + connectScopes: CONTENTS_READ, + }, + /** Fetch a release plus the previous release and tag comparison in one call. */ + getReleaseContext: { + description: bundles.getReleaseContextDescription, + inputSchema: bundles.getReleaseContextInputSchema, + get core() { return bundles.getReleaseContextCore }, + connectScopes: CONTENTS_READ, + }, + /** Create a new release (and its tag if needed) in a GitHub repository. Requires approval by default. */ + createRelease: { + description: releases.createReleaseDescription, + inputSchema: releases.createReleaseInputSchema, + get core() { return releases.createReleaseCore }, + write: true, + connectScopes: CONTENTS_WRITE, + }, + /** Update an existing release — tag, target, title, notes, draft, or prerelease status. Requires approval by default. */ + updateRelease: { + description: releases.updateReleaseDescription, + inputSchema: releases.updateReleaseInputSchema, + get core() { return releases.updateReleaseCore }, + write: true, + connectScopes: CONTENTS_WRITE, + }, + /** Delete a release permanently. Requires approval by default. */ + deleteRelease: { + description: releases.deleteReleaseDescription, + inputSchema: releases.deleteReleaseInputSchema, + get core() { return releases.deleteReleaseCore }, + write: true, + connectScopes: CONTENTS_WRITE, + }, +} satisfies Record + +export type GithubToolName = keyof typeof GITHUB_TOOL_CATALOG + +/** Tool names whose catalog entry is marked `write: true`. */ +export type GithubWriteToolName = { + [K in GithubToolName]: (typeof GITHUB_TOOL_CATALOG)[K] extends { write: true } ? K : never +}[GithubToolName] + +export const ALL_GITHUB_TOOL_NAMES = Object.keys(GITHUB_TOOL_CATALOG) as GithubToolName[] + +export const GITHUB_WRITE_TOOL_NAMES = ALL_GITHUB_TOOL_NAMES.filter( + name => 'write' in GITHUB_TOOL_CATALOG[name], +) as GithubWriteToolName[] + +export function isGithubWriteToolName(name: GithubToolName): name is GithubWriteToolName { + return 'write' in GITHUB_TOOL_CATALOG[name] +} diff --git a/packages/github-tools/src/core/tool-names.ts b/packages/github-tools/src/core/tool-names.ts index 6e7db75..9d9e6ae 100644 --- a/packages/github-tools/src/core/tool-names.ts +++ b/packages/github-tools/src/core/tool-names.ts @@ -1,178 +1,12 @@ -/** - * All GitHub tool names available via {@link createGithubTools}. - * Each key maps to its own string literal for IDE autocomplete and hover docs. - */ -export const GITHUB_TOOL_NAMES = { - /** Get information about a GitHub repository including description, stars, forks, language, and default branch. */ - getRepository: 'getRepository', - /** List branches in a GitHub repository. */ - listBranches: 'listBranches', - /** Get the content of a file from a GitHub repository. Prefer startLine/endLine or maxLines for large files. */ - getFileContent: 'getFileContent', - /** List the file and directory structure of a repository at a given ref. */ - getRepositoryTree: 'getRepositoryTree', - /** Create a new branch in a GitHub repository from an existing branch or commit SHA. Requires approval by default. */ - createBranch: 'createBranch', - /** Delete a branch from a GitHub repository permanently. Requires approval by default. */ - deleteBranch: 'deleteBranch', - /** Fork a GitHub repository to the authenticated user account or a specified organization. Requires approval by default. */ - forkRepository: 'forkRepository', - /** Create a new GitHub repository for the authenticated user or a specified organization. Requires approval by default. */ - createRepository: 'createRepository', - /** Create or update a file in a GitHub repository. Provide the SHA when updating an existing file. Requires approval by default. */ - createOrUpdateFile: 'createOrUpdateFile', - /** List pull requests for a GitHub repository. */ - listPullRequests: 'listPullRequests', - /** Get detailed information about a specific pull request. Body truncated by default (detail: summary). */ - getPullRequest: 'getPullRequest', - /** Create a new pull request in a GitHub repository. Requires approval by default. */ - createPullRequest: 'createPullRequest', - /** Merge a pull request. Requires approval by default. */ - mergePullRequest: 'mergePullRequest', - /** Update a pull request — title, body, state, base branch, or draft status. Requires approval by default. */ - updatePullRequest: 'updatePullRequest', - /** Add a comment to a pull request. Requires approval by default. */ - addPullRequestComment: 'addPullRequestComment', - /** Update the body of a comment on a pull request. Requires approval by default. */ - updatePullRequestComment: 'updatePullRequestComment', - /** Delete a comment from a pull request permanently. Requires approval by default. */ - deletePullRequestComment: 'deletePullRequestComment', - /** List files changed in a pull request with status and stats. Patches omitted by default — set includePatch true for diffs. */ - listPullRequestFiles: 'listPullRequestFiles', - /** List reviews on a pull request (approvals, change requests, and comments). */ - listPullRequestReviews: 'listPullRequestReviews', - /** Submit a pull request review — approve, request changes, or comment with optional inline comments on specific lines. Requires approval by default. */ - createPullRequestReview: 'createPullRequestReview', - /** List review threads on a pull request with comments, resolution state, and the IDs needed to reply or resolve. Unresolved only by default. */ - listPullRequestReviewThreads: 'listPullRequestReviewThreads', - /** Reply to a pull request review comment in its review thread. Requires approval by default. */ - replyToReviewComment: 'replyToReviewComment', - /** Mark a pull request review thread as resolved. Requires approval by default. */ - resolveReviewThread: 'resolveReviewThread', - /** Request reviews from users or teams on a pull request. Requires approval by default. */ - requestReviewers: 'requestReviewers', - /** Fetch pull request details plus files, reviews, and optional CI checks in one call. */ - getPullRequestContext: 'getPullRequestContext', - /** List issues for a GitHub repository (excludes pull requests). */ - listIssues: 'listIssues', - /** Get detailed information about a specific issue. Body truncated by default (detail: summary). */ - getIssue: 'getIssue', - /** Fetch an issue plus available label names and recent comments in one call. */ - getIssueContext: 'getIssueContext', - /** List comments on a GitHub issue. Bodies are truncated by default (detail: summary). Prefer getIssueContext for the first page when triaging. */ - listIssueComments: 'listIssueComments', - /** Create a new issue in a GitHub repository. Requires approval by default. */ - createIssue: 'createIssue', - /** Add a comment to a GitHub issue. Requires approval by default. */ - addIssueComment: 'addIssueComment', - /** Update the body of a comment on a GitHub issue. Requires approval by default. */ - updateIssueComment: 'updateIssueComment', - /** Delete a comment from a GitHub issue permanently. Requires approval by default. */ - deleteIssueComment: 'deleteIssueComment', - /** Close an open GitHub issue. Requires approval by default. */ - closeIssue: 'closeIssue', - /** Update a GitHub issue — title, body, state, labels, milestone, or assignees. Requires approval by default. */ - updateIssue: 'updateIssue', - /** List labels available in a GitHub repository. */ - listLabels: 'listLabels', - /** Add labels to an issue or pull request. Requires approval by default. */ - addLabels: 'addLabels', - /** Remove a label from an issue or pull request. Requires approval by default. */ - removeLabel: 'removeLabel', - /** Create a label in a GitHub repository. Requires approval by default. */ - createLabel: 'createLabel', - /** Update a label in a GitHub repository — name, color, or description. Requires approval by default. */ - updateLabel: 'updateLabel', - /** Delete a label from a GitHub repository permanently. Requires approval by default. */ - deleteLabel: 'deleteLabel', - /** Assign users to an issue or pull request. Requires approval by default. */ - addAssignees: 'addAssignees', - /** Remove assignees from an issue or pull request. Requires approval by default. */ - removeAssignees: 'removeAssignees', - /** Search for code in GitHub repositories. Use qualifiers like "repo:owner/name" to scope the search. Results include matching text snippets when GitHub returns them. */ - searchCode: 'searchCode', - /** Search for GitHub repositories by keyword, topic, language, or other qualifiers. */ - searchRepositories: 'searchRepositories', - /** Search for issues and pull requests across GitHub using search qualifiers like "repo:owner/name is:open". */ - searchIssues: 'searchIssues', - /** List commits for a GitHub repository. Filter by file path to see commits that touched a file. For line-by-line attribution at a given ref, use getBlame instead. */ - listCommits: 'listCommits', - /** Get detailed information about a specific commit, including the list of files changed. Patches omitted by default. */ - getCommit: 'getCommit', - /** Line-level git blame for a file at a commit-like ref (branch, tag, or SHA). Returns contiguous ranges mapping lines to the commits that last modified them. */ - getBlame: 'getBlame', - /** Compare two branches, tags, or commits — ahead/behind counts, commits in between, and differing files. Patches omitted by default. */ - compareCommits: 'compareCommits', - /** List gists for the authenticated user or a specific user. */ - listGists: 'listGists', - /** Get a gist by ID, including file contents. */ - getGist: 'getGist', - /** List comments on a gist. */ - listGistComments: 'listGistComments', - /** Create a new gist with one or more files. Requires approval by default. */ - createGist: 'createGist', - /** Update an existing gist — edit description, update files, or remove files. Requires approval by default. */ - updateGist: 'updateGist', - /** Delete a gist permanently. Requires approval by default. */ - deleteGist: 'deleteGist', - /** Add a comment to a gist. Requires approval by default. */ - createGistComment: 'createGistComment', - /** List GitHub Actions workflows in a repository. */ - listWorkflows: 'listWorkflows', - /** List workflow runs for a repository, optionally filtered by workflow, branch, status, or event. */ - listWorkflowRuns: 'listWorkflowRuns', - /** Get details of a specific workflow run including status, timing, and trigger info. */ - getWorkflowRun: 'getWorkflowRun', - /** List jobs for a workflow run, including step-level status and timing. */ - listWorkflowJobs: 'listWorkflowJobs', - /** Get the log output of a workflow job to diagnose failures. Returns the tail (default 200 lines) with timestamps stripped. */ - getWorkflowJobLogs: 'getWorkflowJobLogs', - /** Trigger a workflow via workflow_dispatch event. Requires approval by default. */ - triggerWorkflow: 'triggerWorkflow', - /** Cancel an in-progress workflow run. Requires approval by default. */ - cancelWorkflowRun: 'cancelWorkflowRun', - /** Re-run a workflow run, optionally only the failed jobs. Requires approval by default. */ - rerunWorkflowRun: 'rerunWorkflowRun', - /** List check runs (Checks API — GitHub Actions and other CI providers) for a commit, branch, or tag. */ - listCheckRuns: 'listCheckRuns', - /** Get the combined commit status (Statuses API — legacy CI integrations) for a commit, branch, or tag. */ - getCombinedStatus: 'getCombinedStatus', - /** Diagnose CI failures for a ref — combined status, failing checks, and failed workflow jobs in one call. */ - getCiFailureContext: 'getCiFailureContext', - /** List discussions in a GitHub repository, most recently updated first, optionally filtered by category. */ - listDiscussions: 'listDiscussions', - /** Get a GitHub discussion by number. Body truncated by default (detail: summary). */ - getDiscussion: 'getDiscussion', - /** Add a comment to a GitHub discussion. Requires approval by default. */ - addDiscussionComment: 'addDiscussionComment', - /** List notification threads for the authenticated user. Requires a token with notifications access. */ - listNotifications: 'listNotifications', - /** Mark a single notification thread as read. Requires approval by default. */ - markNotificationRead: 'markNotificationRead', - /** List reactions on an issue or pull request conversation, with per-emoji counts. */ - listIssueReactions: 'listIssueReactions', - /** React to an issue or pull request with an emoji. Requires approval by default. */ - addIssueReaction: 'addIssueReaction', - /** List reactions on an issue or pull request comment, with per-emoji counts. */ - listCommentReactions: 'listCommentReactions', - /** React to an issue or pull request comment with an emoji. Requires approval by default. */ - addCommentReaction: 'addCommentReaction', - /** List releases for a GitHub repository, newest first (includes drafts and prereleases). */ - listReleases: 'listReleases', - /** Get the latest published release for a GitHub repository (excludes drafts and prereleases). Body truncated by default. */ - getLatestRelease: 'getLatestRelease', - /** Get a specific release by ID, including its assets. Body truncated by default. */ - getRelease: 'getRelease', - /** Fetch a release plus the previous release and tag comparison in one call. */ - getReleaseContext: 'getReleaseContext', - /** Create a new release (and its tag if needed) in a GitHub repository. Requires approval by default. */ - createRelease: 'createRelease', - /** Update an existing release — tag, target, title, notes, draft, or prerelease status. Requires approval by default. */ - updateRelease: 'updateRelease', - /** Delete a release permanently. Requires approval by default. */ - deleteRelease: 'deleteRelease', -} as const +import { ALL_GITHUB_TOOL_NAMES, type GithubToolName } from './catalog' -export type GithubToolName = typeof GITHUB_TOOL_NAMES[keyof typeof GITHUB_TOOL_NAMES] +export type { GithubToolName } from './catalog' +export { ALL_GITHUB_TOOL_NAMES } from './catalog' -export const ALL_GITHUB_TOOL_NAMES = Object.values(GITHUB_TOOL_NAMES) as GithubToolName[] +/** + * All GitHub tool names available via {@link createGithubTools}, keyed by name. + * Derived from `GITHUB_TOOL_CATALOG` — the single source of truth for tools. + */ +export const GITHUB_TOOL_NAMES = Object.fromEntries( + ALL_GITHUB_TOOL_NAMES.map(name => [name, name]), +) as { [K in GithubToolName]: K } diff --git a/packages/github-tools/src/core/write-tools.ts b/packages/github-tools/src/core/write-tools.ts index 822aead..ba3e3c7 100644 --- a/packages/github-tools/src/core/write-tools.ts +++ b/packages/github-tools/src/core/write-tools.ts @@ -1,94 +1,12 @@ -/** - * Write tools that mutate GitHub state. All require user approval by default - * unless overridden via {@link ApprovalConfig}. - */ -export const GITHUB_WRITE_TOOLS = { - /** Create a new branch in a GitHub repository from an existing branch or commit SHA. Requires approval by default. */ - createBranch: 'createBranch', - /** Delete a branch from a GitHub repository permanently. Requires approval by default. */ - deleteBranch: 'deleteBranch', - /** Fork a GitHub repository to the authenticated user account or a specified organization. Requires approval by default. */ - forkRepository: 'forkRepository', - /** Create a new GitHub repository for the authenticated user or a specified organization. Requires approval by default. */ - createRepository: 'createRepository', - /** Create or update a file in a GitHub repository. Requires approval by default. */ - createOrUpdateFile: 'createOrUpdateFile', - /** Create a new pull request in a GitHub repository. Requires approval by default. */ - createPullRequest: 'createPullRequest', - /** Merge a pull request. Requires approval by default. */ - mergePullRequest: 'mergePullRequest', - /** Update a pull request. Requires approval by default. */ - updatePullRequest: 'updatePullRequest', - /** Add a comment to a pull request. Requires approval by default. */ - addPullRequestComment: 'addPullRequestComment', - /** Update a pull request comment. Requires approval by default. */ - updatePullRequestComment: 'updatePullRequestComment', - /** Delete a pull request comment. Requires approval by default. */ - deletePullRequestComment: 'deletePullRequestComment', - /** Submit a pull request review with optional inline comments. Requires approval by default. */ - createPullRequestReview: 'createPullRequestReview', - /** Reply to a pull request review comment in its review thread. Requires approval by default. */ - replyToReviewComment: 'replyToReviewComment', - /** Mark a pull request review thread as resolved. Requires approval by default. */ - resolveReviewThread: 'resolveReviewThread', - /** Request reviews from users or teams on a pull request. Requires approval by default. */ - requestReviewers: 'requestReviewers', - /** Create a new issue in a GitHub repository. Requires approval by default. */ - createIssue: 'createIssue', - /** Add a comment to a GitHub issue. Requires approval by default. */ - addIssueComment: 'addIssueComment', - /** Update a GitHub issue comment. Requires approval by default. */ - updateIssueComment: 'updateIssueComment', - /** Delete a GitHub issue comment. Requires approval by default. */ - deleteIssueComment: 'deleteIssueComment', - /** Close an open GitHub issue. Requires approval by default. */ - closeIssue: 'closeIssue', - /** Update a GitHub issue. Requires approval by default. */ - updateIssue: 'updateIssue', - /** Add labels to an issue or pull request. Requires approval by default. */ - addLabels: 'addLabels', - /** Remove a label from an issue or pull request. Requires approval by default. */ - removeLabel: 'removeLabel', - /** Create a label in a GitHub repository. Requires approval by default. */ - createLabel: 'createLabel', - /** Update a label in a GitHub repository. Requires approval by default. */ - updateLabel: 'updateLabel', - /** Delete a label from a GitHub repository permanently. Requires approval by default. */ - deleteLabel: 'deleteLabel', - /** Assign users to an issue or pull request. Requires approval by default. */ - addAssignees: 'addAssignees', - /** Remove assignees from an issue or pull request. Requires approval by default. */ - removeAssignees: 'removeAssignees', - /** Create a new gist with one or more files. Requires approval by default. */ - createGist: 'createGist', - /** Update an existing gist. Requires approval by default. */ - updateGist: 'updateGist', - /** Delete a gist permanently. Requires approval by default. */ - deleteGist: 'deleteGist', - /** Add a comment to a gist. Requires approval by default. */ - createGistComment: 'createGistComment', - /** Add a comment to a GitHub discussion. Requires approval by default. */ - addDiscussionComment: 'addDiscussionComment', - /** Mark a single notification thread as read. Requires approval by default. */ - markNotificationRead: 'markNotificationRead', - /** React to an issue or pull request with an emoji. Requires approval by default. */ - addIssueReaction: 'addIssueReaction', - /** React to an issue or pull request comment with an emoji. Requires approval by default. */ - addCommentReaction: 'addCommentReaction', - /** Trigger a workflow via workflow_dispatch event. Requires approval by default. */ - triggerWorkflow: 'triggerWorkflow', - /** Cancel an in-progress workflow run. Requires approval by default. */ - cancelWorkflowRun: 'cancelWorkflowRun', - /** Re-run a workflow run, optionally only the failed jobs. Requires approval by default. */ - rerunWorkflowRun: 'rerunWorkflowRun', - /** Create a new release (and its tag if needed) in a GitHub repository. Requires approval by default. */ - createRelease: 'createRelease', - /** Update an existing release. Requires approval by default. */ - updateRelease: 'updateRelease', - /** Delete a release permanently. Requires approval by default. */ - deleteRelease: 'deleteRelease', -} as const +import { GITHUB_WRITE_TOOL_NAMES, type GithubWriteToolName } from './catalog' -export type GithubWriteToolName = typeof GITHUB_WRITE_TOOLS[keyof typeof GITHUB_WRITE_TOOLS] +export type { GithubWriteToolName } from './catalog' +export { GITHUB_WRITE_TOOL_NAMES, isGithubWriteToolName } from './catalog' -export const GITHUB_WRITE_TOOL_NAMES = Object.values(GITHUB_WRITE_TOOLS) as GithubWriteToolName[] +/** + * GitHub tools that perform write operations and require approval by default. + * Derived from the `write: true` entries of `GITHUB_TOOL_CATALOG`. + */ +export const GITHUB_WRITE_TOOLS = Object.fromEntries( + GITHUB_WRITE_TOOL_NAMES.map(name => [name, name]), +) as { [K in GithubWriteToolName]: K } diff --git a/packages/github-tools/src/eve/build.test.ts b/packages/github-tools/src/eve/build.test.ts index 7352c18..624d033 100644 --- a/packages/github-tools/src/eve/build.test.ts +++ b/packages/github-tools/src/eve/build.test.ts @@ -7,9 +7,8 @@ import { createToolRegistry } from './registry' import { getEveTools } from './load-eve' describe('createGithubTools eve integration', () => { - // TypeScript catches typos in registry names but not omissions: a tool added - // to GITHUB_TOOL_NAMES and the AI SDK layer but forgotten here would silently - // never appear in eve. This pins the two catalogs together. + // The registry is derived from GITHUB_TOOL_CATALOG, so parity holds by + // construction — this guards the derivation itself against regressions. it('registers every GITHUB_TOOL_NAMES entry exactly once in the eve registry', () => { const registryNames = createToolRegistry({ token: 'ghp_test' }).map(entry => entry.name) expect(registryNames.sort()).toEqual([...ALL_GITHUB_TOOL_NAMES].sort()) diff --git a/packages/github-tools/src/eve/registry.ts b/packages/github-tools/src/eve/registry.ts index de30f13..51ef801 100644 --- a/packages/github-tools/src/eve/registry.ts +++ b/packages/github-tools/src/eve/registry.ts @@ -1,14 +1,8 @@ import type { ToolModelOutput } from 'eve/tools' import type { z } from 'zod' import type { CommitIdentity } from '../types' -import * as bundles from '../core/bundles' -import * as checks from '../core/checks' -import * as commits from '../core/commits' +import { GITHUB_TOOL_CATALOG, isGithubWriteToolName } from '../core/catalog' import { mergeContextArgs, softenContextSchema, type GithubToolsContext } from '../core/context' -import * as discussions from '../core/discussions' -import * as gists from '../core/gists' -import * as issues from '../core/issues' -import * as notifications from '../core/notifications' import { compareCommitsToModelOutput, getCommitToModelOutput, @@ -16,16 +10,10 @@ import { getPullRequestContextToModelOutput, listPullRequestFilesToModelOutput, } from '../core/model-output' -import * as pullRequests from '../core/pull-requests' -import * as reactions from '../core/reactions' -import * as releases from '../core/releases' -import * as repository from '../core/repository' -import * as search from '../core/search' -import * as workflows from '../core/workflows' import { stripRateLimit } from '../core/rate-limit' import { resolveGithubToken, type GithubTokenInput } from '../core/token' import type { GithubWriteToolName } from '../core/write-tools' -import type { GithubToolName } from '../core/tool-names' +import { ALL_GITHUB_TOOL_NAMES, type GithubToolName } from '../core/tool-names' export type { GithubToolName } from '../core/tool-names' export { ALL_GITHUB_TOOL_NAMES } from '../core/tool-names' @@ -73,6 +61,8 @@ const GITHUB_EVE_TOOL_MODEL_OUTPUT = { compareCommits: modelOutputAdapter(compareCommitsToModelOutput), } satisfies Partial ToolModelOutput>> +const EVE_TOOL_MODEL_OUTPUTS: Partial ToolModelOutput>> = GITHUB_EVE_TOOL_MODEL_OUTPUT + /** Whether a GitHub tool has a built-in eve `toModelOutput` projection. */ export function hasGithubEveToolModelOutput(name: GithubToolName): boolean { return Object.hasOwn(GITHUB_EVE_TOOL_MODEL_OUTPUT, name) @@ -110,563 +100,27 @@ function isErrorPayload(output: unknown): output is { error: string } { } export function createToolRegistry(ctx: ToolBuildContext): ToolRegistryEntry[] { - const entries: ToolRegistryEntry[] = [ - { - name: 'getRepository', - description: repository.getRepositoryDescription, - inputSchema: repository.getRepositoryInputSchema, - execute: withToken(repository.getRepositoryCore, ctx), - }, - { - name: 'listBranches', - description: repository.listBranchesDescription, - inputSchema: repository.listBranchesInputSchema, - execute: withToken(repository.listBranchesCore, ctx), - }, - { - name: 'getFileContent', - description: repository.getFileContentDescription, - inputSchema: repository.getFileContentInputSchema, - execute: withToken(repository.getFileContentCore, ctx), - toModelOutput: GITHUB_EVE_TOOL_MODEL_OUTPUT.getFileContent, - }, - { - name: 'getRepositoryTree', - description: repository.getRepositoryTreeDescription, - inputSchema: repository.getRepositoryTreeInputSchema, - execute: withToken(repository.getRepositoryTreeCore, ctx), - }, - { - name: 'createBranch', - writeTool: 'createBranch', - description: repository.createBranchDescription, - inputSchema: repository.createBranchInputSchema, - execute: withToken(repository.createBranchCore, ctx), - }, - { - name: 'deleteBranch', - writeTool: 'deleteBranch', - description: repository.deleteBranchDescription, - inputSchema: repository.deleteBranchInputSchema, - execute: withToken(repository.deleteBranchCore, ctx), - }, - { - name: 'forkRepository', - writeTool: 'forkRepository', - description: repository.forkRepositoryDescription, - inputSchema: repository.forkRepositoryInputSchema, - execute: withToken(repository.forkRepositoryCore, ctx), - }, - { - name: 'createRepository', - writeTool: 'createRepository', - description: repository.createRepositoryDescription, - inputSchema: repository.createRepositoryInputSchema, - execute: withToken(repository.createRepositoryCore, ctx), - }, - { - name: 'createOrUpdateFile', - writeTool: 'createOrUpdateFile', - description: repository.createOrUpdateFileDescription, - inputSchema: repository.createOrUpdateFileInputSchema, - execute: withToken(repository.createOrUpdateFileCore, ctx, { - author: ctx.author, - committer: ctx.committer, - coAuthors: ctx.coAuthors, - }), - }, - { - name: 'listPullRequests', - description: pullRequests.listPullRequestsDescription, - inputSchema: pullRequests.listPullRequestsInputSchema, - execute: withToken(pullRequests.listPullRequestsCore, ctx), - }, - { - name: 'getPullRequest', - description: pullRequests.getPullRequestDescription, - inputSchema: pullRequests.getPullRequestInputSchema, - execute: withToken(pullRequests.getPullRequestCore, ctx), - }, - { - name: 'createPullRequest', - writeTool: 'createPullRequest', - description: pullRequests.createPullRequestDescription, - inputSchema: pullRequests.createPullRequestInputSchema, - execute: withToken(pullRequests.createPullRequestCore, ctx), - }, - { - name: 'mergePullRequest', - writeTool: 'mergePullRequest', - description: pullRequests.mergePullRequestDescription, - inputSchema: pullRequests.mergePullRequestInputSchema, - execute: withToken(pullRequests.mergePullRequestCore, ctx, { coAuthors: ctx.coAuthors }), - }, - { - name: 'updatePullRequest', - writeTool: 'updatePullRequest', - description: pullRequests.updatePullRequestDescription, - inputSchema: pullRequests.updatePullRequestInputSchema, - execute: withToken(pullRequests.updatePullRequestCore, ctx), - }, - { - name: 'addPullRequestComment', - writeTool: 'addPullRequestComment', - description: pullRequests.addPullRequestCommentDescription, - inputSchema: pullRequests.addPullRequestCommentInputSchema, - execute: withToken(pullRequests.addPullRequestCommentCore, ctx), - }, - { - name: 'updatePullRequestComment', - writeTool: 'updatePullRequestComment', - description: pullRequests.updatePullRequestCommentDescription, - inputSchema: pullRequests.updatePullRequestCommentInputSchema, - execute: withToken(pullRequests.updatePullRequestCommentCore, ctx), - }, - { - name: 'deletePullRequestComment', - writeTool: 'deletePullRequestComment', - description: pullRequests.deletePullRequestCommentDescription, - inputSchema: pullRequests.deletePullRequestCommentInputSchema, - execute: withToken(pullRequests.deletePullRequestCommentCore, ctx), - }, - { - name: 'listPullRequestFiles', - description: pullRequests.listPullRequestFilesDescription, - inputSchema: pullRequests.listPullRequestFilesInputSchema, - execute: withToken(pullRequests.listPullRequestFilesCore, ctx), - toModelOutput: GITHUB_EVE_TOOL_MODEL_OUTPUT.listPullRequestFiles, - }, - { - name: 'listPullRequestReviews', - description: pullRequests.listPullRequestReviewsDescription, - inputSchema: pullRequests.listPullRequestReviewsInputSchema, - execute: withToken(pullRequests.listPullRequestReviewsCore, ctx), - }, - { - name: 'createPullRequestReview', - writeTool: 'createPullRequestReview', - description: pullRequests.createPullRequestReviewDescription, - inputSchema: pullRequests.createPullRequestReviewInputSchema, - execute: withToken(pullRequests.createPullRequestReviewCore, ctx), - }, - { - name: 'listPullRequestReviewThreads', - description: pullRequests.listPullRequestReviewThreadsDescription, - inputSchema: pullRequests.listPullRequestReviewThreadsInputSchema, - execute: withToken(pullRequests.listPullRequestReviewThreadsCore, ctx), - }, - { - name: 'replyToReviewComment', - writeTool: 'replyToReviewComment', - description: pullRequests.replyToReviewCommentDescription, - inputSchema: pullRequests.replyToReviewCommentInputSchema, - execute: withToken(pullRequests.replyToReviewCommentCore, ctx), - }, - { - name: 'resolveReviewThread', - writeTool: 'resolveReviewThread', - description: pullRequests.resolveReviewThreadDescription, - inputSchema: pullRequests.resolveReviewThreadInputSchema, - execute: withToken(pullRequests.resolveReviewThreadCore, ctx), - }, - { - name: 'requestReviewers', - writeTool: 'requestReviewers', - description: pullRequests.requestReviewersDescription, - inputSchema: pullRequests.requestReviewersInputSchema, - execute: withToken(pullRequests.requestReviewersCore, ctx), - }, - { - name: 'getPullRequestContext', - description: bundles.getPullRequestContextDescription, - inputSchema: bundles.getPullRequestContextInputSchema, - execute: withToken(bundles.getPullRequestContextCore, ctx), - toModelOutput: GITHUB_EVE_TOOL_MODEL_OUTPUT.getPullRequestContext, - }, - { - name: 'getIssueContext', - description: bundles.getIssueContextDescription, - inputSchema: bundles.getIssueContextInputSchema, - execute: withToken(bundles.getIssueContextCore, ctx), - }, - { - name: 'listIssues', - description: issues.listIssuesDescription, - inputSchema: issues.listIssuesInputSchema, - execute: withToken(issues.listIssuesCore, ctx), - }, - { - name: 'getIssue', - description: issues.getIssueDescription, - inputSchema: issues.getIssueInputSchema, - execute: withToken(issues.getIssueCore, ctx), - }, - { - name: 'listIssueComments', - description: issues.listIssueCommentsDescription, - inputSchema: issues.listIssueCommentsInputSchema, - execute: withToken(issues.listIssueCommentsCore, ctx), - }, - { - name: 'createIssue', - writeTool: 'createIssue', - description: issues.createIssueDescription, - inputSchema: issues.createIssueInputSchema, - execute: withToken(issues.createIssueCore, ctx), - }, - { - name: 'addIssueComment', - writeTool: 'addIssueComment', - description: issues.addIssueCommentDescription, - inputSchema: issues.addIssueCommentInputSchema, - execute: withToken(issues.addIssueCommentCore, ctx), - }, - { - name: 'updateIssueComment', - writeTool: 'updateIssueComment', - description: issues.updateIssueCommentDescription, - inputSchema: issues.updateIssueCommentInputSchema, - execute: withToken(issues.updateIssueCommentCore, ctx), - }, - { - name: 'deleteIssueComment', - writeTool: 'deleteIssueComment', - description: issues.deleteIssueCommentDescription, - inputSchema: issues.deleteIssueCommentInputSchema, - execute: withToken(issues.deleteIssueCommentCore, ctx), - }, - { - name: 'closeIssue', - writeTool: 'closeIssue', - description: issues.closeIssueDescription, - inputSchema: issues.closeIssueInputSchema, - execute: withToken(issues.closeIssueCore, ctx), - }, - { - name: 'updateIssue', - writeTool: 'updateIssue', - description: issues.updateIssueDescription, - inputSchema: issues.updateIssueInputSchema, - execute: withToken(issues.updateIssueCore, ctx), - }, - { - name: 'listLabels', - description: issues.listLabelsDescription, - inputSchema: issues.listLabelsInputSchema, - execute: withToken(issues.listLabelsCore, ctx), - }, - { - name: 'addLabels', - writeTool: 'addLabels', - description: issues.addLabelsDescription, - inputSchema: issues.addLabelsInputSchema, - execute: withToken(issues.addLabelsCore, ctx), - }, - { - name: 'removeLabel', - writeTool: 'removeLabel', - description: issues.removeLabelDescription, - inputSchema: issues.removeLabelInputSchema, - execute: withToken(issues.removeLabelCore, ctx), - }, - { - name: 'createLabel', - writeTool: 'createLabel', - description: issues.createLabelDescription, - inputSchema: issues.createLabelInputSchema, - execute: withToken(issues.createLabelCore, ctx), - }, - { - name: 'updateLabel', - writeTool: 'updateLabel', - description: issues.updateLabelDescription, - inputSchema: issues.updateLabelInputSchema, - execute: withToken(issues.updateLabelCore, ctx), - }, - { - name: 'deleteLabel', - writeTool: 'deleteLabel', - description: issues.deleteLabelDescription, - inputSchema: issues.deleteLabelInputSchema, - execute: withToken(issues.deleteLabelCore, ctx), - }, - { - name: 'addAssignees', - writeTool: 'addAssignees', - description: issues.addAssigneesDescription, - inputSchema: issues.addAssigneesInputSchema, - execute: withToken(issues.addAssigneesCore, ctx), - }, - { - name: 'removeAssignees', - writeTool: 'removeAssignees', - description: issues.removeAssigneesDescription, - inputSchema: issues.removeAssigneesInputSchema, - execute: withToken(issues.removeAssigneesCore, ctx), - }, - { - name: 'listIssueReactions', - description: reactions.listIssueReactionsDescription, - inputSchema: reactions.listIssueReactionsInputSchema, - execute: withToken(reactions.listIssueReactionsCore, ctx), - }, - { - name: 'addIssueReaction', - writeTool: 'addIssueReaction', - description: reactions.addIssueReactionDescription, - inputSchema: reactions.addIssueReactionInputSchema, - execute: withToken(reactions.addIssueReactionCore, ctx), - }, - { - name: 'listCommentReactions', - description: reactions.listCommentReactionsDescription, - inputSchema: reactions.listCommentReactionsInputSchema, - execute: withToken(reactions.listCommentReactionsCore, ctx), - }, - { - name: 'addCommentReaction', - writeTool: 'addCommentReaction', - description: reactions.addCommentReactionDescription, - inputSchema: reactions.addCommentReactionInputSchema, - execute: withToken(reactions.addCommentReactionCore, ctx), - }, - { - name: 'listDiscussions', - description: discussions.listDiscussionsDescription, - inputSchema: discussions.listDiscussionsInputSchema, - execute: withToken(discussions.listDiscussionsCore, ctx), - }, - { - name: 'getDiscussion', - description: discussions.getDiscussionDescription, - inputSchema: discussions.getDiscussionInputSchema, - execute: withToken(discussions.getDiscussionCore, ctx), - }, - { - name: 'addDiscussionComment', - writeTool: 'addDiscussionComment', - description: discussions.addDiscussionCommentDescription, - inputSchema: discussions.addDiscussionCommentInputSchema, - execute: withToken(discussions.addDiscussionCommentCore, ctx), - }, - { - name: 'listNotifications', - description: notifications.listNotificationsDescription, - inputSchema: notifications.listNotificationsInputSchema, - execute: withToken(notifications.listNotificationsCore, ctx), - }, - { - name: 'markNotificationRead', - writeTool: 'markNotificationRead', - description: notifications.markNotificationReadDescription, - inputSchema: notifications.markNotificationReadInputSchema, - execute: withToken(notifications.markNotificationReadCore, ctx), - }, - { - name: 'searchCode', - description: search.searchCodeDescription, - inputSchema: search.searchCodeInputSchema, - execute: withToken(search.searchCodeCore, ctx), - }, - { - name: 'searchRepositories', - description: search.searchRepositoriesDescription, - inputSchema: search.searchRepositoriesInputSchema, - execute: withToken(search.searchRepositoriesCore, ctx), - }, - { - name: 'searchIssues', - description: search.searchIssuesDescription, - inputSchema: search.searchIssuesInputSchema, - execute: withToken(search.searchIssuesCore, ctx), - }, - { - name: 'listCommits', - description: commits.listCommitsDescription, - inputSchema: commits.listCommitsInputSchema, - execute: withToken(commits.listCommitsCore, ctx), - }, - { - name: 'getCommit', - description: commits.getCommitDescription, - inputSchema: commits.getCommitInputSchema, - execute: withToken(commits.getCommitCore, ctx), - toModelOutput: GITHUB_EVE_TOOL_MODEL_OUTPUT.getCommit, - }, - { - name: 'getBlame', - description: commits.getBlameDescription, - inputSchema: commits.getBlameInputSchema, - execute: withToken(commits.getBlameCore, ctx), - }, - { - name: 'compareCommits', - description: commits.compareCommitsDescription, - inputSchema: commits.compareCommitsInputSchema, - execute: withToken(commits.compareCommitsCore, ctx), - toModelOutput: GITHUB_EVE_TOOL_MODEL_OUTPUT.compareCommits, - }, - { - name: 'listGists', - description: gists.listGistsDescription, - inputSchema: gists.listGistsInputSchema, - execute: withToken(gists.listGistsCore, ctx), - }, - { - name: 'getGist', - description: gists.getGistDescription, - inputSchema: gists.getGistInputSchema, - execute: withToken(gists.getGistCore, ctx), - }, - { - name: 'listGistComments', - description: gists.listGistCommentsDescription, - inputSchema: gists.listGistCommentsInputSchema, - execute: withToken(gists.listGistCommentsCore, ctx), - }, - { - name: 'createGist', - writeTool: 'createGist', - description: gists.createGistDescription, - inputSchema: gists.createGistInputSchema, - execute: withToken(gists.createGistCore, ctx), - }, - { - name: 'updateGist', - writeTool: 'updateGist', - description: gists.updateGistDescription, - inputSchema: gists.updateGistInputSchema, - execute: withToken(gists.updateGistCore, ctx), - }, - { - name: 'deleteGist', - writeTool: 'deleteGist', - description: gists.deleteGistDescription, - inputSchema: gists.deleteGistInputSchema, - execute: withToken(gists.deleteGistCore, ctx), - }, - { - name: 'createGistComment', - writeTool: 'createGistComment', - description: gists.createGistCommentDescription, - inputSchema: gists.createGistCommentInputSchema, - execute: withToken(gists.createGistCommentCore, ctx), - }, - { - name: 'listWorkflows', - description: workflows.listWorkflowsDescription, - inputSchema: workflows.listWorkflowsInputSchema, - execute: withToken(workflows.listWorkflowsCore, ctx), - }, - { - name: 'listWorkflowRuns', - description: workflows.listWorkflowRunsDescription, - inputSchema: workflows.listWorkflowRunsInputSchema, - execute: withToken(workflows.listWorkflowRunsCore, ctx), - }, - { - name: 'getWorkflowRun', - description: workflows.getWorkflowRunDescription, - inputSchema: workflows.getWorkflowRunInputSchema, - execute: withToken(workflows.getWorkflowRunCore, ctx), - }, - { - name: 'listWorkflowJobs', - description: workflows.listWorkflowJobsDescription, - inputSchema: workflows.listWorkflowJobsInputSchema, - execute: withToken(workflows.listWorkflowJobsCore, ctx), - }, - { - name: 'getWorkflowJobLogs', - description: workflows.getWorkflowJobLogsDescription, - inputSchema: workflows.getWorkflowJobLogsInputSchema, - execute: withToken(workflows.getWorkflowJobLogsCore, ctx), - }, - { - name: 'triggerWorkflow', - writeTool: 'triggerWorkflow', - description: workflows.triggerWorkflowDescription, - inputSchema: workflows.triggerWorkflowInputSchema, - execute: withToken(workflows.triggerWorkflowCore, ctx), - }, - { - name: 'cancelWorkflowRun', - writeTool: 'cancelWorkflowRun', - description: workflows.cancelWorkflowRunDescription, - inputSchema: workflows.cancelWorkflowRunInputSchema, - execute: withToken(workflows.cancelWorkflowRunCore, ctx), - }, - { - name: 'rerunWorkflowRun', - writeTool: 'rerunWorkflowRun', - description: workflows.rerunWorkflowRunDescription, - inputSchema: workflows.rerunWorkflowRunInputSchema, - execute: withToken(workflows.rerunWorkflowRunCore, ctx), - }, - { - name: 'listCheckRuns', - description: checks.listCheckRunsDescription, - inputSchema: checks.listCheckRunsInputSchema, - execute: withToken(checks.listCheckRunsCore, ctx), - }, - { - name: 'getCombinedStatus', - description: checks.getCombinedStatusDescription, - inputSchema: checks.getCombinedStatusInputSchema, - execute: withToken(checks.getCombinedStatusCore, ctx), - }, - { - name: 'getCiFailureContext', - description: bundles.getCiFailureContextDescription, - inputSchema: bundles.getCiFailureContextInputSchema, - execute: withToken(bundles.getCiFailureContextCore, ctx), - }, - { - name: 'listReleases', - description: releases.listReleasesDescription, - inputSchema: releases.listReleasesInputSchema, - execute: withToken(releases.listReleasesCore, ctx), - }, - { - name: 'getLatestRelease', - description: releases.getLatestReleaseDescription, - inputSchema: releases.getLatestReleaseInputSchema, - execute: withToken(releases.getLatestReleaseCore, ctx), - }, - { - name: 'getRelease', - description: releases.getReleaseDescription, - inputSchema: releases.getReleaseInputSchema, - execute: withToken(releases.getReleaseCore, ctx), - }, - { - name: 'getReleaseContext', - description: bundles.getReleaseContextDescription, - inputSchema: bundles.getReleaseContextInputSchema, - execute: withToken(bundles.getReleaseContextCore, ctx), - }, - { - name: 'createRelease', - writeTool: 'createRelease', - description: releases.createReleaseDescription, - inputSchema: releases.createReleaseInputSchema, - execute: withToken(releases.createReleaseCore, ctx), - }, - { - name: 'updateRelease', - writeTool: 'updateRelease', - description: releases.updateReleaseDescription, - inputSchema: releases.updateReleaseInputSchema, - execute: withToken(releases.updateReleaseCore, ctx), - }, - { - name: 'deleteRelease', - writeTool: 'deleteRelease', - description: releases.deleteReleaseDescription, - inputSchema: releases.deleteReleaseInputSchema, - execute: withToken(releases.deleteReleaseCore, ctx), - }, - ] + // Commit-identity options are session-level, not model inputs, so they ride + // alongside the token instead of living in the tool's input schema. + const commitExtras: Partial>> = { + createOrUpdateFile: { author: ctx.author, committer: ctx.committer, coAuthors: ctx.coAuthors }, + mergePullRequest: { coAuthors: ctx.coAuthors }, + } + + const entries = ALL_GITHUB_TOOL_NAMES.map((name): ToolRegistryEntry => { + const descriptor = GITHUB_TOOL_CATALOG[name] + // Argument types vary per tool; `withToken` re-narrows at the dispatch boundary. + const core = descriptor.core as (args: Record & { token: string }) => Promise + const toModelOutput = EVE_TOOL_MODEL_OUTPUTS[name] + return { + name, + ...(isGithubWriteToolName(name) && { writeTool: name }), + description: descriptor.description, + inputSchema: descriptor.inputSchema, + execute: withToken(core, ctx, commitExtras[name]), + ...(toModelOutput && { toModelOutput }), + } + }) if (!ctx.context) return entries diff --git a/packages/github-tools/src/index.test.ts b/packages/github-tools/src/index.test.ts new file mode 100644 index 0000000..a08763a --- /dev/null +++ b/packages/github-tools/src/index.test.ts @@ -0,0 +1,13 @@ +import { describe, expect, it } from 'vitest' +import * as sdk from './index' +import { ALL_GITHUB_TOOL_NAMES } from './core/tool-names' + +describe('index exports', () => { + // `allTools` completeness is enforced at compile time (`satisfies AllGithubTools`); + // the re-export lines at the bottom of index.ts are not, so guard them here. + it('re-exports a factory for every catalog tool', () => { + for (const name of ALL_GITHUB_TOOL_NAMES) { + expect(typeof (sdk as Record)[name], `missing factory export: ${name}`).toBe('function') + } + }) +})