Files
JohnRiceMLandClaude Opus 4.6 a51ac3cbe0 fix: lazy OpenAI client + canonical workspace detection from openclaw.json
Move OpenAI client initialization from module top-level to a lazy
singleton (lib/openai.ts) called inside route handlers. Prevents build
errors when gateway env vars aren't set. Also adds openclaw.json
agents.defaults.workspace as the first workspace detection source,
before falling back to filesystem paths.

Inspired by PR #21 (cherry-picked the good parts, skipped the fragile
cli-utils rewrite and the agents-registry simplification that dropped
sub-agents). Bumps to v0.8.8.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 10:36:17 -05:00

376 lines
12 KiB
JavaScript

#!/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-<agentId> 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)
})