mirror of
https://github.com/JohnRiceML/clawport-ui.git
synced 2026-08-14 00:47:50 +00:00
- Add OPENCLAW_GATEWAY_PORT env var support (default 18789) — auto-detected from openclaw.json during setup, all API routes use gatewayBaseUrl() helper - Redesign AI Cost Analysis as "Agent Optimizer" with action chips, structured prompts for Max plan users, and follow-up suggestions - Fix renderMarkdown to handle fenced code blocks (extract before escape, reinsert after rules) - Rewrite buildCostAnalysisPrompt: throughput-focused, structured response format, 350 word cap - Fix efficiency score using effective input (input + cache tokens) - Unified OptimizationCard with UUID resolution and responsive layout - Update all docs and in-app help to mention configurable port - Remove auto-scroll from cost analysis chat - Change insight button from "Fix" to "How to fix" Closes #9 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
345 lines
11 KiB
JavaScript
345 lines
11 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 } 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() {
|
|
// Current OpenClaw layout: ~/.openclaw/agents/main/workspace
|
|
const agentPath = join(homedir(), '.openclaw', 'agents', 'main', 'workspace')
|
|
if (existsSync(agentPath)) return agentPath
|
|
// Legacy layout: ~/.openclaw/workspace
|
|
const legacyPath = join(homedir(), '.openclaw', '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)
|
|
})
|