Skip to content

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

14 Commits

Folders and files

Repository files navigation

Google Calendar Agent

A conversational assistant for your Google Calendar, in the terminal. Ask it what's on your schedule, book meetings, move them, cancel them — in plain English.

You: what do I have on Friday?
AI:  You have one meeting on 4 September: "test", 09:30–10:30 IST,
     with anubhav@example.com. Google Meet link included.

You: move it to 3pm and invite priya@example.com
AI:  Moved "test" to 15:00–16:00 IST and invited priya@example.com.

Built with LangGraph for the agent loop, Groq for inference, and the Google Calendar API — running on Bun.

Features

  • Read — list events in any time range, with free-text search
  • Create — book events with attendees, location, and an auto-generated Google Meet link
  • Update — patch an existing event without disturbing fields you didn't mention
  • Delete — remove events, with the agent confirming first
  • Multi-turn memory — follow-ups like "move it to 3pm" resolve against earlier turns
  • Date grounding — the agent is told the current date and timezone on every call, so "next Tuesday" means what you think it means

Prerequisites

  • Bun 1.3+
  • A Groq API key
  • A Google Cloud project with the Calendar API enabled

Setup

1. Install

git clone <your-repo-url>
cd google-calendar-agent
bun install

2. Create Google OAuth credentials

  1. In the Google Cloud Console, create or select a project.

  2. Enable the Google Calendar API for that project.

  3. Configure the OAuth consent screen. While it's in Testing, add your own Google account under Test users — otherwise the sign-in will be refused.

  4. Under Credentials, create an OAuth client ID of type Web application, and add this authorised redirect URI:

    http://localhost:3600/callback
    

3. Configure your environment

cp .env.example .env

Fill in .env:

Variable Description
GROQ_API_KEY From the Groq console
GOOGLE_CLIENT_ID From your OAuth client
GOOGLE_CLIENT_SECRET From your OAuth client
GOOGLE_CALLBACK_URL http://localhost:3600/callback

Bun loads .env automatically — no dotenv needed.

4. Authorise your calendar

bun run auth

Open http://localhost:3600/auth, grant access, and the tokens are written to token.json in the project root. Stop the server once you see the success page — this is a one-time step, and the refresh token keeps working afterwards.

token.json grants full read/write access to your calendar. It's gitignored; keep it that way.

5. Talk to it

bun run dev
Welcome to personal assistant. Type 'bye' to leave.
You: am I free tomorrow afternoon?

Type bye, exit, or quit to leave.

How it works

The agent is a LangGraph state machine with two nodes. assistant calls the model; if the reply contains tool calls, control moves to tools, which executes them and loops back so the model can read the results. When there are no tool calls left, the answer is returned.

  ┌───────────┐  tool calls   ┌───────┐
  │ assistant │ ────────────► │ tools │
  └───────────┘ ◄──────────── └───────┘
        │          results
        │ no tool calls
        ▼
       done

A MemorySaver checkpointer keeps each conversation's history under a thread id, so a follow-up question can refer back to what was said earlier.

Every call prepends a freshly built system prompt carrying the current date and IANA timezone. Language models have no clock — without this, a request for "this year" gets resolved against the model's training data rather than today.

Observability

Every run is traced to Langfuse — each graph node, model call and tool call, with inputs, outputs, latency and token counts.

Set the Langfuse variables in .env (self-hosted, or a cloud project):

LANGFUSE_PUBLIC_KEY=pk-lf-...
LANGFUSE_SECRET_KEY=sk-lf-...
LANGFUSE_BASE_URL=http://localhost:3000

src/observability.ts registers an OpenTelemetry tracer provider with Langfuse's span processor, and the CLI passes a CallbackHandler on every invocation. Each conversation gets one sessionId, shared with the checkpointer's thread id, so a trace lines up with the conversation it came from.

The agent still answers if the Langfuse keys are missing, but the SDK warns on startup and the export fails noisily on exit, so set the keys or remove the import.

Project layout

index.ts                      CLI entrypoint — the readline loop
src/
  agent/
    graph.ts                  LangGraph wiring, system prompt, checkpointer
    llm.ts                    Groq model with the tools bound
    tools/
      get-events.ts           events.list
      create-event.ts         events.insert, with a Meet link
      update-event.ts         events.patch
      delete-event.ts         events.delete
  google/
    oauth.ts                  OAuth client, consent URL, code exchange
    tokens.ts                 reads and writes token.json
    calendar.ts               authenticated Calendar client
  observability.ts            Langfuse tracing via OpenTelemetry
  server.ts                   one-shot OAuth callback server

The split is deliberate: agent/ knows nothing about Google, and google/ knows nothing about LangGraph. Swapping the model or adding a second calendar provider touches one side only.

Scripts

Command What it does
bun run dev Start the conversational CLI
bun run auth Start the OAuth server for the one-time sign-in
bun run typecheck Type-check without emitting

Design notes

A few decisions that aren't obvious from the code:

The Calendar client is a function, not a shared constant. Building it at module scope would need top-level await, so importing any tool would throw before the OAuth server could run. getCalendar() defers that to the moment a tool is called, where the failure becomes a message the agent can relay.

Updates use events.patch, not events.update. update replaces the whole event, which silently drops the Meet link, attendees, and description on every edit. The patch body is assembled key by key, because a field that is present but undefined clears it.

get-events sets singleEvents: true. The API rejects orderBy: "startTime" without it, since a recurring event has no single start time to sort by.

Tools return real error messages. A tool that swallows a failure and returns a generic string leaves the model unable to explain what went wrong or retry a fixable request.

Optional fields are genuinely optional. A required field the user didn't specify forces the model to invent a value — a required search term, for instance, silently filters out the events you were asking about.

Limitations

  • Single user — one token.json in the project root
  • Conversation history is in memory, so it resets when the process exits
  • Reads and writes the primary calendar only
  • No test suite yet

Licence

MIT

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages