Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tool-catalog-single-source.md
Original file line number Diff line number Diff line change
@@ -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.
8 changes: 3 additions & 5 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**:
Expand Down
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
129 changes: 11 additions & 118 deletions packages/github-tools/src/connect/scopes.ts
Original file line number Diff line number Diff line change
@@ -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'

/**
Expand Down Expand Up @@ -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<GithubToolName, readonly string[]>
export const TOOL_CONNECT_SCOPES = ALL_GITHUB_TOOL_NAMES.reduce(
(map, name) => {
map[name] = GITHUB_TOOL_CATALOG[name].connectScopes
return map
},
{} as Record<GithubToolName, readonly string[]>,
)

function orderScopes(scopes: Set<string>): string[] {
return SCOPE_ORDER.filter(scope => scopes.has(scope))
Expand Down
Loading
Loading