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:
@@ -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';
|
||||
Reference in New Issue
Block a user