mirror of
https://github.com/Heretek-AI/heretek-openclaw.git
synced 2026-08-24 15:57:58 -04:00
914 lines
31 KiB
JavaScript
Executable File
914 lines
31 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
||
|
||
/**
|
||
* Heretek OpenClaw Interactive Setup Wizard
|
||
*
|
||
* A guided setup experience for first-time users.
|
||
* Collects configuration, validates API keys, and generates configuration files.
|
||
*/
|
||
|
||
import readline from 'readline';
|
||
import fs from 'fs';
|
||
import path from 'path';
|
||
import { fileURLToPath } from 'url';
|
||
import { execSync } from 'child_process';
|
||
|
||
const __filename = fileURLToPath(import.meta.url);
|
||
const __dirname = path.dirname(__filename);
|
||
const ROOT_DIR = path.resolve(__dirname, '..');
|
||
|
||
// ANSI color codes for terminal output
|
||
const colors = {
|
||
reset: '\x1b[0m',
|
||
bright: '\x1b[1m',
|
||
dim: '\x1b[2m',
|
||
red: '\x1b[31m',
|
||
green: '\x1b[32m',
|
||
yellow: '\x1b[33m',
|
||
blue: '\x1b[34m',
|
||
magenta: '\x1b[35m',
|
||
cyan: '\x1b[36m',
|
||
white: '\x1b[37m',
|
||
};
|
||
|
||
const symbols = {
|
||
success: '✓',
|
||
error: '✗',
|
||
warning: '⚠',
|
||
info: 'ℹ',
|
||
arrow: '→',
|
||
check: '☑',
|
||
empty: '☐',
|
||
};
|
||
|
||
// Configuration state
|
||
const config = {
|
||
// Deployment type
|
||
deploymentType: 'docker', // 'docker' or 'non-docker'
|
||
|
||
// AI Provider selection
|
||
primaryProvider: null,
|
||
failoverProvider: null,
|
||
|
||
// API Keys
|
||
apiKey: {
|
||
minimax: null,
|
||
zai: null,
|
||
openai: null,
|
||
anthropic: null,
|
||
google: null,
|
||
ollama: null,
|
||
},
|
||
|
||
// Database configuration
|
||
database: {
|
||
type: 'postgresql', // 'postgresql' or 'redis-only'
|
||
postgresUser: 'openclaw',
|
||
postgresPassword: null,
|
||
postgresDb: 'openclaw',
|
||
postgresHost: 'localhost',
|
||
postgresPort: '5432',
|
||
redisHost: 'localhost',
|
||
redisPort: '6379',
|
||
},
|
||
|
||
// Agent configuration
|
||
agents: {
|
||
enableAll: true,
|
||
selectedAgents: [],
|
||
},
|
||
|
||
// Observability
|
||
langfuse: {
|
||
enabled: false,
|
||
publicKey: null,
|
||
secretKey: null,
|
||
host: 'https://cloud.langfuse.com',
|
||
},
|
||
|
||
// Paths
|
||
openclawDir: path.join(process.env.HOME || '', '.openclaw'),
|
||
openclawWorkspace: path.join(process.env.HOME || '', '.openclaw', 'agents'),
|
||
};
|
||
|
||
// Available providers
|
||
const providers = [
|
||
{ id: 'minimax', name: 'MiniMax', description: 'Primary LLM provider (recommended)', envVar: 'MINIMAX_API_KEY' },
|
||
{ id: 'zai', name: 'z.ai', description: 'Failover LLM provider', envVar: 'ZAI_API_KEY' },
|
||
{ id: 'openai', name: 'OpenAI', description: 'OpenAI GPT models', envVar: 'OPENAI_API_KEY' },
|
||
{ id: 'anthropic', name: 'Anthropic', description: 'Claude models', envVar: 'ANTHROPIC_API_KEY' },
|
||
{ id: 'google', name: 'Google', description: 'Gemini models', envVar: 'GOOGLE_API_KEY' },
|
||
{ id: 'ollama', name: 'Ollama', description: 'Local models (free)', envVar: 'OLLAMA_HOST' },
|
||
];
|
||
|
||
// Available agents
|
||
const availableAgents = [
|
||
{ id: 'steward', name: 'Steward', role: 'Orchestrator', description: 'Orchestrates the collective' },
|
||
{ id: 'alpha', name: 'Alpha', role: 'Triad', description: 'Primary deliberator' },
|
||
{ id: 'beta', name: 'Beta', role: 'Triad', description: 'Critical analyst' },
|
||
{ id: 'charlie', name: 'Charlie', role: 'Triad', description: 'Process validator' },
|
||
{ id: 'examiner', name: 'Examiner', role: 'Interrogator', description: 'Questions decisions' },
|
||
{ id: 'explorer', name: 'Explorer', role: 'Scout', description: 'Scouts opportunities' },
|
||
{ id: 'sentinel', name: 'Sentinel', role: 'Guardian', description: 'Security monitor' },
|
||
{ id: 'coder', name: 'Coder', role: 'Artisan', description: 'Code generation' },
|
||
{ id: 'dreamer', name: 'Dreamer', role: 'Visionary', description: 'Creative speculation' },
|
||
{ id: 'empath', name: 'Empath', role: 'Diplomat', description: 'Emotional intelligence' },
|
||
{ id: 'historian', name: 'Historian', role: 'Archivist', description: 'Memory keeper' },
|
||
];
|
||
|
||
// Create readline interface
|
||
const rl = readline.createInterface({
|
||
input: process.stdin,
|
||
output: process.stdout,
|
||
});
|
||
|
||
/**
|
||
* Async prompt function
|
||
*/
|
||
function prompt(question, defaultValue = null) {
|
||
return new Promise((resolve) => {
|
||
const defaultText = defaultValue ? ` [${defaultValue}]` : '';
|
||
rl.question(`${colors.cyan}${question}${defaultText}: ${colors.reset}`, (answer) => {
|
||
resolve(answer.trim() || defaultValue || '');
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Selection prompt
|
||
*/
|
||
async function select(options, question) {
|
||
console.log(`\n${colors.cyan}${question}${colors.reset}`);
|
||
options.forEach((opt, idx) => {
|
||
const desc = opt.description ? ` - ${colors.dim}${opt.description}${colors.reset}` : '';
|
||
console.log(` ${colors.yellow}${idx + 1}${colors.reset}) ${opt.name}${desc}`);
|
||
});
|
||
|
||
while (true) {
|
||
const answer = await prompt(`Enter choice (1-${options.length})`);
|
||
const idx = parseInt(answer, 10) - 1;
|
||
if (idx >= 0 && idx < options.length) {
|
||
return options[idx];
|
||
}
|
||
console.log(`${colors.red}${symbols.error} Invalid choice. Please try again.${colors.reset}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Multi-selection prompt
|
||
*/
|
||
async function multiSelect(options, question, preSelected = []) {
|
||
console.log(`\n${colors.cyan}${question}${colors.reset}`);
|
||
console.log(`${colors.dim}(Press Enter to confirm selection)${colors.reset}\n`);
|
||
|
||
const selected = new Set(preSelected);
|
||
|
||
while (true) {
|
||
options.forEach((opt, idx) => {
|
||
const isSelected = selected.has(opt.id);
|
||
const symbol = isSelected ? colors.green + symbols.check : colors.dim + symbols.empty;
|
||
console.log(` ${symbol}${colors.reset} ${colors.yellow}${idx + 1}${colors.reset}) ${opt.name} - ${opt.description}`);
|
||
});
|
||
|
||
const answer = await prompt('\nEnter choice (1-' + options.length + ', or Enter to confirm)');
|
||
|
||
if (answer === '') {
|
||
if (selected.size === 0) {
|
||
console.log(`${colors.yellow}${symbols.warning} Please select at least one option.${colors.reset}`);
|
||
continue;
|
||
}
|
||
return Array.from(selected);
|
||
}
|
||
|
||
const idx = parseInt(answer, 10) - 1;
|
||
if (idx >= 0 && idx < options.length) {
|
||
const id = options[idx].id;
|
||
if (selected.has(id)) {
|
||
selected.delete(id);
|
||
} else {
|
||
selected.add(id);
|
||
}
|
||
} else {
|
||
console.log(`${colors.red}${symbols.error} Invalid choice.${colors.reset}`);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Password prompt (hidden input)
|
||
*/
|
||
function passwordPrompt(question) {
|
||
return new Promise((resolve) => {
|
||
readline.emit('start');
|
||
process.stdout.write(`${colors.cyan}${question}: ${colors.reset}`);
|
||
|
||
const stdin = process.stdin;
|
||
stdin.setRawMode(true);
|
||
stdin.resume();
|
||
stdin.setEncoding('utf8');
|
||
|
||
let password = '';
|
||
|
||
const onData = (key) => {
|
||
if (key === '\u0003') { // Ctrl+C
|
||
process.exit(0);
|
||
} else if (key === '\r' || key === '\n') { // Enter
|
||
stdin.setRawMode(false);
|
||
stdin.pause();
|
||
console.log();
|
||
resolve(password);
|
||
} else if (key === '\u007f' || key === '\b') { // Backspace
|
||
if (password.length > 0) {
|
||
password = password.slice(0, -1);
|
||
process.stdout.write('\b \b');
|
||
}
|
||
} else if (key.length === 1 && key >= ' ') {
|
||
password += key;
|
||
process.stdout.write('*');
|
||
}
|
||
};
|
||
|
||
stdin.on('data', onData);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Validate API key format
|
||
*/
|
||
function validateApiKey(key, provider) {
|
||
if (!key || key.length < 10) {
|
||
return false;
|
||
}
|
||
|
||
// Basic validation patterns
|
||
const patterns = {
|
||
minimax: /^[\w-]{20,}$/,
|
||
zai: /^[\w-]{20,}$/,
|
||
openai: /^sk-[\w-]{20,}$/,
|
||
anthropic: /^sk-ant-[\w-]{20,}$/,
|
||
google: /^[\w-]{20,}$/,
|
||
};
|
||
|
||
const pattern = patterns[provider];
|
||
if (pattern) {
|
||
return pattern.test(key);
|
||
}
|
||
|
||
return key.length >= 20;
|
||
}
|
||
|
||
/**
|
||
* Test API key (mock - in real implementation would make actual API call)
|
||
*/
|
||
async function testApiKey(key, provider) {
|
||
console.log(`${colors.yellow}${symbols.info} Validating ${provider} API key...${colors.reset}`);
|
||
|
||
// Mock validation - in production, this would make actual API calls
|
||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||
|
||
if (validateApiKey(key, provider)) {
|
||
console.log(`${colors.green}${symbols.success} ${provider} API key is valid${colors.reset}`);
|
||
return true;
|
||
}
|
||
|
||
console.log(`${colors.yellow}${symbols.warning} Could not validate ${provider} API key format${colors.reset}`);
|
||
return true; // Accept anyway for local setup
|
||
}
|
||
|
||
/**
|
||
* Generate secure random string
|
||
*/
|
||
function generateSecureKey(length = 32) {
|
||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'
|
||
let result = '';
|
||
for (let i = 0; i < length; i++) {
|
||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||
}
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* Check if Docker is available
|
||
*/
|
||
function checkDocker() {
|
||
try {
|
||
execSync('docker --version', { stdio: 'ignore' });
|
||
execSync('docker compose version', { stdio: 'ignore' });
|
||
return true;
|
||
} catch {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Check if Node.js is available
|
||
*/
|
||
function checkNode() {
|
||
try {
|
||
const version = execSync('node --version', { encoding: 'utf8' }).trim();
|
||
return { available: true, version };
|
||
} catch {
|
||
return { available: false };
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Print header
|
||
*/
|
||
function printHeader() {
|
||
console.log(`
|
||
${colors.cyan}╔══════════════════════════════════════════════════════════╗${colors.reset}
|
||
${colors.cyan}║${colors.reset} ${colors.cyan}║${colors.reset}
|
||
${colors.cyan}║${colors.reset} ${colors.bright}Heretek OpenClaw Setup Wizard${colors.reset} ${colors.cyan}║${colors.reset}
|
||
${colors.cyan}║${colors.reset} ${colors.dim}Interactive configuration for first-time users${colors.reset} ${colors.cyan}║${colors.reset}
|
||
${colors.cyan}║${colors.reset} ${colors.cyan}║${colors.reset}
|
||
${colors.cyan}╚══════════════════════════════════════════════════════════╝${colors.reset}
|
||
`);
|
||
}
|
||
|
||
/**
|
||
* Print section header
|
||
*/
|
||
function printSection(title) {
|
||
console.log(`\n${colors.magenta}═══════════════════════════════════════════════════════${colors.reset}`);
|
||
console.log(`${colors.magenta}${symbols.arrow} ${title}${colors.reset}`);
|
||
console.log(`${colors.magenta}═══════════════════════════════════════════════════════${colors.reset}\n`);
|
||
}
|
||
|
||
/**
|
||
* Step 1: Welcome and deployment type selection
|
||
*/
|
||
async function stepWelcome() {
|
||
printHeader();
|
||
|
||
console.log(`${colors.white}Welcome to Heretek OpenClaw!${colors.reset}`);
|
||
console.log(`This wizard will guide you through the setup process.\n`);
|
||
|
||
// Check prerequisites
|
||
console.log(`${colors.cyan}Checking prerequisites...${colors.reset}`);
|
||
|
||
const dockerAvailable = checkDocker();
|
||
const nodeCheck = checkNode();
|
||
|
||
if (dockerAvailable) {
|
||
console.log(`${colors.green}${symbols.success} Docker is installed${colors.reset}`);
|
||
} else {
|
||
console.log(`${colors.yellow}${symbols.warning} Docker not found - will configure non-Docker mode${colors.reset}`);
|
||
}
|
||
|
||
if (nodeCheck.available) {
|
||
console.log(`${colors.green}${symbols.success} Node.js ${nodeCheck.version} found${colors.reset}`);
|
||
} else {
|
||
console.log(`${colors.red}${symbols.error} Node.js not found - required for OpenClaw Gateway${colors.reset}`);
|
||
}
|
||
|
||
// Deployment type selection
|
||
printSection('Step 1: Deployment Type');
|
||
|
||
if (dockerAvailable) {
|
||
console.log('Choose your deployment type:\n');
|
||
console.log(` ${colors.yellow}1${colors.reset}) ${colors.green}Docker${colors.reset} - Recommended for most users`);
|
||
console.log(` Includes PostgreSQL, Redis, Ollama, Langfuse containers`);
|
||
console.log(` ${colors.yellow}2${colors.reset}) ${colors.yellow}Non-Docker${colors.reset} - Manual infrastructure setup`);
|
||
console.log(` Requires existing database and services\n`);
|
||
|
||
const choice = await select(
|
||
[
|
||
{ id: 'docker', name: 'Docker', description: 'Recommended' },
|
||
{ id: 'non-docker', name: 'Non-Docker', description: 'Manual setup' },
|
||
],
|
||
'Select deployment type:'
|
||
);
|
||
|
||
config.deploymentType = choice.id;
|
||
} else {
|
||
console.log(`${colors.yellow}${symbols.warning} Docker not available, configuring for non-Docker deployment${colors.reset}`);
|
||
config.deploymentType = 'non-docker';
|
||
}
|
||
|
||
console.log(`\n${colors.green}${symbols.success} Deployment type: ${config.deploymentType}${colors.reset}`);
|
||
}
|
||
|
||
/**
|
||
* Step 2: AI Provider selection
|
||
*/
|
||
async function stepProviderSelection() {
|
||
printSection('Step 2: AI Provider Selection');
|
||
|
||
console.log('Select your primary AI provider:\n');
|
||
|
||
const primary = await select(providers, 'Primary provider:');
|
||
config.primaryProvider = primary.id;
|
||
|
||
console.log(`\n${colors.green}${symbols.success} Primary provider: ${primary.name}${colors.reset}`);
|
||
|
||
// Select failover provider (different from primary)
|
||
const failoverOptions = providers.filter((p) => p.id !== primary.id);
|
||
|
||
console.log(`\nSelect a failover provider (used when primary is unavailable):\n`);
|
||
console.log(` ${colors.yellow}0${colors.reset}) Skip failover configuration\n`);
|
||
|
||
const failover = await select(
|
||
[{ id: 'none', name: 'None', description: 'No failover' }, ...failoverOptions],
|
||
'Failover provider:'
|
||
);
|
||
|
||
if (failover.id !== 'none') {
|
||
config.failoverProvider = failover.id;
|
||
console.log(`\n${colors.green}${symbols.success} Failover provider: ${failover.name}${colors.reset}`);
|
||
} else {
|
||
console.log(`\n${colors.dim}No failover provider configured${colors.reset}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Step 3: API Key collection
|
||
*/
|
||
async function stepApiKeys() {
|
||
printSection('Step 3: API Key Configuration');
|
||
|
||
console.log('Enter your API keys for the selected providers.\n');
|
||
console.log(`${colors.dim}Keys are stored locally in .env file and never transmitted.${colors.reset}\n`);
|
||
|
||
// Primary provider key
|
||
const primaryProvider = providers.find((p) => p.id === config.primaryProvider);
|
||
if (primaryProvider) {
|
||
const key = await passwordPrompt(`${primaryProvider.name} API Key`);
|
||
if (key) {
|
||
config.apiKey[primaryProvider.id] = key;
|
||
await testApiKey(key, primaryProvider.id);
|
||
}
|
||
}
|
||
|
||
// Failover provider key
|
||
if (config.failoverProvider) {
|
||
const failoverProvider = providers.find((p) => p.id === config.failoverProvider);
|
||
if (failoverProvider) {
|
||
const key = await passwordPrompt(`${failoverProvider.name} API Key`);
|
||
if (key) {
|
||
config.apiKey[failoverProvider.id] = key;
|
||
await testApiKey(key, failoverProvider.id);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Optional providers
|
||
console.log(`\n${colors.cyan}Optional API Keys:${colors.reset}`);
|
||
|
||
const optionalProviders = providers.filter(
|
||
(p) => p.id !== config.primaryProvider && p.id !== config.failoverProvider
|
||
);
|
||
|
||
for (const provider of optionalProviders) {
|
||
const answer = await prompt(`Add ${provider.name} API key? (y/N)`);
|
||
if (answer.toLowerCase() === 'y') {
|
||
const key = await passwordPrompt(`${provider.name} API Key`);
|
||
if (key) {
|
||
config.apiKey[provider.id] = key;
|
||
await testApiKey(key, provider.id);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Langfuse observability
|
||
console.log(`\n${colors.cyan}Observability (Langfuse):${colors.reset}`);
|
||
const langfuseAnswer = await prompt('Enable Langfuse observability? (y/N)');
|
||
|
||
if (langfuseAnswer.toLowerCase() === 'y') {
|
||
config.langfuse.enabled = true;
|
||
config.langfuse.publicKey = await prompt('Langfuse Public Key');
|
||
config.langfuse.secretKey = await passwordPrompt('Langfuse Secret Key');
|
||
console.log(`${colors.green}${symbols.success} Langfuse configured${colors.reset}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Step 4: Database configuration
|
||
*/
|
||
async function stepDatabase() {
|
||
printSection('Step 4: Database Configuration');
|
||
|
||
if (config.deploymentType === 'docker') {
|
||
console.log('Docker deployment includes PostgreSQL and Redis.');
|
||
console.log('Configure database credentials:\n');
|
||
|
||
config.database.postgresPassword = generateSecureKey(24);
|
||
console.log(`${colors.dim}Generated secure database password${colors.reset}`);
|
||
|
||
const customDb = await prompt('Use custom database name? (openclaw)', 'openclaw');
|
||
if (customDb && customDb !== 'openclaw') {
|
||
config.database.postgresDb = customDb;
|
||
}
|
||
} else {
|
||
console.log('Enter your database connection details:\n');
|
||
|
||
const dbType = await select(
|
||
[
|
||
{ id: 'postgresql', name: 'PostgreSQL', description: 'Full database with vector support' },
|
||
{ id: 'redis-only', name: 'Redis Only', description: 'Session storage only' },
|
||
],
|
||
'Database type:'
|
||
);
|
||
|
||
config.database.type = dbType.id;
|
||
|
||
if (dbType.id === 'postgresql') {
|
||
config.database.postgresHost = await prompt('PostgreSQL Host', 'localhost');
|
||
config.database.postgresPort = await prompt('PostgreSQL Port', '5432');
|
||
config.database.postgresUser = await prompt('PostgreSQL User', 'openclaw');
|
||
config.database.postgresPassword = await passwordPrompt('PostgreSQL Password');
|
||
config.database.postgresDb = await prompt('Database Name', 'openclaw');
|
||
}
|
||
|
||
config.database.redisHost = await prompt('Redis Host', 'localhost');
|
||
config.database.redisPort = await prompt('Redis Port', '6379');
|
||
}
|
||
|
||
console.log(`\n${colors.green}${symbols.success} Database configured: ${config.database.type}${colors.reset}`);
|
||
}
|
||
|
||
/**
|
||
* Step 5: Agent configuration
|
||
*/
|
||
async function stepAgents() {
|
||
printSection('Step 5: Agent Configuration');
|
||
|
||
console.log('Configure which agents to enable:\n');
|
||
console.log(` ${colors.yellow}1${colors.reset}) Enable all agents (recommended)`);
|
||
console.log(` ${colors.yellow}2${colors.reset}) Select specific agents\n`);
|
||
|
||
const choice = await select(
|
||
[
|
||
{ id: 'all', name: 'Enable All Agents', description: 'Recommended for full functionality' },
|
||
{ id: 'select', name: 'Select Agents', description: 'Choose specific agents' },
|
||
],
|
||
'Agent configuration:'
|
||
);
|
||
|
||
if (choice.id === 'all') {
|
||
config.agents.enableAll = true;
|
||
config.agents.selectedAgents = availableAgents.map((a) => a.id);
|
||
console.log(`\n${colors.green}${symbols.success} All ${availableAgents.length} agents enabled${colors.reset}`);
|
||
} else {
|
||
config.agents.enableAll = false;
|
||
config.agents.selectedAgents = await multiSelect(
|
||
availableAgents,
|
||
'Select agents to enable:',
|
||
['steward'] // Pre-select steward
|
||
);
|
||
console.log(`\n${colors.green}${symbols.success} ${config.agents.selectedAgents.length} agents selected${colors.reset}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Step 6: Path configuration
|
||
*/
|
||
async function stepPaths() {
|
||
printSection('Step 6: Path Configuration');
|
||
|
||
console.log('Configure OpenClaw workspace paths:\n');
|
||
|
||
const defaultDir = path.join(process.env.HOME || '', '.openclaw');
|
||
const customDir = await prompt('OpenClaw directory', defaultDir);
|
||
|
||
if (customDir && customDir !== defaultDir) {
|
||
config.openclawDir = customDir;
|
||
}
|
||
|
||
config.openclawWorkspace = path.join(config.openclawDir, 'agents');
|
||
|
||
console.log(`\n${colors.green}${symbols.success} OpenClaw directory: ${config.openclawDir}${colors.reset}`);
|
||
console.log(`${colors.green}${symbols.success} Agents workspace: ${config.openclawWorkspace}${colors.reset}`);
|
||
}
|
||
|
||
/**
|
||
* Step 7: Review and confirm
|
||
*/
|
||
async function stepReview() {
|
||
printSection('Step 7: Review Configuration');
|
||
|
||
console.log('Please review your configuration:\n');
|
||
|
||
console.log(`${colors.cyan}Deployment:${colors.reset}`);
|
||
console.log(` Type: ${config.deploymentType}`);
|
||
|
||
console.log(`\n${colors.cyan}AI Providers:${colors.reset}`);
|
||
console.log(` Primary: ${config.primaryProvider}`);
|
||
if (config.failoverProvider) {
|
||
console.log(` Failover: ${config.failoverProvider}`);
|
||
}
|
||
|
||
console.log(`\n${colors.cyan}API Keys Configured:${colors.reset}`);
|
||
Object.entries(config.apiKey).forEach(([key, value]) => {
|
||
if (value) {
|
||
console.log(` ${colors.green}${symbols.check}${colors.reset} ${key}`);
|
||
}
|
||
});
|
||
|
||
console.log(`\n${colors.cyan}Database:${colors.reset}`);
|
||
console.log(` Type: ${config.database.type}`);
|
||
if (config.database.type === 'postgresql') {
|
||
console.log(` Host: ${config.database.postgresHost}:${config.database.postgresPort}`);
|
||
console.log(` Database: ${config.database.postgresDb}`);
|
||
}
|
||
console.log(` Redis: ${config.database.redisHost}:${config.database.redisPort}`);
|
||
|
||
console.log(`\n${colors.cyan}Agents:${colors.reset}`);
|
||
console.log(` ${config.agents.enableAll ? 'All agents enabled' : `${config.agents.selectedAgents.length} agents selected`}`);
|
||
|
||
if (config.langfuse.enabled) {
|
||
console.log(`\n${colors.cyan}Observability:${colors.reset}`);
|
||
console.log(` ${colors.green}${symbols.check}${colors.reset} Langfuse enabled`);
|
||
}
|
||
|
||
console.log(`\n${colors.cyan}Paths:${colors.reset}`);
|
||
console.log(` OpenClaw Dir: ${config.openclawDir}`);
|
||
console.log(` Workspace: ${config.openclawWorkspace}`);
|
||
|
||
const confirm = await prompt('\nProceed with this configuration? (Y/n)', 'y');
|
||
|
||
if (confirm.toLowerCase() === 'n') {
|
||
console.log(`${colors.yellow}${symbols.warning} Setup cancelled.${colors.reset}`);
|
||
process.exit(0);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Generate .env file
|
||
*/
|
||
function generateEnvFile() {
|
||
const envContent = `# Heretek OpenClaw Environment Configuration
|
||
# Generated by setup-wizard.js on ${new Date().toISOString()}
|
||
|
||
# =============================================================================
|
||
# LiteLLM Gateway Configuration
|
||
# =============================================================================
|
||
LITELLM_MASTER_KEY=${generateSecureKey(32)}
|
||
LITELLM_SALT_KEY=${generateSecureKey(32)}
|
||
|
||
# =============================================================================
|
||
# AI Provider API Keys
|
||
# =============================================================================
|
||
# Primary Provider: ${config.primaryProvider}
|
||
${config.apiKey.minimax ? `MINIMAX_API_KEY=${config.apiKey.minimax}` : '# MINIMAX_API_KEY=your_minimax_api_key'}
|
||
${config.apiKey.zai ? `ZAI_API_KEY=${config.apiKey.zai}` : '# ZAI_API_KEY=your_zai_api_key'}
|
||
${config.apiKey.openai ? `OPENAI_API_KEY=${config.apiKey.openai}` : '# OPENAI_API_KEY=your_openai_api_key'}
|
||
${config.apiKey.anthropic ? `ANTHROPIC_API_KEY=${config.apiKey.anthropic}` : '# ANTHROPIC_API_KEY=your_anthropic_api_key'}
|
||
${config.apiKey.google ? `GOOGLE_API_KEY=${config.apiKey.google}` : '# GOOGLE_API_KEY=your_google_api_key'}
|
||
|
||
# Ollama (Local Models)
|
||
OLLAMA_HOST=ollama:11434
|
||
|
||
# =============================================================================
|
||
# Database Configuration
|
||
# =============================================================================
|
||
# PostgreSQL
|
||
POSTGRES_USER=${config.database.postgresUser}
|
||
POSTGRES_PASSWORD=${config.database.postgresPassword}
|
||
POSTGRES_DB=${config.database.postgresDb}
|
||
DATABASE_URL=postgresql://${config.database.postgresUser}:${config.database.postgresPassword}@${config.database.postgresHost}:${config.database.postgresPort}/${config.database.postgresDb}
|
||
|
||
# Redis
|
||
REDIS_URL=redis://${config.database.redisHost}:${config.database.redisPort}
|
||
|
||
# =============================================================================
|
||
# OpenClaw Gateway Configuration
|
||
# =============================================================================
|
||
OPENCLAW_DIR=${config.openclawDir}
|
||
OPENCLAW_WORKSPACE=${config.openclawWorkspace}
|
||
|
||
# =============================================================================
|
||
# Observability (Langfuse)
|
||
# =============================================================================
|
||
LANGFUSE_ENABLED=${config.langfuse.enabled}
|
||
${config.langfuse.publicKey ? `LANGFUSE_PUBLIC_KEY=${config.langfuse.publicKey}` : '# LANGFUSE_PUBLIC_KEY=your_langfuse_public_key'}
|
||
${config.langfuse.secretKey ? `LANGFUSE_SECRET_KEY=${config.langfuse.secretKey}` : '# LANGFUSE_SECRET_KEY=your_langfuse_secret_key'}
|
||
LANGFUSE_HOST=${config.langfuse.host}
|
||
|
||
# =============================================================================
|
||
# Deployment Configuration
|
||
# =============================================================================
|
||
DEPLOYMENT_TYPE=${config.deploymentType}
|
||
`;
|
||
|
||
return envContent;
|
||
}
|
||
|
||
/**
|
||
* Generate openclaw.json configuration
|
||
*/
|
||
function generateOpenClawConfig() {
|
||
const baseConfig = JSON.parse(fs.readFileSync(path.join(ROOT_DIR, 'openclaw.json'), 'utf8'));
|
||
|
||
// Filter agents based on selection
|
||
if (!config.agents.enableAll) {
|
||
baseConfig.agents = baseConfig.agents.filter((agent) =>
|
||
config.agents.selectedAgents.includes(agent.id)
|
||
);
|
||
}
|
||
|
||
// Update model routing based on provider selection
|
||
if (config.primaryProvider === 'openai') {
|
||
baseConfig.model_routing.default = 'openai/gpt-4o';
|
||
} else if (config.primaryProvider === 'anthropic') {
|
||
baseConfig.model_routing.default = 'anthropic/claude-sonnet-4-20250514';
|
||
} else if (config.primaryProvider === 'google') {
|
||
baseConfig.model_routing.default = 'google/gemini-2.5-pro';
|
||
}
|
||
|
||
if (config.failoverProvider === 'openai') {
|
||
baseConfig.model_routing.aliases.failover = 'openai/gpt-4o-mini';
|
||
} else if (config.failoverProvider === 'zai') {
|
||
baseConfig.model_routing.aliases.failover = 'zai/glm-5-1';
|
||
}
|
||
|
||
return JSON.stringify(baseConfig, null, 2);
|
||
}
|
||
|
||
/**
|
||
* Generate docker-compose.override.yml for custom configuration
|
||
*/
|
||
function generateDockerComposeOverride() {
|
||
return `# Docker Compose Override
|
||
# Generated by setup-wizard.js
|
||
# Place this file alongside docker-compose.yml to override default configuration
|
||
|
||
version: '3.8'
|
||
|
||
services:
|
||
litellm:
|
||
env_file:
|
||
- .env
|
||
|
||
postgres:
|
||
environment:
|
||
POSTGRES_USER: ${config.database.postgresUser}
|
||
POSTGRES_PASSWORD: ${config.database.postgresPassword}
|
||
POSTGRES_DB: ${config.database.postgresDb}
|
||
ports:
|
||
- "127.0.0.1:5432:5432"
|
||
|
||
redis:
|
||
ports:
|
||
- "127.0.0.1:6379:6379"
|
||
|
||
ollama:
|
||
ports:
|
||
- "127.0.0.1:11434:11434"
|
||
|
||
${config.langfuse.enabled ? `
|
||
langfuse:
|
||
environment:
|
||
- LANGFUSE_PUBLIC_KEY=${config.langfuse.publicKey}
|
||
- LANGFUSE_SECRET_KEY=${config.langfuse.secretKey}
|
||
ports:
|
||
- "127.0.0.1:3000:3000"
|
||
` : ''}
|
||
`;
|
||
}
|
||
|
||
/**
|
||
* Generate setup script for non-Docker deployment
|
||
*/
|
||
function generateSetupScript() {
|
||
return `#!/bin/bash
|
||
# OpenClaw Setup Script
|
||
# Generated by setup-wizard.js on ${new Date().toISOString()}
|
||
|
||
set -e
|
||
|
||
echo "Setting up Heretek OpenClaw..."
|
||
|
||
# Create directories
|
||
mkdir -p ${config.openclawDir}
|
||
mkdir -p ${config.openclawWorkspace}
|
||
|
||
# Copy configuration
|
||
cp openclaw.json ${config.openclawDir}/openclaw.json
|
||
|
||
# Install OpenClaw Gateway (if not already installed)
|
||
if ! command -v openclaw &> /dev/null; then
|
||
echo "Installing OpenClaw Gateway..."
|
||
curl -fsSL https://openclaw.ai/install.sh | bash
|
||
fi
|
||
|
||
# Initialize daemon
|
||
openclaw onboard --install-daemon
|
||
|
||
# Create agent workspaces
|
||
echo "Creating agent workspaces..."
|
||
${config.agents.selectedAgents.map((agentId) => {
|
||
const agent = availableAgents.find((a) => a.id === agentId);
|
||
return `./agents/deploy-agent.sh ${agentId} ${agent?.role || 'agent'}`;
|
||
}).join('\n')}
|
||
|
||
echo ""
|
||
echo "Setup complete!"
|
||
echo "Run 'openclaw gateway status' to verify installation"
|
||
`;
|
||
}
|
||
|
||
/**
|
||
* Write configuration files
|
||
*/
|
||
async function stepWriteConfig() {
|
||
printSection('Writing Configuration Files');
|
||
|
||
// Write .env file
|
||
const envPath = path.join(ROOT_DIR, '.env');
|
||
fs.writeFileSync(envPath, generateEnvFile(), 'utf8');
|
||
console.log(`${colors.green}${symbols.success} Created .env file${colors.reset}`);
|
||
|
||
// Write docker-compose.override.yml (Docker mode)
|
||
if (config.deploymentType === 'docker') {
|
||
const overridePath = path.join(ROOT_DIR, 'docker-compose.override.yml');
|
||
fs.writeFileSync(overridePath, generateDockerComposeOverride(), 'utf8');
|
||
console.log(`${colors.green}${symbols.success} Created docker-compose.override.yml${colors.reset}`);
|
||
}
|
||
|
||
// Write setup script (non-Docker mode)
|
||
if (config.deploymentType === 'non-docker') {
|
||
const setupScriptPath = path.join(ROOT_DIR, 'scripts', 'setup-openclaw.sh');
|
||
fs.writeFileSync(setupScriptPath, generateSetupScript(), 'utf8');
|
||
fs.chmodSync(setupScriptPath, '755');
|
||
console.log(`${colors.green}${symbols.success} Created scripts/setup-openclaw.sh${colors.reset}`);
|
||
}
|
||
|
||
// Create agent directories
|
||
fs.mkdirSync(config.openclawDir, { recursive: true });
|
||
fs.mkdirSync(config.openclawWorkspace, { recursive: true });
|
||
console.log(`${colors.green}${symbols.success} Created OpenClaw directories${colors.reset}`);
|
||
|
||
// Copy openclaw.json to workspace
|
||
const workspaceConfigPath = path.join(config.openclawDir, 'openclaw.json');
|
||
fs.writeFileSync(workspaceConfigPath, generateOpenClawConfig(), 'utf8');
|
||
console.log(`${colors.green}${symbols.success} Created ${workspaceConfigPath}${colors.reset}`);
|
||
|
||
console.log(`\n${colors.green}${symbols.success} Configuration files written successfully!${colors.reset}`);
|
||
}
|
||
|
||
/**
|
||
* Print completion summary
|
||
*/
|
||
function printSummary() {
|
||
printSection('Setup Complete!');
|
||
|
||
console.log(`${colors.green}${symbols.check} Configuration saved${colors.reset}\n`);
|
||
|
||
if (config.deploymentType === 'docker') {
|
||
console.log(`${colors.cyan}Next Steps (Docker Deployment):${colors.reset}\n`);
|
||
console.log(` 1. Start Docker services:`);
|
||
console.log(` ${colors.yellow}docker compose up -d${colors.reset}\n`);
|
||
console.log(` 2. Verify services:`);
|
||
console.log(` ${colors.yellow}docker compose ps${colors.reset}\n`);
|
||
console.log(` 3. Install OpenClaw Gateway:`);
|
||
console.log(` ${colors.yellow}curl -fsSL https://openclaw.ai/install.sh | bash${colors.reset}\n`);
|
||
console.log(` 4. Initialize Gateway:`);
|
||
console.log(` ${colors.yellow}openclaw onboard --install-daemon${colors.reset}\n`);
|
||
console.log(` 5. Copy configuration:`);
|
||
console.log(` ${colors.yellow}cp openclaw.json ~/.openclaw/openclaw.json${colors.reset}\n`);
|
||
console.log(` 6. Create agent workspaces:`);
|
||
console.log(` ${colors.yellow}./agents/deploy-agent.sh steward orchestrator${colors.reset}\n`);
|
||
} else {
|
||
console.log(`${colors.cyan}Next Steps (Non-Docker Deployment):${colors.reset}\n`);
|
||
console.log(` 1. Run the setup script:`);
|
||
console.log(` ${colors.yellow}./scripts/setup-openclaw.sh${colors.reset}\n`);
|
||
console.log(` 2. Start your database services (PostgreSQL, Redis)${colors.reset}\n`);
|
||
console.log(` 3. Verify Gateway status:`);
|
||
console.log(` ${colors.yellow}openclaw gateway status${colors.reset}\n`);
|
||
}
|
||
|
||
console.log(`${colors.cyan}Documentation:${colors.reset}`);
|
||
console.log(` - Local Deployment: ${colors.yellow}docs/deployment/LOCAL_DEPLOYMENT.md${colors.reset}`);
|
||
console.log(` - Setup Wizard: ${colors.yellow}docs/deployment/SETUP_WIZARD.md${colors.reset}`);
|
||
console.log(` - Configuration: ${colors.yellow}docs/CONFIGURATION.md${colors.reset}\n`);
|
||
|
||
console.log(`${colors.green}Happy coding with Heretek OpenClaw!${colors.reset}`);
|
||
console.log(`${colors.dim}🦞 The thought that never ends.${colors.reset}\n`);
|
||
}
|
||
|
||
/**
|
||
* Main wizard function
|
||
*/
|
||
async function main() {
|
||
try {
|
||
await stepWelcome();
|
||
await stepProviderSelection();
|
||
await stepApiKeys();
|
||
await stepDatabase();
|
||
await stepAgents();
|
||
await stepPaths();
|
||
await stepReview();
|
||
await stepWriteConfig();
|
||
printSummary();
|
||
} catch (error) {
|
||
console.error(`\n${colors.red}${symbols.error} Setup failed: ${error.message}${colors.reset}`);
|
||
process.exit(1);
|
||
} finally {
|
||
rl.close();
|
||
}
|
||
}
|
||
|
||
// Run the wizard
|
||
main();
|