| 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 = ["claude-code", "codex", "opencode", "cursor", "gemini", "chatgpt", "claude-chat", "grok", "open-webui", "librechat", "other"] as const; |
| 20 | export type AgentId = (typeof AGENT_IDS)[number]; |
| 21 | export const DEFAULT_AGENT: AgentId = "claude-code"; |
| 22 | /** Where the choice is kept in `localStorage`. */ |
| 23 | export const AGENT_STORAGE_KEY = "g1t:agent"; |
| 24 | |
| 25 | export 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 | |
| 38 | const json = (value: unknown) => JSON.stringify(value, null, 2); |
| 39 | |
| 40 | /** How each agent adds the MCP server at `mcp`. */ |
| 41 | export 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. */ |
| 130 | export const AGENTS: Record<AgentId, AgentSetup> = agentsFor(); |
| 131 | |
| 132 | /** Opens Cursor with g1t ready to add, from Cursor's install-link format. */ |
| 133 | export 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 | |
| 138 | export function parseAgent(value: unknown): AgentId | null { |
| 139 | return typeof value === "string" && (AGENT_IDS as readonly string[]).includes(value) ? (value as AgentId) : null; |
| 140 | } |
| 141 | |
| 142 | type 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 | */ |
| 148 | export 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. */ |
| 157 | export 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 | |
| 170 | const browserStorage = () => (typeof window === "undefined" ? null : window.localStorage); |
| 171 | let current: AgentId | null = null; |
| 172 | const listeners = new Set<() => void>(); |
| 173 | |
| 174 | export 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 | |
| 194 | export 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. */ |
| 199 | export 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 | |
| 205 | export function rememberAgentDefault(id: AgentId): void { |
| 206 | agentCookie("g1t_agent_default", id, 3600); |
| 207 | } |
| 208 | |
| 209 | function 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. */ |
| 217 | export 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 | |
| 229 | export 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 | } |