g1t/apps/web/public/install/setup.mjs
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.
| Record your own agent's sessions automatically | 1 | #!/usr/bin/env node |
| 2 | // Sets up g1t for Claude Code on this machine: signs in through the | |
| 3 | // browser (nothing to paste), keeps the token in ~/.g1t, and adds g1t's | |
| 4 | // hook to ~/.claude/settings.json so sessions are recorded onto the pull | |
| 5 | // requests they belong to. Run by https://g1t.sh/install/claude.sh. | |
| 6 | ||
| 7 | import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; | |
| 8 | import { homedir } from "node:os"; | |
| 9 | import { join } from "node:path"; | |
| 10 | ||
| 11 | const API = process.env.G1T_API ?? "https://api.g1t.sh"; | |
| 12 | const HOME = join(homedir(), ".g1t"); | |
| 13 | const CONFIG = join(HOME, "config.json"); | |
| 14 | const SETTINGS = join(homedir(), ".claude", "settings.json"); | |
| 15 | const HOOK = `node "${join(HOME, "hook.mjs")}"`; | |
| 16 | ||
| 17 | const sleep = (ms) => new Promise((done) => setTimeout(done, ms)); | |
| 18 | ||
| 19 | async function signIn() { | |
| 20 | if (existsSync(CONFIG)) { | |
| 21 | try { | |
| 22 | const config = JSON.parse(readFileSync(CONFIG, "utf8")); | |
| 23 | const me = await fetch(`${API}/user`, { headers: { authorization: `Bearer ${config.token}` } }); | |
| 24 | if (me.ok) { | |
| 25 | console.log(`Already signed in as ${(await me.json()).username}.`); | |
| 26 | return; | |
| 27 | } | |
| 28 | } catch {} | |
| 29 | } | |
| 30 | const started = await ( | |
| 31 | await fetch(`${API}/device/code`, { | |
| 32 | method: "POST", | |
| 33 | headers: { "content-type": "application/json" }, | |
| 34 | body: JSON.stringify({ client_name: "Claude Code on this machine" }), | |
| 35 | }) | |
| 36 | ).json(); | |
| 37 | console.log(`\nOpen ${started.verification_uri_complete}`); | |
| 38 | console.log(`and check that the code there is ${started.user_code}.\n`); | |
| 39 | const deadline = Date.now() + started.expires_in * 1000; | |
| 40 | while (Date.now() < deadline) { | |
| 41 | await sleep(Math.max(started.interval, 2) * 1000); | |
| 42 | const claim = await ( | |
| 43 | await fetch(`${API}/device/token`, { | |
| 44 | method: "POST", | |
| 45 | headers: { "content-type": "application/json" }, | |
| 46 | body: JSON.stringify({ device_code: started.device_code }), | |
| 47 | }) | |
| 48 | ).json(); | |
| 49 | if (claim.status === "approved") { | |
| 50 | mkdirSync(HOME, { recursive: true }); | |
| 51 | writeFileSync(CONFIG, JSON.stringify({ api: API, token: claim.token }, null, 2)); | |
| 52 | try { | |
| 53 | chmodSync(CONFIG, 0o600); | |
| 54 | } catch {} | |
| 55 | console.log(`Signed in as ${claim.username}.`); | |
| 56 | return; | |
| 57 | } | |
| 58 | if (claim.status === "denied" || claim.status === "expired") { | |
| 59 | throw new Error(`Sign-in was ${claim.status}.`); | |
| 60 | } | |
| 61 | } | |
| 62 | throw new Error("Sign-in timed out."); | |
| 63 | } | |
| 64 | ||
| 65 | /** Adds g1t's hook to Claude Code's settings, keeping everything else. */ | |
| 66 | function addHooks() { | |
| 67 | let settings = {}; | |
| 68 | try { | |
| 69 | settings = JSON.parse(readFileSync(SETTINGS, "utf8")); | |
| 70 | } catch {} | |
| 71 | settings.hooks ??= {}; | |
| 72 | for (const [event, matcher] of [ | |
| 73 | ["UserPromptSubmit", undefined], | |
| 74 | ["PostToolUse", "*"], | |
| 75 | ["Stop", undefined], | |
| 76 | ]) { | |
| 77 | const groups = (settings.hooks[event] ??= []); | |
| 78 | const present = groups.some((group) => group.hooks?.some((hook) => hook.command === HOOK)); | |
| 79 | if (!present) { | |
| 80 | groups.push({ ...(matcher ? { matcher } : {}), hooks: [{ type: "command", command: HOOK, timeout: 10 }] }); | |
| 81 | } | |
| 82 | } | |
| 83 | mkdirSync(join(homedir(), ".claude"), { recursive: true }); | |
| 84 | writeFileSync(SETTINGS, JSON.stringify(settings, null, 2)); | |
| 85 | console.log(`Added g1t's hook to ${SETTINGS}.`); | |
| 86 | } | |
| 87 | ||
| 88 | try { | |
| 89 | await signIn(); | |
| 90 | addHooks(); | |
| 91 | console.log("\nDone. Sessions in a g1t pull request's working copy are now recorded onto it."); | |
| 92 | console.log("To give Claude Code g1t's tools as well, run:"); | |
| 93 | console.log(" claude mcp add --transport http g1t https://mcp.g1t.sh\n"); | |
| 94 | } catch (error) { | |
| 95 | console.error(`g1t: ${error.message}`); | |
| 96 | process.exit(1); | |
| 97 | } |