openclaw-agent-builder 0.0.1

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,130 @@
1
+ import { Router } from 'express';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import os from 'os';
5
+
6
+ function expandHome(p) {
7
+ return p && p.startsWith('~') ? path.join(os.homedir(), p.slice(1)) : (p || '');
8
+ }
9
+
10
+ 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
+
26
+ router.get('/openclaw-config', (_req, res) => {
27
+ res.json(readConfig());
28
+ });
29
+
30
+ // GET /api/install-context — detect whether this is a fresh install
31
+ router.get('/install-context', (_req, res) => {
32
+ const cfg = readConfig();
33
+ const defaultWsRaw = cfg.agents?.defaults?.workspace || '~/.openclaw/workspace';
34
+ const defaultWs = expandHome(defaultWsRaw);
35
+ const soulExists = fs.existsSync(path.join(defaultWs, 'SOUL.md'));
36
+ const namedAgents = (cfg.agents?.list || []).filter(a => a.workspace);
37
+ res.json({
38
+ isFreshInstall: !soulExists,
39
+ defaultWorkspace: defaultWsRaw,
40
+ existingAgentCount: namedAgents.length,
41
+ });
42
+ });
43
+
44
+ // Resolve the actual workspace path for an agent, respecting existing config.
45
+ // Falls back to ~/.openclaw/workspace-<id> for new agents.
46
+ router.get('/resolve-workspace/:agentId', (req, res) => {
47
+ const { agentId } = req.params;
48
+ const cfg = readConfig();
49
+ const home = os.homedir();
50
+
51
+ // Check agents.list for an existing entry with explicit workspace
52
+ const existing = (cfg.agents?.list || []).find(a => a.id === agentId);
53
+ if (existing?.workspace) {
54
+ const resolved = existing.workspace.replace(/^~/, home);
55
+ return res.json({ workspace: existing.workspace, resolved, existing: true });
56
+ }
57
+
58
+ // If this agent has no explicit workspace, it inherits agents.defaults.workspace
59
+ // (the default agent pattern — workspace has no -<id> suffix)
60
+ const defaultWs = cfg.agents?.defaults?.workspace || '~/.openclaw/workspace';
61
+ const isDefaultAgent = existing && !existing.workspace;
62
+ if (isDefaultAgent) {
63
+ const resolved = defaultWs.replace(/^~/, home);
64
+ return res.json({ workspace: defaultWs, resolved, existing: true, inheritedDefault: true });
65
+ }
66
+
67
+ // New agent — use workspace-<id> convention
68
+ const workspace = `~/.openclaw/workspace-${agentId}`;
69
+ res.json({ workspace, resolved: path.join(home, `.openclaw/workspace-${agentId}`), existing: false });
70
+ });
71
+
72
+ router.patch('/openclaw-config', (req, res) => {
73
+ try {
74
+ const { agents: newAgents, bindings: newBindings, defaultAgentId, isMainAgent } = req.body;
75
+ const cfg = readConfig();
76
+
77
+ // 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
+ }
82
+
83
+ if (!isMainAgent) {
84
+ // Named agent — upsert into agents.list
85
+ if (!cfg.agents) cfg.agents = {};
86
+ if (!cfg.agents.list) cfg.agents.list = [];
87
+
88
+ if (newAgents && newAgents.list) {
89
+ for (const newAgent of newAgents.list) {
90
+ const idx = cfg.agents.list.findIndex(a => a.id === newAgent.id);
91
+ if (idx >= 0) {
92
+ if (newAgent.workspace) cfg.agents.list[idx].workspace = newAgent.workspace;
93
+ } else {
94
+ cfg.agents.list.push(newAgent);
95
+ }
96
+ }
97
+ }
98
+
99
+ // Ensure exactly one default
100
+ if (defaultAgentId) {
101
+ for (const agent of cfg.agents.list) {
102
+ if (agent.default !== undefined) delete agent.default;
103
+ }
104
+ const defAgent = cfg.agents.list.find(a => a.id === defaultAgentId);
105
+ if (defAgent) defAgent.default = true;
106
+ }
107
+ }
108
+ // isMainAgent === true: the main agent already IS the default via agents.defaults —
109
+ // no agents.list entry needed. Just handle bindings below.
110
+
111
+ // Merge bindings (top-level) — prepend new, deduplicate
112
+ if (!cfg.bindings) cfg.bindings = [];
113
+ if (newBindings) {
114
+ for (const nb of newBindings) {
115
+ const key = nb.agentId + JSON.stringify(nb.match);
116
+ const exists = cfg.bindings.some(b => b.agentId + JSON.stringify(b.match) === key);
117
+ if (!exists) {
118
+ cfg.bindings.unshift(nb);
119
+ }
120
+ }
121
+ }
122
+
123
+ writeConfig(cfg);
124
+ res.json({ ok: true });
125
+ } catch (err) {
126
+ res.status(500).json({ error: err.message });
127
+ }
128
+ });
129
+
130
+ export default router;
@@ -0,0 +1,78 @@
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
+
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
+ function resolveAgentWorkspace(agentId, cfg) {
20
+ const existing = (cfg.agents?.list || []).find(a => a.id === agentId);
21
+ if (existing?.workspace) return existing.workspace;
22
+ // Agent exists but has no explicit workspace — uses agents.defaults.workspace
23
+ if (existing) return cfg.agents?.defaults?.workspace || '~/.openclaw/workspace';
24
+ // New agent
25
+ return `~/.openclaw/workspace-${agentId}`;
26
+ }
27
+
28
+ router.get('/check-paths', (req, res) => {
29
+ const agents = (req.query.agents || '').split(',').filter(Boolean);
30
+ const cfg = readOpenClawConfig();
31
+ const conflicts = agents.map(agentId => {
32
+ const ws = resolveAgentWorkspace(agentId, cfg);
33
+ const p = expandHome(ws);
34
+ return { agentId, path: p, exists: fs.existsSync(p) };
35
+ });
36
+ res.json({ conflicts });
37
+ });
38
+
39
+ router.post('/write', async (req, res) => {
40
+ const { files, basePath, force = false } = req.body;
41
+ const written = [], skipped = [], errors = [];
42
+
43
+ for (const file of files) {
44
+ try {
45
+ let filePath = file.path;
46
+ // Resolve relative to basePath or ~/.openclaw
47
+ if (!path.isAbsolute(filePath)) {
48
+ const base = basePath ? expandHome(basePath) : expandHome('~/.openclaw');
49
+ filePath = path.join(base, filePath);
50
+ } else {
51
+ filePath = expandHome(filePath);
52
+ }
53
+
54
+ const dir = path.dirname(filePath);
55
+ fs.mkdirSync(dir, { recursive: true });
56
+
57
+ if (fs.existsSync(filePath) && !force) {
58
+ skipped.push(filePath);
59
+ continue;
60
+ }
61
+
62
+ if (fs.existsSync(filePath) && force) {
63
+ const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
64
+ const bakPath = `${filePath}.bak-${ts}`;
65
+ fs.copyFileSync(filePath, bakPath);
66
+ }
67
+
68
+ fs.writeFileSync(filePath, file.content, 'utf8');
69
+ written.push(filePath);
70
+ } catch (err) {
71
+ errors.push({ path: file.path, error: err.message });
72
+ }
73
+ }
74
+
75
+ res.json({ written, skipped, errors });
76
+ });
77
+
78
+ export default router;
@@ -0,0 +1,17 @@
1
+ import { Router } from 'express';
2
+ import { generateFiles } from '../generators/index.js';
3
+
4
+ const router = Router();
5
+
6
+ router.post('/', async (req, res) => {
7
+ try {
8
+ const { teamSpec } = req.body;
9
+ if (!teamSpec) return res.status(400).json({ error: 'teamSpec required' });
10
+ const files = generateFiles(teamSpec);
11
+ res.json({ files, errors: [] });
12
+ } catch (err) {
13
+ res.status(500).json({ error: err.message, files: [], errors: [err.message] });
14
+ }
15
+ });
16
+
17
+ export default router;
@@ -0,0 +1,114 @@
1
+ import { Router } from 'express';
2
+ import { spawn, execSync } from 'child_process';
3
+ import { existsSync } from 'fs';
4
+ import os from 'os';
5
+ import path from 'path';
6
+
7
+ const router = Router();
8
+
9
+ // Build an env with extended PATH covering common node/npm install locations
10
+ function extendedEnv() {
11
+ const home = os.homedir();
12
+ const extra = [
13
+ process.env.NVM_BIN, // nvm active bin (already in PATH but be explicit)
14
+ '/usr/local/bin',
15
+ '/opt/homebrew/bin',
16
+ '/opt/homebrew/sbin',
17
+ `${home}/.npm-global/bin`,
18
+ `${home}/.volta/bin`,
19
+ `${home}/.fnm/current/bin`,
20
+ '/usr/bin',
21
+ '/bin',
22
+ ].filter(Boolean);
23
+
24
+ return {
25
+ ...process.env,
26
+ PATH: [...extra, process.env.PATH || ''].join(':'),
27
+ };
28
+ }
29
+
30
+ function tryGetVersion(env) {
31
+ // Try direct command
32
+ try {
33
+ return execSync('openclaw --version', { timeout: 5000, shell: true, env }).toString().trim();
34
+ } catch {}
35
+
36
+ // Try resolving via `which` first, then running the resolved path
37
+ try {
38
+ const bin = execSync('which openclaw', { timeout: 3000, shell: true, env }).toString().trim();
39
+ if (bin) return execSync(`"${bin}" --version`, { timeout: 5000 }).toString().trim();
40
+ } catch {}
41
+
42
+ // Try common absolute paths
43
+ const home = os.homedir();
44
+ const candidates = [
45
+ '/usr/local/bin/openclaw',
46
+ '/opt/homebrew/bin/openclaw',
47
+ `${home}/.npm-global/bin/openclaw`,
48
+ `${home}/.volta/bin/openclaw`,
49
+ ];
50
+ for (const p of candidates) {
51
+ if (existsSync(p)) {
52
+ try { return execSync(`"${p}" --version`, { timeout: 5000 }).toString().trim(); } catch {}
53
+ }
54
+ }
55
+
56
+ return null;
57
+ }
58
+
59
+ // Check if openclaw is installed and get its version
60
+ router.get('/preflight', (_req, res) => {
61
+ const env = extendedEnv();
62
+ const version = tryGetVersion(env);
63
+
64
+ if (version !== null) {
65
+ return res.json({ installed: true, version });
66
+ }
67
+
68
+ // Fallback: if ~/.openclaw/openclaw.json exists, the CLI is installed even if we can't find it in PATH
69
+ const configPath = path.join(os.homedir(), '.openclaw', 'openclaw.json');
70
+ if (existsSync(configPath)) {
71
+ return res.json({ installed: true, version: '(version unknown)' });
72
+ }
73
+
74
+ res.json({ installed: false, version: null });
75
+ });
76
+
77
+ // Run npm install -g openclaw and stream output via SSE
78
+ 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');
82
+
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');
88
+
89
+ const proc = spawn('npm', ['install', '-g', 'openclaw'], {
90
+ shell: true,
91
+ env: extendedEnv(),
92
+ });
93
+
94
+ proc.stdout.on('data', d => send('log', d.toString()));
95
+ proc.stderr.on('data', d => send('log', d.toString()));
96
+
97
+ proc.on('error', err => {
98
+ send('error', err.message);
99
+ res.end();
100
+ });
101
+
102
+ proc.on('close', code => {
103
+ if (code === 0) {
104
+ const env = extendedEnv();
105
+ const version = tryGetVersion(env);
106
+ send('done', version || '(installed)');
107
+ } else {
108
+ send('error', `Install exited with code ${code}`);
109
+ }
110
+ res.end();
111
+ });
112
+ });
113
+
114
+ export default router;
@@ -0,0 +1,30 @@
1
+ import { Router } from 'express';
2
+ import { spawn } from 'child_process';
3
+
4
+ const router = Router();
5
+
6
+ router.post('/validate', (_req, res) => {
7
+ const proc = spawn('openclaw', ['doctor'], { shell: true });
8
+ let output = '';
9
+
10
+ proc.stdout.on('data', d => { output += d.toString(); });
11
+ proc.stderr.on('data', d => { output += d.toString(); });
12
+
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
+ }
23
+ });
24
+
25
+ proc.on('close', code => {
26
+ res.json({ exitCode: code, output, passed: code === 0 });
27
+ });
28
+ });
29
+
30
+ export default router;