A Bun + TypeScript agent that reads unread Gmail messages and streams structured insights over an AG-UI-compatible SSE endpoint.
- Connects to Gmail with OAuth2 (single-user env-based credentials)
- Fetches up to 20 unread emails from INBOX
- Extracts structured insight per email (priority, sentiment, action items, relationship, urgency)
- Drafts a reply for a specific Gmail message with thread-aware context
- Streams markdown insights as AG-UI events for
agent-ui
v1 is implemented and passing the full quality gate:
- Unread inbox fetch + parsing
- Insight extraction with the Vercel AI SDK and schema validation
- AG-UI SSE lifecycle (
RUN_STARTEDtoRUN_FINISHED) - Draft reply generation via
POST /draft-replywith schema-validated output - Health endpoint and request validation
- 100% test coverage and lint/type checks
- Bun 1.1+
- Google Cloud project with Gmail API enabled
- OAuth consent screen configured
- OAuth client credentials (Web application)
- Anthropic API key
- Install dependencies:
bun install- Create a local env file:
cp .env.example .env.local- Fill in:
GMAIL_CLIENT_IDGMAIL_CLIENT_SECRETANTHROPIC_API_KEY
- Generate your Gmail refresh token:
bun run scripts/setup-gmail-oauth.tsThe script opens the Google consent screen, waits for the callback on http://localhost:3456/oauth2callback, then prints GMAIL_REFRESH_TOKEN.
- Add the printed refresh token to
.env.local:
GMAIL_REFRESH_TOKEN=...Development:
bun run devProduction mode:
bun run startDefault port is 3001 (override with PORT).
GET /health->{ "status": "ok" }POST /agent-> AG-UI SSE stream (RUN_STARTED, text events,RUN_FINISHED); no request body requiredPOST /narrative-> AG-UI SSE stream with a concise 48-hour inbox brief and action items; request body is optional (runId,threadId) and malformed JSON safely falls back to defaultsPOST /draft-reply-> AG-UI SSE stream that drafts a reply for one email; requires JSON body:
{
"emailId": "gmail-message-id",
"runId": "optional-run-id",
"threadId": "optional-thread-id",
"voiceInstructions": "optional tone constraints"
}/draft-reply persists the generated reply as a Gmail draft in the same thread and returns gmailDraftId in RUN_FINISHED.result. It does not send email.
/narrative summarizes unread inbox messages in a rolling 48-hour window, emits one terminal SSE event per run (RUN_FINISHED for success/partial success, RUN_ERROR for hard failures), and returns timeframeHours plus actionItemCount in RUN_FINISHED.result.
GET /api-docs.json-> OpenAPI 3.0 specification in JSON formatGET /api-docs.md-> API documentation in Markdown format
The API documentation is automatically generated from the Zod schemas and includes all endpoints, request/response schemas, and descriptions. Access the docs at http://localhost:3001/api-docs.json or http://localhost:3001/api-docs.md.
- Add transient-retry and backoff for unstable Gmail/LLM calls
- Add startup env/config validation with actionable errors
- Add run telemetry (emails fetched, successful insights, skipped failures)
- Expand resilience tests (timeouts, partial failures, malformed payloads)
In ../agent-ui/agents.config.json, add:
{
"id": "gmail-insights-agent",
"name": "Gmail Insights Agent",
"endpoint_url": "http://localhost:3001/agent",
"description": "Reads unread Gmail and streams structured insights"
}Run the full local gate:
bun run checkRun formal verification for draft-reply safety models:
bun run formal:verifyformal:verify runs TLC for the /draft-reply no-send safety model and the abort-safety baseline model, then executes the abort-safety mutation check that must produce a counterexample trace.
Utility for posting Opencode session transcripts to GitHub PR comments:
bun run opencode:append-thread --pr <PR_NUMBER> [--format text|json]- Defaults to JSON mode when run via script default and supports
--json. - Supports
--pr,--repo, and--sessionoverrides. - Supports
--format textand--format json. - Automatically updates existing thread comments and removes stale split comments.
Required credentials:
GITHUB_TOKENorGH_TOKEN(or authenticatedghCLI)
Optional env:
OPENCODE_SESSION_IDOPENCODE_THREAD_FORMAT