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:
Garry Tan
2026-04-11 10:52:30 -10:00
committed by GitHub
parent 0ca2e86acb
commit 91ced664b6
30 changed files with 786 additions and 1132 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1 +1 @@
0.7.0
0.8.0

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

View File

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

View File

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

View File

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

View File

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

View File

@@ -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).

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 || '';

View File

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

View File

@@ -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';

View File

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

View File

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

View File

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

View File

@@ -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)', () => {