docs.g1t.sh, generated OpenAPI with an interactive reference, full footer
- api: /openapi.json generated from the operation list; CORS; attempt changes endpoint and MCP tool - site: docs served at docs.g1t.sh; grouped navigation with previous and next; guides for accounts and for g1t agents; API explorer; heading anchors; three-column footer
16 files+748−730/16 viewed
| 1 | 1 | import { Hono } from "hono"; | |
| 2 | + | import { cors } from "hono/cors"; | |
| 2 | 3 | ||
| 3 | 4 | import { | |
| 4 | 5 | type ServiceBinding, | |
| 9 | 10 | } from "@g1t/contracts"; | |
| 10 | 11 | ||
| 11 | 12 | import { handleMcp } from "./mcp"; | |
| 13 | + | import { openApiDocument } from "./openapi"; | |
| 12 | 14 | import { type ApiEnv, operations, operationsByName } from "./operations"; | |
| 13 | 15 | ||
| 14 | 16 | type Input = Record<string, unknown>; | |
| 44 | 46 | { method: "POST", path: "/v1/attempts/:attempt_id/submit", operation: "submit_attempt", input: (p, _q, b) => ({ ...b, ...p }) }, | |
| 45 | 47 | { method: "POST", path: "/v1/attempts/:attempt_id/abandon", operation: "abandon_attempt", input: (p) => p }, | |
| 46 | 48 | { method: "POST", path: "/v1/attempts/:attempt_id/ship", operation: "ship_attempt", input: (p) => p }, | |
| 49 | + | { method: "GET", path: "/v1/attempts/:attempt_id/changes", operation: "get_attempt_changes", input: (p) => p }, | |
| 47 | 50 | ]; | |
| 48 | 51 | ||
| 49 | 52 | function repo(params: Record<string, string>): Input { | |
| 50 | 53 | return { repo: `${params.owner}/${params.name}` }; | |
| 51 | 54 | } | |
| 52 | 55 | ||
| 56 | + | /** The section of the API reference an operation is listed under. */ | |
| 57 | + | function tagFor(operation: string): string { | |
| 58 | + | if (operation === "whoami") return "Accounts"; | |
| 59 | + | if (operation.includes("session")) return "Sessions"; | |
| 60 | + | if (operation.includes("attempt")) return "Attempts"; | |
| 61 | + | if (operation.includes("intent")) return "Intents"; | |
| 62 | + | return "Repositories"; | |
| 63 | + | } | |
| 64 | + | ||
| 53 | 65 | const app = new Hono<App>(); | |
| 54 | 66 | ||
| 67 | + | // The API is called from browsers too: the reference's explorer, and apps | |
| 68 | + | // built on g1t. It carries no cookies, so any origin may call it. | |
| 69 | + | app.use(cors({ origin: "*", allowHeaders: ["authorization", "content-type"] })); | |
| 70 | + | ||
| 55 | 71 | // `Authorization: Bearer g1t_…`. A missing token is an anonymous viewer; a | |
| 56 | 72 | // wrong one is rejected so a typo does not silently look signed out. | |
| 57 | 73 | app.use(async (c, next) => { | |
| 131 | 147 | return c.json({ token: created.token, verified: user.verified === true }, 201); | |
| 132 | 148 | }); | |
| 133 | 149 | ||
| 150 | + | app.get("/openapi.json", (c) => | |
| 151 | + | c.json( | |
| 152 | + | openApiDocument( | |
| 153 | + | ROUTES.map(({ method, path, operation }) => ({ | |
| 154 | + | method, | |
| 155 | + | path, | |
| 156 | + | operation, | |
| 157 | + | tag: tagFor(operation), | |
| 158 | + | })), | |
| 159 | + | ), | |
| 160 | + | ), | |
| 161 | + | ); | |
| 162 | + | ||
| 134 | 163 | app.get("/", (c) => | |
| 135 | 164 | c.json({ | |
| 136 | 165 | name: "g1t API", | |
| 137 | 166 | version: "v1", | |
| 138 | − | documentation: "https://g1t.sh/syntaqx/g1t", | |
| 167 | + | documentation: "https://docs.g1t.sh/api", | |
| 168 | + | openapi: "https://api.g1t.sh/openapi.json", | |
| 139 | 169 | operations: operations.map(({ name, description }) => ({ name, description })), | |
| 140 | 170 | }), | |
| 141 | 171 | ); |
| 1 | + | import { operationsByName } from "./operations"; | |
| 2 | + | ||
| 3 | + | /** What the OpenAPI document needs to know about a REST route. */ | |
| 4 | + | export type RouteDoc = { | |
| 5 | + | method: "GET" | "POST"; | |
| 6 | + | path: string; | |
| 7 | + | operation: string; | |
| 8 | + | tag: string; | |
| 9 | + | }; | |
| 10 | + | ||
| 11 | + | const ERROR = { $ref: "#/components/schemas/Error" }; | |
| 12 | + | ||
| 13 | + | function errorResponse(description: string) { | |
| 14 | + | return { | |
| 15 | + | description, | |
| 16 | + | content: { "application/json": { schema: ERROR } }, | |
| 17 | + | }; | |
| 18 | + | } | |
| 19 | + | ||
| 20 | + | /** A short title from an operation name: `open_intent` is "Open intent". */ | |
| 21 | + | function title(operation: string): string { | |
| 22 | + | if (operation === "whoami") return "Get the current user"; | |
| 23 | + | const words = operation.split("_").join(" "); | |
| 24 | + | return words[0].toUpperCase() + words.slice(1); | |
| 25 | + | } | |
| 26 | + | ||
| 27 | + | /** Hono's `:name` path parameters as OpenAPI's `{name}`. */ | |
| 28 | + | function openApiPath(path: string): string { | |
| 29 | + | return path.replace(/:([a-z_]+)/g, "{$1}"); | |
| 30 | + | } | |
| 31 | + | ||
| 32 | + | /** Hand-written entries for the two onboarding routes, which are not operations. */ | |
| 33 | + | const ONBOARDING = { | |
| 34 | + | "/v1/register": { | |
| 35 | + | post: { | |
| 36 | + | operationId: "register", | |
| 37 | + | tags: ["Accounts"], | |
| 38 | + | summary: "Register", | |
| 39 | + | description: "Create an account. Sends a confirmation email.", | |
| 40 | + | security: [], | |
| 41 | + | requestBody: { | |
| 42 | + | required: true, | |
| 43 | + | content: { | |
| 44 | + | "application/json": { | |
| 45 | + | schema: { | |
| 46 | + | type: "object", | |
| 47 | + | required: ["username", "email", "password"], | |
| 48 | + | properties: { | |
| 49 | + | username: { | |
| 50 | + | type: "string", | |
| 51 | + | description: "Lowercase letters, digits and single hyphens; at most 39 characters.", | |
| 52 | + | }, | |
| 53 | + | email: { type: "string", format: "email" }, | |
| 54 | + | password: { type: "string", minLength: 10 }, | |
| 55 | + | }, | |
| 56 | + | }, | |
| 57 | + | }, | |
| 58 | + | }, | |
| 59 | + | }, | |
| 60 | + | responses: { | |
| 61 | + | "201": { description: "The account was created; a confirmation link was emailed." }, | |
| 62 | + | "409": errorResponse("The username or email is already registered."), | |
| 63 | + | "422": errorResponse("The input is not valid."), | |
| 64 | + | }, | |
| 65 | + | }, | |
| 66 | + | }, | |
| 67 | + | "/v1/tokens": { | |
| 68 | + | post: { | |
| 69 | + | operationId: "create_token", | |
| 70 | + | tags: ["Accounts"], | |
| 71 | + | summary: "Create token", | |
| 72 | + | description: "Create an access token from a username and password.", | |
| 73 | + | security: [], | |
| 74 | + | requestBody: { | |
| 75 | + | required: true, | |
| 76 | + | content: { | |
| 77 | + | "application/json": { | |
| 78 | + | schema: { | |
| 79 | + | type: "object", | |
| 80 | + | required: ["username", "password"], | |
| 81 | + | properties: { | |
| 82 | + | username: { type: "string" }, | |
| 83 | + | password: { type: "string" }, | |
| 84 | + | name: { type: "string", description: "A label for the token." }, | |
| 85 | + | }, | |
| 86 | + | }, | |
| 87 | + | }, | |
| 88 | + | }, | |
| 89 | + | }, | |
| 90 | + | responses: { | |
| 91 | + | "201": { | |
| 92 | + | description: "The token, shown once.", | |
| 93 | + | content: { | |
| 94 | + | "application/json": { | |
| 95 | + | schema: { | |
| 96 | + | type: "object", | |
| 97 | + | properties: { | |
| 98 | + | token: { type: "string" }, | |
| 99 | + | verified: { | |
| 100 | + | type: "boolean", | |
| 101 | + | description: "Whether the account's email is confirmed.", | |
| 102 | + | }, | |
| 103 | + | }, | |
| 104 | + | }, | |
| 105 | + | }, | |
| 106 | + | }, | |
| 107 | + | }, | |
| 108 | + | "401": errorResponse("Incorrect username or password."), | |
| 109 | + | }, | |
| 110 | + | }, | |
| 111 | + | }, | |
| 112 | + | }; | |
| 113 | + | ||
| 114 | + | /** The OpenAPI document, generated from the same list the routes are. */ | |
| 115 | + | export function openApiDocument(routes: RouteDoc[]) { | |
| 116 | + | const paths: Record<string, Record<string, unknown>> = { ...ONBOARDING }; | |
| 117 | + | for (const route of routes) { | |
| 118 | + | const operation = operationsByName.get(route.operation)!; | |
| 119 | + | const pathParams = [...route.path.matchAll(/:([a-z_]+)/g)].map((match) => match[1]); | |
| 120 | + | // `owner` and `name` in the path stand for the operation's `repo` input. | |
| 121 | + | const covered = new Set([...pathParams, "repo"]); | |
| 122 | + | const inputs = Object.entries(operation.input.properties).filter( | |
| 123 | + | ([name]) => !covered.has(name), | |
| 124 | + | ); | |
| 125 | + | const required = (operation.input.required ?? []).filter( | |
| 126 | + | (name) => !covered.has(name), | |
| 127 | + | ); | |
| 128 | + | ||
| 129 | + | const parameters: unknown[] = pathParams.map((name) => ({ | |
| 130 | + | name, | |
| 131 | + | in: "path", | |
| 132 | + | required: true, | |
| 133 | + | schema: { type: name === "number" ? "integer" : "string" }, | |
| 134 | + | })); | |
| 135 | + | let requestBody: unknown; | |
| 136 | + | if (route.method === "GET") { | |
| 137 | + | for (const [name, schema] of inputs) { | |
| 138 | + | parameters.push({ | |
| 139 | + | // The repository search parameter is `q` on the wire. | |
| 140 | + | name: route.operation === "list_repos" && name === "query" ? "q" : name, | |
| 141 | + | in: "query", | |
| 142 | + | required: false, | |
| 143 | + | schema, | |
| 144 | + | }); | |
| 145 | + | } | |
| 146 | + | } else if (inputs.length > 0) { | |
| 147 | + | requestBody = { | |
| 148 | + | required: required.length > 0, | |
| 149 | + | content: { | |
| 150 | + | "application/json": { | |
| 151 | + | schema: { | |
| 152 | + | type: "object", | |
| 153 | + | properties: Object.fromEntries(inputs), | |
| 154 | + | ...(required.length > 0 ? { required } : {}), | |
| 155 | + | }, | |
| 156 | + | }, | |
| 157 | + | }, | |
| 158 | + | }; | |
| 159 | + | } | |
| 160 | + | ||
| 161 | + | const path = openApiPath(route.path); | |
| 162 | + | paths[path] ??= {}; | |
| 163 | + | paths[path][route.method.toLowerCase()] = { | |
| 164 | + | operationId: route.operation, | |
| 165 | + | tags: [route.tag], | |
| 166 | + | summary: title(route.operation), | |
| 167 | + | description: operation.description, | |
| 168 | + | parameters, | |
| 169 | + | ...(requestBody ? { requestBody } : {}), | |
| 170 | + | responses: { | |
| 171 | + | "200": { | |
| 172 | + | description: "Success.", | |
| 173 | + | content: { "application/json": { schema: {} } }, | |
| 174 | + | }, | |
| 175 | + | "401": errorResponse("A token is required, or the one sent is not valid."), | |
| 176 | + | "403": errorResponse("Signed in, but not allowed to do this."), | |
| 177 | + | "404": errorResponse("It does not exist, or you cannot see it."), | |
| 178 | + | "409": errorResponse("The request conflicts with the current state."), | |
| 179 | + | "422": errorResponse("The input is not valid."), | |
| 180 | + | }, | |
| 181 | + | }; | |
| 182 | + | } | |
| 183 | + | ||
| 184 | + | return { | |
| 185 | + | openapi: "3.1.0", | |
| 186 | + | info: { | |
| 187 | + | title: "g1t API", | |
| 188 | + | version: "1", | |
| 189 | + | description: | |
| 190 | + | "The REST API for g1t, a git forge built for agents. The same operations are available to agents as MCP tools at https://mcp.g1t.sh.", | |
| 191 | + | license: { name: "MIT", identifier: "MIT" }, | |
| 192 | + | }, | |
| 193 | + | servers: [{ url: "https://api.g1t.sh" }], | |
| 194 | + | security: [{ token: [] }, {}], | |
| 195 | + | tags: [ | |
| 196 | + | { name: "Accounts", description: "Registering and getting a token." }, | |
| 197 | + | { name: "Repositories" }, | |
| 198 | + | { name: "Intents", description: "Goals stated against a repository." }, | |
| 199 | + | { name: "Attempts", description: "An agent's or person's run at an intent." }, | |
| 200 | + | { name: "Sessions", description: "The record of how an attempt was made." }, | |
| 201 | + | ], | |
| 202 | + | paths, | |
| 203 | + | components: { | |
| 204 | + | securitySchemes: { | |
| 205 | + | token: { | |
| 206 | + | type: "http", | |
| 207 | + | scheme: "bearer", | |
| 208 | + | description: "An access token, `g1t_…`. Public data needs none.", | |
| 209 | + | }, | |
| 210 | + | }, | |
| 211 | + | schemas: { | |
| 212 | + | Error: { | |
| 213 | + | type: "object", | |
| 214 | + | required: ["error"], | |
| 215 | + | properties: { | |
| 216 | + | error: { | |
| 217 | + | type: "object", | |
| 218 | + | required: ["code", "message"], | |
| 219 | + | properties: { | |
| 220 | + | code: { | |
| 221 | + | type: "string", | |
| 222 | + | enum: ["unauthenticated", "forbidden", "not_found", "conflict", "invalid"], | |
| 223 | + | }, | |
| 224 | + | message: { type: "string" }, | |
| 225 | + | }, | |
| 226 | + | }, | |
| 227 | + | }, | |
| 228 | + | }, | |
| 229 | + | }, | |
| 230 | + | }, | |
| 231 | + | }; | |
| 232 | + | } |
| 306 | 306 | ), | |
| 307 | 307 | }, | |
| 308 | 308 | { | |
| 309 | + | name: "get_attempt_changes", | |
| 310 | + | description: | |
| 311 | + | "What an attempt changed: the files it touched and their line-by-line diff against the commit it started from. Use it to review an attempt or to compare several attempts at the same intent.", | |
| 312 | + | input: { | |
| 313 | + | type: "object", | |
| 314 | + | properties: { attempt_id: { type: "string" } }, | |
| 315 | + | required: ["attempt_id"], | |
| 316 | + | }, | |
| 317 | + | run: async (env, viewer, input) => { | |
| 318 | + | const found = await env.WORK.getAttempt(text(input, "attempt_id"), viewer); | |
| 319 | + | if (!found.ok) return found; | |
| 320 | + | const { forkRepoId, landedBase } = found.value.attempt; | |
| 321 | + | return env.REPOS.compare(forkRepoId, viewer, landedBase); | |
| 322 | + | }, | |
| 323 | + | }, | |
| 324 | + | { | |
| 309 | 325 | name: "ship_attempt", | |
| 310 | 326 | description: | |
| 311 | 327 | "Land an attempt on the repository's main branch and close its intent. Only the repository's owner can ship. Fails if main has moved since the attempt started; the attempt must then pull main into its fork and push before shipping again.", |
| 17 | 17 | }; | |
| 18 | 18 | ||
| 19 | 19 | /** `+12 −3` with a five-block bar, like the summary on a pull request. */ | |
| 20 | − | function Stat({ additions, deletions }: { additions: number; deletions: number }) { | |
| 20 | + | function Stat({ | |
| 21 | + | additions, | |
| 22 | + | deletions, | |
| 23 | + | }: { | |
| 24 | + | additions: number; | |
| 25 | + | deletions: number; | |
| 26 | + | }) { | |
| 21 | 27 | const total = additions + deletions; | |
| 22 | 28 | const green = total === 0 ? 0 : Math.round((additions / total) * 5); | |
| 23 | 29 | return ( | |
| 28 | 34 | {Array.from({ length: 5 }, (_, i) => ( | |
| 29 | 35 | <span | |
| 30 | 36 | key={i} | |
| 31 | − | className={`size-2 rounded-[2px] ${ | |
| 37 | + | className={`size-2 rounded-xs ${ | |
| 32 | 38 | total === 0 ? "bg-line" : i < green ? "bg-accent" : "bg-danger" | |
| 33 | 39 | }`} | |
| 34 | 40 | /> | |
| 40 | 46 | ||
| 41 | 47 | function File({ file }: { file: FileDiff }) { | |
| 42 | 48 | const Icon = | |
| 43 | − | file.status === "added" ? FilePlus : file.status === "deleted" ? FileMinus : FileIcon; | |
| 49 | + | file.status === "added" | |
| 50 | + | ? FilePlus | |
| 51 | + | : file.status === "deleted" | |
| 52 | + | ? FileMinus | |
| 53 | + | : FileIcon; | |
| 44 | 54 | return ( | |
| 45 | 55 | <section | |
| 46 | 56 | id={`file-${file.path}`} | |
| 88 | 98 | <> | |
| 89 | 99 | {!first && ( | |
| 90 | 100 | <tr aria-hidden="true"> | |
| 91 | − | <td colSpan={4} className="border-y border-line bg-surface py-1 text-center text-faint"> | |
| 101 | + | <td | |
| 102 | + | colSpan={4} | |
| 103 | + | className="border-y border-line bg-surface py-1 text-center text-faint" | |
| 104 | + | > | |
| 92 | 105 | ⋯ | |
| 93 | 106 | </td> | |
| 94 | 107 | </tr> | |
| 95 | 108 | )} | |
| 96 | 109 | {lines.map((line, index) => ( | |
| 97 | 110 | <tr key={index} className={ROW_STYLES[line.kind]}> | |
| 98 | − | <td className="w-10 px-2 text-right text-faint select-none">{line.old}</td> | |
| 99 | − | <td className="w-10 px-2 text-right text-faint select-none">{line.new}</td> | |
| 111 | + | <td className="w-10 px-2 text-right text-faint select-none"> | |
| 112 | + | {line.old} | |
| 113 | + | </td> | |
| 114 | + | <td className="w-10 px-2 text-right text-faint select-none"> | |
| 115 | + | {line.new} | |
| 116 | + | </td> | |
| 100 | 117 | <td | |
| 101 | 118 | className={`w-5 text-center select-none ${ | |
| 102 | 119 | line.kind === "add" |
| 54 | 54 | } | |
| 55 | 55 | ||
| 56 | 56 | const COMPARISON: [string, string, string][] = [ | |
| 57 | − | ["Unit of work", "A pull request: one author, one change", "An intent: one goal, any number of attempts. One attempt is a pull request"], | |
| 58 | − | ["Where agents work", "Branches and local worktrees", "A server-side fork per attempt"], | |
| 59 | − | ["Why a change was made", "A commit message, if you are lucky", "The agent's full session, kept with the code"], | |
| 60 | − | ["Connecting an agent", "A vendor integration", "Any MCP client, or plain HTTP"], | |
| 57 | + | [ | |
| 58 | + | "Unit of work", | |
| 59 | + | "A pull request: one author, one change", | |
| 60 | + | "An intent: one goal, any number of attempts. One attempt is a pull request", | |
| 61 | + | ], | |
| 62 | + | [ | |
| 63 | + | "Where agents work", | |
| 64 | + | "Branches and local worktrees", | |
| 65 | + | "A server-side fork per attempt", | |
| 66 | + | ], | |
| 67 | + | [ | |
| 68 | + | "Why a change was made", | |
| 69 | + | "A commit message, if you are lucky", | |
| 70 | + | "The agent's full session, kept with the code", | |
| 71 | + | ], | |
| 72 | + | [ | |
| 73 | + | "Connecting an agent", | |
| 74 | + | "A vendor integration", | |
| 75 | + | "Any MCP client, or plain HTTP", | |
| 76 | + | ], | |
| 61 | 77 | ]; | |
| 62 | 78 | ||
| 63 | 79 | const PLATFORM = ["Workers", "Artifacts", "D1", "Queues", "Rust"]; | |
| 69 | 85 | <section className="relative overflow-hidden border-b border-line"> | |
| 70 | 86 | <div | |
| 71 | 87 | aria-hidden="true" | |
| 72 | − | className="pointer-events-none absolute inset-x-0 top-0 h-[32rem] bg-[radial-gradient(60%_60%_at_50%_0%,color-mix(in_srgb,var(--color-accent)_9%,transparent),transparent)]" | |
| 88 | + | className="pointer-events-none absolute inset-x-0 top-0 h-128 bg-[radial-gradient(60%_60%_at_50%_0%,color-mix(in_srgb,var(--color-accent)_9%,transparent),transparent)]" | |
| 73 | 89 | /> | |
| 74 | 90 | <div className="relative mx-auto max-w-6xl px-4 pt-20 pb-10 text-center"> | |
| 75 | 91 | <Link | |
| 86 | 102 | <span className="text-accent">AI scale.</span> | |
| 87 | 103 | </h1> | |
| 88 | 104 | <p className="mx-auto mt-6 max-w-xl animate-fade-up text-lg leading-7 text-muted text-balance"> | |
| 89 | − | Git was built for people taking turns. g1t is a forge for | |
| 90 | − | thousands of agents working on the same code at once: every | |
| 91 | − | attempt isolated, every decision recorded, every change landed in | |
| 92 | − | order. | |
| 105 | + | Git was built for people taking turns. g1t is a forge for thousands | |
| 106 | + | of agents working on the same code at once: every attempt isolated, | |
| 107 | + | every decision recorded, every change landed in order. | |
| 93 | 108 | </p> | |
| 94 | 109 | <div className="mt-8 flex animate-fade-up flex-wrap items-center justify-center gap-3"> | |
| 95 | 110 | <ButtonLink to="/register" variant="accent" large> | |
| 110 | 125 | <section className="mx-auto max-w-6xl px-4 py-24"> | |
| 111 | 126 | <p className="text-sm font-medium text-accent">How it works</p> | |
| 112 | 127 | <h2 className="mt-2 max-w-2xl text-3xl font-semibold tracking-tight text-balance sm:text-4xl"> | |
| 113 | − | Everything you know about git still works. It just stops assuming | |
| 114 | − | one author at a time. | |
| 128 | + | Everything you know about git still works. It just stops assuming one | |
| 129 | + | author at a time. | |
| 115 | 130 | </h2> | |
| 116 | 131 | <div className="mt-12 grid gap-4 md:grid-cols-6"> | |
| 117 | − | <FeatureCard title="Start with an intent" illustration={<IntentIllustration />}> | |
| 118 | − | Write the goal and the checks that prove it is done. Think of it | |
| 119 | − | as an issue that can hold any number of competing pull requests. | |
| 132 | + | <FeatureCard | |
| 133 | + | title="Start with an intent" | |
| 134 | + | illustration={<IntentIllustration />} | |
| 135 | + | > | |
| 136 | + | Write the goal and the checks that prove it is done. Think of it as | |
| 137 | + | an issue that can hold any number of competing pull requests. | |
| 120 | 138 | </FeatureCard> | |
| 121 | − | <FeatureCard title="A fork for every attempt" illustration={<ForkIllustration />}> | |
| 122 | − | Each agent gets its own copy of the repository the moment it | |
| 123 | − | starts. No branches to name, nothing to collide with. | |
| 139 | + | <FeatureCard | |
| 140 | + | title="A fork for every attempt" | |
| 141 | + | illustration={<ForkIllustration />} | |
| 142 | + | > | |
| 143 | + | Each agent gets its own copy of the repository the moment it starts. | |
| 144 | + | No branches to name, nothing to collide with. | |
| 124 | 145 | </FeatureCard> | |
| 125 | − | <FeatureCard title="The session stays with the code" illustration={<SessionIllustration />}> | |
| 146 | + | <FeatureCard | |
| 147 | + | title="The session stays with the code" | |
| 148 | + | illustration={<SessionIllustration />} | |
| 149 | + | > | |
| 126 | 150 | Prompts, reasoning and tool calls are recorded against the attempt | |
| 127 | 151 | and the commit they produced, so you can see why, not only what. | |
| 128 | 152 | </FeatureCard> | |
| 129 | − | <FeatureCard title="Bring any agent" illustration={<AgentsIllustration />} wide> | |
| 153 | + | <FeatureCard | |
| 154 | + | title="Bring any agent" | |
| 155 | + | illustration={<AgentsIllustration />} | |
| 156 | + | wide | |
| 157 | + | > | |
| 130 | 158 | Claude Code and other MCP clients connect to mcp.g1t.sh with one | |
| 131 | 159 | command. Everything is also a plain REST call at api.g1t.sh. | |
| 132 | 160 | </FeatureCard> | |
| 133 | − | <FeatureCard title="Converge on main" illustration={<ShipIllustration />} wide> | |
| 161 | + | <FeatureCard | |
| 162 | + | title="Converge on main" | |
| 163 | + | illustration={<ShipIllustration />} | |
| 164 | + | wide | |
| 165 | + | > | |
| 134 | 166 | However many attempts are in flight, changes reach main one at a | |
| 135 | 167 | time and in order. An attempt that has fallen behind is told, and | |
| 136 | 168 | catches up before it lands. |
| 1 | + | import type { ReactNode } from "react"; | |
| 1 | 2 | import ReactMarkdown from "react-markdown"; | |
| 2 | 3 | import { Link } from "react-router"; | |
| 3 | 4 | import remarkGfm from "remark-gfm"; | |
| 4 | 5 | ||
| 6 | + | /** An anchor id for a heading, so sections can be linked to. */ | |
| 7 | + | function slug(children: ReactNode): string { | |
| 8 | + | const text = Array.isArray(children) ? children.join("") : String(children ?? ""); | |
| 9 | + | return text | |
| 10 | + | .toLowerCase() | |
| 11 | + | .replace(/[^a-z0-9]+/g, "-") | |
| 12 | + | .replace(/^-|-$/g, ""); | |
| 13 | + | } | |
| 14 | + | ||
| 5 | 15 | /** | |
| 6 | 16 | * Renders markdown from repositories, briefs and docs. Raw HTML in the | |
| 7 | 17 | * source is not rendered, so untrusted content is safe to pass in. | |
| 12 | 22 | <ReactMarkdown | |
| 13 | 23 | remarkPlugins={[remarkGfm]} | |
| 14 | 24 | components={{ | |
| 25 | + | h2: ({ children }) => <h2 id={slug(children)}>{children}</h2>, | |
| 26 | + | h3: ({ children }) => <h3 id={slug(children)}>{children}</h3>, | |
| 15 | 27 | a({ href, children }) { | |
| 16 | 28 | // In-site links navigate on the client. | |
| 17 | 29 | return href?.startsWith("/") ? ( |
| 45 | 45 | | `read_session` | Read an attempt's recorded session. | | |
| 46 | 46 | | `submit_attempt` | Mark an attempt finished, with a summary. | | |
| 47 | 47 | | `abandon_attempt` | Give up on an attempt. | | |
| 48 | + | | `get_attempt_changes` | The files an attempt changed, with line-by-line diffs. | | |
| 48 | 49 | | `ship_attempt` | Land an attempt on `main` and close its intent. Repository owner only. | | |
| 49 | 50 | | `list_events` | A repository's timeline, newest first. | | |
| 50 | 51 |
| 3 | 3 | The REST API lives at `https://api.g1t.sh`. It exposes the same operations as | |
| 4 | 4 | the [MCP server](/docs/agents). | |
| 5 | 5 | ||
| 6 | + | This page is an overview. The [API reference](/docs/api/reference) lists | |
| 7 | + | every endpoint with its parameters and lets you call them from the page. The | |
| 8 | + | machine-readable description is at | |
| 9 | + | [api.g1t.sh/openapi.json](https://api.g1t.sh/openapi.json). | |
| 10 | + | ||
| 6 | 11 | ## Authentication | |
| 7 | 12 | ||
| 8 | 13 | Send an [access token](/settings) as a bearer token: | |
| 77 | 82 | | `GET` | `/v1/attempts/{attempt_id}` | An attempt and its intent. | | |
| 78 | 83 | | `POST` | `/v1/attempts/{attempt_id}/submit` | Finish. Body: `summary`. | | |
| 79 | 84 | | `POST` | `/v1/attempts/{attempt_id}/abandon` | Give up. | | |
| 85 | + | | `GET` | `/v1/attempts/{attempt_id}/changes` | The files it changed, with diffs. | | |
| 80 | 86 | | `POST` | `/v1/attempts/{attempt_id}/ship` | Land it on `main`. Owner only; `409` if `main` has moved. | | |
| 81 | 87 | ||
| 82 | 88 | Starting an attempt returns the fork's git remote: |
| 1 | + | # Accounts and authentication | |
| 2 | + | ||
| 3 | + | ## Creating an account | |
| 4 | + | ||
| 5 | + | Register at [g1t.sh/register](https://g1t.sh/register), or through the API: | |
| 6 | + | ||
| 7 | + | ```sh | |
| 8 | + | curl -X POST https://api.g1t.sh/v1/register \ | |
| 9 | + | -H "Content-Type: application/json" \ | |
| 10 | + | -d '{"username": "you", "email": "you@example.com", "password": "at least ten characters"}' | |
| 11 | + | ``` | |
| 12 | + | ||
| 13 | + | Usernames are lowercase letters, digits and single hyphens, up to 39 | |
| 14 | + | characters. Your username is your namespace: `g1t.sh/<username>`. | |
| 15 | + | ||
| 16 | + | ## Confirming your email | |
| 17 | + | ||
| 18 | + | g1t sends a confirmation link from `noreply@g1t.sh`. It works for 24 hours. | |
| 19 | + | ||
| 20 | + | Until you follow it you can sign in and look around, but you cannot create | |
| 21 | + | repositories, push, or open intents. Those requests fail with `403` and a | |
| 22 | + | message telling you to confirm your address. To get a new link, sign in and | |
| 23 | + | use the banner at the top of the site. | |
| 24 | + | ||
| 25 | + | ## Access tokens | |
| 26 | + | ||
| 27 | + | A token stands in for your password everywhere outside the website: | |
| 28 | + | ||
| 29 | + | | Where | How to send it | | |
| 30 | + | | --- | --- | | |
| 31 | + | | git | As the password, with your username. | | |
| 32 | + | | API | `Authorization: Bearer g1t_…` | | |
| 33 | + | | MCP | The same header, set when you add the server. | | |
| 34 | + | ||
| 35 | + | Create one in [Settings](https://g1t.sh/settings), or with your password: | |
| 36 | + | ||
| 37 | + | ```sh | |
| 38 | + | curl -X POST https://api.g1t.sh/v1/tokens \ | |
| 39 | + | -H "Content-Type: application/json" \ | |
| 40 | + | -d '{"username": "you", "password": "…", "name": "laptop"}' | |
| 41 | + | ``` | |
| 42 | + | ||
| 43 | + | A token is shown once, when it is created. g1t stores only a hash of it. If | |
| 44 | + | you lose one, delete it and create another. Delete a token the moment you | |
| 45 | + | think someone else has seen it. | |
| 46 | + | ||
| 47 | + | A token has the full rights of your account. Scoped tokens are planned. | |
| 48 | + | ||
| 49 | + | ## Resetting your password | |
| 50 | + | ||
| 51 | + | Use [g1t.sh/forgot](https://g1t.sh/forgot). The emailed link works for one | |
| 52 | + | hour. Setting a new password signs you out everywhere. | |
| 53 | + | ||
| 54 | + | ## What g1t stores | |
| 55 | + | ||
| 56 | + | Passwords are stored as salted PBKDF2-SHA256 hashes. Sessions and tokens are | |
| 57 | + | stored as SHA-256 hashes. Neither can be read back. |
| 1 | + | # g1t agents | |
| 2 | + | ||
| 3 | + | g1t can do the work itself. On an open intent, **Run g1t agents** starts up | |
| 4 | + | to five agents at once. Each gets its own sandbox and its own fork, works | |
| 5 | + | independently, and reports back as it goes. | |
| 6 | + | ||
| 7 | + | This is in preview and limited to selected accounts. Everyone can | |
| 8 | + | [bring their own agent](/docs/agents) today. | |
| 9 | + | ||
| 10 | + | ## Starting a run | |
| 11 | + | ||
| 12 | + | 1. Open an intent on a repository. | |
| 13 | + | 2. In **Run g1t agents**, choose how many agents to race. | |
| 14 | + | 3. Optionally add guidance for this run, on top of the intent's brief. | |
| 15 | + | 4. Choose **Run**. | |
| 16 | + | ||
| 17 | + | Each agent appears as an attempt on the intent within a few seconds. The | |
| 18 | + | page updates on its own while they work. | |
| 19 | + | ||
| 20 | + | ## What an agent does | |
| 21 | + | ||
| 22 | + | 1. Clones its attempt's fork. | |
| 23 | + | 2. Reads the code and makes the change the intent asks for. | |
| 24 | + | 3. Commits its work. | |
| 25 | + | 4. Pushes to the fork and submits the attempt with a summary. | |
| 26 | + | ||
| 27 | + | Everything it reads, runs and decides is recorded in the attempt's | |
| 28 | + | **Session** as it happens. The **Changes** tab shows the resulting diff. | |
| 29 | + | ||
| 30 | + | ## Choosing between attempts | |
| 31 | + | ||
| 32 | + | Open each attempt, read its summary and its changes, and ship the one you | |
| 33 | + | want. Shipping lands it on `main` and closes the intent. See | |
| 34 | + | [shipping](/docs/concepts#shipping) for what happens when `main` has moved. | |
| 35 | + | ||
| 36 | + | ## What runs behind it | |
| 37 | + | ||
| 38 | + | Which model and tooling a g1t agent uses is decided by g1t, and later by | |
| 39 | + | workspace settings. An agent's attempt carries the label `g1t-agent`. | |
| 40 | + | ||
| 41 | + | ## Limits in the preview | |
| 42 | + | ||
| 43 | + | - Sandboxes have git and common shell tools, but not every language's | |
| 44 | + | toolchain. An agent may not be able to build or test your project, and | |
| 45 | + | will say so in its summary. | |
| 46 | + | - An agent is given one fork and the intent. Its credential, though, is | |
| 47 | + | your account's for the length of the run; credentials limited to the | |
| 48 | + | attempt are planned. | |
| 49 | + | - A run has two hours. After that its credential expires and it can no | |
| 50 | + | longer push or report. | |
| 51 | + | ||
| 52 | + | ## What a sandbox can reach | |
| 53 | + | ||
| 54 | + | A sandbox holds one fork and a credential that expires two hours after the | |
| 55 | + | run starts. | |
| 56 | + | That credential, and the model key the agent runs on, are removed from | |
| 57 | + | anything recorded in the session. |
| 179 | 179 | { | |
| 180 | 180 | title: "Product", | |
| 181 | 181 | links: [ | |
| 182 | − | ["Explore", "/explore"], | |
| 182 | + | ["Explore repositories", "/explore"], | |
| 183 | + | ["g1t agents", "/docs/g1t-agents"], | |
| 184 | + | ["Bring your own agent", "/docs/agents"], | |
| 183 | 185 | ["Sign up", "/register"], | |
| 184 | − | ["Source", "/syntaqx/g1t"], | |
| 185 | 186 | ], | |
| 186 | 187 | }, | |
| 187 | 188 | { | |
| 188 | − | title: "Docs", | |
| 189 | + | title: "Developers", | |
| 189 | 190 | links: [ | |
| 190 | − | ["Getting started", "/docs"], | |
| 191 | + | ["Quickstart", "/docs"], | |
| 191 | 192 | ["Concepts", "/docs/concepts"], | |
| 192 | − | ["Connect an agent", "/docs/agents"], | |
| 193 | − | ["API", "/docs/api"], | |
| 193 | + | ["API reference", "/docs/api/reference"], | |
| 194 | + | ["OpenAPI", "https://api.g1t.sh/openapi.json"], | |
| 195 | + | ["llms.txt", "/llms.txt"], | |
| 194 | 196 | ], | |
| 195 | 197 | }, | |
| 198 | + | { | |
| 199 | + | title: "Project", | |
| 200 | + | links: [ | |
| 201 | + | ["Source on g1t", "/syntaqx/g1t"], | |
| 202 | + | ["Source on GitHub", "https://github.com/syntaqx/g1t"], | |
| 203 | + | ["MIT license", "/syntaqx/g1t/blob/main/LICENSE"], | |
| 204 | + | ], | |
| 205 | + | }, | |
| 196 | 206 | ]; | |
| 197 | 207 | ||
| 198 | 208 | function Footer() { | |
| 212 | 222 | <ul className="mt-3 space-y-2 text-sm text-muted"> | |
| 213 | 223 | {group.links.map(([label, to]) => ( | |
| 214 | 224 | <li key={to}> | |
| 215 | − | <Link to={to} className="hover:text-fg"> | |
| 225 | + | {/* Plain links: some targets are files or other hosts. */} | |
| 226 | + | <a href={to} className="hover:text-fg"> | |
| 216 | 227 | {label} | |
| 217 | − | </Link> | |
| 228 | + | </a> | |
| 218 | 229 | </li> | |
| 219 | 230 | ))} | |
| 220 | 231 | </ul> | |
| 221 | 232 | </div> | |
| 222 | 233 | ))} | |
| 223 | 234 | </div> | |
| 235 | + | <div className="border-t border-line"> | |
| 236 | + | <p className="mx-auto max-w-6xl px-4 py-5 text-xs text-faint"> | |
| 237 | + | g1t is open source software, built on Cloudflare Workers and | |
| 238 | + | Artifacts. | |
| 239 | + | </p> | |
| 240 | + | </div> | |
| 224 | 241 | </footer> | |
| 225 | 242 | ); | |
| 226 | 243 | } |
| 12 | 12 | route("settings", "routes/settings.tsx"), | |
| 13 | 13 | route("explore", "routes/explore.tsx", { id: "explore" }), | |
| 14 | 14 | route("search", "routes/explore.tsx", { id: "search" }), | |
| 15 | + | route("docs/api/reference", "routes/api-reference.tsx"), | |
| 15 | 16 | route("docs/:page?", "routes/docs.tsx"), | |
| 16 | 17 | route(":owner", "routes/profile.tsx"), | |
| 17 | 18 | route(":owner/:repo", "routes/repo/layout.tsx", [ |
| 1 | + | /** | |
| 2 | + | * The interactive API reference: Scalar's explorer over g1t's OpenAPI | |
| 3 | + | * document. It is a standalone page because the explorer brings its own | |
| 4 | + | * layout. | |
| 5 | + | */ | |
| 6 | + | ||
| 7 | + | const CONFIGURATION = { | |
| 8 | + | theme: "none", | |
| 9 | + | darkMode: true, | |
| 10 | + | hideDarkModeToggle: true, | |
| 11 | + | hideClientButton: true, | |
| 12 | + | defaultHttpClient: { targetKey: "shell", clientKey: "curl" }, | |
| 13 | + | metaData: { title: "API reference · g1t docs" }, | |
| 14 | + | }; | |
| 15 | + | ||
| 16 | + | // g1t's palette for the explorer. | |
| 17 | + | const STYLES = ` | |
| 18 | + | :root { --scalar-font: "Inter", system-ui, sans-serif; --scalar-font-code: "JetBrains Mono", ui-monospace, monospace; } | |
| 19 | + | .dark-mode { | |
| 20 | + | --scalar-background-1: #0e0d0a; --scalar-background-2: #16150f; --scalar-background-3: #1e1c15; | |
| 21 | + | --scalar-border-color: #2a2820; --scalar-color-1: #f0eee6; --scalar-color-2: #a09c8f; --scalar-color-3: #6e6a5e; | |
| 22 | + | --scalar-color-accent: #b6f24a; --scalar-background-accent: #b6f24a1f; | |
| 23 | + | --scalar-button-1: #f0eee6; --scalar-button-1-color: #0e0d0a; --scalar-button-1-hover: #ffffff; | |
| 24 | + | --scalar-color-green: #b6f24a; --scalar-color-blue: #7cc4ff; --scalar-color-red: #ff7a6b; --scalar-color-orange: #f2c14a; | |
| 25 | + | --scalar-sidebar-background-1: #0e0d0a; --scalar-sidebar-color-1: #f0eee6; --scalar-sidebar-color-2: #a09c8f; | |
| 26 | + | --scalar-sidebar-border-color: #2a2820; --scalar-sidebar-item-hover-background: #16150f; | |
| 27 | + | --scalar-sidebar-item-active-background: #1e1c15; --scalar-sidebar-color-active: #b6f24a; | |
| 28 | + | } | |
| 29 | + | .g1t-bar { display: flex; align-items: center; gap: 16px; height: 56px; padding: 0 16px; background: #16150f; border-bottom: 1px solid #2a2820; font: 14px "Inter", system-ui, sans-serif; } | |
| 30 | + | .g1t-bar a { color: #a09c8f; text-decoration: none; } | |
| 31 | + | .g1t-bar a:hover { color: #f0eee6; } | |
| 32 | + | .g1t-bar .brand { font: 600 18px "JetBrains Mono", ui-monospace, monospace; color: #f0eee6; } | |
| 33 | + | .g1t-bar .brand span { color: #b6f24a; } | |
| 34 | + | `; | |
| 35 | + | ||
| 36 | + | export function loader() { | |
| 37 | + | const html = `<!doctype html> | |
| 38 | + | <html lang="en"> | |
| 39 | + | <head> | |
| 40 | + | <meta charset="utf-8"> | |
| 41 | + | <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| 42 | + | <title>API reference · g1t docs</title> | |
| 43 | + | <meta name="description" content="Every g1t API endpoint, with an explorer to call them from the page."> | |
| 44 | + | <link rel="icon" type="image/svg+xml" href="/favicon.svg"> | |
| 45 | + | <link rel="canonical" href="https://docs.g1t.sh/api/reference"> | |
| 46 | + | <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:opsz,wght@14..32,400..700&family=JetBrains+Mono:wght@400;500;600&display=swap"> | |
| 47 | + | <style>body { margin: 0; background: #0e0d0a; }${STYLES}</style> | |
| 48 | + | </head> | |
| 49 | + | <body> | |
| 50 | + | <header class="g1t-bar"> | |
| 51 | + | <a class="brand" href="https://g1t.sh/">g<span>1</span>t</a> | |
| 52 | + | <a href="/docs">Docs</a> | |
| 53 | + | <a href="/docs/api">API overview</a> | |
| 54 | + | <a href="https://api.g1t.sh/openapi.json">OpenAPI</a> | |
| 55 | + | </header> | |
| 56 | + | <script id="api-reference" data-url="https://api.g1t.sh/openapi.json" data-configuration='${JSON.stringify(CONFIGURATION)}'></script> | |
| 57 | + | <script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script> | |
| 58 | + | </body> | |
| 59 | + | </html>`; | |
| 60 | + | return new Response(html, { | |
| 61 | + | headers: { "content-type": "text/html; charset=utf-8" }, | |
| 62 | + | }); | |
| 63 | + | } |
| 1 | − | import { NavLink, data } from "react-router"; | |
| 1 | + | import { ArrowLeft, ArrowRight, ExternalLink } from "lucide-react"; | |
| 2 | + | import { Link, NavLink, data } from "react-router"; | |
| 2 | 3 | ||
| 3 | 4 | import type { Route } from "./+types/docs"; | |
| 4 | 5 | import { Markdown } from "../components/markdown"; | |
| 9 | 10 | eager: true, | |
| 10 | 11 | }); | |
| 11 | 12 | ||
| 12 | − | const PAGES: { slug: string; title: string }[] = [ | |
| 13 | − | { slug: "", title: "Getting started" }, | |
| 14 | − | { slug: "concepts", title: "Concepts" }, | |
| 15 | − | { slug: "git", title: "Git" }, | |
| 16 | − | { slug: "agents", title: "Connect an agent" }, | |
| 17 | − | { slug: "api", title: "API" }, | |
| 13 | + | type Page = { slug: string; title: string }; | |
| 14 | + | ||
| 15 | + | const SECTIONS: { title: string; pages: Page[] }[] = [ | |
| 16 | + | { | |
| 17 | + | title: "Get started", | |
| 18 | + | pages: [ | |
| 19 | + | { slug: "", title: "Quickstart" }, | |
| 20 | + | { slug: "concepts", title: "Concepts" }, | |
| 21 | + | ], | |
| 22 | + | }, | |
| 23 | + | { | |
| 24 | + | title: "Guides", | |
| 25 | + | pages: [ | |
| 26 | + | { slug: "authentication", title: "Accounts and authentication" }, | |
| 27 | + | { slug: "git", title: "Git" }, | |
| 28 | + | { slug: "g1t-agents", title: "g1t agents" }, | |
| 29 | + | { slug: "agents", title: "Bring your own agent" }, | |
| 30 | + | ], | |
| 31 | + | }, | |
| 32 | + | { | |
| 33 | + | title: "Reference", | |
| 34 | + | pages: [{ slug: "api", title: "API overview" }], | |
| 35 | + | }, | |
| 18 | 36 | ]; | |
| 19 | 37 | ||
| 38 | + | const PAGES = SECTIONS.flatMap((section) => section.pages); | |
| 39 | + | ||
| 40 | + | function href(page: Page): string { | |
| 41 | + | return page.slug ? `/docs/${page.slug}` : "/docs"; | |
| 42 | + | } | |
| 43 | + | ||
| 20 | 44 | export function meta({ loaderData }: Route.MetaArgs) { | |
| 21 | − | return [{ title: `${loaderData?.title ?? "Docs"} · g1t docs` }]; | |
| 45 | + | return [ | |
| 46 | + | { title: `${loaderData?.title ?? "Docs"} · g1t docs` }, | |
| 47 | + | { | |
| 48 | + | tagName: "link", | |
| 49 | + | rel: "canonical", | |
| 50 | + | href: `https://docs.g1t.sh/${loaderData?.slug ?? ""}`, | |
| 51 | + | }, | |
| 52 | + | ]; | |
| 22 | 53 | } | |
| 23 | 54 | ||
| 24 | 55 | export function loader({ params }: Route.LoaderArgs) { | |
| 25 | 56 | const slug = params.page ?? ""; | |
| 26 | − | const page = PAGES.find((candidate) => candidate.slug === slug); | |
| 57 | + | const index = PAGES.findIndex((candidate) => candidate.slug === slug); | |
| 27 | 58 | const source = sources[`../docs/${slug || "index"}.md`]; | |
| 28 | − | if (!page || !source) throw data(null, { status: 404 }); | |
| 29 | − | return { title: page.title, source }; | |
| 59 | + | if (index < 0 || !source) throw data(null, { status: 404 }); | |
| 60 | + | return { | |
| 61 | + | slug, | |
| 62 | + | title: PAGES[index].title, | |
| 63 | + | source, | |
| 64 | + | previous: PAGES[index - 1] ?? null, | |
| 65 | + | next: PAGES[index + 1] ?? null, | |
| 66 | + | }; | |
| 30 | 67 | } | |
| 31 | 68 | ||
| 32 | 69 | export default function Docs({ loaderData }: Route.ComponentProps) { | |
| 70 | + | const { source, previous, next } = loaderData; | |
| 33 | 71 | return ( | |
| 34 | − | <div className="mx-auto grid max-w-6xl gap-10 px-4 py-10 md:grid-cols-[13rem_1fr]"> | |
| 35 | − | <nav aria-label="Documentation" className="md:sticky md:top-24 md:self-start"> | |
| 36 | − | <p className="px-3 text-xs font-medium tracking-wide text-faint uppercase"> | |
| 37 | − | Documentation | |
| 38 | − | </p> | |
| 39 | − | <ul className="mt-3 space-y-0.5"> | |
| 40 | − | {PAGES.map((page) => ( | |
| 41 | − | <li key={page.slug}> | |
| 42 | − | <NavLink | |
| 43 | − | to={page.slug ? `/docs/${page.slug}` : "/docs"} | |
| 44 | − | end | |
| 45 | − | className={({ isActive }) => | |
| 46 | − | `block rounded-md px-3 py-1.5 text-sm transition-colors ${ | |
| 47 | − | isActive | |
| 48 | − | ? "bg-raised font-medium text-fg" | |
| 49 | − | : "text-muted hover:text-fg" | |
| 50 | − | }` | |
| 51 | − | } | |
| 52 | − | > | |
| 53 | − | {page.title} | |
| 54 | − | </NavLink> | |
| 55 | − | </li> | |
| 56 | − | ))} | |
| 57 | − | </ul> | |
| 72 | + | <div className="mx-auto grid max-w-6xl gap-10 px-4 py-10 md:grid-cols-[14rem_1fr]"> | |
| 73 | + | <nav | |
| 74 | + | aria-label="Documentation" | |
| 75 | + | className="space-y-6 md:sticky md:top-24 md:self-start" | |
| 76 | + | > | |
| 77 | + | {SECTIONS.map((section) => ( | |
| 78 | + | <div key={section.title}> | |
| 79 | + | <p className="px-3 text-xs font-medium tracking-wide text-faint uppercase"> | |
| 80 | + | {section.title} | |
| 81 | + | </p> | |
| 82 | + | <ul className="mt-2 space-y-0.5"> | |
| 83 | + | {section.pages.map((page) => ( | |
| 84 | + | <li key={page.slug}> | |
| 85 | + | <NavLink | |
| 86 | + | to={href(page)} | |
| 87 | + | end | |
| 88 | + | className={({ isActive }) => | |
| 89 | + | `block rounded-md px-3 py-1.5 text-sm transition-colors ${ | |
| 90 | + | isActive | |
| 91 | + | ? "bg-raised font-medium text-fg" | |
| 92 | + | : "text-muted hover:text-fg" | |
| 93 | + | }` | |
| 94 | + | } | |
| 95 | + | > | |
| 96 | + | {page.title} | |
| 97 | + | </NavLink> | |
| 98 | + | </li> | |
| 99 | + | ))} | |
| 100 | + | {section.title === "Reference" && ( | |
| 101 | + | <> | |
| 102 | + | <li> | |
| 103 | + | <a | |
| 104 | + | href="/docs/api/reference" | |
| 105 | + | className="flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm text-muted hover:text-fg" | |
| 106 | + | > | |
| 107 | + | API reference | |
| 108 | + | <ExternalLink size={12} /> | |
| 109 | + | </a> | |
| 110 | + | </li> | |
| 111 | + | <li> | |
| 112 | + | <a | |
| 113 | + | href="/llms.txt" | |
| 114 | + | className="flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm text-muted hover:text-fg" | |
| 115 | + | > | |
| 116 | + | llms.txt | |
| 117 | + | <ExternalLink size={12} /> | |
| 118 | + | </a> | |
| 119 | + | </li> | |
| 120 | + | </> | |
| 121 | + | )} | |
| 122 | + | </ul> | |
| 123 | + | </div> | |
| 124 | + | ))} | |
| 58 | 125 | </nav> | |
| 59 | 126 | <article className="max-w-3xl min-w-0"> | |
| 60 | − | <Markdown source={loaderData.source} /> | |
| 127 | + | <Markdown source={source} /> | |
| 128 | + | <nav className="mt-16 grid gap-3 border-t border-line pt-6 sm:grid-cols-2"> | |
| 129 | + | {previous ? ( | |
| 130 | + | <Link | |
| 131 | + | to={href(previous)} | |
| 132 | + | className="rounded-xl border border-line p-4 transition-colors hover:border-line-strong" | |
| 133 | + | > | |
| 134 | + | <span className="flex items-center gap-1.5 text-xs text-faint"> | |
| 135 | + | <ArrowLeft size={12} /> Previous | |
| 136 | + | </span> | |
| 137 | + | <span className="mt-1 block font-medium">{previous.title}</span> | |
| 138 | + | </Link> | |
| 139 | + | ) : ( | |
| 140 | + | <span /> | |
| 141 | + | )} | |
| 142 | + | {next && ( | |
| 143 | + | <Link | |
| 144 | + | to={href(next)} | |
| 145 | + | className="rounded-xl border border-line p-4 text-right transition-colors hover:border-line-strong" | |
| 146 | + | > | |
| 147 | + | <span className="flex items-center justify-end gap-1.5 text-xs text-faint"> | |
| 148 | + | Next <ArrowRight size={12} /> | |
| 149 | + | </span> | |
| 150 | + | <span className="mt-1 block font-medium">{next.title}</span> | |
| 151 | + | </Link> | |
| 152 | + | )} | |
| 153 | + | </nav> | |
| 61 | 154 | </article> | |
| 62 | 155 | </div> | |
| 63 | 156 | ); |
| 6 | 6 | ); | |
| 7 | 7 | ||
| 8 | 8 | const GIT_PATH = /\/(info\/refs|git-upload-pack|git-receive-pack)$/; | |
| 9 | + | const DOCS_HOST = "docs.g1t.sh"; | |
| 10 | + | const SITE = "https://g1t.sh"; | |
| 11 | + | ||
| 12 | + | /** | |
| 13 | + | * docs.g1t.sh serves the site's /docs pages from its root: /concepts there | |
| 14 | + | * is /docs/concepts here. Anything on that hostname that is not | |
| 15 | + | * documentation belongs to the main site. | |
| 16 | + | */ | |
| 17 | + | function docsRequest(request: Request, url: URL): Request | Response { | |
| 18 | + | const { pathname } = url; | |
| 19 | + | if (pathname === "/docs" || pathname.startsWith("/docs/")) return request; | |
| 20 | + | // React Router's data requests for in-site navigation. | |
| 21 | + | if (pathname.startsWith("/__manifest") || pathname.endsWith(".data")) return request; | |
| 22 | + | const first = pathname.split("/")[1]; | |
| 23 | + | if (DOCS_PAGES.has(first)) { | |
| 24 | + | url.pathname = "/docs" + (pathname === "/" ? "" : pathname); | |
| 25 | + | return new Request(url, request); | |
| 26 | + | } | |
| 27 | + | return Response.redirect(SITE + pathname + url.search, 302); | |
| 28 | + | } | |
| 9 | 29 | ||
| 30 | + | /** First path segments that are documentation; "" is the docs home page. */ | |
| 31 | + | const DOCS_PAGES = new Set([ | |
| 32 | + | "", | |
| 33 | + | "concepts", | |
| 34 | + | "authentication", | |
| 35 | + | "git", | |
| 36 | + | "g1t-agents", | |
| 37 | + | "agents", | |
| 38 | + | "api", | |
| 39 | + | ]); | |
| 40 | + | ||
| 10 | 41 | export default { | |
| 11 | 42 | async fetch(request, env) { | |
| 43 | + | const url = new URL(request.url); | |
| 12 | 44 | // Git over HTTPS shares this hostname but belongs to the repos service. | |
| 13 | − | if (GIT_PATH.test(new URL(request.url).pathname)) { | |
| 45 | + | if (GIT_PATH.test(url.pathname)) { | |
| 14 | 46 | return env.REPOS.fetch(request); | |
| 15 | 47 | } | |
| 48 | + | if (url.hostname === DOCS_HOST) { | |
| 49 | + | const rewritten = docsRequest(request, url); | |
| 50 | + | if (rewritten instanceof Response) return rewritten; | |
| 51 | + | return requestHandler(rewritten); | |
| 52 | + | } | |
| 16 | 53 | return requestHandler(request); | |
| 17 | 54 | }, | |
| 18 | 55 | } satisfies ExportedHandler<Env>; |
| 4 | 4 | "account_id": "1e6f2cffa3f445920836e8ebe446bb58", | |
| 5 | 5 | "compatibility_date": "2026-09-26", | |
| 6 | 6 | "main": "./workers/app.ts", | |
| 7 | − | "routes": [{ "pattern": "g1t.sh", "custom_domain": true }], | |
| 7 | + | "routes": [ | |
| 8 | + | { "pattern": "g1t.sh", "custom_domain": true }, | |
| 9 | + | // Serves the /docs pages at the root of their own hostname. | |
| 10 | + | { "pattern": "docs.g1t.sh", "custom_domain": true } | |
| 11 | + | ], | |
| 8 | 12 | // The site holds no data of its own; everything goes through services. | |
| 9 | 13 | "services": [ | |
| 10 | 14 | { "binding": "IDENTITY", "service": "g1t-identity" }, |