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.

234 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 = ["claude-code", "codex", "opencode", "cursor", "gemini", "chatgpt", "claude-chat", "grok", "open-webui", "librechat", "other"] as const;
20export type AgentId = (typeof AGENT_IDS)[number];
21export const DEFAULT_AGENT: AgentId = "claude-code";
22/** Where the choice is kept in `localStorage`. */
23export const AGENT_STORAGE_KEY = "g1t:agent";
24
25export type AgentSetup = {
26 id: AgentId;
27 label: string;
28 /** The file the snippet goes in, or null for a command or server URL. */
29 file: string | null;
30 lang: "sh" | "json" | "yaml" | "text";
31 code: string;
32 /** What to do after, in a sentence or two; `code` in backticks. */
33 then: string;
34 /** The file it reads a repository's instructions from. */
35 instructions: string;
36};
37
38const json = (value: unknown) => JSON.stringify(value, null, 2);
39
40/** How each agent adds the MCP server at `mcp`. */
41export function agentsFor(mcp: string = MCP_URL): Record<AgentId, AgentSetup> {
42 return {
43 // On g1t.sh, g1t's plugin: it adds the MCP server itself (as
44 // plugin:g1t:g1t) and records sessions. A self-hosted g1t's server is
45 // added on its own.
46 "claude-code":
47 mcp === MCP_URL
48 ? {
49 id: "claude-code",
50 label: "Claude Code",
51 file: null,
52 lang: "sh",
53 code: PLUGIN_INSTALL,
54 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.",
55 instructions: "CLAUDE.md",
56 }
57 : {
58 id: "claude-code",
59 label: "Claude Code",
60 file: null,
61 lang: "sh",
62 code: `claude mcp add --transport http g1t ${mcp}`,
63 then: "Then run `/mcp` in Claude Code and choose g1t to sign in through your browser.",
64 instructions: "CLAUDE.md",
65 },
66 codex: {
67 id: "codex",
68 label: "Codex",
69 file: null,
70 lang: "sh",
71 code: `codex mcp add g1t --url ${mcp}\ncodex mcp login g1t`,
72 then: "The login opens your browser to sign in to g1t.",
73 instructions: "AGENTS.md",
74 },
75 opencode: {
76 id: "opencode",
77 label: "OpenCode",
78 file: "opencode.json",
79 lang: "json",
80 code: json({
81 $schema: "https://opencode.ai/config.json",
82 mcp: { g1t: { type: "remote", url: mcp, enabled: true } },
83 }),
84 then: "OpenCode signs you in through your browser when it first connects; `opencode mcp auth g1t` starts it by hand.",
85 instructions: "AGENTS.md",
86 },
87 gemini: {
88 id: "gemini", label: "Gemini CLI", file: null, lang: "sh",
89 code: `gemini mcp add --transport http g1t ${mcp}`,
90 then: "Start Gemini CLI and use `/mcp auth g1t` to sign in through your browser.", instructions: "GEMINI.md",
91 },
92 chatgpt: {
93 id: "chatgpt", label: "ChatGPT", file: null, lang: "text", code: mcp,
94 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.", instructions: "AGENTS.md",
95 },
96 "claude-chat": {
97 id: "claude-chat", label: "Claude chat", file: null, lang: "text", code: mcp,
98 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.", instructions: "",
99 },
100 grok: {
101 id: "grok", label: "Grok", file: null, lang: "text", code: mcp,
102 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.", instructions: "",
103 },
104 "open-webui": {
105 id: "open-webui", label: "Open WebUI", file: null, lang: "text", code: mcp,
106 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.", instructions: "",
107 },
108 librechat: {
109 id: "librechat", label: "LibreChat", file: "librechat.yaml", lang: "yaml",
110 code: `mcpServers:\n g1t:\n type: streamable-http\n url: ${JSON.stringify(mcp)}\n requiresOAuth: true`,
111 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.", instructions: "",
112 },
113 other: {
114 id: "other", label: "Other MCP", file: null, lang: "text", code: mcp,
115 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.", instructions: "AGENTS.md",
116 },
117 cursor: {
118 id: "cursor",
119 label: "Cursor",
120 file: ".cursor/mcp.json",
121 lang: "json",
122 code: json({ mcpServers: { g1t: { url: mcp } } }),
123 then: "Use `~/.cursor/mcp.json` for every project. Cursor signs you in through your browser when it first connects.",
124 instructions: "AGENTS.md",
125 },
126 };
127}
128
129/** The snippets for g1t.sh. */
130export const AGENTS: Record<AgentId, AgentSetup> = agentsFor();
131
132/** Opens Cursor with g1t ready to add, from Cursor's install-link format. */
133export function cursorInstallLink(mcp: string = MCP_URL): string {
134 const config = btoa(JSON.stringify({ url: mcp }));
135 return `cursor://anysphere.cursor-deeplink/mcp/install?name=g1t&config=${encodeURIComponent(config)}`;
136}
137
138export function parseAgent(value: unknown): AgentId | null {
139 return typeof value === "string" && (AGENT_IDS as readonly string[]).includes(value) ? (value as AgentId) : null;
140}
141
142type Store = Pick<Storage, "getItem" | "setItem">;
143
144/**
145 * The remembered choice, or the default. Storage can be missing or throw
146 * (private windows, blocked site data); that is the same as no choice.
147 */
148export function readAgentChoice(storage: () => Store | null | undefined, fallback: AgentId = DEFAULT_AGENT): AgentId {
149 try {
150 return parseAgent(storage()?.getItem(AGENT_STORAGE_KEY)) ?? fallback;
151 } catch {
152 return fallback;
153 }
154}
155
156/** Remembers the choice; false when it could not be kept. */
157export function writeAgentChoice(storage: () => Store | null | undefined, id: AgentId): boolean {
158 try {
159 const store = storage();
160 if (!store) return false;
161 store.setItem(AGENT_STORAGE_KEY, id);
162 return true;
163 } catch {
164 return false;
165 }
166}
167
168// --- The page's one choice, which every toggle follows ----------------------
169
170const browserStorage = () => (typeof window === "undefined" ? null : window.localStorage);
171let current: AgentId | null = null;
172const listeners = new Set<() => void>();
173
174export function subscribeAgentChoice(listener: () => void): () => void {
175 listeners.add(listener);
176 const onStorage = (event: StorageEvent) => {
177 if (event.key !== null && event.key !== AGENT_STORAGE_KEY) return;
178 current = parseAgent(event.newValue);
179 listener();
180 };
181 if (typeof window !== "undefined") {
182 window.addEventListener("storage", onStorage);
183 window.addEventListener("focus", listener);
184 }
185 return () => {
186 listeners.delete(listener);
187 if (typeof window !== "undefined") {
188 window.removeEventListener("storage", onStorage);
189 window.removeEventListener("focus", listener);
190 }
191 };
192}
193
194export function getAgentChoice(fallback: AgentId = DEFAULT_AGENT): AgentId {
195 return cookieAgent("g1t_agent") ?? current ?? readAgentChoice(browserStorage, fallback);
196}
197
198/** Only a public UI preference crosses subdomains; no account or provider details. */
199export function cookieAgent(name: string): AgentId | null {
200 if (typeof document === "undefined") return null;
201 try { return parseAgent(document.cookie.split(";").map(s => s.trim()).find(s => s.startsWith(name + "="))?.split("=")[1]); }
202 catch { return null; }
203}
204
205export function rememberAgentDefault(id: AgentId): void {
206 agentCookie("g1t_agent_default", id, 3600);
207}
208
209function agentCookie(name: string, id: AgentId, seconds: number): void {
210 if (typeof document === "undefined") return;
211 const domain = location.hostname === "g1t.sh" || location.hostname.endsWith(".g1t.sh") ? "; Domain=g1t.sh" : "";
212 try { document.cookie = `${name}=${id}; Path=/; Max-Age=${seconds}; SameSite=Lax${location.protocol === "https:" ? "; Secure" : ""}${domain}`; }
213 catch { /* Restricted storage must not prevent switching tabs. */ }
214}
215
216/** The first configured provider, in the same order as the workspace AI page. */
217export function agentForProviders(providers: readonly { provider: string }[]): AgentId {
218 switch (providers[0]?.provider) {
219 case "anthropic": case "anthropic_endpoint": return "claude-code";
220 case "openai": case "azure_openai": return "codex";
221 case "gemini": return "gemini";
222 case "xai": return "grok";
223 case "openai_endpoint": return "open-webui";
224 case undefined: return DEFAULT_AGENT;
225 default: return "opencode";
226 }
227}
228
229export function setAgentChoice(id: AgentId, storage: () => Store | null | undefined = browserStorage): void {
230 current = id;
231 agentCookie("g1t_agent", id, 31536000);
232 writeAgentChoice(storage, id);
233 for (const listener of listeners) listener();
234}