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 <noreply@anthropic.com>
This commit is contained in:
Justin Massa 2026-01-18 19:42:39 -06:00
parent 26a7b915fb
commit 42d26a364a
2 changed files with 74 additions and 54 deletions

View File

@ -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)`);
});

26
start-gchat-tailscale.sh Executable file
View File

@ -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"