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.

300 linesCodeBlameRaw
1/**
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. Shared by the app and documentation.
8 *
9 * Each snippet is built for an MCP server URL: g1t.sh's by default, or a
10 * self-hosted g1t's own (`useAddresses().mcp`).
11 */
12
13/** g1t.sh's MCP server, the same as HOSTED_ADDRESSES.mcp in ./addresses. */
14export const MCP_URL = "https://mcp.g1t.sh";
15
16/** Installs g1t's Claude Code plugin, which adds g1t.sh's MCP server and records sessions. */
17export const PLUGIN_INSTALL = "claude plugin marketplace add https://g1t.sh/flagon-io/g1t-plugins.git\nclaude plugin install g1t@g1t";
18
19export const AGENT_IDS = [
20 "claude-code",
21 "codex",
22 "opencode",
23 "cursor",
24 "gemini",
25 "chatgpt",
26 "claude-chat",
27 "grok",
28 "open-webui",
29 "librechat",
30 "other",
31] as const;
32export type AgentId = (typeof AGENT_IDS)[number];
33export const DEFAULT_AGENT: AgentId = "claude-code";
34/** Where the choice is kept in `localStorage`. */
35export const AGENT_STORAGE_KEY = "g1t:agent";
36
37export type AgentSetup = {
38 id: AgentId;
39 label: string;
40 /** The file the snippet goes in, or null for a command or server URL. */
41 file: string | null;
42 lang: "sh" | "json" | "yaml" | "text";
43 code: string;
44 /** What to do after, in a sentence or two; `code` in backticks. */
45 then: string;
46 /** The file it reads a repository's instructions from. */
47 instructions: string;
48};
49
50const json = (value: unknown) => JSON.stringify(value, null, 2);
51
52/** How each agent adds the MCP server at `mcp`. */
53export function agentsFor(mcp: string = MCP_URL): Record<AgentId, AgentSetup> {
54 return {
55 // On g1t.sh, g1t's plugin: it adds the MCP server itself (as
56 // plugin:g1t:g1t) and records sessions. A self-hosted g1t's server is
57 // added on its own.
58 "claude-code":
59 mcp === MCP_URL
60 ? {
61 id: "claude-code",
62 label: "Claude Code",
63 file: null,
64 lang: "sh",
65 code: PLUGIN_INSTALL,
66 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.",
67 instructions: "CLAUDE.md",
68 }
69 : {
70 id: "claude-code",
71 label: "Claude Code",
72 file: null,
73 lang: "sh",
74 code: `claude mcp add --transport http g1t ${mcp}`,
75 then: "Then run `/mcp` in Claude Code and choose g1t to sign in through your browser.",
76 instructions: "CLAUDE.md",
77 },
78 codex: {
79 id: "codex",
80 label: "Codex",
81 file: null,
82 lang: "sh",
83 code: `codex mcp add g1t --url ${mcp}\ncodex mcp login g1t`,
84 then: "The login opens your browser to sign in to g1t.",
85 instructions: "AGENTS.md",
86 },
87 opencode: {
88 id: "opencode",
89 label: "OpenCode",
90 file: "opencode.json",
91 lang: "json",
92 code: json({
93 $schema: "https://opencode.ai/config.json",
94 mcp: { g1t: { type: "remote", url: mcp, enabled: true } },
95 }),
96 then: "OpenCode signs you in through your browser when it first connects; `opencode mcp auth g1t` starts it by hand.",
97 instructions: "AGENTS.md",
98 },
99 gemini: {
100 id: "gemini",
101 label: "Gemini CLI",
102 file: null,
103 lang: "sh",
104 code: `gemini mcp add --transport http g1t ${mcp}`,
105 then: "Start Gemini CLI and use `/mcp auth g1t` to sign in through your browser.",
106 instructions: "GEMINI.md",
107 },
108 chatgpt: {
109 id: "chatgpt",
110 label: "ChatGPT",
111 file: null,
112 lang: "text",
113 code: mcp,
114 then: "In ChatGPT Plugins, choose Add custom MCP server, enter this URL and choose OAuth. Sign in to g1t and approve access. Availability depends on your plan and workspace settings.",
115 instructions: "AGENTS.md",
116 },
117 "claude-chat": {
118 id: "claude-chat",
119 label: "Claude chat",
120 file: null,
121 lang: "text",
122 code: mcp,
123 then: "In Claude, open Customize > Connectors and add a custom connector with this URL. Choose browser sign-in and Register automatically for the OAuth client. Team administrators may need to add it first. The server must be publicly reachable.",
124 instructions: "",
125 },
126 grok: {
127 id: "grok",
128 label: "Grok",
129 file: null,
130 lang: "text",
131 code: mcp,
132 then: "Open grok.com/connectors, choose New Connector > Custom, enter this URL and sign in to g1t. Business and Enterprise teams may need an administrator to provision it first. The server must be publicly reachable.",
133 instructions: "",
134 },
135 "open-webui": {
136 id: "open-webui",
137 label: "Open WebUI",
138 file: null,
139 lang: "text",
140 code: mcp,
141 then: "An administrator adds this URL under Settings > Admin > Integrations > External Tool Servers. Choose MCP (Streamable HTTP) and OAuth 2.1, then sign in to g1t. Select a model with tool calling, including a local model served by Ollama or an OpenAI-compatible endpoint.",
142 instructions: "",
143 },
144 librechat: {
145 id: "librechat",
146 label: "LibreChat",
147 file: "librechat.yaml",
148 lang: "yaml",
149 code: `mcpServers:\n g1t:\n type: streamable-http\n url: ${JSON.stringify(mcp)}\n requiresOAuth: true`,
150 then: "Add this to your LibreChat configuration and restart it. Connect g1t from the MCP tools menu and complete browser sign-in. Choose a model that supports tool calling; the model provider and your g1t connection are configured separately.",
151 instructions: "",
152 },
153 other: {
154 id: "other",
155 label: "Other MCP",
156 file: null,
157 lang: "text",
158 code: mcp,
159 then: "Add this URL as a remote Streamable HTTP MCP server in your AI client. Choose OAuth and sign in to g1t. Your client must support remote MCP and browser authentication.",
160 instructions: "AGENTS.md",
161 },
162 cursor: {
163 id: "cursor",
164 label: "Cursor",
165 file: ".cursor/mcp.json",
166 lang: "json",
167 code: json({ mcpServers: { g1t: { url: mcp } } }),
168 then: "Use `~/.cursor/mcp.json` for every project. Cursor signs you in through your browser when it first connects.",
169 instructions: "AGENTS.md",
170 },
171 };
172}
173
174/** The snippets for g1t.sh. */
175export const AGENTS: Record<AgentId, AgentSetup> = agentsFor();
176
177/** Opens Cursor with g1t ready to add, from Cursor's install-link format. */
178export function cursorInstallLink(mcp: string = MCP_URL): string {
179 const config = btoa(JSON.stringify({ url: mcp }));
180 return `cursor://anysphere.cursor-deeplink/mcp/install?name=g1t&config=${encodeURIComponent(config)}`;
181}
182
183export function parseAgent(value: unknown): AgentId | null {
184 return typeof value === "string" && (AGENT_IDS as readonly string[]).includes(value) ? (value as AgentId) : null;
185}
186
187type Store = Pick<Storage, "getItem" | "setItem">;
188
189/**
190 * The remembered choice, or the default. Storage can be missing or throw
191 * (private windows, blocked site data); that is the same as no choice.
192 */
193export function readAgentChoice(storage: () => Store | null | undefined, fallback: AgentId = DEFAULT_AGENT): AgentId {
194 try {
195 return parseAgent(storage()?.getItem(AGENT_STORAGE_KEY)) ?? fallback;
196 } catch {
197 return fallback;
198 }
199}
200
201/** Remembers the choice; false when it could not be kept. */
202export function writeAgentChoice(storage: () => Store | null | undefined, id: AgentId): boolean {
203 try {
204 const store = storage();
205 if (!store) return false;
206 store.setItem(AGENT_STORAGE_KEY, id);
207 return true;
208 } catch {
209 return false;
210 }
211}
212
213// --- The page's one choice, which every toggle follows ----------------------
214
215const browserStorage = () => (typeof window === "undefined" ? null : window.localStorage);
216let current: AgentId | null = null;
217const listeners = new Set<() => void>();
218
219export function subscribeAgentChoice(listener: () => void): () => void {
220 listeners.add(listener);
221 const onStorage = (event: StorageEvent) => {
222 if (event.key !== null && event.key !== AGENT_STORAGE_KEY) return;
223 current = parseAgent(event.newValue);
224 listener();
225 };
226 if (typeof window !== "undefined") {
227 window.addEventListener("storage", onStorage);
228 window.addEventListener("focus", listener);
229 }
230 return () => {
231 listeners.delete(listener);
232 if (typeof window !== "undefined") {
233 window.removeEventListener("storage", onStorage);
234 window.removeEventListener("focus", listener);
235 }
236 };
237}
238
239export function getAgentChoice(fallback: AgentId = DEFAULT_AGENT): AgentId {
240 return cookieAgent("g1t_agent") ?? current ?? readAgentChoice(browserStorage, fallback);
241}
242
243/** Only a public UI preference crosses subdomains; no account or provider details. */
244export function cookieAgent(name: string): AgentId | null {
245 if (typeof document === "undefined") return null;
246 try {
247 return parseAgent(
248 document.cookie
249 .split(";")
250 .map((s) => s.trim())
251 .find((s) => s.startsWith(name + "="))
252 ?.split("=")[1],
253 );
254 } catch {
255 return null;
256 }
257}
258
259export function rememberAgentDefault(id: AgentId): void {
260 agentCookie("g1t_agent_default", id, 3600);
261}
262
263function agentCookie(name: string, id: AgentId, seconds: number): void {
264 if (typeof document === "undefined") return;
265 const domain = location.hostname === "g1t.sh" || location.hostname.endsWith(".g1t.sh") ? "; Domain=g1t.sh" : "";
266 try {
267 document.cookie = `${name}=${id}; Path=/; Max-Age=${seconds}; SameSite=Lax${location.protocol === "https:" ? "; Secure" : ""}${domain}`;
268 } catch {
269 /* Restricted storage must not prevent switching tabs. */
270 }
271}
272
273/** The first configured provider, in the same order as the workspace AI page. */
274export function agentForProviders(providers: readonly { provider: string }[]): AgentId {
275 switch (providers[0]?.provider) {
276 case "anthropic":
277 case "anthropic_endpoint":
278 return "claude-code";
279 case "openai":
280 case "azure_openai":
281 return "codex";
282 case "gemini":
283 return "gemini";
284 case "xai":
285 return "grok";
286 case "openai_endpoint":
287 return "open-webui";
288 case undefined:
289 return DEFAULT_AGENT;
290 default:
291 return "opencode";
292 }
293}
294
295export function setAgentChoice(id: AgentId, storage: () => Store | null | undefined = browserStorage): void {
296 current = id;
297 agentCookie("g1t_agent", id, 31536000);
298 writeAgentChoice(storage, id);
299 for (const listener of listeners) listener();
300}