mirror of
https://github.com/garrytan/gbrain.git
synced 2026-08-14 08:53:22 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
714e21db42 |
+7
-1
@@ -19,7 +19,7 @@ for (const op of operations) {
|
||||
}
|
||||
|
||||
// CLI-only commands that bypass the operation layer
|
||||
const CLI_ONLY = new Set(['init', 'upgrade', 'post-upgrade', 'check-update', 'integrations', 'publish', 'check-backlinks', 'lint', 'report', 'import', 'export', 'files', 'embed', 'serve', 'call', 'config', 'doctor', 'migrate', 'eval', 'sync', 'extract', 'features', 'autopilot', 'graph-query', 'jobs', 'agent', 'apply-migrations', 'skillpack-check', 'skillpack', 'resolvers', 'integrity', 'repair-jsonb', 'orphans', 'sources', 'dream', 'check-resolvable', 'routing-eval', 'skillify']);
|
||||
const CLI_ONLY = new Set(['init', 'upgrade', 'post-upgrade', 'check-update', 'integrations', 'publish', 'check-backlinks', 'lint', 'report', 'import', 'export', 'files', 'embed', 'serve', 'call', 'config', 'doctor', 'migrate', 'eval', 'sync', 'extract', 'features', 'autopilot', 'graph-query', 'jobs', 'agent', 'apply-migrations', 'skillpack-check', 'skillpack', 'resolvers', 'integrity', 'repair-jsonb', 'orphans', 'sources', 'dream', 'check-resolvable', 'routing-eval', 'skillify', 'register-mcp']);
|
||||
|
||||
async function main() {
|
||||
// Parse global flags (--quiet / --progress-json / --progress-interval)
|
||||
@@ -381,6 +381,12 @@ async function handleCliOnly(command: string, args: string[]) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (command === 'register-mcp') {
|
||||
const { runRegisterMcp } = await import('./commands/register-mcp.ts');
|
||||
await runRegisterMcp(args);
|
||||
return;
|
||||
}
|
||||
|
||||
if (command === 'dream') {
|
||||
// Dream mirrors doctor's pattern: filesystem phases run without a DB,
|
||||
// so an engine connection failure is non-fatal. runCycle honestly
|
||||
|
||||
@@ -8,6 +8,7 @@ const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
import { saveConfig, loadConfig, toEngineConfig, type GBrainConfig } from '../core/config.ts';
|
||||
import { createEngine } from '../core/engine-factory.ts';
|
||||
import { registerMcpServerInOpenClaw } from './register-mcp.ts';
|
||||
|
||||
export async function runInit(args: string[]) {
|
||||
const isSupabase = args.includes('--supabase');
|
||||
@@ -117,6 +118,7 @@ async function initPGLite(opts: { jsonOutput: boolean; apiKey: string | null; cu
|
||||
...(opts.apiKey ? { openai_api_key: opts.apiKey } : {}),
|
||||
};
|
||||
saveConfig(config);
|
||||
const mcpRegistration = registerMcpServerInOpenClaw({ gbrainConfig: config });
|
||||
|
||||
const stats = await engine.getStats();
|
||||
|
||||
@@ -134,6 +136,18 @@ async function initPGLite(opts: { jsonOutput: boolean; apiKey: string | null; cu
|
||||
} else {
|
||||
console.log('Next: gbrain import <dir>');
|
||||
}
|
||||
if (mcpRegistration.status === 'registered') {
|
||||
console.log(`OpenClaw MCP: registered gbrain at ${mcpRegistration.openclawConfigPath}`);
|
||||
console.log('Restart your OpenClaw gateway so all gbrain tools are discoverable.');
|
||||
} else if (mcpRegistration.status === 'already_registered') {
|
||||
console.log('OpenClaw MCP: gbrain already registered.');
|
||||
console.log('Restart your OpenClaw gateway if it is currently running.');
|
||||
} else if (mcpRegistration.status === 'missing_openclaw_config') {
|
||||
console.log(`OpenClaw MCP: skipped (config not found at ${mcpRegistration.openclawConfigPath}).`);
|
||||
console.log('Run `gbrain register-mcp` after OpenClaw creates its config.');
|
||||
} else {
|
||||
console.log('OpenClaw MCP: skipped (no gbrain config available).');
|
||||
}
|
||||
console.log('');
|
||||
console.log('When you outgrow local: gbrain migrate --to supabase');
|
||||
reportModStatus();
|
||||
@@ -200,6 +214,7 @@ async function initPostgres(opts: { databaseUrl: string; jsonOutput: boolean; ap
|
||||
...(opts.apiKey ? { openai_api_key: opts.apiKey } : {}),
|
||||
};
|
||||
saveConfig(config);
|
||||
const mcpRegistration = registerMcpServerInOpenClaw({ gbrainConfig: config });
|
||||
console.log('Config saved to ~/.gbrain/config.json');
|
||||
|
||||
const stats = await engine.getStats();
|
||||
@@ -217,6 +232,18 @@ async function initPostgres(opts: { databaseUrl: string; jsonOutput: boolean; ap
|
||||
} else {
|
||||
console.log('Next: gbrain import <dir>');
|
||||
}
|
||||
if (mcpRegistration.status === 'registered') {
|
||||
console.log(`OpenClaw MCP: registered gbrain at ${mcpRegistration.openclawConfigPath}`);
|
||||
console.log('Restart your OpenClaw gateway so all gbrain tools are discoverable.');
|
||||
} else if (mcpRegistration.status === 'already_registered') {
|
||||
console.log('OpenClaw MCP: gbrain already registered.');
|
||||
console.log('Restart your OpenClaw gateway if it is currently running.');
|
||||
} else if (mcpRegistration.status === 'missing_openclaw_config') {
|
||||
console.log(`OpenClaw MCP: skipped (config not found at ${mcpRegistration.openclawConfigPath}).`);
|
||||
console.log('Run `gbrain register-mcp` after OpenClaw creates its config.');
|
||||
} else {
|
||||
console.log('OpenClaw MCP: skipped (no gbrain config available).');
|
||||
}
|
||||
reportModStatus();
|
||||
}
|
||||
} finally {
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
import { existsSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'fs';
|
||||
import { dirname, join } from 'path';
|
||||
import { homedir } from 'os';
|
||||
import type { GBrainConfig } from '../core/config.ts';
|
||||
import { loadConfig } from '../core/config.ts';
|
||||
|
||||
type JsonObject = Record<string, unknown>;
|
||||
|
||||
export type RegisterMcpStatus =
|
||||
| 'registered'
|
||||
| 'already_registered'
|
||||
| 'missing_openclaw_config'
|
||||
| 'missing_gbrain_config';
|
||||
|
||||
export interface RegisterMcpResult {
|
||||
status: RegisterMcpStatus;
|
||||
openclawConfigPath: string;
|
||||
}
|
||||
|
||||
interface RegisterMcpOptions {
|
||||
openclawConfigPath?: string;
|
||||
gbrainInstallPath?: string;
|
||||
gbrainConfig?: GBrainConfig | null;
|
||||
}
|
||||
|
||||
function resolveOpenClawConfigPath(): string {
|
||||
return process.env.OPENCLAW_CONFIG_PATH
|
||||
|| join(homedir(), '.openclaw', 'openclaw.json');
|
||||
}
|
||||
|
||||
function getInstallPath(): string {
|
||||
return dirname(dirname(__dirname));
|
||||
}
|
||||
|
||||
function readJsonObject(path: string): JsonObject {
|
||||
const raw = readFileSync(path, 'utf-8');
|
||||
const parsed = JSON.parse(raw) as unknown;
|
||||
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
||||
throw new Error(`OpenClaw config must be a JSON object: ${path}`);
|
||||
}
|
||||
return parsed as JsonObject;
|
||||
}
|
||||
|
||||
function toObject(value: unknown): JsonObject {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
|
||||
return value as JsonObject;
|
||||
}
|
||||
|
||||
function atomicWriteJson(path: string, data: JsonObject): void {
|
||||
const tmpPath = `${path}.tmp-${process.pid}-${Date.now()}`;
|
||||
try {
|
||||
writeFileSync(tmpPath, JSON.stringify(data, null, 2) + '\n');
|
||||
renameSync(tmpPath, path);
|
||||
} catch (err) {
|
||||
try { unlinkSync(tmpPath); } catch { /* best-effort */ }
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
function buildGbrainServer(installPath: string, config: GBrainConfig): JsonObject {
|
||||
const env: JsonObject = {};
|
||||
if (config.database_url) {
|
||||
env.GBRAIN_DATABASE_URL = config.database_url;
|
||||
}
|
||||
|
||||
return {
|
||||
command: 'bun',
|
||||
args: ['run', join(installPath, 'src', 'cli.ts'), 'serve'],
|
||||
env,
|
||||
};
|
||||
}
|
||||
|
||||
export function registerMcpServerInOpenClaw(opts: RegisterMcpOptions = {}): RegisterMcpResult {
|
||||
const openclawConfigPath = opts.openclawConfigPath || resolveOpenClawConfigPath();
|
||||
if (!existsSync(openclawConfigPath)) {
|
||||
return { status: 'missing_openclaw_config', openclawConfigPath };
|
||||
}
|
||||
|
||||
const gbrainConfig = opts.gbrainConfig ?? loadConfig();
|
||||
if (!gbrainConfig) {
|
||||
return { status: 'missing_gbrain_config', openclawConfigPath };
|
||||
}
|
||||
|
||||
const config = readJsonObject(openclawConfigPath);
|
||||
const mcp = toObject(config.mcp);
|
||||
const servers = toObject(mcp.servers);
|
||||
if (servers.gbrain !== undefined) {
|
||||
return { status: 'already_registered', openclawConfigPath };
|
||||
}
|
||||
|
||||
const installPath = opts.gbrainInstallPath || getInstallPath();
|
||||
const next: JsonObject = {
|
||||
...config,
|
||||
mcp: {
|
||||
...mcp,
|
||||
servers: {
|
||||
...servers,
|
||||
gbrain: buildGbrainServer(installPath, gbrainConfig),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
atomicWriteJson(openclawConfigPath, next);
|
||||
return { status: 'registered', openclawConfigPath };
|
||||
}
|
||||
|
||||
function printHelp() {
|
||||
console.log(`Usage: gbrain register-mcp
|
||||
|
||||
Add gbrain's MCP stdio server to your OpenClaw config (idempotent).
|
||||
|
||||
OpenClaw config path:
|
||||
$OPENCLAW_CONFIG_PATH
|
||||
~/.openclaw/openclaw.json (default)
|
||||
`);
|
||||
}
|
||||
|
||||
export async function runRegisterMcp(args: string[]): Promise<void> {
|
||||
if (args.includes('--help') || args.includes('-h')) {
|
||||
printHelp();
|
||||
return;
|
||||
}
|
||||
|
||||
const result = registerMcpServerInOpenClaw();
|
||||
if (result.status === 'registered') {
|
||||
console.log(`Registered gbrain MCP server in ${result.openclawConfigPath}.`);
|
||||
console.log('Restart your OpenClaw gateway so gbrain tools are discoverable.');
|
||||
return;
|
||||
}
|
||||
if (result.status === 'already_registered') {
|
||||
console.log(`OpenClaw config already has mcp.servers.gbrain (${result.openclawConfigPath}).`);
|
||||
console.log('Restart your OpenClaw gateway if it is already running.');
|
||||
return;
|
||||
}
|
||||
if (result.status === 'missing_openclaw_config') {
|
||||
console.log(`OpenClaw config not found at ${result.openclawConfigPath}. Skipping MCP registration.`);
|
||||
console.log('Create/open OpenClaw once, then run: gbrain register-mcp');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('No gbrain config found. Run `gbrain init` first, then `gbrain register-mcp`.');
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
||||
import { mkdtempSync, writeFileSync, readFileSync, rmSync, mkdirSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { tmpdir } from 'os';
|
||||
import { registerMcpServerInOpenClaw } from '../src/commands/register-mcp.ts';
|
||||
|
||||
function tmpDir() {
|
||||
return mkdtempSync(join(tmpdir(), 'register-mcp-test-'));
|
||||
}
|
||||
|
||||
function writeJson(path: string, data: object) {
|
||||
writeFileSync(path, JSON.stringify(data, null, 2));
|
||||
}
|
||||
|
||||
function readJson(path: string) {
|
||||
return JSON.parse(readFileSync(path, 'utf-8'));
|
||||
}
|
||||
|
||||
describe('registerMcpServerInOpenClaw', () => {
|
||||
let dir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
dir = tmpDir();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('adds gbrain MCP server to empty config', () => {
|
||||
const configPath = join(dir, 'openclaw.json');
|
||||
writeJson(configPath, {});
|
||||
|
||||
const result = registerMcpServerInOpenClaw({
|
||||
openclawConfigPath: configPath,
|
||||
gbrainInstallPath: '/opt/gbrain',
|
||||
gbrainConfig: { database_url: 'postgres://localhost/brain', engine: 'postgres' },
|
||||
});
|
||||
|
||||
expect(result.status).toBe('registered');
|
||||
const config = readJson(configPath);
|
||||
expect(config.mcp.servers.gbrain).toBeDefined();
|
||||
expect(config.mcp.servers.gbrain.command).toBe('bun');
|
||||
expect(config.mcp.servers.gbrain.args).toContain('serve');
|
||||
expect(config.mcp.servers.gbrain.env.GBRAIN_DATABASE_URL).toBe('postgres://localhost/brain');
|
||||
});
|
||||
|
||||
test('adds alongside existing MCP servers', () => {
|
||||
const configPath = join(dir, 'openclaw.json');
|
||||
writeJson(configPath, {
|
||||
mcp: {
|
||||
servers: {
|
||||
circleback: { url: 'https://app.circleback.ai/api/mcp' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = registerMcpServerInOpenClaw({
|
||||
openclawConfigPath: configPath,
|
||||
gbrainInstallPath: '/opt/gbrain',
|
||||
gbrainConfig: { database_url: 'postgres://localhost/brain', engine: 'postgres' },
|
||||
});
|
||||
|
||||
expect(result.status).toBe('registered');
|
||||
const config = readJson(configPath);
|
||||
expect(config.mcp.servers.circleback).toBeDefined();
|
||||
expect(config.mcp.servers.gbrain).toBeDefined();
|
||||
});
|
||||
|
||||
test('skips if already registered (idempotent)', () => {
|
||||
const configPath = join(dir, 'openclaw.json');
|
||||
writeJson(configPath, {
|
||||
mcp: { servers: { gbrain: { command: 'bun', args: ['serve'] } } },
|
||||
});
|
||||
|
||||
const result = registerMcpServerInOpenClaw({
|
||||
openclawConfigPath: configPath,
|
||||
gbrainInstallPath: '/opt/gbrain',
|
||||
gbrainConfig: { database_url: 'postgres://localhost/brain', engine: 'postgres' },
|
||||
});
|
||||
|
||||
expect(result.status).toBe('already_registered');
|
||||
});
|
||||
|
||||
test('handles missing OpenClaw config gracefully', () => {
|
||||
const result = registerMcpServerInOpenClaw({
|
||||
openclawConfigPath: join(dir, 'nonexistent.json'),
|
||||
gbrainConfig: { database_url: 'postgres://localhost/brain', engine: 'postgres' },
|
||||
});
|
||||
|
||||
expect(result.status).toBe('missing_openclaw_config');
|
||||
});
|
||||
|
||||
test('preserves all existing config fields', () => {
|
||||
const configPath = join(dir, 'openclaw.json');
|
||||
const original = {
|
||||
agents: { defaults: { model: 'claude-3' } },
|
||||
gateway: { port: 18789 },
|
||||
plugins: { entries: { telegram: { enabled: true } } },
|
||||
mcp: { servers: { other: { url: 'https://example.com' } } },
|
||||
};
|
||||
writeJson(configPath, original);
|
||||
|
||||
registerMcpServerInOpenClaw({
|
||||
openclawConfigPath: configPath,
|
||||
gbrainInstallPath: '/opt/gbrain',
|
||||
gbrainConfig: { database_url: 'postgres://localhost/brain', engine: 'postgres' },
|
||||
});
|
||||
|
||||
const config = readJson(configPath);
|
||||
expect(config.agents.defaults.model).toBe('claude-3');
|
||||
expect(config.gateway.port).toBe(18789);
|
||||
expect(config.plugins.entries.telegram.enabled).toBe(true);
|
||||
expect(config.mcp.servers.other.url).toBe('https://example.com');
|
||||
expect(config.mcp.servers.gbrain).toBeDefined();
|
||||
});
|
||||
|
||||
test('handles null gbrainConfig by falling back to loadConfig', () => {
|
||||
const configPath = join(dir, 'openclaw.json');
|
||||
writeJson(configPath, {});
|
||||
|
||||
// When gbrainConfig is null AND loadConfig returns null, status is missing_gbrain_config.
|
||||
// But on dev machines loadConfig() may succeed, so we just verify it doesn't crash
|
||||
// and returns a valid status.
|
||||
const result = registerMcpServerInOpenClaw({
|
||||
openclawConfigPath: configPath,
|
||||
gbrainConfig: null,
|
||||
});
|
||||
|
||||
expect(['missing_gbrain_config', 'registered']).toContain(result.status);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user