| 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. */ |
| 14 | export 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. */ |
| 17 | export const PLUGIN_INSTALL = "claude plugin marketplace add https://g1t.sh/flagon-io/g1t-plugins.git\nclaude plugin install g1t@g1t"; |
| 18 | |
| 19 | export 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; |
| 32 | export type AgentId = (typeof AGENT_IDS)[number]; |
| 33 | export const DEFAULT_AGENT: AgentId = "claude-code"; |
| 34 | /** Where the choice is kept in `localStorage`. */ |
| 35 | export const AGENT_STORAGE_KEY = "g1t:agent"; |
| 36 | |
| 37 | export 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 | |
| 50 | const json = (value: unknown) => JSON.stringify(value, null, 2); |
| 51 | |
| 52 | /** How each agent adds the MCP server at `mcp`. */ |
| 53 | export 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. */ |
| 175 | export const AGENTS: Record<AgentId, AgentSetup> = agentsFor(); |
| 176 | |
| 177 | /** Opens Cursor with g1t ready to add, from Cursor's install-link format. */ |
| 178 | export 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 | |
| 183 | export function parseAgent(value: unknown): AgentId | null { |
| 184 | return typeof value === "string" && (AGENT_IDS as readonly string[]).includes(value) ? (value as AgentId) : null; |
| 185 | } |
| 186 | |
| 187 | type 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 | */ |
| 193 | export 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. */ |
| 202 | export 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 | |
| 215 | const browserStorage = () => (typeof window === "undefined" ? null : window.localStorage); |
| 216 | let current: AgentId | null = null; |
| 217 | const listeners = new Set<() => void>(); |
| 218 | |
| 219 | export 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 | |
| 239 | export 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. */ |
| 244 | export 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 | |
| 259 | export function rememberAgentDefault(id: AgentId): void { |
| 260 | agentCookie("g1t_agent_default", id, 3600); |
| 261 | } |
| 262 | |
| 263 | function 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. */ |
| 274 | export 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 | |
| 295 | export 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 | } |