Commit

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

syntaqxcommitted Parent54c2401Browse files
16 files+748−730/16 viewed
+31−1
11 import { Hono } from "hono";
2+import { cors } from "hono/cors";
23
34 import {
45 type ServiceBinding,
910 } from "@g1t/contracts";
1011
1112 import { handleMcp } from "./mcp";
13+import { openApiDocument } from "./openapi";
1214 import { type ApiEnv, operations, operationsByName } from "./operations";
1315
1416 type Input = Record<string, unknown>;
4446 { method: "POST", path: "/v1/attempts/:attempt_id/submit", operation: "submit_attempt", input: (p, _q, b) => ({ ...b, ...p }) },
4547 { method: "POST", path: "/v1/attempts/:attempt_id/abandon", operation: "abandon_attempt", input: (p) => p },
4648 { 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 },
4750 ];
4851
4952 function repo(params: Record<string, string>): Input {
5053 return { repo: `${params.owner}/${params.name}` };
5154 }
5255
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+
5365 const app = new Hono<App>();
5466
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+
5571 // `Authorization: Bearer g1t_…`. A missing token is an anonymous viewer; a
5672 // wrong one is rejected so a typo does not silently look signed out.
5773 app.use(async (c, next) => {
131147 return c.json({ token: created.token, verified: user.verified === true }, 201);
132148 });
133149
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+
134163 app.get("/", (c) =>
135164 c.json({
136165 name: "g1t API",
137166 version: "v1",
138− documentation: "https://g1t.sh/syntaqx/g1t",
167+ documentation: "https://docs.g1t.sh/api",
168+ openapi: "https://api.g1t.sh/openapi.json",
139169 operations: operations.map(({ name, description }) => ({ name, description })),
140170 }),
141171 );
+232−0
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+}
+16−0
306306 ),
307307 },
308308 {
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+ {
309325 name: "ship_attempt",
310326 description:
311327 "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.",
+23−6
1717 };
1818
1919 /** `+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+}) {
2127 const total = additions + deletions;
2228 const green = total === 0 ? 0 : Math.round((additions / total) * 5);
2329 return (
2834 {Array.from({ length: 5 }, (_, i) => (
2935 <span
3036 key={i}
31− className={`size-2 rounded-[2px] ${
37+ className={`size-2 rounded-xs ${
3238 total === 0 ? "bg-line" : i < green ? "bg-accent" : "bg-danger"
3339 }`}
3440 />
4046
4147 function File({ file }: { file: FileDiff }) {
4248 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;
4454 return (
4555 <section
4656 id={`file-${file.path}`}
8898 <>
8999 {!first && (
90100 <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+ >
92105 ⋯
93106 </td>
94107 </tr>
95108 )}
96109 {lines.map((line, index) => (
97110 <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>
100117 <td
101118 className={`w-5 text-center select-none ${
102119 line.kind === "add"
+52−20
5454 }
5555
5656 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+ ],
6177 ];
6278
6379 const PLATFORM = ["Workers", "Artifacts", "D1", "Queues", "Rust"];
6985 <section className="relative overflow-hidden border-b border-line">
7086 <div
7187 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)]"
7389 />
7490 <div className="relative mx-auto max-w-6xl px-4 pt-20 pb-10 text-center">
7591 <Link
86102 <span className="text-accent">AI scale.</span>
87103 </h1>
88104 <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.
93108 </p>
94109 <div className="mt-8 flex animate-fade-up flex-wrap items-center justify-center gap-3">
95110 <ButtonLink to="/register" variant="accent" large>
110125 <section className="mx-auto max-w-6xl px-4 py-24">
111126 <p className="text-sm font-medium text-accent">How it works</p>
112127 <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.
115130 </h2>
116131 <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.
120138 </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.
124145 </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+ >
126150 Prompts, reasoning and tool calls are recorded against the attempt
127151 and the commit they produced, so you can see why, not only what.
128152 </FeatureCard>
129− <FeatureCard title="Bring any agent" illustration={<AgentsIllustration />} wide>
153+ <FeatureCard
154+ title="Bring any agent"
155+ illustration={<AgentsIllustration />}
156+ wide
157+ >
130158 Claude Code and other MCP clients connect to mcp.g1t.sh with one
131159 command. Everything is also a plain REST call at api.g1t.sh.
132160 </FeatureCard>
133− <FeatureCard title="Converge on main" illustration={<ShipIllustration />} wide>
161+ <FeatureCard
162+ title="Converge on main"
163+ illustration={<ShipIllustration />}
164+ wide
165+ >
134166 However many attempts are in flight, changes reach main one at a
135167 time and in order. An attempt that has fallen behind is told, and
136168 catches up before it lands.
+12−0
1+import type { ReactNode } from "react";
12 import ReactMarkdown from "react-markdown";
23 import { Link } from "react-router";
34 import remarkGfm from "remark-gfm";
45
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+
515 /**
616 * Renders markdown from repositories, briefs and docs. Raw HTML in the
717 * source is not rendered, so untrusted content is safe to pass in.
1222 <ReactMarkdown
1323 remarkPlugins={[remarkGfm]}
1424 components={{
25+ h2: ({ children }) => <h2 id={slug(children)}>{children}</h2>,
26+ h3: ({ children }) => <h3 id={slug(children)}>{children}</h3>,
1527 a({ href, children }) {
1628 // In-site links navigate on the client.
1729 return href?.startsWith("/") ? (
+1−0
4545 | `read_session` | Read an attempt's recorded session. |
4646 | `submit_attempt` | Mark an attempt finished, with a summary. |
4747 | `abandon_attempt` | Give up on an attempt. |
48+| `get_attempt_changes` | The files an attempt changed, with line-by-line diffs. |
4849 | `ship_attempt` | Land an attempt on `main` and close its intent. Repository owner only. |
4950 | `list_events` | A repository's timeline, newest first. |
5051
+6−0
33 The REST API lives at `https://api.g1t.sh`. It exposes the same operations as
44 the [MCP server](/docs/agents).
55
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+
611 ## Authentication
712
813 Send an [access token](/settings) as a bearer token:
7782 | `GET` | `/v1/attempts/{attempt_id}` | An attempt and its intent. |
7883 | `POST` | `/v1/attempts/{attempt_id}/submit` | Finish. Body: `summary`. |
7984 | `POST` | `/v1/attempts/{attempt_id}/abandon` | Give up. |
85+| `GET` | `/v1/attempts/{attempt_id}/changes` | The files it changed, with diffs. |
8086 | `POST` | `/v1/attempts/{attempt_id}/ship` | Land it on `main`. Owner only; `409` if `main` has moved. |
8187
8288 Starting an attempt returns the fork's git remote:
+57−0
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.
+57−0
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.
+25−8
179179 {
180180 title: "Product",
181181 links: [
182− ["Explore", "/explore"],
182+ ["Explore repositories", "/explore"],
183+ ["g1t agents", "/docs/g1t-agents"],
184+ ["Bring your own agent", "/docs/agents"],
183185 ["Sign up", "/register"],
184− ["Source", "/syntaqx/g1t"],
185186 ],
186187 },
187188 {
188− title: "Docs",
189+ title: "Developers",
189190 links: [
190− ["Getting started", "/docs"],
191+ ["Quickstart", "/docs"],
191192 ["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"],
194196 ],
195197 },
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+ },
196206 ];
197207
198208 function Footer() {
212222 <ul className="mt-3 space-y-2 text-sm text-muted">
213223 {group.links.map(([label, to]) => (
214224 <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">
216227 {label}
217− </Link>
228+ </a>
218229 </li>
219230 ))}
220231 </ul>
221232 </div>
222233 ))}
223234 </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>
224241 </footer>
225242 );
226243 }
+1−0
1212 route("settings", "routes/settings.tsx"),
1313 route("explore", "routes/explore.tsx", { id: "explore" }),
1414 route("search", "routes/explore.tsx", { id: "search" }),
15+ route("docs/api/reference", "routes/api-reference.tsx"),
1516 route("docs/:page?", "routes/docs.tsx"),
1617 route(":owner", "routes/profile.tsx"),
1718 route(":owner/:repo", "routes/repo/layout.tsx", [
+63−0
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+}
+129−36
1−import { NavLink, data } from "react-router";
1+import { ArrowLeft, ArrowRight, ExternalLink } from "lucide-react";
2+import { Link, NavLink, data } from "react-router";
23
34 import type { Route } from "./+types/docs";
45 import { Markdown } from "../components/markdown";
910 eager: true,
1011 });
1112
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+ },
1836 ];
1937
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+
2044 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+ ];
2253 }
2354
2455 export function loader({ params }: Route.LoaderArgs) {
2556 const slug = params.page ?? "";
26− const page = PAGES.find((candidate) => candidate.slug === slug);
57+ const index = PAGES.findIndex((candidate) => candidate.slug === slug);
2758 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+ };
3067 }
3168
3269 export default function Docs({ loaderData }: Route.ComponentProps) {
70+ const { source, previous, next } = loaderData;
3371 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+ ))}
58125 </nav>
59126 <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>
61154 </article>
62155 </div>
63156 );
+38−1
66 );
77
88 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+}
929
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+
1041 export default {
1142 async fetch(request, env) {
43+ const url = new URL(request.url);
1244 // 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)) {
1446 return env.REPOS.fetch(request);
1547 }
48+ if (url.hostname === DOCS_HOST) {
49+ const rewritten = docsRequest(request, url);
50+ if (rewritten instanceof Response) return rewritten;
51+ return requestHandler(rewritten);
52+ }
1653 return requestHandler(request);
1754 },
1855 } satisfies ExportedHandler<Env>;
+5−1
44 "account_id": "1e6f2cffa3f445920836e8ebe446bb58",
55 "compatibility_date": "2026-09-26",
66 "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+ ],
812 // The site holds no data of its own; everything goes through services.
913 "services": [
1014 { "binding": "IDENTITY", "service": "g1t-identity" },