mirror of
https://github.com/JohnRiceML/clawport-ui.git
synced 2026-08-14 08:51:58 +00:00
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>
376 lines
12 KiB
JavaScript
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)
|
|
})
|