ID@agntn/forgesv0.5.1
One API. Every forge.
GitHub, GitLab, Gitea and GitBucket behind one TypeScript provider. Repositories, issues, pull requests, review threads, commits and CI come back in the same shape, and the token is found for you. Library, MCP server or Pi extension, your pick.
- Platforms
- 5
- 4 provider classes
- Resources
- 10
- same methods on each
- Agent tools
- 55
- 12 of them write
$ pnpm add @agntn/forgesnitrojs/nitro
Next Generation Server Toolkit. Create web servers with everything you need and deploy them wherever you prefer.
Branches
- id
- "452269390"
- defaultBranch
- main
- private
- false
- isFork
- false
- cloneUrl
- github.com/nitrojs/nitro.git
CI
Token found for you
createProvider("github") reads the token from env, then asks gh, then opens its config file. GitLab asks glab, Gitea reads the tea config. You write the platform name and nothing else. The panel walks the same 3 repositories as everything below it.
- An explicit token wins, and an empty string is a real choice: read anonymously
- Nothing found throws right away, naming the env vars it tried
- The CLI goes through execFileSync with an argument array. No shell in between
GitHub
No token argument, no problem. The provider walks four places and signs every request with whatever it finds first.
resolveToken
Explicit{ token } in the config
EnvGH_TOKEN, GITHUB_TOKEN
CLIgh auth token
Config~/.config/gh/hosts.yml
- header
- Authorization: token
- anonymous
- 60 requests an hour per address
Merge requests are pull requests here
Every platform paginates its own way and names things its own way, and none of it is your problem. A page is { items, nextCursor }, a state is open or closed, and a pull request knows its branches, its head SHA and whether it's still a draft.
- GitLab iid becomes number, and GitHub issues that are really PRs are filtered out
- list, search, get, create and listComments with the same options everywhere
- PageResult with items and an opaque nextCursor, whether the platform sent Link or x-next-page
History and pipelines, normalized
A commit is a SHA, a message, two identities and its parents. A CI run is a branch, a revision, a lifecycle status and a conclusion that stays null until there is one. Counts a platform withholds come back as null too. Never as zero, zero would be a lie.
- commits.list filters by ref, path, since and until; get adds changed files without patches
- ciRuns.list turns Actions runs, GitLab pipelines and Gitea Actions into one status and one conclusion
- pullRequests.listChecks reads check runs, GitLab pipelines or commit statuses for the head SHA
ciRuns.list
- renovate/all-minor-patch @ d3f1a73completedsuccess
- renovate/all-minor-patch @ d3f1a73completedsuccess
- v3.0.260903-beta @ 0509fb9completedsuccess
Reply, resolve, unresolve
A review thread is a path, a line and its comments, with a state you can flip. The id list gives you is the id reply and resolve take back, whatever the platform calls it underneath.
- GitHub threads go through GraphQL, so isResolved and isOutdated are real, not guessed
- GitLab discussions and Gitea review comments land on the same Thread
- GitBucket has no thread endpoint and says so in a sentence, not a bare 404
src/runtime/internal/openapi.ts:57resolved
- _🗄️ Data Integrity & Integration_ | _🟠 Major_ | _🏗️ Heavy lift_ **Preserve recursive local references.** A self-referen…
- Fixed in ec5fa8b4. Cyclic local references are preserved, the `$defs` or `definitions` container remains in the normali…
src/runtime/internal/routes/openapi.tsresolvedoutdated
- _🗄️ Data Integrity & Integration_ | _🟠 Major_ | _⚡ Quick win_ **Keep local definitions reachable from parameter schemas…
- Fixed in c6f476f0. Parameter schemas now inherit the parent `$defs` or `definitions` only when they contain a local ref…
Four providers, five platforms
GitHub wants Authorization: token, GitLab wants Private-Token, Gitea wants limit instead of per_page. Each provider keeps that to itself and maps its raw responses onto the shared types. GitBucket speaks the GitHub API, so it's the GitHub provider with a baseURL. Forgejo and Codeberg are Gitea the same way. Cloudflare Artifacts gets a class of its own: plain Git storage with a Bearer token and no forge on top. And your own forge is one class away.
Importimport { GitLabProvider } from "@agntn/forges/gitlab"
55 tools, three hosts
forges mcp serves the tools over stdio, the Pi and OMP extensions render them in the terminal. All three call the same executors, so they answer identically and a fix lands once. 12 tools write, and they say so in their annotations, so a client can gate them before a model gets creative.
- Reads fall back to anonymous access, writes and forges_users_authenticated need a credential
- Lists drop bodies and name the tool that reads one in full, so a busy page still fits in a context
- A self hosted FORGES_*_BASE_URL comes from the process environment, never from a tool argument
forges_repos_get
A model asks for a repository by name and gets back the same JSON a script would, from whichever forge it named.
- platform
- "github"
- repo
- "nitrojs/nitro"
- result.id
- "452269390"
- bytes
- 512 characters of JSON
Same calls, every provider
Provider is the abstract base with the ten resource accessors. Concrete classes implement the typed mappers and the platform calls, nothing else leaks upward. Sub path imports give you one provider without dragging in the other two.
- repos, contributionTemplates, code, ciRuns, commits, releases, issues, pullRequests, users, threads
- NotFoundError, AuthenticationError, PermissionError, RateLimitError with retryAfter
- Stable reads cached with an LRU keyed by host and token hash, item reads always fresh
Same calls
import { createProvider } from "@agntn/forges";// GH_TOKEN, then gh auth tokenconst forge = await createProvider("github");const repo = await forge.repos.get("nitrojs", "nitro");const { items } = await forge.pullRequests.list("nitrojs", "nitro", { state: "open" });repo.defaultBranch; // "main"items[0]?.sourceBranch; // "feat/openapi-schema-inference"Start with one command
One install gives you the library, the forges CLI with its MCP server, and the sub path imports. If gh is logged in, the first call below already works.
- PinPre-1.0, so pin exact versions.
- DataIssue bodies, comments and threads are text you didn't write.
- TokensFrom env or the CLI login. Never a tool argument.
First call
$ pnpm add @agntn/forgesimport { createProvider } from "@agntn/forges";const forge = await createProvider("github");const repo = await forge.repos.get("nitrojs", "nitro");repo.id; // "452269390"repo.defaultBranch; // "main"