Skip to content

Add pluggable inbound-webhook framework + Granola meeting summarizer#4

Open
Miyamura80 wants to merge 2 commits into
mainfrom
claude/webhook-listener-meetings-56Ucc
Open

Add pluggable inbound-webhook framework + Granola meeting summarizer#4
Miyamura80 wants to merge 2 commits into
mainfrom
claude/webhook-listener-meetings-56Ucc

Conversation

@Miyamura80
Copy link
Copy Markdown
Contributor

@Miyamura80 Miyamura80 commented May 16, 2026

Summary

Introduces a small framework in webhooks/ for mounting custom inbound webhooks on the existing Bun.serve() instance, and ships the first source: a Granola "meeting finished" handler that streams a Codex summary into Slack.

  • Generic webhook framework. Each source is a factory (ctx: { bot }) => { path, handler } exported from webhooks/<name>.ts. index.ts mounts every factory under Bun.serve({ routes }). Adding a new source = drop a file + one line in index.ts.
  • Shared conventions (documented in CLAUDE.md):
    • Auth via X-Webhook-Secret: $WEBHOOK_SECRET.
    • Per-source dedup via Redis SET key NX EX 604800 (7-day TTL) — retried deliveries return 200 without re-processing.
    • Handlers return 200 immediately and process in the background, so emitter timeouts can't trigger retry storms while Codex is generating.
    • On background-processing failure, post a :warning: notice to the same Slack channel so issues are visible instead of silently swallowed.
  • Granola source (webhooks/granola.ts): POST /api/webhooks/granola accepts { meeting_id, title, transcript, notes?, attendees? }, asks Codex for a Slack-formatted wrap-up (header + 3-5 bullets + optional action items), and streams it into GRANOLA_SLACK_CHANNEL_ID (intended: #edison-os-updates) via bot.channel("slack:Cxxx").post(streamCodex(...)).
  • Refactor: extracted streamCodex out of index.ts into lib/codex.ts so onNewMention and webhook handlers share one implementation.

Files

  • webhooks/types.tsWebhookContext, WebhookRoute, WebhookFactory
  • webhooks/granola.ts — Granola handler
  • lib/codex.ts — shared streamCodex
  • index.ts — wires the webhooks array into Bun.serve({ routes })
  • .env.example — adds WEBHOOK_SECRET and GRANOLA_SLACK_CHANNEL_ID
  • CLAUDE.md — documents the inbound-webhook conventions

Before testing

  1. Add to .env:
    WEBHOOK_SECRET=<something long and random>
    GRANOLA_SLACK_CHANNEL_ID=<Slack channel ID of #edison-os-updates>
    
    (Find the channel ID in Slack: channel header → "View channel details" → scroll to bottom → "Channel ID".)
  2. Make sure local Redis is running (brew services start redis).
  3. Restart the bot: bun run index.ts. Startup logs should show POST /api/webhooks/granola mounted.
  4. The Mac mini runs the bot in tmux behind ngrok — confirm the ngrok tunnel is up if the Granola emitter posts from outside the LAN.

Test plan

  • Bot starts cleanly and logs both POST /api/webhooks/slack and POST /api/webhooks/granola.
  • Missing/invalid X-Webhook-Secret returns 401:
    curl -i -X POST http://localhost:3123/api/webhooks/granola \
      -H "Content-Type: application/json" \
      -d '{"meeting_id":"x","title":"x","transcript":"x"}'
  • Valid request returns 200 immediately and a streamed wrap-up lands in #edison-os-updates a few seconds later:
    curl -X POST http://localhost:3123/api/webhooks/granola \
      -H "X-Webhook-Secret: $WEBHOOK_SECRET" \
      -H "Content-Type: application/json" \
      -d '{"meeting_id":"test-001","title":"Test meeting","transcript":"Alice and Bob discussed shipping the new dashboard. Bob will draft the rollout plan by Friday.","attendees":["Alice","Bob"]}'
  • Replaying the same meeting_id returns 200 Duplicate (already processed) and does not post a second message.
  • Missing required fields returns 400.
  • Force a failure (e.g. temporarily set GRANOLA_SLACK_CHANNEL_ID to an ID the bot can't post to) and confirm a :warning: failure notice attempt is logged.
  • bunx tsc --noEmit is clean (already verified locally).
  • Existing @mention flow still streams a Codex reply (regression check on the streamCodex extraction).

Generated by Claude Code


Summary by cubic

Adds a pluggable inbound-webhook framework and the first source: a Granola “meeting finished” webhook that streams a Codex summary to Slack. Extracts Codex streaming to lib/codex.ts, mounts webhooks on the existing server, and hardens JSON validation and Codex exit handling to prevent silent failures.

  • New Features

    • Mount custom webhook handlers from webhooks/ via a simple factory and Bun.serve routes.
    • Shared conventions: X-Webhook-Secret auth, Redis SET NX EX 7d dedup, 200 immediate with background processing, and :warning: failure notices.
    • Granola endpoint POST /api/webhooks/granola accepts {meeting_id, title, transcript, notes?, attendees?} and posts a Slack-formatted wrap-up to GRANOLA_SLACK_CHANNEL_ID.
    • Startup logs list mounted routes for quick verification.
  • Bug Fixes

    • Reject non-object JSON bodies (null/arrays/primitives) before field access to avoid 500s.
    • Normalize attendees with Array.isArray before .join.
    • In lib/codex.ts, await the subprocess and throw on non-zero exit so errors surface instead of producing empty summaries.

Written for commit dc6d91c. Summary will update on new commits. Review in cubic

Introduces webhooks/ as a place to mount custom webhook listeners on the
same Bun.serve instance. First source is Granola: when a meeting wraps,
the emitter POSTs to /api/webhooks/granola and a Codex-generated summary
is streamed into the configured Slack channel.

- Shared X-Webhook-Secret auth and Redis NX/EX dedup per source
- Handler returns 200 immediately and processes in the background
- Failures post a ⚠️ notice to the same channel so issues are visible
- Extracted streamCodex into lib/codex.ts so mentions and webhooks share it
@qodo-code-review
Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 issues found across 6 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="webhooks/granola.ts">

<violation number="1" location="webhooks/granola.ts:42">
P2: Validate payload types before property access; non-object JSON can currently throw and return 500.</violation>

<violation number="2" location="webhooks/granola.ts:65">
P2: Normalize `attendees` to an array before joining; current code can throw before the catch block.</violation>
</file>

<file name="lib/codex.ts">

<violation number="1" location="lib/codex.ts:35">
P2: Validate the Codex subprocess exit code before returning; non-zero exits are currently treated as success.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
Fix all with cubic | Re-trigger cubic

Comment thread webhooks/granola.ts Outdated
Comment thread webhooks/granola.ts Outdated
Comment thread lib/codex.ts
Addresses three review issues from cubic on PR #4:

- webhooks/granola.ts: reject non-object JSON bodies (null, arrays,
  primitives) before field access, instead of letting a TypeError
  bubble up as a 500.
- webhooks/granola.ts: normalize `attendees` with Array.isArray before
  calling .join, so a malformed (non-array, non-empty) value can no
  longer throw outside the surrounding try/catch in the background
  task.
- lib/codex.ts: await the subprocess and throw on non-zero exit so
  silent codex failures surface as errors instead of empty summaries.
  Callers (webhook handler, onNewMention) now see the failure and can
  react.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants