daimon.email
ExamplesInbound

GitHub Repo Agent

Agent that monitors GitHub notification emails

Overview

A GitHub repo agent monitors GitHub notification emails, extracts meaningful events, and takes automated actions based on repository activity. This example demonstrates pattern matching on notification emails and extracting structured data.

Info

Perfect for automated PR reviews, issue triage, security alerts, or keeping stakeholders informed about repository changes.

How It Works

  1. Create an inbox for GitHub notifications
  2. Configure GitHub to send notifications to the inbox
  3. Poll or use webhooks to receive notifications
  4. Parse GitHub email patterns to extract event data
  5. Take automated actions (comment, label, notify)

Complete Implementation

import { DaimonClient } from 'daimon-email';
import { Octokit } from '@octokit/rest';

const client = new DaimonClient({ apiKey: process.env.DAIMON_API_KEY });
const github = new Octokit({ auth: process.env.GITHUB_TOKEN });

interface GitHubEvent {
  type: 'pull_request' | 'issue' | 'release' | 'security_alert' | 'mention';
  repo: string;
  number?: number;
  title: string;
  url: string;
  action: string;
}

// Step 1: Create inbox for GitHub notifications
async function setupGitHubAgent() {
  const inbox = await client.inboxes.create({
    username: 'github-monitor',
    clientId: 'github-agent-v1'
  });

  console.log(`GitHub notification inbox: ${inbox.address}`);
  console.log(`\nConfigure GitHub:`);
  console.log(`Settings → Notifications → Custom routing`);
  console.log(`Add: ${inbox.address}`);

  return inbox;
}

// Step 2: Poll for GitHub notifications
async function pollGitHubNotifications(inboxId: string) {
  const messages = await client.inboxes.messages.list(inboxId, {
    limit: 50,
    unread: true
  });

  for (const message of messages) {
    if (message.from.includes('notifications@github.com')) {
      await processGitHubNotification(message);
    }
  }
}

// Step 3: Parse GitHub notification patterns
function parseGitHubNotification(message: any): GitHubEvent | null {
  const subject = message.subject;
  const body = message.replyBody || message.body;

  // Pull request patterns
  const prMatch = subject.match(/\[(.+?)\] (.+?) \(#(\d+)\)/);
  if (prMatch) {
    const [, repo, title, number] = prMatch;
    return {
      type: 'pull_request',
      repo,
      number: parseInt(number),
      title,
      url: message.links.find(l => l.url.includes('/pull/'))?.url || '',
      action: detectAction(subject)
    };
  }

  // Issue patterns
  const issueMatch = subject.match(/\[(.+?)\] (.+?) \(#(\d+)\)/);
  if (issueMatch && !subject.includes('Pull Request')) {
    const [, repo, title, number] = issueMatch;
    return {
      type: 'issue',
      repo,
      number: parseInt(number),
      title,
      url: message.links.find(l => l.url.includes('/issues/'))?.url || '',
      action: detectAction(subject)
    };
  }

  // Security alerts
  if (subject.includes('security') || subject.includes('Dependabot')) {
    return {
      type: 'security_alert',
      repo: extractRepo(body),
      title: subject,
      url: message.ctaLinks[0]?.url || '',
      action: 'alert'
    };
  }

  // Mentions
  if (subject.includes('@') || body.includes('mentioned you')) {
    return {
      type: 'mention',
      repo: extractRepo(body),
      title: subject,
      url: message.links[0]?.url || '',
      action: 'mentioned'
    };
  }

  return null;
}

// Helper: Detect action type
function detectAction(subject: string): string {
  if (subject.includes('opened')) return 'opened';
  if (subject.includes('closed')) return 'closed';
  if (subject.includes('merged')) return 'merged';
  if (subject.includes('commented')) return 'commented';
  if (subject.includes('review requested')) return 'review_requested';
  if (subject.includes('approved')) return 'approved';
  return 'unknown';
}

// Helper: Extract repo name
function extractRepo(text: string): string {
  const match = text.match(/github\.com\/([^\/]+\/[^\/\s]+)/);
  return match ? match[1] : 'unknown';
}

// Step 4: Process GitHub events
async function processGitHubNotification(message: any) {
  const event = parseGitHubNotification(message);

  if (!event) {
    console.log(`Skipped non-GitHub notification: ${message.subject}`);
    return;
  }

  console.log(`GitHub event: ${event.type} - ${event.action} on ${event.repo}`);

  // Handle different event types
  switch (event.type) {
    case 'pull_request':
      await handlePullRequest(event);
      break;
    case 'issue':
      await handleIssue(event);
      break;
    case 'security_alert':
      await handleSecurityAlert(event);
      break;
    case 'mention':
      await handleMention(event);
      break;
  }
}

// Step 5: Automated actions based on events
async function handlePullRequest(event: GitHubEvent) {
  const [owner, repo] = event.repo.split('/');

  if (event.action === 'opened') {
    // Auto-label new PRs
    await github.issues.addLabels({
      owner,
      repo,
      issue_number: event.number!,
      labels: ['needs-review']
    });

    console.log(`Labeled PR #${event.number} as needs-review`);
  }

  if (event.action === 'review_requested') {
    // Notify team via Slack or other channel
    console.log(`Review requested for PR #${event.number}: ${event.title}`);
    // await notifySlack(`Review needed: ${event.url}`);
  }
}

async function handleIssue(event: GitHubEvent) {
  const [owner, repo] = event.repo.split('/');

  // Auto-triage issues
  if (event.action === 'opened') {
    console.log(`New issue #${event.number}: ${event.title}`);

    // Could auto-label based on content
    // await github.issues.addLabels({ ... });
  }
}

async function handleSecurityAlert(event: GitHubEvent) {
  console.log(`SECURITY ALERT: ${event.title}`);
  console.log(`URL: ${event.url}`);

  // High priority - notify immediately
  // await notifySlack(`🚨 Security alert: ${event.url}`);
  // await createPagerDutyIncident({ ... });
}

async function handleMention(event: GitHubEvent) {
  console.log(`You were mentioned in ${event.repo}: ${event.title}`);
  console.log(`URL: ${event.url}`);

  // Could auto-respond or add to review queue
}

// Run the agent
async function runGitHubAgent() {
  const inbox = await setupGitHubAgent();

  console.log('\nGitHub agent running...');

  // Poll every 60 seconds
  setInterval(async () => {
    await pollGitHubNotifications(inbox.id);
  }, 60000);

  // Initial poll
  await pollGitHubNotifications(inbox.id);
}

runGitHubAgent();
from daimon_email import DaimonClient
from github import Github
import os
import time
import re
from typing import Optional, Dict

client = DaimonClient(api_key=os.environ.get('DAIMON_API_KEY'))
github_client = Github(os.environ.get('GITHUB_TOKEN'))

# Step 1: Create inbox for GitHub notifications
def setup_github_agent():
    inbox = client.inboxes.create(
        username='github-monitor',
        client_id='github-agent-v1'
    )

    print(f"GitHub notification inbox: {inbox.address}")
    print(f"\nConfigure GitHub:")
    print(f"Settings → Notifications → Custom routing")
    print(f"Add: {inbox.address}")

    return inbox

# Step 2: Poll for GitHub notifications
def poll_github_notifications(inbox_id: str):
    messages = client.inboxes.messages.list(
        inbox_id,
        limit=50,
        unread=True
    )

    for message in messages:
        if 'notifications@github.com' in message['from']:
            process_github_notification(message)

# Step 3: Parse GitHub notification patterns
def parse_github_notification(message: Dict) -> Optional[Dict]:
    subject = message['subject']
    body = message.get('reply_body') or message.get('body')

    # Pull request patterns
    pr_match = re.search(r'\[(.+?)\] (.+?) \(#(\d+)\)', subject)
    if pr_match:
        repo, title, number = pr_match.groups()
        return {
            'type': 'pull_request',
            'repo': repo,
            'number': int(number),
            'title': title,
            'url': next((l['url'] for l in message['links'] if '/pull/' in l['url']), ''),
            'action': detect_action(subject)
        }

    # Issue patterns
    issue_match = re.search(r'\[(.+?)\] (.+?) \(#(\d+)\)', subject)
    if issue_match and 'Pull Request' not in subject:
        repo, title, number = issue_match.groups()
        return {
            'type': 'issue',
            'repo': repo,
            'number': int(number),
            'title': title,
            'url': next((l['url'] for l in message['links'] if '/issues/' in l['url']), ''),
            'action': detect_action(subject)
        }

    # Security alerts
    if 'security' in subject.lower() or 'Dependabot' in subject:
        return {
            'type': 'security_alert',
            'repo': extract_repo(body),
            'title': subject,
            'url': message['cta_links'][0]['url'] if message.get('cta_links') else '',
            'action': 'alert'
        }

    # Mentions
    if '@' in subject or 'mentioned you' in body:
        return {
            'type': 'mention',
            'repo': extract_repo(body),
            'title': subject,
            'url': message['links'][0]['url'] if message.get('links') else '',
            'action': 'mentioned'
        }

    return None

# Helper: Detect action type
def detect_action(subject: str) -> str:
    if 'opened' in subject:
        return 'opened'
    if 'closed' in subject:
        return 'closed'
    if 'merged' in subject:
        return 'merged'
    if 'commented' in subject:
        return 'commented'
    if 'review requested' in subject:
        return 'review_requested'
    if 'approved' in subject:
        return 'approved'
    return 'unknown'

# Helper: Extract repo name
def extract_repo(text: str) -> str:
    match = re.search(r'github\.com/([^/]+/[^/\s]+)', text)
    return match.group(1) if match else 'unknown'

# Step 4: Process GitHub events
def process_github_notification(message: Dict):
    event = parse_github_notification(message)

    if not event:
        print(f"Skipped non-GitHub notification: {message['subject']}")
        return

    print(f"GitHub event: {event['type']} - {event['action']} on {event['repo']}")

    # Handle different event types
    if event['type'] == 'pull_request':
        handle_pull_request(event)
    elif event['type'] == 'issue':
        handle_issue(event)
    elif event['type'] == 'security_alert':
        handle_security_alert(event)
    elif event['type'] == 'mention':
        handle_mention(event)

# Step 5: Automated actions based on events
def handle_pull_request(event: Dict):
    owner, repo = event['repo'].split('/')
    repo_obj = github_client.get_repo(f"{owner}/{repo}")

    if event['action'] == 'opened':
        # Auto-label new PRs
        issue = repo_obj.get_issue(event['number'])
        issue.add_to_labels('needs-review')

        print(f"Labeled PR #{event['number']} as needs-review")

    if event['action'] == 'review_requested':
        # Notify team via Slack or other channel
        print(f"Review requested for PR #{event['number']}: {event['title']}")
        # notify_slack(f"Review needed: {event['url']}")

def handle_issue(event: Dict):
    # Auto-triage issues
    if event['action'] == 'opened':
        print(f"New issue #{event['number']}: {event['title']}")

        # Could auto-label based on content
        # ...

def handle_security_alert(event: Dict):
    print(f"SECURITY ALERT: {event['title']}")
    print(f"URL: {event['url']}")

    # High priority - notify immediately
    # notify_slack(f"🚨 Security alert: {event['url']}")
    # create_pagerduty_incident({ ... })

def handle_mention(event: Dict):
    print(f"You were mentioned in {event['repo']}: {event['title']}")
    print(f"URL: {event['url']}")

    # Could auto-respond or add to review queue

# Run the agent
def run_github_agent():
    inbox = setup_github_agent()

    print('\nGitHub agent running...')

    while True:
        poll_github_notifications(inbox['id'])
        time.sleep(60)  # Poll every 60 seconds

if __name__ == '__main__':
    run_github_agent()

GitHub Notification Patterns

GitHub sends structured notification emails. Common patterns:

  • PR opened: [owner/repo] Title (#123)
  • Issue created: [owner/repo] Issue title (#456)
  • Review requested: [owner/repo] Review requested on PR (#123)
  • Security alert: Dependabot alert: [vulnerability] in owner/repo
  • Mentions: @username mentioned you in owner/repo#123

Advanced: CI/CD Integration

Combine with CI/CD actions:

async function handlePullRequest(event: GitHubEvent) {
  if (event.action === 'opened') {
    // Trigger custom CI checks
    await runCustomLintChecks(event.repo, event.number);

    // Auto-assign reviewers
    await assignReviewers(event.repo, event.number);
  }
}

Next Steps