openclaw-agent-builder 0.0.1 → 0.0.2

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.
package/dist/index.html CHANGED
@@ -4,8 +4,8 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>OpenClaw Agent Builder</title>
7
- <script type="module" crossorigin src="/assets/index-yzWCTaaY.js"></script>
8
- <link rel="stylesheet" crossorigin href="/assets/index-J39606WI.css">
7
+ <script type="module" crossorigin src="/assets/index-CpVghac4.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/assets/index-C7Hk4nus.css">
9
9
  </head>
10
10
  <body>
11
11
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-agent-builder",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "Web-based wizard to create and deploy OpenClaw agents and multi-agent teams",
5
5
  "type": "module",
6
6
  "bin": {
@@ -3,19 +3,13 @@ import { existsSync, readFileSync } from 'fs';
3
3
  import { execSync, spawn } from 'child_process';
4
4
  import path from 'path';
5
5
  import os from 'os';
6
+ import { expandHome } from '../utils/paths.js';
7
+ import { readOpenClawConfig } from '../utils/config.js';
8
+ import { callAnthropic, callOpenAI, resolveApiKey } from '../utils/ai-clients.js';
9
+ import { initSse, sendSse } from '../utils/sse.js';
6
10
 
7
11
  const router = Router();
8
12
 
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
13
  function resolveAgentWorkspace(agentId) {
20
14
  const cfg = readOpenClawConfig();
21
15
  const existing = (cfg.agents?.list || []).find(a => a.id === agentId);
@@ -52,35 +46,6 @@ You are running in builder preview mode. You do not have access to external tool
52
46
  return parts.join('\n\n');
53
47
  }
54
48
 
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
49
  // POST /api/agent-chat/:agentId
85
50
  router.post('/agent-chat/:agentId', async (req, res) => {
86
51
  const { agentId } = req.params;
@@ -90,11 +55,7 @@ router.post('/agent-chat/:agentId', async (req, res) => {
90
55
  const system = buildSystemPrompt(agentId, workspace);
91
56
 
92
57
  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
- }
58
+ const apiKey = resolveApiKey(actualProvider, clientApiKey);
98
59
  if (!apiKey) return res.status(400).json({ error: 'No API key available' });
99
60
 
100
61
  try {
@@ -112,13 +73,7 @@ router.post('/agent-chat/:agentId', async (req, res) => {
112
73
 
113
74
  // POST /api/gateway/restart — restart the OpenClaw gateway via SSE stream
114
75
  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
- }
76
+ initSse(res);
122
77
 
123
78
  const home = os.homedir();
124
79
  const env = {
@@ -132,34 +87,28 @@ router.post('/gateway/restart', (req, res) => {
132
87
  ].filter(Boolean).join(':'),
133
88
  };
134
89
 
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
- }
90
+ // Kill any existing gateway process then start a fresh one in the background.
91
+ // Running via sh -c '...' so the shell exits immediately after forking openclaw.
92
+ sendSse(res, 'log', 'Starting OpenClaw gateway...\n');
93
+
94
+ const proc = spawn('sh', ['-c', 'pkill -f "openclaw gateway" 2>/dev/null; sleep 0.5; openclaw gateway &'], {
95
+ env,
96
+ shell: false,
97
+ });
98
+
99
+ proc.stdout.on('data', d => sendSse(res, 'log', d.toString()));
100
+ proc.stderr.on('data', d => sendSse(res, 'log', d.toString()));
101
+
102
+ proc.on('error', (err) => {
103
+ sendSse(res, 'error', err.message);
155
104
  res.end();
156
105
  });
157
106
 
158
107
  proc.on('close', code => {
159
108
  if (code === 0) {
160
- send('done', 'Gateway restarted — your new agent is live');
109
+ sendSse(res, 'done', 'Gateway started — your agent will be live in a few seconds');
161
110
  } else {
162
- send('error', `Exited with code ${code}. Try running: openclaw restart`);
111
+ sendSse(res, 'error', `Could not start automatically (exit ${code}). Run this in your terminal: openclaw gateway`);
163
112
  }
164
113
  res.end();
165
114
  });
@@ -1,23 +1,11 @@
1
1
  import { Router } from 'express';
2
- import fs from 'fs';
3
- import path from 'path';
4
- import os from 'os';
2
+ import { CONFIG_PATH, readOpenClawConfig, writeOpenClawConfig, backupConfig } from '../utils/config.js';
5
3
 
6
4
  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
5
 
18
6
  // GET /api/capabilities — return current capabilities state from openclaw.json
19
7
  router.get('/capabilities', (_req, res) => {
20
- const cfg = readConfig();
8
+ const cfg = readOpenClawConfig();
21
9
 
22
10
  const memoryPlugin = cfg.plugins?.slots?.memory || null;
23
11
  const lancedb = cfg.plugins?.entries?.['memory-lancedb'] || {};
@@ -66,13 +54,10 @@ router.get('/capabilities', (_req, res) => {
66
54
  router.post('/capabilities', (req, res) => {
67
55
  try {
68
56
  const { memory, webSearch, webFetch, homeAssistant, googlePlaces } = req.body;
69
- const cfg = readConfig();
57
+ const cfg = readOpenClawConfig();
70
58
 
71
59
  // 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
- }
60
+ backupConfig();
76
61
 
77
62
  // ── Memory ──────────────────────────────────────────────────────────────
78
63
  if (memory) {
@@ -185,7 +170,7 @@ router.post('/capabilities', (req, res) => {
185
170
  }
186
171
  }
187
172
 
188
- writeConfig(cfg);
173
+ writeOpenClawConfig(cfg);
189
174
  res.json({ ok: true });
190
175
  } catch (err) {
191
176
  res.status(500).json({ error: err.message });
@@ -1,26 +1,14 @@
1
1
  import { Router } from 'express';
2
- import { readFileSync, writeFileSync } from 'fs';
3
2
  import { join } from 'path';
4
3
  import { homedir } from 'os';
4
+ import { CONFIG_PATH, readOpenClawConfig, writeOpenClawConfig, backupConfig } from '../utils/config.js';
5
5
 
6
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
7
 
20
8
  // Return existing agents (for the agent-comms picker)
21
9
  router.get('/channel/agents', (req, res) => {
22
10
  try {
23
- const cfg = readConfig();
11
+ const cfg = readOpenClawConfig();
24
12
  const agents = (cfg.agents?.list || [])
25
13
  .filter(a => a.id)
26
14
  .map(a => ({
@@ -37,7 +25,7 @@ router.get('/channel/agents', (req, res) => {
37
25
  // Return whether telegram is already configured
38
26
  router.get('/channel/telegram-status', (req, res) => {
39
27
  try {
40
- const cfg = readConfig();
28
+ const cfg = readOpenClawConfig();
41
29
  const tg = cfg.channels?.telegram;
42
30
  res.json({
43
31
  configured: !!(tg?.botToken),
@@ -54,7 +42,7 @@ router.post('/channel/setup', (req, res) => {
54
42
  if (!agentId) return res.status(400).json({ error: 'agentId required' });
55
43
 
56
44
  try {
57
- const cfg = readConfig();
45
+ const cfg = readOpenClawConfig();
58
46
  cfg.agents = cfg.agents || {};
59
47
  cfg.agents.list = cfg.agents.list || [];
60
48
  cfg.bindings = cfg.bindings || [];
@@ -151,7 +139,8 @@ router.post('/channel/setup', (req, res) => {
151
139
  }
152
140
  }
153
141
 
154
- writeConfig(cfg);
142
+ backupConfig();
143
+ writeOpenClawConfig(cfg);
155
144
  res.json({ ok: true });
156
145
  } catch (err) {
157
146
  res.status(500).json({ error: err.message });
@@ -1,74 +1,12 @@
1
1
  import { Router } from 'express';
2
+ import { detectProvider, normalizeModel, callAnthropic, callOpenAI, resolveApiKey } from '../utils/ai-clients.js';
3
+ import { readOpenClawConfig } from '../utils/config.js';
2
4
 
3
5
  const router = Router();
4
6
 
5
7
  const ANTHROPIC_MODELS = ['claude-sonnet-4-6', 'claude-opus-4-6', 'claude-haiku-4-5'];
6
8
  const OPENAI_MODELS = ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo'];
7
9
 
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
10
  router.post('/chat', async (req, res) => {
73
11
  const { messages, apiKey: clientApiKey, model, provider: explicitProvider, system } = req.body;
74
12
 
@@ -83,11 +21,7 @@ router.post('/chat', async (req, res) => {
83
21
 
84
22
  // Resolve API key: use client-supplied key, or fall back to environment variables
85
23
  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
- }
24
+ const apiKey = resolveApiKey(actualProvider, clientApiKey);
91
25
 
92
26
  if (!apiKey) {
93
27
  return res.status(400).json({ error: 'No API key available. Set ANTHROPIC_API_KEY in your environment, or enter a key manually.' });
@@ -111,10 +45,6 @@ router.post('/chat', async (req, res) => {
111
45
  // Check whether the server has credentials available for "OpenClaw default" mode
112
46
  router.get('/chat/openclaw-status', async (req, res) => {
113
47
  try {
114
- const { readFileSync } = await import('fs');
115
- const { join } = await import('path');
116
- const { homedir } = await import('os');
117
-
118
48
  const anthropicKey = process.env.ANTHROPIC_API_KEY;
119
49
  const openaiKey = process.env.OPENAI_API_KEY;
120
50
 
@@ -134,11 +64,7 @@ router.get('/chat/openclaw-status', async (req, res) => {
134
64
  // Legacy endpoint kept for compatibility
135
65
  router.get('/chat/default-model', async (req, res) => {
136
66
  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'));
67
+ const cfg = readOpenClawConfig();
142
68
  const primary = cfg?.agents?.defaults?.model?.primary || null;
143
69
  const provider = primary ? detectProvider(primary) : null;
144
70
  res.json({ model: primary, provider, normalized: primary ? normalizeModel(primary) : null });
@@ -2,34 +2,18 @@ import { Router } from 'express';
2
2
  import fs from 'fs';
3
3
  import path from 'path';
4
4
  import os from 'os';
5
-
6
- function expandHome(p) {
7
- return p && p.startsWith('~') ? path.join(os.homedir(), p.slice(1)) : (p || '');
8
- }
5
+ import { expandHome } from '../utils/paths.js';
6
+ import { CONFIG_PATH, readOpenClawConfig, writeOpenClawConfig, backupConfig, backupTimestamp } from '../utils/config.js';
9
7
 
10
8
  const router = Router();
11
- const CONFIG_PATH = path.join(os.homedir(), '.openclaw', 'openclaw.json');
12
-
13
- function readConfig() {
14
- try {
15
- return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
16
- } catch {
17
- return {};
18
- }
19
- }
20
-
21
- function writeConfig(cfg) {
22
- fs.mkdirSync(path.dirname(CONFIG_PATH), { recursive: true });
23
- fs.writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2), 'utf8');
24
- }
25
9
 
26
10
  router.get('/openclaw-config', (_req, res) => {
27
- res.json(readConfig());
11
+ res.json(readOpenClawConfig());
28
12
  });
29
13
 
30
14
  // GET /api/install-context — detect whether this is a fresh install
31
15
  router.get('/install-context', (_req, res) => {
32
- const cfg = readConfig();
16
+ const cfg = readOpenClawConfig();
33
17
  const defaultWsRaw = cfg.agents?.defaults?.workspace || '~/.openclaw/workspace';
34
18
  const defaultWs = expandHome(defaultWsRaw);
35
19
  const soulExists = fs.existsSync(path.join(defaultWs, 'SOUL.md'));
@@ -45,7 +29,7 @@ router.get('/install-context', (_req, res) => {
45
29
  // Falls back to ~/.openclaw/workspace-<id> for new agents.
46
30
  router.get('/resolve-workspace/:agentId', (req, res) => {
47
31
  const { agentId } = req.params;
48
- const cfg = readConfig();
32
+ const cfg = readOpenClawConfig();
49
33
  const home = os.homedir();
50
34
 
51
35
  // Check agents.list for an existing entry with explicit workspace
@@ -72,13 +56,10 @@ router.get('/resolve-workspace/:agentId', (req, res) => {
72
56
  router.patch('/openclaw-config', (req, res) => {
73
57
  try {
74
58
  const { agents: newAgents, bindings: newBindings, defaultAgentId, isMainAgent } = req.body;
75
- const cfg = readConfig();
59
+ const cfg = readOpenClawConfig();
76
60
 
77
61
  // Backup first
78
- if (fs.existsSync(CONFIG_PATH)) {
79
- const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
80
- fs.copyFileSync(CONFIG_PATH, `${CONFIG_PATH}.bak-${ts}`);
81
- }
62
+ backupConfig();
82
63
 
83
64
  if (!isMainAgent) {
84
65
  // Named agent — upsert into agents.list
@@ -120,7 +101,7 @@ router.patch('/openclaw-config', (req, res) => {
120
101
  }
121
102
  }
122
103
 
123
- writeConfig(cfg);
104
+ writeOpenClawConfig(cfg);
124
105
  res.json({ ok: true });
125
106
  } catch (err) {
126
107
  res.status(500).json({ error: err.message });
@@ -1,21 +1,11 @@
1
1
  import { Router } from 'express';
2
2
  import fs from 'fs';
3
3
  import path from 'path';
4
- import os from 'os';
4
+ import { expandHome } from '../utils/paths.js';
5
+ import { readOpenClawConfig, backupTimestamp } from '../utils/config.js';
5
6
 
6
7
  const router = Router();
7
8
 
8
- function expandHome(p) {
9
- if (p.startsWith('~')) return path.join(os.homedir(), p.slice(1));
10
- return p;
11
- }
12
-
13
- function readOpenClawConfig() {
14
- try {
15
- return JSON.parse(fs.readFileSync(path.join(os.homedir(), '.openclaw', 'openclaw.json'), 'utf8'));
16
- } catch { return {}; }
17
- }
18
-
19
9
  function resolveAgentWorkspace(agentId, cfg) {
20
10
  const existing = (cfg.agents?.list || []).find(a => a.id === agentId);
21
11
  if (existing?.workspace) return existing.workspace;
@@ -60,7 +50,7 @@ router.post('/write', async (req, res) => {
60
50
  }
61
51
 
62
52
  if (fs.existsSync(filePath) && force) {
63
- const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
53
+ const ts = backupTimestamp();
64
54
  const bakPath = `${filePath}.bak-${ts}`;
65
55
  fs.copyFileSync(filePath, bakPath);
66
56
  }
@@ -3,6 +3,7 @@ import { spawn, execSync } from 'child_process';
3
3
  import { existsSync } from 'fs';
4
4
  import os from 'os';
5
5
  import path from 'path';
6
+ import { initSse, sendSse } from '../utils/sse.js';
6
7
 
7
8
  const router = Router();
8
9
 
@@ -76,26 +77,27 @@ router.get('/preflight', (_req, res) => {
76
77
 
77
78
  // Run npm install -g openclaw and stream output via SSE
78
79
  router.post('/preflight/install', (req, res) => {
79
- res.setHeader('Content-Type', 'text/event-stream');
80
- res.setHeader('Cache-Control', 'no-cache');
81
- res.setHeader('Connection', 'keep-alive');
80
+ initSse(res);
82
81
 
83
- function send(type, data) {
84
- res.write(`data: ${JSON.stringify({ type, data })}\n\n`);
85
- }
86
-
87
- send('log', 'Running: npm install -g openclaw\n');
82
+ sendSse(res, 'log', 'Running: npm install -g openclaw\n');
88
83
 
89
84
  const proc = spawn('npm', ['install', '-g', 'openclaw'], {
90
85
  shell: true,
91
86
  env: extendedEnv(),
92
87
  });
93
88
 
94
- proc.stdout.on('data', d => send('log', d.toString()));
95
- proc.stderr.on('data', d => send('log', d.toString()));
89
+ function filterLog(raw) {
90
+ // Strip npm deprecation warnings — they're noise from transitive deps, not errors
91
+ return raw.split('\n')
92
+ .filter(line => !line.includes('npm warn deprecated'))
93
+ .join('\n');
94
+ }
95
+
96
+ proc.stdout.on('data', d => sendSse(res, 'log', filterLog(d.toString())));
97
+ proc.stderr.on('data', d => sendSse(res, 'log', filterLog(d.toString())));
96
98
 
97
99
  proc.on('error', err => {
98
- send('error', err.message);
100
+ sendSse(res, 'error', err.message);
99
101
  res.end();
100
102
  });
101
103
 
@@ -103,9 +105,9 @@ router.post('/preflight/install', (req, res) => {
103
105
  if (code === 0) {
104
106
  const env = extendedEnv();
105
107
  const version = tryGetVersion(env);
106
- send('done', version || '(installed)');
108
+ sendSse(res, 'done', version || '(installed)');
107
109
  } else {
108
- send('error', `Install exited with code ${code}`);
110
+ sendSse(res, 'error', `Install exited with code ${code}`);
109
111
  }
110
112
  res.end();
111
113
  });
@@ -11,15 +11,7 @@ router.post('/validate', (_req, res) => {
11
11
  proc.stderr.on('data', d => { output += d.toString(); });
12
12
 
13
13
  proc.on('error', err => {
14
- if (err.code === 'ENOENT') {
15
- res.json({
16
- exitCode: 127,
17
- output: 'openclaw not found in PATH. Install from https://openclaw.ai',
18
- passed: false,
19
- });
20
- } else {
21
- res.json({ exitCode: 1, output: err.message, passed: false });
22
- }
14
+ res.json({ exitCode: 1, output: err.message, passed: false });
23
15
  });
24
16
 
25
17
  proc.on('close', code => {
@@ -0,0 +1,53 @@
1
+ const ANTHROPIC_VERSION = '2023-06-01';
2
+ const DEFAULT_MAX_TOKENS = 1024;
3
+
4
+ export function detectProvider(model) {
5
+ if (!model) return null;
6
+ const m = model.toLowerCase();
7
+ if (m.includes('claude')) return 'anthropic';
8
+ if (m.includes('gpt') || m.includes('o1') || m.includes('o3')) return 'openai';
9
+ if (m.includes('anthropic/')) return 'anthropic';
10
+ if (m.includes('openai')) return 'openai';
11
+ return null;
12
+ }
13
+
14
+ export function normalizeModel(model) {
15
+ if (model && model.includes('/')) return model.split('/').pop();
16
+ return model;
17
+ }
18
+
19
+ export function resolveApiKey(provider, clientKey) {
20
+ if (clientKey) return clientKey;
21
+ if (provider === 'anthropic') return process.env.ANTHROPIC_API_KEY || null;
22
+ if (provider === 'openai') return process.env.OPENAI_API_KEY || null;
23
+ return null;
24
+ }
25
+
26
+ export async function callAnthropic({ apiKey, model, messages, system, maxTokens = DEFAULT_MAX_TOKENS }) {
27
+ const body = { model: normalizeModel(model), max_tokens: maxTokens, messages };
28
+ if (system) body.system = system;
29
+ const res = await fetch('https://api.anthropic.com/v1/messages', {
30
+ method: 'POST',
31
+ headers: {
32
+ 'x-api-key': apiKey,
33
+ 'anthropic-version': ANTHROPIC_VERSION,
34
+ 'content-type': 'application/json',
35
+ },
36
+ body: JSON.stringify(body),
37
+ });
38
+ const data = await res.json();
39
+ if (!res.ok) throw new Error(data.error?.message || `Anthropic error ${res.status}`);
40
+ return data.content?.[0]?.text || '';
41
+ }
42
+
43
+ export async function callOpenAI({ apiKey, model, messages, system, maxTokens = DEFAULT_MAX_TOKENS }) {
44
+ const openaiMessages = system ? [{ role: 'system', content: system }, ...messages] : messages;
45
+ const res = await fetch('https://api.openai.com/v1/chat/completions', {
46
+ method: 'POST',
47
+ headers: { 'Authorization': `Bearer ${apiKey}`, 'content-type': 'application/json' },
48
+ body: JSON.stringify({ model: normalizeModel(model), max_tokens: maxTokens, messages: openaiMessages }),
49
+ });
50
+ const data = await res.json();
51
+ if (!res.ok) throw new Error(data.error?.message || `OpenAI error ${res.status}`);
52
+ return data.choices?.[0]?.message?.content || '';
53
+ }
@@ -0,0 +1,27 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+
5
+ export const CONFIG_PATH = path.join(os.homedir(), '.openclaw', 'openclaw.json');
6
+
7
+ export function backupTimestamp() {
8
+ return new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
9
+ }
10
+
11
+ export function readOpenClawConfig() {
12
+ try {
13
+ return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
14
+ } catch { return {}; }
15
+ }
16
+
17
+ export function writeOpenClawConfig(cfg) {
18
+ fs.mkdirSync(path.dirname(CONFIG_PATH), { recursive: true });
19
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2), 'utf8');
20
+ }
21
+
22
+ export function backupConfig() {
23
+ if (fs.existsSync(CONFIG_PATH)) {
24
+ const ts = backupTimestamp();
25
+ fs.copyFileSync(CONFIG_PATH, `${CONFIG_PATH}.bak-${ts}`);
26
+ }
27
+ }
@@ -0,0 +1,6 @@
1
+ import path from 'path';
2
+ import os from 'os';
3
+
4
+ export function expandHome(p) {
5
+ return p && p.startsWith('~') ? path.join(os.homedir(), p.slice(1)) : (p || '');
6
+ }
@@ -0,0 +1,9 @@
1
+ export function initSse(res) {
2
+ res.setHeader('Content-Type', 'text/event-stream');
3
+ res.setHeader('Cache-Control', 'no-cache');
4
+ res.setHeader('Connection', 'keep-alive');
5
+ }
6
+
7
+ export function sendSse(res, type, data) {
8
+ res.write(`data: ${JSON.stringify({ type, data })}\n\n`);
9
+ }