feat: Voice v0.8.0 + feature discovery + Edge Function removal (#55)
* chore: remove Supabase Edge Function MCP deployment The Edge Function never worked reliably. All MCP traffic goes through self-hosted server + ngrok tunnel. Removes deploy-remote.sh, edge-entry.ts, supabase/functions/, .env.production.example, and CHATGPT.md (OAuth not implemented). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: rewrite MCP docs for self-hosted + ngrok deployment All per-client guides updated from Edge Function URLs to self-hosted server + ngrok tunnel pattern. DEPLOY.md rewritten with local vs remote paths. ALTERNATIVES.md now shows self-hosted as primary, with ngrok, Tailscale, and Fly.io/Railway comparison. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: voice recipe v0.8.0 — 25 production patterns from real deployment Identity separation, pre-computed bid system, conversation timing fix, proactive advisor mode, radical prompt compression, OpenAI Realtime Prompting Guide structure, auth-before-speech, brain escalation, stuck watchdog, never-hang-up rule, thinking sounds, fallback TwiML, tool set architecture, trusted user auth, caller routing, dynamic VAD, on-screen debug UI, live moment capture, belt-and-suspenders post-call, mandatory 3-step post-call, WebRTC parity, dual API events, report-aware query routing. WebRTC pseudocode updated with native FormData and 6 gotchas. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: post-upgrade feature discovery framework upgrade.ts captures old version before upgrading, then execs gbrain post-upgrade (new binary) to read migration files and print feature pitches. Migration files get YAML frontmatter with feature_pitch field (headline, description, recipe, tiers). CLI prints excited builder tone post-upgrade. v0.8.0 migration offers voice setup with environment detection (server vs local) and 3-tier progressive disclosure. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add Voice section to README with WebRTC screenshot + tweet link Her out of the box: voice-to-brain with 25 production patterns. WebRTC client screenshot embedded. Remote MCP section rewritten for self-hosted + ngrok. Setup block genericized. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: add recipe validation tests + genericize personal refs 5 new integration tests: secrets completeness, semver version, requires resolution, all-recipes-parse, no-personal-references. Test fixture genericized. CLAUDE.md/TODOS.md/SKILLPACK updated for v0.8.0. build:edge script removed from package.json. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: bump version and changelog (v0.8.0) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +0,0 @@
|
||||
# GBrain Remote MCP Server — Production Config
|
||||
# Copy to .env.production and fill in your values.
|
||||
|
||||
# Supabase pooler URL (Settings > Database > Connection string > Transaction pooler)
|
||||
# Use the transaction pooler (port 6543), NOT the direct connection.
|
||||
DATABASE_URL=postgresql://postgres.xxx:password@aws-0-us-west-1.pooler.supabase.com:6543/postgres
|
||||
|
||||
# OpenAI API key for embeddings
|
||||
OPENAI_API_KEY=sk-...
|
||||
|
||||
# Supabase project ref (the "xxx" from https://xxx.supabase.co)
|
||||
SUPABASE_PROJECT_REF=
|
||||
18
CHANGELOG.md
18
CHANGELOG.md
@@ -2,6 +2,24 @@
|
||||
|
||||
All notable changes to GBrain will be documented in this file.
|
||||
|
||||
## [0.8.0] - 2026-04-11
|
||||
|
||||
### Added
|
||||
|
||||
- **Your AI can answer the phone now.** Voice-to-brain v0.8.0 ships 25 production patterns from a real deployment. WebRTC works in a browser tab with just an OpenAI key, phone number via Twilio is optional. Your agent picks its own name and personality. Pre-computed engagement bids mean it greets you with something specific ("dude, your social radar caught something wild today"), not "how can I help you?" Context-first prompts, proactive advisor mode, caller routing, dynamic noise suppression, stuck watchdog, thinking sounds during tool calls. This is the "Her" experience, out of the box.
|
||||
- **Upgrade = feature discovery.** When you upgrade to v0.8.0, the CLI tells you what's new and your agent offers to set up voice immediately. WebRTC-first (zero setup), then asks about a phone number. Migration files now have YAML frontmatter with `feature_pitch` so every future version can pitch its headline feature through the upgrade flow.
|
||||
- **Remote MCP simplified.** The Supabase Edge Function deployment is gone. Remote MCP now uses a self-hosted server + ngrok tunnel. Simpler, more reliable, works with any AI client. All `docs/mcp/` guides updated to reflect the actual production architecture.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Voice recipe is now 25 production patterns deep.** Identity separation, pre-computed bid system, context-first prompts, proactive advisor mode, conversation timing (the #1 fix), no-repetition rule, radical prompt compression (13K to 4.7K tokens), OpenAI Realtime Prompting Guide structure, auth-before-speech, brain escalation, stuck watchdog, never-hang-up rule, thinking sounds, fallback TwiML, tool set architecture, trusted user auth, caller routing, dynamic VAD, on-screen debug UI, live moment capture, belt-and-suspenders post-call, mandatory 3-step post-call, WebRTC parity, dual API event handling, report-aware query routing.
|
||||
- **WebRTC session pseudocode updated.** Native FormData, `tools` in session config, `type: 'realtime'` on all session.update calls. WebRTC transcription NOT supported over data channel (use Whisper post-call).
|
||||
- **MCP docs rewritten.** All per-client guides (Claude Code, Claude Desktop, Cowork, Perplexity) updated from Edge Function URLs to self-hosted + ngrok pattern.
|
||||
|
||||
### Removed
|
||||
|
||||
- **Supabase Edge Function MCP deployment.** `scripts/deploy-remote.sh`, `supabase/functions/gbrain-mcp/`, `src/edge-entry.ts`, `.env.production.example`, `docs/mcp/CHATGPT.md` all removed. The Edge Function never worked reliably. Self-hosted + ngrok is the path.
|
||||
|
||||
## [0.7.0] - 2026-04-11
|
||||
|
||||
### Added
|
||||
|
||||
@@ -31,9 +31,8 @@ markdown files (tool-agnostic, work with both CLI and plugin contexts).
|
||||
- `src/core/search/` — Hybrid search: vector + keyword + RRF + multi-query expansion + dedup
|
||||
- `src/core/embedding.ts` — OpenAI text-embedding-3-large, batch, retry, backoff
|
||||
- `src/mcp/server.ts` — MCP stdio server (generated from operations)
|
||||
- `supabase/functions/gbrain-mcp/index.ts` — Remote MCP server (Supabase Edge Function)
|
||||
- `src/edge-entry.ts` — Curated bundle entry point for Edge Function (excludes fs-dependent modules)
|
||||
- `src/commands/auth.ts` — Standalone token management (create/list/revoke/test)
|
||||
- `src/commands/upgrade.ts` — Self-update CLI with post-upgrade feature discovery
|
||||
- `src/core/schema-embedded.ts` — AUTO-GENERATED from schema.sql (run `bun run build:schema`)
|
||||
- `src/schema.sql` — Full Postgres + pgvector DDL (source of truth, generates schema-embedded.ts)
|
||||
- `src/commands/integrations.ts` — Standalone integration recipe management (no DB needed)
|
||||
@@ -50,8 +49,8 @@ markdown files (tool-agnostic, work with both CLI and plugin contexts).
|
||||
- `docs/guides/quiet-hours.md` — Notification hold + timezone-aware delivery
|
||||
- `docs/guides/diligence-ingestion.md` — Data room to brain pages pipeline
|
||||
- `docs/designs/HOMEBREW_FOR_PERSONAL_AI.md` — 10-star vision for integration system
|
||||
- `scripts/deploy-remote.sh` — One-script remote MCP deployment
|
||||
- `docs/mcp/` — Per-client setup guides (Claude Desktop, Code, Cowork, Perplexity, ChatGPT)
|
||||
- `docs/mcp/` — Per-client setup guides (Claude Desktop, Code, Cowork, Perplexity)
|
||||
- `skills/migrations/` — Version migration files with feature_pitch YAML frontmatter
|
||||
- `openclaw.plugin.json` — ClawHub bundle plugin manifest
|
||||
|
||||
## Commands
|
||||
|
||||
45
README.md
45
README.md
@@ -18,7 +18,7 @@ GBrain is designed to be installed and operated by an AI agent. If you don't hav
|
||||
Copy this block into OpenClaw or Hermes Agent. The agent will install GBrain, set up the brain schema, import your files, configure all integrations, and verify everything works. GBrain needs a persistent agent that can run cron jobs and background sync.
|
||||
|
||||
```
|
||||
GOAL: I want Garry Tan's exact opinionated AI agent setup. Get me
|
||||
GOAL: I want an opinionated AI agent setup that actually works. Get me
|
||||
there. Install GBrain, set up the brain schema, import my files,
|
||||
configure all integrations, and verify everything works. Do not skip
|
||||
steps. Verify each step before proceeding. If a step fails, stop
|
||||
@@ -144,6 +144,28 @@ An agent without this loop answers from stale context. An agent with it gets sma
|
||||
> "Prep me for my meeting with Jordan in 30 minutes"
|
||||
> — pulls dossier, shared history, recent activity, open threads
|
||||
|
||||
## Voice: "Her" Out of the Box
|
||||
|
||||
The voice integration is the strongest demonstration of why a personal brain matters.
|
||||
Call a phone number. Your AI answers. It knows who's calling, pulls their full context
|
||||
from thousands of people pages, references your last meeting, and responds like someone
|
||||
who actually knows your world. When the call ends, a structured brain page appears with
|
||||
the transcript, entity detection, and cross-references.
|
||||
|
||||
This isn't a demo. It runs on a real phone number, screens unknown callers, and gets
|
||||
smarter with every call. Your agent picks its own name and personality. WebRTC works in
|
||||
a browser tab with zero setup. A real phone number is optional.
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/images/voice-client.png" alt="Voice client connected" width="300" />
|
||||
</p>
|
||||
|
||||
> [See it in action](https://x.com/garrytan/status/2043022208512172263)
|
||||
|
||||
The voice recipe ships with GBrain: [Voice-to-Brain](recipes/twilio-voice-brain.md).
|
||||
Your agent installs it, sets up the voice server, and you have a working AI phone line
|
||||
in 30 minutes. 25 production patterns from a real deployment included.
|
||||
|
||||
## How this happened
|
||||
|
||||
I was setting up my [OpenClaw](https://openclaw.ai) agent and started a markdown brain repo. One page per person, one page per company, compiled truth on top, append-only timeline on the bottom. The agent got smarter the more it knew, so I kept feeding it. Within a week I had 10,000+ markdown files, 3,000+ people with compiled dossiers, 13 years of calendar data, 280+ meeting transcripts, and 300+ captured original ideas.
|
||||
@@ -279,20 +301,23 @@ GBrain exposes 30 MCP tools via stdio. Add this to your MCP client config:
|
||||
|
||||
This gives your agent `get_page`, `put_page`, `search`, `query`, `add_link`, `traverse_graph`, `sync_brain`, `file_upload`, and 22 more tools. All generated from the same operation definitions as the CLI.
|
||||
|
||||
#### Remote MCP Server (Claude Desktop, Cowork, Perplexity, ChatGPT)
|
||||
#### Remote MCP Server (Claude Desktop, Cowork, Perplexity)
|
||||
|
||||
Access your brain from any device, any AI client. Deploy as a serverless endpoint on your existing Supabase instance:
|
||||
Access your brain from any device, any AI client. Run `gbrain serve` behind an HTTP
|
||||
server with a public tunnel:
|
||||
|
||||
```bash
|
||||
cp .env.production.example .env.production # fill in 3 values
|
||||
bash scripts/deploy-remote.sh # links, builds, deploys
|
||||
bun run src/commands/auth.ts create "claude-desktop" # get a token
|
||||
# Set up a public tunnel (see recipes/ngrok-tunnel.md)
|
||||
ngrok http 8787 --url your-brain.ngrok.app
|
||||
|
||||
# Create a bearer token for your client
|
||||
bun run src/commands/auth.ts create "claude-desktop"
|
||||
```
|
||||
|
||||
Then add to your AI client:
|
||||
- **Claude Code:** `claude mcp add gbrain -t http https://YOUR_REF.supabase.co/functions/v1/gbrain-mcp/mcp -H "Authorization: Bearer TOKEN"`
|
||||
- **Claude Desktop:** Settings > Integrations > Add (NOT JSON config)
|
||||
- **Perplexity Computer:** Settings > Connectors > Add remote MCP
|
||||
- **Claude Code:** `claude mcp add gbrain -t http https://your-brain.ngrok.app/mcp -H "Authorization: Bearer TOKEN"`
|
||||
- **Claude Desktop:** Settings > Integrations > Add (NOT JSON config, [details](docs/mcp/CLAUDE_DESKTOP.md))
|
||||
- **Perplexity:** Settings > Connectors > Add remote MCP ([details](docs/mcp/PERPLEXITY.md))
|
||||
|
||||
Per-client setup guides: [`docs/mcp/`](docs/mcp/DEPLOY.md)
|
||||
|
||||
@@ -603,7 +628,7 @@ ADMIN
|
||||
gbrain revert <slug> <version-id> Revert to previous version
|
||||
gbrain config [get|set] <key> [value] Brain config
|
||||
gbrain serve MCP server (stdio, local)
|
||||
scripts/deploy-remote.sh Deploy remote MCP server (Supabase Edge Functions)
|
||||
gbrain upgrade Self-update with feature discovery
|
||||
bun run src/commands/auth.ts Token management (create/list/revoke/test)
|
||||
gbrain call <tool> '<json>' Raw tool invocation
|
||||
gbrain --tools-json Tool discovery (JSON)
|
||||
|
||||
20
TODOS.md
20
TODOS.md
@@ -33,7 +33,7 @@
|
||||
**Depends on:** PGLite engine shipping (to have a real use case for the PR).
|
||||
|
||||
### ChatGPT MCP support (OAuth 2.1)
|
||||
**What:** Add OAuth 2.1 with Dynamic Client Registration to the Edge Function so ChatGPT can connect.
|
||||
**What:** Add OAuth 2.1 with Dynamic Client Registration to the self-hosted MCP server so ChatGPT can connect.
|
||||
|
||||
**Why:** ChatGPT requires OAuth 2.1 for MCP connectors. Bearer token auth is NOT supported. This is the only major AI client that can't use GBrain remotely.
|
||||
|
||||
@@ -41,9 +41,9 @@
|
||||
|
||||
**Cons:** OAuth 2.1 is a significant implementation: authorization endpoint, token endpoint, PKCE flow, dynamic client registration. Estimated CC: ~3-4 hours.
|
||||
|
||||
**Context:** Discovered during DX review (2026-04-10). All other clients (Claude Desktop/Code/Cowork, Perplexity) work with bearer tokens. See `docs/mcp/CHATGPT.md` for current status.
|
||||
**Context:** Discovered during DX review (2026-04-10). All other clients (Claude Desktop/Code/Cowork, Perplexity) work with bearer tokens. The Edge Function deployment was removed in v0.8.0. OAuth needs to be added to the self-hosted HTTP MCP server (or `gbrain serve --http` when implemented).
|
||||
|
||||
**Depends on:** v0.6.0 remote MCP server (shipped).
|
||||
**Depends on:** `gbrain serve --http` (not yet implemented).
|
||||
|
||||
## P1 (new from v0.7.0)
|
||||
|
||||
@@ -88,18 +88,18 @@
|
||||
|
||||
**Depends on:** v0.7.0 recipe format (shipped).
|
||||
|
||||
### Fly.io HTTP server as alternative deployment
|
||||
**What:** Add `gbrain serve --http` and a Dockerfile/fly.toml for users who prefer a traditional server over Edge Functions.
|
||||
### `gbrain serve --http` + Fly.io/Railway deployment
|
||||
**What:** Add `gbrain serve --http` as a thin HTTP wrapper around the stdio MCP server. Include a Dockerfile/fly.toml for cloud deployment.
|
||||
|
||||
**Why:** Avoids the Deno bundling seam. Bun runs natively. No 60s timeout. No cold start. Codex flagged the bundle strategy as "permanent maintenance tax."
|
||||
**Why:** The Edge Function deployment was removed in v0.8.0. Remote MCP now requires a custom HTTP wrapper around `gbrain serve`. A built-in `--http` flag would make this zero-effort. Bun runs natively, no bundling seam, no 60s timeout, no cold start.
|
||||
|
||||
**Pros:** Simpler code path, no edge-entry.ts needed, no Deno compat concerns. Supports sync_brain and file_upload remotely.
|
||||
**Pros:** Simpler remote MCP setup. Users run `gbrain serve --http` behind ngrok instead of building a custom server. Supports all 30 operations remotely (including sync_brain and file_upload).
|
||||
|
||||
**Cons:** Users need a Fly.io account. Not zero-infra.
|
||||
**Cons:** Users need ngrok ($8/mo) or a cloud host (Fly.io $5/mo, Railway $5/mo). Not zero-infra.
|
||||
|
||||
**Context:** From CEO review (2026-04-10). Edge Functions are the primary path. Fly.io is for power users who want full operation support remotely.
|
||||
**Context:** The production deployment at wintermute uses a custom Hono server wrapping `gbrain serve`. This TODO would formalize that pattern into the CLI. ChatGPT OAuth 2.1 support depends on this.
|
||||
|
||||
**Depends on:** v0.6.0 remote MCP server (shipped).
|
||||
**Depends on:** v0.8.0 (Edge Function removal shipped).
|
||||
|
||||
## Completed
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ Wiring up your life.
|
||||
|-------|---------------|
|
||||
| [Credential Gateway](integrations/credential-gateway.md) | ClawVisor / Hermes for Gmail, Calendar, Contacts |
|
||||
| [Meeting & Call Webhooks](integrations/meeting-webhooks.md) | Circleback transcripts + Quo/OpenPhone SMS/calls |
|
||||
| [Voice-to-Brain](../recipes/twilio-voice-brain.md) | Phone calls create brain pages via Twilio + OpenAI Realtime |
|
||||
| [Voice-to-Brain](../recipes/twilio-voice-brain.md) | Phone calls + WebRTC browser calls create brain pages. 25 production patterns: identity separation, bid system, conversation timing, proactive advisor, prompt compression, caller routing, dynamic VAD, real-time logging, belt-and-suspenders post-call |
|
||||
| [Email-to-Brain](../recipes/email-to-brain.md) | Gmail messages flow into entity pages via deterministic collector |
|
||||
| [X-to-Brain](../recipes/x-to-brain.md) | Twitter monitoring with deletion detection + engagement velocity |
|
||||
| [Calendar-to-Brain](../recipes/calendar-to-brain.md) | Google Calendar events become searchable daily brain pages |
|
||||
|
||||
BIN
docs/images/voice-client.png
Normal file
BIN
docs/images/voice-client.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 220 KiB |
@@ -1,54 +1,64 @@
|
||||
# Alternative: Self-Hosted MCP Server
|
||||
# Remote MCP Deployment Options
|
||||
|
||||
If you prefer running GBrain on your own machine instead of Supabase Edge Functions, you can expose `gbrain serve --http` via a tunnel.
|
||||
GBrain's MCP server runs via `gbrain serve` (stdio transport). To make it
|
||||
accessible from other devices and AI clients, you need an HTTP wrapper and
|
||||
a public tunnel. Here are your options.
|
||||
|
||||
## Tailscale Funnel
|
||||
## ngrok (recommended)
|
||||
|
||||
[Tailscale Funnel](https://tailscale.com/kb/1223/tailscale-funnel) gives you a permanent public HTTPS URL with automatic TLS. Free tier available.
|
||||
|
||||
```bash
|
||||
# 1. Install Tailscale
|
||||
brew install tailscale
|
||||
|
||||
# 2. Start gbrain with HTTP transport (when available)
|
||||
gbrain serve --http 3000
|
||||
|
||||
# 3. Expose via Funnel
|
||||
tailscale funnel 3000
|
||||
# Your brain is now at https://your-machine.ts.net
|
||||
```
|
||||
|
||||
Pros: zero deployment, no Deno bundling, no cold start, no timeout limits.
|
||||
Cons: requires your machine to be running and connected.
|
||||
|
||||
## ngrok
|
||||
|
||||
[ngrok](https://ngrok.com) provides temporary or persistent tunnels.
|
||||
[ngrok](https://ngrok.com) provides instant public tunnels. The Hobby tier
|
||||
($8/mo) gives you a fixed domain that never changes.
|
||||
|
||||
```bash
|
||||
# 1. Install ngrok
|
||||
brew install ngrok
|
||||
|
||||
# 2. Start gbrain with HTTP transport
|
||||
gbrain serve --http 3000
|
||||
# 2. Start your MCP server (behind an HTTP wrapper)
|
||||
# See docs/mcp/DEPLOY.md for the server setup
|
||||
|
||||
# 3. Expose via ngrok
|
||||
ngrok http 3000
|
||||
# Use the generated URL in your MCP client config
|
||||
ngrok http 8787 --url your-brain.ngrok.app
|
||||
```
|
||||
|
||||
Pros: quick setup, works behind firewalls.
|
||||
Cons: free tier URLs change on restart (paid tier for persistent URLs), requires running process.
|
||||
See the [ngrok-tunnel recipe](../../recipes/ngrok-tunnel.md) for full setup
|
||||
including auth token configuration and fixed domain setup.
|
||||
|
||||
## When to use alternatives vs Edge Functions
|
||||
## Tailscale Funnel
|
||||
|
||||
| | Edge Functions | Tailscale/ngrok |
|
||||
|--|---|---|
|
||||
| Works when laptop is off | Yes | No |
|
||||
| Zero cold start | No (~300ms) | Yes |
|
||||
| No timeout limits | No (60s) | Yes |
|
||||
| sync_brain remotely | No | Yes |
|
||||
| file_upload remotely | No | Yes |
|
||||
| Extra accounts needed | None | Tailscale or ngrok |
|
||||
[Tailscale Funnel](https://tailscale.com/kb/1223/tailscale-funnel) gives you
|
||||
a permanent public HTTPS URL with automatic TLS. Free tier available. Best for
|
||||
private networks where you control both endpoints.
|
||||
|
||||
Note: `gbrain serve --http` is planned but not yet implemented. Currently only stdio transport is available via `gbrain serve`.
|
||||
```bash
|
||||
# 1. Install Tailscale
|
||||
brew install tailscale
|
||||
|
||||
# 2. Expose your MCP server
|
||||
tailscale funnel 8787
|
||||
# Your brain is now at https://your-machine.ts.net
|
||||
```
|
||||
|
||||
## Fly.io / Railway (always-on)
|
||||
|
||||
For production deployments that need to run 24/7 without your machine:
|
||||
|
||||
- **Fly.io:** $5-10/mo, global edge, `fly deploy`
|
||||
- **Railway:** $5/mo, git push deploy
|
||||
|
||||
Both run Bun natively. No bundling, no Deno, no cold start, no timeout limits.
|
||||
|
||||
## Comparison
|
||||
|
||||
| | ngrok | Tailscale | Fly.io/Railway |
|
||||
|--|---|---|---|
|
||||
| Cost | $8/mo (Hobby) | Free | $5-10/mo |
|
||||
| Fixed URL | Yes (Hobby) | Yes | Yes |
|
||||
| Works when laptop is off | No | No | Yes |
|
||||
| Cold start | None | None | None |
|
||||
| Timeout limits | None | None | None |
|
||||
| All 30 operations | Yes | Yes | Yes |
|
||||
| Setup time | 5 min | 10 min | 15 min |
|
||||
|
||||
**Note:** `gbrain serve --http` (built-in HTTP transport) is planned but not yet
|
||||
implemented. Currently, remote MCP requires a custom HTTP wrapper around `gbrain serve`.
|
||||
See [DEPLOY.md](DEPLOY.md) for details.
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
# Connect GBrain to ChatGPT
|
||||
|
||||
**Status: Coming Soon**
|
||||
|
||||
ChatGPT requires OAuth 2.1 with Dynamic Client Registration for MCP connectors. Bearer token authentication is not supported by ChatGPT's MCP integration.
|
||||
|
||||
This is tracked as a P0 priority for GBrain v0.7.
|
||||
|
||||
## What's needed
|
||||
|
||||
- OAuth 2.1 authorization endpoint on the Edge Function
|
||||
- Token endpoint with PKCE flow
|
||||
- Dynamic Client Registration support
|
||||
- ChatGPT Developer Mode (available on Pro/Team/Enterprise/Edu plans)
|
||||
|
||||
## Workaround
|
||||
|
||||
Until OAuth support ships, you can use GBrain with ChatGPT via a bridge:
|
||||
|
||||
1. Run `gbrain serve` locally
|
||||
2. Use a tool like [mcp-remote](https://github.com/nichochar/mcp-remote) to bridge stdio to HTTP with OAuth support
|
||||
|
||||
## Timeline
|
||||
|
||||
Follow [Issue #22](https://github.com/garrytan/gbrain/issues/22) for updates on ChatGPT OAuth support.
|
||||
@@ -1,14 +1,27 @@
|
||||
# Connect GBrain to Claude Code
|
||||
|
||||
## Setup
|
||||
## Option 1: Local (recommended, zero server needed)
|
||||
|
||||
```bash
|
||||
claude mcp add gbrain -- gbrain serve
|
||||
```
|
||||
|
||||
That's it. Claude Code spawns `gbrain serve` as a stdio subprocess. No server, no
|
||||
tunnel, no token needed. Works with both PGLite and Supabase engines.
|
||||
|
||||
## Option 2: Remote (access from any machine)
|
||||
|
||||
If you have GBrain running on a server with a public tunnel (see
|
||||
[ngrok-tunnel recipe](../../recipes/ngrok-tunnel.md)):
|
||||
|
||||
```bash
|
||||
claude mcp add gbrain -t http \
|
||||
https://YOUR_REF.supabase.co/functions/v1/gbrain-mcp/mcp \
|
||||
https://YOUR-DOMAIN.ngrok.app/mcp \
|
||||
-H "Authorization: Bearer YOUR_TOKEN"
|
||||
```
|
||||
|
||||
Replace `YOUR_REF` with your Supabase project ref and `YOUR_TOKEN` with a token from `bun run src/commands/auth.ts create "claude-code"`.
|
||||
Replace `YOUR-DOMAIN` with your ngrok domain and `YOUR_TOKEN` with a token
|
||||
from `bun run src/commands/auth.ts create "claude-code"`.
|
||||
|
||||
## Verify
|
||||
|
||||
|
||||
@@ -2,27 +2,32 @@
|
||||
|
||||
Two ways to get GBrain into Cowork sessions:
|
||||
|
||||
## Option 1: Remote (via Edge Function)
|
||||
## Option 1: Remote (via self-hosted server + tunnel)
|
||||
|
||||
For Team/Enterprise plans, an org Owner adds the connector:
|
||||
|
||||
1. Go to **Organization Settings > Connectors**
|
||||
2. Add a new connector with the MCP server URL:
|
||||
```
|
||||
https://YOUR_REF.supabase.co/functions/v1/gbrain-mcp/mcp
|
||||
https://YOUR-DOMAIN.ngrok.app/mcp
|
||||
```
|
||||
3. Optionally add Bearer token authentication in Advanced Settings
|
||||
3. Add Bearer token authentication in Advanced Settings
|
||||
(create one with `bun run src/commands/auth.ts create "cowork"`)
|
||||
4. Save
|
||||
|
||||
Note: Cowork connects from Anthropic's cloud, not your device. The Edge Function is already publicly reachable via Supabase.
|
||||
Note: Cowork connects from Anthropic's cloud, not your device. Your server
|
||||
must be publicly reachable (ngrok, Tailscale Funnel, or cloud-hosted).
|
||||
|
||||
## Option 2: Local Bridge (via Claude Desktop)
|
||||
|
||||
If you already have GBrain configured in Claude Desktop (either via `gbrain serve` stdio or the remote MCP integration), Cowork gets access automatically. Claude Desktop bridges local MCP servers into Cowork via its SDK layer.
|
||||
If you already have GBrain configured in Claude Desktop (via `gbrain serve`
|
||||
stdio or a remote integration), Cowork gets access automatically. Claude
|
||||
Desktop bridges local MCP servers into Cowork via its SDK layer.
|
||||
|
||||
This means: if `gbrain serve` is running and configured in Claude Desktop, you don't need the Edge Function for Cowork at all.
|
||||
This means: if `gbrain serve` is running and configured in Claude Desktop,
|
||||
you don't need a separate server for Cowork.
|
||||
|
||||
## Which to use?
|
||||
|
||||
- **Remote Edge Function:** works even when your laptop is closed, available to all org members
|
||||
- **Remote server:** works even when your laptop is closed, available to all org members
|
||||
- **Local Bridge:** zero extra setup if Claude Desktop already has GBrain, but requires your machine to be running
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Connect GBrain to Claude Desktop
|
||||
|
||||
**Important:** Claude Desktop does NOT connect to remote MCP servers via `claude_desktop_config.json`. That file only works for local stdio servers. Remote HTTP servers must be added through the GUI.
|
||||
**Important:** Claude Desktop does NOT connect to remote MCP servers via
|
||||
`claude_desktop_config.json`. That file only works for local stdio servers.
|
||||
Remote HTTP servers must be added through the GUI.
|
||||
|
||||
## Setup
|
||||
|
||||
@@ -9,9 +11,12 @@
|
||||
3. Click **Add Integration** (or **Add Connector**)
|
||||
4. Enter the MCP server URL:
|
||||
```
|
||||
https://YOUR_REF.supabase.co/functions/v1/gbrain-mcp/mcp
|
||||
https://YOUR-DOMAIN.ngrok.app/mcp
|
||||
```
|
||||
Replace `YOUR-DOMAIN` with your ngrok domain (see
|
||||
[ngrok-tunnel recipe](../../recipes/ngrok-tunnel.md) for setup).
|
||||
5. Set authentication to **Bearer Token** and paste your token
|
||||
(create one with `bun run src/commands/auth.ts create "claude-desktop"`)
|
||||
6. Save
|
||||
|
||||
## Verify
|
||||
@@ -26,6 +31,9 @@ Claude Desktop will use your GBrain tools automatically.
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
**Using claude_desktop_config.json for remote servers** — this silently fails. The JSON config only works for local stdio MCP servers. Remote HTTP servers must be added via Settings > Integrations.
|
||||
**Using claude_desktop_config.json for remote servers** — this silently fails
|
||||
with no error message. The JSON config only works for local stdio MCP servers.
|
||||
Remote HTTP servers must be added via Settings > Integrations in the GUI.
|
||||
|
||||
**Using the wrong URL** — make sure the URL ends with `/mcp` (not `/health` or just the function name).
|
||||
**Using the wrong URL** — make sure the URL ends with `/mcp` (not `/health`
|
||||
or just the base domain).
|
||||
|
||||
@@ -1,39 +1,51 @@
|
||||
# Deploy GBrain Remote MCP Server
|
||||
|
||||
Deploy your personal knowledge brain as a serverless MCP endpoint on your existing Supabase instance. Works with Claude Desktop, Claude Code, Cowork, and Perplexity Computer.
|
||||
Access your brain from any device, any AI client. GBrain's MCP server runs locally
|
||||
via `gbrain serve` (stdio). For remote access, wrap it in an HTTP server behind a
|
||||
public tunnel.
|
||||
|
||||
## Prerequisites
|
||||
## Two Paths
|
||||
|
||||
- GBrain already set up (`gbrain init` completed, data imported)
|
||||
- [Supabase CLI](https://supabase.com/docs/guides/cli) installed
|
||||
- Your Supabase project ref (the `xxx` from `https://xxx.supabase.co`)
|
||||
|
||||
## Quick Start
|
||||
### Local (zero setup)
|
||||
|
||||
```bash
|
||||
# 1. Fill in your config
|
||||
cp .env.production.example .env.production
|
||||
# Edit .env.production with your DATABASE_URL, OPENAI_API_KEY, SUPABASE_PROJECT_REF
|
||||
|
||||
# 2. Deploy (one command)
|
||||
bash scripts/deploy-remote.sh
|
||||
|
||||
# 3. Create an access token
|
||||
DATABASE_URL=$DATABASE_URL bun run src/commands/auth.ts create "my-client"
|
||||
# Save the token — it's shown once
|
||||
|
||||
# 4. Test it
|
||||
bun run src/commands/auth.ts test \
|
||||
https://YOUR_REF.supabase.co/functions/v1/gbrain-mcp/mcp \
|
||||
--token YOUR_TOKEN
|
||||
gbrain serve
|
||||
```
|
||||
|
||||
## Authentication
|
||||
Works with Claude Code, Cursor, Windsurf, and any MCP client that supports stdio.
|
||||
No server, no tunnel, no token needed.
|
||||
|
||||
GBrain uses bearer tokens stored in your database (SHA-256 hashed). Each token has a name for identification.
|
||||
### Remote (any device, any AI client)
|
||||
|
||||
```
|
||||
Your AI client (Claude Desktop, Perplexity, etc.)
|
||||
→ ngrok tunnel (https://YOUR-DOMAIN.ngrok.app)
|
||||
→ Your HTTP server (wraps gbrain serve)
|
||||
→ Supabase Postgres (via pooler connection string)
|
||||
```
|
||||
|
||||
This requires:
|
||||
1. A machine running `gbrain serve` behind an HTTP wrapper
|
||||
2. A public tunnel (ngrok, Tailscale, or cloud host)
|
||||
3. Bearer token auth for security
|
||||
|
||||
## Remote Setup
|
||||
|
||||
### 1. Set up the tunnel
|
||||
|
||||
See the [ngrok-tunnel recipe](../../recipes/ngrok-tunnel.md) for full setup.
|
||||
Quick version:
|
||||
|
||||
```bash
|
||||
# Create a token
|
||||
brew install ngrok
|
||||
ngrok config add-authtoken YOUR_TOKEN
|
||||
ngrok http 8787 --url your-brain.ngrok.app # Hobby tier for fixed domain
|
||||
```
|
||||
|
||||
### 2. Create access tokens
|
||||
|
||||
```bash
|
||||
# Create a token for each client
|
||||
bun run src/commands/auth.ts create "claude-desktop"
|
||||
|
||||
# List all tokens
|
||||
@@ -43,50 +55,48 @@ bun run src/commands/auth.ts list
|
||||
bun run src/commands/auth.ts revoke "claude-desktop"
|
||||
```
|
||||
|
||||
Tokens are per-client. Create one for each device/app. Revoke individually if compromised.
|
||||
Tokens are per-client. Create one for each device/app. Revoke individually
|
||||
if compromised. Tokens are stored SHA-256 hashed in your database.
|
||||
|
||||
## Updating
|
||||
### 3. Connect your AI client
|
||||
|
||||
When you update GBrain (new operations, bug fixes):
|
||||
- **Claude Code:** [setup guide](CLAUDE_CODE.md)
|
||||
- **Claude Desktop:** [setup guide](CLAUDE_DESKTOP.md) (must use GUI, not JSON config)
|
||||
- **Claude Cowork:** [setup guide](CLAUDE_COWORK.md)
|
||||
- **Perplexity:** [setup guide](PERPLEXITY.md)
|
||||
|
||||
### 4. Verify
|
||||
|
||||
```bash
|
||||
git pull
|
||||
bash scripts/deploy-remote.sh
|
||||
```
|
||||
|
||||
Your tokens survive upgrades. Check your deployed version:
|
||||
|
||||
```bash
|
||||
curl https://YOUR_REF.supabase.co/functions/v1/gbrain-mcp/health
|
||||
bun run src/commands/auth.ts test \
|
||||
https://YOUR-DOMAIN.ngrok.app/mcp \
|
||||
--token YOUR_TOKEN
|
||||
```
|
||||
|
||||
## Operations
|
||||
|
||||
All 28 GBrain operations are available remotely except:
|
||||
- `sync_brain` (may exceed 60s Edge Function timeout)
|
||||
- `file_upload` (may exceed 60s timeout with large files)
|
||||
All 30 GBrain operations are available remotely, including `sync_brain` and
|
||||
`file_upload` (no timeout limits with self-hosted server).
|
||||
|
||||
These remain CLI-only via `gbrain serve` (stdio).
|
||||
## Deployment Options
|
||||
|
||||
See [ALTERNATIVES.md](ALTERNATIVES.md) for a comparison of ngrok, Tailscale
|
||||
Funnel, and cloud hosts (Fly.io, Railway).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**"supabase: command not found"**
|
||||
Install: `brew install supabase/tap/supabase` or `npm install -g supabase`
|
||||
|
||||
**Edge Function deploys but returns 500**
|
||||
Check that OPENAI_API_KEY is set: `supabase secrets list`
|
||||
|
||||
**"missing_auth" error**
|
||||
Include the Authorization header: `Authorization: Bearer YOUR_TOKEN`
|
||||
|
||||
**"invalid_token" error**
|
||||
Run `bun run src/commands/auth.ts list` to see active tokens. The token may have been revoked or mistyped.
|
||||
Run `bun run src/commands/auth.ts list` to see active tokens.
|
||||
|
||||
**"service_unavailable" error**
|
||||
Database connection failed. Check your Supabase dashboard for outages or connection pool limits.
|
||||
Database connection failed. Check your Supabase dashboard for outages.
|
||||
|
||||
**Claude Desktop doesn't connect**
|
||||
Remote MCP servers must be added via Settings > Integrations, NOT claude_desktop_config.json. See [CLAUDE_DESKTOP.md](CLAUDE_DESKTOP.md).
|
||||
Remote servers must be added via Settings > Integrations, NOT
|
||||
`claude_desktop_config.json`. See [CLAUDE_DESKTOP.md](CLAUDE_DESKTOP.md).
|
||||
|
||||
## Expected Latencies
|
||||
|
||||
@@ -99,4 +109,7 @@ Remote MCP servers must be added via Settings > Integrations, NOT claude_desktop
|
||||
| put_page | 100-500ms | Write + trigger search_vector update |
|
||||
| get_stats | < 100ms | Aggregate query |
|
||||
|
||||
Cold start adds ~300-500ms on the first request after idle (Postgres connection setup via pgbouncer).
|
||||
**Note:** `gbrain serve --http` (built-in HTTP transport) is planned but not yet
|
||||
implemented. Currently, remote MCP requires a custom HTTP wrapper. See the
|
||||
production deployment pattern in the [voice recipe](../../recipes/twilio-voice-brain.md)
|
||||
for a reference implementation.
|
||||
|
||||
@@ -7,11 +7,15 @@ Perplexity Computer supports remote MCP servers with bearer token authentication
|
||||
1. Open Perplexity (requires Pro subscription)
|
||||
2. Go to **Settings > Connectors** (or **MCP Servers**)
|
||||
3. Add a new remote connector:
|
||||
- **URL:** `https://YOUR_REF.supabase.co/functions/v1/gbrain-mcp/mcp`
|
||||
- **URL:** `https://YOUR-DOMAIN.ngrok.app/mcp`
|
||||
- **Authentication:** API Key / Bearer Token
|
||||
- **Token:** your GBrain access token
|
||||
(create one with `bun run src/commands/auth.ts create "perplexity"`)
|
||||
4. Save
|
||||
|
||||
Replace `YOUR-DOMAIN` with your ngrok domain (see
|
||||
[ngrok-tunnel recipe](../../recipes/ngrok-tunnel.md) for setup).
|
||||
|
||||
## Verify
|
||||
|
||||
In a Perplexity conversation, ask it to use your brain:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gbrain",
|
||||
"version": "0.7.0",
|
||||
"version": "0.8.0",
|
||||
"description": "Postgres-native personal knowledge brain with hybrid RAG search",
|
||||
"type": "module",
|
||||
"main": "src/core/index.ts",
|
||||
@@ -18,7 +18,6 @@
|
||||
"build": "bun build --compile --outfile bin/gbrain src/cli.ts",
|
||||
"build:all": "bun build --compile --target=bun-darwin-arm64 --outfile bin/gbrain-darwin-arm64 src/cli.ts && bun build --compile --target=bun-linux-x64 --outfile bin/gbrain-linux-x64 src/cli.ts",
|
||||
"build:schema": "bash scripts/build-schema.sh",
|
||||
"build:edge": "bun run build:schema && bun build src/edge-entry.ts --format=esm --outfile=supabase/functions/gbrain-mcp/gbrain-core.js --external=postgres --external=openai --external=fs --external=os --external=path --external=crypto --external=child_process --external=@aws-sdk/client-s3 --external=@anthropic-ai/sdk --external=gray-matter --minify",
|
||||
"test": "bun test",
|
||||
"test:e2e": "bun test test/e2e/",
|
||||
"prepublish:clawhub": "bun run build:all",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
id: twilio-voice-brain
|
||||
name: Voice-to-Brain
|
||||
version: 0.7.0
|
||||
version: 0.8.0
|
||||
description: Phone calls create brain pages via Twilio + OpenAI Realtime + GBrain MCP. Callers talk, brain pages appear.
|
||||
category: sense
|
||||
requires: [ngrok-tunnel]
|
||||
@@ -230,7 +230,7 @@ If using free tier, copy the URL from the ngrok output (changes every restart).
|
||||
Note: ngrok runs in the foreground. Run it in a background process or new terminal tab.
|
||||
|
||||
The same ngrok account can also serve your GBrain MCP server (see
|
||||
[ngrok Setup](docs/mcp/NGROK_SETUP.md) for the full multi-service pattern).
|
||||
[ngrok-tunnel recipe](recipes/ngrok-tunnel.md) for the full multi-service pattern).
|
||||
|
||||
### Step 4: Create Voice Server
|
||||
|
||||
@@ -286,18 +286,23 @@ The voice server needs these components in `server.mjs`:
|
||||
```
|
||||
POST /session:
|
||||
sdp = request.body // caller's SDP offer
|
||||
form = new FormData()
|
||||
form.append('sdp', sdp)
|
||||
form.append('session', JSON.stringify({
|
||||
|
||||
sessionConfig = JSON.stringify({
|
||||
type: 'realtime',
|
||||
model: 'gpt-4o-realtime-preview',
|
||||
audio: {output: {voice: VOICE}},
|
||||
instructions: buildPrompt(null)
|
||||
}))
|
||||
audio: { output: { voice: VOICE } },
|
||||
instructions: buildPrompt(null),
|
||||
tools: TOOL_SETS.unauthenticated,
|
||||
})
|
||||
|
||||
// Use native FormData (Node 18+) — NOT manual multipart
|
||||
fd = new FormData()
|
||||
fd.set('sdp', sdp)
|
||||
fd.set('session', sessionConfig)
|
||||
|
||||
response = POST 'https://api.openai.com/v1/realtime/calls'
|
||||
Authorization: Bearer OPENAI_API_KEY
|
||||
body: form
|
||||
body: fd // fetch() sets Content-Type automatically
|
||||
|
||||
return response.text() // SDP answer
|
||||
```
|
||||
@@ -306,6 +311,8 @@ The voice server needs these components in `server.mjs`:
|
||||
- `voice` goes under `audio.output.voice`, not top-level
|
||||
- Do NOT send `turn_detection` in session config (not accepted by `/v1/realtime/calls`)
|
||||
- Do NOT send `session.update` on connect (server already configured it)
|
||||
- All `session.update` calls must include `type: 'realtime'` to avoid session.type errors
|
||||
- `input_audio_transcription` is NOT supported over WebRTC data channel — use Whisper post-call on recorded audio instead
|
||||
- Trigger greeting via data channel after WebRTC connects
|
||||
|
||||
**Reference implementation:** The architecture above and the OpenAI Realtime API
|
||||
@@ -464,3 +471,198 @@ The watchdog restarts the server if it crashes."
|
||||
- Free ngrok URLs change every time ngrok restarts
|
||||
- The watchdog (Step 9) handles this automatically
|
||||
- For a permanent URL: upgrade to ngrok paid ($8/mo) for a static domain, or deploy to Fly.io/Railway instead
|
||||
|
||||
## Production Patterns (Recommended)
|
||||
|
||||
These patterns come from a production voice deployment handling real calls daily.
|
||||
They are NOT required for basic setup. **Implement them AFTER the smoke test passes.**
|
||||
Each pattern is self-contained and optional.
|
||||
|
||||
### Agent Identity & Engagement
|
||||
|
||||
#### Identity Separation
|
||||
**Problem:** A voice agent pretending to be the full AI system creates uncanny valley.
|
||||
**Pattern:** The voice agent picks its own name and personality, distinct from the main
|
||||
AI brain. "I work with [Brain], [Owner]'s AI." Lighter, more playful, more curious.
|
||||
|
||||
#### Pre-Computed Bid System
|
||||
**Problem:** Dead air kills engagement. Voice agents wait passively.
|
||||
**Pattern:** At call start, scan live context and pre-compute up to 10 engagement bids.
|
||||
Two types: informative (tasks, calendar, social radar) and relational (curiosity templates).
|
||||
Bids go INTO the prompt so the agent picks from a list. Use bids #1 and #2 for greeting,
|
||||
cycle the rest during conversation. Never ask "anything else?" — bring up the next bid.
|
||||
|
||||
#### Context-First Prompt
|
||||
**Problem:** Voice agent greets generically because it doesn't know what's happening today.
|
||||
**Pattern:** Load live context at call start: tasks, calendar, location, social radar,
|
||||
morning briefing. Position context FIRST in the prompt (before rules) so the model sees
|
||||
it immediately and uses it in the greeting. Try/catch per section. Cap 500-1000 chars each.
|
||||
|
||||
#### Proactive Advisor Mode
|
||||
**Problem:** Voice agents are reactive task machines.
|
||||
**Pattern:** The agent drives the conversation. Anticipate decisions on stale tasks.
|
||||
Suggest capitalizing on trending items. Connect upcoming events with brain context.
|
||||
"Dead air is your enemy" — fill every pause. Never wait passively.
|
||||
|
||||
#### Conversation Timing (the #1 fix)
|
||||
**Problem:** Voice agents interrupt mid-thought AND go silent when the caller is done.
|
||||
Both feel terrible. Early "fill every pause" instructions cause the agent to talk over
|
||||
the caller while they're thinking.
|
||||
**Pattern:** Replace blanket "never be silent" with nuanced timing rules:
|
||||
- **Caller talking or thinking:** SHUT UP. Even 3-5 second pauses mid-thought, wait.
|
||||
Incomplete sentence or mid-story = still thinking. Do not interrupt.
|
||||
- **Caller done** (complete thought + 2-3 seconds silence): NOW respond. Use a bid,
|
||||
ask a follow-up, or pivot to the next topic.
|
||||
- **Detection heuristic:** Incomplete sentence = still thinking. Complete statement +
|
||||
silence = done. Question directed at you = respond immediately.
|
||||
- **Hard rule:** Never let silence go past 5 seconds after a COMPLETE thought.
|
||||
|
||||
Add this as a labeled section in the system prompt (e.g., `# CRITICAL: Conversation Timing`)
|
||||
positioned prominently so the model sees it early. This came from real usage feedback
|
||||
and is the single highest-impact voice quality improvement.
|
||||
|
||||
#### No Repetition Rule
|
||||
**Problem:** Voice agent cycles back to the same bid multiple times in a call.
|
||||
**Pattern:** Add to the system prompt: "Do NOT repeat yourself. If you already said
|
||||
something, move to the NEXT bid. Vary your responses." Simple but addresses a real
|
||||
annoyance that compounds over longer calls.
|
||||
|
||||
### Prompt Engineering
|
||||
|
||||
#### Radical Prompt Compression
|
||||
**Problem:** Long system prompts increase latency and cost on every turn.
|
||||
**Pattern:** Compress aggressively. Production went 13K to 4.7K tokens (65% cut).
|
||||
Bullets over prose, cut repetition, behavior-first. Every token costs latency + money.
|
||||
|
||||
#### OpenAI Realtime Prompting Guide Structure
|
||||
**Problem:** Prose paragraphs parse slowly for the model.
|
||||
**Pattern:** Use labeled markdown sections: `# Role & Objective`, `# Personality & Tone`,
|
||||
`# Rules`, `# Conversation Flow` with state machine substates (`## State 1: VERIFY`,
|
||||
`## State 2: GREETING`, `## State 3: CONVERSATION`), `# Trust`.
|
||||
|
||||
#### Auth-Before-Speech
|
||||
**Problem:** Auth flow adds dead air at call start.
|
||||
**Pattern:** Call the auth tool BEFORE speaking any greeting. Then speak "Hey, code's on
|
||||
its way." Shaves seconds off the round-trip.
|
||||
|
||||
#### Brain Escalation
|
||||
**Problem:** Voice agent can't answer complex questions that need the full brain.
|
||||
**Pattern:** If caller says "talk to [Brain]" or asks a deep question, immediately route
|
||||
to main AI via gateway tool with verbal bridge: "one sec, checking with [Brain]."
|
||||
|
||||
### Call Reliability
|
||||
|
||||
#### Stuck Watchdog
|
||||
**Problem:** Calls go silent when VAD stalls or tool execution hangs.
|
||||
**Pattern:** 20-second timer. If no audio out: clear input buffer, inject "you still
|
||||
there?" system message, force `response.create`.
|
||||
|
||||
#### Never Hang Up
|
||||
**Problem:** AI agents try to end calls.
|
||||
**Pattern:** Hard prompt rule: only the caller decides when the call ends. Never say
|
||||
goodbye, "I'll let you go," or wrap-up language. If silence, ask "you still there?"
|
||||
|
||||
#### Thinking Sound
|
||||
**Problem:** Dead air during slow tool execution.
|
||||
**Pattern:** Pre-generate g711_ulaw audio chunks in a JSON array. Loop at 20ms intervals
|
||||
during slow tools (brain search, web lookup). Stop when tool result returns.
|
||||
|
||||
#### Fallback TwiML
|
||||
**Problem:** Voice agent crashes, callers get silence.
|
||||
**Pattern:** `/fallback` endpoint returns TwiML forwarding to owner's cell. Configure as
|
||||
Twilio fallback URL.
|
||||
|
||||
### Authentication & Authorization
|
||||
|
||||
#### Tool Set Architecture
|
||||
**Problem:** Unauthenticated callers accessing write operations.
|
||||
**Pattern:** Four sets: READ_TOOLS (all callers), WRITE_TOOLS (owner), SCOPED_WRITE_TOOLS
|
||||
(trusted users), GATEWAY_TOOLS (authenticated). LLM doesn't see write tools until auth
|
||||
succeeds. Upgrade via `session.update` with new tools array. All `session.update` calls
|
||||
must include `type: 'realtime'`.
|
||||
|
||||
#### Trusted User Auth with Callback
|
||||
**Problem:** People other than the owner need authenticated access.
|
||||
**Pattern:** Phone registry + callback verification. Each user gets a scope: full,
|
||||
household, content, operational. Scope determines which tools they access.
|
||||
|
||||
#### Caller Routing
|
||||
**Problem:** Different callers need different experiences.
|
||||
**Pattern:** `buildPrompt(callerPhone)` returns different system prompts: owner (OTP),
|
||||
trusted (callback), inner circle (warm greeting + transfer), known (greeting, message),
|
||||
unknown (screen + message).
|
||||
|
||||
### Voice Quality
|
||||
|
||||
#### Dynamic VAD / Noise Mode
|
||||
**Problem:** Background noise causes false triggers or missed speech.
|
||||
**Pattern:** `set_noise_mode` tool adjusts VAD threshold mid-call. Presets: quiet (0.7),
|
||||
normal (0.85), noisy (0.95), very_noisy (0.98). Agent calls proactively on noise.
|
||||
|
||||
#### On-Screen Debug UI
|
||||
**Problem:** console.log is useless when testing from a phone.
|
||||
**Pattern:** WebRTC client displays tool calls, results, errors, and key events inline.
|
||||
|
||||
### Real-Time Awareness
|
||||
|
||||
#### Live Moment Capture
|
||||
**Problem:** Important things said during a call are lost if the call drops or the
|
||||
post-call summary tool doesn't fire.
|
||||
**Pattern:** When the caller shares something important (feedback, ideas, personal
|
||||
stories, decisions), log it in real-time using a `log_voice_request` tool. Don't
|
||||
wait until the call ends. Tell the caller: "Got that, sending it to [Brain] now."
|
||||
Also stream key moments to [messaging platform] during the call so the main agent
|
||||
has awareness before the call is over.
|
||||
|
||||
#### Belt-and-Suspenders Post-Call
|
||||
**Problem:** Post-call processing depends on the voice agent remembering to call the
|
||||
`post_call_summary` tool. If the call drops or the agent forgets, the call is lost.
|
||||
**Pattern:** Both the tool-based AND the automatic call-end handler should post
|
||||
structured signals. The call-end handler (fires on WebSocket close or `/call-end`)
|
||||
should post to [messaging platform] with:
|
||||
- Audio file path
|
||||
- Transcript file path (or warning if missing)
|
||||
- Tools used during the call
|
||||
- Explicit instruction: "[Brain]: Read the call, summarize, take action."
|
||||
|
||||
This ensures every call gets processed regardless of whether the voice agent
|
||||
remembered to call the summary tool. Belt and suspenders.
|
||||
|
||||
### Post-Call Processing
|
||||
|
||||
#### Mandatory 3-Step Post-Call
|
||||
**Problem:** Main agent doesn't know a call happened.
|
||||
**Pattern:** Every call ends with three steps:
|
||||
1. **Messaging notification** — summary to [messaging platform]
|
||||
2. **Transcript to brain** — `brain/meetings/YYYY-MM-DD-call-{caller}.md`
|
||||
3. **Audio to storage** — Twilio MP3 or WebRTC webm/opus, uploaded to cloud storage
|
||||
|
||||
#### WebRTC Audio + Transcript Parity
|
||||
**Problem:** WebRTC calls don't go through Twilio, no automatic logging.
|
||||
**Pattern:** Client captures audio (MediaRecorder, webm/opus) and transcript (per-turn
|
||||
POST to `/transcript`). On call end, POST to `/call-end` saves JSON log. Both channels
|
||||
produce identical output formats. Note: `input_audio_transcription` is NOT supported
|
||||
over WebRTC data channel — use Whisper post-call instead.
|
||||
|
||||
#### Dual API Event Handling
|
||||
**Problem:** OpenAI Realtime API changed event names.
|
||||
**Pattern:** Handle both `response.audio.delta` (old) and `response.output_audio.delta`
|
||||
(new). Same for `.done` events. Future-proofs against API changes.
|
||||
|
||||
### Brain Query Optimization
|
||||
|
||||
#### Report-Aware Query Routing
|
||||
**Problem:** Voice queries about specific topics trigger slow vector searches.
|
||||
**Pattern:** Check the question against a keyword map BEFORE full brain search:
|
||||
|
||||
| Keyword | Report Loaded |
|
||||
|---------|--------------|
|
||||
| email, inbox, mail | inbox sweep report |
|
||||
| social, twitter, mentions | social radar report |
|
||||
| briefing, morning | morning briefing |
|
||||
| meeting | meeting sync report |
|
||||
| slack | slack scan report |
|
||||
| content, ideas | content ideas report |
|
||||
|
||||
Load up to 2,500 chars of matching report. Break after first match. Fall back to full
|
||||
brain search if no keyword matches.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
# Generate src/core/schema-embedded.ts from src/schema.sql
|
||||
# One source of truth: schema.sql is the canonical file.
|
||||
# This script produces a TypeScript constant for use in compiled binaries and Edge Functions.
|
||||
# This script produces a TypeScript constant for use in compiled binaries.
|
||||
set -e
|
||||
SCHEMA_FILE="src/schema.sql"
|
||||
OUT_FILE="src/core/schema-embedded.ts"
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Deploy GBrain Remote MCP Server to Supabase Edge Functions.
|
||||
# Prerequisites: .env.production filled in, supabase CLI installed.
|
||||
set -e
|
||||
|
||||
# Check supabase CLI
|
||||
if ! command -v supabase >/dev/null 2>&1; then
|
||||
echo "Error: supabase CLI not found."
|
||||
echo "Install: brew install supabase/tap/supabase"
|
||||
echo " or: npm install -g supabase"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Load env
|
||||
if [ ! -f .env.production ]; then
|
||||
echo "Error: .env.production not found."
|
||||
echo "Copy .env.production.example to .env.production and fill in your values."
|
||||
exit 1
|
||||
fi
|
||||
source .env.production
|
||||
|
||||
if [ -z "$SUPABASE_PROJECT_REF" ]; then
|
||||
echo "Error: SUPABASE_PROJECT_REF not set in .env.production"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Deploying GBrain Remote MCP Server..."
|
||||
echo " Project: $SUPABASE_PROJECT_REF"
|
||||
echo ""
|
||||
|
||||
# Link project
|
||||
supabase link --project-ref "$SUPABASE_PROJECT_REF"
|
||||
|
||||
# Set secrets
|
||||
supabase secrets set OPENAI_API_KEY="$OPENAI_API_KEY"
|
||||
|
||||
# Build the Edge Function bundle
|
||||
echo ""
|
||||
echo "Building Edge Function bundle..."
|
||||
bun install
|
||||
bun run build:edge
|
||||
echo ""
|
||||
|
||||
# Deploy
|
||||
echo "Deploying Edge Function..."
|
||||
supabase functions deploy gbrain-mcp --no-verify-jwt
|
||||
echo ""
|
||||
|
||||
# Print success
|
||||
URL="https://${SUPABASE_PROJECT_REF}.supabase.co/functions/v1/gbrain-mcp/mcp"
|
||||
echo "================================================"
|
||||
echo " GBrain Remote MCP Server deployed!"
|
||||
echo "================================================"
|
||||
echo ""
|
||||
echo " URL: $URL"
|
||||
echo ""
|
||||
echo " Next steps:"
|
||||
echo " 1. Create a token:"
|
||||
echo " DATABASE_URL=\$DATABASE_URL bun run src/commands/auth.ts create \"my-client\""
|
||||
echo ""
|
||||
echo " 2. Test it:"
|
||||
echo " bun run src/commands/auth.ts test $URL --token <your-token>"
|
||||
echo ""
|
||||
echo " 3. Add to Claude Code:"
|
||||
echo " claude mcp add gbrain -t http $URL -H \"Authorization: Bearer <token>\""
|
||||
echo ""
|
||||
echo " See docs/mcp/ for per-client setup guides."
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gbrain",
|
||||
"version": "0.7.0",
|
||||
"version": "0.8.0",
|
||||
"description": "Personal knowledge brain with hybrid RAG search",
|
||||
"skills": [
|
||||
{
|
||||
|
||||
141
skills/migrations/v0.8.0.md
Normal file
141
skills/migrations/v0.8.0.md
Normal file
@@ -0,0 +1,141 @@
|
||||
---
|
||||
version: 0.8.0
|
||||
feature_pitch:
|
||||
headline: "Your AI can answer the phone now"
|
||||
description: "WebRTC works in a browser tab, phone number optional. Your agent picks its own name and personality."
|
||||
recipe: twilio-voice-brain
|
||||
tiers:
|
||||
- name: webrtc-local
|
||||
label: "Try voice locally (browser only)"
|
||||
prerequisites: [node, openai-key]
|
||||
env: local
|
||||
- name: webrtc-remote
|
||||
label: "Voice from anywhere (browser + tunnel)"
|
||||
prerequisites: [node, openai-key, ngrok]
|
||||
env: server
|
||||
- name: phone
|
||||
label: "Real phone number"
|
||||
prerequisites: [twilio-account]
|
||||
cost: "~$20/mo"
|
||||
---
|
||||
|
||||
# v0.8.0 Migration: Voice-to-Brain Production Patterns + Remote MCP Simplification
|
||||
|
||||
This migration introduces the voice-to-brain feature discovery flow and updates
|
||||
the remote MCP deployment path.
|
||||
|
||||
## What's New
|
||||
|
||||
### Voice-to-Brain v0.8.0
|
||||
|
||||
The voice recipe now includes 21 production patterns from a real deployment:
|
||||
identity separation, pre-computed engagement bids, context-first prompts,
|
||||
proactive advisor mode, prompt compression, caller routing, dynamic VAD,
|
||||
post-call processing with audio recording, and report-aware query routing.
|
||||
|
||||
WebRTC works out of the box with just an OpenAI key. A phone number via
|
||||
Twilio is optional.
|
||||
|
||||
### Remote MCP Simplified
|
||||
|
||||
The Supabase Edge Function deployment path has been removed. Remote MCP now
|
||||
uses a self-hosted server + ngrok tunnel. Simpler, more reliable, works with
|
||||
any AI client.
|
||||
|
||||
## Agent Instructions
|
||||
|
||||
### Step 1: Check if voice-to-brain is already installed
|
||||
|
||||
```bash
|
||||
ls ~/.gbrain/integrations/twilio-voice-brain/heartbeat.jsonl 2>/dev/null
|
||||
```
|
||||
|
||||
If the file exists: "Voice is already set up from a previous install. Skipping."
|
||||
Proceed to Step 6.
|
||||
|
||||
If not: proceed to Step 2.
|
||||
|
||||
### Step 2: Detect environment
|
||||
|
||||
Check for indicators of server vs local deployment:
|
||||
- Is there an ngrok or cloudflared process running? (`pgrep -f ngrok` or `pgrep -f cloudflared`)
|
||||
- Is there a public URL configured in `~/.gbrain/config.json`?
|
||||
- Is the OS Linux (likely server) or macOS (likely local)?
|
||||
|
||||
**Server** (OpenClaw/Hermes on Render/Railway): use 2-tier flow (WebRTC remote, then phone)
|
||||
**Local** (macOS with PGLite brain): use 3-tier flow (WebRTC local, then remote, then phone)
|
||||
|
||||
### Step 3: Pitch voice to the user
|
||||
|
||||
Tell the user:
|
||||
|
||||
"v0.8.0 adds Voice — your AI can answer phone calls and create brain pages from
|
||||
every conversation. Your agent picks its own name, knows who's calling, and greets
|
||||
them with context from the brain.
|
||||
|
||||
[If server]: WebRTC works with your existing tunnel. Just needs an OpenAI key.
|
||||
[If local]: WebRTC works right in your browser. Just needs Node + OpenAI key.
|
||||
|
||||
Want me to set up voice? (yes / not now)"
|
||||
|
||||
If the user says "not now" or declines: "No problem. You can set up voice anytime
|
||||
with `gbrain integrations show twilio-voice-brain`." Skip to Step 6.
|
||||
|
||||
### Step 4: Environment-appropriate setup
|
||||
|
||||
**Server — Tier 1: WebRTC remote**
|
||||
|
||||
Read `recipes/twilio-voice-brain.md` and execute:
|
||||
1. Step 1 (prerequisites check)
|
||||
2. Step 2, Credential 2 only (OpenAI API key — ngrok is already running)
|
||||
3. Step 4 (create voice server)
|
||||
4. Step 6 (start server and verify health)
|
||||
|
||||
Tell the user: "Voice is live! Open https://YOUR-URL/call in a browser to try it."
|
||||
|
||||
**Local — Tier 1: WebRTC local only**
|
||||
|
||||
Read `recipes/twilio-voice-brain.md` and execute:
|
||||
1. Step 1 (prerequisites check)
|
||||
2. Step 2, Credential 2 only (OpenAI API key)
|
||||
3. Step 4 (create voice server)
|
||||
4. Start server on localhost:8765
|
||||
|
||||
Tell the user: "Voice is live! Open http://localhost:8765/call in your browser."
|
||||
|
||||
**Local — Tier 2 offer:**
|
||||
|
||||
"Want voice from anywhere? Adding ngrok gives you a public URL so you can call
|
||||
from any browser or connect a phone number later."
|
||||
|
||||
If yes: Execute Step 3 from the recipe (ngrok setup) and the ngrok-tunnel recipe.
|
||||
If no: "No problem. Local-only WebRTC is fully functional."
|
||||
|
||||
### Step 5: After WebRTC works, ask about phone number
|
||||
|
||||
"WebRTC is working. Want to add a real phone number so people can call you?
|
||||
This needs a Twilio account (~$20/mo for 100 min of calls)."
|
||||
|
||||
If yes: Execute Step 2 (Twilio credentials), Step 5 (Twilio phone number setup),
|
||||
and Step 7 (smoke test — the phone calls the user).
|
||||
If no: "No problem. You can add a phone number anytime with
|
||||
`gbrain integrations show twilio-voice-brain`."
|
||||
|
||||
### Step 6: Update Edge Function references (if applicable)
|
||||
|
||||
If the user previously had Edge Function MCP configured:
|
||||
- The Edge Function deployment has been removed in v0.8.0
|
||||
- Remote MCP now uses self-hosted server + ngrok tunnel
|
||||
- Read `docs/mcp/DEPLOY.md` for the updated setup
|
||||
- If they had Claude Desktop configured with the old URL, help them update it
|
||||
|
||||
### Step 7: Done
|
||||
|
||||
Log the migration completion:
|
||||
```bash
|
||||
mkdir -p ~/.gbrain/migrations
|
||||
echo '{"version":"0.8.0","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","voice_setup":"RESULT"}' >> ~/.gbrain/migrations/completed.jsonl
|
||||
```
|
||||
|
||||
Tell the user: "v0.8.0 migration complete. Run `gbrain integrations doctor` to
|
||||
verify all integrations are healthy."
|
||||
@@ -18,7 +18,7 @@ for (const op of operations) {
|
||||
}
|
||||
|
||||
// CLI-only commands that bypass the operation layer
|
||||
const CLI_ONLY = new Set(['init', 'upgrade', 'check-update', 'integrations', 'import', 'export', 'files', 'embed', 'serve', 'call', 'config', 'doctor', 'migrate']);
|
||||
const CLI_ONLY = new Set(['init', 'upgrade', 'post-upgrade', 'check-update', 'integrations', 'import', 'export', 'files', 'embed', 'serve', 'call', 'config', 'doctor', 'migrate']);
|
||||
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
@@ -232,6 +232,11 @@ async function handleCliOnly(command: string, args: string[]) {
|
||||
await runUpgrade(args);
|
||||
return;
|
||||
}
|
||||
if (command === 'post-upgrade') {
|
||||
const { runPostUpgrade } = await import('./commands/upgrade.ts');
|
||||
runPostUpgrade();
|
||||
return;
|
||||
}
|
||||
if (command === 'check-update') {
|
||||
const { runCheckUpdate } = await import('./commands/check-update.ts');
|
||||
await runCheckUpdate(args);
|
||||
|
||||
@@ -1,21 +1,27 @@
|
||||
import { execSync } from 'child_process';
|
||||
import { existsSync, readFileSync, readdirSync, writeFileSync, mkdirSync } from 'fs';
|
||||
import { join, resolve } from 'path';
|
||||
import { VERSION } from '../version.ts';
|
||||
|
||||
export async function runUpgrade(args: string[]) {
|
||||
if (args.includes('--help') || args.includes('-h')) {
|
||||
console.log('Usage: gbrain upgrade\n\nSelf-update the CLI.\n\nDetects install method (bun, binary, clawhub) and runs the appropriate update.');
|
||||
console.log('Usage: gbrain upgrade\n\nSelf-update the CLI.\n\nDetects install method (bun, binary, clawhub) and runs the appropriate update.\nAfter upgrading, shows what\'s new and offers to set up new features.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Capture old version BEFORE upgrading (Codex finding: old binary runs this code)
|
||||
const oldVersion = VERSION;
|
||||
const method = detectInstallMethod();
|
||||
|
||||
console.log(`Detected install method: ${method}`);
|
||||
|
||||
let upgraded = false;
|
||||
switch (method) {
|
||||
case 'bun':
|
||||
console.log('Upgrading via bun...');
|
||||
try {
|
||||
execSync('bun update gbrain', { stdio: 'inherit', timeout: 120_000 });
|
||||
verifyUpgrade();
|
||||
upgraded = true;
|
||||
} catch {
|
||||
console.error('Upgrade failed. Try running manually: bun update gbrain');
|
||||
}
|
||||
@@ -31,7 +37,7 @@ export async function runUpgrade(args: string[]) {
|
||||
console.log('Upgrading via ClawHub...');
|
||||
try {
|
||||
execSync('clawhub update gbrain', { stdio: 'inherit', timeout: 120_000 });
|
||||
verifyUpgrade();
|
||||
upgraded = true;
|
||||
} catch {
|
||||
console.error('ClawHub upgrade failed. Try: clawhub update gbrain');
|
||||
}
|
||||
@@ -44,17 +50,134 @@ export async function runUpgrade(args: string[]) {
|
||||
console.log(' clawhub update gbrain');
|
||||
console.log(' Download from https://github.com/garrytan/gbrain/releases');
|
||||
}
|
||||
|
||||
if (upgraded) {
|
||||
const newVersion = verifyUpgrade();
|
||||
// Save old version for post-upgrade migration detection
|
||||
saveUpgradeState(oldVersion, newVersion);
|
||||
// Run post-upgrade feature discovery (reads migration files from the NEW binary)
|
||||
try {
|
||||
execSync('gbrain post-upgrade', { stdio: 'inherit', timeout: 30_000 });
|
||||
} catch {
|
||||
// post-upgrade is best-effort, don't fail the upgrade
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function verifyUpgrade() {
|
||||
function verifyUpgrade(): string {
|
||||
try {
|
||||
const output = execSync('gbrain --version', { encoding: 'utf-8', timeout: 10_000 }).trim();
|
||||
console.log(`Upgrade complete. Now running: ${output}`);
|
||||
return output.replace(/^gbrain\s*/i, '').trim();
|
||||
} catch {
|
||||
console.log('Upgrade complete. Could not verify new version.');
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function saveUpgradeState(oldVersion: string, newVersion: string) {
|
||||
try {
|
||||
const dir = join(process.env.HOME || '', '.gbrain');
|
||||
mkdirSync(dir, { recursive: true });
|
||||
const statePath = join(dir, 'upgrade-state.json');
|
||||
const state: Record<string, unknown> = existsSync(statePath)
|
||||
? JSON.parse(readFileSync(statePath, 'utf-8'))
|
||||
: {};
|
||||
state.last_upgrade = {
|
||||
from: oldVersion,
|
||||
to: newVersion,
|
||||
ts: new Date().toISOString(),
|
||||
};
|
||||
writeFileSync(statePath, JSON.stringify(state, null, 2));
|
||||
} catch {
|
||||
// best-effort
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Post-upgrade feature discovery. Reads migration files between old and new version,
|
||||
* prints feature pitches from YAML frontmatter. Called by `gbrain post-upgrade` which
|
||||
* runs the NEW binary after upgrade completes.
|
||||
*/
|
||||
export function runPostUpgrade() {
|
||||
try {
|
||||
const statePath = join(process.env.HOME || '', '.gbrain', 'upgrade-state.json');
|
||||
if (!existsSync(statePath)) return;
|
||||
const state = JSON.parse(readFileSync(statePath, 'utf-8'));
|
||||
const lastUpgrade = state.last_upgrade;
|
||||
if (!lastUpgrade?.from || !lastUpgrade?.to) return;
|
||||
|
||||
// Find migration files in version range
|
||||
const migrationsDir = findMigrationsDir();
|
||||
if (!migrationsDir) return;
|
||||
|
||||
const files = readdirSync(migrationsDir)
|
||||
.filter(f => f.match(/^v\d+\.\d+\.\d+\.md$/))
|
||||
.sort();
|
||||
|
||||
for (const file of files) {
|
||||
const version = file.replace(/^v/, '').replace(/\.md$/, '');
|
||||
if (isNewerThan(version, lastUpgrade.from)) {
|
||||
const content = readFileSync(join(migrationsDir, file), 'utf-8');
|
||||
const pitch = extractFeaturePitch(content);
|
||||
if (pitch) {
|
||||
console.log('');
|
||||
console.log(`NEW: ${pitch.headline}`);
|
||||
if (pitch.description) console.log(pitch.description);
|
||||
if (pitch.recipe) {
|
||||
console.log(`Run \`gbrain integrations show ${pitch.recipe}\` to set it up.`);
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// post-upgrade is best-effort
|
||||
}
|
||||
}
|
||||
|
||||
function findMigrationsDir(): string | null {
|
||||
// Try relative to this file (source install)
|
||||
const candidates = [
|
||||
resolve(__dirname, '../../skills/migrations'),
|
||||
resolve(process.cwd(), 'skills/migrations'),
|
||||
resolve(process.cwd(), 'node_modules/gbrain/skills/migrations'),
|
||||
];
|
||||
for (const dir of candidates) {
|
||||
if (existsSync(dir)) return dir;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function extractFeaturePitch(content: string): { headline: string; description?: string; recipe?: string } | null {
|
||||
// Parse YAML frontmatter for feature_pitch
|
||||
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
||||
if (!fmMatch) return null;
|
||||
const fm = fmMatch[1];
|
||||
|
||||
const headlineMatch = fm.match(/headline:\s*["']?(.+?)["']?\s*$/m);
|
||||
if (!headlineMatch) return null;
|
||||
|
||||
const descMatch = fm.match(/description:\s*["']?(.+?)["']?\s*$/m);
|
||||
const recipeMatch = fm.match(/recipe:\s*["']?(.+?)["']?\s*$/m);
|
||||
|
||||
return {
|
||||
headline: headlineMatch[1],
|
||||
description: descMatch?.[1],
|
||||
recipe: recipeMatch?.[1],
|
||||
};
|
||||
}
|
||||
|
||||
function isNewerThan(version: string, baseline: string): boolean {
|
||||
const v = version.split('.').map(Number);
|
||||
const b = baseline.split('.').map(Number);
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if ((v[i] || 0) > (b[i] || 0)) return true;
|
||||
if ((v[i] || 0) < (b[i] || 0)) return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function detectInstallMethod(): 'bun' | 'binary' | 'clawhub' | 'unknown' {
|
||||
const execPath = process.execPath || '';
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { join } from 'path';
|
||||
import { homedir } from 'os';
|
||||
import type { EngineConfig } from './types.ts';
|
||||
|
||||
// Lazy-evaluated to avoid calling homedir() at module scope (breaks in Deno Edge Functions)
|
||||
// Lazy-evaluated to avoid calling homedir() at module scope (breaks in serverless/bundled environments)
|
||||
function getConfigDir() { return join(homedir(), '.gbrain'); }
|
||||
function getConfigPath() { return join(getConfigDir(), 'config.json'); }
|
||||
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
/**
|
||||
* Edge Function bundle entry point.
|
||||
*
|
||||
* Curated exports for Supabase Edge Functions (Deno runtime).
|
||||
* Excludes modules that depend on Node.js filesystem APIs:
|
||||
* - db.ts (reads schema.sql from disk — now uses schema-embedded.ts)
|
||||
* - config.ts (reads ~/.gbrain/config.json via homedir())
|
||||
* - import-file.ts (uses readFileSync/statSync)
|
||||
* - sync.ts (git-based, local filesystem)
|
||||
*/
|
||||
export { operations, operationsByName, OperationError } from './core/operations.ts';
|
||||
export type { Operation, OperationContext, ParamDef } from './core/operations.ts';
|
||||
export { PostgresEngine } from './core/postgres-engine.ts';
|
||||
export type { BrainEngine } from './core/engine.ts';
|
||||
export * from './core/types.ts';
|
||||
export { VERSION } from './version.ts';
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"imports": {
|
||||
"postgres": "npm:postgres@3",
|
||||
"openai": "npm:openai@4",
|
||||
"@anthropic-ai/sdk": "npm:@anthropic-ai/sdk@0",
|
||||
"@aws-sdk/client-s3": "npm:@aws-sdk/client-s3@3",
|
||||
"gray-matter": "npm:gray-matter@4",
|
||||
"child_process": "node:child_process",
|
||||
"@modelcontextprotocol/sdk/server/index.js": "npm:@modelcontextprotocol/sdk@1/server/index.js",
|
||||
"@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js": "npm:@modelcontextprotocol/sdk@1/server/webStandardStreamableHttp.js",
|
||||
"@modelcontextprotocol/sdk/types.js": "npm:@modelcontextprotocol/sdk@1/types.js",
|
||||
"hono": "npm:hono@4",
|
||||
"hono/cors": "npm:hono@4/cors",
|
||||
"crypto": "node:crypto"
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -1,288 +0,0 @@
|
||||
/**
|
||||
* GBrain Remote MCP Server — Supabase Edge Function
|
||||
*
|
||||
* Exposes GBrain operations as remote MCP tools via Streamable HTTP transport.
|
||||
* Auth via bearer tokens stored in access_tokens table (SHA-256 hashed).
|
||||
*
|
||||
* Deploy: supabase functions deploy gbrain-mcp --no-verify-jwt
|
||||
* URL: https://<project>.supabase.co/functions/v1/gbrain-mcp/mcp
|
||||
*/
|
||||
import { Hono } from 'hono';
|
||||
import { cors } from 'hono/cors';
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||
import { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js';
|
||||
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
||||
import postgres from 'postgres';
|
||||
import { createHash } from 'crypto';
|
||||
import { operations, OperationError, PostgresEngine, VERSION } from './gbrain-core.js';
|
||||
import type { OperationContext } from './gbrain-core.js';
|
||||
|
||||
// Operations excluded from remote (may exceed 60s Edge Function timeout)
|
||||
const REMOTE_EXCLUDED = new Set(['sync_brain', 'file_upload']);
|
||||
const remoteOps = operations.filter((op: any) => !REMOTE_EXCLUDED.has(op.name));
|
||||
|
||||
// Database connection (lazy, one per isolate)
|
||||
let engine: PostgresEngine | null = null;
|
||||
let sql: ReturnType<typeof postgres> | null = null;
|
||||
|
||||
function getDbUrl(): string {
|
||||
// @ts-ignore: Deno env
|
||||
return Deno.env.get('SUPABASE_DB_URL') || Deno.env.get('DATABASE_URL') || '';
|
||||
}
|
||||
|
||||
function getOpenAiKey(): string {
|
||||
// @ts-ignore: Deno env
|
||||
return Deno.env.get('OPENAI_API_KEY') || '';
|
||||
}
|
||||
|
||||
async function getEngine(): Promise<PostgresEngine> {
|
||||
if (!engine) {
|
||||
engine = new PostgresEngine();
|
||||
await engine.connect({ database_url: getDbUrl(), poolSize: 1 });
|
||||
}
|
||||
return engine;
|
||||
}
|
||||
|
||||
function getDirectSql(): ReturnType<typeof postgres> {
|
||||
if (!sql) {
|
||||
sql = postgres(getDbUrl(), { max: 1 });
|
||||
}
|
||||
return sql;
|
||||
}
|
||||
|
||||
// Auth: check bearer token against access_tokens table
|
||||
async function authenticateToken(authHeader: string | null): Promise<{ valid: boolean; name?: string; error?: string; status?: number }> {
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
return {
|
||||
valid: false,
|
||||
error: JSON.stringify({
|
||||
error: 'missing_auth',
|
||||
message: "Authorization header required. Use 'Bearer <token>' format.",
|
||||
docs: 'docs/mcp/DEPLOY.md#authentication',
|
||||
}),
|
||||
status: 401,
|
||||
};
|
||||
}
|
||||
|
||||
const token = authHeader.slice(7);
|
||||
const hash = createHash('sha256').update(token).digest('hex');
|
||||
|
||||
try {
|
||||
const conn = getDirectSql();
|
||||
const rows = await conn`
|
||||
SELECT name, revoked_at FROM access_tokens
|
||||
WHERE token_hash = ${hash}
|
||||
`;
|
||||
|
||||
if (rows.length === 0) {
|
||||
return {
|
||||
valid: false,
|
||||
error: JSON.stringify({
|
||||
error: 'invalid_token',
|
||||
message: "Token not recognized. Run 'bun run src/commands/auth.ts list' to see active tokens.",
|
||||
docs: 'docs/mcp/DEPLOY.md#troubleshooting',
|
||||
}),
|
||||
status: 401,
|
||||
};
|
||||
}
|
||||
|
||||
if (rows[0].revoked_at) {
|
||||
const revokedDate = new Date(rows[0].revoked_at as string).toISOString().slice(0, 10);
|
||||
return {
|
||||
valid: false,
|
||||
error: JSON.stringify({
|
||||
error: 'token_revoked',
|
||||
message: `This token was revoked on ${revokedDate}. Create a new one with 'bun run src/commands/auth.ts create <name>'.`,
|
||||
docs: 'docs/mcp/DEPLOY.md#token-management',
|
||||
}),
|
||||
status: 403,
|
||||
};
|
||||
}
|
||||
|
||||
// Update last_used_at
|
||||
const conn2 = getDirectSql();
|
||||
await conn2`UPDATE access_tokens SET last_used_at = now() WHERE token_hash = ${hash}`;
|
||||
|
||||
return { valid: true, name: rows[0].name as string };
|
||||
} catch {
|
||||
return {
|
||||
valid: false,
|
||||
error: JSON.stringify({
|
||||
error: 'service_unavailable',
|
||||
message: 'Database connection failed. Check Supabase dashboard for status.',
|
||||
docs: 'docs/mcp/DEPLOY.md#troubleshooting',
|
||||
}),
|
||||
status: 503,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Log MCP request for auditing
|
||||
async function logRequest(tokenName: string, operation: string, latencyMs: number, status: string) {
|
||||
try {
|
||||
const conn = getDirectSql();
|
||||
await conn`
|
||||
INSERT INTO mcp_request_log (token_name, operation, latency_ms, status)
|
||||
VALUES (${tokenName}, ${operation}, ${latencyMs}, ${status})
|
||||
`;
|
||||
} catch {
|
||||
// Best effort, don't crash on log failure
|
||||
console.error('[gbrain-mcp] Failed to log request');
|
||||
}
|
||||
}
|
||||
|
||||
// Create MCP Server with tool handlers
|
||||
function createMcpServer(eng: PostgresEngine): Server {
|
||||
const server = new Server(
|
||||
{ name: 'gbrain', version: VERSION },
|
||||
{ capabilities: { tools: {} } },
|
||||
);
|
||||
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||
tools: remoteOps.map((op: any) => ({
|
||||
name: op.name,
|
||||
description: op.description,
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: Object.fromEntries(
|
||||
Object.entries(op.params).map(([k, v]: [string, any]) => [k, {
|
||||
type: v.type === 'array' ? 'array' : v.type,
|
||||
...(v.description ? { description: v.description } : {}),
|
||||
...(v.enum ? { enum: v.enum } : {}),
|
||||
...(v.items ? { items: { type: v.items.type } } : {}),
|
||||
}]),
|
||||
),
|
||||
required: Object.entries(op.params)
|
||||
.filter(([, v]: [string, any]) => v.required)
|
||||
.map(([k]: [string, any]) => k),
|
||||
},
|
||||
})),
|
||||
}));
|
||||
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request: any) => {
|
||||
const { name, arguments: params } = request.params;
|
||||
const op = remoteOps.find((o: any) => o.name === name);
|
||||
if (!op) {
|
||||
return { content: [{ type: 'text', text: `Error: Unknown tool: ${name}` }], isError: true };
|
||||
}
|
||||
|
||||
const ctx: OperationContext = {
|
||||
engine: eng,
|
||||
config: {
|
||||
engine: 'postgres',
|
||||
database_url: getDbUrl(),
|
||||
openai_api_key: getOpenAiKey(),
|
||||
},
|
||||
logger: {
|
||||
info: (msg: string) => console.log(`[info] ${msg}`),
|
||||
warn: (msg: string) => console.warn(`[warn] ${msg}`),
|
||||
error: (msg: string) => console.error(`[error] ${msg}`),
|
||||
},
|
||||
dryRun: !!(params?.dry_run),
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await op.handler(ctx, params || {});
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof OperationError) {
|
||||
return { content: [{ type: 'text', text: JSON.stringify(e.toJSON(), null, 2) }], isError: true };
|
||||
}
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
return { content: [{ type: 'text', text: `Error: ${msg}` }], isError: true };
|
||||
}
|
||||
});
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
// Hono app — routes: /mcp (MCP transport), /health (monitoring)
|
||||
const app = new Hono().basePath('/gbrain-mcp');
|
||||
|
||||
app.use('/*', cors({
|
||||
origin: '*',
|
||||
allowMethods: ['GET', 'POST', 'DELETE', 'OPTIONS'],
|
||||
allowHeaders: ['Content-Type', 'Authorization', 'Mcp-Session-Id', 'Mcp-Protocol-Version', 'Last-Event-ID'],
|
||||
exposeHeaders: ['Mcp-Session-Id'],
|
||||
}));
|
||||
|
||||
// Health check
|
||||
app.get('/health', async (c) => {
|
||||
const authHeader = c.req.header('Authorization');
|
||||
|
||||
// Unauth: minimal response
|
||||
if (!authHeader) {
|
||||
try {
|
||||
const conn = getDirectSql();
|
||||
await conn`SELECT 1`;
|
||||
return c.json({ status: 'ok', version: VERSION });
|
||||
} catch {
|
||||
return c.json({ status: 'error' }, 503);
|
||||
}
|
||||
}
|
||||
|
||||
// Auth: detailed checks
|
||||
const auth = await authenticateToken(authHeader);
|
||||
if (!auth.valid) return c.json({ status: 'error' }, auth.status);
|
||||
|
||||
const checks: Record<string, string> = {};
|
||||
try {
|
||||
const conn = getDirectSql();
|
||||
await conn`SELECT 1`;
|
||||
checks.postgres = 'ok';
|
||||
} catch {
|
||||
checks.postgres = 'error';
|
||||
}
|
||||
|
||||
try {
|
||||
const conn = getDirectSql();
|
||||
const ext = await conn`SELECT extname FROM pg_extension WHERE extname = 'vector'`;
|
||||
checks.pgvector = ext.length > 0 ? 'ok' : 'missing';
|
||||
} catch {
|
||||
checks.pgvector = 'error';
|
||||
}
|
||||
|
||||
checks.openai = getOpenAiKey() ? 'configured' : 'missing';
|
||||
|
||||
const status = Object.values(checks).every(v => v === 'ok' || v === 'configured') ? 'ok' : 'degraded';
|
||||
return c.json({ status, version: VERSION, checks });
|
||||
});
|
||||
|
||||
// MCP endpoint
|
||||
app.all('/mcp', async (c) => {
|
||||
// Auth check
|
||||
const auth = await authenticateToken(c.req.header('Authorization') || null);
|
||||
if (!auth.valid) {
|
||||
return new Response(auth.error, {
|
||||
status: auth.status || 401,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
const eng = await getEngine();
|
||||
const server = createMcpServer(eng);
|
||||
|
||||
const transport = new WebStandardStreamableHTTPServerTransport({
|
||||
// Stateless mode — no sessions needed for single-user personal brain
|
||||
});
|
||||
|
||||
await server.connect(transport);
|
||||
|
||||
try {
|
||||
const response = await transport.handleRequest(c.req.raw);
|
||||
|
||||
// Log the request (await to ensure it completes before isolate dies)
|
||||
const latency = Date.now() - startTime;
|
||||
await logRequest(auth.name || 'unknown', 'mcp_request', latency, 'success');
|
||||
|
||||
return response;
|
||||
} catch (e) {
|
||||
const latency = Date.now() - startTime;
|
||||
await logRequest(auth.name || 'unknown', 'mcp_request', latency, 'error');
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
||||
// @ts-ignore: Deno.serve
|
||||
Deno.serve(app.fetch);
|
||||
@@ -211,4 +211,75 @@ describe('twilio-voice-brain recipe', () => {
|
||||
expect(secret.where).toContain('https://');
|
||||
}
|
||||
});
|
||||
|
||||
test('recipe has all required secrets', () => {
|
||||
const { readFileSync } = require('fs');
|
||||
const content = readFileSync(
|
||||
new URL('../recipes/twilio-voice-brain.md', import.meta.url),
|
||||
'utf-8'
|
||||
);
|
||||
const recipe = parseRecipe(content, 'twilio-voice-brain.md');
|
||||
expect(recipe).not.toBeNull();
|
||||
const secretNames = recipe!.frontmatter.secrets.map((s: any) => s.name);
|
||||
expect(secretNames).toContain('TWILIO_ACCOUNT_SID');
|
||||
expect(secretNames).toContain('TWILIO_AUTH_TOKEN');
|
||||
expect(secretNames).toContain('OPENAI_API_KEY');
|
||||
});
|
||||
|
||||
test('recipe version is valid semver', () => {
|
||||
const { readFileSync } = require('fs');
|
||||
const content = readFileSync(
|
||||
new URL('../recipes/twilio-voice-brain.md', import.meta.url),
|
||||
'utf-8'
|
||||
);
|
||||
const recipe = parseRecipe(content, 'twilio-voice-brain.md');
|
||||
expect(recipe).not.toBeNull();
|
||||
expect(recipe!.frontmatter.version).toMatch(/^\d+\.\d+\.\d+$/);
|
||||
});
|
||||
|
||||
test('recipe requires resolve to existing recipe files', () => {
|
||||
const { readFileSync, existsSync } = require('fs');
|
||||
const { resolve } = require('path');
|
||||
const content = readFileSync(
|
||||
new URL('../recipes/twilio-voice-brain.md', import.meta.url),
|
||||
'utf-8'
|
||||
);
|
||||
const recipe = parseRecipe(content, 'twilio-voice-brain.md');
|
||||
expect(recipe).not.toBeNull();
|
||||
const recipesDir = new URL('../recipes/', import.meta.url).pathname;
|
||||
for (const dep of recipe!.frontmatter.requires) {
|
||||
const depPath = resolve(recipesDir, `${dep}.md`);
|
||||
expect(existsSync(depPath)).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// --- All recipes parse without error ---
|
||||
|
||||
describe('all recipes', () => {
|
||||
test('every recipe file in recipes/ parses correctly', () => {
|
||||
const { readFileSync, readdirSync } = require('fs');
|
||||
const { resolve } = require('path');
|
||||
const recipesDir = new URL('../recipes/', import.meta.url).pathname;
|
||||
const files = readdirSync(recipesDir).filter((f: string) => f.endsWith('.md'));
|
||||
expect(files.length).toBeGreaterThan(0);
|
||||
for (const file of files) {
|
||||
const content = readFileSync(resolve(recipesDir, file), 'utf-8');
|
||||
const recipe = parseRecipe(content, file);
|
||||
expect(recipe).not.toBeNull();
|
||||
expect(recipe!.frontmatter.id).toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
test('no recipe contains personal references', () => {
|
||||
const { readFileSync, readdirSync } = require('fs');
|
||||
const { resolve } = require('path');
|
||||
const recipesDir = new URL('../recipes/', import.meta.url).pathname;
|
||||
const files = readdirSync(recipesDir).filter((f: string) => f.endsWith('.md'));
|
||||
const personalPatterns = /wintermute|mercury|16507969501|\+1650796/i;
|
||||
for (const file of files) {
|
||||
const content = readFileSync(resolve(recipesDir, file), 'utf-8');
|
||||
expect(content).not.toMatch(personalPatterns);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -97,7 +97,7 @@ describe('slugifyPath', () => {
|
||||
});
|
||||
|
||||
test('Apple Notes example 2', () => {
|
||||
expect(slugifyPath('Apple Notes/2018-12-14 Garry Tan Photo.md')).toBe('apple-notes/2018-12-14-garry-tan-photo');
|
||||
expect(slugifyPath('Apple Notes/2018-12-14 Team Photo.md')).toBe('apple-notes/2018-12-14-team-photo');
|
||||
});
|
||||
|
||||
test('Apple Notes example 3 (parens and ellipsis)', () => {
|
||||
|
||||
Reference in New Issue
Block a user