openclaw-agent-builder 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,168 @@
1
+ import { Router } from 'express';
2
+ import { existsSync, readFileSync } from 'fs';
3
+ import { execSync, spawn } from 'child_process';
4
+ import path from 'path';
5
+ import os from 'os';
6
+
7
+ const router = Router();
8
+
9
+ function expandHome(p) {
10
+ return p.startsWith('~') ? path.join(os.homedir(), p.slice(1)) : p;
11
+ }
12
+
13
+ function readOpenClawConfig() {
14
+ try {
15
+ return JSON.parse(readFileSync(path.join(os.homedir(), '.openclaw', 'openclaw.json'), 'utf8'));
16
+ } catch { return {}; }
17
+ }
18
+
19
+ function resolveAgentWorkspace(agentId) {
20
+ const cfg = readOpenClawConfig();
21
+ const existing = (cfg.agents?.list || []).find(a => a.id === agentId);
22
+ if (existing?.workspace) return expandHome(existing.workspace);
23
+ if (existing) return expandHome(cfg.agents?.defaults?.workspace || '~/.openclaw/workspace');
24
+ return expandHome(`~/.openclaw/workspace-${agentId}`);
25
+ }
26
+
27
+ function readWsFile(workspace, filename) {
28
+ const p = path.join(workspace, filename);
29
+ return existsSync(p) ? readFileSync(p, 'utf8').trim() : null;
30
+ }
31
+
32
+ function buildSystemPrompt(agentId, workspace) {
33
+ const soul = readWsFile(workspace, 'SOUL.md');
34
+ const agents = readWsFile(workspace, 'AGENTS.md');
35
+ const user = readWsFile(workspace, 'USER.md');
36
+ const identity = readWsFile(workspace, 'IDENTITY.md');
37
+ const tools = readWsFile(workspace, 'TOOLS.md');
38
+ const memory = readWsFile(workspace, 'MEMORY.md');
39
+
40
+ const parts = [];
41
+ if (soul) parts.push(soul);
42
+ if (identity) parts.push(`---\n${identity}`);
43
+ if (agents) parts.push(`---\n${agents}`);
44
+ if (user) parts.push(`---\n${user}`);
45
+ if (tools) parts.push(`---\n${tools}`);
46
+ if (memory) parts.push(`---\n## Long-term memory\n${memory}`);
47
+
48
+ parts.push(`---
49
+ ## Preview mode
50
+ You are running in builder preview mode. You do not have access to external tools (web, files, shell) in this preview — respond based on your personality and knowledge only. Mention this briefly if asked about tools or actions. When the OpenClaw gateway is running, you will have full capabilities.`);
51
+
52
+ return parts.join('\n\n');
53
+ }
54
+
55
+ async function callAnthropic({ apiKey, model, messages, system }) {
56
+ const res = await fetch('https://api.anthropic.com/v1/messages', {
57
+ method: 'POST',
58
+ headers: {
59
+ 'x-api-key': apiKey,
60
+ 'anthropic-version': '2023-06-01',
61
+ 'content-type': 'application/json',
62
+ },
63
+ body: JSON.stringify({ model, max_tokens: 1024, system, messages }),
64
+ });
65
+ const data = await res.json();
66
+ if (!res.ok) throw new Error(data.error?.message || `Anthropic error ${res.status}`);
67
+ return data.content[0]?.text || '';
68
+ }
69
+
70
+ async function callOpenAI({ apiKey, model, messages, system }) {
71
+ const res = await fetch('https://api.openai.com/v1/chat/completions', {
72
+ method: 'POST',
73
+ headers: { Authorization: `Bearer ${apiKey}`, 'content-type': 'application/json' },
74
+ body: JSON.stringify({
75
+ model,
76
+ messages: [{ role: 'system', content: system }, ...messages],
77
+ }),
78
+ });
79
+ const data = await res.json();
80
+ if (!res.ok) throw new Error(data.error?.message || `OpenAI error ${res.status}`);
81
+ return data.choices[0]?.message?.content || '';
82
+ }
83
+
84
+ // POST /api/agent-chat/:agentId
85
+ router.post('/agent-chat/:agentId', async (req, res) => {
86
+ const { agentId } = req.params;
87
+ const { messages, apiKey: clientApiKey, provider = 'anthropic', model } = req.body;
88
+
89
+ const workspace = resolveAgentWorkspace(agentId);
90
+ const system = buildSystemPrompt(agentId, workspace);
91
+
92
+ const actualProvider = provider === 'openclaw' ? 'anthropic' : provider;
93
+ let apiKey = clientApiKey;
94
+ if (!apiKey) {
95
+ if (actualProvider === 'anthropic') apiKey = process.env.ANTHROPIC_API_KEY;
96
+ else if (actualProvider === 'openai') apiKey = process.env.OPENAI_API_KEY;
97
+ }
98
+ if (!apiKey) return res.status(400).json({ error: 'No API key available' });
99
+
100
+ try {
101
+ let content;
102
+ if (actualProvider === 'openai') {
103
+ content = await callOpenAI({ apiKey, model: model || 'gpt-4o', messages, system });
104
+ } else {
105
+ content = await callAnthropic({ apiKey, model: model || 'claude-sonnet-4-6', messages, system });
106
+ }
107
+ res.json({ content });
108
+ } catch (err) {
109
+ res.status(500).json({ error: err.message });
110
+ }
111
+ });
112
+
113
+ // POST /api/gateway/restart — restart the OpenClaw gateway via SSE stream
114
+ router.post('/gateway/restart', (req, res) => {
115
+ res.setHeader('Content-Type', 'text/event-stream');
116
+ res.setHeader('Cache-Control', 'no-cache');
117
+ res.setHeader('Connection', 'keep-alive');
118
+
119
+ function send(type, data) {
120
+ res.write(`data: ${JSON.stringify({ type, data })}\n\n`);
121
+ }
122
+
123
+ const home = os.homedir();
124
+ const env = {
125
+ ...process.env,
126
+ PATH: [
127
+ '/usr/local/bin', '/opt/homebrew/bin',
128
+ process.env.NVM_BIN,
129
+ `${home}/.npm-global/bin`,
130
+ `${home}/.volta/bin`,
131
+ process.env.PATH || '',
132
+ ].filter(Boolean).join(':'),
133
+ };
134
+
135
+ send('log', 'Restarting OpenClaw gateway...\n');
136
+
137
+ const proc = spawn('openclaw', ['restart'], { shell: true, env });
138
+ proc.stdout.on('data', d => send('log', d.toString()));
139
+ proc.stderr.on('data', d => send('log', d.toString()));
140
+
141
+ proc.on('error', () => {
142
+ // Fallback: launchctl on macOS
143
+ send('log', 'Trying launchctl...\n');
144
+ try {
145
+ const uid = execSync('id -u', { shell: true }).toString().trim();
146
+ const out = execSync(
147
+ `launchctl kickstart -k gui/${uid}/com.openclaw.gateway`,
148
+ { shell: true, env, timeout: 10000 }
149
+ ).toString();
150
+ send('log', out);
151
+ send('done', 'Gateway restarted');
152
+ } catch (e) {
153
+ send('error', 'Could not restart automatically. Run: openclaw restart');
154
+ }
155
+ res.end();
156
+ });
157
+
158
+ proc.on('close', code => {
159
+ if (code === 0) {
160
+ send('done', 'Gateway restarted — your new agent is live');
161
+ } else {
162
+ send('error', `Exited with code ${code}. Try running: openclaw restart`);
163
+ }
164
+ res.end();
165
+ });
166
+ });
167
+
168
+ export default router;
@@ -0,0 +1,195 @@
1
+ import { Router } from 'express';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import os from 'os';
5
+
6
+ const router = Router();
7
+ const CONFIG_PATH = path.join(os.homedir(), '.openclaw', 'openclaw.json');
8
+
9
+ function readConfig() {
10
+ try { return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8')); } catch { return {}; }
11
+ }
12
+
13
+ function writeConfig(cfg) {
14
+ fs.mkdirSync(path.dirname(CONFIG_PATH), { recursive: true });
15
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2), 'utf8');
16
+ }
17
+
18
+ // GET /api/capabilities — return current capabilities state from openclaw.json
19
+ router.get('/capabilities', (_req, res) => {
20
+ const cfg = readConfig();
21
+
22
+ const memoryPlugin = cfg.plugins?.slots?.memory || null;
23
+ const lancedb = cfg.plugins?.entries?.['memory-lancedb'] || {};
24
+ const memoryCore = cfg.plugins?.entries?.['memory-core'] || {};
25
+
26
+ const webSearch = cfg.tools?.web?.search || {};
27
+ const webFetch = cfg.tools?.web?.fetch || {};
28
+ const browser = cfg.browser || {};
29
+ const haSkill = cfg.skills?.entries?.homeassistant || {};
30
+ const goPlaces = cfg.skills?.entries?.goplaces || {};
31
+
32
+ res.json({
33
+ memory: {
34
+ mode: memoryPlugin === 'memory-lancedb' ? 'lancedb'
35
+ : memoryPlugin === 'memory-core' ? 'core'
36
+ : 'none',
37
+ lancedb: {
38
+ autoRecall: lancedb.config?.autoRecall ?? true,
39
+ autoCapture: lancedb.config?.autoCapture ?? false,
40
+ embeddingApiKey: lancedb.config?.embedding?.apiKey || '',
41
+ embeddingModel: lancedb.config?.embedding?.model || 'text-embedding-3-small',
42
+ },
43
+ },
44
+ webSearch: {
45
+ enabled: !!webSearch.provider,
46
+ provider: webSearch.provider || 'brave',
47
+ braveApiKey: webSearch.apiKey || '',
48
+ perplexityApiKey: webSearch.perplexity?.apiKey || '',
49
+ },
50
+ webFetch: {
51
+ enabled: webFetch.enabled !== false && !!cfg.browser?.enabled,
52
+ },
53
+ homeAssistant: {
54
+ enabled: !!haSkill.env?.HA_TOKEN,
55
+ token: haSkill.env?.HA_TOKEN || '',
56
+ url: haSkill.env?.HA_URL || 'http://homeassistant.local:8123',
57
+ },
58
+ googlePlaces: {
59
+ enabled: !!goPlaces.apiKey,
60
+ apiKey: goPlaces.apiKey || '',
61
+ },
62
+ });
63
+ });
64
+
65
+ // POST /api/capabilities — write capabilities to openclaw.json
66
+ router.post('/capabilities', (req, res) => {
67
+ try {
68
+ const { memory, webSearch, webFetch, homeAssistant, googlePlaces } = req.body;
69
+ const cfg = readConfig();
70
+
71
+ // Backup
72
+ if (fs.existsSync(CONFIG_PATH)) {
73
+ const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
74
+ fs.copyFileSync(CONFIG_PATH, `${CONFIG_PATH}.bak-${ts}`);
75
+ }
76
+
77
+ // ── Memory ──────────────────────────────────────────────────────────────
78
+ if (memory) {
79
+ if (!cfg.plugins) cfg.plugins = {};
80
+ if (!cfg.plugins.entries) cfg.plugins.entries = {};
81
+
82
+ if (memory.mode === 'lancedb') {
83
+ cfg.plugins.slots = { ...(cfg.plugins.slots || {}), memory: 'memory-lancedb' };
84
+ cfg.plugins.entries['memory-lancedb'] = {
85
+ enabled: true,
86
+ config: {
87
+ autoRecall: memory.lancedb?.autoRecall ?? true,
88
+ autoCapture: memory.lancedb?.autoCapture ?? false,
89
+ embedding: {
90
+ apiKey: memory.lancedb?.embeddingApiKey || '',
91
+ model: memory.lancedb?.embeddingModel || 'text-embedding-3-small',
92
+ },
93
+ },
94
+ };
95
+ if (cfg.plugins.entries['memory-core']) {
96
+ cfg.plugins.entries['memory-core'].enabled = false;
97
+ }
98
+ } else if (memory.mode === 'core') {
99
+ cfg.plugins.slots = { ...(cfg.plugins.slots || {}), memory: 'memory-core' };
100
+ cfg.plugins.entries['memory-core'] = { enabled: true };
101
+ if (cfg.plugins.entries['memory-lancedb']) {
102
+ cfg.plugins.entries['memory-lancedb'].enabled = false;
103
+ }
104
+ } else {
105
+ // none — disable all memory plugins
106
+ delete cfg.plugins?.slots?.memory;
107
+ if (cfg.plugins.entries['memory-lancedb']) cfg.plugins.entries['memory-lancedb'].enabled = false;
108
+ if (cfg.plugins.entries['memory-core']) cfg.plugins.entries['memory-core'].enabled = false;
109
+ }
110
+ }
111
+
112
+ // ── Web Search ──────────────────────────────────────────────────────────
113
+ if (!cfg.tools) cfg.tools = {};
114
+ if (!cfg.tools.web) cfg.tools.web = {};
115
+
116
+ if (webSearch) {
117
+ if (!webSearch.enabled) {
118
+ delete cfg.tools.web.search;
119
+ } else if (webSearch.provider === 'brave') {
120
+ cfg.tools.web.search = {
121
+ ...(cfg.tools.web.search || {}),
122
+ provider: 'brave',
123
+ apiKey: webSearch.braveApiKey || '',
124
+ };
125
+ } else if (webSearch.provider === 'perplexity') {
126
+ cfg.tools.web.search = {
127
+ ...(cfg.tools.web.search || {}),
128
+ provider: 'perplexity',
129
+ perplexity: {
130
+ apiKey: webSearch.perplexityApiKey || '',
131
+ baseUrl: 'https://openrouter.ai/api/v1',
132
+ model: 'perplexity/sonar-pro',
133
+ },
134
+ };
135
+ }
136
+ }
137
+
138
+ // ── Web Fetch / Browser ─────────────────────────────────────────────────
139
+ if (webFetch !== undefined) {
140
+ cfg.tools.web.fetch = { enabled: !!webFetch?.enabled };
141
+ cfg.browser = { enabled: !!webFetch?.enabled, headless: true };
142
+ }
143
+
144
+ // ── Home Assistant ──────────────────────────────────────────────────────
145
+ if (homeAssistant) {
146
+ if (!cfg.skills) cfg.skills = {};
147
+ if (!cfg.skills.entries) cfg.skills.entries = {};
148
+ if (!homeAssistant.enabled) {
149
+ delete cfg.skills.entries.homeassistant;
150
+ } else {
151
+ cfg.skills.entries.homeassistant = {
152
+ env: {
153
+ HA_TOKEN: homeAssistant.token || '',
154
+ HA_URL: homeAssistant.url || 'http://homeassistant.local:8123',
155
+ },
156
+ };
157
+ }
158
+ }
159
+
160
+ // ── Google Places ───────────────────────────────────────────────────────
161
+ if (googlePlaces) {
162
+ if (!cfg.skills) cfg.skills = {};
163
+ if (!cfg.skills.entries) cfg.skills.entries = {};
164
+ if (!googlePlaces.enabled) {
165
+ delete cfg.skills.entries.goplaces;
166
+ } else {
167
+ cfg.skills.entries.goplaces = { apiKey: googlePlaces.apiKey || '' };
168
+ }
169
+ }
170
+
171
+ // ── Per-agent tool deny ─────────────────────────────────────────────────
172
+ const { agentToolDeny } = req.body;
173
+ if (agentToolDeny && typeof agentToolDeny === 'object') {
174
+ if (!cfg.agents) cfg.agents = {};
175
+ if (!cfg.agents.list) cfg.agents.list = [];
176
+ for (const [agentId, denied] of Object.entries(agentToolDeny)) {
177
+ if (!Array.isArray(denied) || denied.length === 0) continue;
178
+ const idx = cfg.agents.list.findIndex(a => a.id === agentId);
179
+ if (idx >= 0) {
180
+ cfg.agents.list[idx].tools = {
181
+ ...(cfg.agents.list[idx].tools || {}),
182
+ deny: denied,
183
+ };
184
+ }
185
+ }
186
+ }
187
+
188
+ writeConfig(cfg);
189
+ res.json({ ok: true });
190
+ } catch (err) {
191
+ res.status(500).json({ error: err.message });
192
+ }
193
+ });
194
+
195
+ export default router;
@@ -0,0 +1,161 @@
1
+ import { Router } from 'express';
2
+ import { readFileSync, writeFileSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { homedir } from 'os';
5
+
6
+ const router = Router();
7
+ const CONFIG_PATH = join(homedir(), '.openclaw', 'openclaw.json');
8
+
9
+ function readConfig() {
10
+ try { return JSON.parse(readFileSync(CONFIG_PATH, 'utf8')); }
11
+ catch { return {}; }
12
+ }
13
+
14
+ function writeConfig(cfg) {
15
+ const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
16
+ writeFileSync(`${CONFIG_PATH}.bak-${ts}`, JSON.stringify(readConfig(), null, 2));
17
+ writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2));
18
+ }
19
+
20
+ // Return existing agents (for the agent-comms picker)
21
+ router.get('/channel/agents', (req, res) => {
22
+ try {
23
+ const cfg = readConfig();
24
+ const agents = (cfg.agents?.list || [])
25
+ .filter(a => a.id)
26
+ .map(a => ({
27
+ id: a.id,
28
+ name: a.identity?.name || a.name || a.id,
29
+ emoji: a.identity?.emoji || '🤖',
30
+ }));
31
+ res.json({ agents });
32
+ } catch {
33
+ res.json({ agents: [] });
34
+ }
35
+ });
36
+
37
+ // Return whether telegram is already configured
38
+ router.get('/channel/telegram-status', (req, res) => {
39
+ try {
40
+ const cfg = readConfig();
41
+ const tg = cfg.channels?.telegram;
42
+ res.json({
43
+ configured: !!(tg?.botToken),
44
+ allowFrom: tg?.allowFrom || [],
45
+ });
46
+ } catch {
47
+ res.json({ configured: false, allowFrom: [] });
48
+ }
49
+ });
50
+
51
+ // Write channel config for a new agent
52
+ router.post('/channel/setup', (req, res) => {
53
+ const { channel, agentId, agentName, discord, telegram, agentComms = [] } = req.body;
54
+ if (!agentId) return res.status(400).json({ error: 'agentId required' });
55
+
56
+ try {
57
+ const cfg = readConfig();
58
+ cfg.agents = cfg.agents || {};
59
+ cfg.agents.list = cfg.agents.list || [];
60
+ cfg.bindings = cfg.bindings || [];
61
+ cfg.channels = cfg.channels || {};
62
+ cfg.tools = cfg.tools || {};
63
+
64
+ // ── Discord ──────────────────────────────────────────────────────────────
65
+ if (channel === 'discord' && discord?.token) {
66
+ cfg.channels.discord = cfg.channels.discord || { enabled: true, accounts: {} };
67
+ cfg.channels.discord.accounts = cfg.channels.discord.accounts || {};
68
+
69
+ const guilds = {};
70
+ if (discord.guildId) {
71
+ guilds[discord.guildId] = {
72
+ requireMention: discord.requireMention ?? false,
73
+ channels: discord.channelId ? { [discord.channelId]: {} } : {},
74
+ };
75
+ }
76
+
77
+ cfg.channels.discord.accounts[agentId] = {
78
+ token: discord.token,
79
+ allowBots: false,
80
+ groupPolicy: 'allowlist',
81
+ streaming: 'partial',
82
+ dmPolicy: 'pairing',
83
+ allowFrom: discord.allowFrom || [],
84
+ guilds,
85
+ activity: agentName || agentId,
86
+ status: 'online',
87
+ activityType: 4,
88
+ };
89
+
90
+ // Remove any existing bindings for this agent then prepend fresh ones
91
+ cfg.bindings = cfg.bindings.filter(b => b.agentId !== agentId);
92
+
93
+ if (discord.channelId && discord.guildId) {
94
+ cfg.bindings.unshift({
95
+ agentId,
96
+ match: {
97
+ channel: 'discord',
98
+ accountId: agentId,
99
+ peer: { kind: 'channel', id: discord.channelId },
100
+ },
101
+ });
102
+ }
103
+ cfg.bindings.unshift({
104
+ agentId,
105
+ match: { channel: 'discord', accountId: agentId },
106
+ });
107
+ }
108
+
109
+ // ── Telegram ─────────────────────────────────────────────────────────────
110
+ if (channel === 'telegram') {
111
+ cfg.channels.telegram = cfg.channels.telegram || {};
112
+ cfg.channels.telegram.enabled = true;
113
+ if (telegram?.token) cfg.channels.telegram.botToken = telegram.token;
114
+ if (telegram?.allowFrom?.length) {
115
+ const existing = cfg.channels.telegram.allowFrom || [];
116
+ cfg.channels.telegram.allowFrom = [...new Set([...existing, ...telegram.allowFrom])];
117
+ }
118
+ }
119
+
120
+ // ── Add / update agent entry in agents.list ───────────────────────────────
121
+ const existingIdx = cfg.agents.list.findIndex(a => a.id === agentId);
122
+ const agentEntry = {
123
+ id: agentId,
124
+ name: agentId,
125
+ workspace: join(homedir(), `.openclaw/workspace-${agentId}`),
126
+ identity: { name: agentName || agentId },
127
+ subagents: { allowAgents: agentComms },
128
+ };
129
+
130
+ if (existingIdx >= 0) {
131
+ cfg.agents.list[existingIdx] = { ...cfg.agents.list[existingIdx], ...agentEntry };
132
+ } else {
133
+ cfg.agents.list.push(agentEntry);
134
+ }
135
+
136
+ // ── tools.agentToAgent ────────────────────────────────────────────────────
137
+ cfg.tools.agentToAgent = cfg.tools.agentToAgent || { enabled: true, allow: [] };
138
+ if (!cfg.tools.agentToAgent.allow.includes(agentId)) {
139
+ cfg.tools.agentToAgent.allow.push(agentId);
140
+ }
141
+
142
+ // ── Update existing agents so they can talk back ──────────────────────────
143
+ for (const peerId of agentComms) {
144
+ const peer = cfg.agents.list.find(a => a.id === peerId);
145
+ if (peer) {
146
+ peer.subagents = peer.subagents || {};
147
+ peer.subagents.allowAgents = peer.subagents.allowAgents || [];
148
+ if (!peer.subagents.allowAgents.includes(agentId)) {
149
+ peer.subagents.allowAgents.push(agentId);
150
+ }
151
+ }
152
+ }
153
+
154
+ writeConfig(cfg);
155
+ res.json({ ok: true });
156
+ } catch (err) {
157
+ res.status(500).json({ error: err.message });
158
+ }
159
+ });
160
+
161
+ export default router;
@@ -0,0 +1,150 @@
1
+ import { Router } from 'express';
2
+
3
+ const router = Router();
4
+
5
+ const ANTHROPIC_MODELS = ['claude-sonnet-4-6', 'claude-opus-4-6', 'claude-haiku-4-5'];
6
+ const OPENAI_MODELS = ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo'];
7
+
8
+ function detectProvider(model) {
9
+ if (!model) return null;
10
+ const m = model.toLowerCase();
11
+ if (m.includes('claude')) return 'anthropic';
12
+ if (m.includes('gpt') || m.includes('o1') || m.includes('o3')) return 'openai';
13
+ if (m.includes('anthropic/')) return 'anthropic';
14
+ if (m.includes('openai')) return 'openai';
15
+ return null;
16
+ }
17
+
18
+ function normalizeModel(model) {
19
+ // Strip provider prefix: "anthropic/claude-sonnet-4-6" → "claude-sonnet-4-6"
20
+ if (model && model.includes('/')) {
21
+ return model.split('/').pop();
22
+ }
23
+ return model;
24
+ }
25
+
26
+ async function callAnthropic({ apiKey, model, messages, system, maxTokens = 1024 }) {
27
+ const body = {
28
+ model: normalizeModel(model),
29
+ max_tokens: maxTokens,
30
+ messages,
31
+ };
32
+ if (system) body.system = system;
33
+
34
+ const res = await fetch('https://api.anthropic.com/v1/messages', {
35
+ method: 'POST',
36
+ headers: {
37
+ 'x-api-key': apiKey,
38
+ 'anthropic-version': '2023-06-01',
39
+ 'content-type': 'application/json',
40
+ },
41
+ body: JSON.stringify(body),
42
+ });
43
+
44
+ const data = await res.json();
45
+ if (!res.ok) throw new Error(data.error?.message || `Anthropic error ${res.status}`);
46
+ return data.content?.[0]?.text || '';
47
+ }
48
+
49
+ async function callOpenAI({ apiKey, model, messages, system, maxTokens = 1024 }) {
50
+ const openaiMessages = system
51
+ ? [{ role: 'system', content: system }, ...messages]
52
+ : messages;
53
+
54
+ const res = await fetch('https://api.openai.com/v1/chat/completions', {
55
+ method: 'POST',
56
+ headers: {
57
+ 'Authorization': `Bearer ${apiKey}`,
58
+ 'content-type': 'application/json',
59
+ },
60
+ body: JSON.stringify({
61
+ model: normalizeModel(model),
62
+ max_tokens: maxTokens,
63
+ messages: openaiMessages,
64
+ }),
65
+ });
66
+
67
+ const data = await res.json();
68
+ if (!res.ok) throw new Error(data.error?.message || `OpenAI error ${res.status}`);
69
+ return data.choices?.[0]?.message?.content || '';
70
+ }
71
+
72
+ router.post('/chat', async (req, res) => {
73
+ const { messages, apiKey: clientApiKey, model, provider: explicitProvider, system } = req.body;
74
+
75
+ if (!messages || !model) {
76
+ return res.status(400).json({ error: 'messages and model are required' });
77
+ }
78
+
79
+ const provider = explicitProvider || detectProvider(model);
80
+ if (!provider) {
81
+ return res.status(400).json({ error: `Cannot detect provider for model: ${model}` });
82
+ }
83
+
84
+ // Resolve API key: use client-supplied key, or fall back to environment variables
85
+ const actualProvider = provider === 'openclaw' ? 'anthropic' : provider;
86
+ let apiKey = clientApiKey;
87
+ if (!apiKey) {
88
+ if (actualProvider === 'anthropic') apiKey = process.env.ANTHROPIC_API_KEY;
89
+ else if (actualProvider === 'openai') apiKey = process.env.OPENAI_API_KEY;
90
+ }
91
+
92
+ if (!apiKey) {
93
+ return res.status(400).json({ error: 'No API key available. Set ANTHROPIC_API_KEY in your environment, or enter a key manually.' });
94
+ }
95
+
96
+ try {
97
+ let content;
98
+ if (actualProvider === 'anthropic') {
99
+ content = await callAnthropic({ apiKey, model, messages, system });
100
+ } else if (actualProvider === 'openai') {
101
+ content = await callOpenAI({ apiKey, model, messages, system });
102
+ } else {
103
+ return res.status(400).json({ error: `Unsupported provider: ${actualProvider}` });
104
+ }
105
+ res.json({ content });
106
+ } catch (err) {
107
+ res.status(500).json({ error: err.message });
108
+ }
109
+ });
110
+
111
+ // Check whether the server has credentials available for "OpenClaw default" mode
112
+ router.get('/chat/openclaw-status', async (req, res) => {
113
+ try {
114
+ const { readFileSync } = await import('fs');
115
+ const { join } = await import('path');
116
+ const { homedir } = await import('os');
117
+
118
+ const anthropicKey = process.env.ANTHROPIC_API_KEY;
119
+ const openaiKey = process.env.OPENAI_API_KEY;
120
+
121
+ // Always use a capable model for the builder — env key availability determines provider
122
+ let model = null;
123
+ let provider = null;
124
+ if (anthropicKey) { model = 'claude-sonnet-4-6'; provider = 'anthropic'; }
125
+ else if (openaiKey) { model = 'gpt-4o'; provider = 'openai'; }
126
+
127
+ const available = !!(model && provider);
128
+ res.json({ available, provider, model, normalized: model ? normalizeModel(model) : null });
129
+ } catch {
130
+ res.json({ available: false, provider: null, model: null, normalized: null });
131
+ }
132
+ });
133
+
134
+ // Legacy endpoint kept for compatibility
135
+ router.get('/chat/default-model', async (req, res) => {
136
+ try {
137
+ const { readFileSync } = await import('fs');
138
+ const { join } = await import('path');
139
+ const { homedir } = await import('os');
140
+ const configPath = join(homedir(), '.openclaw', 'openclaw.json');
141
+ const cfg = JSON.parse(readFileSync(configPath, 'utf8'));
142
+ const primary = cfg?.agents?.defaults?.model?.primary || null;
143
+ const provider = primary ? detectProvider(primary) : null;
144
+ res.json({ model: primary, provider, normalized: primary ? normalizeModel(primary) : null });
145
+ } catch {
146
+ res.json({ model: null, provider: null, normalized: null });
147
+ }
148
+ });
149
+
150
+ export default router;