# Phorge > Phorge is Git infrastructure for coding agents. Use the JavaScript SDK or the HTTP API to create and work with repositories without a local checkout. Standard Git over HTTPS is also available. Canonical documentation: - Product and JavaScript SDK guide: https://phorge.net/docs - JavaScript SDK symbol reference: https://phorge.net/docs/symbols - Complete HTTP API reference: https://phorge.net/docs/http - OpenAPI 3.1 description of the HTTP API: https://phorge.net/openapi.json - Sitemap of every public page: https://phorge.net/sitemap.xml - Create or manage a namespace: https://platform.phorge.net/ - SDK repository: https://github.com/sf-tools/phorge - Service status: https://phorge.net/health Every page on https://phorge.net returns Markdown when requested with `Accept: text/markdown` and sets `Vary: Accept`. Unknown paths return HTTP 404 with a short Markdown body (or JSON under `/api/`) that links back to these references. ## Guidance for coding agents - Prefer the JavaScript SDK when running on Node.js. - Use `npm install phorge`. The package name is `phorge`. - Never print, transmit, or commit a namespace private signing key. Load it from a secret or protected PEM file. - Repository creation is idempotent. Repeating `createRepo` for a repository owned by the namespace returns the existing repository. - Use `store.repo("name")` when the repository already exists and creation is unnecessary. - Use `expectedHeadSha` for concurrent writes. On HTTP 412 `HEAD_MOVED`, fetch the new head, reconsider or rebuild the change, and retry deliberately. Never retry the old write blindly. - Prefer clone-free reads and writes. Clone only when a tool specifically requires a working tree. - Treat pagination cursors as opaque and pass `next_cursor` back unchanged. - Do not invent SDK methods, API routes, request fields, or response fields. The supported surface is listed below and in the linked documentation. ## JavaScript SDK The SDK requires Node.js and is installed from npm: ```sh npm install phorge ``` Create a namespace at https://platform.phorge.net/create and save the downloaded private-key PEM. Phorge stores the corresponding public key. The SDK signs short-lived JWTs locally; the private key does not leave the caller's process. ### Create a client and repository ```ts import { readFile } from "node:fs/promises"; import { GitStorage } from "phorge"; const privateKey = await readFile( "./personal-phorge-private-key.pem", "utf8", ); const store = new GitStorage({ name: "personal", // namespace key: privateKey, // ES256 or RS256 private-key PEM tokenTtlSeconds: 900, // optional; defaults to 900 }); const repo = await store.createRepo({ id: "workspace", ttlSeconds: 86_400, // optional; omit or use 0 for no expiry private: true, // optional; defaults to true }); ``` `actor` is optional request-audit metadata. When supplied on `GitStorage`, it appears as the JWT's `actor` claim. It is not a Git author or committer and is omitted from the token when unset. Use an existing repository without creating it: ```ts const repo = store.repo("workspace"); ``` List repositories: ```ts let cursor; do { const page = await store.listRepos({ cursor, limit: 100 }); for (const repository of page.items) { console.log(repository.name, repository.git_url); } cursor = page.has_more ? page.next_cursor : undefined; } while (cursor); ``` ### Create a commit from file operations ```ts const result = await repo .createCommit({ targetBranch: "main", commitMessage: "Update worker", expectedHeadSha: currentHeadSha, author: { name: "Agent", email: "agent@example.com", }, }) .addFileFromString("src/worker.ts", source) .addFile("assets/data.bin", bytes, { mode: "100644" }) .deletePath("src/old-worker.ts") .send(); console.log(result.commitSha, result.ref); ``` The author is also the Git committer by default. Set `committer: { name, email }` only when the committer must differ. Supported file modes are `100644`, `100755`, and `120000`. A commit builder can be sent only once and must contain at least one file operation. Create a commit from a unified diff: ```ts const result = await repo.createCommitFromDiff({ branch: "main", expectedHeadSha: currentHeadSha, message: "Apply generated patch", diff: unifiedDiff, author: { name: "Agent", email: "agent@example.com" }, }); ``` `expectedHeadSha` is required for unified-diff commits. It is optional for file-operation commits, but concurrent agents should provide it. ### Read without cloning ```ts const files = await repo.listFiles({ ref: "main", path: "src", recursive: true, metadata: true, limit: 100, }); const file = await repo.getFileStream("src/main.ts", { ref: "main", range: "bytes=0-1023", }); const commits = await repo.listCommits({ ref: "main", limit: 20 }); const commit = await repo.getCommit(commits.items[0].sha); const patch = await repo.getCommitDiff(commits.items[0].sha); const branchDiff = await repo.getBranchDiff("main", "agent-1"); const matches = await repo.grep("TODO", { ref: "main", context: 2 }); const attribution = await repo.blame("src/main.ts", { ref: "main" }); const archive = await repo.getArchiveStream({ ref: "main", path: "src" }); ``` `getFileStream` and `getArchiveStream` return Node.js readable streams. Reads accept a branch, tag, or commit SHA. File streams support byte ranges. ### Branches, merges, and tags ```ts const branches = await repo.listBranches({ limit: 100 }); await repo.createBranch({ name: "agent-1", startPoint: "main", expectedHeadSha: currentHeadSha, }); const preview = await repo.previewMerge({ source: "agent-1", target: "main", expectedHeadSha: currentMainHeadSha, mode: "fast-forward-preferred", }); await repo.merge({ source: "agent-1", target: "main", expectedHeadSha: currentMainHeadSha, mode: "fast-forward-preferred", message: "Merge agent work", }); const updatedBranches = await repo.listBranches({ limit: 100 }); const newMain = updatedBranches.items.find((branch) => branch.name === "main"); if (!newMain) throw new Error("main branch not found"); await repo.deleteBranch("agent-1", sourceHeadSha); await repo.createTag({ name: "v1.0.0", target: "main", expectedHeadSha: newMain.sha, message: "Release v1.0.0", }); await repo.deleteTag("v1.0.0", tagRefSha); ``` Merge modes are `fast-forward-only` and `fast-forward-preferred`. Previewing a merge does not move the target ref. ### Standard Git over HTTPS The clone helper defaults to a shallow, blob-filtered clone: ```ts await repo.clone("./workspace"); ``` Generate a short-lived read-only remote URL for the stock Git client: ```ts const remoteURL = await repo.getRemoteURL({ scope: "read", ttl: 600 }); ``` ```sh git clone '' workspace ``` The Phorge dashboard also provides **Generate clone link** beside each repository. It issues a read-only URL valid for 10 minutes based on the signed-in user's namespace membership. It never asks for or uploads the namespace private key. Generate a customer-signed write URL when push access is required: ```ts const writeURL = await repo.getRemoteURL({ scope: "write", ttl: 600, operations: ["no-force-push"], refPolicies: ["refs/heads/agent-*"], }); ``` Generated URLs contain credentials. Treat them as secrets and do not log, commit, or share them. `scope` is `read` or `write`. `ttl` and `ttlSeconds` are aliases. ### SDK errors ```ts import { PhorgeError } from "phorge"; try { await operation(); } catch (error) { if (error instanceof PhorgeError) { console.error(error.status, error.code, error.message, error.details); } throw error; } ``` `PhorgeError` exposes `status`, `code`, `message`, and optional structured `details`. ## HTTP API Use the HTTP API from other languages or when direct request control is useful. Base URL: ```text https://{namespace}.api.phorge.net ``` Git remote URL: ```text https://{namespace}.phorge.net/{repo}.git ``` Send JSON bodies with `Content-Type: application/json` except for the multipart file-operation commit endpoint. Authenticate with: ```http Authorization: Bearer ``` Repository names occupy one URL segment. Percent-encode grouped names: `team/storefront` becomes `team%2Fstorefront` under `/repos/`. Percent-encode file paths and ref names used in path segments. ### HTTP authentication Sign JWTs using an active ES256 or RS256 namespace key. The SDK handles this automatically. For direct HTTP clients, use this shape: ```json { "alg": "ES256", "kid": "SHA256:public-key-fingerprint", "typ": "JWT" } ``` ```json { "iss": "your-org", "actor": "agent-7", "repo": "workspace", "scopes": ["git:read", "repo:write"], "iat": 1786579200, "exp": 1786580100, "refPolicies": ["refs/heads/agent-*"] } ``` - `iss`: namespace. - `actor`: optional request-audit identifier. It is unrelated to Git author and committer identities. - `repo`: exact repository name, or `*` where documented. - `scopes`: allowed capabilities. - `iat` and `exp`: NumericDate values. - `kid`: optional SHA-256 public-key fingerprint. Including it selects the intended active key directly. - `refPolicies`: optional Git-style patterns restricting writable refs. - `ops`: optional operation restrictions. Scopes: - `git:read`: repository metadata and read endpoints. - `git:write`: write access through the Git HTTPS protocol. - `repo:write`: repository creation and HTTP writes. - `org:read` with `repo: "*"`: repository listing. Public repository reads may omit a token. Creation, listing, private reads, and writes require authentication. ### Repository endpoints - `POST /repos`: create a repository. Body: `{ "name": string, "ttl_seconds"?: number, "private"?: boolean }`. Omitted or zero `ttl_seconds` means no scheduled deletion; a positive value schedules deletion. Returns HTTP 201 when created or HTTP 200 when the owned repository already exists. - `GET /repos?cursor={cursor}&limit={limit}`: list repositories. Requires `org:read` and `repo: "*"`. - `GET /repos/{repo}`: repository metadata. - `DELETE /repos/{repo}`: permanently delete a repository with `repo:delete`. Example: ```sh curl --fail-with-body \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ --data '{ "name": "workspace", "ttl_seconds": 86400, "private": true }' \ https://your-org.api.phorge.net/repos ``` ### Read endpoints Unless noted otherwise, reads require `git:read`, accept `ref`, and default to `HEAD`. - `GET /repos/{repo}/files`: list files. Supports `ref`, `path`, `recursive`, `metadata`, `cursor`, and `limit`. - `GET|HEAD /repos/{repo}/files/{path}`: stream a file. Supports `ref`, one `Range`, and `If-None-Match`. A range response uses HTTP 206. - `repo.getFileURL(path, { ref, ttlSeconds })`: generate a short-lived browser URL for exactly one private file and ref. It is read-only, defaults to 600 seconds, and cannot exceed 3600 seconds. Treat it as a secret until it expires. - `GET /repos/{repo}/archive`: stream a tar.gz archive. Supports `ref` and `path`. - `GET /repos/{repo}/grep`: search content. Requires `q`; supports `ref`, `context` from 0 to 20, `cursor`, and `limit`. - `GET /repos/{repo}/blame`: return `{ "porcelain": "..." }`. Requires `path`; supports `ref`. - `GET /repos/{repo}/branches`: list branches. Supports `cursor` and `limit`. - `GET /repos/{repo}/commits`: list commits. Supports `ref`, `cursor`, and `limit`. - `GET /repos/{repo}/commits/{sha}`: get one commit. - `GET /repos/{repo}/commits/{sha}/diff`: return a commit patch as `text/x-diff`. - `GET /repos/{repo}/diff?base={base}&head={head}`: return a three-dot diff as `text/x-diff`. ### Write endpoints Writes require `repo:write`. Successful mutations return HTTP 201. Use `expectedHeadSha` to coordinate concurrent agents. - `POST /repos/{repo}/commits`: commit file operations. `expectedHeadSha` is optional but recommended. - `POST /repos/{repo}/commits/apply-diff`: apply a unified diff and commit it. `expectedHeadSha` is required. - `POST /repos/{repo}/branches`: create a branch. `expectedHeadSha` must match the resolved `startPoint`. - `DELETE /repos/{repo}/branches/{branch}?expectedHeadSha={sha}`: conditionally delete a branch. - `POST /repos/{repo}/merges/preview`: preview without moving the target ref. - `POST /repos/{repo}/merges`: merge `source` into `target`. - `POST /repos/{repo}/tags`: create a lightweight or annotated tag. - `DELETE /repos/{repo}/tags/{tag}?expectedHeadSha={sha}`: conditionally delete a tag. File-operation commits use `multipart/form-data`. The first part is `metadata`; each upsert points to a later raw file part with `content_part`. Deletes have no file part. Example metadata: ```json { "branch": "main", "expectedHeadSha": "012345...", "message": "Update worker", "author": { "name": "Agent", "email": "agent@example.com" }, "committer": { "name": "Automation", "email": "automation@example.com" }, "operations": [ { "operation": "upsert", "path": "src/worker.ts", "content_part": "file-0", "mode": "100644" }, { "operation": "delete", "path": "src/old-worker.ts" } ] } ``` `author` and `committer` are optional. When only `author` is provided, it is used for both Git identities. If neither is provided, Phorge uses `Phorge `; request `actor` metadata is never written into the commit. Send a raw file part named `file-0` after the metadata. Part names must be unique and every declared part must be present. File bytes are not Base64 encoded. The complete multipart request is limited to 128 MiB and remains subject to repository storage quotas. Unified-diff commit body: ```json { "branch": "main", "expectedHeadSha": "012345...", "message": "Apply generated patch", "diff": "diff --git a/a.txt b/a.txt\n...", "author": { "name": "Agent", "email": "agent@example.com" } } ``` Branch body: ```json { "name": "agent-7", "startPoint": "main", "expectedHeadSha": "012345..." } ``` Merge or merge-preview body: ```json { "source": "agent-7", "target": "main", "expectedHeadSha": "012345...", "mode": "fast-forward-preferred", "message": "Merge agent work", "author": { "name": "Agent", "email": "agent@example.com" } } ``` Tag body: ```json { "name": "v1.0.0", "target": "main", "expectedHeadSha": "012345...", "message": "Release v1.0.0", "tagger": { "name": "Release Bot", "email": "releases@example.com" } } ``` ### Pagination List endpoints use `limit` and `cursor`. The default limit is 20 and the maximum is 100. ```json { "items": [], "next_cursor": "opaque-value", "has_more": true } ``` ### HTTP errors Errors use a stable code and may include structured details: ```json { "code": "HEAD_MOVED", "message": "ref has moved", "details": { "ref": "refs/heads/main", "expected": "012345...", "actual": "abcdef..." } } ``` - HTTP 400: malformed JSON, cursor, path, query input, or revision. - HTTP 401: missing or invalid token. - HTTP 403: insufficient scope, ref policy, or quota. - HTTP 404: repository, file, ref, or route not found. - HTTP 409: merge conflict or non-fast-forward merge. - HTTP 412 `HEAD_MOVED`: expected ref no longer matches. - HTTP 416 `INVALID_RANGE`: requested byte range is invalid. - HTTP 422: invalid repository name, ref, commit operation, diff, or merge input. - HTTP 428 `EXPECTED_HEAD_REQUIRED`: required expected SHA was omitted. - HTTP 429 `RATE_LIMITED`: request rate exceeded. For behavior not covered here, use the canonical documentation at https://phorge.net/docs and https://phorge.net/docs/http.