From a51ac3cbe006b6c685e65ae1eda9ec39f0741b5e Mon Sep 17 00:00:00 2001 From: JohnRiceML Date: Mon, 23 Mar 2026 10:36:17 -0500 Subject: [PATCH] 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 --- app/api/chat/[id]/route.ts | 11 +++-------- app/api/kanban/chat/[id]/route.ts | 10 +++------- app/api/transcribe/route.ts | 9 ++------- app/api/tts/route.ts | 9 ++------- lib/openai.ts | 20 ++++++++++++++++++++ lib/setup-detection.ts | 23 ++++++++++++++++++----- package-lock.json | 4 ++-- package.json | 2 +- scripts/setup.mjs | 14 +++++++++++++- 9 files changed, 64 insertions(+), 38 deletions(-) create mode 100644 lib/openai.ts diff --git a/app/api/chat/[id]/route.ts b/app/api/chat/[id]/route.ts index 481fc8b..89a28bc 100644 --- a/app/api/chat/[id]/route.ts +++ b/app/api/chat/[id]/route.ts @@ -3,14 +3,8 @@ export const runtime = 'nodejs' import { getAgent } from '@/lib/agents' import { validateChatMessages } from '@/lib/validation' import { hasImageContent, extractImageAttachments, buildTextPrompt, sendViaOpenClaw } from '@/lib/anthropic' -import OpenAI from 'openai' -import { gatewayBaseUrl } from '@/lib/env' - -// Route through the OpenClaw gateway — no separate API key needed -const openai = new OpenAI({ - baseURL: gatewayBaseUrl(), - apiKey: process.env.OPENCLAW_GATEWAY_TOKEN, -}) +import { getOpenAIClient } from '@/lib/openai' +import type OpenAI from 'openai' const GATEWAY_TOKEN = process.env.OPENCLAW_GATEWAY_TOKEN || '' @@ -18,6 +12,7 @@ export async function POST( request: Request, { params }: { params: Promise<{ id: string }> } ) { + const openai = getOpenAIClient() const { id } = await params const agent = await getAgent(id) diff --git a/app/api/kanban/chat/[id]/route.ts b/app/api/kanban/chat/[id]/route.ts index f8336fe..f2fb890 100644 --- a/app/api/kanban/chat/[id]/route.ts +++ b/app/api/kanban/chat/[id]/route.ts @@ -1,13 +1,8 @@ export const runtime = 'nodejs' import { getAgent } from '@/lib/agents' -import OpenAI from 'openai' -import { gatewayBaseUrl } from '@/lib/env' - -const openai = new OpenAI({ - baseURL: gatewayBaseUrl(), - apiKey: process.env.OPENCLAW_GATEWAY_TOKEN, -}) +import { getOpenAIClient } from '@/lib/openai' +import type OpenAI from 'openai' const MAX_TITLE = 500 const MAX_DESC = 5000 @@ -27,6 +22,7 @@ export async function POST( request: Request, { params }: { params: Promise<{ id: string }> } ) { + const openai = getOpenAIClient() const { id } = await params const agent = await getAgent(id) diff --git a/app/api/transcribe/route.ts b/app/api/transcribe/route.ts index b2fba72..50958f1 100644 --- a/app/api/transcribe/route.ts +++ b/app/api/transcribe/route.ts @@ -1,14 +1,9 @@ export const runtime = 'nodejs' -import OpenAI from 'openai' -import { gatewayBaseUrl } from '@/lib/env' - -const openai = new OpenAI({ - baseURL: gatewayBaseUrl(), - apiKey: process.env.OPENCLAW_GATEWAY_TOKEN, -}) +import { getOpenAIClient } from '@/lib/openai' export async function POST(request: Request) { + const openai = getOpenAIClient() let formData: FormData try { formData = await request.formData() diff --git a/app/api/tts/route.ts b/app/api/tts/route.ts index ac36bcd..76a88d9 100644 --- a/app/api/tts/route.ts +++ b/app/api/tts/route.ts @@ -1,14 +1,9 @@ export const runtime = 'nodejs' -import OpenAI from 'openai' -import { gatewayBaseUrl } from '@/lib/env' - -const openai = new OpenAI({ - baseURL: gatewayBaseUrl(), - apiKey: process.env.OPENCLAW_GATEWAY_TOKEN, -}) +import { getOpenAIClient } from '@/lib/openai' export async function POST(request: Request) { + const openai = getOpenAIClient() try { const { text, voice } = await request.json() diff --git a/lib/openai.ts b/lib/openai.ts new file mode 100644 index 0000000..63c1576 --- /dev/null +++ b/lib/openai.ts @@ -0,0 +1,20 @@ +import OpenAI from 'openai' +import { gatewayBaseUrl } from './env' + +/** + * Lazy-initialized OpenAI client routed through the OpenClaw gateway. + * + * Call inside route handlers, not at module top level, so builds don't + * fail when environment variables aren't set. + */ +let _openai: OpenAI | null = null + +export function getOpenAIClient(): OpenAI { + if (!_openai) { + _openai = new OpenAI({ + baseURL: gatewayBaseUrl(), + apiKey: process.env.OPENCLAW_GATEWAY_TOKEN || '', + }) + } + return _openai +} diff --git a/lib/setup-detection.ts b/lib/setup-detection.ts index aba020c..b0c9cc7 100644 --- a/lib/setup-detection.ts +++ b/lib/setup-detection.ts @@ -21,15 +21,28 @@ import { execSync } from 'child_process' * Detect the default workspace path. * * Checks in order: - * 1. ~/.openclaw/agents/main/workspace (current agent-scoped layout) - * 2. ~/.openclaw/workspace-main (multi-agent layout, main agent) - * 3. ~/.openclaw/workspace-* (multi-agent layout, any agent) - * 4. ~/.openclaw/workspace (legacy single-workspace layout) + * 1. ~/.openclaw/openclaw.json agents.defaults.workspace (canonical) + * 2. ~/.openclaw/agents/main/workspace (current agent-scoped layout) + * 3. ~/.openclaw/workspace-main (multi-agent layout, main agent) + * 4. ~/.openclaw/workspace-* (multi-agent layout, any agent) + * 5. ~/.openclaw/workspace (legacy single-workspace layout) */ export function detectWorkspacePath(): string | null { const base = join(homedir(), '.openclaw') - // 1. Current agent-scoped layout + // 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 diff --git a/package-lock.json b/package-lock.json index 30f870a..9144d8d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "clawport-ui", - "version": "0.8.7", + "version": "0.8.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "clawport-ui", - "version": "0.8.7", + "version": "0.8.8", "license": "MIT", "dependencies": { "@dagrejs/dagre": "^2.0.4", diff --git a/package.json b/package.json index b6d4ef0..3c41c72 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "clawport-ui", - "version": "0.8.7", + "version": "0.8.8", "description": "Open-source dashboard for managing, monitoring, and chatting with your OpenClaw AI agents.", "homepage": "https://clawport.dev", "repository": { diff --git a/scripts/setup.mjs b/scripts/setup.mjs index 6d7e5e4..6e4f91c 100644 --- a/scripts/setup.mjs +++ b/scripts/setup.mjs @@ -45,7 +45,19 @@ function exec(cmd) { function detectWorkspacePath() { const base = join(homedir(), '.openclaw') - // 1. Current agent-scoped layout + // 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