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/README.md +3 -3
- package/bin/cli.js +5 -2
- package/dist/assets/index-C7Hk4nus.css +1 -0
- package/dist/assets/index-CpVghac4.js +227 -0
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/server/routes/agentChat.js +22 -73
- package/server/routes/capabilities.js +5 -20
- package/server/routes/channel.js +6 -17
- package/server/routes/chat.js +4 -78
- package/server/routes/config.js +8 -27
- package/server/routes/files.js +3 -13
- package/server/routes/preflight.js +15 -13
- package/server/routes/validate.js +1 -9
- package/server/utils/ai-clients.js +53 -0
- package/server/utils/config.js +27 -0
- package/server/utils/paths.js +6 -0
- package/server/utils/sse.js +9 -0
- package/dist/assets/index-J39606WI.css +0 -1
- package/dist/assets/index-yzWCTaaY.js +0 -228
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-
|
|
8
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
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
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
proc
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
109
|
+
sendSse(res, 'done', 'Gateway started — your agent will be live in a few seconds');
|
|
161
110
|
} else {
|
|
162
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
57
|
+
const cfg = readOpenClawConfig();
|
|
70
58
|
|
|
71
59
|
// Backup
|
|
72
|
-
|
|
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
|
-
|
|
173
|
+
writeOpenClawConfig(cfg);
|
|
189
174
|
res.json({ ok: true });
|
|
190
175
|
} catch (err) {
|
|
191
176
|
res.status(500).json({ error: err.message });
|
package/server/routes/channel.js
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
142
|
+
backupConfig();
|
|
143
|
+
writeOpenClawConfig(cfg);
|
|
155
144
|
res.json({ ok: true });
|
|
156
145
|
} catch (err) {
|
|
157
146
|
res.status(500).json({ error: err.message });
|
package/server/routes/chat.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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 });
|
package/server/routes/config.js
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
59
|
+
const cfg = readOpenClawConfig();
|
|
76
60
|
|
|
77
61
|
// Backup first
|
|
78
|
-
|
|
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
|
-
|
|
104
|
+
writeOpenClawConfig(cfg);
|
|
124
105
|
res.json({ ok: true });
|
|
125
106
|
} catch (err) {
|
|
126
107
|
res.status(500).json({ error: err.message });
|
package/server/routes/files.js
CHANGED
|
@@ -1,21 +1,11 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
|
-
import
|
|
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 =
|
|
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
|
|
80
|
-
res.setHeader('Cache-Control', 'no-cache');
|
|
81
|
-
res.setHeader('Connection', 'keep-alive');
|
|
80
|
+
initSse(res);
|
|
82
81
|
|
|
83
|
-
|
|
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
|
-
|
|
95
|
-
|
|
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
|
-
|
|
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
|
-
|
|
108
|
+
sendSse(res, 'done', version || '(installed)');
|
|
107
109
|
} else {
|
|
108
|
-
|
|
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
|
-
|
|
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,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
|
+
}
|