g1t/apps/web/app/lib/agent-setup.ts

151 lines5,208 bytesCodeBlame

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

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

This file's history is long; its oldest lines are credited to the oldest commit read.