From 42d26a364a9495a81f6ead5c61aaec44c71e5681 Mon Sep 17 00:00:00 2001 From: Justin Massa Date: Sun, 18 Jan 2026 19:42:39 -0600 Subject: [PATCH] feat: async GChat webhook with Tailscale Funnel support - Change from execSync to exec for non-blocking AI calls - Acknowledge messages immediately, send response via Chat API - Add 5-minute timeout (vs 25s) since async doesn't block - Add start-gchat-tailscale.sh launcher for Tailscale Funnel - Remove ngrok dependency Co-Authored-By: Claude Opus 4.5 --- src/googlechat/run-webhook.ts | 102 ++++++++++++++++------------------ start-gchat-tailscale.sh | 26 +++++++++ 2 files changed, 74 insertions(+), 54 deletions(-) create mode 100755 start-gchat-tailscale.sh diff --git a/src/googlechat/run-webhook.ts b/src/googlechat/run-webhook.ts index 899bac8a7..16b4310b8 100644 --- a/src/googlechat/run-webhook.ts +++ b/src/googlechat/run-webhook.ts @@ -1,11 +1,27 @@ #!/usr/bin/env npx tsx -import { execSync } from "node:child_process"; +import { exec } from "node:child_process"; import express, { type Request, type Response } from "express"; const PORT = 18792; const app = express(); app.use(express.json()); +// Path to the gchat sender script +const GCHAT_SENDER = "/Users/justinmassa/chief-of-staff/scripts/gchat_sender.py"; +const PYTHON = "/Users/justinmassa/chief-of-staff/.venv/bin/python"; + +// Send message via Chat API (async, no timeout concerns) +function sendChatMessage(spaceId: string, text: string): void { + const escapedText = text.replace(/'/g, "'\\''").replace(/\n/g, "\\n"); + exec( + `${PYTHON} -c "import sys; sys.path.insert(0, '/Users/justinmassa/chief-of-staff/scripts'); from gchat_sender import send_message; send_message('${escapedText}', '${spaceId}')"`, + { timeout: 30000 }, + (err) => { + if (err) console.error("[googlechat] Failed to send response:", err.message); + } + ); +} + // Health check app.get("/health", (_req: Request, res: Response) => { res.json({ ok: true, provider: "googlechat" }); @@ -17,15 +33,10 @@ app.post("/webhook/googlechat", async (req: Request, res: Response) => { const event = req.body; const chat = event.chat || {}; - // Detect event type from payload structure const isAddedToSpace = !!chat.addedToSpacePayload; const isMessage = !!chat.messagePayload; - const eventType = isAddedToSpace - ? "ADDED_TO_SPACE" - : isMessage - ? "MESSAGE" - : "UNKNOWN"; + const eventType = isAddedToSpace ? "ADDED_TO_SPACE" : isMessage ? "MESSAGE" : "UNKNOWN"; console.log(`[googlechat] Received event: ${eventType}`); if (isAddedToSpace) { @@ -35,7 +46,7 @@ app.post("/webhook/googlechat", async (req: Request, res: Response) => { chatDataAction: { createMessageAction: { message: { - text: `Hello ${user}! I'm Clawdbot, your AI assistant. Send me a message and I'll respond!`, + text: `Hello ${user}! I'm Clawdette, your AI assistant. Send me a message and I'll respond!`, }, }, }, @@ -52,48 +63,36 @@ app.post("/webhook/googlechat", async (req: Request, res: Response) => { console.log(`[googlechat] Message from ${senderName}: ${text}`); - let responseText: string; - try { - // Use clawdbot CLI to get AI response - // Escape the text for shell - const escapedText = text.replace(/'/g, "'\\''"); - const sessionId = `googlechat:${spaceId}`; - const result = execSync( - `clawdbot agent --message '${escapedText}' --session-id '${sessionId}' --local`, - { - timeout: 25000, // 25 second timeout (Google Chat times out at ~30s) - encoding: "utf-8", - maxBuffer: 1024 * 1024, - }, - ); - responseText = - result.trim() || "I processed your message but have no response."; - console.log( - `[googlechat] AI Response: ${responseText.slice(0, 100)}...`, - ); - } catch (err: unknown) { - const error = err as { message?: string; killed?: boolean }; - console.error(`[googlechat] CLI error:`, error.message); - if (error.killed) { - responseText = - "Sorry, the request timed out. Please try a simpler question."; - } else { - responseText = - "Sorry, I encountered an error processing your message."; - } - } + // Acknowledge immediately - no blocking! + res.json({}); - res.json({ - hostAppDataAction: { - chatDataAction: { - createMessageAction: { - message: { - text: responseText, - }, - }, - }, + // Process AI response asynchronously (can take minutes, that's fine) + const escapedText = text.replace(/'/g, "'\\''"); + const sessionId = `googlechat:${spaceId}`; + + console.log(`[googlechat] Processing async for space ${spaceId}...`); + + exec( + `clawdbot agent --message '${escapedText}' --session-id '${sessionId}' --local`, + { + timeout: 300000, // 5 minute timeout + maxBuffer: 1024 * 1024, }, - }); + (err, stdout, stderr) => { + if (err) { + console.error(`[googlechat] AI error:`, err.message); + sendChatMessage(spaceId, "Sorry, I encountered an error processing your message."); + return; + } + + const responseText = stdout.trim() || "I processed your message but have no response."; + console.log(`[googlechat] AI Response (${responseText.length} chars): ${responseText.slice(0, 100)}...`); + + // Send response via Chat API + sendChatMessage(spaceId, responseText); + } + ); + return; } @@ -106,10 +105,5 @@ app.post("/webhook/googlechat", async (req: Request, res: Response) => { app.listen(PORT, () => { console.log(`[googlechat] Webhook server running on port ${PORT}`); - console.log( - `[googlechat] Local: http://localhost:${PORT}/webhook/googlechat`, - ); - console.log( - `[googlechat] Use ngrok URL + /webhook/googlechat for Google Chat config`, - ); + console.log(`[googlechat] Mode: ASYNC (responds via Chat API, no timeout issues)`); }); diff --git a/start-gchat-tailscale.sh b/start-gchat-tailscale.sh new file mode 100755 index 000000000..dcd28297f --- /dev/null +++ b/start-gchat-tailscale.sh @@ -0,0 +1,26 @@ +#!/bin/bash +# Start Clawdbot Google Chat with Tailscale Funnel +# Runs at login via LaunchAgent + +# Set PATH for launchd (which has minimal environment) +export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:$HOME/Library/pnpm:$PATH" + +cd "$(dirname "$0")" + +LOG_FILE="/tmp/googlechat-webhook.log" + +echo "$(date): Starting Clawdbot GChat..." >> "$LOG_FILE" + +# Kill any existing webhook server +pkill -f "run-webhook" 2>/dev/null +sleep 1 + +# Start webhook server +nohup npx tsx src/googlechat/run-webhook.ts >> "$LOG_FILE" 2>&1 & +sleep 3 + +# Ensure Tailscale Funnel is running on the right port +tailscale funnel --bg 18792 >> "$LOG_FILE" 2>&1 + +echo "$(date): GChat webhook started on port 18792" >> "$LOG_FILE" +echo "$(date): Funnel: https://justins-laptop.tail73ba30.ts.net/webhook/googlechat" >> "$LOG_FILE"