AI coding agent specification. Human documentation in README.md. Read https://github.blog/ai-and-ml/generative-ai/spec-driven-development-using-markdown-as-a-programming-language-when-building-with-ai/ to understand the approach.
Keep the app in one file main.go.
Running github-brain without arguments starts the main interactive TUI. The only subcommand is mcp which starts the MCP server.
github-brain [-m <home>] # Start interactive TUI
github-brain mcp [args] # Start MCP server
If the GitHub Brain home directory doesn't exist, create it.
Concurrency control:
- Prevent concurrent
pulloperations using database lock - Return error if
pullalready running - Lock renewal: 1-second intervals
- Lock expiration: 5 seconds
- Release the lock when
pullfinishes mcpcommand can run concurrently
Use RFC3339 date format consistently.
Use https://pkg.go.dev/log/slog for logging (slog.Info, slog.Error). Do not use fmt.Println or log.Println.
When github-brain is run without arguments, display an interactive menu:
╭────────────────────────────────────────────────────────────────╮
│ GitHub Brain / 🏠 Home 1.0.0 │
│ │
│ ▶ � Pull Sync GitHub data to local database │
│ 🔧 Setup Configure GitHub username and organization │
│ 🚪 Exit Ctrl+C │
╰────────────────────────────────────────────────────────────────╯
After login but no organization configured:
╭────────────────────────────────────────────────────────────────╮
│ GitHub Brain / 🏠 Home 👤 @wham · 1.0.0 │
│ │
│ ▶ � Pull Sync GitHub data to local database │
│ 🔧 Setup Configure GitHub username and organization │
│ 🚪 Exit Ctrl+C │
╰────────────────────────────────────────────────────────────────╯
After successful login with organization configured:
╭────────────────────────────────────────────────────────────────╮
│ GitHub Brain / 🏠 Home 👤 @wham · 🏢 my-org · 1.0.0 │
│ │
│ ▶ 🔄 Pull Sync GitHub data to local database │
│ 🔧 Setup Configure GitHub username and organization │
│ 🚪 Exit Ctrl+C │
╰────────────────────────────────────────────────────────────────╯
The title bar contains:
- Left side:
GitHub Brain / <emoji> <screen> - Right side:
👤 @<username> · 🏢 <org> · <version>(right-aligned, version in dim style)
Right side components (shown only when available):
👤 @username- Shown when logged in🏢 org- Shown when organization is configured<version>- Always shown, in dim style
- Use arrow keys (↑/↓) or j/k to navigate
- Press Enter to select
- Press Esc to go back one screen (in submenus and dialogs)
- Press Ctrl+C to exit the app from any screen
- Highlight current selection with
▶(blue) - When going back, remember and restore the previous cursor position
- 🔄 Pull - Runs the pull operation (see pull section)
- 🔧 Setup - Opens the setup submenu (see Setup Menu section)
- 🚪 Exit - Exit the application (Ctrl+C)
- Start with Pull selected on first launch
- When returning from a submenu, restore the previous selection
- On startup, check if GITHUB_TOKEN exists and is valid
- Show menu with appropriate status in title bar
- When user selects Setup, show the setup submenu
- When user selects Pull, prompt for organization if not set, then run pull
- After pull completes or fails, show the standard "← Back" option (styled like Setup menu) and wait for Enter/Esc key, then return to menu
- When user selects Exit, exit cleanly
The Setup submenu provides authentication and configuration options:
╭────────────────────────────────────────────────────────────────╮
│ GitHub Brain / 🔧 Setup 👤 Not logged in │
│ │
│ ▶ ✨ Login with device Recommended for organization owners │
│ 🔑 Login with PAT Works without organization ownership │
│ 📝 Advanced Edit configuration file │
│ ← Back Esc │
╰────────────────────────────────────────────────────────────────╯
After login:
╭────────────────────────────────────────────────────────────────╮
│ GitHub Brain / 🔧 Setup 👤 @wham · 1.0.0 │
│ │
│ ▶ ✨ Login with device Recommended for organization owners │
│ 🔑 Login with PAT Works without organization ownership │
│ 🏢 Select organization Choose organization to sync │
│ 📝 Advanced Edit configuration file │
│ ← Back Esc │
╰────────────────────────────────────────────────────────────────╯
- ✨ Login with device - Recommended for organization owners. Runs the OAuth device flow (see OAuth Login section)
- 🔑 Login with PAT - Works without organization ownership. Manually enter a PAT (see PAT Login section)
- 🏢 Select organization - Choose organization to sync (see Select Organization section). Only shown when logged in
- 📝 Advanced - Edit configuration file
{HomeDir}/.env - ← Back - Return to main menu (Esc)
Opens the .env file located at {HomeDir}/.env using the system default editor:
- Use
opencommand on macOS - Use
xdg-opencommand on Linux - Create the file if it doesn't exist (empty file)
- Show brief message and return to setup menu
Use Bubble Tea framework (https://github.com/charmbracelet/bubbletea) for terminal UI:
- Core packages:
github.com/charmbracelet/bubbletea- TUI framework (Elm Architecture)github.com/charmbracelet/lipgloss- Styling and layoutgithub.com/charmbracelet/bubbles/spinner- Built-in animated spinners
- Architecture:
- Main menu is the root Bubble Tea model
- Login and Pull are sub-views that take over the screen temporarily
- Background goroutines send messages to update UI via
tea.Program.Send() - Framework handles all cursor positioning, screen clearing, and render batching
- Window resize events handled automatically via
tea.WindowSizeMsg
- Implementation:
UIProgressstruct wrapstea.Programand implementsProgressInterface- No manual ANSI escape codes or cursor management
- No Console struct needed - Bubble Tea handles everything
- Messages sent to model via typed message structs (e.g.,
itemUpdateMsg,logMsg)
- Graceful shutdown:
UIProgresshas adonechannel to track whenRun()completes- The goroutine running
Run()closes thedonechannel when finished Stop()callsQuit()then waits on thedonechannel before returning- This ensures alternate screen mode is properly exited and terminal state is restored
- Playful enhancements:
- Animated spinner using
bubbles/spinnerwith Dot style - Smooth color transitions for status changes (pending → active → complete)
- Celebration emojis at milestones (✨ at 1000+ items, 🎉 at 5000+)
- Right-aligned comma-formatted counters
- Animated spinner using
Interactive GitHub authentication using OAuth Device Flow. Stores the resulting token in the .env file.
The app uses a registered OAuth App for authentication:
- Client ID: Embedded in the binary (public, safe to commit)
- Client Secret: Not required for device flow (public clients)
- Scopes:
read:org repo(read organization data and full repository access)
-
Request device code from GitHub:
POST https://github.com/login/device/code client_id=<CLIENT_ID>&scope=read:org repo -
GitHub returns:
device_code: Secret code for pollinguser_code: Code user enters (e.g.,ABCD-1234)verification_uri:https://github.com/login/deviceexpires_in: Code expiration (usually 900 seconds)interval: Polling interval (usually 5 seconds)
-
Display the code and open browser:
╭────────────────────────────────────────────────────────────────╮ │ GitHub Brain / 🔧 Setup / ✨ Login with device 1.0.0 │ │ │ │ 1. Opening browser to https://github.com/login/device │ │ │ │ 2. Enter this code: │ │ │ │ ╭────────────────────╮ │ │ │ F934-7E83 │ │ │ ╰────────────────────╯ │ │ │ │ 3. Grant access to the organizations you are planning to use │ │ with GitHub Brain │ │ │ │ ⠋ Waiting for authorization... │ │ │ │ Press Esc to cancel │ │ │ ╰────────────────────────────────────────────────────────────────╯With user logged in (and organization configured):
╭────────────────────────────────────────────────────────────────╮ │ GitHub Brain / 🔧 Setup / ✨ Login with device 👤 @wham · 🏢 my-org · 1.0.0 │ │ │ │ 1. Opening browser to https://github.com/login/device │ │ │ │ 2. Enter this code: │ │ │ │ ╭────────────────────╮ │ │ │ F934-7E83 │ │ │ ╰────────────────────╯ │ │ │ │ 3. Grant access to the organizations you are planning to use │ │ with GitHub Brain │ │ │ │ ⠋ Waiting for authorization... │ │ │ │ Press Esc to cancel │ │ │ ╰────────────────────────────────────────────────────────────────╯ -
Poll for access token:
POST https://github.com/login/oauth/access_token client_id=<CLIENT_ID>&device_code=<DEVICE_CODE>&grant_type=urn:ietf:params:oauth:grant-type:device_code -
Handle poll responses:
authorization_pending: Keep pollingslow_down: Increase interval by 5 secondsexpired_token: Code expired, start overaccess_denied: User denied, show error- Success: Returns
access_token(long-lived, does not expire)
-
On success, save token to
.envfile and navigate to Select Organization screen (see Select Organization section) -
After organization is selected, return to Setup menu
Manual authentication using a Personal Access Token (PAT). Useful when OAuth flow is not available or when using fine-grained tokens.
-
Open browser to pre-filled PAT creation page:
https://github.com/settings/personal-access-tokens/new?name=github-brain&description=https%3A%2F%2Fgithub.com%2Fwham%2Fgithub-brain&issues=read&pull_requests=read&discussions=read&members=read -
Display token input screen:
╭────────────────────────────────────────────────────────────────╮ │ GitHub Brain / 🔧 Setup / 🔑 Login with PAT 1.0.0 │ │ │ │ 1. Opening browser to create new PAT at github.com │ │ │ │ 2. Set resource owner to the organization you want to use │ │ │ │ 3. Copy the PAT │ │ │ │ ▶ Paste the PAT and press Enter: █ │ │ │ │ ← Back Esc │ ╰────────────────────────────────────────────────────────────────╯With user logged in (and organization configured):
╭────────────────────────────────────────────────────────────────╮ │ GitHub Brain / 🔧 Setup / 🔑 Login with PAT 👤 @wham · 🏢 my-org · 1.0.0 │ │ │ │ 1. Opening browser to create new PAT at github.com │ │ │ │ 2. Set resource owner to the organization you want to use │ │ │ │ 3. Copy the PAT │ │ │ │ ▶ Paste the PAT and press Enter: █ │ │ │ │ ← Back Esc │ ╰────────────────────────────────────────────────────────────────╯ -
Verify the token by calling
viewer { login }GraphQL query -
On success, save token to
.envfile and navigate to Select Organization screen (see Select Organization section) -
After organization is selected, return to Setup menu
Save token and organization to {HomeDir}/.env file:
- If
.envexists and hasGITHUB_TOKEN, replace it - If
.envexists withoutGITHUB_TOKEN, append it - If
.envdoesn't exist, create it - Same logic for
ORGANIZATION
Format:
GITHUB_TOKEN=gho_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
ORGANIZATION=my-org
OAuth App tokens are long-lived and do not expire unless revoked.
- Use Bubble Tea for the interactive UI (consistent with
pullcommand) - Use
github.com/pkg/browserto open the verification URL - Use
github.com/charmbracelet/bubbles/textinputfor organization input - Poll interval: Start with GitHub's
intervalvalue (usually 5 seconds) - Timeout: Code expires after
expires_inseconds (usually 15 minutes) - After saving token, verify it works by fetching
viewer { login }
Allows user to select or change the organization to sync. This screen is accessible from the Setup menu (only shown when logged in) and is also shown automatically after successful login with device or PAT.
-
On entry, if
GITHUB_TOKENis available, fetch user's organizations via GraphQL (max 100, ordered alphabetically):{ viewer { organizations(first: 100, orderBy: { field: LOGIN, direction: ASC }) { nodes { login } } } } -
Display organization selection screen with selectable list and text input (show first 10 matches):
╭────────────────────────────────────────────────────────────────╮ │ GitHub Brain / 🔧 Setup / 🏢 Select organization 👤 @wham · 1.0.0 │ │ │ │ ▶ my-company │ │ open-source-org │ │ another-org │ │ │ │ Or enter manually: │ │ │ │ ▶ ← Back Esc │ ╰────────────────────────────────────────────────────────────────╯When "Enter manually" is selected (navigate down past the list):
╭────────────────────────────────────────────────────────────────╮ │ GitHub Brain / 🔧 Setup / 🏢 Select organization 👤 @wham · 1.0.0 │ │ │ │ my-company │ │ open-source-org │ │ another-org │ │ │ │ ▶ Or enter manually: █ │ │ │ │ ▶ ← Back Esc │ ╰────────────────────────────────────────────────────────────────╯When typing in the text input (filters from all 100 orgs, shows top 10 matches):
╭────────────────────────────────────────────────────────────────╮ │ GitHub Brain / 🔧 Setup / 🏢 Select organization 👤 @wham · 1.0.0 │ │ │ │ my-company │ │ │ │ ▶ Or enter manually: my█ │ │ │ │ ▶ ← Back Esc │ ╰────────────────────────────────────────────────────────────────╯When no matches (shows "Enter manually" instead of "Or enter manually"):
╭────────────────────────────────────────────────────────────────╮ │ GitHub Brain / 🔧 Setup / 🏢 Select organization 👤 @wham · 1.0.0 │ │ │ │ ▶ Enter manually: xyz█ │ │ │ │ ▶ ← Back Esc │ ╰────────────────────────────────────────────────────────────────╯If no organizations found:
╭────────────────────────────────────────────────────────────────╮ │ GitHub Brain / 🔧 Setup / 🏢 Select organization 👤 @wham · 1.0.0 │ │ │ │ No organizations found │ │ │ │ ▶ Enter manually: █ │ │ │ │ ▶ ← Back Esc │ ╰────────────────────────────────────────────────────────────────╯While loading organizations (with spinner):
╭────────────────────────────────────────────────────────────────╮ │ GitHub Brain / 🔧 Setup / 🏢 Select organization 👤 @wham · 1.0.0 │ │ │ │ ⠋ Loading organizations... │ │ │ │ ▶ ← Back Esc │ ╰────────────────────────────────────────────────────────────────╯ -
On selection (from list or text input):
- Save
ORGANIZATIONto.envfile - If accessed from Setup menu, return to Setup menu
- If accessed after login flow, continue to completion screen
- Save
- Use arrow keys (↑/↓) to navigate organization list (max 10 displayed)
- Navigate down past the list to select "Enter manually" option
- Typing only works when "Enter manually" is selected
- Typing filters from all organizations (up to 100), shows top 10 matches
- Press Enter to select highlighted organization or submit manual entry
- Press Esc to go back without changing
- Query organizations only when screen is entered (not cached)
- Show spinner while loading organizations
- Handle GraphQL errors gracefully - show "Enter custom name" option if query fails
- Use
github.com/charmbracelet/bubbles/textinputfor custom organization input - Limit to 10 organizations for clean UI display
Accessed from the main menu. Before starting pull:
- Check if
ORGANIZATIONis set in environment/.env - If not set, prompt user to enter organization name (similar to login flow)
- Save organization to
.envif entered - Proceed with pull operation
Config resolution:
Organization: From.envor prompted (required)GithubToken: From.env(required - redirect to login if missing)HomeDir: GitHub Brain home directory (default:~/.github-brain)DBDir: SQLite database path, constructed as<HomeDir>/dbExcludedRepositories: FromEXCLUDED_REPOSITORIESenv var (comma-separated, optional)
Operation:
- Verify no concurrent
pullexecution - Measure GraphQL request rate every second. Adjust
/spin speed based on rate - If
Config.Forceis set, remove all data from database before pulling. IfConfig.Itemsis set, only remove specified items - Pull items: Repositories, Discussions, Issues, Pull Requests
- Always pull all items (no selective sync from TUI)
- Maintain console output showing selected items and status
- Use
log/slogcustom logger for last 10 log messages with timestamps in console output - On completion or error, show the standard "← Back" option (styled like Setup menu) and wait for Enter/Esc key before returning to main menu
Bubble Tea handles all rendering automatically:
- No manual cursor management or screen clearing
- No debouncing or mutex locks needed
- Automatic terminal resize handling
- Smooth animations with
tea.Tick - Background goroutines send messages to update UI via channels
Console at the beginning of pull:
╭────────────────────────────────────────────────────────────────╮
│ GitHub Brain / 🔄 Pull 👤 @wham · 🏢 my-org · 1.0.0 │
│ │
│ 📋 Repositories │
│ 📋 Discussions │
│ 📋 Issues │
│ 📋 Pull Requests │
│ │
│ 📊 API Status ✅ 0 🟡 0 ❌ 0 │
│ 🚀 Rate Limit ? / ? used, resets ? │
│ │
│ 💬 Activity │
│ 21:37:12 ✨ Summoning data from the cloud... │
│ 21:37:13 🔍 Fetching current user info │
│ │
│ │
│ │
│ │
╰────────────────────────────────────────────────────────────────╯
Console during first item pull:
╭────────────────────────────────────────────────────────────────╮
│ GitHub Brain / 🔄 Pull 👤 @wham · 🏢 my-org · 1.0.0 │
│ │
│ ⠋ Repositories: 1,247 │
│ 📋 Discussions │
│ 📋 Issues │
│ 📋 Pull Requests │
│ │
│ 📊 API Status ✅ 120 🟡 1 ❌ 2 │
│ 🚀 Rate Limit 1,000 / 5,000 used, resets in 2h 15m │
│ │
│ 💬 Activity │
│ 21:37:54 📦 Wrangling repositories... │
│ 21:37:55 📄 Fetching page 12 │
│ 21:37:56 💾 Processing batch 3 (repos 201-300) │
│ 21:37:57 ⚡ Rate limit: 89% remaining │
│ 21:37:58 ✨ Saved 47 repositories to database │
│ │
╰────────────────────────────────────────────────────────────────╯
Console when first item completes:
╭────────────────────────────────────────────────────────────────╮
│ GitHub Brain / 🔄 Pull 👤 @wham · 🏢 my-org · 1.0.0 │
│ │
│ ✅ Repositories: 2,847 │
│ ⠙ Discussions: 156 │
│ 📋 Issues │
│ 📋 Pull Requests │
│ │
│ 📊 API Status ✅ 160 🟡 1 ❌ 2 │
│ 🚀 Rate Limit 1,500 / 5,000 used, resets in 1h 45m │
│ │
│ 💬 Activity │
│ 21:41:23 🎉 Repositories completed (2,847 synced) │
│ 21:41:24 💬 Herding discussions... │
│ 21:41:25 📄 Fetching from auth-service │
│ 21:41:26 💾 Processing batch 1 │
│ 21:41:27 ✨ Found 23 new discussions │
│ │
╰────────────────────────────────────────────────────────────────╯
Console when an error occurs:
╭────────────────────────────────────────────────────────────────╮
│ GitHub Brain / 🔄 Pull 👤 @wham · 🏢 my-org · 1.0.0 │
│ │
│ ✅ Repositories: 2,847 │
│ ❌ Discussions: 156 (errors) │
│ 📋 Issues │
│ 📋 Pull Requests │
│ │
│ 📊 API Status ✅ 160 🟡 1 ❌ 5 │
│ 🚀 Rate Limit 1,500 / 5,000 used, resets in 1h 45m │
│ │
│ 💬 Activity │
│ 21:42:15 ❌ API Error: Rate limit exceeded │
│ 21:42:16 ⏳ Retrying in 30 seconds... │
│ 21:42:47 ⚠️ Repository access denied: private-repo │
│ 21:42:48 ➡️ Continuing with next repository... │
│ 21:42:49 ❌ Failed to save discussion #4521 │
│ │
╰────────────────────────────────────────────────────────────────╯
- 📋 = Pending (not started)
- ⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏ = Spinner (active item, bright blue)
- ✅ = Completed (bright green)
- ❌ = Failed (bright red)
- Purple border color (#874BFD light / #7D56F4 dark)
- Responsive width:
max(76, terminalWidth - 4) - Box expands to full terminal width
- Numbers formatted with commas:
1,247 - Time format:
15:04:05(HH:MM:SS) - Rate limit: friendly format like
2h 15m - API Status: ✅ success, 🟡 warning (yellow circle), ❌ errors
- Note: Avoid
⚠️ emoji (has variation selector causing width issues)
Bubble Tea Message Handling:
- All UI updates use
tea.Program.Send()to send typed messages - Model's
Update()method processes messages and returns new state - View automatically re-renders when model changes
- No manual cursor or screen management needed
Box Drawing:
- Use standard lipgloss borders - no custom border painting or string manipulation
- Rounded borders (╭╮╰╯) styled with
lipgloss.RoundedBorder() - Title rendered as bold text inside the box, not embedded in border
- Static purple border color (#874BFD light / #7D56F4 dark)
- Responsive width:
max(64, terminalWidth - 4)
Spinners:
- Use
bubbles/spinnerwith Dot style (⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏) - Spinner state managed by Bubble Tea's
spinner.Model - Only one spinner shown at a time (for active item)
- Spinner ticks handled via
spinner.TickMsg
Number Formatting:
- Add commas to numbers > 999:
1,247not1247 - Helper function:
formatNumber(n int) string - Used in item counts, API stats, and rate limit display
Time Formatting:
- Activity logs:
15:04:05format (HH:MM:SS only) - Rate limit resets:
formatTimeRemaining()returns friendly format like "2h 15m"
Window Resize:
- Listen for
tea.WindowSizeMsgin model'sUpdate() - Store width/height in model state
- Layout adjusts automatically on next render
UI Style System:
Define styles once as global variables, reuse everywhere. Use semantic color names.
Colors (ANSI 256):
borderColor- AdaptiveColor#874BFDlight /#7D56F4dark (purple)accentColor- Color12(bright blue) - primary UI accentdimColor- Color240(gray)successColor- Color10(bright green)errorColor- Color9(bright red)warnColor- Color220(gold/yellow)
Text styles (global variables):
titleStyle- Bold, no color - for menu item names, section headersdimStyle- Foreground dimColor - for inactive/secondary textsuccessStyle- Foreground successColor - for success messages ✅errorStyle- Foreground errorColor - for error messages ❌accentStyle- Foreground accentColor - for active items, spinnersselectedStyle- Foreground accentColor + Bold - for selected item text, keyboard hints
Component styles (global variables):
selectorStyle- Foreground accentColor - for▶cursor indicator
Box helper function:
boxStyle(width int) lipgloss.Style- creates border style with rounded border, borderColor, padding 0/1, and specified width
Usage patterns:
- Menu selector:
selectorStyle.Render("▶") - Menu item name:
titleStyle.Render(name) - Menu item description (unselected):
dimStyle.Render(desc) - Menu item description (selected):
selectedStyle.Render(desc) - Keyboard hint:
selectedStyle.Render("Esc") - Spinner: Use
accentStyleas spinner style - Text input prompt: Use
accentStyleas prompt style
Milestone Celebrations:
- 1,000 items: ✨ emoji in log
- 5,000 items: 🎉 emoji in log
- 10,000 items: 🚀✨🎉 emojis in log
- Triggered in
itemCompleteMsghandler
- Get most recent
updated_attimestamp from database for repositories - Query repositories for
Config.Organization
{
organization(login: "<organization>") {
repositories(
isArchived: false
isFork: false
first: 100
after: null
orderBy: { field: UPDATED_AT, direction: DESC }
) {
nodes {
name
hasDiscussionsEnabled
hasIssuesEnabled
updatedAt
}
pageInfo {
hasNextPage
endCursor
}
}
}
}- Query repositories ordered by
updatedAtdescending - Stop pulling when hitting repository with
updatedAtolder than recorded timestamp - Save each repository immediately. Avoid storing all repositories in memory. No long-running transactions
- Save or update by primary key
name - Filter
isArchived: falseandisFork: falsefor faster processing - Ignore
Config.ExcludedRepositoriesin this step, OK to save them in the database
- Query discussions for each repository with
has_discussions_enabled: trueand not inConfig.ExcludedRepositories - Record most recent repository discussion
updated_attimestamp from database before pulling first page
{
repository(owner: "github", name: "licensing") {
discussions(first: 100, orderBy: { field: UPDATED_AT, direction: DESC }) {
nodes {
url
title
body
createdAt
updatedAt
author {
login
}
}
}
}
}- If provided repository doesn't exist, GraphQL will return following error:
{
"data": {
"repository": null
},
"errors": [
{
"type": "NOT_FOUND",
"path": [
"repository"
],
"locations": [
{
"line": 2,
"column": 3
}
],
"message": "Could not resolve to a Repository with the name 'github/non-existing-repository'."
}
]
}
- If repository doesn't exist, remove the repository, and all associated discussions, issues, and pull requests from the database and continue
- Query discussions ordered by most recent
updatedAt - Stop pulling when hitting discussions with
updatedAtolder than recorded timestamp - Save each discussion immediately. Avoid storing all discussions in memory. No long-running transactions
- Save or update by primary key
url - Preserve the discussion markdown body
- Query issues for each repository which has
has_issues_enabled: trueand not inConfig.ExcludedRepositories - Record most recent repository issue
updated_attimestamp from database before pulling first page
{
repository(owner: "<organiation>", name: "<repository>") {
issues(first: 100, orderBy: { field: UPDATED_AT, direction: DESC }) {
nodes {
url
title
body
createdAt
updatedAt
closedAt
author {
login
}
}
}
}
}- If provided repository doesn't exist, GraphQL will return following error:
{
"data": {
"repository": null
},
"errors": [
{
"type": "NOT_FOUND",
"path": [
"repository"
],
"locations": [
{
"line": 2,
"column": 3
}
],
"message": "Could not resolve to a Repository with the name 'github/non-existing-repository'."
}
]
}
- If repository doesn't exist, remove the repository, and all associated discussions, issues, and pull requests from the database and continue
- Query issues ordered by most recent
updatedAt - Stop pulling when hitting issue with
updatedAtolder than recorded timestamp - Only pull issues updated in the last 400 days. Stop pulling when hitting issue updated older than that
- Save each issue immediately. Avoid storing all issues in memory. No long-running transactions
- Save or update by primary key
url - Preserve the issue markdown body
- Query pull requests for each repository that's not in
Config.ExcludedRepositories - Record most recent repository pull request
updated_attimestamp from database before pulling first page
{
repository(owner: "<organiation>", name: "<repository>") {
pullRequests(first: 100, orderBy: { field: UPDATED_AT, direction: DESC }) {
nodes {
url
title
body
createdAt
updatedAt
closedAt
mergedAt
author {
login
}
}
}
}
}- If provided repository doesn't exist, GraphQL will return following error:
{
"data": {
"repository": null
},
"errors": [
{
"type": "NOT_FOUND",
"path": [
"repository"
],
"locations": [
{
"line": 2,
"column": 3
}
],
"message": "Could not resolve to a Repository with the name 'github/non-existing-repository'."
}
]
}
- If repository doesn't exist, remove the repository, and all associated discussions, issues, and pull requests from the database and continue
- Query pull requests ordered by most recent
updatedAt - Stop pulling when hitting pull request with
updatedAtolder than recorded timestamp - Only pull pull requests updated in the last 400 days. Stop pulling when hitting pull request updated older than that
- Save each pull request immediately. Avoid storing all pull requests in memory. No long-running transactions
- Save or update by primary key
url - Preserve the pull request markdown body
- Fetch the current authenticated user with:
{
viewer {
login
}
}- Truncate
searchFTS5 table and repopulate it fromdiscussions,issues, andpull_requeststables - When repopulating the search index:
- Use the current authenticateduser's login stored in memory
- Query for all unique repository names where the user is the author in
discussions,issues, orpull_requests - For each item being inserted into the search table, calculate
booston the fly:- Set to
2.0if the item's repository is in the user's contribution set (2x boost) - Set to
1.0otherwise (no boost)
- Set to
- Insert into search table with the calculated
boostvalue
- Fetch the current authenticated user before processing repositories
- This step always runs, even when using
-ito select specific items
{
viewer {
login
}
}- Store the username in memory for use during search index rebuild
- This is a single quick request that should complete immediately
- Use the official MCP SDK for Go: https://github.com/modelcontextprotocol/go-sdk
- Important: Pull library repository for docs/examples instead of using
go doc - Update library to latest version when modifying MCP code
- Use stdio MCP transport
- Implement only tools listed below
When parameters are required, use mcp.Required() to mark them as required. Do not include required in the parameter description.
Lists discussions with optional filtering. Discussions are separated by ---.
repository: Filter by repository name. Example:auth-service. Defaults to any repository in the organization.created_from: Filter bycreated_atafter the specified date. Example:2025-06-18T19:19:08Z. Defaults to any date.created_to: Filter bycreated_atbefore the specified date. Example:2025-06-18T19:19:08Z. Defaults to any date.authors: Array of author usernames. Example:[john_doe, jane_doe]. Defaults to any author.fields: Array of fields to include in the response. Available fields:["title", "url", "repository", "created_at", "author", "body"]. Defaults to all fields.
Validate fields parameter. If it contains invalid fields, output:
Invalid fields: <invalid_fields>
Use one of the available fields: <available_fields>
Where <invalid_fields> is a comma-separated list of invalid fields, and <available_fields> is a comma-separated list of available fields.
Next, prepare the query statement and execute. Order by created_at ascending. If no discussions are found, output:
No discussions found.
If discussions are found, start looping through them and output for each:
## <title>
- URL: <url>
- Repository: <repository>
- Created at: <created_at>
- Author: <author>
<body>
---
The example above includes all fields. If fields parameter is provided, only include those fields in the output.
While looping through discussions, keep track of the total size of the response. If the next discussions would take the response size over 990 kb, stop the loop. Prepend the response with:
Showing only the first <n> discussions. There's <x> more, please refine your search. Use `created_from` and `created_to` parameters
to narrow the results.
---
Where <n> is the number of discussions shown, and <x> is the number of discussions not shown.
Lists issues with optional filtering. Issues are separated by ---.
repository: Filter by repository name. Example:auth-service. Defaults to any repository in the organization.created_from: Filter bycreated_atafter the specified date. Example:2025-06-18T19:19:08Z. Defaults to any date.created_to: Filter bycreated_atbefore the specified date. Example:2025-06-18T19:19:08Z. Defaults to any date.closed_from: Filter byclosed_atafter the specified date. Example:2025-06-18T19:19:08Z. Defaults to any date.closed_to: Filter byclosed_atbefore the specified date. Example:2025-06-18T19:19:08Z. Defaults to any date.authors: Array of author usernames. Example:[john_doe, jane_doe]. Defaults to any author.fields: Array of fields to include in the response. Available fields:["title", "url", "repository", "created_at", "closed_at", "author", "status", "body"]. Defaults to all fields.
Validate fields parameter. If it contains invalid fields, output:
Invalid fields: <invalid_fields>
Use one of the available fields: <available_fields>
Where <invalid_fields> is a comma-separated list of invalid fields, and <available_fields> is a comma-separated list of available fields.
Next, prepare the query statement and execute. Order by created_at ascending. If no issues are found, output:
No issues found.
If issues are found, start looping through them and output for each:
## <title>
- URL: <url>
- Repository: <repository>
- Created at: <created_at>
- Closed at: <closed_at>
- Author: <author>
- Status: <open/closed>
<body>
---
The example above includes all fields. If fields parameter is provided, only include those fields in the output.
While looping through issues, keep track of the total size of the response. If the next issue would take the response size over 990 kb, stop the loop. Prepend the response with:
Showing only the first <n> issues. There's <x> more, please refine your search.
---
Where <n> is the number of issues shown, and <x> is the number of issues not shown.
Lists pull requests with optional filtering. Pull requests are separated by ---.
repository: Filter by repository name. Example:auth-service. Defaults to any repository in the organization.created_from: Filter bycreated_atafter the specified date. Example:2025-06-18T19:19:08Z. Defaults to any date.created_to: Filter bycreated_atbefore the specified date. Example:2025-06-18T19:19:08Z. Defaults to any date.merged_from: Filter bymerged_atafter the specified date. Example:2025-06-18T19:19:08Z. Defaults to any date.merged_to: Filter bymerged_atbefore the specified date. Example:2025-06-18T19:19:08Z. Defaults to any date.closed_from: Filter byclosed_atafter the specified date. Example:2025-06-18T19:19:08Z. Defaults to any date.closed_to: Filter byclosed_atbefore the specified date. Example:2025-06-18T19:19:08Z. Defaults to any date.authors: Array of author usernames. Example:[john_doe, jane_doe]. Defaults to any author.fields: Array of fields to include in the response. Available fields:["title", "url", "repository", "created_at", "merged_at", "closed_at", "author", "status", "body"]. Defaults to all fields.
Validate fields parameter. If it contains invalid fields, output:
Invalid fields: <invalid_fields>
Use one of the available fields: <available_fields>
Where <invalid_fields> is a comma-separated list of invalid fields, and <available_fields> is a comma-separated list of available fields.
Next, prepare the query statement and execute. Order by created_at ascending. If no pull requests are found, output:
No pull requests found.
If pull requests are found, output:
Total <x> pull requests found.
Next, start looping through them and output for each:
## <title>
- URL: <url>
- Repository: <repository>
- Created at: <created_at>
- Merged at: <merged_at>
- Closed at: <closed_at>
- Author: <author>
- Status: <open/closed>
<body>
---
The example above includes all fields. If fields parameter is provided, only include those fields in the output.
While looping through pull requests, keep track of the total size of the response. If the next pull request would take the response size over 990 kb, stop the loop. Prepend the response with:
Showing only the first <n> pull requests. There's <x> more, please refine your search.
---
Where <n> is the number of pull requests shown, and <x> is the number of pull requests not shown.
Full-text search across discussions, issues, and pull requests.
query: Search query string. Example:authentication bug. (required)fields: Array of fields to include in the response. Available fields:["title", "url", "repository", "created_at", "author", "type", "state", "body"]. Defaults to all fields.
Validate fields parameter. If it contains invalid fields, output:
Invalid fields: <invalid_fields>
Use one of the available fields: <available_fields>
Where <invalid_fields> is a comma-separated list of invalid fields, and <available_fields> is a comma-separated list of available fields.
Next, prepare the FTS5 search query using the search table. Build the query with:
- Use FTS5 MATCH operator for the search query
- Order by
bm25(search)for optimal relevance ranking (titles are weighted 5x higher in BM25 base scoring, then additional multipliers applied for state and recency) - Limit to 20 results
- Use the unified SearchEngine implementation shared with the UI
If no results are found, output:
No results found for "<query>".
If results are found, start looping through results and output for each:
## <title>
- URL: <url>
- Type: <type>
- Repository: <repository>
- Created at: <created_at>
- Author: <author>
- State: <state>
<body>
---
The example above includes all fields. If fields parameter is provided, only include those fields in the output.
For the body field, show the full content from the matched item.
Each prompt should just return the template string with parameter interpolation, and the MCP client will handle calling the actual tools.
Generates a summary of the user's accomplishments based on created discussions, closed issues, and closed pull requests.
username: Username. Example:john_doe. (required)period: Examples "last week", "from August 2025 to September 2025", "2024-01-01 - 2024-12-31"
Summarize the accomplishments of the user <username> during <period>, focusing on the most significant contributions first. Use the following approach:
- Use
list_discussionsto gather discussions they created within<period>. - Use
list_issuesto gather issues they closed within<period>. - Use
list_pull_requeststo gather pull requests they closed within<period>. - Aggregate all results, removing duplicates.
- Prioritize and highlight:
- Discussions (most important)
- Pull requests (next most important)
- Issues (least important)
- For each contribution, include a direct link and relevant metrics or facts.
- Present a concise, unified summary that mixes all types of contributions, with the most impactful items first.
Use GitHub's GraphQL API exclusively. Use https://github.com/shurcooL/githubv4 package. 100 results per page, max 100 concurrent requests (GitHub limit).
Implement comprehensive error handling with unified retry and recovery strategies:
Primary Rate Limit (Points System):
- GitHub uses a points-based system: 5,000 points per hour for personal access tokens
- Each query consumes points based on complexity (minimum 1 point)
- Track headers:
x-ratelimit-limit,x-ratelimit-remaining,x-ratelimit-used,x-ratelimit-reset - When exceeded: response status
200with error message,x-ratelimit-remaining=0 - Wait until
x-ratelimit-resettime before retrying
Secondary Rate Limits (Abuse Prevention):
- No more than 100 concurrent requests (shared across REST and GraphQL)
- No more than 2,000 points per minute for GraphQL endpoint
- No more than 90 seconds of CPU time per 60 seconds of real time
- When exceeded: status
200or403with error message - If
retry-afterheader present: wait that many seconds - If no
retry-after: wait at least 1 minute, then exponential backoff - GraphQL queries without mutations = 1 point, with mutations = 5 points (for secondary limits)
Unified Error Handling:
- Centralize all error handling in
handleGraphQLErrorfunction - Maximum 10 retries per request with exponential backoff (5s base, 30m max wait)
- Handle different error types:
- Primary rate limit: wait until
x-ratelimit-reset+ 30s buffer, retry indefinitely - Secondary rate limit: use
retry-afterheader or wait 1+ minutes with exponential backoff - Network errors (
EOF,connection reset,broken pipe,i/o timeout): wait 60-120s with jitter - 5xx server errors: exponential backoff retry
- Repository not found: no retry, remove from database
- Timeouts (>10 seconds): GitHub terminates request, additional points deducted next hour
- Primary rate limit: wait until
- Clear stale rate limit states after extended failures (>5 minutes)
- Reset HTTP client connection pool on persistent network failures
Proactive Management:
- Check global rate limit state before each request
- Add conservative delays between requests based on points utilization:
-
90% points used: 3-5 seconds delay
-
70% points used: 2-3 seconds delay
-
50% points used: 1-2 seconds delay
- Normal: 1-1.5 seconds delay (GitHub recommends 1+ second between mutations)
- During recovery: 5-10 seconds depending on error type
-
Concurrency and Timeouts:
- Limit concurrent requests to 50 using semaphore (conservative limit to prevent rate limiting)
- Global rate limit state shared across all goroutines with mutex protection
- Context cancellation support for all wait operations
- Request timeout: 10 seconds (GitHub's server timeout)
- Page-level timeout: 5 minutes
- Global operation timeout: 3 minutes for repository processing completion
Avoid making special request to get page count. For the first page request, you don't have to display the page count since you don't know it yet. For subsequent pages, you can display the page number in the status message.
SQLite database in {Config.DbDir}/{Config.Organization}.db (create folder if needed). Avoid transactions. Save each GraphQL item immediately. Use github.com/mattn/go-sqlite3 package. Build with FTS5 support.
The application uses a simple GUID-based versioning system to handle schema changes:
- Single schema version GUID for the entire database
- On any schema change, generate a new unique GUID
- At startup, check if stored schema GUID matches current GUID
- If different or missing, drop entire database and recreate from scratch
const SCHEMA_GUID = "550e8400-e29b-41d4-a716-446655440001" // Change this GUID on any schema modification- Check if database exists and has a
schema_versiontable - If table exists, read the stored GUID and compare with
SCHEMA_GUID - If GUID matches, proceed normally
- If GUID is different or missing:
- Log schema version mismatch
- Drop entire database file
- Create new database with current schema
- Store current
SCHEMA_GUIDinschema_versiontable
- Update table definitions, indexes, or constraints in code
- Generate new unique GUID and update
SCHEMA_GUIDconstant - On next startup, application detects GUID mismatch
- Drops entire database and recreates with new schema
- All data is re-fetched from GitHub APIs
-
Primary key:
url -
Index:
repository -
Index:
author -
Index:
created_at -
Index:
updated_at -
Index:
repository, created_at -
url: Primary key (e.g.,https://github.com/org/repo/discussions/1) -
title: Discussion title -
body: Discussion content -
created_at: Creation timestamp (e.g.,2023-01-01T00:00:00Z) -
updated_at: Last update timestamp -
repository: Repository name, without organization prefix (e.g.,repo) -
author: Username
-
Primary key:
url -
Index:
repository -
Index:
author -
Index:
created_at -
Index:
updated_at -
Index:
closed_at -
Index:
repository, created_at -
url: Primary key (e.g.,https://github.com/org/repo/issues/1) -
title: Issue title -
body: Issue content -
created_at: Creation timestamp (e.g.,2023-01-01T00:00:00Z) -
updated_at: Last update timestamp -
closed_at: Optional close timestamp (e.g.,2023-01-01T00:00:00Z). Null if open. -
repository: Repository name, without organization prefix (e.g.,repo) -
author: Username
-
Primary key:
url -
Index:
repository -
Index:
author -
Index:
created_at -
Index:
updated_at -
Index:
merged_at -
Index:
closed_at -
Index:
repository, created_at -
url: Pull request URL (e.g.,https://github.com/org/repo/pulls/1) -
title: Pull request title -
body: Pull request content -
created_at: Creation timestamp (e.g.,2023-01-01T00:00:00Z) -
updated_at: Last update timestamp -
merged_at: Optional merge timestamp (e.g.,2023-01-01T00:00:00Z). Null if not merged. -
closed_at: Optional close timestamp (e.g.,2023-01-01T00:00:00Z). Null if open. -
repository: Repository name, without organization prefix (e.g.,repo) -
author: Username
-
Primary key:
name -
Index:
updated_at -
name: Repository name (e.g.,repo), without organization prefix -
has_discussions_enabled: Boolean indicating if the repository has discussions feature enabled -
has_issues_enabled: Boolean indicating if the repository has issues feature enabled -
updated_at: Last update timestamp
- FTS5 virtual table for full-text search across discussions, issues, and pull requests
- Indexed columns:
type,title,body,url,repository,author - Unindexed columns:
created_at,state,boost boost: Numeric value (1.0 or 2.0) used to multiply BM25 scores for ranking, stored at index time (2.0 for user's contributed repos, 1.0 otherwise)- Uses
bm25(search, 1.0, 5.0, 1.0, 1.0, 1.0, 1.0)ranking with 5x title weight for relevance scoring - Search results should be ordered by:
(bm25(search) * boost * state_boost * recency_boost)for optimal relevance - Ranking factors:
- Title weight (5x): Title matches are weighted 5x higher than other fields in BM25 scoring (column order: type=1.0, title=5.0, body=1.0, url=1.0, repository=1.0, author=1.0)
- User-contributed repos (2x): Items from repositories where the user has contributed get 2x boost (stored in
boostcolumn at index time) - Open state (1.5x): Open items get 1.5x boost at query time:
CASE WHEN state = 'open' THEN 1.5 ELSE 1.0 END - Recency decay: Time-based decay calculated at query time based on
created_at:- Recent (<30 days): 1.0 (full score)
- Medium (30-180 days): 0.85
- Older (>180 days): 0.7
- SQL:
CASE WHEN julianday('now') - julianday(created_at) < 30 THEN 1.0 WHEN julianday('now') - julianday(created_at) < 180 THEN 0.85 ELSE 0.7 END
- Stores the current schema GUID for version tracking
- Single row table with
guidcolumn - Used to detect schema changes and trigger database recreation
Performance indexes are implemented to optimize common query patterns:
- Single-column indexes on
created_at,updated_at,closed_at,merged_atoptimize date range filtering andORDER BYoperations - Used by MCP tools for date-filtered queries (e.g.,
created_from,created_toparameters)
- Composite indexes on
(repository, created_at)optimize queries that filter by repository and sort by date - Critical for incremental sync operations using
MAX(updated_at)queries
- Index on
repositories.updated_atoptimizesMAX(updated_at)queries for determining last sync timestamps
Quick install (recommended):
npm install -g github-brainNPM handles:
- Platform detection (macOS, Linux, Windows)
- Architecture detection (x64, arm64)
- Automatic binary download via
optionalDependencies - PATH configuration
- Easy updates:
npm update -g github-brain - Easy uninstall:
npm uninstall -g github-brain
Manual installation: Download the appropriate archive for your platform from releases:
# Specific version
curl -L https://github.com/wham/github-brain/releases/download/v1.2.3/github-brain-darwin-arm64.tar.gz · tar xzUse go vet for code quality checks.
Running go vet:
# Standalone
go vet ./...
# Integrated with build (via scripts/run)
./scripts/run [command]CI Integration:
go vetruns automatically on all PRs via.github/workflows/build.yml- Build fails if
go vetfinds issues (blocking) - In local development (
scripts/run),go vetruns but is non-blocking to allow rapid iteration
Coded in .github/workflow/release.yml and .github/workflow/build.yml.
- Semantic Versioning: Automatically bump version on every merge to
mainbased on PR labels - PR Label Requirements (build fails without one of these):
major- Breaking changes, incompatible API changes (e.g., 1.0.0 → 2.0.0)minor- New features, backward compatible (e.g., 1.0.0 → 1.1.0)patch- Bug fixes, backward compatible (e.g., 1.0.0 → 1.0.1)
- Starting version: 1.0.0
- Version storage: GitHub releases (reads latest release tag, increments based on PR label)
- Release artifacts: GitHub release created with tag
v{version}(e.g., v1.2.3) - NPM packages: Main package and platform packages all published with same version
- NPM handles "latest": No need for GitHub "latest" release - npm automatically serves latest version
- Embed version and build date at compile time:
go build -ldflags "-X main.Version={semver} -X main.BuildDate=$(date -u +%Y-%m-%d)" - Display with
--version:github-brain 1.2.3 (2025-10-29) - NPM package version always matches binary version
Read https://github.com/mattn/go-sqlite3?tab=readme-ov-file#compiling to understand CGO requirements for SQLite FTS5 support.
darwin-amd64- Intel Macsdarwin-arm64- Apple Siliconlinux-amd64- x86_64 servers/desktopslinux-arm64- ARM servers (AWS Graviton, Raspberry Pi)windows-amd64- Windows machines
GitHub Releases:
- Archives:
github-brain-{platform}.tar.gz(Unix),.zip(Windows) - Executables inside archives:
github-brain(Unix),github-brain.exe(Windows) - Checksums:
SHA256SUMS.txtfor verification - Tagged with version:
v1.2.3
NPM Packages:
- Main package:
github-brain- contains Node.js shim and installation logic - Platform packages:
github-brain-{platform}-{arch}- contain platform-specific binariesgithub-brain-darwin-arm64github-brain-darwin-x64github-brain-linux-arm64github-brain-linux-x64github-brain-windows
- All packages published with same version number
Build System:
- Built natively on platform-specific GitHub Actions runners (ubuntu-latest, macos-latest, windows-latest)
- Linux ARM64 cross-compiled using
gcc-aarch64-linux-gnu - CGO build flags:
CGO_CFLAGS="-DSQLITE_ENABLE_FTS5",CGO_LDFLAGS="-lm"(Linux only)