@agntn/forges

Unified Git Provider - single TypeScript API for GitHub, GitLab, Gitea, GitBucket, and Cloudflare Artifacts

Packages

Package details

extension

Install @agntn/forges from npm and Pi will load the resources declared by the package manifest.

$ pi install npm:@agntn/forges
Package
@agntn/forges
Version
0.6.0
Published
Oct 26, 2026
Downloads
1,212/mo · 372/wk
Author
oritwoen
License
MIT
Types
extension
Size
657.1 KB
Dependencies
6 dependencies · 3 peers
Pi manifest JSON
{
  "image": "https://forges.agntn.dev/image.png",
  "extensions": [
    "./extensions/pi/forges.ts"
  ]
}

Security note

Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.

README

@agntn/forges

npm version npm downloads license Ask DeepWiki

⚒️ Four forges, ten resources, 59 agent tools. You ask for a pull request, you get a pull request.

Why?

Every Git host does the same job and none of them agree on the words. Pull request or merge request, per_page or limit, Link or x-next-page, and GitLab's URL number is the iid not the id. An agent with four clients will pick the wrong number. Talk to one Provider and let it remember which header is which.

Docs, and an explorer that runs the same calls: forges.agntn.dev.

✨ Features

  • 🧩 Four forges, one Provider. GitHub, GitLab, Gitea and GitBucket. Same repos.get, same Issue, same PullRequest.
  • 🔑 It finds the token. Explicit value, then env, then gh or glab, then the CLI config file. First hit wins.
  • 📦 Loads one platform. createProvider("github") is async. It imports GitHub and leaves GitLab on disk.
  • 🆔 IDs are strings. Even when the API sent a number. A count the forge withholds is missing, not 0.
  • 🫥 Empty string is guest. { token: "" } is anonymous on purpose. Leave token out and you get AuthenticationError, not a quiet guest session.
  • 🤖 59 tools, four surfaces. MCP, Pi, OMP and the CLI share one definition of each. Fourteen tools write to the host.
  • 🚫 Missing is 501. Code search on Gitea is not an empty page. You get a ForgesError with status 501.
  • 🧭 GitBucket is GitHub plus baseURL. Forgejo and Codeberg are Gitea plus baseURL. Same class, different host.
  • 🚚 The plumbing is forges. It sends the requests, picks the endpoints and walks the pages. This package keeps the shapes your agent already reads. One HTTP client fewer in the world.
  • ☁️ Cloudflare Artifacts too. Git without the forge. Repos, files and the log through the same calls, with the namespace as owner. Ask it for issues and you get a 501, because there's nobody there to file one.

📦 Install

pnpm add @agntn/forges

Node.js 26 or newer.

🚀 First call

import { createProvider } from "@agntn/forges";

const codeberg = await createProvider("gitea", {
  token: "",
  baseURL: "https://codeberg.org",
});

const repo = await codeberg.repos.get({ owner: "forgejo", repo: "forgejo" });
console.log(repo.fullName, repo.description, repo.defaultBranch);
forgejo/forgejo Beyond coding. We forge. forgejo

They said it, not me. No key. You still pass the host. Empty string is guest.

Logged into gh? Drop the config object.

const github = await createProvider("github");
const hello = await github.repos.get({ owner: "octocat", repo: "Hello-World" });
console.log(hello.fullName, hello.description, hello.defaultBranch);
octocat/Hello-World My first repository on GitHub! master

GitHub's first hello. Default branch is still master.

Commands

Every agent tool is a command too. Drop the forges_ prefix, swap underscores for dashes, and forges_repos_get becomes forges repos-get:

forges repos-get --repo octocat/Hello-World
forges issues-search-global --owner agntn --state open --labels '["bug"]'
forges mcp

Options are the tool's own arguments. A list or an object goes in as JSON, --json prints the details instead of the text. The text is exactly what a model reads, so your shell script and your agent finally get the same answer. forges --help lists all 55, forges <command> --help shows one. pnpm add -g @agntn/forges puts it on your PATH, pnpm exec forges works inside a project.

🧠 Library

import { createProvider } from "@agntn/forges";

const github = await createProvider("github");
const hello = { owner: "octocat", repo: "Hello-World" };
const repo = await github.repos.get(hello);

const open = await github.pullRequests.list(hello, { state: "open" });
const more = await github.pullRequests.list(hello, { state: "open", cursor: open.nextCursor });

const gitlab = await createProvider("gitlab", {
  token: "glpat-…",
  baseURL: "https://gitlab.example.com",
});
const gitbucket = await createProvider("github", {
  token: "…",
  baseURL: "https://gitbucket.example.com/api/v3",
});

Ten resources on every provider. repos, issues, pullRequests, threads. Then commits, ciRuns, releases, contributionTemplates, code, users. Lists come back as items plus nextCursor. Hand it back as cursor and you get the next page. No cursor, no more pages. It's opaque on purpose, so don't build one by hand: the provider refuses it. totalCount only when the forge counted. Search adds incomplete when the answer is known to be partial. Guides: Authentication, Repositories, Issues, Pull requests, Review threads, Commits, CI and releases, Templates, Code search.

Local Git

@agntn/forges/local checks a fetched checkout without changing it. Git with --no-lazy-fetch support must be on PATH; inspection also needs ls-files --deduplicate.

import { inspectLocal, verifyLocalMerge } from "@agntn/forges/local";

const inspection = await inspectLocal({
  cwd: "/path/to/checkout",
  paths: ["*AGENTS.md"],
  historyLimit: 3,
});

const evidence = await verifyLocalMerge({
  cwd: "/path/to/checkout",
  head: "topic",
  mergeCommit: "66c39f4bccd275e930420f408b7c311b9c494af8",
  target: "origin/main",
  paths: ["package.json", "README.md"],
});
console.log(evidence.mergeReachable, evidence.pathsMatch);

Status rows and tracked files are paged separately. Continue with statusOffset: inspection.nextStatusOffset or filesOffset: inspection.nextFilesOffset until it's null, keeping paths unchanged. Limits and the concurrent edit caveat sit in the JSDoc of inspectLocal and its options.

Replace the sample mergeCommit with the forge's actual merge or squash SHA for that PR, not the current target tip. The two booleans answer different questions: is that commit in the target's local history, and do the selected paths match the PR head? Neither authorizes deleting a branch.

🗺️ Providers

Platform Provider Auth header Threads Code search
GitHub github Authorization: token GraphQL, real flags global, owner, repository
GitLab gitlab Private-Token REST discussions token required, Premium for global
Gitea, Forgejo, Codeberg gitea + baseURL Authorization: token one thread per review comment none
GitBucket github + baseURL Authorization: token none none
Cloudflare Artifacts artifacts Authorization: Bearer none none

Code search on Gitea is a 501, not an empty page. Host pages: Platforms.

🤖 Agents

forges mcp
pi install npm:@agntn/forges
omp install @agntn/forges
{
  "mcpServers": {
    "forges": { "command": "npx", "args": ["-y", "@agntn/forges", "mcp"] }
  }
}

MCP, Pi and OMP all hit the same 59 tools. Fourteen write to the host, so ask forges_users_authenticated who you are before a model does, details in the Agents guide.

🚫 What this does not do

Hosted files, trees, branches, release assets, webhooks, org admin. The review loop is the scope: what was proposed, what was said, whether it passed, what shipped.

🧩 Adding a provider

A class extending Provider, the typed mappers, and a 501 for every method you skip. Copy from Custom providers.

🛠️ Development

pnpm install     # docs/ too, one workspace and one lockfile
pnpm test        # vp test in watch mode
pnpm test:run    # single run, as CI does
pnpm typecheck   # tsc, then build, then the extension graph
pnpm lint
pnpm run docs    # the site, it bundles src/
pnpm run build   # obuild

The run in pnpm run docs isn't decoration. Bare pnpm docs is pnpm 12's own command, and all you get back is Usage: pnpm docs <PACKAGE>. Versions live in the catalogs of pnpm-workspace.yaml, so one line bumps Nuxt for the site and unstorage for the library and the worker alike.

💛 Thanks

I wrote a lot of this with help from Claude for Open Source and Codex for Open Source. Grateful for both <3

📄 License

MIT