flagon-io/g1t

public

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/apps/web/app/lib/agent-setup.ts

141 lines4,735 bytesCodeBlame
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; keep them in step with `apps/docs/src/data/agents.ts`, the
8 * docs' copy.
9 */
10
11export const MCP_URL = "https://mcp.g1t.sh";
12
13export const AGENT_IDS = ["claude-code", "codex", "opencode", "cursor"] as const;
14export type AgentId = (typeof AGENT_IDS)[number];
15export const DEFAULT_AGENT: AgentId = "claude-code";
16/** Where the choice is kept in `localStorage`. */
17export const AGENT_STORAGE_KEY = "g1t:agent";
18
19export type AgentSetup = {
20 id: AgentId;
21 label: string;
22 /** The file the snippet goes in, or null for a terminal command. */
23 file: string | null;
24 lang: "sh" | "json";
25 code: string;
26 /** What to do after, in a sentence or two; `code` in backticks. */
27 then: string;
28 /** The file it reads a repository's instructions from. */
29 instructions: string;
30};
31
32const json = (value: unknown) => JSON.stringify(value, null, 2);
33
34export const AGENTS: Record<AgentId, AgentSetup> = {
35 "claude-code": {
36 id: "claude-code",
37 label: "Claude Code",
38 file: null,
39 lang: "sh",
40 code: `claude mcp add --transport http g1t ${MCP_URL}`,
41 then: "Then run `/mcp` in Claude Code and choose g1t to sign in through your browser.",
42 instructions: "CLAUDE.md",
43 },
44 codex: {
45 id: "codex",
46 label: "Codex",
47 file: null,
48 lang: "sh",
49 code: `codex mcp add g1t --url ${MCP_URL}\ncodex mcp login g1t`,
50 then: "The login opens your browser to sign in to g1t.",
51 instructions: "AGENTS.md",
52 },
53 opencode: {
54 id: "opencode",
55 label: "OpenCode",
56 file: "opencode.json",
57 lang: "json",
58 code: json({
59 $schema: "https://opencode.ai/config.json",
60 mcp: { g1t: { type: "remote", url: MCP_URL, enabled: true } },
61 }),
62 then: "OpenCode signs you in through your browser when it first connects; `opencode mcp auth g1t` starts it by hand.",
63 instructions: "AGENTS.md",
64 },
65 cursor: {
66 id: "cursor",
67 label: "Cursor",
68 file: ".cursor/mcp.json",
69 lang: "json",
70 code: json({ mcpServers: { g1t: { url: MCP_URL } } }),
71 then: "Use `~/.cursor/mcp.json` for every project. Cursor signs you in through your browser when it first connects.",
72 instructions: "AGENTS.md",
73 },
74};
75
76/** Opens Cursor with g1t ready to add, from Cursor's install-link format. */
77export function cursorInstallLink(): string {
78 const config = btoa(JSON.stringify({ url: MCP_URL }));
79 return `cursor://anysphere.cursor-deeplink/mcp/install?name=g1t&config=${encodeURIComponent(config)}`;
80}
81
82export function parseAgent(value: unknown): AgentId | null {
83 return typeof value === "string" && (AGENT_IDS as readonly string[]).includes(value) ? (value as AgentId) : null;
84}
85
86type Store = Pick<Storage, "getItem" | "setItem">;
87
88/**
89 * The remembered choice, or the default. Storage can be missing or throw
90 * (private windows, blocked site data); that is the same as no choice.
91 */
92export function readAgentChoice(storage: () => Store | null | undefined): AgentId {
93 try {
94 return parseAgent(storage()?.getItem(AGENT_STORAGE_KEY)) ?? DEFAULT_AGENT;
95 } catch {
96 return DEFAULT_AGENT;
97 }
98}
99
100/** Remembers the choice; false when it could not be kept. */
101export function writeAgentChoice(storage: () => Store | null | undefined, id: AgentId): boolean {
102 try {
103 const store = storage();
104 if (!store) return false;
105 store.setItem(AGENT_STORAGE_KEY, id);
106 return true;
107 } catch {
108 return false;
109 }
110}
111
112// --- The page's one choice, which every toggle follows ----------------------
113
114const browserStorage = () => (typeof window === "undefined" ? null : window.localStorage);
115let current: AgentId | null = null;
116const listeners = new Set<() => void>();
117
118export function subscribeAgentChoice(listener: () => void): () => void {
119 listeners.add(listener);
120 const onStorage = (event: StorageEvent) => {
121 if (event.key !== AGENT_STORAGE_KEY) return;
122 current = parseAgent(event.newValue) ?? DEFAULT_AGENT;
123 listener();
124 };
125 if (typeof window !== "undefined") window.addEventListener("storage", onStorage);
126 return () => {
127 listeners.delete(listener);
128 if (typeof window !== "undefined") window.removeEventListener("storage", onStorage);
129 };
130}
131
132export function getAgentChoice(): AgentId {
133 current ??= readAgentChoice(browserStorage);
134 return current;
135}
136
137export function setAgentChoice(id: AgentId, storage: () => Store | null | undefined = browserStorage): void {
138 current = id;
139 writeAgentChoice(storage, id);
140 for (const listener of listeners) listener();
141}