Skip to content
962 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Sidebar: the panels really slide1import type { ActionsApi } from "./actions";
Billing accounts, terms and enterprises; g1t is no longer free2import type { BillingAdminApi, BillingApi } from "./billing";
Deployments: a preview for every pull request, production on g1t.page3import type { DeploymentsApi } from "./deployments";
Projects: what a workspace builds and runs, first on every page4import type { ProjectsApi } from "./projects";
Sidebar: the panels really slide5import type { EventsApi } from "./events";
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace6import type { IdentityAdminApi, IdentityApi } from "./identity";
Sidebar: the panels really slide7import type { IntegrationsApi } from "./integrations";
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member8import type { PackagesApi } from "./packages";
Sidebar: the panels really slide9import type { WebhooksApi } from "./webhooks";
10import type { ReposApi } from "./repos";
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar11import type { PullDetail, WorkApi } from "./work";
12import type { Result } from "./result";
Fast pages, required checks on the branch, self-hosted runners, honest incidents13import type { RunnersApi } from "./runners";
Sidebar: the panels really slide14
15/** A service binding, as far as these clients need it. */
16export type ServiceBinding = {
17 fetch(input: string, init?: RequestInit): Promise<Response>;
18};
19
20/**
21 * Calls a method on a service that speaks the JSON protocol used by the
22 * Rust services: `POST /rpc/<method>` with the arguments as the body.
23 */
24async function rpc<T>(
25 service: ServiceBinding,
26 method: string,
27 args: object,
28): Promise<T> {
29 // The hostname is ignored; a service binding always reaches its service.
30 const response = await service.fetch(`https://service/rpc/${method}`, {
31 method: "POST",
32 headers: { "content-type": "application/json" },
33 body: JSON.stringify(args),
34 });
35 if (!response.ok) {
36 throw new Error(`${method} failed with status ${response.status}`);
37 }
38 return (await response.json()) as T;
39}
40
41export function identityClient(service: ServiceBinding): IdentityApi {
42 const call = <T>(method: string, args: object) => rpc<T>(service, method, args);
43 return {
Merge invite emails that confirm the address: the emailed link carries a proof only that email has, so signing up from it needs no code; shared links and typed codes still confirm44 register: (username, email, password, inviteCode, client, emailProof) =>
45 call("register", {
46 username,
47 email,
48 password,
49 invite_code: inviteCode ?? null,
50 email_proof: emailProof ?? null,
51 client: client ?? null,
52 }),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look53 signIn: (username, password, client) => call("sign_in", { username, password, client: client ?? null }),
Merge main (membership, two-factor, GitHub repo roles) into tokens54 twoFactorSignIn: (challenge, code, client) => call("two_factor_sign_in", { challenge, code, client: client ?? null }),
Sidebar: the panels really slide55 signOut: (sessionToken) => call("sign_out", { sessionToken }),
56 resendVerification: (user) => call("resend_verification", { user }),
57 verifyEmail: (token) => call("verify_email", { token }),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look58 requestPasswordReset: (email, client) => call("request_password_reset", { email, client: client ?? null }),
Sidebar: the panels really slide59 resetPassword: (token, password) =>
60 call("reset_password", { token, password }),
61 deviceStart: (clientName) => call("device_start", { clientName }),
62 deviceLookup: (userCode) => call("device_lookup", { userCode }),
63 deviceResolve: (userCode, user, approve) =>
64 call("device_resolve", { userCode, user, approve }),
65 deviceClaim: (deviceCode) => call("device_claim", { deviceCode }),
66 oauthAuthorize: (user, approval) => call("oauth_authorize", { user, ...approval }),
67 oauthExchange: (code, codeVerifier, clientId, redirectUri) =>
68 call("oauth_exchange", { code, codeVerifier, clientId, redirectUri }),
69 oauthRefresh: (refreshToken, clientId) =>
70 call("oauth_refresh", { refreshToken, clientId }),
71 listOAuthGrants: (user) => call("list_oauth_grants", { user }),
72 revokeOAuthGrant: (user, id) => call("revoke_oauth_grant", { user, id }),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step73 updateOAuthGrant: (user, id, grant) =>
74 call("update_oauth_grant", { user, id, scopes: grant.scopes }),
Sidebar: the panels really slide75 createWorkspace: (user, slug, name) => call("create_workspace", { user, slug, name }),
76 getWorkspace: (slug) => call("get_workspace", { slug }),
Merge branch 'worktree-agent-a2013627e5ea4ab13'77 workspaceResidency: (slug) => call("workspace_residency", { slug }),
78 setWorkspaceResidency: (actor, slug, residency) => call("set_workspace_residency", { actor, slug, residency }),
Sidebar: the panels really slide79 listMembers: (slug, viewer) => call("list_members", { slug, viewer }),
80 addMember: (actor, slug, username) => call("add_member", { actor, slug, username }),
81 removeMember: (actor, slug, username) =>
Merge main (membership, two-factor, GitHub repo roles) into tokens82 call("remove_member", { actor, slug, username, surface: "web" }),
83 updateMember: (actor, slug, username, change) =>
84 call("update_member", { actor, slug, username, role: change.role ?? null, org_roles: change.org_roles ?? null, surface: "web" }),
85 transferOwnership: (actor, slug, username) => call("transfer_ownership", { actor, slug, username, surface: "web" }),
86 leaveWorkspace: (user, slug) => call("leave_workspace", { user, slug, surface: "web" }),
87 setMemberPrivileges: (actor, slug, change) => call("set_member_privileges", { actor, slug, privileges: change, surface: "web" }),
88 setTwoFactorRequirement: (actor, slug, required) =>
89 call("set_two_factor_requirement", { actor, slug, required, surface: "web" }),
Sidebar: the panels really slide90 updateWorkspace: (actor, slug, details) =>
91 call("update_workspace", { actor, slug, ...details }),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains92 renameWorkspace: (actor, slug, newSlug) => call("rename_workspace", { actor, slug, newSlug }),
93 checkWorkspaceRename: (actor, slug, newSlug) =>
94 call("check_workspace_rename", { actor, slug, newSlug }),
95 resolveSlug: (slug) => call("resolve_slug", { slug }),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look96 deleteWorkspace: (actor, slug, confirm) => call("delete_workspace", { actor, slug, confirm }),
97 checkWorkspaceDeletion: (actor, slug) => call("check_workspace_deletion", { actor, slug }),
Workspace names and icons, and a component kit for every control98 setWorkspaceAvatar: (actor, slug, image) => call("set_workspace_avatar", { actor, slug, image }),
99 setUserAvatar: (user, image) => call("set_user_avatar", { user, image }),
Sidebar: the panels really slide100 listWorkspaceTokens: (slug, viewer) => call("list_workspace_tokens", { slug, viewer }),
101 removeWorkspaceToken: (actor, slug, id) =>
102 call("remove_workspace_token", { actor, slug, id }),
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers103 createToken: (actor, input) =>
104 call("create_token", {
105 actor,
106 owner: input.owner ?? null,
Settings: fine-grained tokens, workspace token Admin, workspace personal access token rules107 name: input.name,
108 description: input.description ?? null,
109 ttl_seconds: input.ttlSeconds,
110 workspace: input.workspace,
111 repository_selection: input.repositorySelection,
112 repositories: input.repositories,
113 permissions: input.permissions,
Merge main into Artifacts Phase 2114 website: input.website ?? false,
Settings: fine-grained tokens, workspace token Admin, workspace personal access token rules115 }),
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers116 updateToken: (actor, id, change, owner) =>
117 call("update_token", {
118 actor,
Settings: fine-grained tokens, workspace token Admin, workspace personal access token rules119 id,
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers120 owner: owner ?? null,
Settings: fine-grained tokens, workspace token Admin, workspace personal access token rules121 name: change.name ?? null,
122 description: change.description ?? null,
123 repository_selection: change.repositorySelection ?? null,
124 repositories: change.repositories ?? null,
125 permissions: change.permissions ?? null,
Merge main into Artifacts Phase 2126 website: change.website ?? null,
Settings: fine-grained tokens, workspace token Admin, workspace personal access token rules127 }),
128 getTokenPolicy: (slug, viewer) => call("get_token_policy", { slug, viewer }),
129 setTokenPolicy: (actor, slug, change) =>
130 call("set_token_policy", {
131 actor,
132 slug,
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers133 allow_tokens_for_all_workspaces: change.allowTokensForAllWorkspaces ?? null,
134 allow_tokens_for_this_workspace: change.allowTokensForThisWorkspace ?? null,
Settings: fine-grained tokens, workspace token Admin, workspace personal access token rules135 require_approval: change.requireApproval ?? null,
136 max_lifetime_days: change.maxLifetimeDays ?? null,
137 forbid_no_expiry: change.forbidNoExpiry ?? null,
138 surface: "web",
139 }),
140 listMemberTokens: (actor, slug, filter = {}) =>
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers141 call("list_member_tokens", { actor, slug, status: filter.status ?? null }),
Settings: fine-grained tokens, workspace token Admin, workspace personal access token rules142 reviewTokenRequest: (actor, slug, id, approve, reason) =>
143 call("review_token_request", { actor, slug, id, approve, reason: reason ?? null, surface: "web" }),
144 revokeMemberToken: (actor, slug, id, reason) =>
145 call("revoke_member_token", { actor, slug, id, reason: reason ?? null, surface: "web" }),
Sidebar: the panels really slide146 userForSession: (sessionToken) => call("user_for_session", { sessionToken }),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look147 registration: () => call("registration", {}),
148 listInvites: (user) => call("list_invites", { user }),
149 createInvite: (user, options = {}) =>
Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page)150 call("create_invite", { user, email: options.email ?? null, workspace: options.workspace ?? null, join: options.join ?? null, join_role: options.join ? (options.joinRole ?? null) : null }),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look151 revokeInvite: (user, id) => call("revoke_invite", { user, id }),
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas152 checkInvite: (code, client, options = {}) =>
153 call("check_invite", {
154 code,
155 client: client ?? null,
156 viewer: options.viewer ?? null,
157 any_status: options.anyStatus ?? false,
Merge invite emails that confirm the address: the emailed link carries a proof only that email has, so signing up from it needs no code; shared links and typed codes still confirm158 email_proof: options.emailProof ?? null,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas159 }),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look160 acceptInvite: (user, code) => call("accept_invite", { user, code }),
Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040)161 inviteMember: (actor, slug, who) =>
162 call("invite_member", { actor, slug, email: who.email ?? "", username: who.username ?? null, role: who.role ?? null }),
163 listInvitations: (user) => call("list_invitations", { user }),
164 acceptInvitation: (user, id) => call("accept_invitation", { user, id, surface: "web" }),
165 declineInvitation: (user, id) => call("decline_invitation", { user, id, surface: "web" }),
166 findPeople: (query, limit) => call("find_people", { query, limit: limit ?? null }),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look167 workspaceInvites: (slug, viewer) => call("workspace_invites", { slug, viewer }),
168 revokeWorkspaceInvite: (actor, slug, id) => call("revoke_workspace_invite", { actor, slug, id }),
169 requestAccess: (email, about, client) => call("request_access", { email, about, client: client ?? null }),
Sidebar: the panels really slide170 userForGitCredentials: (username, secret) =>
171 call("user_for_git_credentials", { username, secret }),
172 userForAccessToken: (token) => call("user_for_access_token", { token }),
173 userForSshKey: (fingerprint) => call("user_for_ssh_key", { fingerprint }),
174 userByUsername: (username) => call("user_by_username", { username }),
175 usernames: (ids) => call("usernames", { ids }),
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)176 usersForAudience: (ids) => call("users_for_audience", { ids }),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains177 profile: (username) => call("profile", { username }),
178 updateProfile: (actor, fields) => call("update_profile", { actor, ...fields }),
179 profileWorkspaces: (username, viewer, publicIn) =>
180 call("profile_workspaces", { username, viewer, public: publicIn }),
The apps you pin to your dock are kept with your account, per workspace and in your order, so the dock is the same on every device: identity keeps them in dock_pins and answers dock_pins and set_dock_pins, the dock reads them with the rest of the page, pins kept only on this device carry over with your first change, this device's copy still draws the dock when identity can't be reached, a pin that can't be saved says so, and they go when you or the workspace do; the workspaces guide says how.181 dockPins: (user, workspace) => call("dock_pins", { user, workspace }),
182 setDockPins: (user, workspace, apps) => call("set_dock_pins", { user, workspace, apps }),
Sidebar: the panels really slide183 listSshKeys: (user) => call("list_ssh_keys", { user }),
184 addSshKey: (user, title, publicKey) =>
185 call("add_ssh_key", { user, title, publicKey }),
186 removeSshKey: (user, id) => call("remove_ssh_key", { user, id }),
187 listAccessTokens: (user) => call("list_access_tokens", { user }),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step188 createAccessToken: (user, name, ttlSeconds, grant) =>
189 call("create_access_token", {
190 user,
191 name,
192 ttlSeconds,
193 scopes: grant?.scopes ?? null,
194 listed: grant?.listed ?? false,
195 }),
Sidebar: the panels really slide196 createAgentToken: (onBehalfOf, scope, ttlSeconds) =>
197 call("create_agent_token", { onBehalfOf, scope, ttlSeconds }),
198 removeAccessToken: (user, id) => call("remove_access_token", { user, id }),
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API199 createRunCredential: (input) => call("create_run_credential", input),
200 bindRunCredentials: (tokenHashes, runId) => call("bind_run_credentials", { tokenHashes, runId }),
201 revokeRunCredentials: (target) => call("revoke_run_credentials", target),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look202 // Who has access to a repository; see access.ts.
203 repoAccess: (owner, name, viewer) => call("repo_access", { viewer, path: { namespace: owner, name } }),
204 addCollaborator: (actor, owner, name, invitee, role) =>
205 call("add_collaborator", { actor, path: { namespace: owner, name }, invitee, role }),
206 setCollaboratorRole: (actor, owner, name, username, role) =>
207 call("set_collaborator_role", { actor, path: { namespace: owner, name }, username, role }),
208 removeCollaborator: (actor, owner, name, username) =>
209 call("remove_collaborator", { actor, path: { namespace: owner, name }, username }),
210 collaboratorPermission: (viewer, owner, name, username) =>
211 call("collaborator_permission", { viewer, path: { namespace: owner, name }, username }),
212 myRepoInvitations: (user) => call("my_repo_invitations", { user }),
213 respondRepoInvitation: (user, id, accept) => call("respond_repo_invitation", { user, id, accept }),
214 revokeRepoInvitation: (actor, owner, name, id) =>
215 call("revoke_repo_invitation", { actor, path: { namespace: owner, name }, id }),
216 setBasePermission: (actor, slug, base) => call("set_base_permission", { actor, slug, base_permission: base }),
217 outsideCollaborators: (viewer, slug) => call("outside_collaborators", { viewer, slug }),
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca218 // A repository's deploy keys; see deploy-keys.ts.
219 listDeployKeys: (viewer, owner, name) => call("list_deploy_keys", { viewer, path: { namespace: owner, name } }),
220 addDeployKey: (actor, owner, name, key) =>
221 call("add_deploy_key", { actor, path: { namespace: owner, name }, title: key.title, key: key.key, read_only: key.readOnly }),
222 removeDeployKey: (actor, owner, name, id) => call("remove_deploy_key", { actor, path: { namespace: owner, name }, id }),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar223 // Teams; see teams.ts.
224 listTeams: (viewer, workspace, query) => call("list_teams", { viewer, workspace, query: query ?? null }),
225 getTeam: (viewer, workspace, team) => call("get_team", { viewer, workspace, team }),
226 createTeam: (actor, workspace, team) => call("create_team", { actor, workspace, ...team }),
Merge branch 'worktree-agent-ad7c6d88d93adc817'227 setTeamCreation: (actor, slug, setting) => call("set_team_creation", { actor, slug, team_creation: setting }),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar228 updateTeam: (actor, workspace, team, changes) => call("update_team", { actor, workspace, team, ...changes }),
229 deleteTeam: (actor, workspace, team) => call("delete_team", { actor, workspace, team }),
230 teamMembers: (viewer, workspace, team, includeChildTeams) =>
231 call("team_members", { viewer, workspace, team, include_child_teams: includeChildTeams ?? false }),
232 setTeamMember: (actor, workspace, team, username, role) =>
233 call("set_team_member", { actor, workspace, team, username, role }),
234 removeTeamMember: (actor, workspace, team, username) =>
235 call("remove_team_member", { actor, workspace, team, username }),
236 childTeams: (viewer, workspace, team) => call("child_teams", { viewer, workspace, team }),
237 teamRepos: (viewer, workspace, team) => call("team_repos", { viewer, workspace, team }),
238 setTeamRepo: (actor, workspace, team, owner, name, role) =>
239 call("set_team_repo", { actor, workspace, team, repo: { namespace: owner, name }, role }),
240 removeTeamRepo: (actor, workspace, team, owner, name) =>
241 call("remove_team_repo", { actor, workspace, team, repo: { namespace: owner, name } }),
242 userTeams: (viewer, workspace, username) => call("user_teams", { viewer, workspace, username }),
243 teamMemberships: (viewer, workspace) => call("team_memberships", { viewer, workspace }),
People and teams are front and centre: one directory of people and agents with presence, local time, titles, teams and what each owns; profiles with manager and reports and the agents they work with; an org chart with each team's agents beside the person who leads it; and teams of any mix, with a lead, a channel, a budget agents keep to and the agents on them. Every agent is told its teams each turn (who leads, who owns what, who's around and who to page), and the team page shows exactly what. Member management is Members and invites; the people and teams guide says how.244 teamAgents: (viewer, workspace, team) => call("team_agents", { viewer, workspace, team }),
245 setTeamAgent: (actor, workspace, team, agentId) => call("set_team_agent", { actor, workspace, team, agent_id: agentId }),
246 removeTeamAgent: (actor, workspace, team, agentId) => call("remove_team_agent", { actor, workspace, team, agent_id: agentId }),
247 agentTeams: (workspace, agentId, homeTeam) => call("agent_teams", { workspace, agent_id: agentId, home_team: homeTeam }),
248 // People; see people.ts.
249 peopleDirectory: (viewer, workspace) => call("people_directory", { viewer, workspace }),
250 setMemberProfile: (actor, workspace, username, changes) => call("set_member_profile", { actor, workspace, username, ...changes }),
Sidebar: the panels really slide251 };
252}
253
Agents and memory, checks and conflicts, profiles, slug renames, custom domains254/**
255 * The slug a renamed workspace has now, for a `workspace.renamed` handler:
256 * asked of identity by the workspace's id, so that renames delivered twice
257 * or out of order converge. Falls back to the event's `to`.
258 */
259export async function currentWorkspaceSlug(
260 identity: ServiceBinding,
261 renamed: { workspaceId: string; to: string },
262): Promise<string> {
263 const names = await identityClient(identity).usernames([renamed.workspaceId]);
264 return names[renamed.workspaceId] ?? renamed.to;
265}
266
267/** The slugs whose rows move to `current`: the two a rename names, less `current`. */
268export function staleSlugs(renamed: { from: string; to: string }, current: string): string[] {
269 return [...new Set([renamed.from, renamed.to])].filter((slug) => slug !== current);
270}
271
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look272/**
273 * Where a transferred repository is now, as `namespace/name`, for a
274 * `repo.transferred` handler: asked of repos by id, so transfers delivered
275 * twice or out of order converge. Falls back to the event's destination.
276 */
277export async function currentRepoPath(
278 repos: ServiceBinding,
279 transferred: { repoId: string; name: string; to: string },
280): Promise<string> {
281 const path = await rpc<{ namespace: string; name: string } | null>(repos, "path_by_id", { id: transferred.repoId });
282 return path ? `${path.namespace}/${path.name}` : `${transferred.to}/${transferred.name}`;
283}
284
285/** The paths whose rows move to `current`: the two a transfer names, less `current`. */
286export function stalePaths(transferred: { name: string; from: string; to: string }, current: string): string[] {
287 return [...new Set([transferred.from, transferred.to].map((ns) => `${ns}/${transferred.name}`))].filter(
288 (path) => path !== current,
289 );
290}
291
292/**
293 * A repository's path change, from `repo.transferred` or `repo.renamed`,
294 * read the same way: the two paths (`namespace/name`) the event names, old
295 * then new. Null for any other event.
296 */
297export type RepoMove = { repoId: string; paths: [string, string] };
298
299export function repoMove(event: { type: string; data: unknown }): RepoMove | null {
300 const data = event.data as Record<string, string>;
301 if (event.type === "repo.transferred") {
302 return { repoId: data.repoId!, paths: [`${data.from}/${data.name}`, `${data.to}/${data.name}`] };
303 }
304 if (event.type === "repo.renamed") {
305 return { repoId: data.repoId!, paths: [`${data.namespace}/${data.from}`, `${data.namespace}/${data.to}`] };
306 }
307 return null;
308}
309
310/**
311 * Where a moved repository is now, as `namespace/name`: asked of repos by
312 * id, so moves delivered twice or out of order converge. Falls back to the
313 * event's new path.
314 */
315export async function currentMovedPath(repos: ServiceBinding, move: RepoMove): Promise<string> {
316 const path = await rpc<{ namespace: string; name: string } | null>(repos, "path_by_id", { id: move.repoId });
317 return path ? `${path.namespace}/${path.name}` : move.paths[1];
318}
319
320/** The paths whose rows move to `current`: the two a move names, less `current`. */
321export function staleMovedPaths(move: RepoMove, current: string): string[] {
322 return [...new Set(move.paths)].filter((path) => path !== current);
323}
324
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace325/** Staff-only identity. Only sudo binds to it; see `IdentityAdminApi`. */
326export function identityAdminClient(service: ServiceBinding): IdentityAdminApi {
327 const call = <T>(method: string, args: object) => rpc<T>(service, method, args);
328 return {
329 workspaces: (query) => call("admin_workspaces", { query: query ?? null }),
330 workspace: (slug) => call("admin_workspace", { slug }),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look331 waitlist: (query, status) => call("admin_waitlist", { query: query ?? null, status: status ?? null }),
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas332 waitlistPending: () => call("admin_waitlist_pending", {}),
333 decideWaitlist: (id, approve, staff, note) => call("admin_decide_waitlist", { id, approve, staff, note: note ?? null }),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look334 invites: (query) => call("admin_invites", { query: query ?? null }),
335 revokeInvite: (id, staff) => call("admin_revoke_invite", { id, staff }),
336 mintInvite: (email, staff) => call("admin_mint_invite", { email, staff }),
337 grantInvites: (target, name, amount, note, staff) =>
338 call("admin_grant_invites", { target, name, amount, note, staff }),
339 inviteTree: (username) => call("admin_invite_tree", { username }),
340 workspaceInvites: (slug) => call("admin_workspace_invites", { slug }),
Merge shared invite links: label, uses, expiry, domains; joined through recorded (identity 0038)341 sharedInvites: () => call("admin_shared_invites", {}),
342 createSharedInvite: (link, staff) =>
343 call("admin_create_shared_invite", {
344 label: link.label,
345 max_uses: link.maxUses,
346 expires_on: link.expiresOn,
347 domains: link.domains,
348 staff,
349 }),
350 revokeSharedInvite: (id, staff) => call("admin_revoke_shared_invite", { id, staff }),
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member351 deletedWorkspaces: () => call("admin_deleted_workspaces", {}),
352 restoreWorkspace: (workspaceId, staff) => call("admin_restore_workspace", { workspaceId, staff }),
353 purgeWorkspace: (workspaceId, staff, confirm) => call("admin_purge_workspace", { workspaceId, staff, confirm }),
Merge branch 'worktree-agent-a8385d293d42c913a'354 aliases: () => call("admin_aliases", {}),
355 setAlias: (alias, workspace, note, staff) => call("admin_set_alias", { alias, workspace, note, staff }),
356 removeAlias: (alias, reason, staff) => call("admin_remove_alias", { alias, reason, staff }),
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace357 };
358}
359
Sidebar: the panels really slide360export function reposClient(service: ServiceBinding): ReposApi {
361 const call = <T>(method: string, args: object) => rpc<T>(service, method, args);
362 return {
363 get: (path, viewer) => call("get", { path, viewer }),
364 getById: (id, viewer) => call("get_by_id", { id, viewer }),
Fast pages, required checks on the branch, self-hosted runners, honest incidents365 readable: (ids, viewer) => call("readable", { ids, viewer }),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains366 publicNamespaces: (ownerId) => call("public_namespaces", { ownerId }),
Sidebar: the panels really slide367 list: (viewer, options = {}) => call("list", { viewer, ...options }),
368 create: (owner, input) => call("create", { owner, ...input }),
369 update: (actor, path, changes) => call("update", { actor, path, ...changes }),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look370 transfer: (actor, path, to) => call("transfer", { actor, path, to }),
371 delete: (actor, path, confirm) => call("delete", { actor, path, confirm }),
372 deleted: (viewer, namespace) => call("deleted", { viewer, namespace }),
373 restore: (actor, path) => call("restore", { actor, path }),
374 purge: (actor, path, confirm) => call("purge", { actor, path, confirm }),
375 rename: (actor, path, name) => call("rename", { actor, path, name }),
376 archive: (actor, path, archived) => call("archive", { actor, path, archived }),
377 setVisibility: (actor, path, isPrivate, confirm) => call("set_visibility", { actor, path, isPrivate, confirm }),
378 setDefaultBranch: (actor, path, branch) => call("set_default_branch", { actor, path, branch }),
379 renameBranch: (actor, path, from, to) => call("rename_branch", { actor, path, from, to }),
380 resolveBranch: (repoId, branch) => call("resolve_branch", { repoId, branch }),
381 statusById: (id) => call("status_by_id", { id }),
Merge branch 'worktree-agent-a2013627e5ea4ab13'382 storageOptions: () => call("storage_options", {}),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look383 resolvePath: (path) => call("resolve_path", { path }),
Sidebar: the panels really slide384 tree: (path, viewer, ref, treePath) =>
385 call("tree", { path, viewer, ref, treePath }),
386 blob: (path, viewer, ref, filePath) =>
387 call("blob", { path, viewer, ref, filePath }),
388 log: (path, viewer, ref, limit) => call("log", { path, viewer, ref, limit }),
389 blame: (path, viewer, ref, filePath) => call("blame", { path, viewer, ref, filePath }),
390 forkForPull: (sourceId, pullId, actor) =>
391 call("fork_for_pull", { sourceId, pullId, actor }),
392 gitAccess: (path, viewer, service) =>
393 call("git_access", { path, viewer, service }),
394 branches: (path, viewer) => call("branches", { path, viewer }),
Branches and Tags pages, each file's last commit, and the branch menu on files395 lastCommits: (path, viewer, ref, treePath) => call("last_commits", { path, viewer, ref, treePath }),
Merge project overview: one branch_drift call, spliced histories, cached tags, 6 repos calls instead of 25396 branchDrift: (path, viewer, base, heads) => call("branch_drift", { path, viewer, base, heads }),
Branches and Tags pages, each file's last commit, and the branch menu on files397 tags: (path, viewer) => call("tags", { path, viewer }),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97398 about: (path, viewer) => call("about", { path, viewer }),
399 languages: (path, viewer) => call("languages", { path, viewer }),
400 contributors: (path, viewer) => call("contributors", { path, viewer }),
401 license: (path, viewer) => call("license", { path, viewer }),
402 stars: (path, viewer) => call("stars", { path, viewer }),
403 star: (actor, path, starred) => call("star", { path, actor, starred }),
404 stargazers: (path, viewer, page) => call("stargazers", { path, viewer, page: page ?? null }),
405 starred: (username, viewer) => call("starred", { username, viewer }),
406 releases: (path, viewer) => call("releases", { path, viewer }),
407 release: (path, viewer, which) => call("release", { path, viewer, id: which.id ?? null, tag: which.tag ?? null, latest: which.latest ?? false }),
408 createRelease: (actor, path, release) => call("create_release", { path, actor, ...release }),
409 updateRelease: (actor, path, id, change) => call("update_release", { path, actor, id, ...change }),
410 deleteRelease: (actor, path, id) => call("delete_release", { path, actor, id }),
Download ZIP from the Code button; a slimmer lifecycle panel; agent steps say what was done, not sandbox paths411 listFiles: (repoId, ref, limit) => call("list_files", { repoId, ref, skipDirs: [], limit }),
412 rawBlobs: (repoId, hashes, maxBytes) => call("raw_blobs", { repoId, hashes, maxBytes }),
Merge g1tusercontent.com: registry answers run nothing in a browser, the site's pages run only their own scripts, repository files and avatars on their own origin, raw files rate limited per address413 rawFile: (repoId, ref, path, maxBytes) => call("raw_file", { repoId, ref, path, maxBytes }),
Fast pages, required checks on the branch, self-hosted runners, honest incidents414 commitFile: (repo, actor, file) => call("commit_file", { repo, actor, ...file }),
Sidebar: the panels really slide415 land: (sourceId, actor, branch) => call("land", { sourceId, actor, branch }),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar416 compare: (repoId, viewer, base, head, baseBranch) => call("compare", { repoId, viewer, base, head, baseBranch }),
Merge branch 'worktree-agent-ac5b181a013e54348'417 claimBackups: (limit, maxRunning) => call("claim_backups", { limit, maxRunning }),
418 // The sandbox's own calls are snake_case (they come through the API).
419 failBackup: (jobId, token, error) => call("backup_fail", { job_id: jobId, token, error }),
Sidebar: the panels really slide420 };
421}
422
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar423/**
424 * `PullDetail`'s own fields that the work service writes in snake_case:
425 * `g1t_contracts::work::PullDetail` has no camelCase renaming, though what
426 * it holds (`Pull`, `CheckRun` and the rest) does. Each with its name here.
427 */
428const PULL_DETAIL_FIELDS = [
429 ["review_pending", "reviewPending"],
430 ["earlier_checks", "earlierChecks"],
431 ["required_checks", "requiredChecks"],
432 ["code_owners", "codeOwners"],
433] as const;
434
435/**
436 * A pull request's detail as the work service sent it, with its own fields
437 * in the camelCase this package uses: read at the edge, by `workClient`'s
438 * `getPull`. Either spelling is taken, so a service that already sends
439 * camelCase reads the same.
440 */
441export function pullDetailFromWire(raw: Record<string, unknown>): PullDetail {
442 const detail: Record<string, unknown> = { ...raw };
443 for (const [snake, camel] of PULL_DETAIL_FIELDS) {
444 if (snake in detail) {
445 if (!(camel in detail)) detail[camel] = detail[snake];
446 delete detail[snake];
447 }
448 }
449 if (typeof detail.reviewPending !== "boolean") detail.reviewPending = false;
450 return detail as PullDetail;
451}
452
Sidebar: the panels really slide453export function workClient(service: ServiceBinding): WorkApi {
454 const call = <T>(method: string, args: object) => rpc<T>(service, method, args);
455 return {
456 openIssue: (actor, repo, input) => call("open_issue", { actor, repo, ...input }),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step457 delegateIssue: (actor, repo, input) => call("delegate_issue", { actor, repo, ...input }),
Sidebar: the panels really slide458 listIssues: (repo, viewer, filter = {}) => call("list_issues", { repo, viewer, ...filter }),
459 getIssue: (repo, number, viewer) => call("get_issue", { repo, number, viewer }),
460 updateIssue: (actor, repo, number, input) =>
461 call("update_issue", { actor, repo, number, ...input }),
462 closeIssue: (actor, repo, number, reason) =>
463 call("close_issue", { actor, repo, number, reason }),
464 reopenIssue: (actor, repo, number) => call("reopen_issue", { actor, repo, number }),
465 listLabels: (repo, viewer) => call("list_labels", { repo, viewer }),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar466 saveLabel: (actor, repo, label) => call("save_label", { actor, repo, ...label }),
467 deleteLabel: (actor, repo, name) => call("delete_label", { actor, repo, name }),
468 addDefaultLabels: (actor, repo) => call("add_default_labels", { actor, repo }),
469 setLabels: (actor, repo, number, labels, change = "set") =>
470 call("set_labels", { actor, repo, number, labels, change }),
471 listMilestones: (repo, viewer, state) => call("list_milestones", { repo, viewer, state }),
472 getMilestone: (repo, number, viewer) => call("get_milestone", { repo, number, viewer }),
473 saveMilestone: (actor, repo, milestone) => call("save_milestone", { actor, repo, ...milestone }),
474 deleteMilestone: (actor, repo, number) => call("delete_milestone", { actor, repo, number }),
Sidebar: the panels really slide475 counts: (repo, viewer) => call("counts", { repo, viewer }),
476 addComment: (actor, repo, number, comment) =>
477 call("add_comment", { actor, repo, number, ...comment }),
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts478 editComment: (actor, repo, commentId, body) => call("edit_comment", { actor, repo, commentId, body }),
479 deleteComment: (actor, repo, commentId) => call("delete_comment", { actor, repo, commentId }),
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar480 workspaceAgentComment: (repo, number, agent, actingFor, body) =>
481 call("workspace_agent_comment", { repo, number, agent, acting_for: actingFor, body }),
482 workspaceAgentReview: (repo, number, agent, actingFor, verdict, body) =>
483 call("workspace_agent_review", { repo, number, agent, acting_for: actingFor, verdict, body }),
Sidebar: the panels really slide484 startChecks: (pullId) => call("start_checks", { pullId }),
485 reportChecks: (runId, token, report) =>
486 call("report_checks", { runId, token, ...report }),
487 startReview: (pullId) => call("start_review", { pullId }),
488 failReview: (runId, token, error) => call("report_review", { runId, token, error }),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains489 startMergecheck: (pullId) => call("start_mergecheck", { pullId }),
490 failMergecheck: (pullId, token, error) => call("report_mergecheck", { pullId, token, error }),
Sidebar: the panels really slide491 advance: (pullId) => call("advance", { pullId }),
492 stall: (pullId, reason) => call("stall", { pullId, reason }),
493 managedPulls: (repoId) => call("managed_pulls", { repoId }),
494 queue: (repo, viewer) => call("queue", { repo, viewer }),
495 queueBuild: (repoId) => call("queue_build", { repoId }),
496 failQueue: (entryId, token, error) => call("report_queue", { entryId, token, error }),
497 removeFromQueue: (actor, repo, number) => call("remove_from_queue", { actor, repo, number }),
498 messageAgent: (actor, repo, number, body) => call("message_agent", { actor, repo, number, body }),
499 catchUpJob: (pullId) => call("catch_up_job", { pullId }),
Agents asked while not at work are woken to answer500 wakeForMessages: (pullId) => call("wake_for_messages", { pullId }),
Sidebar: the panels really slide501 getSettings: (repo, viewer) => call("get_settings", { repo, viewer }),
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge502 // Rulesets travel in snake_case (rules.ts).
503 listRulesets: (owner, viewer, includeParents = false) =>
504 call("list_rulesets", { viewer, ...owner, include_parents: includeParents }),
505 getRuleset: (owner, id, viewer) => call("get_ruleset", { viewer, ...owner, id }),
506 saveRuleset: (actor, owner, ruleset, id) => call("save_ruleset", { actor, ...owner, id: id ?? null, ruleset }),
507 deleteRuleset: (actor, owner, id) => call("delete_ruleset", { actor, ...owner, id }),
508 effectiveRules: (repo, name, viewer, target = "branch") => call("effective_rules", { viewer, repo, name, target }),
509 ruleEvaluations: (owner, viewer, filter = {}) => call("rule_evaluations", { viewer, ...owner, ...filter }),
Fast pages, required checks on the branch, self-hosted runners, honest incidents510 seenChecks: (repo, viewer) => call("seen_checks", { repo, viewer }),
Merge checks: statuses and check runs on every commit511 commitChecks: (repo, viewer, shas) => call("commit_checks", { repo, viewer, shas }),
512 getCheckRun: (repo, id, viewer) => call("get_check_run", { repo, id, viewer }),
513 checkRunAnnotations: (repo, id, viewer) => call("check_run_annotations", { repo, id, viewer }),
514 requestCheckAction: (actor, repo, id, identifier) => call("request_check_action", { actor, repo, id, identifier }),
515 rerequestCheckRun: (actor, repo, id) => call("rerequest_check_run", { actor, repo, id }),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar516 codeownersErrors: (repo, viewer, ref) => call("codeowners_errors", { repo, viewer, ref: ref ?? null }),
Sidebar: the panels really slide517 updateSettings: (actor, repo, settings) =>
518 call("update_settings", { actor, repo, settings }),
519 openPull: (actor, repo, input) => call("open_pull", { actor, repo, ...input }),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar520 listPulls: (repo, viewer, state, filter = {}) => call("list_pulls", { repo, viewer, state, ...filter }),
Fast pages, required checks on the branch, self-hosted runners, honest incidents521 pullsForRepos: (repoIds, viewer, limit) => call("pulls_for_repos", { repoIds, viewer, limit }),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar522 // Its own fields arrive in snake_case: read into camelCase here.
523 getPull: (repo, number, viewer) =>
524 call<Result<Record<string, unknown>>>("get_pull", { repo, number, viewer }).then(
525 (found): Result<PullDetail> => (found.ok ? { ok: true, value: pullDetailFromWire(found.value) } : found),
526 ),
Sidebar: the panels really slide527 updatePull: (actor, repo, number, changes) =>
528 call("update_pull", { actor, repo, number, ...changes }),
Catching up with main takes seconds when the two sides touched different files529 catchUpPull: (actor, repo, number) => call("catch_up_pull", { actor, repo, number }),
Sidebar: the panels really slide530 readyPull: (actor, repo, number, summary) =>
531 call("ready_pull", { actor, repo, number, summary }),
532 closePull: (actor, repo, number) => call("close_pull", { actor, repo, number }),
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts533 reopenPull: (actor, repo, number) => call("reopen_pull", { actor, repo, number }),
534 convertPullToDraft: (actor, repo, number) => call("convert_pull_to_draft", { actor, repo, number }),
Sidebar: the panels really slide535 mergePull: (actor, repo, number, options = {}) =>
536 call("merge_pull", { actor, repo, number, ...options }),
537 listActivePulls: (viewer) => call("list_active_pulls", { viewer }),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains538 byAuthor: (username, viewer, filter = {}) => call("by_author", { username, viewer, ...filter }),
Chat controls, public profiles, shadcn selects, and no Docs tab in a project539 contributions: (username, viewer) => call("contributions", { username, viewer }),
Sidebar: the panels really slide540 startPlan: (actor, repo, brief) => call("start_plan", { actor, repo, brief }),
541 failPlan: (planId, token, error) => call("report_plan", { planId, token, error }),
542 getPlan: (repo, viewer, id) => call("get_plan", { repo, viewer, id }),
543 listPlans: (repo, viewer) => call("list_plans", { repo, viewer }),
544 applyPlan: (actor, repo, id, options = {}) =>
545 call("apply_plan", { actor, repo, id, ...options }),
546 queueIssue: (actor, repo, number, queued) =>
547 call("queue_issue", { actor, repo, number, queued }),
548 readyIssues: (repoId) => call("ready_issues", { repoId }),
549 listAssignedIssues: (viewer) => call("list_assigned_issues", { viewer }),
550 appendSession: (actor, repo, number, entries) =>
551 call("append_session", { actor, repo, number, entries }),
552 readSession: (repo, number, viewer, afterSeq = 0) =>
553 call("read_session", { repo, number, viewer, afterSeq }),
554 };
555}
556
557export function billingClient(service: ServiceBinding): BillingApi {
558 const call = <T>(method: string, args: object) => rpc<T>(service, method, args);
559 return {
560 status: () => call("status", {}),
561 account: (workspace, viewer) => call("account", { workspace, viewer }),
562 ledger: (workspace, viewer) => call("ledger", { workspace, viewer }),
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging563 credits: (workspace, viewer) => call("credits", { workspace, viewer }),
The statement is a month at a time, a line per kind of charge564 statement: (workspace, viewer, month = null, group = "day") => call("statement", { workspace, viewer, month, group }),
565 statementEntries: (workspace, viewer, filter) =>
566 call("statement_entries", {
567 workspace,
568 viewer,
569 month: filter.month,
570 kind: filter.kind,
571 day: filter.day ?? null,
572 project: filter.project ?? null,
573 before: filter.before ?? null,
574 }),
Sidebar: the panels really slide575 usage: (workspace, viewer, since) => call("usage", { workspace, viewer, since }),
Mission control shows model usage, yours and the workspace's: tokens, cost, active days, cache share, each day, and the mix576 tokenUsage: (workspace, viewer, options = {}) =>
577 call("token_usage", { workspace, viewer, person: options.person ?? null, days: options.days ?? null }),
578 recordTokens: (usage) => call("record_tokens", usage),
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens579 gatewayModels: () => call("gateway_models", {}),
Merge branch 'main' into actions-toolkit-oidc-artifacts580 modelDefaults: () => call("model_defaults", {}),
581 recordDiscovery: (provider, models, by, error = null) => call("record_discovery", { provider, models, by, error }),
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens582 gatewayAdmit: (workspace) => call("gateway_admit", { workspace }),
583 recordGateway: (record) => call("record_gateway", record),
584 gatewayRequests: (workspace, viewer, options = {}) =>
585 call("gateway_requests", { workspace, viewer, limit: options.limit ?? null, before: options.before ?? null }),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look586 checkout: (actor, workspace, amountCents, returnUrl, method = "card") =>
587 call("checkout", { actor, workspace, amountCents, returnUrl, method }),
Sidebar: the panels really slide588 confirm: (workspace, viewer, session) => call("confirm", { workspace, viewer, session }),
589 canStart: (workspace) => call("can_start", { workspace }),
A free allowance on g1t's models, so anyone can try its agents590 trial: (workspace, exempt) => call("trial", { workspace, exempt }),
Sidebar: the panels really slide591 startRun: (run) => call("start_run", run),
Paid features: a workspace turns on Deployments with a monthly plan592 features: (workspace, viewer) => call("features", { workspace, viewer }),
593 subscribe: (actor, workspace, feature, returnUrl) =>
594 call("subscribe", { actor, workspace, feature, returnUrl }),
595 confirmSubscription: (workspace, viewer, session) =>
596 call("confirm_subscription", { workspace, viewer, session }),
597 cancelSubscription: (actor, workspace, feature, resume = false) =>
598 call("cancel_subscription", { actor, workspace, feature, resume }),
599 hasFeature: (workspace, feature) => call("has_feature", { workspace, feature }),
Merge Stripe Tax, the card fee on card payments, and one free workspace per person600 freeWorkspaces: (workspaces) => call("free_workspaces", { workspaces }),
Paid features: a workspace turns on Deployments with a monthly plan601 chargeFeature: (charge) => call("charge_feature", charge),
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace602 billingPortal: (actor, workspace, returnUrl) => call("billing_portal", { actor, workspace, return_url: returnUrl }),
Every sandbox is metered by the second603 recordSandbox: (usage) => call("record_sandbox", usage),
Usage limits: unpaid usage can only go so far604 limit: (workspace, viewer) => call("limit", { workspace, viewer }),
605 checkLimit: (workspace) => call("check_limit", { workspace }),
Prices keep themselves current with what g1t pays606 prices: () => call("prices", {}),
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas607 notePending: (workspace, source, costMicros, detail = null) => call("note_pending", { workspace, source, costMicros, detail }),
608 usageMeters: (workspace, viewer) => call("usage_meters", { workspace, viewer }),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look609 setSpendLimit: (actor, workspace, spendLimitMicros, useFullLimit = false, raiseOnce = false) =>
610 call("set_spend_limit", { actor, workspace, spendLimitMicros, use_full_limit: useFullLimit, raise_once: raiseOnce }),
Two limits, real invoices, trust that grows by itself, sales signals611 invoices: (workspace, viewer) => call("invoices", { workspace, viewer }),
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put612 entitlements: (workspace) => call("entitlements", { workspace }),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look613 reserve: (reservation) => call("reserve", reservation),
614 settle: (reservationId, actualMicros) => call("settle", { reservationId, actualMicros }),
615 cardCheck: (actor, workspace, returnUrl) => call("card_check", { actor, workspace, returnUrl }),
616 confirmCardCheck: (workspace, viewer, session) => call("confirm_card_check", { workspace, viewer, session }),
617 requestLimit: (actor, workspace, request) => call("request_limit", { actor, workspace, ...request }),
618 limitRequests: (workspace, viewer) => call("limit_requests", { workspace, viewer }),
619 confirmSpike: (actor, workspace, keepGoing) => call("confirm_spike", { actor, workspace, keepGoing }),
620 setCaps: (actor, workspace, caps) => call("set_caps", { actor, workspace, ...caps }),
Usage, Billing settings and prepaid AI credit; fixes from the UX audit621 usageReport: (workspace, viewer, range) =>
622 call("usage_report", { workspace, viewer, from: range.from, until: range.until, products: range.products ?? [], projects: range.projects ?? [] }),
623 aiCredit: (workspace, viewer) => call("ai_credit", { workspace, viewer }),
624 buyAiCredit: (actor, workspace, amountCents, returnUrl) => call("buy_ai_credit", { actor, workspace, amountCents, returnUrl }),
625 confirmAiCredit: (workspace, viewer, session) => call("confirm_ai_credit", { workspace, viewer, session }),
626 setAiReload: (actor, workspace, reload) => call("set_ai_reload", { actor, workspace, ...reload }),
627 setBudget: (actor, workspace, budget) => call("set_budget", { actor, workspace, ...budget }),
628 billingDetails: (workspace, viewer) => call("billing_details", { workspace, viewer }),
629 setBillingDetails: (actor, workspace, details) => call("set_billing_details", { actor, workspace, ...details }),
Sidebar: the panels really slide630 };
631}
632
Billing accounts, terms and enterprises; g1t is no longer free633export function billingAdminClient(service: ServiceBinding): BillingAdminApi {
634 const call = <T>(method: string, args: object) => rpc<T>(service, method, args);
635 return {
636 accounts: (query) => call("admin_accounts", { query: query ?? null }),
637 account: (id) => call("admin_account", { id }),
638 setTerms: (id, terms, by) => call("admin_set_terms", { id, terms, by }),
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put639 setAllowances: (id, allowances, note, by) => call("admin_set_allowances", { id, allowances, note, by }),
Billing accounts, terms and enterprises; g1t is no longer free640 createEnterprise: (name, workspaces, by) => call("admin_create_enterprise", { name, workspaces, by }),
641 attach: (workspace, account, by) => call("admin_attach", { workspace, account, by }),
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging642 credit: (workspace, amountMicros, note, by, options = { kind: "goodwill" }) =>
643 call("admin_credit", {
644 workspace,
645 amount_micros: amountMicros,
646 note,
647 by,
648 kind: options.kind,
649 expires_at: options.expiresAt ?? null,
650 refund_for: options.refundFor ?? null,
651 refund_day: options.refundDay ?? null,
652 }),
653 credits: (filter = {}) =>
654 call("admin_credits", {
655 workspace: filter.workspace ?? null,
656 kind: filter.kind ?? null,
657 month: filter.month ?? null,
658 by: filter.by ?? null,
659 }),
660 revokeCredit: (id, note, by) => call("admin_revoke_credit", { id, note, by }),
sudo: reset a test workspace's billing so it starts again as a new customer; refused on a live Stripe key, for comped workspaces and for an enterprise's661 resetBilling: (workspace, confirm, note, by) => call("admin_reset_billing", { workspace, confirm, note, by }),
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace662 billingLink: (workspace, by) => call("admin_billing_link", { workspace, by }),
Stripe's webhook secret is a Worker secret, STRIPE_WEBHOOK_SECRET, from a destination made in Stripe's dashboard663 stripe: (fix = false, by) => call("admin_stripe", { fix, by: by ?? null }),
Stripe webhooks, enterprise invoices, and sudo for both664 enterpriseBilling: (id, email, by) => call("admin_enterprise_billing", { id, email, by }),
Merge Stripe Tax, the card fee on card payments, and one free workspace per person665 enterpriseAddress: (id, address, taxIdType, taxId, by) => call("admin_enterprise_address", { id, address, taxIdType, taxId, by }),
Stripe webhooks, enterprise invoices, and sudo for both666 invoiceEnterprise: (id, by) => call("admin_invoice_enterprise", { id, by }),
667 accountsFor: (workspaces) => call("admin_accounts", { query: null, workspaces }),
Two limits, real invoices, trust that grows by itself, sales signals668 signals: () => call("admin_signals", {}),
669 overview: () => call("admin_overview", {}),
670 sales: (workspace) => call("admin_sales", { workspace }),
671 setSales: (workspace, record, by) =>
672 call("admin_set_sales", {
673 workspace,
674 stage: record.stage,
675 owner: record.owner ?? null,
676 next_step: record.nextStep ?? null,
677 next_at: record.nextAt ?? null,
678 by,
679 }),
680 addNote: (workspace, text, by) => call("admin_add_note", { workspace, text, by }),
681 workspaceInvoices: (workspace) => call("admin_workspace_invoices", { workspace }),
Billing lists every invoice and every staff change; signals carry follow-ups682 allInvoices: (filter = {}) => call("admin_invoices", { status: filter.status ?? null, month: filter.month ?? null }),
683 audit: (filter = {}) =>
684 call("admin_audit", { by: filter.by ?? null, action: filter.action ?? null, before: filter.before ?? null }),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look685 limitRequests: (status = "open") => call("admin_limit_requests", { status }),
686 decideLimitRequest: (id, decision, amountMicros, note, by) =>
687 call("admin_decide_limit_request", { id, decision, amount_micros: amountMicros, note, by }),
688 overages: () => call("admin_overages", {}),
689 goodwill: (workspace, amountMicros, reason, by, day = null) =>
690 call("admin_goodwill", { workspace, amount_micros: amountMicros, reason, by, day }),
691 velocity: () => call("admin_velocity", {}),
692 recordPayment: (workspace, amountMicros, reference, note, by) =>
693 call("admin_record_payment", { workspace, amount_micros: amountMicros, reference, note, by }),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily694 costs: (days) => call("admin_costs", { days: days ?? null }),
695 costAlerts: () => call("admin_cost_alerts", {}),
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays696 spendCaps: () => call("admin_spend_caps", {}),
697 liftBreaker: (note, by) => call("admin_lift_breaker", { note, by }),
Merge platform pause and the hourly usage watcher: staff can pause compute, schedules, indexing or renders for everyone, the watcher emails on a breach and is never blind quietly, and the models proxy holds each run to its cap (billing 0051, integrations 0006)698 platformGuard: () => call("admin_platform_guard", {}),
699 setPause: (level, paused, note, by) => call("admin_set_pause", { level, paused, note, by }),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily700 decideProposal: (id, decision, note, by) => call("admin_decide_proposal", { id, decision, note, by }),
701 setCostSettings: (settings, by) => call("admin_set_cost_settings", { settings, by }),
702 setCostMapping: (mapping, by) =>
703 call("admin_set_cost_mapping", {
704 product: mapping.product,
705 meter: mapping.meter,
706 bucket: mapping.bucket ?? "",
707 price_meter: mapping.priceMeter ?? null,
708 own_meter: mapping.ownMeter ?? null,
709 scale_to_own: mapping.scaleToOwn ?? false,
710 drift_percent: mapping.driftPercent ?? null,
711 note: mapping.note ?? "",
712 remove: mapping.remove ?? false,
713 by,
714 }),
715 runCosts: (by) => call("admin_run_costs", { by }),
Merge branch 'main' into actions-toolkit-oidc-artifacts716 models: () => call("admin_models", {}),
717 decideModel: (model, decision, details, reason, by) =>
718 call("admin_decide_model", {
719 model,
720 decision,
721 name: details.name ?? null,
722 tier_hint: details.tierHint ?? null,
723 prices: details.prices
724 ? {
725 input_micros: details.prices.inputMicros,
726 output_micros: details.prices.outputMicros,
727 cache_read_micros: details.prices.cacheReadMicros,
728 cache_write_micros: details.prices.cacheWriteMicros,
729 cache_write_1h_micros: details.prices.cacheWrite1hMicros,
730 threshold: details.prices.threshold,
731 over_input_micros: details.prices.overInputMicros,
732 over_output_micros: details.prices.overOutputMicros,
733 over_cache_read_micros: details.prices.overCacheReadMicros,
734 over_cache_write_micros: details.prices.overCacheWriteMicros,
735 over_cache_write_1h_micros: details.prices.overCacheWrite1hMicros,
736 }
737 : null,
738 reason,
739 by,
740 }),
741 setModelDefault: (purpose, value, reason, by) =>
742 call("admin_set_model_default", {
743 purpose,
744 model: value.model ?? null,
745 tier: value.tier ?? null,
746 effort: value.effort ?? null,
747 reason,
748 by,
749 }),
Billing accounts, terms and enterprises; g1t is no longer free750 };
751}
752
Sidebar: the panels really slide753export function eventsClient(service: ServiceBinding): EventsApi {
754 const call = <T>(method: string, args: object) => rpc<T>(service, method, args);
755 return {
756 publish: (events) => call("publish", { events }),
757 list: (query) => call("list", query),
758 };
759}
760
761export function integrationsClient(service: ServiceBinding): IntegrationsApi {
762 const call = <T>(method: string, args: object) => rpc<T>(service, method, args);
763 return {
764 list: (workspace, viewer) => call("list", { workspace, viewer }),
765 connect: (actor, workspace, input) => call("connect", { actor, workspace, ...input }),
766 update: (actor, workspace, id, input) => call("update", { actor, workspace, id, ...input }),
767 disconnect: (actor, workspace, id) => call("disconnect", { actor, workspace, id }),
768 test: (actor, workspace, id) => call("test", { actor, workspace, id }),
769 deliveries: (workspace, viewer, id) => call("deliveries", { workspace, viewer, id }),
770 resolve: (workspace, viewer, reference) => call("resolve", { workspace, viewer, reference }),
771 references: (workspace, text, limit) => call("references", { workspace, text, limit }),
772 import: (actor, repo, reference, assign) => call("import", { actor, repo, reference, assign }),
773 links: (repo, number) => call("links", { repo, number }),
774 modelProvider: (workspace) => call("model_provider", { workspace }),
775 openModelSession: (run) => call("open_model_session", run),
776 modelUpstream: (token) => call("model_upstream", { token }),
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens777 gatewayUpstream: (workspace) => call("gateway_upstream", { workspace }),
AI Gateway: OpenAI's format, open models, and your own providers778 gatewayProviders: (workspace) => call("gateway_providers", { workspace }),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily779 closeModelSessions: (tokenHashes) => call("close_model_sessions", { token_hashes: tokenHashes }),
Merge platform pause and the hourly usage watcher: staff can pause compute, schedules, indexing or renders for everyone, the watcher emails on a breach and is never blind quietly, and the models proxy holds each run to its cap (billing 0051, integrations 0006)780 capModelSessions: (tokenHashes, capMicros) => call("cap_model_sessions", { token_hashes: tokenHashes, cap_micros: capMicros }),
Sidebar: the panels really slide781 routes: (workspace, viewer) => call("routes", { workspace, viewer }),
782 setRoutes: (actor, workspace, routes) => call("set_routes", { actor, workspace, routes }),
783 };
784}
785
786export function webhooksClient(service: ServiceBinding): WebhooksApi {
787 const call = <T>(method: string, args: object) => rpc<T>(service, method, args);
788 return {
789 list: (viewer, owner) => call("list", { viewer, ...owner }),
790 create: (actor, owner, input) => call("create", { actor, ...owner, ...input }),
791 update: (actor, owner, id, input) => call("update", { actor, ...owner, id, ...input }),
792 delete: (actor, owner, id) => call("delete", { actor, ...owner, id }),
793 ping: (actor, owner, id) => call("ping", { actor, ...owner, id }),
794 deliveries: (viewer, owner, id) => call("deliveries", { viewer, ...owner, id }),
795 redeliver: (actor, owner, deliveryId) => call("redeliver", { actor, ...owner, deliveryId }),
796 };
797}
798
799export function actionsClient(service: ServiceBinding): ActionsApi {
800 const call = <T>(method: string, args: object) => rpc<T>(service, method, args);
801 return {
802 workflows: (repo, viewer) => call("workflows", { repo, viewer }),
803 runs: (repo, viewer, filter = {}) => call("runs", { repo, viewer, ...filter }),
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)804 run: (repo, viewer, id, attempt) => call("run", { repo, viewer, id, attempt: attempt ?? null }),
Sidebar: the panels really slide805 logs: (repo, viewer, job, after = 0) => call("logs", { repo, viewer, job, after }),
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)806 summaries: (repo, viewer, id, attempt) => call("summaries", { repo, viewer, id, attempt: attempt ?? null }),
807 jobLogText: (repo, viewer, job) => call("job_log_text", { repo, viewer, job }),
808 runLogs: (repo, viewer, id, attempt) => call("run_logs", { repo, viewer, id, attempt: attempt ?? null }),
Sidebar: the panels really slide809 dispatch: (actor, repo, workflow, ref, inputs) => call("dispatch", { actor, repo, workflow, ref, inputs }),
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)810 cancel: (actor, repo, id, force = false) => call("cancel", { actor, repo, id, force }),
811 rerun: (actor, repo, id, failedOnly = false, options = {}) =>
812 call("rerun", { actor, repo, id, failed_only: failedOnly, job: options.job ?? null, debug: options.debug ?? false }),
Sidebar: the panels really slide813 setWorkflowEnabled: (actor, repo, workflow, enabled) =>
814 call("set_workflow_enabled", { actor, repo, workflow, enabled }),
815 settings: (actor, owner, kind) => call("settings", { actor, ...owner, kind }),
Secrets and variables: one list, rows per environment, for workflows and deployments816 setSetting: (actor, owner, kind, name, value, options = {}) =>
817 call("set_setting", { actor, ...owner, kind, name, value, ...options }),
818 deleteSetting: (actor, owner, kind, name, id) => call("delete_setting", { actor, ...owner, kind, name, id }),
Merge branch 'worktree-agent-a3abfcce648e87dca'819 approveRun: (actor, repo, id) => call("approve_run", { actor, repo, id }),
820 pendingDeployments: (repo, viewer, id) => call("pending_deployments", { repo, viewer, id }),
821 reviewDeployments: (actor, repo, id, state, environments = [], comment) =>
822 call("review_deployments", { actor, repo, id, state, environments, comment: comment ?? null }),
823 actionsSettings: (repo, viewer) => call("actions_settings", { repo, viewer }),
824 setActionsSettings: (actor, repo, change) => call("set_actions_settings", { actor, repo, ...change }),
825 workspaceActionsSettings: (workspace, viewer) => call("workspace_actions_settings", { workspace, viewer }),
826 setWorkspaceActionsSettings: (actor, workspace, change) =>
827 call("set_workspace_actions_settings", { actor, workspace, ...change }),
828 environments: (repo, viewer) => call("environments", { repo, viewer }),
829 setEnvironment: (actor, repo, name, change) => call("set_environment", { actor, repo, name, ...change }),
830 deleteEnvironment: (actor, repo, name) => call("delete_environment", { actor, repo, name }),
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2831 artifacts: (repo, viewer, filter = {}) => call("artifacts", { repo, viewer, ...filter }),
832 artifactDownload: (repo, viewer, by) => call("artifact_download", { repo, viewer, ...by }),
833 deleteArtifact: (actor, repo, id) => call("delete_artifact", { actor, repo, id }),
834 artifactRetention: (repo, viewer, days) => call("artifact_retention", { repo, viewer, days }),
Sidebar: the panels really slide835 };
836}
837
Deployments: a preview for every pull request, production on g1t.page838
Fast pages, required checks on the branch, self-hosted runners, honest incidents839/** Self-hosted runners, kept by the actions service. */
840export function runnersClient(service: ServiceBinding): RunnersApi {
841 const call = <T>(method: string, args: object) => rpc<T>(service, method, args);
842 return {
843 stuck: (viewer) => call("stuck_jobs", { viewer }),
The Runners page shows the machines a workspace's agents and workflow jobs run on, in Workspace under Compute: g1t's cloud beside your own runners, what each is running now with a link to it, what's waiting, this month's machine time and its cost (your own runners' free), where agent work and workflow jobs run, adding a runner, and groups; runner_activity in Actions says what runs where, work handed to your runners keeps its agent run, and the self-hosted runners guide says how.844 activity: (actor, workspace) => call("runner_activity", { actor, workspace }),
Fast pages, required checks on the branch, self-hosted runners, honest incidents845 list: (actor, owner) => call("runners", { actor, ...owner }),
846 createToken: (actor, owner, group) => call("create_registration_token", { actor, ...owner, group }),
847 remove: (actor, owner, id) => call("remove_runner", { actor, ...owner, id }),
848 groups: (actor, workspace) => call("runner_groups", { actor, workspace }),
849 setGroup: (actor, workspace, group) => call("set_runner_group", { actor, workspace, ...group }),
850 deleteGroup: (actor, workspace, id) => call("delete_runner_group", { actor, workspace, id }),
851 settings: (actor, owner) => call("runner_settings", { actor, ...owner }),
852 setSettings: (actor, owner, change) => call("set_runner_settings", { actor, ...owner, ...change }),
853 };
854}
855
Deployments: a preview for every pull request, production on g1t.page856export function deploymentsClient(service: ServiceBinding): DeploymentsApi {
857 const call = <T>(method: string, args: object) => rpc<T>(service, method, args);
858 return {
Projects: what a workspace builds and runs, first on every page859 settings: (project, viewer) => call("settings", { project, viewer }),
860 updateSettings: (actor, project, changes) => call("update_settings", { actor, project, changes }),
861 list: (project, viewer) => call("list", { project, viewer }),
862 get: (project, id, viewer) => call("get", { project, id, viewer }),
863 redeploy: (actor, project, branch) => call("redeploy", { actor, project, branch }),
864 takeDown: (actor, project, branch) => call("take_down", { actor, project, branch }),
865 overview: (workspace, viewer) => call("overview", { workspace, viewer }),
Deployments: a preview for every pull request, production on g1t.page866 usage: (workspace, viewer) => call("usage", { workspace, viewer }),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains867 domains: (project, viewer) => call("domains", { project, viewer }),
868 addDomain: (actor, project, hostname, options = {}) =>
869 call("add_domain", { actor, project, hostname, twin: !!options.twin }),
870 removeDomain: (actor, project, id) => call("remove_domain", { actor, project, id }),
871 refreshDomain: (actor, project, id) => call("refresh_domain", { actor, project, id }),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97872 repoDeployments: (repo, viewer, filter = {}) => call("list_deployments", { repo, viewer, ...filter }),
873 repoDeployment: (repo, id, viewer) => call("get_deployment", { repo, id, viewer }),
874 environments: (repo, viewer) => call("list_environments", { repo, viewer }),
875 createDeployment: (actor, repo, input) => call("create_deployment", { repo, actor, ...input }),
876 createDeploymentStatus: (actor, repo, id, input) => call("create_deployment_status", { repo, actor, id, ...input }),
Deployments: a preview for every pull request, production on g1t.page877 };
878}
Projects: what a workspace builds and runs, first on every page879
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member880/** Packages: the registries beside the code (services/packages). */
881export function packagesClient(service: ServiceBinding): PackagesApi {
882 const call = <T>(method: string, args: object) => rpc<T>(service, method, args);
883 return {
884 list: (workspace, viewer, filter = {}) =>
885 call("list_packages", {
886 workspace,
887 viewer,
888 ecosystem: filter.ecosystem ?? null,
889 repo_id: filter.repoId ?? null,
890 query: filter.query ?? null,
891 }),
892 get: (workspace, ecosystem, name, viewer) => call("get_package", { workspace, ecosystem, name, viewer }),
893 deleteVersion: (actor, workspace, ecosystem, name, version, surface) =>
894 call("delete_version", { actor, workspace, ecosystem, name, version, surface: surface ?? null }),
895 deletePackage: (actor, workspace, ecosystem, name, surface) =>
896 call("delete_package", { actor, workspace, ecosystem, name, surface: surface ?? null }),
897 set: (actor, workspace, ecosystem, name, change, surface) =>
898 call("set_package", {
899 actor,
900 workspace,
901 ecosystem,
902 name,
903 visibility: change.visibility ?? null,
904 link: change.link ?? null,
905 unlink: change.unlink ?? false,
Merge packages: roles, Actions access, source label, soft delete, API906 inherit_access: change.inheritAccess ?? null,
907 surface: surface ?? null,
908 }),
909 settings: (workspace, ecosystem, name, viewer) => call("package_settings", { workspace, ecosystem, name, viewer }),
910 deleted: (workspace, viewer) => call("deleted_packages", { workspace, viewer }),
911 restorePackage: (actor, workspace, ecosystem, name, surface) =>
912 call("restore_package", { actor, workspace, ecosystem, name, surface: surface ?? null }),
913 restoreVersion: (actor, workspace, ecosystem, name, version, surface) =>
914 call("restore_version", { actor, workspace, ecosystem, name, version, surface: surface ?? null }),
915 setAccess: (actor, workspace, ecosystem, name, who, role, surface) =>
916 call("set_package_access", {
917 actor,
918 workspace,
919 ecosystem,
920 name,
921 user: "user" in who ? who.user : null,
922 team: "team" in who ? who.team : null,
923 role,
924 surface: surface ?? null,
925 }),
926 removeAccess: (actor, workspace, ecosystem, name, who, surface) =>
927 call("remove_package_access", {
928 actor,
929 workspace,
930 ecosystem,
931 name,
932 user: "user" in who ? who.user : null,
933 team: "team" in who ? who.team : null,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member934 surface: surface ?? null,
935 }),
Merge packages: roles, Actions access, source label, soft delete, API936 setActionsAccess: (actor, workspace, ecosystem, name, repo, role, surface) =>
937 call("set_actions_access", { actor, workspace, ecosystem, name, repo, role, surface: surface ?? null }),
938 removeActionsAccess: (actor, workspace, ecosystem, name, repo, surface) =>
939 call("remove_actions_access", { actor, workspace, ecosystem, name, repo, surface: surface ?? null }),
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member940 storage: (workspace) => call("storage", { workspace }),
941 storageAll: () => call("storage_all", {}),
Composer from the workspace's own repositories, and go get from g1t.sh942 syncComposer: (repoId) => call("sync_composer", { repo_id: repoId }),
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member943 };
944}
945
Projects: what a workspace builds and runs, first on every page946export function projectsClient(service: ServiceBinding): ProjectsApi {
947 const call = <T>(method: string, args: object) => rpc<T>(service, method, args);
948 return {
949 list: (workspace, viewer) => call("list", { workspace, viewer }),
950 get: (workspace, slug, viewer) => call("get", { workspace, slug, viewer }),
951 byRepo: (repoId) => call("by_repo", { repoId }),
952 create: (actor, workspace, input) => call("create", { actor, workspace, input }),
953 update: (actor, workspace, slug, changes) => call("update", { actor, workspace, slug, changes }),
A project is an app or a library: libraries show their package and how to ship a release, not production954 deploymentsChanged: (projectId, enabled) => call("deployments_changed", { projectId, enabled }),
Projects: pinned and recent projects, per person per workspace955 shortcuts: (workspace, viewer) => call("shortcuts", { workspace, viewer }),
956 pin: (actor, workspace, slug, position) => call("pin", { actor, workspace, slug, position: position ?? null }),
957 unpin: (actor, workspace, slug) => call("unpin", { actor, workspace, slug }),
958 reorderPins: (actor, workspace, slugs) => call("reorder_pins", { actor, workspace, slugs }),
959 visited: (actor, projectId) => call("visited", { actor, projectId }),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97960 publicLinks: (repos) => call("public_links", { repos }),
Projects: what a workspace builds and runs, first on every page961 };
962}

This file's history is long; its oldest lines are credited to the oldest commit read.