#!/usr/bin/env node // ClawPort -- Auto-detect environment and write .env.local // Usage: npm run setup import { execSync } from 'node:child_process' import { readFileSync, writeFileSync, existsSync, mkdirSync, accessSync, readdirSync } from 'node:fs' import { constants } from 'node:fs' import { resolve, join } from 'node:path' import { createInterface } from 'node:readline' import { homedir } from 'node:os' // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- const green = (s) => `\x1b[32m${s}\x1b[0m` const yellow = (s) => `\x1b[33m${s}\x1b[0m` const red = (s) => `\x1b[31m${s}\x1b[0m` const dim = (s) => `\x1b[2m${s}\x1b[0m` const bold = (s) => `\x1b[1m${s}\x1b[0m` function ask(question) { const rl = createInterface({ input: process.stdin, output: process.stdout }) return new Promise((resolve) => { rl.question(question, (answer) => { rl.close() resolve(answer.trim()) }) }) } function exec(cmd) { try { return execSync(cmd, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim() } catch { return null } } // --------------------------------------------------------------------------- // Detectors // --------------------------------------------------------------------------- function detectWorkspacePath() { const base = join(homedir(), '.openclaw') // 1. Read from openclaw.json (canonical source) const configPath = join(base, 'openclaw.json') if (existsSync(configPath)) { try { const config = JSON.parse(readFileSync(configPath, 'utf-8')) const ws = config?.agents?.defaults?.workspace if (typeof ws === 'string' && existsSync(ws)) return ws } catch { // Invalid JSON, fall through } } // 2. Current agent-scoped layout const agentPath = join(base, 'agents', 'main', 'workspace') if (existsSync(agentPath)) return agentPath // 2. Multi-agent layout: workspace-main const multiMain = join(base, 'workspace-main') if (existsSync(multiMain)) return multiMain // 3. Multi-agent layout: first workspace- found try { const entries = readdirSync(base) const workspaceDirs = entries .filter((e) => typeof e === 'string' && e.startsWith('workspace-')) .sort() if (workspaceDirs.length > 0) return join(base, workspaceDirs[0]) } catch { // ~/.openclaw doesn't exist } // 4. Legacy single-workspace layout const legacyPath = join(base, 'workspace') if (existsSync(legacyPath)) return legacyPath return null } function detectOpenClawBin() { const cmd = process.platform === 'win32' ? 'where' : 'which' return exec(`${cmd} openclaw`) } function detectGatewayToken() { const configPath = join(homedir(), '.openclaw', 'openclaw.json') if (!existsSync(configPath)) return null try { const config = JSON.parse(readFileSync(configPath, 'utf-8')) const token = config?.gateway?.auth?.token return typeof token === 'string' ? token : null } catch { return null } } function detectGatewayPort() { const configPath = join(homedir(), '.openclaw', 'openclaw.json') if (!existsSync(configPath)) return null try { const config = JSON.parse(readFileSync(configPath, 'utf-8')) const port = config?.gateway?.http?.port return typeof port === 'number' ? port : null } catch { return null } } function checkHttpEndpointEnabled() { const configPath = join(homedir(), '.openclaw', 'openclaw.json') if (!existsSync(configPath)) return null // can't check try { const config = JSON.parse(readFileSync(configPath, 'utf-8')) return config?.gateway?.http?.endpoints?.chatCompletions?.enabled === true } catch { return null } } function enableHttpEndpoint() { const configPath = join(homedir(), '.openclaw', 'openclaw.json') if (!existsSync(configPath)) return false try { const config = JSON.parse(readFileSync(configPath, 'utf-8')) if (!config.gateway) config.gateway = {} if (!config.gateway.http) config.gateway.http = {} if (!config.gateway.http.endpoints) config.gateway.http.endpoints = {} if (!config.gateway.http.endpoints.chatCompletions) config.gateway.http.endpoints.chatCompletions = {} config.gateway.http.endpoints.chatCompletions.enabled = true writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8') return true } catch { return false } } async function checkGatewayRunning(port = 18789) { try { const res = await fetch(`http://127.0.0.1:${port}/`, { signal: AbortSignal.timeout(3000), }) return res.ok || res.status > 0 } catch { return false } } // --------------------------------------------------------------------------- // Main // --------------------------------------------------------------------------- async function main() { console.log() console.log(bold(' ClawPort Setup')) console.log(dim(' Auto-detecting your OpenClaw configuration...\n')) // Detect all values const detectedPort = detectGatewayPort() const detected = { WORKSPACE_PATH: detectWorkspacePath(), OPENCLAW_BIN: detectOpenClawBin(), OPENCLAW_GATEWAY_TOKEN: detectGatewayToken(), OPENCLAW_GATEWAY_PORT: detectedPort, } const port = detected.OPENCLAW_GATEWAY_PORT || 18789 const gatewayUp = await checkGatewayRunning(port) // Report findings const entries = [ ['WORKSPACE_PATH', detected.WORKSPACE_PATH], ['OPENCLAW_BIN', detected.OPENCLAW_BIN], ['OPENCLAW_GATEWAY_TOKEN', detected.OPENCLAW_GATEWAY_TOKEN], ] let allFound = true for (const [name, value] of entries) { if (value) { const display = name === 'OPENCLAW_GATEWAY_TOKEN' ? value.slice(0, 8) + '...' + value.slice(-4) : value console.log(` ${green('+')} ${bold(name)}`) console.log(` ${dim(display)}`) } else { allFound = false console.log(` ${red('x')} ${bold(name)}`) console.log(` ${red('Not found')}`) } } // Port detection if (detectedPort && detectedPort !== 18789) { console.log(` ${green('+')} ${bold('OPENCLAW_GATEWAY_PORT')}`) console.log(` ${dim(String(detectedPort))} ${dim('(custom)')}`) } else if (detectedPort) { console.log(` ${green('+')} ${bold('OPENCLAW_GATEWAY_PORT')}`) console.log(` ${dim('18789 (default)')}`) } // Gateway status console.log() if (gatewayUp) { console.log(` ${green('+')} Gateway running at ${dim(`localhost:${port}`)}`) } else { console.log(` ${yellow('!')} Gateway not responding at localhost:${port}`) console.log(` ${dim('Start it with: openclaw gateway run')}`) } // Check HTTP chat completions endpoint const httpEnabled = checkHttpEndpointEnabled() if (httpEnabled === true) { console.log(` ${green('+')} HTTP chat completions endpoint ${dim('enabled')}`) } else if (httpEnabled === false) { console.log(` ${yellow('!')} HTTP chat completions endpoint is ${bold('disabled')}`) console.log(` ${dim('ClawPort needs this to chat with agents.')}`) const enable = await ask(` ${yellow('?')} Enable it in openclaw.json? (Y/n) `) if (enable.toLowerCase() !== 'n') { if (enableHttpEndpoint()) { console.log(` ${green('+')} Enabled! ${dim('Restart the gateway for this to take effect.')}`) } else { console.log(` ${red('x')} Could not update openclaw.json. Enable it manually:`) console.log(` ${dim('Set gateway.http.endpoints.chatCompletions.enabled = true in ~/.openclaw/openclaw.json')}`) } } } console.log() // Handle missing values const final = { ...detected } if (!final.WORKSPACE_PATH) { const answer = await ask(` ${yellow('?')} Enter your WORKSPACE_PATH: `) if (answer && existsSync(answer)) { final.WORKSPACE_PATH = answer } else if (answer) { console.log(` ${yellow('Warning: path does not exist yet')}`) final.WORKSPACE_PATH = answer } else { console.log(`\n ${red('Aborted.')} WORKSPACE_PATH is required.`) process.exit(1) } } if (!final.OPENCLAW_BIN) { const answer = await ask(` ${yellow('?')} Enter path to openclaw binary: `) if (answer) { final.OPENCLAW_BIN = answer } else { console.log(`\n ${red('Aborted.')} OPENCLAW_BIN is required.`) process.exit(1) } } if (!final.OPENCLAW_GATEWAY_TOKEN) { console.log(` ${dim('Find your token in ~/.openclaw/openclaw.json under gateway.auth.token')}`) const answer = await ask(` ${yellow('?')} Enter your gateway token: `) if (answer) { final.OPENCLAW_GATEWAY_TOKEN = answer } else { console.log(`\n ${red('Aborted.')} OPENCLAW_GATEWAY_TOKEN is required.`) process.exit(1) } } // Gateway port — auto-detected or prompt if (!final.OPENCLAW_GATEWAY_PORT) { const portAnswer = await ask(` ${yellow('?')} Gateway port ${dim('(Enter for default 18789)')}: `) if (portAnswer && portAnswer !== '18789') { final.OPENCLAW_GATEWAY_PORT = parseInt(portAnswer, 10) } } // Support --cwd flag for CLI usage (clawport setup writes .env.local into the package dir) const cwdFlag = process.argv.find((a) => a.startsWith('--cwd=')) let targetDir = cwdFlag ? cwdFlag.split('=')[1] : process.cwd() // When installed globally (e.g. /usr/lib/node_modules/clawport-ui), targetDir may not be writable. // Use ~/.config/clawport-ui/.env.local in that case so setup works without sudo. function canWriteToDir(dir) { try { accessSync(dir, constants.W_OK) return true } catch { return false } } if (!canWriteToDir(targetDir)) { const userConfigDir = join(homedir(), '.config', 'clawport-ui') if (!existsSync(userConfigDir)) { mkdirSync(userConfigDir, { recursive: true }) } targetDir = userConfigDir console.log(` ${yellow('!')} Package directory is not writable; using ${dim(targetDir)} for .env.local`) console.log() } // Check if .env.local already exists const envPath = resolve(targetDir, '.env.local') if (existsSync(envPath)) { const overwrite = await ask(` ${yellow('?')} .env.local already exists. Overwrite? (y/N) `) if (overwrite.toLowerCase() !== 'y') { console.log(`\n ${dim('Keeping existing .env.local')}`) process.exit(0) } } // Confirm console.log() console.log(dim(' Will write .env.local with:')) console.log(` WORKSPACE_PATH=${dim(final.WORKSPACE_PATH)}`) console.log(` OPENCLAW_BIN=${dim(final.OPENCLAW_BIN)}`) console.log(` OPENCLAW_GATEWAY_TOKEN=${dim(final.OPENCLAW_GATEWAY_TOKEN.slice(0, 8) + '...')}`) if (final.OPENCLAW_GATEWAY_PORT && final.OPENCLAW_GATEWAY_PORT !== 18789) { console.log(` OPENCLAW_GATEWAY_PORT=${dim(String(final.OPENCLAW_GATEWAY_PORT))}`) } console.log() const confirm = await ask(` ${bold('Write .env.local?')} (Y/n) `) if (confirm.toLowerCase() === 'n') { console.log(`\n ${dim('Aborted.')}`) process.exit(0) } // Write const lines = [ '# ClawPort -- generated by npm run setup', `# Created: ${new Date().toISOString()}`, '', '# Required', `WORKSPACE_PATH=${final.WORKSPACE_PATH}`, `OPENCLAW_BIN=${final.OPENCLAW_BIN}`, `OPENCLAW_GATEWAY_TOKEN=${final.OPENCLAW_GATEWAY_TOKEN}`, '', ] // Only write port if non-default if (final.OPENCLAW_GATEWAY_PORT && final.OPENCLAW_GATEWAY_PORT !== 18789) { lines.push('# Gateway port (default: 18789)') lines.push(`OPENCLAW_GATEWAY_PORT=${final.OPENCLAW_GATEWAY_PORT}`) lines.push('') } lines.push('# Optional -- uncomment to enable voice features') lines.push('# ELEVENLABS_API_KEY=') lines.push('') const content = lines.join('\n') writeFileSync(envPath, content, 'utf-8') console.log() console.log(` ${green('Done!')} .env.local written${targetDir !== (cwdFlag ? cwdFlag.split('=')[1] : process.cwd()) ? ` to ${dim(targetDir)}` : ''}.`) console.log() const startCmd = cwdFlag ? 'clawport dev' : 'npm run dev' console.log(` Next steps:`) if (!gatewayUp) { console.log(` 1. Start the gateway: ${dim('openclaw gateway run')}`) console.log(` 2. Start ClawPort: ${dim(startCmd)}`) } else { console.log(` ${dim(startCmd)}`) } console.log() } main().catch((err) => { console.error(`\n ${red('Error:')} ${err.message}`) process.exit(1) })