Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
168 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.

API: notifications over REST and MCP, with notifications scopes1/**
2 * How each coding agent connects to g1t's MCP server, and which one this
3 * browser last chose. Every agent toggle on a page follows the same choice,
4 * and it is remembered for the next visit.
5 *
6 * The commands follow each agent's own documentation for adding a remote
7 * MCP server; keep them in step with `apps/docs/src/data/agents.ts`, the
8 * docs' copy.
9 *
10 * Each snippet is built for an MCP server URL: g1t.sh's by default, or a
11 * self-hosted g1t's own (`useAddresses().mcp`).
12 */
13
14/** g1t.sh's MCP server, the same as HOSTED_ADDRESSES.mcp in ./addresses. */
15export const MCP_URL = "https://mcp.g1t.sh";
16
Merge branch 'worktree-agent-ab3c375781462035f'17/** Installs g1t's Claude Code plugin, which adds g1t.sh's MCP server and records sessions. */
18export const PLUGIN_INSTALL = "claude plugin marketplace add https://g1t.sh/flagon-io/g1t.git\nclaude plugin install g1t@g1t";
19
API: notifications over REST and MCP, with notifications scopes20export const AGENT_IDS = ["claude-code", "codex", "opencode", "cursor"] as const;
21export type AgentId = (typeof AGENT_IDS)[number];
22export const DEFAULT_AGENT: AgentId = "claude-code";
23/** Where the choice is kept in `localStorage`. */
24export const AGENT_STORAGE_KEY = "g1t:agent";
25
26export type AgentSetup = {
27 id: AgentId;
28 label: string;
29 /** The file the snippet goes in, or null for a terminal command. */
30 file: string | null;
31 lang: "sh" | "json";
32 code: string;
33 /** What to do after, in a sentence or two; `code` in backticks. */
34 then: string;
35 /** The file it reads a repository's instructions from. */
36 instructions: string;
37};
38
39const json = (value: unknown) => JSON.stringify(value, null, 2);
40
41/** How each agent adds the MCP server at `mcp`. */
42export function agentsFor(mcp: string = MCP_URL): Record<AgentId, AgentSetup> {
43 return {
Merge branch 'worktree-agent-ab3c375781462035f'44 // On g1t.sh, g1t's plugin: it adds the MCP server itself (as
45 // plugin:g1t:g1t) and records sessions. A self-hosted g1t's server is
46 // added on its own.
47 "claude-code":
48 mcp === MCP_URL
49 ? {
50 id: "claude-code",
51 label: "Claude Code",
52 file: null,
53 lang: "sh",
54 code: PLUGIN_INSTALL,
55 then: "The plugin adds g1t's MCP server and records your sessions. Start Claude Code again, run `/mcp` and choose `plugin:g1t:g1t` to sign in through your browser. Skip `claude mcp add` with the plugin installed, or every g1t tool is listed twice.",
56 instructions: "CLAUDE.md",
57 }
58 : {
59 id: "claude-code",
60 label: "Claude Code",
61 file: null,
62 lang: "sh",
63 code: `claude mcp add --transport http g1t ${mcp}`,
64 then: "Then run `/mcp` in Claude Code and choose g1t to sign in through your browser.",
65 instructions: "CLAUDE.md",
66 },
API: notifications over REST and MCP, with notifications scopes67 codex: {
68 id: "codex",
69 label: "Codex",
70 file: null,
71 lang: "sh",
72 code: `codex mcp add g1t --url ${mcp}\ncodex mcp login g1t`,
73 then: "The login opens your browser to sign in to g1t.",
74 instructions: "AGENTS.md",
75 },
76 opencode: {
77 id: "opencode",
78 label: "OpenCode",
79 file: "opencode.json",
80 lang: "json",
81 code: json({
82 $schema: "https://opencode.ai/config.json",
83 mcp: { g1t: { type: "remote", url: mcp, enabled: true } },
84 }),
85 then: "OpenCode signs you in through your browser when it first connects; `opencode mcp auth g1t` starts it by hand.",
86 instructions: "AGENTS.md",
87 },
88 cursor: {
89 id: "cursor",
90 label: "Cursor",
91 file: ".cursor/mcp.json",
92 lang: "json",
93 code: json({ mcpServers: { g1t: { url: mcp } } }),
94 then: "Use `~/.cursor/mcp.json` for every project. Cursor signs you in through your browser when it first connects.",
95 instructions: "AGENTS.md",
96 },
97 };
98}
99
100/** The snippets for g1t.sh. */
101export const AGENTS: Record<AgentId, AgentSetup> = agentsFor();
102
103/** Opens Cursor with g1t ready to add, from Cursor's install-link format. */
104export function cursorInstallLink(mcp: string = MCP_URL): string {
105 const config = btoa(JSON.stringify({ url: mcp }));
106 return `cursor://anysphere.cursor-deeplink/mcp/install?name=g1t&config=${encodeURIComponent(config)}`;
107}
108
109export function parseAgent(value: unknown): AgentId | null {
110 return typeof value === "string" && (AGENT_IDS as readonly string[]).includes(value) ? (value as AgentId) : null;
111}
112
113type Store = Pick<Storage, "getItem" | "setItem">;
114
115/**
116 * The remembered choice, or the default. Storage can be missing or throw
117 * (private windows, blocked site data); that is the same as no choice.
118 */
119export function readAgentChoice(storage: () => Store | null | undefined): AgentId {
120 try {
121 return parseAgent(storage()?.getItem(AGENT_STORAGE_KEY)) ?? DEFAULT_AGENT;
122 } catch {
123 return DEFAULT_AGENT;
124 }
125}
126
127/** Remembers the choice; false when it could not be kept. */
128export function writeAgentChoice(storage: () => Store | null | undefined, id: AgentId): boolean {
129 try {
130 const store = storage();
131 if (!store) return false;
132 store.setItem(AGENT_STORAGE_KEY, id);
133 return true;
134 } catch {
135 return false;
136 }
137}
138
139// --- The page's one choice, which every toggle follows ----------------------
140
141const browserStorage = () => (typeof window === "undefined" ? null : window.localStorage);
142let current: AgentId | null = null;
143const listeners = new Set<() => void>();
144
145export function subscribeAgentChoice(listener: () => void): () => void {
146 listeners.add(listener);
147 const onStorage = (event: StorageEvent) => {
148 if (event.key !== AGENT_STORAGE_KEY) return;
149 current = parseAgent(event.newValue) ?? DEFAULT_AGENT;
150 listener();
151 };
152 if (typeof window !== "undefined") window.addEventListener("storage", onStorage);
153 return () => {
154 listeners.delete(listener);
155 if (typeof window !== "undefined") window.removeEventListener("storage", onStorage);
156 };
157}
158
159export function getAgentChoice(): AgentId {
160 current ??= readAgentChoice(browserStorage);
161 return current;
162}
163
164export function setAgentChoice(id: AgentId, storage: () => Store | null | undefined = browserStorage): void {
165 current = id;
166 writeAgentChoice(storage, id);
167 for (const listener of listeners) listener();
168}

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